forked from arti/vt1200
feat(stream): payload skeleton with one startup malloc
This commit is contained in:
1 parent
1a68630534
commit
4f6b0bf1a6
4 files changed
+571
No files matched your search
Executable
+75
@@ -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
|
||||
@@ -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'
|
||||
Executable
+178
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user