Skip to content

RustyNES v2.3.0 — "Datum II" (PPU-accuracy capstone + true multi-viewport tool windows)

Choose a tag to compare

@github-actions github-actions released this 05 Aug 12:31
be4fbef

RustyNES v2.3.0 — "Datum II" (PPU-accuracy capstone + true multi-viewport tool windows)

The capstone that closes the v2.2.6 → v2.3.0 NESdev-remediation line. Both remaining forum-reported accuracy concerns were investigated under reproduce-before-fixing discipline and turned out to be already correct — so the substance of this release landed elsewhere: tool panels that open as real OS windows, and a frame-pacing defect that had been quietly degrading every session with a debugger panel open.

AccuracyCoin holds 141/141 (100.00%) and nestest is 0-diff — and the gate now enforces that as an exact count rather than a 60% floor.


Tool windows are now real OS windows

v2.2.9 shipped a detach affordance with an honest caveat: on a single-viewport egui_winit integration, show_viewport_immediate merely embedded the panel inside the main window, so the Windows-10 "every tool window is trapped in the main window" report was not actually resolved. It is now.

The new crates/rustynes-frontend/src/detached.rs gives each detached panel a real winit window with its own egui Context / State / Renderer and wgpu Surface, sharing the main device and queue. It needs no unsafe — unlike egui's native immediate-viewport path, whose re-entrant callback must create a window from an &ActiveEventLoop that is only valid during event dispatch (which is why eframe erases that lifetime into a 'static thread-local). Instead each window renders on its own RedrawRequested, in its own stack frame, where the panel's &mut Nes borrow is freshly re-acquired; a thread-local render target makes the shared panel dispatch paint exactly one panel per window.

Every tool window is detachable now. The nine panels that predated the shared helper — CPU, Cartridge Info / Header, Lua Script, BasicBot, Input bindings, TAStudio, Settings, Netplay, RetroAchievements — were converted, and BasicBot's bespoke embed-viewport implementation was retired in favour of the shared path. Detached windows inherit the main window's theme, UI zoom and locale, and open at the size their docked window actually had (captured at the click, not from a static table). Detach / Reattach use real Font Awesome glyphs; the previous existed in neither the icon font nor the default font and silently rendered as nothing. wasm is single-canvas and keeps panels docked, unchanged.

Known limitation: egui cannot place a control in a native window's title bar — that bar belongs to the window manager — so Detach is a small top-row control rather than sitting beside the close button. Reattach works from the detached window's own button and from its OS close button.

The frame-pacing fix (stutter / high p99)

Profiling the detach work surfaced a root cause that predated it. The overlay-visible render branch held the emulator mutex — in its own comment's words — "until after the present call". Inside Gfx::render_with_overlay, the blocking Surface::get_current_texture runs before the egui pass. So on every frame with the debugger overlay or any nes-reading tool panel open, the winit thread owned the lock across a swapchain wait (up to a full display refresh), the entire egui build, the encode, the submit and the present — while the emulation thread sat parked on emu.lock(), unable to produce a frame. Each detached window would have added another such acquisition, which is why it worsened per window.

DebuggerOverlay::render_shell is now split into run_shell_ui (needs &mut Nes; the caller holds the lock) and paint_shell (pure GPU work, lock released). The framebuffer already came from present_staging, so nes_for_render was the only reason to hold the lock at all. DetachedManager is split the same way.

Separately, pace_frames answered "is a ROM loaded?" by taking the emulator mutex on every about_to_wait iteration — a tight spin in the wall-clock regime — which could block the UI thread for a full produce (~4 ms) each time. It now reads the lock-free EmuControl::has_rom atomic, falling back to the locked read only when no emulation thread exists.

Detached panels also repaint on per-panel tiers — Live (60 Hz) for continuously-changing state, Throttled (~10 Hz) for status, and interaction-only for static panels like Cheats and ROM Info — so a wall of open tool windows costs almost nothing while idle.

PPU: −5.13% frame cost, byte-identical

A measure-first campaign (v2.3.0 P1 in docs/performance.md) that began by discarding its own baseline: the first profile ran on a contended machine at 39% criterion outliers; re-measured quiet it reported 2%, which is the only reason the subsequent deltas mean anything.

perf annotate — the same instrument that redirected the v2.2.3 P4 investigation — showed the cost was not the state machines. In tick_sprite_eval_per_dot the two hottest instructions in the entire body were its own push/ret: pure call overhead across 89,342 calls per frame, which LLVM had declined to inline. And tick_oam_bus derived sprite_height and the y-test reference before its dot-0 early-out, computing and discarding both.

Workload before after change
nes_run_frame_nestest_fast 3.8987 ms 3.7830 ms −5.13% (95% CI −5.60…−4.60, p = 0.00)
nes_run_frame_flowing_palette_fast 2.7314 ms 2.6354 ms −3.51% (95% CI −3.93…−3.10, p = 0.00)

Both clear the project's >3% adoption bar on both workloads. Byte-identity was verified, not asserted: AccuracyCoin 141/141 through the exact-count gate, the nestest golden CPU log 0-diff, and rustynes-ppu 91/91 unit tests.

Accuracy: both forum items verified already-correct

The SMB left-edge column and the Rad Racer hybrid-address render were both investigated by reproducing first — and both were found already correct in the shipped build, resolved earlier by the v2.0.0 "Timebase" rewrite and the v2.0.3 2-cycle-ALE promotion, predating the report. SMB's leftmost background column renders real content matching the blessed golden; the hybrid-address model passes the authoritative AccuracyCoin "Hybrid Addresses" test and renders Rad Racer's road and horizon cleanly. Disabling the delayed-CopyV drops exactly that test to 140/141, which is what the new exact gate now guards. See ADR 0030's v2.3.0 update.

Honesty and provenance

  • The ≤ 2 ms frame-cost figure is now labeled a design-phase aspiration, not a gate. It was written before the cycle-accurate core existed, for 2018-era hardware. The core measures ~3.8 ms (~23% of the NTSC budget) and that is knowingly accepted: the dominant costs are work the accuracy model requires, and the obvious levers were already measured and rejected (emit_pixel bounds-check elision and the SIMD blitter both made the shipped default slower). Recorded so no contributor optimizes toward it by trading away accuracy.
  • TriCNES is no longer described as "transistor-level." It is a cycle-accurate C# emulator built on a detailed sub-cycle state machine. "Transistor level" properly denotes die-derived simulations such as Visual2C02 / phantom2c02, which the repository already cites correctly. Corrected in source, NOTICE, README, ADR 0030, and the published v2.0.2 / v2.2.5 release notes — it remains the correct oracle for the ALE/hybrid-address tests, on the strength of its sub-cycle model.
  • GeraNES reference comments corrected. Dangling source-file paths and a quoted C++ line were reworded to state honestly that its source was consulted as a cross-reference confirming nesdev-documented behavior, with no code copied. Reviewed two-sided against the upstream source and the nesdev register maps: the coinciding operations are the single correct expression of a documented register layout. That is the maintainers' assessment offered for expert review, not a certification — this project does not self-certify license cleanliness, and the residual uncertainties are named in docs/originality-and-provenance.md §3.
  • libretro core license declared as GPLv3, the notation mesen / melonDS / bsnes use; the SPDX GPL-3.0-or-later stays in the Cargo metadata.

Compatibility

Save states, movies (.rnm), and netplay replays are unchanged from v2.2.x. The deterministic #![no_std] chip stack is byte-identical except for the intentional, oracle-gated PPU optimization above, which is byte-identical by construction and verified as such.