use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use crate::components::{GlobalTransform, Parent, Transform};
use crate::ecs::{ColumnTicks, Entity, MAX_CHANGE_AGE, PipelineContext, Tick};
use crate::gfx::transform::{IDENTITY, mat4_mul};
pub type WorldMatrix = [[f32; 4]; 4];
const NO_SLOT: u32 = u32::MAX;
const CYCLIC: u32 = u32::MAX;
const VISITING: u32 = u32::MAX - 1;
const UNVISITED: u32 = u32::MAX - 2;
const DIRTY_BUDGET_DIVISOR: usize = 8;
#[derive(Clone, Copy)]
struct SourceTicks {
transform: ColumnTicks,
parent: Tick,
}
#[derive(Default)]
pub struct TransformCache {
entity: Vec<Entity>,
local: Vec<WorldMatrix>,
world: Vec<WorldMatrix>,
parent: Vec<u32>,
depth: Vec<u32>,
order: Vec<u32>,
child_start: Vec<u32>,
child_list: Vec<u32>,
slot_of: Vec<u32>,
visited: Vec<u32>,
pass: u32,
path: Vec<u32>,
dirty: Vec<u32>,
stack: Vec<u32>,
offsets: Vec<u32>,
last: Option<SourceTicks>,
}
impl TransformCache {
fn slot_of(&self, entity: Entity) -> Option<u32> {
let slot = *self.slot_of.get(entity.index() as usize)?;
if slot == NO_SLOT {
return None;
}
(self.entity[slot as usize] == entity).then_some(slot)
}
fn resolve(&mut self, ctx: &PipelineContext) {
self.entity.clear();
self.local.clear();
for (entity, transform) in ctx.query_with_entity::<Transform>() {
self.entity.push(entity);
self.local.push(transform.model_matrix());
}
let slots = self.entity.len();
let widest = self
.entity
.iter()
.map(|e| e.index() as usize)
.max()
.map_or(0, |i| i + 1);
self.slot_of.clear();
self.slot_of.resize(widest, NO_SLOT);
for (slot, entity) in self.entity.iter().enumerate() {
self.slot_of[entity.index() as usize] = slot as u32;
}
self.parent.clear();
self.parent.resize(slots, NO_SLOT);
for (entity, parent) in ctx.query_with_entity::<Parent>() {
if let Some(slot) = self.slot_of(entity) {
self.parent[slot as usize] = self.slot_of(parent.0).unwrap_or(NO_SLOT);
}
}
self.compute_depths();
self.order_by_depth();
self.build_children();
self.world.clear();
self.world.resize(slots, IDENTITY);
for i in 0..self.order.len() {
let slot = self.order[i] as usize;
self.world[slot] = self.compose(slot);
}
self.visited.clear();
self.visited.resize(slots, 0);
self.pass = 0;
}
fn compose(&self, slot: usize) -> WorldMatrix {
match (self.depth[slot], self.parent[slot]) {
(CYCLIC, _) | (_, NO_SLOT) => self.local[slot],
(_, parent) => mat4_mul(self.world[parent as usize], self.local[slot]),
}
}
fn compute_depths(&mut self) {
self.depth.clear();
self.depth.resize(self.local.len(), UNVISITED);
for start in 0..self.depth.len() {
if self.depth[start] != UNVISITED {
continue;
}
self.path.clear();
let mut current = start;
let base = loop {
match self.depth[current] {
UNVISITED => {}
VISITING | CYCLIC => break CYCLIC,
known => break known + 1,
}
self.depth[current] = VISITING;
self.path.push(current as u32);
match self.parent[current] {
NO_SLOT => break 0,
parent => current = parent as usize,
}
};
let mut depth = base;
for &slot in self.path.iter().rev() {
self.depth[slot as usize] = depth;
if depth != CYCLIC {
depth += 1;
}
}
}
}
fn order_by_depth(&mut self) {
let deepest = self
.depth
.iter()
.copied()
.filter(|&d| d != CYCLIC)
.max()
.unwrap_or(0) as usize;
let cyclic_bucket = deepest + 1;
self.offsets.clear();
self.offsets.resize(cyclic_bucket + 1, 0);
for &depth in &self.depth {
let bucket = if depth == CYCLIC {
cyclic_bucket
} else {
depth as usize
};
self.offsets[bucket] += 1;
}
let mut offset = 0;
for count in &mut self.offsets {
let bucket = *count;
*count = offset;
offset += bucket;
}
self.order.clear();
self.order.resize(self.depth.len(), 0);
for slot in 0..self.depth.len() {
let bucket = if self.depth[slot] == CYCLIC {
cyclic_bucket
} else {
self.depth[slot] as usize
};
self.order[self.offsets[bucket] as usize] = slot as u32;
self.offsets[bucket] += 1;
}
}
fn build_children(&mut self) {
let slots = self.parent.len();
self.child_start.clear();
self.child_start.resize(slots + 1, 0);
for &parent in &self.parent {
if parent != NO_SLOT {
self.child_start[parent as usize + 1] += 1;
}
}
for i in 0..slots {
self.child_start[i + 1] += self.child_start[i];
}
self.child_list.clear();
self.child_list.resize(self.child_start[slots] as usize, 0);
self.offsets.clear();
self.offsets.extend_from_slice(&self.child_start[..slots]);
for slot in 0..slots {
let parent = self.parent[slot];
if parent == NO_SLOT {
continue;
}
let at = self.offsets[parent as usize] as usize;
self.offsets[parent as usize] += 1;
self.child_list[at] = slot as u32;
}
}
fn write_all(&self, ctx: &mut PipelineContext) {
for (slot, &entity) in self.entity.iter().enumerate() {
if let Some(global) = ctx.get_mut::<GlobalTransform>(entity) {
global.0 = self.world[slot];
}
}
}
fn resolve_incremental(&mut self, ctx: &mut PipelineContext, since: Tick) -> bool {
let budget = (self.entity.len() / DIRTY_BUDGET_DIVISOR).max(1);
self.dirty.clear();
for (entity, transform) in ctx.changed_rows::<Transform>(since) {
if self.dirty.len() >= budget {
return false;
}
let Some(slot) = self.slot_of(entity) else {
return false;
};
self.local[slot as usize] = transform.model_matrix();
self.dirty.push(slot);
}
self.dirty
.sort_unstable_by_key(|&slot| self.depth[slot as usize]);
self.pass = self.pass.wrapping_add(1);
if self.pass == 0 {
self.visited.fill(0);
self.pass = 1;
}
for i in 0..self.dirty.len() {
self.walk_subtree(ctx, self.dirty[i]);
}
true
}
fn walk_subtree(&mut self, ctx: &mut PipelineContext, seed: u32) {
self.stack.clear();
self.stack.push(seed);
while let Some(slot) = self.stack.pop() {
let slot = slot as usize;
if self.visited[slot] == self.pass {
continue;
}
self.visited[slot] = self.pass;
self.world[slot] = self.compose(slot);
if let Some(global) = ctx.get_mut::<GlobalTransform>(self.entity[slot]) {
global.0 = self.world[slot];
}
let (start, end) = (
self.child_start[slot] as usize,
self.child_start[slot + 1] as usize,
);
self.stack.extend_from_slice(&self.child_list[start..end]);
}
}
fn world_map(&self) -> BTreeMap<Entity, WorldMatrix> {
self.entity
.iter()
.copied()
.zip(self.world.iter().copied())
.collect()
}
}
pub fn resolve_world_matrices(ctx: &PipelineContext) -> BTreeMap<Entity, WorldMatrix> {
let mut cache = TransformCache::default();
cache.resolve(ctx);
cache.world_map()
}
pub fn propagate_transforms(ctx: &mut PipelineContext) {
let mut cache = TransformCache::default();
cache.resolve(ctx);
cache.write_all(ctx);
}
pub fn propagate_transforms_cached(ctx: &mut PipelineContext, cache: &mut TransformCache) {
let transform = ctx.column_ticks::<Transform>();
let parent = ctx.changed_tick::<Parent>();
if let Some(last) = cache.last {
if last.transform.changed == transform.changed && last.parent == parent {
return;
}
let targeted_only = last.parent == parent
&& last.transform.bulk == transform.bulk
&& last.transform.structural == transform.structural
&& transform
.changed
.get()
.wrapping_sub(last.transform.changed.get())
<= MAX_CHANGE_AGE;
if targeted_only && cache.resolve_incremental(ctx, last.transform.changed) {
cache.last = Some(SourceTicks { transform, parent });
return;
}
}
cache.resolve(ctx);
cache.write_all(ctx);
cache.last = Some(SourceTicks { transform, parent });
}
pub fn reparent(ctx: &mut PipelineContext, child: Entity, new_parent: Option<Entity>) {
use crate::components::Children;
if let Some(old) = ctx.remove::<Parent>(child)
&& let Some(siblings) = ctx.get_mut::<Children>(old.0)
{
siblings.0.retain(|&e| e != child);
}
if let Some(parent) = new_parent {
ctx.insert(child, Parent(parent));
match ctx.get_mut::<Children>(parent) {
Some(kids) => {
if !kids.0.contains(&child) {
kids.0.push(child);
}
}
None => ctx.insert(parent, Children(crate::memory::InlineVec::one(child))),
}
}
propagate_transforms(ctx);
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use crate::components::Children;
use crate::ecs::{Arena, ComponentStorage, FrameContext, NoPayloads, Resources};
use crate::gfx::profile::FrameProfile;
const IDENTITY4: WorldMatrix = IDENTITY;
struct TestWorld {
components: ComponentStorage,
blob: NoPayloads,
profile: FrameProfile,
resources: Resources,
scratch: Arena,
}
impl TestWorld {
fn new() -> TestWorld {
TestWorld {
components: ComponentStorage::default(),
blob: NoPayloads,
profile: FrameProfile::default(),
resources: Resources::new(),
scratch: Arena::with_capacity(64 * 1024),
}
}
fn ctx(&mut self) -> PipelineContext<'_> {
PipelineContext {
components: &mut self.components,
blob: &mut self.blob,
profile: &mut self.profile,
resources: &mut self.resources,
frame: FrameContext::new(&self.scratch),
}
}
}
fn translate(x: f32) -> Transform {
Transform {
position: [x, 0.0, 0.0],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
}
}
fn spawn(ctx: &mut PipelineContext, t: Transform, parent: Option<Entity>) -> Entity {
let entity = ctx.components.spawn();
ctx.insert(entity, t);
ctx.insert(entity, GlobalTransform::default());
if let Some(p) = parent {
ctx.insert(entity, Parent(p));
}
entity
}
fn global(ctx: &PipelineContext, entity: Entity) -> WorldMatrix {
ctx.get::<GlobalTransform>(entity).unwrap().0
}
fn chain(ctx: &mut PipelineContext, depth: usize) -> Vec<Entity> {
let mut chain = Vec::with_capacity(depth);
let mut parent = None;
for i in 0..depth {
let entity = spawn(ctx, translate(i as f32 + 1.0), parent);
chain.push(entity);
parent = Some(entity);
}
chain
}
#[test]
fn propagate_transforms_composes_parent_then_child() {
let parent_t = Transform {
position: [1.0, 2.0, 3.0],
rotation_deg: [0.0, 30.0, 0.0],
scale: [1.0, 1.0, 1.0],
};
let child_t = Transform {
position: [0.0, 0.0, 1.0],
rotation_deg: [10.0, 0.0, 5.0],
scale: [2.0, 2.0, 2.0],
};
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let parent_e = spawn(&mut ctx, parent_t, None);
let child_e = spawn(&mut ctx, child_t, Some(parent_e));
propagate_transforms(&mut ctx);
assert_eq!(
global(&ctx, parent_e),
parent_t.model_matrix(),
"root world = local"
);
assert_eq!(
global(&ctx, child_e),
mat4_mul(parent_t.model_matrix(), child_t.model_matrix()),
"child world = parent_world * local"
);
}
#[test]
fn cached_propagation_matches_the_uncached_path() {
let parent_t = Transform {
position: [1.0, 2.0, 3.0],
rotation_deg: [0.0, 30.0, 0.0],
scale: [1.0, 1.0, 1.0],
};
let child_t = Transform {
position: [0.0, 0.0, 1.0],
rotation_deg: [10.0, 0.0, 5.0],
scale: [2.0, 2.0, 2.0],
};
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let parent_e = spawn(&mut ctx, parent_t, None);
let child_e = spawn(&mut ctx, child_t, Some(parent_e));
let mut cache = TransformCache::default();
propagate_transforms_cached(&mut ctx, &mut cache);
assert_eq!(global(&ctx, parent_e), parent_t.model_matrix());
assert_eq!(
global(&ctx, child_e),
mat4_mul(parent_t.model_matrix(), child_t.model_matrix())
);
}
#[test]
fn cached_propagation_skips_until_a_transform_changes() {
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let t0 = translate(1.0);
let e = spawn(&mut ctx, t0, None);
let mut cache = TransformCache::default();
propagate_transforms_cached(&mut ctx, &mut cache);
assert_eq!(global(&ctx, e), t0.model_matrix());
ctx.get_mut::<GlobalTransform>(e).unwrap().0 = IDENTITY4;
propagate_transforms_cached(&mut ctx, &mut cache);
assert_eq!(
global(&ctx, e),
IDENTITY4,
"unchanged Transform => propagation skipped"
);
let t1 = Transform {
position: [0.0, 5.0, 0.0],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
};
*ctx.get_mut::<Transform>(e).unwrap() = t1;
propagate_transforms_cached(&mut ctx, &mut cache);
assert_eq!(
global(&ctx, e),
t1.model_matrix(),
"changed Transform => propagation recomputed"
);
}
#[test]
fn moving_a_root_recomposes_its_whole_subtree() {
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let links = chain(&mut ctx, 4);
let mut cache = TransformCache::default();
propagate_transforms_cached(&mut ctx, &mut cache);
let moved = translate(100.0);
*ctx.get_mut::<Transform>(links[0]).unwrap() = moved;
propagate_transforms_cached(&mut ctx, &mut cache);
let mut expected = moved.model_matrix();
assert_eq!(global(&ctx, links[0]), expected);
for (depth, &link) in links.iter().enumerate().skip(1) {
let local = translate(depth as f32 + 1.0).model_matrix();
expected = mat4_mul(expected, local);
assert_eq!(global(&ctx, link), expected, "link at depth {depth}");
}
}
#[test]
fn an_untouched_subtree_is_not_rewritten() {
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let root_a = spawn(&mut ctx, translate(1.0), None);
let child_a = spawn(&mut ctx, translate(2.0), Some(root_a));
let root_b = spawn(&mut ctx, translate(3.0), None);
let child_b = spawn(&mut ctx, translate(4.0), Some(root_b));
let mut cache = TransformCache::default();
propagate_transforms_cached(&mut ctx, &mut cache);
ctx.get_mut::<GlobalTransform>(child_b).unwrap().0 = IDENTITY4;
*ctx.get_mut::<Transform>(root_a).unwrap() = translate(50.0);
propagate_transforms_cached(&mut ctx, &mut cache);
assert_eq!(
global(&ctx, child_a),
mat4_mul(
translate(50.0).model_matrix(),
translate(2.0).model_matrix()
),
"the moved root's subtree recomposed"
);
assert_eq!(
global(&ctx, child_b),
IDENTITY4,
"the other tree was never walked"
);
assert_eq!(global(&ctx, root_b), translate(3.0).model_matrix());
}
#[test]
fn a_dirty_ancestor_and_descendant_resolve_against_the_new_parent() {
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let links = chain(&mut ctx, 3);
let mut cache = TransformCache::default();
propagate_transforms_cached(&mut ctx, &mut cache);
*ctx.get_mut::<Transform>(links[2]).unwrap() = translate(7.0);
*ctx.get_mut::<Transform>(links[0]).unwrap() = translate(9.0);
propagate_transforms_cached(&mut ctx, &mut cache);
let root = translate(9.0).model_matrix();
let mid = mat4_mul(root, translate(2.0).model_matrix());
assert_eq!(global(&ctx, links[0]), root);
assert_eq!(global(&ctx, links[1]), mid);
assert_eq!(
global(&ctx, links[2]),
mat4_mul(mid, translate(7.0).model_matrix())
);
}
#[test]
fn a_spawned_entity_forces_a_full_resolve() {
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let root = spawn(&mut ctx, translate(1.0), None);
let mut cache = TransformCache::default();
propagate_transforms_cached(&mut ctx, &mut cache);
let late = spawn(&mut ctx, translate(5.0), Some(root));
propagate_transforms_cached(&mut ctx, &mut cache);
assert_eq!(
global(&ctx, late),
mat4_mul(translate(1.0).model_matrix(), translate(5.0).model_matrix()),
"the entity added after the last resolve composed correctly"
);
}
#[test]
fn a_despawned_entity_leaves_the_survivors_correct() {
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let root = spawn(&mut ctx, translate(1.0), None);
let doomed = spawn(&mut ctx, translate(2.0), Some(root));
let kept = spawn(&mut ctx, translate(3.0), Some(root));
let mut cache = TransformCache::default();
propagate_transforms_cached(&mut ctx, &mut cache);
ctx.despawn(doomed);
*ctx.get_mut::<Transform>(root).unwrap() = translate(20.0);
propagate_transforms_cached(&mut ctx, &mut cache);
assert_eq!(
global(&ctx, kept),
mat4_mul(
translate(20.0).model_matrix(),
translate(3.0).model_matrix()
)
);
}
#[test]
fn a_whole_column_write_falls_back_to_a_full_resolve() {
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let root = spawn(&mut ctx, translate(1.0), None);
let child = spawn(&mut ctx, translate(2.0), Some(root));
let mut cache = TransformCache::default();
propagate_transforms_cached(&mut ctx, &mut cache);
for t in ctx.query_mut::<Transform>() {
t.position[1] += 3.0;
}
propagate_transforms_cached(&mut ctx, &mut cache);
let shifted = |x: f32| Transform {
position: [x, 3.0, 0.0],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
};
assert_eq!(global(&ctx, root), shifted(1.0).model_matrix());
assert_eq!(
global(&ctx, child),
mat4_mul(shifted(1.0).model_matrix(), shifted(2.0).model_matrix())
);
}
#[test]
fn a_dirty_set_past_the_budget_falls_back_and_still_resolves() {
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let entities: Vec<Entity> = (0..16)
.map(|i| spawn(&mut ctx, translate(i as f32), None))
.collect();
let mut cache = TransformCache::default();
propagate_transforms_cached(&mut ctx, &mut cache);
for (i, &e) in entities.iter().enumerate().take(8) {
*ctx.get_mut::<Transform>(e).unwrap() = translate(100.0 + i as f32);
}
propagate_transforms_cached(&mut ctx, &mut cache);
for (i, &e) in entities.iter().enumerate() {
let expected = if i < 8 {
translate(100.0 + i as f32)
} else {
translate(i as f32)
};
assert_eq!(global(&ctx, e), expected.model_matrix(), "entity {i}");
}
}
#[test]
fn a_parent_without_a_transform_leaves_the_child_a_root() {
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let bare = ctx.components.spawn();
ctx.insert(bare, Children(crate::memory::InlineVec::new()));
let child = spawn(&mut ctx, translate(4.0), Some(bare));
propagate_transforms(&mut ctx);
assert_eq!(global(&ctx, child), translate(4.0).model_matrix());
}
#[test]
fn resolve_world_matrices_breaks_parent_cycle() {
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let a_t = translate(1.0);
let b_t = Transform {
position: [0.0, 2.0, 0.0],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
};
let a = ctx.components.spawn();
ctx.insert(a, a_t);
let b = ctx.components.spawn();
ctx.insert(b, b_t);
ctx.insert(a, Parent(b));
ctx.insert(b, Parent(a));
let resolved = resolve_world_matrices(&ctx);
assert_eq!(resolved.len(), 2);
assert_eq!(resolved.get(&a).copied(), Some(a_t.model_matrix()));
assert_eq!(resolved.get(&b).copied(), Some(b_t.model_matrix()));
}
#[test]
fn an_entity_below_a_cycle_falls_back_to_its_local() {
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let a = spawn(&mut ctx, translate(1.0), None);
let b = spawn(&mut ctx, translate(2.0), Some(a));
ctx.insert(a, Parent(b));
let below = spawn(&mut ctx, translate(3.0), Some(b));
propagate_transforms(&mut ctx);
assert_eq!(global(&ctx, a), translate(1.0).model_matrix());
assert_eq!(global(&ctx, b), translate(2.0).model_matrix());
assert_eq!(global(&ctx, below), translate(3.0).model_matrix());
}
#[test]
fn a_very_deep_chain_resolves_iteratively() {
const DEPTH: usize = 20_000;
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let mut links = Vec::with_capacity(DEPTH);
let mut parent = None;
for _ in 0..DEPTH {
let entity = spawn(&mut ctx, translate(1.0), parent);
links.push(entity);
parent = Some(entity);
}
propagate_transforms(&mut ctx);
assert_eq!(global(&ctx, links[DEPTH - 1])[3][0], DEPTH as f32);
}
#[test]
fn reparent_recomposes_child_world_matrix_and_relists() {
let (a_t, b_t, child_t) = (translate(10.0), translate(-5.0), translate(1.0));
let mut world = TestWorld::new();
let mut ctx = world.ctx();
let a = spawn(&mut ctx, a_t, None);
let b = spawn(&mut ctx, b_t, None);
let child = spawn(&mut ctx, child_t, None);
reparent(&mut ctx, child, Some(a));
let under_a = global(&ctx, child);
assert_eq!(
under_a,
mat4_mul(a_t.model_matrix(), child_t.model_matrix())
);
assert_eq!(ctx.get::<Children>(a).unwrap().0, vec![child]);
reparent(&mut ctx, child, Some(b));
let under_b = global(&ctx, child);
assert_eq!(
under_b,
mat4_mul(b_t.model_matrix(), child_t.model_matrix())
);
assert_ne!(under_a, under_b, "the child actually moved");
assert!(
ctx.get::<Children>(a).unwrap().0.is_empty(),
"A unlisted the child"
);
assert_eq!(ctx.get::<Children>(b).unwrap().0, vec![child]);
assert_eq!(ctx.get::<Parent>(child).unwrap().0, b);
reparent(&mut ctx, child, None);
assert_eq!(global(&ctx, child), child_t.model_matrix());
assert!(ctx.get::<Parent>(child).is_none(), "child is now a root");
assert!(
ctx.get::<Children>(b).unwrap().0.is_empty(),
"B unlisted the child"
);
}
}