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
}
pub(crate) fn fill_components_bbox(
keep: &[bool],
cols: usize,
rows: usize,
min_size: usize,
) -> Vec<bool> {
let mut out = vec![false; keep.len()];
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 size = 0usize;
stack.push(start);
seen[start] = true;
while let Some(i) = stack.pop() {
size += 1;
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 size < min_size {
continue; }
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,
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,
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 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.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.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;
}
}
}
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()
));
}
#[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 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),
]
);
}
}