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}