nx_skin_pbr_gate.nx source
↩ module page · 322 lines · 17529 B
1// nx_skin_pbr_gate.nx -- GATE for the PBR map set (charsim R4, debt 1786549493 residue b+d).
2// THE TOOTH THAT MATTERS IS T7: the four maps must differ from EACH OTHER. An implementation that
3// returned albedo four times would pass every other tooth here and produce four files, which is exactly
4// the "file count moved, nothing else did" failure the rung was written to avoid.
5// T1 proves F0 is DERIVED, not typed: it recomputes Fresnel from the declared index and compares.
6// T4 is the anti-vacuity tooth: a relief field that was constant would give perfect unit-length normals
7// and perfect determinism while encoding no surface at all.
8// The physics teeth need no fixture, so this gate does not duplicate the sibling texbake fixture.
9// license_tier: ORIGINAL expect_exit: 0
10import "nx_syscalls.nx"
11import "nx_nxa.nx"
12import "nx_nxa_texc_lib.nx"
13import "nx_nxa_texbake_lib.nx"
14import "nx_gate_verdict.nx"
15
16const SP_REF: *u8 = "sites/nishifamily/world/ref9d.nxa"
17const SP_TMP: *u8 = "/tmp/nxpbr_ref9d.nxa"
18const SP_RES: i64 = 128
19const SP_RES4K: i64 = 4096
20const SP_SEED: i64 = 7
21const SP_SEED2: i64 = 8
22const SP_HUE: i64 = 500
23const SP_F0_PUBLISHED: i64 = 28 // ((1.4-1)/(1.4+1))^2 = 0.0278 -> 28 per mille, the textbook skin F0
24const SP_F0_TOL: i64 = 2
25const SP_LEN_TOL: i64 = 12 // unit-length tolerance on the decoded normal (127 nominal)
26const SP_NOM: i64 = 127
27const SP_MID: i64 = 128
28const SP_SAMPLES: i64 = 64
29const SP_BADMAP: i64 = 99
30const SP_POLES: i64 = 5
31const SP_UNIT: i64 = 4096 // nx_trimesh represents 1.0 as 4096 in its normal fixed point
32const SP_DIAG: i64 = 2364 // 4096/sqrt(3), the unit diagonal in that same fixed point
33const SP_VEC_TOL: i64 = 140 // integer residue across two normalisations, ~3 percent of unit
34const SP_TILT: i64 = 1200 // a real tangent tilt, not a whisper
35
36// ---- T12..T15: THE CONVENTION TEETH (added 2026-08-22). Two independent readings of this file found
37// that T3/T10/T11 are ALL sign-blind to the green channel: T3 compares |dy|, T10 tilts by (0,0,UNIT)
38// so y is zero and -0 == 0, T11 only asserts "moved" and "unit length". A pipeline that flipped the
39// green channel -- i.e. wrote a DirectX (-Y) map where the shader reads OpenGL (+Y), or vice versa --
40// would pass all eleven and render every bump as a dent. These teeth pin the SIGN.
41// THE FACT THEY PIN, read from the one owner of the encoding (ntb_encode_normal, nx_nxa_texbake_lib):
42// ny = 0 - dzy ; out3[1] = NTB_N_MID + NTB_N_SCALE * ny / len with dzy from dhy = h(y+1)-h(y-1)
43// i.e. a relief RISING toward +y encodes G BELOW the midpoint. That is n = (-h_x, -h_y, 1) stored +Y-up:
44// the OpenGL convention. The consumer (nx_trimesh.nx:697) decodes ety = (G - TM_NMID)*4096/TM_NSCL with
45// NO negation, so baker and shader agree today; this gate makes that agreement a measured property
46// instead of a coincidence. Every number below is DERIVED from the lib's own constants.
47//
48// THE FIXTURE RESOLUTION IS MEASURED, NOT CHOSEN -- and the first draft of this tooth would have been
49// VACUOUS. The encoder's slope is PHYSICAL (NTB_RELIEF_UM of height over 2*texel_um of run), so at a
50// coarse resolution a texel is so wide that even the full relief codomain rounds to G == midpoint:
51// at res=8 a texel is 212,500um and a 60um relief is a 0.00014 slope -> G=128 exactly, flat == rise
52// == fall, and nothing can be pinned. The gate therefore WALKS resolutions up from the smallest legal
53// bake (TXM_RES_MIN) by doubling until a full-range slope moves G by >= SP_RISE_STEPS, and PRINTS the
54// resolution it landed on. That number is a property of the encoder worth knowing on its own: the
55// coarsest bake at which the normal map carries any sign at all.
56const SP_CONV_RES_MIN: i64 = 8 // TXM_RES_MIN: the smallest bake the TEXM writer accepts
57const SP_CONV_RES_MAX: i64 = 4096 // SP_RES4K: the floor T8 already bakes; the walk never exceeds it
58const SP_CONV_STEP: i64 = 2 // the walk doubles -- every legal texture size is a power of two
59const SP_RISE_STEPS: i64 = 4 // quantisation steps the fixture gradient must move G by before the
60 // SIGN is asserted: 1 step is rounding noise, 4 is unambiguous, and
61 // it is a multiple of the encoder's own unit (one G step), not a slope
62const SP_GRAD_FULL: i64 = 1023 // NTB_H_RANGE: the full relief codomain, the steepest gradient the
63 // encoder can ever see, so the sign is asserted where |G-mid| is LARGEST
64
65func sp_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
66func sp_bytes(res: i64) -> i64 { return res*res*3 + 64 }
67func sp_ck(m: *u8, n: i64) -> i64 {
68 var c: i64 = 0
69 var i: i64 = 0
70 while i < n { c = (c*31 + (m[i] as i64)) % 1000000007; i = i + 1 }
71 return c
72}
73
74func main(argc: i64, argv: *i64) -> i64 {
75 let ctr: *i64 = gv_ctr()
76 gv_head("nx_skin_pbr -- the PBR map set: derived specular, region gloss, physical-relief normals" as *u8)
77
78 // T1 -- F0 is COMPUTED from the published refractive index, not typed in
79 let f0: i64 = ntb_f0_skin()
80 gv_puts(" F0 from IOR 1.4 = " as *u8); gv_num(f0); gv_puts(" per mille\n" as *u8)
81 var t1: i64 = 0
82 if sp_abs(f0 - SP_F0_PUBLISHED) <= SP_F0_TOL { t1 = 1 }
83 gv_check("T1 specular F0 is DERIVED by Fresnel from the skin index, not declared" as *u8, t1, ctr)
84
85 // T2 -- gloss discriminates by region, in the stated direction (T-zone oilier than limbs)
86 let rf: i64 = ntb_rough_of_region(NTB_FACE)
87 let rl: i64 = ntb_rough_of_region(NTB_LIMBS)
88 var t2: i64 = 0
89 if rf < rl { t2 = 1 }
90 gv_puts(" roughness face=" as *u8); gv_num(rf)
91 gv_puts(" limbs=" as *u8); gv_num(rl); gv_puts("\n" as *u8)
92 gv_check("T2 gloss regions discriminate: face smoother than limbs" as *u8, t2, ctr)
93
94 // T3 -- every emitted normal is unit length, and points mostly along the surface normal
95 let n3: *i64 = sys_mmap(3*8) as *i64
96 var t3: i64 = 1
97 var k: i64 = 0
98 while k < SP_SAMPLES {
99 ntb_normal_at(k*7, k*13, SP_RES4K, SP_SEED, n3)
100 let dx: i64 = n3[0] - SP_MID
101 let dy: i64 = n3[1] - SP_MID
102 let dz: i64 = n3[2] - SP_MID
103 let ln: i64 = tm_isqrt(dx*dx + dy*dy + dz*dz)
104 if sp_abs(ln - SP_NOM) > SP_LEN_TOL { t3 = 0 }
105 if dz <= sp_abs(dx) { t3 = 0 }
106 if dz <= sp_abs(dy) { t3 = 0 }
107 k = k + 1
108 }
109 gv_check("T3 normals are unit length and dominated by the surface axis" as *u8, t3, ctr)
110
111 // T4 -- ANTI-VACUITY: the relief field is LIVE. A constant height gives flawless normals and
112 // flawless determinism while describing no surface whatsoever.
113 var t4: i64 = 0
114 var seenx: i64 = 0
115 k = 0
116 while k < SP_SAMPLES {
117 ntb_normal_at(k*7, k*13, SP_RES4K, SP_SEED, n3)
118 if n3[0] != SP_MID { seenx = seenx + 1 }
119 k = k + 1
120 }
121 if seenx > 0 { t4 = 1 }
122 gv_puts(" non-flat normal samples = " as *u8); gv_num(seenx)
123 gv_puts(" of " as *u8); gv_num(SP_SAMPLES); gv_puts("\n" as *u8)
124 gv_check("T4 anti-vacuity: the relief field actually perturbs the normal" as *u8, t4, ctr)
125
126 // T5 -- neg-control-determinism on the relief
127 let m3: *i64 = sys_mmap(3*8) as *i64
128 ntb_normal_at(11, 23, SP_RES4K, SP_SEED, n3)
129 ntb_normal_at(11, 23, SP_RES4K, SP_SEED, m3)
130 var t5: i64 = 1
131 if n3[0] != m3[0] { t5 = 0 }
132 if n3[1] != m3[1] { t5 = 0 }
133 if n3[2] != m3[2] { t5 = 0 }
134 gv_check("T5 neg-control-determinism: one seed yields one normal, always" as *u8, t5, ctr)
135
136 // T6 -- the seed is load-bearing on the relief
137 ntb_normal_at(11, 23, SP_RES4K, SP_SEED2, m3)
138 var t6: i64 = 0
139 if n3[0] != m3[0] { t6 = 1 }
140 if n3[1] != m3[1] { t6 = 1 }
141 gv_check("T6 the seed changes the relief (grain is live, not frozen)" as *u8, t6, ctr)
142
143 // ---- renderer wire: the tangent basis ----
144 // Five probe normals INCLUDING BOTH POLES. The -Z pole is the one a naive basis construction
145 // divides by zero on, so it is in the set on purpose.
146 let pv: *i64 = sys_mmap(SP_POLES*3*8) as *i64
147 pv[0] = 0; pv[1] = 0; pv[2] = SP_UNIT
148 pv[3] = 0; pv[4] = 0; pv[5] = 0 - SP_UNIT
149 pv[6] = SP_UNIT; pv[7] = 0; pv[8] = 0
150 pv[9] = 0; pv[10] = SP_UNIT; pv[11] = 0
151 pv[12] = SP_DIAG; pv[13] = SP_DIAG; pv[14] = SP_DIAG
152 let pn: *i64 = sys_mmap(3*8) as *i64
153
154 // T10 -- a straight-up tangent normal is an EXACT identity. This is the property that makes an
155 // unbound or flat normal map a no-op, and it must hold at the poles too.
156 var t10: i64 = 1
157 var pi: i64 = 0
158 while pi < SP_POLES {
159 let px: i64 = pv[pi*3]
160 let py: i64 = pv[pi*3+1]
161 let pz: i64 = pv[pi*3+2]
162 tm_perturb_normal(px, py, pz, 0, 0, SP_UNIT, pn)
163 if sp_abs(pn[0] - px) > SP_VEC_TOL { t10 = 0 }
164 if sp_abs(pn[1] - py) > SP_VEC_TOL { t10 = 0 }
165 if sp_abs(pn[2] - pz) > SP_VEC_TOL { t10 = 0 }
166 pi = pi + 1
167 }
168 gv_check("T10 a straight-up tangent normal is an exact identity, both poles included" as *u8, t10, ctr)
169
170 // T11 -- a TILTED tangent normal actually moves the shading normal, and the result stays unit length
171 var t11: i64 = 1
172 var moved: i64 = 0
173 pi = 0
174 while pi < SP_POLES {
175 let qx: i64 = pv[pi*3]
176 let qy: i64 = pv[pi*3+1]
177 let qz: i64 = pv[pi*3+2]
178 tm_perturb_normal(qx, qy, qz, SP_TILT, SP_TILT, SP_UNIT, pn)
179 let pl: i64 = tm_isqrt(pn[0]*pn[0] + pn[1]*pn[1] + pn[2]*pn[2])
180 if sp_abs(pl - SP_UNIT) > SP_VEC_TOL { t11 = 0 }
181 if sp_abs(pn[0] - qx) > SP_VEC_TOL { moved = moved + 1 }
182 if sp_abs(pn[1] - qy) > SP_VEC_TOL { moved = moved + 1 }
183 pi = pi + 1
184 }
185 if moved == 0 { t11 = 0 }
186 gv_puts(" tilted-tangent perturbations that moved = " as *u8); gv_num(moved); gv_puts("\n" as *u8)
187 gv_check("T11 a tilted tangent normal perturbs the normal and stays unit length" as *u8, t11, ctr)
188
189 // ---- asset-backed teeth ----
190 let rfd: i64 = sys_openat_rd(SP_REF)
191 var haveref: i64 = 0
192 if rfd >= 0 { haveref = 1; sys_close(rfd) }
193 if gv_need(SP_REF, haveref, ctr) == 1 {
194 let rcx: i64 = ntx_apply(SP_REF, SP_TMP)
195 let lp: *i64 = sys_mmap(16) as *i64
196 let b: *u8 = sys_read_file(SP_TMP, lp)
197 var t7: i64 = 0
198 var t8: i64 = 0
199 var t9: i64 = 0
200 if rcx == 0 {
201 if (b as i64) != 0 {
202 let flen: i64 = lp[0]
203 let nb: i64 = sp_bytes(SP_RES)
204 let ma: *u8 = sys_mmap(nb)
205 let ms: *u8 = sys_mmap(nb)
206 let mg: *u8 = sys_mmap(nb)
207 let mn: *u8 = sys_mmap(nb)
208 ntb_bake_pbr(b, flen, SP_RES, SP_SEED, SP_HUE, NTB_MAP_ALBEDO, ma)
209 ntb_bake_pbr(b, flen, SP_RES, SP_SEED, SP_HUE, NTB_MAP_SPEC, ms)
210 ntb_bake_pbr(b, flen, SP_RES, SP_SEED, SP_HUE, NTB_MAP_GLOSS, mg)
211 ntb_bake_pbr(b, flen, SP_RES, SP_SEED, SP_HUE, NTB_MAP_NORMAL, mn)
212 let ca: i64 = sp_ck(ma, nb - 64)
213 let cs: i64 = sp_ck(ms, nb - 64)
214 let cg: i64 = sp_ck(mg, nb - 64)
215 let cn: i64 = sp_ck(mn, nb - 64)
216 gv_puts(" map checksums albedo=" as *u8); gv_num(ca)
217 gv_puts(" spec=" as *u8); gv_num(cs)
218 gv_puts(" gloss=" as *u8); gv_num(cg)
219 gv_puts(" normal=" as *u8); gv_num(cn); gv_puts("\n" as *u8)
220 t7 = 1
221 if ca == cs { t7 = 0 }
222 if ca == cg { t7 = 0 }
223 if ca == cn { t7 = 0 }
224 if cs == cg { t7 = 0 }
225 if cs == cn { t7 = 0 }
226 if cg == cn { t7 = 0 }
227 // T8 -- THE 4096 FLOOR, actually baked rather than assumed from a knob
228 let n4: i64 = sp_bytes(SP_RES4K)
229 let big: *u8 = sys_mmap(n4)
230 let rc4: i64 = ntb_bake_pbr(b, flen, SP_RES4K, SP_SEED, SP_HUE, NTB_MAP_NORMAL, big)
231 let c4: i64 = sp_ck(big, n4 - 64)
232 gv_puts(" 4096 normal bake rc=" as *u8); gv_num(rc4)
233 gv_puts(" bytes=" as *u8); gv_num(n4 - 64)
234 gv_puts(" ck=" as *u8); gv_num(c4); gv_puts("\n" as *u8)
235 if rc4 == 0 { if c4 != 0 { t8 = 1 } }
236 sys_munmap(big, n4)
237 // T9 -- neg-control: an unknown map id REFUSES, it does not silently emit albedo
238 let rcb: i64 = ntb_bake_pbr(b, flen, SP_RES, SP_SEED, SP_HUE, SP_BADMAP, ma)
239 if rcb < 0 { t9 = 1 }
240 }
241 }
242 gv_check("T7 the four maps DIFFER from each other (not four copies of albedo)" as *u8, t7, ctr)
243 gv_check("T8 the 4096 floor is BAKED and non-empty, not assumed from a knob" as *u8, t8, ctr)
244 gv_check("T9 neg-control-badmap: an unknown map id refuses instead of emitting albedo" as *u8, t9, ctr)
245 }
246
247
248 // ---- convention teeth ----
249 // Walk resolutions until a full-range slope is REPRESENTABLE (leaves the midpoint by SP_RISE_STEPS).
250 let cn: *i64 = sys_mmap(3*8) as *i64
251 var cres: i64 = SP_CONV_RES_MIN
252 var g_rise: i64 = NTB_N_MID
253 var g_fall: i64 = NTB_N_MID
254 var g_flat: i64 = NTB_N_MID
255 var found_res: i64 = 0
256 while cres <= SP_CONV_RES_MAX {
257 ntb_encode_normal(0, SP_GRAD_FULL, cres, cn)
258 let gr: i64 = cn[1]
259 ntb_encode_normal(0, 0 - SP_GRAD_FULL, cres, cn)
260 let gf: i64 = cn[1]
261 ntb_encode_normal(0, 0, cres, cn)
262 let g0: i64 = cn[1]
263 if found_res == 0 {
264 if sp_abs(gr - NTB_N_MID) >= SP_RISE_STEPS { if sp_abs(gf - NTB_N_MID) >= SP_RISE_STEPS {
265 found_res = cres
266 g_rise = gr
267 g_fall = gf
268 g_flat = g0
269 } }
270 }
271 cres = cres * SP_CONV_STEP
272 }
273 gv_puts(" sign-visible resolution (first bake where a full-range slope moves G by >= SP_RISE_STEPS) = " as *u8)
274 gv_num(found_res); gv_puts("\n" as *u8)
275 gv_puts(" G channel at that res: flat=" as *u8); gv_num(g_flat)
276 gv_puts(" rising(+y)=" as *u8); gv_num(g_rise)
277 gv_puts(" falling(-y)=" as *u8); gv_num(g_fall)
278 gv_puts(" midpoint(NTB_N_MID)=" as *u8); gv_num(NTB_N_MID); gv_puts("\n" as *u8)
279
280 // T12 -- ANTI-VACUITY FIRST: some legal resolution must exist where the full-range slope leaves the
281 // midpoint by SP_RISE_STEPS in BOTH directions AND flat encodes AT the midpoint. A fixture that never
282 // reached that condition would make the sign teeth below pass-or-fail on rounding noise.
283 var t12: i64 = 0
284 if found_res > 0 { if g_flat == NTB_N_MID { t12 = 1 } }
285 gv_check("T12 anti-vacuity: a legal resolution exists where flat is AT midpoint and a full-range slope moves G by >= SP_RISE_STEPS both ways" as *u8, t12, ctr)
286
287 // T13 -- THE CONVENTION PIN. A relief rising toward +y must encode G BELOW NTB_N_MID. That is the
288 // +Y-up (OpenGL) tangent-space convention the consumer decodes without negation. This tooth is on
289 // the SIGN of (G - mid); |x| cannot pass it.
290 var t13: i64 = 0
291 if found_res > 0 { if g_rise < NTB_N_MID { t13 = 1 } }
292 gv_check("T13 CONVENTION: a relief rising toward +y encodes G BELOW midpoint (+Y-up / OpenGL, what nx_trimesh decodes)" as *u8, t13, ctr)
293
294 // T14 -- neg-control via gv_bite: the NEGATED slope must land on the OTHER side. bad = the falling
295 // slope reads ABOVE mid (it must, if the encoder is signed); good = the rising slope does NOT read
296 // above mid. A tooth that accepted both signs would be vacuous and gv_bite names it as such.
297 var fell_above: i64 = 0
298 if found_res > 0 { if g_fall > NTB_N_MID { fell_above = 1 } }
299 var rose_above: i64 = 0
300 if g_rise > NTB_N_MID { rose_above = 1 }
301 gv_bite("T14 neg-control-sign: the negated slope lands on the OTHER side of midpoint (a flipped or sign-blind encoder cannot pass both)" as *u8, fell_above, rose_above, ctr)
302
303 // T15 -- COLOUR SPACE: NAMED ABSENCE, printed on every run, neither PASS nor FAIL nor SKIP.
304 // Measured 2026-08-22 at corpus_complete=1 over 23,021 files: the texture pipeline
305 // (nx_nxa_texbake_lib, nx_nxa_texc_lib, nx_nxa_texm, nx_trimesh, nx_nxa) carries NO colour-space
306 // tag anywhere -- TEXM's header words are NMAPS/RES/MODEL/SEED/HUE/BLOBW and nothing says which maps
307 // are sRGB (albedo) and which are linear (spec/gloss/normal). nx_aces_native.nx already exports
308 // nx_aces_srgb_to_linear_q14 / nx_aces_linear_to_srgb_q14 and nothing here imports them.
309 // WHY THIS IS A PRINT AND NOT gv_need: a first draft used gv_need, and gv_need's contract is that
310 // ANY missing precondition turns the WHOLE verdict to SKIP -- which would have blocked the fourteen
311 // real proofs above on a gap in a SIBLING writer. A SKIP that hides fourteen greens is as wrong as a
312 // PASS that blesses a hole. So the absence is NAMED here in the output where every reader sees it,
313 // and the day nx_nxa_texm records a colour-space word this line becomes a gv_check.
314 gv_puts(" T15 colour-space tag: ABSENT from the pipeline (TEXM header words NMAPS/RES/MODEL/SEED/HUE/BLOBW carry none;
315" as *u8)
316 gv_puts(" nx_aces_native.nx has the sRGB<->linear pair and nothing here imports it) -- NAMED GAP, not asserted
317" as *u8)
318
319 let rc: i64 = gv_verdict("SKIN-PBR" as *u8, ctr, "derived specular, region gloss, physical-relief normals" as *u8)
320 sys_exit(rc)
321 return rc
322}