Files
vt1200/harness/udpstream/frame_parse.c
T
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

119 lines
3.6 KiB
C

/*
* frame_parse.c -- receive-side frame check for the VT1200 video stream.
*
* fp_parse() answers one question: is the block of bytes the transport just
* delivered a complete, uncorrupted frame of the format frame.py writes? It
* checks, in this order, the length bound, the magic and version, and finally
* the CRC over the 12 header bytes.
*
* The CRC is CCITT-FALSE (polynomial 0x1021, initial value 0xFFFF, no input or
* output reflection, no final xor), the same one frame.py implements with a
* 256-entry table. It covers header bytes 0..11 ONLY (ruling R14): the bytes
* at offset 12 are the CRC itself and the payload is never walked. A
* payload-wide CRC cost ~139 instructions per byte on this CPU and made the
* parser the frame-rate ceiling; every datagram already carries an Ethernet
* FCS the LANCE validates, and the length bound below catches truncation.
*
* So a corrupted PAYLOAD byte does not make fp_parse() reject a frame -- the
* chip's FCS is the check for that -- while a corrupted HEADER byte does.
*
* This file is compiled twice: by the 1987 TI C compiler inside the payload
* build (build-stream.sh stages it as FRMPSR.C) and by the host cc for
* tests/test_frame_parse.c. So it is K&R C -- no prototypes in definitions,
* no declarations after statements, no '//', no 'const' -- and it must not
* assume a 32-bit int beyond the fields it masks.
*/
#include "frmpsr.h"
/*
* One byte through the CRC, bit by bit. frame.py builds a table from the
* identical loop; that is the whole reason this is written the long way, so
* the two can be compared line for line.
*/
static unsigned int fp_crc_byte(crc, byte)
unsigned int crc;
unsigned int byte;
{
int i;
crc = (crc ^ ((byte & 0xFF) << 8)) & 0xFFFF;
for (i = 0; i < 8; i++) {
if (crc & 0x8000) {
crc = ((crc << 1) ^ 0x1021) & 0xFFFF;
} else {
crc = (crc << 1) & 0xFFFF;
}
}
return crc;
}
/* Fold len bytes at buf into crc. */
static unsigned int fp_crc_add(crc, buf, len)
unsigned int crc;
unsigned char *buf;
unsigned int len;
{
unsigned int i;
for (i = 0; i < len; i++) {
crc = fp_crc_byte(crc, buf[i]);
}
return crc;
}
/*
* int fp_parse(unsigned char *buf, unsigned int len, fp_header *out)
*
* See frame_parse.h for the contract. A NULL buffer or header is a rejection
* rather than a crash: the payload calls this with a pointer it published
* itself, and a zero there means "no slot yet".
*
* The CRC covers header bytes 0..11 only, so the body is never touched: a
* 20 KB frame costs the same as a 1 KB one (ruling R14).
*/
int fp_parse(buf, len, out)
unsigned char *buf;
unsigned int len;
fp_header *out;
{
unsigned int plen, stored, crc;
if (buf == 0 || out == 0) {
return 0;
}
if (len < FP_HEADER_SIZE) {
return 0;
}
if (buf[0] != FP_MAGIC0 || buf[1] != FP_MAGIC1) {
return 0;
}
if (buf[2] != FP_VERSION) {
return 0;
}
plen = (unsigned int) buf[6] | ((unsigned int) buf[7] << 8);
/*
* Subtracting rather than adding: plen can be 0xFFFF, and
* FP_HEADER_SIZE + plen would still fit a 32-bit unsigned int but not a
* 16-bit one. len is known to be at least FP_HEADER_SIZE here.
*/
if (len - FP_HEADER_SIZE < plen) {
return 0;
}
stored = ((unsigned int) buf[FP_CRC_OFF + 1] << 8)
| (unsigned int) buf[FP_CRC_OFF];
crc = fp_crc_add(0xFFFF, buf, FP_PREFIX_SIZE);
if (crc != stored) {
return 0;
}
out->kind = (unsigned int) buf[3];
out->frame_no = (unsigned int) buf[4] | ((unsigned int) buf[5] << 8);
out->payload_len = plen;
out->rows = (unsigned int) buf[8];
out->stretch = (unsigned int) buf[9];
out->payload_off = FP_HEADER_SIZE;
return 1;
}