feat(presenter): add opt-in split-rate high refresh presentation
Summary
Adds an opt-in high refresh mode. The game still simulates at its authored 60 Hz. The presenter captures each tick's draws and renders the extra display frames by interpolating the camera, skinned characters (bone palettes) and matched props between the last two ticks. With RECOMP_SPLIT_RATE unset, every split path is skipped and behaviour is the same as main.
Settings (docs/building.md, design in docs/wayfinder/research/high-frame-rate-split.md):
RECOMP_SPLIT_RATE=autouses the display refresh rate. A number (100/120/144/240ā¦) sets the rate; with--vsync, rates above the display are lowered to it, and rates within 1% snap to it.RECOMP_SPLIT_STRICT=1turns fallbacks back into hard stops, for test gates.
When interpolation hits a case it doesn't model, that draw shows its normal 60 Hz frame and the reason is logged once. Closing the window during any presenter call now exits normally.
Known limits, documented in the review doc: water and some non-character objects (the shop preview) still step at 60 Hz; added input latency is about one tick and hasn't been measured; non-divisor rates (144 on a 240 Hz display) judder.
Overlap: 4e5b809 carries the same window-close fix as #80. Whichever merges second should drop its copy.
Verification
public-cichecks run locally on this branch: tools tests,public_export.py verify --require-public-tree, public Debug build, and ctest. All passed.- Agent smoke runs (attract demo, 4.5Ć, 8x MSAA, SMAA, --vsync) with split off,
auto(240 Hz display) and144: all closed cleanly with exit code 0 and no fallbacks. PresentMon showed 234.8 displayed frames/s atautoand 60.0 with split off. The user's play test of this exact build is still pending.
Contribution checklist
- The change follows
AGENTS.mdandCONTRIBUTING.md. - No XBE, generated game C, retail instruction bytes, extracted assets or
filenames, saves, BIOS data, private paths, or run evidence are included. - Third-party material and its license are identified, if applicable.
Summary by cubic
Adds an opt-in high refresh presentation mode that interpolates between 60 Hz simulation ticks without changing gameplay timing. With RECOMP_SPLIT_RATE unset, behavior is identical to main.
RECOMP_SPLIT_RATE=autouses the display refresh rate; a numeric value sets the rate. Rates above the display are lowered with--vsync, rates within 1% snap to it, and split disables below 30 Hz displays.- Interpolation covers the camera, skinned characters, the ball, and single-draw rigid props. Unmodeled cases fall back to the normal 60 Hz frame and log the reason once.
RECOMP_SPLIT_STRICT=1turns fallbacks into hard stops for test gates.- Known limits: water and the shop preview still run at 60 Hz, added input latency is about one tick, and non-divisor rates judder.
- Closing the window during any presenter call now exits normally instead of stopping with an error.
Reliability and tooling
- Interrupted or failed pin updates restore the previous selection and metadata; manual-call targets are validated and pinned like the rest of the recipe.
- Presenter error handling is unified so a window close is treated as a normal exit from every call site, including memory release.
- Split replay refreshes point-light transforms, rejects invalidated palette bindings, and now preserves range fog; sub-30 Hz display modes resolve without splitting.
- Recapture span indexes are reused across packets to avoid redundant hashing in the capture path.
- Review findings added deferred frame-dump bursts with a pre-created staging pool, idle-time texture preparation for the D3D11 backend, a synthetic backend replay test, and abort-free frame dump row allocation; oversized fixed dump counts are rejected.
Verification
public-cichecks pass on this branch, including public Debug builds and ctest.- Smoke runs with split off,
auto, and144all exited cleanly; PresentMon measured 234.8 displayed frames/s atautoversus 60.0 with split off. - A user play test of this build is still pending.