From 4f143e3a26f33fe1e1aa4f71c3514cdd5a305e51 Mon Sep 17 00:00:00 2001 From: Cyd Date: Wed, 15 Jul 2026 15:40:29 -0500 Subject: [PATCH] i860 emu: EXACT pipelined FP model (MAME-validated) -- N-R divide passes unhooked; draw_scene #1 runs with 41K IGC board accesses The functional pipe model is replaced with exact i860 semantics, validated against MAME's i860 core (src_opers[] and insn_dualop/insn_fadd_sub verbatim): - Precision bits: SOURCE = bit8, RESULT = bit7 (swapped from our old reading; .ds is invalid except famov). For DUAL ops: sp = multiplier source prec, rp = adder source prec AND all results. - Dual ops (sub 0x00-0x1f): bit10 selects PFAM (fdest <- A-pipe retire) vs PFMAM (fdest <- M-pipe retire) -- NOT "pipelined". FLAGM operands (the A-pipe entries in the DPC table) read the M-pipe in the PFMAM family. T-loads on DPC 2,3,6,7,8,0xb,0xc; K-loads on DPC 1,3,5,7 (from fsrc1 at mul precision; T from the M-pipe last stage). - fdest BYPASS: pipelined ops whose source register equals fdest read the retiring pipe value instead of the stale register (dual mul: op2 only; dual add + scalar pipelined fadd/fmul: both operands). - Pipe stages carry (value, result-precision); push rounds to single when rp=0 (exact magic-constant float->int bit games); retire encodes with the pusher's precision. Adder = 3 stages; multiplier = 2 (double) / 3 (single); pfld = 3-stage load pipe; graphics fiadd/fisub = 1-stage pipe, 64-bit .dd. ACCEPTANCE: (1) the firmware's own Newton-Raphson integer divide now computes 16/16 = 1 through the pipes -- the intdiv hook is retired (authentic execution); (2) draw_scene #1 from the snapshot executes real render phases: coefficient fld.d bursts, 0x800-byte queue-page initialization, tile enqueue, and the first region flush -- 40,959 IGC/board MMIO accesses (previously 0). Frame still running at the profiling time budget (~240M steps in). Co-Authored-By: Claude Opus 4.8 --- emulator/firmware-decomp/emu860.py | 194 +++++++++++++++++++-------- emulator/firmware-decomp/emu_main.py | 5 +- 2 files changed, 143 insertions(+), 56 deletions(-) diff --git a/emulator/firmware-decomp/emu860.py b/emulator/firmware-decomp/emu860.py index c11ec5c..fcc5869 100644 --- a/emulator/firmware-decomp/emu860.py +++ b/emulator/firmware-decomp/emu860.py @@ -263,6 +263,28 @@ class I860: for i in range(size // 4): self.mem.w32(ea + i * 4, self.frd(b0 + i)) return + if op in (0x18, 0x19): # pfld.y (pipelined load: 3-stage load pipe) + fl = w & 7 + size = 4 if (fl & 2) else 8 + if op & 1: + off = s16(imm & (0x10000 - size)) + else: + off = self.rd(src1) + ea = self.rd(src2) + off + if fl & 1: # auto-increment + self.wr(src2, ea) + self._fp_pipes() + entry = (self.mem.r32(ea), self.mem.r32(ea + 4) if size == 8 else None) + out = self._lpipe[2] + self._lpipe[2] = self._lpipe[1]; self._lpipe[1] = self._lpipe[0] + self._lpipe[0] = entry + lo, hi = out + if hi is None: + self.fwr(dest, lo) + else: + b0 = dest & ~1 + self.fwr(b0, lo); self.fwr(b0 | 1, hi) + return if op == 0x0d: # flush #const(src2)[++] (cache-line flush) # Store-format split offset; cache line = 32B so low 5 bits are free: # bit0 = auto-increment (src2 <- src2+offset). No memory effect for us. @@ -396,21 +418,38 @@ class I860: # ---- pipelined FP unit state (functional model, no cycle timing) ---- # Adder pipe = 3-stage; multiplier pipe = 2-stage double / 3-stage single; + # load pipe (pfld) = 3-stage; graphics pipe (pfiadd/pfisub) = 1-stage. # KR/KI/T = the dual-operation constant/transfer registers. + # Each stage carries (value, result-precision): the value is rounded to + # SINGLE at push time when the pushing op has rp=0 (this is what makes the + # compiler's magic-constant float->int bit games come out exact), and the + # retire is ENCODED with the pusher's precision, not the current op's. def _fp_pipes(self): - if not hasattr(self, '_apipe'): - self._apipe = [0.0, 0.0, 0.0] - self._mpipe = [0.0, 0.0, 0.0] - self._kr = 0.0 - self._ki = 0.0 - self._t = 0.0 + if not hasattr(self, '_apipe') or (self._apipe and + not isinstance(self._apipe[0], tuple)): + # fresh init, or normalize legacy float-only snapshots + old_a = getattr(self, '_apipe', [0.0] * 3) + old_m = getattr(self, '_mpipe', [0.0] * 3) + self._apipe = [(float(v), 1) for v in old_a] + self._mpipe = [(float(v), 1) for v in old_m] + self._lpipe = [(0, None)] * 3 # pfld: (lo bits, hi bits | None) + self._gpipe = [0] # graphics: raw 32-bit int + self._kr = getattr(self, '_kr', 0.0) + self._ki = getattr(self, '_ki', 0.0) + self._t = getattr(self, '_t', 0.0) return self._apipe, self._mpipe - def _padv(self, pipe, val, depth): + def _round_rp(self, val, rp): + if rp: + return val + return self.b2f(self.f2b(val)) # round to single + + def _padv(self, pipe, val, rp, depth): + """Push (val,rp) into stage 0; return the (val,rp) leaving stage depth-1.""" out = pipe[depth - 1] for i in range(depth - 1, 0, -1): pipe[i] = pipe[i - 1] - pipe[0] = val + pipe[0] = (self._round_rp(val, rp), rp) return out # PFAM/PFSM dual-op routing, decoded from the validated mnemonic grammar @@ -424,65 +463,95 @@ class I860: # is loaded from src1. sub 0x10-0x1f = PFSM (A-unit subtracts). # Encoded per DPC as (m1,m2, a1,a2, kload, tload) with operand tags: # s1 s2 kr ki t am (adder-pipe result) mm (mul-pipe result) + # Dual-operation DPC routing, verbatim from the i860 spec (validated against + # MAME's i860 core src_opers[] table): (m1, m2, a1, a2, kload, tload). + # 'PP' = the FLAGM operand: A-pipe last stage for the PFAM family (P=1), + # M-pipe last stage for the PFMAM family (P=0). _DUAL = { - 0x0: ('kr','s2', 's1','mm', None, False), # r2p1 - 0x1: ('kr','s2', 't', 'mm', 'kr', False), # r2pt - 0x2: ('kr','s2', 's1','am', None, False), # r2ap1 - 0x3: ('kr','s2', 't', 'am', 'kr', False), # r2apt - 0x4: ('ki','s2', 's1','mm', None, False), # i2p1 - 0x5: ('ki','s2', 't', 'mm', 'ki', False), # i2pt - 0x6: ('ki','s2', 's1','am', None, False), # i2ap1 - 0x7: ('ki','s2', 't', 'am', 'ki', False), # i2apt - 0x8: ('kr','am', 's1','s2', None, True), # rat1p2 (T <- Mres) - 0x9: ('s1','s2', 'am','mm', None, False), # m12apm - 0xa: ('kr','am', 's1','s2', None, False), # ra1p2 - 0xb: ('s1','s2', 't', 'am', None, False), # m12tpa (per grammar) - 0xc: ('ki','am', 's1','s2', None, True), # iat1p2 (T <- Mres) - 0xd: ('s1','s2', 't', 'mm', None, False), # m12tpm - 0xe: ('ki','am', 's1','s2', None, False), # ia1p2 - 0xf: ('s1','s2', 't', 'am', None, False), # m12tpa + 0x0: ('kr','s2', 's1','mm', False, False), # r2p1 + 0x1: ('kr','s2', 't', 'mm', True, False), # r2pt + 0x2: ('kr','s2', 's1','PP', False, True), # r2ap1 + 0x3: ('kr','s2', 't', 'PP', True, True), # r2apt + 0x4: ('ki','s2', 's1','mm', False, False), # i2p1 + 0x5: ('ki','s2', 't', 'mm', True, False), # i2pt + 0x6: ('ki','s2', 's1','PP', False, True), # i2ap1 + 0x7: ('ki','s2', 't', 'PP', True, True), # i2apt + 0x8: ('kr','PP', 's1','s2', False, True), # rat1p2 + 0x9: ('s1','s2', 'PP','mm', False, False), # m12apm + 0xa: ('kr','PP', 's1','s2', False, False), # ra1p2 + 0xb: ('s1','s2', 't', 'PP', False, True), # m12ttpa + 0xc: ('ki','PP', 's1','s2', False, True), # iat1p2 + 0xd: ('s1','s2', 't', 'mm', False, False), # m12tpm + 0xe: ('ki','PP', 's1','s2', False, False), # ia1p2 + 0xf: ('s1','s2', 't', 'PP', False, False), # m12tpa } def exec_fp(self, w, src1, src2, dest): sub = w & 0x7f - sp = (w >> 7) & 1 # source precision (1 = double) - rp = (w >> 8) & 1 # result precision - pbit = (w >> 10) & 1 # P: pipelined - if sub < 0x20: # PFAM (0x00-0x0f) / PFSM (0x10-0x1f) + # MAME-validated bit assignments: SOURCE precision = bit8, RESULT = bit7. + sp = 1 if (w & 0x100) else 0 + rp = 1 if (w & 0x080) else 0 + pbit = (w >> 10) & 1 # pipelined (scalar ops) / PFAM-vs-PFMAM (dual ops) + if sub < 0x20: # dual ops: PF[M]AM (bit4=0) / PF[M]SM (bit4=1) + # sp = MULTIPLIER source precision; rp = ADDER source AND all results. ap, mp = self._fp_pipes() mdepth = 2 if sp else 3 - am_out = ap[2] # retiring adder result - mm_out = mp[mdepth - 1] # retiring multiplier result - v = {'s1': self.rdf(src1, sp), 's2': self.rdf(src2, sp), - 'kr': self._kr, 'ki': self._ki, 't': self._t, - 'am': am_out, 'mm': mm_out} + am_out, am_rp = ap[2] + mm_out, mm_rp = mp[mdepth - 1] + pipe_val = am_out if pbit else mm_out # FLAGM operand + fdest bypass m1, m2, a1, a2, kload, tload = self._DUAL[sub & 0xf] - newm = v[m1] * v[m2] - newa = (v[a1] - v[a2]) if (sub & 0x10) else (v[a1] + v[a2]) - if kload == 'kr': self._kr = v['s1'] - elif kload == 'ki': self._ki = v['s1'] - if tload: self._t = mm_out - self._padv(ap, newa, 3) - self._padv(mp, newm, mdepth) - self.wrf(dest, am_out, rp) # fdest <- adder pipe output + def val(tag, prec): + if tag == 's1': return self.rdf(src1, prec) + if tag == 's2': return self.rdf(src2, prec) + if tag == 'kr': return self._kr + if tag == 'ki': return self._ki + if tag == 't': return self._t + if tag == 'mm': return mm_out + return pipe_val # 'PP' + v1 = val(m1, sp); v2 = val(m2, sp) + if m2 == 's2' and dest and src2 == dest: # fdest bypass (mul op2 only) + v2 = pipe_val + newm = v1 * v2 + u1 = val(a1, rp); u2 = val(a2, rp) + if a1 == 's1' and dest and src1 == dest: u1 = pipe_val + if a2 == 's2' and dest and src2 == dest: u2 = pipe_val + newa = (u1 - u2) if (sub & 0x10) else (u1 + u2) + if tload: self._t = mm_out # T <- M-pipe last stage + if kload: # K <- fsrc1 (mul precision) + if m1 == 'ki': self._ki = self.rdf(src1, sp) + else: self._kr = self.rdf(src1, sp) + if pbit: self.wrf(dest, am_out, am_rp) # PFAM: fdest <- A retire + else: self.wrf(dest, mm_out, mm_rp) # PFMAM: fdest <- M retire + self._padv(ap, newa, rp, 3) + self._padv(mp, newm, rp, mdepth) return - if sub == 0x20: # fmul / pfmul - r = self.rdf(src1, sp) * self.rdf(src2, sp) + if sub in (0x20, 0x24): # fmul / pfmul (0x24 = pfmul3: 3-stage) + v1 = self.rdf(src1, sp); v2 = self.rdf(src2, sp) if pbit: ap, mp = self._fp_pipes() - self.wrf(dest, self._padv(mp, r, 2 if sp else 3), rp) + depth = 3 if sub == 0x24 else (2 if sp else 3) + out, orp = mp[depth - 1] + if dest and src1 == dest: v1 = out # fdest bypass + if dest and src2 == dest: v2 = out + self.wrf(dest, out, orp) + self._padv(mp, v1 * v2, rp, depth) else: - self.wrf(dest, r, rp) - elif sub in (0x30, 0x31): # fadd/fsub / pfadd/pfsub - a = self.rdf(src1, sp); b = self.rdf(src2, sp) - r = (a - b) if sub == 0x31 else (a + b) + self.wrf(dest, v1 * v2, rp) + elif sub in (0x30, 0x31, 0x33): # fadd/fsub/famov (+ pipelined forms) + v1 = self.rdf(src1, sp); v2 = self.rdf(src2, sp) if pbit: ap, mp = self._fp_pipes() - self.wrf(dest, self._padv(ap, r, 3), rp) + out, orp = ap[2] + if dest and src1 == dest: v1 = out # fdest bypass + if dest and src2 == dest: v2 = out + if sub == 0x33: r = v1 + else: r = (v1 - v2) if sub == 0x31 else (v1 + v2) + self.wrf(dest, out, orp) + self._padv(ap, r, rp, 3) else: + if sub == 0x33: r = v1 + else: r = (v1 - v2) if sub == 0x31 else (v1 + v2) self.wrf(dest, r, rp) - elif sub == 0x33: # famov (move src1, precision-convert) - self.wrf(dest, self.rdf(src1, sp), rp) elif sub == 0x22: # frcp (reciprocal of src2) b = self.rdf(src2, sp) self.wrf(dest, (1.0 / b) if b else 0.0, rp) @@ -513,10 +582,25 @@ class I860: self.fwr(b0, prod & 0xFFFFFFFF); self.fwr(b0 | 1, (prod >> 32) & 0xFFFFFFFF) elif sub == 0x40: # fxfr FP->int self.wr(dest, self.frd(src1)) - elif sub == 0x49: # fiadd (integer add in FP unit, 32-bit) - self.fwr(dest, self.frd(src1) + self.frd(src2)) - elif sub == 0x4d: # fisub - self.fwr(dest, self.frd(src1) - self.frd(src2)) + elif sub in (0x49, 0x4d): # fiadd/fisub (graphics-unit int add/sub; + # .ss = 32-bit, .dd = 64-bit across register pairs; P = 1-stage pipe) + if sp: + a = self.frd(src1 & ~1) | (self.frd((src1 & ~1) | 1) << 32) + b = self.frd(src2 & ~1) | (self.frd((src2 & ~1) | 1) << 32) + m = (1 << 64) - 1 + else: + a = self.frd(src1); b = self.frd(src2); m = MASK32 + r = ((a + b) if sub == 0x49 else (a - b)) & m + if pbit: + self._fp_pipes() + out = self._gpipe[0] + self._gpipe[0] = r + r = out + if sp: + b0 = dest & ~1 + self.fwr(b0, r & MASK32); self.fwr(b0 | 1, (r >> 32) & MASK32) + else: + self.fwr(dest, r) elif sub == 0x5f: # fnop pass else: diff --git a/emulator/firmware-decomp/emu_main.py b/emulator/firmware-decomp/emu_main.py index fb8f06a..3169055 100644 --- a/emulator/firmware-decomp/emu_main.py +++ b/emulator/firmware-decomp/emu_main.py @@ -56,7 +56,10 @@ MAPS = { alloc=0xf042c628, v2p=0xf042c300, lockacq=0xf0423360, lockrel=0xf04233b8, chain=None, chain_tbl=None, chain_tail=None, - intdiv=0xf042ee00, # runtime int divide (r22 = r22/r23) + # intdiv hook retired 2026-07-15: the exact MAME-validated pipeline model + # (PFAM/PFMAM families, FLAGM->MPIPE, sp/rp bit swap, fdest bypass) makes + # the firmware's own Newton-Raphson divide compute correctly. + intdiv=None, seeds=[ (0x1000, 0), # _processorId ],