From 41d3cecd49f7fab047c9149308b4d94f0cf1af08 Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Tue, 3 Nov 2026 20:03:47 +0000 Subject: [PATCH] feat(termtris): run the game on the VT1200 X server Board, score panel, preview and help/scores toggles draw through the ROM Xlib cell grid; keys arrive as real X KeyPress events. The backend is now monochrome (the VT1200 raster is one bit deep) and flushes whole rows so the in-machine server can drain the request stream. --- apps/termtris/build.sh | 4 +- apps/termtris/build/render.coff | Bin 3788 -> 3922 bytes apps/termtris/build/render.map | 82 ++-- apps/termtris/build/termtris.coff | Bin 0 -> 15157 bytes apps/termtris/build/termtris.map | 141 +++++++ apps/termtris/evidence/render_frame3400.png | Bin 19845 -> 18967 bytes apps/termtris/evidence/termtris_frame3300.png | Bin 0 -> 20986 bytes apps/termtris/evidence/termtris_move_3200.png | Bin 0 -> 19458 bytes .../evidence/termtris_panels_3200.png | Bin 0 -> 22396 bytes apps/termtris/evidence/termtris_quit_3200.png | Bin 0 -> 19407 bytes apps/termtris/src/game.c | 4 +- apps/termtris/src/vt1200/compat.c | 6 + apps/termtris/src/vt1200/main.c | 29 +- apps/termtris/src/vt1200/scoredb.c | 76 ++++ apps/termtris/src/vt1200/stdio.h | 1 + apps/termtris/src/vt1200/x11.c | 90 ++-- apps/termtris/src/vt1200/xlib.h | 2 + apps/termtris/tests/input.lua | 3 + apps/termtris/tests/input.sh | 2 +- apps/termtris/tests/play.lua | 345 ++++++++++++++++ apps/termtris/tests/play.sh | 25 ++ apps/termtris/tests/play_keys.lua | 390 ++++++++++++++++++ apps/termtris/tests/play_keys.sh | 35 ++ 23 files changed, 1151 insertions(+), 84 deletions(-) create mode 100644 apps/termtris/build/termtris.coff create mode 100644 apps/termtris/build/termtris.map create mode 100644 apps/termtris/evidence/termtris_frame3300.png create mode 100644 apps/termtris/evidence/termtris_move_3200.png create mode 100644 apps/termtris/evidence/termtris_panels_3200.png create mode 100644 apps/termtris/evidence/termtris_quit_3200.png create mode 100644 apps/termtris/src/vt1200/scoredb.c create mode 100644 apps/termtris/tests/play.lua create mode 100755 apps/termtris/tests/play.sh create mode 100644 apps/termtris/tests/play_keys.lua create mode 100755 apps/termtris/tests/play_keys.sh diff --git a/apps/termtris/build.sh b/apps/termtris/build.sh index 601be0b..39c3b06 100755 --- a/apps/termtris/build.sh +++ b/apps/termtris/build.sh @@ -91,7 +91,7 @@ run() { compile_c() { base=$1 rm -f "$WORK/$base.IF" "$WORK/$base.OPT" "$WORK/$base.ASM" "$WORK/$base.obj" - run "$base.IF" ./gspac.exe -k "$base.C" "$base.IF" + run "$base.IF" ./gspac.exe -k -i. "$base.C" "$base.IF" run "$base.OPT" ./gspopt.exe -k -O2 "$base.IF" "$base.OPT" run "$base.ASM" ./gspcg.exe -m "$base.OPT" "$base.ASM" "$base.TMP" run "$base.obj" ./gspa.exe "$base.ASM" @@ -120,7 +120,7 @@ compile_asm ENTRY compile_asm CINIT compile_asm CINITZ -OUTNAME=$(printf '%s' "$MAIN" | tr 'A-Z' 'a-z') +OUTNAME=${OUTNAME:-$(printf '%s' "$MAIN" | tr 'A-Z' 'a-z')} CMD="$WORK/$MAIN.CMD" { printf '%s\n' '-a' diff --git a/apps/termtris/build/render.coff b/apps/termtris/build/render.coff index e00108eea8a5beaf981a4766b570c1bd1a3f4ed0..20171e734748afebaf5b0de6bc4b34170d983d17 100644 GIT binary patch delta 1979 zcmZ`)VQf=X6h8NDkJsC`jt9Z`U~E1{ySf3(Btcy=Y>BOmtxx*HKSp9-ltl)*EVCjS zO$)M=6$7ph6HsG{wJwcYO9sn+(jRMpPN-2sghXvjmN5y9F^*p*_1yQIZ4sN?`|kbD zch3FpIrrXv)BIga=p(3ywX2LKykGqI3mqPTE^zwjA?eH1&hbp{8H1PzNh zXS{EXd8Je80XknD92!KT;)-IdD2b!_60TAGa|4Wr){-;hu|7_5jsN`gqs5jeah?|Q zg~X74fOmfEEYo-9o(Fd@L3Y~;p~VvIB>BpPoPNA%;QHN+wm5?AOj zlww!tX6(^_6T^n|g$-$(nT#AV(yC%OHrP-4L5*O$4sH%dFB#G(dpcXw#9CrTP|m0`zct`h=^KT+oj zA(yJ#(Y{DmLn~_U)Y-<22=Cu=nc7OR#+xh!D;5|&hY?sREb2P+t+i@jg zf6H~p$7Qe5aydi8R*jyfM~*6TpLdD2X~*KEL7t~!!OxE!-7_Ttos@w}cY78+5h091 zB7!!ekJPt|I8M!G%VR=zME|P(*$~dh3`S`-3>gX?uVCO8`0Hv4J^t*J>(rk zMGiTInvzBwhcs$@y<%&oEY7WnOd3lvBU`#On?*tK4*e8w53!@*JiR<$nvEkEHdMZs zxvn;f?z%@ZKK%;FRhNlzNn&I+@Vd@c0JEmvy&*giC_Pseh!NYYs2Y9dt8f4_6i zwe6w;ZR&Gtk}5AMbyL$CfM-%|5aZDA%cB@SKR8;Kr%u{VvQ$36ZygtFG^XdUD|{5`W(xS1o!6)38&1C-GBD)Q%6;w-C>_}n_1v}quz8Z{k;OG@U)xs z2ea_dWEvZBs9yB;ssr}qu9eL^JPzARNc>0V**UY8i=Ml9nnWqpGZDcGTJIgROUYR` zIMz*J&0&63If>_4Fs_w(fx4tmT?=@5$}nnFyCh!zD?^u`VHe2{^Vm)bfagYRIw>gl zU7|{Jl-&(1?2?yiO>7cp6Uts9ygn(c&YHV25jMO{a3-& zgvj6rkIN>?fg6|duZav~=q& zR+0Rne{PXi4>K}8&2_|&J<*?_k8sjX{eX2h3F;TDt<3ia>!GJmUL6t`6`&cLR)cIu z#P5s}_Lspy;BYXofA}51$L+AV!(PLD2^nnrCKD%c5L=gC5bf+0tSoXC;cyM|B_bN` z-sA*=10!z?3>|j=i=Y`Y*jQdGaV{eguR(4g!m?hGa2pY&IaCcTAjAq$3G^odaeTZh zZe>J@HK7~O$Vyi6+JMLfN41v|k#Ki4(v1j~eoa;A-3VbFIGa&{HrsA;=Hig;W%4%= CRA~1A delta 1801 zcmZ`(Z)jUp6hHU1F?oGWmyOBNwY6!N?5%$maoE{)XyQyv?eezxK^QvBLl<1vT8fl8 zMM0rUYtBkUtOvPnukxWHdvjo>?kFZHpG_%4hn z9(7H5Ys$L(GF1auU+*94M`Ecbgt$C)09vHrY8E7eV!fuSM^ol3fF^pgIX|u^6h?6|00|b_U&Hhxnwvte=F5eYd;{aI zlcP(=xxC!HJgYFDTzY{^$WJW$gqVBT95mn25O`xyYsaFt3t5(GSi!2?9j$e{1T6 z2{o|pxd?6U;LkR0C#M8#~)bLHe-jdbuuTHln<_2gpM?x^OS`HATXxX zAQPQE;YMk}JoN7L$A`Nasc1ILnXE-EAK)t4HtI&7-jg6=<8+nj&{)^3Oe`Q3L% zVO5ZQ0*gTpr_SNj%J(-ea@VB4wap$+Q-}^7ZdqW(25LtXn||jwn2LQ#;baxNq}Y&D z_$`!ULK}w_zXklxx}>u@TLU8fA09BOCq>ft{fGYb)O2bL z766;{tL})NPD&NtaaF`*B~nYx9>#uDO4qb9*>0&=<047vFxN`MUgid#LOGPnD0gBL zzQqdU5F!yiMCb*~fcN}}_ssA+fLF!8T$j0m`3f@VaaATR;2b`p^ir*v-YMlxK0`QA zflN~(o>t{}M|^L+=^Hw4e}JGH68NMlqP{~UQi0q;gjTsM;SM66CjWLQhY&4%S;@pNrRjv#`q-nlKc3nA>oxL)bNoZdy&zP_h_ GmHY>q6h@r@ diff --git a/apps/termtris/build/render.map b/apps/termtris/build/render.map index da7b319..50449f4 100644 --- a/apps/termtris/build/render.map +++ b/apps/termtris/build/render.map @@ -1,7 +1,7 @@ ***************************************************** TMS340 COFF Linker Version 5.01 ***************************************************** -Tue Nov 03 19:00:04 2026 +Tue Nov 03 19:54:29 2026 OUTPUT FILE NAME: ENTRY POINT SYMBOL: "_start" address: 018a0000 @@ -20,21 +20,21 @@ SECTION ALLOCATION MAP output attributes/ section page origin length input sections -------- ---- ---------- ---------- ---------------- -.text 0 018a0000 00003af0 +.text 0 018a0000 00003f20 018a0000 00000150 ENTRY.obj (.text) 018a0150 00001280 RENDER.obj (.text) - 018a13d0 00002510 X11.obj (.text) - 018a38e0 00000210 XGFIX.obj (.text) + 018a13d0 00002940 X11.obj (.text) + 018a3d10 00000210 XGFIX.obj (.text) -.cinit 0 018a3af0 000000a0 - 018a3af0 00000050 RENDER.obj (.cinit) - 018a3b40 00000050 X11.obj (.cinit) +.cinit 0 018a3f20 000000a0 + 018a3f20 00000050 RENDER.obj (.cinit) + 018a3f70 00000050 X11.obj (.cinit) -.bss 0 01a00000 0001d080 UNINITIALIZED +.bss 0 01a00000 00019480 UNINITIALIZED 01a00000 00008000 ENTRY.obj (.bss) 01a08000 00000260 RENDER.obj (.bss) - 01a08260 00012e20 X11.obj (.bss) - 01a1b080 00002000 XGFIX.obj (.bss) + 01a08260 0000f220 X11.obj (.bss) + 01a17480 00002000 XGFIX.obj (.bss) .data 0 018a0000 00000000 UNINITIALIZED @@ -49,37 +49,37 @@ address name address name 018a0370 _main 018a0000 _start 01a08000 _mark 018a0370 _main 018a0000 _start 018a13d0 _vt_root_window -01a1afa0 _term_clearscr 018a14b0 _vt_x11_init -01a1afe0 _term_cursor 018a1b60 _x11_clearscr -01a08220 _term_height 018a1d50 _x11_reset -01a1b020 _term_ibmchar 018a1d90 _x11_cursor -018a3870 _term_init 018a1df0 _x11_setcolor -018a2600 _term_putstr 018a1f10 _x11_setcursor -01a1af80 _term_reset 018a2160 _x11_ibmchar -01a1b000 _term_setcolor 018a2600 _term_putstr -01a1afc0 _term_setcursor 018a27a0 _vt_x11_flush -01a1b060 _term_type 018a3550 _vt_x11_term_init -01a08200 _term_width 018a3870 _term_init -01a1b040 _termenv 018a38e0 _vt_install_xg -01a1aee0 _vt_dpy 018a3af0 etext -01a1af20 _vt_gc 018a3af0 cinit -018a38e0 _vt_install_xg 01a00000 .bss +01a173a0 _term_clearscr 018a14b0 _vt_x11_init +01a173e0 _term_cursor 018a1b60 _x11_clearscr +01a08220 _term_height 018a1d20 _x11_reset +01a17420 _term_ibmchar 018a1d60 _x11_cursor +018a3ca0 _term_init 018a1dc0 _x11_setcolor +018a25b0 _term_putstr 018a1ee0 _x11_setcursor +01a17380 _term_reset 018a2130 _x11_ibmchar +01a17400 _term_setcolor 018a25b0 _term_putstr +01a173c0 _term_setcursor 018a2750 _vt_x11_flush +01a17460 _term_type 018a3980 _vt_x11_term_init +01a08200 _term_width 018a3ca0 _term_init +01a17440 _termenv 018a3d10 _vt_install_xg +01a172e0 _vt_dpy 018a3f20 etext +01a17320 _vt_gc 018a3f20 cinit +018a3d10 _vt_install_xg 01a00000 .bss 018a13d0 _vt_root_window 01a08000 _mark -01a1af00 _vt_win 01a08200 _term_width -01a1af60 _vt_x11_drain_events 01a08220 _term_height -018a27a0 _vt_x11_flush 01a1aee0 _vt_dpy -018a14b0 _vt_x11_init 01a1af00 _vt_win -01a1af40 _vt_x11_progress 01a1af20 _vt_gc -018a3550 _vt_x11_term_init 01a1af40 _vt_x11_progress -018a1b60 _x11_clearscr 01a1af60 _vt_x11_drain_events -018a1d90 _x11_cursor 01a1af80 _term_reset -018a2160 _x11_ibmchar 01a1afa0 _term_clearscr -018a1d50 _x11_reset 01a1afc0 _term_setcursor -018a1df0 _x11_setcolor 01a1afe0 _term_cursor -018a1f10 _x11_setcursor 01a1b000 _term_setcolor -018a3af0 cinit 01a1b020 _term_ibmchar -018a0000 edata 01a1b040 _termenv -01a1d080 end 01a1b060 _term_type -018a3af0 etext 01a1d080 end +01a17300 _vt_win 01a08200 _term_width +01a17360 _vt_x11_drain_events 01a08220 _term_height +018a2750 _vt_x11_flush 01a172e0 _vt_dpy +018a14b0 _vt_x11_init 01a17300 _vt_win +01a17340 _vt_x11_progress 01a17320 _vt_gc +018a3980 _vt_x11_term_init 01a17340 _vt_x11_progress +018a1b60 _x11_clearscr 01a17360 _vt_x11_drain_events +018a1d60 _x11_cursor 01a17380 _term_reset +018a2130 _x11_ibmchar 01a173a0 _term_clearscr +018a1d20 _x11_reset 01a173c0 _term_setcursor +018a1dc0 _x11_setcolor 01a173e0 _term_cursor +018a1ee0 _x11_setcursor 01a17400 _term_setcolor +018a3f20 cinit 01a17420 _term_ibmchar +018a0000 edata 01a17440 _termenv +01a19480 end 01a17460 _term_type +018a3f20 etext 01a19480 end [38 symbols] diff --git a/apps/termtris/build/termtris.coff b/apps/termtris/build/termtris.coff new file mode 100644 index 0000000000000000000000000000000000000000..5836bfc488318373bb6ab8263d460e90a57fc24e GIT binary patch literal 15157 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 W&JYU~Q^P>tu3l`-(3t%l?EeGx5qy*Y literal 0 HcmV?d00001 diff --git a/apps/termtris/build/termtris.map b/apps/termtris/build/termtris.map new file mode 100644 index 0000000..af6ca6a --- /dev/null +++ b/apps/termtris/build/termtris.map @@ -0,0 +1,141 @@ +***************************************************** +TMS340 COFF Linker Version 5.01 +***************************************************** +Tue Nov 03 20:01:11 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 00014940 + 018a0000 00000150 ENTRY.obj (.text) + 018a0150 00001c90 MAIN.obj (.text) + 018a1de0 0000ce50 GAME.obj (.text) + 018aec30 00000d20 SCOREDB.obj (.text) + 018af950 00002a40 X11.obj (.text) + 018b2390 00001640 COMPAT.obj (.text) + 018b39d0 00000210 XGFIX.obj (.text) + 018b3be0 00000450 rts.lib (.text) + 018b4030 000001a0 rts.lib (.text) + 018b41d0 00000230 rts.lib (.text) + 018b4400 00000150 rts.lib (.text) + 018b4550 00000120 rts.lib (.text) + 018b4670 00000110 rts.lib (.text) + 018b4780 000001c0 rts.lib (.text) + +.cinit 0 018b4940 000012d0 + 018b4940 00000050 MAIN.obj (.cinit) + 018b4990 00001190 GAME.obj (.cinit) + 018b5b20 00000050 SCOREDB.obj (.cinit) + 018b5b70 00000050 X11.obj (.cinit) + 018b5bc0 00000050 rts.lib (.cinit) + +.bss 0 01a00000 0001ec20 UNINITIALIZED + 01a00000 00008000 ENTRY.obj (.bss) + 01a08000 00000400 MAIN.obj (.bss) + 01a08400 00004a40 GAME.obj (.bss) + 01a0ce40 00000b80 SCOREDB.obj (.bss) + 01a0d9c0 0000f220 X11.obj (.bss) + 01a1cbe0 00000020 COMPAT.obj (.bss) + 01a1cc00 00002000 XGFIX.obj (.bss) + 01a1ec00 00000020 rts.lib (.bss) + +.data 0 018a0000 00000000 UNINITIALIZED + + +GLOBAL SYMBOLS + +address name address name +-------- ---- -------- ---- +01a00000 .bss 018a0000 .text +018a0000 .data 018a0000 .data +018a0000 .text 018a0000 edata +01a08a00 _blackpf 018a0000 _start +018a2f40 _cleanup_game 018a0150 _now_ms +01a08380 _dbg_events 018a03d0 _sleep_ms +01a083c0 _dbg_focus 018a0530 _vt_poll_event +01a083a0 _dbg_last 018a0ea0 _vt_handle_event +01a08360 _dbg_loops 018a1700 _wait_display +01a083e0 _dbg_rev 018a1740 _main +018b2390 _fflush 018a1de0 _init_game +018a84e0 _game_input 018a2f40 _cleanup_game +018a1de0 _init_game 018a3130 _update +018a1740 _main 018a84e0 _game_input +018b3be0 _memcpy 018aef90 _read_scores +018b3d80 _memmove 018af310 _save_score +018b4030 _memset 018af8a0 _print_scores +01a089a0 _monochrome 018af950 _vt_root_window +018a0150 _now_ms 018afa30 _vt_x11_init +01a089e0 _onlyascii 018b01e0 _x11_clearscr +018af8a0 _print_scores 018b03a0 _x11_reset +018b2400 _putchar 018b03e0 _x11_cursor +01a08940 _quit 018b0440 _x11_setcolor +018b41d0 _rand 018b0560 _x11_setcursor +018aef90 _read_scores 018b07b0 _x11_ibmchar +01a08a20 _rotstep 018b0c30 _term_putstr +018af310 _save_score 018b0dd0 _vt_x11_flush +01a08a80 _scr 018b2000 _vt_x11_term_init +018a03d0 _sleep_ms 018b2320 _term_init +018b2c10 _sprintf 018b2390 _fflush +018b4330 _srand 018b2400 _putchar +018a0000 _start 018b2c10 _sprintf +01a1cbe0 _stdout 018b39d0 _vt_install_xg +018b4400 _strcmp 018b3be0 _memcpy +018b4550 _strcpy 018b3d80 _memmove +018b4670 _strlen 018b4030 _memset +018b4780 _strncpy 018b41d0 _rand +01a1cb00 _term_clearscr 018b4330 _srand +01a1cb40 _term_cursor 018b4400 _strcmp +01a08a60 _term_height 018b4550 _strcpy +01a1cb80 _term_ibmchar 018b4670 _strlen +018b2320 _term_init 018b4780 _strncpy +018b0c30 _term_putstr 018b4940 cinit +01a1cae0 _term_reset 018b4940 etext +01a1cb60 _term_setcolor 01a00000 .bss +01a1cb20 _term_setcursor 01a08360 _dbg_loops +01a1cbc0 _term_type 01a08380 _dbg_events +01a08a40 _term_width 01a083a0 _dbg_last +01a1cba0 _termenv 01a083c0 _dbg_focus +01a08960 _tick_interval 01a083e0 _dbg_rev +018a3130 _update 01a08940 _quit +01a08980 _use_bell 01a08960 _tick_interval +01a089c0 _use_gfxchar 01a08980 _use_bell +01a0ce40 _username 01a089a0 _monochrome +01a1ca40 _vt_dpy 01a089c0 _use_gfxchar +01a1ca80 _vt_gc 01a089e0 _onlyascii +018a0ea0 _vt_handle_event 01a08a00 _blackpf +018b39d0 _vt_install_xg 01a08a20 _rotstep +018a0530 _vt_poll_event 01a08a40 _term_width +018af950 _vt_root_window 01a08a60 _term_height +01a1ca60 _vt_win 01a08a80 _scr +01a1cac0 _vt_x11_drain_events 01a0ce40 _username +018b0dd0 _vt_x11_flush 01a1ca40 _vt_dpy +018afa30 _vt_x11_init 01a1ca60 _vt_win +01a1caa0 _vt_x11_progress 01a1ca80 _vt_gc +018b2000 _vt_x11_term_init 01a1caa0 _vt_x11_progress +018a1700 _wait_display 01a1cac0 _vt_x11_drain_events +018b01e0 _x11_clearscr 01a1cae0 _term_reset +018b03e0 _x11_cursor 01a1cb00 _term_clearscr +018b07b0 _x11_ibmchar 01a1cb20 _term_setcursor +018b03a0 _x11_reset 01a1cb40 _term_cursor +018b0440 _x11_setcolor 01a1cb60 _term_setcolor +018b0560 _x11_setcursor 01a1cb80 _term_ibmchar +018b4940 cinit 01a1cba0 _termenv +018a0000 edata 01a1cbc0 _term_type +01a1ec20 end 01a1cbe0 _stdout +018b4940 etext 01a1ec20 end + +[77 symbols] diff --git a/apps/termtris/evidence/render_frame3400.png b/apps/termtris/evidence/render_frame3400.png index dfc882f79cf41180dd315f52b75d34e04c2c59cd..05de4424d7c9a7f45ab2e5ee6964311a032c87ec 100644 GIT binary patch literal 18967 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

>kRa&J@12$Jwu-IArdyf83cw<$& zU3za1>k4}Y07lv*pYUL2ri2%3dvXuVTq{9~GZVra%4{gIfq6z()rnE{V*#}|5-q*X zo)Ho&Wd+QZC?DD1v9m$8a|v_x#|HD}c5b^KrXn)x=d}fSNPKf9sJxn&*TxGZ&Ob*E zhj#Nwc(H8kYU&NaB2s^$>&uLIC=?mmG$_zEN!{$*;FKTH&`v|&r!mgVHWklB8#6p6 zjlJ{ummfvMDaQ$5Ufj-Q&Xlx`wjX-T@>I^;UDSAMg~mwfA0G*}zPvqCKH%5<^3I9t0Bt9sP_D-&;RXfcEcu&UG%6 zv`{z6TDc;AGw@~u+ zZ+A44slN_pW-U`tP0CZWoOMxZ%;YBbDRa=PBLuWa z5JPSfR_j~tgLs=+>~O^=@jYegq0nP5m>JOTsdALm{hyCW3JWvFczMKIy1=cB#lfF~ zd}?01jt=+97WbI|U`g4R!DPn!fPzk{ZaR>1Q|>K!yqtq|GU6`5m{)W+QZwux)VsFga30ahd*x59o)+|x9cSN9w{n9 zbrR)Q_$XXzOyb^{{f<#K?fquKahiQ3h4Zt#&T)Q}$>@M6{fF?a`NAFs1#@M8#YY^t ztp+kh8lm;kQkX15H3Vu~t;`?C-Wu7La4gJ79!W;?q0*s?j_TzAD{9`krb8Ysrm&%Vrq<^urc$40-#t2!?}mR5)2~vXx0+T`4AC6w zo*7QpW<~h=rj_8COZWTzHng4eDb=)B(TL@O0=kPa_IJ220&SZ8v67^s%v3STIDK|5 zA+tul1!KWldpN$D0(vv^{tI+sPuwI+txHT;Q2EU|k^3qSo||58YY@ikMccz2?79D5 zJL!*g`|nz>z^$)l%9hiqlayB7L&B!214y;xx=$vmuDJH z>65B1%`KE5v z5{|h72Nf=rbfMEHPhqaGTk8bWR;)`#-=oczh3A@{7&?aCZRSd6HrH2l$ss{laN=GQ zhkj;DO_(Rf>p+EyGgVqh?s&=z62p;y8Vl~7=^tT4x%vo>4~9xF6?zNgvABwD^|u19 z?@)tzw761_K!b_yre(fa%%L8Lx3RV$L$<6$NYS~$G3DTwiuTF>!Cx^%BY~T&+m-;m{Lz>8vyf`593o5%<|M2s1@=f=#0Vl=?pnqoEv5h zrn0J|20t7w&KmVkn4E2@wlTcgG8cVY@+hCy@y@efnfbdEoL{-aCWn?mu0BY&%v?@MpWeLtazu2m3bXaBMd>DFX?))8*#sj?Np(z_yK z;!mR^u8~2s@yFQW$ycK3iS1(lVOdm>%VRCHLpR;|mF$D;9RY^_iZ5~kwx;51KoVhD zmd_(5cw#EkXVW`waM0h)6hxEqu?*{&nIWwA(8TfWF$ra8akX^}#6e%)Rup>;CWcm`s|;xgnNQQIo=$@ z2((p9LcS;oVaZ&fsd4YLOuY$_H+fTbk`tOuGodON^6|Erql#fl=L}P>!)(>h=t#+% zkR-|0!nt-csT=n)P1VbQON=Z*pkghH1P@+zvLY<4@t492M&i9}Zi_Yr42xpN#@8!5 z-p<5Cf^w|#A3?0Bm1sRwd<{SLF#nKsPuuKIB67}(6y^Aaw6D-WbJWwRGKl9yODg}Y zn~->F+|w$}M{kFuPv%oS7Y+|Oq^*_=xmPG>GJC;H=BijD`;zQgV9w?gQ^pIJUQkb< ziL|Ut6QBzQ-R5uUbMXKPJX5=z2P()nj~v*8IV)n+tdpR00}`_-rkb)by=T|xA<$-0 z49+xR!BDJEQO@7Vl^*o<%E6sMbOq8V(q7<~-9a@=iH`f(&m0<47MFtZkHF~or|CV- z>DsQ9Kd;|#U&$6!(MnA(I{V|=-X|gJ>qRT2FG%%wQVF%Ksrs)`De$11-6Qq&8V-&v zGHp1%{m9QF?S0Bt#jDhoFI>#<-5wT>BvB8$vSq|KPA*K6qNWSKXsKU`!~L?i>MSe8 z!ldaE8ceSXkJ^T6zghB2*L@-2Di*!Ha2eV!ydI^Y78LMo>Pj(d*Y5_5nlhq1xf^uhOcn z?A6u{SS9!^>@deB;-x+9)hQ~FY93UN z(vY^eBY`6WA5!>Q)^`Nq*6|f>uN=>D-s>#eWhLF-Ic3%2x4G^ zy??k%W3LT$<=d4FwW=GX=QNMk-_;q3(^{8YTXp;xQBxdrRq@`{%B9={?ID1P@RN9*o(M_1*%t+to|IY7(P1o^Ho^xlV4#H9Vn5_e|GeUCp%XiQ0+g*5D9Y)FjVRo zz?O>jo!H(b8B70zE$9R++nxU>TiClls_f6UAk3>V87Gk;!GvZwt|*(giX!9RF67MRrVmc#fy+H8gjYu7;wy<~sR;_|GRxc@&m$NXOTg%6>x37I#V z@9Ab94b$*V`Wjkkk23?GiIrKU_H0R~WbR%+NT0XI+-2`Zr?EcqU$J7&n}C)~9qYIOsb_qd+KG!laFu;}0yd zX#>nN<6<_{IQ|YH(Ss-JJauO>^dX2s0;Om9mbB*fc#W?lL~h%eqy2~PYLnqAe)l#P z=wZUCoYc2uu_n(n_(6Ow_P;HV|8j^uifK*#wQ`hkZoBL$mNv(Z2|SO4$`*wxl=oOe^n7GFytI8lV9BuzN*Ogw#5J1O>sqVal*$91^v7Y=n9~U8; zCLzz6BwaLCXA91u*w#H(S&~*=7hJ>3(s8zUcgPintfnx|{lD*x_y~TtmzY^k8rzxqU|1%;_NO**TE1Tb+5X>Oj=rUve_c0h;0rw)>(- zpl2nz#;t{6)fPmlH;z!VADj4Q4Tgj=$2X}RW12~tCvULTkYdZbY%%*;1WGKzMaRfy zT_L`ccGgA=doW;8q&LO)k<)aH#ZSHnyqLbvS9j{-1=;bwwz)|v7~vi+tlPz>7o|NH z^*fWcm2oOd$1_uTgK)luKH?nm(l7j*3p1^aloVtCVNtd3KruR>aEs}RkITsSZ+Qy7 zio1}{7$w=ASwo<#h@I1q@6Cl+?jZ~HY6dBP-1EZ8( z&p@R`%1$+9sNi!4d=|^=>l%!-0?zp|{KpVv#NdmU=4QJWdj}2%fFUX<_y2&sxyCqXI*FAB`+|MJFGc+tfBe?96iS^Z@Ue*xz&&+wNQ{VPoT6;l7o z8U9L&{z@SK%8>tmtS013@60&np--La0{~Ywz)$H@_ z)*Cj*g@=Ym;KGg8N{=Oig~J4S~VYN8x7~%_if21ot5@4lcWjb{_oUe*rDtNZtSd literal 19845 zcmeHvX;_ozx-QnXEbCIXwUsUvuoa}JfEBGWX1OZGNEJl}8A7yGnG_HRks-utt+iCp zFr+AuRzQRRQ6U5fBmzzhLPbL=AwJN*&^E2Mh z@b%2Don8yxS^AENiOB-wwynMH-jU<#Herc?(@j3^7+)y!&MpzDv zzM5|Xe$O13bbTp0h45G~pc#8S_c)j^_EV4KhHKMHrYZjmQBE$-1X^WTn`%1fQHDFl zKSh^3h39RkI%8$>smf#pl3=}H^?m%D`oU5C23NLPnPjF9im{IACV$fu{F1)*JlKMO zeKq$+yY5Y$P`4M&=+Vw+Z^D~=@bV-WDhWfWO@8rEt503LgEQHz4DGdyMGO}(75B~h zaXmtrH^CZ-yEr-L@f~~VaWfOTd`#e>3u4K7@WW@ISM1l^GBXJ*isK)`*)>jnMp5@c zo7N}A+>uv=#cNwf^O?+-Y%8Ajcv7Y8fon?p{l)3H{i=ct#Msa?u($74z6A!Oer98i zKBk?sPe#|xzfr{=Tp9+(Mh9=2nRNCkI+K{HC#J_kHi?@(OhS`=z^mE3|k1|^v=Jw85}t{lS8`L($Qub%Vxz-rxF&?769!%P!56}nHAZc?AF z5;LLeAX$YI&`ssK!9(toCc7*tmPK7xRjX{hGM_u+PXLmPZN!bCP113~Y^z5evPa-8 zq|epxJw~9TYUm=W-1IoNJTr6oy#(!>5OtzcCebF{u@~?f^gx#QMQ5zS5#oxkykeO) zU-#BN+}NCb*oxwnUa|tj7U=00pr>cV;-o9_xaU>i^-tW)dC*8oDh**$$<$D^<~poA zyyo*Ry{c$;`goXhjjDBTUg_j)2NQWHOq)c{(|h1FyPZ40FPA*I(HRIR#+kL?dNPagpPR{uWH>m&?4F%*|H)eV~ z!!by+kuY{z28433NcYBLK$YIJ@Lt$>x{6-~?XAg7x|`It77benHu__vvIIe+R>MOT znyYT&+%%WWuLBJ@EohDDTNhjOA9T;))5UlFNnYjTB!8uz_jxijggc=h zs>!U>t={c%Hb38W^Zk!o1{rnKI~GT8F7jHQf2?HS_NH}C3Fi7U_I@c>z`JFB;dWgc zEqCtf8t6Rp`Mh!bUU^iOD>x=|^ZHlY8Y1xwos*L+Q~!pJ)1t-8_4{ECXadhjBf+v3 zuxw(2no_UY-~|s}ULK_LwgpnG?p!`s&1+jgDETIrgGoLIZsv0TB-tRi<`93=dDW*b4tmg3*cAI@DuWQ1nbf$PARR~WfsksStIAIp$VlcoSd+U zmv;Y4W7UVZU)rmzL1!JNtmZu#B6pm$^|H31gg*B^!L;b=LJJj(e~>CE(kGW5XKFn& z8Ov}s+qU~tUv{Fv&I=Dcr^H{{eWDpxW$Eyac$mi@rt_J*DbfG*?XTe0_m>hM;az{s zXZ8d=|KPFCvO3lN`xh^UII0-cxEfa-HcXJR#J6oa%04mXS={b87UE7{`}u+ zUy$P?OxsRS`rA)rqrekWQ=nIisUKf2#-EqoeTu)AO~UZ&OB$&3qZy)nK-@c1#QnIw z7*8DCma(RK@a3@8Lqu&(UDT%Pq>+OY(A6agdO@_wo522jyxW1%EXk}Nwka(So@6N^&GY|gizvlgg#amC{VbzcBw78*WWSG$>UA}zr#;LSspm_JagxU?N@YdN{S~T z=lT(%12cyA<)P;>MTnM}A4x_v^6txNfBdM>?O0uPfE3qA!41kGB@TnW{>Wk1|=N z87?LDPPrxD)ipdhpe)VE>ZUR4!l>$jFaO!%pGbZ{CXlpYQ85EN7JGPD5ww1 zM~^|9H_|d36m@&-K4Volr|eX{?3j<`m8E@%&xVYf^pc~M1Bq3A&JW`TU6#x7B&Ko7 zbAFCuEq6Bn*Bbgxg|{32#BPtqnH0O85wkkfJX`i><5i{g z7B2WFVpR@1tRY^(g&5SC!El*G($`q~SoHW2J_?p6-7gg;rD$VV>ylRskeiDx`&C`Y z7SxgtH^e#ZYW7jJZhYZ!*2@JLnzZY|kW?gG{h(%S>cC&3d2}gP7QWDdwXWkq(*6Q8 zOVsAz_$(HZBUIQUJ|u+i6Dl&pS0fiyrP7Ce50~*@Nw;B|(r8j#5-Dx08;idwHq#uq zTYL@66Lee@cCOat`V>9zcro5Oas8QjD$wCj)C2zFB0v-_Yol=69fNy!Ht_xmGgsC6f^#$26{OLt=g zyJHTL=%d3Sf>`Q?5HSj!U)E{Xudz$PaF{Q0F}2Ol)TdZZJ?MIE(+`SLNFj-V6Yutk z-@S0mS_~09-YO(Lrr(Rxe?K9ZM(2xZ)l3$5P7;2IQuaV-Ix@dsdywP(3WDSHsw(Lz z^$tjbJJ+Pn6GkZRt%kQaA_=~@gN~tdQTTKstx?+5!~40@tQT9W>8vyFHF#y#{La!2 ziX13kLnvaAg*`LqR$;ldXRQboT@y)TNDD?4^os|^W7xdW4krZuv;$%r@BV}%Cv2Oo zW5Z-CH>EI=T68oHf~K!Pz``Rt}Hm1IVTJc0?7)^&j|(zq@uhp>}^=5=!8jC>|v6F zxDxuWJ()Kl?k3jOAeW*W))C*7#}7P`*?zzuJs}2bV2OWkdeA5HQJxKPo+F90U%ybQVl>Ev&gjE6d zEa`_#&Xhk-1J|~yynfE52vQWW=#n=>e!s&!F~7bB0&J|exXmIB$7Tb6+W^FG zFV~ADuc-4AQtwKVV41LIvYO2TgXL4Y2GJKIsf&`YV0J?|hXvf!c3vI!QA>$il{473 z81euj$L+&bxjiv#mHCV>vZ=bq^-KMRCxToRW(!s#yVXZC=CUSvG4*l-@#(@D=U*XL z?8`m%a5fn$gUdQ!0B^>Jv;<0Y@S_bbX3Uh_XQy{U=UpfB>);ZwWq;X1NR2L(bqvtSpCd z*(YEReI4zzItC6bm+oy%X}W#F2mp7N$iL1vhw!XNV|{1%pr~UA$$AQ5oPPI;6D4M@K@ovP4fd>e z)KQSo0Q{he(ZK3LUlaA^YsW`zEOe}%Xa@)x79L#ueMcWx^r!P??Cscoew0gaiWkp5 zYwfft<9~Uj=p}G-JK2xFnWca!ks>e~P4B5(a;V|3;uZ`c*U$lkWT`^Yhrh~9giz}6 zki+VcAj8bH?2%s8;1A)>fS;jZkG%Ja%nA+Mt`G(Ho~DvVB5)#)glTF*fJph}vIIS- zDlXj7Thq@P6TWen)X?mM(c3&Jg{VD^LS+`u;@?<%RFwG?FrbId_1>Gs-n+}lKPoSH z1nuCrR^Ff_0ryw`yE7_SiANU%_<$e}f~?hF+f-g8uWyilHb{sC`%bBWVVtibNs?^D?_a?i}Q|XI2a_QtD40 zNjpmQabSP<2^$i{;S}I^#Lwl7nJ-Rd{nbc~B66 zx#u!tlf$PUWbh@@>tU@G>5268x2;C7hX@-k0my4JekFK^AcKsP*1d*r?vGx;7|DU* znt{_!6ui%$oJxs48;&{MKY03j(<+4&%I$rt^WB!FQa zig&+njr;ZFL4U@9kP4%)^as%O3EqNLh= zy~c82xt1)o;rLb=6a+Qb;EDfeyLbaWiecZluF9wn0yZ%EaNmrvcNex?UyZ&#C3=vP z^haDnA*O>a-G?J>;Ly^Bj0l6Dl7#)jbq)TiW(M}A^qo9N-5~eAh&7W6A*eRk{1Y7u zh`VMMr}UH5ILIVp9f!rJ(BQUN3?JJUWPeZ7n{?HsRWbn?urlc#`N(t@=j$xu2lf#As36dH&I-@60T!FvX&DQd0((f-lQB(}kwdzmY1evjKF zp%r|?-I5mg`K0~{uj7rtwk=UBtu1~lyKW}3v4Vy1QQm5NK7=#DBk0WUF8;CDK0=kL zZD3m$n54ILwYLvhjKERQEtFqx4JMD23#m2_E0ZsiP8w~yY3 zn1o$4o0HHsXCO>891)&9R%zN96)7f8svx0sy*KgBr@b`4{M7e`Q)qW#P2P!02EAyVGN6N%o(hH&7%%4m;!|VZ1!qDH& zTbe)KE=Op<#qFg`H&H}OiRcv+gNQ8sxAdNdWw5TaamHIsaqtf;RQZf|=WN{N zE(&6)mRDU1nu&}cF2S~mI4D2l2IEFZQ(klJuvs*3;R%4~r^rFLdVlFfV{;BGKDvKl zfY&4!60E>KW>PuM5JjbeVCN5b{i($A85{DLO@A`!Kas2Cf7_VSQFB=jia@5~R_Vsd z*Ki$Vqf3mO<;y=|k-?k)ubGKyIdg_Auo0MpoyU~5hwN{lW;jXncMrX8voxnm%z*so zJ$Lxhb+5$}tUgQkM~3`AGOmAOfp5-87DV@17|kq?)jQyL#FV^J9WxaY{(&L?zwWo= zgSf#zPUlkqhDkfpYUQo>$jJiBHr%=Rb19XOShEZDPTr4$0|StWE9tb%Zc`y?%$7EokemB#~S6T zL!*|O zD@NL&Fvph7mtU(;dW@ z2)6@gB{ZSr)of>e0o5=SDTjICOg_f%YWU{z@@YrDff2vokjMj>wYz(aoOA7`*gYZx zo`$k!J?0@iBMz603wAs-N+;&=Mh8!?sY*3sZEqtsgqnH|&OO6vtI}Awj6>*KE{|Of zL9G;x0yMEGL0tN0$-{p?q3hnjkn}NnPO&!B%s?oR9G>#f(Xz6Gk@^TI`{0t@uVuid z^N3lxOKW|id4i$uFBk>&XL7wU$T-J4h5sjc*0MBR)a*PMl-4||yS)gq15nESO_uhT z1}|k{zV+EF{na)*DnkvK3-)f1akBD*lAbXAUHTtcgZ#{oGx5Z~kz3@`8s$&63&V^V zEanD?rkt^HI(g$A%`#Isp$d8pO2!OX>;8cqR09NvHBU^nLx2QH&yrx>tN=!NGFO;r zl-vG)n$7ZH!rZ66wXw>G`{cYR#Z(m{*d0_VL0uytXjWaLzge1w*NEF=CG0?VOUZ$& zqh)TYMIC66ar+05)F)8d^*5=^VYz>iWLz?s!ct_D?gePuj(AzB|JHnIT8e%z8?Bk> zkR4%S#=gl8r1H49Um2fa(&Xr82kOxHt>fKZw(-)>Hj+nMBMjMmC=@#);Tf}@L6N3$ zVlCXCJo6V}$>{g`ejdwL_(bv!$g8T=!+l=iRYAGB<@!8ZAn2m8!@=pi<-3v@UZxzu3&vvM3JsNPq;{-JR zCW?Z8`o=L&y6z^N57$?B|B$K0XwPIZtBt$T7LfWF|FKdQ8-Y=?NqY;u#~tayY~+S( z+4x<0><&MdGF`X;8Fzi{xjQqZl_Ft&if`DN{^9pmrZ@%*Aev?AFYc986fi*x)#9CD812fB z(&XVNgfC96RPj=*Fp}3wQ*if!7d=2#r4|+4pQtLfEod55d)3C^J5XqK5VNS2!u(z+ z!Am=x6;&01EZ-xP4e4aPo{hiOTfrp9Yey&B7iWfz3$w0-?bU4WA>pMSY@Yhrd;Rz! z(PaB!+w2$G@3IvvGAOpCZu&s|Ib2&X2pio00Xi|LybhlDA62Q#B)Nea<;oP)imZioJ&$m#T&qP- z)`TDBzt@fL6TcNF)%F|;AWl~&IB#*oI(l4!-_6h3=37+ZUNKP0^LN(WZ<%nWQyQf! zhe9>N0yka9`QpVdHp<$wei4Ym<{4{Zx;G)G;s{au`0~M>bb)V?pDIp%cbwF#nQTYg zxvyGY*xD}R1Qs^w+6x48uXytvnFe;lRW&pfrEA%lZr}U3W6e2c7bI~|u?wJvQbg0y z^JB^U)|s+1d490|tQQPV-_!l48r=1Hy+OuQcNC&N^8CZH`2AC53fRSw>!Lxm*nE{f zodQc4**Y?S6%#7%B|M*97|8=}w*PD=D!6yxbv340M;;Kxgl_4**-;w&HChCDh<v~N+YvS>Z%!*LD5822HWVjqss`1b-?P;pLr8c)pIK$}&Biyn_V4|-}1 zSE#@=b}@)g!=~`UHp06B@F@;oXxBq#!C!_eiHTi*grX95nj8BT7i;mQ^#i z_uj3zwRg6-^q83}4H)m)Em})Y-9aBOXksbOPgtbma!bxw99ak(>;KH~EE(zOL^obbmEV6E+{iw`LRWG&|z5CJxu;CNt0|=*iMc+W3vkEk3V>oIoQx# zu=9`@^Gx{&z1(UuB&`@;b`0+ zlCo-J^we{Mr5P-muw z`vmGv0G+2rp+Ky}l7?2XM8_5sEPs^VmmB@G!^cfG!jk!neqkgv2c#n==w54qpo;E8g-) zonui~M}FxAl9o61tth zwWTHE^i9MB!8U4SEN5~q(JU*IXx7;uTh)uMXq0vu(%>BS=o9e+LsN0Q;dpy6@uaN} zr)v3Lc4GT1bW^-;L$w&OfcUgDEe| zN{u_FS7!zCR-*yXc4Md(ipUO(1l7V`bNSSLLD=m@ygt)l-TzCew9p0xbq3TJs>(D9 zpJoliKC_tM(GF~0V)LK+TSN1VH~!euk49w1?2`xm7op_tVYctL9T%?{wP@v9+w>&a zAH32$-49joaRli*lrqq~jY0`sC>s4P%#p8>!Ociw>IdLMg}40%yK2p_SvMD)d!la% z8|Y!3Z^=bq`~FS<4gXRzWm1W1k9V!mR){TsQ0?Ii2EWip z=L7dyEwAi7_q+{m=ek<=qgd;|KofmYF!(JUbRZ1e5`6}~&Y=5am+I3sH#DIZ;G?IM zT^QH%&pb{;?Ow~lXFVmRRN4}{sRGlz-V~dN*#*8n*^(?iQcquewiPwtkz@Ka3YKUqLA4xe)Q$Tgwn`S#L$pRp zM*DcmiM}uOrMtfl0R+Zt0|+H|#{18fR)howVz%VLY20`2O!m1j$JH_#_;RM29kbS- z+t}DK|2mASr|C9(ZSod4;RnQ~O(<0N$$EI(31jtP^lN??;pl3|yEgZ53!$d*dJ=gh ztxr)UATe{TM~*fpeN{f%#{ynKU)gN^gh2%i)~Tzkky%3OGH-ucb+z<_zH6htn0#_- zZHYSQYUIoWxGShWgL9hA*6$6*>x{-jdj|w@n|escRShoj(S^Jets(@O;<+TYJ4JNc zmsLe3H?Ghd%2QvZ&5Y;8O^-LNfLnuLovp&83l)M`n-Tk5Y-ih(s_Y=?j9HcV(QHZj^W&`2|3PM~`?QAL*gwmw1_w~9Y7 z%X)-BjJClAu~zN!sD&DhtEdGt=0h(BHaHw-p;PLAA#sh=g5S~C5yu1J=NRQ7YZ@C1 zx5bm%K=^ zIRtPKNoag1q{x&lrpMs|#Yxvjmt&9&Fo3xWq-pyE4YViXog3c49#y?m0>wxJALUiY zXsRgU4gGM5XBvmcyxP~upbPb#JK&qYku9RCTl#8yiv`L8 zIEIv6MXAfgVql`pX=dOMaR4{6_CLCYZ$;jv@1FWnqywBdtnJcM^m^RK$nC!LKsoi{ zwb7l$?r^gQWO4{41-Z+{&r{sc?^GDcz%^^TaQ^4GXKQ+hC!%7F-11M>b|E70J$uV2J@1)75VR`B=f?u-o@rH3!&`Cw1nm4DZD=0P-kZ8L&p&WsBF z&mR1#sl8{<;Low0Js%C~hJ38l#do>@(ukSGtF*fQ5TCV)C9+Kgllt-9M&xK>wa+JB z;y$IvUKesBd+=iMF=l3>_8C9iO1s*Ugi&!b${dltp)9L%BWEodrt(`*I^{Q9TPt_j zGcUB&NS|7Okc&g8+VM1rb~BRjFLKPhenhxB0C4sz^-rZ&T#kt|d>GO?W_Nh4e-(}Z$fESH;6W;3=lWHa$EubcnCm$dp>>4pm z?t3puDIl=`r3%oTgIZZ&A(Pb}WYyer-%&6`*+wW4i?rvXra|IQ;EU;#vxJO|*z+}|`15OYt^}J} z)v1z?LtSQZ$AM0J9Hvw6#>KXgKuhnyeGqUFN-}pa4d(P3_#~MpH8|#c=#MEtmEdE> zuCC=T)+VfT0)J#5fakztUZ7kFkGb7A^ZEWm;Lr1Y;rS=wu{%@i+*)O}`{1!T!0tdB zIGKW@_3Lg=ty@%tm;;ZQ0pXGda0fVk0l=m3JLGf0zfXjE(>Hu z^<&=%d~U#FbAjW64nDLH^q1<#ewbRf68L<0%n|@n_*n^8Yr0zCbze=bvwWghtbek} z)RRvZ9$ctj_tdaX_BViG;3xk@D!>WZU!(%^@t0IVn)H_f0=w{+t%B_HUuGRxh39{H zKwuUA0t*mY_zNuj1s48-p!v!FKcIEpoK@#}MTN(&q=5hLnMI=K-o!nzhZ3>-;}4qb z*s{ZO#X5)eo1IsDjERYkiN(g)Ezul{jYr|{u|q?q^JM@ diff --git a/apps/termtris/evidence/termtris_frame3300.png b/apps/termtris/evidence/termtris_frame3300.png new file mode 100644 index 0000000000000000000000000000000000000000..92dafb50bba599c627d5b9dc22397f265167f427 GIT binary patch literal 20986 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 literal 0 HcmV?d00001 diff --git a/apps/termtris/evidence/termtris_quit_3200.png b/apps/termtris/evidence/termtris_quit_3200.png new file mode 100644 index 0000000000000000000000000000000000000000..098ab58ebd90278d98366160625b29c0b94e38d7 GIT binary patch literal 19407 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 YgV)+DzKH!1%u_*H+wCpbed7E70MbA&RsaA1 literal 0 HcmV?d00001 diff --git a/apps/termtris/src/game.c b/apps/termtris/src/game.c index 6209491..4eb2cb6 100644 --- a/apps/termtris/src/game.c +++ b/apps/termtris/src/game.c @@ -25,6 +25,8 @@ along with this program. If not, see . #include "term.h" #include "scoredb.h" +extern long now_ms(void); + int quit; long tick_interval; @@ -190,7 +192,7 @@ int init_game(void) scores = read_scores(0, 10); memset(&cur_score, 0, sizeof cur_score); - srand(time(0)); + srand((unsigned)now_ms()); pause = 0; gameover = 0; diff --git a/apps/termtris/src/vt1200/compat.c b/apps/termtris/src/vt1200/compat.c index 02c67b7..b216082 100644 --- a/apps/termtris/src/vt1200/compat.c +++ b/apps/termtris/src/vt1200/compat.c @@ -17,6 +17,12 @@ int fflush(FILE *fp) return 0; } +int putchar(int c) +{ + (void)c; + return c; +} + static char *fmt_num(char *out, long v, int width, int zero, int left, int unsig) { char tmp[24]; diff --git a/apps/termtris/src/vt1200/main.c b/apps/termtris/src/vt1200/main.c index ce75f2b..650b978 100644 --- a/apps/termtris/src/vt1200/main.c +++ b/apps/termtris/src/vt1200/main.c @@ -18,6 +18,7 @@ static vt_u32 evbuf[24]; static long ms_base; static int ms_last = -1; static int focused; +volatile vt_u32 dbg_loops, dbg_events, dbg_last, dbg_focus, dbg_rev; long now_ms(void) { @@ -88,10 +89,18 @@ void vt_handle_event(void) int n; VT_XNEXT(vt_dpy, (void *)evbuf); + dbg_events = dbg_events + 1; + dbg_last = evbuf[0]; 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] == 5) { /* ButtonRelease: sample focus */ + vt_u32 fw; + int rv; + VT_XGETFOCUS(vt_dpy, &fw, &rv); + dbg_focus = fw; + dbg_rev = (vt_u32)rv; } else if(evbuf[0] == 12) { /* Expose */ game_input('`'); } else if(evbuf[0] == 9) { /* FocusIn */ @@ -111,17 +120,33 @@ int main(void) { long d; - if(vt_x11_init() != 0) - return 1; + /* The display server may still be coming up when this process is + * created; retry for a while before giving up. */ + { + int tries; + for(tries = 0; tries < 40; tries++) { + if(vt_x11_init() == 0) break; + VT_SLEEP(50); + } + if(tries == 40) return 1; + } + onlyascii = 1; + use_gfxchar = 0; + monochrome = 1; /* the VT1200 raster is 1 bit deep */ if(init_game() == -1) return 2; vt_x11_flush(); for(;;) { + dbg_loops = dbg_loops + 1; VT_XSYNC(vt_dpy, 0); while(vt_poll_event()) { vt_handle_event(); } + if(quit) { + cleanup_game(); + break; + } d = update(now_ms()); sleep_ms((int)d); vt_x11_flush(); diff --git a/apps/termtris/src/vt1200/scoredb.c b/apps/termtris/src/vt1200/scoredb.c new file mode 100644 index 0000000..0c227af --- /dev/null +++ b/apps/termtris/src/vt1200/scoredb.c @@ -0,0 +1,76 @@ +/* + * scoredb.c -- RAM-only high scores (the VT1200 has no filesystem). + */ +#include +#include +#include "scoredb.h" + +#define NENT 10 + +char *username = "VT1200"; + +static struct score_entry list[NENT]; +static char names[NENT][16]; +static int inited; + +static void init_list(void) +{ + int i; + if(inited) return; + inited = 1; + for(i = 0; i < NENT; i++) { + strcpy(names[i], "VT1200"); + list[i].user = names[i]; + list[i].score = 0; + list[i].lines = 0; + list[i].level = 0; + list[i].next = (i + 1 < NENT) ? &list[i + 1] : 0; + } +} + +struct score_entry *read_scores(FILE *fp, int max_scores) +{ + (void)fp; + (void)max_scores; + init_list(); + return &list[0]; +} + +static void copy_entry(struct score_entry *dst, struct score_entry *src) +{ + long sc = src->score, ln = src->lines, lv = src->level; + char *u = (src->user && *src->user) ? src->user : username; + strncpy(dst->user, u, 15); + dst->user[15] = 0; + dst->score = sc; + dst->lines = ln; + dst->level = lv; +} + +int save_score(struct score_entry *sc) +{ + int pos, i; + if(!sc) return 0; + init_list(); + for(pos = 0; pos < NENT; pos++) { + if(sc->score > list[pos].score) break; + } + if(pos >= NENT) return 0; + for(i = NENT - 1; i > pos; i--) { + long s = list[i - 1].score, l = list[i - 1].lines, v = list[i - 1].level; + char tmp[16]; + strcpy(tmp, list[i - 1].user); + strcpy(list[i].user, tmp); + list[i].score = s; + list[i].lines = l; + list[i].level = v; + } + copy_entry(&list[pos], sc); + return 1; +} + +int print_scores(int num) +{ + (void)num; + return 0; +} diff --git a/apps/termtris/src/vt1200/stdio.h b/apps/termtris/src/vt1200/stdio.h index 66f6fe9..7a3b79c 100644 --- a/apps/termtris/src/vt1200/stdio.h +++ b/apps/termtris/src/vt1200/stdio.h @@ -11,5 +11,6 @@ extern FILE *stdout; int sprintf(char *buf, char *fmt, ...); int fflush(FILE *fp); +int putchar(int c); #endif diff --git a/apps/termtris/src/vt1200/x11.c b/apps/termtris/src/vt1200/x11.c index d2ea2bc..a79053e 100644 --- a/apps/termtris/src/vt1200/x11.c +++ b/apps/termtris/src/vt1200/x11.c @@ -19,7 +19,7 @@ struct vt_cell { unsigned short ch; - unsigned char fg, bg, dirty; + unsigned char inv, dirty; }; static struct vt_cell cells[ROWS * COLS]; @@ -51,14 +51,18 @@ vt_u32 vt_root_window(void *dpy) int vt_x11_init(void) { + vt_x11_progress = 11; if(!vt_install_xg()) return -1; + vt_x11_progress = 12; vt_dpy = VT_XOPEN("0::0"); if(!vt_dpy) return -1; + vt_x11_progress = 13; vt_win = VT_XCREATE(vt_dpy, vt_root_window(vt_dpy), 80, 80, 480, 312, 2, 1, 0); if(!vt_win) return -1; + vt_x11_progress = 14; VT_XSELECT(vt_dpy, vt_win, 0x22000FUL); vt_gc = VT_XGC(vt_dpy, vt_win, 0, 0); if(!vt_gc) @@ -73,8 +77,7 @@ void x11_clearscr(void) int i; for(i = 0; i < ROWS * COLS; i++) { cells[i].ch = ' '; - cells[i].fg = 0; - cells[i].bg = 0; + cells[i].inv = 0; cells[i].dirty = 1; } cur_row = cur_col = 0; @@ -113,8 +116,9 @@ void x11_ibmchar(unsigned char c, unsigned char attr) if(cur_col < 0 || cur_col >= COLS) return; p = &cells[cur_row * COLS + cur_col]; p->ch = c; - p->fg = (attr >> 4) & 0xf; - p->bg = attr & 0xf; + /* The VT1200 raster is one bit deep, so follow the game's monochrome + * convention (ansi.c -m): colour is dropped, the bold bit reverses. */ + p->inv = (attr & 0x80) ? 1 : 0; p->dirty = 1; cur_col++; if(cur_col >= COLS) { @@ -132,42 +136,54 @@ void term_putstr(const char *s, unsigned char attr) void vt_x11_flush(void) { - int i, row_dirty = 0, dirty_any = 0; - char one[2]; + int r, c, any = 0; + char line[COLS + 1]; + vt_x11_progress = 1; - for(i = 0; i < ROWS * COLS; i++) { - struct vt_cell *p = &cells[i]; - vt_x11_progress = 0x100 + (vt_u32)i; - if(p->dirty) { - int x, y, bgpx, fgpx; - x = (i % COLS) * CELL_W; - y = (i / COLS) * CELL_H; - bgpx = p->bg == 0 ? 0 : 1; - fgpx = p->fg == 0 ? 0 : 1; - VT_XSETFG(vt_dpy, vt_gc, (vt_u32)bgpx); - VT_XFILL(vt_dpy, vt_win, vt_gc, x, y, CELL_W, CELL_H); - VT_XSETFG(vt_dpy, vt_gc, (vt_u32)fgpx); - one[0] = (char)p->ch; - one[1] = 0; - VT_XDRAW(vt_dpy, vt_win, vt_gc, x, y + BASE_OFF, one, 1); - p->dirty = 0; - row_dirty = 1; - dirty_any = 1; + for(r = 0; r < ROWS; r++) { + struct vt_cell *row = &cells[r * COLS]; + int dirty = 0; + + for(c = 0; c < COLS; c++) { + if(row[c].dirty) dirty = 1; } - if((i % COLS) == COLS - 1 && row_dirty) { - vt_u32 ev[24]; - vt_x11_progress = 0x10000 + (vt_u32)(i / COLS); - VT_XSYNC(vt_dpy, 0); - if(vt_x11_drain_events) { - while(*(vt_u32 *)((char *)vt_dpy + 0x54) != 0) { - VT_XNEXT(vt_dpy, (void *)ev); - } - } - row_dirty = 0; + if(!dirty) continue; + + vt_x11_progress = 0x100 + (vt_u32)r; + any = 1; + + /* repaint the whole row: black background, then the text. + * One fill + one string per row keeps the request stream small + * enough that the in-machine X server can drain it. */ + VT_XSETFG(vt_dpy, vt_gc, 0); + VT_XFILL(vt_dpy, vt_win, vt_gc, 0, r * CELL_H, COLS * CELL_W, CELL_H); + + for(c = 0; c < COLS; c++) { + line[c] = row[c].ch ? (char)row[c].ch : ' '; } + line[COLS] = 0; + + VT_XSETFG(vt_dpy, vt_gc, 1); + VT_XDRAW(vt_dpy, vt_win, vt_gc, 0, r * CELL_H + BASE_OFF, line, COLS); + + /* inverse (bold) cells: white block with black text */ + for(c = 0; c < COLS; c++) { + int start; + if(!row[c].inv) continue; + start = c; + while(c < COLS && row[c].inv) c++; + VT_XSETFG(vt_dpy, vt_gc, 1); + VT_XFILL(vt_dpy, vt_win, vt_gc, start * CELL_W, + r * CELL_H, (c - start) * CELL_W, CELL_H); + VT_XSETFG(vt_dpy, vt_gc, 0); + VT_XDRAW(vt_dpy, vt_win, vt_gc, start * CELL_W, + r * CELL_H + BASE_OFF, line + start, c - start); + } + + for(c = 0; c < COLS; c++) row[c].dirty = 0; } - if(dirty_any) { - VT_XSYNC(vt_dpy, 0); + if(any) { + VT_XFLUSH(vt_dpy); } vt_x11_progress = 0xFFFFFFUL; } diff --git a/apps/termtris/src/vt1200/xlib.h b/apps/termtris/src/vt1200/xlib.h index a51293e..342c4c8 100644 --- a/apps/termtris/src/vt1200/xlib.h +++ b/apps/termtris/src/vt1200/xlib.h @@ -26,6 +26,7 @@ typedef int (*vt_xfill_t)(void *, vt_u32, vt_u32, int, int, typedef int (*vt_xdraw_t)(void *, vt_u32, vt_u32, int, int, const char *, int); typedef int (*vt_sleep_t)(int); +typedef int (*vt_xgetfocus_t)(void *, vt_u32 *, int *); #define VT_XOPEN ((vt_xopen_t)0x3F9C2570UL) #define VT_XCREATE ((vt_xcreate_t)0x3FD41B90UL) @@ -41,6 +42,7 @@ typedef int (*vt_sleep_t)(int); #define VT_XDRAW ((vt_xdraw_t)0x3FDA0000UL) #define VT_XPENDING ((int (*)(void *))0x3FD67620UL) #define VT_SLEEP ((vt_sleep_t)0x3FB33A20UL) +#define VT_XGETFOCUS ((vt_xgetfocus_t)0x3FD55210UL) /* display->screens[] is at bit 0x480 / byte 0x90; screen->root at bit 0x40 / * byte 8 (see dwt/notes/GUI_APPS.md and dwt/gui/xhello.asm). */ diff --git a/apps/termtris/tests/input.lua b/apps/termtris/tests/input.lua index 2f619ee..ca3c703 100644 --- a/apps/termtris/tests/input.lua +++ b/apps/termtris/tests/input.lua @@ -35,6 +35,7 @@ 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 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"] @@ -151,6 +152,8 @@ local function key_step(frames, key) 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 end local function r32(a) diff --git a/apps/termtris/tests/input.sh b/apps/termtris/tests/input.sh index c43cd69..6453f57 100755 --- a/apps/termtris/tests/input.sh +++ b/apps/termtris/tests/input.sh @@ -20,7 +20,7 @@ LOADER_OUT="$APP/build" \ -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" || { +grep -q 'INPUT: .*bytes=.*61 9B 41 9B 42 9B 44 9B 43 30 0A 7F 71' "$LOG" || { echo "input.sh: mapped key sequence missing (see $LOG)" >&2 exit 1 } diff --git a/apps/termtris/tests/play.lua b/apps/termtris/tests/play.lua new file mode 100644 index 0000000..5f35daf --- /dev/null +++ b/apps/termtris/tests/play.lua @@ -0,0 +1,345 @@ +-- 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 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 + 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' diff --git a/apps/termtris/tests/play.sh b/apps/termtris/tests/play.sh new file mode 100755 index 0000000..56f88fb --- /dev/null +++ b/apps/termtris/tests/play.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# play.sh -- load termtris, click it active, and snapshot the board. +set -eu +DIR=$(cd "$(dirname "$0")" && pwd) +APP=$(cd "$DIR/.." && pwd) +ROOT=$(cd "$APP/../.." && pwd) +M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} +EV="$APP/evidence" +LOG="$APP/build/play.log" +mkdir -p "$EV" "$APP/build" +rm -f "$EV/loader_frame3300.png" +VT1200_SKIP_LEVEL1=1 LOADER_SNAP=1 \ +LOADER_AT=2500 LOADER_END=3300 \ +LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ +LOADER_COFF="$APP/build/termtris.coff" \ +LOADER_OUT="$EV" \ +"$M" vt1200 -rompath "$ROOT/harness/mame/roms" \ + -video none -sound none -nothrottle -skip_gameinfo \ + -debug -debugger none \ + -autoboot_script "$DIR/play.lua" -autoboot_delay 0 \ + >"$LOG" 2>&1 || true +grep -E 'PLAY:|LOADER: result|LOADER: snapshot' "$LOG" | tail -6 +[ -s "$EV/loader_frame3300.png" ] || { echo "play.sh: no snapshot (see $LOG)" >&2; exit 1; } +cp "$EV/loader_frame3300.png" "$EV/termtris_frame3300.png" +echo "play: $EV/termtris_frame3300.png" diff --git a/apps/termtris/tests/play_keys.lua b/apps/termtris/tests/play_keys.lua new file mode 100644 index 0000000..38f5f77 --- /dev/null +++ b/apps/termtris/tests/play_keys.lua @@ -0,0 +1,390 @@ +-- 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 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'] + +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 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' diff --git a/apps/termtris/tests/play_keys.sh b/apps/termtris/tests/play_keys.sh new file mode 100755 index 0000000..4d396b9 --- /dev/null +++ b/apps/termtris/tests/play_keys.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# play_keys.sh -- run the real game through move, panels and quit paths. +set -eu +DIR=$(cd "$(dirname "$0")" && pwd) +APP=$(cd "$DIR/.." && pwd) +ROOT=$(cd "$APP/../.." && pwd) +M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} +EV="$APP/evidence" +mkdir -p "$EV" "$APP/build" +for mode in ${MODES:-move panels quit}; do + LOG="$APP/build/play_$mode.log" + rm -f "$EV/loader_frame3200.png" + DL=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_loops") print $(i-1)}' "$APP/build/termtris.map" | head -1) + DE=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_events") print $(i-1)}' "$APP/build/termtris.map" | head -1) + DT=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_last") print $(i-1)}' "$APP/build/termtris.map" | head -1) + DF=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_focus") print $(i-1)}' "$APP/build/termtris.map" | head -1) + DR=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_rev") print $(i-1)}' "$APP/build/termtris.map" | head -1) + DQ=$(awk '{for(i=2;i<=NF;i++) if($i=="_quit") print $(i-1)}' "$APP/build/termtris.map" | head -1) + DTICK=$(awk '{for(i=2;i<=NF;i++) if($i=="_tick_interval") print $(i-1)}' "$APP/build/termtris.map" | head -1) + VT1200_SKIP_LEVEL1=1 LOADER_SNAP=1 PLAY_MODE=$mode DIAG_LOOPS="0x$DL" DIAG_EVENTS="0x$DE" DIAG_LAST="0x$DT" DIAG_FOCUS="0x$DF" DIAG_REV="0x$DR" DIAG_QUIT="0x$DQ" \ + DIAG_TICK="0x$DTICK" \ + LOADER_AT=2500 LOADER_END=3200 \ + LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ + LOADER_COFF="$APP/build/termtris.coff" \ + LOADER_OUT="$EV" \ + "$M" vt1200 -rompath "$ROOT/harness/mame/roms" \ + -video none -sound none -nothrottle -skip_gameinfo \ + -debug -debugger none \ + -autoboot_script "$DIR/play_keys.lua" -autoboot_delay 0 \ + >"$LOG" 2>&1 || true + grep -E 'PLAYKEYS:|LOADER: result|LOADER: snapshot' "$LOG" | tail -5 + [ -s "$EV/loader_frame3200.png" ] || { echo "play_keys.sh: no $mode snapshot" >&2; exit 1; } + cp "$EV/loader_frame3200.png" "$EV/termtris_${mode}_3200.png" +done +echo "play_keys: $EV/termtris_move_3200.png $EV/termtris_panels_3200.png $EV/termtris_quit_3200.png"