code wiki / _hdl_build / nx_visual_diff.nx

nx_visual_diff.nx source

↩ module page · 167 lines · 6736 B

1// nx_visual_diff.nx -- sovereign VISUAL PARITY instrument (lib): compares two rendered screenshots 2// (e.g. the Nishi browser vs a Chrome/Edge/Firefox oracle of the SAME page) and turns "looks the same" 3// into NUMBERS: (1) grid-cell parity -- split the shared region into a GxG grid, compare each cell's mean 4// color, report the matched-cell permille + the worst cells with coordinates; (2) content extent -- the 5// last y row that differs from the page background, i.e. the page's rendered height (layout parity: if 6// Chrome fits a page in 1500px and we take 4500px, the ratio IS the layout bug). Grid means tolerate 7// font-rasterizer differences while catching structural drift (missing blocks, wrong colors, stretched 8// rows). This is the S21 "same output as Chrome" benchmark's measuring stick. license_tier: ORIGINAL 9import "nx_syscalls.nx" 10import "nx_png_decoder.nx" 11 12const VD_IMG_BYTES: i64 = 32 // px,w,h,nc 13const VD_WORST_SLOTS: i64 = 5 // top-N worst cells reported (cx,cy,delta triples) 14 15struct VdImg { 16 px: *u8, 17 w: i64, 18 h: i64, 19 nc: i64, 20} 21 22// load a PNG into a VdImg; returns 0 as *VdImg on any failure 23func vd_load(path: *u8) -> *VdImg { 24 let lb: *i64 = sys_mmap(16) as *i64 25 let buf: *u8 = sys_read_file(path, lb) 26 if (buf as i64) == 0 { return 0 as *VdImg } 27 let res: *NxPngResult = nx_png_decode(buf, lb[0]) 28 if (res as i64) == 0 { return 0 as *VdImg } 29 if res.error_code != 0 { return 0 as *VdImg } 30 let hdr: *NxPngHeader = res.header 31 let im: *VdImg = sys_mmap(VD_IMG_BYTES as nx_size) as *VdImg 32 im.px = res.pixels 33 im.w = hdr.width 34 im.h = hdr.height 35 im.nc = res.n_channels 36 return im 37} 38 39// mean r+g+b over a cell [x0,x1)x[y0,y1) of img rows offset by yoff; writes r,g,b into out[0..2] 40func vd_cell_mean(im: *VdImg, yoff: i64, x0: i64, x1: i64, y0: i64, y1: i64, out: *i64) -> i64 { 41 var sr: i64 = 0 42 var sg: i64 = 0 43 var sb: i64 = 0 44 var n: i64 = 0 45 var y: i64 = y0 46 while y < y1 { 47 var x: i64 = x0 48 while x < x1 { 49 let o: i64 = ((y + yoff) * im.w + x) * im.nc 50 sr = sr + (im.px[o] as i64) 51 sg = sg + (im.px[o+1] as i64) 52 sb = sb + (im.px[o+2] as i64) 53 n = n + 1 54 x = x + 1 55 } 56 y = y + 1 57 } 58 if n == 0 { out[0]=0; out[1]=0; out[2]=0; return 0 } 59 out[0] = sr / n 60 out[1] = sg / n 61 out[2] = sb / n 62 return 0 63} 64 65// grid parity: compare region [0,cw)x[0,ch) of A (rows shifted ay) vs B (rows shifted by), grid gx x gy. 66// A cell matches when the mean-channel delta <= thresh. Returns matched-permille. 67// out: [0]=matched cells, [1]=total cells, then VD_WORST_SLOTS x (cx, cy, delta) worst-first. 68func vd_grid_parity(a: *VdImg, ay: i64, b: *VdImg, by: i64, cw: i64, ch: i64, gx: i64, gy: i64, thresh: i64, out: *i64) -> i64 { 69 // REFUSE degenerate/out-of-range windows (2026-07-29): a NEGATIVE height here iterated zero 70 // rows per cell, every mean came back (0,0,0) on BOTH sides, and the instrument reported a 71 // vacuous 100.0% -- the drift harness then banked eight perfect frames that were never 72 // compared. An instrument must fail loud on a window it cannot actually measure. 73 if cw <= 0 { return 0 - 1 } 74 if ch <= 0 { return 0 - 1 } 75 if ay < 0 { return 0 - 1 } 76 if by < 0 { return 0 - 1 } 77 if ay + ch > a.h { return 0 - 1 } 78 if by + ch > b.h { return 0 - 1 } 79 if cw > a.w { return 0 - 1 } 80 if cw > b.w { return 0 - 1 } 81 let am: *i64 = sys_mmap(32) as *i64 82 let bm: *i64 = sys_mmap(32) as *i64 83 var matched: i64 = 0 84 var total: i64 = 0 85 // worst-cell tracker (insertion into VD_WORST_SLOTS slots, worst first) 86 var wi: i64 = 0 87 while wi < VD_WORST_SLOTS { out[2 + wi*3] = 0 - 1; out[2 + wi*3 + 1] = 0 - 1; out[2 + wi*3 + 2] = 0 - 1; wi = wi + 1 } 88 var cy: i64 = 0 89 while cy < gy { 90 var cx: i64 = 0 91 while cx < gx { 92 let x0: i64 = (cx * cw) / gx 93 let x1: i64 = ((cx + 1) * cw) / gx 94 let y0: i64 = (cy * ch) / gy 95 let y1: i64 = ((cy + 1) * ch) / gy 96 vd_cell_mean(a, ay, x0, x1, y0, y1, am) 97 vd_cell_mean(b, by, x0, x1, y0, y1, bm) 98 var dr: i64 = am[0] - bm[0] 99 if dr < 0 { dr = 0 - dr } 100 var dg: i64 = am[1] - bm[1] 101 if dg < 0 { dg = 0 - dg } 102 var db: i64 = am[2] - bm[2] 103 if db < 0 { db = 0 - db } 104 let delta: i64 = (dr + dg + db) / 3 105 total = total + 1 106 if delta <= thresh { matched = matched + 1 } 107 else { 108 // insert into worst list (descending by delta) 109 var s: i64 = 0 110 var placed: i64 = 0 111 while s < VD_WORST_SLOTS { 112 if placed == 0 { 113 if delta > out[2 + s*3 + 2] { 114 // shift down from the end 115 var t: i64 = VD_WORST_SLOTS - 1 116 while t > s { 117 out[2+t*3] = out[2+(t-1)*3] 118 out[2+t*3+1] = out[2+(t-1)*3+1] 119 out[2+t*3+2] = out[2+(t-1)*3+2] 120 t = t - 1 121 } 122 out[2+s*3] = cx 123 out[2+s*3+1] = cy 124 out[2+s*3+2] = delta 125 placed = 1 126 } 127 } 128 s = s + 1 129 } 130 } 131 cx = cx + 1 132 } 133 cy = cy + 1 134 } 135 out[0] = matched 136 out[1] = total 137 if total == 0 { return 0 } 138 return (matched * 1000) / total 139} 140 141// content extent: last row y (0-based, +1 = rendered height) where ANY sampled pixel differs from the 142// page background (sampled at (2,2)) by more than tol in any channel. Sampling stride 3 px. 143func vd_content_extent(im: *VdImg, tol: i64) -> i64 { 144 let bo: i64 = (2 * im.w + 2) * im.nc 145 let br: i64 = im.px[bo] as i64 146 let bg: i64 = im.px[bo+1] as i64 147 let bb: i64 = im.px[bo+2] as i64 148 var y: i64 = im.h - 1 149 while y >= 0 { 150 var x: i64 = 0 151 while x < im.w { 152 let o: i64 = (y * im.w + x) * im.nc 153 var dr: i64 = (im.px[o] as i64) - br 154 if dr < 0 { dr = 0 - dr } 155 var dg: i64 = (im.px[o+1] as i64) - bg 156 if dg < 0 { dg = 0 - dg } 157 var db: i64 = (im.px[o+2] as i64) - bb 158 if db < 0 { db = 0 - db } 159 if dr > tol { return y + 1 } 160 if dg > tol { return y + 1 } 161 if db > tol { return y + 1 } 162 x = x + 3 163 } 164 y = y - 1 165 } 166 return 0 167}