lance_cmd.c is a payload that drives the LANCE with its own code (no ROM
calls) and answers a command protocol on ethertype 0x88B6, so a unit whose
chip we have taken over can still be inspected:
read (peek), stats, poke, jump, reboot
The reply path was dead for a while and the cause was not the chip: the RX
ring had been grown to 16 entries (0x3020..0x309F) while TXR stayed at
0x3060, so all eight TX descriptors lay inside the RX ring. The chip's RX
write-back set OWN in what we believed were TX descriptors, and every reply
was discarded by transmit_poll() as "owned but not start of packet"
(r 00309A 8000 / w 00309A 0000 in the DMA log). TX ring moved to 0x30C0,
with a compile-time guard so the rings cannot overlap again.
Receive path aligned with the three drivers available locally (4.4BSD
pmax if_le.c, NetBSD am7990.c, Linux 7990.c):
* STP *and* ENP required on one descriptor, else the frame is counted as
chained and dropped rather than parsed from the middle of a buffer;
* the 4-byte FCS the chip writes into the buffer is subtracted, as all
three do (rmd3 - 4);
* re-arm restores RMD2 and clears RMD3, ownership written last;
* TX kick is TDMD|INEA (no reference driver re-writes STRT);
* a TX slot still owned is skipped instead of clobbered.
RX buffers are 1526 B so a full 1514-byte MTU frame fits one descriptor;
1500 B would have chained it.
vtcmd.py drives all of it; bench-sweep.sh sweeps size x rate. Measured on
the real VT1200 (10 Mbit half-duplex hub): 856-858 fps of 1514-byte frames
at 99.9-100% delivery, i.e. wire limited, and 1500 fps of 60-byte frames at
99.8-99.9%. NETWORK_REPORT.md sections 68-72 record the evidence, including
the 64-descriptor build that died with an illegal opcode on the unit and was
reverted.
VT1200 execution harness (MAME)
A minimal MAME machine that loads the VT1200 firmware into a TMS34010 CPU so we can run it under MAME's debugger: instruction trace, breakpoints, single-stepping, and direct memory watch. It is a visibility tool, not an accurate VT1200 emulation.
Everything in this directory is source-of-truth in this repo; the MAME source
tree is disposable and lives outside the project
(/home/arti/code/k-space/mame0289).
Working conventions
Five rules. Each of them has already cost this project real time, which is why they are written down rather than assumed.
-
Never search from
/. A filesystem-widefind /orgrep -ris slow, visible to whoever is at the keyboard, and never necessary. Usergwith an explicit path — this directory, or/home/arti/code/k-space/mame0289/src(within whichsrc/devices/machine,src/devices/bus,src/osd/modules/netdevcover most questions). Never/usr,/opt,/homeat large,/procor/sys. Worth knowing why this keeps happening:scn2681.cppdoes not exist (the SCN2681 lives inmc68681.*), so a search for it walks the entire disk before giving up. -
Stage only your own paths — and do not edit a file someone else is holding.
git add <path> [<path>…]— nevergit add -A, nevergit commit -a. Several people and workers share this repository at once, and a-Asweeps someone else's in-flight files into your commit. That happened twice in one day, and then a third time because the orchestrator edited this very README while a worker held it — so note that an uncommitted edit of yours can be swept into someone else's commit just as easily as an unstaged file. If you are about to touch a file another worker owns, don't: say what you want changed instead. If a collision does happen, unstage,git commit --amend, and say so. -
One sample is not evidence. "It never paints", "the driver only models 256 KiB of RAM", and "it is stuck" were all wrong, and all three came from a single observation. Before concluding, either compare across time (several frames, several minutes, several runs) or read the source. A claim about behaviour needs more than one look; a claim about the code needs the code.
-
Changing MAME requires a quote and a second reader. Plausible-sounding sentences get assembled to justify a number that was really found by trial and error — one such edit was reverted after an independent reader found the datasheet said the opposite. So: quote the sentence or the source line, add a revert note, and have someone else read the citation before it is believed.
-
Do not fake it. No responder that feeds the firmware the answer a test wants. Where a real device is unknown, model what the documentation actually supports and report the gap; where a stand-in is genuinely unavoidable, make it opt-in and name it for what it is — see the divergences table above:
VT1200_IO_PATH_STUB,VT1200_SKIP_LEVEL1, and the tablet default are all documented divergences, not quiet assumptions. And the ROM stays byte-identical (md5 b46a3e442ccb86134d2356bd70d22fd2): patch at runtime if you must, never the file.
harness/mame/
├── vt1200.cpp # the MAME driver (symlinked into the MAME tree)
├── build.sh # clone-safe build wrapper
├── *.lua # probe / capture / dump scripts (many; see the reports)
└── roms/vt1200/ # MAME ROM dir (symlink to ../../vt1200_v22.bin)
Reports in this directory:
| file | what it records |
|---|---|
GATE_CLEANUP_REPORT.md |
promoting the working config to the default; retiring the settled env gates |
TABLET_REPORT.md |
the VSXXX-AB tablet: the self-test device-code ID, the 5-byte absolute report, and absolute host-pointer input |
LANCE_REPORT.md |
the LANCE Ethernet model: the register file, the 8-byte address ROM and its checksum, the byte↔bit DMA, and the remaining loopback/CRC blocker |
LANCE_LOOPBACK_REPORT.md |
C — LANCE TEST: the TX buffer does carry a correct FCS in the passing DTCR=1 case and deliberately carries none in the failing one; the residual mismatch is MAME never setting RMD1_FRAM |
NVRAM_REPORT.md |
the battery-backed NVR (bit 0x6000000): previously unmodelled (its address was swallowed by the video-memory mapping), now a persisted nvram_device; the font path lives there, the font data does not |
FONT_STORAGE_REPORT.md |
the TFTP font "Disk full" abort: a capture proves the terminal sends TFTP error 3, and it is the firmware's tftp_close (0x3F5EC860) reporting an incomplete read, not a storage condition; the font buffers are in correctly-mapped RAM, so there is no second map overlap |
RENDER_REPORT.md |
the active-pixel scanline renderer, and the geometry it derived |
DUART_REPORT.md |
the DUARTs: wiring, the ISR-driven transmit, the datasheet-verified model edits |
BRINGUP_LOG.md |
the whole bring-up narrative, including withdrawn claims — read it sceptically |
ROMPATCH_REPORT.md |
runtime ROM patching (debug only — see the caveat) |
HELLO_WORLD_REPORT.md |
the TMS34010 SDK assembles an 82-byte payload of our own code; it runs, writes RAM markers and paints a VRAM block. Debugger entry, not a delivery mechanism |
Address model (the important part)
The TMS34010 is bit-addressed: a register/PC holds a bit address, and a
pointer P refers to physical byte P >> 3. MAME models this with a program
space of memory_access<32, 1, 3>, i.e. the address-map unit is 1 bit
(a 16-bit word spans 16 address units). Source: tms34010.h — note the
TMS34020 differs (memory_access<32, 2, 3>), so this is not interchangeable
with the 34020. Therefore:
- Address-map entries here are bit addresses, not byte addresses.
read_word(0x3f200000)returns ROM bytes 0..1 (physical byte 0x07e40000).- The reset vector is read from logical bit
0xffffffe0, which maps to the last 4 bytes of the image (file offset0x1bfffc).
The driver maps:
| region | bit range | bytes |
|---|---|---|
| RAM | 0x00000000–0x001fffff |
4 MiB at byte 0 |
| ROM | 0x3f200000–0x3fffffff |
1.75 MiB at byte 0x07e40000 |
| ROM aliases | 0x7f…, 0xbf…, 0xff200000–0xffffffff |
same ROM (top 2 logical bits not decoded) |
The 0xff… alias is what makes the reset-vector page resolve.
What is modelled by default
This changed — see GATE_CLEANUP_REPORT.md. The working configuration used
to be a six-variable incantation; it is now the default, and a bare
vt1200 vt1200 with no environment runs it.
- Video RAM at bit
0x04000000— required for the level-1 VIDEO MEMORY test. - The board-window handshake model (two-tick busy bit).
- MAME's
SCN2681DUART at DUART1 (0x0640000, host/printer), with input port IP4–IP6 held high — that is how the driver reads the VR150 monitor switch (0x70). The level-1 host-port transmit is ISR-driven, so a real DUART is required here, not a convenience. - MAME's
SCN2681at DUART2 (0x0620000, keyboard/mouse). This one is a faithfulness fix, not a convenience — the real board has one. Without it the board write-back latch answers a read of DUART2's write-only IMR with its own written value, so the OS services a phantom interrupt that can never clear. - The real input devices on DUART2, driven by MAME's host input: a
DEC LK401 keyboard (channel A, 4800 8N1) and a pointing device on
channel B (4800 8O1) — a DEC VSXXX-AB tablet (the default, absolute)
or, with
VT1200_MOUSE=1, a DEC VSXXX-AA mouse (relative). All are modelled fromreference/keyboard-mouse/PROTOCOL.mdand wired to the SCN2681 at the bit level (rx_a_w/rx_b_wanda_tx_cb/b_tx_cb). They answerFD/Tbecause that is the protocol, not to satisfy a test. SeeINPUT_REPORT.mdandTABLET_REPORT.md. - A mono palette (pens 0/1 = black/white) and an active-pixel scanline renderer
that reads VRAM. See
RENDER_REPORT.md. - The LANCE Ethernet controller — MAME's
am79c90(AM79C90) at the firmware's two register windows (bit0x5900000= RAP,0x5800000= RDP), with its bus-master DMA bound (LANCE byteB→ main-RAM wordB >> 1) and its interrupt on INT1. The Ethernet address ROM at bit0x6800000is modelled as the eight bytes the firmware actually reads:08 00 2b 16 4b 5a c1 86(MAC + a checksum it validates). SeeLANCE_REPORT.md. - The NVR (battery-backed settings RAM) at bit
0x6000000— a persistednvram_deviceover the 0x800-word user-settings block, saved to MAME's nvram directory. Before this it was unmodelled and its address was swallowed by the video-memory mapping, so settings never persisted. SeeNVRAM_REPORT.md.
A bare run gets through POST and then fails level 0's I/O data path test, so it
lands in the interactive diagnostics menu (VERSION 2.08 - SELECTED TEST TYPE:)
instead of reaching level 1.
That test wants 10 bytes on DUART2 channel A and nothing is attached, so this
is an honest failure. It used to "pass" by accident: the write-back latch
returned 0x0099 from DUART2's status register, and bit 0 of 0x99 is RXRDY —
stuck asserted — so the firmware's drain loop counted ten phantom 0x8C bytes.
It is not satisfiable by any documented device — see
reference/level0-10byte-puzzle.md. The routine writes exactly one byte (0xE1,
"disable auto-repeat", which has no reply), sets MR2A=0x07 so the channel is in
normal mode, not loopback (confirmed statically and from a live access log),
and checks only the count: the received byte is stored and never compared, and
there is no expected-value table. The keyboard's FD/power-up reply is 4 bytes,
the mouse's T reply is 4, the tablet's 5 — none is 10. A faithful LK401 would not
help either: the init sequence's receiver-reset flushes the FIFO.
The real terminal does pass this test, so the bytes exist on real hardware — they are simply not inferable from the ROM, and the only honest way to close the question is to observe a real keyboard port at power-up. No faithful responder can be derived from any document.
So instead there is an opt-in stand-in, VT1200_IO_PATH_STUB=1. Read what it
is carefully, because it is easy to misread:
- It is not a keyboard model and it is not any documented device. It stands in for an unknown test fixture on the keyboard port.
- Its bytes are arbitrary (
0x00). Nothing compares them: the routine stores the received byte and never reads it back, and there is no expected-value table, so only the count matters. Feeding a pattern would imply a protocol that does not exist. - It triggers on the exact thing the test does: the one
0xE1write to DUART2-ATHRA(handler reg 3, window0x0620000, store at bit3FDCE8B0), then feeds 10 bytes back on the same channel's receive path via the SCN2681's ownrx_fifo_push(). It replies only while the channel is polled (IMR = 0x00, the level-0 condition), so it cannot inject into the interrupt-enabled OS phase or cause an interrupt there. - It is off by default. A bare run still fails level 0 exactly as before and says so. This is deliberate: the faithful configuration stays the default.
With the stub on the boot clears level 0 and reaches level 1, where the DUART
tests fail: HOST PORT - 15, then PRINTER PORT - 16. It does not park at
HOST PORT - 15. Each failure is reported, the board LED blinks once (finite),
the firmware prints TYPE 'GO' TO CONTINUE (which nothing answers and which does
not wait), and the runner continues. It then enables DUART1's counter-ready
interrupt and wedges in a DUART1 interrupt storm — that is the faithful
ceiling, not HOST PORT - 15. It does not produce a booted OS. See
LEVEL1_CONTINUE_REPORT.md.
There is no infinite board-LED loop on this path. 3F208060/3F208650 are
never executed here, and the boot loop's 3F204440 jane L3F208060 is never
reached (the level-1 runner never returns). The blink that is seen is the finite
L3F208660 called by the error handler. A level-1 failure is fatal only through
L3FDF1010 → L3FDF1270, the genuinely infinite blink, which is entered when the
status word *A13(0xFFA0) is nonzero; the DUART failures leave it zero.
How far the OS gets
With VT1200_SKIP_LEVEL1=1 (below) the OS boots to a running X11 server and on
to the Terminal Manager: the root-window weave renders, the cursor is handed
over 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".
Confirmed in the GUI and from frame dumps: skip_frame3000.png (the weave) and
duart2_default_tm_frame3000.png (the Terminal Manager). The real terminal shows
the same sequence — see reference/vt1200-boot-video/.
The OS's keyboard and mouse drivers initialise and speak the real protocol: it
programs DUART2 (MR1A=0x13, MR2A=0x07, CSRA/CSRB=0x99, ACR=0x60,
IMR=0x33) and sends FD plus LED/click/bell/auto-repeat to the keyboard and 'T'
to the mouse. Input now works: the LK401 and VSXXX-AA device models answer
those commands, the OS reads 01 00 00 00 / A0 02 00 00, switches the mouse to
stream mode, and the X11 cursor moves with the mouse while keycodes reach the OS.
See INPUT_REPORT.md (the cursor evidence is the input_cursor_*.png frames).
The channel-B device is by default the VSXXX-AB tablet: it identifies
as device code 0100, so the OS switches to 5-byte absolute reports, and the
emulated cursor follows the host pointer's absolute position (driven through
IPT_LIGHTGUN_X/Y, i.e. MAME needs -lightgun -lightgun_device mouse). See
TABLET_REPORT.md (evidence: tablet_cursor_*.png). VT1200_MOUSE=1 puts
the VSXXX-AA mouse back (device code 0010, 3-byte relative reports). The mouse
is the faithful part — the real terminal almost certainly has a mouse — so the
tablet default is a deliberate divergence (below).
The OS now also programs the LANCE: it reads the Ethernet address ROM,
programs CSR1/2/3 with its init block, issues INIT, sees IDON, issues STRT
and polls CSR0 in its interrupt path. The Terminal Manager therefore shows the
real hardware address — Ethernet Address 08-00-2B-16-4B-5A, LAT Host Name LAT_08002B164B5A (it used to show 00-00-00-00-00-00). The
Could not determine IP address banner is the missing address the plan allows
(no network peer), not a LANCE failure. See LANCE_REPORT.md.
Not fully working: the firmware's C - LANCE TEST (its internal loopback) still
fails, but not because our DMA loses the FCS. The test's failing case is a
negative test that deliberately sends a frame with no FCS and expects a CRC
error; the frame that carries an FCS (the DTCR=1 passing case) is delivered
intact and passes. The one bit left is RMD1_FRAM — the test expects
RMD1 = 0x6B00, MAME reports 0x4B00, and MAME never sets FRAM. See
LANCE_LOOPBACK_REPORT.md; the M - ETHERNET ROM TEST passes.
Getting past level 1
VT1200_SKIP_LEVEL1=1 drives DUART1's IP3 high, which the firmware takes as
a signal to jump to the OS cold-start and skip all 15 level-1 tests. Note
what this is: a modelled board input, and the real machine reads IP3 low.
So a level-1-skipped run is evidence about the OS, graphics and crash
behaviour, and is not evidence about the real terminal's POST.
The skip is deliberately not the default: the real machine runs level 1, so the faithful configuration stays the default and the switch is how we diverge.
Deliberate divergences from the target machine
Three harness behaviours are not what the real terminal does. They exist so the emulator can be driven and inspected, and they are collected here so a reader cannot mistake the harness for the machine:
| divergence | default | what it does | how to restore the real machine |
|---|---|---|---|
| VSXXX-AB tablet | on | puts the tablet, not the mouse, on DUART2-B; absolute reports let the host pointer drive the cursor positionally | VT1200_MOUSE=1 |
VT1200_SKIP_LEVEL1 |
off | drives DUART1 IP3 high, jumping past all 15 level-1 tests to the OS cold-start | leave it unset |
VT1200_IO_PATH_STUB |
off | feeds 10 bytes on DUART2-A after the level-0 0xE1 write, so the count-only I/O data path test passes |
leave it unset |
The tablet is the one that is on by default because the OS is only reachable
through the skip gate, and there the tablet's absolute pointer is what makes the
Terminal Manager usable. It is a value-aware inverse switch: the default
lives in the code, not in a wrapper script setting VT1200_TABLET, so
VT1200_MOUSE=1 (or VT1200_MOUSE=0 for the tablet) always flips it either way.
Environment knobs (21; down from 33)
Almost all are opt-in and off by default — there is no longer a "working
incantation" to remember, because everything the firmware needs to run is now the
default. The one exception is that the tablet is on by default; VT1200_MOUSE
is the switch that restores the faithful mouse (see the divergences above).
Instrumentation: VT1200_DUARTLOG, VT1200_LANCELOG (every LANCE RAP/RDP and
address-ROM access with the PC), VT1200_LANCE_DMA_LOG (every LANCE bus-master
DMA word access: byte address, value, byte lanes, PC — how the transmit buffer and
the descriptor write-back are read directly), VT1200_BOARD_LOG, VT1200_BOARD_MAP,
VT1200_IRQLOG, VT1200_LOG_UNMAPPED, VT1200_DPYLOG (+_FRAMES),
VT1200_DUMP_FRAME (+_FRAMES), VT1200_DUMP_RAM (+_FRAMES),
VT1200_DUMP_VRAM (+_FRAMES), VT1200_DUMP_DIR, VT1200_NO_RENDER,
VT1200_INPUT_LOG (per-byte keyboard/mouse/tablet TX/RX trace, with emulated
time). Behaviour switches: VT1200_SKIP_LEVEL1, VT1200_MOUSE (put the
VSXXX-AA mouse (relative) back on DUART2-B; the VSXXX-AB tablet is the default —
either pointing device needs MAME's -lightgun -lightgun_device mouse to follow
the host pointer), VT1200_IO_PATH_STUB (the level-0 test-fixture stand-in
described above — not a keyboard model, off by default).
Regression baseline
progress.lua at 200 frames, no env:
PC=3FDCE400 samples=198 (3 distinct PCs)
This has moved twice, both deliberately:
samples=199(1 PC) →samples=53(78 PCs): the working configuration became the default, so a bare run stopped wedging at the hardware-ready gate and began executing real POST work.samples=53(78 PCs) →samples=198(3 PCs): DUART2 became a realSCN2681. Level 0's I/O data path test now fails honestly instead of being satisfied by the write-back latch's phantomRXRDY, so the run parks in the diagnostics menu. That looks like a regression and is not one — it is the removal of a false pass.
The tablet-default change did NOT move this baseline (198 → 198), and that is
expected, not an oversight. The tablet changes what DUART2-B carries on the wire
— its T self-test reply is A0 04 00 00 versus the mouse's A0 02 00 00, and
it then frames 5-byte absolute reports instead of 3-byte relative ones — but at
200 frames a no-env run is still parked in the level-0 diagnostics menu and
has never addressed DUART2-B at all: the T/R exchange happens only in the OS
phase, which the no-env run does not reach. The only DUART2-B traffic in the
window is the device's unsolicited power-up self-test, which nothing clocks in.
The OS-phase render is unchanged too: with VT1200_SKIP_LEVEL1=1 the frame-3000
dump is pixel-identical for the tablet and for VT1200_MOUSE=1
(magick compare -metric AE = 0), and both reproduce the committed
tablet_cursor_*.png frames exactly.
The no-env value is unchanged by VT1200_IO_PATH_STUB — stub off is the
default, so the samples=198 (3 PCs) line above still holds. The stub is a separate
observed configuration:
# VT1200_IO_PATH_STUB=1, progress.lua at 200 frames
PC=3FDCE400 samples=53 (75 distinct PCs)
That is a genuine move, not a false pass: with the count-only level-0 test
satisfied, the run leaves the level-0 wait and executes level-0/level-1 work. It
is not parked at HOST PORT - 15: the console transcript continues
(LEVEL1/2 TESTING **, LEVEL1/2 ERROR -> HOST PORT - 15,
LEVEL1/2 ERROR -> PRINTER PORT - 16, TYPE 'GO' TO CONTINUE), and the run ends
in a DUART1 counter-ready interrupt storm (from frame ~2839; PC in the DUART1
INT1 ISR, polling ISR reg 5 = 0x08). See LEVEL1_CONTINUE_REPORT.md. It is
recorded as a different baseline because it is opt-in; the default must stay
samples=198.
It is a change detector, not a specification. If it moves without you meaning it to, find out why.
Build
MAME source is required. A shallow 0.289 checkout is large and slow to fetch.
git clone --depth 1 --branch mame0289 \
https://github.com/mamedev/mame.git /home/arti/code/k-space/mame0289
harness/mame/build.sh # symlinks the driver in and runs make
build.sh runs make -j$JOBS SUBTARGET=vt1200 SOURCES=src/mame/dec/vt1200.cpp, which builds a single-driver binary
/home/arti/code/k-space/mame0289/vt1200.
The MAME tree is large and the build is memory-hungry: build.sh
defaults to -j8, not all cores. Override with JOBS=4 harness/mame/build.sh on a small/low-memory machine. Progress is written to
harness/mame/build.log.
Run
Headless execution and inspection use MAME's Lua debugger API from
-autoboot_script. (-debugscript only works when a debugger UI drives the
console, so it is not usable headless.)
cd /home/arti/code/k-space/vt1200
M=/home/arti/code/k-space/mame0289/vt1200
# trace execution from the reset vector
$M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
-debug -debugger none \
-autoboot_script harness/mame/trace.lua -autoboot_delay 0
# demonstrate the pcfReadBitmaps n*4 overflow (OVF_N overrides n, 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
The Lua API exposes manager.machine.debugger, device.debug (step, go,
bpset, wpset), CPU registers (cpu.state['A0'].value), memory
(cpu.spaces['program']:read_u32/write_u32), and frame hooks.
Drive the keyboard and mouse (input_inject.lua)
The LK401/VSXXX-AA models read MAME's host input, so they can be driven from Lua for a headless acceptance run:
M=/home/arti/code/k-space/mame0289/vt1200
VT1200_SKIP_LEVEL1=1 \
VT1200_DUMP_FRAMES=4400,4600 VT1200_DUMP_DIR=/tmp \
$M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
-debug -debugger none \
-autoboot_script harness/mame/input_inject.lua -autoboot_delay 0
input_inject.lua walks the mouse from frame 4400 and taps A at frame 4450.
input_probe.lua prints every device port/field tag. See INPUT_REPORT.md.
Trace from the reset vector (trace.lua)
Prints the reset PC (expect 3F200040), disassembles the power-on code to
reset.dasm, and writes an instruction trace of the first two frames to
vt1200-reset.trace.
Exercise the pcfReadBitmaps overflow (overflow.lua)
Instead of booting the parser we jump directly into its arithmetic block with
a crafted in-memory PCF_CharInfo:
a9 = 0x00040000 ; fake pCS; pCS->nChars at a9+0x270
a13 = 0x00060000 ; fake arg blk; n (from pcfReadInt32) at a13+0x20
write_u32(0x00040270, 0) ; nChars == 0
write_u32(0x00060020, n) ; n
pc = 3f984e70
break at 3f985060 ; just after 'sll 0x2,a11'
This is possible because the font parser's field moves use field 1, which
SETF 0,0,1 leaves at 32 bits, so n is a full 32-bit pcfReadInt32 value.
See FINDINGS.md for results and the rest of the analysis.
Run our own payload (hello.lua)
harness/hello-world/ is an 82-byte payload written in GSPA assembly, built by
the 1987 TI SDK (GSPA/GSPLNK under DOSBox). hello.lua loads it into main
RAM at bit 0x01800000 at frame 120, then arms a breakpoint on the level-0
menu idle loop (3FDCE400) whose action is { pc = 01800000 ; g }.
This is debugger entry, not a delivery mechanism: it proves the toolchain produces runnable code and the code runs on the emulated CPU, not how code would reach a real terminal.
M=/home/arti/code/k-space/mame0289/vt1200
HELLO_END=150 VT1200_DUMP_FRAME=145 VT1200_DUMP_RAM=145 \
VT1200_DUMP_DIR=harness/hello-world/evidence \
$M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
-debug -debugger none \
-autoboot_script harness/mame/hello.lua -autoboot_delay 0
The payload writes 0xDEAD, 0xBEEF and a 1024-iteration counter at bit
0x01810000, and fills a 256x64 block in video RAM at row 100, column 64.
The script reads every value back and prints ok/WRONG; a frame dump shows
the block at screen x 256..511, y 136..199. HELLO_AT=9999 is the control run
(nothing loads, markers stay zero). See HELLO_WORLD_REPORT.md and
harness/hello-world/README.md.