code wiki / (root) / nx_camera_q14.nx

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}