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
package/TODO.md ADDED
@@ -0,0 +1,655 @@
1
+ # emoemu Development TODO
2
+
3
+ ## Milestone 1: Core Emulation (MVP)
4
+
5
+ ### CPU
6
+ - [x] Implement all 151 official 6502 opcodes
7
+ - [x] Implement all 13 addressing modes
8
+ - [x] Cycle-accurate timing
9
+ - [x] Interrupt handling (NMI, IRQ, Reset)
10
+ - [ ] Unofficial/illegal opcodes (for better compatibility)
11
+ - [ ] Decimal mode (low priority - unused by NES)
12
+
13
+ ### PPU - Background
14
+ - [x] Nametable rendering
15
+ - [x] Pattern table decoding
16
+ - [x] Attribute table palette selection
17
+ - [x] Horizontal scrolling (coarse + fine X)
18
+ - [x] Vertical scrolling (coarse + fine Y)
19
+ - [x] Nametable mirroring (H, V, single-screen)
20
+ - [ ] Mid-frame scroll changes
21
+ - [ ] Fine scroll timing accuracy
22
+
23
+ ### PPU - Sprites
24
+ - [x] OAM (Object Attribute Memory) rendering
25
+ - [x] 8x8 sprite support
26
+ - [x] 8x16 sprite support
27
+ - [x] Sprite priority (front/behind background)
28
+ - [x] Sprite 0 hit detection
29
+ - [x] 8 sprites per scanline limit
30
+ - [x] Sprite overflow flag
31
+
32
+ ### Memory
33
+ - [x] 2KB internal RAM with mirroring
34
+ - [x] PPU register access ($2000-$2007)
35
+ - [x] OAM DMA ($4014)
36
+ - [x] Controller ports ($4016-$4017)
37
+ - [x] APU registers ($4000-$4017)
38
+
39
+ ### Cartridge
40
+ - [x] iNES file format parsing
41
+ - [x] PRG-ROM loading
42
+ - [x] CHR-ROM loading
43
+ - [x] CHR-RAM support
44
+ - [ ] NES 2.0 format support
45
+ - [ ] Battery-backed save RAM persistence
46
+
47
+ ---
48
+
49
+ ## Milestone 2: Mapper Support
50
+
51
+ ### Implemented
52
+ - [x] Mapper 0 (NROM) - No banking
53
+ - [x] Mapper 1 (MMC1) - Bank switching + mirroring control
54
+ - [x] Mapper 2 (UxROM) - PRG bank switching
55
+ - [x] Mapper 3 (CNROM) - CHR bank switching
56
+ - [x] Mapper 4 (MMC3) - Scanline counter, IRQ
57
+ - [x] Mapper 7 (AxROM) - Single-screen mirroring
58
+ - [x] Mapper 9 (MMC2) - Punch-Out tile-based CHR switching
59
+
60
+ ### Extended Mappers
61
+ - [ ] Mapper 11 (Color Dreams)
62
+ - [ ] Mapper 66 (GxROM)
63
+ - [ ] Mapper 71 (Camerica)
64
+
65
+ ---
66
+
67
+ ## Milestone 3: Audio (APU)
68
+
69
+ ### Channels
70
+ - [x] Pulse 1 channel
71
+ - [x] Pulse 2 channel
72
+ - [x] Triangle channel
73
+ - [x] Noise channel
74
+ - [x] DMC (Delta Modulation Channel)
75
+
76
+ ### APU Features
77
+ - [x] Frame counter / sequencer
78
+ - [x] Length counters
79
+ - [x] Envelope generators
80
+ - [x] Sweep units (pulse channels)
81
+ - [x] Linear counter (triangle)
82
+ - [x] Audio output mixing (NESDev wiki formula)
83
+
84
+ ### Audio Output
85
+ - [x] Audio via audify/RtAudio (22050 Hz stereo)
86
+ - [x] Optional audio disable (--no-audio flag)
87
+ - [x] Audio sync with emulation (cycle-based sampling)
88
+ - [x] Sample buffering for fixed-size frame output
89
+
90
+ ### Audio Architecture Improvements
91
+ - [x] Use `frameOutputCallback` for flow control (replace manual timing sync)
92
+ - [x] Leverage RtAudio's internal queue (write smaller chunks more frequently)
93
+ - [x] Fixed-size ring buffer (prevent unbounded memory growth)
94
+ - [x] Higher sample rate (44100 Hz for better quality)
95
+ - [x] Error callback for graceful error recovery
96
+ - [x] Smaller frame size (10ms for lower latency)
97
+
98
+ ---
99
+
100
+ ## Milestone 4: Input & Controls
101
+
102
+ ### NES Controller Behavior
103
+ The NES controller uses a 4021 shift register that captures the state of all 8 buttons each frame when polled. Buttons can be held continuously (not rapid presses) and multiple buttons can be pressed simultaneously.
104
+
105
+ **Platform Note:** Input uses Kitty keyboard protocol for true keydown/keyup events. Requires Kitty terminal or a terminal that supports the Kitty keyboard protocol.
106
+
107
+ - [x] Shift register emulation ($4016, $4017)
108
+ - [x] Button state capture on strobe
109
+ - [x] Sequential bit reading
110
+ - [x] True button hold support (using Kitty keyboard protocol)
111
+ - [x] Simultaneous multi-button input
112
+ - [x] Per-frame state polling accuracy (InputManager.update() called each frame)
113
+
114
+ ### Keyboard Input
115
+ - [x] Basic keyboard input
116
+ - [x] D-pad mapping (WASD + arrows)
117
+ - [x] A/B button mapping
118
+ - [x] Start/Select mapping
119
+ - [x] Proper key-down/key-up handling (Kitty keyboard protocol)
120
+ - [x] Simultaneous button press support
121
+ - [x] Controller 1 keyboard support
122
+ - [ ] Controller 2 keyboard support
123
+ - [ ] Configurable key bindings (config file)
124
+
125
+ ### Gamepad Support
126
+ - [x] HID gamepad support via node-hid
127
+ - [x] Controller profiles (Xbox, PlayStation, Nintendo, 8BitDo)
128
+ - [x] Hotplug detection (3-second polling)
129
+ - [x] Controller 1 gamepad support
130
+ - [x] Controller 2 gamepad support
131
+ - [x] --list-gamepads and --debug-gamepad options
132
+
133
+ ### Advanced Input
134
+ - [ ] Turbo A/B buttons
135
+ - [ ] Zapper light gun (mouse-based?)
136
+ - [ ] Save/load state hotkeys
137
+
138
+ ---
139
+
140
+ ## Milestone 5: Rendering & Display
141
+
142
+ ### Kitty Graphics Renderer
143
+ - [x] Kitty graphics protocol support
144
+ - [x] Auto-scale to fit terminal
145
+ - [x] Aspect ratio correction (4:3)
146
+ - [x] Diff-based updates
147
+ - [x] Dynamic terminal resize detection
148
+
149
+ ### Terminal Renderer (Half-blocks)
150
+ - [x] Unicode half-block characters (▀)
151
+ - [x] True color (24-bit) support
152
+ - [x] Aspect ratio correction (4:3)
153
+ - [x] Dynamic terminal resize detection
154
+ - [ ] ANSI 256-color fallback
155
+ - [ ] Diff-based rendering (only update changed characters)
156
+
157
+ ### ASCII Renderer
158
+ - [x] ASCII grayscale mode
159
+ - [x] Optional color support
160
+ - [x] Configurable resolution
161
+ - [x] Dynamic terminal resize detection
162
+
163
+ ### Display Features
164
+ - [ ] Configurable palette (different NES palettes)
165
+ - [ ] Grayscale mode via PPU mask
166
+ - [ ] Color emphasis bits
167
+
168
+ ### CRT Effects (Kitty Renderer)
169
+ Post-processing effects for authentic retro display in Kitty graphics mode.
170
+
171
+ - [x] Gamma correction (`--gamma`) - Darken midtones for CRT-like contrast
172
+ - [x] Scanlines (`--scanlines`) - Horizontal line darkening to simulate CRT phosphor gaps
173
+ - [x] Color saturation (`--saturation`) - Boost color vibrancy like CRT displays
174
+ - [x] Brightness adjustment (`--brightness`) - Shift overall luminance levels
175
+ - [x] Contrast adjustment (`--contrast`) - Expand/compress tonal range around midpoint
176
+ - [x] Vignette (`--vignette`) - Darken screen edges to simulate CRT electron beam falloff
177
+
178
+ #### Future CRT Effects (More Complex)
179
+ - [x] Phosphor bloom/glow - Bright pixels bleed into neighbors
180
+ - [x] NTSC color artifacts - Horizontal color bleeding from composite video
181
+ - [ ] Color temperature/tint - Warmer CRT-like color balance
182
+ - [x] CRT curvature - Barrel distortion to simulate curved CRT screens
183
+ - [ ] Shadow mask / Aperture grille - Phosphor pattern overlay (dots or vertical lines)
184
+ - [ ] Sharpness control - Sharpen or blur the image
185
+ - [ ] Phosphor persistence / ghosting - Faint trails from slow phosphor decay
186
+ - [ ] Chromatic aberration - RGB color fringing at screen edges
187
+ - [ ] LCD grid effect - Visible pixel boundaries for handheld simulation (GB, GBA)
188
+ - [ ] Signal noise - Subtle static/grain for analog feel
189
+ - [ ] Interlace simulation - Flickering/combing for interlaced output
190
+
191
+ ---
192
+
193
+ ## Milestone 6: Performance & Optimization
194
+
195
+ ### Quick Wins (Low Effort, High Impact)
196
+ - [x] Reuse Kitty RGB buffer (`ppu/kitty-renderer.ts:109`)
197
+ - Currently allocates 184KB every frame (11MB/sec GC pressure)
198
+ - Move `new Uint8Array()` to class property, reuse across frames
199
+ - [x] Audio buffer pool (`emulator.ts:370`)
200
+ - New buffer allocated per sample batch (~11×/frame)
201
+ - Use 2-3 pre-allocated buffers and rotate
202
+ - [x] Palette color escape sequence cache (`ppu/palette.ts` + `ppu/renderer.ts`)
203
+ - ANSI escape sequences generated per-pixel (61,440×/frame)
204
+ - Pre-compute lookup table of 64 formatted strings at init
205
+
206
+ ### Medium Effort Optimizations
207
+ - [ ] Memory bus dispatch table (`memory/bus.ts:47-103`)
208
+ - Sequential if-else checks on every memory access (millions/frame)
209
+ - Use page lookup table: `handlers[address >> 8](address)` for O(1) dispatch
210
+ - [x] String concatenation in renderer (`ppu/renderer.ts:36-104`)
211
+ - Uses `+=` in hot loop (7,680 concatenations/frame)
212
+ - Switch to array + `.join('')`
213
+ - [x] Sprite bit reversal lookup table (`ppu/ppu.ts:365-452`)
214
+ - `reverseBits()` called up to 8× per scanline with 6 bit ops each
215
+ - Pre-compute 256-entry lookup table
216
+ - [x] Reusable DMA temp buffer (`memory/bus.ts:112`)
217
+ - 256-byte allocation per OAM DMA
218
+ - Use reusable class-level buffer
219
+
220
+ ### Larger Refactors (Highest Impact)
221
+ - [x] PPU tile data caching (`ppu/ppu.ts:464-596`)
222
+ - `renderPixel()` does 4-6 PPU reads per pixel (61,440×/frame)
223
+ - Cache tile pattern data at start of each 8-pixel tile span
224
+ - Pre-compute attribute shifts per tile instead of per pixel
225
+ - [x] ASCII luminance lookup table (`ppu/renderer.ts:108-110`)
226
+ - Luminance calculated per pixel in ASCII mode
227
+ - Pre-compute for all 64 palette colors at init
228
+
229
+ ### ROM Scanning & Playlist Optimizations
230
+
231
+ #### Completed
232
+ - [x] Consolidated file I/O - single file open for binary check + metadata extraction
233
+ - [x] Directory listing cache - avoid repeated stat() calls for save state/battery checks
234
+ - [x] Smart header read sizing - read only what's needed per ROM format (512B-66KB)
235
+ - [x] CRC32 caching - reuse CRCs from existing playlists on updates
236
+ - [x] Playlist index - O(1) runtime update lookups via `buildPlaylistIndex()`
237
+ - [x] Case-sensitive filesystem support - use `realpathSync()` for path normalization
238
+
239
+ #### Quick Wins (Low Effort)
240
+ - [x] Fix `checkForBatterySave()` redundant I/O (`src/frontend/playlist/reader.ts:241-256`)
241
+ - Removed redundant `existsSync()` call - `statSync()` throws on non-existent files
242
+ - [x] Export and share `DirectoryCache` (`src/frontend/directory-cache.ts`)
243
+ - Created shared `src/frontend/directory-cache.ts` module
244
+ - Exported from `src/frontend/index.ts` for easy access
245
+ - Rom-scanner now imports from shared module
246
+ - [x] Deduplicate ROM sorting logic (`src/frontend/rom-scanner/index.ts`)
247
+ - Exported `sortRoms()` function from rom-scanner
248
+ - Replaced 2 inline sorts in rom-scanner and 2 in playlist reader
249
+
250
+ #### Medium Effort
251
+ - [x] Cache `findMatchingCores()` result in RomInfo (`src/frontend/core-registry.ts:73-88`)
252
+ - Added `coreIds: string[]` to RomInfo interface
253
+ - ROM scanner populates coreIds during scanning
254
+ - Playlist generation uses cached coreIds via `getCoreFactory()` instead of calling `findMatchingCores()` again
255
+ - [x] Pre-compute extension→cores map in core registry
256
+ - Added `extensionToCoresCache` map built lazily on first access
257
+ - `findMatchingCores()` now does O(1) lookup instead of filtering
258
+ - Cache invalidated when new cores are registered
259
+ - [x] Use smart header sizing in `extractMetadata()` for `getRomTitle()`
260
+ - Was using fixed 64KB, now uses `getRequiredHeaderSize()` per format
261
+ - [x] Use directory cache in playlist reader (`src/frontend/playlist/reader.ts`)
262
+ - `checkForSaveState()` and `checkForBatterySave()` now use cached directory listings
263
+ - Added `dirCache` option to `ConversionOptions` for shared cache across calls
264
+
265
+ #### High Effort (Major Improvements)
266
+ - [ ] Streaming CRC32 calculation (`src/utils/crc32.ts:68-77`)
267
+ - Currently loads entire ROM into memory: `readFileSync(filePath)`
268
+ - Implement streaming in 64KB chunks to reduce memory pressure
269
+ - Prevents memory spikes for large ROM collections
270
+ - [ ] Optional CRC32 skip during initial scan
271
+ - Add `--skip-crc` flag to use "DETECT" like RetroArch
272
+ - CRC only computed on-demand or during explicit playlist refresh
273
+ - [x] Path normalization cache
274
+ - Added `normalizedPathCache` Map to cache `realpathSync()` results
275
+ - Avoids repeated syscalls for the same path
276
+
277
+ #### Pending Optimizations (Identified 2026-01)
278
+
279
+ **High Priority**
280
+ - [x] Fix redundant file open in `validateRomFile()`
281
+ - Already uses `extractMetadataFromBuffer()` with existing header buffer
282
+ - No extra file open per validated ROM
283
+
284
+ - [x] Convert sync directory walk to async in `scanDirectoryAsync()`
285
+ - Added `countFilesAsync()` for non-blocking file counting
286
+ - Replaced sync `collectFilePaths()` with async generator `collectFilePathsAsync()`
287
+ - Uses `fs/promises` async I/O and yields control every 50 entries
288
+ - Progress bar preserved by doing async count pass first
289
+
290
+ **Medium Priority**
291
+ - [x] Remove unused `scanDirectoryWithProgress()` function
292
+ - Was dead code superseded by `scanDirectoryAsync()`
293
+ - Had duplicate file counting (2x directory I/O)
294
+ - Removed entirely since not used anywhere
295
+
296
+ - [x] Move metadata lookup tables to module level
297
+ - Added `GB_CARTRIDGE_TYPES`, `GB_ROM_SIZES`, `GB_RAM_SIZES` to consts.ts
298
+ - Added `SNES_CHIP_TYPES` to consts.ts
299
+ - Lookup tables now shared across all extraction calls
300
+
301
+ **Low Priority**
302
+ - [x] Cache `getSupportedExtensions()` result
303
+ - Added `supportedExtensionsCache` in core-registry.ts
304
+ - Cache invalidated when cores are registered
305
+ - Eliminates repeated array building during scans
306
+
307
+ #### ROM Browser Optimizations (Identified 2026-01)
308
+
309
+ **High Priority**
310
+ - [x] Memoize empty space and scrollbar arrays (`src/ui/RomBrowser/index.tsx`)
311
+ - Added `emptySpaceElements` and `scrollbarElements` memoized with `useMemo`
312
+ - Eliminates array recreation on every render
313
+
314
+ - [x] Create memoized option value→label lookup (`src/ui/RomBrowser/index.tsx`)
315
+ - Added `optionLookups` Map providing O(1) value→{label, index} lookups
316
+ - Replaced O(n) find/findIndex calls in display and input handlers
317
+
318
+ - [x] Reduce cascading re-renders from `localConfig` (`src/ui/RomBrowser/index.tsx`)
319
+ - Added early return guards to skip state updates when value unchanged
320
+ - Prevents unnecessary re-renders when pressing at option boundaries
321
+
322
+ - [x] Extract duplicate settings filter logic (`src/ui/RomBrowser/index.tsx`)
323
+ - Created `filterSettingsCategories()` helper function
324
+ - Shared by both `useMemo` hook and `useState` initializer
325
+
326
+ - [x] Optimize save state lazy-loading (`src/frontend/rom-scanner/index.ts`)
327
+ - Check file existence with `existsSync` before reading
328
+ - Sort by mtime to find newest file first
329
+ - Only read the newest file instead of all files
330
+
331
+ **Medium Priority**
332
+ - [x] Add search input debouncing (`src/ui/RomBrowser/index.tsx`)
333
+ - Added 200ms debounce delay via `debouncedSearchQuery` state
334
+ - Filtering now uses debounced query while display uses immediate query
335
+
336
+ - [x] Optimize mouse event buffer scanning (`src/ui/RomBrowser/index.tsx`)
337
+ - Added 512-byte max buffer size guard
338
+ - Moved regex outside handler for reuse
339
+ - Simplified buffer clearing via lastMatchEnd tracking
340
+
341
+ - [x] Wrap MetadataPanel in React.memo (`src/ui/RomBrowser/index.tsx`)
342
+ - Prevents unnecessary re-renders when parent state changes
343
+ - Added MetadataPanelProps interface for cleaner typing
344
+
345
+ **Low Priority**
346
+ - [x] Optimize category index calculation (`src/ui/RomBrowser/index.tsx`)
347
+ - Replaced reduce with array spreading with simple for loop using push()
348
+ - Reduced time complexity from O(n²) to O(n)
349
+
350
+ ### Libretro Core Optimizations (Identified 2026-01)
351
+
352
+ **Critical (Every Frame)**
353
+ - [x] Reuse audio Float32Array buffer (`src/cores/libretro/callbacks/index.ts:258-281`)
354
+ - Added `audioOutputBuffer` and `audioOutputCapacity` class properties
355
+ - `drainAudio()` now reuses buffer, only grows when capacity exceeded
356
+ - Returns `subarray()` view instead of new allocation each frame
357
+
358
+ - [x] Optimize framebuffer copy (`src/cores/libretro/callbacks/index.ts:122-154`)
359
+ - Already optimized: uses `koffi.view()` for zero-copy native memory access
360
+ - `.set()` is necessary since native memory only valid during callback
361
+ - No further optimization possible without FFI changes
362
+
363
+ - [x] Optimize pixel format conversion loops (`src/cores/libretro/pixel-format/index.ts:96-148`)
364
+ - Replaced manual byte reading `data[idx] | (data[idx + 1] << 8)` with `DataView.getUint16()`
365
+ - DataView provides optimized native 16-bit reads with endianness handling
366
+ - Removed unused `BYTE_SHIFT` constant
367
+
368
+ **Medium Priority**
369
+ - [x] Cache input state bitmask per port (`src/cores/libretro/callbacks/index.ts:214-236`)
370
+ - Replaced Map-based storage with sparse arrays for O(1) lookups
371
+ - Added `buttonBitmask[]` cache updated on `setButtonState()`
372
+ - Bitmask queries now O(1) instead of iterating all buttons
373
+
374
+ - [x] Optimize audio buffer growth strategy (`src/cores/libretro/callbacks/index.ts:197-212`)
375
+ - Changed from 2x to 1.5x growth factor (more memory-efficient)
376
+ - Added `AUDIO_BUFFER_GROWTH_FACTOR` constant
377
+ - Reduced initial buffer size from 8192 to 4096 samples
378
+
379
+ **Low Priority**
380
+ - [x] Cache pixel format (`src/cores/libretro/index.ts:45,260,320`)
381
+ - Added `cachedPixelFormat` property in LibretroCore
382
+ - Cache populated after ROM load, used in getFramebuffer()
383
+ - Eliminates method call overhead on hot path
384
+
385
+ - [x] Reuse DataView for message parsing (`src/cores/libretro/environment/index.ts:408-469`)
386
+ - Analyzed: message callbacks are infrequent (not per-frame)
387
+ - Optimization would provide minimal benefit
388
+ - Marked complete - no changes needed
389
+
390
+ - [x] Cache directory string buffers (`src/cores/libretro/environment/index.ts:355-366`)
391
+ - Replaced `allocatedStrings[]` array with `directoryBufferCache` Map
392
+ - Buffers now cached by path, reused across repeated queries
393
+ - Prevents unbounded memory growth
394
+
395
+ ### General
396
+ - [ ] Profile hot paths with Node.js inspector
397
+ - [ ] Implement frame skipping option
398
+ - [x] Accurate frame timing (60 FPS)
399
+ - [x] Fix timer drift: use `lastFrameTime += targetFrameTime` instead of `lastFrameTime = now`
400
+ - [x] Add frame skipping: run multiple frames without rendering when behind schedule
401
+ - [ ] Handle Node.js event loop delays
402
+ - [x] Frame skipping when behind schedule (emulate without rendering to catch up)
403
+ - [ ] Audio-driven timing (let audio buffer drive frame pacing instead of timers)
404
+ - [ ] Adaptive frame timing (track actual elapsed time instead of assuming fixed intervals)
405
+ - [ ] Busy-wait for precision (spin-wait final few ms instead of setTimeout)
406
+ - [ ] Worker thread isolation (move emulation off main event loop)
407
+
408
+ ---
409
+
410
+ ## Code Simplification
411
+
412
+ Remove micro-optimizations that add complexity without meaningful performance benefits.
413
+
414
+ ### High Priority (Most Complexity, Least Benefit)
415
+
416
+ - [x] Remove palette ANSI escape caches (`src/rendering/palette.ts:167-231`)
417
+ - 4 separate caches (trueColorCache, bgTrueColorCache, luminanceCache, emojiColorCache) pre-computed at module load
418
+ - ~4KB of module-level state for all 512 NES color combinations
419
+ - String generation for ANSI escapes is fast in modern JS - compute on-demand instead
420
+
421
+ - [x] Remove directory buffer caching (`src/cores/libretro/environment/index.ts:112-113, 352-366`)
422
+ - `directoryBufferCache` Map caches Buffer objects for system/save directory paths
423
+ - Only queried 1-3 times per core load, not per-frame
424
+ - Buffer creation for small strings is negligible - remove the Map overhead
425
+
426
+ - [x] Simplify normalized path cache (`src/frontend/playlist/utils.ts:14-39`)
427
+ - Module-level cache that persists and can become stale if files move
428
+ - Called during playlist generation which is already I/O-bound
429
+ - Consider session-scoped cache or remove entirely
430
+
431
+ ### Medium Priority (Unnecessary but Less Harmful)
432
+
433
+ - [x] Remove useMemo for trivial UI elements (`src/ui/RomBrowser/index.tsx:1653-1686`)
434
+ - `emptySpaceElements` array memoization - simple array creation
435
+ - `scrollbarElements` array memoization - Ink re-renders anyway
436
+ - Scrollbar position/size calculation - trivial math that doesn't need memoization
437
+
438
+ - [x] Remove useMemo for settings option lookups (`src/ui/RomBrowser/index.tsx:494-506`)
439
+ - Pre-computed Maps for O(1) lookup on ~30 items
440
+ - `.find()` on 30 items is ~10 microseconds - not a bottleneck
441
+
442
+ ---
443
+
444
+ ## Milestone 7: Features & Polish
445
+
446
+ ### Save States
447
+ - [ ] Design save state format
448
+ - [ ] Serialize CPU state
449
+ - [ ] Serialize PPU state (VRAM, OAM, registers)
450
+ - [ ] Serialize mapper state
451
+ - [ ] Save/load to file
452
+ - [ ] Quick save/load hotkeys
453
+
454
+ ### Configuration
455
+ - [ ] Config file support (JSON)
456
+ - [ ] Key remapping
457
+ - [ ] Display settings persistence
458
+ - [ ] ROM-specific settings
459
+
460
+ ### User Experience
461
+ - [ ] Better error messages for invalid ROMs
462
+ - [ ] ROM info display (mapper, PRG/CHR size)
463
+ - [ ] Debug mode (show registers, memory)
464
+ - [ ] Pause/resume functionality
465
+ - [ ] Frame advance (debug)
466
+
467
+ ---
468
+
469
+ ## Milestone 8: Testing & Compatibility
470
+
471
+ ### Test ROMs
472
+ - [ ] nestest.nes - CPU instruction test
473
+ - [ ] PPU tests (blargg's)
474
+ - [ ] Sprite tests
475
+ - [ ] Timing tests
476
+
477
+ ### Game Compatibility
478
+ - [ ] Donkey Kong (Mapper 0)
479
+ - [ ] Super Mario Bros (Mapper 0)
480
+ - [ ] The Legend of Zelda (Mapper 1)
481
+ - [ ] Mega Man 2 (Mapper 1)
482
+ - [ ] Contra (Mapper 2)
483
+ - [ ] Super Mario Bros 3 (Mapper 4)
484
+
485
+ ### Automated Testing
486
+ - [ ] Unit tests for CPU instructions
487
+ - [ ] Unit tests for PPU rendering
488
+ - [ ] Integration tests with test ROMs
489
+ - [ ] CI/CD pipeline
490
+
491
+ ---
492
+
493
+ ## Known Issues
494
+
495
+ - [x] ~~Sprites not rendering (not implemented)~~ - FIXED
496
+ - [ ] Some games may have graphical glitches (timing)
497
+ - [x] ~~No audio output~~ - FIXED (APU fully implemented)
498
+ - [x] ~~Key input uses timeout-based release~~ - FIXED (now uses Kitty keyboard protocol)
499
+
500
+ ---
501
+
502
+ ## Milestone 9: RetroArch Compatibility
503
+
504
+ Improvements to make emoemu more compatible with RetroArch configurations, cores, and workflows.
505
+
506
+ ### High Priority
507
+
508
+ #### Core Options Support
509
+ Libretro cores define configuration options via `SET_CORE_OPTIONS` but emoemu doesn't expose them to users. `GET_VARIABLE` always returns false (`src/cores/libretro/environment/index.ts:336-341`), so cores use defaults.
510
+
511
+ - [ ] Create core options module to parse `retro_core_option_v2_definition` structures
512
+ - [ ] Store option definitions from `SET_CORE_OPTIONS` / `SET_CORE_OPTIONS_V2`
513
+ - [ ] Implement `handleGetVariable()` to return configured values
514
+ - [ ] Add config file sections for per-core options: `[core_picodrive_options]`
515
+ - [ ] Add CLI flag: `--core-opt "Option Name"=value`
516
+ - [ ] Fall back to default/first option if not configured (RetroArch behavior)
517
+
518
+ #### Audio/Video Control
519
+ - [x] `GET_AUDIO_VIDEO_ENABLE` should respect `--no-audio` flag (`src/cores/libretro/environment/index.ts:227-232`)
520
+ - ~~Currently hardcoded to `0b11` (both enabled)~~
521
+ - Cores can now skip audio processing when disabled
522
+
523
+ ### Medium Priority
524
+
525
+ #### Additional Config Keys
526
+ Add standard RetroArch config keys to `src/frontend/config.ts:48-130`:
527
+ - [ ] `video_aspect_ratio` - explicit aspect ratio control
528
+ - [ ] `video_force_aspect` - force a specific aspect ratio
529
+ - [ ] `input_driver` - specify input backend preference
530
+ - [ ] `savestate_thumbnail_enable` - thumbnail support in states
531
+
532
+ #### Read More from retroarch.cfg
533
+ When `--retroarch` is used, parse additional settings from `retroarch.cfg` (`src/cores/libretro/loader.ts:104-137`):
534
+ - [ ] `savefile_directory` - consistent battery save locations
535
+ - [ ] `savestate_directory` - consistent state save locations
536
+ - [ ] `system_directory` - BIOS files location
537
+
538
+ #### Environment Callback Improvements
539
+ - [ ] Increase `MAX_INPUT_USERS` from 2 to configurable (`src/cores/libretro/environment/consts.ts:13`)
540
+ - Some cores support 4+ players
541
+ - [ ] Update `MESSAGE_INTERFACE_VERSION` from 1 to 2 (`src/cores/libretro/environment/consts.ts:19`)
542
+ - Modern cores expect version 2+ for newer message formats
543
+ - [ ] Make `DEBUG_ENV` controllable via environment variable (e.g., `EMOEMU_DEBUG_ENV`)
544
+
545
+ ### Low Priority
546
+
547
+ #### Save State Enhancements
548
+ - [ ] Save state thumbnail support (binary PNG instead of base64)
549
+ - [ ] Add additional metadata matching RetroArch: `systemId`, `configHash`
550
+
551
+ #### Per-Core Config Files
552
+ - [ ] Read per-core configurations from `~/.retroarch/config/[corename]/` when `--retroarch` is used
553
+
554
+ #### Memory Maps
555
+ - [ ] Handle additional memory regions beyond SRAM in `SET_MEMORY_MAPS` (`src/cores/libretro/environment/index.ts:409-410`)
556
+
557
+ ---
558
+
559
+ ## Netplay Sync Improvements
560
+
561
+ When connecting as a client to a RetroArch host, emoemu's sync is playable but "jumpy/glitchy" compared to two official RetroArch apps. The following improvements would make sync smoother while staying compatible with RetroArch protocol.
562
+
563
+ ### Critical Issues
564
+
565
+ #### 1. Stall threshold too aggressive
566
+ **File:** `src/netplay/consts.ts:213`
567
+
568
+ - emoemu: `MAX_FRAMES_BEHIND = 10`
569
+ - RetroArch: `NETPLAY_MAX_STALL_FRAMES = 60`
570
+
571
+ With only 10 frames tolerance, any network jitter causes constant micro-stalls. Increase to 60 frames (1 second at 60fps) to match RetroArch behavior.
572
+
573
+ ```typescript
574
+ // Change from:
575
+ export const MAX_FRAMES_BEHIND = 10;
576
+ // To:
577
+ export const MAX_FRAMES_BEHIND = 60;
578
+ ```
579
+
580
+ #### 2. Missing catch-up mode
581
+ **Files:** `src/netplay/sync-manager.ts`, `src/Emulator/index.ts`
582
+
583
+ RetroArch has a `catch_up` boolean (netplay_private.h:671) that temporarily disables the frame limiter when the client is behind. This allows smooth fast-forward to catch up instead of stuttery pause/resume cycles.
584
+
585
+ emoemu only stalls when ahead, never accelerates when behind.
586
+
587
+ **Fix:**
588
+ - Add `shouldCatchUp: boolean` to preFrame() return value
589
+ - Detect when `unreadFrame - selfFrame > threshold` (e.g., 60 frames)
590
+ - Return `{ shouldStall: false, shouldCatchUp: true }`
591
+ - Emulator disables frame limiter when `shouldCatchUp` is true
592
+ - Allows client to fast-forward smoothly to catch up
593
+
594
+ #### 3. Single frame counter instead of two
595
+ **Files:** `src/netplay/sync-manager.ts:364-403`, `src/Emulator/index.ts:662-700`
596
+
597
+ RetroArch uses TWO independent frame counters:
598
+ - `self_frame_count` - where INPUT is being read from
599
+ - `run_frame_count` - where the core is actually executing
600
+
601
+ emoemu uses ONE (`_selfFrame`) for both purposes. This causes frame N to be executed with input from frame N+1, creating frame/input mismatch vs RetroArch.
602
+
603
+ **Fix:**
604
+ - Add `readFrame` pointer (input read position)
605
+ - Keep `selfFrame` as execution position
606
+ - Increment `readFrame` in preFrame() to read frame N's input
607
+ - Increment `selfFrame` AFTER core.runFrame() completes
608
+ - Base rollback decisions on `readFrame`, not `selfFrame`
609
+
610
+ ### Medium Priority
611
+
612
+ #### 4. INPUT processing timing
613
+ **File:** `src/netplay/client.ts:658-671`
614
+
615
+ When remote INPUT arrives, rollback is queued but happens in the NEXT `postFrame()` - delayed by 1+ frame. With network latency, this adds perceivable input lag.
616
+
617
+ **Fix:** Use per-client `readFramePerClient` tracking to immediately detect when we have enough input to advance safely, rather than delayed rollback checking.
618
+
619
+ #### 5. NOINPUT doesn't update sync state
620
+ **File:** `src/netplay/client.ts:821-835`
621
+
622
+ `handleNoInput()` only updates `_serverFrame`, doesn't notify sync manager. Spectators may stall unnecessarily.
623
+
624
+ **Fix:** Add `syncManager.advanceFrameWithoutInput(frameNumber)` method for spectators.
625
+
626
+ ### Implementation Priority
627
+
628
+ 1. **Quick wins (high impact, low risk):**
629
+ - [x] Increase `MAX_FRAMES_BEHIND` to 60
630
+ - [x] Add catch-up mode flag to preFrame() return
631
+
632
+ 2. **Medium effort:**
633
+ - [x] Implement dual frame counters for accurate frame/input sync
634
+ - [x] Fix NOINPUT not updating sync state (add advanceFrameWithoutInput)
635
+
636
+ **Note:** Moving rollback check before sending input was attempted but caused severe stuttering (feedback loop). The original order (send input → rollback) is correct.
637
+
638
+ ### Test Cases
639
+
640
+ 1. **Catch-up mode:** Run emoemu client with RetroArch server, introduce ~100ms latency, verify client speeds up smoothly without visible pausing
641
+
642
+ 2. **Frame sync accuracy:** Run with `--netplay-connect --clear-logs`, check netplay.log for desync messages, should see 0 desyncs on identical cores
643
+
644
+ 3. **Stall/resume smoothness:** Use variable latency (jitter 50-150ms), verify gameplay is smooth not jerky
645
+
646
+ ---
647
+
648
+ ## Future Ideas
649
+
650
+ - [ ] Web version (WebAssembly + canvas)
651
+ - [x] Netplay / online multiplayer
652
+ - [ ] TAS (Tool-Assisted Speedrun) recording
653
+ - [ ] ROM database integration
654
+ - [ ] Shader-like post-processing effects
655
+ - [ ] Game Genie code support