code wiki / _hdl_build / nx_visual_diff.nx

nx_visual_diff.nx source

↩ module page · 155 lines · 6097 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 let am: *i64 = sys_mmap(32) as *i64 70 let bm: *i64 = sys_mmap(32) as *i64 71 var matched: i64 = 0 72 var total: i64 = 0 73 // worst-cell tracker (insertion into VD_WORST_SLOTS slots, worst first) 74 var wi: i64 = 0 75 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 } 76 var cy: i64 = 0 77 while cy < gy { 78 var cx: i64 = 0 79 while cx < gx { 80 let x0: i64 = (cx * cw) / gx 81 let x1: i64 = ((cx + 1) * cw) / gx 82 let y0: i64 = (cy * ch) / gy 83 let y1: i64 = ((cy + 1) * ch) / gy 84 vd_cell_mean(a, ay, x0, x1, y0, y1, am) 85 vd_cell_mean(b, by, x0, x1, y0, y1, bm) 86 var dr: i64 = am[0] - bm[0] 87 if dr < 0 { dr = 0 - dr } 88 var dg: i64 = am[1] - bm[1] 89 if dg < 0 { dg = 0 - dg } 90 var db: i64 = am[2] - bm[2] 91 if db < 0 { db = 0 - db } 92 let delta: i64 = (dr + dg + db) / 3 93 total = total + 1 94 if delta <= thresh { matched = matched + 1 } 95 else { 96 // insert into worst list (descending by delta) 97 var s: i64 = 0 98 var placed: i64 = 0 99 while s < VD_WORST_SLOTS { 100 if placed == 0 { 101 if delta > out[2 + s*3 + 2] { 102 // shift down from the end 103 var t: i64 = VD_WORST_SLOTS - 1 104 while t > s { 105 out[2+t*3] = out[2+(t-1)*3] 106 out[2+t*3+1] = out[2+(t-1)*3+1] 107 out[2+t*3+2] = out[2+(t-1)*3+2] 108 t = t - 1 109 } 110 out[2+s*3] = cx 111 out[2+s*3+1] = cy 112 out[2+s*3+2] = delta 113 placed = 1 114 } 115 } 116 s = s + 1 117 } 118 } 119 cx = cx + 1 120 } 121 cy = cy + 1 122 } 123 out[0] = matched 124 out[1] = total 125 if total == 0 { return 0 } 126 return (matched * 1000) / total 127} 128 129// content extent: last row y (0-based, +1 = rendered height) where ANY sampled pixel differs from the 130// page background (sampled at (2,2)) by more than tol in any channel. Sampling stride 3 px. 131func vd_content_extent(im: *VdImg, tol: i64) -> i64 { 132 let bo: i64 = (2 * im.w + 2) * im.nc 133 let br: i64 = im.px[bo] as i64 134 let bg: i64 = im.px[bo+1] as i64 135 let bb: i64 = im.px[bo+2] as i64 136 var y: i64 = im.h - 1 137 while y >= 0 { 138 var x: i64 = 0 139 while x < im.w { 140 let o: i64 = (y * im.w + x) * im.nc 141 var dr: i64 = (im.px[o] as i64) - br 142 if dr < 0 { dr = 0 - dr } 143 var dg: i64 = (im.px[o+1] as i64) - bg 144 if dg < 0 { dg = 0 - dg } 145 var db: i64 = (im.px[o+2] as i64) - bb 146 if db < 0 { db = 0 - db } 147 if dr > tol { return y + 1 } 148 if dg > tol { return y + 1 } 149 if db > tol { return y + 1 } 150 x = x + 3 151 } 152 y = y - 1 153 } 154 return 0 155}