Linear RAM Text Overlay Interface
1Overview
This document specifies a text layer for an Apple II video card. An Apple II program puts 16-bit text cells in one linear block of main or auxiliary RAM. The card watches writes to that block, keeps a private copy, and draws the text above the normal Apple picture.
- The layer does not replace Apple text, lores, hires, or Super Hi-Res video.
- The buffer uses a simple row-major layout: one character byte followed by one attribute byte for each cell.
- The program may place the buffer at any byte address from
$0200through$BFFFin main or auxiliary RAM, subject to the range checks in §5. - Version 1.0 supplies fixed 8×14 and 8×16 fonts, VT100 and CP437 character modes, VGA-style colors, blink, a hardware cursor, and an optional transparent background.
- A staged ARM then SHOW flow lets software fill a new buffer before the card shows it.
The card only watches RAM writes. It does not change Apple RAM, drive the data bus for RAM cycles, stop the processor, or change Apple video soft switches.
Conventions
The interface occupies the 16-byte DEVSEL block of one slot: $C080 + s×$10 for slot s. The slot is implementation-defined; a card may use any slot whose DEVSEL block it owns. This document writes the bytes as offsets +$0 through +$F, and software finds the slot with the scan in §9. Multi-byte values use little-endian order. Bit 0 is the low bit.
The interface asserts no IRQ, NMI, DMA, or RDY signal. It drives the Apple data bus only for reads from its own DEVSEL block.
2DEVSEL register reference
| Offset | Name | R/W | Function |
|---|---|---|---|
| +$0 | INDEX | RW | Selects one indirect register, 0–255. |
| +$1 | DATA | RW | Reads or writes the selected indirect register. INDEX does not change. |
| +$2 | DATA_INC | RW | Reads or writes the selected indirect register, then increments INDEX modulo 256. |
| +$3 | CMD | W | Issues one command from the table below. |
| +$4 | STATUS | R | Current capture and display state. |
| +$5–+$7 | RESERVED | R | Read as 0. Writes have no effect. |
| +$8–+$D | MAGIC | R | ASCII LINTXT: $4C $49 $4E $54 $58 $54. |
| +$E | SIG | R | Always $4C (’L’). |
| +$F | VER | R | Interface version in BCD; $10 means 1.0. |
Commands
| Value | Name | Action |
|---|---|---|
| $00 | OFF | Hide the layer at the next output frame and stop buffer capture. |
| $01 | ARM | Check and latch the staged buffer settings, clear the card’s new shadow buffer, and start capture. The shown layer does not change. |
| $02 | SHOW | At the next output frame, make the armed buffer active and show it. |
| $03 | HIDE | Hide the layer at the next output frame but keep capture armed. |
Other command values are reserved and have no effect. The card ignores every CMD write while BUSY is 1. An accepted ARM sets BUSY during the CMD write cycle itself. An accepted SHOW, HIDE, or OFF sets FRAME_PENDING during the CMD write cycle itself. The first STATUS read after a CMD write therefore shows the command’s effect; software never sees an accepted command that is still invisible in STATUS.
STATUS bits
| Bit | Name | Meaning when set |
|---|---|---|
| 7 | BUSY | The card is clearing or changing an internal shadow buffer. |
| 6 | STALE | The card lost one or more watched writes. It has hidden the layer. Software must ARM and refill it. |
| 5 | CONFIG_ERROR | The last ARM request failed a range or value check. |
| 4 | FRAME_PENDING | A SHOW, HIDE, or OFF change waits for the next output frame. |
| 3–2 | RESERVED | Always 0 in version 1.0. |
| 1 | ARMED | The card watches writes to the latched buffer range. |
| 0 | VISIBLE | The layer is part of the current output frame. |
3Indirect register reference
Software selects a byte with INDEX and reads or writes it through DATA. DATA_INC makes a run of registers cheaper to set. Unknown indirect registers read as 0 and ignore writes.
| Index | Name | R/W | Function |
|---|---|---|---|
| $00 | BASE_LO | RW | Staged Apple RAM buffer address bits 7:0. |
| $01 | BASE_HI | RW | Staged Apple RAM buffer address bits 15:8. |
| $02 | CONFIG | RW | Staged bank, character mode, font, blink, and background settings; see §4. |
| $03 | COLS | RW | Staged column count, 1–255. Zero is invalid. |
| $04 | ROWS | RW | Staged row count, 1–127. Zero is invalid. |
| $05 | ORIGIN_X_LO | RW | Staged left edge in output pixels, bits 7:0. |
| $06 | ORIGIN_X_HI | RW | Staged left edge in output pixels, bits 15:8. |
| $07 | ORIGIN_Y_LO | RW | Staged top edge in output pixels, bits 7:0. |
| $08 | ORIGIN_Y_HI | RW | Staged top edge in output pixels, bits 15:8. |
| $09 | SCALE | RW | Vertical scale in bits 7:4 and horizontal scale in bits 3:0. Each is 1–15; zero is invalid. |
| $0A | CURSOR_X | RW | Live cursor column, 0–255. |
| $0B | CURSOR_Y | RW | Live cursor row, 0–127. |
| $0C | CURSOR_CTRL | RW | Live cursor enable, blink, and shape; see §7. |
| $0D | FILL_CHAR | RW | Character value used when ARM clears the new shadow buffer. |
| $0E | FILL_ATTR | RW | Attribute value used when ARM clears the new shadow buffer. |
| $0F | RESERVED | R | Reads as 0. |
| $10 | CANVAS_W_LO | R | Current output canvas width in pixels, bits 7:0. |
| $11 | CANVAS_W_HI | R | Current output canvas width in pixels, bits 15:8. |
| $12 | CANVAS_H_LO | R | Current output canvas height in pixels, bits 7:0. |
| $13 | CANVAS_H_HI | R | Current output canvas height in pixels, bits 15:8. |
| $14 | ACTIVE_BASE_LO | R | Shown buffer base, bits 7:0. |
| $15 | ACTIVE_BASE_HI | R | Shown buffer base, bits 15:8. |
| $16 | ACTIVE_CONFIG | R | Shown buffer CONFIG value. |
| $17 | ACTIVE_COLS | R | Shown buffer column count. |
| $18 | ACTIVE_ROWS | R | Shown buffer row count. |
| $19–$1D | ACTIVE_LAYOUT | R | Shown ORIGIN_X low/high, ORIGIN_Y low/high, and SCALE, in that order. |
| $1E | CAPS | R | Version 1.0 returns $7F; bits 6:0 report staged handoff, cursor, transparent background, 8×16, 8×14, CP437, and VT100 support. |
| $1F–$FF | RESERVED | R | Read as 0. Writes have no effect. |
Writes to staged registers never change the shown layer or the watched RAM range. ARM latches all staged registers except the live cursor registers. SHOW later copies the armed layout to the active layout at one output-frame edge.
4Cell and character formats
Cells use two bytes with no row padding:
cell = row * COLS + column address = BASE + 2 * cell address + 0 character address + 1 attribute
A 16-bit little-endian store would hold attribute×$100 + character. An 80×24 buffer uses 3,840 bytes. An 80×25 buffer uses 4,000 bytes.
CONFIG bits
| Bit | Name | 0 | 1 |
|---|---|---|---|
| 0 | AUX_BANK | Main RAM | Auxiliary RAM |
| 1 | CP437 | VT100 character mode | CP437 character mode |
| 2 | FONT_16 | 8×14 font | 8×16 font |
| 3 | BLINK_MODE | Attribute bit 7 is background bit 3 | Attribute bit 7 enables blink |
| 4 | TRANSPARENT | Draw the cell background color | Let Apple video show through clear glyph pixels |
| 7–5 | RESERVED | Write 0; reads return 0. | |
VT100 character mode
The low seven character bits select a glyph. Character bit 7 requests underline. The card draws underline across all eight logical pixels on font row FONT_H−2; the last font row remains clear.
| Stored code | Glyph |
|---|---|
| $00–$1F | DEC Special Graphics codes $5F–$7E, in order. |
| $20–$7E | US ASCII. |
| $7F | Blank. |
A program sets underline by ORing $80 into any stored code. This includes a space, so an underlined space still draws a line. A terminal parser maps bold to a bright foreground color and resolves reverse video by swapping foreground and background before it stores the cell.
CP437 character mode
All eight character bits select one of the 256 fixed CP437 glyphs. No character bit remains for underline. This mode suits ANSI and BBS screens that need the full IBM graphic set.
5Apple RAM capture
ARM forms one watched range from BASE, CONFIG.AUX_BANK, COLS, and ROWS. The byte count is 2×COLS×ROWS. A valid range meets every rule below:
- COLS and ROWS are not zero.
- Both scale nibbles are from 1 through 15.
- BASE is not below
$0200. - BASE through BASE + byte_count − 1 stays in one bank and below
$C000. - The addition does not wrap past
$FFFF. - Reserved CONFIG bits are zero.
BASE need not be aligned. The $0200 floor keeps zero page and the stack outside the watched range; those pages follow ALTZP and are always in use. The floor also makes an ARM with the reset defaults fail, because BASE resets to 0. Version 1.0 does not accept slot space, ROM, or language-card RAM. Software owns the chosen range and must not also use it for code, disk work space, or other changing data.
The card watches the actual target bank of each Apple bus write. It must account for AUXWRITE, 80STORE, PAGE2, and any other bank-routing state. Writes from a DMA bus master count in the same way as processor writes. On a machine with no auxiliary RAM, a buffer with AUX_BANK = 1 never receives writes; ARM cannot detect this, and the layer then shows only fill cells.
If the buffer overlaps native text or graphics memory, writes can change both the normal Apple picture and the overlay. Use unused RAM when the two pictures must stay independent.
ARM
- The card sets BUSY during the ARM write cycle itself.
- The card checks all staged values. If any check fails, it sets CONFIG_ERROR, clears BUSY, and changes no active or armed state.
- The card latches the staged values as the new capture settings.
- The card fills its private shadow cells with FILL_CHAR and FILL_ATTR. It does not write those values to Apple RAM.
- While the fill runs, BUSY stays 1 and watched writes need not be kept.
- When the fill ends, the card clears BUSY and sets ARMED. From then on it keeps every write in the range.
Software polls STATUS until BUSY is 0, then checks CONFIG_ERROR, then writes every cell that SHOW should display. The card cannot read old bytes that were already in Apple RAM before ARM.
Lost writes
If the card cannot keep a watched write after ARMED becomes 1, it sets STALE, clears ARMED, and hides the layer at the next output frame. It must not keep showing data that it knows may be wrong. A new valid ARM clears STALE and CONFIG_ERROR.
6Handoff and frame rules
SHOW changes the active base, bank, grid, origin, scale, format, and font together at one output-frame edge. It never exposes a mix of old and new settings. SHOW has no effect unless ARMED is 1, BUSY is 0, and STALE and CONFIG_ERROR are both 0. SHOW does not clear ARMED: after the flip, the armed range is the active range, and capture continues on it. A later HIDE then SHOW shows the same buffer again without a new ARM.
After SHOW, later watched writes update the shown layer in bus order. A card may delay those writes before they reach its output, but it must preserve their order. The exact delay is implementation-defined and should not exceed two output frames under normal load.
HIDE and OFF also take effect at an output-frame edge. HIDE leaves capture armed. OFF stops capture after it hides the layer.
Double buffering
Software may keep one buffer visible while it prepares another:
- Leave buffer A visible.
- Stage buffer B, issue ARM, and wait for BUSY to clear.
- Do not change buffer A after ARM; the card now watches buffer B.
- Fill buffer B and issue SHOW.
- The card switches from A to B at one output-frame edge.
Use non-overlapping ranges for A and B. ARM may clear the card’s private copy of the new range, so an overlap could change cells that the old active layout still reads.
To know when the flip is complete, poll FRAME_PENDING until it is 0. Do not poll VISIBLE for this; VISIBLE is already 1 while the card flips from A to B.
7Color, background, blink, and cursor
The attribute byte follows VGA order:
bits 3:0 foreground color, 0-15
bits 6:4 background color bits 2:0
bit 7 blink when CONFIG.BLINK_MODE = 1
background color bit 3 when CONFIG.BLINK_MODE = 0
In blink mode, the background range is 0–7. Outside blink mode, the background range is 0–15 and cells do not blink. All blinking cells share one phase: visible for 500 ms, hidden for 500 ms. On the hidden phase, glyph and underline pixels become background pixels, or clear pixels when CONFIG.TRANSPARENT is 1.
The interface does not observe $C022. Software writes VGA-order color values straight into each attribute byte.
When TRANSPARENT is 0, clear glyph pixels use the cell background color. When TRANSPARENT is 1, clear glyph pixels show the normal Apple picture below the layer. This setting applies to the whole buffer; version 1.0 has no per-cell transparency bit, so an ANSI cell with equal foreground and background stays a solid color block.
Fixed palette
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Color | ||||||||||||||||
| RGB | 000 | 00A | 0A0 | 0AA | A00 | A0A | A50 | AAA | 555 | 55F | 5F5 | 5FF | F55 | F5F | FF5 | FFF |
This is the standard VGA/ANSI color order. It differs from the IIgs $C022 color order used by Text Overlay Card Interface 1.0.
Cursor
| CURSOR_CTRL bit | Name | Meaning |
|---|---|---|
| 0 | ENABLE | Draw the cursor when its row and column are inside the active grid. |
| 1 | BLINK | Blink the cursor on the same 500 ms phase as blinking cells. |
| 3–2 | SHAPE | 0 block, 1 underline, 2 left bar, 3 reserved. |
| 7–4 | RESERVED | Write 0; reads return 0. |
A block cursor reverses foreground and background selection for the cell. An underline cursor forces the logical row FONT_H−2 to foreground. A left-bar cursor forces logical column 0 to foreground. Cursor changes take effect at the next output frame and do not change Apple RAM or the card’s text shadow.
8Canvas, placement, and clipping
The canvas is the card’s full active Apple picture, not its border or bezel. Pixel (0,0) is its top-left corner. CANVAS_W and CANVAS_H report its current size. The reference implementation reports 1120×768 for legacy Apple modes and 1280×800 for SHR. Card-owned menus and status marks may draw above this text layer.
The unscaled cell size is 8×14 or 8×16 pixels. SCALE sets an integer size on each axis. The card places cell (x,y) at:
left = ORIGIN_X + x * 8 * horizontal_scale top = ORIGIN_Y + y * FONT_H * vertical_scale
The card clips any part outside the canvas. An origin beyond the right or bottom edge is valid but shows no cells. The card does not resize the grid when the output mode changes; software can read the new canvas size and ARM a new layout.
An 80×24 VT100 grid with the 8×14 font and SCALE $22 is 1280×672 pixels. On the reference SHR canvas it sits at (0,64). On the 1120×768 legacy canvas, SCALE $21 makes the same grid 640×672 pixels and centers it at (240,48). Software should read CANVAS_W and CANVAS_H before it chooses these values.
9Reset and card detection
Reset state
After card or Apple reset: the layer is hidden and not armed; INDEX, BASE, and origin are 0; CONFIG is $08 (main RAM, VT100, 8×14, blink mode, opaque background); COLS is 80; ROWS is 24; SCALE is $11; FILL_CHAR is $20; FILL_ATTR is $07; and the cursor is off at (0,0). STATUS is 0. BASE resets below the $0200 floor, so an ARM fails with CONFIG_ERROR until software stages a real address.
Detection
The interface adds no slot-ROM or $C800 decode; it does not disturb the ROM its host slot already carries. Every identification byte sits at a fixed DEVSEL offset, in the manner of Apple’s fixed-address auxiliary-memory entry points: identification must never depend on card state such as INDEX. Detection uses reads only. A detector scans the candidate slots; in each slot it reads SIG and VER first, and only if those match does it read the six MAGIC bytes.
Etiquette is part of the protocol. A probe must never write to a slot that has not passed every check — not even to INDEX. On some cards — a Disk II controller above all — every DEVSEL access has side effects; the two reads of +$E and +$F are the least intrusive probe possible.
; Entry: X = slot * $10. PTR = any free zero-page pair.
; Exit: carry clear = interface found in that slot.
DETECT LDA $C08E,X
CMP #$4C ; SIG = 'L'
BNE NOCARD
LDA $C08F,X
CMP #$10 ; version 1.0 or later
BCC NOCARD
TXA
ORA #$88 ; DEVSEL base + $8 = first MAGIC byte
STA PTR
LDA #$C0
STA PTR+1
LDY #0
MAGIC LDA (PTR),Y
CMP IDSTR,Y
BNE NOCARD
INY
CPY #6
BCC MAGIC
CLC
RTS
NOCARD SEC
RTS
IDSTR HEX 4C494E545854 ; "LINTXT", high bits clear
The ID string uses a HEX directive because Merlin’s double-quoted ASC sets the high bits, and the compare would then always fail.
The reference implementation exposes these registers only while its SmartPort interface owns slot 7. It must not answer at $C0F0–$C0FF while SuperSprite owns that slot.
10Programming example
This SHR example stages an 80×24 VT100 buffer in main RAM at $6000. It uses an 8×14 font at 2× scale and places the 1280×672 layer at (0,64), centered on a 1280×800 canvas. A program that can also run in legacy modes must read the canvas registers and use the legacy layout above. This example assumes the reference slot-7 binding, so the DEVSEL block sits at $C0F0.
LDA #$00
STA $C0F0 ; INDEX = BASE_LO
LDA #$00
STA $C0F2 ; BASE_LO = $00; INDEX increments
LDA #$60
STA $C0F2 ; BASE_HI = $60
LDA #$08
STA $C0F2 ; main, VT100, 8x14, blink, opaque
LDA #80
STA $C0F2 ; COLS
LDA #24
STA $C0F2 ; ROWS
LDA #$00
STA $C0F2 ; ORIGIN_X = 0
LDA #$00
STA $C0F2
LDA #$40
STA $C0F2 ; ORIGIN_Y = $0040 = 64
LDA #$00
STA $C0F2
LDA #$22
STA $C0F2 ; 2x horizontal, 2x vertical
LDA #$01
STA $C0F3 ; CMD: ARM; BUSY sets in this write cycle
WAIT LDA $C0F4
BMI WAIT ; wait for BUSY = 0
AND #$20
BNE BADCFG
JSR FILL6000 ; write all 3,840 bytes after ARM
LDA #$02
STA $C0F3 ; CMD: SHOW at next output frame
SHOWN LDA $C0F4
AND #$10
BNE SHOWN ; FRAME_PENDING clears at the frame edge
RTS
Before it exits, a program must remove its layer and wait for the clean output frame to take effect. OFF hides the layer and disarms capture; it does not change the Apple RAM buffer.
LDA #$00
STA $C0F3 ; CMD: OFF
CLEAN LDA $C0F4
AND #$10
BNE CLEAN ; do not return while the layer is visible
To write cell (column,row), compute $6000 + 2×(row×80 + column), store the character first, then the attribute. Either byte order is valid. A frame can contain one old byte and one new byte if the two stores cross an output-frame edge; use the staged second buffer when a whole-screen change must be exact.
11Compliance
Normative: the 16-byte DEVSEL block and its register offsets, indirect register meanings, command and status rules, range checks, the two-byte row-major cell layout, CONFIG and attribute bits, VT100 and CP437 code maps, fixed palette, cursor shapes, reset state, capture of the resolved RAM bank, lost-write behavior, frame-edge handoff, and the MAGIC, SIG, and VER bytes at their fixed DEVSEL addresses.
Implementation-defined: the slot, and any co-residency rules inside it (the reference implementation shares slot 7 with SmartPort and yields to SuperSprite and the boot menu); output canvas size; write-to-output delay within the stated two-frame goal; and the drawing order of card-owned menus above the text layer. It is worth reiterating that the reference implementation places the block in slot 7's DEVSEL space, $C0F0–$C0FF, alongside its SmartPort interface, which uses only that slot's ROM and expansion-ROM decodes. The two operate at the same time: a SmartPort call and an overlay register access never contend. The block is exposed exactly when SmartPort is, and must not answer while SuperSprite owns the slot.
Reserved for future versions: CONFIG bits 7:5, CURSOR_CTRL bits 7:4 and shape 3, DEVSEL offsets +$5 through +$7, indirect registers $1F through $FF, command values above $03, and STATUS bits 3:2. A future font-upload extension would use this reserved space.
Linear RAM Text Overlay Interface 1.0 draft · 2026-08-11 · Reference implementation: Appletini One. Review before implementation.
The source of this page is
docs/linear_ram_text_overlay_interface_v1.0.html
in the firmware repository; a demo that uses it is software/textoverlay.a65.