use core::any::Any;
use unlit_ecs::{Entity, LocalWorld};
use unlit_wgpu::resources::ResourceGraph;
use unlit_wgpu::scene::Scene;
use unlit_wgpu::specialize::SurfaceKey;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct FrameOrder(pub i32);
impl FrameOrder {
pub const MESH: Self = Self(0);
pub const OVERLAY: Self = Self(100);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RenderContext {
pub device: Entity,
pub queue: Entity,
pub graph: Entity,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FrameTarget {
pub surface: SurfaceKey,
pub width: u32,
pub height: u32,
}
#[derive(Clone, Copy, Debug, Default)]
struct FrameTargetSlot(Option<FrameTarget>);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct InputCapture {
pub pointer: bool,
pub keyboard: bool,
}
impl InputCapture {
pub fn any(self) -> bool {
self.pointer || self.keyboard
}
}
pub fn frame_target(world: &LocalWorld) -> Option<FrameTarget> {
world
.query::<&FrameTargetSlot>()
.next()
.and_then(|(_, slot)| slot.0)
}
pub fn set_frame_target(world: &LocalWorld, target: FrameTarget) -> bool {
let Some(entity) = world.query::<&FrameTargetSlot>().next().map(|(e, _)| e) else {
return false;
};
world
.with_mut::<FrameTargetSlot, _>(entity, |slot| slot.0 = Some(target))
.is_some()
}
pub(crate) fn unset_frame_target(world: &LocalWorld) -> bool {
let Some(entity) = world.query::<&FrameTargetSlot>().next().map(|(e, _)| e) else {
return false;
};
world
.with_mut::<FrameTargetSlot, _>(entity, |slot| slot.0 = None)
.is_some()
}
pub trait FrameSource: 'static {
fn build_scene(
&mut self,
world: &LocalWorld,
ctx: RenderContext,
encoder: &mut wgpu::CommandEncoder,
);
fn scene(&self) -> &Scene;
fn order(&self) -> FrameOrder;
fn release(&mut self, world: &LocalWorld) {
let _ = world;
}
}
pub trait AnySource: Any + FrameSource {}
impl<T: FrameSource> AnySource for T {}
pub struct Source {
source: Box<dyn AnySource>,
order: Option<FrameOrder>,
mount_index: u64,
}
impl Source {
pub fn new(source: impl FrameSource) -> Self {
Self {
source: Box::new(source),
order: None,
mount_index: 0,
}
}
pub fn with_order(mut self, order: FrameOrder) -> Self {
self.order = Some(order);
self
}
pub fn order(&self) -> FrameOrder {
self.order.unwrap_or_else(|| self.source.order())
}
pub fn set_order(&mut self, order: Option<FrameOrder>) {
self.order = order;
}
pub fn mount_index(&self) -> u64 {
self.mount_index
}
pub fn as_mut<T: FrameSource>(&mut self) -> Option<&mut T> {
let erased: &mut dyn Any = &mut *self.source;
erased.downcast_mut::<T>()
}
pub fn as_ref<T: FrameSource>(&self) -> Option<&T> {
let erased: &dyn Any = &*self.source;
erased.downcast_ref::<T>()
}
pub fn build_scene(
&mut self,
world: &LocalWorld,
ctx: RenderContext,
encoder: &mut wgpu::CommandEncoder,
) {
self.source.build_scene(world, ctx, encoder);
}
pub fn scene(&self) -> &Scene {
self.source.scene()
}
pub fn release(&mut self, world: &LocalWorld) {
self.source.release(world);
}
}
impl core::fmt::Debug for Source {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Source")
.field("order", &self.order())
.field("mount_index", &self.mount_index)
.finish_non_exhaustive()
}
}
#[derive(Debug, Default)]
struct MountCounter(u64);
pub fn spawn_context(
world: &mut LocalWorld,
device: wgpu::Device,
queue: wgpu::Queue,
graph: ResourceGraph,
) -> RenderContext {
let device = world.spawn((device,));
let queue = world.spawn((queue,));
let graph = world.spawn((graph,));
world.spawn((MountCounter::default(),));
world.spawn((FrameTargetSlot::default(),));
world.spawn((InputCapture::default(),));
RenderContext {
device,
queue,
graph,
}
}
pub fn spawn_source(world: &mut LocalWorld, source: impl FrameSource) -> Entity {
let source = Source::new(source);
let index = next_mount_index(world);
world.spawn((bump_mount(source, index),))
}
pub fn spawn_source_at(
world: &mut LocalWorld,
order: FrameOrder,
source: impl FrameSource,
) -> Entity {
let source = Source::new(source).with_order(order);
let index = next_mount_index(world);
world.spawn((bump_mount(source, index),))
}
pub fn despawn_source(world: &LocalWorld, entity: Entity) {
world.queue().push(DespawnSource { entity });
}
struct DespawnSource {
entity: Entity,
}
impl unlit_ecs::Command<unlit_ecs::LocalMode> for DespawnSource {
fn apply(self: Box<Self>, world: &mut LocalWorld) {
if let Some(mut source) = world.get_mut::<Source>(self.entity) {
source.release(world);
}
world.despawn(self.entity);
}
}
fn next_mount_index(world: &LocalWorld) -> u64 {
let Some(counter) = world.query::<&MountCounter>().next().map(|(e, _)| e) else {
return 0;
};
world
.with_mut::<MountCounter, _>(counter, |counter| {
let index = counter.0;
counter.0 += 1;
index
})
.unwrap_or(0)
}
fn bump_mount(mut source: Source, index: u64) -> Source {
source.mount_index = index;
source
}
pub type AmbiguousGroup = (FrameOrder, Vec<Entity>);
#[derive(Debug, Default)]
pub struct SourceOrder {
keys: Vec<(FrameOrder, u64, Entity)>,
order: Vec<Entity>,
ambiguous: Vec<AmbiguousGroup>,
}
impl SourceOrder {
pub fn resolve(&mut self, world: &LocalWorld) {
self.keys.clear();
self.keys.extend(
world
.query::<&Source>()
.map(|(entity, source)| (source.order(), source.mount_index(), entity)),
);
self.keys
.sort_unstable_by_key(|(order, mount_index, _)| (*order, *mount_index));
self.order.clear();
self.ambiguous.clear();
let mut start = 0;
while start < self.keys.len() {
let order = self.keys[start].0;
let mut end = start + 1;
while end < self.keys.len() && self.keys[end].0 == order {
end += 1;
}
if end - start > 1 {
let group = self.keys[start..end]
.iter()
.map(|(_, _, entity)| *entity)
.collect();
self.ambiguous.push((order, group));
}
start = end;
}
self.order
.extend(self.keys.iter().map(|(_, _, entity)| *entity));
}
pub fn entities(&self) -> &[Entity] {
&self.order
}
pub fn ambiguous(&self) -> &[AmbiguousGroup] {
&self.ambiguous
}
}
#[derive(Debug, Default)]
pub struct OrderWarnings {
reported: Vec<AmbiguousGroup>,
}
impl OrderWarnings {
pub fn check(&mut self, ambiguous: &[AmbiguousGroup]) -> bool {
if self.reported == ambiguous {
return false;
}
for (order, group) in ambiguous {
log::warn!(
"{} sources declared the same FrameOrder({}): entities {:?}; \
recording them in mount order — give them distinct orders to \
choose explicitly",
group.len(),
order.0,
group,
);
}
self.reported = ambiguous.to_vec();
!ambiguous.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_context(world: &mut LocalWorld) -> RenderContext {
let (device, queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
spawn_context(world, device, queue, ResourceGraph::new())
}
struct RecordingSource {
order: FrameOrder,
built: Rc<Cell<u32>>,
scene: Scene,
}
impl RecordingSource {
fn new(order: FrameOrder) -> Self {
Self {
order,
built: Rc::new(Cell::new(0)),
scene: Scene::new(),
}
}
}
impl FrameSource for RecordingSource {
fn build_scene(
&mut self,
_world: &LocalWorld,
_ctx: RenderContext,
_encoder: &mut wgpu::CommandEncoder,
) {
self.built.set(self.built.get() + 1);
}
fn scene(&self) -> &Scene {
&self.scene
}
fn order(&self) -> FrameOrder {
self.order
}
}
use std::cell::{Cell, RefCell};
use std::rc::Rc;
fn record_order(world: &LocalWorld) -> (Vec<Entity>, Vec<AmbiguousGroup>) {
let mut resolved = SourceOrder::default();
resolved.resolve(world);
(resolved.entities().to_vec(), resolved.ambiguous().to_vec())
}
#[test]
fn explicit_order_wins_over_mount_order() {
let mut world = LocalWorld::new();
spawn_context(
&mut world,
wgpu::Device::noop(&wgpu::DeviceDescriptor::default()).0,
wgpu::Device::noop(&wgpu::DeviceDescriptor::default()).1,
ResourceGraph::new(),
);
let overlay = spawn_source(&mut world, RecordingSource::new(FrameOrder::OVERLAY));
let mesh = spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH));
let (order, ambiguous) = record_order(&world);
assert_eq!(order, vec![mesh, overlay], "the lower order records first");
assert!(ambiguous.is_empty(), "distinct orders are not ambiguous");
}
#[test]
fn equal_orders_keep_mount_order_across_a_despawn() {
let mut world = LocalWorld::new();
test_context(&mut world);
let first = spawn_source(&mut world, RecordingSource::new(FrameOrder::OVERLAY));
let second = spawn_source(&mut world, RecordingSource::new(FrameOrder::OVERLAY));
let third = spawn_source(&mut world, RecordingSource::new(FrameOrder::OVERLAY));
assert_eq!(record_order(&world).0, vec![first, second, third]);
world.despawn(first);
assert_eq!(
record_order(&world).0,
vec![second, third],
"mount order survives a despawn"
);
}
#[test]
fn an_explicit_order_overrides_the_one_the_source_declares() {
let mut world = LocalWorld::new();
test_context(&mut world);
let a = spawn_source_at(
&mut world,
FrameOrder::OVERLAY,
RecordingSource::new(FrameOrder::MESH),
);
let b = spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH));
assert_eq!(record_order(&world).0, vec![b, a]);
let _ = world.with_mut::<Source, _>(a, |source| source.set_order(None));
assert_eq!(
record_order(&world).0,
vec![a, b],
"dropping the override falls back to the declared order"
);
}
#[test]
fn typed_access_downcasts_and_rejects_the_wrong_type() {
struct OtherSource(Scene);
impl FrameSource for OtherSource {
fn build_scene(
&mut self,
_world: &LocalWorld,
_ctx: RenderContext,
_encoder: &mut wgpu::CommandEncoder,
) {
}
fn scene(&self) -> &Scene {
&self.0
}
fn order(&self) -> FrameOrder {
FrameOrder::MESH
}
}
let mut world = LocalWorld::new();
test_context(&mut world);
let recording = spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH));
let other = spawn_source(&mut world, OtherSource(Scene::new()));
assert!(
world
.with_mut::<Source, _>(recording, |s| s.as_mut::<RecordingSource>().is_some())
.unwrap()
);
assert!(
world
.with_mut::<Source, _>(recording, |s| s.as_mut::<OtherSource>().is_none())
.unwrap()
);
assert!(
world
.with_mut::<Source, _>(other, |s| s.as_mut::<OtherSource>().is_some())
.unwrap()
);
assert!(
world
.with_mut::<Source, _>(other, |s| s.as_ref::<RecordingSource>().is_none())
.unwrap()
);
}
#[test]
fn one_query_builds_every_source_type() {
struct CountingSource(u32, Scene);
impl FrameSource for CountingSource {
fn build_scene(
&mut self,
_world: &LocalWorld,
_ctx: RenderContext,
_encoder: &mut wgpu::CommandEncoder,
) {
self.0 += 1;
}
fn scene(&self) -> &Scene {
&self.1
}
fn order(&self) -> FrameOrder {
FrameOrder::OVERLAY
}
}
let mut world = LocalWorld::new();
let ctx = test_context(&mut world);
spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH));
spawn_source(&mut world, CountingSource(0, Scene::new()));
let mut encoder = wgpu::Device::noop(&wgpu::DeviceDescriptor::default())
.0
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
let entities: Vec<Entity> = world.query::<&Source>().map(|(e, _)| e).collect();
assert_eq!(entities.len(), 2, "both sources are seen by one query type");
for entity in entities {
world
.with_mut::<Source, _>(entity, |source| {
source.build_scene(&world, ctx, &mut encoder)
})
.unwrap();
}
}
#[test]
fn every_source_is_built_before_any_is_recorded() {
let mut world = LocalWorld::new();
let ctx = test_context(&mut world);
let order = Rc::new(RefCell::new(Vec::new()));
struct PhasedSource {
name: &'static str,
log: Rc<RefCell<Vec<String>>>,
scene: Scene,
}
impl FrameSource for PhasedSource {
fn build_scene(
&mut self,
_world: &LocalWorld,
_ctx: RenderContext,
_encoder: &mut wgpu::CommandEncoder,
) {
self.log.borrow_mut().push(format!("build {}", self.name));
}
fn scene(&self) -> &Scene {
&self.scene
}
fn order(&self) -> FrameOrder {
FrameOrder::MESH
}
}
for name in ["a", "b"] {
spawn_source(
&mut world,
PhasedSource {
name,
log: Rc::clone(&order),
scene: Scene::new(),
},
);
}
let mut encoder = wgpu::Device::noop(&wgpu::DeviceDescriptor::default())
.0
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
let entities: Vec<Entity> = world.query::<&Source>().map(|(e, _)| e).collect();
for entity in &entities {
world
.with_mut::<Source, _>(*entity, |source| {
source.build_scene(&world, ctx, &mut encoder)
})
.unwrap();
}
order.borrow_mut().push("record".to_string());
assert_eq!(
&*order.borrow(),
&["build a", "build b", "record"],
"both sources build before the record phase starts"
);
}
#[test]
fn a_source_reports_its_declared_order_until_overridden() {
let source = Source::new(RecordingSource::new(FrameOrder::OVERLAY));
assert_eq!(source.order(), FrameOrder::OVERLAY);
let mut source =
Source::new(RecordingSource::new(FrameOrder::MESH)).with_order(FrameOrder::OVERLAY);
assert_eq!(source.order(), FrameOrder::OVERLAY, "the override wins");
source.set_order(None);
assert_eq!(source.order(), FrameOrder::MESH, "and can be dropped");
}
#[test]
fn an_ambiguity_is_reported_once_and_again_only_when_it_changes() {
let mut world = LocalWorld::new();
test_context(&mut world);
let a = spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH));
let b = spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH));
let (_, ambiguous) = record_order(&world);
assert_eq!(ambiguous.len(), 1, "the two share one order");
assert_eq!(ambiguous[0].0, FrameOrder::MESH);
assert_eq!(ambiguous[0].1, vec![a, b], "in mount order");
let mut warnings = OrderWarnings::default();
assert!(warnings.check(&ambiguous), "the first frame reports it");
assert!(!warnings.check(&ambiguous), "a steady frame stays quiet");
let _ =
world.with_mut::<Source, _>(b, |source| source.set_order(Some(FrameOrder::OVERLAY)));
let (_, resolved) = record_order(&world);
assert!(resolved.is_empty());
assert!(!warnings.check(&resolved));
let _ = world.with_mut::<Source, _>(b, |source| source.set_order(Some(FrameOrder::MESH)));
let (_, again) = record_order(&world);
assert!(warnings.check(&again), "the ambiguity is back");
assert!(!warnings.check(&again), "and still reports only once");
}
#[test]
fn resolving_the_order_reuses_its_buffers() {
let mut world = LocalWorld::new();
test_context(&mut world);
for _ in 0..4 {
spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH));
}
spawn_source(&mut world, RecordingSource::new(FrameOrder::OVERLAY));
let mut resolved = SourceOrder::default();
resolved.resolve(&world);
let order_capacity = resolved.order.capacity();
let ambiguous_capacity = resolved.ambiguous.capacity();
assert_eq!(resolved.entities().len(), 5);
assert_eq!(resolved.ambiguous().len(), 1, "four share one order");
let first = world.query::<&Source>().next().expect("a source").0;
world.despawn(first);
resolved.resolve(&world);
assert_eq!(resolved.entities().len(), 4);
assert_eq!(
resolved.order.capacity(),
order_capacity,
"the order buffer is reused rather than reallocated"
);
assert_eq!(
resolved.ambiguous.capacity(),
ambiguous_capacity,
"the ambiguity buffer is reused rather than reallocated"
);
}
#[test]
fn distinct_orders_are_never_ambiguous() {
let mut world = LocalWorld::new();
test_context(&mut world);
spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH));
spawn_source(&mut world, RecordingSource::new(FrameOrder::OVERLAY));
let (order, ambiguous) = record_order(&world);
assert_eq!(order.len(), 2);
assert!(ambiguous.is_empty());
assert!(!OrderWarnings::default().check(&ambiguous));
}
struct CaptureLogger;
static CAPTURED: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
impl log::Log for CaptureLogger {
fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool {
true
}
fn log(&self, record: &log::Record<'_>) {
if let Ok(mut captured) = CAPTURED.lock() {
captured.push(record.args().to_string());
}
}
fn flush(&self) {}
}
fn capture_logs(f: impl FnOnce()) -> Vec<String> {
static LOGGER: CaptureLogger = CaptureLogger;
let _ = log::set_logger(&LOGGER);
log::set_max_level(log::LevelFilter::Warn);
CAPTURED.lock().expect("the capture mutex").clear();
f();
let captured = CAPTURED.lock().expect("the capture mutex");
captured.clone()
}
#[test]
fn the_ambiguity_warning_names_the_group_order_and_entities() {
let mut world = LocalWorld::new();
test_context(&mut world);
let a = spawn_source(&mut world, RecordingSource::new(FrameOrder::OVERLAY));
let b = spawn_source(&mut world, RecordingSource::new(FrameOrder::OVERLAY));
let messages = capture_logs(|| {
let (_, ambiguous) = record_order(&world);
let mut warnings = OrderWarnings::default();
assert!(warnings.check(&ambiguous), "the group is reported");
});
assert_eq!(messages.len(), 1, "exactly one warning: {messages:?}");
let message = &messages[0];
assert!(
message.contains('2'),
"the group's size is named: {message}"
);
assert!(
message.contains(&FrameOrder::OVERLAY.0.to_string()),
"the order's value is named: {message}"
);
assert!(
message.contains(&format!("{a:?}")) && message.contains(&format!("{b:?}")),
"both entities are named: {message}"
);
}
#[test]
fn distinct_orders_log_nothing() {
let mut world = LocalWorld::new();
test_context(&mut world);
spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH));
spawn_source(&mut world, RecordingSource::new(FrameOrder::OVERLAY));
let messages = capture_logs(|| {
let (_, ambiguous) = record_order(&world);
assert!(!OrderWarnings::default().check(&ambiguous));
});
assert!(messages.is_empty(), "nothing to warn about: {messages:?}");
}
#[test]
fn a_steady_frame_repeats_nothing_in_the_log() {
let mut world = LocalWorld::new();
test_context(&mut world);
let a = spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH));
let _b = spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH));
let mut warnings = OrderWarnings::default();
let messages = capture_logs(|| {
for _ in 0..5 {
let (_, ambiguous) = record_order(&world);
warnings.check(&ambiguous);
}
});
assert_eq!(
messages.len(),
1,
"five steady frames warn once: {messages:?}"
);
let _ =
world.with_mut::<Source, _>(a, |source| source.set_order(Some(FrameOrder::OVERLAY)));
let messages = capture_logs(|| {
let (_, resolved) = record_order(&world);
assert!(!warnings.check(&resolved), "resolved: nothing to say");
});
assert!(messages.is_empty(), "resolving is silent: {messages:?}");
}
#[test]
fn re_applying_the_same_order_keeps_the_log_quiet() {
let mut world = LocalWorld::new();
test_context(&mut world);
let a = spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH));
let b = spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH));
let mut warnings = OrderWarnings::default();
let messages = capture_logs(|| {
for _ in 0..3 {
let _ = world
.with_mut::<Source, _>(a, |source| source.set_order(Some(FrameOrder::MESH)));
let _ = world
.with_mut::<Source, _>(b, |source| source.set_order(Some(FrameOrder::MESH)));
let (_, ambiguous) = record_order(&world);
warnings.check(&ambiguous);
}
});
assert_eq!(
messages.len(),
1,
"the second and third frames change nothing: {messages:?}"
);
}
#[test]
fn a_three_way_ambiguity_is_one_group_of_three() {
let mut world = LocalWorld::new();
test_context(&mut world);
let ids: Vec<Entity> = (0..3)
.map(|_| spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH)))
.collect();
let (_, ambiguous) = record_order(&world);
assert_eq!(ambiguous.len(), 1, "one group, not pairs");
assert_eq!(ambiguous[0].1, ids);
}
#[test]
fn mount_indices_are_monotonic() {
let mut world = LocalWorld::new();
test_context(&mut world);
let indices: Vec<u64> = (0..3)
.map(|_| {
let entity = spawn_source(&mut world, RecordingSource::new(FrameOrder::MESH));
world
.with_mut::<Source, _>(entity, |s| s.mount_index())
.unwrap()
})
.collect();
assert_eq!(indices, vec![0, 1, 2]);
}
}
#[cfg(test)]
mod release_tests {
use super::*;
use unlit_wgpu::resources::{Resource as GraphResource, ResourceId};
struct OwningSource {
context: RenderContext,
node: Option<ResourceId>,
scene: Scene,
}
impl FrameSource for OwningSource {
fn build_scene(
&mut self,
world: &LocalWorld,
ctx: RenderContext,
_encoder: &mut wgpu::CommandEncoder,
) {
self.context = ctx;
if self.node.is_none() {
self.node = world
.get_mut::<ResourceGraph>(ctx.graph)
.expect("the context's graph exists")
.insert_strong(GraphResource::Virtual, &[])
.ok();
}
}
fn scene(&self) -> &Scene {
&self.scene
}
fn order(&self) -> FrameOrder {
FrameOrder::OVERLAY
}
fn release(&mut self, world: &LocalWorld) {
let Some(node) = self.node.take() else {
return;
};
world
.get_mut::<ResourceGraph>(self.context.graph)
.expect("the context's graph exists")
.remove_drop(node);
}
}
fn noop_context(world: &mut LocalWorld) -> RenderContext {
spawn_context(
world,
wgpu::Device::noop(&wgpu::DeviceDescriptor::default()).0,
wgpu::Device::noop(&wgpu::DeviceDescriptor::default()).1,
ResourceGraph::new(),
)
}
#[test]
fn removing_a_source_releases_the_nodes_it_owns() {
let mut world = LocalWorld::new();
let ctx = noop_context(&mut world);
let entity = spawn_source(
&mut world,
OwningSource {
context: ctx,
node: None,
scene: Scene::new(),
},
);
let mut encoder = wgpu::Device::noop(&wgpu::DeviceDescriptor::default())
.0
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
world
.with_mut::<Source, _>(entity, |source| {
source.build_scene(&world, ctx, &mut encoder)
})
.expect("the source entity exists");
let node = world
.with_mut::<Source, _>(entity, |source| {
source.as_mut::<OwningSource>().unwrap().node
})
.flatten()
.expect("the source registered a node");
assert!(
world
.get_mut::<ResourceGraph>(ctx.graph)
.unwrap()
.get(node)
.is_some(),
"the node is in the graph while the source lives"
);
let before = world.get_mut::<ResourceGraph>(ctx.graph).unwrap().len();
despawn_source(&world, entity);
assert!(world.contains(entity), "removal is queued, not immediate");
world.apply();
assert!(!world.contains(entity), "the source is gone");
let graph = world.get_mut::<ResourceGraph>(ctx.graph).unwrap();
assert!(
graph.get(node).is_none(),
"the source's node was released with it"
);
assert!(
graph.len() < before,
"the graph shrank: {} was {before}",
graph.len()
);
}
#[test]
fn removing_a_source_that_owns_nothing_still_removes_it() {
let mut world = LocalWorld::new();
let ctx = noop_context(&mut world);
let entity = spawn_source(
&mut world,
OwningSource {
context: ctx,
node: None,
scene: Scene::new(),
},
);
despawn_source(&world, entity);
world.apply();
assert!(!world.contains(entity));
}
}