Files
vt1200/harness/mame/vidstream.lua
T

229 lines
7.9 KiB
Lua

-- 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'