diff --git a/apps/termtris/tests/fmt.lua b/apps/termtris/tests/fmt.lua index 0af6df3..0843ca6 100644 --- a/apps/termtris/tests/fmt.lua +++ b/apps/termtris/tests/fmt.lua @@ -1,354 +1,24 @@ --- loader.lua -- debugger-assisted COFF loader acceptance harness. --- --- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to --- the OS, then at frame LOADER_AT pokes: --- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin) --- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000) --- * the loader descriptor at 0x01882C00 --- and sets PC = 0x01880000. It does not use the network, the rdebug service --- or any firmware transport: this is the debugger-assisted milestone. --- --- The loader itself copies the COFF sections to their linked bit addresses and --- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes --- the input and reads the result; it never writes the app's code or markers. --- --- Env: --- LOADER_BIN loader payload (default dwt/loader/build/loader.bin) --- 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_OUT evidence directory (default dwt/loader/evidence) --- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0) --- LOADER_VERBOSE 1 = print the result block every 25 frames +-- fmt.lua -- six formatted strings, read back from the app's output cells. +-- fmt.sh checks each FMT:n line byte-for-byte. +local dir = (debug.getinfo(1, "S").source:gsub("^@", ""):match("^(.*)/") or ".") +local H = dofile(dir .. "/harness.lua") -local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end - -local machine = manager.machine local FMT_MARK = tonumber(os.getenv("FMT_MARK") or "0x01A00000") -local cpu = machine.devices[':maincpu'] -local dbg = machine.debugger -local prog = cpu.spaces['program'] - -if not dbg then - print("LOADER: debugger not enabled; pass -debug") - machine:exit() - return -end - -local AT = tonumber(os.getenv("LOADER_AT") or "2500") -local ENDF = tonumber(os.getenv("LOADER_END") or "2700") -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") -local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence" -local SNAP = os.getenv("LOADER_SNAP") == "1" -local MODE = tonumber(os.getenv("LOADER_MODE") or "0") -local A8DESC = os.getenv("LOADER_A8_DESC") == "1" -local VERBOSE = os.getenv("LOADER_VERBOSE") == "1" - -local LOADER_BASE = 0x01880000 -local DESC = 0x01882C00 -local RESULT = 0x01882D00 -local RETURN_STUB = 0x01883100 -local RETURN_SP = 0x01883110 - -local function rd(p) return io.open(p, "rb") end -local function slurp(p) - local f, err = rd(p) - if not f then error("cannot open " .. p .. ": " .. tostring(err)) end - local s = f:read("*a") - f:close() - return s -end - -local function u16(s, o) - return s:byte(o + 1) + s:byte(o + 2) * 256 -end - -local function u32(s, o) - return s:byte(o + 1) + s:byte(o + 2) * 256 - + s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216 -end - -local function parse_coff(d) - local c = { data = d } - c.magic = u16(d, 0) - c.nscns = u16(d, 2) - c.opthdr = u16(d, 16) - c.sections = {} - if c.opthdr > 0 then - c.optmagic = u16(d, 20) - c.entry = u32(d, 20 + 16) - c.textstart = u32(d, 20 + 20) - c.datastart = u32(d, 20 + 24) - end - local base = 20 + c.opthdr - for i = 0, c.nscns - 1 do - local o = base + i * 40 - local name = d:sub(o + 1, o + 8):gsub("%z", "") - c.sections[#c.sections + 1] = { - name = name, - paddr = u32(d, o + 8), - size = u32(d, o + 16), -- bits - scnptr = u32(d, o + 20), - flags = u32(d, o + 36), - } - end - return c -end - -local function poke_bytes(bitaddr, data) - local n = math.floor((#data + 1) / 2) - for i = 0, n - 1 do - local lo = data:byte(i * 2 + 1) or 0 - local hi = data:byte(i * 2 + 2) or 0 - prog:write_u16(bitaddr + i * 16, lo + hi * 256) - end - return n -end - -local function verify_bytes(bitaddr, data) - local n = math.floor((#data + 1) / 2) - local bad = 0 - for i = 0, n - 1 do - local lo = data:byte(i * 2 + 1) or 0 - local hi = data:byte(i * 2 + 2) or 0 - if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then - bad = bad + 1 - end - end - return n, bad -end - -local function w32(a, v) prog:write_u32(a, v) end -local function r32(a) - local ok, v = pcall(function() return prog:read_u32(a) end) - if ok then return v end - return 0xDEADBEEF -end local function rstr(a, n) local t = {} for i = 0, n - 1 do - local c = prog:read_u8(a + i * 8) + local c = H.prog:read_u8(a + i * 8) if c == 0 then break end t[#t + 1] = string.char(c) end return table.concat(t) end -local loader = slurp(BIN) -local coff = slurp(COFF) -local app = parse_coff(coff) - -print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE))) -print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF))) -print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s", - app.magic, app.nscns, app.opthdr, - app.optmagic and string.format("%04X", app.optmagic) or "none", - hex(app.entry or 0), hex(app.textstart or 0))) -for _, s in ipairs(app.sections) do - print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x", - s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags)) -end - -local TRACE = os.getenv("LOADER_TRACE") == "1" - -local frames = 0 -local injected = false -local done = false -local last_count = -1 - -local function status_name(v) - local t = { - [0] = "OK", - [1] = "bad descriptor", - [2] = "truncated COFF / section headers", - [3] = "bad COFF magic", - [4] = "section size not whole words", - [5] = "target outside RAM", - [6] = "target overlaps loader page or COFF buffer", - [7] = "relocation records present (unsupported)", - [8] = "target not in a free-list block", - [9] = "no entry point", - } - return t[v] or ("error " .. tostring(v)) -end - -local function result_dump() - local st = r32(RESULT + 0x000) - print(string.format( - "LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d", - st, status_name(st), hex(r32(RESULT + 0x020)), - hex(r32(RESULT + 0x040)), r32(RESULT + 0x060), - hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0), - hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0), - r32(RESULT + 0x100), hex(r32(RESULT + 0x120)), - r32(RESULT + 0x140), r32(RESULT + 0x160))) -end - -local function inject() - -- 1. loader itself - local n, bad = 0, 0 - poke_bytes(LOADER_BASE, loader) - n, bad = verify_bytes(LOADER_BASE, loader) - print(string.format("LOADER: loader poked %d words, readback %d bad at %s", - n, bad, hex(LOADER_BASE))) - - -- 2. COFF image - poke_bytes(BUF, coff) - n, bad = verify_bytes(BUF, coff) - print(string.format("LOADER: COFF poked %d words, readback %d bad at %s", - n, bad, hex(BUF))) - - -- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful - for _, s in ipairs(app.sections) do - if s.flags & 0x80 ~= 0 then - for i = 0, math.floor(s.size / 16) - 1 do - prog:write_u16(s.paddr + i * 16, 0xA5A5) - end - print(string.format("LOADER: pre-filled .bss %s..%s with A5A5", - hex(s.paddr), hex(s.paddr + s.size))) - end +H.result_dump(function() + for i = 0, 5 do + print(string.format("FMT:%d [%s]", i, rstr(FMT_MARK + i * 0x80, 16))) end +end) - -- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin) - w32(DESC + 0x00, 0) - prog:write_u16(DESC + 0x00, 0x4C44) - prog:write_u16(DESC + 0x10, 0x5231) - w32(DESC + 0x20, BUF) - w32(DESC + 0x40, #coff) - w32(DESC + 0x60, MODE) - print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d", - hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10), - hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60))) - - -- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC - -- itself); mode 1 returns here, and the stub resumes the interrupted - -- PC so the caller yields and the scheduler can run the child. - local orig_pc = cpu.state['PC'].value - if MODE == 1 then - prog:write_u16(RETURN_STUB, 0xC080) -- jauc - prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF) - prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF) - else - prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self - end - - -- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at - -- the return slot so mode=1 would return to the spin stub. - -- Leave SP alone for mode 0: the loader pushes onto the current - -- process stack, then restores it and resumes at ORIG_PC. (mode 1 - -- would use the return stub.) - w32(0x01882A20, orig_pc) -- LD_ORIG_PC - print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc))) - cpu.state['A8'].value = A8DESC and DESC or 0 - if MODE == 1 then - -- Simulate a CALLA from the current process: push the return - -- address on the *firmware* stack (a real transport would do this). - local sp = cpu.state['SP'].value - 0x20 - w32(sp, RETURN_STUB) - cpu.state['SP'].value = sp - end - cpu.state['PC'].value = LOADER_BASE - injected = true - print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)", - frames, hex(LOADER_BASE), hex(cpu.state['SP'].value))) -end - -local function inspect() - result_dump() - - -- Verify the placement from RAM: .text/.data must equal the file bytes. - for _, s in ipairs(app.sections) do - if s.scnptr ~= 0 and s.size > 0 then - local nb = math.floor(s.size / 8) - local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb) - local _, bad = verify_bytes(s.paddr, bytes) - print(string.format("LOADER: placed %s at %s: %d bytes, %d differing", - s.name, hex(s.paddr), nb, bad)) - end - end - - local base = COFF:match("([^/]+)$") or COFF - if base:find("marker") then - print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d", - hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)), - hex(r32(0x018E0060)), r32(0x018E0080))) - elseif base:find("xhello") then - print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s", - r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)), - hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130)))) - end - if base:find("fmt") then - for i = 0, 5 do - print(string.format("FMT:%d [%s]", i, rstr(FMT_MARK + i * 0x80, 16))) - end - end - print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) -end - -emu.register_frame_done(function() - frames = frames + 1 - if frames == AT and not injected then - inject() - if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end - end - if injected and TRACE and frames == AT + 1 then - dbg:command("trace off") - print("LOADER: trace written to /tmp/opencode/ld.tr") - end - if injected and not done 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). - end - if VERBOSE and frames % 25 == 0 then - result_dump() - end - if base_contains_marker then - local c = r32(0x018E0080) - if c ~= last_count then - if last_count ~= -1 then - print(string.format("LOADER: marker counter %d -> %d (child running)", - last_count, c)) - end - last_count = c - 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 - end - end - machine:exit() - end -end, "vt1200-loader") - --- (Lua has no forward use of a name defined later; set a small flag here.) -base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil - -dbg.visible_cpu = cpu -dbg.execution_state = 'run' +H.run() diff --git a/apps/termtris/tests/harness.lua b/apps/termtris/tests/harness.lua new file mode 100644 index 0000000..bddf365 --- /dev/null +++ b/apps/termtris/tests/harness.lua @@ -0,0 +1,354 @@ +-- harness.lua -- the shared debugger-assisted COFF loader harness. +-- +-- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to +-- the OS, then at frame LOADER_AT pokes the loader payload at bit 0x01880000, +-- the app's GSPLNK COFF at LOADER_BUF (default bit 0x01900000) and the loader +-- descriptor at 0x01882C00, and sets PC = 0x01880000. No network, no rdebug: +-- this is the debugger-assisted milestone. The loader copies the COFF sections +-- to their linked bit addresses and calls +-- create_process(entry, "LOADER", 0x6000, 1, 0); this harness only pokes the +-- input and reads the result, it never writes the app's code. +-- +-- A test is configuration plus hooks: +-- +-- local H = dofile( .. "/harness.lua") +-- H.config{ tablet = "SMOKE" } +-- H.sequence(3100, 20, { { ":lk401:K26", "A" } }) +-- H.result_dump(function() print(...) end) +-- H.run() +-- +-- Env (the tests/*.sh contract): LOADER_BIN, LOADER_COFF, LOADER_BUF, +-- LOADER_AT, LOADER_END, LOADER_OUT, LOADER_SNAP, LOADER_VERBOSE, and the +-- DIAG_LOOPS/EVENTS/LAST/FOCUS/REV/QUIT/TICK bit addresses (a missing one +-- prints -1). +-- +-- The exit is the fixed LOADER_END frame, never an early completion: the tests +-- read the snapshot at that frame (loader_frame.png). + +local H = {} + +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'] + +H.machine, H.cpu, H.dbg, H.prog, H.hex = machine, cpu, dbg, prog, hex + +local LOADER_BASE = 0x01880000 +local DESC = 0x01882C00 +local RESULT = 0x01882D00 + +local cfg = { + bin = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin", + coff = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff", + buf = tonumber(os.getenv("LOADER_BUF") or "0x01900000"), + at = tonumber(os.getenv("LOADER_AT") or "2500"), + endf = tonumber(os.getenv("LOADER_END") or "2700"), + out = os.getenv("LOADER_OUT") or "dwt/loader/evidence", + snap = os.getenv("LOADER_SNAP") == "1", + verbose = os.getenv("LOADER_VERBOSE") == "1", +} + +local diag = { + loops = tonumber(os.getenv("DIAG_LOOPS") or "0") or 0, + events = tonumber(os.getenv("DIAG_EVENTS") or "0") or 0, + last = tonumber(os.getenv("DIAG_LAST") or "0") or 0, + focus = tonumber(os.getenv("DIAG_FOCUS") or "0") or 0, + rev = tonumber(os.getenv("DIAG_REV") or "0") or 0, + quit = tonumber(os.getenv("DIAG_QUIT") or "0") or 0, + tick = tonumber(os.getenv("DIAG_TICK") or "0") or 0, +} + +local function slurp(p) + local f, err = io.open(p, "rb") + if not f then error("cannot open " .. p .. ": " .. tostring(err)) end + local s = f:read("*a") + f:close() + return s +end + +local function u16(s, o) return s:byte(o + 1) + s:byte(o + 2) * 256 end +local function u32(s, o) + return s:byte(o + 1) + s:byte(o + 2) * 256 + + s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216 +end + +local function parse_coff(d) + local c = { data = d, sections = {} } + c.magic = u16(d, 0) + c.nscns = u16(d, 2) + c.opthdr = u16(d, 16) + if c.opthdr > 0 then + c.optmagic = u16(d, 20) + c.entry = u32(d, 20 + 16) + c.textstart = u32(d, 20 + 20) + c.datastart = u32(d, 20 + 24) + end + local base = 20 + c.opthdr + for i = 0, c.nscns - 1 do + local o = base + i * 40 + c.sections[#c.sections + 1] = { + name = d:sub(o + 1, o + 8):gsub("%z", ""), + paddr = u32(d, o + 8), + size = u32(d, o + 16), -- bits + scnptr = u32(d, o + 20), + flags = u32(d, o + 36), + } + end + return c +end + +local function poke_bytes(bitaddr, data) + local n = math.floor((#data + 1) / 2) + for i = 0, n - 1 do + local lo = data:byte(i * 2 + 1) or 0 + local hi = data:byte(i * 2 + 2) or 0 + prog:write_u16(bitaddr + i * 16, lo + hi * 256) + end + return n +end + +local function verify_bytes(bitaddr, data) + local n = math.floor((#data + 1) / 2) + local bad = 0 + for i = 0, n - 1 do + local lo = data:byte(i * 2 + 1) or 0 + local hi = data:byte(i * 2 + 2) or 0 + if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then + bad = bad + 1 + end + end + return n, bad +end + +local function w32(a, v) prog:write_u32(a, v) end +local function r32(a) + local ok, v = pcall(function() return prog:read_u32(a) end) + if ok then return v end + return 0xDEADBEEF +end +H.r32 = r32 + +local STATUS = { + "OK", "bad descriptor", "truncated COFF / section headers", + "bad COFF magic", "section size not whole words", "target outside RAM", + "target overlaps loader page or COFF buffer", + "relocation records present (unsupported)", + "target not in a free-list block", "no entry point", +} +local function status_name(v) return STATUS[v + 1] or ("error " .. tostring(v)) end + +local loader, coff, app + +local function prepare() + loader = slurp(cfg.bin) + coff = slurp(cfg.coff) + app = parse_coff(coff) + + print(string.format("LOADER: payload %s (%d B) -> bit %s", cfg.bin, #loader, hex(LOADER_BASE))) + print(string.format("LOADER: COFF %s (%d B) -> bit %s", cfg.coff, #coff, hex(cfg.buf))) + print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s", + app.magic, app.nscns, app.opthdr, + app.optmagic and string.format("%04X", app.optmagic) or "none", + hex(app.entry or 0), hex(app.textstart or 0))) + for _, s in ipairs(app.sections) do + print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x", + s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags)) + end +end + +local function dump_result() + local st = r32(RESULT + 0x000) + print(string.format( + "LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d", + st, status_name(st), hex(r32(RESULT + 0x020)), + hex(r32(RESULT + 0x040)), r32(RESULT + 0x060), + hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0), + hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0), + r32(RESULT + 0x100), hex(r32(RESULT + 0x120)), + r32(RESULT + 0x140), r32(RESULT + 0x160))) +end + +local function cell(addr) return addr ~= 0 and r32(addr) or -1 end + +local function dump_diag() + local focus = diag.focus ~= 0 and string.format("%08X", r32(diag.focus)) or "-1" + print(string.format("DIAG: loops=%d events=%d last=%d focus=%s rev=%d quit=%d", + cell(diag.loops), cell(diag.events), cell(diag.last), focus, + cell(diag.rev), cell(diag.quit))) + print(string.format("DIAG: tick=%d quit=%d", cell(diag.tick), cell(diag.quit))) +end + +local events = {} +local frame_hooks = {} +local result_hooks = {} +local done_hooks = {} +local tableted = false + +local function field(tag, name) + local port = machine.ioport.ports[tag] + if not port then error("no input port " .. tag) end + local f = port.fields[name] + if not f then error("no field " .. name .. " on " .. tag) end + return f +end + +-- Schedule one input event at `frame`, then print `message` (if any). +function H.key(frame, tag, name, value, message) + events[#events + 1] = { frame = frame, field = field(tag, name), value = value, message = message } +end + +-- Schedule a log line at a frame. +function H.message(frame, text) + events[#events + 1] = { frame = frame, message = text } +end + +-- Schedule a press/release pair for each {tag, field} entry, `step` frames +-- apart, half a step between the press and the release. +function H.sequence(first, step, keys) + local half = math.floor(step / 2) + for i, k in ipairs(keys) do + local f = first + (i - 1) * step + H.key(f, k[1], k[2], 1) + H.key(f + half, k[1], k[2], 0) + end +end + +function H.config(opts) + for k, v in pairs(opts or {}) do cfg[k] = v end + if cfg.tablet and not tableted then + tableted = true + local label = type(cfg.tablet) == "string" and cfg.tablet or "LOADER" + H.key(2900, ":tablet:TABLET_X", "Tablet X", 700) + H.key(2900, ":tablet:TABLET_Y", "Tablet Y", 500, label .. ": tablet move") + H.key(3000, ":tablet:TABLET_BTN", "P1 Button 1", 1, label .. ": click down") + H.key(3020, ":tablet:TABLET_BTN", "P1 Button 1", 0, label .. ": click up") + end +end + +function H.on_frame(fn) frame_hooks[#frame_hooks + 1] = fn end +function H.result_dump(fn) result_hooks[#result_hooks + 1] = fn end +function H.on_done(fn) done_hooks[#done_hooks + 1] = fn end + +local frames = 0 +local injected = false +local done = false + +local function inject() + -- 1. loader itself + poke_bytes(LOADER_BASE, loader) + local n, bad = verify_bytes(LOADER_BASE, loader) + print(string.format("LOADER: loader poked %d words, readback %d bad at %s", + n, bad, hex(LOADER_BASE))) + + -- 2. COFF image + poke_bytes(cfg.buf, coff) + n, bad = verify_bytes(cfg.buf, coff) + print(string.format("LOADER: COFF poked %d words, readback %d bad at %s", + n, bad, hex(cfg.buf))) + + -- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful + for _, s in ipairs(app.sections) do + if s.flags & 0x80 ~= 0 then + for i = 0, math.floor(s.size / 16) - 1 do + prog:write_u16(s.paddr + i * 16, 0xA5A5) + end + print(string.format("LOADER: pre-filled .bss %s..%s with A5A5", + hex(s.paddr), hex(s.paddr + s.size))) + end + end + + -- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (debug resume) + w32(DESC + 0x00, 0) + prog:write_u16(DESC + 0x00, 0x4C44) + prog:write_u16(DESC + 0x10, 0x5231) + w32(DESC + 0x20, cfg.buf) + w32(DESC + 0x40, #coff) + w32(DESC + 0x60, 0) + print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d", + hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10), + hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60))) + + -- 5. enter the loader. A8=0 selects the fixed descriptor; mode 0 restores + -- the interrupted state and resumes at LD_ORIG_PC, so the caller yields + -- and the scheduler can run the child. + local orig_pc = cpu.state['PC'].value + w32(0x01882A20, orig_pc) -- LD_ORIG_PC + print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc))) + cpu.state['A8'].value = 0 + cpu.state['PC'].value = LOADER_BASE + injected = true + print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)", + frames, hex(LOADER_BASE), hex(cpu.state['SP'].value))) +end + +local function inspect() + dump_result() + + -- Verify the placement from RAM: .text/.data must equal the file bytes. + for _, s in ipairs(app.sections) do + if s.scnptr ~= 0 and s.size > 0 then + local nb = math.floor(s.size / 8) + local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb) + local _, bad = verify_bytes(s.paddr, bytes) + print(string.format("LOADER: placed %s at %s: %d bytes, %d differing", + s.name, hex(s.paddr), nb, bad)) + end + end + + for _, fn in ipairs(result_hooks) do fn() end + dump_diag() + print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) +end + +local function snapshot() + if not cfg.snap then return end + local screen = nil + for _, s in pairs(machine.screens) do screen = s break end + if not screen then return end + local out = cfg.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 + +local function fire_events() + for _, e in ipairs(events) do + if e.frame == frames then + if e.field then e.field:set_value(e.value) end + if e.message then print(e.message) end + end + end +end + +function H.run() + if not dbg then + print("LOADER: debugger not enabled; pass -debug") + machine:exit() + return + end + dbg.visible_cpu = cpu + dbg.execution_state = 'run' + prepare() + + emu.register_frame_done(function() + frames = frames + 1 + fire_events() + for _, fn in ipairs(frame_hooks) do fn(frames) end + if frames == cfg.at and not injected then inject() end + if injected and not done and cfg.verbose and frames % 25 == 0 then dump_result() end + if frames >= cfg.endf then + if injected then + done = true + inspect() + for _, fn in ipairs(done_hooks) do fn() end + snapshot() + end + machine:exit() + end + end, "vt1200-loader") +end + +return H diff --git a/apps/termtris/tests/input.lua b/apps/termtris/tests/input.lua index fd89e54..6ff3ab4 100644 --- a/apps/termtris/tests/input.lua +++ b/apps/termtris/tests/input.lua @@ -1,387 +1,30 @@ --- loader.lua -- debugger-assisted COFF loader acceptance harness. --- --- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to --- the OS, then at frame LOADER_AT pokes: --- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin) --- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000) --- * the loader descriptor at 0x01882C00 --- and sets PC = 0x01880000. It does not use the network, the rdebug service --- or any firmware transport: this is the debugger-assisted milestone. --- --- The loader itself copies the COFF sections to their linked bit addresses and --- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes --- the input and reads the result; it never writes the app's code or markers. --- --- Env: --- LOADER_BIN loader payload (default dwt/loader/build/loader.bin) --- 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_OUT evidence directory (default dwt/loader/evidence) --- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0) --- LOADER_VERBOSE 1 = print the result block every 25 frames +-- input.lua -- main.c's key map against injected keys. input.sh checks the +-- INPUT: line for the mapped byte sequence. +local dir = (debug.getinfo(1, "S").source:gsub("^@", ""):match("^(.*)/") or ".") +local H = dofile(dir .. "/harness.lua") -local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end - -local machine = manager.machine local INPUT_MARK = tonumber(os.getenv("INPUT_MARK") or "0x01A00000") -local ports = machine.ioport.ports -local key_a = ports[":lk401:K26"].fields["A"] -local key_up = ports[":lk401:K77"].fields["Up"] -local key_down = ports[":lk401:K76"].fields["Down"] -local key_left = ports[":lk401:K74"].fields["Left"] -local key_right = ports[":lk401:K75"].fields["Right"] -local key_zero = ports[":lk401:K10"].fields["0 )"] -local key_ret = ports[":lk401:K68"].fields["Return"] -local key_del = ports[":lk401:K67"].fields["Delete"] -local key_q = ports[":lk401:K13"].fields["Q"] -local key_p = ports[":lk401:K22"].fields["P"] -local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"] -local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"] -local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"] -local cpu = machine.devices[':maincpu'] -local dbg = machine.debugger -local prog = cpu.spaces['program'] -if not dbg then - print("LOADER: debugger not enabled; pass -debug") - machine:exit() - return -end +H.config{ tablet = "INPUT" } +H.sequence(3100, 20, { + { ":lk401:K26", "A" }, + { ":lk401:K77", "Up" }, + { ":lk401:K76", "Down" }, + { ":lk401:K74", "Left" }, + { ":lk401:K75", "Right" }, + { ":lk401:K10", "0 )" }, + { ":lk401:K68", "Return" }, + { ":lk401:K67", "Delete" }, + { ":lk401:K13", "Q" }, + { ":lk401:K22", "P" }, +}) -local AT = tonumber(os.getenv("LOADER_AT") or "2500") -local ENDF = tonumber(os.getenv("LOADER_END") or "2700") -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") -local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence" -local SNAP = os.getenv("LOADER_SNAP") == "1" -local MODE = tonumber(os.getenv("LOADER_MODE") or "0") -local A8DESC = os.getenv("LOADER_A8_DESC") == "1" -local VERBOSE = os.getenv("LOADER_VERBOSE") == "1" +H.result_dump(function() + local n = H.r32(INPUT_MARK + 0x40) + local t = {} + for i = 0, n - 1 do t[#t + 1] = string.format("%02X", H.r32(INPUT_MARK + (3 + i) * 0x20)) end + print(string.format("INPUT: magic=%08X updates=%d n=%d bytes=%s", + H.r32(INPUT_MARK), H.r32(INPUT_MARK + 0x20), n, table.concat(t, " "))) +end) -local LOADER_BASE = 0x01880000 -local DESC = 0x01882C00 -local RESULT = 0x01882D00 -local RETURN_STUB = 0x01883100 -local RETURN_SP = 0x01883110 - -local function rd(p) return io.open(p, "rb") end -local function slurp(p) - local f, err = rd(p) - if not f then error("cannot open " .. p .. ": " .. tostring(err)) end - local s = f:read("*a") - f:close() - return s -end - -local function u16(s, o) - return s:byte(o + 1) + s:byte(o + 2) * 256 -end - -local function u32(s, o) - return s:byte(o + 1) + s:byte(o + 2) * 256 - + s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216 -end - -local function parse_coff(d) - local c = { data = d } - c.magic = u16(d, 0) - c.nscns = u16(d, 2) - c.opthdr = u16(d, 16) - c.sections = {} - if c.opthdr > 0 then - c.optmagic = u16(d, 20) - c.entry = u32(d, 20 + 16) - c.textstart = u32(d, 20 + 20) - c.datastart = u32(d, 20 + 24) - end - local base = 20 + c.opthdr - for i = 0, c.nscns - 1 do - local o = base + i * 40 - local name = d:sub(o + 1, o + 8):gsub("%z", "") - c.sections[#c.sections + 1] = { - name = name, - paddr = u32(d, o + 8), - size = u32(d, o + 16), -- bits - scnptr = u32(d, o + 20), - flags = u32(d, o + 36), - } - end - return c -end - -local function poke_bytes(bitaddr, data) - local n = math.floor((#data + 1) / 2) - for i = 0, n - 1 do - local lo = data:byte(i * 2 + 1) or 0 - local hi = data:byte(i * 2 + 2) or 0 - prog:write_u16(bitaddr + i * 16, lo + hi * 256) - end - return n -end - -local function verify_bytes(bitaddr, data) - local n = math.floor((#data + 1) / 2) - local bad = 0 - for i = 0, n - 1 do - local lo = data:byte(i * 2 + 1) or 0 - local hi = data:byte(i * 2 + 2) or 0 - if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then - bad = bad + 1 - end - end - return n, bad -end - -local function w32(a, v) prog:write_u32(a, v) end -local function key_step(frames, key) - if frames == 3100 then key_a:set_value(1) end - if frames == 3110 then key_a:set_value(0) end - if frames == 3120 then key_up:set_value(1) end - if frames == 3130 then key_up:set_value(0) end - if frames == 3140 then key_down:set_value(1) end - if frames == 3150 then key_down:set_value(0) end - if frames == 3160 then key_left:set_value(1) end - if frames == 3170 then key_left:set_value(0) end - if frames == 3180 then key_right:set_value(1) end - if frames == 3190 then key_right:set_value(0) end - if frames == 3200 then key_zero:set_value(1) end - if frames == 3210 then key_zero:set_value(0) end - if frames == 3220 then key_ret:set_value(1) end - if frames == 3230 then key_ret:set_value(0) end - if frames == 3240 then key_del:set_value(1) end - if frames == 3250 then key_del:set_value(0) end - if frames == 3260 then key_q:set_value(1) end - if frames == 3270 then key_q:set_value(0) end - if frames == 3280 then key_p:set_value(1) end - if frames == 3290 then key_p:set_value(0) end -end - -local function r32(a) - local ok, v = pcall(function() return prog:read_u32(a) end) - if ok then return v end - return 0xDEADBEEF -end - -local loader = slurp(BIN) -local coff = slurp(COFF) -local app = parse_coff(coff) - -print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE))) -print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF))) -print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s", - app.magic, app.nscns, app.opthdr, - app.optmagic and string.format("%04X", app.optmagic) or "none", - hex(app.entry or 0), hex(app.textstart or 0))) -for _, s in ipairs(app.sections) do - print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x", - s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags)) -end - -local TRACE = os.getenv("LOADER_TRACE") == "1" - -local frames = 0 -local injected = false -local done = false -local last_count = -1 - -local function status_name(v) - local t = { - [0] = "OK", - [1] = "bad descriptor", - [2] = "truncated COFF / section headers", - [3] = "bad COFF magic", - [4] = "section size not whole words", - [5] = "target outside RAM", - [6] = "target overlaps loader page or COFF buffer", - [7] = "relocation records present (unsupported)", - [8] = "target not in a free-list block", - [9] = "no entry point", - } - return t[v] or ("error " .. tostring(v)) -end - -local function result_dump() - local st = r32(RESULT + 0x000) - print(string.format( - "LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d", - st, status_name(st), hex(r32(RESULT + 0x020)), - hex(r32(RESULT + 0x040)), r32(RESULT + 0x060), - hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0), - hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0), - r32(RESULT + 0x100), hex(r32(RESULT + 0x120)), - r32(RESULT + 0x140), r32(RESULT + 0x160))) -end - -local function inject() - -- 1. loader itself - local n, bad = 0, 0 - poke_bytes(LOADER_BASE, loader) - n, bad = verify_bytes(LOADER_BASE, loader) - print(string.format("LOADER: loader poked %d words, readback %d bad at %s", - n, bad, hex(LOADER_BASE))) - - -- 2. COFF image - poke_bytes(BUF, coff) - n, bad = verify_bytes(BUF, coff) - print(string.format("LOADER: COFF poked %d words, readback %d bad at %s", - n, bad, hex(BUF))) - - -- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful - for _, s in ipairs(app.sections) do - if s.flags & 0x80 ~= 0 then - for i = 0, math.floor(s.size / 16) - 1 do - prog:write_u16(s.paddr + i * 16, 0xA5A5) - end - print(string.format("LOADER: pre-filled .bss %s..%s with A5A5", - hex(s.paddr), hex(s.paddr + s.size))) - end - end - - -- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin) - w32(DESC + 0x00, 0) - prog:write_u16(DESC + 0x00, 0x4C44) - prog:write_u16(DESC + 0x10, 0x5231) - w32(DESC + 0x20, BUF) - w32(DESC + 0x40, #coff) - w32(DESC + 0x60, MODE) - print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d", - hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10), - hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60))) - - -- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC - -- itself); mode 1 returns here, and the stub resumes the interrupted - -- PC so the caller yields and the scheduler can run the child. - local orig_pc = cpu.state['PC'].value - if MODE == 1 then - prog:write_u16(RETURN_STUB, 0xC080) -- jauc - prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF) - prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF) - else - prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self - end - - -- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at - -- the return slot so mode=1 would return to the spin stub. - -- Leave SP alone for mode 0: the loader pushes onto the current - -- process stack, then restores it and resumes at ORIG_PC. (mode 1 - -- would use the return stub.) - w32(0x01882A20, orig_pc) -- LD_ORIG_PC - print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc))) - cpu.state['A8'].value = A8DESC and DESC or 0 - if MODE == 1 then - -- Simulate a CALLA from the current process: push the return - -- address on the *firmware* stack (a real transport would do this). - local sp = cpu.state['SP'].value - 0x20 - w32(sp, RETURN_STUB) - cpu.state['SP'].value = sp - end - cpu.state['PC'].value = LOADER_BASE - injected = true - print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)", - frames, hex(LOADER_BASE), hex(cpu.state['SP'].value))) -end - -local function inspect() - result_dump() - - -- Verify the placement from RAM: .text/.data must equal the file bytes. - for _, s in ipairs(app.sections) do - if s.scnptr ~= 0 and s.size > 0 then - local nb = math.floor(s.size / 8) - local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb) - local _, bad = verify_bytes(s.paddr, bytes) - print(string.format("LOADER: placed %s at %s: %d bytes, %d differing", - s.name, hex(s.paddr), nb, bad)) - end - end - - local base = COFF:match("([^/]+)$") or COFF - if base:find("marker") then - print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d", - hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)), - hex(r32(0x018E0060)), r32(0x018E0080))) - elseif base:find("xhello") then - print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s", - r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)), - hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130)))) - end - if base:find("input") then - local n = r32(INPUT_MARK + 0x40) - local t = {} - for i = 0, n - 1 do t[#t + 1] = string.format("%02X", r32(INPUT_MARK + (3 + i) * 0x20)) end - print(string.format("INPUT: magic=%08X updates=%d n=%d bytes=%s", - r32(INPUT_MARK), r32(INPUT_MARK + 0x20), n, table.concat(t, " "))) - end - print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) -end - -emu.register_frame_done(function() - frames = frames + 1 - if frames == 2900 then tabx:set_value(700); taby:set_value(500); print("INPUT: tablet move") end - if frames == 3000 then tabb:set_value(1); print("INPUT: click down") end - if frames == 3020 then tabb:set_value(0); print("INPUT: click up") end - key_step(frames, nil) - if frames == AT and not injected then - inject() - if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end - end - if injected and TRACE and frames == AT + 1 then - dbg:command("trace off") - print("LOADER: trace written to /tmp/opencode/ld.tr") - end - if injected and not done 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). - end - if VERBOSE and frames % 25 == 0 then - result_dump() - end - if base_contains_marker then - local c = r32(0x018E0080) - if c ~= last_count then - if last_count ~= -1 then - print(string.format("LOADER: marker counter %d -> %d (child running)", - last_count, c)) - end - last_count = c - 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 - end - end - machine:exit() - end -end, "vt1200-loader") - --- (Lua has no forward use of a name defined later; set a small flag here.) -base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil - -dbg.visible_cpu = cpu -dbg.execution_state = 'run' +H.run() diff --git a/apps/termtris/tests/play.lua b/apps/termtris/tests/play.lua index 9e91d32..4787df8 100644 --- a/apps/termtris/tests/play.lua +++ b/apps/termtris/tests/play.lua @@ -1,350 +1,8 @@ --- loader.lua -- debugger-assisted COFF loader acceptance harness. --- --- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to --- the OS, then at frame LOADER_AT pokes: --- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin) --- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000) --- * the loader descriptor at 0x01882C00 --- and sets PC = 0x01880000. It does not use the network, the rdebug service --- or any firmware transport: this is the debugger-assisted milestone. --- --- The loader itself copies the COFF sections to their linked bit addresses and --- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes --- the input and reads the result; it never writes the app's code or markers. --- --- Env: --- LOADER_BIN loader payload (default dwt/loader/build/loader.bin) --- 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_OUT evidence directory (default dwt/loader/evidence) --- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0) --- LOADER_VERBOSE 1 = print the result block every 25 frames +-- play.lua -- load termtris, click it active, and snapshot the board. +-- play.sh checks LOADER: result status=0, the DIAG tick, and the snapshot. +local dir = (debug.getinfo(1, "S").source:gsub("^@", ""):match("^(.*)/") or ".") +local H = dofile(dir .. "/harness.lua") -local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end +H.config{ tablet = "PLAY" } -local machine = manager.machine -local PTICK = tonumber(os.getenv("DIAG_TICK") or "0") -local PQUIT = tonumber(os.getenv("DIAG_QUIT") or "0") -local ports = machine.ioport.ports -local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"] -local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"] -local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"] -local cpu = machine.devices[':maincpu'] -local dbg = machine.debugger -local prog = cpu.spaces['program'] - -if not dbg then - print("LOADER: debugger not enabled; pass -debug") - machine:exit() - return -end - -local AT = tonumber(os.getenv("LOADER_AT") or "2500") -local ENDF = tonumber(os.getenv("LOADER_END") or "2700") -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") -local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence" -local SNAP = os.getenv("LOADER_SNAP") == "1" -local MODE = tonumber(os.getenv("LOADER_MODE") or "0") -local A8DESC = os.getenv("LOADER_A8_DESC") == "1" -local VERBOSE = os.getenv("LOADER_VERBOSE") == "1" - -local LOADER_BASE = 0x01880000 -local DESC = 0x01882C00 -local RESULT = 0x01882D00 -local RETURN_STUB = 0x01883100 -local RETURN_SP = 0x01883110 - -local function rd(p) return io.open(p, "rb") end -local function slurp(p) - local f, err = rd(p) - if not f then error("cannot open " .. p .. ": " .. tostring(err)) end - local s = f:read("*a") - f:close() - return s -end - -local function u16(s, o) - return s:byte(o + 1) + s:byte(o + 2) * 256 -end - -local function u32(s, o) - return s:byte(o + 1) + s:byte(o + 2) * 256 - + s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216 -end - -local function parse_coff(d) - local c = { data = d } - c.magic = u16(d, 0) - c.nscns = u16(d, 2) - c.opthdr = u16(d, 16) - c.sections = {} - if c.opthdr > 0 then - c.optmagic = u16(d, 20) - c.entry = u32(d, 20 + 16) - c.textstart = u32(d, 20 + 20) - c.datastart = u32(d, 20 + 24) - end - local base = 20 + c.opthdr - for i = 0, c.nscns - 1 do - local o = base + i * 40 - local name = d:sub(o + 1, o + 8):gsub("%z", "") - c.sections[#c.sections + 1] = { - name = name, - paddr = u32(d, o + 8), - size = u32(d, o + 16), -- bits - scnptr = u32(d, o + 20), - flags = u32(d, o + 36), - } - end - return c -end - -local function poke_bytes(bitaddr, data) - local n = math.floor((#data + 1) / 2) - for i = 0, n - 1 do - local lo = data:byte(i * 2 + 1) or 0 - local hi = data:byte(i * 2 + 2) or 0 - prog:write_u16(bitaddr + i * 16, lo + hi * 256) - end - return n -end - -local function verify_bytes(bitaddr, data) - local n = math.floor((#data + 1) / 2) - local bad = 0 - for i = 0, n - 1 do - local lo = data:byte(i * 2 + 1) or 0 - local hi = data:byte(i * 2 + 2) or 0 - if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then - bad = bad + 1 - end - end - return n, bad -end - -local function w32(a, v) prog:write_u32(a, v) end -local function r32(a) - local ok, v = pcall(function() return prog:read_u32(a) end) - if ok then return v end - return 0xDEADBEEF -end - -local loader = slurp(BIN) -local coff = slurp(COFF) -local app = parse_coff(coff) - -print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE))) -print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF))) -print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s", - app.magic, app.nscns, app.opthdr, - app.optmagic and string.format("%04X", app.optmagic) or "none", - hex(app.entry or 0), hex(app.textstart or 0))) -for _, s in ipairs(app.sections) do - print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x", - s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags)) -end - -local TRACE = os.getenv("LOADER_TRACE") == "1" - -local frames = 0 -local injected = false -local done = false -local last_count = -1 - -local function status_name(v) - local t = { - [0] = "OK", - [1] = "bad descriptor", - [2] = "truncated COFF / section headers", - [3] = "bad COFF magic", - [4] = "section size not whole words", - [5] = "target outside RAM", - [6] = "target overlaps loader page or COFF buffer", - [7] = "relocation records present (unsupported)", - [8] = "target not in a free-list block", - [9] = "no entry point", - } - return t[v] or ("error " .. tostring(v)) -end - -local function result_dump() - local st = r32(RESULT + 0x000) - print(string.format( - "LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d", - st, status_name(st), hex(r32(RESULT + 0x020)), - hex(r32(RESULT + 0x040)), r32(RESULT + 0x060), - hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0), - hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0), - r32(RESULT + 0x100), hex(r32(RESULT + 0x120)), - r32(RESULT + 0x140), r32(RESULT + 0x160))) -end - -local function inject() - -- 1. loader itself - local n, bad = 0, 0 - poke_bytes(LOADER_BASE, loader) - n, bad = verify_bytes(LOADER_BASE, loader) - print(string.format("LOADER: loader poked %d words, readback %d bad at %s", - n, bad, hex(LOADER_BASE))) - - -- 2. COFF image - poke_bytes(BUF, coff) - n, bad = verify_bytes(BUF, coff) - print(string.format("LOADER: COFF poked %d words, readback %d bad at %s", - n, bad, hex(BUF))) - - -- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful - for _, s in ipairs(app.sections) do - if s.flags & 0x80 ~= 0 then - for i = 0, math.floor(s.size / 16) - 1 do - prog:write_u16(s.paddr + i * 16, 0xA5A5) - end - print(string.format("LOADER: pre-filled .bss %s..%s with A5A5", - hex(s.paddr), hex(s.paddr + s.size))) - end - end - - -- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin) - w32(DESC + 0x00, 0) - prog:write_u16(DESC + 0x00, 0x4C44) - prog:write_u16(DESC + 0x10, 0x5231) - w32(DESC + 0x20, BUF) - w32(DESC + 0x40, #coff) - w32(DESC + 0x60, MODE) - print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d", - hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10), - hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60))) - - -- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC - -- itself); mode 1 returns here, and the stub resumes the interrupted - -- PC so the caller yields and the scheduler can run the child. - local orig_pc = cpu.state['PC'].value - if MODE == 1 then - prog:write_u16(RETURN_STUB, 0xC080) -- jauc - prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF) - prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF) - else - prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self - end - - -- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at - -- the return slot so mode=1 would return to the spin stub. - -- Leave SP alone for mode 0: the loader pushes onto the current - -- process stack, then restores it and resumes at ORIG_PC. (mode 1 - -- would use the return stub.) - w32(0x01882A20, orig_pc) -- LD_ORIG_PC - print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc))) - cpu.state['A8'].value = A8DESC and DESC or 0 - if MODE == 1 then - -- Simulate a CALLA from the current process: push the return - -- address on the *firmware* stack (a real transport would do this). - local sp = cpu.state['SP'].value - 0x20 - w32(sp, RETURN_STUB) - cpu.state['SP'].value = sp - end - cpu.state['PC'].value = LOADER_BASE - injected = true - print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)", - frames, hex(LOADER_BASE), hex(cpu.state['SP'].value))) -end - -local function inspect() - result_dump() - - -- Verify the placement from RAM: .text/.data must equal the file bytes. - for _, s in ipairs(app.sections) do - if s.scnptr ~= 0 and s.size > 0 then - local nb = math.floor(s.size / 8) - local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb) - local _, bad = verify_bytes(s.paddr, bytes) - print(string.format("LOADER: placed %s at %s: %d bytes, %d differing", - s.name, hex(s.paddr), nb, bad)) - end - end - - local base = COFF:match("([^/]+)$") or COFF - if base:find("marker") then - print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d", - hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)), - hex(r32(0x018E0060)), r32(0x018E0080))) - elseif base:find("xhello") then - print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s", - r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)), - hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130)))) - end - if PTICK ~= 0 or PQUIT ~= 0 then - print(string.format("DIAG: tick=%d quit=%d", PTICK ~= 0 and r32(PTICK) or -1, PQUIT ~= 0 and r32(PQUIT) or -1)) - end - print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) -end - -emu.register_frame_done(function() - frames = frames + 1 - if frames == 2900 then tabx:set_value(700); taby:set_value(500); print("PLAY: tablet move") end - if frames == 3000 then tabb:set_value(1); print("PLAY: click down") end - if frames == 3020 then tabb:set_value(0); print("PLAY: click up") end - if frames == AT and not injected then - inject() - if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end - end - if injected and TRACE and frames == AT + 1 then - dbg:command("trace off") - print("LOADER: trace written to /tmp/opencode/ld.tr") - end - if injected and not done 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). - end - if VERBOSE and frames % 25 == 0 then - result_dump() - end - if base_contains_marker then - local c = r32(0x018E0080) - if c ~= last_count then - if last_count ~= -1 then - print(string.format("LOADER: marker counter %d -> %d (child running)", - last_count, c)) - end - last_count = c - 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 - end - end - machine:exit() - end -end, "vt1200-loader") - --- (Lua has no forward use of a name defined later; set a small flag here.) -base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil - -dbg.visible_cpu = cpu -dbg.execution_state = 'run' +H.run() diff --git a/apps/termtris/tests/play_keys.lua b/apps/termtris/tests/play_keys.lua index 38f5f77..26bb831 100644 --- a/apps/termtris/tests/play_keys.lua +++ b/apps/termtris/tests/play_keys.lua @@ -1,390 +1,34 @@ --- loader.lua -- debugger-assisted COFF loader acceptance harness. --- --- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to --- the OS, then at frame LOADER_AT pokes: --- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin) --- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000) --- * the loader descriptor at 0x01882C00 --- and sets PC = 0x01880000. It does not use the network, the rdebug service --- or any firmware transport: this is the debugger-assisted milestone. --- --- The loader itself copies the COFF sections to their linked bit addresses and --- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes --- the input and reads the result; it never writes the app's code or markers. --- --- Env: --- LOADER_BIN loader payload (default dwt/loader/build/loader.bin) --- 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_OUT evidence directory (default dwt/loader/evidence) --- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0) --- LOADER_VERBOSE 1 = print the result block every 25 frames +-- play_keys.lua -- drive the real game through its move, panels and quit +-- paths. play_keys.sh runs one PLAY_MODE per invocation and parses the DIAG +-- loops/events/last/focus/rev/quit line. +local dir = (debug.getinfo(1, "S").source:gsub("^@", ""):match("^(.*)/") or ".") +local H = dofile(dir .. "/harness.lua") -local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end +local MODE = os.getenv("PLAY_MODE") or "move" +print("PLAYKEYS: mode=" .. MODE) -local machine = manager.machine -local PLAY_MODE = os.getenv("PLAY_MODE") or "move" -print("PLAYKEYS: mode="..PLAY_MODE) -local DL = tonumber(os.getenv("DIAG_LOOPS") or "0") -local DE = tonumber(os.getenv("DIAG_EVENTS") or "0") -local DT = tonumber(os.getenv("DIAG_LAST") or "0") -local DF = tonumber(os.getenv("DIAG_FOCUS") or "0") -local DR = tonumber(os.getenv("DIAG_REV") or "0") -local DQ = tonumber(os.getenv("DIAG_QUIT") or "0") -local DTICK = tonumber(os.getenv("DIAG_TICK") or "0") -local ports = machine.ioport.ports -local key_a = ports[":lk401:K26"].fields["A"] -local key_d = ports[":lk401:K28"].fields["D"] -local key_s = ports[":lk401:K27"].fields["S"] -local key_w = ports[":lk401:K14"].fields["W"] -local key_0 = ports[":lk401:K10"].fields["0 )"] -local key_p = ports[":lk401:K22"].fields["P"] -local key_r = ports[":lk401:K16"].fields["R"] -local key_h = ports[":lk401:K31"].fields["H"] -local key_q = ports[":lk401:K13"].fields["Q"] -local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"] -local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"] -local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"] -local cpu = machine.devices[':maincpu'] -local dbg = machine.debugger -local prog = cpu.spaces['program'] +H.key(2900, ":tablet:TABLET_X", "Tablet X", 700) +H.key(2900, ":tablet:TABLET_Y", "Tablet Y", 500, "PLAYKEYS: move") +H.key(3000, ":tablet:TABLET_BTN", "P1 Button 1", 1, "PLAYKEYS: click down") +H.key(3020, ":tablet:TABLET_BTN", "P1 Button 1", 0, "PLAYKEYS: click up") -if not dbg then - print("LOADER: debugger not enabled; pass -debug") - machine:exit() - return +if MODE == "move" then + H.sequence(3100, 20, { + { ":lk401:K26", "A" }, + { ":lk401:K28", "D" }, + { ":lk401:K27", "S" }, + { ":lk401:K14", "W" }, + { ":lk401:K10", "0 )" }, + }) +elseif MODE == "panels" then + H.sequence(3100, 20, { + { ":lk401:K31", "H" }, + { ":lk401:K16", "R" }, + }) +elseif MODE == "quit" then + H.message(3095, "PLAYKEYS: quit arm") + H.key(3100, ":lk401:K13", "Q", 1, "PLAYKEYS: Q down") + H.key(3110, ":lk401:K13", "Q", 0, "PLAYKEYS: Q up") end -local AT = tonumber(os.getenv("LOADER_AT") or "2500") -local ENDF = tonumber(os.getenv("LOADER_END") or "2700") -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") -local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence" -local SNAP = os.getenv("LOADER_SNAP") == "1" -local MODE = tonumber(os.getenv("LOADER_MODE") or "0") -local A8DESC = os.getenv("LOADER_A8_DESC") == "1" -local VERBOSE = os.getenv("LOADER_VERBOSE") == "1" - -local LOADER_BASE = 0x01880000 -local DESC = 0x01882C00 -local RESULT = 0x01882D00 -local RETURN_STUB = 0x01883100 -local RETURN_SP = 0x01883110 - -local function rd(p) return io.open(p, "rb") end -local function slurp(p) - local f, err = rd(p) - if not f then error("cannot open " .. p .. ": " .. tostring(err)) end - local s = f:read("*a") - f:close() - return s -end - -local function u16(s, o) - return s:byte(o + 1) + s:byte(o + 2) * 256 -end - -local function u32(s, o) - return s:byte(o + 1) + s:byte(o + 2) * 256 - + s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216 -end - -local function parse_coff(d) - local c = { data = d } - c.magic = u16(d, 0) - c.nscns = u16(d, 2) - c.opthdr = u16(d, 16) - c.sections = {} - if c.opthdr > 0 then - c.optmagic = u16(d, 20) - c.entry = u32(d, 20 + 16) - c.textstart = u32(d, 20 + 20) - c.datastart = u32(d, 20 + 24) - end - local base = 20 + c.opthdr - for i = 0, c.nscns - 1 do - local o = base + i * 40 - local name = d:sub(o + 1, o + 8):gsub("%z", "") - c.sections[#c.sections + 1] = { - name = name, - paddr = u32(d, o + 8), - size = u32(d, o + 16), -- bits - scnptr = u32(d, o + 20), - flags = u32(d, o + 36), - } - end - return c -end - -local function poke_bytes(bitaddr, data) - local n = math.floor((#data + 1) / 2) - for i = 0, n - 1 do - local lo = data:byte(i * 2 + 1) or 0 - local hi = data:byte(i * 2 + 2) or 0 - prog:write_u16(bitaddr + i * 16, lo + hi * 256) - end - return n -end - -local function verify_bytes(bitaddr, data) - local n = math.floor((#data + 1) / 2) - local bad = 0 - for i = 0, n - 1 do - local lo = data:byte(i * 2 + 1) or 0 - local hi = data:byte(i * 2 + 2) or 0 - if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then - bad = bad + 1 - end - end - return n, bad -end - -local function w32(a, v) prog:write_u32(a, v) end -local function r32(a) - local ok, v = pcall(function() return prog:read_u32(a) end) - if ok then return v end - return 0xDEADBEEF -end - -local loader = slurp(BIN) -local coff = slurp(COFF) -local app = parse_coff(coff) - -print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE))) -print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF))) -print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s", - app.magic, app.nscns, app.opthdr, - app.optmagic and string.format("%04X", app.optmagic) or "none", - hex(app.entry or 0), hex(app.textstart or 0))) -for _, s in ipairs(app.sections) do - print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x", - s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags)) -end - -local TRACE = os.getenv("LOADER_TRACE") == "1" - -local frames = 0 -local injected = false -local done = false -local last_count = -1 - -local function status_name(v) - local t = { - [0] = "OK", - [1] = "bad descriptor", - [2] = "truncated COFF / section headers", - [3] = "bad COFF magic", - [4] = "section size not whole words", - [5] = "target outside RAM", - [6] = "target overlaps loader page or COFF buffer", - [7] = "relocation records present (unsupported)", - [8] = "target not in a free-list block", - [9] = "no entry point", - } - return t[v] or ("error " .. tostring(v)) -end - -local function result_dump() - local st = r32(RESULT + 0x000) - print(string.format( - "LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d", - st, status_name(st), hex(r32(RESULT + 0x020)), - hex(r32(RESULT + 0x040)), r32(RESULT + 0x060), - hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0), - hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0), - r32(RESULT + 0x100), hex(r32(RESULT + 0x120)), - r32(RESULT + 0x140), r32(RESULT + 0x160))) -end - -local function inject() - -- 1. loader itself - local n, bad = 0, 0 - poke_bytes(LOADER_BASE, loader) - n, bad = verify_bytes(LOADER_BASE, loader) - print(string.format("LOADER: loader poked %d words, readback %d bad at %s", - n, bad, hex(LOADER_BASE))) - - -- 2. COFF image - poke_bytes(BUF, coff) - n, bad = verify_bytes(BUF, coff) - print(string.format("LOADER: COFF poked %d words, readback %d bad at %s", - n, bad, hex(BUF))) - - -- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful - for _, s in ipairs(app.sections) do - if s.flags & 0x80 ~= 0 then - for i = 0, math.floor(s.size / 16) - 1 do - prog:write_u16(s.paddr + i * 16, 0xA5A5) - end - print(string.format("LOADER: pre-filled .bss %s..%s with A5A5", - hex(s.paddr), hex(s.paddr + s.size))) - end - end - - -- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin) - w32(DESC + 0x00, 0) - prog:write_u16(DESC + 0x00, 0x4C44) - prog:write_u16(DESC + 0x10, 0x5231) - w32(DESC + 0x20, BUF) - w32(DESC + 0x40, #coff) - w32(DESC + 0x60, MODE) - print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d", - hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10), - hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60))) - - -- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC - -- itself); mode 1 returns here, and the stub resumes the interrupted - -- PC so the caller yields and the scheduler can run the child. - local orig_pc = cpu.state['PC'].value - if MODE == 1 then - prog:write_u16(RETURN_STUB, 0xC080) -- jauc - prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF) - prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF) - else - prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self - end - - -- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at - -- the return slot so mode=1 would return to the spin stub. - -- Leave SP alone for mode 0: the loader pushes onto the current - -- process stack, then restores it and resumes at ORIG_PC. (mode 1 - -- would use the return stub.) - w32(0x01882A20, orig_pc) -- LD_ORIG_PC - print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc))) - cpu.state['A8'].value = A8DESC and DESC or 0 - if MODE == 1 then - -- Simulate a CALLA from the current process: push the return - -- address on the *firmware* stack (a real transport would do this). - local sp = cpu.state['SP'].value - 0x20 - w32(sp, RETURN_STUB) - cpu.state['SP'].value = sp - end - cpu.state['PC'].value = LOADER_BASE - injected = true - print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)", - frames, hex(LOADER_BASE), hex(cpu.state['SP'].value))) -end - -local function inspect() - result_dump() - - -- Verify the placement from RAM: .text/.data must equal the file bytes. - for _, s in ipairs(app.sections) do - if s.scnptr ~= 0 and s.size > 0 then - local nb = math.floor(s.size / 8) - local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb) - local _, bad = verify_bytes(s.paddr, bytes) - print(string.format("LOADER: placed %s at %s: %d bytes, %d differing", - s.name, hex(s.paddr), nb, bad)) - end - end - - local base = COFF:match("([^/]+)$") or COFF - if base:find("marker") then - print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d", - hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)), - hex(r32(0x018E0060)), r32(0x018E0080))) - elseif base:find("xhello") then - print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s", - r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)), - hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130)))) - end - if DL ~= 0 then - print(string.format("DIAG: loops=%d events=%d last=%d focus=%08X rev=%d quit=%d", r32(DL), r32(DE), r32(DT), DF ~= 0 and r32(DF) or 0, DR ~= 0 and r32(DR) or 0, DQ ~= 0 and r32(DQ) or 0)) - if DTICK ~= 0 then - print(string.format("DIAG: tick=%d", r32(DTICK))) - end - end - print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) -end - -emu.register_frame_done(function() - frames = frames + 1 - if frames == 2900 then tabx:set_value(700); taby:set_value(500); print("PLAYKEYS: move") end - if frames == 3000 then tabb:set_value(1); print("PLAYKEYS: click down") end - if frames == 3020 then tabb:set_value(0); print("PLAYKEYS: click up") end - if PLAY_MODE == "move" then - if frames == 3100 then key_a:set_value(1) end - if frames == 3110 then key_a:set_value(0) end - if frames == 3120 then key_d:set_value(1) end - if frames == 3130 then key_d:set_value(0) end - if frames == 3140 then key_s:set_value(1) end - if frames == 3150 then key_s:set_value(0) end - if frames == 3160 then key_w:set_value(1) end - if frames == 3170 then key_w:set_value(0) end - if frames == 3180 then key_0:set_value(1) end - if frames == 3190 then key_0:set_value(0) end - elseif PLAY_MODE == "panels" then - if frames == 3100 then key_h:set_value(1) end - if frames == 3110 then key_h:set_value(0) end - if frames == 3120 then key_r:set_value(1) end - if frames == 3130 then key_r:set_value(0) end - elseif PLAY_MODE == "quit" then - if frames == 3095 then print("PLAYKEYS: quit arm") end - if frames == 3100 then print("PLAYKEYS: Q down"); key_q:set_value(1) end - if frames == 3110 then print("PLAYKEYS: Q up"); key_q:set_value(0) end - end - if frames == AT and not injected then - inject() - if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end - end - if injected and TRACE and frames == AT + 1 then - dbg:command("trace off") - print("LOADER: trace written to /tmp/opencode/ld.tr") - end - if injected and not done 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). - end - if VERBOSE and frames % 25 == 0 then - result_dump() - end - if base_contains_marker then - local c = r32(0x018E0080) - if c ~= last_count then - if last_count ~= -1 then - print(string.format("LOADER: marker counter %d -> %d (child running)", - last_count, c)) - end - last_count = c - 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 - end - end - machine:exit() - end -end, "vt1200-loader") - --- (Lua has no forward use of a name defined later; set a small flag here.) -base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil - -dbg.visible_cpu = cpu -dbg.execution_state = 'run' +H.run() diff --git a/apps/termtris/tests/render.lua b/apps/termtris/tests/render.lua index 2931358..12cc707 100644 --- a/apps/termtris/tests/render.lua +++ b/apps/termtris/tests/render.lua @@ -1,347 +1,17 @@ --- loader.lua -- debugger-assisted COFF loader acceptance harness. --- --- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to --- the OS, then at frame LOADER_AT pokes: --- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin) --- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000) --- * the loader descriptor at 0x01882C00 --- and sets PC = 0x01880000. It does not use the network, the rdebug service --- or any firmware transport: this is the debugger-assisted milestone. --- --- The loader itself copies the COFF sections to their linked bit addresses and --- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes --- the input and reads the result; it never writes the app's code or markers. --- --- Env: --- LOADER_BIN loader payload (default dwt/loader/build/loader.bin) --- 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_OUT evidence directory (default dwt/loader/evidence) --- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0) --- LOADER_VERBOSE 1 = print the result block every 25 frames +-- render.lua -- paint the cell-grid test pattern and snapshot the window. The +-- PNG is the evidence render.sh keeps. +local dir = (debug.getinfo(1, "S").source:gsub("^@", ""):match("^(.*)/") or ".") +local H = dofile(dir .. "/harness.lua") -local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end - -local machine = manager.machine local RENDER_MARK = tonumber(os.getenv("RENDER_MARK") or "0x01A08000") local X11P = tonumber(os.getenv("X11_PROGRESS") or "0") -local cpu = machine.devices[':maincpu'] -local dbg = machine.debugger -local prog = cpu.spaces['program'] -if not dbg then - print("LOADER: debugger not enabled; pass -debug") - machine:exit() - return -end +H.result_dump(function() + print(string.format("RENDER: magic=%08X dpy=%08X win=%08X gc=%08X flus=%08X progress=%08X", + H.r32(RENDER_MARK), H.r32(RENDER_MARK + 0x20), H.r32(RENDER_MARK + 0x40), + H.r32(RENDER_MARK + 0x60), H.r32(RENDER_MARK + 0x80), + X11P ~= 0 and H.r32(X11P) or 0)) + print(string.format("RENDER: pending=%d", H.r32(RENDER_MARK + 0xA0))) +end) -local AT = tonumber(os.getenv("LOADER_AT") or "2500") -local ENDF = tonumber(os.getenv("LOADER_END") or "2700") -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") -local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence" -local SNAP = os.getenv("LOADER_SNAP") == "1" -local MODE = tonumber(os.getenv("LOADER_MODE") or "0") -local A8DESC = os.getenv("LOADER_A8_DESC") == "1" -local VERBOSE = os.getenv("LOADER_VERBOSE") == "1" - -local LOADER_BASE = 0x01880000 -local DESC = 0x01882C00 -local RESULT = 0x01882D00 -local RETURN_STUB = 0x01883100 -local RETURN_SP = 0x01883110 - -local function rd(p) return io.open(p, "rb") end -local function slurp(p) - local f, err = rd(p) - if not f then error("cannot open " .. p .. ": " .. tostring(err)) end - local s = f:read("*a") - f:close() - return s -end - -local function u16(s, o) - return s:byte(o + 1) + s:byte(o + 2) * 256 -end - -local function u32(s, o) - return s:byte(o + 1) + s:byte(o + 2) * 256 - + s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216 -end - -local function parse_coff(d) - local c = { data = d } - c.magic = u16(d, 0) - c.nscns = u16(d, 2) - c.opthdr = u16(d, 16) - c.sections = {} - if c.opthdr > 0 then - c.optmagic = u16(d, 20) - c.entry = u32(d, 20 + 16) - c.textstart = u32(d, 20 + 20) - c.datastart = u32(d, 20 + 24) - end - local base = 20 + c.opthdr - for i = 0, c.nscns - 1 do - local o = base + i * 40 - local name = d:sub(o + 1, o + 8):gsub("%z", "") - c.sections[#c.sections + 1] = { - name = name, - paddr = u32(d, o + 8), - size = u32(d, o + 16), -- bits - scnptr = u32(d, o + 20), - flags = u32(d, o + 36), - } - end - return c -end - -local function poke_bytes(bitaddr, data) - local n = math.floor((#data + 1) / 2) - for i = 0, n - 1 do - local lo = data:byte(i * 2 + 1) or 0 - local hi = data:byte(i * 2 + 2) or 0 - prog:write_u16(bitaddr + i * 16, lo + hi * 256) - end - return n -end - -local function verify_bytes(bitaddr, data) - local n = math.floor((#data + 1) / 2) - local bad = 0 - for i = 0, n - 1 do - local lo = data:byte(i * 2 + 1) or 0 - local hi = data:byte(i * 2 + 2) or 0 - if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then - bad = bad + 1 - end - end - return n, bad -end - -local function w32(a, v) prog:write_u32(a, v) end -local function r32(a) - local ok, v = pcall(function() return prog:read_u32(a) end) - if ok then return v end - return 0xDEADBEEF -end - -local loader = slurp(BIN) -local coff = slurp(COFF) -local app = parse_coff(coff) - -print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE))) -print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF))) -print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s", - app.magic, app.nscns, app.opthdr, - app.optmagic and string.format("%04X", app.optmagic) or "none", - hex(app.entry or 0), hex(app.textstart or 0))) -for _, s in ipairs(app.sections) do - print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x", - s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags)) -end - -local TRACE = os.getenv("LOADER_TRACE") == "1" - -local frames = 0 -local injected = false -local done = false -local last_count = -1 - -local function status_name(v) - local t = { - [0] = "OK", - [1] = "bad descriptor", - [2] = "truncated COFF / section headers", - [3] = "bad COFF magic", - [4] = "section size not whole words", - [5] = "target outside RAM", - [6] = "target overlaps loader page or COFF buffer", - [7] = "relocation records present (unsupported)", - [8] = "target not in a free-list block", - [9] = "no entry point", - } - return t[v] or ("error " .. tostring(v)) -end - -local function result_dump() - local st = r32(RESULT + 0x000) - print(string.format( - "LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d", - st, status_name(st), hex(r32(RESULT + 0x020)), - hex(r32(RESULT + 0x040)), r32(RESULT + 0x060), - hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0), - hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0), - r32(RESULT + 0x100), hex(r32(RESULT + 0x120)), - r32(RESULT + 0x140), r32(RESULT + 0x160))) -end - -local function inject() - -- 1. loader itself - local n, bad = 0, 0 - poke_bytes(LOADER_BASE, loader) - n, bad = verify_bytes(LOADER_BASE, loader) - print(string.format("LOADER: loader poked %d words, readback %d bad at %s", - n, bad, hex(LOADER_BASE))) - - -- 2. COFF image - poke_bytes(BUF, coff) - n, bad = verify_bytes(BUF, coff) - print(string.format("LOADER: COFF poked %d words, readback %d bad at %s", - n, bad, hex(BUF))) - - -- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful - for _, s in ipairs(app.sections) do - if s.flags & 0x80 ~= 0 then - for i = 0, math.floor(s.size / 16) - 1 do - prog:write_u16(s.paddr + i * 16, 0xA5A5) - end - print(string.format("LOADER: pre-filled .bss %s..%s with A5A5", - hex(s.paddr), hex(s.paddr + s.size))) - end - end - - -- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin) - w32(DESC + 0x00, 0) - prog:write_u16(DESC + 0x00, 0x4C44) - prog:write_u16(DESC + 0x10, 0x5231) - w32(DESC + 0x20, BUF) - w32(DESC + 0x40, #coff) - w32(DESC + 0x60, MODE) - print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d", - hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10), - hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60))) - - -- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC - -- itself); mode 1 returns here, and the stub resumes the interrupted - -- PC so the caller yields and the scheduler can run the child. - local orig_pc = cpu.state['PC'].value - if MODE == 1 then - prog:write_u16(RETURN_STUB, 0xC080) -- jauc - prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF) - prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF) - else - prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self - end - - -- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at - -- the return slot so mode=1 would return to the spin stub. - -- Leave SP alone for mode 0: the loader pushes onto the current - -- process stack, then restores it and resumes at ORIG_PC. (mode 1 - -- would use the return stub.) - w32(0x01882A20, orig_pc) -- LD_ORIG_PC - print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc))) - cpu.state['A8'].value = A8DESC and DESC or 0 - if MODE == 1 then - -- Simulate a CALLA from the current process: push the return - -- address on the *firmware* stack (a real transport would do this). - local sp = cpu.state['SP'].value - 0x20 - w32(sp, RETURN_STUB) - cpu.state['SP'].value = sp - end - cpu.state['PC'].value = LOADER_BASE - injected = true - print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)", - frames, hex(LOADER_BASE), hex(cpu.state['SP'].value))) -end - -local function inspect() - result_dump() - - -- Verify the placement from RAM: .text/.data must equal the file bytes. - for _, s in ipairs(app.sections) do - if s.scnptr ~= 0 and s.size > 0 then - local nb = math.floor(s.size / 8) - local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb) - local _, bad = verify_bytes(s.paddr, bytes) - print(string.format("LOADER: placed %s at %s: %d bytes, %d differing", - s.name, hex(s.paddr), nb, bad)) - end - end - - local base = COFF:match("([^/]+)$") or COFF - if base:find("marker") then - print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d", - hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)), - hex(r32(0x018E0060)), r32(0x018E0080))) - elseif base:find("xhello") then - print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s", - r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)), - hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130)))) - end - if base:find("render") then - print(string.format("RENDER: magic=%08X dpy=%08X win=%08X gc=%08X flus=%08X progress=%08X", - r32(RENDER_MARK), r32(RENDER_MARK + 0x20), r32(RENDER_MARK + 0x40), - r32(RENDER_MARK + 0x60), r32(RENDER_MARK + 0x80), - X11P ~= 0 and r32(X11P) or 0)) - print(string.format("RENDER: pending=%d", r32(RENDER_MARK + 0xA0))) - end - print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) -end - -emu.register_frame_done(function() - frames = frames + 1 - if frames == AT and not injected then - inject() - if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end - end - if injected and TRACE and frames == AT + 1 then - dbg:command("trace off") - print("LOADER: trace written to /tmp/opencode/ld.tr") - end - if injected and not done 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). - end - if VERBOSE and frames % 25 == 0 then - result_dump() - end - if base_contains_marker then - local c = r32(0x018E0080) - if c ~= last_count then - if last_count ~= -1 then - print(string.format("LOADER: marker counter %d -> %d (child running)", - last_count, c)) - end - last_count = c - 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 - end - end - machine:exit() - end -end, "vt1200-loader") - --- (Lua has no forward use of a name defined later; set a small flag here.) -base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil - -dbg.visible_cpu = cpu -dbg.execution_state = 'run' +H.run() diff --git a/apps/termtris/tests/smoke.lua b/apps/termtris/tests/smoke.lua index 9e05357..d27f6ea 100644 --- a/apps/termtris/tests/smoke.lua +++ b/apps/termtris/tests/smoke.lua @@ -1,355 +1,19 @@ --- loader.lua -- debugger-assisted COFF loader acceptance harness. --- --- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to --- the OS, then at frame LOADER_AT pokes: --- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin) --- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000) --- * the loader descriptor at 0x01882C00 --- and sets PC = 0x01880000. It does not use the network, the rdebug service --- or any firmware transport: this is the debugger-assisted milestone. --- --- The loader itself copies the COFF sections to their linked bit addresses and --- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes --- the input and reads the result; it never writes the app's code or markers. --- --- Env: --- LOADER_BIN loader payload (default dwt/loader/build/loader.bin) --- 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_OUT evidence directory (default dwt/loader/evidence) --- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0) --- LOADER_VERBOSE 1 = print the result block every 25 frames +-- smoke.lua -- Xlib key delivery: click the window, press A, and require a +-- real KeyPress plus XLookupString('a'). smoke.sh greps the SMOKE: line. +local dir = (debug.getinfo(1, "S").source:gsub("^@", ""):match("^(.*)/") or ".") +local H = dofile(dir .. "/harness.lua") -local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end - -local machine = manager.machine local MARK = tonumber(os.getenv("SMOKE_MARK") or "0x01A00000") -local ports = machine.ioport.ports -local key_a = ports[":lk401:K26"].fields["A"] -local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"] -local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"] -local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"] -local cpu = machine.devices[':maincpu'] -local dbg = machine.debugger -local prog = cpu.spaces['program'] -if not dbg then - print("LOADER: debugger not enabled; pass -debug") - machine:exit() - return -end +H.config{ tablet = "SMOKE" } +H.key(3100, ":lk401:K26", "A", 1, "SMOKE: A down") +H.key(3120, ":lk401:K26", "A", 0, "SMOKE: A up") -local AT = tonumber(os.getenv("LOADER_AT") or "2500") -local ENDF = tonumber(os.getenv("LOADER_END") or "2700") -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") -local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence" -local SNAP = os.getenv("LOADER_SNAP") == "1" -local MODE = tonumber(os.getenv("LOADER_MODE") or "0") -local A8DESC = os.getenv("LOADER_A8_DESC") == "1" -local VERBOSE = os.getenv("LOADER_VERBOSE") == "1" +H.result_dump(function() + print(string.format("SMOKE: magic=%08X dpy=%08X win=%08X n=%d type=%d keycode=%d lookup_n=%d ch=%02X ks=%04X keymark=%08X", + H.r32(MARK + 0x00), H.r32(MARK + 0x20), H.r32(MARK + 0x40), H.r32(MARK + 0x60), + H.r32(MARK + 0x80), H.r32(MARK + 0xA0), H.r32(MARK + 0xC0), H.r32(MARK + 0xE0), + H.r32(MARK + 0x100), H.r32(MARK + 0x120))) +end) -local LOADER_BASE = 0x01880000 -local DESC = 0x01882C00 -local RESULT = 0x01882D00 -local RETURN_STUB = 0x01883100 -local RETURN_SP = 0x01883110 - -local function rd(p) return io.open(p, "rb") end -local function slurp(p) - local f, err = rd(p) - if not f then error("cannot open " .. p .. ": " .. tostring(err)) end - local s = f:read("*a") - f:close() - return s -end - -local function u16(s, o) - return s:byte(o + 1) + s:byte(o + 2) * 256 -end - -local function u32(s, o) - return s:byte(o + 1) + s:byte(o + 2) * 256 - + s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216 -end - -local function parse_coff(d) - local c = { data = d } - c.magic = u16(d, 0) - c.nscns = u16(d, 2) - c.opthdr = u16(d, 16) - c.sections = {} - if c.opthdr > 0 then - c.optmagic = u16(d, 20) - c.entry = u32(d, 20 + 16) - c.textstart = u32(d, 20 + 20) - c.datastart = u32(d, 20 + 24) - end - local base = 20 + c.opthdr - for i = 0, c.nscns - 1 do - local o = base + i * 40 - local name = d:sub(o + 1, o + 8):gsub("%z", "") - c.sections[#c.sections + 1] = { - name = name, - paddr = u32(d, o + 8), - size = u32(d, o + 16), -- bits - scnptr = u32(d, o + 20), - flags = u32(d, o + 36), - } - end - return c -end - -local function poke_bytes(bitaddr, data) - local n = math.floor((#data + 1) / 2) - for i = 0, n - 1 do - local lo = data:byte(i * 2 + 1) or 0 - local hi = data:byte(i * 2 + 2) or 0 - prog:write_u16(bitaddr + i * 16, lo + hi * 256) - end - return n -end - -local function verify_bytes(bitaddr, data) - local n = math.floor((#data + 1) / 2) - local bad = 0 - for i = 0, n - 1 do - local lo = data:byte(i * 2 + 1) or 0 - local hi = data:byte(i * 2 + 2) or 0 - if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then - bad = bad + 1 - end - end - return n, bad -end - -local function w32(a, v) prog:write_u32(a, v) end -local function r32(a) - local ok, v = pcall(function() return prog:read_u32(a) end) - if ok then return v end - return 0xDEADBEEF -end - -local loader = slurp(BIN) -local coff = slurp(COFF) -local app = parse_coff(coff) - -print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE))) -print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF))) -print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s", - app.magic, app.nscns, app.opthdr, - app.optmagic and string.format("%04X", app.optmagic) or "none", - hex(app.entry or 0), hex(app.textstart or 0))) -for _, s in ipairs(app.sections) do - print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x", - s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags)) -end - -local TRACE = os.getenv("LOADER_TRACE") == "1" - -local frames = 0 -local injected = false -local done = false -local last_count = -1 - -local function status_name(v) - local t = { - [0] = "OK", - [1] = "bad descriptor", - [2] = "truncated COFF / section headers", - [3] = "bad COFF magic", - [4] = "section size not whole words", - [5] = "target outside RAM", - [6] = "target overlaps loader page or COFF buffer", - [7] = "relocation records present (unsupported)", - [8] = "target not in a free-list block", - [9] = "no entry point", - } - return t[v] or ("error " .. tostring(v)) -end - -local function result_dump() - local st = r32(RESULT + 0x000) - print(string.format( - "LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d", - st, status_name(st), hex(r32(RESULT + 0x020)), - hex(r32(RESULT + 0x040)), r32(RESULT + 0x060), - hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0), - hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0), - r32(RESULT + 0x100), hex(r32(RESULT + 0x120)), - r32(RESULT + 0x140), r32(RESULT + 0x160))) -end - -local function inject() - -- 1. loader itself - local n, bad = 0, 0 - poke_bytes(LOADER_BASE, loader) - n, bad = verify_bytes(LOADER_BASE, loader) - print(string.format("LOADER: loader poked %d words, readback %d bad at %s", - n, bad, hex(LOADER_BASE))) - - -- 2. COFF image - poke_bytes(BUF, coff) - n, bad = verify_bytes(BUF, coff) - print(string.format("LOADER: COFF poked %d words, readback %d bad at %s", - n, bad, hex(BUF))) - - -- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful - for _, s in ipairs(app.sections) do - if s.flags & 0x80 ~= 0 then - for i = 0, math.floor(s.size / 16) - 1 do - prog:write_u16(s.paddr + i * 16, 0xA5A5) - end - print(string.format("LOADER: pre-filled .bss %s..%s with A5A5", - hex(s.paddr), hex(s.paddr + s.size))) - end - end - - -- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin) - w32(DESC + 0x00, 0) - prog:write_u16(DESC + 0x00, 0x4C44) - prog:write_u16(DESC + 0x10, 0x5231) - w32(DESC + 0x20, BUF) - w32(DESC + 0x40, #coff) - w32(DESC + 0x60, MODE) - print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d", - hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10), - hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60))) - - -- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC - -- itself); mode 1 returns here, and the stub resumes the interrupted - -- PC so the caller yields and the scheduler can run the child. - local orig_pc = cpu.state['PC'].value - if MODE == 1 then - prog:write_u16(RETURN_STUB, 0xC080) -- jauc - prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF) - prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF) - else - prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self - end - - -- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at - -- the return slot so mode=1 would return to the spin stub. - -- Leave SP alone for mode 0: the loader pushes onto the current - -- process stack, then restores it and resumes at ORIG_PC. (mode 1 - -- would use the return stub.) - w32(0x01882A20, orig_pc) -- LD_ORIG_PC - print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc))) - cpu.state['A8'].value = A8DESC and DESC or 0 - if MODE == 1 then - -- Simulate a CALLA from the current process: push the return - -- address on the *firmware* stack (a real transport would do this). - local sp = cpu.state['SP'].value - 0x20 - w32(sp, RETURN_STUB) - cpu.state['SP'].value = sp - end - cpu.state['PC'].value = LOADER_BASE - injected = true - print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)", - frames, hex(LOADER_BASE), hex(cpu.state['SP'].value))) -end - -local function inspect() - result_dump() - - -- Verify the placement from RAM: .text/.data must equal the file bytes. - for _, s in ipairs(app.sections) do - if s.scnptr ~= 0 and s.size > 0 then - local nb = math.floor(s.size / 8) - local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb) - local _, bad = verify_bytes(s.paddr, bytes) - print(string.format("LOADER: placed %s at %s: %d bytes, %d differing", - s.name, hex(s.paddr), nb, bad)) - end - end - - local base = COFF:match("([^/]+)$") or COFF - if base:find("marker") then - print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d", - hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)), - hex(r32(0x018E0060)), r32(0x018E0080))) - elseif base:find("xhello") then - print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s", - r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)), - hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130)))) - end - if base:find("smoke") then - print(string.format("SMOKE: magic=%08X dpy=%08X win=%08X n=%d type=%d keycode=%d lookup_n=%d ch=%02X ks=%04X keymark=%08X", - r32(MARK + 0x00), r32(MARK + 0x20), r32(MARK + 0x40), r32(MARK + 0x60), - r32(MARK + 0x80), r32(MARK + 0xA0), r32(MARK + 0xC0), r32(MARK + 0xE0), - r32(MARK + 0x100), r32(MARK + 0x120))) - end - print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) -end - -emu.register_frame_done(function() - frames = frames + 1 - if frames == 2900 then tabx:set_value(700); taby:set_value(500); print("SMOKE: tablet move") end - if frames == 3000 then tabb:set_value(1); print("SMOKE: click down") end - if frames == 3020 then tabb:set_value(0); print("SMOKE: click up") end - if frames == 3100 then key_a:set_value(1); print("SMOKE: A down") end - if frames == 3120 then key_a:set_value(0); print("SMOKE: A up") end - if frames == AT and not injected then - inject() - if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end - end - if injected and TRACE and frames == AT + 1 then - dbg:command("trace off") - print("LOADER: trace written to /tmp/opencode/ld.tr") - end - if injected and not done 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). - end - if VERBOSE and frames % 25 == 0 then - result_dump() - end - if base_contains_marker then - local c = r32(0x018E0080) - if c ~= last_count then - if last_count ~= -1 then - print(string.format("LOADER: marker counter %d -> %d (child running)", - last_count, c)) - end - last_count = c - 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 - end - end - machine:exit() - end -end, "vt1200-loader") - --- (Lua has no forward use of a name defined later; set a small flag here.) -base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil - -dbg.visible_cpu = cpu -dbg.execution_state = 'run' +H.run()