SWI 0x0A — ArcTan2¶
- Entry:
0x000004FC(THUMB; real address 0x4FC, entry word at odd 0x4FD) - Status: verified (hardware-checked 2026-07-08: cycle counts + 393,216-point octant-boundary sweep matching the documented algorithm bit-for-bit)
Summary¶
Full 360° arctangent of a vector (x, y). Returns an unsigned angle in the range
0x0000..0xFFFF (0x10000 = 360°, counter-clockwise, 0 = +X axis). It selects an
octant from the signs and relative magnitudes of x and y, computes a ratio
whose magnitude is guaranteed <= 1 (so SWI 0x09 ArcTan stays in its accurate
range), calls ArcTan, and adds the correct quadrant/octant offset.
Parameters¶
| Reg | In | Meaning |
|---|---|---|
| r0 | s32 | X component |
| r1 | s32 | Y component |
Returns¶
| Reg | Out | Meaning |
|---|---|---|
| r0 | u16 | Angle 0x0000..0xFFFF, where 0x4000=90°, 0x8000=180°, 0xC000=270° |
Clobbered registers¶
r2–r7 are used as scratch (pushed/popped). r1 is destroyed.
Hardware audit (2026-07-08, canary r4–r12 + CPSR snapshot): caller-visible clobbers: r0 = angle; r1 is clobbered on the general path but untouched on the axis fast path; r3 = 0x170 leftover; r2 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¶
Axis special cases (handled first, no division):
- y == 0: return 0x0000 if x >= 0, else 0x8000 (180°).
- x == 0 (and y != 0): return 0x4000 (90°) if y >= 0, else 0xC000 (270°).
General case: precompute x<<14, y<<14, -x, -y, and the offset
constants 0x4000 (90°) and 0x8000 (180°). Then, based on the signs of x and
y and the comparison of |x| vs |y|, pick one of eight octants. In every
octant the code divides the smaller-magnitude axis (shifted left 14) by the
larger-magnitude axis using SWI 0x06 Div, so the quotient fed to ArcTan has
magnitude <= 1.0 in Q1.14. It then calls ArcTan and combines
(hardware-exact — comparison operators and formulas below reproduce the
boundary sweep bit-for-bit; the axis cases y==0 / x==0 never reach here):
| Octant (sign x, sign y, magnitude test) | Ratio passed to ArcTan | Result formula |
|---|---|---|
x≥0, y>0, x ≥ y |
(y<<14)/x | A |
x≥0, y>0, x < y |
(x<<14)/y | 0x4000 − A |
x<0, y>0, −x < y |
(x<<14)/y | 0x4000 − A |
x<0, y>0, −x ≥ y |
(y<<14)/x | 0x8000 + A |
x<0, y<0, −x > −y |
(y<<14)/x | 0x8000 + A |
x<0, y<0, −x ≤ −y |
(x<<14)/y | 0xC000 − A |
x>0, y<0, x ≤ −y |
(x<<14)/y | 0xC000 − A |
x>0, y<0, x > −y |
(y<<14)/x | 0x10000 + A (wraps to low bits) |
where A = ArcTan(ratio) (signed). 0xC000 = 0x4000 + 0x8000; the last row
uses 0x8000<<1 = 0x10000 with a negative A, whose low 16 bits give the
correct near-360° angle. The final result is masked to 16 bits.
An earlier draft of this table (from static analysis alone) had the
(x>0, y<0, |x|>|y|) and (x<0, y<0, |x|≥|y|) formulas swapped
(0x8000 + A ↔ 0x10000 + A); the hardware sweep settled it as above. Note
that on the SW diagonal x == y < 0 the two adjacent formulas coincide
(0x8000 + 0x2000 = 0xC000 − 0x2000 = 0xA000), so the boundary assignment
there is unobservable from outputs.
A reference implementation of this dispatch (+ Div truncation toward zero + the
bit-exact ArcTan model) reproduces all 393,216 boundary-sweep points with
zero mismatches (tools/analyze_boundary.py,
results/arctan2_boundary_dump.csv).
Note: the internal Div/ArcTan calls are reached through tiny THUMB→ARM trampoline
veneers (add r3,pc / bx r3) at 0x3A4 (→ Div at 0x3B4) and 0x470 (→ ArcTan
at 0x474), since ArcTan2 is THUMB while Div/ArcTan are ARM.
Edge cases & known bugs¶
- Verified against
atan2:(1,1)→0x2000 (45°),(-1,1)→0x6000 (135°),(-1,-1)→0xA000 (225°),(1,-1)→0xE000 (315°),(100,50)→0x12E4 (26.56°),(50,100)→0x2D1C (63.44°). Axis cases(1,0)→0,(-1,0)→0x8000,(0,1)→0x4000,(0,-1)→0xC000. - Boundary sweep (hardware, all magnitudes k=1..0x7FFF/−0x8000..−1): every
diagonal ray is exactly constant —
(k,k)→0x2000,(−k,−k)→0xA000,(k,−k)→0xE000,(−k,k)→0x6000— and both axes are exactly constant ((k,0)→0,(−k,0)→0x8000,(0,k)→0x4000,(0,−k)→0xC000). One step off the diagonal,(k,k+1)→0x2001and(k,k−1)→0x1FFFfor everyk ≥ 0x4000(a single output unit; below that the Div truncation makes the offset slightly larger). (0,0)returns0x0000(hardware-verified): they==0axis path is taken before thex==0branch, and it treatsx=0as non-negative. (Earlier draft predicted0x4000; real hardware returns 0.)- By always dividing the larger axis by the smaller, ArcTan's
|input|>1divergence is never triggered, so ArcTan2 stays accurate in all quadrants. - Small residual error comes entirely from ArcTan's approximation and from the
integer Div truncation of the ratio (loses fractional precision when
|x|,|y|are small).
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).
| Input (x, y) | Cycles | Result |
|---|---|---|
| (0, 0) | 73 | 0x0000 |
| (0x4000, 0) | 73 | 0x0000 |
| (0, 0x4000) | 78 | 0x4000 |
| (0x1000, 0x3000) | 342 | 0x32E5 |
| (0x4000, 0x4000) | 361 | 0x2000 |
| (-1, -1) | 364 | 0xA000 |
Axis cases return almost immediately; the general path (one Div + one ArcTan plus octant fix-up) costs 342–364.
Open questions (need hardware verification)¶
None. The 2026-07-08 boundary sweep (diagonals, axes, diagonal±1 — 393,216
points) matches the documented dispatch bit-for-bit, resolving (0,0)
(returns 0x0000), the octant formula assignment, and the boundary
comparison operators.
GBATEK cross-reference¶
Agrees with GBATEK (0..0xFFFF output, 0x4000=90°). Adds the explicit octant
selection table, the "divide larger by smaller" trick that keeps ArcTan in range,
and the axis special-case results including (0,0).