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        let mut vector_spaces = Vec::<crate::BlendSpace>::new();
79        for graph in graphs {
80            let plan = graph
81                .compile()
82                .map_err(|error| ShaderDiagnostics::error(error.to_string()))?;
83            for node in plan.order {
84                match &graph.nodes[node.index] {
85                    RenderNode::Effect { effect, .. }
86                    | RenderNode::Transition { effect, .. }
87                    | RenderNode::Generator { effect, .. } => {
88                        if seen.insert(effect.clone()) {
89                            effects.push(effect.clone());
90                        }
91                    }
92                    RenderNode::Composite { layers, .. } => {
93                        needs_composite = true;
94                        composite_layers.push(layers.len());
95                    }
96                    RenderNode::Vector { .. } => {
97                        let space = graph.blend_space.unwrap_or(self.blend_space);
98                        if !vector_spaces.contains(&space) {
99                            vector_spaces.push(space);
100                        }
101                    }
102                    RenderNode::Source(_) => {}
103                }
104            }
105        }
106        self.warmup(&effects)?;
107        for layers in composite_layers {
108            self.warm_composite(layers);
109        }
110        for space in &vector_spaces {
111            self.warm_vector(*space);
112        }
113        Ok(WarmupReport {
114            runtime_effects: effects.len(),
115            builtins: usize::from(needs_composite) + usize::from(!vector_spaces.is_empty()),
116        })
117    }
118
119    /// Executes the reachable portion of a graph and returns its owned output.
120    ///
121    /// # Errors
122    /// Returns a structural error or the first rendering failure.
123    #[allow(clippy::too_many_lines)]
124    pub fn execute_graph(&self, graph: &RenderGraph<'_>) -> Result<RenderedTexture, GraphError> {
125        let mut command_encoder = self.command_encoder("alpha2 render graph encoder");
126        let graph_work = self.encode_graph(graph, &mut command_encoder)?;
127        graph_work.flush_uniforms(self.gpu.queue());
128        self.submit([command_encoder.finish()])
129            .map_err(GraphError::Render)?;
130        Ok(self.complete_graph_work(graph_work, true))
131    }
132
133    #[allow(clippy::too_many_lines)]
134    pub(crate) fn encode_graph(
135        &self,
136        graph: &RenderGraph<'_>,
137        encoder: &mut wgpu::CommandEncoder,
138    ) -> Result<EncodedGraph, GraphError> {
139        self.validate_size(graph.size)?;
140        let plan = graph.compile()?;
141        if matches!(graph.nodes[plan.output.index], RenderNode::Source(_)) {
142            return Err(GraphError::SourceOutput);
143        }
144        let format = self.intermediate_format();
145        let uniform_alignment = u64::from(self.limits.min_uniform_buffer_offset_alignment.max(1));
146        let uniform_capacity = graph_uniform_capacity(self, graph, &plan.order, uniform_alignment);
147        let mut uniforms = self
148            .uniform_buffers
149            .acquire(self.gpu.device(), uniform_capacity);
150        let mut outputs: Vec<Option<RenderedTexture>> =
151            (0..graph.nodes.len()).map(|_| None).collect();
152        let blend_space = graph.blend_space.unwrap_or(self.blend_space).shader_value();
153        let cached = self.cached_results(graph, &plan.order, plan.output, blend_space);
154        let mut pending_timings = Vec::<PendingEffectTiming>::new();
155        let mut encoded_nodes = 0_u64;
156        let mut encoded_render_passes = 0_u64;
157        let mut cache_inserts = Vec::new();
158        // Reading a clock per pass is profiling cost, so it is opt-in.
159        let timing = self.pass_timing_epoch();
160        for (position, node_id) in plan.order.iter().copied().enumerate() {
161            let started = timing.map(|epoch| (epoch, Instant::now()));
162            if let Some(hit) = cached.hits[node_id.index].clone() {
163                outputs[node_id.index] = Some(hit);
164                release_inputs(graph, &plan.last_use, &mut outputs, node_id, position);
165                continue;
166            }
167            if !cached.needed[node_id.index] {
168                // Only results found in the cache consume it: release its inputs where they
169                // would have been.
170                release_inputs(graph, &plan.last_use, &mut outputs, node_id, position);
171                continue;
172            }
173            let output = match &graph.nodes[node_id.index] {
174                RenderNode::Source(_) => None,
175                RenderNode::Effect {
176                    effect,
177                    source,
178                    inputs,
179                    params,
180                    resources,
181                    runtime,
182                } => {
183                    if self
184                        .effect_definition(effect)
185                        .is_some_and(|definition| definition.kind != EffectKind::Effect)
186                    {
187                        return Err(GraphError::EffectKindMismatch);
188                    }
189                    let source_view = graph_view(graph, &outputs, *source);
190                    // Empty, and so not allocated, for an effect without named inputs.
191                    let input_views: Vec<(&str, &wgpu::TextureView)> = inputs
192                        .iter()
193                        .map(|(name, node)| (name, graph_view(graph, &outputs, node)))
194                        .collect();
195                    let target = self.textures.acquire(
196                        self.gpu.device(),
197                        graph.size,
198                        format,
199                        "alpha2 graph effect target",
200                    );
201                    if let Some(pending) = self.encode_effect_pass(
202                        encoder,
203                        EffectPass {
204                            id: effect,
205                            kind: crate::RenderNodeKind::Effect,
206                            source_a: source_view,
207                            source_b: None,
208                            inputs: &input_views,
209                            size: graph.size,
210                            params,
211                            resources,
212                            runtime: *runtime,
213                            output: target.view(),
214                        },
215                        &mut uniforms,
216                    )? {
217                        pending_timings.push(pending);
218                    }
219                    encoded_render_passes = encoded_render_passes.saturating_add(1);
220                    Some(RenderedTexture::pooled(graph.size, format, target))
221                }
222                RenderNode::Transition {
223                    effect,
224                    from,
225                    to,
226                    params,
227                    resources,
228                    runtime,
229                } => {
230                    if self
231                        .effect_definition(effect)
232                        .is_some_and(|definition| definition.kind != EffectKind::Transition)
233                    {
234                        return Err(GraphError::EffectKindMismatch);
235                    }
236                    let from_view = graph_view(graph, &outputs, *from);
237                    let to_view = graph_view(graph, &outputs, *to);
238                    let target = self.textures.acquire(
239                        self.gpu.device(),
240                        graph.size,
241                        format,
242                        "alpha2 graph transition target",
243                    );
244                    if let Some(pending) = self.encode_effect_pass(
245                        encoder,
246                        EffectPass {
247                            id: effect,
248                            kind: crate::RenderNodeKind::Transition,
249                            source_a: from_view,
250                            source_b: Some(to_view),
251                            inputs: &[],
252                            size: graph.size,
253                            params,
254                            resources,
255                            runtime: *runtime,
256                            output: target.view(),
257                        },
258                        &mut uniforms,
259                    )? {
260                        pending_timings.push(pending);
261                    }
262                    encoded_render_passes = encoded_render_passes.saturating_add(1);
263                    Some(RenderedTexture::pooled(graph.size, format, target))
264                }
265                RenderNode::Generator {
266                    effect,
267                    params,
268                    resources,
269                    runtime,
270                } => {
271                    if self
272                        .effect_definition(effect)
273                        .is_some_and(|definition| definition.kind != EffectKind::Generator)
274                    {
275                        return Err(GraphError::EffectKindMismatch);
276                    }
277                    let target = self.textures.acquire(
278                        self.gpu.device(),
279                        graph.size,
280                        format,
281                        "alpha2 graph generator target",
282                    );
283                    // A generator samples nothing; its bindings still need a texture.
284                    if let Some(pending) = self.encode_effect_pass(
285                        encoder,
286                        EffectPass {
287                            id: effect,
288                            kind: crate::RenderNodeKind::Generator,
289                            source_a: &self.empty_input,
290                            source_b: None,
291                            inputs: &[],
292                            size: graph.size,
293                            params,
294                            resources,
295                            runtime: *runtime,
296                            output: target.view(),
297                        },
298                        &mut uniforms,
299                    )? {
300                        pending_timings.push(pending);
301                    }
302                    encoded_render_passes = encoded_render_passes.saturating_add(1);
303                    Some(RenderedTexture::pooled(graph.size, format, target))
304                }
305                RenderNode::Vector { scene } => {
306                    let target = self.textures.acquire(
307                        self.gpu.device(),
308                        graph.size,
309                        format,
310                        "alpha2 graph vector target",
311                    );
312                    encoded_render_passes =
313                        encoded_render_passes.saturating_add(self.encode_vector(
314                            encoder,
315                            scene,
316                            graph.size,
317                            graph.blend_space.unwrap_or(self.blend_space),
318                            target.view(),
319                            &mut uniforms,
320                        )?);
321                    Some(RenderedTexture::pooled(graph.size, format, target))
322                }
323                RenderNode::Composite { layers, background } => {
324                    let views = layers
325                        .iter()
326                        .map(|layer| graph_view(graph, &outputs, layer.source))
327                        .collect::<Vec<_>>();
328                    let composite_layers = layers
329                        .iter()
330                        .zip(&views)
331                        .map(|(layer, view)| CompositeLayer {
332                            texture: view,
333                            visual: layer.visual,
334                            blend_mode: layer.blend_mode,
335                        })
336                        .collect::<Vec<_>>();
337                    let target = self.textures.acquire(
338                        self.gpu.device(),
339                        graph.size,
340                        format,
341                        "alpha2 graph composite target",
342                    );
343                    encoded_render_passes =
344                        encoded_render_passes.saturating_add(self.encode_layers_pass(
345                            encoder,
346                            graph.size,
347                            &composite_layers,
348                            CompositeTarget {
349                                background: *background,
350                                blend_space: graph.blend_space.unwrap_or(self.blend_space),
351                                output: target.texture(),
352                                output_view: target.view(),
353                            },
354                            &mut uniforms,
355                        )?);
356                    Some(RenderedTexture::pooled(graph.size, format, target))
357                }
358            };
359            if let Some((epoch, started)) = started
360                && !matches!(graph.nodes[node_id.index], RenderNode::Source(_))
361            {
362                let label = graph.nodes[node_id.index].kind().pass_label();
363                crate::diagnostics::record_pass(
364                    &self.diagnostics,
365                    epoch,
366                    label,
367                    started.elapsed(),
368                    crate::TimingSource::CpuEncoding,
369                );
370            }
371            if let (Some(output), Some((key, pinned))) = (&output, cached.store[node_id.index]) {
372                cache_inserts.push((key, output.clone(), pinned));
373            }
374            outputs[node_id.index] = output;
375            if !matches!(graph.nodes[node_id.index], RenderNode::Source(_)) {
376                encoded_nodes = encoded_nodes.saturating_add(1);
377            }
378            release_inputs(graph, &plan.last_use, &mut outputs, node_id, position);
379        }
380
381        let texture = outputs[plan.output.index]
382            .take()
383            .ok_or(GraphError::SourceOutput)?;
384        Ok(EncodedGraph {
385            texture,
386            cache_inserts,
387            uniforms,
388            pending_timings,
389            encoded_nodes,
390            encoded_render_passes,
391        })
392    }
393
394    pub(crate) fn complete_graph_work(
395        &self,
396        encoded: EncodedGraph,
397        standalone_submission: bool,
398    ) -> RenderedTexture {
399        let EncodedGraph {
400            texture,
401            cache_inserts,
402            uniforms,
403            pending_timings,
404            encoded_nodes,
405            encoded_render_passes,
406        } = encoded;
407        uniforms.recycle();
408        if !cache_inserts.is_empty() {
409            let mut cache = self.lock_result_cache();
410            for (key, result, pinned) in &cache_inserts {
411                cache.insert(*key, result, *pinned);
412            }
413        }
414        crate::diagnostics::record_graph_work(
415            &self.diagnostics,
416            encoded_nodes,
417            encoded_render_passes,
418            standalone_submission,
419        );
420        for timing in pending_timings {
421            timing.map_after_submit();
422        }
423        texture
424    }
425}
426
427fn graph_uniform_capacity(
428    renderer: &Renderer,
429    graph: &RenderGraph<'_>,
430    order: &[RenderNodeId],
431    alignment: u64,
432) -> u64 {
433    let mut capacity = 0_u64;
434    for node in order {
435        match &graph.nodes[node.index] {
436            RenderNode::Effect { effect, .. }
437            | RenderNode::Transition { effect, .. }
438            | RenderNode::Generator { effect, .. } => {
439                let parameter_bytes = renderer
440                    .effects
441                    .get(effect)
442                    .map_or(0, |registered| registered.definition.params.len());
443                let parameter_bytes =
444                    u64::try_from(param_uniform_bytes(parameter_bytes)).unwrap_or(u64::MAX);
445                capacity = aligned_uniform_end(capacity, parameter_bytes, alignment);
446                capacity = aligned_uniform_end(
447                    capacity,
448                    float_uniform_bytes(RUNTIME_UNIFORM_WORDS),
449                    alignment,
450                );
451            }
452            RenderNode::Composite { layers, .. } => {
453                let passes = renderer
454                    .composite_batch_count(layers.iter().map(|layer| layer.visual), graph.size);
455                for _ in 0..passes {
456                    capacity = aligned_uniform_end(
457                        capacity,
458                        float_uniform_bytes(BATCH_UNIFORM_FLOATS),
459                        alignment,
460                    );
461                }
462            }
463            RenderNode::Vector { scene } => {
464                // A slice per fill and per layer, and one for the encode pass.
465                let slices = scene
466                    .ops
467                    .iter()
468                    .filter(|op| !matches!(op, alpha2_geometry::VectorOp::PushLayer { .. }))
469                    .count()
470                    + 1;
471                for _ in 0..slices {
472                    capacity = aligned_uniform_end(
473                        capacity,
474                        crate::vector::VECTOR_UNIFORM_BYTES,
475                        alignment,
476                    );
477                }
478            }
479            RenderNode::Source(_) => {}
480        }
481    }
482    capacity
483}
484
485fn graph_view<'a>(
486    graph: &'a RenderGraph<'_>,
487    outputs: &'a [Option<RenderedTexture>],
488    node: RenderNodeId,
489) -> &'a wgpu::TextureView {
490    match &graph.nodes[node.index] {
491        RenderNode::Source(view) => view,
492        _ => outputs[node.index]
493            .as_ref()
494            .expect("compiled dependency has an output")
495            .view(),
496    }
497}
498
499/// Drops the results `node` was the last to read, returning them to the pool.
500fn release_inputs(
501    graph: &RenderGraph<'_>,
502    last_use: &[Option<usize>],
503    outputs: &mut [Option<RenderedTexture>],
504    node: RenderNodeId,
505    position: usize,
506) {
507    for dependency in graph.nodes[node.index].dependencies() {
508        if last_use[dependency.index] == Some(position) {
509            outputs[dependency.index] = None;
510        }
511    }
512}
513
514/// What the result cache holds for one graph: results found, results to
515/// keep, and which nodes must still be drawn.
516struct CachedResults {
517    hits: Vec<Option<RenderedTexture>>,
518    store: Vec<Option<(RenderCacheKey, bool)>>,
519    needed: Vec<bool>,
520}
521
522impl Renderer {
523    /// Looks up the graph's cache candidates and works out which nodes a hit
524    /// makes unnecessary. With the cache off or no candidates, every node is
525    /// drawn and nothing is kept.
526    fn cached_results(
527        &self,
528        graph: &RenderGraph<'_>,
529        order: &[RenderNodeId],
530        output: RenderNodeId,
531        blend_space: f32,
532    ) -> CachedResults {
533        let count = graph.nodes.len();
534        let mut cached = CachedResults {
535            hits: vec![None; count],
536            store: vec![None; count],
537            needed: vec![true; count],
538        };
539        if graph.intents.iter().all(Option::is_none) {
540            return cached;
541        }
542        let mut cache = self.lock_result_cache();
543        if !cache.enabled() {
544            return cached;
545        }
546        let eligibility = graph.eligibility();
547        let (mut hits, mut misses, mut ineligible) = (0, 0, 0);
548        // From the output down: a node is looked up only if something still
549        // needs it, and a hit's inputs are needed only by others.
550        cached.needed = vec![false; count];
551        cached.needed[output.index] = true;
552        for node in order.iter().rev() {
553            if !cached.needed[node.index] {
554                continue;
555            }
556            if let Some(intent) = graph.intents[node.index] {
557                if eligibility[node.index] == crate::CacheEligibility::Eligible {
558                    let key = renderer_node_key(graph.cache_keys[node.index], blend_space);
559                    let pinned = intent == crate::CacheIntent::Pin;
560                    if let Some(texture) = cache.get(key, pinned) {
561                        cached.hits[node.index] = Some(texture);
562                        hits += 1;
563                        continue;
564                    }
565                    cached.store[node.index] = Some((key, pinned));
566                    misses += 1;
567                } else if intent != crate::CacheIntent::Bypass {
568                    ineligible += 1;
569                }
570            }
571            for input in graph.nodes[node.index].dependencies() {
572                cached.needed[input.index] = true;
573            }
574        }
575        let skipped = order
576            .iter()
577            .filter(|node| {
578                !cached.needed[node.index]
579                    && cached.hits[node.index].is_none()
580                    && !matches!(graph.nodes[node.index], RenderNode::Source(_))
581            })
582            .count() as u64;
583        cache.record(hits, misses, ineligible, skipped);
584        cached
585    }
586}