emoemu 0.1.0

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.
Files changed (213) hide show
  1. package/.claude/settings.local.json +77 -0
  2. package/.node-version +1 -0
  3. package/CLAUDE.md +435 -0
  4. package/README.md +404 -0
  5. package/TODO.md +655 -0
  6. package/dist/index.cjs +25108 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +25085 -0
  9. package/docs/building-libretro-cores-arm-mac.md +237 -0
  10. package/docs/config-file-format.md +488 -0
  11. package/docs/cores-trd.md +425 -0
  12. package/docs/headless-hardware-rendering-trd.md +676 -0
  13. package/docs/libretro-cores-trd.md +997 -0
  14. package/docs/mupen64-software-rendering-trd.md +751 -0
  15. package/docs/n64-support-trd.md +306 -0
  16. package/docs/native-rendering-trd.md +540 -0
  17. package/docs/native-ui-rendering-trd.md +1195 -0
  18. package/docs/netplay-trd.md +665 -0
  19. package/docs/retroarch-netplay-docs.md +277 -0
  20. package/docs/save-state-format.md +666 -0
  21. package/eslint.config.js +111 -0
  22. package/icon/icon.png +0 -0
  23. package/icon/icon.pxd +0 -0
  24. package/package.json +63 -0
  25. package/pnpm-workspace.yaml +10 -0
  26. package/src/Emulator/consts.ts +14 -0
  27. package/src/Emulator/index.ts +2496 -0
  28. package/src/Emulator/saveState/index.ts +155 -0
  29. package/src/Emulator/screenshot/index.ts +160 -0
  30. package/src/Emulator/terminalDimensions/index.ts +79 -0
  31. package/src/Emulator/types.ts +83 -0
  32. package/src/cli/commands/consts.ts +10 -0
  33. package/src/cli/commands/index.ts +462 -0
  34. package/src/cli/parseArgs/consts.ts +17 -0
  35. package/src/cli/parseArgs/index.ts +457 -0
  36. package/src/cli/parseArgs/types.ts +61 -0
  37. package/src/cli/runEmulator/index.ts +406 -0
  38. package/src/cli/runEmulator/types.ts +7 -0
  39. package/src/consts.ts +19 -0
  40. package/src/core/button/consts.ts +35 -0
  41. package/src/core/button/index.ts +123 -0
  42. package/src/core/core.ts +300 -0
  43. package/src/core/index.ts +19 -0
  44. package/src/cores/libretro/api/index.ts +334 -0
  45. package/src/cores/libretro/api/types.ts +148 -0
  46. package/src/cores/libretro/callbacks/consts.ts +41 -0
  47. package/src/cores/libretro/callbacks/index.ts +456 -0
  48. package/src/cores/libretro/consts.ts +45 -0
  49. package/src/cores/libretro/coreOptions/consts.ts +36 -0
  50. package/src/cores/libretro/coreOptions/index.ts +222 -0
  51. package/src/cores/libretro/environment/consts.ts +118 -0
  52. package/src/cores/libretro/environment/index.ts +1095 -0
  53. package/src/cores/libretro/index.ts +937 -0
  54. package/src/cores/libretro/loader/index.ts +496 -0
  55. package/src/cores/libretro/pixelFormat/consts.ts +43 -0
  56. package/src/cores/libretro/pixelFormat/index.ts +397 -0
  57. package/src/cores/libretro/types.ts +339 -0
  58. package/src/frontend/AudioManager/index.ts +420 -0
  59. package/src/frontend/SettingsManager/index.ts +250 -0
  60. package/src/frontend/config/index.ts +608 -0
  61. package/src/frontend/config/tests.ts +354 -0
  62. package/src/frontend/config/types.ts +36 -0
  63. package/src/frontend/consts.ts +114 -0
  64. package/src/frontend/coreBuilder/index.ts +644 -0
  65. package/src/frontend/coreBuilder/types.ts +15 -0
  66. package/src/frontend/coreDownloader/index.ts +620 -0
  67. package/src/frontend/coreDownloader/types.ts +17 -0
  68. package/src/frontend/corePreferences/index.ts +69 -0
  69. package/src/frontend/coreRegistry/index.ts +276 -0
  70. package/src/frontend/directoryCache/index.ts +75 -0
  71. package/src/frontend/index.ts +79 -0
  72. package/src/frontend/notifications/consts.ts +14 -0
  73. package/src/frontend/notifications/index.ts +250 -0
  74. package/src/frontend/playlist/consts.ts +168 -0
  75. package/src/frontend/playlist/index.ts +899 -0
  76. package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
  77. package/src/frontend/playlist/labelFormatter/index.ts +155 -0
  78. package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
  79. package/src/frontend/playlist/reader/index.ts +559 -0
  80. package/src/frontend/playlist/sync/index.ts +511 -0
  81. package/src/frontend/playlist/systemLookup/index.ts +233 -0
  82. package/src/frontend/playlist/utils/index.ts +50 -0
  83. package/src/frontend/romScanner/consts.ts +348 -0
  84. package/src/frontend/romScanner/index.ts +1957 -0
  85. package/src/frontend/saveServices/consts.ts +2 -0
  86. package/src/frontend/saveServices/index.ts +313 -0
  87. package/src/frontend/serviceProvider/index.ts +108 -0
  88. package/src/frontend/serviceProvider/types.ts +13 -0
  89. package/src/index.ts +428 -0
  90. package/src/input/Controller/consts.ts +50 -0
  91. package/src/input/Controller/index.ts +81 -0
  92. package/src/input/GamepadManager/consts.ts +22 -0
  93. package/src/input/GamepadManager/index.ts +418 -0
  94. package/src/input/InputManager/consts.ts +86 -0
  95. package/src/input/InputManager/index.ts +593 -0
  96. package/src/input/InputMapper/consts.ts +33 -0
  97. package/src/input/InputMapper/index.ts +436 -0
  98. package/src/input/consts.ts +410 -0
  99. package/src/input/gamepadProfiles/index.ts +1100 -0
  100. package/src/input/index.ts +38 -0
  101. package/src/input/inputUtils/index.ts +31 -0
  102. package/src/netplay/FrameBuffer/consts.ts +2 -0
  103. package/src/netplay/FrameBuffer/index.ts +364 -0
  104. package/src/netplay/FrameBuffer/tests.ts +286 -0
  105. package/src/netplay/InputBuffer/consts.ts +7 -0
  106. package/src/netplay/InputBuffer/index.ts +347 -0
  107. package/src/netplay/InputBuffer/tests.ts +166 -0
  108. package/src/netplay/NetplayClient/index.ts +976 -0
  109. package/src/netplay/NetplayConnection/index.ts +536 -0
  110. package/src/netplay/NetplayDiscovery/consts.ts +41 -0
  111. package/src/netplay/NetplayDiscovery/index.ts +525 -0
  112. package/src/netplay/NetplayServer/index.ts +1407 -0
  113. package/src/netplay/SyncManager/index.ts +984 -0
  114. package/src/netplay/SyncManager/tests.ts +419 -0
  115. package/src/netplay/consts.ts +371 -0
  116. package/src/netplay/crc32/consts.ts +14 -0
  117. package/src/netplay/crc32/index.ts +97 -0
  118. package/src/netplay/crc32/tests.ts +40 -0
  119. package/src/netplay/index.ts +41 -0
  120. package/src/netplay/netplayLogger/consts.ts +30 -0
  121. package/src/netplay/netplayLogger/index.ts +345 -0
  122. package/src/netplay/protocol/consts.ts +86 -0
  123. package/src/netplay/protocol/index.ts +1280 -0
  124. package/src/netplay/protocol/tests.ts +606 -0
  125. package/src/netplay/protocol/types.ts +20 -0
  126. package/src/netplay/types.ts +395 -0
  127. package/src/rendering/KittyRenderer/index.ts +616 -0
  128. package/src/rendering/NativeRenderer/index.ts +279 -0
  129. package/src/rendering/NativeRenderer/tests.ts +133 -0
  130. package/src/rendering/TerminalRenderer/index.ts +770 -0
  131. package/src/rendering/consts.ts +401 -0
  132. package/src/rendering/fonts/CozetteVector.ttf +0 -0
  133. package/src/rendering/index.ts +26 -0
  134. package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
  135. package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
  136. package/src/rendering/nativeUi/consts.ts +6 -0
  137. package/src/rendering/nativeUi/index.ts +20 -0
  138. package/src/rendering/postProcessing/consts.ts +38 -0
  139. package/src/rendering/postProcessing/index.ts +923 -0
  140. package/src/rendering/shared/ansi/consts.ts +12 -0
  141. package/src/rendering/shared/ansi/index.ts +104 -0
  142. package/src/rendering/shared/consts.ts +2 -0
  143. package/src/rendering/shared/fitToTerminal/index.ts +67 -0
  144. package/src/ui/AddRomsPrompt/consts.ts +13 -0
  145. package/src/ui/AddRomsPrompt/index.tsx +781 -0
  146. package/src/ui/App/consts.ts +2 -0
  147. package/src/ui/App/index.tsx +456 -0
  148. package/src/ui/AppCapabilities/index.tsx +67 -0
  149. package/src/ui/ConfigContext/index.tsx +56 -0
  150. package/src/ui/CoreManager/consts.ts +11 -0
  151. package/src/ui/CoreManager/index.tsx +779 -0
  152. package/src/ui/CoreSelector/consts.ts +2 -0
  153. package/src/ui/CoreSelector/index.tsx +251 -0
  154. package/src/ui/DialogContainer/index.tsx +42 -0
  155. package/src/ui/DialogOptionsList/index.tsx +61 -0
  156. package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
  157. package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
  158. package/src/ui/GamepadContext/consts.ts +15 -0
  159. package/src/ui/GamepadContext/index.tsx +295 -0
  160. package/src/ui/NativeDialog/index.tsx +120 -0
  161. package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
  162. package/src/ui/NetplayPauseMenu/consts.ts +2 -0
  163. package/src/ui/NetplayPauseMenu/index.tsx +97 -0
  164. package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
  165. package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
  166. package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
  167. package/src/ui/RomBrowser/consts.ts +61 -0
  168. package/src/ui/RomBrowser/index.tsx +1164 -0
  169. package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
  170. package/src/ui/RomBrowser/types.ts +67 -0
  171. package/src/ui/SaveStateDialog/consts.ts +2 -0
  172. package/src/ui/SaveStateDialog/index.tsx +225 -0
  173. package/src/ui/WarningDialog/index.tsx +113 -0
  174. package/src/ui/consts.ts +27 -0
  175. package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
  176. package/src/ui/hooks/useClearTerminal/index.ts +37 -0
  177. package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
  178. package/src/ui/hooks/useGamepad/consts.ts +21 -0
  179. package/src/ui/hooks/useGamepad/index.ts +194 -0
  180. package/src/ui/index.ts +27 -0
  181. package/src/utils/buffer/consts.ts +17 -0
  182. package/src/utils/buffer/index.ts +129 -0
  183. package/src/utils/color/consts.ts +58 -0
  184. package/src/utils/color/index.ts +183 -0
  185. package/src/utils/compression/consts.ts +50 -0
  186. package/src/utils/compression/index.ts +101 -0
  187. package/src/utils/consts.ts +2 -0
  188. package/src/utils/crc32/consts.ts +22 -0
  189. package/src/utils/crc32/index.ts +83 -0
  190. package/src/utils/ensureDirectory/index.ts +10 -0
  191. package/src/utils/format/consts.ts +8 -0
  192. package/src/utils/format/index.ts +53 -0
  193. package/src/utils/getErrorMessage/index.ts +10 -0
  194. package/src/utils/index.ts +113 -0
  195. package/src/utils/ini/index.ts +200 -0
  196. package/src/utils/kitty/consts.ts +13 -0
  197. package/src/utils/kitty/index.ts +181 -0
  198. package/src/utils/logger/consts.ts +35 -0
  199. package/src/utils/logger/index.ts +217 -0
  200. package/src/utils/paths/consts.ts +18 -0
  201. package/src/utils/paths/index.ts +151 -0
  202. package/src/utils/png/consts.ts +34 -0
  203. package/src/utils/png/index.ts +131 -0
  204. package/src/utils/readJsonFile/index.ts +16 -0
  205. package/src/utils/rotateLogFile/index.ts +44 -0
  206. package/src/utils/safeClose/index.ts +10 -0
  207. package/src/utils/terminal/consts.ts +8 -0
  208. package/src/utils/terminal/index.ts +102 -0
  209. package/src/utils/thumbnailRenderer/consts.ts +2 -0
  210. package/src/utils/thumbnailRenderer/index.ts +147 -0
  211. package/src/utils/typedError/index.ts +26 -0
  212. package/tsconfig.json +31 -0
  213. package/vitest.config.ts +13 -0
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Core Interface Definitions
3
+ *
4
+ * Defines the interface that all system emulator cores must implement.
5
+ * This enables a multi-core architecture similar to libretro where
6
+ * different gaming systems can share common infrastructure.
7
+ */
8
+
9
+ /**
10
+ * Button definition for input mapping
11
+ */
12
+ export interface ButtonDefinition {
13
+ /** Button ID (0-based index used in setButtonState) */
14
+ id: number;
15
+
16
+ /** Display name (e.g., "A", "Start", "L") */
17
+ name: string;
18
+
19
+ /** Suggested keyboard key */
20
+ defaultKey: string;
21
+
22
+ /** Suggested gamepad button */
23
+ defaultGamepad: string;
24
+ }
25
+
26
+ /**
27
+ * System information describing a core's capabilities and requirements.
28
+ * This is used by the frontend to configure rendering, audio, and input.
29
+ */
30
+ export interface SystemInfo {
31
+ /** Unique identifier (e.g., "nes", "gba") */
32
+ id: string;
33
+
34
+ /** Human-readable name (e.g., "Nintendo Entertainment System") */
35
+ name: string;
36
+
37
+ /** File extensions this core handles (e.g., [".nes", ".unf"]) */
38
+ extensions: string[];
39
+
40
+ /** Native framebuffer width in pixels */
41
+ width: number;
42
+
43
+ /** Native framebuffer height in pixels */
44
+ height: number;
45
+
46
+ /** Target frames per second (e.g., 60.0988 for NES NTSC) */
47
+ fps: number;
48
+
49
+ /** Preferred audio sample rate in Hz */
50
+ sampleRate: number;
51
+
52
+ /** Pixel aspect ratio for correct display (e.g., 8/7 for NES) */
53
+ pixelAspectRatio: number;
54
+
55
+ /** Maximum number of controller ports */
56
+ maxPlayers: number;
57
+
58
+ /** Button definitions for this system */
59
+ buttons: ButtonDefinition[];
60
+
61
+ /**
62
+ * Framebuffer color format:
63
+ * - 'rgb15': Uint16Array of 15-bit RGB (xBBBBBGGGGGRRRRR)
64
+ * - 'rgb24': Uint8Array of RGB triplets
65
+ */
66
+ colorSpace: 'rgb15' | 'rgb24';
67
+
68
+ /** Core name for netplay (e.g., "PicoDrive") - defaults to name if not set */
69
+ coreName?: string;
70
+
71
+ /** Core version for netplay (e.g., "2.05-3365b17") */
72
+ coreVersion?: string;
73
+ }
74
+
75
+ /**
76
+ * Audio configuration returned by the core
77
+ */
78
+ export interface AudioConfig {
79
+ /** Sample rate in Hz */
80
+ sampleRate: number;
81
+
82
+ /** Number of audio channels (1 = mono, 2 = stereo) */
83
+ channels: 1 | 2;
84
+ }
85
+
86
+ /**
87
+ * Severity/importance of a user-facing message.
88
+ * Values align with libretro SET_MESSAGE_EXT severity levels.
89
+ */
90
+ export type MessageSeverity = 'debug' | 'info' | 'warn' | 'error';
91
+
92
+ /**
93
+ * Message from core to frontend for display to user
94
+ */
95
+ export interface CoreMessage {
96
+ /** Message text */
97
+ msg: string;
98
+
99
+ /** Duration in milliseconds (0 = use default) */
100
+ duration: number;
101
+
102
+ /** Priority (higher = more important, displaces lower priority messages) */
103
+ priority: number;
104
+
105
+ /** Message type: 'notification' | 'status' | 'progress' */
106
+ type: 'notification' | 'status' | 'progress';
107
+
108
+ /** Progress value: -1 = indeterminate, 0-100 = percentage (for type='progress') */
109
+ progress: number;
110
+
111
+ /** Severity/importance of the message (default: 'info') */
112
+ severity: MessageSeverity;
113
+ }
114
+
115
+ /** Callback for receiving core messages */
116
+ export type CoreMessageCallback = (message: CoreMessage) => void;
117
+
118
+ /**
119
+ * Main Core interface that all system emulators must implement.
120
+ *
121
+ * The lifecycle is:
122
+ * 1. Construct the core
123
+ * 2. Call getSystemInfo() to get capabilities
124
+ * 3. Call loadRom() to load a game
125
+ * 4. Optionally call setState() to restore a save state
126
+ * 5. Call reset() if starting fresh (skipped if restoring state)
127
+ * 6. Main loop: runFrame(), getFramebuffer(), render
128
+ * 7. Call destroy() when done
129
+ */
130
+ export interface Core {
131
+ //==========================================================================
132
+ // Lifecycle
133
+ //==========================================================================
134
+
135
+ /**
136
+ * Get system information describing the core's capabilities.
137
+ * Can be called before loadRom() to determine if this core handles a ROM.
138
+ */
139
+ getSystemInfo(): SystemInfo;
140
+
141
+ /**
142
+ * Load a ROM/game file.
143
+ * @param romPath Path to the ROM file
144
+ * @throws Error if ROM is invalid or unsupported
145
+ */
146
+ loadRom(romPath: string): void;
147
+
148
+ /**
149
+ * Reset the emulated system to power-on state.
150
+ * Should be called after loadRom() unless restoring a save state.
151
+ */
152
+ reset(): void;
153
+
154
+ /**
155
+ * Clean up resources (close files, release audio, etc.).
156
+ * Called before destroying the core instance.
157
+ */
158
+ destroy(): void;
159
+
160
+ //==========================================================================
161
+ // Emulation
162
+ //==========================================================================
163
+
164
+ /**
165
+ * Run emulation for one frame.
166
+ * Updates the framebuffer and generates audio samples via callback.
167
+ */
168
+ runFrame(): void;
169
+
170
+ /**
171
+ * Check if the core completed a frame.
172
+ * Useful for cores with variable-rate timing.
173
+ */
174
+ isFrameComplete(): boolean;
175
+
176
+ //==========================================================================
177
+ // Video Output
178
+ //==========================================================================
179
+
180
+ /**
181
+ * Get the current framebuffer.
182
+ * Format depends on SystemInfo.colorSpace:
183
+ * - 'rgb15': Uint16Array of 15-bit RGB values
184
+ * - 'rgb24': Uint8Array of RGB triplets
185
+ */
186
+ getFramebuffer(): Uint8Array | Uint16Array;
187
+
188
+ //==========================================================================
189
+ // Audio Output
190
+ //==========================================================================
191
+
192
+ /**
193
+ * Get audio configuration (sample rate, channels).
194
+ */
195
+ getAudioConfig(): AudioConfig;
196
+
197
+ /**
198
+ * Set callback for audio sample output.
199
+ * The core calls this callback when audio samples are ready.
200
+ * @param callback Function to receive Float32Array of samples, or null to disable
201
+ */
202
+ setAudioCallback(callback: ((samples: Float32Array) => void) | null): void;
203
+
204
+ /**
205
+ * Set audio enable flag (optional).
206
+ * Tells the core whether to generate audio samples, allowing cores to
207
+ * skip audio processing when muted. Not all cores implement this.
208
+ * @param enabled Whether audio generation is enabled
209
+ */
210
+ setAudioEnabled?(enabled: boolean): void;
211
+
212
+ /**
213
+ * Set message callback for core notifications (optional).
214
+ * Receives messages like "State saved", "Disk inserted", etc.
215
+ * @param callback Function to receive messages, or null to disable
216
+ */
217
+ setMessageCallback?(callback: CoreMessageCallback | null): void;
218
+
219
+ //==========================================================================
220
+ // Input
221
+ //==========================================================================
222
+
223
+ /**
224
+ * Set button state for a controller.
225
+ * @param port Controller port (0-based, up to maxPlayers-1)
226
+ * @param button Button ID from SystemInfo.buttons
227
+ * @param pressed Whether the button is currently pressed
228
+ */
229
+ setButtonState(port: number, button: number, pressed: boolean): void;
230
+
231
+ /**
232
+ * Get current button state for a controller (for status display).
233
+ * @param port Controller port (0-based)
234
+ * @returns Map of button ID to pressed state
235
+ */
236
+ getButtonState(port: number): Map<number, boolean>;
237
+
238
+ /**
239
+ * Set analog stick axis value (optional - only for cores that support analog input).
240
+ * @param port Controller port (0-based)
241
+ * @param index Analog stick (0=left, 1=right)
242
+ * @param axis Axis (0=X, 1=Y)
243
+ * @param value Normalized value from -1.0 to 1.0 (or raw int16)
244
+ */
245
+ setAnalogState?(port: number, index: number, axis: number, value: number): void;
246
+
247
+ //==========================================================================
248
+ // State Management
249
+ //==========================================================================
250
+
251
+ /**
252
+ * Serialize the current emulation state for saving.
253
+ * Returns raw binary data that can be written directly to a file.
254
+ */
255
+ getState(): Buffer | null;
256
+
257
+ /**
258
+ * Restore emulation state from a previous save.
259
+ * @param state Raw binary state data
260
+ * @throws Error if state is invalid
261
+ */
262
+ setState(state: Buffer): void;
263
+
264
+ //==========================================================================
265
+ // Battery/SRAM (Optional - for games with save functionality)
266
+ //==========================================================================
267
+
268
+ /**
269
+ * Check if the current game has battery-backed save data.
270
+ * @returns true if the game supports saving to SRAM
271
+ */
272
+ hasBatterySave(): boolean;
273
+
274
+ /**
275
+ * Get battery-backed RAM contents for saving to disk.
276
+ * @returns SRAM data or null if not supported
277
+ */
278
+ getBatteryRam(): Uint8Array | null;
279
+
280
+ /**
281
+ * Load battery-backed RAM from disk.
282
+ * @param data SRAM data to restore
283
+ */
284
+ setBatteryRam(data: Uint8Array): void;
285
+ }
286
+
287
+ /** Type alias for framebuffer data */
288
+ export type FrameBuffer = Uint8Array | Uint16Array;
289
+
290
+ /** Narrows a framebuffer to Uint16Array when colorSpace is 'rgb15' */
291
+ export const isRgb15Buffer = (
292
+ colorSpace: 'rgb15' | 'rgb24',
293
+ _buffer: FrameBuffer,
294
+ ): _buffer is Uint16Array => colorSpace === 'rgb15';
295
+
296
+ /** Narrows a framebuffer to Uint8Array when colorSpace is 'rgb24' */
297
+ export const isRgb24Buffer = (
298
+ colorSpace: 'rgb15' | 'rgb24',
299
+ _buffer: FrameBuffer,
300
+ ): _buffer is Uint8Array => colorSpace === 'rgb24';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Core Module Exports
3
+ *
4
+ * Re-exports all core interface definitions and button utilities.
5
+ */
6
+
7
+ export type {
8
+ ButtonDefinition,
9
+ SystemInfo,
10
+ AudioConfig,
11
+ Core,
12
+ } from './core';
13
+
14
+ export {
15
+ StandardButton,
16
+ getButtonName,
17
+ DEFAULT_KEYBOARD_MAP,
18
+ areOppositeDirections,
19
+ } from './button';
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Libretro API FFI bindings using koffi
3
+ */
4
+
5
+ import koffi from "koffi";
6
+ import type { RetroSystemInfo, RetroSystemAVInfo } from "..";
7
+ import {
8
+ isPartialRetroSystemInfo,
9
+ isPartialRetroSystemAVInfo,
10
+ } from "./types";
11
+
12
+ export * from './types';
13
+
14
+ // Define koffi struct types for libretro API
15
+ // These are used by koffi internally when binding functions
16
+ koffi.struct("retro_game_geometry", {
17
+ base_width: "unsigned int",
18
+ base_height: "unsigned int",
19
+ max_width: "unsigned int",
20
+ max_height: "unsigned int",
21
+ aspect_ratio: "float",
22
+ });
23
+
24
+ koffi.struct("retro_system_timing", {
25
+ fps: "double",
26
+ sample_rate: "double",
27
+ });
28
+
29
+ koffi.struct("retro_system_av_info", {
30
+ geometry: "retro_game_geometry",
31
+ timing: "retro_system_timing",
32
+ });
33
+
34
+ koffi.struct("retro_system_info", {
35
+ library_name: "const char*",
36
+ library_version: "const char*",
37
+ valid_extensions: "const char*",
38
+ need_fullpath: "bool",
39
+ block_extract: "bool",
40
+ });
41
+
42
+ koffi.struct("retro_game_info", {
43
+ path: "const char*",
44
+ data: "const void*",
45
+ size: "size_t",
46
+ meta: "const char*",
47
+ });
48
+
49
+ export const retro_variable = koffi.struct("retro_variable", {
50
+ key: "const char*",
51
+ value: "const char*",
52
+ });
53
+
54
+ // Callback type definitions
55
+ export const retro_environment_t = koffi.proto(
56
+ "bool retro_environment_t(unsigned int cmd, void* data)"
57
+ );
58
+ // Use const uint8_t* for the framebuffer pointer so koffi can properly decode it
59
+ export const retro_video_refresh_t = koffi.proto(
60
+ "void retro_video_refresh_t(const uint8_t* data, unsigned int width, unsigned int height, size_t pitch)"
61
+ );
62
+ export const retro_audio_sample_t = koffi.proto(
63
+ "void retro_audio_sample_t(int16_t left, int16_t right)"
64
+ );
65
+ export const retro_audio_sample_batch_t = koffi.proto(
66
+ "size_t retro_audio_sample_batch_t(_Inout_ int16_t* data, size_t frames)"
67
+ );
68
+ export const retro_input_poll_t = koffi.proto("void retro_input_poll_t()");
69
+ export const retro_input_state_t = koffi.proto(
70
+ "int16_t retro_input_state_t(unsigned int port, unsigned int device, unsigned int index, unsigned int id)"
71
+ );
72
+ // Log callback - variadic args not supported in koffi callbacks, so we capture level + format string only.
73
+ // Extra printf args passed by C are ignored (cdecl convention handles stack cleanup on caller side).
74
+ export const retro_log_printf_t = koffi.proto(
75
+ "void retro_log_printf_t(int level, const char* fmt)"
76
+ );
77
+
78
+ // Type for koffi registered callback
79
+ export type KoffiCallback = ReturnType<typeof koffi.register>;
80
+
81
+
82
+ type AnyFunction = (...args: any[]) => any;
83
+
84
+ /**
85
+ * LibretroAPI class wraps the native libretro core functions via FFI
86
+ */
87
+ export class LibretroAPI {
88
+ private lib: koffi.IKoffiLib;
89
+
90
+ // Core lifecycle functions
91
+ retro_init!: () => void;
92
+ retro_deinit!: () => void;
93
+ retro_api_version!: () => number;
94
+ retro_get_system_info!: AnyFunction;
95
+ retro_get_system_av_info!: AnyFunction;
96
+ retro_set_controller_port_device!: (port: number, device: number) => void;
97
+ retro_reset!: () => void;
98
+ retro_run!: () => void;
99
+ retro_load_game!: AnyFunction;
100
+ retro_unload_game!: () => void;
101
+ retro_get_region!: () => number;
102
+
103
+ // Serialization functions
104
+ retro_serialize_size!: () => number;
105
+ retro_serialize!: (data: Buffer, size: number) => boolean;
106
+ retro_unserialize!: (data: Buffer, size: number) => boolean;
107
+
108
+ // Memory access functions
109
+ retro_get_memory_data!: (id: number) => Buffer | null;
110
+ retro_get_memory_size!: (id: number) => number;
111
+
112
+ // Callback setters
113
+ retro_set_environment!: (cb: KoffiCallback) => void;
114
+ retro_set_video_refresh!: (cb: KoffiCallback) => void;
115
+ retro_set_audio_sample!: (cb: KoffiCallback) => void;
116
+ retro_set_audio_sample_batch!: (cb: KoffiCallback) => void;
117
+ retro_set_input_poll!: (cb: KoffiCallback) => void;
118
+ retro_set_input_state!: (cb: KoffiCallback) => void;
119
+
120
+ constructor(corePath: string) {
121
+ this.lib = koffi.load(corePath);
122
+ this.bindFunctions();
123
+ }
124
+
125
+ private bindFunctions(): void {
126
+ // Core lifecycle
127
+ this.retro_init = this.lib.func("void retro_init()");
128
+ this.retro_deinit = this.lib.func("void retro_deinit()");
129
+ this.retro_api_version = this.lib.func("unsigned int retro_api_version()");
130
+ this.retro_get_system_info = this.lib.func(
131
+ "void retro_get_system_info(retro_system_info* info)"
132
+ );
133
+ this.retro_get_system_av_info = this.lib.func(
134
+ "void retro_get_system_av_info(retro_system_av_info* info)"
135
+ );
136
+ this.retro_set_controller_port_device = this.lib.func(
137
+ "void retro_set_controller_port_device(unsigned int port, unsigned int device)"
138
+ );
139
+ this.retro_reset = this.lib.func("void retro_reset()");
140
+ this.retro_run = this.lib.func("void retro_run()");
141
+ this.retro_load_game = this.lib.func(
142
+ "bool retro_load_game(const retro_game_info* game)"
143
+ );
144
+ this.retro_unload_game = this.lib.func("void retro_unload_game()");
145
+ this.retro_get_region = this.lib.func("unsigned int retro_get_region()");
146
+
147
+ // Serialization
148
+ this.retro_serialize_size = this.lib.func("size_t retro_serialize_size()");
149
+ this.retro_serialize = this.lib.func(
150
+ "bool retro_serialize(void* data, size_t size)"
151
+ );
152
+ this.retro_unserialize = this.lib.func(
153
+ "bool retro_unserialize(const void* data, size_t size)"
154
+ );
155
+
156
+ // Memory access
157
+ this.retro_get_memory_data = this.lib.func(
158
+ "void* retro_get_memory_data(unsigned int id)"
159
+ );
160
+ this.retro_get_memory_size = this.lib.func(
161
+ "size_t retro_get_memory_size(unsigned int id)"
162
+ );
163
+
164
+ // Callback setters - use koffi pointer() to create proper callback pointer types
165
+ this.retro_set_environment = this.lib.func(
166
+ "retro_set_environment",
167
+ "void",
168
+ [koffi.pointer(retro_environment_t)]
169
+ );
170
+ this.retro_set_video_refresh = this.lib.func(
171
+ "retro_set_video_refresh",
172
+ "void",
173
+ [koffi.pointer(retro_video_refresh_t)]
174
+ );
175
+ this.retro_set_audio_sample = this.lib.func(
176
+ "retro_set_audio_sample",
177
+ "void",
178
+ [koffi.pointer(retro_audio_sample_t)]
179
+ );
180
+ this.retro_set_audio_sample_batch = this.lib.func(
181
+ "retro_set_audio_sample_batch",
182
+ "void",
183
+ [koffi.pointer(retro_audio_sample_batch_t)]
184
+ );
185
+ this.retro_set_input_poll = this.lib.func(
186
+ "retro_set_input_poll",
187
+ "void",
188
+ [koffi.pointer(retro_input_poll_t)]
189
+ );
190
+ this.retro_set_input_state = this.lib.func(
191
+ "retro_set_input_state",
192
+ "void",
193
+ [koffi.pointer(retro_input_state_t)]
194
+ );
195
+ }
196
+
197
+ /**
198
+ * Get system info from the loaded core
199
+ */
200
+ getSystemInfo(): RetroSystemInfo {
201
+ // Allocate a buffer for the struct
202
+ const infoType = koffi.resolve("retro_system_info");
203
+ const infoBuf = Buffer.alloc(koffi.sizeof(infoType));
204
+
205
+ // Call the function with the buffer as output
206
+ this.retro_get_system_info(infoBuf);
207
+
208
+ // Decode the result and validate with type guard
209
+ const decoded: unknown = koffi.decode(infoBuf, infoType);
210
+ if (!isPartialRetroSystemInfo(decoded)) {
211
+ return {
212
+ library_name: "",
213
+ library_version: "",
214
+ valid_extensions: "",
215
+ need_fullpath: false,
216
+ block_extract: false,
217
+ };
218
+ }
219
+
220
+ return {
221
+ library_name: decoded.library_name ?? "",
222
+ library_version: decoded.library_version ?? "",
223
+ valid_extensions: decoded.valid_extensions ?? "",
224
+ need_fullpath: decoded.need_fullpath ?? false,
225
+ block_extract: decoded.block_extract ?? false,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Get audio/video info from the loaded core (call after loading a game)
231
+ */
232
+ getSystemAVInfo(): RetroSystemAVInfo {
233
+ // Allocate a buffer for the struct
234
+ const infoType = koffi.resolve("retro_system_av_info");
235
+ const infoBuf = Buffer.alloc(koffi.sizeof(infoType));
236
+
237
+ // Call the function with the buffer as output
238
+ this.retro_get_system_av_info(infoBuf);
239
+
240
+ // Decode the result and validate with type guard
241
+ const decoded: unknown = koffi.decode(infoBuf, infoType);
242
+ const defaultResult: RetroSystemAVInfo = {
243
+ geometry: { base_width: 0, base_height: 0, max_width: 0, max_height: 0, aspect_ratio: 0 },
244
+ timing: { fps: 0, sample_rate: 0 },
245
+ };
246
+
247
+ if (!isPartialRetroSystemAVInfo(decoded)) {
248
+ return defaultResult;
249
+ }
250
+
251
+ const { geometry, timing } = decoded;
252
+
253
+ return {
254
+ geometry: {
255
+ base_width: geometry?.base_width ?? 0,
256
+ base_height: geometry?.base_height ?? 0,
257
+ max_width: geometry?.max_width ?? 0,
258
+ max_height: geometry?.max_height ?? 0,
259
+ aspect_ratio: geometry?.aspect_ratio ?? 0,
260
+ },
261
+ timing: {
262
+ fps: timing?.fps ?? 0,
263
+ sample_rate: timing?.sample_rate ?? 0,
264
+ },
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Load a game into the core
270
+ */
271
+ loadGame(
272
+ path: string | null,
273
+ data: Buffer | null,
274
+ meta: string | null = null
275
+ ): boolean {
276
+ const gameInfo = {
277
+ path: path,
278
+ data: data,
279
+ size: data ? data.length : 0,
280
+ meta: meta,
281
+ };
282
+ return this.retro_load_game(gameInfo) as boolean;
283
+ }
284
+
285
+ /**
286
+ * Read data from a memory region by ID
287
+ * Uses koffi.view to get a direct ArrayBuffer view of native memory
288
+ */
289
+ getMemoryData(id: number): Uint8Array | null {
290
+ const size = this.retro_get_memory_size(id);
291
+ if (size === 0) {return null;}
292
+
293
+ const ptr = this.retro_get_memory_data(id);
294
+ if (!ptr) {return null;}
295
+
296
+ // Use koffi.view to get an ArrayBuffer view of native memory
297
+ const arrayBuffer = koffi.view(ptr, size) as ArrayBuffer;
298
+ const view = new Uint8Array(arrayBuffer);
299
+
300
+ // Copy to a new buffer (don't return the direct view to native memory)
301
+ const result = new Uint8Array(size);
302
+ result.set(view);
303
+ return result;
304
+ }
305
+
306
+ /**
307
+ * Write data to a memory region by ID
308
+ * Uses koffi.view to get a writable ArrayBuffer view of native memory
309
+ */
310
+ setMemoryData(id: number, data: Uint8Array): void {
311
+ const size = this.retro_get_memory_size(id);
312
+ if (size === 0) {return;}
313
+
314
+ const ptr = this.retro_get_memory_data(id);
315
+ if (!ptr) {return;}
316
+
317
+ const copySize = Math.min(data.length, size);
318
+
319
+ // Use koffi.view to get a writable ArrayBuffer view of native memory
320
+ const arrayBuffer = koffi.view(ptr, copySize) as ArrayBuffer;
321
+ const target = new Uint8Array(arrayBuffer);
322
+
323
+ // Copy data to the memory region
324
+ target.set(data.subarray(0, copySize));
325
+ }
326
+
327
+ /**
328
+ * Unload the library
329
+ */
330
+ destroy(): void {
331
+ // Note: koffi doesn't have a direct unload method for modern versions
332
+ // The library will be garbage collected when all references are released
333
+ }
334
+ }