arti a4ed68c0e4 README: rewrite as the project front page -- status, findings, tidbits
The old README still described the repo as ROM images plus a Ghidra
setup. The project now spans the MAME harness, the real terminal, the
COFF/SDK toolchain and the networking reverse engineering, so rewrite it
as the entry point.

New top-level content:
- a dated status table (ROM, Ghidra, MAME, real unit, our code,
  networking, LAT, font path), with the open items named as open;
- what the firmware actually contains (MIT X11 R4, BSD TCP/IP via
  386BSD, BIND, telnet, TI RTS, fonts);
- the address model table, corrected against harness/mame/vt1200.cpp
  (RAM 0..0x1FFFFFF; DUART bit 0x06200000/0x06400000);
- how the faithful MAME run stops at level 0, why no documented device
  can pass it, and what the stub does and does not prove;
- the real-unit debug channel (TCP 771), the challenge, and a table of
  what is proven on hardware;
- the three routes for running our own code, and the heap model;
- the LANCE + BSD stack mapping, the RMD1_FRAM blocker and the claim
  deferred for real silicon;
- LAT and the idle-crash investigation, and the two font results;
- a curated "odd findings" list and a documentation index.

Factual corrections made while checking sources: the rdebug gate is a
SETF 2 field (the port was never closed), CSR0=0x1BB has ERR clear, the
pcfReadBitmaps overflow is not reachable on the TFTP path, and the
real-unit framebuffer is 1024x2048 line-interleaved (the "line doubled"
look was a partial read). The XCROSS 123k figure is the unmapped log,
not a VT1200_CAL proxy hit. ROM md5 unchanged.
2026-10-30 12:03:01 +02:00

DEC VT1200 X Terminal — ROM images, emulation, and what we found

The DEC VT1200 is a monochrome X Window System terminal (circa 1990). It runs the X server, the terminal manager, the window manager and a VT emulator entirely from ROM on a Texas Instruments TMS34010 CPU, with 2–4 MB of RAM and no virtual memory.

This repository started as the firmware ROM dumps and has grown into a full reverse-engineering and execution project: a MAME machine that runs the firmware, a Ghidra database with the bit-address problems solved, a 1987 TI toolchain that still builds working code, and a debug channel into a real terminal over the LAN.

The ROM stays byte-identical throughout. Every experiment patches at runtime, if at all; the reference image is vt1200_v22.bin (md5 b46a3e442ccb86134d2356bd70d22fd2).


Status at a glance

As of 2026-10-30.

Area State
ROM images Done. v2.1 and v2.2 reconstructed from 14 EPROMs each, bank order and byte interleave proven, checksums verified.
Disassembly / Ghidra Done. TMS34010 module rebuilt for Ghidra 12, bit-address pointer constants materialised as references (~40k in v2.2).
MAME harness Running. Boots POST, fails level 0 honestly, parks in the diagnostics menu. With VT1200_SKIP_LEVEL1=1 it boots the X server and reaches the Terminal Manager, with keyboard, mouse/tablet and LANCE.
Real terminal Opened. The firmware's hidden debug TCP service on port 771 is reversed, its challenge is solved offline, and we can read, write, dump the ROM, load COFF images and run them.
Our own code Runs. TI SDK assembly, a C program (HELLO in video RAM), and an X11 window — on the emulator and on the real unit.
Networking RE Mapped. The LANCE driver and the BSD-derived TCP/IP stack are mapped function by function, with explicit firmware-specific entries where no counterpart exists. One claim still needs real silicon.
LAT / the idle crash Open. The receive path is audited, the crash is narrowed to service announcements (not the data stream), and a soak procedure is ready for when the unit is back.
Font path Characterised, not exploitable as hoped. The known pcfReadBitmaps overflow is real but is not reachable through the TFTP font loader.

What the machine actually runs

The image is not just DEC code. reference/third-party-code.md maps what is inside vt1200_v22.bin:

component evidence
MIT X11 R4 — server (dix, mfb/cfb) and Xlib mfb*, pcf*, snf*, bdf*, Xlib strings; exact match to X11R4 pcfrbitmaps.c
BSD 4.3/4.4 TCP/IP $Header/@(#) IDs for the socket/TCP/IP/UDP/mbuf set, imported at DEC by craig, 1989-10-05/06
BSD BIND resolver, telnet + TN3270, inetd, lpd lineage, libc pieces file IDs and code match
TI C runtime V2.00 strlen/strcpy/strcmp match RTS.SRC instruction for instruction
Bitstream / Bigelow & Holmes / DEC Japan fonts copyright strings in the font data

Source-ID strings (RCS $Header:, SCCS @(#)) name the original files and dates. They are strong evidence of provenance, but they are file names, not licence statements.


Quick start

md5sum vt1200_v22.bin        # b46a3e442ccb86134d2356bd70d22fd2

./reconstruct.py v2.1        # -> vt1200_v21.bin (address order)
./reconstruct.py v2.2        # -> vt1200_v22.bin
python3 -m unittest test_reconstruct test_ptr_constants -v

harness/mame/build.sh              # build the MAME single-driver binary
harness/mame/run.sh                # faithful: stops in the diagnostics menu
harness/mame/run.sh skip           # VT1200_SKIP_LEVEL1=1: boot the OS to the GUI
harness/mame/run.sh stub           # + VT1200_IO_PATH_STUB=1: get past level 0
harness/mame/run.sh pie            # runtime-patch the failing checks; level 1 runs
harness/mame/run.sh net            # skip + tablet + LANCE on a host tap

The confirmed memory map is in MEMORY_MAP.md; the harness detail — devices, env knobs, regression baseline, working conventions — is in harness/mame/README.md.


ROM images

Folder Firmware ROMs Part numbers
v2.1/ V2.1 14 × 128 KiB 23-210E9 .. 23-223E9
v2.2/ V2.2 14 × 128 KiB 23-274E9 .. 23-287E9

Each file is 131072 bytes and is named by its DEC part number. Dumped and published by 9track.net (Dave/9track collection): https://9track.net/roms/ (section "DEC VT1200", category Terminal), files at https://9track.net/roms/files/dec/vt1200/. Fetched 2026-10-15. The bitsavers terminal archive does not host VT1200 images (checked /bits/ and /pdf/dec/terminal/), so 9track.net is the canonical source.

Firmware layout (reverse-engineered)

The 14 EPROMs are byte-interleaved 16-bit words, not a flat byte stream. Within each consecutive pair of part numbers, the odd part number holds the low byte and the even part number holds the high byte:

v2.1 pairs: (210,211) (212,213) (214,215) (216,217) (218,219) (220,221) (222,223)
v2.2 pairs: (274,275) (276,277) (278,279) (280,281) (282,283) (284,285) (286,287)

The seven 256 KiB banks are laid out so that file offset == physical byte - 0x07E40000: the highest part-number pair sits at the lowest address. reconstruct.py writes that 1.75 MiB (0x1C0000) image by default; pass --part-order for the legacy ascending-part-number ordering.

Evidence the interleave is correct: only it produces readable text, e.g. -DEC-Terminal-Medium-R-Normal--14-100-100-100-C-80-ISO8859-1, WM XIO Error, packet_size = %d exceeds MAXSIZE_RCVPKT, and SCCS headers such as $Header: ctype.c,v 1.1 89/10/05 19:40:49 craig Exp $. Getting the interleave wrong shifts every string by one byte (DEC becomes EDC).


The address model (read this before any address)

The TMS34010 is bit-addressed. A register, PC or stored pointer P selects a bit; the referred physical byte is P >> 3. The top two logical address bits are not emitted, so physical_bit == logical_bit & 0x3FFFFFFF.

region physical byte bit address
RAM (4 MiB) 0x000000 0x00000000
Video RAM — 0x04000000
DUART2 (keyboard/mouse) 0x00C40000 0x06200000
DUART1 (host/printer) 0x00C80000 0x06400000
NVR (battery-backed settings) — 0x06000000
Ethernet address ROM (8 bytes) — 0x06800000
LANCE RDP / RAP — 0x05800000 / 0x05900000
ROM (1.75 MiB) 0x07E40000 0x3F200000
ROM aliases (0x7f…, 0xbf…, 0xff200000–0xffffffff) same ROM top 2 bits undecoded
Reset vector file 0x1BFFFC logical 0xFFFFFFE0 → 0x3F200040

Consequences that cost real time if forgotten:

  • read_word(0x3f200000) returns ROM bytes 0..1 (physical 0x07e40000).
  • MAME models the program space as memory_access<32, 1, 3> — the address-map unit is one bit, so a 16-bit word spans 16 address units. The TMS34020 uses memory_access<32, 2, 3> and is not interchangeable.
  • Ghidra loads the image at byte base 0x07E40000; firmware bit address = Ghidra address × 8. unidasm/dis34 print bit addresses; Ghidra prints byte addresses.
  • currentProgram.getImageBase() is not the ROM base — use the loaded block.

Disassembly and Ghidra

MAME's bundled unidasm has a tms34010 backend:

unidasm vt1200_v22.bin -arch tms34010 -basepc 0x7E40000 -count 40

smart34010's dis34 (note -b is a bit-address base; two encodings were corrected and validated by round-trip through the 1987 assembler — see harness/sdk-roundtrip/):

dis34 -a -l -b 3F200000 vt1200_v21.bin

tools/vt1200_map.py prints the vectors and verifies the per-bank header checksums:

python3 tools/vt1200_map.py vt1200_v22.bin

tools/ptr_constants.py finds bit-address pointer constants and resolves their targets (--strings for string targets, --tsv for a table). On v2.2 it finds ~44,700 constants, of which ~3,680 distinct targets are strings, e.g. 0x004B08 <- 0x3F225840 'LAT: could not allocate XCB for …'.

Ghidra has no built-in TMS34010 language, so we install the community module ZPaul2Fresh8/Ghidra_TMS34010:

  • tools/ghidra-tms34010/ — vendored, patched module source
  • tools/ghidra-tms34010-local.patch — all local changes vs. upstream
  • tools/install-ghidra-plugin.sh — installs and compiles it (needs sudo)

The patch fixes two things: the Ghidra 12 pspec schema (space="rom" lowercase, initialized="false" on memory blocks) and the upstream Midway arcade board map, whose GFX/ROM blocks collided with the loaded VT1200 image.

ghidra/ is a ready-to-open project (ghidra/vt1200.gpr) with both images imported at byte base 0x07E40000. ghidra_scripts/FixPtrConstants.java creates the bit-constant references (37,187 in v2.1, 40,290 in v2.2); AssertPtrFix.java is the headless regression check. Caveat: the module's pcode semantics are incomplete, so flow-following cannot expand far — the boot bank is disassembled linearly as well.


Running the firmware under MAME

A minimal machine (harness/mame/vt1200.cpp) loads the firmware into a TMS34010 so it can be run under MAME's debugger: trace, breakpoints, single-stepping, memory watch. It is a visibility tool, not an accurate VT1200 emulation.

A bare harness/mame/run.sh with no environment is the faithful configuration. It models video RAM, the board-window handshake, both DUARTs (real SCN2681 devices), the LK401 keyboard and a pointing device, the LANCE, the NVR and the Ethernet address ROM. Two of those were not conveniences:

  • the level-1 host-port transmit is ISR-driven, so a fake DUART cannot work;
  • without a real DUART2, the board write-back latch answered a read of the write-only IMR with its own value, so the OS serviced a phantom interrupt that could never clear (and the GUI never appeared).

Where a faithful run stops

A bare run passes POST and then fails level 0's I/O data path test, parking in the interactive diagnostics menu:

VERSION 2.08 - SELECTED TEST TYPE:

That test wants 10 bytes on DUART2 channel A, and nothing is attached. It is not satisfiable by any documented device: the routine writes one byte (0xE1, "disable auto-repeat", which has no reply), sets the channel to normal mode, not loopback, and checks only the count — the received byte is stored and never compared. The keyboard's reply is 4 bytes, the mouse's 4, the tablet's 5. A faithful LK401 would not help: the init sequence's receiver reset flushes the FIFO. See reference/level0-10byte-puzzle.md.

It used to "pass" by accident, because the write-back latch returned 0x0099 from the status register and bit 0 is RXRDY, feeding the drain loop ten phantom bytes. Making DUART2 real moved the regression baseline — that looks like a regression and is the removal of a false pass.

The one deliberate test-fixture stand-in

VT1200_IO_PATH_STUB=1 is not a keyboard model. It stands in for an unknown fixture on the keyboard port: after the one 0xE1 write it feeds 10 arbitrary (0x00) bytes back on the same receive path, only while the channel is polled. Nothing compares them; only the count matters. It is off by default, and the faithful run still fails level 0 exactly as before.

With it on, level 0 clears and level 1 reports HOST PORT - 15 and PRINTER PORT - 16, prints TYPE 'GO' TO CONTINUE, and continues — then wedges in a DUART1 counter-ready interrupt storm from frame ~2839. It does not boot the OS, and it does not park at HOST PORT - 15.

How far the OS gets

VT1200_SKIP_LEVEL1=1 drives DUART1 IP3 high, which the firmware takes as a signal to jump to the OS cold-start and skip all 15 level-1 tests. This is a modelled board input; the real machine reads IP3 low. A skipped run is evidence about the OS, graphics and crash behaviour, and is not evidence about the real terminal's POST.

With the skip, the OS boots a running X11 server and reaches the Terminal Manager: the root-window weave renders, the cursor is handed to the terminal's own mouse cursor, and the V2.2 box appears — title bar, Messages, Ethernet/IP/LAT, a Memory bar, and "All Diagnostics Passed". The keyboard and mouse drivers initialise and speak the real protocol; the LANCE is initialised and the TM shows the real hardware address 08-00-2B-16-4B-5A. Evidence frames: harness/mame/skip_frame3000.png, duart2_default_tm_frame3000.png, tablet_cursor_*.png.

Regression baseline

progress.lua at 200 frames, no env:

PC=3FDCE400 samples=198 (3 distinct PCs)

It is a change detector, not a specification. It has moved twice, both deliberately; harness/mame/README.md records why, including the VT1200_IO_PATH_STUB=1 value (samples=53, 75 PCs) as a separate baseline.

Deliberate divergences

divergence default what it does restore the real machine
VSXXX-AB tablet on tablet on DUART2-B; absolute host-pointer reports VT1200_MOUSE=1
VT1200_SKIP_LEVEL1 off DUART1 IP3 high → skip all level-1 tests leave unset
VT1200_IO_PATH_STUB off 10 bytes after the level-0 0xE1 write leave unset

The full env-knob table (21 knobs, down from 33) is in harness/mame/README.md.


The real terminal

The unit is a stock v2.2 machine with MAC 08:00:2b:16:4b:5a, static IP 100.103.100.103, reached from host 100.103.255.254.

The big find is that the firmware runs a remote debugger it calls DEBUG TCP Port, listening on TCP 771 (dwt/notes/rdebug-static.md). Over one connection a client can read and write the entire address space, call any address as a function, capture console output, draw text, and make the terminal open an outbound telnet connection.

The path is gated, and the static note's first gate turned out not to be one: the settings word around 0xAECD0 reads 0x8070, but the compare is a SETF 2 field at 0xAECD2 — two bits, and bits 2..3 are zero, so the connection is accepted. The real gate is a 16-byte echo challenge at bit 0x3F59FDC0 (dwt_prf). It is solved offline: dwt/challenge.py computes the reply from the challenge alone, and two live sessions passed the memcmp. That gave us, on real hardware:

proven on the real unit evidence
full ROM dump byte-identical to vt1200_v22.bin dwt/evidence/real-unit-rom.bin
the read primitive (peek), screenshots assembled over it dwt/evidence/real-unit-canvas.png
the write path: malloc → poke → COFF loader → create_process dwt/loader/evidence/hostload/
the first C program on the terminal (HELLO into VRAM) harness/c-hello/
a real X11 window created by an injected client dwt/gui/, dwt/notes/GUI_APPS.md
a LAN TFTP fetch and load dwt/notes/TCPIP.md
NVRAM and the LANCE address ROM read back and matching the model dwt/nvram_dump.py, dwt/rom_dump.py

The terminal's own MOP remote console also works (dwt/mop_console.py); SHOW reports Monitor Type VR319 and Firmware Revision V2.2.

Real-unit display geometry (it is not what the emulator shows)

  • The framebuffer is 1024×2048, line-interleaved, 256 KiB: the image sits on even rows at 1:1, odd rows carry a mask. The visible raster is 1280×1024, with the 1024-wide canvas at x=192.
  • A 128 KiB read holds only the top 512 image lines; doubling that is what produced the early "line doubled" and "missing bottom" pictures. The full 256 KiB read plus dwt/screenshot.py --field even --stretch 1280x1024 matches the glass.
  • The Terminal Manager's 0x55/0xff row pattern is a non-displayed mask, not a background setting: setting the root background to 100% white changes the root and leaves the TM pattern intact, while the screen looks solid white.
  • The bottom Ethernet Port Error - 24 box is the separate AUI/transceiver termination test, not the address ROM test — that one passes.

MAME models a linear 1bpp framebuffer and therefore never shows the interleave; that is a display-fetch difference, not a CPU difference.


Running our own code

Three independent routes exist, in increasing order of realism:

  1. MAME debugger entry. harness/hello-world/ is an 82-byte GSPA payload built by the 1987 TI SDK (GSPA/GSPLNK under DOSBox). hello.lua loads it at bit 0x01800000, arms a breakpoint on the diagnostics idle loop, and jumps to it. It writes RAM markers and paints a VRAM block. This proves the toolchain, not a delivery mechanism.
  2. COFF loader + create_process. dwt/loader/loader.asm takes a linked GSPLNK COFF already in RAM, places sections at their header addresses, zeroes .bss, and runs the image as a first-class process via create_process(entry, "LOADER", 0x6000, 1, 0). The loader also refuses a target outside a heap free-list block (status 8), which is a real check, not a reservation.
  3. Real hardware. dwt/loader/hostload.py drives the whole chain over rdebug: malloc → poke the COFF → poke the loader → descriptor → create_process → markers/PNG. harness/c-hello/build.sh builds a K&R C program for the real unit (ROW_BYTES 256) and build.sh emu for MAME (128). dwt/gui/xhello.asm does XOpenDisplay → XCreateSimpleWindow → XMapWindow → XSync from a firmware-created process and leaves a real window on the screen.

Address convention in the payload work: Ghidra = 0x7E40000 + file offset, and firmware bit address = Ghidra × 8.

The heap, and why payload addresses matter

Payloads are poked to fixed scratch addresses at bit 0x01800000 (byte 0x00300000) and above, at the top of the 4 MiB RAM. On a default boot that region is not allocated — it lies inside the largest free block (byte 0x12B772..0x400000, ~2.97 MB), so the OS may hand it out. There is a real allocator and it is callable: malloc at bit 0x3F701230, free at 0x3F702440, a bit-addressed first-fit over three arenas. The global free-byte counter is itself in bits (bit 0x1E8BA0); on the real unit it reported 3,251,638 bytes free. dwt/heapmap.py maps the free list read-only.


Networking: the LANCE and the BSD stack

harness/mame/NETWORK_REPORT.md is the reference (4,000+ lines, all measurement and disassembly).

The driver. The LANCE is an AM79C90 at two register windows (RAP bit 0x05900000, RDP bit 0x05800000), with bus-master DMA where LANCE byte B lands in main-RAM word B >> 1, and its interrupt on INT1. The driver does not hard-code the registers in its normal paths; it keeps a device struct — found in a live RAM dump at bit 0x23CBF0. The Ethernet address ROM is eight bytes, 08 00 2b 16 4b 5a c1 86: the six-byte MAC plus two checksum bytes the firmware validates.

The stack. It is 4.3/4.4BSD, imported at DEC in October 1989 and descended from the 386BSD line. Every function entry the report mentions now has a mapping row: a quoted 4.4BSD counterpart where one exists (leinit, leintr, ipintr, ip_output, icmp_input, icmp_reflect, icmp_send, tcp_input, tcp_dooptions, tcp_mss, udp_input, udp_output, rip_input, in_arpinput, arpresolve, in_pcblookup), otherwise an explicit firmware-specific row saying why. The coverage check is mechanical, not a spot check: 225 addressed sites, 143 initially absent from a mapping row, of which 41 sat in 26 distinct entry points — all 26 are now mapped. The honest caveat: fourteen entries are marked firmware-specific because no counterpart was found, which is not a proof that none exists.

What a faithful model still owes. C — LANCE TEST (the firmware's internal loopback) fails on one bit: the test expects RMD1 = 0x6B00, MAME reports 0x4B00 and never sets RMD1_FRAM. It is not an FCS loss — the passing DTCR=1 case carries a correct FCS, and the failing case is a negative test that deliberately sends none. M — ETHERNET ROM TEST passes.

One claim needs real silicon. On the real unit, INIT → IDON → STRT works (CSR0 = 0x1B3, then a stable 0x33), but with a bare-TDMD kick the transmitter never services the demand (TDMD stays set, the TX descriptor stays owned at TMD1 = 0x8338). MAME cannot tell the two kicks apart, so this is deferred until the hardware is back. Note the corrected decode: CSR0 = 0x1BB has ERR clear; the anomaly is INIT still set while IDON is set.

BOOTP

harness/serve.py --bootp answers the request the terminal broadcasts at power-up, so its address no longer has to be typed in. Two details worth keeping: chaddr carries eight bytes (MAC plus the two checksum bytes), and the request is only visible at L2 — the host IP stack never receives it from the MAME tap, so the responder is an AF_PACKET socket, while the packet itself stays an RFC 951 reply. It is off by default.


LAT, and the crash we are chasing

lat/ holds the normative DEC LAT specification (AA-NL26A-TE, June 1989), the latd implementation and the Wireshark dissector, so the firmware's LAT subsystem can be read against a spec and against working code.

lat/notes/lat-receive-audit.md audits the receive path. The demultiplexer walks nbr_slots (wire byte 1) with no length bound, and slots are acted on: Data_a is delivered, Start/Stop/Reject mutate session state. On a real unit this produced an idle crash:

LEVEL0 ERROR -> Illegal OPCODE Occurrence - 28
    007AA3F0   00200008   00000260   3F9B8EA0

Decoded as bit addresses, only 3F9B8EA0 is in ROM; the rest are RAM. The CPU was executing out of RAM — the signature of a hijack via memory corruption.

Two candidate triggers were ranked: the LAT receive path, and the font parser. Replaying the exact 27,529-byte session stream did not reproduce it (twice over, ~2.4×). The remaining protocol difference is latd's periodic LAT service announcements (message type 0xA, multicast), 91 of them in the crashing session, none in the replay. The soak procedure in NEXT_SESSION.md (20 s heartbeat, watch both screen and wire keepalives) is written up and waiting on the unit.


Fonts, and two results that correct earlier hopes

  • The TFTP "Disk full" abort is not storage. A capture shows the terminal itself sends TFTP error 3; it is the firmware's tftp_read buffer check (bufsize (%d) too small for %d bytes) reporting an incomplete read. The font buffers are in correctly-mapped RAM, so there is no second map overlap.
  • The pcfReadBitmaps overflow is real but unreachable on the TFTP path. The arithmetic is confirmed (n is a full 32-bit pcfReadInt32, no bound check; n=0x10000001 → fosAlloc(0x40000004), n=0x40000000 → alloc of 0), but the TFTP font loader recognises the PCF magic and calls a metadata reader that handles PROPERTIES/ACCELERATORS/METRICS/ENCODINGS and silently skips the BITMAPS table. A crafted num_chars is never read there. See harness/mame/PCF_VALIDATION_REPORT.md.

The earlier "disk full" symptom was traced to a client-side condition, which is also why a server-side change cannot cause or cure it.


Odd findings and other tidbits

  • Every 256 KiB ROM bank starts with an 8-byte header [checksum16][?][AAAA][5555]. tools/vt1200_map.py verifies them.
  • The reset vector is identical in v2.1 and v2.2, and points at byte-identical POST code (0x3F200040 → 0x07E40008).
  • The bank order is reversed: the highest part numbers sit at the lowest addresses. A natural-looking ascending order silently produces garbage.
  • The TM's stipple is a mask the display never shows. The raw framebuffer is striped; the glass is white because only the even field is fetched.
  • The debug service is documented in the firmware itself — the string is literally DEBUG TCP Port, next to the X11 port (0x1770 = 6000) in the same descriptor table.
  • The "closed" rdebug gate was a field-size mistake. 0x8070 looked non-zero, so the port looked shut; the compare is SETF 2, a two-bit field, and bits 2..3 are zero. The port was always open.
  • The challenge has an ordering rule. Send the opcode alone, read and answer the challenge, then send the handler's arguments. Sent together, dwt_prf eats the first 16 argument bytes as the answer and the handler reads shifted fields.
  • The challenge is per-8-byte independent. Changing bytes 0–7 never changes output bytes 8–15 (0/64 single-bit trials), so the transform is blockwise.
  • The debug connection writes a message into the Terminal Manager: each connection adds a Debug connection from IP host … line to Messages. Five lines in the first real-unit screenshot are five connections, not an error.
  • The firmware contains a full X11 R4 server and Xlib, so our payloads are X clients. XOpenDisplay from the rdebug dispatcher never completes; the same code works from a process created by create_process, with the Display * kept in a callee-saved register.
  • MAME's address-map precedence is last-registered-wins, not first — which is why one VT1200_CAL proxy over 0x00400000 saw zero hits while another over 0x00000000 fired, and why the "unmapped" log's 123k accesses were not unmapped at all (XCROSS_REPORT.md). The labels were right; the mental model was wrong.
  • The level-1 VIDEO MEMORY failure is test code 22, a 16-bit walking-bit check plus a 32-bit pattern over 0x20000 units starting at bit 0x04000000 (CODE22_REPORT.md).
  • The NVR was invisible because its address was swallowed by the video-memory mapping; it is at bit 0x6000000 and the font path lives there, not the font data.
  • scn2681.cpp does not exist in the MAME tree — the SCN2681 lives in mc68681.*. Searching for the former walks the whole disk before failing.
  • A blank LAT window is not a crash. sleep 3600 printed nothing and an idle session is legitimately blank; the connect handshake was identical to the healthy session. Use an output-producing soak.
  • A capture filter can hide the suspect: ether host <mac> does not capture multicast. Use ether proto 0x6004.
  • The 1987 TI toolchain still works under DOSBox, and as34/ld34 were ported to 64-bit Linux for assembler-level work. dis34 had two encoding bugs, both fixed and round-tripped through GSPA/GSPLNK (harness/sdk-roundtrip/).

Documentation index

file what it records
MEMORY_MAP.md the confirmed ROM/RAM/vector map, with per-section confidence
harness/mame/README.md harness devices, env knobs, divergences, regression baseline
harness/mame/NETWORK_REPORT.md LANCE driver + BSD stack mapping, function by function
harness/mame/GATE_CLEANUP_REPORT.md how the working config became the default
harness/mame/DUART_REPORT.md DUART wiring, ISR-driven transmit, datasheet-verified edits
harness/mame/RENDER_REPORT.md the active-pixel scanline renderer and its geometry
harness/mame/INPUT_REPORT.md LK401 + VSXXX-AA, and the cursor evidence
harness/mame/TABLET_REPORT.md the VSXXX-AB tablet and 5-byte absolute reports
harness/mame/LANCE_REPORT.md the LANCE model, address ROM, byte↔bit DMA
harness/mame/LANCE_LOOPBACK_REPORT.md why C — LANCE TEST fails (one bit)
harness/mame/NVRAM_REPORT.md the battery-backed settings RAM
harness/mame/LEVEL1_CONTINUE_REPORT.md what the stub run actually does
harness/mame/CODE22_REPORT.md which test raises VIDEO MEMORY - 22
harness/mame/XCROSS_REPORT.md MAME address-map precedence, one instrument at a time
harness/mame/FONT_STORAGE_REPORT.md the TFTP "Disk full" is a client-side read check
harness/mame/PCF_VALIDATION_REPORT.md the overflow exists but the TFTP path never reaches it
harness/mame/HELLO_WORLD_REPORT.md the SDK payload, debugger entry only
harness/mame/FINDINGS.md font-parser and ROM-mapping analysis
harness/mame/BRINGUP_LOG.md the bring-up narrative, including withdrawn claims
harness/BOOTP_REPORT.md the BOOTP responder and why it is L2
dwt/notes/rdebug-static.md the debug TCP service, statically
dwt/notes/CHALLENGE_REVERSE.md the 16-byte challenge, solved
dwt/notes/REAL_UNIT.md first contact, geometry, corrections
dwt/notes/GUI_APPS.md the X11 architecture and what a payload can do
dwt/notes/LOADER.md the COFF loader and create_process
dwt/notes/TCPIP.md the socket API and the TFTP fetch
dwt/notes/HEAP.md the allocator and where payloads live
dwt/notes/MESSAGEBOX.md the bottom diagnostics box and its status mask
lat/notes/lat-receive-audit.md the LAT receive path, finding by finding
reference/level0-10byte-puzzle.md why level 0 cannot be satisfied faithfully
reference/level1-test-sequence.md the 15 level-1 tests and their codes
reference/third-party-code.md what open-source code is in the ROM
reference/tms34010-sdk.md the 1987 toolchain, buildable today
EXPLOIT_NOTES.md the original code-execution audit and the crash decode
NEXT_SESSION.md the LAT soak handoff (older; see the reports for later work)

Repository layout

vt1200/
├── v2.1/  v2.2/                 # original 28 EPROM dumps
├── reconstruct.py               # de-interleave -> address-ordered image
├── vt1200_v21.bin  vt1200_v22.bin
├── MEMORY_MAP.md                # the confirmed memory map
├── ghidra/  ghidra_scripts/     # Ghidra project + Verify/Stats/FixPtr/Assert
├── tools/
│   ├── vt1200_map.py            # vectors + bank-header checksums
│   ├── ptr_constants.py         # find/resolve bit-address constants
│   ├── smart34010/              # dis34 (two encoding fixes)
│   ├── as34/                    # TMS34010 binutils, ported to 64-bit Linux
│   ├── tms34010-sdk/            # the 1987 TI SDK (under DOSBox)
│   └── ghidra-tms34010/         # patched processor module + install script
├── harness/
│   ├── mame/                    # the MAME driver, Lua probes, reports
│   ├── hello-world/             # 82-byte SDK payload (debugger entry)
│   ├── c-hello/                 # first C program, runs on real hardware
│   ├── udpstream/               # UDP/TCP receive-throughput payloads
│   ├── sdk-roundtrip/           # dis34 fixes vs. the real assembler
│   └── serve.py                 # TFTP (+ optional BOOTP) server
├── dwt/                         # real-unit debug client and notes
│   ├── challenge.py  exploit.py # the challenge and the rdebug client
│   ├── loader/                  # COFF loader + hostload over rdebug
│   ├── gui/                     # injected X11 window
│   ├── tcpip/                   # socket API payloads
│   └── notes/                   # the real-unit investigation
├── lat/                         # LAT spec, implementations, audit, probes
├── reference/                   # datasheets, manuals, disassembly, boot video
├── docs/                        # DEC/TI manuals (PDF + .txt), wiki drafts
└── images/                      # real-unit photographs

Ground rules

The project has five hard-won rules, written down in harness/mame/README.md: search with an explicit path (never from /), stage only your own paths (never git add -A), one sample is not evidence, any change to MAME needs a quote and a second reader, and do not fake it — model what the documentation supports, report the gap, and make any stand-in opt-in and named for what it is.

That last rule is why the faithful configuration is the default, why VT1200_IO_PATH_STUB is off and described as a test fixture rather than a keyboard, and why harness/mame/BRINGUP_LOG.md keeps its withdrawn claims.

Languages
C 23.7%
Python 19.7%
Lua 12.2%
C++ 11.9%
Shell 9.8%
Other 22.6%