use core::any::Any;
use unlit_ecs::{Entity, World};
use unlit_wgpu::capabilities::DeviceCapabilities;
use unlit_wgpu::resources::ResourceGraph;
use unlit_wgpu::scene::{Scene, ViewportRect};
use unlit_wgpu::specialize::SurfaceKey;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct FrameSourceOrder(pub i32);
impl FrameSourceOrder {
pub const MESH: Self = Self(0);
pub const UI: Self = Self(100);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RenderContext {
pub device: Entity,
pub queue: Entity,
pub graph: Entity,
pub capabilities: Entity,
}
impl RenderContext {
pub fn maintain_scope<'w>(self, world: &'w World) -> MaintainScope<'w> {
MaintainScope { world, ctx: self }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "reflect", derive(facet::Facet))]
pub struct RenderContextInfo {
#[cfg_attr(feature = "reflect", facet(opaque, proxy = unlit_ecs::EntityProxy))]
pub device: Entity,
#[cfg_attr(feature = "reflect", facet(opaque, proxy = unlit_ecs::EntityProxy))]
pub queue: Entity,
#[cfg_attr(feature = "reflect", facet(opaque, proxy = unlit_ecs::EntityProxy))]
pub graph: Entity,
#[cfg_attr(feature = "reflect", facet(opaque, proxy = unlit_ecs::EntityProxy))]
pub capabilities: Entity,
}
impl RenderContext {
#[must_use]
pub fn of(world: &World, anchor: Entity) -> Option<Self> {
world.get::<RenderContext>(anchor).map(|context| *context)
}
pub fn info(self) -> RenderContextInfo {
RenderContextInfo {
device: self.device,
queue: self.queue,
graph: self.graph,
capabilities: self.capabilities,
}
}
}
#[must_use = "the graph is maintained when the scope is dropped"]
pub struct MaintainScope<'w> {
world: &'w World,
ctx: RenderContext,
}
impl Drop for MaintainScope<'_> {
fn drop(&mut self) {
if let Some(mut graph) = self.world.get_mut::<ResourceGraph>(self.ctx.graph) {
graph.maintain();
}
}
}
#[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, PartialEq)]
pub struct FrameViewport(pub ViewportRect);
#[derive(Clone, Copy, Debug, Default)]
struct FrameViewportSlot(Option<FrameViewport>);
#[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: &World) -> Option<FrameTarget> {
world
.query::<&FrameTargetSlot>()
.next()
.and_then(|(_, slot)| slot.0)
}
pub fn set_frame_target(world: &World, 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: &World) -> 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 fn frame_viewport(world: &World) -> Option<FrameViewport> {
world
.query::<&FrameViewportSlot>()
.next()
.and_then(|(_, slot)| slot.0)
}
pub fn set_frame_viewport(world: &World, viewport: Option<FrameViewport>) -> bool {
let Some(entity) = world.query::<&FrameViewportSlot>().next().map(|(e, _)| e) else {
return false;
};
world
.with_mut::<FrameViewportSlot, _>(entity, |slot| slot.0 = viewport)
.is_some()
}
pub trait FrameSource: 'static {
fn build_scene(
&mut self,
world: &World,
ctx: RenderContext,
encoder: &mut wgpu::CommandEncoder,
);
fn scene(&self) -> &Scene;
fn order(&self) -> FrameSourceOrder;
}
pub trait AnySource: Any + FrameSource {}
impl<T: FrameSource> AnySource for T {}
pub struct Source {
source: Box<dyn AnySource>,
order: Option<FrameSourceOrder>,
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: FrameSourceOrder) -> Self {
self.order = Some(order);
self
}
pub fn order(&self) -> FrameSourceOrder {
self.order.unwrap_or_else(|| self.source.order())
}
pub fn set_order(&mut self, order: Option<FrameSourceOrder>) {
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: &World,
ctx: RenderContext,
encoder: &mut wgpu::CommandEncoder,
) {
self.source.build_scene(world, ctx, encoder);
}
pub fn scene(&self) -> &Scene {
self.source.scene()
}
}
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 World,
device: wgpu::Device,
queue: wgpu::Queue,
graph_component: ResourceGraph,
capabilities: DeviceCapabilities,
) -> RenderContext {
let graph = world.reserve_entity();
let context = RenderContext {
device: world.reserve_entity(),
queue: world.reserve_entity(),
graph,
capabilities: world.reserve_entity(),
};
world.spawn_at(context.device, (device, context));
world.spawn_at(context.queue, (queue, context));
world.spawn_at(context.capabilities, (capabilities, context));
world.spawn_at(graph, (graph_component, context));
world.spawn((MountCounter::default(),));
world.spawn((FrameTargetSlot::default(),));
world.spawn((FrameViewportSlot::default(),));
world.spawn((InputCapture::default(),));
context
}
pub trait WorldSourceExt {
fn spawn_source(&mut self, source: impl FrameSource) -> Entity;
fn spawn_source_at(&mut self, order: FrameSourceOrder, source: impl FrameSource) -> Entity;
}
impl WorldSourceExt for World {
fn spawn_source(&mut self, source: impl FrameSource) -> Entity {
let source = Source::new(source);
let index = next_mount_index(self);
self.spawn((bump_mount(source, index),))
}
fn spawn_source_at(&mut self, order: FrameSourceOrder, source: impl FrameSource) -> Entity {
let source = Source::new(source).with_order(order);
let index = next_mount_index(self);
self.spawn((bump_mount(source, index),))
}
}
fn next_mount_index(world: &World) -> 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 = (FrameSourceOrder, Vec<Entity>);
#[derive(Debug, Default)]
pub struct SourceOrder {
keys: Vec<(FrameSourceOrder, u64, Entity)>,
order: Vec<Entity>,
ambiguous: Vec<AmbiguousGroup>,
}
impl SourceOrder {
pub fn resolve(&mut self, world: &World) {
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 FrameSourceOrder({}): 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 World) -> RenderContext {
let (device, queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
spawn_context(
world,
device,
queue,
ResourceGraph::new(),
DeviceCapabilities::default(),
)
}
struct RecordingSource {
order: FrameSourceOrder,
built: Rc<Cell<u32>>,
scene: Scene,
}
impl RecordingSource {
fn new(order: FrameSourceOrder) -> Self {
Self {
order,
built: Rc::new(Cell::new(0)),
scene: Scene::new(),
}
}
}
impl FrameSource for RecordingSource {
fn build_scene(
&mut self,
_world: &World,
_ctx: RenderContext,
_encoder: &mut wgpu::CommandEncoder,
) {
self.built.set(self.built.get() + 1);
}
fn scene(&self) -> &Scene {
&self.scene
}
fn order(&self) -> FrameSourceOrder {
self.order
}
}
use std::cell::{Cell, RefCell};
use std::rc::Rc;
fn record_order(world: &World) -> (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 = World::new();
spawn_context(
&mut world,
wgpu::Device::noop(&wgpu::DeviceDescriptor::default()).0,
wgpu::Device::noop(&wgpu::DeviceDescriptor::default()).1,
ResourceGraph::new(),
DeviceCapabilities::default(),
);
let overlay = world.spawn_source(RecordingSource::new(FrameSourceOrder::UI));
let mesh = world.spawn_source(RecordingSource::new(FrameSourceOrder::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 = World::new();
test_context(&mut world);
let first = world.spawn_source(RecordingSource::new(FrameSourceOrder::UI));
let second = world.spawn_source(RecordingSource::new(FrameSourceOrder::UI));
let third = world.spawn_source(RecordingSource::new(FrameSourceOrder::UI));
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 = World::new();
test_context(&mut world);
let a = world.spawn_source_at(
FrameSourceOrder::UI,
RecordingSource::new(FrameSourceOrder::MESH),
);
let b = world.spawn_source(RecordingSource::new(FrameSourceOrder::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: &World,
_ctx: RenderContext,
_encoder: &mut wgpu::CommandEncoder,
) {
}
fn scene(&self) -> &Scene {
&self.0
}
fn order(&self) -> FrameSourceOrder {
FrameSourceOrder::MESH
}
}
let mut world = World::new();
test_context(&mut world);
let recording = world.spawn_source(RecordingSource::new(FrameSourceOrder::MESH));
let other = world.spawn_source(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: &World,
_ctx: RenderContext,
_encoder: &mut wgpu::CommandEncoder,
) {
self.0 += 1;
}
fn scene(&self) -> &Scene {
&self.1
}
fn order(&self) -> FrameSourceOrder {
FrameSourceOrder::UI
}
}
let mut world = World::new();
let ctx = test_context(&mut world);
world.spawn_source(RecordingSource::new(FrameSourceOrder::MESH));
world.spawn_source(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 = World::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: &World,
_ctx: RenderContext,
_encoder: &mut wgpu::CommandEncoder,
) {
self.log.borrow_mut().push(format!("build {}", self.name));
}
fn scene(&self) -> &Scene {
&self.scene
}
fn order(&self) -> FrameSourceOrder {
FrameSourceOrder::MESH
}
}
for name in ["a", "b"] {
world.spawn_source(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(FrameSourceOrder::UI));
assert_eq!(source.order(), FrameSourceOrder::UI);
let mut source = Source::new(RecordingSource::new(FrameSourceOrder::MESH))
.with_order(FrameSourceOrder::UI);
assert_eq!(source.order(), FrameSourceOrder::UI, "the override wins");
source.set_order(None);
assert_eq!(source.order(), FrameSourceOrder::MESH, "and can be dropped");
}
#[test]
fn an_ambiguity_is_reported_once_and_again_only_when_it_changes() {
let mut world = World::new();
test_context(&mut world);
let a = world.spawn_source(RecordingSource::new(FrameSourceOrder::MESH));
let b = world.spawn_source(RecordingSource::new(FrameSourceOrder::MESH));
let (_, ambiguous) = record_order(&world);
assert_eq!(ambiguous.len(), 1, "the two share one order");
assert_eq!(ambiguous[0].0, FrameSourceOrder::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(FrameSourceOrder::UI)));
let (_, resolved) = record_order(&world);
assert!(resolved.is_empty());
assert!(!warnings.check(&resolved));
let _ =
world.with_mut::<Source, _>(b, |source| source.set_order(Some(FrameSourceOrder::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 = World::new();
test_context(&mut world);
for _ in 0..4 {
world.spawn_source(RecordingSource::new(FrameSourceOrder::MESH));
}
world.spawn_source(RecordingSource::new(FrameSourceOrder::UI));
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 = World::new();
test_context(&mut world);
world.spawn_source(RecordingSource::new(FrameSourceOrder::MESH));
world.spawn_source(RecordingSource::new(FrameSourceOrder::UI));
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 = World::new();
test_context(&mut world);
let a = world.spawn_source(RecordingSource::new(FrameSourceOrder::UI));
let b = world.spawn_source(RecordingSource::new(FrameSourceOrder::UI));
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(&FrameSourceOrder::UI.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 = World::new();
test_context(&mut world);
world.spawn_source(RecordingSource::new(FrameSourceOrder::MESH));
world.spawn_source(RecordingSource::new(FrameSourceOrder::UI));
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 = World::new();
test_context(&mut world);
let a = world.spawn_source(RecordingSource::new(FrameSourceOrder::MESH));
let _b = world.spawn_source(RecordingSource::new(FrameSourceOrder::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(FrameSourceOrder::UI)));
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 = World::new();
test_context(&mut world);
let a = world.spawn_source(RecordingSource::new(FrameSourceOrder::MESH));
let b = world.spawn_source(RecordingSource::new(FrameSourceOrder::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(FrameSourceOrder::MESH))
});
let _ = world.with_mut::<Source, _>(b, |source| {
source.set_order(Some(FrameSourceOrder::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 = World::new();
test_context(&mut world);
let ids: Vec<Entity> = (0..3)
.map(|_| world.spawn_source(RecordingSource::new(FrameSourceOrder::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 = World::new();
test_context(&mut world);
let indices: Vec<u64> = (0..3)
.map(|_| {
let entity = world.spawn_source(RecordingSource::new(FrameSourceOrder::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::{ResHandle, Resource as GraphResource};
struct OwningSource {
context: RenderContext,
node: Option<ResHandle>,
scene: Scene,
}
impl FrameSource for OwningSource {
fn build_scene(
&mut self,
world: &World,
ctx: RenderContext,
_encoder: &mut wgpu::CommandEncoder,
) {
self.context = ctx;
if self.node.is_none() {
self.node = Some(
world
.get_mut::<ResourceGraph>(ctx.graph)
.expect("the context's graph exists")
.insert(GraphResource::Virtual, None),
);
}
}
fn scene(&self) -> &Scene {
&self.scene
}
fn order(&self) -> FrameSourceOrder {
FrameSourceOrder::UI
}
}
struct EarlyReturnSource {
context: RenderContext,
node: Option<ResHandle>,
scene: Scene,
}
impl FrameSource for EarlyReturnSource {
fn build_scene(
&mut self,
world: &World,
ctx: RenderContext,
_encoder: &mut wgpu::CommandEncoder,
) {
let _maintain = ctx.maintain_scope(world);
self.context = ctx;
if self.node.is_none() {
self.node = Some(
world
.get_mut::<ResourceGraph>(ctx.graph)
.expect("the context's graph exists")
.insert(GraphResource::Virtual, None),
);
} else {
self.node = None;
}
}
fn scene(&self) -> &Scene {
&self.scene
}
fn order(&self) -> FrameSourceOrder {
FrameSourceOrder::MESH
}
}
fn noop_context(world: &mut World) -> RenderContext {
spawn_context(
world,
wgpu::Device::noop(&wgpu::DeviceDescriptor::default()).0,
wgpu::Device::noop(&wgpu::DeviceDescriptor::default()).1,
ResourceGraph::new(),
DeviceCapabilities::default(),
)
}
fn build_once(world: &mut World, entity: Entity, ctx: RenderContext) {
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");
}
fn graph_len(world: &World, ctx: RenderContext) -> usize {
world.get_mut::<ResourceGraph>(ctx.graph).unwrap().len()
}
fn maintain(world: &mut World, ctx: RenderContext) {
world
.get_mut::<ResourceGraph>(ctx.graph)
.unwrap()
.maintain();
}
#[test]
fn dropping_a_source_releases_the_nodes_it_owns() {
let mut world = World::new();
let ctx = noop_context(&mut world);
let entity = world.spawn_source(OwningSource {
context: ctx,
node: None,
scene: Scene::new(),
});
build_once(&mut world, entity, ctx);
assert_eq!(
graph_len(&world, ctx),
1,
"the source registered exactly the node it owns"
);
world.despawn(entity);
assert_eq!(
graph_len(&world, ctx),
1,
"dropping an id does not itself remove the node"
);
maintain(&mut world, ctx);
assert_eq!(
graph_len(&world, ctx),
0,
"maintain collected the node nothing refers to"
);
}
#[test]
fn removing_a_source_that_owns_nothing_still_removes_it() {
let mut world = World::new();
let ctx = noop_context(&mut world);
let entity = world.spawn_source(OwningSource {
context: ctx,
node: None,
scene: Scene::new(),
});
world.despawn(entity);
assert!(!world.contains(entity));
maintain(&mut world, ctx);
assert_eq!(graph_len(&world, ctx), 0);
}
#[test]
fn one_maintain_drops_the_nodes_every_source_gave_up() {
let mut world = World::new();
let ctx = noop_context(&mut world);
let mut entities = Vec::new();
for _ in 0..2 {
entities.push(world.spawn_source(OwningSource {
context: ctx,
node: None,
scene: Scene::new(),
}));
}
for &entity in &entities {
build_once(&mut world, entity, ctx);
}
assert_eq!(graph_len(&world, ctx), 2, "each source registered a node");
for &entity in &entities {
world.despawn(entity);
}
assert_eq!(
graph_len(&world, ctx),
2,
"dropping an id does not itself remove the node"
);
maintain(&mut world, ctx);
assert_eq!(
graph_len(&world, ctx),
0,
"the single maintain collected every unreferenced node"
);
}
#[test]
fn a_build_that_returns_early_still_maintains() {
let mut world = World::new();
let ctx = noop_context(&mut world);
let entity = world.spawn_source(EarlyReturnSource {
context: ctx,
node: None,
scene: Scene::new(),
});
build_once(&mut world, entity, ctx);
assert_eq!(graph_len(&world, ctx), 1, "the source registered a node");
build_once(&mut world, entity, ctx);
assert_eq!(
graph_len(&world, ctx),
0,
"the early return maintained the graph"
);
}
}