Skip to content

video: do not synthesize a picture for a line whose fetch never ran#167

Merged
LinuxJedi merged 1 commit into
mainfrom
fix/uncaptured-line-phantom-render
Jul 11, 2026
Merged

video: do not synthesize a picture for a line whose fetch never ran#167
LinuxJedi merged 1 commit into
mainfrom
fix/uncaptured-line-phantom-render

Conversation

@LinuxJedi

Copy link
Copy Markdown
Owner

Problem

In the Rampage (TEK, 1994) end scroller, a row of bright, word-skewed dots appears one line above the big letters (reported with copperline-state-20260711154559.clstate / the matching screenshot).

The demo's copper list turns the display off (BPLCON0=$0000) at line 227, then at WAIT v248,h14 re-points BPLxPT at the scroller bitmap, rewrites DDFSTRT $38->$28 (landing ~h42, after the line's comparator point at h40 has passed), and re-enables BPLCON0=$3200 (~h50). Agnus therefore starts no fetch run on line 248; the first bitmap row is fetched on line 249, and the letters start at 250.

Copperline's DMA side models this correctly - the slot map shows zero bitplane slots on line 248 and a full 69-slot run on 249. But the renderer's legacy register-derived fallback (used when a line has no captured DMA row) synthesized a fetch for line 248 anyway, painting a phantom copy of bitmap row 0 with per-plane word skew (planes 1+3 shifted +16 lores px, plane 2 +32 px - the pattern of joining the lores fetch unit mid-way).

Fix

Gate the synthesized re-fetch path on the DMA capture: when the frame carries captured bitplane rows, a line whose capture slot is empty fetched nothing and paints no playfield.

  • A captured row that merely disagrees with the register-derived geometry (mid-row rewrites) still takes the re-fetch path as before.
  • Frames rendered without any capture (COPPERLINE_RENDER_LIVE_CHIP_RAM) keep the synthesized path.
  • This generalizes the capture-authority rule the sub-$18 DDFSTRT gate (vAmigaTS Agnus/DDF oldhwstop3/4) already applied.

The remaining faint dot row on line 249 is authentic: the demo's scroller ring buffer carries stray bits in its first bitmap row (identical across all three planes, scrolling along with the letters), and the real fetch displays them. vAmiga could not be used as a cross-check for this scene - VAHeadless sticks on the demo's dunes scene from ~350s and never reaches the scroller.

Verification

  • New regression test bus::tests::line_without_captured_fetch_paints_no_playfield - fails without the gate, passes with it.
  • Full cargo test --release green, including the golden probe render suite (goldens unchanged, no re-bless).
  • Rampage scene at 424.5s: full-frame diff against the previous build is exactly the 36 phantom pixels on the one row; the letters and the authentic row are untouched.
  • gen-x at 80s and 150s (mid-line BPLCON0 stress cases): byte-identical screenshots before/after.
  • cargo clippy and cargo fmt --check clean.

Agnus arms a line's bitplane fetch run at the DDFSTRT comparator match.
When BPLCON0 is raised mid-line only after that point has passed (the
Rampage bottom-scroller band entry: display off since v227, then at
WAIT v248,h14 the copper re-points BPLxPT and lands DDFSTRT $38->$28 at
~h42 and BPLCON0=$3200 at ~h50), no run starts until the next line. The
DMA side already models this (the line allocates no bitplane slots), but
the renderer's register-derived fallback still synthesized a fetch for
the line, painting a phantom, per-plane word-skewed copy of the first
bitmap row one line above the real first fetch row.

Gate the synthesized re-fetch path on the DMA capture: when the frame
carries captured bitplane rows, a line whose capture slot is empty
fetched nothing and paints no playfield. A captured row that merely
disagrees with the register-derived geometry still takes the re-fetch
path, and frames rendered without any capture
(COPPERLINE_RENDER_LIVE_CHIP_RAM) keep the synthesized path. This is the
same capture-authority rule the sub-$18 DDFSTRT gate (vAmigaTS
oldhwstop3/4) already applied, generalized.

The faint second dot row on the line below is authentic: the demo's
scroller ring buffer carries stray bits in its first bitmap row, and the
real fetch displays them.

Regression test: bus::tests::line_without_captured_fetch_paints_no_playfield
(fails without the gate). Golden probe renders are unchanged; gen-x
80s/150s screenshots are byte-identical.
@LinuxJedi LinuxJedi merged commit 1071ab5 into main Jul 11, 2026
8 checks passed
@LinuxJedi LinuxJedi deleted the fix/uncaptured-line-phantom-render branch July 11, 2026 16:05
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