pdretro 0.1.2__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
pdretro/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .emulator import Emulator, SystemInfo, AVInfo, VideoFrame, AudioFrame, RetroButton, RetroAnalogStick, RetroAnalogAxis
2
+ __all__ = ["Emulator", "SystemInfo", "AVInfo", "VideoFrame", "AudioFrame", "RetroButton", "RetroAnalogStick", "RetroAnalogAxis"]
pdretro/emulator.py ADDED
@@ -0,0 +1,258 @@
1
+ import _ra_wrapper
2
+ import numpy as np
3
+ import os
4
+ from dataclasses import dataclass
5
+ from enum import IntEnum
6
+
7
+ class RetroButton(IntEnum):
8
+ """RetroArch button IDs matching libretro RETRO_DEVICE_ID_JOYPAD_*"""
9
+ B = 0
10
+ Y = 1
11
+ SELECT = 2
12
+ START = 3
13
+ UP = 4
14
+ DOWN = 5
15
+ LEFT = 6
16
+ RIGHT = 7
17
+ A = 8
18
+ X = 9
19
+ L = 10
20
+ R = 11
21
+ L2 = 12
22
+ R2 = 13
23
+ L3 = 14
24
+ R3 = 15
25
+
26
+ class RetroAnalogStick(IntEnum):
27
+ """Analog stick selection"""
28
+ LEFT = 0
29
+ RIGHT = 1
30
+
31
+ class RetroAnalogAxis(IntEnum):
32
+ """Analog axis selection"""
33
+ X = 0
34
+ Y = 1
35
+
36
+ @dataclass
37
+ class SystemInfo:
38
+ library_name: str
39
+ library_version: str
40
+ valid_extensions: list[str]
41
+ need_fullpath: bool
42
+
43
+ @classmethod
44
+ def from_dict(cls, info: dict) -> 'SystemInfo':
45
+ return cls(
46
+ library_name=info['library_name'],
47
+ library_version=info['library_version'],
48
+ valid_extensions=info['valid_extensions'].split('|'),
49
+ need_fullpath=info['need_fullpath']
50
+ )
51
+
52
+ @dataclass
53
+ class AVInfo:
54
+ fps: float
55
+ sample_rate: float
56
+ base_width: int
57
+ base_height: int
58
+ max_width: int
59
+ max_height: int
60
+ aspect_ratio: float
61
+
62
+ @classmethod
63
+ def from_dict(cls, info: dict) -> 'AVInfo':
64
+ return cls(**info)
65
+
66
+ @dataclass
67
+ class VideoFrame:
68
+ data: np.ndarray # Raw pixel data (pointer / bytes)
69
+ width: int
70
+ height: int
71
+ pitch: int
72
+ format: int
73
+
74
+ @classmethod
75
+ def from_dict(cls, frame: dict) -> 'VideoFrame':
76
+ return cls(**frame)
77
+
78
+ def to_rgb(self) -> np.ndarray:
79
+ # Allocate output array once
80
+ out = np.empty((self.height, self.width, 3), dtype=np.uint8)
81
+
82
+ if self.format == 1: # XRGB8888
83
+ raw = np.frombuffer(self.data, dtype=np.uint32).reshape(self.height, self.pitch // 4)
84
+ raw = raw[:, :self.width] # just view, no copy
85
+ out[..., 0] = (raw >> 16) & 0xFF # R
86
+ out[..., 1] = (raw >> 8) & 0xFF # G
87
+ out[..., 2] = raw & 0xFF # B
88
+
89
+ elif self.format == 0: # 0RGB1555
90
+ raw = np.frombuffer(self.data, dtype=np.uint16).reshape(self.height, self.pitch // 2)
91
+ raw = raw[:, :self.width]
92
+ out[..., 0] = ((raw >> 10) & 0x1F) << 3
93
+ out[..., 1] = ((raw >> 5) & 0x1F) << 3
94
+ out[..., 2] = (raw & 0x1F) << 3
95
+
96
+ elif self.format == 2: # RGB565
97
+ raw = np.frombuffer(self.data, dtype=np.uint16).reshape(self.height, self.pitch // 2)
98
+ raw = raw[:, :self.width]
99
+ out[..., 0] = ((raw >> 11) & 0x1F) << 3
100
+ out[..., 1] = ((raw >> 5) & 0x3F) << 2
101
+ out[..., 2] = (raw & 0x1F) << 3
102
+
103
+ else:
104
+ raise ValueError(f"Unsupported video format: {self.format}")
105
+
106
+ return out
107
+
108
+
109
+
110
+ @dataclass
111
+ class AudioFrame:
112
+ data: np.ndarray # Stereo int16 samples
113
+ frames: int
114
+ sample_rate: float
115
+
116
+ @classmethod
117
+ def from_dict(cls, audio: dict) -> 'AudioFrame':
118
+ return cls(**audio)
119
+
120
+ class Emulator:
121
+ def __init__(self, core_path: str):
122
+ self.core_path = os.path.abspath(core_path)
123
+
124
+ if not os.path.isfile(core_path):
125
+ raise FileNotFoundError(f"Core not found: {core_path}")
126
+
127
+ _ra_wrapper.init_core(self.core_path)
128
+ self._system_info = SystemInfo.from_dict(_ra_wrapper.get_system_info())
129
+ self._av_info = None
130
+ self._game_loaded = False
131
+
132
+ def __enter__(self):
133
+ return self
134
+
135
+ def __exit__(self, exc_type, exc_value, traceback):
136
+ try:
137
+ _ra_wrapper.shutdown()
138
+ except:
139
+ pass
140
+
141
+ @property
142
+ def system_info(self) -> SystemInfo:
143
+ return self._system_info
144
+
145
+ @property
146
+ def av_info(self) -> AVInfo:
147
+ if not self._game_loaded or self._av_info is None:
148
+ raise RuntimeError("No game loaded")
149
+ return self._av_info
150
+
151
+
152
+ @property
153
+ def is_game_loaded(self) -> bool:
154
+ return self._game_loaded
155
+
156
+ def load_game(self, rom_path: str):
157
+ rom_path = os.path.abspath(rom_path)
158
+
159
+ if not os.path.isfile(rom_path):
160
+ raise FileNotFoundError(f"ROM not found: {rom_path}")
161
+
162
+ _ra_wrapper.load_game(rom_path)
163
+ self._av_info = AVInfo.from_dict(_ra_wrapper.get_av_info())
164
+ self._game_loaded = True
165
+
166
+ def unload_game(self):
167
+ if self._game_loaded:
168
+ _ra_wrapper.unload_game()
169
+ self._game_loaded = False
170
+ self._av_info = None
171
+
172
+ def reset(self):
173
+ if not self._game_loaded:
174
+ raise RuntimeError("No game loaded")
175
+ _ra_wrapper.reset()
176
+
177
+ def step(self):
178
+ if not self._game_loaded:
179
+ raise RuntimeError("No game loaded")
180
+ _ra_wrapper.step()
181
+
182
+ def get_video_frame(self) -> VideoFrame:
183
+ return VideoFrame.from_dict(_ra_wrapper.get_video_frame())
184
+
185
+ def get_audio_frame(self) -> AudioFrame:
186
+ return AudioFrame.from_dict(_ra_wrapper.get_audio_frame())
187
+
188
+ def set_button(self, port: int, button: int | RetroButton, pressed: bool):
189
+ """Set button state
190
+
191
+ Args:
192
+ port: Controller port (0-3)
193
+ button: Button ID (0-15) or RetroButton enum
194
+ pressed: True if pressed, False if released
195
+
196
+ Example:
197
+ emu.set_button(0, RetroButton.A, True)
198
+ emu.set_button(0, 8, True) # Also valid
199
+ """
200
+ _ra_wrapper.set_button(port, int(button), pressed)
201
+
202
+ def set_analog(self, port: int, stick: int | RetroAnalogStick, axis: int | RetroAnalogAxis, value: int):
203
+ """Set analog stick axis value
204
+
205
+ Args:
206
+ port: Controller port (0-3)
207
+ stick: Which stick (0=left, 1=right) or RetroAnalogStick enum
208
+ axis: Which axis (0=X, 1=Y) or RetroAnalogAxis enum
209
+ value: Analog value (-32768 to 32767, 0 is center)
210
+
211
+ Example:
212
+ emu.set_analog(0, RetroAnalogStick.LEFT, RetroAnalogAxis.X, 32767)
213
+ emu.set_analog(0, 0, 0, 32767) # Also valid
214
+ """
215
+ _ra_wrapper.set_analog(port, int(stick), int(axis), value)
216
+
217
+ def clear_input(self):
218
+ """Clear all input state (release all buttons, center all sticks)"""
219
+ _ra_wrapper.clear_input()
220
+
221
+ def _frame_generator(self):
222
+ while True:
223
+ self.step()
224
+ yield self.get_video_frame(), self.get_audio_frame()
225
+
226
+ def _video_frame_generator(self):
227
+ while True:
228
+ self.step()
229
+ yield self.get_video_frame()
230
+
231
+ def _audio_frame_generator(self):
232
+ while True:
233
+ self.step()
234
+ yield self.get_audio_frame()
235
+
236
+ @property
237
+ def frames(self):
238
+ if not self._game_loaded:
239
+ raise RuntimeError("No game loaded")
240
+ return self._frame_generator()
241
+
242
+ @property
243
+ def video_frames(self):
244
+ if not self._game_loaded:
245
+ raise RuntimeError("No game loaded")
246
+ return self._video_frame_generator()
247
+
248
+ @property
249
+ def audio_frames(self):
250
+ if not self._game_loaded:
251
+ raise RuntimeError("No game loaded")
252
+ return self._audio_frame_generator()
253
+
254
+ def __del__(self):
255
+ try:
256
+ _ra_wrapper.shutdown()
257
+ except:
258
+ pass
@@ -0,0 +1,651 @@
1
+ Metadata-Version: 2.4
2
+ Name: pdretro
3
+ Version: 0.1.2
4
+ Summary: Python wrapper for libretro cores
5
+ Author-email: Colin Politi <urboycolinthepanda@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: homepage, https://github.com/ColinThePanda/pdretro
8
+ Project-URL: Bug_Tracker, https://github.com/ColinThePanda/pdretro/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: numpy>=1.20.0
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=7.0.0; extra == "test"
17
+ Requires-Dist: Pillow>=9.0.0; extra == "test"
18
+ Dynamic: license-file
19
+
20
+ # pdretro
21
+
22
+ ## Python Libretro Wrapper
23
+
24
+ **Headless RetroArch emulation with a clean Python API for frame-by-frame control, audio capture, and seamless game swapping**
25
+
26
+ [![Python](https://img.shields.io/pypi/pyversions/pdretro?style=flat-square&logo=python&logoColor=white)](https://pypi.org/project/pdretro/)
27
+ [![License](https://img.shields.io/github/license/yourusername/pdretro?style=flat-square)](LICENSE)
28
+
29
+ [Installation](#installation) • [Quick Start](#quick-start) • [Usage](#usage) • [Documentation](#documentation)
30
+
31
+ ---
32
+
33
+ ## Overview
34
+
35
+ 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.
36
+
37
+ ### Key Features
38
+
39
+ - **Pure Headless Operation**: No SDL, OpenGL, or GUI dependencies
40
+ - **Frame-Perfect Control**: Step through emulation frame-by-frame with Python generators
41
+ - **Zero-Copy Video Access**: Direct memory access to frame buffers via NumPy arrays
42
+ - **Audio Streaming**: Capture PCM16 stereo audio synchronized with video frames
43
+ - **Hot-Swappable ROMs**: Load and unload games without restarting the core
44
+ - **Save State Support**: Serialize and restore emulation state at any time
45
+ - **Multiple Input Types**: Full controller and analog stick support (4 ports)
46
+ - **Fast & Lightweight**: Minimal C wrapper around libretro with Numba-optimized pixel conversion
47
+
48
+ ## Installation
49
+
50
+ ### Install with pip
51
+
52
+ ```bash
53
+ pip install pdretro
54
+ ```
55
+
56
+ ### Prerequisites
57
+
58
+ **Required:**
59
+
60
+ - Python 3.8 or higher
61
+ - NumPy 1.20.0+
62
+ - Libretro cores (`.so`/`.dll`/`.dylib` files)
63
+
64
+ **Optional:**
65
+
66
+ - Pillow (for image output in examples)
67
+ - pytest (for running tests)
68
+
69
+ ### Obtaining Libretro Cores
70
+
71
+ Download precompiled cores from:
72
+
73
+ - **RetroArch Buildbot**: https://buildbot.libretro.com/nightly/
74
+ - **Platform-specific packages**: Most Linux distributions include `libretro-*` packages
75
+
76
+ **Common cores:**
77
+
78
+ - `snes9x_libretro` - SNES emulation
79
+ - `genesis_plus_gx_libretro` - Genesis/Mega Drive
80
+ - `gambatte_libretro` - Game Boy / Game Boy Color
81
+ - `mgba_libretro` - Game Boy Advance
82
+ - `nestopia_libretro` - NES
83
+
84
+ ## Quick Start
85
+
86
+ ```python
87
+ from pdretro import Emulator
88
+
89
+ # Initialize emulator with a core
90
+ with Emulator("cores/snes9x_libretro.so") as emu:
91
+ # Load a ROM
92
+ emu.load_game("roms/super_mario_world.sfc")
93
+
94
+ # Get system information
95
+ print(f"Core: {emu.system_info.library_name} v{emu.system_info.library_version}")
96
+ print(f"Running at {emu.av_info.fps} FPS")
97
+
98
+ # Run 60 frames
99
+ for i, (video, audio) in enumerate(emu.frames):
100
+ if i >= 60:
101
+ break
102
+
103
+ # Convert to RGB and process
104
+ rgb_frame = video.to_rgb()
105
+ print(f"Frame {i}: {rgb_frame.shape}, Audio: {audio.frames} samples")
106
+ ```
107
+
108
+ ### Generator-Based Workflow
109
+
110
+ ```python
111
+ # Video-only generator
112
+ for video in emu.video_frames:
113
+ frame = video.to_rgb() # Shape: (height, width, 3)
114
+ # Process frame...
115
+
116
+ # Audio-only generator
117
+ for audio in emu.audio_frames:
118
+ samples = audio.data # NumPy array (frames, 2)
119
+ # Process audio...
120
+
121
+ # Combined generator
122
+ for video, audio in emu.frames:
123
+ # Process both simultaneously
124
+ pass
125
+ ```
126
+
127
+ ## Usage
128
+
129
+ ### Basic Emulation
130
+
131
+ ```python
132
+ from pdretro import Emulator
133
+
134
+ # Initialize and load
135
+ emu = Emulator("cores/snes9x_libretro.so")
136
+ emu.load_game("roms/game.sfc")
137
+
138
+ # Step through frames manually
139
+ emu.step()
140
+ video = emu.get_video_frame()
141
+ audio = emu.get_audio_frame()
142
+
143
+ # Or use generators
144
+ for video, audio in emu.frames:
145
+ # Your processing logic
146
+ pass
147
+
148
+ # Cleanup
149
+ emu.unload_game()
150
+ emu.shutdown() # Or use context manager
151
+ ```
152
+
153
+ ### Context Manager Pattern
154
+
155
+ ```python
156
+ with Emulator("cores/mgba_libretro.so") as emu:
157
+ emu.load_game("roms/pokemon.gba")
158
+
159
+ # Generator automatically handles cleanup
160
+ for i, video in enumerate(emu.video_frames):
161
+ if i >= 100:
162
+ break
163
+ # Process frames...
164
+ # Automatic cleanup on exit
165
+ ```
166
+
167
+ ### Input Control
168
+
169
+ ```python
170
+ from pdretro import Emulator
171
+
172
+ with Emulator("cores/snes9x_libretro.so") as emu:
173
+ emu.load_game("roms/game.sfc")
174
+
175
+ # Button press (port 0, button A)
176
+ import _ra_wrapper
177
+ _ra_wrapper.set_button(0, 8, True) # RA_BUTTON_A = 8
178
+
179
+ # Step with input applied
180
+ emu.step()
181
+
182
+ # Release button
183
+ _ra_wrapper.set_button(0, 8, False)
184
+
185
+ # Clear all inputs
186
+ _ra_wrapper.clear_input()
187
+ ```
188
+
189
+ **Available buttons:**
190
+
191
+ ```python
192
+ RA_BUTTON_B = 0
193
+ RA_BUTTON_Y = 1
194
+ RA_BUTTON_SELECT = 2
195
+ RA_BUTTON_START = 3
196
+ RA_BUTTON_UP = 4
197
+ RA_BUTTON_DOWN = 5
198
+ RA_BUTTON_LEFT = 6
199
+ RA_BUTTON_RIGHT = 7
200
+ RA_BUTTON_A = 8
201
+ RA_BUTTON_X = 9
202
+ RA_BUTTON_L = 10
203
+ RA_BUTTON_R = 11
204
+ ```
205
+
206
+ ### Analog Input
207
+
208
+ ```python
209
+ # Set left analog stick on port 0
210
+ _ra_wrapper.set_analog(
211
+ port=0,
212
+ stick=0, # 0=left, 1=right
213
+ axis=0, # 0=X, 1=Y
214
+ value=32767 # Range: -32768 to 32767
215
+ )
216
+ ```
217
+
218
+ ### Save States
219
+
220
+ ```python
221
+ # Get required state size
222
+ state_size = _ra_wrapper.get_state_size()
223
+
224
+ if state_size > 0:
225
+ # Save state
226
+ state_data = _ra_wrapper.serialize_state()
227
+
228
+ # Run some frames...
229
+ for _ in range(100):
230
+ emu.step()
231
+
232
+ # Restore state
233
+ _ra_wrapper.unserialize_state(state_data)
234
+ ```
235
+
236
+ ### Multiple ROMs
237
+
238
+ ```python
239
+ with Emulator("cores/snes9x_libretro.so") as emu:
240
+ # Load first game
241
+ emu.load_game("roms/game1.sfc")
242
+ for i, video in enumerate(emu.video_frames):
243
+ if i >= 60:
244
+ break
245
+
246
+ # Switch to second game
247
+ emu.unload_game()
248
+ emu.load_game("roms/game2.sfc")
249
+ for i, video in enumerate(emu.video_frames):
250
+ if i >= 60:
251
+ break
252
+ ```
253
+
254
+ ## Documentation
255
+
256
+ ### Architecture
257
+
258
+ pdretro uses a two-layer architecture:
259
+
260
+ ```
261
+ ┌─────────────────────────────────┐
262
+ │ Python API (emulator.py) │ ← High-level interface
263
+ │ - Emulator class │
264
+ │ - Generator methods │
265
+ │ - NumPy integration │
266
+ └─────────────────────────────────┘
267
+
268
+ ┌─────────────────────────────────┐
269
+ │ C Extension (_ra_wrapper) │ ← Low-level wrapper
270
+ │ - Core loading/management │
271
+ │ - Frame stepping │
272
+ │ - Input handling │
273
+ └─────────────────────────────────┘
274
+
275
+ ┌─────────────────────────────────┐
276
+ │ Libretro Core (.so) │ ← Emulation engine
277
+ │ - Game logic │
278
+ │ - Video/audio generation │
279
+ └─────────────────────────────────┘
280
+ ```
281
+
282
+ ### Data Classes
283
+
284
+ #### `SystemInfo`
285
+
286
+ Contains core metadata:
287
+
288
+ ```python
289
+ @dataclass
290
+ class SystemInfo:
291
+ library_name: str # e.g., "Snes9x"
292
+ library_version: str # e.g., "1.62.3"
293
+ valid_extensions: list[str] # e.g., ["sfc", "smc"]
294
+ need_fullpath: bool # Whether core needs full ROM path
295
+ ```
296
+
297
+ #### `AVInfo`
298
+
299
+ Audio/video specifications:
300
+
301
+ ```python
302
+ @dataclass
303
+ class AVInfo:
304
+ fps: float # Target frames per second
305
+ sample_rate: float # Audio sample rate (Hz)
306
+ base_width: int # Native video width
307
+ base_height: int # Native video height
308
+ max_width: int # Maximum width
309
+ max_height: int # Maximum height
310
+ aspect_ratio: float # Pixel aspect ratio
311
+ ```
312
+
313
+ #### `VideoFrame`
314
+
315
+ Video frame data:
316
+
317
+ ```python
318
+ @dataclass
319
+ class VideoFrame:
320
+ data: np.ndarray # Raw pixel buffer
321
+ width: int # Frame width
322
+ height: int # Frame height
323
+ pitch: int # Bytes per row
324
+ format: int # Pixel format (0/1/2)
325
+
326
+ def to_rgb(self) -> np.ndarray:
327
+ # Returns: (height, width, 3) uint8 array
328
+ ```
329
+
330
+ **Supported pixel formats:**
331
+
332
+ - `0`: 0RGB1555 (16-bit)
333
+ - `1`: XRGB8888 (32-bit)
334
+ - `2`: RGB565 (16-bit)
335
+
336
+ #### `AudioFrame`
337
+
338
+ Audio sample data:
339
+
340
+ ```python
341
+ @dataclass
342
+ class AudioFrame:
343
+ data: np.ndarray # Shape: (frames, 2), dtype: int16
344
+ frames: int # Number of stereo frames
345
+ sample_rate: float # Sample rate in Hz
346
+ ```
347
+
348
+ ### Video Frame Conversion
349
+
350
+ The `VideoFrame.to_rgb()` method efficiently converts raw pixel data to RGB:
351
+
352
+ ```python
353
+ video = emu.get_video_frame()
354
+ rgb = video.to_rgb() # NumPy array (height, width, 3)
355
+
356
+ # Use with PIL
357
+ from PIL import Image
358
+ img = Image.fromarray(rgb, 'RGB')
359
+ img.save('frame.png')
360
+
361
+ # Use with OpenCV
362
+ import cv2
363
+ bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
364
+ cv2.imwrite('frame.png', bgr)
365
+ ```
366
+
367
+ ### Performance Considerations
368
+
369
+ #### Frame Rate Control
370
+
371
+ pdretro operates in a pull-based model—Python controls the frame rate:
372
+
373
+ ```python
374
+ import time
375
+
376
+ target_fps = emu.av_info.fps
377
+ frame_time = 1.0 / target_fps
378
+
379
+ for video, audio in emu.frames:
380
+ start = time.time()
381
+
382
+ # Process frame...
383
+
384
+ # Maintain target FPS
385
+ elapsed = time.time() - start
386
+ if elapsed < frame_time:
387
+ time.sleep(frame_time - elapsed)
388
+ ```
389
+
390
+ #### Zero-Copy Access
391
+
392
+ Video and audio data use NumPy array views with no copying:
393
+
394
+ ```python
395
+ # Efficient: view into existing buffer
396
+ video = emu.get_video_frame()
397
+ print(video.data.flags.owndata) # False
398
+
399
+ # Efficient: direct RGB conversion
400
+ rgb = video.to_rgb() # Uses NumPy vectorized operations
401
+ ```
402
+
403
+ #### Generator Efficiency
404
+
405
+ Generators maintain minimal memory footprint:
406
+
407
+ ```python
408
+ # Memory-efficient: only one frame in memory
409
+ for video in emu.video_frames:
410
+ process(video)
411
+
412
+ # Memory-inefficient: loads all frames
413
+ frames = list(emu.video_frames) # Don't do this!
414
+ ```
415
+
416
+ ### Testing
417
+
418
+ Run the test suite:
419
+
420
+ ```bash
421
+ # Install test dependencies
422
+ pip install pytest pillow
423
+
424
+ # Run all tests
425
+ pytest tests/
426
+
427
+ # Run with coverage
428
+ pytest tests/ --cov=pdretro
429
+ ```
430
+
431
+ **Test requirements:**
432
+
433
+ - SNES9x core: `snes9x_libretro.dll` in `cores/`
434
+ - F-Zero ROM: `f-zero.sfc` in `roms/`
435
+
436
+ ## Examples
437
+
438
+ ### Save Screenshots
439
+
440
+ ```python
441
+ from pdretro import Emulator
442
+ from PIL import Image
443
+ import time
444
+
445
+ with Emulator("cores/snes9x_libretro.so") as emu:
446
+ emu.load_game("roms/game.sfc")
447
+
448
+ time.sleep(5) # Skip black start screen
449
+
450
+ # Save screenshot
451
+ video = emu.get_video_frame()
452
+ rgb = video.to_rgb()
453
+ Image.fromarray(rgb, 'RGB').save('screenshot.png')
454
+ ```
455
+
456
+ ### Record Video
457
+
458
+ ```python
459
+ from pdretro import Emulator
460
+ import cv2
461
+ import numpy as np
462
+ import time
463
+
464
+ with Emulator("cores/snes9x_libretro.so") as emu:
465
+ emu.load_game("roms/game.sfc")
466
+
467
+ # Setup video writer
468
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
469
+ out = cv2.VideoWriter(
470
+ 'output.mp4',
471
+ fourcc,
472
+ emu.av_info.fps,
473
+ (emu.av_info.base_width, emu.av_info.base_height)
474
+ )
475
+
476
+ # Record 300 frames (10 seconds at 30fps)
477
+ for i, video in enumerate(emu.video_frames):
478
+ if i >= 300:
479
+ break
480
+
481
+ rgb = video.to_rgb()
482
+ bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
483
+ out.write(bgr)
484
+ time.sleep(1/30)
485
+
486
+ out.release()
487
+ ```
488
+
489
+ ### AI Training Loop
490
+
491
+ ```python
492
+ from pdretro import Emulator
493
+ import numpy as np
494
+
495
+ def train_agent():
496
+ with Emulator("cores/snes9x_libretro.so") as emu:
497
+ emu.load_game("roms/game.sfc")
498
+
499
+ for episode in range(1000):
500
+ # Reset game
501
+ emu.reset()
502
+
503
+ for frame_num in range(1000):
504
+ # Get current state
505
+ video = emu.get_video_frame()
506
+ state = video.to_rgb()
507
+
508
+ # Agent decides action
509
+ action = agent.get_action(state)
510
+
511
+ # Apply action
512
+ _ra_wrapper.clear_input()
513
+ if action == 0: # Jump
514
+ _ra_wrapper.set_button(0, 8, True) # A button
515
+
516
+ # Step emulation
517
+ emu.step()
518
+
519
+ # Get reward and train
520
+ reward = compute_reward(state)
521
+ agent.train(state, action, reward)
522
+ ```
523
+
524
+ ### Audio Analysis
525
+
526
+ ```python
527
+ from pdretro import Emulator
528
+ import numpy as np
529
+ import matplotlib.pyplot as plt
530
+
531
+ with Emulator("cores/snes9x_libretro.so") as emu:
532
+ emu.load_game("roms/game.sfc")
533
+
534
+ # Collect audio samples
535
+ audio_buffer = []
536
+ for i, audio in enumerate(emu.audio_frames):
537
+ if i >= 60: # 2 seconds at 30fps
538
+ break
539
+ audio_buffer.append(audio.data)
540
+
541
+ # Concatenate all audio
542
+ full_audio = np.concatenate(audio_buffer, axis=0)
543
+
544
+ # Analyze
545
+ left_channel = full_audio[:, 0]
546
+ right_channel = full_audio[:, 1]
547
+
548
+ # Plot waveform
549
+ plt.plot(left_channel[:1000])
550
+ plt.title('Audio Waveform')
551
+ plt.show()
552
+ ```
553
+
554
+ ## Troubleshooting
555
+
556
+ ### Core Loading Issues
557
+
558
+ ```bash
559
+ # Error: "Failed to load core"
560
+ # Check core path and architecture (32/64-bit)
561
+
562
+ # Linux: verify with
563
+ file cores/snes9x_libretro.so
564
+ ldd cores/snes9x_libretro.so # Check dependencies
565
+
566
+ # Windows: use Dependency Walker
567
+ ```
568
+
569
+ ### ROM Loading Failures
570
+
571
+ ```bash
572
+ # Error: "Failed to load game"
573
+ # Verify ROM format matches core's valid_extensions
574
+
575
+ with Emulator("cores/snes9x_libretro.so") as emu:
576
+ print(emu.system_info.valid_extensions)
577
+ # ['sfc', 'smc', 'swc', 'fig', 'bs', 'st']
578
+ ```
579
+
580
+ ### Performance Issues
581
+
582
+ **Slow frame processing:**
583
+
584
+ - Use NumPy vectorized operations
585
+ - Avoid list comprehensions on large arrays
586
+ - Profile with `cProfile` to find bottlenecks
587
+
588
+ **Memory leaks:**
589
+
590
+ - Always use context managers or call `shutdown()`
591
+ - Don't hold references to `VideoFrame` objects
592
+ - Clear input state after use
593
+
594
+ ### Platform-Specific Notes
595
+
596
+ **Windows:**
597
+
598
+ - Use `.dll` cores from RetroArch buildbot
599
+ - Ensure Visual C++ Redistributable is installed
600
+
601
+ **Linux:**
602
+
603
+ - Use `.so` cores
604
+ - May need to install `libgomp1` for some cores
605
+
606
+ **macOS:**
607
+
608
+ - Use `.dylib` cores
609
+ - May need to allow core in Security & Privacy settings
610
+
611
+ ## Project Structure
612
+
613
+ ```
614
+ pdretro/
615
+ ├── src/
616
+ │ ├── pdretro/
617
+ │ │ ├── __init__.py
618
+ │ │ └── emulator.py # High-level Python API
619
+ │ └── wrapper/
620
+ │ ├── ra_wrapper.c # Core wrapper implementation
621
+ │ ├── ra_wrapper.h # C API header
622
+ │ └── ra_wrapper_python.c # CPython extension
623
+ ├── tests/
624
+ │ └── test.py # Test suite
625
+ ├── setup.py # Build configuration
626
+ ├── pyproject.toml
627
+ └── README.md
628
+ ```
629
+
630
+ ## Roadmap
631
+
632
+ - [ ] Implement memory search/manipulation
633
+ - [ ] Cheat code support
634
+ - [ ] Network play hooks
635
+
636
+ ## License
637
+
638
+ MIT License - Copyright (c) 2026
639
+
640
+ See [LICENSE](LICENSE) for full text.
641
+
642
+ ## Links
643
+
644
+ - **PyPI**: https://pypi.org/project/pdretro/
645
+ - **GitHub**: https://github.com/ColinThePanda/pdretro
646
+ - **Issues**: https://github.com/ColinThePanda/pdretro/issues
647
+ - **Libretro**: https://www.libretro.com/
648
+
649
+ ## Acknowledgments
650
+
651
+ Built on the [libretro API](https://www.libretro.com/) - a simple but powerful emulation framework enabling cross-platform emulator development.
@@ -0,0 +1,8 @@
1
+ _ra_wrapper.cp313-win_amd64.pyd,sha256=9xGqmTeBDC0U0irGaWNW-BTOZ0gR9WJpaohsWa0ZEgI,19968
2
+ pdretro/__init__.py,sha256=4yjXtUCAL-rnGbox-xqEpYik_gXt6SqltXBz8q6jAIs,252
3
+ pdretro/emulator.py,sha256=Q6cC3OQoaMpy1AKIskpd-90VuVHjT-oqr1MZBbrnOjw,7558
4
+ pdretro-0.1.2.dist-info/licenses/LICENSE,sha256=qOvs5ApAj5oPv8OieRSqbR30jUWKhp9nStv62fxcmfU,1090
5
+ pdretro-0.1.2.dist-info/METADATA,sha256=hN8TrqWSn24aQLduaO-U4fqpL4RJ5xyGsFo4U4rkvc0,16422
6
+ pdretro-0.1.2.dist-info/WHEEL,sha256=T5i2ODeLs0s2las6bzoWK6w-WYjxkhtXh7SRJsBsjo4,102
7
+ pdretro-0.1.2.dist-info/top_level.txt,sha256=CbQDJyuLDonumwYRM0WRyzxsVApOVWv_CNqbSeLuxV8,20
8
+ pdretro-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.1)
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313-win_amd64
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Colin Politi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ _ra_wrapper
2
+ pdretro