Commit Graph
236 Commits
Author SHA1 Message Date
arti 0c93078ee3 feat(badapple): polygon tracing and rasterisation 2026-11-01 00:21:17 +02:00
arti c7569efb7d feat(badapple): frame extraction primitives 2026-11-01 00:20:35 +02:00
arti 9deada6cef docs: run the r5.01 C toolchain end to end under 2ine
The null-selector fix in the 2ine fork unblocks gspac/gspopt/gspcg, so
document the full pipeline: build 2ine, extract the tools, the four C
stages, absolute placement, and the firmware round-trip.

Record the two gotchas found while testing: gspcl still stops after
gspac because the children exit 139 at teardown, and GSPA 5.01 rejects
b15, so the 32 KB round-trip differs at one SP access (same physical
register, A15/B15).
2026-11-01 00:18:06 +02:00
arti b75048fb87 Merge remote-tracking branch 'wut/master' 2026-11-01 00:06:09 +02:00
arti 3d586106bf plan: Bad Apple 30s vector demo implementation 2026-11-01 00:03:03 +02:00
arti bc3b20113c spec: Bad Apple 30s vector-polygon demo 2026-10-31 23:59:46 +02:00
arti 6febcf00f4 feat(rx): rectangle blits for stretch-1 strips; ring depth 16 -> 32 in the encoder
Two things the 60 fps work needed, and one ceiling it found.

Rectangle blit: a strip frame with stretch 1 has one source row per screen row,
so the source advances by 1280 bits and a group of rows inside one fragment is
contiguous -- one PIXBLT with SPTCH = 1280 copies the group.  Measured on the
tile path, a blit call costs ~0.45 ms in MAME regardless of size (1,200 tile
calls accepted 9 frames where 128 strip calls accepted 73 for the same pixels),
so call count, not word count, is what a frame is charged for.  paint_rect()
takes the stretch-1 frame 9 rows at a time and falls back to the per-row path if
a row straddles a datagram.

Ring depth: vidsrc still said RING_DATAGRAMS = 16 and capped a strip frame at
144 rows, which stopped being true when the receive ring was deepened to 32 for
the dictionary upload.  It is 32 now, the encoder may send up to the header's
255 rows, and the three tests that encoded the old ceiling were updated to it.

The ceiling this found: a picture-as-strips full frame is 29 datagrams, i.e.
1,740 datagrams/s at 60 fps, while the emulated LANCE delivers ~450/s (1,935
datagrams over 4.3 s, measured).  So keyframes cannot be sent as whole strips at
60 fps in MAME whatever the decoder does -- the datagram rate binds first.  The
tile delta (about one datagram a frame) remains the right transport; what costs
is its blit-call count.

Verified: modes rx, draw and tiles still PASS (draw and tiles with their pixel
checkers); 155 unit tests pass.
2026-10-31 23:27:27 +02:00
arti c5526332f0 feat(badapple): stylise the clip into blocks, and let the sender widen its keyframe cadence
Two knobs for the 60 fps attempt, both measured on Bad Apple:

clip_from_video.py --block N averages each NxN block, thresholds the average and
draws the block flat.  The point is destination words: at 60 fps the clip changes
80 tiles/frame unstylised, 54 at 8x8 and 29 at 16x16, against the ~44-tile budget
that MAME's ~3.4 us/word allows in 16.7 ms.  A side effect worth knowing: with
16-pixel blocks every 32x32 tile is one of exactly 2^4 combinations, so the 256
entry dictionary represents the stylised clip with zero error (measured: 16
entries used, "reconstruction 0 bit(s)", PSNR inf, mean delta 141 B against 496 B
for the unstylised clip).

vidsend.py --keyframe-every N exposes the cadence that TileSender already had as
a constructor argument.  A keyframe repaints all 1,280 tiles (~264 ms of MAME CPU
at the measured 0.22 ms/tile), so the default 60-frame cadence costs ~4.4 ms on
average per frame at 60 fps -- a tenth of the budget spent on repairs.  The
13,167-frame stylised clip ships 220 keyframes at the default; 600 measured 6 in
the same window.

Measured on the stylised 60 fps cache (13,167 frames, 16 dictionary entries):
  --fps 60, default cadence   643 of 805 accepted (80%), drops=8
  --fps 45                     484 of 520 accepted, drops=0, sender PASS
  --fps 60, --keyframe-every 600  451 of 519 accepted, drops=5
So the stylised clip roughly doubles acceptance against the same 60 fps send
(40% -> 80%) but MAME still caps the clean rate around 36-45 fps.
2026-10-31 23:12:25 +02:00
opencode.energia.arti.ee 005dfb02d5 docs: how to use the 1990 r5.01 (OS/2) TMS34010 toolchain
The r5.01 tools are a superset of the 1987 DOS set: ANSI C, one driver,
gspopt, a bigger runtime, and 35.8% smaller .text at -o2 on the same
source. Add a practical guide:

* what the package contains and how to extract it
* running it: OS/2 1.3 under 86Box (full toolchain) and 2ine (assembler/
  linker/converter, with the patched fork linked)
* the gspcl option table and verified command lines (-o0/-o1/-o2, -pk,
  -al, -z/-lrts.lib), plus the manual gspac -> gspopt -> gspcg -> gspa
  pipeline and the linker command file
* gotchas: -o semantics, bit-addressed section sizes, CRLF, AUTOEXEC not
  running in the guest, emulated-keyboard limits, one-way assembler
  compatibility
* measured code-size and throughput numbers for both toolchains

Link it from the docs index and the repository layout.
2026-10-31 20:51:06 +00:00
arti 9139322892 perf(tiles): program the blit constants once per frame, not once per tile
Spec 8.3's hoist, and the 60 fps work showed it was the per-tile cost that
mattered, not the per-frame one.

Every tile_blit() call used to save 8 B registers, load B1/B3/B4/B7/B8/B9 and
write 11 I/O registers (PSIZE, CONVSP, CONVDP, PMASK, the CONTROL read-modify-
write and its read-back) -- all constants.  The user's guide marks only SADDR
and DADDR changed by PIXBLT, and the TMS34010 agent's reading of it confirms
B1/B3 survive, so those two are the only ones a per-tile call has to reload.

vram_tile.asm now exports tile_init_() (the constants, once per frame, called
from paint_tiles/paint_delta) and a tile_blit_() that is setf, two pushes, two
register loads, pixblt, two pops, rets.

Measured on the real 140-tile deltas at a 60 fps send, same window, terminal's
own status frames:

  before the hoist   162 accepted
  after the hoist    231 accepted   (+43%)

and the one-tile stream (where tile cost is nil) already runs at 60 fps with
DINT: 600 frames sent at 59.8 fps, 579 accepted (96.5%), drops=0.

Pixel verdicts unchanged: modes tiles, delta and replay all RESULT PASS with
tile_check; 155 unit tests pass.

What the remaining gap is, for the next reader: MAME's PIXBLT moves about
3.4 us per destination word (0.22 ms per 32x32 tile, measured from the accepted
rate) against the unit's measured 0.40 us/word for FILL.  A 140-tile delta is
8,960 words, i.e. ~30 ms of MAME CPU against a 16.7 ms frame -- so 60 fps for
this content is bounded by the emulator's pixel engine, not by the design.
Whether the unit can do it turns on the one number nobody has measured there:
PIXBLT B,XY's per-word rate (spec 10, risk table).
2026-10-31 22:40:42 +02:00
arti 1bb3643144 feat(badapple): measure the 60 fps wire budget before changing any format
Spec 11's first task: decide from the clip, not from assumptions, whether the
raw-tile scheme (no dictionary, tag + bitmap per changed tile) fits a 60 fps
frame.  measure_wire.py extracts at 1280x960/60 fps with the hard threshold the
design requires, diffs each frame on the 40x30 picture grid, and totals the
payload a TILES_DELTA would carry:

    4 + 3*count + 128*m

Full clip, 13,167 frames: mean 16,049 B/frame, p50 14,755, p95 33,353, max
51,523; 22.5% of frames exceed the 21 KB one-frame budget; 87% of changed tiles
are neither solid black nor white, so they carry a 128-byte bitmap (the design
assumed a third); mean 140 changed tiles/frame, max 1,173 at a cut.  Link duty
at 60 fps would be 72% of the measured 1.27 MB/s.

Two consequences for the design, recorded here rather than discovered later:
the raw-tile scheme is borderline rather than comfortable, and the tile churn at
60 fps (140) is nearly the same as at 15 fps (165) -- motion between adjacent
60 fps frames is mostly sub-tile, so sending faster does not divide the wire by
four.
2026-10-31 22:36:34 +02:00
arti 8de6d94e84 feat(rx): mask interrupts for good -- the OS timer was halving the frame rate
Spec 8.9 decides the payload may kill the resident OS; this is the first half of
that, and it is a rate fix as much as a robustness one.

With the OS still ticking, the payload's loop is preempted and the machine
accepts ~15 frames/s **whatever the payload size**: a 5-byte one-tile delta
measured the same as a 140-tile one (195 frames in 13.4 s, MAME at 100% speed,
frames_rx from the payload's own status frames).  That is scheduling, not
decode -- the loop itself runs ~20k iterations/s.

VID_OWN_CPU (default 1) takes one DINT before the main loop and makes vid_eint_()
a no-op, so nothing preempts the LANCE poll.  Measured on the same one-tile
stream:

  OS ticking (before)   15.0 fps accepted, 400 frames in 15.4 s
  DINT at startup       24.0 fps accepted, 385/400 frames, drops=0
  and, paced differently, 31.5 fps with 387/400 -- the sender, not the terminal,
  is now the throttle (its socket blocks once the tap's drain rate is exceeded)

Consequence stated where it is made: the OS's drivers stop being serviced, so
its console and network stack are gone for good.  The payload owns the machine
once it takes the LANCE either way.  The harness reaches the payload through the
debugger, not the OS, so the replay modes are unaffected -- verified: modes
tiles, delta and replay all RESULT PASS with their pixel checkers after the
change.

Set VID_OWN_CPU=0 at build time to reproduce the old burst-EINT behaviour for a
comparison run.
2026-10-31 22:33:07 +02:00
arti d211ff0770 fix(badapple): --no-load must not wait for ICMP; survive a slow readback
Two faults the 60 fps work exposed, both in the delivery tooling:

* `demo.sh --no-load` waited for the terminal to answer ping.  A loaded payload
  owns the LANCE and stops the OS's stack with it, so that gate can never pass
  once the payload is running -- the recovery path documented for "already
  jumped" deadlocked for its full 180 s.  The gate now runs only when this
  invocation is going to load the payload.

* `rdebug_load.py` used a 5 s socket timeout and no retry, so a readback peek
  that timed out on a freshly booted OS failed a load whose pokes had already
  been acknowledged.  Timeout is 15 s (--timeout) and the readback retries once
  before it is believed.

Tests: the retry path is pinned with a fake remote that times out on the first
peek (one timeout, then the successful pass over every chunk) and one that never
answers (it must raise rather than loop).  9 tests in harness/badapple pass.
2026-10-31 22:20:58 +02:00
arti c166c6fdec fix(badapple): refuse a non-cache --tiles path before the jump
demo.sh loaded (and jumped) the payload before it ever looked at --tiles, so a
wrong path -- the frames directory rather than the cache make_tiles.py writes --
only surfaced after the machine had been handed to a payload that never returns,
forcing a reboot to retry.  It now checks dictionary.bin/stream.bin first, names
the frames-vs-cache confusion when the directory holds raw frames, and exits 2
without touching the network.  The README's failure table gains that row and the
--no-load recovery for an instance that has already been jumped.
2026-10-31 22:05:48 +02:00
arti 650ec9a0cd spec: Bad Apple at 60 fps exact — raw tagged tiles, no dictionary 2026-10-31 21:54:48 +02:00
arti d6aa6f46c1 docs(badapple): how the clip data is prepared, with the measured costs
The setup section said 'a clip cache' without saying where a clip comes from,
what it costs, or how to tell the result is sane.  Adds a Data preparation
section: the public sources (and why an upscale is not better), the frame
extraction (3,292 frames, 489 MB, ~12 s, LSB-first), the cache build (~2 min
47 s, four files, 6.1 MB), and the make_tiles summary to compare against --
including that a saturated 256-entry dictionary and 24 dB PSNR are expected, not
a failure.
2026-10-31 21:27:09 +02:00
arti 8f831eaf33 fix(video): model the real unit's VR319 monitor
The harness forced the VR150 monitor switch, so the firmware wrote the OS
screen at 1024 px / 128-byte rows.  VT1200_VRAM_REAL then re-read that
framebuffer at the VR319 pitch, which sheared the OS and wrapped the right
quarter of every row.

Model VR319 (switch 0x60) by default -- the real unit's mode -- so the
firmware itself programs 1280x1024 with a 256-byte row pitch (CONVDP=0x14).
The renderer's pitch follows VT1200_MONITOR, and the destination span now
comes from HEBLNK..HSBLNK instead of x=0, placing the active 1280 px at
x=336 in MAME's 1696-wide raster.

Verified: clean Terminal Manager at frame 600, legible faithful diagnostics
menu and level-1 error screens, tile mode PASS with tile_check 1280/1280
tiles, tablet/mouse pixel-identical.  VT1200_MONITOR=VR150 reproduces the
old geometry; the regression baseline moves 198 -> 199 (2 PCs) and is
recorded in harness/mame/README.md.
2026-10-31 20:53:51 +02:00
arti eabc3051be feat(badapple): the demo as a tool -- rdebug delivery, clip builder, one command
A new folder so the demo can be reproduced instead of re-derived: the delivery
and the clip packing were living in throwaway scripts under /tmp, and the only
written route to a real clip was an ffmpeg recipe that packs MSB first -- the
order that mirrors every eight pixels on a display.

  harness/badapple/rdebug_load.py    image -> terminal RAM over the firmware's
                                     own remote-debug service (TCP 771), then
                                     jump to the entry from vidstream.env
  harness/badapple/clip_from_video.py video -> LSB-first 1-bpp frames for
                                     make_tiles.py --clip
  harness/badapple/demo.sh           ICMP gate, load, send; one command
  harness/badapple/README.md         setup, run in MAME, run on real hardware,
                                     what success looks like, failure modes

The loader records the two protocol facts that cost time to find: a 512-byte
poke chunk (1024 is mis-parsed as a following opcode: "Invalid command
0xA5A5A5A5"), and that nothing can be probed after the jump, so the image is
read back BEFORE jumping and the run is observed from the machine afterwards.

Tests cover what a wrong assumption would silently corrupt -- chunk arithmetic,
the linkage the entry point is read from, and pixel 0 landing on bit 0 -- and
the tool was exercised end to end against MAME: demo.sh loaded the payload and
the terminal reported DICT_BYTES=32768, DICT_BAD=0, RX_OK=28, TILES_DROP=0
while the sender saw frames_rx=129, drops=0.

153 tests pass (7 new).
2026-10-31 20:20:35 +02:00
arti f7490d2e09 feat(opencode): add tms34010 agent with the whole User's Guide preloaded
Subagent/primary agent whose system prompt inlines docs/TMS34010-users-guide.txt
(SPVU001A, 1.75 MB, ~361k tokens) via {file:} substitution, so whole-book
questions -- cross-chapter synthesis, complete opcode lists, encoding and
timing tables -- are answered from the text in one pass.

Read-only (edit and subagent denied) and keeps read/shell, so it can render a
page with pdftoppm when the OCR text layer is mangled: section 12 prints XY as
XV and checkmarks as 'J'.
2026-10-31 20:03:06 +02:00
arti 9af7b4fb8d fix(rx): release descriptors individually instead of flipping the whole ring
The receive loop re-armed all 16 descriptors into the other slot after every
completed frame.  During a burst that re-pointed descriptors the chip had
ALREADY filled but the consumer had not read, so those frames were lost without
a trace: the LANCE DMA log shows all 23 dictionary datagrams written by the
chip, 1.132 ms apart (the MTU/bandwidth poll cadence), while the payload's own
counters accounted for 4 (DICT_BYTES=1448, dict_bad=3).  Nothing was dropped by
MAME; the ring was being rewritten under the frames it held.

rx_switch()/rx_armed are gone.  A descriptor is held (rx_hold) while its
datagram belongs to the frame being assembled, and rx_release() hands each one
back -- into the other slot -- only once that frame has been parsed or
abandoned.  Nothing the chip delivered is ever re-pointed unread.

Measured, same zero-gap 23-datagram dictionary upload:
  before                      dgram=4   DICT_BYTES=1448   dict_bad=3
  release fix, 16 descriptors dgram=18  DICT_BYTES=24616  dict_bad=0
  plus a 32-descriptor ring   dgram=23  DICT_BYTES=32768  dict_bad=0

The ring is now 32 descriptors because 23 datagrams is the burst the protocol
actually defines (a whole 32 KB dictionary at MAX_BODY 1448), and 16 could not
hold it while the consumer copied.  SLOT_BYTES grows to 49152 to hold the ring;
POOL_BYTES does NOT grow -- the 160 KB "framebuffer reserve" was unused slack
(the decoder paints straight from the fragments), so the extra 24,576 B per slot
comes out of it and the LANCE block keeps its fixed 0x3C000 tail.

RX_OK is now published after rx_release(), not inside the dispatch.  It is the
"frame done" signal a reader polls, and it used to become visible while
descriptors were still being rewritten: ring mode caught a descriptor holding
slot B's high byte with slot A's low word.  A reader that sees RX_OK move now
sees a painted picture and a consistent ring.

vidstream.lua's ring mode follows: it asserts the invariant that replaced the
flip -- the consumed descriptors move to the other slot, the untouched ones stay
-- instead of "all 16 in slot B".

Evidence: 146 unit tests pass; modes rx, draw, ring, tiles, delta and replay all
PASS with their pixel checkers (draw 1024/1024 rows, tiles and replay via
tile_check).  End to end over rdebug with the STOCK sender (no pacing): the
dictionary burst completes (DICT_READY=1, DICT_BYTES=32768, dict_bad=0) and
RX_OK=100 frames paint, TILES_DROP=0.
2026-10-31 19:45:34 +02:00
arti 5af949b338 fix(tiles): pack LSB first, the terminal's scanout order; render the real layout
The tile pipeline packed every row MSB first: pixel i -> bit 7 - i % 8. The
encoder and tile_check.py shared that convention, so every MAME replay check
passed while a display showed each group of eight pixels mirrored.

Evidence, from the real clip's own replay dump (vram-replay-200.bin, frame 200
of Bad Apple, 1,228,800 pixels):

  decoded MSB first   1,215 mismatched  <- what the pipeline produced
  decoded LSB first   3,113 mismatched  <- the mirrored picture, a sawtooth

and both the real unit and MAME decode LSB first: dwt/screenshot.py --real
("LSB-first bits", measured on live VRAM) and vt1200.cpp's
(vram[bit >> 4] >> (bit & 15)) & 1.  A zoomed edge of that dump showed the
comb of triangles; after this change the same edge is a smooth curve.

vidsrc.Rows.pack_row now sets bit i % 8 of byte i // 8.  The test that pins it
was written first and watched fail: it decodes the packed row with the
hardware's rule, which no MSB-first packer can pass.  The two older tests that
asserted MSB first were rewritten -- they were asserting the bug -- and the
synthetic clip now packs through a bit-reversal table, so the committed cache
is regenerated (PSNR 29.47 -> 31.30 dB).  The layout claims in tilecost.py,
vram_tile.asm and make_tiles.load_clip are corrected so the clip format states
the order instead of leaving it to whoever writes the frames.

Separately, MAME's renderer reads 128-byte rows while a payload targeting the
real unit writes 256-byte rows (vram_tile.asm DPTCH=0x800; c-hello/README.md:
"a payload writing at row*2048 bits lands on scanline row").  VT1200_VRAM_REAL=1
now draws the framebuffer at 2048 bits / 1280 px from x=0.  Off by default: the
Level-1 error text is only legible at the derived 128-byte pitch, and that is
what the faithful boot needs.  The branch only touches the bitmap, so the
regression baseline is unmoved (PROG_FRAMES=200 -> PC=3FDCE400, samples=198,
3 distinct PCs), and the default frame-3000 render of the Terminal Manager is
unchanged.

Verified end to end on the 3,292-frame Bad Apple clip: RX_OK=3196, TILES_DROP=0,
3292/3292 frames dumped and matched by tile_check.py, and the dumps decode to
1,379 / 4,125 mismatched pixels of 1,228,800 (quantisation) under the hardware
rule versus 3,101 / 9,793 under MSB.  MAME's own frame dumps with
VT1200_VRAM_REAL=1 show the clip unsheared.

Revert note for the driver: delete the m_render_real branch and its getenv and
vt1200.cpp returns to the measured 128-byte renderer byte for byte.  No second
reader reviewed that citation; it is quoted in the comment for the next one.
2026-10-31 18:06:14 +02:00
opencode.energia.arti.ee 173f3bebda plan: record the outstanding unit/soak work as the only remainder 2026-10-31 08:56:33 +00:00
opencode.energia.arti.ee 10bdd1a73c spec: delta wire form is grouped; status cadence has out-of-cadence reports 2026-10-31 03:37:21 +00:00
opencode.energia.arti.ee 75f4d35733 docs: delta wire form and status cadence; require four status frames 2026-10-31 03:37:16 +00:00
opencode.energia.arti.ee a84f5f2811 fix(tiles): early status on dictionary failure, decode-time field, chunk bound 2026-10-31 03:35:14 +00:00
opencode.energia.arti.ee b995c2caaa docs: correct the tile-stream wire overhead and test counts 2026-10-31 03:11:22 +00:00
opencode.energia.arti.ee 79e27680c9 feat(tiles): bad apple tile stream end to end 2026-10-31 03:06:04 +00:00
opencode.energia.arti.ee b331b90c39 test(tiles): multi-datagram delta case and stricter block diff 2026-10-31 02:41:04 +00:00
opencode.energia.arti.ee 8f8454461d feat(tiles): delta frames and periodic keyframes 2026-10-31 02:38:40 +00:00
opencode.energia.arti.ee 2fa232ba56 feat(tiles): 32x32 PIXBLT B,XY tile blit and full-frame paint 2026-10-31 02:20:50 +00:00
opencode.energia.arti.ee 0b726fda3a feat(tiles): dictionary storage and kind dispatch in the payload
The payload assembles KIND_DICT_CHUNK frames into a 32 KB dictionary at
POOL_PTR + 2 * SLOT_BYTES and learns the tile kinds:

* frame_parse.h gains FP_KIND_TILES_FULL (2), FP_KIND_TILES_DELTA (3) and
  FP_KIND_DICT_CHUNK (4), exactly frame.py's KINDS table, plus the marker
  cells DICT_READY/DICT_BYTES/DICT_BAD and TILES_DROP.  A host test parses
  the header and compares it with KINDS so the two cannot drift apart.
* dict_chunk() enforces an ascending, contiguous offset (no gap, overlap,
  duplicate or reorder; a mid-entry offset only as a continuation), a chunk
  that does not overrun the last entry, and no chunk after DICT_READY.  A
  refused chunk leaves the prefix alone.  Readiness is exactly 32768 bytes
  (256 whole entries), so a short upload never becomes ready.
* The dispatcher routes STRIP to the blit, DICT_CHUNK to the assembler, and
  refuses TILES_FULL/TILES_DELTA without painting, counting them in
  TILES_DROP (Task 4 adds the tile blit).
* A compile-time assertion refuses a POOL_BYTES that cannot hold the
  dictionary; the 1987 preprocessor does not fold macro arithmetic in #if,
  so it is a defined array whose negative length fails the build.
* VT1200_MODE=dict injects both tile kinds before the dictionary, 23
  contiguous chunks with a duplicate offset, a gap and an overrunning tail
  mixed in, and reads the stored 32 KB back byte for byte.
2026-10-31 01:55:06 +00:00
opencode.energia.arti.ee 84349d6da3 feat(tiles): chunked dictionary upload protocol 2026-10-31 01:35:22 +00:00
opencode.energia.arti.ee d3b2e59be6 spec: dictionary entry stride is 1024 bits, not 128 2026-10-31 01:33:42 +00:00
opencode.energia.arti.ee a6e4ac44ad feat(tiles): deterministic dictionary builder and quantiser 2026-10-31 01:32:02 +00:00
opencode.energia.arti.ee cd4dbe2317 plan: tile codec (scheme C) — dictionary, tile blit, deltas, bad apple 2026-10-31 01:28:05 +00:00
opencode.energia.arti.ee 22c539bf74 fix(stream): ring ownership window, FCS-error accounting, unit loader procedure 2026-10-31 01:25:14 +00:00
opencode.energia.arti.ee 9917c5659a fix(stream): delivery metric normalised to the status frame counter 2026-10-31 01:08:54 +00:00
opencode.energia.arti.ee 49a943583c feat(stream): real rx ring over chained descriptors; unit measurement script 2026-10-31 01:03:59 +00:00
opencode.energia.arti.ee 7eb7b61234 plan: task 7 carries the real rx ring and the unit measurement 2026-10-31 00:31:12 +00:00
opencode.energia.arti.ee 53b1580abd feat(stream): paced sender and terminal status frames 2026-10-31 00:26:26 +00:00
opencode.energia.arti.ee 5cd708460a plan: align the remaining CRC text with R14 (header-only) 2026-10-31 00:01:57 +00:00
opencode.energia.arti.ee 16148d0d24 fix(stream): header-only frame CRC; draw-mode evidence guards
Ruling R14: the frame CRC now covers header bytes 0..11 only.  A
payload-wide CRC cost the terminal ~139 instructions per payload byte
(the parser, not the wire or the blitter, was the frame-rate ceiling),
while every datagram already carries an Ethernet FCS the LANCE checks
and the length bound catches truncation.  frame.py, fp_parse and the
harness's own frame signer follow the new rule; a corrupted payload
byte is now ACCEPTED and a corrupted header byte rejected, stated
explicitly in both test suites, and the C golden vectors were
regenerated from frame.py.

Measured after: RX_READY_A write -> first RX_OK read is 1 emulated
frame for a 20,480-byte frame (84 before), which is the frame hook's
resolution.

Ruling R15: draw mode deletes its dump files before the run (wrapper
and Lua) so a failed run cannot be judged on the previous screen; a
wait that hits its cap reports "timed out waiting for the frame" and
the checker reports it as a timeout, not as pixel mismatches; the
checker asserts the VRAM canary word is 0xA5C3, so a payload that
stopped arming it fails instead of passing quietly.

rx mode now corrupts a header byte in slot B, because a payload flip is
accepted by design under R14.
2026-10-30 23:59:20 +00:00
opencode.energia.arti.ee 5e9b2e9c57 spec+plan: header-only frame CRC (the body is covered by the Ethernet FCS) 2026-10-30 23:50:35 +00:00
opencode.energia.arti.ee 45110270a7 feat(stream): row-strip blit with SPTCH=0 vertical stretch
One PIXBLT B,XY per source row paints the whole screen: DYDX names
`stretch` rows and SPTCH = 0 makes each of them read the same packed
source row, so the vertical stretch costs no extra CPU work.

vram_b.asm carries the primitive (PSIZE=1, CONVSP/CONVDP=0x14,
PMASK=0, OFFSET=0x04000000, DPTCH=0x800, raster op 0) plus DINT/EINT
thunks; vidstream.c masks interrupts for the burst (spec 8.9), refuses
geometry the screen cannot hold, and counts an OS repaint through a
sentinel word in the unused right edge of the last screen row
(OS_TOUCH, 0x01810220).

MAME mode draw injects a 128-row frame (stretch 8 = 1024 screen rows,
ruling R3) and dumps the screen; vidstream_check.py compares all 1024
rows against the bytes actually delivered.  RED: 0/1024 rows matched.
GREEN: 1024/1024, RX_OK=1, OS_TOUCH=0.

Fixes a latent harness bug found on the way: `bpclear <address>` takes
a breakpoint INDEX, so the idle-loop breakpoint was never cleared; once
a burst masked interrupts long enough, its stale action fired again and
re-entered main (a second malloc, counters back to 0).
2026-10-30 23:43:02 +00:00
opencode.energia.arti.ee 487bb1789a feat(stream): frame parser shared by host tests and payload 2026-10-30 23:16:48 +00:00
opencode.energia.arti.ee 213083f2e0 plan: task 4 verified by host unit tests (the driver has no network interface) 2026-10-30 22:52:46 +00:00
opencode.energia.arti.ee 18b496146b fix(stream): absolute payload path; pool must be word-aligned and below the payload 2026-10-30 22:47:02 +00:00
opencode.energia.arti.ee 4f6b0bf1a6 feat(stream): payload skeleton with one startup malloc 2026-10-30 22:40:23 +00:00
opencode.energia.arti.ee 1a68630534 feat(stream): 1-bpp row packing and strip frame encoder 2026-10-30 22:21:24 +00:00
opencode.energia.arti.ee 4122ebe616 plan: task 2 must pass the strip payload into the CRC 2026-10-30 22:18:37 +00:00