Skip to content

SWI 0x0F — ObjAffineSet

  • Entry: 0x00000CE0 (ARM)
  • Status: verified (hardware-checked 2026-07-08: cycle counts and return values measured on real GBA via the worker ROM)

Summary

Computes the 2×2 affine matrix (PA/PB/PC/PD) for one or more rotated/scaled OBJs (sprites) from a rotation angle and independent X/Y scale factors. Like BgAffineSet but with no reference-point calculation and a caller-supplied output stride, so results can be written directly into OAM (where the four matrix entries are spaced 8 bytes apart) or into a packed array.

Parameters

Reg In Meaning
r0 ptr Source array of ObjAffineSource structs (8 bytes each).
r1 ptr Destination base address for the first PA value.
r2 u32 Number of entries to process.
r3 u32 Output stride in bytes between successive halfword writes. Use 2 for a packed array, 8 to write straight into OAM matrix slots.

ObjAffineSource (input, 8 bytes)

Offset Type Meaning
0x00 s16 X scale, 8.8 fixed point.
0x02 s16 Y scale, 8.8 fixed point.
0x04 u16 Rotation angle; only the high 8 bits used (0–255 = full circle).
0x06 Padding.

Output

Four s16 values in 8.8 fixed point written at r1, r1+r3, r1+2·r3, r1+3·r3: PA, PB, PC, PD in that order.

Returns

None (results written via r1/r3).

Clobbered registers

r8, r9, sl, fp, ip used internally (restored from frame). r0 advances by 8 per entry; r1 advances by 4·r3 per entry.

Hardware audit (2026-07-08, canary r4–r12 + CPSR snapshot): caller-visible clobbers: r0/r1 advanced past the processed structs; r2 (count) and r3 (offset) preserved. r2, r4–r12, r13, and CPSR (flags and mode) came back bit-identical on every tested path. r11/r12 are explained by the SWI dispatcher (it pushes {r11, r12, lr} — see 10_irq_boot_and_iwram.md) and CPSR by the SPSR restore on return; r2 and r4–r10 must be preserved or restored by the routine itself — where a static note above claims r2 is destroyed, the hardware disagrees at the caller level. Raw data: results/clobber_audit_pass2.csv.

Algorithm

For each entry (count in r2, decremented, exits when negative):

  1. Angle → sin/cos using the shared internal sine table at 0x00000D5C (256-entry, 1.14 fixed point). Index = high byte of the angle field. sin = SineTable[index], cos = SineTable[(index + 64) & 255].
  2. Matrix (each product >> 14, giving 8.8 output):
  3. PA = (cos × scaleX) >> 14 → store, advance r1 by r3
  4. PB = −(sin × scaleX) >> 14 → store, advance
  5. PC = (sin × scaleY) >> 14 → store, advance
  6. PD = (cos × scaleY) >> 14 → store, advance
  7. Advance source by 8; repeat.

This is the same matrix as BgAffineSet; only the storage (strided post-increment via STRH [r1], r3) and the absence of the reference-point step differ.

Edge cases & known bugs

  • Count 0 / negative: initial count − 1 < 0 test exits immediately.
  • Only the high byte of the angle is used (256 steps).
  • No overflow saturation on the 8.8 results.
  • The stride r3 is a raw byte offset applied after every halfword store; a value < 2 would overlap successive entries.

Rounding & saturation (static analysis)

Ghidra 12.1.2 decompilation confirms: Each of the four written halfwords is (sin/cos × scale) >> 14. The >> 14 is an arithmetic shift on a signed int (sign-preserving, truncating toward −∞ — negative products round down, not toward zero), and results are written through a plain (short)/(int) store that wraps on overflow with no saturation. The sin/cos values come from a 256-entry signed table at 0x00000D5C indexed by the angle's high byte (cos = (angle>>8)+0x40).

Cycle count

Hardware-measured net CPU cycles (worker-ROM harness, TM0/TM1 cascade at F/1, 13-cycle baseline subtracted; identical across 3 runs — see 02_hardware_verification_checklist.md § Measurement setup).

n=1: 127 (identical for offset=2 and offset=8); n=4: 313 (EWRAM) → ≈62 cycles per entry + ≈65 fixed.

Open questions (need hardware verification)

  • Confirm hardware treats the OAM-stride (r3 = 8) case as expected end-to-end.

GBATEK cross-reference

Matches GBATEK's ObjAffineSet (8-byte source, stride parameter, shared sine table). Documents the shared sine table at 0x00000D5C in 1.14 format.