diff --git a/harness/sdk-scratch/.gitignore b/harness/sdk-scratch/.gitignore new file mode 100644 index 0000000..8a1ad5c --- /dev/null +++ b/harness/sdk-scratch/.gitignore @@ -0,0 +1,28 @@ +# extracted SDK material and run products — never staged +work/ +cgtools/ +sdb/ +disk1987/ +disk1985asm/ +disk1985demo/ +os2test/ +bench/ +dosemu-home/ + +*.bin +*.obj +*.out +*.map +*.lst +*.hex +*.tag +*.HI +*.LO +*.$$$ + +# local intermediate text/disassembly +*.txt +*.asm +bench.py +__pycache__/ +*.pyc diff --git a/harness/sdk-scratch/README.md b/harness/sdk-scratch/README.md new file mode 100644 index 0000000..d17c62e --- /dev/null +++ b/harness/sdk-scratch/README.md @@ -0,0 +1,47 @@ +# SDK scratch: the 1987 TMS34010 DOS toolchain + +This directory is a scratch area for the investigation in +`reference/tms34010-sdk.md`. The SDK under `tools/tms34010-sdk/` is only read; +everything here is extracted work product. + +## Round trip + +```sh +./roundtrip.sh 0x1000 32768 3F208000 +``` + +`OFFSET SIZE BASE_BIT`: disassemble that ROM region with +`tools/smart34010/dis34 -o`, convert it to GSPA syntax with `dis2as34.py`, +assemble/link/convert with the 1987 DOS tools under DOSBox, interleave the +high/low Intel-hex files, and diff against the ROM. It prints `BYTE-EXACT` +and the md5 of the reassembled region on success. + +The tools are extracted on first use from: + +``` +tools/tms34010-sdk/1987-05 TMS34010 SDK/ + 1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img +``` + +into `work/` (A2 = assembler/linker, C2 = C compiler/runtime). The ROM is +never written. + +## Benchmark + +```sh +python3 -u bench2.py # DOSBox vs dosemu, 3 runs, two workloads +``` + +`bench2.py` uses `dosbox-bench.conf` (dynamic core, `cycles=max`) and a private +dosemu `HOME` in `dosemu-home/`. The dosemu batch must end in `exitemu`; +`-E` alone leaves DOS running. Results are in the report. + +## Notes + +* `dis2as34.py` needs the `-o` opcode comments from `dis34`; they are what + disambiguate `MOVI` short/long, long relative `JR` vs absolute `JA`, and the + B-file `SP`. +* `gsprom -i` writes two files (high and low byte); interleave low then high. + Its record addresses are 16-bit word indices, not bit addresses. +* DOSBox 8.3 names are uppercased; check for `RT.OBJ`, not `rt.obj`. dosemu's + `lredir` keeps lowercase names. diff --git a/harness/sdk-scratch/bench2.py b/harness/sdk-scratch/bench2.py new file mode 100644 index 0000000..fe47a76 --- /dev/null +++ b/harness/sdk-scratch/bench2.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""DOSBox vs dosemu on the real workload: GSPA assembling TMS34010 source. + +Workload A: gspa t1 t1.obj t1.lst -l (4 KB region, listing) +Workload B: gspa -q rt16 rt16.obj (16 KB region, no listing) + +Both headless: DOSBox with SDL dummy drivers and core=dynamic/cycles=max; +dosemu in -t (terminal/S-Lang) mode. Three measured runs each, plus one +warm-up. Raw COFF md5 differs only in the 4-byte time stamp, so a +timestamp-normalised md5 is also reported. +""" +import hashlib +import os +import statistics +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +BENCH = ROOT / "bench" +DHOME = ROOT / "dosemu-home" +DBENCH = DHOME / ".dosemu" / "drive_c" / "bench" +RUNS = 3 + + +def norm_md5(p): + if not p.exists(): + return "-" + b = bytearray(p.read_bytes()) + if len(b) >= 8: + b[4:8] = b"\0\0\0\0" + return hashlib.md5(bytes(b)).hexdigest() + + +def raw_md5(p): + return hashlib.md5(p.read_bytes()).hexdigest() if p.exists() else "-" + + +def clean(d, base): + for name in (base + ".OBJ", base + ".LST", "MACLIB.$$$", + base.lower() + ".obj", base.lower() + ".lst", "maclib.$$$"): + f = d / name + if f.exists(): + f.unlink() + + +def run_dosbox(base, args): + clean(BENCH, base) + cmds = [] + for _ in range(1): + cmds += ["-c", "gspa " + args] + env = dict(os.environ, SDL_VIDEODRIVER="dummy", SDL_AUDIODRIVER="dummy") + t0 = time.perf_counter() + rc = subprocess.run( + ["timeout", "120", "dosbox", "-conf", str(ROOT / "dosbox-bench.conf"), + "-noprimaryconf", "-nolocalconf", + "-c", "mount c " + str(BENCH), "-c", "c:"] + cmds + + ["-c", "exit", "-exit"], + env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode + dt = time.perf_counter() - t0 + # DOSBox uppercases 8.3 names + obj = BENCH / (base.upper() + ".OBJ") + return dt, rc, raw_md5(obj), norm_md5(obj), obj.stat().st_size if obj.exists() else 0 + + +def run_dosemu(base, args, bat): + clean(DBENCH, base) + script = "@echo off\r\nc:\r\ncd \\bench\r\ngspa %s\r\nexitemu\r\n" % args + (DBENCH / bat).write_bytes(script.encode()) + env = dict(os.environ, HOME=str(DHOME)) + t0 = time.perf_counter() + rc = subprocess.run( + ["timeout", "120", "dosemu", "-t", "-quiet", "-E", r"c:\bench" + "\\" + bat], + env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode + dt = time.perf_counter() - t0 + # dosemu/lredir keeps the lowercase names written by FreeCom + obj = DBENCH / (base.lower() + ".obj") + if not obj.exists(): + obj = DBENCH / (base.upper() + ".OBJ") + return dt, rc, raw_md5(obj), norm_md5(obj), obj.stat().st_size if obj.exists() else 0 + + +def bench(name, base, args, bat): + print("\n== %s == gspa %s" % (name, args)) + run_dosbox(base, args) # warm-up + run_dosemu(base, args, bat) + results = {} + for label, fn in (("dosbox", lambda: run_dosbox(base, args)), + ("dosemu", lambda: run_dosemu(base, args, bat))): + rows = [fn() for _ in range(RUNS)] + results[label] = rows + for dt, rc, raw, norm, size in rows: + print(" %-7s %6.2fs rc=%d size=%d raw=%s norm=%s" % + (label, dt, rc, size, raw[:8], norm[:8])) + print(" %-7s median %.2fs" % (label, statistics.median(r[0] for r in rows))) + n0 = {r[3] for r in results["dosbox"]} + n1 = {r[3] for r in results["dosemu"]} + print(" normalised object identical: %s (%s)" % (n0 == n1 and len(n0) == 1, n0)) + + +if __name__ == "__main__": + bench("A: 4 KB with listing", "t1", "t1 t1.obj t1.lst -l", "bench.bat") + bench("B: 16 KB, no listing", "rt16", "-q rt16 rt16.obj", "bench16.bat") diff --git a/harness/sdk-scratch/dis2as34.py b/harness/sdk-scratch/dis2as34.py new file mode 100644 index 0000000..7314775 --- /dev/null +++ b/harness/sdk-scratch/dis2as34.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Convert smart34010 (dis34 -o) output into TI gspa/as34 2.00 syntax. + +Usage: dis2as34.py BASE_HEX SIZE_BYTES INPUT.ASM > OUT.ASM + +The input must be produced with ``dis34 -o`` (opcode words in a trailing +comment) because the 1987 assembler is *not* byte-faithful on its own: + +* ``MOVI`` is one mnemonic with a short (16-bit, sign-extended) and a long + (32-bit) form. gspa auto-selects by "does the value fit in 16 bits", so a + long MOVI of 0x53E0 (which the ROM encodes as a 3-word instruction) is + silently shortened -- and sign extension then changes its meaning. The + explicit ``W`` / ``L`` operand forces the form. +* the same applies to ADDI/CMPI/SUBI. +* dis34 prints a *long relative* JR (16-bit displacement) as ``ja``, the + same mnemonic it uses for the 3-word absolute JA. The opcode word count + disambiguates: 2 words = long JR, 3 words = absolute JA. +* dis34 prints B-file register 15 as ``sp`` too; gspa's bare ``sp`` is the + A-file register. B-file SP has to be written ``b15`` (the assembler rejects + ``a15``). +* register-indirect displacements are signed 16-bit fields. dis34 prints the + raw two's-complement bits (0xFFE0); gspa rejects those as out of bounds, so + they are rendered as signed decimal (-32). +* constants: dis34 emits ``0xHHHH``; gspa wants ``0HHHHh`` (must start with a + decimal digit, so 0xC00000B0 becomes 0C00000B0h). +* a PC-relative branch whose target is not an instruction boundary in the + region (outside it, or -- as happens in data tables -- inside a multi-word + instruction) cannot be expressed as a label. gspa also mishandles a + negative absolute ``.set`` target in a JR. The exact opcode words are + therefore emitted as ``.word`` for those branches. +""" +import re +import sys + +DISP = re.compile(r"(\*\s*[ab]\d+|\*\s*sp)\(\s*0x([0-9a-fA-F]+)\s*\)") +HEX = re.compile(r"0x([0-9a-fA-F]+)") +FORM_OPS = {"movi", "addi", "cmpi", "subi"} +RELATIVE = re.compile(r"^(jreq|jrne|jrlt|jrle|jrgt|jrge|jrc|jrnc|jrlo|jrls|jrhi|jrhs|jrnz|jrz|" + r"jruc|jrxn|jrxnn|jrxz|jrxeq|jrxgt|jrxle|jryn|jrynn|jryz|jryeq|jrygt|jryle|" + r"call|callr)$") +DSJ_OPS = {"dsj", "dsjs", "dsjeq", "dsjne"} + + +def ti_hex(digits: str) -> str: + return "0" + digits.upper() + "h" + + +def fix_disp(m): + base, digits = m.group(1), m.group(2) + v = int(digits, 16) + if v >= 0x8000: + return f"{base}({v - 0x10000})" + return f"{base}(0{digits.upper()}h)" + + +def raw_words(words): + return " .word " + ", ".join(ti_hex(w) for w in words) + + +def convert(src: str, base: int, size: int) -> str: + end = base + size * 8 + lines = src.splitlines() + defined = set() + for line in lines: + code = line.partition(";")[0] + dm = re.match(r"^(L[0-9A-Fa-f]{8}):", code) + if dm: + defined.add(dm.group(1).upper()) + + out = [] + referenced = set() + for line in lines: + code, sep, comment = line.partition(";") + for lm in re.findall(r"\b(L[0-9A-Fa-f]{8})\b", code): + referenced.add(lm.upper()) + words = re.findall(r"\b[0-9a-fA-F]{4}\b", comment) + stripped = code.rstrip() + # long relative JR printed as JA (see module docstring) + mm = re.match(r"^(\s*)ja([a-z]+)(\s+)(\S.*)$", stripped) + if mm and len(words) == 2: + stripped = f"{mm.group(1)}jr{mm.group(2)}{mm.group(3)}{mm.group(4)}" + parts = stripped.split() + op = parts[0].lower() if parts else "" + # B-file SP (see docstring) + if op == "rl" and words and stripped.endswith(", sp"): + if int(words[0], 16) & 0x1f == 0x1f: + stripped = stripped[:-3] + "b15" + # short/long immediate forms (see docstring) + if op in FORM_OPS and words: + if len(words) == 2: + stripped += ", W" + mm = re.match(r"^(\s*\w+\s+)0x([0-9a-fA-F]+)(,.*)$", stripped) + if mm and int(mm.group(2), 16) >= 0x8000: + stripped = (mm.group(1) + str(int(mm.group(2), 16) - 0x10000) + + mm.group(3)) + elif len(words) == 3: + stripped += ", L" + # relative branch to a label that is not an instruction boundary + mm = re.match(r"^(\s*)(\w+)(\s+.*?)(L[0-9A-Fa-f]{8})\s*$", stripped) + if mm and mm.group(4).upper() not in defined: + op2 = mm.group(2).lower() + if op2 in DSJ_OPS or RELATIVE.match(op2): + stripped = raw_words(words) + # relative branch to an absolute address outside the region + mm = re.match(r"^(\s*)([a-z]+)(\s+)0x([0-9a-fA-F]+)\s*$", stripped) + if mm and RELATIVE.match(mm.group(2).lower()): + target = int(mm.group(4), 16) + if not (base <= target < end): + stripped = raw_words(words) + stripped = DISP.sub(fix_disp, stripped) + stripped = HEX.sub(lambda mm: ti_hex(mm.group(1)), stripped) + out.append(stripped) + missing = sorted(referenced - defined) + if missing: + out.append("") + for name in missing: + off = int(name[1:], 16) - base + out.append(f"{name} .set " + ("0%Xh" % off if off >= 0 else str(off))) + return "\n".join(out) + "\n" + + +if __name__ == "__main__": + base = int(sys.argv[1], 16) + size = int(sys.argv[2]) + sys.stdout.write(convert(open(sys.argv[3]).read(), base, size)) diff --git a/harness/sdk-scratch/dosbox-bench.conf b/harness/sdk-scratch/dosbox-bench.conf new file mode 100644 index 0000000..45b29f3 --- /dev/null +++ b/harness/sdk-scratch/dosbox-bench.conf @@ -0,0 +1,17 @@ +[sdl] +fullscreen=false +autolock=false +output=surface + +[dosbox] +machine=svga_s3 +memsize=16 + +[cpu] +core=dynamic +cycles=max + +[mixer] +nosound=true + +[autoexec] diff --git a/harness/sdk-scratch/dosbox.conf b/harness/sdk-scratch/dosbox.conf new file mode 100644 index 0000000..d591e25 --- /dev/null +++ b/harness/sdk-scratch/dosbox.conf @@ -0,0 +1,17 @@ +[sdl] +fullscreen=false +autolock=false +output=surface + +[dosbox] +machine=svga_s3 +memsize=16 + +[cpu] +core=normal +cycles=3000 + +[mixer] +nosound=true + +[autoexec] diff --git a/harness/sdk-scratch/roundtrip.sh b/harness/sdk-scratch/roundtrip.sh new file mode 100755 index 0000000..7eb15ad --- /dev/null +++ b/harness/sdk-scratch/roundtrip.sh @@ -0,0 +1,104 @@ +#!/bin/sh +# Assemble a region of the VT1200 ROM with the 1987 TI TMS34010 toolchain +# (GSPA/GSPLNK/GSPROM, run under DOSBox) and compare the result byte-for-byte +# with the original. +# +# Usage: roundtrip.sh OFFSET SIZE BASE_BIT +# OFFSET byte offset into vt1200_v22.bin +# SIZE region size in bytes +# BASE_BIT bit address the region is linked at (e.g. 3F208000) +# +# Example: roundtrip.sh 0x1000 16384 3F208000 +# +# The ROM is only read. All work happens in this directory. +set -eu + +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +DIR=$(cd "$(dirname "$0")" && pwd) +WORK="$DIR/work" +ROM="$ROOT/vt1200_v22.bin" +DIS="$ROOT/tools/smart34010/dis34" + +OFF=${1:?offset} +SIZE=${2:?size} +BASE=${3:?base bit address} +OFF=$((OFF)) +SIZE=$((SIZE)) + +mkdir -p "$WORK" +# tools: extract once from the 1987-11-06 floppy image +if [ ! -x "$WORK/GSPA.EXE" ]; then + IMG="$ROOT/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img" + mcopy -s -n -i "$IMG" "::/A2/*" "$WORK/" + mcopy -s -n -i "$IMG" "::/C2/*" "$WORK/" +fi + +# TI hex constants must begin with a decimal digit +TBASE=$BASE +case "$TBASE" in + [0-9]*) ;; + *) TBASE="0$TBASE" ;; +esac + +dd if="$ROM" of="$WORK/rt.bin" bs=1 skip="$OFF" count="$SIZE" 2>/dev/null +"$DIS" -b "$BASE" -o "$WORK/rt.bin" > "$WORK/rt.dis" +python3 "$DIR/dis2as34.py" "$BASE" "$SIZE" "$WORK/rt.dis" > "$WORK/RT.ASM" +sed -i '1i .text' "$WORK/RT.ASM" +echo '.end' >> "$WORK/RT.ASM" + +cat > "$WORK/RT.CMD" </dev/null 2>&1 + +python3 - "$WORK" "$SIZE" <<'EOF' +import sys +work, size = sys.argv[1], int(sys.argv[2]) + +def readhex(p): + d = {} + for line in open(p): + line = line.strip() + if not line.startswith(':'): + continue + b = bytes.fromhex(line[1:]) + if b[3] != 0: + continue + a = (b[1] << 8) | b[2] + for i, x in enumerate(b[4:4 + b[0]]): + d[a + i] = x + return d + +hi = readhex(work + "/RTHI.HI") +lo = readhex(work + "/RTLO.LO") +out = bytearray() +for a in sorted(hi): + out.append(lo[a]) + out.append(hi[a]) +orig = open(work + "/rt.bin", "rb").read() +n = min(len(orig), len(out)) +bad = [i for i in range(n) if orig[i] != out[i]] +print("assembled %d bytes, original %d bytes, %d differing" % (len(out), len(orig), len(bad))) +if len(out) != len(orig) or bad: + print("FAIL") + for i in bad[:5]: + print(" %#x: orig %s out %s" % (i, orig[i:i+8].hex(), out[i:i+8].hex())) + sys.exit(1) +print("md5 " + __import__('hashlib').md5(out).hexdigest() + " BYTE-EXACT") +EOF diff --git a/reference/tms34010-sdk.md b/reference/tms34010-sdk.md new file mode 100644 index 0000000..7edcbd7 --- /dev/null +++ b/reference/tms34010-sdk.md @@ -0,0 +1,482 @@ +# The TMS34010 SDK in `tools/tms34010-sdk`: what runs, and what it buys us + +Investigated 2026-10-18. Short version: + +* The **1987 DOS toolchain runs** under DOSBox (and dosemu) on Linux: GSPA + (assembler), GSPLNK (linker), GSPROM (hex converter), GSPAR (archiver), and + the K&R C compiler (GSPCPP/GSPCC/GSPCG). +* `smart34010` output **does** assemble with it, but not literally: five small + fixups are needed. With them, a **32 KB region of the v2.2 ROM reassembles + byte-for-byte**, md5-identical to the original. See the results table. +* The **1990 r5.01 tools are OS/2 1.x NE binaries**, not DOS. They do not run in + DOSBox; using them would need an OS/2 environment. The 1987 DOS tools are the + usable set. +* There is **no `as34` / `lnk34`** in this SDK. The assembler is `GSPA.EXE` and + the linker `GSPLNK.EXE` (the names `smart34010`'s README mentions are from a + later TI package). GSPA 2.00 is enough for the round trip. +* The C compiler works and links with `rts.lib` into an absolute COFF image. + It is **K&R C, not ANSI**: `int f(int a)` is a syntax error, `int f(a) int a;` + works. +* For a VT1200 payload the useful part is the **address model**: the linker's + `SECTIONS` origin is a **bit address**, the COFF carries the full 32-bit bit + address, and the bytes are extracted from GSPROM's high/low EPROM files. The + old converter's own hex addresses wrap at 64 KB and must not be used for + placement. + +Everything below was run against the ROM with md5 +`b46a3e442ccb86134d2356bd70d22fd2`; the ROM was only read. + +--- + +## 1. What is in the SDK, and what actually executes + +| package | files | host | runs here? | +|---|---|---|---| +| `1985-05-20 … Assembly Language Package` | GSPA/GSPLNK/GSPAR **v1.04** | DOS MZ | yes, but older | +| `1987-11-06 … Simulator-C Tools-ASM Tools.img` | GSPA **2.00**, GSPLNK **2.00**, GSPAR 2.00, GSPROM **2.01**, GSPCV 2.00 (A2); GSPCPP/GSPCC/GSPCG **2.01**, RTS.LIB, FLIB.LIB (C2) | DOS MZ | **yes — the set used here** | +| `1990-11-19 … Code Generation Tools r5.01` | gspa, gspac, gspar, gspcg, gspcl, gsplnk, gspopt, gsprom, clist | **OS/2 1.x NE** | **no** (DOSBox cannot run NE/OS2) | +| `1990-11-19 … C Source Debugger r5.00` | db340.exe (DOS MZ), gspmon | DOS + target monitor | debugger runs, but needs a monitor/target link | +| `1991-07 TIGA Promo Kit` | DOS art/demo disks, TIGA docs | DOS | not relevant to VT1200 | +| `Docs/` | Users Guide, Assembly Tools Guide, C Compiler Guide, reference cards, TIGA, production data | PDF | reference only | +| `Patents/` | five graphics-architecture patents | PDF | not relevant | + +The 1990 NE files were checked both with `file` and by parsing the NE header: +signature `NE`, version 5.1, **target OS byte `0x01` = OS/2**. The 1985/1987 +files are plain `MZ for MS-DOS`, 16-bit real mode, and need at most 640 KB. + +The **1987 disk is the sweet spot**: v2.00 is the version whose tools guide +documents the `W`/`L` immediate-form operands and the `.usect`/`.if` features +used below. The 1985 v1.04 tools are older and were not needed. + +### Running them headless + +DOSBox 0.74-3 works with the SDL dummy drivers; a bare `-conf` file with +`core=dynamic` / `cycles=max` makes it ~5× faster than the `cycles=3000` config +used earlier. The commands in `roundtrip.sh` show the exact form. + +dosemu 1.4.0.8 also works: `dosemu -t -quiet -E 'c:\bench\bench.bat'`, where the +batch ends in `exitemu`. `-E` alone does not exit DOS. See the benchmark +section. + +--- + +## 2. The round-trip experiment (the proof) + +### Method + +For a region at file offset `OFF`, size `SIZE`, linked at bit address `BASE`: + +1. `dis34 -b BASE -o region.bin > region.dis` — disassemble with the opcode + words in a trailing comment (`-o` is required; the fixups use them). +2. `dis2as34.py BASE SIZE region.dis > RT.ASM` — apply the five fixups + (section 3), prepend `.text`, append `.end`. +3. Assemble and link: + + ``` + gspa rt rt.obj rt.lst -l + gsplnk rt.cmd + gsprom -i rt.out rthi rtlo + ``` + + with `rt.cmd`: + + ``` + -a + -o rt.out + -m rt.map + rt.obj + SECTIONS + { + .text 3F208000h: {} + } + ``` + + The origin is a **bit address**. `-a` makes an absolute module; `-m` writes a + map; `-e` would set an entry point. +4. `gsprom -i` writes **two** Intel-hex files (high byte and low byte for a + 16-bit EPROM pair). Interleave **low byte then high byte** per word. +5. Compare with `dd`-extracted original bytes. + +`roundtrip.sh` does exactly this and prints the md5. + +### Results + +| region | file offset | base (bit) | size | result | +|---|---|---|---|---| +| boot/POST code | `0x1000` | `3F208000` | 4 KB | **0 differing**, md5 `a4018fed12e062658bc81957f149265b` | +| same, larger | `0x1000` | `3F208000` | 16 KB | **0 differing**, md5 `0a2e679bc864d52d32c59740142c5b96` | +| same, larger | `0x1000` | `3F208000` | **32 KB** | **0 differing**, md5 `43adb6669571041985f3e26fd9ff7cb9` | +| another code region | `0x8000` | `3F240000` | 4 KB | **0 differing**, md5 `438e743431d56e0c12ededce1c36952a` | + +The ROM md5 after all runs is unchanged: `b46a3e442ccb86134d2356bd70d22fd2`. + +A 64 KB region trips `dis34` itself (`unexpected end of input at byte 65536`); +split larger spans into ≤32 KB pieces. This is a disassembler limitation, not an +assembler one. + +### What this proves, and what it does not + +It proves the assembler accepts the disassembler's instruction syntax and +reproduces the **exact instruction encodings** for 32 KB of real firmware, +including PC-relative branches, absolute calls, data words, and immediate +forms. It does **not** prove the payload runs on a VT1200; nothing here was +executed on the target. + +--- + +## 3. The five fixups (and why they are needed) + +`dis34` output is *close* to GSPA syntax but not directly assemblable. The +converter `harness/sdk-scratch/dis2as34.py` is the executable form of this list. + +1. **Hex constants.** GSPA has no `0x`: a hex constant must start with a decimal + digit and end in `h`. `0x220` → `0220h`; `0xC00000B0` → `0C00000B0h` (nine + digits are accepted and truncated to 32 bits). GSPCV does the same conversion + for the old `>xxxx` syntax. + +2. **`MOVI` short vs long.** GSPA has one `MOVI` mnemonic with a 16-bit + sign-extended form and a 32-bit form, and auto-selects by "does the value fit + in 16 bits (unsigned)". That is **not** encoding-preserving: a long + `MOVI 0x53E0` (three words) becomes a short `MOVIW` (two words), and the + short form sign-extends to `0xFFFF53E0`. Force the form with the third + operand: `movi 053E0h, sp, L` or `..., W`. The opcode word count in the `-o` + comment tells which form the ROM used. The same applies to `ADDI`, `CMPI`, + and `SUBI`. + +3. **B-file `SP`.** `dis34` prints register 15 as `sp` regardless of file. + Bit 4 of the encoded register field says which file: `0x0F` = A-file SP, + `0x1F` = B-file SP. GSPA's bare `sp` matched the ROM for most forms (memory + operands such as `*sp(offset)` assembled as B15; `MOVI`/`MMFM` register + operands as A15), but an `RL` destination chose A15 where the ROM has B15 + (0x312F vs 0x313F). `b15` forces the B-file register; `a15` is rejected. + Check the register field, not the printed name. + +4. **Long relative `JR` printed as `JA`.** In `dis34`, the 2-word relative jump + (opcode low byte `0x00`, 16-bit displacement) is printed with the `ja` + mnemonic, the same one used for the 3-word absolute `JA`. The word count + distinguishes them: 2 words → rewrite to `jr`, 3 words → leave `ja`. + Without this, every long relative branch grows by a word and the image + diverges. + +5. **Branches whose target is not an instruction boundary.** This happens for + targets outside the disassembled region (common) and, in data-table areas, + for targets inside a multi-word instruction. GSPA also mishandles a negative + absolute `.set` value in a `JR` (it emits the absolute `JA` form and errors). + Emit the exact opcode words from the `-o` comment as `.word` for those + branches. For `DSJS`, whose short range is only ±32 words, this is the only + reliable way. + +One practical note: with the slow `cycles=3000` DOSBox config, a 16 KB source +with `-l` takes about 35 s wall time; a 32 KB source takes about 1.5–2 minutes. +That is DOSBox being cycle-limited, not the assembler hanging. The benchmark +section uses `core=dynamic`/`cycles=max`, which removes most of it. + +--- + +## 4. Addresses, sections, and output formats + +### The unit is bits + +The TMS34010 is bit-addressed. In this toolchain: + +* The assembler's location counter and labels are in **bits**; each instruction + word advances it by 16. A listing address of `00000030` is 3 words in. +* `SECTIONS` binding is a **bit address**: `.text 3F208000h: {}` produced + `.text 0 3F208000 00020000` in the map (origin bit, length bits). +* The linker's `MEMORY` origins/lengths are bits too. +* The COFF output carries the true 32-bit bit address. In `RT16.OUT` the + optional header says `text = 0x3F208000`, `code size = 0x20000` bits, and the + `.text` section header says `paddr = vaddr = 0x3F208000`, `size = 0x20000` + bits. The file header magic is `0x0090`, flags `0x103` (including `0x40`, + the TMS34010 little-endian "16 bits per word, LSB first" flag). +* To place a payload at bit address `A`, link `.text A: {}` and put file byte + `i` at physical byte address `A>>3 + i`. Do not re-link at a different address + than the load address: the C compiler emits `CALLA` and absolute pointers. + +### GSPROM's hex addresses are not the load address + +`gsprom -i in out_hi out_lo` produces two 8-bit Intel-hex files, one high byte +and one low byte per 16-bit word. The address in those records is a **16-bit +word index inside a 64 KB byte window**, not the bit address and not the full +byte address. Measured: + +| section bit origin | first record address | +|---|---| +| `0x00020000` | `0x2000` | +| `0x3F208000` | `0x0800` | +| `0x3F240000` | `0x4000` | + +That matches `((bit >> 3) & 0xFFFF) >> 1`. The data order is still correct, so +take the byte stream in order and place it at the address from the map/COFF. +`gsprom -t` (TI-tagged) has the same 16-bit address limitation. + +For a loader, the clean source of truth is the **COFF** (`-a` output): section +`paddr`/`vaddr` and the optional header's entry point are full 32-bit bit +addresses. A small COFF reader can emit a raw image with its true load address; +GSPROM is only a convenience for an EPROM programmer. + +### Linker command syntax gotchas + +* Comments are `/* … */`; `;` is a syntax error in a command file. +* `.text > prog` is a syntax error. Use either + `.text 3F208000h: {}` (bind directly) or `.text: {} > prog` (named memory). +* `-a` = absolute executable, `-m` = map, `-o` = output, `-e` = entry symbol, + `-l lib` = library. `-c` / `-cr` select the C ROM/RAM autoinit models. + +--- + +## 5. The C compiler + +It runs, and the whole pipeline works: + +``` +gspcpp cmain # .c -> .cpp +gspcc cmain # .cpp -> .if +gspcg cmain # .if -> .asm +gspa cmain # .asm -> .obj +gsplnk cmain.cmd # .obj + rts.lib -> .out, map +gsprom -i cmain.out … +``` + +A test with `fib` and `main` linked cleanly with: + +``` +-c +-o cmain.out +-m cmain.map +cmain.obj +-l rts.lib +MEMORY +{ + prog: origin = 3F208000h, length = 10000h + ram: origin = 0h, length = 100000h +} +SECTIONS +{ + .text: {} > prog + .cinit: {} > prog + .data: {} > prog + .bss: {} > ram +} +``` + +The map confirms: + +``` +ENTRY POINT SYMBOL: "_c_int00" address: 3F2082F0 +.text 3F208000 00000A90 +.cinit 3F208A90 00000060 +.bss 00000000 0000FE40 (uninitialized) +``` + +So the compiler, assembler, runtime library, linker, and converter form a +complete C path. **It was not executed on a VT1200.** + +### Language and ABI facts worth knowing + +From `SPVU005A` and the generated code: + +* **K&R C only.** ANSI prototypes (`int f(int a)`) are a syntax error; + `int f(a) int a; { … }` works. Plan for old-style declarations. +* `char` 8 bits, `short` 16, **`int` and `long` 32**, pointers **32-bit bit + addresses**, `float` 32, `double` 64. +* Alignment: objects are on 16-bit boundaries (bit addresses with four LSBs + zero); structure/array members are packed. +* Dedicated registers: `SP` = system stack, `A14` (`STK`) = program stack, + `A13` (`FP`) = frame pointer; `A8` = integer/pointer return value. `B0–B14` + are unused by the compiler and available to assembly. Callee saves the A + registers it uses except `A8`; `FS1` must be 32 inside C code. +* Arguments are pushed on the **program stack** in reverse order, widened to + 32 bits; calls are `CALLA` (absolute), so **code is position-dependent**. +* Runtime: link `rts.lib` with `-c` (ROM autoinit; `boot.obj` is pulled in and + `_c_int00` becomes the entry) or `-cr` (loader does the `.cinit` copy). The + boot code allocates `SYS_STACK` in `.bss`, sets `FP`/`STK`/`SP`, calls + `_main`, then `_exit`. +* C sections: `.text` (code + string literals + float constants), `.cinit` + (initialized-global table), `.bss` (globals + both stacks + heap). `.data` is + the assembler's, not the C compiler's. `.bss` must be RAM. + +For a payload, the simplest C model is: link `.text`/`.cinit` at the load +address, put `.bss` in a free RAM range, and enter at `_c_int00`. A pure-assembly +payload avoids the runtime entirely. + +--- + +## 6. Building a VT1200 payload: practical recipe + +Assembly-only payload (smallest, no runtime): + +``` + .text +start: + movi 01234h, a0 + move a0, @0C0000010h, 0 ; example on-chip register + rets + .end +``` + +Link command: + +``` +-a +-e start +-o payload.out +-m payload.map +payload.obj +SECTIONS +{ + .text 02000000h: {} /* example: bit 0x02000000 = byte 0x400000, in RAM */ +} +``` + +Then either read `payload.out` as COFF (true bit address in the section header) +or `gsprom -i payload.out hi lo`, interleave low/high, and place the bytes at +`0x02000000 >> 3 = 0x400000`. + +The RAM region is `0x0000000`–`0x1FFFFFF` (4 MiB) and the ROM is +`0x3F200000`–`0x3FFFFFFF` in the MAME model; choose a free RAM range for a +payload, or load into RAM and jump. The SDK supplies the image and its entry +point, **not** the network delivery mechanism. Delivery is a separate problem: +the COFF gives a loader the exact bit address and entry point; GSPROM's hex is +only for burning EPROMs. + +For C, use the same idea with `.text`/`.cinit` at the payload address and +`.bss` in RAM; `-c` will include `boot.obj` and make `_c_int00` the entry. + +--- + +## 7. Documentation: what the SDK adds over the repo + +The repo already has `docs/TMS34010-users-guide.pdf`/`.txt` (the 1988 hardware +user's guide). The SDK's unique, useful additions are: + +* **`SPVU004A TMS34010 Assembly Language Tools Users Guide (1987).pdf`** — the + key document: assembler directives and expression rules, the COFF appendix + (file/section/relocation/symbol formats), linker `MEMORY`/`SECTIONS`, + `gsprom`, and the simulator. This is where the bit-address and `W`/`L` + behaviour is specified. +* **`SPVU004A … Assembly Language Tools Reference Card (1987).pdf`** — compact + invocation/option/directive summary; the fastest lookup. +* **`SPVU005A TMS34010 C Compiler Reference Guide (1988-08).pdf`** — data + types, ABI, runtime environment, library reference. The ABI section is what + makes C/assembly interworking possible. +* **`SPVU005A … C Compiler Reference Card (1988-08).pdf`** — quick ABI/options. +* `SPVU005 TMS34010 C Compiler Users Guide (1986-12).pdf` — older; the 1988 + guide supersedes it. +* `SPVU002A Software Development Board Users Guide` + pocket reference — the + SDB and its monitor; useful only if you have that board. +* `SPVU021A C Source Debugger Users Guide (1991-09)` — the `db340`/`gspmon` + monitor workflow; not directly applicable to the VT1200 unless a compatible + monitor can be loaded. +* `SPVU006 Math Graphics Function Library` and the 1987 `Graphics Math Function + Library` disk (`GRAFIX.LIB`, `TEXT.LIB`, `MATH.LIB` + sources) — optional + drawing primitives; not evaluated against the VT1200's mono framebuffer. +* `SPVS002A/C` production data — hardware electrical/timing detail; the repo's + user's guide covers the programming model. +* `SPVU015A/C` TIGA, `SPVU018A` TIGA art, `SPVU027` graphics library — TIGA is + a PC graphics-interface standard, not the VT1200 terminal; not relevant. +* `Docs/Video Interfaces/*` — TLC3407x RAMDAC/palette datasheets; the VT1200 is + monochrome, so not relevant. +* `Patents/` — graphics-architecture patents; no code or format value. + +--- + +## 8. Findings about `smart34010` itself + +These are worth fixing upstream (they cost real time here): + +1. **Long relative `JR` is printed as `JA`.** In `dis34.c`, the `0x00` low-byte + case (16-bit displacement, 2-word instruction) calls `printf("ja%s …")`; + only the 8-bit case prints `jr%s`. This makes the output non-assemblable + without the word-count fix. Example: `jagt L3F211EE0 ; c700 0167` is a + relative jump, but `jagt` is the 3-word absolute instruction. +2. **B-file `SP` is printed as `sp`.** The register name hides bit 4; `sp` + (A-file) and `b15` (B-file) are different encodings. +3. **`movi` hides the short/long choice.** The disassembler prints one mnemonic + for both encodings, while the assembler auto-selects (and may change the + meaning via sign extension). The `-o` word count is the only clue. +4. **64 KB input fails** with `unexpected end of input at byte 65536`; use + ≤32 KB regions or fix the offset arithmetic. +5. Data tables decoded linearly can produce branches into the middle of + instructions; `-a` (analyze) may help, but for byte-exact reassembly the + `.word` fallback in the converter is what matters. + +The good news: the instruction syntax itself (mnemonics, operands, `@`/`*` +addressing, labels, `.word` data) matches GSPA 2.00 closely; the problems are +these specific ambiguities, not a different dialect. + +--- + +## 9. DOSBox vs dosemu: benchmark + +Separate from the toolchain result. The owner asked whether dosemu is actually +faster. Workload is the real assembler, **GSPA 2.00** (there is no `as34` in +this SDK), on the round-trip sources. + +* **A:** `gspa t1 t1.obj t1.lst -l` — 4 KB region, 1,478 lines, with listing. +* **B:** `gspa -q rt16 rt16.obj` — 16 KB region, 6,124 lines, no listing. +* DOSBox 0.74-3, `core=dynamic`, `cycles=max`, `SDL_VIDEODRIVER=dummy`, + `SDL_AUDIODRIVER=dummy`. +* dosemu 1.4.0.8, `-t -quiet`, private `HOME`, batch ending `exitemu`. +* 1 warm-up + 3 measured runs; median; all exit status 0. +* COFF output compared with the 4-byte time stamp zeroed (raw COFF differs only + in that stamp). + +| workload | DOSBox median | dosemu median | ratio | +|---|---:|---:|---:| +| A: 4 KB with listing | **1.99 s** | **0.35 s** | 5.7× | +| B: 16 KB no listing | **2.23 s** | **0.43 s** | 5.2× | + +The timestamp-normalised object md5 is identical for every run of each +workload and across the two emulators: + +* A: `e9c59beb4a5e29e391495ba2b312fc79` +* B: `69a525b4689f60f51afc3059b66b66a8` + +Raw md5s differ because bytes 4–7 of the COFF header are the creation time; the +assembled bytes are the same. + +Setup cost: DOSBox needed only an existing config; dosemu auto-created +`~/.dosemu`, required a drive C mapping and a batch (because `-E` does not +exit without `exitemu`), and prints harmless CD-ROM/`unknown window sizes` +warnings. That is a one-off few-minute setup, not per-run. + +**Recommendation:** dosemu is materially faster (≈5×) under these settings and +is worth using for batch/iterative assembly. DOSBox remains the known-good +fallback and is what `roundtrip.sh` uses. Note that with DOSBox's original +`cycles=3000` config the gap is much larger (the 16 KB listing workload took +~35 s), because DOSBox was being cycle-limited, not because dosemu is 70× +faster. + +--- + +## 10. Reproduction + +Scripts live in `harness/sdk-scratch/` (scratch; not part of the SDK): + +* `roundtrip.sh OFF SIZE BASE` — extract, disassemble, convert, assemble, link, + convert, interleave, diff. Example: + `./roundtrip.sh 0x1000 32768 3F208000` +* `dis2as34.py BASE SIZE IN.ASM` — the five fixups. +* `bench2.py` — the DOSBox/dosemu benchmark (needs the private dosemu `HOME` + under `harness/sdk-scratch/dosemu-home`). +* `dosbox.conf` / `dosbox-bench.conf` — headless DOSBox configs. + +The SDK itself is untouched; all extraction goes to `harness/sdk-scratch/` +(`work/`, `cgtools/`, `sdb/`, `disk1987/`), which is not staged. + +--- + +## 11. Open questions / not tested + +* No payload was executed on a VT1200 or in MAME. The round trip proves the + encoder, not the runtime or the network delivery. +* No OS/2 environment was set up, so the r5.01 tools (and their `gspopt` + optimizer) were not exercised. If the newer code generator matters, that is a + separate OS/2-in-QEMU project. +* The 1987 `GSPSIM` simulator was not run; MAME is the better execution model + for this project. +* The Math Graphics Function Library and TIGA libraries were not evaluated + against the VT1200's monochrome display. +* The optimal free RAM range for a payload depends on what the OS has allocated; + the toolchain can link anywhere, but the loader must choose a safe range.