From 43d0258c0cc34301f5931b5ad0cdf93b306376bd Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 16 Jul 2026 19:06:32 -0500 Subject: [PATCH] emu860: implement i860 dual-instruction mode (DIM) delay-slot semantics THE bug that silently culled all non-VSTRIP geometry in every capture. In DIM the chip executes FP+core instruction PAIRS, and a delayed branch's slot is the whole NEXT PAIR (2 words). The serial interpreter executed one delay word, silently skipping the pair's core half -- e.g. the corner transform's final fst.d z,w (0xf04213a4, in the delay pair of its bri): the last bbox corner kept stale z/w, the in-place buffer decayed across passes (stale w=0 loses the translation), every object classified fully-outside, and the classify->clip-draw path emitted nothing while the VSTRIP path (different codegen) worked. Fix: DIM state machine (_dim/_dim_on/_dim_exit/_dim_half) per i860 PRM ch.8 -- entry d.fpop -> one more serial instr -> DIM; exit pair-with-D=0 -> one more pair -> serial; pair halves tracked positionally (reset at control transfers); fnop/d.fnop (0xb0000000/0xb0000200, the shrd-encoded FP-slot filler whose 0x200 bit IS the D bit) recognized as FP halves -- missing it misaligned the halves, missed the fnop(D=0) exit markers, and leaked DIM into serial code. Delay-slot width decided from DIM state at branch FETCH time. Acceptance: (1) corner transform now writes all 8 corners, w=1.0, and the model-view matrix gains its real translation row (was zeros -- the concat had the same bug); (2) cap7 regression clean, now 90 verts/frame vs 45 (a second instance survives the no-longer-false cull); (3) klngvid runs past its draws cleanly (previously wandered into the data segment). Debug chain: flowtrace/cliptrace/planecheck/xformcheck2/wandertrap.py (scratchpad) -- plane test hand-verified correct 10/10, inputs proven stale, final store traced to the skipped delay pair. Co-Authored-By: Claude Opus 4.8 --- emulator/firmware-decomp/emu860.py | 68 ++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/emulator/firmware-decomp/emu860.py b/emulator/firmware-decomp/emu860.py index c08e7a1..0a8c501 100644 --- a/emulator/firmware-decomp/emu860.py +++ b/emulator/firmware-decomp/emu860.py @@ -129,6 +129,18 @@ class I860: self.tailn = 60 # ring buffer of last-N executed self.tail = [] self.stopat = set() # halt when pc first reaches one of these + # Dual-instruction mode (DIM) tracking. In DIM the chip fetches FP+core + # PAIRS (FP at 8-aligned addr, core at +4) and a delayed branch's "slot" + # is the whole NEXT PAIR (2 words). A serial interpreter is equivalent + # for straight-line dual code, but must widen delay slots or it silently + # SKIPS the pair's core half (found via 0xf0421180's final fst.d z,w -- + # the corner-transform corruption that culled every clip-path object). + # Entry: d.fpop in serial -> one more serial instr -> DIM. + # Exit: pair with FP D=0 -> one more dual pair -> serial (i860 PRM ch.8). + self._dim = False # dual mode active + self._dim_on = False # entry armed (one more serial instr) + self._dim_exit = None # None | 'armed' | 'last' + self._dim_half = 0 # 0 = expecting the pair's FP half def log(self, s): self.logf.write(s + "\n") @@ -158,6 +170,34 @@ class I860: def cc(self): return (self.cr[1] >> 2) & 1 # ------------- one instruction ------------- + def _dim_track(self, w, pc): + """Advance the dual-instruction-mode state machine after executing (w, pc). + Pair halves are tracked POSITIONALLY (_dim_half alternates; reset to the FP + half at every control transfer) -- not by address parity, which breaks when + a dual region's pairs aren't 8-aligned and then leaks DIM into serial code. + FP-half instructions = op-0x12 FP escapes AND fnop/d.fnop (0xb0000000 / + 0xb0000200 -- shrd r0,r0,r0 encoding used as the FP-slot filler; its 0x200 + bit IS the D bit). Missing fnop made the tracker misalign halves and miss + the fnop(D=0) exit markers -> DIM leaked into serial code.""" + fp = (((w >> 26) & 0x3f) == 0x12) or ((w & 0xfffffdff) == 0xb0000000) + if self._dim: + if self._dim_half == 0: # FP half of a dual pair + if fp and not (w & 0x200) and self._dim_exit is None: + self._dim_exit = 'armed' # D clear: one more pair, then off + self._dim_half = 1 + else: # core half: a pair completed + if self._dim_exit == 'last': + self._dim = False; self._dim_exit = None + elif self._dim_exit == 'armed': + self._dim_exit = 'last' + self._dim_half = 0 + else: + if self._dim_on: # the one-more serial instr ran + self._dim = True; self._dim_on = False + self._dim_exit = None; self._dim_half = 0 + elif fp and (w & 0x200): # d.fpop in serial: arm entry + self._dim_on = True + def step(self): pc = self.pc if pc in self.stopat: @@ -169,26 +209,38 @@ class I860: if self.trace and self.steps < self.trace: m, ops = dis860.decode(w, pc) self.log(f"{pc:#010x}: {w:08x} {m:<10} {ops}") - self._branch = None + dim_at_fetch = self._dim # DIM-ness of a branch's slot is fixed at + self._branch = None # fetch (pair membership), not post-exec self.execute(w, pc) self.steps += 1 + self._dim_track(w, pc) if self.stop: return False if self._branch is not None: target, delayed = self._branch if delayed: - # execute one delay-slot instruction, then jump + # delay slot: ONE instruction in serial mode; the whole next + # PAIR (2 words) in dual-instruction mode. + nslots = 2 if dim_at_fetch else 1 ds = pc + 4 - w2 = self.mem.r32(ds) - if self.trace and self.steps < self.trace: - m, ops = dis860.decode(w2, ds) - self.log(f"{ds:#010x}: {w2:08x} {m:<10} {ops} ; [delay slot]") self._branch = None - self.execute(w2, ds) - self.steps += 1 + for _ in range(nslots): + w2 = self.mem.r32(ds) + if self.tailn: + self.tail.append((ds, w2)) + if len(self.tail) > self.tailn: self.tail.pop(0) + if self.trace and self.steps < self.trace: + m, ops = dis860.decode(w2, ds) + self.log(f"{ds:#010x}: {w2:08x} {m:<10} {ops} ; [delay slot]") + self.execute(w2, ds) + self.steps += 1 + self._dim_track(w2, ds) + ds += 4 self.pc = self._branch[0] if self._branch else target + self._dim_half = 0 # branch target starts a fresh pair else: self.pc = target + self._dim_half = 0 else: self.pc = pc + 4 return not self.stop