Files
vt1200/harness/c-hello/README.md
T
arti 8f831eaf33 fix(video): model the real unit's VR319 monitor
The harness forced the VR150 monitor switch, so the firmware wrote the OS
screen at 1024 px / 128-byte rows.  VT1200_VRAM_REAL then re-read that
framebuffer at the VR319 pitch, which sheared the OS and wrapped the right
quarter of every row.

Model VR319 (switch 0x60) by default -- the real unit's mode -- so the
firmware itself programs 1280x1024 with a 256-byte row pitch (CONVDP=0x14).
The renderer's pitch follows VT1200_MONITOR, and the destination span now
comes from HEBLNK..HSBLNK instead of x=0, placing the active 1280 px at
x=336 in MAME's 1696-wide raster.

Verified: clean Terminal Manager at frame 600, legible faithful diagnostics
menu and level-1 error screens, tile mode PASS with tile_check 1280/1280
tiles, tablet/mouse pixel-identical.  VT1200_MONITOR=VR150 reproduces the
old geometry; the regression baseline moves 198 -> 199 (2 PCs) and is
recorded in harness/mame/README.md.
2026-10-31 20:53:51 +02:00

118 lines
5.6 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's VR150 mode
The two framebuffers differ, and the payload carries the difference as
`ROW_BYTES`. **The harness now defaults to the real unit's VR319 monitor**, so
MAME's default geometry is the real one; `VT1200_MONITOR=VR150` restores the old
MAME layout (and needs the `build.sh emu` image).
| | real unit (measured) = MAME VR319 (default) | MAME VR150 |
|---|---|---|
| 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.