use bevy::ecs::entity::Entity;
use bevy::ecs::resource::Resource;
use bevy::ecs::system::{Res, ResMut};
use bevy::platform::collections::HashMap;
use bevy::render::sync_world::{MainEntity, MainEntityHashMap};
use bevy::ui_render::ExtractedUiNodes;
use bevy::ui_render::box_shadow::ExtractedBoxShadows;
use bevy::ui_render::ui_texture_slice_pipeline::ExtractedUiTextureSlices;
use super::ExtractedUiLayers;
const PARKED: Entity = Entity::PLACEHOLDER;
type Store<T> = MainEntityHashMap<(Entity, T)>;
#[derive(Resource, Default)]
pub struct ParkedUiNodes {
cameras: MainEntityHashMap<Entity>,
epoch: u64,
clean: Vec<bool>,
}
pub fn park_cached_layer_members(
extracted: Res<ExtractedUiLayers>,
mut parked: ResMut<ParkedUiNodes>,
mut nodes: ResMut<ExtractedUiNodes>,
mut shadows: ResMut<ExtractedBoxShadows>,
mut slices: ResMut<ExtractedUiTextureSlices>,
) {
park(
&mut parked,
extracted.membership_epoch,
&extracted.membership,
extracted.layers.iter().map(|layer| !layer.needs_capture),
&mut nodes,
&mut shadows.box_shadows,
&mut slices.slices,
);
}
fn park<S, T>(
state: &mut ParkedUiNodes,
epoch: u64,
membership: &HashMap<MainEntity, usize>,
clean: impl Iterator<Item = bool> + Clone,
nodes: &mut ExtractedUiNodes,
shadows: &mut Store<S>,
slices: &mut Store<T>,
) {
let ParkedUiNodes {
cameras,
epoch: seen_epoch,
clean: seen_clean,
} = state;
#[cfg(debug_assertions)]
let fresh: Vec<bool> = clean.clone().collect();
let full = *seen_epoch != epoch || !seen_clean.iter().copied().eq(clean.clone());
if full {
*seen_epoch = epoch;
seen_clean.clear();
seen_clean.extend(clean);
}
if cameras.is_empty() && !seen_clean.contains(&true) {
return;
}
let want = |main: &MainEntity| {
membership
.get(main)
.is_some_and(|&idx| seen_clean.get(idx) == Some(&true))
};
let ExtractedUiNodes { uinodes, changed } = nodes;
if full {
route_all(uinodes, &want, cameras);
route_all(shadows, &want, cameras);
route_all(slices, &want, cameras);
cameras.retain(|main, _| want(main));
} else {
for main in changed.iter().filter(|main| want(main)) {
if let Some(entry) = uinodes.get_mut(main) {
route(*main, entry, true, cameras);
}
if let Some(entry) = shadows.get_mut(main) {
route(*main, entry, true, cameras);
}
if let Some(entry) = slices.get_mut(main) {
route(*main, entry, true, cameras);
}
}
}
#[cfg(debug_assertions)]
{
let clean_member = |main: &MainEntity| {
membership
.get(main)
.is_some_and(|&idx| fresh.get(idx) == Some(&true))
};
let stray = |main: &MainEntity, camera: Entity| {
camera == PARKED && !(clean_member(main) && cameras.contains_key(main))
};
debug_assert!(
!uinodes.iter().any(|(main, entry)| stray(main, entry.0))
&& !shadows.iter().any(|(main, entry)| stray(main, entry.0))
&& !slices.iter().any(|(main, entry)| stray(main, entry.0)),
"a UI entry is parked outside a clean cached layer, or without its camera"
);
}
}
fn route_all<T>(
store: &mut Store<T>,
want: &impl Fn(&MainEntity) -> bool,
cameras: &mut MainEntityHashMap<Entity>,
) {
for (main, entry) in store.iter_mut() {
route(*main, entry, want(main), cameras);
}
}
fn route<T>(
main: MainEntity,
entry: &mut (Entity, T),
park: bool,
cameras: &mut MainEntityHashMap<Entity>,
) {
match (park, entry.0 == PARKED) {
(true, false) => {
cameras.insert(main, entry.0);
entry.0 = PARKED;
}
(false, true) => match cameras.get(&main) {
Some(&camera) => entry.0 = camera,
None => debug_assert!(false, "parked UI entry without a saved camera"),
},
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn node(i: u32) -> MainEntity {
MainEntity::from(Entity::from_raw_u32(i).unwrap())
}
fn camera(i: u32) -> Entity {
Entity::from_raw_u32(1000 + i).unwrap()
}
#[derive(Default)]
struct World {
state: ParkedUiNodes,
nodes: ExtractedUiNodes,
shadows: Store<()>,
slices: Store<()>,
membership: HashMap<MainEntity, usize>,
epoch: u64,
clean: Vec<bool>,
}
impl World {
fn two_layers(clean: [bool; 2]) -> Self {
let mut world = World {
membership: [(node(1), 0), (node(2), 0), (node(3), 1)].into(),
clean: clean.into(),
..Default::default()
};
for i in 1..=4 {
world.extract(i, camera(0));
}
world
}
fn extract(&mut self, i: u32, camera: Entity) {
self.nodes
.uinodes
.insert(node(i), (camera, Default::default()));
self.nodes.changed.insert(node(i));
}
fn run(&mut self) {
park(
&mut self.state,
self.epoch,
&self.membership,
self.clean.iter().copied(),
&mut self.nodes,
&mut self.shadows,
&mut self.slices,
);
self.nodes.changed.clear();
}
fn camera_of(&self, i: u32) -> Entity {
self.nodes.uinodes[&node(i)].0
}
}
#[test]
fn parks_only_members_of_clean_layers() {
let mut world = World::two_layers([true, false]);
world.run();
assert_eq!(world.camera_of(1), PARKED);
assert_eq!(world.camera_of(2), PARKED);
assert_eq!(world.camera_of(3), camera(0));
assert_eq!(world.camera_of(4), camera(0));
}
#[test]
fn a_capturing_layer_gets_its_members_back() {
let mut world = World::two_layers([true, true]);
world.extract(2, camera(7));
world.run();
assert_eq!(world.camera_of(1), PARKED);
assert_eq!(world.camera_of(3), PARKED);
world.clean = vec![false, true];
world.run();
assert_eq!(world.camera_of(1), camera(0));
assert_eq!(world.camera_of(2), camera(7));
assert_eq!(world.camera_of(3), PARKED);
world.clean = vec![false, false];
world.run();
assert_eq!(world.camera_of(3), camera(0));
assert!(world.state.cameras.is_empty());
}
#[test]
fn a_node_leaving_its_layer_is_unparked() {
let mut world = World::two_layers([true, true]);
world.run();
world.membership.remove(&node(2));
world.epoch += 1;
world.run();
assert_eq!(world.camera_of(1), PARKED);
assert_eq!(world.camera_of(2), camera(0));
world.membership.clear();
world.clean.clear();
world.epoch += 1;
world.run();
for i in 1..=4 {
assert_eq!(world.camera_of(i), camera(0));
}
assert!(world.state.cameras.is_empty());
}
#[test]
fn a_membership_epoch_reroutes_under_equal_flags() {
let mut world = World::two_layers([true, false]);
world.run();
assert_eq!(world.camera_of(1), PARKED);
world.membership = [(node(1), 1), (node(2), 1), (node(3), 0)].into();
world.epoch += 1;
world.run();
assert_eq!(world.camera_of(1), camera(0));
assert_eq!(world.camera_of(2), camera(0));
assert_eq!(world.camera_of(3), PARKED);
}
#[test]
fn a_reextracted_member_is_parked_again() {
let mut world = World::two_layers([true, false]);
world.run();
world.extract(1, camera(5));
world.run();
assert_eq!(world.camera_of(1), PARKED);
world.clean = vec![false, false];
world.run();
assert_eq!(world.camera_of(1), camera(5));
}
#[test]
fn a_removed_entry_leaves_no_row() {
let mut world = World::two_layers([true, false]);
world.run();
world.nodes.uinodes.remove(&node(1));
world.nodes.changed.insert(node(1));
world.run();
world.clean = vec![false, false];
world.run();
assert!(world.state.cameras.is_empty());
assert_eq!(world.camera_of(2), camera(0));
}
#[test]
fn nothing_is_touched_while_every_layer_captures() {
let mut world = World::two_layers([false, false]);
world.run();
for i in 1..=4 {
assert_eq!(world.camera_of(i), camera(0));
}
assert!(world.state.cameras.is_empty());
}
#[test]
fn shadow_and_slice_entries_follow_their_node() {
let mut world = World::two_layers([true, false]);
world.shadows.insert(node(1), (camera(0), ()));
world.shadows.insert(node(3), (camera(0), ()));
world.membership.insert(node(9), 0);
world.epoch += 1;
world.slices.insert(node(9), (camera(2), ()));
world.run();
assert_eq!(world.shadows[&node(1)].0, PARKED);
assert_eq!(world.shadows[&node(3)].0, camera(0));
assert_eq!(world.slices[&node(9)].0, PARKED);
world.clean = vec![false, false];
world.run();
assert_eq!(world.shadows[&node(1)].0, camera(0));
assert_eq!(world.slices[&node(9)].0, camera(2));
}
#[test]
fn random_frames_keep_the_routing_invariants() {
const NODES: u32 = 24;
const LAYERS: usize = 4;
let mut seed = 0x9E37_79B9_7F4A_7C15_u64;
let mut rand = move |below: u32| {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed % below as u64) as u32
};
let mut world = World {
clean: vec![false; LAYERS],
..Default::default()
};
let mut model: HashMap<MainEntity, Entity> = HashMap::default();
let mut seen = [0usize; 2];
for _ in 0..2000 {
for _ in 0..rand(4) {
let i = rand(NODES);
match rand(6) {
0 | 1 => {
let layer = rand(LAYERS as u32) as usize;
world.clean[layer] = !world.clean[layer];
}
2 => {
match rand(LAYERS as u32 + 1) as usize {
LAYERS => world.membership.remove(&node(i)),
layer => world.membership.insert(node(i), layer),
};
world.epoch += 1;
}
3 => {
world.nodes.uinodes.remove(&node(i));
world.nodes.changed.insert(node(i));
model.remove(&node(i));
}
_ => {
let camera = camera(rand(3));
world.extract(i, camera);
model.insert(node(i), camera);
}
}
}
world.run();
for (main, (slot, _)) in world.nodes.uinodes.iter() {
let in_clean_layer = world
.membership
.get(main)
.is_some_and(|&layer| world.clean[layer]);
assert_eq!(*slot == PARKED, in_clean_layer, "{main:?} misrouted");
seen[in_clean_layer as usize] += 1;
let camera = if in_clean_layer {
world.state.cameras[main]
} else {
*slot
};
assert_eq!(camera, model[main], "{main:?} lost its camera");
}
}
assert!(seen[0] > 1000 && seen[1] > 1000, "vacuous walk: {seen:?}");
}
}