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 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-17 17:24:33 -05:00
co-authored by Claude Opus 4.8
parent 2a11838d74
commit 38dd84f5ec
+18 -2
View File
@@ -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)