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