Skip to content

SWI 0x0E — BgAffineSet

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

Summary

Builds the affine transformation parameters (PA/PB/PC/PD) and the internal reference-point offset for one or more rotated/scaled backgrounds. For each input entry it computes the 2×2 matrix from a rotation angle and independent X/Y scale factors, then derives the background reference point so that a chosen on-screen "display center" maps to a chosen texture-space "origin".

Parameters

Reg In Meaning
r0 ptr Source array of BgAffineSource structs (20 bytes each).
r1 ptr Destination array of BgAffineDest structs (16 bytes each).
r2 u32 Number of entries to process.

BgAffineSource (input, 20 bytes)

Offset Type Meaning
0x00 s32 Texture-space origin X, 24.8 fixed point (signed).
0x04 s32 Texture-space origin Y, 24.8 fixed point.
0x08 s16 On-screen display center X (integer pixels).
0x0A s16 On-screen display center Y.
0x0C s16 X scale, 8.8 fixed point.
0x0E s16 Y scale, 8.8 fixed point.
0x10 u16 Rotation angle; only the high 8 bits are used (0–255 = full circle).
0x12 Padding.

BgAffineDest (output, 16 bytes)

Offset Type Meaning
0x00 s16 PA (dx), 8.8.
0x02 s16 PB (dmx), 8.8.
0x04 s16 PC (dy), 8.8.
0x06 s16 PD (dmy), 8.8.
0x08 s32 Reference point X, 24.8.
0x0C s32 Reference point Y, 24.8.

Returns

None (results written to r1 array).

Clobbered registers

r3–r9, sl, fp, ip used internally (r4–fp restored from frame). r0/r1 advance.

Hardware audit (2026-07-08, canary r4–r12 + CPSR snapshot): caller-visible clobbers: r0/r1 advanced past the processed structs; r2 (count) preserved; r3 = 0. 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 (loop count in r2, decremented, exits when it goes negative):

  1. Angle → sin/cos. Take the high byte of the angle field as an index 0–255. sin = SineTable[index], cos = SineTable[(index + 64) & 255] (a 64-entry = 90° phase shift). See the trig table note below.
  2. Matrix (each product arithmetic-shifted right by 14):
  3. PA = (cos × scaleX) >> 14
  4. PB = −(sin × scaleX) >> 14
  5. PC = (sin × scaleY) >> 14
  6. PD = (cos × scaleY) >> 14

The sine table is 1.14 fixed point and the scales are 8.8, so the >>14 yields 8.8 results as required by the hardware BGxPA–PD registers. 3. Reference point. With display center (dx, dy) and origin (ox, oy): - RefX = ox − (PA × dx + PB × dy) - RefY = oy − (PC × dx + PD × dy)

Computed with MLA chains; PA–PD are 8.8, the display centers are integer, so products are .8 and add to the 24.8 origin, giving 24.8 reference points. 4. Advance source by 20 and destination by 16; repeat.

Internal sine table (0x00000D5C)

256 signed 16-bit entries in 1.14 fixed point (0x4000 = 1.0). Entry i = round(16384 × sin(i · 2π/256)). Verified against a true sine: max error ≈ 6e-5. Sample values: [0]=0, [32]=11585 (≈0.707), [64]=16384 (=1.0), [128]=0, [192]=−16384. Cosine is obtained by indexing +64. The same table is shared by ObjAffineSet (SWI 0x0F).

Edge cases & known bugs

  • Count 0 (or negative when treated as signed): the initial count − 1 < 0 test exits immediately; nothing written.
  • Only the high byte of the 16-bit angle contributes; the low 8 bits are ignored, giving 256 angular steps (1.406° each).
  • No overflow saturation: large scale values can overflow the 8.8 PA–PD range and wrap.

Rounding & saturation (static analysis)

Ghidra 12.1.2 decompilation confirms: Each matrix element is (sin/cos × scale) >> 14 and each reference-point term is a sum of (element × coord) >> 14 products. 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: 188; n=4: 533 (EWRAM src/dst) → ≈115 cycles per entry + ≈73 fixed.

Open questions (need hardware verification)

  • Confirm the 24.8 interpretation of the origin/reference fields on hardware.
  • Rounding behavior (truncation via ASR) at negative products.

GBATEK cross-reference

Matches GBATEK's BgAffineSet struct layout and matrix. Adds the concrete sine table location (0x00000D5C), its 1.14 format and the +64 cosine indexing.