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.
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.
An 82-byte payload written in GSPA assembly, linked at bit 01800000h and
loaded into main RAM by harness/mame/hello.lua, executes on the CPU: it
writes DEAD/BEEF and a 1024-iteration counter into RAM and fills a 256x64
block in video RAM, which appears in the frame dump at x 256..511,
y 136..199. Entry is a debugger breakpoint on the level-0 menu idle loop,
not a delivery mechanism. Includes the build script, a COFF .text
extractor (gsprom's hex addresses wrap at 64 KB), the run log, RAM excerpt,
frame crops including a no-load control, and HELLO_WORLD_REPORT.md.
The 16-bit-displacement jump (low byte 0x00, two words) is JRcc, not the
three-word JAcc (low byte 0x80). Printing it as ja made linear
disassembly report the wrong control flow and made every such branch grow
by a word when reassembled.
Register 15 is the stack pointer in either file: 0x0F is A15 and 0x1F is
B15. The disassembler printed both as sp, hiding B-file stack-pointer
code. Print b15 when the R bit selects the B file.
Validated with the 1987 TI GSPA/GSPLNK/GSPROM round trip: the 32 KB region
at bit 3F208000 still reassembles byte-for-byte (md5
43adb6669571041985f3e26fd9ff7cb9), as do the 4 KB and 16 KB spans and the
0x8000/3F240000 region. The converter no longer needs its ja->jr or
sp->b15 rewrites (5 fixups -> 3); a whole-ROM diff contains only those two
output categories. The ROM is unchanged
(b46a3e442ccb86134d2356bd70d22fd2).
The 1987 DOS toolchain (GSPA/GSPLNK/GSPROM) runs under DOSBox and
reassembles 32 KB of the v2.2 ROM byte-for-byte after five documented
fixups to smart34010 output. The 1990 r5.01 tools are OS/2 NE and do
not run in DOS. Adds the converter, round-trip script, and the
DOSBox/dosemu benchmark.
The TFTP font path loads through 3F96B850 and parses with the metadata
reader 3F97EF00, which handles only PROPERTIES/ACCELERATORS/METRICS/
ENCODINGS and silently skips BITMAPS. A font with num_chars=0x40000001
therefore passes the loader (LOADER-A-SUCCESS) without the field ever
being read, and pcfReadBitmaps (3F984970, multiply 3F985050) is never
entered. pcfReadBitmaps has exactly one caller, 3F98BB70, in the X11
font-request reader 3F98AC20, not the TFTP fetch path.
The 'refused after 1024 bytes' of evil-real.pcf is the METRICS table:
its big-endian compressed count 0x00DF is read little-endian as 57088,
so fosAlloc(57088*16) fails at ~906 bytes; BITMAPS (offset 2024) is
never reached. Baseline.pcf fails the same way; baseline_le.pcf puts
that one 2-byte field in the order the loader reads it, parses cleanly,
and still ignores the crafted BITMAPS count.
No ROM patch, no check disabled, no serve.py edit. Baselines unchanged:
no env samples=198, stub samples=53.
Isolated-netns test setup (a private tap-mess-0-0 so a second MAME can run
without holding the owner's tap), the one-file/one-index TFTP servers, the
trace/probe run wrappers, and the two server logs quoted in
FONT_STORAGE_REPORT.md.
A tcpdump on the tap plus a live execution trace settle the 'Disk full or
allocation exceeded' abort: the TERMINAL sends TFTP error 3, from
tftp_close (0x3F5EC860), which reports an incomplete read (the last DATA
block read was a full 512-byte block) using its fixed text for code 3. It
is not a capacity or disk condition.
The font buffers (path/index read buffer bit 0x9316D0, fos parser stream
buffer 0x227FD0, parsed-font cache 0x227F50) are all in main RAM
(0x00000000-0x01FFFFFF), mapped and writable; the writes land in RAM dumps.
No second map overlap exists, so the NVR fix did not cause this. vt1200.cpp
is unchanged and every baseline is intact.
New: fonttrace.lua, fontprobe.lua, two evidence pcaps, FONT_STORAGE_REPORT.md.
The terminal broadcasts an RFC 951 BOOTP request at power-up. Add an opt-in
responder that answers it with yiaddr 192.168.99.2 and siaddr 192.168.99.1,
echoing htype/hlen/xid/chaddr. It offers no boot file: the request's sname and
file are empty, so the reply's are too.
Off by default -- a bare 'serve.py 192.168.99.1' behaves exactly as before
(TFTP on port 69, nothing else). Enable with --bootp.
Delivery note: the host IP stack never receives the source-0.0.0.0 broadcast,
so the responder attaches to the MAME tap's L2 (AF_PACKET) and writes the reply
frame back. See harness/BOOTP_REPORT.md for the evidence and the still-open
'User settings invalid' / 'Disk full' symptoms.
The terminal's settings NVR was not modelled: 0x6000000 fell inside the
video-memory mapping (0x04000000..0x1fffffff), so NVR reads returned stale
framebuffer words and writes were lost. That is the owner's 'User settings
invalid' plus 'settings do not persist' (and the font path is stored here).
Map a persisted NVRAM over the 0x800-word settings block; MAME now saves and
reloads it (nvram/vt1200/nvram), verified write->restart->read. No MAME-tree
change.
The failing loopback case is a negative test: its TX buffer deliberately has
no FCS, and the test expects a CRC error. The DTCR=1 case that does carry an
FCS is delivered intact and passes, so the DMA is not dropping four bytes.
The sole remaining mismatch is one bit: the test wants RMD1=0x6B00
(ERR|FRAM|CRC|STP|ENP) and MAME's am79c90 reports 0x4B00 -- it never sets
RMD1_FRAM. No MAME-tree change made; reported for a second reader.