use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::ops::Range;
use frust_core::widget::PlatformViewFrame;
use frust_scene::Command;
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>,
z_index: u32,
},
UpdateParams {
slot_id: u64,
params_json: String,
},
Dispose {
slot_id: u64,
},
Overlay {
overlay_id: u64,
above_slot_id: u64,
rect: Rect,
command_range: Range<usize>,
},
DisposeOverlay {
overlay_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>,
last_z_index: u32,
missing_streak: u32,
}
#[derive(Clone, Debug)]
struct OverlayEntry {
overlay_id: u64,
last_rect: Rect,
last_range: Range<usize>,
missing_streak: u32,
}
#[derive(Debug, Default)]
pub struct PlatformViewState {
live: BTreeMap<u64, SlotEntry>,
overlays: BTreeMap<u64, OverlayEntry>,
last_overlay_id: u64,
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 {
self.ingest_pass(frames, input_shields, &[], Rect::ZERO, false)
}
pub fn ingest_scene(
&mut self,
frames: &[PlatformViewFrame],
input_shields: &[Rect],
commands: &[Command],
viewport: Rect,
) -> bool {
self.ingest_pass(frames, input_shields, commands, viewport, true)
}
fn ingest_pass(
&mut self,
frames: &[PlatformViewFrame],
input_shields: &[Rect],
commands: &[Command],
viewport: Rect,
overlay_aware: bool,
) -> bool {
let mut batch = Vec::new();
let mut seen = std::collections::BTreeSet::new();
let order = paint_order(frames);
let mut z_of = vec![0u32; frames.len()];
for (rank, &i) in order.iter().enumerate() {
z_of[i] = u32::try_from(rank).unwrap_or(u32::MAX);
}
for (i, frame) in frames.iter().enumerate() {
let z_index = z_of[i];
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
{
dispose_overlay_above(&mut self.overlays, frame.slot_id, &mut batch);
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(),
z_index,
});
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,
last_z_index: z_index,
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)
|| (overlay_aware && entry.last_z_index != z_index)
{
batch.push(ViewCommand::Update {
slot_id: frame.slot_id,
rect: frame.rect,
clip: frame.clip,
visible: frame.visible,
shields: shields.clone(),
z_index,
});
entry.last_rect = frame.rect;
entry.last_clip = frame.clip;
entry.last_visible = frame.visible;
entry.last_shields = shields;
}
entry.last_z_index = z_index;
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(),
z_index: entry.last_z_index,
});
entry.last_visible = false;
}
if entry.missing_streak >= DISPOSE_AFTER_MISSING_FRAMES {
dispose_overlay_above(&mut self.overlays, slot_id, &mut batch);
batch.push(ViewCommand::Dispose { slot_id });
disposed.push(slot_id);
}
}
for slot_id in disposed {
self.live.remove(&slot_id);
}
self.ingest_overlays(frames, &order, commands, viewport, &mut batch);
self.push_batch(batch)
}
fn ingest_overlays(
&mut self,
frames: &[PlatformViewFrame],
order: &[usize],
commands: &[Command],
viewport: Rect,
batch: &mut Vec<ViewCommand>,
) {
let total = commands.len();
let mut cuts = order
.iter()
.map(|&i| &frames[i])
.filter(|frame| frame.visible)
.peekable();
let mut with_content = BTreeSet::new();
while let Some(slot) = cuts.next() {
let start = slot.command_index.min(total);
let end = cuts
.peek()
.map_or(total, |next| next.command_index.min(total))
.max(start);
let content_from = match commands.get(start) {
Some(Command::ClearRect { .. }) => (start + 1).min(end),
_ => start,
};
if !commands[content_from..end].iter().any(Command::is_drawing) {
continue;
}
let range = content_from..end;
with_content.insert(slot.slot_id);
match self.overlays.get_mut(&slot.slot_id) {
Some(entry) => {
entry.missing_streak = 0;
if entry.last_range != range || rect_changed(entry.last_rect, viewport) {
batch.push(ViewCommand::Overlay {
overlay_id: entry.overlay_id,
above_slot_id: slot.slot_id,
rect: viewport,
command_range: range.clone(),
});
entry.last_range = range;
entry.last_rect = viewport;
}
}
None => {
self.last_overlay_id += 1;
let overlay_id = self.last_overlay_id;
batch.push(ViewCommand::Overlay {
overlay_id,
above_slot_id: slot.slot_id,
rect: viewport,
command_range: range.clone(),
});
self.overlays.insert(
slot.slot_id,
OverlayEntry {
overlay_id,
last_rect: viewport,
last_range: range,
missing_streak: 0,
},
);
}
}
}
let mut disposed = Vec::new();
for (&slot_id, entry) in self.overlays.iter_mut() {
if with_content.contains(&slot_id) {
continue;
}
entry.missing_streak += 1;
if entry.missing_streak >= DISPOSE_AFTER_MISSING_FRAMES {
batch.push(ViewCommand::DisposeOverlay {
overlay_id: entry.overlay_id,
});
disposed.push(slot_id);
} else if !entry.last_range.is_empty() {
let empty = entry.last_range.start..entry.last_range.start;
batch.push(ViewCommand::Overlay {
overlay_id: entry.overlay_id,
above_slot_id: slot_id,
rect: entry.last_rect,
command_range: empty.clone(),
});
entry.last_range = empty;
}
}
for slot_id in disposed {
self.overlays.remove(&slot_id);
}
}
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(),
z_index: entry.last_z_index,
});
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() {
let mut batch = Vec::new();
dispose_overlay_above(&mut self.overlays, slot_id, &mut batch);
batch.push(ViewCommand::Dispose { slot_id });
self.push_batch(batch)
} 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(),
z_index: entry.last_z_index,
});
}
for (&above_slot_id, entry) in self.overlays.iter() {
batch.push(ViewCommand::Overlay {
overlay_id: entry.overlay_id,
above_slot_id,
rect: entry.last_rect,
command_range: entry.last_range.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 disposed_overlays: BTreeSet<u64> = self
.pending
.iter()
.filter_map(|cmd| match cmd {
ViewCommand::DisposeOverlay { overlay_id } => Some(*overlay_id),
_ => None,
})
.collect();
let mut compacted = Vec::with_capacity(
disposed_overlays.len() + disposed.len() + self.live.len() * 2 + self.overlays.len(),
);
for overlay_id in disposed_overlays {
compacted.push(ViewCommand::DisposeOverlay { overlay_id });
}
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 paint_order(frames: &[PlatformViewFrame]) -> Vec<usize> {
let mut order: Vec<usize> = (0..frames.len()).collect();
order.sort_by_key(|&i| frames[i].command_index);
order
}
fn dispose_overlay_above(
overlays: &mut BTreeMap<u64, OverlayEntry>,
slot_id: u64,
batch: &mut Vec<ViewCommand>,
) {
if let Some(entry) = overlays.remove(&slot_id) {
batch.push(ViewCommand::DisposeOverlay {
overlay_id: entry.overlay_id,
});
}
}
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(),
command_index: 0,
}
}
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(),
z_index: 0,
},
]
);
}
#[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(),
z_index: 0,
}]
);
}
#[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(),
z_index: 0,
}]
);
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(),
z_index: 0,
}]
);
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(),
z_index: 0,
},
]
);
}
#[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(),
z_index: 0,
},
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(),
z_index: 1,
},
]
);
}
#[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(),
z_index: 1,
},
]
);
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(),
z_index: 0,
}]
);
}
#[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(),
z_index: 0,
},
ViewCommand::Update {
slot_id: 2,
rect: r(20.0, 0.0, 30.0, 10.0),
clip: None,
visible: false,
shields: Vec::new(),
z_index: 1,
},
]
);
}
#[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(),
z_index: 0,
}]
);
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(),
z_index: 0,
},
]
);
}
#[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,
ViewCommand::Overlay { .. } | ViewCommand::DisposeOverlay { .. } => false,
}));
}
#[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(),
z_index: 0,
},
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(),
z_index: 1,
},
]
);
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],
z_index: 0,
}]
);
}
#[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));
}
const VIEWPORT: Rect = Rect::new(0.0, 0.0, 400.0, 800.0);
fn draw() -> Command {
Command::BackdropBlur {
rect: r(0.0, 0.0, 10.0, 10.0),
radius: 4.0,
transform: kurbo::Affine::IDENTITY,
}
}
fn punch() -> Command {
Command::ClearRect {
rect: r(0.0, 0.0, 100.0, 100.0),
transform: kurbo::Affine::IDENTITY,
}
}
fn push_clip() -> Command {
Command::PushClip {
rect: r(0.0, 0.0, 10.0, 10.0),
transform: kurbo::Affine::IDENTITY,
}
}
fn frame_at(slot_id: u64, command_index: usize) -> PlatformViewFrame {
PlatformViewFrame {
command_index,
..frame(slot_id, r(0.0, 0.0, 100.0, 100.0), true)
}
}
fn overlay_commands(state: &PlatformViewState) -> Vec<ViewCommand> {
state
.commands()
.1
.iter()
.filter(|c| {
matches!(
c,
ViewCommand::Overlay { .. } | ViewCommand::DisposeOverlay { .. }
)
})
.cloned()
.collect()
}
fn update_z_indices(state: &PlatformViewState) -> Vec<(u64, u32)> {
state
.commands()
.1
.iter()
.filter_map(|c| match c {
ViewCommand::Update {
slot_id, z_index, ..
} => Some((*slot_id, *z_index)),
_ => None,
})
.collect()
}
#[test]
fn one_slot_with_drawing_content_after_it_gets_one_overlay() {
let mut state = PlatformViewState::new();
let commands = [draw(), push_clip(), draw(), draw(), Command::PopClip];
assert!(state.ingest_scene(&[frame_at(1, 1)], &[], &commands, VIEWPORT));
assert_eq!(
overlay_commands(&state),
vec![ViewCommand::Overlay {
overlay_id: 1,
above_slot_id: 1,
rect: VIEWPORT,
command_range: 1..5,
}]
);
assert_eq!(update_z_indices(&state), vec![(1, 0)]);
assert!(matches!(
state.commands().1.last(),
Some(ViewCommand::Overlay { .. })
));
}
#[test]
fn a_slots_own_punch_is_not_content_above_it() {
let mut state = PlatformViewState::new();
state.ingest_scene(&[frame_at(1, 1)], &[], &[draw(), punch()], VIEWPORT);
assert!(overlay_commands(&state).is_empty());
let mut state = PlatformViewState::new();
state.ingest_scene(
&[frame_at(1, 1)],
&[],
&[draw(), punch(), push_clip(), draw(), Command::PopClip],
VIEWPORT,
);
assert_eq!(
overlay_commands(&state),
vec![ViewCommand::Overlay {
overlay_id: 1,
above_slot_id: 1,
rect: VIEWPORT,
command_range: 2..5,
}]
);
}
#[test]
fn structural_only_content_after_a_slot_gets_no_overlay() {
let mut state = PlatformViewState::new();
let commands = [
draw(),
push_clip(),
Command::PopClip,
Command::PushLayer {
rect: r(0.0, 0.0, 10.0, 10.0),
alpha: 0.5,
transform: kurbo::Affine::IDENTITY,
},
Command::PopLayer,
];
state.ingest_scene(&[frame_at(1, 1)], &[], &commands, VIEWPORT);
assert!(overlay_commands(&state).is_empty());
let mut state = PlatformViewState::new();
state.ingest_scene(&[frame_at(1, 3)], &[], &[draw(), draw(), draw()], VIEWPORT);
assert!(overlay_commands(&state).is_empty());
}
#[test]
fn two_slots_get_two_overlays_with_ranges_and_z_order() {
let mut state = PlatformViewState::new();
let commands = [
draw(),
draw(),
draw(),
push_clip(),
draw(),
Command::PopClip,
];
state.ingest_scene(&[frame_at(2, 3), frame_at(1, 1)], &[], &commands, VIEWPORT);
assert_eq!(update_z_indices(&state), vec![(2, 1), (1, 0)]);
assert_eq!(
overlay_commands(&state),
vec![
ViewCommand::Overlay {
overlay_id: 1,
above_slot_id: 1,
rect: VIEWPORT,
command_range: 1..3,
},
ViewCommand::Overlay {
overlay_id: 2,
above_slot_id: 2,
rect: VIEWPORT,
command_range: 3..6,
},
]
);
}
#[test]
fn content_only_between_two_slots_overlays_the_lower_one() {
let mut state = PlatformViewState::new();
let commands = [draw(), push_clip(), Command::PopClip];
state.ingest_scene(&[frame_at(1, 0), frame_at(2, 1)], &[], &commands, VIEWPORT);
assert_eq!(
overlay_commands(&state),
vec![ViewCommand::Overlay {
overlay_id: 1,
above_slot_id: 1,
rect: VIEWPORT,
command_range: 0..1,
}]
);
}
#[test]
fn a_hidden_slot_is_not_a_cut() {
let mut state = PlatformViewState::new();
let mut hidden = frame_at(2, 1);
hidden.visible = false;
state.ingest_scene(&[frame_at(1, 0), hidden], &[], &[draw(), draw()], VIEWPORT);
assert_eq!(
overlay_commands(&state),
vec![ViewCommand::Overlay {
overlay_id: 1,
above_slot_id: 1,
rect: VIEWPORT,
command_range: 0..2,
}]
);
}
#[test]
fn an_unchanged_overlay_emits_nothing_and_a_moved_range_is_resent() {
let mut state = PlatformViewState::new();
let commands = [draw(), draw(), draw()];
state.ingest_scene(&[frame_at(1, 1)], &[], &commands, VIEWPORT);
state.acknowledge(state.commands().0);
assert!(!state.ingest_scene(&[frame_at(1, 1)], &[], &commands, VIEWPORT));
assert!(state.commands().1.is_empty());
let commands = [draw(), draw(), draw(), draw()];
assert!(state.ingest_scene(&[frame_at(1, 2)], &[], &commands, VIEWPORT));
assert_eq!(
overlay_commands(&state),
vec![ViewCommand::Overlay {
overlay_id: 1,
above_slot_id: 1,
rect: VIEWPORT,
command_range: 2..4,
}]
);
}
#[test]
fn overlay_is_disposed_after_its_content_is_gone_for_the_dispose_streak() {
let mut state = PlatformViewState::new();
state.ingest_scene(&[frame_at(1, 1)], &[], &[draw(), draw()], VIEWPORT);
state.acknowledge(state.commands().0);
state.ingest_scene(&[frame_at(1, 1)], &[], &[draw()], VIEWPORT);
assert_eq!(
overlay_commands(&state),
vec![ViewCommand::Overlay {
overlay_id: 1,
above_slot_id: 1,
rect: VIEWPORT,
command_range: 1..1,
}]
);
state.acknowledge(state.commands().0);
for _ in 1..(DISPOSE_AFTER_MISSING_FRAMES - 1) {
state.ingest_scene(&[frame_at(1, 1)], &[], &[draw()], VIEWPORT);
}
assert!(
overlay_commands(&state).is_empty(),
"nothing more until the dispose streak is reached"
);
assert!(state.ingest_scene(&[frame_at(1, 1)], &[], &[draw()], VIEWPORT));
assert_eq!(
overlay_commands(&state),
vec![ViewCommand::DisposeOverlay { overlay_id: 1 }]
);
assert!(
!state
.commands()
.1
.iter()
.any(|c| matches!(c, ViewCommand::Dispose { .. }))
);
state.acknowledge(state.commands().0);
state.ingest_scene(&[frame_at(1, 1)], &[], &[draw(), draw()], VIEWPORT);
assert_eq!(
overlay_commands(&state),
vec![ViewCommand::Overlay {
overlay_id: 2,
above_slot_id: 1,
rect: VIEWPORT,
command_range: 1..2,
}]
);
}
#[test]
fn content_returning_before_disposal_reuses_the_overlay() {
let mut state = PlatformViewState::new();
state.ingest_scene(&[frame_at(1, 1)], &[], &[draw(), draw()], VIEWPORT);
state.ingest_scene(&[frame_at(1, 1)], &[], &[draw()], VIEWPORT);
state.acknowledge(state.commands().0);
state.ingest_scene(&[frame_at(1, 1)], &[], &[draw(), draw()], VIEWPORT);
assert_eq!(
overlay_commands(&state),
vec![ViewCommand::Overlay {
overlay_id: 1,
above_slot_id: 1,
rect: VIEWPORT,
command_range: 1..2,
}]
);
}
#[test]
fn a_frame_without_platform_views_emits_no_overlays() {
let mut state = PlatformViewState::new();
assert!(!state.ingest_scene(&[], &[], &[draw(), draw()], VIEWPORT));
assert_eq!(state.commands(), (0, &[][..]));
}
#[test]
fn plain_ingest_rank_only_change_emits_nothing() {
let mut state = PlatformViewState::new();
state.ingest(&[frame_at(1, 0), frame_at(2, 1)], &[]);
state.acknowledge(state.commands().0);
assert!(!state.ingest(&[frame_at(1, 1), frame_at(2, 0)], &[]));
assert!(state.commands().1.is_empty());
}
#[test]
fn plain_ingest_culled_sibling_keeps_survivor_quiet() {
let mut state = PlatformViewState::new();
state.ingest(&[frame_at(1, 0), frame_at(2, 1), frame_at(3, 2)], &[]);
state.acknowledge(state.commands().0);
assert!(!state.ingest(&[frame_at(1, 0), frame_at(3, 2)], &[]));
assert!(update_z_indices(&state).is_empty());
}
#[test]
fn plain_ingest_update_for_another_reason_carries_the_current_rank() {
let mut state = PlatformViewState::new();
state.ingest(&[frame_at(1, 0), frame_at(2, 1)], &[]);
state.acknowledge(state.commands().0);
let moved = PlatformViewFrame {
rect: r(10.0, 0.0, 110.0, 100.0),
..frame_at(1, 1)
};
assert!(state.ingest(&[moved, frame_at(2, 0)], &[]));
assert_eq!(update_z_indices(&state), vec![(1, 1)]);
}
#[test]
fn overlay_aware_rank_change_is_announced_with_the_current_rank() {
let mut state = PlatformViewState::new();
state.ingest_scene(&[frame_at(1, 0), frame_at(2, 1)], &[], &[], VIEWPORT);
state.acknowledge(state.commands().0);
assert!(state.ingest_scene(&[frame_at(1, 1), frame_at(2, 0)], &[], &[], VIEWPORT));
assert_eq!(update_z_indices(&state), vec![(1, 1), (2, 0)]);
}
#[test]
fn plain_ingest_never_emits_overlays() {
let mut state = PlatformViewState::new();
state.ingest(&[frame_at(1, 1)], &[]);
assert!(overlay_commands(&state).is_empty());
assert_eq!(update_z_indices(&state), vec![(1, 0)]);
}
#[test]
fn a_z_order_change_alone_emits_an_update() {
let mut state = PlatformViewState::new();
state.ingest_scene(&[frame_at(1, 0), frame_at(2, 1)], &[], &[], VIEWPORT);
state.acknowledge(state.commands().0);
assert!(state.ingest_scene(&[frame_at(1, 1), frame_at(2, 0)], &[], &[], VIEWPORT));
assert_eq!(update_z_indices(&state), vec![(1, 1), (2, 0)]);
}
#[test]
fn retiring_a_slot_disposes_its_overlay_first() {
let mut state = PlatformViewState::new();
state.ingest_scene(&[frame_at(1, 1)], &[], &[draw(), draw()], VIEWPORT);
state.acknowledge(state.commands().0);
assert!(state.retire(1));
assert_eq!(
state.commands().1,
&[
ViewCommand::DisposeOverlay { overlay_id: 1 },
ViewCommand::Dispose { slot_id: 1 },
]
);
}
#[test]
fn a_slot_disposed_by_its_missing_streak_takes_its_overlay_with_it() {
let mut state = PlatformViewState::new();
state.ingest_scene(&[frame_at(1, 1)], &[], &[draw(), draw()], VIEWPORT);
for _ in 0..DISPOSE_AFTER_MISSING_FRAMES {
state.ingest_scene(&[], &[], &[draw()], VIEWPORT);
}
let cmds = state.commands().1;
let dispose_overlay = cmds
.iter()
.position(|c| *c == ViewCommand::DisposeOverlay { overlay_id: 1 })
.expect("the overlay is disposed");
let dispose_slot = cmds
.iter()
.position(|c| *c == ViewCommand::Dispose { slot_id: 1 })
.expect("the slot is disposed");
assert!(dispose_overlay < dispose_slot);
assert_eq!(
cmds.iter()
.filter(|c| matches!(c, ViewCommand::DisposeOverlay { .. }))
.count(),
1
);
}
#[test]
fn surface_recreate_replays_live_overlays_after_the_slots() {
let mut state = PlatformViewState::new();
state.ingest_scene(&[frame_at(1, 1)], &[], &[draw(), draw()], VIEWPORT);
state.acknowledge(state.commands().0);
assert!(state.reset_for_surface_recreate());
let cmds = state.commands().1;
assert_eq!(cmds.len(), 3);
assert!(matches!(cmds[0], ViewCommand::Create { slot_id: 1, .. }));
assert!(matches!(
cmds[1],
ViewCommand::Update {
slot_id: 1,
z_index: 0,
..
}
));
assert_eq!(
cmds[2],
ViewCommand::Overlay {
overlay_id: 1,
above_slot_id: 1,
rect: VIEWPORT,
command_range: 1..2,
}
);
}
#[test]
fn overlays_ride_the_frame_pairing_release_gate() {
let mut state = PlatformViewState::new();
let mut pairing = FramePairing::new();
state.ingest_scene(&[frame_at(1, 1)], &[], &[draw()], VIEWPORT);
pairing.record(state.commands().0, 1);
state.ingest_scene(&[frame_at(1, 1)], &[], &[draw(), draw()], VIEWPORT);
pairing.record(state.commands().0, 2);
let (_, released) = state.commands_up_to(pairing.releasable_generation(1, 2));
assert!(
!released
.iter()
.any(|c| matches!(c, ViewCommand::Overlay { .. }))
);
let (_, released) = state.commands_up_to(pairing.releasable_generation(2, 2));
assert!(
released
.iter()
.any(|c| matches!(c, ViewCommand::Overlay { .. }))
);
}
}