diff --git a/crates/xenia-cpu/src/context.rs b/crates/xenia-cpu/src/context.rs index 26b9733..5826120 100644 --- a/crates/xenia-cpu/src/context.rs +++ b/crates/xenia-cpu/src/context.rs @@ -101,6 +101,12 @@ pub struct PpcContext { pub reserved_line: u32, pub reserved_val: u64, pub has_reservation: bool, + /// PPCBUG-151 — width of the active reservation: 4 = `lwarx` (word), + /// 8 = `ldarx` (doubleword), 0 = no reservation. `stwcx.` requires + /// width==4; `stdcx.` requires width==8. Cross-width pairs fail + /// deterministically with CR0.EQ=0. Cleared alongside `has_reservation` + /// on every `stwcx.`/`stdcx.` exit (success or failure). + pub reservation_width: u8, /// M3.7 — generation stamp returned by [`crate::ReservationTable::reserve`] /// at the most recent `lwarx`/`ldarx`. Paired with `reserved_line`; /// `stwcx.`/`stdcx.` pass this back to `try_commit`. Meaningful only @@ -159,6 +165,7 @@ impl PpcContext { reserved_line: 0, reserved_val: 0, has_reservation: false, + reservation_width: 0, reserved_generation: 0, reservation_table: None, hw_id: 0, diff --git a/crates/xenia-cpu/src/interpreter.rs b/crates/xenia-cpu/src/interpreter.rs index c4694b5..63db6a4 100644 --- a/crates/xenia-cpu/src/interpreter.rs +++ b/crates/xenia-cpu/src/interpreter.rs @@ -1124,6 +1124,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.reserved_line = ea & !RESERVATION_MASK; ctx.reserved_val = val as u64; ctx.has_reservation = true; + ctx.reservation_width = 4; // PPCBUG-151: word reservation if let Some(t) = &ctx.reservation_table { if t.is_enabled() { ctx.reserved_generation = t.reserve(ea, ctx.hw_id); @@ -1140,17 +1141,21 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - .as_ref() .filter(|t| t.is_enabled()) .cloned(); + // PPCBUG-151: stwcx. requires a word (lwarx) reservation; + // a doubleword (ldarx) reservation must not commit here. + let width_ok = ctx.reservation_width == 4; let success = if let Some(t) = &table_route { // Table-routed: success iff the slot still holds our // reservation AND the per-ctx flag agrees (the per-ctx // flag would be cleared by an intervening write or // context switch). ctx.has_reservation + && width_ok && ctx.reserved_line == line && t.try_commit(ea, ctx.reserved_generation, ctx.hw_id) } else { // Legacy per-ctx path (M2 default). - ctx.has_reservation && ctx.reserved_line == line + ctx.has_reservation && width_ok && ctx.reserved_line == line }; if success { mem.write_u32(ea, ctx.gpr[instr.rs()] as u32); @@ -1176,6 +1181,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - } } ctx.has_reservation = false; + ctx.reservation_width = 0; // PPCBUG-151: always clear on exit ctx.pc += 4; } @@ -4282,6 +4288,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.reserved_line = ea & !RESERVATION_MASK; ctx.reserved_val = val; ctx.has_reservation = true; + ctx.reservation_width = 8; // PPCBUG-151: doubleword reservation if let Some(t) = &ctx.reservation_table { if t.is_enabled() { ctx.reserved_generation = t.reserve(ea, ctx.hw_id); @@ -4297,12 +4304,16 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - .as_ref() .filter(|t| t.is_enabled()) .cloned(); + // PPCBUG-151: stdcx. requires a doubleword (ldarx) reservation; + // a word (lwarx) reservation must not commit here. + let width_ok = ctx.reservation_width == 8; let success = if let Some(t) = &table_route { ctx.has_reservation + && width_ok && ctx.reserved_line == line && t.try_commit(ea, ctx.reserved_generation, ctx.hw_id) } else { - ctx.has_reservation && ctx.reserved_line == line + ctx.has_reservation && width_ok && ctx.reserved_line == line }; if success { mem.write_u64(ea, ctx.gpr[instr.rs()]); @@ -4324,6 +4335,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - } } ctx.has_reservation = false; + ctx.reservation_width = 0; // PPCBUG-151: always clear on exit ctx.pc += 4; } PpcOpcode::ldbrx => { @@ -6077,6 +6089,86 @@ mod tests { /// Regression: `lvebx` must preserve the prior contents of the /// destination VR for lanes other than the loaded byte. Previously + // ---------- PPCBUG-151: cross-width reservation pairs must fail ---------- + + /// PPCBUG-151: `lwarx` (width=4) followed by `stdcx.` (requires width=8) + /// must fail with CR0.EQ=0. Memory must remain unchanged. + #[test] + fn lwarx_then_stdcx_cross_width_fails() { + let table = std::sync::Arc::new(crate::ReservationTable::new()); + table.enable(); + + let mut ctx = PpcContext::new(); + ctx.reservation_table = Some(table.clone()); + ctx.hw_id = 0; + let mut mem = TestMem::new(); + + // r4=0x1000 (target addr), r5=0 (index), r6=value to (attempt to) store. + ctx.gpr[4] = 0x1000; + ctx.gpr[5] = 0; + ctx.gpr[6] = 0xDEAD_BEEF_CAFE_BABEu64; + + // Instr 0: lwarx r3, r4, r5 (opcode 31, XO 20, Rc=0) + let lwarx = (31u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (20 << 1); + write_instr(&mut mem, 0, lwarx); + // Instr 1: stdcx. r6, r4, r5 (opcode 31, XO 214, Rc=1) + let stdcx = (31u32 << 26) | (6 << 21) | (4 << 16) | (5 << 11) | (214 << 1) | 1; + write_instr(&mut mem, 4, stdcx); + + // Execute lwarx — must set a word reservation (width=4). + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert!(ctx.has_reservation, "lwarx must set has_reservation"); + assert_eq!(ctx.reservation_width, 4, "lwarx must set reservation_width=4"); + + // Execute stdcx. — width mismatch (needs 8, got 4); must fail. + step(&mut ctx, &mut mem); + assert!(!ctx.cr[0].eq, "stdcx. must fail when reservation was set by lwarx (cross-width)"); + // Memory at 0x1000-0x1007 must be unchanged (still zero). + assert_eq!(mem.read_u64(0x1000), 0, "stdcx. must not write on cross-width failure"); + // Width must be cleared on exit. + assert_eq!(ctx.reservation_width, 0, "stdcx. must clear reservation_width on exit"); + } + + /// PPCBUG-151: `ldarx` (width=8) followed by `stwcx.` (requires width=4) + /// must fail with CR0.EQ=0. Memory must remain unchanged. + #[test] + fn ldarx_then_stwcx_cross_width_fails() { + let table = std::sync::Arc::new(crate::ReservationTable::new()); + table.enable(); + + let mut ctx = PpcContext::new(); + ctx.reservation_table = Some(table.clone()); + ctx.hw_id = 0; + let mut mem = TestMem::new(); + + // r4=0x1000 (target addr), r5=0 (index), r6=value to (attempt to) store. + ctx.gpr[4] = 0x1000; + ctx.gpr[5] = 0; + ctx.gpr[6] = 0xCCCC_CCCCu64; + + // Instr 0: ldarx r3, r4, r5 (opcode 31, XO 84, Rc=0) + let ldarx = (31u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (84 << 1); + write_instr(&mut mem, 0, ldarx); + // Instr 1: stwcx. r6, r4, r5 (opcode 31, XO 150, Rc=1) + let stwcx = (31u32 << 26) | (6 << 21) | (4 << 16) | (5 << 11) | (150 << 1) | 1; + write_instr(&mut mem, 4, stwcx); + + // Execute ldarx — must set a doubleword reservation (width=8). + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert!(ctx.has_reservation, "ldarx must set has_reservation"); + assert_eq!(ctx.reservation_width, 8, "ldarx must set reservation_width=8"); + + // Execute stwcx. — width mismatch (needs 4, got 8); must fail. + step(&mut ctx, &mut mem); + assert!(!ctx.cr[0].eq, "stwcx. must fail when reservation was set by ldarx (cross-width)"); + // Memory at 0x1000 must be unchanged (still zero). + assert_eq!(mem.read_u32(0x1000), 0, "stwcx. must not write on cross-width failure"); + // Width must be cleared on exit. + assert_eq!(ctx.reservation_width, 0, "stwcx. must clear reservation_width on exit"); + } + /// the handler started from a zeroed buffer. #[test] fn test_lvebx_preserves_other_lanes() {