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 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 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 pub fn warmup_graph(&self, graph: &RenderGraph<'_>) -> Result<WarmupReport, ShaderDiagnostics> {
63 self.warmup_graphs(&[graph])
64 }
65
66 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 #[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 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 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 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 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 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
499fn 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
514struct CachedResults {
517 hits: Vec<Option<RenderedTexture>>,
518 store: Vec<Option<(RenderCacheKey, bool)>>,
519 needed: Vec<bool>,
520}
521
522impl Renderer {
523 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 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}