forked from arti/vt1200
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.
118 lines
5.6 KiB
Markdown
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.
|