diff --git a/.github/release-notes/v2.1.8.md b/.github/release-notes/v2.1.8.md new file mode 100644 index 00000000..b2667207 --- /dev/null +++ b/.github/release-notes/v2.1.8.md @@ -0,0 +1,26 @@ +# RustyNES v2.1.8 — "Fathom" (performance — "Tempo") + +The **performance** step of the v2.1.5 → v2.2.0 "deepen the existing project" run — profile-first optimization of the hot render + display paths, every change proven byte-identical (or shipped default-off behind a runtime knob). The shipped default build is **byte-identical**: **AccuracyCoin 141/141 (100.00%)**, nestest 0-diff, `visual_regression` / `pal_apu_tests` unchanged. Two PRs (#284, #286). + +## Performance + +### Specialized visible-scanline fast PPU dot path (A1) — #284 + +Profiling (`perf` on a mixed corpus) put `Ppu::tick` at **46.5% of frame self-time** — the real hot spot. A1 adds a straight-line fast path for the common visible-scanline case (visible scanline, dots 1..256, rendering stably enabled, no sub-dot disturbance), running the identical helper sequence with the statically-dead event branches pruned; the instant anything sub-scanline happens (a `$2000/$2001/$2005/$2006/$2007` write, a `$2007` read during render, a sprite-0-hit dot, rendering toggling, an armed OAMADDR corruption from the v2.1.7 `Rp2c02G` model, etc.) it **drops to the exact 2-cycle-ALE loop**. + +- **Measured: ~12% faster** on the rendering-heavy `nes_run_frame_nestest` (4.54 → 3.98 ms/frame); neutral on the rendering-disabled path. +- **Byte-identity is the contract, and it's proven:** a new `fast_dotloop_diff` differential test runs the corpus (nestest, flowing_palette, oam_stress, AccuracyCoin, MMC1/MMC3 Holy Mapperel, the scanline demo — and a dedicated `Rp2c02G` OAMADDR-corruption dimension) through **both** the fast and exact paths and asserts **bit-identical** framebuffer + palette-index buffer + audio + CPU cycles + full snapshot, every frame. +- **Shipped default-OFF** (opt-in `Nes::set_fast_dotloop`) — as the roadmap's highest-risk item, it's held off the shipped default this cut so the build stays byte-identical, with promotion-to-default recommended after review + a clean-host Criterion confirmation. +- Honest architectural finding: a *whole-scanline batch* is precluded by the v2.0.0 lockstep every-cycle-bus-access scheduler (the PPU is never invited to run a scanline uninterrupted), so this is a per-dot specialization, not a dot-batch. + +### Vectorized software blitter + wasm size/startup (A2/A4) — #286 + +- **A2 — SIMD software blitter** (`crates/rustynes-frontend/src/gfx_blit.rs`): scalar / `blit_u32` / `wide::u32x8` (desktop) / `core::arch::wasm32` `v128` (`+simd128`) / scalar-fallback paths for the palette-index → RGBA conversion, all asserted **SIMD == scalar byte-identical** (`simd_equals_scalar_byte_identical`, sweeping all 512 LUT entries for both the composite-2C02 and RGB-2C05 LUTs). Honest measured result: the conversion is a **memory-bound LUT gather** (~19 GiB/s, the box's single-thread DRAM ceiling), so no SIMD path clears the >3% adoption bar — the dispatcher **keeps scalar-`u32` as the default** and the SIMD variants are validated + available but not forced. (The NTSC ladders are GPU WGSL shaders, so there's no hot CPU display transform there to vectorize.) +- **A4 — wasm size/startup**: `wasm-opt -O4` (SIMD + bulk-memory preserved) shrinks the wasm-bindgen output 12.7 → 11.1 MiB raw; streaming instantiation documented. Real `trunk build --release`: **3.99 MiB gzip total — 1.01 MiB headroom under the 5 MiB budget.** + +## Notes + +- Version: workspace `2.1.7 → 2.1.8` (Cargo.lock refreshed). No dependency or mobile changes (the mobile apps re-release at v2.3.0). +- **No default-build behavior change.** The fast dot path is default-off (and differential-tested bit-identical when on); the SIMD blitter is byte-identical to scalar and stays non-default. AccuracyCoin stays **141/141 (100.00%)**, nestest 0-diff, `visual_regression` byte-identical, save-state and the 60-ROM commercial oracle unchanged. +- Docs updated in-change: `docs/performance.md` (the profiled fast-path design + measured before/after + the A2/A4 tables), `docs/ppu-2c02.md`, `docs/frontend.md`. New `fast_dotloop_diff` + `gfx_blit` tests. +- Full host gate green: fmt · clippy (native all-targets + feature combos + all wasm32 targets incl. `+simd128`) · rustdoc (`-D warnings`) · `no_std` cross-compile (`thumbv7em`) · markdownlint (pinned v0.39.0) · the CPU/PPU + `--features test-roms` suites (incl. `fast_dotloop_diff` 3/3, `gfx_blit` 3/3) · a real `trunk build --release` under the gzip budget · version consistency. diff --git a/CHANGELOG.md b/CHANGELOG.md index d2c01329..60dd5c3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ cycle-accurate core later replaced. ## [Unreleased] +## [2.1.8] - 2026-07-12 - "Fathom" (performance — default-off specialized fast PPU dot-loop (differential-tested byte-identical, +12% rendering-heavy) + SIMD software blitter + wasm size/startup pass; "Tempo") + ### Performance - **Specialized visible-scanline fast dot path (v2.1.8 "Performance" A1; diff --git a/Cargo.lock b/Cargo.lock index 279b91f2..051054b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4248,7 +4248,7 @@ dependencies = [ [[package]] name = "rustynes-android" -version = "2.1.7" +version = "2.1.8" dependencies = [ "android-activity", "android_logger", @@ -4266,7 +4266,7 @@ dependencies = [ [[package]] name = "rustynes-apu" -version = "2.1.7" +version = "2.1.8" dependencies = [ "bitflags 2.13.0", "criterion", @@ -4279,7 +4279,7 @@ dependencies = [ [[package]] name = "rustynes-cheevos" -version = "2.1.7" +version = "2.1.8" dependencies = [ "cc", "ureq", @@ -4287,7 +4287,7 @@ dependencies = [ [[package]] name = "rustynes-core" -version = "2.1.7" +version = "2.1.8" dependencies = [ "bitflags 2.13.0", "criterion", @@ -4304,7 +4304,7 @@ dependencies = [ [[package]] name = "rustynes-cpu" -version = "2.1.7" +version = "2.1.8" dependencies = [ "bitflags 2.13.0", "criterion", @@ -4315,7 +4315,7 @@ dependencies = [ [[package]] name = "rustynes-frontend" -version = "2.1.7" +version = "2.1.8" dependencies = [ "anstyle", "arboard", @@ -4369,11 +4369,11 @@ dependencies = [ [[package]] name = "rustynes-gfx-shaders" -version = "2.1.7" +version = "2.1.8" [[package]] name = "rustynes-hdpack" -version = "2.1.7" +version = "2.1.8" dependencies = [ "lewton", "png", @@ -4384,7 +4384,7 @@ dependencies = [ [[package]] name = "rustynes-ios" -version = "2.1.7" +version = "2.1.8" dependencies = [ "bytemuck", "cpal", @@ -4398,7 +4398,7 @@ dependencies = [ [[package]] name = "rustynes-libretro" -version = "2.1.7" +version = "2.1.8" dependencies = [ "libc", "rust-libretro", @@ -4407,7 +4407,7 @@ dependencies = [ [[package]] name = "rustynes-mappers" -version = "2.1.7" +version = "2.1.8" dependencies = [ "bitflags 2.13.0", "criterion", @@ -4419,7 +4419,7 @@ dependencies = [ [[package]] name = "rustynes-mobile" -version = "2.1.7" +version = "2.1.8" dependencies = [ "rustynes-core", "rustynes-hdpack", @@ -4434,14 +4434,14 @@ dependencies = [ [[package]] name = "rustynes-monetization" -version = "2.1.7" +version = "2.1.8" dependencies = [ "uniffi", ] [[package]] name = "rustynes-netplay" -version = "2.1.7" +version = "2.1.8" dependencies = [ "futures-util", "js-sys", @@ -4457,7 +4457,7 @@ dependencies = [ [[package]] name = "rustynes-ppu" -version = "2.1.7" +version = "2.1.8" dependencies = [ "bitflags 2.13.0", "criterion", @@ -4469,14 +4469,14 @@ dependencies = [ [[package]] name = "rustynes-ra" -version = "2.1.7" +version = "2.1.8" dependencies = [ "rustynes-cheevos", ] [[package]] name = "rustynes-script" -version = "2.1.7" +version = "2.1.8" dependencies = [ "mlua", "piccolo", @@ -4487,7 +4487,7 @@ dependencies = [ [[package]] name = "rustynes-test-harness" -version = "2.1.7" +version = "2.1.8" dependencies = [ "insta", "png", diff --git a/Cargo.toml b/Cargo.toml index 29bd2326..cb377f46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ members = [ ] [workspace.package] -version = "2.1.7" +version = "2.1.8" edition = "2024" rust-version = "1.96" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index fc3aba43..6dfbe55d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- Build Status License: MIT OR Apache-2.0 Version Rust: 1.96
+ Build Status License: MIT OR Apache-2.0 Version Rust: 1.96
AccuracyCoin nestest Try in browser
Platform

@@ -775,16 +775,17 @@ and the Material-for-MkDocs documentation handbook at ## Current Release -RustyNES's current release is **v2.1.7 "Fathom" ("Stepping")**, the hardware-revisions -step of the v2.1.5 → v2.2.0 "deepen the project" run — it adds an opt-in, default-off -**selectable PPU die-revision** model (2C02 rev-E+ OAMADDR/`$2003` corruption à la *Huge -Insect*, power-up palette variants) and a **power-on RAM model** (all-zero default, opt-in -seeded/patterned — for games that read uninitialized RAM like *Final Fantasy*), plus an -opt-in **2A03 die-revision** (`Rp2A03G`/`Rp2A03H`) config that gates the "unexpected read" -DMA behavior. The last is the honest **DMA frontier** work: direct instrumentation proved -the die-revision DMA gate is a **no-op on every committed oracle ROM** (and no reference -emulator branches DMA on die stepping), so the residual is documented in **ADR 0033** -rather than faked. Every toggle defaults off, so the shipped build stays **byte-identical**: +RustyNES's current release is **v2.1.8 "Fathom" ("Tempo")**, the performance step of the +v2.1.5 → v2.2.0 "deepen the project" run — it adds a **specialized visible-scanline fast +PPU dot path** (a straight-line renderer for clean dots that drops to the exact 2-cycle-ALE +loop the instant any sub-scanline disturbance occurs; **~12% faster** on rendering-heavy +frames), a set of **SIMD-validated software palette→RGBA blitter paths** (scalar / `wide` / +wasm `+simd128`, all proven byte-identical — but the conversion is a **memory-bound LUT +gather**, so no SIMD path clears the >3% bar and **scalar stays the default**; the vector +paths are validated and available, not a shipped speedup), and a **wasm size/startup pass** +(`wasm-opt -O4` → 3.99 MiB gzip, comfortably under budget). The fast dot path is +**default-off** and **differential-tested bit-identical** to the exact path across the +corpus (framebuffer + audio + cycles + snapshot) — so the shipped build stays byte-identical: **AccuracyCoin stays 141/141 (100.00%)**, nestest 0-diff, `visual_regression` / `pal_apu_tests` unchanged. @@ -912,7 +913,7 @@ If you use RustyNES in academic research, please cite: author = {RustyNES Contributors}, title = {RustyNES: A Cycle-Accurate NES Emulator in Rust}, year = {2026}, - version = {2.1.7}, + version = {2.1.8}, url = {https://github.com/doublegate/RustyNES}, note = {Cycle-accurate NES emulator on a master-clock-precise scheduler; AccuracyCoin 100\% (141/141), nestest 0-diff; 172 mapper families,