The failure
same-process replay diverged in the RetroArch adapter gate. Two replays of one boot state and one input log, inside one process, agree byte for byte through emuFrame 811, then differ on channel values at identical frame numbers.
expected ch_c581_c582: 16665 ch_c57e_c57f: 9000 ch_c4e6...: 4294902274
actual ch_c581_c582: 404 ch_c57e_c57f: 9001 ch_c4e6...: 4294902279
Identical emuFrame values in both passes rule out a miscounted advance. The diverging channels are counter-shaped, which is what accumulated timing drift inside the emulator looks like.
Rate
Measured 2026-08-27 across pull requests and main:
| period |
pass |
fail |
| before the frame-advance barrier fix |
0 |
4 |
| since that fix |
3 |
2 |
requiredSampleSize for a 20% → 80% effect at alpha 0.05, power 0.8 is n≈10 per arm. There are 4 and 5. The barrier fix appears to have helped and provably did not eliminate the divergence.
What was already tried
gap() sends one run-loop iteration between frame advances and is a barrier: FRAMEADVANCE returns as soon as RetroArch accepts it, so without an intervening iteration the next advance can be issued while the previous frame is still settling. command returns null when it gives up and gap discarded that return, so a barrier that did not happen looked exactly like one that did. That is fixed and the failure still recurs.
Ruled out: COMMAND_TIMEOUT is 5s with a resend loop, so a timeout there means RetroArch is wedged rather than busy.
Also seen once: RetroArch never reported PAUSED — a boot failure, a separate infrastructure symptom on the same job.
Current state
The job is quarantined out of requiredCiJobs in release-gate.mts. It still runs on every pull request and its result is still visible; a release no longer claims RetroArch replay as a verified property.
That was chosen over the alternative that actually occurred twice: rerunning the job until green and then tagging, which launders a red into a green and is the exact failure the 0.9.0 gate was built to stop.
To close this
Root-cause the divergence and restore the job to requiredCiJobs. A test case in release-gate.test.mts asserts the quarantine and will fail on restoration, deliberately, so putting it back is a conscious act.
The repo's own docs/adapters.md already notes that several libretro cores "are known to be non-reproducible across processes and would honestly be trusted-recorder." This is worse than that: it is non-reproducible within one process. If root-causing fails, changing RetroArch's declared verification mode is the honest alternative.
The failure
same-process replay divergedin the RetroArch adapter gate. Two replays of one boot state and one input log, inside one process, agree byte for byte throughemuFrame 811, then differ on channel values at identical frame numbers.Identical
emuFramevalues in both passes rule out a miscounted advance. The diverging channels are counter-shaped, which is what accumulated timing drift inside the emulator looks like.Rate
Measured 2026-08-27 across pull requests and
main:requiredSampleSizefor a 20% → 80% effect at alpha 0.05, power 0.8 is n≈10 per arm. There are 4 and 5. The barrier fix appears to have helped and provably did not eliminate the divergence.What was already tried
gap()sends one run-loop iteration between frame advances and is a barrier:FRAMEADVANCEreturns as soon as RetroArch accepts it, so without an intervening iteration the next advance can be issued while the previous frame is still settling.commandreturns null when it gives up andgapdiscarded that return, so a barrier that did not happen looked exactly like one that did. That is fixed and the failure still recurs.Ruled out:
COMMAND_TIMEOUTis 5s with a resend loop, so a timeout there means RetroArch is wedged rather than busy.Also seen once:
RetroArch never reported PAUSED— a boot failure, a separate infrastructure symptom on the same job.Current state
The job is quarantined out of
requiredCiJobsinrelease-gate.mts. It still runs on every pull request and its result is still visible; a release no longer claims RetroArch replay as a verified property.That was chosen over the alternative that actually occurred twice: rerunning the job until green and then tagging, which launders a red into a green and is the exact failure the 0.9.0 gate was built to stop.
To close this
Root-cause the divergence and restore the job to
requiredCiJobs. A test case inrelease-gate.test.mtsasserts the quarantine and will fail on restoration, deliberately, so putting it back is a conscious act.The repo's own
docs/adapters.mdalready notes that several libretro cores "are known to be non-reproducible across processes and would honestly betrusted-recorder." This is worse than that: it is non-reproducible within one process. If root-causing fails, changing RetroArch's declared verification mode is the honest alternative.