code wiki / _hdl_build / nx_frame_sanity.nx
nx_frame_sanity.nx source
↩ module page · 124 lines · 5776 B
1// nx_frame_sanity.nx -- EXPERIENTIAL frame verifier: mechanizes the eyeball for rendered RGB frames.
2// WHY: three Gx framing bugs (pure-sky win frame, camera buried in hillside, 50.4% blown-white) each
3// passed every numeric gate tooth and were caught ONLY by a human eyeball (rule 5). vqoe's probe835
4// taught the same lesson for video (median GREEN while 100% of frames never decoded). This lib gives
5// every gate a cheap composable tooth: "does this frame LOOK like a scene a player would accept?"
6// Metrics (all integer, deterministic, RGB24 caller fb like nx_swgpu/nx_gamehud):
7// [0] blown_permil -- pixels clipped to near-white (the tonemap-missing class)
8// [1] black_permil -- pixels near-black (the void/underexposed class)
9// [2] edge_permil -- luminance-step density (detail present at all; pure sky scores ~0)
10// [3] cedge_permil -- edge density in the CENTER THIRD (framing: the subject belongs mid-frame;
11// a sky-stare or floor-stare has its detail off-center or absent)
12// [4] flat_permil -- horizontal-run flatness (px == right neighbor); gradient skies are flat
13// fs_verdict applies DATA-DRIVEN thresholds (caller-supplied; keep them in a conf, rule 11).
14// HONEST RESIDUAL: cannot judge SEMANTICS (is that the right building?) -- it kills the gross framing
15// classes only; the realism-loop critic stays the fidelity judge. LIB, no main. license_tier: ORIGINAL
16import "nx_syscalls.nx"
17
18const FS_NMETRIC: i64 = 5
19const FS_M_BLOWN: i64 = 0
20const FS_M_BLACK: i64 = 1
21const FS_M_EDGE: i64 = 2
22const FS_M_CEDGE: i64 = 3
23const FS_M_FLAT: i64 = 4
24
25func fs_lum(fb: *u8, o: i64) -> i64 {
26 // integer luma: (2R + 5G + B) / 8 -- cheap, monotone, good enough for step detection
27 return ((fb[o] & 0xff) * 2 + (fb[o+1] & 0xff) * 5 + (fb[o+2] & 0xff)) / 8
28}
29
30// score fb (w*h*3) into out[FS_NMETRIC], all values permil of their denominator
31func fs_score(fb: *u8, w: i64, h: i64, out: *i64) -> i64 {
32 var blown: i64 = 0
33 var black: i64 = 0
34 var edges: i64 = 0
35 var cedges: i64 = 0
36 var flat: i64 = 0
37 var y: i64 = 0
38 let cx0: i64 = w / 3
39 let cx1: i64 = (w * 2) / 3
40 let cy0: i64 = h / 3
41 let cy1: i64 = (h * 2) / 3
42 var ctot: i64 = 0
43 var htot: i64 = 0
44 while y < h {
45 var x: i64 = 0
46 while x < w {
47 let o: i64 = (y * w + x) * 3
48 let r: i64 = fb[o] & 0xff
49 let g: i64 = fb[o+1] & 0xff
50 let b: i64 = fb[o+2] & 0xff
51 if r >= 250 { if g >= 250 { if b >= 250 { blown = blown + 1 } } }
52 if r <= 5 { if g <= 5 { if b <= 5 { black = black + 1 } } }
53 if x + 1 < w {
54 htot = htot + 1
55 let l0: i64 = fs_lum(fb, o)
56 let l1: i64 = fs_lum(fb, o + 3)
57 var d: i64 = l0 - l1
58 if d < 0 { d = 0 - d }
59 if d >= 24 {
60 edges = edges + 1
61 var incen: i64 = 0
62 if x >= cx0 { if x < cx1 { if y >= cy0 { if y < cy1 { incen = 1 } } } }
63 if incen == 1 { cedges = cedges + 1 }
64 }
65 if d <= 1 { flat = flat + 1 }
66 }
67 x = x + 1
68 }
69 y = y + 1
70 }
71 ctot = (cx1 - cx0) * (cy1 - cy0)
72 if ctot < 1 { ctot = 1 }
73 if htot < 1 { htot = 1 }
74 let n: i64 = w * h
75 out[FS_M_BLOWN] = blown * 1000 / n
76 out[FS_M_BLACK] = black * 1000 / n
77 out[FS_M_EDGE] = edges * 1000 / htot
78 out[FS_M_CEDGE] = cedges * 1000 / ctot
79 out[FS_M_FLAT] = flat * 1000 / htot
80 return 0
81}
82
83// i64-fb adapter (seq372): scores a PACKED-pixel frame (v = r | g<<8 | b<<16 -- the nx_swgpu AND
84// nx_game_raster format) by unpacking into a scratch RGB24 buffer and delegating to fs_score.
85// ONE scoring implementation, two fb formats -- add a verb, never widen (nx_cc arity law).
86func fs_score_i64(fb: *i64, w: i64, h: i64, out: *i64) -> i64 {
87 let tmp: *u8 = sys_mmap(w * h * 3)
88 var p: i64 = 0
89 let n: i64 = w * h
90 while p < n {
91 let v: i64 = fb[p]
92 tmp[p*3] = (v & 0xff) as u8
93 tmp[p*3+1] = ((v >> 8) & 0xff) as u8
94 tmp[p*3+2] = ((v >> 16) & 0xff) as u8
95 p = p + 1
96 }
97 return fs_score(tmp, w, h, out)
98}
99
100// thresholds th: [0]=blown_max [1]=black_max [2]=edge_min [3]=cedge_min [4]=flat_max (permil).
101// Returns 1 = frame experientially sane, 0 = a gross framing/exposure defect. Each threshold is a
102// SEPARATE refusal so a gate can print WHICH experiential axis failed.
103func fs_verdict(out: *i64, th: *i64) -> i64 {
104 if out[FS_M_BLOWN] > th[0] { return 0 }
105 if out[FS_M_BLACK] > th[1] { return 0 }
106 if out[FS_M_EDGE] < th[2] { return 0 }
107 if out[FS_M_CEDGE] < th[3] { return 0 }
108 if out[FS_M_FLAT] > th[4] { return 0 }
109 return 1
110}
111
112// default thresholds CALIBRATED AGAINST REAL FRAMES (2026-07-20, the calibration the first cut got
113// wrong: a synthetic textured scene measured edge 308 permil, but REAL smooth-shaded swgpu gameplay
114// frames (gx7 512x384, tonemapped, band terrain) measure edge 2-5 / cedge 5-8 / flat 954-982, while
115// the pure-sky bug class measures 0 / 0 / 1000 and a win-banner fill 0/0/999 -- the gap is clean but
116// two orders of magnitude lower than synthetic. Consumers score GAMEPLAY frames, not banner/UI fills.)
117func fs_default_th(th: *i64) -> i64 {
118 th[0] = 200 // >20% clipped white = the Gx-6 class (real bug measured 504)
119 th[1] = 700 // >70% pure black = void stare
120 th[2] = 1 // <0.1% edges = featureless (real frames 2-5; sky/banner = 0)
121 th[3] = 2 // center third empty = subject off-frame (real frames 5-8; sky-win class = 0)
122 th[4] = 992 // >99.2% flat = gradient/solid (real frames 954-982; sky/banner 999-1000)
123 return 0
124}