From 4f6b0bf1a687e00028e09e2446979a1e5ce2c503 Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Fri, 30 Oct 2026 22:40:23 +0000 Subject: [PATCH] feat(stream): payload skeleton with one startup malloc --- harness/mame/run-vidstream.sh | 75 ++++++++++ harness/mame/vidstream.lua | 228 ++++++++++++++++++++++++++++++ harness/udpstream/build-stream.sh | 178 +++++++++++++++++++++++ harness/udpstream/vidstream.c | 90 ++++++++++++ 4 files changed, 571 insertions(+) create mode 100755 harness/mame/run-vidstream.sh create mode 100644 harness/mame/vidstream.lua create mode 100755 harness/udpstream/build-stream.sh create mode 100644 harness/udpstream/vidstream.c diff --git a/harness/mame/run-vidstream.sh b/harness/mame/run-vidstream.sh new file mode 100755 index 0000000..2cfe86f --- /dev/null +++ b/harness/mame/run-vidstream.sh @@ -0,0 +1,75 @@ +#!/bin/sh +# run-vidstream.sh -- build the streaming payload and run its MAME check. +# +# harness/mame/run-vidstream.sh # build, then mode alloc +# BENCH=0 harness/mame/run-vidstream.sh # use the existing .bin +# VT1200_MODE=rx harness/mame/run-vidstream.sh # Task 4's mode +# STREAM_POOL_BYTES=0x7FFF000 harness/mame/run-vidstream.sh # force fallback +# +# The run uses VT1200_SKIP_LEVEL1=1: the payload's heap is only initialised in +# the OS phase. In the diagnostics menu the free counter (0x1E8BA0) and the +# free-list head (0x1E8AC0) both read 0, so the malloc success path cannot be +# tested there. The skip is a documented divergence (harness/mame/README.md). +# +# Exit status: 0 only if vidstream.lua printed "VID: RESULT PASS". +set -eu + +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "$HERE/../.." && pwd) +MAME=${MAME:-/home/arti/code/k-space/mame0289/vt1200} +BENCH=${BENCH:-1} +MODE=${VT1200_MODE:-alloc} +LOGDIR=${LOGDIR:-/tmp/opencode} +LOG="$LOGDIR/vidstream-mame.log" + +if [ ! -x "$MAME" ]; then + echo "run-vidstream.sh: no MAME binary at $MAME (set MAME=...)" >&2 + exit 1 +fi +if [ "$BENCH" != "0" ]; then + STREAM_POOL_BYTES=${STREAM_POOL_BYTES:-249856} \ + "$ROOT/harness/udpstream/build-stream.sh" +fi +if [ ! -f "$ROOT/harness/udpstream/build/vidstream.bin" ]; then + echo "run-vidstream.sh: no build/vidstream.bin (run without BENCH=0)" >&2 + exit 1 +fi + +# POOL_BYTES / STATIC_PTR come from the image that was just built, so the check +# compares against the linked fallback address rather than a guess. +POOL_BYTES=249856 +STATIC_PTR=0 +ENTRY_PTR=0x018C0000 +ENVFILE="$ROOT/harness/udpstream/build/vidstream.env" +if [ -f "$ENVFILE" ]; then + # shellcheck disable=SC1090 + . "$ENVFILE" +fi + +mkdir -p "$LOGDIR" +set +e +VT1200_SKIP_LEVEL1=1 \ +VT1200_MODE="$MODE" \ +VID_POOL_BYTES="$POOL_BYTES" \ +VID_STATIC_PTR="$STATIC_PTR" \ +VID_ENTRY_PTR="$ENTRY_PTR" \ +"$MAME" vt1200 -rompath "$HERE/roms" -video none -sound none -nothrottle \ + -skip_gameinfo -debug -debugger none \ + -seconds_to_run "${VID_SECONDS:-120}" \ + -autoboot_script "$HERE/vidstream.lua" -autoboot_delay 0 \ + > "$LOG" 2>&1 +STATUS=$? +set -e + +grep '^VID:' "$LOG" || true + +if [ "$STATUS" -ne 0 ]; then + echo "run-vidstream.sh: MAME exited $STATUS; log $LOG" >&2 + tail -20 "$LOG" >&2 + exit "$STATUS" +fi +if ! grep -q '^VID: RESULT PASS' "$LOG"; then + echo "run-vidstream.sh: check failed (log $LOG)" >&2 + exit 1 +fi +exit 0 diff --git a/harness/mame/vidstream.lua b/harness/mame/vidstream.lua new file mode 100644 index 0000000..e1610cf --- /dev/null +++ b/harness/mame/vidstream.lua @@ -0,0 +1,228 @@ +-- vidstream.lua -- load the streaming payload and check its startup allocation. +-- +-- MECHANISM: debugger entry, not a delivery mechanism. The firmware is run +-- with VT1200_SKIP_LEVEL1=1 (a documented divergence) to the OS, because the +-- level-0 diagnostics menu's heap is NOT initialised (both the free counter at +-- 0x1E8BA0 and the free-list head at 0x1E8AC0 read 0 there -- measured; see +-- the task-3 report). The payload is poked into RAM at VID_AT and entered at +-- the OS idle loop by a breakpoint, the same way harness/mame/hello.lua enters +-- its payload at the diagnostics idle loop. +-- +-- Mode "alloc" (Task 3) asserts: +-- * POOL_PTR is non-zero and its BYTE address is in 0x100000..0x400000; +-- * the heap free counter dropped by POOL_BYTES (plus allocator header); +-- * on a failed malloc, POOL_PTR is the linked static buffer and the low bit +-- of POOL_FREE (before) marks the fallback, i.e. the payload keeps running. +-- +-- Marker page: bit 0x01810000. A marker named 0x018100NN in the plan is a +-- BYTE offset into that page (0x01810024 is four bits after 0x01810020 -- +-- inside the same 32-bit cell -- so the literal reading cannot work). The C +-- side uses the same rule: bit = 0x01810000 + NN * 8. +-- +-- Env: +-- VT1200_MODE alloc (default; rx/draw arrive in later tasks) +-- VID_AT load/entry frame (default 2400, the OS phase) +-- VID_END inspection/exit frame (default VID_AT + 60) +-- VID_PAYLOAD raw .text image (default harness/udpstream/build/vidstream.bin) +-- VID_ORIGIN payload load (poke) bit address (default 0x018C0000) +-- VID_ENTRY_PTR _start bit address from vidstream.env (default VID_ORIGIN) +-- VID_ENTRY_PC breakpoint PC used to enter (default 0x3F579970, OS idle) +-- VID_POOL_BYTES POOL_BYTES the image was built with (default 249856) +-- VID_STATIC_PTR linked address of the fallback pool, from vidstream.env + +local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end + +local machine = manager.machine +local cpu = machine.devices[':maincpu'] +local dbg = machine.debugger +local prog = cpu.spaces['program'] + +if not dbg then + print("VID ERROR: debugger not enabled; pass -debug") + machine:exit() + return +end + +local MODE = os.getenv("VT1200_MODE") or "alloc" +local AT = tonumber(os.getenv("VID_AT") or "2400") +local STOP_AT = tonumber(os.getenv("VID_END") or tostring(AT + 60)) +local ORIGIN = tonumber(os.getenv("VID_ORIGIN") or "0x018C0000") +local ENTRY_PC = tonumber(os.getenv("VID_ENTRY_PC") or "0x3F579970") +local PATH = os.getenv("VID_PAYLOAD") or "harness/udpstream/build/vidstream.bin" +local POOL_BYTES = tonumber(os.getenv("VID_POOL_BYTES") or "249856") +local STATIC_PTR = tonumber(os.getenv("VID_STATIC_PTR") or "0") +-- _start is at the linked entry point, not at the section origin: GSPLNK +-- places ENTRY.obj after the C object (see the map). build-stream.sh +-- publishes the address; ORIGIN is only where the raw .text is poked. +local ENTRY_PTR = tonumber(os.getenv("VID_ENTRY_PTR") or "0x018C0000") + +local MARK_PAGE = 0x01810000 +local POOL_PTR = MARK_PAGE + 0x20 * 8 +local POOL_FREEB = MARK_PAGE + 0x24 * 8 +local POOL_FREEA = MARK_PAGE + 0x28 * 8 +local VID_ALIVE = MARK_PAGE + 0x2c * 8 + +if MODE ~= "alloc" then + print("VID ERROR: mode '" .. MODE .. "' is not implemented in Task 3") + machine:exit() + return +end + +local payload = nil +do + local f, err = io.open(PATH, "rb") + if not f then + print("VID ERROR: cannot open " .. PATH .. ": " .. tostring(err)) + machine:exit() + return + end + payload = f:read("*a") + f:close() + print(string.format("VID: payload %s bytes=%d origin=%s", PATH, #payload, hex(ORIGIN))) +end + +local frames = 0 +local loaded = false +local entered = false +local armed = false +local direct = false +local alive0 = nil +local alive_last = nil + +local function load_payload() + local n = math.floor(#payload / 2) + local words = {} + local bad = 0 + for i = 0, n - 1 do + local lo = payload:byte(i * 2 + 1) + local hi = payload:byte(i * 2 + 2) + words[i] = lo | (hi << 8) + prog:write_u16(ORIGIN + i * 16, words[i]) + end + for i = 0, n - 1 do + if prog:read_u16(ORIGIN + i * 16) ~= words[i] then + bad = bad + 1 + end + end + print(string.format("VID: loaded %d words at bit %s; readback %s (%d bad)", + n, hex(ORIGIN), bad == 0 and "OK" or "FAILED", bad)) + -- Enter through the idle loop, exactly like hello.lua. The action only + -- assigns PC and resumes; the payload proves itself. + dbg:command("bpset " .. hex(ENTRY_PC) .. ",,{ pc = " .. hex(ENTRY_PTR) .. " ; g }") + armed = true +end + +local function in_payload(pc) + return pc >= ORIGIN and pc < ORIGIN + #payload * 8 +end + +local function enter_direct(reason) + dbg:command("bpclear " .. hex(ENTRY_PC)) + cpu.state['PC'].value = ENTRY_PTR + direct = true + print(string.format("VID: frame %d: %s; entered payload directly at %s", + frames, reason, hex(ENTRY_PTR))) +end + +local function inspect() + local ptr = prog:read_u32(POOL_PTR) + local freeb = prog:read_u32(POOL_FREEB) + local freea = prog:read_u32(POOL_FREEA) + local alive = prog:read_u32(VID_ALIVE) + local pbyte = math.floor(ptr / 8) + local ok = true + + print(string.format("VID: marker page %s: POOL_PTR=%s POOL_FREE(before)=%d POOL_FREE(after)=%d", + hex(MARK_PAGE), hex(ptr), freeb, freea)) + print(string.format("VID: heartbeat %d (was %s) -- payload %s", + alive, alive_last and tostring(alive_last) or "unset", + (in_payload(cpu.state['PC'].value) or direct) and "still running" or "NOT RUNNING")) + if alive_last ~= nil and alive == alive_last then + ok = false + print("VID: heartbeat did not advance -> FAIL") + end + + if ptr == 0 then + print(string.format("VID: pool pointer %s -> FAIL", hex(ptr))) + ok = false + elseif pbyte < 0x100000 or pbyte > 0x400000 then + print(string.format("VID: pool pointer %s (byte %s) outside 100000..400000 -> FAIL", + hex(ptr), hex(pbyte, 6))) + ok = false + else + print(string.format("VID: pool pointer in range: %s (byte %s) -> PASS", + hex(ptr), hex(pbyte, 6))) + end + + if (freeb & 1) == 1 then + -- malloc failed: the low bit of the before value is the static flag. + local before = freeb - 1 + if STATIC_PTR ~= 0 and ptr == STATIC_PTR then + print(string.format("VID: static fallback at %s, flag set, heap untouched (%d -> %d bytes) -> PASS", + hex(ptr), before, freea)) + else + print(string.format("VID: static fallback wrong: pointer %s, expected %s -> FAIL", + hex(ptr), hex(STATIC_PTR))) + ok = false + end + else + local drop = freeb - freea + local delta = drop - POOL_BYTES + if delta < 0 then delta = -delta end + if drop > 0 and delta <= 64 then + print(string.format("VID: heap free before/after differ by %d bytes (POOL_BYTES=%d, header delta=%d) -> PASS", + drop, POOL_BYTES, drop - POOL_BYTES)) + else + print(string.format("VID: heap free before/after differ by %d bytes, expected ~%d -> FAIL", + drop, POOL_BYTES)) + ok = false + end + end + + print(ok and "VID: RESULT PASS" or "VID: RESULT FAIL") +end + +emu.register_frame_done(function() + frames = frames + 1 + local pcv = cpu.state['PC'].value + + if frames == AT and not loaded then + loaded = true + load_payload() + end + + if loaded and not entered and in_payload(pcv) then + entered = true + dbg:command("bpclear " .. hex(ENTRY_PC)) + alive0 = prog:read_u32(VID_ALIVE) + print(string.format("VID: payload entered by frame %d (PC=%s, heartbeat=%d)", + frames, hex(pcv), alive0)) + end + + if loaded and not entered and armed and frames == AT + 10 then + enter_direct("breakpoint at " .. hex(ENTRY_PC) .. " did not fire") + entered = true + alive0 = prog:read_u32(VID_ALIVE) + end + + if entered and frames % 10 == 0 and frames < STOP_AT then + alive_last = prog:read_u32(VID_ALIVE) + end + + if frames >= STOP_AT then + if not entered then + print("VID: payload never entered -> FAIL") + print("VID: RESULT FAIL") + else + local ok2, err = pcall(inspect) + if not ok2 then + print("VID: ERROR " .. tostring(err)) + print("VID: RESULT FAIL") + end + end + machine:exit() + end +end, "vt1200-vidstream") + +dbg.visible_cpu = cpu +dbg.execution_state = 'run' diff --git a/harness/udpstream/build-stream.sh b/harness/udpstream/build-stream.sh new file mode 100755 index 0000000..3313fd1 --- /dev/null +++ b/harness/udpstream/build-stream.sh @@ -0,0 +1,178 @@ +#!/bin/sh +# Build the video-stream payload (C + ROM-API thunks). +# +# harness/udpstream/build-stream.sh +# STREAM_POOL_BYTES=0x7FFF000 harness/udpstream/build-stream.sh # fail the malloc +# +# .text at bit 0x018C0000, .bss at 0x01C00000 (both inside the firmware's large +# free block), entry _start, no .cinit (the runtime's autoinit pass does not +# run -- see harness/c-hello/README.md). +# +# POOL_BYTES is compile-time: the script writes poolcfg.h from +# STREAM_POOL_BYTES (default 249856 = 244 KB: two 24 KB slots + 160 KB +# framebuffer + 32 KB dictionary + 4 KB slack). +# +# Outputs: build/vidstream.bin (raw .text, what vidstream.lua loads), +# build/vidstream.coff, vidstream.map, vidstream.lst, entry.lst, +# vidstream.env (POOL_BYTES, the linked entry point and the address of +# the static fallback pool, which the MAME check asserts against). +set -eu + +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +DIR=$(cd "$(dirname "$0")" && pwd) +WORK="$DIR/work" +OUT="$DIR/build" +IMG="$ROOT/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img" +DOSBOX_CONF="$ROOT/harness/sdk-scratch/dosbox-bench.conf" + +TEXT=018C0000 +BSS=01C00000 +POOL_BYTES=${STREAM_POOL_BYTES:-249856} + +mkdir -p "$WORK" "$OUT" + +if [ ! -x "$WORK/GSPCG.EXE" ]; then + mcopy -s -n -i "$IMG" "::/A2/*" "$WORK/" + mcopy -s -n -i "$IMG" "::/C2/*" "$WORK/" +fi + +SRC=${STREAM_SRC:-$DIR/vidstream.c} +cp "$SRC" "$WORK/VIDSTRM.C" +cp "$DIR/romapi.h" "$WORK/ROMAPI.H" +cp "$DIR/entry.asm" "$WORK/ENTRY.ASM" +cp "$DIR/vram.asm" "$WORK/VRAM.ASM" + +# vram_b.asm is Task 5's PIXBLT B,XY primitive. Link it when it exists so the +# build does not have to be edited again that day; a Task 3 tree has only +# vram.asm and links exactly what it uses. +VRAMB_OBJ= +VRAMB_LST= +if [ -f "$DIR/vram_b.asm" ]; then + cp "$DIR/vram_b.asm" "$WORK/VRAMB.ASM" + VRAMB_OBJ=VRAMB.obj + VRAMB_LST=VRAMB.LST +fi + +# The allocator request size is a compile-time constant (the 1987 compiler has +# no -D we rely on). One generated header keeps the default in one place. +printf '#define POOL_BYTES %s\n' "$POOL_BYTES" > "$WORK/POOLCFG.H" + +{ + printf '%s\n' '-a' + printf '%s\n' '-o VIDSTRM.out' + printf '%s\n' '-m VIDSTRM.map' + printf '%s\n' '-e _start' + printf '%s\n' 'VIDSTRM.obj' + printf '%s\n' 'ENTRY.obj' + printf '%s\n' 'VRAM.obj' + [ -z "$VRAMB_OBJ" ] || printf '%s\n' "$VRAMB_OBJ" + printf '%s\n' '-l RTS.LIB' + printf '%s\n' 'MEMORY' + printf '%s\n' '{' + printf '%s\n' " prog: origin = ${TEXT}h, length = 20000h" + printf '%s\n' " ram: origin = ${BSS}h, length = 200000h" + printf '%s\n' '}' + printf '%s\n' 'SECTIONS' + printf '%s\n' '{' + printf '%s\n' ' .text: {} > prog' + printf '%s\n' ' .data: {} > prog' + printf '%s\n' ' .cinit: {} > prog' + printf '%s\n' ' .bss: {} > ram' + printf '%s\n' '}' +} > "$WORK/VIDSTRM.CMD" +sed -i 's/$/\r/' "$WORK/VIDSTRM.CMD" + +rm -f "$WORK"/VIDSTRM.OBJ "$WORK"/VIDSTRM.OUT "$WORK"/VIDSTRM.LST \ + "$WORK"/VIDSTRM.MAP "$WORK"/VIDSTRM.ASM "$WORK"/VIDSTRM.CPP \ + "$WORK"/VIDSTRM.IF "$WORK"/ENTRY.OBJ "$WORK"/ENTRY.LST \ + "$WORK"/VRAM.OBJ "$WORK"/VRAM.LST "$WORK"/VRAMB.OBJ "$WORK"/VRAMB.LST + +run_dosbox() { + SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ + timeout 240 dosbox -conf "$DOSBOX_CONF" -noprimaryconf -nolocalconf \ + -c "mount c $WORK" -c "c:" \ + -c "gspcpp VIDSTRM" \ + -c "gspcc VIDSTRM" \ + -c "gspcg VIDSTRM" \ + -c "gspa VIDSTRM VIDSTRM.obj VIDSTRM.lst -l" \ + -c "gspa ENTRY ENTRY.obj ENTRY.lst -l" \ + -c "gspa VRAM VRAM.obj VRAM.lst -l" \ + "$@" \ + -c "gsplnk VIDSTRM.cmd" \ + -c "exit" -exit > "$WORK/dosbox.log" 2>&1 +} +if [ -n "$VRAMB_OBJ" ]; then + run_dosbox -c "gspa VRAMB VRAMB.obj VRAMB.lst -l" +else + run_dosbox +fi + +if [ ! -f "$WORK/VIDSTRM.OUT" ]; then + echo "build-stream.sh: GSPLNK produced no VIDSTRM.OUT" >&2 + sed 's/\r$//' "$WORK/VIDSTRM.MAP" 2>/dev/null | tail -20 >&2 || true + for f in VIDSTRM.LST ENTRY.LST VRAM.LST $VRAMB_LST; do + sed 's/\r$//' "$WORK/$f" 2>/dev/null | grep -i 'error' | grep -vi 'no errors' >&2 || true + done + exit 1 +fi +for f in VIDSTRM.LST ENTRY.LST VRAM.LST $VRAMB_LST; do + if sed 's/\r$//' "$WORK/$f" | grep -i 'error' | grep -qvi 'no errors'; then + echo "build-stream.sh: $f diagnostics:" >&2 + sed 's/\r$//' "$WORK/$f" | grep -in 'error' | grep -vi 'no errors' >&2 + exit 1 + fi +done + +cp "$WORK/VIDSTRM.OUT" "$OUT/vidstream.coff" +sed 's/\r$//' "$WORK/VIDSTRM.MAP" > "$OUT/vidstream.map" +sed 's/\r$//' "$WORK/VIDSTRM.LST" > "$OUT/vidstream.lst" +sed 's/\r$//' "$WORK/ENTRY.LST" > "$OUT/entry.lst" +sed 's/\r$//' "$WORK/VRAM.LST" > "$OUT/vram.lst" +if [ -n "$VRAMB_LST" ]; then + sed 's/\r$//' "$WORK/VRAMB.LST" > "$OUT/vramb.lst" +fi + +# Guard: the marker block (MARK in the source) must sit clear of .text, or the +# payload overwrites its own code with marker writes (an illegal-opcode crash). +python3 - "$SRC" "$OUT/vidstream.map" <<'PYCHK' +import re, sys +src, mp = sys.argv[1], sys.argv[2] +m = re.search(r'#define\s+MARK\s+(0x[0-9A-Fa-f]+)', open(src).read()) +if m: + mark = int(m.group(1), 16) + for ln in open(mp): + f = ln.split() + if len(f) >= 4 and f[0] == '.text': + start, size = int(f[2], 16), int(f[3], 16) + end = start + size + if end > mark: + sys.stderr.write("build: ERROR .text ends at 0x%X, past MARK 0x%X\n" % (end, mark)) + sys.exit(1) + print("build: .text ends 0x%X, MARK 0x%X (%d bytes clear)" % (end, mark, (mark - end) // 8)) +PYCHK + +# The raw .text image is what the debugger loads (vidstream.lua). +python3 "$ROOT/harness/hello-world/cofextract.py" \ + "$WORK/VIDSTRM.OUT" "$OUT/vidstream.bin" + +# The static-fallback address and the entry point are linked, not guessed: +# publish them (bits) for the MAME check, which enters at _start and asserts +# that a failed malloc lands exactly on the static buffer. +STATIC_PTR=$(sed 's/\r$//' "$WORK/VIDSTRM.MAP" \ + | awk '$2 == "_pool_static" { print $1; exit }') +if [ -z "$STATIC_PTR" ]; then + echo "build-stream.sh: _pool_static missing from the map (fallback cannot be checked)" >&2 + exit 1 +fi +ENTRY_PTR=$(sed 's/\r$//' "$WORK/VIDSTRM.MAP" \ + | awk '/ENTRY POINT SYMBOL/ { print $NF; exit }') +if [ -z "$ENTRY_PTR" ]; then + echo "build-stream.sh: no entry point in the map" >&2 + exit 1 +fi +printf 'POOL_BYTES=%s\nSTATIC_PTR=0x%s\nENTRY_PTR=0x%s\n' \ + "$POOL_BYTES" "$STATIC_PTR" "$ENTRY_PTR" > "$OUT/vidstream.env" + +echo "build-stream.sh: $OUT/vidstream.bin ($(wc -c < "$OUT/vidstream.bin") bytes)" +grep -E "ENTRY POINT|^\.(text|cinit|bss)" "$OUT/vidstream.map" | head -8 +echo "build-stream.sh: POOL_BYTES=$POOL_BYTES entry bit 0x$ENTRY_PTR static fallback bit 0x$STATIC_PTR" diff --git a/harness/udpstream/vidstream.c b/harness/udpstream/vidstream.c new file mode 100644 index 0000000..8f11bba --- /dev/null +++ b/harness/udpstream/vidstream.c @@ -0,0 +1,90 @@ +/* + * vidstream.c -- terminal side of the VT1200 video stream (startup skeleton). + * + * Task 3 only: claim the working set with ONE malloc at startup and publish + * where it landed. The receive ring (Task 4) and the blit loop (Task 5) build + * on the pointer this file leaves behind. + * + * Marker layout. The design plan names markers as 0x01810020, 0x01810024, + * 0x01810030, ... Those cannot be read literally as bit addresses: the + * TMS34010 is bit-addressed, so 0x01810024 is FOUR BITS after 0x01810020, + * inside the same 32-bit cell. They are byte offsets into the marker page + * (bit 0x01810000, byte 0x00302000), which is the only reading under which + * the plan's 4-byte spacing holds. So a named marker 0x018100NN lives at + * + * bit 0x01810000 + NN * 8 + * + * (POOL_PTR = 0x01810100, POOL_FREE before/after = 0x01810120/0x01810140). + * vidstream.lua uses the same rule. See the task-3 report. + * + * K&R C only: no prototypes, no '//', declarations before statements. + */ +#include "romapi.h" +#include "poolcfg.h" /* POOL_BYTES, written by build-stream.sh */ + +#define MARK_PAGE 0x01810000 /* bit address of the marker page */ +#define POOL_PTR (MARK_PAGE + 0x20 * 8) /* u32 bit pointer to the pool */ +#define POOL_FREEB (MARK_PAGE + 0x24 * 8) /* u32 heap free bytes, before */ +#define POOL_FREEA (MARK_PAGE + 0x28 * 8) /* u32 heap free bytes, after */ +#define VID_ALIVE (MARK_PAGE + 0x2c * 8) /* u32 heartbeat while spinning */ + +#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 */ + +/* + * 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. Task 5 fills the blit_* words. + */ +unsigned int vram_pattern; +unsigned int blit_src, blit_sptch, blit_daddr, blit_dydx; + +/* + * 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; +} + +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; + + 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; + *(unsigned int *)POOL_PTR = ptr; /* publish last: readers see a complete record */ + + for (;;) { + *(unsigned int *)VID_ALIVE = *(unsigned int *)VID_ALIVE + 1; + } +}