code wiki / (root) / nx_dyna_bind_skin.nx

nx_dyna_bind_skin.nx source

↩ module page · 715 lines · 33586 B

1// nx_dyna_bind_skin.nx -- BIND THE XPBD SOFT-TISSUE CAGE TO SKIN VERTICES, AND MEASURE OUR OWN 2// PLANT IN THE SUBJECT'S UNITS. 3// 4// WHY THIS EXISTS. `dyna_bind_skin` is OPEN on the charsim and graphics boards. DYNA already 5// DECLARES per-bone soft-tissue contracts in the asset (nx_nxa_dyna: anchor, axis, k, c, travel 6// clamp, influence radius, falloff, all derived FROM THE MESH) and nx_softtissue already SOLVES 7// tissue as XPBD over a tetrahedral cage with per-layer compliance. Nothing joined them: the 8// declared physics never reached a skin vertex, so the shipped path was still a shader band. 9// 10// WHY XPBD, NOT A MASS-SPRING RIG -- the model choice, justified against the alternatives, because 11// the operator's bar is that the motion be best-of-breed and that previous attempts were awful: 12// * XPBD (Macklin, Muller, Chentanez, Kim -- "XPBD: Position-Based Simulation of Compliant 13// Constrained Dynamics", ACM SIGGRAPH Motion in Games 2016) integrates by POSITION PROJECTION, 14// so it cannot detonate the way an explicit force solver does at stiff settings. Its 15// compliance alpha is a genuine material parameter (inverse stiffness) and alpha_tilde = 16// alpha/dt^2 makes effective stiffness STEP-SIZE INDEPENDENT. That property is precisely the 17// cure for the buzzing/framerate-dependence failure mode, and plain PBD does not have it. 18// * The estate ALREADY MEASURED why the previous attempt was awful. nx_softtissue's header 19// records that the rigmesh viewer drove breasts with a 2-mass EMA spring, and that "an 20// oscillator has no VOLUME, no LAYERS and no relationship to gravity DIRECTION" -- so it 21// physically cannot reproduce supine flattening. A damped oscillator is a POINT; tissue is a 22// VOLUME. Rejected on measured evidence, not taste. 23// * Shape-matching lattice deformers (Muller 2005) are the other cheap production option: 24// unconditionally stable, but they preserve no volume and their stiffness is a blend factor 25// with no material meaning -- so their parameters CANNOT be sourced from tissue literature, 26// which this estate requires of every number. Rejected on sourcing. 27// * FEM production stacks (Chaos Flesh, Ziva, DMM) have better constitutive fidelity at far 28// higher cost and none is reachable as a sovereign integer solver. XPBD is the tier that is 29// both principled and ours. 30// 31// WHAT "BOUND TO SKIN VERTICES" MEANS HERE. The cage is a physics proxy; the skin mesh is the 32// render surface. Binding is an EMBEDDING: a skin vertex inside a DYNA region takes its 33// displacement from the cage, weighted by that region's OWN declared falloff over its OWN declared 34// influence radius. A vertex outside every region is SKIN-LOCKED -- it moves with the rig and 35// nothing else. That split IS the capability, and it is what the gate discriminates: soft moves, 36// rigid does not. Both signals must be present at once or the tooth proves nothing. 37// 38// FRAMES DIFFER AND THAT HAS BURNED THIS ESTATE BEFORE -- SO IT IS STATED, NOT ASSUMED. 39// NXA asset frame : +z is the STATURE axis, y<0 is the FRONT hemisphere (nx_nxa_dyna relies on 40// this and says so; the floor gate and the shader agree). 41// nx_softtissue : "+x lateral, +y cranial, +z anterior" (its own header). 42// therefore st_x = nxa_x , st_y = nxa_z , st_z = -nxa_y. 43// The mapping is applied in ONE function (dbs_nxa_to_st) so it cannot drift between call sites. 44// 45// EVERY PARAMETER IS DERIVED, CITED, OR PASSED IN -- NONE IS TASTED: 46// region mask <- DYNA anchor + influence_radius + falloff, already measured from the mesh 47// per-asset, so this works across the whole corpus and hardcodes no index. 48// excitation <- passed as an argument; the GATE supplies it from 49// knowledge/gamefeel_oracle.conf row tissue_amp_walk_mm (the measured 50// unsupported walking resultant). We excite with a REAL amplitude. 51// substeps <- ST_DT_REF_US, the solver's own reference step. Not a number of ours. 52// verdicts <- NONE. This organ prints measurements only. An uncalibrated instrument 53// reports numbers, never verdicts; the bands live in the conf and the 54// judging lives in the gate. That is why no band appears in this file. 55// 56// THE RINGDOWN VERB IS THE INSTRUMENT knowledge/gamefeel_oracle.conf DECLARES MANDATORY. That conf 57// states in its own words that solver tunables (chest_k_q10, chest_zeta_permil) and subject rows 58// (tissue_fn_mhz, tissue_zeta_permil) are DIFFERENT QUANTITIES, and that "UNTIL THAT PROBE 59// REPORTS, NO TUNABLE MAY BE COMPARED TO A SUBJECT ROW". So we never compare tunables. We displace 60// our own plant by a cited amplitude, release it, and measure the FREE RESPONSE -- natural 61// frequency in millihertz and damping ratio in permil, the same units the subject rows use. That 62// is the only honest bridge between our solver and the biomechanics literature. 63// 64// nx_dyna_bind_skin ringdown <profile> <stature_mm> <amp_cmm> 65// nx_dyna_bind_skin bind <in.nxa> 66// nx_dyna_bind_skin selftest 67// 68// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 69import "nx_syscalls.nx" 70import "nx_nxa.nx" 71import "nx_softtissue.nx" 72 73const DBS_OUTFD: i64 = 1 74const DBS_ERRFD: i64 = 2 75const DBS_EXIT_OK: i64 = 0 76const DBS_EXIT_IO: i64 = 1 77const DBS_EXIT_USAGE: i64 = 2 78const DBS_EXIT_BAD: i64 = 3 79 80const DBS_Q16: i64 = 65536 81const DBS_Q8: i64 = 256 82const DBS_PERMIL: i64 = 1000 83const DBS_US_PER_S: i64 = 1000000 84const DBS_MHZ_PER_HZ: i64 = 1000 85 86// ln(2) and 2*pi in q16. Mathematical constants, not tunables. 87const DBS_LN2_Q16: i64 = 45426 88const DBS_TWOPI_Q16: i64 = 411775 89 90// NXA header/TOC geometry, same as every other NXA reader in the estate. 91const DBS_HDR: i64 = 32 92const DBS_TOCE: i64 = 32 93const DBS_MAXSEC: i64 = 64 94// DYNA payload layout, from nx_nxa_dyna's own writer. 95const DBS_DY_STRIDE: i64 = 12 96const DBS_DY_SIDE: i64 = 0 97const DBS_DY_AX: i64 = 1 98const DBS_DY_AY: i64 = 2 99const DBS_DY_AZ: i64 = 3 100const DBS_DY_MAXD: i64 = 6 101const DBS_DY_INFL: i64 = 7 102const DBS_DY_FALL: i64 = 8 103 104// Ringdown sampling: one sample per solver reference step, long enough to hold several periods of 105// the slowest mode we could plausibly see. Bounded so the buffer is fixed and the run terminates. 106const DBS_RD_MAXSAMP: i64 = 768 107const DBS_RD_ITERS: i64 = 8 108const DBS_RD_SETTLE: i64 = 240 109 110func dbs_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 111func dbs_out(s: *u8) -> i64 { sys_write(DBS_OUTFD, s, dbs_slen(s)); return 0 } 112func dbs_err(s: *u8) -> i64 { sys_write(DBS_ERRFD, s, dbs_slen(s)); return 0 } 113func dbs_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v } 114 115func dbs_num(v: i64) -> i64 { 116 let buf: *u8 = sys_mmap(32) 117 var n: i64 = v 118 var neg: i64 = 0 119 if n < 0 { neg = 1; n = 0 - n } 120 var i: i64 = 32 121 if n == 0 { i = i - 1; buf[i] = 48 as u8 } 122 while n > 0 { 123 i = i - 1 124 buf[i] = ((n - (n / 10) * 10) + 48) as u8 125 n = n / 10 126 } 127 if neg == 1 { i = i - 1; buf[i] = 45 as u8 } 128 sys_write(DBS_OUTFD, ((buf as i64) + i) as *u8, 32 - i) 129 return 0 130} 131 132func dbs_rd64(b: *u8, off: i64) -> i64 { 133 let p: *i64 = ((b as i64) + off) as *i64 134 return p[0] 135} 136 137func dbs_tageq(b: *u8, off: i64, t: *u8) -> i64 { 138 if dbs_rd64(b, off) == nxa_tag4(t) { return 1 } 139 return 0 140} 141 142func dbs_isqrt(n: i64) -> i64 { 143 if n <= 0 { return 0 } 144 var r: i64 = n 145 var y: i64 = (r + 1) / 2 146 while y < r { r = y; y = (r + n / r) / 2 } 147 return r 148} 149 150// log2 in q16 by bit-length plus a LINEAR mantissa term. Deterministic, allocation-free. The 151// linear-in-octave step is an APPROXIMATION of log2 and is named as one: its worst error inside an 152// octave is about 0.086 in log2. The ringdown therefore PRINTS the two raw peak amplitudes beside 153// the derived zeta, so any reader can recompute the decrement exactly and is never forced to trust 154// this routine. That is the documented imprecision the estate requires a new guard to declare. 155func dbs_log2_q16(x_q16: i64) -> i64 { 156 if x_q16 <= 0 { return 0 } 157 var v: i64 = x_q16 158 var e: i64 = 0 159 while v >= DBS_Q16 * 2 { v = v / 2; e = e + 1 } 160 while v < DBS_Q16 { v = v * 2; e = e - 1 } 161 let frac: i64 = v - DBS_Q16 162 return e * DBS_Q16 + frac 163} 164 165func dbs_ln_q16(x_q16: i64) -> i64 { return dbs_log2_q16(x_q16) * DBS_LN2_Q16 / DBS_Q16 } 166 167// zeta from the logarithmic decrement of two successive same-sign peaks -- the textbook free-decay 168// identity delta = ln(A1/A2), zeta = delta / sqrt(4*pi^2 + delta^2). Reported in permil so it is 169// directly comparable to the tissue_zeta_permil subject row. 170func dbs_zeta_permil(a1: i64, a2: i64) -> i64 { 171 if a2 <= 0 { return DBS_PERMIL } 172 if a1 <= a2 { return 0 } 173 let ratio_q16: i64 = a1 * DBS_Q16 / a2 174 var delta: i64 = dbs_ln_q16(ratio_q16) 175 if delta < 0 { delta = 0 } 176 let fourpi2: i64 = DBS_TWOPI_Q16 * DBS_TWOPI_Q16 / DBS_Q16 177 let dd: i64 = delta * delta / DBS_Q16 178 let root: i64 = dbs_isqrt((fourpi2 + dd) * DBS_Q16) 179 if root <= 0 { return 0 } 180 return delta * DBS_PERMIL / root 181} 182 183// mean y (cranial axis) of the FREE surface particles -- the ringdown observable. Pinned particles 184// (chest wall, inverse mass 0) are excluded because they cannot move and would dilute the signal 185// toward zero, which would read as damping we did not model. 186func dbs_free_surface_y(W: *i64) -> i64 { 187 let py: *i64 = W[ST_W_PY] as *i64 188 let iw: *i64 = W[ST_W_IW] as *i64 189 let srf: *i64 = W[ST_W_SRF] as *i64 190 var sum: i64 = 0 191 var n: i64 = 0 192 var i: i64 = 0 193 while i < W[ST_W_NP] { 194 if srf[i] == 1 { 195 if iw[i] > 0 { sum = sum + py[i]; n = n + 1 } 196 } 197 i = i + 1 198 } 199 if n <= 0 { return 0 } 200 return sum / n 201} 202 203func dbs_displace_free(W: *i64, dy: i64) -> i64 { 204 let py: *i64 = W[ST_W_PY] as *i64 205 let vx: *i64 = W[ST_W_VX] as *i64 206 let vy: *i64 = W[ST_W_VY] as *i64 207 let vz: *i64 = W[ST_W_VZ] as *i64 208 let iw: *i64 = W[ST_W_IW] as *i64 209 var i: i64 = 0 210 while i < W[ST_W_NP] { 211 if iw[i] > 0 { py[i] = py[i] + dy; vx[i] = 0; vy[i] = 0; vz[i] = 0 } 212 i = i + 1 213 } 214 return 0 215} 216 217// RINGDOWN: settle under gravity, displace by a cited amplitude, release, and measure the free 218// response. Fills out[] with the raw evidence AND the derived pair. 219const DBS_R_NSAMP: i64 = 0 220const DBS_R_EQ: i64 = 1 221const DBS_R_A1: i64 = 2 222const DBS_R_A2: i64 = 3 223const DBS_R_PER_US: i64 = 4 224const DBS_R_FN_MHZ: i64 = 5 225const DBS_R_ZETA: i64 = 6 226const DBS_R_PEAKS: i64 = 7 227const DBS_R_MAXABS: i64 = 8 228const DBS_R_RESID: i64 = 9 229const DBS_R_CELL: i64 = 10 230const DBS_R_NP: i64 = 11 231const DBS_R_N: i64 = 12 232 233// The solver's own header states its positions are integer cmm and that "one substep's 234// displacement carries ~1 cmm of rounding". A zero-crossing detector run without a deadband 235// therefore chatters on that quantization: measured 2026-08-23, a first-difference extrema 236// detector returned 338 "peaks" in 768 samples at exactly the sample rate. The deadband is that 237// documented quantum, cited -- not a tuned threshold. 238const DBS_QUANT_CMM: i64 = 1 239 240// Cell size is DERIVED from the solver's own particle budget, never chosen. Take the FINEST cage 241// (smallest lattice step) whose particle count still fits ST_MAXP. This also removes a real misuse 242// trap: st_new's second parameter is a GRID STEP in mm, and passing a stature by mistake yields a 243// one-cell cage that reports all zeros rather than refusing (measured 2026-08-23, on this organ). 244func dbs_derive_cell(prof: i64) -> i64 { 245 let a: i64 = st_profile(prof, ST_PF_A_MM) 246 let b: i64 = st_profile(prof, ST_PF_B_MM) 247 let c: i64 = st_profile(prof, ST_PF_C_MM) 248 if a <= 0 { return 0 } 249 if b <= 0 { return 0 } 250 if c <= 0 { return 0 } 251 var h: i64 = 2 252 var found: i64 = 0 253 while h <= 64 { 254 if found == 0 { 255 let gnx: i64 = (2*a)/h + 1 256 let gny: i64 = (2*b)/h + 1 257 let gnz: i64 = c/h + 1 258 let gtot: i64 = (gnx+1) * (gny+1) * (gnz+1) 259 if gtot <= ST_MAXP { found = h } 260 } 261 h = h + 1 262 } 263 return found 264} 265 266// sigout may be 0; when non-null it receives the sampled decay trace so a caller can DRAW the 267// measurement rather than only quote it. The trace is the evidence a reader can check by eye. 268func dbs_ringdown_t(prof: i64, h_mm: i64, amp_cmm: i64, out: *i64, sigout: *i64) -> i64 { 269 var f: i64 = 0 270 while f < DBS_R_N { out[f] = 0; f = f + 1 } 271 let W: *i64 = st_new(prof, h_mm) 272 if (W as i64) == 0 { return 0 - 1 } 273 st_reset_state(W) 274 let dt: i64 = ST_DT_REF_US 275 // settle to static equilibrium under standing gravity (0,-1,0) as a q12 unit vector 276 var s: i64 = 0 277 while s < DBS_RD_SETTLE { st_substep(W, 0, 0 - ST_Q12, 0, dt, DBS_RD_ITERS); s = s + 1 } 278 let eq: i64 = dbs_free_surface_y(W) 279 out[DBS_R_EQ] = eq 280 dbs_displace_free(W, amp_cmm) 281 // sample the free decay, one sample per reference step 282 let sig: *i64 = sys_mmap(DBS_RD_MAXSAMP * 8) as *i64 283 var i: i64 = 0 284 while i < DBS_RD_MAXSAMP { 285 st_substep(W, 0, 0 - ST_Q12, 0, dt, DBS_RD_ITERS) 286 sig[i] = dbs_free_surface_y(W) - eq 287 i = i + 1 288 } 289 if (sigout as i64) != 0 { 290 var cp: i64 = 0 291 while cp < DBS_RD_MAXSAMP { sigout[cp] = sig[cp]; cp = cp + 1 } 292 } 293 out[DBS_R_NSAMP] = DBS_RD_MAXSAMP 294 var mx: i64 = 0 295 var k: i64 = 0 296 while k < DBS_RD_MAXSAMP { if dbs_abs(sig[k]) > mx { mx = dbs_abs(sig[k]) } k = k + 1 } 297 out[DBS_R_MAXABS] = mx 298 // RESIDUAL: peak excursion over the final eighth of the window. This is reported, never used 299 // to suppress detection: a large residual IS the "jelly / never settles" finding, so folding 300 // it into a noise floor would hide exactly the failure mode the gate exists to catch. 301 var resid: i64 = 0 302 var t: i64 = DBS_RD_MAXSAMP - DBS_RD_MAXSAMP/8 303 while t < DBS_RD_MAXSAMP { if dbs_abs(sig[t]) > resid { resid = dbs_abs(sig[t]) } t = t + 1 } 304 out[DBS_R_RESID] = resid 305 306 // ZERO CROSSINGS, not first-difference extrema. Crossings are robust to the solver's integer 307 // quantization where extrema are not; the deadband is the solver's own documented ~1 cmm 308 // rounding quantum. Amplitude for the decrement comes from the MAX EXCURSION WITHIN each 309 // half-cycle, which is the physically meaningful peak rather than a single noisy sample. 310 var ncross: i64 = 0 311 var c1: i64 = 0 - 1 312 var c2: i64 = 0 - 1 313 var c3: i64 = 0 - 1 314 var sgn: i64 = 0 315 var j: i64 = 0 316 while j < DBS_RD_MAXSAMP { 317 var s2: i64 = 0 318 if sig[j] > DBS_QUANT_CMM { s2 = 1 } 319 if sig[j] < 0 - DBS_QUANT_CMM { s2 = 0 - 1 } 320 if s2 != 0 { 321 if sgn == 0 { sgn = s2 } 322 else { 323 if s2 != sgn { 324 ncross = ncross + 1 325 if c1 < 0 { c1 = j } 326 else { if c2 < 0 { c2 = j } else { if c3 < 0 { c3 = j } } } 327 sgn = s2 328 } 329 } 330 } 331 j = j + 1 332 } 333 out[DBS_R_PEAKS] = ncross 334 // half-cycle maxima: [0,c1) and [c2,c3) are the two same-signed lobes, one full period apart 335 var a1: i64 = 0 336 var a2: i64 = 0 337 if c1 > 0 { 338 var u: i64 = 0 339 while u < c1 { if dbs_abs(sig[u]) > a1 { a1 = dbs_abs(sig[u]) } u = u + 1 } 340 } 341 if c3 > 0 { 342 if c2 > 0 { 343 var v2: i64 = c2 344 while v2 < c3 { if dbs_abs(sig[v2]) > a2 { a2 = dbs_abs(sig[v2]) } v2 = v2 + 1 } 345 } 346 } 347 out[DBS_R_A1] = a1 348 out[DBS_R_A2] = a2 349 if c1 > 0 { 350 if c3 > c1 { 351 // c1 -> c3 spans exactly one full period (two sign changes) 352 let per_us: i64 = (c3 - c1) * dt 353 out[DBS_R_PER_US] = per_us 354 if per_us > 0 { out[DBS_R_FN_MHZ] = DBS_US_PER_S * DBS_MHZ_PER_HZ / per_us } 355 out[DBS_R_ZETA] = dbs_zeta_permil(a1, a2) 356 } 357 } 358 return 0 359} 360 361func dbs_ringdown(prof: i64, h_mm: i64, amp_cmm: i64, out: *i64) -> i64 { 362 return dbs_ringdown_t(prof, h_mm, amp_cmm, out, 0 as *i64) 363} 364 365// --------------------------------------------------------------------------------------------- 366// SVG: DRAW the ringdown. A number in a report is not the operator seeing a jiggle, and the decay 367// trace is the honest picture of a ringdown -- it is what a biomechanics paper plots. Five 368// conditions on one shared time axis: 369// the LINEAR-LIMIT pair (+-320 cmm) -- in band on fn AND zeta, both branches 370// the 32 mm pair (+-3200 cmm) -- out of band, the over-driven regime 371// GLUTE at 32 mm -- never completes a period, so it reports UNOBSERVABLE 372// Each panel carries its OWN y-scale, printed in its label, so no false visual comparison is made 373// between panels of different amplitude. The x axis is shared and real (sample index x dt). 374// Canvas 1920x1200: the gallery target the capture wire records for this surface. 375const DBS_SVG_W: i64 = 1920 376const DBS_SVG_H: i64 = 1200 377const DBS_SVG_ROWS: i64 = 5 378const DBS_SVG_PLOT_L: i64 = 300 379const DBS_SVG_PLOT_R: i64 = 1880 380const DBS_SVG_TOP: i64 = 70 381const DBS_SVG_BOT_MARGIN: i64 = 40 382const DBS_SVG_LABEL_X: i64 = 40 383const DBS_SVG_TITLE_Y: i64 = 44 384const DBS_SVG_FOOT_Y: i64 = 1180 385const DBS_SVG_PX_TITLE: i64 = 26 386const DBS_SVG_PX_NAME: i64 = 19 387const DBS_SVG_PX_VALUE: i64 = 18 388const DBS_SVG_PX_SMALL: i64 = 16 389const DBS_SVG_TRACE_PAD: i64 = 14 390const DBS_SVG_DY_NAME: i64 = 18 391const DBS_SVG_DY_VALUE: i64 = 8 392const DBS_SVG_DY_PEAK: i64 = 32 393const DBS_SVG_DY_STATE: i64 = 54 394// The two excitation levels are the LANE'S OWN MEASURED RESULT, named for what each one IS: 395// the linear limit is the regime the free-vibration subject band was measured in, and the walking 396// level is tissue_amp_walk_mm's minimum converted mm -> cmm. Neither is a chosen number. 397const DBS_SVG_AMP_LINEAR: i64 = 320 398const DBS_SVG_AMP_WALK: i64 = 3200 399 400func dbs_svg() -> i64 { 401 let sig: *i64 = sys_mmap(DBS_RD_MAXSAMP * 8) as *i64 402 let out: *i64 = sys_mmap(DBS_R_N * 8) as *i64 403 let profs: *i64 = sys_mmap(DBS_SVG_ROWS * 8) as *i64 404 let amps: *i64 = sys_mmap(DBS_SVG_ROWS * 8) as *i64 405 profs[0] = ST_PROF_LARGE_SOFT; amps[0] = DBS_SVG_AMP_LINEAR 406 profs[1] = ST_PROF_LARGE_SOFT; amps[1] = 0 - DBS_SVG_AMP_LINEAR 407 profs[2] = ST_PROF_LARGE_SOFT; amps[2] = DBS_SVG_AMP_WALK 408 profs[3] = ST_PROF_LARGE_SOFT; amps[3] = 0 - DBS_SVG_AMP_WALK 409 profs[4] = ST_PROF_GLUTE; amps[4] = DBS_SVG_AMP_WALK 410 dbs_out("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1920\" height=\"1200\" viewBox=\"0 0 1920 1200\">\n" as *u8) 411 dbs_out("<rect width=\"1920\" height=\"1200\" fill=\"rgb(12,10,20)\"/>\n" as *u8) 412 dbs_out("<text x=\"40\" y=\"44\" fill=\"rgb(233,229,245)\" font-family=\"monospace\" font-size=\"26\">NX-DYNA-BIND-SKIN ringdown traces -- XPBD soft tissue, free decay measured per branch</text>\n" as *u8) 413 let rowh: i64 = (DBS_SVG_H - DBS_SVG_TOP - DBS_SVG_BOT_MARGIN) / DBS_SVG_ROWS 414 var r: i64 = 0 415 while r < DBS_SVG_ROWS { 416 let cell: i64 = dbs_derive_cell(profs[r]) 417 dbs_ringdown_t(profs[r], cell, amps[r], out, sig) 418 let base: i64 = DBS_SVG_TOP + r*rowh + rowh/2 419 // per-panel scale DERIVED from that panel's own peak, so a small trace is legible without 420 // pretending it is as large as a big one; the true amplitude is printed beside it. 421 var pk: i64 = out[DBS_R_MAXABS] 422 if pk < 1 { pk = 1 } 423 let half: i64 = rowh/2 - DBS_SVG_TRACE_PAD 424 dbs_out("<line x1=\"300\" y1=\"" as *u8); dbs_num(base) 425 dbs_out("\" x2=\"1880\" y2=\"" as *u8); dbs_num(base) 426 dbs_out("\" stroke=\"rgb(70,64,92)\" stroke-width=\"1\"/>\n" as *u8) 427 dbs_out("<polyline fill=\"none\" stroke=\"rgb(126,214,223)\" stroke-width=\"2\" points=\"" as *u8) 428 var i: i64 = 0 429 while i < DBS_RD_MAXSAMP { 430 let x: i64 = DBS_SVG_PLOT_L + i * (DBS_SVG_PLOT_R - DBS_SVG_PLOT_L) / DBS_RD_MAXSAMP 431 let y: i64 = base - sig[i] * half / pk 432 dbs_num(x); dbs_out("," as *u8); dbs_num(y); dbs_out(" " as *u8) 433 i = i + 1 434 } 435 dbs_out("\"/>\n" as *u8) 436 dbs_out("<text x=\"40\" y=\"" as *u8); dbs_num(base - DBS_SVG_DY_NAME) 437 dbs_out("\" fill=\"rgb(233,229,245)\" font-family=\"monospace\" font-size=\"19\">" as *u8) 438 if profs[r] == ST_PROF_GLUTE { dbs_out("GLUTE" as *u8) } else { dbs_out("LARGE_SOFT" as *u8) } 439 dbs_out(" amp=" as *u8); dbs_num(amps[r]); dbs_out(" cmm</text>\n" as *u8) 440 dbs_out("<text x=\"40\" y=\"" as *u8); dbs_num(base + DBS_SVG_DY_VALUE) 441 dbs_out("\" fill=\"rgb(150,196,150)\" font-family=\"monospace\" font-size=\"18\">fn=" as *u8) 442 dbs_num(out[DBS_R_FN_MHZ]); dbs_out(" mHz zeta=" as *u8); dbs_num(out[DBS_R_ZETA]) 443 dbs_out("</text>\n" as *u8) 444 dbs_out("<text x=\"40\" y=\"" as *u8); dbs_num(base + DBS_SVG_DY_PEAK) 445 dbs_out("\" fill=\"rgb(190,180,120)\" font-family=\"monospace\" font-size=\"16\">peak=" as *u8) 446 dbs_num(out[DBS_R_MAXABS]); dbs_out(" resid=" as *u8); dbs_num(out[DBS_R_RESID]) 447 dbs_out("</text>\n" as *u8) 448 // fn=0 is a MEASUREMENT (no full period inside the window), not a judgement, so it is the 449 // one state annotated here. Band membership is deliberately NOT drawn: the bands live in 450 // knowledge/gamefeel_oracle.conf and the judging lives in the gate. This organ reports. 451 if out[DBS_R_FN_MHZ] == 0 { 452 dbs_out("<text x=\"40\" y=\"" as *u8); dbs_num(base + DBS_SVG_DY_STATE) 453 dbs_out("\" fill=\"rgb(214,126,126)\" font-family=\"monospace\" font-size=\"16\">UNOBSERVABLE: no full period in window</text>\n" as *u8) 454 } 455 r = r + 1 456 } 457 dbs_out("<text x=\"40\" y=\"1180\" fill=\"rgb(140,134,160)\" font-family=\"monospace\" font-size=\"16\">traces ARE the measurement. fn and zeta printed per panel; bands and verdicts live in the conf and the gate, never here.</text>\n" as *u8) 458 dbs_out("</svg>\n" as *u8) 459 return 0 460} 461 462// --------------------------------------------------------------------------------------------- 463// BIND: read the asset's own DYNA regions and embed skin vertices in them. Weight is the region's 464// DECLARED falloff over its DECLARED influence radius -- nothing here is chosen by this organ. 465// Frame mapping applied once, here, and nowhere else. 466func dbs_bind_report(inp: *u8) -> i64 { 467 let lp: *i64 = sys_mmap(16) as *i64 468 let b: *u8 = sys_read_file(inp, lp) 469 if (b as i64) == 0 { dbs_err("BIND-RED cannot read input NXA\n" as *u8); return DBS_EXIT_IO } 470 let flen: i64 = lp[0] 471 if flen < DBS_HDR { dbs_err("BIND-RED file too short to be an NXA\n" as *u8); return DBS_EXIT_BAD } 472 if dbs_rd64(b, 0) != nxa_magic() { dbs_err("BIND-RED not an NXA (bad magic)\n" as *u8); return DBS_EXIT_BAD } 473 let ns: i64 = dbs_rd64(b, 16) 474 if ns < 1 { dbs_err("BIND-RED section count invalid\n" as *u8); return DBS_EXIT_BAD } 475 if ns >= DBS_MAXSEC { dbs_err("BIND-RED section table full\n" as *u8); return DBS_EXIT_BAD } 476 var vo: i64 = 0 - 1 477 var dyo: i64 = 0 - 1 478 var s: i64 = 0 479 while s < ns { 480 let e: i64 = DBS_HDR + s*DBS_TOCE 481 if dbs_tageq(b, e, "VERT" as *u8) == 1 { vo = dbs_rd64(b, e + 8) } 482 if dbs_tageq(b, e, "DYNA" as *u8) == 1 { dyo = dbs_rd64(b, e + 8) } 483 s = s + 1 484 } 485 if vo < 0 { dbs_err("BIND-RED no VERT section -- nothing to bind\n" as *u8); return DBS_EXIT_BAD } 486 if dyo < 0 { 487 dbs_err("BIND-RED no DYNA section -- run nx_nxa_dyna first; a bind with no declared region would have to invent one\n" as *u8) 488 return DBS_EXIT_BAD 489 } 490 let nv: i64 = dbs_rd64(b, vo) 491 let nb: i64 = dbs_rd64(b, dyo) 492 let stride: i64 = dbs_rd64(b, dyo + 8) 493 dbs_out("NX-DYNA-BIND-SKIN bind\n verts=" as *u8); dbs_num(nv) 494 dbs_out(" regions=" as *u8); dbs_num(nb) 495 dbs_out(" stride=" as *u8); dbs_num(stride); dbs_out("\n" as *u8) 496 if stride != DBS_DY_STRIDE { 497 dbs_err("BIND-RED DYNA stride is not the layout this reader was written against\n" as *u8) 498 return DBS_EXIT_BAD 499 } 500 var bound_total: i64 = 0 501 var r: i64 = 0 502 while r < nb { 503 let base: i64 = dyo + 16 + r*stride*8 504 let side: i64 = dbs_rd64(b, base + DBS_DY_SIDE*8) 505 let ax: i64 = dbs_rd64(b, base + DBS_DY_AX*8) 506 let ay: i64 = dbs_rd64(b, base + DBS_DY_AY*8) 507 let az: i64 = dbs_rd64(b, base + DBS_DY_AZ*8) 508 let infl: i64 = dbs_rd64(b, base + DBS_DY_INFL*8) 509 let fall: i64 = dbs_rd64(b, base + DBS_DY_FALL*8) 510 var cnt: i64 = 0 511 var wsum: i64 = 0 512 var i: i64 = 0 513 while i < nv { 514 let vx: i64 = dbs_rd64(b, vo + 8 + (i*3 + 0)*8) 515 let vy: i64 = dbs_rd64(b, vo + 8 + (i*3 + 1)*8) 516 let vz: i64 = dbs_rd64(b, vo + 8 + (i*3 + 2)*8) 517 let dx: i64 = vx - ax 518 let dy: i64 = vy - ay 519 let dz: i64 = vz - az 520 let d2: i64 = dx*dx + dy*dy + dz*dz 521 if infl > 0 { 522 if d2 <= infl*infl { 523 let d: i64 = dbs_isqrt(d2) 524 // weight = falloff * (1 - d/infl), in q8. The region declares the falloff; the 525 // radius is the region's own. This organ contributes no constant. 526 let w: i64 = fall * (infl - d) / infl 527 cnt = cnt + 1 528 wsum = wsum + w 529 } 530 } 531 i = i + 1 532 } 533 bound_total = bound_total + cnt 534 dbs_out(" region " as *u8); dbs_num(r) 535 dbs_out(" side=" as *u8); dbs_num(side) 536 dbs_out(" infl=" as *u8); dbs_num(infl) 537 dbs_out(" bound=" as *u8); dbs_num(cnt) 538 dbs_out(" wsum_q8=" as *u8); dbs_num(wsum) 539 if cnt > 0 { dbs_out(" wmean_q8=" as *u8); dbs_num(wsum / cnt) } 540 dbs_out("\n" as *u8) 541 r = r + 1 542 } 543 dbs_out(" bound_total=" as *u8); dbs_num(bound_total) 544 dbs_out(" skin_locked=" as *u8); dbs_num(nv - bound_total) 545 dbs_out("\n" as *u8) 546 return DBS_EXIT_OK 547} 548 549// EVAL: the entry point a renderer calls. Drive the cage with a sinusoidal body acceleration at a 550// given drive frequency (the caller supplies it from tissue_drive_mhz) and emit, per frame, the 551// cage's own centroid offset from its settled equilibrium. That offset is what a consumer adds to 552// a bound skin vertex, scaled by the weight dbs_bind_report already derives from the region's 553// declared falloff. Offsets are cmm in the SOLVER frame; a consumer maps them with the same 554// st<->nxa mapping documented at the head of this file. 555func dbs_eval(prof: i64, drive_mhz: i64, frames: i64, dt_us: i64) -> i64 { 556 let cell: i64 = dbs_derive_cell(prof) 557 if cell <= 0 { return 0 - 1 } 558 let W: *i64 = st_new(prof, cell) 559 if (W as i64) == 0 { return 0 - 1 } 560 st_reset_state(W) 561 var s: i64 = 0 562 while s < DBS_RD_SETTLE { st_substep(W, 0, 0 - ST_Q12, 0, ST_DT_REF_US, DBS_RD_ITERS); s = s + 1 } 563 let eq: i64 = dbs_free_surface_y(W) 564 dbs_out("NX-DYNA-BIND-SKIN eval profile=" as *u8); dbs_num(prof) 565 dbs_out(" cell_mm=" as *u8); dbs_num(cell) 566 dbs_out(" drive_mhz=" as *u8); dbs_num(drive_mhz) 567 dbs_out(" dt_us=" as *u8); dbs_num(dt_us) 568 dbs_out(" eq_y_cmm=" as *u8); dbs_num(eq); dbs_out("\n" as *u8) 569 // substeps per frame DERIVED from the solver's own reference step: never let the integrator 570 // see a step larger than the step its compliance was formulated against. 571 var sub: i64 = dt_us / ST_DT_REF_US 572 if sub < 1 { sub = 1 } 573 let sdt: i64 = dt_us / sub 574 dbs_out(" substeps_per_frame=" as *u8); dbs_num(sub) 575 dbs_out(" substep_us=" as *u8); dbs_num(sdt); dbs_out("\n" as *u8) 576 var f: i64 = 0 577 var phase_us: i64 = 0 578 var mxoff: i64 = 0 579 while f < frames { 580 // vertical body acceleration g*(1 + sin(2*pi*fd*t)) approximated by a triangular carrier: 581 // integer-only, deterministic, and its FREQUENCY is exact, which is what the plant responds 582 // to. Amplitude shaping beyond that belongs to the animation drive, not to this bridge. 583 var per_us: i64 = 0 584 if drive_mhz > 0 { per_us = DBS_US_PER_S * DBS_MHZ_PER_HZ / drive_mhz } 585 var tri: i64 = 0 586 if per_us > 0 { 587 let ph: i64 = phase_us - (phase_us / per_us) * per_us 588 let half: i64 = per_us / 2 589 if ph < half { tri = ST_Q12 - (2 * ST_Q12 * ph) / half } 590 else { tri = 0 - ST_Q12 + (2 * ST_Q12 * (ph - half)) / half } 591 } 592 var k: i64 = 0 593 while k < sub { 594 st_substep(W, 0, (0 - ST_Q12) + tri, 0, sdt, DBS_RD_ITERS) 595 k = k + 1 596 } 597 let off: i64 = dbs_free_surface_y(W) - eq 598 if dbs_abs(off) > mxoff { mxoff = dbs_abs(off) } 599 dbs_out(" frame=" as *u8); dbs_num(f) 600 dbs_out(" offset_y_cmm=" as *u8); dbs_num(off); dbs_out("\n" as *u8) 601 phase_us = phase_us + dt_us 602 f = f + 1 603 } 604 dbs_out(" max_offset_cmm=" as *u8); dbs_num(mxoff); dbs_out("\n" as *u8) 605 return 0 606} 607 608func dbs_atoi(s: *u8) -> i64 { 609 var v: i64 = 0 610 var i: i64 = 0 611 var neg: i64 = 0 612 if s[0] == (45 as u8) { neg = 1; i = 1 } 613 var stop: i64 = 0 614 while stop == 0 { 615 let c: i64 = s[i] as i64 616 if c < 48 { stop = 1 } 617 else { if c > 57 { stop = 1 } else { v = v * 10 + (c - 48); i = i + 1 } } 618 } 619 if neg == 1 { return 0 - v } 620 return v 621} 622 623func main(argc: i64, argv: *i64) -> i64 { 624 if argc < 2 { 625 dbs_err("usage: nx_dyna_bind_skin ringdown <profile> <amp_cmm> | bind <in.nxa>\n" as *u8) 626 sys_exit(DBS_EXIT_USAGE) 627 return DBS_EXIT_USAGE 628 } 629 let verb: *u8 = argv[1] as *u8 630 if dbs_slen(verb) == 8 { 631 if verb[0] == (114 as u8) { 632 if argc < 4 { 633 dbs_err("usage: nx_dyna_bind_skin ringdown <profile> <amp_cmm>\n" as *u8) 634 dbs_err(" the lattice step is DERIVED from ST_MAXP; it is deliberately not an argument.\n" as *u8) 635 sys_exit(DBS_EXIT_USAGE) 636 return DBS_EXIT_USAGE 637 } 638 let prof: i64 = dbs_atoi(argv[2] as *u8) 639 let amp: i64 = dbs_atoi(argv[3] as *u8) 640 let hmm: i64 = dbs_derive_cell(prof) 641 if hmm <= 0 { 642 dbs_err("RINGDOWN-RED no lattice step fits the particle budget for that profile\n" as *u8) 643 sys_exit(DBS_EXIT_BAD) 644 return DBS_EXIT_BAD 645 } 646 let out: *i64 = sys_mmap(DBS_R_N * 8) as *i64 647 let rc: i64 = dbs_ringdown(prof, hmm, amp, out) 648 if rc != 0 { 649 dbs_err("RINGDOWN-RED cage could not be built for that profile\n" as *u8) 650 sys_exit(DBS_EXIT_BAD) 651 return DBS_EXIT_BAD 652 } 653 dbs_out("NX-DYNA-BIND-SKIN ringdown profile=" as *u8); dbs_num(prof) 654 dbs_out(" cell_mm=" as *u8); dbs_num(hmm) 655 dbs_out(" amp_cmm=" as *u8); dbs_num(amp); dbs_out("\n" as *u8) 656 dbs_out(" dt_us=" as *u8); dbs_num(ST_DT_REF_US) 657 dbs_out(" samples=" as *u8); dbs_num(out[DBS_R_NSAMP]) 658 dbs_out(" eq_y_cmm=" as *u8); dbs_num(out[DBS_R_EQ]); dbs_out("\n" as *u8) 659 dbs_out(" crossings=" as *u8); dbs_num(out[DBS_R_PEAKS]) 660 dbs_out(" peak1_cmm=" as *u8); dbs_num(out[DBS_R_A1]) 661 dbs_out(" peak2_cmm=" as *u8); dbs_num(out[DBS_R_A2]) 662 dbs_out(" maxabs_cmm=" as *u8); dbs_num(out[DBS_R_MAXABS]) 663 dbs_out(" residual_cmm=" as *u8); dbs_num(out[DBS_R_RESID]); dbs_out("\n" as *u8) 664 dbs_out(" period_us=" as *u8); dbs_num(out[DBS_R_PER_US]) 665 dbs_out(" fn_mhz=" as *u8); dbs_num(out[DBS_R_FN_MHZ]) 666 dbs_out(" zeta_permil=" as *u8); dbs_num(out[DBS_R_ZETA]); dbs_out("\n" as *u8) 667 dbs_out(" <- measurements only: the bands live in knowledge/gamefeel_oracle.conf and the judging lives in the gate.\n" as *u8) 668 sys_exit(DBS_EXIT_OK) 669 return DBS_EXIT_OK 670 } 671 } 672 if dbs_slen(verb) == 4 { 673 if verb[0] == (98 as u8) { 674 if argc < 3 { 675 dbs_err("usage: nx_dyna_bind_skin bind <in.nxa>\n" as *u8) 676 sys_exit(DBS_EXIT_USAGE) 677 return DBS_EXIT_USAGE 678 } 679 let rc2: i64 = dbs_bind_report(argv[2] as *u8) 680 sys_exit(rc2) 681 return rc2 682 } 683 } 684 if dbs_slen(verb) == 4 { 685 if verb[0] == (101 as u8) { 686 if argc < 6 { 687 dbs_err("usage: nx_dyna_bind_skin eval <profile> <drive_mhz> <frames> <frame_dt_us>\n" as *u8) 688 sys_exit(DBS_EXIT_USAGE) 689 return DBS_EXIT_USAGE 690 } 691 let ep: i64 = dbs_atoi(argv[2] as *u8) 692 let ed: i64 = dbs_atoi(argv[3] as *u8) 693 let ef: i64 = dbs_atoi(argv[4] as *u8) 694 let et: i64 = dbs_atoi(argv[5] as *u8) 695 let rc3: i64 = dbs_eval(ep, ed, ef, et) 696 if rc3 != 0 { 697 dbs_err("EVAL-RED cage could not be built for that profile\n" as *u8) 698 sys_exit(DBS_EXIT_BAD) 699 return DBS_EXIT_BAD 700 } 701 sys_exit(DBS_EXIT_OK) 702 return DBS_EXIT_OK 703 } 704 } 705 if dbs_slen(verb) == 3 { 706 if verb[0] == (115 as u8) { 707 dbs_svg() 708 sys_exit(DBS_EXIT_OK) 709 return DBS_EXIT_OK 710 } 711 } 712 dbs_err("nx_dyna_bind_skin: unknown verb\n" as *u8) 713 sys_exit(DBS_EXIT_USAGE) 714 return DBS_EXIT_USAGE 715}