From 38dd84f5ec0f94239fec487854bff9857dd634f1 Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 17 Jul 2026 17:24:33 -0500 Subject: [PATCH] i860: annul the delay slot of a not-taken bc.t/bnc.t (fixes >512-object captures) The interpreter executed bc.t/bnc.t's delay-slot instruction on both the taken and not-taken paths. On real i860 the ".t" conditional branches annul that slot when NOT taken -- the compiler fills it with the loop body's first pointer load, valid only when the branch continues: xor 0,r4,r0 ; CC = (node == NULL) bnc.t loop ; continue while node != NULL fld.d 0x20(r4),f16 ; annulled when node==NULL; else reads *NULL For cap7/trek/batest (<512 live objects) the stray reads land in dead registers, so the bug stayed latent -- cap7's rendered coefficient stream is byte-identical before/after this change (verified: md5 52e16774... over the first 3 draws). But the same stray access corrupted the object-registry tail-find walk (REGISTER @0xf04041f8) whenever the 512-bucket handle hash chained (>512 live objects), orphaning entries so FIND_REMOTE missed -> NULL -> firmware exit() ("Attempt to add NULL to a list"). That killed every content capture (fxtest/sdemo4/glblade) ~1/3 in, before any draw. Fix: a _squash flag skips the delay slot (whole next pair in DIM) on a not-taken bc.t/bnc.t. fxtest now replays all 18987 commands and emits 3.67M coefficient words of real geometry (previously 0 -- it died at cmd 6277); cap7's full mission is unaffected. Co-Authored-By: Claude Opus 4.8 --- emulator/firmware-decomp/emu860.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/emulator/firmware-decomp/emu860.py b/emulator/firmware-decomp/emu860.py index 0a8c501..c822800 100644 --- a/emulator/firmware-decomp/emu860.py +++ b/emulator/firmware-decomp/emu860.py @@ -126,6 +126,7 @@ class I860: self.stop = False self.stopmsg = '' self._branch = None # (target, delayed?) + self._squash = False # annul the next delay slot (bc.t/bnc.t not taken) self.tailn = 60 # ring buffer of last-N executed self.tail = [] self.stopat = set() # halt when pc first reaches one of these @@ -211,6 +212,7 @@ class I860: self.log(f"{pc:#010x}: {w:08x} {m:<10} {ops}") 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._squash = False # cleared each fetch; set by a not-taken .t self.execute(w, pc) self.steps += 1 self._dim_track(w, pc) @@ -241,6 +243,14 @@ class I860: else: self.pc = target self._dim_half = 0 + elif self._squash: + # bc.t / bnc.t NOT taken: the i860 annuls the delay-slot instruction + # (whole next PAIR in DIM). Skip it -- executing it corrupts loops whose + # exit relies on the slot NOT running (e.g. the registry tail-find walk, + # which only chains -- and only breaks -- above 512 live objects). + nslots = 2 if dim_at_fetch else 1 + self._squash = False + self.pc = pc + 4 + nslots * 4 else: self.pc = pc + 4 return not self.stop @@ -371,11 +381,17 @@ class I860: self.wr(1, pc + 8); self.branch(pc + 4 + s26(w & 0x03ffffff) * 4); return if op in (0x1c, 0x1d): # bc / bc.t (branch if CC) tgt = pc + 4 + s26(w & 0x03ffffff) * 4 - if self.cc(): self.branch(tgt, delayed=(op == 0x1d)) + if self.cc(): + self.branch(tgt, delayed=(op == 0x1d)) + elif op == 0x1d: # bc.t not taken: i860 ANNULS the delay slot + self._squash = True return if op in (0x1e, 0x1f): # bnc / bnc.t (branch if !CC) tgt = pc + 4 + s26(w & 0x03ffffff) * 4 - if not self.cc(): self.branch(tgt, delayed=(op == 0x1f)) + if not self.cc(): + self.branch(tgt, delayed=(op == 0x1f)) + elif op == 0x1f: # bnc.t not taken: i860 ANNULS the delay slot + self._squash = True return if op in (0x14, 0x15, 0x16, 0x17): # btne / bte (compare & branch) broff = s16(((dest << 11) | (w & 0x7ff)) & 0xffff)