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,676 @@
1
+ # Headless Hardware Rendering - Technical Requirements Document
2
+
3
+ This document describes the requirements and implementation approach for supporting libretro cores that require OpenGL/Vulkan hardware rendering in emoemu, using headless GPU contexts with pixel readback.
4
+
5
+ ## Overview
6
+
7
+ ### Goals
8
+
9
+ 1. **Enable GPU-Accelerated Cores**: Support libretro cores that require OpenGL or Vulkan hardware rendering
10
+ 2. **Headless Operation**: Create GPU contexts without windows, suitable for terminal-based rendering
11
+ 3. **Seamless Integration**: After GPU rendering, read pixels back and pipe them through existing Kitty/Unicode/ASCII renderers
12
+ 4. **Cross-Platform Support**: Work on macOS, Linux, and Windows
13
+
14
+ ### Non-Goals
15
+
16
+ 1. Direct GPU output to terminal (not possible)
17
+ 2. Real-time GPU-to-terminal streaming (always requires pixel readback)
18
+ 3. Vulkan support in initial implementation (focus on OpenGL first)
19
+ 4. WebGL compatibility (need full OpenGL, not WebGL subset)
20
+
21
+ ### Background: Why Hardware Rendering?
22
+
23
+ Some libretro cores require hardware-accelerated rendering:
24
+
25
+ | Core | System | Why GPU Required |
26
+ |------|--------|------------------|
27
+ | Beetle PSX HW | PlayStation | Hardware-accelerated rendering with enhancements |
28
+ | ParaLLEl N64 | Nintendo 64 | Vulkan-based RDP emulation |
29
+ | PPSSPP | PSP | OpenGL ES for PSP GPU emulation |
30
+ | Dolphin | GameCube/Wii | Modern GPU required for performance |
31
+ | Flycast | Dreamcast | PowerVR GPU emulation |
32
+
33
+ Currently, emoemu returns `false` for `SET_HW_RENDER` environment callbacks, causing these cores to fail initialization or fall back to slower software rendering (when available).
34
+
35
+ ---
36
+
37
+ ## Architecture
38
+
39
+ ### Current Flow (Software Rendering)
40
+
41
+ ```
42
+ ┌─────────────────┐ video callback ┌──────────────────┐
43
+ │ Libretro Core │ ──────────────────────► │ CallbackManager │
44
+ │ (CPU renders) │ (pixel buffer) │ (framebuffer) │
45
+ └─────────────────┘ └────────┬─────────┘
46
+
47
+
48
+ ┌──────────────────┐
49
+ │ Kitty Renderer │
50
+ └──────────────────┘
51
+ ```
52
+
53
+ ### Proposed Flow (Hardware Rendering)
54
+
55
+ ```
56
+ ┌─────────────────┐ ┌──────────────────────────┐
57
+ │ Libretro Core │ ──── OpenGL calls ────► │ HardwareRenderContext │
58
+ │ (GPU renders) │ │ - Headless EGL/CGL ctx │
59
+ └─────────────────┘ │ - FBO management │
60
+ │ - glReadPixels() │
61
+ └────────────┬─────────────┘
62
+
63
+ pixel readback
64
+
65
+
66
+ ┌──────────────────┐
67
+ │ CallbackManager │
68
+ │ (framebuffer) │
69
+ └────────┬─────────┘
70
+
71
+
72
+ ┌──────────────────┐
73
+ │ Kitty Renderer │
74
+ └──────────────────┘
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Libretro Hardware Rendering API
80
+
81
+ ### Environment Commands
82
+
83
+ | Command | Direction | Purpose |
84
+ |---------|-----------|---------|
85
+ | `SET_HW_RENDER` | Core → Frontend | Core requests hardware context |
86
+ | `GET_HW_RENDER_INTERFACE` | Core → Frontend | Get extended HW interface |
87
+ | `GET_PREFERRED_HW_RENDER` | Core → Frontend | Ask frontend's preferred context type |
88
+ | `SET_HW_SHARED_CONTEXT` | Core → Frontend | Request shared context |
89
+
90
+ ### retro_hw_render_callback Structure
91
+
92
+ The core passes this struct via `SET_HW_RENDER`:
93
+
94
+ | Field | Type | Description |
95
+ |-------|------|-------------|
96
+ | `context_type` | `enum` | OpenGL, OpenGL Core, GLES2, GLES3, or Vulkan |
97
+ | `version_major` | `unsigned` | Requested OpenGL major version |
98
+ | `version_minor` | `unsigned` | Requested OpenGL minor version |
99
+ | `depth` | `bool` | Core needs depth buffer |
100
+ | `stencil` | `bool` | Core needs stencil buffer |
101
+ | `bottom_left_origin` | `bool` | Coordinate system origin |
102
+ | `cache_context` | `bool` | Frontend should cache context |
103
+ | `debug_context` | `bool` | Request debug context |
104
+ | `context_reset` | `callback` | Called when context is created/reset |
105
+ | `context_destroy` | `callback` | Called before context destruction |
106
+ | `get_current_framebuffer` | `callback` | **Frontend provides**: Returns FBO id |
107
+ | `get_proc_address` | `callback` | **Frontend provides**: Returns GL function pointer |
108
+
109
+ ### API Flow
110
+
111
+ ```
112
+ 1. Core calls retro_load_game()
113
+ └── Core calls SET_HW_RENDER with requirements
114
+ └── Frontend creates headless GL context
115
+ └── Frontend writes get_current_framebuffer and get_proc_address to struct
116
+ └── Returns true if successful
117
+
118
+ 2. Frontend calls context_reset callback
119
+ └── Core initializes its GL resources
120
+
121
+ 3. Per frame in retro_run():
122
+ └── Core calls get_current_framebuffer() to get FBO
123
+ └── Core renders to FBO using OpenGL
124
+ └── Core does NOT call video_refresh callback (or calls with NULL)
125
+ └── Frontend calls glReadPixels() to get frame
126
+
127
+ 4. On shutdown:
128
+ └── Frontend calls context_destroy callback
129
+ └── Core cleans up GL resources
130
+ └── Frontend destroys GL context
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Platform-Specific Context Creation
136
+
137
+ ### macOS: CGL (Core Graphics OpenGL)
138
+
139
+ macOS does not use EGL. Instead, use the CGL API from the OpenGL framework:
140
+
141
+ ```typescript
142
+ // Load OpenGL framework
143
+ const opengl = koffi.load('/System/Library/Frameworks/OpenGL.framework/OpenGL');
144
+
145
+ // CGL types
146
+ const CGLContextObj = koffi.pointer('CGLContextObj', koffi.opaque());
147
+ const CGLPixelFormatObj = koffi.pointer('CGLPixelFormatObj', koffi.opaque());
148
+
149
+ // Key functions
150
+ const CGLChoosePixelFormat = opengl.func('int CGLChoosePixelFormat(int*, CGLPixelFormatObj*, int*)');
151
+ const CGLCreateContext = opengl.func('int CGLCreateContext(CGLPixelFormatObj, CGLContextObj, CGLContextObj*)');
152
+ const CGLSetCurrentContext = opengl.func('int CGLSetCurrentContext(CGLContextObj)');
153
+ const CGLDestroyContext = opengl.func('int CGLDestroyContext(CGLContextObj)');
154
+ ```
155
+
156
+ **Pixel format attributes for offscreen rendering:**
157
+
158
+ ```typescript
159
+ const attributes = [
160
+ kCGLPFAOpenGLProfile, kCGLOGLPVersion_3_2_Core, // OpenGL 3.2 Core
161
+ kCGLPFAColorSize, 24,
162
+ kCGLPFADepthSize, 24,
163
+ kCGLPFAStencilSize, 8,
164
+ kCGLPFAOffScreen, // Critical: offscreen rendering
165
+ 0 // Terminator
166
+ ];
167
+ ```
168
+
169
+ ### Linux: EGL
170
+
171
+ EGL provides platform-agnostic OpenGL context creation:
172
+
173
+ ```typescript
174
+ // Load EGL and OpenGL ES libraries
175
+ const egl = koffi.load('libEGL.so.1');
176
+ const gles = koffi.load('libGLESv2.so.2');
177
+
178
+ // Or for full OpenGL:
179
+ const gl = koffi.load('libGL.so.1');
180
+
181
+ // Key EGL functions
182
+ const eglGetDisplay = egl.func('void* eglGetDisplay(void*)');
183
+ const eglInitialize = egl.func('int eglInitialize(void*, int*, int*)');
184
+ const eglChooseConfig = egl.func('int eglChooseConfig(void*, int*, void*, int, int*)');
185
+ const eglCreateContext = egl.func('void* eglCreateContext(void*, void*, void*, int*)');
186
+ const eglMakeCurrent = egl.func('int eglMakeCurrent(void*, void*, void*, void*)');
187
+ const eglGetProcAddress = egl.func('void* eglGetProcAddress(const char*)');
188
+ ```
189
+
190
+ **Surfaceless context (no window/pbuffer needed):**
191
+
192
+ ```typescript
193
+ // Use EGL_NO_SURFACE for surfaceless context
194
+ const EGL_NO_SURFACE = null;
195
+ eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, context);
196
+ ```
197
+
198
+ **Alternative: Pbuffer surface (more compatible):**
199
+
200
+ ```typescript
201
+ const pbufferAttribs = [
202
+ EGL_WIDTH, 1920,
203
+ EGL_HEIGHT, 1080,
204
+ EGL_NONE
205
+ ];
206
+ const surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
207
+ ```
208
+
209
+ ### Windows: EGL via ANGLE or WGL
210
+
211
+ **Option 1: ANGLE (Recommended)**
212
+
213
+ ANGLE provides EGL on Windows, translating OpenGL ES to DirectX:
214
+
215
+ ```typescript
216
+ const egl = koffi.load('libEGL.dll'); // From ANGLE
217
+ const gles = koffi.load('libGLESv2.dll');
218
+ // Same EGL API as Linux
219
+ ```
220
+
221
+ **Option 2: WGL (Native)**
222
+
223
+ ```typescript
224
+ const opengl32 = koffi.load('opengl32.dll');
225
+ const wglCreateContext = opengl32.func('void* wglCreateContext(void*)');
226
+ const wglMakeCurrent = opengl32.func('int wglMakeCurrent(void*, void*)');
227
+ const wglGetProcAddress = opengl32.func('void* wglGetProcAddress(const char*)');
228
+ ```
229
+
230
+ WGL requires a device context (DC), which typically requires a window. Workaround: create a hidden window.
231
+
232
+ ---
233
+
234
+ ## Implementation Components
235
+
236
+ ### 1. HardwareRenderContext Interface
237
+
238
+ ```typescript
239
+ // src/cores/libretro/hardware-render/types.ts
240
+
241
+ export enum HWContextType {
242
+ NONE = 0,
243
+ OPENGL = 1,
244
+ OPENGLES2 = 2,
245
+ OPENGL_CORE = 3,
246
+ OPENGLES3 = 4,
247
+ OPENGLES_VERSION = 5,
248
+ VULKAN = 6,
249
+ }
250
+
251
+ export interface HWRenderRequest {
252
+ contextType: HWContextType;
253
+ versionMajor: number;
254
+ versionMinor: number;
255
+ depth: boolean;
256
+ stencil: boolean;
257
+ bottomLeftOrigin: boolean;
258
+ cacheContext: boolean;
259
+ debugContext: boolean;
260
+ }
261
+
262
+ export interface HardwareRenderContext {
263
+ /** Initialize the headless GL context */
264
+ init(request: HWRenderRequest): boolean;
265
+
266
+ /** Get the FBO id for the core to render to */
267
+ getCurrentFramebuffer(): number;
268
+
269
+ /** Get a GL function pointer by name */
270
+ getProcAddress(symbol: string): bigint;
271
+
272
+ /** Called after core initializes its GL resources */
273
+ contextReset(): void;
274
+
275
+ /** Called before destroying the context */
276
+ contextDestroy(): void;
277
+
278
+ /** Read rendered pixels from FBO */
279
+ readPixels(width: number, height: number): Uint8Array;
280
+
281
+ /** Resize the FBO */
282
+ resize(width: number, height: number): void;
283
+
284
+ /** Clean up all resources */
285
+ destroy(): void;
286
+ }
287
+ ```
288
+
289
+ ### 2. FBO Management
290
+
291
+ The frontend must provide a framebuffer object (FBO) for the core to render into:
292
+
293
+ ```typescript
294
+ // Pseudocode for FBO setup
295
+ const fbo = glGenFramebuffers(1);
296
+ const colorTexture = glGenTextures(1);
297
+ const depthRenderbuffer = glGenRenderbuffers(1);
298
+
299
+ glBindTexture(GL_TEXTURE_2D, colorTexture);
300
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, null);
301
+
302
+ glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer);
303
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
304
+
305
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
306
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexture, 0);
307
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer);
308
+ ```
309
+
310
+ ### 3. Pixel Readback
311
+
312
+ After each frame, read pixels from the FBO:
313
+
314
+ ```typescript
315
+ readPixels(width: number, height: number): Uint8Array {
316
+ const pixels = new Uint8Array(width * height * 4); // RGBA
317
+
318
+ glBindFramebuffer(GL_FRAMEBUFFER, this.fbo);
319
+ glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
320
+
321
+ // OpenGL origin is bottom-left, libretro expects top-left
322
+ // Flip vertically if needed (based on bottom_left_origin flag)
323
+ if (!this.bottomLeftOrigin) {
324
+ this.flipVertical(pixels, width, height);
325
+ }
326
+
327
+ return pixels;
328
+ }
329
+ ```
330
+
331
+ ### 4. get_proc_address Implementation
332
+
333
+ Cores need to load OpenGL functions dynamically:
334
+
335
+ ```typescript
336
+ getProcAddress(symbol: string): bigint {
337
+ // Platform-specific function lookup
338
+ if (process.platform === 'darwin') {
339
+ return CGLGetProcAddress(symbol);
340
+ } else if (process.platform === 'linux') {
341
+ return eglGetProcAddress(symbol) || dlsym(RTLD_DEFAULT, symbol);
342
+ } else {
343
+ return wglGetProcAddress(symbol) || GetProcAddress(opengl32, symbol);
344
+ }
345
+ }
346
+ ```
347
+
348
+ ### 5. Environment Handler Updates
349
+
350
+ ```typescript
351
+ // In environment/index.ts
352
+
353
+ case RETRO_ENVIRONMENT.SET_HW_RENDER: {
354
+ const request = this.parseHwRenderCallback(data);
355
+
356
+ // Check if we support this context type
357
+ if (request.contextType === HWContextType.VULKAN) {
358
+ logger.warn('Vulkan not supported, rejecting SET_HW_RENDER', 'HWRender');
359
+ return false;
360
+ }
361
+
362
+ // Create hardware render context
363
+ this.hwContext = createHardwareContext();
364
+ if (!this.hwContext.init(request)) {
365
+ logger.error('Failed to create hardware render context', 'HWRender');
366
+ return false;
367
+ }
368
+
369
+ // Write our callbacks back to the struct
370
+ this.writeHwRenderCallbacks(data, {
371
+ get_current_framebuffer: this.hwContext.getCurrentFramebuffer.bind(this.hwContext),
372
+ get_proc_address: this.hwContext.getProcAddress.bind(this.hwContext),
373
+ });
374
+
375
+ return true;
376
+ }
377
+
378
+ case RETRO_ENVIRONMENT.GET_PREFERRED_HW_RENDER: {
379
+ // Tell core we prefer OpenGL Core profile
380
+ writeUInt32LE(data, HWContextType.OPENGL_CORE);
381
+ return true;
382
+ }
383
+ ```
384
+
385
+ ---
386
+
387
+ ## Performance Considerations
388
+
389
+ ### Pixel Readback Latency
390
+
391
+ `glReadPixels()` is synchronous and stalls the GPU pipeline. Mitigation strategies:
392
+
393
+ | Strategy | Description | Complexity |
394
+ |----------|-------------|------------|
395
+ | **PBO (Pixel Buffer Object)** | Async readback using buffer objects | Medium |
396
+ | **Double/Triple buffering** | Read from previous frame's PBO | Medium |
397
+ | **Compute shader copy** | Copy to mapped buffer via compute | High |
398
+
399
+ **PBO async readback example:**
400
+
401
+ ```typescript
402
+ // Setup: create two PBOs for double buffering
403
+ const pbos = [glGenBuffer(), glGenBuffer()];
404
+ let currentPbo = 0;
405
+
406
+ // Each frame:
407
+ // 1. Start async read into current PBO
408
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[currentPbo]);
409
+ glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, 0);
410
+
411
+ // 2. Map previous PBO (already finished) and get pixels
412
+ const prevPbo = 1 - currentPbo;
413
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[prevPbo]);
414
+ const pixels = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
415
+ // Copy pixels...
416
+ glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
417
+
418
+ // 3. Swap
419
+ currentPbo = prevPbo;
420
+ ```
421
+
422
+ ### Memory Usage
423
+
424
+ | Component | Memory |
425
+ |-----------|--------|
426
+ | FBO color buffer (1080p RGBA) | ~8 MB |
427
+ | FBO depth/stencil buffer | ~8 MB |
428
+ | PBO for readback (x2) | ~16 MB |
429
+ | Readback buffer (CPU) | ~8 MB |
430
+ | **Total** | ~40 MB |
431
+
432
+ ### Frame Timing
433
+
434
+ Hardware rendering adds latency:
435
+
436
+ ```
437
+ Core renders to FBO ──┬── GPU time (varies)
438
+
439
+ glReadPixels() ──┼── GPU→CPU transfer + stall
440
+
441
+ Terminal render ──┴── Already exists
442
+ ```
443
+
444
+ With async PBO readback, effective latency is 1 frame behind but throughput is maintained.
445
+
446
+ ---
447
+
448
+ ## File Structure
449
+
450
+ ```
451
+ src/cores/libretro/hardware-render/
452
+ ├── index.ts # Factory and common logic
453
+ ├── types.ts # Interfaces and enums
454
+ ├── context-cgl.ts # macOS CGL implementation
455
+ ├── context-egl.ts # Linux/Windows EGL implementation
456
+ ├── context-wgl.ts # Windows WGL fallback (optional)
457
+ ├── fbo.ts # Framebuffer object management
458
+ ├── gl-loader.ts # OpenGL function pointer loading
459
+ └── consts.ts # GL constants
460
+ ```
461
+
462
+ ---
463
+
464
+ ## Implementation Phases
465
+
466
+ ### Phase 1: macOS CGL Context
467
+
468
+ 1. Implement CGL context creation via koffi
469
+ 2. Create offscreen pixel format
470
+ 3. Basic FBO setup
471
+ 4. Implement `get_proc_address` for macOS
472
+ 5. Simple `glReadPixels` (synchronous)
473
+ 6. Test with a simple OpenGL core (e.g., Beetle PSX HW in software fallback mode)
474
+
475
+ ### Phase 2: Core Integration
476
+
477
+ 1. Update environment handler for `SET_HW_RENDER`
478
+ 2. Implement callback struct parsing/writing
479
+ 3. Wire up `context_reset`/`context_destroy` callbacks
480
+ 4. Integrate pixel readback into frame loop
481
+ 5. Handle resolution changes via `SET_GEOMETRY`
482
+
483
+ ### Phase 3: Linux EGL Support
484
+
485
+ 1. Implement EGL context creation
486
+ 2. Handle surfaceless vs pbuffer contexts
487
+ 3. Test on Linux systems with and without GPU
488
+
489
+ ### Phase 4: Performance Optimization
490
+
491
+ 1. Implement PBO double-buffering
492
+ 2. Profile and optimize readback latency
493
+ 3. Consider compute shader approach for high-res content
494
+
495
+ ### Phase 5: Windows Support
496
+
497
+ 1. Add ANGLE/EGL support
498
+ 2. Fallback to WGL with hidden window if needed
499
+ 3. Test on various Windows configurations
500
+
501
+ ### Phase 6: Vulkan (Future)
502
+
503
+ 1. Create headless Vulkan instance
504
+ 2. Render to offscreen image
505
+ 3. Copy to host-visible memory
506
+ 4. Significantly more complex than OpenGL
507
+
508
+ ---
509
+
510
+ ## Testing Strategy
511
+
512
+ ### Test Cores
513
+
514
+ | Core | Context Type | Notes |
515
+ |------|--------------|-------|
516
+ | Beetle PSX HW | OpenGL Core | Falls back to software if HW fails |
517
+ | mGBA | OpenGL (optional) | Has software fallback |
518
+ | Sameboy | OpenGL (optional) | Has software fallback |
519
+
520
+ ### Validation Criteria
521
+
522
+ 1. **Context creation**: GL context initializes without errors
523
+ 2. **FBO completeness**: `glCheckFramebufferStatus` returns `GL_FRAMEBUFFER_COMPLETE`
524
+ 3. **Rendering**: Core produces visible output
525
+ 4. **Pixel accuracy**: Readback matches expected output
526
+ 5. **Performance**: Maintains target framerate
527
+
528
+ ### Debug Tools
529
+
530
+ - Set `debug_context = true` to get GL debug callbacks
531
+ - Use `GL_KHR_debug` extension for detailed error messages
532
+ - Log all GL calls in development mode
533
+
534
+ ---
535
+
536
+ ## Limitations and Risks
537
+
538
+ ### Known Limitations
539
+
540
+ 1. **Vulkan not supported initially**: Focus on OpenGL first
541
+ 2. **Performance overhead**: Pixel readback adds latency
542
+ 3. **GPU required**: No software fallback for HW-only cores
543
+ 4. **Platform dependencies**: Each platform needs specific implementation
544
+
545
+ ### Risks
546
+
547
+ | Risk | Mitigation |
548
+ |------|------------|
549
+ | Some cores may not work with offscreen context | Test widely, document unsupported cores |
550
+ | Performance may be insufficient for demanding cores | Implement PBO async readback |
551
+ | koffi FFI complexity with GL callbacks | Careful memory management, extensive testing |
552
+ | macOS deprecating OpenGL | Still works, consider Metal bridge long-term |
553
+
554
+ ---
555
+
556
+ ## Alternatives Considered
557
+
558
+ ### Alternative 1: headless-gl npm Package
559
+
560
+ The [headless-gl](https://github.com/stackgl/headless-gl) package provides WebGL 1.0 in Node.js.
561
+
562
+ **Pros:**
563
+ - Simple npm install
564
+ - Cross-platform
565
+ - Well-tested
566
+
567
+ **Cons:**
568
+ - WebGL 1.0 only (subset of OpenGL ES 2.0)
569
+ - Many cores require OpenGL 3.x+ features
570
+ - No OpenGL Core profile support
571
+
572
+ **Verdict:** Insufficient for most hardware-accelerated cores.
573
+
574
+ ### Alternative 2: Separate Renderer Process
575
+
576
+ Run a helper process with full GPU access, communicate via IPC.
577
+
578
+ **Pros:**
579
+ - Isolation from main process
580
+ - Could use native GPU frameworks
581
+ - Easier error recovery
582
+
583
+ **Cons:**
584
+ - IPC overhead for frame data (~8MB/frame at 1080p)
585
+ - Complex synchronization
586
+ - Additional process management
587
+
588
+ **Verdict:** Too complex and slow for real-time rendering.
589
+
590
+ ### Alternative 3: Virtual Display (Xvfb)
591
+
592
+ Use X virtual framebuffer on Linux.
593
+
594
+ **Pros:**
595
+ - Works with any X11 application
596
+ - Well-established technique
597
+
598
+ **Cons:**
599
+ - Linux only
600
+ - Requires X11 (not Wayland native)
601
+ - Additional dependency
602
+
603
+ **Verdict:** Linux-only, not cross-platform solution.
604
+
605
+ ---
606
+
607
+ ## Resources
608
+
609
+ - [Libretro OpenGL Accelerated Cores](https://docs.libretro.com/development/cores/opengl-cores/)
610
+ - [retro_hw_render_callback Struct Reference](https://buildbot.libretro.com/doxygen/a23611.html)
611
+ - [libretro.h Source](https://github.com/libretro/RetroArch/blob/master/libretro-common/include/libretro.h)
612
+ - [headless-gl GitHub](https://github.com/stackgl/headless-gl)
613
+ - [EGL Reference](https://registry.khronos.org/EGL/sdk/docs/man/html/)
614
+ - [CGL Reference (Apple)](https://developer.apple.com/documentation/opengl/cgl)
615
+ - [ANGLE Project](https://chromium.googlesource.com/angle/angle)
616
+
617
+ ---
618
+
619
+ ## Appendix: GL Constants
620
+
621
+ Common OpenGL constants needed for implementation:
622
+
623
+ ```typescript
624
+ // Framebuffer targets
625
+ const GL_FRAMEBUFFER = 0x8D40;
626
+ const GL_READ_FRAMEBUFFER = 0x8CA8;
627
+ const GL_DRAW_FRAMEBUFFER = 0x8CA9;
628
+
629
+ // Framebuffer attachments
630
+ const GL_COLOR_ATTACHMENT0 = 0x8CE0;
631
+ const GL_DEPTH_ATTACHMENT = 0x8D00;
632
+ const GL_STENCIL_ATTACHMENT = 0x8D20;
633
+ const GL_DEPTH_STENCIL_ATTACHMENT = 0x821A;
634
+
635
+ // Framebuffer status
636
+ const GL_FRAMEBUFFER_COMPLETE = 0x8CD5;
637
+
638
+ // Pixel formats
639
+ const GL_RGBA = 0x1908;
640
+ const GL_UNSIGNED_BYTE = 0x1401;
641
+
642
+ // Buffer targets (for PBO)
643
+ const GL_PIXEL_PACK_BUFFER = 0x88EB;
644
+
645
+ // Texture targets
646
+ const GL_TEXTURE_2D = 0x0DE1;
647
+
648
+ // Renderbuffer formats
649
+ const GL_DEPTH24_STENCIL8 = 0x88F0;
650
+ const GL_RGBA8 = 0x8058;
651
+ ```
652
+
653
+ ---
654
+
655
+ ## Appendix: retro_hw_render_callback Struct Layout
656
+
657
+ For parsing/writing the callback struct via koffi:
658
+
659
+ ```typescript
660
+ // 64-bit layout (pointers are 8 bytes)
661
+ const RETRO_HW_RENDER_CALLBACK_LAYOUT = {
662
+ context_type: { offset: 0, size: 4 }, // enum (uint32)
663
+ context_reset: { offset: 8, size: 8 }, // function pointer
664
+ get_current_framebuffer: { offset: 16, size: 8 }, // function pointer
665
+ get_proc_address: { offset: 24, size: 8 }, // function pointer
666
+ depth: { offset: 32, size: 1 }, // bool
667
+ stencil: { offset: 33, size: 1 }, // bool
668
+ bottom_left_origin: { offset: 34, size: 1 }, // bool
669
+ version_major: { offset: 36, size: 4 }, // unsigned int
670
+ version_minor: { offset: 40, size: 4 }, // unsigned int
671
+ cache_context: { offset: 44, size: 1 }, // bool
672
+ context_destroy: { offset: 48, size: 8 }, // function pointer
673
+ debug_context: { offset: 56, size: 1 }, // bool
674
+ // Total size: ~64 bytes (with padding)
675
+ };
676
+ ```