video: correct super-hi-res colour output and programmable-scan vertical presentation#252
Merged
Merged
Conversation
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.
Contributor
There was a problem hiding this comment.
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_outputand 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.
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 topalette[10](green Tux), colour 7 topalette[15](whitened text), and glyph edges topalette[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 throughdenise_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_windowmirrors the HSYNC helper, the bus latches the v-window at the frame wrap beside the h-window (serde-skipped, recomputed after state load), andapply_presentation_v_windowplaces 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_RAWsaves 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
ocs_bpu7_hamhost-flaky perf test, failing identically on an unmodified tree.#AAAAAAtext matching the programmed console palette, no edge fringes, multisync-correct vertical proportions, and pixel-sharp 1432x1074 output.cargo clippyandcargo fmt --checkclean; wasm frontend check passes;docs/internals/video.mdupdated 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_RAWcaptures programmable scans after the presentation resample rather than the raw canvas.