From b99d7e4d8db359834d73cab9a7a552624a67b8e9 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Thu, 30 Jul 2026 18:18:41 -0400 Subject: [PATCH 1/4] chore(release): cut v1.25.0 "Workbench" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version ceremony for the frontend-parity release: the ten increments T-FP-A through T-FP-G2 (PRs #260, #271, #262-#269) are on `main` and green, so this promotes them from `[Unreleased]` to a named, tagged release. ## Version bump `1.24.0` -> `1.25.0` across the workspace manifest and all 16 member crates (`rustysnes-libretro` inherits via `version.workspace = true`). `Cargo.lock` regenerated; `cargo metadata` and a full `cargo check --workspace` confirm the graph still resolves. MINOR, not MAJOR or PATCH, and the classification is load-bearing rather than habitual: every item is additive and default-off or default-inert (`video.stack` defaults to `Off`, the preset chain is empty, the debugger needs `debug-hooks`, GPU timing needs `gpu-timing`, a unity per-voice gain is a bit-exact bypass in the DSP), so an existing `config.toml` renders and sounds byte-identically. Save-state `FORMAT_VERSION` stays at 10 — no format break, which is what would have forced a MAJOR under `docs/adr/0006`. ## Documents updated - `CHANGELOG.md` — `[Unreleased]` promoted to `## [1.25.0] "Workbench" - 2026-07-30` with a release summary: the five headline capabilities, the additive/default-off posture, and a named section on the recurring dead-config defect class this release exists to remove. A fresh empty `[Unreleased]` is left in place for the next cycle. - `docs/STATUS.md` — the single source of truth. `Current release` moved to `v1.25.0 "Workbench"` with `v1.24.0` demoted into the precedent list, plus a paragraph stating explicitly that this release changes no emulation behaviour, that the accuracy dashboard / per-suite pass counts / coprocessor tier matrix below it are unchanged, and naming the two engine-side fixes that rode along (`Cpu::interrupts_taken`; the `delta::Chain` front-eviction defect, which affected the rewind buffer as much as the new greenzone). - `README.md` — version badge to v1.25.0, and the `Current Release` section rewritten. That section was **stale at v1.8.0** — seventeen releases behind — so this is a drift repair, not just a bump. The Roadmap list gained the three rungs that had accumulated undocumented (`v1.9.0`-`v1.20.0`, `v1.21.0`-`v1.24.0`, `v1.25.0`). - `to-dos/VERSION-PLAN.md` — a full `v1.25.0 "Workbench"` rung in the house style: per-ticket detail for A/B/C1/C2/D/E/F/G1/G2, the dead-config rule that came out of it ("grep a setting's readers, not its definition"), the two engine-side fixes, and the verification matrix. - `.gitignore` — ignores for the artifact types this release can now produce: perf CSV session logs, `.y4m`/`.wav` captures, and dropped-in shader presets and symbol maps. Deliberately **anchored and specifically named** (`/*.wav`, `/rustysnes-*.png`) rather than bare `*.wav`/`*.png` globs, which would also swallow committed test fixtures and `screenshots/`. Verified `screenshots/montage.png` is still tracked. `to-dos/ROADMAP.md` already carried the `T-FP-*` rows as `landed v1.25.0`; no change needed. ## Verification - `cargo test --workspace`: 804 passed, 0 failed, 1 ignored. - `cargo clippy --workspace --all-targets -- -D warnings`: clean. - Per-feature frontend clippy across all eight sets (default, full, emu-thread, debug-hooks, cheats, scripting, hd-pack, gpu-timing). The `full` set excludes `emu-thread`, so default+full alone is not sufficient — this gap has shipped a real bug before. - `no_std` core builds for `thumbv7em-none-eabihf`. - `cargo fmt --check` and `RUSTDOCFLAGS=-D warnings cargo doc --workspace` clean. - `markdownlint` clean on every edited region. The residual MD060 findings are a newer-linter rule the project's own docs note is ungated in CI, and the two MD024 hits are pre-existing at lines 1698/3489 of the CHANGELOG's history. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 17 ++++++++ CHANGELOG.md | 44 +++++++++++++++++++ Cargo.lock | 36 ++++++++-------- Cargo.toml | 2 +- README.md | 23 ++++++++-- crates/rustysnes-android/Cargo.toml | 2 +- crates/rustysnes-apu/Cargo.toml | 2 +- crates/rustysnes-cart/Cargo.toml | 2 +- crates/rustysnes-cheevos/Cargo.toml | 2 +- crates/rustysnes-core/Cargo.toml | 2 +- crates/rustysnes-cpu/Cargo.toml | 2 +- crates/rustysnes-frontend/Cargo.toml | 2 +- crates/rustysnes-gfx-shaders/Cargo.toml | 2 +- crates/rustysnes-ios/Cargo.toml | 2 +- crates/rustysnes-mobile/Cargo.toml | 2 +- crates/rustysnes-monetization/Cargo.toml | 2 +- crates/rustysnes-netplay/Cargo.toml | 2 +- crates/rustysnes-ppu/Cargo.toml | 2 +- crates/rustysnes-savestate/Cargo.toml | 2 +- crates/rustysnes-script/Cargo.toml | 2 +- crates/rustysnes-test-harness/Cargo.toml | 2 +- docs/STATUS.md | 20 ++++++++- to-dos/VERSION-PLAN.md | 55 ++++++++++++++++++++++++ 23 files changed, 189 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index bfff5861..3c9f0b50 100644 --- a/.gitignore +++ b/.gitignore @@ -237,3 +237,20 @@ WARP.md # Antigravity PR reviewer: large-diff on-disk handoff (transient, removed by the script). /.agy-review-work/ + +# --- Frontend capture / telemetry output (`v1.25.0`, T-FP-B / T-FP-G1) --- +# These land in the user's data directory by default, but the emulator can be pointed anywhere and +# is routinely run straight out of a checkout — so a stray capture must not become a commit +# candidate. Anchored to the repo root and named specifically rather than a bare `*.wav`/`*.png`, +# which would also swallow committed test fixtures and the README's screenshots. +/rustysnes-perf-*.csv +/*.y4m +/*.wav +/rustysnes-*.png +/captures/ + +# Shader presets and symbol maps a user drops in to try (inputs, never project files). +/presets/ +/*.slangp +/*.cgp +/*.sym diff --git a/CHANGELOG.md b/CHANGELOG.md index 64b97516..365cf633 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.25.0] "Workbench" - 2026-07-30 + +**The frontend catches up with the engine.** RustySNES's emulation core has been at the Mesen2/ares +bar for several releases while its egui shell lagged its own NES sibling; this release closes that +gap in ten reviewed increments (`T-FP-A` … `T-FP-G2`), taking the frontend from "boots games" to a +tool you can debug, profile, record, retime, and author a TAS with. + +Everything here is **additive and default-off or default-inert**: `video.stack` defaults to `Off`, +the preset chain is empty, the debugger needs `debug-hooks`, GPU timing needs `gpu-timing`, and the +per-voice mixer's unity gain is a bit-exact bypass in the DSP. An existing `config.toml` renders and +sounds byte-identically, and no save-state format changed (`FORMAT_VERSION` stays at 10). + +The five headline capabilities: + +- **A real debugger** — conditional breakpoints over an expression evaluator, an instruction trace, + a control-flow event log, a WRAM access heat map, symbol maps, a memory editor with freezes, an + OAM viewer, a cart memory map, and an inline 65C816 assembler that assembles by *searching the + real disassembler* rather than carrying a second opcode table. +- **A multi-pass shader stack** with `RetroArch`-shaped per-pass semantics, richer CRT and an NTSC + composite pass, `.slangp`/`.cgp` preset parsing, and a best-effort GLSL→WGSL bridge through naga — + every untranslatable pass named rather than silently black. Verified by **offscreen GPU goldens**, + which is new infrastructure: the previous shader tests only checked that WGSL parsed. +- **Observability and honest pacing** — p50/p95/p99/max distributions over emulation, present, GPU + and audio-buffer time, an opt-in CSV session log, and `PacingMode` turned from a setting nothing + read into one resolved against the monitor's real refresh and the surface's real present-mode caps, + with any declined request **reported** rather than silently downgraded. +- **An 8-voice audio mixer** (gain, mute, solo, VU meters) plus **compressed rewind** — XOR-delta and + RLE with periodic keyframes, no new dependency, bit-identical round-trip. +- **TAStudio** — a piano roll, a greenzone that reuses the rewind compression, and invalidation as + the *default* consequence of an edit. + +Plus the infrastructure the above plugs into: compile-time-checked localisation in five languages +wired through the whole menu bar and every Settings tab, a populated Help menu, and `Settings` split +into per-tab renderers. + +### A recurring defect this release was written to remove + +Five settings shipped in earlier versions that **round-tripped through `config.toml` while nothing +read them** — `gamepad`, `video.integer_scale`, `config.p2`, `audio.device`, and `PacingMode`. Each +looked functional and did nothing. All five are now live, and the pattern was explicitly hunted: +grep a setting's *readers*, not its definition. Two more instances were caught during review of this +very release — a shader preset path that persisted while nothing loaded it at startup, and an i18n +catalogue with sixty translated strings and eight consumers. + ### Fixed - **Soft-patching: a hostile BPS patch could panic instead of erroring.** Two arithmetic paths took diff --git a/Cargo.lock b/Cargo.lock index 16ed41b4..eee60b93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,7 +29,7 @@ dependencies = [ [[package]] name = "accuracysnes-gen" -version = "1.24.0" +version = "1.25.0" [[package]] name = "adler2" @@ -3825,7 +3825,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustysnes-android" -version = "1.24.0" +version = "1.25.0" dependencies = [ "android_logger", "bytemuck", @@ -3841,7 +3841,7 @@ dependencies = [ [[package]] name = "rustysnes-apu" -version = "1.24.0" +version = "1.25.0" dependencies = [ "bitflags 2.13.0", "rustysnes-savestate", @@ -3849,7 +3849,7 @@ dependencies = [ [[package]] name = "rustysnes-cart" -version = "1.24.0" +version = "1.25.0" dependencies = [ "bitflags 2.13.0", "rustysnes-savestate", @@ -3858,7 +3858,7 @@ dependencies = [ [[package]] name = "rustysnes-cheevos" -version = "1.24.0" +version = "1.25.0" dependencies = [ "cc", "ureq", @@ -3866,7 +3866,7 @@ dependencies = [ [[package]] name = "rustysnes-core" -version = "1.24.0" +version = "1.25.0" dependencies = [ "criterion", "rustysnes-apu", @@ -3881,7 +3881,7 @@ dependencies = [ [[package]] name = "rustysnes-cpu" -version = "1.24.0" +version = "1.25.0" dependencies = [ "bitflags 2.13.0", "rustysnes-savestate", @@ -3889,7 +3889,7 @@ dependencies = [ [[package]] name = "rustysnes-frontend" -version = "1.24.0" +version = "1.25.0" dependencies = [ "anstyle", "arboard", @@ -3931,11 +3931,11 @@ dependencies = [ [[package]] name = "rustysnes-gfx-shaders" -version = "1.24.0" +version = "1.25.0" [[package]] name = "rustysnes-ios" -version = "1.24.0" +version = "1.25.0" dependencies = [ "bytemuck", "log", @@ -3948,7 +3948,7 @@ dependencies = [ [[package]] name = "rustysnes-libretro" -version = "1.24.0" +version = "1.25.0" dependencies = [ "libc", "rust-libretro", @@ -3957,7 +3957,7 @@ dependencies = [ [[package]] name = "rustysnes-mobile" -version = "1.24.0" +version = "1.25.0" dependencies = [ "rustysnes-cart", "rustysnes-core", @@ -3967,14 +3967,14 @@ dependencies = [ [[package]] name = "rustysnes-monetization" -version = "1.24.0" +version = "1.25.0" dependencies = [ "uniffi", ] [[package]] name = "rustysnes-netplay" -version = "1.24.0" +version = "1.25.0" dependencies = [ "js-sys", "rustysnes-core", @@ -3986,7 +3986,7 @@ dependencies = [ [[package]] name = "rustysnes-ppu" -version = "1.24.0" +version = "1.25.0" dependencies = [ "bitflags 2.13.0", "rustysnes-cart", @@ -3996,14 +3996,14 @@ dependencies = [ [[package]] name = "rustysnes-savestate" -version = "1.24.0" +version = "1.25.0" dependencies = [ "thiserror 2.0.18", ] [[package]] name = "rustysnes-script" -version = "1.24.0" +version = "1.25.0" dependencies = [ "mlua", "rustysnes-core", @@ -4012,7 +4012,7 @@ dependencies = [ [[package]] name = "rustysnes-test-harness" -version = "1.24.0" +version = "1.25.0" dependencies = [ "insta", "rustysnes-apu", diff --git a/Cargo.toml b/Cargo.toml index 5ab569df..82133186 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ ] [workspace.package] -version = "1.24.0" +version = "1.25.0" edition = "2024" rust-version = "1.96" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index d998b528..30df6379 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,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
65C816 oracle: 0-diff SPC700 oracle: 0-diff GitHub Pages
Platform

@@ -623,9 +623,14 @@ Material-for-MkDocs documentation handbook at ## Current Release -RustySNES's current release is **v1.8.0 "Tracepoint"**. See [`docs/STATUS.md`](docs/STATUS.md) -and [`CHANGELOG.md`](CHANGELOG.md) for the full release history (`v0.1.0` through `v1.8.0`) and -per-release detail. +RustySNES's current release is **v1.25.0 "Workbench"** — the frontend release that brings the +egui shell up to the engine's level: a full debugger, a multi-pass shader stack with `.slangp` +support, performance distributions and hardware-resolved frame pacing, an 8-voice audio mixer with +compressed rewind, A/V capture, a virtual pad, and TAStudio. It is **additive throughout**: an +existing `config.toml` renders and sounds byte-identically, and no save-state format changed. + +See [`docs/STATUS.md`](docs/STATUS.md) and [`CHANGELOG.md`](CHANGELOG.md) for the full release +history (`v0.1.0` through `v1.25.0`) and per-release detail. - **Download:** the [GitHub Releases](https://github.com/doublegate/RustySNES/releases) page — desktop binaries for Linux, macOS (aarch64), and Windows. @@ -656,6 +661,16 @@ emulator, tracked in lockstep rather than a frozen snapshot native 3x default. - **`v1.8.0 "Tracepoint"`** — debugger depth II: a Memory Compare panel and an in-app Docs/glossary panel. +- **`v1.9.0 "Marionette"` - `v1.20.0 "Aperture"`** — scripting reach, HD packs, RetroAchievements, + shaders and accessibility, the Android/iOS mobile ladder, a PGO/BOLT pipeline, and a UI/UX-parity + pass — all with the accuracy dashboard unchanged. +- **`v1.21.0 "Touchstone"` - `v1.24.0 "Ensemble"`** — the accuracy rungs: the per-dot PPU compositor + becomes the sole renderer, the DSP-1 Mode-7 floor is fixed and then the NEC-DSP host interface + made pin-exact, and DSP-3 + ST011 complete the NEC coprocessor family. +- **`v1.25.0 "Workbench"`** — the frontend catches up with the engine, in ten reviewed increments + (`T-FP-A`-`T-FP-G2`): debugger, shader stack + `.slangp`/GLSL bridge, observability + real pacing, + audio mixer + compressed rewind, A/V capture + virtual pad + databases, and TAStudio. Five + settings that had round-tripped through `config.toml` while nothing read them are now live. The full roadmap lives in [`to-dos/ROADMAP.md`](to-dos/ROADMAP.md) (the phase spine) and [`to-dos/VERSION-PLAN.md`](to-dos/VERSION-PLAN.md) (the named release ladder, including the diff --git a/crates/rustysnes-android/Cargo.toml b/crates/rustysnes-android/Cargo.toml index a6964c7a..a278d27f 100644 --- a/crates/rustysnes-android/Cargo.toml +++ b/crates/rustysnes-android/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rustysnes-android" description = "RustySNES: Android JNI presentation host (wgpu-on-Surface, no emulation logic)" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-apu/Cargo.toml b/crates/rustysnes-apu/Cargo.toml index 2d066ed3..227c2c28 100644 --- a/crates/rustysnes-apu/Cargo.toml +++ b/crates/rustysnes-apu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustysnes-apu" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-cart/Cargo.toml b/crates/rustysnes-cart/Cargo.toml index c4ff33f6..56ed2088 100644 --- a/crates/rustysnes-cart/Cargo.toml +++ b/crates/rustysnes-cart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustysnes-cart" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-cheevos/Cargo.toml b/crates/rustysnes-cheevos/Cargo.toml index a5d9f8a7..e6a4c53e 100644 --- a/crates/rustysnes-cheevos/Cargo.toml +++ b/crates/rustysnes-cheevos/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rustysnes-cheevos" description = "Native-only RetroAchievements (rcheevos) FFI wrapper for RustySNES" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-core/Cargo.toml b/crates/rustysnes-core/Cargo.toml index 16912f2a..3d8c530b 100644 --- a/crates/rustysnes-core/Cargo.toml +++ b/crates/rustysnes-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustysnes-core" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-cpu/Cargo.toml b/crates/rustysnes-cpu/Cargo.toml index 8e3b64eb..64ec39a5 100644 --- a/crates/rustysnes-cpu/Cargo.toml +++ b/crates/rustysnes-cpu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustysnes-cpu" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-frontend/Cargo.toml b/crates/rustysnes-frontend/Cargo.toml index e48ba047..e7169392 100644 --- a/crates/rustysnes-frontend/Cargo.toml +++ b/crates/rustysnes-frontend/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rustysnes-frontend" description = "RustySNES: cross-platform SNES emulator binary (winit + wgpu + cpal + egui)" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-gfx-shaders/Cargo.toml b/crates/rustysnes-gfx-shaders/Cargo.toml index b799ddfe..9b79da6e 100644 --- a/crates/rustysnes-gfx-shaders/Cargo.toml +++ b/crates/rustysnes-gfx-shaders/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rustysnes-gfx-shaders" description = "RustySNES: shared WGSL presentation-shader sources (blit + post-filters)" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-ios/Cargo.toml b/crates/rustysnes-ios/Cargo.toml index b848d184..52982820 100644 --- a/crates/rustysnes-ios/Cargo.toml +++ b/crates/rustysnes-ios/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rustysnes-ios" description = "RustySNES: iOS Metal presentation host (wgpu-on-CAMetalLayer, no emulation logic)" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-mobile/Cargo.toml b/crates/rustysnes-mobile/Cargo.toml index cb15d9a8..5f587169 100644 --- a/crates/rustysnes-mobile/Cargo.toml +++ b/crates/rustysnes-mobile/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rustysnes-mobile" description = "RustySNES: UniFFI mobile bridge (Kotlin/Swift bindings over the emulation core)" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-monetization/Cargo.toml b/crates/rustysnes-monetization/Cargo.toml index cc9ea3fa..59bec76c 100644 --- a/crates/rustysnes-monetization/Cargo.toml +++ b/crates/rustysnes-monetization/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rustysnes-monetization" description = "RustySNES: dormant entitlement/ad-pacing policy scaffold (mobile-only, never a dependency of the deterministic core)" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-netplay/Cargo.toml b/crates/rustysnes-netplay/Cargo.toml index f6c92fc2..7416242b 100644 --- a/crates/rustysnes-netplay/Cargo.toml +++ b/crates/rustysnes-netplay/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rustysnes-netplay" description = "RustySNES: GGPO-style rollback netplay (UDP native, WebRTC browser)" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-ppu/Cargo.toml b/crates/rustysnes-ppu/Cargo.toml index bb5a759b..1c6fcad6 100644 --- a/crates/rustysnes-ppu/Cargo.toml +++ b/crates/rustysnes-ppu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustysnes-ppu" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-savestate/Cargo.toml b/crates/rustysnes-savestate/Cargo.toml index be49397c..19972bb4 100644 --- a/crates/rustysnes-savestate/Cargo.toml +++ b/crates/rustysnes-savestate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustysnes-savestate" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-script/Cargo.toml b/crates/rustysnes-script/Cargo.toml index 9f1b4453..d4377459 100644 --- a/crates/rustysnes-script/Cargo.toml +++ b/crates/rustysnes-script/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rustysnes-script" description = "RustySNES: sandboxed Lua 5.4 scripting (memory read/write + a per-frame callback)" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/rustysnes-test-harness/Cargo.toml b/crates/rustysnes-test-harness/Cargo.toml index a1735b74..55819855 100644 --- a/crates/rustysnes-test-harness/Cargo.toml +++ b/crates/rustysnes-test-harness/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustysnes-test-harness" -version = "1.24.0" +version = "1.25.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/docs/STATUS.md b/docs/STATUS.md index a7939a32..5735771a 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -3,7 +3,7 @@ This file is authoritative for per-suite pass counts, the board / coprocessor matrix, and version policy. Everything else defers to it. -**Current release:** `v1.24.0 "Ensemble"` (`v1.23.0 "Cadence"`, `v1.22.0 "Horizon"`, `v1.21.0 "Touchstone"`, `v0.1.0 "Foundation"`, +**Current release:** `v1.25.0 "Workbench"` (`v1.24.0 "Ensemble"`, `v1.23.0 "Cadence"`, `v1.22.0 "Horizon"`, `v1.21.0 "Touchstone"`, `v0.1.0 "Foundation"`, `v0.2.0 "Persistence"`, `v0.3.0 "Continuum"`, `v0.4.0 "Completion"`, `v0.5.0 "Fidelity"`, `v0.6.0 "Shippable"`, `v0.7.0 "Resolution"`, `v0.8.0 "Community"`, `v0.9.0 "Threshold"`, `v1.0.0 "Zenith"`, `v1.0.1 "Aftertouch"`, `v1.1.0 "Latchkey"`, `v1.2.0 "Phosphor"`, @@ -41,6 +41,24 @@ It is a model-consistency + response-latency change, **not** a rendering change chips are byte-identical to the `v1.22.0` baseline — and it structurally retires the "stop at first `RQM=set`" hazard class. The DSP sub-clock phase is serialized, so save-state `FORMAT_VERSION` bumps `9 → 10` (older blobs loud-fail per `docs/adr/0006`). +**`v1.25.0 "Workbench"` is a frontend release — it changes no emulation behaviour at all.** The +engine has been at the accuracy bar for several versions while the egui shell lagged its own NES +sibling; this closes that gap in ten reviewed increments (`T-FP-A`-`T-FP-G2`): a real debugger +(expression-conditional breakpoints, instruction trace, control-flow event log, WRAM access heat +map, symbol maps, memory editor with freezes, OAM viewer, cart map, inline 65C816 assembler), a +multi-pass shader stack with `.slangp` parsing and a best-effort GLSL→WGSL naga bridge (verified by +**offscreen GPU goldens** — new infrastructure; the prior shader tests only checked that WGSL +parsed), p50/p95/p99/max performance distributions plus a CSV session log, hardware-resolved frame +pacing, an 8-voice audio mixer, XOR-delta+RLE rewind compression, A/V capture, a virtual pad, and +TAStudio. Everything is additive and **default-off or default-inert** — `video.stack` defaults to +`Off`, the debugger needs `debug-hooks`, GPU timing needs `gpu-timing`, and a unity per-voice gain is +a bit-exact bypass in the DSP — so an existing `config.toml` renders and sounds byte-identically. +**No save-state format change** (`FORMAT_VERSION` stays at 10), and the accuracy dashboard, +per-suite pass counts, and coprocessor tier matrix below are unchanged. Two engine-side fixes ride +along, both found by frontend work and both invisible to rendering: `Cpu::interrupts_taken` +bookkeeping so a debugger can tell a vectored step from an executed one, and a `delta::Chain` +eviction defect (a keyframe evicted from the front orphaned every delta that referenced it) that +affected the **rewind buffer** as much as the new greenzone. See `CHANGELOG.md` for full per-release detail. `v1.0.0` closes the production-cut gate: `Board: Send` (unblocking `emu-thread` to compile/test/lint clean for the first time, though it stays off-by-default pending full feature parity — see `docs/frontend.md`), the five diff --git a/to-dos/VERSION-PLAN.md b/to-dos/VERSION-PLAN.md index 5885e65b..9fea66f0 100644 --- a/to-dos/VERSION-PLAN.md +++ b/to-dos/VERSION-PLAN.md @@ -1500,3 +1500,58 @@ board renders byte-identically; no save-state format change. `docs/cart.md`, `docs/STATUS.md`. `cargo test --workspace` green; fmt, clippy (default + per-feature), no_std, doc all green. This completes the NEC DSP family: DSP-1/2/3/4 + ST010/011, six chips, one engine. + +### `v1.25.0 "Workbench"` — the frontend catches up with the engine — **RELEASED 2026-07-30** + +Ten reviewed increments (`T-FP-A` … `T-FP-G2`, one PR each) taking the egui shell from "boots games" +to a tool you can debug, profile, record, retime, and author a TAS with. **No emulation behaviour +changes**: additive and default-off or default-inert throughout, an existing `config.toml` renders +and sounds byte-identically, and save-state `FORMAT_VERSION` stays at 10. + +- **`T-FP-A` Infrastructure.** Compile-time-checked localisation (`crate::i18n`) in English, Spanish, + French, German, and Japanese — each locale is an array indexed by a `Msg` enum, so **adding a + string without translating it fails the build**, where a runtime map would surface a blank label + nobody notices. Wired through the entire menu bar and all four Settings tabs. Plus `crate::perf`'s + distribution primitives, a populated Help menu, and `render_settings` split into per-tab renderers. +- **`T-FP-B` Observability + pacing.** p50/p95/p99/max over produce/present/GPU/audio-occupancy, + sparklines, an opt-in CSV session log, an optional `gpu-timing` feature over `wgpu` timestamp + queries (reporting *unavailable* rather than a fabricated number where the adapter lacks it), and + `PacingMode` made real — resolved against the monitor's reported refresh and the surface's actual + present-mode caps, with a request the hardware cannot honour **reported as declined**. +- **`T-FP-C1`/`C2` The debugger.** Core `debug-hooks` instruction trace, control-flow event log, and + WRAM access heat map on `watchpoint.rs`'s zero-cost-when-off contract; then memory editor with + freezes, OAM viewer, cart map, an expression evaluator driving conditional breakpoints, symbol + maps, trace/callstack/event viewers, and an inline 65C816 assembler that **searches the real + disassembler** instead of carrying a second opcode table — correct by construction against the + decoder, and a new opcode becomes assemblable for free. +- **`T-FP-D`/`E` The shader stack.** An ordered multi-pass chain with `RetroArch`-shaped per-pass + keys, generic `#pragma parameter` sliders generated *from the pass*, richer CRT and an RGB-domain + NTSC pass, then `.slangp`/`.cgp` parsing and a best-effort GLSL→WGSL bridge through naga where + every untranslatable pass is **named** rather than silently black. Verified by **offscreen wgpu + goldens** — new infrastructure, and the enabler: the prior shader tests only checked that WGSL + parsed, which a shader that compiles and renders the wrong thing passes. +- **`T-FP-F` Audio + rewind.** Additive per-voice gains and output taps at the existing per-voice-mute + site (unity is a **bit-exact bypass**), an 8-voice mixer panel, XOR-delta + RLE rewind compression + with periodic keyframes and **no new dependency**, and `save_state_into` removing run-ahead's + per-frame allocation. +- **`T-FP-G1`/`G2` Media + TAStudio.** Raw `.y4m` + `.wav` capture with an exact-rational frame rate + and drift **reported, not corrected**; a virtual pad sharing one geometry source with its renderer; + CRC32-keyed game and Game Genie databases; input macros; and TAStudio — piano roll, greenzone + reusing the rewind compression, invalidation as the default consequence of an edit. + +**The defect class this release was written to remove.** Five settings shipped earlier that +round-tripped through `config.toml` while nothing read them (`gamepad`, `video.integer_scale`, +`config.p2`, `audio.device`, `PacingMode`) are now live; two more of the same shape were caught +during review of this release itself. The rule that came out of it: **grep a setting's readers, not +its definition.** + +**Two engine-side fixes ride along**, both found by frontend work, both invisible to rendering: +`Cpu::interrupts_taken` bookkeeping so a debugger can distinguish a vectored step from an executed +one (without it, an NMI arriving after a `JSR` was logged as a second `Call`), and a `delta::Chain` +eviction defect where a keyframe evicted from the front orphaned every delta that referenced it — +which affected the **rewind buffer** as much as the new greenzone. + +**Verification.** `cargo test --workspace` 804 passing / 1 ignored; per-feature frontend clippy +across all eight feature sets (the `--features full` set excludes `emu-thread`, so default+full is +not enough); `rustysnes-apu`/`-core`/`-cpu` clippy; `no_std` core builds for `thumbv7em-none-eabihf`; +fmt and `RUSTDOCFLAGS=-D warnings cargo doc` clean; the AccuracySNES battery unchanged. From 3d9a4483c4046d5fe19a7d55e3cbc3a5a6a023a5 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Thu, 30 Jul 2026 18:25:19 -0400 Subject: [PATCH 2/4] chore(release): widen the capture ignores that are provably safe to widen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review fix for #272. The root-anchored patterns miss a capture written from a subdirectory, which is the common case for a test runner or a `cd`-ed shell. Each pattern's breadth is now set from what is actually COMMITTED rather than from caution: - Zero tracked `.y4m`, `.wav`, `.slangp`, `.cgp`, `.sym`, or `rustysnes-perf-*.csv` files exist, so all six widen to recursive `**/` safely. - Screenshots cannot. The repo tracks **712** PNGs, 30 of them `rustysnes-*.png` (the branding banners), and `assets/RustySNES_Icon_Pack/transparent/icon/rustysnes-icon-transparent-1024.png` matches even a generic `*-NNNN.png`. So the split is by precision, not by timidity: `screenshot::file_stem`'s default-title form (`rustysnes-` + exactly four digits + `.png`) has zero tracked collisions and goes recursive; the ROM-titled form `-NNNN.png` stays root-anchored, because recursive it would ignore that icon. - `**/captures/` added alongside `/captures/` — pointing the emulator at a captures directory makes none of the above matter, which is the outcome the review's "scope strictly to /captures/" suggestion is really after. Verified both directions rather than asserted: `git ls-files | git check-ignore --stdin -v` reports NOTHING, i.e. no tracked file became ignored; and `captures/x.y4m`, `sub/dir/out.wav`, `rustysnes-perf-3.csv`, `rustysnes-0007.png`, `SUPER_MARIO-0001.png`, `deep/nest/rustysnes-0002.png`, `my.slangp` are all ignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --- .gitignore | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 3c9f0b50..f5f42de3 100644 --- a/.gitignore +++ b/.gitignore @@ -241,16 +241,32 @@ WARP.md # --- Frontend capture / telemetry output (`v1.25.0`, T-FP-B / T-FP-G1) --- # These land in the user's data directory by default, but the emulator can be pointed anywhere and # is routinely run straight out of a checkout — so a stray capture must not become a commit -# candidate. Anchored to the repo root and named specifically rather than a bare `*.wav`/`*.png`, -# which would also swallow committed test fixtures and the README's screenshots. -/rustysnes-perf-*.csv -/*.y4m -/*.wav -/rustysnes-*.png +# candidate. +# +# Each pattern's breadth was checked against what is actually COMMITTED rather than assumed. The +# extensions below have zero tracked files, so they are safe to match recursively: +**/*.y4m +**/*.wav +**/rustysnes-perf-*.csv + +# Screenshots are the exception, and the reason the rest of this block is not simply `*.png`. +# `screenshot::file_stem` writes `<sanitized-rom-title>-NNNN.png`, defaulting to `rustysnes-NNNN` +# — but the repo tracks **712** PNGs, 30 of them named `rustysnes-*.png` (the branding banners), +# and `assets/RustySNES_Icon_Pack/transparent/icon/rustysnes-icon-transparent-1024.png` even +# matches a generic `*-NNNN.png`. So: +# - the default-title form is exact enough to match recursively (zero tracked collisions); +# - the ROM-titled form stays ROOT-ANCHORED, since a recursive version would ignore real assets. +**/rustysnes-[0-9][0-9][0-9][0-9].png +/*-[0-9][0-9][0-9][0-9].png + +# The recommended destination for captures — point the emulator here and nothing above has to be +# relied on at all. /captures/ +**/captures/ -# Shader presets and symbol maps a user drops in to try (inputs, never project files). +# Shader presets and symbol maps a user drops in to try (inputs, never project files). Zero +# tracked files of any of these extensions, so recursive is safe. +**/*.slangp +**/*.cgp +**/*.sym /presets/ -/*.slangp -/*.cgp -/*.sym From d1500fc41fe38c7f61152e63250e76e9b4b01351 Mon Sep 17 00:00:00 2001 From: DoubleGate <parobek@gmail.com> Date: Thu, 30 Jul 2026 18:31:26 -0400 Subject: [PATCH 3/4] chore(release): drop the redundant /captures/ ignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review fix for #272. `**/captures/` already matches the repo root — `**/` expands to zero or more directories — so the separate `/captures/` line ignored nothing the pattern above it did not. Verified in a scratch repo containing only `**/captures/`: both `captures/a.txt` and `sub/captures/b.txt` are ignored. Re-checked here after the removal, plus `git ls-files | git check-ignore --stdin` still reports no tracked file ignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f5f42de3..68458ce9 100644 --- a/.gitignore +++ b/.gitignore @@ -260,8 +260,7 @@ WARP.md /*-[0-9][0-9][0-9][0-9].png # The recommended destination for captures — point the emulator here and nothing above has to be -# relied on at all. -/captures/ +# relied on at all. `**/` already covers the repo root, so no separate `/captures/` is needed. **/captures/ # Shader presets and symbol maps a user drops in to try (inputs, never project files). Zero From d5c88868b9b4e0c5cda91feebb38027e4fca5413 Mon Sep 17 00:00:00 2001 From: DoubleGate <parobek@gmail.com> Date: Thu, 30 Jul 2026 18:39:57 -0400 Subject: [PATCH 4/4] =?UTF-8?q?fix(reviewer):=20repair=20the=20delete=20st?= =?UTF-8?q?ep=20I=20broke=20in=20#270=20=E2=80=94=20gh=20api=20has=20no=20?= =?UTF-8?q?--arg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression I introduced in #270 and did not verify. `--arg`/`--argjson` are **jq** flags; `gh api` has no such options and rejects them. So the delete pipeline errored on every run, `2>/dev/null` swallowed the message, and with `set -o pipefail` the non-zero status propagated and `set -e` killed the script — AFTER the review comment had already been posted. Two visible consequences, both confirmed: - The `review` job has gone red on every run since #270 merged. Every run before it succeeded; every run after posted its comment and then exited 1. - Stale review comments stopped being deleted. PR #272 accumulated **three** bot comments where the design is one, which is the observable proof the delete step had been dead rather than merely noisy. The fix fetches the JSON raw and pipes it into a real `jq`, which does take `--arg`/`--argjson`. (`--paginate` without `--jq` emits one array per page; jq reads that stream fine, applying `.[]` per input.) The ids are captured first and the loop reads from the captured value, so a listing failure degrades to "leave them in place" with a logged warning instead of taking the script down. stderr is no longer discarded on that call. Swallowing it is what made a guaranteed-failing command look like a mysterious exit code, and cost the diagnosis more than the noise would have. The script now ends with an explicit `exit 0`, so a stray non-zero from the delete loop can never again be reported as "the review failed" once the comment is published. Verified: `bash -n` clean; `shellcheck -S warning` shows only the pre-existing SC1007 on line 138; and the exact pipeline shape, run under `set -euo pipefail` against #272's real comment list, exits 0 and selects precisely the two stale comments while correctly excluding the newest. This lands on the release branch because it is my regression currently making a check red on this very PR; holding the release for a separate round-trip would add delay without adding safety. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --- scripts/agy-review.sh | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/scripts/agy-review.sh b/scripts/agy-review.sh index 17ec42ac..4a300629 100755 --- a/scripts/agy-review.sh +++ b/scripts/agy-review.sh @@ -574,13 +574,30 @@ else # `--arg`/`--argjson` rather than shell interpolation into the filter: the marker is an HTML # comment today, but a quote or a backslash in it would otherwise break the jq program itself # rather than simply not matching. - gh api "repos/${REPO}/issues/${PR}/comments" --paginate \ - --arg marker "$MARKER" --argjson new_id "$new_comment_id" \ - --jq '.[] | select(.user.type == "Bot" and .user.login == "github-actions[bot]") | select(.body | contains($marker)) | select(.id != $new_id) | .id' 2>/dev/null \ - | while read -r cid; do - [ -n "$cid" ] || continue - if ! gh api -X DELETE "repos/${REPO}/issues/comments/${cid}" >/dev/null 2>&1; then - log "warning: could not delete prior review comment ${cid}; a duplicate may result" - fi - done + # + # Those are JQ flags, so the JSON is fetched raw and piped into a real `jq` — `gh api` has no + # `--arg`/`--argjson` of its own and rejects them. Handing them to `gh api --jq` made it exit + # non-zero on every run; with the old `2>/dev/null` swallowing the message and `set -o pipefail` + # in force, the script then died *after* posting, so the stale comments were never deleted and + # the job went red for a reason nothing printed. stderr is kept this time for exactly that + # reason. (`--paginate` without `--jq` emits one JSON array per page; `jq` reads that stream + # fine, applying `.[]` to each.) + stale_ids="$( + gh api "repos/${REPO}/issues/${PR}/comments" --paginate \ + | jq -r --arg marker "$MARKER" --argjson new_id "$new_comment_id" \ + '.[] | select(.user.type == "Bot" and .user.login == "github-actions[bot]") | select(.body | contains($marker)) | select(.id != $new_id) | .id' + )" || { + log "warning: could not list prior review comments; leaving them in place" + stale_ids="" + } + while read -r cid; do + [ -n "$cid" ] || continue + if ! gh api -X DELETE "repos/${REPO}/issues/comments/${cid}" >/dev/null 2>&1; then + log "warning: could not delete prior review comment ${cid}; a duplicate may result" + fi + done <<< "$stale_ids" fi + +# The delete loop above is the last real work; end on a defined status so a stray non-zero from +# it can never be mistaken for "the review failed" once the comment is already published. +exit 0