Skip to content

RustyNES v2.3.6 — "Sounding" (measuring, and what a measurement may claim)

Latest

Choose a tag to compare

@github-actions github-actions released this 18 Aug 04:19
3f99a3d

RustyNES v2.3.6 "Sounding" is about measuring — and about what a measurement is allowed to claim. A sounding is a depth taken with its uncertainty attached, and that is what every workstream here has in common. Two features that shipped in earlier releases turn out never to have worked at all. Two new tools are added, both built to decline to answer rather than guess. And an optimization campaign is closed on the strength of three measured rejections and the mechanism that explains them.

The accuracy contract is verified, not asserted. The emulation core gains one const fn getter and nothing else, but AccuracyCoin was re-run regardless: 141/141 on the authoritative RAM decoder, nestest 0-diff.

Two shipped features never worked

Pixel Provenance returned an empty report for effectively every user

The v2.3.2 "Lucid" marquee — click a pixel, get its whole causal chain — has been non-functional since it shipped, for two independent reasons.

Run-ahead erased the record before the UI could read it. Run-ahead defaults to 1, and its per-frame rollback cleared both provenance stores. That clear is correct for a save-state load and for netplay rollback. It was wrong here for a reason unrelated to the restore itself: run-ahead's rollback is the last thing before the frontend releases the emulator lock, so the panel's first opportunity to look was always after the wipe. It did not discard a stale timeline; it discarded the record for the frame on screen.

Clicking a pixel was never implemented. The panel offered two coordinate spinboxes and no hit-test at all, while the docs and release notes said "point at" a pixel.

The reason this survived four releases is worth stating: two source comments and four documentation claims asserted the opposite of their own code. One comment said the clear left "exactly the visible frame's writes — which is the timeline the user is looking at", two lines above code that emptied both stores completely. Prose asserting an intent is what stopped anyone checking whether the code met it. The corrections keep the wrong text, quoted, alongside the evidence that disproves it.

Duck Hunt could never score

Duck Hunt's Zapper protocol is "see nothing for one frame, then a bright spot in the next". The light bit was sampled at end-of-frame, so a read during frame N returned frame N−1 — the probe was exactly inverted against the protocol the game relies on. A second defect had to go with it: the aperture sampled rows the beam had not finished painting.

Score went 000000 → 000500.

The Latency Oracle

Tools → Analysis → Latency Oracle. How many frames of input lag does this game have, and what run-ahead depth removes them?

Every emulator makes finding that number a manual ritual — hold a direction, frame-advance until the sprite moves, subtract one. RetroArch documents exactly that procedure; RustyNES's own settings panel said only "1 fits most games". Nothing measured it.

It measures it: replay the current moment twice, once with a button held and once with nothing pressed, and report the first frame that differs. On a deterministic core two replays of identical state can differ for exactly one reason, so the answer is a property of the ROM rather than of the run.

Being honest is the hard part, because a latency number gets acted on:

  • None and Some(0) are different answers and are never collapsed. Some(0) means the game reacted on the next frame; None means the probe could not tell.
  • Six buttons across three observables (framebuffer → audio → work RAM), requiring agreement. START is deliberately excluded: it pauses many games, which is a reaction — to a menu, not to gameplay — and counting it would over-report.
  • Per-button evidence is shown even for confident results. A tool that publishes only its conclusion cannot be checked.
  • It recommends; it never applies. Each extra frame of run-ahead costs roughly a whole frame of emulation, so raising it can drop frames on a marginal host. The depth appears with an explicit Apply button, and a test fails if storing a report ever queues a config write by itself.

The RAM Atlas

Tools → Analysis → RAM Atlas. What is each byte of the 2 KiB work RAM for?

Not "which addresses hold 42" — every emulator answers that, this one included, with RAM Search, RAM Watch and per-address access counts. Those narrow a set you already have a hypothesis about. None of them says what an address is, because observation alone cannot: an address counting up while the score counts up might be the score, or a frame counter that happens to be running.

Two stages, with deliberately different confidence:

  • Observation is correlation. Every address is labelled untouched / frame tick / rising / falling / sparse / volatile. All 2048 labels come back marked untested, so observation is structurally incapable of claiming an effect.
  • Verification is a fact, and a narrow one. Poke the byte, re-simulate from the same anchor, compare.

The honesty properties are the design, not decoration:

  • Liveness is relative to its lens, and every verdict names the one it used. The same byte is routinely Live through work RAM and Inert through the framebuffer.
  • Untested is a third state, distinct from Inert: "we did not look" and "we looked and saw nothing" are different claims. An unaffordable verification spends zero trials rather than a wasted baseline.
  • Inert is not "unused." A byte the game rewrites from a master copy every frame reads inert because your change is overwritten before it can matter.
  • Every threshold is public, so the panel can show "changed on 178 of 179 transitions, at or above the 90% frame-tick threshold" beside the label. A cutoff you cannot see is one you cannot disagree with.

There is no "verify everything" button: verification costs two re-simulations per address, so all 2048 would be over four thousand runs and tens of minutes. The batch is capped at 16 and skips untouched addresses.

The menus are regrouped by task

Tools had reached twenty flat entries spanning cheats, TAS authoring, media capture, multiplayer, ROM inspection and provenance analysis. Debug listed "CPU" and "Lua Script" as peers in a fifteen-item column.

Tools is now Cheats at the top level, then Movies & Recording, Audio, Input, Game Data, Analysis and HD Pack — with Netplay and RetroAchievements below a separator, since they change what the session is rather than being tools you point at the game. Debug splits into Chip State, Memory and Execution, plus Symbols. Emulation's two FDS entries become one Famicom Disk System submenu.

No entry is removed, none changes what it dispatches, and nothing moves between top-level menus — only the depth at which it sits.

APU Workstream D is closed

The 18.7% of frame time figure stands: it is a correct v2.3.1 attribution, visible only because that pass attributed by source file, since fat LTO inlines the APU into cpu_clock and hides it from a symbol profile. What is settled is narrower — the figure is not recoverable by gating per-cycle bookkeeping, the only strategy the workstream ever tried.

One adoption (C1, shipped in v2.3.5 at −3.3% to −4.2%), three measured rejections, one declined on inspection, two left unmeasured deliberately.

The three rejections share one cause, and it generalises: under lto = "fat" with codegen-units = 1 the guarded code is already inlined, its repeated loads already merged, and the elided branches always-not-taken and perfectly predicted. The rule worth keeping — "this work is inert on almost every cycle" predicts a win only if the work is actually executed, and under fat LTO with perfect prediction it largely is not.

One measurement lesson is recorded because it will recur: D1's first A/B run looked like a textbook win at −3.81% (p = 0.00) on a shipped default workload and was entirely an artifact. The order-bias control, which benches the reference against itself, drifted −3.73% on that same workload with no code change at all — because the harness benchmarks the reference immediately after a 44.9-second fat-LTO compile across all cores, on a machine that has not cooled down.

Three defects the review of the RAM Atlas caught

Worth naming, because all three are the kind that ship silently:

  • The audio observable was structurally dead. Observable::AudioEnergy never saw any audio — the trial loop emptied its buffer and never filled it, so every frame of every trial reported zero energy. Nothing failed, because a lens that returns a constant never disagrees with itself: the Latency Oracle's audio fallback silently degraded to work RAM, and the RAM Atlas's audio lens would have called every address inert. The comment above the missing call read "Drain EVERY frame, whatever the observable".
  • The Atlas was not gated during locked sessions. Both actions advance the live emulator and Verify pokes memory, so under netplay or a TAS record/replay it would diverge a timeline other peers are lockstepped to, and under RetroAchievements hardcore it is the write that mode exists to forbid. It now reads the same predicate emu.write uses.
  • An address outside work RAM was reported Inert. The poke was skipped, the two identical trials agreed, and a confident verdict was issued for a byte never touched — the precise failure mode that module's own documentation warns against.

Also in this release

  • rustynes-probe, the deterministic re-simulation engine both new tools consume: anchor, replay under controlled variation, locate the first divergence. Budgeted, and the budget is binding rather than advisory.
  • A probe trial no longer clears or pollutes the caller's rewind ring. This is a bug an earlier fix in this same release reported as closed and did not close — it changed only the final restore, leaving every trial's restore untouched, so the ring was still being cleared 21 times per measurement. Fixing it revealed a second defect underneath: the ring then grows, with replayed frames that never happened on your timeline. Separately pinned: no measurement taken before the fix needed re-running.
  • Attestation tests closing the last item of #360.
  • Docs: specs for both new tools, a user-facing Analysis tools guide page, and a corrected menu reference. docs/pixel-provenance.md was also added to the docs-site nav, having been built but unreachable since v2.3.2.

Not fixed here

  • libretro/docs#1180 — the licence correction on the libretro documentation site is still open upstream.
  • The upstream libretro .info still reads v2.3.5, deliberately. RetroArch reads dist/info/rustynes_libretro.info from libretro/libretro-super, a separate copy nothing syncs automatically; upstream syncs are batched to MINOR releases, so the next one is v2.4.0. A stale display_version misreports a number; the thing that actually went wrong in v2.2.9 was a stale licence, and anything touching licence, supported extensions, or declared capabilities still syncs immediately regardless.
  • Latency Oracle and RAM Atlas persistence and export paths — per-game persistence, and seeding the Watch/Cheat panels, the Lua API and RetroAchievements authoring from an atlas. Additive, and better shaped once the labels have been used in anger.

Two upstream items did land since v2.3.5: libretro-super#2069 (RetroArch now reads GPLv3+) and RetroArch#19416, which adds RustyNES to the Apple App Store core list — so iOS/iPadOS/tvOS availability arrives with the next App Store RetroArch build, on libretro's cadence.