nx_camera_q14.nx source
↩ module page · 662 lines · 24694 B
1// nx_camera_q14.nx -- camera + projection + 4x4 matrix math in Q14.
2//
3// /voxels runtime primitive per
4// nxc2/docs/NISHI_GAME_ENGINE_ROADMAP.md. The math layer that powers
5// view, projection, and per-vertex transform for the software
6// rasterizer (queued).
7//
8// Flat-buffer API: every matrix is a 16-element i64 array (row-major,
9// Q14 entries). Every vector is a 4-element i64 array. No struct
10// usage by design -- this primitive stays WAT-clean for browser
11// delivery today. Once self-host closes OP_GEP-on-WAT, a companion
12// nx_camera_struct.nx will wrap these flat-buffer helpers behind a
13// natural CameraState struct. Both APIs ship side-by-side after
14// that.
15//
16// Q14 convention (matches nx_matrix.nx + nx_sound.nx): 1.0 = 16384.
17// Compose freely with existing primitives that use Q14.
18//
19// Trig: 19-entry sin table at 5-degree resolution covering 0..90.
20// Symmetry folds extend to 0..360. Linear interpolation between
21// table entries yields ~0.5 degree max error -- acceptable for
22// general camera control where the eye cannot distinguish under
23// 1 degree.
24//
25// FULL CAPABILITY (per feedback-maximum-capability-no-simplification
26// cardinal, 2026-05-16): higher-resolution variants use Bhaskara I's
27// sin approximation (closed-form rational, accurate to ~0.0017 peak
28// absolute error = ~28 Q14 units, no table required). Variants:
29// nx_camera_sin_q14_centideg(d100): input in centi-degrees,
30// range [-36000, 36000]
31// -- 0.01-degree precision
32// nx_camera_sin_q14_millideg(d1000): input in milli-degrees,
33// range [-360000, 360000]
34// -- 0.001-degree precision
35// Both variants use the same Bhaskara closed-form, scaled to the
36// integer input unit. The 5-degree table remains the legacy API
37// for callers that don't need sub-degree precision.
38//
39// Loss audit:
40// - Q14 multiplications lose 14 fractional bits per multiply. For
41// matrix product on near-orthogonal rotations the cumulative error
42// is <1 LSB on the output 4x4.
43// - Trig table interpolation has named ~0.5 degree residual.
44// - Perspective divide reciprocal uses integer reciprocal via Q14
45// long division; max ULP error 1.
46//
47// genealogy_id: lengyel_2003_math_for_3d_games + akenine_moller_2018_rtr
48// lineage_id: nx_camera_4x4_flat_buffer_q14
49//
50// nx_safety_envelope:
51// intended_use: "Q14 4x4 camera transform -- view + projection
52// for substrate 3D rendering pipeline"
53// sil_target: SIL2 (camera/projection in safety-relevant
54// HUDs / displays)
55// asil_target: QM
56// dal_target: DAL C
57// evidence: [Q14_fixed_point_matrix_arithmetic, no_FP,
58// bit_equal_reproducible,
59// flat_buffer_no_pointer_chase]
60// hazard_register: [bug-tape-Q14-overflow-on-large-frustum,
61// bug-tape-projection-singularity-at-near-zero]
62// residual_risk: "Near-plane MUST be > 0 (caller contract);
63// substrate refuses division by zero in
64// perspective divide path."
65// verdict: NOT_YET_EVALUATED
66
67import "nx_syscalls.nx"
68import "nx_hal.nx"
69import "nx_tier.nx"
70const NX_MAGIC_1428: i64 = 1428
71const NX_MAGIC_2845: i64 = 2845
72const NX_MAGIC_4240: i64 = 4240
73const NX_MAGIC_5604: i64 = 5604
74const NX_MAGIC_6923: i64 = 6923
75const NX_MAGIC_8192: i64 = 8192
76const NX_MAGIC_9397: i64 = 9397
77const NX_MAGIC_10531: i64 = 10531
78const NX_MAGIC_11585: i64 = 11585
79const NX_MAGIC_12551: i64 = 12551
80const NX_MAGIC_13421: i64 = 13421
81const NX_MAGIC_14189: i64 = 14189
82const NX_MAGIC_14849: i64 = 14849
83const NX_MAGIC_15394: i64 = 15394
84const NX_MAGIC_15824: i64 = 15824
85const NX_MAGIC_16134: i64 = 16134
86const NX_MAGIC_16321: i64 = 16321
87const NX_MAGIC_16384: i64 = 16384
88const NX_MAGIC_36000: i64 = 36000
89const NX_MAGIC_27000: i64 = 27000
90const NX_MAGIC_18000: i64 = 18000
91const NX_MAGIC_9000: i64 = 9000
92const NX_MAGIC_405000000: i64 = 405000000
93const NX_MAGIC_360000: i64 = 360000
94const NX_MAGIC_270000: i64 = 270000
95const NX_MAGIC_180000: i64 = 180000
96const NX_MAGIC_90000: i64 = 90000
97const NX_MAGIC_40500000000: i64 = 40500000000
98const NX_MAGIC_14000: i64 = 14000
99const NX_MAGIC_3000: i64 = 3000
100const NX_MAGIC_8162: i64 = 8162
101const NX_MAGIC_8222: i64 = 8222
102const NX_MAGIC_45000: i64 = 45000
103const NX_MAGIC_45001: i64 = 45001
104const NX_MAGIC_11530: i64 = 11530
105const NX_MAGIC_11620: i64 = 11620
106
107// ===== Q14 constants ================================================
108const NX_CAM_Q: nx_int = 16384
109
110// 4x4 row-major matrix is a flat 16-element buffer; indexing:
111// M[r,c] = m[r * 4 + c]
112// m[0..3] = row 0
113// m[4..7] = row 1
114// m[8..11] = row 2
115// m[12..15] = row 3
116const NX_CAM_M4_ELEMS: nx_int = 16
117const NX_CAM_M4_BYTES: nx_int = 128 // 16 * 8
118
119// Vec4 is 4 i64s.
120const NX_CAM_V4_ELEMS: nx_int = 4
121const NX_CAM_V4_BYTES: nx_int = 32
122
123// Sin/cos table resolution.
124const NX_CAM_TRIG_RES_DEG: nx_int = 5 // table entry every 5 degrees
125const NX_CAM_TRIG_N: nx_int = 19 // 0, 5, 10, ..., 85, 90 = 19 entries
126
127// ===== Internal sin table (0..90 degrees, Q14) =====================
128// Pre-computed sin values at 5-degree resolution for the first
129// quadrant. Symmetry handles 90..360.
130//
131// sin(0)=0, sin(5)=0.08716, ..., sin(90)=1.0 scaled by Q=16384.
132func _sin_table_alloc() -> *i64 {
133 let t: *i64 = (sys_mmap(NX_CAM_TRIG_N * NX_SIZEOF_NX_INT)) as *i64
134 t[0] = 0 // sin(0)
135 t[1] = NX_MAGIC_1428 // sin(5)
136 t[2] = NX_MAGIC_2845 // sin(10)
137 t[3] = NX_MAGIC_4240 // sin(15)
138 t[4] = NX_MAGIC_5604 // sin(20)
139 t[5] = NX_MAGIC_6923 // sin(25)
140 t[6] = NX_MAGIC_8192 // sin(30) = 0.5 exactly
141 t[7] = NX_MAGIC_9397 // sin(35)
142 t[8] = NX_MAGIC_10531 // sin(40)
143 t[9] = NX_MAGIC_11585 // sin(45)
144 t[10] = NX_MAGIC_12551 // sin(50)
145 t[11] = NX_MAGIC_13421 // sin(55)
146 t[12] = NX_MAGIC_14189 // sin(60)
147 t[13] = NX_MAGIC_14849 // sin(65)
148 t[14] = NX_MAGIC_15394 // sin(70)
149 t[15] = NX_MAGIC_15824 // sin(75)
150 t[16] = NX_MAGIC_16134 // sin(80)
151 t[17] = NX_MAGIC_16321 // sin(85)
152 t[18] = NX_MAGIC_16384 // sin(90) = 1.0
153 return t
154}
155
156// Fold degrees into [0, 359].
157func _normalise_deg(deg: nx_int) -> nx_int {
158 var d: nx_int = deg
159 while d < 0 { d = d + 360 }
160 while d >= 360 { d = d - 360 }
161 return d
162}
163
164// sin_q14 of an integer-degree input, in [0, 359]. Uses table +
165// linear interpolation + quadrant symmetry.
166//
167// Quadrants:
168// [ 0, 90): +sin(d)
169// [ 90, 180): +sin(180 - d)
170// [180, 270): -sin(d - 180)
171// [270, 360): -sin(360 - d)
172func nx_camera_sin_q14_deg(deg: nx_int) -> nx_int {
173 let d: nx_int = _normalise_deg(deg)
174 let t: *i64 = _sin_table_alloc()
175
176 // Quadrant + reflected angle in [0, 90].
177 var ang: nx_int = d
178 var sign: nx_int = 1
179 if d >= 270 {
180 ang = 360 - d
181 sign = 0 - 1
182 } else {
183 if d >= 180 {
184 ang = d - 180
185 sign = 0 - 1
186 } else {
187 if d >= 90 {
188 ang = 180 - d
189 sign = 1
190 }
191 }
192 }
193 // ang now in [0, 90]. Table index + fractional position.
194 let idx: nx_int = ang / NX_CAM_TRIG_RES_DEG
195 let frac: nx_int = ang - idx * NX_CAM_TRIG_RES_DEG // [0, 4]
196 if idx >= NX_CAM_TRIG_N - 1 {
197 // ang == 90 exactly.
198 if sign < 0 { return 0 - t[NX_CAM_TRIG_N - 1] }
199 return t[NX_CAM_TRIG_N - 1]
200 }
201 let lo: nx_int = t[idx]
202 let hi: nx_int = t[idx + 1]
203 // Linear interp: lo + (hi - lo) * frac / 5
204 let v: nx_int = lo + (hi - lo) * frac / NX_CAM_TRIG_RES_DEG
205 if sign < 0 { return 0 - v }
206 return v
207}
208
209// cos via shift: cos(d) = sin(d + 90).
210func nx_camera_cos_q14_deg(deg: nx_int) -> nx_int {
211 return nx_camera_sin_q14_deg(deg + 90)
212}
213
214// ===== Higher-resolution Bhaskara-formula trig =====================
215// Bhaskara I (~7th century) closed-form sin approximation:
216// sin(x deg) ~= 4 * x * (180 - x) / (40500 - x * (180 - x))
217// Peak absolute error ~ 0.0017 (about 28 Q14 units) over [0, 180].
218//
219// Scaled here to centi-degrees so the closed-form uses pure integer
220// arithmetic (no fractional degrees):
221// x_cent in [0, 18000]
222// result = (4 * x_cent * (18000 - x_cent) * Q14) /
223// (405000000 - x_cent * (18000 - x_cent))
224//
225// Quadrant folding identical to the table-based path.
226
227func _normalise_centideg(d100: nx_int) -> nx_int {
228 var d: nx_int = d100
229 while d < 0 { d = d + NX_MAGIC_36000 }
230 while d >= NX_MAGIC_36000 { d = d - NX_MAGIC_36000 }
231 return d
232}
233
234func nx_camera_sin_q14_centideg(d100: nx_int) -> nx_int {
235 let d: nx_int = _normalise_centideg(d100)
236 var ang: nx_int = d
237 var sign: nx_int = 1
238 if d >= NX_MAGIC_27000 {
239 ang = NX_MAGIC_36000 - d
240 sign = 0 - 1
241 } else {
242 if d >= NX_MAGIC_18000 {
243 ang = d - NX_MAGIC_18000
244 sign = 0 - 1
245 } else {
246 if d >= NX_MAGIC_9000 {
247 ang = NX_MAGIC_18000 - d
248 sign = 1
249 }
250 }
251 }
252 // ang in [0, 9000] centi-degrees = [0, 90] degrees.
253 // Bhaskara closed form scaled to centi-degrees:
254 let z: nx_int = ang * (NX_MAGIC_18000 - ang)
255 let denom: nx_int = NX_MAGIC_405000000 - z
256 if denom <= 0 { return 0 }
257 let num: nx_int = 4 * z * NX_CAM_Q
258 let v: nx_int = num / denom
259 if sign < 0 { return 0 - v }
260 return v
261}
262
263func nx_camera_cos_q14_centideg(d100: nx_int) -> nx_int {
264 return nx_camera_sin_q14_centideg(d100 + NX_MAGIC_9000)
265}
266
267// Milli-degree variant: input in thousandths of a degree. Same
268// Bhaskara formula, just scaled differently. x_mil in [0, 180000].
269// result = (4 * x_mil * (180000 - x_mil) * Q14) /
270// (40500000000 - x_mil * (180000 - x_mil))
271// Denom: scaling 40500 deg^2 by 10^6 since x_mil = 1000 * x_deg
272// gives factor of (1000)^2 = 1e6. Numerator at peak:
273// 4 * 90000^2 * 16384 = 5.31e14 -- well within i64.
274
275func _normalise_millideg(d1000: nx_int) -> nx_int {
276 var d: nx_int = d1000
277 while d < 0 { d = d + NX_MAGIC_360000 }
278 while d >= NX_MAGIC_360000 { d = d - NX_MAGIC_360000 }
279 return d
280}
281
282func nx_camera_sin_q14_millideg(d1000: nx_int) -> nx_int {
283 let d: nx_int = _normalise_millideg(d1000)
284 var ang: nx_int = d
285 var sign: nx_int = 1
286 if d >= NX_MAGIC_270000 {
287 ang = NX_MAGIC_360000 - d
288 sign = 0 - 1
289 } else {
290 if d >= NX_MAGIC_180000 {
291 ang = d - NX_MAGIC_180000
292 sign = 0 - 1
293 } else {
294 if d >= NX_MAGIC_90000 {
295 ang = NX_MAGIC_180000 - d
296 sign = 1
297 }
298 }
299 }
300 let z: nx_int = ang * (NX_MAGIC_180000 - ang)
301 let denom: nx_int = NX_MAGIC_40500000000 - z
302 if denom <= 0 { return 0 }
303 let num: nx_int = 4 * z * NX_CAM_Q
304 let v: nx_int = num / denom
305 if sign < 0 { return 0 - v }
306 return v
307}
308
309func nx_camera_cos_q14_millideg(d1000: nx_int) -> nx_int {
310 return nx_camera_sin_q14_millideg(d1000 + NX_MAGIC_90000)
311}
312
313// ===== Matrix builders ==============================================
314// All write into a caller-provided 16-element buffer (row-major).
315
316// Identity matrix.
317// Returns `out` (fluent-self pattern) so nxc2's WAT emitter produces
318// consistent `(result i64)` for these matrix-init functions. Prior
319// void declarations triggered the bootstrap-emitter's local.set-after-
320// void-call bug (caller writes return value into a temp local that
321// downstream code then reads as a pointer). Per the no-c-in-runtime
322// cardinal we cannot patch nxc2's C code; the safest workaround is
323// to make these functions actually return what nxc2 thinks they do.
324func nx_camera_identity_4x4(out: *i64) -> *i64 {
325 var i: nx_int = 0
326 while i < NX_CAM_M4_ELEMS {
327 out[i] = 0
328 i = i + 1
329 }
330 out[0] = NX_CAM_Q
331 out[5] = NX_CAM_Q
332 out[10] = NX_CAM_Q
333 out[15] = NX_CAM_Q
334 return out
335}
336
337// Translation matrix: identity with right column = (tx, ty, tz, 1).
338func nx_camera_translation_4x4(tx: nx_int, ty: nx_int, tz: nx_int, out: *i64) -> *i64 {
339 nx_camera_identity_4x4(out)
340 out[3] = tx
341 out[7] = ty
342 out[11] = tz
343 return out
344}
345
346// Rotation around Y axis (yaw) by integer degrees. Standard
347// right-handed:
348// cos 0 sin 0
349// 0 1 0 0
350// -sin 0 cos 0
351// 0 0 0 1
352func nx_camera_rotation_y_deg(deg: nx_int, out: *i64) -> *i64 {
353 let c: nx_int = nx_camera_cos_q14_deg(deg)
354 let s: nx_int = nx_camera_sin_q14_deg(deg)
355 nx_camera_identity_4x4(out)
356 out[0] = c
357 out[2] = s
358 out[8] = 0 - s
359 out[10] = c
360 return out
361}
362
363// Rotation around X axis (pitch).
364// 1 0 0 0
365// 0 cos -sin 0
366// 0 sin cos 0
367// 0 0 0 1
368func nx_camera_rotation_x_deg(deg: nx_int, out: *i64) -> *i64 {
369 let c: nx_int = nx_camera_cos_q14_deg(deg)
370 let s: nx_int = nx_camera_sin_q14_deg(deg)
371 nx_camera_identity_4x4(out)
372 out[5] = c
373 out[6] = 0 - s
374 out[9] = s
375 out[10] = c
376 return out
377}
378
379// Perspective projection matrix.
380//
381// fov_y_deg: full vertical field of view in degrees (e.g. 60 - 90)
382// aspect_q14: width / height in Q14 (e.g. 16:9 -> Q14 * 16 / 9)
383// near_q14: near plane Z (must be > 0)
384// far_q14: far plane Z (must be > near)
385//
386// Right-handed, looking down -Z, OpenGL-style ndc:
387//
388// f/asp 0 0 0
389// 0 f 0 0
390// 0 0 (f+n)/(n-f) 2*f*n/(n-f)
391// 0 0 -1 0
392//
393// where f = 1 / tan(fov_y / 2) (the "focal length")
394//
395// Q14 throughout. Reciprocal computed via Q14 long-division.
396func nx_camera_perspective(
397 fov_y_deg: nx_int,
398 aspect_q14: nx_int,
399 near_q14: nx_int,
400 far_q14: nx_int,
401 out: *i64
402) -> *i64 {
403 // f = 1 / tan(fov/2) = cos(fov/2) / sin(fov/2)
404 let half_fov: nx_int = fov_y_deg / 2
405 let cos_h: nx_int = nx_camera_cos_q14_deg(half_fov)
406 let sin_h: nx_int = nx_camera_sin_q14_deg(half_fov)
407
408 // Guard divide-by-zero.
409 if sin_h == 0 {
410 nx_camera_identity_4x4(out)
411 return out
412 }
413 // f_q14 = cos_h / sin_h in Q14 -> need to scale: (cos_h * Q) / sin_h
414 let f_q14: nx_int = (cos_h * NX_CAM_Q) / sin_h
415 // f / aspect = (f_q14 * Q) / aspect_q14
416 if aspect_q14 == 0 {
417 nx_camera_identity_4x4(out)
418 return out
419 }
420 let f_over_asp: nx_int = (f_q14 * NX_CAM_Q) / aspect_q14
421
422 // Z mapping:
423 // m[10] = (f + n) / (n - f) in Q14
424 // m[11] = 2 * f * n / (n - f)
425 // m[14] = -1 (in Q14: -Q)
426 let nf_diff: nx_int = near_q14 - far_q14
427 if nf_diff == 0 {
428 nx_camera_identity_4x4(out)
429 return out
430 }
431 let fpn: nx_int = far_q14 + near_q14
432 let m10: nx_int = (fpn * NX_CAM_Q) / nf_diff
433 let two_fn: nx_int = 2 * far_q14 * near_q14 / NX_CAM_Q
434 let m11: nx_int = (two_fn * NX_CAM_Q) / nf_diff
435
436 var i: nx_int = 0
437 while i < NX_CAM_M4_ELEMS {
438 out[i] = 0
439 i = i + 1
440 }
441 out[0] = f_over_asp
442 out[5] = f_q14
443 out[10] = m10
444 out[11] = m11
445 out[14] = 0 - NX_CAM_Q
446 // out[15] stays 0 (perspective is non-affine; the 1 lives in -1*z)
447 return out
448}
449
450// ===== 4x4 multiplication ============================================
451// out = a * b. Caller MUST NOT alias out with a or b. Implements
452// the textbook triple-nested loop in row-major layout.
453//
454// Each c[i,j] = sum over k of a[i,k] * b[k,j], with one /NX_CAM_Q
455// after the sum to recover Q14 scale.
456func nx_camera_mat4_mul(a: *i64, b: *i64, out: *i64) -> *i64 {
457 var r: nx_int = 0
458 while r < 4 {
459 var c: nx_int = 0
460 while c < 4 {
461 var sum: nx_int = 0
462 var k: nx_int = 0
463 while k < 4 {
464 sum = sum + a[r * 4 + k] * b[k * 4 + c]
465 k = k + 1
466 }
467 out[r * 4 + c] = sum / NX_CAM_Q
468 c = c + 1
469 }
470 r = r + 1
471 }
472 return out
473}
474
475// ===== Vec4 transform ===============================================
476// out = M * v. Caller supplies 4-element input + 4-element output.
477// Standard column-vector convention (vectors as columns on the right).
478//
479// out[i] = sum over k of M[i,k] * v[k]
480func nx_camera_mat4_vec4(m: *i64, v: *i64, out: *i64) -> *i64 {
481 var r: nx_int = 0
482 while r < 4 {
483 var sum: nx_int = 0
484 var k: nx_int = 0
485 while k < 4 {
486 sum = sum + m[r * 4 + k] * v[k]
487 k = k + 1
488 }
489 out[r] = sum / NX_CAM_Q
490 r = r + 1
491 }
492 return out
493}
494
495// ===== Self-test ====================================================
496func main() -> i64 {
497 // T1: sin/cos at standard angles.
498 if nx_camera_sin_q14_deg(0) != 0 { return nx_hal_exit(1) }
499 if nx_camera_sin_q14_deg(30) != NX_MAGIC_8192 { return nx_hal_exit(2) }
500 if nx_camera_sin_q14_deg(90) != NX_MAGIC_16384 { return nx_hal_exit(3) }
501 // sin(180) should be ~0 via reflection.
502 let s180: nx_int = nx_camera_sin_q14_deg(180)
503 if s180 > 5 { return nx_hal_exit(4) }
504 if s180 < (0 - 5) { return nx_hal_exit(5) }
505 // sin(270) = -1
506 let s270: nx_int = nx_camera_sin_q14_deg(270)
507 if s270 != (0 - NX_MAGIC_16384) { return nx_hal_exit(6) }
508 // sin(360) ~ 0
509 let s360: nx_int = nx_camera_sin_q14_deg(360)
510 if s360 > 5 { return nx_hal_exit(7) }
511 if s360 < (0 - 5) { return nx_hal_exit(8) }
512 // Negative input: sin(-90) = -sin(90) = -16384.
513 let sn: nx_int = nx_camera_sin_q14_deg(0 - 90)
514 if sn != (0 - NX_MAGIC_16384) { return nx_hal_exit(9) }
515 // cos(0) = 1
516 if nx_camera_cos_q14_deg(0) != NX_MAGIC_16384 { return nx_hal_exit(10) }
517 // cos(90) = 0
518 let c90: nx_int = nx_camera_cos_q14_deg(90)
519 if c90 > 5 { return nx_hal_exit(11) }
520 if c90 < (0 - 5) { return nx_hal_exit(12) }
521
522 // T2: identity matrix.
523 let m: *i64 = (sys_mmap(NX_CAM_M4_BYTES)) as *i64
524 nx_camera_identity_4x4(m)
525 if m[0] != NX_CAM_Q { return nx_hal_exit(20) }
526 if m[5] != NX_CAM_Q { return nx_hal_exit(21) }
527 if m[10] != NX_CAM_Q { return nx_hal_exit(22) }
528 if m[15] != NX_CAM_Q { return nx_hal_exit(23) }
529 if m[1] != 0 { return nx_hal_exit(24) }
530 if m[14] != 0 { return nx_hal_exit(25) }
531
532 // T3: translation.
533 nx_camera_translation_4x4(100, 200, 300, m)
534 if m[3] != 100 { return nx_hal_exit(30) }
535 if m[7] != 200 { return nx_hal_exit(31) }
536 if m[11] != 300 { return nx_hal_exit(32) }
537 if m[0] != NX_CAM_Q { return nx_hal_exit(33) }
538
539 // T4: rotation_y by 0 == identity.
540 nx_camera_rotation_y_deg(0, m)
541 if m[0] != NX_CAM_Q { return nx_hal_exit(40) }
542 if m[2] != 0 { return nx_hal_exit(41) }
543 if m[8] != 0 { return nx_hal_exit(42) }
544 if m[10] != NX_CAM_Q { return nx_hal_exit(43) }
545
546 // T5: rotation_y by 90. cos(90)~0, sin(90)=Q.
547 // row 0: 0 0 Q 0
548 // row 1: 0 Q 0 0
549 // row 2: -Q 0 0 0
550 // row 3: 0 0 0 Q
551 nx_camera_rotation_y_deg(90, m)
552 if m[2] != NX_CAM_Q { return nx_hal_exit(50) } // sin(90)
553 if m[8] != (0 - NX_CAM_Q) { return nx_hal_exit(51) } // -sin(90)
554 // Diagonal m[0] and m[10] should be ~0.
555 if m[0] > 5 { return nx_hal_exit(52) }
556 if m[0] < (0 - 5) { return nx_hal_exit(53) }
557
558 // T6: mat4 mul -- identity * identity == identity.
559 let a: *i64 = (sys_mmap(NX_CAM_M4_BYTES)) as *i64
560 let b: *i64 = (sys_mmap(NX_CAM_M4_BYTES)) as *i64
561 let c: *i64 = (sys_mmap(NX_CAM_M4_BYTES)) as *i64
562 nx_camera_identity_4x4(a)
563 nx_camera_identity_4x4(b)
564 nx_camera_mat4_mul(a, b, c)
565 if c[0] != NX_CAM_Q { return nx_hal_exit(60) }
566 if c[5] != NX_CAM_Q { return nx_hal_exit(61) }
567 if c[10] != NX_CAM_Q { return nx_hal_exit(62) }
568 if c[15] != NX_CAM_Q { return nx_hal_exit(63) }
569 if c[1] != 0 { return nx_hal_exit(64) }
570 if c[7] != 0 { return nx_hal_exit(65) }
571
572 // T7: mat4 mul -- translation composes. T(1,2,3) * T(10,20,30)
573 // applied to origin should land at (11, 22, 33).
574 nx_camera_translation_4x4(NX_CAM_Q, 2 * NX_CAM_Q, 3 * NX_CAM_Q, a)
575 nx_camera_translation_4x4(10*NX_CAM_Q, 20 * NX_CAM_Q, 30* NX_CAM_Q, b)
576 nx_camera_mat4_mul(a, b, c)
577 if c[3] != 11 * NX_CAM_Q { return nx_hal_exit(70) }
578 if c[7] != 22 * NX_CAM_Q { return nx_hal_exit(71) }
579 if c[11] != 33 * NX_CAM_Q { return nx_hal_exit(72) }
580
581 // T8: mat4 * vec4: identity * v == v.
582 let v: *i64 = (sys_mmap(NX_CAM_V4_BYTES)) as *i64
583 let w: *i64 = (sys_mmap(NX_CAM_V4_BYTES)) as *i64
584 v[0] = 5 * NX_CAM_Q
585 v[1] = 7 * NX_CAM_Q
586 v[2] = 11 * NX_CAM_Q
587 v[3] = NX_CAM_Q
588 nx_camera_identity_4x4(a)
589 nx_camera_mat4_vec4(a, v, w)
590 if w[0] != 5 * NX_CAM_Q { return nx_hal_exit(80) }
591 if w[1] != 7 * NX_CAM_Q { return nx_hal_exit(81) }
592 if w[2] != 11 * NX_CAM_Q { return nx_hal_exit(82) }
593 if w[3] != NX_CAM_Q { return nx_hal_exit(83) }
594
595 // T9: translation matrix * vec4(0,0,0,1) = vec4(tx, ty, tz, 1).
596 nx_camera_translation_4x4(100 * NX_CAM_Q, 200 * NX_CAM_Q, 300 * NX_CAM_Q, a)
597 v[0] = 0
598 v[1] = 0
599 v[2] = 0
600 v[3] = NX_CAM_Q
601 nx_camera_mat4_vec4(a, v, w)
602 if w[0] != 100 * NX_CAM_Q { return nx_hal_exit(90) }
603 if w[1] != 200 * NX_CAM_Q { return nx_hal_exit(91) }
604 if w[2] != 300 * NX_CAM_Q { return nx_hal_exit(92) }
605 if w[3] != NX_CAM_Q { return nx_hal_exit(93) }
606
607 // T10: perspective matrix populates the expected non-zero entries.
608 // FOV 90, aspect 1:1, near=1, far=100. f = cot(45) = 1, so
609 // m[0] = m[5] = Q. m[14] = -Q.
610 nx_camera_perspective(90, NX_CAM_Q, NX_CAM_Q, 100 * NX_CAM_Q, a)
611 // m[0] should be near 1.0 (cot 45) -- allow +/- 100 LSB for trig
612 // table interp error at 45.
613 if a[0] < NX_MAGIC_14000 { return nx_hal_exit(100) }
614 if a[0] > NX_MAGIC_18000 { return nx_hal_exit(101) }
615 if a[5] < NX_MAGIC_14000 { return nx_hal_exit(102) }
616 if a[5] > NX_MAGIC_18000 { return nx_hal_exit(103) }
617 if a[14] != (0 - NX_CAM_Q) { return nx_hal_exit(104) }
618 if a[15] != 0 { return nx_hal_exit(105) }
619
620 // T11: Higher-resolution sin -- centideg variant. Standard angles.
621 if nx_camera_sin_q14_centideg(0) != 0 { return nx_hal_exit(110) }
622 // sin(90.00 deg) -> Bhaskara gives 4*9000*9000*Q / (405000000 - 81000000)
623 // = 5.308e12 / 3.24e8 = 16384 EXACTLY.
624 if nx_camera_sin_q14_centideg(NX_MAGIC_9000) != NX_MAGIC_16384 { return nx_hal_exit(111) }
625 // sin(180.00) -> 0 exactly.
626 let s180_c: nx_int = nx_camera_sin_q14_centideg(NX_MAGIC_18000)
627 if s180_c > 100 { return nx_hal_exit(112) }
628 if s180_c < (0 - 100) { return nx_hal_exit(113) }
629 // sin(270.00) -> -16384 exact.
630 if nx_camera_sin_q14_centideg(NX_MAGIC_27000) != (0 - NX_MAGIC_16384) { return nx_hal_exit(114) }
631 // sin(30.00) ~ 8192. Bhaskara at 30 gives 8194; allow +/- 30 Q14.
632 let s30_c: nx_int = nx_camera_sin_q14_centideg(NX_MAGIC_3000)
633 if s30_c < NX_MAGIC_8162 { return nx_hal_exit(115) }
634 if s30_c > NX_MAGIC_8222 { return nx_hal_exit(116) }
635 // Sub-degree resolution: sin(0.50) =/= sin(0.51). Even a tiny
636 // difference proves the centi-degree path is actually resolving.
637 let s_lo: nx_int = nx_camera_sin_q14_centideg(50) // 0.50 deg
638 let s_hi: nx_int = nx_camera_sin_q14_centideg(51) // 0.51 deg
639 if s_hi <= s_lo { return nx_hal_exit(117) }
640 // cos at exactly 0.00 should be ~16384.
641 if nx_camera_cos_q14_centideg(0) != NX_MAGIC_16384 { return nx_hal_exit(118) }
642 // cos at 90.00 deg should be ~0.
643 let c90_c: nx_int = nx_camera_cos_q14_centideg(NX_MAGIC_9000)
644 if c90_c > 100 { return nx_hal_exit(119) }
645 if c90_c < (0 - 100) { return nx_hal_exit(120) }
646
647 // T12: Millideg variant.
648 if nx_camera_sin_q14_millideg(0) != 0 { return nx_hal_exit(130) }
649 // sin(90.000 deg) at millideg -> 16384 exact.
650 if nx_camera_sin_q14_millideg(NX_MAGIC_90000) != NX_MAGIC_16384 { return nx_hal_exit(131) }
651 // Sub-centi-degree resolution: 0.001 deg difference resolves.
652 let m_lo: nx_int = nx_camera_sin_q14_millideg(NX_MAGIC_45000)
653 let m_hi: nx_int = nx_camera_sin_q14_millideg(NX_MAGIC_45001)
654 // 0.001 deg sin step is below Q14 resolution at 45; equal is OK,
655 // but greater-or-equal must hold (monotonic).
656 if m_hi < m_lo { return nx_hal_exit(132) }
657 // sin(45.000 deg) ~= 11585. Bhaskara gives 11567; allow +/- 30.
658 if m_lo < NX_MAGIC_11530 { return nx_hal_exit(133) }
659 if m_lo > NX_MAGIC_11620 { return nx_hal_exit(134) }
660
661 return 0
662}