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 e00108e..20171e7 100644 Binary files a/apps/termtris/build/render.coff and b/apps/termtris/build/render.coff differ 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 0000000..5836bfc Binary files /dev/null and b/apps/termtris/build/termtris.coff differ 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 dfc882f..05de442 100644 Binary files a/apps/termtris/evidence/render_frame3400.png and b/apps/termtris/evidence/render_frame3400.png differ diff --git a/apps/termtris/evidence/termtris_frame3300.png b/apps/termtris/evidence/termtris_frame3300.png new file mode 100644 index 0000000..92dafb5 Binary files /dev/null and b/apps/termtris/evidence/termtris_frame3300.png differ diff --git a/apps/termtris/evidence/termtris_move_3200.png b/apps/termtris/evidence/termtris_move_3200.png new file mode 100644 index 0000000..1da3486 Binary files /dev/null and b/apps/termtris/evidence/termtris_move_3200.png differ diff --git a/apps/termtris/evidence/termtris_panels_3200.png b/apps/termtris/evidence/termtris_panels_3200.png new file mode 100644 index 0000000..586a0a1 Binary files /dev/null and b/apps/termtris/evidence/termtris_panels_3200.png differ diff --git a/apps/termtris/evidence/termtris_quit_3200.png b/apps/termtris/evidence/termtris_quit_3200.png new file mode 100644 index 0000000..098ab58 Binary files /dev/null and b/apps/termtris/evidence/termtris_quit_3200.png differ 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"