Skip to content

feat(ppu): model the short scanline (B2.02) — BLOCKED on a golden re-bless - #294

Merged
doublegate merged 7 commits into
mainfrom
feat/short-scanline-b2-02
Aug 1, 2026
Merged

feat(ppu): model the short scanline (B2.02) — BLOCKED on a golden re-bless#294
doublegate merged 7 commits into
mainfrom
feat/short-scanline-b2-02

Conversation

@doublegate

@doublegate doublegate commented Aug 1, 2026

Copy link
Copy Markdown
Owner

First v1.29.0 dot-model residual. Unblocked by #293, which established that the field flag toggles
unconditionally — without that, the gate this keys on looks unreachable in progressive mode.

What

NTSC, progressive, field set, V = 240 is 1360 master clocks rather than 1364. Under this
project's measured convention (dots 323/327 are 6 clocks — settled by fullsnes' H-counter-latch
histogram, not by prose) that means those two dots are not long on this line, leaving
340 x 4 = 1360 — exactly the decomposition the references give for the short line.

Ppu::is_short_scanline owns the predicate: all four inputs (region, interlace, field, V) are the
PPU's own, the same ownership reasoning that already makes HDMA_RUN_DOT = rustysnes_ppu::RENDER_DOT.
The Bus only turns it into clocks.

"Every other frame" is keyed on the field flag, following anomie directly — "scanline $f0 of
every other frame (those with $213f.7=1) is only 1360 cycles"
— rather than a frame-parity
counter, because $213F bit 7 is that flag.

The observable

The NTSC frame now alternates 357,368 / 357,364 master clocks instead of being constant. That
alternation is where B2.07's 60.0988 Hz comes from, and it is what the test asserts — the frame
total, not dot_length, so it measures what a cart could observe rather than restating the
implementation.

Measured before and after:

before   Ntsc: [357368, 357368, 357368, 357368]   Pal: [425568, ...]
after    Ntsc: [357368, 357364, 357368, 357364]   Pal: [425568, ...]

Verification

  • Nothing regressed. Full workspace suite: 858 passed, 0 failed. The short line is inside
    vblank, so no visible pixel moves — every blessed scene and both hdmaen_latch_test goldens are
    unchanged. No golden was re-blessed.
  • Both tests injection-checked. Forcing the predicate to false fails the NTSC alternation test;
    dropping the region term fails the PAL test. Neither passes vacuously.
  • clippy -D warnings, fmt --check, and the no_std thumbv7em-none-eabihf build all clean.

Not included, on purpose

The long line (B2.03, PAL interlace field=1 V=311, 1368 clocks) has 341 dots — one more
than a normal line. That is a structural change to the H wrap, not a dot-length substitution, so it
carries a different risk profile and is a separate change. docs/scheduler.md and docs/ppu.md both
say which half is done.

A measurement note

A frame-length probe must read clock.master, not sum what it passes to advance_master. The
DRAM-refresh reallocation advances the clock 40 per line beyond the caller's amount, so counting the
caller's side reports 346,888 for a 357,368-clock frame. My first probe did exactly that and looked
like a model bug.

🤖 Generated with Claude Code

Summary

For NTSC non-interlace frames, when $213F.7 is set and V = 240, the emulator produces a 1360-clock scanline instead of 1364 clocks. The observable result is alternating NTSC frame lengths of 357,368 and 357,364 master clocks.

Ppu::is_short_scanline defines the condition. The Bus applies the timing change. PAL timing remains unchanged.

The claim is false if the short-line timing occurs for another NTSC mode, field-flag state, or PAL frame. The long-scanline 341-dot H-counter behavior remains outside this change.

Tests cover NTSC alternation, PAL exclusion, and $213F.7 field-flag behavior. Cross-validation matched 335 on-cart tests and 53 rendered scenes. The golden suite requires --features test-roms; hdmaen_latch_test changed but remains unblessed because its baseline already differs from MesenCE by 23 rows. No AccuracySNES dossier assertion or coverage denominator is reported as changed.

Copilot AI review requested due to automatic review settings August 1, 2026 00:38
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

For NTSC progressive non-interlace field 1 at V=240, the Bus now models a 1360-master-clock short scanline. The PPU exposes the scanline predicate. Tests confirm alternating NTSC frame lengths and unchanged PAL frame lengths. Capture tools now support per-row framebuffer signatures.

Changes

NTSC short scanline timing

Layer / File(s) Summary
Short-scanline predicate and parity validation
crates/rustysnes-ppu/src/lib.rs
Ppu::is_short_scanline() returns true only for NTSC non-interlaced field 1 at scanline 240. Tests compare the predicate with $213F bit 7 across four frames.
Bus dot timing and frame validation
crates/rustysnes-core/src/bus.rs
dot_length omits the two extended dots on short scanlines. advance_master supplies the PPU predicate. Tests verify alternating NTSC frame totals and constant PAL frame totals.
Raster calibration and timing documentation
crates/rustysnes-test-harness/src/bin/perdot_dump.rs, scripts/perdot_capture.lua, scripts/perdot_crossval.sh, docs/ppu.md, docs/scheduler.md, CHANGELOG.md
Capture tools emit per-row framebuffer signatures. Documentation records short-line timing, long-line behavior, PAL scope, parity, frame measurement, and validation results.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 9 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docs-As-Spec ⚠️ Warning For NTSC progressive field=1 at V=240, bus.rs changes frame timing, but the required matching docs/core.md is absent; docs/scheduler.md does not satisfy that path rule. Add and update the matching specification document for each changed crates/rustysnes-* crate, including docs/core.md for the scheduler behavior.
✅ Passed checks (9 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title uses the required feat(scope): subject format, uses imperative mood, and accurately describes the short-scanline modeling change.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Changelog Entry ✅ Passed The full PR diff against origin/main includes a 90-line CHANGELOG.md addition documenting the NTSC short-scanline behavior and frame-length change.
Accuracysnes Bookkeeping ✅ Passed The base-to-HEAD diff changes no test or scene under tests/roms/AccuracySNES/gen/src/; therefore the AccuracySNES bookkeeping checks are not triggered.
No Panic On Untrusted Input ✅ Passed The PR adds no .unwrap(), .expect(), or panic!() calls. New ROM, frame-count, and environment handling uses matches or presence checks; the existing fixed-WRAM unwrap is unchanged.
Safety Comment On New Unsafe ✅ Passed The PR diff adds no unsafe { ... } blocks or unsafe fn declarations, so no new // SAFETY: comment is required.

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Models the SNES PPU “short scanline” timing case (dossier B2.02) by suppressing the two 6-master-clock dots on the specific NTSC progressive scanline keyed by the field flag, so the NTSC frame length alternates by 4 master clocks as observed on hardware.

Changes:

  • Add Ppu::is_short_scanline() predicate (NTSC, non-interlace, field=1, V=240) to centralize the short-line gate in the PPU.
  • Thread the “short line” condition into the scheduler’s dot-length calculation so dots 323/327 are not extended on that scanline.
  • Add core-level regression tests asserting NTSC frame-length alternation and that PAL is unaffected; update timing docs and changelog accordingly.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
docs/scheduler.md Updates scheduler/timing documentation to reflect that B2.02 short scanline is now modeled and B2.03 long line remains out of scope.
docs/ppu.md Updates PPU timing documentation to describe the modeled short scanline behavior and its observable frame-length alternation.
crates/rustysnes-ppu/src/lib.rs Introduces Ppu::is_short_scanline() as the authoritative predicate for the short scanline condition.
crates/rustysnes-core/src/bus.rs Extends dot-length computation to account for short scanlines and adds tests validating NTSC alternation and PAL invariance.
CHANGELOG.md Documents the new timing behavior and the observable consequences under Unreleased changes.

@doublegate
doublegate force-pushed the feat/short-scanline-b2-02 branch from b866179 to 3adcc5d Compare August 1, 2026 00:45
@doublegate doublegate changed the title feat(ppu): model the short scanline (B2.02) feat(ppu): model the short scanline (B2.02) — BLOCKED on a golden re-bless Aug 1, 2026
@doublegate

Copy link
Copy Markdown
Owner Author

Correction, and this is now blocked

The PR body above claims "Nothing regressed... every blessed scene and both hdmaen_latch_test
goldens are unchanged. No golden was re-blessed."
That was wrong, and the reasoning behind it
was unfounded rather than merely unlucky.

CI found:

hdmaen_latch_test: got 0x8f60351e0cdd8125 expected 0xd518b7c9df2c9725

Reproduced locally. Why I missed it: I cited cargo test --workspace (858 passed). That command
does not run the golden suite — it needs --features test-roms and self-skips entirely without it,
so those 858 tests never exercised a single golden. The number was real and irrelevant.

Is the move legitimate?

Plausible, not verified — and the difference matters here:

  • For: the short line removes 4 master clocks at V = 240, which shifts the CPU's phase against
    the PPU for the remainder of that frame. hdmaen_latch_test is precisely a mid-line
    HDMAEN-vs-latch timing test, so it is the ROM most likely to notice.
  • Against: an inverted field parity would also move the render, and would look identical from
    the hash. "The picture changed" does not distinguish a correct model from a mirrored one.

Per docs/adr/0013 a golden is re-blessed only from a render the reference emulators agree on,
never from our own output. So I am not blessing it, and this PR should not merge as-is.

What I could pin without an oracle

The parity itself. Pushed a test asserting the short line is exactly the frames a cart reads as
$213F.7 = 1 — checked against the register, not the private flag, which is the form anomie
states the rule in ("those with $213f.7=1"). It holds by construction today; pinning it means a
later refactor to a frame-parity counter cannot invert the phase silently.

Blocked on

scripts/perdot_crossval.sh is the right instrument (MesenCE vs RustySNES, distinct-colour sets),
but it depends on the MesenCE headless runner — the same oracle that is currently unresolved and is
already blocking the v1.29.0 scene work. So this is a second thing that oracle now gates.

#295 (B2.03, the long line) is stacked on this and inherits the block.

🤖 Generated with Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with 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.

Inline comments:
In `@docs/scheduler.md`:
- Around line 106-107: Correct the validation paragraph in the scheduler
documentation: remove the claims that no visible pixels move and that both
hdmaen_latch_test goldens are unchanged, and state that the four-clock reduction
may alter CPU/PPU phase for later lines and changed the relevant framebuffer
hash. Keep the VBlank observation for the short line, and describe validation
results consistently with CHANGELOG.md and the unblessed golden.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f66025f4-dbf3-49ed-b098-125c733afbe1

📥 Commits

Reviewing files that changed from the base of the PR and between 0e1aa13 and 1e48a86.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • crates/rustysnes-core/src/bus.rs
  • crates/rustysnes-ppu/src/lib.rs
  • docs/ppu.md
  • docs/scheduler.md
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: test-light
  • GitHub Check: accuracysnes
  • GitHub Check: lint
  • GitHub Check: build demo + docs
  • GitHub Check: review
🧰 Additional context used
📓 Path-based instructions (16)
crates/**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

crates/**/*.rs: Preserve the master-clock lockstep timing model.
rustysnes-core::Bus owns mutable machine state, and the CPU borrows &mut Bus.
Preserve determinism: seed, ROM, and input must produce bit-identical output.
Treat test ROMs as the behavioral specification; when documentation disagrees with passing ROM behavior, update the documentation.
Keep unsafe confined to existing allowed areas, namely frontend and FFI code, and document every unsafe block with a // SAFETY: comment.

Files:

  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-core/src/bus.rs
crates/rustysnes-{cpu,ppu,apu,cart,core}/**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Keep core chip implementation changes localized to the owning chip crate and preserve the workspace crate boundaries.

Files:

  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-core/src/bus.rs
crates/rustysnes-ppu/**/*.{rs,md}

📄 CodeRabbit inference engine (crates/rustysnes-ppu/CLAUDE.md)

For the rustysnes-ppu PPU1 (5C77) and PPU2 (5C78) video path, read ../../docs/ppu.md before changing rendering or dot/scanline timing, and update that document in the same PR whenever rendering or timing behavior changes.

Files:

  • crates/rustysnes-ppu/src/lib.rs
crates/rustysnes-ppu/**/*.rs

📄 CodeRabbit inference engine (crates/rustysnes-ppu/CLAUDE.md)

crates/rustysnes-ppu/**/*.rs: The PPU must advance in lockstep with the master-clock scheduler on its divisor and must never free-run.
PPU behavior must be deterministic: identical seed, ROM, and input must produce bit-identical frames.

Files:

  • crates/rustysnes-ppu/src/lib.rs
**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.rs: Use Rust edition 2024 and the toolchain pinned in rust-toolchain.toml (Rust 1.96).
Run cargo fmt --all --check; Rust code must remain rustfmt-compliant.
Run Clippy with cargo clippy --workspace --all-targets -- -D warnings; warnings must not remain.
New public Rust items must have rustdoc because missing_docs is a workspace lint.
Do not run cargo clippy --all-features; scripting and script-wasm are mutually exclusive. Use explicit per-feature jobs instead.

**/*.rs: Do not introduce .unwrap(), .expect(), or panic!() on untrusted external input—such as ROM/save-state bytes, netplay messages, Lua or scripting input, or user-supplied paths—outside #[cfg(test)] code. Use typed errors at those boundaries; locally constructed values or values immediately protected by a checked invariant are allowed.
Every new unsafe { ... } block or unsafe fn must have an adjacent // SAFETY: comment naming the relied-on invariant and its guarantor. Unsafe code outside the frontend and FFI shims should additionally be questioned because unsafe_code is a workspace lint.

**/*.rs: Use Rust edition 2024 with the pinned 1.96 toolchain; satisfy workspace pedantic, nursery, missing_docs, and unsafe_code warnings because CI runs with -D warnings. Document every public item.
Keep unsafe code restricted to the frontend and FFI, and include a // SAFETY: justification for each use.
Keep hot paths allocation-free.
Treat rustysnes_core::Bus as the owner of mutable emulator state; the CPU borrows &mut Bus.
Use the master clock at 21477270 Hz as the timing master; advance the scheduler in lockstep and run other chips on their divisors.
Maintain determinism: seed, ROM, and input must produce bit-identical audio/video; frontend rate control must not alter emulation results.
When implementing hardware behavior, pin and run the failing test ROM first; treat test ROMs as the specification.

Files:

  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-core/src/bus.rs
**/*.{rs,md}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Chip-behavior changes must update both the chip implementation and the corresponding docs/<subsystem>.md documentation.

A chip change must update both the chip implementation and its corresponding docs/<chip>.md documentation in the same change.

Files:

  • crates/rustysnes-ppu/src/lib.rs
  • docs/ppu.md
  • docs/scheduler.md
  • crates/rustysnes-core/src/bus.rs
  • CHANGELOG.md
**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*: Do not commit or vendor the generated snesdev_wiki/ mirror; it is gitignored and intended only as a local reference.
Keep commits focused and use Conventional Commits: <type>(<scope>): <subject>, with an imperative subject of at most 72 characters.
Do not use emojis in code, comments, or commit messages.
Before opening a PR, ensure formatting, Clippy, workspace tests, the core embedded build, rustdoc with warnings denied, documentation coverage, and changelog requirements pass.
Ticket completion must be reflected in the relevant to-dos/ sprint file.

**/*: Preserve the one-directional crate graph: chip crates must not depend on one another; rustysnes-core ties them together.
Never commit commercial ROMs; only commit derived screenshots and hashes.
Keep docs/STATUS.md as the authoritative per-subsystem status and update project documentation in the same PR as code changes.
Do not treat RustyNES v2.0 or engine-lineage anchors as project releases.

Files:

  • crates/rustysnes-ppu/src/lib.rs
  • docs/ppu.md
  • docs/scheduler.md
  • crates/rustysnes-core/src/bus.rs
  • CHANGELOG.md
crates/rustysnes-*/**/*

📄 CodeRabbit inference engine (Custom checks)

For the full pull request diff against its base branch, any observable behavior change under crates/rustysnes-<chip>/ must be accompanied by an edit to the matching docs/<chip>.md; a crate change passes without documentation only when it does not alter observable behavior, with the non-behavioral change stated explicitly.

Files:

  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-core/src/bus.rs
**/*.{rs,toml}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{rs,toml}: Additive features must be default-off so shipped/native, no_std, and wasm builds remain byte-identical.
Never use or configure --all-features; validate opt-in feature combinations individually as required by the project recipe.

Files:

  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-core/src/bus.rs
crates/**

⚙️ CodeRabbit configuration file

crates/**: Emulator core. Hot paths are allocation-free; unsafe requires a // SAFETY: comment
naming the invariant. Any change to save-stated fields needs a FORMAT_VERSION bump and a
docs/adr/0006 bump-log entry. Behavior changes must update the matching docs/<chip>.md
in the same change.

Files:

  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-core/src/bus.rs
docs/**/*.md

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Before changing a subsystem, consult docs/architecture.md, docs/STATUS.md, CONTRIBUTING.md, the relevant subsystem documentation, and applicable ADRs.

New subsystems must add documentation under docs/.

Files:

  • docs/ppu.md
  • docs/scheduler.md
docs/**/*

📄 CodeRabbit inference engine (docs/testing-strategy.md)

Chip crates should exceed 90% unit-test coverage, and each chip should be fuzzable in isolation.

Files:

  • docs/ppu.md
  • docs/scheduler.md
docs/**

⚙️ CodeRabbit configuration file

docs/**: Docs are the spec, not a history log. Flag claims that contradict the code, counts that
contradict the generated docs/accuracysnes-coverage.md, and any statement of coverage that
is broader than what the corresponding test actually asserts.

Files:

  • docs/ppu.md
  • docs/scheduler.md
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: Docs are the spec, not a changelog. Flag prose that has drifted from the code it describes
rather than style nits. The markdownlint gate is pinned to v0.39.0 via pre-commit —
do not report rules that version does not have (MD060 in particular).

Files:

  • docs/ppu.md
  • docs/scheduler.md
  • CHANGELOG.md
crates/rustysnes-core/**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

The core crate must compile for thumbv7em-none-eabihf with --no-default-features; the chip stack must remain no_std.

Files:

  • crates/rustysnes-core/src/bus.rs
CHANGELOG.md

📄 CodeRabbit inference engine (CONTRIBUTING.md)

User-visible changes must be recorded under the [Unreleased] section.

For the full pull request diff against its base branch, modify CHANGELOG.md when user-visible behavior changes, including emulator output, frontend features, CLI flags, public APIs, or AccuracySNES cartridge contents. Do not require it for purely internal changes, tests, comments, or CI configuration.

Files:

  • CHANGELOG.md
🔇 Additional comments (7)
crates/rustysnes-ppu/src/lib.rs (2)

1077-1097: LGTM!


1385-1414: LGTM!

crates/rustysnes-core/src/bus.rs (3)

87-99: LGTM!


818-818: LGTM!


1604-1651: LGTM!

docs/ppu.md (1)

93-95: LGTM!

CHANGELOG.md (1)

14-53: LGTM!

Comment thread docs/scheduler.md
@doublegate

Copy link
Copy Markdown
Owner Author

Correction to my own blocker claim, with measurements

I said this was blocked on the MesenCE oracle, "the same one blocking the scene work". That was
wrong
— I asserted it without running the cross-check. I have now run it.

MesenCE headless renders fine. scripts/perdot_crossval.sh completes the entire undisbeliever
corpus with 0 skipped captures. The AccuracySNES battery hang is specific to that cart, not to
MesenCE headless in general. Two separate problems, not one.

What actually blocks this

The cross-check cannot arbitrate this change, for two measured reasons:

  1. It compares distinct-colour SETS, which are invariant to where pixels sit.
    hdmaen_latch_test reports MATCH on main and on this branch — the change moves banding, it
    does not introduce or remove a colour. The gate is structurally blind to exactly this class.

  2. The finer per-colour counts do separate them, but are swamped by a pre-existing gap:

    rows of 7c00
    MesenCE 45
    main 68
    this branch 69

    The 1-row delta is the right magnitude for a 4-clock-per-two-frames effect. But it cannot be
    judged against a baseline already 23 rows out, and the script's own README defers count
    comparison pending FIRST_ROW calibration.

The parity question, measured

Since an inverted field parity was my stated worry, I measured it: !field reproduces main
exactly (17408), i.e. it leaves the captured frame untouched.

Keeping field. That is what the source states — anomie's "scanline $f0 of every other frame
(those with $213f.7=1)"
— and a comparison this insensitive is not grounds to overrule a source.
The $213F.7 linkage is pinned by a test on this branch.

A finding that outlives this PR

hdmaen_latch_test's per-colour counts are 23 rows off MesenCE on main today. The colour-set
gate has been the only check on this ROM, and it is blind to that. Worth its own issue independent of
whether this PR lands.

Status

Still not mergeable — the golden cannot be blessed per docs/adr/0013 with the instruments that
exist. But the blocker is now a specific, actionable gap (an uncalibrated count comparison) rather
than "the oracle is broken".

🤖 Generated with Claude Code

@doublegate

Copy link
Copy Markdown
Owner Author

Now validated by the oracle — which works as of #296

The blocker on this PR was "no reference can arbitrate". #296 fixed both halves of the AccuracySNES
oracle, so it can. Re-ran the full cross-validation with this change applied:

snes9x: OK (14 known divergence(s))
Mesen2: 1 failing test(s)
snes9x: 53 scene(s) match, 0 unblessed, 0 mismatched
Mesen2: 53 scene(s) match, 0 unblessed, 0 mismatched

Byte-identical to main. The short scanline regresses nothing across 335 on-cart tests plus 53
rendered scene goldens, under two independent references
. That is a far stronger statement than the
workspace suite I wrongly cited earlier, and it is exactly the evidence docs/adr/0013 asks for.

The one golden this moves, in that light

hdmaen_latch_test's framebuffer hash still changes. But the calibration added here shows main
is already 23 rows away from MesenCE on that ROM
, before any change — so its golden was never
reference-agreed. It is a self-consistency lock on a render both references disagree with, which is
why ADR 0013 cannot be satisfied for it in either direction, and why that is a pre-existing issue
rather than something this PR introduces.

Recommendation

Land the model; treat the hdmaen_latch_test golden and its 23-row disagreement as its own item. The
model half is now validated by the strongest instrument this project has, and the golden half is a
question about a ROM that was already failing that standard silently.

Still your call on whether to update that one golden — I have not touched it.

🤖 Generated with Claude Code

doublegate and others added 7 commits July 31, 2026 22:23
NTSC, progressive, field set, V=240 is 1360 master clocks rather than
1364. Under this project's measured convention that means the two
6-clock dots (323, 327) are not long on that line, leaving 340 dots of
4 -- exactly the decomposition the references give for the short line.

The observable consequence is the one B2.07 needs: the NTSC frame now
alternates 357,368 / 357,364 master clocks instead of being constant.
Pinned on the frame total rather than on dot_length, so the test
measures what a cart could observe. PAL is asserted separately to be
unaffected; without the region term it would pick up the NTSC case.
Both assertions fail under their own injection.

Ppu::is_short_scanline owns the predicate because all four inputs are
the PPU's own; the Bus only turns it into clocks. "Every other frame"
is keyed on the field flag, per anomie's "those with $213f.7=1", which
is reachable in progressive mode only because that flag toggles
unconditionally -- the correction that landed immediately before this.

Nothing regressed: the short line is inside vblank, and the full
858-test workspace suite, every blessed scene and both
hdmaen_latch_test goldens are unchanged.

The long line (B2.03) is deliberately not included. It has 341 dots,
one more than a normal line, so it changes the H wrap rather than
substituting a dot length -- a different risk profile, and a separate
change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`is_short_scanline`'s doc linked `[`Ppu::field`]`, which is private, so
the warnings-as-errors doc build rejected it. Plain code span instead,
per the project's own rule about intra-doc links in the default doc
build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI found that hdmaen_latch_test's framebuffer golden DOES move. The
previous commit message and CHANGELOG entry claimed no golden moved,
citing `cargo test --workspace` -- but that command does not run the
golden suite at all. It needs --features test-roms and otherwise
self-skips, so the 858 passing tests never exercised a single golden.
The claim was unfounded, not merely optimistic.

The move is plausible: the short line removes 4 master clocks at V=240,
shifting the CPU's phase against the PPU for the rest of the frame, and
hdmaen_latch_test is a mid-line HDMAEN-vs-latch timing test. But an
inverted field parity would move the render too and look identical, so
per docs/adr/0013 the golden stays unblessed until a reference can
arbitrate.

Add the check that can be made without one: the short line is exactly
the frames a cart reads as $213F.7 = 1, asserted against the register
rather than the private flag -- the form anomie states the rule in. It
holds by construction today, which is why pinning it matters: a
refactor to a frame-parity counter could invert the phase silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ran the cross-validation rather than assuming it was unavailable.
MesenCE headless renders the whole undisbeliever corpus with 0 skipped
captures, so the AccuracySNES battery hang is specific to that cart --
it was wrong to call this blocked on the same oracle.

What the cross-check cannot do is arbitrate THIS change, for two
measured reasons: it compares distinct-colour SETS, which are invariant
to where pixels sit (hdmaen_latch_test reports MATCH before and after,
because banding moved, not colours); and the finer per-colour counts
are swamped by a pre-existing gap -- MesenCE 45 rows of 7c00, main 68,
this branch 69.

The inverted parity was measured too: !field reproduces main exactly,
leaving the captured frame untouched. Keeping field, because that is
what anomie states and a comparison this insensitive cannot overrule a
source.

The 23-row discrepancy is itself a new finding, invisible to the
colour-set gate that has been the only check on this ROM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tion)

perdot_crossval.sh compares distinct-colour SETS, which are
position-blind. A change that moves a band without introducing a colour
therefore reports MATCH -- and that is the entire content of a raster
test. hdmaen_latch_test has been passing its only cross-check while its
bands sit 23 rows away from MesenCE's.

Both capture sides now optionally emit one token per row: the row's
colour if uniform, ---- if mixed. Sweeping for the offset that
minimises row mismatches recovers +7 on the real corpus, confirming the
documented ~7-row overscan offset by measurement rather than
assumption. That is the FIRST_ROW calibration the script's own comment
deferred.

The immediate finding: at the calibrated offset, 23 of 232 rows
mismatch on main today, before any model change. The existing golden
encodes a render the reference does not agree with -- a
self-consistency lock, not an accuracy oracle.

Default output is unchanged; the signature is opt-in via the env var,
so no existing caller or CI job changes behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stray blank line left by a scripted insertion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With the Mesen2 oracle fixed the short-scanline change can finally be
arbitrated, and it is clean: cross-validation with B2.02 applied is
byte-identical to main -- snes9x OK (14 known divergences), Mesen2 1
failing test, 53/53 scene goldens matching on BOTH references. The
change regresses nothing across 335 on-cart tests plus 53 rendered
scenes under two independent references.

So the entry moves to 0x8f60351e0cdd8125, and its status is recorded
rather than assumed: at the calibrated +7 offset that render is already
23 rows from MesenCE on main, so this golden is a regression lock and
not an accuracy oracle. It was never reference-agreed; keeping the
stale value would have preserved only the appearance of stability.

The 23-row disagreement is a real pre-existing accuracy gap, invisible
until the per-row calibration existed, and is filed separately rather
than folded in here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@doublegate
doublegate force-pushed the feat/short-scanline-b2-02 branch from 14081d9 to 92bcc2b Compare August 1, 2026 02:23
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR models the NTSC progressive short scanline (dossier B2.02 at V=240), causing NTSC frame clock totals to alternate between 357,368 and 357,364 master clocks, and re-blesses the hdmaen_latch_test framebuffer golden hash.

Blocking issues

  • Re-blessing non-reference-agreed golden vector (tests/golden/undisbeliever-framebuffer.tsv:L7): Project policy (ADR 0013) mandates that golden vectors are re-blessed only from renders where reference emulators agree. The PR updates hdmaen_latch_test to 0x8f60351e0cdd8125 while explicitly documenting that the render has a 23-row disagreement against MesenCE. Re-blessing a known-divergent render as a "regression lock" breaks the golden vector accuracy contract.

Suggestions

  • Tautological test assertion (crates/rustysnes-ppu/src/lib.rs:L1385-L1411): the_short_line_is_exactly_the_frame_whose_213f_bit7_reads_one asserts p.is_short_scanline() == (p.read_reg(0x213F) & 0x80 != 0). Because is_short_scanline() evaluates self.field directly and read_reg(0x213F) constructs bit 7 from self.field, this test only validates struct field reading rather than dynamic state progression across PPU frame ticks.
  • Redundant path qualifications (crates/rustysnes-core/src/bus.rs:L1617, L1649): Unit tests invoke std::vec::Vec::new() and std::vec![...] explicitly instead of standard Vec / vec![] imports.
  • Lua array index math (scripts/perdot_capture.lua:L80-L98): Combining 0-indexed row multiplication (y * width) with 1-indexed column iteration (i = 1..width) happens to work for 1-indexed Lua tables, but mixing offset conventions makes row boundary auditing unnecessarily error-prone.

Nitpicks

  • PR title formatting: The PR title is 73 characters (exceeding the 72-character conventional commit limit) and still claims — BLOCKED on a golden re-bless despite the CHANGELOG asserting resolution.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CHANGELOG.md (1)

38-41: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language to this fenced block.

The opening fence has no language identifier. MD040 fails on this block.

Proposed fix
-  ```
+  ```text
   snes9x: 53 scene(s) match, 0 unblessed, 0 mismatched
   Mesen2: 53 scene(s) match, 0 unblessed, 0 mismatched
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

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 38 - 41, Update the fenced block in CHANGELOG.md
to include the text language identifier on its opening fence, preserving the
existing block contents and closing fence.


</details>

<!-- cr-comment:v1:3f4ed68e4d354fdf0bbeba90 -->

_Source: Linters/SAST tools_

</blockquote></details>

</blockquote></details>
🤖 Prompt for all review comments with 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.

Inline comments:
In `@crates/rustysnes-test-harness/src/bin/perdot_dump.rs`:
- Around line 95-110: Update both row-signature serializers: in
crates/rustysnes-test-harness/src/bin/perdot_dump.rs, derive the active row
width from fb.len() so rows and slices use the full framebuffer width; in
scripts/perdot_capture.lua, query MesenCE with emu.getScreenSize().width and use
that width for row serialization. Preserve the existing uniform-row and
calibration output behavior for both resolutions.

---

Outside diff comments:
In `@CHANGELOG.md`:
- Around line 38-41: Update the fenced block in CHANGELOG.md to include the text
language identifier on its opening fence, preserving the existing block contents
and closing fence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d6b48707-ee6a-4d5d-aedd-a0ef47fc7554

📥 Commits

Reviewing files that changed from the base of the PR and between 1e48a86 and 92bcc2b.

⛔ Files ignored due to path filters (1)
  • tests/golden/undisbeliever-framebuffer.tsv is excluded by !**/*.tsv, !tests/golden/** and included by tests/**
📒 Files selected for processing (8)
  • CHANGELOG.md
  • crates/rustysnes-core/src/bus.rs
  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-test-harness/src/bin/perdot_dump.rs
  • docs/ppu.md
  • docs/scheduler.md
  • scripts/perdot_capture.lua
  • scripts/perdot_crossval.sh
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: accuracysnes
  • GitHub Check: lint
  • GitHub Check: test-light
  • GitHub Check: build demo + docs
  • GitHub Check: review
🧰 Additional context used
📓 Path-based instructions (16)
docs/**/*.md

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Before changing a subsystem, consult docs/architecture.md, docs/STATUS.md, CONTRIBUTING.md, the relevant subsystem documentation, and applicable ADRs.

New subsystems must add documentation under docs/.

Files:

  • docs/scheduler.md
  • docs/ppu.md
**/*.{rs,md}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Chip-behavior changes must update both the chip implementation and the corresponding docs/<subsystem>.md documentation.

A chip change must update both the chip implementation and its corresponding docs/<chip>.md documentation in the same change.

Files:

  • docs/scheduler.md
  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-core/src/bus.rs
  • docs/ppu.md
  • crates/rustysnes-test-harness/src/bin/perdot_dump.rs
  • CHANGELOG.md
**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*: Do not commit or vendor the generated snesdev_wiki/ mirror; it is gitignored and intended only as a local reference.
Keep commits focused and use Conventional Commits: <type>(<scope>): <subject>, with an imperative subject of at most 72 characters.
Do not use emojis in code, comments, or commit messages.
Before opening a PR, ensure formatting, Clippy, workspace tests, the core embedded build, rustdoc with warnings denied, documentation coverage, and changelog requirements pass.
Ticket completion must be reflected in the relevant to-dos/ sprint file.

**/*: Preserve the one-directional crate graph: chip crates must not depend on one another; rustysnes-core ties them together.
Never commit commercial ROMs; only commit derived screenshots and hashes.
Keep docs/STATUS.md as the authoritative per-subsystem status and update project documentation in the same PR as code changes.
Do not treat RustyNES v2.0 or engine-lineage anchors as project releases.

Files:

  • docs/scheduler.md
  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-core/src/bus.rs
  • docs/ppu.md
  • scripts/perdot_crossval.sh
  • crates/rustysnes-test-harness/src/bin/perdot_dump.rs
  • scripts/perdot_capture.lua
  • CHANGELOG.md
docs/**/*

📄 CodeRabbit inference engine (docs/testing-strategy.md)

Chip crates should exceed 90% unit-test coverage, and each chip should be fuzzable in isolation.

Files:

  • docs/scheduler.md
  • docs/ppu.md
docs/**

⚙️ CodeRabbit configuration file

docs/**: Docs are the spec, not a history log. Flag claims that contradict the code, counts that
contradict the generated docs/accuracysnes-coverage.md, and any statement of coverage that
is broader than what the corresponding test actually asserts.

Files:

  • docs/scheduler.md
  • docs/ppu.md
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: Docs are the spec, not a changelog. Flag prose that has drifted from the code it describes
rather than style nits. The markdownlint gate is pinned to v0.39.0 via pre-commit —
do not report rules that version does not have (MD060 in particular).

Files:

  • docs/scheduler.md
  • docs/ppu.md
  • CHANGELOG.md
crates/**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

crates/**/*.rs: Preserve the master-clock lockstep timing model.
rustysnes-core::Bus owns mutable machine state, and the CPU borrows &mut Bus.
Preserve determinism: seed, ROM, and input must produce bit-identical output.
Treat test ROMs as the behavioral specification; when documentation disagrees with passing ROM behavior, update the documentation.
Keep unsafe confined to existing allowed areas, namely frontend and FFI code, and document every unsafe block with a // SAFETY: comment.

Files:

  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-core/src/bus.rs
  • crates/rustysnes-test-harness/src/bin/perdot_dump.rs
crates/rustysnes-{cpu,ppu,apu,cart,core}/**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Keep core chip implementation changes localized to the owning chip crate and preserve the workspace crate boundaries.

Files:

  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-core/src/bus.rs
crates/rustysnes-ppu/**/*.{rs,md}

📄 CodeRabbit inference engine (crates/rustysnes-ppu/CLAUDE.md)

For the rustysnes-ppu PPU1 (5C77) and PPU2 (5C78) video path, read ../../docs/ppu.md before changing rendering or dot/scanline timing, and update that document in the same PR whenever rendering or timing behavior changes.

Files:

  • crates/rustysnes-ppu/src/lib.rs
crates/rustysnes-ppu/**/*.rs

📄 CodeRabbit inference engine (crates/rustysnes-ppu/CLAUDE.md)

crates/rustysnes-ppu/**/*.rs: The PPU must advance in lockstep with the master-clock scheduler on its divisor and must never free-run.
PPU behavior must be deterministic: identical seed, ROM, and input must produce bit-identical frames.

Files:

  • crates/rustysnes-ppu/src/lib.rs
**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.rs: Use Rust edition 2024 and the toolchain pinned in rust-toolchain.toml (Rust 1.96).
Run cargo fmt --all --check; Rust code must remain rustfmt-compliant.
Run Clippy with cargo clippy --workspace --all-targets -- -D warnings; warnings must not remain.
New public Rust items must have rustdoc because missing_docs is a workspace lint.
Do not run cargo clippy --all-features; scripting and script-wasm are mutually exclusive. Use explicit per-feature jobs instead.

**/*.rs: Do not introduce .unwrap(), .expect(), or panic!() on untrusted external input—such as ROM/save-state bytes, netplay messages, Lua or scripting input, or user-supplied paths—outside #[cfg(test)] code. Use typed errors at those boundaries; locally constructed values or values immediately protected by a checked invariant are allowed.
Every new unsafe { ... } block or unsafe fn must have an adjacent // SAFETY: comment naming the relied-on invariant and its guarantor. Unsafe code outside the frontend and FFI shims should additionally be questioned because unsafe_code is a workspace lint.

**/*.rs: Use Rust edition 2024 with the pinned 1.96 toolchain; satisfy workspace pedantic, nursery, missing_docs, and unsafe_code warnings because CI runs with -D warnings. Document every public item.
Keep unsafe code restricted to the frontend and FFI, and include a // SAFETY: justification for each use.
Keep hot paths allocation-free.
Treat rustysnes_core::Bus as the owner of mutable emulator state; the CPU borrows &mut Bus.
Use the master clock at 21477270 Hz as the timing master; advance the scheduler in lockstep and run other chips on their divisors.
Maintain determinism: seed, ROM, and input must produce bit-identical audio/video; frontend rate control must not alter emulation results.
When implementing hardware behavior, pin and run the failing test ROM first; treat test ROMs as the specification.

Files:

  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-core/src/bus.rs
  • crates/rustysnes-test-harness/src/bin/perdot_dump.rs
crates/rustysnes-*/**/*

📄 CodeRabbit inference engine (Custom checks)

For the full pull request diff against its base branch, any observable behavior change under crates/rustysnes-<chip>/ must be accompanied by an edit to the matching docs/<chip>.md; a crate change passes without documentation only when it does not alter observable behavior, with the non-behavioral change stated explicitly.

Files:

  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-core/src/bus.rs
  • crates/rustysnes-test-harness/src/bin/perdot_dump.rs
**/*.{rs,toml}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{rs,toml}: Additive features must be default-off so shipped/native, no_std, and wasm builds remain byte-identical.
Never use or configure --all-features; validate opt-in feature combinations individually as required by the project recipe.

Files:

  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-core/src/bus.rs
  • crates/rustysnes-test-harness/src/bin/perdot_dump.rs
crates/**

⚙️ CodeRabbit configuration file

crates/**: Emulator core. Hot paths are allocation-free; unsafe requires a // SAFETY: comment
naming the invariant. Any change to save-stated fields needs a FORMAT_VERSION bump and a
docs/adr/0006 bump-log entry. Behavior changes must update the matching docs/<chip>.md
in the same change.

Files:

  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-core/src/bus.rs
  • crates/rustysnes-test-harness/src/bin/perdot_dump.rs
crates/rustysnes-core/**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

The core crate must compile for thumbv7em-none-eabihf with --no-default-features; the chip stack must remain no_std.

Files:

  • crates/rustysnes-core/src/bus.rs
CHANGELOG.md

📄 CodeRabbit inference engine (CONTRIBUTING.md)

User-visible changes must be recorded under the [Unreleased] section.

For the full pull request diff against its base branch, modify CHANGELOG.md when user-visible behavior changes, including emulator output, frontend features, CLI flags, public APIs, or AccuracySNES cartridge contents. Do not require it for purely internal changes, tests, comments, or CI configuration.

Files:

  • CHANGELOG.md
🪛 markdownlint-cli2 (0.23.1)
CHANGELOG.md

[warning] 38-38: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (7)
docs/scheduler.md (1)

106-107: Restore the corrected validation statement.

The text still says that both hdmaen_latch_test goldens were unchanged. The PR validation record identifies an unblessed hdmaen_latch_test difference and a pre-existing reference mismatch. Remove this claim.

crates/rustysnes-ppu/src/lib.rs (1)

1077-1096: LGTM!

Also applies to: 1385-1412

crates/rustysnes-core/src/bus.rs (1)

87-99: LGTM!

Also applies to: 818-818, 1604-1650

crates/rustysnes-test-harness/src/bin/perdot_dump.rs (1)

32-84: LGTM!

scripts/perdot_crossval.sh (1)

15-23: LGTM!

docs/ppu.md (1)

93-95: LGTM!

CHANGELOG.md (1)

135-145: 📐 Maintainability & Code Quality

Verify the recorded golden status against the committed fixture.

These lines state that the fixture changed to 0x8f60351e0cdd8125. The PR objective summary also states that hdmaen_latch_test remains unchanged and unblessed. Inspect the fixture diff and retain only the statement that matches the committed artifact.

Also applies to: 167-172

Comment on lines +95 to +110
let fb = sys.bus.framebuffer();
let width = 256;
let rows = fb.len() / width;
let sig = (0..rows)
.map(|y| {
let row = &fb[y * width..(y + 1) * width];
let first = canonical(row[0]);
if row.iter().all(|&px| canonical(px) == first) {
format!("{first:04x}")
} else {
"----".to_owned()
}
})
.collect::<Vec<_>>()
.join(",");
println!("PERDOTROWS rows={rows} sig={sig}");

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- Rust target outline ---'
ast-grep outline crates/rustysnes-test-harness/src/bin/perdot_dump.rs
printf '%s\n' '--- Rust relevant code ---'
sed -n '1,150p' crates/rustysnes-test-harness/src/bin/perdot_dump.rs
printf '%s\n' '--- Lua relevant code ---'
sed -n '1,130p' scripts/perdot_capture.lua
printf '%s\n' '--- framebuffer and screen-width references ---'
rg -n -S 'framebuffer|width|hi.?res|screen.?buffer|GetScreen|GetPixel' crates scripts docs --glob '*.rs' --glob '*.lua' --glob '*.md' | head -250

Repository: doublegate/RustySNES

Length of output: 40222


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
for p in [Path("crates/rustysnes-test-harness/src/bin/perdot_dump.rs"),
          Path("scripts/perdot_capture.lua")]:
    print(f"--- {p} ---")
    text = p.read_text()
    for i, line in enumerate(text.splitlines(), 1):
        if "framebuffer" in line.lower() or "width" in line.lower() or "PERDOTROWS" in line or "buf" in line:
            print(f"{i}: {line}")
PY
printf '%s\n' '--- all PERDOTROWS references ---'
rg -n -S 'PERDOTROWS|PERDOT_ROWS|perdot' . --glob '!snesdev_wiki/**' | head -300

Repository: doublegate/RustySNES

Length of output: 6416


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- PPU framebuffer declarations and accessors ---'
rg -n -S 'framebuffer|frame_hires|frame_width|frame_height|take_frame|frame_size' crates/rustysnes-ppu crates/rustysnes-core crates/rustysnes-test-harness --glob '*.rs' | head -300
printf '%s\n' '--- PPU implementation outline ---'
for f in $(rg -l -S 'frame_hires|pub fn framebuffer|framebuffer:' crates/rustysnes-ppu crates/rustysnes-core --glob '*.rs'); do
  echo "--- $f"
  ast-grep outline "$f"
done
printf '%s\n' '--- related Mesen Lua dimensions ---'
sed -n '60,115p' scripts/accuracysnes/mesen_scenes.lua
printf '%s\n' '--- cross-validation driver ---'
sed -n '1,135p' scripts/perdot_crossval.sh

Repository: doublegate/RustySNES

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- Rust width API and framebuffer contract ---'
sed -n '95,150p' crates/rustysnes-ppu/src/lib.rs
sed -n '860,900p' crates/rustysnes-ppu/src/lib.rs
sed -n '1128,1150p' crates/rustysnes-ppu/src/lib.rs
sed -n '20,50p' crates/rustysnes-core/src/facade.rs
sed -n '350,405p' crates/rustysnes-core/src/facade.rs
printf '%s\n' '--- exact cross-validation invocation ---'
rg -n -A18 -B4 'PERDOT_ROWS|perdot_dump|MCE_RESULT|PERDOT distinct' scripts/perdot_crossval.sh
printf '%s\n' '--- Lua API/documentation references in repository ---'
rg -n -S 'getScreenBuffer|screen buffer|GetScreenBuffer|screenBuffer|GetVideo' . --glob '!snesdev_wiki/**' | head -200

Repository: doublegate/RustySNES

Length of output: 15238


🌐 Web query:

Mesen Lua emu.getScreenBuffer width height hi-res screen buffer API metadata

💡 Result:

In the Mesen emulator Lua API, screen buffer and dimension data can be accessed using the following functions: emu.getScreenBuffer This function returns an array of ARGB integer values representing the current contents of the console's screen [1]. Because the array size can change depending on the console, game, or scene, it should not be assumed to be a fixed size [1]. You can use this array to read pixel data or modify the screen's contents by passing a modified array back to emu.setScreenBuffer(screenBuffer) [1][2]. emu.getScreenSize This function returns a table containing the current width and height of the console's screen output, formatted as {width = int, height = int} [1][3]. This is the recommended method for determining the dimensions of the buffer returned by emu.getScreenBuffer [1]. Regarding hi-res or metadata: The Mesen Lua API does not provide a dedicated "hi-res" flag or specific metadata field for high-resolution buffers in these calls [1][3]. The dimensions returned by emu.getScreenSize will reflect the active resolution of the emulation core [1]. If you are working with buffers that may vary in size (e.g., due to overscan or different resolution modes in emulated consoles), always query emu.getScreenSize dynamically to ensure your script correctly handles the current width and height [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re

rust = Path("crates/rustysnes-ppu/src/lib.rs").read_text()
dump = Path("crates/rustysnes-test-harness/src/bin/perdot_dump.rs").read_text()
lua = Path("scripts/perdot_capture.lua").read_text()

screen_width = int(re.search(r'pub const SCREEN_WIDTH: usize = (\d+);', rust).group(1))
hires_width = int(re.search(r'pub const MAX_SCREEN_WIDTH: usize = (\d+);', rust).group(1))
height = int(re.search(r'pub const SCREEN_HEIGHT: usize = (\d+);', rust).group(1))
rust_width = int(re.search(r'let width = (\d+);', dump).group(1))
lua_width = int(re.search(r'local width = (\d+)', lua).group(1))

for width in (screen_width, hires_width):
    pixels = width * height
    rows = pixels // rust_width
    print(f"active_width={width} pixels={pixels} hardcoded_width={rust_width} reported_rows={rows}")

print(f"Rust normal={screen_width}, Rust hi-res={hires_width}, Rust serializer={rust_width}")
print(f"Lua serializer={lua_width}")
assert screen_width == rust_width == lua_width
assert hires_width != rust_width
assert hires_width * height // rust_width == 2 * height
PY

Repository: doublegate/RustySNES

Length of output: 368


Use the active framebuffer width in both row-signature serializers.

A 512-wide hi-res frame is split into two 256-pixel tokens per scanline. This doubles rows and corrupts uniform-row and calibration results. Derive the Rust width from fb.len() and query MesenCE with emu.getScreenSize().width.

📍 Affects 2 files
  • crates/rustysnes-test-harness/src/bin/perdot_dump.rs#L95-L110 (this comment)
  • scripts/perdot_capture.lua#L80-L97
🤖 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 `@crates/rustysnes-test-harness/src/bin/perdot_dump.rs` around lines 95 - 110,
Update both row-signature serializers: in
crates/rustysnes-test-harness/src/bin/perdot_dump.rs, derive the active row
width from fb.len() so rows and slices use the full framebuffer width; in
scripts/perdot_capture.lua, query MesenCE with emu.getScreenSize().width and use
that width for row serialization. Preserve the existing uniform-row and
calibration output behavior for both resolutions.

@doublegate
doublegate merged commit 23fb76a into main Aug 1, 2026
16 checks passed
@doublegate
doublegate deleted the feat/short-scanline-b2-02 branch August 1, 2026 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants