diff --git a/.gitignore b/.gitignore
index bfff5861..68458ce9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -237,3 +237,35 @@ 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.
+#
+# 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 `-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. `**/` 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
+# tracked files of any of these extensions, so recursive is safe.
+**/*.slangp
+**/*.cgp
+**/*.sym
+/presets/
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 @@
-

+


@@ -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/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
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.