You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
True 480i/576i ASCAL scaler output over direct_video
Context
The MiSTer board is used with direct_video=1 (realtime analog video via HDMI DAC). Cores with native interlaced output (Minimig) produce true 480i on a CRT, but cores that only output 480p (ao486) cannot. Goal: patch the ASCAL scaler so it can render any core's video (and the Linux/menu framebuffer) as true interlaced 480i/576i β alternating 262/263-line (312/313 PAL) fields with a half-line vsync offset β delivered over the direct_video HDMI pins, giving ~480 visible NTSC lines.
Two features, per user decisions:
vga_scaler=1 + direct_video=1 together routes the ASCAL output to the HDMI/DV pins for game cores (today only cores hardwiring VGA_SCALER=1, like ao486, get this). Works with unmodified core bitstreams β it's pure Linux-side routing via the existing CONF_VGA_FB mux (production-tested path, sys_top.v:1317-1320).
New ini key DIRECT_VIDEO_INTERLACE=1: the DV scaler mode becomes true 480i (576i when MENU_PAL=1). Requires rebuilt core bitstreams (ascal.vhd + sys_top.v change); Main detects support via a capability bit and silently falls back otherwise. Menu/Linux fb renders at ~480 lines too. v1 is free-running (vsync_adjust stays disabled under direct_video; no genlock/lltune changes).
Key insight: the existing 15 kHz TV mode (tvmodes[0], 640Γ240, vtotal 262 @12.587 MHz, video.cpp:142-148) is already the correct field modeline for 480i β 240p and 480i share identical line timing. ASCAL only needs to alternate field length, delay vsync by Β½ line on the long field, and offset vertical sampling per field.
All 4 repos share byte-identical sys/ascal.vhd + sys/sys_top.v (PSX: CRLF line endings, sys_top β β15 line offset). Edit Menu_MiSTer first, replicate to ao486/Minimig verbatim, PSX with CRLF preserved.
Enable bit: reuse mode(4) ("TBD" per header line ~89; port already 5 bits at :206; verified only bits 3 and 2:0 are read anywhere β o_hmode/o_vmode copy all 5 bits but only (2:0) are consumed, so it rides through harmlessly). Already latched into both clock domains: o_mode<=mode (:1883), i_mode<=mode (:1305). Alias o_ilace <= o_mode(4). Update header docs.
New signals:o_ilace, o_fld (master field reg, '1' = long field, init '0'), o_fld_dsp (display-aligned copy), o_vtotal_eff, o_hs2p/o_hs2/o_vs2start/o_vs2end (half-line VS precompute), o_ivsize_eff, o_ivdouble (weave/decimation mode flag).
A1. Field counter + per-field vtotal (OSWEEP, :2755-2830)
Register o_vtotal_eff <= o_vtotal + 1 when o_ilace='1' and o_fld='1' else o_vtotal (the :1895 input latch is untouched; only the wrap compare changes).
In the wrap branch (:2772): compare against o_vtotal_eff; on wrap toggle o_fld (force '0' when o_ilace='0').
o_fld toggles when o_vcpt_pre3 wraps (3 lines before display wrap). Shift a display-aligned copy alongside the existing pre2/pre/vcpt shifts (:2781-2783): o_fld_dsp follows 3 lines later. o_fld (pre3-aligned) drives vtotal_eff, sampling phase, decim addressing, preload β everything on the fetch side; o_fld_dsp drives only the vsync window. This removes any constraint between vsend and vtotal: with the DV porch rewrite the NTSC field modeline becomes vact=254, vsstart=256, vsend=260, vtotal=262 β vsync runs almost to the wrap, so a single field register toggling at pre3-wrap would flip mid-vsync-pulse and glitch VS. The two-register split fixes this by construction (o_fld_dsp toggles at display line 0, VS window is lines 256..261).
A2. Half-line vsync (:2791-2793 + precompute near :1906)
Precompute (static, from latched modeline): o_hs2p <= o_hsstart + o_htotal/2; if >= o_htotal then o_hs2 <= o_hs2p - o_htotal, o_vs2start/o_vs2end <= o_vsstart/o_vsend + 1, else no wrap.
Replace o_vsv(0):
IF o_ilace='1'AND o_fld_dsp='1'THEN-- long field: VS delayed by htotal/2
o_vsv(0)<=to_std_logic((o_vcpt=o_vs2start AND o_hcpt>=o_hs2) OR
(o_vcpt>o_vs2start AND o_vcpt<o_vs2end) OR
(o_vcpt=o_vs2end AND o_hcpt<o_hs2));
ELSE-- existing expression unchangedENDIF;
Field pairing invariant (verified arithmetically): long field = vtotal+1 lines with VS delayed +Β½ line; short field = vtotal lines, VS aligned. Both VS-to-VS spacings = vtotal+0.5 lines (NTSC: 262.5 β 525-line frame, 59.94 Hz fields). Same invariant as Minimig (agnus_beamcounter.v:376-383).
Hsync/DE/border need no change: hsync depends only on o_hcpt (uniform train, required for analog interlace); the long field's extra line has o_vcpt = vtotal β₯ vdisp β DE/picture low β one extra blank line. The 12-stage o_vsv pipeline and downstream consumers (buffer swap on VS falling edge :1934-1965, fb_base latch) handle the mid-line edge fine β one event per field.
A3. Vertical sampling β two sub-modes
Output fetch bandwidth allows β€ ~1.25 source-line fetches per output line (800-clock line vs 640-pixel copies), so:
Bias mode (o_ivsize β€ o_vsize): pure resample-phase shift. Edit o_vacc_ini (:2051): when o_ilace='1' AND o_ivdouble='0' AND o_fld='0' β o_vacc_ini <= o_vsize (= stock value + o_ivsize_eff, i.e. +Β½ output step = the correct field offset; always < 2Β·o_vsize so the divider/frame-init at :2086-2092 need no change; the un-biased field is bit-identical to stock). Works with all interpolation modes. The biased field is o_fld='0' because a field's raster position is set by the preceding field's vsync (delayed VS β next field drawn Β½ line lower β must sample Β½ step later).
Decimation/weave mode (o_ivdouble='1', active when o_ilace='1' AND o_ivsize > o_vsize; Linux guarantees o_ivsize = 2Β·o_vsize then): Bresenham runs 1:1 via o_ivsize_eff <= o_ivsize/2; addressing reads physical line 2n + f where f = NOT o_fld:
:2166 o_adrs_pre <= to_integer(o_vacpt(10 DOWNTO 0) & (NOT o_fld)) * to_integer(o_stride); (new ELSIF branch between the bob branch and the default).
Preload block :2170-2175: fload=2 β physical line f (+ o_stride when o_fld='0'); fload=1 β physical line 2+f (+ 2Β·o_stride, + 3Β·o_stride when o_fld='0'). o_alt masks unchanged.
o_rline (:2147): o_rline <= NOT o_fld when o_ivdouble='1' else o_vacpt(0) β in weave mode the physical line parity is constant per field, so interlaced-input (two field buffers) composes correctly: each output field reads entirely from the matching input field buffer. Gives true 480i passthrough for interlaced-input cores too (field phase unsynced in v1 β acceptable).
Replace o_ivsize with o_ivsize_eff at exactly: :2079, :2088 (accumulator step) and :2898, :2904 (VSCAL bottom clamp). Nowhere else.
Register o_ivdouble/o_ivsize_eff near the :1982-1988 fb override (after it, so o_fb_vsize is respected).
A4. Input-side: don't pre-downscale the frame (critical, was missed by initial design)
i_vdown <= (i_vsize > i_ovsize AND DOWNSCALE) with i_ovsize <= o_vsize (:1311-1333) and DOWNSCALE defaults true (generic :121, not overridden in sys_top) β so a 480-line core image would be blurred to ~254 lines before storage, destroying the interlace detail. Fix at :1333:
IF i_mode(4)='1'THEN i_ovsize<=2*o_vsize; ELSE i_ovsize<=o_vsize; ENDIF; -- <ASYNC>
Effects: 480-line input with o_vsize=240 β stored full (weave); >2Β·o_vsize inputs downscale to exactly 2Β·o_vsize β weave; β€ o_vsize inputs unaffected (bias). Also makes 480i inputs take the full-weave storage path (:1322 comparison) instead of half-frames. i_mode(4) is already available in the i domain.
A5. Guards
Bob-deint: o_bob_deint <= bob_deint AND NOT o_ilace; (:1915) β bob's half-line hack + output fields is nonsense; weave fallback.
VRR: add o_ilace='0' to o_vrr_sync (:2826) so a misconfigured VRR can never jump the line counter mid-field.
o_ilace='0' β every new mux collapses to the stock expression (bit-exact progressive regression).
A6. Sanity checklist (inspection, no sim)
Field lengths 262+263=525 (312+313=625); one VS per field, equal spacing = vtotal+0.5 both directions; bias field is the one following the delayed VS; decim: fld=1 shows physical lines 0,2,β¦, fld=0 shows 1,3,β¦; preload lines match first two fetched lines per field; no remaining bare o_ivsize in the vertical Bresenham; o_fld_dsp stable across the whole VS window.
Part B β sys_top.v (4 small edits Γ4 repos; PSX β β15 lines)
Nothing else: HDMI/VGA muxes, aspect window, csync module, OSD all verified compatible (csync serration window never overlaps the mid-line VS edge; OSD is DE-driven and already interlace-aware).
Capability β video.cpp next to supports_pr() (:201): static bool supports_ilace() = spi_uio_cmd(UIO_SET_VIDEO) & 4 (cache like the others; note existing helpers cache the masked value β replicate the pattern correctly with a shared cached word). Export int video_dv_scaler_forced() in video.h: cfg.direct_video && cfg.vga_scaler (routing does not require ilace support β works with unmodified cores).
Routing (feature 1) β user_io.cpp:3028 in user_io_send_buttons(): if (vga_fb || video_dv_scaler_forced()) map |= CONF_VGA_FB;. Plain OR with the existing set_vga_fb OSD toggling and ao486's hardwired VGA_SCALER=1; no interactions.
15 kHz tvmodes[0]/[2] + ilace β 480i/576i. Re-entry via cfg_has_video_sections() re-derives the flag; VRR unreachable under DV.
SET_VIDEO transmit β video.cpp word loop (:2248-2257): else if (i == 5) spi_w(((v_cur.param.ilace ? 1 : 0) << 15) | v_fix.item[i]);. DV porch rewrite (:2186-2197), PLL (12.587 MHz), and hdmi_config_set_mode() all unchanged (pr=0; ADV7513 below-min-clock status quo of DV today).
Weave window via VSET (critical) β with DV porches the field raster is 254 active lines, so a 480-line source is not 2:1. In video_scaling_adjust() (:2980, already called unconditionally on every res change, :3316), add a branch before the existing logic, active when direct_video + ilace mode is live:
uint32_tsrc_h=height << (vi->interlaced ? 1 : 0); // frame heightif (src_h>scrh) spi_uio_cmd16(UIO_SETHEIGHT, src_h >> 1); // exact 2:1 weave windowelse/* fall through to existing VSET logic (bias mode handles any iv<=vs) */
src_h>>1 handles odd heights (1-line input downscale, still weave); src_h > 2*scrh also resolves correctly (input downscales to 2Β·o_vsize β weave). This yields true weave for 480-line (window 240), 400-line DOS text (window 200, letterboxed), and interlaced inputs.
Menu fb at 480i β video_fb_config() (:3419): with il = v_cur.param.ilace: size guard uses item[5] << il, and fb_height = (v_cur.item[5] << il) / fb_scale_y; (do not touch the pr term). video_fb_enable()/fb_write_module_params() unchanged (window regs stay in field coordinates; source height doubles β ascal sees fb 2Β·o_vsize β weave). fb_cmd1/fb_cmd2 (:4063-4099): clamp against item[5]<<il, gate the item[5]<300 240p heuristic with && !il, halve window heights accordingly. Menu logo halving (:3929): add && !v_cur.param.ilace.
Cosmetic/deferred β filter-menu un-greying under DV-scaler mode, "640x240i" info string, VIC 6/7 infoframe: explicitly out of scope for v1.
Execution order
ascal.vhd in Menu_MiSTer (Part A) β copy to ao486/Minimig; PSX via unix2dos-style CRLF-preserving copy.
sys_top.v Γ4 (Part B; PSX offsets).
Main_MiSTer (Part C).
Verification
Static RTL check: if ghdl is available, ghdl -s --std=08 (or -a) on ascal.vhd for syntax; otherwise careful diff review against the sanity checklist in A6. Verify with grep that no bare o_ivsize remains in the vertical Bresenham and that mode concat is 5 bits.
Main_MiSTer build:make in Main_MiSTer (needs the ARM cross-toolchain; if absent, at minimum g++ -fsyntax-only the touched translation units with the repo's include paths).
Progressive regression by construction: with mode(4)=0 every new mux reduces to the stock expression β inspect each edit for this property.
Hardware (user): rebuild Menu + ao486 (+ optionally Minimig) in Quartus; deploy new MiSTer binary. Test matrix: (a) key off β identical behavior to today; (b) menu core NTSC + key β CRT shows 480i menu (line-alternating fields, sharp text); (c) ao486 640Γ480 + vga_scaler=1 + key β true 480i DOS/Windows; (d) 720Γ400 text mode β 400-line weave letterboxed; (e) PAL menu β 576i; (f) OSD open/close under (c) β no mode glitches; (g) old (un-rebuilt) core + key β clean fallback to current 240p DV behavior.
ao486_MiSTer - an example of a core where one might want 480i output but the core doesn't support it directly
Main_MiSTer - main mister FPGA library, may need for context on config parsing and invariants
Menu_MiSTer - the menu core, responsible for script running and forwarding the Linux framebuffer out the ascal scaler
PSX_MiSTer - a representative core that supports native interlaced output
Minimig-AGA_MiSTer - another representative core that supports native interlaced output
I have a MiSTer FPGA board attached via direct_video (basically realtime analog video sent out via an HDMI DAC in a special mode). Cores such as PSX and Minimig support native interlaced output. Cores which support non-interlaced 480p output cannot render to 480i. My goal is to patch the ASCAL scaler such that it can scale 480p to true 480i output, allowing ~480 lines of visible NTSC resolution (~240 lines per field).
This requires two main things:
Make ASCAL output 480i fields, alternating properly each frame with proper handling
Allow vga_scaler and direct_video to be set at the same time and work correctly, outputting interlaced video in the timing and format that direct_video with the representative cores use. (Careful not to confuse direct_video with the analog IO board, which I am not using.)
π€ Claude Progress
2026-07-27 02:49:49
I'll start by exploring these repos to understand the ASCAL scaler, the direct_video path, and how interlaced cores handle field timing β then I'll put together an implementation plan.
π¦ Tool: ToolSearch
Entered plan mode. You should now focus on exploring the codebase and designing an implementation approach.
In plan mode, you should:
1. Thoroughly explore the codebase to understand existing patterns
2. Identify similar features and architectural approaches
3. Consider multiple approaches and their trade-offs
4. Use AskUserQuestion if you need to clarify the approach
5. Design a concrete implementation strategy
6. When ready, use ExitPlanMode to present your plan for approval
Remember: DO NOT write or edit any files yet. This is a read-only exploration and planning phase.
π¦ Tool Call
2026-07-27 02:50:21
π¦ Tool: Agent
{
"description": "Explore ASCAL scaler internals",
"subagent_type": "Explore",
"run_in_background": false,
"prompt": "Be very thorough. Explore the MiSTer FPGA ASCAL video scaler to understand how its output timing generation works, in preparation for adding true interlaced (480i) output support.\n\nRepos available under /home/izzie/Desktop/mister-scaler/: Menu_MiSTer, Main_MiSTer, PSX_MiSTer, Minimig-AGA_MiSTer, ao486_MiSTer. The ascal scaler is typically at <core>/sys/ascal.vhd.\n\nQue... [2624 chars total, truncated]
π¦ Tool Call
2026-07-27 02:50:40
π¦ Tool: Agent
{
"description": "Explore direct_video signal path",
"subagent_type": "Explore",
"run_in_background": false,
"prompt": "Be very thorough. Explore the MiSTer FPGA framework's video output paths in the FPGA (Verilog/SystemVerilog side), focusing on direct_video and vga_scaler, in preparation for making the ASCAL scaler output true interlaced 480i over direct_video.\n\nRepos under /home/izzie/Desktop/mister-scaler/: Menu_MiSTer, Main_MiSTer, PSX_MiSTer, Minimig-AGA_MiSTer, ao486_MiSTer. Foc... [2962 chars total, truncated]
π¦ Tool Call
2026-07-27 02:50:55
π¦ Tool: Agent
{
"description": "Explore Main_MiSTer video config",
"subagent_type": "Explore",
"run_in_background": false,
"prompt": "Be very thorough. Explore the Main_MiSTer Linux-side code (repo: /home/izzie/Desktop/mister-scaler/Main_MiSTer) to understand how video modes, the ASCAL scaler, direct_video, and vga_scaler are configured from the ARM/HPS side. This is prep for patching the system so the ascal scaler can output true 480i, with vga_scaler and direct_video active simultaneously.\n\nQuesti... [2705 chars total, truncated]
π¦ Tool Result
2026-07-27 02:56:42
β Result (Agent):
# ASCAL Output Timing β Structured Report
All line numbers below refer to `/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd` unless stated. Line numbers are valid for **all four** copies (see Β§1).
---
## 1. Location & identity of ascal.vhd
| File | md5 | lines |
|---|---|---|
| `/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd` | `1b17376e8bf19b5c3c96c42d8d588ae6` | 3033 |
| `/home/izzie/Desktop/mister-scaler/ao486_MiSTer/sys/ascal.vhd` | `1b17376e8bf19b5c3c96c42d8d588ae6` | 3033 |
| `/home/izzie/Desktop/mister-scaler/Minimig-AGA_MiSTer/sys/ascal.vhd` | `1b17376e8bf19b5c3c96c42d8d588ae6` | 3033 |
| `/home/izzie/Desktop/mister-scaler/PSX_MiSTer/sys/ascal.vhd` | `556b0d0faf0768f0a5a50068875c3444` | 3033 |
PSX differs **only in line endings** (CRLF vs LF) β `diff <(tr -d '\r' β¦)` is empty. So: **one single upstream file, four identical copies.** A patch developed against one applies verbatim to all (mind CRLF for PSX).
`Main_MiSTer` is the HPS-side C++ userspace (no ascal.vhd); it drives the scaler over the SPI/UIO "user_io" channel.
---
## 2. Header comment block (register/feature documentation)
Lines 1β115. Key excerpts:
- **Features** (12β20): "Arbitrary output video format / Autodetect input image size or fixed window / **Progressive and interlaced input** / Interpolation β¦ / Avalon bus 128 or 64 bits / Optional triple buffering / Support for external low lag syntonization". Note: interlace is listed as an **input** feature only.
- **Downscaling** (23β30): interlaced video with vertical size < deinterlaced frame β process half-frames only.
- **5 clock domains** (33β38): `i_xxx` input video, `o_xxx` output video, `avl_xxx` Avalon, `poly_xxx` filter coeffs, `pal_xxx` palette.
- **O_FB_FORMAT** (41β45): `[2:0]` 011=8bpp/100=16bpp/101=24bpp/110=32bpp, `[3]` 565 vs 1555, `[4]` RGB/BGR, `[5]` TBD.
- **Image header** (48β70) written into RAM when `HEADER=TRUE`: byte `5:4` Attributes β `b0 Interlaced`, `b1 Field number`, `b2 H downscaled`, `b3 V downscaled`, `b4 Triple buffered`, `b7-5 Frame counter`. (These describe the *stored frame*, i.e. input side.)
- **MODE[2:0]** (75β83): 000 Nearest, 001 Bilinear, 010 Sharp Bilinear, 011 Bicubic, 100 Polyphase, 101/110/111 TBD.
- **MODE[3]** (85β87): 0 = single framebuffer (direct), 1 = triple buffering.
- **MODE[4] : TBD** (line 89) β **a free mode bit, the natural place for an "interlaced output" enable.**
- **Generics** (94β113): `MASK`, `RAMBASE`, `RAMSIZE`, `INTER` (True=autodetect interlaced *input*), `HEADER`, `PALETTE`, `PALETTE2`, `DOWNSCALE`, `BYTESWAP`, `FRAC`, `OHRES`, `IHRES`, `N_DW`, `N_AW`, `N_BURST`.
---
## 3. Port map / "register map" of the output timing
### Output timing inputs (entity, lines 204β230)
` ` `
204 run : IN std_logic -- 1=Enable output image
205 freeze : IN std_logic
206 mode : IN unsigned(4 DOWNTO 0)
207 bob_deint : IN std_logic
209-211 -- SYNC |_________________________/"""""""""\_______|
-- DE |""""""""""""""""""\________________________|
-- RGB | <#IMAGE#> ^HDISP |
-- ^HMIN ^HMAX ^HSSTART ^HSEND ^HTOTAL
212 htotal : IN natural RANGE 0 TO 4095
213 hsstart : IN natural RANGE 0 TO 4095
214 hsend : IN natural RANGE 0 TO 4095
215 hdisp : IN natural RANGE 0 TO 4095
216 hmin : IN natural RANGE 0 TO 4095
217 hmax : IN natural RANGE 0 TO 4095 -- 0 <= hmin < hmax < hdisp
218 vtotal : IN natural RANGE 0 TO 4095
219 vsstart : IN natural RANGE 0 TO 4095
220 vsend : IN natural RANGE 0 TO 4095
221 vdisp : IN natural RANGE 0 TO 4095
222 vmin : IN natural RANGE 0 TO 4095
223 vmax : IN natural RANGE 0 TO 4095 -- 0 <= vmin < vmax < vdisp
224 vrr : IN std_logic
225 vrrmax : IN natural RANGE 0 TO 4095
226 swblack : IN std_logic -- 3 black frames on resolution switch
229 format : IN unsigned(1 DOWNTO 0) -- 00=16bpp565 01=24bpp 10=32bpp
` ` `
`hmin/hmax/vmin/vmax` = the *scaled image window* inside the display area; outside it the `o_border` colour is emitted. `hdisp/vdisp` = active area; `htotal/vtotal` = full frame; `hsstart/hsend`, `vsstart/vsend` = sync.
These are sampled into `o_*` registers at the top of `Scalaire` (lines 1886β1902), then `o_hsize <= o_hmax-o_hmin+1`, `o_vsize <= o_vmax-o_vmin+1` (1904β1905). **`o_vsize` is the scaler's "output active height" and is what the vertical Bresenham divides by.**
### Where the values come from (Menu_MiSTer `sys/sys_top.v`)
Instantiation at `sys/sys_top.v:714-826`:
` ` `
714 ascal #(
715 .RAMBASE(32'h20000000),
717/719 .RAMSIZE(32'h00200000 | 32'h00800000) // MISTER_SMALL_VBUF
733 .FRAC(8),
735 .N_BURST(2048), // MENU_CORE only; default 256
737 .N_DW(128), .N_AW(28)
) ascal (
743 .reset_na(~reset_req), .run(1), .freeze(freeze), .bob_deint(bob_deint),
749-757 .i_clk(clk_ihdmi) .i_ce(ce_hpix) .i_r/g/b(hr_out,hg_out,hb_out)
.i_hs(hhs_fix) .i_vs(hvs_fix) .i_fl(f1) .i_de(hde_emu)
758-762 .iauto(1) .himin(0) .himax(0) .vimin(0) .vimax(0)
764-773 .o_clk(clk_hdmi) .o_ce(scaler_out)
.o_r/g/b(hdmi_data[23:16 / 15:8 / 7:0])
.o_hs(hdmi_hs) .o_vs(hdmi_vs) .o_de(hdmi_de) .o_vbl(hdmi_vbl) .o_brd(hdmi_brd)
774 .o_lltune(lltune)
775 .htotal (WIDTH + HFP + HBP + HS[11:0])
776 .hsstart(WIDTH + HFP)
777 .hsend (WIDTH + HFP + HS[11:0])
778 .hdisp (WIDTH)
779-780 .hmin(hmin) .hmax(hmax)
781 .vtotal (HEIGHT + VFP + VBP + VS[11:0])
782 .vsstart(HEIGHT + VFP)
783 .vsend (HEIGHT + VFP + VS[11:0])
784 .vdisp (HEIGHT)
785-786 .vmin(vmin) .vmax(vmax)
787-789 .vrr(vrr_mode) .vrrmax(HEIGHT+VBP+VS[11:0]+1) .swblack(hdmi_blackout)
791 .mode({~lowlat, LFB_EN ? LFB_FLT : |scaler_flt, 2'b00})
792-795 .poly_clk/.poly_a/.poly_dw/.poly_wr (coef_* from HPS)
813-819 .o_fb_ena(FB_EN) .o_fb_hsize(FB_WIDTH) .o_fb_vsize(FB_HEIGHT)
.o_fb_format(FB_FMT) .o_fb_base(FB_BASE) .o_fb_stride(FB_STRIDE)
821-826 Avalon to vbuf (clk_100m)
` ` `
Note `.mode(...)` is only **4 bits** wide (`{1,1,2'b00}`) driving a 5-bit port β **`mode(4)` is currently constant 0**, matching "MODE[4] : TBD".
`WIDTH/HFP/HS/HBP/HEIGHT/VFP/VS/VBP/HDMI_PR` are registers at `sys_top.v:1053-1061` (defaults 1920x1080@60), written by the HPS in the `cmd == 'h20` handler at `sys_top.v:421-449`:
` ` `
422 case(cnt[2:0])
423 0: {HDMI_PR, vrr_mode, WIDTH} <= {io_din[15:14], io_din[11:0]};
424 1: HFP <= io_din[11:0];
425 2: HS <= {io_din[15], io_din[11:0]}; // bit15 = hsync polarity
426 3: HBP <= io_din[11:0];
427 4: HEIGHT <= io_din[11:0];
428 5: VFP <= io_din[11:0];
429 6: VS <= {io_din[15], io_din[11:0]}; // bit15 = vsync polarity
430 7: VBP <= io_din[11:0];
(cnt>=8: PLL reconfig words; cnt==8 also latches {lowlat,cfg_done})
` ` `
`hmin/hmax/vmin/vmax` are **computed in the FPGA**, not sent by HPS β aspect-ratio state machine at `sys_top.v:902-995`; final centering at `sys_top.v:983-988`:
` ` `
hmini <= ((WIDTH - videow)>>1); hmaxi <= hmini + videow - 1;
vmini <= ((HEIGHT - videoh)>>1); vmaxi <= vmini + videoh - 1;
` ` `
(`hdmi_height/hdmi_width` clamped by `VSET/HSET` at `sys_top.v:909-910`.)
### HPS-side (Main_MiSTer)
- Command constants: `/home/izzie/Desktop/mister-scaler/Main_MiSTer/user_io.h:42-73`
- `UIO_SET_VIDEO 0x20` (timing + PLL), `UIO_GET_VRES 0x23`, `UIO_SETHEIGHT 0x27` (VSET), `UIO_SET_FLTCOEF 0x2A`, `UIO_SET_FLTNUM 0x2B`, `UIO_GET_VMODE 0x2C`, `UIO_SET_VPOS 0x2D`, `UIO_SET_FBUF 0x2F`, `UIO_SETWIDTH 0x37` (HSET/FREESCALE), `UIO_SETSYNC 0x38` (vs_line), `UIO_SET_AR_CUST 0x3A`, `UIO_SHADOWMASK 0x3E`.
- Struct that maps 1:1 onto the 0x20 payload: `Main_MiSTer/video.cpp:151-179` (`vmode_custom_param_t`: `mode, hact, hfp, hs, hbp, vact, vfp, vs, vbp, pll[12], hpol, vpol, vic, rb, pr`).
- Transmit loop: `Main_MiSTer/video.cpp:2245-2266`
` ` `
spi_uio_cmd_cont(UIO_SET_VIDEO);
for (i=1..8) { i==1 -> (pr<<15)|(use_vrr<<14)|hact ; i==3 -> (hpol<<15)|hs ; i==7 -> (vpol<<15)|vs ; else item[i] }
for (i=9..20) PLL words
` ` `
- Input-side interlace flag (read back from core, not scaler): `Main_MiSTer/video.cpp:2798` `video_info->interlaced = (res & 0x100)`; used to pick the `VFILTER_ILACE` vertical filter at `video.cpp:566` and for the `"β¦i"` suffix at `video.cpp:2838, 2894`. `video.h:29 bool interlaced;`. **There is no HPS notion of interlaced HDMI output**; `get_edid_vmode` even rejects interlaced EDID modes (`video.cpp:1944`).
---
## 4. Output timing generator
### `OSWEEP` process β `ascal.vhd:2755-2830` (this is *the* output sync generator)
` ` `vhdl
2761 IF o_ce='1' THEN
2763 IF o_hcpt+1<o_htotal THEN o_hcpt<=(o_hcpt+1) MOD 4096;
2765 ELSE
2766 o_hcpt<=0;
2768-2770 IF o_vcpt_sync /= 4095 THEN o_vcpt_sync<=o_vcpt_sync+1; END IF;
2772 IF o_vcpt_pre3+1>=o_vtotal THEN o_vcpt_pre3<=0;
2774 ELSIF o_vrr_sync2 THEN o_vcpt_pre3<=o_vsstart; o_sync<=false;
2777 ELSE o_vcpt_pre3<=(o_vcpt_pre3+1) MOD 4096; END IF;
2781 o_vcpt_pre2<=o_vcpt_pre3;
2782 o_vcpt_pre <=o_vcpt_pre2;
2783 o_vcpt <=o_vcpt_pre;
2785 END IF;
2786 o_end(0)<= (o_vcpt>=o_vdisp); -- VBLANK
2787 o_dev(0)<= (o_hcpt<o_hdisp AND o_vcpt<o_vdisp); -- DE
2788 o_pev(0)<= (o_hcpt>=o_hmin AND o_hcpt<=o_hmax AND
o_vcpt>=o_vmin AND o_vcpt<=o_vmax); -- picture (else border)
2790 o_hsv(0)<= (o_hcpt>=o_hsstart AND o_hcpt<o_hsend); -- HS
2791 o_vsv(0)<= (o_vcpt=o_vsstart AND o_hcpt>=o_hsstart) OR
(o_vcpt>o_vsstart AND o_vcpt<o_vsend) OR
(o_vcpt=o_vsend AND o_hcpt<o_hsstart); -- VS
2795 o_vss <= (o_vcpt_pre2>=o_vmin AND o_vcpt_pre2<=o_vmax); -- to lltune bit 4
2796-2800 o_hsv/o_vsv/o_dev/o_pev/o_end shifted through 12-stage pipelines (0..11)
2802-2808 IF o_run='0' THEN stage(2) of all forced to '0'
` ` `
VRR handling: `2811-2827` (`o_vcpt_sync`, `o_vrr_sync/2`, early restart to `o_vsstart`).
**Crucial for 480i:** `o_vcpt_pre3` is the *live* line counter; `o_vcpt_pre2`, `o_vcpt_pre`, `o_vcpt` are 1-line-delayed copies β i.e. the framebuffer fetch pipeline runs **3 lines ahead** of the pixel being displayed. Anything that must be prepared per-frame is keyed off `o_vcpt_pre3`/`o_vcpt_pre2`.
**VS granularity:** `o_vsv(0)` at 2791-2793 is line-granular but *phase-aligned to `o_hsstart`* β VS asserts at the hsync-start point of line `vsstart` and deasserts at the hsync-start point of line `vsend`. There is **no half-line capability**: to get a proper half-line-offset odd-field VS you'd need to make the `o_hcpt>=o_hsstart` comparison switchable to `o_hcpt >= (o_hsstart + o_htotal/2) mod o_htotal` for one field (and handle the wrap into the adjacent line). Many consumer displays tolerate a "fake 480i" without half-line offset, but a strictly-conformant 480i needs this edit.
### Final pixel/sync output β `VSCAL`, `ascal.vhd:2969-2979`
` ` `
2971 o_hs <= o_hsv(11);
2972 o_vs <= o_vsv(11);
2973 o_de <= o_dev(11);
2974 o_vbl<= o_end(11);
2975-2977 o_r/o_g/o_b <= x"00" (then overwritten by selected interpolator, 2980-3007)
2978 o_brd<= not o_pev(11);
3009-3013 IF o_pev(11)='0' THEN o_r/g/b <= o_border
` ` `
### Low-lag tuning bus β `ascal.vhd:3021-3030`
` ` `
o_lltune <= (0=>i_vss, 1=>i_pde, 2=>i_inter, 3=>i_flm, 4=>o_vss, 5=>i_pce, 6=>i_clk, 7=>o_clk, others=>'0');
` ` `
Decoded in `Menu_MiSTer/sys/pll_hdmi_adj.vhd:7-15` ("2: Input Interlaced mode, 3: Input Interlaced field, 4: Output Image frame"). This is the HDMI PLL frequency servo β **it locks output frame rate to input frame rate using `o_vss` (one pulse per output frame)**. If you halve the output frame period (fields), this servo will see 2Γ the frame rate and mis-lock. This is a second, non-obvious hook point.
---
## 5. Output-side signals
| Signal | Line | Dir | Notes |
|---|---|---|---|
| `o_r/o_g/o_b` | 149-151 | OUT | 8 bpc |
| `o_hs` | 152 | OUT | from `o_hsv(11)` |
| `o_vs` | 153 | OUT | from `o_vsv(11)` |
| `o_de` | 154 | OUT | from `o_dev(11)` |
| `o_vbl` | 155 | OUT | vertical blank, `o_vcpt>=o_vdisp` |
| `o_brd` | 156 | OUT | border enable, `not o_pev(11)` |
| `o_ce` | 157 | **IN** | clock enable (driven by `scaler_out` reg, `sys_top.v:335,417,765`) |
| `o_clk` | 158 | IN | output pixel clock |
| `o_border` | 161 | IN | 24-bit border colour |
| `o_lltune` | 190 | OUT | PLL servo bus |
**There is no field-indicator output.** No `o_fl`, no `o_field`, no interlace-related output port whatsoever. The only interlace signal in the output clock domain is `o_inter` (line 475), which is `i_inter` resynchronised (line 1917) and describes the **input**.
---
## 6. Existing notion of interlace (INPUT side only)
### Detection β `InAT` process, `ascal.vhd:1216-1225`
` ` `vhdl
IF NOT INTER THEN i_intercnt<=0;
ELSIF i_pfl /= i_fl_pre THEN i_intercnt<=3; -- i_fl toggled -> interlaced
ELSIF i_pvs='1' AND i_vs_pre='0' AND i_intercnt>0 THEN
i_intercnt<=i_intercnt-1;
END IF;
i_inter <= to_std_logic(i_intercnt>0);
` ` `
`i_flm` (field of the current frame) latched at first DE of the frame: `1231-1233` `IF i_pde='1' AND i_de_pre='0' THEN i_flm<=i_pfl; END IF;`.
Signals declared at `369-370`: `i_intercnt : natural RANGE 0 TO 3; i_inter, i_half, i_flm : std_logic`.
### Weave storage β `ascal.vhd:1235-1252`
At start-of-frame, the write pointer is offset by one line for the odd field:
` ` `vhdl
IF i_inter='1' AND i_flm='0' AND i_half='0' AND INTER THEN
i_line<='1'; i_wfl(o_ibuf1) <= '0';
i_adrsi <= N_BURST*i_hburst + N_BURST*HEADER; -- start one line down
ELSE
i_line<='0'; i_wfl(o_ibuf0) <= '1';
i_adrsi <= N_BURST*HEADER;
END IF;
` ` `
and end-of-line advances by **two** line strides (`1564-1575`):
` ` `vhdl
IF i_inter='1' AND i_half='0' THEN
-- Skip every other line for interlaced video
i_adrsi <= i_adrsi + N_BURST*(i_hburst+1); -- (partial-burst case)
...
i_adrsi <= i_adrsi + N_BURST*i_hburst;
` ` `
`i_line` is carried to `i_wline` β `avl_wline` and selects framebuffer 0 vs 1 for the write (`1746-1758`), i.e. **the two fields land in two separate triple-buffer slots and are woven on read**.
### Vertical size decision β `ascal.vhd:1318-1332`
` ` `vhdl
IF i_inter='0' THEN i_vsize<=i_vmaxmin; i_half<='0'; -- progressive
ELSIF i_ovsize<2*i_vmaxmin THEN i_vsize<=i_vmaxmin; i_half<='1'; -- interlaced but downscaling: half frames
ELSE i_vsize<=2*i_vmaxmin;i_half<='0'; -- interlaced: double height
` ` `
### Buffer bookkeeping β `Scalaire`, `ascal.vhd:1917-1972`
`i_endframe0/1` (`1257-1258`) toggle `o_ibuf0/o_ibuf1` independently per field; `1967-1971` forces `o_ibuf1<=o_ibuf0` / `o_obuf1<=o_obuf0` when `o_inter='0'`.
### Read-side field select β `ascal.vhd:2147` and `2149-2167`
` ` `vhdl
o_rline <= o_vacpt(0); -- Even/Odd line for interlaced video -> selects buf0/buf1 in Avaloir (1770-1775)
-- bob deinterlace half-line offset hack:
IF o_inter='1' AND o_bob_deint='1' THEN
IF o_iwfl(o_obuf0)='0' THEN
IF o_vacpt=0 OR o_rline='1' THEN o_adrs_pre <= o_vacpt * o_stride;
ELSE o_adrs_pre <= (o_vacpt-1)*o_stride; END IF;
ELSE o_adrs_pre <= (o_vacpt(11:1)&'0')*o_stride; END IF;
ELSE o_adrs_pre <= o_vacpt * o_stride;
` ` `
**Conclusion: zero interlace support on the output side.** The output timing generator is purely progressive.
---
## 7. Output vertical position / framebuffer line fetch
### The vertical Bresenham accumulator β `Scalaire`, state `sHSYNC`, `ascal.vhd:2077-2110`
` ` `vhdl
2051 o_vacc_ini <= (o_vsize - o_ivsize + 8192) MOD 8192;
2052 o_hacc_ini <= (o_hsize + o_ihsize + 8192) MOD 8192;
2054-2056 --Alternate phase
--o_vacc_ini<=o_ivsize;
--o_hacc_ini<=(2*o_hsize - o_ihsize + 8192) MOD 8192;
2078 dif_v := (o_vacc_next - 2*o_vsize + 16384) MOD 16384;
2079-2085 IF o_prim THEN o_vacc <= (dif_v>=8192) ? o_vacc_next : dif_v; END IF;
2086-2092 IF dif_v>=8192 THEN o_vacc_next <= (o_vacc_next + 2*o_ivsize) MOD 8192; vcarry:=false;
ELSE o_vacc_next <= dif_v; vcarry:=true; END IF;
2087 IF o_vcpt_pre2 = o_vmin THEN -- *** START OF FRAME INIT ***
2088 o_vacc <= o_vacc_ini;
2089 o_vacc_next <= o_vacc_ini + 2*o_ivsize;
2090 o_vacpt <= x"001";
2091 o_vacptl <= "01";
2092 vcarry_v := false;
2093 END IF;
2094-2099 IF vcarry_v THEN o_vacpt<=o_vacpt+1; END IF;
IF vcarry_v AND o_prim THEN o_vacptl<=o_vacptl+1; END IF;
2100 o_vcarrym <= o_vcarrym OR vcarry_v;
2103 o_divstart <= to_std_logic(NOT vcarry_v); -- kick off vfrac division
2104-2110 IF NOT vcarry_v OR o_fload>0 THEN
IF (o_vpe='1' AND o_vcarrym) OR o_fload>0 THEN o_state<=sREAD;
ELSE o_state<=sDISP; END IF; END IF;
` ` `
Semantics: the FSM loops in `sHSYNC` consuming source lines while `vcarry_v` is true. Each output line adds `2*o_ivsize`; each source-line advance subtracts `2*o_vsize`. So the vertical step is `o_ivsize / o_vsize` source lines per output line. `o_vacpt` = integer source line index (12-bit), `o_vacptl` = its low 2 bits used as the 4-line ring-buffer rotation index.
### Fractional phase β `ODivider`, `ascal.vhd:1828-1861`
`o_vfrac = o_vacc / (2*o_vsize)` (12-bit, restoring divider, 12 iterations, started by `o_divstart`). `o_vfrac` drives:
- `fracnn_v := o_vfrac(11)` β which half of the interval (line-pair selection) at `2845`
- bilinear/sharp-bilinear (`2929/2933`), bicubic (`2948-2956`), polyphase LUT address (`2378, 2382`).
### DRAM line address β `ascal.vhd:2149-2180`
` ` `vhdl
2166 o_adrs_pre <= to_integer(o_vacpt) * to_integer(o_stride); -- normal path
2169 IF o_adrsa='1' THEN
2170-2172 IF o_fload=2 THEN o_adrs <= o_hbcpt*N_BURST; o_alt<="1111"; -- preload src line 0
2173-2175 ELSIF o_fload=1 THEN o_adrs <= o_hbcpt*N_BURST + o_stride; o_alt<="0100"; -- preload src line 1
2176-2178 ELSE o_adrs <= o_adrs_pre + o_hbcpt*N_BURST; o_alt<=altx(o_vacptl+1);
` ` `
- `o_fload` is set to 2 at `2039-2041` when `o_vcpt_pre3 = o_vmin` (top of frame β force-preload 2 lines).
- `o_read` toggle at `2146` β `Avaloir` (`1693-1698, 1770-1775`) adds `avl_o_offset0/1` chosen by `o_rline` = `o_vacpt(0)`.
- `altx()` at `748-756` maps the 2-bit rotation to a 1-hot write mask for `o_line0..3`; the write happens in `HSCAL`/`OLBUF` (`2687-2688`, `2716-2753`).
- `VSCAL` reads the 4 line buffers and reorders them by `o_vacptl` (`2846-2884`), clamping at the last source line via `to_integer(o_vacpt) >= o_ivsize` (`2896-2916`).
### Output state machine β `ascal.vhd:2058-2143`
`sDISP β sHSYNC β sREAD β sWAITREAD β sDISP`. `o_prim` (2063, 2101) marks the first iteration of `sHSYNC` in a line; `o_vcarrym` (2064, 2100) records whether any source line was consumed; `o_vpe` (2046, `o_vcpt_pre<o_vmax AND o_vcpt_pre>=o_vmin`) gates fetching to the active window.
---
## 8. Search for existing interlaced-output work in these repos
`grep -rn "480i|interlac|vga_scaler"` over all `*/sys/`:
- **No** mention of 480i or interlaced *output* anywhere. All matches are input-side.
- `Menu_MiSTer/sys/video_cleaner.sv:29-30, 69` β `input interlace; ... if (interlace & f1) begin` (coreβscaler input path).
- `*/sys/osd.v:203` β `f1 <= ~f1; // skip every other frame for interlace compatibility.`
- `Menu_MiSTer/sys/pll_hdmi_adj.vhd:10-11` β lltune bit docs.
- `sys_top.v:307/309` `vga_scaler = cfg[2] | vga_force_scaler` β this is just "route the scaler output to the analog VGA DAC" (`vgas_en` at `sys_top.v:1516`, `vga_out vga_scaler_out` at `sys_top.v:1473-1489`), **not** an interlaced feature. Note `sys_top.v:1520-1524` apply `VS[12]`/`HS[12]` polarity inversion on that path.
- All four repos are clean upstream MiSTer-devel checkouts (Menu `3c3634c`, Main `ffcdf1e`, PSX `801618c`, Minimig-AGA `809b955`, ao486 `4ba37be`) β no local forks/patches.
---
## 9. Assessment β where field alternation would hook in
### A. Signalling the mode in
1. **`mode(4)`** (`ascal.vhd:206`, documented "TBD" at line 89) is free and already zero-padded by `sys_top.v:791`. Change to `.mode({interlaced_out, ~lowlat, flt, 2'b00})` and add an HPS bit. Alternatively add a dedicated `o_inter_ena` port.
2. HPS side: `Main_MiSTer/video.cpp:2245-2266` β spare bits exist in word 1 (`pr<<15 | use_vrr<<14 | hact`) and in `vmode_custom_param_t.unused[6]` (`video.cpp:178`). `sys_top.v:421-449` decodes.
### B. Frame/field counter (new state, output clock domain)
Add e.g. `o_field : std_logic` toggled once per output frame. The natural place is `OSWEEP` at `ascal.vhd:2772-2779`, in the `o_vcpt_pre3+1>=o_vtotal` wrap branch. It must be sampled by the same 3-line-delayed pipeline as `o_vcpt` if you also need a display-time field indicator (add `o_field_pre3/pre2/pre/β¦` alongside `2781-2783` and a 12-stage delay next to `o_vsv` at `2796-2800`).
### C. Vertical timing per field
- `o_vtotal` must alternate 262/263 (or 312/313). Cleanest: alternate `o_vtotal` inside the wrap branch at `2772`, driven by the field bit, or add a separate `vtotal_odd` input. Note `o_vtotal` is re-latched from the `vtotal` input every clock at `1895` β so per-field variation must be applied *downstream* of that latch (a local `o_vtotal_eff`) or the assignment at 1895 will overwrite it.
- **VS half-line offset** would require changing the three comparisons at `2791-2793` to use a per-field `hsstart_eff = o_hsstart` / `(o_hsstart + o_htotal/2) mod o_htotal`, plus care with the wrap across the line boundary. This is the one genuinely fiddly bit. Skipping it gives "line-doubled-look 480i" that many sinks accept but is not strictly CEA-861 conformant.
### D. Vertical source-line alternation (the actual "skip every other line")
This is the cleanest hook and there are exactly two candidate points, both inside `Scalaire`:
1. **`o_vacc_ini` at `ascal.vhd:2051`** β the initial accumulator phase. The author even left a hint at `2054-2056` (`--Alternate phase / --o_vacc_ini<=o_ivsize;`). Biasing this by half of one output-line step (`o_ivsize`) for the odd field shifts the whole vertical resample grid by half an output line β the correct, filter-aware way to do it if you keep `o_vsize` = per-field active height (240) and `o_ivsize` = full source height (480), so the step is 2 source lines/output line and half a step = 1 source line.
2. **The frame-init block at `ascal.vhd:2087-2093`** β `o_vacpt <= x"001"; o_vacptl <= "01";`. Setting these to `x"002"/"10"` (plus matching `o_vacc/o_vacc_next` bias) on the odd field is the direct "start one source line lower" approach. Simplest for nearest-neighbour; for bilinear/bicubic/polyphase you want (1) so `o_vfrac` stays consistent.
### E. Framebuffer preload must follow
`ascal.vhd:2170-2175` hardcodes the two preload lines as source line **0** (`o_hbcpt*N_BURST`) and source line **1** (`+ o_stride`). For an odd field starting at source line 1 these become lines 1 and 2 β i.e. `o_adrs <= o_hbcpt*N_BURST + (field ? o_stride : 0)` and `+ (field ? 2 : 1)*o_stride`. Missing this produces a corrupt first couple of lines on every odd field. The `o_alt` masks `"1111"`/`"0100"` at `2172/2175` also encode which ring-buffer slots are primed and would need to track the changed `o_vacptl` start value.
### F. Interaction with existing input interlace
`o_rline <= o_vacpt(0)` (`2147`) already uses `o_vacpt`'s LSB to pick framebuffer 0/1 when the *input* is interlaced. If you shift `o_vacpt` by 1 for the odd output field while the input is also interlaced, you silently swap which input field is read. Interlaced output + interlaced input therefore needs `o_rline` to be decoupled from the output field offset (e.g. keep a separate "logical source line" for buffer selection). Same for the bob-deint address hack at `2149-2167`. Safest initial scope: **only enable interlaced output when `o_inter='0'` (progressive input) or when `o_fb_ena='1'` (framebuffer mode)**.
### G. Downstream / non-ascal consequences
- **`o_lltune` bit 4 = `o_vss`** (`ascal.vhd:3025-3026`, `o_vss` at `2795`) feeds the HDMI PLL servo `pll_hdmi_adj.vhd`. With field output, `o_vss` pulses at field rate (2Γ) and the servo's `ofsize` vs `ifsize` comparison will fight. Either divide `o_vss` by 2 for the lltune bus or gate it to even fields. **This is the most likely source of "it compiles but the picture rolls".**
- `sys_top.v:1032` (`old_vs1 <= hdmi_vs`) and `sys_top.v:872` (`fb_vbl <= hdmi_vbl`) consume `o_vs`/`o_vbl` for HPS framebuffer/vsync signalling β will see 2Γ rate.
- OSD / shadowmask / vga_out chain (`sys_top.v:1154-1204`, `1473-1489`) is per-pixel and agnostic; but HDMI infoframe/VIC selection in `Main_MiSTer/video.cpp:1671 hdmi_config_set_mode()` would need the interlaced VIC and the `hdmi_config` regs set accordingly for a sink to accept true 480i.
- `Main_MiSTer/video.cpp:1918-2020 get_edid_vmode()` explicitly bails on interlaced EDID modes (`video.cpp:1944`) β 480i modes would have to come from `video_mode` config strings, not EDID.
### Summary of the minimal touch list in ascal.vhd
| Purpose | Location |
|---|---|
| Enable bit | port `mode` line 206 (use bit 4), latched at 1886 into `o_mode` |
| Field toggle + per-field `vtotal` | `OSWEEP` 2772-2779 (and don't let 1895 clobber it) |
| Half-line VS (optional, for conformance) | `OSWEEP` 2791-2793 |
| Vertical resample phase per field | `o_vacc_ini` at 2051 (hint at 2054-2056) |
| Source-line start index per field | frame init at 2087-2093 (`o_vacpt`/`o_vacptl`) |
| Top-of-field preload addresses | 2170-2175 (`o_fload=2/1` branches, plus `o_alt`) |
| Guard against input-interlace collision | `o_rline` at 2147, bob hack at 2149-2167 |
| PLL servo rate fix | `o_lltune` at 3021-3030 (`o_vss` from 2795) |
π¦ Tool Result
2026-07-27 02:59:17
β Result (Agent):
I have a complete picture. Here is the report.
---
# MiSTer Linux-side (Main_MiSTer) video/ascal/direct_video/vga_scaler analysis
All paths relative to `/home/izzie/Desktop/mister-scaler/`. FPGA references are from `Menu_MiSTer/sys/` (identical `sys` across the cores in this tree).
---
## 1. How Main programs the ascal output video mode
### 1.1 Data structures
`Main_MiSTer/video.cpp:145-186` β the mode is a 32-word `uint32_t item[32]` union'd with named fields:
` ` `
item[0] = mode index / 1 = custom
item[1..4] = hact, hfp, hs, hbp
item[5..8] = vact, vfp, vs, vbp
item[9..20] = PLL reconfig payload (12 words)
item[21..25] = hpol, vpol, vic, rb, pr
item[26..31] = unused[6] <-- 6 free uint32 slots
` ` `
Plus `double Fpix`. A `static_assert` at `video.cpp:186` ties the struct size to `item[32]`.
Preset tables: `vmodes[]` at `video.cpp:118-134` (15 HDMI modes, each `{8 timing params}, Fpix, vic_mode, pr`), and `tvmodes[]` at `video.cpp:137-143` (4 direct-video TV modes: NTSC 15K 640x240, NTSC 31K 640x480, PAL 15K 640x288, PAL 31K 640x576).
### 1.2 The programming sequence β `video_set_mode()` at `video.cpp:2177-2278`
This is the single funnel for everything the scaler needs. Order:
1. `setGamma()` / `setScaler()` (`video.cpp:2181-2182`). `setScaler()` (`video.cpp:574-596`) sends `UIO_SET_AR_CUST` (4 words of custom aspect ratio) then `set_vfilter(1)`.
2. `v_cur = *v`; build `v_fix` (`video.cpp:2184`). **If `cfg.direct_video`**, `v_fix` porches are rewritten (see Β§3).
3. `if (Fpix) setPLL(Fpix, &v_cur)` (`video.cpp:2202`) β fills `item[9..20]`.
4. VRR fixup (`video.cpp:2203-2243`) β recomputes Fpix from `vrr_max_fr` and pads `vfp`.
5. **The actual SPI transaction** (`video.cpp:2245-2276`):
` ` `c
spi_uio_cmd_cont(UIO_SET_VIDEO); // 0x20, EnableIO + 16-bit cmd word
for (int i = 1; i <= 8; i++) {
if (i == 1) spi_w((v_cur.param.pr << 15) | ((use_vrr?1:0) << 14) | v_fix.item[i]);
else if (i == 3) spi_w((!!v_cur.param.hpol << 15) | v_fix.item[i]);
else if (i == 7) spi_w((!!v_cur.param.vpol << 15) | v_fix.item[i]);
else spi_w(v_fix.item[i]);
}
for (int i = 9; i < 21; i++) {
if (i & 1) spi_w(v_cur.item[i] | (first_word && Fpix && cfg.vsync_adjust==2 && !is_menu() ? 0x8000 : 0) | 0x4000);
else { spi_w(v_cur.item[i]); spi_w(v_cur.item[i] >> 16); }
}
DisableIO();
hdmi_config_set_mode(&v_cur); // ADV7513 i2c: 0x17 sync invert, 0x3B pixel rep, 0x3C VIC
video_fb_config();
setShadowMask();
` ` `
`spi_uio_cmd_cont` is `EnableIO(); spi_w(cmd)` β `Main_MiSTer/spi.cpp:106-110`.
### 1.3 What the FPGA does with it β `Menu_MiSTer/sys/sys_top.v:419-457`
` ` `
cnt 0: {HDMI_PR, vrr_mode, WIDTH } <= {io_din[15:14], io_din[11:0]}
cnt 1: HFP <= io_din[11:0]
cnt 2: HS <= {io_din[15], io_din[11:0]} // HS[12] = hsync polarity
cnt 3: HBP <= io_din[11:0]
cnt 4: HEIGHT <= io_din[11:0]
cnt 5: VFP <= io_din[11:0]
cnt 6: VS <= {io_din[15], io_din[11:0]} // VS[12] = vsync polarity
cnt 7: VBP <= io_din[11:0]
cnt >= 8: PLL reconfig β (cnt%4==0) cfg_custom_p1 <= io_din[5:0] (Avalon address)
(cnt%4==1) cfg_custom_p2[15:0]
(cnt%4==2) cfg_custom_p2[31:16], toggle cfg_custom_t, rewind cnt to 4
cnt == 8 also: {lowlat, cfg_done} <= {io_din[15], 1'b1}
` ` `
**Only bits [11:0] are consumed for each timing word.** Free bits per word: word1[13:12], word2[15:12], word3[14:12], word4[15:12], word5[15:12], word6[15:12], word7[14:12], word8[15:12]. That is where an "interlaced output" flag belongs.
These regs feed ascal directly at `sys_top.v:742-800`:
` ` `
.htotal (WIDTH+HFP+HBP+HS[11:0]) .hsstart (WIDTH+HFP) .hsend (WIDTH+HFP+HS[11:0]) .hdisp (WIDTH)
.vtotal (HEIGHT+VFP+VBP+VS[11:0]) .vsstart (HEIGHT+VFP) .vsend (HEIGHT+VFP+VS[11:0]) .vdisp (HEIGHT)
.hmin/.hmax/.vmin/.vmax (window, computed in sys_top.v:892-994 from ARX/ARY/VSET/HSET/LFB_*)
.mode ({~lowlat, LFB_EN ? LFB_FLT : |scaler_flt, 2'b00})
.o_clk (clk_hdmi = hdmi_clk_out from pll_hdmi)
` ` `
### 1.4 PLL reconfig (Fpix) β `video.cpp:215-307`
`findPLLpar()` (`video.cpp:221-256`) and `setPLL()` (`video.cpp:259-307`). Reference is FPGA_CLK1_50 = 50 MHz. It solves `Fvco = Fout*C`, `M = Fvco/50`, `K = frac`, requiring `Fvco >= 400 MHz`, `K` outside (0.05, 0.95), `Fvco <= 1500 MHz`.
The 12-word payload written to `item[9..20]` is Altera fPLL reconfig register pairs `{addr, data32}`:
| item | value | meaning |
|---|---|---|
| 9 | 4 | addr 4 = M counter |
| 10 | `getPLLdiv(m)` | hi/lo duty split, bit17 = odd-divide |
| 11 | 3 | addr 3 = N counter |
| 12 | 0x10000 | N = bypass |
| 13 | 5 | addr 5 = C0 counter |
| 14 | `getPLLdiv(c)` | |
| 15 | 9 | addr 9 = charge pump |
| 16 | 2 | |
| 17 | 8 | addr 8 = bandwidth |
| 18 | 7 | |
| 19 | 7 | addr 7 = fractional-K |
| 20 | `k = ko * 2^32` | |
`getPLLdiv()` at `video.cpp:215-219`. The odd words carry `| 0x4000` (write strobe) and word 9 also carries `| 0x8000` when `cfg.vsync_adjust == 2 && !is_menu()` β that is the `lowlat` bit at `sys_top.v:447`.
FPGA side: `sys_top.v:1071-1122` latches `cfg_custom_p1/p2` into `adj_address/adj_data`, feeds `pll_hdmi_adj` (`sys_top.v:999-1015`), which arbitrates with its own low-latency tuning writes and drives `pll_cfg_hdmi` β `pll_hdmi` (`sys_top.v:1042-1049`).
---
## 2. Video mode definitions, custom parsing, interlace support
### 2.1 Presets and ini parsing
- `vmodes[]` `video.cpp:118-134`, `tvmodes[]` `video.cpp:137-143`.
- ini keys `VIDEO_MODE`, `VIDEO_MODE_PAL`, `VIDEO_MODE_NTSC` β `cfg.video_conf*` at `cfg.cpp:47-49`, storage `cfg.h:68-70`.
- `parse_custom_video_mode()` `video.cpp:2283-2371`. Accepted forms:
- 1 token β preset index into `vmodes[]`
- 3 tokens (`W,H,Hz`) β `video_calculate_cvt()`
- 9 or 11 tokens β full modeline `hact,hfp,hs,hbp,vact,vfp,vs,vbp,pclk_kHz[,hpol,vpol]`
- β₯21 tokens β raw `item[]` dump including PLL words
- Trailing string flags loop at `video.cpp:2313-2329`: `+vsync -vsync +hsync -hsync cvt cvtrb pr`. **Anything else is a hard `cfg_error`.** This is where an `i` / `interlace` token would go.
- `store_custom_video_mode()` `video.cpp:2373-2388` falls back to `vmodes[]` and defaults to 1080p when `support_FHD`.
- Per-resolution ini sections `[video=WxH@RR]`: matched at `cfg.cpp:255-267`, key string built by `video_get_core_mode_name()` `video.cpp:2861-2878`. **Note the section name is built from the core's `width x height` only β the interlace flag is not part of the key**, so a 480i and 480p core mode collide on the same section name.
### 2.2 Existing interlace support β input only
Every interlace reference in Main is about the **input** from the core:
- `VideoInfo::interlaced` β `video.h:29`, set at `video.cpp:2798` from `res & 0x100`.
- `VFILTER_ILACE` (=3) β `video.h:6`; selected in `set_vfilter()` at `video.cpp:566`.
- `VFILTER_INTERLACE_DEFAULT` ini key β `cfg.cpp:141`, `cfg.h:108`.
- Display string suffix `"i"` β `video.cpp:2838`, `video.cpp:2894`.
- YC mode cache key β `video.cpp:3109`.
- DV1 SPD InfoFrame bit 0 β `video.cpp:3169`.
- Menu OSD "Intl filter" β `menu.cpp:3186-3193`.
**Rejection:** `video.cpp:1942-1946` β if the EDID preferred timing has the interlace flag (`x[17] & 0x80`), Main prints *"EDID: preferred mode is interlaced. Fall back to default video mode."* and returns 0.
**There is no interlaced OUTPUT anywhere.** `ascal.vhd`'s output timing generator (`Menu_MiSTer/sys/ascal.vhd:2760-2800`) is strictly progressive: single `o_vcpt_pre3` counter modulo `o_vtotal`, `o_vsv` asserted on an absolute line window, no field toggle, no half-line offset. Interlace inside ascal exists only on the *input* side (`i_fl`, `i_inter`, `i_intercnt`, `i_flm` at `ascal.vhd:1205-1260`) and in weave/bob read addressing (`ascal.vhd:2147-2170`).
---
## 3. What `direct_video` changes on the Linux side
`cfg.direct_video` is `uint8_t`, ini key `DIRECT_VIDEO`, range 0-2 (`cfg.cpp:74`, `cfg.h:28`). Value 2 = "auto-detect DAC" (fork-specific).
### 3.1 Mode selection β `video_mode_load()` `video.cpp:2570-2628`
` ` `c
if (cfg.direct_video == 2) β should_auto_enable_direct_video() // video.cpp:2523-2566
reads EDID mfg id, matches dv_dac.txt / dv_dac_user.txt (load_dac_file video.cpp:2415-2490)
on match: cfg.direct_video=1 and overrides cfg.hdmi_limited, cfg.hdmi_audio_96k, cfg.csync
if (cfg.direct_video && cfg.vsync_adjust) { cfg.vsync_adjust = 0; } // video.cpp:2594-2598
if (cfg.direct_video) {
int mode = cfg.menu_pal ? 2 : 0;
if (cfg.forced_scandoubler) mode++;
v_def = tvmodes[mode]; setPLL(tvmodes[mode].Fpix, &v_def);
vmode_def = 1; vmode_pal = 0; vmode_ntsc = 0; // video.cpp:2600-2614
}
` ` `
**`video_mode=` from MiSTer.ini is entirely ignored when `direct_video` is on.** The scaler is hard-pinned to one of the four 15/31 kHz TV modes.
### 3.2 Blanking/border rewrite for the ADV7513 β `video.cpp:2186-2197`
` ` `c
#define FB_DV_LBRD 3 / FB_DV_RBRD 6 / FB_DV_UBRD 2 / FB_DV_BBRD 2 // video.cpp:54-57
v_fix.item[2] = FB_DV_RBRD; v_fix.item[4] = FB_DV_LBRD;
v_fix.item[1] += (v_cur.item[2]-v_fix.item[2]) + (v_cur.item[4]-v_fix.item[4]);
v_fix.item[6] = FB_DV_BBRD; v_fix.item[8] = FB_DV_UBRD;
v_fix.item[5] += (v_cur.item[6]-v_fix.item[6]) + (v_cur.item[8]-v_fix.item[8]);
` ` `
Htotal/Vtotal are preserved but the porches collapse to a 3/6/2/2 minimum and everything else becomes active β so ascal's `hdisp`/`vdisp` spans nearly the whole frame. This is what lets the Linux framebuffer/OSD fill the DAC's raster edge to edge. **This rewrite would corrupt any real 480i modeline you tried to hand to ascal while direct_video is on.**
Corresponding framebuffer offsets: `video_fb_enable()` `video.cpp:3357-3361` and `fb_cmd1` `video.cpp:4146-4152`:
` ` `c
xoff = v_cur.item[4] - FB_DV_LBRD; yoff = v_cur.item[8] - FB_DV_UBRD;
` ` `
### 3.3 Other direct_video effects
| Effect | Location |
|---|---|
| `set_vrr_mode()` skipped | `video.cpp:2198-2201` |
| ADV7513 `pr_flags = 0` (auto pixel-rep + auto VIC) when `direct_video && is_menu()` | `video.cpp:1678` |
| ypbpr CSC only when `vga_mode_int==1 && direct_video==1` | `video.cpp:1216`, `video.cpp:1416` |
| DV1 custom SPD InfoFrame (carries input interlace, rotation, de_h/de_v, pixrep) instead of standard SPD | `video.cpp:3158-3193` |
| `set_vga_fb(enable)` mirrored from fb enable | `video.cpp:3403` |
| Menu logo halved when `v_cur.item[5] < 300` (240p heuristic) | `video.cpp:3929` |
| `fb_cmd1` uses independent x/y integer scaling for 240p/288p | `video.cpp:4082-4093` |
| Scaler filter menu items all greyed out | `menu.cpp:3156,3167,3173,3177,3183,3193,3294,3317` |
| Info popup y-position | `menu.cpp:7987` |
| `CONF_DIRECT_VIDEO` / `CONF_DIRECT_VIDEO2` sent | `user_io.cpp:3026-3027` |
**csync:** `cfg.csync` defaults to 1 (`cfg.cpp:594`), ini `COMPOSITE_SYNC` (`cfg.cpp:40`), and can be force-set per-DAC from `dv_dac.txt` field 4 (`video.cpp:2448-2459, 2556-2559`). It is *not* a timing offset β it only sets `CONF_CSYNC` (cfg[3]) which selects `dv_cs_osd` over `dv_hs_osd` in `sys_top.v:1240-1242`.
---
## 4. `vga_scaler` handling and mutual exclusivity
### Linux side
- `cfg.vga_scaler`, `uint8_t`, ini `VGA_SCALER` 0-1 β `cfg.cpp:42`, `cfg.h:17`.
- Used in exactly **one** place: `user_io_send_buttons()` `user_io.cpp:3017` β `map |= CONF_VGA_SCALER`.
- The whole cfg word is sent via `spi_uio_cmd16(UIO_BUT_SW, map)` at `user_io.cpp:3062`.
**There is NO mutual-exclusion logic between `vga_scaler` and `direct_video` on the Linux side.** Nothing in `cfg.cpp` or `video.cpp` clears or warns about either when the other is set. Both bits go out simultaneously.
### FPGA side (`sys_top.v`)
` ` `verilog
wire vga_fb = cfg[12] | vga_force_scaler; // :295
wire direct_video = cfg[10]; // :296
wire vga_scaler = cfg[2] | vga_force_scaler; // :309
wire vgas_en = vga_fb | vga_scaler; // :1516
// HDMI pins:
clkselect({1'b1, ~vga_fb & direct_video}) // :1266 β clk_vid if DV & !fb, else hdmi_clk_out
hs <= (~vga_fb & direct_video) ? hdmi_dv_hs : (direct_video & csync_en) ? hdmi_cs_osd : hdmi_hs_osd; // :1317-1320
// VGA pins:
clkselect({1'b1, ~vga_fb & ~vga_scaler}) // :1346
VGA_* <= vgas_en ? vgas_* : vga_* // :1521-1529 (vgas_* = vga_out fed from ascal β :1473-1489)
` ` `
So the combination **already works in hardware**, in two distinct regimes:
| `direct_video` | `vga_scaler` | `vga_fb` | HDMI pins | VGA pins |
|---|---|---|---|---|
| 1 | 0 | 0 | raw core timing (`clk_vid`) | raw core (`clk_vid`) |
| 1 | **1** | 0 | raw core timing (`clk_vid`) | **ascal output (`hdmi_clk_out`)** |
| 1 | x | **1** | **ascal output (`hdmi_clk_out`)** | ascal output |
| 0 | 1 | 0 | ascal output | ascal output |
Row 2 is the target configuration. `vga_fb` is set by Main only via `video_fb_enable()` β `if (cfg.direct_video) set_vga_fb(enable)` (`video.cpp:3403`, `user_io.cpp:2992-2996`, `CONF_VGA_FB` = bit 12 at `user_io.h:153`) β i.e. the DAC gets the Linux framebuffer through ascal whenever the fb/OSD terminal is up.
**Linux-side blockers for row 2 (direct_video + vga_scaler simultaneously):**
1. `video_mode_load()` `video.cpp:2600-2614` β with `direct_video` on, ascal is pinned to `tvmodes[]` (640x240p @ 12.587 MHz or 640x480p @ 25.175). No way to give the scaler an independent (e.g. 480i) modeline. `vmode_pal`/`vmode_ntsc` are zeroed too.
2. `video_set_mode()` `video.cpp:2186-2197` β the FB_DV_* porch rewrite unconditionally mangles whatever timing you set.
3. `video_mode_load()` `video.cpp:2594-2598` β `vsync_adjust` is force-cleared, so the scaler can never phase-lock to the core's refresh in this mode.
4. `video_mode_adjust()` `video.cpp:3249-3288` β the whole adjust path is gated behind `cfg.vsync_adjust || cfg.vscale_mode >= 4`; with vsync_adjust forced to 0 it only runs under vscale_modeβ₯4.
5. `menu.cpp:3156-3193` β the OSD hard-disables every scaler filter selector when `cfg.direct_video`, even though ascal is genuinely active on the VGA path.
6. `video.cpp:3929` / `video.cpp:4082` β `cfg.direct_video && v_cur.item[5] < 300` is used as a "this is 240p TV" heuristic; a 480i mode with `vact=240` per field would trip it, one with `vact=480` would not.
---
## 5. Interlaced INPUT detection from cores
`get_video_info()` `video.cpp:2774-2826`:
` ` `c
spi_uio_cmd_cont(UIO_GET_VRES); // 0x23
uint16_t res = spi_w(0); // word 0
... width, height, htime, vtime, ptime, vtimeh, ctime (32-bit pairs), pixrep, de_h, de_v
video_info->interlaced = (res & 0x100) != 0;
video_info->rotated = (res & 0x200) != 0;
` ` `
FPGA source: `Menu_MiSTer/sys/hps_io.sv:502` β `vc_dout`, `hps_io.sv:875`:
` ` `verilog
1: dout <= {video_rotated, |vid_int, vid_nres}; // [7:0]=nres, [8]=|vid_int, [9]=rotated
` ` `
`vid_int` is a 2-deep shift register of the core's `VGA_F1` field signal sampled at vsync (`hps_io.sv:942`), i.e. "field toggled in either of the last two frames". `f1` comes from the core via `HPS_BUS[45]` (`sys_top.v:1760, 1771`).
Consumers: filter selection `video.cpp:566`; core description string `video.cpp:2838, 2894`; YC cache key `video.cpp:3109`; DV1 SPD byte `video.cpp:3169`. Note `res` changing triggers the whole re-detect path (`video_mode_adjust()` `video.cpp:3242-3316`), so **changing the interlace state alone will not force a mode change** unless `vid_nres` also increments β and `vid_nres` is only bumped on the `~f1` (even) field (`hps_io.sv:943-953`).
---
## 6. Menu core / framebuffer β ascal flow
1. **Memory:** `FB_ADDR = 0x20000000 + 32MB` (`video.cpp:36-37`), `FB_SIZE = 1920*1080`. `fb_init()` `video.cpp:2390-2413` mmaps 3 pages of `FB_SIZE*4` via `shmem_map` and sends `spi_uio_cmd16(UIO_SET_FBUF, 0)`.
2. **Geometry:** `video_fb_config()` `video.cpp:3419-3447`, called at the end of every `video_set_mode()`. Computes `fb_scale` from `cfg.fb_size`, then
` ` `c
fb_scale_y = v_cur.param.pr == 0 ? fb_scale : fb_scale * 2;
fb_width = v_cur.item[1] / fb_scale_x;
fb_height = v_cur.item[5] / fb_scale_y;
` ` `
**This is the only place the output mode's "pixel doubling" affects the fb geometry β an interlaced output flag would need the same hook here.**
3. **Kernel fb:** `fb_write_module_params()` `video.cpp:3322-3335` writes `"8888 1 <w> <h> <stride>"` to `/sys/module/MiSTer_fb/parameters/mode` (offloaded).
4. **Enable + register writes:** `video_fb_enable()` `video.cpp:3337-3406`:
` ` `c
spi_uio_cmd_cont(UIO_SET_FBUF); // 0x2F
spi_w(FB_EN | FB_FMT_RxB | FB_FMT_8888); // 0x8000 | 0b10000 | 0b00110
spi_w(addr & 0xFFFF); spi_w(addr >> 16);
spi_w(fb_width); spi_w(fb_height);
spi_w(xoff); spi_w(xoff + v_cur.item[1] - 1); // scaled L / R
spi_w(yoff); spi_w(yoff + v_cur.item[5] - 1); // scaled T / B
spi_w(fb_width * 4); // stride
` ` `
FPGA decode at `sys_top.v:459-473`: `{LFB_EN, LFB_FLT, LFB_FMT}`, `LFB_BASE`, `LFB_WIDTH/HEIGHT`, `LFB_HMIN/HMAX/VMIN/VMAX`, `LFB_STRIDE`. `LFB_HMIN..VMAX` become ascal's `hmin/hmax/vmin/vmax` (`sys_top.v:938-943`), overriding the ARX/ARY-derived window. Format bits at `video.cpp:39-51`.
5. **Menu core specifics:**
- `if (is_menu()) user_io_status_set("[8:5]", (fb_enabled && !fb_num) ? 0x160 : 0)` (`video.cpp:3404`) β tells Menu_MiSTer to blank its own pattern.
- Menu core itself: `Menu_MiSTer/menu.sv:36-40` ties `VGA_F1 = 0`, `VGA_SCALER = 0`, `VGA_DISABLE = 0` β **the menu core never asserts `vga_force_scaler`**, so on the Menu core `vga_fb` and `vga_scaler` come purely from cfg[12]/cfg[2].
- Menu core is compiled with `MENU_CORE` β ascal `.N_BURST(2048)` (`sys_top.v:735-737`).
- `video_fb_state()` `video.cpp:3408-3417` returns `fb_enabled && !fb_num` for the menu.
- `video_menu_bg()` `video.cpp:3747-3998` draws into `fb_base` page `n`.
- If `fb_terminal` is on and neither `vga_scaler` nor `vga_fb` routes ascal to VGA, `menu.cpp:850-871` puts up the "enable scaler on VGA: vga_scaler=1" warning on the analog OSD.
6. `video_cmd()` `video.cpp:4042-4179` handles runtime `fb_cmd0/fb_cmd1/fb_cmd2` from scripts, re-sending `UIO_SET_FBUF` with new format/geometry.
---
## 7. The cfg SPI word (direct_video / csync / ypbpr / vga_scaler)
Bit definitions `Main_MiSTer/user_io.h:141-154`:
| Bit | Name | Source |
|---|---|---|
| 0 | `BUTTON1` (menu btn) | |
| 1 | `BUTTON2` (user btn / kbd reset) | |
| 2 | `CONF_VGA_SCALER` | `cfg.vga_scaler` |
| 3 | `CONF_CSYNC` | `cfg.csync` |
| 4 | `CONF_FORCED_SCANDOUBLER` | `cfg.forced_scandoubler` |
| 5 | `CONF_YPBPR` | `cfg.vga_mode_int == 1` |
| 6 | `CONF_AUDIO_96K` | `cfg.hdmi_audio_96k` |
| 7 | `CONF_DVI` | `cfg.dvi_mode == 1` |
| 8 | `CONF_HDMI_LIMITED1` | `cfg.hdmi_limited & 1` |
| 9 | `CONF_VGA_SOG` | `cfg.vga_sog` |
| 10 | `CONF_DIRECT_VIDEO` | `cfg.direct_video` (1 or 2) |
| 11 | `CONF_HDMI_LIMITED2` | `cfg.hdmi_limited & 2` |
| 12 | `CONF_VGA_FB` | runtime `vga_fb` static |
| 13 | `CONF_DIRECT_VIDEO2` | `cfg.direct_video == 2` |
| 14, 15 | **free** | |
Assembled and sent in `user_io_send_buttons()` `user_io.cpp:3006-3064`; transmitted as `spi_uio_cmd16(UIO_BUT_SW, map)` (`user_io.cpp:3062`, `UIO_BUT_SW` = 0x01). Only sent when `map` changes or `force` is set. `video_set_mode()` callers pair it with `user_io_send_buttons(1)` (`video.cpp:3287`, `3296`, `3303`, `2712`, `4195`).
FPGA: `sys_top.v:417-421` (`cmd == 1` β `cfg <= io_din; cfg_set <= 1; scaler_out <= 1`), decoded at `sys_top.v:288-310`. Bits 14/15 are unused β a **`CONF_INTERLACED` flag could go at bit 14 or 15**, though putting it in the `UIO_SET_VIDEO` timing words is cleaner since it's per-mode, not per-config.
---
## 8. PLL constraints β is 27 MHz reachable?
Constraint chain in `findPLLpar()`/`setPLL()` (`video.cpp:221-307`):
- `while ((Fout*c) < 400) c++;` β VCO must be β₯ 400 MHz
- `m = Fvco/50`, `ko = frac`; requires `ko == 0` or `0.05 < ko < 0.95`; retries with larger `c` otherwise; gives up if `Fvco > 1500`.
**27.000 MHz:** `c = 15` β `Fvco = 405.0`, `m = 8`, `ko = 0.1` β accepted on the first iteration. `getPLLdiv(15)` = `0x20000 | (8<<8) | 7` = `0x20807`. Exact, no rounding error.
Other 480i-relevant clocks:
- 13.5 MHz: `c = 30` β Fvco 405, m 8, ko 0.1 β OK.
- 27.0 MHz Γ 1.001 (13.5/1.001 NTSC): `c=15`, Fvco 404.595, m=8, ko=0.0919 β OK.
**No PLL blocker.** Two guardrails to be aware of:
- `video_mode_adjust()` `video.cpp:3266-3271` rejects a computed `Fpix < 2` or `> 300` MHz when auto-adjusting.
- `get_edid_vmode()` `video.cpp:2000` rejects EDID modes above 210 MHz (irrelevant here).
Also relevant to a 27 MHz output: the ADV7513 in row-2 config is not the target sink (the DAC taps the raw pins). But if you *did* want the ADV7513 to emit real 480i, `hdmi_config_set_mode()` (`video.cpp:1671-1706`) is the only place VIC (`0x3C`) and pixel-repetition (`0x3B`) are written; 480i = VIC 6/7 with 2Γ pixel repetition, so `pr_flags` would need `0b01001000` and a new `vic` value in the mode. There is currently no 480i entry in `vmodes[]`.
---
## 9. vsync_adjust / low-latency / frame-sync interactions
- `cfg.vsync_adjust`, ini `VSYNC_ADJUST` 0-2 (`cfg.cpp:51`, `cfg.h:33`).
- `1` = retune the output PLL each time the core's `vtime` changes.
- `2` = same plus **low-latency mode**: `video_set_mode()` `video.cpp:2266` ORs `0x8000` into the first PLL word β `lowlat` at `sys_top.v:447` β `pll_hdmi_adj.llena` (`sys_top.v:1004`) closes a loop on ascal's `o_lltune` (`sys_top.v:774`), continuously nudging the output pixel clock to keep the output raster chasing the input raster. Also feeds ascal's `mode[3] = ~lowlat` (`sys_top.v:791`), which selects direct/triple-buffer behaviour.
- `lowlat` is suppressed for `is_menu()` (`video.cpp:2266`).
- **Force-disabled by direct_video** at `video.cpp:2594-2598`.
- Driver: `video_mode_adjust()` `video.cpp:3218-3320`, called from the main poll loop. On a `vid_changed` event it re-parses ini if there are `[video=...]` sections, calls `video_mode_select()` (`video.cpp:3036-3086`, picks `v_def`/`v_pal`/`v_ntsc` by `vtime > 1_800_000`), `video_resolution_adjust()` (`video.cpp:2911-2978`, vscale_modeβ₯4 β recomputes a CVT modeline for integer scaling β **this would clobber a hand-written interlaced modeline**), computes `Fpix` from `vtime` and the modeline totals, then `video_set_mode()`.
- `video_scaling_adjust()` `video.cpp:2980-3034` sends `UIO_SETHEIGHT` / `UIO_SETWIDTH` derived from `vm->item[5]` / `vm->item[1]`. For interlaced output the semantics of `item[5]` (field lines vs frame lines) determines whether integer-scale math is right or off by 2Γ.
- `frame_timer.cpp` (`get_vtime()` at :32, clamps to 50-75 Hz, else 59.94 fallback) is autofire-only; it uses `current_video_info.vtime`, i.e. the **core's** rate, not the output. With interlaced output at 60 fields/s vs 30 frames/s this is a place a definition mismatch could show up.
- `vs_wait()` `video.cpp:3634-3652` uses `FBIO_WAITFORVSYNC` on `/dev/fb0` β debug/timing only.
- VRR (`set_vrr_mode()` `video.cpp:2039-2175`, bit 14 of timing word 1) is skipped entirely under direct_video and is fundamentally incompatible with a fixed interlaced raster.
---
## 10. Where an "interlaced output mode" flag has to be plumbed
**ini β cfg**
- `Main_MiSTer/cfg.h:11-125` β new field (e.g. `uint8_t interlaced_output;`) *or* keep it per-mode-only.
- `Main_MiSTer/cfg.cpp:38+` β new `ini_vars[]` entry if a standalone key is wanted.
- Preferred: a new modeline flag token in `parse_custom_video_mode()`'s flag loop, `video.cpp:2313-2329` (add `"i"` / `"interlace"` next to `"pr"`; the loop `cfg_error`s on unknown tokens today).
**cfg β vmode struct**
- `video.cpp:145-172` β add a named field to `vmode_custom_param_t`. Slots `item[26..31]` (`unused[6]`) are free and the `static_assert` at `video.cpp:186` will keep the layout honest. Remember only `item[0..20]` are ever serialised, so a flag stored there must be explicitly picked up in `video_set_mode()`.
- `store_custom_video_mode()` `video.cpp:2373-2388` resets `rb`, `pr`, `vic` from `vmodes[]` β must not silently clear the new flag.
- `video_calculate_cvt_int()` `video.cpp:4218-4325` sets `param.pr`/`param.rb`; would need to either preserve or refuse the interlace flag.
**vmode β FPGA**
- `video_set_mode()` `video.cpp:2248-2257` β OR the flag into a free bit of one of the eight timing words. Cleanest: word 5 (`VHEIGHT`) bit 15, or word 1 bits [13:12]. FPGA reads only `[11:0]` today.
- `Menu_MiSTer/sys/sys_top.v:419-437` β extend the `cmd == 'h20` decode to latch it (e.g. `4: {ILACE, HEIGHT} <= {io_din[15], io_din[11:0]};`).
- `sys_top.v:742-800` β new ascal port.
- `Menu_MiSTer/sys/ascal.vhd` β new input in the entity (`ascal.vhd:200-230`) and real work in the output timing generator at `ascal.vhd:2760-2800`: field counter, per-field vtotal (262/263), half-line vsync offset, `o_vacpt` stepping by 2 with per-field phase, and `o_vsv` generation that respects the half-line. Also `o_vacc_ini`/`o_vsize` (`ascal.vhd:2050-2051`) since the vertical scale ratio changes meaning under interlace.
**Direct-video interaction (must be handled or an interlaced mode is unreachable)**
- `video.cpp:2600-2614` β allow `video_mode=` / a new `video_mode_dv=` to override the `tvmodes[]` pin when `direct_video` is on.
- `video.cpp:2186-2197` β make the FB_DV_* porch rewrite conditional (skip it, or halve the vertical borders, for an interlaced mode).
- `video.cpp:2594-2598` β the blanket `vsync_adjust = 0` under direct_video.
**Downstream consumers to update**
- `video_fb_config()` `video.cpp:3437` β `fb_scale_y` doubling is keyed only on `param.pr`.
- `video_fb_enable()` `video.cpp:3369-3373` and `fb_cmd1` `video.cpp:4155-4163` β `scaled bottom = yoff + v_cur.item[5] - 1`.
- `video_scaling_adjust()` `video.cpp:2988-3010` β `UIO_SETHEIGHT` from `item[5]`.
- `video_resolution_adjust()` `video.cpp:2911-2978` β will recompute a progressive CVT modeline under `vscale_mode >= 4`.
- `hdmi_config_set_mode()` `video.cpp:1671-1706` β VIC/pixel-rep if the ADV7513 must actually transmit 480i.
- `video_scaler_description()` `video.cpp:2842-2849` β add the `i` suffix (currently only the *core* description has one).
- `video.cpp:3929` and `video.cpp:4082` β the `cfg.direct_video && v_cur.item[5] < 300` "240p" heuristics.
- `menu.cpp:3156-3193, 3294, 3317` β un-grey the filter menu when ascal is genuinely live on the VGA path.
- `cfg.cpp:255-267` / `video.cpp:2861-2878` β `[video=WxH@RR]` section keys don't encode interlace, so 480i and 480p core modes alias.
**No blocker found for:** the PLL path (27 MHz is exact), the SPI transport (plenty of free bits in both the `UIO_SET_VIDEO` timing words and the `UIO_BUT_SW` cfg word), and `vga_scaler` + `direct_video` coexistence (the FPGA muxes already support it; there is no Linux-side exclusion). The real work is (a) removing the direct_video mode-pinning and porch rewrite in `video.cpp`, and (b) adding genuine interlaced output generation to `ascal.vhd`, which does not exist in any form today.
π¦ Tool Result
2026-07-27 03:00:24
β Result (Agent):
I have everything I need. Here is the full report.
---
# MiSTer framework video paths β direct_video, vga_scaler, ASCAL, and interlace
Framework versions: `Menu_MiSTer/sys/sys_top.v`, `Minimig-AGA_MiSTer/sys/sys_top.v`, `ao486_MiSTer/sys/sys_top.v` are **byte-identical** (md5 `f22545a2β¦`, dated 2026-06-03). `PSX_MiSTer/sys/sys_top.v` is an **older sys** (2025-12-04, md5 `ef6b86c3β¦`). `vga_out.sv`, `video_mixer.sv` are identical in all four; `ascal.vhd` and `hps_io.sv` differ between PSX and the rest.
Unless noted, line numbers below are **Menu_MiSTer** (= Minimig = ao486). PSX equivalents given where they differ.
---
## 1. `direct_video`: origin and effect on the output mux
### Where the signal comes from
It is **not a pin and not from the core** β it is bit 10 of the 16-bit `cfg` register that the HPS pushes over the UIO SPI bus, snooped directly by `sys_top` (not by `hps_io`):
- `/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/sys_top.v:414-418` β `if(cmd == 1) begin cfg <= io_din; cfg_set <= 1; scaler_out <= 1; end`. Command `1` = `UIO_BUT_SW` (`/home/izzie/Desktop/mister-scaler/Main_MiSTer/user_io.h:13`).
- `/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/sys_top.v:296` β `wire direct_video = cfg[10];`
- HPS side that builds the word: `/home/izzie/Desktop/mister-scaler/Main_MiSTer/user_io.cpp:3017-3028` (`user_io_send_buttons`), sent via `spi_uio_cmd16(UIO_BUT_SW, map)` at `user_io.cpp:3060`.
- `MISTER_DEBUG_NOHDMI` builds hardwire `direct_video = 1`, `vga_fb = 0` (`sys_top.v:291-293`).
PSX: identical lines (`PSX_MiSTer/sys/sys_top.v:292-309`).
### The two things it switches
1. **HDMI pixel clock source** (`sys_top.v:1262-1272`):
` ` `
cyclonev_clkselect hdmi_clk_sw(.clkselect({1'b1, ~vga_fb & direct_video}),
.inclk({clk_vid, hdmi_clk_out, 2'b00}), .outclk(hdmi_tx_clk));
` ` `
`clkselect=2'b11 β clk_vid` (core video clock), `2'b10 β hdmi_clk_out` (pll_hdmi / ASCAL clock). `hdmi_tx_clk` then feeds `altddio_out hdmiclk_ddr` β `HDMI_TX_CLK` (`sys_top.v:1274-1297`).
2. **HDMI data/sync mux** (`sys_top.v:1304-1337`, PSX `1289-1319`):
` ` `verilog
hs <= (~vga_fb & direct_video) ? hdmi_dv_hs : (direct_video & csync_en) ? hdmi_cs_osd : hdmi_hs_osd;
vs <= (~vga_fb & direct_video) ? hdmi_dv_vs : hdmi_vs_osd;
de <= (~vga_fb & direct_video) ? hdmi_dv_de : hdmi_de_osd;
d <= (~vga_fb & direct_video) ? hdmi_dv_data : hdmi_data_osd;
assign {HDMI_TX_HS,HDMI_TX_VS,HDMI_TX_DE,HDMI_TX_D} = {hdmi_out_hs,hdmi_out_vs,hdmi_out_de,hdmi_out_d};
` ` `
Note the mux condition is `~vga_fb & direct_video`, **not** `direct_video`, and:
- `sys_top.v:295` `wire vga_fb = cfg[12] | vga_force_scaler;`
- `sys_top.v:1772` `.VGA_SCALER(vga_force_scaler)` β i.e. **a core asserting `VGA_SCALER` cancels the dv path and puts the ASCAL output on the HDMI pins even when `direct_video=1`**, with csync support (`direct_video & csync_en β hdmi_cs_osd`).
- `ao486_MiSTer/ao486.sv:439` `assign VGA_SCALER = 1;` β **ao486 never uses the dv path**; its "direct video" is already ASCAL-over-DV-pins.
- `Minimig-AGA_MiSTer/Minimig.sv:114` `assign VGA_SCALER = FB_EN;` β Minimig switches to that path when the RTG/Picasso framebuffer is on.
- `PSX_MiSTer/PSX.sv:193` `assign VGA_SCALER = 0;`, `Menu_MiSTer/menu.sv:39` `= 0` β those use the real dv path.
- HPS also sets `cfg[12]` (`CONF_VGA_FB`, `user_io.h:153`) when the Linux framebuffer is shown under direct video: `video.cpp:3403 if (cfg.direct_video) set_vga_fb(enable);`, `user_io.cpp:2992`.
3. Third, smaller effect: the core is told there is no scaler geometry β `sys_top.v:1778-1779` `.HDMI_WIDTH(direct_video ? 12'd0 : hdmi_width), .HDMI_HEIGHT(direct_video ? 12'd0 : hdmi_height)`.
ASCAL itself is *not* disabled in dv mode (it keeps running on `clk_hdmi`, `sys_top.v:714-830`) β its output is simply not selected.
---
## 2. The `dv_*` path: format the ADV7513/DAC receives
Generator: `sys_top.v:1207-1254` (PSX `1192-1239`), all in the **`clk_vid`** domain.
Source select (`sys_top.v:1256-1260`):
` ` `
{dv_data_osd, dv_hs_osd, dv_vs_osd, dv_cs_osd} = ~yc_en ? {vga_data_osd, vga_hs_osd, vga_vs_osd, vga_cs_osd}
: {yc_o, yc_hs, yc_vs, yc_cs};
` ` `
so dv taps the **analog-side chain after scanlines + OSD** (`scanlines` `sys_top.v:1383-1399` β `osd vga_osd` `1401-1422` β `csync csync_vga` `1424-1425`), optionally replaced by the YC/CVBS modulator (`yc_out` `1435-1452`). It never passes through `vga_out.sv`, so **no YPbPr matrix is applied in the FPGA** for dv.
Key properties:
- **Pixel repetition is implicit.** `dv_d1 <= dv_data_osd; β¦ dv_data <= dv_d2;` run every `clk_vid` edge, *not* gated by `ce_pix` (`sys_top.v:1241-1253`). `HDMI_TX_CLK = clk_vid`, so each core pixel is transmitted `clk_vid/ce_pix` times. The HPS measures this and reports it (`hps_io.sv:891 vid_pixrep`, printed at `Main_MiSTer/video.cpp:2895`). The ADV7513 is additionally told about repetition through reg `0x3B`: `video.cpp:1678-1680` β `if (cfg.direct_video && is_menu()) pr_flags = 0` (**automatic** pixel repetition + VIC detection) else manual (`0b01000000`, or `0b01001000` for 2Γ clock modes).
- **Sync content**: `sys_top.v:1242` `dv_hs1 <= csync_en ? dv_cs_osd : dv_hs_osd;` and `1243 dv_vs1 <= dv_vs_osd;`. So **HDMI_TX_HS carries CSync when `cfg[3]` (csync) is set**, HDMI_TX_VS always carries VSync. Both are **active-high going into the ADV7513**; polarity for the analog world is fixed by ADV7513 reg `0x17` bits 5/6 (`video.cpp:1682-1691`, `hdmi_config_set_mode`), driven from `vm->param.hpol/vpol` β and the `tvmodes[]` used by direct video all have hpol=vpol=0 (`video.cpp:142-148`) β both inverted β negative-going sync out of the DAC. There is **no packing of sync into data bits** anywhere.
- **DE is synthetic and covers almost the entire raster** (`sys_top.v:1215-1239`):
` ` `verilog
if (ce_pix) begin
hss <= (hss << 1) | dv_hs_osd;
old_hs <= dv_hs_osd;
if (~old_hs && dv_hs_osd) begin // per-line work, at HS rising edge
old_vs <= dv_vs_osd;
if (~&vcnt) vcnt <= vcnt + 1'd1;
if (~old_vs & dv_vs_osd) begin
if (vcnt != vcnt_ll || vcnt < vcnt_l) vsz <= vcnt; // frame height learner
vcnt_l <= vcnt; vcnt_ll <= vcnt_l;
end
if (old_vs & ~dv_vs_osd) vcnt <= 0;
if (vcnt == 1) vde <= 1;
if (vcnt == vsz - 3) vde <= 0;
end
dv_de1 <= !{hss,dv_hs_osd} && vde; // DE = "no HS in last 5 ce_pix" AND in vde window
end
` ` `
DE is low only during HSync +4 pixels, and vertically from line 1 to `vsz-4`. Combined with `sys_top.v:1388` (`.din(de_emu ? {r_out,g_out,b_out} : 24'd0)`) the whole raster including blanking/border is shipped as **black active pixels**, preserving the core's exact analog timing at the DAC. This is the defining property of direct video.
- 3-stage pipeline delay (`dv_d1/d2/data`) matches the DE generator latency; a further re-register happens in the `hdmi_tx_clk` domain (`sys_top.v:1304-1332`) β note `dv_*` is generated on `clk_vid` and `hdmi_tx_clk` *is* `clk_vid` in dv mode, so this is a plain 2-stage delay, not a CDC.
### hdmi_config / ADV7513 for direct video
There is **no `hdmi_config` module in the FPGA** β the ADV7513 is configured by Linux over IΒ²C, passed through by `cyclonev_hps_interface_peripheral_i2c hdmi_i2c` (`sys_top.v:1129-1139`).
- Init table: `/home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp:1414-1594` (`hdmi_config_init`), addr `0x39`, `i2c_open` at `video.cpp:1422`.
- Direct-video-specific: `int ypbpr = (cfg.vga_mode_int == 1) && (cfg.direct_video == 1)` at `video.cpp:1416` and `1216` β **the RGBβYPbPr conversion for direct video is done by the ADV7513 CSC** (fixed coefficient set at `video.cpp:1361-1400`, regs `0x18-0x2F`), not by `vga_out.sv`. Limited-range CSC is bypassed when ypbpr.
- `hdmi_config_set_mode` (`video.cpp:1671-1707`) writes `0x17` (sync invert), `0x3B` (pixel repetition), `0x3C` (VIC β `tvmodes` have vic 0).
- HPS video modes for direct video: `video.cpp:2600-2618` picks from `tvmodes[]` (`video.cpp:142-148`): 640Γ240@15k, 640Γ480@31k, 640Γ288@15k PAL, 640Γ576@31k PAL. **No interlaced mode exists today.**
- Border trick: `video_set_mode` (`video.cpp:2186-2196`) rewrites porches to `FB_DV_LBRD/RBRD/UBRD/BBRD = 3/6/2/2` (`video.cpp:54-57`) and adds the removed porch to hact/vact β this is what makes ASCAL/framebuffer output cover the whole raster when the scaler drives the DV pins.
---
## 3. How interlaced cores present interlace, and what the framework does with it
### Core β framework
`emu.VGA_F1` β `f1` (`sys_top.v:1771`, decl `1686`). Complete list of uses of `f1` in `sys_top.v` β there are only three:
- `sys_top.v:756` `.i_fl(f1)` β ASCAL field input.
- `sys_top.v:1760` `HPS_BUS[45]` β `hps_io.sv:230 .f1(HPS_BUS[45])` β `video_calc` (`hps_io.sv:875,942-943`): `dout[8] = |vid_int`, and the resolution counters are only latched on even fields (`if(~f1)`). HPS reads it at `video.cpp:2798` `video_info->interlaced = (res & 0x100) != 0`.
- PSX (older sys): `PSX_MiSTer/sys/sys_top.v:741, 1748 ({fb_en, sl, f1, β¦}), 1759`.
**`f1` is not used anywhere in the dv path, the analog VGA path, the csync module, or `vga_out.sv`.** In `direct_video` mode the field flag is effectively discarded by the framework; whatever interlace exists is whatever the core put into HS/VS.
### PSX β field flag only, progressive-looking sync
- `PSX_MiSTer/PSX.sv:1547` `assign VGA_F1 = status[14] ? 1'b0 : video_aspect.interlace;`
- source: `PSX_MiSTer/rtl/gpu_videoout_sync.vhd:276` `videoout_out.interlace <= GPUSTAT_VerRes and interlacedDisplayField;` (field toggles at `:160-165, 191-205`).
- Sync generation `gpu_videoout_sync.vhd:100-104` (vsync = lines 5..8 of the field in 480i, 10..13 in 240p) and `:373-374` (hsync from `nextHCount`, reset identically every line, `:355-356`). **Both fields have identical, line-aligned sync β no half-line offset.** Only the VRAM line fetched alternates (`:181-188, 211-215`).
- Path to `VGA_HS/VS`: raw GPU sync β `video_aspect` regs (`PSX.sv:1631-1634`) β `gamma_corr` (`PSX.sv:1683-1700`) β `VGA_VS/VGA_HS` (`PSX.sv:1544-1545`). **No `video_mixer`, no scandoubler.**
- β Over direct_video today, PSX 480i comes out as **two 262/263-line fields drawn at the same vertical position** (i.e. 240p-like), with the odd/even content alternating β no true interlace on a CRT.
### Minimig β real interlaced sync
- `Minimig-AGA_MiSTer/Minimig.sv:741` `assign VGA_F1 = field1;`, from `rtl/agnus_beamcounter.v:348` `assign field1 = ~long_frame;` (long_frame toggles per frame when `lace`, `:316-327`; extra line `:332-346`).
- **True half-line offset vsync** at `rtl/agnus_beamcounter.v:376-383`:
` ` `verilog
if ((vpos==vsstrt+1 && hpos==hsstrt && long_frame) || (vpos==vsstrt && hpos==hcenter && !long_frame))
_vsync <= 1'b0;
else if ((vpos==vsstop && hpos==hcenter && long_frame) || (vpos==vsstop && hpos==hsstrt && !long_frame))
_vsync <= 1'b1;
` ` `
(`hcenter` = `HCENTER_VAL` `:82`, "position of vsync pulse during the long field"). The Amiga's own serrated csync (`:387-398`) is **not** exported β Minimig only wires `_hsync/_vsync` (`Minimig.sv:618-619`), and the framework regenerates csync itself.
- `Minimig.sv:718-736`: `video_mixer` with `scandoubler = (fx || forced_scandoubler) & ~lace` (`:717`) β **scandoubler is force-disabled when interlaced**, so the raw interlaced sync passes through.
- Caveat: `video_mixer.sv:206-216` only updates `VGA_HS/VGA_VS` when `CE_PIXEL` is high, so the half-line edge is quantized to the pixel-enable grid (Minimig `ce_out` = 7/14/28 MHz, `Minimig.sv:690-712`) β harmless.
- β **Minimig over direct_video already produces genuine 480i/576i**; PSX and any "F1-only" core does not. That asymmetry is the crux of the project.
### What the framework does downstream with an interlaced signal in dv mode
- `sync_fix` (`sys_top.v:1707-1708`, module `1882-1907`) auto-normalizes polarity (counts high vs low time) β fine for interlace.
- `csync` module (`sys_top.v:1914-1949`) generates csync by shifting HS left by one line during VS; it measures `line_len` from the previous line and has **no equalization/serration pulses and no field awareness** β a candidate area to revisit for proper 480i csync.
- dv DE line counter (`sys_top.v:1220-1239`) detects VS edges **only at HS rising edges**, so a half-line-shifted VS is rounded to the nearest line; with alternating 263/262-line fields the `vsz` learner (`if (vcnt != vcnt_ll || vcnt < vcnt_l)`) settles on the **shorter** field length (it compares against two frames ago, so equal-parity fields never trigger the first condition). DE windows are therefore identical for both fields, half a line out of phase on one of them.
---
## 4. `vga_out.sv`
`/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/vga_out.sv` (73 lines, identical in all four repos). It does exactly two things:
- RGBβYPbPr matrix when `ypbpr_en` (`:34-61`, output `:71 assign dout = ypbpr_en ? {pr, y, pb} : rgb;` β note ordering Pr/Y/Pb on the 24-bit bus, matching the DAC's R/G/B lanes).
- 3-cycle delay of `hsync/vsync/csync/de` and RGB to match the matrix latency (`:63-68`).
It does **not** generate csync (that's the `csync` module in `sys_top.v:1914`), does **not** do sync-on-green (SOG is an external analog function; the FPGA only drives `sog & ~cs1` onto the analog-board pin: `sys_top.v:134,182`, `sog = cfg[9]` at `:305`), does **not** do YC/CVBS (that's `yc_out.sv`, which is also field-agnostic), and has **no interlace/field awareness at all**.
Two instances exist: `vga_out vga_scaler_out` on `clk_hdmi` for the scalerβanalog path (`sys_top.v:1473-1487`) and `vga_out vga_out` on `clk_vid` for the direct analog path (`sys_top.v:1494-1508`).
---
## 5. `vga_scaler` β what it muxes, and interaction with direct_video
- `sys_top.v:309` `wire vga_scaler = cfg[2] | vga_force_scaler;` (`CONF_VGA_SCALER = 0b100`, `user_io.h:143`; HPS `cfg.vga_scaler` at `user_io.cpp:3017`, INI key `VGA_SCALER` at `cfg.cpp:42`).
- `sys_top.v:1516` `wire vgas_en = vga_fb | vga_scaler;` selects the analog output source (`sys_top.v:1518-1529`): `vgas_*` (scaler, clk_hdmi) vs `vga_*` (direct, clk_vid).
- Analog clock mux `sys_top.v:1344-1349`: `clkselect({1'b1, ~vga_fb & ~vga_scaler})` β `clk_vid` when neither, `hdmi_clk_out` otherwise β `VGA_TX_CLK`.
- Output pins `sys_top.v:1521-1525`, incl. per-mode sync polarity XOR from `HS[12]`/`VS[12]` in scaler mode and `csync_en` selection.
**Are `direct_video` and `vga_scaler` mutually exclusive?**
- **FPGA: no interlock.** They control two independent muxes. With both set: HDMI pins = dv (clk_vid), analog VGA pins = ASCAL output (clk_hdmi). This works electrically and would be a valid configuration.
- **HPS: no interlock either.** `cfg.vga_scaler` is only read at `user_io.cpp:3017`; nothing in `video.cpp`/`cfg.cpp` clears it when `direct_video` is on. The practical reason it's "exclusive" is that in direct_video the HPS programs the scaler to a 15k/31k TV mode with 3/6/2/2 borders (`video.cpp:2186-2196, 2600-2618`), so the analog output would show a full-raster scaler image with DV timings.
- The genuine exclusivity is **`vga_fb` vs `direct_video`** (`sys_top.v:1317-1320`): `vga_fb` (from `cfg[12]` or the core's `VGA_SCALER`) *overrides* direct video on the HDMI pins.
---
## 6. Scaler (non-direct_video) mode signal flow and clocking
` ` `
emu.VGA_R/G/B, VGA_HS/VS/DE (clk_vid, qualified by CE_PIXEL=ce_pix)
β sync_fix hs/vs sys_top.v:1707-1708
β scanlines VGA_scanlines (clk_vid, ce_pix) sys_top.v:1383-1399
din = de_emu ? RGB : 24'd0 sys_top.v:1388
β taps for the scaler: sys_top.v:1712-1719
clk_ihdmi = clk_vid ; ce_hpix = vga_ce_sl
hr/hg/hb = vga_data_sl ; hhs/hvs/hde = *_sl
β ascal (i_clk=clk_ihdmi=clk_vid, i_ce=ce_hpix, i_fl=f1) sys_top.v:714-830
o_clk = clk_hdmi = hdmi_clk_out (pll_hdmi) sys_top.v:712, 1041-1049
o_ce = scaler_out (=1 once cfg received) sys_top.v:335,417,765
timing from cfg 0x20 regs: htotal/hsstart/hsend/vtotal/vsstart/vsend sys_top.v:775-788
β shadowmask HDMI_shadowmask (clk_hdmi) sys_top.v:1159-1178
β osd hdmi_osd (clk_hdmi) sys_top.v:1183-1201
β csync csync_hdmi(clk_hdmi,β¦) β hdmi_cs_osd sys_top.v:1204
β mux + register on hdmi_tx_clk(=hdmi_clk_out) sys_top.v:1304-1332
β HDMI_TX_D/HS/VS/DE ; HDMI_TX_CLK via altddio_out sys_top.v:1274-1297,1334-1337
` ` `
and for the analog jack when `vga_scaler`/`vga_fb`:
` ` `
hdmi_data_osd/hs/vs/de + hdmi_cs_osd
β vga_out vga_scaler_out (clk_hdmi, ypbpr_en) sys_top.v:1473-1487
din = {24{hdmi_de_osd}} & hdmi_data_osd (blanks forced black)
β vgas_o/vgas_hs/vgas_vs/vgas_cs
β VGA_R/G/B[5:0], VGA_HS = (csync_en ? ~vgas_cs : ~vgas_hs) ^ HS[12]
VGA_VS = (~vgas_vs ^ VS[12] | csync_en) & subcarrier_out
sys_top.v:1521-1525
β VGA_TX_CLK from vga_clk_sw (= hdmi_clk_out here) sys_top.v:1344-1378
` ` `
**csync for VGA in scaler mode comes from `csync csync_hdmi` on `clk_hdmi`** (`sys_top.v:1204`), i.e. derived from the *scaler's* HS/VS, then delay-matched through `vga_scaler_out`. In non-scaler mode it comes from `csync csync_vga` on `clk_vid` (`sys_top.v:1425`).
`ce_pix` handling: the core's `CE_PIXEL` gates `scanlines`, which emits `ce_out` (`vga_ce_sl`) as ASCAL's `i_ce`; ASCAL's output side runs full-rate (`o_ce = scaler_out = 1`). `video_mixer.sv:188` is where cores typically synthesize `CE_PIXEL` (with the `fs_osc` oscillating-ce fixup).
---
## 7. The cfg register (16-bit, UIO cmd `0x01`)
FPGA parse: `sys_top.v:414-418` β `cfg`; bit meanings at `sys_top.v:288-311`. HPS assembly: `Main_MiSTer/user_io.cpp:3011-3028`, bit names `Main_MiSTer/user_io.h:143-154`.
| bit | `sys_top.v` | HPS name / INI |
|---|---|---|
| 0 | `BUTTON1` (menu button) | `user_io.h` BUTTON1 |
| 1 | `BUTTON2` (user/reset) | BUTTON2 |
| 2 | `vga_scaler` (`:309`) | `CONF_VGA_SCALER` / `vga_scaler` |
| 3 | `csync_en` (`:300`) | `CONF_CSYNC` / `composite_sync` |
| 4 | `forced_scandoubler` (`:303`) | `CONF_FORCED_SCANDOUBLER` |
| 5 | `ypbpr_en` (`:304`) | `CONF_YPBPR` (`vga_mode_int==1`) |
| 6 | `audio_96k` (`:299`) | `CONF_AUDIO_96K` |
| 7 | β (DVI) | `CONF_DVI` (used only by HPS/ADV7513 reg 0xAF) |
| 8 | β | `CONF_HDMI_LIMITED1` (HPS CSC only) |
| 9 | `sog` (`:305`) | `CONF_VGA_SOG` |
| **10** | **`direct_video`** (`:296`) | `CONF_DIRECT_VIDEO` |
| 11 | β | `CONF_HDMI_LIMITED2` |
| 12 | `vga_fb` (`:295`) | `CONF_VGA_FB` (`set_vga_fb`, `user_io.cpp:2992`) |
| 13 | β | `CONF_DIRECT_VIDEO2` (auto-DV-by-EDID marker) |
The *video mode* is a separate UIO command `0x20` (`UIO_SET_VIDEO`), parsed at `sys_top.v:419-450`; word 0 = `{HDMI_PR, vrr_mode, WIDTH[11:0]}`, word 2 = `{hpol, HS[11:0]}`, word 6 = `{vpol, VS[11:0]}`, words 8+ = PLL reconfig; sent from `Main_MiSTer/video.cpp:2246-2271`. Other relevant snooped commands: `0x27` VSET, `0x2A/0x2B` scaler filters, `0x2F` Linux framebuffer (`LFB_*`), `0x3E` shadowmask, `0x41` YC/subcarrier.
`hps_io.sv` never sees `direct_video` β it only re-decodes cmd `1` for the buttons and exposes `forced_scandoubler`/`status` to cores.
---
## 8. Menu vs PSX sys_top diff
Only these differences exist (`diff Menu_MiSTer/sys/sys_top.v PSX_MiSTer/sys/sys_top.v`) β **the entire video mux, dv path, ASCAL wiring and VGA output are identical**:
| Menu (new) | PSX (old) |
|---|---|
| `gp_in` includes `~HDMI_TX_INT` (`:231`) | 8'd0 filler (PSX `:231`) |
| `vol_boost` (`:319`, `:477`, audio_out `.boost`) | absent, `acx_att <= io_din[4:0]` |
| `sl_r` computed inside cfg block, **not** on HPS_BUS | `sl_r`/`sl` + `fb_en` exported: `HPS_BUS({fb_en, sl, f1, HDMI_TX_VS, β¦})` (PSX `:1748`); `emu_ports` `[48:0]` vs Menu `[45:0]` (`Menu_MiSTer/sys/emu_ports.vh:9`) |
| io_dout_sys replies for `0x2B/0x2F/0x3E/0x44` (feature probing) | absent |
| ascal `N_BURST(2048)` under `MENU_CORE`, `DOWNSCALE_NN`, different `ascal.vhd` md5 | older ascal |
So any change made to the video path can be written once against the new sys and back-ported to PSX with a constant line offset (dv block Menu 1207 β PSX 1192, HDMI mux Menu 1304 β PSX 1289, VGA pins Menu 1521 β PSX 1506).
---
## Flow diagrams
### (a) Scaler β HDMI (`direct_video=0`)
` ` `
core emu ββ(clk_vid, ce_pix)ββ> sync_fix β> scanlines ββ¬β> [analog branch, see (c)]
ββ> ASCAL (i_clk=clk_vid, i_ce, i_fl=f1)
DDR3 vbuf <βββ frame buffer / deinterlace
ββ(clk_hdmi=pll_hdmi)β> shadowmask β> osd_hdmi ββ¬β> csync_hdmi
ββ> mux(1317-1320)
β> reg @hdmi_tx_clk β> HDMI_TX_D/HS/VS/DE
HDMI_TX_CLK = hdmi_clk_out
` ` `
### (b) direct_video (`direct_video=1`, `vga_fb=0`)
` ` `
core emu ββ(clk_vid, ce_pix)ββ> sync_fix β> scanlines(din = de?RGB:black) β> osd_vga ββ¬β> csync_vga ββ
ββββββββββββββββ€
[yc_out if yc_en] βββββββββββββββββββββββββ€
v
dv_data/dv_hs(=cs if csync_en)/dv_vs, dv_de = synthetic full-raster DE
(all @clk_vid, sys_top.v:1207-1254)
β
ASCAL output ignored βββββββββ€ mux(1317-1320) = (~vga_fb & direct_video)
v
HDMI_TX_D = 24b RGB (pixel-repeated at clk_vid/ce_pix)
HDMI_TX_HS = HS or CSync ; HDMI_TX_VS = VS ; HDMI_TX_DE β whole raster
HDMI_TX_CLK = clk_vid
ADV7513: RGBβYPbPr via CSC if ypbpr, sync inverted by reg 0x17, PR by 0x3B
field info: f1 β ASCAL(i_fl) and HPS_BUS[45] only. NOT in this path.
` ` `
### (bβ²) direct_video with `vga_fb`/core `VGA_SCALER=1` (ao486 always; Minimig RTG; menu FB)
` ` `
core β scanlines β ASCAL(o_clk=clk_hdmi, timing = TV mode with 3/6/2/2 borders from video.cpp:2186)
β shadowmask β osd_hdmi β csync_hdmi
β HDMI_TX_HS = (direct_video & csync_en) ? hdmi_cs_osd : hdmi_hs_osd
HDMI_TX_VS/DE/D = hdmi_*_osd, HDMI_TX_CLK = hdmi_clk_out (β12.587/25.175 MHz)
` ` `
**This is already "ASCAL out over direct video" and is the natural place to add 480i.**
### (c) vga_scaler (analog jack)
` ` `
vgas_en = vga_fb | vga_scaler
vgas_en=0: scanlines β osd_vga β csync_vga β vga_out(clk_vid, ypbpr) β VGA_R/G/B/HS/VS (VGA_TX_CLK=clk_vid)
vgas_en=1: ASCAL β shadowmask β osd_hdmi β csync_hdmi β vga_out(clk_hdmi, ypbpr) β VGA pins ^HS[12]/^VS[12]
(VGA_TX_CLK=hdmi_clk_out)
SOG: pin driven by `sog & ~cs1` (sys_top.v:134,182); subcarrier for external encoders sys_top.v:1459-1466
` ` `
---
## Implications for "ASCAL outputs true 480i over direct_video"
1. **The path already exists**: `direct_video=1` + `vga_fb=1` (`cfg[12]`, or a core asserting `VGA_SCALER`) routes ASCAL's output, with csync support, to the HDMI pins at `hdmi_clk_out` (`sys_top.v:1266,1317-1320`). ao486 (`ao486.sv:439`) proves this path is production-tested. No new mux is strictly required β but note `hdmi_de_osd` (real active-area DE) is used there, not the full-raster synthetic dv DE, which is why the HPS shrinks the porches to 3/6/2/2 (`video.cpp:2186-2196`).
2. **ASCAL cannot emit interlace today.** Its output timing generator is unconditionally progressive: `ascal.vhd:2763-2793` (`o_hcpt`, `o_vcpt_pre3`, `o_hsv/o_vsv/o_dev`) with `o_vsv(0) <= (o_vcpt=o_vsstart AND o_hcpt>=o_hsstart) OR (o_vcpt>o_vsstart AND o_vcpt<o_vsend) OR (o_vcpt=o_vsend AND o_hcpt<o_hsstart)`. Producing 480i needs (a) a per-field toggle that alternates `o_vtotal` by one line / shifts the VS comparison by `htotal/2`, and (b) an output-side line-parity offset when addressing the frame buffer (`o_vacpt`/`o_rline` at `ascal.vhd:2147-2155`, which currently only serves weave/bob de-interlacing of the *input*).
3. **HPS support is missing**: `tvmodes[]` (`video.cpp:142-148`) has no interlaced entry, `parse_custom_video_mode` has no interlace flag, and `video_set_mode`/`hdmi_config_set_mode` never sets an interlaced VIC or ADV7513 interlace handling. A 480i mode also breaks the `vic=0` + `pr` assumptions in `video.cpp:1671-1693`.
4. **Framework glue to review for 480i**: the `csync` module has no serration/equalization and no field awareness (`sys_top.v:1914-1949`); the dv DE/`vsz` learner assumes constant field length (`sys_top.v:1220-1239`); `hps_io.sv:942-949` only latches resolution on even fields; `video_mixer.sv:206-216` quantizes sync edges to `CE_PIXEL`.
5. **Per-core input side**: Minimig already delivers true half-line-offset interlaced sync (`agnus_beamcounter.v:376-383`) *and* `VGA_F1`; PSX delivers only `VGA_F1` with line-aligned sync (`gpu_videoout_sync.vhd:100-104, 373-374`); ao486 hardwires `VGA_F1=0` (`ao486.sv:437`). So an ASCAL-side 480i output generator (driven by ASCAL's own field counter, not the input's) is the only approach that works uniformly across all three.
π¦ Tool Call
2026-07-27 03:02:56
β Question for User:
Opt-in: How should 480i scaler output be enabled by the user? (Under direct_video, video_mode= from MiSTer.ini is currently ignored entirely β the scaler is pinned to one of 4 fixed TV modes.)
New ini key (Recommended)
Add e.g. direct_video_interlace=1 to MiSTer.ini. Explicit, no behavior change for existing setups; combined with vga_scaler=1 + direct_video=1 it selects the 480i field mode.
Auto when both flags set
direct_video=1 + vga_scaler=1 implies 480i output. Fewer knobs, but changes meaning of an (admittedly near-unused) existing combination, and you can't get 240p scaler output over DV anymore.
video_mode flag token
Honor video_mode= under direct_video and add an 'ilace' flag token to the modeline parser. Most flexible (arbitrary interlaced modelines) but the biggest Linux-side change.
Menu at 480i: Should the Menu core / Linux framebuffer (OSD, scripts, menu background) also render at 480i (720x480-ish fb), or stay at 240p with only game-core video getting 480i?
Yes, menu too (Recommended)
Linux fb gets ~480 visible lines (sharper menu/scripts). Needs a small hook in video_fb_config() where fb height is computed (same place pixel-repeat already doubles it).
Games only
Menu core keeps the existing 240p fb path untouched; only in-core video goes through the interlaced mode. Slightly smaller diff, menu stays chunky.
Genlock: Should the first version attempt to sync output fields to the core's frame rate (vsync_adjust / low-latency PLL servo), or free-run? Under direct_video, vsync_adjust is currently force-disabled, and the PLL servo (pll_hdmi_adj via o_lltune) assumes one pulse per progressive frame β field-rate output would confuse it without extra work.
Free-run first (Recommended)
Keep vsync_adjust disabled under direct_video as today. Triple-buffered scaler absorbs the rate mismatch (occasional dropped/repeated field). Smallest, safest first version; genlock can be a follow-up.
Include genlock now
Also fix the o_vss/lltune field-rate issue and un-force vsync_adjust under direct_video, so output field rate locks to the core. More moving parts to debug on hardware at once.
π¦ Tool Call
2026-07-27 03:05:40
π¦ Tool: Agent
{
"description": "Design ascal 480i RTL change",
"subagent_type": "Plan",
"run_in_background": false,
"prompt": "Design a precise RTL implementation plan for adding true interlaced (480i/576i) OUTPUT support to the MiSTer ASCAL scaler. You are designing the VHDL change only; Linux-side plumbing is handled separately. Read the actual code carefully before finalizing \u2014 do not trust my summary blindly.\n\nFile: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd (3033 lines; id... [7945 chars total, truncated]
π¦ Tool Call
2026-07-27 03:06:29
π¦ Tool: Agent
{
"description": "Design Linux/sys_top plumbing",
"subagent_type": "Plan",
"run_in_background": false,
"prompt": "Design an implementation plan for the Linux-side (Main_MiSTer) and sys_top.v plumbing that lets the MiSTer ASCAL scaler output true 480i/576i over direct_video. The ascal.vhd interlace RTL itself is designed separately \u2014 assume ascal gains an \"interlaced output\" enable (probably a new port or mode(4) bit) that, given a field modeline (e.g. 640x240 vtotal 262), alternat... [8163 chars total, truncated]
π¦ Tool Result
2026-07-27 03:16:34
β Result (Agent):
Plan complete, written to /home/izzie/.claude/plans/sorted-giggling-pearl-agent-a33890c07006bb6b4.md. Full plan follows.
# Implementation Plan: True 480i/576i ASCAL Output over direct_video
Assumes ascal.vhd gains interlaced output via `mode(4)` β verified viable: the port is already `unsigned(4 DOWNTO 0)` (ascal.vhd:206), MODE[4] is documented "TBD" (ascal.vhd:93), and sys_top currently drives only 4 bits into it.
## Verified corrections to your stated context
- **Bit 13 of the cfg word is taken**: `CONF_DIRECT_VIDEO2` (user_io.h:154, user_io.cpp:3027) for `direct_video=2` auto-detect. Bits 14/15 are free β but the design needs no new cfg-word bit at all.
- **SET_VIDEO indexing**: the C loop sends `item[1..8]` as FPGA `cnt 0..7`, so the vact word (C `item[5]`) is FPGA `cnt==4` (`4: HEIGHT <= io_din[11:0]`, sys_top.v:427). Bit 15 there is free. (cnt0 bit15=HDMI_PR, bit14=vrr_mode β both 1-bit; cnt2/cnt6 bit15 = sync polarity.)
- **There is NO pixel repetition on 15kHz modes today.** `tvmodes[]` (video.cpp:139-144) have `pr=0`, Fpix=12.587MHz. `hdmi_config_set_mode()` (video.cpp:1671-1706): `cfg.direct_video && is_menu()` β `pr_flags=0` (ADV auto β with VIC=0 this repeats nothing); `pr!=0` β `0b01001000` (manual PR, 2x clock β used only for 2560-wide modes, video.cpp:4308-4316, where timings/Fpix are halved and ADV multiplies the clock); else `0b01000000`. So the ADV7513 simply runs at 12.587MHz, below its rated minimum β existing direct_video status quo. **The 480i field mode sets nothing PR-related**: pr=0, Fpix unchanged.
- **DV porch rewrite preserves vtotal**: NTSC field 240+(4β2)+(14β2)=254 active, +2+4+2 = 262 total; PAL 288β304, total 312. Correct field modelines for the ascal RTL (which alternates 262/263, 312/313).
- **A capability handshake already exists**: `spi_uio_cmd(UIO_SET_VIDEO)` with no payload returns `'b11` (sys_top.v:398); bit0=`supports_pr()` (video.cpp:201), bit1=`supports_vrr()`. Bit 2 is the natural interlace-capability bit.
- **Menu core under direct_video already routes ascal to the DV pins** via `video_fb_enable()` β `set_vga_fb(1)` (video.cpp:3403) β CONF_VGA_FB β `vga_fb=cfg[12]` β HDMI mux takes the ascal path (sys_top.v:1317-1320); ao486 does it permanently via `assign VGA_SCALER=1` (ao486.sv:439) β `vga_force_scaler` (sys_top.v:295). PSX has `assign VGA_SCALER=0` (PSX.sv:193). Forcing CONF_VGA_FB from Main is exactly equivalent (plain OR, no double-set hazard).
- ao486 and Menu sys_top.v are **line-identical** at every edit site; **PSX offsets β β15 lines** and its SET_VIDEO decode already matches (incl. HDMI_PR), so backport is mechanical.
## 1. cfg.h / cfg.cpp β new ini key
- Name: **`DIRECT_VIDEO_INTERLACE`** (consistent with DIRECT_VIDEO / FORCED_SCANDOUBLER); `uint8_t direct_video_interlace;` added after `direct_video` at cfg.h:28.
- cfg.cpp: next to line 75: `{ "DIRECT_VIDEO_INTERLACE", (void*)(&(cfg.direct_video_interlace)), UINT8, 0, 1 },` β default 0 via zero-init.
## 2. Routing β force CONF_VGA_FB from Main (recommended; no FPGA routing change)
The FPGA alternative (`vga_fb = cfg[12] | (direct_video & vga_scaler & new_bit)`) is rejected: it needs a new cfg bit plus per-core rebuilds for zero functional gain.
- video.cpp (near supports_pr, ~:215): add `static bool supports_ilace()` reading version bit 2 (`& 4`), plus exported helper:
`int video_dv_scaler_forced() { return cfg.direct_video && cfg.direct_video_interlace && cfg.vga_scaler && supports_ilace(); }` (declare in video.h).
- user_io.cpp:3028: `if (vga_fb || video_dv_scaler_forced()) map |= CONF_VGA_FB;`
- Interactions (all verified): OSD/script `set_vga_fb(0)` toggling can't drop the bit (OR keeps it) β no change in video_fb_enable(); ao486's hardwired path is a no-op overlap; menu core works via existing set_vga_fb(1) regardless of vga_scaler; `vga_nag()` (menu.cpp:847-875) targets the OSD_VGA instance (legacy VGA connector), not the DV output β leave alone.
- Gating on `supports_ilace()` means old-sys cores degrade to today's raw-DV behavior, never a mis-programmed mode.
## 3. vmode struct + video_mode_load()
- video.cpp:178: `uint32_t unused[6];` β `uint32_t ilace; uint32_t unused[5];` (item[26]; static_assert at :188 holds; parse/store_custom_video_mode never touch it, so custom modelines stay progressive; video_mode_load's memset re-zeroes it each pass).
- video_mode_load() direct_video branch (video.cpp:2599-2614):
` ` `c
int mode = cfg.menu_pal ? 2 : 0;
int ilace = (cfg.direct_video_interlace && supports_ilace()) ? 1 : 0;
if (!ilace && cfg.forced_scandoubler) mode++;
...
v_def.param.ilace = ilace;
` ` `
**forced_scandoubler recommendation: the interlace key wins** (more specific opt-in; the 31k tvmode contradicts it) β printf a note when both are set. Leave CONF_FORCED_SCANDOUBLER in the cfg word untouched; it only affects the core's raw output, which isn't displayed when vga_fb is forced.
- PAL: menu_pal β tvmodes[2] (640x288, vtotal 312) + ilace β 576i, same key.
- Re-entry safety verified: the `cfg_has_video_sections()` path (video.cpp:3244-3247, 3299-3306) re-runs video_mode_load (ilace re-derived) before re-sending v_def β nothing clobbers the flag with a progressive mode; the `vsync_adjust` branch is unreachable under direct_video (forced 0 at :2593-2597); `use_vrr` stays 0 (set_vrr_mode only called in the !direct_video branch of video_set_mode).
## 4. video_set_mode() + ADV7513
- Word loop (video.cpp:2248-2258), add: `else if (i == 5) spi_w(((v_cur.param.ilace ? 1 : 0) << 15) | v_fix.item[i]);` β bit15 of the vact word (FPGA cnt 4); old sys_tops ignore it.
- Porch rewrite (2186-2197): unchanged (vtotal preserved, see above).
- PLL/PR: unchanged β Fpix 12.587 (setPLL at :2609); once vga_fb=1 the clock mux (sys_top.v:1264-1268) puts hdmi_clk_out (pll_hdmi) on the pins.
- hdmi_config_set_mode(): no change. Game cores get `0b01000000`/VIC 0, menu gets auto β same registers as today; DV DACs ignore infoframes. (Optional later: VIC 6/7 for 480i.)
## 5. sys_top.v (Menu_MiSTer; ao486 identical lines; PSX β β15)
1. **:398** `io_dout_sys <= 'b11;` β `'b111` (capability bit 2). [PSX :394]
2. **:427** `4: HEIGHT <= io_din[11:0];` β `4: {ILACE,HEIGHT} <= {io_din[15],io_din[11:0]};` [PSX :415]
3. **:1061** add `reg ILACE = 0;` next to `reg HDMI_PR = 0;` [PSX :1046]
4. **:791** `.mode({~lowlat,LFB_EN ? LFB_FLT : |scaler_flt,2'b00}),` β `.mode({ILACE,~lowlat,LFB_EN ? LFB_FLT : |scaler_flt,2'b00}),` β now exactly 5 bits. [PSX :776]
Nothing else: aspect window math (:892-994) and LFB overrides (:955-960) operate in field-raster coordinates (HEIGHT=254 after DV porch rewrite), identical to today's OSD-over-DV path; HDMI_PR stays 0; csync (:1204, module :1914) passes ascal's hs/vs through.
## 6. Menu/Linux fb at 480i (video.cpp)
Verified principle: all output-window programming (LFB_VMIN/VMAX, fb window regs, UIO_SETHEIGHT/VSET) stays in **field coordinates** (0..239 within item[5]); only the fb **source** height doubles β ascal sees ivsize=480 with o_vsize=240 per field.
- **video_fb_config()** (3419-3447), with `const int il = v_cur.param.ilace ? 1 : 0;`:
- :3427 guard: `(v_cur.item[1] * (v_cur.item[5] << il)) > FB_SIZE`
- :3439 `fb_height = (v_cur.item[5] << il) / fb_scale_y;`
- Do NOT touch fb_scale_y's pr term β pr doubling is the opposite direction (halves fb height to keep square pixels at doubled display width). Result: 640x480 / 640x576 menu fb at scale 1.
- **video_fb_enable()** (3337-3406): no change β sends fb_height (480) as source height, window `yoff..yoff+item[5]-1` (240 field lines). fb_write_module_params: no change.
- **fb_cmd1/fb_cmd2** (4063-4099): clamp height against `item[5]<<il`; gate the 240p heuristic with `&& !il` (:4082); generic branch compares against `item[5]<<il`; window in field coords: `vmin = (v_cur.item[5] - ((height*divy) >> il)) / 2; vmax = vmin + ((height*divy) >> il) - 1;` fb_cmd2: `height = (item[5]<<il)/div`, vmax stays `item[5]-1`.
- **Menu logo halving** (:3929): add `&& !v_cur.param.ilace`.
- **video_scaling_adjust** (2990-3034): no change (field units both sides).
## 7. Ordering constraints
None new. `scaler_out` enables on the first cfg word (cmd==1, :414-418); `cfg_done` on SET_VIDEO PLL completion (:447); ILACE latches with SET_VIDEO and feeds ascal continuously. Existing sequence is already correct: `video_init()` (user_io.cpp:1495) β SET_VIDEO with ILACE β `user_io_send_buttons(1)` (:1499) with forced CONF_VGA_FB; every later video_set_mode is followed by a forced button send (video.cpp:3247/3288/3300/3306, 2712). `supports_ilace()` probes lazily post-FPGA-load, same as supports_pr.
## 8. Risks / open questions
1. **ascal RTL contract** (external): mode(4)=1 must alternate 262/263 (312/313) fields with half-line vsync, offset per-field sampling, and keep o_vbl/o_brd/o_de sane per field (shadowmask/osd are downstream). Define behavior when a game core feeds 240p content (each field scaled from the same frame β acceptable?).
2. **csync module** (sys_top.v:1914-1948) has no equalization pulses; with half-line vsync starts some TVs may misjudge field phase. Hardware test; possible follow-up serration work (kept out of v1).
3. **OSD on interlaced output**: hdmi_osd draws identically in both fields β "fat line" OSD with edge flicker. Cosmetic, accept.
4. ADV7513 at 12.587MHz stays below spec β unchanged status quo.
5. Free-running fields (vsync_adjust=0): cadence not genlocked; triple-buffer absorbs, as today. Accepted v1.
6. Every core needs a sys rebuild for the capability bit + mode(4); until then the key silently degrades to raw DV (by design). Document in ini template.
7. Filter-menu un-greying (menu.cpp:3156-3193) via `!video_dv_scaler_forced()` β nice-to-have, deferred.
8. `video_mode_cmd` (:4185) debug path can inject a progressive modeline (ilace=0 β coherent fallback; no guard).
9. `show_video_info` string (:2847) will print "640x240" β cosmetic follow-up.
## Suggested order
sys_top.v (4 one-liners, then copy ao486/Minimig, PSX at β15) β cfg key β video.cpp flag plumbing (supports_ilace, struct, mode_load, set_mode) β user_io.cpp OR-in β fb geometry/heuristics β hardware matrix (menu NTSC/PAL, ao486, generic core + vga_scaler=1, scripts fb, OSD toggling, key-off regression).
### Critical Files for Implementation
- /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp
- /home/izzie/Desktop/mister-scaler/Main_MiSTer/user_io.cpp
- /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/sys_top.v
- /home/izzie/Desktop/mister-scaler/Main_MiSTer/cfg.cpp
- /home/izzie/Desktop/mister-scaler/Main_MiSTer/cfg.h
π¦ Tool Result
2026-07-27 03:27:05
β Result (Agent):
The Write tool isn't available in this read-only context, so I'll deliver the complete plan inline.
---
# Implementation Plan: True Interlaced (480i/576i) Output in ASCAL
## 0. Corrections to the briefing (verified against code)
Three places where the provided context is wrong or incomplete; the plan below accounts for all of them.
**C1 β The suggested field pairing is wrong.** The briefing says "field 1 = 263 lines, vsync aligned; field 2 = 262 lines, vsync delayed by htotal/2". That produces vsync leading-edge spacings of 19 + 244 = 263.0ββ¦ concretely: (263β244)+244.5 = 263.5 lines and (262β244.5)+244 = 261.5 lines β alternating 263.5/261.5, not 262.5/262.5. A CRT would show line pairing. The invariant (verified against `agnus_beamcounter.v:376-383`: long field 313, vsync at line 3.0; short field 312, vsync at 2.5 β both spacings (313β3)+2.5 = (312β2.5)+3 = 312.5) is: **(vtotal_eff β vsync_position) must differ by exactly 0.5 between the fields**. With a *+half-line delayed* vsync, the *delayed* field must be the **long** one: long field = vtotal+1 lines with vsync at vsstart+0.5; short field = vtotal lines with vsync at vsstart. Then both spacings are (vtotal+1βvsstartβ0.5)+vsstart = (vtotalβvsstart)+vsstart+0.5 = vtotal+0.5 = 262.5. (Minimig instead delays the short field by β0.5; both are valid, mine avoids a vsstartβ1 underflow case.)
**C2 β Sampling bias belongs to the field *following* the delayed vsync, not the delayed field itself.** A field's content is vertically positioned on the CRT by the vsync at the end of the *previous* counter field. Retrace distance to active line L: short-field vsync β (vtotal β vsstart) + L; long-field (delayed) vsync β (vtotal+1 β vsstart β 0.5) + L = half a line *more* β content drawn half a pitch *lower*. Lower raster must show *later* source positions. So: the field whose *preceding* field was the long/delayed one gets the +Β½-step sampling bias. With a single toggling register `o_fld` where `o_fld='1'` β long+delayed sync, the sampling bias applies when **`o_fld='0'`** (i.e. bias keyed on `NOT o_fld`).
**C3 β "Works for arbitrary input heights" is false for `o_ivsize > o_vsize`.** The o-side copy engine writes 1 pixel/`o_clk` into the line buffers and the state machine issues exactly one line-fetch per hsync (`sHSYNCβsREAD` once, `o_alt=altx(o_vacptl+1)` once, ascal.vhd:2104-2110, 2169-2180). At 480i timing (htotal=800 clocks @12.587 MHz, 640-px lines) the sustainable line-consumption rate is β€ 800/640 β 1.25 source lines per output line. The pure phase-bias approach consumes `ivsize/vsize` lines per field line β for the flagship 640Γ480 menu framebuffer (iv=480, vs=240) that is 2.0: physically impossible (1280 pixel-writes per 800-clock line), and additionally `o_vacc` would exceed `2*o_vsize` making `o_vfrac` overflow (ODivider, 1836-1857, produces quotient mod 4096). Note the *core* (non-framebuffer) path structurally guarantees `o_ivsize β€ o_vsize` (`i_vdown` at 1312 with `i_ovsize<=o_vsize` at 1333, `i_vrsize` at 1601; interlaced inputs take the `i_half` path at 1318-1330), so only `o_fb_ena` can violate it. **Plan: two sub-modes** β phase-bias for `iv β€ vs` (all interpolation modes, arbitrary heights), and a *field-decimation* address mode for `iv = 2Β·vs` exactly (true weave; 1 fetch/line; covers the 480-line menu FB and is bandwidth-clean). `vs < iv < 2Β·vs` and `iv > 2Β·vs` are unsupported (Linux contract; today these are unreachable anyway).
Minor: tvmodes verified at `Main_MiSTer/video.cpp:142-148` β NTSC 15K = {640,30,60,70, 240,4,4,14} @12.587 β htotal=800, hsstart=670, hsend=730, vtotal=262, vsstart=244, vsend=248. PAL 15K β vtotal=312, vsstart=294, vsend=298. All other briefing line references checked and correct. All four `ascal.vhd` copies are byte-identical modulo CRLF (md5-verified), so one edit set replicates 4Γ.
---
## 1. Mode-bit choice: reuse `mode(4)`
Use **`mode(4)`** (documented "TBD" at line 92, declared 5 bits at line 206). Justification over a new port:
- It is already latched into the o_clk domain: `o_mode <= mode` at 1883 (the standard `-- <ASYNC>` quasi-static pattern; the bit is static per modeline). No new CDC.
- Verified unused everywhere: only `i_mode(3)`/`i_mode(2:0)` and `o_mode(3)`/`o_mode(2:0)` are read (grep: 1194, 1309, 1974, 2008, 2014). It harmlessly rides into `o_hmode`/`o_vmode`/`i_mode`.
- Zero entity/port churn across 4 repos; sys_top today passes a 4-bit concat that Verilog zero-extends, so `mode[4]=0` everywhere until explicitly driven.
Entity diff: **none**. Documentation diff at line 92:
` ` `vhdl
-- MODE[4]
-- 0 : Progressive output
-- 1 : Interlaced output (field-based modeline; alternates vtotal / vtotal+1)
` ` `
In-architecture alias (concurrent, near line 1824):
` ` `vhdl
o_ilace <= o_mode(4);
` ` `
sys_top.v (two edits, Menu_MiSTer/sys/sys_top.v):
1. Decode (UIO `'h20`, line 423 β bit 13 of word 0 is free: `[15:14]` taken by `{HDMI_PR,vrr_mode}`, `[11:0]` by WIDTH):
` ` `verilog
0: {HDMI_PR,vrr_mode,ilace_out,WIDTH} <= {io_din[15:14], io_din[13], io_din[11:0]};
` ` `
with `reg ilace_out = 0;` next to `vrr_mode` (line 336).
2. Instantiation (line 791):
` ` `verilog
.mode ({ilace_out, ~lowlat, LFB_EN ? LFB_FLT : |scaler_flt, 2'b00}),
` ` `
## 2. New signals (declare near lines 462-476)
` ` `vhdl
SIGNAL o_ilace : std_logic; -- alias of o_mode(4)
SIGNAL o_fld : std_logic := '0'; -- field register. '1' = long field (vtotal+1, delayed VS)
SIGNAL o_vtotal_eff : natural RANGE 0 TO 4096; -- per-field total (needs 4096, hence not uint12)
SIGNAL o_hs2p : natural RANGE 0 TO 8190; -- hsstart + htotal/2, pre-wrap
SIGNAL o_hs2 : uint12; -- mid-line VS pixel position (wrapped)
SIGNAL o_vs2start,o_vs2end : uint12; -- VS line numbers for the delayed window
SIGNAL o_ivsize_eff : uint12; -- ivsize seen by the vertical Bresenham
SIGNAL o_ivdouble : std_logic; -- '1': field-decimation (iv = 2*vs) mode
` ` `
`o_fld` gets its power-up value from the declaration initializer, matching OSWEEP's existing reset-less style (cf. `o_bibu` at 538). It lives in OSWEEP; all readers are in the same o_clk domain.
## 3. Field register and effective vtotal (OSWEEP, 2757-2830)
Edit the frame-wrap block (2772-2779) and add the registered `o_vtotal_eff` (inside the process, outside `IF o_ce` is fine β it must only be valid one full line after `o_fld` toggles, and it is):
` ` `vhdl
IF o_ilace='1' AND o_fld='1' THEN
o_vtotal_eff<=o_vtotal+1; -- long field, delayed VS
ELSE
o_vtotal_eff<=o_vtotal;
END IF;
...
IF o_vcpt_pre3+1>=o_vtotal_eff THEN -- was: >=o_vtotal
o_vcpt_pre3<=0;
IF o_ilace='1' THEN o_fld<=NOT o_fld; ELSE o_fld<='0'; END IF;
ELSIF o_vrr_sync2 THEN ...
` ` `
- The 1886-1905 input latch is untouched; `o_vtotal` stays a pure copy of the port. Only the wrap comparison uses `o_vtotal_eff`.
- Toggle happens when `pre3` wraps, i.e. 3-4 counter lines before `o_vcpt` wraps. Every consumer is safe: vsync comparisons run at `o_vcpt = vsstart..vsend+1 β€ 249/299`, and the toggle occurs at `o_vcpt = vtotal_effβ4 β₯ 258/308` (contract in Β§11). The frame-init (`o_vcpt_pre2=o_vmin`, 2086) and the fload preload (`o_vcpt_pre3=o_vmin`, 2039) both fire *after* the toggle, so the whole fetch/sample pipeline of a field consistently sees that field's `o_fld`.
- `o_ilace='0'` forces `o_fld='0'` at the next wrap β all new muxes fall back to bit-identical stock behavior.
## 4. Half-line vsync (edit 2791-2793 + precompute in Scalaire latch section)
Precompute static registers in Scalaire after line 1906 (multi-cycle settling is fine, these depend only on latched modeline values):
` ` `vhdl
o_hs2p <= o_hsstart + o_htotal/2;
IF o_hs2p >= o_htotal THEN
o_hs2 <= o_hs2p - o_htotal;
o_vs2start<= o_vsstart + 1;
o_vs2end <= o_vsend + 1;
ELSE
o_hs2 <= o_hs2p;
o_vs2start<= o_vsstart;
o_vs2end <= o_vsend;
END IF;
` ` `
Replace the `o_vsv(0)` assignment (2791-2793):
` ` `vhdl
IF o_ilace='1' AND o_fld='1' THEN
-- Long field : VSync delayed by htotal/2 (starts and ends mid-line)
o_vsv(0)<=to_std_logic((o_vcpt=o_vs2start AND o_hcpt>=o_hs2) OR
(o_vcpt>o_vs2start AND o_vcpt<o_vs2end) OR
(o_vcpt=o_vs2end AND o_hcpt<o_hs2));
ELSE
o_vsv(0)<=to_std_logic((o_vcpt=o_vsstart AND o_hcpt>=o_hsstart) OR
(o_vcpt>o_vsstart AND o_vcpt<o_vsend) OR
(o_vcpt=o_vsend AND o_hcpt<o_hsstart));
END IF;
` ` `
**Concrete NTSC check** (htotal=800, hsstart=670, vsstart=244, vsend=248, vtotal=262): `o_hs2p = 670+400 = 1070 β₯ 800` β `o_hs2=270, o_vs2start=245, o_vs2end=249`. Short field VS: (244,px670)β(248,px670), 4.0 lines. Long field VS: (245,px270)β(249,px270), 4.0 lines, exactly htotal/2 later in the field. Leading-edge spacing in pixels: short field length 262Β·800=209600, edge at 244Β·800+670=195870; long edge at 245Β·800+270=196270. Shortβlong: (209600β195870)+196270 = 210000 px = 262.5 lines; longβshort: (210400β196270)+195870 = 210000 px. Two fields = 420000 px = 525 lines @12.587 MHz β 59.94 Hz fields. PAL: hs2p=1070β same wrap, vs2 = 295..299 < 312 β, 625 lines/frame.
The line-wrap at the field boundary needs no extra handling because the window `(vsstart,hsstart)+H/2 β¦ (vsend,hsstart)+H/2` is contiguous in (vcpt,hcpt) lexicographic order and, by the Β§11 contract (`vsend+1 β€ vtotalβ4`), never straddles the vcpt wrap.
**Pipeline stages:** nothing else needed. `o_vsv(1 TO 11)` shift (2797) and the stage-11 output (2972) delay the mid-line edge by 12 `o_ce` ticks exactly like every other sync edge. Side consumers of `o_vsv(0)/(1)` keep working: the buffer-swap falling-edge detects at 1934/1938/1956 and the avl `o_fb_base` latch at 1702-1717 just move half a line later within vblank β still exactly one event per field.
## 5. Hsync / DE / border: no change (argument)
- `o_hsv(0)` (2790) depends only on `o_hcpt` β every counter line, including the extra 263rd line and the vsync region, gets an identical hsync. This is precisely what analog interlace requires (uninterrupted hsync train; only vsync moves).
- `o_dev/o_pev/o_end` (2786-2789) depend on `(o_hcpt, o_vcpt)` against `hdisp/vdisp/hmin..vmax`. Both fields display the identical window at identical `o_vcpt`; the long field's extra line has `o_vcpt = vtotal β₯ vdisp` β DE low, `o_pev` low β border/black, i.e. just one more blank line. No comparator changes, no overflow (`uint12` holds vtotal+1 β€ 4096-1 per contract vtotal β€ 4094).
- Border colour mux (3009-3013) driven by `o_pev(11)` β unchanged.
## 6. Vertical sampling
### 6a. Effective ivsize / decimation flag (Scalaire latch section, after the `o_fb_ena` override at 1982-1988)
` ` `vhdl
IF o_ilace='1' AND o_ivsize>o_vsize THEN
o_ivdouble <='1';
o_ivsize_eff<=o_ivsize/2; -- requires o_ivsize = 2*o_vsize (Linux contract), even
ELSE
o_ivdouble <='0';
o_ivsize_eff<=o_ivsize;
END IF;
` ` `
(Registered, quasi-static: `o_ivsize` changes only at the VS-falling latch (1942) or the fb override (1983); consumed at frame init two-plus lines later.)
Then replace `o_ivsize` by `o_ivsize_eff` at exactly these four vertical-path sites β and nowhere else (`o_ihsize*` sites 1992-2005, 2202 are horizontal; the bob block 2155-2164 is unreachable in decim mode, see Β§8):
- 2079: `o_vacc_next<=(o_vacc_next + 2*o_ivsize_eff) MOD 8192;`
- 2088: `o_vacc_next<=o_vacc_ini + 2*o_ivsize_eff;`
- 2898: `IF to_integer(o_vacpt)>o_ivsize_eff THEN` (VSCAL bottom clamp)
- 2904: `ELSIF to_integer(o_vacpt)=o_ivsize_eff THEN`
### 6b. Phase bias (edit 2051)
` ` `vhdl
IF o_ilace='1' AND o_ivdouble='0' AND o_fld='0' THEN
-- Offset field (follows the long/delayed-VS field): sample +1/2 output step lower.
-- (o_vsize - o_ivsize_eff) + o_ivsize_eff = o_vsize : always in [0,2*o_vsize), no wrap.
o_vacc_ini<=o_vsize;
ELSE
o_vacc_ini<=(o_vsize - o_ivsize_eff + 8192) MOD 8192;
END IF;
` ` `
Asymmetric bias (field A = stock, field B = stock + `ivsize_eff`) is chosen over the symmetric Β±ivsize/2 split deliberately: the biased value is *exactly* `o_vsize`, which for `iv β€ vs` is always `< 2*o_vsize`, so **`o_vacc` never exceeds the divider range at init and the frame-init block at 2087-2093 needs no `o_vacpt`/`o_vacptl` change** (both fields init `vacpt=x"001"`, `vacptl="01"`). The cost is a constant 0.25-field-line downward shift of the whole picture (βΒΌ scanline) β imperceptible, and it keeps the top-line behavior in the well-tested stock regime (symmetric bias would make field A's init vacc negative-encoded, feeding garbage `o_vfrac` into bicubic/polyphase on the top line).
**Verification trace, bias path, iv=200 source β 240-line field (bilinear, `bil_calc` = `pixq(1)Β·(1βf)+pixq(2)Β·f`, verified at 795-807; window identity: `pixq(i) = o_lineN[(o_vacptl+2+i) mod 4]`, newest fetch (source line `o_vacpt`) lands in slot `o_vacptl+1`, so the interp pair is source lines `(o_vacptβ2, o_vacptβ1)`):**
2iv=400, 2vs=480, ini_A=40, ini_B=240.
- Field B, hsync execs (sHSYNC 2069-2110): init: vacc=240, next=640, vacpt=1, vacptl=01. Next hsync: dif=640β480=160 β carry: vacc=160, nextβ160β+400=560, vacpt=2, vacptl=10, `o_vfrac`=160/480=β .
- Displayed line k=0 (uses the values from the hsync of the preceding counter line): pair = (source 0, source 1), f=β β position β . Ideal: pos_stock(0)+iv/2vs = (0.5Β·200/240β0.5) + 200/480 = β1/12 + 5/12 = **β ** β.
- k=1: dif=560β480=80 β carry: vacc=80, vacpt=3, f=80/480=1/6, pair=(1,2) β position 7/6. Ideal: β +5/6 = **7/6** β.
- Preload sufficiency: fload=2 filled all four slots with source line 0, fload=1 put line 1 in slot 2 (`o_alt="0100"`); at k=0, vacptl=10 β pixq=(l0,l1,l2,l3)=(line0,line0,line1,line2-in-flight) β pair (line0,line1) present β; the in-flight pixq(3) situation is identical to stock's first line (benign, only poly/bicubic tap). **No preload change for the bias path.**
- Field A is bit-identical to stock by construction. 1:1 case (iv=vs=240): field A shows source line k exactly; field B shows k+0.5 (f=0.5 constant) β.
### 6c. Field-decimation mode (`o_ivdouble='1'`, iv=2Β·vs) β address edits at 2147-2180
The Bresenham runs 1:1 (`iv_eff = vs` β ini=0, fracβ‘0, one carry and one fetch per line β stock-equivalent load). The interlace geometry comes purely from addressing: effective line n of sampling-field f = physical line 2n+f, f = `NOT o_fld` (Β§0-C2).
Edit 2165-2167 (keep the bob branch first; it is unreachable here, Β§8):
` ` `vhdl
IF o_inter='1' AND o_bob_deint='1' THEN
... (2155-2164 unchanged)
ELSIF o_ivdouble='1' THEN
-- Interlaced output from double-height framebuffer : read lines 2n+field
o_adrs_pre<=to_integer(o_vacpt(10 DOWNTO 0) & (NOT o_fld)) * to_integer(o_stride);
ELSE
o_adrs_pre<=to_integer(o_vacpt) * to_integer(o_stride);
END IF;
` ` `
Edit the preload/fetch address block 2169-2180 (effective lines 0 and 1 = physical `f` and `2+f`):
` ` `vhdl
IF o_adrsa='1' THEN
IF o_fload=2 THEN
IF o_ivdouble='1' AND o_fld='0' THEN -- sampling field = '1' : odd lines
o_adrs<=to_unsigned(o_hbcpt * N_BURST,32) + o_stride;
ELSE
o_adrs<=to_unsigned(o_hbcpt * N_BURST,32);
END IF;
o_alt<="1111";
ELSIF o_fload=1 THEN
IF o_ivdouble='1' THEN
IF o_fld='0' THEN -- odd field : line 3
o_adrs<=to_unsigned(o_hbcpt * N_BURST,32) + (o_stride & '0') + o_stride;
ELSE -- even field : line 2
o_adrs<=to_unsigned(o_hbcpt * N_BURST,32) + (o_stride & '0');
END IF;
ELSE
o_adrs<=to_unsigned(o_hbcpt * N_BURST,32) + o_stride;
END IF;
o_alt<="0100";
ELSE
o_adrs<=to_unsigned(o_adrs_pre + (o_hbcpt * N_BURST),32);
o_alt<=altx(o_vacptl + 1);
END IF;
END IF;
` ` `
(`o_alt` masks unchanged. `o_stride & '0'` = 2Β·stride, numeric_std zero-extends the narrower add operand. `o_adrs_pre` max = 481Β·stride β 1.2 MB < 2^24 β. Timing of `o_fld` for preloads: fload=2 fires at `pre3=vmin`, which is β₯1 line after the toggle, so the *new* field's value is used β.)
**Verification trace, decim, 480-line FB β 240-line field, sampling field f:** init: vacc=0, next=480, vacpt=1, vacptl=01; slots after preload = {s0:f, s1:f, s2:2+f, s3:f}. Next hsync: dif=480β480=0 β carry: vacc=0, vacpt=2, vacptl=10, frac=0; fetch effective line 2 = physical 4+f into slot 3. Displayed k=0: pixq=(s0,s1,s2,s3), pair=(s1,s2)=(f, 2+f), f=0 β **line f** β (nearest and bilinear identical at frac 0). k=1: vacpt=3, vacptl=11, fetch 6+fβslot0; pixq=(s1,s2,s3,s0), pair=(2+f, 4+f), frac 0 β **line 2+f** β. Pure weave; field `o_fld='1'` (long) shows even lines on the upper raster, field `o_fld='0'` shows odd lines half a pitch lower β.
## 7. Triple buffering / per-field frame grab (point 6)
No change needed. The output-side buffer swap keys on the `o_vsv` falling edge (1934-1965), which now occurs once per **field** β so each field independently grabs the newest completed input frame (`buf_next`), giving the desired 60 fps β 60 fields behavior with tear-free swaps in vblank. `o_ivsize/o_hdown/...` re-latch per field (1941-1944) is harmless (static per resolution). `swblack`'s `o_newres` counts down in fields instead of frames (3 black fields β 50 ms β fine). Framebuffer mode bypasses buffers entirely (`avl_o_offset* <= o_fb_base`, 1714-1717).
## 8. Input-interlace collision (point 7)
- **Bias path composes safely with `o_inter='1'`** β no suppression needed: the bias only changes `o_vacc_ini`, i.e. *when* carries occur; the mapping `o_vacpt β source line` and hence `o_rline <= o_vacpt(0)` (2147) buffer selection is untouched. (Note: with a 240-line output window, 480i inputs take the `i_half='1'` path (1322-1325) and are stored as whole half-frames, exactly as with today's 240p output β same behavior, plus the half-line output phase. True field-locked 480i passthrough needs genlock = v2.)
- **Decim path cannot collide**: it requires `o_ivsize > o_vsize`, which the core path structurally never produces (`i_vrsize β€ i_ovsize = o_vsize`, lines 1312/1333/1601) β only `o_fb_ena` reaches it. In decim, `o_rline` toggles meaninglessly but both avl offsets equal `o_fb_base` (1716-1717) β don't-care. **Menu LFB works in both flavors**: FB height β€ 240 β bias path; FB height = 480 β true weave.
- **Bob guard** (recommended, one line at 1915): `o_bob_deint <= bob_deint AND NOT o_ilace;` β bob's half-line address hack plus output fields is visually nonsensical; falling back to weave is strictly better.
## 9. o_vbl / o_vss / lltune / VRR (point 8)
- `o_vbl` (`o_end`) and `o_vss` (2795): unchanged; per-field semantics are correct.
- lltune bit 4 (3026): now pulses per field with alternating 262/263-line periods. `pll_hdmi_adj` would dither if vsync_adjust were enabled β **Linux contract: vsync_adjust off when ilace** (already planned for direct_video). No RTL change; flagged as the only lltune hazard.
- VRR guard (2826): `o_vrr_sync<=(o_vrr='1' AND o_ilace='0' AND (o_sync OR o_sync_max) AND o_vcpt2>=o_vdisp AND o_vcpt2<o_vsstart);` β prevents the `o_vcpt_pre3<=o_vsstart` jump (2774-2776) from ever fighting the field alternation even if Linux misconfigures.
## 10. Reset / runtime toggle (point 9)
`o_fld` initializes to '0' via declaration (OSWEEP has no reset clause β consistent with `o_vcpt*`). Enabling `mode(4)` mid-stream: alternation starts at the next frame wrap; at most one field with stale phase (mode changes are always accompanied by a full modeline reprogram and usually swblack). Disabling: `o_fld` forced '0' at the next wrap; every mux (`vtotal_eff`, vsv, vacc_ini, adrs, bob-gate) then evaluates to the stock expression β output is bit-identical to today's progressive path.
## 11. Downstream sys_top audit (verified, no changes needed)
- **OSD** (`osd.v:155-255`): uses no vsync at all for logic β it counts lines from DE edges and detects frame start from the long DE gap (`h_cnt > 4*dsp_width`, line 201), and already contains explicit interlace support (`f1 <= ~f1; // skip every other frame for interlace compatibility`, line 203). Alternating 262/263 fields and mid-line VS are invisible to it. OSD renders identically in both fields β line-doubled on the CRT, correct.
- **shadowmask** (`shadowmask.sv`): `vcount` resets on VS falling edge per field β pattern restarts each field; cosmetic only (and off for TV modes).
- **csync module** (sys_top.v:1914-1949, instantiated at 1204 for the scaler/HDMI path feeding both HDMI DACs and `vga_out` when `vga_scaler`): it measures `hs_len` and `line_len` from the hsync train only (unaffected β hsync is uniform in both fields). During VS it forces `csync_hs` high from `(next hsync start β hs_len)` to the next hsync rising edge, i.e. line-rate serration. A mid-line VS edge lands at px 270, where `csync_hs` is deterministically 0 (serration window is px 610-670) β the broad-sync starts/ends cleanly mid-line β which is exactly what defines field 2 to a CRT. Output quality equals the existing csync (no true half-line equalization pulses, a pre-existing limitation CRTs tolerate). **Acceptable.**
- **HDMI mux** (1304-1337): pure registered mux, no frame-shape assumptions. (Real HDMI *displays* may reject non-CEA 480i; the target is DAC bridges β Linux concern.)
- **vga_out / yc_out path** (1430-1530): combinational/per-pixel; `vga_scaler` path reuses `hdmi_*_osd` + `csync_hdmi` per above. `vs_wait`/'h30 (line 548) still sees one VS per field. No breakage.
## 12. Ordered edit list
`Menu_MiSTer/sys/ascal.vhd` first, then byte-replicate (preserving CRLF for PSX) to the other three:
1. Comment block line 88-92: document MODE[4].
2. Signal declarations (Β§2) near 462-476.
3. Concurrent alias `o_ilace <= o_mode(4);` near 1824.
4. Scalaire latch section after 1906: `o_hs2p/o_hs2/o_vs2start/o_vs2end` (Β§4).
5. Scalaire after 1988: `o_ivdouble/o_ivsize_eff` (Β§6a).
6. Line 1915: bob gate (Β§8).
7. Line 2051: field-biased `o_vacc_ini` (Β§6b).
8. Lines 2079, 2088: `o_ivsize` β `o_ivsize_eff`.
9. Lines 2147-2180: decim `o_adrs_pre` + fload addresses (Β§6c).
10. OSWEEP 2772-2779: `o_vtotal_eff` + `o_fld` toggle (Β§3).
11. OSWEEP 2791-2793: dual vsync window (Β§4).
12. OSWEEP 2826: VRR guard (Β§9).
13. VSCAL 2898, 2904: `o_ivsize` β `o_ivsize_eff`.
14. `sys_top.v` (per repo): `ilace_out` reg + 'h20 decode bit 13 + `.mode` concat (Β§1).
## 13. Simulation-free sanity checklist (point 10)
1. **Progressive regression**: with `mode(4)=0`, every new mux collapses to the stock expression (`o_fld` forced 0; `o_ivdouble=0` β `o_ivsize_eff=o_ivsize`; vacc_ini else-branch = original line 2051). Diff-inspect each edit for this property.
2. **Line budget**: field lengths = vtotal / vtotal+1 (o_fld=1 long); sum = 2Β·vtotal+1 = 525 (NTSC), 625 (PAL).
3. **VS edges**: exactly one VS pulse per field, both (vsendβvsstart) lines long; leading-edge spacing = vtotal+0.5 lines in *both* directions (pixel arithmetic in Β§4).
4. **Pairing invariant**: (vtotal_eff β vsync_start_position) = 18.0 (short) vs 18.5 (long) β differs by exactly 0.5, matching the Minimig invariant (310 vs 309.5).
5. **Content mapping**: bias field = `o_fld='0'` (follows the long field's delayed vsync β lower raster β +Β½ step / odd lines). Nearest, 1:1: field fld=1 shows source lines k, field fld=0 shows k+0.5 (rounds to k+1-ish). Decim: fld=1 shows 0,2,β¦,478; fld=0 shows 1,3,β¦,479.
6. **First/last line**: bias-field init `o_vacc = o_vsize < 2Β·o_vsize` (frac=0.5, divider in range); `o_vacpt` peaks at β€ ivsize_eff+1, covered by the 2898/2904 clamps.
7. **Modeline contract (document; optionally VHDL asserts)**: `vsend+2 β€ vtotalβ3` (o_fld stable across VS and wrap; NTSC 250β€259 β, PAL 300β€309 β); `vdisp β€ vtotalβ4`; htotal even; vtotal β€ 4094; `o_ivsize β€ o_vsize` or `= 2Β·o_vsize` (even); vrr=0; vsync_adjust off.
8. **Bandwidth**: decim and bias paths both issue exactly one line-fetch and β€ ihsize pixel-copies per hsync β identical to stock load.
9. **Buffer swap**: VS falling edge exists in both fields (mid-line edge included) β `buf_next` once per field.
10. **grep checks**: no remaining `2*o_ivsize` in sHSYNC; `o_vtotal` appears in the wrap comparison only via `o_vtotal_eff`; `o_fld` read nowhere before its declaration-init.
### Critical Files for Implementation
- /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd (all RTL edits; lines 92, 462-476, 1824, 1906-1988, 2039-2110, 2147-2180, 2772-2800, 2826, 2898-2916)
- /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/sys_top.v (mode wiring line 791, 'h20 decode line 423, csync/OSD/HDMI-mux downstream audit)
- /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp (tvmodes at 142-148 β modeline contract the RTL assumes)
- /home/izzie/Desktop/mister-scaler/PSX_MiSTer/sys/ascal.vhd (replication target with CRLF; ao486/Minimig copies are byte-identical to Menu)
- /home/izzie/Desktop/mister-scaler/Minimig-AGA_MiSTer/rtl/agnus_beamcounter.v (reference for the field/vsync pairing invariant, lines 316-383)
2030 ------------------------------------------------------
2031 lev_inc_v:='0';
2032 lev_dec_v:='0';
2033
2034 -- acpt : Pixel position within current data word
2035 -- dcpt : Destination image position
2036
2037 -- Force preload 2 lines at top of screen
2038 IF o_hsv(0)='1' AND o_hsv(1)='0' THEN
2039 IF o_vcpt_pre3=o_vmin THEN
2040 o_fload<=2;
2041 o_bibu<='0';
2042 END IF;
2043 o_hsp<='1';
2044 END IF;
2045
2046 o_vpe<=to_std_logic(o_vcpt_pre<o_vmax AND o_vcpt_pre>=o_vmin);
2047 o_divstart<='0';
2048 o_adrsa<='0';
2049 o_adrsb<=o_adrsa;
2050
2051 o_vacc_ini<=(o_vsize - o_ivsize + 8192) MOD 8192;
2052 o_hacc_ini<=(o_hsize + o_ihsize + 8192) MOD 8192;
2053
2054 --Alternate phase
2055 --o_vacc_ini<=o_ivsize;
2056 --o_hacc_ini<=(2*o_hsize - o_ihsize + 8192) MOD 8192;
2057
2058 CASE o_state IS
2059 --------------------------------------------------
2060 WHEN sDISP =>
2061 IF o_hsp='1' THEN
2062 o_state<=sHSYNC;
2063 o_hsp<='0';
2064 END IF;
2065 o_prim<=true;
2066 o_vcarrym<=false;
2067
2068 --------------------------------------------------
2069 WHEN sHSYNC =>
2070 dif_v :=(o_vacc_next - 2*o_vsize + 16384) MOD 16384;
2071 IF o_prim THEN
2072 IF dif_v>=8192 THEN
2073 o_vacc <=o_vacc_next;
2074 ELSE
2075 o_vacc <=dif_v;
2076 END IF;
2077 END IF;
2078 IF dif_v>=8192 THEN
2079 o_vacc_next<=(o_vacc_next + 2*o_ivsize) MOD 8192;
2080 vcarry_v:=false;
2081 ELSE
2082 o_vacc_next<=dif_v;
2083 vcarry_v:=true;
2084 END IF;
2085
2086 IF o_vcpt_pre2=o_vmin THEN
2087 o_vacc <=o_vacc_ini;
2088 o_vacc_next<=o_vacc_ini + 2*o_ivsize;
2089 o_vacpt <=x"001";
2090 o_vacptl<="01";
2091 vcarry_v:=false;
2092 END IF;
2093
2094 IF vcarry_v THEN
2095 o_vacpt<=o_vacpt+1;
2096 END IF;
2097 IF vcarry_v AND o_prim THEN
2098 o_vacptl<=o_vacptl+1;
2099 END IF;
2100 o_vcarrym <= o_vcarrym OR vcarry_v;
2101 o_prim <= false;
2102 o_hbcpt<=0; -- Clear burst counter on line
2103 o_divstart<=to_std_logic(NOT vcarry_v);
2104 IF NOT vcarry_v OR o_fload>0 THEN
2105 IF (o_vpe='1' AND o_vcarrym) OR o_fload>0 THEN
2106 o_state<=sREAD;
2107 ELSE
2108 o_state<=sDISP;
2109 END IF;
2110 END IF;
2111
2112 WHEN sREAD =>
2113 -- Read a block
2114 IF o_readlev<2 AND o_adrsb='1' THEN
2115 lev_inc_v:='1';
2116 o_read_pre<=NOT o_read_pre;
2117 o_state <=sWAITREAD;
2118 o_bibu<=NOT o_bibu;
2119 END IF;
2120 prim_v:=to_std_logic(o_hbcpt=0);
2121 last_v:=to_std_logic(o_hbcpt=o_hburst-1);
2122 bib_v :=o_bibu;
2123 off_v :=pixoffset(o_adrs + o_fb_base(NB_LA-1 DOWNTO 0),o_fb_format);
2124 IF o_fb_ena='0' THEN
2125 off_v:=0;
2126 END IF;
2127 o_adrsa<='1';
2128
2129 WHEN sWAITREAD =>
2130 IF o_readack='1' THEN
2131 o_hbcpt<=o_hbcpt+1;
2132 IF o_hbcpt<o_hburst-1 THEN
2133 -- If not finshed line, read more
2134 o_state<=sREAD;
2135 ELSE
2136 o_state<=sDISP;
2137 IF o_fload>=1 THEN
2138 o_fload<=o_fload-1;
2139 END IF;
2140 END IF;
2141 END IF;
2142
2143 --------------------------------------------------
2144 END CASE;
2145
2146 o_read<=o_read_pre AND o_run;
2147 o_rline<=o_vacpt(0); -- Even/Odd line for interlaced video
2148
2149 ----
2150 -- When bob deinterlacing we read lines from one buffer (the most current) but we read them twice
2151 -- (in contrast to weave deinterlacing where we read each 480p line from alternating buffers)
2152 -- To counteract the severe vibrating/motion with bob deinterlacing, we need to offset one field
2153 -- by a half-line. This is done by only reading the first line of the 'even' frame once
2154
2155 IF o_inter='1' AND o_bob_deint='1' THEN
2156 IF o_iwfl(o_obuf0)='0' THEN
2157 IF o_vacpt=0 OR o_rline='1' THEN
2158 o_adrs_pre <= to_integer(o_vacpt) * to_integer(o_stride);
2159 ELSE
2160 o_adrs_pre <= (to_integer(o_vacpt)-1) * to_integer(o_stride);
2161 END IF;
2162 ELSE
2163 o_adrs_pre <= to_integer(o_vacpt(11 DOWNTO 1) & "0") * to_integer(o_stride);
2164 END IF;
2165 ELSE
2166 o_adrs_pre<=to_integer(o_vacpt) * to_integer(o_stride);
2167 END IF;
2168
2169 IF o_adrsa='1' THEN
2170 IF o_fload=2 THEN
2171 o_adrs<=to_unsigned(o_hbcpt * N_BURST,32);
2172 o_alt<="1111";
2173 ELSIF o_fload=1 THEN
2174 o_adrs<=to_unsigned(o_hbcpt * N_BURST,32) + o_stride;
2175 o_alt<="0100";
2176 ELSE
2177 o_adrs<=to_unsigned(o_adrs_pre + (o_hbcpt * N_BURST),32);
2178 o_alt<=altx(o_vacptl + 1);
2179 END IF;
2180 END IF;
2181
2182 ------------------------------------------------------
2183 -- Copy from buffered memory to pixel lines
2184 o_sh<='0';
2185 o_dcpt_clr <= '0';
2186 o_dcpt_inc <= '0';
2187
2188 CASE o_copy IS
2189 WHEN sWAIT =>
2755 -----------------------------------------------------------------------------
2756 -- Output video sweep
2757 OSWEEP:PROCESS(o_clk) IS
2758 BEGIN
2759 IF rising_edge(o_clk) THEN
2760
2761 IF o_ce='1' THEN
2762 -- Output pixels count
2763 IF o_hcpt+1<o_htotal THEN
2764 o_hcpt<=(o_hcpt+1) MOD 4096;
2765 ELSE
2766 o_hcpt<=0;
2767
2768 IF o_vcpt_sync /= 4095 THEN
2769 o_vcpt_sync <= o_vcpt_sync+1;
2770 END IF;
2771
2772 IF o_vcpt_pre3+1>=o_vtotal THEN
2773 o_vcpt_pre3<=0;
2774 ELSIF o_vrr_sync2 THEN
2775 o_vcpt_pre3<=o_vsstart;
2776 o_sync<=false;
2777 ELSE
2778 o_vcpt_pre3<=(o_vcpt_pre3+1) MOD 4096;
2779 END IF;
2780
2781 o_vcpt_pre2<=o_vcpt_pre3;
2782 o_vcpt_pre<=o_vcpt_pre2;
2783 o_vcpt<=o_vcpt_pre;
2784 END IF;
2785
2786 o_end(0)<=to_std_logic(o_vcpt>=o_vdisp);
2787 o_dev(0)<=to_std_logic(o_hcpt<o_hdisp AND o_vcpt<o_vdisp);
2788 o_pev(0)<=to_std_logic(o_hcpt>=o_hmin AND o_hcpt<=o_hmax AND
2789 o_vcpt>=o_vmin AND o_vcpt<=o_vmax);
2790 o_hsv(0)<=to_std_logic(o_hcpt>=o_hsstart AND o_hcpt<o_hsend);
2791 o_vsv(0)<=to_std_logic((o_vcpt=o_vsstart AND o_hcpt>=o_hsstart) OR
2792 (o_vcpt>o_vsstart AND o_vcpt<o_vsend) OR
2793 (o_vcpt=o_vsend AND o_hcpt<o_hsstart));
2794
2795 o_vss<=to_std_logic(o_vcpt_pre2>=o_vmin AND o_vcpt_pre2<=o_vmax);
2796 o_hsv(1 TO 11)<=o_hsv(0 TO 10);
2797 o_vsv(1 TO 11)<=o_vsv(0 TO 10);
2798 o_dev(1 TO 11)<=o_dev(0 TO 10);
2799 o_pev(1 TO 11)<=o_pev(0 TO 10);
2800 o_end(1 TO 11)<=o_end(0 TO 10);
2801
2802 IF o_run='0' THEN
2803 o_hsv(2)<='0';
2804 o_vsv(2)<='0';
2805 o_dev(2)<='0';
2806 o_pev(2)<='0';
2807 o_end(2)<='0';
2808 END IF;
2809 END IF;
2810
2811 o_vcpt_sync2<=o_vcpt_sync;
2812 o_vrr_min<=(o_vcpt_sync2<o_vtotal);
2813 o_vrr_min2<=o_vrr_min;
2814 o_vrr_max<=(o_vcpt_sync2<o_vrrmax);
2815 o_vrr_max2<=o_vrr_max;
2816
2817 IF o_isync2='1' THEN
2818 o_vcpt_sync<=0;
2819 o_sync_max<=o_vrr_max2;
2820 IF o_vrr_min2 THEN
2821 o_sync<=true;
2822 END iF;
2823 END IF;
2824
2825 o_vcpt2<=o_vcpt_pre3;
2826 o_vrr_sync<=(o_vrr='1' AND (o_sync OR o_sync_max) AND o_vcpt2>=o_vdisp AND o_vcpt2<o_vsstart);
2827 o_vrr_sync2<=o_vrr_sync;
2828
2829 END IF;
2830 END PROCESS OSWEEP;
2831
2832 -----------------------------------------------------------------------------
2833 -- Vertical Scaler
2834 VSCAL:PROCESS(o_clk) IS
# True 480i/576i ASCAL scaler output over direct_video
## Context
The MiSTer board is used with `direct_video=1` (realtime analog video via HDMI DAC). Cores with native interlaced output (Minimig) produce true 480i on a CRT, but cores that only output 480p (ao486) cannot. Goal: patch the ASCAL sca
... [13909 chars total, truncated]
π¦ Tool Result
2026-07-27 03:34:43
β Result (Write):
File created successfully at: /home/izzie/.claude/plans/sorted-giggling-pearl.md (file state is current in your context β no need to Read it back)
True 480i/576i ASCAL scaler output over direct_video
Context
The MiSTer board is used with direct_video=1 (realtime analog video via HDMI DAC). Cores with native interlaced output (Minimig) produce true 480i on a CRT, but cores that only output 480p (ao486) cannot. Goal: patch the ASCAL scaler so it can render any core's video (and the Linux/menu framebuffer) as true interlaced 480i/576i β alternating 262/263-line (312/313 PAL) fields with a half-line vsync offset β delivered over the direct_video HDMI pins, giving ~480 visible NTSC lines.
Two features, per user decisions:
vga_scaler=1 + direct_video=1 together routes the ASCAL output to the HDMI/DV pins for game cores (today only cores hardwiring VGA_SCALER=1, like ao486, get this). Works with unmodified core bitstreams β it's pure Linux-side routing via the existing CONF_VGA_FB mux (production-tested path, sys_top.v:1317-1320).
New ini key DIRECT_VIDEO_INTERLACE=1: the DV scaler mode becomes true 480i (576i when MENU_PAL=1). Requires rebuilt core bitstreams (ascal.vhd + sys_top.v change); Main detects support via a capability bit and silently falls back otherwise. Menu/Linux fb renders at ~480 lines too. v1 is free-running (vsync_adjust stays disabled under direct_video; no genlock/lltune changes).
Key insight: the existing 15 kHz TV mode (tvmodes[0], 640Γ240, vtotal 262 @12.587 MHz, video.cpp:142-148) is already the correct field modeline for 480i β 240p and 480i share identical line timing. ASCAL only needs to alternate field length, delay vsync by Β½ line on the long field, and offset vertical sampling per field.
All 4 repos share byte-identical sys/ascal.vhd + sys/sys_top.v (PSX: CRLF line endings, sys_top β β15 line offset). Edit Menu_MiSTer first, replicate to ao486/Minimig verbatim, PSX with CRLF preserved.
Enable bit: reuse mode(4) ("TBD" per header line ~89; port already 5 bits at :206; verified only bits 3 and 2:0 are read anywhere β o_hmode/o_vmode copy all 5 bits but only (2:0) are consumed, so it rides through harmlessly). Already latched into both clock domains: o_mode<=mode (:1883), i_mode<=mode (:1305). Alias o_ilace <= o_mode(4). Update header docs.
New signals:o_ilace, o_fld (master field reg, '1' = long field, init '0'), o_fld_dsp (display-aligned copy), o_vtotal_eff, o_hs2p/o_hs2/o_vs2start/o_vs2end (half-line VS precompute), o_ivsize_eff, o_ivdouble (weave/decimation mode flag).
A1. Field counter + per-field vtotal (OSWEEP, :2755-2830)
Register o_vtotal_eff <= o_vtotal + 1 when o_ilace='1' and o_fld='1' else o_vtotal (the :1895 input latch is untouched; only the wrap compare changes).
In the wrap branch (:2772): compare against o_vtotal_eff; on wrap toggle o_fld (force '0' when o_ilace='0').
o_fld toggles when o_vcpt_pre3 wraps (3 lines before display wrap). Shift a display-aligned copy alongside the existing pre2/pre/vcpt shifts (:2781-2783): o_fld_dsp follows 3 lines later. o_fld (pre3-aligned) drives vtotal_eff, sampling phase, decim addressing, preload β everything on the fetch side; o_fld_dsp drives only the vsync window. This removes any constraint between vsend and vtotal: with the DV porch rewrite the NTSC field modeline becomes vact=254, vsstart=256, vsend=260, vtotal=262 β vsync runs almost to the wrap, so a single field register toggling at pre3-wrap would flip mid-vsync-pulse and glitch VS. The two-register split fixes this by construction (o_fld_dsp toggles at display line 0, VS window is lines 256..261).
A2. Half-line vsync (:2791-2793 + precompute near :1906)
Precompute (static, from latched modeline): o_hs2p <= o_hsstart + o_htotal/2; if >= o_htotal then o_hs2 <= o_hs2p - o_htotal, o_vs2start/o_vs2end <= o_vsstart/o_vsend + 1, else no wrap.
Replace o_vsv(0):
IF o_ilace='1'AND o_fld_dsp='1'THEN-- long field: VS delayed by htotal/2
o_vsv(0)<=to_std_logic((o_vcpt=o_vs2start AND o_hcpt>=o_hs2) OR
(o_vcpt>o_vs2start AND o_vcpt<o_vs2end) OR
(o_vcpt=o_vs2end AND o_hcpt<o_hs2));
ELSE-- existing expression unchangedENDIF;
Field pairing invariant (verified arithmetically): long field = vtotal+1 lines with VS delayed +Β½ line; short field = vtotal lines, VS aligned. Both VS-to-VS spacings = vtotal+0.5 lines (NTSC: 262.5 β 525-line frame, 59.94 Hz fields). Same invariant as Minimig (agnus_beamcounter.v:376-383).
Hsync/DE/border need no change: hsync depends only on o_hcpt (uniform train, required for analog interlace); the long field's extra line has o_vcpt = vtotal β₯ vdisp β DE/picture low β one extra blank line. The 12-stage o_vsv pipeline and downstream consumers (buffer swap on VS falling edge :1934-1965, fb_base latch) handle the mid-line edge fine β one event per field.
A3. Vertical sampling β two sub-modes
Output fetch bandwidth allows β€ ~1.25 source-line fetches per output line (800-clock line vs 640-pixel copies), so:
Bias mode (o_ivsize β€ o_vsize): pure resample-phase shift. Edit o_vacc_ini (:2051): when o_ilace='1' AND o_ivdouble='0' AND o_fld='0' β o_vacc_ini <= o_vsize (= stock value + o_ivsize_eff, i.e. +Β½ output step = the correct field offset; always < 2Β·o_vsize so the divider/frame-init at :2086-2092 need no change; the un-biased field is bit-identical to stock). Works with all interpolation modes. The biased field is o_fld='0' because a field's raster position is set by the preceding field's vsync (delayed VS β next field drawn Β½ line lower β must sample Β½ step later).
Decimation/weave mode (o_ivdouble='1', active when o_ilace='1' AND o_ivsize > o_vsize; Linux guarantees o_ivsize = 2Β·o_vsize then): Bresenham runs 1:1 via o_ivsize_eff <= o_ivsize/2; addressing reads physical line 2n + f where f = NOT o_fld:
:2166 o_adrs_pre <= to_integer(o_vacpt(10 DOWNTO 0) & (NOT o_fld)) * to_integer(o_stride); (new ELSIF branch between the bob branch and the default).
Preload block :2170-2175: fload=2 β physical line f (+ o_stride when o_fld='0'); fload=1 β physical line 2+f (+ 2Β·o_stride, + 3Β·o_stride when o_fld='0'). o_alt masks unchanged.
o_rline (:2147): o_rline <= NOT o_fld when o_ivdouble='1' else o_vacpt(0) β in weave mode the physical line parity is constant per field, so interlaced-input (two field buffers) composes correctly: each output field reads entirely from the matching input field buffer. Gives true 480i passthrough for interlaced-input cores too (field phase unsynced in v1 β acceptable).
Replace o_ivsize with o_ivsize_eff at exactly: :2079, :2088 (accumulator step) and :2898, :2904 (VSCAL bottom clamp). Nowhere else.
Register o_ivdouble/o_ivsize_eff near the :1982-1988 fb override (after it, so o_fb_vsize is respected).
A4. Input-side: don't pre-downscale the frame (critical, was missed by initial design)
i_vdown <= (i_vsize > i_ovsize AND DOWNSCALE) with i_ovsize <= o_vsize (:1311-1333) and DOWNSCALE defaults true (generic :121, not overridden in sys_top) β so a 480-line core image would be blurred to ~254 lines before storage, destroying the interlace detail. Fix at :1333:
IF i_mode(4)='1'THEN i_ovsize<=2*o_vsize; ELSE i_ovsize<=o_vsize; ENDIF; -- <ASYNC>
Effects: 480-line input with o_vsize=240 β stored full (weave); >2Β·o_vsize inputs downscale to exactly 2Β·o_vsize β weave; β€ o_vsize inputs unaffected (bias). Also makes 480i inputs take the full-weave storage path (:1322 comparison) instead of half-frames. i_mode(4) is already available in the i domain.
A5. Guards
Bob-deint: o_bob_deint <= bob_deint AND NOT o_ilace; (:1915) β bob's half-line hack + output fields is nonsense; weave fallback.
VRR: add o_ilace='0' to o_vrr_sync (:2826) so a misconfigured VRR can never jump the line counter mid-field.
o_ilace='0' β every new mux collapses to the stock expression (bit-exact progressive regression).
A6. Sanity checklist (inspection, no sim)
Field lengths 262+263=525 (312+313=625); one VS per field, equal spacing = vtotal+0.5 both directions; bias field is the one following the delayed VS; decim: fld=1 shows physical lines 0,2,β¦, fld=0 shows 1,3,β¦; preload lines match first two fetched lines per field; no remaining bare o_ivsize in the vertical Bresenham; o_fld_dsp stable across the whole VS window.
Part B β sys_top.v (4 small edits Γ4 repos; PSX β β15 lines)
Nothing else: HDMI/VGA muxes, aspect window, csync module, OSD all verified compatible (csync serration window never overlaps the mid-line VS edge; OSD is DE-driven and already interlace-aware).
Capability β video.cpp next to supports_pr() (:201): static bool supports_ilace() = spi_uio_cmd(UIO_SET_VIDEO) & 4 (cache like the others; note existing helpers cache the masked value β replicate the pattern correctly with a shared cached word). Export int video_dv_scaler_forced() in video.h: cfg.direct_video && cfg.vga_scaler (routing does not require ilace support β works with unmodified cores).
Routing (feature 1) β user_io.cpp:3028 in user_io_send_buttons(): if (vga_fb || video_dv_scaler_forced()) map |= CONF_VGA_FB;. Plain OR with the existing set_vga_fb OSD toggling and ao486's hardwired VGA_SCALER=1; no interactions.
15 kHz tvmodes[0]/[2] + ilace β 480i/576i. Re-entry via cfg_has_video_sections() re-derives the flag; VRR unreachable under DV.
SET_VIDEO transmit β video.cpp word loop (:2248-2257): else if (i == 5) spi_w(((v_cur.param.ilace ? 1 : 0) << 15) | v_fix.item[i]);. DV porch rewrite (:2186-2197), PLL (12.587 MHz), and hdmi_config_set_mode() all unchanged (pr=0; ADV7513 below-min-clock status quo of DV today).
Weave window via VSET (critical) β with DV porches the field raster is 254 active lines, so a 480-line source is not 2:1. In video_scaling_adjust() (:2980, already called unconditionally on every res change, :3316), add a branch before the existing logic, active when direct_video + ilace mode is live:
uint32_tsrc_h=height << (vi->interlaced ? 1 : 0); // frame heightif (src_h>scrh) spi_uio_cmd16(UIO_SETHEIGHT, src_h >> 1); // exact 2:1 weave windowelse/* fall through to existing VSET logic (bias mode handles any iv<=vs) */
src_h>>1 handles odd heights (1-line input downscale, still weave); src_h > 2*scrh also resolves correctly (input downscales to 2Β·o_vsize β weave). This yields true weave for 480-line (window 240), 400-line DOS text (window 200, letterboxed), and interlaced inputs.
Menu fb at 480i β video_fb_config() (:3419): with il = v_cur.param.ilace: size guard uses item[5] << il, and fb_height = (v_cur.item[5] << il) / fb_scale_y; (do not touch the pr term). video_fb_enable()/fb_write_module_params() unchanged (window regs stay in field coordinates; source height doubles β ascal sees fb 2Β·o_vsize β weave). fb_cmd1/fb_cmd2 (:4063-4099): clamp against item[5]<<il, gate the item[5]<300 240p heuristic with && !il, halve window heights accordingly. Menu logo halving (:3929): add && !v_cur.param.ilace.
Cosmetic/deferred β filter-menu un-greying under DV-scaler mode, "640x240i" info string, VIC 6/7 infoframe: explicitly out of scope for v1.
Execution order
ascal.vhd in Menu_MiSTer (Part A) β copy to ao486/Minimig; PSX via unix2dos-style CRLF-preserving copy.
sys_top.v Γ4 (Part B; PSX offsets).
Main_MiSTer (Part C).
Verification
Static RTL check: if ghdl is available, ghdl -s --std=08 (or -a) on ascal.vhd for syntax; otherwise careful diff review against the sanity checklist in A6. Verify with grep that no bare o_ivsize remains in the vertical Bresenham and that mode concat is 5 bits.
Main_MiSTer build:make in Main_MiSTer (needs the ARM cross-toolchain; if absent, at minimum g++ -fsyntax-only the touched translation units with the repo's include paths).
Progressive regression by construction: with mode(4)=0 every new mux reduces to the stock expression β inspect each edit for this property.
Hardware (user): rebuild Menu + ao486 (+ optionally Minimig) in Quartus; deploy new MiSTer binary. Test matrix: (a) key off β identical behavior to today; (b) menu core NTSC + key β CRT shows 480i menu (line-alternating fields, sharp text); (c) ao486 640Γ480 + vga_scaler=1 + key β true 480i DOS/Windows; (d) 720Γ400 text mode β 400-line weave letterboxed; (e) PAL menu β 576i; (f) OSD open/close under (c) β no mode glitches; (g) old (un-rebuilt) core + key β clean fallback to current 240p DV behavior.
440 IF b=1 THEN RETURN base+size; END IF;
441 IF b=2 THEN RETURN base+(size(30 DOWNTO 0) & '0'); END IF;
442 RETURN base;
443 END FUNCTION;
444
445 ----------------------------------------------------------
446 -- Output
447 SIGNAL o_run : std_logic;
448 SIGNAL o_freeze : std_logic;
449 SIGNAL o_bob_deint : std_logic;
450 SIGNAL o_iwfl : std_logic_vector(2 DOWNTO 0);
451 SIGNAL o_mode,o_hmode,o_vmode : unsigned(4 DOWNTO 0);
452 SIGNAL o_format : unsigned(5 DOWNTO 0);
453 SIGNAL o_fb_pal_dr : unsigned(23 DOWNTO 0);
454 SIGNAL o_fb_pal_dr2 : unsigned(23 DOWNTO 0);
455 SIGNAL o_fb_pal_dr_x2 : unsigned(47 DOWNTO 0);
456 SIGNAL pal_idx: unsigned(7 DOWNTO 0);
457 SIGNAL pal_idx_lsb: std_logic;
458 SIGNAL pal1_mem : arr_uv48(0 TO 127);
459 SIGNAL pal2_mem : arr_uv24(0 TO 255);
460 ATTRIBUTE ramstyle of pal1_mem : signal is "no_rw_check";
461 ATTRIBUTE ramstyle of pal2_mem : signal is "no_rw_check";
462 SIGNAL o_htotal,o_hsstart,o_hsend : uint12;
463 SIGNAL o_hmin,o_hmax,o_hdisp,o_v_hmin_adj : uint12;
464 SIGNAL o_hsize,o_vsize : uint12;
465 SIGNAL o_vtotal,o_vsstart,o_vsend : uint12;
466 SIGNAL o_vrr,o_isync,o_isync2 : std_logic;
467 SIGNAL o_vrr_sync,o_vrr_sync2 : boolean;
468 SIGNAL o_vrr_min,o_vrr_min2 : boolean;
469 SIGNAL o_vrr_max,o_vrr_max2 : boolean;
470 SIGNAL o_vcpt_sync,o_vcpt_sync2, o_vrrmax : uint12;
471 SIGNAL o_sync, o_sync_max : boolean;
472 SIGNAL o_vmin,o_vmax,o_vdisp : uint12;
473 SIGNAL o_divcpt : natural RANGE 0 TO 36;
474 SIGNAL o_iendframe0,o_iendframe02,o_iendframe1,o_iendframe12 : std_logic;
475 SIGNAL o_bufup0,o_bufup1,o_inter : std_logic;
476 SIGNAL o_ibuf0,o_ibuf1,o_obuf0,o_obuf1 : natural RANGE 0 TO 2;
477 TYPE enum_o_state IS (sDISP,sHSYNC,sREAD,sWAITREAD);
478 SIGNAL o_state : enum_o_state;
479 TYPE enum_o_copy IS (sWAIT,sSHIFT,sCOPY);
480 SIGNAL o_copy : enum_o_copy;
481 SIGNAL o_pshift : natural RANGE 0 TO 15;
482 SIGNAL o_readack,o_readack_sync,o_readack_sync2 : std_logic;
483 SIGNAL o_readdataack,o_readdataack_sync,o_readdataack_sync2 : std_logic;
484 SIGNAL o_copyv : unsigned(0 TO 14);
485 SIGNAL o_adrs : unsigned(31 DOWNTO 0); -- Avalon address
486 SIGNAL o_adrs_pre : natural RANGE 0 TO 2**24-1;
487 SIGNAL o_stride : unsigned(13 DOWNTO 0);
488 SIGNAL o_adrsa,o_adrsb,o_rline : std_logic;
489 SIGNAL o_ad,o_ad1,o_ad2,o_ad3 : natural RANGE 0 TO 2*BLEN-1;
490 SIGNAL o_adturn : std_logic;
491 SIGNAL o_dr : unsigned(N_DW-1 DOWNTO 0);
492 SIGNAL o_shift : unsigned(0 TO N_DW+15);
493 SIGNAL o_sh,o_sh1,o_sh2,o_sh3,o_sh4 : std_logic;
494 SIGNAL o_reset_na : std_logic;
495 SIGNAL o_dpram : arr_dw(0 TO BLEN*2-1);
496 ATTRIBUTE ramstyle OF o_dpram : SIGNAL IS "no_rw_check";
497 SIGNAL o_line0,o_line1,o_line2,o_line3 : arr_pix(0 TO OHRESL-1);
498 SIGNAL o_linf0,o_linf1,o_linf2,o_linf3 : arr_pix(0 TO OHRESM-1);
499
500 ATTRIBUTE ramstyle OF o_line0 : SIGNAL IS "no_rw_check";
501 ATTRIBUTE ramstyle OF o_line1 : SIGNAL IS "no_rw_check";
502 ATTRIBUTE ramstyle OF o_line2 : SIGNAL IS "no_rw_check";
503 ATTRIBUTE ramstyle OF o_line3 : SIGNAL IS "no_rw_check";
504 ATTRIBUTE ramstyle OF o_linf0 : SIGNAL IS "no_rw_check";
505 ATTRIBUTE ramstyle OF o_linf1 : SIGNAL IS "no_rw_check";
506 ATTRIBUTE ramstyle OF o_linf2 : SIGNAL IS "no_rw_check";
507 ATTRIBUTE ramstyle OF o_linf3 : SIGNAL IS "no_rw_check";
508 SIGNAL o_wadl,o_radl0,o_radl1,o_radl2,o_radl3 : natural RANGE 0 TO OHRESH-1;
509 SIGNAL o_ldr0,o_ldr1,o_ldr2,o_ldr3,o_ldw : type_pix;
510 SIGNAL o_ler0,o_ler1,o_ler2,o_ler3 : type_pix;
511 SIGNAL o_lex0,o_lex1,o_lex2,o_lex3 : std_logic;
512 SIGNAL o_wr : unsigned(3 DOWNTO 0);
513 SIGNAL o_hcpt,o_vcpt,o_vcpt_pre,o_vcpt_pre2,o_vcpt_pre3,o_vcpt2 : uint12;
514 SIGNAL o_ihsize,o_ihsizem,o_ivsize : uint12;
515 SIGNAL o_ihsize_temp, o_ihsize_temp2 : natural RANGE 0 TO 32767;
516
517 SIGNAL o_vfrac : unsigned(11 DOWNTO 0);
518 SIGNAL o_hfrac : arr_frac(0 TO 9);
519 ATTRIBUTE ramstyle OF o_hfrac : SIGNAL IS "logic"; -- avoid blockram shift register
520
521 SIGNAL o_hacc,o_hacc_ini,o_hacc_next,o_vacc,o_vacc_next,o_vacc_ini : natural RANGE 0 TO 4*OHRESH-1;
522 SIGNAL o_hsv,o_vsv,o_dev,o_pev,o_end : unsigned(0 TO 11);
523 SIGNAL o_hsp,o_vss : std_logic;
524 SIGNAL o_vcarrym,o_prim : boolean;
525 SIGNAL o_read,o_read_pre : std_logic;
526 SIGNAL o_readlev,o_copylev : natural RANGE 0 TO 2;
527 SIGNAL o_hburst,o_hbcpt : natural RANGE 0 TO 31;
528 SIGNAL o_fload : natural RANGE 0 TO 3;
529 SIGNAL o_acpt,o_acpt1,o_acpt2,o_acpt3,o_acpt4 : natural RANGE 0 TO 15; -- Alternance pixels FIFO
530 SIGNAL o_dshi : natural RANGE 0 TO 3;
531 SIGNAL o_first,o_last,o_last1,o_last2 : std_logic;
532 SIGNAL o_lastt1,o_lastt2,o_lastt3,o_lastt4 : std_logic;
533 SIGNAL o_alt,o_altx : unsigned(3 DOWNTO 0);
534 SIGNAL o_hdown,o_vdown : std_logic;
535 SIGNAL o_primv,o_lastv,o_bibv : unsigned(0 TO 2);
536 TYPE arr_uint4 IS ARRAY (natural RANGE <>) OF natural RANGE 0 TO 15;
537 SIGNAL o_off : arr_uint4(0 TO 2);
538 SIGNAL o_bibu : std_logic :='0';
539 SIGNAL o_dcptv : arr_uint12(13 TO 14);
540 SIGNAL o_dcpt_clr, o_dcpt_inc : std_logic;
541 SIGNAL o_dcptv_clr, o_dcptv_inc : std_logic_vector(1 TO 12);
542 SIGNAL o_hpixs,o_hpix0,o_hpix1,o_hpix2,o_hpix3 : type_pix;
543 SIGNAL o_hpixq : arr_pixq(2 TO 8);
544 ATTRIBUTE ramstyle OF o_hpixq : SIGNAL IS "logic"; -- avoid blockram shift register
ugrep: warning: Menu_MiSTer/sys/ascal.vhd: No such file or directory
ugrep: warning: Menu_MiSTer/sys/ascal.vhd: No such file or directory
ugrep: warning: Menu_MiSTer/sys/ascal.vhd: No such file or directory
ugrep: warning: Menu_MiSTer/sys/ascal.vhd: No such file or directory
π¦ Tool Call
2026-07-27 03:40:44
π¦ Tool: BashDescription: Check uint12 subtype and all o_ivsize uses
339: SUBTYPE uint12 IS natural RANGE 0 TO 4095;
514: SIGNAL o_ihsize,o_ihsizem,o_ivsize : uint12;
1942: o_ivsize<=i_vrsize; -- <ASYNC>
1951: IF (swblack = '1' and o_fb_ena = '0' and (o_ihsize /= i_hrsize or o_ivsize /= i_vrsize)) then
1984: o_ivsize<=o_fb_vsize;
2051: o_vacc_ini<=(o_vsize - o_ivsize + 8192) MOD 8192;
2055: --o_vacc_ini<=o_ivsize;
2079: o_vacc_next<=(o_vacc_next + 2*o_ivsize) MOD 8192;
2088: o_vacc_next<=o_vacc_ini + 2*o_ivsize;
2898: IF to_integer(o_vacpt)>o_ivsize THEN
2904: ELSIF to_integer(o_vacpt)=o_ivsize THEN
558: SIGNAL o_vacptl : unsigned(1 DOWNTO 0);
sed -n '555,560p' Menu_MiSTer/sys/ascal.vhd && grep -n "o_vacpt<=\|o_vacpt <=" Menu_MiSTer/sys/ascal.vhd
π¦ Tool Result
2026-07-27 03:40:48
β Result (Bash):
SIGNAL o_divstart : std_logic;
SIGNAL o_divrun : std_logic;
SIGNAL o_hacpt,o_vacpt : unsigned(11 DOWNTO 0);
SIGNAL o_vacptl : unsigned(1 DOWNTO 0);
signal o_newres : integer range 0 to 3;
2089: o_vacpt <=x"001";
2095: o_vacpt<=o_vacpt+1;
π€ Claude
2026-07-27 03:40:52
All types confirmed. Making the ascal.vhd edits now:
-- MODE[4]
-- 0 : Progressive output
-- 1 : Interlaced output. Timing parameters describe one field (vtotal lines).
-- Fields alternate vtotal / vtotal+1 lines; VSync is delayed by htotal/2
--
... [410 chars total, truncated]
π¦ Tool Result
2026-07-27 03:40:55
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
SIGNAL o_vtotal,o_vsstart,o_vsend : uint12;
-- Interlaced output
SIGNAL o_ilace : std_logic;
SIGNAL o_fld,o_fld_pre2,o_fld_pre,o_fld_dsp : std_logic :='0';
SIGNAL o_vtotal_eff : natural RANGE 0 T
... [374 chars total, truncated]
π¦ Tool Result
2026-07-27 03:41:00
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
o_vrr <=vrr;
o_vrrmax <= vrrmax;
-- Interlaced output
o_ilace <=o_mode(4);
-- Precompute mid-line VSync position for the long field
o_hs2p <=o_hsstart + o_htotal/2;
IF o_hs
... [407 chars total, truncated]
π¦ Tool Result
2026-07-27 03:41:07
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
o_bob_deint <= bob_deint AND NOT o_ilace; -- Bob deint. pointless with interlaced output
π¦ Tool Result
2026-07-27 03:41:12
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
-- Framebuffer mode.
IF o_fb_ena='1' THEN
o_ihsize<=o_fb_hsize;
o_ivsize<=o_fb_vsize;
o_format<=o_fb_format;
o_hdown<='0';
o_vdown<='0';
END IF;
New:
-- Framebuffer mode.
IF o_fb_ena='1' THEN
o_ihsize<=o_fb_hsize;
o_ivsize<=o_fb_vsize;
o_format<=o_fb_format;
o_hdown<='0';
o_vdown<='0';
END IF;
-- Interlaced output :
... [477 chars total, truncated]
π¦ Tool Result
2026-07-27 03:41:18
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
π€ Claude
2026-07-27 03:41:21
Now the input-side fix (don't pre-downscale when interlaced output is on):
i_ohsize<=o_hsize; -- <ASYNC>
IF i_mode(4)='1' THEN
-- Interlaced output : keep (or downscale to) full frame height
i_ovsize<=2*o_vsize; -- <ASYNC>
ELSE
i_ovsize<=o_vsize; -
... [221 chars total, truncated]
π¦ Tool Result
2026-07-27 03:41:23
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
π¦ Tool Call
2026-07-27 03:41:29
π¦ Tool: BashDescription: Check i_ovsize declaration and uses
IF i_mode(4)='1' THEN
-- Interlaced output : keep (or downscale to) full frame height
i_ovsize<=2*o_vsize; -- <ASYNC>
ELSE
i_ovsize<=o_vsize; -- <ASYNC>
END IF;
New:
IF i_mode(4)='1' AND o_vsize<=2047 THEN
-- Interlaced output : keep (or downscale to) full frame height
i_ovsize<=2*o_vsize; -- <ASYNC>
ELSE
i_ovsize<=o_vsize; -- <ASYNC>
EN
... [205 chars total, truncated]
π¦ Tool Result
2026-07-27 03:41:39
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
π€ Claude
2026-07-27 03:41:42
Now the vertical Bresenham edits (o_vacc_ini, accumulator steps):
o_vacc_ini<=(o_vsize - o_ivsize + 8192) MOD 8192;
o_hacc_ini<=(o_hsize + o_ihsize + 8192) MOD 8192;
New:
IF o_ilace='1' AND o_ivdouble='0' AND o_fld='0' THEN
-- Interlaced output : this field samples half a step lower.
-- = (o_vsize - o_ivsize_eff) + o_ivsize_eff : always < 2*o_vsize
o_vac
... [345 chars total, truncated]
π¦ Tool Result
2026-07-27 03:41:45
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
IF dif_v>=8192 THEN
o_vacc_next<=(o_vacc_next + 2*o_ivsize) MOD 8192;
vcarry_v:=false;
New:
IF dif_v>=8192 THEN
o_vacc_next<=(o_vacc_next + 2*o_ivsize_eff) MOD 8192;
vcarry_v:=false;
π¦ Tool Result
2026-07-27 03:42:00
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
π€ Claude
2026-07-27 03:42:04
Now o_rline, the weave address, and the preload block:
o_read<=o_read_pre AND o_run;
o_rline<=o_vacpt(0); -- Even/Odd line for interlaced video
New:
o_read<=o_read_pre AND o_run;
IF o_ivdouble='1' THEN
-- Interlaced output weave : physical line parity is constant per field
o_rline<=NOT o_fld;
ELSE
o_rline<=o_vacpt(0); -- Even/
... [240 chars total, truncated]
π¦ Tool Result
2026-07-27 03:42:06
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
<tool_use_error>String to replace not found in file.
String: ELSE
o_adrs_pre <= (to_integer(o_vacpt(11 DOWNTO 1) & "0") * to_integer(o_stride));
END IF;
ELSE
o_adrs_pre<=to_integer(o_vacpt) * to_integer(o_stride);
END IF;</tool_use_error>
π¦ Tool Call
2026-07-27 03:42:13
π¦ Tool: BashDescription: View current bob/address block after edits shifted lines
sed -n '2200,2245p' Menu_MiSTer/sys/ascal.vhd
π¦ Tool Result
2026-07-27 03:42:13
β Result (Bash):
ELSE
o_rline<=o_vacpt(0); -- Even/Odd line for interlaced video
END IF;
----
-- When bob deinterlacing we read lines from one buffer (the most current) but we read them twice
-- (in contrast to weave deinterlacing where we read each 480p line from alternating buffers)
-- To counteract the severe vibrating/motion with bob deinterlacing, we need to offset one field
-- by a half-line. This is done by only reading the first line of the 'even' frame once
IF o_inter='1' AND o_bob_deint='1' THEN
IF o_iwfl(o_obuf0)='0' THEN
IF o_vacpt=0 OR o_rline='1' THEN
o_adrs_pre <= to_integer(o_vacpt) * to_integer(o_stride);
ELSE
o_adrs_pre <= (to_integer(o_vacpt)-1) * to_integer(o_stride);
END IF;
ELSE
o_adrs_pre <= to_integer(o_vacpt(11 DOWNTO 1) & "0") * to_integer(o_stride);
END IF;
ELSE
o_adrs_pre<=to_integer(o_vacpt) * to_integer(o_stride);
END IF;
IF o_adrsa='1' THEN
IF o_fload=2 THEN
o_adrs<=to_unsigned(o_hbcpt * N_BURST,32);
o_alt<="1111";
ELSIF o_fload=1 THEN
o_adrs<=to_unsigned(o_hbcpt * N_BURST,32) + o_stride;
o_alt<="0100";
ELSE
o_adrs<=to_unsigned(o_adrs_pre + (o_hbcpt * N_BURST),32);
o_alt<=altx(o_vacptl + 1);
END IF;
END IF;
------------------------------------------------------
-- Copy from buffered memory to pixel lines
o_sh<='0';
o_dcpt_clr <= '0';
o_dcpt_inc <= '0';
CASE o_copy IS
WHEN sWAIT =>
o_copyv(0)<='0';
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
IF o_ce='1' THEN
-- Output pixels count
IF o_hcpt+1<o_htotal THEN
o_hcpt<=(o_hcpt+1) MOD 4096;
ELSE
o_hcpt<=0;
IF o_vcpt_sync /= 4095 THEN
o_vcpt_sync <= o_vcpt_sy
... [524 chars total, truncated]
New:
IF o_ce='1' THEN
-- Interlaced output : long field is one line longer
IF o_ilace='1' AND o_fld='1' THEN
o_vtotal_eff<=o_vtotal + 1;
ELSE
o_vtotal_eff<=o_vtotal;
END IF;
... [963 chars total, truncated]
π¦ Tool Result
2026-07-27 03:42:47
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
o_hsv(0)<=to_std_logic(o_hcpt>=o_hsstart AND o_hcpt<o_hsend);
o_vsv(0)<=to_std_logic((o_vcpt=o_vsstart AND o_hcpt>=o_hsstart) OR
(o_vcpt>o_vsstart AND o_vcpt<o_vsend) OR
... [245 chars total, truncated]
New:
o_hsv(0)<=to_std_logic(o_hcpt>=o_hsstart AND o_hcpt<o_hsend);
IF o_ilace='1' AND o_fld_dsp='1' THEN
-- Interlaced output, long field : VSync delayed by htotal/2
o_vsv(0)<=to_std_logi
... [556 chars total, truncated]
π¦ Tool Result
2026-07-27 03:42:56
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
o_vrr_sync<=(o_vrr='1' AND (o_sync OR o_sync_max) AND o_vcpt2>=o_vdisp AND o_vcpt2<o_vsstart);
New:
o_vrr_sync<=(o_vrr='1' AND o_ilace='0' AND (o_sync OR o_sync_max) AND o_vcpt2>=o_vdisp AND o_vcpt2<o_vsstart);
π¦ Tool Result
2026-07-27 03:42:58
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd has been updated successfully. (file state is current in your context β no need to Read it back)
π¦ Tool Call
2026-07-27 03:43:12
π¦ Tool: BashDescription: Check for VHDL tools and remaining bare o_ivsize uses
/usr/bin/ghdl
---
98:-- (o_ivsize = 2 * o_vsize), lines are woven (read 2n+field) instead.
529: SIGNAL o_ihsize,o_ihsizem,o_ivsize : uint12;
1976: o_ivsize<=i_vrsize; -- <ASYNC>
1985: IF (swblack = '1' and o_fb_ena = '0' and (o_ihsize /= i_hrsize or o_ivsize /= i_vrsize)) then
2018: o_ivsize<=o_fb_vsize;
2025: -- both fields (o_ivsize = 2*o_vsize), else phase-bias mode.
2026: IF o_ilace='1' AND o_ivsize>o_vsize THEN
2105: --o_vacc_ini<=o_ivsize;
π¦ Tool Call
2026-07-27 03:43:19
π¦ Tool: BashDescription: Analyze edited ascal.vhd with ghdl
cd /tmp/claude-1000/-home-izzie-Desktop-mister-scaler/9e999fa4-192e-4345-9713-5069a3bbd7aa/scratchpad && ghdl -a --std=08 /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd 2>&1| head -30
π¦ Tool Result
2026-07-27 03:43:21
β Result (Bash):
/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd:435:55:warning: declaration of "freeze" hides port "freeze" [-Whide]
FUNCTION buf_next(a,b : natural RANGE 0 TO 2; freeze : std_logic := '0') RETURN natural IS
^
/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd:579:97:warning: declaration of "format" hides port "format" [-Whide]
format : unsigned(1 DOWNTO 0)) RETURN unsigned IS
^
/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd:593:30:warning: declaration of "i_dw" hides signal "i_dw" [-Whide]
FUNCTION shift_ipack(i_dw : unsigned(N_DW-1 DOWNTO 0);
^
/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd:597:30:warning: declaration of "format" hides port "format" [-Whide]
format : unsigned(1 DOWNTO 0)) RETURN unsigned IS
^
/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd:628:66:warning: declaration of "format" hides port "format" [-Whide]
format : unsigned(1 DOWNTO 0)) RETURN boolean IS
^
/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd:646:65:warning: declaration of "format" hides port "format" [-Whide]
format : unsigned(5 DOWNTO 0)) RETURN unsigned IS
^
/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd:697:66:warning: declaration of "format" hides port "format" [-Whide]
format : unsigned(5 DOWNTO 0)) RETURN boolean IS
^
/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd:716:90:warning: declaration of "format" hides port "format" [-Whide]
format : unsigned(5 DOWNTO 0)) RETURN type_pix IS
^
/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd:737:82:warning: declaration of "format" hides port "format" [-Whide]
format : unsigned (5 DOWNTO 0)) RETURN natural IS
^
/home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd:1131:26:warning: declaration of "frac" hides generic "FRAC" [-Whide]
FUNCTION poly_nn(frac : unsigned(FRAC-1 DOWNTO 0)) RETURN poly_phase_t IS
^
Shell cwd was reset to /home/izzie/Desktop/mister-scaler
π¦ Tool Call
2026-07-27 03:43:26
π¦ Tool: BashDescription: Filter ghdl output for real errors
cd /tmp/claude-1000/-home-izzie-Desktop-mister-scaler/9e999fa4-192e-4345-9713-5069a3bbd7aa/scratchpad && ghdl -a --std=08 /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd 2>&1| grep -v Whide | grep -v "^\s*\^"| grep -v "hides";echo"exit=$?"; ghdl -a --std=08 /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd 2>&1| grep -c "error"
π¦ Tool Result
2026-07-27 03:43:28
β Result (Bash):
FUNCTION buf_next(a,b : natural RANGE 0 TO 2; freeze : std_logic := '0') RETURN natural IS
format : unsigned(1 DOWNTO 0)) RETURN unsigned IS
FUNCTION shift_ipack(i_dw : unsigned(N_DW-1 DOWNTO 0);
format : unsigned(1 DOWNTO 0)) RETURN unsigned IS
format : unsigned(1 DOWNTO 0)) RETURN boolean IS
format : unsigned(5 DOWNTO 0)) RETURN unsigned IS
format : unsigned(5 DOWNTO 0)) RETURN boolean IS
format : unsigned(5 DOWNTO 0)) RETURN type_pix IS
format : unsigned (5 DOWNTO 0)) RETURN natural IS
FUNCTION poly_nn(frac : unsigned(FRAC-1 DOWNTO 0)) RETURN poly_phase_t IS
exit=0
0
π¦ Tool Call
2026-07-27 03:43:34
π¦ Tool: TaskUpdate
{
"taskId": "1",
"status": "completed"
}
π¦ Tool Result
2026-07-27 03:43:34
β Result (TaskUpdate):
Updated task #1 status
π¦ Tool Call
2026-07-27 03:43:35
π¦ Tool: TaskUpdate
{
"taskId": "2",
"status": "in_progress"
}
π¦ Tool Result
2026-07-27 03:43:35
β Result (TaskUpdate):
Updated task #2 status
π¦ Tool Call
2026-07-27 03:43:40
π¦ Tool: BashDescription: Replicate edited ascal.vhd to all repos (CRLF for PSX)
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/sys_top.v has been updated successfully. (file state is current in your context β no need to Read it back)
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/sys_top.v has been updated successfully. (file state is current in your context β no need to Read it back)
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/sys_top.v has been updated successfully. (file state is current in your context β no need to Read it back)
The file /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/sys_top.v has been updated successfully. (file state is current in your context β no need to Read it back)
π¦ Tool Call
2026-07-27 03:44:34
π¦ Tool: BashDescription: Copy edited sys_top.v to ao486 and Minimig
The file /home/izzie/Desktop/mister-scaler/PSX_MiSTer/sys/sys_top.v has been updated successfully. (file state is current in your context β no need to Read it back)
The file /home/izzie/Desktop/mister-scaler/PSX_MiSTer/sys/sys_top.v has been updated successfully. (file state is current in your context β no need to Read it back)
The file /home/izzie/Desktop/mister-scaler/PSX_MiSTer/sys/sys_top.v has been updated successfully. (file state is current in your context β no need to Read it back)
The file /home/izzie/Desktop/mister-scaler/PSX_MiSTer/sys/sys_top.v has been updated successfully. (file state is current in your context β no need to Read it back)
The file /home/izzie/Desktop/mister-scaler/Main_MiSTer/cfg.h has been updated successfully. (file state is current in your context β no need to Read it back)
π¦ Tool Call
2026-07-27 03:45:07
π¦ Tool: BashDescription: View cfg.cpp around DIRECT_VIDEO entry
The file /home/izzie/Desktop/mister-scaler/Main_MiSTer/cfg.cpp has been updated successfully. (file state is current in your context β no need to Read it back)
The file /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp has been updated successfully. (file state is current in your context β no need to Read it back)
The file /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp has been updated successfully. (file state is current in your context β no need to Read it back)
π¦ Tool Call
2026-07-27 03:45:33
π¦ Tool: BashDescription: Find declaration spot in video.h
grep -n "video_fb_state\|int video_" Main_MiSTer/video.h | head
64 int hasAPI1_5();
65
66 void video_fb_enable(int enable, int n = 0);
67 int video_fb_state();
68 void video_menu_bg(int n, int idle = 0);
69 int video_bg_has_picture();
70 int video_chvt(int num);
71 void video_cmd(char *cmd);
72 void video_mode_cmd(char *cmd);
73
void video_fb_enable(int enable, int n = 0);
int video_fb_state();
New:
void video_fb_enable(int enable, int n = 0);
int video_fb_state();
int video_dv_scaler_forced();
π¦ Tool Result
2026-07-27 03:45:40
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.h has been updated successfully. (file state is current in your context β no need to Read it back)
int mode = cfg.menu_pal ? 2 : 0;
if (cfg.forced_scandoubler) mode++;
memset(&v_def, 0, sizeof(v_def));
v_def.item[0] = mode;
for (int i = 0; i < 8; i++) v_def.item[i + 1] = tvmodes[mode].v
... [245 chars total, truncated]
New:
int ilace = (cfg.direct_video_interlace && supports_ilace()) ? 1 : 0;
int mode = cfg.menu_pal ? 2 : 0;
if (cfg.forced_scandoubler)
{
// interlaced output implies the 15KHz field timing, sca
... [527 chars total, truncated]
π¦ Tool Result
2026-07-27 03:45:51
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp has been updated successfully. (file state is current in your context β no need to Read it back)
if (i == 1) spi_w((v_cur.param.pr << 15) | ((use_vrr ? 1 : 0) << 14) | v_fix.item[i]);
//hsync polarity
else if (i == 3) spi_w((!!v_cur.param.hpol << 15) | v_fix.item[i]);
//vsync polarity
e
... [266 chars total, truncated]
New:
if (i == 1) spi_w((v_cur.param.pr << 15) | ((use_vrr ? 1 : 0) << 14) | v_fix.item[i]);
//hsync polarity
else if (i == 3) spi_w((!!v_cur.param.hpol << 15) | v_fix.item[i]);
//interlaced output
... [367 chars total, truncated]
π¦ Tool Result
2026-07-27 03:45:58
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp has been updated successfully. (file state is current in your context β no need to Read it back)
π€ Claude
2026-07-27 03:46:13
Now the VSET weave-window branch in video_scaling_adjust():
static void video_scaling_adjust(const VideoInfo *vi, const vmode_custom_t *vm)
{
if (cfg.direct_video && vm->param.ilace)
{
// Interlaced scaler output: when the source frame is taller than the
... [834 chars total, truncated]
π¦ Tool Result
2026-07-27 03:46:17
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp has been updated successfully. (file state is current in your context β no need to Read it back)
int fb_scale = cfg.fb_size;
if (fb_scale <= 1)
{
if (((v_cur.item[1] * v_cur.item[5]) > FB_SIZE))
fb_scale = 2;
else
fb_scale = 1;
}
else if (fb_scale == 3) fb_scale = 2;
else if (fb_
... [412 chars total, truncated]
New:
int fb_scale = cfg.fb_size;
// interlaced output: framebuffer holds both fields (double height)
const int fb_il = v_cur.param.ilace ? 1 : 0;
if (fb_scale <= 1)
{
if (((v_cur.item[1] * (v_cur.
... [550 chars total, truncated]
π¦ Tool Result
2026-07-27 03:46:30
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp has been updated successfully. (file state is current in your context β no need to Read it back)
π¦ Tool Call
2026-07-27 03:46:35
π¦ Tool: BashDescription: Locate 240p heuristics and fb_cmd functions
The file /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp has been updated successfully. (file state is current in your context β no need to Read it back)
int accept = 0;
int fmt = 0, rb = 0, div = -1, width = -1, height = -1;
uint16_t hmin, hmax, vmin, vmax;
if (sscanf(cmd, "fb_cmd0 %d %d %d", &fmt, &rb, &div) == 3)
{
if (div >= 1 && div <
... [382 chars total, truncated]
New:
int accept = 0;
int fmt = 0, rb = 0, div = -1, width = -1, height = -1;
uint16_t hmin, hmax, vmin, vmax;
// interlaced output: fb source holds both fields (double height),
// while the scale
... [567 chars total, truncated]
π¦ Tool Result
2026-07-27 03:47:33
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp has been updated successfully. (file state is current in your context β no need to Read it back)
if (sscanf(cmd, "fb_cmd2 %d %d %d", &fmt, &rb, &div) == 3)
{
if (div >= 1 && div <= 4)
{
width = v_cur.item[1] / div;
height = (v_cur.item[5] << il) / div;
hmin = vmin = 0;
h
... [279 chars total, truncated]
π¦ Tool Result
2026-07-27 03:47:36
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp has been updated successfully. (file state is current in your context β no need to Read it back)
if (width < 120 || width > (int)v_cur.item[1]) width = v_cur.item[1];
if (height < 120 || height > (int)v_cur.item[5]) height = v_cur.item[5];
int divx = 1;
int divy = 1;
if (cfg.direc
... [739 chars total, truncated]
New:
if (width < 120 || width > (int)v_cur.item[1]) width = v_cur.item[1];
if (height < 120 || height > (int)(v_cur.item[5] << il)) height = v_cur.item[5] << il;
int divx = 1;
int divy = 1;
... [784 chars total, truncated]
π¦ Tool Result
2026-07-27 03:47:43
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp has been updated successfully. (file state is current in your context β no need to Read it back)
π€ Claude Progress
2026-07-27 03:47:47
Last piece β force CONF_VGA_FB in user_io.cpp:
π¦ Tool: ReadFile:/home/izzie/Desktop/mister-scaler/Main_MiSTer/user_io.cpp (offset: 3024, limit: 8)
π¦ Tool Result
2026-07-27 03:47:48
β Result (Read):
3024 if (cfg.hdmi_limited & 1) map |= CONF_HDMI_LIMITED1;
3025 if (cfg.hdmi_limited & 2) map |= CONF_HDMI_LIMITED2;
3026 if (cfg.direct_video) map |= CONF_DIRECT_VIDEO;
3027 if (cfg.direct_video == 2) map |= CONF_DIRECT_VIDEO2;
3028 if (vga_fb) map |= CONF_VGA_FB;
3029
3030 if ((map != key_map) || force)
3031 {
if (vga_fb || video_dv_scaler_forced()) map |= CONF_VGA_FB;
π¦ Tool Result
2026-07-27 03:47:56
β Result (Edit):
The file /home/izzie/Desktop/mister-scaler/Main_MiSTer/user_io.cpp has been updated successfully. (file state is current in your context β no need to Read it back)
π¦ Tool Call
2026-07-27 03:48:05
π¦ Tool: BashDescription: Check include and toolchain availability
grep -n '#include "video.h"' Main_MiSTer/user_io.cpp | head -1; ls Main_MiSTer/Makefile 2>/dev/null; which arm-linux-gnueabihf-g++ 2>/dev/null;echo"host g++: $(which g++)"
π¦ Tool: BashDescription: Syntax-check video.cpp with host g++
cd Main_MiSTer && g++ -fsyntax-only -std=gnu++14 -I. video.cpp 2>&1| head -20
π¦ Tool Result
2026-07-27 03:48:16
β Result (Bash):
In file included from support/megacd/megacd.h:42,
from support.h:44,
from video.cpp:31:
support/megacd/../../cd.h:4:10: fatal error: libchdr/chd.h: No such file or directory
4 | #include <libchdr/chd.h>
| ^~~~~~~~~~~~~~~
compilation terminated.
sys/ascal.vhd | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++--------
sys/sys_top.v | 7 ++--
2 files changed, 112 insertions(+), 19 deletions(-)
diff --git a/sys/ascal.vhd b/sys/ascal.vhd
index a735e6b..1382a3a 100644
--- a/sys/ascal.vhd
+++ b/sys/ascal.vhd
@@ -89,7 +89,13 @@ USE ieee.numeric_std.ALL;
-- 0 : Direct. Single framebuffer.
-- 1 : Triple buffering
--- MODE[4] : TBD
+-- MODE[4]
+-- 0 : Progressive output
+-- 1 : Interlaced output. Timing parameters describe one field (vtotal lines).
+-- Fields alternate vtotal / vtotal+1 lines; VSync is delayed by htotal/2
+-- on the long field and vertical sampling is offset by half a step on the
+-- field that follows it. When the source holds both fields
+-- (o_ivsize = 2 * o_vsize), lines are woven (read 2n+field) instead.
-- MASK : Enable / Disable selected interpoler
-- 0:Nearest 1:Bilinear 2:SharpBilinear 3:Bicubic 4:Polyphase
@@ -463,6 +469,15 @@ ARCHITECTURE rtl OF ascal IS
SIGNAL o_hmin,o_hmax,o_hdisp,o_v_hmin_adj : uint12;
SIGNAL o_hsize,o_vsize : uint12;
SIGNAL o_vtotal,o_vsstart,o_vsend : uint12;
+ -- Interlaced output
+ SIGNAL o_ilace : std_logic;
+ SIGNAL o_fld,o_fld_pre2,o_fld_pre,o_fld_dsp : std_logic :='0';
+ SIGNAL o_vtotal_eff : natural RANGE 0 TO 4096;
+ SIGNAL o_hs2p : natural RANGE 0 TO 8191;
+ SIGNAL o_hs2 : uint12;
+ SIGNAL o_vs2start,o_vs2end : uint12;
+ SIGNAL o_ivsize_eff : uint12;
+ SIGNAL o_ivdouble : std_logic;
SIGNAL o_vrr,o_isync,o_isync2 : std_logic;
SIGNAL o_vrr_sync,o_vrr_sync2 : boolean;
SIGNAL o_vrr_min,o_vrr_min2 : boolean;
@@ -1330,7 +1345,12 @@ BEGIN
END IF;
i_ohsize<=o_hsize; -- <ASYNC>
- i_ovsize<=o_vsize; -- <ASYNC>
+ IF i_mode(4)='1' AND o_vsize<=2047 THEN
+ -- Interlaced output : keep (or downscale to) full frame height
+ i_ovsize<=2*o_vsize; -- <ASYNC>
+ ELSE
+ i_ovsize<=o_vsize; -- <ASYNC>
+ END IF;
----------------------------------------------------
-- Downscaling vertical
@@ -1905,6 +1925,20 @@ BEGIN
o_vrr <=vrr;
o_vrrmax <= vrrmax;
+ -- Interlaced output
+ o_ilace <=o_mode(4);
+ -- Precompute mid-line VSync position for the long field
+ o_hs2p <=o_hsstart + o_htotal/2;
+ IF o_hs2p>=o_htotal THEN
+ o_hs2 <=o_hs2p - o_htotal;
+ o_vs2start<=o_vsstart + 1;
+ o_vs2end <=o_vsend + 1;
+ ELSE
+ o_hs2 <=o_hs2p;
+ o_vs2start<=o_vsstart;
+ o_vs2end <=o_vsend;
+ END IF;
+
--------------------------------------------
-- Triple buffering.
-- For intelaced video, half frames are updated independently
@@ -1912,7 +1946,7 @@ BEGIN
o_isync <= '0';
o_isync2 <= o_isync;
o_freeze <= freeze;
- o_bob_deint <= bob_deint;
+ o_bob_deint <= bob_deint AND NOT o_ilace; -- Bob deint. pointless with interlaced output
o_iwfl <= i_wfl;
o_inter <=i_inter; -- <ASYNC>
o_iendframe0<=i_endframe0; -- <ASYNC>
@@ -1987,6 +2021,16 @@ BEGIN
o_vdown<='0';
END IF;
+ -- Interlaced output : weave (line decimation) when the source holds
+ -- both fields (o_ivsize = 2*o_vsize), else phase-bias mode.
+ IF o_ilace='1' AND o_ivsize>o_vsize THEN
+ o_ivdouble <='1';
+ o_ivsize_eff<=o_ivsize/2;
+ ELSE
+ o_ivdouble <='0';
+ o_ivsize_eff<=o_ivsize;
+ END IF;
+
-- 011=8bpp(palette) 100=16bpp 101=24bpp 110=32bpp
CASE o_format(2 DOWNTO 0) IS
WHEN "011" => o_ihsize_temp <= o_ihsize;
@@ -2048,7 +2092,13 @@ BEGIN
o_adrsa<='0';
o_adrsb<=o_adrsa;
- o_vacc_ini<=(o_vsize - o_ivsize + 8192) MOD 8192;
+ IF o_ilace='1' AND o_ivdouble='0' AND o_fld='0' THEN
+ -- Interlaced output : this field samples half a step lower.
+ -- = (o_vsize - o_ivsize_eff) + o_ivsize_eff : always < 2*o_vsize
+ o_vacc_ini<=o_vsize;
+ ELSE
+ o_vacc_ini<=(o_vsize - o_ivsize_eff + 8192) MOD 8192;
+ END IF;
o_hacc_ini<=(o_hsize + o_ihsize + 8192) MOD 8192;
--Alternate phase
@@ -2076,7 +2126,7 @@ BEGIN
END IF;
END IF;
IF dif_v>=8192 THEN
- o_vacc_next<=(o_vacc_next + 2*o_ivsize) MOD 8192;
+ o_vacc_next<=(o_vacc_next + 2*o_ivsize_eff) MOD 8192;
vcarry_v:=false;
ELSE
o_vacc_next<=dif_v;
@@ -2085,7 +2135,7 @@ BEGIN
IF o_vcpt_pre2=o_vmin THEN
o_vacc <=o_vacc_ini;
- o_vacc_next<=o_vacc_ini + 2*o_ivsize;
+ o_vacc_next<=o_vacc_ini + 2*o_ivsize_eff;
o_vacpt <=x"001";
o_vacptl<="01";
vcarry_v:=false;
@@ -2144,7 +2194,12 @@ BEGIN
END CASE;
o_read<=o_read_pre AND o_run;
- o_rline<=o_vacpt(0); -- Even/Odd line for interlaced video
+ IF o_ivdouble='1' THEN
+ -- Interlaced output weave : physical line parity is constant per field
+ o_rline<=NOT o_fld;
+ ELSE
+ o_rline<=o_vacpt(0); -- Even/Odd line for interlaced video
+ END IF;
----
-- When bob deinterlacing we read lines from one buffer (the most current) but we read them twice
@@ -2162,16 +2217,33 @@ BEGIN
ELSE
o_adrs_pre <= to_integer(o_vacpt(11 DOWNTO 1) & "0") * to_integer(o_stride);
END IF;
+ ELSIF o_ivdouble='1' THEN
+ -- Interlaced output weave : read physical lines 2n + field
+ o_adrs_pre<=to_integer(o_vacpt(10 DOWNTO 0) & (NOT o_fld)) * to_integer(o_stride);
ELSE
o_adrs_pre<=to_integer(o_vacpt) * to_integer(o_stride);
END IF;
IF o_adrsa='1' THEN
IF o_fload=2 THEN
- o_adrs<=to_unsigned(o_hbcpt * N_BURST,32);
+ IF o_ivdouble='1' AND o_fld='0' THEN
+ -- Interlaced output weave, odd field : first line is physical line 1
+ o_adrs<=to_unsigned(o_hbcpt * N_BURST,32) + o_stride;
+ ELSE
+ o_adrs<=to_unsigned(o_hbcpt * N_BURST,32);
+ END IF;
o_alt<="1111";
ELSIF o_fload=1 THEN
- o_adrs<=to_unsigned(o_hbcpt * N_BURST,32) + o_stride;
+ IF o_ivdouble='1' THEN
+ -- Interlaced output weave : second line is physical line 2 + field
+ IF o_fld='0' THEN
+ o_adrs<=to_unsigned(o_hbcpt * N_BURST,32) + (o_stride & '0') + o_stride;
+ ELSE
+ o_adrs<=to_unsigned(o_hbcpt * N_BURST,32) + (o_stride & '0');
+ END IF;
+ ELSE
+ o_adrs<=to_unsigned(o_hbcpt * N_BURST,32) + o_stride;
+ END IF;
o_alt<="0100";
ELSE
o_adrs<=to_unsigned(o_adrs_pre + (o_hbcpt * N_BURST),32);
@@ -2759,6 +2831,13 @@ BEGIN
IF rising_edge(o_clk) THEN
IF o_ce='1' THEN
+ -- Interlaced output : long field is one line longer
+ IF o_ilace='1' AND o_fld='1' THEN
+ o_vtotal_eff<=o_vtotal + 1;
+ ELSE
+ o_vtotal_eff<=o_vtotal;
+ END IF;
+
-- Output pixels count
IF o_hcpt+1<o_htotal THEN
o_hcpt<=(o_hcpt+1) MOD 4096;
@@ -2769,8 +2848,9 @@ BEGIN
o_vcpt_sync <= o_vcpt_sync+1;
END IF;
- IF o_vcpt_pre3+1>=o_vtotal THEN
+ IF o_vcpt_pre3+1>=o_vtotal_eff THEN
o_vcpt_pre3<=0;
+ o_fld<=o_ilace AND NOT o_fld;
ELSIF o_vrr_sync2 THEN
o_vcpt_pre3<=o_vsstart;
o_sync<=false;
@@ -2781,6 +2861,11 @@ BEGIN
o_vcpt_pre2<=o_vcpt_pre3;
o_vcpt_pre<=o_vcpt_pre2;
o_vcpt<=o_vcpt_pre;
+ -- Field bit aligned with each stage of the line counter pipeline;
+ -- o_fld_dsp is aligned with the displayed line counter o_vcpt.
+ o_fld_pre2<=o_fld;
+ o_fld_pre<=o_fld_pre2;
+ o_fld_dsp<=o_fld_pre;
END IF;
o_end(0)<=to_std_logic(o_vcpt>=o_vdisp);
@@ -2788,9 +2873,16 @@ BEGIN
o_pev(0)<=to_std_logic(o_hcpt>=o_hmin AND o_hcpt<=o_hmax AND
o_vcpt>=o_vmin AND o_vcpt<=o_vmax);
o_hsv(0)<=to_std_logic(o_hcpt>=o_hsstart AND o_hcpt<o_hsend);
- o_vsv(0)<=to_std_logic((o_vcpt=o_vsstart AND o_hcpt>=o_hsstart) OR
- (o_vcpt>o_vsstart AND o_vcpt<o_vsend) OR
- (o_vcpt=o_vsend AND o_hcpt<o_hsstart));
+ IF o_ilace='1' AND o_fld_dsp='1' THEN
+ -- Interlaced output, long field : VSync delayed by htotal/2
+ o_vsv(0)<=to_std_logic((o_vcpt=o_vs2start AND o_hcpt>=o_hs2) OR
+ (o_vcpt>o_vs2start AND o_vcpt<o_vs2end) OR
+ (o_vcpt=o_vs2end AND o_hcpt<o_hs2));
+ ELSE
+ o_vsv(0)<=to_std_logic((o_vcpt=o_vsstart AND o_hcpt>=o_hsstart) OR
+ (o_vcpt>o_vsstart AND o_vcpt<o_vsend) OR
+ (o_vcpt=o_vsend AND o_hcpt<o_hsstart));
+ END IF;
o_vss<=to_std_logic(o_vcpt_pre2>=o_vmin AND o_vcpt_pre2<=o_vmax);
o_hsv(1 TO 11)<=o_hsv(0 TO 10);
@@ -2823,7 +2915,7 @@ BEGIN
END IF;
o_vcpt2<=o_vcpt_pre3;
- o_vrr_sync<=(o_vrr='1' AND (o_sync OR o_sync_max) AND o_vcpt2>=o_vdisp AND o_vcpt2<o_vsstart);
+ o_vrr_sync<=(o_vrr='1' AND o_ilace='0' AND (o_sync OR o_sync_max) AND o_vcpt2>=o_vdisp AND o_vcpt2<o_vsstart);
o_vrr_sync2<=o_vrr_sync;
END IF;
@@ -2895,13 +2987,13 @@ BEGIN
o_vpix_inner(1 TO 5)<=o_vpix_inner(0 TO 4);
-- CYCLE 8
- IF to_integer(o_vacpt)>o_ivsize THEN
+ IF to_integer(o_vacpt)>o_ivsize_eff THEN
IF fracnn_v = '0' THEN
o_vpixq_pre<=(o_vpix_outer(0), o_vpix_inner(5), o_vpix_inner(5), o_vpix_inner(5));
ELSE
o_vpixq_pre<=(o_vpix_outer(0), o_vpix_outer(1), o_vpix_outer(1), o_vpix_outer(1));
END IF;
- ELSIF to_integer(o_vacpt)=o_ivsize THEN
+ ELSIF to_integer(o_vacpt)=o_ivsize_eff THEN
IF fracnn_v = '0' THEN
o_vpixq_pre<=(o_vpix_outer(0), o_vpix_inner(5), o_vpix_outer(1), o_vpix_outer(1));
ELSE
# Memory Index
- [MiSTer 480i scaler project](mister-480i-scaler-project.md) β ASCAL interlaced-output patch: architecture, mode(4)/weave/bias design, pending hardware test
π¦ Tool Result
2026-07-27 03:49:52
β Result (Write):
File created successfully at: /home/izzie/.claude/projects/-home-izzie-Desktop-mister-scaler/memory/MEMORY.md (file state is current in your context β no need to Read it back)
π€ Claude
2026-07-27 03:50:14
Implementation complete. All changes are in place across the five repos, ghdl-clean on the VHDL and g++-syntax-clean on the C++ side. Here's the summary:
What was built
ASCAL now has a true interlaced output mode (mode(4), previously a "TBD" bit). Given the existing 15 kHz field modeline (640Γ240, vtotal 262 β which already has correct 480i line timing), it alternates a 262-line field with a 263-line field whose vsync is delayed by half a scanline (same field-pairing invariant Minimig's Agnus uses), producing a proper 525-line / 59.94 Hz interlaced raster. Two vertical sampling modes handle content:
Weave (source = exactly 2Γ the output window, e.g. 480β240): each field reads physical lines 2n+field β full 480-line detail, this is the flagship path for ao486 480p and the menu framebuffer.
Phase-bias (source β€ window height): the resample accumulator starts half a step lower on alternate fields β works with every interpolation mode for 240p-ish content.
Two subtle bugs were caught during design review and are handled in the code: the input pipeline would have silently pre-downscaled 480-line video to 254 lines (destroying the interlace detail β fixed by doubling i_ovsize when interlacing), and the direct_video porch rewrite puts vsync 2 lines from the frame wrap, which would have glitched a naΓ―ve field register mid-pulse (fixed with a display-aligned field copy, o_fld_dsp).
Linux side (Main_MiSTer):
New ini key DIRECT_VIDEO_INTERLACE=1 (with DIRECT_VIDEO=1) selects the 480i mode (576i with MENU_PAL=1); it overrides FORCED_SCANDOUBLER with a log note.
VGA_SCALER=1 + DIRECT_VIDEO=1 now routes the scaler to the DV/HDMI pins for any core, by forcing the existing CONF_VGA_FB mux bit β this part works even with unmodified core bitstreams.
Main probes a new capability bit (SET_VIDEO reply bit 2) and silently falls back to today's 240p DV behavior on cores that haven't been rebuilt.
On every core resolution change it forces an exact 2:1 window via UIO_SETHEIGHT (e.g. 480βwindow 240, 400-line DOS textβwindow 200 letterboxed) so weave stays exact; the menu/Linux framebuffer doubles to ~480 lines, and the 240p heuristics (logo halving, fb_cmd integer scaling) are gated off when interlaced.
What you need to do to test
Rebuild Menu and ao486 (and optionally Minimig/PSX) in Quartus β ascal.vhd + sys_top.v changed in each core's sys/.
Cross-compile Main_MiSTer (ARM toolchain isn't on this machine; the touched files pass host g++ -fsyntax-only).
Set DIRECT_VIDEO=1, DIRECT_VIDEO_INTERLACE=1, VGA_SCALER=1, COMPOSITE_SYNC=1 and check on the CRT: menu at 480i, ao486 DOS at 480i, then the regression case with the key off.
Known v1 limitations (by design, per your choices)
Free-running fields β no genlock to the core's frame rate (occasional dropped/repeated field; the lltune PLL servo would need field-rate handling for v2). The csync module has no true equalization pulses (same as today's DV β CRTs tolerate it, but it's the first suspect if a set won't lock field phase). Interlaced inputs through this mode weave with random field phase.
The full plan with per-edit rationale is saved at ~/.claude/plans/sorted-giggling-pearl.md if you want to review anything before synthesis.
π§ USER #1
2026-07-27 04:11:20
Just tested on real hardware. It worked in one shot, I am blown away!
Let's durably document what was done on these repos and commit them to a branch.
π€ Claude
2026-07-27 04:11:36
Fantastic β one-shot on real hardware is a great result! Let me document and commit. First, checking repo states and doc conventions:
π¦ Tool Call
2026-07-27 04:11:38
π¦ Tool: BashDescription: Check git status and branch of all five repos
== Main_MiSTer
M cfg.cpp
M cfg.h
M user_io.cpp
M video.cpp
M video.h
?? db/
?? menu.flow.rpt
?? menu.map.rpt
?? menu.map.summary
?? menu.qpf
?? menu.qsf
master
== Menu_MiSTer
M menu.qsf
M sys/ascal.vhd
M sys/sys_top.v
master
== ao486_MiSTer
M sys/ascal.vhd
M sys/sys_top.v
master
== Minimig-AGA_MiSTer
M sys/ascal.vhd
M sys/sys_top.v
MiSTer
== PSX_MiSTer
M sys/ascal.vhd
M sys/sys_top.v
main
no docs dir
π¦ Tool Call
2026-07-27 04:12:01
π¦ Tool: BashDescription: Check what changed in Menu_MiSTer/menu.qsf
git -C Menu_MiSTer diff menu.qsf | head -30
π¦ Tool Result
2026-07-27 04:12:03
β Result (Bash):
diff --git a/menu.qsf b/menu.qsf
index 577fcc3..50d312a 100644
--- a/menu.qsf
+++ b/menu.qsf
@@ -15,7 +15,7 @@ set_global_assignment -name PARTITION_NETLIST_TYPE SOURCE -section_id Top
set_global_assignment -name PARTITION_FITTER_PRESERVATION_LEVEL PLACEMENT_AND_ROUTING -section_id Top
set_global_assignment -name PARTITION_COLOR 16764057 -section_id Top
-set_global_assignment -name LAST_QUARTUS_VERSION "17.0.2 Standard Edition"
+set_global_assignment -name LAST_QUARTUS_VERSION "17.0.2 Lite Edition"
set_global_assignment -name GENERATE_RBF_FILE ON
set_global_assignment -name PROJECT_OUTPUT_DIRECTORY output_files
π€ Claude
2026-07-27 04:12:07
That's just Quartus stamping its edition into the file β I'll leave it uncommitted. Now writing the durable documentation into Main_MiSTer:
# True interlaced (480i/576i) scaler output over direct_video
This feature lets the ASCAL scaler emit **genuine interlaced 480i (NTSC) / 576i
(PAL)** video over the `direct_video` HDMI/DAC path. Cores that can only
produce progressive output (e.g. ao486 at 640Γ480) β and the Linux/menu
framebuffer
... [6362 chars total, truncated]
π¦ Tool Result
2026-07-27 04:12:42
β Result (Write):
File created successfully at: /home/izzie/Desktop/mister-scaler/Main_MiSTer/docs/direct_video_interlace.md (file state is current in your context β no need to Read it back)
π€ Claude
2026-07-27 04:12:55
Now branching and committing each repo. Core repos first (identical change set):
π¦ Tool Call
2026-07-27 04:13:07
π¦ Tool: BashDescription: Create branch and commit sys changes in the four core repos
forrin Menu_MiSTer ao486_MiSTer Minimig-AGA_MiSTer PSX_MiSTer;docd /home/izzie/Desktop/mister-scaler/$r
git checkout -b direct-video-interlace
git add sys/ascal.vhd sys/sys_top.v
git commit -m "$(cat <<'EOF'sys: true interlaced (480i/576i) scaler output for direct_videoascal.vhd gains an interlaced output mode on mode(4) (previously TBD).Given a field modeline (e.g. the 640x240 vtotal=262 direct_video TV mode),the output sweep alternates vtotal / vtotal+1 line fields with VSyncdelayed ... [2174 chars total, truncated]
π¦ Tool Result
2026-07-27 04:13:09
β Result (Bash):
Switched to a new branch 'direct-video-interlace'
2 files changed, 112 insertions(+), 19 deletions(-)
Switched to a new branch 'direct-video-interlace'
2 files changed, 112 insertions(+), 19 deletions(-)
Switched to a new branch 'direct-video-interlace'
2 files changed, 112 insertions(+), 19 deletions(-)
Switched to a new branch 'direct-video-interlace'
2 files changed, 112 insertions(+), 19 deletions(-)
π¦ Tool Call
2026-07-27 04:13:23
π¦ Tool: BashDescription: Branch and commit Main_MiSTer changes with docs
cd /home/izzie/Desktop/mister-scaler/Main_MiSTer
git checkout -b direct-video-interlace
git add cfg.h cfg.cpp video.h video.cpp user_io.cpp docs/direct_video_interlace.md
git commit -m "$(cat <<'EOF'Add DIRECT_VIDEO_INTERLACE: true 480i/576i scaler output over direct_videoNew MiSTer.ini option DIRECT_VIDEO_INTERLACE=1 (with DIRECT_VIDEO=1)switches the direct_video scaler mode from 240p/288p to genuineinterlaced 480i/576i: the 15KHz TV field modeline is sent with a newinterlace flag (SET_VI... [2113 chars total, truncated]
π¦ Tool Result
2026-07-27 04:13:25
β Result (Bash):
Switched to a new branch 'direct-video-interlace'
[direct-video-interlace 91801a4] Add DIRECT_VIDEO_INTERLACE: true 480i/576i scaler output over direct_video
6 files changed, 182 insertions(+), 13 deletions(-)
create mode 100644 docs/direct_video_interlace.md
π¦ Tool Call
2026-07-27 04:13:31
π¦ Tool: BashDescription: Verify all five repos committed on the new branch
cd /home/izzie/Desktop/mister-scaler &&forrin Main_MiSTer Menu_MiSTer ao486_MiSTer Minimig-AGA_MiSTer PSX_MiSTer;doecho"== $r ($(git -C $r branch --show-current))"; git -C $r log --oneline -1; git -C $r status --short | head -4;done
Plan file: ~/.claude/plans/sorted-giggling-pearl.md. Status: all code done, ghdl-clean, g++ syntax-clean; NOT yet hardware-tested β user must rebuild Menu/ao486 cores in Quartus + Main_MiSTer with ARM
... [241 chars total, truncated]
New:
Plan file: ~/.claude/plans/sorted-giggling-pearl.md. Status: **verified working on real hardware first try (2026-07-27, CRT via direct_video DAC)**. Committed on branch `direct-video-interlace` in all
... [555 chars total, truncated]
π¦ Tool Result
2026-07-27 04:13:42
β Result (Edit):
The file /home/izzie/.claude/projects/-home-izzie-Desktop-mister-scaler/memory/mister-480i-scaler-project.md has been updated successfully.
π€ Claude
2026-07-27 04:13:54
Done β everything is documented and committed on a direct-video-interlace branch in all five repos:
Repo
Commit
Contents
Main_MiSTer
91801a4
5 source files + new docs/direct_video_interlace.md
Menu_MiSTer
c2f85ee
sys/ascal.vhd + sys/sys_top.v
ao486_MiSTer
9c9898d
same sys pair
Minimig-AGA_MiSTer
5364e1b
same sys pair
PSX_MiSTer
42f31e4
same sys pair (CRLF preserved)
The documentation lives in Main_MiSTer/docs/direct_video_interlace.md β it covers usage (the ini keys and rebuild requirement), the field-timing and weave/phase-bias design, a table of every protocol change (SET_VIDEO word 5 bit 15, capability reply bit 2, ascal mode(4), the CONF_VGA_FB forcing), per-repo change summaries, and the known limitations (free-running fields, line-rate-only csync serration, unsynced field phase for interlaced inputs). The commit messages in the core repos carry the full RTL rationale too, so the design survives even when sys/ gets overwritten by a framework update and someone needs to re-apply it from history.
Deliberately left uncommitted: the Quartus build artifacts in Main_MiSTer (db/, menu.* reports) and the Menu_MiSTer/menu.qsf modification β that one is just Quartus stamping "Lite Edition" over "Standard Edition" from your compile, so I didn't fold it into the feature commit.
Note the repos' default branches differ (master/master/master/MiSTer/main), so if you push and open PRs, each branch targets its own default. Congratulations on the one-shot bring-up β if you later want the v2 genlock (locking field cadence to the core via the lltune servo), the groundwork and the known hazards are already written down in the doc.