vram_blit_ copies a 1-bpp rectangle from RAM to VRAM. The manual has no L,L
variant, so the destination is XY and the hardware forms the address:
B0=SADDR B1=SPTCH B2=DADDR(XY) B3=DPTCH=0x800 B4=OFFSET=0x04000000
B7=DYDX PSIZE=1 PMASK=0 CONVSP=CONVDP=0x14 CONTROL raster-op=0
SPTCH is a parameter because 0 makes every row read the same source address,
which is how one expanded row can be stamped eight times down the screen.
The .bss region in the linker command grows from 0x40000 to 0x200000 address
units (32 KB -> 256 KB) so a 1280x1024 1-bpp frame buffer (163840 B) fits; the
previous .coff had been built from sources that were not yet committed.
bench-sweep.sh gains its executable bit.
Measured on the unit: byte stores to DRAM 13.92 us, to VRAM 13.93 us -- the
same, so the display arbiter is not involved. In MAME 16-bit stores halve the
time exactly (863 ms vs 1588 ms per frame), i.e. the cost is per store
instruction, not per byte.
Against the graphics engine (FILL L 0.40 us/word, PIXBLT L,XY 0.62 us/word)
the CPU is 25-35x slower per word. So frames must never be built with CPU
stores: the LANCE can chain descriptors so a 160 KB frame lands contiguously
in a pre-armed buffer with no CPU copy, and one PIXBLT L,XY puts it on screen.
Budget: full frame ~5 fps (129 ms wire + 60 ms blit), 640x512 ~21 fps,
320x240 ~80 fps. Report section 75; cmd 10 (PIXBLT from RAM) and cmd 11
(16-bit store control) are the benchmarks.
cmd 9's first version wrote to 0x01A10000 (bits), chosen as "above MARK, below
.bss". On the unit that was not usable RAM: no reply in 120 s and the machine
stopped answering ping and rdebug -- one power cycle.
The target is now `static unsigned char drambuf[512]`, placed by the linker
inside .bss, which the payload already relies on for its labels and strings.
Loop and store count are unchanged, so the per-store figure stays comparable.
Report 74.5 records it.
cmd 8 alternates 0xFFFF/0x0000 per frame so the rate is visible: 300 frames
in 9.775 s on the unit, 32.6 ms each, 30.69 fps sustained.
cmd 9 is the same byte-store loop aimed at a DRAM buffer, to measure the CPU's
store rate without the VRAM arbiter and size a frame-expansion stage.
vtcmd.py --dram selects it.
Report 74.3 records the independent Ghidra pass over the ROM: the firmware
sets CONVSP/CONVDP=0x14 (pitch 0x800 bits = 256 B), PSIZE=1, PMASK=0 and
OFFSET=0x04000000 for XY addressing -- the same values vram.asm uses -- and
fills with FILL_L chunked at 0x7FC0 pixels because DYDX is 15 bits per axis.
There is no VBLT and no wait-state/arbitration register in the ROM, so the
12 us/byte CPU store cost is inherent to this board, not a setting.
Byte stores to VRAM measure 2284 ms per 1280x1024 frame on the real unit
(0.44 fps) -- about a scanline period per byte, i.e. almost no access through
the arbiter. vram.asm now drives the GSP's array engine instead: FILL L with
DADDR/DPTCH/DYDX/COLOR1 (B2/B3/B7/B9), PSIZE=1, PMASK=0 and PPOP=0, per the
TMS34010 User's Guide's implied-operand table, with CONTROL read-modify-written
so CD/RM/RR survive.
Measured: 32.7 ms/frame, 30.6 fps on the unit; 27.1 ms in MAME (74x over byte
stores there, 70x on hardware). 2,508,224 destination words/s = 0.40 us/word.
Consequence for video: a full uncompressed frame is wire-limited at 8 fps
(163840 B at 1.27 MB/s); a small source expanded on the device is limited by
the ~33 ms transfer, i.e. ~30 fps full screen if the expansion uses PIXBLT.
build-lance.sh assembles vram.asm and links VRAM.obj; MARK moved to 0x01A00000
because .text had come within 146 bytes of it.
Loading the COFF over rdebug pokes thousands of words one at a time, so how
long the payload takes to appear varies by tens of seconds -- a fixed sleep is
either wasteful or too short. The payload now broadcasts three frames the
moment its ring is armed: ethertype 0x88B6, 'K' 'S', opcode 0xFF, magic
"LANC". `vtcmd.py wait` listens for it; measured at 8.7 s in MAME.
Two details found by measurement:
* The announce must use TX slot 0, because the chip transmits only from its
ring position -- arming slot 7 while the position is 0 never sends
anything (the first attempt did exactly that and was silently lost).
* After the announce the reply cursor advances to slot 1 to match the chip's
new position, so replies do not collide with the announce descriptor.
txfull then stays 0 across consecutive replies; before this it read 1.
test-osboot.sh ties the hand-back together with no sleep: listen, load, wait
for the announce, stats, osboot, wait for :771. Report sections 73.3/73.4.
osboot at 17:11:21, rdebug back at 17:12:01 (40 s), ping OK, and the VRAM
screenshot shows the Terminal Manager with a live display, the right MAC/IP/
LAT name and the clean-boot banner.
Same machine, same payload as the two failed reset-vector attempts: reboot
0/2 (POST, never returns), osboot 1/1 (OS cold start, 40 s). The display
returns before the network -- the operator could see the screen at 32 s while
port 771 was still closed.
Recorded as 1 of 1, not as reliable: section 72.3 already lists three
conclusions drawn from single observations, so a second run is needed before
this is the standard hand-back.
The reset vector is the wrong entry: it starts POST, which re-programs the
board window from a state a payload has already changed. The firmware has a
second entry, 0x3F5788C0, where the boot gate at 0x3F2042D0 branches when it
skips level 1. It begins with DINT, CSR0=STOP on the LANCE, and a board-window
re-program -- exactly what a takeover breaks.
MAME, from the LANCE access log: the payload's last access and the OS's first
are 130 us apart, then pc=3F578940 writes CSR0=0x0004 (STOP) and pc=3FDDCAE0
(the OS driver) re-initialises the chip, after which the OS keeps servicing it.
cmd 6 / vtcmd.py osboot. Unit run pending; the report section is 73.
The operator confirms attempt 1's recovery at 88 s was a manual power cycle,
not the payload. That makes the record 0 confirmed successes out of 2, and
the first reading of attempt 1 -- "black and hung, needs a power cycle" --
was right after all.
Three corrections are now recorded in section 72.3, because they are one
mistake made three times: "hung" from a single sample, then "works" from the
same sample after it recovered, then "works" again when that recovery was
attributed to the payload. The recovery had an external cause, which one
observation cannot tell apart from the payload working.
The Ghidra finding stands: the firmware has no software reboot, so there is
no supported entry to jump to. Untested hypothesis for the hang remains
that the reset path leaves the LANCE busy-mastering; CSR0_STOP before the
jump would be the single-variable test.
vtcmd.py reboot requires --force and now says plainly that it has never
worked and that a power cycle should be expected.
Attempt 1 (16:58:45) came back: port 771 reopened at 88 s, the unit pinged,
and a VRAM screenshot showed the Terminal Manager. Attempt 2 (17:04:15,
after a power cycle) never came back -- no ping, no rdebug at 130 s, monitor
asleep, operator reports it stuck.
The previous commit recorded attempt 1 as "it works, no power cycle needed".
That was a single sample and attempt 2 falsified it. Section 72.3 now records
both runs as a table, the 1-of-2 rate, and the two corrections made on the way
(first "hung", then "works").
Untested hypothesis: the reset path does not reset the LANCE, and the payload
hands over with bus-master DMA enabled; an in-flight transfer or pending
interrupt may leave the board-window handshake at 0xC00000B0/0xC0000100
waiting forever. The single-variable test would be CSR0_STOP before the jump.
vtcmd.py reboot requires --force again and states the measured rate.
Issued while the payload owned the LANCE: the reply arrived, control went to
0x3F200040, the screen went black -- and the OS was back 88 s later. Port 771
reopened on its own, the unit answered ping, and a VRAM screenshot shows the
Terminal Manager with the right MAC/IP/LAT name and the clean-boot
"Ethernet Port Error - 24" banner. A taken-over unit can therefore hand
itself back without a power cycle.
An earlier reading of the same run reported "black and hung, needs a power
cycle". That was a single observation taken inside the ~90 s POST window.
The black screen is the reset path reprogramming the board/video window
(0xC00000B0, 0xC0000100) and not re-enabling the display until POST ends.
vtcmd.py reboot now says how long it takes and warns not to power cycle
during it, instead of refusing to run.
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.
Panel read off the unit after loading lance_bcast.coff with the host sending
0x88B5 broadcasts: TXD=5, RXF=5, ET=0x88B5, CSR0=0x0073 (STRT|TXON|RXON|INEA)
and REST=0x0073. First exchange with a real peer using only our own driver
code.
Two display bugs found by reading it: the glyph table had no 'X' (so TXD/RXF
printed as 'T D'/'R F') and the three MAC words were drawn on top of each
other. Both fixed: X added, MAC on three rows, REST moved to y=878.
lance_bcast.c drives the chip entirely on its own -- init block, PADR/LADRF,
eight TX and eight RX descriptors, INIT/IDON/STRT, TDMD, completion polling --
and test-bcast.sh runs it in MAME and reports:
TXD=6 RXF=5 ET=0x88B5 source MAC 0xC6FC ok=1 PASS
The tap capture separates the directions: six 60-byte 0x88b5 broadcasts from
08:00:2b:16:4b:5a (our payload) and 49 from the host. RXF counts host frames
(early stop at five) and the recorded source MAC is the host's.
Fixes behind it: per-frame TX descriptor (the chip advances its own ring
position), cfg-net needed for a tap to exist at all, pkill -x instead of -f,
and two bit-vs-byte addressing bugs.
Normal (promiscuous) mode instead of loopback: programs its own init block,
PADR and LADRF, arms eight RX descriptors, transmits six 60-byte broadcast
frames (ethertype 0x88B5) through its own TX ring with TDMD kicks, then counts
received frames and records the last source MAC and ethertype.
Everything is the payload's own code -- CSR accesses, init block, rings,
descriptors, DMA results. Panel: TXD (transmitted), RXF (received), ET (last
ethertype), MAC (last source), CSR0, REST.
The terminal shows 'Ethernet Port Error - 24' after a clean boot while its port
works, so it is the firmware's own Ethernet self-test result (which also fails
in MAME per LANCE_LOOPBACK_REPORT), not damage from the payload.
Consequences: the banner is not evidence about the takeover, and the 'needs a
power cycle' claim now rests only on the runs that crashed; for the REST=0x0073
build the network was never checked after the run.
They were drawn at y=850, overlapping the REST text row (846-867), which is
what shows as a stray box over REST in the unit photo. The text panel already
reports OK as YES/NO.
Photo images/20261030_155855.JPG shows CSR0/CSR1=0x07B3, TMD1=0x8300,
RMD1=0x0300, MCNT=0x001C, OK=YES (loopback reproduced with the text build) and
REST=0x0073 = INIT|STRT|TXON|RXON|INEA, i.e. the chip is running again after
the payload's 0x7F41 -> 0x0042 hand-back.
The OS however shows 'Ethernet Port Error - 24' and stays off the network, so
the recovery failure is in the firmware's driver state, not in the LANCE. A
takeover run therefore still costs a power cycle.
The text panel grew .text past MARK and the payload overwrote its own code
with cell() writes (illegal opcode on the unit). The build now parses
build/lance.map and aborts if the end of .text reaches MARK, printing the
remaining gap.
.text went from 0x018C6140 to 0x018CACD0, so the marker cells at 0x018C8000
landed inside the payload's own code; cell() then overwrote instructions and
the CPU faulted with an illegal opcode before drawing anything.
MARK moved to 0x018D0000 (~2.6 KB past the end of .text).
The restore wrote 0x0042 into 0x23CC50, overwriting the driver's RDP pointer
(0x05800000); the next CSR access then targeted 0x0042 and the terminal died
with an illegal opcode. The TX kick does move @0x23CC50,a3 / move *a3,a5 --
it reads the live CSR0 and ORs TDMD, there is no shadow cell.
Report: section 10 corrected, section 62 added with the disassembly and the
consequence.
The panel now draws labelled hex text instead of 16 white boxes: a 5x7 glyph
table for 0-9/A-F plus the letters the labels need, built at runtime because
this toolchain never runs .cinit. Rows: CSR0, CSR1, TMD1, RMD1, MCNT, OK and
REST (CSR0 after handing the chip back), on a 32px pitch.
The restore now matches the firmware's own init: CSR0 = 0x7F41
(INIT | INEA | all status bits write-1-to-clear) instead of a plain INIT, then
poll IDON, then 0x0042 (STRT | INEA), plus a retry if TXON does not come up and
an update of the driver's CSR0 shadow cell 0x23CC50.
Note: on the first post-fix run the network still did not recover, so the
restore remains a convenience at best; results are read from the panel.
Panel read off the VT1200 after loading lance_fixlen.coff (via
dwt/loader/hostload.py): CSR0=0x7B3, TMD1=0x8300, RMD1=0x0300 (OWN clear,
STP|ENP), MCNT=0x001C, ok=1 -- byte-identical to the MAME reference
(0x7B3/0x8300/0x0300/0x1C/1).
So the firmware's init/ring/descriptor/TDMD sequence is confirmed on the
machine, and the earlier failure (CSR0=0x1BB, TMD1=0x8338, no RX) was the
payload's descriptor-length encoding: the field must be -len (0xFFE8/0xFA0A),
which MAME masked via get_buf_length()'s 0xF000 OR.
Also records that the payload kills the network while it runs (results are
panel-only) and that the give-the-chip-back block did not restore it.
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.
Replaces the 30-address spot check with a mechanical pass: every report address
is classified against the MMTM prologue table, so interior labels are shown to
be interior rather than asserted. 225 addresses, 82 mapped, 143 unmapped, of
which 102 fall inside a mapped function and 41 sit in 26 unmapped entries.
Maps those 26: arpresolve (if_ether.c:261) for 0x3F60FB60, ifa_ifwithnet
(if.c:189) for 0x3F623180, tcp_respond (tcp_subr.c:112) for 0x3F6BD9F0,
leerror (if_le.c:972) for 0x3F9DAFF0, the leintr TINT half for 0x3F9D8A50,
leinit/lereset for 0x3F9D27D0, the tcp timer pair for 0x3F604C30, and marks the
rest firmware-specific with the reason.
Also corrects the RX entry: the function starts at 0x3F602DB0, not 0x3F602E00.
Maps tcp_slowtimo/tcp_fasttimo (tcp_timer.c:76/51), ether_output
(if_ethersubr.c:69) for the device transmit method 0x3F607C90, m_pullup
(uipc_mbuf.c:440) for 0x3F6B0160, arpresolve for 0x3F6105E0, leinit/lereset
for 0x3FDDCBE0, leintr for the diagnostics handler 0x3FDD8E80 and leprobe for
the address-ROM reader 0x3FDD74B0. Marks the MAC formatter, IP-output helpers,
OS primitives (locks/deferred work) and buffer helpers firmware-specific.
Also documents why a raw address grep shows 225 addresses but only 66 inside
the mapping sections: the rest are internal labels and call sites inside
mapped functions, and mapping is per function entry.
tcp_output (tcp_output.c:46), tcp_template (tcp_subr.c:65), tcp_close
(tcp_subr.c:244), tcp_timers/TCPT_2MSL (tcp_timer.c:144/158) and timeout
(kern_clock.c:232) for the TCP side; ifa_ifwithaddr (if.c:141) for 0x3F61AB30;
in_cksum (in_cksum.c:24, ADDCARRY at :20) for 0x3FB2C290; bcmp (bcmp.c:18) for
0x3F942230; the ntohl macro for 0x3FB2C000. Marked firmware-specific:
0x3F61B000 (lock wrapper), 0x3F61BFC0 (packet accessor), 0x3F60BD40 (queue
post), 0x3FB2C140 (compare helper).
Also: the 120 written at TIME_WAIT is 2 * TCPTV_MSL with PR_SLOWHZ=2
(tcp_timer.h:60, tcp_input.c:1068), so TCB+0x80 is t_timer[TCPT_2MSL].
Adds rows for the field-codec dispatcher 0x3F660380 / table 0x1E3D80 (8-byte
entries, fn0 at +0, fn1 at byte 4) and its entry-2 pair 0x3F615E00 (convert,
a ntohl-style swap) / 0x3F616090 (compare, the in_pcblookup address-test
idiom) -- the operations are ordinary BSD, the table-driven codec is firmware
specific (no lib/libc/rpc in this tree, no kernel fn-pair table).
Adds 0x3F65A570 and 0x3F6A86D0 as firmware-specific (cmpi 0x72 character-coded
id; not reachable from any EtherType), and the receive re-arm 0x3F9D17E0
(referenced at 0x3F602EC0/0x3F603100) mapped to lereset/ledrinit.
Extends 56.3's coverage statement to match.
It said every addressed function has a BSD counterpart. The protocol layer
does; the deferred receive/submit path and the interrupt dispatcher are
firmware glue with no 4.4BSD equivalent (section 56).
Adds the quoted identifiers that three rows lacked: ip_output.c:258-261
(htons len/off, zero sum, in_cksum -- operation for operation with
0x3F62EA00), udp_input's preamble and in_pcblookup(&udb, ip_src, uh_sport,
ip_dst, uh_dport) at udp_usrreq.c:223, udp_output's in_cksum at :415, and
tcp_dooptions at tcp_input.c:1309 (called from :387 and :567).
Adds the seven addressed functions that were missing: 0x3F602E00, 0x3F9E0B80,
0x3F600200, 0x3F603250, 0x3F9DC890, 0x3F9DCF50 are VT1200 glue (BSD calls
ether_input inline from lerint at if_le.c:761 and if_start directly), and
0x3F7144C0 is the firmware's own interrupt dispatcher.
Corrects 54.4, which claimed every addressed function has a BSD counterpart.
struct inpcb (sys/netinet/in_pcb.h) lays out inp_faddr, inp_fport, inp_laddr,
inp_lport at 12/16/18/22 with 16-bit alignment -- exactly the ROM offsets
0x60/0x80/0x90/0xB0, in the same order in_pcblookup tests them. A 32-bit
aligned build would put them at 12/16/20/24, so the match also pins the
firmware ABI to 16-bit alignment, consistent with struct protosw's 6/10.
0x3F61EC00 is rip_input() (raw_ip.c:58): PCB-list walk on protocol/laddr/
faddr, sbappendaddr delivery, and the ips_noproto counter we recorded at
0x96500. The dsj gate compares that PCB field against 1 -- the raw socket
bound to ICMP -- so the 5/8/13..18 skip list is the raw-ICMP filter for types
the kernel answers itself.
The two TCP state tables map onto tcp_input's two switch(tp->t_state) sites
(506 and 865); table A's shared handler for CLOSING/LAST_ACK/TIME_WAIT matches
the source's fallthrough group at 840-842. ICMP checksum verify and reflect
match ip_icmp.c:184 and 522-523.
Adds the ROM -> BSD correspondence with the construct that identifies each
pair: ipintr/ip_input's inetsw[ip_protox[]] dispatch (both tables found in RAM
at 0x1E0F70 and 0xD6BE0, struct protosw offsets 6/10 match the dump),
icmp_input/icmp_reflect/icmp_send, tcp_input/tcp_dooptions/tcp_mss,
udp_input/udp_output, in_pcblookup (line-for-line: matchwild=3, lport, laddr,
faddr/fport, best-match), arpresolve/arplookup/in_arpinput/arprequest, and the
pmax LANCE driver's leinit/lereset/ledrinit/lestart/leintr/lerint.
lestart() writes the descriptor length as plain -len (if_le.c:485), which for
24 bytes is 0xFFE8 -- confirming section 50 from a third direction and showing
our 0x0FE8/0x0A0A were simply positive values.
original-bsd spans 1979..4.4BSD-Lite2 and carries the same if_ether.c string as
the ROM. Its pmax (DECstation) if_le.c clears IDON before STRT, Linux does too,
but NetBSD's lance.c does not -- and neither does the VT1200 firmware itself
(0x3FDDCBE0 writes CSR1/2/3 then 0x7F41 and the live path goes INIT -> poll
IDON -> 0x0042 with no IDON clear). Since the firmware works on the real
machine, the IDON hypothesis is dead and the descriptor length encoding
(section 50) is the remaining explanation.
The local NetBSD tree reaches back to 1992 with the 386BSD-0.1 import
(a319363d6037). Its sys/netinet/if_ether.c:350 carries the exact ROM string
'arp: ether address is broadcast for IP address', which the modern tree no
longer uses ('link address'), placing the firmware in that lineage.
The 386BSD ip_icmp.c switch contains exactly the case labels that section 46
derived from the ROM's dsj + compare chain (REDIRECT, ECHO, TSTAMP,
TSTAMPREPLY, IREQ, IREQREPLY, MASKREQ, MASKREPLY), confirming that reading
from source rather than inference.
harness/mame/NETWORK_REPORT.md: how the VT1200 firmware drives the LANCE,
from ROM disassembly and MAME traces. 50 sections, each claim tied to a bit
address, a trace count or a quoted source line. Covers the driver device
struct (0x23CBF0), init (0x3F9D4BC0), descriptor/DMA encoding, the live
handler (0x3F9D5B40) with per-status-bit dispatch, the RX poll (0x3F9D62B0),
ring arming (0x3F9D36C0), TX completion (0x3F9D8AF0) and the single transmit
path every protocol uses (0x3F6084E0 -> 0x3F9DC890 -> 0x3F9D4BC0), plus the
IP protocol dispatch (0x3F6270F0), ICMP (343-count chain), TCP state tables
and live TIME_WAIT, the ARP responder (0x3F6119A0) and the socket 4-tuple
demux confirmed by a memory dump.
It also records what was withdrawn: five successive explanations for the
real-silicon CSR0=0x1BB / TMD1=0x8338 result, and the descriptor-length
encoding bug found by comparing Linux 7990.c and NetBSD am7990.c (length must
be 0xF000 | -len; MAME masks the difference in get_buf_length).
harness/udpstream: the LANCE loopback/takeover payload and the variants used
to test the init/kick orderings in MAME (bare TDMD, STOP before INIT, double
INIT, long wait with CSR0 sampling, IDON clear, corrected descriptor length).
build-lance.sh takes LANCE_SRC/LANCE_OUT so the variants build from one script.
The 1987 TI K&R toolchain already compiled and linked a fib test, but nothing
had ever been executed on a VT1200. This is that program: hello.c draws HELLO
into video RAM (1 bpp, bit 0x04000000, no X11) and is loaded through the COFF
loader with hostload.py, on the real unit and in MAME.
Two runtime traps shaped the design, both recorded in the README:
* boot.obj's exit() is `for(;;)` and _c_int00 ends in `reti`. That is a
standalone-ROM runtime, not a process: the child spun forever, and because
the VT1200 OS is cooperative it took the whole terminal down (illegal
opcode on hardware). entry.asm replaces boot.obj -- set STK/FP, call
_main, `rets` back to the create_process trampoline, the same return
xhello uses -- and the image carries no .cinit (tables are built in code).
* Pointers are bit addresses and pointer arithmetic scales by object size,
so p + n on a char * is n bytes. The pre-fix frame in evidence/ is that
bug: 32 one-byte bars spread across a row.
Framebuffer geometry is not portable: the real unit is 1280 px wide at a
256-byte row pitch, MAME's canvas is 1024 px at 128. build.sh builds both
(ROW_BYTES), and the README records how the real pitch was measured from a
real-unit frame rather than taken from MAME.
Evidence: real-unit frames in harness/c-hello/evidence/ (the pre-fix stripes
and the working HELLO), and dwt/loader/evidence/real-unit-xhello/ for the X
window that ran on the unit earlier.
Andrew Morton's Unix-hosted assembler/linker for the TMS34010, fetched from the gsptools archive and patched to build on x86-64. The K&R sources silently truncate pointers there: implicit int parameter declarations and fixed-argument printf wrappers. The patch fixes those and the flex 2.6 yy_init mismatch.
hello.as34.g is harness/hello-world/hello.asm in as34 syntax. as34/ld34 reproduce the GSPA-built payload byte-for-byte (82 bytes, md5 db91788991ebba476b55a11127a36475), and the MAME hello.lua run reports the same markers, the same 1024-word VRAM block and a byte-identical frame.
build.sh downloads the upstream tarball at build time (sha256 pinned); the tree is not vendored.