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