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}