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)>,
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 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 pub fn warmup_graph(&self, graph: &RenderGraph<'_>) -> Result<WarmupReport, ShaderDiagnostics> {
61 self.warmup_graphs(&[graph])
62 }
63
64 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 #[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 release_inputs(graph, &plan.last_use, &mut outputs, node_id, position);
179 continue;
180 }
181 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 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 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
522fn 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
539fn 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
554struct CachedResults {
556 hits: Vec<Option<RenderedTexture>>,
557 store: Vec<Option<(RenderCacheKey, bool)>>,
558 needed: Vec<bool>,
559}
560
561impl Renderer {
562 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 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}