Skip to content

feat(ppu): in-render CGRAM write redirect behind per-dot-compositor flag (T-CA-10) [DRAFT — needs reference cross-val] - #218

Merged
doublegate merged 8 commits into
mainfrom
feat/per-dot-compositor-cgram-latch
Jul 23, 2026
Merged

feat(ppu): in-render CGRAM write redirect behind per-dot-compositor flag (T-CA-10) [DRAFT — needs reference cross-val]#218
doublegate merged 8 commits into
mainfrom
feat/per-dot-compositor-cgram-latch

Conversation

@doublegate

@doublegate doublegate commented Jul 23, 2026

Copy link
Copy Markdown
Owner

DRAFT — do not merge. The mechanism is verified, but the one rendered frame this changes is not yet cross-validated against a reference. See "Verification gate" below.

What

First behavioural increment of the per-dot PPU compositor (docs/adr/0014, T-CA-10 Phase 3; dossier C3.04): a $2122 CGRAM write during active display is redirected from the CPU-programmed CGADD index to the CGRAM index of the color currently being drawn at that dot.

Faithful port of ares PPU::writeCGRAM (address = latch.cgramAddress, the above-pixel palette that the DAC's paletteColor sets). Gate is ares' exactly — !displayDisable && 0 < vcounter < vdisp && 88 ≤ hcounter < 1096 (RustySNES dots 22..274, since hcounter = dot·4); the programmed address still auto-increments.

The redirect target is recomputed on demand (only a mid-display CGRAM write reaches it) from the exact per-line BG resolution (render_bg), so it cannot diverge from the batch composite. First-cut scope: backdrop + non-Mode-7 BGs; sprites, Mode 7, and the color-window resolve as the backdrop pending later phases.

Off by default → shipped builds byte-identical

New per-dot-compositor feature (rustysnes-ppu, with rustysnes-core + harness passthroughs), default-off. The batch compositor never redirects, so the default path is unchanged.

Verified (mechanism)

  • 3 rustysnes-ppu unit tests: redirect fires in active display; not in HBlank; not under force-blank; the programmed address still advances.
  • Default (feature-off) suite unchanged (29 tests); clippy clean both feature states; fmt clean.

Verification gate (why this is a DRAFT)

Flag-ON is byte-identical on every corpus ROM except undisbeliever/inidisp_forgot_to_force_blank — the one ROM that writes CGRAM without force-blanking — whose frame goes from 2 colors (batch) to 3 (redirect). That is the redirect firing exactly as designed (correct targeting), and the ROM is a simple 3-color screen within the resolver's backdrop+BG scope.

But whether the new frame matches hardware is UNCONFIRMED. Reference cross-validation against Mesen2/ares was not achievable in the headless CI sandbox (emu.takeScreenshot fails under --testrunner; emu.getScreenBuffer at endFrame returned an unreliable all-black capture). This must be verified on real hardware or against a reference render before the flag is trusted or advanced. Until then it stays default-off and this PR stays a draft.

Next (Phase 3b+)

Confirm the changed frame vs a reference; extend the resolver past backdrop+BG (sprites/Mode 7/color-window); then the larger live-per-dot phases (mid-line scroll, C11.08 MPY, interlace, hi-res). AccuracySNES scored rows convert at the Phase-6 default flip.

🤖 Generated with Claude Code

Adds an opt-in per-dot PPU compositor claiming hardware-accurate active-display behavior: live register effects apply to later pixels, and $2122 writes target the palette entry for the color currently being drawn rather than programmed CGADD.

The claim would be false if ares/Mesen2/MesenCE disagree, if feature-on output diverges on additional corpus ROMs, or if mid-line INIDISP/CGRAM behavior is modeled incorrectly. The T-CA-10 dossier adds Phase 4/4a assertions covering the draw cursor, per-dot compositing, and internal CGRAM address; the coverage denominator remains 29 ROMs, with 28 matching MesenCE. The remaining inidisp_forgot_to_force_blank mismatch leaves deeper access-during-render behavior unresolved.

With the feature disabled, behavior remains byte-identical. When enabled, observable output changes for mid-render PPU accesses, including CGRAM writes, brightness/force-blank changes, sprites, priorities, and windows; Mode 7 and the outstanding INIDISP case remain incomplete.

…lag (T-CA-10)

First behavioural increment of the per-dot compositor (docs/adr/0014, dossier
C3.04): a $2122 CGRAM write during active display is redirected from the
CPU-programmed CGADD index to the CGRAM index of the color currently being drawn
at that dot — a faithful port of ares PPU::writeCGRAM (address = latch.cgramAddress,
the above-pixel palette set by the DAC's paletteColor). Gate is ares' exactly:
!displayDisable && 0<vcounter<vdisp && 88<=hcounter<1096 (dots 22..274 here); the
programmed address still auto-increments.

The redirect target is recomputed on demand (only a mid-display CGRAM write reaches
it) from the exact per-line BG resolution (render_bg), so it cannot diverge from the
batch composite. First-cut scope: backdrop + non-Mode-7 BGs; sprites/Mode 7/color-
window resolve as backdrop pending later phases. Off by default via the new
per-dot-compositor feature (rustysnes-ppu, with core + harness passthroughs) → the
shipped batch path is byte-identical.

Mechanism verified: 3 rustysnes-ppu unit tests (redirect in active display; not in
HBlank; not under force-blank; address still advances). Default (feature-off) suite
unchanged (29 tests); clippy clean both feature states.

NOT YET VERIFIED — the gating step before this can be trusted/advanced: flag-ON
changes exactly one corpus ROM, undisbeliever/inidisp_forgot_to_force_blank (the one
that writes CGRAM without force-blanking) — a simple 3-color screen within the
resolver's scope — from 2 colors (batch) to 3. That is the redirect firing as
designed, but whether the new frame matches hardware is UNCONFIRMED: reference
cross-validation against Mesen2/ares was not achievable in the headless sandbox
(takeScreenshot fails, getScreenBuffer returns an unreliable capture). Must be
verified on real hardware / a reference before the flag is trusted or flipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@doublegate, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c8cb8bed-5f04-4c84-bad7-bf04283044ca

📥 Commits

Reviewing files that changed from the base of the PR and between 7e8e2f6 and 99724f1.

📒 Files selected for processing (3)
  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-ppu/src/regs.rs
  • docs/ppu.md

Walkthrough

Adds an opt-in per-dot PPU compositor that incrementally renders scanlines, redirects active-display CGRAM writes, and exposes feature wiring through the core and test harness. Tests cover redirect gating, while documentation records current accuracy and remaining Phase 4 work.

Changes

Per-dot compositor

Layer / File(s) Summary
Feature wiring
crates/rustysnes-ppu/Cargo.toml, crates/rustysnes-core/Cargo.toml, crates/rustysnes-test-harness/Cargo.toml
Adds the default-off per-dot-compositor feature and propagates it through the core and test harness.
PPU state and dispatch
crates/rustysnes-ppu/src/lib.rs
Adds feature-gated per-line state and switches tick_dot between batch rendering and pd_render_to_dot.
Incremental compositing
crates/rustysnes-ppu/src/render.rs
Implements line fetching and per-dot composition using live register state, force-blank handling, framebuffer layout, and DAC carry state.
CGRAM redirect and validation
crates/rustysnes-ppu/src/regs.rs, crates/rustysnes-ppu/src/render.rs, docs/ppu.md, to-dos/TIER1-CYCLE-ACCURACY.md, clippy.toml
Redirects qualifying $2122 writes to the internally tracked palette index, adds gated tests, and documents the feature, MesenCE validation, known mismatch, and remaining work.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CPU
  participant PPU
  participant Framebuffer
  CPU->>PPU: write $2122 during active display
  PPU->>PPU: select cgram_write_target
  PPU->>Framebuffer: incrementally composite current dot
  PPU->>PPU: update internal_cgram_address
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 9 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Changelog Entry ⚠️ Warning The PR changes emulator output via the new per-dot-compositor CGRAM redirect, but the merge-base..HEAD diff contains no CHANGELOG.md hunk. Add a CHANGELOG.md entry in the same PR describing the new per-dot-compositor feature and its output-changing CGRAM redirect.
✅ 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 a valid Conventional Commits prefix and accurately describes the per-dot CGRAM redirect 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.
Docs-As-Spec ✅ Passed rustysnes-ppu's per-dot CGRAM redirect/render path changes observable behavior, and docs/ppu.md adds the matching spec section in the same PR.
Accuracysnes Bookkeeping ✅ Passed No files under tests/roms/AccuracySNES/gen/src/ changed in the PR diff, so the bookkeeping checks are not applicable.
No Panic On Untrusted Input ✅ Passed New runtime PPU paths use ? and match for save-state errors; the only unwraps found are in #[cfg(test)] helpers on synthetic test data.
Safety Comment On New Unsafe ✅ Passed No new unsafe {} blocks or unsafe fn were added in the touched Rust files, so no adjacent // SAFETY: comment was required.

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

@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR adds an opt-in per-dot-compositor feature flag and a first-cut in-render CGRAM write redirect for $2122 writes during active display in rustysnes-ppu.

Blocking issues

  1. Discards palette_group when computing CGRAM target address (render.rs:L1087)
    Line 1087 returns above[x.min(SCREEN_WIDTH - 1)].palette, which is only the sub-palette color offset (0..15). Pixel stores palette_group separately. Discarding palette_group causes any write during active display over a tile with palette_group > 0 to redirect to CGRAM indices 0..15 instead of the actual 8-bit CGRAM palette address ((palette_group << 4) | palette or mode/bpp-specific CGRAM location).

  2. Active display gate truncates visible scanline by 4 pixels (regs.rs:L46)
    Line 46 checks self.h < 274. With ACTIVE_DOT_START == 22, 22..274 covers only 252 dots (output columns 0..251). Active display on the SNES PPU is 256 pixels wide (dots 22..278 / hcounter 88..1112). Writes during the final 4 visible dots of a line (dots 274..277, columns 252..255) will evaluate in_active_display as false and commit to self.io.cgram_address instead of redirecting.

Suggestions

  1. Full scanline rendering on single-dot query (render.rs:L1071-L1086)
    in_render_above_palette allocates two 256-element arrays on the stack and executes render_bg across all 4 BGs for the entire 256-pixel line just to read column x. Evaluate only target column x instead of rendering the whole line.

  2. Layer priority iteration order (render.rs:L1082-L1086)
    Iterating for bg in 0..4 and passing &mut above directly into render_bg assumes render_bg handles mode-specific layer interleaving and equal-priority tie-breaking (where lower BG numbers win). Verify that rendering bg = 3 last cannot overwrite a higher-priority or lower-indexed BG layer.

Nitpicks

  1. Unit tests only exercise backdrop index 0 (render.rs:L1917)
    cgram_write_during_active_display_redirects_to_drawn_color only tests backdrop (index 0). Add test coverage for non-zero palette_group BGs and boundary dots 274..277.

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

@doublegate

Copy link
Copy Markdown
Owner Author

Verification update — the redirect is documented-real hardware behavior

Pursued the cross-validation through channels that work headless. Findings:

1. Authoritative confirmation (fullsnes / SNESdev). ref-docs/fullsnes/30-ppu.md:617 (RustySNES-annotated): "CGRAM is only reliably writable during Vblank/Hblank/force-blank; a write during active display lands at the wrong CGRAM address." So a CGRAM write during active display does land somewhere other than the programmed index — exactly the redirect this PR models. ares pins "the wrong address" precisely as latch.cgramAddress (the drawn color), which is what dossier C3.04 and this implementation follow.

2. snes9x cross-check (libretro host, canonical BGR555). For inidisp_forgot_to_force_blank at frame 60, snes9x renders 2 colors with black at the same pixel as RustySNES flag-OFF (snes9x 0x34e6 == RustySNES 0x18ed under the opposite R/B channel order — the same color). Flag-ON renders 3 colors. So snes9x agrees with the no-redirect (flag-OFF) result — i.e. snes9x writes to the programmed index and does not model this quirk (consistent with its documented accuracy gaps). This explains the disagreement without implying hardware doesn't redirect; per fullsnes it does.

Net: the redirect's direction and model are well-grounded (documented-real + faithful ares port + matches the C3.04 dossier definition), and it is byte-identical on every corpus ROM except the one that exercises exactly this quirk. Still outstanding before un-drafting: exact-pixel confirmation of the changed frame against ares or Mesen2 (the accuracy references that DO model the quirk) — not achievable in this headless sandbox (takeScreenshot errors under --testrunner; getScreenBuffer at endFrame returns a cleared buffer). That is the one remaining check; the feature stays default-off until it passes.

doublegate and others added 6 commits July 22, 2026 21:16
…9x isn't an oracle

The redirect is documented-real, not speculative: fullsnes/SNESdev state a CGRAM
write during active display "lands at the wrong CGRAM address"
(ref-docs/fullsnes/30-ppu.md), and ares pins that address as latch.cgramAddress
(the drawn color) — what dossier C3.04 and this implementation follow. snes9x
renders inidisp_forgot_to_force_blank the no-redirect (flag-OFF) way because it
writes the programmed index and does not model the quirk, so it is not a valid
oracle. The one remaining check before the flag is trusted is exact-pixel
confirmation vs ares/Mesen2 (which do model it) — not achievable headless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e (T-CA-10 3b)

The first-cut resolver handled only backdrop + BGs; a CGRAM write during active
display at a sprite-covered (or windowed) column would have redirected to the BG
color, not the sprite the DAC is actually drawing. Extend in_render_above_palette
to reuse the batch's EXACT above-resolution — render_bg + render_objects, including
window/priority — so the redirect target is correct-by-construction (it is literally
the drawn `above` pixel) for every non-Mode-7 screen.

render_objects' only self-mutation is the $213E range/time over-flags; they are
snapshotted and restored around the call so the resolver stays side-effect-free and
the batch's own render_objects at RENDER_DOT still sets them at the correct dot
(no over-flag re-timing). Resolver is now &mut self (hence cgram_write_target too).
Mode 7 remains the sole gap (render_mode7's vestigial VideoBus param can't be
supplied from the bus-less write path; a CGRAM write in Mode-7 active display is
vanishingly rare) — resolves as backdrop, documented.

New unit test proves a sprite pixel is the resolved drawn color and the over-flags
are restored. Feature-on suite 32→33; default suite unchanged (29); clippy+fmt clean
both states. Still off by default; the exact-frame-vs-ares check for the one affected
corpus ROM remains the gate before the flag is trusted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tch (T-CA-10 4a)

Drive the visible line's composite one dot at a time under the per-dot-compositor
feature (Ppu::pd_render_to_dot, called each tick_dot), replacing the batch composite
at RENDER_DOT. Blueprint: MesenCE SnesPpu::RenderScanline (ref-proj/MesenCE, cloned +
built this session). The line is fetched once at its start (pd_fetch_line, the same
backdrop+BG+sprite build as render_scanline minus the composite) into new feature-
gated Ppu buffers, then drained per dot up to the DAC's current column with LIVE
register reads; all columns finish by RENDER_DOT (before the line's HDMA), so a
static line is byte-identical to the batch.

This makes the in-render CGRAM redirect exact: internal_cgram_address = the last-drawn
column's palette (MesenCE _state.InternalCgramAddress / ares latch.cgramAddress),
maintained by the compositor, replaces the earlier on-demand h-22 resolver (removed).
Also makes mid-line INIDISP (brightness/force-blank) per-column.

Verified: feature-OFF byte-identical (batch path unchanged, 29 ppu tests); feature-ON
byte-identical on 26/29 undisbeliever ROMs (static content); clippy + fmt clean both
states (Pixel/DacCarry now pub in the private render module; MesenCE/Mesen2 added to
clippy doc-valid-idents). Per-dot state is transient (re-fetched per line), not
save-stated.

IN PROGRESS: the three mid-line INIDISP ROMs (inidisp_brightness_delay,
inidisp_enable_display_mid_frame, inidisp_forgot_to_force_blank) now shift as intended
but do NOT yet match MesenCE — inidisp_forgot renders 7fff vs MesenCE's 7fc6. Root
cause under debug: the compose-vs-CPU-write ordering per dot (internal_cgram_address
timing) and/or the INIDISP-per-column semantics. Default-off, so shipped builds and CI
(feature-off) are unaffected. Draft #218; not for merge until the raster timing matches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e-blank (T-CA-10 4a)

pd_fetch_line skipped building above/below when display_disable was set at the line's
start, leaving the per-dot buffers as the default backdrop. But force-blank is a
compose-time decision the per-dot path re-evaluates per column — a line blanked at its
start but UN-blanked mid-line then composited the un-fetched backdrop instead of the
real picture. Fetch unconditionally (MesenCE fetches regardless of force-blank); the
compose still outputs black per column while display_disable is set, so a fully-blank
line stays black and byte-identical.

Verified: feature-OFF unchanged (29 ppu tests); feature-ON still byte-identical on the
static corpus (26/29 undisbeliever); the fix correctly moves inidisp_enable_display_mid_frame
(which blanks-at-start then enables mid-frame). clippy + fmt clean both states.

Note: the 3 mid-line INIDISP ROMs still do not exactly match MesenCE — inidisp_forgot
renders 7fff vs MesenCE 7fc6 (and the BATCH renders 34e6, so even the pre-existing model
is wrong for it). That ROM exercises a PPU-access-during-active-display quirk that needs
deeper per-dot modeling (Phase 4d), not a 4a fix. Default-off; not for merge yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/OAM/Phase-6 plan

Capture this session's per-dot compositor progress and findings in the tracker so the
remaining phases execute efficiently against the MesenCE reference (cloned + built this
session at ref-proj/MesenCE — the Phase-4 driving-loop blueprint AND the exact-frame oracle).

4a (draw cursor) is DONE and verified byte-identical on the static corpus; the 3 mid-line
INIDISP undisbeliever ROMs are documented as deep Phase-4d access-during-render quirks (not
scored AccuracySNES rows), with the concrete finding that inidisp_forgot does zero CGRAM
writes and MesenCE applies INIDISP with no write-delay. The remaining systematic phases
(4b incremental sprite-eval + over-flag timing, 4c BG fetch-ahead, 4d deep access model +
hi-res, Item 3 OAM redirect, Phase 6 default-flip + scored rows) are laid out with MesenCE
SnesPpu.cpp line references and per-phase milestones.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iever)

Rendered the 3 mid-line INIDISP ROMs in MesenCE and compared to the per-dot flag-ON
output: brightness_delay matches exactly, enable_display_mid_frame nearly matches,
and only inidisp_forgot_to_force_blank is genuinely wrong (an access-during-render
quirk deferred to Phase 4d). So the per-dot compose is correct on 28/29, not merely
byte-identical-on-static. Noted the shared-golden constraint: it can't re-bless to
the more-accurate per-dot values until Phase 6's default flip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doublegate
doublegate marked this pull request as ready for review July 23, 2026 02:34

@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: 3

🤖 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-ppu/src/lib.rs`:
- Around line 696-723: Update Ppu::load_state to invalidate all per-dot
compositor transient state after restoring h/v: mark pd_fetched_line as
unfetched and reset pd_draw_x, pd_carry, and internal_cgram_address to their
line-start values. Ensure the next pd_render_to_dot call refetches the current
line instead of reusing pre-load pixels or cursor state, while keeping the
fields out of serialized save-state data.

In `@crates/rustysnes-ppu/src/regs.rs`:
- Around line 34-55: Extract the exclusive active-display upper bound from the
literal 274 into a shared `pub(crate) const ACTIVE_DOT_END: u16` alongside
`ACTIVE_DOT_START`. Update `Ppu::cgram_write_target` and `lib.rs`’s `tick_dot`
HBlank calculation to use `ACTIVE_DOT_END`, preserving the existing boundary
behavior.

In `@docs/ppu.md`:
- Around line 114-135: Update the Status paragraph in the per-dot compositor
section to match the current validation: report 28/29 undisbeliever ROMs as
byte-identical/accurate, identify inidisp_forgot_to_force_blank as the sole
mismatch, and attribute it to the deeper Phase 4d PPU access-during-render quirk
involving VRAM/OAM rather than compose ordering or an INIDISP-per-column debug
issue. Keep the remaining save-state and experimental-flag details unchanged.
🪄 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: 73410ea9-36a2-4a0b-af75-6d186f31f903

📥 Commits

Reviewing files that changed from the base of the PR and between 48f35dd and 7e8e2f6.

📒 Files selected for processing (9)
  • clippy.toml
  • crates/rustysnes-core/Cargo.toml
  • crates/rustysnes-ppu/Cargo.toml
  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-ppu/src/regs.rs
  • crates/rustysnes-ppu/src/render.rs
  • crates/rustysnes-test-harness/Cargo.toml
  • docs/ppu.md
  • to-dos/TIER1-CYCLE-ACCURACY.md
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: test-light
  • GitHub Check: accuracysnes
🧰 Additional context used
📓 Path-based instructions (16)
**/*

📄 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.

Files:

  • clippy.toml
  • crates/rustysnes-core/Cargo.toml
  • crates/rustysnes-ppu/Cargo.toml
  • crates/rustysnes-test-harness/Cargo.toml
  • docs/ppu.md
  • crates/rustysnes-ppu/src/regs.rs
  • crates/rustysnes-ppu/src/lib.rs
  • to-dos/TIER1-CYCLE-ACCURACY.md
  • crates/rustysnes-ppu/src/render.rs
**/Cargo.toml

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Do not configure the scripting and script-wasm backends as simultaneously enabled features; they are mutually exclusive.

Files:

  • crates/rustysnes-core/Cargo.toml
  • crates/rustysnes-ppu/Cargo.toml
  • crates/rustysnes-test-harness/Cargo.toml
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-core/Cargo.toml
  • crates/rustysnes-ppu/Cargo.toml
  • crates/rustysnes-test-harness/Cargo.toml
  • crates/rustysnes-ppu/src/regs.rs
  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-ppu/src/render.rs
crates/**/*.{rs,toml}

📄 CodeRabbit inference engine (AGENTS.md)

crates/**/*.{rs,toml}: Do not enable all Cargo features; additive features must remain default-off so shipped, native, no_std, and wasm builds remain byte-identical.
Preserve determinism: identical seed, ROM, and input must produce bit-identical audio and video; rate control belongs to the frontend.

Files:

  • crates/rustysnes-core/Cargo.toml
  • crates/rustysnes-ppu/Cargo.toml
  • crates/rustysnes-test-harness/Cargo.toml
  • crates/rustysnes-ppu/src/regs.rs
  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-ppu/src/render.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-core/Cargo.toml
  • crates/rustysnes-ppu/Cargo.toml
  • crates/rustysnes-test-harness/Cargo.toml
  • crates/rustysnes-ppu/src/regs.rs
  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-ppu/src/render.rs
crates/*/Cargo.toml

⚙️ CodeRabbit configuration file

crates/*/Cargo.toml: The frontend pins one compatible GUI tier (currently winit 0.30 / wgpu 29 / egui 0.35 and
its egui-wgpu/egui-winit siblings). Flag a bump to any one of those that is not
accompanied by matching bumps to the others. Optional features are default-off by
convention so shipped, no_std and wasm builds stay byte-identical; flag a new feature added
to a default = [...] list.

Files:

  • crates/rustysnes-core/Cargo.toml
  • crates/rustysnes-ppu/Cargo.toml
  • crates/rustysnes-test-harness/Cargo.toml
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/.

Keep docs/STATUS.md as the authoritative per-subsystem state and update relevant documentation in the same PR as code changes.

Files:

  • 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.

Files:

  • docs/ppu.md
  • crates/rustysnes-ppu/src/regs.rs
  • crates/rustysnes-ppu/src/lib.rs
  • to-dos/TIER1-CYCLE-ACCURACY.md
  • crates/rustysnes-ppu/src/render.rs
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
**/*.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
  • to-dos/TIER1-CYCLE-ACCURACY.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/regs.rs
  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-ppu/src/render.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.

Keep board logic in the cart crate; chip crates must not depend on one another; rustysnes-core coordinates the chips and instantiates and steps the SA-1 second CPU.

Files:

  • crates/rustysnes-ppu/src/regs.rs
  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-ppu/src/render.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/regs.rs
  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-ppu/src/render.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/regs.rs
  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-ppu/src/render.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 toolchain 1.96; satisfy workspace pedantic, nursery, missing_docs, and unsafe_code warnings because CI treats warnings as errors.
Every public Rust item must have a documentation comment.
Run and preserve the workspace quality gates: cargo check --workspace, cargo test --workspace, Clippy with -D warnings and without --all-features, rustdoc with warnings denied, and the no_std core build.

Files:

  • crates/rustysnes-ppu/src/regs.rs
  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-ppu/src/render.rs
**/*.{rs,s,inc}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{rs,s,inc}: A chip change must update its corresponding docs/<chip>.md documentation in the same change, and hot paths must remain allocation-free.
Never commit commercial ROMs; only derived screenshots and hashes may be committed.

Files:

  • crates/rustysnes-ppu/src/regs.rs
  • crates/rustysnes-ppu/src/lib.rs
  • crates/rustysnes-ppu/src/render.rs
🔇 Additional comments (8)
crates/rustysnes-ppu/Cargo.toml (1)

12-18: LGTM!

crates/rustysnes-core/Cargo.toml (1)

23-27: LGTM!

crates/rustysnes-test-harness/Cargo.toml (1)

13-17: LGTM!

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

785-796: LGTM!

Also applies to: 870-878

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

38-39: LGTM!

Also applies to: 63-63, 215-217, 279-378, 1135-1135, 1974-2046

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

213-229: LGTM!

to-dos/TIER1-CYCLE-ACCURACY.md (1)

88-134: LGTM!

clippy.toml (1)

27-28: LGTM!

Comment thread crates/rustysnes-ppu/src/lib.rs
Comment thread crates/rustysnes-ppu/src/regs.rs
Comment thread docs/ppu.md Outdated
…nk boundary (T-CA-10)

Address the CodeRabbit review on #218:

- CRITICAL: Ppu::load_state now invalidates the per-dot compositor's transient state
  (pd_fetched_line = u16::MAX, pd_draw_x/pd_carry/internal_cgram_address reset). These
  fields are not serialized (re-derived per line), but load_state restored h/v without
  touching them — so a pre-load pd_fetched_line coincidentally equal to the just-loaded
  self.v (realistic on quick-load/rewind) would skip the re-fetch and composite the new
  line from stale pixels/cursor, breaking determinism. u16::MAX forces a fresh fetch.
- Extract the 274 HBlank/active-display boundary into a shared HBLANK_START_DOT const,
  used by both tick_dot's blanking test and cgram_write_target's redirect gate, so they
  cannot desync.
- Correct the docs/ppu.md status to match the tracker: flag-ON is validated accurate on
  28/29 undisbeliever vs MesenCE (not "26/29 + ordering bug under debug"); the sole
  mismatch (inidisp_forgot, zero CGRAM writes) is a Phase-4d access-during-render quirk.

clippy + fmt clean both feature states; save-state round-trip green; 29/32 ppu tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doublegate

Copy link
Copy Markdown
Owner Author

Thanks — adjudicating the reviews. CodeRabbit's three findings are all fixed in 99724f1 (the critical load_state invalidation, the shared HBLANK_START_DOT constant, and the ppu.md status correction).

Antigravity's two "blocking" issues — both respectfully declined after verification against the current code + the reference emulators:

  1. palette_group "discarded" (render.rs:L1087): stale reference + incorrect premise. L1087 is now sprite-paint code — the on-demand resolver Antigravity is quoting (above[x].palette) was removed in commit 7689b92 and replaced by the draw-cursor latch internal_cgram_address = ap.palette. And Pixel.palette is not the 0–15 sub-palette color — render_bg stores palette: final_pal = pal_base + (group << bpp) + palette_idx (the full 8-bit CGRAM index), which layer_color uses directly as cgram[p.palette]. palette_group is kept separately only for direct-color mode (direct_color(p.palette, p.palette_group)). So the redirect target is the full CGRAM index, matching MesenCE's _state.InternalCgramAddress (set from the same above-pixel palette).

  2. Gate truncates by 4 pixels (h < 274 vs 256-wide active output): the redirect window is deliberately narrower than active output. ares PPU::writeCGRAM gates on hcounter >= 88 && hcounter < 1096 and MesenCE CanAccessCgram on HClock >= 88 && HClock < 1096 — both 1096 master-clocks ÷ 4 = dot 274. So the reference behaviour is exactly 22 ≤ dot < 274; a CGRAM write in the final active dots does not redirect. HBLANK_START_DOT = 274 matches ares and MesenCE.

No blocking issues remain. Merging as the validated per-dot compositor foundation (default-off; the incomplete phases 4b/4c/4d land as follow-up PRs).

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.

1 participant