use std::collections::{BTreeMap, BTreeSet};
use unlit_ecs::{Entity, World};
use unlit_wgpu::scene::{Scene, ViewportRect};
use crate::components::RenderLoadOps;
use crate::order::{OrderWarnings, Ordering};
use crate::pass::{FramePass, FramePassOrder, PassInfo};
use crate::source::{FrameSourceOrder, RenderContext, Source};
pub struct Renderer {
context: RenderContext,
passes: Ordering<FramePassOrder>,
pass_warnings: OrderWarnings<FramePassOrder>,
sources: Ordering<FrameSourceOrder>,
source_warnings: OrderWarnings<FrameSourceOrder>,
scenes: BTreeMap<(Entity, Entity), SceneSlot>,
placement_warnings: PlacementWarnings,
reached: BTreeSet<Entity>,
unreached: Vec<Entity>,
}
#[derive(Default)]
struct SceneSlot {
scene: Scene,
used: bool,
}
#[derive(Default)]
struct PlacementWarnings {
reported: Vec<Entity>,
}
impl PlacementWarnings {
fn check(&mut self, unreached: &[Entity]) -> bool {
if self.reported == unreached {
return false;
}
if !unreached.is_empty() {
log::warn!(
"{} sources are reached by no pass, so nothing draws them: entities {:?}; move each one under the pass it belongs to, or into the shared subtree to draw it into every pass",
unreached.len(),
unreached
);
}
self.reported.clear();
self.reported.extend_from_slice(unreached);
!unreached.is_empty()
}
}
impl Renderer {
pub fn new(ctx: RenderContext) -> Self {
Self {
context: ctx,
passes: Ordering::default(),
pass_warnings: OrderWarnings::default(),
sources: Ordering::default(),
source_warnings: OrderWarnings::default(),
scenes: BTreeMap::new(),
placement_warnings: PlacementWarnings::default(),
reached: BTreeSet::new(),
unreached: Vec::new(),
}
}
pub fn context(&self) -> RenderContext {
self.context
}
pub fn render(&mut self, world: &World) {
profiling::scope!("renderer.frame");
let device = world
.root_scope()
.query_one::<&wgpu::Device>(self.context.device)
.expect("the context's device resource exists")
.clone();
let queue = world
.root_scope()
.query_one::<&wgpu::Queue>(self.context.queue)
.expect("the context's queue resource exists")
.clone();
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("unlit3d::encoder"),
});
for slot in self.scenes.values_mut() {
slot.used = false;
}
self.reached.clear();
self.unreached.clear();
{
profiling::scope!("renderer.frame.build_sources");
let context = self.context;
let root = world.root_scope();
for (pass_entity, pass) in root.query::<(Entity, &FramePass)>() {
let Some(info) = self.pass_info(world, &pass) else {
continue;
};
let pass_scope = world.scope(pass_entity).unwrap_or(root);
for (entity, mut source) in pass_scope.query::<(Entity, &mut Source)>() {
let scope = world.scope(entity).unwrap_or(pass_scope);
let scene = self.scene_for(pass_entity, entity);
source.build_scene(&scope, context, info, scene, &mut encoder);
}
}
}
{
profiling::scope!("renderer.frame.check_placement");
self.note_placement(world);
}
{
profiling::scope!("renderer.frame.resolve_passes");
self.passes.resolve(
world
.root_scope()
.query::<(Entity, &FramePass)>()
.map(|(entity, pass)| (pass.order, entity)),
);
self.pass_warnings.check(self.passes.ambiguous());
}
for index in 0..self.passes.entities().len() {
let pass_entity = self.passes.entities()[index];
let root = world.root_scope();
let Ok(pass) = root.query_one::<&FramePass>(pass_entity) else {
continue;
};
let Some(attachments) = pass.attachments(world, self.context) else {
continue;
};
let pass_scope = world.scope(pass_entity).unwrap_or(root);
{
profiling::scope!("renderer.frame.resolve_sources");
self.sources.resolve(
pass_scope
.query::<(Entity, &Source)>()
.map(|(entity, source)| (source.order(), entity)),
);
self.source_warnings
.check_for(pass_entity, self.sources.ambiguous());
}
{
profiling::scope!("renderer.frame.record_pass");
let whole =
ViewportRect::new(attachments.width() as f32, attachments.height() as f32);
let load = root
.query_one::<&RenderLoadOps>(pass_entity)
.map_or_else(|_| RenderLoadOps::default(), |load| *load);
let mut render_pass =
attachments.begin_pass(&mut encoder, load.color, load.depth, load.stencil);
for index in 0..self.sources.entities().len() {
let entity = self.sources.entities()[index];
if let Some(scene) = self.scene_slot(pass_entity, entity) {
scene.record(&mut render_pass, Some(whole));
}
}
}
}
self.scenes.retain(|_, slot| slot.used);
self.source_warnings.retain_owners(self.passes.entities());
{
profiling::scope!("renderer.frame.submit");
queue.submit([encoder.finish()]);
}
}
fn scene_for(&mut self, pass: Entity, source: Entity) -> &mut Scene {
let slot = self.scenes.entry((pass, source)).or_default();
slot.used = true;
slot.scene.draws.clear();
&mut slot.scene
}
fn scene_slot(&self, pass: Entity, source: Entity) -> Option<&Scene> {
self.scenes
.get(&(pass, source))
.filter(|slot| slot.used)
.map(|slot| &slot.scene)
}
fn note_placement(&mut self, world: &World) {
let root = world.root_scope();
for (pass_entity, _) in root.query::<(Entity, &FramePass)>() {
let pass_scope = world.scope(pass_entity).unwrap_or(root);
for (entity, _) in pass_scope.query::<(Entity, &Source)>() {
self.reached.insert(entity);
}
}
for (entity, _) in root.query::<(Entity, &Source)>() {
if !self.reached.contains(&entity) {
self.unreached.push(entity);
}
}
self.placement_warnings.check(&self.unreached);
}
fn pass_info(&self, world: &World, pass: &FramePass) -> Option<PassInfo> {
let attachments = pass.attachments(world, self.context)?;
Some(PassInfo {
surface: attachments.surface_key(),
width: attachments.width(),
height: attachments.height(),
viewport: pass.viewport,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use unlit_ecs::Scope;
use crate::pass::{FramePassOrder, Pass};
use crate::source::{FrameSource, spawn_context};
use unlit_wgpu::capabilities::DeviceCapabilities;
struct Nowhere;
impl FrameSource for Nowhere {
fn build_scene(
&mut self,
_scope: &Scope<'_>,
_ctx: RenderContext,
_info: PassInfo,
_scene: &mut Scene,
_encoder: &mut wgpu::CommandEncoder,
) {
}
fn order(&self) -> FrameSourceOrder {
FrameSourceOrder::MESH
}
}
fn world_with_a_pass() -> (World, Renderer, Entity) {
let mut world = World::new();
let (device, queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
let ctx = spawn_context(
&mut world,
device,
queue,
unlit_wgpu::resources::ResourceGraph::new(),
DeviceCapabilities::default(),
);
let pass = world.root_scope_mut().spawn(Pass::new(FramePassOrder(0)));
(world, Renderer::new(ctx), pass)
}
#[test]
fn a_parked_pass_reaches_the_sources_under_it() {
let (mut world, mut renderer, pass) = world_with_a_pass();
let under = world
.scope_mut(pass)
.expect("the pass is spawned")
.spawn(unlit_ecs::bundle![Source::new(Nowhere)]);
let beside = world
.root_scope_mut()
.spawn(unlit_ecs::bundle![Source::new(Nowhere)]);
world
.root_scope()
.query_one::<&mut FramePass>(pass)
.map(|mut pass| pass.park())
.expect("the pass is spawned");
renderer.note_placement(&world);
assert!(
!renderer.reached.contains(&beside),
"a source beside the passes is reached by none of them"
);
assert_eq!(
renderer.unreached,
vec![beside],
"a parked pass still reaches the source under it, so only the \
misplaced one is reported"
);
assert!(renderer.reached.contains(&under));
}
}