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}