diff --git a/scripts/m1/add.c b/scripts/m1/add.c new file mode 100644 index 0000000..1f76448 --- /dev/null +++ b/scripts/m1/add.c @@ -0,0 +1 @@ +int add(int a, int b) { return a + b; } diff --git a/scripts/m1/const_gep.c b/scripts/m1/const_gep.c new file mode 100644 index 0000000..8bd6aa3 --- /dev/null +++ b/scripts/m1/const_gep.c @@ -0,0 +1,3 @@ +static int arr[4]; + +int *second(void) { return &arr[1]; } diff --git a/scripts/m1/entry.asm b/scripts/m1/entry.asm new file mode 100644 index 0000000..514ce8d --- /dev/null +++ b/scripts/m1/entry.asm @@ -0,0 +1,36 @@ +; entry.asm -- M1 smoke-test entry, linked at bit 018A0000. +; +; M1 has no stack frame, no calls and no TI ABI. The generated functions are +; leaf functions: i32/pointer arguments in A0-A3, result in A0. +; +; Marker words (bit addresses, 0x20 bits apart = 4 bytes): +; 018A8000 add(20, 22) +; 018A8020 load(018A8100) +; 018A8040 second() == &arr[1] +; +; _late is linked but not called: it is a compile-time check that a surviving +; GEP offset is 24 bits, not 192. + .text + .globl _start +_start: + setf 32, 0, 1 ; FS1 = 32 for 32-bit MOVE + setf 16, 0, 0 ; FS0 = 16, for any i16 code + movi 20, a0 + movi 22, a1 + calla _add + movi 018A8000h, a2 + move a0, *a2, 1 + + movi 018A8100h, a2 + movi 12345678h, a3 + move a3, *a2, 1 + move a2, a0 + calla _load + movi 018A8020h, a2 + move a0, *a2, 1 + + calla _second + movi 018A8040h, a2 + move a0, *a2, 1 + + rets diff --git a/scripts/m1/late.c b/scripts/m1/late.c new file mode 100644 index 0000000..8bbf8e3 --- /dev/null +++ b/scripts/m1/late.c @@ -0,0 +1,9 @@ +/* Late-GEP check: p + 3 bytes must reach llc as ADDI 24 -- the address unit is + * a bit and tms-bitaddr has already scaled the offset -- not ADDI 192. + * + * The plan's source also compared p + 3 with another pointer. That form + * lowers to setcc/branches, which the M1 backend deliberately does not have; + * this form exercises the same late-GEP contract without it. See the M1 + * implementation design, sections 7.3 and 7.5. + */ +int late(char *p) { return (int)(p + 3); } diff --git a/scripts/m1/link.cmd b/scripts/m1/link.cmd new file mode 100644 index 0000000..2abf15f --- /dev/null +++ b/scripts/m1/link.cmd @@ -0,0 +1,16 @@ +-a +-o M1.OUT +-m M1.MAP +-e _start +M1.obj +MEMORY +{ + prog: origin = 018A0000h, length = 8000h + ram: origin = 018A9000h, length = 1000h +} +SECTIONS +{ + .text: {} > prog + .data: {} > prog + .bss: {} > ram +} diff --git a/scripts/m1/load.c b/scripts/m1/load.c new file mode 100644 index 0000000..d73be88 --- /dev/null +++ b/scripts/m1/load.c @@ -0,0 +1 @@ +int load(int *p) { return *p; } diff --git a/scripts/m1/m1-check.lua b/scripts/m1/m1-check.lua new file mode 100644 index 0000000..c205c38 --- /dev/null +++ b/scripts/m1/m1-check.lua @@ -0,0 +1,36 @@ +-- m1-check.lua -- load the M1 COFF with dwt/loader/loader.lua and read the +-- marker words the generated code wrote. +-- +-- M1_LOADER_LUA path to vendor/vt1200/dwt/loader/loader.lua +-- M1_EXPECT_LOAD expected load() result (default 0x12345678) +-- M1_EXPECT_SECOND expected &arr[1] in bits +-- +-- The marker read runs one frame before loader.lua's inspection/exit frame. + +dofile(os.getenv("M1_LOADER_LUA")) + +local ENDF = tonumber(os.getenv("LOADER_END") or "2700") +local EXPECT_LOAD = tonumber(os.getenv("M1_EXPECT_LOAD") or "0x12345678") +local EXPECT_SECOND = tonumber(os.getenv("M1_EXPECT_SECOND") or "0x018A9020") +local frames = 0 +local checked = false + +emu.register_frame_done(function() + frames = frames + 1 + if frames == ENDF - 1 and not checked then + checked = true + local prog = manager.machine.devices[':maincpu'].spaces['program'] + local add = prog:read_u32(0x018A8000) + local load = prog:read_u32(0x018A8020) + local second = prog:read_u32(0x018A8040) + print(string.format("M1: add=%d load=0x%08X second=0x%08X", + add, load, second)) + if add == 42 and load == EXPECT_LOAD and second == EXPECT_SECOND then + print("M1 MAME: PASS") + else + print(string.format( + "M1 MAME: FAIL (want add=42 load=0x%08X second=0x%08X)", + EXPECT_LOAD, EXPECT_SECOND)) + end + end +end, "m1-check") diff --git a/scripts/m1/run.sh b/scripts/m1/run.sh new file mode 100755 index 0000000..f96e82f --- /dev/null +++ b/scripts/m1/run.sh @@ -0,0 +1,154 @@ +#!/bin/sh +# M1 end-to-end smoke test. +# +# scripts/m1/run.sh +# +# Compiles four C functions with scripts/tms-clang + llc -O0, assembles and +# links them with the 1987 GSPA/GSPLNK 2.00 under DOSBox, then loads the COFF +# into MAME through dwt/loader/loader.lua and checks the marker words: +# +# add(20, 22) == 42 +# load(&word) == 0x12345678 +# second() == &arr[1] (arr + 32 bits) +# +# Overridable: TMS_CLANG TMS_OPT TMS_BITADDR TMS_LLC MAME DOSBOX DOSBOX_CONF +# SDK_IMG M1_WORK M1_SDK LOADER_AT LOADER_END +set -eu + +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +DIR=$ROOT/scripts/m1 + +LLVM_BIN=${LLVM_BIN:-$ROOT/build/llvm-clean-build/bin} +TMS_CLANG=${TMS_CLANG:-$LLVM_BIN/clang} +TMS_OPT=${TMS_OPT:-$LLVM_BIN/opt} +TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr} +TMS_LLC=${TMS_LLC:-$LLVM_BIN/llc} + +MAME=${MAME:-$ROOT/build/mame-src/vt1200} +DOSBOX=${DOSBOX:-dosbox} +DOSBOX_CONF=${DOSBOX_CONF:-$ROOT/vendor/vt1200/harness/sdk-scratch/dosbox-bench.conf} +SDK_IMG=${SDK_IMG:-$ROOT/vendor/vt1200/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img} + +WORK=${M1_WORK:-$ROOT/build/m1} +SDK=${M1_SDK:-$ROOT/build/m1-sdk} +ASM=$WORK/asm +LOADER_LUA=$ROOT/vendor/vt1200/dwt/loader/loader.lua +LOADER_BIN=$ROOT/vendor/vt1200/dwt/loader/build/loader.bin +LOADER_AT=${LOADER_AT:-2500} +LOADER_END=${LOADER_END:-2700} + +fail() { + echo "m1: $*" >&2 + exit 1 +} + +for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_BITADDR" "$TMS_LLC"; do + [ -x "$tool" ] || fail "missing tool: $tool" +done + +mkdir -p "$ASM" "$WORK/evidence" "$WORK/run/nvram" "$WORK/run/cfg" + +echo "== compile and select" +export TMS_CLANG TMS_OPT TMS_BITADDR +for name in add load late const_gep; do + "$ROOT/scripts/tms-clang" -O2 -o "$ASM/$name.ll" "$DIR/$name.c" + "$TMS_LLC" -march=tms34010 -O0 -filetype=asm \ + "$ASM/$name.ll" -o "$ASM/$name.s" \ + || fail "llc failed for $name.c" + echo " $name.s: $(grep -c . "$ASM/$name.s") lines" +done + +echo "== check the bit-address constants" +grep -Eq '^[[:space:]]*ADDI[[:space:]]+24,[[:space:]]*A0' "$ASM/late.s" \ + || fail "late.s has no ADDI 24 (late GEP got scaled again?)" +grep -Eq 'ADDI[[:space:]]+192' "$ASM/late.s" \ + && fail "late.s contains ADDI 192: late GEP was scaled twice" +grep -Eq '^[[:space:]]*MOVI[[:space:]]+_arr\+32,[[:space:]]*A0' "$ASM/const_gep.s" \ + || fail "const_gep.s has no 'MOVI _arr+32': constant GEP addend is not in bits" +grep -Eq '_arr\+4[^0-9]' "$ASM/const_gep.s" \ + && fail "const_gep.s references _arr+4 (bytes) instead of _arr+32 (bits)" +echo " late.s: $(grep -E 'ADDI' "$ASM/late.s" | head -1 | sed 's/^[[:space:]]*//')" +echo " const_gep.s: $(grep -E 'MOVI' "$ASM/const_gep.s" | head -1 | sed 's/^[[:space:]]*//')" + +echo "== extract the 1987 SDK" +if [ ! -f "$SDK/GSPA.EXE" ]; then + command -v mcopy >/dev/null 2>&1 || fail "mtools (mcopy) is required" + [ -f "$SDK_IMG" ] || fail "SDK image not found: $SDK_IMG" + mkdir -p "$SDK" + mcopy -s -n -i "$SDK_IMG" "::/A2/*" "$SDK/" + mcopy -s -n -i "$SDK_IMG" "::/C2/*" "$SDK/" +fi +[ -f "$SDK/GSPA.EXE" ] || fail "GSPA.EXE not extracted into $SDK" + +echo "== assemble and link with GSPA/GSPLNK under DOSBox" +cat "$DIR/entry.asm" "$ASM/add.s" "$ASM/load.s" "$ASM/late.s" \ + "$ASM/const_gep.s" > "$SDK/M1.ASM" +# The linker command file must keep CRLF line endings. +sed 's/\r$//' "$DIR/link.cmd" | sed 's/$/\r/' > "$SDK/M1.CMD" +rm -f "$SDK/M1.OBJ" "$SDK/M1.OUT" "$SDK/M1.MAP" "$SDK/M1.LST" + +SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ +timeout "${DOSBOX_TIMEOUT:-300}" "$DOSBOX" -conf "$DOSBOX_CONF" \ + -noprimaryconf -nolocalconf \ + -c "mount c $SDK" -c "c:" \ + -c "gspa M1 M1.obj M1.lst -l" \ + -c "gsplnk M1.cmd" \ + -c "exit" -exit > "$WORK/dosbox.log" 2>&1 \ + || { tail -20 "$WORK/dosbox.log" >&2; fail "DOSBox failed"; } + +[ -f "$SDK/M1.LST" ] || fail "GSPA produced no listing (see $WORK/dosbox.log)" +sed 's/\r$//' "$SDK/M1.LST" > "$WORK/m1.lst" +grep -q 'No Errors' "$WORK/m1.lst" \ + || { grep -i -B2 -A2 'error' "$WORK/m1.lst" >&2 || true; fail "GSPA reported errors"; } +[ -f "$SDK/M1.OUT" ] || fail "GSPLNK produced no M1.OUT" +sed 's/\r$//' "$SDK/M1.MAP" > "$WORK/m1.map" +cp "$SDK/M1.OUT" "$WORK/m1.coff" +cp "$SDK/M1.ASM" "$WORK/m1.asm" + +ENTRY=$(awk '$2 == "_start" { print $1; exit }' "$WORK/m1.map") +[ "$ENTRY" = "018A0000" ] || fail "linker entry _start is '$ENTRY', want 018A0000" +grep -q 'ENTRY POINT SYMBOL: "_start"' "$WORK/m1.map" \ + || fail "linker map has no _start entry point" + +BSS_ORIGIN=$(awk '$1 == ".bss" && $2 == "0" { print $3; exit }' "$WORK/m1.map") +BSS_LENGTH=$(awk '$1 == ".bss" && $2 == "0" { print $4; exit }' "$WORK/m1.map") +[ -n "$BSS_ORIGIN" ] || fail "linker map has no .bss section" +[ "$BSS_LENGTH" = "00000080" ] \ + || fail ".bss is $BSS_LENGTH bits, expected 00000080 (only arr[4])" +EXPECT_SECOND=$(printf '0x%X' "$((0x$BSS_ORIGIN + 32))") # &arr[1] = arr + 32 bits +echo " linked .text at 018A0000, .bss at $BSS_ORIGIN (128 bits)" +echo " expected &arr[1] = $EXPECT_SECOND" + +echo "== run the COFF in MAME" +[ -x "$MAME" ] || fail "MAME binary not built: $MAME" +[ -f "$LOADER_LUA" ] || fail "COFF loader missing: $LOADER_LUA" +[ -f "$LOADER_BIN" ] || fail "COFF loader payload missing: $LOADER_BIN" + +VT1200_SKIP_LEVEL1=1 \ +LOADER_AT="$LOADER_AT" LOADER_END="$LOADER_END" \ +LOADER_BIN="$LOADER_BIN" LOADER_COFF="$WORK/m1.coff" \ +LOADER_OUT="$WORK/evidence" \ +M1_LOADER_LUA="$LOADER_LUA" \ +M1_EXPECT_LOAD=0x12345678 M1_EXPECT_SECOND="$EXPECT_SECOND" \ +timeout "${MAME_TIMEOUT:-600}" "$MAME" vt1200 \ + -rompath "$ROOT/vendor/vt1200/harness/mame/roms" \ + -video none -sound none -nothrottle -skip_gameinfo \ + -nvram_directory "$WORK/run/nvram" \ + -cfg_directory "$WORK/run/cfg" \ + -debug -debugger none \ + -autoboot_script "$DIR/m1-check.lua" -autoboot_delay 0 \ + > "$WORK/mame.log" 2>&1 \ + || { tail -30 "$WORK/mame.log" >&2; fail "MAME run failed"; } + +MARKERS=$(grep -E '^M1: add=' "$WORK/mame.log" | tail -1 || true) +[ -n "$MARKERS" ] || { tail -30 "$WORK/mame.log" >&2; fail "MAME produced no M1 markers"; } +echo " $MARKERS" + +grep -q 'LOADER: result status=0 (OK)' "$WORK/mame.log" \ + || { grep 'LOADER: result' "$WORK/mame.log" >&2 || true; fail "COFF loader did not report status 0"; } +grep -q 'placed .text .* 0 differing' "$WORK/mame.log" \ + || fail "COFF loader could not verify the placed .text bytes" +grep -q '^M1 MAME: PASS' "$WORK/mame.log" \ + || { tail -30 "$WORK/mame.log" >&2; fail "marker check failed"; } + +echo "M1 MAME: PASS" diff --git a/scripts/tests/m1/run.sh b/scripts/tests/m1/run.sh new file mode 100755 index 0000000..c9f222b --- /dev/null +++ b/scripts/tests/m1/run.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# M1 test gate: compile four C functions, assemble/link with GSPA/GSPLNK, +# load the COFF in MAME and check the marker words. +set -eu +ROOT=$(cd "$(dirname "$0")/../../.." && pwd) +exec "$ROOT/scripts/m1/run.sh" "$@"