AntOS Font, Videotext and Terminal Architecture


Overview

AntOS renders all text — boot logs, shell sessions, AntOS UI, application text, Teletext-style videotext pages — through a single unified character-cell subsystem. A logical character grid maps to a logical pixel framebuffer, which then goes either to the local display (currently via LT8912B HDMI bridge for dev bring-up) or, in the final Ant64 hardware, over MIPI directly to FireStorm for compositing with sprite and tilemap layers.

The character grid is sized for TV viewing distance rather than monitor distance — glyphs need to be physically large enough to read from 2-3 metres away. This drives the choice of cell size (10×12 pixels) and grid dimensions (one of several modes, see below).

The system supports both modern terminal semantics (full VT100/ANSI parsing, 256-colour palette, text styles) and classic videotext semantics (40×25 alphamosaic grid with cell-level colour, compatible with Teletext / CEEFAX / Prestel content models).


Logical Display Modes

The character renderer operates against a logical pixel framebuffer that is independent of the physical output resolution. AntOS code only ever writes to the logical layer; sys_mipi_present() handles the path from logical layer to physical output (currently a 2× or 3× integer upscale to the LT8912B's HDMI output, eventually a direct MIPI transfer to FireStorm which handles its own composition).

This separation means the same AntOS code runs unchanged across:

  • Current dev hardware (DeMon + LT8912B + HDMI TV)
  • Final Ant64 hardware (DeMon → MIPI → FireStorm → output composition)
  • Any future output paths (LVDS panels, composite/SCART, headless capture)

Three logical modes are defined:

Mode Logical resolution Cell grid Aspect Use
Terminal 640×360 64×30 16:9 Shell, boot logs, code, diagnostics
System 320×180 32×15 16:9 AntOS UI, chunky retro displays
Videotext 400×300 40×25 4:3 Teletext-compatible pages, mosaic graphics

All three modes use 10×12-pixel character cells — the same font, same renderer, same VT100 parser, just different logical layer dimensions. Glyph size on the physical display stays comparable across modes because the upscale factor compensates: Terminal mode scales 2× to 1280×720, System mode scales 4× to 1280×720, Videotext mode scales 3× to 1200×720 pillarboxed (correct for 4:3 content).

Switching modes is a single call (sys_videotext_set_mode()) — reallocates the logical FB, changes the cell grid dimensions, updates the upscale factor. Cell buffer contents migrate or clear depending on the call.


Cell Format

Each cell is 4 bytes (32 bits) packed in a flat array. Cell at row r, column c lives at offset (r * cols + c) * 4. Cell grid dimensions are mode-dependent.

Byte 0:  Character (low 8 bits of 10-bit glyph index)
Byte 1:  Foreground colour (8-bit palette index)
Byte 2:  Background colour (8-bit palette index)
Byte 3:  Attribute byte
         bits 0-1: character index high bits (10-bit total → 1024 glyphs)
         bit 2:    flash
         bit 3:    underline
         bit 4:    italic        (render-time shift)
         bit 5:    bold          (render-time OR)
         bit 6:    strikeout
         bit 7:    reverse       (swap FG/BG at render time)

Cell buffer memory budget:

Mode Cells Buffer size
Terminal (64×30) 1920 7.5 KB
System (32×15) 480 1.9 KB
Videotext (40×25) 1000 4 KB

All trivially fit in BSRAM. The active mode's cell buffer can be hot in scratch and the renderer can stream cells without cache pressure.

Character index — 1024 glyphs

The 10-bit character index allocates as:

Range Use
0–127 Standard ASCII
128–255 Extended glyphs: box drawing, arrows, Teletext mosaics, symbols
256–511 Ant64-specific glyphs: logos, UI icons, system status indicators
512–1023 User-defined glyphs (UDGs) — application-supplied bitmaps

UDGs (the classic Spectrum/BBC capability) are stored in a separate RAM region the renderer falls back to when char code ≥ 512. This lets BASIC programs, games, and AntOS apps define their own glyphs without recompiling the system font.

Palette — 256 colours, cell-level

Foreground and background are 8-bit palette indices, both per cell. The palette itself is a separate 256-entry RGB888 table (768 bytes), system-wide and modifiable at runtime. Cell-level colour means every cell can have an independent colour pair — no "attribute clash" limitations like the Spectrum had.

The default palette is Ant64-designed (to be specified — Atari-style 16 hues × 16 lumas is a strong candidate, ties aesthetically to the font choice).

Text style bits

Bit Effect Implementation
Flash Glyph alternates between visible and BG-coloured at ~2 Hz Renderer uses a global frame counter, swaps FG↔BG when in "off" phase
Underline Horizontal line drawn at baseline (row 8) Renderer ORs an extra horizontal line into the glyph during render
Italic Top half of cell shifted right 1 pixel Render-time transform, see below
Bold Glyph ORed with itself shifted right 1 pixel Render-time transform, see below
Strikeout Horizontal line drawn at x-height midpoint (row 5) Same as underline, different row
Reverse FG and BG colours swapped at render time Used for cursor, selection, highlighted items

All six bits combine freely. Reverse + flash is a perfectly valid cursor style. Bold + italic + underline is a perfectly valid emphasised hyperlink.


Render-Time Style Transforms

Italic and bold are achieved by bitmap transforms during rendering, not by separate font ROMs. This keeps font storage at 1× the base size (48 KB for 1024 glyphs × 12 rows × 2 bytes/row of 10-bit-wide data) rather than 4× the size for separate regular/bold/italic/bold-italic fonts.

Italic shift

The top half of the cell (rows 0-5 of the 12-row cell) is shifted right by 1 pixel at render time. The bottom half (rows 6-11) stays in place. This produces a stepped slant — not a true smooth italic, but visually unmistakable as italic at TV viewing distance.

Regular            Italic
Row 0: .X..        Row 0: ..X.   ← shifted right 1
Row 1: .X..        Row 1: ..X.   ← shifted right 1
Row 2: .XXX        Row 2: ..XXX  ← shifted right 1
Row 3: .X..X       Row 3: ..X..X ← shifted right 1
Row 4: .X..X       Row 4: ..X..X ← shifted right 1
Row 5: .XX.X       Row 5: ..XX.X ← shifted right 1
Row 6: .X..X       Row 6: .X..X   (unchanged)
Row 7: .X..X       Row 7: .X..X   (unchanged)
Row 8: ----        Row 8: ----    (baseline, unchanged)
Row 9: ....        Row 9: ....    (unchanged)
Row 10:....        Row 10:....    (unchanged)
Row 11:....        Row 11:....    (unchanged)

The 1-pixel right sidebearing in every glyph absorbs the shift without colliding with the next cell. Glyphs that already use column 9 of the cell (the sidebearing column) must be designed to leave that column clear in the top half.

Bold OR

Each row of the glyph is ORed with itself shifted right by 1 pixel at render time. This thickens every horizontal stroke independently — vertical strokes become 2 pixels wide, horizontal strokes stay 1 pixel tall but extend 1 pixel further right, diagonal strokes get filled in stepwise.

Regular            Bold
Row N: .X..X       Row N: .XX.XX  ← ORed with self<<1

The 1-pixel right sidebearing absorbs the bold extension — glyphs visually touch each other slightly, which is the correct "bold" appearance at this resolution.

Bold-italic

Italic transform applied first (top half shifts right 1), then bold OR applied per row to the post-italic bitmap. Both transforms compose cleanly with no special case.

Cost

Per-glyph render cost adds two conditional row-level operations (italic shift if bit set, bold OR if bit set). Both are cheap compared to the glyph fetch + pixel write to the framebuffer. Negligible total CPU impact.


Font Specification

Design grid

10 pixels wide × 12 pixels tall, with the following zone layout:

Row  0  ┐
Row  1  │  Cap / ascender zone        (rows 0-2, 3 rows)
Row  2  ┘
Row  3  ┐
Row  4  │
Row  5  │  x-height zone              (rows 3-7, 5 rows)
Row  6  │
Row  7  ┘
Row  8  ─  Baseline                   (row 8)
Row  9  ┐
Row 10  │  Descender zone             (rows 9-11, 3 rows)
Row 11  ┘
  • Capitals, ascenders (A-Z, b d f h k l t) occupy rows 0-7 (8 rows tall — matches Atari 8-bit proportions exactly)
  • Lowercase x-height glyphs (a c e i m n o r s u v w x z) occupy rows 3-7 (5 rows tall)
  • Descenders (g j p q y) occupy rows 3-10 (5 rows of x-height + 3 rows of descender)
  • Baseline at row 8 — all glyphs rest on this row
  • Column 9 is the right sidebearing — kept clear for italic shift and bold OR to extend into

Style — Atari 8-bit inspired, with descenders

The font is visually based on the Atari 8-bit ROM character set, preserving its distinctive characteristics:

  • Slightly chunky stroke weight (heavier than CGA, lighter than C64 PETSCII)
  • Square-ish proportions on capitals
  • Single-storey lowercase g (the iconic Atari g)
  • Quirky Q (open tail), J (descending), S (balanced curves)
  • Clean, unambiguous numerals
  • Lowercase that looks designed-on-purpose

The crucial improvement over the original Atari font: proper descenders on g j p q y. The Atari 8-bit's 8×8 cell forced descenders to be truncated or omitted entirely — g looked like a small block, p lost its tail. With 12 rows and a baseline at row 8, descenders get a proper 3-row tail zone.

This is intentionally not a slavish copy of any specific 8-bit machine's font. The Atari style is the inspiration; the font is hand-designed for the Ant64 with the larger cell budget and proper typographic features that the originals couldn't afford.

Glyph set

Range Contents
0-31 Control characters (mostly unrendered — used by VT100 parser)
32-126 Standard printable ASCII
127 Cursor block or DEL glyph
128-191 Teletext mosaic graphics (2×3 sixel patterns)
192-223 Box drawing characters (single + double line)
224-239 Arrows, geometric symbols
240-255 Currency, special punctuation
256-511 Ant64 system glyphs: logos, UI icons, status indicators
512-1023 UDG region — application-defined

Font ROM storage

Each glyph: 12 rows × 10 bits = 120 bits. Stored as 12 × 2 bytes per glyph (16 bits per row, low 10 used) = 24 bytes per glyph. Whole font ROM: 1024 × 24 = 24 KB. Comfortably in BSRAM, or even on-chip flash if needed.

Note: only the first 512 glyphs are baked-in system glyphs. The upper 512 (UDG region) are RAM-resident and writable.


Mosaic Graphics (Teletext)

Videotext mode supports the classic Teletext alphamosaic graphics scheme. Each mosaic character represents a 2×3 grid of pixels-in-a-block:

Mosaic cell (10×12 pixel cell):

┌─────┬─────┐
│  0  │  1  │   2 columns × 5 pixels wide each
├─────┼─────┤
│  2  │  3  │   3 rows × 4 pixels tall each
├─────┼─────┤
│  4  │  5  │
└─────┴─────┘

10×12 cell divides exactly into 5×4 mosaic blocks — both dimensions integer with no fractional pixels. This is one of the architectural reasons for choosing 10×12 as the cell size; smaller cells (8×8, 8×16) produce fractional mosaic blocks and look bad.

Six mosaic bits per character give 2⁶ = 64 mosaic combinations, occupying char codes 160-223 in the Teletext spec (with separate "contiguous" and "separated" mosaic variants).

The mosaic glyphs are pre-rendered into the font ROM at design time — no special render path needed, they're just glyphs like everything else.

Cell-level FG/BG colours apply to mosaics the same way they apply to characters, giving full Teletext-spec colour rendering: each cell independently colourable, no cross-cell attribute constraints.


VT100/ANSI Compatibility Layer

A parser sits in front of the cell buffer, accepting standard VT100/ANSI escape sequences and translating them to cell-buffer writes:

Sequence Effect
ESC[<n>;<m>H Move cursor to row n, column m
ESC[<n>A/B/C/D Move cursor n rows/columns
ESC[2J Clear screen
ESC[K Clear to end of line
ESC[<n>m SGR — set graphics rendition (colour, style)
ESC[7m Reverse video
ESC[1m / ESC[22m Bold on/off
ESC[3m / ESC[23m Italic on/off
ESC[4m / ESC[24m Underline on/off
ESC[5m / ESC[25m Flash on/off
ESC[9m / ESC[29m Strikeout on/off
ESC[<n>;<n>;<n>;<n>;<n>m Extended SGR for 256-colour

This is the same parser AntOS already uses for terminal output — the existing term.init() / term.shutdown() Luau library doesn't need to change. The byte sink underneath just changes from "UART transmit" to "videotext cell write."


Memory Budget

Resource Size Storage
Cell buffer (active) up to 7.5 KB BSRAM
Logical pixel framebuffer 75 KB (320×180) to 691 KB (640×360) PSRAM
Font ROM (system glyphs, chars 0-511) 12 KB Flash or BSRAM
UDG RAM (chars 512-1023) 12 KB PSRAM
Palette table (256 × RGB888) 768 bytes BSRAM
VT100 parser state < 64 bytes Stack

Total system-text footprint: well under 1 MB even in the largest mode. Fits comfortably alongside the 30 MB+ of PSRAM available for application use.


Implementation Path

This subsystem builds on the existing sys_videotext.c/h stubs in the AntOS tree. The bring-up sequence will be roughly:

  1. Define the cell format and font ROM data structures in sys_videotext.h
  2. Bake an initial 10×12 system font (Atari-inspired, with descenders) — separate font-design pass, output as a .c byte array
  3. Implement the cell-to-pixel renderer — given a cell buffer and font ROM, render to the logical framebuffer with all six style bits handled
  4. Implement the VT100 parser — accept byte stream, produce cell writes
  5. Wire to existing terminal abstractionterm.init() becomes a no-op on UART and a setup call on videotext
  6. Mode-switching APIsys_videotext_set_mode(MODE_TERMINAL / MODE_SYSTEM / MODE_VIDEOTEXT)
  7. UDG supportsys_videotext_define_glyph(index, bitmap) for chars 512-1023
  8. Teletext page renderer — using the existing mosaic glyphs in chars 128-191, layout follows Level 1 spec

The font design and the renderer can be developed in parallel; both feed into a final integration test.


A Note on the Eventual FireStorm Path

When the FireStorm FPGA path replaces the LT8912B bridge, this entire subsystem stays the same. AntOS continues to write to the logical cell buffer; the renderer continues to produce a 320×180 or 640×360 logical framebuffer; sys_mipi_present() changes from "2× nearest-neighbor upscale to a display framebuffer for the DPI peripheral" to "pack the logical framebuffer into MIPI lines headed for FireStorm RX #0."

FireStorm's compositor then merges the AntOS text layer with sprites, tilemaps, copper effects, and whatever else into the final output. The text layer is just one input to the compositor, treated identically to how DeMon's other UI layers are treated today.

The text layer's pixel data is also small enough that it fits comfortably in the MIPI bandwidth alongside everything else flowing over that link — at 640×360 RGB888 / 60 Hz, the text layer is ~40 MB/s, ~10% of the link's available bandwidth. Plenty of headroom for audio buffers, sprite data, command lists, and bulk transfers on the same physical link (see the MIPI as a Data Fabric section in the Memory Architecture document).

Important: The Ant64 family of home computers are at early design/prototype stage, everything you see here is subject to change.