diff --git a/crates/xenia-cpu/src/interpreter.rs b/crates/xenia-cpu/src/interpreter.rs index 582c673..85fb181 100644 --- a/crates/xenia-cpu/src/interpreter.rs +++ b/crates/xenia-cpu/src/interpreter.rs @@ -5100,6 +5100,72 @@ mod tests { assert_eq!(r, StepResult::Continue, "non-db16cyc or-self stays Continue"); } + #[test] + fn test_smt_priority_hints_are_nops_not_yields() { + // iterate-2H spin/yield/sync hint-class audit. The PowerPC SMT + // thread-priority hints `or 1,1,1` / `or 2,2,2` / `or 3,3,3` / `or 6,6,6` + // (and the db8cyc family `or 26..30`) are reserved no-op encodings. + // Canary's `InstrEmit_orx` emits `f.Nop()` for EVERY `or rX,rX,rX` + // (RT==RB==RA && !Rc) form EXCEPT the exact db16cyc code 0x7FFFFB78, + // which alone gets `f.DelayExecution()`. So ours must NOT yield on any + // of these — over-yielding would diverge from canary and perturb the + // deterministic schedule. (Audit evidence: none of 1/2/3/6/26..30 even + // appear in Sylpheed's image; only `or 31,31,31` (db16cyc) is used as a + // spin hint. This test locks the no-over-yield invariant regardless.) + for r in [1u32, 2, 3, 6, 26, 27, 28, 29, 30] { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // or rN,rN,rN, Rc=0: 31<<26 | r<<21 | r<<16 | r<<11 | 444<<1 + let raw = (31u32 << 26) | (r << 21) | (r << 16) | (r << 11) | (444 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + ctx.gpr[r as usize] = 0xDEAD_BEEF_F00D_BA11; + let res = step(&mut ctx, &mut mem); + assert_eq!( + ctx.gpr[r as usize], 0xDEAD_BEEF_F00D_BA11, + "or {r},{r},{r} is value-neutral" + ); + assert_eq!(ctx.pc, 4, "or {r},{r},{r} advances PC"); + assert_eq!( + res, + StepResult::Continue, + "priority hint or {r},{r},{r} is a plain no-op (canary Nop), NOT a yield" + ); + } + } + + #[test] + fn test_lwsync_ptesync_eieio_isync_decode_as_benign_noops() { + // Memory/sync barrier class. Canary keys `sync` on XO=598 only, so + // sync (L=0), lwsync (L=1), ptesync (L=2) all map to the same + // `InstrEmit_sync` -> `MemoryBarrier`; `eieio` -> `MemoryBarrier`; + // `isync` -> `Nop`. Under our single-host interpreter every one is a + // value-neutral no-op that advances PC and must DECODE (never trap as + // unknown). This guards the L-field disambiguation and the decode path. + let cases: &[(u32, &str)] = &[ + (0x7C00_04AC, "sync"), // L=0 + (0x7C20_04AC, "lwsync"), // L=1 + (0x7C40_04AC, "ptesync"), // L=2 + (0x7C00_06AC, "eieio"), + (0x4C00_012C, "isync"), + ]; + for &(raw, name) in cases { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + let pre_xer = ctx.xer(); + let pre_fpscr = ctx.fpscr; + let pre_gpr = ctx.gpr; + write_instr(&mut mem, 0x200, raw); + ctx.pc = 0x200; + let res = step(&mut ctx, &mut mem); + assert_eq!(res, StepResult::Continue, "{name} continues"); + assert_eq!(ctx.pc, 0x204, "{name} advances PC (decoded, did not trap)"); + assert_eq!(ctx.xer(), pre_xer, "{name} leaves XER"); + assert_eq!(ctx.fpscr, pre_fpscr, "{name} leaves FPSCR"); + assert_eq!(ctx.gpr, pre_gpr, "{name} leaves GPRs"); + } + } + #[test] fn test_fadd() { let mut ctx = PpcContext::new();