forked from arti/vt1200
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).
310 lines
11 KiB
C
310 lines
11 KiB
C
/*
|
|
* vidstream.c -- terminal side of the VT1200 video stream.
|
|
*
|
|
* Task 3 claimed the working set with ONE malloc at startup and published
|
|
* where it landed. Task 4 added the receive side: two 24 KB frame slots at the
|
|
* head of that pool, and a poll loop that hands each completed slot to
|
|
* fp_parse() (frame_parse.c, shared with the host unit test) and counts what
|
|
* it accepted. Task 5 blits an accepted strip frame to the screen: one
|
|
* PIXBLT B,XY per source row, repeated down the row `stretch` times by
|
|
* SPTCH=0 (vram_b.asm).
|
|
*
|
|
* Pool layout inside the single allocation (POOL_BYTES = 249856 = 244 KB):
|
|
*
|
|
* +0x00000 frame slot A 24576 bytes
|
|
* +0x06000 frame slot B 24576 bytes
|
|
* +0x0C000 framebuffer 163840 bytes (scheme A, Task 5)
|
|
* +0x34000 dictionary 32768 bytes
|
|
* +0x3C000 slack 4096 bytes
|
|
*
|
|
* A slot holds one complete frame -- 16-byte header and payload -- exactly as
|
|
* it arrives on the wire, so the bytes fp_parse() validates are the bytes the
|
|
* blit reads. Neither slot is ever split across a ring wrap: the two ring
|
|
* entries' descriptors are re-armed per slot at frame start.
|
|
*
|
|
* Screen geometry. VRAM is at bit 0x04000000 and a screen row is 0x800 bits
|
|
* (256 bytes), the real unit's pitch (harness/c-hello/README.md). MAME's own
|
|
* renderer decodes the same memory at 128 bytes a row; that difference is a
|
|
* property of the harness, so the draw test compares VRAM as memory, not the
|
|
* rendered picture.
|
|
*
|
|
* OS repaint canary (spec 8.9). The blit covers screen rows 0..1023 and
|
|
* columns 0..1279 only. One 16-bit sentinel word sits in the unused right
|
|
* edge of the last screen row (row 1023, column 1280), outside every pixel the
|
|
* blit writes. If something else -- the resident OS -- paints over us it also
|
|
* disturbs that corner; OS_TOUCH counts how often that was seen. This
|
|
* decoder has no partial update: an accepted frame repaints the whole screen
|
|
* anyway, so detecting the touch and repainting are the same act here, and
|
|
* OS_TOUCH is the record.
|
|
*
|
|
* Marker layout: see the table in frame_parse.h. Markers are 32-bit cells on
|
|
* the byte-aligned page at bit 0x01810000, 0x20 bits (4 bytes) apart. The
|
|
* design plan names them 0x01810020, 0x01810024, ... but the TMS34010 is
|
|
* bit-addressed, so 0x01810024 is four BITS after 0x01810020 -- inside the
|
|
* same cell. The plan's numbers are byte offsets into the page; the rule is
|
|
*
|
|
* marker named 0x018100NN -> bit 0x01810000 + NN * 8
|
|
*
|
|
* (POOL_PTR = 0x01810100, ...). Task 5's OS_TOUCH is 0x01810220 (ruling R12),
|
|
* i.e. cell 0x44 -- free between the RX_* counters and the injection cells.
|
|
* vidstream.lua uses the same rule.
|
|
*
|
|
* K&R C only: no prototypes, no '//', declarations before statements.
|
|
*/
|
|
#include "romapi.h"
|
|
#include "poolcfg.h" /* POOL_BYTES, written by build-stream.sh */
|
|
#include "frmpsr.h" /* frame_parse.h under an 8.3 name; see that file */
|
|
|
|
#define HEAP_FREE 0x1E8BA0 /* firmware free-byte counter, IN BITS */
|
|
#define MARK 0x01A00000 /* .text must end before this bit address */
|
|
|
|
#define STATIC_BYTES 249856 /* the designed working set, 244 KB */
|
|
|
|
/* Screen memory and the strip geometry (see the header comment). */
|
|
#define VRAM_BASE 0x04000000 /* video RAM, bit address */
|
|
#define VRAM_ROW 0x800 /* 2048 bits = 256 bytes a screen row */
|
|
#define SCREEN_ROWS 1024 /* rows in the framebuffer */
|
|
#define ROW_PIXELS 1280 /* a strip row is 1280 pixels */
|
|
#define ROW_BYTES 160 /* 1280 pixels, 1 bpp */
|
|
|
|
/*
|
|
* The sentinel word: last screen row, first column past the 1280 the blit
|
|
* writes. Outside the blitted area by construction, inside mapped VRAM (the
|
|
* next 256-byte row would be bit 0x06000000, which the driver maps as the NVR,
|
|
* not as video memory).
|
|
*/
|
|
#define SENT_ADDR (VRAM_BASE + (SCREEN_ROWS - 1) * VRAM_ROW + ROW_PIXELS)
|
|
#define SENT_VALUE 0xA5C3
|
|
|
|
/*
|
|
* The two frame slots. SLOT_BYTES is the plan's 24 KB: a 128-row strip frame
|
|
* (128 x 160 B + 16 B header = 20,496 B = 15 packets) fits, and so does any
|
|
* smaller strip. Slot addresses are bit addresses, because that is what a
|
|
* pointer holds in this toolchain (see harness/c-hello/README.md: "pointers
|
|
* are bit addresses").
|
|
*/
|
|
#define SLOT_BYTES 24576
|
|
|
|
/*
|
|
* Static fallback. pool_static is global so the linker map names it and the
|
|
* MAME check can assert the exact address a failed malloc falls back to.
|
|
* stack_cushion and the five VRAM words below sit between it and ENTRY.ASM's
|
|
* _cstack: TMS34010 pushes grow downward, so the 4 KB stack spills into those
|
|
* 4 KB, never into the pool.
|
|
*/
|
|
unsigned int pool_static[STATIC_BYTES / 4];
|
|
unsigned int stack_cushion[1024];
|
|
|
|
/*
|
|
* VRAM.ASM's externals. vram.asm is linked into every payload in this
|
|
* directory; the C side owns the values. vram_b.asm (PIXBLT B,XY) shares
|
|
* blit_src/blit_daddr/blit_dydx and ignores blit_sptch, because its SPTCH is
|
|
* always 0 -- that is what makes one source row fill `stretch` screen rows.
|
|
*/
|
|
unsigned int vram_pattern;
|
|
unsigned int blit_src, blit_sptch, blit_daddr, blit_dydx;
|
|
|
|
extern void vram_blit_b_(); /* vram_b.asm: PIXBLT B,XY, SPTCH = 0 */
|
|
extern void vid_dint_(); /* vram_b.asm: DINT */
|
|
extern void vid_eint_(); /* vram_b.asm: EINT */
|
|
|
|
/*
|
|
* The firmware's free-space counter is a 32-bit count IN BITS at
|
|
* 0x1E8BA0 (dwt/notes/HEAP.md). POOL_FREE reports bytes, because the
|
|
* marker is for humans and the check compares against POOL_BYTES.
|
|
*/
|
|
static unsigned int heap_bytes()
|
|
{
|
|
return *(unsigned int *)HEAP_FREE / 8;
|
|
}
|
|
|
|
/*
|
|
* blit_row(src, daddr_xy, dydx)
|
|
*
|
|
* One PIXBLT B,XY. The three values are published where vram_b.asm reads
|
|
* them (see the note there on the calling form; the assembly entry point takes
|
|
* no arguments). `src` is the packed 1-bpp source row as a bit address,
|
|
* `daddr_xy` is the screen position as (y << 16) | x, and `dydx` is the
|
|
* rectangle as (screen rows << 16) | pixels. The primitive always sets
|
|
* SPTCH = 0, so the "screen rows" dimension reads the same source row that
|
|
* many times: that is the vertical stretch.
|
|
*/
|
|
static void blit_row(src, daddr_xy, dydx)
|
|
unsigned int src;
|
|
unsigned int daddr_xy;
|
|
unsigned int dydx;
|
|
{
|
|
blit_src = src;
|
|
blit_daddr = daddr_xy;
|
|
blit_dydx = dydx;
|
|
vram_blit_b_();
|
|
}
|
|
|
|
/*
|
|
* paint_frame(slot, h)
|
|
*
|
|
* Draw one accepted strip frame: source row r goes down screen rows
|
|
* r*stretch .. r*stretch+stretch-1. A frame the screen cannot hold is not
|
|
* drawn: zero rows or zero stretch, more than SCREEN_ROWS screen rows, or a
|
|
* payload shorter than the rows it claims (fp_parse() has already proved that
|
|
* payload_len bytes arrived, so that bound is real). A refused frame never
|
|
* touches VRAM, so a malformed header cannot paint garbage over a good one.
|
|
*
|
|
* The whole burst runs with interrupts masked (spec 8.9) so the resident OS
|
|
* cannot repaint between two of our source rows and tear the picture.
|
|
*/
|
|
static void paint_frame(slot, h)
|
|
unsigned int slot;
|
|
fp_header *h;
|
|
{
|
|
unsigned int r, rows, stretch, src, sent;
|
|
|
|
rows = h->rows;
|
|
stretch = h->stretch;
|
|
if (h->kind != FP_KIND_STRIP || rows == 0 || stretch == 0) {
|
|
return;
|
|
}
|
|
if (rows * stretch > SCREEN_ROWS) {
|
|
return;
|
|
}
|
|
if (rows * ROW_BYTES > h->payload_len) {
|
|
return;
|
|
}
|
|
|
|
/* The canary word lives outside every pixel this frame writes. If it
|
|
* is not ours any more, something else painted VRAM: count it and put
|
|
* it back. There is no partial update here -- this frame repaints the
|
|
* whole screen anyway -- so OS_TOUCH is the record, not a trigger. */
|
|
sent = (unsigned int)*(unsigned short *)SENT_ADDR;
|
|
if (sent != SENT_VALUE) {
|
|
*(unsigned int *)OS_TOUCH = *(unsigned int *)OS_TOUCH + 1;
|
|
*(unsigned short *)SENT_ADDR = SENT_VALUE;
|
|
}
|
|
|
|
vid_dint_();
|
|
src = slot + h->payload_off * 8;
|
|
for (r = 0; r < rows; r++) {
|
|
blit_row(src + r * ROW_PIXELS,
|
|
(r * stretch) << 16,
|
|
(stretch << 16) | ROW_PIXELS);
|
|
}
|
|
vid_eint_();
|
|
}
|
|
|
|
/*
|
|
* Handle one slot if the receive path has raised its ready flag.
|
|
*
|
|
* In the real transport the LANCE receive interrupt fills the slot and sets
|
|
* the flag; MAME cannot hand this emulated LANCE an Ethernet frame (the
|
|
* driver attaches no network interface -- ruling R11), so in the harness
|
|
* vidstream.lua writes the slot and sets the flag instead. The payload does
|
|
* not care which of the two did it.
|
|
*
|
|
* `len` is the number of bytes the transport actually delivered. fp_parse()
|
|
* rejects a frame whose payload_len wants more than that, which is the rule
|
|
* that makes payload_len plus the received length authoritative rather than a
|
|
* descriptor field.
|
|
*/
|
|
static void rx_poll(slot)
|
|
unsigned int slot;
|
|
{
|
|
unsigned int readyp, lenp, basep, base, len, ready;
|
|
fp_header h;
|
|
int ok;
|
|
|
|
if (slot == 0) {
|
|
readyp = RX_READY_A;
|
|
lenp = RX_LEN_A;
|
|
basep = RX_SLOT_A;
|
|
} else {
|
|
readyp = RX_READY_B;
|
|
lenp = RX_LEN_B;
|
|
basep = RX_SLOT_B;
|
|
}
|
|
|
|
ready = *(unsigned int *)readyp;
|
|
if (ready == 0) {
|
|
return;
|
|
}
|
|
|
|
base = *(unsigned int *)basep;
|
|
len = *(unsigned int *)lenp;
|
|
|
|
ok = 0;
|
|
if (base != 0) {
|
|
ok = fp_parse((unsigned char *)base, len, &h);
|
|
}
|
|
|
|
if (ok) {
|
|
*(unsigned int *)RX_OK = *(unsigned int *)RX_OK + 1;
|
|
*(unsigned int *)RX_LAST_LEN = h.payload_len;
|
|
*(unsigned int *)RX_SLOT = slot;
|
|
/* Only an accepted frame reaches VRAM: the bytes just validated
|
|
* are the bytes the blit reads, and a corrupt frame can never
|
|
* paint over a good one. */
|
|
paint_frame(base, &h);
|
|
} else {
|
|
*(unsigned int *)RX_BAD = *(unsigned int *)RX_BAD + 1;
|
|
}
|
|
|
|
/* Release the slot last: the checks read the counters, and a slot
|
|
* freed before its counter moved would look like a dropped frame. */
|
|
*(unsigned int *)readyp = 0;
|
|
}
|
|
|
|
int main()
|
|
{
|
|
unsigned int before, after, ptr;
|
|
|
|
*(unsigned int *)VID_ALIVE = 0;
|
|
*(unsigned int *)POOL_PTR = 0;
|
|
*(unsigned int *)POOL_FREEB = 0;
|
|
*(unsigned int *)POOL_FREEA = 0;
|
|
|
|
/* Receive record: counters and the two slots are cleared before anything
|
|
* is published, so a reader never sees a stale ready flag or a slot
|
|
* address that has no allocation behind it. */
|
|
*(unsigned int *)RX_OK = 0;
|
|
*(unsigned int *)RX_BAD = 0;
|
|
*(unsigned int *)RX_LAST_LEN = 0;
|
|
*(unsigned int *)RX_SLOT = 0;
|
|
*(unsigned int *)RX_SLOT_A = 0;
|
|
*(unsigned int *)RX_SLOT_B = 0;
|
|
*(unsigned int *)RX_READY_A = 0;
|
|
*(unsigned int *)RX_LEN_A = 0;
|
|
*(unsigned int *)RX_READY_B = 0;
|
|
*(unsigned int *)RX_LEN_B = 0;
|
|
*(unsigned int *)OS_TOUCH = 0;
|
|
|
|
/* Arm the VRAM canary before any frame is accepted, so a repaint
|
|
* between now and the first blit is counted rather than mistaken for
|
|
* our own last frame. */
|
|
*(unsigned short *)SENT_ADDR = SENT_VALUE;
|
|
|
|
before = heap_bytes();
|
|
ptr = malloc_(POOL_BYTES); /* malloc_ takes BYTES, returns BITS */
|
|
after = heap_bytes();
|
|
|
|
if (ptr == 0) {
|
|
/* The fallback is the 244 KB static pool by definition; a larger
|
|
* request still reports the failure through the low-bit flag. */
|
|
ptr = (unsigned int)pool_static;
|
|
*(unsigned int *)POOL_FREEB = before | 1;
|
|
} else {
|
|
*(unsigned int *)POOL_FREEB = before;
|
|
}
|
|
*(unsigned int *)POOL_FREEA = after;
|
|
|
|
/* Slot B follows slot A in the same pool. Both are published before
|
|
* POOL_PTR so the harness can find them from either one. */
|
|
*(unsigned int *)RX_SLOT_A = ptr;
|
|
*(unsigned int *)RX_SLOT_B = ptr + SLOT_BYTES * 8;
|
|
*(unsigned int *)POOL_PTR = ptr; /* publish last: a complete record */
|
|
|
|
for (;;) {
|
|
*(unsigned int *)VID_ALIVE = *(unsigned int *)VID_ALIVE + 1;
|
|
rx_poll(0);
|
|
rx_poll(1);
|
|
}
|
|
}
|