diff --git a/dwt/loader/README.md b/dwt/loader/README.md
index 331828c..ba2b32a 100644
--- a/dwt/loader/README.md
+++ b/dwt/loader/README.md
@@ -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.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=`.
diff --git a/dwt/loader/loader.lua b/dwt/loader/loader.lua
index fe87ab4..57e8173 100644
--- a/dwt/loader/loader.lua
+++ b/dwt/loader/loader.lua
@@ -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.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")