1use crate::kernels::geometry::{js_ceil, js_floor, js_hypot, js_round};
12use crate::kernels::pool::SyncPtr;
13use crate::kernels::region::Region;
14
15pub const PARTNERS: usize = 6;
16pub const VERDICT_MIN: u32 = 3;
17
18#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct Box {
21 pub x0: i32,
22 pub y0: i32,
23 pub w: i32,
24 pub h: i32,
25}
26
27impl Box {
28 pub fn of(rect: &crate::kernels::geometry::Rect, factor: i32) -> Box {
29 let f = factor as f64;
30 let x0 = js_floor(rect.x / f) - 1;
31 let y0 = js_floor(rect.y / f) - 1;
32 Box {
33 x0,
34 y0,
35 w: js_ceil((rect.x + rect.width) / f) - x0 + 1,
36 h: js_ceil((rect.y + rect.height) / f) - y0 + 1,
37 }
38 }
39 #[inline]
40 pub fn cells(&self) -> usize {
41 (self.w.max(0) as usize) * (self.h.max(0) as usize)
42 }
43}
44
45pub struct RegionSlot {
47 pub region: Region,
48 pub box_: Box,
49 pub dmin: f64,
50 pub interior: Vec<u8>,
52 pub inside: Vec<u8>,
55}
56
57pub struct Layer {
59 pub canvas: u32,
60 pub pose_x: f64,
61 pub pose_y: f64,
62 pub score: Vec<i8>,
63 pub comparisons: Vec<u8>,
64 pub pairs: u32,
65 pub box_gray: Vec<u8>,
66}
67
68pub struct Frame {
69 pub index: u32,
70 pub bytes: usize,
71 pub layers: Vec<Option<Layer>>,
73}
74
75pub struct Verdict {
77 pub slot: usize,
78 pub bits: Vec<u8>,
79 pub clean: Vec<u8>,
80}
81
82pub struct Finalized {
83 pub index: u32,
84 pub verdicts: Vec<Verdict>,
85 pub voted_layers: u32,
86 pub thin_layers: u32,
87}
88
89pub struct Ring {
90 pub factor: i32,
91 pub tau: i32,
92 pub radius: i32,
93 pub native_width: f64,
94 pub native_height: f64,
95 pub analysis_width: usize,
96 pub analysis_height: usize,
97 pub budget_bytes: usize,
98 pub slots: Vec<RegionSlot>,
99 frames: Vec<Frame>,
100 bytes: usize,
101 building: Vec<Option<Layer>>,
103}
104
105#[inline]
111fn threshold(comparisons: u32) -> i32 {
112 comparisons as i32 - 2 * ((comparisons as f64 * 0.75).ceil() as i32)
113}
114
115#[inline]
117fn window_agrees(gray: &[u8], start: usize, count: usize, value: u8, tau: u8) -> bool {
118 #[cfg(target_feature = "simd128")]
119 {
120 use core::arch::wasm32::*;
121 if count <= 16 && start + 16 <= gray.len() {
122 let a = unsafe { v128_load(gray.as_ptr().add(start) as *const v128) };
124 let v = u8x16_splat(value);
125 let diff = v128_or(u8x16_sub_sat(a, v), u8x16_sub_sat(v, a));
126 let within = u8x16_le(diff, u8x16_splat(tau));
127 let lanes = u8x16_lt(
128 u8x16(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15),
129 u8x16_splat(count as u8),
130 );
131 return v128_any_true(v128_and(within, lanes));
132 }
133 }
134 gray[start..start + count]
135 .iter()
136 .any(|&g| (value as i32 - g as i32).unsigned_abs() <= tau as u32)
137}
138
139impl Ring {
140 #[allow(clippy::too_many_arguments)]
141 pub fn new(
142 factor: i32,
143 noise: f64,
144 native_width: usize,
145 native_height: usize,
146 analysis_width: usize,
147 analysis_height: usize,
148 budget_bytes: usize,
149 regions: Vec<Region>,
150 ) -> Ring {
151 let factor = factor.max(1);
152 let phase = if factor == 1 { 0 } else { 26 };
154 let tau = phase.max(js_round(noise * 2.6));
155 let radius = factor;
156 let (nw, nh) = (native_width as f64, native_height as f64);
157 let slots = regions
158 .into_iter()
159 .map(|region| {
160 let box_ = Box::of(®ion.rect, factor);
161 let dmin = (0.25 * region.rect.width.min(region.rect.height)).max(64.0);
162 let (gw, gh) = (analysis_width as i32, analysis_height as i32);
163 let mut inside = vec![0u8; box_.cells()];
164 for ly in 0..box_.h {
165 for lx in 0..box_.w {
166 let (ax, ay) = (box_.x0 + lx, box_.y0 + ly);
167 let ok = ax >= 0
168 && ay >= 0
169 && ax < gw
170 && ay < gh
171 && region.contains((ax * factor) as f64, (ay * factor) as f64, nw, nh);
172 inside[(ly * box_.w + lx) as usize] = ok as u8;
173 }
174 }
175 let mut interior = vec![0u8; box_.cells()];
176 for ly in 0..box_.h {
177 for lx in 0..box_.w {
178 if lx < radius
179 || ly < radius
180 || lx >= box_.w - radius
181 || ly >= box_.h - radius
182 {
183 continue;
184 }
185 let mut ok = true;
186 'taps: for oy in -1..=1 {
187 for ox in -1..=1 {
188 let x = ((box_.x0 + lx + ox) * factor) as f64;
189 let y = ((box_.y0 + ly + oy) * factor) as f64;
190 if !region.contains(x, y, nw, nh) {
191 ok = false;
192 break 'taps;
193 }
194 }
195 }
196 interior[(ly * box_.w + lx) as usize] = ok as u8;
197 }
198 }
199 RegionSlot {
200 region,
201 box_,
202 dmin,
203 interior,
204 inside,
205 }
206 })
207 .collect::<Vec<_>>();
208 let building = (0..slots.len()).map(|_| None).collect();
209 Ring {
210 factor,
211 tau,
212 radius,
213 native_width: nw,
214 native_height: nh,
215 analysis_width,
216 analysis_height,
217 budget_bytes,
218 slots,
219 frames: Vec::new(),
220 bytes: 0,
221 building,
222 }
223 }
224
225 pub fn resident_frames(&self) -> usize {
226 self.frames.len()
227 }
228
229 fn box_gray(&self, slot: &RegionSlot, gray: &[u8]) -> Vec<u8> {
239 let b = slot.box_;
240 let (gw, gh) = (self.analysis_width as i32, self.analysis_height as i32);
241 let bw = b.w as usize;
242 let mut out = vec![0u8; b.cells()];
243 for ly in 0..b.h {
244 let ay = b.y0 + ly;
245 if ay < 0 || ay >= gh {
246 continue;
247 }
248 let row = ly as usize * bw;
249 let gray_row = (ay * gw) as usize;
250 if self.factor == 1 {
251 let lx0 = (-b.x0).clamp(0, b.w);
252 let lx1 = (gw - b.x0).clamp(0, b.w);
253 if lx1 > lx0 {
254 let ax0 = (b.x0 + lx0) as usize;
255 out[row + lx0 as usize..row + lx1 as usize].copy_from_slice(
256 &gray[gray_row + ax0..gray_row + ax0 + (lx1 - lx0) as usize],
257 );
258 }
259 continue;
260 }
261 for lx in 0..b.w {
262 let ax = b.x0 + lx;
263 if ax < 0 || ax >= gw {
264 continue;
265 }
266 let centre = gray[gray_row + ax as usize] as u32;
267 let mut sum = 0u32;
268 for oy in -1..=1 {
269 let tly = ly + oy;
270 let tap_row = ((ay + oy) * gw) as isize;
271 for ox in -1..=1 {
272 let tlx = lx + ox;
273 let inside = tlx >= 0
275 && tly >= 0
276 && tlx < b.w
277 && tly < b.h
278 && slot.inside[tly as usize * bw + tlx as usize] != 0;
279 sum += if inside {
280 gray[(tap_row + (ax + ox) as isize) as usize] as u32
281 } else {
282 centre
283 };
284 }
285 }
286 out[row + lx as usize] = ((2 * sum + 9) / 18) as u8;
288 }
289 }
290 out
291 }
292
293 fn partners(&self, slot: usize, canvas: u32, pose_x: f64, pose_y: f64) -> Vec<usize> {
301 let dmin = self.slots[slot].dmin;
302 let mut candidates: Vec<(usize, f64, u32, u32)> = Vec::new();
303 for (at, frame) in self.frames.iter().enumerate() {
304 let Some(layer) = &frame.layers[slot] else {
305 continue;
306 };
307 if layer.canvas != canvas {
308 continue;
309 }
310 let d = js_hypot(pose_x - layer.pose_x, pose_y - layer.pose_y);
311 if d >= dmin {
312 candidates.push((at, d, layer.pairs, frame.index));
313 }
314 }
315 if candidates.len() <= PARTNERS {
316 return candidates.into_iter().map(|c| c.0).collect();
317 }
318 candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
320 let n = candidates.len();
321 let mut seen: Vec<u32> = Vec::with_capacity(PARTNERS);
322 let mut out: Vec<usize> = Vec::with_capacity(PARTNERS);
323 let take = |c: &(usize, f64, u32, u32), seen: &mut Vec<u32>, out: &mut Vec<usize>| {
324 if !seen.contains(&c.3) {
325 seen.push(c.3);
326 out.push(c.0);
327 }
328 };
329 take(&candidates[0], &mut seen, &mut out);
330 take(&candidates[1], &mut seen, &mut out);
331 take(&candidates[n - 1], &mut seen, &mut out);
332 let bands = PARTNERS - out.len();
333 let (lo, hi) = (2usize, n - 1);
334 let mut band = 0usize;
335 while band < bands && hi > lo {
336 let from = lo + band * (hi - lo) / bands;
337 let to = lo + (band + 1) * (hi - lo) / bands;
338 let mut best: Option<&(usize, f64, u32, u32)> = None;
339 for c in &candidates[from..to] {
340 if seen.contains(&c.3) {
341 continue;
342 }
343 let better = match best {
344 None => true,
345 Some(b) => c.2 < b.2 || (c.2 == b.2 && c.1 > b.1),
346 };
347 if better {
348 best = Some(c);
349 }
350 }
351 if let Some(b) = best {
352 take(b, &mut seen, &mut out);
353 }
354 band += 1;
355 }
356 out
357 }
358
359 fn compare(&self, slot: usize, t: &mut Layer, s: &mut Layer) {
364 let b = self.slots[slot].box_;
365 let interior = &self.slots[slot].interior;
366 let f = self.factor as f64;
367 let dx = (t.pose_x - s.pose_x) / f;
368 let dy = (t.pose_y - s.pose_y) / f;
369 t.pairs += 1;
370 s.pairs += 1;
371 let radius = self.radius;
372 let tau = self.tau.clamp(0, 255) as u8;
375 let bw = b.w as usize;
376 let rows = b.h.max(0) as usize;
383 let mut verdicts = vec![0u8; b.cells()];
384 let chunks = crate::kernels::pool::chunks_for(b.cells() * 4, 8 * 1024);
385 let out = SyncPtr(verdicts.as_mut_ptr());
386 let (tg, sg) = (&t.box_gray, &s.box_gray);
387 crate::kernels::pool::par_for(chunks, |c| {
388 for ly in crate::kernels::pool::split(rows, chunks, c) as i32
389 ..crate::kernels::pool::split(rows, chunks, c + 1) as i32
390 {
391 let sy_row = js_round(ly as f64 + dy);
392 if sy_row < 0 || sy_row >= b.h {
393 continue;
394 }
395 let py_lo = (sy_row - radius).max(0);
396 let py_hi = (sy_row + radius).min(b.h - 1);
397 let row_i = ly as usize * bw;
398 let row_s = sy_row as usize * bw;
399 let verdict_row =
401 unsafe { std::slice::from_raw_parts_mut(out.get().add(row_i), bw) };
402 for lx in 0..b.w {
403 let i = row_i + lx as usize;
404 if interior[i] == 0 {
405 continue;
406 }
407 let sx = js_round(lx as f64 + dx);
408 if sx < 0 || sx >= b.w || interior[row_s + sx as usize] == 0 {
409 continue;
410 }
411 let value = tg[i];
412 let x_lo = (sx - radius).max(0) as usize;
413 let count = ((sx + radius).min(b.w - 1) as usize + 1) - x_lo;
414 let agree = tau == 255
416 || window_agrees(sg, sy_row as usize * bw + x_lo, count, value, tau)
417 || (py_lo..=py_hi).any(|py| {
418 py != sy_row
419 && window_agrees(sg, py as usize * bw + x_lo, count, value, tau)
420 });
421 verdict_row[lx as usize] = if agree { 1 } else { 2 };
422 }
423 }
424 });
425 for ly in 0..b.h {
426 let sy_row = js_round(ly as f64 + dy);
427 if sy_row < 0 || sy_row >= b.h {
428 continue;
429 }
430 let row_i = ly as usize * bw;
431 let row_s = sy_row as usize * bw;
432 for lx in 0..b.w {
433 let i = row_i + lx as usize;
434 if verdicts[i] == 0 {
435 continue;
436 }
437 let si = row_s + js_round(lx as f64 + dx) as usize;
438 let delta: i8 = if verdicts[i] == 1 { 1 } else { -1 };
439 t.comparisons[i] = t.comparisons[i].saturating_add(1);
440 t.score[i] = t.score[i].saturating_add(delta);
441 s.comparisons[si] = s.comparisons[si].saturating_add(1);
442 s.score[si] = s.score[si].saturating_add(delta);
443 }
444 }
445 }
446
447 pub fn observe(&mut self, slot: usize, canvas: u32, pose_x: f64, pose_y: f64, gray: &[u8]) {
450 let cells = self.slots[slot].box_.cells();
451 let mut layer = Layer {
452 canvas,
453 pose_x,
454 pose_y,
455 score: vec![0i8; cells],
456 comparisons: vec![0u8; cells],
457 pairs: 0,
458 box_gray: self.box_gray(&self.slots[slot], gray),
459 };
460 for at in self.partners(slot, canvas, pose_x, pose_y) {
461 let mut partner = self.frames[at].layers[slot]
463 .take()
464 .expect("partner layer present");
465 self.compare(slot, &mut layer, &mut partner);
466 self.frames[at].layers[slot] = Some(partner);
467 }
468 self.building[slot] = Some(layer);
469 }
470
471 pub fn push_frame(&mut self, index: u32) -> Vec<Finalized> {
474 let layers = std::mem::replace(
475 &mut self.building,
476 (0..self.slots.len()).map(|_| None).collect(),
477 );
478 if layers.iter().all(|l| l.is_none()) {
479 return Vec::new();
480 }
481 let bytes = layers
482 .iter()
483 .flatten()
484 .map(|l| l.box_gray.len() + l.score.len() + l.comparisons.len())
485 .sum();
486 self.frames.push(Frame {
487 index,
488 bytes,
489 layers,
490 });
491 self.bytes += bytes;
492 let mut out = Vec::new();
493 while self.bytes > self.budget_bytes && self.frames.len() > 1 {
494 let evicted = self.frames.remove(0);
495 self.bytes -= evicted.bytes;
496 out.push(self.finalize(evicted));
497 }
498 out
499 }
500
501 pub fn drain(&mut self) -> Vec<Finalized> {
503 let frames = std::mem::take(&mut self.frames);
504 self.bytes = 0;
505 frames.into_iter().map(|f| self.finalize(f)).collect()
506 }
507
508 fn finalize(&self, frame: Frame) -> Finalized {
509 let mut verdicts = Vec::new();
510 let (mut voted, mut thin) = (0u32, 0u32);
511 for (slot, layer) in frame.layers.iter().enumerate() {
512 let Some(layer) = layer else {
513 continue;
514 };
515 voted += 1;
516 if layer.pairs < VERDICT_MIN {
517 thin += 1;
518 }
519 let bytes = layer.score.len().div_ceil(8);
520 let mut bits = vec![0u8; bytes];
521 let mut clean = vec![0u8; bytes];
522 let mut any = false;
523 for (i, (&score, &comparisons)) in
524 layer.score.iter().zip(&layer.comparisons).enumerate()
525 {
526 let (score, comparisons) = (score as i32, comparisons as u32);
527 if comparisons >= 2 && score <= threshold(comparisons) {
528 bits[i >> 3] |= 1 << (i & 7);
529 any = true;
530 } else if comparisons >= VERDICT_MIN && score >= -threshold(comparisons) {
531 clean[i >> 3] |= 1 << (i & 7);
532 any = true;
533 }
534 }
535 if any {
536 verdicts.push(Verdict { slot, bits, clean });
537 }
538 }
539 Finalized {
540 index: frame.index,
541 verdicts,
542 voted_layers: voted,
543 thin_layers: thin,
544 }
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn ratio_threshold_matches_the_measured_rule() {
554 assert_eq!(threshold(2), -2);
555 assert_eq!(threshold(3), -3);
556 assert_eq!(threshold(4), -2);
557 assert_eq!(threshold(5), -3);
558 assert_eq!(threshold(6), -4);
559 }
560
561 #[test]
562 fn box_gray_rounding_matches_math_round() {
563 for sum in 0u32..=(9 * 255) {
564 let expected = ((sum as f64) / 9.0 + 0.5).floor() as u32;
565 assert_eq!((2 * sum + 9) / 18, expected, "sum {sum}");
566 }
567 }
568}