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}