diff --git a/.github/release-notes/v3.5.0.md b/.github/release-notes/v3.5.0.md index 6d02e0ea..405a1f98 100644 --- a/.github/release-notes/v3.5.0.md +++ b/.github/release-notes/v3.5.0.md @@ -51,20 +51,20 @@ v3.5.0 is a creator-tools release. Movies record the Zapper and the other expans - **The Famicom peripherals**: the Family BASIC keyboard from a PS/2 keyboard, the Power Pad on port 2, and the second controller's microphone, gated against the NESdev pages. - **Provenance disclosures** (the sibling's `docs/provenance.md`, "Disclosures"): three reads that touched derived regions of the oracle while this release was written, none of them used: the oracle's Power Pad region, read before it carried its header (the RTL follows the NESdev page and is gated against it); the comment above the oracle's EEPROM model (the data sheets were followed); and the Namco 163's output level, seen in a search result (the RTL's level comes from the NESdev page's measurements, and the audio comparison is blind to a uniform scale). They are recorded for outside review, not certified clean. - **The MMC5 detector was checked by mutation**: six deliberate defects, three caught at once; one (the 241st-line exit) is unreachable, since line 240 makes no reads, and the other two were caught after a new stimulus that reads the IRQ status by hand (`mapper5irqack406`). -- **The oracle pin moves to v3.5.0**: the sibling pins this release's merge and re-checks every golden against it before its own release; the counts are in its v3.5.0 notes. +- **The oracle pin moves to v3.5.0** (this release's merge, `5d441f44`): `VERIFY=1` reproduces all 256 stems and 1,156 artifacts byte for byte. - **The co-simulation ladder**: on the die 315 passed, 0 failed, 4 expected failures; off the die 365 passed, 0 failed, 10 expected failures. The four on both are `chrram-fetch` (18 of 185,725 fetches), *Mississippi Satsujin Jiken* frame 140, *Micro Machines* frame 71 and the Sunsoft 5B timed gate; of the six off the die only, five are where the oracle departs from the NESdev pages or the EEPROM data sheets (two Bandai EEPROM stimuli, the VRC7's WRAM enable, and the VRC6 and MMC5 audio timing), each graded both ways, and one is v3.4.0's *Ganbare Goemon Gaiden* frame 25, eight pixels lost to the SDRAM's CHR latency. -- **New bitstreams** are compiled from the sibling's release commit after this release merges and attached here and to the sibling's release; their seeds, checksums and timing margins are in its v3.5.0 notes. +- **New bitstreams** at seed 2, BUILD_DATE 2026-10-23, each byte-identical across two clean compiles: the off-die `RustyNES_MiSTer-v3.5.0.rbf` (md5 `737048b65de5c712cc5d40fc26b906fa`, setup +0.380 / hold +0.081 ns, SDRAM read capture +0.436 / +1.176 ns over all four corners) and the on-die `RustyNES_MiSTer-v3.5.0-lite.rbf` (md5 `3db762d73bdc3d685638121cd0ee360b`, setup +0.231 / hold +0.074 ns). They replace a seed-3 pair attached for a few hours on 2026-10-23: seed 3 followed an outdated reading of the seed rule (found in the MiSTer release review), and the rebuild also carries that review's fixes, among them two Family BASIC keys held across their twin's release and the off-die OSD's ROM-size message. ## Verification - `cargo test --workspace --features test-roms,commercial-roms --no-fail-fast`: 3,920 passed, 0 failed, 19 ignored, with the commercial suites at 60/0, 169/0 and 6/0. - `cargo fmt --check`; `cargo clippy --workspace --all-targets -D warnings`, and again with `scripting`, `scripting,hd-pack`, `retroachievements` and `full`, for both browser builds, and for the excluded `rustynes-cosim`; `cargo doc` with `-D warnings`; the `thumbv7em` `no_std` build; markdownlint. All clean. -- MiSTer: the co-simulation ladders above; the pin re-verification and the two-compile bitstream checks are recorded in the sibling's v3.5.0 notes. +- MiSTer: the co-simulation ladders above; `VERIFY=1` at `5d441f44` (256 stems, 1,156 artifacts); `release-rbf.sh v3.5.0 --no-build` passed every check against the compile date; both release assets downloaded back match the md5 above. ## Install - Download the pre-built binaries for Linux, macOS, and Windows below. -- The MiSTer bitstreams are attached below once they are compiled from the sibling's release commit, as a release candidate, not hardware-verified: `RustyNES_MiSTer-v3.5.0.rbf` (off-die, needs SDRAM) and `RustyNES_MiSTer-v3.5.0-lite.rbf` (on-die). +- The MiSTer bitstreams are attached below as a release candidate, not hardware-verified: `RustyNES_MiSTer-v3.5.0.rbf` (off-die, needs SDRAM) and `RustyNES_MiSTer-v3.5.0-lite.rbf` (on-die). - The WebAssembly build is live at [doublegate.github.io/RustyNES](https://doublegate.github.io/RustyNES/). - The RetroArch core is in RetroArch's Online Updater on the platforms the libretro buildbot publishes to. - Licensed under GPL-3.0-or-later. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad26701e..df55d37d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -614,6 +614,34 @@ jobs: with: cache-key: full-build - run: cargo build -p rustynes-frontend --features full --locked + # 2026-10-23 (after v3.5.0) — the OS shared-memory store (`shm.rs`, + # `script-ipc`) has a Windows implementation and an Apple one + # (`target_vendor = "apple"`: variadic `shm_open`, `PSHMNAMLEN`, a `u16` + # `st_mode`), each with its own tests, among them v3.5.0's Windows + # grow-while-another-program-holds-it recovery. This job built them and no + # job ran them: the `test` matrix reaches Windows and macOS only on a full + # run and without `script-ipc`, and the feature steps above are Linux only. + # This runs the store's, the script host's and memif's tests on both, on + # every ready PR (macOS: Antigravity, #625). + # + # `full`, not `script-ipc` alone: it is the feature set the step above + # just built, so the dependencies are reused rather than rebuilt under a + # second fingerprint, and it adds the `memif::` tests (Antigravity, #625). + # The checks after it exist because a passing summary proves too little: + # an empty filter exits 0, and libtest ORs its filters, so one group can + # carry the summary while another matches nothing (CodeRabbit and + # Antigravity, #625). So each group must show a passing test, and the + # Windows-only grow-recovery test must be among them by name. + - shell: bash + run: | + set -o pipefail + cargo test -p rustynes-frontend --lib --features full --locked -- shm:: script_host:: memif:: 2>&1 | tee test.log + for group in shm script_host memif; do + grep -qE "^test ${group}::[^ ]+ \.\.\. ok$" test.log || { echo "no passing ${group}:: test ran"; exit 1; } + done + if [ "$RUNNER_OS" = Windows ]; then + grep -qE '^test script_host::tests::a_larger_write_with_a_reader_attached_is_refused_and_the_region_kept \.\.\. ok$' test.log + fi test-roms: name: test (test-roms feature) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f39a444..b4304888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,37 @@ cycle-accurate core later replaced. ## [Unreleased] +### Changed + +- **CI runs the shared-memory store's Windows and macOS tests**: the Lua IPC + transports' OS shared-memory code has Windows and Apple implementations, and + v3.5.0 added a Windows test for growing a region another program still + holds. The `full-build` job compiled that code on both systems and no job ran + its tests. Both legs now run the store's, the script host's and `memif`'s + tests on the `full` build; each group must show a passing test, and on + Windows the grow test must pass by name (`T-WIN-SCRIPT-IPC-CI`). Its first + run passed on Windows, including the grow test, and failed one test on + macOS: the test that plants a shared-memory object other users can read + changed the object's mode with `fchmod`, which macOS does not support on + such objects. The refusal itself works there; the test now plants with the + mode the system gives a new object and reads that mode back. + +### Documentation + +- **v3.5.0's release notes file matches the published release**: the MiSTer + pin, bitstream and verification lines were placeholders in the merged file + and were filled in on the release page once the sibling had pinned the merge + and compiled the bitstreams; the file now carries the published text. +- **v3.5.0's Linux download is the plain build**: the release's PGO gate did + not promote. The profile-guided build was byte-identical to the plain one on + the oracle check and measured 4.92% slower (the gate needs more than 3% + faster), as v3.4.1's measured 5.05% slower. The only promotion in the release + history is still v2.9.6's. +- New tickets from the v3.5.0 reviews: `T-MMC5-LATCH-VBLANK` (the MMC5 split + and ExGrafix CHR latches survive VBlank in both cores; a documentation and + test-ROM question first), `T-WIN-SCRIPT-IPC-CI` and `T-V350-REVIEW-RESIDUE` + (suggestions not taken, to be confirmed or refuted). + ## [3.5.0] - 2026-10-22 - "Chorus" (creator tools: the full movie input stream, Lua WebSockets and shared memory, an MCP server, speedrun timing and dump checks; and the MiSTer core's MMC5, VRC6, Namco 163, VRC7 and Bandai FCG boards, off the die) ### Breaking diff --git a/crates/rustynes-frontend/src/shm.rs b/crates/rustynes-frontend/src/shm.rs index a8356de1..7a64bd50 100644 --- a/crates/rustynes-frontend/src/shm.rs +++ b/crates/rustynes-frontend/src/shm.rs @@ -884,10 +884,22 @@ mod tests { #[test] fn a_squatted_object_with_open_permissions_is_refused() { use std::ffi::CString; + // Unlinks the planted name when dropped, so a failing assertion below + // does not leave the object in the system's namespace (Antigravity, + // #625); dropped explicitly before the fresh create at the end. + struct Plant(CString); + impl Drop for Plant { + fn drop(&mut self) { + // SAFETY: a valid C string naming the object planted below; + // unlinking a name that is already gone only returns ENOENT. + unsafe { libc::shm_unlink(self.0.as_ptr()) }; + } + } let name = unique("sq"); let c = CString::new(os_object_name(&name)).unwrap(); - // SAFETY: a valid C string; a new object, mode 0666 (umask may clear - // some bits; the fchmod below sets them regardless). + // SAFETY: a valid C string; a new object, mode 0666 less the umask. + // Outside Apple the fchmod below then forces 0666; on Apple the plant + // keeps the reduced mode (see there). let fd = unsafe { libc::shm_open( c.as_ptr(), @@ -896,22 +908,54 @@ mod tests { ) }; assert!(fd >= 0, "plant: {}", io::Error::last_os_error()); + let plant = Plant(c); // SAFETY: `fd` is the descriptor just opened. - unsafe { - assert_eq!(libc::ftruncate(fd, 4096), 0); - assert_eq!(libc::fchmod(fd, 0o666), 0); + assert_eq!(unsafe { libc::ftruncate(fd, 4096) }, 0); + // macOS does not implement `fchmod` on a POSIX shared-memory + // descriptor (it fails; found when CI first ran this test there, #625), + // so on Apple the plant keeps the mode `shm_open` gave it: 0666 less + // the umask, 0644 under the usual 022, which others can read and the + // check must refuse. The expected message is built from the mode + // actually read back rather than assumed. + #[cfg(not(target_vendor = "apple"))] + // SAFETY: `fd` is the descriptor just opened. + assert_eq!(unsafe { libc::fchmod(fd, 0o666) }, 0); + // `st_mode` is a u32 on Linux and a u16 on macOS, as in `check_owner`. + #[allow(clippy::useless_conversion)] + // SAFETY: `fd` is open; `st` is a valid out-pointer for `fstat`. + let mode = unsafe { + let mut st: libc::stat = std::mem::zeroed(); + assert_eq!(libc::fstat(fd, &raw mut st), 0); libc::close(fd); - } + u32::from(st.st_mode) & 0o777 + }; + // Both preconditions only bind on Apple, where the plant keeps the + // umask-reduced mode: an unusual umask such as 0700 leaves 0066, which + // passes the second check but which `open`/`create` cannot open read- + // write, so they would fail with an OS error before the owner check + // runs (CodeRabbit, #625). + assert_eq!( + mode & 0o600, + 0o600, + "the plant is mode {mode:03o}: this process's umask removes owner \ + read/write, so O_RDWR cannot reach the owner check" + ); + assert_ne!( + mode & 0o077, + 0, + "the plant is mode {mode:03o}: this process's umask leaves the \ + object private, so no squatted object can be planted to test" + ); + let expected = format!("mode {mode:03o}"); for err in [ SharedRegion::open(&name).unwrap_err(), SharedRegion::create(&name, 4096).unwrap_err(), ] { assert_eq!(err.kind(), io::ErrorKind::PermissionDenied, "{err}"); - assert!(err.to_string().contains("mode 666"), "{err}"); + assert!(err.to_string().contains(&expected), "{err}"); } // Clean up the plant; then the normal path works again. - // SAFETY: a valid C string naming the object planted above. - unsafe { libc::shm_unlink(c.as_ptr()) }; + drop(plant); let r = SharedRegion::create(&name, 4096).expect("a fresh create works"); assert!(r.created()); assert!(SharedRegion::open(&name).is_ok(), "and so does an open"); diff --git a/docs/SALVAGE_MANIFEST.md b/docs/SALVAGE_MANIFEST.md index 22101580..36cce7b4 100644 --- a/docs/SALVAGE_MANIFEST.md +++ b/docs/SALVAGE_MANIFEST.md @@ -364,3 +364,35 @@ ladder worktree copies; `live/turn1.json`, a short-lived TURN credential that must not be kept. Earlier the same evening, 10 GB of this session's regenerable build output (a cargo target directory and Verilator `obj_dir` trees) was deleted to recover a full `/tmp`. + +## 2026-10-23 — session f63c2db0, after the v3.5.0 release + +Scope set by the maintainer: this session's scratch, curated tier. The dry run +found 60,331 files (4.9 GB) in the session scratchpad. The cut was made by +kind, not by name: build output (cargo `target` / `base-target` trees, `.o`, +`.dwo`, `.bc`, `.rmeta`, Verilator `obj_dir`, wasm `dist-*`), the CodeQL +bundle, databases and repository copies (2.5 GB), downloaded release binaries +and `.rbf` copies (on the GitHub releases and in `salvaged/evidence/v3.5.0-quartus/`), +and any PNG, snapshot, CSV or binary over 256 KB, which takes the golden IRQ +CSVs (`VERIFY=1` rebuilds them from the pinned oracle) and the 137 MB DUT dump. +**Moved: 7,395 files (166 MB)** into `salvaged/tmp-scratch/f63c2db0/`, relative +paths kept, no destination existed, no errors: + +- `v350/` (7,267): the v3.5.0 agent and merge work. Oracle worktree scratch + (`ora-mmc5irq`, `ora-desktop`, `ora-script`, `ora-movie`, `ora-mcp`, + `ora-small`), sibling board work (`sib-mmc5`, `sib-namco-vrc6`, + `sib-fcg-vrc7`, `sib-tier`, with the generated stimulus `.nes` files), + `m5det` / `m5merge` / `mmc5eq` (the MMC5 detector change and merge), + the mutation runs, gate logs, review replies for #624 and #69, the seed-sweep + logs in `quartus/`, and the cut, review and PR-body drafts. +- loose files (90) and `mister/`, `v341/`, `cfdocs/`: v3.4.1 and v3.4.2 notes, + merge logs, CI workflow experiments, and a Worker tail record (no credential). + +Checked for credentials before writing this entry: the only hits are repository +copies of the hosted service's docs and tests, Cloudflare's own credential +documentation, and the tail record above. + +**Left in /tmp:** 53,776 files (5.1 GB) of the kinds above; the 181 +background-task output logs; the VeridianOS agent slug (37 files) and the +`rusty2600-target`, `spectre-target` and `octollm-target` build trees, all of +which belong to other projects; the zellij log and harness cache-state JSON. diff --git a/docs/agents/mister-cosim.md b/docs/agents/mister-cosim.md index 1ba6f598..83288b2e 100644 --- a/docs/agents/mister-cosim.md +++ b/docs/agents/mister-cosim.md @@ -40,6 +40,7 @@ - **THE ELEVEN-VARIANT SWEEP CLOSED (v2.6.23): `chrram-live` GREEN, 32,861 -> 0 PIXELS.** The axis the sweep missed (recorded above) was what a `$2007` access during rendering COMPUTES FROM, not what it computes. On silicon the access pulses the rendering pipeline's existing load rather than performing its own increment. Three changes, each exposing the next: `v_pipeline` (increment the value *this dot's* pipeline load produced, 21,941 -> 2,646 diverging fetches), `v_acc_addr` (RESOLVE the access against that same value, -> 18, which stopped 33 nametable cells being written one coarse-X cell early), and `chr_wr_addr` sourced from it too (-> 0 pixels). `chr_wr` asserts 4,389 times before and after — same stimulus. The `chr_wr_addr` step is a MAINTAINER DECISION (ADR 0037 territory: the write address the oracle now follows is a case the wiki declares undefined), taken knowingly and kept in the RTL comment marked resolved-by-decision rather than deleted, so it governs the next case like it. `chrram-fetch` stays `run_xfail` at 18 of 185,725 — all garbage sprite-window nametable fetches (dots 257-320) whose data is discarded, which is why the picture is byte-perfect while the trace is not. - **GOLDEN RECIPES: 97 OF 110 MANIFESTS, GENERATED NOT TRANSCRIBED, AND 472 ARTIFACTS BYTE-FOR-BYTE (v2.6.23).** `tb/gen_golden_recipes.py` derives each recipe row from fields the manifest already carries (`frames_req`, `ckpt_interval`, `obs_count` sizing `--irq-trace`, `press_start`), because 110 hand-transcribed caps is 110 silent ways to under-cover a golden. `VERIFY=1 tb/fetch-goldens.sh` regenerates into scratch and diffs against `goldens/` WITHOUT writing to it — **472 artifacts byte-for-byte** across goldens recorded as rustynes 2.5.1 through 2.6.22, fifteen releases. **Every apparent difference across four diff passes was a recipe defect, never oracle drift**: a `.irq.csv` diagnostic nothing reads; a guessed `--apu-trace` cap the exporter correctly REFUSED rather than silently under-covering; eleven blargg stems producing nothing because the exporter names artifacts after the ROM FILE's stem, not the target stem; and AccuracyCoin differing in 11.4% of its framebuffer with a MATCHING rom hash — actually a missing `--press-start`, caught by `accuracycoin_status` refusing to score a vacuous run ("every one of the 144 SCORED entries is NotRun"). The ROM is located by `rom_sha256` (content), never the manifest's unusable `rom` path (relative / `../../../`-relative / absolute-under-$HOME / pointing into deleted `/tmp/blargg-apu/` depending on the stem). - **FITTER SEED 4 PINNED ON BEST-OVERALL, NOT STRICT BINDING-MARGIN (v2.6.23).** RTL changed, so by `RustyNES.qsf`'s own rule the prior sweep described a design that no longer exists. Re-swept, 5 seeds, all closing; hold binds at every seed (0.078-0.115ns vs setup's 0.247-0.421ns), so the established rule mechanically selects seed 1 (+0.115 hold). Maintainer decision pinned seed 4 instead (+0.112 hold, +0.421 setup) on BEST OVERALL — within 3ps of seed 1 on hold while beating it by 174ps on setup — with the reasoning AND the falsifying experiment (re-fit one seed repeatedly, measure run-to-run variance; not yet done) written into the `.qsf` alongside the pinned seed, not left implicit. Marked `<- pinned`, not `<- published`, since no bitstream had been built from it yet at write time. Seed 5 — the PREVIOUS release's outright winner — is joint-worst on this RTL: a sweep result belongs to one specific RTL, never carries forward. +- **THE SEED RULE IS THE ONE IN THE `.qsf`, NOT THE ONE A SCRIPT PRINTS (v3.5.0).** `scripts/seed-sweep.sh`'s footer still printed the pre-v2.6.23 rule ("maximise the BINDING margin"), and the v3.5.0 pin followed the printout: seed 3, 4 ps ahead of seed 2 on on-die hold, over seed 2's 132 ps more on-die setup and its far better headline off-die result (+0.224 / +0.088 against +0.027 / +0.081 in the 2026-10-22 sweep, before the review fixes; the released seed-2 rebuild of 2026-10-23 measured +0.380 / +0.081). CodeRabbit caught it on sibling #69, after the seed-3 pair had been on the public oracle release for about four hours; both releases were rebuilt and re-uploaded at seed 2. The script now prints the recorded criterion (sibling `800b1d6`). **Read the criterion from the qsf's v2.6.23 entry**, and treat any tool that restates a rule as a copy that can go stale. - **AN AUDIT'S "THE RTL DISAGREES WITH THE ORACLE" DOES NOT SAY WHICH SIDE IS WRONG (v2.8.2).** RTL audit R-3.5a said `cart.sv` checked MMC1's reset bit before the consecutive-write filter "while the oracle filters first", and called the RTL the defect. nesdev MMC1 says a reset is never ignored and only a DATA write on the cycle after a write is, so the ORACLE was wrong (`m001_mmc1.rs`, fixed red-first in v2.8.2) and the RTL was left alone. The v2.7.0 pulse-1 sweep finding had the same shape. Before fixing either side of a disagreement, read the wiki for the rule itself; the audit ledger now has an INVERTED verdict for this case (`docs/audits/README.md`). - **A DRIFTING LOOP REACHES A DOT ONLY IF ITS PERIOD IS COPRIME WITH THE FRAME (v2.8.2).** The first `ppulatch072` draft, an 11-cycle `$2002`/`$2000` loop, is 33 dots, and 11 divides the 89,342-dot NTSC frame (2·11·31·131), so the loop reaches only 3 of 33 landing residues and could miss the VBL set dot forever. A `NOP` makes it 13 cycles (39 dots, coprime), and 39 frames cover every dot. Coverage was then read from the oracle's `irq.csv` (reads at scanline 241 dots 0-4, dot 1 twice), not assumed. The reverse case is `apureload042`'s: a walk resolves a residue class, not a cycle, so a one-cycle target inside a long period needs a PLACED write instead. - **`release-rbf.sh`'S STALENESS CHECK READS MTIMES, AND A BRANCH SWITCH REWRITES THEM (v2.8.2).** After the sibling's v2.8.2 PR merged, `git switch main && git pull` touched `rtl/apu2a03.sv`, `rtl/cart/cart.sv` and `rtl/ppu2c02.sv`, and `scripts/release-rbf.sh v2.8.2 --no-build` then refused: "RustyNES.sta.rpt is OLDER than 3 source file(s)". The content was identical -- `git diff b09ee32 927ef4a -- rtl/` (the compiled commit against the merge) was 0 lines -- so the report matched the tree and only the timestamps moved. The assets were uploaded with the script's own fallback command, `gh release upload v2.8.2 releases/.rbf --clobber`. The check is right to be conservative; the lesson is ORDER: stage and upload from the PR branch before switching, or prove content identity against the WORKING TREE before overriding it, never by argument: `git diff -- rtl/` (commit against working tree, so staged and unstaged edits count) must be empty AND `git status --porcelain -- rtl/` must print nothing (an untracked RTL file is an input too). `git diff HEAD` compares two commits and would miss an uncommitted edit (CodeRabbit on #559). diff --git a/to-dos/ROADMAP.md b/to-dos/ROADMAP.md index e0ee106a..35bfccf4 100644 --- a/to-dos/ROADMAP.md +++ b/to-dos/ROADMAP.md @@ -1476,6 +1476,9 @@ re-minted: `T-PS-dual-runahead` (ADR 0032; **DONE v3.1.0**), `T-MISTER-SAVESTATE | `T-RA-HARDCORE-RESUME` | Android's auto-resume of the on-background state goes through `load_state`, so in hardcore it is refused and the game starts from power-on; RA asks a resumed session to drop to casual instead. A maintainer decision: keep the refusal, or resume and drop to casual | v3.4.0's audit | a maintainer decision | | `T-NETPLAY-MESH-NATIVE` | Native 3-4 player netplay over `mesh_net.rs`, then mobile. **Desktop DONE v3.4.0** (direct IP and LAN, over the authenticated mesh with the Four Score); mobile (MOB-09) follows | NET-01, MOB-09 | v3.4.0 | | `T-MOVIE-INPUT-STREAM` | The Zapper, microphone, Vs. coins and FDS events in `.rnm`. **DONE v3.5.0** (`.rnm` format 7, `rustynes_core::ExtraInput`; every expansion device and Datach scans too; netplay unchanged) | CR-03 | v3.5.0 | +| `T-MMC5-LATCH-VBLANK` | The MMC5's split-screen and ExGrafix CHR bank latches survive VBlank (found in the v3.5.0 sibling review). In the oracle, `split_chr_bank_latch` and `ex_chr_bank_latch` are cleared only by the next nametable fetch or a `$5104` write, and the MiSTer core matches it, so a `$2007` CHR read during VBlank uses the last rendered tile's bank. The NESdev MMC5 page suggests hardware may not; settle it from the page and a test ROM, not from either core. Gate: a stimulus that reads CHR through `$2007` in VBlank after a split or ExGrafix scanline, with the expected bank stated from documentation, passing in both cores. The same change corrects `mmc5.sv`'s scanline-detection comment ("the read after three matching nametable reads"), left alone at v3.5.0 so the tagged tree matched the compiled one | (#69 review) | v3.5.1 | +| `T-WIN-SCRIPT-IPC-CI` | The OS shared-memory store's Windows and Apple code was compiled by `full-build` and tested by no CI job (found v3.5.0): the `test` matrix reaches Windows and macOS only on a full run and without `script-ipc`. **Wired 2026-10-23** (the v3.5.0 post-release PR): both `full-build` legs (Windows and macOS) run the `shm::`, `script_host::` and `memif::` tests on the `full` build, each group must show a passing test, and Windows must pass the grow test by name. Closes when that step has passed once on both | (#624 review) | v3.5.1 | +| `T-V350-REVIEW-RESIDUE` | Antigravity suggestions on #624 and #69 not taken at v3.5.0, each to be confirmed or refuted against the code: a device key's latched state across a rebind; `lua_eval` from the MCP server while a real-time run is going; reads of user-chosen ROM, patch and splits paths with no size bound; and in the sibling, the dead `x_hit_q` register and the `nt_ovr` / IRQ outputs not gated by the board select. Gate: each item closed with a failing test and its fix, or with the reason it does not apply | (#624, #69 review) | v3.5.1 | | `T-LIBRETRO-OPTIONS-V2` | Core Options v2 and float-audio negotiation, behind capability checks | ecosystem 3 | v3.6.0 | | `T-LIBRETRO-CONSOLES` | Switch, PS Vita and 3DS investigated for the libretro core (D28) | LR-04 | v3.6.0 | | `T-ANDROID-API37` | `ACCESS_LOCAL_NETWORK` for LAN netplay, lifecycle-safe background audio, `targetSdk` 37 | ecosystem 5 | v3.6.0 |