Files
vt1200/harness/c-hello
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
..

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

./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):

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):

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.