fix(loader): exit the harness on completion, and add LOADER_END=0 #9

Merged
arti merged 1 commits from opencode.siil.arti.ee/vt1200:fix/issue-3-loader-completion into master 2026-11-13 11:54:56 +02:00
2 changed files with 128 additions and 25 deletions

No files matched your search

+41 -1
View File
@@ -56,5 +56,45 @@ The script pokes the loader at `0x01880000` and the COFF at `LOADER_BUF`
network, the rdebug service or a firmware transport.
Knobs: `LOADER_MODE=1` (callable/return instead of debug-resume),
`LOADER_A8_DESC=1` (descriptor in `A8`), `LOADER_SNAP=1` (PNG at `LOADER_END`),
`LOADER_A8_DESC=1` (descriptor in `A8`), `LOADER_SNAP=1` (PNG at the exit frame),
`LOADER_VERBOSE=1`, `LOADER_TRACE=1`.
### Environment
| variable | default | meaning |
|---|---|---|
| `LOADER_AT` | `2500` | frame at which the loader and COFF are poked and `PC` is set |
| `LOADER_END` | `2700` | fixed inspection/exit frame; **`0` = run until completion** |
| `LOADER_MAX` | `6000` | `LOADER_END=0` only: give up (with a warning) at this frame |
| `LOADER_BIN` | `dwt/loader/build/loader.bin` | loader payload poked at bit `0x01880000` |
| `LOADER_COFF` | `dwt/loader/build/marker.coff` | app COFF poked at `LOADER_BUF` |
| `LOADER_BUF` | `0x01900000` | COFF buffer bit address |
| `LOADER_OUT` | `dwt/loader/evidence` | directory for the optional PNG |
| `LOADER_SNAP` | `0` | `1` writes `loader_frame<frame>.png` at the exit frame |
### Completion and `LOADER_END=0`
The harness detects completion when all three hold:
1. the loader has written `RESULT_STATUS` (it is `0xFFFFFFFF` until then),
2. `PC` is outside `[LOADER_BASE, LOADER_BASE+0x400)`, and
3. the result block has been unchanged for 10 frames.
On detection it prints one line and sets `done` once:
```
LOADER: complete at frame 2510 status=00000000
```
With a **fixed** `LOADER_END` the run still exits at that frame, so the snapshot
filename and every existing fixed-frame capture are unchanged; the completion
line is informational and suppresses the startup warning. With **`LOADER_END=0`**
the run exits as soon as completion is detected, and if it is not detected by
`LOADER_MAX` it prints
```
LOADER: warning: app still in startup at LOADER_MAX=6000
```
and exits there. With a fixed `LOADER_END` that is reached first, the warning is
`LOADER: warning: app still in startup at LOADER_END=<n>`.
+87 -24
View File
@@ -17,9 +17,10 @@
-- LOADER_COFF app COFF (default dwt/loader/build/marker.coff)
-- LOADER_BUF COFF buffer bit address (default 0x01900000)
-- LOADER_AT injection frame (default 2500)
-- LOADER_END inspection/exit frame (default 2700)
-- LOADER_END inspection/exit frame (default 2700; 0 = wait for completion)
-- LOADER_MAX LOADER_END=0 only: give up after this many frames (default 6000)
-- LOADER_OUT evidence directory (default dwt/loader/evidence)
-- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0)
-- LOADER_SNAP 1 = write a PNG at the exit frame (default 0)
-- LOADER_VERBOSE 1 = print the result block every 25 frames
local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end
@@ -37,6 +38,9 @@ end
local AT = tonumber(os.getenv("LOADER_AT") or "2500")
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local MAXF = tonumber(os.getenv("LOADER_MAX") or "6000")
local WAIT = ENDF == 0
local STABLE = 10
local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin"
local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff"
local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000")
@@ -177,6 +181,42 @@ local function result_dump()
r32(RESULT + 0x140), r32(RESULT + 0x160)))
end
-- The loader writes RESULT_STATUS last (it is 0xFFFFFFFF until then). The
-- whole result block is the completion signal: status written, PC out of the
-- loader's entry page, and the block unchanged for STABLE frames. This keeps
-- a write that is still in progress from looking final.
local RESULT_CELLS = { 0x000, 0x020, 0x040, 0x060, 0x080, 0x0A0, 0x0C0,
0x0E0, 0x100, 0x120, 0x140, 0x160, 0x180 }
local function result_signature()
local t = {}
for i, off in ipairs(RESULT_CELLS) do
t[i] = string.format("%08X", r32(RESULT + off))
end
return table.concat(t, " ")
end
local stable_signature = nil
local stable_frames = 0
local function complete()
local st = r32(RESULT + 0x000)
local pc = cpu.state['PC'].value
if st == 0xFFFFFFFF or (pc >= LOADER_BASE and pc < LOADER_BASE + 0x400) then
stable_signature = nil
stable_frames = 0
return false
end
local sig = result_signature()
if sig == stable_signature then
stable_frames = stable_frames + 1
else
stable_signature = sig
stable_frames = 1
end
return stable_frames >= STABLE
end
local function inject()
-- 1. loader itself
local n, bad = 0, 0
@@ -273,6 +313,26 @@ local function inspect()
print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value)))
end
local finished = false
local function finish()
if finished then return end
finished = true
if injected then
inspect()
if SNAP then
local screen = nil
for _, s in pairs(machine.screens) do screen = s break end
if screen then
local out = OUT .. "/loader_frame" .. frames .. ".png"
local ok, err = pcall(function() return screen:snapshot(out) end)
print(string.format("LOADER: snapshot %s ok=%s %s",
out, tostring(ok), tostring(err or "")))
end
end
end
machine:exit()
end
emu.register_frame_done(function()
frames = frames + 1
if frames == AT and not injected then
@@ -283,20 +343,23 @@ emu.register_frame_done(function()
dbg:command("trace off")
print("LOADER: trace written to /tmp/opencode/ld.tr")
end
if injected and not done then
if injected then
if TRACE and frames <= AT + 40 then
print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s",
frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value),
hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)),
hex(r32(RESULT + 0x020))))
end
local st = r32(RESULT + 0x000)
-- status 0 means OK; nonzero is final too once parsing has begun
if st ~= 0xFFFFFFFF and not done then
-- The loader writes 0 on success. A nonzero status is final
-- only after it has been written once (it is 0 from power-on,
-- so require the loader to have run: PC no longer at entry or
-- the app counter moved).
-- Status 0 means OK; any nonzero status the loader wrote is final too.
-- done is set exactly once, on the frame completion is detected.
if not done and complete() then
done = true
print(string.format("LOADER: complete at frame %d status=%s",
frames, hex(r32(RESULT + 0x000))))
if WAIT then
finish()
return
end
end
if VERBOSE and frames % 25 == 0 then
result_dump()
@@ -312,22 +375,22 @@ emu.register_frame_done(function()
end
end
end
if frames >= ENDF then
if injected then
done = true
inspect()
if SNAP then
local screen = nil
for _, s in pairs(machine.screens) do screen = s break end
if screen then
local out = OUT .. "/loader_frame" .. frames .. ".png"
local ok, err = pcall(function() return screen:snapshot(out) end)
print(string.format("LOADER: snapshot %s ok=%s %s",
out, tostring(ok), tostring(err or "")))
end
if WAIT then
-- LOADER_END=0: run until completion, but never forever.
if frames >= MAXF then
if not done then
print(string.format("LOADER: warning: app still in startup at LOADER_MAX=%d", MAXF))
end
finish()
end
machine:exit()
elseif frames >= ENDF then
-- A fixed LOADER_END still exits at that frame (the snapshot contract;
-- existing tests read loader_frame<ENDF>.png). Completion is reported
-- above but only LOADER_END=0 asks to exit early.
if not done then
print(string.format("LOADER: warning: app still in startup at LOADER_END=%d", ENDF))
end
finish()
end
end, "vt1200-loader")