code wiki / (root) / nx_xport_lib.nx

nx_xport_lib.nx source

↩ module page · 906 lines · 39836 B

1// nx_xport_lib.nx -- DC12: ONE AUTHORED ASSET INTO FOUR CONSUMERS, MEASURED BY ONE RULER. 2// 3// WHY THIS EXISTS: the /compare/dcc board claims a single authored asset serves storefront, game, VR and 4// clinical use. Today that is a PLAN. Nothing measures it, so a consumer could silently receive a 5// decimated, re-scaled or re-wound mesh and no instrument in the estate would say so. This lib turns the 6// claim into a number per consumer. 7// 8// THE RULER IS md_measure AND THERE IS NOT A SECOND ONE. nx_mmdev_lib already measures exact 9// point-to-triangle deviation in tenths of a millimetre and is gate-proven to read a known 5.0 mm lift as 10// 5.0 mm and a self-compare as zero. A new deviation metric on this board would be a duplicate ruler, i.e. 11// a defect. Everything below COMPOSES it. The exporters are composed too -- write_glb and write_obj are 12// called, never re-implemented -- so what is measured is the artifact the real storefront lane ships. 13// 14// WHAT IS MEASURED, PER CONSUMER: the artifact is written, then the geometry is RECOVERED BACK OUT OF THE 15// ARTIFACT BYTES (not from an in-memory copy that never crossed the format) and measured against the 16// source in BOTH directions. Deviation alone is not sufficient and this lib says so in code: a mesh 17// mirrored about its own centre has ZERO point-to-triangle deviation and is still wrong, so orientation 18// (the sign of the signed volume) and scale (per-axis AABB extent) are separate axes, not decoration. 19// 20// THE BOUND IS DERIVED, NEVER PICKED. Every consumer holds coordinates in its own integer unit. A round 21// trip through a unit of U integer steps per metre, from the ruler's NM_UNIT_PER_M steps per metre, spends 22// at most half a step in each direction, and NOTHING AT ALL when U is an integer multiple of the ruler's 23// unit (the forward map is a multiplication and the inverse an exact division). The glTF path adds a 24// second, computed term: write_glb encodes each integer coordinate with gl_i2f32, which TRUNCATES the 25// mantissa, so the loss is the float32 ulp AT THIS ASSET'S OWN COORDINATE MAGNITUDE -- computed from the 26// mesh, zero while the magnitude fits the 24-bit significand, and rising by itself if it ever does not. 27// 28// ABSTENTION IS NOT A SCORE OF ZERO. A consumer that cannot be measured reports UNREACHED with a named 29// reason. A consumer that silently reads zero deviation is indistinguishable from one that was never 30// measured, which is the whole failure this rung exists to prevent. 31// 32// DECLARED IMPRECISIONS, so no reader takes them as exact: 33// (1) The volume-magnitude tolerance uses the AABB as a proxy for surface area. A convoluted mesh has 34// more area per unit volume than its box, so on such a mesh that tolerance is OPTIMISTIC. It is a 35// SUPPORTING axis only; the load-bearing orientation test is the volume SIGN, which needs no 36// tolerance and cannot be optimistic. 37// (2) The signed volume is the enclosed volume only for a CLOSED mesh. For an open mesh it is still a 38// valid like-for-like shape statistic, because source and recovered are evaluated with the SAME 39// origin (the source AABB minimum), but it is not a volume. 40// (3) md_measure's own declared imprecisions carry through: its mean is valence-weighted and its p95 is 41// exact to a 0.1 mm bin. Read its header. 42// (4) The glTF recovery reads accessor element counts by scanning the JSON chunk for the count key in 43// write_glb's own emission order. It is a targeted scan of a format we own the writer of, not a 44// general JSON parser, and every structural assumption it makes is checked before it is used. 45// 46// OWED CONSOLIDATION, named rather than left silent: the NXA VERT unit is spelled here as 47// XP_NXA_UNIT_PER_M and ALSO as MG_NXA_UNITS_PER_M in _hdl_build/nx_mesh2glb.nx, while nx_fbx2nxa and 48// nx_softtissue each restate it in prose. The format owner nx_nxa.nx should own the unit the way it 49// already owns the magic, the tags and the checksum. That edit is not made here because nx_nxa.nx is 50// imported by thirty-odd organs and a shared-tree edit does not belong inside an unrelated rung's proof. 51// Likewise xp_nxa_write is the fourth place an NXA container is assembled (nx_fbx2nxa inline in main, 52// nx_nxa_texc_gate and nx_nxa_texbake_gate as fixtures); a shared writer on nx_nxa.nx is the retire target. 53// 54// license_tier: ORIGINAL No hw writes (Rule 26). 55 56import "nx_syscalls.nx" 57import "nx_vecmath.nx" 58import "nx_nxmesh_lib.nx" 59import "nx_mmdev_lib.nx" 60import "nx_nxa.nx" 61import "nx_gltf_export.nx" 62import "nx_obj_export.nx" 63import "nx_ge_str_lib.nx" 64 65// ---- the unit ladder, all as INTEGER UNITS PER METRE ------------------------------------------- 66// NM_UNIT_PER_M = 10000 (0.1 mm) is the ruler's unit and comes from nx_nxmesh_lib. 67// write_glb emits a node scale of 0.0009765625 = 1/1024, so its integers are 1024 per metre. Both the 68// glb and the obj writer take the same fx1024 vertex buffer; the obj header says so in its own words. 69const XP_GL_UNIT_PER_M: i64 = 1024 70// NXA VERT is 0.01 mm. Three independent witnesses: nx_fbx2nxa's conversion comment, nx_mesh2glb's 71// MG_NXA_UNITS_PER_M = 100000, and nx_softtissue's "positions in cmm (0.01 mm) -- the NXA VERT unit". 72const XP_NXA_UNIT_PER_M: i64 = 100000 73 74const XP_MODE_0644: i64 = 420 75const XP_PATH_CAP: i64 = 512 76const XP_PERMIL: i64 = 1000 77// 2^62. The accumulator ceiling every derived divisor below is sized against, leaving a full factor of 78// two of headroom under the i64 maximum. 79const XP_I64_SAFE: i64 = 4611686018427387904 80 81// ---- glTF container constants, all read off write_glb's own emission ---------------------------- 82const XP_GLB_MAGIC: i64 = 1179937895 // "glTF" little-endian 83const XP_GLB_VER: i64 = 2 84const XP_GLB_JSONLEN_OFF: i64 = 12 85const XP_GLB_JSON_OFF: i64 = 20 // 12 byte header + 8 byte chunk header 86const XP_GLB_CHUNKHDR: i64 = 8 87const XP_GLB_VEC3_BYTES: i64 = 12 88const XP_GLB_U32_BYTES: i64 = 4 89// POSITION and NORMAL are both VEC3 float32, so the index data begins two vertex arrays in. 90const XP_GLB_IDX_START_MUL: i64 = 24 91// The node scale is the ONLY thing that tells a reader what the integers mean. If it is not the declared 92// 1/1024 the recovery REFUSES rather than guessing, because a silent mis-scale is exactly the defect 93// this rung exists to catch. 94const XP_GLB_SCALE_LIT: *u8 = "\"scale\":[0.0009765625,0.0009765625,0.0009765625]" 95const XP_GLB_COUNT_KEY: *u8 = "\"count\":" 96 97// ---- NXA container constants ------------------------------------------------------------------- 98const XP_NXA_SECTIONS: i64 = 2 // VERT + TRIS 99const XP_NXA_TOC_BYTE: i64 = 32 100const XP_NXA_TOC_ROW_WORDS: i64 = 4 // tag, byte offset, word length, payload check 101 102// ---- weld hash --------------------------------------------------------------------------------- 103// Teschner et al. 2003 spatial hash multipliers. CORRECTNESS DOES NOT DEPEND ON THEM: every probe 104// compares the full coordinate triple exactly, so a bad multiplier costs probes and never an answer. 105const XP_HASH_A: i64 = 73856093 106const XP_HASH_B: i64 = 19349663 107const XP_HASH_C: i64 = 83492791 108// Table sized so it is at most half full, which keeps linear probing at expected O(1). 109const XP_HASH_LOAD_INV: i64 = 2 110 111// One permil of slack on the volume axis absorbs the integer division inside xp_vol6 itself. 112const XP_VOL_PERMIL_FLOOR: i64 = 1 113 114// ---- consumers --------------------------------------------------------------------------------- 115const XP_C_STORE_GLB: i64 = 0 116const XP_C_STORE_OBJ: i64 = 1 117const XP_C_GAME_NXA: i64 = 2 118const XP_C_VR_FRAME: i64 = 3 119const XP_C_CLINICAL: i64 = 4 120const XP_C_N: i64 = 5 121 122// ---- per-consumer result slots ----------------------------------------------------------------- 123const XP_R_REACHED: i64 = 0 // 1 iff an artifact was produced for this consumer 124const XP_R_WHY: i64 = 1 // 0 when the verdict is measured, else the reason it is not 125const XP_R_BYTES: i64 = 2 126const XP_R_TRIS_SRC: i64 = 3 127const XP_R_TRIS_REC: i64 = 4 128const XP_R_MEAN_AB: i64 = 5 // source vertices -> recovered surface 129const XP_R_P95_AB: i64 = 6 130const XP_R_MAX_AB: i64 = 7 131const XP_R_MEAN_BA: i64 = 8 // recovered vertices -> source surface 132const XP_R_P95_BA: i64 = 9 133const XP_R_MAX_BA: i64 = 10 134const XP_R_VOLSIGN_OK: i64 = 11 135const XP_R_VOL_PERMIL: i64 = 12 136const XP_R_VOL_PERMIL_BOUND: i64 = 13 // -1 = UNMEASURED (degenerate AABB), never a silent zero 137const XP_R_EXT_DELTA: i64 = 14 138const XP_R_EXT_BOUND: i64 = 15 139const XP_R_BOUND: i64 = 16 // the derived 3D deviation bound, in tenths of a millimetre 140const XP_R_AXIS: i64 = 17 // per-axis bound = quantisation term + float32 term 141const XP_R_AXISQ: i64 = 18 142const XP_R_AXISF: i64 = 19 143const XP_R_VERTS: i64 = 20 144const XP_R_PASS: i64 = 21 145const XP_R_N: i64 = 22 146 147// ---- reasons a consumer is not measured -------------------------------------------------------- 148const XP_WHY_OK: i64 = 0 149const XP_WHY_NO_MESH_PARAM: i64 = 1 // the consumer's entry point takes no mesh at all 150const XP_WHY_RASTER_ONLY: i64 = 2 // the consumer's artifact is an image: geometry is not in it 151const XP_WHY_ARTIFACT_MISSING: i64 = 3 152const XP_WHY_ARTIFACT_MALFORMED: i64 = 4 153const XP_WHY_SCALE_UNDECLARED: i64 = 5 // refused rather than mis-scaled 154const XP_WHY_RULER_REFUSED: i64 = 6 // md_measure itself declined; its code is in the ruler's res 155const XP_WHY_COUNT_MISMATCH: i64 = 7 156 157const XP_F_GLB: *u8 = "xport.glb" 158const XP_F_OBJ: *u8 = "xport.obj" 159const XP_F_NXA: *u8 = "xport.nxa" 160const XP_F_MSH: *u8 = "xport.nxmsh2" 161 162// ---- the VR abstention, expressed so it CANNOT go stale ---------------------------------------- 163// nx_vrframe is the VR consumer and it cannot receive an authored mesh, for two reasons that are facts 164// about its source rather than opinions about the lane: vf_build takes no mesh argument (its whole scene 165// is vf_pill, a six-row constant table), and what it emits is a PNG, so geometry could not be recovered 166// from it even if a mesh could be fed in. Both reasons are CHECKED against the live source on every run. 167// If either changes the claim must be re-adjudicated, not inherited -- a standing list of unreachable 168// things that nobody re-measures becomes a list nobody trusts. 169const XP_VR_SRC: *u8 = "buildroot/runtime/_hdl_build/nx_vrframe.nx" 170const XP_VR_SIG: *u8 = "func vf_build(distort: i64, outpath: *u8) -> i64" 171const XP_VR_RASTER: *u8 = "write_png(combo" 172 173func xp_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 174func xp_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v } 175 176func xp_res() -> *i64 { 177 let r: *i64 = sys_mmap(XP_C_N * XP_R_N * 8) as *i64 178 var i: i64 = 0 179 while i < XP_C_N * XP_R_N { r[i] = 0; i = i + 1 } 180 return r 181} 182func xp_rs(res: *i64, c: i64, slot: i64, v: i64) -> i64 { res[c*XP_R_N + slot] = v; return 0 } 183func xp_rg(res: *i64, c: i64, slot: i64) -> i64 { return res[c*XP_R_N + slot] } 184 185func xp_name(c: i64) -> *u8 { 186 if c == XP_C_STORE_GLB { return "storefront-glb" as *u8 } 187 if c == XP_C_STORE_OBJ { return "storefront-obj" as *u8 } 188 if c == XP_C_GAME_NXA { return "game-nxa" as *u8 } 189 if c == XP_C_VR_FRAME { return "vr-vrframe" as *u8 } 190 if c == XP_C_CLINICAL { return "clinical-nxmsh2" as *u8 } 191 return "unknown" as *u8 192} 193func xp_file_of(c: i64) -> *u8 { 194 if c == XP_C_STORE_GLB { return XP_F_GLB } 195 if c == XP_C_STORE_OBJ { return XP_F_OBJ } 196 if c == XP_C_GAME_NXA { return XP_F_NXA } 197 if c == XP_C_CLINICAL { return XP_F_MSH } 198 return "" as *u8 199} 200// Integer units per metre in which THIS consumer holds a coordinate. 0 = the consumer holds no geometry. 201func xp_unit_of(c: i64) -> i64 { 202 if c == XP_C_STORE_GLB { return XP_GL_UNIT_PER_M } 203 if c == XP_C_STORE_OBJ { return XP_GL_UNIT_PER_M } 204 if c == XP_C_GAME_NXA { return XP_NXA_UNIT_PER_M } 205 if c == XP_C_CLINICAL { return NM_UNIT_PER_M } 206 return 0 207} 208// Does this consumer's artifact store coordinates as IEEE-754 float32? glb does (gl_i2f32 into the BIN 209// chunk) and NXMSH2 does (nm_put_tri). obj writes decimal integers and NXA writes i64 words: neither 210// touches a float, so neither pays the float32 term. 211func xp_uses_f32(c: i64) -> i64 { 212 if c == XP_C_STORE_GLB { return 1 } 213 if c == XP_C_CLINICAL { return 1 } 214 return 0 215} 216 217// ---- unit conversion, rounded to nearest, sign-symmetric --------------------------------------- 218func xp_q(v: i64, num: i64, den: i64) -> i64 { 219 if den == 0 { return 0 } 220 var neg: i64 = 0 221 var a: i64 = v 222 if a < 0 { neg = 1; a = 0 - a } 223 let r: i64 = (a * num * 2 + den) / (den * 2) 224 if neg == 1 { return 0 - r } 225 return r 226} 227 228// ---- the derived bound ------------------------------------------------------------------------- 229// Per-axis round-trip bound, in the ruler's own units. 230// EXACT CASE: when the destination has an integer multiple of the ruler's steps per metre, the forward 231// map is a multiplication and the inverse an exact division. Nothing is lost and the bound is ZERO -- 232// stated as arithmetic, not as a hopeful tolerance. 233// GENERAL CASE: forward costs at most half a destination step and the inverse at most half a ruler step, 234// so |v' - v| <= (NM/U)/2 + 1/2 ruler units, i.e. ceil((NM + U) / 2U). 235func xp_axis_bound(dst_per_m: i64) -> i64 { 236 if dst_per_m <= 0 { return 0 } 237 if dst_per_m % NM_UNIT_PER_M == 0 { return 0 } 238 return (NM_UNIT_PER_M + dst_per_m + dst_per_m*2 - 1) / (dst_per_m*2) 239} 240 241// The float32 term, COMPUTED from the asset's own coordinate magnitude rather than guessed. 242// gl_i2f32 truncates the mantissa: for an integer of magnitude m with e = floor(log2 m), every bit below 243// 2^(e-23) is dropped. While e <= 23 the integer fits the 24-bit significand exactly and the term is 244// ZERO; above it the term is one ulp at that magnitude, converted up into ruler units so the bound can 245// never come out optimistic. 246func xp_f32_axis_term(maxabs_dst: i64, dst_per_m: i64) -> i64 { 247 if maxabs_dst <= 0 { return 0 } 248 if dst_per_m <= 0 { return 0 } 249 var e: i64 = 0 250 var t: i64 = maxabs_dst 251 while t > 1 { t = t / 2; e = e + 1 } 252 if e <= VM_F32_MANT_BITS { return 0 } 253 var ulp: i64 = 1 254 var k: i64 = 0 255 while k < e - VM_F32_MANT_BITS { ulp = ulp * 2; k = k + 1 } 256 return (ulp * NM_UNIT_PER_M + dst_per_m - 1) / dst_per_m 257} 258 259// A vertex displaced by at most `axis` on each of three axes moves at most sqrt(3)*axis, and the 260// point-to-triangle deviation the ruler reports can never exceed that, because the displaced vertex is 261// itself a vertex OF the recovered surface. vm_isqrt floors, so +1 keeps this a ceiling. 262func xp_bound3d(axis_nm: i64) -> i64 { 263 if axis_nm <= 0 { return 0 } 264 return vm_isqrt(3 * axis_nm * axis_nm) + 1 265} 266 267// ---- mesh statistics --------------------------------------------------------------------------- 268// out6 = [minx miny minz maxx maxy maxz]. Returns the triangle count, 0 if empty. 269func xp_aabb(m: *u8, out6: *i64) -> i64 { 270 let nt: i64 = nm_ntris(m) 271 if nt <= 0 { return 0 } 272 let tb: i64 = nm_tri_base(m) 273 var a: i64 = 0 274 while a < 3 { out6[a] = nm_coord(m, tb, 0, 0, a); out6[3+a] = out6[a]; a = a + 1 } 275 var t: i64 = 0 276 while t < nt { 277 var v: i64 = 0 278 while v < 3 { 279 a = 0 280 while a < 3 { 281 let x: i64 = nm_coord(m, tb, t, v, a) 282 if x < out6[a] { out6[a] = x } 283 if x > out6[3+a] { out6[3+a] = x } 284 a = a + 1 285 } 286 v = v + 1 287 } 288 t = t + 1 289 } 290 return nt 291} 292 293func xp_extent(bb: *i64) -> i64 { 294 var e: i64 = bb[3] - bb[0] 295 if bb[4] - bb[1] > e { e = bb[4] - bb[1] } 296 if bb[5] - bb[2] > e { e = bb[5] - bb[2] } 297 return e 298} 299 300func xp_maxabs(m: *u8) -> i64 { 301 let bb: *i64 = sys_mmap(6*8) as *i64 302 if xp_aabb(m, bb) == 0 { return 0 } 303 var mx: i64 = 0 304 var a: i64 = 0 305 while a < 3 { 306 if xp_abs(bb[a]) > mx { mx = xp_abs(bb[a]) } 307 if xp_abs(bb[3+a]) > mx { mx = xp_abs(bb[3+a]) } 308 a = a + 1 309 } 310 return mx 311} 312 313// Worst per-axis difference between two meshes' AABB extents. A rescale -- including the classic unit 314// confusion of shipping millimetres where metres were meant -- shows up here even where a deviation 315// figure would not. Returns -1 if either mesh is empty. 316func xp_ext_delta(a: *u8, b: *u8) -> i64 { 317 let ba: *i64 = sys_mmap(6*8) as *i64 318 let bb: *i64 = sys_mmap(6*8) as *i64 319 if xp_aabb(a, ba) == 0 { return 0 - 1 } 320 if xp_aabb(b, bb) == 0 { return 0 - 1 } 321 var w: i64 = 0 322 var i: i64 = 0 323 while i < 3 { 324 let d: i64 = xp_abs((ba[3+i]-ba[i]) - (bb[3+i]-bb[i])) 325 if d > w { w = d } 326 i = i + 1 327 } 328 return w 329} 330 331// Divisor that keeps the signed-volume accumulator inside i64. |a . (b x c)| is bounded by 3*ext^3 once 332// every component is taken relative to the AABB minimum, and the accumulator must hold nt such terms. 333// Returns 0 for UNMEASURABLE (an extent past the ruler's own declared bound), never a wrong divisor. 334func xp_vol_div(m: *u8, bb: *i64) -> i64 { 335 let nt: i64 = nm_ntris(m) 336 if nt <= 0 { return 0 } 337 var ext: i64 = xp_extent(bb) 338 if ext < 1 { ext = 1 } 339 if ext > MD_EXTENT_MAX { return 0 } 340 let per: i64 = 3 * ext * ext * ext 341 let allow: i64 = XP_I64_SAFE / nt 342 if per <= allow { return 1 } 343 return per / allow + 1 344} 345 346// Six times the signed volume, coordinates taken relative to `org` and each term reduced by `div`. 347// The SIGN is the orientation witness and needs no tolerance: a mirror or a reversed winding flips it. 348func xp_vol6(m: *u8, org: *i64, div: i64) -> i64 { 349 if div <= 0 { return 0 } 350 let nt: i64 = nm_ntris(m) 351 let tb: i64 = nm_tri_base(m) 352 var acc: i64 = 0 353 var t: i64 = 0 354 while t < nt { 355 let ax: i64 = nm_coord(m, tb, t, 0, 0) - org[0] 356 let ay: i64 = nm_coord(m, tb, t, 0, 1) - org[1] 357 let az: i64 = nm_coord(m, tb, t, 0, 2) - org[2] 358 let bx: i64 = nm_coord(m, tb, t, 1, 0) - org[0] 359 let by: i64 = nm_coord(m, tb, t, 1, 1) - org[1] 360 let bz: i64 = nm_coord(m, tb, t, 1, 2) - org[2] 361 let cx: i64 = nm_coord(m, tb, t, 2, 0) - org[0] 362 let cy: i64 = nm_coord(m, tb, t, 2, 1) - org[1] 363 let cz: i64 = nm_coord(m, tb, t, 2, 2) - org[2] 364 let kx: i64 = by*cz - bz*cy 365 let ky: i64 = bz*cx - bx*cz 366 let kz: i64 = bx*cy - by*cx 367 acc = acc + (ax*kx + ay*ky + az*kz) / div 368 t = t + 1 369 } 370 return acc 371} 372 373// Volume tolerance in permil, derived from the AABB. A surface offset of d changes volume by about 374// area*d, and for a box area/volume is 2*(1/ex + 1/ey + 1/ez). Returns -1 for UNMEASURED when the AABB 375// is degenerate -- an abstention, never a silent zero. 376// DECLARED OPTIMISM: a convoluted mesh has more area per unit volume than its own box, so on such a mesh 377// this is a floor rather than a ceiling. It is a supporting axis; the sign test above is the strict one. 378func xp_vol_permil_bound(bb: *i64, bound3d: i64) -> i64 { 379 let ex: i64 = bb[3] - bb[0] 380 let ey: i64 = bb[4] - bb[1] 381 let ez: i64 = bb[5] - bb[2] 382 if ex <= 0 { return 0 - 1 } 383 if ey <= 0 { return 0 - 1 } 384 if ez <= 0 { return 0 - 1 } 385 let area2: i64 = ey*ez + ex*ez + ex*ey 386 let vol: i64 = ex*ey*ez 387 return XP_PERMIL * 2 * bound3d * area2 / vol + XP_VOL_PERMIL_FLOOR 388} 389 390// ---- welding: triangle soup -> the indexed mesh every real exporter takes -------------------- 391// vbuf must hold 3*nt*3 words and fbuf 3*nt words. Returns the welded vertex count. Vertices are 392// quantised into the destination unit FIRST, so two soup corners that the destination cannot tell apart 393// become one vertex -- which is exactly what an exporter does, and the reason the recovery must come back 394// out of the artifact rather than from a saved copy. 395func xp_weld(m: *u8, dst_per_m: i64, vbuf: *i64, fbuf: *i64) -> i64 { 396 let nt: i64 = nm_ntris(m) 397 if nt <= 0 { return 0 } 398 let tb: i64 = nm_tri_base(m) 399 let tabn: i64 = nt * 3 * XP_HASH_LOAD_INV + 1 400 let tab: *i64 = sys_mmap(tabn * 8) as *i64 401 var i: i64 = 0 402 while i < tabn { tab[i] = 0; i = i + 1 } 403 var nv: i64 = 0 404 var t: i64 = 0 405 while t < nt { 406 var v: i64 = 0 407 while v < 3 { 408 let qx: i64 = xp_q(nm_coord(m, tb, t, v, 0), dst_per_m, NM_UNIT_PER_M) 409 let qy: i64 = xp_q(nm_coord(m, tb, t, v, 1), dst_per_m, NM_UNIT_PER_M) 410 let qz: i64 = xp_q(nm_coord(m, tb, t, v, 2), dst_per_m, NM_UNIT_PER_M) 411 var h: i64 = (qx*XP_HASH_A + qy*XP_HASH_B + qz*XP_HASH_C) % tabn 412 if h < 0 { h = 0 - h } 413 var idx: i64 = 0 - 1 414 var scan: i64 = 1 415 while scan == 1 { 416 let s: i64 = tab[h] 417 if s == 0 { 418 vbuf[nv*3] = qx; vbuf[nv*3+1] = qy; vbuf[nv*3+2] = qz 419 tab[h] = nv + 1 420 idx = nv 421 nv = nv + 1 422 scan = 0 423 } else { 424 let cc: i64 = s - 1 425 var same: i64 = 0 426 if vbuf[cc*3] == qx { if vbuf[cc*3+1] == qy { if vbuf[cc*3+2] == qz { same = 1 } } } 427 if same == 1 { 428 idx = cc 429 scan = 0 430 } else { 431 h = h + 1 432 if h >= tabn { h = 0 } 433 } 434 } 435 } 436 fbuf[t*3+v] = idx 437 v = v + 1 438 } 439 t = t + 1 440 } 441 return nv 442} 443 444// Indexed mesh in `src_per_m` units -> an NXMSH2 triangle soup in the ruler's units. `out` must hold 445// nm_file_bytes(0, nf) bytes. Returns 0, or -1 on an index outside the vertex array. 446func xp_soup_from_indexed(vbuf: *i64, fbuf: *i64, nv: i64, nf: i64, src_per_m: i64, out: *u8) -> i64 { 447 if nf <= 0 { return 0 - 1 } 448 if nv <= 0 { return 0 - 1 } 449 nm_put_u32(out, NM_OFF_NLAYERS, 0) 450 nm_put_u32(out, NM_OFF_NTRIS, nf) 451 let tb: i64 = nm_tri_base(out) 452 let p: *i64 = sys_mmap(9*8) as *i64 453 var t: i64 = 0 454 while t < nf { 455 var v: i64 = 0 456 while v < 3 { 457 let vi: i64 = fbuf[t*3+v] 458 if vi < 0 { return 0 - 1 } 459 if vi >= nv { return 0 - 1 } 460 p[v*3] = xp_q(vbuf[vi*3], NM_UNIT_PER_M, src_per_m) 461 p[v*3+1] = xp_q(vbuf[vi*3+1], NM_UNIT_PER_M, src_per_m) 462 p[v*3+2] = xp_q(vbuf[vi*3+2], NM_UNIT_PER_M, src_per_m) 463 v = v + 1 464 } 465 nm_put_tri(out, tb, t, p) 466 t = t + 1 467 } 468 return 0 469} 470 471// ---- artifact writers -------------------------------------------------------------------------- 472func xp_path(dir: *u8, name: *u8, out: *u8) -> i64 { 473 var p: i64 = 0 474 var i: i64 = 0 475 while dir[i] != (0 as u8) { out[p] = dir[i]; p = p + 1; i = i + 1 } 476 out[p] = 47 as u8; p = p + 1 477 i = 0 478 while name[i] != (0 as u8) { out[p] = name[i]; p = p + 1; i = i + 1 } 479 out[p] = 0 as u8 480 return p 481} 482 483func xp_write_bytes(b: *u8, n: i64, path: *u8) -> i64 { 484 let fd: i64 = sys_openat_wr(path, XP_MODE_0644) 485 if fd < 0 { return 0 - 1 } 486 sys_write(fd, b, n) 487 sys_close(fd) 488 return n 489} 490 491// NXA v1 container carrying VERT + TRIS, assembled through nx_nxa.nx's own magic, tag and checksum 492// primitives so it cannot drift from what nxa_find will accept. 493func xp_nxa_write(vbuf: *i64, fbuf: *i64, nv: i64, nf: i64, path: *u8) -> i64 { 494 if nv <= 0 { return 0 - 1 } 495 if nf <= 0 { return 0 - 1 } 496 let vwl: i64 = 1 + nv*3 497 let twl: i64 = 1 + nf*3 498 let voff: i64 = XP_NXA_TOC_BYTE + XP_NXA_SECTIONS*XP_NXA_TOC_ROW_WORDS*8 499 let toff: i64 = voff + vwl*8 500 let total: i64 = toff + twl*8 501 let b: *u8 = sys_mmap(total + 16) 502 let h: *i64 = b as *i64 503 h[0] = nxa_magic() 504 h[1] = NXA_VER 505 h[2] = XP_NXA_SECTIONS 506 let tb: *i64 = ((b as i64) + XP_NXA_TOC_BYTE) as *i64 507 let vp: *i64 = ((b as i64) + voff) as *i64 508 vp[0] = nv 509 var i: i64 = 0 510 while i < nv*3 { vp[1+i] = vbuf[i]; i = i + 1 } 511 let tp: *i64 = ((b as i64) + toff) as *i64 512 tp[0] = nf 513 i = 0 514 while i < nf*3 { tp[1+i] = fbuf[i]; i = i + 1 } 515 tb[0] = nxa_tag4("VERT" as *u8); tb[1] = voff; tb[2] = vwl; tb[3] = nxa_check2(1, vp, vwl) 516 tb[4] = nxa_tag4("TRIS" as *u8); tb[5] = toff; tb[6] = twl; tb[7] = nxa_check2(1, tp, twl) 517 h[3] = nxa_check2(1, tb, XP_NXA_SECTIONS*XP_NXA_TOC_ROW_WORDS) 518 if xp_write_bytes(b, total, path) < 0 { return 0 - 1 } 519 return total 520} 521 522// ---- artifact readers: recover the geometry the consumer ACTUALLY received ---------------------- 523func xp_atoi(b: *u8, o: i64, n: i64) -> i64 { 524 var i: i64 = o 525 var neg: i64 = 0 526 if i < n { if b[i] == (45 as u8) { neg = 1; i = i + 1 } } 527 var v: i64 = 0 528 var go: i64 = 1 529 while go == 1 { 530 if i >= n { go = 0 } else { 531 let ch: i64 = b[i] as i64 532 if ch < 48 { go = 0 } else { 533 if ch > 57 { go = 0 } else { v = v*10 + (ch - 48); i = i + 1 } 534 } 535 } 536 } 537 if neg == 1 { return 0 - v } 538 return v 539} 540 541func xp_rec_glb(b: *u8, n: i64, out: *i64) -> *u8 { 542 out[0] = XP_WHY_ARTIFACT_MALFORMED 543 if n < XP_GLB_JSON_OFF + XP_GLB_CHUNKHDR { return 0 as *u8 } 544 if nm_u32(b, 0) != XP_GLB_MAGIC { return 0 as *u8 } 545 if nm_u32(b, 4) != XP_GLB_VER { return 0 as *u8 } 546 let jl: i64 = nm_u32(b, XP_GLB_JSONLEN_OFF) 547 if XP_GLB_JSON_OFF + jl + XP_GLB_CHUNKHDR > n { return 0 as *u8 } 548 let js: *u8 = ((b as i64) + XP_GLB_JSON_OFF) as *u8 549 if ge_find(js, jl, XP_GLB_SCALE_LIT, xp_slen(XP_GLB_SCALE_LIT), 0) < 0 { 550 out[0] = XP_WHY_SCALE_UNDECLARED 551 return 0 as *u8 552 } 553 let kl: i64 = xp_slen(XP_GLB_COUNT_KEY) 554 let p0: i64 = ge_find(js, jl, XP_GLB_COUNT_KEY, kl, 0) 555 if p0 < 0 { return 0 as *u8 } 556 let p1: i64 = ge_find(js, jl, XP_GLB_COUNT_KEY, kl, p0 + kl) 557 if p1 < 0 { return 0 as *u8 } 558 let p2: i64 = ge_find(js, jl, XP_GLB_COUNT_KEY, kl, p1 + kl) 559 if p2 < 0 { return 0 as *u8 } 560 let c0: i64 = xp_atoi(js, p0 + kl, jl) 561 let c1: i64 = xp_atoi(js, p1 + kl, jl) 562 let c2: i64 = xp_atoi(js, p2 + kl, jl) 563 if c0 <= 0 { return 0 as *u8 } 564 if c0 != c1 { return 0 as *u8 } 565 if c2 <= 0 { return 0 as *u8 } 566 if c2 % 3 != 0 { return 0 as *u8 } 567 let nv: i64 = c0 568 let nf: i64 = c2 / 3 569 let binoff: i64 = XP_GLB_JSON_OFF + jl + XP_GLB_CHUNKHDR 570 let binlen: i64 = nm_u32(b, XP_GLB_JSON_OFF + jl) 571 if binoff + binlen > n { return 0 as *u8 } 572 if nv*XP_GLB_IDX_START_MUL + nf*XP_GLB_VEC3_BYTES > binlen { return 0 as *u8 } 573 let bin: *u8 = ((b as i64) + binoff) as *u8 574 let vb: *i64 = sys_mmap(nv*3*8) as *i64 575 var i: i64 = 0 576 while i < nv*3 { vb[i] = vm_f32_to_int(nm_u32(bin, i*XP_GLB_U32_BYTES), 1); i = i + 1 } 577 let fb: *i64 = sys_mmap(nf*3*8) as *i64 578 i = 0 579 while i < nf*3 { fb[i] = nm_u32(bin, nv*XP_GLB_IDX_START_MUL + i*XP_GLB_U32_BYTES); i = i + 1 } 580 let om: *u8 = sys_mmap(nm_file_bytes(0, nf) + 16) 581 if xp_soup_from_indexed(vb, fb, nv, nf, XP_GL_UNIT_PER_M, om) != 0 { return 0 as *u8 } 582 out[0] = XP_WHY_OK 583 out[1] = nf 584 out[2] = nv 585 return om 586} 587 588func xp_line_end(b: *u8, ls: i64, n: i64) -> i64 { 589 var le: i64 = ls 590 var sc: i64 = 1 591 while sc == 1 { 592 if le >= n { sc = 0 } else { 593 if b[le] == (10 as u8) { sc = 0 } else { le = le + 1 } 594 } 595 } 596 return le 597} 598 599// Reads the next integer from [cur[0], end), skipping leading spaces. ok[0] = 1 if one was read. 600func xp_next_int(b: *u8, cur: *i64, end: i64, ok: *i64) -> i64 { 601 var i: i64 = cur[0] 602 var sk: i64 = 1 603 while sk == 1 { 604 if i >= end { sk = 0 } else { 605 if b[i] == (32 as u8) { i = i + 1 } else { sk = 0 } 606 } 607 } 608 var neg: i64 = 0 609 if i < end { if b[i] == (45 as u8) { neg = 1; i = i + 1 } } 610 var v: i64 = 0 611 var got: i64 = 0 612 var go: i64 = 1 613 while go == 1 { 614 if i >= end { go = 0 } else { 615 let ch: i64 = b[i] as i64 616 if ch < 48 { go = 0 } else { 617 if ch > 57 { go = 0 } else { v = v*10 + (ch - 48); i = i + 1; got = 1 } 618 } 619 } 620 } 621 cur[0] = i 622 ok[0] = got 623 if got == 0 { return 0 } 624 if neg == 1 { return 0 - v } 625 return v 626} 627 628// Advance past the rest of the current whitespace-delimited token (an OBJ face vertex carries a 629// "//normal" tail after its index). 630func xp_skip_tok(b: *u8, cur: *i64, end: i64) -> i64 { 631 var i: i64 = cur[0] 632 var sk: i64 = 1 633 while sk == 1 { 634 if i >= end { sk = 0 } else { 635 if b[i] == (32 as u8) { sk = 0 } else { i = i + 1 } 636 } 637 } 638 cur[0] = i 639 return 0 640} 641 642func xp_rec_obj(b: *u8, n: i64, out: *i64) -> *u8 { 643 out[0] = XP_WHY_ARTIFACT_MALFORMED 644 var nv: i64 = 0 645 var nf: i64 = 0 646 var i: i64 = 0 647 while i < n { 648 let ls: i64 = i 649 let le: i64 = xp_line_end(b, ls, n) 650 if le - ls >= 2 { 651 if b[ls] == (118 as u8) { if b[ls+1] == (32 as u8) { nv = nv + 1 } } 652 if b[ls] == (102 as u8) { if b[ls+1] == (32 as u8) { nf = nf + 1 } } 653 } 654 i = le + 1 655 } 656 if nv <= 0 { return 0 as *u8 } 657 if nf <= 0 { return 0 as *u8 } 658 let vb: *i64 = sys_mmap(nv*3*8) as *i64 659 let fb: *i64 = sys_mmap(nf*3*8) as *i64 660 let cur: *i64 = sys_mmap(16) as *i64 661 let ok: *i64 = sys_mmap(16) as *i64 662 var vi: i64 = 0 663 var fi: i64 = 0 664 i = 0 665 while i < n { 666 let ls: i64 = i 667 let le: i64 = xp_line_end(b, ls, n) 668 if le - ls >= 2 { 669 if b[ls] == (118 as u8) { if b[ls+1] == (32 as u8) { 670 cur[0] = ls + 2 671 var a: i64 = 0 672 while a < 3 { 673 let x: i64 = xp_next_int(b, cur, le, ok) 674 if ok[0] == 0 { return 0 as *u8 } 675 vb[vi*3+a] = x 676 a = a + 1 677 } 678 vi = vi + 1 679 } } 680 if b[ls] == (102 as u8) { if b[ls+1] == (32 as u8) { 681 cur[0] = ls + 2 682 var a: i64 = 0 683 while a < 3 { 684 let x: i64 = xp_next_int(b, cur, le, ok) 685 if ok[0] == 0 { return 0 as *u8 } 686 fb[fi*3+a] = x - 1 687 xp_skip_tok(b, cur, le) 688 a = a + 1 689 } 690 fi = fi + 1 691 } } 692 } 693 i = le + 1 694 } 695 if vi != nv { return 0 as *u8 } 696 if fi != nf { return 0 as *u8 } 697 let om: *u8 = sys_mmap(nm_file_bytes(0, nf) + 16) 698 if xp_soup_from_indexed(vb, fb, nv, nf, XP_GL_UNIT_PER_M, om) != 0 { return 0 as *u8 } 699 out[0] = XP_WHY_OK 700 out[1] = nf 701 out[2] = nv 702 return om 703} 704 705func xp_rec_nxa(b: *u8, n: i64, out: *i64) -> *u8 { 706 out[0] = XP_WHY_ARTIFACT_MALFORMED 707 let vwo: i64 = nxa_find(b, n, nxa_tag4("VERT" as *u8)) 708 if vwo < 0 { return 0 as *u8 } 709 let two: i64 = nxa_find(b, n, nxa_tag4("TRIS" as *u8)) 710 if two < 0 { return 0 as *u8 } 711 let h: *i64 = b as *i64 712 let nv: i64 = h[vwo] 713 let nf: i64 = h[two] 714 if nv <= 0 { return 0 as *u8 } 715 if nf <= 0 { return 0 as *u8 } 716 let vx: *i64 = ((b as i64) + vwo*8 + 8) as *i64 717 let tr: *i64 = ((b as i64) + two*8 + 8) as *i64 718 let om: *u8 = sys_mmap(nm_file_bytes(0, nf) + 16) 719 if xp_soup_from_indexed(vx, tr, nv, nf, XP_NXA_UNIT_PER_M, om) != 0 { return 0 as *u8 } 720 out[0] = XP_WHY_OK 721 out[1] = nf 722 out[2] = nv 723 return om 724} 725 726// The clinical lane reads NXMSH2 natively, so its recovery is the file itself -- but it is still READ 727// BACK OFF DISK, because a writer that truncated or mis-ordered bytes would show up here and nowhere else. 728func xp_rec_msh(b: *u8, n: i64, out: *i64) -> *u8 { 729 out[0] = XP_WHY_ARTIFACT_MALFORMED 730 if n < NM_HDR { return 0 as *u8 } 731 let nt: i64 = nm_ntris(b) 732 if nt <= 0 { return 0 as *u8 } 733 if nm_file_bytes(nm_nlayers(b), nt) > n { return 0 as *u8 } 734 out[0] = XP_WHY_OK 735 out[1] = nt 736 out[2] = nt * 3 737 return b 738} 739 740func xp_recover(c: i64, b: *u8, n: i64, out: *i64) -> *u8 { 741 out[0] = XP_WHY_ARTIFACT_MALFORMED 742 out[1] = 0 743 out[2] = 0 744 if c == XP_C_STORE_GLB { return xp_rec_glb(b, n, out) } 745 if c == XP_C_STORE_OBJ { return xp_rec_obj(b, n, out) } 746 if c == XP_C_GAME_NXA { return xp_rec_nxa(b, n, out) } 747 if c == XP_C_CLINICAL { return xp_rec_msh(b, n, out) } 748 out[0] = XP_WHY_NO_MESH_PARAM 749 return 0 as *u8 750} 751 752// ---- the VR abstention, re-measured on every run ------------------------------------------------ 753// out2[0]/out2[1] carry where each reason was found (-1 = no longer present). Returns 1 while BOTH 754// reasons still hold, 0 if either has changed, -1 if the source could not be read. 755func xp_vr_still_unreached(out2: *i64) -> i64 { 756 let ln: *i64 = sys_mmap(16) as *i64 757 let b: *u8 = sys_read_file(XP_VR_SRC, ln) 758 out2[0] = 0 - 1 759 out2[1] = 0 - 1 760 if (b as i64) == 0 { return 0 - 1 } 761 let n: i64 = ln[0] 762 out2[0] = ge_find(b, n, XP_VR_SIG, xp_slen(XP_VR_SIG), 0) 763 out2[1] = ge_find(b, n, XP_VR_RASTER, xp_slen(XP_VR_RASTER), 0) 764 if out2[0] < 0 { return 0 } 765 if out2[1] < 0 { return 0 } 766 return 1 767} 768 769// ---- ONE MESH IN, EVERY CONSUMER'S OWN ARTIFACT OUT --------------------------------------------- 770// Returns the number of consumers an artifact was produced for. A consumer we cannot reach end to end is 771// recorded UNREACHED with its reason; it is never faked and never scored. 772func xp_emit_all(m: *u8, dir: *u8, res: *i64) -> i64 { 773 let nt: i64 = nm_ntris(m) 774 var reached: i64 = 0 775 var c: i64 = 0 776 while c < XP_C_N { 777 xp_rs(res, c, XP_R_TRIS_SRC, nt) 778 c = c + 1 779 } 780 // VR: named UNREACHED before anything else, so no later step can quietly score it. 781 xp_rs(res, XP_C_VR_FRAME, XP_R_REACHED, 0) 782 xp_rs(res, XP_C_VR_FRAME, XP_R_WHY, XP_WHY_NO_MESH_PARAM) 783 if nt <= 0 { return 0 } 784 let path: *u8 = sys_mmap(XP_PATH_CAP) 785 786 // Storefront: ONE weld at the exporter's own fx1024 unit feeds BOTH writers, because glb and obj are 787 // two renderings of the same indexed mesh -- which is exactly how the storefront lane ships them. 788 let vb: *i64 = sys_mmap(nt*3*3*8) as *i64 789 let fb: *i64 = sys_mmap(nt*3*8) as *i64 790 let nvg: i64 = xp_weld(m, XP_GL_UNIT_PER_M, vb, fb) 791 xp_path(dir, XP_F_GLB, path) 792 let gb: i64 = write_glb(vb, fb, nvg, nt, path) 793 xp_rs(res, XP_C_STORE_GLB, XP_R_BYTES, gb) 794 xp_rs(res, XP_C_STORE_GLB, XP_R_VERTS, nvg) 795 if gb > 0 { xp_rs(res, XP_C_STORE_GLB, XP_R_REACHED, 1); reached = reached + 1 } 796 xp_path(dir, XP_F_OBJ, path) 797 let ob: i64 = write_obj(vb, fb, nvg, nt, path) 798 xp_rs(res, XP_C_STORE_OBJ, XP_R_BYTES, ob) 799 xp_rs(res, XP_C_STORE_OBJ, XP_R_VERTS, nvg) 800 if ob > 0 { xp_rs(res, XP_C_STORE_OBJ, XP_R_REACHED, 1); reached = reached + 1 } 801 802 // Game: the world lane loads .nxa donors, whose VERT unit is ten times finer than the ruler's. 803 let vb2: *i64 = sys_mmap(nt*3*3*8) as *i64 804 let fb2: *i64 = sys_mmap(nt*3*8) as *i64 805 let nva: i64 = xp_weld(m, XP_NXA_UNIT_PER_M, vb2, fb2) 806 xp_path(dir, XP_F_NXA, path) 807 let ab: i64 = xp_nxa_write(vb2, fb2, nva, nt, path) 808 xp_rs(res, XP_C_GAME_NXA, XP_R_BYTES, ab) 809 xp_rs(res, XP_C_GAME_NXA, XP_R_VERTS, nva) 810 if ab > 0 { xp_rs(res, XP_C_GAME_NXA, XP_R_REACHED, 1); reached = reached + 1 } 811 812 // Clinical: md_measure reads NXMSH2, so the artifact is the mesh file itself. 813 xp_path(dir, XP_F_MSH, path) 814 let mb: i64 = nm_file_bytes(nm_nlayers(m), nt) 815 let cb: i64 = xp_write_bytes(m, mb, path) 816 xp_rs(res, XP_C_CLINICAL, XP_R_BYTES, cb) 817 xp_rs(res, XP_C_CLINICAL, XP_R_VERTS, nt*3) 818 if cb > 0 { xp_rs(res, XP_C_CLINICAL, XP_R_REACHED, 1); reached = reached + 1 } 819 return reached 820} 821 822// ---- THE VERDICT -------------------------------------------------------------------------------- 823// For every consumer an artifact was produced for, recover the geometry OUT OF THE ARTIFACT and measure 824// it against the source with md_measure in both directions, plus orientation and scale. Returns the 825// number of REACHED consumers that FAILED; a consumer that abstains is neither a pass nor a fail. 826func xp_consumers(m: *u8, dir: *u8, res: *i64) -> i64 { 827 let path: *u8 = sys_mmap(XP_PATH_CAP) 828 let out: *i64 = sys_mmap(4*8) as *i64 829 let ln: *i64 = sys_mmap(16) as *i64 830 let sbb: *i64 = sys_mmap(6*8) as *i64 831 let nts: i64 = xp_aabb(m, sbb) 832 let sdiv: i64 = xp_vol_div(m, sbb) 833 let svol: i64 = xp_vol6(m, sbb, sdiv) 834 let maxabs: i64 = xp_maxabs(m) 835 var fails: i64 = 0 836 var c: i64 = 0 837 while c < XP_C_N { 838 if xp_rg(res, c, XP_R_REACHED) == 1 { 839 let u: i64 = xp_unit_of(c) 840 let aq: i64 = xp_axis_bound(u) 841 var af: i64 = 0 842 if xp_uses_f32(c) == 1 { af = xp_f32_axis_term(xp_q(maxabs, u, NM_UNIT_PER_M), u) } 843 let axis: i64 = aq + af 844 let bnd: i64 = xp_bound3d(axis) 845 let extb: i64 = axis * 2 846 xp_rs(res, c, XP_R_AXISQ, aq) 847 xp_rs(res, c, XP_R_AXISF, af) 848 xp_rs(res, c, XP_R_AXIS, axis) 849 xp_rs(res, c, XP_R_BOUND, bnd) 850 xp_rs(res, c, XP_R_EXT_BOUND, extb) 851 let vpb: i64 = xp_vol_permil_bound(sbb, bnd) 852 xp_rs(res, c, XP_R_VOL_PERMIL_BOUND, vpb) 853 xp_path(dir, xp_file_of(c), path) 854 let raw: *u8 = sys_read_file(path, ln) 855 if (raw as i64) == 0 { 856 xp_rs(res, c, XP_R_WHY, XP_WHY_ARTIFACT_MISSING) 857 xp_rs(res, c, XP_R_PASS, 0) 858 } else { 859 let rec: *u8 = xp_recover(c, raw, ln[0], out) 860 if (rec as i64) == 0 { 861 xp_rs(res, c, XP_R_WHY, out[0]) 862 xp_rs(res, c, XP_R_PASS, 0) 863 fails = fails + 1 864 } else { 865 let ntr: i64 = nm_ntris(rec) 866 xp_rs(res, c, XP_R_TRIS_REC, ntr) 867 let ra: *i64 = md_res() 868 let rb: *i64 = md_res() 869 let e1: i64 = md_measure(m, rec, ra) 870 let e2: i64 = md_measure(rec, m, rb) 871 xp_rs(res, c, XP_R_MEAN_AB, md_mean(ra)) 872 xp_rs(res, c, XP_R_P95_AB, ra[MD_R_P95]) 873 xp_rs(res, c, XP_R_MAX_AB, ra[MD_R_MAX]) 874 xp_rs(res, c, XP_R_MEAN_BA, md_mean(rb)) 875 xp_rs(res, c, XP_R_P95_BA, rb[MD_R_P95]) 876 xp_rs(res, c, XP_R_MAX_BA, rb[MD_R_MAX]) 877 let rvol: i64 = xp_vol6(rec, sbb, sdiv) 878 var vs: i64 = 0 879 if svol > 0 { if rvol > 0 { vs = 1 } } 880 if svol < 0 { if rvol < 0 { vs = 1 } } 881 if svol == 0 { if rvol == 0 { vs = 1 } } 882 xp_rs(res, c, XP_R_VOLSIGN_OK, vs) 883 var vp: i64 = 0 884 if svol != 0 { vp = xp_abs(rvol - svol) * XP_PERMIL / xp_abs(svol) } 885 xp_rs(res, c, XP_R_VOL_PERMIL, vp) 886 let ed: i64 = xp_ext_delta(m, rec) 887 xp_rs(res, c, XP_R_EXT_DELTA, ed) 888 var pass: i64 = 1 889 if e1 != 0 { pass = 0; xp_rs(res, c, XP_R_WHY, XP_WHY_RULER_REFUSED) } 890 if e2 != 0 { pass = 0; xp_rs(res, c, XP_R_WHY, XP_WHY_RULER_REFUSED) } 891 if ntr != nts { pass = 0; xp_rs(res, c, XP_R_WHY, XP_WHY_COUNT_MISMATCH) } 892 if ra[MD_R_MAX] > bnd { pass = 0 } 893 if rb[MD_R_MAX] > bnd { pass = 0 } 894 if vs != 1 { pass = 0 } 895 if ed < 0 { pass = 0 } 896 if ed > extb { pass = 0 } 897 if vpb >= 0 { if vp > vpb { pass = 0 } } 898 xp_rs(res, c, XP_R_PASS, pass) 899 if pass == 0 { fails = fails + 1 } 900 } 901 } 902 } 903 c = c + 1 904 } 905 return fails 906}