Phase 8 batch 3 — FPU and VMX float test gap closure.
14 new tests:
- Single FPU (187): fadds, fmuls
- Double FPU (208): fmul, fdiv (zero-numerator), fneg, fabs, fmr
- FPU convert/compare (228): fcmpu, fcfid
- VMX float compare (438): vcmpeqfp lane mask
- VMX rounding (439): vrfip, vrfim, vrfiz
- VMX convert (440): vctsxs saturation to INT_MAX/INT_MIN
The VMX VX-form encoding nit (XO is 11 bits at PPC 21-31, host bits 10-0,
with bit 0 the LSB — not bit 1) was caught by initial test failures and
fixed before commit. VC-form (vcmpeqfp) has the same "XO at bit 0" layout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 8 batch 1 — test gap closure for the branch/CR-logical/SPR/MSR/
FPSCR/cache+sync groups.
12 new tests across the affected groups:
- PPCBUG-055 branch: blr, bctr, bcl-LK-on-not-taken
- PPCBUG-070 CR logical: cror, crand, crxor (crclr idiom)
- PPCBUG-067 trap+sc: sc smoke, tw TO=0 never-traps
- PPCBUG-081-085 SPR/MSR/FPSCR moves: mfcr 8-field assembly, mtfsb1/mtfsb0
- PPCBUG-089 cache+sync: sync state-non-mutation smoke
These groups previously had near-zero unit test coverage. New tests lock
in the current ISA-correct behavior; would catch a regression in any of
the dispatch/encoding/result paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P7 of the PPC instruction audit fix application: re-ran the
ppc-manual generator (`python3 ppc-manual/generator/generate_manual.py`)
to regenerate all 350 family pages from current xenia-rs and
xenia-canary source. The 3 audit-cited stale snapshots
(PPCBUG-066/117/145) are now refreshed.
Note: the `ppc-manual/` directory is not versioned in xenia-rs/.git,
so this commit is purely the audit-findings status update + report
section. The regen itself happened in-place outside this repo.
Verification: post-regen grep confirms the old "For now, just trace
and continue" stub is gone from every page, and modern constructs
(trap::evaluate, current reservation_line model) appear correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P6 phase merged at 112202c. Update audit-findings.md status fields
(13 PPCBUGs marked applied) and append the P6 progress section to
audit-report-2026-04-29.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P6 review nit: replace the inline `const VX_ALL_MASK` in the mcrfs arm
with the existing `fpscr::VX_ALL` constant (single source of truth).
Behaviorally identical.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 6 batch 4 — overflow/cleanup verification.
- PPCBUG-022 mulld_ov INT_MIN * -1: the audit-claimed missing edge case
is actually handled by `i64::checked_mul()` (returns None when the
result would be -i64::MIN = i64::MAX+1, which doesn't fit). New
regression tests in overflow.rs confirm: INT_MIN * -1 overflows;
INT_MIN * 1 doesn't; (INT_MIN+1) * -1 = INT_MAX, no overflow.
Audit's claim was incorrect; documented by the new tests.
- PPCBUG-021 (overflow.rs OE checks at bit 63): largely auto-resolved
by P4 batch 6 (16993bb), which switched all 32-bit ABI ops to inline
`true_sum != (result32 as i32) as i128`. Helpers like add_ov_64 are
now only called from 64-bit ABI ops where bit-63 is correct.
- PPCBUG-027 (rlwimix upper-32 zeros): auto-resolved by P4 (rlwimix
now writes via `as u32 as u64` truncation).
- PPCBUG-039 (cntlzdx 32-bit-ABI): wontfix per audit — only matters
if a 32-bit-ABI binary emits cntlzd, which compilers don't.
Remaining low-impact items (PPCBUG-642 ISA-undefined fmt_bcctr decr,
PPCBUG-643/644 SIMM/D-form hex display, PPCBUG-367/368 vupkhpx/vpkpx
channel ordering, PPCBUG-487/495 vsum operand naming, PPCBUG-515/516
lvebx/lvsr documentation, PPCBUG-601 decode_op6 invariant doc) are
left for a P9 or follow-up batch — they're cosmetic/test-coverage
items rather than correctness bugs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 6 batch 3 — SPR/MSR/VSCR semantics.
- PPCBUG-078 mtmsrd L=1: PowerISA requires partial-MSR-write — only
MSR[EE] (u64 bit 15) and MSR[RI] (u64 bit 0) modified, all other
MSR bits preserved. Used by kernel code to toggle external interrupts.
Previously merged with mtmsr (full overwrite), silently corrupting
MSR for any L=1 caller.
- PPCBUG-080 mfvscr: ISA places VSCR in the rightmost word of VD with
bytes 0-11 zeroed. Previously copied the full 128-bit ctx.vscr,
leaking stale upper data to guest. Now zero-extends per canary.
- PPCBUG-068 mcrfs VX summary: when mcrfs clears VX* exception bits,
the VX summary bit at FPSCR[2] must be recomputed (clears if all
contributors are 0; remains 1 otherwise). Previously left stale,
causing subsequent CR-test sequences to misread the FPU state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 6 batch 2 — XER TBC enabling + load/store-multiple cleanups.
- PPCBUG-123/124/161/566 (coupled): XER TBC field was unmodelled —
`ctx.xer()` always returned 0 in bits 0-6, and `ctx.set_xer()`
silently discarded any TBC writes. Result: `lswx` and `stswx` were
permanent no-ops (the `while bytes_left > 0` loop never executed).
Fix: add `pub xer_tbc: u8` to `PpcContext`; wire into `xer()` and
`set_xer()`. Initialize to 0 in `PpcContext::new()`. lswx/stswx
bodies are correct as-is once the infrastructure is wired.
- PPCBUG-125 lmw: PowerISA marks `lmw rT, D(rA)` invalid when rA is
in [rT..31]; canary skips the write to rA to preserve the EA base.
Now matches canary.
- PPCBUG-126/162 lswi/stswi: replaced `instr.rb()` with `instr.nb()`
for the NB field. Both accessors return identical values today
(bits 16-20), but the maintenance hazard from the misnomer is now
removed. A future `rb()` type-system refactor would have broken
lswi/stswi silently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 6 batch 1 — trap/sc semantics.
- PPCBUG-063 trap PC: previously ctx.pc was incremented to CIA+4 BEFORE
StepResult::Trap returned, forcing handlers to .wrapping_sub(4) to
recover the faulting instruction address. Now ctx.pc stays at CIA on
trap, matching SRR0 semantics on real hardware. Critical for any
future SEH/exception-delivery path (e.g. the Sylpheed C++ throw work).
- PPCBUG-065 typed-trap logging: `twi 31, r0, IMM` is the Xbox 360
CRT/kernel typed-trap convention encoding C++ exception class via
SIMM. The trace now logs the SIMM type code when this pattern fires.
Routing the type code via a StepResult payload requires an enum
extension (multiple consumer sites) that's deferred.
- PPCBUG-064 sc LEV logging: `sc 2` is the Xbox 360 hypervisor-call
convention; canary dispatches it to a different handler than `sc 0`.
Now logs a warning when LEV != 0. Routing LEV=2 to a HypervisorCall
variant also requires a StepResult enum extension; deferred.
The two enum-extension follow-ups can land as a structural sub-batch
once a clear consumer (SEH dispatch, hypervisor-call HLE) is in place.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P5 phase merged at d39d0ba. Update audit-findings.md status fields
(21 PPCBUGs marked applied) and append the P5 progress section to
audit-report-2026-04-29.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 5 of the PPC instruction audit fix application: FPU correctness
across the scalar FPU and VMX float arithmetic. ~22 PPCBUGs across
6 sub-sections (5a-5f).
- f6a444b: 5a — round_to_i64 + vrfin round-to-even (PPCBUG-221+227, 432)
- 26b9897: 5b — FMA VXISI + NaN sign preservation (PPCBUG-181/182/183/202/203/205)
- 49bf74f: 5c — FPU XX-on-inexact for conversions (PPCBUG-223/224/225/229/230)
- 538fa5a: 5d — VSCR.NJ subnormal flush for VMX float (PPCBUG-435/436/437)
- 6ba8f83: 5e — fresx canary parity (PPCBUG-184)
- 6fe2cbf: 5f — single-FMA vnmsubfp + vctsxs NaN saturation (PPCBUG-426/427/433)
- 05f2f72: review-fix nit — vrfin uses stdlib round_ties_even
Independent reviewer found no blocking issues; two minor follow-up
items remain open for tracking. The vrfin nit was applied immediately
in 05f2f72.
Three substantive PPCBUGs were explicitly deferred — each requires
substantial helper rework that's better landed as focused sub-batches:
- PPCBUG-201: FPSCR.RN for double arithmetic (MXCSR set/restore wrappers)
- PPCBUG-185: FPSCR.NI flush for scalar FPU (NI bit constant + post-op flush)
- PPCBUG-180/200: XX/FR/FI in update_after_op (pre-vs-post-round comparison)
These remain Status: open in audit-findings.md and will be picked up in
a P5b sub-batch or P9 (test gaps) per planning.
Verification at merge: cargo test --workspace --release reports 498
passed, 0 failed. Acid test deferred to end of all phases per user
direction.
Phase 5 batch 6 (5f): saturation and FMA-rounding fixes.
- PPCBUG-426 vnmsubfp: was `bi - ai * ci` (two rounding steps); now
`-ai.mul_add(ci, -bi)` which is mathematically equivalent (= bi - ai*ci)
but uses a single FMA round per ISA.
- PPCBUG-427 vnmsubfp128: same single-FMA fix.
- PPCBUG-433 vctsxs / vcfpsxws128 NaN saturation: AltiVec ISA saturates
NaN to INT_MIN (0x80000000); xenia returned 0. The vctuxs (unsigned)
NaN→0 is correct per ISA.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 5 batch 5 (5e): minimal-viable fix for the estimate-precision
family. Hardware Xenon `fres` produces a ~12-bit LUT estimate; xenia
and canary both produce a fully IEEE single reciprocal, but canary
pre-quantizes the f64 input to f32 to at least match the input
precision. Now matches canary.
PPCBUG-428..431 (vrefp/vrsqrtefp/vexptefp/vlogefp) already operate on
f32 inputs naturally (no f64 → f32 quantization step needed); the
estimate-precision deviation is purely the output side. Newton-Raphson
convergence is unaffected. Documented in audit-findings.md as
LOW-impact full-fix-requires-LUT.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 5 batch 4 (5d) — partial: VSCR.NJ subnormal flush for VMX float
arithmetic. Xbox 360 always boots with NJ=1, so games expect inputs
and outputs flushed to ±0.
- PPCBUG-435 vaddfp/vaddfp128/vsubfp/vsubfp128/vmulfp128: previously
no flush at all on these opcodes (only vmaddfp family flushed).
Now flushes both inputs and output per Canary's unconditional model.
- PPCBUG-436 vmsum3fp128/vmsum4fp128: per-product intermediates now
flushed individually (was only the final sum).
- PPCBUG-437 vmaddfp/vmaddfp128/vmaddcfp128/vnmsubfp/vnmsubfp128:
outputs now flushed (inputs were already flushed).
PPCBUG-185 (FPSCR.NI flush for scalar FPU) deferred — requires adding
a NI bit constant and post-op flush wrapper across all *sx arms; will
land in a focused sub-batch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 5 batch 3 (5c) — partial: targeted XX-on-inexact fixes for the
float-to-int and double-to-single conversion family. (PPCBUG-180/200,
the broader update_after_op XX/FR/FI rework, deferred to a focused
sub-batch.)
- PPCBUG-225 frspx: set XX when the f64→f32 round produces a different
value (i.e. precision loss). Almost every frsp call is inexact —
previously games polling FPSCR.XX never saw the set bit after a frsp.
- PPCBUG-224 fcfidx: set XX when the i64 input has > 53 significant
bits (precision lost in conversion to f64).
- PPCBUG-229 fctidx/fctidzx: set XX when input is non-integer (fractional
part discarded by the conversion).
- PPCBUG-230 fctiwx/fctiwzx: same shape for word-width conversions.
- PPCBUG-223 verified already correct in current code (fcmpo sets
VXSNAN/VXVC on NaN operands; the audit-cited drift was already fixed).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 5 batch 2 (5b): VXISI / NaN handling for the FMA family.
The 8 FMA opcodes (fmaddx/fmaddsx/fmsubx/fmsubsx/fnmaddx/fnmaddsx/fnmsubx/
fnmsubsx) all share two fix shapes:
1. VXISI on the add/sub step. The previous code passed `a*c` to
check_invalid_add, which has separate rounding from the FMA. In
extreme cases this gives the wrong sign (PPCBUG-202) or wrong infinity
status. Worse, fmsub/fnmadd/fnmsub had NO add-step VXISI check at all
(PPCBUG-181/182/203). The fnmsub pattern is the canonical Newton-
Raphson step — the most common FPU path in Xbox 360 graphics code.
2. NaN sign preservation in fnmadd/fnmsub. ISA Book I §4.3.4 forbids
negation of a NaN FMA result; Rust's unary `-` flips the IEEE-754
sign bit (PPCBUG-183/205).
Fixes:
- fpscr.rs: new helper `check_invalid_fma_add(ctx, a, c, b, sub)` that
derives VXISI from input properties (mathematical-product sign +
b sign) instead of from the lossy `a*c` value. Also covers SNaN.
- interpreter.rs: all 8 FMA arms now use the new helper; fnmadd[s]/
fnmsub[s] gate the negation on `!fma.is_nan()`.
Tests:
- fmsub_inf_minus_inf_sets_vxisi: regression for PPCBUG-203.
- fnmadd_nan_input_preserves_nan_sign: regression for PPCBUG-205.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 5 batch 1 (5a): round-to-int correctness.
PPCBUG-221+227 (coupled): round_to_i64 NearestEven tie-breaking used
`(diff - 0.5).abs() < f64::EPSILON` to detect half-integers, but for
|v| > 2^52 every f64 value is an exact integer (v.trunc() == v), giving
diff == 0. The buggy check fell through to v.round() (round-half-away-
from-zero), giving wrong results for large odd half-integers. Replaced
with a fractional-part-only check that's exact for |v| <= 2^52 and
degenerates to truncation above.
PPCBUG-432: vrfin/vrfin128 used Rust's `f32::round()` which is round-
half-away-from-zero. ISA requires round-to-nearest-even (banker's
rounding). Implemented inline.
PPCBUG-201 (FPSCR.RN for double arithmetic) deferred — requires
MXCSR-set/restore wrappers around 10+ FPU arms; will land in a focused
sub-batch after the remaining 5a-5f fixes.
Tests:
- round_to_i64_nearest_even_on_tie: extended with 0.5, 1.5, -0.5, -1.5.
- round_to_i64_non_tie_cases: 0.4/0.6 (non-tie sanity).
- round_to_i32_nearest_even_on_tie: PPCBUG-227 coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P4 phase merged at d945aea. Update audit-findings.md status fields
(43 PPCBUGs marked applied) and append the P4 progress section to
audit-report-2026-04-29.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 4 of the PPC instruction audit fix application: 32-bit ABI writeback
truncation across the integer ALU. Six commits + one review-fix land
~43 PPCBUG IDs.
- e18a0a4: 4a active poisoning, NOT/SUB family (006/008/018/019/028/029/030/031/033)
- 145a7a4: 4a/4d coupled extsbx+extshx + CR0 (034+035+036+037)
- bf8208e: 4b immediate ALU (001/002/003/004/005/007)
- 82a9bff: 4b mul/div + srawx coupled (009/010+011/041+042+043)
- 20a730d: 4b halfword + lwa loads (095/096/097/098/105)
- 16993bb: 4c latent + 4d CR0 catch-all (012-017/020/023-026/032/044)
- 49103bb: review-fix — subfx/subfcx OE predicate + mulli test rigor
Independent reviewer caught a blocking issue: subfx/subfcx OE handlers
in batch 6 hadn't been migrated to the inline 32-bit overflow predicate
(`true_diff != (result32 as i32) as i128`), still using the legacy
`sum_overflow_64` which gave spurious OV=1 for any legitimate i32::MIN
result. Fixed in 49103bb with two new discriminating regression tests.
Verification at merge: cargo test --workspace --release reports 494
passed, 0 failed. Acid test deferred to end of all phases per user
direction.
The 32-bit ABI invariant — every GPR write zero-extends from a u32
result, every CR0 update views the result as i32 — is now systematically
restored across the integer ALU. Downstream 64-bit unsigned compares
(the addis-incident shape) can no longer be fed polluted upper bits.
Independent reviewer of the P4 branch found two issues:
(1) BLOCKING — subfx and subfcx OE handlers still called the legacy
`overflow::sum_overflow_64(true_diff, result32 as u64)` while batch 6
had migrated all add* sites to the inline `true_sum != (result32 as i32)
as i128` form. The legacy helper compares `true_diff` against
`(result32 as u64) as i64 as i128`, which views any bit-31-set result
as a positive i64 (e.g. result=0x80000000 → +2147483648 in i64). For a
legitimate i32::MIN result with no actual 32-bit overflow, this caused
spurious OV=1.
Concrete repro now caught by `subfo_no_spurious_ov_when_result_has_bit31_set`:
r3=1, r4=0x80000001 → result=0x80000000, true_diff=-2147483648, no OV.
Pre-fix: spurious OV=1.
(2) Minor — `mulli_overflow_wraps_to_32` rubber-stamped: with ra=0x80000000
and imm=2, both pre-fix (`as i64 as u64`) and post-fix (`as u32 as u64`)
write the same value. Replaced with ra=u64::MAX (polluted upper bits) where
pre-fix writes 0xFFFFFFFF_FFFFFFFE and post-fix writes 0x00000000_FFFFFFFE.
Fixes:
- interpreter.rs subfx/subfcx OE: switch to inline 32-bit predicate
matching the rest of batch 6.
- subfo_sets_xer_ov_on_min_minus_one: renamed and updated to test 32-bit
overflow (r4=0x80000000 - 1 = 0x7FFFFFFF, OV=1).
- New: subfo_no_spurious_ov_when_result_has_bit31_set (PPCBUG-017
review-fix regression).
- New: subfco_no_spurious_ov_when_result_has_bit31_set (same for PPCBUG-007).
- mulli_overflow_wraps_to_32: redesigned with polluted upper bits to
actually discriminate pre/post fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 4 batch 6: latent writeback truncation (4c) and CR0 catch-all (4d).
~13 PPCBUGs across all remaining 32-bit ABI ALU sites.
Latent writeback (4c) — the 4a/4b fixes already eliminate the upstream
poisoning, but a defensive truncation here catches any future regression:
- PPCBUG-012 addx, PPCBUG-013 addcx, PPCBUG-014 addex, PPCBUG-015 addzex,
PPCBUG-016 addmex, PPCBUG-017 subfx — all rewritten to compute on u32
operands and write `as u64`. CA computed via 32-bit unsigned compare.
Overflow now uses `true_sum != (result32 as i32) as i128` (32-bit
predicate, since sum_overflow_64 is i64-bounded).
- PPCBUG-032 andx/orx/xorx — CR0 catch-all only (results inherit upper
bits from operands; once those are clean, no truncation needed).
CR0 catch-all (4d) — fix the `update_cr_signed(0, X as i64)` pattern at
every 32-bit-ABI Rc=1 path:
- PPCBUG-020 catch-all: applied to mulhwx, mulhwux, divwux, mullwx (was
already done in batch 4), addx/addcx/addex/addzex/addmex/subfx (now in
4c above), andx/orx/xorx, andix, andisx, slwx, srwx, cntlzwx,
rlwinmx, rlwimix, rlwnmx, mullwx (already), divwx (already),
srawx/srawix (already in batch 4).
- PPCBUG-023 andisx: now correctly classifies bit-31 results as CR0.LT.
- PPCBUG-024 rlwinmx, PPCBUG-025 rlwimix, PPCBUG-026 rlwnmx.
- PPCBUG-044 slwx/srwx: bit-31 result like 0x80000000 now CR0.LT.
64-bit ABI ops (rldicl/rldicr/rldic/rldimi/rldcl/rldcr, sldx/srdx/sradx/
sradix, mulhdx/mulhdux/mulldx, divdx/divdux, cntlzdx) intentionally retain
the 64-bit `as i64` form per ISA — these are 64-bit-mode instructions.
Updated old tests:
- addo_sets_xer_ov_on_signed_overflow_and_stickies_so: i32::MAX + 1 → INT_MIN.
- addx_rc_uses_64bit_compare_not_32bit: renamed to ..._uses_32bit_compare_in_xbox_abi
with assertions flipped to the correct 32-bit ABI behavior.
New tests:
- andisx_sign_bit_set_classifies_lt (PPCBUG-023).
- slwx_high_bit_result_classifies_lt (PPCBUG-044).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 4 batch 5: 5 PPCBUGs in the load family. lha/lhax/lhau/lhaux
sign-extended halfword results to u64 (active poisoning for negative
halfwords); lwa/lwax/lwaux sign-extended u32 results.
- PPCBUG-095/096/097/098 lha[ux]: `as i16 as i64 as u64` →
`as i16 as i32 as u32 as u64`. Sign-extend to i32 then zero-extend.
Common trigger: int16_t struct fields, PCM samples, packed vertex
deltas. Memory 0x8000 was producing 0xFFFFFFFF_FFFF8000.
- PPCBUG-105 lwa/lwax/lwaux: `as i32 as i64 as u64` → `as u64`.
Per-canary the 64-bit-mode form sign-extends, but in 32-bit ABI we
must zero-extend (canary's behavior is rescued by x86 register
zeroing in JIT; pure interpreter has no escape). Memory 0x80000000
was producing 0xFFFFFFFF_80000000.
Tests:
- lha_negative_halfword_zero_extends_upper (PPCBUG-095).
- lhaux_negative_halfword_clean_writeback (PPCBUG-098 + EA update).
- lwa_high_bit_set_zero_extends_upper (PPCBUG-105).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 4 batch 3: 6 PPCBUGs in the same-shape-as-addis (4b) sub-section.
All share the pattern of computing on 64-bit values when the 32-bit ABI
requires u32 arithmetic.
- PPCBUG-001 addi: `li rT, -1` produced 0xFFFFFFFF_FFFFFFFF; now 0x00000000_FFFFFFFF.
- PPCBUG-002 addic: writeback truncated + CA from u32 unsigned compare
matching canary's `AddDidCarry`.
- PPCBUG-003 addicx: same plus CR0 i32 view (regression vs. the frozen
ppc-manual snapshot which had the correct form).
- PPCBUG-004 mulli: 64-bit signed product now truncated to 32 bits.
- PPCBUG-005 subficx: writeback + CA in u32 space; removes the bits-32-63
pollution from sign-extended negative SIMM.
- PPCBUG-007 subfcx: defensive 32-bit truncation of CA compare. Same shape
as the compare that broke addis (0x828F3F98 / 0x828F3F68 case).
Tests:
- addi_li_neg_one_zero_extends_upper (PPCBUG-001).
- addic_carry_uses_32bit_compare (PPCBUG-002).
- mulli_overflow_wraps_to_32 (PPCBUG-004).
- subficx_neg_simm_zero_extends (PPCBUG-005).
- subfcx_addis_incident_case (PPCBUG-007 — exact addis-incident case).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 4 batch 2: extsbx and extshx writeback truncation + CR0 view fix.
Coupled per audit — must land together because the writeback fix would
silently break CR0 sign classification if the CR0 fix didn't ship in
the same commit.
Before:
- extsbx: `as i8 as i64 as u64` — every negative byte poisoned upper
32 bits (active poisoning, not latent). 0x80 → 0xFFFFFFFF_FFFFFF80.
- extshx: same shape for halfwords.
- CR0: `as i64` view — accidentally correct on the buggy 64-bit form
because the high bits matched the byte's sign bit.
After:
- extsbx: `as i8 as i32 as u32 as u64` — sign-extend to i32 then
zero-extend to u64. 0x80 → 0x00000000_FFFFFF80.
- extshx: same for halfwords.
- CR0: `as u32 as i32 as i64` — i32 view, so a result with bit 31 set
is correctly classified as negative under the 32-bit ABI.
Tests:
- extsbx_negative_byte_zero_extends_upper: 0x80 input → 0x00000000_FFFFFF80
with CR0.LT set.
- extshx_negative_halfword_zero_extends_upper: same shape for 0x8000.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 4 batch 1: 9 PPCBUGs in the active-poisoning sub-section. All
follow the pattern `!val` on u64, which unconditionally flips the upper
32 bits and poisons the GPR even with clean inputs — every execution
corrupts the high 32 bits regardless of upstream state.
Sub/neg family:
- PPCBUG-006 negx: `(!ra).wrapping_add(1)` on u64 + neg_ov_64 checks
64-bit INT_MIN. Fix: do arithmetic in u32, OE checks PPC[ra32==0x80000000].
- PPCBUG-008 subfex: same shape as above plus 64-bit unsigned CA compare.
Fix: cast all operands to u32, compute, write `as u64`.
- PPCBUG-018 subfzex: `!ra` on u64. Fix: u32 arithmetic.
- PPCBUG-019 subfmex: `!ra` on u64 + always-true CA edge (`!ra != 0`
was always true for clean ra<0xFFFFFFFF because high bits of !u64
are non-zero). Fix: u32 arithmetic; CA predicate now correct.
Logical NOT family:
- PPCBUG-028 orcx: rs | !rb on u64 → high-bit poison.
- PPCBUG-029 norx: !(rs|rb) — the `not` simplified mnemonic. Hot path,
every `not` corrupted GPR upper 32 bits.
- PPCBUG-030 nandx: !(rs&rb).
- PPCBUG-031 eqvx: !(rs^rb). The common `eqv rA,rA,rA` set-to-all-ones
idiom now produces 0x00000000_FFFFFFFF instead of 0xFFFFFFFF_FFFFFFFF.
- PPCBUG-033 andcx: rs & !rb.
CR0 update at every Rc=1 path now uses `as u32 as i32 as i64` so a result
with bit 31 set gets classified as negative under the 32-bit ABI (was
positive before because upper bits were ones; will be positive in new
truncated form unless we cast through i32). This pre-emptively addresses
PPCBUG-020 for these specific opcodes; the catch-all sweep in batch 6
covers the remaining sites.
Tests:
- nego_sets_ov_only_on_int_min: updated from i64::MIN → 0x80000000 (32-bit).
- test_subfze_carry_only_when_ra_zero_and_ca_one: result expectations
updated from u64::MAX → 0xFFFFFFFF (low 32 bits, upper 32 zero).
- New: neg_clean_input_no_upper_bits (PPCBUG-006 regression).
- New: norx_not_simplified_keeps_upper_bits_clean (PPCBUG-029 regression).
- New: eqvx_self_self_self_sets_low32_to_all_ones (PPCBUG-031 regression).
- New: andcx_bit_clear_keeps_upper_clean (PPCBUG-033 regression).
- New: subfex_clean_inputs_no_upper_bits (PPCBUG-008 regression).
- New: subfmex_ra_max_ca_zero_clears_ca (PPCBUG-019 always-true CA fix).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P3 phase merged at f3ebaba. Update audit-findings.md status fields and
append the P3 progress section to audit-report-2026-04-29.md, including
the new PPCBUG-700 discovery (VMX128 register accessor canary-compliance).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 3 of the PPC instruction audit fix application. Six commits land
six independent (or coupled) PPCBUG fixes:
- cedee3c: PPCBUG-510 stvewx128 16-byte corruption → 4-byte word write
- 52ece4b: PPCBUG-424+425 vmaddfp128/vmaddcfp128 operand swap (VA*VD+VB)
- 3d8e2ce: PPCBUG-053+054 32-bit CTR semantics in bcx/bclrx + mtspr CTR
- d4f6ea7: PPCBUG-640+650 fmt_bc spurious bdnzge/bdzge condition suffix
- 2be25bd: PPCBUG-641+649 sync vs lwsync L-field disambiguation
- 7609dcd: PPCBUG-700 VMX128 register accessors → canary bitfield layout
PPCBUG-700 was a discovery during phase end-to-end review: an independent
reviewer cross-checked our va128/vb128/vd128/vx128r_rc_bit accessors
against canary's `FormatVX128*` bitfield struct (xenia-canary
`ppc_decode_data.h:484-663`) and found the bit positions were wrong on
all four. The audit's line-2958 "confirmed-clean" assessment was based
on a miscount of LSB-first packed C++ bitfields. Real Xbox 360 game code
follows canary's convention, so any production VMX128 instruction with
register VR >= 32 was silently mis-decoded — though no unit test
exercised that path until 52ece4b's operand-swap fix exposed the
inconsistency. Subsumes PPCBUG-422's prescribed Rc-bit position.
Verification at merge: `cargo test --workspace --release` clean across
all crates; targeted vmx128/decoder/disasm-golden tests green. Acid test
(`-n 4B --parallel`) deferred to end-of-all-phases per user direction.
Independent review of P3 batch 2 (52ece4b) found that all three VMX128
register accessors disagreed with canary's FormatVX128/VX128_R bitfield
struct (`xenia-canary/src/xenia/cpu/ppc/ppc_decode_data.h:484-663`). The
audit at line 2958 had marked these "confirmed-clean" but had miscounted
LSB-first bitfield offsets.
Canary's actual layout (LSB-first, GCC/Clang/MSVC on x86):
VA128 = VA128l(5) | VA128h(1)<<5 | VA128H(1)<<6
= PPC[11:15] | PPC[26]<<5 | PPC[21]<<6 (7-bit selector, 3 fields)
VB128 = VB128l(5) | VB128h(2)<<5
= PPC[16:20] | PPC[30:31]<<5 (7-bit selector, 2 fields)
VD128 = VD128l(5) | VD128h(2)<<5
= PPC[6:10] | PPC[28:29]<<5 (7-bit selector, 2 fields)
VX128_R Rc = PPC[25] (host bit 6) not PPC[27] as prior fix had
The buggy convention was internally consistent with hand-crafted test
fixtures (which set bits 29/21/22 to encode the high registers, matching
the buggy accessor). Real Xbox 360 game code follows canary's convention,
so any production VMX128 instruction with VR >= 32 was silently mis-decoded
— but no unit test exercised that path until the va128 fix in 52ece4b
exposed the inconsistency.
Changes:
- decoder.rs: rewrite va128/vb128/vd128/vx128r_rc_bit to canary positions.
Drop the speculative `key4_dt` dot-form dispatch in decode_op6 — canary
has no separate dot-form opcodes for VX128_R compute ops; Rc is a
runtime modifier read by the interpreter via vx128r_rc_bit().
- decoder.rs tests: rewrite vmx128_test_word helper for canary layout;
rename/re-encode vmx128_vd128_*, vmx128_va128_*, vmx128_vb128_* tests.
- interpreter.rs: update encode_vpkd3d128 test helper to encode VD via
canary's VD128h field; tests now pass vd=96 explicitly.
- tests/disasm_goldens.rs: replace the vrlimi128/vsrw128/vpermwi128/
vperm128 hand-encoded raws with canary-compliant encodings; introduce
a shared `encode_vx128` helper.
- tests/golden/vmx128_registers.json: re-encode 9 entries (vperm128,
vsrw128 ×2, vpermwi128, vrlimi128 ×2, vmaddfp128, vmaddcfp128,
vnmsubfp128) to canary-compliant raws preserving the same expected
operand strings.
- audit-findings.md: new PPCBUG-700 entry documenting the discovery and
invalidating the audit's "confirmed-clean" assessment.
Affects all VMX128 binary ops (vaddfp128, vsubfp128, vmulfp128, vand128,
vor128, vxor128, vnor128, vandc128, vsel128, vslo128, vsro128, vperm128,
vsrw128, vmaddfp128, vmaddcfp128, vnmsubfp128, vpkd3d128, vpkshss128,
vpkshus128, vpkswss128, vpkswus128, vpkuhum128, vpkuhus128, vpkuwum128,
vpkuwus128, vmsum3fp128, vmsum4fp128, vrlimi128, vpermwi128 — 30+
opcodes), plus VX128_R compare dot-forms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PPCBUG-641: PpcOpcode::sync emitted "sync" regardless of the L-field at
PPC bit 10. The Xbox 360 acquire barrier (encoding 0x7C2004AC, L=1) is
lwsync, used in every spinlock. The disassembly DB stored every lwsync
as `mnemonic='sync'`, so `SELECT WHERE mnemonic='lwsync'` returned zero
rows regardless of binary content.
PPCBUG-649 (companion): the golden fixture for lwsync had no ext_mnemonic
field, pinning the wrong output and defeating regression detection.
Fix: in disasm.rs, gate on `(instr.raw >> 21) & 1` (PPC bit 10) — when
set, emit the lwsync extended form. Update extended_mnemonics.json
fixture to expect `ext_mnemonic: "lwsync"`.
Note: this is the disassembler-side fix only. The interpreter-side
PPCBUG-088 (lwsync vs sync semantics) is separate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PPCBUG-640: For BO=16 (bdnz: decrement CTR, branch if non-zero, ignore CR)
and BO=18 (bdz: same with branch-if-zero), `fmt_bc` fell through to the
`if decr` block and computed `cond_name_opt` from the don't-care BI=0 /
cond_true=false pair, yielding `Some("ge")`. The output was therefore
`bdnzge` / `bdzge` — a CTR-only branch with a spurious CR-derived suffix.
PPCBUG-650 (companion): the golden fixture pinned the wrong output, so
the regression had no detection signal until now.
`fmt_bclr` already had the correct `if decr && uncond` guard at line 872
producing `bdnzlr` / `bdzlr`. `fmt_bc` lacked the equivalent.
Fix: gate the condition string on `!uncond` inside the `if decr` block.
For BO=16/18 (uncond bit set), the condition suffix is now empty.
Tests: extended_mnemonics.json fixture rows for bdnz/bdz now expect the
correct `ext_mnemonic: "bdnz"` / `"bdz"`.
Impact: every analysis-DB query for `bdnz` loops (common in pixel-shader
and vertex processing) was returning zero rows; matches stored as `bdnzge`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PPCBUG-053: bcx and bclrx tested `ctx.ctr != 0` against the full 64-bit
register, but the Xbox 360 ABI runs CTR as a 32-bit counter (canary
explicitly truncates: `f.Truncate(ctr, INT32_TYPE)`). When upstream 64-bit
GPR pollution flowed through `mtspr CTR, rN`, the upper 32 bits stayed
non-zero forever; bdnz then looped past the intended 32-bit zero point
because the 64-bit comparison still saw the high bits.
PPCBUG-054: `mtspr CTR` writeback wrote the full 64-bit GPR value,
acting as a firewall gap that fed PPCBUG-053. Defensive truncation
prevents CTR from ever acquiring non-zero upper 32 bits independently
of the GPR-pollution source.
Fixes:
- interpreter.rs:849, 879: ctr_ok now uses `(ctx.ctr as u32) != 0`
- interpreter.rs:1523: mtspr CTR writes `val as u32 as u64`
Tests:
- bcx_bdnz_uses_32bit_ctr_compare: bdnz with CTR=0x0000_0001_0000_0001
decrements to 0x0000_0001_0000_0000 and exits (low 32 bits = 0).
- bclrx_uses_32bit_ctr_compare: same coverage for bdnzlr.
- mtspr_ctr_truncates_to_32_bits: gpr=0xFFFF_FFFF_8000_0001 → ctr=0x8000_0001.
Coupled fix per the audit: PPCBUG-053 and PPCBUG-054 land together because
either alone is necessary-but-not-sufficient — the truncation prevents new
pollution, the 32-bit compare protects against any pollution that slipped
in via routes other than mtspr (e.g. mfctr-mtctr roundtrips).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PPCBUG-424: vmaddfp128 computed VA×VB+VD instead of ISA-mandated VA×VD+VB.
PPCBUG-425: vmaddcfp128 computed VD×VB+VA instead of ISA-mandated VA×VD+VB.
Root-cause discovered while writing the operand-order regression tests:
va128() was extracting PPC bits 6-10 (the same field as vd128's low 5 bits),
not PPC bits 11-15 where VA lives in VX128 form. This meant va128() silently
aliased vd128 for any instruction where VA != VD, making the operand swap
invisible in the existing denorm-flush test (which used VA == VD == v2).
Fixes in this commit:
- decoder.rs: va128() now extracts PPC bits 11-15 (host bits 20-16) + bit29.
The vmx128_va128_uses_bit29 test encoding updated to match the correct field.
- interpreter.rs: vmaddfp128 changed from ai.mul_add(bi,di) to ai.mul_add(di,bi)
(VA×VD+VB). vmaddcfp128 changed from di.mul_add(bi,ai) to ai.mul_add(di,bi).
vmaddfp128_flushes_denormal_inputs redesigned with distinct VA/VD/VB registers
(v1/v2/v3) so the flush test is independent of the accessor fix.
New vmaddfp128_operand_order_va_times_vd_plus_vb and
vmaddcfp128_operand_order_va_times_vd_plus_vb tests verify 2×3+10=16.
- disasm_goldens.rs + vmx128_registers.json: vmaddfp128/vmaddcfp128/vnmsubfp128
golden raws updated to properly encode VA at PPC bits 11-15 (new raws:
0x146328D4 / 0x14632914 / 0x14632954). vperm128 / vsrw128 golden operands
updated to reflect correct VA extraction (v4 instead of v3/v0).
Affects all VMX128 binary ops that call va128(): vaddfp128, vsubfp128,
vmulfp128, vmaddfp128, vmaddcfp128, vnmsubfp128, vperm128, vsrw128 etc.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
stvewx128 was aligning EA to 16 bytes and writing all 16 bytes of the
vector, corrupting 12 adjacent bytes on every call. ISA semantics:
word-align EA, extract word lane (EA & 0xF) >> 2, write 4 bytes only.
The non-128 stvewx was already correct; stvewx128 was never updated.
Mirror the stvewx body with instr.vs128() substituted for instr.rs().
The invalidate_for_write call from P1 now covers the correct word-aligned
EA rather than the over-wide 16-byte range.
interpreter.rs: stvewx128 arm (~line 2984)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vpkd3d128 was storing the pack codec output directly into vd128 without
applying the MakePermuteMask permutation that merges the packed scalar(s)
into the previous register value according to pack (slot layout) and shift
(destination lane offset).
PPCBUG-363: vpkd3d128 was missing the post-pack lane-placement step.
PPCBUG-369: vpkd3d128 pack field not extracted; pack=0 still worked
(identity), but pack=1/2/3 always wrote raw out instead of blending.
Fix: extract `pack = uimm & 3` and `shift = instr.vx128_4_z()` from the
VX128_4 IMM and z fields. For pack==0 (identity) store out directly as
before. For pack 1-3, read the existing vd128 value and select 4 u32
words from {prev, out} using the 3×4 static permutation tables from
canary ppc_emit_altivec.cc:2126-2188.
Tables derived from canary MakePermuteMask(r0,l0,…r3,l3):
pack=1 (VPACK_32): out[3] placed at lane (3-shift), prev elsewhere
pack=2 (64-bit): out[2..3] placed at lanes (2-shift)..(3-shift)
pack=3 (64-bit): same as pack=2 except shift=3 → out[2] at lane 3
Tests: vpkd3d128_pack0_legacy_unchanged, vpkd3d128_pack1_shift0_d3d_vertex_pack,
vpkd3d128_pack1_shift3_puts_out3_at_lane0
interpreter.rs: vpkd3d128 arm (~line 3999)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PPCBUG-565: Add vx128_5_sh() to decoder.rs — 4-bit shift at PPC bits
22-25 (host bits 6-9). The correct MSB is at PPC bit 22 (host bit 9).
PPCBUG-361: vsldoi128 was reading the SH MSB from host bit 4 (PPC bit
27, reserved) instead of host bit 9 (PPC bit 22). All shift amounts >= 8
decoded incorrectly (e.g. shift=8 executed as shift=0). Replace the
inline bit-shuffle with instr.vx128_5_sh().
Also fix vx128_p_perm_assembles_correctly test: replace nonexistent
DecodedInstr::from_raw() calls with struct literal construction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PPCBUG-563: Add vx128_4_imm() (PPC bits 11-15) and vx128_4_z() (PPC bits
24-25) accessors to decoder.rs for VX128_4-form instructions.
PPCBUG-315: vrlimi128 was reading z from host bits 16-17 (a subset of IMM)
and mask from host bits 2-5 (a reserved/XO region). Replace with the
correct accessors: z selects which word-lane to start the rotation from
(0-3); IMM is the 5-bit per-lane blend mask.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PPCBUG-562: Add vc_rc_bit() (PPC bit 21) and vx128r_rc_bit() (PPC bit 27)
to decoder.rs. The generic rc_bit() reads bit 0 (PPC bit 31); all vcmp XO
values are even so bit 0 is always 0, making CR6 permanently dead.
PPCBUG-275/276/420/421: Replace rc_bit() with vc_rc_bit() at all 8 pure
VC-form vcmp arms (vcmpequb, vcmpequh, vcmpgtub, vcmpgtsb, vcmpgtuh,
vcmpgtsh, vcmpgtuw, vcmpgtsw) and with the correct per-form accessor at
the 4 combined arms (vcmpeqfp|128, vcmpgefp|128, vcmpgtfp|128,
vcmpequw|128) and vcmpbfp|128.
PPCBUG-422: VX128_R-form 128-variants in combined arms now use
vx128r_rc_bit() instead of vc_rc_bit().
PPCBUG-423/600: Add 5 dot-form key entries to decode_op6 so
vcmp*fp128./vcmpequw128. decode as the correct opcode instead of Invalid.
Uses a 5-bit key (bits22-24 + bit25 + bit27) for dot-forms to avoid
aliasing against the shift/merge group (which sets bit25=1 when bit27=1).
Interpreter uses vx128r_rc_bit() to conditionally update CR6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PPCBUG-561: Add DecodedInstr::mb_md() to decoder.rs — the correct MD-form
6-bit mask-begin reconstruction (MB[4:0] at PPC bits 21-25, MB[5] at PPC
bit 26). The disassembler already had the correct local formula; this
promotes it to a single source of truth on DecodedInstr.
PPCBUG-046: All 6 doubleword-rotate arms (rldicl, rldicr, rldic, rldimi,
rldcl, rldcr) inlined "(instr.mb() << 1) | ((instr.raw >> 1) & 1)" which
reads SH5 (host bit 1) instead of MB5 (host bit 5). For the canonical
"clrldi r3, r4, 32" zero-extend idiom (mb=32 → MB5=1, MB[4:0]=0), the
wrong formula produced mb=0, making the instruction a no-op and leaving
upper 32 bits of the GPR polluted. Replace all 6 sites with instr.mb_md().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PPCBUG-040: decoder.rs sh64() assembled the XS-form shift amount as
(SH[4:0] << 1) | SH[5] instead of (SH[5] << 5) | SH[4:0]. Every
`sradi` with shift N ∈ 1..=62 executed with a completely wrong shift
count (e.g. shift=32 executed as shift=1).
PPCBUG-560: disasm_goldens.rs rldicl() test helper was encoding sh[5:1]
at PPC bits 16-20 and sh[0] at PPC bit 30 — exactly backwards. The wrong
encoder and wrong decoder cancelled out, hiding PPCBUG-040 from tests.
Fix both together so tests validate ISA-correct encodings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements PPCBUG-107 cascade fix: every store opcode in the interpreter
now calls ReservationTable::invalidate_for_write(ea) when a reservation
table is active and at least one thread holds a reservation. This restores
correct lwarx/stwcx. LL/SC semantics under --parallel --reservations-table.
Batches merged:
PPCBUG-107,140-144: stw/stwu/stwx/stwux/stwbrx
PPCBUG-130,150: stb/stbu/stbx/stbux/sth/sthu/sthx/sthux/sthbrx/std/stdu/stdx/stdux/stdbrx
PPCBUG-160,167: stmw/stswi/stswx + all FP stores (stfs/stfd families)
PPCBUG-511-514: 16 VMX stores (stvx/stvxl/stvebx/stvehx/stvewx/stvlx/stvrx families)
PPCBUG-151: stwcx./stdcx. width discriminator (reservation_width: u8 in PpcContext)
PPCBUG-108: debug_assert + doc on legacy single-context reservation path
Review fixes: stswi/stswx two-line guard; dcbz/dcbz128 guards added (missed in audit)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PPCBUG-160 partial: stswi's single invalidate_for_write(ea) only covered
the first cache line; with nb up to 32, the write span can cross a 128-byte
line boundary. Replace with two-call guard:
first_line = ea & !RESERVATION_MASK
last_line = ea.wrapping_add(nb - 1) & !RESERVATION_MASK
invalidate first; if last != first, invalidate last.
PPCBUG-160 partial: stswx had the same single-call gap; nb from XER[0:6]
can be up to 127 bytes. Same two-call guard applied; wrapped in `if nb > 0`
to guard against nb==0 underflow (XER TBC field is 0 when no bytes to store).
dcbz: zeroes 32 bytes at a 32-byte-aligned EA — touches exactly one 128-byte
cache line; add canonical single-call invalidate guard (was entirely missing).
dcbz128: zeroes 128 bytes at a 128-byte-aligned EA — one full reservation
line; add canonical single-call invalidate guard (was entirely missing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds doc comments above lwarx/ldarx/stwcx./stdcx. clarifying that the
legacy per-ctx reservation path is only correct in strict lockstep
(single host thread); under --parallel the M3 scheduler must enable
the cross-thread ReservationTable before spawning a second host thread.
A debug_assert fires in the legacy stwcx./stdcx. branch if a
non-primary HW slot (hw_id != 0) takes that path — surfacing
ReservationTable-disabled misconfiguration early in debug builds.
Note: the primary slot (hw_id==0) racing other parallel slots is
not caught by the assert; that case requires the table to be enabled.
Affected:
PPCBUG-108 legacy per-ctx reservation path cannot invalidate
cross-thread; informational — no behavioral change
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Track lwarx vs ldarx reservation width in PpcContext as a u8 (4 = word,
8 = doubleword, 0 = none). stwcx. requires width==4; stdcx. requires
width==8. Cross-width pairs (lwarx + stdcx., ldarx + stwcx.) now fail
deterministically with CR0.EQ=0 instead of spuriously succeeding.
The width is held per-thread; the cross-thread reservation table keeps
its existing slot encoding because each host thread consults its own
ctx.reservation_width before committing.
Affected:
PPCBUG-151 stwcx./stdcx. shared the same reservation slot without
width discriminator; cross-width commits silently succeeded
Tests: lwarx_then_stdcx_cross_width_fails,
ldarx_then_stwcx_cross_width_fails
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>