MAD-65
Contents

MAD-65 Future Features — designed, NOT implemented#

Two features that are fully designed but not built. They live here rather than in the OS references so those stay descriptions of what the ROMs actually do. Nothing in this document is implemented; it exists so the design work does not have to be re-derived.

See MAD65_GPU_OS.md for the implemented instruction set and MAD65_CPU_OS.md for the CPU-side builders and the replay machinery both features would have to hook into.


"Comb" rendering#

Two related ideas, both built on the same trick: render each source row to every second destination row. The drawn thing becomes 2× taller on screen for the same number of source rows, and the skipped rows are simply never written — whatever was underneath shows through. The result is a hard-edged horizontal comb, i.e. a ghost/hologram/scanline look. Neither is implemented; this section records the design and the gotchas so the work does not have to be re-derived.

Expectation-setting on the look: the display is 400×300 logical row-doubled to 800×600, so a skipped logical row is a 2-physical-scanline gap next to a 2-physical-scanline band. This reads as bold venetian blinds, not as a smooth 50 % fade. It is the right tool when you want something to look unstable; it is the wrong tool when you want "faded".

A — Comb sprites (ghost sprites)#

A new opcode ($51 is free, arguments identical to OP_SPRITE) that blits an existing sprite with a doubled vertical step. Same sprite data, same sprite-table slot, no new asset format.

B — Comb backgrounds#

The same stride trick applied to the background layer. The economics are completely different from sprites — see below.

Sequencing#

A is self-contained and adds no new plumbing. B is the bigger unlock but touches the replay records; once B's rectangular background blit exists, its comb variant is nearly free. Naming note: call it comb / ghost, not "semi-transparent" — the latter oversells what is actually on screen.


Vertical (TATE) mode#

Support for vertical games: the physical monitor is turned on its side and the game is authored for a 300×400 portrait screen. Nothing is implemented; this section records the design and the decisions so the work does not have to be re-derived.

There is no "vertical mode"#

The single most important finding: a 90° rotation maps the 400×300 framebuffer onto itself, and every drawing routine in gpu_os.s already works in framebuffer coordinates, not screen coordinates. SPR_BLITTER walks SPR_VRAM += 50 per row; line_core is Bresenham-symmetric; op_load is a linear page copy. None of them know or care which way the monitor is bolted.

So the whole feature is: pre-rotate the assets, and add text opcodes that use a rotated font. There is:

The developer works directly in framebuffer coordinates and thinks "x runs down the screen". A transform layer in the CPU1 command builders was considered and rejected: it buys little and it breaks on sprite anchors, because (X,Y) is the top-left corner in framebuffer coordinates and after rotation that is a different corner on screen — the builder would have to know each sprite's dimensions.

Note the wire format needs no change: OP_PIXEL already carries X and Y as 16-bit, and the line family's half-res bytes (0–199 / 0–149) stay in range when swapped.

Rotation direction: CLOCKWISE (decided)#

The monitor is turned 90° clockwise — its top edge goes to the viewer's right. Consequently:

framebuffer axis points (viewer) carries
fb-x + (0–399, 50 byte columns) down text lines, 8 px each → 50 lines, exact fit
fb-y + (0–299, 50-byte stride) left characters within a line, 8 rows each

Characters must advance along fb-y — that is what makes vertical text byte-aligned, and it is not negotiable. Because fb-y + points left under CW, the character index is the one that runs backwards: character c sits at a decreasing fb-y, so the inner loop advances by -400 instead of +400. The line index is the identity (screen line L → byte column L, top to bottom).

Why clockwise. The cost is symmetric — either direction requires reversing exactly one of the two indices, which is one subtraction at setup and sbc vs adc in the inner loop, the same cycle count. So it was decided on other grounds:

  1. The line axis is the exact-fit one (400/8 = 50, zero slack); keeping it a direct index is tidier when reading a memory dump.
  2. The required negation folds into the char axis, where ×8 + offset is computed anyway, rather than adding a second reversed index elsewhere.
  3. Most single-direction monitor pivot stands go clockwise, so anyone building a MAD-65 without a dual-pivot stand can still use it.

History gives no convention to copy. 1980s vertical cabinets used ordinary 4:3 raster CRTs bolted in rotated 90°, with the board designed vertical from the start and the artwork stored pre-rotated — architecturally the same thing as this design, not a horizontal game flipped afterwards. But direction varied freely per product (MAME encodes it per-driver as ROT90 vs ROT270, and both are common; even a single manufacturer went both ways). There is no "wrong" answer to match.

This decision is cheap to reverse. The direction appears in exactly three places: the rotate-90 flag in the asset script, the same flag in the font generator, and the sign of the char-axis advance in VTEXT. Art is authored in screen space and rotated by the script, so flipping direction later is a rebuild, not a redraw — provided the direction lives in a single shared ROT_DIR constant and is not hardcoded twice.

The text grid — 37 × 50#

axis maths result
lines (fb-x) 400 / 8 = 50 exactly lines L = 0–49, byte column L, no slack
chars (fb-y) 37 × 8 = 296, 300 − 296 = 4 chars C = 0–36, 2 px top + 2 px bottom

37 was chosen over 36: it is the maximum, the 2 px margins are symmetric (4 physical pixels, since a logical pixel is 2×2), and an odd count gives a true centre column (18) for centring strings.

The OFF = 2 margin costs nothing whatever its value, because along fb-y it is a whole-row adjustment folded into the base address (+ OFF × 50), not a bit shift. If a fatter border is ever wanted, 36 columns at OFF = 6 is equally free.

VTEXT / VTEXT_BG — proposed $62 / $63#

Slots $62/$63 keep them in the $6x text family and leave $70/$71 clear for a VTILE pair later.

$62  VTEXT      L (0-49), C (0-36), scroll, ...string bytes..., $00   → VRAM-image
$63  VTEXT_BG   L (0-49), C (0-36), scroll, ...string bytes..., $00   → VRAM-background

Same wire shape as TEXT/TEXT_BG, so the CPU1 command builders are a copy-paste.

Address maths — structurally identical to draw_text_core, with the axes swapped. Today that routine does text_row_lo/hi[TEXT_Y] (= Y × 400) + TEXT_X + DRAW_BASE_HI in 25 inline cycles. The vertical version needs a new vtext_col_lo/hi table (37 entries, 74 bytes) holding 400 × c + 50 × OFF, plus the line index L added directly, plus DRAW_BASE_HI. Same shape, same cost. Per glyph: 8 bytes stored at 50-byte stride, then the pointer advances by -400 (clockwise).

The vertical opcodes come out simpler than $60. Smooth scroll along the on-screen horizontal axis is motion along fb-y, i.e. a whole-row offset — free — instead of a sub-pixel bit shift. That deletes the expensive and awkward parts of draw_text_core:

What it does need instead is a char-axis clip — stop when a cell would run past the fb-y edge — which is cheaper than the guard scheme it replaces.

Rotated font#

A second 768-byte table (font_data_v), generated at build time from fonts.s by a script, in the manner of gen_build_date.py. Glyph orientation is entirely the generator's problem and costs nothing at runtime — it is not a design decision, only a flag.

ROM space is not a concern: CODE ends well short of SPRITE0 at $F400 (check gpu_os.map for the current figure). Add to gpu.cfg:

FONT_V:  load=ROM, type=ro, start=$F000;     # 768 B at $F000-$F2FF, clear of SPRITE0 ($F400)

What it costs the developer#

Everything else is asset tooling: a rotate-90 script for sprites, tiles and backgrounds, run at build time. Four consequences worth knowing before designing a vertical game:

  1. OP_HDOT_LINE ($49) loses its fast path. Its 10–14× speedup is an $AA memfill along the byte axis, which is structurally fb-x-only. In vertical mode it draws an on-screen vertical dotted line; an on-screen horizontal rule (HUD separators, borders) becomes 1 bit per row at 50-byte stride — slow. There is no trick that recovers this: the byte packing has a preferred axis and rotation moves it. This is the only genuine loss.
  2. Sprite size quantisation lands on the wrong axis. SPR_WBYTES supports 1/2/4/8 bytes = 8/16/32/64 px of framebuffer width, and the 128 px cap likewise — after rotation these quantise on-screen height. A vertical shooter wants tall, narrow ships: 24×40 on screen is 40×24 in framebuffer terms, and 40 is not a legal width, so it rounds up to 64 (wasting data) or becomes a meta-sprite. This is the wrinkle most likely to bite in practice.
  3. The cheap and expensive motion axes swap. Sub-byte SPR_SHIFTS cost applies to fb-x = on-screen vertical, so vertical motion is the expensive axis and horizontal dodging is free byte-steps — the mirror of a horizontal game. Not worse, but it inverts the tuning intuition.
  4. On a CRT the scanline structure appears as vertical stripes. With 400×300 row-doubled to 800×600 that is pronounced, and it reads very differently from the usual horizontal scanline look. Comb rendering's gaps run parallel to the scanlines in either orientation, so the two stay aligned.

Tooling#

madsim needs a rotate-the-window hotkey so the monitor does not have to be physically turned during development. This is purely a display-side transpose on blit; madsim is deliberately not told about vertical mode, because there is no mode for it to be told about. The same applies to the Verilator sim (sim/mad65_sim.cpp) if wanted there.