Files
vt1200/harness/udpstream/send.py
T
arti 874d1fbbf5 networking RE report + LANCE payload variants
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.
2026-10-30 01:25:21 +02:00

122 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""Host sender for the VT1200 udpstream spike.
Sends full 1280x1024 frames as 8-row datagrams:
+0 u16 start row (LE) +2 u16 row count +4 rows*160 bytes
Usage:
python3 harness/udpstream/send.py --host 100.103.100.103 --fps 5
python3 harness/udpstream/send.py --host 100.103.100.103 --fps 0 --seconds 5
The frame is a black field with a white bar (encoding the frame number), a
moving box, and a border, so a screenshot proves the whole frame arrived and
which frame it was.
"""
from __future__ import annotations
import argparse
import socket
import struct
import sys
import time
W, H = 1280, 1024
ROW_BYTES = W // 8 # 160
ROWS_PER_PKT = 8
PORT = 7000
def frame(n: int) -> bytes:
"""One 1-bpp frame, packed rows of ROW_BYTES, LSB-first."""
rows = bytearray(H * ROW_BYTES)
# border: rows 0,1,H-2,H-1 and columns 0..3 / W-4..W-1 white
for y in (0, 1, H - 2, H - 1):
rows[y * ROW_BYTES:(y + 1) * ROW_BYTES] = b"\xff" * ROW_BYTES
for y in range(H):
r = rows[y * ROW_BYTES:(y + 1) * ROW_BYTES]
r[0] = 0xFF
r[ROW_BYTES - 1] = 0xFF
rows[y * ROW_BYTES:(y + 1) * ROW_BYTES] = r
# moving 128x128 box
x0 = (n * 37) % (W - 128)
y0 = (n * 23) % (H - 128)
for y in range(y0, y0 + 128):
base = y * ROW_BYTES
for xb in range(x0 // 8, (x0 + 128) // 8):
rows[base + xb] = 0xFF
# frame-number bar on row 64: 16 pixels of white per 16-frame step
y = 64
base = y * ROW_BYTES
for i in range(n % 64):
x = 32 + i * 16
rows[base + (x // 8)] |= 0xFF
rows[base + (x // 8) + 1] |= 0xFF
return bytes(rows)
def main(argv: list[str]) -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--host", default="100.103.100.103")
ap.add_argument("--port", type=int, default=PORT)
ap.add_argument("--fps", type=float, default=5.0,
help="target frames/s; 0 = as fast as possible")
ap.add_argument("--pkt-gap-ms", type=float, default=0.0,
help="sleep between datagrams; gives the cooperative OS a "
"chance to run (a continuous burst starves it)")
ap.add_argument("--seconds", type=float, default=0,
help="stop after N seconds (0 = run until Ctrl-C)")
a = ap.parse_args(argv[1:])
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1 << 20)
dst = (a.host, a.port)
n = sent_bytes = sent_pkts = 0
t0 = last = time.monotonic()
frames_at_last = 0
try:
while True:
f = frame(n)
for r in range(0, H, ROWS_PER_PKT):
payload = f[r * ROW_BYTES:(r + ROWS_PER_PKT) * ROW_BYTES]
pkt = struct.pack("<HH", r, ROWS_PER_PKT) + payload
sock.sendto(pkt, dst)
sent_bytes += len(pkt)
sent_pkts += 1
if a.pkt_gap_ms:
time.sleep(a.pkt_gap_ms / 1000.0)
n += 1
now = time.monotonic()
if now - last >= 1.0:
rate = (n - frames_at_last) / (now - last)
print(f"{n:6d} frames {rate:5.2f} fps "
f"{sent_bytes / (now - t0) / 1e6:5.3f} MB/s "
f"{sent_pkts / (now - t0):6.0f} pkt/s", flush=True)
last = now
frames_at_last = n
if a.seconds and now - t0 >= a.seconds:
break
if a.fps > 0:
sleep = t0 + n / a.fps - time.monotonic()
if sleep > 0:
time.sleep(sleep)
except KeyboardInterrupt:
pass
finally:
dt = time.monotonic() - t0
print(f"sent {n} frames, {sent_bytes} bytes in {dt:.1f}s "
f"({sent_bytes / dt / 1e6:.2f} MB/s, {sent_pkts / dt:.0f} pkt/s)",
flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))