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,997 @@
1
+ # Libretro Core Support - Technical Requirements Document
2
+
3
+ This document describes the design for loading and running native libretro cores (RetroArch cores) within emoemu, enabling support for additional systems like Sega Genesis, PlayStation, and others without writing new emulation code.
4
+
5
+ ## Overview
6
+
7
+ ### Goals
8
+
9
+ 1. **Leverage Existing Cores**: Use battle-tested libretro cores (PicoDrive, Beetle PSX, etc.) instead of writing new emulators
10
+ 2. **Seamless Integration**: Wrap libretro cores to implement our existing `Core` interface
11
+ 3. **Cross-Platform**: Support macOS (.dylib), Linux (.so), and Windows (.dll)
12
+ 4. **Minimal Overhead**: Efficient data passing between JavaScript and native code
13
+
14
+ ### Non-Goals
15
+
16
+ 1. Full RetroArch compatibility (shaders, netplay, achievements, etc.)
17
+ 2. Dynamic core downloading (cores must be provided by user)
18
+ 3. Support for cores requiring OpenGL/Vulkan contexts
19
+
20
+ ### Background: Libretro API
21
+
22
+ Libretro is a C API that defines a standard interface between emulator cores and frontends. Key characteristics:
23
+
24
+ - **Dynamic Libraries**: Cores are `.so`/`.dll`/`.dylib` files
25
+ - **Callback-Based**: Frontend provides function pointers for video/audio/input
26
+ - **Environment Queries**: Cores request capabilities via environment callback
27
+ - **Standardized I/O**: Common formats for framebuffer, audio samples, input polling
28
+
29
+ Popular libretro cores:
30
+ - **PicoDrive**: Sega Genesis/Mega Drive, Master System, Game Gear, 32X, Sega CD
31
+ - **Beetle PSX**: PlayStation 1
32
+ - **mGBA**: Game Boy Advance
33
+ - **Snes9x**: Super Nintendo (alternative to our native core)
34
+ - **Mupen64Plus-Next**: Nintendo 64
35
+
36
+ ---
37
+
38
+ ## Architecture
39
+
40
+ ### High-Level Design
41
+
42
+ ```
43
+ ┌─────────────────────────────────────────────────────────┐
44
+ │ emoemu Frontend │
45
+ │ (Emulator, Renderers, Audio, Input, State Manager) │
46
+ └─────────────────────────┬───────────────────────────────┘
47
+ │ Core Interface
48
+
49
+ ┌─────────────────────────────────────────────────────────┐
50
+ │ LibretroCore │
51
+ │ (Implements Core, wraps libretro API) │
52
+ └─────────────────────────┬───────────────────────────────┘
53
+ │ FFI (koffi)
54
+
55
+ ┌─────────────────────────────────────────────────────────┐
56
+ │ Native Libretro Core (.so/.dylib) │
57
+ │ (picodrive_libretro.dylib, etc.) │
58
+ └─────────────────────────────────────────────────────────┘
59
+ ```
60
+
61
+ ### Directory Structure
62
+
63
+ ```
64
+ src/cores/libretro/
65
+ ├── index.ts # LibretroCore class (implements Core)
66
+ ├── api.ts # FFI bindings for libretro functions
67
+ ├── types.ts # Libretro type definitions
68
+ ├── environment.ts # Environment callback handler
69
+ ├── callbacks.ts # Video/audio/input callback implementations
70
+ └── core-info.ts # Core metadata and system info mapping
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Libretro API Bindings
76
+
77
+ ### Core Functions
78
+
79
+ The libretro API consists of ~25 functions. Essential ones for basic operation:
80
+
81
+ ```typescript
82
+ // src/cores/libretro/api.ts
83
+
84
+ import koffi from 'koffi';
85
+
86
+ // Type definitions
87
+ const retro_game_info = koffi.struct('retro_game_info', {
88
+ path: 'const char*',
89
+ data: 'const void*',
90
+ size: 'size_t',
91
+ meta: 'const char*',
92
+ });
93
+
94
+ const retro_system_info = koffi.struct('retro_system_info', {
95
+ library_name: 'const char*',
96
+ library_version: 'const char*',
97
+ valid_extensions: 'const char*',
98
+ need_fullpath: 'bool',
99
+ block_extract: 'bool',
100
+ });
101
+
102
+ const retro_system_av_info = koffi.struct('retro_system_av_info', {
103
+ geometry: koffi.struct({
104
+ base_width: 'unsigned',
105
+ base_height: 'unsigned',
106
+ max_width: 'unsigned',
107
+ max_height: 'unsigned',
108
+ aspect_ratio: 'float',
109
+ }),
110
+ timing: koffi.struct({
111
+ fps: 'double',
112
+ sample_rate: 'double',
113
+ }),
114
+ });
115
+
116
+ // Callback types
117
+ const retro_video_refresh_t = koffi.proto(
118
+ 'void retro_video_refresh_t(const void* data, unsigned width, unsigned height, size_t pitch)'
119
+ );
120
+ const retro_audio_sample_t = koffi.proto(
121
+ 'void retro_audio_sample_t(int16_t left, int16_t right)'
122
+ );
123
+ const retro_audio_sample_batch_t = koffi.proto(
124
+ 'size_t retro_audio_sample_batch_t(const int16_t* data, size_t frames)'
125
+ );
126
+ const retro_input_poll_t = koffi.proto('void retro_input_poll_t()');
127
+ const retro_input_state_t = koffi.proto(
128
+ 'int16_t retro_input_state_t(unsigned port, unsigned device, unsigned index, unsigned id)'
129
+ );
130
+ const retro_environment_t = koffi.proto(
131
+ 'bool retro_environment_t(unsigned cmd, void* data)'
132
+ );
133
+
134
+ export class LibretroAPI {
135
+ private lib: koffi.IKoffiLib;
136
+
137
+ // Core functions
138
+ retro_init: () => void;
139
+ retro_deinit: () => void;
140
+ retro_api_version: () => number;
141
+ retro_get_system_info: (info: any) => void;
142
+ retro_get_system_av_info: (info: any) => void;
143
+ retro_set_controller_port_device: (port: number, device: number) => void;
144
+ retro_reset: () => void;
145
+ retro_run: () => void;
146
+ retro_load_game: (game: any) => boolean;
147
+ retro_unload_game: () => void;
148
+
149
+ // Serialization
150
+ retro_serialize_size: () => number;
151
+ retro_serialize: (data: Buffer, size: number) => boolean;
152
+ retro_unserialize: (data: Buffer, size: number) => boolean;
153
+
154
+ // Memory access
155
+ retro_get_memory_data: (id: number) => Buffer | null;
156
+ retro_get_memory_size: (id: number) => number;
157
+
158
+ // Callback setters
159
+ retro_set_video_refresh: (cb: any) => void;
160
+ retro_set_audio_sample: (cb: any) => void;
161
+ retro_set_audio_sample_batch: (cb: any) => void;
162
+ retro_set_input_poll: (cb: any) => void;
163
+ retro_set_input_state: (cb: any) => void;
164
+ retro_set_environment: (cb: any) => void;
165
+
166
+ constructor(corePath: string) {
167
+ this.lib = koffi.load(corePath);
168
+ this.bindFunctions();
169
+ }
170
+
171
+ private bindFunctions(): void {
172
+ this.retro_init = this.lib.func('void retro_init()');
173
+ this.retro_deinit = this.lib.func('void retro_deinit()');
174
+ this.retro_api_version = this.lib.func('unsigned retro_api_version()');
175
+ this.retro_run = this.lib.func('void retro_run()');
176
+ this.retro_reset = this.lib.func('void retro_reset()');
177
+ this.retro_load_game = this.lib.func('bool retro_load_game(retro_game_info*)');
178
+ this.retro_unload_game = this.lib.func('void retro_unload_game()');
179
+ // ... bind remaining functions
180
+ }
181
+
182
+ destroy(): void {
183
+ this.lib.unload();
184
+ }
185
+ }
186
+ ```
187
+
188
+ ### Memory Constants
189
+
190
+ ```typescript
191
+ // Memory region IDs for retro_get_memory_data/size
192
+ export const RETRO_MEMORY = {
193
+ SAVE_RAM: 0, // Battery-backed save RAM
194
+ RTC: 1, // Real-time clock
195
+ SYSTEM_RAM: 2, // Main system RAM
196
+ VIDEO_RAM: 3, // Video RAM
197
+ } as const;
198
+
199
+ // Device types for retro_set_controller_port_device
200
+ export const RETRO_DEVICE = {
201
+ NONE: 0,
202
+ JOYPAD: 1,
203
+ MOUSE: 2,
204
+ KEYBOARD: 3,
205
+ LIGHTGUN: 4,
206
+ ANALOG: 5,
207
+ } as const;
208
+
209
+ // Joypad button IDs for retro_input_state
210
+ export const RETRO_DEVICE_ID_JOYPAD = {
211
+ B: 0,
212
+ Y: 1,
213
+ SELECT: 2,
214
+ START: 3,
215
+ UP: 4,
216
+ DOWN: 5,
217
+ LEFT: 6,
218
+ RIGHT: 7,
219
+ A: 8,
220
+ X: 9,
221
+ L: 10,
222
+ R: 11,
223
+ L2: 12,
224
+ R2: 13,
225
+ L3: 14,
226
+ R3: 15,
227
+ } as const;
228
+ ```
229
+
230
+ ---
231
+
232
+ ## Environment Callback
233
+
234
+ The environment callback is how cores query frontend capabilities and configure behavior. This is the most complex part of the integration.
235
+
236
+ ### Environment Commands
237
+
238
+ ```typescript
239
+ // src/cores/libretro/environment.ts
240
+
241
+ // Subset of environment commands (full list has 100+ commands)
242
+ export const RETRO_ENVIRONMENT = {
243
+ // Video
244
+ SET_PIXEL_FORMAT: 10, // Request pixel format (0555, XRGB8888, RGB565)
245
+ GET_SYSTEM_DIRECTORY: 9, // Path to system files (BIOS)
246
+ SET_GEOMETRY: 37, // Change video geometry mid-game
247
+ GET_VARIABLE: 15, // Get core option value
248
+ SET_VARIABLES: 16, // Define core options
249
+
250
+ // Input
251
+ SET_INPUT_DESCRIPTORS: 31, // Describe input layout
252
+ GET_INPUT_BITMASKS: 52, // Request bitmask input polling
253
+
254
+ // Save
255
+ GET_SAVE_DIRECTORY: 31, // Path for save files
256
+
257
+ // Info
258
+ GET_LOG_INTERFACE: 27, // Logging callback
259
+ SET_SUPPORT_NO_GAME: 18, // Core can run without ROM
260
+ GET_CORE_OPTIONS_VERSION: 52, // Options API version
261
+ } as const;
262
+
263
+ // Pixel formats
264
+ export const RETRO_PIXEL_FORMAT = {
265
+ XRGB1555: 0, // 15-bit, X ignored
266
+ XRGB8888: 1, // 32-bit XRGB
267
+ RGB565: 2, // 16-bit RGB
268
+ } as const;
269
+
270
+ export class EnvironmentHandler {
271
+ private pixelFormat = RETRO_PIXEL_FORMAT.XRGB1555;
272
+ private variables = new Map<string, string>();
273
+ private systemDirectory = './system';
274
+ private saveDirectory = './saves';
275
+
276
+ /** Handle environment callback from core */
277
+ handle(cmd: number, data: Buffer | null): boolean {
278
+ switch (cmd) {
279
+ case RETRO_ENVIRONMENT.SET_PIXEL_FORMAT:
280
+ if (data) {
281
+ this.pixelFormat = data.readUInt32LE(0);
282
+ return this.pixelFormat <= RETRO_PIXEL_FORMAT.RGB565;
283
+ }
284
+ return false;
285
+
286
+ case RETRO_ENVIRONMENT.GET_SYSTEM_DIRECTORY:
287
+ if (data) {
288
+ // Write string pointer to data
289
+ // Complex: requires allocating native string
290
+ }
291
+ return true;
292
+
293
+ case RETRO_ENVIRONMENT.GET_VARIABLE:
294
+ // Core requesting option value
295
+ return this.handleGetVariable(data);
296
+
297
+ case RETRO_ENVIRONMENT.SET_VARIABLES:
298
+ // Core defining available options
299
+ return this.handleSetVariables(data);
300
+
301
+ case RETRO_ENVIRONMENT.GET_LOG_INTERFACE:
302
+ // Provide logging callback
303
+ return this.handleLogInterface(data);
304
+
305
+ case RETRO_ENVIRONMENT.SET_INPUT_DESCRIPTORS:
306
+ // Core describing its input layout
307
+ return true; // Accept but we use our own mapping
308
+
309
+ default:
310
+ // Unknown command - return false
311
+ return false;
312
+ }
313
+ }
314
+
315
+ getPixelFormat(): number {
316
+ return this.pixelFormat;
317
+ }
318
+
319
+ private handleGetVariable(data: Buffer | null): boolean {
320
+ // Parse retro_variable struct, look up value, write back
321
+ return false; // Stub
322
+ }
323
+
324
+ private handleSetVariables(data: Buffer | null): boolean {
325
+ // Parse variable definitions, store defaults
326
+ return true; // Stub
327
+ }
328
+
329
+ private handleLogInterface(data: Buffer | null): boolean {
330
+ // Provide logging callback
331
+ return false; // Stub - logging not implemented
332
+ }
333
+ }
334
+ ```
335
+
336
+ ### Callback Registration
337
+
338
+ ```typescript
339
+ // src/cores/libretro/callbacks.ts
340
+
341
+ import koffi from 'koffi';
342
+
343
+ export class CallbackManager {
344
+ private videoCallback: koffi.IKoffiRegisteredCallback | null = null;
345
+ private audioCallback: koffi.IKoffiRegisteredCallback | null = null;
346
+ private inputPollCallback: koffi.IKoffiRegisteredCallback | null = null;
347
+ private inputStateCallback: koffi.IKoffiRegisteredCallback | null = null;
348
+ private environmentCallback: koffi.IKoffiRegisteredCallback | null = null;
349
+
350
+ // Current frame data
351
+ framebuffer: Uint8Array | null = null;
352
+ frameWidth = 0;
353
+ frameHeight = 0;
354
+ framePitch = 0;
355
+
356
+ // Audio buffer
357
+ audioBuffer: Int16Array = new Int16Array(4096);
358
+ audioSamples = 0;
359
+
360
+ // Input state
361
+ private buttonState = new Map<number, Map<number, boolean>>();
362
+
363
+ constructor(private envHandler: EnvironmentHandler) {}
364
+
365
+ createCallbacks(api: LibretroAPI): void {
366
+ // Video refresh callback
367
+ this.videoCallback = koffi.register(
368
+ (data: Buffer, width: number, height: number, pitch: number) => {
369
+ this.frameWidth = width;
370
+ this.frameHeight = height;
371
+ this.framePitch = pitch;
372
+
373
+ // Copy framebuffer (data may be invalidated after callback returns)
374
+ const size = height * pitch;
375
+ if (!this.framebuffer || this.framebuffer.length < size) {
376
+ this.framebuffer = new Uint8Array(size);
377
+ }
378
+ data.copy(this.framebuffer, 0, 0, size);
379
+ },
380
+ koffi.proto('void (*)(const void*, unsigned, unsigned, size_t)')
381
+ );
382
+
383
+ // Audio sample batch callback
384
+ this.audioCallback = koffi.register(
385
+ (data: Buffer, frames: number): number => {
386
+ // Copy interleaved stereo samples
387
+ const samples = frames * 2;
388
+ if (this.audioSamples + samples > this.audioBuffer.length) {
389
+ // Grow buffer
390
+ const newBuffer = new Int16Array(this.audioBuffer.length * 2);
391
+ newBuffer.set(this.audioBuffer);
392
+ this.audioBuffer = newBuffer;
393
+ }
394
+ for (let i = 0; i < samples; i++) {
395
+ this.audioBuffer[this.audioSamples++] = data.readInt16LE(i * 2);
396
+ }
397
+ return frames;
398
+ },
399
+ koffi.proto('size_t (*)(const int16_t*, size_t)')
400
+ );
401
+
402
+ // Input poll callback (called before input_state queries)
403
+ this.inputPollCallback = koffi.register(
404
+ () => {
405
+ // Nothing to do - we update state externally
406
+ },
407
+ koffi.proto('void (*)()')
408
+ );
409
+
410
+ // Input state callback
411
+ this.inputStateCallback = koffi.register(
412
+ (port: number, device: number, index: number, id: number): number => {
413
+ if (device !== RETRO_DEVICE.JOYPAD) return 0;
414
+ const portState = this.buttonState.get(port);
415
+ return portState?.get(id) ? 1 : 0;
416
+ },
417
+ koffi.proto('int16_t (*)(unsigned, unsigned, unsigned, unsigned)')
418
+ );
419
+
420
+ // Environment callback
421
+ this.environmentCallback = koffi.register(
422
+ (cmd: number, data: Buffer | null): boolean => {
423
+ return this.envHandler.handle(cmd, data);
424
+ },
425
+ koffi.proto('bool (*)(unsigned, void*)')
426
+ );
427
+
428
+ // Register callbacks with core
429
+ api.retro_set_environment(this.environmentCallback);
430
+ api.retro_set_video_refresh(this.videoCallback);
431
+ api.retro_set_audio_sample_batch(this.audioCallback);
432
+ api.retro_set_input_poll(this.inputPollCallback);
433
+ api.retro_set_input_state(this.inputStateCallback);
434
+ }
435
+
436
+ setButtonState(port: number, button: number, pressed: boolean): void {
437
+ let portState = this.buttonState.get(port);
438
+ if (!portState) {
439
+ portState = new Map();
440
+ this.buttonState.set(port, portState);
441
+ }
442
+ portState.set(button, pressed);
443
+ }
444
+
445
+ getButtonState(port: number): Map<number, boolean> {
446
+ return this.buttonState.get(port) ?? new Map();
447
+ }
448
+
449
+ /** Drain audio buffer and return samples */
450
+ drainAudio(): Float32Array {
451
+ const samples = new Float32Array(this.audioSamples);
452
+ for (let i = 0; i < this.audioSamples; i++) {
453
+ samples[i] = this.audioBuffer[i] / 32768;
454
+ }
455
+ this.audioSamples = 0;
456
+ return samples;
457
+ }
458
+
459
+ destroy(): void {
460
+ // Callbacks are cleaned up when koffi lib is unloaded
461
+ this.videoCallback = null;
462
+ this.audioCallback = null;
463
+ this.inputPollCallback = null;
464
+ this.inputStateCallback = null;
465
+ this.environmentCallback = null;
466
+ }
467
+ }
468
+ ```
469
+
470
+ ---
471
+
472
+ ## LibretroCore Implementation
473
+
474
+ ### Core Class
475
+
476
+ ```typescript
477
+ // src/cores/libretro/index.ts
478
+
479
+ import { readFileSync } from 'fs';
480
+ import { basename, extname } from 'path';
481
+ import { Core, SystemInfo, AudioConfig, CoreState, ButtonDefinition } from '../../core/core.js';
482
+ import { LibretroAPI } from './api.js';
483
+ import { EnvironmentHandler, RETRO_PIXEL_FORMAT } from './environment.js';
484
+ import { CallbackManager, RETRO_DEVICE_ID_JOYPAD } from './callbacks.js';
485
+ import { registerCore } from '../../frontend/core-registry.js';
486
+
487
+ export class LibretroCore implements Core {
488
+ private api: LibretroAPI;
489
+ private envHandler: EnvironmentHandler;
490
+ private callbacks: CallbackManager;
491
+ private systemInfo: SystemInfo;
492
+ private romPath = '';
493
+ private romData: Buffer | null = null;
494
+
495
+ constructor(corePath: string) {
496
+ this.envHandler = new EnvironmentHandler();
497
+ this.api = new LibretroAPI(corePath);
498
+ this.callbacks = new CallbackManager(this.envHandler);
499
+
500
+ // Set environment callback before init (required by some cores)
501
+ this.callbacks.createCallbacks(this.api);
502
+
503
+ // Initialize core
504
+ this.api.retro_init();
505
+
506
+ // Get system info
507
+ this.systemInfo = this.buildSystemInfo();
508
+ }
509
+
510
+ private buildSystemInfo(): SystemInfo {
511
+ const info = {} as any;
512
+ this.api.retro_get_system_info(info);
513
+
514
+ const avInfo = { geometry: {}, timing: {} } as any;
515
+ // Note: av_info not available until game is loaded for some cores
516
+
517
+ return {
518
+ id: `libretro-${info.library_name.toLowerCase().replace(/\s+/g, '-')}`,
519
+ name: `${info.library_name} (libretro)`,
520
+ extensions: info.valid_extensions.split('|').map((e: string) => `.${e}`),
521
+ width: 320, // Updated after ROM load
522
+ height: 240, // Updated after ROM load
523
+ fps: 60, // Updated after ROM load
524
+ sampleRate: 44100,
525
+ pixelAspectRatio: 1,
526
+ maxPlayers: 2,
527
+ buttons: this.getDefaultButtons(),
528
+ colorSpace: 'rgb24', // We convert internally
529
+ };
530
+ }
531
+
532
+ private getDefaultButtons(): ButtonDefinition[] {
533
+ // Standard libretro joypad layout
534
+ return [
535
+ { id: RETRO_DEVICE_ID_JOYPAD.A, name: 'A', defaultKey: 'k', defaultGamepad: 'A' },
536
+ { id: RETRO_DEVICE_ID_JOYPAD.B, name: 'B', defaultKey: 'j', defaultGamepad: 'B' },
537
+ { id: RETRO_DEVICE_ID_JOYPAD.X, name: 'X', defaultKey: 'i', defaultGamepad: 'X' },
538
+ { id: RETRO_DEVICE_ID_JOYPAD.Y, name: 'Y', defaultKey: 'u', defaultGamepad: 'Y' },
539
+ { id: RETRO_DEVICE_ID_JOYPAD.L, name: 'L', defaultKey: 'q', defaultGamepad: 'LB' },
540
+ { id: RETRO_DEVICE_ID_JOYPAD.R, name: 'R', defaultKey: 'e', defaultGamepad: 'RB' },
541
+ { id: RETRO_DEVICE_ID_JOYPAD.SELECT, name: 'Select', defaultKey: ' ', defaultGamepad: 'Back' },
542
+ { id: RETRO_DEVICE_ID_JOYPAD.START, name: 'Start', defaultKey: 'Enter', defaultGamepad: 'Start' },
543
+ { id: RETRO_DEVICE_ID_JOYPAD.UP, name: 'Up', defaultKey: 'w', defaultGamepad: 'DPadUp' },
544
+ { id: RETRO_DEVICE_ID_JOYPAD.DOWN, name: 'Down', defaultKey: 's', defaultGamepad: 'DPadDown' },
545
+ { id: RETRO_DEVICE_ID_JOYPAD.LEFT, name: 'Left', defaultKey: 'a', defaultGamepad: 'DPadLeft' },
546
+ { id: RETRO_DEVICE_ID_JOYPAD.RIGHT, name: 'Right', defaultKey: 'd', defaultGamepad: 'DPadRight' },
547
+ ];
548
+ }
549
+
550
+ getSystemInfo(): SystemInfo {
551
+ return this.systemInfo;
552
+ }
553
+
554
+ loadRom(romPath: string): void {
555
+ this.romPath = romPath;
556
+ this.romData = readFileSync(romPath);
557
+
558
+ const gameInfo = {
559
+ path: romPath,
560
+ data: this.romData,
561
+ size: this.romData.length,
562
+ meta: null,
563
+ };
564
+
565
+ const success = this.api.retro_load_game(gameInfo);
566
+ if (!success) {
567
+ throw new Error(`Failed to load ROM: ${romPath}`);
568
+ }
569
+
570
+ // Update system info with actual values
571
+ const avInfo = { geometry: {}, timing: {} } as any;
572
+ this.api.retro_get_system_av_info(avInfo);
573
+
574
+ this.systemInfo.width = avInfo.geometry.base_width;
575
+ this.systemInfo.height = avInfo.geometry.base_height;
576
+ this.systemInfo.fps = avInfo.timing.fps;
577
+ this.systemInfo.sampleRate = avInfo.timing.sample_rate;
578
+ this.systemInfo.pixelAspectRatio = avInfo.geometry.aspect_ratio || 1;
579
+
580
+ // Set up controller ports
581
+ this.api.retro_set_controller_port_device(0, RETRO_DEVICE.JOYPAD);
582
+ this.api.retro_set_controller_port_device(1, RETRO_DEVICE.JOYPAD);
583
+ }
584
+
585
+ reset(): void {
586
+ this.api.retro_reset();
587
+ }
588
+
589
+ destroy(): void {
590
+ this.api.retro_unload_game();
591
+ this.api.retro_deinit();
592
+ this.callbacks.destroy();
593
+ this.api.destroy();
594
+ }
595
+
596
+ runFrame(): void {
597
+ this.api.retro_run();
598
+
599
+ // Push audio samples if callback is set
600
+ if (this.audioCallback && this.callbacks.audioSamples > 0) {
601
+ const samples = this.callbacks.drainAudio();
602
+ this.audioCallback(samples);
603
+ }
604
+ }
605
+
606
+ isFrameComplete(): boolean {
607
+ return true; // libretro cores always complete one frame per run()
608
+ }
609
+
610
+ getFramebuffer(): Uint8Array {
611
+ const fb = this.callbacks.framebuffer;
612
+ if (!fb) return new Uint8Array(0);
613
+
614
+ // Convert to RGB24 based on pixel format
615
+ return this.convertFramebuffer(
616
+ fb,
617
+ this.callbacks.frameWidth,
618
+ this.callbacks.frameHeight,
619
+ this.callbacks.framePitch
620
+ );
621
+ }
622
+
623
+ private convertFramebuffer(
624
+ data: Uint8Array,
625
+ width: number,
626
+ height: number,
627
+ pitch: number
628
+ ): Uint8Array {
629
+ const format = this.envHandler.getPixelFormat();
630
+ const output = new Uint8Array(width * height * 3);
631
+ let outIdx = 0;
632
+
633
+ for (let y = 0; y < height; y++) {
634
+ const rowOffset = y * pitch;
635
+
636
+ for (let x = 0; x < width; x++) {
637
+ let r: number, g: number, b: number;
638
+
639
+ switch (format) {
640
+ case RETRO_PIXEL_FORMAT.XRGB8888: {
641
+ const idx = rowOffset + x * 4;
642
+ b = data[idx];
643
+ g = data[idx + 1];
644
+ r = data[idx + 2];
645
+ break;
646
+ }
647
+ case RETRO_PIXEL_FORMAT.RGB565: {
648
+ const idx = rowOffset + x * 2;
649
+ const pixel = data[idx] | (data[idx + 1] << 8);
650
+ r = ((pixel >> 11) & 0x1f) << 3;
651
+ g = ((pixel >> 5) & 0x3f) << 2;
652
+ b = (pixel & 0x1f) << 3;
653
+ break;
654
+ }
655
+ case RETRO_PIXEL_FORMAT.XRGB1555:
656
+ default: {
657
+ const idx = rowOffset + x * 2;
658
+ const pixel = data[idx] | (data[idx + 1] << 8);
659
+ r = ((pixel >> 10) & 0x1f) << 3;
660
+ g = ((pixel >> 5) & 0x1f) << 3;
661
+ b = (pixel & 0x1f) << 3;
662
+ break;
663
+ }
664
+ }
665
+
666
+ output[outIdx++] = r;
667
+ output[outIdx++] = g;
668
+ output[outIdx++] = b;
669
+ }
670
+ }
671
+
672
+ return output;
673
+ }
674
+
675
+ // Audio
676
+ private audioCallback: ((samples: Float32Array) => void) | null = null;
677
+
678
+ getAudioConfig(): AudioConfig {
679
+ return {
680
+ sampleRate: this.systemInfo.sampleRate,
681
+ channels: 2, // libretro is always stereo
682
+ };
683
+ }
684
+
685
+ setAudioCallback(callback: ((samples: Float32Array) => void) | null): void {
686
+ this.audioCallback = callback;
687
+ }
688
+
689
+ // Input
690
+ setButtonState(port: number, button: number, pressed: boolean): void {
691
+ this.callbacks.setButtonState(port, button, pressed);
692
+ }
693
+
694
+ getButtonState(port: number): Map<number, boolean> {
695
+ return this.callbacks.getButtonState(port);
696
+ }
697
+
698
+ // State management
699
+ getState(): CoreState {
700
+ const size = this.api.retro_serialize_size();
701
+ const buffer = Buffer.alloc(size);
702
+ const success = this.api.retro_serialize(buffer, size);
703
+
704
+ return {
705
+ version: 1,
706
+ coreId: this.systemInfo.id,
707
+ gameId: this.romPath,
708
+ data: {
709
+ state: success ? buffer.toString('base64') : null,
710
+ },
711
+ };
712
+ }
713
+
714
+ setState(state: CoreState): void {
715
+ if (state.coreId !== this.systemInfo.id) {
716
+ throw new Error(`State core mismatch: expected ${this.systemInfo.id}, got ${state.coreId}`);
717
+ }
718
+
719
+ const stateData = state.data.state as string | null;
720
+ if (!stateData) return;
721
+
722
+ const buffer = Buffer.from(stateData, 'base64');
723
+ const success = this.api.retro_unserialize(buffer, buffer.length);
724
+
725
+ if (!success) {
726
+ throw new Error('Failed to load state');
727
+ }
728
+ }
729
+
730
+ getStateVersion(): number {
731
+ return 1;
732
+ }
733
+
734
+ // Battery save
735
+ hasBatterySave(): boolean {
736
+ return this.api.retro_get_memory_size(RETRO_MEMORY.SAVE_RAM) > 0;
737
+ }
738
+
739
+ getBatteryRam(): Uint8Array | null {
740
+ const size = this.api.retro_get_memory_size(RETRO_MEMORY.SAVE_RAM);
741
+ if (size === 0) return null;
742
+
743
+ const data = this.api.retro_get_memory_data(RETRO_MEMORY.SAVE_RAM);
744
+ if (!data) return null;
745
+
746
+ return new Uint8Array(data);
747
+ }
748
+
749
+ setBatteryRam(data: Uint8Array): void {
750
+ const ptr = this.api.retro_get_memory_data(RETRO_MEMORY.SAVE_RAM);
751
+ if (!ptr) return;
752
+
753
+ const size = this.api.retro_get_memory_size(RETRO_MEMORY.SAVE_RAM);
754
+ const copySize = Math.min(data.length, size);
755
+ data.copy(ptr, 0, 0, copySize);
756
+ }
757
+ }
758
+
759
+ // Memory constants
760
+ const RETRO_MEMORY = {
761
+ SAVE_RAM: 0,
762
+ RTC: 1,
763
+ SYSTEM_RAM: 2,
764
+ VIDEO_RAM: 3,
765
+ };
766
+
767
+ const RETRO_DEVICE = {
768
+ NONE: 0,
769
+ JOYPAD: 1,
770
+ MOUSE: 2,
771
+ KEYBOARD: 3,
772
+ LIGHTGUN: 4,
773
+ ANALOG: 5,
774
+ };
775
+ ```
776
+
777
+ ---
778
+
779
+ ## Core Discovery and Registration
780
+
781
+ ### Dynamic Core Loading
782
+
783
+ ```typescript
784
+ // src/cores/libretro/loader.ts
785
+
786
+ import { existsSync, readdirSync } from 'fs';
787
+ import { join, basename, extname } from 'path';
788
+ import { platform } from 'os';
789
+ import { LibretroCore } from './index.js';
790
+ import { registerCore } from '../../frontend/core-registry.js';
791
+
792
+ /** Platform-specific library extension */
793
+ function getLibraryExtension(): string {
794
+ switch (platform()) {
795
+ case 'darwin': return '.dylib';
796
+ case 'win32': return '.dll';
797
+ default: return '.so';
798
+ }
799
+ }
800
+
801
+ /** Scan directory for libretro cores and register them */
802
+ export function loadLibretroCores(coreDirectory: string): void {
803
+ if (!existsSync(coreDirectory)) return;
804
+
805
+ const ext = getLibraryExtension();
806
+ const files = readdirSync(coreDirectory).filter(f => f.endsWith(`_libretro${ext}`));
807
+
808
+ for (const file of files) {
809
+ const corePath = join(coreDirectory, file);
810
+ const coreName = basename(file, `_libretro${ext}`);
811
+
812
+ try {
813
+ // Create a temporary instance to get system info
814
+ const tempCore = new LibretroCore(corePath);
815
+ const info = tempCore.getSystemInfo();
816
+ tempCore.destroy();
817
+
818
+ // Register core factory
819
+ registerCore(info.id, {
820
+ create: () => new LibretroCore(corePath),
821
+ extensions: info.extensions,
822
+ name: info.name,
823
+ });
824
+
825
+ console.log(`Loaded libretro core: ${info.name}`);
826
+ } catch (error) {
827
+ console.warn(`Failed to load libretro core ${file}:`, error);
828
+ }
829
+ }
830
+ }
831
+ ```
832
+
833
+ ### CLI Integration
834
+
835
+ ```typescript
836
+ // In src/index.ts
837
+
838
+ import { loadLibretroCores } from './cores/libretro/loader.js';
839
+
840
+ // Load libretro cores from default locations
841
+ const coreSearchPaths = [
842
+ './cores', // Local cores directory
843
+ join(homedir(), '.config/emoemu/cores'), // User config
844
+ '/usr/lib/libretro', // System (Linux)
845
+ '/usr/local/lib/libretro', // Homebrew (macOS)
846
+ ];
847
+
848
+ for (const path of coreSearchPaths) {
849
+ loadLibretroCores(path);
850
+ }
851
+ ```
852
+
853
+ ---
854
+
855
+ ## Considerations and Challenges
856
+
857
+ ### Memory Management
858
+
859
+ 1. **Framebuffer Copying**: Native framebuffer must be copied in the callback since it may be invalidated after the callback returns.
860
+
861
+ 2. **String Handling**: Environment callbacks that return strings (paths) require careful native memory allocation.
862
+
863
+ 3. **Buffer Lifetimes**: ROM data must remain valid for the lifetime of the core.
864
+
865
+ ### Thread Safety
866
+
867
+ 1. **Single-Threaded**: Libretro cores expect single-threaded operation. Do not call core functions from multiple threads.
868
+
869
+ 2. **Callback Context**: Callbacks execute in the same thread as `retro_run()`.
870
+
871
+ ### Core Compatibility
872
+
873
+ Not all libretro cores will work:
874
+
875
+ | Category | Compatibility |
876
+ |----------|--------------|
877
+ | Software-rendered cores | Full support |
878
+ | OpenGL cores | Not supported (no GL context) |
879
+ | Vulkan cores | Not supported |
880
+ | Hardware-accelerated cores | Not supported |
881
+ | Cores requiring BIOS files | Requires user to provide BIOS |
882
+
883
+ ### Performance Considerations
884
+
885
+ 1. **FFI Overhead**: Each callback incurs FFI crossing overhead. The video callback is called once per frame, audio potentially multiple times.
886
+
887
+ 2. **Buffer Conversion**: Pixel format conversion adds CPU overhead. Consider caching converted buffers.
888
+
889
+ 3. **Memory Copies**: Minimize copies between native and JS buffers where possible.
890
+
891
+ ### Supported Cores (Initial Target)
892
+
893
+ | Core | System | Status |
894
+ |------|--------|--------|
895
+ | picodrive | Sega Genesis/MD, SMS, Game Gear, 32X, Sega CD | Target |
896
+ | gambatte | Game Boy / Game Boy Color | Target (alternative to native) |
897
+ | mgba | Game Boy Advance | Target |
898
+ | snes9x | SNES | Target (alternative to native) |
899
+ | nestopia | NES | Target (alternative to native) |
900
+ | fceumm | NES | Target (alternative to native) |
901
+
902
+ ---
903
+
904
+ ## Dependencies
905
+
906
+ ### Required Packages
907
+
908
+ ```json
909
+ {
910
+ "dependencies": {
911
+ "koffi": "^2.8.0"
912
+ }
913
+ }
914
+ ```
915
+
916
+ ### Alternative FFI Libraries
917
+
918
+ | Library | Pros | Cons |
919
+ |---------|------|------|
920
+ | koffi | Fast, modern, good callback support | Newer, less battle-tested |
921
+ | node-ffi-napi | Well-established, widely used | Slower, complex callback setup |
922
+ | sbffi | Very fast | Limited features |
923
+
924
+ **Recommendation**: Use `koffi` for its superior callback support and performance.
925
+
926
+ ---
927
+
928
+ ## Testing Strategy
929
+
930
+ ### Unit Tests
931
+
932
+ 1. **API Binding Tests**: Verify function signatures match libretro header
933
+ 2. **Environment Handler Tests**: Test command parsing and responses
934
+ 3. **Pixel Format Conversion Tests**: Verify correct color conversion
935
+
936
+ ### Integration Tests
937
+
938
+ 1. **Core Loading**: Test loading various core types
939
+ 2. **ROM Loading**: Test ROM load/unload cycle
940
+ 3. **Frame Execution**: Verify frame output and timing
941
+ 4. **State Save/Load**: Test serialization round-trip
942
+ 5. **Input Mapping**: Verify button state propagation
943
+
944
+ ### Manual Testing
945
+
946
+ 1. Test with reference cores (picodrive, gambatte)
947
+ 2. Verify audio/video sync
948
+ 3. Test save states across core restarts
949
+ 4. Verify battery save persistence
950
+
951
+ ---
952
+
953
+ ## Future Enhancements
954
+
955
+ 1. **Core Options UI**: Expose core-specific options (e.g., region, video filters)
956
+ 2. **Subsystem Support**: Support cores with multiple content types (e.g., BIOS + ROM)
957
+ 3. **Cheats**: Implement cheat code interface
958
+ 4. **Rewind**: Leverage libretro's serialization for rewind feature
959
+ 5. **Run-Ahead**: Reduce input latency using state save/load
960
+ 6. **Disk Control**: Support multi-disc games (PlayStation)
961
+
962
+ ---
963
+
964
+ ## Appendix: Libretro API Reference
965
+
966
+ ### Essential Functions
967
+
968
+ | Function | Description |
969
+ |----------|-------------|
970
+ | `retro_init()` | Initialize core (call once) |
971
+ | `retro_deinit()` | Cleanup core (call once) |
972
+ | `retro_load_game()` | Load ROM/content |
973
+ | `retro_unload_game()` | Unload ROM/content |
974
+ | `retro_run()` | Execute one frame |
975
+ | `retro_reset()` | Reset to power-on state |
976
+ | `retro_serialize()` | Save state to buffer |
977
+ | `retro_unserialize()` | Load state from buffer |
978
+
979
+ ### Callback Registration Order
980
+
981
+ ```
982
+ 1. retro_set_environment() # Before retro_init() for some cores
983
+ 2. retro_init()
984
+ 3. retro_set_video_refresh()
985
+ 4. retro_set_audio_sample_batch()
986
+ 5. retro_set_input_poll()
987
+ 6. retro_set_input_state()
988
+ 7. retro_load_game()
989
+ 8. retro_get_system_av_info() # After load_game()
990
+ 9. Main loop: retro_run()
991
+ ```
992
+
993
+ ### Resources
994
+
995
+ - [Libretro Documentation](https://docs.libretro.com/)
996
+ - [libretro.h Header](https://github.com/libretro/libretro-common/blob/master/include/libretro.h)
997
+ - [RetroArch Core Downloads](https://buildbot.libretro.com/nightly/)