Skip to content

fps-unlock-timing (2026-10-27)

Unlimited present rate WITHOUT speed-up. Before: fps_cap N made the virtual vsync fire at N Hz (and mtl_present.m hold frames for 1/N), so a higher cap ran the game faster.

What changed

  • runtime/src/timescale.[ch]: fps_mode (game pacing / match display / custom N / uncapped) as a HOST property (timescale_set_fps, timescale_present_period_ns), TS_GAME_VSYNC_NS, shared timescale_deadline_step, NAPPLE_FRAME_TRACE CSV.
  • runtime/src/services/vi.c: vsync period = 1/60 s / Speed only (no gfx rate read).
  • gfx: napple_gfx_vsync_period_ns always 1/60; mtl_present.m (pacing half only): guest-visible frame time is swap interval/60/speed in every mode, the present rate only decides which finished frames are shown (host_min); displaySyncEnabled = !uncapped && vsync (new host_vsync_off setting, wired from the vsync setting which previously did nothing).
  • Settings: fps_mode + fps_cap (custom rate). Migration of old files: fps_cap 0 -> game, 1000 -> uncapped, N -> custom N and Speed N/60 when the file has no speed_percent (decision: keep the old feel explicit instead of silent). Shared menu napple_fps_choice* for the macOS overlay and the iOS panel source.
  • app.m: env NAPPLE_FPS_MODE/NAPPLE_FPS_HZ/NAPPLE_VSYNC. NAPPLE_TURBO is now present-rate only (use NAPPLE_SPEED).
  • tools/fps/frame_trace.py: title x mode matrix, windowed soak.
  • Docs: Timing section in docs/architecture/runtime-services.md, Timing paragraph in each docs/titles/*.md.

Verified

  • make -C runtime test (test_timescale 55 checks incl. 10 simulated seconds at present rates 30/60/144/999/display/uncapped: identical vsync count and game clock, +-0.1 %), test-host, make -C gfx test, make -C frontend test (53 settings checks incl. migration).
  • Headless matrix (20 s, 4 titles x game/60/120/30/uncapped): game clock 1.000, guest vsync 60.00 Hz, presents never above vsync in all 20 runs, all exit 0 except one ACNH start (see below; reran twice OK).
  • Windowed 60 s soaks: vsync drift <0.2 frame everywhere; p99/median present interval: SMO 1.01 (on and off), ACNH 1.25 / 1.01, LAS 2.1-2.2 and MK8D 4.8-5.0 FAIL the 1.5x bar. Those two present at 46-56 fps because of the known gfx throughput limit (GPU/CPU per frame, hitches at scene loads), not pacing; the pacing code is unchanged for them in game mode. Not fixed here (gfx performance).

Not verified / open

  • Headless has no layer, so custom-30 does not drop frames there (trace shows 60 presents); the drop path (host_min) and display-sync off are only exercised windowed and were not trace-measured at custom/uncapped modes.
  • No real extra game frames: per-title timestep patches are the later package; titles' vsync-dependence is inferred (docs/titles Timing).
  • Intermittent ACNH headless start: guest Break (reason 0) at sdk+0xc80730 ~1 s in (1 of ~8 runs, exit 70), independent of fps mode; bug outside this area.
  • HUD already showed game fps and shown fps separately (display.c untouched to avoid a render-scale conflict); a rename to guest/present is a one-word follow-up after render-scale lands.