Skip to content

video: correct super-hi-res colour output and programmable-scan vertical presentation#252

Merged
LinuxJedi merged 4 commits into
mainfrom
fix/shres-sample-resolve-and-vsync-window
Jul 22, 2026
Merged

video: correct super-hi-res colour output and programmable-scan vertical presentation#252
LinuxJedi merged 4 commits into
mainfrom
fix/shres-sample-resolve-and-vsync-window

Conversation

@LinuxJedi

@LinuxJedi LinuxJedi commented Jul 22, 2026

Copy link
Copy Markdown
Owner

What

Following up on #247: the Linux/m68k amifb console (31 kHz VARBEAMEN super-hi-res, FMODE=3, 4 bitplanes) was readable but rendered with wrong colours - a green Tux, text whitened from its palette grey, and red/cyan fringes on every glyph edge that looked like a bitplane phase error - and the picture was vertically inflated compared with a real multisync monitor (FS-UAE reference). Two commits:

denise: resolve each SHRES 35 ns sample through the full palette
Super-hi-res output built each 70 ns framebuffer pixel's colour as palette[(left & 3) | (right & 3) << 2] - an encoded pair of the two 35 ns samples, truncating every sample to two bitplanes. The encoding only ever made sense against ECS Denise's 2-plane SHRES limit; real Denise/Lisa resolve every 35 ns sample through the full palette pipeline. On the 4-plane console the pair encoding mapped solid colour 14 to palette[10] (green Tux), colour 7 to palette[15] (whitened text), and glyph edges to palette[12]/palette[3] (the light-red/cyan fringes). It was not a timing bug: captured plane words and per-plane scroll delays were verified identical across planes before touching the output stage. Each half now resolves independently through denise_playfield_output (which itself routes the AGA BPLAM/dual-playfield/EHB path or the OCS/ECS path) and the two colours blend per channel into the one hires-pitch framebuffer pixel - a documented framebuffer-pitch compromise, with a true 35 ns output path left as the TODO it already is for SHRES sprites. The composite sample keeps its full index bits for collisions and sprite priority.

video: anchor the programmable-scan presentation at the vertical sync pulse
The vertical counterpart of #247's sync-anchored horizontal window. Programmable frames stretched their captured rows over the full glass height; a multisync locks its vertical deflection to the programmed pulse, so with VARVSYEN the glass now covers the frame from the VSYNC trailing edge to the next pulse and the captured window sits where the mode's own porches place it, bordered by blanked rows (amifb: 400 of 445 post-sync lines). programmable_vsync_window mirrors the HSYNC helper, the bus latches the v-window at the frame wrap beside the h-window (serde-skipped, recomputed after state load), and apply_presentation_v_window places the rows. Modes without a programmed vertical sync, and all standard scans, present exactly as before.

video: render programmable super-hi-res scans on a 35 ns canvas
With the colours fixed, SHRES output was still soft: the framebuffer's hi-res (70 ns) pixel pitch halved the horizontal resolution (the 640-pixel console occupied 320 canvas pixels, each 35 ns pair blended), and the presentation stretch magnified the loss. Frames now carry a canvas supersample factor: a programmable scan driving super-hi-res paints a double-width (1432-pixel, 35 ns pitch) canvas and each half of the resolved SHRES pair lands as its own pixel. Every logical coordinate in the replay (comparators, fetch origins, sprite positions, collision buffers) stays in the classic hi-res-pitch domain - only the framebuffer writes fan out - so standard scans are byte-identical (probe goldens unchanged) and their SHRES screens keep the blended classic pitch. The width threads through the whole presentation: the desktop window shows the wide canvas 1:1 on a 2x HiDPI texture, screenshots save 1432x1074 (4:3 kept, SHOT_RAW saves native width), the deinterlacer/recorder/frame-analyzer/CCP capture/browser frontend all carry or report it. Console text is now pixel-sharp at the full programmed resolution; true 35 ns sprite placement remains a TODO.

Verification

  • Full workspace suite green (1791 tests), including the probe-golden renders byte-identical and the new unit tests (SHRES per-sample resolve, 4-plane index regression named for the amifb console, v-window row placement/clip/degenerate cases, the canvas-scale gate, and the 35 ns texture copy paths).
  • Image-regression baselines green including the DblPAL programmable-scan structural test; the only failure is the documented ocs_bpu7_ham host-flaky perf test, failing identically on an unmodified tree.
  • DblPAL Workbench A/B against main: horizontal placement unchanged; the only delta is the intended vertical border placement, and it is byte-identical before/after the 35 ns canvas commit (DblPAL:LowRes uses 70 ns pixels, no SHRES bit, so it correctly stays on the classic canvas). OS3.9 A1200 Workbench boots clean.
  • Debian 3.1/m68k console verified at boot (t=70) and login (t=321): correct Tux (yellow/orange beak and feet), grey #AAAAAA text matching the programmed console palette, no edge fringes, multisync-correct vertical proportions, and pixel-sharp 1432x1074 output.
  • cargo clippy and cargo fmt --check clean; wasm frontend check passes; docs/internals/video.md updated with the SHRES output model and the two-axis sync-anchored presentation.

Noted for follow-up (pre-existing on main, unchanged here): the DblPAL Workbench title bar sits ~2 characters left of the sync-anchored glass window since the wide-FMODE scroll fold landed, and COPPERLINE_SHOT_RAW captures programmable scans after the presentation resample rather than the raw canvas.

Super-hi-res output built the framebuffer pixel's colour as
palette[(left & 3) | (right & 3) << 2] - an encoded pair of the two
35 ns samples covered by the 70 ns framebuffer pixel, truncating every
sample to two bitplanes. That encoding only ever made sense for ECS
Denise's 2-plane SHRES limit; real Denise/Lisa resolve every 35 ns
sample through the full palette pipeline. On a 4-plane AGA SHRES screen
(the Linux/m68k amifb 31 kHz console, FMODE=3) the pair encoding turned
solid colour 14 into palette[10], colour 7 into palette[15], and glyph
edges into palette[12]/palette[3] - a green Tux, whitened grey text,
and red/cyan edge fringes that looked like a bitplane phase error (the
captured plane words and per-plane delays were verified identical).

Resolve each half independently through denise_playfield_output (which
routes the AGA BPLAM/dual-playfield/EHB path or the OCS/ECS path by
itself) and blend the two resolved colours per channel into the one
hires-pitch framebuffer pixel. The blend is a framebuffer-pitch
compromise, not hardware; a true 35 ns output path stays a TODO shared
with SHRES sprite resolution. The composite sample now also keeps the
full index bits for collisions and sprite priority instead of masking
to two planes.
… pulse

The horizontal half of this shipped in #247: with VARHSYEN programmed,
the glass shows the line from the HSYNC trailing edge to the next
pulse. Vertically, a programmable frame still stretched its captured
visible rows over the full glass height, so the amifb 31 kHz console's
400 lines filled all 537 glass rows and the picture looked vertically
inflated next to a real multisync (whose vertical deflection locks to
the programmed sync: 400 of the frame's 445 post-sync lines).

Mirror the horizontal model: programmable_vsync_window (VARBEAMEN +
VARVSYEN, sane VSSTRT < VSSTOP <= VTOTAL) exposes the programmed
pulse, the bus latches a presentation v-window at the frame wrap
beside the h-window (serde-skipped, recomputed after a state load),
RenderInput carries it to the presentation pass, and
apply_presentation_v_window places the captured rows inside the
frame-minus-sync glass span with blanked border rows where the mode's
own porches put them. Modes without a programmed vertical sync (and
all standard scans) present exactly as before. The COPPERLINE_DBG_SHOT
debug-screenshot path applies the same window.
Copilot AI review requested due to automatic review settings July 22, 2026 10:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR improves Copperline’s video output correctness for programmable-scan super-hi-res modes by (1) resolving each 35 ns SHRES half-sample through the full palette pipeline (fixing AGA 4-bitplane colour/index truncation) and (2) anchoring programmable-scan vertical presentation to the programmed VSYNC pulse (matching multisync monitor behaviour and adding correct vertical borders where porches place them).

Changes:

  • Fix SHRES playfield output to resolve each 35 ns sample independently through denise_playfield_output and blend the two resulting colours into the 70 ns framebuffer pixel.
  • Add a programmable-scan vertical “glass window” (presentation_v_window) computed from the programmed VSYNC pulse and apply it during presentation to place/crop/pad captured rows.
  • Wire the new vertical presentation window through all presentation consumers (desktop, web, bench, headless screenshot path) and document the updated model.

Reviewed changes

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

Show a summary per file
File Description
src/video/window/present.rs Thread presentation_v_window into the windowed presentation path.
src/video/window.rs Pass vertical presentation window into post_process_rendered_field in both call sites.
src/video/present_common.rs Extend post-process signature and implement/apply apply_presentation_v_window with unit tests.
src/video/bitplane/tests.rs Update/add SHRES tests: per-half palette resolve and 4-plane AGA index regression coverage.
src/video/bitplane/output.rs Keep full SHRES composite sample bits; add 24-bit half-sample blend and per-sample resolve for SHRES output.
src/video/bitplane.rs Store/expose presentation_v_window in RenderInput and pass control into SHRES output.
src/cpu.rs Apply vertical presentation window for programmable-scan screenshots and pass correct presented row count to scaler.
src/chipset/agnus.rs Add programmable_vsync_window() to expose the programmed VSYNC pulse when VARVSYEN is active.
src/bus/frame_capture.rs Latch/carry per-frame presentation_v_window alongside the existing horizontal window.
src/bus.rs Add frame_presentation_v_window() and compute/track the vertical window at frame wrap (serde-skipped).
src/bin/bench.rs Provide vertical window to post_process_rendered_field in benchmark path.
docs/internals/video.md Document two-axis sync-anchored programmable-scan presentation and SHRES output blending model.
crates/copperline-web/src/lib.rs Provide vertical window to post_process_rendered_field in the web frontend.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

The framebuffer's hi-res (70 ns) pixel pitch halved SHRES horizontal
resolution: the 640-pixel amifb console occupied 320 canvas pixels,
every 35 ns sample pair blended into one, and the presentation then
stretched those onto the glass - readable but visibly soft next to a
reference emulator rendering SHRES natively.

Frames now carry a canvas supersample factor (canvas_scale_for): a
programmable scan that drives super-hi-res anywhere in the frame paints
a double-width canvas whose columns are 35 ns apart, and each half of
the resolved SHRES pair lands as its own pixel (with its own genlock
transparency) instead of blending. Every logical coordinate in the
replay - comparators, fetch origins, sprite positions, the collision
and playfield-mask buffers - stays in the classic hi-res-pitch domain;
only the framebuffer writes fan out, with non-SHRES pixels and sprites
doubled. Standard 15 kHz scans keep the single-width canvas
byte-identical (probe goldens unchanged), and their SHRES screens keep
the blended pair on the classic pitch. True 35 ns sprite placement
remains a TODO.

The canvas width threads through the presentation: the post-render
stretch and vertical glass window scale with it, the deinterlacer
carries per-push width (weave history drops on a pitch change like a
row-count change), the desktop window shows the 1432-pixel canvas 1:1
on a 2x HiDPI texture (nearest on non-HiDPI), screenshots save
1432x1074 keeping the 4:3 glass shape and SHOT_RAW saves the native
width, the video recorder folds back to its fixed classic width, the
frame-analyzer underlay samples at the canvas pitch, the browser
frontend passes the doubled width straight to its canvas, and the CCP
capture methods report the width.

The Linux/m68k amifb 31 kHz console (640 px SHRES, FMODE=3) is the
regression example: text is now pixel-sharp at the full programmed
resolution.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.

Comment thread src/video/deinterlace.rs Outdated
Comment thread src/screenshot.rs
…roup

Review follow-ups: the weave path's per-push motion-mask allocation
moves onto the Deinterlacer as a reusable buffer, downsample_x_into now
averages every source pixel group (not just the 2:1 fast path) matching
its comment, and the canvas-scale test drops redundant clones of the
Copy event type flagged by clippy with --all-targets.
@LinuxJedi
LinuxJedi merged commit 713dcc4 into main Jul 22, 2026
14 checks passed
@LinuxJedi
LinuxJedi deleted the fix/shres-sample-resolve-and-vsync-window branch July 22, 2026 11:21
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