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.
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.
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.