From c0f71ab2bd77b8dc02b5708701abe11a28a60ca8 Mon Sep 17 00:00:00 2001
From: DoubleGate
Date: Sun, 12 Jul 2026 02:08:04 -0400
Subject: [PATCH 1/2] =?UTF-8?q?release:=20cut=20v2.1.8=20"Fathom"=20(perfo?=
=?UTF-8?q?rmance=20=E2=80=94=20"Tempo")?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Cuts the v2.1.8 release — the performance step of the v2.1.5 → v2.2.0 "deepen the
existing project" run. Bumps the workspace version 2.1.7 → 2.1.8, renames the
CHANGELOG `[Unreleased]` section to `[2.1.8]`, refreshes Cargo.lock, syncs the
README badge / current-release paragraph / BibTeX, and authors
`.github/release-notes/v2.1.8.md`.
v2.1.8 is profile-first performance work, every change byte-identical (or
default-off behind a runtime knob), on the byte-identical default core
(AccuracyCoin 141/141, nestest 0-diff, visual_regression / pal_apu_tests
unchanged):
- #284 — specialized visible-scanline fast PPU dot path (profiled: Ppu::tick was
46.5% of frame self-time), ~12% faster on rendering-heavy frames, shipped
default-OFF (Nes::set_fast_dotloop) and differential-tested bit-identical to
the exact path across the corpus (incl. the v2.1.7 Rp2c02G corruption path).
- #286 — vectorized software palette->RGBA blitter (scalar/wide/wasm-simd128,
SIMD==scalar byte-identical; memory-bound so scalar stays default per the >3%
bar) + a wasm size pass (wasm-opt -O4 -> 3.99 MiB gzip, under budget).
No default-build behavior change.
Co-Authored-By: Claude Opus 4.8
---
.github/release-notes/v2.1.8.md | 26 ++++++++++++++++++++++++
CHANGELOG.md | 2 ++
Cargo.lock | 36 ++++++++++++++++-----------------
Cargo.toml | 2 +-
README.md | 26 ++++++++++++------------
5 files changed, 60 insertions(+), 32 deletions(-)
create mode 100644 .github/release-notes/v2.1.8.md
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..0001148b 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-

+


@@ -775,18 +775,18 @@ 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 **vectorized software palette→RGBA blitter** (scalar / `wide` / wasm `+simd128`
+paths), 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), and the SIMD
+blitter is proven byte-identical to scalar — so the shipped build stays byte-identical:
**AccuracyCoin stays 141/141 (100.00%)**, nestest 0-diff, `visual_regression` /
-`pal_apu_tests` unchanged.
+`pal_apu_tests` unchanged. (The SIMD blitter is memory-bound, so scalar stays the default
+per the >3% adoption bar — the paths are validated and available.)
The v2.1.x line opened with **v2.1.0**, the accuracy-remediation release (a display-only
PPU palette-backdrop-override fix, 86 mapper families promoted BestEffort → Curated, and
@@ -912,7 +912,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,
From 89f2d667c5c395e2d6f719749826929d70486761 Mon Sep 17 00:00:00 2001
From: DoubleGate
Date: Sun, 12 Jul 2026 02:12:13 -0400
Subject: [PATCH 2/2] docs(readme): clarify SIMD blitter is
validated-not-a-speedup (memory-bound)
Adopts Copilot's nit on the v2.1.8 release PR: 'vectorized blitter' could imply
a SIMD speedup, but scalar stays the default because palette->RGBA is a
memory-bound LUT gather (no path clears the >3% bar). Reworded to 'SIMD-validated
blitter paths ... not a shipped speedup' so the byte-identical-but-non-default
reality is clear upfront rather than only in the trailing parenthetical.
Co-Authored-By: Claude Opus 4.8
---
README.md | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 0001148b..6dfbe55d 100644
--- a/README.md
+++ b/README.md
@@ -779,14 +779,15 @@ RustyNES's current release is **v2.1.8 "Fathom" ("Tempo")**, the performance ste
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 **vectorized software palette→RGBA blitter** (scalar / `wide` / wasm `+simd128`
-paths), 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), and the SIMD
-blitter is proven byte-identical to scalar — so the shipped build stays byte-identical:
+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. (The SIMD blitter is memory-bound, so scalar stays the default
-per the >3% adoption bar — the paths are validated and available.)
+`pal_apu_tests` unchanged.
The v2.1.x line opened with **v2.1.0**, the accuracy-remediation release (a display-only
PPU palette-backdrop-override fix, 86 mapper families promoted BestEffort → Curated, and