Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Interlace scenes: the recorded Mesen2 nondeterminism is GONE, but interlace is blocked for a
different reason.** Probed and withdrawn. Three consecutive Mesen2 runs and two snes9x runs of an
interlace scene produced identical results, so the standing note that "Mesen2 alternates between
both field parities run-to-run on one build" no longer reproduces — that was almost certainly the
`emu.setInput` defect fixed earlier, and "run any field-dependent capture twice per host" can be
retired as the *reason* interlace is blocked.

The actual blocker is that the two hosts emit different **widths** for the same interlace scene:
snes9x `256x224`, Mesen2 `512x478`. Hi-res worked because both emitted 512 wide and differed only
in height, which `HiResEven` absorbs by deriving the row step from the observed height. Here no
single `(xstep, ystep)` pair describes both reductions, so `extract` as designed — one declared
rule per scene, every host applying the same reduction — is insufficient.
Comment on lines +21 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Describe HiResEven as host-specific row handling.

The entry says HiResEven derives its row step from the observed height. The Rust consumer uses fixed scene rows and does not observe a height-based row step. Mesen2 handles doubled rows in its own host. State that each host applies the declared HiResEven rule with its own validated row mapping.

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

In `@CHANGELOG.md` around lines 21 - 25, Update the HiResEven description in the
changelog to clarify that it is host-specific row handling: each host applies
the declared rule using its own validated row mapping, rather than implying the
Rust consumer derives the row step from observed height. Preserve the existing
explanation of differing host widths and reductions.


`docs/adr/0013` records the three options and argues against the tempting one: a generic
"downsample whatever you get to 256x224" rule would subsume `HiResEven` but give up the
declared-not-inferred property, and would then silently accept a 256-wide frame for a scene that
must be 512.

- **`C10.04` attempted and withdrawn: it is blocked by the same exclusion that unblocked `C5.15`.**
A Mode 5 scene with `MOSAIC = $01` was written against the control the working rules require — the
same canvas with mosaic off — and hashed **identically to it on both references**
Expand Down
40 changes: 40 additions & 0 deletions docs/adr/0013-accuracysnes-framebuffer-oracle.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,43 @@ a state no picture can show; an unshowable scene hashes stably and every emulato
The control scene is what caught it, and its doc comment said in advance what an equal hash would
mean. **Write the control first; it is the only thing that distinguishes "the references agree" from
"nothing happened".**

### Interlace needs a rule the current design cannot express

Probed 2026-08-02, scene withdrawn. Two findings, and the second is the blocker.

**The good news: Mesen2 is no longer nondeterministic here.** The earlier note that "Mesen2
alternates between both field parities run-to-run on one build" no longer reproduces — three
consecutive runs of an interlace scene produced the identical result, as did two of snes9x. That
nondeterminism was almost certainly the `emu.setInput` defect fixed earlier, and the standing advice
to "run any field-dependent capture twice per host" can be retired as the *reason* interlace is
blocked. It is blocked for a different reason.

**The blocker: the two hosts emit different WIDTHS for the same interlace scene.**

| scene kind | snes9x | Mesen2 |
|---|---|---|
| ordinary | `256x224` | `256x239` |
| Mode 5 hi-res | `512x224` | `512x478` |
| **interlace** | **`256x224`** | **`512x478`** |

Hi-res worked because both hosts emitted **512 wide** and differed only in height, which `HiResEven`
absorbs by deriving the row step from the observed height. Interlace does not: snes9x renders a
single field at 256 wide while Mesen2 doubles both dimensions. `HiResEven`'s `width == 512` assertion
cannot hold for both, and no single `(xstep, ystep)` pair describes both reductions.

So `extract` as designed — one declared rule per scene, each host applying the same reduction — is
insufficient for interlace. The options, none of them free:

1. A `Downsample` rule that reduces *whatever* the host emits to 256x224 by deriving both steps from
the observed geometry. General, and it subsumes `HiResEven` — but it gives up the
**declared-not-inferred** property that is the whole reason `extract` catches a core rendering
256 wide when it should render 512.
2. A per-host reduction table, so a rule can say "snes9x: as-is; Mesen2: halve both". Keeps the
declaration honest at the cost of the hosts no longer being interchangeable.
3. Declare the expected geometry per rule *and* per host, and reject anything else — the strictest,
and the most to maintain.
Comment on lines +296 to +303

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 8 \
  'enum Extract|HiResEven|extract|visible_width|SCENE_W|SCENE_H|FIRST_ROW|SCENE_BUF_LEN' \
  --glob '*.rs' --glob '*.lua' --glob '*.c' .

Repository: doublegate/RustySNES

Length of output: 50377


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- ADR 0013 target section ---'
sed -n '240,315p' docs/adr/0013-accuracysnes-framebuffer-oracle.md

printf '%s\n' '--- extraction-related files ---'
git ls-files | rg 'AccuracySNES|accuracysnes|0013|scene|libretro' | head -200

printf '%s\n' '--- host extraction definitions and call sites ---'
rg -n -C 5 \
  'HiResEven|hires-even|FIRST_ROW|SCENE_H|SCENE_W|line.?double|512x478|512x224|256x239|256x224|extract' \
  tests scripts crates docs/adr/0013-accuracysnes-framebuffer-oracle.md \
  --glob '*.rs' --glob '*.c' --glob '*.lua' --glob '*.md' --glob '*.tsv' \
  | head -1000

Repository: doublegate/RustySNES

Length of output: 50379


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- ADR 0013 complete extraction supplement ---'
sed -n '128,205p' docs/adr/0013-accuracysnes-framebuffer-oracle.md
sed -n '268,307p' docs/adr/0013-accuracysnes-framebuffer-oracle.md

printf '%s\n' '--- Rust extraction implementation ---'
sed -n '65,225p' crates/rustysnes-test-harness/tests/accuracysnes_scenes.rs

printf '%s\n' '--- C extraction implementation ---'
sed -n '120,265p' scripts/accuracysnes/libretro_crossval.c

printf '%s\n' '--- Lua extraction implementation ---'
sed -n '120,180p' scripts/accuracysnes/mesen_scenes.lua

printf '%s\n' '--- scene manifest extraction tags ---'
awk -F '\t' 'NF >= 4 { print NR ":" $0 }' tests/roms/AccuracySNES/build/scenes.tsv | head -120

Repository: doublegate/RustySNES

Length of output: 29247


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- interlace references and Rust PPU geometry ---'
rg -n -C 4 \
  'interlace|INTERLACE|interlaced|pseudo.?hires|frame_hires|visible_height|SCREEN_HEIGHT|overscan' \
  docs crates/rustysnes-ppu crates/rustysnes-test-harness scripts/accuracysnes \
  --glob '*.md' --glob '*.rs' --glob '*.c' --glob '*.lua' \
  | head -1200

printf '%s\n' '--- exact relevant Rust PPU constants and accessors ---'
rg -n -C 8 \
  'pub const (SCREEN_WIDTH|MAX_SCREEN_WIDTH|SCREEN_HEIGHT)|fn visible_height|frame_hires|interlace' \
  crates/rustysnes-ppu/src --glob '*.rs' | head -500

printf '%s\n' '--- extraction-shape verifier ---'
python3 - <<'PY'
from dataclasses import dataclass

SCENE_W, SCENE_H = 256, 224
cases = {
    "snes9x interlace": (256, 224, 1, 1, 0),
    "Mesen2 interlace": (512, 478, 2, 2, 7),
}
for name, (w, h, xstep, ystep, first_row) in cases.items():
    sample_w = (SCENE_W - 1) if w == 512 else SCENE_W
    source_rows = list(range(first_row * ystep, (first_row + SCENE_H) * ystep, ystep))
    source_cols = [(1 + x) * xstep for x in range(sample_w)] if w == 512 else list(range(sample_w))
    print(name)
    print(f"  source geometry={w}x{h}")
    print(f"  sampled geometry={sample_w}x{len(source_rows)}")
    print(f"  first/last source row={source_rows[0] if source_rows else None}/{source_rows[-1] if source_rows else None}")
    print(f"  first/last source col={source_cols[0] if source_cols else None}/{source_cols[-1] if source_cols else None}")
    assert source_rows[-1] < h
    assert source_cols[-1] < w
PY

Repository: doublegate/RustySNES

Length of output: 50379


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- Rust PPU geometry symbols ---'
rg -n -C 6 \
  'pub const (SCREEN_WIDTH|MAX_SCREEN_WIDTH|SCREEN_HEIGHT)|pub const fn visible_height|fn visible_height|SETINI|is_interlace|interlace' \
  crates/rustysnes-ppu/src/lib.rs crates/rustysnes-ppu/src/render.rs crates/rustysnes-ppu/src/registers.rs \
  --glob '*.rs' | head -700

printf '%s\n' '--- targeted interlace documentation ---'
rg -n -C 8 \
  'interlace|field.?doubling|448|478' \
  docs/ppu.md docs/accuracysnes-coverability-audit-2026-07-23.md \
  docs/adr/0013-accuracysnes-framebuffer-oracle.md docs/adr/0014-per-dot-ppu-compositor.md \
  --glob '*.md' | head -700

printf '%s\n' '--- verifier for the documented Mesen2 reduction ---'
python3 - <<'PY'
SCENE_W, SCENE_H = 256, 224
source_w, source_h = 512, 478
first_row, xstep, ystep, first_col = 7, 2, 2, 1
sample_w = SCENE_W - first_col
rows = [(y + first_row) * ystep for y in range(SCENE_H)]
cols = [(x + first_col) * xstep for x in range(sample_w)]
print(f"halved geometry: {source_w // xstep}x{source_h // ystep}")
print(f"cropped sample: {sample_w}x{len(rows)}")
print(f"source rows: {rows[0]}..{rows[-1]}")
print(f"source cols: {cols[0]}..{cols[-1]}")
assert source_w // xstep == 256
assert source_h // ystep == 239
assert len(rows) == SCENE_H
assert rows[-1] < source_h
assert cols[-1] < source_w
PY

Repository: doublegate/RustySNES

Length of output: 28114


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- ADR interlace section ---'
sed -n '276,305p' docs/adr/0013-accuracysnes-framebuffer-oracle.md

printf '%s\n' '--- Rust interlace output status ---'
sed -n '193,200p' docs/ppu.md
sed -n '315,321p' docs/ppu.md
sed -n '550,560p' docs/ppu.md

printf '%s\n' '--- conflicting host geometry statements ---'
sed -n '136,152p' docs/adr/0013-accuracysnes-framebuffer-oracle.md
sed -n '196,206p' scripts/accuracysnes/libretro_crossval.c

printf '%s\n' '--- deterministic geometry verifier ---'
python3 - <<'PY'
SCENE_W, SCENE_H = 256, 224

# The ADR's measured Mesen2 interlace frame and the proposed "halve both" reduction.
source_w, source_h = 512, 478
reduced_w, reduced_h = source_w // 2, source_h // 2
print(f"Mesen2 source={source_w}x{source_h}; halve-both result={reduced_w}x{reduced_h}")
assert (reduced_w, reduced_h) == (256, 239)
assert (reduced_w, reduced_h) != (SCENE_W, SCENE_H)

# Existing Mesen2 host constants crop seven leading output rows after line halving.
first_row, ystep = 7, 2
sample_rows = [(y + first_row) * ystep for y in range(SCENE_H)]
print(f"Mesen2 sampled rows={len(sample_rows)}, source range={sample_rows[0]}..{sample_rows[-1]}")
assert len(sample_rows) == SCENE_H
assert sample_rows[-1] < source_h

# RustySNES currently exposes only 224 or 239 visible rows, not interlace-doubled rows.
rust_heights = {224, 239}
print(f"RustySNES documented visible heights={sorted(rust_heights)}")
assert 478 not in rust_heights
PY

Repository: doublegate/RustySNES

Length of output: 6024


Define a complete per-host extraction contract.

For Mesen2, 512x478 reduced by two produces 256x239, not 256x224. Specify the row crop, FIRST_ROW, column mapping, sample dimensions, expected geometry, and rejection behavior for each host. RustySNES currently exposes only 224 or 239 rows because interlace field doubling is not implemented. Also reconcile the ADR’s snes9x 256x224 entry with the 256x448 geometry documented in scripts/accuracysnes/libretro_crossval.c. Without this contract, hosts can hash different pixel sets under the same rule.

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

In `@docs/adr/0013-accuracysnes-framebuffer-oracle.md` around lines 296 - 303,
Expand the ADR’s extraction rules into a complete per-host contract covering
Mesen2 and snes9x: define row cropping, FIRST_ROW, column mapping, sample
dimensions, accepted input geometry, and rejection behavior. Correct Mesen2’s
512x478 reduction to specify how 239 rows become the required 224, document
RustySNES’s current 224/239-row limitation, and reconcile the snes9x 256x224
entry with the 256x448 geometry in libretro_crossval.c.

Source: Path instructions


Option 1 is the tempting one and should be resisted on its own: it would make the hi-res case
silently accept a 256-wide frame for a scene that must be 512. If interlace is worth covering, option
2 is the honest shape.
Loading