Files
vt1200/harness/c-hello/README.md
T
arti 41b0979aef c-hello: first C program on the VT1200, running on real hardware
The 1987 TI K&R toolchain already compiled and linked a fib test, but nothing
had ever been executed on a VT1200. This is that program: hello.c draws HELLO
into video RAM (1 bpp, bit 0x04000000, no X11) and is loaded through the COFF
loader with hostload.py, on the real unit and in MAME.

Two runtime traps shaped the design, both recorded in the README:

  * boot.obj's exit() is `for(;;)` and _c_int00 ends in `reti`.  That is a
    standalone-ROM runtime, not a process: the child spun forever, and because
    the VT1200 OS is cooperative it took the whole terminal down (illegal
    opcode on hardware).  entry.asm replaces boot.obj -- set STK/FP, call
    _main, `rets` back to the create_process trampoline, the same return
    xhello uses -- and the image carries no .cinit (tables are built in code).
  * Pointers are bit addresses and pointer arithmetic scales by object size,
    so p + n on a char * is n bytes.  The pre-fix frame in evidence/ is that
    bug: 32 one-byte bars spread across a row.

Framebuffer geometry is not portable: the real unit is 1280 px wide at a
256-byte row pitch, MAME's canvas is 1024 px at 128.  build.sh builds both
(ROW_BYTES), and the README records how the real pitch was measured from a
real-unit frame rather than taken from MAME.

Evidence: real-unit frames in harness/c-hello/evidence/ (the pre-fix stripes
and the working HELLO), and dwt/loader/evidence/real-unit-xhello/ for the X
window that ran on the unit earlier.
2026-10-29 18:25:38 +02:00

116 lines
5.3 KiB
Markdown

# harness/c-hello — the first C program on the VT1200
A C hello world built with the **1987 TI TMS34010 SDK** (K&R compiler,
`GSPCC`/`GSPCG` 2.01, `GSPA`/`GSPLNK` 2.00, `RTS.LIB`) and run on **real
hardware** — not a debugger poke of assembly, and no X11: it writes the word
`HELLO` straight into video RAM.
It is also the project's smallest end-to-end proof of the C path:
```
gspcpp -> gspcc -> gspcg -> gspa -> gsplnk (DOSBox, 1987 tools)
-> hello.coff (GSPLNK COFF, sections at bit addresses)
-> dwt/loader/hostload.py (rdebug -> malloc -> poke COFF -> COFF loader)
-> loader.asm (free-list check, copy .text, zero .bss, create_process)
-> _start (entry.asm) -> main -> VRAM at bit 0x04000000
```
## Files
| file | what it is |
|---|---|
| `hello.c` | the payload: 5x7 font, 8x-scaled `HELLO` in a black 256x72 box, markers at `0x018A8000` |
| `entry.asm` | the process entry: sets `STK`/`FP`, `calla _main`, `rets` (replaces `boot.obj`) |
| `build.sh` | DOSBox wrapper; `build.sh` = real unit (256-byte row pitch), `build.sh emu` = MAME (128) |
| `build/hello.coff` | linked image for the real unit, entry `_start` |
| `build/hello-emu.coff` | same source with `ROW_BYTES 128`, for MAME's canvas |
| `build/hello.map`, `hello.lst`, `entry.lst`, `hello.s` | link map and listings |
| `evidence/hello_frame.png` | real unit, **pre-fix**: the pointer-arithmetic bug, one-byte bars spread over a whole row |
| `evidence/hello_real_frame.png` | real unit, working: the current box at x=40 and, to its right, the previous run's box at x=320 (nothing erases VRAM between runs) |
| `work/` | extracted SDK + DOS scratch (git-ignored) |
## Build
```sh
./harness/c-hello/build.sh # real unit: .text 0x018A0000, .bss 0x01A00000
./harness/c-hello/build.sh emu # MAME: same addresses, 128-byte row pitch
```
Both land inside the firmware's large free block (byte `0x0C03AA..0x400000` on a
fresh boot). The link map must show `.cinit` empty and entry `_start`:
```
ENTRY POINT SYMBOL: "_start" address: 018A17E0
.text 018A0000 .cinit 018A1870 (size 0) .bss 01A00000
```
## Run
Real unit (rdebug on TCP 771; the terminal needs the OS up):
```sh
python3 dwt/heapmap.py --host 100.103.100.103 --at 0x018A0000 # read-only preflight
python3 dwt/heapmap.py --host 100.103.100.103 --at 0x01A00000
python3 dwt/loader/hostload.py --host 100.103.100.103 \
--coff harness/c-hello/build/hello.coff \
--out harness/c-hello/evidence --real --settle 3
python3 dwt/solve.py --host 100.103.100.103 --peek 0x018A8000 --len 8 # HELL DONE
python3 dwt/screenshot.py --host 100.103.100.103 --real --out /tmp/hello.png
```
MAME (debugger-assisted, no network):
```sh
VT1200_SKIP_LEVEL1=1 LOADER_AT=2500 LOADER_END=2800 LOADER_SNAP=1 \
LOADER_COFF=$PWD/harness/c-hello/build/hello-emu.coff LOADER_OUT=/tmp/chello \
/home/arti/code/k-space/mame0289/vt1200 vt1200 \
-rompath harness/mame/roms -video none -sound none -nothrottle -skip_gameinfo \
-debug -debugger none -autoboot_script dwt/loader/loader.lua -autoboot_delay 0
```
Success = loader status 0, `entry=0x018A17E0`, markers `48454C4C`/`444F4E45`
(`HELL`/`DONE`), and the box on screen. On the real unit the box is at
**x=40, y=800**, 256x72 pixels.
## Three traps this image exists to record
1. **`boot.obj`'s `exit()` is `for(;;)` and `_c_int00` ends in `reti`.**
That is a standalone-ROM runtime, not a process. Linked with `-c`, the child
finishes `main`, spins forever (the linked `jruc self` at `0x018A1520`), and
because the VT1200 OS is cooperative it starves the whole machine; on real
hardware the terminal fell over with an illegal opcode. `-cr` is no better —
it still pulls in `boot.obj` and still defines `_c_int00`. Hence
`entry.asm`: no `boot.obj`, entry `_start`, plain `rets` back to the
`create_process` trampoline (the same return `xhello` uses). `SP` (the OS's
interrupt stack) is left alone; only `STK`/`FP` are set.
2. **Pointers are bit addresses and pointer arithmetic scales by object size.**
A `char *` step is 8 bits, so `p + px*8` advances 8 *bytes*, not one. Address
bytes as `addr + px*8` bits and cast. The pre-fix frame
(`evidence/hello_frame.png`) is that bug: 32 one-byte bars spread over a
whole row.
3. **No `.cinit`.** With `entry.asm` the runtime's autoinit pass never runs, so
the image must not need static initializers. The font and order tables are
filled in `build_font()` at runtime.
## Geometry is not portable between the real unit and MAME
The two framebuffers differ, and the payload carries the difference as
`ROW_BYTES`:
| | real unit (measured) | MAME |
|---|---|---|
| base | bit `0x04000000` | bit `0x04000000` |
| row pitch | **256 bytes** (`ROW_BYTES 256`) | 128 bytes (`VMEM_PITCH_BITS`) |
| pixels per row | 1280 (160 bytes) + 96 B `0x55` mask | 1024 |
| screen | 1280x1024 | 1024x864 snapshot in MAME's 1280x901 |
The real pitch is measured, not assumed: a payload writing at `row*2048` bits
lands on scanline `row` and a 256-pixel box at pixel 40 appears at x=40 — and
the pre-fix frame's bars, written 8 bytes apart from byte 40, run to the end of
a 1280-pixel line. Under a 128-byte pitch the same writes would wrap into the
next memory row. `dwt/screenshot.py --real` (1280 wide, stride 256, LSB-first)
also decodes live VRAM correctly, which pins the bit order.
Do not use a MAME frame as proof of pixel placement: same code, different
framebuffer.