Skip to content

chore(deps): every dependency at its newest -- egui 0.36 + wgpu 30, rcheevos 12.5.0, Gradle 9.8.0 - #570

Merged
doublegate merged 7 commits into
mainfrom
chore/dependency-refresh
Sep 28, 2026
Merged

doublegate merged 7 commits into
mainfrom
chore/dependency-refresh

Conversation

@doublegate

@doublegate doublegate commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Every dependency brought to its newest release, major and minor, with the changes the project needs to accommodate them. Supersedes Dependabot #564, #566, #567, #568 and #569.

Commit What
9fe28af8 The five Dependabot updates, as Dependabot wrote them (Gradle wrapper and distribution checksums verified against Gradle's own; gradlew.bat kept at this repo's LF line endings)
49efc183 egui 0.35 -> 0.36 + wgpu 29 -> 30 + naga 29 -> 30, every other Rust dependency, the Trunk wasm-bindgen pin, and the two trailing Action pins
98dbb18d rcheevos 12.3.0 -> 12.5.0 (vendored RetroAchievements C runtime) and the CHANGELOG entry

The one thing to review closely: a vendored egui-winit

egui 0.36 requires wgpu 30, and egui-winit 0.36.2 (the newest release) does not compile for wasm32 (E0407 on DroppedFile::bytes, re-measured today). Upstream fixed it on main but has not released it. Rather than a git-rev patch, which deny.toml forbids, vendor/egui-winit/ carries the crates.io 0.36.2 package with a three-site cfg(not(target_arch = "wasm32")) guard, wired through [patch.crates-io] the same way vendor/rust-libretro-sys is. vendor/egui-winit/VENDORED.md records the fix and how to remove it once a fixed release ships.

This reverses the 2026-09-18 decision to wait for that release, per the instruction to take every major bump.

Accommodations

  • wgpu 30: presenting moved to Queue::present, color_space: Auto (unchanged picture), apply_limit_buckets: false (unchanged limits), and a fallible get_mapped_range. Applied in the frontend, Android and iOS.
  • egui 0.36: several ordered texture updates can now arrive per frame, and all of them are applied in order. A texture delta dropped unapplied now asserts in debug builds; the one path that could do that (a detached window closing between building and drawing its frame) now discards it explicitly.
  • rcheevos 12.5.0: rc_client_user_t grew a time_t. The ABI size guard failed on it first, and the Rust mirror now carries the field.
  • bincode removed, not bumped: it was unused, and 3.0.0 is a compile_error! tombstone.
  • deny.toml: the CC0-1.0 allowance is dropped. Its only user (hexf-parse, via naga 29) is gone.

Deliberately not moved

  • The Rust toolchain stays at 1.96.0. rust-toolchain.toml documents why: 1.97+ breaks the four Apple libretro buildbot jobs until their before_script is changed. The toolchain is not a crate, and moving it is a separate decision.
  • The getrandom 0.2/0.3 dep: aliases exist to enable the wasm backend of the versions rand_core/ahash resolve. Moving them would disconnect the script-wasm build from its RNG.
  • Glance stays on its newer 1.3.0-alpha02 line, above the latest stable.

Verification

fmt; clippy for the workspace and every frontend feature set; wasm32 clippy (default, wasm-canvas, script-wasm); rustdoc -D warnings; the no_std build; cosim and fuzz checks; cargo deny all ok; cargo ndk arm64 check; Android :app:testFossDebugUnitTest passes; trunk build --release succeeds; --features test-roms 2,869 passed / 0 failed; AccuracyCoin 144/144.

Not verified: rustynes-ios's gfx_metal.rs (needs the Apple SDK), and no frame presented on a display from this build. The present path changed, so a p99 frame-pacing capture on wgpu 30 is owed.

🤖 Generated with Claude Code

https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of graphics updates when detached windows close or rendering encounters an error, helping keep texture resources in sync.
    • Improved resilience of graphics timing measurements when data is unavailable.
  • Maintenance
    • Updated graphics, Android, iOS, and other supporting components to newer compatible versions.

doublegate and others added 3 commits September 28, 2026 18:22
The contents of Dependabot PRs #564, #566, #567, #568 and #569, taken whole from
their branches (none of their files had changed on main since each branch was
cut), so they land together with the rest of the dependency refresh:

- #564 taiki-e/install-action 2.87.15 -> 2.87.20 (security.yml)
- #566 androidx.core:core-ktx 1.19.0 -> 1.19.1
- #567 Gradle wrapper 9.7.1 -> 9.8.0. The wrapper jar is a binary executed on
  every build, so it was checked before use: sha256 238e777f... equals
  services.gradle.org/distributions/gradle-9.8.0-wrapper.jar.sha256, and the
  distributionSha256Sum bafd5ce9... equals gradle-9.8.0-bin.zip.sha256.
- #568 thiserror 2.0.20 -> 2.0.21 (rustynes-cosim lockfile)
- #569 thiserror 2.0.21, cc 1.5.1, uniffi 0.32.2 (workspace lockfile)

gradlew.bat keeps this repository's LF line endings (the mixed-line-ending
pre-commit hook, --fix=lf); Dependabot's copy used Gradle's CRLF, so the file's
content is Gradle's 9.8.0 script with LF line ends, as 9.7.1's was.

Verified here: :app:testFossDebugUnitTest BUILD SUCCESSFUL on Gradle 9.8.0.
The following commits bump everything else.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
… its newest

Maintainer instruction (2026-09-28): every dependency bump, major and minor,
in one PR, with whatever the project needs to accommodate them. This commit is
the Rust and GitHub Actions half; rcheevos follows.

## egui 0.35 -> 0.36, wgpu 29 -> 30, naga 29 -> 30 -- and why this is vendored

These move together: egui-wgpu 0.36 depends on wgpu 30, and a graph holding
wgpu 29 and 30 at once fails every device/queue handoff. The move had been
held since 2026-09-18 on one upstream fact, re-measured today by compiling a
throwaway crate with this project's egui-winit feature set:

    egui-winit 0.36.2 (newest release) for wasm32-unknown-unknown
    error[E0407]: method `bytes` is not a member of trait `egui::DroppedFile`

RustyNES ships a web demo, so that is a hard stop. Upstream fixed it on main
(PR #8516) without a release. The git-rev patch was refused before because
deny.toml denies git sources; instead vendor/egui-winit/ carries the crates.io
0.36.2 package plus a three-site `cfg(not(target_arch = "wasm32"))` guard
(the `dropped_file` module, its `use`, and the one construction site), wired
through [patch.crates-io] exactly as vendor/rust-libretro-sys already is, and
excluded from the workspace. License texts (MIT OR Apache-2.0) are copied from
upstream at tag 0.36.2 because the package ships none; NOTICE credits it.
VENDORED.md records the fix and the removal steps. Proven: the probe crate,
pointed at the vendored path, compiles for wasm32 (verified rebuilt from the
path, not cached) and natively.

## The API changes, applied fresh on main

The blocked branch chore/egui-0.36-wgpu-30-blocked (left untouched) recorded
five deltas; each was re-derived against the 0.36.2 / 30.0.1 sources:

- `SurfaceTexture::present()` -> `Queue::present(texture)`: frontend gfx.rs
  (3 sites -- the third is behind `hd-pack`, which the default build does not
  compile), detached.rs, Android (1), iOS (2).
- `RequestAdapterOptions.apply_limit_buckets = false`, wgpu's Default: the
  bucketing is a fingerprinting mitigation that would round real limits down.
- `SurfaceConfiguration.color_space = SurfaceColorSpace::Auto`, the documented
  default that reproduces wgpu's historical choice, so the picture is unchanged.
- `get_mapped_range()` returns Result: the GPU-timing callback skips a sample
  on failure instead of panicking for telemetry.
- `TexturesDelta.set` is `HashMap<TextureId, SmallVec<[ImageDelta; 1]>>` (one id
  can carry several ORDERED deltas per frame -- all applied, in order) and
  `free` is a HashSet. Two helpers in debugger/mod.rs, `upload_texture_sets`
  and `take_texture_frees`, serve every site.

One change the branch did not need: TexturesDelta now implements Drop, and
that Drop `debug_assert!`s the delta is EMPTY. A frame whose texture updates
are dropped unapplied now panics in debug builds -- it was always a silent bug
(egui never resends them). The main-window path already uploaded before the
swapchain acquire (v2.7.3 DESK-01) and moves the delta out, so a skipped paint
drops an empty delta. The detached-window path had one early return -- the
window closed between phase 1 and phase 2 -- that dropped the delta whole; it
now clears it explicitly, since no renderer remains to receive it.

## Everything else

- bitflags 2.13, realfft 3.5, libc 0.2.189, wasm-bindgen 0.2.129,
  wasm-bindgen-futures 0.4.79, js-sys/web-sys 0.3.106, plus every compatible
  lockfile update (workspace, rustynes-cosim, fuzz).
- web/Trunk.toml's wasm-bindgen CLI pin 0.2.128 -> 0.2.129. It MUST equal the
  library in Cargo.lock; a mismatch fails the Pages deploy while wasm clippy
  still passes. `trunk build --release` run: success.
- bincode REMOVED, not bumped: nothing in the workspace uses it (only comments
  mention it), and bincode 3.0.0's entire source is
  `compile_error!("https://xkcd.com/2347/")` -- verified by building it.
- deny.toml drops the CC0-1.0 allowance: its only user was hexf-parse, which
  naga 30 no longer pulls in, and cargo-deny flagged it as unmatched. An unused
  licence allowance only widens the policy.
- Actions: gradle/actions/setup-gradle 6.3.0 -> 6.4.0 (additive: job-summary
  annotations, dependency-graph plugin 1.5.0), taiki-e/install-action 2.87.20 ->
  2.87.21. Every major tag was already current; the two SHA pins
  (actions/checkout v7.0.1, dtolnay/rust-toolchain master) match upstream.
- NOT moved, by design: the getrandom 0.2/0.3 `dep:` aliases exist only to turn
  on the wasm backend of the versions rand_core 0.6 and ahash still resolve;
  bumping them to 0.4 would add an unused third getrandom and cut the
  script-wasm build off from its RNG. Android dependencies were all at latest
  stable already (Glance stays on its newer 1.3.0-alpha02 line).

## Verified

fmt; clippy workspace + scripting + scripting,hd-pack + retroachievements +
full; wasm32 clippy default, wasm-canvas AND script-wasm; RUSTDOCFLAGS=-D
warnings cargo doc; thumbv7em no_std; rustynes-cosim and fuzz check;
cargo deny advisories/bans/licenses/sources ok; cargo ndk arm64 check of
rustynes-android + rustynes-mobile; Android :app:testFossDebugUnitTest BUILD
SUCCESSFUL; --features test-roms --release 2,869 passed / 0 failed / 20
ignored; AccuracyCoin 144/144.

NOT verified here: rustynes-ios's gfx_metal.rs (cfg(target_os = "ios"); its
build needs the Apple SDK for ring's C). Its three edits mirror Android's,
which compile; the first compile is the v2.9.3 device checklist's step B1.
No frame has been presented from this build on a display: the present path
changed, so a p99 frame-pacing capture on wgpu 30 is owed before any pacing
number is trusted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
… struct

The vendored RetroAchievements C runtime (crates/rustynes-cheevos/vendor/
rcheevos, MIT) moves to the newest release, v12.5.0, as the last library in
this dependency refresh.

What was replaced, and what was not. The vendor directory holds a SUBSET of
upstream (build.rs documents it: no disc/zip/encrypted hashing, no libretro or
RAIntegration glue). Every one of those 50 files exists in 12.5.0, so exactly
those 50 were replaced -- 27 changed. The two new files in the included
directories, a Visual Studio debugger visualizer (.natvis) and
rc_client_raintegration.h, are left out, consistent with the documented
exclusions.

The size guard did its job. ffi::abi_guard::struct_sizes_match_c failed on the
first run: rc_client_user_t is 56 bytes in C and the Rust mirror said 48.
12.4 appended `time_t avatar_last_updated`. The mirror gains the field as
i64, following the convention rc_client_user_game_summary_t already uses
("time_t is 8 bytes on the targets we build"); the guard pins that per target.
Because the guard compares SIZES, a same-size reordering would pass it, so the
header diff was read in full: every other change is additive -- a new games-list
API and code-note editor API (unused here), the same appended field in the
rc_api_user response structs (not mirrored), and a new console id
(RC_CONSOLE_PLAYSTATION_3). No mirrored struct moved a field.

The client identity follows automatically: RA_USER_AGENT's `rcheevos/<version>`
clause is generated from rc_version.h by build.rs; its test passes against
12.5.0. NOTICE, originality-and-provenance.md section 5 and the http.rs example
now say 12.5.0; the dated User-Agent examples in older docs stay as written.

Also here: the CHANGELOG [Unreleased] entry for the whole refresh.

Verified: rustynes-cheevos tests 9/9 (abi_guard included) after the mirror
fix; clippy -p rustynes-frontend --features retroachievements clean; the
workspace test-roms run (2,869 / 0) included this crate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
Copilot AI balanced review requested due to automatic review settings September 28, 2026 22:24
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Repository: doublegate/RustyNES/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: f5eb0ad5-c000-4469-8c12-12f496f9ef4b

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: doublegate/RustyNES/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: e5babaac-3793-433a-ac8e-dbdea0436437

📥 Commits

Reviewing files that changed from the base of the PR and between 4bbc3b3 and 98dbb18.

⛔ Files ignored due to path filters (40)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
  • android/gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar
  • crates/rustynes-cheevos/vendor/rcheevos/include/rc_api_editor.h is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/include/rc_api_user.h is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/include/rc_client.h is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/include/rc_consoles.h is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rapi/rc_api_common.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rapi/rc_api_common.h is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rapi/rc_api_editor.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rapi/rc_api_info.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rapi/rc_api_runtime.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rapi/rc_api_user.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rc_client.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rc_compat.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rc_compat.h is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rc_util.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rc_version.h is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rcheevos/condition.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rcheevos/condset.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rcheevos/consoleinfo.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rcheevos/memref.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rcheevos/operand.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rcheevos/rc_internal.h is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rcheevos/rc_validate.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rcheevos/richpresence.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rcheevos/value.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rhash/hash.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rhash/hash_rom.c is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cheevos/vendor/rcheevos/src/rhash/rc_hash_internal.h is excluded by !crates/rustynes-cheevos/vendor/**
  • crates/rustynes-cosim/Cargo.lock is excluded by !**/*.lock
  • vendor/egui-winit/Cargo.toml is excluded by !vendor/**
  • vendor/egui-winit/LICENSE-APACHE is excluded by !vendor/**
  • vendor/egui-winit/LICENSE-MIT is excluded by !vendor/**
  • vendor/egui-winit/README.md is excluded by !vendor/**
  • vendor/egui-winit/VENDORED.md is excluded by !vendor/**
  • vendor/egui-winit/src/clipboard.rs is excluded by !vendor/**
  • vendor/egui-winit/src/dropped_file.rs is excluded by !vendor/**
  • vendor/egui-winit/src/lib.rs is excluded by !vendor/**
  • vendor/egui-winit/src/safe_area.rs is excluded by !vendor/**
  • vendor/egui-winit/src/window_settings.rs is excluded by !vendor/**
📒 Files selected for processing (25)
  • .github/workflows/android.yml
  • .github/workflows/security.yml
  • CHANGELOG.md
  • Cargo.toml
  • NOTICE
  • android/app/build.gradle.kts
  • android/gradle/wrapper/gradle-wrapper.properties
  • android/gradlew
  • android/gradlew.bat
  • crates/rustynes-android/Cargo.toml
  • crates/rustynes-android/src/gfx.rs
  • crates/rustynes-apu/Cargo.toml
  • crates/rustynes-cheevos/src/ffi.rs
  • crates/rustynes-cheevos/src/http.rs
  • crates/rustynes-frontend/Cargo.toml
  • crates/rustynes-frontend/src/debugger/mod.rs
  • crates/rustynes-frontend/src/detached.rs
  • crates/rustynes-frontend/src/gfx.rs
  • crates/rustynes-frontend/web/Trunk.toml
  • crates/rustynes-ios/Cargo.toml
  • crates/rustynes-ios/src/gfx_metal.rs
  • crates/rustynes-libretro/Cargo.toml
  • deny.toml
  • docs/agents/dependencies.md
  • docs/originality-and-provenance.md
💤 Files with no reviewable changes (1)
  • deny.toml

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The pull request updates Rust and Android dependencies, moves the graphics stack to egui 0.36 and wgpu 30, and changes graphics configuration and texture-delta handling. It also refreshes the Android Gradle wrapper and updates rcheevos ABI and version records.

Changes

Dependency refresh and graphics updates

Layer / File(s) Summary
Workspace dependencies and egui wasm patch
Cargo.toml, crates/rustynes-*/Cargo.toml, crates/rustynes-frontend/web/Trunk.toml, deny.toml, CHANGELOG.md, NOTICE, docs/agents/dependencies.md
Workspace and crate dependencies are updated, including egui 0.36 and wgpu 30. Cargo patches egui-winit to a vendored copy with a wasm32 fix. Dependency notes and license records are updated.
wgpu 30 platform rendering
crates/rustynes-android/Cargo.toml, crates/rustynes-android/src/gfx.rs, crates/rustynes-frontend/src/gfx.rs, crates/rustynes-ios/Cargo.toml, crates/rustynes-ios/src/gfx_metal.rs
The Android, desktop, and iOS renderers set adapter limit bucketing and surface color space explicitly. Their frame paths present through the queue. GPU-timing readback skips a sample when mapped-range lookup fails.
Texture delta processing
crates/rustynes-frontend/src/debugger/mod.rs, crates/rustynes-frontend/src/detached.rs
Debugger rendering uploads ordered texture updates and takes texture frees from deltas. Detached-window presentation clears pending deltas when the window is closed and handles extracted frees on its exit paths.
Android Gradle toolchain and CI
.github/workflows/android.yml, .github/workflows/security.yml, android/app/build.gradle.kts, android/gradle/wrapper/gradle-wrapper.properties, android/gradlew, android/gradlew.bat
The Android Gradle wrapper moves to 9.8.0, AndroidX Core KTX moves to 1.19.1, and CI action versions are updated. The wrapper scripts change Java launch and exit handling.
rcheevos ABI and version records
crates/rustynes-cheevos/src/ffi.rs, crates/rustynes-cheevos/src/http.rs, NOTICE, docs/originality-and-provenance.md
rc_client_user_t adds the avatar_last_updated field. Version references change to rcheevos 12.5.0.

Priority: ⬇️ Low

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

Change: Other

Sequence Diagram(s)

sequenceDiagram
  participant Delta as Prepared texture delta
  participant Present as DetachedManager::present
  participant Renderer as egui_wgpu::Renderer
  participant Queue as wgpu::Queue
  Delta->>Present: Provide prepared texture updates
  alt Window is closed
    Present->>Delta: Clear pending texture delta
  else Window is open
    Present->>Renderer: Upload texture updates before surface acquisition
    Present->>Renderer: Free extracted texture IDs on exit paths
    Present->>Queue: Present rendered frame
  end
Loading

Merge Risk: ⚪ Minimal · up to 98dbb

This dependency refresh adapts rendering to wgpu 30 and egui 0.36 and updates the Android build tooling. No merge-blocking issue was identified. iOS graphics and on-display frame pacing were not verified, as the author also notes.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 98dbb

The coordinated desktop, web, and mobile rendering migration merits design review. The inspected paths show no demonstrated new security boundary bypass, but the vendored web dependency and failure recovery are not fully verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — Effective reachability includes frontend web and native rendering plus Android and iOS presentation adaptations. The inspected changes do not establish new tenant, credential, or service authority; dependency and vendored-source effects remain less completely covered.

Trust Boundaries and Controls

  • inferred — The inspected renderer callers continue to pass application-owned framebuffers and debugger overlays. No newly exposed route from external content to GPU command-encoding authority was established.

Resilience and Maintainability Implications

  • observed — The detached presenter accounts for texture ownership on normal frames, surface-acquisition failures, and closure before presentation. Asynchronous device-loss recovery is not established by these explicit branches.

Hardening Proposals

  • proposed — Before web rollout, verify the vendored egui-winit changes against the upstream package and retain a path to remove the override when a fixed release is available.
🚥 Pre-merge checks | ✅ 7 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.22% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 8 files. (16 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Safety Comment On New Unsafe Blocks ⚠️ Warning The PR adds unsafe blocks in the new vendored Rust files without the required adjacent // SAFETY: comments. vendor/egui-winit/src/clipboard.rs:184 has only #[expect(unsafe_code)]. `vendor/egui-w… Add an adjacent // SAFETY: comment before unsafe { smithay_clipboard::Clipboard::new(...) } that states why the Wayland display pointer is valid. Add an adjacent // SAFETY: comment before the outer unsafe block in `vendor/egui-winit/s…
✅ Passed checks (7 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main dependency refresh and names the primary upgrades, including egui, wgpu, rcheevos, and Gradle.
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 PASS: The only PR change under the four checked chip crates is crates/rustynes-apu/Cargo.toml, where the dev-only realfft dependency changes from 3.4 to 3.5. The diff contains no CPU, PPU, APU, or…
Changelog Entry For User-Visible Changes ✅ Passed CHANGELOG.md gains a new ### Dependencies entry directly under [Unreleased]. It records the egui texture-update behavior, detached-window texture discard handling, wgpu presentation changes, and t…
No Unwrap/Expect/Panic On Untrusted Input ✅ Passed No new exact .unwrap(), .expect(), or panic!() invocation is introduced by the PR. The diff changes the GPU timing path to handle get_mapped_range() with .ok(), while its existing fixed-buff…
Full details: Docstring Coverage

Explanation

Docstring coverage is 72.22% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 8 files. (16 skipped: 16 unsupported.)

Full details: Safety Comment On New Unsafe Blocks

Explanation

The PR adds unsafe blocks in the new vendored Rust files without the required adjacent // SAFETY: comments. vendor/egui-winit/src/clipboard.rs:184 has only #[expect(unsafe_code)]. vendor/egui-winit/src/safe_area.rs:29 has no SAFETY comment adjacent to the outer block; the comment at line 39 documents only the nested cast_unchecked operation. The Android, iOS, and existing ABI-check unsafe blocks are pre-existing or already documented, so the failure is caused by the newly added vendor code.

Resolution

Add an adjacent // SAFETY: comment before unsafe { smithay_clipboard::Clipboard::new(...) } that states why the Wayland display pointer is valid. Add an adjacent // SAFETY: comment before the outer unsafe block in vendor/egui-winit/src/safe_area.rs that states the main-thread and Objective-C type invariants. Keep the nested cast comment if it remains useful.

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

@doublegate

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@context7

context7 Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Docs7 for doublegate/rustynes

Result Status Action
Deployment ➖ Not used —
Content review ➖ Did not run. This site has no agent runs available this month. Wait for the monthly reset or check your Docs7 plan. —

Commit dce3f05

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@socket-security

socket-security Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR updates core graphics, UI, and build dependencies to their newest versions, resolving breaking API changes in wgpu 30 and egui 0.36 while patching rcheevos bindings for 32-bit ABI compatibility.

Blocking issues

None found.

Suggestions

  • crates/rustynes-cheevos/vendor/rcheevos: Changing time fields (unlock_time, beaten_time, completed_time) from i64 to libc::time_t fixes the immediate ABI layout mismatch, but introduces a Y2K38 vulnerability on 32-bit platforms (like armeabi-v7a) where libc::time_t is an i32. Verify if the C side of rcheevos uses a fixed 64-bit integer or a platform-dependent time_t, and ensure this won't overflow in 2038.
  • crates/rustynes-frontend/src/detached.rs: When self.windows.get_mut(&id) returns None in upload_detached_textures, clearing the TexturesDelta correctly prevents the Drop panic, but silently swallows the UI state change. Log a warning here to surface window-management desyncs.
  • vendor/egui-winit: The local patch to disable the dropped_file module on wasm32 to bypass the E0407 error should include a comment linking to an upstream egui issue or PR so the vendored code doesn't become permanently orphaned.

Nitpicks

  • The std::mem::take workaround to safely extract values from TexturesDelta before it drops is used repeatedly; consider moving upload_texture_sets and take_texture_frees into a local extension trait to keep the UI extraction code clean.

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

Earlier review rounds (newest first)
Round reviewed at 2026-09-28 23:17 UTC

Antigravity review (Gemini via Ultra)

  1. Updates workspace dependencies (egui, wgpu, rcheevos, Gradle) to their newest releases, including a vendored workaround to unblock the WASM build on egui-winit 0.36.2.

Blocking issues

None found.

Suggestions

  • vendor/egui-winit/VENDORED.md: Consider adding a direct link to the upstream fix (emilk/egui#8516) so the release that unblocks deleting this vendored copy can be easily tracked.
  • crates/rustynes-frontend/src/gfx.rs (around line 262): Discarding the get_mapped_range() error via .ok() is justified by the telemetry comment, but consider logging it (e.g., log::trace!) to aid in diagnosing missing timing samples.

Nitpicks

  • The Cargo.toml patch section comment correctly explains why egui-winit is vendored, but could briefly reference the upstream PR number for context.

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

Round reviewed at 2026-09-28 22:43 UTC

Antigravity review (Gemini via Ultra)

Updates UI, graphics, and achievement backend dependencies to their latest major versions, adapting API calls and vendoring egui-winit to maintain WASM compatibility.

Blocking issues

  • FFI struct layout mismatch: In crates/rustynes-cheevos/src/ffi.rs, avatar_last_updated is hardcoded as i64. The underlying C type time_t is not universally 64-bit (it remains 32-bit on wasm32-unknown-unknown and 32-bit ARM targets like armv7-linux-androideabi). Hardcoding i64 breaks the struct layout on these platforms, leading to memory corruption during FFI calls. Use libc::time_t or the appropriate target-specific type instead.

Suggestions

  • Vendoring documentation: The patched egui-winit crate in vendor/egui-winit/ should include a brief README or comment documenting the cfg(not(target_arch = "wasm32")) patch for NativeFile and an upstream issue link so the context isn't lost during the next bump.
  • wgpu limits: Setting apply_limit_buckets: false in SurfaceConfiguration makes sense for native, but double-check if strict browsers might flag or block the WebGL/WebGPU context in the WASM build without fingerprinting mitigations.

Nitpicks

  • None found.

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

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.

Copilot review overview

🔵 Needs a closer look

The broad vendored C refresh and cross-platform presentation migration include unverified iOS and on-display execution paths.

Review effort: Balanced
Findings: 5 Low severity

Open (5)
What changed in this PR

Refreshes Rust, Android, UI/GPU, and RetroAchievements dependencies while adapting platform integrations and preserving wasm compatibility.

Changes:

  • Upgrades egui/wgpu/naga, Gradle, UniFFI, rcheevos, and supporting dependencies.
  • Migrates presentation and texture-delta handling to new APIs.
  • Vendors a narrowly patched egui-winit 0.36.2 for wasm support.
File Description
.github/​workflows/​android.yml Updates Android CI tooling.
.github/​workflows/​security.yml Updates security action pin.
CHANGELOG.md Records dependency refresh.
Cargo.lock Resolves updated Rust graph.
Cargo.toml Updates workspace dependencies and patch.
NOTICE Adds vendored egui attribution.
android/​app/​build.gradle.kts Updates Android dependency.
android/​gradle/​wrapper/​gradle-wrapper.jar Updates Gradle wrapper binary.
android/​gradle/​wrapper/​gradle-wrapper.properties Selects Gradle 9.8.0.
android/​gradlew Updates Unix wrapper.
android/​gradlew.bat Updates Windows wrapper.
crates/​rustynes-android/​Cargo.toml Updates Android graphics dependencies.
crates/​rustynes-android/​src/​gfx.rs Migrates wgpu presentation.
crates/​rustynes-apu/​Cargo.toml Removes unused bincode dependency.
crates/​rustynes-cheevos/​src/​ffi.rs Mirrors updated rcheevos ABI.
crates/​rustynes-cheevos/​src/​http.rs Updates version expectations.
crates/​rustynes-cheevos/​vendor/​rcheevos/​include/​rc_api_editor.h Updates editor API declarations.
crates/​rustynes-cheevos/​vendor/​rcheevos/​include/​rc_api_user.h Updates user API declarations.
crates/​rustynes-cheevos/​vendor/​rcheevos/​include/​rc_client.h Updates client ABI.
crates/​rustynes-cheevos/​vendor/​rcheevos/​include/​rc_consoles.h Updates console definitions.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rapi/​rc_api_common.c Updates shared API parsing.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rapi/​rc_api_common.h Updates internal API declarations.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rapi/​rc_api_editor.c Updates editor API implementation.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rapi/​rc_api_info.c Updates information API parsing.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rapi/​rc_api_runtime.c Updates runtime API parsing.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rapi/​rc_api_user.c Updates user API parsing.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rc_client.c Updates rcheevos client runtime.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rc_compat.c Updates compatibility helpers.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rc_compat.h Updates compatibility declarations.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rc_util.c Updates runtime utilities.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rc_version.h Sets rcheevos 12.5.0.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rcheevos/​condition.c Updates condition handling.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rcheevos/​condset.c Updates condition-set handling.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rcheevos/​consoleinfo.c Updates console metadata.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rcheevos/​memref.c Updates memory-reference handling.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rcheevos/​operand.c Updates operand evaluation.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rcheevos/​rc_internal.h Updates internal declarations.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rcheevos/​rc_validate.c Updates validation logic.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rcheevos/​richpresence.c Updates rich-presence parsing.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rcheevos/​value.c Updates value evaluation.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rhash/​hash.c Updates hashing support.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rhash/​hash_rom.c Updates ROM hashing.
crates/​rustynes-cheevos/​vendor/​rcheevos/​src/​rhash/​rc_hash_internal.h Updates hash internals.
crates/​rustynes-cosim/​Cargo.lock Resolves cosim dependency updates.
crates/​rustynes-frontend/​Cargo.toml Updates frontend dependencies.
crates/​rustynes-frontend/​src/​debugger/​mod.rs Handles ordered texture deltas.
crates/​rustynes-frontend/​src/​detached.rs Updates detached-window rendering.
crates/​rustynes-frontend/​src/​gfx.rs Migrates desktop wgpu APIs.
crates/​rustynes-frontend/​web/​Trunk.toml Synchronizes wasm-bindgen CLI.
crates/​rustynes-ios/​Cargo.toml Updates iOS graphics dependency.
crates/​rustynes-ios/​src/​gfx_metal.rs Migrates Metal presentation.
crates/​rustynes-libretro/​Cargo.toml Updates libc dependency.
deny.toml Removes obsolete license allowance.
docs/​agents/​dependencies.md Documents the temporary vendor patch.
docs/​originality-and-provenance.md Records vendored egui provenance.
vendor/​egui-winit/​Cargo.toml Adds vendored crate metadata.
vendor/​egui-winit/​LICENSE-APACHE Adds Apache license.
vendor/​egui-winit/​LICENSE-MIT Adds MIT license.
vendor/​egui-winit/​README.md Adds upstream README.
vendor/​egui-winit/​VENDORED.md Documents patch and removal process.
vendor/​egui-winit/​src/​clipboard.rs Vendors clipboard integration.
vendor/​egui-winit/​src/​dropped_file.rs Vendors native file handling.
vendor/​egui-winit/​src/​lib.rs Applies wasm-specific guards.
vendor/​egui-winit/​src/​safe_area.rs Vendors safe-area support.
vendor/​egui-winit/​src/​window_settings.rs Vendors window settings.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Cargo.toml
Comment thread android/gradle/wrapper/gradle-wrapper.properties Outdated
Comment thread crates/rustynes-android/Cargo.toml Outdated
Comment thread crates/rustynes-frontend/Cargo.toml Outdated
Comment thread crates/rustynes-ios/Cargo.toml Outdated
doublegate and others added 2 commits September 28, 2026 18:38
…d egui

The version bumps in #570 left comments describing the tree before them.
Copilot flagged five; a grep for the old numbers found four more of the
same kind. None change behaviour, but each would mislead the next person
bumping these crates:

- Cargo.toml: the wgpu comment and the long "HELD AT 0.35" block said egui
  was pinned because egui-winit 0.36 cannot build for wasm. It is no longer
  held; the block now says egui-winit is vendored and states the condition
  for removing the vendored copy (a published egui-winit carrying upstream
  PR #8516).
- rustynes-android / rustynes-ios Cargo.toml: "same wgpu major (29)" -> 30.
- rustynes-frontend Cargo.toml: the naga comment now says naga moves with
  wgpu 30.
- gradle-wrapper.properties: the checksum-source comment named the 9.7.1
  .sha256; it now names gradle-9.8.0-bin.zip.sha256, the file the pinned
  checksum was verified against.
- android/build.gradle.kts: notes the 9.8.0 wrapper move.
- gfx_metal.rs (two comments) and docs/ios.md: wgpu 30 does have a
  CoreAnimationLayer surface target, behind cfg(metal); the UiKit
  window-handle path is kept because it is what the app already hands over
  and it needs no new unsafe. The old text said no such variant existed.

Verified: cargo metadata resolves; fmt clean. Comment-only otherwise.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
agy's review of #570 flagged the new rc_client_user_t.avatar_last_updated
field, declared as i64 for a C time_t. The finding is real, and older than
this PR: unlock_time (rc_client_achievement_t) and beaten_time /
completed_time (rc_client_user_game_summary_t) had used the same
hard-coded i64 since the FFI was written, under a comment claiming time_t
is "8 bytes on the targets we build".

That holds for the default targets (x86_64 / arm64 desktop, arm64 and
x86_64 Android, iOS) and for Windows, where time_t is 64-bit. It fails on
32-bit Android (armeabi-v7a is an opt-in ABI in android/app/build.gradle.kts)
and on 32-bit glibc, where time_t is 32 bits. There a fixed i64 widens the
field, which misplaces every field after it and grows the struct, so a read
through the mirror returns the wrong bytes.

Measured, not argued. The existing abi_guard compares each mirror's
size_of with the C sizeof from the vendored static_asserts.c. Built and run
as i686-unknown-linux-gnu (gcc -m32):

  before: struct_sizes_match_c FAILED, rc_client_achievement_t 84 vs 80
  after:  9 passed, 0 failed (the guard included)

The fix uses libc::time_t (libc 0.2.189, already in the dependency graph
through the frontend and libretro, so no new crate enters the build). It is
a target-only dependency, like ureq, because the crate is empty on wasm32.
Assigning 0 in client.rs is unchanged, since a literal infers either width.

Also verified: cargo test -p rustynes-cheevos (native, 9 passed); clippy
-D warnings on rustynes-cheevos and on the frontend with
retroachievements; cargo ndk -t armeabi-v7a check -p rustynes-cheevos.
The ARMv7 build compiled but was not run: there is no ARMv7 runner here.
i686 is the executed stand-in with the same 32-bit time_t.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
@doublegate

Copy link
Copy Markdown
Owner Author

Replying to the Antigravity review of 98dbb18d:

Blocking, time_t as i64: agreed, fixed in 5aaeb64d. The problem was older than this PR. unlock_time, beaten_time and completed_time had used the same hard-coded i64 from the start. All four fields now use libc::time_t. Checked by running the existing abi_guard as i686-unknown-linux-gnu, which has a 32-bit time_t:

  • before the fix it failed: rc_client_achievement_t was 84 bytes in Rust against 80 in C;
  • after the fix, 9 tests passed.

cargo ndk -t armeabi-v7a check also compiles. One correction to the finding: wasm32 does not apply. rustynes-cheevos is an empty crate on wasm32, and the browser build uses its own path.

Vendoring documentation: already there. vendor/egui-winit/VENDORED.md names the three patched sites and the upstream fix (emilk/egui PR #8516, merged 2026-09-07 but not yet released). It also gives the removal condition. Each patched site in src/lib.rs is marked RustyNES vendored patch.

apply_limit_buckets: no change. It is a field of RequestAdapterOptions, not SurfaceConfiguration. false is wgpu's Default. The flag only controls whether wgpu itself rounds adapter limits down. It does not affect whether a browser creates a WebGL or WebGPU context, and browsers apply their own fingerprinting limits under the page either way. The wasm build has been through trunk build --release and the Pages deploy on this PR, and both succeed.

doublegate and others added 2 commits September 28, 2026 18:40
…eleted

docs/agents/dependencies.md still said `chore/egui-0.36-wgpu-30-blocked`
was the ONLY copy of the egui 0.36 / wgpu 30 migration and that deleting it
would destroy the work. Since #570 landed the migration, that warning is
false. The maintainer asked for the branch to be checked for anything #570
lacked and then deleted; this records the result where the next session
will look.

The comparison, so nobody needs to re-derive it:
- 5 commits ahead of its merge base (v2.3.2 era). The four feature commits
  (per-byte write attribution, per-pixel causal record, the Pixel
  Provenance panel, `rustynes verify`) are on main: their files exist there
  (ppu/src/provenance.rs, debugger/provenance_panel.rs,
  docs/pixel-provenance.md, the cli.rs verify subcommand).
- 69b00e5, the migration itself, touches the same API sites as #570,
  counted by grep in both trees: color_space 4, apply_limit_buckets 3,
  get_mapped_range 2, the same Queue::present sites, and per-id delta lists
  applied in order. Its explanatory comments say the same as #570's.
- #570 does more at one site: in DetachedWindows::present the branch's
  early return for a closed window drops a non-empty TexturesDelta, which
  trips egui 0.36's debug assertion. #570 clears it first.
- Its version pins (egui 0.36, wgpu/naga 30) and Action bumps are
  superseded by the newer ones in #570.

The branch was deleted locally and on GitHub; its tip was 69b00e5,
recorded in the doc in case it has to be recovered from the reflog or a
clone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
…mpiles

CodeRabbit's "Safety comment on new unsafe blocks" check flagged
vendor/egui-winit/src/clipboard.rs:184 and safe_area.rs:29, which have no
adjacent `// SAFETY:` comment. Both are upstream's code. VENDORED.md
promises that every file apart from the three marked wasm32 sites is
byte-for-byte the crates.io 0.36.2 package, and that promise is what makes
the eventual removal a simple delete. Annotating upstream's code would
break it, so the answer goes in VENDORED.md instead.

Checked rather than assumed (`cargo tree -p rustynes-frontend -e features
-i egui-winit`: bytemuck, links, wayland, webbrowser, x11):
- the smithay clipboard block exists only with the `clipboard` feature,
  which is not enabled (the dependency uses default-features = false);
- safe_area.rs is cfg(target_os = "ios"), and the iOS app is SwiftUI over
  rustynes-ios. It does not use egui-winit.

So neither block is in any binary this project ships. The note says to
review them if that changes, e.g. if `clipboard` is enabled. It makes no
claim that upstream's blocks are sound.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
@doublegate

Copy link
Copy Markdown
Owner Author

@coderabbitai, on the two pre-merge warnings:

Safety comments in vendor/egui-winit/: answered in VENDORED.md, not in the vendored source (dce3f054, pushed with the next batch). Both blocks are upstream's, and that directory promises the package is byte-for-byte crates.io 0.36.2 apart from three marked wasm32 sites. Neither block is compiled into a RustyNES build, per cargo tree -e features -i egui-winit:

  • the smithay clipboard block needs the clipboard feature, which RustyNES does not enable;
  • safe_area.rs is cfg(target_os = "ios"), and the iOS app does not use egui-winit.

Docstring coverage: no change. The two functions this PR adds, upload_texture_sets and take_texture_frees, have full doc comments. The rest of the 18 are existing functions where this PR changed a single call to the wgpu 30 or egui 0.36 API. Documenting those is outside a dependency PR.

Also since your review: the time_t mirror fix (5aaeb64d, from the Antigravity review, checked red then green on i686) and the stale version comments Copilot found (6f3b3645).

@coderabbitai

coderabbitai Bot commented Sep 28, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd -i 'VENDORED.md|safe_area.rs|lib.rs|mod.rs|Cargo.toml' vendor crates/rustynes-frontend | head -100
printf '\n--- VENDORED.md ---\n'
cat vendor/egui-winit/VENDORED.md
printf '\n--- frontend function map ---\n'
ast-grep outline crates/rustynes-frontend/src/debugger/mod.rs --match 'upload_texture_sets|take_texture_frees' --view expanded
printf '\n--- relevant declarations and imports ---\n'
rg -n -C5 'upload_texture_sets|take_texture_frees|egui-winit|clipboard|safe_area|RustyNES vendored patch' crates/rustynes-frontend/src/debugger/mod.rs Cargo.toml vendor/egui-winit/Cargo.toml vendor/egui-winit/src/lib.rs vendor/egui-winit/src/safe_area.rs | head -230

Length of output: 1329


🏁 Script executed:

#!/bin/bash
set -uo pipefail
printf '%s\n' '--- checkout ---'
git rev-parse --short HEAD
git status --short --branch
printf '%s\n' '--- vendor files located ---'
fd -i 'VENDORED.md|safe_area.rs' . || true
printf '%s\n' '--- tracked vendored paths ---'
git ls-files '*egui-winit*' | head -60
printf '%s\n' '--- frontend symbol map ---'
ast-grep outline crates/rustynes-frontend/src/debugger/mod.rs --match 'upload_texture_sets|take_texture_frees' --view expanded || true
printf '%s\n' '--- frontend function context ---'
rg -n -B12 -A15 'fn (upload_texture_sets|take_texture_frees)\b' crates/rustynes-frontend/src/debugger/mod.rs || true
printf '%s\n' '--- PR head metadata ---'
gh pr view 570 --repo doublegate/RustyNES --json headRefOid,headRefName --jq '{headRefOid,headRefName}' || true

Length of output: 4151


🏁 Script executed:

#!/bin/bash
set -uo pipefail
printf '%s\n' '--- vendoring document at checked-out head ---'
git show HEAD:vendor/egui-winit/VENDORED.md || true
printf '%s\n' '--- feature and cfg evidence at checked-out head ---'
git show HEAD:vendor/egui-winit/Cargo.toml | rg -n -C3 '^\[features\]|clipboard|smithay|default' | head -110 || true
git show HEAD:vendor/egui-winit/src/safe_area.rs | head -55 || true
rg -n -C3 'egui-winit|features|default-features' crates/rustynes-frontend/Cargo.toml Cargo.toml | head -130 || true
printf '%s\n' '--- cited commit availability ---'
for c in dce3f054 6f3b3645; do git cat-file -t "$c" 2>&1 || true; done

Length of output: 17248


@doublegate, the tracked VENDORED.md is readable through Git even though the checkout did not materialize vendor/. At PR head 5aaeb64d, it documents the three wasm32 patches, but it does not contain the safety-block explanation you attribute to dce3f054. That commit is not available in this checkout. I cannot verify the new explanation, so the safety-comment warning remains unresolved pending a checkout that includes it.

The docstring warning is withdrawn. Both upload_texture_sets and take_texture_frees have full doc comments at the checked-out head.

You are interacting with an AI system.

@doublegate

Copy link
Copy Markdown
Owner Author

Replying to the Antigravity rounds on eb710383 / dce3f054 (no blocking issues). Nothing below changes code.

  • Y2K38 on 32-bit time_t: outside what the mirror controls. The vendored rcheevos declares these fields as the platform's time_t (rc_client.h: time_t unlock_time;, time_t avatar_last_updated; and so on). An FFI mirror has to match C's layout, and a wider Rust field is exactly the corruption 5aaeb64d fixed. On a 32-bit target the C library itself stores a 32-bit value, and only upstream rcheevos or a 64-bit-time_t ABI can change that. The fields are timestamps RetroAchievements displays; nothing here does arithmetic on them.
  • Log when a detached window is gone: declined. DetachedWindows::present finding no window means the user closed it between building the frame and presenting it. That is ordinary, not a desync, and a warning would fire on normal use.
  • Link the upstream fix from the vendored copy: already there. vendor/egui-winit/VENDORED.md cites emilk/egui PR #8516, including its merge date, and says the copy is removed once a release carries the fix.
  • log::trace! on a failed get_mapped_range(): declined. It sits in a per-frame GPU-timing callback, and a skipped sample is already visible as a missing timing sample.
  • Extension trait for the delta helpers: declined. There are two free functions with two call sites each, and a trait would add a type to read without removing a line.

@doublegate
doublegate merged commit d8b02d2 into main Sep 28, 2026
34 checks passed
@doublegate
doublegate deleted the chore/dependency-refresh branch September 28, 2026 23:49
doublegate added a commit that referenced this pull request Sep 29, 2026
#570 moved the renderers from wgpu 29 to 30, changing the present call
(Queue::present) and adding an explicit surface colour space. The frame
pacing on the new path had not been measured; this is that measurement,
taken as an A/B rather than a lone number, because a p99 in isolation
reports the host as much as the code (see the v2.3.3 F5 section).

Method: v2.9.2 (4bbc3b3, wgpu 29, built in a worktree) against 9dd64f2
(wgpu 30), run alternately old/new/old/new for 45 s each with
RUSTYNES_PERF_LOG=1 on the CC0 flowing_palette.nes (perf_capture.sh's
default). Captures started once the one-minute load average was under 2.
The config was identical and read from the CSV headers: run-ahead 0, rewind
on, Mailbox, 119.991 Hz Wayland monitor. All four pass perf_log_check.py
with present_discarded=0.

Result, consistent in both pairs:
  presented p50  5.45 / 5.70 ms  ->  8.36 / 8.35 ms
  presented p95 15.63 / 15.67    -> 11.38 / 10.35
  presented p99 16.93 / 16.94    -> 11.97 / 11.70
  rwait p95     14.68 / 14.47    ->  8.48 /  8.45
  produced max  27.0 / 26.9      -> 19.0 / 19.0
  cost p95       2.99 / 2.98     ->  2.95 / 2.94
Both builds present at the monitor's rate (mean 8.37 ms). Under wgpu 29
those presents came in bunches; under wgpu 30 they land one vblank apart.
Emulation cost is unchanged.

Limits, stated in the doc: one host, one ROM, Mailbox only (not Fifo, 60 Hz
or run-ahead), present timing rather than what reached the eye, and not
attributed to a specific wgpu change, since the present-path move and the
colour-space field landed together. The raw CSVs stay in the session
scratchpad.

This closes the "p99 frame-pacing capture owed on wgpu 30" item carried
since #570.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
doublegate added a commit that referenced this pull request Sep 29, 2026
…ndings fixed (#571)

* docs(plan): v2.9.3 "Handset" -- the device checklist, and what can be proven first

The line plan gives v2.9.3 one subject: the mobile device checklist, run by
the maintainer on real Android and iOS devices. This plan adds what this side
can do around it, each with a measurable gate: one consolidated run sheet and a
checksummed debug APK; the Android rows an emulator can reach, run first on
Pixel_8_API_34 and recorded as EMULATOR, never as a device result; the Android
JVM unit tests in CI (the workflow builds bundles but has never run
testFossDebugUnitTest); the five open Dependabot updates consolidated so the
device run tests the build that ships; and the sibling oracle pin moved to
v2.9.2 with VERIFY=1, because AUD-03 changed oracle behaviour and the
co-simulation has not been compared with it.

Blocked and recorded: the device runs, and every Swift change (no Swift
toolchain on Linux; B1 is its first compile). The release waits for the
maintainer's run sheet. v2.9.2's plan row is marked released.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* ci(android): run the app's JVM unit tests as a gate

Nothing in CI ran android/app/src/test/ until now. The Android workflow
cross-builds the libraries (the link gate) and bundles release AABs in a job
that is continue-on-error by design, so the Kotlin tests -- PersistenceTest,
and v2.9.2's SocdTest, added precisely to pin a mobile behaviour -- had only
ever run on a developer's machine. The v2.9.2 frontend agent found and
reported this; v2.9.3 is the mobile release, so it closes it.

New job kotlin-unit-tests: the bundle job's toolchain (Rust with the two
Android targets, cargo-ndk, Temurin 17, the latest NDK, Gradle), because
preBuild depends on cargoNdkBuild and uniffiBindgen, then
./gradlew :app:testFossDebugUnitTest, uploading the HTML report on failure.
Unlike the bundle job it is NOT continue-on-error: a failing test fails the
workflow. Same scope as the bundle: every main push and dispatch, and a PR
that changes the Android app.

Shown to fail: with SocdTest's first assertion broken locally, the same task
reports "11 tests completed, 1 failed" and BUILD FAILED; restored, it passes.
actionlint clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(plan): v2.9.3 builds on the dependency refresh in #570

The maintainer asked (2026-09-28) for every dependency bump, major and minor,
in its own PR rather than inside v2.9.3, so the plan's item 4 no longer
consolidates the five Dependabot updates here. #570 carries them with
everything else: egui 0.36 + wgpu 30 (vendored egui-winit for the web build),
rcheevos 12.5.0, UniFFI 0.32.2, Gradle 9.8.0. v2.9.3 rebases onto it once it
merges, so the device run exercises the build that ships -- the new wgpu
present path and the regenerated UniFFI bindings above all.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* ci(android): the JVM unit-test job uses setup-gradle v6.4.0 like its siblings

The kotlin-unit-tests job was written before #570 and pinned
gradle/actions/setup-gradle@v6.3.0. #570 moved the workflow's other
setup-gradle step to v6.4.0, so after the rebase onto d8b02d2 one file
carried two pins of the same action. Aligned to v6.4.0, so a later
Dependabot update moves both together. Nothing else in the job changes.

The plan's item 4 now records the merge (d8b02d2) and the rebase.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* ci(ios): type-check the real gfx_metal.rs on the Linux host

#570 moved wgpu from 29 to 30 and changed crates/rustynes-ios/src/gfx_metal.rs
in three places: `Queue::present(frame)` replacing `SurfaceTexture::present`,
`SurfaceConfiguration::color_space`, and
`RequestAdapterOptions::apply_limit_buckets`. The PR recorded that file as
never compiled, because nothing on a pull request builds the iOS target:
`ios.yml` is tag-triggered and needs an Apple SDK for the vendored C (ring).
scripts/ios-host-typecheck.sh, in CI's lint job since v2.7.4, covered
audio.rs and ffi.rs and deliberately stubbed gfx_metal as "genuinely needs
iOS".

That turned out to be true only at runtime. gfx_metal.rs imports nothing
iOS-specific: wgpu, raw-window-handle (whose UiKit handle types exist on
every platform), bytemuck, pollster and rustynes-gfx-shaders. The script
therefore generates a second throwaway crate that includes the real file
via #[path] and runs `cargo check` on it. wgpu's version is read from the
iOS crate's Cargo.toml, exactly as cpal's already was, and the workspace
Cargo.lock is copied in so the check resolves the versions the iOS build
uses (wgpu 30.0.1 on both sides, confirmed from the two lockfiles).

Evidence the check can fail: in a scratch copy of the file, putting back
the wgpu 29 call (`frame.present()` for `self.queue.present(frame)`) gives

    error[E0599]: no method named `present` found for struct
    `wgpu::SurfaceTexture` in the current scope

The real file checks clean, so #570's gfx_metal.rs edits now have a
compiler. What this does NOT show: creating a Metal surface, presenting a
frame, or anything else at runtime. Those remain on the device checklist.

The script runs end to end locally (the existing 16 audio/FFI tests plus
the new check). docs/ios.md and a CHANGELOG [Unreleased] "Testing" entry
record it. The entry also covers the Kotlin unit-test job added earlier on
this branch, which had no CHANGELOG line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(mobile): the v2.9.3 run sheet, with an emulator pre-run of the Android rows

v2.9.3's subject is the maintainer's device run of the mobile checklist.
This adds docs/mobile-v2.9.3-run-sheet.md: one sheet carrying every v2.7.4
and v2.9.2 row unchanged (A1-A15, I1-I19), five rows for what #570 changed
on mobile (D1-D5: the wgpu 30 present path on each platform, rcheevos
12.5.0 login, and the 32-bit Android struct layout the time_t fix touched),
the exact build to install, and an Emulator column filled in before any
device time is spent.

The build: RustyNES-v2.9.3-pre-foss-debug.apk from 2ed51d3 (main at
d8b02d2 plus this branch), SHA-256 abb1fb4b...ffdfbb, 67,310,419 bytes,
arm64-v8a + x86_64. The sheet says to rebuild and re-record the checksum if
the app, core or bridge changes before the run.

The emulator pre-run (Pixel_8_API_34, Android 14, software GPU), driven over
adb with input events, the app's own deep links and screenshots:
- A1 PASS: a ROM that increments $6000 on each power-on read 1, 2, 3 in
  battery/<sha>.sav across two force-stops.
- A3 PASS with a corrected step (below); A4, A6, A8, A10 PASS; A7 PASS for
  rotate, sleep/wake and PiP-return on the GPU renderer (13 captures, the
  picture in every one); D1 covered by A7 (Vulkan device created).
- A8 in numbers: paused on the GPU renderer 0.6-1.0% CPU over 30 s, against
  about 148% running.
- A2, A5, A9, A11-A15, D3, D5: NOT RUN, each with the reason.

A checklist correction found by running it: A3 says Home stops the sound at
once, but MainActivity.onUserLeaveHint enters picture-in-picture when a game
is running and unpaused, and the game keeps playing there. That is row A6's
expected result, so A3 as written fails on any PiP-capable device through
no fault of the app. What A3 is for (AND-03: audio stops once the app is
really backgrounded) was tested with the screen turned off: the AudioTrack
went from `started` to `paused` within 1 s and resumed on wake. The sheet
states the corrected step, and the v2.7.4 checklist now points to it.

Not a defect: StrictMode flags main-thread disk access at
MainActivity.kt:1859. That is the BuildConfig.DEBUG-only autoload.nes check,
which release builds never run.

A procedural trap, recorded in the sheet: the pad menu's first button,
"Close", closes the GAME. One A4 attempt tapped it expecting to close the
menu, unloaded the ROM, and was repeated. Logcat showed no activity
recreation (one instance throughout), which is how the cause was pinned to
the tap and not to the app.

scripts/mobile-battery-counter-rom.py is the A1 ROM's generator, written
from nesdev's iNES/MMC1 pages (MMC1, battery bit, PRG-RAM enabled
explicitly). Its output is byte-identical to the ROM used on the emulator,
and the host core counts 1, 2, 3 across three boots with SRAM carried
between them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* ci(android): run the Kotlin unit tests when the bridge or this workflow changes

Copilot on #571: the kotlin-unit-tests job, added in this branch, was gated
on the cross-build job's `app` output, which the paths filter sets only for
`android/**`. #571 changes the workflow and not the app, so the job it adds
was skipped on its own PR: the gate had never run in CI. The mutation test
recorded when it was added (11 tests, 1 failed) ran locally, not through
the workflow wiring.

The job now reads a separate `kotlin` output that also covers:
- crates/rustynes-mobile/**, because the Kotlin tests compile against the
  UniFFI bindings generated from that crate, so a bridge change can break
  them without touching android/; and
- .github/workflows/android.yml, so a change to the gate exercises the gate.

`app` is unchanged. It still drives the ~15-minute bundle job, whose own
comment explains why that job runs on a PR only for app changes.

Verified: actionlint (pre-commit) clean. The proof that it now runs is this
push: the job should appear as run, not skipped, on #571's next CI.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* build(android): opt into ABIs with -PrustynesAbis; an armeabi-v7a build for D5

Copilot on #571: run-sheet row D5 (RetroAchievements on 32-bit Android,
the platform whose struct layout the time_t fix in 5aaeb64 changed) could
not be run with the documented APK. That APK carries only arm64-v8a and
x86_64, so a 32-bit-only device cannot install it, and a 64-bit device runs
the 64-bit library and never exercises a 32-bit time_t. The build comment
called armeabi-v7a "opt-in" but gave no way to opt in short of editing
builtAbis.

`builtAbis` now reads an optional Gradle property: `-PrustynesAbis=a,b`
replaces the default list. Unset or empty, it is `arm64-v8a, x86_64` as
before, so no existing build changes. The release variant's `shipAbi` is
separate and untouched.

Built with it: `./gradlew :app:assembleFossDebug -PrustynesAbis=armeabi-v7a`
gives an APK whose only native directory is lib/armeabi-v7a/
(librustynes_mobile.so: ELF 32-bit LSB, ARM, EABI5, for Android), SHA-256
4e75ff99...cfd3b21, 39,375,703 bytes. The run sheet records it and makes
D5's step install that APK and confirm `primaryCpuAbi` is armeabi-v7a
before recording a result. It also notes that many recent 64-bit phones no
longer run 32-bit apps, in which case D5 is NOT RUN.

Not verified: that APK has not run anywhere. The x86_64 emulator cannot run
ARM code, so D5 still needs a device.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(perf): the p99 capture owed on wgpu 30 -- presents are now even

#570 moved the renderers from wgpu 29 to 30, changing the present call
(Queue::present) and adding an explicit surface colour space. The frame
pacing on the new path had not been measured; this is that measurement,
taken as an A/B rather than a lone number, because a p99 in isolation
reports the host as much as the code (see the v2.3.3 F5 section).

Method: v2.9.2 (4bbc3b3, wgpu 29, built in a worktree) against 9dd64f2
(wgpu 30), run alternately old/new/old/new for 45 s each with
RUSTYNES_PERF_LOG=1 on the CC0 flowing_palette.nes (perf_capture.sh's
default). Captures started once the one-minute load average was under 2.
The config was identical and read from the CSV headers: run-ahead 0, rewind
on, Mailbox, 119.991 Hz Wayland monitor. All four pass perf_log_check.py
with present_discarded=0.

Result, consistent in both pairs:
  presented p50  5.45 / 5.70 ms  ->  8.36 / 8.35 ms
  presented p95 15.63 / 15.67    -> 11.38 / 10.35
  presented p99 16.93 / 16.94    -> 11.97 / 11.70
  rwait p95     14.68 / 14.47    ->  8.48 /  8.45
  produced max  27.0 / 26.9      -> 19.0 / 19.0
  cost p95       2.99 / 2.98     ->  2.95 / 2.94
Both builds present at the monitor's rate (mean 8.37 ms). Under wgpu 29
those presents came in bunches; under wgpu 30 they land one vblank apart.
Emulation cost is unchanged.

Limits, stated in the doc: one host, one ROM, Mailbox only (not Fifo, 60 Hz
or run-ahead), present timing rather than what reached the eye, and not
attributed to a specific wgpu change, since the present-path move and the
colour-space field landed together. The raw CSVs stay in the session
scratchpad.

This closes the "p99 frame-pacing capture owed on wgpu 30" item carried
since #570.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(mappers): rebuild mapper 28 (Action 53) to the NESdev spec

A June review thread on #97 (Gemini, "high") said Action53M28's PRG decode
hard-coded 1-bit inner masks and so broke outer bank sizes above 32 KiB. It
was never answered. Checked against the vendored wiki page
(nesdev_wiki/output/Action_53_mapper.md) the board was wrong in more places
than the thread said:

- PRG: the outer register was shifted left by `size + 1` instead of
  supplying the TOP bits of the bank number; the inner register was masked to
  one bit whatever the size; mode 2 fixed $C000 and mode 3 mapped both
  halves to one bank, where the wiki has mode 2 (UNROM #180) fix $8000 and
  mode 3 (UNROM #2) fix $C000.
- CHR: register $00 was stored and never used; the board has 32 KiB of CHR
  RAM in four 8 KiB banks, not 8 KiB.
- Mirroring: D4 of a $00/$01 write, which selects the 1-screen page while
  mirroring is 1-screen (AxROM emulation), was ignored.
- Power-on: bank 1 sat at $C000; the wiki specifies the last 16 KiB.

The resolver now builds the 9-bit bank number (A22-A14) as `outer << 1 |
A14`, with its low `size + 1` bits replaced by inner-register bits: `inner
<< 1 | A14` in the 32 KiB modes, `inner` for the switchable UNROM half. The
fixed UNROM half keeps all outer bits, as the wiki says ("treated as 32K").
Power-on is mode 0 with outer $FF, which puts the last bank at $C000 for any
power-of-two image; reset leaves the registers alone.

Tests, all red on the old code before the fix (mutation-checked by
restoring the old resolver and power-on, then the old CHR offset and a
removed D4 latch; each mutant fails its test):
- m28_prg_banking_matches_every_row_of_the_wiki_table: the wiki's 12-row
  o/i table is transcribed as data and expanded bit by bit ("o" = topmost
  outer bits, "i" = bottommost inner bits). Every mode value in each row,
  all 256 outer and 16 inner values, at $8000 and $C000, on an 8 MiB image
  so nothing wraps.
- m28_powers_on_with_the_last_bank_at_c000, m28_chr_register_banks_32k_of_chr_ram,
  m28_d4_selects_the_single_screen_only_in_one_screen_modes.
- m28_loads_a_version_1_state_with_8k_of_chr: mapper 28 now has its own
  section version (2, carrying 32 KiB of CHR RAM). A version-1 snapshot
  still loads, its 8 KiB into CHR bank 0, so no save state breaks.

End to end: tests/roms/nes-test-roms/other/test28.nes (Damian Yerrick's
comprehensive test, already in the tree and run by nothing) failed its first
check ("DOES NOT POWER ON WITH LAST BANK IN $C000-$FFFF") before the fix.
It now passes that and stops at check 2, which writes $FF to the outer
register over a ROM $00 and expects bank 0 at $8000 with bank 31 at $C000.
The wiki's own formula cannot produce that pair in mode 0. Compared
black-box (screen output only, rung 3 of the provenance ladder), the Mesen
and Nestopia libretro cores stop at the same check 2 on the same frame.
So the new board matches both references on this ROM, and check 2 is not
treated as a spec here. The wiki's reference-implementation page (rung 1,
public documentation) was read to confirm the reading of the table; the
code is written from the prose table.

docs/mappers.md's row and note are updated. `cargo test -p
rustynes-mappers`: 811 passed in the lib target, all other targets green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(mappers): serialize_header writes back every byte parse_header reads

A review thread on #86 (Gemini) noted that serialize_header re-encodes the
NES 2.0 byte-13 high nibble (Vs. hardware type) from a single DualSystem
bool: types 1-4 (UniSystem boards with protection ICs) came back as 0 and
type 6 as 5. It was never answered, and it matters more than a format nit:
the debugger's header editor (header_editor.rs, write_header_to_file)
writes serialize_header's full 16 bytes back over the ROM file, so saving
any edit to such a ROM silently changed its byte 13.

The same serializer also wrote bytes 14 and 15 as zero ("reserved/extended;
left zero"). In NES 2.0 those are the miscellaneous-ROM count (bits 0-1) and
the default expansion device (bits 0-5), so the editor erased both too.
docs/cartridge-format.md meanwhile described serialize_header as "the exact
inverse of parse_header"; that was not true for these three fields, and the
doc now says what was lost and when it stopped.

Header gains vs_hardware_type (the full nibble, 0 unless NES 2.0 + Vs.),
misc_rom_count and default_expansion_device, decoded by a new
nes2_tail_fields helper (0 for iNES 1.0, whose bytes 13-15 are padding).
vs_dual_system stays the flag emulation consults, derived as type 5 or 6.
The serializer writes the parsed type back as read, and reconciles it only
when the flag has been changed since (the editor's DualSystem checkbox):
setting the flag on a non-dual type writes 5, clearing it on type 5 or 6
writes 0.

Tests, red before the fix (both FAILED at their first assert):
- nes2_round_trip_keeps_byte_13_high_nibble_and_bytes_14_15: all 16
  hardware types, with non-zero bytes 14 and 15, compared byte for byte.
- toggling_dual_system_rewrites_only_what_it_must: type 3 -> set the flag
  -> 5; type 6 stays 6; clearing the flag on 6 -> 0.

cargo test -p rustynes-mappers --lib: 813 passed. clippy -D warnings clean
(the decode moved into a helper to keep parse_header under the 100-line
limit rather than allowing the lint). cargo check --workspace
--all-targets clean: no other code builds Header by struct literal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(frontend): a palette file that fails to load no longer erases the setting

A review thread on #39 (Gemini): when the configured `.pal` could not be
read or parsed, apply_palette_from_config set `[graphics] palette_file` to
None and saved the config. A failure that is only temporary (a network
share that dropped, a USB drive not mounted yet at startup) therefore
deleted the user's setting permanently, with only an eprintln to show for
it. The June draft reply called this "resolved" and "intentional", but no
comment, ADR or doc recorded that decision; the code comment only worried
about a "phantom filename" in the UI, which is the lesser harm.

The read-and-parse step is now config::load_pal_file(path) -> Result<pal,
String>. It takes only a path, so it cannot change the config, and its
error names the path and the reason (the I/O error, or "N bytes; a palette
needs at least 192"). apply_palette_from_config calls it, warns, and uses
the built-in palette for this session only; the configured path stays, so
the palette comes back on the next start once the file is readable.
apply_palette_from_config and apply_active_palette drop from &mut self to
&self: nothing they do writes state any more (clippy's
needless_pass_by_ref_mut found the second one).

Test: load_pal_file_reports_missing_and_short_files_without_touching_config
covers a missing file, a 100-byte file and a valid one. The guarantee that
matters, that a failure leaves the config alone, is held by the signature
(no config parameter, &self callers) rather than asserted at runtime; there
is no cheap way to build an App in a unit test.

clippy -D warnings clean for the frontend natively, with scripting+hd-pack,
and for both wasm32 builds (the function is native-only).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(mappers): NSF bank registers $5FF8-$5FFF read as open bus

A review thread on #44 (Copilot): for a non-bankswitched NSF,
cpu_read_unmapped reported $5FF8-$5FFF as mapped, while cpu_read returned 0
there and cpu_write ignored them, so a read latched 0 onto the data bus
instead of leaving it floating.

The vendored spec settles more than the thread asked.
nesdev_wiki/output/NSF.md's "Summary of Addresses" lists every address an
NSF may read, and $5FF8-$5FFF is not on it at all; they appear only in the
writable list, "if bankswitching is enabled". They are write-only in every
case, so the fix makes the whole window unmapped for reads, bankswitched or
not. The bank registers keep their write path and their priority over the
expansion-audio windows (docs/apu-2a03.md); only the read side changes.

Test bank_register_window_reads_as_open_bus builds a plain NSF and a
bankswitched one (header $070-$077 set) and requires all eight addresses
to report unmapped; it FAILED on the old code at its first address.
cargo test -p rustynes-mappers --lib: 814 passed; clippy -D warnings clean.
docs/mappers.md's NSF paragraph says the registers are write-only.

Not done here: the same spec list says MMC5 ExRAM ($5C00-$5FF5) and the
multiplier ($5205-$5206) are readable for an MMC5 tune. NsfExpansion
serves neither (only $4800 N163 and $5015 MMC5 status), so they read as
open bus. That is missing emulation rather than this defect and is left for
its own change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(core): Nes::from_nsf documents the NSFE and expansion audio it plays

A review thread on #44 (Copilot) said the from_nsf doc advertised NSFe while
the code handled only NESM. The doc was later rewritten to "only NESM; NSFe
and expansion-chip audio are documented deferrals", and then the code moved
on: rustynes_mappers::parse_nsf dispatches on the magic to parse_nsfe
(v2.1.x), and NsfExpansion drives the VRC6/VRC7/FDS/MMC5/N163/5B cores. So
the doc was wrong again, in the opposite direction.

The comment now states both, verified in the code (nsf.rs:146 is_nsfe ->
parse_nsfe; bus.rs:1133 parse_nsf), and notes the correction. It also says
that a non-60 Hz tune calls `play` from the mapper's cycle-timer IRQ rather
than from vblank NMI; the old text described only the NMI path.

rustdoc -D warnings and clippy clean for rustynes-core. Comment-only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(mappers): point the mirroring-override note at the game DB's own docs

A review thread on #37 (Copilot) raised two things in docs/mappers.md's
"Per-game mirroring override" paragraph. The first, a hyphenated crate
name written as a Rust path, was fixed long ago: it now reads
`rustynes_frontend::game_db`, a re-export of rustynes_gamedb. The second
was not: the paragraph still ended "See docs/compatibility.md §Input
devices for the device side". That section is a controller-support list,
unrelated to mirroring, and the game database has no device side; it
corrects header fields.

The sentence now points at the rustynes-gamedb crate docs
(crates/rustynes-gamedb/src/lib.rs), which describe the database's format,
its CRC32 keys and what it is allowed to change at load. No markdown doc
covers that. The CHANGELOG pointer is kept. markdownlint clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(harness): fds_trace no longer calls a truncated disk-info block a match

A review thread on #29 (Gemini): fds_trace compares the disk-info block the
FDS BIOS read against the raw .fds, and when either side was short the
byte loop compared only the overlap, so a clean prefix printed "MATCHES the
raw .fds exactly". The empty case was guarded later; the truncated case
was not. The read block is located with windows(15), so it can be 15-55
bytes long when the read stream ends mid-block, and then a partial compare
still reported a full match. That verdict is what the tool exists to give.

The comparison is now compare_info_block(got, expected), returning
(BlockVerdict, diffs) with four verdicts: NoReference (either side empty),
Matches and Diverged (all 56 bytes compared), and Truncated { compared }
(fewer than 56 available). Truncated prints how many bytes were compared,
both lengths, and whether the prefix differed, and never prints MATCHES.
The per-byte DIVERGENCE lines are unchanged.

Tests in the binary (it requires the commercial-roms feature):
a_truncated_block_with_a_clean_prefix_is_not_a_match, and
full_blocks_match_or_diverge_and_empty_has_no_reference. Mutation: forcing
the Truncated branch off makes the first test fail, so it can catch the
reported defect. clippy -D warnings clean with --features commercial-roms.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(debugger): event heatmap keeps its aspect; reuse the event buffer

Two review threads on #43 (Gemini), both still true of current code.

Aspect: draw_heatmap sized the grid as w = available width,
h = min(w * 312/341, 320). Whenever the 320 px cap bound (any width above
about 350 px, including the panel's 700 px default) the width stayed full,
so the grid stretched horizontally and every dot x scanline cell was
mis-proportioned, despite a comment promising the aspect was kept. Sizing
is now heatmap_size(avail_width): when the cap binds the width shrinks to
cap * DOTS / LINES. Test heatmap_keeps_its_aspect_when_the_height_cap_binds
checks 64-1600 px for the cap, the width bound and the ratio (within 1e-4),
and that the 700 px default is narrowed. Putting the old formula back
fails it.

Allocation: every repaint collected nes.events() into a fresh Vec<Ev>
while the panel was open. EventPanelState now owns the buffer. `show` takes
it with mem::take (so `state` can still be borrowed mutably by the
heatmap and table), clears it, refills it and puts it back, keeping its
capacity. There is no early return between take and restore. No test
covers the allocation itself: it would need an egui context and a running
Nes, for a debug-panel-only cost.

clippy -D warnings clean for the frontend. frontend lib tests: event_panel
1 passed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(shaders): Bisqwit NTSC window stays on the line at the picture edges

A review thread on #33 (Gemini) asked for two things in the Bisqwit NTSC
fragment shader: clip the UV-space letterbox to black, and clamp the
filter's centre so the full FW-sample window stays in range. The first
landed later (fs_main's bounds check and the row clamp). The second never
did. `center` is i32(suv.x * 2048) unclamped, and signal_sample returns 0
for any tap outside [0, 2048). So at the leftmost and rightmost output
columns part of the 12-sample window summed zeros, and the edges came out
darker than the picture. The June draft reply said this was done; it was
not (`git log -S'clamp(center'` finds nothing).

center is now clamped to [FW/2 - 1, 2048 - 1 - FW/2]. The window runs from
center - (FW/2 - 1) to center + FW/2, so every tap lands on the line. Near
the edges this moves the sampling point inward by at most half a window
(6 samples, under one NES pixel), which is the trade the review proposed.
The carrier phase is still computed from the unclamped per-tap t, so the
chroma demodulation is unchanged.

The edit is made in the generator (ntsc_bisqwit.rs) and in its committed
output crates/rustynes-gfx-shaders/src/bisqwit.wgsl, which the Android
renderer also loads. shared_bisqwit_wgsl_matches_generator checks the two
are byte-identical, and shader_parses_and_validates runs naga's WGSL front
end and validator. Both pass, along with the rest of the module's 5 tests.

Not verified: the visual result. There is no GPU in the test run, and the
edge change is under one pixel wide. Nothing else in the shader changed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(frontend): about_fx::pump states its real per-keystroke KDF cost

Found while verifying the replies to #83's about_fx threads. The finding
there (Gemini: the key derivation runs on the UI thread) is declined by
design, and the reason is written in pump's doc comment. But that comment
also claimed "a single intentional attempt is one KDF, not one per
keystroke", and the code does not do that. `last_try` caches only the
previous trailing window. Once the feature is armed, every keystroke after
the first TRY_LEN (11) characters shifts the window and runs one
50,000-round SHA-256 KDF plus a decrypt attempt.

The comment now says exactly that, notes the correction, and keeps the
design reason, restated honestly: the cost is paid only by someone who
deliberately armed the feature and is typing into it. No code change.
rustdoc -D warnings (default gate) and clippy clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(apu): a NaN channel gain falls back to unity instead of reaching the mixer

Found while verifying the reply to a #93 thread (Gemini: set_channel_gain
could panic on NaN). The panic claim is false, since f32::clamp does not
panic on a NaN receiver. But f32::clamp passes NaN through, so a NaN gain
was stored as-is, and one NaN term makes every mixed sample for that output
NaN. The only route in is a hand-edited config, and only on the
off-by-default per-channel mixing path. Cheap to close.

set_channel_gain now stores 1.0 (unity, the neutral value) for a NaN and
clamps everything else to 0.0..=2.0 as before. The infinities already
clamped to the range ends. Unity was chosen over 0.0 so a bad config value
does not silently mute a channel.

Test channel_gain_rejects_nan_and_clamps_infinities, red before the fix
(it read back NaN): NaN -> 1.0, +inf -> 2.0, -inf -> 0.0.
cargo test -p rustynes-apu --lib: 160 passed, 3 ignored. clippy clean;
the no_std thumbv7em build of rustynes-core still compiles. Default
(unity) output is untouched: unity_gain_produces_byte_identical_samples
passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(changelog): v2.9.3 Fixed entries for the ten review findings

Records the user-visible fixes from 2a084d5..52b2067 under [Unreleased]
Fixed: mapper 28 rebuilt to the spec (save states still load), the header
editor writing back every byte it read, the palette setting surviving a
failed load, NSF $5FF8-$5FFF open bus, the Bisqwit NTSC edge darkening, the
event heatmap's aspect and allocation, the NaN channel-gain guard and the
fds_trace truncated verdict. Also says where they came from: of 244 never-
answered review threads on PRs #29-#97 checked against current code, 234 were
answered and resolved and these ten still held.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(mobile): the run sheet names the APKs rebuilt with the v2.9.3 fixes

The eleven fix commits change crates the Android app links (rustynes-mappers
for mapper 28, the header serializer and NSF open bus; rustynes-apu for the
gain guard), so the APKs recorded against 2ed51d3 no longer matched the code
under review. Both were rebuilt from d1283a5 with a clean tree:

  fossDebug (arm64-v8a + x86_64)  c4e3543e...ad1f32fad5a7f25  67,310,835 bytes
  armeabi-v7a (-PrustynesAbis)    040eabc9...8da19d00e4c56f25  39,375,943 bytes

The ABI sets were confirmed from each APK's lib/ directories. The emulator
column is kept and now says it was produced on the earlier build, and why it
was not re-run: none of the new changes touch the lifecycle, audio-focus or
renderer paths those rows exercise. markdownlint clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix: #571 review round -- extended console type, Rust ABI union, doc fixes

Header editor (CodeRabbit, outside-diff on docs/cartridge-format.md:36).
The byte-13 row was wrong, and checking it against NES_2_0 showed the
serializer was too: for console type 3 (Extended), byte 13's LOW nibble is
the extended console type (VT01-VT32, EPSM, decimal-mode famiclones), and
`parse_header` never read it, so saving any such header wrote it back as 0
-- the same class of defect e36f478 fixed for the Vs. hardware type.
`Header` gains `extended_console_type`, read by `nes2_tail_fields` and
written back only when the console type is Extended.
`nes2_round_trip_keeps_the_extended_console_type` round-trips all 16 values;
it failed before the serializer change (value 1 came back 0) and passes now.
`parse_header` hit clippy's 100-line limit by one; the battery/trainer bits
moved into the struct literal rather than allowing the lint (clippy counts
code lines, so moving comments did not help).

Android (CodeRabbit, major). `-PrustynesAbis=armeabi-v7a` made cargo-ndk
build ONLY the 32-bit library, but `uniffiBindgen` generates the Kotlin
bindings from the arm64 `.so`. The run-sheet v7 APK built only because an
earlier build had left an arm64 library in target/ -- on a clean checkout
the bindings step would fail or, worse, read a stale library. cargo-ndk now
builds `(builtAbis + shipAbi).distinct()`; the debug variant still packages
`builtAbis` only, so the v7 APK keeps a single `lib/armeabi-v7a/`. Also the
`as String?` cast is now `as? String` (agy), so a non-string property
falls back to the default instead of throwing at configuration time.

Docs:
- run sheet A8: the row's step is one minute and the emulator reading was
  30 s, so it is NOT RUN with the reading recorded as preliminary, not PASS.
- v2.9.3 plan: the JVM unit tests were a pre-PR gap this release closes
  (`kotlin-unit-tests`), not a present-tense "never runs".
- performance.md v2.9.3: the raw CSVs are named at their kept location
  (git-ignored `salvaged/evidence/v2.9.3/pacing/`), not "the session
  scratchpad". The `present_discarded=0` claim itself was re-checked, not
  changed: perf_log_check.py prints 0 for all four CSVs.
- apu-2a03.md: the per-channel gain clamp and the v2.9.3 NaN fallback
  (Docs-As-Spec for 52b2067).
- CHANGELOG: the header-editor entry names the extended console type.

Verified: cargo fmt; clippy -D warnings (workspace, all targets); rustdoc
-D warnings; markdownlint (pinned, via pre-commit); rustynes-mappers 815
lib tests; cargo test --workspace 2,547 passed, 0 failed. The Gradle change is
verified by rebuilding both run-sheet APKs from this commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(mobile): the run sheet names the APKs rebuilt from e1cc919

e1cc919 changed rustynes-mappers (the NES 2.0 extended console type), so
under the sheet's own rule the d1283a5 APKs were stale. Both are rebuilt
from e1cc919:

  fossDebug (arm64-v8a + x86_64)
    7d3b42106d61e420e6eaf1f5da54fbadb3f0e5d26c21e176958cb6fb47f3e300
    67,311,011 bytes
  fossDebug, -PrustynesAbis=armeabi-v7a (row D5)
    ea384af65e37b2709b379b2e7c48645ead35a37410789ce344591a0c64db503f
    39,375,999 bytes

The v7 build also verifies e1cc919's Gradle fix: cargo-ndk's log now shows
"Building armeabi-v7a" followed by "Building arm64-v8a", so the bindings
step reads an arm64 library built in the same run. The APK itself still
carries only lib/armeabi-v7a/ (unzip -l).

Trap worth recording: the first v7 repackage came out at 44,208,886 bytes,
about 5 MB larger than its predecessor, with every zip entry identical in
size and compression (unzip -v). AGP's incremental packager reuses the APK
file in place and can leave free space inside it. Deleting the output APK
before assembling gave 39,375,999 bytes, i.e. the old size plus the 56
bytes the two rebuilt libraries grew. Checksum a freshly written APK, not
a repackaged one.

The emulator column is unchanged: the new code only affects how the header
editor writes byte 13 for Extended-console images, and no emulator row
exercises that path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(frontend): a palette load reads only the 192 bytes it uses

`load_pal_file` (added in 1c9ab63) read the configured `.pal` with
`std::fs::read`, i.e. the whole file into memory. The path is user-supplied
config and can name anything, so a huge file, a FIFO or an endless device
allocated without bound; on `/dev/zero` the read grows its buffer until the
allocator fails. Raised by agy on #571 (round on e1cc919).

`parse_pal` only ever looks at the first 192 bytes (64 RGB triples), so the
read is now `File::open(path)?.take(192).read_to_end(..)`. Nothing is
rejected for being large: a 1,536-byte palette with the emphasis variants
still loads its first 64 colours, exactly as before, and the short-file
error still reports the real length because it is below the cap.

Red first: `load_pal_file_reads_only_what_a_palette_needs` (Linux only; it
needs a device that never ends) runs the load on `/dev/zero` in a thread
with a 10 s receive timeout. On the previous code it FAILED on the timeout;
now it returns 64 black colours immediately. The earlier missing/short/good
test still passes.

Declined from the same review, with reasons in the PR: agy's nitpick that
the short-file message could mislead if `parse_pal` ever grew content
validation -- today length is its only failure, and the message says so.

Verified: rustynes-frontend load_pal_file tests 2/2; clippy -D warnings on
rustynes-frontend all targets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(changelog): record the four new public Header fields

agy's blocking finding on #571 is correct by VERSION-PLAN's own wording:
`rustynes-core` does `pub use rustynes_mappers;`, which puts
`rustynes_mappers::Header` inside "the chip-crate types it re-exports", and
adding a field to a struct whose fields are all public breaks struct-literal
construction outside the crate. e36f478 and e1cc919 added
`vs_hardware_type`, `misc_rom_count`, `default_expansion_device` and
`extended_console_type` to stop the header editor zeroing those bytes.

The maintainer chose to keep the fields in this patch release and record the
change rather than rework the fix into an additive
`serialize_header_preserving` or re-cut as a MINOR (2026-09-29). The facts
behind that: no crate is published to crates.io, nothing in the workspace
builds a `Header` with a literal (the only external caller of
`serialize_header` is the frontend header editor, which mutates a parsed
header), and the whole workspace builds and passes. The only earlier
additions to `Header`'s fields came in v1.0.0 and v1.3.0, both MINOR or
MAJOR; this is the first in a PATCH, which is why it is written down.

The CHANGELOG's header-editor entry now names the four fields and says who
has to act (code outside this repository that builds a `Header` literal).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* fix(mappers): keep unedited header bytes; deprecate serialize_header

Replaces the header-editor fix from e36f478 and e1cc919, which added four
public fields to `rustynes_mappers::Header` (`vs_hardware_type`,
`extended_console_type`, `misc_rom_count`, `default_expansion_device`) so
the canonical encoder could write those bytes back. agy on #571 pointed out
that this is an API break by VERSION-PLAN's definition: `rustynes-core`
does `pub use rustynes_mappers;`, which puts `Header` in "the chip-crate
types it re-exports", and a field added to a struct whose fields are all
public breaks struct-literal construction. The maintainer chose (2026-09-29)
to handle it the way ADR 0042 handles the v2.7.5 deprecations: add in a
patch, deprecate in a patch, break only at v3.0.0. `Header` is back to its
v2.9.2 shape, so this commit removes the break instead of documenting it
(02187e0's CHANGELOG note is replaced).

Mechanism. `serialize_header_preserving(h, original)` parses `original`
(the 16 bytes `h` came from), compares field by field, and for each field
whose value changed copies only that field's bits from the canonical
encoding of `h`. Everything else in `original` survives, including bits
`Header` never modelled, so the fix is more complete than the fields were.
The fields covered only the bytes someone had noticed; this keeps the NVRAM
nibbles, exponent-notation sizes, reserved bits and the junk dumpers left
in iNES 1.0 bytes 8-15 as well. Two edits re-encode more by design: a
console-type change rewrites byte 13, whose meaning depends on the console
type, and toggling `is_nes2` re-encodes the whole header, since bytes 7-15
change meaning with the format. So does an `original` that does not parse.
The editor keeps the bytes it read, writes through the new function, and
replaces them with what it wrote after a successful write.

A second defect, found by the new per-field test: the canonical encoder
wrote byte 7's high nibble as `((mapper_id >> 4) as u8) & 0xF0`, i.e.
mapper bits 8-11, where bits 4-7 belong. Every mapper from 16 up therefore
came back wrong (mapper 66 as 2). The expression dates from the v1.0.0
transplant (dba2e75), and the header editor has written its output to
disk since v1.7.0. The loader only ever parses, so emulation was never
affected. `canonical_encoding_round_trips_every_mapper_id` pinned it red
(mapper 16 read back as 0) before the one-line fix; the pre-existing
round-trip tests only used mappers below 16.

`serialize_header` is `#[deprecated(since = "2.9.3")]` and is now a
wrapper over a private `canonical_header`; ADR 0042 gains a dated
amendment putting its removal, and whether `Header` should model the
remaining bytes (with `#[non_exhaustive]`), on the v3.0.0 list.

Tests (rustynes-mappers, header module 33/33):
- identity: every byte-7 x byte-13 pair (65,536) plus 200,000 random
  headers, every one that parses, must come back byte for byte;
- `each_edit_rewrites_only_its_own_bits`: 11 edits on a header with every
  unmodelled bit set, each checked against the changed-bit mask it is
  allowed, and parsed back;
- the Vs. hardware type / bytes 14-15, extended console type and
  DualSystem-toggle cases carried over from the branch;
- iNES 1.0 edits never touch bytes 8-15; a format change or bad original
  encodes canonically; a console change re-encodes byte 13.
Mutation check, all five caught: dual toggle writing the whole of byte 13,
no diffing (always canonical), the CHR-RAM edit clobbering the NVRAM
nibble, a battery edit ignored, NES 2.0-only fields written on iNES.

Docs in the same change: `docs/cartridge-format.md` (the "exact inverse"
paragraph was false for the canonical encoder and now says what each
function does), `docs/frontend.md`'s header-editor entry, the editor's
module doc, CHANGELOG (two Fixed entries, a Deprecated entry), ADR 0042.

Verified: cargo fmt --check; clippy -D warnings on the workspace (all
targets), rustynes-frontend with `full`, and rustynes-frontend for
wasm32; rustdoc -D warnings; the thumbv7em no_std build; cargo test
--workspace 2,554 passed, 0 failed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(mobile): the run sheet names the APKs rebuilt from 1b4c588

1b4c588 changed rustynes-mappers (the header writer and the canonical
encoder's byte-7 mapper nibble), so under the sheet's own rule the
e1cc919 APKs were stale, even though neither app calls the header
writer. Both rebuilt from 1b4c588 with the APK output deleted first:

  fossDebug (arm64-v8a + x86_64)
    01fdc7e8b65b93d4e68f9b6c576baf0e44394cf0b7d0886e9a03bc18d42a8f3a
    67,310,731 bytes
  fossDebug, -PrustynesAbis=armeabi-v7a (row D5)
    3590989ad131889735dd6a4d20bfba02615affddb31dbd832ac4ec9f730a1af4
    39,375,895 bytes, lib/armeabi-v7a/ only

The sheet's summary of what this build adds over the emulator pre-run's
now says the header writer is not reachable from the apps, which is why
the emulator column still stands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
doublegate added a commit that referenced this pull request Sep 29, 2026
… prepared (#572)

* v2.9.3 "Handset" -- the old review threads closed, and the mobile run prepared

The release cut for v2.9.3, on top of #571 (462e23d) and #570 (d8b02d2).
Workspace and rustynes-cosim move to 2.9.3 (both lockfiles), the libretro
.info's display_version with them, and every current-release anchor the
three anchor audits cover: README (badge, Current Release, the two hardware
paragraphs), AGENTS.md, ARCHITECTURE.md, OVERVIEW.md, ROADMAP.md,
SECURITY.md, SUPPORT.md, VERSION-PLAN.md (header and a new (current) row),
docs/STATUS.md and to-dos/ROADMAP.md. CHANGELOG [Unreleased] becomes
[2.9.3] with a summary paragraph; .github/release-notes/v2.9.3.md is the
body release-auto.yml publishes.

The release carries one maintainer decision that is not a routine cut
(2026-09-29): the mobile device runs, the SuperStation One board session
and the fixes each produces move AFTER v3.0.0, and v2.9.3 ships without its
device run. That contradicts ADR 0041, whose Decision defines v3.0.0 as the
release in which a bitstream is verified on real hardware, and the v3.0.0
plan's gate (every bring-up row PASS). This commit records the move and the
conflict without resolving it:

- ADR 0041 gets a dated amendment stating both, and that what v3.0.0 ships
  is open for the maintainer. ADR 0042's API break would make v3.0.0 MAJOR
  on its own, so the number does not force an answer.
- The v3.0.0 plan is marked "under revision" with its text kept; the v2.9.x
  line plan notes that v2.9.4+ ("whatever the board finds") will not happen
  in this line; the plans index, to-dos/ROADMAP.md and the run sheet say the
  same. The "no hardware has run any bitstream" anchors stay unflipped.
- The v2.9.3 plan gains an Outcome section: what shipped (run sheet,
  emulator pre-run, CI for the Kotlin tests and the iOS renderer, #570, the
  sibling pin, the ten review fixes) and what is not established (every
  Swift change since v2.7.4 uncompiled, no Android row on a device).

`release_anchor_audit` caught one site the transform missed: the
release-line chain in to-dos/ROADMAP.md still ended "v2.9.2 ..., the
current release". The first test-roms run stopped at that binary (2,811
passed, 1 failed); the chain now ends at v2.9.3 and the run was repeated
with --no-fail-fast.

No code changes here besides the version strings. The Android versionName
(2.0.4) is not a release anchor and was not moved by v2.9.2 either.

Verified on this tree: `cargo test --workspace --features test-roms
--release --no-fail-fast` 2,887 passed, 0 failed, 20 ignored; release_anchor_audit, release_state_prose_audit,
release_notes_render_audit, cosim_manifest_audit, libretro_info_audit and
contribution_checklist_audit pass; pinned markdownlint clean on every
edited file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

* docs(release): state the real CI scopes and the partial emulator pre-run

Copilot on #572 (14 threads, all correct): the v2.9.3 release prose said the
Android unit tests and the iOS renderer are "checked on every pull request".
Neither is. `android.yml`'s `kotlin-unit-tests` runs on `main` pushes and on
PRs whose diff matches the `kotlin` filter (`android/**`,
`crates/rustynes-mobile/**`, the workflow itself); `ci.yml`'s lint job, which
runs `scripts/ios-host-typecheck.sh`, is skipped when `changes` reports no
code change. The tagline now says "added to CI" in all eight anchor copies;
README, the release notes and the CHANGELOG Testing entry state the scopes.

Also corrected:
- The release notes and README said every emulator-reachable Android row
  ran. A2 (switch games and back) is emulator-reachable and was not run, and
  A8 has a 30 s reading against a one-minute step. Both now list what ran.
  The v2.9.3 plan's Outcome no longer calls every unrun row device-only.
- VERSION-PLAN's "Planned next (not yet released)" table still listed the
  shipped v2.8.x and v2.9.x lines and described v3.0.0 as the hardware-
  verified core. The shipped rows are gone; v3.0.0 is "scope under revision"
  (ADR 0041's 2026-09-29 amendment), and an "after v3.0.0" row holds the
  hardware work.

Verified: release_anchor_audit, release_state_prose_audit,
release_notes_render_audit pass; pinned markdownlint clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

---------

Co-authored-by: Claude Opus 5.5 (1M context) <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