Tier 0 emulator: i860 interpreter boots VREND.MNG + runs real render code

emu860.py -- an i860 interpreter (reuses the dis860 decoder) that executes the
real Division firmware. This is the foundation of the preservation-grade
emulator (run the board's own code, vs the GL bridge which interprets the wire).

- Boots VREND.MNG cleanly: i860 init (psr/dirbase/fsr), enables the MMU, then
  idle-spins at 0xf0400590 waiting for a transputer interrupt (init complete).
  Board-config regs modelled (0xfffff720=2, 0xfffff70c=1) via map_control().
- Call harness (cpu.call): invoke firmware functions directly, bypassing the
  (un-emulated) transputer that normally drives the CCB.
- Correct memory map: .data/.bss linked LOW (DATA_BASE=0), so globals resolve.
- FP unit with double precision (register pairs): fadd/fsub/fmul/famov/frcp/
  frsqr/fix/ftrunc/fgt/feq/fxfr/fiadd/fisub.
- Delay slots, control regs, sparse MMIO memory with trap/logging, tail-trace
  and stopat debug aids.

draw_scene now runs real code (incl. a double int->float conversion) until it
needs accumulated scene state -- next step is replaying a captured wire command
sequence through the command dispatcher (jump table @0x47cb8) so the scene
builds and draw_scene emits the IGC coefficient stream (Tier-1 handoff).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-14 23:38:25 -05:00
co-authored by Claude Fable 5
parent 1323397a50
commit e28232e409
2 changed files with 502 additions and 0 deletions
+72
View File
@@ -155,3 +155,75 @@ case and read what it writes — the board-side fog disable (`_eof_doFOG`) and a
DAC/colour-map change. That is the ground-truth answer to grayscale-squash+defog
vs a palette. (Anchor utils seen: 0x2c330 = most-called util/log, 0x4408 =
logger, 0xb120/0xb688 = effect helpers.)
## Tier 0 emulator: i860 interpreter (emu860.py) — BOOTS THE FIRMWARE
`emu860.py` executes the real `VREND.MNG` on an emulated i860 (reusing `dis860`'s
decode). First run (`python emu860.py <VREND.MNG> --trace 30`): 5000 instructions,
**zero unimplemented ops**. It runs the firmware's i860 boot sequence correctly —
`psr`/`dirbase`/`fsr` setup, a board-register write at `0x8380_7000`, then it
starts **polling the CCB at `0xFFFFF7xx`** (the `CM200IO.H` `INPUT_CCB` region),
dispatching on `IO_BREAK`(1)/`IO_ACK`(2). I.e. the i860 is waiting on the
**transputer** for work — exactly the T425↔i860 model.
Validated working: integer arith/logic/shift, loads/stores, control regs,
branches + **delay slots**, basic single-precision FP.
**NEXT (Tier 0 cont.):**
1. Model the **CCB handshake** (`0xFFFFxxxx`: `INPUT_CCB 0xF000`/`OUTPUT_CCB
0xF300`, `CLEAR_INT`, the `IO_*` opcodes) so the firmware advances past the
poll loop into its main command loop. Stub the transputer side: feed a
captured wire in, ACK the CCB.
2. Model the **board register region** (`0x8380_xxxx`) as it's touched.
3. Add remaining instructions as the geometry path needs them (dual-instruction
mode, the pipelined `pf*`/graphics FP ops).
4. Capture the **IGC coefficient stream** the firmware emits (Tier 1 handoff).
Then Tier 1 = rasterize the captured coefficients; Tier 2 = the T425 + timing.
### Tier 0 update: full boot mapped → idle (waiting for transputer interrupt)
With `map_control()` feeding the board-config regs (`0xfffff720=2`,
`0xfffff70c=1`), the firmware runs its whole low-level boot in ~73 instructions:
clear the control-register block, write POST-style status codes, **enable the
MMU** (`dirbase` ATE), then **idle-spin at `0xf0400590` (`bri r1`→self)**. That's
init-complete — a self-spin breaks only on an **interrupt**, so the i860 is
waiting for the transputer to hand it CCB commands. The whole application runs in
the interrupt handler, not the boot path.
So the next phase is the **command-processing path**:
1. i860 **interrupt/trap** (INT → PC←`0xFFFFFF00`) + the **MMU** (ATE is now on;
try identity-map first).
2. The **CCB** struct + a **transputer stub** that writes a command and asserts
the interrupt.
3. Inject a captured wire command → the handler dispatches (jump table @0x47cb8)
→ runs → emits IGC coefficients (Tier-1 handoff).
emu860 debug aids added: `map_control()`, tail ring-buffer, `stopat`.
### Tier 0 update: call harness + real globals + FP core (draw_scene runs real code)
Autonomous session progress on emu860.py:
- **Call harness** (`cpu.call(addr, args)`): invoke firmware functions directly
(PGI conv: args r16.., ret r16, r1=return, r2=sp), bypassing the un-emulated
transputer. Validated on `velocirender_statistics`.
- **Data placement FIXED:** `.data`/`.bss` are linked LOW (code builds global
addrs 0xebc4..0x22644). `DATA_BASE=0x0` (verified: globals now carry real data,
e.g. `[0xebc4]` is ASCII, `[0x10]=0xffffffff`). Was the reason cold calls saw
zero globals.
- **FP unit rewritten with DOUBLE precision** (register pairs f[N]=low/f[N+1]=hi):
fadd/fsub/fmul/famov/frcp/frsqr/fix/ftrunc/fgt/feq/fxfr/fiadd/fisub, precision
from bits 7/8. (pf* pipelined ops treated non-pipelined for now.)
- **Result:** `draw_scene` now executes real code (299 instrs incl. a double
int→float conversion helper) until it needs SCENE STATE — cold-calling it on an
empty scene follows uninitialised scene pointers and derails. Confirms a frame
needs the full wire sequence replayed first.
**NEXT (the command-replay phase):** find the top-level command handler
(`velocirender_input`/`remote_velocirender`; dispatcher head ~`0xf040eeb0`, jump
table @file 0x47cb8), feed a **captured wire command sequence** (create/dcs/
list_add → draw_scene) via a CCB struct at `0xffffe000`, so the scene builds and
`draw_scene` renders → capture the IGC coefficient stream (Tier-1 handoff).
Boot config regs: `map_control()` sets `0xfffff720=2`, `0xfffff70c=1`. Note the
transputer set up the MMU/page-tables + trap vector, so the natural interrupt
boot-flow needs the transputer OR the direct-call harness (used here).