Replace the one-basic-block-per-slot-per-round lockstep dispatch with a
SUPERBLOCK runner: each slot-visit chains straight-line blocks through
their terminating branches up to a deterministic instruction budget,
amortizing the per-round (timebase/coord/round_schedule) and per-slot
(worker_prologue) dispatch tax over ~128 instructions instead of ~6.
Yield-points (end the chain, return to the round) are pure functions of
guest state, preserving the lockstep cross-thread interleaving correctness:
- non-Continue step result (Yield/SystemCall/Trap/Unimpl/Halted);
db16cyc Yield is the spin-wait producer hand-off.
- sync-sensitive block: lwarx/ldarx/stwcx./stdcx. or sync/eieio/isync
(new PpcOpcode::is_sync_sensitive, flagged on DecodedBlock at build).
- MMIO touch: new GuestMemory::mmio_access_count() watermark, sampled
per block, keeps GPU/register ordering at one-block granularity.
- next PC leaves ordinary guest code (import thunk / halt sentinel /
unmapped) -> hand to the full worker_prologue next round.
- instruction budget reached.
Instruction-count/clock accounting stays exact: per-block cycle_count
deltas are summed and handed to worker_epilogue once (instruction_count +
decrement_quantum advance by the precise retired count). XENIA_SUPERBLOCK_BUDGET=1
reproduces the old one-block schedule byte-for-byte.
Budget tuned to 128 (env-overridable): boot progression stays healthy up
to 256, sharp cliff at ~384 (a boot producer/consumer handoff starves);
128 is 3x below the cliff. Also scale the inline-GPU per-round fairness
cap with the budget (flat 64 throttled GPU command processing 17x under
superblocks and collapsed the present loop).
PERF (check -n 100M --gpu-inline): 25.3 -> 42.7 MIPS (1.69x); 1B: 26.0 ->
41.4 MIPS (1.59x). Callgrind n=5M: host instructions 2.178B -> 1.507B
(-31%); worker_prologue -90%, coord_pre_round -91%, begin_slot_visit /
round_schedule_into / coord_post_round / update_timestamp_bundle each
~-90%; interpreter execute byte-identical (real work unchanged).
GATES: C1 boot progression 150M draws 7391/swaps 2164 (baseline 7415/2172),
1B draws 88547/swaps 29228 linear no stall, K8888 decode + RTs=2 intact.
C2 determinism: n50m stable digest byte-identical across fresh runs;
golden re-baselined intentionally (pacing-only deltas: imports 333453->243387,
draws 1274->1279). C3 milestone-1 render: texture_decodes/draws/swaps/
present cadence track baseline (3AJ fade-in pacing preserved). C4: 690
tests green (+2 sync_sensitive).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
309 lines
12 KiB
Rust
309 lines
12 KiB
Rust
/// All PPC opcodes supported by the Xbox 360, including VMX128 extensions.
|
|
/// Directly mirrors the C++ PPCOpcode enum from ppc_opcode.h.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
#[repr(u32)]
|
|
#[allow(non_camel_case_types)]
|
|
pub enum PpcOpcode {
|
|
// ALU
|
|
addcx, addex, addi, addic, addicx, addis, addmex, addx, addzex,
|
|
andcx, andisx, andix, andx,
|
|
// Branch
|
|
bcctrx, bclrx, bcx, bx,
|
|
// Compare
|
|
cmp, cmpi, cmpl, cmpli,
|
|
// Count leading zeros
|
|
cntlzdx, cntlzwx,
|
|
// Condition register
|
|
crand, crandc, creqv, crnand, crnor, cror, crorc, crxor,
|
|
// Data cache
|
|
dcbf, dcbi, dcbst, dcbt, dcbtst, dcbz, dcbz128,
|
|
// Division
|
|
divdux, divdx, divwux, divwx,
|
|
// Sync/barrier
|
|
eieio,
|
|
// Logical
|
|
eqvx, extsbx, extshx, extswx,
|
|
// FPU
|
|
fabsx, faddsx, faddx, fcfidx, fcmpo, fcmpu, fctidx, fctidzx, fctiwx, fctiwzx,
|
|
fdivsx, fdivx, fmaddsx, fmaddx, fmrx, fmsubsx, fmsubx, fmulsx, fmulx,
|
|
fnabsx, fnegx, fnmaddsx, fnmaddx, fnmsubsx, fnmsubx, fresx, frspx, frsqrtex,
|
|
fselx, fsqrtsx, fsqrtx, fsubsx, fsubx,
|
|
// Instruction cache
|
|
icbi, isync,
|
|
// Load byte
|
|
lbz, lbzu, lbzux, lbzx,
|
|
// Load doubleword
|
|
ld, ldarx, ldbrx, ldu, ldux, ldx,
|
|
// Load float
|
|
lfd, lfdu, lfdux, lfdx, lfs, lfsu, lfsux, lfsx,
|
|
// Load halfword
|
|
lha, lhau, lhaux, lhax, lhbrx, lhz, lhzu, lhzux, lhzx,
|
|
// Load multiple/string
|
|
lmw, lswi, lswx,
|
|
// Load vector
|
|
lvebx, lvehx, lvewx, lvewx128, lvlx, lvlx128, lvlxl, lvlxl128,
|
|
lvrx, lvrx128, lvrxl, lvrxl128,
|
|
lvsl, lvsl128, lvsr, lvsr128,
|
|
lvx, lvx128, lvxl, lvxl128,
|
|
// Load word
|
|
lwa, lwarx, lwaux, lwax, lwbrx, lwz, lwzu, lwzux, lwzx,
|
|
// Move CR
|
|
mcrf, mcrfs, mcrxr,
|
|
// Move from special
|
|
mfcr, mffsx, mfmsr, mfspr, mftb, mfvscr,
|
|
// Move to special
|
|
mtcrf, mtfsb0x, mtfsb1x, mtfsfix, mtfsfx, mtmsr, mtmsrd, mtspr, mtvscr,
|
|
// Multiply
|
|
mulhdux, mulhdx, mulhwux, mulhwx, mulldx, mulli, mullwx,
|
|
// Logical
|
|
nandx, negx, norx, orcx, ori, oris, orx,
|
|
// Rotate
|
|
rldclx, rldcrx, rldiclx, rldicrx, rldicx, rldimix, rlwimix, rlwinmx, rlwnmx,
|
|
// System call
|
|
sc,
|
|
// Shift
|
|
sldx, slwx, sradix, sradx, srawix, srawx, srdx, srwx,
|
|
// Store byte
|
|
stb, stbu, stbux, stbx,
|
|
// Store doubleword
|
|
std, stdbrx, stdcx, stdu, stdux, stdx,
|
|
// Store float
|
|
stfd, stfdu, stfdux, stfdx, stfiwx, stfs, stfsu, stfsux, stfsx,
|
|
// Store halfword
|
|
sth, sthbrx, sthu, sthux, sthx,
|
|
// Store multiple/string
|
|
stmw, stswi, stswx,
|
|
// Store vector
|
|
stvebx, stvehx, stvewx, stvewx128, stvlx, stvlx128, stvlxl, stvlxl128,
|
|
stvrx, stvrx128, stvrxl, stvrxl128,
|
|
stvx, stvx128, stvxl, stvxl128,
|
|
// Store word
|
|
stw, stwbrx, stwcx, stwu, stwux, stwx,
|
|
// Subtract
|
|
subfcx, subfex, subficx, subfmex, subfx, subfzex,
|
|
// Sync
|
|
sync,
|
|
// Trap
|
|
td, tdi, tw, twi,
|
|
// VMX integer
|
|
vaddcuw, vaddfp, vaddfp128, vaddsbs, vaddshs, vaddsws,
|
|
vaddubm, vaddubs, vadduhm, vadduhs, vadduwm, vadduws,
|
|
vand, vand128, vandc, vandc128,
|
|
vavgsb, vavgsh, vavgsw, vavgub, vavguh, vavguw,
|
|
vcfpsxws128, vcfpuxws128, vcfsx, vcfux,
|
|
vcmpbfp, vcmpbfp128, vcmpeqfp, vcmpeqfp128,
|
|
vcmpequb, vcmpequh, vcmpequw, vcmpequw128,
|
|
vcmpgefp, vcmpgefp128, vcmpgtfp, vcmpgtfp128,
|
|
vcmpgtsb, vcmpgtsh, vcmpgtsw, vcmpgtub, vcmpgtuh, vcmpgtuw,
|
|
vcsxwfp128, vctsxs, vctuxs, vcuxwfp128,
|
|
vexptefp, vexptefp128, vlogefp, vlogefp128,
|
|
vmaddcfp128, vmaddfp, vmaddfp128,
|
|
vmaxfp, vmaxfp128, vmaxsb, vmaxsh, vmaxsw, vmaxub, vmaxuh, vmaxuw,
|
|
vmhaddshs, vmhraddshs,
|
|
vminfp, vminfp128, vminsb, vminsh, vminsw, vminub, vminuh, vminuw,
|
|
vmladduhm,
|
|
vmrghb, vmrghh, vmrghw, vmrghw128, vmrglb, vmrglh, vmrglw, vmrglw128,
|
|
vmsum3fp128, vmsum4fp128,
|
|
vmsummbm, vmsumshm, vmsumshs, vmsumubm, vmsumuhm, vmsumuhs,
|
|
vmulesb, vmulesh, vmuleub, vmuleuh, vmulfp128,
|
|
vmulosb, vmulosh, vmuloub, vmulouh,
|
|
vnmsubfp, vnmsubfp128, vnor, vnor128,
|
|
vor, vor128,
|
|
vperm, vperm128, vpermwi128, vpkd3d128,
|
|
vpkpx, vpkshss, vpkshss128, vpkshus, vpkshus128,
|
|
vpkswss, vpkswss128, vpkswus, vpkswus128,
|
|
vpkuhum, vpkuhum128, vpkuhus, vpkuhus128,
|
|
vpkuwum, vpkuwum128, vpkuwus, vpkuwus128,
|
|
vrefp, vrefp128,
|
|
vrfim, vrfim128, vrfin, vrfin128, vrfip, vrfip128, vrfiz, vrfiz128,
|
|
vrlb, vrlh, vrlimi128, vrlw, vrlw128,
|
|
vrsqrtefp, vrsqrtefp128,
|
|
vsel, vsel128,
|
|
vsl, vslb, vsldoi, vsldoi128, vslh, vslo, vslo128, vslw, vslw128,
|
|
vspltb, vsplth, vspltisb, vspltish, vspltisw, vspltisw128, vspltw, vspltw128,
|
|
vsr, vsrab, vsrah, vsraw, vsraw128, vsrb, vsrh, vsro, vsro128, vsrw, vsrw128,
|
|
vsubcuw, vsubfp, vsubfp128, vsubsbs, vsubshs, vsubsws,
|
|
vsububm, vsububs, vsubuhm, vsubuhs, vsubuwm, vsubuws,
|
|
vsum2sws, vsum4sbs, vsum4shs, vsum4ubs, vsumsws,
|
|
vupkd3d128, vupkhpx, vupkhsb, vupkhsb128, vupkhsh,
|
|
vupklpx, vupklsb, vupklsb128, vupklsh,
|
|
vxor, vxor128,
|
|
// XOR immediate
|
|
xori, xoris, xorx,
|
|
// Invalid
|
|
Invalid,
|
|
}
|
|
|
|
impl PpcOpcode {
|
|
/// Returns true if this opcode is a branch instruction.
|
|
pub fn is_branch(&self) -> bool {
|
|
matches!(self, Self::bx | Self::bcx | Self::bclrx | Self::bcctrx)
|
|
}
|
|
|
|
/// Returns true if this opcode is a system call.
|
|
pub fn is_syscall(&self) -> bool {
|
|
matches!(self, Self::sc)
|
|
}
|
|
|
|
/// Returns true if this opcode unconditionally ends a basic block:
|
|
/// any branch, system call, trap, or `Invalid` (decoder couldn't
|
|
/// recognize the instruction — execution will hit the
|
|
/// `Unimplemented` arm and we don't want to swallow the boundary
|
|
/// inside a cached block).
|
|
///
|
|
/// Notably *not* terminating: `mtmsr`/`mtmsrd`/`isync`/`mfmsr`.
|
|
/// On real hardware these have synchronization semantics (a context
|
|
/// synchronizing event for `isync`, MSR rewrite for the `mt*`s) but
|
|
/// our interpreter has no asynchronous-exception model and no
|
|
/// out-of-order execution — they execute as plain ALU/move ops and
|
|
/// don't change control flow synchronously. Block-cache replay is
|
|
/// still bit-for-bit identical to per-instruction dispatch for
|
|
/// those.
|
|
///
|
|
/// Used by the basic-block cache (`block_cache.rs`) to know when to
|
|
/// stop accumulating instructions during a forward decode walk.
|
|
pub fn terminates_block(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
Self::bx | Self::bcx | Self::bclrx | Self::bcctrx
|
|
| Self::sc
|
|
| Self::td | Self::tdi | Self::tw | Self::twi
|
|
| Self::Invalid
|
|
)
|
|
}
|
|
|
|
/// Returns true if this is a load instruction.
|
|
pub fn is_load(&self) -> bool {
|
|
matches!(self,
|
|
Self::lbz | Self::lbzu | Self::lbzux | Self::lbzx |
|
|
Self::lhz | Self::lhzu | Self::lhzux | Self::lhzx |
|
|
Self::lha | Self::lhau | Self::lhaux | Self::lhax |
|
|
Self::lwz | Self::lwzu | Self::lwzux | Self::lwzx |
|
|
Self::lwa | Self::lwax | Self::lwaux |
|
|
Self::ld | Self::ldu | Self::ldux | Self::ldx |
|
|
Self::lfs | Self::lfsu | Self::lfsux | Self::lfsx |
|
|
Self::lfd | Self::lfdu | Self::lfdux | Self::lfdx |
|
|
Self::lhbrx | Self::lwbrx | Self::ldbrx |
|
|
Self::lmw | Self::lswi | Self::lswx |
|
|
Self::lwarx | Self::ldarx
|
|
)
|
|
}
|
|
|
|
/// Returns true if this is a store instruction.
|
|
pub fn is_store(&self) -> bool {
|
|
matches!(self,
|
|
Self::stb | Self::stbu | Self::stbux | Self::stbx |
|
|
Self::sth | Self::sthu | Self::sthux | Self::sthx |
|
|
Self::stw | Self::stwu | Self::stwux | Self::stwx |
|
|
Self::std | Self::stdu | Self::stdux | Self::stdx |
|
|
Self::stfs | Self::stfsu | Self::stfsux | Self::stfsx |
|
|
Self::stfd | Self::stfdu | Self::stfdux | Self::stfdx |
|
|
Self::sthbrx | Self::stwbrx | Self::stdbrx |
|
|
Self::stmw | Self::stswi | Self::stswx |
|
|
Self::stwcx | Self::stdcx | Self::stfiwx
|
|
)
|
|
}
|
|
|
|
/// Returns true if this opcode is a cross-thread synchronization
|
|
/// point at which the superblock runner MUST yield back to the
|
|
/// round-robin scheduler so the lockstep interleaving stays
|
|
/// fine-grained enough to preserve correct cross-thread ordering:
|
|
///
|
|
/// - reserved load/store (`lwarx`/`ldarx`/`stwcx.`/`stdcx.`): the
|
|
/// atomic primitive other threads race on. Running past one
|
|
/// without returning to the scheduler would let a single slot
|
|
/// win/lose a reservation across many blocks before any peer
|
|
/// observes it.
|
|
/// - memory barriers (`sync`/`eieio`/`isync`): the guest explicitly
|
|
/// demands a global ordering point here; honour it by ending the
|
|
/// superblock so the scheduler re-interleaves.
|
|
///
|
|
/// Purely a function of the opcode (no guest data), so the yield
|
|
/// decision is deterministic and the schedule reproduces byte-identically.
|
|
/// Note: `sc` (syscall) and traps already `terminates_block`, and
|
|
/// import-thunk / halt-sentinel PCs are handled by the per-block
|
|
/// prologue re-check in the superblock loop — they are not listed here.
|
|
#[inline]
|
|
pub fn is_sync_sensitive(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
Self::lwarx | Self::ldarx | Self::stwcx | Self::stdcx
|
|
| Self::sync | Self::eieio | Self::isync
|
|
)
|
|
}
|
|
|
|
pub fn name(&self) -> &'static str {
|
|
match self {
|
|
Self::Invalid => "invalid",
|
|
_ => {
|
|
// Use debug formatting to get the variant name
|
|
// This is a placeholder - in practice we'd have a lookup table
|
|
"?"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for PpcOpcode {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
std::fmt::Debug::fmt(self, f)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn terminates_block_includes_all_branches() {
|
|
assert!(PpcOpcode::bx.terminates_block());
|
|
assert!(PpcOpcode::bcx.terminates_block());
|
|
assert!(PpcOpcode::bclrx.terminates_block());
|
|
assert!(PpcOpcode::bcctrx.terminates_block());
|
|
}
|
|
|
|
#[test]
|
|
fn terminates_block_includes_sc_and_traps() {
|
|
assert!(PpcOpcode::sc.terminates_block());
|
|
assert!(PpcOpcode::td.terminates_block());
|
|
assert!(PpcOpcode::tdi.terminates_block());
|
|
assert!(PpcOpcode::tw.terminates_block());
|
|
assert!(PpcOpcode::twi.terminates_block());
|
|
}
|
|
|
|
#[test]
|
|
fn terminates_block_includes_invalid() {
|
|
// Decoder failure must end the block — otherwise an unknown
|
|
// opcode would be replayed inside a cached block without going
|
|
// through the per-instruction Unimplemented path.
|
|
assert!(PpcOpcode::Invalid.terminates_block());
|
|
}
|
|
|
|
#[test]
|
|
fn terminates_block_excludes_straight_line_ops() {
|
|
// Common ALU and load/store ops must NOT terminate a block.
|
|
assert!(!PpcOpcode::addi.terminates_block());
|
|
assert!(!PpcOpcode::addis.terminates_block());
|
|
assert!(!PpcOpcode::addx.terminates_block());
|
|
assert!(!PpcOpcode::cmpi.terminates_block());
|
|
assert!(!PpcOpcode::cmp.terminates_block());
|
|
assert!(!PpcOpcode::lwz.terminates_block());
|
|
assert!(!PpcOpcode::stw.terminates_block());
|
|
assert!(!PpcOpcode::lbzx.terminates_block());
|
|
assert!(!PpcOpcode::ori.terminates_block());
|
|
assert!(!PpcOpcode::oris.terminates_block());
|
|
assert!(!PpcOpcode::rlwinmx.terminates_block());
|
|
}
|
|
|
|
#[test]
|
|
fn terminates_block_excludes_msr_and_sync_ops() {
|
|
// Documented decision: synchronizing ops execute as ALU within
|
|
// a block since the interpreter has no async-exception model.
|
|
assert!(!PpcOpcode::mtmsr.terminates_block());
|
|
assert!(!PpcOpcode::mtmsrd.terminates_block());
|
|
assert!(!PpcOpcode::isync.terminates_block());
|
|
assert!(!PpcOpcode::sync.terminates_block());
|
|
assert!(!PpcOpcode::mfmsr.terminates_block());
|
|
}
|
|
}
|