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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user