use std::collections::{BTreeMap, BTreeSet, VecDeque};
use frust_core::widget::PlatformViewFrame;
use kurbo::Rect;
pub const EPSILON_PX: f64 = 0.5;
pub const HIDE_AFTER_MISSING_FRAMES: u32 = 2;
pub const DISPOSE_AFTER_MISSING_FRAMES: u32 = 30;
pub const MAX_PENDING_COMMANDS: usize = 256;
pub const MAX_FRAMES_IN_FLIGHT: u64 = 12;
#[derive(Clone, Debug, PartialEq)]
pub enum ViewCommand {
Create {
slot_id: u64,
view_type: String,
params_json: String,
interactive: bool,
},
Update {
slot_id: u64,
rect: Rect,
clip: Option<Rect>,
visible: bool,
shields: Vec<Rect>,
},
UpdateParams {
slot_id: u64,
params_json: String,
},
Dispose {
slot_id: u64,
},
}
#[derive(Clone, Debug)]
struct SlotEntry {
view_type: String,
params_json: String,
params_generation: u64,
interactive: bool,
last_rect: Rect,
last_clip: Option<Rect>,
last_visible: bool,
last_shields: Vec<Rect>,
missing_streak: u32,
}
#[derive(Debug, Default)]
pub struct PlatformViewState {
live: BTreeMap<u64, SlotEntry>,
pending: Vec<ViewCommand>,
pending_gens: Vec<u64>,
generation: u64,
acked_generation: u64,
overflow_logged: bool,
}
impl PlatformViewState {
pub fn new() -> Self {
Self::default()
}
pub fn ingest(&mut self, frames: &[PlatformViewFrame], input_shields: &[Rect]) -> bool {
let mut batch = Vec::new();
let mut seen = std::collections::BTreeSet::new();
for frame in frames {
seen.insert(frame.slot_id);
let shields = resolve_shields(frame, input_shields);
if let Some(entry) = self.live.get(&frame.slot_id)
&& entry.view_type != frame.view_type
{
batch.push(ViewCommand::Dispose {
slot_id: frame.slot_id,
});
self.live.remove(&frame.slot_id);
}
match self.live.get_mut(&frame.slot_id) {
None => {
batch.push(ViewCommand::Create {
slot_id: frame.slot_id,
view_type: frame.view_type.clone(),
params_json: frame.params_json.clone(),
interactive: frame.interactive,
});
batch.push(ViewCommand::Update {
slot_id: frame.slot_id,
rect: frame.rect,
clip: frame.clip,
visible: frame.visible,
shields: shields.clone(),
});
self.live.insert(
frame.slot_id,
SlotEntry {
view_type: frame.view_type.clone(),
params_json: frame.params_json.clone(),
params_generation: frame.params_generation,
interactive: frame.interactive,
last_rect: frame.rect,
last_clip: frame.clip,
last_visible: frame.visible,
last_shields: shields,
missing_streak: 0,
},
);
}
Some(entry) => {
entry.missing_streak = 0;
if entry.last_visible != frame.visible
|| rect_changed(entry.last_rect, frame.rect)
|| clip_changed(entry.last_clip, frame.clip)
|| shields_changed(&entry.last_shields, &shields)
{
batch.push(ViewCommand::Update {
slot_id: frame.slot_id,
rect: frame.rect,
clip: frame.clip,
visible: frame.visible,
shields: shields.clone(),
});
entry.last_rect = frame.rect;
entry.last_clip = frame.clip;
entry.last_visible = frame.visible;
entry.last_shields = shields;
}
if entry.params_generation != frame.params_generation {
batch.push(ViewCommand::UpdateParams {
slot_id: frame.slot_id,
params_json: frame.params_json.clone(),
});
entry.params_json = frame.params_json.clone();
entry.params_generation = frame.params_generation;
}
}
}
}
let mut disposed = Vec::new();
for (&slot_id, entry) in self.live.iter_mut() {
if seen.contains(&slot_id) {
continue;
}
entry.missing_streak += 1;
if entry.missing_streak == HIDE_AFTER_MISSING_FRAMES && entry.last_visible {
batch.push(ViewCommand::Update {
slot_id,
rect: entry.last_rect,
clip: entry.last_clip,
visible: false,
shields: entry.last_shields.clone(),
});
entry.last_visible = false;
}
if entry.missing_streak >= DISPOSE_AFTER_MISSING_FRAMES {
batch.push(ViewCommand::Dispose { slot_id });
disposed.push(slot_id);
}
}
for slot_id in disposed {
self.live.remove(&slot_id);
}
self.push_batch(batch)
}
pub fn suspend_all(&mut self) -> bool {
let mut batch = Vec::new();
for (&slot_id, entry) in self.live.iter_mut() {
if entry.last_visible {
batch.push(ViewCommand::Update {
slot_id,
rect: entry.last_rect,
clip: entry.last_clip,
visible: false,
shields: entry.last_shields.clone(),
});
entry.last_visible = false;
}
}
self.push_batch(batch)
}
pub fn retire(&mut self, slot_id: u64) -> bool {
if self.live.remove(&slot_id).is_some() {
self.push_batch(vec![ViewCommand::Dispose { slot_id }])
} else {
false
}
}
pub fn reset_for_surface_recreate(&mut self) -> bool {
let mut batch = Vec::new();
self.replay_live_into(&mut batch);
self.push_batch(batch)
}
fn replay_live_into(&self, batch: &mut Vec<ViewCommand>) {
for (&slot_id, entry) in self.live.iter() {
batch.push(ViewCommand::Create {
slot_id,
view_type: entry.view_type.clone(),
params_json: entry.params_json.clone(),
interactive: entry.interactive,
});
batch.push(ViewCommand::Update {
slot_id,
rect: entry.last_rect,
clip: entry.last_clip,
visible: entry.last_visible,
shields: entry.last_shields.clone(),
});
}
}
pub fn commands(&self) -> (u64, &[ViewCommand]) {
(self.generation, &self.pending)
}
pub fn commands_up_to(&self, max_generation: u64) -> (u64, &[ViewCommand]) {
let end = self.pending_gens.partition_point(|&g| g <= max_generation);
let reported = if end == 0 {
self.acked_generation
} else {
self.pending_gens[end - 1]
};
(reported, &self.pending[..end])
}
pub fn acknowledge(&mut self, generation: u64) {
if generation <= self.acked_generation {
return;
}
self.acked_generation = generation;
let keep_from = self
.pending_gens
.partition_point(|&g| g <= self.acked_generation);
self.pending.drain(0..keep_from);
self.pending_gens.drain(0..keep_from);
}
fn push_batch(&mut self, batch: Vec<ViewCommand>) -> bool {
if batch.is_empty() {
return false;
}
self.generation += 1;
let batch_generation = self.generation;
self.pending_gens
.extend(std::iter::repeat_n(batch_generation, batch.len()));
self.pending.extend(batch);
if self.pending.len() > MAX_PENDING_COMMANDS {
self.compact_to_net_effect();
}
true
}
fn compact_to_net_effect(&mut self) {
let disposed: BTreeSet<u64> = self
.pending
.iter()
.filter_map(|cmd| match cmd {
ViewCommand::Dispose { slot_id } => Some(*slot_id),
_ => None,
})
.collect();
let mut compacted = Vec::with_capacity(disposed.len() + self.live.len() * 2);
for slot_id in disposed {
compacted.push(ViewCommand::Dispose { slot_id });
}
self.replay_live_into(&mut compacted);
if !self.overflow_logged {
self.overflow_logged = true;
log::warn!(
"frust-shell: platform-view command backlog exceeded {MAX_PENDING_COMMANDS} \
un-acknowledged entries (is the native side polling?); compacted {} entries \
into a {}-command replay. Logged once per process.",
self.pending.len(),
compacted.len()
);
}
self.pending_gens.clear();
self.pending_gens.resize(compacted.len(), self.generation);
self.pending = compacted;
}
}
#[derive(Debug, Default)]
pub struct FramePairing {
due: VecDeque<(u64, u64)>,
idle_ticks: u64,
}
const MAX_TRACKED_BATCHES: usize = 256;
impl FramePairing {
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, generation: u64, frame_id: u64) {
if self.due.len() >= MAX_TRACKED_BATCHES {
self.due.pop_front();
}
self.due.push_back((generation, frame_id));
self.idle_ticks = 0;
}
pub fn note_idle_tick(&mut self) {
self.idle_ticks = self.idle_ticks.saturating_add(1);
}
pub fn releasable_generation(&self, presented_frame_id: u64, submitted_frame_id: u64) -> u64 {
let stale_cursor = submitted_frame_id.saturating_add(self.idle_ticks);
for &(generation, due_frame) in &self.due {
let on_screen = due_frame <= presented_frame_id;
let stranded = stale_cursor.saturating_sub(due_frame) >= MAX_FRAMES_IN_FLIGHT;
if !on_screen && !stranded {
return generation.saturating_sub(1);
}
}
u64::MAX
}
pub fn acknowledge(&mut self, generation: u64) {
while let Some(&(g, _)) = self.due.front() {
if g <= generation {
self.due.pop_front();
} else {
break;
}
}
}
pub fn clear(&mut self) {
self.due.clear();
self.idle_ticks = 0;
}
pub fn is_empty(&self) -> bool {
self.due.is_empty()
}
}
fn resolve_shields(frame: &PlatformViewFrame, input_shields: &[Rect]) -> Vec<Rect> {
if !frame.interactive {
return Vec::new();
}
let mut resolved = frame.shields.clone();
for shield in input_shields {
if shield.overlaps(frame.rect) && !resolved.iter().any(|kept| !rect_changed(*kept, *shield))
{
resolved.push(*shield);
}
}
resolved
}
fn shields_changed(a: &[Rect], b: &[Rect]) -> bool {
a.len() != b.len() || a.iter().zip(b).any(|(a, b)| rect_changed(*a, *b))
}
fn rect_changed(a: Rect, b: Rect) -> bool {
(a.x0 - b.x0).abs() >= EPSILON_PX
|| (a.y0 - b.y0).abs() >= EPSILON_PX
|| (a.x1 - b.x1).abs() >= EPSILON_PX
|| (a.y1 - b.y1).abs() >= EPSILON_PX
}
fn clip_changed(a: Option<Rect>, b: Option<Rect>) -> bool {
match (a, b) {
(None, None) => false,
(Some(a), Some(b)) => rect_changed(a, b),
_ => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn frame(slot_id: u64, rect: Rect, visible: bool) -> PlatformViewFrame {
PlatformViewFrame {
slot_id,
view_type: "dev.frust.Test".to_string(),
params_json: String::new(),
params_generation: 0,
rect,
clip: None,
visible,
interactive: false,
shields: Vec::new(),
}
}
fn r(x0: f64, y0: f64, x1: f64, y1: f64) -> Rect {
Rect::new(x0, y0, x1, y1)
}
fn interactive_frame(slot_id: u64, rect: Rect) -> PlatformViewFrame {
PlatformViewFrame {
interactive: true,
..frame(slot_id, rect, true)
}
}
fn last_update_shields(state: &PlatformViewState) -> Vec<Rect> {
state
.commands()
.1
.iter()
.rev()
.find_map(|cmd| match cmd {
ViewCommand::Update { shields, .. } => Some(shields.clone()),
_ => None,
})
.expect("an Update command in the backlog")
}
#[test]
fn new_slot_emits_create_then_update_in_order() {
let mut state = PlatformViewState::new();
let changed = state.ingest(&[frame(1, r(0.0, 0.0, 100.0, 50.0), true)], &[]);
assert!(changed);
let (generation, cmds) = state.commands();
assert_eq!(generation, 1);
assert_eq!(
cmds,
&[
ViewCommand::Create {
slot_id: 1,
view_type: "dev.frust.Test".to_string(),
params_json: String::new(),
interactive: false,
},
ViewCommand::Update {
slot_id: 1,
rect: r(0.0, 0.0, 100.0, 50.0),
clip: None,
visible: true,
shields: Vec::new(),
},
]
);
}
#[test]
fn sub_epsilon_rect_change_emits_nothing() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 100.0, 50.0), true)], &[]);
state.acknowledge(1);
let changed = state.ingest(&[frame(1, r(0.2, 0.2, 100.2, 50.2), true)], &[]);
assert!(!changed);
let (generation, cmds) = state.commands();
assert_eq!(generation, 1); assert!(cmds.is_empty());
}
#[test]
fn past_epsilon_rect_change_emits_update() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 100.0, 50.0), true)], &[]);
state.acknowledge(1);
let changed = state.ingest(&[frame(1, r(1.0, 0.0, 100.0, 50.0), true)], &[]);
assert!(changed);
let (generation, cmds) = state.commands();
assert_eq!(generation, 2);
assert_eq!(
cmds,
&[ViewCommand::Update {
slot_id: 1,
rect: r(1.0, 0.0, 100.0, 50.0),
clip: None,
visible: true,
shields: Vec::new(),
}]
);
}
#[test]
fn missing_two_consecutive_ingests_hides_a_visible_slot() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
state.acknowledge(1);
let changed = state.ingest(&[], &[]);
assert!(!changed);
assert_eq!(state.commands().1, &[]);
let changed = state.ingest(&[], &[]);
assert!(changed);
assert_eq!(
state.commands().1,
&[ViewCommand::Update {
slot_id: 1,
rect: r(0.0, 0.0, 10.0, 10.0),
clip: None,
visible: false,
shields: Vec::new(),
}]
);
state.acknowledge(state.commands().0);
let changed = state.ingest(&[], &[]);
assert!(!changed);
}
#[test]
fn missing_past_dispose_threshold_disposes_the_slot() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
state.acknowledge(1);
for _ in 0..(DISPOSE_AFTER_MISSING_FRAMES - 1) {
state.ingest(&[], &[]);
}
let (_, cmds) = state.commands();
assert!(!cmds.contains(&ViewCommand::Dispose { slot_id: 1 }));
let changed = state.ingest(&[], &[]);
assert!(changed);
let (_, cmds) = state.commands();
assert!(cmds.contains(&ViewCommand::Dispose { slot_id: 1 }));
}
#[test]
fn revive_after_hide_is_a_plain_update_not_a_create() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
state.ingest(&[], &[]); state.ingest(&[], &[]); state.acknowledge(state.commands().0);
let changed = state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
assert!(changed);
let (_, cmds) = state.commands();
assert_eq!(
cmds,
&[ViewCommand::Update {
slot_id: 1,
rect: r(0.0, 0.0, 10.0, 10.0),
clip: None,
visible: true,
shields: Vec::new(),
}]
);
assert!(!cmds.iter().any(|c| matches!(c, ViewCommand::Create { .. })));
}
#[test]
fn revive_after_dispose_creates_fresh() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
for _ in 0..DISPOSE_AFTER_MISSING_FRAMES {
state.ingest(&[], &[]);
}
state.acknowledge(state.commands().0);
let changed = state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
assert!(changed);
let (_, cmds) = state.commands();
assert_eq!(
cmds,
&[
ViewCommand::Create {
slot_id: 1,
view_type: "dev.frust.Test".to_string(),
params_json: String::new(),
interactive: false,
},
ViewCommand::Update {
slot_id: 1,
rect: r(0.0, 0.0, 10.0, 10.0),
clip: None,
visible: true,
shields: Vec::new(),
},
]
);
}
#[test]
fn acknowledge_compacts_the_backlog() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
let (gen1, _) = state.commands();
state.ingest(&[frame(2, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
let (gen2, cmds) = state.commands();
assert_eq!(cmds.len(), 4);
state.acknowledge(gen1);
let (generation_after_ack, cmds_after_ack) = state.commands();
assert_eq!(generation_after_ack, gen2); assert_eq!(cmds_after_ack.len(), 2);
state.acknowledge(gen1);
assert_eq!(state.commands().1.len(), 2);
}
#[test]
fn replay_after_reset_reproduces_every_live_slot() {
let mut state = PlatformViewState::new();
state.ingest(
&[
frame(1, r(0.0, 0.0, 10.0, 10.0), true),
frame(2, r(20.0, 0.0, 30.0, 10.0), false),
],
&[],
);
state.acknowledge(state.commands().0);
assert_eq!(state.commands().1, &[]);
let changed = state.reset_for_surface_recreate();
assert!(changed);
let (_, cmds) = state.commands();
assert_eq!(
cmds,
&[
ViewCommand::Create {
slot_id: 1,
view_type: "dev.frust.Test".to_string(),
params_json: String::new(),
interactive: false,
},
ViewCommand::Update {
slot_id: 1,
rect: r(0.0, 0.0, 10.0, 10.0),
clip: None,
visible: true,
shields: Vec::new(),
},
ViewCommand::Create {
slot_id: 2,
view_type: "dev.frust.Test".to_string(),
params_json: String::new(),
interactive: false,
},
ViewCommand::Update {
slot_id: 2,
rect: r(20.0, 0.0, 30.0, 10.0),
clip: None,
visible: false,
shields: Vec::new(),
},
]
);
}
#[test]
fn two_slot_interleaving_does_not_cross_contaminate() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
state.acknowledge(state.commands().0);
let changed = state.ingest(
&[
frame(1, r(0.0, 0.0, 10.0, 10.0), true),
frame(2, r(50.0, 50.0, 60.0, 60.0), true),
],
&[],
);
assert!(changed);
let (_, cmds) = state.commands();
assert_eq!(
cmds,
&[
ViewCommand::Create {
slot_id: 2,
view_type: "dev.frust.Test".to_string(),
params_json: String::new(),
interactive: false,
},
ViewCommand::Update {
slot_id: 2,
rect: r(50.0, 50.0, 60.0, 60.0),
clip: None,
visible: true,
shields: Vec::new(),
},
]
);
state.acknowledge(state.commands().0);
let changed = state.ingest(&[frame(1, r(5.0, 0.0, 15.0, 10.0), true)], &[]);
assert!(changed);
assert_eq!(
state.commands().1,
&[ViewCommand::Update {
slot_id: 1,
rect: r(5.0, 0.0, 15.0, 10.0),
clip: None,
visible: true,
shields: Vec::new(),
}]
);
}
#[test]
fn params_generation_change_emits_update_params() {
let mut state = PlatformViewState::new();
let mut f = frame(1, r(0.0, 0.0, 10.0, 10.0), true);
f.params_json = "{\"a\":1}".to_string();
state.ingest(&[f], &[]);
state.acknowledge(state.commands().0);
let mut f2 = frame(1, r(0.0, 0.0, 10.0, 10.0), true);
f2.params_json = "{\"a\":2}".to_string();
f2.params_generation = 1;
let changed = state.ingest(&[f2], &[]);
assert!(changed);
assert_eq!(
state.commands().1,
&[ViewCommand::UpdateParams {
slot_id: 1,
params_json: "{\"a\":2}".to_string(),
}]
);
}
#[test]
fn suspend_all_hides_every_visible_slot_immediately() {
let mut state = PlatformViewState::new();
state.ingest(
&[
frame(1, r(0.0, 0.0, 10.0, 10.0), true),
frame(2, r(20.0, 0.0, 30.0, 10.0), true),
],
&[],
);
state.acknowledge(state.commands().0);
let changed = state.suspend_all();
assert!(changed);
let (_, cmds) = state.commands();
assert_eq!(
cmds,
&[
ViewCommand::Update {
slot_id: 1,
rect: r(0.0, 0.0, 10.0, 10.0),
clip: None,
visible: false,
shields: Vec::new(),
},
ViewCommand::Update {
slot_id: 2,
rect: r(20.0, 0.0, 30.0, 10.0),
clip: None,
visible: false,
shields: Vec::new(),
},
]
);
}
#[test]
fn suspend_all_skips_already_hidden_slots() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), false)], &[]);
state.acknowledge(state.commands().0);
let changed = state.suspend_all();
assert!(!changed);
assert_eq!(state.commands().1, &[]);
}
#[test]
fn suspend_all_on_no_live_slots_is_a_noop() {
let mut state = PlatformViewState::new();
assert!(!state.suspend_all());
assert_eq!(state.commands().0, 0);
}
#[test]
fn revive_after_suspend_all_is_a_plain_update() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
state.acknowledge(state.commands().0);
state.suspend_all();
state.acknowledge(state.commands().0);
let changed = state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
assert!(changed);
let (_, cmds) = state.commands();
assert_eq!(
cmds,
&[ViewCommand::Update {
slot_id: 1,
rect: r(0.0, 0.0, 10.0, 10.0),
clip: None,
visible: true,
shields: Vec::new(),
}]
);
assert!(!cmds.iter().any(|c| matches!(c, ViewCommand::Create { .. })));
}
#[test]
fn retire_disposes_immediately_regardless_of_missing_streak() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
state.acknowledge(state.commands().0);
assert!(state.retire(1));
assert_eq!(state.commands().1, &[ViewCommand::Dispose { slot_id: 1 }]);
assert!(!state.retire(1));
assert!(!state.retire(999));
}
#[test]
fn view_type_swap_disposes_and_recreates_in_the_same_ingest() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
state.acknowledge(state.commands().0);
let mut swapped = frame(1, r(0.0, 0.0, 10.0, 10.0), true);
swapped.view_type = "dev.frust.Other".to_string();
let changed = state.ingest(&[swapped], &[]);
assert!(changed);
assert_eq!(
state.commands().1,
&[
ViewCommand::Dispose { slot_id: 1 },
ViewCommand::Create {
slot_id: 1,
view_type: "dev.frust.Other".to_string(),
params_json: String::new(),
interactive: false,
},
ViewCommand::Update {
slot_id: 1,
rect: r(0.0, 0.0, 10.0, 10.0),
clip: None,
visible: true,
shields: Vec::new(),
},
]
);
}
#[test]
fn view_type_swap_does_not_disturb_other_slots() {
let mut state = PlatformViewState::new();
state.ingest(
&[
frame(1, r(0.0, 0.0, 10.0, 10.0), true),
frame(2, r(20.0, 0.0, 30.0, 10.0), true),
],
&[],
);
state.acknowledge(state.commands().0);
let mut swapped = frame(2, r(20.0, 0.0, 30.0, 10.0), true);
swapped.view_type = "dev.frust.Other".to_string();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true), swapped], &[]);
let (_, cmds) = state.commands();
assert!(cmds.iter().all(|c| match c {
ViewCommand::Create { slot_id, .. }
| ViewCommand::Update { slot_id, .. }
| ViewCommand::UpdateParams { slot_id, .. }
| ViewCommand::Dispose { slot_id } => *slot_id == 2,
}));
}
#[test]
fn unchanged_view_type_never_disposes() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
state.acknowledge(state.commands().0);
state.ingest(&[frame(1, r(5.0, 0.0, 15.0, 10.0), true)], &[]);
assert!(
!state
.commands()
.1
.iter()
.any(|c| matches!(c, ViewCommand::Dispose { .. }))
);
}
fn ingest_until_compaction(
state: &mut PlatformViewState,
mut frames_at: impl FnMut(f64) -> Vec<PlatformViewFrame>,
) -> f64 {
let mut x = 0.0;
loop {
x += 1.0;
assert!(x < 10_000.0, "backlog cap never tripped");
let before = state.commands().1.len();
state.ingest(&frames_at(x), &[]);
if state.commands().1.len() < before {
return x;
}
}
}
#[test]
fn backlog_cap_compacts_into_a_live_slot_replay() {
let mut state = PlatformViewState::new();
let x = ingest_until_compaction(&mut state, |x| {
vec![
frame(1, r(x, 0.0, x + 10.0, 10.0), true),
frame(2, r(x + 20.0, 0.0, x + 30.0, 10.0), true),
]
});
let (generation, cmds) = state.commands();
assert!(cmds.len() <= MAX_PENDING_COMMANDS);
assert_eq!(
cmds,
&[
ViewCommand::Create {
slot_id: 1,
view_type: "dev.frust.Test".to_string(),
params_json: String::new(),
interactive: false,
},
ViewCommand::Update {
slot_id: 1,
rect: r(x, 0.0, x + 10.0, 10.0),
clip: None,
visible: true,
shields: Vec::new(),
},
ViewCommand::Create {
slot_id: 2,
view_type: "dev.frust.Test".to_string(),
params_json: String::new(),
interactive: false,
},
ViewCommand::Update {
slot_id: 2,
rect: r(x + 20.0, 0.0, x + 30.0, 10.0),
clip: None,
visible: true,
shields: Vec::new(),
},
]
);
state.acknowledge(generation - 1);
assert_eq!(state.commands().1.len(), 4);
state.acknowledge(generation);
assert!(state.commands().1.is_empty());
}
#[test]
fn backlog_cap_keeps_a_dropped_slots_dispose() {
let mut state = PlatformViewState::new();
state.ingest(
&[
frame(1, r(0.0, 0.0, 10.0, 10.0), true),
frame(2, r(20.0, 0.0, 30.0, 10.0), true),
],
&[],
);
for _ in 0..DISPOSE_AFTER_MISSING_FRAMES {
state.ingest(&[frame(2, r(20.0, 0.0, 30.0, 10.0), true)], &[]);
}
assert!(
state
.commands()
.1
.contains(&ViewCommand::Dispose { slot_id: 1 })
);
ingest_until_compaction(&mut state, |x| {
vec![frame(2, r(x + 20.0, 0.0, x + 30.0, 10.0), true)]
});
let (_, cmds) = state.commands();
assert_eq!(cmds[0], ViewCommand::Dispose { slot_id: 1 });
assert!(
cmds.iter()
.any(|c| matches!(c, ViewCommand::Create { slot_id: 2, .. }))
);
assert!(
!cmds
.iter()
.any(|c| matches!(c, ViewCommand::Create { slot_id: 1, .. }))
);
}
#[test]
fn a_steadily_acking_native_side_never_trips_the_cap() {
let mut state = PlatformViewState::new();
for i in 0..1_000 {
let x = i as f64;
state.ingest(&[frame(1, r(x, 0.0, x + 10.0, 10.0), true)], &[]);
assert!(state.commands().1.len() <= MAX_PENDING_COMMANDS);
state.acknowledge(state.commands().0);
}
assert!(state.commands().1.is_empty());
}
#[test]
fn commands_up_to_releases_only_the_due_prefix() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
let gen1 = state.commands().0;
state.ingest(&[frame(1, r(5.0, 0.0, 15.0, 10.0), true)], &[]);
let gen2 = state.commands().0;
assert_eq!(state.commands().1.len(), 3);
assert_eq!(state.commands_up_to(0), (0, &[][..]));
let (reported, cmds) = state.commands_up_to(gen1);
assert_eq!(reported, gen1);
assert_eq!(cmds.len(), 2);
let (reported, cmds) = state.commands_up_to(gen2);
assert_eq!(reported, gen2);
assert_eq!(cmds.len(), 3);
}
#[test]
fn commands_up_to_reports_the_acked_generation_when_it_releases_nothing() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
state.acknowledge(state.commands().0);
state.ingest(&[frame(1, r(9.0, 0.0, 19.0, 10.0), true)], &[]);
let (reported, cmds) = state.commands_up_to(1);
assert!(cmds.is_empty());
assert_eq!(reported, 1, "a held poll must not ack anything new");
}
#[test]
fn gate_releases_a_batch_once_its_own_frame_is_presented() {
let mut pairing = FramePairing::new();
pairing.record(1, 10);
pairing.record(2, 11);
assert_eq!(pairing.releasable_generation(9, 11), 0);
assert_eq!(pairing.releasable_generation(10, 11), 1);
assert_eq!(pairing.releasable_generation(11, 11), u64::MAX);
}
#[test]
fn gate_releases_everything_before_the_first_held_batch() {
let mut pairing = FramePairing::new();
pairing.record(2, 10);
assert_eq!(pairing.releasable_generation(9, 10), 1);
}
#[test]
fn gate_releases_everything_when_nothing_is_paired() {
let pairing = FramePairing::new();
assert!(pairing.is_empty());
assert_eq!(pairing.releasable_generation(0, 0), u64::MAX);
}
#[test]
fn escape_hatch_releases_a_batch_whose_frame_was_dropped() {
let mut pairing = FramePairing::new();
pairing.record(1, 5); pairing.record(2, 6);
let submitted = 5 + MAX_FRAMES_IN_FLIGHT - 1;
assert_eq!(pairing.releasable_generation(4, submitted), 0);
let submitted = 5 + MAX_FRAMES_IN_FLIGHT;
assert_eq!(pairing.releasable_generation(4, submitted), 1);
let submitted = 6 + MAX_FRAMES_IN_FLIGHT;
assert_eq!(pairing.releasable_generation(4, submitted), u64::MAX);
}
#[test]
fn gate_acknowledge_drops_applied_pairings() {
let mut pairing = FramePairing::new();
pairing.record(1, 10);
pairing.record(2, 11);
pairing.acknowledge(1);
assert_eq!(pairing.releasable_generation(10, 11), 1);
pairing.acknowledge(2);
assert!(pairing.is_empty());
assert_eq!(pairing.releasable_generation(0, 0), u64::MAX);
}
#[test]
fn gate_clear_releases_a_suspend_or_replay_batch() {
let mut pairing = FramePairing::new();
pairing.record(1, 10); assert_eq!(pairing.releasable_generation(9, 10), 0);
pairing.clear();
assert_eq!(pairing.releasable_generation(9, 10), u64::MAX);
}
#[test]
fn idle_ticks_release_a_batch_whose_frame_never_presents() {
let mut pairing = FramePairing::new();
pairing.record(1, 10);
assert_eq!(
pairing.releasable_generation(9, 10),
0,
"held while that frame could still land"
);
for _ in 0..(MAX_FRAMES_IN_FLIGHT - 1) {
pairing.note_idle_tick();
assert_eq!(
pairing.releasable_generation(9, 10),
0,
"still inside the staleness bound"
);
}
pairing.note_idle_tick();
assert_eq!(
pairing.releasable_generation(9, 10),
u64::MAX,
"the idle stretch strands a frame that will never present"
);
}
#[test]
fn an_idle_released_batch_is_served_exactly_once() {
let mut state = PlatformViewState::new();
let mut pairing = FramePairing::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
let generation = state.commands().0;
pairing.record(generation, 10);
let releasable = pairing.releasable_generation(9, 10);
assert!(state.commands_up_to(releasable).1.is_empty());
for _ in 0..MAX_FRAMES_IN_FLIGHT {
pairing.note_idle_tick();
}
let releasable = pairing.releasable_generation(9, 10);
let (reported, cmds) = state.commands_up_to(releasable);
assert_eq!(cmds.len(), 2, "the held Create+Update finally go out");
assert_eq!(reported, generation);
state.acknowledge(reported);
pairing.acknowledge(reported);
pairing.note_idle_tick();
let releasable = pairing.releasable_generation(9, 10);
assert!(
state.commands_up_to(releasable).1.is_empty(),
"applied once, never re-applied"
);
assert!(pairing.is_empty());
}
#[test]
fn a_presented_frame_releases_before_the_idle_bound_is_reached() {
let mut pairing = FramePairing::new();
pairing.record(1, 10);
pairing.note_idle_tick();
pairing.note_idle_tick();
assert_eq!(
pairing.releasable_generation(9, 10),
0,
"held: the frame is still well inside the bound"
);
assert_eq!(
pairing.releasable_generation(10, 10),
u64::MAX,
"the present releases it exactly as before"
);
}
#[test]
fn a_produced_batch_resets_the_idle_stretch() {
let mut pairing = FramePairing::new();
pairing.record(1, 10);
for _ in 0..(MAX_FRAMES_IN_FLIGHT - 1) {
pairing.note_idle_tick();
}
pairing.record(2, 11);
assert_eq!(
pairing.releasable_generation(9, 11),
0,
"a spent idle stretch cannot strand a live pipeline"
);
for _ in 0..MAX_FRAMES_IN_FLIGHT {
pairing.note_idle_tick();
}
assert_eq!(pairing.releasable_generation(9, 11), u64::MAX);
}
#[test]
fn clear_drops_the_idle_stretch_with_the_pairings() {
let mut pairing = FramePairing::new();
pairing.record(1, 10);
for _ in 0..MAX_FRAMES_IN_FLIGHT {
pairing.note_idle_tick();
}
pairing.clear();
pairing.record(2, 1);
assert_eq!(pairing.releasable_generation(0, 1), 1);
}
#[test]
fn gate_tracking_is_bounded_by_an_unacking_native_side() {
let mut pairing = FramePairing::new();
for i in 1..=(MAX_TRACKED_BATCHES as u64 * 2) {
pairing.record(i, i);
}
let oldest_tracked = MAX_TRACKED_BATCHES as u64 + 1;
assert_eq!(
pairing.releasable_generation(0, oldest_tracked),
oldest_tracked - 1
);
}
#[test]
fn an_interactive_slot_carries_only_the_intersecting_shields() {
let mut state = PlatformViewState::new();
let slot = r(0.0, 0.0, 100.0, 100.0);
let over = r(10.0, 10.0, 40.0, 40.0); let elsewhere = r(500.0, 500.0, 540.0, 540.0);
state.ingest(&[interactive_frame(1, slot)], &[over, elsewhere]);
assert_eq!(
last_update_shields(&state),
vec![over],
"only the shield overlapping the slot rides its Update"
);
}
#[test]
fn a_non_interactive_slot_carries_no_shields_at_all() {
let mut state = PlatformViewState::new();
let slot = r(0.0, 0.0, 100.0, 100.0);
state.ingest(&[frame(1, slot, true)], &[r(10.0, 10.0, 40.0, 40.0)]);
assert!(
last_update_shields(&state).is_empty(),
"shields are meaningless to a host that isn't forwarding touches"
);
}
#[test]
fn a_moving_shield_emits_an_update_and_a_jittering_one_does_not() {
let mut state = PlatformViewState::new();
let slot = r(0.0, 0.0, 100.0, 100.0);
state.ingest(&[interactive_frame(1, slot)], &[r(10.0, 10.0, 40.0, 40.0)]);
state.acknowledge(state.commands().0);
let changed = state.ingest(&[interactive_frame(1, slot)], &[r(10.2, 10.2, 40.2, 40.2)]);
assert!(!changed, "sub-epsilon shield drift emits nothing");
let moved = r(10.0, 60.0, 40.0, 90.0);
let changed = state.ingest(&[interactive_frame(1, slot)], &[moved]);
assert!(changed);
assert_eq!(
state.commands().1,
&[ViewCommand::Update {
slot_id: 1,
rect: slot,
clip: None,
visible: true,
shields: vec![moved],
}]
);
}
#[test]
fn a_shield_leaving_the_pass_clears_it_from_the_slot() {
let mut state = PlatformViewState::new();
let slot = r(0.0, 0.0, 100.0, 100.0);
state.ingest(&[interactive_frame(1, slot)], &[r(10.0, 10.0, 40.0, 40.0)]);
state.acknowledge(state.commands().0);
let changed = state.ingest(&[interactive_frame(1, slot)], &[]);
assert!(changed);
assert!(last_update_shields(&state).is_empty());
}
#[test]
fn manual_shield_local_rects_are_kept_and_unioned_without_duplicates() {
let mut state = PlatformViewState::new();
let slot = r(0.0, 0.0, 100.0, 100.0);
let manual = r(0.0, 0.0, 20.0, 20.0);
let auto = r(50.0, 50.0, 70.0, 70.0);
let mut f = interactive_frame(1, slot);
f.shields = vec![manual];
state.ingest(&[f], &[manual, auto]);
assert_eq!(
last_update_shields(&state),
vec![manual, auto],
"manual rects first, then the auto-collected ones, deduped"
);
}
#[test]
fn a_replay_preserves_each_slots_resolved_shields() {
let mut state = PlatformViewState::new();
let slot = r(0.0, 0.0, 100.0, 100.0);
let over = r(10.0, 10.0, 40.0, 40.0);
state.ingest(&[interactive_frame(1, slot)], &[over]);
state.acknowledge(state.commands().0);
state.reset_for_surface_recreate();
assert_eq!(last_update_shields(&state), vec![over]);
}
#[test]
fn a_retired_slot_disposes_immediately_and_the_next_ingest_is_quiet() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
state.acknowledge(state.commands().0);
assert!(state.retire(1), "teardown disposes on the spot");
assert_eq!(state.commands().1, &[ViewCommand::Dispose { slot_id: 1 }]);
state.acknowledge(state.commands().0);
let changed = state.ingest(&[], &[]);
assert!(
!changed,
"the retired slot is already forgotten — no Hide, no second Dispose"
);
}
#[test]
fn a_merely_culled_slot_is_never_disposed_by_the_retire_path() {
let mut state = PlatformViewState::new();
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
state.acknowledge(state.commands().0);
for _ in 0..(DISPOSE_AFTER_MISSING_FRAMES - 1) {
state.ingest(&[], &[]);
}
let (_, cmds) = state.commands();
assert!(
cmds.iter()
.any(|c| matches!(c, ViewCommand::Update { visible: false, .. })),
"the culled slot is hidden"
);
assert!(
!cmds
.iter()
.any(|c| matches!(c, ViewCommand::Dispose { .. })),
"but never disposed without a real teardown signal"
);
state.acknowledge(state.commands().0);
state.ingest(&[frame(1, r(0.0, 0.0, 10.0, 10.0), true)], &[]);
assert!(
!state
.commands()
.1
.iter()
.any(|c| matches!(c, ViewCommand::Create { .. }))
);
}
#[test]
fn deterministic_replay_produces_an_identical_command_stream() {
let sequence: Vec<Vec<PlatformViewFrame>> = vec![
vec![frame(1, r(0.0, 0.0, 10.0, 10.0), true)],
vec![
frame(1, r(0.0, 0.0, 10.0, 10.0), true),
frame(2, r(20.0, 0.0, 30.0, 10.0), true),
],
vec![frame(2, r(20.0, 0.0, 30.0, 10.0), true)], vec![frame(2, r(20.0, 0.0, 30.0, 10.0), true)], ];
let run = |sequence: &[Vec<PlatformViewFrame>]| -> Vec<ViewCommand> {
let mut state = PlatformViewState::new();
let mut all = Vec::new();
for frames in sequence {
state.ingest(frames, &[]);
all.extend(state.commands().1.iter().cloned());
state.acknowledge(state.commands().0);
}
all
};
assert_eq!(run(&sequence), run(&sequence));
}
}