nx_movement_base_library_t139.nx
buildroot/runtime/nx_movement_base_library_t139.nx
about
nx_nxa_anim_lib.nx -- THE ONE NXA CLIP EVALUATOR: channel-2 packed world-delta keys, sampled at a
clip time, blended toward an idle POSE, and written as the DUAL-QUATERNION ROWS the cast shader skins
with. Integer-only and syscall-free so the wasm engine imports it; every consumer (engine, gates,
native players) reads ONE implementation of the format's own runtime form.
WHY IT EXISTS (GE23, measured 2026-09-06 on the served beach page): the WebGPU cast pipeline already
skins on the GPU from a per-frame joint table, and the ONLY per-frame page logic left was animEval --
hand-written JavaScript sampling ANIM tracks with shortest-path nlerp, subtracting a fitted linear
root drift, blending toward the frozen stand pose, then forming T = b + dt - D b and the dual half
T x real / 2. That is the format's normative runtime form (knowledge/nxa_format_spec.md: M x = D (x - b)
+ b + dt, shortest-path lerp between keys) expressed in the page instead of the engine. This lib is
that arithmetic in NishiLang, driven by the ENGINE's own stride phase rather than a page clock.
THE UNIT TRAP, NAMED (spec + nx_nxa_play's audit): ANIM channel-2 dt LANES are MILLIMETRES packed
/100 to fit i16, while SKEL binds and the rows are in 0.01 mm model units, so dt lanes are scaled by
NA_DT_MM_TO_MODEL here. POSE entry dt words are ALREADY model units (nx_nxa_pose applies the x100
at freeze time; debt 1785380029) and are NOT scaled again. Two sections, two conventions, both cited.
KEY LAYOUT (spec, channel 2, two words per key, little-endian lanes):
w0 = [t_ms u16][dqx i16][dqy i16][dqz i16] w1 = [dqw i16][dtx i16][dty i16][dtz i16]
ROW LAYOUT (what nxGpuCast uploads, per joint): real = q (q12), dual = (T x q)/2 with T in model units,
laid out as out[0..nj*4) real rows then out[nj*4..nj*8) dual rows; the page divides both by 4096.
license_tier: ORIGINAL No hw writes (Rule 26).
dependencies 1 imports · 0 importers
imports: nx_nxa_fk.nx
imported by: nobody (leaf or entry point)
structs
| none |
consts
| 26 | const NA_Q12: i64 = 4096 |
| 27 | const NA_LANE_MASK: i64 = 65535 |
| 28 | const NA_LANE_HALF: i64 = 32767 |
| 29 | const NA_LANE_FULL: i64 = 65536 |
| 30 | const NA_LANE_B1: i64 = 16 |
| 31 | const NA_LANE_B2: i64 = 32 |
| 32 | const NA_LANE_B3: i64 = 48 |
| 33 | const NA_DT_MM_TO_MODEL: i64 = 100 // spec: ANIM dt lanes are mm packed /100; binds are 0.01 mm |
| 34 | const NA_TRK_HDR_W: i64 = 3 // per track: [joint][channel][nkeys] |
| 35 | const NA_CH2: i64 = 2 |
| 36 | const NA_CH2_KEY_W: i64 = 2 |
| 37 | const NA_CH0_KEY_W: i64 = 4 // legacy verbose T keys, 32 bytes -- skipped, never sampled |
| 38 | const NA_CH1_KEY_W: i64 = 5 // legacy verbose R keys, 40 bytes -- skipped, never sampled |
| 39 | const NA_ROW_W: i64 = 8 // real x4 + dual x4 per joint |
| 40 | const NA_IDX_W: i64 = 2 // per joint: key word offset (or -1), nkeys |
| 41 | const NA_SKEL_W: i64 = 8 // spec: [parent][tx ty tz][qx qy qz qw] |
| 42 | const NA_SKEL_TX: i64 = 1 |
| 43 | const NA_SKEL_TY: i64 = 2 |
| 44 | const NA_SKEL_TZ: i64 = 3 |
| 45 | const NA_POSE_ENTRY_W: i64 = 8 // spec: [joint][q x4][dt x3] |
| 46 | const NA_RV_W: i64 = 6 // x0 y0 vx16 vy16 t0 span |
| 47 | const NA_RV_FX: i64 = 65536 // drift velocity carried as units per ms x 65536 |
| 48 | const NA_ROOT_Z: i64 = 89000 // the page's root rule: pelvis-region bind nearest (0,0,.52H) |
| 49 | const NA_ROOT_XW: i64 = 2 // ... weighting |x| twice, verbatim from loadNPC |
| 50 | const NA_MPB_DEFAULT: i64 = 700 // the page's shipped default ms-per-block before a travel fit |
| 51 | const NA_TRAVEL_MIN_PERMIL: i64 = 1500 // the page fits cadence only when the root travels > 1.5 blocks |
| 52 | const NA_FIG_UNITS: i64 = 171530 // the reference figure's VERT extent, 0.01 mm units (1.7153 m) |
| 53 | const NA_FIG_BLOCKS_CENTI: i64 = 178 // ... which the page maps to 1.78 world blocks |
| 54 | const NA_SCR_W: i64 = 64 // scratch words na_rows needs from its caller |
| 55 | const NA_RC_BAD_TRACKS: i64 = 0 - 1 |
| 56 | const NA_RC_OVERRUN: i64 = 0 - 2 |
functions
| 59 | func na_i16(w: i64, sh: i64) -> i64 |
| 64 | func na_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v } called by 1: na_root_joint |
| 69 | func na_index(anim: *i64, nw: i64, nj: i64, tidx: *i64) -> i64 |
| 102 | func na_key_t(anim: *i64, ko: i64, i: i64) -> i64 { return anim[ko + i*NA_CH2_KEY_W] & NA_LANE_MASK } |
| 103 | func na_key_q(anim: *i64, ko: i64, i: i64, q: *i64) -> i64 |
| 112 | func na_key_d(anim: *i64, ko: i64, i: i64, d: *i64) -> i64 |
| 125 | func na_index_order(anim: *i64, tidx: *i64, nj: i64, order: *i64) -> i64 calls 1: na_key_t |
| 144 | func na_key_linear(anim: *i64, ko: i64, nk: i64, t: i64) -> i64 |
| 152 | func na_key_ordered(anim: *i64, ko: i64, nk: i64, t: i64) -> i64 |
| 163 | func na_eval_track(anim: *i64, ko: i64, nk: i64, t: i64, q: *i64, d: *i64, scr: *i64) -> i64 |
| 167 | func na_eval_track_ordered(anim: *i64, ko: i64, nk: i64, t: i64, q: *i64, d: *i64, scr: *i64) -> i64 |
| 171 | func na_eval_key(anim: *i64, ko: i64, nk: i64, c: i64, t: i64, q: *i64, d: *i64, scr: *i64) -> i64 |
| 200 | func na_root_joint(skel: *i64, nj: i64) -> i64 calls 1: na_abs |
| 215 | func na_fit_root(anim: *i64, tidx: *i64, rj: i64, rv: *i64, scr: *i64) -> i64 |
| 242 | func na_mpb(anim: *i64, tidx: *i64, rj: i64, dur: i64, scr: *i64) -> i64 |
| 278 | func na_rows(anim: *i64, tidx: *i64, nj: i64, skel: *i64, pq: *i64, pd: *i64, haspose: i64, rv: *i64, t: i64, iw: i64, out: *i64, scr: *i64) -> i64 calls 1: na_rows_indexed |
| 282 | func na_rows_indexed(anim: *i64, tidx: *i64, order: *i64, nj: i64, skel: *i64, pq: *i64, pd: *i64, haspose: i64, rv: *i64, t: i64, iw: i64, out: *i64, scr: *i64) -> i64 |