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.
This commit is contained in:
opencode.energia.arti.ee committed 2026-11-03 20:03:47 +00:00
1 parent c1945cfc73
commit 41d3cecd49
23 files changed
+1151 -84

No files matched your search

+2 -2
View File
@@ -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'
Binary file not shown.
+41 -41
View File
@@ -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: <RENDER.out>
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]
Binary file not shown.
+141
View File
@@ -0,0 +1,141 @@
*****************************************************
TMS340 COFF Linker Version 5.01
*****************************************************
Tue Nov 03 20:01:11 2026
OUTPUT FILE NAME: <MAIN.out>
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]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+3 -1
View File
@@ -25,6 +25,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
#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;
+6
View File
@@ -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];
+27 -2
View File
@@ -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();
+76
View File
@@ -0,0 +1,76 @@
/*
* scoredb.c -- RAM-only high scores (the VT1200 has no filesystem).
*/
#include <stdio.h>
#include <string.h>
#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;
}
+1
View File
@@ -11,5 +11,6 @@ extern FILE *stdout;
int sprintf(char *buf, char *fmt, ...);
int fflush(FILE *fp);
int putchar(int c);
#endif
+53 -37
View File
@@ -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;
}
+2
View File
@@ -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). */
+3
View File
@@ -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)
+1 -1
View File
@@ -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
}
+345
View File
@@ -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 <orig_pc>
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'
+25
View File
@@ -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"
+390
View File
@@ -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 <orig_pc>
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'
+35
View File
@@ -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"