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}