High-level overview
The application follows a multi-process architecture with clear separation of concerns:Native addon loads libretro cores
The
gamelord_libretro.node native addon (apps/desktop/native/src/libretro_core.cc) loads libretro .dylib cores directly via dlopen, implementing the full libretro frontend API including environment callbacks, video/audio/input handling.Utility process runs emulation loop
The emulation loop runs in a dedicated Electron utility process (
core-worker.ts) with hybrid sleep+spin frame pacing (~0.1-0.5ms jitter). It sends video frames and audio samples to the main process via postMessage.Main process coordinates communication
The main process forwards frames/audio to the renderer via
webContents.send with Buffer. EmulationWorkerClient manages the worker lifecycle and request/response protocol.Renderer displays frames and plays audio
The renderer displays frames on a
<canvas> via WebGL shaders and plays audio via Web Audio API with seamless chunk scheduling.Process architecture
Main process
The main process (apps/desktop/src/main.ts) handles:
- Window management - Creates and manages both the library window and game windows
- IPC coordination - Routes messages between renderer and utility processes
- Library services - ROM scanning, metadata fetching, database management
- Worker lifecycle - Spawns and manages utility process workers for emulation
- File system operations - Save states, SRAM, screenshots, artwork cache
The main process never blocks on emulation - all core execution happens in isolated utility processes.
Renderer process
Two separate renderer processes: Library window (index.html):
- Game library grid with cover art
- Search, filter, and sorting
- Settings and core management UI
- Artwork sync progress
game-window.html):
- WebGL canvas for frame rendering
- Web Audio API for audio playback
- Shader effects (CRT, LCD, scanlines)
- Control overlays (pause, FPS counter)
- Input capture (keyboard, gamepad)
Utility process (emulation worker)
A dedicated Electron utility process (core-worker.ts) for each running game:
- Loads the native addon dynamically via
require() - Drives the emulation loop with precise frame timing
- Handles save states and SRAM persistence
- Isolated from main process for crash resilience
Native addon architecture
Thegamelord_libretro.node addon bridges Node.js and libretro cores.
Key components
LibretroCore class (libretro_core.cc)
LibretroCore class (libretro_core.cc)
The main C++ class wrapping libretro functionality:
- Dynamic loading - Uses
dlopen(macOS/Linux) orLoadLibrary(Windows) to load.dylib/.dllcores - Function resolution - Resolves all libretro API function pointers via
dlsym - Environment callbacks - Implements libretro environment interface for core configuration
- Frame buffers - Lock-free circular ring buffer for audio (16384 samples), video frame buffer with XRGB8888 → RGBA conversion
- State serialization - Wraps core’s save state functions
- Memory management - Exposes SRAM read/write for battery-backed saves
Static callbacks
Static callbacks
Libretro uses C function pointers, so callbacks are static and route through a singleton:
N-API bindings
N-API bindings
All C++ methods are exposed to JavaScript via N-API:
Audio buffer design
Replacedstd::mutex + std::vector with a fixed-size circular ring buffer:
- Size: 16384 samples (power-of-2 for modulo optimization)
- Lock-free: Producer and consumer run on same thread, no atomics needed
- Overflow handling: Wraps around with
memcpyand modulo arithmetic - Performance: Eliminates mutex syscalls, vector reallocation, and O(n) erase operations
Frame timing and synchronization
Hybrid sleep+spin loop
The emulation loop uses a two-phase approach for sub-millisecond precision:- Sleep phase:
setTimeoutfor bulk of frame time minus spin threshold - Spin phase: Busy-wait the last ~2ms for precise timing
Vsync-aligned rendering
The renderer never draws directly from IPC events. Instead:- IPC frames are buffered in memory
requestAnimationFramereads the latest buffered frame- WebGL draws are aligned with display vsync
- Multiple IPC frames between vsyncs are naturally skipped
SharedArrayBuffer optimization
When available (requiresCross-Origin-Embedder-Policy: require-corp headers):
- Video: Double-buffered SAB with atomic active-buffer flag
- Audio: SPSC ring buffer for zero-copy transfer
- Fallback: Copy-based IPC via
Bufferserialization
shared-frame-protocol.ts for implementation details.
Core management
Core discovery and download
CoreDownloader (apps/desktop/src/main/emulator/CoreDownloader.ts) manages libretro cores:
- Downloads cores on-demand from libretro buildbot
- Stores cores in
~/Library/Application Support/GameLord/cores/ - Supports multiple cores per system (e.g., NES: fceumm, nestopia, mesen)
- Platform detection for
.dylib(macOS),.dll(Windows),.so(Linux)
EmulatorCore abstraction
Abstract base class (EmulatorCore.ts) defines the interface:
- LibretroNativeCore - Native addon integration (primary)
- RetroArchCore - Legacy overlay mode for external RetroArch process
Library management
ROM scanning
LibraryService (apps/desktop/src/main/services/LibraryService.ts) handles ROM discovery:
New-files-first ordering
Processes unknown ROMs before known ones so new games appear in UI immediately.
Zip extraction
Non-arcade systems extract ROMs from.zip at scan time:
- Extracted ROMs cached in
<userData>/roms-cache/with hash-prefixed filenames - Arcade
.zipfiles passed through natively (MAME expects zips) - Cache cleaned up on game removal
Metadata and artwork
ArtworkService integrates with ScreenScraper API:
- Matches games via MD5/SHA-1/CRC32 hashes
- Downloads cover art, screenshots, metadata
- Caches artwork with
artwork://custom protocol - Progressive loading - updates UI as each game resolves
State management
Save states
Managed in the worker process:- Slots 0-9: Manual save states
- Slot 99: Autosave (created on pause/close)
- Per-ROM subdirectories:
saveStatesDir/RomName/state-N.sav
SRAM persistence
Battery-backed saves (.srm files):
- Loaded at game start:
loadSram() - Saved periodically and on exit:
saveScrapeam() - Skips all-zero data (no save present)
- Path:
sramDir/RomName.srm
IPC protocol
All cross-process communication uses Electron’s IPC:Main ↔ Renderer
preload.ts.
Main ↔ Worker
core-worker-protocol.ts.
Performance optimizations
Lock-free audio buffer
Fixed-size circular buffer eliminates mutex overhead and vector reallocation.
SharedArrayBuffer transfers
Zero-copy frame transfer between processes when SAB is available.
Vsync-aligned draws
Renderer draws via
requestAnimationFrame to match display refresh.Dense grid packing
CSS Grid
dense layout with computed aspect ratios eliminates dead space.Viewport culling
Virtualizes library grid for >100 games, reducing DOM nodes from 1200+ to ~40.
Mtime-based cache
Rescanning 2000+ unchanged ROMs completes in under 1 second (stat-only, no re-hash).
Security considerations
Path validation
All file paths are validated beforedlopen() and filesystem operations:
Process isolation
Utility processes provide crash resilience:- Core crashes don’t affect main process or library window
- Each game runs in isolated worker
- Failed workers can be respawned
Related resources
Building
Build system, dependencies, and platform-specific setup
Configuration
Environment variables and configuration options
Libretro cores
Core integration, system support, and BIOS requirements
Testing
Test suite, coverage, and testing guidelines