10 KiB
VT1200 execution harness — results
Companion to README.md in this directory. This records what was built, the
exact commands, the evidence, and what was learned about the two font questions.
1. What was set up
A minimal MAME machine for the VT1200 firmware, built as a single-driver subtarget of MAME 0.289:
| File | Purpose |
|---|---|
harness/mame/vt1200.cpp |
the driver (TMS34010 + RAM + ROM) |
harness/mame/build.sh |
build wrapper (-j8, registers the driver) |
harness/mame/trace.lua |
headless run: trace execution from the reset vector |
harness/mame/overflow.lua |
headless run: exercise pcfReadBitmaps n*4 |
harness/mame/roms/vt1200/ |
MAME ROM dir (symlink to vt1200_v22.bin) |
MAME source lives outside the project (disposable) at
/home/arti/code/k-space/mame0289 (shallow clone of tag mame0289).
The driver is deliberately incomplete: only the CPU, 4 MiB of RAM at bit address 0, and the ROM are modelled. LANCE Ethernet, the DUARTs and the board peripherals are not modelled, so the terminal never boots to the X server. That is fine: the goal is execution visibility, and reset + breakpoints + single-step + trace are enough.
Address model (the thing that had to be right)
memory_access<32,16,3> ⇒ the address-map unit is 1 bit. The ROM is mapped
at bit 0x3f200000 (its byte base 0x07e40000 × 8), and the reset-vector page
at logical bit 0xffffffe0 is covered by mirroring the ROM into the top
logical quadrant 0xff200000–0xffffffff. The reset vector
(0x3f200040) lands on ROM file offset 0x1bfffc.
2. Build and run
# build (memory-hungry; defaults to -j8)
cd /home/arti/code/k-space/vt1200
harness/mame/build.sh # -> /home/arti/code/k-space/mame0289/vt1200
# run: trace from the reset vector
M=/home/arti/code/k-space/mame0289/vt1200
$M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
-debug -debugger none -autoboot_script harness/mame/trace.lua -autoboot_delay 0
# run: pcfReadBitmaps n*4 overflow (OVF_N optional, decimal)
$M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
-debug -debugger none -debuglog -seconds_to_run 5 \
-autoboot_script harness/mame/overflow.lua -autoboot_delay 0
Notes on MAME headless debugging: -debugscript does not work without a
debugger UI (the console only processes scripts while the debugger UI drives
the loop). The reliable headless route is the Lua debugger API
(manager.machine.debugger, device.debug, emu.register_frame_done) driven
from -autoboot_script.
3. Evidence it executes the firmware
trace.lua prints the reset PC and writes an instruction trace of the first
two frames (harness/mame/vt1200-reset.trace.gz, 650 815 lines; a head excerpt
is in reset-trace.head.txt):
[space] program shift=3 data_width=16
PC at harness start = 3F200040 (expect 3F200040)
after 2 frames: PC=3FDF99F0
First instructions of the trace (bit addresses; byte = /8):
3F200040: DINT
3F200050: SETF 10h,0,0
3F200060: SETF 20h,0,1
3F200070: CLR A0
3F200080: MOVE A0,@64000D0h,0 ; board peripheral
3F2000B0: MOVI FFh,A0
3F2000D0: MOVB A0,@64000F0h
3F200100: MOVI Fh,A0
3F200120: MOVB A0,@64000E0h
3F200150: CLR A0
3F200160: MOVE A0,@62000D0h,0 ; board peripheral
3F200190: MOVI 4h,A0
3F2001B0: MOVE A0,@62000F0h,0
3F2001E0: CLR A8
3F2001F0: MOVI 1h,A0
3F200220: MOVB A0,@64000F0h
3F200250: CLR A0
3F200260: MOVE A0,@C00000B0h,0 ; CONTROL
3F200290: MOVE A0,@C0000080h,0 ; DPYCTL
3F2002C0: MOVE A0,@C0000100h,0 ; HSTCTLH
...
This matches the known power-on code (DINT; SETF 16,0,0; SETF 0,0,1; CLR A0 …)
and the peripheral/CONTROL writes documented in MEMORY_MAP.md. The CPU then
runs several hundred thousand instructions of POST and ends the second frame at
0x3fdf99f0 (PIXBLT B,XY).
In-emulator pcfReadBitmaps overflow
overflow.lua jumps straight into the arithmetic block and stops after the
shift. Measured results (register a11 is the argument to fosAlloc):
| n (input) | n*4 / fosAlloc arg |
note |
|---|---|---|
0x00001000 |
0x00004000 |
control, no wrap |
0x10000001 |
0x40000004 |
wraps |
0x40000000 |
0x00000000 |
fosAlloc(0), then 4 GiB of writes |
0x40000001 |
0x00000004 |
fosAlloc(4), then 4 GiB of writes |
RESULT n=40000001 a11_fosAlloc_arg=00000004 nChars=40000001
4. Findings
4.1 pcfReadBitmaps arithmetic and num_chars*4 (confirmed)
The firmware's pcfReadBitmaps is at bit 0x3f984e70 (byte 0x0f09ce).
Disassembly (bit addresses) of the relevant block:
3f984e70: move *a9(0x270),a11,1 ; a11 = pCS->nChars
3f984e90: jrne L3F984EE0 ; nChars != 0 -> just cross-check n
3f984ea0: move *a13(0x20),*a9(0x270),1 ; nChars == 0 -> nChars = n
...
3f985030: move *a13(0x20),a11,1 ; a11 = n (pcfReadInt32 result)
3f985050: sll 0x2,a11 ; a11 = n*4 <-- NO overflow check
3f985090: calla L3F703D90 ; fosAlloc(n*4, 1)
3f9850c0: move a8,*a13(0x40),1 ; pCS->pBitOffsets = result
...
3f985260: (loop i = 0 .. n-1) ; writes n CARD32 entries
3f985290: move a10,a7 ; a7 = i
3f9852a0: sll 0x5,a7 ; i*32 bits = i*4 bytes
3f9852d0: add a11,a7 ; dest = offsets + i*4
3f985310: calla L3F96EEE0 ; read 4 bytes from file into dest
There is no bound check on n between the read and the multiply, exactly
matching X11R4 reference/x11/r4/contrib/server/pcf/fonts/lib/pcf/pcfrbitmaps.c
(offsets = fosAlloc(n*sizeof(CARD32)); for (i=0;i<n;i++) offsets[i]=…).
n = 0x40000001 makes n*4 wrap to 4 while the loop writes ~4 GiB.
One important detail verified against MAME's core: the font code uses the
TMS34010 field-1 moves (…,1), and SETF 0,0,1 leaves field 1 at 32
bits. So move *a13(0x20),a11,1 really is a 32-bit load — n is the full
32-bit pcfReadInt32, not a 16-bit value. The emulator run above confirms it.
4.2 The ~1 KB TFTP font limit
The failure seen on the wire (ACK block 1, ACK block 2, then TFTP
ERROR 3/ENOSPACE) is the firmware's own tftp_read giving up:
3f5eed30: move *a13(0xffa0),a11,1 ; a11 = bufsize / remaining capacity
3f5eed50: cmp a11,a9 ; a9 = current DATA payload length
3f5eed60: jrle L3F5EEED0 ; payload <= capacity -> copy it
...
3f5eeda0: movi 0x3f5f0c98,a8 ; "tftp_read: bufsize (%d) too small for %d bytes\n"
3f5eee50: movk 3,a9 ; TFTP error code 3 (ENOSPACE)
3f5eee90: calla L3F5EF610 ; send error packet
So ENOSPACE is not a disk error: it is tftp_read rejecting a datagram
because the destination buffer's remaining space is smaller than the payload.
The observed 1024-byte cutoff (works ≤ 2 blocks, fails on the 3rd) implies the
font receive buffer is ~1 KiB. tftp_read itself uses a 0x204-byte (516 =
512+4) datagram buffer (movi 0x204 at 3f5ed5e0), so the ~1 KiB limit is in
the file accumulation buffer, not the packet buffer. Pinning the exact size
needs a breakpoint on 3f5eed30 while a live transfer runs, or a small
additional harness.
5. Q1 — why the small synthetic font is rejected
Not yet answered with certainty. What is established:
- The firmware contains a full X11R4-era PCF reader. The dispatcher
(
pcfReadFont) is around bit0x3f98bxxx; it printsFailed to read PCF format properties/accelerators/encodings/metrics/bitmapswhen a table reader returns failure, and the per-reader strings (unknown bitmap format 0x%x,bad count in pcfReadBitmaps (expected %d, got %d),bad packed/unpacked metrics, glyph=…,pcfReadBitmaps called with bitmaps loaded, …) all match the R4 sources. - The
fonts.dirpath also has a name check:font name different: name in path file='%s'/name in font file='%s'(bit0x3f990538), a strong candidate for "name listed" but "OpenFont unmatched". - A freshly generated
baseline.pcfhas all nine tables and version0x70636601, so the version/TOC checks should pass; the failure is one of the per-table or name checks.
Why it isn't answered yet: invoking the parser requires the fos file
abstraction (fosRead at 0x3f96eee0, fosAlloc at 0x3f703d90,
fosFilePtr at *a13(0xffe0)), which the boot path sets up only after the
network/TFTP fetch. The harness can run the CPU, but reaching pcfReadFont
means either booting far enough (needs LANCE/DUART models — out of reach) or a
small purpose-built call stub.
6. Blockers and remaining effort
- Q1 needs a call harness. Cheapest next step: give the driver a writable
overlay at the
fosRead/fosAllocaddresses, have Lua write a tiny TMS34010 stub there that serves bytes from a RAM copy ofbaseline.pcf, then set PC to the PCF loader with a craftedfosFilePtr. That lets us single-step the exact bail path. Effort: moderate (hand-encoding a handful of TMS34010 instructions, or assembling withas34). - The ~1 KB limit can be pinned by breaking at
0x3f5eed30during a real transfer, or by the same call harness feeding oversized data. - Full boot (X server, network) needs LANCE + DUART + video models; that is a large, separate project and was explicitly out of scope.
- The driver is a visibility tool, not an accurate VT1200: the CRT timing, palette and peripherals are stubs.
7. Tree layout (new files)
harness/mame/
vt1200.cpp driver (source of truth; symlinked into MAME)
build.sh build wrapper (JOBS=8 default)
trace.lua reset-vector trace
overflow.lua pcfReadBitmaps n*4 harness
reset.dasm disassembly of the reset vector window
reset-trace.head.txt first 60 traced instructions
vt1200-reset.trace.gz full 650k-instruction trace
roms/vt1200/vt1200_v22.bin -> ../../vt1200_v22.bin
harness/dis/
fontregion-v22.txt dis34 of the font code
tftp-v22.txt dis34 of the TFTP client
full-v22.txt.gz dis34 of the whole ROM