use core::any::Any;
use core::ops::DerefMut;
use unlit_ecs::bundle;
use unlit_ecs::{Bundle, DynamicBundle, Entity, EntityAccessError, Scope, ScopeMut, World};
use unlit_wgpu::capabilities::DeviceCapabilities;
use unlit_wgpu::resources::ResourceGraph;
use unlit_wgpu::scene::Scene;
use crate::frame::FrameClock;
use crate::order::OrderKey;
use crate::pass::PassInfo;
#[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);
}
impl OrderKey for FrameSourceOrder {
const ORDER: &'static str = "FrameSourceOrder";
const SUBJECT: &'static str = "sources";
fn value(self) -> i32 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RenderContext {
pub device: Entity,
pub queue: Entity,
pub graph: Entity,
pub capabilities: Entity,
pub clock: Entity,
}
impl RenderContext {
pub fn graph<'w>(self, scope: &'w Scope<'w>) -> impl DerefMut<Target = ResourceGraph> + 'w {
scope
.query_one::<&mut ResourceGraph>(self.graph)
.expect("the context's resource graph exists")
}
pub fn maintain_scope<'w>(self, scope: Scope<'w>) -> MaintainScope<'w> {
MaintainScope { scope, ctx: self }
}
}
impl RenderContext {
pub fn of(world: &World, anchor: Entity) -> Result<Self, EntityAccessError> {
world
.root_scope()
.query_one::<&RenderContext>(anchor)
.map(|context| *context)
}
}
#[must_use = "the graph is maintained when the scope is dropped"]
pub struct MaintainScope<'w> {
scope: Scope<'w>,
ctx: RenderContext,
}
impl Drop for MaintainScope<'_> {
fn drop(&mut self) {
if let Ok(mut graph) = self.scope.query_one::<&mut ResourceGraph>(self.ctx.graph) {
graph.maintain();
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OpenUrl {
pub url: String,
pub new_tab: bool,
}
#[derive(Debug, Default)]
pub struct OpenUrls(Vec<OpenUrl>);
impl OpenUrls {
pub fn push(&mut self, url: impl Into<String>, new_tab: bool) {
self.0.push(OpenUrl {
url: url.into(),
new_tab,
});
}
pub fn replace(&mut self, urls: impl IntoIterator<Item = OpenUrl>) {
self.0.clear();
self.0.extend(urls);
}
}
pub fn drain_open_urls(world: &World) -> Vec<OpenUrl> {
let Some(entity) = world
.root_scope()
.query::<(Entity, &OpenUrls)>()
.next()
.map(|(e, _)| e)
else {
return Vec::new();
};
world
.root_scope()
.query_one::<&mut OpenUrls>(entity)
.map(|mut urls| std::mem::take(&mut urls.0))
.unwrap_or_default()
}
pub trait FrameSource: 'static {
fn build_scene(
&mut self,
scope: &Scope<'_>,
ctx: RenderContext,
info: PassInfo,
scene: &mut Scene,
encoder: &mut wgpu::CommandEncoder,
);
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>,
}
impl Source {
pub fn new(source: impl FrameSource) -> Self {
Self {
source: Box::new(source),
order: None,
}
}
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 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,
scope: &Scope<'_>,
ctx: RenderContext,
info: PassInfo,
scene: &mut Scene,
encoder: &mut wgpu::CommandEncoder,
) {
self.source.build_scene(scope, ctx, info, scene, encoder);
}
}
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())
.finish_non_exhaustive()
}
}
struct ContextRow<T> {
resource: T,
context: RenderContext,
}
impl<T: 'static> Bundle for ContextRow<T> {
fn into_bundle(self) -> DynamicBundle {
bundle![self.resource, self.context]
}
}
fn spawn_row<T: 'static>(
shared: &mut ScopeMut<'_>,
entity: Entity,
resource: T,
context: RenderContext,
) {
shared
.spawn_at(entity, ContextRow { resource, context })
.expect("the handle was just reserved");
}
pub fn spawn_context(
world: &mut World,
device: wgpu::Device,
queue: wgpu::Queue,
graph_component: ResourceGraph,
capabilities: DeviceCapabilities,
) -> RenderContext {
let graph = world.reserve_entity("render_graph");
let context = RenderContext {
device: world.reserve_entity("gpu_device"),
queue: world.reserve_entity("gpu_queue"),
graph,
capabilities: world.reserve_entity("device_capabilities"),
clock: world.reserve_entity("frame_clock"),
};
let shared = world.shared();
let mut shared_view = world
.scope_mut(shared)
.expect("the shared scope exists from the moment the world is built");
spawn_row(&mut shared_view, context.device, device, context);
spawn_row(&mut shared_view, context.queue, queue, context);
spawn_row(
&mut shared_view,
context.capabilities,
capabilities,
context,
);
spawn_row(&mut shared_view, graph, graph_component, context);
shared_view.spawn_named("open_urls", bundle![OpenUrls::default()]);
spawn_row(
&mut shared_view,
context.clock,
FrameClock::default(),
context,
);
context
}
#[cfg(test)]
mod tests {
use super::*;
use unlit_wgpu::specialize::SurfaceKey;
use crate::order::{AmbiguousGroup, OrderWarnings, Ordering};
fn test_info() -> PassInfo {
PassInfo {
surface: SurfaceKey {
color_format: wgpu::TextureFormat::Rgba8UnormSrgb,
depth_stencil_format: None,
sample_count: 1,
},
width: 128,
height: 96,
viewport: None,
}
}
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>>,
}
impl RecordingSource {
fn new(order: FrameSourceOrder) -> Self {
Self {
order,
built: Rc::new(Cell::new(0)),
}
}
}
impl FrameSource for RecordingSource {
fn build_scene(
&mut self,
_view: &Scope<'_>,
_ctx: RenderContext,
_info: PassInfo,
_scene: &mut Scene,
_encoder: &mut wgpu::CommandEncoder,
) {
self.built.set(self.built.get() + 1);
}
fn order(&self) -> FrameSourceOrder {
self.order
}
}
use std::cell::{Cell, RefCell};
use std::rc::Rc;
fn record_order(world: &World) -> (Vec<Entity>, Vec<AmbiguousGroup<FrameSourceOrder>>) {
let mut resolved = Ordering::<FrameSourceOrder>::default();
resolved.resolve(
world
.root_scope()
.query::<(Entity, &Source)>()
.map(|(entity, source)| (source.order(), entity)),
);
(resolved.entities().to_vec(), resolved.ambiguous().to_vec())
}
#[test]
fn explicit_order_wins_over_spawn_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
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::UI
)),]);
let mesh = world
.root_scope_mut()
.spawn(bundle![Source::new(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_fall_back_to_entity_order() {
let mut world = World::new();
test_context(&mut world);
let first = world
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::UI
)),]);
let second = world
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::UI
)),]);
let third = world
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::UI
)),]);
assert_eq!(record_order(&world).0, vec![first, second, third]);
world.root_scope_mut().despawn(second).unwrap();
assert_eq!(
record_order(&world).0,
vec![first, third],
"the surviving sources keep their relative order"
);
}
#[test]
fn an_explicit_order_overrides_the_one_the_source_declares() {
let mut world = World::new();
test_context(&mut world);
let a = world.root_scope_mut().spawn(bundle![
Source::new(RecordingSource::new(FrameSourceOrder::MESH))
.with_order(FrameSourceOrder::UI),
]);
let b = world
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::MESH
)),]);
assert_eq!(record_order(&world).0, vec![b, a]);
let _ = world
.root_scope()
.query_one::<&mut Source>(a)
.map(|mut 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;
impl FrameSource for OtherSource {
fn build_scene(
&mut self,
_view: &Scope<'_>,
_ctx: RenderContext,
_info: PassInfo,
_scene: &mut Scene,
_encoder: &mut wgpu::CommandEncoder,
) {
}
fn order(&self) -> FrameSourceOrder {
FrameSourceOrder::MESH
}
}
let mut world = World::new();
test_context(&mut world);
let recording = world
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::MESH
)),]);
let other = world
.root_scope_mut()
.spawn(bundle![Source::new(OtherSource)]);
assert!(
world
.root_scope()
.query_one::<&mut Source>(recording)
.map(|mut s| s.as_mut::<RecordingSource>().is_some())
.unwrap()
);
assert!(
world
.root_scope()
.query_one::<&mut Source>(recording)
.map(|mut s| s.as_mut::<OtherSource>().is_none())
.unwrap()
);
assert!(
world
.root_scope()
.query_one::<&mut Source>(other)
.map(|mut s| s.as_mut::<OtherSource>().is_some())
.unwrap()
);
assert!(
world
.root_scope()
.query_one::<&mut Source>(other)
.map(|s| s.as_ref::<RecordingSource>().is_none())
.unwrap()
);
}
#[test]
fn one_query_builds_every_source_type() {
struct CountingSource(u32);
impl FrameSource for CountingSource {
fn build_scene(
&mut self,
_view: &Scope<'_>,
_ctx: RenderContext,
_info: PassInfo,
_scene: &mut Scene,
_encoder: &mut wgpu::CommandEncoder,
) {
self.0 += 1;
}
fn order(&self) -> FrameSourceOrder {
FrameSourceOrder::UI
}
}
let mut world = World::new();
let ctx = test_context(&mut world);
world
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::MESH
)),]);
world
.root_scope_mut()
.spawn(bundle![Source::new(CountingSource(0))]);
let mut encoder = wgpu::Device::noop(&wgpu::DeviceDescriptor::default())
.0
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
let entities: Vec<Entity> = world
.root_scope()
.query::<(Entity, &Source)>()
.map(|(e, _)| e)
.collect();
assert_eq!(entities.len(), 2, "both sources are seen by one query type");
let mut scene = Scene::new();
for entity in entities {
world
.root_scope()
.query_one::<&mut Source>(entity)
.map(|mut source| {
let scope = world
.scope(entity)
.unwrap_or_else(|_| panic!("a source entity is its own scope: {entity:?}"));
source.build_scene(&scope, ctx, test_info(), &mut scene, &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>>>,
}
impl FrameSource for PhasedSource {
fn build_scene(
&mut self,
_view: &Scope<'_>,
_ctx: RenderContext,
_info: PassInfo,
_scene: &mut Scene,
_encoder: &mut wgpu::CommandEncoder,
) {
self.log.borrow_mut().push(format!("build {}", self.name));
}
fn order(&self) -> FrameSourceOrder {
FrameSourceOrder::MESH
}
}
for name in ["a", "b"] {
world
.root_scope_mut()
.spawn(bundle![Source::new(PhasedSource {
name,
log: Rc::clone(&order),
}),]);
}
let mut encoder = wgpu::Device::noop(&wgpu::DeviceDescriptor::default())
.0
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
let entities: Vec<Entity> = world
.root_scope()
.query::<(Entity, &Source)>()
.map(|(e, _)| e)
.collect();
let mut scene = Scene::new();
for entity in &entities {
world
.root_scope()
.query_one::<&mut Source>(*entity)
.map(|mut source| {
let scope = world
.scope(*entity)
.unwrap_or_else(|_| panic!("a source entity is its own scope: {entity:?}"));
source.build_scene(&scope, ctx, test_info(), &mut scene, &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
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::MESH
)),]);
let b = world
.root_scope_mut()
.spawn(bundle![Source::new(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 entity order");
let mut warnings = OrderWarnings::<FrameSourceOrder>::default();
assert!(warnings.check(&ambiguous), "the first frame reports it");
assert!(!warnings.check(&ambiguous), "a steady frame stays quiet");
let _ = world
.root_scope()
.query_one::<&mut Source>(b)
.map(|mut source| source.set_order(Some(FrameSourceOrder::UI)));
let (_, resolved) = record_order(&world);
assert!(resolved.is_empty());
assert!(!warnings.check(&resolved));
let _ = world
.root_scope()
.query_one::<&mut Source>(b)
.map(|mut 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
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::MESH
)),]);
}
world
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::UI
)),]);
let mut resolved = Ordering::<FrameSourceOrder>::default();
resolved.resolve(
world
.root_scope()
.query::<(Entity, &Source)>()
.map(|(entity, source)| (source.order(), entity)),
);
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
.root_scope()
.query::<(Entity, &Source)>()
.next()
.expect("a source")
.0;
world.root_scope_mut().despawn(first).unwrap();
resolved.resolve(
world
.root_scope()
.query::<(Entity, &Source)>()
.map(|(entity, source)| (source.order(), entity)),
);
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
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::MESH
)),]);
world
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::UI
)),]);
let (order, ambiguous) = record_order(&world);
assert_eq!(order.len(), 2);
assert!(ambiguous.is_empty());
assert!(!OrderWarnings::<FrameSourceOrder>::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
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::UI
)),]);
let b = world
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::UI
)),]);
let messages = capture_logs(|| {
let (_, ambiguous) = record_order(&world);
let mut warnings = OrderWarnings::<FrameSourceOrder>::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
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::MESH
)),]);
world
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::UI
)),]);
let messages = capture_logs(|| {
let (_, ambiguous) = record_order(&world);
assert!(!OrderWarnings::<FrameSourceOrder>::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
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::MESH
)),]);
let _b = world
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::MESH
)),]);
let mut warnings = OrderWarnings::<FrameSourceOrder>::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
.root_scope()
.query_one::<&mut Source>(a)
.map(|mut 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
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::MESH
)),]);
let b = world
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::MESH
)),]);
let mut warnings = OrderWarnings::<FrameSourceOrder>::default();
let messages = capture_logs(|| {
for _ in 0..3 {
let _ = world
.root_scope()
.query_one::<&mut Source>(a)
.map(|mut source| source.set_order(Some(FrameSourceOrder::MESH)));
let _ = world
.root_scope()
.query_one::<&mut Source>(b)
.map(|mut 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
.root_scope_mut()
.spawn(bundle![Source::new(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 later_entities_break_a_tie_last() {
let mut world = World::new();
test_context(&mut world);
let ids: Vec<Entity> = (0..3)
.map(|_| {
world
.root_scope_mut()
.spawn(bundle![Source::new(RecordingSource::new(
FrameSourceOrder::MESH
)),])
})
.collect();
assert_eq!(record_order(&world).0, ids);
}
}
#[cfg(test)]
mod release_tests {
use super::*;
use unlit_wgpu::resources::{ResHandle, Resource as GraphResource};
fn test_info() -> PassInfo {
PassInfo {
surface: unlit_wgpu::specialize::SurfaceKey {
color_format: wgpu::TextureFormat::Rgba8UnormSrgb,
depth_stencil_format: None,
sample_count: 1,
},
width: 128,
height: 96,
viewport: None,
}
}
struct OwningSource {
context: RenderContext,
node: Option<ResHandle>,
}
impl FrameSource for OwningSource {
fn build_scene(
&mut self,
scope: &Scope<'_>,
ctx: RenderContext,
_info: PassInfo,
_scene: &mut Scene,
_encoder: &mut wgpu::CommandEncoder,
) {
self.context = ctx;
if self.node.is_none() {
self.node = Some(
scope
.query_one::<&mut ResourceGraph>(ctx.graph)
.expect("the context's graph exists")
.insert(GraphResource::Virtual, None),
);
}
}
fn order(&self) -> FrameSourceOrder {
FrameSourceOrder::UI
}
}
struct EarlyReturnSource {
context: RenderContext,
node: Option<ResHandle>,
}
impl FrameSource for EarlyReturnSource {
fn build_scene(
&mut self,
scope: &Scope<'_>,
ctx: RenderContext,
_info: PassInfo,
_scene: &mut Scene,
_encoder: &mut wgpu::CommandEncoder,
) {
let _maintain = ctx.maintain_scope(*scope);
self.context = ctx;
if self.node.is_none() {
self.node = Some(
scope
.query_one::<&mut ResourceGraph>(ctx.graph)
.expect("the context's graph exists")
.insert(GraphResource::Virtual, None),
);
} else {
self.node = None;
}
}
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 });
let mut scene = Scene::new();
world
.root_scope()
.query_one::<&mut Source>(entity)
.map(|mut source| {
let scope = world
.scope(entity)
.unwrap_or_else(|_| panic!("a source entity is its own scope: {entity:?}"));
source.build_scene(&scope, ctx, test_info(), &mut scene, &mut encoder)
})
.unwrap_or_else(|_| panic!("the source entity exists: {entity:?}"));
}
fn graph_len(world: &World, ctx: RenderContext) -> usize {
world
.root_scope()
.query_one::<&mut ResourceGraph>(ctx.graph)
.unwrap()
.len()
}
fn maintain(world: &mut World, ctx: RenderContext) {
world
.root_scope()
.query_one::<&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
.root_scope_mut()
.spawn(bundle![Source::new(OwningSource {
context: ctx,
node: None,
}),]);
build_once(&mut world, entity, ctx);
assert_eq!(
graph_len(&world, ctx),
1,
"the source registered exactly the node it owns"
);
world.root_scope_mut().despawn(entity).unwrap();
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
.root_scope_mut()
.spawn(bundle![Source::new(OwningSource {
context: ctx,
node: None,
}),]);
world.root_scope_mut().despawn(entity).unwrap();
assert!(!world.root_scope().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
.root_scope_mut()
.spawn(bundle![Source::new(OwningSource {
context: ctx,
node: None,
}),]),
);
}
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.root_scope_mut().despawn(entity).unwrap();
}
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
.root_scope_mut()
.spawn(bundle![Source::new(EarlyReturnSource {
context: ctx,
node: None,
}),]);
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"
);
}
}