-
Notifications
You must be signed in to change notification settings - Fork 0
Emulation Timing
The Linux port runs each game in a separate game-host process. The emulation loop and the GL/Vulkan window live there; the library window only feeds it a target geometry. Presentation is a native Wayland toplevel with a vsync'd EGL swap. See Emulator/EmulatorSession.cs for the decoupled loop.
The loop is audio-master-clocked with dynamic rate control (DRC) — RetroArch's model. Each frame, SdlAudio.ApplyDrc() nudges the resampler frequency ratio by ≤0.5% to hold the audio input queue at a target cushion. That servo is the primary sync mechanism: the core is paced to its content rate (_fps) and audio is resampled to match, instead of letting raw buffer drain dictate the loop. Coarse watermark guards (backpressure / low-watermark catch-up) are only far-extreme backstops.
SdlAudio exposes two buffer readouts and you must use the right one for the right job:
-
QueuedMs— a wall-clock estimate (total samples enqueued minus playback time elapsed). Use it where you want a smooth time-based figure. -
QueuedMsReal/QueuedMsSmoothed— reflect the actual device drain, so they're the correct control signal for pacing and DRC. The coarse guards servo the smoothed value so a single device gulp can't fire a spurious drain-stall.
The in-loop pacing is a lever for A/B testing on real hardware:
-
stopwatch(default) — high-res frame timer to the content rate; sleep most of the budget, spin the last ~1 ms. Audio thresholds are guards only. -
audio— sound-clock: after eachretro_run()adds ~1 frame of audio, wait for the device to drain back to the cushion. The device consumes at exactlysample_rate, so this paces the loop to real time at the core's true rate with no timer wobble. -
spin— pure busy-spin to the budget (lowest timer wobble, burns a core).
DRC is opt-in via EMUTASTIC_DRC=1 outside the GL spike path; the default decoupled GL path applies it every frame.
HW cores (Dreamcast, GameCube, N64 GL path) run in the decoupled loop, paced by the audio
each retro_run actually produces — the game's own clock. The loop measures the
sample-frames enqueued during the call, smooths that with an EMA, clamps it to 0.5×–4× of
the nominal rate (the handler's HardwareTargetFps, e.g. Dreamcast forces 60, else the
core's av_info.timing.fps), and uses the result as that iteration's frame budget. Silent
scenes fall back to the nominal rate. The present thread is vsync-paced separately and
shows the latest produced frame; a missed vblank repeats a frame instead of slowing the core.
This is why 30fps titles need no special-casing: a title that advances two game frames
per retro_run emits ~33ms of audio per call, so its budget is ~33ms and it settles at
30 calls/sec. (Pacing to the nominal rate instead — the pre-fix behavior — flooded the
audio queue at 2× realtime; see the Dreamcast page for the bug's history.)
One subtlety in the overfill backstop (the audio cushion drain): the loop enters the drain on the smoothed occupancy (so a single device gulp can't trigger a spurious stall) but exits on the live queue — the smoothed value is only refreshed at the loop top, so polling it inside the wait reads a frozen number and the wait never ends until its guard expires. That frozen read was a long-standing multi-second-stall bug, fixed alongside the audio-progress pacing.
EMUTASTIC_GL_PRESENT_THREAD (on by default) runs the core on the emu thread paced as above, while a separate present thread owns the GL window and shows the latest frame at vsync. The blocking FIFO swap on Wayland provides the vsync backpressure; re-presenting a duplicate on a slow frame is correct. This is what decouples display hitches from emulation speed. A single-threaded "swap-is-the-clock" path (EMUTASTIC_GL_PRESENT_THREAD=0) and an opt-in Vulkan overlay (EMUTASTIC_VULKAN=1) exist for A/B comparison.
The Windows build had a class of bugs rooted in WaveOut draining in steps synchronized to the DWM compositor rate (the notorious "N64 locked at 48fps on a 144Hz monitor" — 144 ÷ 3 = 48 drain steps/sec). The Linux port has neither WaveOut nor DWM: audio goes through SDL3 audio streams whose drain we read directly, and presentation is our own vsync'd Wayland toplevel. The compositor-synchronized-drain failure mode therefore doesn't exist on Linux — frame pacing is governed entirely by the audio-master clock + DRC and the present thread's vsync swap described above.
Console Notes
- Nintendo 64
- Nintendo 3DS
- GameCube
- Sega Saturn
- Dreamcast
- PlayStation
- PlayStation Portable
- TurboGrafx-CD
- Neo Geo
- Arcade
- Vectrex
- Philips CD-i
- Atari Jaguar
Features
- Artwork & Metadata
- Cheats
- Cloud Sync
- Controllers
- Disc-Based Systems
- Disk Swapping
- Portable Mode
- RetroAchievements
- ROM Hacks
- Hardcore Compliance
Technical
Platforms
Legal