diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e75f23c..33246636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,93 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **AccuracySNES: the Mesen2 oracle is FIXED — one `emu.setInput` call too many.** The battery has + never run under MesenCE; it now completes: `magic='ACSN'`, `R_DONE=$A5`, **335/335** status bytes + written, and `crossval.sh` reports `Mesen2: 1 failing test(s)` where it previously timed out. + + **Root cause.** In this MesenCE build's `--testRunner`, `emu.setInput`'s port argument does **not** + select a controller — indices 0, 1 and 2 all land on **controller 1** (verified through the cart's + own `V_PAD_HELD`). `mesen_crossval.lua` called it twice, so the second call, intended for port 2, + **overwrote port 1**: the cart saw `PAD2_CONTRACT` (`$60A0`) on controller 1. `$60A0` contains no + Start, and the pre-battery menu waits for Start — so the cart booted, ran init, cleared `R_STATUS`, + reached the menu and sat there forever, behind a static picture that showed nothing. + + Every recorded symptom follows from that one line, and several earlier conclusions were wrong + because of it: the "completes 14 of 335 and stops at `A3.03`" figure was an all-zero status array + misread, and two cart-side `A3.03` fixes were aimed at a layer that was never at fault. + + **Stated, not hidden:** port 2 cannot be driven from Lua in this build, so rows depending on + `PAD2_CONTRACT` are not cross-validated by this runner. The in-repo harness and the snes9x libretro + driver drive both ports, so those rows remain covered elsewhere. + + **The scene half is fixed too, and it was the same class of bug.** `mesen_scenes.lua` held **no + input contract at all** — no `setInput` anywhere — so the cart never left its pre-battery menu + there either, never ran the battery, and never reached the scene loop that follows it. The symptom + read as a scene-loop problem and was an input problem. With one port-1 call added: + + ``` + snes9x: 53 scene(s) match, 0 unblessed, 0 mismatched + Mesen2: 53 scene(s) match, 0 unblessed, 0 mismatched + ``` + + **Both halves of the oracle now arbitrate.** `docs/adr/0013` requires a golden be blessed only from + a render the references agree on, and for the first time both references produce one — which is the + gate the `v1.29.0` scene work has been waiting behind. + + What remains is no longer an oracle failure but a **finding**: Mesen2 reports 1 failing test, + catalogue index 11 (`A2.10`), while snes9x reports only its 14 known divergences. RustySNES failing + alone is this project's signature for a real bug rather than a broken test — worth checking which + before investigating, per the standing heuristic. + +- **AccuracySNES oracle: the recorded diagnosis does not reproduce, and the blocker is a different + one.** Re-measured with a new, deliberately minimal second instrument + (`scripts/accuracysnes/mesen_wram_probe.lua`) — every prior conclusion rested on + `mesen_crossval.lua`'s own read, with nothing independent checking that read. + + Three findings, two of which overturn what was written down: + + - **MesenCE headless is not broken.** It renders the entire undisbeliever corpus with **0 skipped + captures** and runs the AccuracySNES cart to exit 0. Treating "the Mesen oracle" as a single + broken thing was wrong — rendering works; only the battery read does not. The scene work is + therefore **not** blocked by whatever blocks the battery. + - **The render channel cannot observe the battery.** MesenCE and RustySNES produce byte-identical + colour histograms for the cart at 60/300/500/800/900 frames, with and without the input contract + held, and the picture is static throughout in both. A hang and a clean run look the same, so any + diagnosis has to go through WRAM. + - **The battery writes no verdict at all — not 14 of 335.** At `RESULTS = $7E:F000` the magic reads + as bytes that *differ between runs* (uninitialised WRAM, never written) and `R_STATUS[0..39]` are + all zero at both 600 and 1500 frames. The "completes 14 then stops at `A3.03`" figure does not + reproduce — which explains why two cart-side `A3.03` fixes moved nothing: they were chasing a + number that was not there. + + The open question is now **"why does the battery never start"**, not "why does it stop". + + The first suspect — that the contract's constant hold from power-on denies a menu its press edge — + was tested and **refuted**: applying the contract from frame 0, 30 or 120 gives the same result at + 1200 frames, as does declaring `--snes.port1.type=SnesController` explicitly. + + A "24 non-zero bytes" lead recorded mid-investigation was **my own over-read** — those bytes sit at + indices 336-359, past the end of a 335-entry array — and is retracted. The real signal is the + reverse: `R_STATUS[0..334]` is **zero** while the surrounding WRAM is **random**, and since MesenCE + powers on with randomised WRAM, an all-zero array is not the power-on state. Something zeroed it — + `runtime.s`'s own clear loop — so **the cart's init runs and reaches the pre-battery menu**. The + precise statement is not "the battery never starts" but "the cart never leaves its menu", whose + exit condition the source names as *wait for Start*. + + Two further suspects were raised and both refuted by measurement. **Auto-joypad:** at frame 300 + `NMITIMEN` reads `$42` — bit 0 clear, auto-read *disabled* — so `$4218`-`$421F` reading `0000` is + correct, not a fault; the cart states at `runtime.s:9` that it reads `$4016` **manually**. + **Software edge:** the menu computes `V_PAD_NEW = HELD & ~LAST` and seeds both on entry, so a + held button yields NEW = 0 forever — but pressing Start at frame 300 or 600 of a 2000-frame run + changes nothing either. + + What remains is a **harness-side** conclusion: `emu.setInput` from an `inputPolled` callback does + not appear to reach the cart's manual `$4016` strobe read under `--testRunner`. Every host-side way + of supplying the press has been tried and the cart observes none, while its init demonstrably runs. + The in-repo harness and the snes9x libretro driver both work, so this is MesenCE-runner input + plumbing, not contract design. Next step: a minimal ROM that only reads `$4016` and stores it, + to test the channel in isolation. Four probes are committed so none of this is re-run. + - **PPU: the field flag's doc contradicted the code, and nothing pinned either.** `Ppu::field` is `$213F` bit 7 and toggles at the end of **every** frame; its doc comment said "toggles each frame when interlace is on", which describes neither the code nor the hardware. Only the flag's *use* is diff --git a/docs/accuracysnes-plan.md b/docs/accuracysnes-plan.md index 6bd73d50..7c9a1d23 100644 --- a/docs/accuracysnes-plan.md +++ b/docs/accuracysnes-plan.md @@ -626,6 +626,131 @@ threaded to it. Expect goldens to move, and note that a previous attempt at the mapping change was reverted for exactly that reason — so the guard test named in the roadmap lands **first**, before `dot_length` is touched. +#### Group E — the batch to author next, picked against what the oracle can adjudicate + +Chosen 2026-08-01, now that both halves of the Mesen2 oracle arbitrate. Two corrections to the +`v1.29.0` scoping first, both material: + +1. **"34 rows need new SPC700 emitters" overstates it.** `gen/src/tests/apu.rs` already has + `dsp_write` (3309) and `dsp_read_to` (3315), and `apu_upload`/`release_to_ipl` are in place. Any + row whose observable is a **DSP register** therefore needs no new opcode encoding at all — the + emitter gap is real only for timer/GAIN/echo-coefficient *opcodes*, not for DSP access. +2. **Pick DSP-observable rows first**, because they are the ones the working oracle can arbitrate: + a verdict byte is scored on-cart and now cross-checked by MesenCE and snes9x together. + +**The batch, in order:** + +| row | assertion | why first | +|---|---|---| +| `E8.01` | KON/KOFF polled every **second** sample (16 kHz) `[ERRATA]` | pure DSP-register observable; the errata flag means the reference is explicit, and a 1-sample granularity is measurable through ENDX/ENVX without new opcodes | +| `E9.02` | noise output is **highpass-filtered** `[ERRATA]` | DSP-observable via OUTX on a NON voice; errata-flagged, so the expected behaviour is stated rather than inferred | +| `E5.06` | BRR 15-bit wrap: clamp to 16 bits, then `+4000h..+7FFFh → -4000h..-1` | already has decoder scaffolding from the landed `E5` rows | + +**Explicitly NOT first: `E1.11`** ("TSET1/TCLR1 read the target twice"). Inspected and set aside as a +likely **vacuous** row: the second read is a dummy, and on a `$FD`-`$FF` timer-output target the +first read has already cleared the counter, so the second observes and changes nothing. The timer +cannot increment between two reads six cycles apart. Unless a distinguishing observable is found +first, this is the shape [[accuracysnes-vacuous-test-trap]] exists to prevent — design the observable +before writing the row. + +**Before authoring any of them,** settle `A2.10` above. It is the only unexplained reference +disagreement in the battery, and a row batch authored while one row is unexplained risks attributing +a new failure to the wrong cause. + +#### First arbitrated finding: `A2.10` — Mesen2 is the outlier, 2-vs-1 + +The moment the oracle started working it produced a result the project has never been able to see. +`crossval.sh` reports `Mesen2: 1 failing test(s)`, catalogue index **11 = cart `A2.10`, "PEI does not +page-wrap"** (scored; source: WDC datasheet + superfamicom.org addressing notes; verdict slot +`$7EF02B`). + +RustySNES **passes** it and snes9x passes it too — `A2.10` is not among snes9x's 14 known +divergences. So this is **2-vs-1 with Mesen2 as the outlier**, which inverts the usual shape: the +standing heuristic warns that *RustySNES failing alone* means a real bug, and that is not what this +is. + +**Do not yet record it as a known-good Mesen2 divergence.** That would need a third opinion — ares or +bsnes on the same row — and the heuristic's own caveat applies with force here: a harness bug +upstream of an implementation produces exactly this signature, and one already did once in this +project (the `$F8`/`$F9` retraction). The cheap next step is to run the row against ares or bsnes. If +they agree with RustySNES and snes9x, add a `MESEN2_KNOWN_FAILURES` entry mirroring the snes9x one, +with the rationale, so `crossval.sh` reports AGREEMENT instead of `DISAGREEMENT` and stops masking +future real divergences behind a known one. + +Until then `crossval.sh`'s `DISAGREEMENT` verdict is **correct and should stay** — one genuinely +unexplained row is exactly what it exists to report. + +#### The Mesen oracle — the "14 of 335" reading does not reproduce + +Re-measured 2026-08-01 with a second, independent instrument +(`scripts/accuracysnes/mesen_wram_probe.lua`), because every prior conclusion rested on +`mesen_crossval.lua`'s own read with nothing checking that read. + +Established, and some of it overturns what was recorded here: + +- **MesenCE headless is not broken.** It renders the whole undisbeliever corpus + (`scripts/perdot_crossval.sh`, **0 skipped captures**) and runs the AccuracySNES cart to exit 0. + Notes treating "the Mesen oracle" as one broken thing were wrong: rendering works, and only the + battery read does not. Two problems, not one — which also means the scene work is **not** blocked + by whatever blocks the battery. +- **MesenCE and RustySNES render the cart byte-identically** (same colour histogram) at 60, 300, 500, + 800 and 900 frames, with and without the input contract held, and the picture is static throughout + in both. So **the render channel cannot observe the battery at all** — a hang and a clean run look + identical. Any diagnosis must go through WRAM. +- **The battery writes no verdict at all — not 14.** At `RESULTS = $7E:F000` (snesWorkRam offset + `$F000`, the address `runtime.inc` defines and `mesen_crossval.lua` already uses), the magic reads + as bytes that **differ between runs** — uninitialised WRAM, never written — and `R_STATUS[0..39]` + are **all zero** at both 600 and 1500 frames. "Completes 14 of 335 and stops at `A3.03`" does not + reproduce, and the two `A3.03` hypotheses chased earlier were chasing a number that is not there. + +The question is therefore no longer "why does it stop at `A3.03`" but **"why does the battery never +start under MesenCE"** — a different search, and one that explains why two cart-side fixes moved +nothing. + +**The press-edge suspect is REFUTED.** `scripts/accuracysnes/mesen_edge_probe.lua` applies the +contract from frame 0, 30 or 120 — the latter two manufacturing a rising edge — and all three give +the same result at 1200 frames: the magic never reads `ACSN`, `R_DONE` never reads `$A5`, and +`R_STATUS` holds exactly **24** non-zero bytes. The battery is not waiting for an edge. The probe is +committed so the next attempt does not re-run a settled negative. + +**RETRACTED: the "24 non-zero bytes" was my own over-read.** They sit at indices **336-359**, past +the end of a 335-entry array — I read 360 bytes and mistook what follows the array for a signal. + +**The real signal is the opposite one: `R_STATUS[0..334]` is ZERO while the surrounding WRAM is +random.** MesenCE powers on with randomised WRAM (the header at `$F000` reads differently every +run), so an all-zero status array is not the power-on state — **something zeroed it**. That +something is `runtime.s`'s own clear loop, which means **the cart's init runs and reaches the +pre-battery menu**. It is not dead at reset, and "the battery never starts" is more precisely "the +cart never leaves its menu". + +The menu's exit condition is named in the source immediately below that loop: *"wait for Start to +begin the battery"*. Two ways of supplying it are already ruled out — a rising edge +(`mesen_edge_probe.lua`, frames 0/30/120 identical) and declaring `--snes.port1.type=SnesController` +explicitly alongside port 2. + +**The auto-joypad suspect is also refuted, by reading the registers.** At frame 300 under +`--testRunner`, `NMITIMEN ($4200) = $42` — **bit 0 clear**, auto-joypad *disabled* — and +`$4218`-`$421F` all read `0000`, which is the correct value when it is off, not a fault. The cart +says so itself: `runtime.s` line 9, *"Controller input is read MANUALLY through `$4016`, not via +auto-joypad"*, and line 570 `stz NMITIMEN ; ... (we read $4016 by hand)`. + +**The software-edge theory is refuted too, at every timing tried.** The menu computes `V_PAD_NEW` as +`HELD & ~LAST` and seeds both to the same value on entry, so a button already down yields NEW = 0 +forever — which looked like the answer. But pressing Start at frame 300 or 600 (well past any +plausible menu entry, 2000-frame runs) changes nothing. + +**What remains, and it is a harness-side conclusion rather than a cart-side one:** `emu.setInput` +from an `inputPolled` callback does not appear to reach the cart's **manual `$4016` strobe read** +under `--testRunner`. Every host-side way of supplying the press has now been tried and the cart +observes none of them, while the cart's own init demonstrably runs. The next step is therefore to +verify the channel itself — drive a *minimal* ROM that does nothing but manually read `$4016` and +write the result to WRAM, and see whether `emu.setInput` reaches it at all. If it does not, the fix +belongs in how the runner supplies input, and no amount of cart-side change will help. + +Worth noting for whoever picks this up: **the in-repo harness and the snes9x libretro driver both +work**, so this is specifically a MesenCE-runner input-plumbing problem, not a contract design +problem. + #### `A5.18` — why it is parked despite working `BRK` cannot be measured without `RTI`: a `BRK` that never returns cannot be repeated, so any diff --git a/scripts/accuracysnes/mesen_crossval.lua b/scripts/accuracysnes/mesen_crossval.lua index 36c0488a..be9080c9 100644 --- a/scripts/accuracysnes/mesen_crossval.lua +++ b/scripts/accuracysnes/mesen_crossval.lua @@ -89,15 +89,31 @@ end -- nothing held, so every runner holds the same mask and the cart asserts against it. Mesen2's docs -- are explicit that setInput belongs in an inputPolled callback, since otherwise the state may not -- be applied before the ROM reads it. +-- +-- ONE setInput call, deliberately. This is what made the battery never run under MesenCE for +-- several sessions, and it is worth stating exactly, because the failure was silent and total: +-- +-- In this MesenCE build's --testRunner, the port argument does NOT select a controller. Sending +-- PAD_CONTRACT to index 0 and again to index 1 both land on CONTROLLER 1 (verified: the cart's own +-- V_PAD_HELD reads $9050 either way; sending to index 2 does the same). So the second call here -- +-- intended for port 2 -- OVERWROTE port 1, and the cart saw PAD2_CONTRACT ($60A0) on controller 1. +-- +-- $60A0 contains no Start. The pre-battery menu waits for Start. So the cart booted, ran its init, +-- cleared R_STATUS, reached the menu and sat there forever -- with nothing on screen to show it, +-- since the menu is a static picture. Every symptom followed from that: no ACSN magic, R_DONE +-- never $A5, and an all-zero status array that earlier notes misread as "completes 14 of 335". +-- +-- With the single call the battery runs to completion: magic ACSN, R_DONE $A5, 335/335 status bytes +-- written. Do not "restore" the port-2 call without first proving the port argument works. +-- +-- CONSEQUENCE, stated rather than hidden: port 2 cannot be driven from Lua in this build, so rows +-- that depend on PAD2_CONTRACT ($60A0: Y + Select + A + L) are not cross-validated by this runner. +-- The in-repo harness and the snes9x libretro driver do drive both ports, so those rows are still +-- covered -- just not by MesenCE. local function onInput() emu.setInput({ b = true, start = true, x = true, r = true, y = false, select = false, a = false, l = false, up = false, down = false, left = false, right = false }, 0) - -- Port 2 (PAD2_CONTRACT = $60A0: Y + Select + A + L). Needs a device in port 2, which the - -- testrunner invocation supplies via --snes.port2.type=SnesController (see crossval.sh). - emu.setInput({ y = true, select = true, a = true, l = true, - b = false, start = false, x = false, r = false, - up = false, down = false, left = false, right = false }, 1) end emu.addEventCallback(onInput, emu.eventType.inputPolled) diff --git a/scripts/accuracysnes/mesen_edge_probe.lua b/scripts/accuracysnes/mesen_edge_probe.lua new file mode 100644 index 00000000..772248f9 --- /dev/null +++ b/scripts/accuracysnes/mesen_edge_probe.lua @@ -0,0 +1,42 @@ +-- MesenCE probe: does the AccuracySNES battery need a press EDGE on the contract buttons? +-- +-- HOLD_FROM is the frame at which the input contract is first applied, so HOLD_FROM=0 reproduces the +-- constant hold every runner uses and any larger value manufactures a rising edge. +-- +-- RESULT (2026-08-01): REFUTED. HOLD_FROM 0, 30 and 120 all produce the same shape at 1200 frames -- +-- magic never reads ACSN, R_DONE never reads $A5, and R_STATUS has exactly 24 non-zero bytes. The +-- edge is not what the battery is waiting for. Kept so the next attempt does not re-run it. +-- +-- The 24 is the live lead, not the zero: it is CONSTANT across runs while the magic bytes VARY, so it +-- is not uninitialised WRAM (which would vary too), and indices 0-39 read zero, so those bytes sit at +-- index >= 40. Find which indices they are before theorising further. + +local TARGET = tonumber(os.getenv("MCE_FRAMES") or "900") +local HOLD_FROM = tonumber(os.getenv("HOLD_FROM") or "0") -- frame at which the contract is applied +local RES = os.getenv("MCE_RESULT") or "/tmp/edge.txt" +local frames = 0 +local function onInput() + local on = frames >= HOLD_FROM + emu.setInput({ b=on, start=on, x=on, r=on, y=false, select=false, a=false, l=false, + up=false, down=false, left=false, right=false }, 0) + emu.setInput({ y=on, select=on, a=on, l=on, b=false, start=false, x=false, r=false, + up=false, down=false, left=false, right=false }, 1) +end +local function onEndFrame() + frames = frames + 1 + if frames < TARGET then return end + local f = io.open(RES, "w") + local magic = "" + for i = 0, 3 do magic = magic .. string.format("%02x", emu.read(0xF000 + i, emu.memType.snesWorkRam)) end + local done = emu.read(0xF008, emu.memType.snesWorkRam) + local nonzero = 0 + for i = 0, 359 do + if emu.read(0xF020 + i, emu.memType.snesWorkRam) ~= 0 then nonzero = nonzero + 1 end + end + f:write(string.format("hold_from=%d frames=%d magic=%s done=%02x status_nonzero=%d\n", + HOLD_FROM, frames, magic, done, nonzero)) + f:close() + emu.stop(0) +end +emu.addEventCallback(onInput, emu.eventType.inputPolled) +emu.addEventCallback(onEndFrame, emu.eventType.endFrame) diff --git a/scripts/accuracysnes/mesen_pad_held_probe.lua b/scripts/accuracysnes/mesen_pad_held_probe.lua new file mode 100644 index 00000000..e0b44a80 --- /dev/null +++ b/scripts/accuracysnes/mesen_pad_held_probe.lua @@ -0,0 +1,26 @@ +local TARGET = tonumber(os.getenv("MCE_FRAMES") or "600") +local HOLD_FROM = tonumber(os.getenv("HOLD_FROM") or "0") +local RES = os.getenv("MCE_RESULT") or "/tmp/held.txt" +local frames = 0 +local function onInput() + local on = frames >= HOLD_FROM + emu.setInput({ b=on, start=on, x=on, r=on, y=false, select=false, a=false, l=false, + up=false, down=false, left=false, right=false }, 0) + emu.setInput({ y=on, select=on, a=on, l=on, b=false, start=false, x=false, r=false, + up=false, down=false, left=false, right=false }, 1) +end +local function onEndFrame() + frames = frames + 1 + if frames < TARGET then return end + local function w(off) -- VAR_BASE $7E:E000 -> snesWorkRam offset $E000 + return emu.read(0xE000 + off, emu.memType.snesWorkRam) + | (emu.read(0xE000 + off + 1, emu.memType.snesWorkRam) << 8) + end + local f = io.open(RES, "w") + f:write(string.format("hold_from=%d frames=%d V_PAD_HELD=%04x V_PAD_LAST=%04x V_PAD_NEW=%04x\n", + HOLD_FROM, frames, w(4), w(6), w(8))) + f:close() + emu.stop(0) +end +emu.addEventCallback(onInput, emu.eventType.inputPolled) +emu.addEventCallback(onEndFrame, emu.eventType.endFrame) diff --git a/scripts/accuracysnes/mesen_pad_probe.lua b/scripts/accuracysnes/mesen_pad_probe.lua new file mode 100644 index 00000000..dca7259c --- /dev/null +++ b/scripts/accuracysnes/mesen_pad_probe.lua @@ -0,0 +1,26 @@ +local TARGET = tonumber(os.getenv("MCE_FRAMES") or "300") +local RES = os.getenv("MCE_RESULT") or "/tmp/pad.txt" +local frames = 0 +local function onInput() + emu.setInput({ b=true, start=true, x=true, r=true, y=false, select=false, a=false, l=false, + up=false, down=false, left=false, right=false }, 0) + emu.setInput({ y=true, select=true, a=true, l=true, b=false, start=false, x=false, r=false, + up=false, down=false, left=false, right=false }, 1) +end +local function onEndFrame() + frames = frames + 1 + if frames < TARGET then return end + local f = io.open(RES, "w") + -- Auto-joypad results ($4218-$421F) and the enable bit ($4200 bit0), read as CPU registers. + local function reg(a) return emu.read(a, emu.memType.snesMemory) end + f:write(string.format("frames=%d NMITIMEN($4200)=%02x\n", frames, reg(0x4200))) + f:write(string.format("JOY1=%02x%02x JOY2=%02x%02x JOY3=%02x%02x JOY4=%02x%02x\n", + reg(0x4219), reg(0x4218), reg(0x421B), reg(0x421A), + reg(0x421D), reg(0x421C), reg(0x421F), reg(0x421E))) + -- What the cart itself believes: the runtime keeps the merged pad somewhere in VAR_BASE, but the + -- register read above is the decisive one -- it is what the menu polls. + f:close() + emu.stop(0) +end +emu.addEventCallback(onInput, emu.eventType.inputPolled) +emu.addEventCallback(onEndFrame, emu.eventType.endFrame) diff --git a/scripts/accuracysnes/mesen_scenes.lua b/scripts/accuracysnes/mesen_scenes.lua index 5f00826b..1c523ba8 100644 --- a/scripts/accuracysnes/mesen_scenes.lua +++ b/scripts/accuracysnes/mesen_scenes.lua @@ -154,4 +154,21 @@ local function onFrame() end end +-- The host input contract, which this script was missing ENTIRELY -- which is why it reported "no +-- scenes at all": with nothing held the cart never leaves its pre-battery menu (that menu waits for +-- Start), so it never runs the battery and never reaches the scene loop that follows it. The symptom +-- read like a scene-loop problem and was an input problem, the same one `mesen_crossval.lua` had in +-- a different form. +-- +-- ONE call, port index 0. In this MesenCE build's --testRunner the port argument does not select a +-- controller -- 0, 1 and 2 all land on controller 1 -- so a second call for port 2 would OVERWRITE +-- this one with PAD2_CONTRACT ($60A0), which contains no Start. See `mesen_crossval.lua`'s comment +-- for the full account; do not add a port-2 call here without first proving the argument works. +local function onInput() + emu.setInput({ b = true, start = true, x = true, r = true, + y = false, select = false, a = false, l = false, + up = false, down = false, left = false, right = false }, 0) +end + +emu.addEventCallback(onInput, emu.eventType.inputPolled) emu.addEventCallback(onFrame, emu.eventType.endFrame) diff --git a/scripts/accuracysnes/mesen_status_index_probe.lua b/scripts/accuracysnes/mesen_status_index_probe.lua new file mode 100644 index 00000000..7b573653 --- /dev/null +++ b/scripts/accuracysnes/mesen_status_index_probe.lua @@ -0,0 +1,40 @@ +-- MesenCE probe: WHICH R_STATUS indices are non-zero, and the raw RESULTS header. +-- +-- Written to check a "24 non-zero bytes" reading that turned out to be an over-read: those bytes are +-- at indices 336-359, past the end of the 335-entry array. Reading a byte count without reading the +-- indices is how that went unnoticed for a whole round -- hence this probe. +-- +-- What it establishes (2026-08-01): R_STATUS[0..334] is entirely ZERO while the RESULTS header at +-- $F000 is RANDOM and differs every run. MesenCE powers on with randomised WRAM, so an all-zero +-- array is not the power-on state -- runtime.s's own clear loop zeroed it, which means the cart's +-- init runs and it reaches the pre-battery menu. The open question is why it never leaves that menu, +-- whose exit condition the source names as "wait for Start". + +local TARGET = tonumber(os.getenv("MCE_FRAMES") or "1200") +local RES = os.getenv("MCE_RESULT") or "/tmp/idx.txt" +local frames = 0 +local function onInput() + emu.setInput({ b=true, start=true, x=true, r=true, y=false, select=false, a=false, l=false, + up=false, down=false, left=false, right=false }, 0) + emu.setInput({ y=true, select=true, a=true, l=true, b=false, start=false, x=false, r=false, + up=false, down=false, left=false, right=false }, 1) +end +local function onEndFrame() + frames = frames + 1 + if frames < TARGET then return end + local f = io.open(RES, "w") + local hits = {} + for i = 0, 359 do + local b = emu.read(0xF020 + i, emu.memType.snesWorkRam) + if b ~= 0 then hits[#hits+1] = string.format("%d=%02x", i, b) end + end + f:write("nonzero_indices: " .. table.concat(hits, " ") .. "\n") + -- Also dump the header words so we can see whether ANY of RESULTS was written. + local hdr = {} + for i = 0, 31 do hdr[#hdr+1] = string.format("%02x", emu.read(0xF000 + i, emu.memType.snesWorkRam)) end + f:write("header F000..F01F: " .. table.concat(hdr, " ") .. "\n") + f:close() + emu.stop(0) +end +emu.addEventCallback(onInput, emu.eventType.inputPolled) +emu.addEventCallback(onEndFrame, emu.eventType.endFrame) diff --git a/scripts/accuracysnes/mesen_wram_probe.lua b/scripts/accuracysnes/mesen_wram_probe.lua new file mode 100644 index 00000000..4ad94572 --- /dev/null +++ b/scripts/accuracysnes/mesen_wram_probe.lua @@ -0,0 +1,50 @@ +-- Minimal MesenCE WRAM probe for the AccuracySNES results block. +-- +-- Exists because the battery oracle's diagnosis had been built on `mesen_crossval.lua`'s own +-- reading, with no independent check that the read itself was sound. This script does one thing -- +-- run N frames with the input contract held, then dump `RESULTS` ($7E:F000, snesWorkRam offset +-- $F000) -- so a claim about how far the battery got can be made from a second, much smaller +-- instrument. +-- +-- MCE_FRAMES=1500 MCE_RESULT=/tmp/w.txt SDL_VIDEODRIVER=offscreen SDL_AUDIODRIVER=dummy \ +-- ref-proj/MesenCE/bin/linux-x64/Release/Mesen --testRunner \ +-- --snes.port2.type=SnesController scripts/accuracysnes/mesen_wram_probe.lua \ +-- tests/roms/AccuracySNES/build/accuracysnes.sfc +-- +-- What it found (2026-08-01): the magic is uninitialised WRAM that DIFFERS between runs, and every +-- status byte in the first 40 catalogue slots is zero at 600 and at 1500 frames. So the battery is +-- not stopping partway -- it never writes a verdict at all. + +local TARGET = tonumber(os.getenv("MCE_FRAMES") or "900") +local RES = os.getenv("MCE_RESULT") or "/tmp/wram.txt" +local frames = 0 +local function onInput() + emu.setInput({ b=true, start=true, x=true, r=true, y=false, select=false, a=false, l=false, + up=false, down=false, left=false, right=false }, 0) + emu.setInput({ y=true, select=true, a=true, l=true, b=false, start=false, x=false, r=false, + up=false, down=false, left=false, right=false }, 1) +end +local function onEndFrame() + frames = frames + 1 + if frames < TARGET then return end + local f = io.open(RES, "w") + -- WRAM offset of $7E:F000 is 0xF000. Dump the magic + a slice of the status array. + local magic = "" + for i = 0, 3 do + magic = magic .. string.format("%02x", emu.read(0xF000 + i, emu.memType.snesWorkRam)) + end + f:write(string.format("frames=%d magic@F000=%s\n", frames, magic)) + local nonzero, first_zero = 0, -1 + local bytes = {} + for i = 0, 359 do + local b = emu.read(0xF020 + i, emu.memType.snesWorkRam) + if b ~= 0 then nonzero = nonzero + 1 elseif first_zero < 0 then first_zero = i end + if i < 40 then bytes[#bytes+1] = string.format("%02x", b) end + end + f:write(string.format("status nonzero=%d first_zero_index=%d\n", nonzero, first_zero)) + f:write("first40=" .. table.concat(bytes, " ") .. "\n") + f:close() + emu.stop(0) +end +emu.addEventCallback(onInput, emu.eventType.inputPolled) +emu.addEventCallback(onEndFrame, emu.eventType.endFrame)