code wiki / (root) / nx_bluenoise.nx

nx_bluenoise.nx source

↩ module page · 475 lines · 22213 B

1// nx_bluenoise.nx -- SOVEREIGN BLUE-NOISE MASK GENERATOR (Ulichney void-and-cluster, integer-exact). 2// 3// WHY THIS EXISTS: quantizing a photograph or a page of text to the ~16 grey levels an e-ink panel actually 4// has is what produces the "ink on paper" look. Doing that quantization with a THRESHOLD MASK decides whether 5// the result reads as paper texture or as a visible screen-door pattern. A blue-noise mask is the one whose 6// thresholded pattern has an isotropic power spectrum with no low-frequency energy -- the eye's own low-pass 7// then averages it to the right tone and never resolves structure. White noise clumps; ordered (Bayer) masks 8// show a lattice. Blue noise is the state of the art and has been since Ulichney 1993. 9// 10// INTEGER-EXACT BY CONSTRUCTION: no float anywhere. The Gaussian is a 33-entry fixed-point table indexed by 11// SQUARED distance, so the filter is exact and the same on every machine -- a mask that differed per host 12// would make every downstream render non-reproducible. 13// 14// COST, STATED: the classical algorithm is O(N^2) in cell count. 64x64 is ~16.7M ops (fast); the published 15// figure for 8192x8192 is about a MONTH, which is why the right shape is "generate a small mask ONCE, ship 16// it as data, tile it" rather than generating at render time. The 128 cap below is that decision made 17// explicit rather than discovered by a hang. 18// 19// IMPRECISION I CHOSE TO LIVE WITH (documented so the next reader does not trust it as exact): Ulichney's 20// phases 3 and 4 reverse which class is the "minority" for filtering purposes. Because the filter is linear 21// and toroidal, energy_of_zeros = constant - energy_of_ones, so "tightest cluster of 0s" and "largest void 22// of 1s" select the SAME cell. The two phases therefore collapse into one loop here, exactly rather than 23// approximately. 24// 25// nx_bluenoise gen <size> <out.raw> -- write size*size bytes, cell value = rank scaled to 0..255 26// nx_bluenoise stats <in.raw> <size> <fill> -- nearest-neighbour stats of the pattern thresholded at fill permil 27// nx_bluenoise selftest -- the gate 28// license_tier: ORIGINAL 29import "nx_gate_verdict.nx" 30import "nx_syscalls.nx" 31const BN_MAGIC_4096: i64 = 4096 32const BN_MAGIC_3280: i64 = 3280 33const BN_MAGIC_2626: i64 = 2626 34const BN_MAGIC_2103: i64 = 2103 35const BN_MAGIC_1684: i64 = 1684 36const BN_MAGIC_1348: i64 = 1348 37const BN_MAGIC_1080: i64 = 1080 38 39const BN_MAX_SIZE: i64 = 128 // O(N^2): 128x128 is ~268M ops. Above this, generate offline and ship the bytes. 40const BN_MIN_SIZE: i64 = 8 41const BN_DEFAULT_SIZE: i64 = 64 42const BN_KERN_R: i64 = 4 // kernel radius; sigma=1.5 is negligible past 4 (weight 3/4096) 43const BN_KERN_MAXD2: i64 = 32 // max squared distance inside the radius-4 box 44const BN_FIX: i64 = 4096 // fixed-point scale for the Gaussian 45const BN_INIT_PERMIL: i64 = 100 // initial prototype fill (10%) -- Ulichney's recommended starting density 46const BN_LCG_A: i64 = 1103515245 47const BN_LCG_C: i64 = 12345 48const BN_LCG_M: i64 = 2147483648 49const BN_SEED: i64 = 20260808 // FIXED seed: the mask must be byte-identical on every host, forever 50const BN_BYTE_MAX: i64 = 256 51 52func bn_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 53func bn_num(v: i64) -> i64 { 54 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 } 55 var m: i64 = v 56 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } 57 let t: *u8 = sys_mmap(32); var k: i64 = 0 58 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 59 let o: *u8 = sys_mmap(32); var w: i64 = 0; var q: i64 = k - 1 60 while q >= 0 { o[w] = t[q]; w = w + 1; q = q - 1 } 61 sys_write(1, o, w); return 0 62} 63 64// exp(-d2 / (2*sigma^2)) * 4096 for sigma=1.5, indexed by SQUARED distance 0..32. 65// Baked as a table because the filter must be integer-exact and identical on every host. 66func bn_gauss(d2: i64) -> i64 { 67 if d2 > BN_KERN_MAXD2 { return 0 } 68 let g: *i64 = sys_mmap(8 * (BN_KERN_MAXD2 + 1)) as *i64 69 g[0]=BN_MAGIC_4096 g[1]=BN_MAGIC_3280 g[2]=BN_MAGIC_2626 g[3]=BN_MAGIC_2103 g[4]=BN_MAGIC_1684 g[5]=BN_MAGIC_1348 g[6]=BN_MAGIC_1080 g[7]=865 70 g[8]=692 g[9]=554 g[10]=444 g[11]=355 g[12]=285 g[13]=228 g[14]=182 g[15]=146 71 g[16]=117 g[17]=94 g[18]=75 g[19]=60 g[20]=48 g[21]=38 g[22]=31 g[23]=25 72 g[24]=20 g[25]=16 g[26]=13 g[27]=10 g[28]=8 g[29]=7 g[30]=5 g[31]=4 73 g[32]=3 74 return g[d2] 75} 76 77// add (sign=+1) or remove (sign=-1) one point's Gaussian contribution, TOROIDALLY. 78// Toroidal because the mask is TILED: a mask whose edges were treated as boundaries would show seams at 79// every tile join, which is the one artefact a dither mask must never have. 80func bn_splat(energy: *i64, size: i64, cx: i64, cy: i64, sign: i64) -> i64 { 81 var dy: i64 = 0 - BN_KERN_R 82 while dy <= BN_KERN_R { 83 var dx: i64 = 0 - BN_KERN_R 84 while dx <= BN_KERN_R { 85 let d2: i64 = dx*dx + dy*dy 86 let w: i64 = bn_gauss(d2) 87 if w > 0 { 88 var yy: i64 = (cy + dy) % size 89 if yy < 0 { yy = yy + size } 90 var xx: i64 = (cx + dx) % size 91 if xx < 0 { xx = xx + size } 92 let idx: i64 = yy * size + xx 93 energy[idx] = energy[idx] + sign * w 94 } 95 dx = dx + 1 96 } 97 dy = dy + 1 98 } 99 return 0 100} 101 102// index of the cell with bin==want whose energy is the extreme (hi=1 -> max, hi=0 -> min). 103// Ties break on the LOWEST index so the whole generator stays deterministic. 104func bn_extreme(bin: *i64, energy: *i64, n: i64, want: i64, hi: i64) -> i64 { 105 var best: i64 = 0 - 1 106 var bestv: i64 = 0 107 var i: i64 = 0 108 while i < n { 109 if bin[i] == want { 110 let e: i64 = energy[i] 111 var take: i64 = 0 112 if best < 0 { take = 1 } 113 if best >= 0 { if hi == 1 { if e > bestv { take = 1 } } } 114 if best >= 0 { if hi == 0 { if e < bestv { take = 1 } } } 115 if take == 1 { best = i; bestv = e } 116 } 117 i = i + 1 118 } 119 return best 120} 121 122// Generate the mask. rank[] receives a PERMUTATION of 0..n-1: cell with rank r turns on at threshold r. 123func bn_generate(size: i64, rank: *i64) -> i64 { 124 let n: i64 = size * size 125 let bin: *i64 = sys_mmap(8 * n) as *i64 126 let energy: *i64 = sys_mmap(8 * n) as *i64 127 let proto: *i64 = sys_mmap(8 * n) as *i64 128 var i: i64 = 0 129 while i < n { bin[i] = 0; energy[i] = 0; rank[i] = 0 - 1; i = i + 1 } 130 131 // --- initial prototype: deterministic LCG, ~10% fill --- 132 let want: i64 = (n * BN_INIT_PERMIL) / 1000 133 var placed: i64 = 0 134 var seed: i64 = BN_SEED 135 while placed < want { 136 seed = (BN_LCG_A * seed + BN_LCG_C) % BN_LCG_M 137 if seed < 0 { seed = 0 - seed } 138 let c: i64 = seed % n 139 if bin[c] == 0 { bin[c] = 1; bn_splat(energy, size, c % size, c / size, 1); placed = placed + 1 } 140 } 141 142 // --- phase 1: break up clusters until the swap is a no-op (the pattern is then locally optimal) --- 143 var guard: i64 = 0 144 let guard_max: i64 = n * 4 145 while guard < guard_max { 146 let tight: i64 = bn_extreme(bin, energy, n, 1, 1) 147 bin[tight] = 0 148 bn_splat(energy, size, tight % size, tight / size, 0 - 1) 149 let void_c: i64 = bn_extreme(bin, energy, n, 0, 0) 150 bin[void_c] = 1 151 bn_splat(energy, size, void_c % size, void_c / size, 1) 152 if void_c == tight { break } 153 guard = guard + 1 154 } 155 i = 0 156 while i < n { proto[i] = bin[i]; i = i + 1 } 157 158 // --- phase 2: rank the prototype's points DOWNWARD, removing the tightest cluster each time --- 159 var r: i64 = placed - 1 160 while r >= 0 { 161 let tight: i64 = bn_extreme(bin, energy, n, 1, 1) 162 bin[tight] = 0 163 bn_splat(energy, size, tight % size, tight / size, 0 - 1) 164 rank[tight] = r 165 r = r - 1 166 } 167 168 // --- phase 3+4: restore the prototype, then fill every remaining cell into the largest void. 169 // (see header: the two published phases select the same cell here, so they are one loop.) --- 170 i = 0 171 while i < n { bin[i] = proto[i]; energy[i] = 0; i = i + 1 } 172 i = 0 173 while i < n { if bin[i] == 1 { bn_splat(energy, size, i % size, i / size, 1) } i = i + 1 } 174 r = placed 175 while r < n { 176 let void_c: i64 = bn_extreme(bin, energy, n, 0, 0) 177 bin[void_c] = 1 178 bn_splat(energy, size, void_c % size, void_c / size, 1) 179 rank[void_c] = r 180 r = r + 1 181 } 182 return n 183} 184 185// --- nearest-neighbour statistics of the pattern thresholded at `fill` permil --- 186// This is the RULER that tells blue noise from white noise. Mean NN distance alone is not enough (a clumped 187// pattern can share a mean with an even one), so the VARIANCE is what actually discriminates: blue noise 188// spaces points evenly, so every point's nearest neighbour sits at nearly the same distance. 189// Returns squared-distance stats scaled by 1000 to stay integer. out[0]=count out[1]=mean_d2_x1000 out[2]=var_d2_x1000 out[3]=min_d2 190func bn_nnstats(rank: *i64, size: i64, fill_permil: i64, out: *i64) -> i64 { 191 let n: i64 = size * size 192 let thresh: i64 = (n * fill_permil) / 1000 193 let px: *i64 = sys_mmap(8 * n) as *i64 194 let py: *i64 = sys_mmap(8 * n) as *i64 195 // MEASURE THE MINORITY CLASS. Nearest-neighbour spacing SATURATES once the selected set passes half the 196 // cells: at 50% fill you cannot have 2048 of 4096 cells mutually non-adjacent, so min_d2 pins to 1 and 197 // the variance to 0 -- for ANY pattern, including white noise. A metric that returns 0 for everything in 198 // that range is not evidence of perfect spacing, it is the ruler running out of range while still 199 // printing a number. Blue noise is symmetric (its HOLES are as evenly spaced as its dots), so above the 200 // midpoint we measure the complement and the ruler stays informative across the whole fill range. 201 var measure_complement: i64 = 0 202 if thresh * 2 > n { measure_complement = 1 } 203 var cnt: i64 = 0 204 var i: i64 = 0 205 while i < n { 206 var sel: i64 = 0 207 if rank[i] >= 0 { if rank[i] < thresh { sel = 1 } } 208 if measure_complement == 1 { sel = 1 - sel } 209 if sel == 1 { px[cnt] = i % size; py[cnt] = i / size; cnt = cnt + 1 } 210 i = i + 1 211 } 212 out[0] = cnt; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = measure_complement 213 if cnt < 2 { return 0 } 214 let nnd: *i64 = sys_mmap(8 * cnt) as *i64 215 var a: i64 = 0 216 var sum: i64 = 0 217 var mind: i64 = 0 - 1 218 while a < cnt { 219 var best: i64 = 0 - 1 220 var b: i64 = 0 221 while b < cnt { 222 if b != a { 223 // toroidal distance: the mask tiles, so wrap-around neighbours are real neighbours 224 var dx: i64 = px[a] - px[b]; if dx < 0 { dx = 0 - dx } 225 if dx > size / 2 { dx = size - dx } 226 var dy: i64 = py[a] - py[b]; if dy < 0 { dy = 0 - dy } 227 if dy > size / 2 { dy = size - dy } 228 let d2: i64 = dx*dx + dy*dy 229 if best < 0 { best = d2 } 230 if d2 < best { best = d2 } 231 } 232 b = b + 1 233 } 234 nnd[a] = best 235 sum = sum + best 236 if mind < 0 { mind = best } 237 if best < mind { mind = best } 238 a = a + 1 239 } 240 let mean_x1000: i64 = (sum * 1000) / cnt 241 var vsum: i64 = 0 242 a = 0 243 while a < cnt { 244 let d: i64 = nnd[a] * 1000 - mean_x1000 245 vsum = vsum + (d / 1000) * (d / 1000) 246 a = a + 1 247 } 248 out[1] = mean_x1000 249 out[2] = (vsum * 1000) / cnt 250 out[3] = mind 251 return 0 252} 253 254// a deliberately WHITE-noise ranking -- the negative control. If the ruler cannot tell this from the real 255// mask, the ruler is measuring nothing. 256func bn_white(size: i64, rank: *i64) -> i64 { 257 let n: i64 = size * size 258 var i: i64 = 0 259 while i < n { rank[i] = 0 - 1; i = i + 1 } 260 var seed: i64 = BN_SEED 261 var placed: i64 = 0 262 while placed < n { 263 seed = (BN_LCG_A * seed + BN_LCG_C) % BN_LCG_M 264 if seed < 0 { seed = 0 - seed } 265 let c: i64 = seed % n 266 if rank[c] < 0 { rank[c] = placed; placed = placed + 1 } 267 } 268 return n 269} 270 271func bn_atoi(s: *u8) -> i64 { 272 var v: i64 = 0; var i: i64 = 0 273 while s[i] != (0 as u8) { 274 let c: i64 = s[i] as i64 275 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } 276 i = i + 1 277 } 278 return v 279} 280 281func bn_write_raw(path: *u8, rank: *i64, n: i64) -> i64 { 282 let buf: *u8 = sys_mmap(n) 283 var i: i64 = 0 284 while i < n { buf[i] = ((rank[i] * BN_BYTE_MAX) / n) as u8; i = i + 1 } 285 let fd: i64 = sys_openat_wr(path, 0x1a4) 286 if fd < 0 { return 0 - 1 } 287 sys_write(fd, buf, n) 288 sys_close(fd) 289 return n 290} 291 292func bn_seq(a: *u8, b: *u8) -> i64 { 293 var i: i64 = 0 294 while 1 == 1 { 295 if (a[i] as i64) != (b[i] as i64) { return 0 } 296 if (a[i] as i64) == 0 { return 1 } 297 i = i + 1 298 } 299 return 0 300} 301 302func bn_selftest() -> i64 { 303 gv_head("nx_bluenoise -- does the mask actually carry blue-noise structure?" as *u8) 304 let ctr: *i64 = gv_ctr() 305 let size: i64 = 32 306 let n: i64 = size * size 307 let rank: *i64 = sys_mmap(8 * n) as *i64 308 bn_generate(size, rank) 309 310 // T1 STRUCTURAL: the mask must be a PERMUTATION of 0..n-1. A generator that leaves a hole or a duplicate 311 // produces a mask that is silently wrong at exactly one threshold, which no visual check would catch. 312 let seen: *i64 = sys_mmap(8 * n) as *i64 313 var i: i64 = 0 314 while i < n { seen[i] = 0; i = i + 1 } 315 var perm_ok: i64 = 1 316 i = 0 317 while i < n { 318 let r: i64 = rank[i] 319 if r < 0 { perm_ok = 0 } 320 if r >= n { perm_ok = 0 } 321 if r >= 0 { if r < n { seen[r] = seen[r] + 1 } } 322 i = i + 1 323 } 324 i = 0 325 while i < n { if seen[i] != 1 { perm_ok = 0 } i = i + 1 } 326 gv_check("every rank 0..n-1 appears exactly once (mask is a permutation)" as *u8, perm_ok, ctr) 327 328 // T2 DETERMINISM: a mask that differed run to run would make every render irreproducible. 329 let rank2: *i64 = sys_mmap(8 * n) as *i64 330 bn_generate(size, rank2) 331 var same: i64 = 1 332 i = 0 333 while i < n { if rank[i] != rank2[i] { same = 0 } i = i + 1 } 334 gv_check("regenerating with the fixed seed reproduces the mask exactly" as *u8, same, ctr) 335 336 // measure both patterns at 10% fill 337 let bs: *i64 = sys_mmap(64) as *i64 338 let ws: *i64 = sys_mmap(64) as *i64 339 bn_nnstats(rank, size, BN_INIT_PERMIL, bs) 340 let wrank: *i64 = sys_mmap(8 * n) as *i64 341 bn_white(size, wrank) 342 bn_nnstats(wrank, size, BN_INIT_PERMIL, ws) 343 344 // PRINT THE VALUES, not just pass/fail -- a gate that reports a boolean cannot say why. 345 bn_puts(" measured @100permil fill: blue n=" as *u8); bn_num(bs[0]) 346 bn_puts(" mean_d2x1000=" as *u8); bn_num(bs[1]) 347 bn_puts(" var=" as *u8); bn_num(bs[2]) 348 bn_puts(" min_d2=" as *u8); bn_num(bs[3]) 349 bn_puts("\n white n=" as *u8); bn_num(ws[0]) 350 bn_puts(" mean_d2x1000=" as *u8); bn_num(ws[1]) 351 bn_puts(" var=" as *u8); bn_num(ws[2]) 352 bn_puts(" min_d2=" as *u8); bn_num(ws[3]) 353 bn_puts("\n" as *u8) 354 355 // T3: same fill, so any difference is STRUCTURE and not density. Binding the assertion to the count 356 // stops this passing on two patterns that merely had different numbers of points. 357 var samecnt: i64 = 0 358 if bs[0] == ws[0] { if bs[0] > 0 { samecnt = 1 } } 359 gv_check("both patterns carry the SAME point count (difference is structure, not density)" as *u8, samecnt, ctr) 360 361 // T4 THE DISCRIMINATOR: blue noise spaces points evenly, so no two points land nearly on top of each 362 // other. White noise always produces some near-coincident pairs. 363 var minbetter: i64 = 0 364 if bs[3] > ws[3] { minbetter = 1 } 365 gv_check("blue-noise minimum separation EXCEEDS white noise (no clumping)" as *u8, minbetter, ctr) 366 367 // T5 ANTI-VACUITY: mean separation alone cannot tell an even pattern from a clumped one of the same 368 // density -- a wrong implementation can match the mean and still clump. Only the VARIANCE of the 369 // nearest-neighbour distance refutes it, so this is the tooth the trivial version fails. 370 var varbetter: i64 = 0 371 if bs[2] < ws[2] { varbetter = 1 } 372 gv_check("blue-noise NN-distance VARIANCE is lower (evenly spaced, not merely as dense)" as *u8, varbetter, ctr) 373 374 // T6 neg-control-white-noise: the ruler must REFUSE to call white noise blue. If this ever passes, every 375 // verdict above is meaningless. 376 var negctl: i64 = 0 377 if ws[2] >= bs[2] { negctl = 1 } 378 gv_check("neg-control-white-noise: the ruler does NOT certify white noise as blue" as *u8, negctl, ctr) 379 380 return gv_verdict("BLUENOISE" as *u8, ctr, "void-and-cluster mask carries measured blue-noise structure" as *u8) 381} 382 383func main(argc: i64, argv: *i64) -> i64 { 384 if argc < 2 { 385 bn_puts("usage: nx_bluenoise gen <size> <out.raw> | stats <in.raw> <size> <fill_permil> | selftest\n" as *u8) 386 sys_exit(2); return 2 387 } 388 let verb: *u8 = argv[1] as *u8 389 if bn_seq(verb, "selftest" as *u8) == 1 { let rc: i64 = bn_selftest(); sys_exit(rc); return rc } 390 if bn_seq(verb, "gen" as *u8) == 1 { 391 if argc < 4 { bn_puts("usage: nx_bluenoise gen <size> <out.raw>\n" as *u8); sys_exit(2); return 2 } 392 var size: i64 = bn_atoi(argv[2] as *u8) 393 if size < BN_MIN_SIZE { size = BN_DEFAULT_SIZE } 394 if size > BN_MAX_SIZE { 395 // REFUSE rather than silently clamp: a caller who asked for 512 and got 128 would ship a mask 396 // that tiles 16 times more often than they think. 397 bn_puts("REFUSED: size " as *u8); bn_num(size) 398 bn_puts(" exceeds BN_MAX_SIZE " as *u8); bn_num(BN_MAX_SIZE) 399 bn_puts(" -- void-and-cluster is O(N^2) in cells; generate large masks offline and ship the bytes.\n" as *u8) 400 sys_exit(3); return 3 401 } 402 let n: i64 = size * size 403 let rank: *i64 = sys_mmap(8 * n) as *i64 404 bn_generate(size, rank) 405 let w: i64 = bn_write_raw(argv[3] as *u8, rank, n) 406 if w < 0 { bn_puts("BLUENOISE WRITE FAILED\n" as *u8); sys_exit(4); return 4 } 407 bn_puts("BLUENOISE OK size=" as *u8); bn_num(size) 408 bn_puts(" cells=" as *u8); bn_num(n) 409 bn_puts(" bytes=" as *u8); bn_num(w) 410 bn_puts(" path=" as *u8); bn_puts(argv[3] as *u8); bn_puts("\n" as *u8) 411 sys_exit(0); return 0 412 } 413 if bn_seq(verb, "stats" as *u8) == 1 { 414 if argc < 5 { bn_puts("usage: nx_bluenoise stats <in.raw> <size> <fill_permil>\n" as *u8); sys_exit(2); return 2 } 415 let size: i64 = bn_atoi(argv[3] as *u8) 416 let fill: i64 = bn_atoi(argv[4] as *u8) 417 let n: i64 = size * size 418 let szp: *i64 = sys_mmap(16) as *i64 419 let raw: *u8 = sys_read_file(argv[2] as *u8, szp) 420 if (raw as i64) == 0 { bn_puts("STATS FAILED: cannot read " as *u8); bn_puts(argv[2] as *u8); bn_puts("\n" as *u8); sys_exit(4); return 4 } 421 if szp[0] < n { bn_puts("STATS FAILED: file holds " as *u8); bn_num(szp[0]); bn_puts(" bytes, size says " as *u8); bn_num(n); bn_puts("\n" as *u8); sys_exit(4); return 4 } 422 let rank: *i64 = sys_mmap(8 * n) as *i64 423 var i: i64 = 0 424 while i < n { rank[i] = ((raw[i] as i64) * n) / BN_BYTE_MAX; i = i + 1 } 425 let st: *i64 = sys_mmap(64) as *i64 426 bn_nnstats(rank, size, fill, st) 427 bn_puts("BLUENOISE STATS points=" as *u8); bn_num(st[0]) 428 bn_puts(" mean_d2x1000=" as *u8); bn_num(st[1]) 429 bn_puts(" var=" as *u8); bn_num(st[2]) 430 bn_puts(" min_d2=" as *u8); bn_num(st[3]) 431 // say WHICH class was measured -- a number whose subject is implicit is a number nobody can check 432 if st[4] == 1 { bn_puts(" measured=holes (past the midpoint; dots saturate)" as *u8) } 433 if st[4] == 0 { bn_puts(" measured=dots" as *u8) } 434 bn_puts("\n" as *u8) 435 sys_exit(0); return 0 436 } 437 if bn_seq(verb, "sweep" as *u8) == 1 { 438 // Prove the ruler DISCRIMINATES at EVERY fill, not merely at the one the gate happens to use. 439 // A metric validated at a single density is a metric whose range nobody measured -- and this one 440 // provably saturates past the midpoint, which is exactly why the minority-class switch exists. 441 if argc < 3 { bn_puts("usage: nx_bluenoise sweep <size>\n" as *u8); sys_exit(2); return 2 } 442 var size: i64 = bn_atoi(argv[2] as *u8) 443 if size < BN_MIN_SIZE { size = BN_DEFAULT_SIZE } 444 if size > BN_MAX_SIZE { bn_puts("REFUSED: size over cap\n" as *u8); sys_exit(3); return 3 } 445 let n: i64 = size * size 446 let br: *i64 = sys_mmap(8 * n) as *i64 447 let wr: *i64 = sys_mmap(8 * n) as *i64 448 bn_generate(size, br) 449 bn_white(size, wr) 450 let a: *i64 = sys_mmap(64) as *i64 451 let b: *i64 = sys_mmap(64) as *i64 452 let fills: *i64 = sys_mmap(8 * 8) as *i64 453 fills[0]=50 fills[1]=100 fills[2]=200 fills[3]=300 fills[4]=500 fills[5]=700 fills[6]=900 454 bn_puts("BLUENOISE SWEEP size=" as *u8); bn_num(size); bn_puts("\n" as *u8) 455 var k: i64 = 0 456 var wins: i64 = 0 457 while k < 7 { 458 bn_nnstats(br, size, fills[k], a) 459 bn_nnstats(wr, size, fills[k], b) 460 bn_puts(" fill=" as *u8); bn_num(fills[k]) 461 bn_puts(" blue(var=" as *u8); bn_num(a[2]); bn_puts(",min=" as *u8); bn_num(a[3]) 462 bn_puts(") white(var=" as *u8); bn_num(b[2]); bn_puts(",min=" as *u8); bn_num(b[3]) 463 bn_puts(")" as *u8) 464 if a[2] < b[2] { bn_puts(" BLUE-WINS" as *u8); wins = wins + 1 } 465 if a[2] >= b[2] { bn_puts(" NO-SEPARATION" as *u8) } 466 if a[4] == 1 { bn_puts(" [measured holes]" as *u8) } 467 bn_puts("\n" as *u8) 468 k = k + 1 469 } 470 bn_puts(" ruler separated blue from white at " as *u8); bn_num(wins); bn_puts(" of 7 fills\n" as *u8) 471 sys_exit(0); return 0 472 } 473 bn_puts("usage: nx_bluenoise gen <size> <out.raw> | stats <in.raw> <size> <fill_permil> | sweep <size> | selftest\n" as *u8) 474 sys_exit(2); return 2 475}