1use serde::Serialize;
3
4use super::{BufferId, PassWork, RenderGraph, RenderPass, TargetView};
5use crate::shader::{BufferSource, TextureSource};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum LightingPort {
11 GBuffer(u32),
13 SceneDepth,
14 DiffuseLight,
15 SpecularLight,
16 ShadowMap,
18 ScreenSpaceShadows,
20 AmbientOcclusion,
22 HdrColour,
24}
25
26#[derive(Clone, Debug, PartialEq, Serialize)]
28pub struct LightingBuffer {
29 pub buffer: BufferId,
30 pub source: BufferSource,
31}
32
33#[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#[derive(Clone, Debug, Default, PartialEq, Serialize)]
51pub struct LightingPipeline {
52 pub imports: Vec<(LightingPort, TargetView)>,
53 pub buffers: Vec<LightingBuffer>,
54 pub passes: Vec<u32>,
56 pub exports: Vec<(LightingPort, TargetView)>,
57 pub geometry_stencil: Option<GeometryStencil>,
58 pub notices: Vec<String>,
60}
61
62impl LightingPipeline {
63 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 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}