Skip to content

feat(gfx): v2.2.8 "Aperture II" — gamma-correct + sharper scanlines - #345

Merged
doublegate merged 3 commits into
mainfrom
feat/v2.2.8-aperture-ii-video
Aug 4, 2026
Merged

feat(gfx): v2.2.8 "Aperture II" — gamma-correct + sharper scanlines#345
doublegate merged 3 commits into
mainfrom
feat/v2.2.8-aperture-ii-video

Conversation

@doublegate

@doublegate doublegate commented Aug 4, 2026

Copy link
Copy Markdown
Owner

v2.2.8 "Aperture II" — gamma-correct + sharper scanlines

Third of the v2.2.6 → v2.3.0 NESdev-remediation line. Addresses the forum
feedback on gamma-aware resampling and bilinear-soft scanlines.

Stacked on #344 (v2.2.7). Base is feat/v2.2.7-timbre-ii-audio; will retarget
to main once #344 merges.

⚠️ VISUAL VERIFICATION PENDING — please confirm before merge. These are
shader/appearance changes. naga validates that the WGSL compiles and the native +
wasm builds are clean, but it cannot confirm the look. Please check on a real
display (sharper scanlines when the scanline filter is on) and in a browser
(WebGL2 gamma). The shipped native default is byte-identical (new behavior is
gated behind aux = 0), so nothing changes unless scanlines are enabled or you're
on WebGL2.

Presentation-only — core untouched

Nothing here touches emulation, so the pre-shader framebuffer + save-states + every
golden vector are byte-identical (AccuracyCoin 141/141, nestest 0-diff). A prior
investigation established the native path is already gamma-correct (the NES texture
is Rgba8UnormSrgb, so the sampler decodes to linear before filtering + the scanline
math), and the base BLEP is an 81.6 dB-SFDR decimator — so the real gaps were the
WebGL2 non-sRGB path and scanline sharpness, which this targets. The advanced
CRT stacks (royale/guest/megatron) were already gamma-correct and are untouched.

Changed

  • Gamma-correct scanlines + mask (aux.y). The darkening now runs in linear
    light
    . aux.y = 0 (native sRGB) leaves it linear — byte-identical output;
    aux.y = 1 (plain UNORM, e.g. WebGL2) sRGB-decodes on read + re-encodes on output,
    fixing a browser-only gamma error.
  • Sharper scanlines (aux.x, default 0.5). Blends the soft parabola → a narrow
    Gaussian beam for crisp vertical boundaries. aux.x = 0 = the exact pre-v2.2.8
    profile; visible only when scanlines are enabled.
  • Base CRT_WGSL uniform 12 → 16 floats (rect+crop+params+aux); wired on both the
    desktop (crt.rs) and Android (gfx.rs) hosts.

Checks

naga validation green · desktop builds · native + wasm32 clippy -D warnings clean ·
cargo fmt + markdownlint clean · rustynes-frontend tests 464/464 · no golden
snapshots move (CRT output isn't golden-tested). Docs: docs/crt-composite.md +
STATUS/README/AGENTS/CHANGELOG.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved CRT rendering with gamma-correct scanlines and aperture-mask darkening.
    • Added a sharper Gaussian scanline profile for clearer, more defined visuals.
    • Applied consistent CRT presentation improvements across desktop, Android, iOS, and WebGL2.
  • Documentation

    • Updated release notes, status information, README references, and CRT rendering documentation for v2.2.8.

Copilot AI lite review requested due to automatic review settings August 4, 2026 16:46
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@doublegate, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e2ac6a09-7b68-4006-93bc-9d4b6f7c8486

📥 Commits

Reviewing files that changed from the base of the PR and between 3badffd and fc74fdf.

📒 Files selected for processing (4)
  • AGENTS.md
  • CHANGELOG.md
  • crates/rustynes-gfx-shaders/src/lib.rs
  • docs/crt-composite.md
📝 Walkthrough

Walkthrough

Version 2.2.8 adds gamma-aware CRT scanline processing and configurable sharpness. Frontend, Android, and iOS hosts upload the new shader parameters. Release metadata and documentation identify v2.2.8 “Aperture II.”

Changes

CRT presentation update

Layer / File(s) Summary
CRT shader controls and processing
crates/rustynes-gfx-shaders/src/lib.rs, docs/crt-composite.md
The CRT shader accepts sharpness and gamma-linearization controls. It performs optional sRGB conversion and configurable scanline processing.
Host uniform wiring
crates/rustynes-frontend/src/crt.rs, crates/rustynes-android/src/gfx.rs, crates/rustynes-ios/src/gfx_metal.rs
CRT hosts populate auxiliary uniform values from fixed sharpness and surface-format state.
v2.2.8 release metadata
Cargo.toml, README.md, CHANGELOG.md, docs/STATUS.md, AGENTS.md
Version references and release documentation now identify v2.2.8 “Aperture II” and describe the CRT presentation changes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CrtFilter
  participant AndroidGfx
  participant IOSMetal
  participant CRT_WGSL
  CrtFilter->>CRT_WGSL: upload sharpness and linearization values
  AndroidGfx->>CRT_WGSL: upload CRT auxiliary values
  IOSMetal->>CRT_WGSL: upload CRT auxiliary values
  CRT_WGSL->>CRT_WGSL: decode, process, and encode CRT colors
Loading
🚥 Pre-merge checks | ✅ 8 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Changelog Entry For User-Visible Changes ⚠️ Warning The PR changes user-visible scanline and gamma behavior, but CHANGELOG.md leaves [Unreleased] empty and places the entry only under [2.2.8]. Add a concise user-visible entry for the gamma-correct scanlines and sharper CRT behavior directly under CHANGELOG.md's [Unreleased] heading.
✅ Passed checks (8 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the v2.2.8 graphics update for gamma-correct rendering and sharper scanlines.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Docs-As-Spec Sync ✅ Passed The PR diff from the v2.2.7 base changes no files in rustynes-cpu, -ppu, -apu, or -mappers; the docs-as-spec condition is not triggered.
No Unwrap/Expect/Panic On Untrusted Input ✅ Passed The PR adds no new unwrap(), expect(), or panic!() calls; changed code only computes and uploads uniforms. Existing risky calls are inside #[cfg(test)] shader validation.
Safety Comment On New Unsafe Blocks ✅ Passed The two-commit Rust diff adds no unsafe blocks or unsafe fn; structural inspection finds only pre-existing Android and iOS surface blocks.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v2.2.8-aperture-ii-video

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates RustyNES to the v2.2.8 “Aperture II” presentation-fidelity release, focusing on gamma-correct scanline/mask darkening and a sharper scanline profile in the shared CRT WGSL shader while keeping emulation/core outputs byte-identical by default.

Changes:

  • Extend the base CRT_WGSL uniform block (12 → 16 floats) and implement gamma linearization (aux.y) plus scanline sharpness blending (aux.x).
  • Wire the new CRT uniform layout and defaults through the desktop frontend (crt.rs) and Android renderer (gfx.rs).
  • Bump release/version metadata across docs and workspace manifests (README/STATUS/CHANGELOG/AGENTS, Cargo.toml/Cargo.lock).

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
README.md Bumps visible version strings and updates “Current Release” narrative to v2.2.8.
docs/STATUS.md Updates the top-of-file “current release” banner to v2.2.8 with presentation details.
docs/crt-composite.md Documents the new base CRT pass aux semantics (gamma + sharpness).
crates/rustynes-gfx-shaders/src/lib.rs Implements the CRT shader’s new aux behavior and expands the uniform layout to 16 floats.
crates/rustynes-frontend/src/crt.rs Updates uniform packing and defaults (sharpness + linearize flag) for the desktop CRT pass.
crates/rustynes-android/src/gfx.rs Updates uniform semantics/packing for Android and sets CRT aux values for filters 1/2.
CHANGELOG.md Adds the v2.2.8 release entry describing the presentation changes.
Cargo.toml Bumps workspace package version to 2.2.8.
Cargo.lock Propagates the 2.2.8 version bump across workspace crates.
AGENTS.md Updates the “Current release” line to v2.2.8.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/rustynes-gfx-shaders/src/lib.rs
Comment thread CHANGELOG.md
Comment thread AGENTS.md Outdated
Base automatically changed from feat/v2.2.7-timbre-ii-audio to main August 4, 2026 20:55
doublegate and others added 2 commits August 4, 2026 17:01
v2.2.8 "Aperture II" addresses the NESdev-forum feedback on gamma-aware resampling
and bilinear-soft scanlines. It is presentation-only: nothing here touches the
emulation core, so the pre-shader framebuffer, save-states, and every golden vector
are byte-identical (AccuracyCoin 141/141, nestest 0-diff), and the shipped native
default is byte-identical to v2.2.7 (the new behavior is gated behind aux = 0).

The base BLEP audio and the advanced CRT stacks (royale/guest/megatron, already
gamma-correct via their own gamma_in/gamma_out) are untouched. A prior investigation
established that the native path is already gamma-correct (the NES framebuffer texture
is Rgba8UnormSrgb, so the sampler decodes to linear before filtering + the scanline
math, and the surface re-encodes on write) and the base BLEP is an 81.6 dB-SFDR
band-limited decimator -- so the real gaps were the WebGL2 non-sRGB path and scanline
sharpness, which this release targets.

The base CRT/scanline pass (CRT_WGSL) uniform grows from 12 to 16 floats
(rect + crop + params + aux); both hosts already had a 16-float-capable buffer
(Android shares it with the NTSC pass; the desktop CrtFilter is extended here).

Gamma-correct scanlines + aperture mask (aux.y)
- The scanline/mask darkening now happens in linear light. aux.y = 0 (native: sRGB
  texture + surface) leaves the value linear -- byte-identical shipped output.
  aux.y = 1 (a plain UNORM path, e.g. WebGL2, which neither decodes on sample nor
  encodes on write) makes the shader sRGB-decode on read and re-encode before output,
  so a scanline valley is 50% of the LINEAR luminance, not the gamma-encoded value.
  This is a real browser-only gamma fix.

Sharper scanlines (aux.x, default 0.5)
- The scanline profile blends from the original soft parabola (0) to a narrow
  Gaussian beam (1) for crisp vertical row boundaries instead of the linear-sampler
  blur -- the sharper scanlines the feedback asked for. aux.x = 0 reproduces the
  pre-v2.2.8 profile exactly; the effect is only visible when scanlines are enabled.

Hosts
- Desktop (crates/rustynes-frontend/src/crt.rs): CrtFilter's uniform extended to 16
  floats; sharpness (0.5) + linearize (from surface_format.is_srgb()) fields wired in
  new() and the per-frame render().
- Android (crates/rustynes-android/src/gfx.rs): the CRT/scanline filters (1, 2) now
  set aux = (0.5, linearize, 0, 0); linearize derives from the surface format (sRGB
  there, so 0 -- Android output unchanged). NTSC/Bisqwit aux paths untouched.

Verification
- naga validation (crt::tests::shader_parses_and_validates + the CRT stack) green;
  desktop frontend builds; native + wasm32 clippy -D warnings clean; cargo fmt +
  markdownlint clean; rustynes-frontend tests 464/464. The CRT filter output is not
  golden-tested (presentation-only), so no snapshots move.
- VISUAL VERIFICATION PENDING: naga proves the WGSL compiles, not that it looks
  right. The sharper-scanline default and the WebGL2 gamma round-trip must be
  confirmed on a real display + a browser before merge.

Docs: docs/crt-composite.md gains a "gamma + sharpness" section; STATUS/README/AGENTS/
CHANGELOG updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ilot #345)

Address the three Copilot review threads on #345 ("Aperture II"):

- **iOS Metal renderer parity (the substantive one).** `write_uniforms` in
  `crates/rustynes-ios/src/gfx_metal.rs` hard-wrote `aux = 0` for every filter,
  so the scanline (1) and CRT (2) filters on iOS never received the v2.2.8
  `aux.x` scanline-sharpness or `aux.y` gamma-linearize knobs that the shared
  `CRT_WGSL` now consumes — iOS silently kept the old soft-parabola, gamma-
  encoded scanlines while desktop (`crt.rs`) and Android (`gfx.rs`) got the
  corrected path. It now computes `crt_linearize` from the surface's sRGB-ness
  (0 on the native sRGB Metal surface, 1 on a non-sRGB surface) and passes
  `aux = [0.5, crt_linearize, 0, 0]` for filters 1/2, byte-for-byte matching
  the Android host. On the native sRGB surface `crt_linearize == 0`, so the
  Metal CRT/scanline output stays byte-identical to pre-v2.2.8 — this closes a
  cross-platform-consistency gap, not a behavior change on the shipped default.

- **CHANGELOG + AGENTS wording (backwards phrasing).** "the new behavior is
  gated behind `aux = 0`" read the flag inside-out: `aux = 0` is the
  *compatibility* path (the exact pre-v2.2.8 scanline profile), and the new
  linear-light + sharper-scanline path is what activates for a *non-zero*
  `aux` (the WebGL2 non-sRGB path, or when the scanline knob is raised). Both
  the CHANGELOG entry and the AGENTS.md current-release note now say so.

Verification: `rustfmt --edition 2024 --check` clean; `cargo check -p
rustynes-ios` clean on Linux (the uniform-writing path is not Metal-gated, so
it type-checks off-device); `pre-commit run markdownlint` passes on the two
docs. Presentation-only — the emulation core, AccuracyCoin 141/141, and nestest
0-diff are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doublegate
doublegate force-pushed the feat/v2.2.8-aperture-ii-video branch from 6240145 to 3badffd Compare August 4, 2026 21:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AGENTS.md`:
- Line 30: Update the v2.2.4 history entry in AGENTS.md to remove its stale
“current release” wording, replacing it with “the v2.2.4 release” or equivalent.
Preserve the v2.2.8 entry as the sole current-release record and leave the
historical release details unchanged.

In `@CHANGELOG.md`:
- Around line 47-49: Update the v2.2.8 release entry to document the iOS Metal
host wiring alongside the existing desktop and Android hosts, referencing the
shared 16-float CRT uniform; if iOS is intentionally excluded, explicitly state
the reason instead.

In `@crates/rustynes-gfx-shaders/src/lib.rs`:
- Around line 102-105: Replace the power-gamma conversion in the shader’s
linearize path at crates/rustynes-gfx-shaders/src/lib.rs lines 102-105 with the
sRGB EOTF using the 0.04045 breakpoint, and update the inverse conversion at
lines 149-152 to the sRGB OETF using the 0.0031308 breakpoint. Align the
corresponding transfer-function documentation at docs/crt-composite.md lines
55-61 with these piecewise formulas.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 75093a99-2f9a-4c6b-895b-82bd0281106e

📥 Commits

Reviewing files that changed from the base of the PR and between 655c6a1 and 3badffd.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
📒 Files selected for processing (10)
  • AGENTS.md
  • CHANGELOG.md
  • Cargo.toml
  • README.md
  • crates/rustynes-android/src/gfx.rs
  • crates/rustynes-frontend/src/crt.rs
  • crates/rustynes-gfx-shaders/src/lib.rs
  • crates/rustynes-ios/src/gfx_metal.rs
  • docs/STATUS.md
  • docs/crt-composite.md

Comment thread AGENTS.md
Comment thread CHANGELOG.md Outdated
Comment thread crates/rustynes-gfx-shaders/src/lib.rs
…bbit #345)

Address CodeRabbit's re-review of the iOS-aux commit — three findings:

- **Major (functional correctness): use the exact sRGB piecewise transfer, not
  pow(2.2).** The `aux.y = 1` WebGL2 round-trip is meant to reproduce what the
  native path gets for free from a hardware sRGB texture/surface. But that
  hardware applies the IEC 61966-2-1 piecewise curve (a linear segment below
  `0.04045` / `0.0031308`, a 2.4-exponent power segment above), whereas the
  shader linearized with `pow(rgb, 2.2)` / re-encoded with `pow(rgb, 1/2.2)` —
  a plain power gamma that diverges from true sRGB most in the shadows. That
  made the WebGL2 result subtly *disagree* with the native sRGB path it exists
  to match. `CRT_WGSL` now carries `srgb_to_linear` / `linear_to_srgb` helpers
  implementing the exact transfer (component-wise `select` on the breakpoint),
  and the two `pow` sites call them. Presentation-only and native-path-inert:
  the native surface sets `aux.y = 0`, so this branch never runs there and the
  shipped native default stays byte-identical; only the WebGL2 appearance
  changes, now correctly. naga validation (`crt::tests::shader_parses_and_validates`)
  green.
- **iOS host in the CHANGELOG.** The v2.2.8 "Changed" host-wiring sentence now
  lists the iOS Metal host alongside desktop and Android (the parity the prior
  commit added), and the gamma bullet documents the exact-sRGB transfer.
- **Stale "current release" label.** The v2.2.4 entry in the AGENTS.md lineage
  paragraph still called v2.2.4 "the current release"; reworded to point at the
  actual current-release paragraph, so AGENTS.md carries one current-release
  record.

`docs/crt-composite.md` aligned to the exact-transfer description. rustfmt +
markdownlint clean; the emulation core, AccuracyCoin 141/141, and nestest 0-diff
are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR bumps the crate workspace to v2.2.8 "Aperture II" and updates the CRT presentation pipeline to perform scanline and aperture-mask darkening in linear light with a selectable Gaussian beam sharpness profile.

Blocking issues

None found.

Suggestions

  • crates/rustynes-gfx-shaders/src/lib.rs:L94-L97: In linear_to_srgb, pow(c, vec3<f32>(1.0 / 2.4)) is evaluated for all vector components prior to select. On GPUs where select does not short-circuit branch execution, passing zero or sub-zero values into pow can yield NaN or driver-specific floating-point exceptions. Guard the input with max(c, vec3<f32>(1e-5)) before exponentiation.
  • crates/rustynes-frontend/src/crt.rs:L232, crates/rustynes-android/src/gfx.rs:L461, crates/rustynes-ios/src/gfx_metal.rs:L510: Scanline sharpness (sharpness / aux.x) is hardcoded to 0.5 across frontend, Android, and iOS backends without exposing a configuration field or setter for user adjustment.
  • Lack of test coverage: No unit tests or shader uniform layout assertions were added to verify the srgb_to_linear / linear_to_srgb transfer functions or 64-byte uniform alignment, violating the requirement that tests accompany behavior changes.

Nitpicks

  • crates/rustynes-frontend/src/crt.rs:L128-L134: Struct fields sharpness and linearize on CrtFilter are private without accessor methods; storing aux: [f32; 4] directly would reduce boilerplate and match the uniform layout.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

doublegate added a commit that referenced this pull request Aug 4, 2026
…ilot #346)

Copilot flagged a real UX regression in the v2.2.9 detachable-window
conversion: routing every tool panel through the shared `detachable_window`
helper dropped each panel's bespoke `egui::Window` builder options —
`default_pos`, `default_size`, `default_width` / `min_width`, and
`resizable(false)` on ROM Info / Input Display / ROM Database / Performance.
Losing the `default_pos` values in particular collapsed the debugger's designed
workspace layout into egui's default overlap cascade on first open, and four
fixed-size panels silently became resizable.

`detachable_window` now takes a `WindowCfg { default_pos, default_size,
default_width, min_width, resizable }` (all `Option`, `Copy + Default`) and
applies each set field to the docked `egui::Window`; all 19 call sites (18
panels; `cheat_panel` has a native + a wasm variant) pass back their exact prior
values, so first-open placement/size and the four non-resizable panels are
restored. The config applies to the docked form only — a detached panel is a
real OS window the window manager sizes and places (egui persists the docked
window's own position/size by id after first open, so `WindowCfg` only seeds the
first appearance). Native + wasm32 `clippy -D warnings` clean on both feature
sets.

Also (proactive, matching the CodeRabbit finding already fixed on #345): the
v2.2.4 entry in the AGENTS.md lineage paragraph still called v2.2.4 "the current
release" — reworded to point at the actual current-release paragraph so
AGENTS.md carries a single current-release record.

Frontend-only; the emulation core, AccuracyCoin 141/141, and nestest 0-diff are
untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doublegate
doublegate merged commit b05ad53 into main Aug 4, 2026
28 checks passed
@doublegate
doublegate deleted the feat/v2.2.8-aperture-ii-video branch August 4, 2026 21:44
doublegate added a commit that referenced this pull request Aug 4, 2026
…ilot #346)

Copilot flagged a real UX regression in the v2.2.9 detachable-window
conversion: routing every tool panel through the shared `detachable_window`
helper dropped each panel's bespoke `egui::Window` builder options —
`default_pos`, `default_size`, `default_width` / `min_width`, and
`resizable(false)` on ROM Info / Input Display / ROM Database / Performance.
Losing the `default_pos` values in particular collapsed the debugger's designed
workspace layout into egui's default overlap cascade on first open, and four
fixed-size panels silently became resizable.

`detachable_window` now takes a `WindowCfg { default_pos, default_size,
default_width, min_width, resizable }` (all `Option`, `Copy + Default`) and
applies each set field to the docked `egui::Window`; all 19 call sites (18
panels; `cheat_panel` has a native + a wasm variant) pass back their exact prior
values, so first-open placement/size and the four non-resizable panels are
restored. The config applies to the docked form only — a detached panel is a
real OS window the window manager sizes and places (egui persists the docked
window's own position/size by id after first open, so `WindowCfg` only seeds the
first appearance). Native + wasm32 `clippy -D warnings` clean on both feature
sets.

Also (proactive, matching the CodeRabbit finding already fixed on #345): the
v2.2.4 entry in the AGENTS.md lineage paragraph still called v2.2.4 "the current
release" — reworded to point at the actual current-release paragraph so
AGENTS.md carries a single current-release record.

Frontend-only; the emulation core, AccuracyCoin 141/141, and nestest 0-diff are
untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
doublegate added a commit that referenced this pull request Aug 5, 2026
… tool windows (#346)

* fix(tas): wire TAStudio piano-roll edits to the emulator + robust .bk2 import (v2.2.9)

Two of the v2.2.9 'Studio II' items, both objectively verified:

TAStudio inputs now drive the emulator. handle_tas_requests (the piano-roll panel
path) only mutated the editor's input_log on a SetInput and never re-derived the
running Nes, so a cell edit looked disconnected from emulation (the NESdev-forum
'TAStudio inputs do not seem to be connected up' report). It now tracks an
input_dirty flag across the batch and does a single deterministic re-seek to the
cursor afterward, exactly like the scripting path (apply_tas_commands). InsertFrame
/ DeleteFrame / StampMacro also mark dirty; Seek / CreateBranch / LoadBranch reseat
the Nes themselves.

.bk2 import honors the LogKey column order. The parser ignored the LogKey: line and
mapped pad columns by fixed U D L R S s B A position, so a BizHawk movie authored
with a different column order or extra columns mapped every button to the wrong bit
('.bk2 did not play back'). parse_log_key now reads the per-port column order from
the LogKey (# groups, | columns), maps each column by its button name (ignoring the
'Pn ' prefix), and falls back to the standard order when a group is truncated/exotic
(preserving the existing tests). parse_pad maps by that column list and tolerates a
group LONGER than the modeled columns (extra buttons like a mic are ignored). A new
test proves a non-standard order + an extra column.

.bk2 import feedback is on-screen. handle_movie_import surfaced every outcome via
eprintln! to a terminal nobody sees (so a failed import looked like nothing
happened). It now sets the on-screen status line for each path (no ROM, parse error,
wrong-ROM seek failure, success) via StatusMessage. Consolidated the file's nine
per-function StatusMessage imports into one module-level use.

Verification: bk2 tests 7/7 (incl. the new order test), rustynes-frontend 464/464,
core no_std cross-compile clean, clippy -D warnings + fmt clean on both crates.
Remaining v2.2.9 item: floating tool windows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ui): detachable/floating tool windows (v2.2.9 "Studio II")

Addresses the NESdev-forum report that tool windows are trapped inside the
main OS window on Windows 10: every debugger/tool panel used
`egui::Window::new(...)` inside the single central viewport, so it could never
leave the host window.

Adds a shared `detachable_window` helper in `debugger/mod.rs` that gives each
panel a "⧉ Detach" button. Detached, the panel renders in a real OS window via
`ctx.show_viewport_immediate` (the same egui multi-viewport mechanism
`basic_bot_panel` already used) with a "⧉ Reattach" button; the OS window's
close button reattaches too. A `DebuggerOverlay::detached_panels:
HashSet<&'static str>` (keyed by each panel's stable id) tracks which panels
are floating across frames.

**Native-only by construction.** egui multi-viewport needs winit multi-window,
absent on wasm, so the detached branch and the Detach button are
`#[cfg(not(target_arch = "wasm32"))]`; on wasm the panel always renders docked
in an `egui::Window`, unchanged. The helper carries a wasm-scoped
`allow(clippy::needless_pass_by_ref_mut)` plus a `let _ = (&detached, id)`
discard so both the rustc `unused_variables` and clippy `needless_pass_by_ref_mut`
lints stay green there without desyncing the native signature (verified:
`cargo clippy -p rustynes-frontend --target wasm32-unknown-unknown --lib --bins`
clean for both the default and `wasm-canvas` feature sets).

17 panels are routed through the helper (PPU, OAM, APU, Memory, Event Viewer,
NSF, Mapper, Watch, Trace, Cheats [native + wasm cfg variants], ROM Database,
Performance, Documentation, Input Display, Audio Mixer, Replay/TAS, Memory
Compare, ROM Info), each dropping its bespoke `.resizable()/.default_pos()/
.default_size()/.min_width()` builder options for the shared affordance. Panels
whose `show()` returns a value (`cpu_panel`) or that already own multi-window /
config-heavy bodies (settings, netplay, cheevos, input-rebind, tastudio,
basic_bot) are intentionally left for a follow-up.

Frontend-only — the deterministic core, save-states, and every golden vector
are untouched (AccuracyCoin 141/141, nestest 0-diff).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(release): bump to v2.2.9 "Studio II" + docs

Version bump 2.2.8 → 2.2.9 (workspace `version`, Cargo.lock) and the
docs-as-spec sync for the "Studio II" release — the fourth step of the
v2.2.6 → v2.3.0 NESdev-remediation line, capping the TAStudio-wiring, `.bk2`
playback, and detachable-tool-window work committed earlier on this branch.

- **CHANGELOG.md** — new `[2.2.9]` section (Fixed: TAStudio piano-roll edits
  now drive the emulator, `.bk2` playback honors the `LogKey` column order;
  Added: detachable/floating tool windows across 17 panels, native-only).
- **docs/STATUS.md** (single source of truth) — current-release lead reset to
  v2.2.9, demoting v2.2.8 to "Built on".
- **README.md** — Current Release lead updated to v2.2.9.
- **AGENTS.md** — both the top current-release block and the operating-note
  paragraph lead with v2.2.9; the "never claim a version later than …" guard
  and the v2.2.6 → v2.3.0 line-summary bump to mark v2.2.8/v2.2.9 shipped.
- **docs/frontend.md** — detachable multi-viewport tool windows moved out of
  the Deferred list into shipped (v2.2.9), with the `detachable_window` /
  `show_viewport_immediate` mechanism noted.

Frontend-only across the whole release, so the deterministic core, save-states,
and every golden vector are byte-identical: **AccuracyCoin 141/141**, nestest
0-diff. The detached-window behavior itself awaits an on-device (ideally
Windows-10) visual check; the mechanism compiles + clippy-passes on native and
both wasm feature sets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ui): preserve per-panel window geometry in detachable_window (Copilot #346)

Copilot flagged a real UX regression in the v2.2.9 detachable-window
conversion: routing every tool panel through the shared `detachable_window`
helper dropped each panel's bespoke `egui::Window` builder options —
`default_pos`, `default_size`, `default_width` / `min_width`, and
`resizable(false)` on ROM Info / Input Display / ROM Database / Performance.
Losing the `default_pos` values in particular collapsed the debugger's designed
workspace layout into egui's default overlap cascade on first open, and four
fixed-size panels silently became resizable.

`detachable_window` now takes a `WindowCfg { default_pos, default_size,
default_width, min_width, resizable }` (all `Option`, `Copy + Default`) and
applies each set field to the docked `egui::Window`; all 19 call sites (18
panels; `cheat_panel` has a native + a wasm variant) pass back their exact prior
values, so first-open placement/size and the four non-resizable panels are
restored. The config applies to the docked form only — a detached panel is a
real OS window the window manager sizes and places (egui persists the docked
window's own position/size by id after first open, so `WindowCfg` only seeds the
first appearance). Native + wasm32 `clippy -D warnings` clean on both feature
sets.

Also (proactive, matching the CodeRabbit finding already fixed on #345): the
v2.2.4 entry in the AGENTS.md lineage paragraph still called v2.2.4 "the current
release" — reworded to point at the actual current-release paragraph so
AGENTS.md carries a single current-release record.

Frontend-only; the emulation core, AccuracyCoin 141/141, and nestest 0-diff are
untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* license: relicense to GPL-3.0-or-later — RustyNES is a derivative work of GPL emulators

RustyNES incorporates and is derived from code from GPL-licensed NES emulators.
It is therefore a derivative work distributable only under the GPL, and this
commit relicenses it from `MIT OR Apache-2.0` to **GPL-3.0-or-later**, credits
the derived-from sources per subsystem, and withdraws the incorrect "no GPL
source incorporated" position taken in v2.2.5 "Colophon".

Context. A NESdev community review found that the codebase contains bugs,
constants, variable names, code ordering, and comments referencing specific
upstream files, functions, and line numbers that go well beyond using an
emulator as a testing oracle. That is correct. The project's own in-source
comments, before a v2.2.5 edit reworded them, said as much: "Faithful port of
Mesen2's `ProcessSpriteEvaluation` (`NesPpu.cpp:1015-1141`)", "Ported bit-for-bit
from puNES `JV001.c`", "numeric tables ported verbatim from Bisqwit's C", and
~12 "Ported from Mesen2 `<file>.h`" mapper comments. v2.2.5 reframed that code as
"oracle cross-checks" and kept a permissive license the combined work was not
entitled to use. Laundering GPL code through AI tooling does not change its
license, and responsibility for what landed in the tree rests with the project.

Derived-from sources and their licenses (full file-by-file table in
docs/originality-and-provenance.md Section 1):
  - Mesen2 (GPL-3.0-or-later): CPU unstable-store opcodes; the PPU
    sprite-evaluation FSM + OAM-data-bus model; ~15 mapper boards (Bandai EEPROM,
    JY Company, Waixing, Sachen, Txc, NTDEC, Kaiser, MMC3 variants, FK23C,
    CoolBoy); the Bisqwit NTSC filter tables; the UNIF tables; the debug-symbol
    importer; the PGO harness.
  - puNES (GPL-2.0-or-later): JV001 / mapper 147 (bit-for-bit); the FDS per-CRC
    drive-timing table.
  - FCEUX (GPL-2.0-or-later): UNIF handling; some mapper banking.
  - Nestopia UE (GPL-2.0-or-later): FME-7 / 5B audio detail.
Every upstream grants "or (at your option) any later version", so the
GPL-2.0-or-later material upgrades to v3 and GPL-3.0-or-later is the correct,
consistent expression for the combined work. GeraNES (GPL-3.0-only) was used as
an oracle only, with no code derived, so it does not force `-only`.

Changes:
  - LICENSE is now the GPLv3 text; LICENSE-MIT and LICENSE-APACHE are removed;
    the workspace + rustynes-cheevos `license` fields become GPL-3.0-or-later;
    deny.toml allows GPL-3.0-or-later for the project's own crates (cargo-deny
    `check licenses` = ok); release.yml packages LICENSE instead of the two
    removed files.
  - docs/originality-and-provenance.md is rewritten to lead with the derivation
    table and the derivative-work declaration; NOTICE attributes each GPL
    upstream and the code derived from it; README, AGENTS, CONTRIBUTING, SUPPORT,
    ROADMAP, the in-app About/CLI/doc-panel strings, the Android about_body
    (EN + ES), and the libretro `.info` license field all state GPL-3.0-or-later.
  - New ADR 0036 records the decision, the SPDX rationale, and the GPLv3/App-Store
    distribution caveat. The scattered "port of" comments are deliberately NOT
    restored (they were imprecise; the audited derivation table supersedes them),
    but the derivation is now stated plainly and completely.
  - Incorporated permissive components (emu2413/MIT, TriCNES/MIT, rcheevos/MIT,
    blip_buf/LGPL-2.1-or-later, fonts) are GPL-compatible and keep their notices.

Zero emulation-core behavior change: AccuracyCoin holds 141/141 and nestest is
0-diff by construction. This is a licensing and documentation correction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: bump README version badge to v2.2.9

Carry-over fix: the version badge still read v2.2.8 after the v2.2.9 doc bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(provenance): add GPLv3 SPDX + per-file provenance headers to derived source

Follows the v2.2.9 relicense: now that the accurate license and attribution are
established, mark the derived source itself. Each of the 23 files that contains
code derived from a GPL emulator gains a top-of-file header:

    // SPDX-License-Identifier: GPL-3.0-or-later
    //
    // Provenance: <what is derived, from which upstream file/function>. See
    // docs/originality-and-provenance.md (Section 1) and NOTICE ...

so the license and the specific upstream are discoverable at the point of use —
e.g. `rustynes-ppu/src/ppu.rs` names Mesen2 `NesPpu.cpp`
(`ProcessSpriteEvaluation` / `ReadSpriteRam`) plus the TriCNES (MIT) octal-latch
model; `rustynes-mappers/src/fds.rs` names puNES `fds.c`;
`rustynes-frontend/src/ntsc_bisqwit.rs` records the verbatim-ported Bisqwit
tables via Mesen2. The ~15 Mesen2-derived mapper boards, the CPU unstable-store
opcodes, the emu2413/blip_buf audio, the CRT-shader reimplementations, the debug-
symbol importer, and the PGO harness are all likewise marked.

This is the accurate replacement for the old scattered, imprecise per-line "port
of" comments (not restored verbatim); the SPDX + provenance headers plus the
audited §1 derivation table are the discoverable record. CHANGELOG, ADR 0036, and
docs/originality-and-provenance.md §8 are updated to describe this approach.

Comments only — `cargo fmt --all --check` clean, `cargo check --workspace`
compiles, zero behavior change (AccuracyCoin 141/141, nestest 0-diff).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: address CodeRabbit #346 review + add the provenance-failure post-mortem

Two things: the maintainer-directed forensic analysis of the GPL-provenance
failure, and the CodeRabbit review pass on the v2.2.9 PR.

**docs/provenance-failure-postmortem.md (new).** A complete, evidence-cited
reconstruction of how RustyNES came to incorporate lifted GPL emulator code
despite a black-box-only instruction: the timeline across RustyNES_v2 (the
private "engine stack" where the porting happened, 2026-05, Opus 4.7) and the
2026-06-13 transplant into this repo; the two distinct failures (the port itself,
then the v2.2.5 scrubbing of the honest "port of" comments); the root causes
(GPL source on disk + accuracy-bar goal + no firewall; the guardrail post-dating
the violation; honest-at-build-time then laundered; multi-session framing
propagation; trusted AI self-attestation); and an honest accounting of what is
NOT recoverable (the RustyNES_v2 porting-era session logs are gone). The single
hardest fact: the original "Faithful port of Mesen2's ..." comments still exist
verbatim in RustyNES_v2 today — only this public repo scrubbed them. Linked from
originality-and-provenance.md §8.

**CodeRabbit #346 review (9 threads):**
- **`.bk2` LogKey empty-field bug (Major, data integrity).** `parse_log_key`
  filtered out empty positional fields, shifting later columns/groups (an empty
  console group promoted P2's map into P1; an empty interior column misaligned
  buttons so `U.A` replayed as `Up` alone). Now strips only the syntax delimiters
  and keeps interior empties; +regression test for both cases.
- **TAS branch/load ordering (correctness).** `CreateBranch` / `LoadBranch`
  cleared `input_dirty` without flushing pending edits, so a branch snapshot
  captured stale state; they now `ed.seek` to flush first.
- **`WindowCfg` -> `ViewportBuilder` (Major).** The detached branch maps default
  size / position / resizability onto the viewport, not just the docked window.
- **Multi-viewport honesty (Major).** RustyNES's frontend is a single-viewport
  `egui_winit` integration, so `show_viewport_immediate` renders the "detached"
  panel EMBEDDED in the main window rather than a separate OS window — it does not
  yet fully resolve the Windows-10 trapped-window report. Documented honestly in
  code, CHANGELOG, AGENTS.md, and docs/frontend.md; true OS-window detach is
  tracked follow-up. Corrects an overclaim.
- Doc/metadata: panel count 17 -> 18; ADR 0036 "by construction" -> verified
  release-check evidence + STATUS link; README badge/BibTeX -> v2.2.9; SUPPORT
  current-release v2.0.4 -> v2.2.9; libretro display_version -> v2.2.9; Android
  about_body 139/139 -> 141/141 and 168 -> 172 mappers (EN + ES).

Core-affecting fixes (bk2, TAS) are core/frontend only; cargo check + the bk2
tests pass. AccuracyCoin 141/141 unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(postmortem): the black-box instruction was given but not enforced/followed

Reconcile the root-cause framing with the maintainer's correction: the black-box
/ oracle-only instruction WAS given — the failure was that it was not mechanically
enforced (no barrier at the tool boundary; no persisted written rule in the loaded
guidance until 2026-06-13) and the porting model did not follow it. §4.1 and §4.2
reframed from "no guardrail / the guardrail post-dated the violation" to
"instruction given, neither persisted early nor enforced"; §4.5 sharpened (an
instruction the agent can silently disregard and then falsely certify is not a
control). The evidentiary caveat is unchanged: the porting-era logs are gone, so
the exact wording/timing of the spoken instruction cannot be quoted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(postmortem): formatting + self-consistency pass on the maintainer's edits

Non-substantive cleanup of the maintainer's review edits: stripped trailing
whitespace (§1, §2, and the closing NOTE), evened out the wrap widths the inline
edits left uneven, standardized hyphen-as-dash to em-dash in the NOTE, fixed one
phrase that didn't parse ("finally did baseline" -> "finally did become the
baseline"), and reconciled §5 with §4.2's "written instruction" framing (dropped
the now-inconsistent "or verbally"; the honest "cannot be quoted, logs gone"
point is unchanged). No substantive claims or the maintainer's wording/voice
were altered. markdownlint clean, no trailing whitespace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(provenance): correct the laundered per-function comments at ported sites

Restore accurate, license-specific attribution at each genuine ported site,
replacing the v2.2.5-laundered false-independence claims that were STILL in the
shipped per-function comments (the SPDX top-of-file headers alone did not undo
them). At each ported function the comment now names the upstream file/function
and its license, instead of asserting "an independent implementation … no
third-party emulator code is incorporated" / "cross-checked … as oracles".

Corrected (18 files) — examples:
  - `cpu.rs` SH* stores → Mesen2 `NesCpu.h` (`SyaSxaAxa`), GPL-3.0-or-later
  - `ppu.rs` OAM-data-bus / sprite-eval → Mesen2 `NesPpu.cpp`
    (`ProcessSpriteEvaluation` / `ReadSpriteRam`), GPL-3.0-or-later
  - `ntsc_bisqwit.rs` → tables ported verbatim via Mesen2 `BisqwitNtscFilter`,
    GPL-3.0-or-later
  - `blip.rs` → BLEP technique derived from Shay Green's `blip_buf`,
    LGPL-2.1-or-later (our kernel is a finer 32-phase refinement)
  - mapper boards (m016 Bandai EEPROM, m035/lib JY, m176/m268 FK23C/CoolBoy,
    m513/mmc3_clones/sachen_discrete Sachen, multicart/ntdec NTDEC/Txc, kaiser
    Waixing) → their specific Mesen2 `.h` sources, GPL-3.0-or-later; CoolBoy also
    FCEUX (GPL-2.0-or-later)
  - `unif.rs` → Mesen2 `UnifLoader.cpp` (GPLv3) + FCEUX `unif.cpp` (GPLv2)
  - `sachen_discrete.rs` JV001 → puNES `JV001.c` / `mapper_147.c` (GPL-2.0-or-later)
  - `fds.rs` per-CRC drive table → puNES `src/core/fds.c` (GPL-2.0-or-later)
  - `source_map.rs` → mirrors Mesen2 `DbgImporter` (GPL-3.0-or-later)

Deliberately LEFT unchanged (no over-attribution): `opll.rs` (already honestly
"a pure-Rust port of emu2413", MIT), `pgo_trainer.rs` (honest `PGOHelper`
pattern), `palette_gen.rs` / `crt_stack.rs` (documented method / genuine
look-reimplementation, no false claim), and `m069_sunsoft_fme7.rs` (its Mesen2
mentions are genuine `_volumeLut` oracle cross-checks). Genuine oracle-comparison
mentions ("matches Mesen2", "Mesen2-independent oracle") were NOT converted.

Comments only — `cargo fmt`, `cargo check`, and `cargo clippy -D warnings`
(cpu/ppu/apu/mappers/frontend) all clean. Zero behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: add the themed PDF of the provenance-failure post-mortem (ref-docs/)

A styled PDF rendering of docs/provenance-failure-postmortem.md for the reference
corpus, at ref-docs/RustyNES_Provenance-Failure-Postmortem.pdf. Themed as a
forensic incident record — oxblood-crimson accents (severity), a charcoal serif
body (official-record readability), dark-slate evidence-table headers, and
monospace for commit hashes / file paths — with a title block, table of contents,
and page footers. 8 pages, US Letter. Built with pandoc 3.6.1 -> WeasyPrint 68.1
from the committed markdown; content is identical to the source document.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: add ingestible provenance/license guardrails for AI-assisted emulator dev

A forward-looking, general-purpose ruleset distilled from the provenance-failure
post-mortem: docs/ai-emulator-provenance-guardrails.md. Written to be dropped
into a project's agent instructions / permanent memory BEFORE development starts
so the same trap cannot recur — in this project or any other emulator project
using prior art (reference emulators, test ROMs).

Contents: why emulators are a special trap for AI agents (accuracy is convergent
+ references are mostly copyleft); a classification of every external input
(documentation / test ROMs / observable oracles / incorporated components) and
what each permits; the reference firewall (oracles are run and observed, never
opened and read — enforced by a denied read-path + a CI check, not by prose);
attribution on four consistent surfaces (site comment + SPDX + central table +
NOTICE) with a no-over-attribution rule; license arithmetic (the or-later grant,
combined-work copyleft, the license gate); mechanical enforcement; a
pre-development checklist; a paste-ready guardrail block for CLAUDE.md/AGENTS.md;
a remediation runbook (do NOT scrub); and a red-flags table of the thoughts that
precede the failure. Intended for sharing as NESdev-community best-guidance.

Cross-linked from the post-mortem's §7 (Lessons and prevention) as the actionable
counterpart. markdownlint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: restyle the post-mortem PDF — calm cool palette, sans-serif, 4pp

Redesign per maintainer feedback ("less harsh on the eyes"): a cool blue/teal
palette (navy title, blue headings + rules, teal accents) on soft cool-slate
text, with RED reserved only for genuine takeaways; humanist sans-serif
throughout (Fira Sans, with FiraCode for code) at a comfortable weight/leading;
a two-column layout for an ideal ~55-60 character measure (per readability
research); and the maintainer's closing NOTE set apart in a full-width
light-blue "Maintainer's Statement" box. Compacted from 8 pages to 4 (US Letter).
Built pandoc 3.6.1 -> a bs4 DOM assembler (full-width title + evidence table
between 2-column prose groups) -> WeasyPrint 68.1. Content unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(guardrails): make the provenance guardrails a standalone, console-agnostic doc

Generalize docs/ai-emulator-provenance-guardrails.md so it stands on its own as a
community resource — no references to any specific project or its source-tree
documents, and applicable to emulation of ANY console, not just the NES:

- Reworded the opening to describe the failure pattern generically (no specific
  project, no cross-link to a project post-mortem) and to state it applies to
  NES / SNES / Genesis / Game Boy / N64 / PlayStation / arcade / etc.
- Broadened the reference-emulator examples across consoles (Mesen2/FCEUX,
  bsnes/Mesen-S, Genesis Plus GX/BlastEm, SameBoy/mGBA, ares/higan/MAME, …) in
  the bucket table and the paste-ready block.
- Genericized the example provenance comment, the central-table / provenance-doc
  filenames (`PROVENANCE.md` or equivalent), the firewall-check grep, and the
  "matches reference X" over-attribution example.
- Removed the "case study" / "this project" phrasings and the two links to
  project source-tree docs (post-mortem, derivation table); the remediation and
  closing now speak generally.

Content and rules are unchanged; only the framing is now project-neutral and
multi-console. markdownlint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(pdf): convert provenance post-mortem to full-width single-column layout

Reflow `ref-docs/RustyNES_Provenance-Failure-Postmortem.pdf` from the
prior dense two-column treatment to a single wide-column, full-page
layout. The two-column measure forced an awkward mid-title column break
and cramped the first page; the evidence timeline table (already a
full-width block) sat inconsistently between the two flowed columns.

Typographic changes (theme CSS, WeasyPrint pipeline unchanged otherwise):
- `.cols` collapses from `columns: 2` (with a column rule) to a plain
  single-column block; body text switches from justified to left-aligned
  (ragged right) for the wider measure.
- Base type 8.75pt -> 10.2pt, line-height 1.4 -> 1.5; page margins
  widened to 1.9/2.1/1.6/2.1cm to hold the single-column measure near a
  comfortable ~80-char line rather than a full 19cm bleed.
- Headings scaled to the new base (h2 11.5 -> 13.5pt, h3 9.6 -> 11pt),
  the evidence table 6.9 -> 8.4pt, code blocks 7.6 -> 8.6pt, and the
  maintainer's NOTE box 8.6 -> 10pt.

Cool blue/teal structure, red-only-for-takeaways emphasis, and the
blue-shaded "MAINTAINER'S STATEMENT" NOTE box are all preserved; the
document grows from 4 to 5 pages, which the maintainer accepted
("regardless of the final page count"). Source markdown
`docs/provenance-failure-postmortem.md` is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(provenance): unquote maintainer's statement; italicize it in the PDF

Two presentation tweaks to the closing "Maintainer's Statement":

- Source `docs/provenance-failure-postmortem.md`: remove the surrounding
  quotation marks from the DoubleGate NOTE so the statement reads as a
  first-person remark rather than a quoted block. Wording unchanged.
- Regenerated `ref-docs/RustyNES_Provenance-Failure-Postmortem.pdf`: the
  PDF assembly now wraps everything after "NOTE (from DoubleGate): " in
  an <em>, so the statement body renders italic while the "NOTE
  (from DoubleGate):" label stays upright. This is a PDF-only styling
  step (the assembler splits the note paragraph at the "): " marker and
  re-parents the trailing nodes — including the `~/.claude/` code span —
  under an emphasis element); the Markdown source carries no emphasis
  markup so its own rendering is unaffected.

Layout, theme, and page count (5) are otherwise unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(provenance): render the section cross-reference as bold "7. Lessons and prevention"

In the maintainer's NOTE, change the cross-reference from "#7 'Lessons
and prevention'" to a bold "7. Lessons and prevention" in both surfaces:

- Source `docs/provenance-failure-postmortem.md`: wrap the phrase in
  `**...**` and drop the `#` prefix and single quotes.
- Regenerated `ref-docs/RustyNES_Provenance-Failure-Postmortem.pdf`: the
  phrase is now a <strong> inside the italic NOTE body, so it reads as a
  bold section label (the assembler re-parents it under the note's <em>
  along with the rest of the statement body).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(pdf): render "Fiskbit" bold and upright in the maintainer's NOTE

PDF-only styling: within the italic NOTE body, "Fiskbit" is now wrapped
in a <strong class="upright"> so it reads bold and non-italicized while
the rest of the statement stays italic. The assembler splits the note
text run and re-parents the name under an upright strong; the theme adds
`.note-box strong.upright { font-style: normal; }` to cancel the
inherited italic (leaving the bold "7. Lessons and prevention" label,
also a strong, italic as before). Markdown source unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(ref): add themed PDF of the AI-emulator provenance guardrails

Render `docs/ai-emulator-provenance-guardrails.md` to a styled PDF at
`ref-docs/AI-Emulator-Provenance-Guardrails.pdf`, using the same cool
"Calm" family theme as RustyNES_Provenance-Failure-Postmortem.pdf so the
two provenance documents read as a set.

Pipeline (pandoc gfm -> html5, a small BeautifulSoup title-block/emphasis
pass, WeasyPrint with a dedicated theme CSS):
- Full-width single-column, humanist sans (Fira Sans), blue/teal
  structure; red reserved for the few hardest takeaways ("capability +
  availability + accuracy objective", "C used as if it were A",
  "source physically unavailable to the agent", "Never launder").
- Title block reflecting the doc's own framing (community best-guidance;
  ready-to-ingest ruleset), running header/footer retitled for this doc.
- The document's own structures styled to match: the "in one sentence"
  blockquote becomes a teal TL;DR callout; GitHub task-list checklists
  render as blue-outlined checkboxes (the real <input> hidden, the box
  drawn as an absolutely-positioned gutter marker — reliable in
  WeasyPrint); the paste-ready block keeps the monospace code panel;
  tables get the navy-header/zebra treatment with hyphenated long words.

8 pages. The Markdown source is unchanged; this is a presentation
artifact derived from it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(pdf): fix the paste-ready guardrail block sizing in the guardrails PDF

The §8 code panel rendered at 8.6pt, so its ~95-char lines wrapped raggedly
inside the box and one bullet's leading "-" was orphaned onto its own line.
Drop the monospace size to 6.9pt (the block's lines now fit the panel
width) and add a hanging indent so any residual continuation line stays
readable under its bullet. The document tightens from 8 to 7 pages;
nothing else changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(agents): ingest the provenance/license firewall as the top development rule

Add a `## MOST IMPORTANT RULE — Provenance & license firewall` section at
the top of the AGENTS.md project block (reached via the CLAUDE.md / GEMINI.md
symlinks), positioned before "## What this is" so it is the first substantive
guidance every session loads. It is declared to outrank everything else in the
file.

Motivation: RustyNES is a corrected provenance failure — GPL emulator source
(Mesen2, puNES, FCEUX, GeraNES) was reproduced despite a black-box instruction,
the honest "ported from X" comments were later scrubbed, and the project was
relicensed MIT/Apache -> GPL-3.0-or-later as the derivative work it actually is.
The failure was caught by an outside NESdev reviewer, not by tooling, which is
the empirical basis for the "do not self-certify" clause. The full preventive
ruleset now lives in docs/ai-emulator-provenance-guardrails.md (with a forensic
post-mortem in docs/provenance-failure-postmortem.md); this section is the
always-loaded distillation that binds an agent before it touches any file.

The section encodes the six non-negotiables — the REFERENCE FIREWALL (reference
emulators are black-box oracles whose output may be observed but whose source is
never read or reproduced; the local ref-proj/ clone is removed from disk and
stays gitignored so the source is out of reach by design), IMPLEMENT FROM DOCS,
IF YOU DERIVE SAY SO AND STOP (attribute at the site + originality doc §1 +
NOTICE + SPDX; keep the license GPL-3.0-or-later-compatible), NEVER LAUNDER, NO
OVER-ATTRIBUTION, and DO NOT SELF-CERTIFY — and points at the mechanical
enforcement that backs the prose (the gitignore / dockerignore / markdownlintignore
/ CodeRabbit exclusions, deny.toml, and the per-file SPDX + provenance headers),
on the principle that a rule the tooling enforces beats a rule an agent is merely
asked to follow. Also updates the existing `.markdownlintignore` note to record
that ref-proj/ is now removed from disk but retained in the ignore lists as a
firewall guard rather than as a build convenience.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(provenance): normalize in-source reference citations to upstream paths

Delete the removed-clone `ref-proj/` prefix from every in-source provenance
citation so each names the upstream project + file directly (attribution
surface #1 of the guardrails: "the upstream project, the specific file/function,
and its license"), rather than a path into a local working copy that no longer
exists. `ref-proj/GeraNES/src/GeraNES/Mappers/Mapper0NN.h` becomes
`GeraNES/src/GeraNES/Mappers/Mapper0NN.h`; `ref-proj/Mesen2/Core/...`,
`ref-proj/TriCNES/Emulator.cs`, `ref-proj/tetanes`, and `ref-proj/fceux/...`
likewise. The one non-path use — m024_vrc6.rs's "a cross-check against the whole
`ref-proj/` field" — is reworded to "the whole field of reference emulators".

This is a pure path/wording normalization: it does not change any derivation
claim. The sites that genuinely document a port keep their verb and license
verbatim — ppu.rs still reads "Ported from TriCNES (`TriCNES/Emulator.cs`, MIT,
commit 9199870)", and m093_sunsoft3r.rs still says its `writePrg` matches "the
designated reference `GeraNES/src/GeraNES/Mappers/Mapper093.h`, whose `writePrg`
opens with `data &= readPrg(addr);`". Nothing is softened, laundered, or
over-attributed; only the dangling local-clone prefix is removed.

Scope: 30 files across rustynes-core (movie_interop), rustynes-frontend (crt +
two debugger panels), rustynes-mappers (28 board modules), and rustynes-ppu.
The changes are comments and doc-comments only — no code tokens move — so the
compiled `#![no_std]` chip stack is byte-identical, the deterministic contract
is untouched, and AccuracyCoin holds 141/141 and nestest stays 0-diff by
construction. Verified: `cargo check --workspace` clean,
`RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` clean,
`cargo fmt --all --check` clean, and `git grep "ref-proj/" -- crates/**/*.rs`
now returns nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: account for the ref-proj/ removal + reference firewall across docs and ignores

The local `ref-proj/` reference-emulator clone (Mesen2, puNES, FCEUX, GeraNES,
TriCNES, tetanes, ...) has been removed from disk. Update the surrounding
documentation and ignore configuration so nothing points a developer — or an
agent — back at reference-emulator source, and so the firewall is stated where
the setup that used to depend on ref-proj/ lived.

- .gitignore: keep the `/ref-proj/` entry but re-annotate it as a *firewall
  guard* — the directory is removed and must never re-enter the working tree,
  because its copyleft source is what made RustyNES a derivative work. The entry
  now cross-links the guardrails doc and the AGENTS.md top rule. (The parallel
  ignores in .dockerignore / .markdownlintignore / .pre-commit-config.yaml /
  .coderabbit.yaml are retained unchanged for the same belt-and-suspenders
  reason.)

- Oracle / trace tooling (docs/tooling/oracle-tooling-setup.md,
  docs/ppu-trace-tooling.md): add a REFERENCE FIREWALL banner and rewrite the
  ref-proj/ paths. These guides build and instrument a reference emulator to
  capture its *output* for cross-diffing — legitimate black-box-oracle use — so
  they now state that any such build must live out-of-tree, outside the agent's
  allowed paths, and be used for output only; the committed golden vectors
  (crates/rustynes-test-harness/golden/) remain the preferred, self-contained
  path that needs no reference source at all.

- Provenance / spec docs: originality-and-provenance.md records that the §1
  derivation table was cross-checked against the sources at the time (the
  since-removed ref-proj/ clone) and stands on its named upstream citations;
  STATUS.md, to-dos/ROADMAP.md, adr/0030, adr/0006, apu-2a03.md,
  hd-pack-zelda-troubleshooting.md, and SALVAGE_MANIFEST.md have their ref-proj/
  citations normalized to upstream (or, where they said "vendored ref-proj/X",
  corrected to "out-of-tree" / "in-repo", since the clone is no longer vendored).

- Discoverability: add a "Provenance & Licensing" section to
  docs/DOCUMENTATION_INDEX.md and a matching group to the mkdocs nav so the
  guardrails, post-mortem, originality record, and ADR 0036 are linked from the
  documentation entry points rather than only from AGENTS.md.

Frozen / historical trees (docs/archive/, to-dos/archive/, to-dos/plans/**,
ref-docs/, .github/release-notes/, CHANGELOG-FULL.md) deliberately keep their
ref-proj/ mentions as immutable record. No behavior changes; markdownlint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: document the reference firewall + guardrails in README and CHANGELOG

Surface the provenance guardrails on the two developer-facing entry points.

- README.md: add a "Reference firewall (so it does not recur)" paragraph to the
  License section, immediately after the existing GPLv3-derivation and
  AI-assistance disclosures. It names the forensic post-mortem and the
  console-agnostic guardrails ruleset (with the themed PDFs in ref-docs/),
  states that it is the project's top development rule ingested into AGENTS.md,
  and summarizes the firewall: reference emulators are black-box oracles whose
  output may be observed but whose source is never read; the ref-proj/ clone is
  removed and gitignored so that source is out of reach; hardware behavior is
  implemented from documentation and test ROMs; genuine derivation is attributed
  and license-checked, never laundered. Notes the guardrails are shared as
  community best-guidance for other AI-assisted emulator projects.

- CHANGELOG.md [Unreleased]: add a "Provenance guardrails + reference firewall"
  block recording the new guardrails doc + post-mortem + PDFs, the ingestion
  into AGENTS.md and the memory bank, the ref-proj/ removal and the retained
  firewall ignores, the comments-only normalization of in-source citations to
  upstream paths (deterministic core byte-identical), the out-of-tree oracle
  posture in the tooling docs, and the new documentation-index / mkdocs-nav
  entries.

Documentation only. markdownlint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(provenance): align the §1 derivation table with the in-source citations

A CodeRabbit review of #346 flagged a per-site ↔ central-table inconsistency in
the provenance record. A full audit of every `derived from Mesen2's \`X\`` comment
against its `docs/originality-and-provenance.md` §1 row confirmed six files whose
in-source citations name an upstream Mesen2 header that the central table row
omitted. Each such header is a shared chip/transform that a sibling file already
lists in its own row, so the table under-reported which files derive from it:

- `kaiser.rs` (mapper-253 IRQ board, line 614) derives from `Waixing/Mapper253.h`.
- `m035_jy_asic.rs` (`invert_prg_bits`, line 315) derives from `InvertPrgBits`.
- `m176_bmc_fk23c.rs` (CoolBoy banking, line 554) derives from `Mmc3Variants/MMC3_Coolboy.h`.
- `m513_sachen_9602.rs` (TxcChip accumulator, line 349) derives from `Txc/TxcChip.h`.
- `mmc3_clones.rs` (Sachen 8259A/B/C, mappers 138/139/141, line 784) derives from `Sachen/Sachen8259.h`.
- `ntdec.rs` (BMC-11160, line 1262) derives from `Txc/Bmc11160.h`.

Add each missing upstream header to the corresponding table row so the central
derivation record is fully consistent with the per-site attributions, per the
provenance guardrails' "provenance-comment ↔ table consistency" rule. This is an
alignment, not new attribution: each derivation is already asserted verbatim in
the source comment; the table now records what the code already documents (a
correction toward completeness, not over-attribution). No license changes — all
six sources are Mesen2 (GPL-3.0-or-later), already the project's license.
Documentation only; markdownlint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: resolve CodeRabbit provenance-firewall review on the tooling/ignore docs

Address four follow-on CodeRabbit findings on #346:

- .gitignore: the ref-proj/ guard comment claimed the referenced emulators'
  source is uniformly "copyleft". That over-generalized — Mesen2 / puNES / FCEUX
  / GeraNES are GPL, but TriCNES is MIT. Reword to state the licenses are
  project-specific (copyleft for the four GPL oracles; MIT for TriCNES) and note
  that the MIT TriCNES source is instead deliberately vendored, with attribution,
  under crates/rustynes-test-harness/golden/tricnes/. The /ref-proj/ ignore rule
  itself is unchanged.

- docs/tooling/oracle-tooling-setup.md: resolve a genuine contradiction. The two
  firewall notes said "any Mesen2 / TriCNES build must live out of tree", but the
  same page (§2a) vendors the complete MIT TriCNES source in-repo at
  golden/tricnes/tricnes-full-src/ and calls the in-tree harness the preferred
  path. Scope the out-of-tree / never-reproduce rule to the copyleft references
  (Mesen2, puNES, FCEUX, GeraNES) and state TriCNES explicitly as the MIT
  exception whose in-repo vendoring is license-compatible and not a firewall
  violation. The committed golden vectors remain the preferred, no-live-emulator
  path.

- docs/originality-and-provenance.md and docs/DOCUMENTATION_INDEX.md: describe the
  removed reference-emulator clone without reproducing its literal repository path
  in these provenance/index prose additions (the path stays authoritatively named
  where it is load-bearing — the /ref-proj/ ignore rule and the AGENTS.md rule).

Documentation only; markdownlint clean. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(core): bound .bk2 LogKey parsing against #-group allocation amplification

`bk2_interop::parse_log_key` collected every `#`-separated group of the BizHawk
`LogKey` header into a `Vec<&str>` and then read only `groups[1]` (P1) and
`groups[2]` (P2). A hostile `.bk2` whose `LogKey` is padded with a large run of
`#` delimiters therefore allocated one `&str` slot (~16 bytes on 64-bit) per
empty group — an unbounded, ~16x-of-input allocation on an untrusted import path,
the same DoS class the v2.2.0 `Movie::deserialize` fuzzing already closed
elsewhere.

Read the three groups we actually consume (console, P1, P2) directly from the
`split('#')` iterator via `next()` instead of collecting. `split` still yields
empty groups, so `next()` preserves the empty console slot (`##P1…`) and keeps
P1/P2 from shifting left into it — the behavior is byte-identical for every valid
movie, only the unbounded intermediate allocation is removed. The parse now
touches at most three groups regardless of how many `#` the input contains.

Adds `log_key_bounded_against_pathological_group_padding`, which imports a movie
whose `LogKey` carries 100k trailing `#` delimiters and asserts P1/P2 still map
correctly (the trailing groups are ignored), as the standing regression guard.
This is the `.bk2` *import* path only; the deterministic chip stack and every
golden vector are untouched (AccuracyCoin 141/141 unaffected).

Reported by CodeRabbit as an outside-diff-range finding on #346.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(changelog): correct windowing scope, fold guardrails into the v2.2.9 notes

Three corrections to the [2.2.9] release notes (which release-auto.yml publishes
verbatim, so accuracy and completeness there matter):

- **Honesty fix (CodeRabbit outside-diff-range finding).** The intro note claimed
  detached tool windows use "egui multi-viewport (real OS windows)", which
  contradicted both the detailed "Fixed" entry and AGENTS.md: the frontend is a
  single-viewport `egui_winit` integration, so `show_viewport_immediate` renders a
  detached panel *embedded in the main window*, not as a separate OS window, and
  the Windows-10 "trapped window" report is therefore not yet fully resolved.
  Reword the note to state the embedded scope honestly and point at the v2.3.0
  multi-viewport follow-up, matching the rest of the section.

- **Fold [Unreleased] into [2.2.9].** The provenance-guardrails + reference-firewall
  work (guardrails doc + post-mortem + PDFs, ingestion into AGENTS.md and memory,
  the ref-proj/ removal and citation normalization, the §1 derivation-table audit,
  and the MIT-TriCNES vendoring exception) all ship in v2.2.9, so it belongs in
  the v2.2.9 notes rather than a separate [Unreleased] section the release body
  would omit. Recorded as a new "Added — Provenance & license firewall" subsection.

- **Log the .bk2 import hardening** (the `LogKey` allocation-amplification bound)
  in the same subsection.

Documentation only; markdownlint + fmt clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants