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 after the submission is accepted; keeping them earlier could leave
20    /// unwritten textures after a failure.
21    cache_inserts: Vec<(RenderCacheKey, RenderedTexture, bool)>,
22    uniforms: UniformArenaLease,
23    pending_timings: Vec<PendingEffectTiming>,
24    encoded_nodes: u64,
25    encoded_render_passes: u64,
26}
27
28impl EncodedGraph {
29    pub(crate) fn texture(&self) -> &RenderedTexture {
30        &self.texture
31    }
32
33    pub(crate) fn flush_uniforms(&self, queue: &wgpu::Queue) {
34        self.uniforms.flush(queue);
35    }
36}
37
38impl Renderer {
39    /// A deterministic key for the graph and the settings that affect its pixels (blend space,
40    /// applied quality controls, intermediate format). Adapters that store pictures alike give
41    /// equal keys.
42    ///
43    /// # Errors
44    /// Returns [`GraphError::MissingOutput`] when no output was selected.
45    pub fn graph_cache_key(&self, graph: &RenderGraph<'_>) -> Result<RenderCacheKey, GraphError> {
46        let quality = self.quality_controls();
47        let plan = graph.compile_with_quality(quality)?;
48        Ok(renderer_cache_key(
49            plan.cache_key,
50            graph.blend_space.unwrap_or(self.blend_space).shader_value(),
51            quality,
52            self.intermediate_format(),
53        ))
54    }
55
56    /// Initializes all pipelines referenced by one graph.
57    ///
58    /// # Errors
59    /// Returns graph validation errors or diagnostics for an unknown effect.
60    pub fn warmup_graph(&self, graph: &RenderGraph<'_>) -> Result<WarmupReport, ShaderDiagnostics> {
61        self.warmup_graphs(&[graph])
62    }
63
64    /// Initializes the deduplicated pipeline set referenced by project graphs.
65    ///
66    /// # Errors
67    /// Returns diagnostics for a malformed graph or an unknown effect.
68    pub fn warmup_graphs(
69        &self,
70        graphs: &[&RenderGraph<'_>],
71    ) -> Result<WarmupReport, ShaderDiagnostics> {
72        let mut effects = Vec::<EffectId>::new();
73        let mut seen = HashSet::<EffectId>::new();
74        let mut needs_composite = false;
75        let mut composite_layers = Vec::<usize>::new();
76        let mut vector_spaces = Vec::<crate::BlendSpace>::new();
77        for graph in graphs {
78            let plan = graph
79                .compile()
80                .map_err(|error| ShaderDiagnostics::error(error.to_string()))?;
81            for node in plan.order {
82                match &graph.nodes[node.index] {
83                    RenderNode::Effect { effect, .. }
84                    | RenderNode::Transition { effect, .. }
85                    | RenderNode::Generator { effect, .. } => {
86                        if seen.insert(effect.clone()) {
87                            effects.push(effect.clone());
88                        }
89                    }
90                    RenderNode::Composite { layers, .. } => {
91                        needs_composite = true;
92                        composite_layers.push(layers.len());
93                    }
94                    RenderNode::Vector { .. } => {
95                        let space = graph.blend_space.unwrap_or(self.blend_space);
96                        if !vector_spaces.contains(&space) {
97                            vector_spaces.push(space);
98                        }
99                    }
100                    RenderNode::Source(_) => {}
101                }
102            }
103        }
104        self.warmup(&effects)?;
105        for layers in composite_layers {
106            self.warm_composite(layers);
107        }
108        for space in &vector_spaces {
109            self.warm_vector(*space);
110        }
111        Ok(WarmupReport {
112            runtime_effects: effects.len(),
113            builtins: usize::from(needs_composite) + usize::from(!vector_spaces.is_empty()),
114        })
115    }
116
117    /// Executes the reachable portion of a graph and returns its owned output.
118    ///
119    /// # Errors
120    /// Returns a structural error or the first rendering failure.
121    #[allow(clippy::too_many_lines)]
122    pub fn execute_graph(&self, graph: &RenderGraph<'_>) -> Result<RenderedTexture, GraphError> {
123        let mut command_encoder = self.command_encoder("alpha2 render graph encoder");
124        let graph_work = self.encode_graph(graph, &mut command_encoder)?;
125        graph_work.flush_uniforms(self.gpu.queue());
126        self.submit([command_encoder.finish()])
127            .map_err(GraphError::Render)?;
128        Ok(self.complete_graph_work(graph_work, true))
129    }
130
131    #[allow(clippy::too_many_lines)]
132    pub(crate) fn encode_graph(
133        &self,
134        graph: &RenderGraph<'_>,
135        encoder: &mut crate::renderer::RenderEncoder,
136    ) -> Result<EncodedGraph, GraphError> {
137        self.validate_size(graph.size)?;
138        let plan = graph.compile()?;
139        if matches!(graph.nodes[plan.output.index], RenderNode::Source(_)) {
140            return Err(GraphError::SourceOutput);
141        }
142        let format = self.intermediate_format();
143        let uniform_alignment = u64::from(self.limits.min_uniform_buffer_offset_alignment.max(1));
144        let uniform_capacity = graph_uniform_capacity(self, graph, &plan.order, uniform_alignment);
145        let mut uniforms = self
146            .uniform_buffers
147            .acquire(self.gpu.device(), uniform_capacity);
148        let mut outputs: Vec<Option<RenderedTexture>> =
149            (0..graph.nodes.len()).map(|_| None).collect();
150        let blend_space = graph.blend_space.unwrap_or(self.blend_space).shader_value();
151        let cached = self.cached_results(graph, &plan.order, plan.output, blend_space);
152        let pixels = u64::from(graph.size.width) * u64::from(graph.size.height);
153        let predicted = plan
154            .order
155            .iter()
156            .filter(|node| cached.hits[node.index].is_none() && cached.needed[node.index])
157            .fold(crate::FrameCost::default(), |mut total, node| {
158                if let Some(cost) = self.node_cost(&graph.nodes[node.index]) {
159                    total.add(cost, pixels);
160                }
161                total
162            });
163        self.plan_submissions(encoder, predicted);
164        let mut pending_timings = Vec::<PendingEffectTiming>::new();
165        let mut encoded_nodes = 0_u64;
166        let mut encoded_render_passes = 0_u64;
167        let mut cache_inserts = Vec::new();
168        let timing = self.pass_timing_epoch();
169        for (position, node_id) in plan.order.iter().copied().enumerate() {
170            let started = timing.map(|epoch| (epoch, Instant::now()));
171            if let Some(hit) = cached.hits[node_id.index].clone() {
172                outputs[node_id.index] = Some(hit);
173                release_inputs(graph, &plan.last_use, &mut outputs, node_id, position);
174                continue;
175            }
176            if !cached.needed[node_id.index] {
177                // A cache hit still releases its inputs where drawing would have.
178                release_inputs(graph, &plan.last_use, &mut outputs, node_id, position);
179                continue;
180            }
181            // Recorded for the report of a loss this submission causes.
182            if let RenderNode::Effect { effect, .. }
183            | RenderNode::Transition { effect, .. }
184            | RenderNode::Generator { effect, .. } = &graph.nodes[node_id.index]
185            {
186                self.submission_clock.note_effect(effect);
187            }
188            let output = match &graph.nodes[node_id.index] {
189                RenderNode::Source(_) => None,
190                RenderNode::Effect {
191                    effect,
192                    source,
193                    inputs,
194                    params,
195                    resources,
196                    runtime,
197                } => {
198                    if self
199                        .effect_definition(effect)
200                        .is_some_and(|definition| definition.kind != EffectKind::Effect)
201                    {
202                        return Err(GraphError::EffectKindMismatch);
203                    }
204                    let source_view = graph_view(graph, &outputs, *source)?;
205                    let input_views: Vec<(&str, &wgpu::TextureView)> = inputs
206                        .iter()
207                        .map(|(name, node)| Ok((name, graph_view(graph, &outputs, node)?)))
208                        .collect::<Result<_, GraphError>>()?;
209                    let target = self.textures.acquire(
210                        self.gpu.device(),
211                        graph.size,
212                        format,
213                        "alpha2 graph effect target",
214                    );
215                    if let Some(pending) = self.encode_effect_pass(
216                        encoder,
217                        EffectPass {
218                            id: effect,
219                            kind: crate::RenderNodeKind::Effect,
220                            source_a: source_view,
221                            source_b: None,
222                            inputs: &input_views,
223                            size: graph.size,
224                            params,
225                            resources,
226                            runtime: *runtime,
227                            output: target.view(),
228                        },
229                        &mut uniforms,
230                    )? {
231                        pending_timings.push(pending);
232                    }
233                    encoded_render_passes = encoded_render_passes.saturating_add(1);
234                    Some(RenderedTexture::pooled(graph.size, format, target))
235                }
236                RenderNode::Transition {
237                    effect,
238                    from,
239                    to,
240                    params,
241                    resources,
242                    runtime,
243                } => {
244                    if self
245                        .effect_definition(effect)
246                        .is_some_and(|definition| definition.kind != EffectKind::Transition)
247                    {
248                        return Err(GraphError::EffectKindMismatch);
249                    }
250                    let from_view = graph_view(graph, &outputs, *from)?;
251                    let to_view = graph_view(graph, &outputs, *to)?;
252                    let target = self.textures.acquire(
253                        self.gpu.device(),
254                        graph.size,
255                        format,
256                        "alpha2 graph transition target",
257                    );
258                    if let Some(pending) = self.encode_effect_pass(
259                        encoder,
260                        EffectPass {
261                            id: effect,
262                            kind: crate::RenderNodeKind::Transition,
263                            source_a: from_view,
264                            source_b: Some(to_view),
265                            inputs: &[],
266                            size: graph.size,
267                            params,
268                            resources,
269                            runtime: *runtime,
270                            output: target.view(),
271                        },
272                        &mut uniforms,
273                    )? {
274                        pending_timings.push(pending);
275                    }
276                    encoded_render_passes = encoded_render_passes.saturating_add(1);
277                    Some(RenderedTexture::pooled(graph.size, format, target))
278                }
279                RenderNode::Generator {
280                    effect,
281                    params,
282                    resources,
283                    runtime,
284                } => {
285                    if self
286                        .effect_definition(effect)
287                        .is_some_and(|definition| definition.kind != EffectKind::Generator)
288                    {
289                        return Err(GraphError::EffectKindMismatch);
290                    }
291                    let target = self.textures.acquire(
292                        self.gpu.device(),
293                        graph.size,
294                        format,
295                        "alpha2 graph generator target",
296                    );
297                    // A generator samples nothing, but its bindings still need a texture.
298                    if let Some(pending) = self.encode_effect_pass(
299                        encoder,
300                        EffectPass {
301                            id: effect,
302                            kind: crate::RenderNodeKind::Generator,
303                            source_a: &self.empty_input,
304                            source_b: None,
305                            inputs: &[],
306                            size: graph.size,
307                            params,
308                            resources,
309                            runtime: *runtime,
310                            output: target.view(),
311                        },
312                        &mut uniforms,
313                    )? {
314                        pending_timings.push(pending);
315                    }
316                    encoded_render_passes = encoded_render_passes.saturating_add(1);
317                    Some(RenderedTexture::pooled(graph.size, format, target))
318                }
319                RenderNode::Vector { scene } => {
320                    encoder.place_pass(crate::submission_planner::VECTOR_COST, pixels, false);
321                    let target = self.textures.acquire(
322                        self.gpu.device(),
323                        graph.size,
324                        format,
325                        "alpha2 graph vector target",
326                    );
327                    encoded_render_passes =
328                        encoded_render_passes.saturating_add(self.encode_vector(
329                            encoder,
330                            scene,
331                            graph.size,
332                            graph.blend_space.unwrap_or(self.blend_space),
333                            target.view(),
334                            &mut uniforms,
335                        )?);
336                    Some(RenderedTexture::pooled(graph.size, format, target))
337                }
338                RenderNode::Composite { layers, background } => {
339                    encoder.place_pass(
340                        crate::submission_planner::COMPOSITE_LAYER_COST
341                            .times(u64::try_from(layers.len()).unwrap_or(u64::MAX)),
342                        pixels,
343                        false,
344                    );
345                    let views = layers
346                        .iter()
347                        .map(|layer| graph_view(graph, &outputs, layer.source))
348                        .collect::<Result<Vec<_>, _>>()?;
349                    let composite_layers = layers
350                        .iter()
351                        .zip(&views)
352                        .map(|(layer, view)| CompositeLayer {
353                            texture: view,
354                            visual: layer.visual,
355                            blend_mode: layer.blend_mode,
356                        })
357                        .collect::<Vec<_>>();
358                    let target = self.textures.acquire(
359                        self.gpu.device(),
360                        graph.size,
361                        format,
362                        "alpha2 graph composite target",
363                    );
364                    encoded_render_passes =
365                        encoded_render_passes.saturating_add(self.encode_layers_pass(
366                            encoder,
367                            graph.size,
368                            &composite_layers,
369                            CompositeTarget {
370                                background: *background,
371                                blend_space: graph.blend_space.unwrap_or(self.blend_space),
372                                output: target.texture(),
373                                output_view: target.view(),
374                            },
375                            &mut uniforms,
376                        )?);
377                    Some(RenderedTexture::pooled(graph.size, format, target))
378                }
379            };
380            if let Some((epoch, started)) = started
381                && !matches!(graph.nodes[node_id.index], RenderNode::Source(_))
382            {
383                let label = graph.nodes[node_id.index].kind().pass_label();
384                crate::diagnostics::record_pass(
385                    &self.diagnostics,
386                    epoch,
387                    label,
388                    started.elapsed(),
389                    crate::TimingSource::CpuEncoding,
390                );
391            }
392            if let (Some(output), Some((key, pinned))) = (&output, cached.store[node_id.index]) {
393                cache_inserts.push((key, output.clone(), pinned));
394            }
395            outputs[node_id.index] = output;
396            if !matches!(graph.nodes[node_id.index], RenderNode::Source(_)) {
397                encoded_nodes = encoded_nodes.saturating_add(1);
398            }
399            release_inputs(graph, &plan.last_use, &mut outputs, node_id, position);
400        }
401
402        let texture = outputs[plan.output.index]
403            .take()
404            .ok_or(GraphError::SourceOutput)?;
405        Ok(EncodedGraph {
406            texture,
407            cache_inserts,
408            uniforms,
409            pending_timings,
410            encoded_nodes,
411            encoded_render_passes,
412        })
413    }
414
415    /// Predicted per-pixel cost of drawing `node`; `None` for sources.
416    fn node_cost(&self, node: &RenderNode<'_>) -> Option<crate::effect_cost::EffectCost> {
417        match node {
418            RenderNode::Effect { effect, .. }
419            | RenderNode::Transition { effect, .. }
420            | RenderNode::Generator { effect, .. } => {
421                self.effects.get(effect).map(|registered| registered.cost)
422            }
423            RenderNode::Composite { layers, .. } => Some(
424                crate::submission_planner::COMPOSITE_LAYER_COST
425                    .times(u64::try_from(layers.len()).unwrap_or(u64::MAX)),
426            ),
427            RenderNode::Vector { .. } => Some(crate::submission_planner::VECTOR_COST),
428            RenderNode::Source(_) => None,
429        }
430    }
431
432    pub(crate) fn complete_graph_work(
433        &self,
434        encoded: EncodedGraph,
435        standalone_submission: bool,
436    ) -> RenderedTexture {
437        let EncodedGraph {
438            texture,
439            cache_inserts,
440            uniforms,
441            pending_timings,
442            encoded_nodes,
443            encoded_render_passes,
444        } = encoded;
445        uniforms.recycle();
446        if !cache_inserts.is_empty() {
447            let mut cache = self.lock_result_cache();
448            for (key, result, pinned) in &cache_inserts {
449                cache.insert(*key, result, *pinned);
450            }
451        }
452        crate::diagnostics::record_graph_work(
453            &self.diagnostics,
454            encoded_nodes,
455            encoded_render_passes,
456            standalone_submission,
457        );
458        for timing in pending_timings {
459            timing.map_after_submit();
460        }
461        texture
462    }
463}
464
465fn graph_uniform_capacity(
466    renderer: &Renderer,
467    graph: &RenderGraph<'_>,
468    order: &[RenderNodeId],
469    alignment: u64,
470) -> u64 {
471    let mut capacity = 0_u64;
472    for node in order {
473        match &graph.nodes[node.index] {
474            RenderNode::Effect { effect, .. }
475            | RenderNode::Transition { effect, .. }
476            | RenderNode::Generator { effect, .. } => {
477                let parameter_bytes = renderer
478                    .effects
479                    .get(effect)
480                    .map_or(0, |registered| registered.definition.params.len());
481                let parameter_bytes =
482                    u64::try_from(param_uniform_bytes(parameter_bytes)).unwrap_or(u64::MAX);
483                capacity = aligned_uniform_end(capacity, parameter_bytes, alignment);
484                capacity = aligned_uniform_end(
485                    capacity,
486                    float_uniform_bytes(RUNTIME_UNIFORM_WORDS),
487                    alignment,
488                );
489            }
490            RenderNode::Composite { layers, .. } => {
491                let passes = renderer
492                    .composite_batch_count(layers.iter().map(|layer| layer.visual), graph.size);
493                for _ in 0..passes {
494                    capacity = aligned_uniform_end(
495                        capacity,
496                        float_uniform_bytes(BATCH_UNIFORM_FLOATS),
497                        alignment,
498                    );
499                }
500            }
501            RenderNode::Vector { scene } => {
502                let slices = scene
503                    .ops
504                    .iter()
505                    .filter(|op| !matches!(op, alpha2_geometry::VectorOp::PushLayer { .. }))
506                    .count()
507                    + 1;
508                for _ in 0..slices {
509                    capacity = aligned_uniform_end(
510                        capacity,
511                        crate::vector::VECTOR_UNIFORM_BYTES,
512                        alignment,
513                    );
514                }
515            }
516            RenderNode::Source(_) => {}
517        }
518    }
519    capacity
520}
521
522/// The texture a dependency is drawn from; `None` for a CPU source.
523fn graph_view<'a>(
524    graph: &'a RenderGraph<'_>,
525    outputs: &'a [Option<RenderedTexture>],
526    node: RenderNodeId,
527) -> Result<&'a wgpu::TextureView, GraphError> {
528    match &graph.nodes[node.index] {
529        RenderNode::Source(view) => view
530            .wgpu_view()
531            .ok_or(GraphError::Render(crate::RenderError::InvalidSource)),
532        _ => Ok(outputs[node.index]
533            .as_ref()
534            .expect("compiled dependency has an output")
535            .view()),
536    }
537}
538
539/// Returns to the pool the results `node` was the last to read.
540fn release_inputs(
541    graph: &RenderGraph<'_>,
542    last_use: &[Option<usize>],
543    outputs: &mut [Option<RenderedTexture>],
544    node: RenderNodeId,
545    position: usize,
546) {
547    for dependency in graph.nodes[node.index].dependencies() {
548        if last_use[dependency.index] == Some(position) {
549            outputs[dependency.index] = None;
550        }
551    }
552}
553
554/// Cache hits, results to keep, and nodes still to draw for one graph.
555struct CachedResults {
556    hits: Vec<Option<RenderedTexture>>,
557    store: Vec<Option<(RenderCacheKey, bool)>>,
558    needed: Vec<bool>,
559}
560
561impl Renderer {
562    /// Looks up cache candidates and marks which nodes hits make unnecessary. With the cache off or
563    /// no candidates, everything is drawn and nothing kept.
564    fn cached_results(
565        &self,
566        graph: &RenderGraph<'_>,
567        order: &[RenderNodeId],
568        output: RenderNodeId,
569        blend_space: f32,
570    ) -> CachedResults {
571        let count = graph.nodes.len();
572        let mut cached = CachedResults {
573            hits: vec![None; count],
574            store: vec![None; count],
575            needed: vec![true; count],
576        };
577        if graph.intents.iter().all(Option::is_none) {
578            return cached;
579        }
580        let mut cache = self.lock_result_cache();
581        if !cache.enabled() {
582            return cached;
583        }
584        let eligibility = graph.eligibility();
585        let (mut hits, mut misses, mut ineligible) = (0, 0, 0);
586        // From the output down: look a node up only if still needed; a hit's inputs are then needed
587        // only by others.
588        cached.needed = vec![false; count];
589        cached.needed[output.index] = true;
590        for node in order.iter().rev() {
591            if !cached.needed[node.index] {
592                continue;
593            }
594            if let Some(intent) = graph.intents[node.index] {
595                if eligibility[node.index] == crate::CacheEligibility::Eligible {
596                    let key = renderer_node_key(graph.cache_keys[node.index], blend_space);
597                    let pinned = intent == crate::CacheIntent::Pin;
598                    if let Some(texture) = cache.get(key, pinned) {
599                        cached.hits[node.index] = Some(texture);
600                        hits += 1;
601                        continue;
602                    }
603                    cached.store[node.index] = Some((key, pinned));
604                    misses += 1;
605                } else if intent != crate::CacheIntent::Bypass {
606                    ineligible += 1;
607                }
608            }
609            for input in graph.nodes[node.index].dependencies() {
610                cached.needed[input.index] = true;
611            }
612        }
613        let skipped = order
614            .iter()
615            .filter(|node| {
616                !cached.needed[node.index]
617                    && cached.hits[node.index].is_none()
618                    && !matches!(graph.nodes[node.index], RenderNode::Source(_))
619            })
620            .count() as u64;
621        cache.record(hits, misses, ineligible, skipped);
622        cached
623    }
624}