Files
vt1200/harness/mame/FINDINGS.md
T

10 KiB
Raw Blame History

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 bit 0x3f98bxxx; it prints Failed to read PCF format properties/accelerators/encodings/metrics/bitmaps when 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.dir path also has a name check: font name different: name in path file='%s' / name in font file='%s' (bit 0x3f990538), a strong candidate for "name listed" but "OpenFont unmatched".
  • A freshly generated baseline.pcf has all nine tables and version 0x70636601, 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/fosAlloc addresses, have Lua write a tiny TMS34010 stub there that serves bytes from a RAM copy of baseline.pcf, then set PC to the PCF loader with a crafted fosFilePtr. That lets us single-step the exact bail path. Effort: moderate (hand-encoding a handful of TMS34010 instructions, or assembling with as34).
  • The ~1 KB limit can be pinned by breaking at 0x3f5eed30 during 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