Skip to content
RustyNES v2.3.8 "Parallax" — which pixels differ, not just which frame

Parallax is the apparent shift of an object seen from two positions, and
the displacement is the measurement. That is what this release adds.

`Probe` could already say whether two configurations of the same ROM
diverge and AT WHICH FRAME, and could say nothing about where or why. A
trial reduces each frame to one u64 — the right shape for detecting a
difference and the wrong shape for explaining one. A hash says frame 412
differs and has nothing to hand to Pixel Provenance, which is where an
answer actually lives.

THE DIVERGENCE LENS

`divergence::localise` re-runs both configurations to the detected frame,
keeps the full output instead of its hash, and reports the SHAPE of the
difference: population count, first pixel in raster order, and the
inclusive bounding box. Count and box separate kinds of bug from each
other — one pixel is a sprite or a palette entry, 256 in a row is a
scanline, tens of thousands is a scroll or a mode change.
`is_single_scanline` is offered rather than left to call sites, because
it is the distinction a caller acts on and the inclusive comparison is
easy to get wrong.

It localises on the INDEX framebuffer — 256x240 u16s of
`(emphasis << 6) | colour`, the PPU's own per-pixel output before the
palette lookup. Half the bytes and at least as sensitive, because the
RGBA buffer is a pure function of it given the same palette. That proviso
holds because both trials run on the same instance and therefore share
whatever palette is loaded; it is written down rather than assumed,
because a future two-instance lens would have to revisit it.

THREE ANSWERS, AND THE THIRD IS THE POINT

`Identical`, `Differs`, and `Inconclusive` — for an exhausted budget, or
two trials that cannot be compared. The Latency Oracle's precedent
applies directly: `None` and `Some(0)` were never collapsed there, and "I
stopped looking" must not arrive wearing the same shape as "they agree".

The budget is checked UP FRONT for all four trials, mirroring
`atlas::verify_liveness`. Spending two on detection and then finding the
localisation pair unaffordable would consume the budget that would have
answered the question. Detection disagreeing with the pixel diff also
returns `Inconclusive` rather than picking a winner.

THE ANSWER IS A CAUSE, NOT A COORDINATE

Trial-scoped provenance capture hands a located pixel to the machinery
that already answers "what wrote this, and from which instruction", so
`localise_explained` returns the diverging pixel's provenance from BOTH
configurations: the winning layer, the pattern row, the nametable and
attribute addresses, the palette entry, the emphasis mask.

That closed the sub-frame item WITHOUT BISECTION, and the plan's framing
was wrong rather than merely unresolved. It offered a choice between
bisecting the frame and riding the records; proving the cheap option
viable produced a third answer that is better rather than cheaper. A
cycle index says WHEN. A differing `pattern_addr` says the two runs
fetched different tile data, which is a lead. Bisection would have spent
about seventeen extra trials to produce the weaker answer, so it is not
implemented — as a decision, recorded in `docs/divergence-lens.md` so the
question is not rediscovered. It remains the only route in a build
without `debug-hooks`.

For audio the records ride directly, so `localise_audio` resolves a
divergence to the CPU CYCLE.

FIXED, INSIDE THE SAME WORK

The Lens left the emulator THIRTY FRAMES AHEAD of where it started. A
trial restores the anchor on the way in and not on the way out — which is
deliberate, and is what lets the Lens read the trial's final frame
straight off `nes` — but the outermost caller has to put the timeline
back, and did not. Through the panel, asking a question advanced the
user's game. Both entry points now restore through one wrapper, and that
restore is guarded so it does not clear the caller's provenance, which is
the v2.3.7 defect one layer up.

TWO NOTES WORTH KEEPING

No new engine primitive was needed, and the estimate that one would be
was wrong in the cheap direction. The same trial-restore asymmetry that
caused the bug above is what makes the diverging frame readable.

A documentation claim was retracted mid-implementation.
`differing_fields` was documented as possibly empty at a located pixel.
It cannot be: an index-framebuffer entry is `(emphasis << 6) | colour`,
so a differing entry means colour or the emphasis mask differs. Under the
permissive reading an empty result is something to render; under the
correct one it means the two records did not come from the two
configurations. A mutation reading both from the same trial was caught
only once the assertion was tightened to match the stricter claim.

WHERE IT LIVES, AND WHAT IT IS CUT FROM

Tools -> Analysis -> Divergence Lens, over a headless
`rustynes_probe::divergence` core tested independently of it. Spec:
`docs/divergence-lens.md`.

This tag is cut from v2.3.8's own boundary commit rather than from
`main`, which had already advanced six PRs into v2.3.9. Its artifacts
therefore contain exactly Parallax and nothing from Crucible.

ACCURACY

`rustynes-apu`, `rustynes-ppu` and `rustynes-core` all change, so
AccuracyCoin is VERIFIED rather than asserted: 141/141 (100.00%) on the
authoritative RAM decoder, nestest 0-diff. Also green on this tree: fmt,
workspace clippy under -D warnings, the no_std thumbv7em cross-compile,
and every workspace suite.