//! [`RenderPipeline`]: a data-driven, ordered list of composable render passes. //! //! Stage 4's renderer drew everything in one hardcoded pass. Stage 5 generalizes //! that into a list of named [`RenderPass`]es that share one frame's targets and //! run in order. A project enables only the passes it needs — this is the //! mechanism behind *scalable fidelity*: a flat unlit/low-poly look (or a //! stylized post effect) versus a full realistic stack, paying only for the //! passes turned on. //! //! The Stage-4 forward pass is retrofitted onto this as [`ForwardPass`], so the //! default pipeline ([`RenderPipeline::forward`]) is just `[Clear, Forward]` and //! produces pixel-identical output. Later stages add passes (shadows, //! post-process, overlay UI) **without touching the renderer core** — they //! register a pass. use crate::math::{Color, Rect, Transform, Vec2}; use super::{clear_view, Camera, ForwardRenderer, Lighting, RenderObject}; /// Everything one frame's passes operate on: the shared color target and the /// scene view to draw. /// /// Passes share the same `color` target (and, as the pipeline grows, depth and /// intermediate textures), which is what makes them *composable*: a clear pass /// fills the target, the forward pass draws into it, a future post pass reads and /// rewrites it. pub struct FrameContext<'a> { /// The GPU device. pub device: &'a wgpu::Device, /// The GPU queue. pub queue: &'a wgpu::Queue, /// The color target every pass renders into. pub color: &'a wgpu::TextureView, /// Target size in physical pixels (the whole color target the pipeline /// is writing into). pub size: (u32, u32), /// The sub-rectangle of the target that drawing is restricted to, in /// physical pixels (`min` = upper-left, `max` = lower-right). Passes /// configure the wgpu viewport from this and the camera uses its /// aspect ratio for the projection. /// /// `None` means "use the full target" — the default for headless tests /// and for hosts that render to a whole window. The editor sets this to /// the Viewport tab's rect from the docking shell so picking and /// projection align with what the user sees inside the tab rather than /// stretching across the whole window. pub viewport_rect: Option, /// The background clear color (used by [`ClearPass`]). pub clear_color: Color, /// The camera to render from. pub camera: &'a Camera, /// The camera's world placement. pub view_transform: &'a Transform, /// Scene lighting. pub lighting: &'a Lighting, /// The drawables, already culled by the host (e.g. by camera /// [`visibility`](Camera::visibility)). pub objects: &'a [RenderObject<'a>], } impl FrameContext<'_> { /// The viewport rect [`viewport_rect`](Self::viewport_rect) resolves to — /// the explicit sub-rect when set, otherwise the full target. pub fn resolved_viewport(&self) -> Rect { self.viewport_rect.unwrap_or_else(|| { Rect::from_min_size( Vec2::ZERO, Vec2::new(self.size.0.max(1) as f32, self.size.1.max(1) as f32), ) }) } } /// One stage of the frame. Implement this to add a custom pass; register it on a /// [`RenderPipeline`]. Passes are owned by the pipeline and run in order. pub trait RenderPass { /// Records this pass's GPU work for the frame. fn run(&mut self, frame: &mut FrameContext<'_>); } struct PassEntry { name: String, enabled: bool, pass: Box, } /// An ordered, named list of render passes. /// /// Add passes with [`add_pass`](Self::add_pass), toggle them with /// [`set_enabled`](Self::set_enabled), or drop them with [`remove`](Self::remove) /// — all without touching any pass's implementation. [`render`](Self::render) /// runs every enabled pass in order against one [`FrameContext`]. #[derive(Default)] pub struct RenderPipeline { passes: Vec, } impl RenderPipeline { /// An empty pipeline (no passes). pub fn new() -> Self { Self::default() } /// The default forward pipeline: a [`ClearPass`] followed by a /// [`ForwardPass`]. Pixel-identical to the Stage-4 renderer's output. pub fn forward(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self { let mut pipeline = Self::new(); pipeline.add_pass("clear", ClearPass); pipeline.add_pass("forward", ForwardPass::new(device, color_format)); pipeline } /// Appends a named pass (enabled). Replaces any existing pass with the same /// name, keeping its position. pub fn add_pass(&mut self, name: impl Into, pass: impl RenderPass + 'static) { let name = name.into(); let entry = PassEntry { name: name.clone(), enabled: true, pass: Box::new(pass), }; match self.passes.iter_mut().find(|e| e.name == name) { Some(existing) => *existing = entry, None => self.passes.push(entry), } } /// Inserts a pass before the pass named `before` (or at the end if not /// found). Useful for slotting a post effect into a fixed position. pub fn insert_before( &mut self, before: &str, name: impl Into, pass: impl RenderPass + 'static, ) { let entry = PassEntry { name: name.into(), enabled: true, pass: Box::new(pass), }; match self.passes.iter().position(|e| e.name == before) { Some(index) => self.passes.insert(index, entry), None => self.passes.push(entry), } } /// Enables or disables the named pass. Returns whether it exists. pub fn set_enabled(&mut self, name: &str, enabled: bool) -> bool { match self.passes.iter_mut().find(|e| e.name == name) { Some(entry) => { entry.enabled = enabled; true } None => false, } } /// Removes the named pass. Returns whether it existed. pub fn remove(&mut self, name: &str) -> bool { let before = self.passes.len(); self.passes.retain(|e| e.name != name); self.passes.len() != before } /// Whether a pass with this name is registered. pub fn has_pass(&self, name: &str) -> bool { self.passes.iter().any(|e| e.name == name) } /// The pass names in execution order. pub fn pass_names(&self) -> impl Iterator { self.passes.iter().map(|e| e.name.as_str()) } /// Runs every enabled pass in order against `frame`. pub fn render(&mut self, frame: &mut FrameContext<'_>) { for entry in &mut self.passes { if entry.enabled { entry.pass.run(frame); } } } } /// A pass that clears the color target to [`FrameContext::clear_color`]. /// /// Conventionally the first pass, so later passes load over the cleared /// background (matching the Stage-4 clear-then-draw flow). pub struct ClearPass; impl RenderPass for ClearPass { fn run(&mut self, frame: &mut FrameContext<'_>) { clear_view(frame.device, frame.queue, frame.color, frame.clear_color); } } /// A pass that draws the frame's objects with the lit forward renderer. /// /// Wraps the Stage-4 [`ForwardRenderer`]; the color target is *loaded* (so a /// preceding [`ClearPass`] shows through), depth is managed internally. pub struct ForwardPass { renderer: ForwardRenderer, } impl ForwardPass { /// Builds a forward pass for the given color target format. pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self { Self { renderer: ForwardRenderer::new(device, color_format), } } /// The wrapped renderer's color format. pub fn color_format(&self) -> wgpu::TextureFormat { self.renderer.color_format() } } impl RenderPass for ForwardPass { fn run(&mut self, frame: &mut FrameContext<'_>) { self.renderer.render( frame.device, frame.queue, frame.color, frame.size, frame.resolved_viewport(), frame.camera, frame.view_transform, frame.lighting, frame.objects, ); } }