Skip to content

Instantly share code, notes, and snippets.

@iwalton3
Last active July 27, 2026 16:33
Show Gist options
  • Select an option

  • Save iwalton3/db1ceda30a95e272150f0d7dede9f231 to your computer and use it in GitHub Desktop.

Select an option

Save iwalton3/db1ceda30a95e272150f0d7dede9f231 to your computer and use it in GitHub Desktop.
Claude session where MiSTer ASCAL scaler gained an interlaced output mode

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:

  1. 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).
  2. 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.


Part A β€” ascal.vhd (interlaced output RTL)

File: Menu_MiSTer/sys/ascal.vhd (then replicate Γ—3).

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 unchanged
END IF;

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; END IF; -- <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)

  1. :398 capability probe: if(io_din[7:0] == 'h20) io_dout_sys <= 'b11; β†’ 'b111 (bit 2 = interlace support).
  2. :427 SET_VIDEO decode: 4: {ILACE, HEIGHT} <= {io_din[15], io_din[11:0]}; (bit 15 of the vact word is free).
  3. reg ILACE = 0; next to the other video regs (~:1053-1061).
  4. :791 .mode({ILACE, ~lowlat, LFB_EN ? LFB_FLT : |scaler_flt, 2'b00}), β€” now exactly 5 bits.

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


Part C β€” Main_MiSTer (Linux side)

  1. cfg key β€” cfg.h (:28, after direct_video): uint8_t direct_video_interlace;. cfg.cpp (:75): { "DIRECT_VIDEO_INTERLACE", (void*)(&(cfg.direct_video_interlace)), UINT8, 0, 1 },.

  2. 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).

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

  4. vmode struct + mode selection β€” video.cpp:178: uint32_t unused[6] β†’ uint32_t ilace; uint32_t unused[5]; (static_assert at :186 keeps layout honest). In video_mode_load()'s direct_video branch (:2600-2614):

    int ilace = (cfg.direct_video_interlace && supports_ilace()) ? 1 : 0;
    int mode = cfg.menu_pal ? 2 : 0;
    if (!ilace && cfg.forced_scandoubler) mode++;   // interlace key overrides scandoubler; log a note
    ...
    v_def.param.ilace = ilace;

    15 kHz tvmodes[0]/[2] + ilace β‡’ 480i/576i. Re-entry via cfg_has_video_sections() re-derives the flag; VRR unreachable under DV.

  5. 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).

  6. 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_t src_h = height << (vi->interlaced ? 1 : 0);   // frame height
    if (src_h > scrh) spi_uio_cmd16(UIO_SETHEIGHT, src_h >> 1);  // exact 2:1 weave window
    else /* 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.

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

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

  1. ascal.vhd in Menu_MiSTer (Part A) β†’ copy to ao486/Minimig; PSX via unix2dos-style CRLF-preserving copy.
  2. sys_top.v Γ—4 (Part B; PSX offsets).
  3. 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.

Claude Agent Conversation Log

Source: /home/izzie/.claude/projects/-home-izzie-Desktop-mister-scaler/9e999fa4-192e-4345-9713-5069a3bbd7aa.jsonl

Session Metadata

  • Session ID: 9e999fa4-192e-4345-9713-5069a3bbd7aa
  • Total Messages: 546

πŸ§‘ USER #0

2026-07-27 02:49:20

⏭ Skip to next user message

There are 4 repos in this folder:

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

  1. Make ASCAL output 480i fields, alternating properly each frame with proper handling
  2. 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

{
  "query": "select:EnterPlanMode,ExitPlanMode",
  "max_results": 2
}

πŸ“¦ Tool Result

2026-07-27 02:49:50

βœ… Result (ToolSearch):

[{'type': 'tool_reference', 'tool_name': 'EnterPlanMode'}, {'type': 'tool_reference', 'tool_name': 'ExitPlanMode'}]

πŸ“¦ Tool Call

2026-07-27 02:49:52

πŸ“¦ Tool: EnterPlanMode (no input)


πŸ“¦ Tool Result

2026-07-27 02:49:52

βœ… Result (EnterPlanMode):

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

  1. 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.
  2. 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.
  3. 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?

  1. 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).
  2. 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.

  1. 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.
  2. 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)

πŸ“¦ Tool Call

2026-07-27 03:28:33

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd (offset: 1290, limit: 60)


πŸ“¦ Tool Result

2026-07-27 03:28:33

βœ… Result (Read):

1290					END IF;
1291	
1292					IF i_pvs='1' AND i_vs_pre='0' AND (i_inter='0' OR i_pfl='0') THEN
1293						i_vdmax<=i_vimax;
1294					END IF;
1295					i_hdmax<=i_himax;
1296	
1297					IF i_format="00" OR i_format="11" THEN -- 16bpp
1298						i_hburst<=(i_hrsize*2 + N_BURST - 1) / N_BURST;
1299					ELSIF i_format="01" THEN -- 24bpp
1300						i_hburst<=(i_hrsize*3 + N_BURST - 1) / N_BURST;
1301					ELSE -- 32bpp
1302						i_hburst<=(i_hrsize*4 + N_BURST - 1) / N_BURST;
1303					END IF;
1304					----------------------------------------------------
1305					i_mode<=mode; -- <ASYNC>
1306					i_format<=format; -- <ASYNC>
1307	
1308					-- Downscaling : Nearest or bilinear
1309					i_bil<=to_std_logic(i_mode(2 DOWNTO 0)/="000" AND NOT DOWNSCALE_NN);
1310	
1311					i_hdown<=to_std_logic(i_hsize>i_ohsize AND DOWNSCALE); --H downscale
1312					i_vdown<=to_std_logic(i_vsize>i_ovsize AND DOWNSCALE); --V downscale
1313	
1314					----------------------------------------------------
1315					i_hsize  <=(4096+i_hmax-i_hmin+1) MOD 4096;
1316					i_vmaxmin<=(4096+i_vmax-i_vmin+1) MOD 4096;
1317	
1318					IF i_inter='0' THEN
1319						-- Non interlaced
1320						i_vsize<=i_vmaxmin;
1321						i_half <='0';
1322					ELSIF i_ovsize<2*i_vmaxmin THEN
1323						-- Interlaced, but downscaling, use only half frames
1324						i_vsize<=i_vmaxmin;
1325						i_half <='1';
1326					ELSE
1327						-- Interlaced : Double image height
1328						i_vsize<=2*i_vmaxmin;
1329						i_half <='0';
1330					END IF;
1331	
1332					i_ohsize<=o_hsize; -- <ASYNC>
1333					i_ovsize<=o_vsize; -- <ASYNC>
1334	
1335					----------------------------------------------------
1336					-- Downscaling vertical
1337					i_divstart<='0';
1338					IF i_de_delay=16 THEN
1339						IF (i_vacc + 2*i_ovsize) < 2*i_vsize THEN
1340							i_vacc<=(i_vacc + 2*i_ovsize) MOD 8192;
1341							i_vnp<='0';
1342						ELSE
1343							i_vacc<=(i_vacc + 2*i_ovsize - 2*i_vsize + 8192) MOD 8192;
1344							i_vnp<='1';
1345						END IF;
1346						i_divstart<='1';
1347	
1348						IF i_vcpt=i_vmin THEN
1349						 i_vacc<=(i_vsize - i_ovsize + 8192) MOD 8192;

πŸ“¦ Tool Call

2026-07-27 03:28:34

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd (offset: 1876, limit: 120)


πŸ“¦ Tool Result

2026-07-27 03:28:34

βœ… Result (Read):

1876				o_read_pre<='0';
1877				o_readlev<=0;
1878				o_copylev<=0;
1879				o_hsp<='0';
1880	
1881			ELSIF rising_edge(o_clk) THEN
1882				------------------------------------------------------
1883				o_mode   <=mode; -- <ASYNC> ?
1884				o_format <="0001" & format; -- <ASYNC> ?
1885	
1886				o_run    <=run; -- <ASYNC> ?
1887	
1888				o_htotal <=htotal; -- <ASYNC> ?
1889				o_hsstart<=hsstart; -- <ASYNC> ?
1890				o_hsend  <=hsend; -- <ASYNC> ?
1891				o_hdisp  <=hdisp; -- <ASYNC> ?
1892				o_hmin   <=hmin; -- <ASYNC> ?
1893				o_hmax   <=hmax; -- <ASYNC> ?
1894	
1895				o_vtotal <=vtotal; -- <ASYNC> ?
1896				o_vsstart<=vsstart; -- <ASYNC> ?
1897				o_vsend  <=vsend; -- <ASYNC> ?
1898				o_vdisp  <=vdisp; -- <ASYNC> ?
1899				o_vmin   <=vmin; -- <ASYNC> ?
1900				o_vmax   <=vmax; -- <ASYNC> ?
1901	
1902				o_hsize  <=o_hmax - o_hmin + 1;
1903				o_vsize  <=o_vmax - o_vmin + 1;
1904	
1905				o_vrr    <=vrr;
1906				o_vrrmax <= vrrmax;
1907	
1908				--------------------------------------------
1909				-- Triple buffering.
1910				-- For intelaced video, half frames are updated independently
1911				-- Input : Toggle buffer at end of input frame
1912				o_isync <= '0';
1913				o_isync2 <= o_isync;
1914				o_freeze <= freeze;
1915				o_bob_deint <= bob_deint;
1916				o_iwfl <= i_wfl;
1917				o_inter  <=i_inter; -- <ASYNC>
1918				o_iendframe0<=i_endframe0; -- <ASYNC>
1919				o_iendframe02<=o_iendframe0;
1920				IF o_iendframe0='1' AND o_iendframe02='0' THEN
1921					o_ibuf0<=buf_next(o_ibuf0,o_obuf0,o_freeze);
1922					o_bufup0<='1';
1923					o_isync <= '1';
1924				END IF;
1925				o_iendframe1<=i_endframe1; -- <ASYNC>
1926				o_iendframe12<=o_iendframe1;
1927				IF o_iendframe1='1' AND o_iendframe12='0' THEN
1928					o_ibuf1<=buf_next(o_ibuf1,o_obuf1,o_freeze);
1929					o_bufup1<='1';
1930					o_isync <= '1';
1931				END IF;
1932	
1933				-- Output : Change framebuffer, and image properties, at VS falling edge
1934				IF o_vsv(1)='1' AND o_vsv(0)='0' AND o_bufup0='1' THEN
1935					o_obuf0<=buf_next(o_obuf0,o_ibuf0,o_freeze);
1936					o_bufup0<='0';
1937				END IF;
1938				IF o_vsv(1)='1' AND o_vsv(0)='0' AND o_bufup1='1' THEN
1939					o_obuf1<=buf_next(o_obuf1,o_ibuf1,o_freeze);
1940					o_bufup1<='0';
1941					o_ihsize<=i_hrsize; -- <ASYNC>
1942					o_ivsize<=i_vrsize; -- <ASYNC>
1943					o_hdown<=i_hdown; -- <ASYNC>
1944					o_vdown<=i_vdown; -- <ASYNC>
1945	
1946					IF (o_newres > 0) then
1947						o_newres <= o_newres- 1;
1948					END IF;
1949				END IF;
1950	
1951				IF (swblack = '1' and o_fb_ena = '0' and (o_ihsize /= i_hrsize or o_ivsize /= i_vrsize)) then
1952					o_newres <= 3;
1953				END IF;
1954	
1955				-- Simultaneous change of input and output framebuffers
1956				IF o_vsv(1)='1' AND o_vsv(0)='0' AND
1957					o_iendframe0='1' AND o_iendframe02='0' THEN
1958					o_bufup0<='0';
1959					o_obuf0<=o_ibuf0;
1960				END IF;
1961				IF o_vsv(1)='1' AND o_vsv(0)='0' AND
1962					o_iendframe1='1' AND o_iendframe12='0' THEN
1963					o_bufup1<='0';
1964					o_obuf1<=o_ibuf1;
1965				END IF;
1966	
1967				-- Non-interlaced, use same buffer for even and odd lines
1968				IF o_inter='0' THEN
1969					o_ibuf1<=o_ibuf0;
1970					o_obuf1<=o_obuf0;
1971				END IF;
1972	
1973				-- Triple buffer disabled
1974				IF o_mode(3)='0' THEN
1975					o_obuf0<=0;
1976					o_obuf1<=0;
1977					o_ibuf0<=0;
1978					o_ibuf1<=0;
1979				END IF;
1980	
1981				-- Framebuffer mode.
1982				IF o_fb_ena='1' THEN
1983					o_ihsize<=o_fb_hsize;
1984					o_ivsize<=o_fb_vsize;
1985					o_format<=o_fb_format;
1986					o_hdown<='0';
1987					o_vdown<='0';
1988				END IF;
1989	
1990				-- 011=8bpp(palette) 100=16bpp 101=24bpp 110=32bpp
1991				CASE o_format(2 DOWNTO 0) IS
1992					WHEN "011" => o_ihsize_temp <= o_ihsize;
1993					WHEN "100" => o_ihsize_temp <= o_ihsize * 2;
1994					WHEN "110" => o_ihsize_temp <= o_ihsize * 4;
1995					WHEN OTHERS => o_ihsize_temp <= o_ihsize * 3;

πŸ“¦ Tool Call

2026-07-27 03:29:31

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd (offset: 2030, limit: 160)


πŸ“¦ Tool Result

2026-07-27 03:29:31

βœ… Result (Read):

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 =>

πŸ“¦ Tool Call

2026-07-27 03:29:32

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd (offset: 2755, limit: 80)


πŸ“¦ Tool Result

2026-07-27 03:29:33

βœ… Result (Read):

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

πŸ“¦ Tool Call

2026-07-27 03:29:44

πŸ“¦ Tool: Bash Description: Verify mode bit usage, DOWNSCALE generic, sys_top edit sites

grep -n "o_hmode\|o_vmode\|i_mode(\|o_mode(" Menu_MiSTer/sys/ascal.vhd | head -30; echo ---; grep -n "DOWNSCALE" Menu_MiSTer/sys/sys_top.v ao486_MiSTer/sys/sys_top.v | head; echo ---; grep -rn "MISTER_DOWNSCALE" ao486_MiSTer/*.qsf ao486_MiSTer/sys/*.tcl Menu_MiSTer/*.qsf Menu_MiSTer/sys/*.tcl 2>/dev/null | head; echo ---; sed -n '395,400p;420,450p;787,793p' Menu_MiSTer/sys/sys_top.v

πŸ“¦ Tool Result

2026-07-27 03:29:44

βœ… Result (Bash):

451:	SIGNAL o_mode,o_hmode,o_vmode : unsigned(4 DOWNTO 0);
1194:			i_head(84)<=i_mode(3);
1309:				i_bil<=to_std_logic(i_mode(2 DOWNTO 0)/="000" AND NOT DOWNSCALE_NN);
1974:			IF o_mode(3)='0' THEN
2008:			o_hmode<=o_mode;
2011:				o_hmode(2 DOWNTO 0)<="000";
2014:			o_vmode<=o_mode;
2017:				o_vmode(2 DOWNTO 0)<="000";
2380:			o_v_poly_use_adaptive <= to_std_logic((o_vmode(2 DOWNTO 0)/="000") AND (o_v_poly_adaptive = '1'));
2381:			o_h_poly_use_adaptive <= to_std_logic((o_hmode(2 DOWNTO 0)/="000") AND (o_h_poly_adaptive = '1'));
2385:			IF o_vmode(2 DOWNTO 0)/="000" THEN
2391:			IF o_hmode(2 DOWNTO 0)/="000" THEN
2638:			IF o_hmode(0)='1' THEN -- Bilinear
2691:			CASE o_hmode(2 DOWNTO 0) IS
2927:				IF o_vmode(0)='1' THEN -- Bilinear
2980:				CASE o_vmode(2 DOWNTO 0) IS
---
ao486_MiSTer/sys/sys_top.v:732:	`ifdef MISTER_DOWNSCALE_NN
ao486_MiSTer/sys/sys_top.v:733:		.DOWNSCALE_NN("true"),
Menu_MiSTer/sys/sys_top.v:732:	`ifdef MISTER_DOWNSCALE_NN
Menu_MiSTer/sys/sys_top.v:733:		.DOWNSCALE_NN("true"),
---
---
				areset <= 1;
				io_dout_sys <= 'b11;
			end
			if(io_din[7:0] == 'h20) io_dout_sys <= 'b11;
`ifdef MISTER_DISABLE_ADAPTIVE
			if(io_din[7:0] == 'h2B) io_dout_sys <= {fb_en, sl_r, 4'b0110};
				cfg_set <= 0;
				if(cnt<8) begin
					case(cnt[2:0])
						0: {HDMI_PR,vrr_mode,WIDTH} <= {io_din[15:14], io_din[11:0]};
						1: HFP    <= io_din[11:0];
						2: HS     <= {io_din[15], io_din[11:0]};
						3: HBP    <= io_din[11:0];
						4: HEIGHT <= io_din[11:0];
						5: VFP    <= io_din[11:0];
						6: VS     <= {io_din[15],io_din[11:0]};
						7: VBP    <= io_din[11:0];
					endcase
`ifndef MISTER_DEBUG_NOHDMI
					if(cnt == 1) begin
						cfg_custom_p1 <= 0;
						cfg_custom_p2 <= 0;
						cfg_custom_t <= ~cfg_custom_t;
					end
				end
				else begin
					if(cnt[1:0]==0) cfg_custom_p1 <= io_din[5:0];
					if(cnt[1:0]==1) cfg_custom_p2[15:0]  <= io_din;
					if(cnt[1:0]==2) begin
						cfg_custom_p2[31:16] <= io_din;
						cfg_custom_t <= ~cfg_custom_t;
						cnt[2:0] <= 3'b100;
					end
					if(cnt == 8) {lowlat,cfg_done} <= {io_din[15],1'b1};
`endif
				end
			end
		.vrr      (vrr_mode),
		.vrrmax   (HEIGHT + VBP + VS[11:0] + 12'd1),
		.swblack  (hdmi_blackout),

		.mode     ({~lowlat,LFB_EN ? LFB_FLT : |scaler_flt,2'b00}),
		.poly_clk (clk_sys),
		.poly_a   (coef_addr),

πŸ“¦ Tool Call

2026-07-27 03:29:55

πŸ“¦ Tool: Bash Description: Check DOWNSCALE generic wiring and per-core macro defines

sed -n '714,742p' Menu_MiSTer/sys/sys_top.v; echo ---; grep -rn "MISTER_DOWNSCALE\b" --include=*.qsf --include=*.tcl --include=*.sdc --include=*.v -r ao486_MiSTer Menu_MiSTer Minimig-AGA_MiSTer PSX_MiSTer 2>/dev/null | grep -v sys_top | head; echo ---; grep -rn "VERILOG_MACRO" ao486_MiSTer/*.qsf Menu_MiSTer/*.qsf PSX_MiSTer/*.qsf Minimig-AGA_MiSTer/*.qsf 2>/dev/null | head -20

πŸ“¦ Tool Result

2026-07-27 03:29:55

βœ… Result (Bash):

ascal 
	#(
		.RAMBASE(32'h20000000),
	`ifdef MISTER_SMALL_VBUF
		.RAMSIZE(32'h00200000),
	`else
		.RAMSIZE(32'h00800000),
	`endif
	`ifndef MISTER_FB
		.PALETTE2("false"),
	`else
		`ifndef MISTER_FB_PALETTE
			.PALETTE2("false"),
		`endif
	`endif
	`ifdef MISTER_DISABLE_ADAPTIVE
		.ADAPTIVE("false"),
	`endif
	`ifdef MISTER_DOWNSCALE_NN
		.DOWNSCALE_NN("true"),
	`endif
		.FRAC(8),
`ifdef MENU_CORE
		.N_BURST(2048),
`endif
		.N_DW(128),
		.N_AW(28)
	)
	ascal
---
---
Minimig-AGA_MiSTer/Minimig.qsf:56:set_global_assignment -name VERILOG_MACRO "MISTER_FB=1"
Minimig-AGA_MiSTer/Minimig.qsf:57:set_global_assignment -name VERILOG_MACRO "MISTER_FB_PALETTE=1"
PSX_MiSTer/PSX.qsf:54:set_global_assignment -name VERILOG_MACRO "MISTER_FB=1"
PSX_MiSTer/PSX.qsf:57:set_global_assignment -name VERILOG_MACRO "MISTER_DOWNSCALE_NN=1"
PSX_MiSTer/PSX.qsf:60:#set_global_assignment -name VERILOG_MACRO "MISTER_DISABLE_ADAPTIVE=1"
PSX_MiSTer/PSX.qsf:63:#set_global_assignment -name VERILOG_MACRO "MISTER_DISABLE_YC=1"
PSX_MiSTer/PSX.qsf:66:set_global_assignment -name VERILOG_MACRO "MISTER_DISABLE_ALSA=1"
PSX_MiSTer/PSX_DualSDRAM.qsf:54:set_global_assignment -name VERILOG_MACRO "MISTER_FB=1"
PSX_MiSTer/PSX_DualSDRAM.qsf:57:set_global_assignment -name VERILOG_MACRO "MISTER_DOWNSCALE_NN=1"
PSX_MiSTer/PSX_DualSDRAM.qsf:60:#set_global_assignment -name VERILOG_MACRO "MISTER_DISABLE_ADAPTIVE=1"
PSX_MiSTer/PSX_DualSDRAM.qsf:63:#set_global_assignment -name VERILOG_MACRO "MISTER_DISABLE_YC=1"
PSX_MiSTer/PSX_DualSDRAM.qsf:66:set_global_assignment -name VERILOG_MACRO "MISTER_DISABLE_ALSA=1"
Menu_MiSTer/menu.qsf:11:set_global_assignment -name VERILOG_MACRO "MENU_CORE=1"
ao486_MiSTer/ao486.qsf:57:set_global_assignment -name VERILOG_MACRO "MISTER_FB=1"
ao486_MiSTer/ao486.qsf:60:set_global_assignment -name VERILOG_MACRO "MISTER_FB_PALETTE=1"
ao486_MiSTer/ao486.qsf:62:#set_global_assignment -name VERILOG_MACRO "MISTER_DUAL_SDRAM=1"
ao486_MiSTer/ao486.qsf:65:#set_global_assignment -name VERILOG_MACRO "MISTER_DEBUG_NOHDMI=1"

πŸ“¦ Tool Call

2026-07-27 03:30:00

πŸ“¦ Tool: Bash Description: Check DOWNSCALE generic default in ascal entity

grep -n "DOWNSCALE" Menu_MiSTer/sys/ascal.vhd | head; sed -n '116,145p' Menu_MiSTer/sys/ascal.vhd

πŸ“¦ Tool Result

2026-07-27 03:30:00

βœ… Result (Bash):

103:-- DOWNSCALE : True=Support downscaling False=Downscaling disabled
121:		DOWNSCALE    : boolean := true;
126:		DOWNSCALE_NN : boolean := false;
1309:				i_bil<=to_std_logic(i_mode(2 DOWNTO 0)/="000" AND NOT DOWNSCALE_NN);
1311:				i_hdown<=to_std_logic(i_hsize>i_ohsize AND DOWNSCALE); --H downscale
1312:				i_vdown<=to_std_logic(i_vsize>i_ovsize AND DOWNSCALE); --V downscale
1657:	DownLine:IF DOWNSCALE GENERATE
2009:			IF o_hdown='1' AND DOWNSCALE THEN
2015:			IF o_vdown='1' AND DOWNSCALE THEN
		MASK         : unsigned(7 DOWNTO 0) :=x"FF";
		RAMBASE      : unsigned(31 DOWNTO 0);
		RAMSIZE      : unsigned(31 DOWNTO 0) := x"0080_0000"; -- =8MB
		INTER        : boolean := true;
		HEADER       : boolean := true;
		DOWNSCALE    : boolean := true;
		BYTESWAP     : boolean := true;
		PALETTE      : boolean := true;
		PALETTE2     : boolean := true;
		ADAPTIVE     : boolean := true;
		DOWNSCALE_NN : boolean := false;
		FRAC         : natural RANGE 4 TO 8 :=4;
		OHRES        : natural RANGE 1 TO 4096 :=2304;
		IHRES        : natural RANGE 1 TO 2048 :=2048;
		N_DW         : natural RANGE 64 TO 128 := 128;
		N_AW         : natural RANGE 8 TO 32 := 32;
		N_BURST      : natural := 256 -- 256 bytes per burst
		);
	PORT (
		------------------------------------
		-- Input video
		i_r   : IN  unsigned(7 DOWNTO 0);
		i_g   : IN  unsigned(7 DOWNTO 0);
		i_b   : IN  unsigned(7 DOWNTO 0);
		i_hs  : IN  std_logic; -- H sync
		i_vs  : IN  std_logic; -- V sync
		i_fl  : IN  std_logic; -- Interlaced field
		i_de  : IN  std_logic; -- Display Enable
		i_ce  : IN  std_logic; -- Clock Enable
		i_clk : IN  std_logic; -- Input clock

πŸ“¦ Tool Call

2026-07-27 03:30:06

πŸ“¦ Tool: Bash Description: Verify tvmodes, mode_load, set_mode loop, fb_config in video.cpp

cd Main_MiSTer && sed -n '137,148p;2594,2620p' video.cpp && echo --- && sed -n '2245,2262p' video.cpp && echo --- && sed -n '3419,3448p' video.cpp && echo --- && sed -n '196,214p' video.cpp && echo --- && grep -n "supports_pr\|supports_vrr" video.cpp | head -6

πŸ“¦ Tool Result

2026-07-27 03:30:06

βœ… Result (Bash):

{ { 2048,  48,  32,  80, 1536,  2,  4, 38 }, 209.318, 0, 0 }, //13 2048x1536@60
	{ { 1280,  24,  16,  40, 1440,  3,  5, 33 }, 120.75,  0, 1 }, //14 2560x1440@60 (pr)
};
#define VMODES_NUM (sizeof(vmodes) / sizeof(vmodes[0]))

vmode_t tvmodes[] =
{
	{{ 640, 30, 60, 70, 240,  4, 4, 14 }, 12.587, 0, 0 }, //NTSC 15K
	{{ 640, 16, 96, 48, 480,  8, 4, 33 }, 25.175, 0, 0 }, //NTSC 31K
	{{ 640, 30, 60, 70, 288,  6, 4, 14 }, 12.587, 0, 0 }, //PAL 15K
	{{ 640, 16, 96, 48, 576,  2, 4, 42 }, 25.175, 0, 0 }, //PAL 31K
};
	if (cfg.direct_video && cfg.vsync_adjust)
	{
		printf("Disabling vsync_adjust because of enabled direct video.\n");
		cfg.vsync_adjust = 0;
	}

	if (cfg.direct_video)
	{
		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].vpar[i];
		setPLL(tvmodes[mode].Fpix, &v_def);

		vmode_def = 1;
		vmode_pal = 0;
		vmode_ntsc = 0;
	}
	else
	{
		vmode_def = 0;
		if (!strlen(cfg.video_conf) && !strlen(cfg.video_conf_pal) && !strlen(cfg.video_conf_ntsc))
		{
			vmode_def = get_edid_vmode(&v_def);
---
	printf("Send HDMI parameters:\n");
	spi_uio_cmd_cont(UIO_SET_VIDEO);
	printf("video: ");
	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]);
		//hsync polarity
		else if (i == 3) spi_w((!!v_cur.param.hpol << 15) | v_fix.item[i]);
		//vsync polarity
		else if (i == 7) spi_w((!!v_cur.param.vpol << 15) | v_fix.item[i]);
		else spi_w(v_fix.item[i]);
		printf("%d(%d), ", v_cur.item[i], v_fix.item[i]);
	}

	printf("%chsync, %cvsync\n", !!v_cur.param.hpol ? '+' : '-', !!v_cur.param.vpol ? '+' : '-');

	printf("PLL: ");
	for (int i = 9; i < 21; i++)
---
static void video_fb_config()
{
	PROFILE_FUNCTION();

	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_scale > 4) fb_scale = 4;

	const int fb_scale_x = fb_scale;
	const int 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;

	brd_x = cfg.vscale_border / fb_scale_x;
	brd_y = cfg.vscale_border / fb_scale_y;

	if (fb_enabled) video_fb_enable(1, fb_num);

	fb_write_module_params();
}

---
static void video_calculate_cvt(int horiz_pixels, int vert_pixels, float refresh_rate, int reduced_blanking, vmode_custom_t *vmode);

static vmode_custom_t v_cur = {}, v_def = {}, v_pal = {}, v_ntsc = {};
static int vmode_def = 0, vmode_pal = 0, vmode_ntsc = 0;

static bool supports_pr()
{
	static uint16_t video_version = 0xffff;
	if (video_version == 0xffff) video_version = spi_uio_cmd(UIO_SET_VIDEO) & 1;
	return video_version != 0;
}

static bool supports_vrr()
{
	static uint16_t video_version = 0xffff;
	if (video_version == 0xffff) video_version = spi_uio_cmd(UIO_SET_VIDEO) & 2;
	return video_version != 0;
}

---
201:static bool supports_pr()
208:static bool supports_vrr()
2174:	if (!supports_vrr() || cfg.vsync_adjust) use_vrr = 0;
2380:	if (vmodes[mode].pr == 1 && !supports_pr()) mode = 8;
4335:	if (h_pixels > 2048 && !supports_pr())

πŸ“¦ Tool Call

2026-07-27 03:32:12

πŸ“¦ Tool: Bash Description: Read video_scaling_adjust and video_mode_adjust gating

cd Main_MiSTer && sed -n '2980,3040p' video.cpp && echo ====== && sed -n '3218,3320p' video.cpp

πŸ“¦ Tool Result

2026-07-27 03:32:12

❌ Error (Bash):

Exit code 1
/bin/bash: line 1: cd: Main_MiSTer: No such file or directory

πŸ“¦ Tool Call

2026-07-27 03:32:19

πŸ“¦ Tool: Bash Description: Read video_scaling_adjust and video_mode_adjust gating

sed -n '2980,3040p' video.cpp && echo ====== && sed -n '3218,3320p' video.cpp

πŸ“¦ Tool Result

2026-07-27 03:32:19

βœ… Result (Bash):

static void video_scaling_adjust(const VideoInfo *vi, const vmode_custom_t *vm)
{
	if (cfg.vscale_mode >= 4)
	{
		spi_uio_cmd16(UIO_SETHEIGHT, 0);
		spi_uio_cmd16(UIO_SETWIDTH, 0);
		return;
	}

	const uint32_t height = vi->rotated ? vi->width : vi->height;

	uint32_t scrh = vm->item[5];
	if (scrh)
	{
		if (cfg.vscale_mode && height)
		{
			uint32_t div = 1 << (cfg.vscale_mode - 1);
			uint32_t mag = (scrh*div) / height;
			scrh = (height * mag) / div;
			printf("Set vertical scaling to : %d\n", scrh);
			spi_uio_cmd16(UIO_SETHEIGHT, scrh);
		}
		else if (cfg.vscale_border)
		{
			uint32_t border = cfg.vscale_border * 2;
			if ((border + 100) > scrh) border = scrh - 100;
			scrh -= border;
			printf("Set max vertical resolution to : %d\n", scrh);
			spi_uio_cmd16(UIO_SETHEIGHT, scrh);
		}
		else
		{
			spi_uio_cmd16(UIO_SETHEIGHT, 0);
		}
	}

	uint32_t scrw = vm->item[1];
	if (scrw)
	{
		if (cfg.vscale_border && !(cfg.vscale_mode && height))
		{
			uint32_t border = cfg.vscale_border * 2;
			if ((border + 100) > scrw) border = scrw - 100;
			scrw -= border;
			printf("Set max horizontal resolution to : %d\n", scrw);
			spi_uio_cmd16(UIO_SETWIDTH, scrw);
		}
		else
		{
			spi_uio_cmd16(UIO_SETWIDTH, 0);
		}
	}

	minimig_set_adjust(2);
}

bool video_mode_select(uint32_t vtime, vmode_custom_t* out_mode)
{
	vmode_custom_t *v = &v_def;
	bool adjustable = true;

======
void video_mode_adjust(bool force)
{
	static bool rep_force = false;
	if (force) rep_force = true;

	VideoInfo video_info;

	const bool vid_changed = get_video_info(rep_force, &video_info);
	current_video_info = video_info;

	if (vid_changed || rep_force)
	{
		show_video_info(&video_info, &v_cur);
		set_yc_mode();
		spd_config_update();
	}
	rep_force = false;

	static int menu = 0;
	int menu_now = menu_present();
	if(menu != menu_now && cfg.spd_quirk < 2) spd_config_update();
	menu = menu_now;

	if (vid_changed && !is_menu())
	{
		if (cfg_has_video_sections())
		{
			cfg_parse();
			video_mode_load();
			user_io_send_buttons(1);
		}

		if ((cfg.vsync_adjust || cfg.vscale_mode >= 4))
		{
			const uint32_t vtime = video_info.vtime;

			printf("\033[1;33madjust_video_mode(%u): vsync_adjust=%d vscale_mode=%d.\033[0m\n", vtime, cfg.vsync_adjust, cfg.vscale_mode);

			vmode_custom_t new_mode;
			bool adjust = video_mode_select(vtime, &new_mode);

			video_resolution_adjust(&video_info, &new_mode);

			vmode_custom_t *v = &new_mode;
			double Fpix = 0;
			if (adjust)
			{
				Fpix = 100 * (v->item[1] + v->item[2] + v->item[3] + v->item[4]) * (v->item[5] + v->item[6] + v->item[7] + v->item[8]);
				Fpix /= vtime;
				if (Fpix < 2.f || Fpix > 300.f)
				{
					printf("Estimated Fpix(%.4f MHz) is outside supported range. Canceling auto-adjust.\n", Fpix);
					Fpix = 0;
				}

				float hz = 100000000.0f / vtime;
				if (cfg.refresh_min && hz < cfg.refresh_min)
				{
					printf("Estimated frame rate (%f Hz) is less than REFRESH_MIN(%f Hz). Canceling auto-adjust.\n", hz, cfg.refresh_min);
					Fpix = 0;
				}

				if (cfg.refresh_max && hz > cfg.refresh_max)
				{
					printf("Estimated frame rate (%f Hz) is more than REFRESH_MAX(%f Hz). Canceling auto-adjust.\n", hz, cfg.refresh_max);
					Fpix = 0;
				}
			}

			video_set_mode(v, Fpix);
			user_io_send_buttons(1);
			rep_force = true;
		}
		else if (use_vrr == VRR_MISTER)
		{
			int fr = 1000000000 / video_info.vtime;
			int fr_min = (fr - 10) / 10;
			int fr_max = (fr + 19) / 10;
			vrr_modes[VRR_MISTER].min_fr = fr_constrain(fr_min);
			vrr_modes[VRR_MISTER].max_fr = fr_constrain(fr_max);
			printf("*** video_mode_adjust: fr=%d.%d, fr_min=%d, fr_max=%d\n", fr / 10, fr % 10, vrr_modes[VRR_MISTER].min_fr, vrr_modes[VRR_MISTER].max_fr);
			video_set_mode(&v_def, 0);
			user_io_send_buttons(1);
			rep_force = true;
		}
		else if (cfg_has_video_sections()) // if we have video sections but aren't updating the resolution for other reasons, then do it here
		{
			video_set_mode(&v_def, 0);
			user_io_send_buttons(1);
			rep_force = true;
		}
		else
		{
			set_vfilter(1); // force update filters in case interlacing changed
		}

		video_scaling_adjust(&video_info, &v_cur);
	}
	else
	{
		set_vfilter(0); // update filters if flags have changed
	}
}

πŸ“¦ Tool Call

2026-07-27 03:34:43

πŸ“¦ Tool: Write File: /home/izzie/.claude/plans/sorted-giggling-pearl.md

# 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)


πŸ“¦ Tool Call

2026-07-27 03:34:46

πŸ“‹ Submitting plan for approval...


πŸ“¦ Tool Result

2026-07-27 03:38:54

βœ… Plan Approved

⏭ Skip to next user message

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:

  1. 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).
  2. 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.


Part A β€” ascal.vhd (interlaced output RTL)

File: Menu_MiSTer/sys/ascal.vhd (then replicate Γ—3).

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 unchanged
END IF;

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; END IF; -- <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)

  1. :398 capability probe: if(io_din[7:0] == 'h20) io_dout_sys <= 'b11; β†’ 'b111 (bit 2 = interlace support).
  2. :427 SET_VIDEO decode: 4: {ILACE, HEIGHT} <= {io_din[15], io_din[11:0]}; (bit 15 of the vact word is free).
  3. reg ILACE = 0; next to the other video regs (~:1053-1061).
  4. :791 .mode({ILACE, ~lowlat, LFB_EN ? LFB_FLT : |scaler_flt, 2'b00}), β€” now exactly 5 bits.

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


Part C β€” Main_MiSTer (Linux side)

  1. cfg key β€” cfg.h (:28, after direct_video): uint8_t direct_video_interlace;. cfg.cpp (:75): { "DIRECT_VIDEO_INTERLACE", (void*)(&(cfg.direct_video_interlace)), UINT8, 0, 1 },.

  2. 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).

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

  4. vmode struct + mode selection β€” video.cpp:178: uint32_t unused[6] β†’ uint32_t ilace; uint32_t unused[5]; (static_assert at :186 keeps layout honest). In video_mode_load()'s direct_video branch (:2600-2614):

    int ilace = (cfg.direct_video_interlace && supports_ilace()) ? 1 : 0;
    int mode = cfg.menu_pal ? 2 : 0;
    if (!ilace && cfg.forced_scandoubler) mode++;   // interlace key overrides scandoubler; log a note
    ...
    v_def.param.ilace = ilace;

    15 kHz tvmodes[0]/[2] + ilace β‡’ 480i/576i. Re-entry via cfg_has_video_sections() re-derives the flag; VRR unreachable under DV.

  5. 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).

  6. 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_t src_h = height << (vi->interlaced ? 1 : 0);   // frame height
    if (src_h > scrh) spi_uio_cmd16(UIO_SETHEIGHT, src_h >> 1);  // exact 2:1 weave window
    else /* 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.

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

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

  1. ascal.vhd in Menu_MiSTer (Part A) β†’ copy to ao486/Minimig; PSX via unix2dos-style CRLF-preserving copy.
  2. sys_top.v Γ—4 (Part B; PSX offsets).
  3. 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.

πŸ“¦ Tool Call

2026-07-27 03:38:59

πŸ“¦ Tool: ToolSearch

{
  "query": "select:TaskCreate,TaskUpdate",
  "max_results": 2
}

πŸ“¦ Tool Result

2026-07-27 03:38:59

βœ… Result (ToolSearch):

[{'type': 'tool_reference', 'tool_name': 'TaskCreate'}, {'type': 'tool_reference', 'tool_name': 'TaskUpdate'}]

πŸ“¦ Tool Call

2026-07-27 03:39:04

πŸ“¦ Tool: TaskCreate

{
  "subject": "Implement interlaced output RTL in ascal.vhd (Menu_MiSTer)",
  "description": "Part A of plan: mode(4) enable, field registers, per-field vtotal, half-line vsync, bias + decimation vertical sampling, input-side i_ovsize doubling, guards.",
  "activeForm": "Editing ascal.vhd interlace RTL"
}

πŸ“¦ Tool Result

2026-07-27 03:39:04

βœ… Result (TaskCreate):

Task #1 created successfully: Implement interlaced output RTL in ascal.vhd (Menu_MiSTer)


πŸ“¦ Tool Call

2026-07-27 03:39:06

πŸ“¦ Tool: TaskCreate

{
  "subject": "Replicate ascal.vhd to ao486/Minimig/PSX",
  "description": "Copy edited ascal.vhd verbatim to ao486_MiSTer and Minimig-AGA_MiSTer; PSX copy with CRLF line endings preserved.",
  "activeForm": "Replicating ascal.vhd"
}

πŸ“¦ Tool Result

2026-07-27 03:39:06

βœ… Result (TaskCreate):

Task #2 created successfully: Replicate ascal.vhd to ao486/Minimig/PSX


πŸ“¦ Tool Call

2026-07-27 03:39:08

πŸ“¦ Tool: TaskCreate

{
  "subject": "Edit sys_top.v in all 4 repos (ILACE plumbing)",
  "description": "Part B: capability bit ('b111), SET_VIDEO cnt4 bit15 decode, reg ILACE, 5-bit mode concat. PSX at ~-15 line offset.",
  "activeForm": "Editing sys_top.v files"
}

πŸ“¦ Tool Result

2026-07-27 03:39:08

βœ… Result (TaskCreate):

Task #3 created successfully: Edit sys_top.v in all 4 repos (ILACE plumbing)


πŸ“¦ Tool Call

2026-07-27 03:39:10

πŸ“¦ Tool: TaskCreate

{
  "subject": "Implement Main_MiSTer Linux-side changes",
  "description": "Part C: cfg key DIRECT_VIDEO_INTERLACE, supports_ilace(), video_dv_scaler_forced() + CONF_VGA_FB force, vmode ilace field + mode selection, SET_VIDEO bit, VSET weave window in video_scaling_adjust, menu fb doubling, heuristic gating.",
  "activeForm": "Editing Main_MiSTer"
}

πŸ“¦ Tool Result

2026-07-27 03:39:10

βœ… Result (TaskCreate):

Task #4 created successfully: Implement Main_MiSTer Linux-side changes


πŸ“¦ Tool Call

2026-07-27 03:39:11

πŸ“¦ Tool: TaskCreate

{
  "subject": "Verify: syntax checks, regression greps, build",
  "description": "ghdl syntax check if available, grep invariants (no bare o_ivsize in vertical Bresenham, 5-bit mode concat), Main_MiSTer compile/syntax check, final review of progressive-regression property.",
  "activeForm": "Verifying changes"
}

πŸ“¦ Tool Result

2026-07-27 03:39:11

βœ… Result (TaskCreate):

Task #5 created successfully: Verify: syntax checks, regression greps, build


πŸ“¦ Tool Call

2026-07-27 03:39:14

πŸ“¦ Tool: TaskUpdate

{
  "taskId": "1",
  "status": "in_progress"
}

πŸ“¦ Tool Result

2026-07-27 03:39:14

βœ… Result (TaskUpdate):

Updated task #1 status


πŸ“¦ Tool Call

2026-07-27 03:39:21

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd (offset: 72, limit: 25)


πŸ“¦ Tool Result

2026-07-27 03:39:21

βœ… Result (Read):

72	--------------------------------------------
73	
74	LIBRARY ieee;
75	USE ieee.std_logic_1164.ALL;
76	USE ieee.numeric_std.ALL;
77	
78	-- MODE[2:0]
79	-- 000 : Nearest
80	-- 001 : Bilinear
81	-- 010 : Sharp Bilinear
82	-- 011 : Bicubic
83	-- 100 : Polyphase
84	-- 101 : TBD
85	-- 110 : TBD
86	-- 111 : TBD
87	
88	-- MODE[3]
89	-- 0 : Direct. Single framebuffer.
90	-- 1 : Triple buffering
91	
92	-- MODE[4] : TBD
93	
94	-- MASK      : Enable / Disable selected interpoler
95	--             0:Nearest 1:Bilinear 2:SharpBilinear 3:Bicubic 4:Polyphase
96	-- RAMBASE   : RAM base address for framebuffer

πŸ“¦ Tool Call

2026-07-27 03:39:22

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd (offset: 440, limit: 105)


πŸ“¦ Tool Result

2026-07-27 03:39:22

βœ… Result (Read):

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

πŸ“¦ Tool Call

2026-07-27 03:39:22

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd (offset: 2884, limit: 40)


πŸ“¦ Tool Result

2026-07-27 03:39:22

βœ… Result (Read):

2884					END CASE;
2885	
2886					IF fracnn_v = '0' THEN
2887						o_vpix_outer<=(pixq_v(0), pixq_v(2), pixq_v(3));
2888						o_vpix_inner(0)<=pixq_v(1);
2889					ELSE
2890						o_vpix_outer<=(pixq_v(0), pixq_v(1), pixq_v(3));
2891						o_vpix_inner(0)<=pixq_v(2);
2892					END IF;
2893	
2894					-- CYCLE 3-7
2895					o_vpix_inner(1 TO 5)<=o_vpix_inner(0 TO 4);
2896	
2897					-- CYCLE 8
2898					IF to_integer(o_vacpt)>o_ivsize THEN
2899						IF fracnn_v = '0' THEN
2900							o_vpixq_pre<=(o_vpix_outer(0), o_vpix_inner(5), o_vpix_inner(5), o_vpix_inner(5));
2901						ELSE
2902							o_vpixq_pre<=(o_vpix_outer(0), o_vpix_outer(1), o_vpix_outer(1), o_vpix_outer(1));
2903						END IF;
2904					ELSIF to_integer(o_vacpt)=o_ivsize THEN
2905						IF fracnn_v = '0' THEN
2906							o_vpixq_pre<=(o_vpix_outer(0), o_vpix_inner(5), o_vpix_outer(1), o_vpix_outer(1));
2907						ELSE
2908							o_vpixq_pre<=(o_vpix_outer(0), o_vpix_outer(1), o_vpix_inner(5), o_vpix_inner(5));
2909						END IF;
2910					ELSE
2911						IF fracnn_v = '0' THEN
2912							o_vpixq_pre<=(o_vpix_outer(0), o_vpix_inner(5), o_vpix_outer(1), o_vpix_outer(2));
2913						ELSE
2914							o_vpixq_pre<=(o_vpix_outer(0), o_vpix_outer(1), o_vpix_inner(5), o_vpix_outer(2));
2915						END IF;
2916					END IF;
2917	
2918					-- CYCLE 9
2919					o_vpixq<=o_vpixq_pre;
2920	
2921					-- BILINEAR / SHARP BILINEAR -----------------------
2922					-- C8 : Pre-calc Sharp Bilinear
2923					o_v_sbil_t<=sbil_frac1(o_vfrac);

πŸ€– Claude

2026-07-27 03:40:38

Now implementing Part A. First, quick type checks:


πŸ“¦ Tool Call

2026-07-27 03:40:40

πŸ“¦ Tool: Bash Description: Check uint12 subtype and all o_ivsize uses

grep -n "SUBTYPE uint12\|uint12 IS" Menu_MiSTer/sys/ascal.vhd; grep -n "o_ivsize" Menu_MiSTer/sys/ascal.vhd; grep -n "o_vacpt " Menu_MiSTer/sys/ascal.vhd | head -3; grep -n "SIGNAL o_vacpt\|o_vacptl" Menu_MiSTer/sys/ascal.vhd | head -5

πŸ“¦ Tool Result

2026-07-27 03:40:40

βœ… Result (Bash):

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: Bash Description: Check uint12 subtype and all o_ivsize uses

cd /home/izzie/Desktop/mister-scaler && grep -n "SUBTYPE uint12" Menu_MiSTer/sys/ascal.vhd; grep -n "o_ivsize" Menu_MiSTer/sys/ascal.vhd; grep -n "SIGNAL o_vacpt" Menu_MiSTer/sys/ascal.vhd

πŸ“¦ Tool Result

2026-07-27 03:40:44

βœ… Result (Bash):

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);

πŸ“¦ Tool Call

2026-07-27 03:40:48

πŸ“¦ Tool: Bash Description: Find o_vacpt declaration

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:


πŸ“¦ Tool Call

2026-07-27 03:40:55

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

-- MODE[4] : TBD

New:

-- 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)


πŸ“¦ Tool Call

2026-07-27 03:41:00

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

	SIGNAL o_vtotal,o_vsstart,o_vsend : uint12;

New:

	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)


πŸ“¦ Tool Call

2026-07-27 03:41:07

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

			o_vrr    <=vrr;
			o_vrrmax <= vrrmax;

New:

			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)


πŸ“¦ Tool Call

2026-07-27 03:41:11

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

			o_bob_deint <= bob_deint;

New:

			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)


πŸ“¦ Tool Call

2026-07-27 03:41:18

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

			-- 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):


πŸ“¦ Tool Call

2026-07-27 03:41:23

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

				i_ohsize<=o_hsize; -- <ASYNC>
				i_ovsize<=o_vsize; -- <ASYNC>

New:

				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: Bash Description: Check i_ovsize declaration and uses

grep -n "i_ovsize\b" Menu_MiSTer/sys/ascal.vhd | head; grep -n "SIGNAL i_ohsize\|i_ovsize :" Menu_MiSTer/sys/ascal.vhd

πŸ“¦ Tool Result

2026-07-27 03:41:29

βœ… Result (Bash):

398:	SIGNAL i_ohsize,i_ovsize : uint12;
1215:			i_head(15 DOWNTO 0) <="0000" & to_unsigned(i_ovsize,12);
1327:				i_vdown<=to_std_logic(i_vsize>i_ovsize AND DOWNSCALE); --V downscale
1337:				ELSIF i_ovsize<2*i_vmaxmin THEN
1350:					i_ovsize<=2*o_vsize; -- <ASYNC>
1352:					i_ovsize<=o_vsize; -- <ASYNC>
1359:					IF (i_vacc + 2*i_ovsize) < 2*i_vsize THEN
1360:						i_vacc<=(i_vacc + 2*i_ovsize) MOD 8192;
1363:						i_vacc<=(i_vacc + 2*i_ovsize - 2*i_vsize + 8192) MOD 8192;
1369:					 i_vacc<=(i_vsize - i_ovsize + 8192) MOD 8192;
398:	SIGNAL i_ohsize,i_ovsize : uint12;

πŸ“¦ Tool Call

2026-07-27 03:41:39

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

				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):


πŸ“¦ Tool Call

2026-07-27 03:41:45

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

			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)


πŸ“¦ Tool Call

2026-07-27 03:42:00

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

					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)


πŸ“¦ Tool Call

2026-07-27 03:42:01

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

						o_vacc     <=o_vacc_ini;
						o_vacc_next<=o_vacc_ini + 2*o_ivsize;

New:

						o_vacc     <=o_vacc_ini;
						o_vacc_next<=o_vacc_ini + 2*o_ivsize_eff;

πŸ“¦ Tool Result

2026-07-27 03:42:02

βœ… 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:42:04

Now o_rline, the weave address, and the preload block:


πŸ“¦ Tool Call

2026-07-27 03:42:06

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

			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 Call

2026-07-27 03:42:10

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

				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;

New:

				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
		
... [363 chars total, truncated]

πŸ“¦ Tool Result

2026-07-27 03:42:10

❌ Error (Edit):

<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: Bash Description: 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';

πŸ“¦ Tool Call

2026-07-27 03:42:31

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

					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
			
... [399 chars total, truncated]

New:

					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_pr
... [1105 chars total, truncated]

πŸ“¦ Tool Result

2026-07-27 03:42:31

βœ… 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:42:47

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

			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)


πŸ“¦ Tool Call

2026-07-27 03:42:56

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

				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)


πŸ“¦ Tool Call

2026-07-27 03:42:58

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

			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)


πŸ“¦ Tool Call

2026-07-27 03:43:03

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

				IF to_integer(o_vacpt)>o_ivsize THEN

New:

				IF to_integer(o_vacpt)>o_ivsize_eff THEN

πŸ“¦ Tool Result

2026-07-27 03:43:03

βœ… 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:43:04

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/ascal.vhd Old:

				ELSIF to_integer(o_vacpt)=o_ivsize THEN

New:

				ELSIF to_integer(o_vacpt)=o_ivsize_eff THEN

πŸ“¦ Tool Result

2026-07-27 03:43:04

βœ… 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:43:12

πŸ“¦ Tool: Bash Description: Check for VHDL tools and remaining bare o_ivsize uses

which ghdl nvc 2>/dev/null; echo "---"; grep -n "o_ivsize\b" Menu_MiSTer/sys/ascal.vhd | grep -v o_ivsize_eff

πŸ“¦ Tool Result

2026-07-27 03:43:12

βœ… Result (Bash):

/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: Bash Description: 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: Bash Description: 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: Bash Description: Replicate edited ascal.vhd to all repos (CRLF for PSX)

cp Menu_MiSTer/sys/ascal.vhd ao486_MiSTer/sys/ascal.vhd && cp Menu_MiSTer/sys/ascal.vhd Minimig-AGA_MiSTer/sys/ascal.vhd && sed 's/$/\r/' Menu_MiSTer/sys/ascal.vhd > PSX_MiSTer/sys/ascal.vhd && md5sum */sys/ascal.vhd && diff <(tr -d '\r' < PSX_MiSTer/sys/ascal.vhd) Menu_MiSTer/sys/ascal.vhd && echo "PSX CRLF copy matches"

πŸ“¦ Tool Result

2026-07-27 03:43:43

βœ… Result (Bash):

b1a56460a28202ea973fcdb48d3be311  ao486_MiSTer/sys/ascal.vhd
b1a56460a28202ea973fcdb48d3be311  Menu_MiSTer/sys/ascal.vhd
b1a56460a28202ea973fcdb48d3be311  Minimig-AGA_MiSTer/sys/ascal.vhd
95881e74bd116c7423c56346b85c40cd  PSX_MiSTer/sys/ascal.vhd
PSX CRLF copy matches

πŸ€– Claude Progress

2026-07-27 03:43:53

ascal.vhd replicated identically (PSX CRLF-preserved, verified). Now sys_top.v edits β€” checking exact contexts in Menu and PSX first: πŸ“¦ Tool: TaskUpdate

{
  "taskId": "2",
  "status": "completed"
}

πŸ“¦ Tool: TaskUpdate

{
  "taskId": "3",
  "status": "in_progress"
}

πŸ“¦ Tool Result

2026-07-27 03:43:54

βœ… Result (TaskUpdate):

Updated task #3 status


πŸ“¦ Tool Result

2026-07-27 03:43:54

βœ… Result (TaskUpdate):

Updated task #2 status


πŸ“¦ Tool Call

2026-07-27 03:43:59

πŸ“¦ Tool: Bash Description: Locate the 4 edit sites in Menu and PSX sys_top.v

grep -n "io_dout_sys <= 'b11\|4: HEIGHT\|reg  *HDMI_PR\|HDMI_PR = 0\|\.mode\b\|reg .*vrr_mode" Menu_MiSTer/sys/sys_top.v PSX_MiSTer/sys/sys_top.v; echo ---; sed -n '390,400p' Menu_MiSTer/sys/sys_top.v; echo ---PSX---; sed -n '380,420p' PSX_MiSTer/sys/sys_top.v

πŸ“¦ Tool Result

2026-07-27 03:43:59

βœ… Result (Bash):

PSX_MiSTer/sys/sys_top.v:335:reg        vrr_mode = 0;
PSX_MiSTer/sys/sys_top.v:394:			if(io_din[7:0] == 'h20) io_dout_sys <= 'b11;
PSX_MiSTer/sys/sys_top.v:415:						4: HEIGHT <= io_din[11:0];
PSX_MiSTer/sys/sys_top.v:776:		.mode     ({~lowlat,LFB_EN ? LFB_FLT : |scaler_flt,2'b00}),
PSX_MiSTer/sys/sys_top.v:1046:reg         HDMI_PR = 0;
Menu_MiSTer/sys/sys_top.v:336:reg        vrr_mode = 0;
Menu_MiSTer/sys/sys_top.v:396:				io_dout_sys <= 'b11;
Menu_MiSTer/sys/sys_top.v:398:			if(io_din[7:0] == 'h20) io_dout_sys <= 'b11;
Menu_MiSTer/sys/sys_top.v:427:						4: HEIGHT <= io_din[11:0];
Menu_MiSTer/sys/sys_top.v:791:		.mode     ({~lowlat,LFB_EN ? LFB_FLT : |scaler_flt,2'b00}),
Menu_MiSTer/sys/sys_top.v:1061:reg         HDMI_PR = 0;
---
				acx1 <= 3;
				acx2 <= 1;
				acy0 <= -24'd6216759;
				acy1 <=  24'd6143386;
				acy2 <= -24'd2023767;
				areset <= 1;
				io_dout_sys <= 'b11;
			end
			if(io_din[7:0] == 'h20) io_dout_sys <= 'b11;
`ifdef MISTER_DISABLE_ADAPTIVE
			if(io_din[7:0] == 'h2B) io_dout_sys <= {fb_en, sl_r, 4'b0110};
---PSX---
			cmd <= io_din[7:0];
			cnt <= 0;
			if(io_din[7:0] == 'h30) vs_wait <= 1;
			if(io_din[7:0] == 'h39) begin
				aflt_rate <= 7056000;
				acx  <= 4258969;
				acx0 <= 3;
				acx1 <= 3;
				acx2 <= 1;
				acy0 <= -24'd6216759;
				acy1 <=  24'd6143386;
				acy2 <= -24'd2023767;
				areset <= 1;
			end
			if(io_din[7:0] == 'h20) io_dout_sys <= 'b11;
`ifndef MISTER_DEBUG_NOHDMI
			if(io_din[7:0] == 'h40) io_dout_sys <= fb_crc;
`endif
			if(io_din[7:0] == 'h42) io_dout_sys <= {1'b1, frame_cnt};
		end
		else begin
			cnt <= cnt + 1'd1;
			if(cmd == 1) begin
				cfg <= io_din;
				cfg_set <= 1;
				scaler_out <= 1;
			end
			if(cmd == 'h20) begin
				cfg_set <= 0;
				if(cnt<8) begin
					case(cnt[2:0])
						0: {HDMI_PR,vrr_mode,WIDTH} <= {io_din[15:14], io_din[11:0]};
						1: HFP    <= io_din[11:0];
						2: HS     <= {io_din[15], io_din[11:0]};
						3: HBP    <= io_din[11:0];
						4: HEIGHT <= io_din[11:0];
						5: VFP    <= io_din[11:0];
						6: VS     <= {io_din[15],io_din[11:0]};
						7: VBP    <= io_din[11:0];
					endcase
`ifndef MISTER_DEBUG_NOHDMI

πŸ“¦ Tool Call

2026-07-27 03:44:06

πŸ“¦ Tool: Bash Description: Confirm sys_top.v identity and line endings

md5sum */sys/sys_top.v; file PSX_MiSTer/sys/sys_top.v Menu_MiSTer/sys/sys_top.v

πŸ“¦ Tool Result

2026-07-27 03:44:06

βœ… Result (Bash):

f22545a2900140a6234fa46de3cdb45f  ao486_MiSTer/sys/sys_top.v
f22545a2900140a6234fa46de3cdb45f  Menu_MiSTer/sys/sys_top.v
f22545a2900140a6234fa46de3cdb45f  Minimig-AGA_MiSTer/sys/sys_top.v
ef6b86c3409a0f63ef0c18d7070382c4  PSX_MiSTer/sys/sys_top.v
PSX_MiSTer/sys/sys_top.v:  ASCII text
Menu_MiSTer/sys/sys_top.v: ASCII text

πŸ“¦ Tool Call

2026-07-27 03:44:11

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/sys_top.v Old:

			if(io_din[7:0] == 'h20) io_dout_sys <= 'b11;

New:

			if(io_din[7:0] == 'h20) io_dout_sys <= 'b111;

πŸ“¦ Tool Result

2026-07-27 03:44:11

βœ… Result (Edit):

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:13

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/sys_top.v Old:

						4: HEIGHT <= io_din[11:0];

New:

						4: {ILACE,HEIGHT} <= {io_din[15],io_din[11:0]};

πŸ“¦ Tool Result

2026-07-27 03:44:13

βœ… Result (Edit):

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:14

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/sys_top.v Old:

reg         HDMI_PR = 0;

New:

reg         HDMI_PR = 0;
reg         ILACE = 0;

πŸ“¦ Tool Result

2026-07-27 03:44:14

βœ… Result (Edit):

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:16

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Menu_MiSTer/sys/sys_top.v Old:

		.mode     ({~lowlat,LFB_EN ? LFB_FLT : |scaler_flt,2'b00}),

New:

		.mode     ({ILACE,~lowlat,LFB_EN ? LFB_FLT : |scaler_flt,2'b00}),

πŸ“¦ Tool Result

2026-07-27 03:44:16

βœ… Result (Edit):

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: Bash Description: Copy edited sys_top.v to ao486 and Minimig

cp Menu_MiSTer/sys/sys_top.v ao486_MiSTer/sys/sys_top.v && cp Menu_MiSTer/sys/sys_top.v Minimig-AGA_MiSTer/sys/sys_top.v && md5sum Menu_MiSTer/sys/sys_top.v ao486_MiSTer/sys/sys_top.v Minimig-AGA_MiSTer/sys/sys_top.v

πŸ“¦ Tool Result

2026-07-27 03:44:34

βœ… Result (Bash):

349022d19d41cdd64bf086025444a9d3  Menu_MiSTer/sys/sys_top.v
349022d19d41cdd64bf086025444a9d3  ao486_MiSTer/sys/sys_top.v
349022d19d41cdd64bf086025444a9d3  Minimig-AGA_MiSTer/sys/sys_top.v

πŸ“¦ Tool Call

2026-07-27 03:44:34

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/PSX_MiSTer/sys/sys_top.v (offset: 392, limit: 4)


πŸ“¦ Tool Result

2026-07-27 03:44:35

βœ… Result (Read):

392					areset <= 1;
393				end
394				if(io_din[7:0] == 'h20) io_dout_sys <= 'b11;
395	`ifndef MISTER_DEBUG_NOHDMI

πŸ“¦ Tool Call

2026-07-27 03:44:39

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/PSX_MiSTer/sys/sys_top.v Old:

			if(io_din[7:0] == 'h20) io_dout_sys <= 'b11;

New:

			if(io_din[7:0] == 'h20) io_dout_sys <= 'b111;

πŸ“¦ Tool Result

2026-07-27 03:44:39

βœ… Result (Edit):

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)


πŸ“¦ Tool Call

2026-07-27 03:44:41

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/PSX_MiSTer/sys/sys_top.v Old:

						4: HEIGHT <= io_din[11:0];

New:

						4: {ILACE,HEIGHT} <= {io_din[15],io_din[11:0]};

πŸ“¦ Tool Result

2026-07-27 03:44:41

βœ… Result (Edit):

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)


πŸ“¦ Tool Call

2026-07-27 03:44:42

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/PSX_MiSTer/sys/sys_top.v Old:

reg         HDMI_PR = 0;

New:

reg         HDMI_PR = 0;
reg         ILACE = 0;

πŸ“¦ Tool Result

2026-07-27 03:44:42

βœ… Result (Edit):

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)


πŸ“¦ Tool Call

2026-07-27 03:44:44

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/PSX_MiSTer/sys/sys_top.v Old:

		.mode     ({~lowlat,LFB_EN ? LFB_FLT : |scaler_flt,2'b00}),

New:

		.mode     ({ILACE,~lowlat,LFB_EN ? LFB_FLT : |scaler_flt,2'b00}),

πŸ“¦ Tool Result

2026-07-27 03:44:44

βœ… Result (Edit):

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)


πŸ“¦ Tool Call

2026-07-27 03:44:54

πŸ“¦ Tool: TaskUpdate

{
  "taskId": "3",
  "status": "completed"
}

πŸ“¦ Tool Result

2026-07-27 03:44:54

βœ… Result (TaskUpdate):

Updated task #3 status


πŸ“¦ Tool Call

2026-07-27 03:44:54

πŸ“¦ Tool: TaskUpdate

{
  "taskId": "4",
  "status": "in_progress"
}

πŸ“¦ Tool Result

2026-07-27 03:44:55

βœ… Result (TaskUpdate):

Updated task #4 status


πŸ“¦ Tool Call

2026-07-27 03:45:00

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp (offset: 145, limit: 46)


πŸ“¦ Tool Result

2026-07-27 03:45:00

βœ… Result (Read):

145		{{ 640, 16, 96, 48, 480,  8, 4, 33 }, 25.175, 0, 0 }, //NTSC 31K
146		{{ 640, 30, 60, 70, 288,  6, 4, 14 }, 12.587, 0, 0 }, //PAL 15K
147		{{ 640, 16, 96, 48, 576,  2, 4, 42 }, 25.175, 0, 0 }, //PAL 31K
148	};
149	
150	// named aliases for vmode_custom_t items
151	struct vmode_custom_param_t
152	{
153		uint32_t mode;
154	
155		// [1]
156		uint32_t hact;
157		uint32_t hfp;
158		uint32_t hs;
159		uint32_t hbp;
160	
161		// [5]
162		uint32_t vact;
163		uint32_t vfp;
164		uint32_t vs;
165		uint32_t vbp;
166	
167		// [9]
168		uint32_t pll[12];
169	
170		// [21]
171		uint32_t hpol;
172		uint32_t vpol;
173		uint32_t vic;
174		uint32_t rb;
175		uint32_t pr;
176	
177		// [26]
178		uint32_t unused[6];
179	};
180	
181	struct vmode_custom_t
182	{
183		union // anonymous
184		{
185			vmode_custom_param_t param;
186			uint32_t item[32];
187		};
188	
189		double Fpix;
190	};

πŸ“¦ Tool Call

2026-07-27 03:45:01

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/cfg.h (offset: 10, limit: 25)


πŸ“¦ Tool Result

2026-07-27 03:45:01

βœ… Result (Read):

10	//// type definitions ////
11	typedef struct {
12		uint32_t keyrah_mode;
13		uint8_t forced_scandoubler;
14		uint8_t key_menu_as_rgui;
15		uint8_t reset_combo;
16		uint8_t csync;
17		uint8_t vga_scaler;
18		uint8_t vga_sog;
19		uint8_t hdmi_audio_96k;
20		uint8_t dvi_mode;
21		uint8_t hdmi_limited;
22		uint8_t hdmi_cec;
23		uint8_t hdmi_cec_sleep;
24		uint8_t hdmi_cec_wake;
25		uint8_t hdmi_cec_input_mode;
26		uint8_t hdmi_cec_power_on;
27		float hdmi_cec_clock;
28		uint8_t direct_video;
29		uint8_t video_info;
30		float refresh_min;
31		float refresh_max;
32		uint8_t controller_info;
33		uint8_t vsync_adjust;
34		uint8_t kbd_nomouse;

πŸ“¦ Tool Call

2026-07-27 03:45:02

πŸ“¦ Tool: Bash Description: Locate cfg.cpp ini entry and user_io buttons code

grep -n "DIRECT_VIDEO" Main_MiSTer/cfg.cpp | head -4; sed -n '3010,3032p' Main_MiSTer/user_io.cpp

πŸ“¦ Tool Result

2026-07-27 03:45:02

βœ… Result (Bash):

74:	{ "DIRECT_VIDEO", (void*)(&(cfg.direct_video)), UINT8, 0, 2 },

	cur_btn = fpga_get_buttons();

	if (user_io_menu_button()) map |= BUTTON1;
	if (user_io_user_button()) map |= BUTTON2;
	if (kbd_reset || kbd_reset_ovr) map |= BUTTON2;

	if (cfg.vga_scaler) map |= CONF_VGA_SCALER;
	if (cfg.vga_sog) map |= CONF_VGA_SOG;
	if (cfg.csync) map |= CONF_CSYNC;
	if (cfg.vga_mode_int == 1) map |= CONF_YPBPR;
	if (cfg.forced_scandoubler) map |= CONF_FORCED_SCANDOUBLER;
	if (cfg.hdmi_audio_96k) map |= CONF_AUDIO_96K;
	if (cfg.dvi_mode == 1) map |= CONF_DVI;
	if (cfg.hdmi_limited & 1) map |= CONF_HDMI_LIMITED1;
	if (cfg.hdmi_limited & 2) map |= CONF_HDMI_LIMITED2;
	if (cfg.direct_video) map |= CONF_DIRECT_VIDEO;
	if (cfg.direct_video == 2) map |= CONF_DIRECT_VIDEO2;
	if (vga_fb) map |= CONF_VGA_FB;

	if ((map != key_map) || force)
	{
		const char *name = get_rbf_path();

πŸ“¦ Tool Call

2026-07-27 03:45:06

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/cfg.h Old:

	uint8_t direct_video;
	uint8_t video_info;

New:

	uint8_t direct_video;
	uint8_t direct_video_interlace;
	uint8_t video_info;

πŸ“¦ Tool Result

2026-07-27 03:45:06

βœ… Result (Edit):

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: Bash Description: View cfg.cpp around DIRECT_VIDEO entry

sed -n '72,76p' Main_MiSTer/cfg.cpp

πŸ“¦ Tool Result

2026-07-27 03:45:07

βœ… Result (Bash):

{ "FB_TERMINAL", (void*)(&(cfg.fb_terminal)), UINT8, 0, 1 },
	{ "OSD_TIMEOUT", (void*)(&(cfg.osd_timeout)), INT16, 0, 3600 },
	{ "DIRECT_VIDEO", (void*)(&(cfg.direct_video)), UINT8, 0, 2 },
	{ "OSD_ROTATE", (void*)(&(cfg.osd_rotate)), UINT8, 0, 2 },
	{ "DEADZONE", (void*)(&(cfg.controller_deadzone)), STRINGARR, sizeof(cfg.controller_deadzone) / sizeof(*cfg.controller_deadzone), sizeof(*cfg.controller_deadzone) },

πŸ“¦ Tool Call

2026-07-27 03:45:10

πŸ“¦ Tool: Bash Description: Prep for cfg.cpp edit

sed -n '1,10p' Main_MiSTer/cfg.cpp >/dev/null; grep -c . Main_MiSTer/cfg.cpp

πŸ“¦ Tool Result

2026-07-27 03:45:10

βœ… Result (Bash):

712

πŸ“¦ Tool Call

2026-07-27 03:45:13

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/cfg.cpp (offset: 73, limit: 3)


πŸ“¦ Tool Result

2026-07-27 03:45:13

βœ… Result (Read):

73		{ "OSD_TIMEOUT", (void*)(&(cfg.osd_timeout)), INT16, 0, 3600 },
74		{ "DIRECT_VIDEO", (void*)(&(cfg.direct_video)), UINT8, 0, 2 },
75		{ "OSD_ROTATE", (void*)(&(cfg.osd_rotate)), UINT8, 0, 2 },

πŸ“¦ Tool Call

2026-07-27 03:45:18

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/cfg.cpp Old:

	{ "DIRECT_VIDEO", (void*)(&(cfg.direct_video)), UINT8, 0, 2 },

New:

	{ "DIRECT_VIDEO", (void*)(&(cfg.direct_video)), UINT8, 0, 2 },
	{ "DIRECT_VIDEO_INTERLACE", (void*)(&(cfg.direct_video_interlace)), UINT8, 0, 1 },

πŸ“¦ Tool Result

2026-07-27 03:45:18

βœ… Result (Edit):

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)


πŸ“¦ Tool Call

2026-07-27 03:45:22

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp Old:

	// [26]
	uint32_t unused[6];
};

New:

	// [26]
	uint32_t ilace;
	uint32_t unused[5];
};

πŸ“¦ Tool Result

2026-07-27 03:45:23

βœ… 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:45:23

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp (offset: 200, limit: 18)


πŸ“¦ Tool Result

2026-07-27 03:45:23

βœ… Result (Read):

200	static int vmode_def = 0, vmode_pal = 0, vmode_ntsc = 0;
201	
202	static bool supports_pr()
203	{
204		static uint16_t video_version = 0xffff;
205		if (video_version == 0xffff) video_version = spi_uio_cmd(UIO_SET_VIDEO) & 1;
206		return video_version != 0;
207	}
208	
209	static bool supports_vrr()
210	{
211		static uint16_t video_version = 0xffff;
212		if (video_version == 0xffff) video_version = spi_uio_cmd(UIO_SET_VIDEO) & 2;
213		return video_version != 0;
214	}
215	
216	static uint32_t getPLLdiv(uint32_t div)
217	{

πŸ“¦ Tool Call

2026-07-27 03:45:30

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp Old:

static bool supports_vrr()
{
	static uint16_t video_version = 0xffff;
	if (video_version == 0xffff) video_version = spi_uio_cmd(UIO_SET_VIDEO) & 2;
	return video_version != 0;
}

New:

static bool supports_vrr()
{
	static uint16_t video_version = 0xffff;
	if (video_version == 0xffff) video_version = spi_uio_cmd(UIO_SET_VIDEO) & 2;
	return video_version != 0;
}

static bool supports_
... [436 chars total, truncated]

πŸ“¦ Tool Result

2026-07-27 03:45: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:45:33

πŸ“¦ Tool: Bash Description: Find declaration spot in video.h

grep -n "video_fb_state\|int video_" Main_MiSTer/video.h | head

πŸ“¦ Tool Result

2026-07-27 03:45:33

βœ… Result (Bash):

67:int video_fb_state();
69:int video_bg_has_picture();
70:int video_chvt(int num);

πŸ“¦ Tool Call

2026-07-27 03:45:37

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.h (offset: 64, limit: 10)


πŸ“¦ Tool Result

2026-07-27 03:45:37

βœ… Result (Read):

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

πŸ“¦ Tool Call

2026-07-27 03:45:40

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.h Old:

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)


πŸ“¦ Tool Call

2026-07-27 03:45:44

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp (offset: 2614, limit: 28)


πŸ“¦ Tool Result

2026-07-27 03:45:44

βœ… Result (Read):

2614		{
2615			int mode = cfg.menu_pal ? 2 : 0;
2616			if (cfg.forced_scandoubler) mode++;
2617	
2618			memset(&v_def, 0, sizeof(v_def));
2619	
2620			v_def.item[0] = mode;
2621			for (int i = 0; i < 8; i++) v_def.item[i + 1] = tvmodes[mode].vpar[i];
2622			setPLL(tvmodes[mode].Fpix, &v_def);
2623	
2624			vmode_def = 1;
2625			vmode_pal = 0;
2626			vmode_ntsc = 0;
2627		}
2628		else
2629		{
2630			vmode_def = 0;
2631			if (!strlen(cfg.video_conf) && !strlen(cfg.video_conf_pal) && !strlen(cfg.video_conf_ntsc))
2632			{
2633				vmode_def = get_edid_vmode(&v_def);
2634			}
2635	
2636			if (!vmode_def)
2637			{
2638				vmode_def = store_custom_video_mode(cfg.video_conf, &v_def);
2639				vmode_pal = store_custom_video_mode(cfg.video_conf_pal, &v_pal);
2640				vmode_ntsc = store_custom_video_mode(cfg.video_conf_ntsc, &v_ntsc);
2641			}

πŸ“¦ Tool Call

2026-07-27 03:45:51

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp Old:

		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)


πŸ“¦ Tool Call

2026-07-27 03:45:58

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp Old:

		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():


πŸ“¦ Tool Call

2026-07-27 03:46:17

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp Old:

static void video_scaling_adjust(const VideoInfo *vi, const vmode_custom_t *vm)
{
	if (cfg.vscale_mode >= 4)

New:

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)


πŸ“¦ Tool Call

2026-07-27 03:46:30

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp Old:

	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: Bash Description: Locate 240p heuristics and fb_cmd functions

grep -n "item\[5\] < 300\|item\[5\]<300" Main_MiSTer/video.cpp; grep -n "fb_cmd1\|fb_cmd2" Main_MiSTer/video.cpp | head

πŸ“¦ Tool Result

2026-07-27 03:46:35

βœ… Result (Bash):

3973:				if (cfg.direct_video && (v_cur.item[5] < 300)) dst_h /= 2;
4126:			if (cfg.direct_video && (v_cur.item[5] < 300))
4106:		if (sscanf(cmd, "fb_cmd2 %d %d %d", &fmt, &rb, &div) == 3)
4119:		if (sscanf(cmd, "fb_cmd1 %d %d %d %d", &fmt, &rb, &width, &height) == 4)

πŸ“¦ Tool Call

2026-07-27 03:46:41

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp (offset: 3960, limit: 20)


πŸ“¦ Tool Result

2026-07-27 03:46:41

βœ… Result (Read):

3960						dst_y = brd_y;
3961					}
3962				}
3963				else
3964				{
3965					dst_x = brd_x;
3966					dst_y = brd_y;
3967					dst_w = width * 2 / 7;
3968					dst_h = src_h * dst_w / src_w;
3969				}
3970	
3971				if (*bg)
3972				{
3973					if (cfg.direct_video && (v_cur.item[5] < 300)) dst_h /= 2;
3974	
3975					imlib_context_set_image(*bg);
3976					imlib_blend_image_onto_image(logo, 1,
3977						0, 0,         //int source_x, int source_y,
3978						src_w, src_h, //int source_width, int source_height,
3979						dst_x, dst_y, //int destination_x, int destination_y,

πŸ“¦ Tool Call

2026-07-27 03:46:42

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp (offset: 4098, limit: 115)


πŸ“¦ Tool Result

2026-07-27 03:46:42

βœ… Result (Read):

4098					height = v_cur.item[5] / div;
4099					hmin = vmin = 0;
4100					hmax = v_cur.item[1] - 1;
4101					vmax = v_cur.item[5] - 1;
4102					accept = 1;
4103				}
4104			}
4105	
4106			if (sscanf(cmd, "fb_cmd2 %d %d %d", &fmt, &rb, &div) == 3)
4107			{
4108				if (div >= 1 && div <= 4)
4109				{
4110					width = v_cur.item[1] / div;
4111					height = v_cur.item[5] / div;
4112					hmin = vmin = 0;
4113					hmax = v_cur.item[1] - 1;
4114					vmax = v_cur.item[5] - 1;
4115					accept = 1;
4116				}
4117			}
4118	
4119			if (sscanf(cmd, "fb_cmd1 %d %d %d %d", &fmt, &rb, &width, &height) == 4)
4120			{
4121				if (width < 120 || width > (int)v_cur.item[1]) width = v_cur.item[1];
4122				if (height < 120 || height > (int)v_cur.item[5]) height = v_cur.item[5];
4123	
4124				int divx = 1;
4125				int divy = 1;
4126				if (cfg.direct_video && (v_cur.item[5] < 300))
4127				{
4128					// TV 240P/288P
4129					while ((width*(divx + 1)) <= (int)v_cur.item[1]) divx++;
4130					while ((height*(divy + 1)) <= (int)v_cur.item[5]) divy++;
4131				}
4132				else
4133				{
4134					while ((width*(divx + 1)) <= (int)v_cur.item[1] && (height*(divx + 1)) <= (int)v_cur.item[5]) divx++;
4135					divy = divx;
4136				}
4137	
4138				hmin = (uint16_t)((v_cur.item[1] - (width * divx)) / 2);
4139				vmin = (uint16_t)((v_cur.item[5] - (height * divy)) / 2);
4140				hmax = hmin + (width * divx) - 1;
4141				vmax = vmin + (height * divy) - 1;
4142				accept = 1;
4143			}
4144	
4145			int bpp = 0;
4146			int sc_fmt = 0;
4147	
4148			if (accept)
4149			{
4150				switch (fmt)
4151				{
4152				case 8888:
4153					bpp = 4;
4154					sc_fmt = FB_FMT_8888;
4155					break;
4156	
4157				case 1555:
4158					bpp = 2;
4159					sc_fmt = FB_FMT_1555;
4160					break;
4161	
4162				case 565:
4163					bpp = 2;
4164					sc_fmt = FB_FMT_565;
4165					break;
4166	
4167				case 8:
4168					bpp = 1;
4169					sc_fmt = FB_FMT_PAL8;
4170					rb = 0;
4171					break;
4172	
4173				default:
4174					accept = 0;
4175				}
4176			}
4177	
4178			if (rb)
4179			{
4180				sc_fmt |= FB_FMT_RxB;
4181				rb = 1;
4182			}
4183	
4184			if(accept)
4185			{
4186				int stride = ((width * bpp) + 15) & ~15;
4187				printf("fb_cmd: new mode: %dx%d => %dx%d color=%d stride=%d\n", width, height, hmax - hmin + 1, vmax - vmin + 1, fmt, stride);
4188	
4189				uint32_t addr = FB_ADDR + 4096;
4190	
4191				int xoff = 0, yoff = 0;
4192				if (cfg.direct_video)
4193				{
4194					xoff = v_cur.item[4] - FB_DV_LBRD;
4195					yoff = v_cur.item[8] - FB_DV_UBRD;
4196				}
4197	
4198				spi_uio_cmd_cont(UIO_SET_FBUF);
4199				spi_w(FB_EN | sc_fmt); // format, enable flag
4200				spi_w((uint16_t)addr); // base address low word
4201				spi_w(addr >> 16);     // base address high word
4202				spi_w(width);          // frame width
4203				spi_w(height);         // frame height
4204				spi_w(xoff + hmin);    // scaled left
4205				spi_w(xoff + hmax);    // scaled right
4206				spi_w(yoff + vmin);    // scaled top
4207				spi_w(yoff + vmax);    // scaled bottom
4208				spi_w(stride);         // stride
4209				DisableIO();
4210	
4211				if (cmd[6] != '2')
4212				{

πŸ“¦ Tool Call

2026-07-27 03:46:48

πŸ“¦ Tool: Read File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp (offset: 4055, limit: 50)


πŸ“¦ Tool Result

2026-07-27 03:46:48

βœ… Result (Read):

4055	
4056			c = (c + 1) % 3;
4057	
4058			imlib_context_set_color(c == 0 ? 255 : 0, c == 1 ? 255 : 0, c == 2 ? 255 : 0, 255);
4059			imlib_image_fill_ellipse(x, y, 10, 10);
4060		}
4061	}
4062	
4063	int video_bg_has_picture()
4064	{
4065		return bg_has_picture;
4066	}
4067	
4068	int video_chvt(int num)
4069	{
4070		static int cur_vt = 0;
4071		if (num)
4072		{
4073			cur_vt = num;
4074			int fd;
4075			if ((fd = open("/dev/tty0", O_RDONLY | O_CLOEXEC)) >= 0)
4076			{
4077				if (ioctl(fd, VT_ACTIVATE, cur_vt)) printf("ioctl VT_ACTIVATE fails\n");
4078				if (ioctl(fd, VT_WAITACTIVE, cur_vt)) printf("ioctl VT_WAITACTIVE fails\n");
4079				close(fd);
4080			}
4081		}
4082	
4083		return cur_vt ? cur_vt : 1;
4084	}
4085	
4086	void video_cmd(char *cmd)
4087	{
4088		if (video_fb_state())
4089		{
4090			int accept = 0;
4091			int fmt = 0, rb = 0, div = -1, width = -1, height = -1;
4092			uint16_t hmin, hmax, vmin, vmax;
4093			if (sscanf(cmd, "fb_cmd0 %d %d %d", &fmt, &rb, &div) == 3)
4094			{
4095				if (div >= 1 && div <= 4)
4096				{
4097					width = v_cur.item[1] / div;
4098					height = v_cur.item[5] / div;
4099					hmin = vmin = 0;
4100					hmax = v_cur.item[1] - 1;
4101					vmax = v_cur.item[5] - 1;
4102					accept = 1;
4103				}
4104			}

πŸ€– Claude Progress

2026-07-27 03:47:25

Now the fb_cmd handlers and logo heuristic: πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp Old:

				if (cfg.direct_video && (v_cur.item[5] < 300)) dst_h /= 2;

New:

				if (cfg.direct_video && (v_cur.item[5] < 300) && !v_cur.param.ilace) dst_h /= 2;

πŸ“¦ Tool Result

2026-07-27 03:47:27

βœ… 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:47:33

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp Old:

		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)


πŸ“¦ Tool Call

2026-07-27 03:47:36

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp Old:

		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] / div;
				hmin = vmin = 0;
				hmax = v_
... [271 chars total, truncated]

New:

		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)


πŸ“¦ Tool Call

2026-07-27 03:47:43

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/video.cpp Old:

			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: Read File: /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		{

πŸ“¦ Tool Call

2026-07-27 03:47:56

πŸ“¦ Tool: Edit File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/user_io.cpp Old:

	if (vga_fb) map |= CONF_VGA_FB;

New:

	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: Bash Description: 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 Result

2026-07-27 03:48:05

βœ… Result (Bash):

32:#include "video.h"
Main_MiSTer/Makefile
host g++: /usr/bin/g++

πŸ“¦ Tool Call

2026-07-27 03:48:08

πŸ“¦ Tool: TaskUpdate

{
  "taskId": "4",
  "status": "completed"
}

πŸ“¦ Tool Result

2026-07-27 03:48:08

βœ… Result (TaskUpdate):

Updated task #4 status


πŸ“¦ Tool Call

2026-07-27 03:48:08

πŸ“¦ Tool: TaskUpdate

{
  "taskId": "5",
  "status": "in_progress"
}

πŸ“¦ Tool Result

2026-07-27 03:48:08

βœ… Result (TaskUpdate):

Updated task #5 status


πŸ“¦ Tool Call

2026-07-27 03:48:14

πŸ“¦ Tool: Bash Description: 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.

πŸ“¦ Tool Call

2026-07-27 03:48:19

πŸ“¦ Tool: Bash Description: Find bundled libchdr headers

cd Main_MiSTer && ls lib/ 2>/dev/null | head; find . -name "chd.h" -path "*libchdr*" | head -3

πŸ“¦ Tool Result

2026-07-27 03:48:19

βœ… Result (Bash):

/bin/bash: line 1: cd: Main_MiSTer: No such file or directory
./lib/libchdr/include/libchdr/chd.h

πŸ“¦ Tool Call

2026-07-27 03:48:24

πŸ“¦ Tool: Bash Description: Check Makefile include flags

grep -n "INCLUDE\|^CFLAGS\|-I" Makefile | head; pwd

πŸ“¦ Tool Result

2026-07-27 03:48:24

βœ… Result (Bash):

19:INCLUDE	= -I./
20:INCLUDE	+= -I./lib/libco
21:INCLUDE	+= -I./lib/miniz
22:INCLUDE	+= -I./lib/md5
23:INCLUDE += -I./lib/lzma
24:INCLUDE += -I./lib/zstd/lib
25:INCLUDE += -I./lib/libchdr/include
26:INCLUDE += -I./lib/bluetooth
27:INCLUDE += -I./lib/serial_server/library
52:DFLAGS	= $(INCLUDE) -D_7ZIP_ST -DPACKAGE_VERSION=\"1.3.3\" -DHAVE_LROUND -DHAVE_STDINT_H -DHAVE_STDLIB_H -DHAVE_SYS_PARAM_H -DENABLE_64_BIT_WORDS=0 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -DVDATE=\"`date +"%y%m%d"`\"
/home/izzie/Desktop/mister-scaler/Main_MiSTer

πŸ“¦ Tool Call

2026-07-27 03:48:28

πŸ“¦ Tool: Bash Description: Syntax-check the three edited C++ files

for f in video.cpp user_io.cpp cfg.cpp; do echo "== $f"; g++ -fsyntax-only -std=gnu++17 -I./ -I./lib/libco -I./lib/miniz -I./lib/md5 -I./lib/lzma -I./lib/zstd/lib -I./lib/libchdr/include -I./lib/bluetooth -I./lib/serial_server/library -D_7ZIP_ST -DVDATE=\"0\" $f 2>&1 | head -8; done

πŸ“¦ Tool Result

2026-07-27 03:48:30

βœ… Result (Bash):

== video.cpp
== user_io.cpp
== cfg.cpp

πŸ“¦ Tool Call

2026-07-27 03:48:46

πŸ“¦ Tool: Bash Description: Verify ILACE edits in all sys_top files and PSX ascal analyzes

cd /home/izzie/Desktop/mister-scaler && grep -c "ILACE" Menu_MiSTer/sys/sys_top.v ao486_MiSTer/sys/sys_top.v Minimig-AGA_MiSTer/sys/sys_top.v PSX_MiSTer/sys/sys_top.v && echo --- && grep -n "'b111\|{ILACE,HEIGHT}\|reg         ILACE\|\.mode     ({ILACE" PSX_MiSTer/sys/sys_top.v && echo --- && cd /tmp/claude-1000/-home-izzie-Desktop-mister-scaler/9e999fa4-192e-4345-9713-5069a3bbd7aa/scratchpad && ghdl -a --std=08 /home/izzie/Desktop/mister-scaler/PSX_MiSTer/sys/ascal.vhd 2>&1 | grep -c error

πŸ“¦ Tool Result

2026-07-27 03:48:48

βœ… Result (Bash):

ao486_MiSTer/sys/sys_top.v:3
Minimig-AGA_MiSTer/sys/sys_top.v:3
Menu_MiSTer/sys/sys_top.v:3
PSX_MiSTer/sys/sys_top.v:3
---
318:initial vol_att = 5'b11111;
394:			if(io_din[7:0] == 'h20) io_dout_sys <= 'b111;
415:						4: {ILACE,HEIGHT} <= {io_din[15],io_din[11:0]};
776:		.mode     ({ILACE,~lowlat,LFB_EN ? LFB_FLT : |scaler_flt,2'b00}),
1047:reg         ILACE = 0;
---
0

πŸ“¦ Tool Call

2026-07-27 03:48:55

πŸ“¦ Tool: Bash Description: Review full ascal.vhd diff

cd /home/izzie/Desktop/mister-scaler && git -C Menu_MiSTer diff --stat && git -C Menu_MiSTer diff sys/ascal.vhd

πŸ“¦ Tool Result

2026-07-27 03:48:58

βœ… Result (Bash):

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

πŸ“¦ Tool Call

2026-07-27 03:49:21

πŸ“¦ Tool: Bash Description: Review Main_MiSTer diffs

git -C Main_MiSTer diff --stat && git -C Main_MiSTer diff cfg.h cfg.cpp user_io.cpp video.h && git -C Main_MiSTer diff video.cpp | head -120

πŸ“¦ Tool Result

2026-07-27 03:49:22

βœ… Result (Bash):

cfg.cpp     |  1 +
 cfg.h       |  1 +
 user_io.cpp |  2 +-
 video.cpp   | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++-----------
 video.h     |  1 +
 5 files changed, 63 insertions(+), 13 deletions(-)
diff --git a/cfg.cpp b/cfg.cpp
index f9ace49..3d0e512 100644
--- a/cfg.cpp
+++ b/cfg.cpp
@@ -72,6 +72,7 @@ static const ini_var_t ini_vars[] =
 	{ "FB_TERMINAL", (void*)(&(cfg.fb_terminal)), UINT8, 0, 1 },
 	{ "OSD_TIMEOUT", (void*)(&(cfg.osd_timeout)), INT16, 0, 3600 },
 	{ "DIRECT_VIDEO", (void*)(&(cfg.direct_video)), UINT8, 0, 2 },
+	{ "DIRECT_VIDEO_INTERLACE", (void*)(&(cfg.direct_video_interlace)), UINT8, 0, 1 },
 	{ "OSD_ROTATE", (void*)(&(cfg.osd_rotate)), UINT8, 0, 2 },
 	{ "DEADZONE", (void*)(&(cfg.controller_deadzone)), STRINGARR, sizeof(cfg.controller_deadzone) / sizeof(*cfg.controller_deadzone), sizeof(*cfg.controller_deadzone) },
 	{ "GAMEPAD_DEFAULTS", (void*)(&(cfg.gamepad_defaults)), UINT8, 0, 1 },
diff --git a/cfg.h b/cfg.h
index f702f0f..9455a6d 100644
--- a/cfg.h
+++ b/cfg.h
@@ -26,6 +26,7 @@ typedef struct {
 	uint8_t hdmi_cec_power_on;
 	float hdmi_cec_clock;
 	uint8_t direct_video;
+	uint8_t direct_video_interlace;
 	uint8_t video_info;
 	float refresh_min;
 	float refresh_max;
diff --git a/user_io.cpp b/user_io.cpp
index 5bb9fe6..85f1e92 100644
--- a/user_io.cpp
+++ b/user_io.cpp
@@ -3025,7 +3025,7 @@ void user_io_send_buttons(char force)
 	if (cfg.hdmi_limited & 2) map |= CONF_HDMI_LIMITED2;
 	if (cfg.direct_video) map |= CONF_DIRECT_VIDEO;
 	if (cfg.direct_video == 2) map |= CONF_DIRECT_VIDEO2;
-	if (vga_fb) map |= CONF_VGA_FB;
+	if (vga_fb || video_dv_scaler_forced()) map |= CONF_VGA_FB;
 
 	if ((map != key_map) || force)
 	{
diff --git a/video.h b/video.h
index 0f02587..074981c 100644
--- a/video.h
+++ b/video.h
@@ -65,6 +65,7 @@ int   hasAPI1_5();
 
 void video_fb_enable(int enable, int n = 0);
 int video_fb_state();
+int video_dv_scaler_forced();
 void video_menu_bg(int n, int idle = 0);
 int video_bg_has_picture();
 int video_chvt(int num);
diff --git a/video.cpp b/video.cpp
index 542ef71..521c645 100644
--- a/video.cpp
+++ b/video.cpp
@@ -175,7 +175,8 @@ struct vmode_custom_param_t
 	uint32_t pr;
 
 	// [26]
-	uint32_t unused[6];
+	uint32_t ilace;
+	uint32_t unused[5];
 };
 
 struct vmode_custom_t
@@ -212,6 +213,18 @@ static bool supports_vrr()
 	return video_version != 0;
 }
 
+static bool supports_ilace()
+{
+	static uint16_t video_version = 0xffff;
+	if (video_version == 0xffff) video_version = spi_uio_cmd(UIO_SET_VIDEO) & 4;
+	return video_version != 0;
+}
+
+int video_dv_scaler_forced()
+{
+	return cfg.direct_video && cfg.vga_scaler;
+}
+
 static uint32_t getPLLdiv(uint32_t div)
 {
 	if (div & 1) return 0x20000 | (((div / 2) + 1) << 8) | (div / 2);
@@ -2250,6 +2263,8 @@ static void video_set_mode(vmode_custom_t *v, double Fpix)
 		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
+		else if (i == 5) spi_w(((v_cur.param.ilace ? 1 : 0) << 15) | v_fix.item[i]);
 		//vsync polarity
 		else if (i == 7) spi_w((!!v_cur.param.vpol << 15) | v_fix.item[i]);
 		else spi_w(v_fix.item[i]);
@@ -2599,13 +2614,20 @@ static void video_mode_load(bool keep_direct_video_auto = false)
 
 	if (cfg.direct_video)
 	{
+		int ilace = (cfg.direct_video_interlace && supports_ilace()) ? 1 : 0;
 		int mode = cfg.menu_pal ? 2 : 0;
-		if (cfg.forced_scandoubler) mode++;
+		if (cfg.forced_scandoubler)
+		{
+			// interlaced output implies the 15KHz field timing, scandoubler contradicts it
+			if (ilace) printf("DIRECT_VIDEO_INTERLACE=1 overrides FORCED_SCANDOUBLER.\n");
+			else 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].vpar[i];
+		v_def.param.ilace = ilace;
 		setPLL(tvmodes[mode].Fpix, &v_def);
 
 		vmode_def = 1;
@@ -2979,6 +3001,25 @@ static void video_resolution_adjust(const VideoInfo *vi, vmode_custom_t *vm)
 
 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
+		// field raster, force an exact 2:1 output window so the fields weave
+		// (full source height is stored, each field reads every other line).
+		const uint32_t height = vi->rotated ? vi->width : vi->height;
+		const uint32_t src_h = height << (vi->interlaced ? 1 : 0);
+		const uint32_t scrh = vm->item[5];
+		if (src_h > scrh && height)
+		{
+			printf("Interlaced output: set weave window height to %d\n", src_h >> 1);
+			spi_uio_cmd16(UIO_SETHEIGHT, src_h >> 1);
+			spi_uio_cmd16(UIO_SETWIDTH, 0);
+			minimig_set_adjust(2);
+			return;
+		}
+		// else fall through: any window height works in phase-bias mode
+	}
+
 	if (cfg.vscale_mode >= 4)
 	{
 		spi_uio_cmd16(UIO_SETHEIGHT, 0);
@@ -3422,9 +3463,12 @@ static void video_fb_config()
 
 	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.item[5]) > FB_SIZE))
+		if (((v_cur.item[1] * (v_cur.item[5] << fb_il)) > FB_SIZE))
 			fb_scale = 2;
 		else
 			fb_scale = 1;
@@ -3436,7 +3480,7 @@ static void video_fb_config()
 	const int 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;
+	fb_height = (v_cur.item[5] << fb_il) / fb_scale_y;
 
 	brd_x = cfg.vscale_border / fb_scale_x;
 	brd_y = cfg.vscale_border / fb_scale_y;
@@ -3926,7 +3970,7 @@ void video_menu_bg(int n, int idle)
 
 			if (*bg)
 			{
-				if (cfg.direct_video && (v_cur.item[5] < 300)) dst_h /= 2;
+				if (cfg.direct_video && (v_cur.item[5] < 300) && !v_cur.param.ilace) dst_h /= 2;

πŸ“¦ Tool Call

2026-07-27 03:49:32

πŸ“¦ Tool: TaskUpdate

{
  "taskId": "5",
  "status": "completed"
}

πŸ“¦ Tool Result

2026-07-27 03:49:32

βœ… Result (TaskUpdate):

Updated task #5 status


πŸ“¦ Tool Call

2026-07-27 03:49:45

πŸ“¦ Tool: Write File: /home/izzie/.claude/projects/-home-izzie-Desktop-mister-scaler/memory/mister-480i-scaler-project.md

---
name: mister-480i-scaler-project
description: Ongoing MiSTer ASCAL 480i/576i output project β€” architecture, status, pending hardware test
metadata:
  type: project
---

Project (implemented 2026-07-26, uncommitted working-tree changes in all 5 repos under ~/Desktop/mister-scaler): patch ASCAL sc
... [1897 chars total, truncated]

πŸ“¦ Tool Result

2026-07-27 03:49:45

βœ… Result (Write):

File created successfully at: /home/izzie/.claude/projects/-home-izzie-Desktop-mister-scaler/memory/mister-480i-scaler-project.md


πŸ“¦ Tool Call

2026-07-27 03:49:52

πŸ“¦ Tool: Write File: /home/izzie/.claude/projects/-home-izzie-Desktop-mister-scaler/memory/MEMORY.md

# 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

  1. Rebuild Menu and ao486 (and optionally Minimig/PSX) in Quartus β€” ascal.vhd + sys_top.v changed in each core's sys/.
  2. Cross-compile Main_MiSTer (ARM toolchain isn't on this machine; the touched files pass host g++ -fsyntax-only).
  3. 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: Bash Description: Check git status and branch of all five repos

for r in Main_MiSTer Menu_MiSTer ao486_MiSTer Minimig-AGA_MiSTer PSX_MiSTer; do echo "== $r"; git -C $r status --short; git -C $r branch --show-current; done; echo; ls Main_MiSTer/docs 2>/dev/null || echo "no docs dir"

πŸ“¦ Tool Result

2026-07-27 04:11:41

βœ… Result (Bash):

== 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: Bash Description: 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:


πŸ“¦ Tool Call

2026-07-27 04:12:42

πŸ“¦ Tool: Write File: /home/izzie/Desktop/mister-scaler/Main_MiSTer/docs/direct_video_interlace.md

# 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: Bash Description: Create branch and commit sys changes in the four core repos

for r in Menu_MiSTer ao486_MiSTer Minimig-AGA_MiSTer PSX_MiSTer; do
cd /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_video

ascal.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 VSync
delayed 
... [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: Bash Description: 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_video

New MiSTer.ini option DIRECT_VIDEO_INTERLACE=1 (with DIRECT_VIDEO=1)
switches the direct_video scaler mode from 240p/288p to genuine
interlaced 480i/576i: the 15KHz TV field modeline is sent with a new
interlace 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: Bash Description: Verify all five repos committed on the new branch

cd /home/izzie/Desktop/mister-scaler && for r in Main_MiSTer Menu_MiSTer ao486_MiSTer Minimig-AGA_MiSTer PSX_MiSTer; do echo "== $r ($(git -C $r branch --show-current))"; git -C $r log --oneline -1; git -C $r status --short | head -4; done

πŸ“¦ Tool Result

2026-07-27 04:13:34

βœ… Result (Bash):

== Main_MiSTer (direct-video-interlace)
91801a4 Add DIRECT_VIDEO_INTERLACE: true 480i/576i scaler output over direct_video
?? db/
?? menu.flow.rpt
?? menu.map.rpt
?? menu.map.summary
== Menu_MiSTer (direct-video-interlace)
c2f85ee sys: true interlaced (480i/576i) scaler output for direct_video
 M menu.qsf
== ao486_MiSTer (direct-video-interlace)
9c9898d sys: true interlaced (480i/576i) scaler output for direct_video
== Minimig-AGA_MiSTer (direct-video-interlace)
5364e1b sys: true interlaced (480i/576i) scaler output for direct_video
== PSX_MiSTer (direct-video-interlace)
42f31e4 sys: true interlaced (480i/576i) scaler output for direct_video

πŸ€– Claude

2026-07-27 04:13:37

All committed. Updating the project memory to reflect the hardware-verified status:


πŸ“¦ Tool Call

2026-07-27 04:13:42

πŸ“¦ Tool: Edit File: /home/izzie/.claude/projects/-home-izzie-Desktop-mister-scaler/memory/mister-480i-scaler-project.md Old:

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.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment