Metadata-Version: 2.4
Name: pdretro
Version: 0.1.1
Summary: Python wrapper for libretro cores
Author-email: Colin Politi <urboycolinthepanda@gmail.com>
License-Expression: MIT
Project-URL: homepage, https://github.com/ColinThePanda/pdretro
Project-URL: Bug_Tracker, https://github.com/ColinThePanda/pdretro/issues
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.20.0
Provides-Extra: test
Requires-Dist: pytest>=7.0.0; extra == "test"
Requires-Dist: Pillow>=9.0.0; extra == "test"
Dynamic: license-file

# pdretro

## Python Libretro Wrapper

**Headless RetroArch emulation with a clean Python API for frame-by-frame control, audio capture, and seamless game swapping**

[![Python](https://img.shields.io/pypi/pyversions/pdretro?style=flat-square&logo=python&logoColor=white)](https://pypi.org/project/pdretro/)
[![License](https://img.shields.io/github/license/yourusername/pdretro?style=flat-square)](LICENSE)

[Installation](#installation) • [Quick Start](#quick-start) • [Usage](#usage) • [Documentation](#documentation)

---

## Overview

pdretro provides a lightweight Python interface to libretro cores, enabling programmatic control of retro game emulation. Built with a pull-based architecture, it allows frame-by-frame stepping, real-time audio/video capture, and dynamic ROM swapping—all without GUI dependencies.

### Key Features

- **Pure Headless Operation**: No SDL, OpenGL, or GUI dependencies
- **Frame-Perfect Control**: Step through emulation frame-by-frame with Python generators
- **Zero-Copy Video Access**: Direct memory access to frame buffers via NumPy arrays
- **Audio Streaming**: Capture PCM16 stereo audio synchronized with video frames
- **Hot-Swappable ROMs**: Load and unload games without restarting the core
- **Save State Support**: Serialize and restore emulation state at any time
- **Multiple Input Types**: Full controller and analog stick support (4 ports)
- **Fast & Lightweight**: Minimal C wrapper around libretro with Numba-optimized pixel conversion

## Installation

### Install with pip

```bash
pip install pdretro
```

### Prerequisites

**Required:**

- Python 3.8 or higher
- NumPy 1.20.0+
- Libretro cores (`.so`/`.dll`/`.dylib` files)

**Optional:**

- Pillow (for image output in examples)
- pytest (for running tests)

### Obtaining Libretro Cores

Download precompiled cores from:

- **RetroArch Buildbot**: https://buildbot.libretro.com/nightly/
- **Platform-specific packages**: Most Linux distributions include `libretro-*` packages

**Common cores:**

- `snes9x_libretro` - SNES emulation
- `genesis_plus_gx_libretro` - Genesis/Mega Drive
- `gambatte_libretro` - Game Boy / Game Boy Color
- `mgba_libretro` - Game Boy Advance
- `nestopia_libretro` - NES

## Quick Start

```python
from pdretro import Emulator

# Initialize emulator with a core
with Emulator("cores/snes9x_libretro.so") as emu:
    # Load a ROM
    emu.load_game("roms/super_mario_world.sfc")

    # Get system information
    print(f"Core: {emu.system_info.library_name} v{emu.system_info.library_version}")
    print(f"Running at {emu.av_info.fps} FPS")

    # Run 60 frames
    for i, (video, audio) in enumerate(emu.frames):
        if i >= 60:
            break

        # Convert to RGB and process
        rgb_frame = video.to_rgb()
        print(f"Frame {i}: {rgb_frame.shape}, Audio: {audio.frames} samples")
```

### Generator-Based Workflow

```python
# Video-only generator
for video in emu.video_frames:
    frame = video.to_rgb()  # Shape: (height, width, 3)
    # Process frame...

# Audio-only generator
for audio in emu.audio_frames:
    samples = audio.data  # NumPy array (frames, 2)
    # Process audio...

# Combined generator
for video, audio in emu.frames:
    # Process both simultaneously
    pass
```

## Usage

### Basic Emulation

```python
from pdretro import Emulator

# Initialize and load
emu = Emulator("cores/snes9x_libretro.so")
emu.load_game("roms/game.sfc")

# Step through frames manually
emu.step()
video = emu.get_video_frame()
audio = emu.get_audio_frame()

# Or use generators
for video, audio in emu.frames:
    # Your processing logic
    pass

# Cleanup
emu.unload_game()
emu.shutdown()  # Or use context manager
```

### Context Manager Pattern

```python
with Emulator("cores/mgba_libretro.so") as emu:
    emu.load_game("roms/pokemon.gba")

    # Generator automatically handles cleanup
    for i, video in enumerate(emu.video_frames):
        if i >= 100:
            break
        # Process frames...
# Automatic cleanup on exit
```

### Input Control

```python
from pdretro import Emulator

with Emulator("cores/snes9x_libretro.so") as emu:
    emu.load_game("roms/game.sfc")

    # Button press (port 0, button A)
    import _ra_wrapper
    _ra_wrapper.set_button(0, 8, True)  # RA_BUTTON_A = 8

    # Step with input applied
    emu.step()

    # Release button
    _ra_wrapper.set_button(0, 8, False)

    # Clear all inputs
    _ra_wrapper.clear_input()
```

**Available buttons:**

```python
RA_BUTTON_B = 0
RA_BUTTON_Y = 1
RA_BUTTON_SELECT = 2
RA_BUTTON_START = 3
RA_BUTTON_UP = 4
RA_BUTTON_DOWN = 5
RA_BUTTON_LEFT = 6
RA_BUTTON_RIGHT = 7
RA_BUTTON_A = 8
RA_BUTTON_X = 9
RA_BUTTON_L = 10
RA_BUTTON_R = 11
```

### Analog Input

```python
# Set left analog stick on port 0
_ra_wrapper.set_analog(
    port=0,
    stick=0,  # 0=left, 1=right
    axis=0,   # 0=X, 1=Y
    value=32767  # Range: -32768 to 32767
)
```

### Save States

```python
# Get required state size
state_size = _ra_wrapper.get_state_size()

if state_size > 0:
    # Save state
    state_data = _ra_wrapper.serialize_state()

    # Run some frames...
    for _ in range(100):
        emu.step()

    # Restore state
    _ra_wrapper.unserialize_state(state_data)
```

### Multiple ROMs

```python
with Emulator("cores/snes9x_libretro.so") as emu:
    # Load first game
    emu.load_game("roms/game1.sfc")
    for i, video in enumerate(emu.video_frames):
        if i >= 60:
            break

    # Switch to second game
    emu.unload_game()
    emu.load_game("roms/game2.sfc")
    for i, video in enumerate(emu.video_frames):
        if i >= 60:
            break
```

## Documentation

### Architecture

pdretro uses a two-layer architecture:

```
┌─────────────────────────────────┐
│     Python API (emulator.py)    │  ← High-level interface
│   - Emulator class              │
│   - Generator methods           │
│   - NumPy integration           │
└─────────────────────────────────┘
              ↕
┌─────────────────────────────────┐
│   C Extension (_ra_wrapper)     │  ← Low-level wrapper
│   - Core loading/management     │
│   - Frame stepping              │
│   - Input handling              │
└─────────────────────────────────┘
              ↕
┌─────────────────────────────────┐
│      Libretro Core (.so)        │  ← Emulation engine
│   - Game logic                  │
│   - Video/audio generation      │
└─────────────────────────────────┘
```

### Data Classes

#### `SystemInfo`

Contains core metadata:

```python
@dataclass
class SystemInfo:
    library_name: str          # e.g., "Snes9x"
    library_version: str       # e.g., "1.62.3"
    valid_extensions: list[str]  # e.g., ["sfc", "smc"]
    need_fullpath: bool        # Whether core needs full ROM path
```

#### `AVInfo`

Audio/video specifications:

```python
@dataclass
class AVInfo:
    fps: float                 # Target frames per second
    sample_rate: float         # Audio sample rate (Hz)
    base_width: int           # Native video width
    base_height: int          # Native video height
    max_width: int            # Maximum width
    max_height: int           # Maximum height
    aspect_ratio: float       # Pixel aspect ratio
```

#### `VideoFrame`

Video frame data:

```python
@dataclass
class VideoFrame:
    data: np.ndarray          # Raw pixel buffer
    width: int                # Frame width
    height: int               # Frame height
    pitch: int                # Bytes per row
    format: int               # Pixel format (0/1/2)

    def to_rgb(self) -> np.ndarray:
        # Returns: (height, width, 3) uint8 array
```

**Supported pixel formats:**

- `0`: 0RGB1555 (16-bit)
- `1`: XRGB8888 (32-bit)
- `2`: RGB565 (16-bit)

#### `AudioFrame`

Audio sample data:

```python
@dataclass
class AudioFrame:
    data: np.ndarray          # Shape: (frames, 2), dtype: int16
    frames: int               # Number of stereo frames
    sample_rate: float        # Sample rate in Hz
```

### Video Frame Conversion

The `VideoFrame.to_rgb()` method efficiently converts raw pixel data to RGB:

```python
video = emu.get_video_frame()
rgb = video.to_rgb()  # NumPy array (height, width, 3)

# Use with PIL
from PIL import Image
img = Image.fromarray(rgb, 'RGB')
img.save('frame.png')

# Use with OpenCV
import cv2
bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
cv2.imwrite('frame.png', bgr)
```

### Performance Considerations

#### Frame Rate Control

pdretro operates in a pull-based model—Python controls the frame rate:

```python
import time

target_fps = emu.av_info.fps
frame_time = 1.0 / target_fps

for video, audio in emu.frames:
    start = time.time()

    # Process frame...

    # Maintain target FPS
    elapsed = time.time() - start
    if elapsed < frame_time:
        time.sleep(frame_time - elapsed)
```

#### Zero-Copy Access

Video and audio data use NumPy array views with no copying:

```python
# Efficient: view into existing buffer
video = emu.get_video_frame()
print(video.data.flags.owndata)  # False

# Efficient: direct RGB conversion
rgb = video.to_rgb()  # Uses NumPy vectorized operations
```

#### Generator Efficiency

Generators maintain minimal memory footprint:

```python
# Memory-efficient: only one frame in memory
for video in emu.video_frames:
    process(video)

# Memory-inefficient: loads all frames
frames = list(emu.video_frames)  # Don't do this!
```

### Testing

Run the test suite:

```bash
# Install test dependencies
pip install pytest pillow

# Run all tests
pytest tests/

# Run with coverage
pytest tests/ --cov=pdretro
```

**Test requirements:**

- SNES9x core: `snes9x_libretro.dll` in `cores/`
- F-Zero ROM: `f-zero.sfc` in `roms/`

## Examples

### Save Screenshots

```python
from pdretro import Emulator
from PIL import Image
import time

with Emulator("cores/snes9x_libretro.so") as emu:
    emu.load_game("roms/game.sfc")

    time.sleep(5) # Skip black start screen

    # Save screenshot
    video = emu.get_video_frame()
    rgb = video.to_rgb()
    Image.fromarray(rgb, 'RGB').save('screenshot.png')
```

### Record Video

```python
from pdretro import Emulator
import cv2
import numpy as np
import time

with Emulator("cores/snes9x_libretro.so") as emu:
    emu.load_game("roms/game.sfc")

    # Setup video writer
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    out = cv2.VideoWriter(
        'output.mp4',
        fourcc,
        emu.av_info.fps,
        (emu.av_info.base_width, emu.av_info.base_height)
    )

    # Record 300 frames (10 seconds at 30fps)
    for i, video in enumerate(emu.video_frames):
        if i >= 300:
            break

        rgb = video.to_rgb()
        bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
        out.write(bgr)
        time.sleep(1/30)

    out.release()
```

### AI Training Loop

```python
from pdretro import Emulator
import numpy as np

def train_agent():
    with Emulator("cores/snes9x_libretro.so") as emu:
        emu.load_game("roms/game.sfc")

        for episode in range(1000):
            # Reset game
            emu.reset()

            for frame_num in range(1000):
                # Get current state
                video = emu.get_video_frame()
                state = video.to_rgb()

                # Agent decides action
                action = agent.get_action(state)

                # Apply action
                _ra_wrapper.clear_input()
                if action == 0:  # Jump
                    _ra_wrapper.set_button(0, 8, True)  # A button

                # Step emulation
                emu.step()

                # Get reward and train
                reward = compute_reward(state)
                agent.train(state, action, reward)
```

### Audio Analysis

```python
from pdretro import Emulator
import numpy as np
import matplotlib.pyplot as plt

with Emulator("cores/snes9x_libretro.so") as emu:
    emu.load_game("roms/game.sfc")

    # Collect audio samples
    audio_buffer = []
    for i, audio in enumerate(emu.audio_frames):
        if i >= 60:  # 2 seconds at 30fps
            break
        audio_buffer.append(audio.data)

    # Concatenate all audio
    full_audio = np.concatenate(audio_buffer, axis=0)

    # Analyze
    left_channel = full_audio[:, 0]
    right_channel = full_audio[:, 1]

    # Plot waveform
    plt.plot(left_channel[:1000])
    plt.title('Audio Waveform')
    plt.show()
```

## Troubleshooting

### Core Loading Issues

```bash
# Error: "Failed to load core"
# Check core path and architecture (32/64-bit)

# Linux: verify with
file cores/snes9x_libretro.so
ldd cores/snes9x_libretro.so  # Check dependencies

# Windows: use Dependency Walker
```

### ROM Loading Failures

```bash
# Error: "Failed to load game"
# Verify ROM format matches core's valid_extensions

with Emulator("cores/snes9x_libretro.so") as emu:
    print(emu.system_info.valid_extensions)
    # ['sfc', 'smc', 'swc', 'fig', 'bs', 'st']
```

### Performance Issues

**Slow frame processing:**

- Use NumPy vectorized operations
- Avoid list comprehensions on large arrays
- Profile with `cProfile` to find bottlenecks

**Memory leaks:**

- Always use context managers or call `shutdown()`
- Don't hold references to `VideoFrame` objects
- Clear input state after use

### Platform-Specific Notes

**Windows:**

- Use `.dll` cores from RetroArch buildbot
- Ensure Visual C++ Redistributable is installed

**Linux:**

- Use `.so` cores
- May need to install `libgomp1` for some cores

**macOS:**

- Use `.dylib` cores
- May need to allow core in Security & Privacy settings

## Project Structure

```
pdretro/
├── src/
│   ├── pdretro/
│   │   ├── __init__.py
│   │   └── emulator.py          # High-level Python API
│   └── wrapper/
│       ├── ra_wrapper.c         # Core wrapper implementation
│       ├── ra_wrapper.h         # C API header
│       └── ra_wrapper_python.c  # CPython extension
├── tests/
│   └── test.py                  # Test suite
├── setup.py                     # Build configuration
├── pyproject.toml
└── README.md
```

## Roadmap

- [ ] Implement memory search/manipulation
- [ ] Cheat code support
- [ ] Network play hooks

## License

MIT License - Copyright (c) 2026

See [LICENSE](LICENSE) for full text.

## Links

- **PyPI**: https://pypi.org/project/pdretro/
- **GitHub**: https://github.com/ColinThePanda/pdretro
- **Issues**: https://github.com/ColinThePanda/pdretro/issues
- **Libretro**: https://www.libretro.com/

## Acknowledgments

Built on the [libretro API](https://www.libretro.com/) - a simple but powerful emulation framework enabling cross-platform emulator development.
