RustyNES v2.3.3 "Cadence" is the display-pacing release. It closes the one measured artefact whose signature matches the long-standing report that the picture "shudders" — and the cause turned out to be a stale statistic rather than a bad threshold.
No emulation-core changes. Every change is frontend or output-only, so AccuracyCoin holds at exactly 141/141 and nestest is 0-diff — verified after the fact, not asserted by construction.
What was actually wrong
The run-ahead throttle was oscillating: at run_ahead = 2 it changed depth 6-7 times per 24 s, and every depth change displaces the displayed frame by the run-ahead depth. That is the picture jumping forward and back.
Six mechanisms were proposed and falsified by measurement before this one surfaced. The throttle is gated to one depth change per median window — but the gate counted 120 frames, and the ring feeding it holds 600:
| value | |
|---|---|
| gate, as written | 120 frames |
ring capacity (perf::WINDOW) |
600 samples |
| where the p50 sits | index 300 of 600 |
| turnover after 120 frames | 20% |
A p50 at index 300 cannot be moved off the previous depth by a fifth of a window. Per-evaluation logging showed transitions arriving in immediate pairs sharing a median to three decimal places:
THR check depth=2 steps=0 cost=12.958 engage_band=12.479 engage=true
THR check depth=1 steps=1 cost=12.958 engage_band=12.479 engage=true
The second line of each pair is decided on a measurement of the depth the first line just left. The release predicate was never wrong — its input was.
The fix, and what it measures
Expressing the gate in terms of the ring it reads, so the two cannot drift apart again:
| before | after | |
|---|---|---|
depth changes / 24 s (run_ahead = 2) |
6-7 | 1 |
| spurious releases | 2 | 0 |
| frames held for the wrong duration | — | 1.31% |
At depth 1 the honest median now reads 8.670 ms, matching the independently measured depth-1 cost of 8.671 ms, predicts 13.005 ms one depth up, and correctly declines to release.
Converging without waiting
The corrected window is right and it cost convergence speed: run_ahead = 3 reached a sustainable depth but spent ~12 s over budget getting there, one full window per step.
The naive asymmetry — engage on less evidence — is a bug, not a fix: at depth 3 the cost exceeds the band at every depth, so it would cascade to depth 0 and discard the feature. Instead the engage arm computes the next depth's cost from the per-frame-linear model, stepping while the prediction is still over budget, and stops where it fits. Releasing is deliberately unchanged and still demands a full window and a real measurement.
| arm | converge | frames wrong | underruns |
|---|---|---|---|
| previous | 12.12 s | 4.82% | 0.60 |
| shipped | 2.80 s | 2.24% | 0.20 |
| ring-reset (rejected) | 4.00 s | 2.02% | 1.00, every capture |
5/5 paired Latin-square rounds favour the adopted arm on both metrics, exact one-sided sign p = 0.0312 — five rounds rather than three because the exact paired test floors at 1/2^N.
The apparatus the diagnosis needed
- Compositor refresh from
wp_presentation. winit'scurrent_monitor()answersNonefor an entire session on compositors that advertise nowl_output, so the refresh is read from the compositor's ownpresentedevent. - Divisor-based display-sync — 1:1-only excluded every 120/144 Hz panel.
- A validity gate that fails closed. An occluded window makes the compositor
discardevery frame, so the refresh estimator never settles and the session silently rides the wall-clock fallback — worth 61-147 dropped frames per 45 s against 6-15.PresentationClock::discarded()had existed since scanout tracing landed and was called by nobody. A diagnostic nobody surfaces is not a diagnostic. - A per-frame trace plus an analysis script that reports lag-1 autocorrelation, run-lengths, and cadence.
Dropped frames fell from 135-254 to 1-9 per capture, and audio underruns to zero.
Rejections, recorded with their numbers
- Slim run-ahead restore — projected ~110 µs, measured 6.9 µs: 0.25% of the run-ahead increment. The estimate came from "the framebuffer is 94% of the snapshot bytes" carried silently into a claim about time. Bytes are not time.
- Ring-reset throttle gate — converged in 4.0 s and matched on cadence, but produced an audio underrun in every one of its three captures.
- Ten core hot-path candidates were measured and rejected in v2.3.1 and remain so.
Corrections to this project's own published numbers
This release also retracts and corrects earlier claims from the same campaign, in place rather than quietly:
- A display-duration metric was computed from producer-side timestamps and was wrong by 20× — the display was ~98.4% correct while the document said 65-74%.
- A percentile was derived by subtracting other percentiles, producing a published "p95 below p50", which is impossible.
- A statistical claim quoted an unpaired p-value for a paired design; alternation balances drift direction but does not buy exchangeability.
What this does not claim
It does not claim the maintainer's reported shudder is resolved. That is a subjective report from a different machine whose frame-budget margin has never been measured, and this campaign already declared victory once on counter evidence and was wrong. What it closes is the one measured artefact matching that description.
Full detail — including every rejected mechanism and its numbers — is in docs/performance.md under the v2.3.3 F1-F28 entries.