-
Notifications
You must be signed in to change notification settings - Fork 14
Video Pipeline
Every core you port was born to talk to a screen that no longer sits on anyone's desk. A Commodore 64 speaks to a 1980s television at roughly fifteen thousand lines per second. An Amiga can weave two half-pictures into one tall one. A Game Boy has no video signal at all — just a tiny liquid-crystal panel and a fistful of pixel pulses. Your job, and the framework's, is to take whatever ragged, gloriously imprecise signal your core produces and deliver it — twice, at the same time — to two connectors on the back of a MEGA65: a digital HDMI socket that demands crisp, standards-clean video, and an analog VGA socket that can drive either a modern VGA monitor or — with the right cable — a genuine period CRT at its original 15 kHz.
This is the cover-to-cover guide to how that happens. It starts from the very idea of a
video signal — no prior knowledge of scanlines, sync pulses, or pixel clocks assumed —
walks through what a MiSTer core actually hands you, and then follows one RGB stream all
the way to both connectors, stage by stage, naming every module it passes through. It
uses the three reference cores as running examples: the Commodore 64 (C64MEGA65),
the Amiga 500 (AExp), and the Game Boy Color (gbc4mega65). Each solves the
same problem differently, and between them they exercise almost every feature the
pipeline has. By the end you should be able to read — and reproduce — every video choice
those three cores make.
It is a concepts article. It teaches you why the pipeline is shaped the way it is and
what each piece does, so that the exact porting steps make sense. Those steps — the
lines you edit in globals.vhd, mega65.vhd, config.vhd, and main.vhd — live in
The Ultimate MiSTer2MEGA65 Porting Guide, and this page points you there. Clocking is
its own deep subject with its own article; where video and clocks meet, this page defers
to Clocking and Clock Domain Crossing.
The MiSTer2MEGA65 video pipeline takes one raw retro RGB stream from your core and splits it into two independent outputs — an analog VGA path that stays faithful to the core's native timing (scandoubled for a modern VGA monitor, or passed raw at 15 kHz to a genuine retro CRT), and a digital HDMI path that rescales every frame into a fixed, standards-clean mode — and almost everything you do as a video porter is deciding how each of those two paths should behave.
Before we can talk about pipelines we have to agree on what a "video signal" even is. If you have never had to think about it — most programmers haven't — this part builds the vocabulary from zero. Every term the rest of the article uses is defined here, in the order you need it. If some of it is already familiar, skim to §1.4; the retro-specific quirks there are where the framework earns its keep.
An old cathode-ray-tube (CRT) television or monitor makes a picture with a single moving dot of light. An electron beam sweeps across the inside of the glass from left to right, painting one horizontal stripe of the image, then snaps back to the left and drops down one row to paint the next stripe. Each of those stripes is a scanline. Stack a few hundred scanlines top to bottom and you have one complete image — a frame. Do that fifty or sixty times a second and persistence of vision turns the flicker into a steady picture.
how a CRT paints a frame
──────────────────────────────▶ scan line 1 (beam sweeps left to right)
then fly back: left, and down one line
──────────────────────────────▶ scan line 2
fly back ...
──────────────────────────────▶ scan line 3
⋮
──────────────────────────────▶ last line
then fly back up to the top: the next frame begins
Two rhythms govern this. The fast one is horizontal: how quickly the beam sweeps one line and returns for the next. The slow one is vertical: how quickly it works down the whole screen and jumps back to the top for the next frame. Everything about video timing is just a careful description of those two return trips.
The beam needs to be told when to snap back. That instruction is the sync pulse — a brief, distinctive electrical signal. A horizontal sync (HSYNC) pulse says "you've reached the right edge; fly back to the left." A vertical sync (VSYNC) pulse says "you've reached the bottom; fly back to the top." A retro machine emits these two pulses continuously, and the display obediently follows them.
The snap-back is not instantaneous, and while it happens the beam must be switched off so it doesn't smear a bright diagonal line across the picture. Switching the beam off is called blanking. So every line is not just "visible pixels then sync" — there is a short blanked gap before the sync pulse and another after it, giving the beam time to settle. Those gaps have names that survive from the CRT era: the front porch (blanked time between the last visible pixel and the start of sync) and the back porch (blanked time between the end of sync and the first visible pixel of the next line).
the anatomy of one scanline (time flows left to right)
|<---------- one line (H total) ---------->|
| visible pixels | FP | sync | BP |
+-----------------------+----+--------+----+
| ### the picture ### | blanked (black) |
+-----------------------+----+--------+----+
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾________‾‾‾‾‾‾ HSYNC
(the dip = sync pulse; FP/BP = front/back porch)
The dip is drawn as the physical CRT pulse (usually active-LOW);
inside M2M the same sync is carried active-HIGH (see §1.3).
The identical idea, rotated ninety degrees, describes the whole frame: some active lines (the ones carrying picture), then a vertical front porch of blank lines, then the vertical sync pulse, then a vertical back porch of blank lines, then the next frame begins.
the anatomy of one frame
+--------------------------------+
| active lines (the picture) | e.g. 576 visible lines
+--------------------------------+
| V front porch (blank lines) |
| V sync pulse |
| V back porch (blank lines) |
+--------------------------------+ ← next frame starts here
Two more definitions and we have the whole language:
-
The pixel clock (also called the dot clock) is the drumbeat that clocks out individual pixels along a line. If a line is 864 pixel-clocks wide (720 visible plus the porches and sync) and there are 625 lines per frame, the frame is
864 × 625 = 540000pixel-clocks; run the pixel clock at 27 MHz and you get27000000 / 540000 = 50frames per second. Every video timing number in this article is ultimately counted in pixel clocks. If the dot clock is a new idea, two short primers repay ten minutes: video timing in Verilog and the XFree86 Video Timings HOWTO. -
Data enable (DE) is a single signal that is high exactly when the beam is inside the visible picture — i.e. not in any porch, sync, or blanking. It is simply "the opposite of blanking," collapsed into one wire. You will meet it constantly, because a lot of hardware would rather be told "paint now / don't paint now" once than reason about four separate porch and sync signals.
There are two historical ways to get this signal from a machine to a screen.
Composite video crams everything — the three colors, brightness, and both sync pulses — onto a single wire, encoded as one analog waveform. It is what a TV's yellow RCA jack carries. It is compact and lossy, and untangling it back into clean pixels is real work.
Separate RGB plus sync keeps everything on its own wire: one line each for red, green, and blue, plus (usually) separate horizontal and vertical sync. It is what a SCART cable (the European TV/AV connector), a VGA cable, and — importantly — the inside of an FPGA all use. Because the colors are never blended together, RGB is the clean, native language of a modern core. Every M2M core speaks it: eight bits each of red, green, and blue, plus separate HSYNC and VSYNC.
This is the form the framework works in throughout, so from here on "the video signal"
means: three 8-bit color busses, an HSYNC, a VSYNC, and — the framework's preferred way of
expressing blanking — a separate HBlank and VBlank signal (each high during
horizontal or vertical blanking respectively). A core that produces HBlank/VBlank
directly is giving the framework exactly what it wants; a core that only has sync pulses
has to have the blanking reconstructed for it, which we get to in Part 2.
A convention worth memorizing now. Inside M2M, sync and blank are active-high: HSYNC is
'1'during the sync pulse, HBlank is'1'during blanking, and so on. Polarity is a recurring source of "why is my picture torn / shifted / black" bugs, and the pipeline assumes active-high at the core boundary. (The transmitted HDMI polarity can differ per video mode — the framework fixes that up for you at the very end; see Part 5. But at the boundary where your core hands over its pixels, everything is active-high.)
Here is the single most consequential fact about retro video, the one that shapes half of the analog pipeline.
Count how many scanlines a machine draws per second — that is its horizontal frequency, and it is the horizontal sync rate. A 1980s home computer or console draws roughly 15,000 lines per second: about 15 kHz. A Commodore 64 is 15.64 kHz; an Amiga is 15.625 kHz. Old televisions were built for exactly this rate.
A PC VGA monitor cannot sync that slowly. The lowest thing a standard VGA monitor understands is 640×480 at 60 Hz, whose horizontal frequency is about 31.5 kHz — twice as fast. Feed a raw 15 kHz signal into a VGA monitor and it displays nothing, or a rolling mess. This is not a small incompatibility you can trim away; it is a factor-of-two gap baked into the hardware.
There are two ways across the gap, and M2M offers both:
-
Draw every line twice. If you emit each source scanline on two output lines instead of one, you halve the time each line is on screen and thereby double the horizontal frequency: 15 kHz becomes ~31 kHz, and a VGA monitor locks. The machine that does this is a scandoubler, and it is the heart of the analog pipeline (Part 4). This is called "line doubling," and it is deliberately dumb — it does not smooth or interpolate, it just shows each line twice.
-
Rebuild the picture at a completely standard resolution. Instead of nudging the line rate, capture whole frames and re-scan them out at, say, exactly 1280×720 at 50 Hz — a resolution every modern display accepts. That is the job of the HDMI scaler (Part 5), and it is a far heavier machine than a scandoubler.
Keep this pair in mind: the analog path gently doubles the native signal; the digital path completely rebuilds it. Nearly every difference between the two halves of the pipeline flows from that.
So far each frame has been a clean top-to-bottom sweep of every line. That is progressive scanning. But CRT televisions had a trick to squeeze more apparent vertical resolution out of the same 15 kHz line rate: interlace.
An interlaced signal splits each frame into two fields. The first field draws only the odd-numbered lines (1, 3, 5, …); the second field draws only the even-numbered lines (2, 4, 6, …), shifted down half a line so they fall in the gaps. Two fields interleaved — hence "interlaced" — make one full-height frame, but each field is sent separately, at the full refresh rate. So an interlaced 50 Hz signal sends 50 fields per second (25 full frames), each field carrying half the lines.
progressive interlaced (two fields, woven)
line 1 ─────── field A: line 1 ───────
line 2 ─────── field B: line 2 ┈┈┈┈┈┈┈
line 3 ─────── field A: line 3 ───────
line 4 ─────── field B: line 4 ┈┈┈┈┈┈┈
(every line, one pass) (odd lines; then even lines, shifted
down half a line to fall in the gaps)
The Commodore 64 and a standard Game Boy are progressive. The Amiga is the interesting
case: its low-resolution modes are progressive, but its high-resolution "laced" modes
(like 640×512) are genuinely interlaced. A signal that switches between the two at runtime
forces the pipeline to detect interlace and cope with it — which is exactly why the Amiga
core, AExp, is our worked example for interlace handling (Part 5). To signal which field
is currently being drawn, an interlaced source toggles a one-bit field flag every field;
that flag is the thread the whole interlace story hangs on.
The last piece of background is regional. Europe's PAL standard runs about 50 frames per second with 625 lines total; North America's and Japan's NTSC runs about 60 frames per second with 525 lines. A PAL machine and an NTSC machine differ in line count, frame rate, and often pixel clock, and many cores can switch between them. For a first port you pick one — usually the machine's home region — and get a picture; you add the other later.
But the deeper lesson hiding in these numbers is that retro timings are not round. A PAL C64 does not run at 50.000 Hz; it runs at about 50.124 Hz. A Game Boy runs at 59.7275 Hz, not 60. NTSC's "60 Hz" is really 59.94. These machines were clocked by cheap crystals divided down by whatever integer was convenient, and the resulting refresh rate is whatever it is. On a CRT that never mattered — the TV just followed the sync. But a modern HDMI display insists on a standard rate (exactly 50.000 or 60.000 Hz), and the gap between "what the core produces" and "what HDMI demands" is the root cause of the tearing that flicker-free exists to cure (Part 5). Tuck that mismatch away; it is the punchline of the digital pipeline.
There is a second imprecision worth flagging now: the shape of the sync pulses. A retro core's sync pulses are whatever width the original hardware happened to make them, and they were only ever meant to satisfy a forgiving CRT. A picky modern monitor, by contrast, guesses which video mode it is being fed partly from the width and polarity of those pulses. A scandoubled retro signal can therefore be technically valid yet get misclassified by a VGA monitor — the reason the analog pipeline offers an optional sync reshaper (Part 4).
There is one more property of a retro picture that trips up almost everyone, and it unlocks a whole feature of the pipeline: a picture's shape is not the same thing as its pixel count.
The display aspect ratio is the shape of the whole picture as it is meant to appear — width to height. A Commodore 64 picture is meant to fill a roughly 4:3 frame, the shape of every period television. That is a fact about how it should look, and by itself it says nothing about how many pixels the machine draws.
The pixel aspect ratio is the shape of a single pixel — a square, or a rectangle taller or
wider than square. Here is the surprise for anyone raised on modern displays: retro pixels are
almost never square. A C64 draws, in round numbers, 320 pixels across by 200 down. If those
pixels were square the picture would be 320:200 = 8:5 = 1.6 — wider than 4:3 — yet a C64 on a TV
shows a ~4:3 (1.33) image. The pixels must therefore be taller than they are wide: the machine
spreads its 200 rows over more height than 320 square pixels would imply. PAL and NTSC machines
differ here too, because their line counts and timings differ, so the very same core can have a
different pixel shape in each region.
State the lesson sharply, because it drives everything downstream: a raster's pixel dimensions tell you almost nothing about the physical shape the picture should take. You always need a second fact — the intended display aspect (equivalently, the pixel aspect) — to present it correctly. A naive port that treats retro pixels as square gets a subtly (or grossly) wrong shape: circles become ovals, playfields squash or stretch.
And it turns one notch more subtle, because the HDMI standard itself uses non-square pixels.
The CEA/HDMI mode the framework calls "576p" is 720×576 encoded pixels, but it is defined as a
4:3 picture — and 720:576 = 5:4, not 4:3, so an HDMI 576p pixel is itself slightly non-square.
The 720×480 mode is the same story. So when the framework fits a retro picture into an HDMI frame,
it cannot trust pixel counts at all; it has to reason about physical shapes — the picture's
intended aspect and the HDMI mode's advertised aspect — and let the pixel arithmetic fall out. That
is precisely what the HDMI output-fitting system does (Part 5.1), and it is why that system asks
you for a ratio like "4:3" or "10:9" rather than a pixel rectangle.
Three phrases, kept straight, make the rest of the article's aspect talk clear: display aspect (the shape of the whole picture), pixel aspect (the shape of one pixel), and non-square pixels (the normal state of affairs for retro machines and for HDMI's SD modes alike).
To make all of this concrete, here is the native video each reference core produces before the framework touches it. These three are the specimens dissected throughout the article.
-
Commodore 64 (
C64MEGA65). The VIC-II video chip emits native RGB with a ~15.64 kHz horizontal frequency and a ~50.124 Hz PAL frame rate. The core's main clock is about 31.53 MHz and the pixel rate is that divided by four (~7.88 MHz). It is progressive. Its active picture is roughly 320×200 of "real" content inside a border; after the core trims and the framework doubles, the analog canvas is 720×540. -
Amiga 500 (
AExp). The OCS chipset emits ~15.625 kHz RGB at ~50 Hz PAL. Low-res modes are progressive; hi-res modes are interlaced, and the core reports the current field so the pipeline can weave them. Its native pixel rate is ~7.09 MHz (low-res) or ~14.19 MHz (any frame containing a hi-res line). -
Game Boy Color (
gbc4mega65). This one has no analog video signal at all. The Game Boy's picture processor drives a 160×144 liquid-crystal panel with per-pixel strobes and a once-per-frame vertical sync — there are no scanlines, no porches, no HSYNC, nothing a monitor could lock to. The core must fabricate a complete raster from scratch, at 59.7275 Hz, and it pads the little 160×144 image up to a 256×224 canvas (with a black border) so there is room for the on-screen menu. This "third kind of core" stresses parts of the pipeline the other two never touch.
Three machines; three horizontal frequencies; one interlaced; one with no sync at all. If the pipeline can carry all three to a modern VGA monitor and an HDMI television, it can carry almost anything. The rest of this article is how it does.
M2M exists to host a MiSTer core — a piece of FPGA logic originally written for the
MiSTer platform — on MEGA65 hardware. So the practical starting point is not "a raw video
signal" in the abstract, but "the specific signals a MiSTer core brings to the table," and
what you must do to them in CORE/vhdl/main.vhd before the framework will accept them. If
you have never seen a MiSTer core, the MiSTer Quick Reference Guide covers the MiSTer
side in full; this part covers only the video handoff.
A MiSTer core emits its video on a small, conventional set of wires. The names vary a little between cores, but the shape is always this:
-
CLK_VIDEO— the clock the video logic runs on. Usually this is the same as the core's system clock. -
CE_PIXEL— a clock enable: a signal that is high for one cycle ofCLK_VIDEOat each pixel instant. Rather than running a separate slow pixel clock, MiSTer keeps everything on one fast clock and marks pixel boundaries with this enable. (If clock enables are unfamiliar, Clocking and Clock Domain Crossing §1.2 explains the idiom in full; it is the single most useful FPGA-clocking idea, and it is everywhere in M2M.) -
R,G,B— the color busses (commonly 8 bits each). -
HSync,VSync— the two sync pulses. -
HBlank,VBlank— blanking signals. Many cores provide these; some do not. -
VGA_DE— the data-enable (high during active video). Some cores provide it, some derive it downstream. -
VGA_F1— for interlaced cores, the field flag (0 = first field, 1 = second).
The MiSTer framework normally consumes these directly. M2M's job is to translate them into its own conventions and feed them into the two-headed pipeline.
The contract between your core and the framework is a set of VHDL ports named video_*,
all outputs of your core, all in your core's video clock domain. Reduced to the essentials:
video_clk_o : out std_logic; -- your pixel-domain clock
video_rst_o : out std_logic;
video_ce_o : out std_logic; -- native pixel clock-enable
video_ce_ovl_o : out std_logic; -- post-scandoubler CE
video_red_o : out std_logic_vector(7 downto 0); -- 8-bit R
video_green_o : out std_logic_vector(7 downto 0);
video_blue_o : out std_logic_vector(7 downto 0);
video_hs_o : out std_logic; -- HSYNC, active high
video_vs_o : out std_logic; -- VSYNC, active high
video_hblank_o : out std_logic; -- HBlank, active high
video_vblank_o : out std_logic; -- VBlank, active highThe mapping from MiSTer to M2M is mostly a rename — R→video_red_o, HSync→video_hs_o,
HBlank→video_hblank_o, and so on — done in main.vhd where you instantiate the core.
In the template you can watch the demo core's vga_* names become the contract's video_*
names at exactly this boundary. Three things, though, are not just renames, and they are
where porters spend their effort:
-
The core must produce its own video clock.
video_clk_ois an output — your core owns its pixel-domain clock and hands it back to the framework, which does not generate it for you. (Split clock ownership is a load-bearing M2M invariant; see Clocking and Clock Domain Crossing §1.3. In practice most cores makevideo_clkequal to their main clock and pace pixels withvideo_ce_o.) -
You must supply two clock enables, not one.
video_ce_oandvideo_ce_ovl_oare both required, and the difference between them is subtle enough to have its own section below (§2.5) and its own payoff in Part 4. -
Blanking must exist and be active-high. If your MiSTer core already outputs
HBlank/VBlank, wire them straight through. If it only gives you sync pulses, you must reconstruct blanking — the subject of §2.3.
Some cores hand you clean HBlank/VBlank. Others give you only HSync/VSync and expect
the surrounding framework to figure out where the active picture is. When you are in the
second situation, you do not have to write a blanking generator from scratch — the framework
ships one you can drop into main.vhd: M2M/vhdl/controllers/MiSTer/video_sync.vhd (Dar's
well-travelled composite_sync/blank generator, used across many MiSTer cores).
You feed it the raw sync pulses and a pixel-domain clock; it counts dots per line and lines
per frame between the sync edges, and from those counts it regenerates clean, standard-width
hsync_out/vsync_out and the hblank/vblank the pipeline wants. It has a ntsc input
to pick the PAL or NTSC line geometry and a wide input for widescreen framing:
entity video_sync is
port(
clk32 : in std_logic; -- a pixel-domain clock
pause : in std_logic; -- freeze (tie to '0' if unused)
hsync : in std_logic; -- raw sync in
vsync : in std_logic;
ntsc : in std_logic; -- 0 = PAL geometry, 1 = NTSC
wide : in std_logic;
hsync_out : out std_logic; -- clean, standard-width sync out
vsync_out : out std_logic;
hblank : out std_logic; -- reconstructed blanking
vblank : out std_logic
);The Commodore 64 core is the reference user of this idea, with a twist worth knowing: it
does not use the framework's copy — it ships its own customized video_sync.vhd. The
VIC-II's native horizontal sync pulse is a lazy 12.69 µs wide; the C64's version reshapes
it down to a tidy 4.82 µs, and at the same time it uses the regenerated blanking to crop
the core's output to a clean 382×270 active window before the framework ever sees it. So
video_sync.vhd is doing three jobs at once here — regenerating blanking, standardizing the
sync width, and trimming the picture — all inside the core, upstream of the framework. That
is the general pattern: whatever conditioning your core's raw signal needs, do it in
main.vhd, so the framework receives a clean, active-high, properly-blanked RGB stream.
The Game Boy is the extreme case: its picture processor produces per-pixel strobes and a once-per-frame vsync, and nothing else — no scanlines, no porches, no horizontal sync. You cannot reshape a raster that does not exist; you have to build one.
The Game Boy core does this with another imported MiSTer module, lcd.v, wrapped for VHDL.
lcd.v runs on the core's video clock (a nominal 67.108864 MHz, exactly twice the machine
clock) and counts out a complete synthetic raster: it defines a 425-pixel-wide line and a
264-line frame in parameters, and generates positive-going HSYNC/VSYNC and both blanking pairs
by comparing free-running counters against those parameters. Each pixel is paced at ten video
clocks, so 425 pixels is about 4250 clocks; because the frame rate is not a clean division, the
last pixel of each line is deliberately stretched by six to make the line exactly 4256 video
clocks. That gives 4256 × 264 = 1,123,584 clocks per frame and thus 67108864 / 1123584 = 59.7275 Hz, the authentic Game Boy rate, fabricated entirely by counting the FPGA clock.
Two more tricks make it work as an M2M source. First, because the incoming pixels arrive on
the machine clock but the raster is scanned out on the video clock, lcd.v writes pixels into
a small frame buffer on one clock and reads them out under the raster on the other —
double-buffering decouples the asynchronous capture from the tidy scan-out. Second, the
core pads the tiny 160×144 image up to a 256×224 canvas by wrapping it in a black border (the
old Super Game Boy screen geometry). That padding is not decoration: a 160×144 picture is far
too small to hang an on-screen menu on, whereas 256×224 (which doubles to a 512×448 analog
canvas, or 32×28 character cells) has room for one. The border exists so the OSM has
somewhere to live.
The lesson reaches past the Game Boy: turning a fixed panel into a video stream means capturing the panel into RAM asynchronously, then scanning it back out under a self-consistent raster you generate yourself. A CRT-native core already is a raster generator and skips all of this; a panel-native core has to become one.
Now the subtle one. You must produce two pixel clock enables, and confusing them is a classic first-port bug.
-
video_ce_ois your core's native pixel rate — one enable pulse per pixel the core actually generates, before any doubling. For the C64 that is main-clock-over-four (~7.88 MHz); for the democore it is a simple divide-by-two of the 54 MHz core clock, giving 27 MHz. -
video_ce_ovl_ois the post-scandoubler pixel rate — one enable per pixel of the analog output, i.e.VGA_DXenables across each visible line — and it must correspond to the analog output resolutionVGA_DX×VGA_DY(which you set inglobals.vhd; Part 3). It is faster thanvideo_ce_o: about twice as fast for a core whose native width is halfVGA_DX, and up to the full video clock for a very low-resolution core. The framework uses it to clock the on-screen-menu overlay and to sample your video onto the analog output at the doubled resolution.
Why two? Because the analog picture that reaches the VGA connector is the scandoubled one —
twice as many lines, running at roughly twice the pixel rate — and the menu overlaid on it
must be drawn at that same doubled rate to line up. The "overlay" enable (_ovl) is that
doubled rate.
The template gets to cheat. Its demo core already renders natively at the full 720×576
overlay resolution, so there is nothing to double, and main.vhd simply ties the two
together:
-- The democore already outputs at full VGA_DX x VGA_DY resolution, so the
-- native rate IS the overlay rate. A real low-res core must NOT do this.
video_ce_ovl_o <= video_ce_o;That line is flagged in the template as an important porting to-do, because it is only
correct when your native resolution already equals VGA_DX×VGA_DY. A genuine low-resolution
core must run video_ce_ovl_o faster — enough enables to lay down VGA_DX pixels across each
doubled line — so the overlay and the analog sampling happen at display resolution rather than
at the tiny native one. (The C64, for instance, holds video_ce_ovl_o high on every video
clock in Standard mode — four times its native pixel enable; only its retro 15 kHz mode drops
to twice native.) Get this wrong and the on-screen menu comes out the wrong size or misaligned. We
return to just how subtle this can get — the Game Boy has to phase-lock the overlay enable
to the scandoubler itself — in Part 4.4, once the scandoubler has been properly introduced.
Finally, if your core is interlaced (§1.5), MiSTer gives you a VGA_F1 field flag. In M2M
you route it out as one more video-domain output, conventionally video_fl_o: it toggles
every field while the machine is in a laced mode and sits constant '0' otherwise. The Amiga
core takes the Minimig chipset's field1 signal and drives video_fl_o with it, one-for-one
with MiSTer's assign VGA_F1 = field1. That single bit is what lets the HDMI scaler stitch
the two fields back into a full-height picture; we follow it all the way through in Part 5.3.
A progressive core simply never drives it, and everything downstream treats a constant '0'
as "not interlaced" — so the feature costs nothing when you don't need it.
With the vocabulary built and the core's signals in hand, we can finally open up the framework itself.
Your core produces exactly one video stream. The MEGA65 has two video connectors. The
framework's central act is to fan that one stream out to both, processing each one
completely differently, and it does so inside a single VHDL entity: av_pipeline.vhd. This
part is the map of that entity — what splits where, on which clock, and, just as important,
which files you edit to steer it.
Here is the whole pipeline on one page. Read it top to bottom: your core hands its RGB
stream to av_pipeline, which immediately forks it into an analog path and a digital
path that never rejoin. Everything below is elaborated in Parts 4 and 5; this is the map
to keep open while you read them.
CORE (your MiSTer core, in CORE/vhdl/main.vhd)
RGB + hs/vs + hblank/vblank + video_ce + video_ce_ovl
all on YOUR video_clk
│
┌────────────┴───────────────────┐
▼ ▼
ANALOG PATH DIGITAL PATH
analog_pipeline.vhd digital_pipeline.vhd
on YOUR video_clk on framework hdmi_clk (+5x tmds)
(keeps native timing) (rebuilt to a fixed HDMI mode)
│ │
scandoubler (15k / 2x->31k) crop / zoom-in
│ │
force RGB=0 outside DE ascal: 1-frame buffer in HyperRAM
│ │
OSM overlay (the menu) OSM overlay (the menu)
│ │
csync + sync reshaper vga_to_hdmi -> TMDS symbols
│ │
phase-shift output regs 4x serialiser (10:1 DDR)
▼ ▼
VGA_RGB + HS/VS out 3 data + 1 clock TMDS pairs
│ │
▼ ▼
ADV7125 DAC -> VGA HDMI connector
(modern VGA monitor,
or 15 kHz retro CRT)
The single most important structural fact is the fork near the top: the same core RGB enters both paths, and from that point on they share nothing. The analog path keeps your core's native timing — doubling it for a VGA monitor, or leaving it raw at 15 kHz for a retro CRT; the digital path throws the timing away and rebuilds the picture at a standard resolution. They even overlay the on-screen menu separately, and — as we will see — they run on entirely different clocks.
The clocks are where the two paths most fundamentally diverge, and understanding this makes the rest fall into place.
The analog path runs on your core's own video_clk. The scandoubler, the analog
overlay, the composite-sync and reshaper stages, and the final output registers are all
clocked by the very clock your core handed the framework. The picture that reaches the VGA
connector is your native picture — line-doubled for a modern VGA monitor, or passed through
untouched at 15 kHz for a retro CRT. Either way, nothing has changed its fundamental timing.
At the very end, the framework even phase-shifts the analog output by clocking the final
registers on the falling edge of video_clk, so the on-board digital-to-analog converter
samples a clean, settled signal. The VGA DAC's own clock (vdac_clk) is literally your
video_clk.
The digital path runs on a framework-generated hdmi_clk. HDMI does not tolerate a
core's not-quite-round timing; it wants a standard pixel clock for a standard mode. So the
framework synthesizes the HDMI pixel clock itself, from the 100 MHz board clock, and — this
is the clever bit — makes it reconfigurable: choosing a different HDMI output mode
actually retunes the pixel clock to that mode's exact frequency at runtime. Alongside the
pixel clock it generates a second clock at five times that rate, tmds_clk, used to
serialize the HDMI bits into HDMI's TMDS line code (Part 5.4). For the default 720p50 mode these are 74.25 MHz and
371.25 MHz respectively.
who clocks what
YOUR video_clk ─────▶ ANALOG path
(scandoubler, analog OSM, VGA out)
native timing: raw 15 kHz, or doubled to 31 kHz
framework hdmi_clk ─▶ DIGITAL path
(ascal, HDMI OSM, TMDS out)
rebuilt to a fixed HDMI mode; the pixel clock
retunes per mode (74.25 / 371.25 MHz at 720p50)
Between them sits one more clock you never own: the audio clock at 12.288 MHz, which carries the sound the HDMI path muxes into its data stream and the analog path sends to the 3.5 mm jack. Audio is out of scope here except where it shares the HDMI wire (Part 5.4).
Because these are genuinely different clock domains, every menu setting the core sends —
"use scandoubler," "HDMI mode 720p50," "zoom in," and so on — is safely carried across a
clock-domain crossing inside av_pipeline before it reaches the path it controls. You do not
have to think about those crossings; the framework builds them. (If you want to understand
what it is doing and why, Clocking and Clock Domain Crossing Part 2 is the reference.)
M2M/vhdl/av_pipeline/video_modes_pkg.vhd is worth opening because it is the concrete
definition of "a video mode," and it is the source of the porch and sync numbers Part 1 talked
about in the abstract. Despite what its name might suggest it is not only about HDMI: it holds
two related things, one for each output path. For the digital path it catalogs the seven
fixed HDMI modes; for the analog path it defines the optional VGA sync-reshaper records and
presets we meet in Part 4.5. Both belong here because both are, at bottom, descriptions of
sync-and-porch timing.
The HDMI side is built on a record, video_modes_t, that is nothing more than the anatomy from
§1.2 written as VHDL fields:
type video_modes_t is record
CLK_KHZ : integer; -- pixel clock, in kHz
CLK_SEL : ...; -- which pixel-clock frequency to synthesize
CEA_CTA_VIC : integer; -- the mode's standard ID number
ASPECT : ...; -- 4:3 or 16:9, for HDMI signalling
PIXEL_REP : std_logic; -- pixel repetition (for SD modes)
H_PIXELS : integer; -- visible width ┐
V_PIXELS : integer; -- visible height │
H_PULSE, H_BP, H_FP : integer; -- the porches │ exactly the
V_PULSE, V_BP, V_FP : integer; -- and sync │ §1.2 anatomy
H_POL, V_POL : std_logic; -- polarities ┘
end record;Constants of this type are gathered into an array, VIDEO_MODE_VECTOR, of seven slots indexed
0 to 6 — built from six distinct timing records, because the PAL 576p record fills two slots
(once behind the "4:3" menu label and once behind the "5:4" one; see below). When the user picks
an HDMI mode in the menu, that choice becomes an index into this vector, and the digital pipeline
reads the porches and polarities straight out of the chosen record.
Two details from this file save real debugging time:
-
CLK_SELis what retunes the HDMI clock. Each mode carries a 3-bit code naming the pixel-clock frequency it needs (25.2, 27, 74.25 MHz, …). The framework feeds that code to a reconfigurable clock generator (video_out_clock.vhd), which reprograms itself on the fly to that frequency — glitch-free, by briefly switching to a backup clock while it retunes. This is why the HDMI pixel clock in §3.2 is "reconfigurable": the mode you pick literally determines the clock frequency. -
The 4:3 and 5:4 labels are deliberately crossed. The menu offers PAL 576p as both "4:3" and "5:4," but both select the same 720×576 timing record — what differs is only how the digital pipeline fits the picture (the "5:4"-labelled mode letterboxes it to a 4:3 shape; the "4:3"-labelled one fills the 5:4-shaped raster). The labels name the target monitor, not the aspect math: as the core's
mega65.vhdexplains in a comment, the 4/3-adjusted image simply looked best on a 5:4 monitor and vice-versa. It is a rare case where a user-facing name intentionally contradicts the geometry — do not "fix" it. (This whole 4:3-versus-5:4 muddle is really the display-aspect-versus-pixel-count distinction from §1.7; Part 5.1 shows how a core now sets the HDMI fit directly, and independently for the normal and zoomed views.)
Almost none of the pipeline is something you build; it is something you configure. Four files carry all of your video decisions, and it helps to know up front which file owns which kind of decision.
CORE/vhdl/globals.vhd — the fixed facts about your video. Here you set the analog output
resolution VGA_DX/VGA_DY (the post-scandoubler size, and the size the on-screen menu is
laid out against), the exact core clock speed CORE_CLK_SPEED, the optional analog sync
reshaper profile VGA_STD_SYNC (Part 4.5), the optional HDMI output-fitting policy HDMI_VIEW
(Part 5.1), and — for audio — the biquad filter coefficients.
Changing VGA_DX/VGA_DY silently resizes the menu's character grid and its video RAM, so
treat it as load-bearing.
CORE/vhdl/mega65.vhd — the live video policy. This is where menu bits become video
behavior. A block of "AV-mode" outputs (all in the QNICE clock domain, because they are driven
by the menu state) tells the framework how to run each path. Every one of them is an output of
your core; the framework consumes them. This table is the control panel of the whole pipeline:
| Core output | Path it steers | What it does |
|---|---|---|
qnice_video_mode_o |
digital | Which of the 7 HDMI modes (an index into VIDEO_MODE_VECTOR) |
qnice_scandoubler_o |
analog | 1 = line-double to 31 kHz ("Standard VGA"); 0 = pass 15 kHz |
qnice_retro15kHz_o |
analog | 1 = the output is a raw 15 kHz signal (adjusts the overlay) |
qnice_csync_o |
analog | 1 = emit composite sync (for SCART) instead of separate H/V |
qnice_zoom_crop_o |
digital | 1 = crop the border and zoom the picture to fill the frame |
qnice_ascal_mode_o |
digital | Scaler filter: nearest / bilinear / sharp / bicubic |
qnice_ascal_polyphase_o |
digital | 1 = use polyphase filters (overrides the above) |
qnice_ascal_triplebuf_o |
digital | Scaler triple-buffering (leave off) |
qnice_dvi_o |
digital | 1 = DVI (strip HDMI audio + info packets) |
qnice_audio_mute_o / _filter_o
|
audio | Mute / apply the biquad filter |
A boolean setting is just qnice_<thing>_o <= qnice_osm_control_i(C_MENU_<x>) — read one bit
of the 256-bit menu-state register. A multi-way setting (the HDMI mode) is a priority chain
over several such bits. The C_MENU_* constants are zero-based line numbers into the menu;
because nothing checks that they still match the menu after you edit it, reordering a menu
line silently points these at the wrong bits. This coupling is the single most common
foot-gun in this file. (The On‐Screen‐Menu (OSM) article covers the menu-state register in
full.)
CORE/vhdl/config.vhd — the menu itself, plus scaler policy. Here you write the menu lines
the user sees, and a couple of video-relevant switches: ASCAL_USAGE decides who controls
the scaler filter — 2 means "the core drives it from menu bits," while 1 means "hands off,
the QNICE firmware controls the scaler itself," which is how all three reference cores implement
their rich HDMI-filter menus (Part 5.2). The config.vhd Switches and Settings page is the
exhaustive reference.
CORE/vhdl/main.vhd — the pixels. Everything in Part 2: instantiate your core, produce the
video_* signals, the two clock enables, and any blanking reconstruction. globals and
config and mega65 decide what the pipeline does; main.vhd decides what the picture is.
One framework feature straddles both paths and deserves a mention here because it appears twice in the pipeline map: the on-screen-menu (OSM) overlay. The QNICE co-processor that runs the Shell draws the menu into a small character-plus-attribute video RAM — one 16-bit word per character cell holding a character code and a color/inverse attribute, rendered through a 16×16 font ROM. The overlay compositor then paints that menu on top of the video wherever the menu is "on," pixel by pixel. It is an opaque replacement, not a blend: where the menu is present, it wins; everywhere else, the video shows through untouched.
The interesting part is that the overlay runs independently in each path — once in the analog pipeline, once in the digital pipeline — each with its own copy of the video RAM (QNICE writes both copies at once). Two facts about it will matter later:
-
The analog overlay is clocked by your
video_ce_ovl_o(the doubled enable from §2.5), because it must be drawn at the scandoubled resolution. The digital overlay is clocked once per HDMI pixel, because the scaler has already produced a clean one-pixel-per-clock stream. -
In the analog "retro 15 kHz" mode, where the picture has half as many lines, the overlay doubles its vertical coordinate so the menu still lands in the right place. This is one reason the
retro15kHzbit exists as a separate signal fromscandoubler.
The rendering internals are the On‐Screen‐Menu (OSM) article's job; for the video pipeline it is enough to know the menu is composited late in each path, at that path's resolution.
Finally, the pipeline does not only push pixels forward; it also measures the stream you feed
it and reports the numbers back to the Shell. A small module, video_counters.vhd, watches your
core's sync and blank signals and reconstructs the full raster anatomy from §1.2 — visible width
and height, each porch, each sync-pulse width — purely by timestamping the edges. A one-pulse-
per-second strobe lets it count horizontal sync pulses per second, giving the horizontal
frequency; from that and the raster totals the firmware derives the vertical refresh rate and
even the effective pixel clock, and shows them on the Shell's "core info" screen.
This is a debugging gift. When you bring up a new core and the picture is wrong, the core-info screen tells you exactly what raster the framework thinks you are producing — is it 720 wide or 360, is the refresh 50 Hz or 25, is the pixel clock what you expected — without a logic analyzer. It reads the signal at the same boundary the pipeline does, so it sees what the pipeline sees.
With the map in hand, we can now walk each path in detail. The analog path first, because it is the simpler of the two and it introduces the scandoubler that everything else refers back to.
The analog path's mission is modest and faithful: take your core's native RGB, deliver it to a display, paint the menu on it, optionally condition its sync, and drive the on-board DAC. It never rescales and never rebuilds — the picture that reaches the VGA port is your picture, at your timing. Its whole reason to exist is fidelity to a display, and it serves two very different ones: a modern VGA monitor, which needs the 15 kHz signal doubled to 31 kHz, and a genuine period CRT — a Commodore or Philips RGB monitor, a broadcast/PVM tube, a SCART TV — which wants the raw 15 kHz signal exactly as the 1980s hardware made it. Here is the chain in order; each stage gets a section.
the analog pipeline (analog_pipeline.vhd, all on YOUR video_clk)
core RGB + HS/VS + HBlank/VBlank + video_ce
│
▼
[1] video_mixer / scandoubler ── line-double 15→31 kHz (if enabled)
│ produces a clean Data Enable (DE)
▼
[2] force RGB = 0 wherever DE = 0 (the VDAC's rule)
│
▼
[3] OSM overlay ── paint the menu (clocked by video_ce_ovl)
│
├──▶ [4a] csync ── build composite sync from HS xor VS
└──▶ [4b] vga_sync_reshaper ── optionally re-shape HS/VS pulses
│
▼
[5] phase-shift registers (clocked on the FALLING edge of video_clk)
│
▼
VGA_RED/GREEN/BLUE + VGA_HS/VS ──▶ ADV7125 DAC ──▶ VGA connector
Recall the great divide of §1.4: a VGA monitor cannot sync to a 15 kHz retro signal. The scandoubler bridges the gap by the crude, effective method of drawing every input line twice. Store one incoming scanline in a small line buffer, then read it out twice as fast, onto two output lines — the picture is unchanged but each line is on screen for half as long, so the horizontal frequency doubles from ~15 kHz to ~31 kHz, and the monitor locks.
line doubling
input (15 kHz): line 1 line 2 line 3
─────── ─────── ───────
output (31 kHz): line 1 line 1 line 2 line 2 line 3 line 3
─────── ─────── ─────── ─────── ─────── ───────
(each source line emitted twice, at double the rate)
The scandoubler is an imported MiSTer module (scandoubler.v), wrapped by video_mixer.sv,
which M2M drives from the analog pipeline. Three things about how M2M uses it are worth knowing:
-
The two clock enables come home here. The scandoubler takes your native
video_ceas its input pixel rate and internally produces a doubled output rate. This is the concrete reason §2.5 asked you for a doubledvideo_ce_ovl— the doubled output is what the analog overlay and the DAC sample. (There is a genuine subtlety about which doubled enable to use; §4.4.) -
The fancy features are switched off. MiSTer's scandoubler can do "hq2x" smoothing and gamma correction; M2M disables both — the gamma stage synthesizes to nothing and the scandoubler runs with hq2x turned off. The analog output is plain line-doubling only — M2M has no menu to author a gamma table, and the sharp, unsmoothed look is what most people want on VGA. (Smoothing and CRT-look filtering do exist in M2M, but on the HDMI side, done by the scaler; Part 5.2.)
-
"Standard mode" means scandoubler on. When people say a core is in "Standard VGA" mode, they mean the scandoubler is engaged and the output is ~31 kHz — the setting for an ordinary VGA monitor or a modern display with a VGA input. This is the default, and it is the mode the sync reshaper (§4.5) applies to.
The scandoubler also emits the clean Data Enable signal the rest of the path relies on: it
computes DE = (not HBlank) and (not VBlank), i.e. "inside the active picture," which the next
stage needs.
Not everyone wants 31 kHz. If you are driving a genuine 15 kHz CRT or a SCART television, you want the raw retro signal, un-doubled, exactly as a period display expects. So the analog path offers three modes, controlled by the three independent AV-mode bits from Part 3.4:
| Mode | scandoubler | retro15kHz | csync | Line rate | For |
|---|---|---|---|---|---|
| Standard VGA | on | 0 | 0 | ~31 kHz | PC VGA monitor / modern display |
| Retro 15 kHz, H+V sync | off | 1 | 0 | ~15 kHz | 15 kHz RGB monitor (separate syncs) |
| Retro 15 kHz, composite sync | off | 1 | 1 | ~15 kHz | SCART TV (via the MiSTer adaptor) |
These really are three independent bits, not one three-way switch — the source even warns that
"scandoubler off does not automatically mean retro 15 kHz on." The retro15kHz bit does not
change the line rate itself (turning the scandoubler off does that); it tells the overlay to
adjust for the halved line count (§3.5) and, as we will see, gates the sync reshaper.
The third mode needs composite sync (CSYNC): a single combined sync signal that carries both
horizontal and vertical timing on one wire, the way a TV expects over a SCART connector. The
framework derives it from the separate syncs with the classic recipe — composite sync is
essentially HSync xor VSync, with the horizontal pulses inverted ("serrated") during the
vertical interval so a TV's horizontal oscillator keeps flywheeling through the vertical pulse.
When CSYNC mode is selected the framework routes this combined signal onto the VGA connector's
HSYNC pin (pin 13) and holds the VSYNC pin (pin 14) high, matching the wiring of the MiSTer
VGA-to-SCART adaptor. In the other two modes the separate HSYNC and VSYNC go to their normal pins.
The C64 core is the reference here: it offers all three as menu items, deriving the AV-mode bits
so that either 15 kHz sub-mode turns the scandoubler off, and only the SCART sub-mode raises
csync. The Amiga and Game Boy cores offer exactly the same three-way choice.
Driving a real tube is a genuinely supported use case, not a curiosity — but it comes with
hardware and safety strings attached, documented per core (the C64's doc/retrotubes.md is the
worked guide). You reach the tube through the VGA connector with the right cable: a VGA-to-SCART
cable — which must contain the correct resistors, because a plain pass-through cable feeds
the wrong voltages — for a Commodore 1084 or Philips monitor; a VGA-to-DB9-RGB cable for the
later Commodore monitors; or a VGA-to-5-BNC cable (RGBS, or RGBHV for separate syncs) for a
broadcast/PVM tube. Two rules protect the equipment: only ever feed a CRT the 15 kHz mode —
31 kHz Standard mode can damage the tube — and turn HDMI flicker-free off for analog use,
because the clock-dithering that smooths HDMI (Part 5.5) injects glitches a CRT will show. Those
are exactly the settings the retro sub-modes exist to provide.
A small but mandatory stage sits right after the scandoubler: force the RGB busses to zero
everywhere outside Data Enable. The MEGA65's video DAC (an Analog Devices ADV7125) misbehaves
if it is fed non-zero color values during blanking — it wants the analog outputs driven to the
blanking level, i.e. black, outside the visible window. So the framework explicitly zeroes R, G,
and B wherever DE = 0:
if DE = '1' then
vga_red <= mix_r; vga_green <= mix_g; vga_blue <= mix_b;
else
vga_red <= (others => '0'); -- black outside DE (ADV7125 needs it)
vga_green <= (others => '0');
vga_blue <= (others => '0');
end if;This is why "sync and blank must be active-high and correct" is not pedantry: the blanking signal literally decides when the DAC is allowed to emit color. A core with wrong blanking gets smeared borders or a confused DAC. (Many MiSTer cores already blank their RGB at the source; the democore does it too, as belt-and-suspenders. The framework enforces it regardless.)
Here the video_ce_ovl thread from §2.5 pays off, and it pays off with a genuine trap.
The analog overlay must be drawn at the scandoubled resolution, so it is clocked by
video_ce_ovl. For a core whose native resolution already equals VGA_DX×VGA_DY, that
overlay enable is just the native one, and the template's video_ce_ovl_o <= video_ce_o is
correct. For a real low-resolution core it must run faster — enough enables to lay down
VGA_DX pixels across each doubled line. (The C64 holds the overlay enable high on every video
clock in Standard mode; only in its retro 15 kHz mode does it drop to twice the native rate.)
But even that rule hides a subtlety that only bites the analog path, and the Game
Boy core is where it bites hardest. The MiSTer scandoubler paces its output pixels on a
free-running half-line schedule of its own — it restarts a counter at the falling edge of the
input's horizontal blank and emits one output pixel every few clocks from there. If you build
video_ce_ovl from your core's native pixel enable instead of from that same reference, the
two grids beat against each other: because the scandoubler's half-line length is not a whole
multiple of the native pixel period, the sampling phase drifts from one output line to the next.
Visibly, alternating lines shift sideways — a comb or ripple pattern — and, far worse, the
regenerated horizontal sync gets quantized differently each line, so its period wobbles by tens
of nanoseconds and analog monitors lose their lock.
The Game Boy core solves this by deriving video_ce_ovl, in Standard mode, from the scandoubler's
own reference — a half-line counter restarted at the same input-hblank falling edge, ticking twice
per output pixel, giving exactly 512 enables per visible line to match its 512-pixel-wide analog
canvas. In the 15 kHz modes, where there is no scandoubler, it instead builds a 2× grid from
phase-locked taps of the native pixel enable.
The lesson for the textbook is precise: counting VGA_DX enables per line is enough for a
CRT-native core, but a scandoubled fixed-panel core must phase-lock the overlay enable to the
scandoubler, not derive it from its own pixel clock. It is the kind of thing you discover in
simulation (the Game Boy authors did) rather than by reading a contract, and it is exactly the
sort of hard-won detail this pipeline hides from cores that don't need it.
We reach the newest piece of the analog pipeline, and a small, self-contained puzzle that illustrates a whole class of real-world video problems: a signal can be electrically perfect and still be misunderstood by the display.
Recall from §1.6 that a modern VGA monitor guesses which video mode it is receiving partly from the width and polarity of the sync pulses, not from the picture itself. VESA's DMT standard prescribes, for each PC mode, a specific horizontal-sync pulse width and specific sync polarities (640×480 at 60 Hz, for instance, uses a 96-pixel-wide negative HSYNC and a 2-line negative VSYNC). A retro core's scandoubled output has whatever sync widths and polarities the original machine happened to use — often a wide, positive pulse nothing like the VESA numbers. Some monitors take one look at those pulses, decide "this is consumer 480p video, not a PC mode," and switch on overscan and consumer image processing, or refuse the signal outright. The picture is there; the monitor is just filing it under the wrong heading.
The VGA sync reshaper (vga_sync_reshaper.vhd) fixes exactly this, and only this. When
enabled, it preserves the leading edge of every sync pulse and the complete raster period,
but replaces the pulse widths and output polarities with values a monitor will recognize. It
does not move the picture, change the resolution, or alter the frame rate — nothing about the
raster geometry or timing changes. It only re-shapes the two sync pulses so the monitor classifies
the mode correctly.
Two design decisions make it safe to leave in the framework for everyone:
-
It is downstream of the split and the overlay, and it only touches Standard mode. The reshaper sits after the analog/digital fork and after the analog OSM, so HDMI always keeps the original core syncs, and the reshaper only engages when the scandoubler is on and neither retro mode is active. In the 15 kHz and composite-sync modes it is a straight wire-through — those modes feed period displays that want the native pulse shapes.
-
Its default is an exact bypass. The configuration is a record, and the stock value,
C_VGA_SYNC_RESHAPER_OFF, disables the logic entirely and passes the syncs through unchanged. A core that does nothing gets byte-identical behavior to a framework without the feature — which is the backward-compatibility promise the whole framework lives by.
You opt in by setting one constant in CORE/vhdl/globals.vhd, VGA_STD_SYNC. The framework
provides ready-made VESA DMT presets and a helper function that converts a preset — expressed as
a physical sync duration at a reference pixel clock — into the number of your video-clock cycles
that duration works out to. That indirection is what lets one preset work for any core, whatever
its clock:
-- OFF by default: an exact wire-through (backward compatible)
constant VGA_STD_SYNC : vga_sync_reshaper_cfg_t := C_VGA_SYNC_RESHAPER_OFF;
-- To opt in: pick a DMT preset and let the framework convert its sync
-- duration into cycles of your own video clock:
-- constant VGA_STD_SYNC : vga_sync_reshaper_cfg_t :=
-- make_vga_sync_reshaper_cfg(C_VGA_SYNC_DMT_640X480_60, CORE_CLK_SPEED);The second argument is your video-clock frequency. It equals CORE_CLK_SPEED only when your
core reuses its main clock for video (as the template does); a core with a separate video clock
passes that clock's frequency instead — the Game Boy, below, passes its VIDEO_CLK_SPEED.
The preset C_VGA_SYNC_DMT_640X480_60 carries the standard 640×480@60 numbers — a 96-pixel HSYNC
at a 25.175 MHz reference clock, a 2-line VSYNC, both negative. make_vga_sync_reshaper_cfg scales
that 96-pixel duration into however many cycles of your video_clk produce the same physical time,
and packages it with the polarities. Two more presets ship for 800×600 and 1024×768. Because the
preset is described in physical time and converted to your clock, you never do the arithmetic
yourself.
This feature is not hypothetical — it exists because a shipping core needed it. The Game Boy's
scandoubled output is 512×448, which is geometrically close to consumer 480p, and it carries
lcd.v's wide, positive 32-pixel horizontal sync. Several analog VGA monitors saw that and engaged
their consumer-video path (overscan, wrong scaling). The Game Boy core turns the reshaper on with
exactly the 640×480 preset, converted against its own video clock:
-- gbc4mega65: present VESA-like sync so Standard mode is read as PC VGA
constant VGA_STD_SYNC : vga_sync_reshaper_cfg_t :=
make_vga_sync_reshaper_cfg(C_VGA_SYNC_DMT_640X480_60, VIDEO_CLK_SPEED);Every source edge and the exact frame period are preserved; only the HSYNC narrows to the VESA
width and both syncs flip to negative polarity — enough for the monitors to reclassify the signal
as a PC mode and drop the consumer processing. It is the whole feature in one real line of a real
globals.vhd.
The reshaper is not the only way to make a monitor happy with an analog picture, and the Amiga
core solves a related but different problem a different way — a useful comparison because it
sharpens what the reshaper is and isn't. AExp does not reshape sync pulses; instead it carries a
runtime, SD-card-tunable screen-centering system that can pan the whole analog picture by
retiming the sync phase, soft-blank overscan edges, and re-crop the HDMI input — per detected Amiga
video mode. The reshaper is a static, compile-time pulse-shape fixup that never moves the raster;
the Amiga's screen-centering is a dynamic, per-mode repositioning that deliberately does move it.
They address adjacent complaints ("the monitor mis-reads my mode" versus "the picture sits off-center
for this particular Amiga mode") and neither replaces the other. Knowing both exist keeps you from
reaching for the wrong tool.
The last analog stage is a set of output registers clocked on the falling edge of video_clk.
Sampling the RGB, HSYNC, and VSYNC half a clock late deliberately phase-shifts them so the ADV7125
DAC latches a steady, settled signal rather than one caught mid-transition; the framework registers
the syncs the same way as the colors so all of them stay aligned on the wire. These registers are
also physically placed (via a floorplanning constraint) close to the FPGA's VGA output pins, to keep
the routing delay to the DAC small. The framework then ties the DAC's own control lines to their
"just pass the analog through" values — no sync-on-green, no DAC-side blanking (the framework did the
blanking itself, in §4.3) — and drives the DAC clock from video_clk.
And that is the whole analog path: native RGB, doubled for a VGA monitor (or left raw for a CRT), menu painted on, sync optionally reshaped, phase-aligned, and handed to the DAC. Nothing here changed your picture's fundamental identity. The digital path, which we turn to now, does the opposite: it takes your picture apart and builds a brand-new one.
Where the analog path preserves, the digital path reconstructs. A modern HDMI display will not accept a C64's 50.124 Hz or a Game Boy's 59.7275 Hz — it wants a standard mode at a standard rate. So the digital path captures whole frames from your core, rescales them to one of the seven fixed HDMI modes, paints the menu, encodes the result as HDMI, and serializes it out the connector. It is a much bigger machine than the analog path, and it leans on the MEGA65's HyperRAM to hold frames while it works. Here is the chain:
the digital pipeline (digital_pipeline.vhd)
core RGB + HS/VS + HBlank/VBlank + video_ce (on YOUR video_clk)
│
▼
[1] crop / zoom-in ── optionally enlarge blanking to shrink the
│ active picture (so the scaler zooms it up)
▼
[2] ascal ── the scaler: autodetect input size, rescale to the
│ ▲ chosen HDMI mode, filter, and buffer one
│ │ frame in ──▶ HyperRAM ◀── read back at the fixed
│ └────────────── (via 128-bit Avalon bus) HDMI rate
▼ (crossing here from video_clk to hdmi_clk)
[3] OSM overlay ── paint the menu at HDMI resolution
│
▼
[4] vga_to_hdmi ── VGA-style timing → TMDS symbols (+ audio, unless DVI)
│
▼
[5] 4x serialiser_10to1 ── 3 colour channels + 1 clock channel
│
▼
TMDS differential pairs ──▶ HDMI connector
Before the scaler rebuilds your picture, two questions have to be answered: what part of your picture is shown, and what shape and size it becomes when it lands in the HDMI frame. The first is cropping (the input side); the second is output fitting (the output side). They are separate mechanisms that happen to share the one "zoom" menu bit, and keeping them straight is the whole of this section.
The first stage is optional and, at first glance, backwards: to zoom in, it makes the picture smaller. The trick is that the scaler downstream always stretches whatever active area it is given to fill its target rectangle. So if you enlarge the blanking around the picture — telling the scaler "the active area is only this inner rectangle" — the scaler blows that smaller rectangle up, and the result is a zoom.
crop.vhd does this by counting pixels and lines from the incoming blanking and, when the "zoom"
menu bit is on, forcing the blanking active for everything outside a fixed inner window. It never
moves or resamples anything; it only adds blanking, which is why it can only zoom in, never out.
There is a sharp gotcha here, and it is a recurring theme of this framework: the crop window is hardcoded to a 320×200 picture — the Commodore 64's native active area — with fixed margins. For the C64 core the window fits perfectly. For any other core it is wrong unless you edit the constants: the Game Boy core, for instance, uses zoom specifically to strip its black Super Game Boy border, and it gets a razor-sharp 5× integer scaling of the 160×144 image at 720p precisely because it tuned the crop for its own geometry.
Cropping decided what the scaler sees. Output fitting decides the rectangle inside the HDMI frame that the scaler draws into — and therefore the picture's shape and any bars around it. This is where the aspect-ratio subtleties from §1.7 come home to roost, and it is a first-class, core-configurable feature.
Recall the problem §1.7 set up: you cannot reason about a picture's shape from pixel counts, because neither retro pixels nor HDMI's SD pixels are square. A 720p frame is physically 16:9; a "576p" frame is 720×576 encoded pixels but physically 4:3. If you want a 4:3 retro picture to look 4:3 on a 16:9 HDMI screen, you must draw it into a narrower rectangle with pillarbox bars left and right; on a 4:3 frame you fill it; and if the target shape is wider than the frame you letterbox with bars top and bottom. Getting that rectangle right, for every combination of HDMI mode and desired aspect, is fiddly — and it depends on the mode's physical aspect, not its pixel count.
The framework does this for you through a single policy, HDMI_VIEW, in CORE/vhdl/globals.vhd. It
holds two independent fits — one for the uncropped view (used while the zoom bit is off) and
one for the cropped view (used while it is on) — so "normal" and "zoomed" can each have their own
shape. Each fit chooses one of three modes:
-
LEGACY— reproduce M2M's historical per-mode rectangle exactly (the 16:9 modes get a 4:3 pillarbox, the 5:4 mode a letterbox). This is what keeps existing cores pixel-for-pixel unchanged. -
FULL_FRAME— fill the entire HDMI active area, edge to edge, aspect be damned. -
ASPECT— fit a physical aspect ratio you name (4:3,5:4,8:7,10:9,16:9,1:1, or anymake_hdmi_fit(w, h)), centered, with pillarbox or letterbox bars as needed.
The clever part is ASPECT mode. You give it a physical ratio like 4:3, and it derives the pixel
rectangle from the HDMI mode's advertised physical aspect (the 4:3-or-16:9 flag the mode sends in
its HDMI info-frame), not from the mode's pixel dimensions. That is exactly what makes it correct
for the non-square-pixel SD modes: it knows 720×576 is physically 4:3 and computes the rectangle so
the result is the shape you asked for, whatever the pixels' squareness. You think in shapes; the
framework does the pixel arithmetic.
Two engineering details are worth knowing. First, every rectangle is computed at elaboration
(synthesis) time — the framework builds a small table of one rectangle per HDMI mode, for each
view, as VHDL constants, and the only runtime hardware is a mux that picks the rectangle for the
current mode and zoom bit; no division happens on the fly. Second, the stock value
C_HDMI_VIEW_LEGACY is {UNCROPPED => LEGACY, CROPPED => FULL_FRAME} — the old behavior exactly
(the normal view keeps the historical placement; zoom fills the frame) — so a core that sets nothing
is completely unaffected. It is the same non-breaking-default discipline as the sync reshaper and the
interlace flag.
Configuring it is one line. To keep the normal view as-is but present, say, a cropped 10:9 picture:
-- default: exactly the legacy placement
constant HDMI_VIEW : hdmi_view_cfg_t := C_HDMI_VIEW_LEGACY;
-- opt in: legacy uncropped view, a 10:9 fit when the user zooms
-- constant HDMI_VIEW : hdmi_view_cfg_t :=
-- make_hdmi_view_cfg(C_HDMI_FIT_LEGACY, C_HDMI_FIT_10_9);So the two mechanisms sit at opposite ends of the scaler: crop.vhd shrinks what goes in, and the
HDMI_VIEW fit shapes what comes out. Together they let a core say, precisely, "show this part of
my picture, at this physical shape, in both the normal and the zoomed view."
The heart of the digital path is ascal.vhd, a large, third-party polyphase video scaler (from the
temlib project; roughly 2900 lines you should never edit). Conceptually its job is simple to state
and hard to do: take the core's native RGB at its native, ragged timing, write each frame into a
single-frame buffer in HyperRAM, and read it back out at the exact timing of the chosen HDMI mode —
resampling in the process from the core's resolution to the mode's resolution.
A few facts about how M2M uses it are enough for a porter:
-
It autodetects your input size. You do not tell the scaler how big your picture is; it measures the active region for itself and reports the detected width and height back to the firmware (those are the numbers you see next to the raw raster measurements on the core-info screen from §3.6). This matters because retro cores have variable, ragged active regions; autodetection is the sane default.
-
The frame buffer lives in HyperRAM — one frame of it. The scaler writes over a wide (128-bit) Avalon memory bus (Avalon is Intel's memory-bus protocol), which a width converter narrows to the 16-bit HyperRAM the MEGA65 actually has. It is allocated exactly one frame —
width × height × 3bytes (24 bits of color per pixel), rounded up to a power of two — and used as a single full-frame buffer: the framebuffer display mode and triple-buffering are both off, so one frame is resident at a time and the write pointer (core) and read pointer (HDMI) chase each other around it. That single-frame chase is the only heavy memory user in the base framework, it is why the HDMI path and any HyperRAM your core uses must share the same arbitrated memory (see HyperRAM for Beginners), and — as §5.5 shows — it is the very thing that makes both the frame-rate mismatch and its cure possible. -
Storing frames in RAM is exactly what makes the mismatch — and flicker-free — possible. Because the scaler decouples the write side (your core's rate) from the read side (HDMI's rate) through a RAM buffer, the two can run at different frame rates at all. That decoupling is a feature, and it is also the source of the tearing problem we solve in §5.5. Hold that thought.
The scaler can resample with several filters, and the choice is what a user perceives as image quality. From cheapest to richest: nearest-neighbor (blocky, sharp pixels), bilinear (soft), sharp bilinear, bicubic, and polyphase (a programmable finite-impulse-response filter with loadable coefficients). The framework builds all of them into the silicon at compile time; a runtime mode word selects which is active, and one bit of that word toggles triple-buffering.
In the template, this is wired straight to the menu: an "HDMI: CRT emulation" item flips the scaler to
polyphase mode. But all three reference cores do something richer. They set the scaler-control policy
to "hands off, the firmware drives it" (the ASCAL_USAGE = 1 setting from §3.4) and implement a full
HDMI filter menu in QNICE assembly. The C64 core's menu is the model: eight options —
No Filter (nearest neighbor)
Sharp Bilinear
Bicubic
Smooth (polyphase, a gentle sharpness curve)
Lanczos (polyphase, a sharp windowed filter)
Scanlines (polyphase: Lanczos horizontally + a scanline
brightness curve vertically) ← the default
CRT (S-Video) (polyphase, an S-Video-like horizontal blur)
CRT (Composite) (polyphase, a composite-like horizontal blur)
The three "native" options just set the scaler's mode; the five polyphase options additionally push a pair of coefficient tables (one horizontal, one vertical) into the scaler's filter RAM. The default, "Scanlines," is bit-for-bit the old single "CRT emulation" toggle — a horizontal Lanczos plus a vertical scanline-darkening curve — and switching filters at runtime just rewrites the coefficient RAM, no core reset needed. The Amiga and Game Boy cores carry the same eight-option system. (The coefficient machinery and the QNICE side belong to QNICE and the Shell ROM (m2m-rom.asm); here it is enough to know the "CRT look" is a scaler filter, chosen at runtime, applied only on HDMI.)
Recall the Amiga's laced hi-res modes from §1.5, where each frame arrives as two half-height fields. The scaler is where they are stitched back together, and following the one-bit field flag through the pipeline is a small, satisfying story — and the model for how the framework grows a new feature without breaking old cores.
The core drives video_fl_o with its field flag (§2.6). The framework carries that bit — under the
name video_fl_i, and crucially defaulting to '0' at every stage — through framework.vhd,
av_pipeline.vhd, and into the scaler's interlace input, i_fl. With interlace enabled, the scaler
watches that bit: when it sees the flag toggling, it concludes the input is interlaced and weaves
— it offsets the write address by one line for alternate fields so the two half-height fields interleave
into one double-height frame in the buffer. When the flag stops toggling (a progressive source), it
disarms itself a few frames later and behaves exactly as before. A constant '0' is therefore
bit-identical to "progressive," which is why a core that never drives the flag is completely unaffected.
That default-'0' discipline is the template for every non-breaking framework enhancement: a new port
that defaults to the old behavior lets existing cores re-synthesize unchanged. The Amiga's interlace
support is the canonical example, and the reshaper of §4.5 follows the same rule with its
C_VGA_SYNC_RESHAPER_OFF default.
There is a nice asymmetry in where deinterlacing happens for the Amiga, which pins down three concepts at once:
- On HDMI, the scaler weaves electronically, so laced modes like 640×512 appear full-height and deinterlaced.
- On analog Standard (scandoubled) VGA, the pipeline is field-blind — it "bobs," showing each field doubled, because the scandoubler does not weave.
- On analog 15 kHz, nothing needs to deinterlace at all: the raw interlaced signal, half-line offset and all, goes straight to a CRT, which weaves it natively the way the Amiga's designers intended — the most authentic picture of the three.
One signal, three destinations, three different answers to "who reassembles the fields."
After the scaler and the HDMI-side menu overlay, the picture is an ordinary VGA-style stream again — RGB plus active-high sync plus data-enable — but now at a clean, standard mode. The last job is to turn that into an actual HDMI signal on wires. Two imported modules (from the Tyto project) do it.
vga_to_hdmi.vhd is the encoder. It does three things worth naming:
-
It fixes the sync polarity for the mode. The pipeline works in active-high sync throughout, but each HDMI mode declares its own required sync polarity; the encoder reconciles them with a single exclusive-nor per sync, so upstream stages never have to care about polarity. (This is the other end of the polarity story from §1.3.)
-
It distinguishes HDMI from DVI. HDMI is DVI plus "data islands" — packets carrying audio samples, audio-clock-regeneration data, and info-frames describing the video and audio format. The
qnice_dvi_omenu bit collapses all of that away: in DVI mode the encoder emits video only, no audio, no info-frames. This is why toggling one menu item can make a DVI-only monitor (or a DVI adapter) go from silent-with-a- picture to working — some sinks choke on the audio islands. Audio rides at a fixed 48 kHz, muxed into these islands, which is the only reason sound appears in a video module at all. -
It emits TMDS symbols, not serial bits. TMDS — transition-minimized differential signaling — is HDMI's line code; the encoder's output is three parallel 10-bit symbol streams, one per color channel, the result of its 8-bit-to-10-bit encoding, which balances the number of ones and zeros on the wire. Turning those parallel symbols into a serial bitstream is a separate step, and people routinely conflate the two.
That separate step is serialiser_10to1_selectio.vhd, and it has one detail that always raises an
eyebrow: it serializes ten bits per pixel using a clock that is only five times the pixel clock, not
ten. The trick is double data rate — the Xilinx output serializer clocks a bit on both edges of
the fast clock, so 5 × 2 = 10 bits per pixel. (Ten bits also exceeds one serializer primitive's
8-bit limit, so two are cascaded in a master/slave pair.) There are four of these serializers: three
carry the color channels, and a fourth carries the HDMI clock — as data. The clock channel is just
the constant pattern "0000011111" pushed through an identical serializer, producing a square wave at
the pixel rate with exactly the same routing and timing as the three data lanes, so all four differential
pairs stay perfectly matched. The HDMI clock is not a separate clock buffer; it is five zeros and five
ones through the same machine as the pixels.
TMDS on the wire (per pixel)
vga_to_hdmi: 8-bit R ─▶ 10-bit symbol ─┐
8-bit G ─▶ 10-bit symbol ─┼─▶ 3 parallel symbol streams
8-bit B ─▶ 10-bit symbol ─┘
│
serialiser (x4): symbol ─▶ serial bits at 5x pixel clock, DDR (=10 bits)
data lane 0,1,2 + clock lane "0000011111"
│
4 differential TMDS pairs ─▶ HDMI
Now the punchline the whole article has been walking toward, and the most important thing a serious core does with the digital pipeline.
Recall two facts. First (§1.6), your core's frame rate is not round: a PAL C64 runs at 50.124 Hz, an Amiga at ~50 Hz, a Game Boy at 59.7275 Hz. Second (§5.2), HDMI insists on an exactly standard rate — 50.000 or 60.000 Hz — and the scaler makes the mismatch survivable by buffering frames in HyperRAM: the core writes frames in at its rate, HDMI reads them out at the standard rate.
But buffering only defers the problem. If the writer and the reader run at even slightly different rates, the write pointer and the read pointer drift relative to each other in the buffer. Eventually the faster one laps the slower one, and at that instant the display shows a frame that is half-new and half-old: a horizontal seam that jumps — a tear, and on a periodic mismatch, a regular flicker.
the write/read chase in the HyperRAM frame buffer
[====================================================] one frame
▲read pointer (HDMI, exactly 50.000 Hz)
▲write pointer (core, ~50.124 Hz — slightly faster)
Each frame, the faster pointer creeps further ahead. When it laps
the other, one displayed frame is part-old + part-new = a tear.
Fix: keep the write pointer a small, steady distance ahead of the
read pointer by nudging the CORE clock a hair faster or slower.
The framework provides the measurement for free. A module, hdmi_flicker_free.vhd, watches the
scaler's HyperRAM traffic and, once per output frame — at the instant the read pointer wraps back to
the start — samples how far ahead the write pointer is. If that lead has shrunk too small, it raises a
signal meaning "the core is too slow"; if it has grown too large, one meaning "the core is too
fast." Between the two thresholds it says nothing — a deliberate dead-band so the decision does not
chatter. Those two signals, hr_high ("too fast") and hr_low ("too slow"), are handed to your core.
The framework cannot act on them, though, because it does not control your clock — you do. So
flicker-free is a mechanism the framework offers and the core must opt into, by running its pixel/
main clock from a source it can nudge, and consuming hr_high/hr_low to nudge it. The template does
not do this — its clock is fixed — which is exactly why the template tears on HDMI and why "flicker-
free is computed but not wired" is a standing note about the demo. A real core wires it, and all three
of ours do, in an instructive way.
The universal recipe is: build two slightly different clock speeds that bracket the exact HDMI rate — one just above it, one just below — and switch between them, glitch-free, based on the feedback, so the core's average rate tracks the HDMI rate and the write pointer stays a steady distance ahead. The subtlety — and the reason flicker-free is genuinely core-specific — is which side the native rate falls on:
the two speeds bracket the fixed HDMI rate; the core dithers
between them so its average lands on it. Which side is "native"
depends on the machine:
slower ◀─ HDMI rate ─▶ faster
C64 (vs 50 Hz): 49.999 Hz │ 50.124 Hz (native is ABOVE)
Amiga (vs 50 Hz): native │ twin (native is BELOW)
Game Boy (vs 60 Hz): native │ twin (native is BELOW)
-
Commodore 64. Its native PAL rate, 50.124 Hz, is above 50. So it builds a near-native "orig" clock giving a 50.124 Hz frame rate and a slightly slower twin giving 49.999 Hz. "Too fast" selects the slower twin; "too slow" selects the original. Two static clock generators feed a glitch-free multiplexer, and the design only releases reset once both are locked. The C64's own manual literally tells users to enable "HDMI: Flicker-free" — the whole two-clock machine exists so that one sentence is true.
-
Amiga (
AExp). Its native rate is below 50. So the mapping is inverted: "too slow" selects the faster twin (above 50), "too fast" falls back to the slower native. Same machinery, opposite wiring — a perfect illustration that you cannot copy another core's flicker-free FSM blindly; you must know which side of the HDMI rate your machine sits on. -
Game Boy (
gbc4mega65). Its native rate, 59.7275 Hz, is below 60, so like the Amiga "too slow" picks the faster twin. But it adds a fixed-panel twist: recall it runs a video clock at exactly twice its machine clock (§2.4), and that 2:1 ratio must never break. So it switches both clocks together, from the same multiplexer select, keeping the ratio intact. A fixed-panel core with a hard N:1 video-to-machine clock ratio has to switch every clock in lockstep; a CRT-native core that reuses one clock for everything does not face this.
In every case a menu item gates the whole thing (users who output to a raw analog monitor want it off, because the speed-dithering that smooths HDMI would inject glitches into a CRT that is following the core's sync directly). And in every case the two clock speeds are so close, and the switch so rare — less than once a second — that nothing visible or audible betrays the dithering. The core's clock is gently, invisibly pulled into lockstep with the display, and the tear never happens.
This is the feature that separates a demo from a shippable core, and it is the clearest example in the whole framework of the division of labor this article keeps returning to: the framework measures and offers; the core decides and acts.
With both paths walked end to end, the last part steps back and compares how our three cores made their choices — the concrete payoff of everything above.
Everything in this article converges on one question a porter has to answer: given my machine, how should each stage of the pipeline behave? The three reference cores answer it three different ways, and laid side by side they turn the abstract pipeline into a set of concrete decisions you can copy. If you have read this far, this table should read as a summary, not a surprise.
Commodore 64 (C64MEGA65) |
Amiga 500 (AExp) |
Game Boy Color (gbc4mega65) |
|
|---|---|---|---|
| Native source | VIC-II, real RGB + sync | OCS chipset, real RGB + sync | LCD panel: pixel strobes, no sync |
| How the raster is made | core reshapes + crops it (own video_sync.vhd, to 382×270) |
emitted by the chipset |
fabricated by lcd.v (425×264) |
| Horizontal frequency | ~15.64 kHz | ~15.625 kHz | synthetic |
| Native frame rate | 50.124 Hz (PAL) | ~50 Hz (PAL) | 59.7275 Hz |
| Main / video clock | ~31.53 MHz | ~28.375 MHz | 33.5 MHz main, 67.1 MHz video (2×) |
| Scan type | progressive | progressive + interlaced hi-res | progressive |
Analog canvas (VGA_DX×VGA_DY) |
720×540 | 720×576 | 512×448 (160×144 in a black SGB border) |
| Analog modes | Standard / 15 kHz H+V / 15 kHz CSYNC | same three | same three |
| HDMI modes offered | PAL 50 Hz (16:9 default, 4:3, 5:4) | PAL 50 Hz only (16:9, 4:3, 5:4) | 60 Hz family (16:9 default, 720×480, 640×480, 800×600) |
| HDMI filters | 8-option custom (Scanlines default) | 8-option custom | 8-option custom |
| Interlace weave | — |
yes (video_fl → scaler) |
— |
Sync reshaper (VGA_STD_SYNC) |
— | — (uses runtime screen-centering) | yes (DMT 640×480@60) |
| Flicker-free | yes — native above 50 Hz (twins 50.124 ↔ 49.999 Hz) | yes — native below 50 Hz (mapping inverted vs C64) | yes — native below 60 Hz; both clocks switch in lockstep |
| Zoom / crop | fits its native 320×200 | screen-centering | strips the black border → 5× integer @ 720p |
Read down each column and you have that core's entire video design. Read across each row and you have a menu of how one decision can go. Three short syntheses:
The Commodore 64 is the "textbook CRT-native core." Its chip emits a real 15 kHz RGB signal; the core
conditions that signal in main.vhd (reshaping the sync and cropping to a clean window), then lets the
framework do the rest — scandouble for VGA, scale for HDMI. Its two contributions to your mental model are
its rich firmware-driven HDMI filter menu (the eight-option system every core now copies) and its
flicker-free implementation, whose native-above-50 clock pair is the canonical example. It uses neither
interlace nor the sync reshaper, because it needs neither.
The Amiga is the "everything-at-once" core. It is the reason the framework has interlace weave at
all: its hi-res laced modes drive video_fl, the scaler weaves on HDMI, the 15 kHz output lets a CRT weave
natively, and Standard VGA bobs — the clearest tour of "where deinterlacing happens." Its flicker-free is
the C64's, inverted, because the Amiga sits below 50 Hz. And instead of the static sync reshaper it
carries a runtime, per-mode screen-centering system — a reminder that the framework's built-in tools are
a starting point, and a core with a specific need can build its own.
The Game Boy is the "third kind of core," and it stresses the pipeline in ways the CRT machines never
do. With no native video signal, it fabricates a raster and double-buffers a fixed panel into a stream.
It is the real-world user of the sync reshaper (its scandoubled 512×448 output was being misread as
consumer 480p). Its video_ce_ovl has to phase-lock to the scandoubler to avoid comb artifacts and
sync jitter — the hardest overlay-CE case in any core. And its flicker-free has to switch two clocks in
lockstep to protect its fixed 2:1 video-to-machine ratio. If you are porting anything that isn't a CRT
machine — a handheld, an LCD console, an arcade board with an exotic scan — the Game Boy is your reference.
Distilling the whole article into the order you actually do things:
-
Get the pixels into
main.vhd. Instantiate your core; map itsR/G/B,HSync,VSync,HBlank,VBlankto thevideo_*_oports. If your core lacks blanking, reconstruct it withvideo_sync.vhd(§2.3). If it has no sync at all, fabricate a raster (§2.4). Ensure sync and blank are active-high and RGB is zero during blanking. -
Produce two clock enables.
video_ce_oat the native pixel rate;video_ce_ovl_oat the post-scandoubler rate matchingVGA_DX/VGA_DY. Do not blindly tie them equal unless your native resolution already fills the analog canvas (§2.5, §4.4). -
Set the fixed facts in
globals.vhd.VGA_DX/VGA_DY(post-scandoubler size),CORE_CLK_SPEED(exact), audio coefficients; only if a monitor misreads your Standard mode,VGA_STD_SYNC(§4.5); and, if you want a specific HDMI shape or a zoomed aspect, theHDMI_VIEWoutput fit (§5.1). -
Wire the menu policy in
mega65.vhd. Drive the AV-mode outputs (§3.4) from menu bits: HDMI mode, the three analog modes, zoom, DVI, audio. Keep theC_MENU_*indices in sync with the menu. -
Write the menu in
config.vhd. The HDMI-mode radio group and the analog-mode choice; setASCAL_USAGE(2for menu-driven filters,1if you build a firmware filter menu like the reference cores). -
Decide on the hard features. For a tear-free HDMI picture, implement flicker-free with a
dual-speed clock that brackets the HDMI rate on the correct side (§5.5). For an interlaced machine,
drive
video_fl_o(§5.3). For a fixed panel, mind the overlay-CE phase-lock (§4.4). - Verify on the core-info screen. Confirm the framework measures the raster, refresh, and pixel clock you expect before chasing anything harder (§3.6).
Steps 1–5 get a picture on both connectors; step 6 is what makes it good; step 7 is how you check your work without a logic analyzer.
- The exact porting steps — The Ultimate MiSTer2MEGA65 Porting Guide turns the checklist above into concrete edits, file by file, with the MiSTer-to-M2M pattern catalog.
- Clocks, clock enables, and building the flicker-free dual clock — [[Clocking and Clock Domain Crossing]] is the companion to this article; flicker-free lives at the intersection of the two.
-
The on-screen menu and its state register — On‐Screen‐Menu (OSM) covers the overlay's rendering
and the
C_MENU_*menu-bit contract this article leaned on. -
The switches you set in config.vhd — config.vhd Switches and Settings is the exhaustive reference
for
ASCAL_USAGE, the menu structure, and the video-related general settings. - The memory the scaler runs on — HyperRAM for Beginners and [[HyperRAM FIFO and Caching Strategies]] explain the RAM the HDMI frame buffer (and flicker-free) depends on.
- The firmware that drives the HDMI filters — QNICE and the Shell ROM (m2m-rom.asm) covers the QNICE side of the eight-option filter menu.
- The MiSTer side you are reproducing — MiSTer Quick Reference Guide describes what a MiSTer core hands you before M2M gets involved.
- Background primers — the video-timing and pixel-clock links in §1.2 are a gentle, ten-minute foundation if any of the raster vocabulary still feels new.