Skip to main content

primase_core/render_graph/
lighting.rs

1//! A lighting subgraph's typed ports. Passes and their programs remain in the source's RenderGraph.
2use serde::Serialize;
3
4use super::{BufferId, PassWork, RenderGraph, RenderPass, TargetView};
5use crate::shader::{BufferSource, TextureSource};
6
7/// The meaning of an image at the lighting boundary, independent of engine names and register numbers.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum LightingPort {
11    /// The raw material output at this attachment, without decoding or re-encoding its surface.
12    GBuffer(u32),
13    SceneDepth,
14    DiffuseLight,
15    SpecularLight,
16    /// A light-space depth image sampled with a comparison sampler.
17    ShadowMap,
18    /// A screen-space shadow term sampled with an ordinary sampler.
19    ScreenSpaceShadows,
20    /// A screen-space occlusion term, independent of shadow visibility.
21    AmbientOcclusion,
22    /// Linear lit colour before tone mapping and display encoding.
23    HdrColour,
24}
25
26/// An externally supplied graph buffer. The selected passes must not also write it.
27#[derive(Clone, Debug, PartialEq, Serialize)]
28pub struct LightingBuffer {
29    pub buffer: BufferId,
30    pub source: BufferSource,
31}
32
33/// An explicit source approximation for geometry stencil before the lighting stages. A consumer writes `value` under
34/// `write_mask` wherever the view's depth differs from far depth; lighting states whose reference is unstated use
35/// `reference`. The reason is always reported, never presented as an observed producer.
36#[derive(Clone, Debug, PartialEq, Serialize)]
37pub struct GeometryStencil {
38    pub target: TargetView,
39    pub value: u32,
40    pub write_mask: u32,
41    pub reference: u32,
42    pub why: String,
43}
44
45/// The part of a source graph a consumer runs between its material draws and presentation.
46/// Its programs compute in the source's program frame ([`crate::shader::ProgramFrame`]), as its material programs do,
47/// so the G-buffer it imports is already in that frame: a consumer converts its constants by their kinds, not its images.
48/// The source selects passes; a consumer never chooses programs by their names. Multiple input views can read the
49/// same port (a source may keep two views of scene depth), but each exported meaning has one view.
50#[derive(Clone, Debug, Default, PartialEq, Serialize)]
51pub struct LightingPipeline {
52    pub imports: Vec<(LightingPort, TargetView)>,
53    pub buffers: Vec<LightingBuffer>,
54    /// Top-level RenderGraph pass indices in strictly increasing source order; groups retain their own order.
55    pub passes: Vec<u32>,
56    pub exports: Vec<(LightingPort, TargetView)>,
57    pub geometry_stencil: Option<GeometryStencil>,
58    /// Source explanations of deliberate reconstruction or approximation, shown by every consumer.
59    pub notices: Vec<String>,
60}
61
62impl LightingPipeline {
63    /// Verify the port types, source order and dependency closure of the selected subgraph. Every read image/buffer
64    /// must be imported or produced earlier. This checks data dependencies, not whether the consumer implements each
65    /// pass or can evaluate an unknown presence condition.
66    pub fn check(&self, graph: &RenderGraph) -> Result<(), String> {
67        let mut images = Vec::new();
68        for (port, view) in self.imports.iter().chain(&self.exports) {
69            graph.view_ok(view, "lighting port")?;
70            let depth = graph.targets[view.target as usize].format.is_depth();
71            if depth != matches!(port, LightingPort::SceneDepth | LightingPort::ShadowMap) {
72                return Err(format!(
73                    "lighting port {port:?} has an incompatible target format"
74                ));
75            }
76            if *port == LightingPort::HdrColour
77                && !matches!(
78                    graph.targets[view.target as usize].format,
79                    super::TargetFormat::Rgba16Float
80                        | super::TargetFormat::Rgba32Float
81                        | super::TargetFormat::Rg11B10Float
82                )
83            {
84                return Err("lighting HDR colour needs a linear floating-point colour target".into());
85            }
86        }
87        for (_, view) in &self.imports {
88            if images.contains(view) {
89                return Err("a lighting image is imported twice".into());
90            }
91            images.push(*view);
92        }
93        let mut buffers = Vec::new();
94        for input in &self.buffers {
95            let buffer = graph
96                .buffers
97                .get(input.buffer as usize)
98                .ok_or_else(|| format!("lighting imports absent buffer {}", input.buffer))?;
99            if buffers.contains(&input.buffer) {
100                return Err(format!("lighting imports buffer {} twice", input.buffer));
101            }
102            match &input.source {
103                BufferSource::Lights(table) => table.verify(buffer.stride)?,
104                BufferSource::LightLists(part) => part.verify(buffer.stride)?,
105                BufferSource::Graph(_) => {
106                    return Err("a lighting buffer import refers to another graph buffer".into());
107                }
108                _ => {}
109            }
110            buffers.push(input.buffer);
111        }
112        for (at, index) in self.passes.iter().enumerate() {
113            if at > 0 && *index <= self.passes[at - 1] {
114                return Err("lighting passes are repeated or outside source order".into());
115            }
116            let pass = graph
117                .passes
118                .get(*index as usize)
119                .ok_or_else(|| format!("lighting selects absent pass {index}"))?;
120            dependencies(pass, &mut images, &mut buffers, &self.buffers)?;
121        }
122        if let Some(stencil) = &self.geometry_stencil {
123            graph.view_ok(&stencil.target, "lighting geometry stencil")?;
124            if !matches!(
125                graph.targets[stencil.target.target as usize].format,
126                super::TargetFormat::Depth32Stencil8 | super::TargetFormat::Depth24Stencil8
127            ) {
128                return Err("lighting geometry stencil needs a depth/stencil image".into());
129            }
130            if stencil.value > 255
131                || stencil.write_mask > 255
132                || stencil.reference > 255
133                || stencil.why.trim().is_empty()
134            {
135                return Err("lighting geometry stencil has invalid bits or no reason".into());
136            }
137            image_read(
138                stencil.target,
139                &self.imports.iter().map(|(_, view)| *view).collect::<Vec<_>>(),
140                "lighting geometry stencil",
141            )?;
142        }
143        let mut ports = Vec::new();
144        for (port, view) in &self.exports {
145            if ports.contains(port) {
146                return Err(format!("lighting exports {port:?} twice"));
147            }
148            ports.push(*port);
149            image_read(*view, &images, "lighting export")?;
150        }
151        if !ports.contains(&LightingPort::HdrColour) {
152            return Err("lighting exports no linear HDR colour".into());
153        }
154        Ok(())
155    }
156}
157
158fn image_read(view: TargetView, images: &[TargetView], what: &str) -> Result<(), String> {
159    if images.iter().any(|available| {
160        available.target == view.target
161            && available.mip == view.mip
162            && available.first_layer <= view.first_layer
163            && (available.layers == 0
164                || (view.layers != 0
165                    && u64::from(view.first_layer) + u64::from(view.layers)
166                        <= u64::from(available.first_layer) + u64::from(available.layers)))
167    }) {
168        Ok(())
169    } else {
170        Err(format!(
171            "{what} reads target {} mip {} before it is imported or produced",
172            view.target, view.mip
173        ))
174    }
175}
176
177fn dependencies(
178    pass: &RenderPass,
179    images: &mut Vec<TargetView>,
180    buffers: &mut Vec<BufferId>,
181    imports: &[LightingBuffer],
182) -> Result<(), String> {
183    let mut writes = Vec::new();
184    let mut buffer_writes = Vec::new();
185    let mut stages = Vec::new();
186    match &pass.work {
187        PassWork::Screen {
188            stages: bound,
189            outputs,
190            ..
191        }
192        | PassWork::Geometry {
193            stages: bound,
194            outputs,
195            ..
196        } => {
197            stages.extend(bound);
198            writes.extend(outputs.colour.iter().map(|(_, v)| *v));
199            if let Some(depth) = &outputs.depth {
200                if depth.read_only_depth {
201                    image_read(depth.view, images, &pass.name)?;
202                } else {
203                    writes.push(depth.view);
204                }
205            }
206        }
207        PassWork::Compute {
208            stage,
209            writes: targets,
210            buffer_writes: bound,
211            ..
212        } => {
213            stages.push(stage);
214            writes.extend(targets.iter().map(|(_, view)| *view));
215            buffer_writes.extend(bound.iter().map(|(_, id)| *id));
216        }
217        PassWork::Clear { target, .. } => writes.push(*target),
218        PassWork::Copy { from, to } => {
219            image_read(*from, images, &pass.name)?;
220            writes.push(*to);
221        }
222        PassWork::ClearBuffer { buffer, .. } => buffer_writes.push(*buffer),
223        PassWork::CopyBuffer { from, to } => {
224            if !buffers.contains(from) {
225                return Err(format!("{} reads unproduced buffer {from}", pass.name));
226            }
227            buffer_writes.push(*to);
228        }
229        PassWork::Group { steps } => {
230            for step in steps {
231                dependencies(step, images, buffers, imports)?;
232            }
233        }
234        // a local light's shadow casters: the consumer draws the scene's casters from each face into the atlas
235        PassWork::Scene {
236            view: super::SceneView::LocalShadow,
237            outputs,
238            frame,
239            ..
240        } => {
241            for (_, view) in frame {
242                image_read(*view, images, &pass.name)?;
243            }
244            writes.extend(outputs.colour.iter().map(|(_, v)| *v));
245            if let Some(depth) = &outputs.depth {
246                writes.push(depth.view);
247            }
248        }
249        PassWork::Scene { .. } | PassWork::Slot { .. } => {
250            return Err(format!(
251                "{} requires scene draws inside the lighting subgraph",
252                pass.name
253            ));
254        }
255    }
256    for stage in stages {
257        for texture in &stage.textures {
258            if let TextureSource::Target { view } = texture.source {
259                image_read(view, images, &pass.name)?;
260            }
261        }
262        for buffer in &stage.buffers {
263            if let BufferSource::Graph(id) = buffer.source
264                && !buffers.contains(&id)
265            {
266                return Err(format!(
267                    "{} reads buffer {id} before it is imported or produced",
268                    pass.name
269                ));
270            }
271        }
272    }
273    for id in buffer_writes {
274        if imports.iter().any(|input| input.buffer == id) {
275            return Err(format!("{} writes imported lighting buffer {id}", pass.name));
276        }
277        buffers.push(id);
278    }
279    images.extend(writes);
280    Ok(())
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use crate::render_graph::{
287        BufferSize, Presence, RenderBuffer, RenderTarget, Runs, TargetFormat, TargetShape, TargetSize,
288    };
289
290    fn view(target: u32) -> TargetView {
291        TargetView {
292            target,
293            mip: 0,
294            first_layer: 0,
295            layers: 0,
296        }
297    }
298    fn graph() -> RenderGraph {
299        let target = |name: &str, format| RenderTarget {
300            name: name.into(),
301            format,
302            size: TargetSize::Fixed {
303                width: 2,
304                height: 2,
305                depth: 1,
306            },
307            shape: TargetShape::Plane,
308            mips: 1,
309            history: false,
310            shared: false,
311        };
312        let pass = |name: &str, work| RenderPass {
313            name: name.into(),
314            shared: false,
315            presence: Presence::Always,
316            runs: Runs::Once,
317            work,
318        };
319        RenderGraph {
320            targets: vec![
321                target("raw material", TargetFormat::Rgba16Float),
322                target("linear lit", TargetFormat::Rgba16Float),
323            ],
324            passes: vec![pass(
325                "handoff",
326                PassWork::Copy {
327                    from: view(0),
328                    to: view(1),
329                },
330            )],
331            ..RenderGraph::default()
332        }
333    }
334    fn pipeline() -> LightingPipeline {
335        LightingPipeline {
336            imports: vec![(LightingPort::GBuffer(0), view(0))],
337            passes: vec![0],
338            exports: vec![(LightingPort::HdrColour, view(1))],
339            ..LightingPipeline::default()
340        }
341    }
342    #[test]
343    fn lighting_ports_require_a_closed_ordered_hdr_handoff() {
344        let g = graph();
345        let p = pipeline();
346        assert_eq!(p.check(&g), Ok(()));
347        let mut missing = p.clone();
348        missing.imports.clear();
349        assert!(missing.check(&g).unwrap_err().contains("before it is imported"));
350        let mut order = p.clone();
351        order.passes.push(0);
352        assert!(order.check(&g).unwrap_err().contains("source order"));
353        let mut output = p.clone();
354        output.exports[0].0 = LightingPort::DiffuseLight;
355        assert!(output.check(&g).unwrap_err().contains("no linear HDR"));
356        let mut wrong = p.clone();
357        wrong.imports[0].0 = LightingPort::SceneDepth;
358        assert!(wrong.check(&g).unwrap_err().contains("incompatible target"));
359        let mut invalid = p.clone();
360        invalid.exports.push((LightingPort::HdrColour, view(0)));
361        assert!(invalid.check(&g).unwrap_err().contains("exports HdrColour twice"));
362        let mut display = g.clone();
363        display.targets[1].format = TargetFormat::Rgba8UnormSrgb;
364        assert!(p.check(&display).unwrap_err().contains("linear floating-point"));
365    }
366
367    #[test]
368    fn a_graph_buffer_cannot_have_two_selected_producers() {
369        let mut g = graph();
370        g.buffers.push(RenderBuffer {
371            name: "list".into(),
372            size: BufferSize::Fixed { bytes: 16 },
373            stride: Some(4),
374            elements: Some(4),
375            history: false,
376            shared: false,
377        });
378        g.passes[0].work = PassWork::ClearBuffer {
379            buffer: 0,
380            value: [0; 4],
381        };
382        let mut p = pipeline();
383        p.buffers.push(LightingBuffer {
384            buffer: 0,
385            source: BufferSource::Unknown {
386                why: "external source".into(),
387            },
388        });
389        assert!(
390            p.check(&g)
391                .unwrap_err()
392                .contains("writes imported lighting buffer")
393        );
394        p.buffers[0].source = BufferSource::Graph(0);
395        assert!(p.check(&g).unwrap_err().contains("another graph buffer"));
396    }
397}