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.
Add the Am7990 Technical Manual and Am79C90 datasheet to
reference/lance-datasheet/ and a report, reference/lance-loopback-fcs.md,
that answers whether MAME's am79c90 receive() FCS condition
(!(MODE_LOOP) || MODE_DTCR) is inverted.
Verdict: MAME is right. The CRC generator and checker share one circuit,
so with LOOP=1 the receiver can only check the FCS when DTCR=1 -- exactly
what MAME does. The proposed inversion would be wrong in both loopback
cases. No MAME change made.
Milestone 1 of docs/superpowers/plans/2026-10-16-lance-ethernet.md.
Modelled:
* MAME am79c90 (AM79C90) at the firmware's two windows -- bit 0x5900000 is
RAP, 0x5800000 is RDP; every other offset in those windows still falls
through to the board model.
* the Ethernet address ROM at bit 0x6800000, as EIGHT bytes. The plan said
six; the firmware's reader (0x3FDD7520) actually loops i = 0..7 and then
validates a folded ones-complement checksum over bytes 0..5 against
bytes 6..7 (0x3FDD78B0). For the MAC 08:00:2b:16:4b:5a that is 0xC186, so
the ROM is 08 00 2b 16 4b 5a c1 86. A six-byte ROM returns expected = 0
and fails LEVEL1/2 ERROR -> ETHERNET ROM CHECKSUM - 23. The checksum
bytes are derived from the firmware's own algorithm (documented in
LANCE_REPORT.md); they are not read off a real PROM.
* the interrupt on INT1, matching the ISR 0x3FA93400 and INTENB bit 1.
* bus-master DMA, translating the LANCE's byte address to the TMS34010's
bit-addressed main RAM (byte B -> word B>>1), cross-checked against the
firmware's own CSR1 = 0xFF60 = RAM word 0x7FB0.
* VT1200_LANCELOG to trace the register/ROM accesses.
Observed with the firmware's own diagnostics menu (driven by the real LK401
model; new menu_keys.lua): the M -- ETHERNET ROM TEST now PASSES. The
C -- LANCE TEST runs the chip for real -- the firmware's init block
(mode 0x004C = LOOP|INTL|DTCR, PADR 08:00:2b:16:4b:5a) is read via the new DMA,
INIT completes (CSR0 = 0x6FE3, IDON), the chip transmits and receives -- but its
internal-loopback receive still reports a CRC error under MAME (RCV descriptor
1 = 0x4B00, message count 0). Cause: MAME's receive() runs the FCS check when
MODE_LOOP && MODE_DTCR (am79c90.cpp:263) even though DTCR means no FCS was
appended. No MAME change was made; quoted and reported in LANCE_REPORT.md.
The OS (skip gate) reads the ROM MAC, programs CSR1/2/3, INITs, sees IDON,
STRTs and polls CSR0; the Terminal Manager now shows Ethernet Address
08-00-2B-16-4B-5A (was 00-00-00-00-00-00) and LAT_08002B164B5A.
Baselines unchanged: no-env samples=198 (3 PCs), VT1200_IO_PATH_STUB samples=53
(75 PCs). ROM md5 unchanged. No MAME tree changes.
The tablet was opt-in (VT1200_TABLET=1) with the mouse as the default. Invert
it: the tablet is selected with no environment, and VT1200_MOUSE=1 (a
value-aware inverse switch, so VT1200_MOUSE=0 selects the tablet) restores the
VSXXX-AA mouse.
The real terminal almost certainly has a mouse, so this is a DELIBERATE
divergence from the target machine, documented in README.md alongside
VT1200_SKIP_LEVEL1 and VT1200_IO_PATH_STUB. It is on by default because the OS
is only reachable through the skip gate and there the tablet's absolute reports
are what make the Terminal Manager usable.
Baselines re-measured, both unchanged:
* no-env progress.lua at 200 frames: PC=3FDCE400 samples=198 (3 PCs) -> same.
DUART2-B is never addressed in that window (the T/R exchange is OS-phase
only), so the reply bytes A0 04 00 00 vs A0 02 00 00 and the 5- vs 3-byte
framing do not reach the CPU.
* VT1200_SKIP_LEVEL1=1 frame 3000: pixel-identical for tablet and
VT1200_MOUSE=1 (magick AE=0); both reproduce the committed
tablet_cursor_*.png frames exactly.
VT1200_TABLET is now a no-op; run.sh's tablet mode is reported to the owner for
rewording (its comments are his).
The terminal tells the two pointing devices apart from the 'T' self-test
device code (0010 mouse / 0100 tablet) and switches report framing on it:
3-byte relative for the mouse, 5-byte absolute (header 110) for the tablet.
Verified against the running OS: with the tablet it reads the self-test as
A0 04 00 00 and then reads RHRB in groups of 5 (groups of 3 for the mouse).
Refactor the VSXXX model into a shared base + mouse/tablet subclasses and
select the device on DUART2-B with VT1200_TABLET=1 (default: the mouse, so
the mouse stays and is selectable, not replaced). The tablet reports its
documented 0..2199 active area (VCB02 D.1) and reads the host pointer
through IPT_LIGHTGUN_X/Y, MAME's absolute control, so the emulated cursor
follows the host pointer positionally: corner (0,0) -> (194,38), centre
(1100,1100) -> (704,467), corner (2199,2199) -> (1214,898), reproducible
pixel-for-pixel on a repeated coordinate. Driving it from the host mouse
needs '-lightgun -lightgun_device mouse' (documented for run.sh; run.sh is
not edited here).
The mouse and keyboard are unchanged, and all baselines hold: no env
PC=3FDCE400 samples=198 (3 PCs); VT1200_IO_PATH_STUB=1 samples=53 (75 PCs);
ROM md5 unchanged. Nothing is tuned to satisfy level 0's I/O data path test.
Add the terminal's actual input parts to the harness as device models on the
already-modelled SCN2681 at DUART2: a DEC LK401 keyboard (channel A, 4800 8N1)
and a DEC VSXXX-AA mouse (channel B, 4800 8O1), both driven by MAME's host
input and wired bit-by-bit to the DUART (rx_a_w/rx_b_w and a_tx_cb/b_tx_cb).
Route: the docs, not MAME's lk201. MAME's lk201 device executes the 68HC05
firmware 23-001s9-00.bin, which is not in our ROM set (the downloaded zip only
carries the 8051 23-004M2-00.bin, MAME's unused second region), so route 1 is
unusable without sourcing that ROM. The models follow PROTOCOL.md: the keyboard
answers FD with the 4-byte self-test and reports documented keycodes honouring
the division modes; the mouse answers T with its 4-byte self-test and streams
3-byte incremental reports.
One physical-layer detail was required: the SCN2681 receiver mis-samples the
first byte after a receiver reset unless the line is explicitly idle-high
before the start bit. Both models now hold TxD at mark when idle (which is
faithful anyway) -- isolated evidence: bit-banging 0x55 into rx_a_w gives
RHRA=D5 with no idle-high, RHRA=55 with four idle-high bits.
Verified (VT1200_SKIP_LEVEL1=1, input_inject.lua): the OS reads the keyboard
keycode 0xC2 (A) and ~900 bytes of VSXXX reports (dx=+16/+32); after the
self-test it switches the mouse to stream mode; the X11 cursor arrow is visible
at frame 4400 and has moved by frame 4600.
Nothing was tuned for the level-0 I/O data-path test: with no env the boot
still fails it honestly and the regression baseline is unchanged
(PC=3FDCE400 samples=198, 3 PCs). VT1200_IO_PATH_STUB=1 (53/75) and the skip
run (Terminal Manager at frame 3000) are also unchanged. ROM md5 unchanged.
See harness/mame/INPUT_REPORT.md.
The stub run does not park at HOST PORT - 15. Both DUART failures are reported,
the board LED blinks once (finite L3F208660), TYPE 'GO' TO CONTINUE is printed
(no input is read), and the level-1 runner continues ('CONTINUING THE TESTS').
The runner never returns to 3F204440, so 3F208060/3F208650 are never executed.
The run then wedges at frame ~2839 in a DUART1 counter-ready interrupt storm:
the ISR polls DUART1 reg 5 (bit 0x06400050) and always reads 0x08; the counter
runs in timer mode (ACR=0x60) with CTUR/CTLR never programmed, so MAME re-arms
at 1 tick and the interrupt re-asserts every ~229 CPU cycles. Frames at 3500 /
6000 / 12000 are byte-identical; RAM differs by one byte.
Corrects README.md and annotates GATE_CLEANUP_REPORT.md, BRINGUP_LOG.md and
DUART_REPORT.md. Adds a console+PC/milestone instrument and the finding report.
The level-0 I/O data path test writes one 0xE1 byte to DUART2-A THRA and
demands exactly 10 bytes back; it checks only the count and never compares
the content, and no documented keyboard/mouse/tablet message is 10 bytes.
Add an opt-in stand-in that watches for that 0xE1 write and feeds 10
arbitrary (0x00) bytes on the channel's receive path via the SCN2681's
public rx_fifo_push(), refilling the 4-deep FIFO on each RHRA read.
This is explicitly NOT a keyboard model: it stands in for an unknown test
fixture on the keyboard port. It replies only while the channel is polled
(IMR=0x00), so it raises no interrupt; with the stub on the run clears
level 0 and reaches level 1's HOST PORT - 15, but still no OS (SKIP_LEVEL1
remains the only route to X11). Default is unchanged: no env still fails
level 0 honestly (progress.lua samples=198/3 PCs); stub-on is 53/75 PCs.
See harness/mame/README.md.
Evidence for the RAP/RDP window pair (0x5900000/0x5800000), INT1 as the LANCE
interrupt, and the separate MAC address ROM at 0x6800000; MAME am79c90 drop-in
assessment with the kn01/ncd68k templates; the TFTP/LAT font delivery path
(no X font server on 7100); acceptance, risks and sequencing.
The real VT1200 board has a DUART2 (SCN2681) at 0x0620000 for the keyboard
(ch A) and mouse (ch B) with INT2 wired. Map it unconditionally instead of
opting in via VT1200_DUART2; the OS's DUART interrupt dispatcher needs a real
read-only ISR distinct from the write-only IMR.
Verified with frame dumps: with ONLY VT1200_SKIP_LEVEL1=1 the Terminal Manager
V2.2 dialog now appears (frame 3000; VRAM md5 73c3c9a1..., identical to the old
skip+DUART2 run).
This does NOT keep the faithful no-env path: level 0's I/O data path test
(L3FDCE470) transmits 0xE1 to DUART2-A and drains the receiver until it has read
10 bytes (SRA RxRDY). The board write-back latch used to alias CSRA/RHRA and
faked that (baseline first divergence: R 06200001 pc=3FDCE670 = 0x0099 vs the
real chip's 0x000C), so the firmware dropped into the interactive diagnostics
menu. No faithful device we have (LK401 sends at most 4 bytes) supplies the 10
bytes; an external TX->RX loopback does not change the transmit count. See
OS_BRINGUP_REPORT.md section 8 (DONE_WITH_CONCERNS).
The OS keyboard/mouse driver does initialise: skip-phase OS code programs
DUART2 (MR1A=0x13, CSRA=0x99, IMR=0x33, ...) and sends FD to the keyboard plus
the documented LED/click/bell/auto-repeat commands, and 'T' to the mouse.
The skip gate reaches the root weave and stops: video RAM and the rendered
frame are byte-identical at frames 3000/10000/30000/45000/60000 (42 s -> 14
min), and all 4 MiB of main RAM is identical at every sample except one byte
that oscillates. The PC histogram is tight: entered at frame ~611, dominated
by 0x3FB30390/0x3FB30110.
Wall: DUART2 (0x0620000, keyboard/mouse) is unmodelled, so the read-only ISR
at 0x06200005 is served by the board write-back latch and returns the value
the OS wrote to the write-only IMR (0x0033). The OS's DUART dispatcher
(0x3FB43800) masks 0x66 -> 0x22, so it services a phantom interrupt that can
never clear, spinning in the channel handler 0x3FB42410 and queue routine
0x3FB300F0. Forcing only 0x06200005 to 0 reproduces the full DUART2-mapped
run byte-for-byte; with VT1200_DUART2=1 the Terminal Manager dialog box from
the reference video appears at frame 3000 and the machine runs on.
Defaults unchanged: no-env still reaches HOST PORT - 15, skip still reaches
the weave. New opt-in knobs VT1200_DUMP_{FRAME,RAM_FRAMES,VRAM_FRAMES} and
resolve_console.py; progress.lua untouched.
A bare 'vt1200 vt1200' now boots exactly as the old six-variable incantation
(VRAM, handshake busy=2, DUART1, monitor switch) did, so the no-env run is no
longer a silently-broken machine.
Promoted (5): VT1200_VRAM, VT1200_BOARD_MODEL, VT1200_BOARD_BUSY_TICKS,
VT1200_DUART, VT1200_DUART_SEL (folded to DUART1-only; DUART2 is the new
opt-in VT1200_DUART2).
Deleted (15): VT1200_CAL/_VID4M/_W8, VT1200_BOARD_RAM/_DEFAULT/_BUSY_W2/
_BUSY_W3/_DMA_TICKS/_SWITCH, VT1200_DUART_LB1/LB2, VT1200_LOWLOG,
VT1200_VRAMLOG, VT1200_STRUCTLOG, VT1200_DUART1_INT2.
VT1200_BOARD_SWITCH is superseded: 0x0640000D is the DUART1 IP register, and
the DUART mapping wins the last-wins slice, so the board model switch was
never consulted (verified: identical output with and without it).
Renamed: VT1200_DUART_IP3 -> VT1200_SKIP_LEVEL1, which now also selects the
OS-phase DUART1 IRQ routing (INT2) instead of POST's INT1, removing the
standalone VT1200_DUART1_INT2 knob. The skip gate itself stays opt-in.
Evidence (equivalence): frame-160/200/600 md5, progress.lua at 200/1500
frames, and the cap.lua console transcript from the no-env run are
byte-identical to the incantation run. Re-baseline: no-env 200-frame hottest
PC 3FDCE400 samples 199 (1 PC) -> samples 53 (78 PCs). ROM md5 unchanged
b46a3e442ccb86134d2356bd70d22fd2. Header comment block updated; see
harness/mame/GATE_CLEANUP_REPORT.md.
Runtime region-writes (on-disk ROM untouched) applied after the level-0 ROM
checksum:
three known failing checks: 3FA73980 CA04->C004, 3FA76060 CB04->C004,
3FA760F0 CB04->C004 (skip the c6/c0/c1 flag stores)
VT1200_L1PATCH_EXTRA adds the second-round checks 3FA761F0 / 3FA763A0
(the retry loop reaches c3/c2 once c0/c1 stop short-circuiting)
VT1200_L1PATCH_VERDICT optionally forces L3FA6E4B0's return (3FA71B60
4D28 move a9,a8 -> 5708 clr a8)
With the five branch patches the HOST PORT test's caller returns 0 and no
'HOST PORT - 15' is printed, but level 1 does not complete: the CPU stays in
the DUART ISR chain (INT1 0x0F60 -> 3FA92F40 -> L3FA78D90 -> 3FA78xxx-3FA7Bxxx)
for >19000 frames with no error. l1p_pie_frame1500.png is a frame dump of the
self-test circle/pie -- the renderer draws curves, fills and blits correctly.
The '256 KiB / driver only models 256 KiB of main RAM' note was from an early
dump and is wrong. The driver maps 0x00000000-0x01ffffff, which on this
bit-addressed space is 0x2000000 units = 0x400000 bytes = 4 MiB, confirmed
empirically (mainram share size == 4194304). Keep the correction with the
original so the stale figure cannot mislead again.