use crate::config::RoiKind;
use crate::detect::diff::TileDiff;
use crate::detect::roi::RoiSet;
#[derive(Debug, Clone)]
pub struct RegionState {
pub label: String,
pub kind: RoiKind,
pub busy: bool,
pub change_rate: f32,
}
#[derive(Debug)]
pub struct Volatility {
window: u16,
busy_threshold: f32,
num_tiles: usize,
ring: Vec<bool>,
ones: Vec<u16>,
slot: usize,
frames_seen: u32,
region_busy: Vec<bool>,
rising: Vec<String>,
falling: Vec<String>,
any_busy: bool,
any_volatile_active: bool,
}
impl Volatility {
pub fn new(window: u16, busy_threshold: f32, num_tiles: usize) -> Self {
let window = window.max(1);
Self {
window,
busy_threshold,
num_tiles,
ring: vec![false; window as usize * num_tiles],
ones: vec![0u16; num_tiles],
slot: 0,
frames_seen: 0,
region_busy: Vec::new(),
rising: Vec::new(),
falling: Vec::new(),
any_busy: false,
any_volatile_active: false,
}
}
#[inline]
pub fn tile_change_rate(&self, idx: usize) -> f32 {
if idx >= self.num_tiles {
return 0.0;
}
self.ones[idx] as f32 / self.window as f32
}
pub fn update(&mut self, diff: &TileDiff, roi: &RoiSet) -> Vec<RegionState> {
let base = self.slot * self.num_tiles;
for tile in 0..self.num_tiles {
let old = self.ring[base + tile];
let new = diff.changed.get(tile).copied().unwrap_or(false);
if old {
self.ones[tile] = self.ones[tile].saturating_sub(1);
}
if new {
self.ones[tile] = (self.ones[tile] + 1).min(self.window);
}
self.ring[base + tile] = new;
}
self.slot = (self.slot + 1) % self.window as usize;
self.frames_seen = self.frames_seen.saturating_add(1);
let regions = roi.regions();
let mut states = Vec::with_capacity(regions.len());
if self.region_busy.len() != regions.len() {
self.region_busy = vec![false; regions.len()];
}
self.rising.clear();
self.falling.clear();
self.any_busy = false;
self.any_volatile_active = false;
for (i, region) in regions.iter().enumerate() {
let rate = if region.tiles.is_empty() {
0.0
} else {
let sum: f32 = region.tiles.iter().map(|&t| self.tile_change_rate(t)).sum();
sum / region.tiles.len() as f32
};
let busy = match region.kind {
RoiKind::Spinner => rate >= self.busy_threshold,
RoiKind::Volatile => rate > 0.0,
_ => false,
};
if region.kind == RoiKind::Spinner {
let prev = self.region_busy[i];
if busy && !prev {
self.rising.push(region.label.clone());
} else if !busy && prev {
self.falling.push(region.label.clone());
}
if busy {
self.any_busy = true;
}
}
if region.kind == RoiKind::Volatile && busy {
self.any_volatile_active = true;
}
self.region_busy[i] = busy;
states.push(RegionState {
label: region.label.clone(),
kind: region.kind,
busy,
change_rate: rate,
});
}
states
}
pub fn busy_rising(&self) -> &[String] {
&self.rising
}
pub fn busy_falling(&self) -> &[String] {
&self.falling
}
pub fn any_busy(&self) -> bool {
self.any_busy
}
pub fn any_volatile_active(&self) -> bool {
self.any_volatile_active
}
}