diff --git a/tools/ppc-manual/alu/addcx.md b/tools/ppc-manual/alu/addcx.md index 9dc3605c..151200d1 100644 --- a/tools/ppc-manual/alu/addcx.md +++ b/tools/ppc-manual/alu/addcx.md @@ -121,9 +121,9 @@ int InstrEmit_addcx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Carry-out is mandatory.** `XER[CA]` is updated unconditionally — `addcx` exists *to* produce the carry. It seeds a multi-word add chain that continues with [`addex`](addex.md) for middle words and [`addzex`](addzex.md)/[`addmex`](addmex.md) for the final word. -- **Carry detection by overflow comparison.** Xenia computes `CA = (result < RA)` — the standard unsigned-add overflow test. Equivalent to `CA = (RA + RB) >> 64` mathematically. This is correct for the 64-bit operand width that the Xenon implements; the spec also allows a 32-bit width selected by the implementation but the 970/Xenon use 64-bit add throughout. -- **No trap on signed overflow.** `addco`/`addco.` only set `XER[OV]` and sticky `XER[SO]`; they do not raise an exception. Xenia-rs leaves the `OE` branch as a `// TODO` (see [`addx`](addx.md) for the same gap). -- **64-bit CR update on Xenon, 32-bit in xenia-rs.** The `Rc=1` CR0 compare reads `result as i32 as i64` in [`interpreter.rs:97`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs); spec demands the full 64-bit signed compare. Flag this as a xenia-rs quirk if you need bit-exact behaviour. +- **Carry is computed on the low 32 bits in Canary.** `AddDidCarry` truncates both operands to `INT32` and tests `RB > ~RA`, so `CA` is the carry out of the low word rather than out of the full 64-bit add. A translator matching Canary derives `CA` from the low 32 bits; the 64-bit PowerPC definition takes the carry out of the most-significant bit. +- **No trap on signed overflow.** `addco`/`addco.` only set `XER[OV]` and sticky `XER[SO]`; they do not raise an exception. Canary does not implement the `OE` path at all — the branch is `XEINSTRNOTIMPLEMENTED()` — and on that path it does not store `CA` either (see [`addx`](addx.md)). +- **64-bit CR update on Xenon, 32-bit in Canary.** `f.UpdateCR(0, v)` truncates `v` to `INT32` before the signed compare with zero; the spec compares the full 64-bit result. Flag this if you need bit-exact 64-bit behaviour. - **`XER[SO]` is sticky** — only `mcrxr` clears it. The `Rc=1` form folds it into `CR0[SO]`. - **Operand aliasing is legal**, just like [`addx`](addx.md). `addc r3, r3, r3` simply doubles `r3` and records whether the result wrapped. diff --git a/tools/ppc-manual/alu/addex.md b/tools/ppc-manual/alu/addex.md index 4ce5262c..958aaa92 100644 --- a/tools/ppc-manual/alu/addex.md +++ b/tools/ppc-manual/alu/addex.md @@ -122,9 +122,9 @@ int InstrEmit_addex(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Carry-in is consumed and carry-out is produced.** `addex` is the middle link of a multi-word add chain seeded by [`addcx`](addcx.md): `RT ← RA + RB + XER[CA]`, then `XER[CA] ← carry_out`. -- **Carry-out detection handles both edges.** Xenia checks `result < ra OR (ca != 0 && result == ra)` — that second clause covers the case where adding the carry-in alone causes the result to *exactly equal* `RA` (i.e. `RB == ~0 && CA == 1`), which still constitutes overflow. The naive `result < ra` test misses it. -- **No trap on signed overflow.** `addeo`/`addeo.` only update `XER[OV]` and sticky `XER[SO]`; xenia-rs leaves the `OE` branch unimplemented. -- **64-bit CR update on Xenon, 32-bit in xenia-rs.** The `Rc=1` arm uses `result as i32 as i64`. For multi-word adds whose final word is the high 32 bits of a 64-bit value, this distinction matters; see [`addx`](addx.md). +- **Carry-out detection handles both edges.** Canary's `AddWithCarryDidCarry` (after PearPC) sets `CA` if `(a + b) > 63`. Unsigned carry is *not* tracked — use [`addcx`](addcx.md) when you need `XER[CA]`. - **`XER[SO]` is sticky.** Once set, it remains set until cleared by `mcrxr`. The `.` record forms copy it into `CR0[SO]`. -- **64-bit CR update on Xenon.** The Xbox 360 Xenon CPU is 64-bit, so `add.` compares the full 64-bit result against zero. **Xenia-rs presently truncates to 32 bits** before the CR update (`result as i32 as i64` in [`interpreter.rs:95`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). If your translator must match xenia bit-for-bit, emit a 32-bit compare; if it must be spec-correct, emit a 64-bit compare. Most Xbox 360 object code works either way because results that overflow 32 bits are rare outside of explicit 64-bit math. -- **OE overflow detection not emulated in xenia-rs.** The `addo` / `addo.` branch in `interpreter.rs` is a `TODO` stub. A faithful translator should still emit the overflow check — titles rarely observe `XER[OV]`, but it's occasionally used by profiling / sanity-checking code paths. +- **64-bit CR update on Xenon.** The Xbox 360 Xenon CPU is 64-bit, so `add.` compares the full 64-bit result against zero. **Canary truncates to 32 bits** before the CR update (`f.UpdateCR(0, v)` compares `Truncate(v, INT32)` with zero). If your translator must match Canary bit-for-bit, emit a 32-bit compare; if it must be spec-correct, emit a 64-bit compare. Most Xbox 360 object code works either way because results that overflow 32 bits are rare outside of explicit 64-bit math. +- **OE overflow detection is not implemented in Canary.** The `addo` / `addo.` branch is `XEINSTRNOTIMPLEMENTED()`. A faithful translator should still emit the overflow check — titles rarely observe `XER[OV]`, but it's occasionally used by profiling / sanity-checking code paths. - **Operand aliasing.** `add r3, r3, r3`, `add r3, r3, r4`, `add r3, r4, r3` are all legal. The addition reads both source operands before writing `RT`. - **No immediate form.** For `RT = RA + imm` use [`addi`](addi.md) / [`addis`](addis.md). Those are distinct opcodes, not a flag on `add`. diff --git a/tools/ppc-manual/alu/addzex.md b/tools/ppc-manual/alu/addzex.md index 53adcb3a..03f5303a 100644 --- a/tools/ppc-manual/alu/addzex.md +++ b/tools/ppc-manual/alu/addzex.md @@ -123,8 +123,8 @@ int InstrEmit_addzex(PPCHIRBuilder& f, const InstrData& i) { - **No `RB` field used.** Like [`addmex`](addmex.md), this XO-form instruction ignores the `RB` slot. Assemblers emit zero there. - **Operation is `RA + 0 + CA` ≡ `RA + CA`.** Used to terminate the *high word* of a multi-word add chain seeded by [`addcx`](addcx.md). After the low-word `addc` produces the carry, all middle words use [`addex`](addex.md), and the final word that has no register operand uses `addze`. - **Carry-out is the simple unsigned overflow test** `result < ra` — same predicate as [`addcx`](addcx.md). `CA' = 1` only if `RA == ~0 && CA == 1`. -- **`OE=1` not implemented in xenia-rs.** The interpreter has no overflow branch at all; spec asks for the standard signed-overflow detect. -- **64-bit CR update on Xenon, 32-bit in xenia-rs** (truncation in [`interpreter.rs:128`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) — see [`addx`](addx.md) for context). +- **`OE=1` not implemented in Canary.** The branch is `XEINSTRNOTIMPLEMENTED()` and does not store `CA`; spec asks for the standard signed-overflow detect. +- **64-bit CR update on Xenon, 32-bit in Canary** (`f.UpdateCR(0, v)` — see [`addx`](addx.md) for context). - **Common idiom: extracting a carry as a 0/1.** `addze rT, 0` (or `addze rT, rN` where `rN == 0`) materialises `XER[CA]` into `rT` as a plain integer. ## Related Instructions diff --git a/tools/ppc-manual/alu/andcx.md b/tools/ppc-manual/alu/andcx.md index 09b49955..9955c227 100644 --- a/tools/ppc-manual/alu/andcx.md +++ b/tools/ppc-manual/alu/andcx.md @@ -109,8 +109,8 @@ int InstrEmit_andcx(PPCHIRBuilder& f, const InstrData& i) { - **Common idiom: `andc r3, r3, r3`** zeroes `r3` (every bit ANDed with its own complement). Cheaper-looking than `xor r3, r3, r3` on some pipelines but functionally identical; the assembler often prefers the `xor` idiom. - **Operand convention is the X-form one** (`RA` is the destination, `RS` and `RB` are sources). Same gotcha as [`andx`](andx.md). - **No `OE`/`XER` side effects.** Only `CR0` is updated when `Rc=1`. -- **64-bit operation** on Xenon; the AND is computed across all 64 bits of `RS` and `~RB`. Xenia-rs uses Rust's bitwise `!` on `u64`, which is the correct full-width complement. -- **64-bit CR update on Xenon, 32-bit in xenia-rs.** [`interpreter.rs:352`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) — same truncation pattern. +- **64-bit operation** on Xenon; the AND is computed across all 64 bits of `RS` and `~RB`. Canary emits a single full-width `f.AndNot(RS, RB)`. +- **64-bit CR update on Xenon, 32-bit in Canary** — `f.UpdateCR(0, ra)`, the same `INT32` truncation. ## Related Instructions diff --git a/tools/ppc-manual/alu/andisx.md b/tools/ppc-manual/alu/andisx.md index 1dd8935f..5789bd2b 100644 --- a/tools/ppc-manual/alu/andisx.md +++ b/tools/ppc-manual/alu/andisx.md @@ -111,7 +111,7 @@ int InstrEmit_andisx(PPCHIRBuilder& f, const InstrData& i) { - **Together with `andi.` covers the entire low 32 bits.** Any 32-bit mask can be applied with `andis. + andi.` (two instructions). Larger masks need `rlwinm` or a constructed register operand to [`andx`](andx.md). - **High 32 bits of result are always zero.** Because the immediate is at bits 32–47, no information from `RS[0:31]` survives. Useful as a quick "extract bits 32–47, zero the rest" primitive. - **CR0 update is unconditional** and uses the standard signed-compare-to-zero semantics with `XER[SO]` folded into `SO`. -- **64-bit CR update on Xenon, 32-bit in xenia-rs.** The `result as i32 as i64` truncation in [`interpreter.rs:326`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) is harmless: the result is bounded by `0x00000000_FFFF0000`, which fits the 32-bit window exactly. +- **64-bit CR update on Xenon, 32-bit in Canary — and here it matters.** The result can reach `0x00000000_FFFF0000`: positive as a 64-bit value, but negative once Canary's `f.UpdateCR(0, v)` truncates it to `INT32`. With bit 32 of the result set (e.g. `0x80000000`), the spec's `CR0` is `GT` while Canary's is `LT`. ## Related Instructions diff --git a/tools/ppc-manual/alu/andix.md b/tools/ppc-manual/alu/andix.md index 4a31d28f..f81d1fcf 100644 --- a/tools/ppc-manual/alu/andix.md +++ b/tools/ppc-manual/alu/andix.md @@ -110,7 +110,7 @@ int InstrEmit_andix(PPCHIRBuilder& f, const InstrData& i) { - **Cannot mask the high half of a register in one instruction.** The immediate covers bits 48–63 only; for higher bits use [`andisx`](andisx.md) (covers bits 32–47) or compose with `rlwinm`/`rldicl`. - **CR0 update is unconditional.** This is part of the encoding, not a flag — the primary opcode (28) *is* `andi.`. - **Common idiom: `andi. r0, rN, mask`** to test bits without disturbing the source — but note `r0` is overwritten and `CR0` is set. If you only need the CR result, prefer `extrwi`/`rlwinm.` for arbitrary masks. -- **64-bit CR update on Xenon, 32-bit in xenia-rs.** Since the AND result has zeros in bits 0–47, the low-32 truncation in [`interpreter.rs:321`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) is harmless here — the result fits in 16 bits, so spec and xenia agree. +- **64-bit CR update on Xenon, 32-bit in Canary.** Since the AND result has zeros in bits 0–47, Canary's `INT32` truncation is harmless here — the result fits in 16 bits, so spec and Canary agree. ## Related Instructions diff --git a/tools/ppc-manual/alu/andx.md b/tools/ppc-manual/alu/andx.md index 0196d4ed..20f2894e 100644 --- a/tools/ppc-manual/alu/andx.md +++ b/tools/ppc-manual/alu/andx.md @@ -108,7 +108,7 @@ int InstrEmit_andx(PPCHIRBuilder& f, const InstrData& i) { - **Operand convention is reversed.** Unlike the arithmetic XO-form (`add RT, RA, RB`), the logical X-form writes `RA` and reads `RS`/`RB`: `and RA, RS, RB`. The destination is the **second** operand encoded. This convention applies to the entire and/or/xor family; mixing them up is a frequent disassembly error. - **No `OE`, no `XER[CA]`, no `XER[OV]`.** Logical operations never affect XER. Only `Rc=1` updates `CR0` (signed compare against zero, with `SO ← XER[SO]`). - **64-bit AND on Xenon.** Both inputs are 64-bit GPRs; the result is the bitwise AND of all 64 bits. -- **64-bit CR update on Xenon, 32-bit in xenia-rs.** The interpreter's `Rc=1` path in [`interpreter.rs:347`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) compares `result as i32 as i64`. For an AND whose high 32 bits are non-zero but low 32 bits are zero (e.g. `r3 = 0x1_0000_0000`, `and. r4, r3, r3`), spec sets CR0 to GT but xenia would set EQ. Flag this if reproducing CR-sensitive behaviour. +- **64-bit CR update on Xenon, 32-bit in Canary.** `f.UpdateCR(0, v)` compares the low 32 bits. For an AND whose high 32 bits are non-zero but low 32 bits are zero (e.g. `r3 = 0x1_0000_0000`, `and. r4, r3, r3`), spec sets CR0 to `GT` but Canary sets `EQ`. Flag this if reproducing CR-sensitive behaviour. - **Operand aliasing.** `and RA, RA, RA` is a no-op except for the optional CR0 update — this is the canonical "test register against zero" pattern when no `cmpwi` is desired (though `cmpwi` is more typical). - **No simplified mnemonic for AND-immediate.** Use [`andix`](andix.md) (`andi.`) or [`andisx`](andisx.md) (`andis.`) for immediate operands; both are *always* record forms (no plain `andi`). diff --git a/tools/ppc-manual/alu/cmp.md b/tools/ppc-manual/alu/cmp.md index 5e193317..46152d88 100644 --- a/tools/ppc-manual/alu/cmp.md +++ b/tools/ppc-manual/alu/cmp.md @@ -143,7 +143,7 @@ CR[BF] <- { LT: a s b, EQ: a = b, SO: XER[SO] } ; signed - **SO is always copied from `XER[SO]`.** This makes overflow observable across arithmetic/compare sequences: an `addo.` followed by `beq` can branch on the record-form flag while `bso` can inspect the sticky overflow. - **`cr0` is the default for record-form ALU**; by convention assemblers and generators reserve `cr0` for the chain of `Rc=1` instructions and use `cr1..cr7` (or `cmp` to an explicit field) for standalone compares. Don't assume `cmp` writes `cr0` unless the `BF` operand says so. - **No register is written** beyond the 4-bit CR field. `cmp` has no `Rc` or `OE` bit. -- **Xenia-rs quirk.** The interpreter recomputes `EQ` after the signed compare to guard against a subtract-cancellation edge case; this is a defensive belt-and-braces against the 32-bit narrowing path. Functionally equivalent to the spec. +- **Canary is one direct compare.** `L` selects either the full 64-bit operands or both truncated to `INT32`, then `f.UpdateCR(BF, lhs, rhs)` sets `LT`/`GT`/`EQ` from a single signed comparison. ⚠️ It never writes the field's `SO` bit — spec copies `XER[SO]` into it — so Canary leaves `SO` holding whatever was there before. ## Related Instructions diff --git a/tools/ppc-manual/alu/cmpi.md b/tools/ppc-manual/alu/cmpi.md index c16aa5cf..01ff14ff 100644 --- a/tools/ppc-manual/alu/cmpi.md +++ b/tools/ppc-manual/alu/cmpi.md @@ -127,7 +127,7 @@ int InstrEmit_cmpi(PPCHIRBuilder& f, const InstrData& i) { - **Simplified mnemonics dominate disassembly.** `cmpwi crN, RA, SIMM` ≡ `cmpi crN, 0, RA, SIMM` and `cmpdi crN, RA, SIMM` ≡ `cmpi crN, 1, RA, SIMM`. The default CR field is `cr0` if omitted. - **`BF` is a CR field (0–7), not a bit.** Same convention as [`cmp`](cmp.md). Distinct standalone compares should target `cr1..cr7` to avoid clobbering the implicit `cr0` chain set up by `Rc=1` arithmetic. - **SO is copied from `XER[SO]`.** This makes overflow observable downstream of an `addo.` / `mulo.` etc. via `bso`/`bns`. -- **Xenia-rs quirk.** The interpreter recomputes `EQ` after the signed subtract, defending against the same 32-bit narrowing edge case noted in [`cmp`](cmp.md). Functionally equivalent to spec. +- **Canary is one direct compare.** As in [`cmp`](cmp.md): the operand and `EXTS(SI)` are compared at 64 or 32 bits by `L`, via `f.UpdateCR(BF, lhs, rhs)`, which does not write the field's `SO` bit (spec copies `XER[SO]`). - **No register written** other than the 4-bit CR field — there is no `Rc` or `OE` bit. ## Related Instructions diff --git a/tools/ppc-manual/alu/cmpl.md b/tools/ppc-manual/alu/cmpl.md index 1d411631..aec4690b 100644 --- a/tools/ppc-manual/alu/cmpl.md +++ b/tools/ppc-manual/alu/cmpl.md @@ -126,7 +126,7 @@ int InstrEmit_cmpl(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Unsigned compare.** Treats both operands as unsigned magnitudes. The simplified mnemonics are `cmplw` (`L=0`) and `cmpld` (`L=1`). -- **`L = 0`: 32-bit operands.** Xenia narrows both registers via `as u32 as u64` so the high 32 bits of `RA`/`RB` are ignored — this matches spec `(RA)[32:63]` semantics. Most Xbox 360 code uses this mode. +- **`L = 0`: 32-bit operands.** Canary truncates both registers to 32 bits before the unsigned compare, so the high 32 bits of `RA`/`RB` are ignored — this matches spec `(RA)[32:63]` semantics. Most Xbox 360 code uses this mode. - **`L = 1`: full 64-bit unsigned compare.** Used in 64-bit pointer arithmetic; rare in game code but appears in kernel-side helpers. - **SO is copied from `XER[SO]`.** `cmpl` does not clear or set sticky overflow; it just exposes the current `SO` in the destination CR field's `SO` slot. - **`BF` is a CR field 0–7.** Same convention as [`cmp`](cmp.md). Two consecutive `cmpl` instructions with the same `BF` simply overwrite the previous result. diff --git a/tools/ppc-manual/alu/cntlzdx.md b/tools/ppc-manual/alu/cntlzdx.md index 002107e8..8c9f4f8b 100644 --- a/tools/ppc-manual/alu/cntlzdx.md +++ b/tools/ppc-manual/alu/cntlzdx.md @@ -114,7 +114,7 @@ int InstrEmit_cntlzdx(PPCHIRBuilder& f, const InstrData& i) { - **Counts across the full 64 bits.** Use [`cntlzwx`](cntlzwx.md) when you only want to count the low 32 bits. - **Useful as `floor(log2(x)) = 63 − cntlzd(x)`** for nonzero `x`. Frequently used in fast normalization, priority encoders, and bit-vector operations. - **`RB` field unused.** This is X-form but only `RS` is read; `RB` is a placeholder slot. -- **`Rc=1` quirk.** `update_cr_signed(0, RA as i64)` is correct in xenia-rs because the result fits in 7 bits and is non-negative. The CR0 result will always be `EQ` (when `RS != 0` and `RA != 0`? — actually `EQ` only when `RS[0] == 1`, i.e. `RA == 0`) or `GT` (when `RS != 0` so `RA > 0`); never `LT`. `EQ` corresponds to "high bit set in `RS`", a useful one-instruction sign test for negative-as-signed values. +- **`Rc=1` CR0 is never `LT`.** The count is 0–64, so Canary's `INT32`-truncating `f.UpdateCR(0, v)` loses nothing: `EQ` exactly when `RS[0] == 1` (count 0), otherwise `GT`. `EQ` therefore means "high bit set in `RS`" — a one-instruction sign test for negative-as-signed values. - **No `XER` side effects.** Counts neither overflow nor carry. ## Related Instructions diff --git a/tools/ppc-manual/alu/cntlzwx.md b/tools/ppc-manual/alu/cntlzwx.md index 88828c79..2783ecfd 100644 --- a/tools/ppc-manual/alu/cntlzwx.md +++ b/tools/ppc-manual/alu/cntlzwx.md @@ -114,7 +114,7 @@ int InstrEmit_cntlzwx(PPCHIRBuilder& f, const InstrData& i) { - **`RA = 32` when the low 32 bits are zero**, regardless of the high 32 bits. A common pitfall: `cntlzw` after computing a 64-bit value can give a counter-intuitive result when the leading 1-bit lives in the high half. - **`RA = 0` when bit 32 (the sign bit of the low word) is set.** This makes `cntlzw RA, RS; cmpwi RA, 0` a one-instruction-pair "is the low half negative" test, though `srawi RA, RS, 31` is more idiomatic. - **High 32 bits of the result are zero.** `RA[0:31] = 0`, `RA[32:63] = count`. -- **`Rc=1` CR0 update is small-positive-only.** Result fits in 6 bits; xenia's `as i32 as i64` truncation in [`interpreter.rs:404`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) is harmless. CR0 will be `EQ` only when `RS[32]` (sign bit of low word) is 1, otherwise `GT`. +- **`Rc=1` CR0 update is small-positive-only.** The result fits in 6 bits, so Canary's `INT32` truncation in `f.UpdateCR(0, v)` is harmless. CR0 is `EQ` only when `RS[32]` (sign bit of the low word) is 1, otherwise `GT`. - **Useful for `floor(log2)` of a 32-bit value.** `31 - cntlzw(x)` for nonzero `x`. ## Related Instructions diff --git a/tools/ppc-manual/alu/divdux.md b/tools/ppc-manual/alu/divdux.md index c0f33ed7..de6c93a1 100644 --- a/tools/ppc-manual/alu/divdux.md +++ b/tools/ppc-manual/alu/divdux.md @@ -124,10 +124,10 @@ int InstrEmit_divdux(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Single undefined case.** Division by zero (`RB == 0`). There is no `INT_MIN/−1` overflow because both operands are unsigned. Xenia-rs returns 0 for the divide-by-zero case ([`interpreter.rs:306`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)); spec leaves `RT` boundedly undefined. +- **Single undefined case.** Division by zero (`RB == 0`). There is no `INT_MIN/−1` overflow because both operands are unsigned. Canary's emitter does not check for zero (a `TODO`); on the x64 backend `DIV` pre-zeroes the result and skips the divide, so `RT = 0`. Spec leaves `RT` boundedly undefined. - **No trap on Xenon.** As with [`divdx`](divdx.md), the processor does not raise an exception; consuming code must guard `RB` first (typically `cmpdi rb, 0; beq skip`). -- **`OE=1` should set `XER[OV]`** on `RB == 0`; xenia-rs ignores `OE` here. -- **`Rc=1` CR0 update is correctly 64-bit.** [`interpreter.rs:311`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) uses `as i64` directly, so the CR0 sign comparison reflects the full 64-bit unsigned quotient cast to signed. For very large unsigned quotients (`> INT64_MAX`) this CR0 will report `LT` even though the unsigned interpretation is positive — a rare but real source of CR-misuse bugs. +- **`OE=1` should set `XER[OV]`** on `RB == 0`; Canary's `OE` branch is `XEINSTRNOTIMPLEMENTED()`. +- **`Rc=1` CR0 update is 32-bit in Canary.** `f.UpdateCR(0, v)` compares the low 32 bits of the 64-bit unsigned quotient as a signed value: a quotient with bit 32 set reads `LT`, and one whose low word is zero reads `EQ` even if its high word is not. Spec compares the full 64-bit value. - **Slow.** Same ~70-cycle non-pipelined cost as the signed variant; consider reciprocal multiply for hot loops. - **Truncating quotient.** Same C-style toward-zero rounding (trivially equal to floor for unsigned). diff --git a/tools/ppc-manual/alu/divdx.md b/tools/ppc-manual/alu/divdx.md index 8f37fb1d..556fd58b 100644 --- a/tools/ppc-manual/alu/divdx.md +++ b/tools/ppc-manual/alu/divdx.md @@ -124,10 +124,10 @@ int InstrEmit_divdx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Two undefined-behaviour cases.** Division by zero (`RB == 0`) and signed-min divided by negative-one (`RA == INT64_MIN && RB == -1`, which would mathematically produce `2^63`, unrepresentable in `i64`). PowerISA leaves `RT` *boundedly undefined* in both cases; **xenia-rs returns 0** ([`interpreter.rs:293`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). Matching this behaviour bit-for-bit is a defacto-spec on Xenon. +- **Two undefined-behaviour cases.** Division by zero (`RB == 0`) and signed-min divided by negative-one (`RA == INT64_MIN && RB == -1`, which would mathematically produce `2^63`, unrepresentable in `i64`). PowerISA leaves `RT` *boundedly undefined* in both cases. Canary's emitter checks neither; its x64 backend skips the divide in both and yields `RT = 0`, so a translator matching Canary returns 0. - **No exception raised.** Xenon does not trap on either undefined case; the consuming code is expected to have validated `RB` first, e.g. with `cmpdi`/`bne`. If you need a trap, follow the divide with [`tw`](../control/tw.md)/`twi` (these live outside the ALU page set). -- **`OE=1` should set `XER[OV]`** for both undefined cases plus any operand triggering overflow; xenia-rs does not implement the `OE` branch. -- **`Rc=1` CR0 update is correctly 64-bit here.** Unlike most ALU pages, [`interpreter.rs:298`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) uses `as i64` (no `as i32` truncation) — divide is one of the few xenia-rs instructions that already matches Xenon spec for the CR0 width. +- **`OE=1` should set `XER[OV]`** for both undefined cases plus any operand triggering overflow; Canary's `OE` branch is `XEINSTRNOTIMPLEMENTED()`. +- **`Rc=1` CR0 update is 32-bit in Canary, as elsewhere.** `f.UpdateCR(0, v)` truncates the 64-bit quotient to `INT32` before the compare; spec compares all 64 bits. - **Latency.** Integer divide is the slowest ALU instruction on Xenon — 70+ cycles, non-pipelined. Hot inner loops avoid it via reciprocal-multiply or shift; expect to see `mulhwu`-based reciprocals in optimised disassembly. - **Rounds toward zero.** The signed quotient truncates toward zero, matching C99/C++11 `/` semantics. Use [`mulldx`](mulldx.md) and a subtract to recover the remainder; there is no `divmod` instruction. diff --git a/tools/ppc-manual/alu/divwux.md b/tools/ppc-manual/alu/divwux.md index dfac6b68..f10da277 100644 --- a/tools/ppc-manual/alu/divwux.md +++ b/tools/ppc-manual/alu/divwux.md @@ -128,10 +128,10 @@ int InstrEmit_divwux(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **32-bit operands, zero-extended result.** Both `RA` and `RB` are read as their low 32 bits, unsigned (`as u32`); the quotient is computed as `u32`, then *zero-extended* to 64 bits. The high 32 bits of `RA`/`RB` are ignored on input and the high 32 bits of `RT` are zero on output. -- **Single undefined case.** Division by zero (`RB == 0`); xenia-rs returns 0 ([`interpreter.rs:251`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). No `INT_MIN/-1` case because the operands are unsigned. +- **Single undefined case.** Division by zero (`RB == 0`); Canary's x64 backend skips the divide and yields `RT = 0` (the emitter itself does not check). No `INT_MIN/-1` case because the operands are unsigned. - **No trap on Xenon.** Same as [`divdx`](divdx.md) — silent undefined result. -- **`OE=1` should set `XER[OV]` on `RB == 0`**; xenia-rs ignores this. -- **`Rc=1` CR0 update.** [`interpreter.rs:256`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) uses `as i32 as i64` — for an unsigned 32-bit quotient stored in the low 32 bits with high zeros, this matches spec exactly; the i32 view will be negative iff the unsigned quotient ≥ 2^31. Worth flagging when comparing CR0 against zero after a large `divwu`. +- **`OE=1` should set `XER[OV]` on `RB == 0`**; Canary's `OE` branch is `XEINSTRNOTIMPLEMENTED()`. +- **`Rc=1` CR0 update.** Canary zero-extends the 32-bit unsigned quotient into `RT` and `f.UpdateCR(0, v)` compares its low 32 bits as signed — so CR0 reads `LT` iff the unsigned quotient ≥ 2^31. Worth flagging when comparing CR0 against zero after a large `divwu`. - **Truncating quotient.** Floor division for non-negative integers; matches C `unsigned` semantics. - **Same slow non-pipelined latency** as `divw`. diff --git a/tools/ppc-manual/alu/divwx.md b/tools/ppc-manual/alu/divwx.md index dc277734..bcb4ecd1 100644 --- a/tools/ppc-manual/alu/divwx.md +++ b/tools/ppc-manual/alu/divwx.md @@ -127,10 +127,10 @@ int InstrEmit_divwx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **32-bit operands, sign-extended result.** Both `RA` and `RB` are read as their low 32 bits, signed; the quotient is computed as `i32`, then sign-extended to 64 bits before being stored in `RT`. The high 32 bits of `RA`/`RB` are *ignored*. -- **Two undefined cases:** `RB == 0` and `RA == INT32_MIN && RB == -1` (quotient `2^31` is unrepresentable). Xenia-rs returns 0 for both ([`interpreter.rs:238`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)); PowerISA leaves `RT` boundedly undefined. +- **Two undefined cases:** `RB == 0` and `RA == INT32_MIN && RB == -1` (quotient `2^31` is unrepresentable). Canary's x64 backend yields 0 for both — it skips the divide — while PowerISA leaves `RT` boundedly undefined. - **No trap on Xenon.** Like [`divdx`](divdx.md), the processor silently produces an undefined value instead of raising an exception. -- **`OE=1` should set `XER[OV]`** in both undefined cases; xenia-rs does not implement this. -- **`Rc=1` CR0 update truncates to 32 bits in xenia-rs.** [`interpreter.rs:243`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) uses `as i32 as i64`. This is *correct* for `divw` because the result is already a sign-extended 32-bit value — high bits agree with the low-32 sign extension. So spec and xenia agree for this instruction. +- **`OE=1` should set `XER[OV]`** in both undefined cases; Canary's `OE` branch is `XEINSTRNOTIMPLEMENTED()`. +- **`Rc=1` CR0 matches spec here.** Canary stores the 32-bit quotient **zero**-extended (spec leaves `RT[0:31]` undefined), but `f.UpdateCR(0, v)` compares the low 32 bits as signed, which is exactly the sign of the 32-bit quotient. - **Truncating quotient.** Rounds toward zero, matching C `/` for `int32_t`. - **Slow.** Same ~30-cycle non-pipelined cost as 64-bit divide; faster than `divd` because the underlying datapath is narrower but still much slower than multiply-then-shift reciprocal sequences. diff --git a/tools/ppc-manual/alu/eieio.md b/tools/ppc-manual/alu/eieio.md index f5c77113..c785c876 100644 --- a/tools/ppc-manual/alu/eieio.md +++ b/tools/ppc-manual/alu/eieio.md @@ -98,9 +98,9 @@ int InstrEmit_eieio(PPCHIRBuilder& f, const InstrData& i) { - **Memory-ordering barrier for caching-inhibited / guarded storage.** `eieio` ensures all preceding loads/stores to caching-inhibited or guarded memory complete before any subsequent such accesses begin. It is *weaker* than [`sync`](sync.md): it does not order cacheable storage and does not flush the store queue. - **No register or CR effects.** Every operand field is unused; assemblers emit the canonical `0x7c0006ac` word. - **Used at MMIO boundaries.** Driver code touching device registers (e.g. the GPU command processor on Xenon) typically pairs writes with `eieio` to enforce write ordering at the bus. -- **Xenia-rs is a no-op.** The interpreter trivially advances PC ([`interpreter.rs:1267`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). Because xenia-rs is a single-threaded interpreter targeting userland Xbox 360 binaries — which never see real MMIO — this is correct: the host's natural program order suffices. +- **Canary emits a memory barrier, not a no-op.** `InstrEmit_eieio` is `f.MemoryBarrier()`. Userland Xbox 360 code never touches real MMIO, so for a sequential C translation the natural program order already suffices. - **Categorised under ALU here**, but operationally it's a memory ordering primitive (xenia-canary places it in `ppc_emit_memory.cc`). Disassembly tools may bin it differently. -- **Distinct from `sync` and `isync`.** All three share xenia's no-op arm in [`interpreter.rs:1266`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs); on real hardware they have very different semantics and latencies. +- **Distinct from `sync` and `isync`.** In Canary `eieio` and `sync` both emit `f.MemoryBarrier()`, while `isync` emits `f.Nop()`; on real hardware the three have very different semantics and latencies. ## Related Instructions diff --git a/tools/ppc-manual/alu/eqvx.md b/tools/ppc-manual/alu/eqvx.md index 7c8c1370..029da42c 100644 --- a/tools/ppc-manual/alu/eqvx.md +++ b/tools/ppc-manual/alu/eqvx.md @@ -110,7 +110,7 @@ int InstrEmit_eqvx(PPCHIRBuilder& f, const InstrData& i) { - **Operand convention is X-form** (`RA` is destination; `RS`, `RB` are sources). - **64-bit operation** on Xenon; `~` is full 64-bit on `u64`. - **No `OE`, no `XER` side effects.** Only `Rc=1` updates `CR0`. -- **64-bit CR update on Xenon, 32-bit in xenia-rs.** [`interpreter.rs:382`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) truncates with `as i32 as i64`. Significant when the high 32 bits of the result differ from the low 32 — e.g. `eqv. RA, RS, RB` with `RS == 0x1_0000_0000`, `RB == 0`: spec sees `0xFFFFFFFE_FFFFFFFF` (`LT`), xenia sees `0xFFFFFFFFFFFFFFFF` (`LT`) — actually both negative here, but the *exact* CR contents differ for finer cases. +- **64-bit CR update on Xenon, 32-bit in Canary.** `f.UpdateCR(0, v)` compares only the low 32 bits, so spec and Canary disagree whenever the high word decides the sign, or the low word is zero while the high word is not. ## Related Instructions diff --git a/tools/ppc-manual/alu/extsbx.md b/tools/ppc-manual/alu/extsbx.md index fb0e59a3..65ab6ef9 100644 --- a/tools/ppc-manual/alu/extsbx.md +++ b/tools/ppc-manual/alu/extsbx.md @@ -109,7 +109,7 @@ int InstrEmit_extsbx(PPCHIRBuilder& f, const InstrData& i) { - **Sign-extends the low 8 bits of `RS` to 64 bits.** Bit 56 of `RS` (the sign bit of the byte) becomes bits 0–55 of `RA`; bits 56–63 are copied verbatim. - **Common after a byte load.** `lbz` zero-extends from memory; `extsb` converts the result to a signed-byte view. Many compilers emit this pair; the recent ISA `lba`/`lbau` family is *not* available on the Xenon, so this two-instruction sequence is the canonical pattern. -- **`Rc=1` updates CR0 from the full 64-bit signed value** — but xenia-rs truncates to 32 bits in [`interpreter.rs:384`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). For `extsb.` this is harmless because the result fits in 8 bits sign-extended; the low 32 bits already encode the sign correctly. +- **`Rc=1` updates CR0 from the full 64-bit signed value** — Canary truncates to 32 bits in `f.UpdateCR(0, v)`. For `extsb.` this is harmless: the result is sign-extended from 8 bits, so the low 32 bits already encode its sign and zero-ness. - **Operand convention** is the X-form one (`RA` destination, `RS` source). Same as the rest of the logical family. - **No `XER` side effects.** - **`RB` field unused.** Set to 0 by assemblers; ignored on decode. diff --git a/tools/ppc-manual/alu/extshx.md b/tools/ppc-manual/alu/extshx.md index 72b90697..c91c70d6 100644 --- a/tools/ppc-manual/alu/extshx.md +++ b/tools/ppc-manual/alu/extshx.md @@ -109,7 +109,7 @@ int InstrEmit_extshx(PPCHIRBuilder& f, const InstrData& i) { - **Sign-extends the low 16 bits of `RS` to 64 bits.** Bit 48 (the sign bit of the half-word) is replicated through bits 0–47 of `RA`. - **Pairs with `lhz`** to convert an unsigned half-word load into a signed half-word value. Note that `lha` already does the sign extension on load — `extsh` is mostly emitted when the half-word is computed in a register first. -- **`Rc=1` CR0 update.** Xenia-rs uses `as i32 as i64` ([`interpreter.rs:389`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)) — harmless here because the sign-extended 16-bit value fits in 32 bits exactly. +- **`Rc=1` CR0 update.** Canary's `INT32` truncation in `f.UpdateCR(0, v)` is harmless here because the sign-extended 16-bit value fits in 32 bits exactly. - **Operand convention** is the X-form one (`RA` destination, `RS` source). - **No `XER` side effects.** - **`RB` field unused.** diff --git a/tools/ppc-manual/alu/extswx.md b/tools/ppc-manual/alu/extswx.md index de300b08..5aba24ea 100644 --- a/tools/ppc-manual/alu/extswx.md +++ b/tools/ppc-manual/alu/extswx.md @@ -109,7 +109,7 @@ int InstrEmit_extswx(PPCHIRBuilder& f, const InstrData& i) { - **Sign-extends the low 32 bits of `RS` to 64 bits.** Bit 32 (sign bit of the word) is replicated through bits 0–31 of `RA`. - **Used heavily in 32-to-64-bit promotion.** Most Xbox 360 ABI parameters are 32-bit; promoting a 32-bit `int` to a 64-bit GPR requires this instruction. Many functions emit it on entry to canonicalise their argument registers. -- **`Rc=1` CR0 update is correctly 64-bit.** [`interpreter.rs:399`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) uses `as i64` (no truncation) — one of the few xenia-rs sites where the spec width is honoured. The signed compare in CR0 reflects the full sign-extended value. +- **`Rc=1` CR0 update is harmless in Canary.** `f.UpdateCR(0, v)` truncates to `INT32`, but the result is the sign extension of its own low word, so sign and zero-ness agree with the full 64-bit compare. - **Operand convention** is the X-form one (`RA` destination, `RS` source). - **No `XER` side effects.** - **`RB` field unused.** diff --git a/tools/ppc-manual/alu/isync.md b/tools/ppc-manual/alu/isync.md index 93adede8..73649781 100644 --- a/tools/ppc-manual/alu/isync.md +++ b/tools/ppc-manual/alu/isync.md @@ -100,7 +100,7 @@ int InstrEmit_isync(PPCHIRBuilder& f, const InstrData& i) { - **Stronger than [`sync`](sync.md) for instruction stream**, weaker for memory stream — `isync` does not order stores against later loads. It only forces a fetch refresh. - **Common idiom: `dcbf` / `icbi` / `sync` / `isync`** — flush data cache, invalidate instruction cache, drain memory, refetch — used by JITs and self-modifying loaders. - **No operands.** Encoded as a fixed-form `XL` instruction; assemblers always emit `0x4c00012c`. -- **Xenia-rs is a no-op.** [`interpreter.rs:1267`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) handles `sync`/`eieio`/`isync` together. Because xenia interprets in straight-line program order without any speculative instruction cache, no barrier behaviour is needed for correctness. +- **Canary emits `f.Nop()`** for `isync` (its own `XEINSTRNOTIMPLEMENTED()` is commented out), while `sync` and `eieio` emit `f.MemoryBarrier()`. A sequential C translation needs no barrier behaviour: straight-line program order already serialises. - **Privilege level: user.** Unlike most cache management ops, `isync` is unprivileged and frequently appears in userland trampolines. ## Related Instructions diff --git a/tools/ppc-manual/alu/mulhdux.md b/tools/ppc-manual/alu/mulhdux.md index 46cdc6ee..77af6ac0 100644 --- a/tools/ppc-manual/alu/mulhdux.md +++ b/tools/ppc-manual/alu/mulhdux.md @@ -113,8 +113,8 @@ int InstrEmit_mulhdux(PPCHIRBuilder& f, const InstrData& i) { - **Returns the high 64 bits of an unsigned 64×64 product.** Operands are zero-extended (treated as unsigned) before multiplication. Pair with [`mulldx`](mulldx.md) for the low 64 bits to form a full 128-bit unsigned product. - **No `OE` bit.** No overflow signal — the high half is a defined function of the inputs even when the product fills 128 bits. -- **Xenia uses native `u128`.** [`interpreter.rs:284`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) widens both operands then shifts. Note `as u128` *zero*-extends, in contrast to [`mulhdx`](mulhdx.md)'s `as i64 as i128` which sign-extends — this is the entire semantic difference. -- **`Rc=1` CR0 update is correctly 64-bit.** Uses `as i64` directly. Because the high half is unsigned, treating it as signed for CR0 means very large unsigned values appear `LT` — keep this in mind when interpreting the CR0 bits after `mulhdu.`. +- **Canary uses `f.MulHi(RA, RB, ARITHMETIC_UNSIGNED)`** — the high 64 bits of the 128-bit *unsigned* product. The signed sibling [`mulhdx`](mulhdx.md) is the same call without `ARITHMETIC_UNSIGNED`; that flag is the entire semantic difference. +- **`Rc=1` CR0 update is 32-bit in Canary.** `f.UpdateCR(0, v)` truncates the unsigned high half to `INT32` and compares it as signed, so a value with bit 32 set reads `LT` even though it is a large positive unsigned number. Spec compares all 64 bits (still as signed) — keep both in mind when reading CR0 after `mulhdu.`. - **Used in reciprocal-multiply division strategies.** Compilers may strength-reduce divide-by-constant into `mulhdu` plus a shift; appears in optimised disassembly. - **Slow.** Same multi-cycle cost as the signed variant. diff --git a/tools/ppc-manual/alu/mulhdx.md b/tools/ppc-manual/alu/mulhdx.md index fa2231a0..fb055e70 100644 --- a/tools/ppc-manual/alu/mulhdx.md +++ b/tools/ppc-manual/alu/mulhdx.md @@ -112,8 +112,8 @@ int InstrEmit_mulhdx(PPCHIRBuilder& f, const InstrData& i) { - **Returns the high 64 bits of a signed 64×64 product.** Pair with [`mulldx`](mulldx.md) (which returns the low 64 bits) to obtain the full 128-bit product. Both must be issued separately; PowerPC has no fused multiply-double-wide instruction. - **No `OE` bit.** This XO-form instruction has no overflow-enable variant — there is no "high half overflow" because the high half is always defined. -- **Xenia widens to `i128` natively.** [`interpreter.rs:275`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) does the multiply in 128 bits then extracts the high 64. The `i64 as i128` casts ensure signed extension on both sides. -- **`Rc=1` CR0 update is correctly 64-bit.** [`interpreter.rs:278`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) uses `as i64` directly. CR0 reflects the sign of the high half: `LT` if the product is negative, `GT` if positive and large enough to overflow into the high half, `EQ` if the product fits in 64 bits *signed* (so the high half is the sign-extension of the low half — but xenia's check uses raw signed-zero compare, which equates only when the high half is exactly zero, i.e. the product is in `[0, 2^63)`). +- **Canary uses `f.MulHi(RA, RB)`** (signed) — the high 64 bits of the 128-bit signed product of the two 64-bit operands. +- **`Rc=1` CR0 reflects the sign of the high half — at 32 bits in Canary.** Per spec: `LT` if the product is negative, `EQ` iff the high half is exactly zero (`0 ≤ product < 2^64`), otherwise `GT`. Canary's `f.UpdateCR(0, v)` compares only the high half's low 32 bits, so it can report `EQ` or the wrong sign when the high half's upper word is non-zero. - **Use [`mulhdux`](mulhdux.md) for the unsigned high half.** The two instructions differ in whether the operands are sign- or zero-extended before the multiply. - **Slow.** 64-bit multiply is multi-cycle on Xenon; combining `mulhd` with `mulld` for a full 128-bit product roughly doubles the cost. diff --git a/tools/ppc-manual/alu/mulhwux.md b/tools/ppc-manual/alu/mulhwux.md index 045aa9e9..c97811e8 100644 --- a/tools/ppc-manual/alu/mulhwux.md +++ b/tools/ppc-manual/alu/mulhwux.md @@ -117,10 +117,10 @@ int InstrEmit_mulhwux(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Inputs are the low 32 bits, zero-extended.** `RA[32:63]` and `RB[32:63]` are treated as unsigned, widened to 64-bit `u64`, multiplied; the high 32 bits of the 64-bit product land in `RT[32:63]`. Xenia masks the high 32 bits of `RT` to zero ([`interpreter.rs:231`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). +- **Inputs are the low 32 bits, zero-extended.** `RA[32:63]` and `RB[32:63]` are treated as unsigned, widened to 64 bits and multiplied; the high 32 bits of the product land in `RT[32:63]`. Canary shifts the product *logically* (`f.Shr(…, 32)`), so `RT[0:31]` is zero. - **Pair with [`mullwx`](mullwx.md) for the full 64-bit unsigned product.** `mullw` returns the low 32 sign-extended; for unsigned use, pair `mulhwu` with `rlwinm` to mask the low half. Xbox 360 compilers commonly emit this combination. - **No `OE` bit.** Same family rule. -- **`Rc=1` CR0 update.** Uses `as i32 as i64` ([`interpreter.rs:234`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). Because the result is bounded by `0xFFFFFFFF` and stored only in the low 32 bits, this CR0 will report `LT` for any unsigned high half ≥ `0x80000000` — a known signed/unsigned interpretation pitfall when `Rc=1` is used with `mulhwu`. +- **`Rc=1` CR0 update.** Canary's `f.UpdateCR(0, v)` compares the low 32 bits as signed. Since that word is the unsigned high half, CR0 reports `LT` for any high half ≥ `0x80000000` — a known signed/unsigned pitfall with `mulhwu.`. - **Common idiom for multi-precision arithmetic.** `mulhwu` + `mullw` + `addc` chains build extended-precision multiplies entirely in 32-bit ops; useful for cryptographic code that targets the Xenon's 32-bit ABI. - **Multi-cycle latency** like the rest of the multiply family. diff --git a/tools/ppc-manual/alu/mulhwx.md b/tools/ppc-manual/alu/mulhwx.md index d08be710..3f22e557 100644 --- a/tools/ppc-manual/alu/mulhwx.md +++ b/tools/ppc-manual/alu/mulhwx.md @@ -117,11 +117,11 @@ int InstrEmit_mulhwx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Inputs are the low 32 bits, signed-extended.** `RA[32:63]` and `RB[32:63]` are sign-extended to 64-bit signed values, multiplied, and the *high* 32 bits of the 64-bit product are returned in `RT[32:63]`. The high 32 bits of `RT` are *implementation-defined* per spec but xenia masks them to zero ([`interpreter.rs:222`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) `& 0xFFFF_FFFF`). +- **Inputs are the low 32 bits, sign-extended.** `RA[32:63]` and `RB[32:63]` are sign-extended to 64 bits and multiplied; the *high* 32 bits of the product are returned in `RT[32:63]`. Spec leaves `RT[0:31]` undefined; Canary shifts *arithmetically* (`f.Sha(…, 32)`), so `RT[0:31]` holds the sign extension of the high half. - **Pair with [`mullwx`](mullwx.md) for the full 64-bit product.** Both can issue independently — no fused 32×32→64 instruction. - **No `OE` bit.** Like all `mulh*` instructions, no overflow flag is produced; the high half is by definition defined. -- **Xenia-rs quirk: high 32 bits zeroed.** Because spec says they're "undefined", legitimately matching either zero, sign-extension, or garbage. Xenia chooses zero, which differs from the literal Xenon behaviour (which sign-extends in some microarchitecture cases). For game code that doesn't read those bits, the difference is invisible. -- **`Rc=1` CR0 update.** [`interpreter.rs:225`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) uses `as i32 as i64` — operates on the truncated low 32 bits, which is correct for the *defined* portion of the result. +- **`RT[0:31]` is implementation-specific.** Because spec leaves those bits undefined, zero, sign extension and garbage are all legal; Canary sign-extends (see above). Code that never reads the upper word cannot tell the difference. +- **`Rc=1` CR0 update.** Canary's `f.UpdateCR(0, v)` compares the low 32 bits — the *defined* portion of the result — so CR0 is correct whatever sits in `RT[0:31]`. - **Multi-cycle latency.** Multiply is the slowest pipelined ALU op; `mulhw` shares the divider/multiplier unit. ## Related Instructions diff --git a/tools/ppc-manual/alu/mulldx.md b/tools/ppc-manual/alu/mulldx.md index ba4ea294..fcb35a6a 100644 --- a/tools/ppc-manual/alu/mulldx.md +++ b/tools/ppc-manual/alu/mulldx.md @@ -115,8 +115,8 @@ int InstrEmit_mulldx(PPCHIRBuilder& f, const InstrData& i) { - **Returns the low 64 bits of a signed 64×64 product.** Equivalent to `(int64_t)(RA * RB)` modulo `2^64`. Both operands are full 64-bit signed; no truncation on input. - **High bits silently lost.** The high 64 bits of the true product are discarded; pair with [`mulhdx`](mulhdx.md) (signed) or [`mulhdux`](mulhdux.md) (unsigned) to recover them. -- **`OE=1` should set `XER[OV]`** when the 128-bit signed product cannot be represented in 64 bits — i.e. when `mulhd RA, RB` is not the sign-extension of `mulld RA, RB`. **Xenia-rs does not implement** `OE` ([`interpreter.rs:264`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) has no `oe()` branch). -- **`Rc=1` CR0 update is correctly 64-bit.** [`interpreter.rs:269`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) uses `as i64` — full 64-bit signed compare. One of the few non-truncating CR0 sites in xenia-rs; means `mulld.` gives spec-correct CR0 even when the result has non-zero high 32 bits. +- **`OE=1` should set `XER[OV]`** when the 128-bit signed product cannot be represented in 64 bits — i.e. when `mulhd RA, RB` is not the sign-extension of `mulld RA, RB`. Canary's `OE` branch is `XEINSTRNOTIMPLEMENTED()`. +- **`Rc=1` CR0 update is 32-bit in Canary.** `f.UpdateCR(0, v)` truncates the 64-bit product to `INT32`, so `mulld.` gives a CR0 that differs from spec whenever the high 32 bits decide the sign, or the low word is zero while the high word is not. - **Same instruction for signed and unsigned low halves.** Modular arithmetic is identical; only the high half (`mulhd` vs `mulhdu`) distinguishes the interpretations. - **Multi-cycle latency** — slowest of the ALU pipelines after divide. diff --git a/tools/ppc-manual/alu/mulli.md b/tools/ppc-manual/alu/mulli.md index 38bc946c..769c3094 100644 --- a/tools/ppc-manual/alu/mulli.md +++ b/tools/ppc-manual/alu/mulli.md @@ -99,7 +99,7 @@ int InstrEmit_mulli(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **64-bit operand, sign-extended 16-bit immediate.** Xenia reads the full 64-bit `RA` as `i64` and the immediate as a sign-extended `i64` ([`interpreter.rs:80-81`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)) — note this differs from the PPC pseudocode header which writes `(RA) * EXTS(SIMM)` as a 64-bit operation but other implementations sometimes treat it as 32×32. On the Xenon (and in xenia-rs), it is genuinely 64-bit. +- **64-bit operand, sign-extended 16-bit immediate.** Canary emits `f.Mul(RA, EXTS16(D))` on the full 64-bit `RA` — genuinely 64-bit, not 32×32. - **Returns the low 64 bits.** No high half is produced — equivalent to `(int64_t)RA * (int64_t)SIMM` modulo `2^64`. There is no `mulhi`-immediate instruction. - **No `Rc`, no `OE`.** This D-form has no flag bits — strictly `RT ← RA * SIMM`. To check overflow, compare the result to `(int32_t)RA * SIMM` after the fact, or use [`mulldx`](mulldx.md) with `OE=1` after materialising the immediate. - **Common compiler idiom.** `mulli` is heavily used for fixed-stride array indexing (`r3 *= sizeof_struct`) when the size is a small signed constant. diff --git a/tools/ppc-manual/alu/mullwx.md b/tools/ppc-manual/alu/mullwx.md index 5ae39b22..37d41c86 100644 --- a/tools/ppc-manual/alu/mullwx.md +++ b/tools/ppc-manual/alu/mullwx.md @@ -129,8 +129,8 @@ if Rc then - **Inputs are the low 32 bits.** `mullw` only looks at `RA[32:63]` and `RB[32:63]`; the high 32 bits of each source are ignored. This is a 32-bit × 32-bit → 64-bit signed multiply. For full 64-bit operands use [`mulldx`](mulldx.md). - **Result is sign-extended to 64 bits.** The 64-bit product fits into a 64-bit GPR without loss. Subsequent 32-bit consumers see `RT[32:63]` (the low 32 bits of the product); use [`mulhwx`](mulhwx.md) for the signed high 32 bits or [`mulhwux`](mulhwux.md) for the unsigned high 32 bits, computed in parallel without this instruction. -- **`OE` overflow test is 32-bit.** `XER[OV]` is set iff the 64-bit signed product cannot be represented in 32 bits — equivalently, iff `RT[32] ≠ RT[33] = … = RT[63]` (sign bit disagrees with the next 32 bits). Xenia-rs does **not** implement this; `OE` on `mullwo` is a no-op in the interpreter. -- **Xenia-rs CR0 update bug footprint.** The interpreter computes CR0 from `result as i32 as i64` — the low 32 bits sign-extended. For a 32×32→64 multiply the high 32 bits may be non-zero even when the low 32 bits are zero, so xenia's CR0 can differ from the spec's (which compares the full 64-bit product to zero). In practice this matters only for code that relies on `mullw.` to detect overflow via CR0 — extremely rare. +- **`OE` overflow test is 32-bit.** `XER[OV]` is set iff the 64-bit signed product cannot be represented in 32 bits — iff `RT[0:32]` are not all equal (the product is not the sign extension of its low word). Canary's `OE` branch is `XEINSTRNOTIMPLEMENTED()`. +- **CR0 compares only the low 32 bits in Canary.** Canary stores the full 64-bit product of the sign-extended words, but `f.UpdateCR(0, v)` compares `Truncate(v, INT32)` with zero. The high 32 bits may be non-zero while the low 32 are zero, so Canary's CR0 can differ from spec's full 64-bit compare. It matters only for code that detects overflow through `mullw.`'s CR0 — rare. - **Latency.** On the Xenon, `mullw` has higher latency than add/sub; many hot inner loops avoid it by strength-reduction or shift-add chains. This is irrelevant for correctness but sometimes explains surprising instruction sequences in disassembly. ## Related Instructions diff --git a/tools/ppc-manual/alu/nandx.md b/tools/ppc-manual/alu/nandx.md index cfe7a5d1..83308a7e 100644 --- a/tools/ppc-manual/alu/nandx.md +++ b/tools/ppc-manual/alu/nandx.md @@ -109,7 +109,7 @@ int InstrEmit_nandx(PPCHIRBuilder& f, const InstrData& i) { - **Operand convention is X-form** (`RA` destination, `RS`/`RB` sources). - **64-bit operation** on Xenon; `~` operates on the full `u64`. - **No `OE` or `XER` side effects.** Only `Rc=1` updates `CR0` (signed compare to zero). -- **64-bit CR update on Xenon, 32-bit in xenia-rs.** [`interpreter.rs:377`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) truncates with `as i32 as i64`. NAND results frequently have all-ones high bits when the low half AND is non-saturating, so the truncation can change CR0 semantics in subtle ways — call out as a quirk if reproducing CR-sensitive behaviour. +- **64-bit CR update on Xenon, 32-bit in Canary.** `f.UpdateCR(0, v)` truncates to `INT32`. NAND results frequently have all-ones high bits, so the low word's sign can differ from the full word's — call it out if reproducing CR-sensitive behaviour. - **Idiom: NAND of two equal values produces NOT.** `nand. RA, RS, RS` ≡ `~RS` with CR0 update. Sometimes used by compilers when `not.` is unavailable in their tablegen. ## Related Instructions diff --git a/tools/ppc-manual/alu/negx.md b/tools/ppc-manual/alu/negx.md index cc847669..548f9156 100644 --- a/tools/ppc-manual/alu/negx.md +++ b/tools/ppc-manual/alu/negx.md @@ -120,9 +120,9 @@ int InstrEmit_negx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Two's-complement negate.** `RT ← ~RA + 1`, equivalent to `0 − RA`. A specialisation of [`subfx`](subfx.md) where `RB` is implicit zero. -- **`INT64_MIN` is its own negation.** `neg(0x8000000000000000) = 0x8000000000000000` — the only fixed point. `nego` should set `XER[OV]` in this case (it is the canonical signed-overflow trigger), but **xenia-rs does not implement `OE`** ([`interpreter.rs:201`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) has no `oe()` branch). +- **`INT64_MIN` is its own negation.** `neg(0x8000000000000000) = 0x8000000000000000` — the only fixed point, and the canonical signed-overflow trigger for which `nego` should set `XER[OV]`. Canary never sets `XER[OV]`: its `OE` path only special-cases an operand that is literally `0x8000000000000000` (`v->AsUint64()`), storing it unchanged and skipping the CR update. - **`RB` field unused.** Set to 0 by assemblers; ignored. -- **`Rc=1` CR0 update truncates to 32 bits in xenia-rs.** [`interpreter.rs:204`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). Important: `neg.` of a 64-bit value with high bits set will give a CR0 that doesn't match spec (which compares the full 64-bit `~RA + 1` to zero). +- **`Rc=1` CR0 update truncates to 32 bits in Canary** (`f.UpdateCR(0, v)`). `neg.` of a 64-bit value whose high word decides the sign gives a CR0 that doesn't match spec, which compares the full 64-bit `~RA + 1` to zero. - **No carry produced.** Use [`subfic`](subficx.md) `RT, RA, 0` (`RT ← 0 − RA` with carry) when you need the borrow. - **Latency: single cycle.** Negate is the cheapest XO-form ALU operation (cheaper than `subf` despite being a special case, because there's no `RB` operand fetch). diff --git a/tools/ppc-manual/alu/norx.md b/tools/ppc-manual/alu/norx.md index 64fd3b11..5a0b27cd 100644 --- a/tools/ppc-manual/alu/norx.md +++ b/tools/ppc-manual/alu/norx.md @@ -109,7 +109,7 @@ int InstrEmit_norx(PPCHIRBuilder& f, const InstrData& i) { - **Operand convention** is X-form (`RA` destination, `RS`/`RB` sources). - **64-bit operation** on Xenon; full 64-bit complement via `!` on `u64`. - **No `OE` or `XER` side effects.** -- **`Rc=1` CR0 update truncates to 32 bits in xenia-rs.** [`interpreter.rs:372`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) uses `as i32 as i64`. Note that NOR almost always produces results with high bits set (since the OR rarely covers all 64 bits), so the truncated CR0 is usually `LT` (negative low half) where spec might give a different signed compare for the full 64-bit value. +- **`Rc=1` CR0 update truncates to 32 bits in Canary** (`f.UpdateCR(0, v)`). NOR almost always sets high bits; the low word's sign decides Canary's CR0, the full word's sign decides spec's. - **`nor.` after a clear-low operation is a common pattern** for testing whether some high-bit mask is empty. ## Related Instructions diff --git a/tools/ppc-manual/alu/orcx.md b/tools/ppc-manual/alu/orcx.md index 4af094ac..ffd24199 100644 --- a/tools/ppc-manual/alu/orcx.md +++ b/tools/ppc-manual/alu/orcx.md @@ -108,9 +108,9 @@ int InstrEmit_orcx(PPCHIRBuilder& f, const InstrData& i) { - **`RA ← RS OR (NOT RB)`.** The complement is on `RB`. Useful for setting bits *outside* a mask — e.g. `orc r3, r3, r4` sets in `r3` every bit *not* set in `r4`. - **Idiom: `orc RA, RS, RS`** = `RS | ~RS` = `-1` (all ones). Cheaper-looking than constructing `−1` via `lis`+`ori`, but the assembler usually prefers `li RA, -1` or `eqv RA, RS, RS`. - **Operand convention** is X-form (`RA` destination, `RS`/`RB` sources). -- **64-bit operation** on Xenon; xenia uses Rust's `!` on `u64` for full-width complement. +- **64-bit operation** on Xenon; Canary emits a full-width `f.Or(RS, f.Not(RB))`. - **No `OE` or `XER` side effects.** -- **`Rc=1` CR0 update truncates to 32 bits in xenia-rs.** [`interpreter.rs:362`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). Because `~RB` typically has high bits set, `orc.` results often appear `LT` in the truncated CR0. +- **`Rc=1` CR0 update truncates to 32 bits in Canary** (`f.UpdateCR(0, v)`). Because `~RB` typically has high bits set, `orc.` results whose low word's sign differs from the full word's give different CR0 in spec and Canary. ## Related Instructions diff --git a/tools/ppc-manual/alu/ori.md b/tools/ppc-manual/alu/ori.md index 03c68b30..86e58dfa 100644 --- a/tools/ppc-manual/alu/ori.md +++ b/tools/ppc-manual/alu/ori.md @@ -106,7 +106,7 @@ int InstrEmit_ori(PPCHIRBuilder& f, const InstrData& i) { - **Immediate is zero-extended.** Only the low 16 bits of `RA` can be affected; the high 48 bits are passed through from `RS` unchanged. - **`ori 0, 0, 0` is the canonical NOP.** All PowerPC NOPs assemble to this encoding (`0x60000000`). Disassemblers usually display this as `nop`. - **Common idiom: build a 32-bit constant via `lis` + `ori`.** `lis r3, hi16; ori r3, r3, lo16` materialises any 32-bit immediate with no CR or XER disturbance. -- **64-bit operation in xenia-rs.** [`interpreter.rs:330`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) — full `u64` OR; high bits unchanged from `RS`. +- **64-bit operation in Canary.** `f.Or(RS, EXTZ16(UI))` on the full `u64`; the high bits come from `RS` unchanged. The canonical no-op `ori 0, 0, 0` is special-cased to `f.Nop()`. - **`RA = 0` reads `r0`** (not the literal zero). Different from `addi`'s `RA0` semantics; `ori` uses the regular `RA` interpretation. ## Related Instructions diff --git a/tools/ppc-manual/alu/oris.md b/tools/ppc-manual/alu/oris.md index 347d5b06..2f0695a8 100644 --- a/tools/ppc-manual/alu/oris.md +++ b/tools/ppc-manual/alu/oris.md @@ -102,7 +102,7 @@ int InstrEmit_oris(PPCHIRBuilder& f, const InstrData& i) { - **No record form.** No `oris.` — same as [`ori`](ori.md). For CR0 updates use [`orx`](orx.md) with `Rc=1`. - **Immediate is zero-extended *then* shifted left 16.** Only bits 32–47 of `RA` (in PowerISA bit numbering) can be affected; the high 32 bits and low 16 bits of `RA` come from `RS` unchanged. - **Common pair with `lis`** to load a 32-bit constant: `lis r3, hi16` (= `addis r3, 0, hi16`), then `ori r3, r3, lo16`. **For unsigned constants whose low half has the high bit set**, `lis` followed by `ori` works cleanly because `ori` is zero-extending; using `addi` instead would sign-extend `lo16` and corrupt the constant. -- **64-bit operation in xenia-rs.** [`interpreter.rs:334`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). +- **64-bit operation in Canary.** `f.Or(RS, EXTZ16(UI) << 16)` on the full `u64`. - **No `XER`, no `CR` effect.** Pure register OR. - **`RA = 0` reads `r0`** (not literal zero); see [`ori`](ori.md). diff --git a/tools/ppc-manual/alu/orx.md b/tools/ppc-manual/alu/orx.md index 66323070..d2bef4fb 100644 --- a/tools/ppc-manual/alu/orx.md +++ b/tools/ppc-manual/alu/orx.md @@ -126,7 +126,7 @@ int InstrEmit_orx(PPCHIRBuilder& f, const InstrData& i) { - **Operand convention** is X-form (`RA` destination, `RS`/`RB` sources). - **64-bit operation** on Xenon; full bitwise OR across 64 bits. - **No `OE` or `XER` side effects.** Only `Rc=1` updates `CR0`. -- **64-bit CR update on Xenon, 32-bit in xenia-rs.** [`interpreter.rs:357`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) truncates with `as i32 as i64`. For `or. RA, RS, RS` (i.e. `mr.`), this means CR0 reflects the low 32 bits of `RS` only — distinguishable from spec only when the high 32 bits are non-zero with all-zero low 32. +- **64-bit CR update on Xenon, 32-bit in Canary.** `f.UpdateCR(0, v)` truncates to `INT32`. For `or. RA, RS, RS` (`mr.`), CR0 reflects the low 32 bits of `RS` only — distinguishable from spec when the high word decides the sign or is non-zero over an all-zero low word. Without `Rc`, Canary turns `or rX, rX, rX` into `f.Nop()` — except the exact word `0x7FFFFB78` (`or r31, r31, r31`), which becomes `f.DelayExecution()`: an idle/yield marker a translator should recognise. - **`or 26, 26, 26` is the Xbox 360 NOP variant** historically used to mark cache lines or signal the dispatch unit (alongside `nop` ≡ `ori 0,0,0`). Disassembly may show this — it has no architectural effect. ## Related Instructions diff --git a/tools/ppc-manual/alu/rldclx.md b/tools/ppc-manual/alu/rldclx.md index 6e901b66..f79ad50a 100644 --- a/tools/ppc-manual/alu/rldclx.md +++ b/tools/ppc-manual/alu/rldclx.md @@ -129,9 +129,9 @@ int InstrEmit_rldclx(PPCHIRBuilder& f, const InstrData& i) { - **`RA ← ROTL64(RS, RB[58:63]) & MASK(MB, 63)`.** Rotate `RS` left by `RB & 0x3F`, then *clear* bits to the left of `MB` — i.e. keep bits `MB..63`, force bits `0..MB-1` to zero. - **Shift comes from a register.** Unlike [`rldiclx`](rldiclx.md), the rotate amount is dynamic. Only the low 6 bits of `RB` are used (`& 0x3F`); the upper 58 bits are silently ignored. -- **`MB` is a split 6-bit field.** Bit 5 of the encoded `mb/me` is *swapped* into bit position 5 (raw bit 30) — xenia decodes via `(instr.mb() << 1) | ((raw >> 1) & 1)` ([`interpreter.rs:587`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). This MDS form is unusual; if you write a decoder, follow this exact bit assembly. +- **`MB` is a split 6-bit field.** Canary assembles it as `(MB5 << 5) | MB`: `MB` is instruction bits 21–25 and `MB5` is bit 26, so the bit *after* the five-bit field is the most significant. Follow that assembly if you write a decoder. - **Mask generation.** `rld_mask_left(MB)` is `(1 << (64 - MB)) - 1` — i.e. clear bits `0..MB-1`, keep bits `MB..63`. When `MB = 0` the mask is all ones; when `MB = 63` only bit 63 survives. -- **`Rc=1` CR0 is correctly 64-bit.** [`interpreter.rs:592`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) uses `as i64` directly — no truncation. The rotate-and-mask family is one of the few xenia-rs instruction groups that already does the spec-correct 64-bit CR0 compare. +- **`Rc=1` CR0 update is 32-bit in Canary.** `f.UpdateCR(0, v)` truncates to `INT32`; spec compares the full 64-bit rotated-and-masked value. - **No `XER` effect.** - **Use over [`rldiclx`](rldiclx.md)** when the shift amount is computed at runtime (e.g. via `cntlzd` for normalisation). diff --git a/tools/ppc-manual/alu/rldcrx.md b/tools/ppc-manual/alu/rldcrx.md index 6ce0bc8b..5247b7ba 100644 --- a/tools/ppc-manual/alu/rldcrx.md +++ b/tools/ppc-manual/alu/rldcrx.md @@ -129,9 +129,9 @@ int InstrEmit_rldcrx(PPCHIRBuilder& f, const InstrData& i) { - **`RA ← ROTL64(RS, RB[58:63]) & MASK(0, ME)`.** Rotate `RS` left by `RB & 0x3F`, then *clear* bits to the right of `ME` — keep bits `0..ME`, force bits `ME+1..63` to zero. - **Shift from register.** Same as [`rldclx`](rldclx.md): only the low 6 bits of `RB` count. -- **`ME` is a split 6-bit field.** Same swap-decoded layout as `MB` in `rldclx`: `(instr.mb() << 1) | ((raw >> 1) & 1)` ([`interpreter.rs:597`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). Note that even though it represents `ME` here, xenia reads it from `instr.mb()` because the field shares the same encoding slot. +- **`ME` is a split 6-bit field.** Encoded exactly like `MB` in [`rldclx`](rldclx.md) — Canary even reads it from the same `InstrData.MDS` fields as `(MB5 << 5) | MB` — and applied as the mask end, `XEMASK(0, ME)`. - **Mask generation.** `rld_mask_right(ME)` = `~((1 << (63 - ME)) - 1)` keeping bits `0..ME`. When `ME = 63` the mask is all ones; when `ME = 0` only bit 0 survives. -- **`Rc=1` CR0 is correctly 64-bit.** Uses `as i64` directly ([`interpreter.rs:602`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). +- **`Rc=1` CR0 update is 32-bit in Canary** (`f.UpdateCR(0, v)`). - **No `XER` effect.** - **Useful for left-shift with arbitrary discard.** `rldcr RA, RS, RB, 63 - n` is functionally close to a left-shift-and-mask sequence, with the rotate variant additionally allowing wrap-around. diff --git a/tools/ppc-manual/alu/rldiclx.md b/tools/ppc-manual/alu/rldiclx.md index 3cb8bd2f..c7404f0b 100644 --- a/tools/ppc-manual/alu/rldiclx.md +++ b/tools/ppc-manual/alu/rldiclx.md @@ -136,9 +136,9 @@ int InstrEmit_rldiclx(PPCHIRBuilder& f, const InstrData& i) { - `srdi RA, RS, n` ≡ `rldicl RA, RS, 64-n, n` — logical right shift by `n`. - `clrldi RA, RS, n` ≡ `rldicl RA, RS, 0, n` — clear top `n` bits. - `extrdi RA, RS, n, b` ≡ `rldicl RA, RS, b+n, 64-n` — extract `n` bits starting at `b`. -- **`SH` is 6 bits, immediate** (bits 16–20 + bit 30). Xenia uses `instr.sh64()` to assemble them. +- **`SH` is 6 bits, immediate** (bits 16–20 + bit 30). Canary assembles it as `(SH5 << 5) | SH`. When `SH == 64 − MB` it emits a plain logical shift right by `MB` — the `srdi` idiom — instead of rotate-and-mask. - **`MB` is 6 bits, split-encoded** (`(instr.mb() << 1) | ((raw >> 1) & 1)`). -- **`Rc=1` CR0 is correctly 64-bit.** Uses `as i64` directly ([`interpreter.rs:551`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). +- **`Rc=1` CR0 update is 32-bit in Canary** (`f.UpdateCR(0, v)`). - **No `XER` effect.** - **Often appears in compiled disassembly** as a generic 64-bit shift. Decoding back to the simplified mnemonic above makes the intent obvious. diff --git a/tools/ppc-manual/alu/rldicrx.md b/tools/ppc-manual/alu/rldicrx.md index 4300f9f7..14c5ae74 100644 --- a/tools/ppc-manual/alu/rldicrx.md +++ b/tools/ppc-manual/alu/rldicrx.md @@ -137,8 +137,8 @@ int InstrEmit_rldicrx(PPCHIRBuilder& f, const InstrData& i) { - `clrrdi RA, RS, n` ≡ `rldicr RA, RS, 0, 63-n` — clear low `n` bits. - `extldi RA, RS, n, b` ≡ `rldicr RA, RS, b, n-1` — extract `n` bits from position `b` left-aligned. - **`SH` is 6 bits, immediate.** Same `instr.sh64()` decode as the rest of the family. -- **`ME` is 6 bits, split-encoded.** Xenia stores it via `instr.mb()` — the field shares the slot with `MB` from sister instructions; the operation just interprets it as the right edge. -- **`Rc=1` CR0 is correctly 64-bit.** Uses `as i64` directly ([`interpreter.rs:561`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). +- **`ME` is 6 bits, split-encoded.** Canary reads it through the `MD` form's `MB`/`MB5` fields — the slot `MB` occupies in sister instructions; the operation just interprets it as the right edge. +- **`Rc=1` CR0 update is 32-bit in Canary** (`f.UpdateCR(0, v)`). When `ME == 63 − SH`, Canary emits a plain `Shl` by `SH` — the `sldi` idiom — instead of rotate-and-mask. - **No `XER` effect.** - **Heavily emitted by 64-bit code generators** for left-shift-and-clear sequences. Recognising the simplified mnemonics aids disassembly. diff --git a/tools/ppc-manual/alu/rldicx.md b/tools/ppc-manual/alu/rldicx.md index 4a9f3159..5c413849 100644 --- a/tools/ppc-manual/alu/rldicx.md +++ b/tools/ppc-manual/alu/rldicx.md @@ -127,11 +127,11 @@ int InstrEmit_rldicx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **`RA ← ROTL64(RS, SH) & MASK(MB, 63 - SH)`.** Rotate `RS` left by `SH` bits, then mask off both ends: clear bits `0..MB-1` *and* clear bits `64-SH..63`. This is the "clear at both edges" variant — useful for inserting a field into an otherwise-zero register. -- **`SH` is a 6-bit immediate** spanning bits 16–20 plus bit 30 of the instruction word. Xenia uses the helper `instr.sh64()` ([`interpreter.rs:566`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)) to assemble the 6 bits. +- **`SH` is a 6-bit immediate** spanning bits 16–20 plus bit 30 of the instruction word. Canary assembles it as `(SH5 << 5) | SH`. - **`MB` is also 6-bit, split-encoded** like the rest of the `rld*` family: `(instr.mb() << 1) | ((raw >> 1) & 1)`. - **Mask is computed as `MASK_LEFT(MB) AND MASK_RIGHT(63 - SH)`.** This produces the equivalent of "left-shift `RS` by `SH` then clear high bits above bit `MB`" — a common pattern when `MB ≤ 63 - SH`. - **Equivalent to a logical shift when `MB = 0`.** `rldic RA, RS, SH, 0` ≡ `sldi RA, RS, SH` (an alias the assembler may prefer). -- **`Rc=1` CR0 is correctly 64-bit.** [`interpreter.rs:571`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) uses `as i64` directly. +- **`Rc=1` CR0 update is 32-bit in Canary** (`f.UpdateCR(0, v)`). - **No `XER` effect.** ## Related Instructions diff --git a/tools/ppc-manual/alu/rldimix.md b/tools/ppc-manual/alu/rldimix.md index ff19058f..6265fd4b 100644 --- a/tools/ppc-manual/alu/rldimix.md +++ b/tools/ppc-manual/alu/rldimix.md @@ -129,10 +129,10 @@ int InstrEmit_rldimix(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **`RA ← (ROTL64(RS, SH) & MASK) | (RA & ~MASK)`.** *Reads* the prior `RA` so it can preserve the bits outside the mask — this is the only `rld*` instruction with `RA` as both source and destination. -- **Mask is `MASK_LEFT(MB) AND MASK_RIGHT(63 - SH)`** ([`interpreter.rs:578`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)) — same span as [`rldicx`](rldicx.md), but the un-masked region is preserved in the destination instead of being zeroed. +- **Mask is `MASK(MB, 63 − SH)`** — Canary's `XEMASK(mb, ~sh)` — the same span as [`rldicx`](rldicx.md), but the un-masked region is preserved from `RA` (`(v & m) | (RA & ~m)`) instead of being zeroed. - **Use to insert a bit-field.** Common idiom: `rldimi RA, RS, b, mask_start` writes `RS`'s low (`64 - mask_start`) bits into `RA` starting at bit `b`. - **`SH` and `MB` decoding** is identical to the rest of the family (6-bit `sh` via `instr.sh64()`, 6-bit `mb` via the swap layout). -- **`Rc=1` CR0 is correctly 64-bit.** Uses `as i64` directly. +- **`Rc=1` CR0 update is 32-bit in Canary** (`f.UpdateCR(0, v)`). - **No `XER` effect.** - **Compile-time pattern.** When you see `rldimi r3, r4, n, m`, the compiler is splicing a value into `r3`; recover the meaning by computing the mask `MASK(m, 63 - n)`. diff --git a/tools/ppc-manual/alu/rlwimix.md b/tools/ppc-manual/alu/rlwimix.md index 73f78280..27b19179 100644 --- a/tools/ppc-manual/alu/rlwimix.md +++ b/tools/ppc-manual/alu/rlwimix.md @@ -128,11 +128,11 @@ int InstrEmit_rlwimix(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **`RA ← (ROTL32(RS[32:63], SH) & MASK) | (RA[32:63] & ~MASK)`.** Reads the low 32 bits of `RS`, rotates them, then *inserts* under the mask back into the low 32 bits of `RA`. The high 32 bits of `RA` are *implementation-defined* per spec; **xenia-rs zeroes them** (the `as u32` cast at [`interpreter.rs:529`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) discards them on read, then `as u64` zero-extends on write). -- **Mask follows the standard `MB..ME` PPC convention.** Both `MB` and `ME` are 5-bit fields; the mask is contiguous when `MB <= ME`, and *wraps* around (a "donut" mask: bits `MB..31` and `0..ME`) when `MB > ME`. Xenia's `rlw_mask(mb, me)` helper handles both cases. +- **`RA ← (ROTL32(RS[32:63], SH) & MASK) | (RA & ~MASK)`.** Reads the low 32 bits of `RS`, rotates them, then *inserts* under the mask. The high 32 bits of `RA` are implementation-defined per spec. Canary rotates a doubled word (`(RS << 32) | RS[32:63]`), masks it with `XEMASK(MB + 32, ME + 32)` and ORs back `RA & ~mask` — so for a contiguous mask (`MB ≤ ME`) `RA`'s high 32 bits are **preserved**, not zeroed. +- **Mask follows the standard `MB..ME` PPC convention.** Both are 5-bit fields; the mask is contiguous when `MB <= ME` and *wraps* (a "donut" mask: bits `MB..31` and `0..ME`) when `MB > ME`. Canary computes it as `XEMASK(MB + 32, ME + 32)` on the 64-bit word, which gives the right low word in both cases. - **`SH` is 5 bits.** Rotate amount is `SH mod 32`; values `≥ 32` are not encodable in this M-form. - **Used for bit-field insertion** (`insrwi RA, RS, n, b` ≡ `rlwimi RA, RS, 32-(b+n), b, b+n-1`). Compilers emit `rlwimi` extensively for struct-bitfield writes. -- **`Rc=1` CR0 update truncates to 32 bits in xenia-rs.** [`interpreter.rs:531`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). Since the high 32 bits of the result are zero, this matches spec's compare on the (defined) low half — but if a real Xenon left high bits non-zero, behaviour would diverge. +- **`Rc=1` CR0 update truncates to 32 bits in Canary** (`f.UpdateCR(0, v)`): it compares the low word only, while spec compares all 64 bits — including whatever `RA` kept in its high word. - **No `XER` effect.** ## Related Instructions diff --git a/tools/ppc-manual/alu/rlwinmx.md b/tools/ppc-manual/alu/rlwinmx.md index a904f73a..50faa90c 100644 --- a/tools/ppc-manual/alu/rlwinmx.md +++ b/tools/ppc-manual/alu/rlwinmx.md @@ -165,9 +165,9 @@ int InstrEmit_rlwinmx(PPCHIRBuilder& f, const InstrData& i) { - `clrlwi RA, RS, n` ≡ `rlwinm RA, RS, 0, n, 31` — clear high `n` bits. - `clrrwi RA, RS, n` ≡ `rlwinm RA, RS, 0, 0, 31-n` — clear low `n` bits. - `extlwi`, `extrwi`, `clrlslwi` — full mnemonic family in PowerISA appendix. -- **Mask convention `MB..ME`** is contiguous when `MB ≤ ME`. When `MB > ME`, the mask is the *complement* of bits `ME+1..MB-1` — a donut/wrap mask. Xenia's `rlw_mask` handles both. +- **Mask convention `MB..ME`** is contiguous when `MB ≤ ME`. When `MB > ME`, the mask is the *complement* of bits `ME+1..MB-1` — a donut/wrap mask. Canary computes it as `XEMASK(MB + 32, ME + 32)` on the 64-bit word; about 90% of `rlwinm`s take its fast path, `(RS << SH) & 0xFFFFFFFF`, when the mask is exactly the bits the shift keeps. - **`SH` is 5 bits**, rotate amount `0..31`. -- **`Rc=1` CR0 update truncates to 32 bits in xenia-rs.** [`interpreter.rs:518`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). Since the result fits in 32 bits, the truncation matches spec exactly. +- **`Rc=1` CR0 update truncates to 32 bits in Canary** (`f.UpdateCR(0, v)`), and that does **not** always match spec: whenever the result's bit 32 (the low word's most-significant bit) is set, spec sees a positive 64-bit value (`GT`) while Canary's `INT32` view is negative (`LT`). For a wrap mask (`MB > ME`) the result also has high-word bits set, as spec allows, so the two compares can disagree there too. - **No `XER` effect.** ## Related Instructions diff --git a/tools/ppc-manual/alu/rlwnmx.md b/tools/ppc-manual/alu/rlwnmx.md index 128c7890..bcace291 100644 --- a/tools/ppc-manual/alu/rlwnmx.md +++ b/tools/ppc-manual/alu/rlwnmx.md @@ -122,12 +122,12 @@ int InstrEmit_rlwnmx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **`RA ← ROTL32(RS[32:63], RB[59:63]) & MASK(MB, ME)`.** Identical to [`rlwinmx`](rlwinmx.md) except the rotate amount comes from the low 5 bits of `RB`. Xenia masks with `& 0x1F` ([`interpreter.rs:535`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). +- **`RA ← ROTL32(RS[32:63], RB[59:63]) & MASK(MB, ME)`.** Identical to [`rlwinmx`](rlwinmx.md) except the rotate amount comes from the low 5 bits of `RB`; Canary takes `RB & 0x1F`. - **Use over `rlwinm`** when the rotate amount is dynamic (e.g. computed from a `cntlzw` for normalisation, or read from a parameter). - **Mask is still 5+5 bits immediate** — `MB` and `ME` are not register-sourced; only the shift is. This is the M-form's quirk: only one of (`SH`, `MB`, `ME`) is variable across the family. - **Donut masks supported.** `MB > ME` produces a wraparound mask, same as `rlwinm`. - **High 32 bits of `RA` are zero** (32-bit operation, then `as u64` zero-extends). -- **`Rc=1` CR0 update truncates to 32 bits in xenia-rs.** [`interpreter.rs:540`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) — harmless because the result already fits in 32 bits. +- **`Rc=1` CR0 update truncates to 32 bits in Canary** (`f.UpdateCR(0, v)`). Not harmless in general: whenever the result's bit 32 (the low word's most-significant bit) is set, spec sees a positive 64-bit value (`GT`) while Canary's `INT32` view is negative (`LT`), and a wrap mask leaves high-word bits set. - **No `XER` effect.** ## Related Instructions diff --git a/tools/ppc-manual/alu/sldx.md b/tools/ppc-manual/alu/sldx.md index 629ba7db..81f5392d 100644 --- a/tools/ppc-manual/alu/sldx.md +++ b/tools/ppc-manual/alu/sldx.md @@ -116,9 +116,9 @@ int InstrEmit_sldx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **64-bit logical left shift.** `RA ← RS << (RB & 0x7F)` if the shift count is `< 64`, otherwise `RA = 0`. Bits shifted past bit 0 are discarded. -- **Critical: shift count is *7 bits*, not 6.** PowerISA reads `RB[57:63]` (7 bits, `0..127`). Counts in `[64, 127]` produce zero, *not* `RS << (count mod 64)`. Xenia respects this with `& 0x7F` and an explicit `if sh < 64` check ([`interpreter.rs:464`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). C semantics' undefined behaviour for `<<` with a count `>= width` is a spec-violation source if you naïvely translate. +- **Critical: shift count is *7 bits*, not 6.** PowerISA reads `RB[57:63]` (7 bits, `0..127`). Counts in `[64, 127]` produce zero, *not* `RS << (count mod 64)`. Canary respects this: `sh = RB & 0x7F`, then `Select(sh >> 6, 0, RS << sh)`. C's undefined behaviour for `<<` with a count `>= width` is a spec-violation source if you translate naïvely. - **No `XER[CA]` produced** by left shifts. Logical right [`srdx`](srdx.md) and arithmetic right [`sradx`](sradx.md) differ here — arithmetic right *does* set `CA`. -- **`Rc=1` CR0 is correctly 64-bit.** [`interpreter.rs:467`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) uses `as i64` directly. CR0 reflects the sign of the full 64-bit shifted value (which is 0 for shifts ≥ 64, otherwise either `LT`/`GT`/`EQ`). +- **`Rc=1` CR0 update is 32-bit in Canary** (`f.UpdateCR(0, v)`); spec compares the full 64-bit shifted value (which is 0 for shifts ≥ 64). - **Strength-reduced from `mulli` for power-of-two multipliers.** - **No `OE` bit.** diff --git a/tools/ppc-manual/alu/slwx.md b/tools/ppc-manual/alu/slwx.md index 9a6ce6bf..f2608695 100644 --- a/tools/ppc-manual/alu/slwx.md +++ b/tools/ppc-manual/alu/slwx.md @@ -117,10 +117,10 @@ int InstrEmit_slwx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **32-bit logical left shift, zero-extended to 64.** `RA ← (RS[32:63] << (RB & 0x3F))[32:63]` if `(RB & 0x3F) < 32`, else `RA = 0`. The high 32 bits of `RA` are always zero (zero-extension of the 32-bit result). -- **Shift count is 6 bits**, `RB[58:63]` — not 7 like [`sldx`](sldx.md). Counts in `[32, 63]` produce zero. Xenia reads the full register but the explicit `if sh < 32` guard in [`interpreter.rs:417`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) prevents Rust UB. -- **Spec quirk worth flagging:** xenia reads `ctx.gpr[instr.rb()] as u32`, which uses the low 32 bits of `RB`, not the spec's `RB & 0x3F`. For ordinary code these agree (counts ≤ 63), but a maliciously high `RB` could in principle differ. In practice this is a non-issue. +- **Shift count is 6 bits**, `RB[58:63]` — not 7 like [`sldx`](sldx.md). Counts in `[32, 63]` produce zero: Canary emits `Select(sh >> 5, 0, RS[32:63] << sh)` and zero-extends the 32-bit result. +- **Count masking matches spec in Canary.** Canary takes `RB & 0x3F` directly, so arbitrarily large `RB` values behave exactly as spec. - **No `XER[CA]` for left shifts.** -- **`Rc=1` CR0 update truncates to 32 bits** ([`interpreter.rs:420`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). Since the high 32 bits are zero, this matches spec exactly. +- **`Rc=1` CR0 update truncates to 32 bits in Canary** (`f.UpdateCR(0, v)`). The zero high word does not make that harmless: whenever the result's bit 32 (the low word's most-significant bit) is set, spec sees a positive 64-bit value (`GT`) while Canary's `INT32` view is negative (`LT`). - **No `OE` bit.** ## Related Instructions diff --git a/tools/ppc-manual/alu/sradix.md b/tools/ppc-manual/alu/sradix.md index 8d87f3a2..6944d096 100644 --- a/tools/ppc-manual/alu/sradix.md +++ b/tools/ppc-manual/alu/sradix.md @@ -131,10 +131,10 @@ int InstrEmit_sradix(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **`RA ← (i64)RS >> SH`**, with `XER[CA]` set when `RS` is negative AND any one-bit was shifted out. -- **`SH` is 6 bits.** Encoded in bits 16–20 (`sh`) plus bit 30 (`sh5`); xenia uses `instr.sh64()` to assemble the 6 bits ([`interpreter.rs:496`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). Range `0..63`. -- **`SH = 0`** is a no-op (sign-extends `RS` to itself), and explicitly clears `XER[CA]` ([`interpreter.rs:498`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). This matches spec. +- **`SH` is 6 bits.** Encoded in bits 16–20 (`sh`) plus bit 30 (`sh5`); Canary assembles `(SH5 << 5) | SH`. Range `0..63`. +- **`SH = 0`** is a no-op (sign-extends `RS` to itself) and explicitly clears `XER[CA]` — Canary stores `CA = 0` on that path. This matches spec. - **Spec divergence: 6-bit immediate, no saturation arm.** Unlike [`sradx`](sradx.md) which has a 7-bit register count and saturates at `≥ 64`, `sradi` always uses a count `< 64` so no special saturation case is needed. -- **`Rc=1` CR0 is correctly 64-bit.** [`interpreter.rs:506`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). +- **`Rc=1` CR0 update is 32-bit in Canary** (`f.UpdateCR(0, v)`). The shift is arithmetic on all 64 bits, so the low word's sign can differ from the full value's — e.g. `0x00000000_C0000000` is positive (`GT`) but reads `LT` once truncated. - **Idiom: `sradi rA, rS, n; addze rA, rA`** — signed integer divide by `2^n` rounded toward zero (the textbook PPC sequence). - **No `OE` bit.** diff --git a/tools/ppc-manual/alu/sradx.md b/tools/ppc-manual/alu/sradx.md index 9e4c70f2..af533933 100644 --- a/tools/ppc-manual/alu/sradx.md +++ b/tools/ppc-manual/alu/sradx.md @@ -129,9 +129,9 @@ int InstrEmit_sradx(PPCHIRBuilder& f, const InstrData& i) { - **64-bit arithmetic (sign-propagating) right shift.** `RA ← (i64)RS >> (RB & 0x7F)` with bits shifted in matching the sign bit of `RS`. Counts ≥ 64 saturate: `RA` becomes all-ones if `RS < 0`, else zero. - **`XER[CA]` is the "lost-ones" indicator.** `CA = 1` iff `RS` is negative AND any of the bits shifted out were `1`. This makes `srad` / `sradi` the standard idiom for "divide negative integer by power of 2 with round-toward-zero" — followed by `addze` to compensate when `CA = 1`. -- **Three branches in xenia.** `sh == 0` (no shift, `CA=0`), `sh < 64` (normal shift, `CA` per spec), and `sh ≥ 64` (saturate to `0` or `−1`, `CA` reflects sign). The `(rs as u64) << (64 - sh) != 0` check at [`interpreter.rs:486`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) extracts whether any non-zero bit was shifted out. +- **One expression in Canary, no branches.** Canary clamps the count to 63 (`sh = RB & 0x7F`, `Min(sh, 0x3F)`) and shifts arithmetically, which saturates counts ≥ 64 to `0` or `−1`. `CA = RS < 0 AND (result << clamp) != RS`, i.e. set only for a negative source that shifted out a 1 bit; a count of 0 therefore gives `CA = 0`. - **Shift count is 7 bits.** Same as [`sldx`](sldx.md): `RB[57:63]`. -- **`Rc=1` CR0 is correctly 64-bit.** [`interpreter.rs:489`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). +- **`Rc=1` CR0 update is 32-bit in Canary** (`f.UpdateCR(0, v)`); it can differ from spec when the high word decides the sign. - **No `OE` bit.** - **Used by signed-divide-by-power-of-2 idiom:** `srad rA, rS, n; addze rA, rA` produces `rS / 2^n` with truncation toward zero rather than toward `-∞`. diff --git a/tools/ppc-manual/alu/srawix.md b/tools/ppc-manual/alu/srawix.md index 08e98a02..9564cc25 100644 --- a/tools/ppc-manual/alu/srawix.md +++ b/tools/ppc-manual/alu/srawix.md @@ -137,11 +137,11 @@ int InstrEmit_srawix(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **`RA ← ((i32)RS >> SH) sign-extended`** with `XER[CA]` set when `RS` is negative AND any low bit was shifted out. -- **`SH` is 5 bits** (immediate, range `0..31`). Unlike [`srawx`](srawx.md), there is no saturation case because the count cannot exceed 31. Xenia reads it via `instr.sh()`. +- **`SH` is 5 bits** (immediate, range `0..31`). Unlike [`srawx`](srawx.md), there is no saturation case because the count cannot exceed 31. Canary reads it from the `RB` field slot (`i.X.RB`). - **`SH = 0`** sign-extends `RS[32:63]` to 64 bits and clears `CA`. This is *not* a no-op when `RS`'s high 32 bits differ from the sign extension of bit 32. - **Common idiom: `srawi rA, rS, 31`** materialises the 32-bit sign of `rS` as `0` or `−1` — the canonical "sign mask" pattern. Often used for branchless `abs` or conditional negation. - **Idiom: `srawi rA, rS, n; addze rA, rA`** — divide signed by `2^n` rounding toward zero. -- **`Rc=1` CR0 update truncates to 32 bits in xenia-rs.** [`interpreter.rs:457`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) — matches spec because the sign-extended result has consistent low/high 32-bit signs. +- **`Rc=1` CR0 update truncates to 32 bits in Canary** (`f.UpdateCR(0, v)`) — which matches spec here, because the result is sign-extended from its low word, so both halves agree on sign and zero-ness. - **No `OE` bit.** ## Related Instructions diff --git a/tools/ppc-manual/alu/srawx.md b/tools/ppc-manual/alu/srawx.md index 497477a1..6f2e5d55 100644 --- a/tools/ppc-manual/alu/srawx.md +++ b/tools/ppc-manual/alu/srawx.md @@ -128,10 +128,10 @@ int InstrEmit_srawx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **32-bit arithmetic right shift, sign-extended to 64.** `RA ← ((i32)RS >> n) sign-extended`, with `XER[CA]` set when `RS[32] = 1` (negative) AND any low bit was shifted out. -- **Shift count is 6 bits**, `RB[58:63]`. Counts `≥ 32` saturate: `RA = -1` (all-ones, sign-extended) if `RS < 0`, else `0`. Xenia handles this in three branches ([`interpreter.rs:432-444`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). +- **Shift count is 6 bits**, `RB[58:63]`. Counts `≥ 32` saturate: `RA = -1` (all-ones, sign-extended) if `RS < 0`, else `0`. Canary does it in one expression: `Sha(RS[32:63], Min(sh, 31))`, sign-extended to 64 bits. - **`SH = 0`** sign-extends `RS` to 64 bits and clears `XER[CA]` — like `extsw`, but additionally writing CA. - **Result is always sign-extended to 64 bits.** `RA[0:31]` matches the sign of `RA[32]`. This is the key difference from [`srwx`](srwx.md) (zero-extension). -- **`Rc=1` CR0 update truncates to 32 bits in xenia-rs.** [`interpreter.rs:443`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) — but since the result is sign-extended, the low 32 bits' sign matches the full 64-bit sign, so spec and xenia agree here. +- **`Rc=1` CR0 update truncates to 32 bits in Canary** (`f.UpdateCR(0, v)`) — but the result is sign-extended, so the low word's sign matches the full 64-bit sign, and spec and Canary agree here. - **Used with [`addzex`](addzex.md)** for signed divide by `2^n` rounding toward zero. - **No `OE` bit.** diff --git a/tools/ppc-manual/alu/srdx.md b/tools/ppc-manual/alu/srdx.md index 23f6581d..70e7e5f0 100644 --- a/tools/ppc-manual/alu/srdx.md +++ b/tools/ppc-manual/alu/srdx.md @@ -116,9 +116,9 @@ int InstrEmit_srdx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **64-bit logical right shift.** `RA ← RS >> (RB & 0x7F)` if the count is `< 64`, else `RA = 0`. Bits shifted in from the high end are zero (no sign extension). -- **Shift count is 7 bits** (`RB[57:63]`). Counts `64..127` produce zero, not `RS >> (count mod 64)`. Xenia respects this with `& 0x7F` and an explicit `if sh < 64` check ([`interpreter.rs:472`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). +- **Shift count is 7 bits** (`RB[57:63]`). Counts `64..127` produce zero, not `RS >> (count mod 64)`. Canary emits `Select(sh & 0x40, 0, RS >> sh)` with `sh = RB & 0x7F`. - **No `XER[CA]` produced.** This is the logical right shift; for arithmetic shift with `XER[CA]` use [`sradx`](sradx.md). -- **`Rc=1` CR0 is correctly 64-bit.** [`interpreter.rs:475`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). Result is non-negative as a signed value (high bit is always cleared by the shift), so CR0 will only ever be `EQ` or `GT`. +- **`Rc=1` CR0 update is 32-bit in Canary** (`f.UpdateCR(0, v)`). Per spec, any non-zero count clears the top bit, so CR0 is `EQ` or `GT` (a count of 0 leaves `RS` itself, which may be negative). Canary compares only the low word, whose most-significant bit can still be set, so it can report `LT` where spec says `GT`. - **No `OE` bit.** - **The `srdi` simplified mnemonic** uses [`rldiclx`](rldiclx.md) instead — `rldicl rA, rS, 64-n, n` — because it can be combined with masking. `srd` is for runtime-variable counts. diff --git a/tools/ppc-manual/alu/srwx.md b/tools/ppc-manual/alu/srwx.md index b0889e89..7405b5ae 100644 --- a/tools/ppc-manual/alu/srwx.md +++ b/tools/ppc-manual/alu/srwx.md @@ -118,9 +118,9 @@ int InstrEmit_srwx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **32-bit logical right shift, zero-extended to 64.** `RA ← (u32)RS >> (RB & 0x3F)` if count `< 32`, else `RA = 0`. The high 32 bits of `RA` are always zero. -- **Shift count is 6 bits**, `RB[58:63]`. Counts `[32, 63]` produce zero (not `RS >> (count mod 32)`); xenia's explicit `if sh < 32` guards against Rust UB ([`interpreter.rs:425`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). +- **Shift count is 6 bits**, `RB[58:63]`. Counts `[32, 63]` produce zero (not `RS >> (count mod 32)`): Canary emits `Select(sh & 0x20, 0, RS[32:63] >> sh)` and zero-extends the result. - **No `XER[CA]` produced.** For arithmetic shift with `XER[CA]` use [`srawx`](srawx.md). -- **`Rc=1` CR0 update truncates to 32 bits in xenia-rs.** [`interpreter.rs:428`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). Since the result has zeroed high 32 bits and zeroed sign bit (high bit of the 32-bit result is always 0 after a non-zero shift), CR0 will be `EQ` or `GT`. +- **`Rc=1` CR0 update truncates to 32 bits in Canary** (`f.UpdateCR(0, v)`). After a non-zero shift the 32-bit result's top bit is 0, so both give `EQ` or `GT`. With a count of 0 the result is `RS[32:63]` itself: if its top bit is set, spec (high word zero) says `GT` and Canary says `LT`. - **No `OE` bit.** - **`srwi` simplified mnemonic** uses [`rlwinmx`](rlwinmx.md), not this instruction. `srw` is for runtime-variable counts. diff --git a/tools/ppc-manual/alu/subfcx.md b/tools/ppc-manual/alu/subfcx.md index c2b1fe8e..f68ce57d 100644 --- a/tools/ppc-manual/alu/subfcx.md +++ b/tools/ppc-manual/alu/subfcx.md @@ -119,9 +119,9 @@ int InstrEmit_subfcx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **`RT ← RB − RA` with `XER[CA]` set on no-borrow.** Same operand-order convention as [`subfx`](subfx.md): the *first* source is subtracted *from* the second. -- **`XER[CA] = 1` means *no borrow occurred*** — i.e. `RB >= RA` as unsigned. PowerISA encodes this as the carry-out of `~RA + RB + 1`, not as a borrow flag. Xenia's `if rb >= ra` test ([`interpreter.rs:157`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)) is the correct boolean encoding. -- **No trap on signed overflow.** `subfco` / `subfco.` set `XER[OV]` and sticky `XER[SO]`; xenia-rs leaves the `OE` arm unimplemented. -- **64-bit CR update on Xenon, 32-bit in xenia-rs.** [`interpreter.rs:160`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) truncates with `as i32 as i64`. Spec demands a full 64-bit signed compare for `subfc.`. +- **`XER[CA] = 1` means *no borrow occurred*** — i.e. `RB >= RA` as unsigned. PowerISA encodes this as the carry-out of `~RA + RB + 1`, not as a borrow flag. Canary's `SubDidCarry` computes it on the low 32 bits: `RB[32:63] >= RA[32:63]` unsigned. +- **No trap on signed overflow.** `subfco` / `subfco.` set `XER[OV]` and sticky `XER[SO]`. Canary's `OE` branch is `XEINSTRNOTIMPLEMENTED()` and does not store `CA` either. +- **64-bit CR update on Xenon, 32-bit in Canary** (`f.UpdateCR(0, v)`). Spec demands a full 64-bit signed compare for `subfc.`. - **Seeds a multi-word subtract chain.** Use as the low-word op; continue with [`subfex`](subfex.md) for middle words and [`subfmex`](subfmex.md)/[`subfzex`](subfzex.md) for the high word. - **Operand aliasing fine.** `subfc r3, r3, r3` always yields `0` with `CA = 1`. diff --git a/tools/ppc-manual/alu/subfex.md b/tools/ppc-manual/alu/subfex.md index 14277082..87a84742 100644 --- a/tools/ppc-manual/alu/subfex.md +++ b/tools/ppc-manual/alu/subfex.md @@ -119,9 +119,9 @@ int InstrEmit_subfex(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **`RT ← ~RA + RB + XER[CA]`.** The middle link of a multi-word subtract chain seeded by [`subfcx`](subfcx.md). `XER[CA]` propagates the borrow from the previous word. -- **Carry-out predicate handles the boundary case.** Xenia computes `CA' = (rb > ra) || (rb == ra && CA != 0)` ([`interpreter.rs:170`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). The second clause covers when `RB == RA` and the previous chain added a `+1` from the input carry — without it, the carry-out would be wrong. -- **`OE=1`** should set `XER[OV]` on signed overflow; xenia-rs ignores it. -- **64-bit CR update on Xenon, 32-bit in xenia-rs.** [`interpreter.rs:173`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). +- **Carry-out predicate handles the boundary case.** `CA' = (RB > RA) || (RB == RA && CA != 0)`. The second clause covers `RB == RA` with an incoming carry supplying the `+1` — without it, the carry-out would be wrong. Canary evaluates it on the low word via `AddWithCarryDidCarry(~RA, RB, CA)`. +- **`OE=1`** should set `XER[OV]` on signed overflow; Canary's `OE` branch is `XEINSTRNOTIMPLEMENTED()` and does not store `CA`. +- **64-bit CR update on Xenon, 32-bit in Canary** (`f.UpdateCR(0, v)`). - **`XER[CA]` must be initialised** (typically by [`subfcx`](subfcx.md)). Reading stale `CA` is a frequent multi-word-subtract bug. - **Symmetry with [`addex`](addex.md).** `subfe RT, RA, RB` ≡ `adde RT, ~RA, RB` (with the implicit complement). diff --git a/tools/ppc-manual/alu/subficx.md b/tools/ppc-manual/alu/subficx.md index 2ecff5d6..b182d237 100644 --- a/tools/ppc-manual/alu/subficx.md +++ b/tools/ppc-manual/alu/subficx.md @@ -108,7 +108,7 @@ int InstrEmit_subficx(PPCHIRBuilder& f, const InstrData& i) { - **`RT ← SIMM − RA` with `XER[CA]` always set.** Note the operand order: the *immediate* is the minuend, not the subtrahend. `subfic rD, rA, 1` computes `1 - rA`, useful for negation-plus-one or one's-complement-style operations. - **Immediate is sign-extended** to 64 bits before the subtract. So `subfic rD, rA, -1` computes `-1 - rA`, equivalent to `~rA`. -- **`XER[CA] = 1` when `SIMM >= RA`** (no borrow). Computed in xenia as `if imm >= ra` ([`interpreter.rs:73`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)) — comparing the sign-extended unsigned representations. +- **`XER[CA] = 1` when `EXTS(SIMM) >= RA`** as unsigned (no borrow). Canary's `SubDidCarry` compares the low 32 bits: `EXTS(SIMM)[32:63] >= RA[32:63]`. - **No `Rc`, no `OE`.** This D-form has no flag bits beyond the implicit `CA` write. - **No record-form variant.** There is no `subfic.` in the ISA; if you need CR0 to also reflect the result, follow with a `cmpwi`. - **Synthesised "subtract immediate"**. Assemblers sometimes accept `subi rD, rA, value` as a shorthand for `addi rD, rA, -value`, but for the carry-producing variant you must use `subfic` explicitly. diff --git a/tools/ppc-manual/alu/subfmex.md b/tools/ppc-manual/alu/subfmex.md index 199ac907..6bfbba7f 100644 --- a/tools/ppc-manual/alu/subfmex.md +++ b/tools/ppc-manual/alu/subfmex.md @@ -124,9 +124,9 @@ int InstrEmit_subfmex(PPCHIRBuilder& f, const InstrData& i) { - **`RT ← ~RA + (−1) + XER[CA]` ≡ `~RA − 1 + CA`.** Terminator for a multi-word subtract chain when the high "minuend" word is implicitly all-ones (e.g. when computing `~x` style negation across many words). - **`RB` field unused.** XO-form but only `RA` is read. -- **Carry-out predicate.** `CA' = (~RA != 0) || (CA != 0)` ([`interpreter.rs:191`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). Only when `RA == ~0` (all ones) AND `CA == 0` does `CA'` become 0 — every other case produces no borrow on this final word. -- **`OE=1`** should set `XER[OV]` on signed overflow; xenia-rs ignores it. -- **64-bit CR update on Xenon, 32-bit in xenia-rs.** [`interpreter.rs:194`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). +- **Carry-out predicate.** `CA' = (~RA != 0) || (CA != 0)`. Only when `RA == ~0` (all ones) AND `CA == 0` does `CA'` become 0 — every other case produces no borrow on this final word. Canary evaluates it on the low word (`AddWithCarryDidCarry`). +- **`OE=1`** should set `XER[OV]` on signed overflow; Canary's `OE` branch is `XEINSTRNOTIMPLEMENTED()` and does not store `CA`. +- **64-bit CR update on Xenon, 32-bit in Canary** (`f.UpdateCR(0, v)`). - **`XER[CA]` must be initialised** (typically by a [`subfcx`](subfcx.md) or [`subfex`](subfex.md) earlier in the chain). - **Symmetric with [`addmex`](addmex.md)**, the add-side terminator. diff --git a/tools/ppc-manual/alu/subfx.md b/tools/ppc-manual/alu/subfx.md index 2d8de523..7cde3140 100644 --- a/tools/ppc-manual/alu/subfx.md +++ b/tools/ppc-manual/alu/subfx.md @@ -126,11 +126,11 @@ if Rc then ## Special Cases & Edge Conditions - **Operand order gotcha.** `subf RT, RA, RB` computes `RT ← RB − RA`, **not** `RA − RB`. This reverses the intuitive ordering seen in x86/ARM. The assembler exposes a simplified mnemonic `sub RT, RX, RY` ≡ `subf RT, RY, RX` that restores the natural order — watch for both forms in disassembly. -- **Implemented as add-with-complement.** Hardware (and xenia) compute `~RA + RB + 1`. All overflow/CR semantics are the same as [`addx`](addx.md) with one operand complemented. +- **Implemented as add-with-complement.** Hardware computes `~RA + RB + 1`; Canary emits the equivalent `RB − RA`. All overflow/CR semantics are the same as [`addx`](addx.md) with one operand complemented. - **No `XER[CA]` update** — use [`subfcx`](subfcx.md) if you need a borrow-out bit. - **No trap on overflow.** `subfo` / `subfo.` only record the event in `XER[OV]` and sticky-set `XER[SO]`. - **Signed-overflow predicate.** `OV = ((RA ^ RB) & (RB ^ RT)) >> 63` — set when operands have different signs and the result's sign differs from `RB`'s. -- **64-bit CR update on Xenon** (xenia-rs truncates to 32 bits; see [`addx`](addx.md) note). +- **64-bit CR update on Xenon** (Canary truncates to 32 bits via `f.UpdateCR(0, v)`; see the [`addx`](addx.md) note). ## Related Instructions diff --git a/tools/ppc-manual/alu/subfzex.md b/tools/ppc-manual/alu/subfzex.md index facd3ed9..3665ec04 100644 --- a/tools/ppc-manual/alu/subfzex.md +++ b/tools/ppc-manual/alu/subfzex.md @@ -123,9 +123,9 @@ int InstrEmit_subfzex(PPCHIRBuilder& f, const InstrData& i) { - **`RT ← ~RA + 0 + XER[CA]` ≡ `~RA + CA`.** The subtract-side high-word terminator for a multi-word subtract chain. Implements `0 - (...) - borrow` for the high word. - **`RB` field unused.** -- **Carry-out predicate.** `CA' = (~RA != 0) || (CA != 0)` ([`interpreter.rs:180`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). Only `RA == ~0 && CA == 0` produces `CA' = 0`; every other case gives no-borrow. -- **`OE=1`** should set `XER[OV]` on signed overflow; xenia-rs ignores. -- **64-bit CR update on Xenon, 32-bit in xenia-rs.** [`interpreter.rs:183`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). +- **Carry-out predicate.** `RT = ~RA + CA`, which carries out only when `~RA` is all ones and `CA = 1`: `CA' = (RA == 0) && (CA != 0)`. (This page previously gave `(~RA != 0) || CA`, which is [`subfmex`](subfmex.md)'s predicate.) Canary evaluates it on the low word via `AddWithCarryDidCarry(~RA, 0, CA)`. +- **`OE=1`** should set `XER[OV]` on signed overflow; Canary's `OE` branch is `XEINSTRNOTIMPLEMENTED()` and does not store `CA`. +- **64-bit CR update on Xenon, 32-bit in Canary** (`f.UpdateCR(0, v)`). - **`XER[CA]` must be initialised** by an earlier carrying instruction. - **Common idiom: extracting `XER[CA]` as 0/-1.** `subfze rT, rN` (where `rN == 0`) materialises `XER[CA]` to `0` or `-1` (`-1 = ~0 + CA = -1 + CA`); pair with [`addzex`](addzex.md) for `0/1` instead. diff --git a/tools/ppc-manual/alu/sync.md b/tools/ppc-manual/alu/sync.md index af78a5f0..a134e986 100644 --- a/tools/ppc-manual/alu/sync.md +++ b/tools/ppc-manual/alu/sync.md @@ -99,7 +99,7 @@ int InstrEmit_sync(PPCHIRBuilder& f, const InstrData& i) { - **`L` field selects sync class.** `L=0` is full *hwsync* (the default). `L=1` is `lwsync` — orders only loads-after-loads, loads-after-stores, and stores-after-stores (not stores-after-loads). The Xenon implements both via the same encoding with `L` (bit 9) selecting variant. Most disassembly shows the unsuffixed `sync` mnemonic, which assembles to `L=0`. - **No register or CR effects.** Pure ordering primitive. - **Used to implement release semantics.** A typical lock-release sequence is `sync; stw r0, lock`. Acquire side uses `lwsync` after the load. -- **Xenia-rs is a no-op.** [`interpreter.rs:1267`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) collapses `sync`, `eieio`, `isync` into PC-advance. Since xenia is single-threaded interpretation, host program order subsumes all PPC ordering. +- **Canary emits `f.MemoryBarrier()`**, as for `eieio`; `isync` is a plain `f.Nop()`. A sequential C translation needs neither: host program order subsumes the PPC ordering. - **Distinct from [`isync`](isync.md)**, which orders the *instruction* stream — `sync` does not refetch instructions. - **Slow on real hardware.** Hundreds of cycles when the store queue is full; hot paths avoid `sync` and use `lwsync` or no barrier when only single-thread ordering is needed. diff --git a/tools/ppc-manual/alu/xori.md b/tools/ppc-manual/alu/xori.md index ed0cc7fa..797c53c1 100644 --- a/tools/ppc-manual/alu/xori.md +++ b/tools/ppc-manual/alu/xori.md @@ -101,7 +101,7 @@ int InstrEmit_xori(PPCHIRBuilder& f, const InstrData& i) { - **No record form.** Like [`ori`](ori.md), there is no `xori.`. To get a CR0 update follow with `cmpwi` or use [`xorx`](xorx.md) with `Rc=1`. - **Immediate is zero-extended** to 64 bits. Only the low 16 bits of `RA` can be flipped; the high 48 bits are passed through from `RS` unchanged. - **`xori 0, 0, 0` is a valid NOP encoding** but the canonical NOP is `ori 0, 0, 0`. Disassemblers should still display this as `xori r0, r0, 0` or recognise it as a no-op. -- **64-bit operation in xenia-rs.** [`interpreter.rs:338`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) — full `u64` XOR with the immediate. +- **64-bit operation in Canary.** `f.Xor(RS, EXTZ16(UI))` on the full `u64`. - **No `XER`, no `CR`** side effects. - **`RA = 0` reads `r0`** (not literal zero); see [`ori`](ori.md). - **Useful for masked toggle.** `xori rA, rS, mask` flips the bits of `rS` indicated by `mask` (low 16 bits only). diff --git a/tools/ppc-manual/alu/xoris.md b/tools/ppc-manual/alu/xoris.md index d2c66947..df2c6d9e 100644 --- a/tools/ppc-manual/alu/xoris.md +++ b/tools/ppc-manual/alu/xoris.md @@ -102,7 +102,7 @@ int InstrEmit_xoris(PPCHIRBuilder& f, const InstrData& i) { - **No record form.** Like all immediate logicals other than `andi.`/`andis.`, `xoris` does not update CR0. - **Immediate is zero-extended *then* shifted left 16.** Only bits 32–47 of `RA` (PowerISA bit numbering) can be flipped; the high 32 bits and low 16 bits of `RA` come from `RS` unchanged. - **Common pattern with [`xori`](xori.md)** to flip arbitrary 32-bit bitmasks: `xoris RA, RS, hi16; xori RA, RA, lo16`. -- **64-bit operation.** [`interpreter.rs:342`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). +- **64-bit operation.** Canary: `f.Xor(RS, EXTZ16(UI) << 16)` on the full `u64`. - **No `XER`, no `CR`.** - **`RA = 0` reads `r0`** (not literal zero). - **Used to toggle the high half of a 32-bit word**, e.g. `xoris r3, r3, 0x8000` flips bit 32 (the sign bit of the low word) — a one-instruction sign-flip on a 32-bit value. diff --git a/tools/ppc-manual/alu/xorx.md b/tools/ppc-manual/alu/xorx.md index 85792516..6d204e42 100644 --- a/tools/ppc-manual/alu/xorx.md +++ b/tools/ppc-manual/alu/xorx.md @@ -110,7 +110,7 @@ int InstrEmit_xorx(PPCHIRBuilder& f, const InstrData& i) { - **Operand convention** is X-form (`RA` destination, `RS`/`RB` sources). - **64-bit operation** on Xenon. - **No `OE` or `XER` side effects.** Only `Rc=1` updates `CR0`. -- **64-bit CR update on Xenon, 32-bit in xenia-rs.** [`interpreter.rs:367`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs) truncates with `as i32 as i64`. For `xor.` whose result has differing high/low halves, spec and xenia diverge; `xor. RA, RS, RS` gives `EQ` either way. +- **64-bit CR update on Xenon, 32-bit in Canary** (`f.UpdateCR(0, v)`). For `xor.` whose result has differing high/low halves, spec and Canary diverge; `xor. RA, RS, RS` gives `EQ` either way. - **Useful as bitmask toggle.** `xor r3, r3, r4` flips in `r3` every bit set in `r4`. - **No `XER[CA]`.** diff --git a/tools/ppc-manual/branch/bcctrx.md b/tools/ppc-manual/branch/bcctrx.md index 571b3abe..41d6875c 100644 --- a/tools/ppc-manual/branch/bcctrx.md +++ b/tools/ppc-manual/branch/bcctrx.md @@ -131,7 +131,7 @@ int InstrEmit_bcctrx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **No CTR decrement.** Unlike [`bcx`](bcx.md) and [`bclrx`](bclrx.md), `bcctr` cannot decrement CTR (the CTR is the *target*). The PowerISA reserves `BO[2] = 0` encodings — they are *invalid* on `bcctrx`. xenia silently ignores `BO[2]`/`BO[3]` and treats every `bcctr` as a pure CR-conditional branch, which matches both the canary emit and real Xenon hardware behaviour. +- **No CTR decrement.** Unlike [`bcx`](bcx.md) and [`bclrx`](bclrx.md), `bcctr` cannot decrement CTR (the CTR is the *target*). The PowerISA reserves `BO[2] = 0` encodings — they are *invalid* on `bcctrx`. Canary ignores `BO[2]`/`BO[3]` and treats every `bcctr` as a pure CR-conditional branch. - **CTR alignment mask.** The target is `CTR & ~3`. Like `bclr`, the low two bits are stripped — a misaligned CTR is silently rounded down rather than trapping. - **BO encoding (CR-only subset).** Because CTR-test bits are unused, only four `BO` patterns are meaningful: @@ -142,9 +142,9 @@ int InstrEmit_bcctrx(PPCHIRBuilder& f, const InstrData& i) { | `1z1zz` | branch always (`bctr`) | | `0000z`/`001at`/etc. | reserved — implementation-defined | -- **Indirect call/dispatch idiom.** `mtctr rN; bctrl` is the canonical PPC indirect call: load function pointer into CTR, call. The xenia interpreter writes `LR ← CIA + 4` only when the branch is taken — this matches the PowerISA, but contrast with `bcx` where `LK` always writes LR (even if the branch is not taken). The C-translation reference handles this asymmetry explicitly. -- **`bctr` for switch tables.** Compilers emit `bctr` (not `bctrl`) for jump-table dispatch, with CTR loaded from a base + (index*4) lookup. Xenia honours this by simply jumping to `CTR & ~3`. -- **Synchronisation.** Marked `sync` in xenia's XML — context-synchronising. JIT backends must ensure prior side effects have committed before the indirect transfer. +- **Indirect call/dispatch idiom.** `mtctr rN; bctrl` is the canonical PPC indirect call: load function pointer into CTR, call. With `LK=1`, PowerISA writes `LR ← CIA + 4` whether or not the branch is taken, and Canary does the same — its `InstrEmit_branch` stores `LR` before the conditional transfer, exactly as for [`bcx`](bcx.md). +- **`bctr` for switch tables.** Compilers emit `bctr` (not `bctrl`) for jump-table dispatch, with CTR loaded from a base + (index*4) lookup. Canary emits an indirect call to the `CTR` value (`CallIndirect`). +- **Synchronisation.** Marked `sync` in Canary's `tools/ppc-instructions.xml` — context-synchronising. JIT backends must ensure prior side effects have committed before the indirect transfer. - **No prediction hint sensitivity.** Xenon predicts indirect branches via a separate target cache; the `BO[4]` hint is mostly cosmetic for `bcctr`. ## Related Instructions diff --git a/tools/ppc-manual/branch/bclrx.md b/tools/ppc-manual/branch/bclrx.md index 562ef1fb..dd4de547 100644 --- a/tools/ppc-manual/branch/bclrx.md +++ b/tools/ppc-manual/branch/bclrx.md @@ -187,10 +187,10 @@ The most common bclr instance in Xbox 360 disassembly is `BO = 0b10100` → `blr - **LR alignment mask.** The target address is `LR & ~3` — the low 2 bits are cleared. This silently ignores a misaligned LR; incoming code should always produce 4-byte-aligned LR values. - **Ordering of CTR decrement and branch.** The CTR is decremented **first**, then compared to zero **after** the decrement. So after `bdnz` at `CTR = 1`, the CTR becomes `0` and the branch is not taken. -- **Self-referential LR write.** `bclrl` writes `LR ← CIA + 4` **before** reading `LR` to set `NIA`. Per the PowerISA, `bclrl` reads the *old* `LR` for the branch target and writes the *new* `LR` with the return address, atomically from software's perspective. Xenia implements it this way (`next_pc` captured first, then `lr` written). +- **Self-referential LR write.** `bclrl` writes `LR ← CIA + 4` **before** reading `LR` to set `NIA`. Per the PowerISA, `bclrl` reads the *old* `LR` for the branch target and writes the *new* `LR` with the return address, atomically from software's perspective. Canary implements it this way: `InstrEmit_bclrx` loads `LR` as the target before `InstrEmit_branch` stores `CIA + 4`. - **Branch prediction hints (`BO[4]`).** The Xenon does static prediction on the basis of these hints, but behaviour is architecturally unobservable. Translators may ignore them. -- **Synchronisation.** `bclr` is **context-synchronising** (hence the `sync` flag in xenia's XML). Translators must ensure side-effecting instructions preceding the branch have committed — trivial in a sequential C translation but relevant for JIT backends. -- **xenia's `LR_HALT_SENTINEL`.** Xenia sets `LR` to `0xBCBCBCBC` at thread start; when the top-level guest function returns via `blr`, the interpreter loop halts cleanly. Translators replicating guest behaviour don't need this — but if you generate a test harness, the sentinel is a convenient "function returned" signal. +- **Synchronisation.** `bclr` is **context-synchronising** (hence the `sync` flag in Canary's `tools/ppc-instructions.xml`). Translators must ensure side-effecting instructions preceding the branch have committed — trivial in a sequential C translation but relevant for JIT backends. +- **Canary's `0xBCBCBCBC` return sentinel.** When the host calls into a guest function, Canary's `Processor::Execute` sets `LR` to `0xBCBCBCBC`, so the top-level `blr` hands control back to the host. Translators replicating guest behaviour don't need this — but if you generate a test harness, the sentinel is a convenient "function returned" signal. ## Related Instructions diff --git a/tools/ppc-manual/branch/bcx.md b/tools/ppc-manual/branch/bcx.md index cb0f3880..ed27afe3 100644 --- a/tools/ppc-manual/branch/bcx.md +++ b/tools/ppc-manual/branch/bcx.md @@ -183,7 +183,7 @@ int InstrEmit_bcx(PPCHIRBuilder& f, const InstrData& i) { - **14-bit signed displacement.** `BD` is a 14-bit signed word-count, scaled by 4 — yielding a ±32 KiB byte range (`−2^15 … +2^15 − 4`). For longer-range conditional control flow, compilers emit a short `bc` over an unconditional `b`. - **CTR decrement happens before the test.** `BO[2]=0` decrements CTR *first*, then `ctr_ok` evaluates against the new value. The classic `bdnz loop` loops `N` times when CTR is initialised to `N`. -- **LR write is unconditional in xenia.** Xenia writes `LR ← CIA + 4` whenever `LK=1`, even on the not-taken path. This matches the PowerISA: `bcl` always sets `LR` regardless of branch outcome — exploited by `bcl 20, 31, $+4` as a self-PC capture (PIC trick). +- **LR write is unconditional in Canary.** Canary writes `LR ← CIA + 4` whenever `LK=1`, even on the not-taken path. This matches the PowerISA: `bcl` always sets `LR` regardless of branch outcome — exploited by `bcl 20, 31, $+4` as a self-PC capture (PIC trick). - **`BO` encoding** — see `bclrx.md` for the full 5-bit table. `bcx` supports the full set, including CTR-only branches (`bdnz`, `bdz`). - **Branch hint encoding.** PPC overloads `BO[4]` as a static prediction hint: 0 = "predict not taken", 1 = "predict taken". The Xenon honours it for forward branches; backwards conditional branches are predicted taken regardless. Translators may ignore the hint. - **Synchronisation.** Marked `sync` — like all branches, `bcx` is context-synchronising. Trivial in interpretation; matters for JIT reorder windows. diff --git a/tools/ppc-manual/branch/sc.md b/tools/ppc-manual/branch/sc.md index 4aa42533..050b9e38 100644 --- a/tools/ppc-manual/branch/sc.md +++ b/tools/ppc-manual/branch/sc.md @@ -113,9 +113,9 @@ int InstrEmit_sc(PPCHIRBuilder& f, const InstrData& i) { - `LEV = 2` — **hypervisor syscall** (`HVcall`). On the Xenon, `sc 2` traps to the Xbox 360 hypervisor; this is how the kernel itself talks to the supervisor below it (e.g., for security operations, encrypted-memory accesses, page table updates). - **`sc` as written by titles.** Almost all guest game code uses `LEV = 0` to call `XboxKrnl.exe`. Game disassembly will show large jump tables of small thunks each ending in `li r0, syscall_no; sc; blr`. - **No condition or status side effects.** `sc` updates *no* general-purpose register on entry — neither LR nor CR. The kernel sees the GPR/FPR snapshot as-is and reads the syscall number out of `r0` (Xbox 360 ABI convention, not architectural). -- **Return path.** Hardware returns from `sc` via [`rfid`](../control/mtmsrd.md)-class instructions in the kernel handler; from the application's perspective execution resumes at `CIA + 4`. Xenia's interpreter realises this by simply pre-incrementing `pc` then returning `StepResult::SystemCall` — the host driver dispatches the syscall and re-enters the loop. -- **xenia divergence vs hardware.** xenia-rs *does not* model the `0xC00` exception vector or save SRR0/SRR1; the `LEV` operand is currently ignored. All `sc` instructions are treated identically and serviced by the host. This is sufficient because Xbox 360 titles don't observe SRR registers and the host kernel is implemented natively. -- **Synchronisation.** Marked `sync` in xenia's XML — `sc` is context-synchronising (hardware completes all prior instructions before raising the exception). JITs must flush pending state before emitting the host call. +- **Return path.** Hardware returns from `sc` via [`rfid`](../control/mtmsrd.md)-class instructions in the kernel handler; from the application's perspective execution resumes at `CIA + 4`. Canary emits `sc` as a host call (`CallExtern` of its syscall handler), after which the translated code simply continues at `CIA + 4`. +- **Canary divergence vs hardware.** Canary does *not* model the `0xC00` exception vector or save SRR0/SRR1. `LEV=0` calls its syscall handler, `LEV=2` is Canary's own marker for an import call, and any other `LEV` is unimplemented. This is sufficient because Xbox 360 titles don't observe SRR registers and Canary implements the kernel natively. +- **Synchronisation.** Marked `sync` in Canary's `tools/ppc-instructions.xml` — `sc` is context-synchronising (hardware completes all prior instructions before raising the exception). JITs must flush pending state before emitting the host call. - **Reserved bits.** Bit 30 is fixed `1`; bits 6–19 and 27–29 are reserved (must be 0). The 1-bit field at position 30 distinguishes the `sc` encoding from `scv` (later PowerISA addition, not present on the Xenon). ## Related Instructions diff --git a/tools/ppc-manual/branch/td.md b/tools/ppc-manual/branch/td.md index 8ae2232d..f4f8cb4d 100644 --- a/tools/ppc-manual/branch/td.md +++ b/tools/ppc-manual/branch/td.md @@ -128,8 +128,8 @@ int InstrEmit_td(PPCHIRBuilder& f, const InstrData& i) { - **64-bit comparison.** Unlike [`tw`](tw.md), `td` always compares the full 64-bit GPRs. On the Xenon (64-bit) this is meaningful; PPC32 implementations don't have `td`. - **No register effects.** Only the side effect is the trap. No CR/LR/CTR/XER updates. - **Hardware behaviour.** When the trap fires, hardware raises a Program interrupt with `SRR1[TRAP]` set and vectors to `0x700`. The Xbox 360 hypervisor / kernel handles it (assertion failure, debugger trap, etc.). -- **xenia simplification.** xenia-rs collapses all four trap variants (`td`, `tdi`, `tw`, `twi`) into one match arm that *unconditionally* logs and returns `StepResult::Trap` — it does **not** evaluate `TO` against the operands. This is a material divergence from the spec: in xenia every trap fires even if the condition is false. Real Xenon code rarely uses non-trivial `TO` masks (typical use is the unconditional `trap` for `__assert` / debugger break), so the divergence is normally invisible. -- **Distinguishing assert vs. break.** Compilers commonly emit `tdne r3, r3` (impossible) or `tdi 0, r0, 0` patterns that *cannot* trap as inert markers. Xenia's blanket trap would mis-fire on these — a small known bug; track it if you see spurious traps. +- **Canary evaluates the condition.** Canary evaluates `TO` properly: it compares the operands for each set bit and emits a conditional trap (`TrapTrue`), and `TO = 0` emits nothing. The `ignore_trap_instructions` cvar suppresses traps entirely. +- **Distinguishing assert vs. break.** Compilers commonly emit `tdne r3, r3` (impossible) or `tdi 0, r0, 0` patterns that *cannot* trap as inert markers. Canary evaluates `TO`, so these stay inert. ## Related Instructions diff --git a/tools/ppc-manual/branch/tdi.md b/tools/ppc-manual/branch/tdi.md index c5ad1b9e..1575f50e 100644 --- a/tools/ppc-manual/branch/tdi.md +++ b/tools/ppc-manual/branch/tdi.md @@ -116,7 +116,7 @@ int InstrEmit_tdi(PPCHIRBuilder& f, const InstrData& i) { - **`TO = 31` is unconditional.** `tdi 31, 0, 0` is a debugger / assert trap. Compilers sometimes use it as a "should not reach" marker. - **64-bit comparison only.** Unlike [`twi`](twi.md), `tdi` always compares the full 64-bit GPR — it has no PPC32 analogue. The Xenon's PPC64 mode makes this meaningful. - **No register effects.** Pure side effect on success: Program interrupt → vector `0x700` with `SRR1[TRAP]=1`. -- **xenia simplification.** xenia-rs unconditionally treats `tdi` as a fired trap, regardless of `TO`/`RA`/`SIMM` values. This diverges from the spec — real hardware would silently fall through when no `TO` bit's condition holds. Most title code uses only the unconditional `trap` form, so the divergence is normally invisible; non-firing assertion patterns (e.g. `tdi 0, r0, 0`) will mis-fire under xenia. +- **Canary evaluates the condition.** Canary evaluates `TO` properly: it compares the operands for each set bit and emits a conditional trap (`TrapTrue`), and `TO = 0` emits nothing. So `tdi 0, r0, 0` falls through, as on hardware. - **Reserved bits.** Bits 6–10 carry the `TO` field; there is no `Rc` / `OE` on D-form trap immediates. ## Related Instructions diff --git a/tools/ppc-manual/branch/tw.md b/tools/ppc-manual/branch/tw.md index 13061d87..82a046fc 100644 --- a/tools/ppc-manual/branch/tw.md +++ b/tools/ppc-manual/branch/tw.md @@ -130,8 +130,8 @@ int InstrEmit_tw(PPCHIRBuilder& f, const InstrData& i) { - **`tw 31, 0, 0` is `trap`.** The simplified mnemonic `trap` expands to `tw 31, r0, r0` — all five `TO` bits set ⇒ unconditional trap. Compilers and the kernel use this as the assertion / debugger break primitive; it appears as `0x7FE00008` in raw bytes. - **Conditional asserts.** GCC's `__builtin_trap` and MSVC's `__assert` macros emit `tw` variants like `twge`/`twlt` to fault on bound-check failures. - **No register effects.** Side effect only: Program interrupt (`0x700`) with `SRR1[TRAP]=1`. -- **xenia simplification.** xenia-rs collapses `td/tdi/tw/twi` into a single arm that *unconditionally* logs and returns `StepResult::Trap` — the `TO` operand is **not evaluated**. Real hardware would silently fall through when no `TO` bit's condition holds. In practice titles use mostly the unconditional `trap`, so the divergence rarely manifests, but inert-marker patterns like `tw 0, r0, r0` will fire under xenia. -- **Inert encoding.** `tw 0, r0, r0` (no `TO` bits set) can never trap on real hardware. It encodes as `0x7C000008` — sometimes used as a structured-NOP marker. Watch for it in xenia traces. +- **Canary evaluates the condition.** Canary evaluates `TO` properly: it compares the operands for each set bit and emits a conditional trap (`TrapTrue`), and `TO = 0` emits nothing. `tw` compares the sign-extended low 32 bits of both operands, and inert markers like `tw 0, r0, r0` fall through. +- **Inert encoding.** `tw 0, r0, r0` (no `TO` bits set) can never trap on real hardware, and Canary emits nothing for `TO = 0`. It encodes as `0x7C000008` — sometimes used as a structured-NOP marker. ## Related Instructions diff --git a/tools/ppc-manual/branch/twi.md b/tools/ppc-manual/branch/twi.md index f51d3e12..9cee3c77 100644 --- a/tools/ppc-manual/branch/twi.md +++ b/tools/ppc-manual/branch/twi.md @@ -123,7 +123,7 @@ int InstrEmit_twi(PPCHIRBuilder& f, const InstrData& i) { - **`twi 31, 0, 0` is unconditional trap.** All `TO` bits set ⇒ guaranteed trap. The simplified mnemonic family (`twnei`, `twgei`, …) is much more common in real code: bound checks, null checks, integer-divide-by-zero pre-checks. - **Compiler usage.** Xbox 360 GCC emits `twnei rN, -1` and similar to validate handle-style return values; the kernel handler turns the trap into an exception delivered to the title. - **No register effects.** Side effect: Program interrupt → vector `0x700` with `SRR1[TRAP]=1`. -- **xenia simplification.** Same as the other three trap forms — xenia-rs unconditionally returns `StepResult::Trap` whenever it decodes any of `tdi`/`twi`/`td`/`tw`, regardless of the `TO` mask or operands. This means `twi 0, r0, 0` (architecturally a guaranteed-no-trap encoding) will spuriously fire under xenia. Keep this in mind when triaging unexpected trap signals. +- **Canary evaluates the condition.** Canary evaluates `TO` properly: it compares the operands for each set bit and emits a conditional trap (`TrapTrue`), and `TO = 0` emits nothing. `twi 0, r0, 0` therefore falls through. One special case: `twi 31, r0, ` (`TO = 31`, `RA = 0`) becomes an unconditional `Trap` carrying `` as its type. - **No `Rc` / `OE`.** D-form trap immediates have neither. ## Related Instructions diff --git a/tools/ppc-manual/control/crand.md b/tools/ppc-manual/control/crand.md index 9b8e3cbf..17fa9f05 100644 --- a/tools/ppc-manual/control/crand.md +++ b/tools/ppc-manual/control/crand.md @@ -113,7 +113,7 @@ int InstrEmit_crand(PPCHIRBuilder& f, const InstrData& i) { - **Combining branch conditions.** The classic use: synthesise complex branch conditions from multiple compare results. Example: `cmpw cr0, r3, r4; cmpw cr1, r5, r6; crand 4*cr0+2, 4*cr0+2, 4*cr1+2; beq cr0, label` branches if `r3==r4 AND r5==r6` using a single conditional branch. - **No `Rc` / `OE`.** XL-form CR-logical ops never set CR0 or XER; they only update the named CR bit. - **Not synchronising.** Pure data-flow on CR; freely reorderable. -- **xenia status.** xenia-rs decodes `crand` (decoder slot 540) but the interpreter snapshot is not embedded on this page — implementation lives in [`crates/xenia-cpu/src/interpreter.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs). xenia-canary's `InstrEmit_crand` emits the equivalent host AND of the two CR bits. +- **Canary status.** Canary's `InstrEmit_crand` emits a host AND of the two CR bits. ## Related Instructions diff --git a/tools/ppc-manual/control/crandc.md b/tools/ppc-manual/control/crandc.md index d69f6d1d..945f27da 100644 --- a/tools/ppc-manual/control/crandc.md +++ b/tools/ppc-manual/control/crandc.md @@ -113,7 +113,7 @@ int InstrEmit_crandc(PPCHIRBuilder& f, const InstrData& i) { - **Use case.** Synthesises "branch if A *and not* B" predicates without a dedicated `cmp` of `B`. Example: branch only if `cr0.EQ` *and not* `cr1.SO` — `crandc 2, 2, 7` then `beq` on `cr0`. - **No `Rc` / `OE`.** Pure CR-bit dataflow; doesn't update CR0 or XER. - **Not synchronising.** Reorderable. -- **xenia status.** Interpreter handles via the generic CR-logical helper. xenia-canary's `InstrEmit_crandc` emits a host AND of `A` and bitwise-NOT of `B`. +- **Canary status.** Canary's `InstrEmit_crandc` emits a host AND of `A` and bitwise-NOT of `B`. ## Related Instructions diff --git a/tools/ppc-manual/control/creqv.md b/tools/ppc-manual/control/creqv.md index 70b9dc97..06acc2b0 100644 --- a/tools/ppc-manual/control/creqv.md +++ b/tools/ppc-manual/control/creqv.md @@ -113,7 +113,7 @@ int InstrEmit_creqv(PPCHIRBuilder& f, const InstrData& i) { - **Use case.** Branch on "A == B" of two prior compare results. Example: `crxor` of CR0.SO and CR1.SO gives "differ"; `creqv` gives "agree". - **No `Rc` / `OE`.** Doesn't touch CR0, XER, or any other state beyond the named bit. - **Not synchronising.** Reorderable. -- **xenia status.** Interpreter dispatches through the generic CR-logical helper; canary emits the host XNOR equivalent. The `crset` simplified form is the most common occurrence in real Xbox 360 code. +- **Canary status.** Canary emits the host equality compare of the two bits (XNOR). The `crset` simplified form is the most common occurrence in real Xbox 360 code. ## Related Instructions diff --git a/tools/ppc-manual/control/crnand.md b/tools/ppc-manual/control/crnand.md index 74777ce3..662460ef 100644 --- a/tools/ppc-manual/control/crnand.md +++ b/tools/ppc-manual/control/crnand.md @@ -113,7 +113,7 @@ int InstrEmit_crnand(PPCHIRBuilder& f, const InstrData& i) { - **Use case.** Branch on "NOT (A AND B)". Less common than the De Morgan equivalent (`cror BT, ¬A, ¬B`), but saves an extra `crnot` step. - **No `Rc` / `OE`.** No CR0 / XER side effects. - **Not synchronising.** Reorderable. -- **xenia status.** Decoded by the generic XL-form CR-logical handler; the interpreter snapshot is shared with `crand`/`cror`/etc. xenia-canary's `InstrEmit_crnand` emits a host AND followed by NOT. +- **Canary status.** Canary's `InstrEmit_crnand` emits a host AND followed by NOT. ## Related Instructions diff --git a/tools/ppc-manual/control/crnor.md b/tools/ppc-manual/control/crnor.md index 9d934ad0..d44e597e 100644 --- a/tools/ppc-manual/control/crnor.md +++ b/tools/ppc-manual/control/crnor.md @@ -113,7 +113,7 @@ int InstrEmit_crnor(PPCHIRBuilder& f, const InstrData& i) { - **Use case.** Branch on "neither A nor B"; or, with `crnot`, simply complement a CR bit before consuming it in a `bcx`. - **No `Rc` / `OE`.** Pure CR-bit dataflow; CR0/XER untouched. - **Not synchronising.** Reorderable. -- **xenia status.** Decoded via the generic CR-logical handler. xenia-canary's `InstrEmit_crnor` emits a host OR followed by NOT. +- **Canary status.** Canary's `InstrEmit_crnor` emits a host OR followed by NOT. ## Related Instructions diff --git a/tools/ppc-manual/control/cror.md b/tools/ppc-manual/control/cror.md index cb315af8..6ff5fa50 100644 --- a/tools/ppc-manual/control/cror.md +++ b/tools/ppc-manual/control/cror.md @@ -113,7 +113,7 @@ int InstrEmit_cror(PPCHIRBuilder& f, const InstrData& i) { - **Use case.** Branch on "A OR B" of two prior compare results — saves an extra branch by collapsing two conditions. - **No `Rc` / `OE`.** Pure CR-bit dataflow. - **Not synchronising.** Reorderable. -- **xenia status.** Most-used CR-logical instruction in real code (almost always as `crmove`). Decoded by the generic XL-form CR-logical handler; canary emits a host OR. +- **Canary status.** Most-used CR-logical instruction in real code (almost always as `crmove`). Canary emits a host OR. ## Related Instructions diff --git a/tools/ppc-manual/control/crorc.md b/tools/ppc-manual/control/crorc.md index 665b4209..3dcaece4 100644 --- a/tools/ppc-manual/control/crorc.md +++ b/tools/ppc-manual/control/crorc.md @@ -114,7 +114,7 @@ int InstrEmit_crorc(PPCHIRBuilder& f, const InstrData& i) { - **Use case.** Compose "if B then A" guards without a separate complement step. - **No `Rc` / `OE`.** Doesn't update CR0 or XER. - **Not synchronising.** Reorderable. -- **xenia status.** Decoded by the generic CR-logical handler; canary emits OR-with-NOT directly. +- **Canary status.** Canary emits OR-with-NOT directly. ## Related Instructions diff --git a/tools/ppc-manual/control/crxor.md b/tools/ppc-manual/control/crxor.md index 34859084..cd7b5d09 100644 --- a/tools/ppc-manual/control/crxor.md +++ b/tools/ppc-manual/control/crxor.md @@ -113,7 +113,7 @@ int InstrEmit_crxor(PPCHIRBuilder& f, const InstrData& i) { - **Use case.** Branch on "A != B"; or, with the `crclr` idiom, zero a CR bit before fall-through CR computation. - **No `Rc` / `OE`.** No CR0 / XER side effects. - **Not synchronising.** Reorderable. -- **xenia status.** Common enough in real code (typically as `crclr 6` for the variadic-FP marker) that translators often special-case the `crclr` pattern. xenia-canary's `InstrEmit_crxor` emits a host XOR; xenia-rs decodes via the generic CR-logical handler. +- **Canary status.** Common enough in real code (typically as `crclr 6` for the variadic-FP marker) that translators often special-case the `crclr` pattern. Canary's `InstrEmit_crxor` emits a host XOR of the two bits. ## Related Instructions diff --git a/tools/ppc-manual/control/mcrf.md b/tools/ppc-manual/control/mcrf.md index 2ebeab44..cab6148d 100644 --- a/tools/ppc-manual/control/mcrf.md +++ b/tools/ppc-manual/control/mcrf.md @@ -110,11 +110,11 @@ int InstrEmit_mcrf(PPCHIRBuilder& f, const InstrData& i) { - **Field-level (4-bit) move.** Unlike the bit-level CR-logical family ([`crand`](crand.md), …, [`crxor`](crxor.md)), `mcrf` copies *all four* bits of a CR field (LT, GT, EQ, SO) in one instruction. `CRFD` and `CRFS` are 3-bit field indices (0..7), each naming a 4-bit slice of the 32-bit CR. - **No source-field clobber.** The source field is read, not modified — `mcrf 0, 1` copies CR1 into CR0 leaving CR1 intact. -- **Same-field is a NOP.** `mcrf cr0, cr0` reads-then-writes the same field; xenia's interpreter still does the assignment but the architectural state is unchanged. +- **Same-field is a NOP.** `mcrf cr0, cr0` reads-then-writes the same field; Canary still emits the four bit copies but the architectural state is unchanged. - **Use case.** Promote a non-default compare result into `cr0` so a default-`cr0` simplified branch (`beq label`) can consume it without spelling out `cr1`/`cr2`/etc. The alternative — `crmove` — would require four `cror` instructions to move all four bits. - **No CR0/XER side effects.** Pure CR-field dataflow. - **Not synchronising.** Reorderable. -- **xenia exact match.** xenia-rs models the CR as an array of eight 4-bit fields, so `mcrf` is a single struct copy (`ctx.cr[crfd] = ctx.cr[crfs]`). Matches PowerISA semantics exactly. +- **Canary exact match.** Canary copies the field bit by bit (`StoreCRField(crfd, bit, LoadCRField(crfs, bit))` for all four bits), matching PowerISA semantics exactly. ## Related Instructions diff --git a/tools/ppc-manual/control/mcrfs.md b/tools/ppc-manual/control/mcrfs.md index 61634b07..2e4b7574 100644 --- a/tools/ppc-manual/control/mcrfs.md +++ b/tools/ppc-manual/control/mcrfs.md @@ -129,7 +129,7 @@ int InstrEmit_mcrfs(PPCHIRBuilder& f, const InstrData& i) { - **CR field destination.** `CRFD` is a 3-bit field index (0..7); the four bits land in their natural positions (LT, GT, EQ, SO) of the chosen CR field. After `mcrfs`, `crf` can be tested with the usual conditional branches. - **Use case.** Inspect a particular FPSCR exception group, then act on it with a `bc` — e.g. test FPSCR[24..27] (the FI / FR / VXSNAN / VXISI cluster) and branch. - **Privilege.** Non-privileged on the Xenon — application-visible. -- **xenia status.** Decoded (decoder slot 727), but the interpreter does **not** ship a body in the snapshot on this page — `mcrfs` is rare in title code. xenia's FPSCR model is incomplete (most exception bits are stubbed), so even when implemented, the cleared bits typically have no observable effect. +- **Canary status.** Implemented: Canary copies the FPSCR field into the CR field and then zeroes all four bits of that FPSCR field (PowerISA clears only the exception bits in it). Its FPSCR model is incomplete (`UpdateFPSCR` is a stub), so the cleared bits rarely have an observable effect. - **No `Rc`.** X-form, but the `Rc` bit position is unused (reserved 0). ## Related Instructions diff --git a/tools/ppc-manual/control/mcrxr.md b/tools/ppc-manual/control/mcrxr.md index 68570b3d..7182cc11 100644 --- a/tools/ppc-manual/control/mcrxr.md +++ b/tools/ppc-manual/control/mcrxr.md @@ -130,7 +130,7 @@ int InstrEmit_mcrxr(PPCHIRBuilder& f, const InstrData& i) { - **Use case.** Saturating-arithmetic loops sample XER[OV] periodically; `mcrxr cr0; bso cr0, overflow` is the canonical "did overflow happen since last check?" idiom. - **CR field destination.** `CRFD` is a 3-bit index (0..7). All other CR fields are preserved. - **No reads of GPRs.** `mcrxr` reads only XER, writes only the chosen CR field and XER. -- **xenia exact match.** xenia-rs implements the full sample-and-clear semantics: writes `lt = SO`, `gt = OV`, `eq = CA`, `so = false`, then zeroes `xer_so`, `xer_ov`, `xer_ca`. Matches PowerISA exactly. +- **Canary exact match.** Canary copies `XER[0:3]` (`SO`, `OV`, `CA` and the reserved bit) into the target CR field, then clears those four XER bits — the full sample-and-clear semantics of PowerISA. - **Deprecated in newer PowerISA.** PowerISA v2.06+ marked `mcrxr` deprecated in favour of `mcrxrx` and explicit `mfxer`/`mtxer` patterns, but the Xenon predates that; titles still emit it freely. ## Related Instructions diff --git a/tools/ppc-manual/control/mfcr.md b/tools/ppc-manual/control/mfcr.md index deba93c1..def01859 100644 --- a/tools/ppc-manual/control/mfcr.md +++ b/tools/ppc-manual/control/mfcr.md @@ -137,9 +137,9 @@ int InstrEmit_mfcr(PPCHIRBuilder& f, const InstrData& i) { - **No CR side effect.** `mfcr` is a read; CR is unmodified. The XL-form's nominal `Rc` bit is unused on this opcode. - **Saving CR across calls.** The Xbox 360 / SysV ABI requires non-volatile CR fields (CR2..CR4) to be preserved across calls. Standard prologue: `mfcr r12; stw r12, 8(r1)`. Epilogue restores via [`mtcrf`](mtcrf.md). - **Bit ordering.** PowerPC numbers bits big-endian (bit 0 = MSB). The encoding into the GPR follows the same convention: CR0.LT lands in bit 32 of the doubleword (the MSB of the low word). C-side translations should mask with `0xFFFFFFFFu` before consuming. -- **`mfocrf` variant.** PowerISA defines `mfocrf` (one CR field), encoded as `mfcr` with the high bit of FXM set. xenia-rs decodes both as the same opcode and ignores the FXM hint, returning the entire CR. This is benign — the spec says implementations may treat `mfocrf` as `mfcr`. +- **`mfocrf` variant.** PowerISA defines `mfocrf` (one CR field), encoded as `mfcr` with the high bit of FXM set. Canary honours it: with exactly one FXM bit set it returns just that field, with any other count it returns 0. The spec leaves the other bits undefined, so this is compliant. - **Not synchronising.** Reorderable. -- **xenia exact match.** xenia-rs packs its eight `CrField` structs into a `u64` via `ctx.cr()`, mirroring spec semantics. +- **Canary exact match.** Plain `mfcr` returns the whole CR (`LoadCR()`), mirroring spec semantics. ## Related Instructions @@ -147,7 +147,7 @@ int InstrEmit_mfcr(PPCHIRBuilder& f, const InstrData& i) { - [`mcrf`](mcrf.md), [`mcrxr`](mcrxr.md), [`mcrfs`](mcrfs.md) — narrower CR-field moves. - [`mfspr`](mfspr.md), [`mtspr`](mtspr.md) — generic SPR moves; CR is *not* an SPR (it has its own opcode). -`mfcr` has no simplified mnemonics. `mfocrf RT, FXM` is a related encoding handled by the same xenia-rs slot. +`mfcr` has no simplified mnemonics. `mfocrf RT, FXM` is a related encoding handled by the same Canary emitter (`InstrEmit_mfcr`). ## IBM Reference diff --git a/tools/ppc-manual/control/mffsx.md b/tools/ppc-manual/control/mffsx.md index bfcd808f..1afa755f 100644 --- a/tools/ppc-manual/control/mffsx.md +++ b/tools/ppc-manual/control/mffsx.md @@ -108,11 +108,11 @@ int InstrEmit_mffsx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Operation.** Reads the 32-bit FPSCR and places it in the **low 32 bits** of `FRT`. The high 32 bits of the destination FPR are architecturally undefined; xenia leaves them as the bit-pattern of the FPSCR cast to `u64` (i.e. the high bits are zero, since FPSCR is 32-bit). PowerISA explicitly permits implementations to leave anything there. +- **Operation.** Reads the 32-bit FPSCR and places it in the **low 32 bits** of `FRT`. The high 32 bits of the destination FPR are architecturally undefined; Canary zero-extends the FPSCR to 64 bits and reinterprets that as the FPR's bit pattern, so the high bits are zero. PowerISA explicitly permits implementations to leave anything there. - **Destination is an FPR, not a GPR.** Use [`stfd`](../memory/stfd.md) to spill the FPR to memory and reload via a GPR if the value is needed in the integer file. -- **`mffs.` (`Rc=1`) updates CR1.** The `Rc` bit copies the high four FPSCR bits (FX, FEX, VX, OX) into CR1's LT/GT/EQ/SO. xenia-rs implements this via `update_cr1_from_fpscr`. +- **`mffs.` (`Rc=1`) updates CR1.** The `Rc` bit copies the high four FPSCR bits (FX, FEX, VX, OX) into CR1's LT/GT/EQ/SO. Canary implements this via `CopyFPSCRToCR1`. - **No FPSCR side effect.** Pure read; FPSCR is not modified (unlike [`mcrfs`](mcrfs.md), which clears sticky exception bits). -- **xenia simplification.** xenia-rs models FPSCR as a `u32` field but **does not actively maintain** most of the IEEE-754 sticky bits — the FPU paths typically leave FPSCR untouched. So `mffs` will return whatever was last explicitly set (often 0 / boot defaults). Real titles use it mostly to save/restore the rounding-mode field around library calls, which xenia happens to handle correctly. +- **Canary simplification.** Canary keeps FPSCR as a 32-bit field but **does not maintain** the IEEE-754 sticky bits — its `UpdateFPSCR` is a stub that only clears `FEX`/`VX`. So `mffs` returns what `mtfsf`/`mtfsfi`/`mcrfs` last wrote (initially 0), zero-extended into the FPR's bit pattern. Real titles use it mostly to save/restore the rounding-mode field around library calls, which works: restoring through `mtfsf` also reloads the host rounding mode. - **Not synchronising.** Reorderable with non-FPU instructions. ## Related Instructions diff --git a/tools/ppc-manual/control/mfmsr.md b/tools/ppc-manual/control/mfmsr.md index 0ace8b49..38103f91 100644 --- a/tools/ppc-manual/control/mfmsr.md +++ b/tools/ppc-manual/control/mfmsr.md @@ -105,7 +105,7 @@ int InstrEmit_mfmsr(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Privileged.** `mfmsr` is supervisor-only; executing it from problem state on real hardware raises a Privileged Instruction interrupt. Xbox 360 game code never executes it directly — it appears only in the kernel image (`xboxkrnl.exe`) and in xenia's HLE bridge. +- **Privileged.** `mfmsr` is supervisor-only; executing it from problem state on real hardware raises a Privileged Instruction interrupt. Title code does use it anyway — Project Sylpheed's disassembly in `sylpheed.db` has 612 `mfmsr` (and 1,239 `mtmsrd`) — and Canary does not enforce privilege. - **MSR layout (Xenon-relevant fields, big-endian bit numbering).** | Bit | Name | Meaning | @@ -120,9 +120,9 @@ int InstrEmit_mfmsr(PPCHIRBuilder& f, const InstrData& i) { | 63 | RI | recoverable interrupt | The Xenon also exposes `MSR[SF]` (bit 0) = 1 for 64-bit mode; `MSR[HV]` (bit 3) for hypervisor. See PowerISA Book III for the full table. -- **Synchronisation.** Marked `sync` in xenia's XML — `mfmsr` is execution-synchronising on real hardware (drains the pipeline before sampling MSR). -- **xenia model.** xenia-rs stores MSR as a flat `u64` and returns it raw. No real bit semantics are modelled — the kernel HLE never observes individual MSR fields. The interpreter ignores privilege. -- **Read of an undocumented field returns 0.** Most of the MSR is zero in xenia because no path explicitly initialises it. +- **Synchronisation.** Marked `sync` in Canary's `tools/ppc-instructions.xml` — `mfmsr` is execution-synchronising on real hardware (drains the pipeline before sampling MSR). +- **Canary model.** Canary stores MSR as a flat 64-bit context field and `mfmsr` returns it raw. No real bit semantics are modelled, and privilege is ignored. +- **Initial value.** Canary starts every thread with `MSR = 0x9030` (its comment: "dumped from a real 360") and changes it only through `mtmsr`/`mtmsrd`. ## Related Instructions diff --git a/tools/ppc-manual/control/mfspr.md b/tools/ppc-manual/control/mfspr.md index 2258ee19..71be340a 100644 --- a/tools/ppc-manual/control/mfspr.md +++ b/tools/ppc-manual/control/mfspr.md @@ -153,30 +153,30 @@ decoded_spr = ((field & 0x1F) << 5) | ((field >> 5) & 0x1F) So a programmer writing `mfspr RT, 8` (read LR) encodes `spr-field = 0x100` — *not* `8`. Assemblers handle this transparently; disassemblers reverse it. When writing a translator that parses raw instruction words, swap the halves explicitly. -## SPR Map (Xenon subset modelled by xenia) +## SPR Map (Xenon subset, with Canary's behaviour) -| Decoded # | Name | Meaning | xenia-rs behaviour | +| Decoded # | Name | Meaning | Canary behaviour | | --- | --- | --- | --- | -| 1 | `XER` | Fixed-point exception register (CA / OV / SO + length field) | packed with `ctx.xer()` | -| 8 | `LR` | Link register | `ctx.lr` | -| 9 | `CTR` | Count register | `ctx.ctr` | -| 18 | `DSISR` | Data-storage interrupt syndrome | returns 0 (stubbed) | -| 19 | `DAR` | Data-access register | returns 0 (stubbed) | -| 256 | `VRSAVE` | Vector-register save mask | `ctx.vrsave` | -| 268 | `TBL` | Time-base lower 32 bits | `ctx.timebase & 0xFFFFFFFF` | -| 269 | `TBU` | Time-base upper 32 bits | `ctx.timebase >> 32` | -| 272–275 | `SPRG0..3` | Software scratch registers (kernel) | returns 0 (stubbed) | -| 287 | `PVR` | Processor-version register | `0x00710800` (Xenon signature) | -| 1008–1009 | `HID0/1` | Hardware implementation registers | returns 0 (stubbed) | -| 1023 | `PIR` | Processor-ID register | returns 0 (stubbed) | +| 1 | `XER` | Fixed-point exception register (CA / OV / SO + length field) | `LoadXER()` | +| 8 | `LR` | Link register | `LoadLR()` | +| 9 | `CTR` | Count register | `LoadCTR()` | +| 18 | `DSISR` | Data-storage interrupt syndrome | not implemented | +| 19 | `DAR` | Data-access register | not implemented | +| 256 | `VRSAVE` | Vector-register save mask | zero-extended `vrsave` | +| 268 | `TBL` | Time-base lower 32 bits | the full 64-bit guest clock (`LoadClock`) | +| 269 | `TBU` | Time-base upper 32 bits | guest clock `>> 32` | +| 272–275 | `SPRG0..3` | Software scratch registers (kernel) | not implemented | +| 287 | `PVR` | Processor-version register | the `pvr` cvar (default `0x710700`) | +| 1008–1009 | `HID0/1` | Hardware implementation registers | not implemented | +| 1023 | `PIR` | Processor-ID register | not implemented | -Unrecognised SPRs return 0 and log a warning. Games rarely read unmodelled SPRs; when they do it's usually clock-skew or sanity checks. +"Not implemented" means Canary treats the `mfspr` as an unimplemented instruction: translating it logs "Unimplemented instr" and, with the default `break_on_unimplemented_instructions`, breaks. Games rarely read unmodelled SPRs; when they do it's usually clock-skew or sanity checks. ## Special Cases & Edge Conditions -- **Privilege.** Some SPRs are privileged on real hardware (MSR, HID0/1, SPRG0..3, DSISR, DAR, PIR). Xbox 360 titles run in a mixed privilege model under the hypervisor; xenia exposes all SPRs without a privilege check because the captured title binaries never contain a real privileged read that should trap. +- **Privilege.** Some SPRs are privileged on real hardware (MSR, HID0/1, SPRG0..3, DSISR, DAR, PIR). Xbox 360 titles run in a mixed privilege model under the hypervisor; Canary does no privilege check, and of the privileged ones it implements none. - **`LR` and `CTR` have dedicated simplified mnemonics.** Assemblers recognise `mflr RT` ≡ `mfspr RT, 8` and `mfctr RT` ≡ `mfspr RT, 9`. Similarly `mfxer RT` ≡ `mfspr RT, 1`. Disassemblers emit the simplified forms; the translation agent should map both forms to the same abstract operation. -- **`mftb` vs. `mfspr TBL/TBU`.** Reading the time-base has a dedicated X-form variant [`mftb`](mftb.md) that uses a separate opcode. Post-Xbox-360 PowerISA deprecated `mfspr TBL/TBU`, but xenia accepts both. Prefer `mftb` in new translations. +- **`mftb` vs. `mfspr TBL/TBU`.** Reading the time-base has a dedicated X-form variant [`mftb`](mftb.md) that uses a separate opcode. Post-Xbox-360 PowerISA deprecated `mfspr TBL/TBU`, but Canary accepts both. Prefer `mftb` in new translations. - **Side-effect-free.** `mfspr` has no effect on any register beyond `RT`. It can be freely reordered with non-SPR-touching instructions. - **No `Rc` / `OE`.** This is an XFX-form instruction; bit 31 is reserved (0). diff --git a/tools/ppc-manual/control/mftb.md b/tools/ppc-manual/control/mftb.md index c2d20e13..6543f302 100644 --- a/tools/ppc-manual/control/mftb.md +++ b/tools/ppc-manual/control/mftb.md @@ -116,7 +116,7 @@ int InstrEmit_mftb(PPCHIRBuilder& f, const InstrData& i) { | 268 | TBL | Time Base, lower 32 bits | | 269 | TBU | Time Base, upper 32 bits | - Other selectors return 0 in xenia and are not used by titles. + Canary returns the full 64-bit guest clock for 268 and the clock's upper 32 bits for any other selector. - **Atomic 64-bit read pattern.** Because `mftb` reads only 32 bits at a time, software performs the canonical retry loop to avoid TBL→TBU rollover skew: ```asm retry: @@ -127,9 +127,9 @@ int InstrEmit_mftb(PPCHIRBuilder& f, const InstrData& i) { bne retry ``` - **Xenon clock rate.** Real hardware ticks the time base at ~3.2 GHz (one tick per CPU clock divided by the architectural ratio). The PVR signature the kernel exposes (`0x00710800`) and the kernel-reported tick rate jointly let titles convert TB ticks to seconds. -- **xenia behaviour.** xenia-rs stores `ctx.timebase` as a `u64` and **increments it once per interpreted instruction**, not per real-time wall clock. This guarantees deterministic replay (same trace ⇒ same TB readings) at the cost of decoupling guest time from host time. Games that rely on TB for real-time sync will run faster or slower depending on host throughput. +- **Canary behaviour.** Canary's `mftb` reads its guest clock (`LoadClock`), which follows host time — TB for SPR 268, otherwise just the upper 32 bits. Guest time therefore tracks real time, and TB readings are not reproducible from run to run. - **`mftb RT` (no operand)** is the simplified mnemonic for `mftb RT, 268` — read the lower half. `mftbu RT` ≡ `mftb RT, 269`. -- **Deprecated alternative.** `mfspr RT, 268`/`269` works on the Xenon (xenia accepts both) but post-PowerISA v2.06 deprecated reading TB through `mfspr`. Prefer `mftb`. +- **Deprecated alternative.** `mfspr RT, 268`/`269` works on the Xenon (Canary accepts both) but post-PowerISA v2.06 deprecated reading TB through `mfspr`. Prefer `mftb`. ## Related Instructions diff --git a/tools/ppc-manual/control/mfvscr.md b/tools/ppc-manual/control/mfvscr.md index a99cfb40..89ede3d9 100644 --- a/tools/ppc-manual/control/mfvscr.md +++ b/tools/ppc-manual/control/mfvscr.md @@ -115,7 +115,7 @@ int InstrEmit_mfvscr(PPCHIRBuilder& f, const InstrData& i) { - **`SAT` is sticky.** Once a saturating vector instruction clamps a result, `VSCR[SAT]` becomes 1 and stays 1 until explicitly cleared via [`mtvscr`](mtvscr.md). Software polls it after a vector batch to detect overflow. - **`NJ` controls denormals.** When `NJ=1` (the Xenon's default), AltiVec single-precision ops flush denormal inputs/outputs to zero (non-IEEE behaviour); `NJ=0` enforces full IEEE. - **VRSAVE.** Writing the entire 128-bit `VD` consumes a vector register slot; software wishing to honour [`VRSAVE`](mtspr.md) bookkeeping should ensure the chosen `VD` is in the live mask. -- **xenia simplification.** xenia-rs stores VSCR as a single value of the same `vr` type (effectively a u128) and copies it directly into `ctx.vr[VD]`. Saturating ops in xenia-rs **do** maintain SAT correctly for the vector ops that are implemented; NJ is honoured for the denormal-flush paths but its effect is small in practice. +- **Canary simplification.** `mfvscr` copies Canary's `vscr_vec` into `VD`. Only `mtvscr` writes that value (it starts with `NJ` set), so `SAT` never appears: Canary does not record saturation at all. - **Not synchronising.** ## Related Instructions diff --git a/tools/ppc-manual/control/mtcrf.md b/tools/ppc-manual/control/mtcrf.md index 537d6cc9..69464244 100644 --- a/tools/ppc-manual/control/mtcrf.md +++ b/tools/ppc-manual/control/mtcrf.md @@ -134,10 +134,10 @@ int InstrEmit_mtcrf(PPCHIRBuilder& f, const InstrData& i) { - **`CRM` is an 8-bit field-mask, MSB-first.** Each bit of `CRM` corresponds to one CR field: `CRM[0]` (mask bit `0x80`) selects CR0, `CRM[1]` (`0x40`) selects CR1, …, `CRM[7]` (`0x01`) selects CR7. Each *set* mask bit causes the corresponding 4-bit slice of `RS[32:63]` to overwrite that CR field; clear mask bits leave the field untouched. - **Slice positions inside `RS`.** Big-endian: bits 32..35 of `RS` map to CR0, bits 36..39 to CR1, …, bits 60..63 to CR7. The high 32 bits of `RS` are ignored. - **`mtcr RS` simplified mnemonic.** When `CRM = 0xFF`, all eight CR fields are written; assemblers fold this into `mtcr RS`. This is the dominant form (function epilogue restoring the saved CR). -- **`mtocrf` variant.** PowerISA defines `mtocrf` as the single-field variant — encoded with the high bit of FXM set and exactly one CRM bit set. xenia-rs treats both as the same opcode and processes whatever `CRM` mask is present, so `mtocrf` works correctly without special handling. +- **`mtocrf` variant.** PowerISA defines `mtocrf` as the single-field variant — encoded with the high bit of FXM set and exactly one CRM bit set. Canary distinguishes it: with exactly one bit set it writes that field, with any other count it zeroes the whole CR (the spec calls that case undefined). - **Use case in ABI.** Save/restore non-volatile CR fields (CR2, CR3, CR4 on the Xbox 360 ABI). The standard restore is `lwz r12, 8(r1); mtcrf 0x38, r12` — `0x38` = bits for CR2|CR3|CR4 — preserving the volatile fields the callee may have already updated. - **No CR0 / XER side effects.** `mtcrf` does not record into CR0; XER is untouched. -- **xenia exact match.** xenia-rs decomposes the CR into a `u32`, applies a per-field mask, and reassembles via `set_cr`. The 8-bit `CRM` walk matches the spec exactly. +- **Canary exact match.** For plain `mtcrf`, Canary walks the 8-bit `CRM` mask and writes each selected field from `RS` (`StoreCR(field, RS)`), matching the spec exactly. - **Not synchronising.** Reorderable. ## Related Instructions @@ -153,7 +153,7 @@ int InstrEmit_mtcrf(PPCHIRBuilder& f, const InstrData& i) { | --- | --- | --- | | `mtcr RS` | `mtcrf 0xFF, RS` | write all eight CR fields from low half of RS | -`mtocrf RS, FXM` is a related encoding handled by the same xenia-rs slot. +`mtocrf RS, FXM` is a related encoding handled by the same Canary emitter (`InstrEmit_mtcrf`). ## IBM Reference diff --git a/tools/ppc-manual/control/mtfsb0x.md b/tools/ppc-manual/control/mtfsb0x.md index 1dd99ee2..286ab5a1 100644 --- a/tools/ppc-manual/control/mtfsb0x.md +++ b/tools/ppc-manual/control/mtfsb0x.md @@ -105,10 +105,10 @@ int InstrEmit_mtfsb0x(PPCHIRBuilder& f, const InstrData& i) { - **Operation.** Clears (sets to 0) **a single named bit** of the 32-bit FPSCR. The bit is selected by `FPSCRD` (a 5-bit absolute index 0..31, big-endian: 0 = MSB = FX). - **The mnemonic name "Bit 0" is misleading.** "0" refers to the *value being written*, not to bit position 0. Pair with [`mtfsb1x`](mtfsb1x.md) which writes a 1. -- **Restricted bits.** Per PowerISA, `mtfsb0` cannot clear bits 1 (FEX) or 2 (VX) — those are summary bits, derived from other FPSCR bits. xenia-rs does **not** enforce this restriction; it will happily flip any bit. In practice no Xbox 360 title relies on the restriction's enforcement. +- **Restricted bits.** Per PowerISA, `mtfsb0` cannot clear bits 1 (FEX) or 2 (VX) — those are summary bits, derived from other FPSCR bits. ⚠️ Canary does not implement `mtfsb0`: its emitter is `XEINSTRNOTIMPLEMENTED`, so translating one logs "Unimplemented instr" and, with the default `break_on_unimplemented_instructions`, breaks. - **`Rc=1`.** `mtfsb0.` (`Rc=1`) updates **CR1** with the high four FPSCR bits (FX, FEX, VX, OX) after the clear. This is the FPU's record-form analogue. - **Common use.** Reset a sticky exception bit ahead of a sequence of FP ops you want to monitor (e.g. `mtfsb0 5` to clear ZX before a divide series, then read it back). -- **xenia simplification.** xenia-rs maintains FPSCR as a `u32` and does the bit clear correctly, but most downstream FP instructions in xenia **do not update** FPSCR exception bits — so monitoring them after `mtfsb0` will see the bits stay at their seed value. Acceptable for titles that use FPSCR only to manage rounding / non-exception state. +- **Canary status.** With `mtfsb0` unimplemented, Canary never clears the bit (and breaks by default, see above). Most FP instructions in Canary do not update FPSCR exception bits either, so titles that manage rounding only through `mtfsf`/`mtfsfi` are unaffected. - **Not synchronising.** Reorderable. ## Related Instructions diff --git a/tools/ppc-manual/control/mtfsb1x.md b/tools/ppc-manual/control/mtfsb1x.md index b43f3e35..eac90573 100644 --- a/tools/ppc-manual/control/mtfsb1x.md +++ b/tools/ppc-manual/control/mtfsb1x.md @@ -105,10 +105,10 @@ int InstrEmit_mtfsb1x(PPCHIRBuilder& f, const InstrData& i) { - **Operation.** Sets (writes 1 to) **a single named bit** of FPSCR. `FPSCRD` is a 5-bit absolute index (0..31), big-endian (0 = MSB = FX). - **Mnemonic name.** "1" denotes the *value written*, not bit position. Pair with [`mtfsb0x`](mtfsb0x.md) for clears. -- **Restricted bits.** PowerISA forbids `mtfsb1` from setting FEX (bit 1) or VX (bit 2) directly — both are summary bits derived from other state. `mtfsb1` *can* set FX (bit 0), which is itself a sticky summary; this is occasionally used to force a `Program` interrupt for testing. xenia-rs does **not** enforce the restriction; setting summary bits will stick until cleared explicitly. +- **Restricted bits.** PowerISA forbids `mtfsb1` from setting FEX (bit 1) or VX (bit 2) directly — both are summary bits derived from other state. `mtfsb1` *can* set FX (bit 0), which is itself a sticky summary; this is occasionally used to force a `Program` interrupt for testing. ⚠️ Canary does not implement `mtfsb1`: its emitter is `XEINSTRNOTIMPLEMENTED`, so translating one logs "Unimplemented instr" and, with the default `break_on_unimplemented_instructions`, breaks. - **`Rc=1`.** `mtfsb1.` (`Rc=1`) updates CR1 with the high four FPSCR bits (FX, FEX, VX, OX) after the set. - **Common use.** Force-set a sticky exception bit to test exception-handling code paths. Also seen in floating-point library setup that wants a known FPSCR seed. -- **xenia simplification.** Same caveat as `mtfsb0`: xenia maintains FPSCR but most FP paths don't read it, so the set has limited downstream effect. The bit will read back correctly via [`mffsx`](mffsx.md). +- **Canary status.** Canary does not implement `mtfsb1` (`XEINSTRNOTIMPLEMENTED`), so the bit is never set and will not read back via [`mffsx`](mffsx.md). - **Not synchronising.** Reorderable. ## Related Instructions diff --git a/tools/ppc-manual/control/mtfsfix.md b/tools/ppc-manual/control/mtfsfix.md index 2be1b1f1..a7ec7e3b 100644 --- a/tools/ppc-manual/control/mtfsfix.md +++ b/tools/ppc-manual/control/mtfsfix.md @@ -127,8 +127,8 @@ int InstrEmit_mtfsfix(PPCHIRBuilder& f, const InstrData& i) { - **Most common use: rounding-mode set.** `mtfsfi 7, 0` selects round-to-nearest, `mtfsfi 7, 1` round-toward-zero, `mtfsfi 7, 2` round-toward-+∞, `mtfsfi 7, 3` round-toward-−∞. The four immediate values map to RN per IEEE-754. Compilers emit this when transitioning into and out of strict-IEEE regions. - **No FPR source.** Unlike [`mtfsfx`](mtfsfx.md), `mtfsfi` doesn't need an FPR — it carries its 4-bit value in the instruction word, making it cheaper for constant updates. - **`Rc=1`.** `mtfsfi.` copies FPSCR's top 4 bits (FX, FEX, VX, OX) into CR1 after the write. -- **Restrictions in newer PowerISA.** v2.05+ disallows writing FEX/VX (summary bits) via `mtfsfi`. xenia-rs does **not** enforce this — the immediate goes straight into the chosen field. -- **xenia simplification.** xenia stores FPSCR as a `u32` and applies the field-shift correctly. Same caveat as `mtfsf`: most xenia FP paths don't honour FPSCR, so the rounding-mode change is architecturally visible (via [`mffsx`](mffsx.md)) but typically does not change subsequent FP results. +- **Restrictions in newer PowerISA.** v2.05+ disallows writing FEX/VX (summary bits) via `mtfsfi`. Canary does **not** enforce this — the immediate goes straight into the chosen field, and when that field holds `RN` Canary also reloads the host rounding mode. +- **Canary behaviour.** Canary applies the field shift correctly, and when the field holds `RN` it also reloads the host rounding mode — so a rounding-mode change made with `mtfsfi` does affect later FP results in Canary. - **Not synchronising.** PowerISA recommends `isync` after rounding-mode changes if subsequent FP correctness depends on the new mode. ## Related Instructions diff --git a/tools/ppc-manual/control/mtfsfx.md b/tools/ppc-manual/control/mtfsfx.md index 26950396..85f37de1 100644 --- a/tools/ppc-manual/control/mtfsfx.md +++ b/tools/ppc-manual/control/mtfsfx.md @@ -141,8 +141,8 @@ int InstrEmit_mtfsfx(PPCHIRBuilder& f, const InstrData& i) { - **Source is the LOW 32 bits of `FRB`.** The high 32 bits are ignored. Software that wants to write a 32-bit pattern typically constructs it in a GPR, stores to memory, and reloads as a double via [`lfd`](../memory/lfd.md). - **Most common use: setting rounding mode.** Compilers wrap calls to ``-style functions with `mtfsf 1, fX` to update only the rounding-mode field (RN, FPSCR field 7). - **`Rc=1` updates CR1.** `mtfsf.` copies FPSCR's top 4 bits (FX, FEX, VX, OX) into CR1 after the write. -- **`L`/`W` bits.** PowerISA v2.05+ adds `L=1` to mean "write all FPSCR bits regardless of FM" and `W=1` to select the upper or lower 32 bits. xenia-rs **ignores** `L` and `W` (always treats `L=0, W=0`), which matches every real Xbox 360 use. -- **xenia simplification.** xenia maintains FPSCR as a `u32` and applies the field mask correctly. However, most FP instructions in xenia don't *read* FPSCR (e.g., divides ignore the rounding mode), so the architecturally-set rounding mode often has no actual effect on results. Acceptable for the title set xenia targets. +- **`L`/`W` bits.** PowerISA v2.05+ adds `L=1` to mean "write all FPSCR bits regardless of FM" and `W=1` to select the upper or lower 32 bits. Canary handles `L=0`; its `L=1` path writes FPSCR but still reports the instruction as unimplemented, and `W` is only asserted to be zero. +- **Canary behaviour.** Canary applies the field mask correctly and, when the mask covers `RN`, reloads the host rounding mode, so conversions and arithmetic that round follow the new mode. The exception bits are another matter: Canary's `UpdateFPSCR` is a stub, so they are never set by FP arithmetic. - **Not synchronising.** Reorderable; PowerISA recommends an `isync` after FPSCR changes that affect subsequent FP behaviour. ## Related Instructions diff --git a/tools/ppc-manual/control/mtmsr.md b/tools/ppc-manual/control/mtmsr.md index cb24458c..369793f9 100644 --- a/tools/ppc-manual/control/mtmsr.md +++ b/tools/ppc-manual/control/mtmsr.md @@ -105,10 +105,10 @@ int InstrEmit_mtmsr(PPCHIRBuilder& f, const InstrData& i) { - **Privileged.** `mtmsr` is supervisor-only on real hardware. Executing it from problem state raises a Privileged Instruction interrupt. Game code never emits it; only the kernel and exception-return paths use it. - **32-bit form.** `mtmsr` writes the **low 32 bits** of MSR (legacy PPC32 form). On the Xenon (a PPC64 implementation), use [`mtmsrd`](mtmsrd.md) for the full 64-bit MSR. Some Xenon kernel sequences still use `mtmsr` to leave the high half untouched while flipping low-half flags like EE/PR. - **Synchronisation.** Marked `sync` — `mtmsr` is **execution-synchronising**. The Xenon must drain all preceding instructions before the new MSR takes effect, and PowerISA recommends a following `isync` to guarantee subsequent instructions execute under the new MSR. -- **`L` operand.** Modern PowerISA defines an `L` bit selecting "EE/RI only" (`L=1`) versus "all" (`L=0`); xenia-rs ignores `L` and writes the entire MSR. Real Xbox 360 kernel code uses both `L=0` and `L=1`. -- **xenia model.** Treats MSR as a flat `u64` field. Both `mtmsr` and `mtmsrd` execute the same body — `ctx.msr = ctx.gpr[rs]`. No privilege or atomicity is enforced; no side effects on TLB / interrupt mask / endianness are simulated. +- **`L` operand.** Modern PowerISA defines an `L` bit selecting "EE/RI only" (`L=1`) versus "all" (`L=0`); Canary's `mtmsr` ignores `L` and writes the entire MSR. Real Xbox 360 kernel code uses both `L=0` and `L=1`. +- **Canary model.** Treats MSR as a flat 64-bit context field. `mtmsr` stores `RS` whole; `mtmsrd` differs (see its page). No privilege or atomicity is enforced; no side effects on TLB / interrupt mask / endianness are simulated. - **No CR / XER side effects.** -- **Caveat for translators.** Because the host kernel runs natively in xenia, the guest MSR has no architectural meaning beyond storage. Code that reads it back via [`mfmsr`](mfmsr.md) will see exactly what was last written. +- **Caveat for translators.** Because Canary implements the kernel natively, the guest MSR has no architectural meaning beyond storage. Code that reads it back via [`mfmsr`](mfmsr.md) sees what `mtmsr` last wrote — `mtmsrd` changes only `EE`. ## Related Instructions diff --git a/tools/ppc-manual/control/mtmsrd.md b/tools/ppc-manual/control/mtmsrd.md index f6df6e1a..1e05d526 100644 --- a/tools/ppc-manual/control/mtmsrd.md +++ b/tools/ppc-manual/control/mtmsrd.md @@ -112,9 +112,9 @@ int InstrEmit_mtmsrd(PPCHIRBuilder& f, const InstrData& i) { - **Privileged.** Like [`mtmsr`](mtmsr.md), supervisor-only. Game code never emits it. - **64-bit form.** Writes all 64 MSR bits — including `MSR[SF]` (bit 0) which selects 64-bit mode, `MSR[HV]` (bit 3, hypervisor), `MSR[EE]` (32, external interrupts), `MSR[PR]` (33, problem state), `MSR[FP]` (34), `MSR[ME]` (35, machine-check enable), `MSR[DR]`/`MSR[IR]` (data/instruction translation, 38/39), `MSR[RI]` (63, recoverable interrupt). On the Xenon kernel this is the canonical MSR-write instruction. -- **`L` operand.** Same `L`-bit selector as `mtmsr`: `L=1` updates only `MSR[EE]` and `MSR[RI]`; `L=0` updates the full register. xenia-rs ignores `L` and always writes the full doubleword (matching the typical kernel use). +- **`L` operand.** Same `L`-bit selector as `mtmsr`: `L=1` updates only `MSR[EE]` and `MSR[RI]`; `L=0` updates the full register. ⚠️ Canary ignores `L` and always updates **only `MSR[EE]`** (mask `0x8000`), leaving every other bit — including `RI` — unchanged. - **Synchronisation.** Marked `sync` — execution-synchronising. PowerISA recommends `isync` afterwards if subsequent fetch / data semantics depend on the new MSR. -- **xenia model.** Shares one interpreter arm with `mtmsr`: `ctx.msr = ctx.gpr[rs]`. No architectural side effects beyond writing the storage; no privilege check. +- **Canary model.** Not shared with `mtmsr`: `mtmsrd` computes `(RS & 0x8000) | (MSR & ~0x8000)`. No architectural side effects beyond writing the storage; no privilege check. - **No CR / XER updates.** - **Used in interrupt return paths.** Kernel handlers commonly write SRR1 (saved MSR) into MSR via `mtmsrd` followed by `rfid` to atomically restore state and jump to SRR0. diff --git a/tools/ppc-manual/control/mtspr.md b/tools/ppc-manual/control/mtspr.md index bd833828..4f7772a0 100644 --- a/tools/ppc-manual/control/mtspr.md +++ b/tools/ppc-manual/control/mtspr.md @@ -129,21 +129,21 @@ int InstrEmit_mtspr(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **SPR halves are swapped in the encoding.** As with [`mfspr`](mfspr.md), the 10-bit `spr` field stores the two 5-bit halves transposed. Software always names the *logical* SPR number; assemblers handle the swap. Decoded number `n = ((field & 0x1F) << 5) | ((field >> 5) & 0x1F)`. -- **SPRs writable from userspace (Xenon, modelled by xenia).** +- **SPRs writable from userspace (Xenon) — the only ones Canary implements.** | Decoded # | Name | Effect | | --- | --- | --- | - | 1 | XER | unpacked into `ctx.xer_so/xer_ov/xer_ca` and length field | - | 8 | LR | `ctx.lr ← RS` | - | 9 | CTR | `ctx.ctr ← RS` | - | 256 | VRSAVE | `ctx.vrsave ← RS & 0xFFFFFFFF` | + | 1 | XER | `StoreXER(RS)` | + | 8 | LR | `StoreLR(RS)` | + | 9 | CTR | `StoreCTR(RS)` | + | 256 | VRSAVE | low 32 bits of `RS` into `vrsave` | -- **SPRs xenia silently swallows (no observable effect).** SPRG0..3, HID0, HID1, DAR, DSISR — these are kernel/diagnostic registers; xenia accepts the write to avoid spurious "unimplemented SPR" warnings, but the value is discarded. -- **Privileged SPRs.** On real hardware, writes to MSR-visible kernel SPRs (SPRG0..3, HID0/1, DSISR, DAR, PIR, etc.) require supervisor mode and trap from problem state. xenia does **not** enforce privilege. -- **Time-base writes are privileged.** `mtspr 268/269` (TBL/TBU) only works in supervisor mode on real hardware. xenia will warn `mtspr: unimplemented SPR` for these — do **not** assume the time base can be guest-written. +- **Every other SPR is unimplemented in Canary.** SPRG0..3, HID0, HID1, DAR, DSISR and the rest are not swallowed: translating the `mtspr` logs "Unimplemented instr" and, with the default `break_on_unimplemented_instructions`, breaks. +- **Privileged SPRs.** On real hardware, writes to MSR-visible kernel SPRs (SPRG0..3, HID0/1, DSISR, DAR, PIR, etc.) require supervisor mode and trap from problem state. Canary does **not** enforce privilege — it implements only XER, LR, CTR and VRSAVE and treats `mtspr` to any other SPR as an unimplemented instruction. +- **Time-base writes are privileged.** `mtspr 268/269` (TBL/TBU) only works in supervisor mode on real hardware, and Canary treats them as unimplemented — do **not** assume the time base can be guest-written. - **Simplified mnemonics.** `mtxer RS` ≡ `mtspr 1, RS`, `mtlr RS` ≡ `mtspr 8, RS`, `mtctr RS` ≡ `mtspr 9, RS`. These dominate Xbox 360 disassembly. - **No CR / XER side effects.** `mtspr` itself doesn't record (the *target* SPR may itself be XER, in which case XER is being directly overwritten). -- **Not synchronising.** xenia's XML omits the `sync` flag; PowerISA does require some `mtspr` cases (e.g. SDR1, MMU regs) to be context-synchronising — none of them appear in title binaries. +- **Not synchronising.** Canary's `tools/ppc-instructions.xml` omits the `sync` flag; PowerISA does require some `mtspr` cases (e.g. SDR1, MMU regs) to be context-synchronising — none of them appear in title binaries. ## Related Instructions diff --git a/tools/ppc-manual/control/mtvscr.md b/tools/ppc-manual/control/mtvscr.md index e1a5966d..1aa8efc0 100644 --- a/tools/ppc-manual/control/mtvscr.md +++ b/tools/ppc-manual/control/mtvscr.md @@ -115,7 +115,7 @@ int InstrEmit_mtvscr(PPCHIRBuilder& f, const InstrData& i) { - **Bits actually significant.** Of the 32 source bits, only **NJ (bit 16)** and **SAT (bit 31)** are architecturally meaningful on the Xenon. All other bits should be written as zero; behaviour for non-zero values is implementation-defined. - **Clearing SAT.** The dominant use is `mtvscr vN` with `vN` zeroed via `vxor vN, vN, vN`, which writes VSCR=0 and thereby clears the sticky SAT bit before a fresh batch of saturating vector ops. - **Setting NJ.** Switching to/from "Java mode" (`NJ=0`, full IEEE denormal handling) versus "Non-Java mode" (`NJ=1`, flush-to-zero) is the other meaningful use. Game audio / DSP code occasionally toggles this to match a precise IEEE expectation. -- **xenia simplification.** xenia-rs stores VSCR identically to a vector register and copies the source straight in: `ctx.vscr = ctx.vr[VB]`. Subsequent xenia AltiVec ops do consult `VSCR[SAT]` for sticky updates, so the architecturally-relevant behaviour is preserved. NJ's flush-to-zero semantics are honoured by xenia's vector denormal paths. +- **Canary simplification.** Canary copies the source straight into its `vscr_vec` and sets its NJ mode from bit 16 of the low word, which switches the host's VMX MXCSR between flush-to-zero + denormals-are-zero and IEEE. `SAT` is not modelled: no Canary AltiVec op records it. - **Not synchronising.** PowerISA does not require `isync` after `mtvscr`, but library code occasionally pairs them as a defensive measure. ## Related Instructions diff --git a/tools/ppc-manual/fpu/fabsx.md b/tools/ppc-manual/fpu/fabsx.md index c3e11088..aaa1b555 100644 --- a/tools/ppc-manual/fpu/fabsx.md +++ b/tools/ppc-manual/fpu/fabsx.md @@ -113,7 +113,7 @@ Affected: FX, FEX, VX, OX (if Rc = 1) ## Special Cases & Edge Conditions - **Bit-pattern operation, no rounding.** `fabs` clears the sign bit (bit 0) of the source FPR's binary64 representation and writes the 64-bit value to the destination unchanged otherwise. No precision loss, no FPSCR exception bits. The mnemonic does not have an `s` variant — there is one form regardless of whether the operand is interpreted as binary32 or binary64. -- **NaN handling.** `fabs(NaN)` returns the same NaN with the sign bit cleared. The signalling/quiet bit is **not** modified, and `FPSCR[VXSNAN]` is **not** raised. xenia-rs uses `f64::abs`, which matches: it is bit-level `x & 0x7FFF_FFFF_FFFF_FFFF`. +- **NaN handling.** `fabs(NaN)` returns the same NaN with the sign bit cleared. The signalling/quiet bit is **not** modified, and `FPSCR[VXSNAN]` is **not** raised. Canary emits `f.Abs`, a pure sign-bit clear. ⚠️ Its `Rc=1` branch is empty, so `fabs.` does not update CR1 in Canary. - **Special values.** `fabs(±0) = +0`; `fabs(±∞) = +∞`; `fabs(±NaN)` = `+NaN` (sign cleared, payload preserved). - **FPSCR is largely untouched.** Hardware specifies `FPRF` is **not** updated by `fabs`, and no exception bits are raised. Notation in the page header about `FPSCR` write is generic — the only meaningful write is via `Rc=1`. - **`Rc=1` (`fabs.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1 (these bits are typically stale or zero). diff --git a/tools/ppc-manual/fpu/faddsx.md b/tools/ppc-manual/fpu/faddsx.md index c768ac3f..6f46bbff 100644 --- a/tools/ppc-manual/fpu/faddsx.md +++ b/tools/ppc-manual/fpu/faddsx.md @@ -107,11 +107,11 @@ int InstrEmit_faddsx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Single precision via double FPRs.** The trailing `s` in the mnemonic means the result is rounded to IEEE-754 binary32 after the addition, then re-encoded into the 64-bit FPR using the binary64 representation of that single-precision value. The host computes `to_single(a + b)`; both source operands are read as full binary64. -- **FPSCR side effects.** Hardware updates `FPRF` (result class), `FR`/`FI` (rounding info), `FX`, and the exception bits — `OX` on overflow, `UX` on underflow, `XX` on inexact, `VXISI` on `±∞ − ±∞`, `VXSNAN` on a signalling-NaN input. xenia-rs does **not** maintain FPSCR in the interpreter snapshot — call this out as a xenia quirk if you depend on cross-instruction FPSCR observation. -- **`Rc=1` (`fadds.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. xenia models this via `update_cr1_from_fpscr()`. +- **FPSCR side effects.** Hardware updates `FPRF` (result class), `FR`/`FI` (rounding info), `FX`, and the exception bits — `OX` on overflow, `UX` on underflow, `XX` on inexact, `VXISI` on `±∞ − ±∞`, `VXSNAN` on a signalling-NaN input. Canary does not compute them: its `UpdateFPSCR` is a stub that clears `FEX` and `VX`, leaves every other FPSCR bit unchanged and, with `Rc=1`, writes CR1 as all zeros. Don't depend on cross-instruction FPSCR observation when matching Canary. +- **`Rc=1` (`fadds.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1 on hardware. Canary's `UpdateFPSCR` stub instead writes CR1 as all zeros. - **NaN propagation.** Any NaN input yields a quiet NaN result; signalling NaNs are quietened (signalling bit cleared) per PowerISA. Host-native `f64 +` may not perform that quietening on every platform. - **`±∞ − ±∞` after rounding.** Although `+`-shaped, opposite-signed infinities still produce `QNaN(VXISI)`. -- **`FPSCR[NI]` (non-IEEE / flush-to-zero)** is set at Xenon boot, so denormal results normally flush to zero. Xenia inherits host semantics, which is IEEE-compliant by default; titles tuned around flush-to-zero may see slightly different denormal rounding under xenia. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. - **Rounding mode** uses `FPSCR[RN]` (00 nearest-even, 01 toward 0, 10 toward +∞, 11 toward −∞). Default is nearest-even and is rarely changed. - **A-form encoding ignores `FRC`.** Bits 21–25 are don't-care for the add family. diff --git a/tools/ppc-manual/fpu/faddx.md b/tools/ppc-manual/fpu/faddx.md index 930a0d4e..307b64be 100644 --- a/tools/ppc-manual/fpu/faddx.md +++ b/tools/ppc-manual/fpu/faddx.md @@ -118,10 +118,10 @@ if Rc then - **Double precision.** `fadd` always operates on IEEE-754 binary64 regardless of whether either source was produced by a single-precision instruction. Single-precision adds use [`faddsx`](faddsx.md) and automatically round the result to binary32 precision. - **No immediate / carry / OE.** FPU arithmetic has no immediate forms, no carry, and no overflow-enable bit. `Rc` is the only modifier — it writes `CR1` from the four top FPSCR bits. -- **FPSCR is always updated.** Even the non-record form (`fadd`) updates `FPSCR[FPRF, FR, FI, FX, …]` as a side effect of execution; xenia's interpreter currently **does not** model this, so translations that rely on observing FPSCR bits across a pair of FPU instructions will diverge from hardware. If your translator needs compatible FPSCR state, emit explicit updates — or accept the simplification, which matches real Xbox 360 title behaviour in practice (titles rarely read FPSCR except via `mffs` for exception sanity checks). -- **NaN propagation.** Per IEEE-754, any NaN input produces a NaN output; PowerPC specifies that the *signalling* bit of the result NaN is cleared (quietening a signalling input). Xenia uses host-native `f64 +`, which may preserve the signalling bit on some platforms — assume quietening for correctness. -- **`±∞ − ±∞` is an invalid operation.** Produces a quiet NaN (`QNaN(VXISI)`) and sets `FPSCR[VXISI]`. Xenia emits the host-native NaN. -- **Denormal handling.** Xenon's default mode flushes denormal results to zero (FPSCR[NI] / "non-IEEE mode" bit set at boot). Xenia inherits host semantics by default; if title code explicitly clears NI (rare) you'll get IEEE-compliant denormals from the host FPU. +- **FPSCR is always updated.** Even the non-record form (`fadd`) updates `FPSCR[FPRF, FR, FI, FX, …]` on hardware. Canary does not compute them: its `UpdateFPSCR` is a stub that clears `FEX` and `VX`, leaves every other FPSCR bit unchanged and, with `Rc=1`, writes CR1 as all zeros, so translations that rely on observing FPSCR bits across a pair of FPU instructions will diverge from hardware. If your translator needs compatible FPSCR state, emit explicit updates — titles rarely read FPSCR except via `mffs` for exception sanity checks. +- **NaN propagation.** Per IEEE-754, any NaN input produces a NaN output; PowerPC specifies that the result NaN is quiet (a signalling input is quietened). Canary emits a host `f.Add` — SSE `addsd` on x64, which also returns a quietened NaN. +- **`±∞ − ±∞` is an invalid operation.** Produces a quiet NaN and sets `FPSCR[VXISI]`. Canary returns the host's default NaN and sets no FPSCR bit; note x64's default NaN is `0xFFF8_0000_0000_0000` (sign set), whereas PowerPC's is `0x7FF8_0000_0000_0000`. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. - **Rounding mode.** `FPSCR[RN]` selects one of four rounding modes (nearest-even, toward 0, toward +∞, toward −∞). Games rarely change RN from the default nearest-even. If your translator needs faithful rounding-mode support emit `fesetround` around the operation. - **Register encoding.** A-form: `FRT`, `FRA`, `FRB`, `FRC`, `Rc` — but `fadd` ignores `FRC` (the "C" multiplier operand used by `fmadd`-style ops). The `FRC` field is architecturally don't-care but typically encoded as 0. diff --git a/tools/ppc-manual/fpu/fcfidx.md b/tools/ppc-manual/fpu/fcfidx.md index 7437eb47..5883cafa 100644 --- a/tools/ppc-manual/fpu/fcfidx.md +++ b/tools/ppc-manual/fpu/fcfidx.md @@ -108,12 +108,12 @@ int InstrEmit_fcfidx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **64-bit signed integer → binary64.** Reads `FRB` as a 64-bit signed integer (the bits, interpreted as `i64`) and converts it to IEEE-754 binary64. xenia-rs implements this as `bits as i64 as f64`. -- **Loss of precision.** binary64 has 53 bits of significand, so `i64` values with magnitude > 2^53 lose low-order bits. This raises `FPSCR[XX, FX]` (inexact) on hardware. xenia-rs does not update FPSCR (xenia quirk) but the rounded value matches host `f64` rules (round-to-nearest-even by default). +- **64-bit signed integer → binary64.** Reads `FRB` as a 64-bit signed integer (the bits, interpreted as `i64`) and converts it to IEEE-754 binary64. Canary: `f.Convert(f.Cast(FRB, INT64), FLOAT64)`. +- **Loss of precision.** binary64 has 53 bits of significand, so `i64` values with magnitude > 2^53 lose low-order bits, raising `FPSCR[XX, FX]` on hardware. Canary rounds per the current rounding mode but raises no FPSCR bits (`UpdateFPSCR` is a stub). - **Always exact for `|x| <= 2^53`.** Within ±9,007,199,254,740,992 the conversion is bit-exact. -- **Rounding mode.** Uses `FPSCR[RN]`. Default nearest-even. Rust's `as f64` from `i64` uses platform-native conversion which on Xenon-target hosts will respect the FE rounding mode; xenia uses the host default. +- **Rounding mode.** Uses `FPSCR[RN]`, default nearest-even. Canary's x64 backend converts with `vcvtsi2sd`, which rounds under the host MXCSR — and Canary loads that from `FPSCR[RN]` whenever the guest writes it through `mtfsf`/`mtfsfi`. - **No NaN/∞ generation.** All `i64` inputs map to finite `f64` outputs (the largest `i64` is well below `f64::MAX`). -- **FPSCR side effects.** Hardware updates `FPRF` (result class) and may set `XX`/`FX` on inexact. xenia does not update FPSCR. +- **FPSCR side effects.** Hardware updates `FPRF` (result class) and may set `XX`/`FX` on inexact. Canary does not compute them: its `UpdateFPSCR` is a stub that clears `FEX` and `VX`, leaves every other FPSCR bit unchanged and, with `Rc=1`, writes CR1 as all zeros. - **`Rc=1` (`fcfid.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **Encoding.** X-form, primary 63, XO 846. Reads `FRB` only. - **Common pairing.** Used after `lfd` of a stored `i64` to bring an integer into the FP pipeline for arithmetic; the inverse direction is [`fctidx`](fctidx.md) / [`fctidzx`](fctidzx.md). diff --git a/tools/ppc-manual/fpu/fcmpo.md b/tools/ppc-manual/fpu/fcmpo.md index 13105dfc..5df9e09c 100644 --- a/tools/ppc-manual/fpu/fcmpo.md +++ b/tools/ppc-manual/fpu/fcmpo.md @@ -142,7 +142,7 @@ int InstrEmit_fcmpx_(PPCHIRBuilder& f, const InstrData& i, bool ordered) { - Either operand NaN → `FPSCR[VXVC] = 1` (invalid-operation: compare on QNaN/SNaN). - Either operand signalling NaN → also `FPSCR[VXSNAN] = 1`. - All NaN cases also set `FX = 1` and `VX = 1`. -- **xenia quirk.** xenia-rs's `fcmpo` body is identical to `fcmpu` — the FPSCR exception bits are not modelled. The xenia source comment explicitly notes "Same as fcmpu but sets FPSCR exception bits for QNaN (not modeled yet)". Title code that polls FPSCR for compare-class invalid-operation will not observe it. +- **Canary's `fcmpo` is `fcmpu`.** Both call `InstrEmit_fcmpx_`, which ignores its `ordered` parameter: the FPSCR exception bits (`VXVC`, `VXSNAN`) and `FPCC` are not modelled. Title code that polls FPSCR for compare-class invalid-operation will not observe it. - **CR field bits.** - `LT` (bit 0) — `FRA < FRB` - `GT` (bit 1) — `FRA > FRB` @@ -150,7 +150,7 @@ int InstrEmit_fcmpx_(PPCHIRBuilder& f, const InstrData& i, bool ordered) { - `SO` (bit 3) — unordered (NaN involved) - **`+0` and `-0` compare equal.** - **No `Rc` bit.** -- **FPSCR side effects.** Hardware updates `FPSCR[FPCC]`, `FX`, `VX`, and (on NaN) `VXVC`/`VXSNAN`. xenia-rs only updates the CR field. +- **FPSCR side effects.** Hardware updates `FPSCR[FPCC]`, `FX`, `VX`, and (on NaN) `VXVC`/`VXSNAN`. Canary only updates the CR field. - **Use case.** Ordered compares are required by C/C++ semantics for `<`, `>`, `<=`, `>=` (which must signal on NaN per IEEE-754). `fcmpu` corresponds to the C `==`/`!=` semantics (which do not signal). - **Encoding.** X-form, primary 63, XO 32. diff --git a/tools/ppc-manual/fpu/fcmpu.md b/tools/ppc-manual/fpu/fcmpu.md index f4283442..376cea7a 100644 --- a/tools/ppc-manual/fpu/fcmpu.md +++ b/tools/ppc-manual/fpu/fcmpu.md @@ -144,11 +144,11 @@ int InstrEmit_fcmpx_(PPCHIRBuilder& f, const InstrData& i, bool ordered) { - `GT` (bit 1) — `FRA > FRB` - `EQ` (bit 2) — `FRA == FRB` - `SO` (bit 3) — **unordered** (one or both operands is NaN) -- **NaN handling.** Either operand NaN → set `SO=1`, clear `LT/GT/EQ`. xenia-rs matches. -- **Signalling NaN.** Per PowerISA, `fcmpu` sets `FPSCR[VXSNAN]` if either operand is a signalling NaN, but does **not** set `FPSCR[VXVC]` (the difference vs `fcmpo`). xenia-rs does **not** model this — **xenia quirk**: `fcmpu` and `fcmpo` are observationally identical in xenia. -- **`+0` and `-0` compare equal.** Standard IEEE rule; xenia's host `<` / `>` on `f64` matches. +- **NaN handling.** Either operand NaN → set `SO=1`, clear `LT/GT/EQ`. Canary matches: it stores `IsNan(A) | IsNan(B)` in the field's fourth bit and ANDs `LT`/`GT`/`EQ` with its complement. +- **Signalling NaN.** Per PowerISA, `fcmpu` sets `FPSCR[VXSNAN]` if either operand is a signalling NaN, but does **not** set `FPSCR[VXVC]` (the difference vs `fcmpo`). Canary models neither, so `fcmpu` and `fcmpo` are observationally identical there. +- **`+0` and `-0` compare equal.** Standard IEEE rule; Canary's host float compares match. - **No `Rc` bit.** The CR field destination is encoded in the instruction (`BF`); there's no record-form variant. -- **FPSCR side effects.** Hardware updates `FPSCR[FPCC]` (the four-bit floating-point condition code) and `FPSCR[FX]`. xenia-rs does not maintain `FPCC`. +- **FPSCR side effects.** Hardware updates `FPSCR[FPCC]` (the four-bit floating-point condition code) and `FPSCR[FX]`. Canary does not maintain `FPCC`. - **Precision-agnostic.** Compares the full binary64 values; works equally for single-precision values stored in FPRs (they are bit-identical to their double-precision representation). - **Encoding.** X-form, primary 63, XO 0. Bits 9–10 of `BF` are unused (reserved 0). diff --git a/tools/ppc-manual/fpu/fctidx.md b/tools/ppc-manual/fpu/fctidx.md index a7ba7745..e0cb0f26 100644 --- a/tools/ppc-manual/fpu/fctidx.md +++ b/tools/ppc-manual/fpu/fctidx.md @@ -126,11 +126,11 @@ int InstrEmit_fctidxx_(PPCHIRBuilder& f, const InstrData& i, ## Special Cases & Edge Conditions - **binary64 → 64-bit signed integer, current rounding mode.** Result is the integer rounded per `FPSCR[RN]`, packed into the 64-bit FPR as raw bits (the FPR is reinterpreted as an `i64` by subsequent `stfd`/integer code). -- **Saturation on out-of-range.** Per PowerISA, values outside `[i64::MIN, i64::MAX]` (or NaN) yield the most-negative integer (`0x8000_0000_0000_0000`) and set `FPSCR[VXCVI, VX, FX]`. xenia-rs special-cases NaN to `0x8000_0000_0000_0000` but **does not saturate** out-of-range finite values — Rust's `as i64` from a too-large `f64` produces an undefined-then-saturated result that may differ from the PPC convention. **xenia quirk:** very-large finite inputs may round to a different sentinel than hardware. -- **xenia round implementation.** xenia uses Rust's `f64::round`, which rounds half-cases **away from zero** (NOT round-to-nearest-even). PowerISA round-to-nearest in default mode rounds half-cases to even. **xenia quirk:** values like `0.5`, `1.5`, `2.5` may produce different integers (xenia: `1, 2, 3`; PPC default: `0, 2, 2`). -- **Rounding mode.** PPC uses `FPSCR[RN]` for the rounding direction. xenia ignores the FPSCR mode and always uses `f64::round` (i.e. round-half-away-from-zero) regardless of `RN`. **xenia quirk:** non-default rounding modes are not respected. -- **Inexact.** Sets `FPSCR[XX, FX]` on any non-integer input. xenia does not update FPSCR. -- **NaN.** Returns sentinel `0x8000_0000_0000_0000` and sets `FPSCR[VXCVI]`. xenia matches the sentinel, but does not raise the FPSCR bit. +- **Saturation on out-of-range.** PowerPC saturates: large positives → `0x7FFF_FFFF_FFFF_FFFF`, large negatives and NaN → `0x8000_0000_0000_0000`, setting `FPSCR[VXCVI, VX, FX]`. Canary's x64 backend reproduces the values — NaN takes an explicit branch to `0x8000…`, `cvtsd2si` yields `0x8000…` on overflow, and a fix-up turns that into `0x7FFF…` when the input was non-negative — but raises no FPSCR bits (`UpdateFPSCR` is a stub). +- **Rounding follows `FPSCR[RN]` in Canary.** `ROUND_DYNAMIC` converts with `cvtsd2si` under the host rounding mode Canary sets from `mtfsf`/`mtfsfi`, so default round-to-nearest resolves half-cases to even: `0.5`, `1.5`, `2.5` → `0`, `2`, `2`, as PowerPC specifies. +- **Rounding mode.** PPC uses `FPSCR[RN]` for the rounding direction. Canary honours it: `ROUND_DYNAMIC` converts with `cvtsd2si` under the host rounding mode, which Canary loads from `FPSCR[RN]` whenever the guest writes it through `mtfsf`/`mtfsfi` (default round-to-nearest-even). +- **Inexact.** Sets `FPSCR[XX, FX]` on any non-integer input on hardware; Canary raises no FPSCR bits (`UpdateFPSCR` is a stub). +- **NaN.** Returns sentinel `0x8000_0000_0000_0000` and sets `FPSCR[VXCVI]`. Canary matches the sentinel through an explicit NaN branch, but does not raise the FPSCR bit. - **`Rc=1` (`fctid.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **Encoding.** X-form, primary 63, XO 814. Reads `FRB` only. - **Pair with `stfd`** to extract the `i64` value to memory or a GPR (Xbox 360 has no direct FPR↔GPR move; round-trip via stack). diff --git a/tools/ppc-manual/fpu/fctidzx.md b/tools/ppc-manual/fpu/fctidzx.md index 4514597a..88d219b2 100644 --- a/tools/ppc-manual/fpu/fctidzx.md +++ b/tools/ppc-manual/fpu/fctidzx.md @@ -124,13 +124,10 @@ int InstrEmit_fctidxx_(PPCHIRBuilder& f, const InstrData& i, ## Special Cases & Edge Conditions -- **binary64 → 64-bit signed integer, round toward zero.** The "z" suffix forces truncation regardless of `FPSCR[RN]`. xenia-rs uses Rust's `as i64` (which truncates toward zero), bypassing the FPSCR rounding mode entirely — this matches PPC `fctidz` semantics correctly. -- **Saturation on out-of-range.** PowerISA: out-of-range or NaN → `0x8000_0000_0000_0000` and `FPSCR[VXCVI, VX, FX]`. xenia handles NaN explicitly with the sentinel, but uses raw `as i64` for finite values; in current Rust (since 1.45) `as i64` from out-of-range `f64` is **defined to saturate** to `i64::MIN`/`i64::MAX`. So: - - **+∞ or large positive → `i64::MAX`** (`0x7FFF_FFFF_FFFF_FFFF`) under xenia. - - **−∞ or large negative → `i64::MIN`** (`0x8000_0000_0000_0000`) under xenia. - - **PPC** spec returns `0x8000_0000_0000_0000` for both. **xenia quirk:** positive overflow returns the wrong sentinel. +- **binary64 → 64-bit signed integer, round toward zero.** The "z" suffix forces truncation regardless of `FPSCR[RN]`. Canary passes `ROUND_TO_ZERO`, which its x64 backend emits as `cvttsd2si` — matching PPC `fctidz` semantics. +- **Saturation on out-of-range.** PowerPC saturates: `+∞` or a large positive → `0x7FFF_FFFF_FFFF_FFFF`, `−∞` or a large negative → `0x8000_0000_0000_0000`, both setting `FPSCR[VXCVI, VX, FX]`. Canary's x64 backend reproduces both values — `cvttsd2si` yields `0x8000…` on any overflow and a fix-up turns it into `0x7FFF…` when the input was non-negative — but raises no FPSCR bits (`UpdateFPSCR` is a stub). - **NaN.** Returns sentinel `0x8000_0000_0000_0000` (matches PPC). -- **Inexact.** Sets `FPSCR[XX, FX]` on any non-integer input. xenia does not update FPSCR (xenia quirk). +- **Inexact.** Sets `FPSCR[XX, FX]` on any non-integer input on hardware; Canary raises no FPSCR bits (`UpdateFPSCR` is a stub). - **No `FPSCR[RN]` dependence.** `fctidz` always truncates; this is the right choice for C/C++ `(int64_t)` casts. - **`Rc=1` (`fctidz.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **Encoding.** X-form, primary 63, XO 815. Reads `FRB` only. @@ -138,7 +135,7 @@ int InstrEmit_fctidxx_(PPCHIRBuilder& f, const InstrData& i, ## Related Instructions -- [`fctidx`](fctidx.md) — same conversion but uses `FPSCR[RN]` (default nearest-even on PPC; xenia uses `f64::round` regardless). +- [`fctidx`](fctidx.md) — same conversion but uses `FPSCR[RN]` (default nearest-even), which Canary honours through the rounding mode `mtfsf`/`mtfsfi` set. - [`fctiwzx`](fctiwzx.md) — 32-bit truncating variant. - [`fctiwx`](fctiwx.md) — 32-bit `FPSCR[RN]`-rounded variant. - [`fcfidx`](fcfidx.md) — inverse direction. diff --git a/tools/ppc-manual/fpu/fctiwx.md b/tools/ppc-manual/fpu/fctiwx.md index ce69e924..2fdc7169 100644 --- a/tools/ppc-manual/fpu/fctiwx.md +++ b/tools/ppc-manual/fpu/fctiwx.md @@ -125,12 +125,12 @@ int InstrEmit_fctiwxx_(PPCHIRBuilder& f, const InstrData& i, ## Special Cases & Edge Conditions -- **binary64 → 32-bit signed integer, current rounding mode.** Result is rounded per `FPSCR[RN]` and packed into the low 32 bits of the destination FPR. The high 32 bits are architecturally undefined per PowerISA but xenia produces zero-extended `u32` (i.e. the high 32 bits are 0). -- **Explicit saturation in xenia.** xenia's body clamps the rounded `f64` to `[i32::MIN as f64, i32::MAX as f64]` before the integer cast — this matches PPC's saturation behaviour for out-of-range positive/negative finite inputs. -- **NaN sentinel.** xenia returns `0x0000_0000_8000_0000` for NaN inputs (i.e. `i32::MIN` in the low word). Matches PPC's `VXCVI` sentinel for NaN/out-of-range. -- **Rounding implementation.** xenia uses `f64::round`, which rounds half-cases **away from zero** rather than to nearest-even. **xenia quirk:** values like `0.5`/`1.5`/`2.5` produce `1`/`2`/`3` under xenia vs `0`/`2`/`2` on PPC default rounding. -- **`FPSCR[RN]` not honored.** xenia always uses `f64::round`, ignoring the rounding-mode field. **xenia quirk** for non-default modes. -- **FPSCR side effects.** PPC: sets `XX`/`FX` on inexact, `VXCVI` on NaN/out-of-range. xenia does not update FPSCR. +- **binary64 → 32-bit signed integer, current rounding mode.** Result is rounded per `FPSCR[RN]` and packed into the low 32 bits of the destination FPR. The high 32 bits are architecturally undefined per PowerISA; Canary sign-extends the 32-bit result into them (for a NaN input it stores `0x0000_0000_8000_0000`). +- **Saturation in Canary.** Its x64 backend clamps the input to `(double)0x7FFFFFFF` (`vminsd`) before `cvtsd2si`, which already returns `0x80000000` for large negatives — so both directions saturate as PowerPC specifies. +- **NaN sentinel.** Canary branches on NaN explicitly and stores `0x0000_0000_8000_0000` — `i32::MIN` in the low word, PPC's `VXCVI` sentinel, with a zero high word. For ordinary results it instead sign-extends the 32-bit integer into the high word (`SignExtend(v, INT64)`); PowerPC leaves those bits undefined. +- **Rounding follows `FPSCR[RN]` in Canary.** `ROUND_DYNAMIC` converts under the host rounding mode Canary sets from `mtfsf`/`mtfsfi`, so default round-to-nearest gives `0`/`2`/`2` for `0.5`/`1.5`/`2.5`, as on PPC. +- **`FPSCR[RN]` honoured in Canary.** `ROUND_DYNAMIC` converts under the host rounding mode, which Canary loads from `FPSCR[RN]` whenever the guest writes it through `mtfsf`/`mtfsfi`. +- **FPSCR side effects.** PPC sets `XX`/`FX` on inexact and `VXCVI` on NaN/out-of-range; Canary raises no FPSCR bits (`UpdateFPSCR` is a stub). - **`Rc=1` (`fctiw.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **Encoding.** X-form, primary 63, XO 14. Reads `FRB` only. - **Common pairing.** Followed by `stfiwx` to store the low-32-bit integer to memory (`stfd` would write the doubleword including the high bits, which on hardware are undefined). @@ -141,7 +141,7 @@ int InstrEmit_fctiwxx_(PPCHIRBuilder& f, const InstrData& i, - [`fctidx`](fctidx.md), [`fctidzx`](fctidzx.md) — 64-bit integer variants. - [`fcfidx`](fcfidx.md) — inverse direction (i64 → f64); for i32 → f64, sign-extend then `fcfid`. - `stfiwx` — store low-32-bits FPR (the canonical companion to `fctiw`/`fctiwz`). -- [`mffsx`](mffsx.md), [`mtfsfx`](mtfsfx.md) — control `FPSCR[RN]` (currently a no-op under xenia for this instruction). +- [`mffsx`](../control/mffsx.md), [`mtfsfx`](../control/mtfsfx.md) — control `FPSCR[RN]`, which Canary honours for this instruction. ## IBM Reference diff --git a/tools/ppc-manual/fpu/fctiwzx.md b/tools/ppc-manual/fpu/fctiwzx.md index 4d6745bd..b11caf7e 100644 --- a/tools/ppc-manual/fpu/fctiwzx.md +++ b/tools/ppc-manual/fpu/fctiwzx.md @@ -125,12 +125,12 @@ int InstrEmit_fctiwxx_(PPCHIRBuilder& f, const InstrData& i, ## Special Cases & Edge Conditions -- **binary64 → 32-bit signed integer, round toward zero.** Truncates regardless of `FPSCR[RN]`. xenia-rs uses `clamp` to saturate to `[i32::MIN, i32::MAX]` then `as i32`, which truncates — matching PPC `fctiwz` semantics. +- **binary64 → 32-bit signed integer, round toward zero.** Truncates regardless of `FPSCR[RN]`. Canary clamps to `(double)0x7FFFFFFF` and converts with `cvttsd2si`, which truncates — matching PPC `fctiwz` semantics. - **Most common conversion in compiled code.** Translates C/C++ `(int32_t)f` casts, which require truncation per the C standard. -- **Saturation on out-of-range.** Hardware saturates to `i32::MAX` for large positives, `i32::MIN` for large negatives or NaN, and sets `FPSCR[VXCVI, VX, FX]`. xenia's explicit `clamp` correctly reproduces the saturation, but does not raise FPSCR bits (xenia quirk). -- **NaN sentinel.** xenia returns `0x0000_0000_8000_0000` (i.e. `i32::MIN` in low 32 bits). Matches PPC sentinel. -- **High 32 bits of FPR.** Architecturally undefined per PowerISA, but xenia produces zero-extended `u32`. Use `stfiwx` (store low 32 bits) — never `stfd` — for the canonical "store this integer" idiom. -- **Inexact.** Sets `FPSCR[XX, FX]` on any non-integer input. xenia does not update FPSCR. +- **Saturation on out-of-range.** Hardware saturates to `i32::MAX` for large positives, `i32::MIN` for large negatives or NaN, and sets `FPSCR[VXCVI, VX, FX]`. Canary reproduces the values (the clamp, `cvttsd2si`'s `0x80000000`, and an explicit NaN branch) but raises no FPSCR bits (`UpdateFPSCR` is a stub). +- **NaN sentinel.** Canary stores `0x0000_0000_8000_0000` — `i32::MIN` in the low word (the PPC sentinel), high word zero. +- **High 32 bits of FPR.** Architecturally undefined per PowerISA; Canary sign-extends the 32-bit result into them (a NaN input gives `0x0000_0000_8000_0000`). Use `stfiwx` (store low 32 bits) — never `stfd` — for the canonical "store this integer" idiom. +- **Inexact.** Sets `FPSCR[XX, FX]` on any non-integer input on hardware; Canary raises no FPSCR bits (`UpdateFPSCR` is a stub). - **`Rc=1` (`fctiwz.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **Encoding.** X-form, primary 63, XO 15. Reads `FRB` only. diff --git a/tools/ppc-manual/fpu/fdivsx.md b/tools/ppc-manual/fpu/fdivsx.md index 4727553e..edce1450 100644 --- a/tools/ppc-manual/fpu/fdivsx.md +++ b/tools/ppc-manual/fpu/fdivsx.md @@ -120,8 +120,8 @@ int InstrEmit_fdivsx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Single precision.** Result is rounded to IEEE-754 binary32 then re-encoded into the 64-bit FPR. xenia computes `to_single(a / b)`. -- **Divide by zero.** Finite/±0 sets `FPSCR[ZX, FX]` and yields ±∞. xenia returns the host ±∞ but does not update FPSCR (xenia quirk). +- **Single precision.** Result is rounded to IEEE-754 binary32 then re-encoded into the 64-bit FPR. Canary computes `ToSingle(Div(a, b))`: `vdivsd`, then `vcvtsd2ss` + `vcvtss2sd` under the host rounding mode. +- **Divide by zero.** Finite/±0 sets `FPSCR[ZX, FX]` and yields ±∞. Canary returns the host ±∞ but sets no FPSCR bit (`UpdateFPSCR` is a stub). - **`0 / 0`** → `FPSCR[VXZDZ, VX, FX]`, quiet NaN result. - **`±∞ / ±∞`** → `FPSCR[VXIDI, VX, FX]`, quiet NaN result. - **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, plus exception bits `OX`, `UX`, `XX`, `ZX`, `VXZDZ`, `VXIDI`, `VXSNAN`. @@ -129,7 +129,7 @@ int InstrEmit_fdivsx(PPCHIRBuilder& f, const InstrData& i) { - **NaN propagation.** Quiet-NaN result for any NaN operand; signalling NaNs are quietened. - **Single-precision overflow** returns ±∞ and sets `OX`/`XX`/`FX`. - **Performance.** Hardware divide is multi-cycle. Title code commonly uses `fres` + Newton-Raphson for hot loops; this instruction is reserved for non-critical paths. -- **Denormal flush.** Xenon boots with `FPSCR[NI]=1`; xenia uses host IEEE behavior. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. - **Encoding.** A-form, primary 59, XO 18. ## Related Instructions diff --git a/tools/ppc-manual/fpu/fdivx.md b/tools/ppc-manual/fpu/fdivx.md index a3e8434b..aa79dae0 100644 --- a/tools/ppc-manual/fpu/fdivx.md +++ b/tools/ppc-manual/fpu/fdivx.md @@ -114,14 +114,14 @@ int InstrEmit_fdivx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Double precision.** Operates on IEEE-754 binary64; [`fdivsx`](fdivsx.md) is the single-precision sibling. -- **Divide by zero.** `FRA / ±0` (with `FRA` finite, non-zero) sets `FPSCR[ZX, FX]` and produces a correctly-signed infinity. xenia relies on host `f64 /`, which produces the same ±∞ — but does not raise `ZX` in the interpreter snapshot. **xenia quirk:** title code that polls FPSCR for divide-by-zero will not observe it. +- **Divide by zero.** `FRA / ±0` (with `FRA` finite, non-zero) sets `FPSCR[ZX, FX]` and produces a correctly-signed infinity. Canary's `vdivsd` produces the same ±∞ — but does not raise `ZX` (`UpdateFPSCR` is a stub). **Canary quirk:** title code that polls FPSCR for divide-by-zero will not observe it. - **`0 / 0`** sets `FPSCR[VXZDZ, VX, FX]` and yields a quiet NaN. - **`±∞ / ±∞`** sets `FPSCR[VXIDI, VX, FX]` and yields a quiet NaN. -- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX` plus exception bits `OX`, `UX`, `XX`, `ZX`, `VXZDZ`, `VXIDI`, `VXSNAN`. xenia-rs does not maintain these. +- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX` plus exception bits `OX`, `UX`, `XX`, `ZX`, `VXZDZ`, `VXIDI`, `VXSNAN`. Canary does not maintain these: its `UpdateFPSCR` is a stub that clears `FEX`/`VX` and, with `Rc=1`, writes CR1 as all zeros. - **`Rc=1` (`fdiv.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **NaN propagation.** Quiet-NaN result for any NaN operand; signalling NaNs are quietened. - **Performance.** Hardware divide is multi-cycle and not pipelined on Xenon. Many titles prefer `fres`/`frsqrte` followed by Newton-Raphson refinement (or by `fmadd` chains) to avoid the divider. -- **Denormal flush.** Xenon boots with `FPSCR[NI]=1`; xenia uses host IEEE. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. - **Encoding.** A-form, primary 63, XO 18. `FRC` is don't-care. ## Related Instructions diff --git a/tools/ppc-manual/fpu/fmaddsx.md b/tools/ppc-manual/fpu/fmaddsx.md index b0df1aa3..baabc208 100644 --- a/tools/ppc-manual/fpu/fmaddsx.md +++ b/tools/ppc-manual/fpu/fmaddsx.md @@ -107,15 +107,15 @@ int InstrEmit_fmaddsx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Single rounding step then single-precision rounding.** PowerISA semantics: compute `(FRA × FRC) + FRB` to infinite precision, then round once to binary32. xenia-rs implements this as `to_single(a.mul_add(c, b))` — the `mul_add` is the single-step fused multiply-add at double precision, then `to_single` rounds the binary64 result to binary32. This matches PPC's "single rounding" requirement because the intermediate `mul_add` is already exact-rounded. +- **Single rounding step then single-precision rounding.** PowerISA semantics: compute `(FRA × FRC) + FRB` to infinite precision, then round once to binary32. Canary rounds twice — the multiply-add in binary64 (`vfmadd213sd` on FMA3 hosts, `vmulsd` + `vaddsd` otherwise), then `ToSingle` to binary32. With FMA3 the result can differ only when the binary64 intermediate lands exactly on a binary32 tie under round-to-nearest; the non-FMA fallback also rounds the product on its own. - **Operand order.** Assembler: `FD, FA, FC, FB` (multiplier `FRC` before addend `FRB`). - **Invalid operations.** `0×∞ + finite` → `VXIMZ`; opposite-signed-∞ collision → `VXISI`. Quiet NaN result with `FPSCR[VX, FX]`. -- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. xenia-rs does not (xenia quirk). +- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. Canary does not (`UpdateFPSCR` is a stub). - **`Rc=1` (`fmadds.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **NaN propagation.** Quiet-NaN result for any NaN operand; signalling NaNs are quietened. - **Single-precision overflow** of the final rounded result returns ±∞ and sets `OX`/`XX`/`FX`. - **Use case.** Dominates single-precision graphics math: matrix–vector multiplies, dot products, lighting equations, normal-map blending. Xbox 360 titles emit `fmadds` constantly. -- **Denormal flush.** Xenon boots with `FPSCR[NI]=1`; xenia uses host IEEE behavior. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. ## Related Instructions diff --git a/tools/ppc-manual/fpu/fmaddx.md b/tools/ppc-manual/fpu/fmaddx.md index 1bbfacb6..0d863d02 100644 --- a/tools/ppc-manual/fpu/fmaddx.md +++ b/tools/ppc-manual/fpu/fmaddx.md @@ -102,14 +102,14 @@ int InstrEmit_fmaddx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Single rounding step.** `fmadd` computes `(FRA × FRC) + FRB` with one IEEE-754 rounding at the end — strictly more accurate than separate multiply + add. xenia-rs uses Rust's `f64::mul_add`, which guarantees a true FMA on hosts with hardware FMA (x86_64 with FMA3, ARM with NEON-FMA); on hosts without it, Rust's stdlib falls back to a software FMA so the semantic match is preserved. +- **Single rounding step.** `fmadd` computes `(FRA × FRC) + FRB` with one IEEE-754 rounding at the end — strictly more accurate than separate multiply + add. Canary matches that only on hosts with FMA3, where it emits `vfmadd213sd`; without FMA3 it falls back to `vmulsd` + `vaddsd`, which rounds twice and can differ in the last bit. - **Operand layout.** A-form: `FRT, FRA, FRC, FRB`. Note the assembler order — `FRC` (multiplier) comes before `FRB` (addend). Encoding bit fields are `FRA` (11–15), `FRB` (16–20), `FRC` (21–25). - **Invalid operations.** `0×∞ + finite` → `VXIMZ`; `∞×x + ∓∞` (after multiplication produces ±∞ that opposes addend sign) → `VXISI`. Quiet NaN result with `FPSCR[VX, FX]` set. -- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. xenia-rs does not update FPSCR (xenia quirk). +- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. Canary does not update FPSCR (`UpdateFPSCR` is a stub). - **`Rc=1` (`fmadd.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **NaN propagation.** Quiet-NaN result for any NaN operand; signalling NaNs are quietened. - **Use case.** Dot products, polynomial evaluation (Horner's method), matrix multiplies, Newton-Raphson divide/sqrt refinement. Hot-path PPC code is dense with `fmadd`. -- **Denormal flush.** Xenon boots with `FPSCR[NI]=1`; xenia uses host IEEE behavior. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. ## Related Instructions diff --git a/tools/ppc-manual/fpu/fmrx.md b/tools/ppc-manual/fpu/fmrx.md index 06ce9860..a546f609 100644 --- a/tools/ppc-manual/fpu/fmrx.md +++ b/tools/ppc-manual/fpu/fmrx.md @@ -102,7 +102,7 @@ int InstrEmit_fmrx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Bit-pattern copy, no rounding.** `fmr` copies the 64-bit binary representation of `FRB` into `FRT` unchanged. No precision loss, no FPSCR exception bits, no NaN quietening. xenia-rs implements this as a plain `f64` copy. +- **Bit-pattern copy, no rounding.** `fmr` copies the 64-bit binary representation of `FRB` into `FRT` unchanged. No precision loss, no FPSCR exception bits, no NaN quietening. Canary copies the value bit-exactly, but also runs its `UpdateFPSCR` stub, which clears `FPSCR[FEX, VX]` — bits a real `fmr` leaves alone. - **NaN preserved verbatim.** Signalling/quiet bit, payload, and sign are all preserved exactly. Unlike arithmetic instructions, `fmr` does **not** quieten signalling NaNs. - **Special values.** All bit patterns pass through untouched, including ±0, ±∞, and any NaN. The destination receives an exact copy. - **FPSCR.** Hardware does **not** update `FPRF` or any exception bit. The "FPSCR write" implied in the header refers only to `Rc=1` updating CR1 from existing FPSCR contents. diff --git a/tools/ppc-manual/fpu/fmsubsx.md b/tools/ppc-manual/fpu/fmsubsx.md index ed12fa89..6e99e850 100644 --- a/tools/ppc-manual/fpu/fmsubsx.md +++ b/tools/ppc-manual/fpu/fmsubsx.md @@ -107,15 +107,15 @@ int InstrEmit_fmsubsx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Single rounding step then round-to-single.** Computes `(FRA × FRC) − FRB` with one fused rounding at double precision, then rounds the binary64 result to binary32. xenia-rs implements this as `to_single(a.mul_add(c, -b))`. +- **Single rounding step then round-to-single.** PowerISA computes `(FRA × FRC) − FRB` exactly and rounds once to binary32. Canary rounds twice: `MulSub` in binary64 (`vfmsub213sd` on FMA3 hosts, `vmulsd` + `vsubsd` otherwise), then `ToSingle`. - **Operand order.** Assembler: `FD, FA, FC, FB`. The multiplier `FRC` precedes the addend `FRB`. - **Invalid operations.** `0×∞ − finite` → `VXIMZ`; `(±∞×x) − ±∞` (same sign) → `VXISI`. Quiet NaN result with `FPSCR[VX, FX]`. -- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. xenia-rs does not (xenia quirk). +- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. Canary does not (`UpdateFPSCR` is a stub). - **`Rc=1` (`fmsubs.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **NaN propagation.** Quiet-NaN result for any NaN operand; signalling NaNs are quietened. - **Single-precision overflow** of the final rounded result returns ±∞ and sets `OX`/`XX`/`FX`. - **Use case.** Newton-Raphson refinement of `fres`: `x_new = x*(2 - d*x)` decomposes to a `fmsubs`/`fnmsubs` pair. Also common in residual-correction loops. -- **Denormal flush.** Xenon boots with `FPSCR[NI]=1`; xenia uses host IEEE behavior. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. ## Related Instructions diff --git a/tools/ppc-manual/fpu/fmsubx.md b/tools/ppc-manual/fpu/fmsubx.md index 8bd15981..1c46198a 100644 --- a/tools/ppc-manual/fpu/fmsubx.md +++ b/tools/ppc-manual/fpu/fmsubx.md @@ -102,15 +102,15 @@ int InstrEmit_fmsubx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Single rounding step.** `fmsub` computes `(FRA × FRC) − FRB` with one rounding at the end. xenia-rs implements this as `a.mul_add(c, -b)`, which is a true FMA on hosts that have hardware support and a software FMA on those that don't. +- **Single rounding step.** `fmsub` computes `(FRA × FRC) − FRB` with one rounding at the end. Canary emits `vfmsub213sd` on FMA3 hosts; without FMA3 it falls back to `vmulsd` + `vsubsd`, which rounds twice. - **Subtle: negate-then-FMA.** Negating `b` before passing to FMA matters for sign of zero and overflow. `(+0×+0) − (+0)` = `+0` in round-to-nearest, but `(+0×+0) − (−0)` = `+0` (the negation flips it before the FMA). Standard IEEE rules apply. - **Operand order.** Assembler: `FD, FA, FC, FB`. - **Invalid operations.** `0×∞ − finite` → `VXIMZ`; same-signed infinity collision (e.g. `(+∞×+1) − (+∞)`) → `VXISI`. Quiet NaN result with `FPSCR[VX, FX]`. -- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. xenia-rs does not update FPSCR (xenia quirk). +- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. Canary does not update FPSCR (`UpdateFPSCR` is a stub). - **`Rc=1` (`fmsub.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **NaN propagation.** Quiet-NaN result for any NaN operand; signalling NaNs are quietened. - **Use case.** Newton-Raphson refinement of reciprocal estimates: `x_new = x*(2 - d*x) = -((d*x) - 2)` uses `fnmsub`, but `fmsub` shows up wherever `(a*c) - b` appears (residuals, error correction). -- **Denormal flush.** Xenon boots with `FPSCR[NI]=1`; xenia uses host IEEE behavior. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. ## Related Instructions @@ -118,7 +118,7 @@ int InstrEmit_fmsubx(PPCHIRBuilder& f, const InstrData& i) { - [`fmaddx`](fmaddx.md), [`fnmaddx`](fnmaddx.md), [`fnmsubx`](fnmsubx.md) — other fused multiply-add variants. - [`fmulx`](fmulx.md), [`fsubx`](fsubx.md) — non-fused decomposition (two rounding steps). - [`fresx`](fresx.md), [`frsqrtex`](frsqrtex.md) — reciprocal helpers refined by fused multiply-subtracts. -- [`fnegx`](fnegx.md) — sign flip (the bit-pattern op behind `-FRB` in xenia's implementation). +- [`fnegx`](fnegx.md) — sign flip; Canary applies the same `Neg` to the fused result in `fnmsub`. ## IBM Reference diff --git a/tools/ppc-manual/fpu/fmulsx.md b/tools/ppc-manual/fpu/fmulsx.md index fab0b9c3..1148c6bf 100644 --- a/tools/ppc-manual/fpu/fmulsx.md +++ b/tools/ppc-manual/fpu/fmulsx.md @@ -111,14 +111,14 @@ int InstrEmit_fmulsx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **A-form quirk: multiplier is `FRC`.** Operands come from `FRA` (bits 11–15) and `FRC` (bits 21–25). xenia decodes via `instr.rc()` (don't confuse with `rc_bit()` for the record bit). -- **Single precision.** Result is rounded to IEEE-754 binary32 then re-encoded into the 64-bit FPR. xenia uses `to_single(a * c)`. +- **A-form quirk: multiplier is `FRC`.** Operands come from `FRA` (bits 11–15) and `FRC` (bits 21–25). Canary reads the multiplier from `i.A.FRC` (don't confuse it with the record bit `Rc`). +- **Single precision.** Result is rounded to IEEE-754 binary32 then re-encoded into the 64-bit FPR. Canary computes `ToSingle(Mul(a, c))`: `vmulsd`, then `vcvtsd2ss` + `vcvtss2sd`. - **`0 × ±∞`** sets `FPSCR[VXIMZ, VX, FX]` and yields a quiet NaN. -- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX` and exception bits `OX`, `UX`, `XX`, `VXIMZ`, `VXSNAN`. xenia-rs does **not** maintain FPSCR (xenia quirk). +- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX` and exception bits `OX`, `UX`, `XX`, `VXIMZ`, `VXSNAN`. Canary does **not** maintain FPSCR (`UpdateFPSCR` is a stub). - **`Rc=1` (`fmuls.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **NaN propagation.** Quiet-NaN result for any NaN operand; signalling NaNs are quietened. - **Single-precision overflow** returns ±∞ and sets `OX`/`XX`/`FX`. -- **Denormal flush.** Xenon boots with `FPSCR[NI]=1`; xenia inherits host IEEE behavior, so subnormal results may differ subtly from hardware. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. - **Encoding.** A-form, primary 59, XO 25. ## Related Instructions diff --git a/tools/ppc-manual/fpu/fmulx.md b/tools/ppc-manual/fpu/fmulx.md index cf1ad7d6..901bd6dc 100644 --- a/tools/ppc-manual/fpu/fmulx.md +++ b/tools/ppc-manual/fpu/fmulx.md @@ -105,14 +105,14 @@ int InstrEmit_fmulx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **A-form quirk: multiplier is `FRC`, not `FRB`.** `fmul` reads operands from the `FRA` (bits 11–15) and `FRC` (bits 21–25) fields, bridging the multiply and fused-multiply-add families. xenia decodes this as `instr.rc()` (the FRC field, distinct from `rc_bit()` for the record bit). +- **A-form quirk: multiplier is `FRC`, not `FRB`.** `fmul` reads operands from the `FRA` (bits 11–15) and `FRC` (bits 21–25) fields, bridging the multiply and fused-multiply-add families. Canary reads the multiplier from `i.A.FRC` (distinct from the record bit `Rc`). - **Double precision.** Operates on IEEE-754 binary64; [`fmulsx`](fmulsx.md) rounds to binary32. - **`0 × ±∞` is invalid.** Sets `FPSCR[VXIMZ, VX, FX]` and yields a quiet NaN. -- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX` plus exception bits `OX` (overflow), `UX` (underflow), `XX` (inexact), `VXIMZ` (0×∞), `VXSNAN` (signalling NaN). xenia-rs does **not** update FPSCR in the interpreter snapshot — xenia quirk. +- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX` plus exception bits `OX` (overflow), `UX` (underflow), `XX` (inexact), `VXIMZ` (0×∞), `VXSNAN` (signalling NaN). Canary does **not** update FPSCR (`UpdateFPSCR` is a stub). - **`Rc=1` (`fmul.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **NaN propagation.** Any NaN operand yields a quiet NaN; signalling NaNs are quietened. - **Sign of result.** Standard IEEE: `sign(a) XOR sign(c)`. `+0 × −0 = −0` and `−x × +∞ = −∞`. -- **Denormal flush.** Xenon boots with `FPSCR[NI]=1` (flush-to-zero); xenia inherits host IEEE behavior, so multiplications that produce subnormal results may differ subtly from hardware. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. - **Rounding mode** uses `FPSCR[RN]` (default nearest-even). ## Related Instructions diff --git a/tools/ppc-manual/fpu/fnabsx.md b/tools/ppc-manual/fpu/fnabsx.md index c6a04a30..dcd9f6b1 100644 --- a/tools/ppc-manual/fpu/fnabsx.md +++ b/tools/ppc-manual/fpu/fnabsx.md @@ -105,7 +105,7 @@ int InstrEmit_fnabsx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Bit-pattern operation, no rounding.** `fnabs` **sets** the sign bit (bit 0) of the source binary64 value to 1, producing `-|FRB|`. No precision change, no exception bits. xenia-rs implements this as `-(b.abs())` — the abs clears the sign bit, then negation sets it. +- **Bit-pattern operation, no rounding.** `fnabs` **sets** the sign bit (bit 0) of the source binary64 value to 1, producing `-|FRB|`. No precision change, no exception bits. Canary emits `Neg(Abs(b))` — `vandpd` clears the sign bit, then `vxorpd` sets it. ⚠️ Its `Rc=1` branch is empty, so `fnabs.` does not update CR1 in Canary. - **NaN handling.** Returns the source NaN with the sign bit set to 1; payload preserved; signalling/quiet bit unchanged. `FPSCR[VXSNAN]` is **not** raised. - **Special values.** `fnabs(±0) = -0`; `fnabs(±∞) = -∞`; `fnabs(±NaN) = -NaN` (sign set, payload preserved). - **FPSCR.** Hardware does not update `FPRF` and does not raise any exception bit. Sign-bit ops are not arithmetic. diff --git a/tools/ppc-manual/fpu/fnegx.md b/tools/ppc-manual/fpu/fnegx.md index 6e7ff8ea..db2d037d 100644 --- a/tools/ppc-manual/fpu/fnegx.md +++ b/tools/ppc-manual/fpu/fnegx.md @@ -106,7 +106,7 @@ int InstrEmit_fnegx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Bit-pattern operation, no rounding.** `fneg` toggles the sign bit (bit 0) of the source binary64 value and writes the 64-bit pattern to the destination. No precision change, no exception bits. -- **NaN handling.** PowerISA specifies that `fneg` toggles the NaN sign bit (unlike `fnmadd` which does **not**). xenia-rs uses Rust's unary `-`, which toggles the sign bit on NaN values for binary64 — semantic match. +- **NaN handling.** PowerISA specifies that `fneg` toggles the NaN sign bit (unlike `fnmadd` which does **not**). Canary's `Neg` is `vxorpd` with the sign mask, which toggles NaN signs too — a match. ⚠️ Its `Rc=1` branch is empty, so `fneg.` does not update CR1 in Canary. - **Special values.** `fneg(+0) = -0`; `fneg(-0) = +0`; `fneg(±∞) = ∓∞`. No `FPSCR[VXSNAN]` raised even on signalling NaN inputs (sign-bit ops are not arithmetic). - **FPSCR.** Hardware does **not** update `FPRF` and does **not** raise any exception bit. The "FPSCR write" in the header refers only to `Rc=1` updating CR1 from existing FPSCR contents. - **`Rc=1` (`fneg.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. diff --git a/tools/ppc-manual/fpu/fnmaddsx.md b/tools/ppc-manual/fpu/fnmaddsx.md index 4667d381..61ebd8dc 100644 --- a/tools/ppc-manual/fpu/fnmaddsx.md +++ b/tools/ppc-manual/fpu/fnmaddsx.md @@ -113,16 +113,16 @@ int InstrEmit_fnmaddsx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Single rounding then negate then round-to-single.** Computes `−((FRA × FRC) + FRB)` and rounds to binary32. xenia-rs uses `to_single(-(a.mul_add(c, b)))` — the negation is a sign-flip on the binary64 intermediate, then `to_single` rounds to binary32. -- **NaN sign behaviour.** PowerISA specifies the negation does **not** flip the sign bit of a NaN result. xenia uses Rust's `Neg`, which does flip the NaN sign bit. Observable only via bit-level inspection. **xenia quirk.** +- **Single rounding then negate then round-to-single.** Computes `−((FRA × FRC) + FRB)` and rounds to binary32. Canary emits `ToSingle(Neg(MulAdd(a, c, b)))` — the negation is a sign flip on the binary64 intermediate, then `ToSingle` rounds to binary32. +- **NaN sign behaviour.** PowerISA specifies the negation does **not** flip the sign bit of a NaN result. Canary's `Neg` is a plain `vxorpd` sign flip, so it does. Observable only via bit-level inspection. **Canary quirk.** - **Operand order.** Assembler: `FD, FA, FC, FB`. - **Invalid operations.** `0×∞` → `VXIMZ`; opposing-infinity collision → `VXISI`. Quiet NaN result with `FPSCR[VX, FX]`. -- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. xenia-rs does not (xenia quirk). +- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. Canary does not (`UpdateFPSCR` is a stub). - **`Rc=1` (`fnmadds.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **NaN propagation.** Quiet-NaN result for any NaN operand; signalling NaNs are quietened. - **Single-precision overflow** returns ±∞ and sets `OX`/`XX`/`FX`. - **Use case.** Single-precision Newton-Raphson refinement and graphics-pipeline math where the negated product-sum form is convenient. -- **Denormal flush.** Xenon boots with `FPSCR[NI]=1`; xenia uses host IEEE behavior. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. ## Related Instructions diff --git a/tools/ppc-manual/fpu/fnmaddx.md b/tools/ppc-manual/fpu/fnmaddx.md index b56b7252..8305fa53 100644 --- a/tools/ppc-manual/fpu/fnmaddx.md +++ b/tools/ppc-manual/fpu/fnmaddx.md @@ -107,15 +107,15 @@ int InstrEmit_fnmaddx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Single rounding step, then sign flip.** Computes `−((FRA × FRC) + FRB)` with one fused rounding for the FMA; the final negation is a bit-pattern sign-flip and does not introduce additional rounding error. xenia-rs implements this as `-(a.mul_add(c, b))`. -- **Sign of NaN.** Per PowerISA, `fnmadd` does **not** flip the sign of a NaN result. xenia uses Rust's `Neg` which does flip the NaN sign bit (`f64::neg`); for IEEE-754 binary64 this is observable through bit-level inspection but not through arithmetic comparisons. **xenia quirk** — title code that inspects NaN sign bits will diverge. +- **Single rounding step, then sign flip.** Computes `−((FRA × FRC) + FRB)` with one fused rounding for the FMA; the final negation is a bit-pattern sign-flip and does not introduce additional rounding error. Canary emits `Neg(MulAdd(a, c, b))`, fused only on FMA3 hosts (`vfmadd213sd`, otherwise `vmulsd` + `vaddsd`). +- **Sign of NaN.** Per PowerISA, `fnmadd` does **not** flip the sign of a NaN result. Canary's `Neg` (`vxorpd` with the sign mask) does; this is observable through bit-level inspection but not through arithmetic comparisons. **Canary quirk** — title code that inspects NaN sign bits will diverge. - **Operand order.** Assembler: `FD, FA, FC, FB`. - **Invalid operations.** Same as `fmadd`: `VXIMZ` for `0×∞`, `VXISI` for opposing-infinity collision. Quiet NaN result. -- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. xenia-rs does not (xenia quirk). +- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. Canary does not (`UpdateFPSCR` is a stub). - **`Rc=1` (`fnmadd.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **NaN propagation.** Quiet-NaN result for any NaN operand; signalling NaNs are quietened. - **Use case.** Computing `-a*c - b` directly without an intermediate negate. Useful in iterative solvers and in transforming polynomial coefficients. -- **Denormal flush.** Xenon boots with `FPSCR[NI]=1`; xenia uses host IEEE behavior. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. ## Related Instructions diff --git a/tools/ppc-manual/fpu/fnmsubsx.md b/tools/ppc-manual/fpu/fnmsubsx.md index 62e55168..5439ff3f 100644 --- a/tools/ppc-manual/fpu/fnmsubsx.md +++ b/tools/ppc-manual/fpu/fnmsubsx.md @@ -113,16 +113,16 @@ int InstrEmit_fnmsubsx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Single rounding then negate then round-to-single.** Computes `−((FRA × FRC) − FRB)` = `FRB − (FRA × FRC)` with one fused rounding at double precision, then rounds to binary32. xenia-rs uses `to_single(-(a.mul_add(c, -b)))`. -- **NaN sign behaviour.** PowerISA: the negation does **not** flip a NaN's sign bit. xenia uses Rust's `Neg` which does. Observable only by bit-level inspection. **xenia quirk.** +- **Single rounding then negate then round-to-single.** Computes `−((FRA × FRC) − FRB)` = `FRB − (FRA × FRC)` and rounds to binary32. Canary emits `ToSingle(Neg(MulSub(a, c, b)))`, the multiply-subtract fused only on FMA3 hosts (`vfmsub213sd`, otherwise `vmulsd` + `vsubsd`). +- **NaN sign behaviour.** PowerISA: the negation does **not** flip a NaN's sign bit. Canary's `Neg` (`vxorpd`) does. Observable only by bit-level inspection. **Canary quirk.** - **Operand order.** Assembler: `FD, FA, FC, FB`. - **Invalid operations.** `0×∞` → `VXIMZ`; same-signed-infinity collision → `VXISI`. Quiet NaN result. -- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. xenia-rs does not (xenia quirk). +- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. Canary does not (`UpdateFPSCR` is a stub). - **`Rc=1` (`fnmsubs.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **NaN propagation.** Quiet-NaN result for any NaN operand; signalling NaNs are quietened. - **Single-precision overflow** returns ±∞ and sets `OX`/`XX`/`FX`. - **Use case.** Single-precision Newton-Raphson divide refinement: `x_new = x*(2 - d*x)` is implemented as a `fnmsubs`/`fmuls` pair throughout Xbox 360 graphics code. -- **Denormal flush.** Xenon boots with `FPSCR[NI]=1`; xenia uses host IEEE behavior. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. ## Related Instructions diff --git a/tools/ppc-manual/fpu/fnmsubx.md b/tools/ppc-manual/fpu/fnmsubx.md index 05a4620e..2520ae71 100644 --- a/tools/ppc-manual/fpu/fnmsubx.md +++ b/tools/ppc-manual/fpu/fnmsubx.md @@ -107,15 +107,15 @@ int InstrEmit_fnmsubx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Single rounding step, then sign flip.** Computes `−((FRA × FRC) − FRB)` = `FRB − (FRA × FRC)`, with one fused rounding. xenia-rs implements this as `-(a.mul_add(c, -b))`, which is mathematically equivalent. -- **NaN sign behaviour.** PowerISA: the negation does **not** flip the sign of a NaN result. xenia uses Rust's `Neg` which does flip the sign bit on NaNs. Observable only via bit-level inspection. **xenia quirk.** +- **Single rounding step, then sign flip.** Computes `−((FRA × FRC) − FRB)` = `FRB − (FRA × FRC)`, with one fused rounding. Canary emits `Neg(MulSub(a, c, b))`, fused only on FMA3 hosts (`vfmsub213sd`, otherwise `vmulsd` + `vsubsd`). +- **NaN sign behaviour.** PowerISA: the negation does **not** flip the sign of a NaN result. Canary's `Neg` (`vxorpd` with the sign mask) does flip the sign bit on NaNs. Observable only via bit-level inspection. **Canary quirk.** - **Operand order.** Assembler: `FD, FA, FC, FB`. - **Invalid operations.** `0×∞` → `VXIMZ`; same-signed-infinity collision (e.g. `(+∞) − (+∞)`) → `VXISI`. Quiet NaN result. -- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. xenia-rs does not (xenia quirk). +- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX`, `OX`, `UX`, `XX`, `VXIMZ`, `VXISI`, `VXSNAN`. Canary does not (`UpdateFPSCR` is a stub). - **`Rc=1` (`fnmsub.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **NaN propagation.** Quiet-NaN result for any NaN operand; signalling NaNs are quietened. - **Use case.** The canonical Newton-Raphson divide refinement step: `x_new = x*(2 - d*x)`. This is the most common operand pattern in compiled PPC graphics code that does software reciprocals. -- **Denormal flush.** Xenon boots with `FPSCR[NI]=1`; xenia uses host IEEE behavior. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. ## Related Instructions diff --git a/tools/ppc-manual/fpu/fresx.md b/tools/ppc-manual/fpu/fresx.md index 7c38c419..accc4c15 100644 --- a/tools/ppc-manual/fpu/fresx.md +++ b/tools/ppc-manual/fpu/fresx.md @@ -113,12 +113,12 @@ int InstrEmit_fresx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Single-precision reciprocal estimate.** PowerISA specifies a *low-precision* approximation of `1/FRB` accurate to roughly 12–14 bits of significand, intended as the seed for a Newton-Raphson refinement step. **xenia quirk:** xenia-rs computes the *full-precision* `1.0 / b` then rounds to single, so it produces a far more accurate result than hardware. Title code that depends on the limited precision of `fres` to trigger refinement loops will still work (the loops just refine an already-correct value), but bit-exact correlation with hardware is impossible. +- **Single-precision reciprocal estimate.** PowerISA specifies a *low-precision* estimate of `1/FRB`, correct to one part in 256, intended as the seed for a Newton-Raphson refinement step. **Canary quirk:** it rounds `FRB` to binary32 and divides exactly (`vdivss` into `1.0`; its comment rejects AVX-512 `vrcp14ss` for precision), so it returns a correctly rounded single-precision reciprocal — far more accurate than hardware. Title code that depends on the limited precision of `fres` to trigger refinement loops will still work (the loops just refine an already-correct value), but bit-exact correlation with hardware is impossible. - **Single precision result.** Final value is rounded to binary32 then re-encoded into the FPR. -- **Divide by zero.** `1/±0` → ±∞ and sets `FPSCR[ZX, FX]`. xenia returns the host ±∞ but does not update FPSCR. +- **Divide by zero.** `1/±0` → ±∞ and sets `FPSCR[ZX, FX]`. Canary returns the host ±∞ but does not update FPSCR. - **`fres(±∞) = ±0`** (correctly signed). - **`fres(NaN) = NaN`**; signalling NaNs are quietened. -- **Overflow / underflow.** May set `OX`/`UX`/`XX`/`FX`. xenia does not update FPSCR. +- **Overflow / underflow.** May set `OX`/`UX`/`XX`/`FX`. Canary does not update FPSCR. - **`Rc=1` (`fres.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **Encoding.** A-form, primary 59, XO 24. Reads `FRB` only; `FRA`/`FRC` are don't-care. - **Use case.** Software reciprocal: `1/d ≈ x = fres(d); x = x*(2 - d*x);` (one Newton-Raphson step recovers full single precision). Two iterations recover full double precision. The `(2 - d*x)` step compiles to `fnmsub`. diff --git a/tools/ppc-manual/fpu/frspx.md b/tools/ppc-manual/fpu/frspx.md index 20738c19..43193e2f 100644 --- a/tools/ppc-manual/fpu/frspx.md +++ b/tools/ppc-manual/fpu/frspx.md @@ -109,12 +109,12 @@ int InstrEmit_frspx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Round to single-precision.** Rounds the binary64 value in `FRB` to binary32 using `FPSCR[RN]`, then re-encodes the result back into the destination as a binary64 representation of that single value. xenia-rs uses `to_single(b)`, which performs `f64 → f32 → f64` round-trip (Rust's `as f32` uses round-to-nearest-even, matching the PPC default). -- **`FPSCR[RN]` not honored in xenia.** Like other conversion ops, xenia's `to_single` is hard-coded to round-to-nearest-even regardless of `FPSCR[RN]`. **xenia quirk** for non-default rounding modes. +- **Round to single-precision.** Rounds the binary64 value in `FRB` to binary32 using `FPSCR[RN]`, then re-encodes the result back into the destination as a binary64 representation of that single value. Canary emits `Convert(b, FLOAT32, ROUND_DYNAMIC)` then `Convert(v, FLOAT64)` — `vcvtsd2ss` + `vcvtss2sd` under the host rounding mode, with a fix-up on each step that carries a NaN's quiet/signalling bit across unchanged. +- **`FPSCR[RN]` honoured in Canary.** `ROUND_DYNAMIC` converts under the host rounding mode, which Canary loads from `FPSCR[RN]` whenever the guest writes it through `mtfsf`/`mtfsfi`. - **Overflow.** Values whose magnitude exceeds binary32's max (~3.4e38) round to ±∞ and set `FPSCR[OX, XX, FX]`. -- **Underflow.** Values whose magnitude is below binary32's smallest normal (~1.2e-38) flush to zero or denormal per `FPSCR[NI]`; `UX`/`XX`/`FX` set on hardware. xenia uses host IEEE. -- **NaN propagation.** Quiet NaNs pass through; signalling NaNs are quietened (sign-bit cleared on the SNaN-quietening payload bit). Host `as f32` does not perform PPC-style quietening; **xenia quirk** for SNaN bit-level inspection. -- **Inexact.** Most rounding produces inexact; sets `FPSCR[XX, FX]`. xenia does not update FPSCR (xenia quirk). +- **Underflow.** Values whose magnitude is below binary32's smallest normal (~1.2e-38) flush to zero or denormal per `FPSCR[NI]`; `UX`/`XX`/`FX` set on hardware. Canary produces the host's denormal — or zero once the guest has set `NI`, which turns on `MXCSR.FZ` — and sets no FPSCR bit. +- **NaN propagation.** Quiet NaNs pass through; signalling NaNs are quietened on hardware. Canary's conversion fix-ups deliberately carry the quiet/signalling bit across, so a signalling NaN stays signalling; **Canary quirk** for SNaN bit-level inspection. +- **Inexact.** Most rounding produces inexact; sets `FPSCR[XX, FX]`. Canary does not update FPSCR (`UpdateFPSCR` is a stub). - **`Rc=1` (`frsp.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **Encoding.** X-form, primary 63, XO 12. Reads `FRB` only. - **Use case.** Compilers emit `frsp` after a chain of `fadd`/`fmul`/etc. when storing the value with `stfs` (store single). Without an explicit `frsp`, the in-FPR double would not match the `stfs`-rounded single. diff --git a/tools/ppc-manual/fpu/frsqrtex.md b/tools/ppc-manual/fpu/frsqrtex.md index 17d3d575..5b5ee5fd 100644 --- a/tools/ppc-manual/fpu/frsqrtex.md +++ b/tools/ppc-manual/fpu/frsqrtex.md @@ -110,9 +110,9 @@ int InstrEmit_frsqrtex(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Reciprocal-square-root estimate.** PowerISA: low-precision approximation of `1/sqrt(FRB)` accurate to roughly 12–14 bits, designed as the seed for Newton-Raphson refinement. **xenia quirk:** xenia-rs computes the *full-precision* `1.0 / b.sqrt()` (no rounding to single — `frsqrte` is double-precision per the spec). The result is far more accurate than hardware. Title code that depends on the limited precision still functions; the NR refinement converges in one iteration on either platform. +- **Reciprocal-square-root estimate.** PowerISA: a low-precision estimate of `1/sqrt(FRB)`, correct to one part in 32, designed as the seed for Newton-Raphson refinement. Canary's x64 backend also returns a low-precision estimate rather than the exact value: `frsqrtefp_helper` takes an 8-bit significand from a 16-entry table indexed by the exponent's parity and the top three significand bits. Whether that matches Xenon bit-for-bit is unverified. - **Double precision result.** Per PowerISA, `frsqrte` returns a binary64 estimate (not a single-rounded value, unlike `fres`). -- **Negative input is invalid.** `frsqrte(x < 0)` (other than `-0`) sets `FPSCR[VXSQRT, VX, FX]` and yields a quiet NaN. xenia returns host NaN (Rust's `f64::sqrt` of a negative is NaN, then `1/NaN` is NaN) but does not raise the FPSCR bit. +- **Negative input is invalid.** `frsqrte(x < 0)` (other than `-0`) sets `FPSCR[VXSQRT, VX, FX]` and yields a quiet NaN. Canary's helper returns the default quiet NaN `0x7FF8_0000_0000_0000` but does not raise the FPSCR bit. - **`frsqrte(+0) = +∞`** and sets `FPSCR[ZX]` per spec. **`frsqrte(-0) = -∞`**. - **`frsqrte(+∞) = +0`**. - **NaN propagation.** Quiet NaN; signalling NaNs are quietened. diff --git a/tools/ppc-manual/fpu/fselx.md b/tools/ppc-manual/fpu/fselx.md index 4802a8f4..41b64939 100644 --- a/tools/ppc-manual/fpu/fselx.md +++ b/tools/ppc-manual/fpu/fselx.md @@ -113,9 +113,9 @@ int InstrEmit_fselx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Non-IEEE branch-free select.** PowerPC-specific; not in the IEEE-754 spec. Semantics: `FRT = (FRA >= 0.0) ? FRC : FRB`. Used pervasively in compiled PPC for `min`/`max`/`clamp`/`copysign` without branches. xenia-rs uses Rust's `>=` which matches. -- **`-0.0` selects `FRC`.** Per PowerISA, `-0` compares as `>= 0`, so it routes to `FRC` (the "true" branch). xenia's `-0.0 >= 0.0` evaluates true in Rust — semantic match. -- **NaN selects `FRB`.** Per PowerISA, NaN does **not** satisfy `>= 0`, so the result is `FRB`. xenia: any comparison with NaN returns false in Rust, so `>= 0` is false → `FRB` selected. Match. +- **Non-IEEE branch-free select.** PowerPC-specific; not in the IEEE-754 spec. Semantics: `FRT = (FRA >= 0.0) ? FRC : FRB`. Used pervasively in compiled PPC for `min`/`max`/`clamp`/`copysign` without branches. Canary emits `Select(CompareSGE(FRA, 0.0), FRC, FRB)`; the x64 compare is `vcomisd` + `setae`, which is false for a NaN, so a NaN in `FRA` selects `FRB` as on PPC. +- **`-0.0` selects `FRC`.** Per PowerISA, `-0` compares as `>= 0`, so it routes to `FRC` (the "true" branch). Canary's `vcomisd` reports `-0.0` equal to `0.0`, so `setae` is true — a match. +- **NaN selects `FRB`.** Per PowerISA, NaN does **not** satisfy `>= 0`, so the result is `FRB`. Canary's `vcomisd` + `setae` is false for an unordered compare, so `FRB` is selected. Match. - **No FPSCR side effects.** `fsel` does **not** raise `VXSNAN` even on signalling NaN inputs, and does **not** update `FPRF`. It is purely a data-movement op. - **`Rc=1` (`fsel.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **A-form encoding.** Reads `FRA, FRB, FRC`, writes `FRT`. Assembler order: `fsel FD, FA, FC, FB` (note: `FRC` before `FRB`). diff --git a/tools/ppc-manual/fpu/fsqrtsx.md b/tools/ppc-manual/fpu/fsqrtsx.md index 2c38990d..170fb1d1 100644 --- a/tools/ppc-manual/fpu/fsqrtsx.md +++ b/tools/ppc-manual/fpu/fsqrtsx.md @@ -105,10 +105,10 @@ int InstrEmit_fsqrtsx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Single precision.** Result is rounded to IEEE-754 binary32 then re-encoded into the destination 64-bit FPR. xenia computes `to_single(b.sqrt())`. +- **Single precision.** Result is rounded to IEEE-754 binary32 then re-encoded into the destination 64-bit FPR. Canary computes `ToSingle(Sqrt(b))`: `vsqrtsd`, then `vcvtsd2ss` + `vcvtss2sd`. - **Negative inputs are invalid.** `sqrt(x < 0)` (other than `-0`) sets `FPSCR[VXSQRT, VX, FX]` and yields a quiet NaN. `sqrt(-0) = -0` per IEEE-754. - **`sqrt(+∞) = +∞`**, exact. -- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX` plus exception bits `XX` (very common — sqrt is rarely exact in single precision), `VXSQRT`, `VXSNAN`. xenia-rs does not update FPSCR (xenia quirk). +- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX` plus exception bits `XX` (very common — sqrt is rarely exact in single precision), `VXSQRT`, `VXSNAN`. Canary does not update FPSCR (`UpdateFPSCR` is a stub). - **`Rc=1` (`fsqrts.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **NaN propagation.** Quiet-NaN result for any NaN operand; signalling NaNs are quietened. - **Performance.** `fsqrts` is a multi-cycle, non-pipelined operation on Xenon. Hot-path code commonly uses `frsqrte` + Newton-Raphson + `fmul`. diff --git a/tools/ppc-manual/fpu/fsqrtx.md b/tools/ppc-manual/fpu/fsqrtx.md index bfb8b522..a4247ab6 100644 --- a/tools/ppc-manual/fpu/fsqrtx.md +++ b/tools/ppc-manual/fpu/fsqrtx.md @@ -105,13 +105,13 @@ int InstrEmit_fsqrtx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Double precision.** Operates on IEEE-754 binary64; [`fsqrtsx`](fsqrtsx.md) is the single-precision sibling. xenia delegates to host `f64::sqrt`. +- **Double precision.** Operates on IEEE-754 binary64; [`fsqrtsx`](fsqrtsx.md) is the single-precision sibling. Canary emits the host's `vsqrtsd`. - **Negative inputs are invalid.** `sqrt(x < 0)` (other than `-0`) sets `FPSCR[VXSQRT, VX, FX]` and yields a quiet NaN. Note: `sqrt(-0) = -0` per IEEE-754 (preserves sign of zero) — host `f64::sqrt` matches. - **`sqrt(+∞) = +∞`**, exact. -- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX` plus exception bits `XX` (inexact, very common since `sqrt` is rarely exact), `VXSQRT`, `VXSNAN`. xenia-rs does **not** update FPSCR (xenia quirk). +- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX` plus exception bits `XX` (inexact, very common since `sqrt` is rarely exact), `VXSQRT`, `VXSNAN`. Canary does **not** update FPSCR (`UpdateFPSCR` is a stub). - **`Rc=1` (`fsqrt.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **NaN propagation.** Quiet-NaN result for any NaN operand; signalling NaNs are quietened. -- **Performance / availability.** `fsqrt` is a Power-ISA optional instruction; some implementations trap as illegal-opcode. Xenon implements it natively. xenia-rs supports it directly. +- **Performance / availability.** `fsqrt` is a Power-ISA optional instruction; some implementations trap as illegal-opcode. Xenon implements it natively. Canary emits it directly as `vsqrtsd`. - **Encoding.** A-form, primary 63, XO 22; reads `FRB` only — `FRA` and `FRC` are don't-care. - **Rounding mode** uses `FPSCR[RN]`. diff --git a/tools/ppc-manual/fpu/fsubsx.md b/tools/ppc-manual/fpu/fsubsx.md index 44d60109..1335dbd9 100644 --- a/tools/ppc-manual/fpu/fsubsx.md +++ b/tools/ppc-manual/fpu/fsubsx.md @@ -111,13 +111,13 @@ int InstrEmit_fsubsx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Single precision.** Result is rounded to IEEE-754 binary32 then re-encoded into the destination 64-bit FPR using the binary64 representation. xenia-rs uses `to_single(a - b)` which performs the round trip via `f64 -> f32 -> f64`. +- **Single precision.** Result is rounded to IEEE-754 binary32 then re-encoded into the destination 64-bit FPR using the binary64 representation. Canary emits `ToSingle(Sub(a, b))`: `vsubsd`, then the `vcvtsd2ss` + `vcvtss2sd` round trip. - **`±∞ − ±∞`** sets `FPSCR[VXISI, VX, FX]` and yields a quiet NaN. -- **FPSCR side effects.** Always updated on hardware: `FPRF`, `FR`, `FI`, `FX`, plus exception bits `OX`, `UX`, `XX`, `VXISI`, `VXSNAN`. xenia-rs does **not** maintain FPSCR in the interpreter snapshot (xenia quirk). +- **FPSCR side effects.** Always updated on hardware: `FPRF`, `FR`, `FI`, `FX`, plus exception bits `OX`, `UX`, `XX`, `VXISI`, `VXSNAN`. Canary does **not** maintain FPSCR (`UpdateFPSCR` is a stub). - **`Rc=1` (`fsubs.`)** copies `FPSCR[FX, FEX, VX, OX]` into CR1. - **NaN propagation.** Quiet-NaN result for any NaN operand; signalling NaNs are quietened. - **Single-precision overflow.** A double-precision result that would round to a binary32 overflow returns ±∞ and sets `OX`/`XX`/`FX`. -- **Denormal flush.** Xenon boots with `FPSCR[NI]=1`; hardware flushes single-precision denormals to zero. xenia inherits host IEEE semantics. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. - **Rounding mode** is taken from `FPSCR[RN]`; default is nearest-even. - **Encoding.** A-form, primary 59, XO 20. `FRC` is don't-care. diff --git a/tools/ppc-manual/fpu/fsubx.md b/tools/ppc-manual/fpu/fsubx.md index 1a65239e..2e98235b 100644 --- a/tools/ppc-manual/fpu/fsubx.md +++ b/tools/ppc-manual/fpu/fsubx.md @@ -107,11 +107,11 @@ int InstrEmit_fsubx(PPCHIRBuilder& f, const InstrData& i) { - **Double precision.** `fsub` operates on IEEE-754 binary64. The single-precision sibling is [`fsubsx`](fsubsx.md), which rounds the result to binary32 before re-encoding it into the 64-bit FPR. - **`±∞ − ±∞` is the canonical invalid case.** Same-signed infinity subtraction (or opposite-signed addition) yields `QNaN(VXISI)` and sets `FPSCR[VXISI, VX, FX]`. -- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX` plus exception bits `OX`, `UX`, `XX`, `VXISI`, `VXSNAN` as appropriate. xenia-rs's interpreter does **not** model FPSCR updates — a xenia quirk that almost never matters in practice. +- **FPSCR side effects.** Hardware updates `FPRF`, `FR`, `FI`, `FX` plus exception bits `OX`, `UX`, `XX`, `VXISI`, `VXSNAN` as appropriate. Canary does **not** model FPSCR updates (`UpdateFPSCR` is a stub) — a divergence that almost never matters in practice. - **`Rc=1` (`fsub.`)** writes `CR1` from `FPSCR[FX, FEX, VX, OX]`. - **NaN propagation.** Any NaN operand yields a quiet NaN; a signalling NaN input is quietened (signalling bit cleared) per PowerISA. Host `f64 -` is relied on for the value. -- **Sign of zero.** `+0 − +0 = +0` in round-to-nearest, `−0` in round-toward-negative-infinity. xenia inherits host semantics. -- **Denormal flush.** Xenon boots with `FPSCR[NI]=1` (non-IEEE mode) so subnormal results flush to zero on hardware. Xenia produces IEEE-compliant denormals from the host FPU; titles relying on flush-to-zero typically see no observable difference for game logic but may see subtle differences in audio DSP. +- **Sign of zero.** `+0 − +0 = +0` in round-to-nearest, `−0` in round-toward-negative-infinity. Canary's `vsubsd` follows the host rounding mode, which it loads from `FPSCR[RN]`. +- **Denormal flush.** That Xenon boots with `FPSCR[NI]=1` is unverified. Canary starts every guest thread in IEEE mode (MXCSR `0x1F80`; its init comment flags the startup state as unchecked) and turns on the host's flush-to-zero (`MXCSR.FZ`) only when the guest sets `NI` through `mtfsf`/`mtfsfi`. - **Encoding.** A-form, primary 63, XO 20. `FRC` is don't-care for sub. ## Related Instructions diff --git a/tools/ppc-manual/memory/dcbf.md b/tools/ppc-manual/memory/dcbf.md index 9c3a8771..7da00627 100644 --- a/tools/ppc-manual/memory/dcbf.md +++ b/tools/ppc-manual/memory/dcbf.md @@ -109,7 +109,7 @@ int InstrEmit_dcbf(PPCHIRBuilder& f, const InstrData& i) { - **Flush = write-back + invalidate.** If the addressed line is dirty in the data cache, it is written to memory; whether dirty or clean, the line is then removed from the cache. Subsequent loads must refill from memory. - **Cache line size.** Xenon's L1/L2 lines are **128 bytes**. The hardware ignores the low seven bits of `EA`, so `dcbf RA, RB` flushes the line containing `EA` regardless of where in that line `EA` lies. There is no `dcbf128` variant — the hint is sized to the architectural line. - **`RA0` semantics.** When `RA = 0`, the base is the literal zero — `dcbf 0, RB` flushes the line containing address `RB`. The instruction has no destination register. -- **Xenia models a no-op.** Xenia-rs's emulator does not maintain a coherent cache model; the decode entry exists but the interpreter typically advances PC without further effect, since target memory is always coherent on the host. This is correct behaviour for an emulator. +- **Canary emits host cache hints only.** It keeps no guest-visible cache model: unless the `disable_prefetch_and_cachecontrol` cvar is set, `dcbf` becomes a host `clflush` over the 128-byte cache line (its comment notes Xenon's 128-byte lines), and guest memory is always coherent on the host. This is correct behaviour for an emulator. - **Unprivileged.** `dcbf` is a problem-state instruction — usable from user code. Storage protection still applies; flushing an unmapped page raises a DSI exception. - **Pair with `sync`.** Hardware `dcbf` does not by itself impose ordering; software that needs the flushed data visible to other masters (DMA, GPU) issues a [`sync`](sync.md) afterwards. - **Self-modifying code companion.** When patching code, the recipe is `dcbst` (push dirty data through to memory) → `sync` → [`icbi`](icbi.md) (invalidate I-cache) → [`isync`](isync.md). `dcbf` is the heavier alternative when the writer also wants the line out of D-cache. diff --git a/tools/ppc-manual/memory/dcbi.md b/tools/ppc-manual/memory/dcbi.md index 27fdb478..065a42d7 100644 --- a/tools/ppc-manual/memory/dcbi.md +++ b/tools/ppc-manual/memory/dcbi.md @@ -96,7 +96,7 @@ _No condition-register or status-register effects._ - **Drops dirty data.** The line is removed from cache **without** writing back, so any modifications that have not already been pushed to memory are lost. Used only when the underlying memory is being repurposed (e.g. DMA window flip, page demap) and stale dirty data would be incorrect. - **Cache line size.** Xenon lines are 128 bytes. The low seven bits of `EA` are ignored — the operation targets the cache line that contains `EA`. - **`RA0` semantics.** When `RA = 0`, base is literal zero, so `dcbi 0, RB` invalidates the line containing address `RB`. -- **Xenia treats it as a no-op.** With no modelled cache, the emulator decodes and advances PC; memory is already authoritative. +- **Canary does not implement it.** `dcbi` is in its opcode table but has no emitter, so translating one logs "Unimplemented instr" and, with the default `break_on_unimplemented_instructions`, breaks. - **Sequencing.** Not synchronising. Pair with [`sync`](sync.md) when invalidation must precede a subsequent load on another thread. - **Architecturally subsumed by `dcbf` for problem state.** Userspace that wants "this line is no longer valuable" must use [`dcbf`](dcbf.md), accepting the write-back cost. diff --git a/tools/ppc-manual/memory/dcbst.md b/tools/ppc-manual/memory/dcbst.md index 2025d859..6138e543 100644 --- a/tools/ppc-manual/memory/dcbst.md +++ b/tools/ppc-manual/memory/dcbst.md @@ -111,7 +111,7 @@ int InstrEmit_dcbst(PPCHIRBuilder& f, const InstrData& i) { - **Self-modifying code stage 1.** The canonical "patch then run" sequence is `stw` (modify) → `dcbst` (push dirty data to memory) → [`sync`](sync.md) → [`icbi`](icbi.md) (invalidate I-cache for the same address) → [`isync`](isync.md). `dcbst` is preferred over `dcbf` here because it leaves the data in D-cache for any subsequent normal reads. - **DMA hand-off.** Used before initiating a GPU or DMA read of a buffer the CPU has just written, to ensure memory holds the latest data. - **Unprivileged.** Available from problem state. -- **Xenia models as no-op.** No cache state is simulated; PC advances and memory is already authoritative. +- **Canary emits a host hint only.** No cache state is simulated: unless the `disable_prefetch_and_cachecontrol` cvar is set, `dcbst` becomes a host `clflush` over the 128-byte cache line, and memory is already authoritative. ## Related Instructions diff --git a/tools/ppc-manual/memory/dcbt.md b/tools/ppc-manual/memory/dcbt.md index 221d4eba..b8a618e1 100644 --- a/tools/ppc-manual/memory/dcbt.md +++ b/tools/ppc-manual/memory/dcbt.md @@ -111,7 +111,7 @@ int InstrEmit_dcbt(PPCHIRBuilder& f, const InstrData& i) { - **Cache line size.** Xenon line is 128 bytes; low seven bits of `EA` are ignored. - **`RA0` semantics.** `RA = 0` selects literal zero — `dcbt 0, RB` prefetches the line containing address `RB`. - **Stream-engine hints.** The Xenon supports up to four hardware data-streams set up by sequences of `dcbt` with a stride; refer to the XDK for the stream-engine encoding (uses bits ignored by the architectural decode). -- **Xenia treats as no-op.** Hints have no observable effect under the emulated memory model. +- **Canary turns it into a host hint.** Unless `disable_prefetch_and_cachecontrol` is set it emits `prefetcht0` over the 128-byte cache line. Hints have no guest-observable effect. - **Unprivileged.** Always available. ## Related Instructions diff --git a/tools/ppc-manual/memory/dcbtst.md b/tools/ppc-manual/memory/dcbtst.md index 9f4aab97..b863239a 100644 --- a/tools/ppc-manual/memory/dcbtst.md +++ b/tools/ppc-manual/memory/dcbtst.md @@ -112,7 +112,7 @@ int InstrEmit_dcbtst(PPCHIRBuilder& f, const InstrData& i) { - **Cache line size.** Xenon line is 128 bytes; the low seven bits of `EA` are ignored. - **`RA0` semantics.** `RA = 0` selects literal zero — `dcbtst 0, RB` prefetches the line containing address `RB`. - **Often replaced by `dcbz128`.** When code knows it will write the **entire** line, `dcbz128` is preferable: it allocates the line and zeros it without reading from memory at all, beating `dcbtst` + first-store. -- **Xenia treats as no-op.** Hints have no observable effect under the emulated memory model. +- **Canary turns it into a host hint.** Unless `disable_prefetch_and_cachecontrol` is set it emits `prefetchw` (or `prefetcht0` on hosts without it) over the 128-byte cache line. Hints have no guest-observable effect. ## Related Instructions diff --git a/tools/ppc-manual/memory/dcbz.md b/tools/ppc-manual/memory/dcbz.md index c5b38b38..bf920334 100644 --- a/tools/ppc-manual/memory/dcbz.md +++ b/tools/ppc-manual/memory/dcbz.md @@ -167,7 +167,7 @@ int InstrEmit_dcbz128(PPCHIRBuilder& f, const InstrData& i) { - **Cache-line size mismatch.** Stock PowerPC `dcbz` zeroes one architectural cache line — 32 bytes on classic POWER, but the **Xenon's L1 line is 128 bytes**. Microsoft added `dcbz128` (encoded with bit-9 set so `RT` field reads as `1`) to clear a true Xenon line in one instruction. Most Xbox 360 code therefore emits `dcbz128`; a stray `dcbz` only zeroes 32 bytes and silently leaves the rest of the line uncleared. - **Alignment is forced via mask.** The effective address is masked by `~31` (`dcbz`) or `~127` (`dcbz128`) before writing — the low bits are dropped, not validated. Calling `dcbz r0, r3` with `r3 = 0x10037` writes zeros to `0x10000..0x1007F`, not `0x10037..0x100B6`. -- **No memory read; pure write.** Real hardware allocates the line in cache and may skip a read-from-memory fill ("cache-line zero" optimisation). Xenia simulates the architectural effect — 32 (or 128) bytes of zero in target memory — without modelling cache state. +- **No memory read; pure write.** Real hardware allocates the line in cache and may skip a read-from-memory fill ("cache-line zero" optimisation). Canary simulates the architectural effect without modelling cache state, and always clears 128 bytes at `EA & ~127` — its comment: "On Xbox360 there is no short cache line." - **`RA0` semantics.** `RA = 0` selects literal zero as the base, so `dcbz128 0, RB` zeros the line containing address `RB`. The update form does not exist for cache-control instructions. - **Block-fill idiom.** Compilers and hand-written copy loops pair `dcbz128` with `stvx` / `stw` sequences to avoid the cache-line read-allocate that a cold store would trigger. Skipping the read is the entire point. - **Privilege.** `dcbz` is unprivileged (problem-state); does not require supervisor mode. It can fault on protection or unmapped memory like an ordinary store. diff --git a/tools/ppc-manual/memory/icbi.md b/tools/ppc-manual/memory/icbi.md index 096368cd..22fb49e1 100644 --- a/tools/ppc-manual/memory/icbi.md +++ b/tools/ppc-manual/memory/icbi.md @@ -110,7 +110,7 @@ int InstrEmit_icbi(PPCHIRBuilder& f, const InstrData& i) { - **Unprivileged.** `icbi` is problem-state, unlike its data-side cousin [`dcbi`](dcbi.md). - **No exception on bad address.** Treated as a hint at the hardware level — invalidating an absent line is harmless. - **Per-thread effect.** On the multithreaded Xenon core, `icbi` propagates across hardware threads sharing the same L1 I-cache; cross-core invalidation requires bus broadcast handled implicitly by the cache coherence protocol. -- **Xenia models as no-op.** No I-cache is simulated; rebuilds of generated code (when applicable) are triggered by the JIT cache-watcher, not by `icbi` itself. +- **Canary emits a no-op.** No I-cache is simulated; Canary's `icbi` emitter is a `Nop`. ## Related Instructions diff --git a/tools/ppc-manual/memory/lbz.md b/tools/ppc-manual/memory/lbz.md index 912f28f8..97e88abc 100644 --- a/tools/ppc-manual/memory/lbz.md +++ b/tools/ppc-manual/memory/lbz.md @@ -255,8 +255,8 @@ int InstrEmit_lbzx(PPCHIRBuilder& f, const InstrData& i) { - **Single-byte read.** The smallest scalar load. No endian concerns at the byte level — `MEM(EA, 1)` returns the literal byte at address `EA`, regardless of host or target byte order. - **Zero-extension to 64 bits.** The high 56 bits of `RT` become zero. Use [`lha`](lha.md) / [`lhax`](lha.md) family for sign-extending byte-equivalent semantics; there is no PowerPC "load byte sign-extended" — you must `lbz` then `extsb` (or use `lha` on a half). -- **`RA0` (non-update forms).** When `RA = 0` in `lbz` / `lbzx`, the base is the literal zero, so `lbz RT, 0x4000(0)` reads from absolute address `0x4000`. Update forms `lbzu` / `lbzux` invoke `RA = 0` (and `RA = RT`) as invalid forms; xenia's interpreter does not check, so well-formed compiler output is assumed. -- **Update-form post-write.** `lbzu` / `lbzux` write the computed `EA` back to `RA` after the load; the snapshot first reads, then assigns `RA ← EA`, matching IBM's "the load and update happen as one operation" wording. +- **`RA0` (non-update forms).** When `RA = 0` in `lbz` / `lbzx`, the base is the literal zero, so `lbz RT, 0x4000(0)` reads from absolute address `0x4000`. Update forms `lbzu` / `lbzux` invoke `RA = 0` (and `RA = RT`) as invalid forms; Canary's emitters do not check, so well-formed compiler output is assumed. +- **Update-form post-write.** `lbzu` / `lbzux` write the computed `EA` back to `RA` after the load; Canary first loads into `RT`, then assigns `RA ← EA`, matching IBM's "the load and update happen as one operation" wording. - **No alignment requirement.** A byte load is intrinsically aligned. Xenon does not raise alignment exceptions for any byte access. - **Common in string and table-lookup code.** Most uses are character-string scans, jump-table dispatches, and packed-bool reads. Compilers also use `lbz` to materialise small immediate constants stored in `.rodata`. diff --git a/tools/ppc-manual/memory/ld.md b/tools/ppc-manual/memory/ld.md index c844f060..9d344a68 100644 --- a/tools/ppc-manual/memory/ld.md +++ b/tools/ppc-manual/memory/ld.md @@ -255,10 +255,10 @@ int InstrEmit_ldx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **DS-form, not D-form.** The displacement is 14 bits scaled by 4 (`EXTS(ds || 0b00)`), giving a signed range of ±32 KiB in 4-byte steps. Bits 30–31 are the extended opcode used to distinguish `ld` (XO=0) from `ldu` (XO=1). The assembler accepts a normal byte displacement and verifies divisibility by 4. -- **Big-endian read.** The 64 bits at `EA..EA+7` form the loaded value, most-significant byte first. Xenia-rs's `mem.read_u64` returns the host-native value of that big-endian doubleword. +- **Big-endian read.** The 64 bits at `EA..EA+7` form the loaded value, most-significant byte first. Canary loads the doubleword and byte-swaps it (`ByteSwap(LoadOffset(…, INT64))`) to get the host-native value. - **No zero/sign-extension question.** `ld` already fills the entire 64-bit register; there is no `lda` (load doubleword algebraic) — the doubleword is the architectural maximum. - **`RA0` (non-update forms).** `RA = 0` in `ld` and `ldx` means base is literal zero. `ld RT, 0x100(0)` reads from absolute `0x100`. -- **Update-form invalid forms.** `ldu` / `ldux` invoke "RA = 0" and "RA = RT" as invalid forms. AIX docs say results are undefined; xenia performs the read first, then writes back `RA ← EA`, which would silently destroy the loaded value if `RA == RT`. +- **Update-form invalid forms.** `ldu` / `ldux` invoke "RA = 0" and "RA = RT" as invalid forms. AIX docs say results are undefined; Canary performs the load first, then writes back `RA ← EA`, which silently destroys the loaded value if `RA == RT`. - **Alignment.** Xenon does not enforce doubleword alignment for `ld` itself — unaligned 8-byte loads are tolerated. However, real POWER cores may take an alignment exception on some implementations; portable code keeps doublewords 8-byte aligned. - **64-bit pointer / counter loads.** Although Xbox 360 user code is 32-bit, kernel structures and TOC entries are doublewords; `ld` is the standard load for them. diff --git a/tools/ppc-manual/memory/ldarx.md b/tools/ppc-manual/memory/ldarx.md index f05c61e9..2461e991 100644 --- a/tools/ppc-manual/memory/ldarx.md +++ b/tools/ppc-manual/memory/ldarx.md @@ -129,9 +129,9 @@ int InstrEmit_ldarx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Reservation set.** Loads the doubleword at `EA` and atomically establishes a *reservation* on that address. A subsequent [`stdcx`](stdcx.md) at the same address completes only if the reservation is still valid. Together they form a standard load-linked / store-conditional pair for lock-free updates. -- **One reservation per thread.** Xenia tracks `reserved_addr` / `reserved_val` / `has_reservation` per-context (see snapshot). Hardware behaves the same: each hardware thread holds at most one reservation at a time. A new `ldarx` (or `lwarx`) discards the prior reservation. -- **Granule.** Architecturally the reservation covers a single naturally-aligned doubleword (8 bytes). On Xenon the practical reservation granule is one **cache line** (128 bytes) — any store to that line by another agent loses the reservation. Xenia simplifies to per-address tracking. -- **Alignment requirement.** `EA` must be 8-byte aligned. An unaligned `ldarx` raises an alignment exception on hardware. Xenia does not check; pass aligned addresses. +- **One reservation per thread.** Hardware: each hardware thread holds at most one reservation at a time, and a new `ldarx` (or `lwarx`) discards the prior one. Canary instead sets a bit for the 64 KiB block holding `EA` in a bitmap shared by all threads and caches the loaded value per thread; taking a second reservation while one is held hits a `DebugBreak` (`int3`) in its helper. +- **Granule.** Architecturally the reservation covers a single naturally-aligned doubleword (8 bytes). On Xenon the practical reservation granule is one **cache line** (128 bytes) — any store to that line by another agent loses the reservation. Canary's granule is a 64 KiB block, and ordinary stores never clear it: `stdcx.` succeeds if this thread still holds the block bit and the doubleword still holds the value `ldarx` read. +- **Alignment requirement.** `EA` must be 8-byte aligned. An unaligned `ldarx` raises an alignment exception on hardware. Canary does not check; pass aligned addresses. - **`RA0` semantics.** When `RA = 0`, base is literal zero — `ldarx RT, 0, RB` reads at exact `RB`. Used in synthetic-zero atomic-init idioms, but rare. - **Reservation-loss events.** Any exception, context switch, or store by another thread to the reserved line clears the reservation. Application code must treat the `stdcx` failure as a normal retry condition, not as an error. - **Pair atomically.** Code must be `ldarx ... do work ... stdcx.` with no intervening loads or stores that could be re-ordered. Optionally fence with [`lwsync`](sync.md) inside the loop. The conditional store sets `CR0[EQ]` to report success. diff --git a/tools/ppc-manual/memory/ldbrx.md b/tools/ppc-manual/memory/ldbrx.md index ae72bc02..92d5c0ad 100644 --- a/tools/ppc-manual/memory/ldbrx.md +++ b/tools/ppc-manual/memory/ldbrx.md @@ -112,7 +112,7 @@ int InstrEmit_ldbrx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Reads little-endian.** `ldbrx` loads 8 bytes and reverses byte order before placing them in `RT`. With Xenon's PowerPC big-endian world view, the architectural effect is "load a little-endian doubleword as if it were big-endian" — useful when consuming network buffers, file headers (PNG IHDR, ZIP CRC32, etc.), or PC-side data structures that store little-endian. -- **Implementation detail.** The xenia snapshot calls `mem.read_u64(ea).swap_bytes()`. `read_u64` already returns the host-native value of the big-endian doubleword at `EA`; `swap_bytes` then flips it, giving the little-endian interpretation. Equivalent to four sequential `lbz` plus shifts, but issued as one micro-op. +- **Implementation detail.** Canary loads the doubleword without the byte swap `ld` applies, which on its little-endian host gives the little-endian interpretation directly. Equivalent to four sequential `lbz` plus shifts, but issued as one micro-op. - **No update form, X-form only.** PowerPC byte-reverse loads come in indexed form only (no `ldbrxu` or DS-form). `EA = (RA|0) + RB`. To increment a pointer, fold the increment into `RB` or use a separate `addi`. - **`RA0` semantics.** When `RA = 0`, base is the literal zero; `ldbrx RT, 0, RB` reads at exact `RB`. - **Alignment.** Like the rest of the byte-reverse family, `ldbrx` does **not** require natural alignment on hardware; the load is done as eight byte reads internally. Xenon may take an alignment exception on cache-inhibited storage. diff --git a/tools/ppc-manual/memory/lfd.md b/tools/ppc-manual/memory/lfd.md index 85d83587..66c410ae 100644 --- a/tools/ppc-manual/memory/lfd.md +++ b/tools/ppc-manual/memory/lfd.md @@ -249,8 +249,8 @@ int InstrEmit_lfdx(PPCHIRBuilder& f, const InstrData& i) { - **No FPSCR side effects.** `lfd` cannot raise IEEE exceptions: it neither rounds nor inspects the value. A signalling NaN read this way stays a signalling NaN until it is consumed by an arithmetic op. - **`RA0` semantics.** In the non-update forms (`lfd`, `lfdx`), `RA = 0` selects literal zero — `lfd FT, 0(0)` loads from absolute address 0. Update forms `lfdu` / `lfdux` invoke `RA = 0` and `RA = RT` (here `RA` is GPR; `RT` is FPR, so the latter cannot collide) as invalid forms when `RA = 0`. - **Alignment.** Xenon tolerates unaligned 8-byte FP loads; PowerISA technically permits implementations to raise alignment exceptions for FP loads, so portable code uses 8-byte aligned addresses. -- **Big-endian read.** Bytes are interpreted big-endian: byte at `EA` is bits 0–7 of the IEEE pattern (sign + part of exponent), byte at `EA+7` is bits 56–63 of the mantissa. `mem.read_f64` in xenia handles the host-side byte-swap. -- **MSR[FP] required.** Like all FP-register accesses, `lfd` requires the FP unit be enabled (MSR[FP]=1). Otherwise a Floating-Point Unavailable interrupt is raised. Xenia assumes FP is always enabled in user code. +- **Big-endian read.** Bytes are interpreted big-endian: byte at `EA` is bits 0–7 of the IEEE pattern (sign + part of exponent), byte at `EA+7` is bits 56–63 of the mantissa. Canary byte-swaps the loaded doubleword and reinterprets it as binary64 (`Cast`), bit-exact. +- **MSR[FP] required.** Like all FP-register accesses, `lfd` requires the FP unit be enabled (MSR[FP]=1). Otherwise a Floating-Point Unavailable interrupt is raised. Canary does not check `MSR[FP]`. - **Pair with [`stfd`](stfd.md).** Store-double is the symmetric counterpart. ## Related Instructions diff --git a/tools/ppc-manual/memory/lfs.md b/tools/ppc-manual/memory/lfs.md index 5d72ff10..96efc715 100644 --- a/tools/ppc-manual/memory/lfs.md +++ b/tools/ppc-manual/memory/lfs.md @@ -254,7 +254,7 @@ int InstrEmit_lfsx(PPCHIRBuilder& f, const InstrData& i) { - **Subnormals.** A binary32 subnormal expands to a binary64 normal — `lfs` quietly normalises. There is no "FPSCR[NI] non-IEEE mode" subnormal-to-zero behaviour applied at this stage on Xenon (NI affects arithmetic, not loads). - **`RA0` semantics.** In `lfs` / `lfsx`, `RA = 0` selects literal zero. Update forms `lfsu` / `lfsux` are invalid with `RA = 0`. - **Alignment.** Xenon tolerates unaligned 4-byte loads; PowerISA permits implementations to raise alignment exceptions for FP loads on cache-inhibited storage. -- **Big-endian read.** Bytes `EA..EA+3` form the binary32 pattern, sign bit at `EA[7]`. Xenia's `mem.read_f32` handles host byte-swap. +- **Big-endian read.** Bytes `EA..EA+3` form the binary32 pattern, sign bit at `EA[7]`. Canary byte-swaps the loaded word, reinterprets it as binary32 and widens it with `vcvtss2sd`, carrying a NaN's quiet/signalling bit across unchanged. - **MSR[FP] required.** Disabled FP unit raises Floating-Point Unavailable. - **Pair with [`stfs`](stfs.md).** Store-single performs the inverse double→single rounding (which **can** raise FPSCR exceptions because that direction may be inexact). diff --git a/tools/ppc-manual/memory/lha.md b/tools/ppc-manual/memory/lha.md index cd816217..978a1c47 100644 --- a/tools/ppc-manual/memory/lha.md +++ b/tools/ppc-manual/memory/lha.md @@ -255,10 +255,10 @@ int InstrEmit_lhax(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Sign-extending half-word load.** Reads 2 bytes big-endian, treats them as a signed 16-bit integer, sign-extends to 64 bits. Compare with [`lhz`](lhz.md), which zero-extends. Xenia's snapshot does the cast chain `u16 -> i16 -> i64 -> u64` to obtain the canonical sign-extended bit pattern. +- **Sign-extending half-word load.** Reads 2 bytes big-endian, treats them as a signed 16-bit integer, sign-extends to 64 bits. Compare with [`lhz`](lhz.md), which zero-extends. Canary emits `SignExtend(ByteSwap(LoadOffset(…, INT16)), INT64)`. - **Big-endian read.** Byte at `EA` is the most-significant 8 bits of the half; byte at `EA+1` is the least-significant. On little-endian hosts `mem.read_u16` returns the big-endian word in host-native form already. - **`RA0` (non-update forms).** `RA = 0` in `lha` and `lhax` selects literal zero — useful for absolute-address access patterns. -- **Update-form invalid forms.** `lhau` / `lhaux` invoke `RA = 0` and `RA = RT` as invalid forms; xenia performs the load before writing back `RA ← EA`, so an `RA = RT` collision silently destroys the loaded value. +- **Update-form invalid forms.** `lhau` / `lhaux` invoke `RA = 0` and `RA = RT` as invalid forms; Canary performs the load before writing back `RA ← EA`, so an `RA = RT` collision silently destroys the loaded value. - **No alignment requirement.** Xenon executes unaligned half-word loads without a fault. - **Common in audio / graphics code.** `lha` is the standard load for signed 16-bit PCM samples and signed 16-bit packed vertex deltas. - **Use `lha` rather than `lhz` + `extsh`.** Both produce the same result, but `lha` is one fused instruction and the compiler will pick it whenever the source type is `int16_t` / `short`. diff --git a/tools/ppc-manual/memory/lhbrx.md b/tools/ppc-manual/memory/lhbrx.md index 41e5b7a5..d7da8116 100644 --- a/tools/ppc-manual/memory/lhbrx.md +++ b/tools/ppc-manual/memory/lhbrx.md @@ -111,7 +111,7 @@ int InstrEmit_lhbrx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Reads little-endian half.** Loads 2 bytes and swaps them: byte at `EA` becomes the low 8 bits of `RT[16:23]`, byte at `EA+1` becomes the upper 8 bits. The xenia snapshot does `mem.read_u16(ea).swap_bytes()`. Effective for parsing little-endian on-disk or network half-word fields. +- **Reads little-endian half.** Loads 2 bytes and swaps them: byte at `EA` becomes the low 8 bits of `RT[16:23]`, byte at `EA+1` becomes the upper 8 bits. Canary loads the half-word without the byte swap `lhz` applies and zero-extends it. Effective for parsing little-endian on-disk or network half-word fields. - **Zero-extension to 64 bits.** Result occupies the full 64-bit GPR; high 48 bits are zero. There is no sign-extending byte-reverse load (`lhbrx` + `extsh` if you need one). - **X-form only — no update form.** Like all byte-reverse loads, only the indexed form exists. `EA = (RA|0) + RB`. Pointer-bumping requires a separate `addi`. - **`RA0` semantics.** When `RA = 0`, base is the literal zero — `lhbrx RT, 0, RB` reads at exact `RB`. diff --git a/tools/ppc-manual/memory/lhz.md b/tools/ppc-manual/memory/lhz.md index 29a70fb8..108dda56 100644 --- a/tools/ppc-manual/memory/lhz.md +++ b/tools/ppc-manual/memory/lhz.md @@ -257,7 +257,7 @@ int InstrEmit_lhzx(PPCHIRBuilder& f, const InstrData& i) { - **Big-endian read, zero-extension.** Reads 2 bytes big-endian, treats them as an unsigned 16-bit integer, zero-extends to 64 bits. The high 48 bits of `RT` become zero. Compare with [`lha`](lha.md), which sign-extends. - **`RA0` (non-update forms).** `RA = 0` in `lhz` / `lhzx` selects literal zero for absolute-address access. Update forms `lhzu` / `lhzux` invoke `RA = 0` and `RA = RT` as invalid forms. -- **Update-form ordering.** Xenia computes `EA`, performs the load, then writes `RA ← EA`. If `RA == RT` (an invalid form per IBM), the load result is overwritten by `EA` immediately. +- **Update-form ordering.** Canary computes `EA`, performs the load into `RT`, then writes `RA ← EA`. If `RA == RT` (an invalid form per IBM), the load result is overwritten by `EA` immediately. - **No alignment requirement.** Xenon executes unaligned half-word loads without faulting. `MEM(EA, 2)` reads the two consecutive bytes at `EA`. - **Common as Unicode codepoint loader.** Xbox 360 system strings are UTF-16; `lhz` is the canonical load for a single 16-bit codepoint. - **Use `lhz` rather than `lbz` × 2 + shift.** One fused instruction is faster and lets the load-store unit handle alignment. diff --git a/tools/ppc-manual/memory/lmw.md b/tools/ppc-manual/memory/lmw.md index 50bc7abd..934ba4fb 100644 --- a/tools/ppc-manual/memory/lmw.md +++ b/tools/ppc-manual/memory/lmw.md @@ -114,12 +114,12 @@ int InstrEmit_lmw(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Bulk register restore.** Loads `(32 - RT)` consecutive 32-bit words starting at `EA` into `RT`, `RT+1`, …, `r31`. Used by AIX/PowerPC ABI prologues/epilogues to restore non-volatile GPRs in one instruction. Modern compilers prefer multiple `lwz` for scheduling; `lmw` survives in older code and hand-rolled context-switch routines. -- **Loop bound from encoding.** Xenia's snapshot iterates `for r in instr.rd()..32`, exactly matching IBM's "load until r31 inclusive" semantic. With `RT = 28`, four registers (r28..r31) are loaded. +- **Loop bound from encoding.** Canary loops over `r(RT)..r31` (`j < 32 - RT`), matching IBM's "load until r31 inclusive" semantic. With `RT = 28`, four registers (r28..r31) are loaded. ⚠️ If `RA` falls in that range (an invalid form), Canary skips the load into `RA`, so the base register keeps its value. - **Each word is zero-extended.** Like `lwz`, every loaded 32-bit word zero-extends into the destination's 64-bit GPR. The high 32 bits of each `r[k]` become zero. - **Big-endian read.** Word at `EA` goes to `r[RT]`, word at `EA+4` goes to `r[RT+1]`, etc. Each word is itself loaded most-significant-byte-first. - **`RA0` semantics.** When `RA = 0`, base is literal zero. Useful for absolute-address restoration. -- **Invalid forms.** AIX docs declare it invalid for `RA` to be in the destination range `[RT, 31]` — a load could overwrite the base register mid-sequence. Xenia performs loads in order without this check. -- **Alignment.** PowerISA requires word-aligned `EA`; an unaligned `lmw` may raise an alignment exception on real hardware. Xenia tolerates it. +- **Invalid forms.** AIX docs declare it invalid for `RA` to be in the destination range `[RT, 31]` — a load could overwrite the base register mid-sequence. Canary skips the load into `RA` in that case, so the base register keeps its value. +- **Alignment.** PowerISA requires word-aligned `EA`; an unaligned `lmw` may raise an alignment exception on real hardware. Canary does not check. - **Performance trap.** On modern PowerPC implementations `lmw` is microcoded — slower than the equivalent sequence of `lwz`. Compilers avoid it. ## Related Instructions diff --git a/tools/ppc-manual/memory/lswi.md b/tools/ppc-manual/memory/lswi.md index a08d6202..4bba4a6f 100644 --- a/tools/ppc-manual/memory/lswi.md +++ b/tools/ppc-manual/memory/lswi.md @@ -101,11 +101,11 @@ int InstrEmit_lswi(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Byte-granular bulk load.** Reads `NB` bytes starting at `EA` and packs them, big-endian, into successive GPRs starting at `RT`. Each filled GPR holds 4 bytes in its low word; partial last words are left- (most-significant-byte-) aligned with trailing zero bytes. The byte count `NB` is held in the `RB` field of the instruction encoding (1..31), with the special case `NB = 0` meaning "32 bytes". -- **Register wraparound at r31 → r0.** The snapshot uses `rd = (rd + 1) % 32`. If the byte count is large enough to spill past `r31`, the next register is `r0`, then `r1`, etc. AIX docs flag the "RA in destination range" and "RB in destination range" cases as invalid; xenia does not check. +- **Register wraparound at r31 → r0.** If the byte count is large enough to spill past `r31`, the next register is `r0`, then `r1`, etc. AIX docs flag the "RA in destination range" and "RB in destination range" cases as invalid. ⚠️ Canary does not implement `lswi`: its emitter is `XEINSTRNOTIMPLEMENTED`, so translating one logs "Unimplemented instr" and, with the default `break_on_unimplemented_instructions`, breaks. - **`RA0` semantics.** `RA = 0` selects literal zero. There is no `RA` post-write — `lswi` is not an update form. -- **Big-endian byte ordering inside each word.** First byte read goes into bits 0–7 of the destination GPR (most-significant byte). Xenia's loop builds `val |= b << (24 - byte_idx * 8)`, matching that bit position. +- **Big-endian byte ordering inside each word.** First byte read goes into bits 0–7 of the destination GPR (most-significant byte). There is no Canary behaviour to compare: it does not implement `lswi`. - **Last partial word.** When `NB` is not a multiple of 4, the final GPR's unused low bytes are zero. The high bits remain whatever the load placed there. -- **Alignment.** The architecture allows arbitrary alignment, but real implementations may take alignment exceptions on cache-inhibited storage; xenia tolerates any address. +- **Alignment.** The architecture allows arbitrary alignment, but real implementations may take alignment exceptions on cache-inhibited storage. Canary does not implement `lswi` (`XEINSTRNOTIMPLEMENTED`), so there is no Canary behaviour to compare. - **Vanishingly rare in compiled code.** Compilers don't emit `lswi`. Hand-written `memcpy` cores from the PowerPC SDK era used it for short copies; otherwise it appears mostly in byte-string init helpers. ## Related Instructions diff --git a/tools/ppc-manual/memory/lswx.md b/tools/ppc-manual/memory/lswx.md index 38f1ac3a..f03e41f5 100644 --- a/tools/ppc-manual/memory/lswx.md +++ b/tools/ppc-manual/memory/lswx.md @@ -100,10 +100,10 @@ int InstrEmit_lswx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Byte count from `XER[25..31]`.** Unlike `lswi` (where the count is encoded as `RB`), `lswx` reads `XER[25..31]` for the byte count `NB` (0..127). Xenia's snapshot does `let nb = (ctx.xer() & 0x7F) as u32;`. `NB = 0` is **not** the "32 bytes" special case here — zero means literally zero bytes, no registers touched. +- **Byte count from `XER[25..31]`.** Unlike `lswi` (where the count is encoded as `RB`), `lswx` reads `XER[25..31]` for the byte count `NB` (0..127). `NB = 0` is **not** the "32 bytes" special case here — zero means literally zero bytes, no registers touched. ⚠️ Canary does not implement `lswx`: its emitter is `XEINSTRNOTIMPLEMENTED`, so translating one logs "Unimplemented instr" and, with the default `break_on_unimplemented_instructions`, breaks. - **Register packing identical to `lswi`.** Bytes are packed big-endian into successive GPRs starting at `RT`, four bytes per register, with wraparound `r31 → r0`. Trailing bytes in the last register are zero-padded on the right. - **`RA0` semantics.** `RA = 0` selects literal zero. The instruction has no update form — `RA` is not modified. -- **Invalid forms.** AIX flags as invalid: `RT` collides with `RA` or `RB` within the destination range; `XER[25..31]` and `NB` byte stream wraps around through both `RA` and `RB`. Xenia performs writes regardless, with last-write-wins semantics. +- **Invalid forms.** AIX flags as invalid: `RT` collides with `RA` or `RB` within the destination range; `XER[25..31]` and `NB` byte stream wraps around through both `RA` and `RB`. Canary does not implement `lswx` (`XEINSTRNOTIMPLEMENTED`). - **Used for non-multiple-of-4 copies.** Together with `lswi`, gives a way to load a runtime-determined byte count without per-byte loops. Compilers don't emit it; rare hand-written copy primitives may. - **Alignment.** Architecture allows arbitrary alignment; cache-inhibited storage may raise alignment exceptions on real hardware. - **No FPSCR / CR effects.** Pure data movement. diff --git a/tools/ppc-manual/memory/lvebx.md b/tools/ppc-manual/memory/lvebx.md index cd2e3028..c8cac24a 100644 --- a/tools/ppc-manual/memory/lvebx.md +++ b/tools/ppc-manual/memory/lvebx.md @@ -107,9 +107,9 @@ int InstrEmit_lvebx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Single-byte element load.** Architecturally `lvebx` loads exactly **one** byte from `EA` and places it in lane `EA mod 16` of the destination vector; the other 15 lanes are *undefined* (PowerISA permits implementations to leave them as garbage). Real hardware: lane `EA mod 16` gets the byte, others are unspecified. -- **Xenia simplification — full-line read.** The xenia snapshot is shared with `lvehx` / `lvewx` and reads the **entire 16-byte aligned line** (`ea & ~0xF`, then 16 bytes), placing it in `VD`. This is stronger than the architectural guarantee — every lane is filled with whatever happened to be at the line — but matches the practical idiom of using these single-element loads to assemble a vector. Code that depends on undefined-lane behaviour will still produce well-defined output under xenia. -- **Operand order subtle.** Unlike `lvx`, the architectural EA is **not** masked. The lane is `EA & 0xF`. Xenia's force-align mask (`& !0xF`) is a deliberate emulator simplification. -- **`RA0` semantics.** When `RA = 0`, base is literal zero; `lvebx VD, 0, RB` reads the byte at `RB` (and, in xenia, the surrounding aligned line). +- **Canary simplification — full-line read.** Canary's `lvebx`, `lvehx` and `lvewx` all emit `lvx`'s body and read the **entire 16-byte aligned line** (`ea & ~0xF`, then 16 bytes), placing it in `VD`. This is stronger than the architectural guarantee — every lane is filled with whatever happened to be at the line — but matches the practical idiom of using these single-element loads to assemble a vector. Code that depends on undefined-lane behaviour will still produce well-defined output under Canary. +- **Operand order subtle.** Unlike `lvx`, the architectural EA is **not** masked. The lane is `EA & 0xF`. ⚠️ Canary treats `lvebx` exactly like `lvx`: it rounds `EA` down to a 16-byte boundary and loads the whole 128-bit vector. +- **`RA0` semantics.** When `RA = 0`, base is literal zero; `lvebx VD, 0, RB` reads the byte at `RB` (and, in Canary, the surrounding aligned line). - **No update form.** No `lvebux` exists. Pointer-bumping requires a separate `addi`. - **No VMX128 sibling.** There is no `lvebx128` — the single-byte load family was kept Altivec-only in the Xbox 360 VMX128 extension, since 16-byte aligned loads (`lvx128`) plus `vperm`/`vsel` are usually faster. - **Common idiom.** Pair with `vperm` or `vsplt*` to broadcast the loaded byte to all lanes, or with `vinsertb` / shifts to assemble a vector from non-adjacent memory locations. diff --git a/tools/ppc-manual/memory/lvehx.md b/tools/ppc-manual/memory/lvehx.md index b3c63251..23a1dff7 100644 --- a/tools/ppc-manual/memory/lvehx.md +++ b/tools/ppc-manual/memory/lvehx.md @@ -107,9 +107,9 @@ int InstrEmit_lvehx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Single half-word element load.** Architecturally `lvehx` loads exactly **two** bytes from `EA` (which must be 2-byte aligned) and places them in the half-word lane `(EA mod 16) >> 1` of the destination vector; the other 7 half-word lanes are *undefined*. -- **EA must be half-aligned.** The low bit of `EA` is masked by hardware to align to 2 — an odd `EA` rounds down. Xenia's shared snapshot rounds further, masking to 16-byte alignment. -- **Xenia simplification — full-line read.** The xenia snapshot is shared with `lvebx` / `lvewx`: `ea & ~0xF` then a full 16-byte read into `VD`. Architectural undefined lanes are filled in deterministically, which is stronger than hardware guarantees but practically convenient. -- **`RA0` semantics.** When `RA = 0`, base is literal zero; `lvehx VD, 0, RB` reads at `RB` (and, in xenia, the surrounding aligned line). +- **EA must be half-aligned.** The low bit of `EA` is masked by hardware to align to 2 — an odd `EA` rounds down. ⚠️ Canary treats `lvehx` exactly like `lvx`: it masks to 16-byte alignment and loads the whole 128-bit vector. +- **Canary simplification — full-line read.** Canary's `lvehx` emits `lvx`'s body, like `lvebx` / `lvewx`: `ea & ~0xF` then a full 16-byte read into `VD`. Architectural undefined lanes are filled in deterministically, which is stronger than hardware guarantees but practically convenient. +- **`RA0` semantics.** When `RA = 0`, base is literal zero; `lvehx VD, 0, RB` reads at `RB` (and, in Canary, the surrounding aligned line). - **No update form.** No `lvehux` exists. - **No VMX128 sibling.** No `lvehx128` — Xbox 360 code prefers `lvx128` plus `vperm`. - **Big-endian half within the lane.** The byte at the lower address is the most-significant byte of the half-word lane. diff --git a/tools/ppc-manual/memory/lvewx.md b/tools/ppc-manual/memory/lvewx.md index 6e119ef2..be99c6d2 100644 --- a/tools/ppc-manual/memory/lvewx.md +++ b/tools/ppc-manual/memory/lvewx.md @@ -161,8 +161,8 @@ int InstrEmit_lvewx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd, ## Special Cases & Edge Conditions - **Single word element load.** Architecturally `lvewx` loads exactly **four** bytes from `EA` (which must be 4-byte aligned) and places them in the word lane `(EA mod 16) >> 2` of the destination vector; the other 3 word lanes are *undefined*. -- **EA must be word-aligned.** The low two bits of `EA` are masked by hardware. Xenia's shared snapshot rounds further to 16-byte alignment for both `lvewx` and `lvewx128`. -- **Xenia simplification — full-line read.** Both `lvewx` and `lvewx128` snapshots load the full aligned 16 bytes from `ea & ~0xF` into the destination vector. Architectural undefined lanes are filled deterministically. +- **EA must be word-aligned.** The low two bits of `EA` are masked by hardware. ⚠️ Canary treats `lvewx` and `lvewx128` exactly like `lvx`: it masks to 16-byte alignment and loads the whole 128-bit vector. +- **Canary simplification — full-line read.** Canary's `lvewx` and `lvewx128` both load the full aligned 16 bytes from `ea & ~0xF` into the destination vector. Architectural undefined lanes are filled deterministically. - **`RA0` semantics.** When `RA = 0`, base is literal zero. - **No update form.** No `lvewux` exists. - **VMX128 sibling.** `lvewx128` shares semantics; the only difference is the operand encoding. VMX128 uses a 7-bit register index split across `VD128l ‖ VD128h` so it can address `v0..v127` instead of the 32-register Altivec space. diff --git a/tools/ppc-manual/memory/lvlx.md b/tools/ppc-manual/memory/lvlx.md index 3cfe0f2a..0fa8c14b 100644 --- a/tools/ppc-manual/memory/lvlx.md +++ b/tools/ppc-manual/memory/lvlx.md @@ -166,8 +166,8 @@ int InstrEmit_lvlx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd, - **Companion idiom.** `lvlx VD, RA, RB ; lvrx Vtemp, RA, RB ; vor VD, VD, Vtemp` produces the unaligned 16 bytes at `EA` regardless of alignment. This was the canonical unaligned-vector-read recipe before `lvsl`/`vperm` shuffles became the more common idiom. - **No alignment masking.** Unlike `lvx`, the EA is **not** rounded down. `EA mod 16` controls how the data is shifted into the destination. - **`RA0` semantics.** `RA = 0` selects literal zero. -- **Microsoft Xbox 360 specific.** `lvlx` and `lvrx` are not in the standard Altivec specification — they are part of Microsoft's VMX128 / Cell BE-style extension, defined in PowerPC Cell and later VMX. The Xbox 360 Xenon supports them (decoder + xenia entry confirm). -- **Implementation in xenia.** The shared snapshot calls `vmx::load_vector_left(mem, ea)`, which performs the unaligned partial-byte read and zero-fills the right side. +- **Microsoft Xbox 360 specific.** `lvlx` and `lvrx` are not in the standard Altivec specification — they are part of Microsoft's VMX128 / Cell BE-style extension, defined in PowerPC Cell and later VMX. The Xbox 360 Xenon supports them, and Canary implements them (its comment: "in Cell docs only"). +- **Implementation in Canary.** Canary emits its `LoadVectorLeft` op: the x64 sequence reads the 16-byte aligned block containing `EA`, shuffles the bytes from `EA` onward into the left of `VD` and zero-fills the right side. - **VMX128 sibling (`lvlx128`).** Same semantics; different operand encoding (7-bit register field, addressing `v0..v127`). - **`lvlxl` is the LRU-hint variant.** Same data behaviour, hint ignored under emulation. diff --git a/tools/ppc-manual/memory/lvlxl.md b/tools/ppc-manual/memory/lvlxl.md index e05e74c5..d94ce0ba 100644 --- a/tools/ppc-manual/memory/lvlxl.md +++ b/tools/ppc-manual/memory/lvlxl.md @@ -173,11 +173,11 @@ int InstrEmit_lvlx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd, ## Special Cases & Edge Conditions - **Same data effect as [`lvlx`](lvlx.md), with LRU cache hint.** Reads `(16 - (EA mod 16))` bytes starting at `EA` into the left side of `VD`; right side zero-filled. The `l` suffix tells the cache the line is least-recently-used — likely streaming, evict early under pressure. -- **Hint ignored under emulation.** Xenia's snapshot is shared with `lvlx` (`PpcOpcode::lvlx | PpcOpcode::lvlxl => …`). Functional behaviour is identical to `lvlx`. +- **Hint ignored under emulation.** Canary's `lvlxl` simply calls its `lvlx` emitter. Functional behaviour is identical to `lvlx`. - **No alignment masking.** Like `lvlx`, the exact `EA` controls how data shifts into the vector. - **`RA0` semantics.** `RA = 0` selects literal zero. - **Microsoft Xbox 360 specific.** Part of the VMX128 / Cell BE extended set, not in baseline Altivec. -- **Used in single-pass streaming reads.** Decoder loops that consume each vector once benefit from the LRU hint on real hardware; xenia gains nothing from it. +- **Used in single-pass streaming reads.** Decoder loops that consume each vector once benefit from the LRU hint on real hardware; Canary gains nothing from it. - **VMX128 sibling (`lvlxl128`).** Identical semantics; alternative operand encoding addressing `v0..v127`. ## Related Instructions diff --git a/tools/ppc-manual/memory/lvrx.md b/tools/ppc-manual/memory/lvrx.md index aa1f83a6..31320d35 100644 --- a/tools/ppc-manual/memory/lvrx.md +++ b/tools/ppc-manual/memory/lvrx.md @@ -175,7 +175,7 @@ int InstrEmit_lvrx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd, - **Right vs. left semantics.** "Right" refers to lower-numbered (high-significance) lanes after rotation, not in any byte-address sense — see PowerISA Cell BE addenda for the exact bit-position formulas. - **No alignment masking.** Like `lvlx`, the exact `EA` is used; the value `EA mod 16` controls how data is rotated. - **`RA0` semantics.** `RA = 0` selects literal zero. -- **Implementation in xenia.** The shared snapshot calls `vmx::load_vector_right(mem, ea)`, returning a zero-filled left side and the requested right-bytes payload. +- **Implementation in Canary.** Canary emits its `LoadVectorRight` op: when `EA` is 16-byte aligned the x64 sequence returns zero without reading memory; otherwise it shuffles the bytes before `EA` in the aligned block into the right of `VD` and zero-fills the left side. - **Microsoft Xbox 360 specific.** Part of VMX128 / Cell BE, not in baseline Altivec. - **VMX128 sibling (`lvrx128`).** Identical semantics; alternative operand encoding. - **`lvrxl` is the LRU-hint variant.** Same data; cache hint ignored under emulation. diff --git a/tools/ppc-manual/memory/lvrxl.md b/tools/ppc-manual/memory/lvrxl.md index a6f9bc25..f0355003 100644 --- a/tools/ppc-manual/memory/lvrxl.md +++ b/tools/ppc-manual/memory/lvrxl.md @@ -181,7 +181,7 @@ int InstrEmit_lvrx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd, ## Special Cases & Edge Conditions - **Same data effect as [`lvrx`](lvrx.md), with LRU cache hint.** Reads `(EA mod 16)` bytes from the previous aligned line into the right half of `VD`; left half zero-filled. The `l` suffix tells the cache the line is least-recently-used. -- **Hint ignored under emulation.** Xenia's snapshot is shared with `lvrx` (`PpcOpcode::lvrx | PpcOpcode::lvrxl => …`). +- **Hint ignored under emulation.** Canary's `lvrxl` simply calls its `lvrx` emitter. - **No alignment masking.** The exact `EA` controls how data shifts. - **`RA0` semantics.** `RA = 0` selects literal zero. - **Microsoft Xbox 360 specific.** Part of the VMX128 / Cell BE extended set. diff --git a/tools/ppc-manual/memory/lvxl.md b/tools/ppc-manual/memory/lvxl.md index bbedd32d..6119c38d 100644 --- a/tools/ppc-manual/memory/lvxl.md +++ b/tools/ppc-manual/memory/lvxl.md @@ -169,12 +169,12 @@ int InstrEmit_lvx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd, ## Special Cases & Edge Conditions - **Same data effect as [`lvx`](lvx.md), but with cache hint.** Loads 16 bytes from `EA & ~0xF` into `VD`. The `l` suffix signals to the cache hardware that the line is **least-recently-used** — a hint that the line will not be reused soon, allowing the cache to evict it preferentially under pressure. Useful in streaming reads (e.g. once-through vertex transforms, decode passes). -- **Hint ignored under emulation.** Xenia's snapshot comment is explicit: "Same as lvx but with cache hint (ignored)". The functional behaviour is identical to `lvx` — only real hardware acts on the hint. +- **Hint ignored under emulation.** Canary's `lvxl` simply calls its `lvx` emitter, so the functional behaviour is identical to `lvx` — only real hardware acts on the hint. - **Alignment is forced, not checked.** Like `lvx`, the low four bits of `EA` are masked. Unaligned `EA` silently rounds down to the 16-byte boundary. - **Big-endian lane layout.** Byte at the aligned base goes into lane 0; byte at base+15 into lane 15. - **`RA0` semantics.** `RA = 0` selects literal zero. - **No update form.** `lvxl` has no `u`-suffix variant. -- **VMX128 sibling (`lvxl128`).** Identical semantics; the only difference is the operand encoding using the split-field 7-bit register index addressing `v0..v127`. Xenia's snapshot dispatches on the opcode to decide which decode helper to use. +- **VMX128 sibling (`lvxl128`).** Identical semantics; the only difference is the operand encoding using the split-field 7-bit register index addressing `v0..v127`. Canary's `lvxl128` calls `lvx128`, which decodes the 7-bit index and shares `lvx`'s body. - **Note: assembler typo.** The Syntax block above shows `lvslx` for the non-128 variant — that is a transcription artefact of the source XML. The real mnemonic is `lvxl`. ## Related Instructions diff --git a/tools/ppc-manual/memory/lwa.md b/tools/ppc-manual/memory/lwa.md index dc89695b..12e69fbd 100644 --- a/tools/ppc-manual/memory/lwa.md +++ b/tools/ppc-manual/memory/lwa.md @@ -210,10 +210,10 @@ int InstrEmit_lwax(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Sign-extending word load (32→64).** Reads 4 bytes big-endian, treats them as a signed int32, sign-extends to 64 bits. The xenia snapshot does the cast chain `u32 -> i32 -> i64 -> u64` to materialise the canonical sign-extended bit pattern. +- **Sign-extending word load (32→64).** Reads 4 bytes big-endian, treats them as a signed int32, sign-extends to 64 bits. Canary emits `SignExtend(ByteSwap(LoadOffset(…, INT32)), INT64)`. - **No `lwau` (D-form-update) in PowerISA.** Only `lwa` (DS-form), `lwax` (X-form), and `lwaux` (X-form-update) exist. The D-form-update slot is occupied by something else in the encoding space — to update with a 16-bit immediate you must use a separate `addi` plus `lwa`. - **DS-form displacement.** Like [`ld`](ld.md), `lwa` uses a 14-bit signed displacement scaled by 4 (`EXTS(ds || 0b00)`). The two encoding bits 30–31 distinguish `lwa` (XO=10) from `ld` (XO=00) and `ldu` (XO=01). -- **`RA0` semantics.** `RA = 0` in `lwa` and `lwax` selects literal zero. `lwaux` invokes `RA = 0` and `RA = RT` as invalid forms; xenia performs the load before writing back `RA`, so an `RA = RT` collision destroys the loaded value. +- **`RA0` semantics.** `RA = 0` in `lwa` and `lwax` selects literal zero. `lwaux` invokes `RA = 0` and `RA = RT` as invalid forms; Canary performs the load before writing back `RA`, so an `RA = RT` collision destroys the loaded value. - **Alignment.** Xenon tolerates unaligned 4-byte loads. PowerISA permits but does not require an alignment exception; some implementations may raise one for cache-inhibited storage. - **Use `lwa` rather than `lwz` + `extsw`.** When the source type is `int32_t`, `lwa` is one fused instruction. - **Common in 64-bit code.** Sign-extending 32-bit fields out of structures (e.g. signed file offsets) into 64-bit GPRs uses this family. diff --git a/tools/ppc-manual/memory/lwarx.md b/tools/ppc-manual/memory/lwarx.md index 3ee5f189..6710995d 100644 --- a/tools/ppc-manual/memory/lwarx.md +++ b/tools/ppc-manual/memory/lwarx.md @@ -131,9 +131,9 @@ int InstrEmit_lwarx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Reservation set on the addressed word.** Loads `MEM(EA, 4)` zero-extended to 64 bits and atomically establishes a *reservation* on `EA`. A subsequent [`stwcx`](stwcx.md) at the same address completes only if the reservation is still valid. Together they form the canonical 32-bit load-linked / store-conditional pair for lock-free updates and futexes. -- **One reservation per thread.** Xenia's snapshot writes `reserved_addr`, `reserved_val`, and `has_reservation` in the per-context state. Hardware behaves the same: each hardware thread has at most one reservation. A second `lwarx` (or `ldarx`) discards the previous reservation. -- **Granule.** Architecturally one naturally-aligned word; on Xenon the practical reservation granule is one **cache line** (128 bytes) — any store to that line by another agent invalidates the reservation. Xenia simplifies to per-address tracking, which can let real-hardware-failing pairs succeed under emulation. -- **Alignment requirement.** `EA` must be 4-byte aligned. An unaligned `lwarx` raises an alignment exception on hardware; xenia does not check. +- **One reservation per thread.** Hardware: each hardware thread has at most one reservation, and a second `lwarx` (or `ldarx`) discards the previous one. Canary instead sets a bit for the 64 KiB block holding `EA` in a bitmap shared by all threads (`lock bts`) and caches the loaded value per thread; taking a second reservation while one is still held hits a `DebugBreak` (`int3`) in its helper rather than replacing the first. The `no_reserved_ops` cvar turns all of this off. +- **Granule.** Architecturally one naturally-aligned word; on Xenon the practical reservation granule is one **cache line** (128 bytes) — any store to that line by another agent invalidates the reservation. Canary's granule is a 64 KiB block, and ordinary stores never clear it: `stwcx.` succeeds if this thread still holds the block bit and the word still holds the value `lwarx` read — which can let real-hardware-failing pairs succeed under emulation. +- **Alignment requirement.** `EA` must be 4-byte aligned. An unaligned `lwarx` raises an alignment exception on hardware; Canary does not check. - **`RA0` semantics.** When `RA = 0`, base is literal zero — `lwarx RT, 0, RB` reads at exact `RB`. - **Reservation-loss events.** Any exception, context switch, or store by another thread to the reserved line clears the reservation. Application code treats `stwcx.` failure (CR0[EQ]=0) as a normal retry condition. - **Pair atomically.** Code must be `lwarx ... do work ... stwcx.` with no intervening loads/stores that could reorder. Optionally fence with [`lwsync`](sync.md) inside the loop. diff --git a/tools/ppc-manual/memory/lwbrx.md b/tools/ppc-manual/memory/lwbrx.md index 4a331b76..e2f0f2a1 100644 --- a/tools/ppc-manual/memory/lwbrx.md +++ b/tools/ppc-manual/memory/lwbrx.md @@ -112,7 +112,7 @@ int InstrEmit_lwbrx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Reads little-endian word.** Loads 4 bytes and reverses byte order. With Xenon's big-endian world view, the architectural effect is "load a little-endian word as if it were big-endian" — the standard parser instruction for PNG/ZIP/RIFF/TGA chunk fields, network protocol fields, and PC-side-formatted data. -- **Implementation detail.** The xenia snapshot calls `mem.read_u32(ea).swap_bytes()`. `read_u32` already returns the host-native value of the big-endian word at `EA`; `swap_bytes` then flips it. +- **Implementation detail.** Canary loads the word without the byte swap `lwz` applies (and zero-extends it), which on its little-endian host gives the little-endian interpretation directly. - **X-form only — no update form.** Only the indexed form exists. `EA = (RA|0) + RB`. Pointer-bumping requires a separate `addi`. - **`RA0` semantics.** When `RA = 0`, base is literal zero; `lwbrx RT, 0, RB` reads at exact `RB`. - **Zero-extension to 64 bits.** Result occupies the full 64-bit GPR; high 32 bits zero. There is no sign-extending byte-reverse load — combine with `extsw` if needed. diff --git a/tools/ppc-manual/memory/lwz.md b/tools/ppc-manual/memory/lwz.md index cb0f5b0a..518a4500 100644 --- a/tools/ppc-manual/memory/lwz.md +++ b/tools/ppc-manual/memory/lwz.md @@ -270,10 +270,10 @@ int InstrEmit_lwzx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Big-endian memory.** The Xenon reads memory big-endian. Translating to little-endian hosts requires a byte-swap on the 32-bit read (or calling a `mem_read_u32_be` helper as in the C example). Matching byte-order helpers in xenia: `mem.read_u32(...)` already returns a host-native `u32` of the big-endian word. +- **Big-endian memory.** The Xenon reads memory big-endian. Translating to little-endian hosts requires a byte-swap on the 32-bit read (or calling a `mem_read_u32_be` helper as in the C example). Canary does exactly that: `ByteSwap(LoadOffset(…, INT32))`, zero-extended to 64 bits. - **Zero-extension to 64 bits.** The result occupies the full 64-bit GPR; the high 32 bits are zero. This is semantically distinct from [`lwa`](lwa.md) / [`lwax`](lwax.md) / [`lwaux`](lwaux.md), which sign-extend. Most Xbox 360 code uses `lwz` for unsigned word loads and for pointer loads (addresses are 32-bit and fit in the low half). - **`RA0` (non-update forms).** In `lwz` and `lwzx`, when the encoded `RA = 0` the base is the literal zero, **not** `r0`. This enables absolute-address loads `lwz RT, 0x8000(0)` and is heavily used to read from statically-linked data near the TOC base. -- **Update forms require `RA ≠ 0`.** `lwzu` / `lwzux` invoke "RA = 0" as an invalid form; AIX docs say the result is undefined and assemblers will refuse to assemble `lwzu RT, d(0)`. Further, `RA = RT` is also invalid (the "effective address" write and the "loaded value" write would race). Xenia implements update forms without these checks; rely on incoming code being well-formed. +- **Update forms require `RA ≠ 0`.** `lwzu` / `lwzux` invoke "RA = 0" as an invalid form; AIX docs say the result is undefined and assemblers will refuse to assemble `lwzu RT, d(0)`. Further, `RA = RT` is also invalid (the "effective address" write and the "loaded value" write would race). Canary implements update forms without these checks; rely on incoming code being well-formed. - **No alignment requirement.** Xenon executes unaligned word loads without a fault (unlike some POWER cores). `MEM(EA, 4)` reads four bytes starting at `EA`, whatever alignment. - **No ordering guarantee.** These are ordinary cached loads; use [`sync`](sync.md) / [`isync`](isync.md) / [`lwsync`](sync.md) for explicit ordering, or [`lwarx`](lwarx.md) for load-reserve semantics. - **Indexed variant operand order.** `lwzx RT, RA, RB` — `RA` is the base (with `RA0` semantics), `RB` is the offset. The variant without `RA0` is `lwzux`. diff --git a/tools/ppc-manual/memory/stb.md b/tools/ppc-manual/memory/stb.md index 8e63cb5b..1e5c2147 100644 --- a/tools/ppc-manual/memory/stb.md +++ b/tools/ppc-manual/memory/stb.md @@ -248,7 +248,7 @@ int InstrEmit_stbx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Single-byte write.** Writes the low 8 bits of `RS` (`(RS)[56:63]` in IBM bit-numbering, equivalently `RS & 0xFF`) at `EA`. The xenia snapshot does `mem.write_u8(ea, ctx.gpr[instr.rs()] as u8)`, which casts the GPR's low byte directly. +- **Single-byte write.** Writes the low 8 bits of `RS` (`(RS)[56:63]` in IBM bit-numbering, equivalently `RS & 0xFF`) at `EA`. Canary stores `Truncate(RS, INT8)`, the GPR's low byte. - **No endian concerns.** A single byte has no endianness — the byte at `EA` is the byte you wrote. - **`RA0` (non-update forms).** `RA = 0` in `stb` and `stbx` selects literal zero as base — useful for absolute writes. Update forms `stbu` / `stbux` invoke `RA = 0` as an invalid form (no `RA = RT` collision since the source is `RS`, not `RT`). - **Update-form post-write.** `stbu` / `stbux` write the computed `EA` back to `RA` after the store. The order is store-then-update; if `RA = RS` the store is unaffected (the store reads `RS` first), but the new `RA` value reflects `EA`, not the original `RS`. diff --git a/tools/ppc-manual/memory/std.md b/tools/ppc-manual/memory/std.md index 8fc8e097..b8540c43 100644 --- a/tools/ppc-manual/memory/std.md +++ b/tools/ppc-manual/memory/std.md @@ -251,7 +251,7 @@ int InstrEmit_stdx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **DS-form, not D-form.** Like [`ld`](ld.md), `std` uses a 14-bit signed displacement scaled by 4 (`EXTS(ds || 0b00)`). Bits 30–31 are the extended opcode used to distinguish `std` (XO=0) from `stdu` (XO=1). Assemblers verify the byte displacement is a multiple of 4. -- **Big-endian write.** The 64-bit value of `RS` is written most-significant-byte-first: `RS[0:7]` to `EA`, `RS[56:63]` to `EA+7`. Xenia's `mem.write_u64` performs the host-side byte swap if needed. +- **Big-endian write.** The 64-bit value of `RS` is written most-significant-byte-first: `RS[0:7]` to `EA`, `RS[56:63]` to `EA+7`. Canary byte-swaps `RS` before the host store. - **`RA0` for `std` and `stdx`.** When `RA = 0`, base is the literal zero. Update forms `stdu` / `stdux` invoke `RA = 0` as an invalid form (no `RA = RS` collision possible — `RS` and `RA` are independent encoding fields, and even if equal the store reads `RS` first). - **Update-form post-write.** `stdu` / `stdux` write `EA` to `RA` after the store. Order is store-then-update. - **Alignment.** Xenon tolerates unaligned doubleword stores. PowerISA permits implementations to raise alignment exceptions; portable code keeps doublewords 8-byte aligned. Cache-inhibited storage may force alignment. diff --git a/tools/ppc-manual/memory/stdbrx.md b/tools/ppc-manual/memory/stdbrx.md index 9d2c6d9c..a9840633 100644 --- a/tools/ppc-manual/memory/stdbrx.md +++ b/tools/ppc-manual/memory/stdbrx.md @@ -110,7 +110,7 @@ int InstrEmit_stdbrx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Writes little-endian doubleword.** Reverses the 8 bytes of `RS` and stores them at `EA`. Compared to a regular `std`, the byte at `EA` becomes `RS[56:63]` (least-significant), and the byte at `EA+7` becomes `RS[0:7]` (most-significant). The xenia snapshot calls `mem.write_u64(ea, ctx.gpr[instr.rs()].swap_bytes())`. +- **Writes little-endian doubleword.** Reverses the 8 bytes of `RS` and stores them at `EA`. Compared to a regular `std`, the byte at `EA` becomes `RS[56:63]` (least-significant), and the byte at `EA+7` becomes `RS[0:7]` (most-significant). Canary stores `RS` without the byte swap `std` applies, which on its little-endian host writes the bytes reversed. - **Used to emit little-endian payloads.** Symmetric counterpart of [`ldbrx`](ldbrx.md). Common when writing PC-side file formats, network packets, or PE/COFF headers from PowerPC code. - **X-form only — no update form, no DS-form.** Only the indexed form exists. `EA = (RA|0) + RB`. Pointer-bumping requires a separate `addi`. - **`RA0` semantics.** When `RA = 0`, base is literal zero. `stdbrx RS, 0, RB` writes at exact `RB`. diff --git a/tools/ppc-manual/memory/stdcx.md b/tools/ppc-manual/memory/stdcx.md index 27d01c53..fcbd5000 100644 --- a/tools/ppc-manual/memory/stdcx.md +++ b/tools/ppc-manual/memory/stdcx.md @@ -141,8 +141,8 @@ int InstrEmit_stdcx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Always sets `Rc=1` (the trailing dot).** The mnemonic is `stdcx.` — there is no non-Rc variant. CR0 is updated unconditionally to communicate success/failure. `EQ=1` means the conditional store succeeded; `EQ=0` means it failed (the prior reservation was cleared and no memory was written). -- **Reservation check.** Xenia's snapshot tests `has_reservation && reserved_addr == ea`. On match it performs `mem.write_u64`, sets `EQ=1`. On mismatch it leaves memory untouched and sets `EQ=0`. In both cases the reservation is cleared (`has_reservation = false`), so a retry must be preceded by a fresh [`ldarx`](ldarx.md). -- **Hardware granule.** PowerISA defines reservation by aligned doubleword; Xenon implementations widen this to one 128-byte cache line. A store by another agent anywhere in the line clears the reservation. Xenia's per-address check is more permissive than hardware. +- **Reservation check.** Canary's store helper fails — no write, `EQ=0` — if the thread holds no reservation. Otherwise it writes `RS` with `lock cmpxchg`, which succeeds only if memory still holds the value `ldarx` loaded; `EQ=1` only then. The reservation is released either way, so a retry must be preceded by a fresh [`ldarx`](ldarx.md). `LT` and `GT` are cleared; `SO` is left as it was instead of being copied from `XER[SO]`. +- **Hardware granule.** PowerISA defines reservation by aligned doubleword; Xenon implementations widen this to one 128-byte cache line. A store by another agent anywhere in the line clears the reservation. Canary works differently: ordinary stores never clear a reservation. `ldarx` sets a bit for the 64 KiB block holding `EA` in a bitmap shared by all threads, and the conditional store succeeds only if this thread still holds that bit and the doubleword still holds the value `ldarx` read. A write elsewhere in the line — or one that puts back the same value — goes unnoticed, while two threads reserving in the same 64 KiB block make the later store fail. - **Alignment requirement.** `EA` must be 8-byte aligned. Unaligned `stdcx.` raises an alignment exception on real hardware. - **`RA0` semantics.** When `RA = 0`, base is literal zero — `stdcx. RS, 0, RB` writes at exact `RB`. - **CR0[SO] reflects XER[SO].** Like all CR-updating ops, CR0[SO] is copied from `XER[SO]` rather than computed from this instruction. diff --git a/tools/ppc-manual/memory/stfd.md b/tools/ppc-manual/memory/stfd.md index 47134c51..6b8beb4a 100644 --- a/tools/ppc-manual/memory/stfd.md +++ b/tools/ppc-manual/memory/stfd.md @@ -241,11 +241,11 @@ int InstrEmit_stfdx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Bit-exact double store.** Writes the 64-bit IEEE binary64 contents of `FRS` directly to memory; no rounding, no format conversion. The xenia snapshot calls `mem.write_f64(ea, ctx.fpr[instr.rs()])`, which preserves the exact bit pattern (including signalling NaNs). +- **Bit-exact double store.** Writes the 64-bit IEEE binary64 contents of `FRS` directly to memory; no rounding, no format conversion. Canary reinterprets the FPR as a 64-bit integer (`Cast`) and stores it byte-swapped, which preserves the exact bit pattern (including signalling NaNs). - **No FPSCR side effects.** Like [`lfd`](lfd.md), `stfd` cannot raise IEEE exceptions: there is no rounding step. Contrast [`stfs`](stfs.md), where double→single rounding **can** raise inexact / overflow / underflow. - **`RA0` (non-update forms).** `RA = 0` in `stfd` and `stfdx` selects literal zero. Update forms `stfdu` / `stfdux` invoke `RA = 0` as an invalid form. - **Update-form post-write.** `stfdu` / `stfdux` write the computed `EA` back to `RA` after the store. No `FRS` / `RA` collision possible — `RS` is an FPR, `RA` is a GPR. -- **Big-endian write.** Byte at `EA` is the FPR's most-significant byte (sign + part of exponent), byte at `EA+7` is the least-significant mantissa byte. Xenia's `mem.write_f64` performs host-side byte-swap. +- **Big-endian write.** Byte at `EA` is the FPR's most-significant byte (sign + part of exponent), byte at `EA+7` is the least-significant mantissa byte. Canary byte-swaps before the host store. - **Alignment.** Xenon tolerates unaligned 8-byte FP stores. PowerISA permits implementations to raise alignment exceptions on cache-inhibited storage. - **MSR[FP] required.** Disabled FP unit raises Floating-Point Unavailable. diff --git a/tools/ppc-manual/memory/stfiwx.md b/tools/ppc-manual/memory/stfiwx.md index 31417892..921c7fec 100644 --- a/tools/ppc-manual/memory/stfiwx.md +++ b/tools/ppc-manual/memory/stfiwx.md @@ -116,7 +116,7 @@ int InstrEmit_stfiwx(PPCHIRBuilder& f, const InstrData& i) { - **X-form only — no D-form, no update form.** The instruction has only the indexed form. Compilers usually pair it with `addi` if a constant offset is needed. - **`RA0` semantics.** When `RA = 0`, base is literal zero; `stfiwx FS, 0, RB` writes at exact `RB`. - **No FPSCR effects.** Pure data movement — does not look at the value, does not round. -- **Big-endian word write.** The 32 bits are written most-significant-byte first into bytes `EA..EA+3`. The xenia snapshot extracts via `to_bits() as u32`, then `mem.write_u32` applies host-side byte-swap. +- **Big-endian word write.** The 32 bits are written most-significant-byte first into bytes `EA..EA+3`. Canary reinterprets the FPR as a 64-bit integer, truncates it to the low word and byte-swaps it before the host store. - **Alignment.** Xenon tolerates unaligned 4-byte writes; cache-inhibited storage may raise alignment exceptions on real hardware. - **MSR[FP] required.** Disabled FP unit raises Floating-Point Unavailable. diff --git a/tools/ppc-manual/memory/stfs.md b/tools/ppc-manual/memory/stfs.md index 070ae3ef..81b992c1 100644 --- a/tools/ppc-manual/memory/stfs.md +++ b/tools/ppc-manual/memory/stfs.md @@ -245,8 +245,8 @@ int InstrEmit_stfsx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Double → single rounding.** `FRS` always holds an IEEE binary64; `stfs` rounds to binary32 using the current `FPSCR[RN]` rounding mode before writing 4 bytes. The xenia snapshot does `ctx.fpr[instr.rs()] as f32`, which Rust defines as round-to-nearest-even; this differs from PPC if `RN` is configured otherwise. Real hardware honours `RN`. -- **FPSCR side effects.** Unlike [`lfs`](lfs.md) / [`lfd`](lfd.md) / [`stfd`](stfd.md), `stfs` **can** raise `FPSCR[XX]` (inexact), `OX` (overflow), `UX` (underflow), and `VXSNAN` (signalling NaN) per IEEE-754 narrowing rules. These take effect even though the write itself succeeds (architecturally — xenia's `as f32` cast does not surface these flags). +- **Double → single is a bit-level conversion, not a rounding.** `FRS` always holds an IEEE binary64; `stfs` does not round it under `FPSCR[RN]`. In the single-precision range it keeps `FRS[0:1]` and `FRS[5:34]` and drops the low significand bits; tiny values are denormalised by shifting — the store conversion of the PowerPC Programming Environments Manual, which Dolphin's `ConvertToSingle` implements. Canary instead converts with `vcvtsd2ss`, which rounds under the host rounding mode. The two agree whenever `FRS` already holds a single-precision value, the normal case after single-precision arithmetic. +- **FPSCR side effects.** None. Like [`lfs`](lfs.md) / [`lfd`](lfd.md) / [`stfd`](stfd.md), `stfs` does not affect the FPSCR (AIX assembler reference). Canary sets no FPSCR bits here either. - **Out-of-range doubles.** Values larger than binary32's max (~3.4e38) round to ±∞; values smaller than min normal flush to ±0 or denormal per `FPSCR[NI]`. NaNs are quieted (the signalling bit drops). - **`RA0` (non-update forms).** `RA = 0` in `stfs` and `stfsx` selects literal zero. Update forms `stfsu` / `stfsux` invoke `RA = 0` as an invalid form. - **Update-form post-write.** `stfsu` / `stfsux` write `EA` back to `RA` after the store. diff --git a/tools/ppc-manual/memory/sth.md b/tools/ppc-manual/memory/sth.md index 4f735162..5afd481a 100644 --- a/tools/ppc-manual/memory/sth.md +++ b/tools/ppc-manual/memory/sth.md @@ -249,7 +249,7 @@ int InstrEmit_sthx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Stores low 16 bits of `RS`.** Writes `(RS)[48:63]` — the low half-word — at `EA`. The xenia snapshot does `mem.write_u16(ea, ctx.gpr[instr.rs()] as u16)`. The high 48 bits of `RS` are ignored: storing a 64-bit value through `sth` silently truncates. +- **Stores low 16 bits of `RS`.** Writes `(RS)[48:63]` — the low half-word — at `EA`. Canary stores `ByteSwap(Truncate(RS, INT16))`. The high 48 bits of `RS` are ignored: storing a 64-bit value through `sth` silently truncates. - **Big-endian write.** Byte at `EA` is the high byte of the half (`RS[48:55]`), byte at `EA+1` is the low byte (`RS[56:63]`). On little-endian hosts the byte-swap happens at the memory boundary. - **`RA0` (non-update forms).** `RA = 0` in `sth` and `sthx` selects literal zero. Update forms `sthu` / `sthux` invoke `RA = 0` as an invalid form. - **Update-form post-write.** `sthu` / `sthux` write the computed `EA` back to `RA` after the store. diff --git a/tools/ppc-manual/memory/sthbrx.md b/tools/ppc-manual/memory/sthbrx.md index 0cee50ea..11f3bb39 100644 --- a/tools/ppc-manual/memory/sthbrx.md +++ b/tools/ppc-manual/memory/sthbrx.md @@ -110,7 +110,7 @@ int InstrEmit_sthbrx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Writes little-endian half.** Takes the low 16 bits of `RS`, swaps the two bytes, writes them at `EA`. After execution, byte at `EA` is `RS[56:63]` (low byte) and byte at `EA+1` is `RS[48:55]` (high byte). The xenia snapshot does `(ctx.gpr[instr.rs()] as u16).swap_bytes()`. +- **Writes little-endian half.** Takes the low 16 bits of `RS`, swaps the two bytes, writes them at `EA`. After execution, byte at `EA` is `RS[56:63]` (low byte) and byte at `EA+1` is `RS[48:55]` (high byte). Canary stores `Truncate(RS, INT16)` without the byte swap `sth` applies. - **Used to emit little-endian half-words.** Symmetric counterpart of [`lhbrx`](lhbrx.md). Common in PNG / ZIP / RIFF chunk emit paths. - **High bits of `RS` ignored.** Storing a 64-bit value through `sthbrx` truncates and reverses only the low half-word; the high 48 bits are not consulted. - **X-form only — no D-form, no update form.** Only the indexed form exists. `EA = (RA|0) + RB`. diff --git a/tools/ppc-manual/memory/stmw.md b/tools/ppc-manual/memory/stmw.md index a86f8fbd..90a83ba7 100644 --- a/tools/ppc-manual/memory/stmw.md +++ b/tools/ppc-manual/memory/stmw.md @@ -110,10 +110,10 @@ int InstrEmit_stmw(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Bulk register save.** Stores `(32 - RS)` consecutive 32-bit words taken from `r[RS]`, `r[RS+1]`, …, `r31` to memory starting at `EA`. The symmetric counterpart of [`lmw`](lmw.md). Used by AIX/PowerPC ABI prologues to save non-volatile GPRs in one instruction. -- **Each store is the low 32 bits of the GPR.** Xenia's snapshot writes `ctx.gpr[r] as u32` — only the low half of the 64-bit GPR. The high 32 bits are discarded; `stmw` cannot save 64-bit values (use a sequence of [`std`](std.md) instead). +- **Each store is the low 32 bits of the GPR.** Canary stores `ByteSwap(Truncate(GPR, INT32))` for each register — only the low half of the 64-bit GPR. The high 32 bits are discarded; `stmw` cannot save 64-bit values (use a sequence of [`std`](std.md) instead). - **Big-endian write.** Word from `r[RS]` lands at `EA`, word from `r[RS+1]` at `EA+4`, etc. Each word is itself written most-significant-byte first. - **`RA0` semantics.** When `RA = 0`, base is the literal zero. Useful for absolute-address restoration. -- **Alignment.** PowerISA requires word-aligned `EA`; an unaligned `stmw` may raise an alignment exception on hardware. Xenia tolerates it. +- **Alignment.** PowerISA requires word-aligned `EA`; an unaligned `stmw` may raise an alignment exception on hardware. Canary does not check. - **Performance trap.** Modern PowerPC implementations microcode `stmw` — typically slower than the same number of `stw` instructions. Compilers prefer the unrolled form. - **Cache-line behaviour.** When the run of words crosses several 128-byte cache lines, each cold line triggers a read-allocate. Pre-clearing with [`dcbz128`](dcbz.md) helps for fresh frames. diff --git a/tools/ppc-manual/memory/stswi.md b/tools/ppc-manual/memory/stswi.md index 4326e64c..169660b8 100644 --- a/tools/ppc-manual/memory/stswi.md +++ b/tools/ppc-manual/memory/stswi.md @@ -101,7 +101,7 @@ int InstrEmit_stswi(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Byte-granular bulk store.** Symmetric counterpart of [`lswi`](lswi.md). Reads the low 32 bits of `RS`, `RS+1`, …, takes the top byte of each (then the next, etc.) and writes successive bytes at `EA`. The byte count `NB` is in the `RB` field of the encoding (1..31), with `NB = 0` meaning "32 bytes". -- **Register wraparound at r31 → r0.** Xenia's snapshot increments `rs = (rs + 1) % 32`. After r31 the source becomes r0, then r1, etc. Rare in practice; AIX flags overlapping register / address ranges as invalid. +- **Register wraparound at r31 → r0.** After r31 the source becomes r0, then r1, etc. Rare in practice; AIX flags overlapping register / address ranges as invalid. ⚠️ Canary does not implement `stswi`: its emitter is `XEINSTRNOTIMPLEMENTED`, so translating one logs "Unimplemented instr" and, with the default `break_on_unimplemented_instructions`, breaks. - **Big-endian byte ordering inside each register.** Writes the most-significant byte first: `mem.write_u8(ea, (val >> 24) as u8)`, then bits 16–23, etc. Matches the byte order produced by [`lswi`](lswi.md), so a `lswi`/`stswi` pair round-trips a buffer. - **Last partial register.** When `NB` is not a multiple of 4, the final source register has its trailing low bytes ignored — only the leading bytes that fit in the byte budget are written. - **`RA0` semantics.** `RA = 0` selects literal zero. `stswi` is not an update form; `RA` is not modified. diff --git a/tools/ppc-manual/memory/stswx.md b/tools/ppc-manual/memory/stswx.md index 4d94ba7f..08f9cf86 100644 --- a/tools/ppc-manual/memory/stswx.md +++ b/tools/ppc-manual/memory/stswx.md @@ -100,10 +100,10 @@ int InstrEmit_stswx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Byte count from `XER[25..31]`.** Unlike `stswi`, the byte count `NB` (0..127) is read from `XER[25..31]`. The xenia snapshot does `let nb = (ctx.xer() & 0x7F) as u32;`. `NB = 0` means literally zero bytes — the instruction becomes a no-op. +- **Byte count from `XER[25..31]`.** Unlike `stswi`, the byte count `NB` (0..127) is read from `XER[25..31]`. `NB = 0` means literally zero bytes — the instruction becomes a no-op. ⚠️ Canary does not implement `stswx`: its emitter is `XEINSTRNOTIMPLEMENTED`, so translating one logs "Unimplemented instr" and, with the default `break_on_unimplemented_instructions`, breaks. - **Register packing identical to `stswi`.** Bytes are pulled from successive GPRs, four bytes per register, big-endian within each register, with wraparound `r31 → r0`. The final partial register's unused trailing bytes are not written. - **`RA0` semantics.** `RA = 0` selects literal zero. The instruction has no update form — `RA` is not modified. -- **Invalid forms.** AIX flags as invalid the cases where the byte-stream wraps through `RA` or `RB` while reading the source registers; xenia performs writes regardless. +- **Invalid forms.** AIX flags as invalid the cases where the byte-stream wraps through `RA` or `RB` while reading the source registers. Canary does not implement `stswx` (`XEINSTRNOTIMPLEMENTED`). - **Big-endian byte ordering inside each register.** Writes most-significant byte of each source GPR's low word first. - **Used for non-multiple-of-4 copies.** Together with `lswx`, gives a way to store a runtime-determined byte count without per-byte loops. Compilers don't emit it. - **Alignment.** Architecture allows arbitrary alignment; cache-inhibited storage may raise alignment exceptions on hardware. diff --git a/tools/ppc-manual/memory/stvebx.md b/tools/ppc-manual/memory/stvebx.md index 2a353526..90dfa6ae 100644 --- a/tools/ppc-manual/memory/stvebx.md +++ b/tools/ppc-manual/memory/stvebx.md @@ -107,7 +107,7 @@ int InstrEmit_stvebx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Single-byte element store.** Architecturally `stvebx` writes exactly **one** byte from lane `EA mod 16` of `VS` to address `EA`. Other lanes are unaffected, and other memory bytes are unaffected. -- **Xenia simplification — full 16-byte write.** The xenia snapshot is shared with `stvehx` / `stvewx` and writes the **entire 16-byte aligned line** (`ea & ~0xF`, then 16 bytes from the vector). This is stronger than the architectural single-byte store — it overwrites 15 adjacent bytes with whatever the source vector holds. Code that depends on architectural per-byte granularity (e.g. interleaved writes from multiple threads / DMA agents into the same line) may behave differently than on hardware. +- **Single-byte write in Canary.** Canary stores just the one element: the byte `VS.b[EA & 0xF]` at `EA`, leaving the rest of the line alone — the architectural granularity. - **`RA0` semantics.** `RA = 0` selects literal zero. - **No update form, no VMX128 sibling.** No `stvebux`; no `stvebx128` — single-byte stores were kept Altivec-only in the Xbox 360 extension. - **Big-endian within the line.** Lane 0 of `VS` corresponds to the byte at the aligned base address. diff --git a/tools/ppc-manual/memory/stvehx.md b/tools/ppc-manual/memory/stvehx.md index eccc72cc..dcf22c7a 100644 --- a/tools/ppc-manual/memory/stvehx.md +++ b/tools/ppc-manual/memory/stvehx.md @@ -109,8 +109,8 @@ int InstrEmit_stvehx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Single half-word element store.** Architecturally `stvehx` writes exactly **two** bytes from half-word lane `(EA mod 16) >> 1` of `VS` to address `EA & ~1` (low bit forced to half-aligned). Other lanes are unaffected, and bytes outside the 2-byte window are unaffected. -- **Xenia simplification — full 16-byte write.** The xenia snapshot is shared with `stvebx` / `stvewx`: writes 16 bytes of the source vector at `ea & ~0xF`. This is stronger than the architectural 2-byte store — it overwrites 14 adjacent bytes that hardware would have left alone. -- **EA forced half-aligned.** Hardware drops the low bit; xenia's shared snapshot drops the low four bits. +- **Two-byte write in Canary.** Canary rounds `EA` down to even and stores just element `(EA & 0xF) >> 1` (byte-swapped) — the architectural 2-byte store, leaving the rest of the line alone. +- **EA forced half-aligned.** Hardware drops the low bit; so does Canary (`EA & ~1`), which stores element `(EA & 0xF) >> 1`. - **`RA0` semantics.** `RA = 0` selects literal zero. - **No update form, no VMX128 sibling.** No `stvehux`; no `stvehx128`. - **Big-endian half within the lane.** The byte at the lower address is the most-significant byte of the half-word lane. diff --git a/tools/ppc-manual/memory/stvewx.md b/tools/ppc-manual/memory/stvewx.md index 7f635e3e..8300ce40 100644 --- a/tools/ppc-manual/memory/stvewx.md +++ b/tools/ppc-manual/memory/stvewx.md @@ -167,8 +167,8 @@ int InstrEmit_stvewx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd, ## Special Cases & Edge Conditions - **Single word element store.** Architecturally `stvewx` writes exactly **four** bytes from word lane `(EA mod 16) >> 2` of `VS` to address `EA & ~3` (low two bits forced to word-aligned). Other lanes are unaffected, and bytes outside the 4-byte window are unaffected. -- **Xenia simplification — full 16-byte write.** Both `stvewx` and `stvewx128` snapshots write the full 16 bytes of the source vector at `ea & ~0xF`. This overwrites 12 bytes that hardware would have left alone. -- **EA forced word-aligned.** Hardware drops the low two bits; xenia's snapshots drop the low four. +- **Four-byte write in Canary.** Both `stvewx` and `stvewx128` round `EA` down to a multiple of 4 and store just element `(EA & 0xF) >> 2` (byte-swapped), leaving the other 12 bytes alone. +- **EA forced word-aligned.** Hardware drops the low two bits; so does Canary (`EA & ~3`) for both `stvewx` and `stvewx128`. - **`RA0` semantics.** `RA = 0` selects literal zero. - **No update form.** No `stvewux`. - **VMX128 sibling (`stvewx128`).** Identical semantics; alternative operand encoding addressing `v0..v127` via the split-field 7-bit register index. diff --git a/tools/ppc-manual/memory/stvlx.md b/tools/ppc-manual/memory/stvlx.md index 10065c75..4d80c675 100644 --- a/tools/ppc-manual/memory/stvlx.md +++ b/tools/ppc-manual/memory/stvlx.md @@ -173,7 +173,7 @@ int InstrEmit_stvlx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd, - **No alignment masking.** Unlike `stvx`, the `EA` is **not** rounded down. `EA mod 16` controls how the source vector splits. - **`RA0` semantics.** `RA = 0` selects literal zero. - **Microsoft Xbox 360 specific.** Part of the VMX128 / Cell BE extended set, not in baseline Altivec. -- **Implementation in xenia.** The shared snapshot calls `vmx::store_vector_left(mem, ea, vs)`, performing the unaligned partial-byte write. +- **Implementation in Canary.** Canary emits its `StoreVectorLeft` op, performing the unaligned partial-byte write. - **VMX128 sibling (`stvlx128`).** Identical semantics; alternative operand encoding addressing `v0..v127`. - **`stvlxl` is the LRU-hint variant.** Same data behaviour, hint ignored under emulation. diff --git a/tools/ppc-manual/memory/stvlxl.md b/tools/ppc-manual/memory/stvlxl.md index 0fc35dba..53ffa865 100644 --- a/tools/ppc-manual/memory/stvlxl.md +++ b/tools/ppc-manual/memory/stvlxl.md @@ -179,7 +179,7 @@ int InstrEmit_stvlx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd, ## Special Cases & Edge Conditions - **Same data effect as [`stvlx`](stvlx.md), with LRU cache hint.** Writes `(16 - (EA mod 16))` bytes from the left half of `VS` starting at `EA`; right half not stored. The `l` suffix marks the touched line as least-recently-used. -- **Hint ignored under emulation.** Xenia's snapshot is shared with `stvlx` (`PpcOpcode::stvlx | PpcOpcode::stvlxl => …`). +- **Hint ignored under emulation.** Canary's `stvlxl` simply calls its `stvlx` emitter. - **No alignment masking.** The exact `EA` controls how data is split. - **`RA0` semantics.** `RA = 0` selects literal zero. - **Microsoft Xbox 360 specific.** Part of VMX128 / Cell BE. diff --git a/tools/ppc-manual/memory/stvrx.md b/tools/ppc-manual/memory/stvrx.md index d65eb4f1..147f4562 100644 --- a/tools/ppc-manual/memory/stvrx.md +++ b/tools/ppc-manual/memory/stvrx.md @@ -175,7 +175,7 @@ int InstrEmit_stvrx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd, - **No alignment masking.** Unlike `stvx`, the exact `EA` is used; `EA mod 16` controls how `VS` splits. - **`RA0` semantics.** `RA = 0` selects literal zero. - **Microsoft Xbox 360 specific.** Part of the VMX128 / Cell BE extended set. -- **Implementation in xenia.** The shared snapshot calls `vmx::store_vector_right(mem, ea, vs)`, performing the unaligned partial-byte write of the right side. +- **Implementation in Canary.** Canary emits its `StoreVectorRight` op, performing the unaligned partial-byte write of the right side. - **VMX128 sibling (`stvrx128`).** Identical semantics; alternative operand encoding addressing `v0..v127`. - **`stvrxl` is the LRU-hint variant.** diff --git a/tools/ppc-manual/memory/stvrxl.md b/tools/ppc-manual/memory/stvrxl.md index 43276fcc..5647fa2b 100644 --- a/tools/ppc-manual/memory/stvrxl.md +++ b/tools/ppc-manual/memory/stvrxl.md @@ -181,7 +181,7 @@ int InstrEmit_stvrx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd, ## Special Cases & Edge Conditions - **Same data effect as [`stvrx`](stvrx.md), with LRU cache hint.** Writes `(EA mod 16)` bytes from the right half of `VS` to the addresses just below `EA & ~0xF`. The `l` suffix marks the touched line as least-recently-used. -- **Hint ignored under emulation.** Xenia's snapshot is shared with `stvrx` (`PpcOpcode::stvrx | PpcOpcode::stvrxl => …`). +- **Hint ignored under emulation.** Canary's `stvrxl` simply calls its `stvrx` emitter. - **No alignment masking.** Exact `EA` used. - **`RA0` semantics.** `RA = 0` selects literal zero. - **Microsoft Xbox 360 specific.** Part of VMX128 / Cell BE. diff --git a/tools/ppc-manual/memory/stvx.md b/tools/ppc-manual/memory/stvx.md index 16951df5..5744f0e5 100644 --- a/tools/ppc-manual/memory/stvx.md +++ b/tools/ppc-manual/memory/stvx.md @@ -155,7 +155,7 @@ MEM(EA, 16) <- byte_order_adjusted(VS) ; lane 0 at EA, lane 15 at EA+15 ## Special Cases & Edge Conditions - **Alignment is forced, not checked.** The low four bits of the effective address are **cleared** before the store — alignment violations silently corrupt adjacent data rather than trap. This differs from scalar `stw` (no alignment enforcement) and from `stvewx` (which stores only one element and keeps the exact EA). -- **Big-endian lane layout.** Vector lane 0 (the most-significant bytes of the 128-bit register) lives at the lowest address; lane 15 at `EA + 15`. On little-endian hosts the whole 16-byte block is byte-swapped at the memory boundary so the PowerPC-visible layout is preserved. Xenia's helper `mem_write_vec128_be` handles this. +- **Big-endian lane layout.** Vector lane 0 (the most-significant bytes of the 128-bit register) lives at the lowest address; lane 15 at `EA + 15`. On little-endian hosts the whole 16-byte block is byte-swapped at the memory boundary so the PowerPC-visible layout is preserved. Canary rounds `EA` down to a 16-byte boundary and stores `ByteSwap(VR)`. - **`RA0` semantics.** When `RA = 0` the base is the literal zero — just like scalar loads/stores. Combined with the alignment mask this lets `stvx VS, 0, RB` store to address `RB & ~0xF`. - **No update form.** Unlike scalar stores, VMX stores have no `u` variant that post-writes the base. Use [`stvxl`](stvxl.md) for the cache-hint variant (suggests "last" — the line is not expected to be reused soon). - **VMX128 sibling (`stvx128`).** Identical semantics; the only difference is the operand encoding. VMX128 uses a 7-bit register index split across three non-contiguous bit fields (`VS128l ‖ VS128h`) so it can address `v0..v127` instead of the 32-register Altivec space. All alignment, byte-order and `RA0` rules are the same. @@ -172,4 +172,4 @@ MEM(EA, 16) <- byte_order_adjusted(VS) ; lane 0 at EA, lane 15 at EA+15 ## IBM Reference - [AIX 7.3 — `stvx` (Store Vector Indexed)](https://www.ibm.com/docs/en/aix/7.3.0?topic=set-stvx-store-vector-indexed-instruction) -- PowerISA Book II (Altivec / VMX). Xbox 360 VMX128 is Microsoft-documented in the XDK; xenia's `ppc-instructions.xml` captures the deltas. +- PowerISA Book I, Vector facility (VMX / AltiVec). Xbox 360 VMX128 is Microsoft-documented in the XDK; Canary's `tools/ppc-instructions.xml` captures the deltas. diff --git a/tools/ppc-manual/memory/stvxl.md b/tools/ppc-manual/memory/stvxl.md index aa7376e7..a560a3dd 100644 --- a/tools/ppc-manual/memory/stvxl.md +++ b/tools/ppc-manual/memory/stvxl.md @@ -169,7 +169,7 @@ int InstrEmit_stvx_(PPCHIRBuilder& f, const InstrData& i, uint32_t vd, ## Special Cases & Edge Conditions - **Same data effect as [`stvx`](stvx.md), with LRU cache hint.** Writes 16 bytes from `VS` at `EA & ~0xF`. The `l` suffix tells the cache the line is least-recently-used — useful for streaming output (e.g. one-pass writes to a render target the producer will not re-read). -- **Hint ignored under emulation.** Xenia's snapshot is shared with the VMX128 variant; it implements only the data side. Hardware uses the hint to choose write-allocate vs. write-streaming behaviour. +- **Hint ignored under emulation.** Canary's `stvxl` simply calls its `stvx` emitter (and `stvxl128` calls `stvx128`); it implements only the data side. Hardware uses the hint to choose write-allocate vs. write-streaming behaviour. - **Alignment is forced, not checked.** Low four bits of `EA` are masked. - **Big-endian lane layout.** Lane 0 of `VS` lands at the aligned base; lane 15 at base+15. - **`RA0` semantics.** `RA = 0` selects literal zero. diff --git a/tools/ppc-manual/memory/stw.md b/tools/ppc-manual/memory/stw.md index 42cf5f48..8c7e9825 100644 --- a/tools/ppc-manual/memory/stw.md +++ b/tools/ppc-manual/memory/stw.md @@ -242,7 +242,7 @@ int InstrEmit_stwx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Stores low 32 bits of `RS`.** Writes `(RS)[32:63]` — the low word of the 64-bit GPR — at `EA`. The xenia snapshot does `mem.write_u32(ea, ctx.gpr[instr.rs()] as u32)`. The high 32 bits are silently truncated; use [`std`](std.md) to store all 64 bits. +- **Stores low 32 bits of `RS`.** Writes `(RS)[32:63]` — the low word of the 64-bit GPR — at `EA`. Canary stores `ByteSwap(Truncate(RS, INT32))`. The high 32 bits are silently truncated; use [`std`](std.md) to store all 64 bits. - **Big-endian write.** `RS[32:39]` (the most-significant byte of the low word) lands at `EA`; `RS[56:63]` at `EA+3`. On little-endian hosts the byte-swap happens at the memory boundary. - **`RA0` (non-update forms).** `RA = 0` in `stw` and `stwx` selects literal zero. Update forms `stwu` / `stwux` invoke `RA = 0` as an invalid form. **The classic frame-allocation idiom** `stwu r1, -framesize(r1)` exploits the update form: it writes the old SP at the new SP and updates `r1` in one instruction. - **Update-form post-write.** `stwu` / `stwux` write `EA` to `RA` after the store. Order is store-then-update, so the new `RA` value reflects the post-update address (typically the new stack-frame base). diff --git a/tools/ppc-manual/memory/stwbrx.md b/tools/ppc-manual/memory/stwbrx.md index 9b995c8c..4ab78025 100644 --- a/tools/ppc-manual/memory/stwbrx.md +++ b/tools/ppc-manual/memory/stwbrx.md @@ -110,7 +110,7 @@ int InstrEmit_stwbrx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Writes little-endian word.** Takes the low 32 bits of `RS`, reverses the four bytes, writes them at `EA`. Byte at `EA` is `RS[56:63]` (low byte); byte at `EA+3` is `RS[32:39]` (high byte). The xenia snapshot does `(ctx.gpr[instr.rs()] as u32).swap_bytes()`. +- **Writes little-endian word.** Takes the low 32 bits of `RS`, reverses the four bytes, writes them at `EA`. Byte at `EA` is `RS[56:63]` (low byte); byte at `EA+3` is `RS[32:39]` (high byte). Canary stores `Truncate(RS, INT32)` without the byte swap `stw` applies. - **Used to emit little-endian payloads.** Symmetric counterpart of [`lwbrx`](lwbrx.md). Common when writing PC-side file formats, network packets, GPU command buffers in little-endian layout, etc. - **High bits of `RS` ignored.** Stores only the low 32 bits; the upper half of the 64-bit GPR is not consulted. - **X-form only — no D-form, no update form.** Only the indexed form exists. `EA = (RA|0) + RB`. diff --git a/tools/ppc-manual/memory/stwcx.md b/tools/ppc-manual/memory/stwcx.md index 9e1f0a5a..63eb6c18 100644 --- a/tools/ppc-manual/memory/stwcx.md +++ b/tools/ppc-manual/memory/stwcx.md @@ -143,9 +143,9 @@ int InstrEmit_stwcx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Always sets `Rc=1` (the trailing dot).** The mnemonic is `stwcx.` — there is no non-Rc variant. CR0 is updated unconditionally to communicate success/failure. `EQ=1` means the conditional store succeeded; `EQ=0` means it failed (the prior reservation was lost; no memory write). -- **Reservation check.** Xenia's snapshot tests `has_reservation && reserved_addr == ea`. On match it performs `mem.write_u32` (low 32 bits of `RS`, big-endian), sets `EQ=1`. On mismatch, no memory write and `EQ=0`. In both cases the reservation is cleared, so a retry must begin with a fresh [`lwarx`](lwarx.md). -- **Hardware granule.** PowerISA defines reservation by aligned word; Xenon implementations widen this to one 128-byte cache line. A store by another agent anywhere in the line clears the reservation. Xenia's per-address check is more permissive than hardware. -- **Alignment requirement.** `EA` must be 4-byte aligned. Unaligned `stwcx.` raises an alignment exception on real hardware; xenia does not check. +- **Reservation check.** Canary's store helper fails — no write, `EQ=0` — if the thread holds no reservation. Otherwise it writes the low 32 bits of `RS` (big-endian) with `lock cmpxchg`, which succeeds only if memory still holds the value `lwarx` loaded; `EQ=1` only then. The reservation is released either way, so a retry must be preceded by a fresh [`lwarx`](lwarx.md). `LT` and `GT` are cleared; `SO` is left as it was instead of being copied from `XER[SO]`. +- **Hardware granule.** PowerISA defines reservation by aligned word; Xenon implementations widen this to one 128-byte cache line. A store by another agent anywhere in the line clears the reservation. Canary works differently: ordinary stores never clear a reservation. `lwarx` sets a bit for the 64 KiB block holding `EA` in a bitmap shared by all threads, and the conditional store succeeds only if this thread still holds that bit and the word still holds the value `lwarx` read. A write elsewhere in the line — or one that puts back the same value — goes unnoticed, while two threads reserving in the same 64 KiB block make the later store fail. +- **Alignment requirement.** `EA` must be 4-byte aligned. Unaligned `stwcx.` raises an alignment exception on real hardware; Canary does not check. - **`RA0` semantics.** When `RA = 0`, base is literal zero — `stwcx. RS, 0, RB` writes at exact `RB`. - **CR0[SO] reflects XER[SO].** Like all CR-updating ops, CR0[SO] is copied from `XER[SO]` rather than computed. - **Spurious failures permitted.** Hardware may report failure even when no actual conflict occurred (e.g. on context switch). Application code treats failure as a normal retry condition. diff --git a/tools/ppc-manual/vmx/vaddfp.md b/tools/ppc-manual/vmx/vaddfp.md index 82f399b7..2bc60b32 100644 --- a/tools/ppc-manual/vmx/vaddfp.md +++ b/tools/ppc-manual/vmx/vaddfp.md @@ -158,12 +158,12 @@ for i in 0..3: ## Special Cases & Edge Conditions -- **Lane indexing is big-endian.** Lane 0 is the **most significant** 4 bytes of the 128-bit register (the one that appears at the lowest byte offset after a `stvx`). Xenia's `Vec128::as_f32x4()` already reads lanes in PPC order on x86-64. When writing C that manipulates individual lanes, index `v.f[0]` as "the byte 0..3" of the big-endian layout. -- **Flush-denormals ("NJ") mode.** Altivec is independent of FPSCR — it has its own 2-bit VSCR (`NJ` for non-Java mode + `SAT` sticky-saturation). VMX float operations honour `VSCR[NJ]`: when set (the Xenon boot default), denormal inputs and outputs are flushed to zero. This is **opposite** to the scalar FPU, which has its own non-IEEE bit. Xenia sets `NJ = 1` at context creation ([`context.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/context.rs)). +- **Lane indexing is big-endian.** Lane 0 is the **most significant** 4 bytes of the 128-bit register (the one that appears at the lowest byte offset after a `stvx`). Canary keeps lanes in PPC order: `lvx`/`stvx` byte-swap each 32-bit lane in place (`vpshufb` with `XMMByteSwapMask`), so host element `i` is PPC lane `i`. When writing C that manipulates individual lanes, index `v.f[0]` as "the byte 0..3" of the big-endian layout. +- **Flush-denormals ("NJ") mode.** Altivec is independent of FPSCR — it has its own 2-bit VSCR (`NJ` for non-Java mode + `SAT` sticky-saturation). VMX float operations honour `VSCR[NJ]`: when set, denormal inputs and outputs are flushed to zero. Canary starts every thread with `NJ` set (`vscr_vec` low word `0x00010000`) and its VMX MXCSR in flush-to-zero + denormals-are-zero mode, while its scalar FPU starts in IEEE mode; `mtvscr` switches the VMX side off when the guest clears `NJ`. Whether Xenon boots the same way is unverified. - **No exception, no trap.** Altivec floats never raise exceptions. NaN inputs produce NaN outputs; `±∞ − ±∞` yields a NaN; there is no VXISI-style status bit. `VSCR[SAT]` is **not** touched by `vaddfp` (it saturates integer ops, not floats). - **Four independent lanes.** Each lane's operation is unaffected by the others. Aliasing between `VA`, `VB`, and `VD` is legal and common (`vaddfp v3, v3, v4`). - **VMX128 sibling (`vaddfp128`).** Semantics identical; only the register encoding differs. VMX128 uses a 7-bit operand ID per source (and destination) built from two or three non-contiguous bit fields — see [`categories/vmx128.md`](../categories/vmx128.md). Any bit pattern encodable as a 32-register VX-form is also encodable as a VMX128 form, so compilers picked the more compact form that reached the needed register range. -- **On x86-64 hosts.** A natural compilation uses `_mm_add_ps` or AVX `vaddps`. These preserve lane indexing because PPC lane 0 maps to x86 lane 3 only if you treat the 128-bit value as "big-endian in memory" — i.e. byte-swap on load/store. With xenia's `_be` memory helpers, `_mm_add_ps` gives the right per-lane result. +- **On x86-64 hosts.** Canary emits `vaddps`. Because its `lvx`/`stvx` swap each 32-bit lane in place rather than reversing all 16 bytes, PPC lane `i` is host lane `i`, and `vaddps` gives the right per-lane result. ## Related Instructions @@ -173,7 +173,7 @@ for i in 0..3: - [`vmaxfp`](vmaxfp.md), [`vminfp`](vminfp.md) — IEEE-754-aware max/min (NaN propagation). - [`vcmpeqfp`](vcmpeqfp.md), [`vcmpgtfp`](vcmpgtfp.md), [`vcmpgefp`](vcmpgefp.md), [`vcmpbfp`](vcmpbfp.md) — compares producing per-lane all-ones / all-zero masks. - [`vrfin`](vrfin.md), [`vrfim`](vrfim.md), [`vrfip`](vrfip.md), [`vrfiz`](vrfiz.md) — round to integer (to-nearest / down / up / toward-zero). -- [`vmulfp`](vmulfp.md) — xenia's helper; not a native Altivec op, included for convenience. Hardware games use `vmaddfp v, va, vc, v0_zero` instead. +- [`vmulfp128`](../vmx128/vmulfp128.md) — the VMX128-only lane-wise multiply (Canary emits a plain `Mul`); standard Altivec has no `vmulfp`, and code there uses `vmaddfp v, va, vc, v0_zero` instead. ## IBM Reference diff --git a/tools/ppc-manual/vmx/vaddsbs.md b/tools/ppc-manual/vmx/vaddsbs.md index 26f38a5c..b0288537 100644 --- a/tools/ppc-manual/vmx/vaddsbs.md +++ b/tools/ppc-manual/vmx/vaddsbs.md @@ -107,7 +107,7 @@ int InstrEmit_vaddsbs(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Sixteen signed-byte lanes, saturating.** Each `VD[i] = clamp(VA[i] + VB[i], -128, +127)` for `i = 0..15`, with both inputs interpreted as signed `int8`. Lane 0 is the most-significant byte (the byte at the lowest address after `stvx`). -- **`VSCR[SAT]` is sticky-set** when *any* lane saturates — either positively (overflow above `+127`) or negatively (underflow below `-128`). The SAT bit is never cleared by this op; software must use [`mtvscr`](mtvscr.md) to clear it. Xenia routes the OR of per-lane saturation flags into `ctx.set_vscr_sat(true)` exactly when at least one lane clamped (see `crate::vmx::sat_add_i8` in [`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)). +- **`VSCR[SAT]` is sticky-set** when *any* lane saturates — either positively (overflow above `+127`) or negatively (underflow below `-128`). The SAT bit is never cleared by this op; software must use [`mtvscr`](../control/mtvscr.md) to clear it. ⚠️ Canary never records `SAT`: its `StoreSAT(DidSaturate(v))` writes a private `vscr_sat` byte, the x64 `DID_SATURATE` is a stub that always yields 0, and [`mfvscr`](../control/mfvscr.md) reads `vscr_vec`, which only `mtvscr` changes. - **Compare with the modulo sibling.** [`vaddubm`](vaddubm.md) is bit-pattern-identical to a hypothetical `vaddsbm` and silently wraps without touching `VSCR[SAT]`. Use `vaddsbs` whenever clipping is desired and you need the sticky overflow flag. - **Asymmetric clamp.** `+127 + 1 = +127`; `-128 + (-1) = -128`. Tests that look for "any saturation" should mask both saturation directions. - **No XER side effects.** Altivec never updates `XER[CA]` / `XER[OV]`. The only status bit affected is `VSCR[SAT]`. diff --git a/tools/ppc-manual/vmx/vaddshs.md b/tools/ppc-manual/vmx/vaddshs.md index 57c6f80c..59a7f0e4 100644 --- a/tools/ppc-manual/vmx/vaddshs.md +++ b/tools/ppc-manual/vmx/vaddshs.md @@ -107,7 +107,7 @@ int InstrEmit_vaddshs(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Eight signed-half lanes, saturating.** Each `VD[i] = clamp(VA[i] + VB[i], -32768, +32767)` for `i = 0..7`, with both inputs interpreted as signed `int16`. Lane 0 (`VD[0..1]` after `stvx`) is the most-significant half. -- **`VSCR[SAT]` is sticky-set** if *any* lane clamps. Once set, it stays set until explicit clear via [`mtvscr`](mtvscr.md). Xenia uses `crate::vmx::sat_add_i16` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)) which returns the per-lane saturation flag; the OR is written back via `ctx.set_vscr_sat(true)`. +- **`VSCR[SAT]` is sticky-set** if *any* lane clamps. Once set, it stays set until explicit clear via [`mtvscr`](../control/mtvscr.md). ⚠️ Canary never records `SAT`: its `StoreSAT(DidSaturate(v))` writes a private `vscr_sat` byte, the x64 `DID_SATURATE` is a stub that always yields 0, and [`mfvscr`](../control/mfvscr.md) reads `vscr_vec`, which only `mtvscr` changes. - **The modulo counterpart is `vadduhm`.** Modulo add for signed and unsigned halves is bit-identical, so [`vadduhm`](vadduhm.md) covers both when wraparound is wanted; switch to `vaddshs` only when clipping with sign awareness is desired. - **Asymmetric clamp.** `+32767 + 1 = +32767`; `-32768 + (-1) = -32768`. - **Common 16-bit DSP idiom.** Audio mixing and fixed-point colour blending lean heavily on `vaddshs` to combine signed Q15 / Q1.15 quantities without wraparound artefacts. diff --git a/tools/ppc-manual/vmx/vaddsws.md b/tools/ppc-manual/vmx/vaddsws.md index e692c9bc..2a18ea7c 100644 --- a/tools/ppc-manual/vmx/vaddsws.md +++ b/tools/ppc-manual/vmx/vaddsws.md @@ -107,7 +107,7 @@ int InstrEmit_vaddsws(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Four signed-word lanes, saturating.** Each `VD[i] = clamp(VA[i] + VB[i], INT32_MIN, INT32_MAX)` for `i = 0..3`. Lane 0 (`VD[0..3]` after `stvx`) is the most-significant word. -- **`VSCR[SAT]` is sticky-set** if any lane clamps. Xenia tracks this through `crate::vmx::sat_add_i32` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)) and ORs the flag into the architectural `VSCR[SAT]`. +- **`VSCR[SAT]` is sticky-set** if any lane clamps. ⚠️ Canary never records `SAT`: its `StoreSAT(DidSaturate(v))` writes a private `vscr_sat` byte, the x64 `DID_SATURATE` is a stub that always yields 0, and [`mfvscr`](../control/mfvscr.md) reads `vscr_vec`, which only `mtvscr` changes. - **No multi-precision carry.** Unlike [`vaddcuw`](vaddcuw.md), `vaddsws` does not expose a per-lane carry/borrow — a saturated lane simply clips; it does not overflow into the adjacent lane. - **Asymmetric clamp.** `INT32_MAX + 1 = INT32_MAX`; `INT32_MIN + (-1) = INT32_MIN`. - **The modulo sibling is `vadduwm`.** Modulo add for signed and unsigned words is bit-identical; switch to `vaddsws` only when clipping with sign awareness is desired. diff --git a/tools/ppc-manual/vmx/vaddubs.md b/tools/ppc-manual/vmx/vaddubs.md index 40ce79e6..89c48495 100644 --- a/tools/ppc-manual/vmx/vaddubs.md +++ b/tools/ppc-manual/vmx/vaddubs.md @@ -107,7 +107,7 @@ int InstrEmit_vaddubs(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Sixteen unsigned-byte lanes, saturating.** Each `VD[i] = min(VA[i] + VB[i], 0xFF)` for `i = 0..15`. Lane 0 is the most-significant byte after `stvx`. -- **`VSCR[SAT]` is sticky-set** if any lane saturates. Once set, it stays set until [`mtvscr`](mtvscr.md) clears it. Xenia computes this with `crate::vmx::sat_add_u8` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)). +- **`VSCR[SAT]` is sticky-set** if any lane saturates. Once set, it stays set until [`mtvscr`](../control/mtvscr.md) clears it. ⚠️ Canary never records `SAT`: its `StoreSAT(DidSaturate(v))` writes a private `vscr_sat` byte, the x64 `DID_SATURATE` is a stub that always yields 0, and [`mfvscr`](../control/mfvscr.md) reads `vscr_vec`, which only `mtvscr` changes. - **One-sided clamp.** Only the upper bound applies (unsigned add cannot underflow). Distinct from [`vaddsbs`](vaddsbs.md), which clips at both `+127` and `-128`. - **Pixel-blend workhorse.** Common usage is to add two unsigned-byte colour vectors with clamp-to-white at `0xFF`. Saturation behaves the same way as `_mm_adds_epu8` on x86 SSE2 — making it a one-to-one host translation candidate. - **Versus modulo.** [`vaddubm`](vaddubm.md) wraps silently and never touches `VSCR[SAT]`. Use `vaddubs` when overflow indicates "too bright" / "out of range" and you want to flag it sticky. diff --git a/tools/ppc-manual/vmx/vadduhs.md b/tools/ppc-manual/vmx/vadduhs.md index 2f76297f..d84b05be 100644 --- a/tools/ppc-manual/vmx/vadduhs.md +++ b/tools/ppc-manual/vmx/vadduhs.md @@ -107,11 +107,11 @@ int InstrEmit_vadduhs(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Eight unsigned-half lanes, saturating.** Each `VD[i] = min(VA[i] + VB[i], 0xFFFF)` for `i = 0..7`. Lane 0 (`VD[0..1]` after `stvx`) is the most-significant half. -- **`VSCR[SAT]` is sticky-set** if any lane clamps. Cleared only by [`mtvscr`](mtvscr.md). Xenia uses `crate::vmx::sat_add_u16` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)) and ORs the per-lane flag. +- **`VSCR[SAT]` is sticky-set** if any lane clamps. Cleared only by [`mtvscr`](../control/mtvscr.md). ⚠️ Canary never records `SAT`: its `StoreSAT(DidSaturate(v))` writes a private `vscr_sat` byte, the x64 `DID_SATURATE` is a stub that always yields 0, and [`mfvscr`](../control/mfvscr.md) reads `vscr_vec`, which only `mtvscr` changes. - **One-sided clamp.** Unsigned add cannot underflow, so only the upper bound `0xFFFF` ever clips. - **The modulo counterpart is `vadduhm`.** Use `vadduhs` when "too large to fit" must be flagged or clipped — typical for accumulating Q16 unsigned counters. - **No XER side effects.** -- **Maps directly to `_mm_adds_epu16`** on SSE2 hosts — semantically identical, including the sticky-saturation observation step (xenia recovers the SAT flag from the per-lane comparison). +- **Maps directly to `_mm_adds_epu16`** on SSE2 hosts — semantically identical for the lanes. Canary emits a saturating unsigned `VectorAdd` but never records `VSCR[SAT]` (x64 `DID_SATURATE` is a stub). - **No VMX128 sibling.** ## Related Instructions diff --git a/tools/ppc-manual/vmx/vadduws.md b/tools/ppc-manual/vmx/vadduws.md index 6290ea66..034f3f7b 100644 --- a/tools/ppc-manual/vmx/vadduws.md +++ b/tools/ppc-manual/vmx/vadduws.md @@ -107,7 +107,7 @@ int InstrEmit_vadduws(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Four unsigned-word lanes, saturating.** Each `VD[i] = min(VA[i] + VB[i], 0xFFFF_FFFF)` for `i = 0..3`. Lane 0 (`VD[0..3]` after `stvx`) is the most-significant word. -- **`VSCR[SAT]` is sticky-set** if any lane clamps. Cleared only via [`mtvscr`](mtvscr.md). Xenia uses `crate::vmx::sat_add_u32` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)). +- **`VSCR[SAT]` is sticky-set** if any lane clamps. Cleared only via [`mtvscr`](../control/mtvscr.md). ⚠️ Canary never records `SAT`: its `StoreSAT(DidSaturate(v))` writes a private `vscr_sat` byte, the x64 `DID_SATURATE` is a stub that always yields 0, and [`mfvscr`](../control/mfvscr.md) reads `vscr_vec`, which only `mtvscr` changes. - **One-sided clamp** at `UINT32_MAX`. There is no underflow path for unsigned add. - **The modulo counterpart is `vadduwm`.** Use `vadduws` only when overflow needs to be visible / clamped; otherwise the modulo form is one cycle and never touches the sticky bit. - **No XER side effects, no carry exposure.** Unlike `vadduwm + vaddcuw`, the saturating form does **not** make the carry available — it is fused into the clamp. diff --git a/tools/ppc-manual/vmx/vand.md b/tools/ppc-manual/vmx/vand.md index d3af20f7..1e3fb781 100644 --- a/tools/ppc-manual/vmx/vand.md +++ b/tools/ppc-manual/vmx/vand.md @@ -161,12 +161,12 @@ int InstrEmit_vand_(PPCHIRBuilder& f, uint32_t vd, uint32_t va, uint32_t vb) { ## Special Cases & Edge Conditions -- **Bitwise across the full 128 bits.** `VD = VA & VB`. Lane width is irrelevant — the AND is bit-for-bit and there is no lane boundary. Xenia chooses to express this as four `u32` ANDs, but any widening (`u8`, `u16`, `u64`, `u128`) is observationally identical. +- **Bitwise across the full 128 bits.** `VD = VA & VB`. Lane width is irrelevant — the AND is bit-for-bit and there is no lane boundary. Canary emits one 128-bit `And`; any split (`u8`, `u16`, `u32`, `u64`) is observationally identical. - **No flags, no exceptions, no `VSCR` interaction.** Pure combinational op; one of the cheapest VMX instructions. - **Common usage with compares.** Compare ops produce per-lane all-ones / all-zero masks; `vand` with the mask selects the matching lanes (clearing the rest). For "select-by-mask" with a non-zero alternative use [`vsel`](vsel.md) instead. - **Idiom: clear lanes.** `vand VD, VD, vZero` zeroes a register; in practice [`vxor VD, VD, VD`](vxor.md) is preferred since it doesn't need a zero-vector source. - **Aliasing legal.** All three operands may overlap. -- **VMX128 sibling (`vand128`).** Identical semantics with the extended 128-register encoding; xenia reuses one match arm via the `vmx_reg_triple` helper (see [`crates/xenia-cpu/src/interpreter.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). +- **VMX128 sibling (`vand128`).** Identical semantics with the extended 128-register encoding; Canary's `vand128` decodes the 7-bit register indices and shares `vand`'s body (`InstrEmit_vand_`). ## Related Instructions diff --git a/tools/ppc-manual/vmx/vandc.md b/tools/ppc-manual/vmx/vandc.md index 015fa54c..24d31bc4 100644 --- a/tools/ppc-manual/vmx/vandc.md +++ b/tools/ppc-manual/vmx/vandc.md @@ -166,7 +166,7 @@ int InstrEmit_vandc_(PPCHIRBuilder& f, uint32_t vd, uint32_t va, uint32_t vb) { - **Compare → mask → mask-out idiom.** A compare produces per-lane all-ones; pair with `vandc` to keep only the lanes where the compare was *false*. The complement avoids an extra [`vnor`](vnor.md) or `vxor` with all-ones. - **No flags, no exceptions, no `VSCR` interaction.** - **Aliasing legal.** `vandc VD, VD, VD` clears `VD` (`x & ~x = 0`). -- **VMX128 sibling (`vandc128`).** Identical semantics with the extended 128-register encoding; xenia reuses one match arm. +- **VMX128 sibling (`vandc128`).** Identical semantics with the extended 128-register encoding; Canary's `vandc128` shares `vandc`'s body after decoding the 7-bit indices. ## Related Instructions diff --git a/tools/ppc-manual/vmx/vavgsb.md b/tools/ppc-manual/vmx/vavgsb.md index 97402d92..d4e2eb6d 100644 --- a/tools/ppc-manual/vmx/vavgsb.md +++ b/tools/ppc-manual/vmx/vavgsb.md @@ -106,7 +106,7 @@ int InstrEmit_vavgsb(PPCHIRBuilder& f, const InstrData& i) { - **Sixteen signed-byte rounding averages.** Each `VD[i] = (VA[i] + VB[i] + 1) >> 1`, performed in arithmetic *wider* than 8 bits (so the `+1` cannot overflow). The result is then truncated back to `int8` — saturation never triggers because the average of two `int8` values fits in `int8`. Rounding is "round half up toward +∞". - **Big-endian byte lanes.** Lane 0 is the most-significant byte after `stvx`. -- **No `VSCR[SAT]` impact.** Mathematical impossibility — `(a + b + 1) / 2` for `a, b ∈ [-128, 127]` always lies in `[-128, 127]`. Xenia's `crate::vmx::avg_i8` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)) widens to `i16` before the add. +- **No `VSCR[SAT]` impact.** Mathematical impossibility — `(a + b + 1) / 2` for `a, b ∈ [-128, 127]` always lies in `[-128, 127]`. x86 has no signed byte average, so Canary's x64 backend computes each lane in a scalar loop as `(a + b + 1) >> 1` on sign-extended 32-bit values. - **No XER side effects.** - **Common usage.** Filtering / decimation passes, motion-compensation half-pel interpolation in older video codecs (the rounding-up bias matches MPEG/H.263 averaging conventions). - **Aliasing legal.** `vavgsb v3, v3, v4` is a typical lowpass-step idiom. diff --git a/tools/ppc-manual/vmx/vavgsh.md b/tools/ppc-manual/vmx/vavgsh.md index 302df52c..e9ce7501 100644 --- a/tools/ppc-manual/vmx/vavgsh.md +++ b/tools/ppc-manual/vmx/vavgsh.md @@ -106,7 +106,7 @@ int InstrEmit_vavgsh(PPCHIRBuilder& f, const InstrData& i) { - **Eight signed-half rounding averages.** Each `VD[i] = (VA[i] + VB[i] + 1) >> 1`, computed in 32-bit arithmetic to avoid overflow on the intermediate sum, then truncated back to `int16`. Rounding is half-up toward +∞. - **Big-endian half lanes.** Lane 0 (`VD[0..1]` after `stvx`) is the most-significant half. -- **No `VSCR[SAT]` impact.** The result is always representable in `int16`. Xenia's `crate::vmx::avg_i16` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)) widens to `i32` before adding. +- **No `VSCR[SAT]` impact.** The result is always representable in `int16`. Canary's x64 backend computes each lane in a scalar loop as `(a + b + 1) >> 1` on sign-extended 32-bit values. - **No XER side effects.** - **Common usage.** Audio sample interpolation, fixed-point Q15 midpoint filters, video upscaling at 16-bit precision. - **Aliasing legal.** `vavgsh v3, v3, v4` collapses two half-precision streams into one. diff --git a/tools/ppc-manual/vmx/vavgsw.md b/tools/ppc-manual/vmx/vavgsw.md index d0569584..d07231b3 100644 --- a/tools/ppc-manual/vmx/vavgsw.md +++ b/tools/ppc-manual/vmx/vavgsw.md @@ -110,7 +110,7 @@ int InstrEmit_vavgsw(PPCHIRBuilder& f, const InstrData& i) { - **Four signed-word rounding averages.** Each `VD[i] = (VA[i] + VB[i] + 1) >> 1`, computed in 64-bit arithmetic to avoid intermediate overflow, then truncated back to `int32`. Rounding is half-up toward +∞. - **Big-endian word lanes.** Lane 0 (`VD[0..3]` after `stvx`) is the most-significant word. -- **No `VSCR[SAT]` impact.** The mathematical result always fits in `int32`. Xenia's `crate::vmx::avg_i32` widens to `i64` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)). +- **No `VSCR[SAT]` impact.** The mathematical result always fits in `int32`. Canary's x64 backend computes each lane in a scalar loop as `(a + b + 1) >> 1` on sign-extended 64-bit values. - **No XER side effects.** - **Aliasing legal.** - **No VMX128 sibling.** diff --git a/tools/ppc-manual/vmx/vavgub.md b/tools/ppc-manual/vmx/vavgub.md index 090765a6..a6da5845 100644 --- a/tools/ppc-manual/vmx/vavgub.md +++ b/tools/ppc-manual/vmx/vavgub.md @@ -106,7 +106,7 @@ int InstrEmit_vavgub(PPCHIRBuilder& f, const InstrData& i) { - **Sixteen unsigned-byte rounding averages.** Each `VD[i] = (VA[i] + VB[i] + 1) >> 1`, computed in 16-bit arithmetic so the `+1` cannot overflow, then truncated back to `u8`. Rounding is half-up. - **Big-endian byte lanes.** Lane 0 is the most-significant byte after `stvx`. -- **No `VSCR[SAT]` impact.** The result always fits in `u8` (the average of two `u8` values is at most `255`). Xenia uses `crate::vmx::avg_u8` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)). +- **No `VSCR[SAT]` impact.** The result always fits in `u8` (the average of two `u8` values is at most `255`). Canary emits `vpavgb`. - **Equivalent to `_mm_avg_epu8`** on x86 SSE2 — semantically identical (rounding mode and width match). - **Common usage.** Pixel-blend `(A + B + 1) / 2`, MPEG/H.264 half-pel motion-compensation averaging, downscale filters, alpha midpoint. - **Aliasing legal.** `vavgub v3, v3, v4`. diff --git a/tools/ppc-manual/vmx/vavguh.md b/tools/ppc-manual/vmx/vavguh.md index c37d11a5..01e1a59c 100644 --- a/tools/ppc-manual/vmx/vavguh.md +++ b/tools/ppc-manual/vmx/vavguh.md @@ -106,7 +106,7 @@ int InstrEmit_vavguh(PPCHIRBuilder& f, const InstrData& i) { - **Eight unsigned-half rounding averages.** Each `VD[i] = (VA[i] + VB[i] + 1) >> 1`, computed in 32-bit arithmetic to avoid the intermediate `+1` overflowing, then truncated to `u16`. Rounding is half-up. - **Big-endian half lanes.** Lane 0 (`VD[0..1]` after `stvx`) is the most-significant half. -- **No `VSCR[SAT]` impact.** The result always fits in `u16`. Xenia uses `crate::vmx::avg_u16` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)). +- **No `VSCR[SAT]` impact.** The result always fits in `u16`. Canary emits `vpavgw`. - **Equivalent to `_mm_avg_epu16`** on x86 SSE2 — same rounding, same width. - **Common usage.** Higher-precision pixel blending (e.g. RGB565 sums after widening), Q16 unsigned filters. - **Aliasing legal.** diff --git a/tools/ppc-manual/vmx/vavguw.md b/tools/ppc-manual/vmx/vavguw.md index 65b77184..70d1ff66 100644 --- a/tools/ppc-manual/vmx/vavguw.md +++ b/tools/ppc-manual/vmx/vavguw.md @@ -106,8 +106,8 @@ int InstrEmit_vavguw(PPCHIRBuilder& f, const InstrData& i) { - **Four unsigned-word rounding averages.** Each `VD[i] = (VA[i] + VB[i] + 1) >> 1`, computed in 64-bit arithmetic to avoid intermediate overflow, then truncated to `u32`. Rounding is half-up. - **Big-endian word lanes.** Lane 0 (`VD[0..3]` after `stvx`) is the most-significant word. -- **No `VSCR[SAT]` impact.** The result always fits in `u32`. Xenia uses `crate::vmx::avg_u32` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)). -- **No SSE2 direct equivalent.** SSE2 only provides `_mm_avg_epu8` and `_mm_avg_epu16`; on x86 hosts xenia has to widen to 64-bit and do the average manually. +- **No `VSCR[SAT]` impact.** The result always fits in `u32`. Canary's x64 backend computes each lane in a scalar loop as `(a + b + 1) >> 1` on zero-extended 64-bit values. +- **No SSE2 direct equivalent.** SSE2 only provides `_mm_avg_epu8` and `_mm_avg_epu16`; on x86 hosts Canary widens each lane to 64 bits and averages in a scalar loop. - **Common usage.** Per-tile counters; midpoint of two 32-bit packed values. - **Aliasing legal.** - **No VMX128 sibling.** diff --git a/tools/ppc-manual/vmx/vcfsx.md b/tools/ppc-manual/vmx/vcfsx.md index 56964085..34853ea8 100644 --- a/tools/ppc-manual/vmx/vcfsx.md +++ b/tools/ppc-manual/vmx/vcfsx.md @@ -118,7 +118,7 @@ int InstrEmit_vcfsx_(PPCHIRBuilder& f, uint32_t vd, uint32_t vb, - **Big-endian word lanes.** Lane 0 (`VD[0..3]` after `stvx`) is the most-significant word. - **Use case.** Q-format fixed-point (`Qm.n`) → IEEE float in one instruction. UIMM gives the fractional bit count, so `vcfsx vD, vB, 16` interprets each lane as Q15.16. - **Inexact rounding.** Values whose magnitude exceeds `2^24` lose mantissa precision (only 24 bits in `binary32`'s significand). The default rounding mode is round-to-nearest-even; VMX has no per-instruction rounding control. -- **`VSCR[NJ]` (flush-denormals)** affects the output if the scaled value is sub-normal. Xenia's `crate::vmx::cvt_i32_to_f32` honours this via the architectural `VSCR[NJ]` snapshot. +- **`VSCR[NJ]` (flush-denormals)** affects the output if the scaled value is sub-normal. Canary converts with `vcvtdq2ps` and multiplies by `2^-UIMM` under its VMX MXCSR, which flushes such results to zero while `NJ` is set. - **No `VSCR[SAT]` or XER changes**, no exceptions raised. - **No VMX128 sibling.** - **Round-trip caveat.** `vctsxs` (the inverse) saturates instead of wrapping, so a `vcfsx`/`vctsxs` round-trip is *not* identity for values outside the signed-int32 representable range — important for fixed-point interpolation kernels. diff --git a/tools/ppc-manual/vmx/vcfux.md b/tools/ppc-manual/vmx/vcfux.md index 15843e28..28f2e17f 100644 --- a/tools/ppc-manual/vmx/vcfux.md +++ b/tools/ppc-manual/vmx/vcfux.md @@ -118,7 +118,7 @@ int InstrEmit_vcfux_(PPCHIRBuilder& f, uint32_t vd, uint32_t vb, - **Big-endian word lanes.** Lane 0 (`VD[0..3]` after `stvx`) is the most-significant word. - **Use case.** Unsigned Q-format fixed-point → IEEE float; common for normalised colour channels (`vcfux vD, vColor, 8` rescales `0..255` to `0..0.996`). - **Inexact rounding.** Magnitudes above `2^24` lose precision. Default rounding is round-to-nearest-even; VMX has no per-instruction rounding control. -- **`VSCR[NJ]`** affects sub-normal outputs. Xenia's `crate::vmx::cvt_u32_to_f32` honours the architectural snapshot. +- **`VSCR[NJ]`** affects sub-normal outputs. Canary multiplies by `2^-UIMM` under its VMX MXCSR, which flushes such results to zero while `NJ` is set. - **No `VSCR[SAT]`, no XER changes, no exceptions.** - **No VMX128 sibling.** - **Round-trip caveat.** Pair with [`vctuxs`](vctuxs.md) for the inverse — but the inverse saturates rather than wraps, so floats above `2^32 − 1` clamp to `0xFFFFFFFF` and stick `VSCR[SAT]`. diff --git a/tools/ppc-manual/vmx/vcmpbfp.md b/tools/ppc-manual/vmx/vcmpbfp.md index 02b916a1..0430ebfd 100644 --- a/tools/ppc-manual/vmx/vcmpbfp.md +++ b/tools/ppc-manual/vmx/vcmpbfp.md @@ -175,7 +175,7 @@ int InstrEmit_vcmpbfp128(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **"Bounds" compare, not equality.** Per word lane, sets two output bits: bit 0 (mask `0x80000000`) if `VA[i] > VB[i]` (out-of-range high) and bit 1 (mask `0x40000000`) if `VA[i] < -VB[i]` (out-of-range low). Bits 2..31 of each lane are zero. -- **NaN inputs are out-of-range in *both* directions.** Xenia sets both `0x80000000` and `0x40000000` if either input is NaN, matching the IBM manual: NaN is treated as "violates both bounds". +- **NaN inputs.** The IBM manual treats NaN as violating both bounds (`0xC0000000`), and Canary's comment says the same, but its code sets only `0x40000000`: the "greater than" test is `vcmpgtps` (false for NaN) and only the "not ≥ −VB" test comes out true. **Canary quirk** for NaN inputs. - **CR6 update when `Rc=1`.** CR6 is set as `[lt=0, gt=0, eq=(no-lane-out-of-range), so=0]` — i.e. only the `eq` bit signifies "all four lanes were within `±VB`". Useful as `bc 12,26` (branch if all in-range) for SIMD clamping loops. - **No `VSCR[SAT]`, no XER changes, no exceptions.** - **The convention is "is point inside box?"** — not a per-lane compare like the other `vcmp*` ops. Output is a flag-pair, not a boolean mask, so it does **not** plug directly into [`vsel`](vsel.md). To get a boolean, OR the two bits down with [`vor`](vor.md) and a shift. diff --git a/tools/ppc-manual/vmx/vcmpeqfp.md b/tools/ppc-manual/vmx/vcmpeqfp.md index d7176c7d..6273f36f 100644 --- a/tools/ppc-manual/vmx/vcmpeqfp.md +++ b/tools/ppc-manual/vmx/vcmpeqfp.md @@ -155,10 +155,10 @@ int InstrEmit_vcmpeqfp128(PPCHIRBuilder& f, const InstrData& i) { - **NaN handling is IEEE-754: never equal.** `NaN == anything` is false (including `NaN == NaN`), so the lane stays zero. This is the standard quiet-compare behaviour — no exception, no sticky flag. - **Sign of zero ignored.** `+0 == -0` per IEEE-754, so the lane is set to all-ones. - **`VSCR[NJ]` — denormals.** With `NJ = 1` (Xenon default), denormal inputs are flushed to `±0` *before* the comparison; `±denormal == ±0` then compares as true. This is one of the few VMX float ops where the NJ flag changes program-visible mask values. -- **CR6 update when `Rc=1`** (`vcmpeqfp.`). CR6 is `{any-true, 0, all-true, 0}` = `[lt = all-true, gt = 0, eq = all-false, so = 0]` in the standard mapping; xenia's `update_cr6_from_vmask` ([`crates/xenia-cpu/src/interpreter.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)) handles the bit packing. Use `bc 12,24` for "all-equal" branches and `bc 4,26` for "any-equal". +- **CR6 update when `Rc=1`** (`vcmpeqfp.`). CR6 = `[lt = all-true, gt = 0, eq = all-false, so = 0]`; Canary's `UpdateCR6` packs exactly that. Use `bc 12,24` for "all-equal" branches and `bc 4,26` for "any-equal". - **Compose with `vsel`.** Mask drives [`vsel`](vsel.md) to pick between two source vectors per lane. Or combine masks with [`vand`](vand.md) / [`vor`](vor.md) / [`vandc`](vandc.md) to express conjunctions. - **No `VSCR[SAT]`, no XER changes, no traps** — even on signaling NaNs (Altivec's quiet-compare semantics). -- **VMX128 sibling (`vcmpeqfp128`).** Identical semantics with the extended 128-register encoding; xenia routes both opcodes to one match arm via `vmx_reg_triple`. +- **VMX128 sibling (`vcmpeqfp128`).** Identical semantics with the extended 128-register encoding; Canary routes both opcodes to one body (`InstrEmit_vcmpxxfp_`). ## Related Instructions diff --git a/tools/ppc-manual/vmx/vcmpequb.md b/tools/ppc-manual/vmx/vcmpequb.md index c686f2c2..f10cbd1e 100644 --- a/tools/ppc-manual/vmx/vcmpequb.md +++ b/tools/ppc-manual/vmx/vcmpequb.md @@ -107,7 +107,7 @@ int InstrEmit_vcmpequb(PPCHIRBuilder& f, const InstrData& i) { - **Per-byte mask: all-ones / all-zero.** Sixteen byte lanes; `VD[i] = (VA[i] == VB[i]) ? 0xFF : 0x00`. Lane 0 is the most-significant byte after `stvx`. - **Sign-agnostic.** Equality compare is identical for signed and unsigned bytes; there is no separate `vcmpeqsb`. -- **CR6 update when `Rc=1`** (`vcmpequb.`). CR6 = `[lt = all-true, gt = 0, eq = all-false, so = 0]` — built by xenia's `crate::vmx::cr6_flags_from_mask` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)). Standard SIMD-search idiom: `vcmpequb. vMask, vData, vNeedle` then `bc 12,26` to branch when *no* lane matched. +- **CR6 update when `Rc=1`** (`vcmpequb.`). CR6 = `[lt = all-true, gt = 0, eq = all-false, so = 0]` — packed by Canary's `UpdateCR6`. Standard SIMD-search idiom: `vcmpequb. vMask, vData, vNeedle` then `bc 12,26` to branch when *no* lane matched. - **Compose with `vsel`.** Mask drives [`vsel`](vsel.md) to pick per-byte between two source vectors. - **Common usage.** `memchr` / `strlen` / character classification — compare against a broadcast byte (often built with [`vspltisb`](vspltisb.md)) and inspect CR6 for early-out. - **No `VSCR` interaction, no XER, no traps.** diff --git a/tools/ppc-manual/vmx/vcmpequw.md b/tools/ppc-manual/vmx/vcmpequw.md index 8a0dd496..bbcfed2d 100644 --- a/tools/ppc-manual/vmx/vcmpequw.md +++ b/tools/ppc-manual/vmx/vcmpequw.md @@ -158,7 +158,7 @@ int InstrEmit_vcmpequw128(PPCHIRBuilder& f, const InstrData& i) { - **Common usage.** Hashtable probe matching, packed-RGBA pixel comparisons, packed-int handle equality. - **No `VSCR` interaction, no XER, no traps.** - **Aliasing legal.** -- **VMX128 sibling (`vcmpequw128`).** Identical semantics with the extended encoding; xenia routes both to one match arm via `vmx_reg_triple`. +- **VMX128 sibling (`vcmpequw128`).** Identical semantics with the extended encoding; Canary routes both to one body (`InstrEmit_vcmpxxi_`). ## Related Instructions diff --git a/tools/ppc-manual/vmx/vcmpgtsb.md b/tools/ppc-manual/vmx/vcmpgtsb.md index 11f0d923..094fdc8c 100644 --- a/tools/ppc-manual/vmx/vcmpgtsb.md +++ b/tools/ppc-manual/vmx/vcmpgtsb.md @@ -107,7 +107,7 @@ int InstrEmit_vcmpgtsb(PPCHIRBuilder& f, const InstrData& i) { - **Per-byte mask: all-ones / all-zero.** Sixteen byte lanes; `VD[i] = (int8(VA[i]) > int8(VB[i])) ? 0xFF : 0x00`. Lane 0 is the most-significant byte after `stvx`. - **Sign matters.** Identical bit patterns to [`vcmpgtub`](vcmpgtub.md) compare differently because of the signed interpretation: e.g. `0xFF > 0x01` is `true` unsigned but `false` signed (`-1 > 1`). -- **CR6 update when `Rc=1`** (`vcmpgtsb.`). CR6 = `[lt = all-true, gt = 0, eq = all-false, so = 0]` — built by xenia's `crate::vmx::cr6_flags_from_mask`. +- **CR6 update when `Rc=1`** (`vcmpgtsb.`). CR6 = `[lt = all-true, gt = 0, eq = all-false, so = 0]` — packed by Canary's `UpdateCR6`. - **Compose with `vsel`.** Mask drives [`vsel`](vsel.md) to select per byte. - **Common usage.** Signed-byte audio thresholding, signed-difference sign extraction (`vsubsbs` then `vcmpgtsb`). - **No `VSCR` interaction, no XER, no traps.** diff --git a/tools/ppc-manual/vmx/vcmpgtub.md b/tools/ppc-manual/vmx/vcmpgtub.md index 7a128da2..e0589ce7 100644 --- a/tools/ppc-manual/vmx/vcmpgtub.md +++ b/tools/ppc-manual/vmx/vcmpgtub.md @@ -107,7 +107,7 @@ int InstrEmit_vcmpgtub(PPCHIRBuilder& f, const InstrData& i) { - **Per-byte mask: all-ones / all-zero.** Sixteen byte lanes; `VD[i] = (uint8(VA[i]) > uint8(VB[i])) ? 0xFF : 0x00`. Lane 0 is the most-significant byte after `stvx`. - **Sign matters.** `0xFF > 0x01` is `true` unsigned but `false` signed (`-1 > 1`); pick `vcmpgtub` only when both sides should be treated as `0..255`. -- **CR6 update when `Rc=1`** (`vcmpgtub.`). CR6 = `[lt = all-true, gt = 0, eq = all-false, so = 0]` — built by xenia's `crate::vmx::cr6_flags_from_mask`. +- **CR6 update when `Rc=1`** (`vcmpgtub.`). CR6 = `[lt = all-true, gt = 0, eq = all-false, so = 0]` — packed by Canary's `UpdateCR6`. - **Compose with `vsel`.** Mask drives [`vsel`](vsel.md) per byte. - **Common usage.** Pixel "brighter than" tests, byte-level histogramming, threshold-based binarisation. - **No `VSCR` interaction, no XER, no traps.** diff --git a/tools/ppc-manual/vmx/vctsxs.md b/tools/ppc-manual/vmx/vctsxs.md index 1edb1d51..d1bb37c2 100644 --- a/tools/ppc-manual/vmx/vctsxs.md +++ b/tools/ppc-manual/vmx/vctsxs.md @@ -116,8 +116,8 @@ int InstrEmit_vctsxs_(PPCHIRBuilder& f, uint32_t vd, uint32_t vb, ## Special Cases & Edge Conditions - **Convert IEEE float lane to signed-Q `int32`, saturating.** For each of the four word lanes, `VD[i] = clamp(round_toward_zero(VB[i] * 2^UIMM), INT32_MIN, INT32_MAX)`. The 5-bit `UIMM` (bits 11..15) gives the Q-format fractional shift, in `0..31`. -- **Saturating, not wrapping.** Out-of-range floats clamp to `INT32_MIN` (negative overflow) or `INT32_MAX` (positive overflow) — *not* the wrap-around behaviour of x86 `cvttps2dq` (which produces `0x80000000` on overflow regardless of sign). Xenia's `crate::vmx::cvt_f32_to_i32_sat` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)) handles the difference. -- **NaN → 0.** A NaN input becomes `0` in the output lane and stickies `VSCR[SAT]`. (Many references state "NaN → INT32_MIN"; verify against [`vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs) for the canonical xenia behaviour, which differs from POWER ISA wording.) +- **Saturating, not wrapping.** Out-of-range floats clamp to `INT32_MIN` (negative overflow) or `INT32_MAX` (positive overflow) — *not* the wrap-around behaviour of x86 `cvttps2dq` (which produces `0x80000000` on overflow regardless of sign). Canary converts with `vcvttps2dq` and then blends `INT32_MAX` into lanes that overflowed from a non-negative input. +- **NaN → 0 in Canary.** It masks NaN lanes to `0` (`vcmpunordps` + `vpandn`). Some references state "NaN → INT32_MIN" for hardware; which one Xenon does is unverified. Canary never records `VSCR[SAT]`. - **`VSCR[SAT]` is sticky-set** if any lane saturates (overflow or NaN). Cleared only by [`mtvscr`](mtvscr.md). - **Rounding is truncate-toward-zero.** Always; no per-instruction rounding control. - **`VSCR[NJ]` flushes denormal *inputs* to zero before scaling** (Xenon default). diff --git a/tools/ppc-manual/vmx/vctuxs.md b/tools/ppc-manual/vmx/vctuxs.md index 28cf7146..92607ab7 100644 --- a/tools/ppc-manual/vmx/vctuxs.md +++ b/tools/ppc-manual/vmx/vctuxs.md @@ -116,7 +116,7 @@ int InstrEmit_vctuxs_(PPCHIRBuilder& f, uint32_t vd, uint32_t vb, ## Special Cases & Edge Conditions - **Convert IEEE float lane to unsigned-Q `uint32`, saturating.** For each of the four word lanes, `VD[i] = clamp(round_toward_zero(VB[i] * 2^UIMM), 0, UINT32_MAX)`. The 5-bit `UIMM` (bits 11..15) gives the Q-format fractional shift, in `0..31`. -- **Saturating, not wrapping.** Negative inputs clamp to `0`; values above `2^32 − 1` clamp to `0xFFFF_FFFF`. NaN → `0`. All clamping events sticky-set `VSCR[SAT]`. Xenia's `crate::vmx::cvt_f32_to_u32_sat` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)) handles the boundaries. +- **Saturating, not wrapping.** Negative inputs clamp to `0`; values above `2^32 − 1` clamp to `0xFFFF_FFFF`. NaN → `0`. All clamping events sticky-set `VSCR[SAT]` on hardware. Canary reproduces the values — `vmaxps` against zero (which also turns NaN into `0`), a rebase for lanes ≥ `2^31`, and an all-ones fix-up for overflow (or `vcvttps2udq` with a mask on AVX-512 hosts) — but never records `SAT`. - **`VSCR[SAT]` sticky.** Cleared only by [`mtvscr`](mtvscr.md). - **Rounding is truncate-toward-zero.** Always. - **`VSCR[NJ]` flushes denormal inputs to zero before scaling** (Xenon default). diff --git a/tools/ppc-manual/vmx/vexptefp.md b/tools/ppc-manual/vmx/vexptefp.md index c03ea765..d777b3a5 100644 --- a/tools/ppc-manual/vmx/vexptefp.md +++ b/tools/ppc-manual/vmx/vexptefp.md @@ -156,7 +156,7 @@ int InstrEmit_vexptefp_(PPCHIRBuilder& f, uint32_t vd, uint32_t vb) { ## Special Cases & Edge Conditions -- **Per-lane base-2 exponent.** Each of the four word lanes computes `VD[i] = 2^VB[i]` in `binary32`. **Note:** the IBM manual specifies a low-precision estimate (≤ 1/16 ULP relative error). Xenia uses Rust's `f32::exp2`, which is full-precision — programs that depend on hardware-quality estimation may observe small numerical differences. +- **Per-lane base-2 exponent.** Each of the four word lanes computes `VD[i] = 2^VB[i]` in `binary32`. **Note:** the IBM manual specifies a low-precision estimate (≤ 1/16 ULP relative error). Canary calls the host's `std::exp2` per lane, which is full-precision — programs that depend on hardware-quality estimation may observe small numerical differences. - **Use `vlogefp` for the inverse.** The natural pair is `vexptefp(vlogefp(x)) = x` for positive finite `x`, modulo each estimate's error budget. - **Big-endian word lanes.** Lane 0 is the most-significant word. - **NaN, ±∞.** `2^NaN = NaN`; `2^(+∞) = +∞`; `2^(-∞) = +0`. Subnormal results may be flushed to `±0` if `VSCR[NJ] = 1` (Xenon default). @@ -170,7 +170,7 @@ int InstrEmit_vexptefp_(PPCHIRBuilder& f, uint32_t vd, uint32_t vb) { - [`vrefp`](vrefp.md) — reciprocal estimate. - [`vrsqrtefp`](vrsqrtefp.md) — reciprocal-square-root estimate. - [`vmaddfp`](vmaddfp.md) — fused multiply-add for change-of-base scaling. -- [`vmulfp`](vmulfp.md) — float multiply (xenia helper). +- [`vmulfp128`](../vmx128/vmulfp128.md) — VMX128-only lane-wise float multiply. ## IBM Reference diff --git a/tools/ppc-manual/vmx/vlogefp.md b/tools/ppc-manual/vmx/vlogefp.md index c8595f30..69a57ae9 100644 --- a/tools/ppc-manual/vmx/vlogefp.md +++ b/tools/ppc-manual/vmx/vlogefp.md @@ -156,7 +156,7 @@ int InstrEmit_vlogefp_(PPCHIRBuilder& f, uint32_t vd, uint32_t vb) { ## Special Cases & Edge Conditions -- **Per-lane base-2 logarithm.** Each of the four word lanes computes `VD[i] = log2(VB[i])` in `binary32`. **Note:** the IBM manual specifies a low-precision estimate (≤ 1/32 ULP relative error). Xenia uses Rust's `f32::log2`, which is full-precision; hardware-precise programs may observe small numerical differences. +- **Per-lane base-2 logarithm.** Each of the four word lanes computes `VD[i] = log2(VB[i])` in `binary32`. **Note:** the IBM manual specifies a low-precision estimate (≤ 1/32 ULP relative error). Canary calls the host's `std::log2` per lane, which is full-precision; hardware-precise programs may observe small numerical differences. - **Use `vexptefp` for the inverse.** Pair gives `2^(log2(x)) ≈ x` for positive finite `x`. - **Big-endian word lanes.** Lane 0 is the most-significant word. - **NaN, negatives, zero, ±∞.** `log2(negative)` and `log2(NaN)` produce NaN; `log2(+0) = -∞`; `log2(-0) = -∞` (per IEEE-754); `log2(+∞) = +∞`. None of these stickies `VSCR[SAT]` — float ops never touch SAT. diff --git a/tools/ppc-manual/vmx/vmaddfp.md b/tools/ppc-manual/vmx/vmaddfp.md index fdd36e4f..066531db 100644 --- a/tools/ppc-manual/vmx/vmaddfp.md +++ b/tools/ppc-manual/vmx/vmaddfp.md @@ -163,7 +163,7 @@ int InstrEmit_vmaddfp128(PPCHIRBuilder& f, const InstrData& i) { - **NaN propagation, ±∞ arithmetic.** Standard IEEE-754: any NaN input yields NaN; `(+∞ * 0)` yields NaN; the sum of `+∞` and `-∞` (e.g. `(+∞ * 1) + -∞`) yields NaN. No trap, no sticky bit. - **`VSCR[NJ]` denormals.** With `NJ = 1` (Xenon default), denormal inputs and outputs are flushed to `±0`. - **No `VSCR[SAT]` change, no XER change, no exceptions.** -- **VMX128 sibling has surprising operand layout — `VD` is also a source.** Xenia's `vmaddfp128` reads `VA`, `VB`, *and `VD` itself* (as the accumulator), computing `VD = (VA * VB) + VD_prev` ([`crates/xenia-cpu/src/interpreter.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)). The standard `vmaddfp` keeps the canonical 4-operand `VA, VC, VB → VD` shape. **This is a real difference in operand encoding** (VX128_3 form vs. VA-form) that compilers must respect — VMX128 sacrifices the third source register slot for the extra register-file bits. +- **VMX128 sibling has surprising operand layout — `VD` is also a source.** Canary's `vmaddfp128` passes `VD` as the addend, computing `VD = (VA * VB) + VD_prev` (its comment: "this resuses VD and swaps the arg order!"). The standard `vmaddfp` keeps the canonical 4-operand `VA, VC, VB → VD` shape. **This is a real difference in operand encoding** (VX128 form vs. VA-form) that compilers must respect — VMX128 sacrifices the third source register slot for the extra register-file bits. - **Aliasing legal.** `vmaddfp v3, v3, v3, v3` works (squares + adds itself). - **Common usage.** Per-lane polynomial evaluation, dot-product accumulation, any matrix multiply inner loop. Pair four `vmaddfp` instructions to do a 4×4 × 4-vec multiply. @@ -171,7 +171,7 @@ int InstrEmit_vmaddfp128(PPCHIRBuilder& f, const InstrData& i) { - [`vnmsubfp`](vnmsubfp.md) — `−((VA * VC) − VB)`; fused negative-multiply-subtract. - [`vaddfp`](vaddfp.md), [`vsubfp`](vsubfp.md) — plain float add / subtract. -- [`vmulfp`](vmulfp.md) — xenia helper for `VA * VC`; on hardware games use `vmaddfp v, va, vc, v0_zero`. +- [`vmulfp128`](../vmx128/vmulfp128.md) — the VMX128-only `VA * VB` multiply; in standard Altivec, games use `vmaddfp v, va, vc, v0_zero` instead. - [`vmaxfp`](vmaxfp.md), [`vminfp`](vminfp.md) — min / max for clamping. - [`vrefp`](vrefp.md), [`vrsqrtefp`](vrsqrtefp.md) — reciprocal / inverse-sqrt estimates that often appear in the same FMA chain. diff --git a/tools/ppc-manual/vmx/vmaxfp.md b/tools/ppc-manual/vmx/vmaxfp.md index 6f49cbfa..bbb00b0b 100644 --- a/tools/ppc-manual/vmx/vmaxfp.md +++ b/tools/ppc-manual/vmx/vmaxfp.md @@ -162,8 +162,8 @@ int InstrEmit_vmaxfp_(PPCHIRBuilder& f, uint32_t vd, uint32_t va, uint32_t vb) { ## Special Cases & Edge Conditions - **Per-lane IEEE max.** Four word lanes; `VD[i] = (VA[i] > VB[i]) ? VA[i] : VB[i]`. -- **NaN propagation surprise.** Xenia uses `if a > b { a } else { b }`, so any NaN comparison evaluates false and the result is `VB`. The IBM manual specifies "the larger of `VA[i]` and `VB[i]`, with NaN handling such that any NaN input yields a NaN result" — this is *not* what xenia does. Hardware's `vmaxfp(NaN, x) = NaN` while xenia returns `x`. **Worth checking against `vmx.rs` for any future correctness fixes.** -- **Sign of zero.** `vmaxfp(+0, -0)` returns `-0` in xenia (since `+0 > -0` is false → returns `b = -0`). The hardware likely returns the sign-positive zero — also worth verifying. +- **NaN propagation.** The IBM manual specifies "the larger of `VA[i]` and `VB[i]`, with NaN handling such that any NaN input yields a NaN result". Canary's x64 `MAX` matches: it blends the NaN back in when either lane is NaN (`VA`'s when both are), and takes `max(+0, −0) = +0` by AND-ing both operand orders of `vmaxps`. +- **Sign of zero.** Canary's `vmaxfp(+0, -0)` returns `+0`: it ANDs both operand orders of `vmaxps` (its comment: "if 0 and -0, return 0! opposite of minfp"). What Xenon returns is unverified. - **`VSCR[NJ]` denormals.** With `NJ = 1` (Xenon default), denormal inputs are flushed to `±0` before comparison. - **No `VSCR[SAT]` change, no XER change, no exceptions.** - **Big-endian word lanes.** Lane 0 is the most-significant word. diff --git a/tools/ppc-manual/vmx/vmhaddshs.md b/tools/ppc-manual/vmx/vmhaddshs.md index d8bfd348..6c12d547 100644 --- a/tools/ppc-manual/vmx/vmhaddshs.md +++ b/tools/ppc-manual/vmx/vmhaddshs.md @@ -112,7 +112,7 @@ int InstrEmit_vmhaddshs(PPCHIRBuilder& f, const InstrData& i) { ``` The "h" in the mnemonic is "high half" — only the upper 17 bits of the 32-bit signed product survive (after >>15), then the accumulator is added. - **Truncating, not rounding.** Bit 14 of the product is discarded silently. Use [`vmhraddshs`](vmhraddshs.md) when half-up rounding is needed (it adds `0x4000` to the product before the shift). The two are otherwise identical. -- **`VSCR[SAT]` is sticky-set** if `prod + VC[i]` overflows `int16`. Cleared only by [`mtvscr`](mtvscr.md). Xenia uses `crate::vmx::sat_i32_to_i16` ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)). +- **`VSCR[SAT]` is sticky-set** if `prod + VC[i]` overflows `int16`. Cleared only by [`mtvscr`](../control/mtvscr.md). ⚠️ Canary does not implement `vmhaddshs`: its emitter is `XEINSTRNOTIMPLEMENTED`, so translating one logs "Unimplemented instr" and, with the default `break_on_unimplemented_instructions`, breaks. - **Pathological case `0x8000 * 0x8000 >> 15`.** Equals `0x10000` in the un-saturated product = `+32768` after the shift, which overflows `int16` even before adding `VC`. The clamp then produces `+32767` and stickies SAT. This is the classic Q15 "minus-one-times-minus-one" gotcha. - **Big-endian half lanes.** Lane 0 is the most-significant half. - **No XER changes, no exceptions.** diff --git a/tools/ppc-manual/vmx/vminfp.md b/tools/ppc-manual/vmx/vminfp.md index dcbbed97..a03856e9 100644 --- a/tools/ppc-manual/vmx/vminfp.md +++ b/tools/ppc-manual/vmx/vminfp.md @@ -162,8 +162,8 @@ int InstrEmit_vminfp_(PPCHIRBuilder& f, uint32_t vd, uint32_t va, uint32_t vb) { ## Special Cases & Edge Conditions - **Per-lane IEEE min.** Four word lanes; `VD[i] = (VA[i] < VB[i]) ? VA[i] : VB[i]`. -- **NaN propagation surprise.** Xenia uses `if a < b { a } else { b }`, so any NaN comparison evaluates false and the result is `VB`. The IBM manual specifies NaN-propagating min — i.e. NaN inputs should yield NaN. Hardware's `vminfp(NaN, x) = NaN` while xenia returns `x`. **Worth checking against `vmx.rs` for any future correctness fixes.** -- **Sign of zero.** `vminfp(+0, -0)` returns `-0` in xenia (since `+0 < -0` is false → returns `b = -0`); hardware likely returns the negative zero too via the same comparator. +- **NaN propagation.** The IBM manual specifies NaN-propagating min — i.e. NaN inputs should yield NaN. Canary's x64 `MIN` matches: it blends the NaN back in when either lane is NaN (`VA`'s when both are), and takes `min(+0, −0) = −0` by OR-ing both operand orders of `vminps`. +- **Sign of zero.** Canary's `vminfp(+0, -0)` returns `-0`: it ORs both operand orders of `vminps`. What Xenon returns is unverified. - **`VSCR[NJ]` denormals.** With `NJ = 1` (Xenon default), denormal inputs are flushed to `±0` before comparison. - **No `VSCR[SAT]` change, no XER change, no exceptions.** - **Big-endian word lanes.** Lane 0 is the most-significant word. diff --git a/tools/ppc-manual/vmx/vmrghw.md b/tools/ppc-manual/vmx/vmrghw.md index 2ed260ea..44efdc5c 100644 --- a/tools/ppc-manual/vmx/vmrghw.md +++ b/tools/ppc-manual/vmx/vmrghw.md @@ -176,7 +176,7 @@ int InstrEmit_vmrghw_(PPCHIRBuilder& f, uint32_t vd, uint32_t va, uint32_t vb) { - **Common usage.** Interleave matrix rows during a 4×4 transpose: four `vmrgh*`/`vmrgl*` pairs swap rows and columns of a 4×4 packed-float matrix. - **No `VSCR` interaction, no XER, no exceptions.** Pure permute. - **Aliasing legal.** `vmrghw v3, v3, v3` doubles each high word. -- **VMX128 sibling (`vmrghw128`).** Identical semantics with the extended encoding; xenia routes via `vmx_reg_triple`. +- **VMX128 sibling (`vmrghw128`).** Identical semantics with the extended encoding; Canary routes both to one body (`InstrEmit_vmrghw_`). ## Related Instructions diff --git a/tools/ppc-manual/vmx/vmrglw.md b/tools/ppc-manual/vmx/vmrglw.md index 677305e7..b6a7fa41 100644 --- a/tools/ppc-manual/vmx/vmrglw.md +++ b/tools/ppc-manual/vmx/vmrglw.md @@ -176,7 +176,7 @@ int InstrEmit_vmrglw_(PPCHIRBuilder& f, uint32_t vd, uint32_t va, uint32_t vb) { - **Common usage.** Bottom half of a 4×4 packed-float matrix transpose; second-half RGBA pixel re-pack after a `vmrghw`. - **No `VSCR` interaction, no XER, no exceptions.** Pure permute. - **Aliasing legal.** -- **VMX128 sibling (`vmrglw128`).** Identical semantics with the extended encoding; xenia routes both via `vmx_reg_triple`. +- **VMX128 sibling (`vmrglw128`).** Identical semantics with the extended encoding; Canary routes both to one body (`InstrEmit_vmrglw_`). ## Related Instructions diff --git a/tools/ppc-manual/vmx/vmsumshs.md b/tools/ppc-manual/vmx/vmsumshs.md index 9b4eb556..f8730e2c 100644 --- a/tools/ppc-manual/vmx/vmsumshs.md +++ b/tools/ppc-manual/vmx/vmsumshs.md @@ -111,7 +111,7 @@ int InstrEmit_vmsumshs(PPCHIRBuilder& f, const InstrData& i) { + int16(VA[2*i+1]) * int16(VB[2*i+1]), INT32_MIN, INT32_MAX) ``` Two signed-half × signed-half products plus a signed-word accumulator, clamped to `int32`. -- **Wide-then-clamp ordering.** Xenia accumulates into `i64` first and clamps the *final* sum to `int32`, exactly matching the IBM specification ([`crates/xenia-cpu/src/vmx.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/vmx.rs)). This avoids spurious mid-sum saturation that would happen if the products were clamped individually. +- **Wide-then-clamp ordering.** The IBM specification accumulates the full sum and clamps only the *final* result to `int32`, which avoids spurious mid-sum saturation that would happen if the products were clamped individually. ⚠️ Canary does not implement `vmsumshs`: its emitter is `XEINSTRNOTIMPLEMENTED`, so translating one logs "Unimplemented instr" and, with the default `break_on_unimplemented_instructions`, breaks. - **`VSCR[SAT]` is sticky-set** if any of the four lane sums saturates. Cleared only via [`mtvscr`](mtvscr.md). - **Big-endian half lanes.** Lane 0 is the most-significant half. - **No XER, no exceptions.** diff --git a/tools/ppc-manual/vmx/vmsumuhs.md b/tools/ppc-manual/vmx/vmsumuhs.md index eec597ef..693ddc39 100644 --- a/tools/ppc-manual/vmx/vmsumuhs.md +++ b/tools/ppc-manual/vmx/vmsumuhs.md @@ -111,7 +111,7 @@ int InstrEmit_vmsumuhs(PPCHIRBuilder& f, const InstrData& i) { + uint16(VA[2*i+1]) * uint16(VB[2*i+1]), 0, UINT32_MAX) ``` Two unsigned-half × unsigned-half products plus an unsigned-word accumulator, clamped to `uint32`. -- **Wide-then-clamp ordering.** Xenia accumulates into `u64` first and clamps the *final* sum to `u32` ([`crates/xenia-cpu/src/interpreter.rs`](https://git.mc02.dev/fabi/xenia-rs/src/commit/8401d4d5112849bf7b0f78f9d620f3620b1ce58d/crates/xenia-cpu/src/interpreter.rs)) — matches the IBM spec. +- **Wide-then-clamp ordering.** The IBM specification accumulates into a wide sum and clamps only the *final* result to `u32`. ⚠️ Canary does not implement `vmsumuhs`: its emitter is `XEINSTRNOTIMPLEMENTED`, so translating one logs "Unimplemented instr" and, with the default `break_on_unimplemented_instructions`, breaks. - **`VSCR[SAT]` is sticky-set** if any lane clamps. Only the upper bound `0xFFFF_FFFF` ever triggers; unsigned overflow on the low side is impossible. - **Big-endian half lanes.** Lane 0 is the most-significant half. - **No XER, no exceptions.** diff --git a/tools/ppc-manual/vmx/vnmsubfp.md b/tools/ppc-manual/vmx/vnmsubfp.md index 9850ae96..90e16782 100644 --- a/tools/ppc-manual/vmx/vnmsubfp.md +++ b/tools/ppc-manual/vmx/vnmsubfp.md @@ -158,11 +158,11 @@ int InstrEmit_vnmsubfp128(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Lane-wise negative multiply-subtract.** Each of the four lanes computes `VD[i] = −((VA[i] × VC[i]) − VB[i])`, i.e. `VB[i] − VA[i] × VC[i]`. The multiply and the subsequent add are **not** a single fused rounding step in xenia — they're a multiply, a subtract, then a negate — but the PowerPC ISA specifies the sequence to behave *as if* it were fused (single IEEE-754 rounding). Hardware Xenon indeed rounds only once. +- **Lane-wise negative multiply-subtract.** Each of the four lanes computes `VD[i] = −((VA[i] × VC[i]) − VB[i])`, i.e. `VB[i] − VA[i] × VC[i]`. The PowerPC ISA specifies the sequence to behave *as if* it were fused (single IEEE-754 rounding), and Canary's comment agrees ("only one rounding should take place"). Canary emits `Neg(MulSub(…))`: fused on FMA3 hosts (`vfmsub213ps`), a separate multiply and subtract otherwise. - **IEEE-754 binary32 lanes.** Follows `VSCR[NJ]`: denormal inputs/outputs flush to zero when `NJ = 1`. - **No VSCR[SAT] update.** VMX float ops never set saturation. - **No FPSCR effect.** Unlike scalar `fnmsub[s]`, `vnmsubfp` does not touch FPSCR. -- **NaN propagation.** A NaN in any of `VA`, `VB`, or `VC` yields a NaN in the corresponding lane. Sign-of-NaN is unspecified but stable in xenia (matches the x86 host's `vfnmadd`-family output). +- **NaN propagation.** A NaN in any of `VA`, `VB`, or `VC` yields a NaN in the corresponding lane. Sign-of-NaN is unspecified; in Canary it is whatever the host multiply-subtract produced, with the sign flipped by `Neg`. - **Big-endian lane indexing.** Lane 0 is the MSB-most 4 bytes. - **VMX128 sibling: [`vnmsubfp128`](vnmsubfp128.md).** Identical operation with access to `v0..v127`. - **No `Rc` bit** on this opcode; it never touches CR. @@ -171,7 +171,7 @@ int InstrEmit_vnmsubfp128(PPCHIRBuilder& f, const InstrData& i) { - [`vmaddfp`](vmaddfp.md) — the positive-rounded fused MAC `(VA × VC) + VB`. - [`vaddfp`](vaddfp.md), [`vsubfp`](vsubfp.md) — the underlying adds/subs. -- [`vmulfp`](vmulfp.md) — xenia-convenience lane-wise float multiply (no native Altivec form; usually encoded as `vmaddfp VD, VA, VC, v0_zero`). +- [`vmulfp128`](../vmx128/vmulfp128.md) — VMX128-only lane-wise float multiply (no standard Altivec form; there, `vmaddfp VD, VA, VC, v0_zero`). - [`vrefp`](vrefp.md), [`vrsqrtefp`](vrsqrtefp.md) — Newton iterations that pair with `vnmsubfp`. - [`vmaxfp`](vmaxfp.md), [`vminfp`](vminfp.md) — the other float-arithmetic primitives. diff --git a/tools/ppc-manual/vmx/vnor.md b/tools/ppc-manual/vmx/vnor.md index cb93a538..c421db49 100644 --- a/tools/ppc-manual/vmx/vnor.md +++ b/tools/ppc-manual/vmx/vnor.md @@ -161,11 +161,11 @@ int InstrEmit_vnor_(PPCHIRBuilder& f, uint32_t vd, uint32_t va, uint32_t vb) { ## Special Cases & Edge Conditions -- **Bitwise NOR across the full 128-bit register.** `VD = ~(VA | VB)`. The operation is lane-agnostic; PPC documents it per-bit, xenia implements it as four 32-bit lanes for convenience but the result is identical to 16-byte or 8-half-word decomposition. +- **Bitwise NOR across the full 128-bit register.** `VD = ~(VA | VB)`. The operation is lane-agnostic; PPC documents it per-bit, and Canary emits `Not(Or(VA, VB))` over the whole vector — identical to any 32-bit, 16-bit or 8-bit decomposition. - **`vnor VD, VA, VA` is the idiomatic `vnot`** (bitwise complement of `VA`). No dedicated `vnot` exists in base Altivec. - **Aliasing is legal.** `vnor v3, v3, v4` or `vnor v3, v3, v3` are well-defined and common. - **No flags.** No CR, XER, VSCR side-effect. -- **VMX128 sibling [`vnor128`](vnor128.md)** provides the same op with access to `v0..v127`; xenia shares the interpreter arm (`vmx_reg_triple` selects the right encoding helper). +- **VMX128 sibling `vnor128`** provides the same op with access to `v0..v127`; Canary's `vnor128` decodes the 7-bit indices and shares `vnor`'s body. - **Useful for mask inversion.** When a compare result needs to be inverted — e.g. "where not equal" — `vnor` of the compare result with itself is cheaper than a dedicated inversion. ## Related Instructions diff --git a/tools/ppc-manual/vmx/vor.md b/tools/ppc-manual/vmx/vor.md index fee03ec6..0ddfeb03 100644 --- a/tools/ppc-manual/vmx/vor.md +++ b/tools/ppc-manual/vmx/vor.md @@ -171,7 +171,7 @@ int InstrEmit_vor_(PPCHIRBuilder& f, uint32_t vd, uint32_t va, uint32_t vb) { ## Special Cases & Edge Conditions -- **Bitwise OR across the full 128-bit register.** Lane-agnostic; xenia implements it as four 32-bit lanes but the result is identical at any granularity. +- **Bitwise OR across the full 128-bit register.** Lane-agnostic; Canary emits one 128-bit `Or` (or a plain copy when `VA == VB`), identical at any granularity. - **`vor VD, VA, VA` is the idiomatic register move.** No dedicated "vmr" exists in base Altivec; compilers recognise the `vor v3, v4, v4` pattern as a move and schedule accordingly. - **Aliasing is legal.** `vor v3, v3, v4` merges the mask in `v4` into `v3`. - **No flags, no VSCR effect.** diff --git a/tools/ppc-manual/vmx/vperm.md b/tools/ppc-manual/vmx/vperm.md index 12c5dec9..7595d7aa 100644 --- a/tools/ppc-manual/vmx/vperm.md +++ b/tools/ppc-manual/vmx/vperm.md @@ -160,7 +160,7 @@ int InstrEmit_vperm128(PPCHIRBuilder& f, const InstrData& i) { - **Upper 3 bits of each `VC` byte are ignored.** Only bits 3..7 (the low 5) are consulted, so values like 0x1F and 0x5F both mean "byte 15 of VB". Software can use those upper bits for its own tagging. - **Pair with [`lvsl`](lvsl.md) / [`lvsr`](lvsr.md) for unaligned 16-byte loads.** `lvsl` produces the selector that shifts "left" by `EA & 0xF` bytes; feeding that into `vperm` with two aligned `lvx` results yields the unaligned 16-byte view. - **Aliasing legal.** `VD` may equal `VA` or `VB`. -- **VMX128 sibling [`vperm128`](vperm128.md).** Same shape with the 7-bit register file. The VMX128 encoding carries `VC` in the 3-bit `VC` sub-field of the `VX128_2` form — which only lets `VC` select one of **8** specific registers, not 128. In xenia's decoder this is `vc128()`. +- **VMX128 sibling `vperm128`.** Same shape with the 7-bit register file. The VMX128 encoding carries `VC` in the 3-bit `VC` sub-field of the `VX128_2` form — which only lets `VC` select one of **8** specific registers, not 128. Canary reads it as `VX128_2.VC`. - **No flags, no VSCR side-effect.** ## Related Instructions diff --git a/tools/ppc-manual/vmx/vpkpx.md b/tools/ppc-manual/vmx/vpkpx.md index 196d90ce..b5d355cd 100644 --- a/tools/ppc-manual/vmx/vpkpx.md +++ b/tools/ppc-manual/vmx/vpkpx.md @@ -118,7 +118,7 @@ int InstrEmit_vpkpx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Pack 4×4 pixel words → 8×16-bit 1-5-5-5 pixels.** For each 32-bit word lane, three bit-fields are sampled and concatenated into a 16-bit `1.5.5.5` (A.R.G.B) format, losing precision but not saturating. -- **Bit-layout of each output half-word.** Bit 0 of the output = bit 7 of the source byte (alpha); the next 5 bits come from the red channel's top 5 bits (bits 8..12 of the source word); then 5 bits of green (bits 16..20); then 5 bits of blue (bits 24..28). Xenia's helper is `vmx::pack_pixel_555` (in `crates/xenia-cpu/src/vmx.rs`). +- **Bit-layout of each output half-word.** Bit 0 of the output = bit 7 of the source byte (alpha); the next 5 bits come from the red channel's top 5 bits (bits 8..12 of the source word); then 5 bits of green (bits 16..20); then 5 bits of blue (bits 24..28). Canary builds it with `vkpkx_in_low` plus an unsigned 32→16 `Pack`; its comment reports agreement over a million random inputs. - **No saturation / no rounding.** The op truncates the lower bits of each channel; `VSCR[SAT]` is **not** affected. - **Big-endian lane order.** `VA`'s 4 words produce the first 4 output half-words (`VD.h[0..3]`); `VB`'s 4 words fill `VD.h[4..7]`. - **Paired with [`vupkhpx`](vupkhpx.md) / [`vupklpx`](vupklpx.md)** — these unpack a 1-5-5-5 pixel back into a word-lane `0x00RRGGBB`-like form for further arithmetic. diff --git a/tools/ppc-manual/vmx/vrefp.md b/tools/ppc-manual/vmx/vrefp.md index a0dee1b4..53786b45 100644 --- a/tools/ppc-manual/vmx/vrefp.md +++ b/tools/ppc-manual/vmx/vrefp.md @@ -156,7 +156,7 @@ int InstrEmit_vrefp_(PPCHIRBuilder& f, uint32_t vd, uint32_t vb) { ## Special Cases & Edge Conditions -- **Lane-wise reciprocal *estimate*.** Each 32-bit float lane of `VB` is approximated by `1.0 / VB[i]`. The PowerPC spec permits an **estimate** accurate to about 1/4096 (≈12 bits); xenia-rs produces the *exact* IEEE-754 reciprocal by dividing, trading accuracy for simplicity. Game code that cares about bit-reproducible behaviour should Newton-iterate with [`vnmsubfp`](vnmsubfp.md) regardless of which backend computes the seed. +- **Lane-wise reciprocal *estimate*.** Each 32-bit float lane of `VB` is approximated by `1.0 / VB[i]`. The PowerPC spec permits an **estimate** accurate to about 1/4096 (≈12 bits); Canary divides exactly (`vdivps` into `1.0`) — its comment notes that AVX `rcpps` misses that 1/4096 bound and broke gameplay in one title. Game code that cares about bit-reproducible behaviour should Newton-iterate with [`vnmsubfp`](vnmsubfp.md) regardless of which backend computes the seed. - **Standard Newton iteration.** `x₁ = x₀ * (2 − VB * x₀)`, expressible as `vnmsubfp x₁, x₀, VB, 2.0f` followed by `vmaddfp x₁, x₀, x₁, 0.0f` (or similar). One iteration roughly doubles the valid bit count. - **IEEE-754 binary32 lanes; `VSCR[NJ]` honoured** (denormals flush to zero when `NJ = 1`). - **No VSCR[SAT] update, no FPSCR update, no exception.** Division by zero yields ±∞; division of zero yields ±∞ too (same sign convention). diff --git a/tools/ppc-manual/vmx/vrfin.md b/tools/ppc-manual/vmx/vrfin.md index 4a6f4beb..d2abe035 100644 --- a/tools/ppc-manual/vmx/vrfin.md +++ b/tools/ppc-manual/vmx/vrfin.md @@ -156,7 +156,7 @@ int InstrEmit_vrfin_(PPCHIRBuilder& f, uint32_t vd, uint32_t vb) { ## Special Cases & Edge Conditions -- **Round to nearest integer.** Each 32-bit float lane of `VB` is rounded to the nearest representable integer value. Xenia-rs uses Rust's `f32::round`, which rounds half-away-from-zero; the hardware Xenon actually implements round-ties-to-even. This is a known small mismatch tracked in xenia. +- **Round to nearest integer.** Each 32-bit float lane of `VB` is rounded to the nearest representable integer value, ties to even on Xenon. Canary emits `vroundps` with rounding mode 0, which is round-half-to-even — a match. - **IEEE-754 binary32 output; `VSCR[NJ]` honoured.** - **Integer-too-big lanes are no-ops** (|x| ≥ 2²³). - **NaN and ±∞** pass through unchanged. diff --git a/tools/ppc-manual/vmx/vrsqrtefp.md b/tools/ppc-manual/vmx/vrsqrtefp.md index d85c6395..e977f1b3 100644 --- a/tools/ppc-manual/vmx/vrsqrtefp.md +++ b/tools/ppc-manual/vmx/vrsqrtefp.md @@ -162,7 +162,7 @@ int InstrEmit_vrsqrtefp_(PPCHIRBuilder& f, uint32_t vd, uint32_t vb) { ## Special Cases & Edge Conditions -- **Lane-wise reciprocal-square-root *estimate*.** Each 32-bit float lane of `VB` is approximated by `1.0 / sqrt(VB[i])`. The PowerPC spec permits a 12-bit estimate; xenia-rs computes the exact IEEE-754 result. Games that depend on Xenon's low-precision estimate may need a helper to truncate bits to match hardware. +- **Lane-wise reciprocal-square-root *estimate*.** Each 32-bit float lane of `VB` is approximated by `1.0 / sqrt(VB[i])`. The PowerPC spec permits a 12-bit estimate; Canary's x64 backend also returns an estimate rather than the exact value, from table-driven helpers (`vrsqrtefp_scalar_helper` / `vrsqrtefp_vector_helper`, built on `GetNormalVRsqrteTable`). Whether that matches Xenon bit-for-bit is unverified. - **Standard Newton iteration (Quake-style):** `x₁ = x₀ * (1.5 − 0.5 * VB * x₀²)`. One pass produces ~24 bits of precision — essentially indistinguishable from a true `1/sqrt`. - **Negative input is a trap** in math terms but not in ISA terms: the hardware returns a QNaN. `sqrt(−x)` for `x > 0` → QNaN. Zero produces `+∞` (and may sticky-set no bits). - **IEEE-754 binary32; `VSCR[NJ]` honoured.** diff --git a/tools/ppc-manual/vmx/vsel.md b/tools/ppc-manual/vmx/vsel.md index cb3c3922..6b4ff399 100644 --- a/tools/ppc-manual/vmx/vsel.md +++ b/tools/ppc-manual/vmx/vsel.md @@ -170,7 +170,7 @@ int InstrEmit_vsel_(PPCHIRBuilder& f, uint32_t vd, uint32_t va, uint32_t vb, - **Per-bit select.** `VD = (VA & ~VC) | (VB & VC)`. Evaluated bit-by-bit across the full 128-bit register — not per-lane. Any granularity (byte / half / word) is valid because each bit is independent. - **Classic "bitwise conditional move".** The canonical use is: compare produces an all-ones / all-zeros mask in `VC`, then `vsel` picks between two data vectors. Because the mask is all-or-nothing per lane, `vsel` behaves identically to a per-lane conditional move in that common case. - **Mask does not need to be all-ones / all-zeros.** Partial masks produce interleaved bits, which is useful for bitfield merges. -- **`vsel128` read pattern is atypical:** the destination `VD` is **also an input**. The VMX128 encoding reuses the destination's 7 bits to carry one of the three source operands (xenia's interpreter arm handles this — see `vsel128` Register Effects above). Compilers express this as `vsel v3, v4, v5, v3` even though `v3` is also the destination. +- **`vsel128` read pattern is atypical:** the destination `VD` is **also an input**. The VMX128 encoding reuses the destination's 7 bits to carry one of the three source operands — Canary's `vsel128` passes `VD` as the select mask `VC`. Compilers express this as `vsel v3, v4, v5, v3` even though `v3` is also the destination. - **No flags, no VSCR.** No dedicated VMX128 separate-control-register sibling; `vsel128` covers the VMX128 case. - **Cheaper than `vand` + `vandc` + `vor`.** `vsel` is a single-cycle primitive on Xenon. diff --git a/tools/ppc-manual/vmx/vsl.md b/tools/ppc-manual/vmx/vsl.md index 0bb2faf7..7a459f3e 100644 --- a/tools/ppc-manual/vmx/vsl.md +++ b/tools/ppc-manual/vmx/vsl.md @@ -109,7 +109,7 @@ int InstrEmit_vsl(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Whole-register bit shift-left.** The 128-bit value `VA` is shifted left by `N` bits, where `N = VB.b[15] & 7` — i.e. the low 3 bits of the **last** (least-significant) byte of `VB`. Bits shifted out the top are discarded; zero-fill on the right. -- **Shift count constraint.** The ISA requires the same 3-bit shift count in all 16 bytes of `VB`; behaviour is undefined otherwise (xenia-rs reads only byte 15 as above). Compilers guarantee this by splatting the shift count first. +- **Shift count constraint.** The ISA requires the same 3-bit shift count in all 16 bytes of `VB`; behaviour is undefined otherwise (Canary reads only byte 15, `Extract(VB, 15) & 7`). Compilers guarantee this by splatting the shift count first. - **Combine with [`vslo`](vslo.md) for up to 127-bit shifts.** `vslo` handles the byte-granular component; `vsl` picks up the remaining 0..7 bits. The canonical 128-bit shift-left is `vslo` followed by `vsl`. - **Big-endian.** "Left" means toward the MSB end of the register. - **No flags, no VSCR.** diff --git a/tools/ppc-manual/vmx/vslo.md b/tools/ppc-manual/vmx/vslo.md index e16f4abf..f70cc977 100644 --- a/tools/ppc-manual/vmx/vslo.md +++ b/tools/ppc-manual/vmx/vslo.md @@ -172,7 +172,7 @@ int InstrEmit_vslo_(PPCHIRBuilder& f, uint32_t vd, uint32_t va, uint32_t vb) { ## Special Cases & Edge Conditions - **Whole-register shift-left by octets (bytes).** `VA` is shifted left by `N` bytes, where `N = (VB.b[15] >> 3) & 0xF` — bits 1..4 of the last byte of `VB`. Right end is zero-filled. `N` saturates at 15 because only 4 bits are honoured. -- **Shift count constraint.** The ISA mandates a uniform 4-bit count across all of `VB`; xenia-rs reads only byte 15. Splat with [`vspltb`](vspltb.md) before invoking when the count is derived dynamically. +- **Shift count constraint.** The ISA mandates a uniform 4-bit count across all of `VB`; Canary reads only byte 15 (`(VB.b[15] & 0x78) >> 3`). Splat with [`vspltb`](vspltb.md) before invoking when the count is derived dynamically. - **Pair with [`vsl`](vsl.md) for full bit-level shifts.** `vslo` contributes the byte-granular part; `vsl` contributes the 0..7 residual bits. - **Big-endian.** "Left" = toward MSB = toward `VD.b[0]`. - **No flags, no VSCR.** diff --git a/tools/ppc-manual/vmx/vsr.md b/tools/ppc-manual/vmx/vsr.md index 58ef8e2d..7efd057c 100644 --- a/tools/ppc-manual/vmx/vsr.md +++ b/tools/ppc-manual/vmx/vsr.md @@ -110,7 +110,7 @@ int InstrEmit_vsr(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Whole-register bit shift-right.** The 128-bit value `VA` is shifted right (toward the LSB end) by `N` bits, where `N = VB.b[15] & 7` — the low 3 bits of the last byte of `VB`. Bits shifted out the bottom are discarded; zero-fill on the top. -- **Shift count constraint.** The ISA mandates the same 3-bit count across all of `VB`; xenia-rs reads only byte 15. Splat the count before use. +- **Shift count constraint.** The ISA mandates the same 3-bit count across all of `VB`; Canary reads only byte 15. Splat the count before use. - **Pair with [`vsro`](vsro.md) for up to 127-bit shifts.** `vsro` contributes the byte-granular component; `vsr` the 0..7 residual bits. - **Big-endian.** "Right" means toward the LSB end (`VD.b[15]`). - **No flags, no VSCR.** diff --git a/tools/ppc-manual/vmx/vsro.md b/tools/ppc-manual/vmx/vsro.md index 0b039f8f..0d3b5eb3 100644 --- a/tools/ppc-manual/vmx/vsro.md +++ b/tools/ppc-manual/vmx/vsro.md @@ -172,7 +172,7 @@ int InstrEmit_vsro_(PPCHIRBuilder& f, uint32_t vd, uint32_t va, uint32_t vb) { ## Special Cases & Edge Conditions - **Whole-register shift-right by octets (bytes).** `VA` is shifted right by `N` bytes, where `N = (VB.b[15] >> 3) & 0xF`. Top end is zero-filled. -- **Shift count constraint.** Uniform 4-bit count required across `VB`; xenia reads only byte 15. +- **Shift count constraint.** Uniform 4-bit count required across `VB`; Canary reads only byte 15. - **Pair with [`vsr`](vsr.md) for full bit-level shifts.** `vsro` handles bytes; `vsr` handles the 0..7 residual. - **Big-endian.** "Right" = toward LSB end (`VD.b[15]`). - **No flags, no VSCR.** diff --git a/tools/ppc-manual/vmx/vsubsbs.md b/tools/ppc-manual/vmx/vsubsbs.md index bfdba8fb..2d1ba92e 100644 --- a/tools/ppc-manual/vmx/vsubsbs.md +++ b/tools/ppc-manual/vmx/vsubsbs.md @@ -107,7 +107,7 @@ int InstrEmit_vsubsbs(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Signed-byte saturating subtract.** `VD.b[i] = clamp_int8(VA.b[i] − VB.b[i])`. Each lane computed as `int8`; any lane that goes below `−128` or above `+127` is clamped and sticky-sets `VSCR[SAT]`. Xenia's helper is `vmx::sat_sub_i8`. +- **Signed-byte saturating subtract.** `VD.b[i] = clamp_int8(VA.b[i] − VB.b[i])`. Each lane computed as `int8`; any lane that goes below `−128` or above `+127` is clamped and sticky-sets `VSCR[SAT]`. Canary clamps the lanes (`VectorSub` with `ARITHMETIC_SATURATE`) but never records `SAT` (x64 `DID_SATURATE` is a stub). - **Sticky VSCR[SAT].** Once set it remains set until explicit `mtvscr` clear. - **Big-endian byte lanes.** - **No `Rc`, no XER.** diff --git a/tools/ppc-manual/vmx/vsubshs.md b/tools/ppc-manual/vmx/vsubshs.md index af28df1b..318114f4 100644 --- a/tools/ppc-manual/vmx/vsubshs.md +++ b/tools/ppc-manual/vmx/vsubshs.md @@ -107,7 +107,7 @@ int InstrEmit_vsubshs(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Signed half-word saturating subtract.** `VD.h[i] = clamp_int16(VA.h[i] − VB.h[i])` for 8 lanes. Overflow clamps to `±0x7FFF` and sticky-sets `VSCR[SAT]`. Xenia uses `vmx::sat_sub_i16`. +- **Signed half-word saturating subtract.** `VD.h[i] = clamp_int16(VA.h[i] − VB.h[i])` for 8 lanes. Overflow clamps to `±0x7FFF` and sticky-sets `VSCR[SAT]`. Canary clamps the lanes (`VectorSub` with `ARITHMETIC_SATURATE`) but never records `SAT` (x64 `DID_SATURATE` is a stub). - **Sticky VSCR[SAT].** - **Big-endian half-word lanes.** - **No `Rc`, no XER.** diff --git a/tools/ppc-manual/vmx/vsubsws.md b/tools/ppc-manual/vmx/vsubsws.md index fcdd2933..58c5b54b 100644 --- a/tools/ppc-manual/vmx/vsubsws.md +++ b/tools/ppc-manual/vmx/vsubsws.md @@ -107,7 +107,7 @@ int InstrEmit_vsubsws(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Signed word saturating subtract.** `VD.w[i] = clamp_int32(VA.w[i] − VB.w[i])` for 4 lanes. Overflow clamps to `±0x7FFFFFFF` and sticky-sets `VSCR[SAT]`. Xenia uses `vmx::sat_sub_i32`. +- **Signed word saturating subtract.** `VD.w[i] = clamp_int32(VA.w[i] − VB.w[i])` for 4 lanes. Overflow clamps to `±0x7FFFFFFF` and sticky-sets `VSCR[SAT]`. Canary clamps the lanes (`VectorSub` with `ARITHMETIC_SATURATE`) but never records `SAT` (x64 `DID_SATURATE` is a stub). - **Sticky VSCR[SAT].** - **Big-endian word lanes.** - **No `Rc`, no XER.** diff --git a/tools/ppc-manual/vmx/vsububs.md b/tools/ppc-manual/vmx/vsububs.md index 7ddbe531..1a66513c 100644 --- a/tools/ppc-manual/vmx/vsububs.md +++ b/tools/ppc-manual/vmx/vsububs.md @@ -107,7 +107,7 @@ int InstrEmit_vsububs(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Unsigned-byte saturating subtract.** `VD.b[i] = clamp_u8(VA.b[i] − VB.b[i])` per lane. Negative results clamp to 0 and sticky-set `VSCR[SAT]`. Xenia uses `vmx::sat_sub_u8`. +- **Unsigned-byte saturating subtract.** `VD.b[i] = clamp_u8(VA.b[i] − VB.b[i])` per lane. Negative results clamp to 0 and sticky-set `VSCR[SAT]`. Canary clamps the lanes (`VectorSub` with `ARITHMETIC_SATURATE`) but never records `SAT` (x64 `DID_SATURATE` is a stub). - **Sticky VSCR[SAT].** - **Common image-processing primitive.** "Floor at zero" for per-channel differences (alpha compositing, edge detection, etc.). - **Big-endian byte lanes.** diff --git a/tools/ppc-manual/vmx/vsubuhs.md b/tools/ppc-manual/vmx/vsubuhs.md index d9666e60..afd2a866 100644 --- a/tools/ppc-manual/vmx/vsubuhs.md +++ b/tools/ppc-manual/vmx/vsubuhs.md @@ -107,7 +107,7 @@ int InstrEmit_vsubuhs(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Unsigned half-word saturating subtract.** `VD.h[i] = clamp_u16(VA.h[i] − VB.h[i])` per lane. Negative results clamp to 0 and sticky-set `VSCR[SAT]`. Xenia uses `vmx::sat_sub_u16`. +- **Unsigned half-word saturating subtract.** `VD.h[i] = clamp_u16(VA.h[i] − VB.h[i])` per lane. Negative results clamp to 0 and sticky-set `VSCR[SAT]`. Canary clamps the lanes (`VectorSub` with `ARITHMETIC_SATURATE`) but never records `SAT` (x64 `DID_SATURATE` is a stub). - **Sticky VSCR[SAT].** - **Big-endian half-word lanes.** - **No `Rc`, no XER.** diff --git a/tools/ppc-manual/vmx/vsubuws.md b/tools/ppc-manual/vmx/vsubuws.md index 332ed39a..20ee0b83 100644 --- a/tools/ppc-manual/vmx/vsubuws.md +++ b/tools/ppc-manual/vmx/vsubuws.md @@ -107,7 +107,7 @@ int InstrEmit_vsubuws(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Unsigned word saturating subtract.** `VD.w[i] = clamp_u32(VA.w[i] − VB.w[i])` per lane. Negative results clamp to 0 and sticky-set `VSCR[SAT]`. Xenia uses `vmx::sat_sub_u32`. +- **Unsigned word saturating subtract.** `VD.w[i] = clamp_u32(VA.w[i] − VB.w[i])` per lane. Negative results clamp to 0 and sticky-set `VSCR[SAT]`. Canary clamps the lanes (`VectorSub` with `ARITHMETIC_SATURATE`) but never records `SAT` (x64 `DID_SATURATE` is a stub). - **Sticky VSCR[SAT].** - **Big-endian word lanes.** - **No `Rc`, no XER.** diff --git a/tools/ppc-manual/vmx/vupkhpx.md b/tools/ppc-manual/vmx/vupkhpx.md index e96ac38d..8d61a3d0 100644 --- a/tools/ppc-manual/vmx/vupkhpx.md +++ b/tools/ppc-manual/vmx/vupkhpx.md @@ -101,7 +101,7 @@ int InstrEmit_vupkhpx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Unpack the high 4 of 8 pixel half-words.** The upper half-words of `VB` (`VB.h[0..3]`) are each decoded from 1-5-5-5 pixel format into a 32-bit word (`1.5.5.5 → 1.8.8.8` with sign-extension on the alpha bit and zero-extension of each colour channel into the high 5 bits of a byte). Xenia uses `vmx::unpack_pixel_555`. +- **Unpack the high 4 of 8 pixel half-words.** The upper half-words of `VB` (`VB.h[0..3]`) are each decoded from 1-5-5-5 pixel format into a 32-bit word (`1.5.5.5 → 1.8.8.8` with sign-extension on the alpha bit and zero-extension of each colour channel into the high 5 bits of a byte). ⚠️ Canary does not implement `vupkhpx`: its emitter is `XEINSTRNOTIMPLEMENTED`, so translating one logs "Unimplemented instr" and, with the default `break_on_unimplemented_instructions`, breaks. - **Output layout.** `VD.w[0..3]` receive the 4 decoded pixels in big-endian order. - **Inverse of the high half of [`vpkpx`](vpkpx.md).** Unpacking loses no information beyond what the 1-5-5-5 format allows. - **No saturation, no flags, no VSCR.** diff --git a/tools/ppc-manual/vmx/vupklpx.md b/tools/ppc-manual/vmx/vupklpx.md index 53fe00a1..cd6a31e3 100644 --- a/tools/ppc-manual/vmx/vupklpx.md +++ b/tools/ppc-manual/vmx/vupklpx.md @@ -101,7 +101,7 @@ int InstrEmit_vupklpx(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Unpack the low 4 of 8 pixel half-words.** The lower half-words of `VB` (`VB.h[4..7]`) are each decoded from 1-5-5-5 pixel format into a 32-bit word (`1.5.5.5 → 1.8.8.8`). Xenia uses `vmx::unpack_pixel_555`. +- **Unpack the low 4 of 8 pixel half-words.** The lower half-words of `VB` (`VB.h[4..7]`) are each decoded from 1-5-5-5 pixel format into a 32-bit word (`1.5.5.5 → 1.8.8.8`). ⚠️ Canary does not implement `vupklpx`: its emitter is `XEINSTRNOTIMPLEMENTED`, so translating one logs "Unimplemented instr" and, with the default `break_on_unimplemented_instructions`, breaks. - **Output layout.** `VD.w[0..3]` receive the 4 decoded pixels in big-endian order. - **Inverse of the low half of [`vpkpx`](vpkpx.md).** - **No saturation, no flags, no VSCR.** diff --git a/tools/ppc-manual/vmx/vxor.md b/tools/ppc-manual/vmx/vxor.md index f641f16e..7df3a786 100644 --- a/tools/ppc-manual/vmx/vxor.md +++ b/tools/ppc-manual/vmx/vxor.md @@ -174,7 +174,7 @@ int InstrEmit_vxor_(PPCHIRBuilder& f, uint32_t vd, uint32_t va, uint32_t vb) { ## Special Cases & Edge Conditions - **Bitwise XOR across the full 128-bit register.** Lane-agnostic. -- **`vxor VD, VD, VD` is the canonical "vector zero" idiom.** Every Xenon compiler uses this to materialise the all-zero vector; xenia-rs's interpreter does not special-case it (it still reads the register), but the JIT / translator can fold it at emit time. +- **`vxor VD, VD, VD` is the canonical "vector zero" idiom.** Every Xenon compiler uses this to materialise the all-zero vector; Canary special-cases it at translate time — when `VA == VB` it stores a zero vector without reading the register. - **Aliasing legal.** `vxor v3, v3, v4` toggles bits from `v4` into `v3`. - **No flags, no VSCR.** - **VMX128 sibling [`vxor128`](vxor128.md).** Identical semantics; wider register file. diff --git a/tools/ppc-manual/vmx128/vcfpsxws128.md b/tools/ppc-manual/vmx128/vcfpsxws128.md index f46c9e6b..0b983764 100644 --- a/tools/ppc-manual/vmx128/vcfpsxws128.md +++ b/tools/ppc-manual/vmx128/vcfpsxws128.md @@ -119,7 +119,7 @@ int InstrEmit_vctsxs_(PPCHIRBuilder& f, uint32_t vd, uint32_t vb, - **Float → signed fixed-point (int32) with explicit scale.** Each lane computes `VD.w[i] = sat_int32(VB[i] * 2^UIMM)`, truncating toward zero and clamping to `[−2^31, 2^31−1]`. `UIMM` is a 5-bit unsigned bias (range 0..31) that specifies a power-of-two pre-scale on the float value. - **Use case: fixed-point pipelines.** The `UIMM` pre-scale lets game code convert a `[0.0, 1.0]` float channel into a `uint16`-range fixed-point value in one instruction (e.g. `UIMM = 15` → scale by 32768). -- **Sticky VSCR[SAT]** set whenever a lane clamps (including NaN inputs, which xenia's `cvt_f32_to_i32_sat` treats as 0 and flags saturation). +- **Sticky VSCR[SAT]** set on hardware whenever a lane clamps. Canary shares `vctsxs`'s body: NaN lanes become 0, out-of-range lanes saturate, and `SAT` is never recorded. - **`VSCR[NJ]` honoured** on the float input side. - **VMX128 register-fusion** applies to `VD` and `VB`: 7-bit register IDs via `VD128l ‖ VD128h` and `VB128l ‖ VB128h`. - **No IBM AIX entry** — this is Xenon-only. The closest standard Altivec op is [`vctsxs`](../vmx/vctsxs.md). diff --git a/tools/ppc-manual/vmx128/vcfpuxws128.md b/tools/ppc-manual/vmx128/vcfpuxws128.md index a84c6ecc..2e3a824a 100644 --- a/tools/ppc-manual/vmx128/vcfpuxws128.md +++ b/tools/ppc-manual/vmx128/vcfpuxws128.md @@ -119,7 +119,7 @@ int InstrEmit_vctuxs_(PPCHIRBuilder& f, uint32_t vd, uint32_t vb, - **Float → unsigned fixed-point (uint32) with explicit scale.** Each lane computes `VD.w[i] = sat_uint32(VB[i] * 2^UIMM)`, truncating toward zero and clamping to `[0, 2^32−1]`. `UIMM` is a 5-bit unsigned bias (range 0..31). - **Negative floats clamp to 0** and sticky-set `VSCR[SAT]`. -- **NaN inputs** → 0 with `VSCR[SAT]` set (xenia's `cvt_f32_to_u32_sat`). +- **NaN inputs** → 0. Canary shares `vctuxs`'s body, whose `vmaxps` against zero turns NaN into 0; it never records `VSCR[SAT]`. - **`VSCR[NJ]` honoured** for denormal inputs. - **VMX128 register-fusion** applies to `VD` and `VB` (7-bit IDs). - **No IBM AIX entry** — Xenon-only. diff --git a/tools/ppc-manual/vmx128/vmaddcfp128.md b/tools/ppc-manual/vmx128/vmaddcfp128.md index 31226042..f9c2e175 100644 --- a/tools/ppc-manual/vmx128/vmaddcfp128.md +++ b/tools/ppc-manual/vmx128/vmaddcfp128.md @@ -109,8 +109,8 @@ int InstrEmit_vmaddcfp128(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Xbox-specific fused multiply-add variant.** Each lane computes `VD[i] = VD[i] * VB[i] + VA[i]` — note that `VD` is both source and destination (xenia reads `VD` first, then writes). This is *not* the standard [`vmaddfp`](../vmx/vmaddfp.md) operand order: the "addend" position is `VA`, the other factor is `VB`, and `VD` carries the on-going accumulator. The mnemonic's trailing `c` denotes "accumulator-in-VD" rather than a separate `VC` operand. -- **Fused, single-rounding.** Xenia uses `f32::mul_add`, which maps to a host FMA instruction when available. Bit-for-bit result depends on host support; xenia-canary's LLVM path emits the equivalent IR node. +- **Xbox-specific fused multiply-add variant.** Canary computes each lane as `VD[i] = VA[i] * VD[i] + VB[i]` (its comment: `(VD) <- ((VA) * (VD)) + (VB)`) — `VD` is both a factor and the destination. This is *not* the standard [`vmaddfp`](../vmx/vmaddfp.md) operand order: `VD` stands where `vmaddfp` has `VC`, and `VB` is the addend. Whether Xenon uses the same order has not been checked on hardware. +- **Fused, single-rounding.** Canary emits `MulAdd`, which is `vfmadd213ps` on FMA3 hosts and `vmulps` + `vaddps` (two roundings) otherwise, so the bit-exact result depends on host support. - **IEEE-754 binary32 lanes; `VSCR[NJ]` honoured.** - **No VSCR[SAT], no FPSCR update.** - **NaN propagation** per IEEE-754. diff --git a/tools/ppc-manual/vmx128/vmsum3fp128.md b/tools/ppc-manual/vmx128/vmsum3fp128.md index 7ad8a9e1..21f5fffb 100644 --- a/tools/ppc-manual/vmx128/vmsum3fp128.md +++ b/tools/ppc-manual/vmx128/vmsum3fp128.md @@ -114,7 +114,7 @@ int InstrEmit_vmsum3fp128(PPCHIRBuilder& f, const InstrData& i) { - **3-way float dot product.** Computes `s = VA[0]*VB[0] + VA[1]*VB[1] + VA[2]*VB[2]` (ignoring lane 3 — the "w" component of a homogeneous vector) and **broadcasts `s` to every lane of `VD`**. Typical call site: 3D vector dot products where the w-component is padding. - **Scalar-result-splatted-across-lanes.** Consuming code can then use any lane of `VD` as the dot-product result. -- **Rounding.** Xenia performs two adds in sequence (no fused triple-add in Rust). The order matches the spec but the summation order affects round-off by ~1 ulp. Games that need deterministic cross-host behaviour typically pre-scale their inputs. +- **Rounding.** Canary widens the lanes to binary64, multiplies and sums there, and rounds to binary32 once at the end (`vcvtsd2ss`), so single-precision summation order does not come into it. Its emitter comment adds that denormal results are made 0 unconditionally. - **IEEE-754 binary32; `VSCR[NJ]` honoured.** - **No VSCR[SAT], no FPSCR update.** - **VMX128 register-fusion** (7-bit IDs on `VA`, `VB`, `VD`). diff --git a/tools/ppc-manual/vmx128/vmsum4fp128.md b/tools/ppc-manual/vmx128/vmsum4fp128.md index 28920796..af5b2ac4 100644 --- a/tools/ppc-manual/vmx128/vmsum4fp128.md +++ b/tools/ppc-manual/vmx128/vmsum4fp128.md @@ -113,7 +113,7 @@ int InstrEmit_vmsum4fp128(PPCHIRBuilder& f, const InstrData& i) { - **4-way float dot product.** Computes `s = VA[0]*VB[0] + VA[1]*VB[1] + VA[2]*VB[2] + VA[3]*VB[3]` (the full xyzw dot) and **broadcasts `s` to every lane of `VD`**. - **Scalar-result-splatted-across-lanes.** Direct mirror of HLSL/GLSL's `float4 dot`. -- **Rounding.** Three sequential adds; round-off order affects result by ~1 ulp. Not an FMA in xenia. +- **Rounding.** Canary widens to binary64, multiplies and sums all four products there, and rounds to binary32 once at the end (`vcvtsd2ss`). Not an FMA. - **IEEE-754 binary32; `VSCR[NJ]` honoured.** - **No VSCR[SAT], no FPSCR update.** - **VMX128 register-fusion** (7-bit IDs on `VA`, `VB`, `VD`). diff --git a/tools/ppc-manual/vmx128/vpermwi128.md b/tools/ppc-manual/vmx128/vpermwi128.md index 856e37df..1ff16d85 100644 --- a/tools/ppc-manual/vmx128/vpermwi128.md +++ b/tools/ppc-manual/vmx128/vpermwi128.md @@ -115,7 +115,7 @@ int InstrEmit_vpermwi128(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Word-level 4-way permute via an 8-bit immediate.** The 8-bit `PERM` immediate (carried in fields `PERMh ‖ PERMl` of the encoding) is treated as **four 2-bit selectors**, one per output word lane. Each 2-bit field selects which of `VB`'s 4 word lanes is copied to the corresponding output lane. -- **Bit layout of the immediate.** Output lane 0 (big-endian MSB word) is selected by bits 6–7 of `PERM`; lane 1 by bits 4–5; lane 2 by bits 2–3; lane 3 by bits 0–1. (In xenia: `sel = (imm >> (2 * (3-i))) & 3`.) +- **Bit layout of the immediate.** Output lane 0 (big-endian MSB word) is selected by bits 6–7 of `PERM`; lane 1 by bits 4–5; lane 2 by bits 2–3; lane 3 by bits 0–1. (In Canary: `MakeSwizzleMask(uimm >> 6, uimm >> 4, uimm >> 2, uimm >> 0)`.) - **Super-set of [`vspltw`](../vmx/vspltw.md).** A splat is `vpermwi128 vD, vB, 0x00` (all lanes = word 0), `0x55` (all = word 1), `0xAA` (all = word 2), `0xFF` (all = word 3). Arbitrary shuffles like "xyzw → wzyx" are a single-instruction operation. - **Immediate-only.** No dynamic selector vector; contrast with [`vperm`](../vmx/vperm.md). - **Single-source.** Unlike `vperm`/`vperm128`, `vpermwi128` only reshuffles one register (`VB`); it cannot interleave two operands. diff --git a/tools/ppc-manual/vmx128/vpkd3d128.md b/tools/ppc-manual/vmx128/vpkd3d128.md index d4e6c2d5..fb7e3c01 100644 --- a/tools/ppc-manual/vmx128/vpkd3d128.md +++ b/tools/ppc-manual/vmx128/vpkd3d128.md @@ -205,10 +205,10 @@ int InstrEmit_vpkd3d128(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Pack four float lanes into a single D3D-format 32-bit word.** The `IMM` field and the `z` sub-operation selector (together carried in bits 6–10 of the encoding in xenia's layout) choose *which* D3D format to emit: - - `D3dColor` — pack 4×float `[0.0, 1.0]` lanes into a 32-bit RGBA8 (A in high byte, B in low byte) — the canonical Direct3D 9 `D3DCOLOR` format. Xenia's helper is `vmx::pack_d3dcolor`. - - Other formats (RGBA16, compressed colour, etc.) are not yet implemented in xenia-rs; the interpreter logs a warning and passes through unchanged. -- **Also performs rotate-left-immediate and mask-insert.** The mnemonic is "Pack D3Dtype, Rotate Left Immediate and Mask Insert": the result of the pack step is rotated and merged into an existing `VD` under an immediate mask. Xenia currently emits only the pack step and overwrites `VD` wholesale; games rarely rely on the rotate-and-insert aspect. +- **Pack four float lanes into a single D3D-format 32-bit word.** In Canary's decoding (`VX128_4` form), `IMM >> 2` chooses *which* D3D format to emit, `IMM & 3` how wide a slot it goes into, and `z` the lane shift: + - `D3dColor` — pack 4×float `[0.0, 1.0]` lanes into a 32-bit RGBA8 (A in high byte, B in low byte) — the canonical Direct3D 9 `D3DCOLOR` format. Canary emits `Pack(PACK_TYPE_D3DCOLOR)`. + - Canary also packs `NORMSHORT2`, `NORMPACKED32` (2:10:10:10), `FLOAT16_2`, `NORMSHORT4`, `FLOAT16_4` and `NORMPACKED64` (4:20:20:20); format 7 is unhandled and reported as an unimplemented instruction. +- **Also merges into the existing `VD`.** The mnemonic is "Pack D3Dtype, Rotate Left Immediate and Mask Insert": the result of the pack step is placed into an existing `VD` rather than overwriting it. Canary does this with a permute whose control depends on `IMM & 3` and `z`, keeping the other lanes of `VD`. - **Sub-operation via the `z` field** (2 bits) + `IMM` (5 bits) gives 7 bits of format selection; the practical set used by Xenon games is small (D3DCOLOR is the dominant one). - **No saturation signal.** The packer saturates floats beyond `[0.0, 1.0]` silently; `VSCR[SAT]` is not touched. - **VMX128 register-fusion** on `VD` and `VB`. diff --git a/tools/ppc-manual/vmx128/vrlimi128.md b/tools/ppc-manual/vmx128/vrlimi128.md index 9a047115..e38d1d39 100644 --- a/tools/ppc-manual/vmx128/vrlimi128.md +++ b/tools/ppc-manual/vmx128/vrlimi128.md @@ -146,10 +146,10 @@ int InstrEmit_vrlimi128(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions -- **Rotate-left-word + mask-insert in one step.** `VB` is rotated left by `IMM & 3` word positions (word-granular, 0..3 — not bits). The resulting rotated vector is merged into the pre-existing `VD` under control of a 4-bit "insert mask" (`fmask`, from bits 26–29 of the encoding in xenia's layout): mask bit `i` = 1 keeps lane `i` from the rotated `VB`; mask bit = 0 keeps lane `i` from the old `VD`. +- **Rotate-left-word + mask-insert in one step.** `VB` is rotated left by `z` word positions (word-granular, 0..3 — not bits). The resulting rotated vector is merged into the pre-existing `VD` under control of a 4-bit "insert mask" (Canary's `VX128_4.IMM`, most-significant bit for lane 0): mask bit = 1 takes the lane from the rotated `VB`; mask bit = 0 keeps the lane from the old `VD`. - **Destructive destination.** `VD` is both source and destination — software must preserve its value or pre-initialise it. - **Typical use: selective-lane overwrite.** Games use this to "rewrite lane `n` of a vector with a shuffled component" without a full permute. A common pattern is "insert a scalar into lane `i` of a vector" where the scalar has been pre-loaded to a known word of `VB`. -- **Mask bit ↔ lane mapping.** Big-endian: mask bit 3 (MSB of the 4-bit mask) controls lane 0; bit 0 controls lane 3. (In xenia: `use_rot = (mask >> (3 − i)) & 1`.) +- **Mask bit ↔ lane mapping.** Big-endian: mask bit 3 (MSB of the 4-bit mask) controls lane 0; bit 0 controls lane 3. (In Canary: lane `i` takes the rotated `VB` when `(IMM >> (3 − i)) & 1`.) - **VMX128 register-fusion** on `VD` and `VB`. - **No IBM AIX entry** — Xenon-only. - **No `Rc`, no XER, no VSCR.** diff --git a/tools/ppc-manual/vmx128/vupkd3d128.md b/tools/ppc-manual/vmx128/vupkd3d128.md index 4bd9467d..9946fc2a 100644 --- a/tools/ppc-manual/vmx128/vupkd3d128.md +++ b/tools/ppc-manual/vmx128/vupkd3d128.md @@ -139,8 +139,8 @@ int InstrEmit_vupkd3d128(PPCHIRBuilder& f, const InstrData& i) { ## Special Cases & Edge Conditions - **Unpack a D3D-format word into 4 float lanes.** The `IMM` field in the encoding selects the target format: - - `D3dColor` — decode a 32-bit RGBA8 (`D3DCOLOR`) into 4 float lanes in `[0.0, 1.0]`. Xenia's helper is `vmx::unpack_d3dcolor`. - - Other formats (UBYTE4N, SHORT2N, etc.) are not yet implemented in xenia-rs; the interpreter logs a warning and passes `VB` through unchanged. + - `D3dColor` — decode a 32-bit RGBA8 (`D3DCOLOR`) into 4 float lanes in `[0.0, 1.0]`. Canary emits `Unpack(PACK_TYPE_D3DCOLOR)`. + - Canary also unpacks `NORMSHORT2`, `NORMPACKED32` (2:10:10:10), `FLOAT16_2`, `NORMSHORT4`, `FLOAT16_4` and `NORMPACKED64` (4:20:20:20); format 7 is unhandled and reported as an unimplemented instruction. - **Inverse of [`vpkd3d128`](vpkd3d128.md).** The same format code used to pack must be used to unpack. - **Source-width is a single 32-bit word** of `VB` (typically lane 0; the helpers read the appropriate component). The other three input word lanes are ignored for `D3DCOLOR`. - **IEEE-754 binary32 outputs,** already normalised to `[0.0, 1.0]` (integer value divided by 255, then cast to float).