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)| Ok((name, graph_view(graph, &outputs, node)?)))
194 .collect::<Result<_, GraphError>>()?;
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::<Result<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>(
488 graph: &'a RenderGraph<'_>,
489 outputs: &'a [Option<RenderedTexture>],
490 node: RenderNodeId,
491) -> Result<&'a wgpu::TextureView, GraphError> {
492 match &graph.nodes[node.index] {
493 RenderNode::Source(view) => view
494 .wgpu_view()
495 .ok_or(GraphError::Render(crate::RenderError::InvalidSource)),
496 _ => Ok(outputs[node.index]
497 .as_ref()
498 .expect("compiled dependency has an output")
499 .view()),
500 }
501}
502
503fn release_inputs(
505 graph: &RenderGraph<'_>,
506 last_use: &[Option<usize>],
507 outputs: &mut [Option<RenderedTexture>],
508 node: RenderNodeId,
509 position: usize,
510) {
511 for dependency in graph.nodes[node.index].dependencies() {
512 if last_use[dependency.index] == Some(position) {
513 outputs[dependency.index] = None;
514 }
515 }
516}
517
518struct CachedResults {
521 hits: Vec<Option<RenderedTexture>>,
522 store: Vec<Option<(RenderCacheKey, bool)>>,
523 needed: Vec<bool>,
524}
525
526impl Renderer {
527 fn cached_results(
531 &self,
532 graph: &RenderGraph<'_>,
533 order: &[RenderNodeId],
534 output: RenderNodeId,
535 blend_space: f32,
536 ) -> CachedResults {
537 let count = graph.nodes.len();
538 let mut cached = CachedResults {
539 hits: vec![None; count],
540 store: vec![None; count],
541 needed: vec![true; count],
542 };
543 if graph.intents.iter().all(Option::is_none) {
544 return cached;
545 }
546 let mut cache = self.lock_result_cache();
547 if !cache.enabled() {
548 return cached;
549 }
550 let eligibility = graph.eligibility();
551 let (mut hits, mut misses, mut ineligible) = (0, 0, 0);
552 cached.needed = vec![false; count];
555 cached.needed[output.index] = true;
556 for node in order.iter().rev() {
557 if !cached.needed[node.index] {
558 continue;
559 }
560 if let Some(intent) = graph.intents[node.index] {
561 if eligibility[node.index] == crate::CacheEligibility::Eligible {
562 let key = renderer_node_key(graph.cache_keys[node.index], blend_space);
563 let pinned = intent == crate::CacheIntent::Pin;
564 if let Some(texture) = cache.get(key, pinned) {
565 cached.hits[node.index] = Some(texture);
566 hits += 1;
567 continue;
568 }
569 cached.store[node.index] = Some((key, pinned));
570 misses += 1;
571 } else if intent != crate::CacheIntent::Bypass {
572 ineligible += 1;
573 }
574 }
575 for input in graph.nodes[node.index].dependencies() {
576 cached.needed[input.index] = true;
577 }
578 }
579 let skipped = order
580 .iter()
581 .filter(|node| {
582 !cached.needed[node.index]
583 && cached.hits[node.index].is_none()
584 && !matches!(graph.nodes[node.index], RenderNode::Source(_))
585 })
586 .count() as u64;
587 cache.record(hits, misses, ineligible, skipped);
588 cached
589 }
590}