use std::sync::Arc;
use std::time::Duration;
use visual_cortex_capture::{Frame, FrameView, Rate};
use crate::debug::{DebugSink, DebugStage};
use crate::detector::DetectorError;
use crate::preprocessor::Preprocessor;
pub(crate) const SIG_LEN: usize = 17;
const VOTE: usize = 2;
const CORROBORATION_FRAC: f32 = 0.35;
const CENSUS_FRAC: f32 = 0.60;
const DRIFT_FRAC: f32 = 0.50;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Signature(pub [u8; SIG_LEN]);
impl Default for Signature {
fn default() -> Self {
Signature([0; SIG_LEN])
}
}
pub(crate) fn is_moving(cur: &Signature, last: &Signature, tol: u8) -> bool {
cur.0
.iter()
.zip(&last.0)
.filter(|(a, b)| a.abs_diff(**b) > tol)
.count()
>= VOTE
}
pub(crate) fn is_novel_vs(cur: &Signature, reference: &[u8; SIG_LEN], nov: u8, con: u8) -> bool {
let sub_votes = cur.0[..16]
.iter()
.zip(&reference[..16])
.filter(|(a, b)| a.abs_diff(**b) > nov)
.count();
sub_votes >= VOTE || cur.0[16].abs_diff(reference[16]) > con
}
fn narrow(base: &[u16; SIG_LEN]) -> [u8; SIG_LEN] {
let mut out = [0u8; SIG_LEN];
for (o, b) in out.iter_mut().zip(base) {
*o = (b >> 8) as u8;
}
out
}
fn widen(sig: &Signature) -> [u16; SIG_LEN] {
let mut out = [0u16; SIG_LEN];
for (o, s) in out.iter_mut().zip(&sig.0) {
*o = (*s as u16) << 8;
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Phase {
Idle,
Kept,
Holdover,
}
#[derive(Debug, Clone)]
pub(crate) struct Block {
pub last: Signature,
pub base: [u16; SIG_LEN],
pub ovl: Signature,
pub stable_ticks: u16,
pub unstable_run: u16,
pub holdover: u16,
pub phase: Phase,
pub initialized: bool,
pub base_frozen: bool,
pub scene_flag: bool,
pub run_corroborated: bool,
pub reseeded: bool,
}
impl Default for Block {
fn default() -> Self {
Self {
last: Signature::default(),
base: [0; SIG_LEN],
ovl: Signature::default(),
stable_ticks: 0,
unstable_run: 0,
holdover: 0,
phase: Phase::Idle,
initialized: false,
base_frozen: false,
scene_flag: false,
run_corroborated: false,
reseeded: false,
}
}
}
pub(crate) struct MaskParams {
pub stable_ticks: u16,
pub baseline_ticks: u32,
pub signature_tolerance: u8,
pub novelty_threshold: u8,
pub contrast_threshold: u8,
pub holdover_ticks: u16,
pub long_run: u16,
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct Globals {
pub scene_tick: bool,
pub corroborated_tick: bool,
pub census_hit: bool,
pub drift_guard: bool,
}
fn reseed(b: &mut Block, sig: &Signature) {
b.base = widen(sig);
b.base_frozen = false;
b.scene_flag = false;
b.run_corroborated = false;
b.unstable_run = 0;
b.phase = Phase::Idle;
b.reseeded = true;
}
fn ema_update(b: &mut Block, sig: &Signature, horizon: u32) {
let t = i64::from(horizon.max(1));
for (base, s) in b.base.iter_mut().zip(&sig.0) {
let target = i64::from(*s) << 8;
let delta = target - i64::from(*base);
let mut step = delta / t;
if step == 0 && delta != 0 {
step = delta.signum(); }
*base = (i64::from(*base) + step) as u16;
}
}
pub(crate) fn step_block(
b: &mut Block,
sig: &Signature,
mv: bool,
census_eligible: bool,
g: &Globals,
p: &MaskParams,
) -> bool {
b.reseeded = false;
if !b.initialized {
b.last = *sig;
b.base = widen(sig);
b.ovl = *sig;
b.initialized = true;
return false; }
b.last = *sig;
if mv {
b.stable_ticks = 0;
b.unstable_run = b.unstable_run.saturating_add(1);
if g.corroborated_tick {
b.run_corroborated = true;
}
if g.scene_tick {
b.scene_flag = true;
}
} else {
b.stable_ticks = b.stable_ticks.saturating_add(1);
}
if census_eligible && g.census_hit {
b.scene_flag = true;
}
let base_u8 = narrow(&b.base);
let stable = b.stable_ticks >= p.stable_ticks;
let settled_now = !mv && b.stable_ticks == p.stable_ticks;
let novel = is_novel_vs(sig, &base_u8, p.novelty_threshold, p.contrast_threshold);
match b.phase {
Phase::Kept => {
if mv {
b.phase = Phase::Holdover;
b.holdover = p.holdover_ticks;
} else {
b.ovl = *sig;
b.unstable_run = 0;
b.run_corroborated = false;
}
true }
Phase::Holdover => {
if !mv && !is_novel_vs(sig, &b.ovl.0, p.novelty_threshold, p.contrast_threshold) {
b.phase = Phase::Kept;
b.unstable_run = 0;
b.run_corroborated = false;
return true;
}
if settled_now {
if novel {
b.phase = Phase::Kept;
b.ovl = *sig;
b.unstable_run = 0;
b.run_corroborated = false;
return true;
}
b.phase = Phase::Idle;
b.base_frozen = false;
b.scene_flag = false;
b.unstable_run = 0;
b.run_corroborated = false;
return false;
}
b.holdover = b.holdover.saturating_sub(1);
if b.holdover == 0 {
b.phase = Phase::Idle;
return false;
}
true }
Phase::Idle => {
if settled_now {
let long_run = b.unstable_run >= p.long_run && b.run_corroborated;
let scene = b.scene_flag;
b.unstable_run = 0;
b.run_corroborated = false;
if long_run || scene {
reseed(b, sig);
return false;
}
b.scene_flag = false;
if novel {
b.phase = Phase::Kept;
b.base_frozen = true;
b.ovl = *sig;
return true;
}
} else if stable && novel {
if g.drift_guard {
reseed(b, sig);
return false;
}
b.phase = Phase::Kept;
b.base_frozen = true;
b.ovl = *sig;
b.unstable_run = 0;
b.run_corroborated = false;
return true;
}
if b.base_frozen && stable && !novel {
b.base_frozen = false; }
if !b.base_frozen && !mv && !novel {
ema_update(b, sig, p.baseline_ticks);
}
false
}
}
}
pub(crate) fn close_and_dilate(keep: &[bool], cols: usize, rows: usize, dilate: u32) -> Vec<bool> {
let d1 = dilate_grid(keep, cols, rows, 1);
let closed = erode_grid(&d1, cols, rows, 1);
if dilate == 0 {
closed
} else {
dilate_grid(&closed, cols, rows, dilate as usize)
}
}
pub(crate) fn dilate_grid(grid: &[bool], cols: usize, rows: usize, rings: usize) -> Vec<bool> {
let mut out = grid.to_vec();
for _ in 0..rings {
let src = out.clone();
for r in 0..rows {
for c in 0..cols {
if src[r * cols + c] {
continue;
}
let neighbors_on = (r > 0 && src[(r - 1) * cols + c])
|| (r + 1 < rows && src[(r + 1) * cols + c])
|| (c > 0 && src[r * cols + c - 1])
|| (c + 1 < cols && src[r * cols + c + 1]);
if neighbors_on {
out[r * cols + c] = true;
}
}
}
}
out
}
fn erode_grid(grid: &[bool], cols: usize, rows: usize, rings: usize) -> Vec<bool> {
let mut out = grid.to_vec();
for _ in 0..rings {
let src = out.clone();
for r in 0..rows {
for c in 0..cols {
if !src[r * cols + c] {
continue;
}
let all_on = (r == 0 || src[(r - 1) * cols + c])
&& (r + 1 >= rows || src[(r + 1) * cols + c])
&& (c == 0 || src[r * cols + c - 1])
&& (c + 1 >= cols || src[r * cols + c + 1]);
if !all_on {
out[r * cols + c] = false;
}
}
}
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Component {
pub members: Vec<usize>,
pub bbox: (usize, usize, usize, usize),
}
pub(crate) fn components(
keep: &[bool],
cols: usize,
rows: usize,
min_size: usize,
) -> Vec<Component> {
let mut out = Vec::new();
let mut seen = vec![false; keep.len()];
let mut stack = Vec::new();
for start in 0..keep.len() {
if !keep[start] || seen[start] {
continue;
}
let (mut r0, mut r1, mut c0, mut c1) = (rows, 0usize, cols, 0usize);
let mut members = Vec::new();
stack.push(start);
seen[start] = true;
while let Some(i) = stack.pop() {
members.push(i);
let (r, c) = (i / cols, i % cols);
r0 = r0.min(r);
r1 = r1.max(r);
c0 = c0.min(c);
c1 = c1.max(c);
let mut push = |j: usize| {
if keep[j] && !seen[j] {
seen[j] = true;
stack.push(j);
}
};
if r > 0 {
push(i - cols);
}
if r + 1 < rows {
push(i + cols);
}
if c > 0 {
push(i - 1);
}
if c + 1 < cols {
push(i + 1);
}
}
if members.len() >= min_size {
out.push(Component {
members,
bbox: (r0, r1, c0, c1),
});
}
}
out
}
pub(crate) fn fill_components_bbox(
keep: &[bool],
cols: usize,
rows: usize,
min_size: usize,
) -> Vec<bool> {
let mut out = vec![false; keep.len()];
for comp in components(keep, cols, rows, min_size) {
let (r0, r1, c0, c1) = comp.bbox;
for r in r0..=r1 {
for c in c0..=c1 {
out[r * cols + c] = true;
}
}
}
out
}
const PANEL_MOVING_FRAC: f32 = 0.20;
const PANEL_DENSITY_FLOOR: f32 = 0.35;
const PANEL_GROWTH_SLACK: f32 = 0.02;
const PANEL_OVERLAP_FRAC: f32 = 0.30;
const PANEL_PENDING_DEADLINE_K: u16 = 8;
const PANEL_ONSET_WINDOW: u16 = 3;
#[derive(Debug, Clone)]
pub(crate) struct Panel {
members: Vec<usize>,
sticky: (usize, usize, usize, usize),
presented: bool,
quiet: u16,
age: u16,
last_kept: usize,
no_growth: u16,
low_support: u16,
}
#[derive(Debug, Default)]
pub(crate) struct PanelTracker {
panels: Vec<Panel>,
}
fn bbox_of(
blocks: impl Iterator<Item = usize>,
cols: usize,
) -> Option<(usize, usize, usize, usize)> {
let mut b: Option<(usize, usize, usize, usize)> = None;
for i in blocks {
let (r, c) = (i / cols, i % cols);
b = Some(match b {
None => (r, r, c, c),
Some((r0, r1, c0, c1)) => (r0.min(r), r1.max(r), c0.min(c), c1.max(c)),
});
}
b
}
impl PanelTracker {
pub(crate) fn reset(&mut self) {
self.panels.clear();
}
pub(crate) fn panels(&self) -> impl Iterator<Item = (bool, (usize, usize, usize, usize))> + '_ {
self.panels.iter().map(|p| (p.presented, p.sticky))
}
pub(crate) fn tick(
&mut self,
moving: &[bool],
keep_raw: &[bool],
cols: usize,
rows: usize,
k_panel: u16,
min_component: usize,
) -> Vec<bool> {
let n = cols * rows;
let mut owner = vec![usize::MAX; n];
for pass in 0..2 {
for (pi, p) in self.panels.iter().enumerate() {
let settled_content = p.quiet >= k_panel && p.last_kept >= min_component;
let routable = if pass == 0 {
!p.presented && (p.age < PANEL_ONSET_WINDOW || settled_content)
} else {
p.presented
};
if routable {
for &m in &p.members {
owner[m] = pi;
}
}
}
}
let core = owner.clone();
for (i, &own) in core.iter().enumerate() {
if own == usize::MAX {
continue;
}
let (r, c) = (i / cols, i % cols);
let mut mark = |j: usize| {
if owner[j] == usize::MAX {
owner[j] = own;
}
};
if r > 0 {
mark(i - cols);
}
if r + 1 < rows {
mark(i + cols);
}
if c > 0 {
mark(i - 1);
}
if c + 1 < cols {
mark(i + 1);
}
}
let len = self.panels.len();
let mut extend: Vec<Vec<usize>> = vec![Vec::new(); len];
let mut dead = vec![false; len];
let mut redirect: Vec<usize> = (0..len).collect();
let mut fresh: Vec<Component> = Vec::new();
fn resolve(redirect: &[usize], mut i: usize) -> usize {
while redirect[i] != i {
i = redirect[i];
}
i
}
for comp in components(moving, cols, rows, 1) {
let mut counts = vec![0usize; len];
for &m in &comp.members {
if owner[m] != usize::MAX {
counts[resolve(&redirect, owner[m])] += 1;
}
}
let mut matched: Vec<usize> = (0..len)
.filter(|&pi| counts[pi] > 0 && !dead[pi])
.filter(|&pi| {
let p = &self.panels[pi];
!p.presented
|| counts[pi] as f32 / comp.members.len() as f32 >= PANEL_OVERLAP_FRAC
})
.collect();
if matched.is_empty() {
if comp.members.len() >= min_component {
fresh.push(comp);
}
continue;
}
matched.sort_by_key(|&pi| (!self.panels[pi].presented, pi));
let survivor = matched[0];
for &victim in &matched[1..] {
let (v_members, v_presented, v_sticky, v_last) = {
let v = &mut self.panels[victim];
(
std::mem::take(&mut v.members),
v.presented,
v.sticky,
v.last_kept,
)
};
let moved = std::mem::take(&mut extend[victim]);
let sv = &mut self.panels[survivor];
sv.members.extend(v_members);
sv.last_kept += v_last;
if v_presented {
sv.presented = true;
let a = sv.sticky;
sv.sticky = (
a.0.min(v_sticky.0),
a.1.max(v_sticky.1),
a.2.min(v_sticky.2),
a.3.max(v_sticky.3),
);
}
extend[survivor].extend(moved);
dead[victim] = true;
redirect[victim] = survivor;
}
extend[survivor].extend_from_slice(&comp.members);
}
let mut in_fresh = vec![false; n];
for comp in &fresh {
for &m in &comp.members {
in_fresh[m] = true;
}
}
for (pi, p) in self.panels.iter().enumerate() {
if dead[pi] || p.presented || !extend[pi].is_empty() {
continue;
}
let covered = p.members.iter().filter(|&&m| in_fresh[m]).count();
if covered * 2 >= p.members.len() {
dead[pi] = true;
}
}
let deadline = k_panel.saturating_mul(PANEL_PENDING_DEADLINE_K);
for (pi, p) in self.panels.iter_mut().enumerate() {
if dead[pi] {
continue;
}
if !p.presented && p.age >= deadline {
dead[pi] = true; continue;
}
if !extend[pi].is_empty() {
p.members.extend_from_slice(&extend[pi]);
p.members.sort_unstable();
p.members.dedup();
}
let total = p.members.len().max(1) as f32;
let mv = p.members.iter().filter(|&&m| moving[m]).count() as f32;
if mv / total <= PANEL_MOVING_FRAC {
p.quiet = p.quiet.saturating_add(1);
} else {
p.quiet = 0;
p.no_growth = 0;
}
p.age = p.age.saturating_add(1);
let support = p.members.iter().filter(|&&m| keep_raw[m]).count();
if p.presented {
if support < min_component {
p.low_support = p.low_support.saturating_add(1);
if p.low_support >= k_panel {
dead[pi] = true; continue;
}
} else {
p.low_support = 0;
}
if let Some(b) = bbox_of(p.members.iter().copied(), cols) {
let sk = p.sticky;
p.sticky = (sk.0.min(b.0), sk.1.max(b.1), sk.2.min(b.2), sk.3.max(b.3));
}
}
if p.quiet >= k_panel {
let kept: Vec<usize> = p.members.iter().copied().filter(|&m| keep_raw[m]).collect();
let slack = ((p.members.len() as f32 * PANEL_GROWTH_SLACK) as usize).max(2);
let growing = kept.len() > p.last_kept.saturating_add(slack);
p.last_kept = kept.len();
p.no_growth = if growing {
0
} else {
p.no_growth.saturating_add(1)
};
let dense_bbox = bbox_of(kept.iter().copied(), cols).filter(|b| {
let area = ((b.1 - b.0 + 1) * (b.3 - b.2 + 1)) as f32;
kept.len() as f32 / area >= PANEL_DENSITY_FLOOR
});
if p.presented {
if let Some(b) = dense_bbox {
if p.no_growth >= 2 {
p.sticky = b; }
}
} else if kept.len() >= min_component && p.no_growth >= 2 {
if let Some(b) = dense_bbox {
p.presented = true;
p.sticky = b;
}
}
}
}
let mut di = dead.iter();
self.panels.retain(|_| !*di.next().unwrap());
for comp in fresh {
self.panels.push(Panel {
sticky: comp.bbox,
members: comp.members,
presented: false,
quiet: 0,
age: 0,
last_kept: 0,
no_growth: 0,
low_support: 0,
});
}
let mut member_of = vec![false; n];
for p in &self.panels {
for &m in &p.members {
member_of[m] = true;
}
}
let orphan: Vec<bool> = (0..n)
.map(|i| keep_raw[i] && !member_of[i] && !moving[i])
.collect();
for comp in components(&orphan, cols, rows, min_component) {
self.panels.push(Panel {
sticky: comp.bbox,
members: comp.members,
presented: false,
quiet: 0,
age: 0,
last_kept: 0,
no_growth: 0,
low_support: 0,
});
}
if std::env::var_os("VC_PANEL_TRACE").is_some() {
for (pi, p) in self.panels.iter().enumerate() {
if p.members.len() >= 40 {
let mv = p.members.iter().filter(|&&m| moving[m]).count();
let kept = p.members.iter().filter(|&&m| keep_raw[m]).count();
eprintln!(
"PANEL {pi}: presented={} age={} quiet={} members={} moving={} kept={} last_kept={} sticky={:?}",
p.presented, p.age, p.quiet, p.members.len(), mv, kept, p.last_kept, p.sticky
);
}
}
eprintln!("PANEL --- tick done ({} panels)", self.panels.len());
}
let mut out = vec![false; n];
for p in self.panels.iter().filter(|p| p.presented) {
let (r0, r1, c0, c1) = p.sticky;
for r in r0..=r1 {
for c in c0..=c1 {
out[r * cols + c] = true;
}
}
}
out
}
}
pub struct StabilityMask {
block: u32,
params: MaskParams,
dilate: u32,
scene_threshold: f32,
fill_components: bool,
min_component: usize,
atomic_panels: bool,
tracker: PanelTracker,
holdover_explicit: bool,
state: Vec<Block>,
dims: (u32, u32),
tick: u64,
sinks: Vec<Box<dyn DebugSink>>,
}
impl Default for StabilityMask {
fn default() -> Self {
Self::new()
}
}
impl StabilityMask {
pub fn new() -> Self {
Self {
block: 16,
params: MaskParams {
stable_ticks: 6,
baseline_ticks: 100,
signature_tolerance: 10,
novelty_threshold: 18,
contrast_threshold: 14,
holdover_ticks: 12,
long_run: 18,
},
dilate: 1,
scene_threshold: 0.65,
fill_components: true,
min_component: 4,
atomic_panels: true,
tracker: PanelTracker::default(),
holdover_explicit: false,
state: Vec::new(),
dims: (0, 0),
tick: 0,
sinks: Vec::new(),
}
}
pub fn block_size(mut self, px: u32) -> Self {
assert!(px > 0, "block size must be positive");
self.block = px;
self
}
pub fn stable_ticks(mut self, k: u32) -> Self {
let k = k.clamp(1, u16::MAX as u32) as u16;
self.params.stable_ticks = k;
self.params.long_run = k.saturating_mul(3);
if !self.holdover_explicit {
self.params.holdover_ticks = k.saturating_mul(2);
}
self
}
pub fn stable_for(self, rate: Rate, duration: Duration) -> Self {
let ticks = (duration.as_secs_f64() / rate.period().as_secs_f64()).ceil() as u32;
self.stable_ticks(ticks.max(1))
}
pub fn baseline_ticks(mut self, t: u32) -> Self {
self.params.baseline_ticks = t.max(1);
self
}
pub fn baseline(self, rate: Rate, duration: Duration) -> Self {
let ticks = (duration.as_secs_f64() / rate.period().as_secs_f64()).ceil() as u32;
self.baseline_ticks(ticks.max(1))
}
pub fn dilate(mut self, rings: u32) -> Self {
self.dilate = rings;
self
}
pub fn signature_tolerance(mut self, tol: u8) -> Self {
self.params.signature_tolerance = tol;
self
}
pub fn baseline_threshold(mut self, thr: u8) -> Self {
self.params.novelty_threshold = thr;
self
}
pub fn contrast_threshold(mut self, thr: u8) -> Self {
self.params.contrast_threshold = thr;
self
}
pub fn scene_threshold(mut self, frac: f32) -> Self {
assert!((0.0..=1.0).contains(&frac), "fraction in 0..=1");
self.scene_threshold = frac;
self
}
pub fn atomic_panels(mut self, on: bool) -> Self {
self.atomic_panels = on;
self
}
pub fn fill_components(mut self, on: bool) -> Self {
self.fill_components = on;
self
}
pub fn min_component(mut self, blocks: usize) -> Self {
self.min_component = blocks.max(1);
self
}
pub fn holdover_ticks(mut self, h: u32) -> Self {
self.params.holdover_ticks = h.clamp(1, u16::MAX as u32) as u16;
self.holdover_explicit = true;
self
}
pub fn debug_sink(mut self, sink: impl DebugSink) -> Self {
self.sinks.push(Box::new(sink));
self
}
fn grid(&self, w: u32, h: u32) -> (usize, usize) {
(
(w as usize).div_ceil(self.block as usize),
(h as usize).div_ceil(self.block as usize),
)
}
}
fn compute_signatures(
view: &FrameView<'_>,
block: u32,
cols: usize,
rows: usize,
) -> Vec<Signature> {
let n = cols * rows;
let b = block as usize;
let mut sub_sum = vec![[0u64; 16]; n];
let mut sub_cnt = vec![[0u32; 16]; n];
let mut sum = vec![0u64; n];
let mut sum_sq = vec![0u64; n];
let mut cnt = vec![0u64; n];
for (y, row) in view.rows().enumerate() {
let br = y / b;
let sr = ((y % b) * 4) / b; for (x, px) in row.chunks_exact(4).enumerate() {
let bc = x / b;
let sc = ((x % b) * 4) / b;
let luma = (px[2] as u32 * 299 + px[1] as u32 * 587 + px[0] as u32 * 114) / 1000;
let bi = br * cols + bc;
let si = sr * 4 + sc;
sub_sum[bi][si] += luma as u64;
sub_cnt[bi][si] += 1;
sum[bi] += luma as u64;
sum_sq[bi] += (luma as u64) * (luma as u64);
cnt[bi] += 1;
}
}
(0..n)
.map(|i| {
let mut sig = [0u8; SIG_LEN];
let mean = sum[i].checked_div(cnt[i]).unwrap_or(0) as u8;
for s in 0..16 {
sig[s] = sub_sum[i][s]
.checked_div(sub_cnt[i][s] as u64)
.map_or(mean, |v| v as u8);
}
sig[16] = if cnt[i] == 0 {
0
} else {
let m = sum[i] as f64 / cnt[i] as f64;
let var = (sum_sq[i] as f64 / cnt[i] as f64) - m * m;
var.max(0.0).sqrt().min(255.0) as u8
};
Signature(sig)
})
.collect()
}
impl Preprocessor for StabilityMask {
fn process(&mut self, view: &FrameView<'_>) -> Result<Arc<Frame>, DetectorError> {
let (w, h) = (view.width(), view.height());
let (cols, rows) = self.grid(w, h);
if self.dims != (w, h) {
self.state = vec![Block::default(); cols * rows];
self.tracker.reset();
self.dims = (w, h);
}
let n = cols * rows;
let p = &self.params;
let sigs = compute_signatures(view, self.block, cols, rows);
let mut moving = vec![false; n];
let mut initialized = 0usize;
let mut idle = 0usize;
let mut perturbed = 0usize;
let mut census_eligible = vec![false; n];
let mut census_count = 0usize;
let mut would_enter = 0usize;
for i in 0..n {
let b = &self.state[i];
if !b.initialized {
continue;
}
initialized += 1;
if b.phase == Phase::Idle {
idle += 1;
}
let mv = is_moving(&sigs[i], &b.last, p.signature_tolerance);
moving[i] = mv;
if mv {
perturbed += 1;
} else {
if b.unstable_run >= p.stable_ticks {
census_eligible[i] = true;
census_count += 1;
}
if b.phase == Phase::Idle
&& b.stable_ticks >= p.stable_ticks
&& is_novel_vs(
&sigs[i],
&narrow(&b.base),
p.novelty_threshold,
p.contrast_threshold,
)
{
would_enter += 1;
}
}
}
let denom = initialized.max(1) as f32;
let frac = perturbed as f32 / denom;
let globals = Globals {
scene_tick: frac >= self.scene_threshold,
corroborated_tick: frac >= CORROBORATION_FRAC,
census_hit: (census_count as f32 / denom) >= CENSUS_FRAC,
drift_guard: (would_enter as f32 / idle.max(1) as f32) >= DRIFT_FRAC,
};
let mut keep_raw = vec![false; n];
for i in 0..n {
keep_raw[i] = step_block(
&mut self.state[i],
&sigs[i],
moving[i],
census_eligible[i],
&globals,
p,
);
}
let keep = if self.atomic_panels {
let presented = self.tracker.tick(
&moving,
&keep_raw,
cols,
rows,
p.stable_ticks,
self.min_component,
);
if self.dilate == 0 {
presented
} else {
dilate_grid(&presented, cols, rows, self.dilate as usize)
}
} else if self.fill_components {
let filled = fill_components_bbox(&keep_raw, cols, rows, self.min_component);
if self.dilate == 0 {
filled
} else {
dilate_grid(&filled, cols, rows, self.dilate as usize)
}
} else {
close_and_dilate(&keep_raw, cols, rows, self.dilate)
};
let mut data = vec![0u8; w as usize * h as usize * 4];
for px in data.chunks_exact_mut(4) {
px[3] = 255;
}
for (y, row) in view.rows().enumerate() {
let br = y / self.block as usize;
for bc in 0..cols {
if !keep[br * cols + bc] {
continue;
}
let x0 = bc * self.block as usize * 4;
let x1 = (((bc + 1) * self.block as usize) * 4).min(row.len());
let dst = y * w as usize * 4;
data[dst + x0..dst + x1].copy_from_slice(&row[x0..x1]);
}
}
let output = Frame::new(w, h, data)
.map(Arc::new)
.map_err(|e| DetectorError::Other(format!("mask render: {e}")))?;
if !self.sinks.is_empty() {
self.emit_debug(view, &keep, cols, &output);
}
self.tick += 1;
Ok(output)
}
fn set_label(&mut self, label: &str) {
for sink in &mut self.sinks {
sink.set_label(label);
}
}
}
impl StabilityMask {
fn emit_debug(&mut self, view: &FrameView<'_>, keep: &[bool], cols: usize, output: &Frame) {
let (w, h) = (view.width(), view.height());
let input = Frame::new(w, h, view.to_vec()).expect("view-sized buffer");
let mut baseline = vec![0u8; w as usize * h as usize * 4];
let mut state = vec![0u8; w as usize * h as usize * 4];
let mut overlay = input.data().to_vec();
for y in 0..h as usize {
let br = y / self.block as usize;
for x in 0..w as usize {
let bc = x / self.block as usize;
let i = (y * w as usize + x) * 4;
let blk = &self.state[br * cols + bc];
let mean: u32 = blk.base[..16].iter().map(|v| (*v >> 8) as u32).sum::<u32>() / 16;
baseline[i] = mean as u8;
baseline[i + 1] = mean as u8;
baseline[i + 2] = mean as u8;
baseline[i + 3] = 255;
let bgra: [u8; 4] = if blk.reseeded {
[0, 0, 220, 255] } else {
match blk.phase {
Phase::Kept => [0, 200, 0, 255], Phase::Holdover => [0, 200, 200, 255], Phase::Idle if blk.base_frozen => [120, 40, 0, 255], Phase::Idle => [0, 0, 0, 255],
}
};
state[i..i + 4].copy_from_slice(&bgra);
if !keep[br * cols + bc] {
overlay[i] /= 4;
overlay[i + 1] /= 4;
overlay[i + 2] /= 4;
}
}
}
if self.atomic_panels {
let bpx = self.block as usize;
for (presented, (r0, r1, c0, c1)) in self.tracker.panels() {
let shade = if presented { 255u8 } else { 110 };
let mut paint = |br: usize, bc: usize| {
let y0 = br * bpx;
let x0 = bc * bpx;
for y in y0..(y0 + bpx).min(h as usize) {
for x in x0..(x0 + bpx).min(w as usize) {
let i = (y * w as usize + x) * 4;
state[i] = shade;
state[i + 1] = shade;
state[i + 2] = shade;
}
}
};
for bc in c0..=c1 {
paint(r0, bc);
paint(r1, bc);
}
for br in r0..=r1 {
paint(br, c0);
paint(br, c1);
}
}
}
let baseline = Frame::new(w, h, baseline).expect("view-sized buffer");
let state = Frame::new(w, h, state).expect("view-sized buffer");
let overlay = Frame::new(w, h, overlay).expect("view-sized buffer");
for sink in &mut self.sinks {
sink.write(self.tick, DebugStage::Input, &input);
sink.write(self.tick, DebugStage::Baseline, &baseline);
sink.write(self.tick, DebugStage::Overlay, &overlay);
sink.write(self.tick, DebugStage::State, &state);
sink.write(self.tick, DebugStage::Output, output);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
fn params() -> MaskParams {
MaskParams {
stable_ticks: 3,
baseline_ticks: 20,
signature_tolerance: 10,
novelty_threshold: 18,
contrast_threshold: 14,
holdover_ticks: 6,
long_run: 9,
}
}
fn flat(luma: u8) -> Signature {
let mut s = [luma; SIG_LEN];
s[16] = 0;
s.into()
}
fn textured(luma: u8, spread: u8, c: u8) -> Signature {
let mut s = [0u8; SIG_LEN];
for (i, v) in s[..16].iter_mut().enumerate() {
*v = if i % 2 == 0 {
luma.saturating_add(spread)
} else {
luma.saturating_sub(spread)
};
}
s[16] = c;
Signature(s)
}
impl From<[u8; SIG_LEN]> for Signature {
fn from(v: [u8; SIG_LEN]) -> Self {
Signature(v)
}
}
const QUIET: Globals = Globals {
scene_tick: false,
corroborated_tick: false,
census_hit: false,
drift_guard: false,
};
fn run(b: &mut Block, script: &[(Signature, Globals)], p: &MaskParams) -> Vec<bool> {
script
.iter()
.map(|(sig, g)| {
let mv = b.initialized && is_moving(sig, &b.last, p.signature_tolerance);
let census = b.initialized && !mv && b.unstable_run >= p.stable_ticks;
step_block(b, sig, mv, census, g, p)
})
.collect()
}
fn quiet(sigs: &[Signature]) -> Vec<(Signature, Globals)> {
sigs.iter().map(|s| (*s, QUIET)).collect()
}
fn rep(sig: Signature, n: usize) -> Vec<Signature> {
vec![sig; n]
}
#[test]
fn single_component_noise_cannot_flip_movement_or_novelty() {
let a = flat(100);
let mut b = a;
b.0[3] = 140; assert!(!is_moving(&b, &a, 10), "one component is not movement");
assert!(
!is_novel_vs(&b, &a.0, 18, 14),
"one component is not novelty"
);
b.0[7] = 140; assert!(is_moving(&b, &a, 10));
assert!(is_novel_vs(&b, &a.0, 18, 14));
}
#[test]
fn dark_on_dark_flat_panel_is_novel_via_contrast() {
let scene = textured(30, 12, 40);
let panel = flat(30);
assert!(
is_novel_vs(&panel, &scene.0, 18, 14),
"flat panel over textured scene must be novel"
);
}
#[test]
fn signatures_capture_subblock_structure() {
let frame = Frame::from_fn(16, 16, |x, _| {
if x < 8 {
[0, 0, 0, 255]
} else {
[255, 255, 255, 255]
}
});
let view = frame
.view(visual_cortex_capture::PxRect {
x: 0,
y: 0,
w: 16,
h: 16,
})
.unwrap();
let sigs = compute_signatures(&view, 16, 1, 1);
let s = &sigs[0].0;
assert!(s[0] < 10 && s[1] < 10, "left sub-cells dark");
assert!(s[2] > 245 && s[3] > 245, "right sub-cells bright");
assert!(s[16] > 100, "half-and-half block has high contrast");
}
#[test]
fn constant_hud_block_is_never_kept() {
let mut b = Block::default();
let out = run(&mut b, &quiet(&rep(textured(100, 20, 30), 30)), ¶ms());
assert!(out.iter().all(|k| !k), "HUD must stay suppressed");
}
#[test]
fn noisy_gameplay_block_is_never_kept() {
let mut b = Block::default();
let sigs: Vec<Signature> = (0..30)
.map(|i| flat(if i % 2 == 0 { 40 } else { 200 }))
.collect();
let out = run(&mut b, &quiet(&sigs), ¶ms());
assert!(out.iter().all(|k| !k), "churn must stay suppressed");
}
#[test]
fn tooltip_becomes_kept_after_k_stable_ticks() {
let mut b = Block::default();
run(&mut b, &quiet(&rep(flat(60), 10)), ¶ms());
let out = run(&mut b, &quiet(&rep(flat(200), 6)), ¶ms());
assert_eq!(out, vec![false, false, false, true, true, true]);
assert_eq!(b.phase, Phase::Kept);
}
#[test]
fn held_tooltip_survives_indefinitely() {
let mut b = Block::default();
run(&mut b, &quiet(&rep(flat(60), 10)), ¶ms());
let out = run(&mut b, &quiet(&rep(flat(200), 200)), ¶ms());
assert!(out[10..].iter().all(|k| *k), "no ghosting, ever");
assert!(b.base_frozen, "baseline frozen under the kept overlay");
}
#[test]
fn dismissed_tooltip_thaws_and_suppresses() {
let mut b = Block::default();
run(&mut b, &quiet(&rep(flat(60), 10)), ¶ms());
run(&mut b, &quiet(&rep(flat(200), 10)), ¶ms());
let out = run(&mut b, &quiet(&rep(flat(60), 10)), ¶ms());
assert_eq!(b.phase, Phase::Idle);
assert!(!b.base_frozen, "baseline resumed after departure");
assert!(!out.last().unwrap(), "restored scene suppressed");
}
#[test]
fn browsing_hand_off_never_goes_dark() {
let mut b = Block::default();
run(&mut b, &quiet(&rep(flat(60), 10)), ¶ms());
run(&mut b, &quiet(&rep(flat(200), 10)), ¶ms()); let mut script = rep(flat(150), 1); script.extend(rep(flat(230), 8)); let out = run(&mut b, &quiet(&script), ¶ms());
assert!(
out.iter().all(|k| *k),
"hand-off must render every tick, got {out:?}"
);
assert_eq!(b.phase, Phase::Kept, "re-latched onto tooltip B");
}
#[test]
fn flash_over_held_tooltip_relatches_without_reseed() {
let p = params();
let mut b = Block::default();
run(&mut b, &quiet(&rep(flat(60), 10)), &p);
run(&mut b, &quiet(&rep(flat(200), 10)), &p); let flash = Globals {
scene_tick: true,
corroborated_tick: true,
census_hit: false,
drift_guard: false,
};
let script = vec![
(flat(255), flash),
(flat(255), flash),
(flat(200), QUIET), (flat(200), QUIET),
];
let out = run(&mut b, &script, &p);
assert!(out.iter().all(|k| *k), "renders through the flash: {out:?}");
assert_eq!(b.phase, Phase::Kept, "immediate ovl re-latch");
assert!(!b.reseeded, "a flash is not a scene change");
assert!(b.base_frozen, "invariant intact: base still frozen");
}
#[test]
fn reveal_after_occluded_scene_change_resolves_on_next_motion() {
let p = params();
let mut b = Block::default();
run(&mut b, &quiet(&rep(flat(60), 10)), &p);
run(&mut b, &quiet(&rep(flat(200), 10)), &p); let scene = Globals {
scene_tick: true,
corroborated_tick: true,
census_hit: false,
drift_guard: false,
};
run(&mut b, &[(flat(150), scene)], &p); run(&mut b, &quiet(&rep(flat(200), 6)), &p); assert_eq!(b.phase, Phase::Kept, "tooltip survives the event");
let out = run(&mut b, &quiet(&rep(flat(120), 8)), &p);
assert!(*out.last().unwrap(), "ambiguous reveal stays rendered");
let walking = Globals {
scene_tick: false,
corroborated_tick: true,
census_hit: false,
drift_guard: false,
};
let script: Vec<(Signature, Globals)> = (0..12)
.map(|i| (flat(if i % 2 == 0 { 80 } else { 170 }), walking))
.collect();
run(&mut b, &script, &p);
let out = run(&mut b, &quiet(&rep(flat(140), 8)), &p);
assert!(!out.last().unwrap(), "re-anchored after motion");
let base = narrow(&b.base);
assert!(
base[0].abs_diff(140) < 10,
"baseline follows the real scene"
);
}
#[test]
fn browsing_hand_off_survives_a_prior_flash() {
let p = params();
let mut b = Block::default();
run(&mut b, &quiet(&rep(flat(60), 10)), &p);
run(&mut b, &quiet(&rep(flat(200), 10)), &p); let flash = Globals {
scene_tick: true,
corroborated_tick: true,
census_hit: false,
drift_guard: false,
};
run(&mut b, &[(flat(255), flash), (flat(255), flash)], &p);
run(&mut b, &quiet(&rep(flat(200), 4)), &p); assert_eq!(b.phase, Phase::Kept);
let mut script = rep(flat(150), 1);
script.extend(rep(flat(230), 8));
let out = run(&mut b, &quiet(&script), &p);
assert!(
out.iter().all(|k| *k),
"hand-off renders throughout: {out:?}"
);
assert_eq!(b.phase, Phase::Kept, "tooltip B kept");
let base = narrow(&b.base);
assert!(
base[0].abs_diff(60) < 10,
"baseline still the scene, never overlay content: base={} frozen={} scene_flag={}",
base[0],
b.base_frozen,
b.scene_flag
);
}
#[test]
fn walk_and_stop_reseeds_instead_of_keeping() {
let p = params();
let mut b = Block::default();
run(&mut b, &quiet(&rep(flat(60), 5)), &p);
let walking = Globals {
scene_tick: false,
corroborated_tick: true, census_hit: false,
drift_guard: false,
};
let script: Vec<(Signature, Globals)> = (0..12)
.map(|i| (flat(if i % 2 == 0 { 90 } else { 180 }), walking))
.collect();
run(&mut b, &script, &p); let out = run(&mut b, &quiet(&rep(flat(140), 8)), &p);
assert!(
out.iter().all(|k| !k),
"new scene must not be kept: {out:?}"
);
let base = narrow(&b.base);
assert!(base[0].abs_diff(140) < 10, "re-seeded to the new scene");
}
#[test]
fn fast_browsing_churn_is_not_corroborated_and_final_tooltip_is_kept() {
let p = params();
let mut b = Block::default();
run(&mut b, &quiet(&rep(flat(60), 10)), &p);
let script: Vec<Signature> = (0..20)
.map(|i| flat(if i % 2 == 0 { 200 } else { 120 }))
.collect();
run(&mut b, &quiet(&script), &p);
assert!(!b.run_corroborated, "local churn is never corroborated");
let out = run(&mut b, &quiet(&rep(flat(200), 6)), &p);
assert!(*out.last().unwrap(), "final tooltip must be kept: {out:?}");
assert_eq!(b.phase, Phase::Kept);
}
#[test]
fn minimap_pulse_train_is_never_kept() {
let p = params();
let mut b = Block::default();
let mut script = Vec::new();
for _ in 0..6 {
script.extend(rep(textured(80, 15, 25), 8)); script.extend(rep(textured(140, 15, 25), 2)); }
let out = run(&mut b, &quiet(&script), &p);
assert!(out.iter().all(|k| !k), "pulsing HUD must stay suppressed");
}
#[test]
fn census_flag_reseeds_short_global_motion() {
let p = params();
let mut b = Block::default();
run(&mut b, &quiet(&rep(flat(60), 5)), &p);
let script: Vec<(Signature, Globals)> = (0..5)
.map(|i| (flat(if i % 2 == 0 { 90 } else { 180 }), QUIET))
.collect();
run(&mut b, &script, &p); let census = Globals {
scene_tick: false,
corroborated_tick: false,
census_hit: true,
drift_guard: false,
};
let mut settle = vec![(flat(140), census); 3];
settle.extend(quiet(&rep(flat(140), 6)));
let out = run(&mut b, &settle, &p);
assert!(out.iter().all(|k| !k), "census settle re-seeds: {out:?}");
}
#[test]
fn drift_guard_reseeds_slow_drift_but_never_pop_ins() {
let p = params();
let guard = Globals {
scene_tick: false,
corroborated_tick: false,
census_hit: false,
drift_guard: true,
};
let mut b = Block::default();
run(&mut b, &quiet(&rep(flat(60), 10)), &p);
let script: Vec<(Signature, Globals)> =
(0..12).map(|i| (flat(60 + 8 * (i + 1)), guard)).collect();
let out = run(&mut b, &script, &p);
assert!(out.iter().all(|k| !k), "ambient fade must not be kept");
let mut b = Block::default();
run(&mut b, &quiet(&rep(flat(60), 10)), &p);
let script: Vec<(Signature, Globals)> =
rep(flat(200), 6).into_iter().map(|s| (s, guard)).collect();
let out = run(&mut b, &script, &p);
assert!(*out.last().unwrap(), "pop-in is kept even under the guard");
}
#[test]
fn holdover_expires_to_idle_when_content_keeps_churning() {
let p = params();
let mut b = Block::default();
run(&mut b, &quiet(&rep(flat(60), 10)), &p);
run(&mut b, &quiet(&rep(flat(200), 10)), &p); let script: Vec<Signature> = (0..12)
.map(|i| flat(if i % 2 == 0 { 90 } else { 170 }))
.collect();
let out = run(&mut b, &quiet(&script), &p);
assert!(out[0], "holdover renders at first");
assert!(!out.last().unwrap(), "expired holdover suppresses");
assert_eq!(b.phase, Phase::Idle);
}
#[test]
fn component_fill_drops_speckle_and_fills_bboxes() {
let cols = 6;
let mut keep = vec![false; 36];
for (r, c) in [(1, 1), (2, 1), (3, 1), (3, 2), (3, 3)] {
keep[r * cols + c] = true;
}
keep[5 * cols + 5] = true; let out = fill_components_bbox(&keep, cols, 6, 4);
assert!(out[cols + 2] && out[cols + 3], "bbox filled the L's notch");
assert!(!out[5 * cols + 5], "speckle dropped");
}
#[test]
fn close_fills_interior_holes_and_dilate_expands() {
let cols = 5;
let mut keep = vec![false; 25];
for (r, c) in [
(1usize, 1usize),
(1, 2),
(1, 3),
(2, 1),
(2, 3),
(3, 1),
(3, 2),
(3, 3),
] {
keep[r * cols + c] = true;
}
let out = close_and_dilate(&keep, cols, 5, 0);
assert!(out[2 * cols + 2], "interior hole must be closed");
let out = close_and_dilate(&keep, cols, 5, 1);
assert!(out[cols], "edge cell reached by one dilation ring");
assert!(
!out[0],
"corner is two steps away; one ring must not reach it"
);
}
#[test]
fn cold_start_keeps_nothing() {
let mut b = Block::default();
assert!(!step_block(
&mut b,
&flat(200),
false,
false,
&QUIET,
¶ms()
));
}
fn grid(cols: usize, rows: usize, rects: &[(usize, usize, usize, usize)]) -> Vec<bool> {
let mut g = vec![false; cols * rows];
for &(r0, r1, c0, c1) in rects {
for r in r0..=r1 {
for c in c0..=c1 {
g[r * cols + c] = true;
}
}
}
g
}
const K_PANEL: u16 = 3;
fn ptick(
t: &mut PanelTracker,
cols: usize,
rows: usize,
moving: &[(usize, usize, usize, usize)],
keep: &[(usize, usize, usize, usize)],
) -> Vec<bool> {
t.tick(
&grid(cols, rows, moving),
&grid(cols, rows, keep),
cols,
rows,
K_PANEL,
1,
)
}
#[test]
fn onset_group_presents_whole_despite_fragmentary_keeps() {
let (c, r) = (10, 10);
let mut t = PanelTracker::default();
let rect = (1, 6, 1, 6);
let out = ptick(&mut t, c, r, &[rect], &[]);
assert!(out.iter().all(|k| !k), "onset tick: nothing rendered");
let frags = [(1, 2, 1, 6), (5, 6, 1, 3)];
for _ in 0..(K_PANEL + 1) {
let out = ptick(&mut t, c, r, &[], &frags);
assert!(out.iter().all(|k| !k), "still settling: no render");
}
let out = ptick(&mut t, c, r, &[], &frags);
assert_eq!(out, grid(c, r, &[(1, 6, 1, 6)]), "whole rect at once");
}
#[test]
fn gameplay_churn_never_presents_and_expires() {
let (c, r) = (10, 10);
let mut t = PanelTracker::default();
for _ in 0..(K_PANEL * PANEL_PENDING_DEADLINE_K + 2) {
let out = ptick(&mut t, c, r, &[(2, 7, 2, 7)], &[(3, 3, 3, 3)]);
assert!(out.iter().all(|k| !k), "churn must never present");
}
assert!(
t.panels().all(|(presented, _)| !presented),
"churn never presents"
);
}
#[test]
fn camera_pan_settles_to_nothing_via_the_group_kept_test() {
let (c, r) = (10, 10);
let mut t = PanelTracker::default();
for _ in 0..3 {
ptick(&mut t, c, r, &[(0, 9, 0, 9)], &[]);
}
for _ in 0..(K_PANEL * PANEL_PENDING_DEADLINE_K + 2) {
let out = ptick(&mut t, c, r, &[], &[]);
assert!(out.iter().all(|k| !k), "no panel from a pan");
}
assert_eq!(t.panels().count(), 0, "churn panel reaped at deadline");
}
fn present_panel(t: &mut PanelTracker, c: usize, r: usize, rect: (usize, usize, usize, usize)) {
ptick(t, c, r, &[rect], &[]);
for _ in 0..(K_PANEL + 2) {
ptick(t, c, r, &[], &[rect]);
}
}
#[test]
fn hand_off_keeps_the_rect_and_dismissal_withdraws_whole() {
let (c, r) = (10, 10);
let mut t = PanelTracker::default();
let rect = (1, 6, 1, 6);
present_panel(&mut t, c, r, rect);
let full = grid(c, r, &[rect]);
for _ in 0..2 {
let out = ptick(&mut t, c, r, &[rect], &[]);
assert_eq!(out, full, "rect stays whole through churn");
}
for _ in 0..K_PANEL {
let out = ptick(&mut t, c, r, &[], &[(1, 4, 1, 6)]);
assert_eq!(out, full, "still whole while settling");
}
let out = ptick(&mut t, c, r, &[], &[(1, 4, 1, 6)]);
assert_eq!(out, grid(c, r, &[(1, 4, 1, 6)]), "atomic snap");
ptick(&mut t, c, r, &[(1, 4, 1, 6)], &[]);
for _ in 0..(K_PANEL - 2) {
let out = ptick(&mut t, c, r, &[], &[]);
assert!(!out.iter().all(|k| !k), "grace while support collapses");
}
let out = ptick(&mut t, c, r, &[], &[]);
assert!(out.iter().all(|k| !k), "withdrawn whole");
assert_eq!(t.panels().count(), 0);
}
#[test]
fn straggler_trickle_does_not_postpone_presentation() {
let (c, r) = (60, 4);
let mut t = PanelTracker::default();
ptick(&mut t, c, r, &[(1, 2, 1, 50)], &[]); let mut presented = false;
for i in 0..(K_PANEL as usize + 4) {
let cols_kept = (30 + i).min(50);
let out = ptick(&mut t, c, r, &[], &[(1, 2, 1, cols_kept)]);
presented = out.iter().any(|k| *k);
if presented {
break;
}
}
assert!(presented, "trickle must not starve presentation");
}
#[test]
fn stale_pending_hypotheses_never_starve_a_new_onset() {
let (c, r) = (10, 10);
let mut t = PanelTracker::default();
for _ in 0..6 {
ptick(&mut t, c, r, &[(1, 8, 1, 8)], &[]);
}
ptick(&mut t, c, r, &[(2, 6, 2, 6), (9, 9, 1, 8)], &[]);
for _ in 0..(K_PANEL + 2) {
ptick(&mut t, c, r, &[(9, 9, 1, 8)], &[(2, 6, 2, 6)]);
}
let out = ptick(&mut t, c, r, &[(9, 9, 1, 8)], &[(2, 6, 2, 6)]);
assert_eq!(
out,
grid(c, r, &[(2, 6, 2, 6)]),
"the settled tooltip must present despite the stale hypothesis"
);
}
#[test]
fn sparse_scene_settle_scatter_never_presents() {
let (c, r) = (20, 20);
let mut t = PanelTracker::default();
ptick(&mut t, c, r, &[(0, 19, 0, 19)], &[]);
let scatter: Vec<(usize, usize, usize, usize)> = (0..400)
.filter(|i| i % 7 == 0)
.map(|i| (i / 20, i / 20, i % 20, i % 20))
.collect();
for _ in 0..(K_PANEL + 6) {
let out = ptick(&mut t, c, r, &[], &scatter);
assert!(out.iter().all(|k| !k), "sparse scatter must not present");
}
}
#[test]
fn sparse_settle_waits_for_assembly_instead_of_dying() {
let (c, r) = (10, 10);
let mut t = PanelTracker::default();
ptick(&mut t, c, r, &[(1, 6, 1, 6)], &[]);
let scatter = [(1, 1, 1, 1), (3, 3, 3, 3), (5, 5, 5, 5), (6, 6, 2, 2)];
for _ in 0..(K_PANEL * 4) {
let out = ptick(&mut t, c, r, &[], &scatter);
assert!(out.iter().all(|k| !k), "sparse: waiting, not rendering");
}
for _ in 0..2 {
ptick(&mut t, c, r, &[], &[(1, 6, 1, 6)]);
}
let out = ptick(&mut t, c, r, &[], &[(1, 6, 1, 6)]);
assert_eq!(out, grid(c, r, &[(1, 6, 1, 6)]), "presents once dense");
}
#[test]
fn disjoint_band_onsets_merge_into_one_panel() {
let (c, r) = (10, 10);
let mut t = PanelTracker::default();
ptick(&mut t, c, r, &[(1, 2, 1, 6)], &[]); ptick(&mut t, c, r, &[(5, 6, 1, 6)], &[]); ptick(&mut t, c, r, &[(3, 4, 1, 6)], &[]); for _ in 0..(K_PANEL + 1) {
let out = ptick(&mut t, c, r, &[], &[(1, 6, 1, 6)]);
assert!(out.iter().all(|k| !k), "no partial band presents");
}
let out = ptick(&mut t, c, r, &[], &[(1, 6, 1, 6)]);
assert_eq!(out, grid(c, r, &[(1, 6, 1, 6)]), "one whole rect");
assert_eq!(t.panels().count(), 1, "bands merged into one panel");
}
#[test]
fn disjoint_panels_present_and_withdraw_independently() {
let (c, r) = (12, 12);
let mut t = PanelTracker::default();
let (a, b) = ((1, 3, 1, 3), (7, 10, 7, 10));
ptick(&mut t, c, r, &[a, b], &[]);
for _ in 0..(K_PANEL + 2) {
ptick(&mut t, c, r, &[], &[a, b]);
}
let both = grid(c, r, &[a, b]);
let out = ptick(&mut t, c, r, &[], &[a, b]);
assert_eq!(out, both, "both panels presented");
ptick(&mut t, c, r, &[b], &[a]);
for _ in 0..(K_PANEL + 1) {
ptick(&mut t, c, r, &[], &[a]);
}
let out = ptick(&mut t, c, r, &[], &[a]);
assert_eq!(out, grid(c, r, &[a]), "only the first remains");
}
#[test]
fn slow_drift_kept_content_presents_via_orphan_seeding() {
let (c, r) = (10, 10);
let mut t = PanelTracker::default();
for _ in 0..(K_PANEL + 4) {
ptick(&mut t, c, r, &[], &[(2, 6, 2, 6)]); }
let out = ptick(&mut t, c, r, &[], &[(2, 6, 2, 6)]);
assert_eq!(out, grid(c, r, &[(2, 6, 2, 6)]), "fade content presents");
}
#[test]
fn cursor_blip_inside_presented_panel_is_harmless() {
let (c, r) = (10, 10);
let mut t = PanelTracker::default();
let rect = (1, 6, 1, 6);
present_panel(&mut t, c, r, rect);
let full = grid(c, r, &[rect]);
for _ in 0..6 {
let out = ptick(&mut t, c, r, &[(3, 3, 3, 3)], &[rect]);
assert_eq!(out, full);
}
assert_eq!(t.panels().count(), 1);
}
#[test]
fn neighboring_motion_does_not_extend_a_panel() {
let (c, r) = (12, 12);
let mut t = PanelTracker::default();
let rect = (1, 4, 1, 4);
present_panel(&mut t, c, r, rect);
let full = grid(c, r, &[rect]);
for _ in 0..4 {
let out = ptick(&mut t, c, r, &[(5, 10, 1, 10)], &[rect]);
assert_eq!(out, full, "edge motion must not bloat the rect");
}
}
#[test]
fn large_pop_in_covering_most_of_the_region_is_revealed() {
use visual_cortex_capture::PxRect;
let mut mask = StabilityMask::new()
.block_size(8)
.stable_ticks(3)
.min_component(1)
.dilate(0);
let full = PxRect {
x: 0,
y: 0,
w: 48,
h: 48,
}; let bg = Frame::solid(48, 48, [60, 60, 60, 255]);
let tip = Frame::from_fn(48, 48, |x, y| {
if y < 24 || (y < 32 && x < 32) {
[230, 230, 230, 255]
} else {
[60, 60, 60, 255]
}
});
for _ in 0..8 {
mask.process(&bg.view(full).unwrap()).unwrap();
}
let mut last = None;
for _ in 0..8 {
last = Some(mask.process(&tip.view(full).unwrap()).unwrap());
}
let out = last.unwrap();
let px = &out.data()[(4 * 48 + 4) * 4..(4 * 48 + 4) * 4 + 3];
assert_eq!(px, &[230, 230, 230], "pop-in region revealed, got {px:?}");
}
#[test]
fn staggered_pop_in_reveals_atomically_through_process() {
use visual_cortex_capture::PxRect;
let mut mask = StabilityMask::new()
.block_size(8)
.stable_ticks(3)
.min_component(1)
.dilate(0);
let full = PxRect {
x: 0,
y: 0,
w: 64,
h: 64,
}; let bg = Frame::solid(64, 64, [60, 60, 60, 255]);
let wave = |rows_to: u32| {
Frame::from_fn(64, 64, move |x, y| {
if (8..rows_to).contains(&y) && (8..40).contains(&x) {
[230, 230, 230, 255]
} else {
[60, 60, 60, 255]
}
})
};
for _ in 0..8 {
mask.process(&bg.view(full).unwrap()).unwrap();
}
let mut outputs = Vec::new();
outputs.push(mask.process(&wave(24).view(full).unwrap()).unwrap());
for _ in 0..12 {
outputs.push(mask.process(&wave(40).view(full).unwrap()).unwrap());
}
let mut fractions = Vec::new();
for out in &outputs {
let mut vis = 0usize;
let mut tot = 0usize;
for y in 8..40usize {
for x in 8..40usize {
let i = (y * 64 + x) * 4;
tot += 1;
if out.data()[i] == 230 {
vis += 1;
}
}
}
fractions.push(vis as f64 / tot as f64);
}
assert!(
fractions.iter().all(|f| *f < 0.05 || *f > 0.95),
"no partial reveals allowed: {fractions:?}"
);
assert!(
fractions.last().unwrap() > &0.95,
"tooltip presented: {fractions:?}"
);
}
#[test]
fn dismissal_withdraws_atomically_through_process() {
use visual_cortex_capture::PxRect;
let mut mask = StabilityMask::new()
.block_size(8)
.stable_ticks(3)
.min_component(1)
.dilate(0);
let full = PxRect {
x: 0,
y: 0,
w: 64,
h: 64,
};
let bg = Frame::solid(64, 64, [60, 60, 60, 255]);
let tip = Frame::from_fn(64, 64, |x, y| {
if (8..40).contains(&y) && (8..40).contains(&x) {
[230, 230, 230, 255]
} else {
[60, 60, 60, 255]
}
});
for _ in 0..8 {
mask.process(&bg.view(full).unwrap()).unwrap();
}
for _ in 0..10 {
mask.process(&tip.view(full).unwrap()).unwrap();
}
let mut fractions = Vec::new();
for _ in 0..10 {
let out = mask.process(&bg.view(full).unwrap()).unwrap();
let mut vis = 0usize;
for y in 8..40usize {
for x in 8..40usize {
if out.data()[(y * 64 + x) * 4] != 0 {
vis += 1;
}
}
}
fractions.push(vis as f64 / 1024.0);
}
assert!(
fractions.iter().all(|f| *f < 0.05 || *f > 0.95),
"withdraw must be atomic: {fractions:?}"
);
assert!(fractions.last().unwrap() < &0.05, "gone: {fractions:?}");
}
#[test]
fn builder_derives_holdover_and_long_run_from_stable_ticks() {
let m = StabilityMask::new().stable_ticks(5);
assert_eq!(m.params.holdover_ticks, 10, "H = 2K");
assert_eq!(m.params.long_run, 15, "U = 3K");
let m = StabilityMask::new().holdover_ticks(5).stable_ticks(10);
assert_eq!(m.params.holdover_ticks, 5, "explicit H wins");
assert_eq!(m.params.long_run, 30);
let m = StabilityMask::new().stable_ticks(300);
assert_eq!(m.params.holdover_ticks, 600);
}
#[test]
fn partial_edge_blocks_average_available_pixels_only() {
use visual_cortex_capture::PxRect;
let frame = Frame::from_fn(19, 13, |x, _| {
if x < 16 {
[40, 40, 40, 255]
} else {
[220, 220, 220, 255]
}
});
let view = frame
.view(PxRect {
x: 0,
y: 0,
w: 19,
h: 13,
})
.unwrap();
let sigs = compute_signatures(&view, 16, 2, 1);
assert!(
sigs[1].0[..16].iter().all(|v| *v > 200),
"partial block averages available pixels: {:?}",
sigs[1].0
);
assert!(sigs[0].0[..16].iter().all(|v| *v < 60));
}
#[test]
fn debug_sinks_receive_all_five_stages_per_tick() {
struct Recording(Arc<Mutex<Vec<(u64, DebugStage)>>>);
impl DebugSink for Recording {
fn write(&mut self, tick: u64, stage: DebugStage, _frame: &Frame) {
self.0.lock().unwrap().push((tick, stage));
}
}
let log = Arc::new(Mutex::new(Vec::new()));
let mut mask = StabilityMask::new()
.block_size(8)
.debug_sink(Recording(log.clone()));
let frame = Frame::solid(8, 8, [60, 60, 60, 255]);
let view = frame
.view(visual_cortex_capture::PxRect {
x: 0,
y: 0,
w: 8,
h: 8,
})
.unwrap();
mask.process(&view).unwrap();
mask.process(&view).unwrap();
use DebugStage::*;
assert_eq!(
*log.lock().unwrap(),
vec![
(0, Input),
(0, Baseline),
(0, Overlay),
(0, State),
(0, Output),
(1, Input),
(1, Baseline),
(1, Overlay),
(1, State),
(1, Output),
]
);
}
}