Repository navigation
chore(deps): every dependency at its newest -- egui 0.36 + wgpu 30, rcheevos 12.5.0, Gradle 9.8.0 - #570
Conversation
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
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Repository: doublegate/RustyNES/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: doublegate/RustyNES/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: ⛔ Files ignored due to path filters (40)
📒 Files selected for processing (25)
💤 Files with no reviewable changes (1)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe 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. ChangesDependency refresh and graphics updates
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
Merge Risk: ⚪ Minimal · up to 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 ReviewSecurity architecture risk: 🟡 Moderate · up to 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 Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 7 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (7 passed)
Full details: Docstring CoverageExplanation 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 BlocksExplanation The PR adds unsafe blocks in the new vendored Rust files without the required adjacent Resolution Add an adjacent ✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
|
@coderabbitai review |
|
Docs7 for doublegate/rustynes
Commit |
✅ Action performedReview finished.
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Antigravity review (Gemini via Ultra)This PR updates core graphics, UI, and build dependencies to their newest versions, resolving breaking API changes in Blocking issuesNone found. Suggestions
Nitpicks
Automated first-pass review by Earlier review rounds (newest first)Round reviewed at 2026-09-28 23:17 UTCAntigravity review (Gemini via Ultra)
Blocking issuesNone found. Suggestions
Nitpicks
Automated first-pass review by Round reviewed at 2026-09-28 22:43 UTCAntigravity review (Gemini via Ultra)Updates UI, graphics, and achievement backend dependencies to their latest major versions, adapting API calls and vendoring Blocking issues
Suggestions
Nitpicks
Automated first-pass review by |
There was a problem hiding this comment.
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
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-winit0.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.
…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
|
Replying to the Antigravity review of Blocking,
Vendoring documentation: already there.
|
…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
|
@coderabbitai, on the two pre-merge warnings: Safety comments in
Docstring coverage: no change. The two functions this PR adds, Also since your review: the |
|
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 -230Length 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}' || trueLength 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; doneLength of output: 17248
The docstring warning is withdrawn. Both You are interacting with an AI system. |
|
Replying to the Antigravity rounds on
|
#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
…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>
… 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>

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.
9fe28af8gradlew.batkept at this repo's LF line endings)49efc18398dbb18dThe one thing to review closely: a vendored
egui-winitegui 0.36 requires wgpu 30, and
egui-winit0.36.2 (the newest release) does not compile for wasm32 (E0407onDroppedFile::bytes, re-measured today). Upstream fixed it onmainbut has not released it. Rather than a git-rev patch, whichdeny.tomlforbids,vendor/egui-winit/carries the crates.io 0.36.2 package with a three-sitecfg(not(target_arch = "wasm32"))guard, wired through[patch.crates-io]the same wayvendor/rust-libretro-sysis.vendor/egui-winit/VENDORED.mdrecords 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
Queue::present,color_space: Auto(unchanged picture),apply_limit_buckets: false(unchanged limits), and a fallibleget_mapped_range. Applied in the frontend, Android and iOS.rc_client_user_tgrew atime_t. The ABI size guard failed on it first, and the Rust mirror now carries the field.bincoderemoved, not bumped: it was unused, and 3.0.0 is acompile_error!tombstone.deny.toml: theCC0-1.0allowance is dropped. Its only user (hexf-parse, via naga 29) is gone.Deliberately not moved
rust-toolchain.tomldocuments why: 1.97+ breaks the four Apple libretro buildbot jobs until theirbefore_scriptis changed. The toolchain is not a crate, and moving it is a separate decision.dep:aliases exist to enable the wasm backend of the versionsrand_core/ahashresolve. Moving them would disconnect the script-wasm build from its RNG.1.3.0-alpha02line, 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 denyall ok;cargo ndkarm64 check; Android:app:testFossDebugUnitTestpasses;trunk build --releasesucceeds;--features test-roms2,869 passed / 0 failed; AccuracyCoin 144/144.Not verified:
rustynes-ios'sgfx_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