Skip to main content

alpha2_render/
graph_execute.rs

1use std::collections::HashSet;
2use web_time::Instant;
3
4use crate::composite::CompositeTarget;
5use crate::graph::{GraphError, RenderCacheKey, RenderGraph, RenderNode, RenderNodeId};
6use crate::graph_cache::{renderer_cache_key, renderer_node_key};
7use crate::runtime_effect::{EffectPass, PendingEffectTiming};
8use crate::uniform::{
9    BATCH_UNIFORM_FLOATS, RUNTIME_UNIFORM_WORDS, UniformArenaLease, aligned_uniform_end,
10    float_uniform_bytes, param_uniform_bytes,
11};
12use crate::{
13    CompositeLayer, EffectId, EffectKind, RenderedTexture, Renderer, ShaderDiagnostics,
14    WarmupReport,
15};
16
17pub(crate) struct EncodedGraph {
18    texture: RenderedTexture,
19    /// Results to keep once the work that draws them is submitted and the
20    /// GPU accepted it: kept any earlier, a failed encode or a rejected
21    /// submission would leave unwritten textures behind.
22    cache_inserts: Vec<(RenderCacheKey, RenderedTexture, bool)>,
23    uniforms: UniformArenaLease,
24    pending_timings: Vec<PendingEffectTiming>,
25    encoded_nodes: u64,
26    encoded_render_passes: u64,
27}
28
29impl EncodedGraph {
30    pub(crate) fn texture(&self) -> &RenderedTexture {
31        &self.texture
32    }
33
34    pub(crate) fn flush_uniforms(&self, queue: &wgpu::Queue) {
35        self.uniforms.flush(queue);
36    }
37}
38
39impl Renderer {
40    /// Returns a deterministic key for the graph and the renderer
41    /// configuration that changes its pixels: the blend space, the quality
42    /// controls (as passes take them), and the intermediate format. Two
43    /// renderers on adapters that store pictures alike give equal keys.
44    ///
45    /// # Errors
46    /// Returns [`GraphError::MissingOutput`] when no output was selected.
47    pub fn graph_cache_key(&self, graph: &RenderGraph<'_>) -> Result<RenderCacheKey, GraphError> {
48        let quality = self.quality_controls();
49        let plan = graph.compile_with_quality(quality)?;
50        Ok(renderer_cache_key(
51            plan.cache_key,
52            graph.blend_space.unwrap_or(self.blend_space).shader_value(),
53            quality,
54            self.intermediate_format(),
55        ))
56    }
57
58    /// Initializes all pipelines referenced by one graph.
59    ///
60    /// # Errors
61    /// Returns graph validation errors or diagnostics for an unknown effect.
62    pub fn warmup_graph(&self, graph: &RenderGraph<'_>) -> Result<WarmupReport, ShaderDiagnostics> {
63        self.warmup_graphs(&[graph])
64    }
65
66    /// Initializes the deduplicated pipeline set referenced by project graphs.
67    ///
68    /// # Errors
69    /// Returns diagnostics for a malformed graph or an unknown effect.
70    pub fn warmup_graphs(
71        &self,
72        graphs: &[&RenderGraph<'_>],
73    ) -> Result<WarmupReport, ShaderDiagnostics> {
74        let mut effects = Vec::<EffectId>::new();
75        let mut seen = HashSet::<EffectId>::new();
76        let mut needs_composite = false;
77        let mut composite_layers = Vec::<usize>::new();
78        for graph in graphs {
79            let plan = graph
80                .compile()
81                .map_err(|error| ShaderDiagnostics::error(error.to_string()))?;
82            for node in plan.order {
83                match &graph.nodes[node.index] {
84                    RenderNode::Effect { effect, .. }
85                    | RenderNode::Transition { effect, .. }
86                    | RenderNode::Generator { effect, .. } => {
87                        if seen.insert(effect.clone()) {
88                            effects.push(effect.clone());
89                        }
90                    }
91                    RenderNode::Composite { layers, .. } => {
92                        needs_composite = true;
93                        composite_layers.push(layers.len());
94                    }
95                    RenderNode::Source(_) => {}
96                }
97            }
98        }
99        self.warmup(&effects)?;
100        for layers in composite_layers {
101            self.warm_composite(layers);
102        }
103        Ok(WarmupReport {
104            runtime_effects: effects.len(),
105            builtins: usize::from(needs_composite),
106        })
107    }
108
109    /// Executes the reachable portion of a graph and returns its owned output.
110    ///
111    /// # Errors
112    /// Returns a structural error or the first rendering failure.
113    #[allow(clippy::too_many_lines)]
114    pub fn execute_graph(&self, graph: &RenderGraph<'_>) -> Result<RenderedTexture, GraphError> {
115        let mut command_encoder = self.command_encoder("alpha2 render graph encoder");
116        let graph_work = self.encode_graph(graph, &mut command_encoder)?;
117        graph_work.flush_uniforms(self.gpu.queue());
118        self.submit([command_encoder.finish()])
119            .map_err(GraphError::Render)?;
120        Ok(self.complete_graph_work(graph_work, true))
121    }
122
123    #[allow(clippy::too_many_lines)]
124    pub(crate) fn encode_graph(
125        &self,
126        graph: &RenderGraph<'_>,
127        encoder: &mut wgpu::CommandEncoder,
128    ) -> Result<EncodedGraph, GraphError> {
129        self.validate_size(graph.size)?;
130        let plan = graph.compile()?;
131        if matches!(graph.nodes[plan.output.index], RenderNode::Source(_)) {
132            return Err(GraphError::SourceOutput);
133        }
134        let format = self.intermediate_format();
135        let uniform_alignment = u64::from(self.limits.min_uniform_buffer_offset_alignment.max(1));
136        let uniform_capacity = graph_uniform_capacity(self, graph, &plan.order, uniform_alignment);
137        let mut uniforms = self
138            .uniform_buffers
139            .acquire(self.gpu.device(), uniform_capacity);
140        let mut outputs: Vec<Option<RenderedTexture>> =
141            (0..graph.nodes.len()).map(|_| None).collect();
142        let blend_space = graph.blend_space.unwrap_or(self.blend_space).shader_value();
143        let cached = self.cached_results(graph, &plan.order, plan.output, blend_space);
144        let mut pending_timings = Vec::<PendingEffectTiming>::new();
145        let mut encoded_nodes = 0_u64;
146        let mut encoded_render_passes = 0_u64;
147        let mut cache_inserts = Vec::new();
148        // Reading a clock per pass is profiling cost, so it is opt-in.
149        let timing = self.pass_timing_epoch();
150        for (position, node_id) in plan.order.iter().copied().enumerate() {
151            let started = timing.map(|epoch| (epoch, Instant::now()));
152            if let Some(hit) = cached.hits[node_id.index].clone() {
153                outputs[node_id.index] = Some(hit);
154                release_inputs(graph, &plan.last_use, &mut outputs, node_id, position);
155                continue;
156            }
157            if !cached.needed[node_id.index] {
158                // Only results found in the cache consume it: release its inputs where they
159                // would have been.
160                release_inputs(graph, &plan.last_use, &mut outputs, node_id, position);
161                continue;
162            }
163            let output = match &graph.nodes[node_id.index] {
164                RenderNode::Source(_) => None,
165                RenderNode::Effect {
166                    effect,
167                    source,
168                    params,
169                    resources,
170                    runtime,
171                } => {
172                    if self
173                        .effect_definition(effect)
174                        .is_some_and(|definition| definition.kind != EffectKind::Effect)
175                    {
176                        return Err(GraphError::EffectKindMismatch);
177                    }
178                    let source_view = graph_view(graph, &outputs, *source);
179                    let target = self.textures.acquire(
180                        self.gpu.device(),
181                        graph.size,
182                        format,
183                        "alpha2 graph effect target",
184                    );
185                    if let Some(pending) = self.encode_effect_pass(
186                        encoder,
187                        EffectPass {
188                            id: effect,
189                            kind: crate::RenderNodeKind::Effect,
190                            source_a: source_view,
191                            source_b: None,
192                            size: graph.size,
193                            params,
194                            resources,
195                            runtime: *runtime,
196                            output: target.view(),
197                        },
198                        &mut uniforms,
199                    )? {
200                        pending_timings.push(pending);
201                    }
202                    encoded_render_passes = encoded_render_passes.saturating_add(1);
203                    Some(RenderedTexture::pooled(graph.size, format, target))
204                }
205                RenderNode::Transition {
206                    effect,
207                    from,
208                    to,
209                    params,
210                    resources,
211                    runtime,
212                } => {
213                    if self
214                        .effect_definition(effect)
215                        .is_some_and(|definition| definition.kind != EffectKind::Transition)
216                    {
217                        return Err(GraphError::EffectKindMismatch);
218                    }
219                    let from_view = graph_view(graph, &outputs, *from);
220                    let to_view = graph_view(graph, &outputs, *to);
221                    let target = self.textures.acquire(
222                        self.gpu.device(),
223                        graph.size,
224                        format,
225                        "alpha2 graph transition target",
226                    );
227                    if let Some(pending) = self.encode_effect_pass(
228                        encoder,
229                        EffectPass {
230                            id: effect,
231                            kind: crate::RenderNodeKind::Transition,
232                            source_a: from_view,
233                            source_b: Some(to_view),
234                            size: graph.size,
235                            params,
236                            resources,
237                            runtime: *runtime,
238                            output: target.view(),
239                        },
240                        &mut uniforms,
241                    )? {
242                        pending_timings.push(pending);
243                    }
244                    encoded_render_passes = encoded_render_passes.saturating_add(1);
245                    Some(RenderedTexture::pooled(graph.size, format, target))
246                }
247                RenderNode::Generator {
248                    effect,
249                    params,
250                    resources,
251                    runtime,
252                } => {
253                    if self
254                        .effect_definition(effect)
255                        .is_some_and(|definition| definition.kind != EffectKind::Generator)
256                    {
257                        return Err(GraphError::EffectKindMismatch);
258                    }
259                    let target = self.textures.acquire(
260                        self.gpu.device(),
261                        graph.size,
262                        format,
263                        "alpha2 graph generator target",
264                    );
265                    // A generator samples nothing; its bindings still need a texture.
266                    if let Some(pending) = self.encode_effect_pass(
267                        encoder,
268                        EffectPass {
269                            id: effect,
270                            kind: crate::RenderNodeKind::Generator,
271                            source_a: &self.empty_input,
272                            source_b: None,
273                            size: graph.size,
274                            params,
275                            resources,
276                            runtime: *runtime,
277                            output: target.view(),
278                        },
279                        &mut uniforms,
280                    )? {
281                        pending_timings.push(pending);
282                    }
283                    encoded_render_passes = encoded_render_passes.saturating_add(1);
284                    Some(RenderedTexture::pooled(graph.size, format, target))
285                }
286                RenderNode::Composite { layers, background } => {
287                    let views = layers
288                        .iter()
289                        .map(|layer| graph_view(graph, &outputs, layer.source))
290                        .collect::<Vec<_>>();
291                    let composite_layers = layers
292                        .iter()
293                        .zip(&views)
294                        .map(|(layer, view)| CompositeLayer {
295                            texture: view,
296                            visual: layer.visual,
297                            blend_mode: layer.blend_mode,
298                        })
299                        .collect::<Vec<_>>();
300                    let target = self.textures.acquire(
301                        self.gpu.device(),
302                        graph.size,
303                        format,
304                        "alpha2 graph composite target",
305                    );
306                    encoded_render_passes =
307                        encoded_render_passes.saturating_add(self.encode_layers_pass(
308                            encoder,
309                            graph.size,
310                            &composite_layers,
311                            CompositeTarget {
312                                background: *background,
313                                blend_space: graph.blend_space.unwrap_or(self.blend_space),
314                                output: target.texture(),
315                                output_view: target.view(),
316                            },
317                            &mut uniforms,
318                        )?);
319                    Some(RenderedTexture::pooled(graph.size, format, target))
320                }
321            };
322            if let Some((epoch, started)) = started
323                && !matches!(graph.nodes[node_id.index], RenderNode::Source(_))
324            {
325                let label = graph.nodes[node_id.index].kind().pass_label();
326                crate::diagnostics::record_pass(
327                    &self.diagnostics,
328                    epoch,
329                    label,
330                    started.elapsed(),
331                    crate::TimingSource::CpuEncoding,
332                );
333            }
334            if let (Some(output), Some((key, pinned))) = (&output, cached.store[node_id.index]) {
335                cache_inserts.push((key, output.clone(), pinned));
336            }
337            outputs[node_id.index] = output;
338            if !matches!(graph.nodes[node_id.index], RenderNode::Source(_)) {
339                encoded_nodes = encoded_nodes.saturating_add(1);
340            }
341            release_inputs(graph, &plan.last_use, &mut outputs, node_id, position);
342        }
343
344        let texture = outputs[plan.output.index]
345            .take()
346            .ok_or(GraphError::SourceOutput)?;
347        Ok(EncodedGraph {
348            texture,
349            cache_inserts,
350            uniforms,
351            pending_timings,
352            encoded_nodes,
353            encoded_render_passes,
354        })
355    }
356
357    pub(crate) fn complete_graph_work(
358        &self,
359        encoded: EncodedGraph,
360        standalone_submission: bool,
361    ) -> RenderedTexture {
362        let EncodedGraph {
363            texture,
364            cache_inserts,
365            uniforms,
366            pending_timings,
367            encoded_nodes,
368            encoded_render_passes,
369        } = encoded;
370        uniforms.recycle();
371        if !cache_inserts.is_empty() {
372            let mut cache = self.lock_result_cache();
373            for (key, result, pinned) in &cache_inserts {
374                cache.insert(*key, result, *pinned);
375            }
376        }
377        crate::diagnostics::record_graph_work(
378            &self.diagnostics,
379            encoded_nodes,
380            encoded_render_passes,
381            standalone_submission,
382        );
383        for timing in pending_timings {
384            timing.map_after_submit();
385        }
386        texture
387    }
388}
389
390fn graph_uniform_capacity(
391    renderer: &Renderer,
392    graph: &RenderGraph<'_>,
393    order: &[RenderNodeId],
394    alignment: u64,
395) -> u64 {
396    let mut capacity = 0_u64;
397    for node in order {
398        match &graph.nodes[node.index] {
399            RenderNode::Effect { effect, .. }
400            | RenderNode::Transition { effect, .. }
401            | RenderNode::Generator { effect, .. } => {
402                let parameter_bytes = renderer
403                    .effects
404                    .get(effect)
405                    .map_or(0, |registered| registered.definition.params.len());
406                let parameter_bytes =
407                    u64::try_from(param_uniform_bytes(parameter_bytes)).unwrap_or(u64::MAX);
408                capacity = aligned_uniform_end(capacity, parameter_bytes, alignment);
409                capacity = aligned_uniform_end(
410                    capacity,
411                    float_uniform_bytes(RUNTIME_UNIFORM_WORDS),
412                    alignment,
413                );
414            }
415            RenderNode::Composite { layers, .. } => {
416                let passes = renderer
417                    .composite_batch_count(layers.iter().map(|layer| layer.visual), graph.size);
418                for _ in 0..passes {
419                    capacity = aligned_uniform_end(
420                        capacity,
421                        float_uniform_bytes(BATCH_UNIFORM_FLOATS),
422                        alignment,
423                    );
424                }
425            }
426            RenderNode::Source(_) => {}
427        }
428    }
429    capacity
430}
431
432fn graph_view<'a>(
433    graph: &'a RenderGraph<'_>,
434    outputs: &'a [Option<RenderedTexture>],
435    node: RenderNodeId,
436) -> &'a wgpu::TextureView {
437    match &graph.nodes[node.index] {
438        RenderNode::Source(view) => view,
439        _ => outputs[node.index]
440            .as_ref()
441            .expect("compiled dependency has an output")
442            .view(),
443    }
444}
445
446/// Drops the results `node` was the last to read, returning them to the pool.
447fn release_inputs(
448    graph: &RenderGraph<'_>,
449    last_use: &[Option<usize>],
450    outputs: &mut [Option<RenderedTexture>],
451    node: RenderNodeId,
452    position: usize,
453) {
454    for dependency in graph.nodes[node.index].dependencies() {
455        if last_use[dependency.index] == Some(position) {
456            outputs[dependency.index] = None;
457        }
458    }
459}
460
461/// What the result cache holds for one graph: results found, results to
462/// keep, and which nodes must still be drawn.
463struct CachedResults {
464    hits: Vec<Option<RenderedTexture>>,
465    store: Vec<Option<(RenderCacheKey, bool)>>,
466    needed: Vec<bool>,
467}
468
469impl Renderer {
470    /// Looks up the graph's cache candidates and works out which nodes a hit
471    /// makes unnecessary. With the cache off or no candidates, every node is
472    /// drawn and nothing is kept.
473    fn cached_results(
474        &self,
475        graph: &RenderGraph<'_>,
476        order: &[RenderNodeId],
477        output: RenderNodeId,
478        blend_space: f32,
479    ) -> CachedResults {
480        let count = graph.nodes.len();
481        let mut cached = CachedResults {
482            hits: vec![None; count],
483            store: vec![None; count],
484            needed: vec![true; count],
485        };
486        if graph.intents.iter().all(Option::is_none) {
487            return cached;
488        }
489        let mut cache = self.lock_result_cache();
490        if !cache.enabled() {
491            return cached;
492        }
493        let eligibility = graph.eligibility();
494        let (mut hits, mut misses, mut ineligible) = (0, 0, 0);
495        // From the output down: a node is looked up only if something still
496        // needs it, and a hit's inputs are needed only by others.
497        cached.needed = vec![false; count];
498        cached.needed[output.index] = true;
499        for node in order.iter().rev() {
500            if !cached.needed[node.index] {
501                continue;
502            }
503            if let Some(intent) = graph.intents[node.index] {
504                if eligibility[node.index] == crate::CacheEligibility::Eligible {
505                    let key = renderer_node_key(graph.cache_keys[node.index], blend_space);
506                    let pinned = intent == crate::CacheIntent::Pin;
507                    if let Some(texture) = cache.get(key, pinned) {
508                        cached.hits[node.index] = Some(texture);
509                        hits += 1;
510                        continue;
511                    }
512                    cached.store[node.index] = Some((key, pinned));
513                    misses += 1;
514                } else if intent != crate::CacheIntent::Bypass {
515                    ineligible += 1;
516                }
517            }
518            for input in graph.nodes[node.index].dependencies() {
519                cached.needed[input.index] = true;
520            }
521        }
522        let skipped = order
523            .iter()
524            .filter(|node| {
525                !cached.needed[node.index]
526                    && cached.hits[node.index].is_none()
527                    && !matches!(graph.nodes[node.index], RenderNode::Source(_))
528            })
529            .count() as u64;
530        cache.record(hits, misses, ineligible, skipped);
531        cached
532    }
533}