From c1945cfc7310d2e4d5579cbaa8d9b3a68def0385 Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Tue, 3 Nov 2026 19:03:14 +0000 Subject: [PATCH] feat(termtris): X11 key map, millisecond clock and main loop --- apps/termtris/build/input.coff | Bin 0 -> 4399 bytes apps/termtris/build/input.map | 95 ++++++++ apps/termtris/src/vt1200/main.c | 130 +++++++++++ apps/termtris/tests/input.c | 32 +++ apps/termtris/tests/input.lua | 381 ++++++++++++++++++++++++++++++++ apps/termtris/tests/input.sh | 27 +++ 6 files changed, 665 insertions(+) create mode 100644 apps/termtris/build/input.coff create mode 100644 apps/termtris/build/input.map create mode 100644 apps/termtris/src/vt1200/main.c create mode 100644 apps/termtris/tests/input.c create mode 100644 apps/termtris/tests/input.lua create mode 100755 apps/termtris/tests/input.sh diff --git a/apps/termtris/build/input.coff b/apps/termtris/build/input.coff new file mode 100644 index 0000000000000000000000000000000000000000..fc02e28b1e911bf1ce0cd2767f89753c76747da8 GIT binary patch literal 4399 zcmbVPZ%iBK8Gp}kJuWvAwJ<6Yw%Zg+YAd7Cs%YIO`hfvuu{v$(Hfiha_q^w` zeOdA$vvlY8{XPGm_j#Z9y)&$qshQ7S{;tf}A;#Etwo|ARZVU7KFu_mzRKPopm+`bG z!p9SwV$6x}BR;RZoVAR#_eEk6FwXBwLsweJfczY6RR27fv>zTDGq4P7WCX)rH5X$^ z5q$l3?Y+>53YxpLe8%kUTQI zAs-C9&@&-q+$}Z|QXlE;ZXU(A6!@TwT_)?}wO6b=f~VSuhdW z7eDAxZtFE4^lTHfR>#tG`xIi@^UKbX*E-Kz0!`A$(4G-t9Z+$esrhKUH1k!=$89dMD4{1x&%C;CRglf`47&CWy_I=C_ zlxN?;Y^c%X3}9A}-(u#RX5R9w>n_x;GIjS|sQb#)op+(`EmIq`i@A*cATso8{ef#e z%8XvKDBA}`S%l|mx9Ap4kBYe@t#KQz)IrqJW+lYdWzU(6KpEY9lkyj=|9ty}PPf{M zHUE#kDc_^~O@9pUFLXEFxAmtBk87*mTPK&Mi<4KTq;18?tSZ@ylVqdg!ud``wJo;# z1a1BGrRms|OC1y?5uATuELg9}X<<8JS(HPpIBDd!L324$QWCYXMa%MkDNh{nG6&TT z(TmE@^*K4&7Lp;^LA8#0gVy-hQQN95-{RP<1?vMsT4=G`3f&GjX2gdz^2N&0&42Mp zCw2uqsZ;Cn!TRJwv_ELBsQ)Z1O(P01Eo??K0b5#-RioDCCE_A&f7FmcUYmR$p{j;* z$=bo3$FhS0;LdcBjUb{~lj9hZ&Uo{M&R2Pz%Idd4AU9N>(*Ibh183X!wG2_oU=Ji zAD0+Bv{Y(|hp0oUbnSq$steaP9&S}@7i4Y7*Q!$0&*`<-K9pZ%=nuXLZg-sfP_2pj zTDSP#RZYLhIljepJ-IN;IiKKKb}fX}tbd^?)wJSi?#lXSng%eM^|vY%brzj3Ag(8U z>Zxz8Zd(b=Ub3_OFRfp4FqdQfl9SDFP0jXG{ND$!l81KM%ua`XBe3L4PYBF@5DlUsou!t&tee(CHA);C9>+`j9 z;Ci7~Pg@*@J{t?6<`Io{QggWk`)cc$c&xN7{k^w z3_W0ByDwX)03&vf-J%2&(-xgZhON-o7;CYx-H$CgAYC{?Brg%^m^!t)Z4kOc_>(b`STLFDilYRY5rCkh@q1Mr91 zpx+NMq|>5;jAKY=Lj`#gLvyFt%DsiYehr>*sf*ILrGebJcWf+N^{q-(3_W-BaPY+X zxlh`s=DN>79nU^u=}!!@Abmgw2YQFYC3r9#85m434uKdyg7NWPyMleuaPR0?-zcLz z5I{@Fr8yEGiAICrqv06X$D-kIoQjlK2YX}v(XdH6)(dO>k+FER_e5FK528oL;7Lg@ z|DvbK=*S4qc>l<;GBG|nGB6s($Gl-4!!Qh%B>P9P=M76HL*hi-3=EuoM@Gj+5CM0+ z5)cosM51v?E9WAqk!V@SaL$mgRu(xt+&9>3sz%br5}1&(iUM<1WvMKN2qG~wF(mQ$ G0Q)buthGA; literal 0 HcmV?d00001 diff --git a/apps/termtris/build/input.map b/apps/termtris/build/input.map new file mode 100644 index 0000000..2ee2198 --- /dev/null +++ b/apps/termtris/build/input.map @@ -0,0 +1,95 @@ +***************************************************** +TMS340 COFF Linker Version 5.01 +***************************************************** +Tue Nov 03 19:02:37 2026 + +OUTPUT FILE NAME: +ENTRY POINT SYMBOL: "_start" address: 018a0000 + + +MEMORY CONFIGURATION + + name origin length attributes + -------- -------- --------- ---------- + prog 018a0000 000020000 RWIX + ram 01a00000 000040000 RWIX + + +SECTION ALLOCATION MAP + + output attributes/ +section page origin length input sections +-------- ---- ---------- ---------- ---------------- +.text 0 018a0000 000043b0 + 018a0000 00000150 ENTRY.obj (.text) + 018a0150 000003e0 INPUT.obj (.text) + 018a0530 00001760 MAIN.obj (.text) + 018a1c90 00002510 X11.obj (.text) + 018a41a0 00000210 XGFIX.obj (.text) + +.cinit 0 018a43b0 000000a0 + 018a43b0 00000050 MAIN.obj (.cinit) + 018a4400 00000050 X11.obj (.cinit) + +.bss 0 01a00000 0001e1c0 UNINITIALIZED + 01a00000 00008000 ENTRY.obj (.bss) + 01a08000 00001040 INPUT.obj (.bss) + 01a09040 00000360 MAIN.obj (.bss) + 01a093a0 00012e20 X11.obj (.bss) + 01a1c1c0 00002000 XGFIX.obj (.bss) + +.data 0 018a0000 00000000 UNINITIALIZED + + +GLOBAL SYMBOLS + +address name address name +-------- ---- -------- ---- +01a00000 .bss 018a0000 .data +018a0000 .data 018a0000 edata +018a0000 .text 018a0000 _start +018a0360 _game_input 018a0000 .text +018a0150 _init_game 018a0150 _init_game +018a18b0 _main 018a0250 _update +01a08040 _mark 018a0360 _game_input +018a0530 _now_ms 018a0530 _now_ms +018a07b0 _sleep_ms 018a07b0 _sleep_ms +018a0000 _start 018a0910 _vt_poll_event +01a1c0e0 _term_clearscr 018a1280 _vt_handle_event +01a1c120 _term_cursor 018a1870 _wait_display +01a08020 _term_height 018a18b0 _main +01a1c160 _term_ibmchar 018a1c90 _vt_root_window +018a4130 _term_init 018a1d70 _vt_x11_init +018a2ec0 _term_putstr 018a2420 _x11_clearscr +01a1c0c0 _term_reset 018a2610 _x11_reset +01a1c140 _term_setcolor 018a2650 _x11_cursor +01a1c100 _term_setcursor 018a26b0 _x11_setcolor +01a1c1a0 _term_type 018a27d0 _x11_setcursor +01a08000 _term_width 018a2a20 _x11_ibmchar +01a1c180 _termenv 018a2ec0 _term_putstr +018a0250 _update 018a3060 _vt_x11_flush +01a1c020 _vt_dpy 018a3e10 _vt_x11_term_init +01a1c060 _vt_gc 018a4130 _term_init +018a1280 _vt_handle_event 018a41a0 _vt_install_xg +018a41a0 _vt_install_xg 018a43b0 cinit +018a0910 _vt_poll_event 018a43b0 etext +018a1c90 _vt_root_window 01a00000 .bss +01a1c040 _vt_win 01a08000 _term_width +01a1c0a0 _vt_x11_drain_events 01a08020 _term_height +018a3060 _vt_x11_flush 01a08040 _mark +018a1d70 _vt_x11_init 01a1c020 _vt_dpy +01a1c080 _vt_x11_progress 01a1c040 _vt_win +018a3e10 _vt_x11_term_init 01a1c060 _vt_gc +018a1870 _wait_display 01a1c080 _vt_x11_progress +018a2420 _x11_clearscr 01a1c0a0 _vt_x11_drain_events +018a2650 _x11_cursor 01a1c0c0 _term_reset +018a2a20 _x11_ibmchar 01a1c0e0 _term_clearscr +018a2610 _x11_reset 01a1c100 _term_setcursor +018a26b0 _x11_setcolor 01a1c120 _term_cursor +018a27d0 _x11_setcursor 01a1c140 _term_setcolor +018a43b0 cinit 01a1c160 _term_ibmchar +018a0000 edata 01a1c180 _termenv +01a1e1c0 end 01a1c1a0 _term_type +018a43b0 etext 01a1e1c0 end + +[46 symbols] diff --git a/apps/termtris/src/vt1200/main.c b/apps/termtris/src/vt1200/main.c new file mode 100644 index 0000000..ce75f2b --- /dev/null +++ b/apps/termtris/src/vt1200/main.c @@ -0,0 +1,130 @@ +/* + * main.c -- the VT1200 termtris event loop. + * + * The game's update()/init_game() draw through the backend's term_* callbacks; + * this file owns the X11 event drain, the real-time clock, and the key map. + */ +#include "game.h" +#include "x11.h" +#include "xlib.h" + +extern int init_game(void); +extern long update(long msec); +extern void game_input(int c); + +extern int vt_install_xg(void); + +static vt_u32 evbuf[24]; +static long ms_base; +static int ms_last = -1; +static int focused; + +long now_ms(void) +{ + int now; + if(ms_last < 0) { + ms_last = *(volatile unsigned short *)0x001E93C0UL; + ms_base = 0; + return 0; + } + now = *(volatile unsigned short *)0x001E93C0UL; + ms_base += (long)(unsigned short)(now - ms_last); + ms_last = now; + return ms_base; +} + +void sleep_ms(int ms) +{ + if(ms < 1) ms = 1; + if(ms > 10) ms = 10; + VT_SLEEP(ms); +} + +int vt_poll_event(void) +{ + return *(vt_u32 *)((char *)vt_dpy + 0x54) != 0; +} + +static void map_keysym(vt_u32 ks, char ch, int n) +{ + switch(ks) { + case 0xFF51UL: /* Left */ + game_input(0x9b); game_input('D'); + return; + case 0xFF53UL: /* Right */ + game_input(0x9b); game_input('C'); + return; + case 0xFF52UL: /* Up */ + game_input(0x9b); game_input('A'); + return; + case 0xFF54UL: /* Down */ + game_input(0x9b); game_input('B'); + return; + case 0xFF0DUL: /* Return */ + game_input('\n'); + return; + case 0xFF08UL: /* BackSpace */ + game_input('\b'); + return; + case 0xFF09UL: /* Tab */ + game_input('\t'); + return; + case 0xFF1BUL: /* Escape */ + game_input(27); + return; + default: + break; + } + if(n > 0) { + if(ch >= 'A' && ch <= 'Z') ch = (char)(ch + ('a' - 'A')); + game_input((int)(unsigned char)ch); + } +} + +void vt_handle_event(void) +{ + char buf[8]; + vt_u32 ks; + int n; + + VT_XNEXT(vt_dpy, (void *)evbuf); + if(evbuf[0] == 2) { /* KeyPress */ + ks = 0; + n = VT_XLOOKUP((void *)evbuf, buf, 8, &ks, 0); + map_keysym(ks, n > 0 ? buf[0] : 0, n); + } else if(evbuf[0] == 12) { /* Expose */ + game_input('`'); + } else if(evbuf[0] == 9) { /* FocusIn */ + focused = 1; + } else if(evbuf[0] == 10) { /* FocusOut */ + if(focused) game_input('p'); + focused = 0; + } +} + +void wait_display(void) +{ + vt_x11_flush(); +} + +int main(void) +{ + long d; + + if(vt_x11_init() != 0) + return 1; + if(init_game() == -1) + return 2; + vt_x11_flush(); + + for(;;) { + VT_XSYNC(vt_dpy, 0); + while(vt_poll_event()) { + vt_handle_event(); + } + d = update(now_ms()); + sleep_ms((int)d); + vt_x11_flush(); + } + return 0; +} diff --git a/apps/termtris/tests/input.c b/apps/termtris/tests/input.c new file mode 100644 index 0000000..8735c32 --- /dev/null +++ b/apps/termtris/tests/input.c @@ -0,0 +1,32 @@ +/* + * input.c -- Task 4 acceptance: link main.c against stubs for the game, inject + * keys, and record the bytes main.c sends to game_input(). + */ +#include "game.h" +#include "x11.h" +#include "xlib.h" + +int term_width, term_height; +volatile vt_u32 mark[128]; + +int init_game(void) +{ + mark[0] = 0x494E5054UL; /* INPT */ + vt_x11_term_init(); + x11_clearscr(); + return 0; +} + +long update(long msec) +{ + (void)msec; + mark[1] = mark[1] + 1; + return 100; +} + +void game_input(int c) +{ + int n = (int)mark[2]; + if(n < 100) mark[3 + n] = (vt_u32)(unsigned char)c; + mark[2] = (vt_u32)(n + 1); +} diff --git a/apps/termtris/tests/input.lua b/apps/termtris/tests/input.lua new file mode 100644 index 0000000..2f619ee --- /dev/null +++ b/apps/termtris/tests/input.lua @@ -0,0 +1,381 @@ +-- 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 + +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 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 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 +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' diff --git a/apps/termtris/tests/input.sh b/apps/termtris/tests/input.sh new file mode 100755 index 0000000..c43cd69 --- /dev/null +++ b/apps/termtris/tests/input.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# input.sh -- Task 4 acceptance: main.c's key map against injected keys. +set -eu +DIR=$(cd "$(dirname "$0")" && pwd) +APP=$(cd "$DIR/.." && pwd) +ROOT=$(cd "$APP/../.." && pwd) +M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} +LOG="$APP/build/input.log" +mkdir -p "$APP/build" +"$APP/build.sh" "$DIR/input.c" "$APP/src/vt1200/main.c" "$APP/src/vt1200/x11.c" "$APP/src/vt1200/xgfix.c" >"$APP/build/input-build.log" 2>&1 +MARK=$(awk '/_mark/ {print $1; exit}' "$APP/build/input.map") +VT1200_SKIP_LEVEL1=1 INPUT_MARK="0x$MARK" \ +LOADER_AT=2500 LOADER_END=3400 \ +LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ +LOADER_COFF="$APP/build/input.coff" \ +LOADER_OUT="$APP/build" \ +"$M" vt1200 -rompath "$ROOT/harness/mame/roms" \ + -video none -sound none -nothrottle -skip_gameinfo \ + -debug -debugger none \ + -autoboot_script "$DIR/input.lua" -autoboot_delay 0 \ + >"$LOG" 2>&1 || true +grep 'INPUT:' "$LOG" | tail -3 +grep -q 'INPUT: .*bytes=.*61 9B 41 9B 42 9B 44 9B 43 30 0A 7F' "$LOG" || { + echo "input.sh: mapped key sequence missing (see $LOG)" >&2 + exit 1 +} +echo "input: PASS"