Skip to main content

alpha2_render/cpu/
execute.rs

1//! Runs a render graph on the CPU with the GPU executor's walk (`graph_execute.rs`), drawing
2//! each node into a [`CpuImage`] by row bands.
3
4use std::sync::Arc;
5use std::sync::atomic::Ordering;
6
7use alpha2_media::YuvEncoding;
8use web_time::Instant;
9
10use super::CpuRenderer;
11use super::composite::{CompositeSettings, LayerInput, composite};
12use super::half;
13use super::image::{CpuImage, PixelBox};
14use super::kernels::{CpuLut, KernelContext};
15use crate::graph::{RenderGraph, RenderNode, RenderNodeId};
16use crate::graph_cache::renderer_node_key;
17use crate::{
18    CacheEligibility, CacheIntent, EffectId, EffectKind, EffectParams, EffectResources,
19    EffectRuntime, GraphError, RenderCacheKey, RenderError, Yuv420pTargets,
20};
21
22struct KernelNode<'a> {
23    effect: &'a EffectId,
24    kind: EffectKind,
25    source_a: Option<RenderNodeId>,
26    source_b: Option<RenderNodeId>,
27    inputs: Vec<(&'a str, RenderNodeId)>,
28    params: &'a EffectParams<'a>,
29    resources: &'a EffectResources,
30    runtime: EffectRuntime,
31}
32
33impl CpuRenderer {
34    /// Draws the reachable part of `graph` and returns its output.
35    ///
36    /// # Errors
37    /// A structural error or the first pass failure: [`RenderError::UnknownEffect`] for an effect
38    /// without a CPU kernel, [`RenderError::InvalidSource`] for a GPU source.
39    #[allow(clippy::too_many_lines)]
40    pub fn execute_graph(&self, graph: &RenderGraph<'_>) -> Result<Arc<CpuImage>, GraphError> {
41        let started = Instant::now();
42        let quality = self.quality_controls();
43        let plan = graph.compile_with_quality(quality)?;
44        if matches!(graph.nodes[plan.output.index], RenderNode::Source(_)) {
45            return Err(GraphError::SourceOutput);
46        }
47        let blend_space = graph.blend_space.unwrap_or(self.blend_space);
48        let cached =
49            self.cached_results(graph, &plan.order, plan.output, blend_space.shader_value());
50        let mut outputs: Vec<Option<Arc<CpuImage>>> = vec![None; graph.nodes.len()];
51        let mut inserts = Vec::new();
52        let mut drawn = 0_u64;
53        let mut high_water = 0_u64;
54        for (position, node) in plan.order.iter().copied().enumerate() {
55            if let Some(hit) = cached.hits[node.index].clone() {
56                outputs[node.index] = Some(hit);
57                release_inputs(graph, &plan.last_use, &mut outputs, node, position);
58                continue;
59            }
60            if !cached.needed[node.index] {
61                release_inputs(graph, &plan.last_use, &mut outputs, node, position);
62                continue;
63            }
64            let image = match &graph.nodes[node.index] {
65                RenderNode::Source(view) => {
66                    let source = view.cpu_source().ok_or(RenderError::InvalidSource)?;
67                    source.presented(self)
68                }
69                RenderNode::Effect {
70                    effect,
71                    source,
72                    inputs,
73                    params,
74                    resources,
75                    runtime,
76                } => Arc::new(self.draw_kernel(
77                    graph,
78                    &outputs,
79                    &KernelNode {
80                        effect,
81                        kind: EffectKind::Effect,
82                        source_a: Some(*source),
83                        source_b: None,
84                        inputs: inputs.iter().collect(),
85                        params,
86                        resources,
87                        runtime: *runtime,
88                    },
89                )?),
90                RenderNode::Transition {
91                    effect,
92                    from,
93                    to,
94                    params,
95                    resources,
96                    runtime,
97                } => Arc::new(self.draw_kernel(
98                    graph,
99                    &outputs,
100                    &KernelNode {
101                        effect,
102                        kind: EffectKind::Transition,
103                        source_a: Some(*from),
104                        source_b: Some(*to),
105                        inputs: Vec::new(),
106                        params,
107                        resources,
108                        runtime: *runtime,
109                    },
110                )?),
111                RenderNode::Generator {
112                    effect,
113                    params,
114                    resources,
115                    runtime,
116                } => Arc::new(self.draw_kernel(
117                    graph,
118                    &outputs,
119                    &KernelNode {
120                        effect,
121                        kind: EffectKind::Generator,
122                        source_a: None,
123                        source_b: None,
124                        inputs: Vec::new(),
125                        params,
126                        resources,
127                        runtime: *runtime,
128                    },
129                )?),
130                RenderNode::Vector { scene } => Arc::new(super::vector::draw(
131                    &self.workers,
132                    &self.frame_pool,
133                    &self.coverage,
134                    scene,
135                    graph.size,
136                    blend_space,
137                )?),
138                RenderNode::Composite { layers, background } => {
139                    let inputs = layers
140                        .iter()
141                        .map(|layer| LayerInput {
142                            image: output(&outputs, layer.source),
143                            visual: layer.visual,
144                            blend_mode: layer.blend_mode,
145                        })
146                        .collect::<Vec<_>>();
147                    Arc::new(composite(
148                        &self.workers,
149                        &self.frame_pool,
150                        &inputs,
151                        CompositeSettings {
152                            size: graph.size,
153                            background: *background,
154                            blend_space,
155                            max_filter_samples: quality.max_filter_samples,
156                        },
157                    )?)
158                }
159            };
160            if !matches!(graph.nodes[node.index], RenderNode::Source(_)) {
161                drawn += 1;
162            }
163            if let Some((key, pinned)) = cached.store[node.index] {
164                inserts.push((key, Arc::clone(&image), pinned));
165            }
166            outputs[node.index] = Some(image);
167            high_water = high_water.max(
168                outputs
169                    .iter()
170                    .flatten()
171                    .map(|image| image.retained_bytes())
172                    .sum(),
173            );
174            release_inputs(graph, &plan.last_use, &mut outputs, node, position);
175        }
176        let image = outputs[plan.output.index]
177            .take()
178            .ok_or(GraphError::SourceOutput)?;
179        if !inserts.is_empty() {
180            let mut cache = self.lock_cache();
181            for (key, image, pinned) in &inserts {
182                cache.insert(*key, image, *pinned);
183            }
184        }
185        self.stats.graphs.fetch_add(1, Ordering::Relaxed);
186        self.stats.nodes.fetch_add(drawn, Ordering::Relaxed);
187        self.stats.render_micros.fetch_add(
188            u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX),
189            Ordering::Relaxed,
190        );
191        self.stats
192            .image_high_water_bytes
193            .fetch_max(high_water, Ordering::Relaxed);
194        Ok(image)
195    }
196
197    /// Draws `graph` and packs it as YUV420 with `encoding` directly into the caller's planes; the
198    /// CPU counterpart of [`crate::Renderer::render_graph_yuv420p`].
199    ///
200    /// # Errors
201    /// [`Self::execute_graph`]'s errors, and [`RenderError::TextureReadback`] for an odd size or a
202    /// target too small.
203    pub fn render_graph_into_yuv420p(
204        &self,
205        graph: &RenderGraph<'_>,
206        encoding: YuvEncoding,
207        targets: &mut Yuv420pTargets<'_>,
208    ) -> Result<(), RenderError> {
209        let image = self
210            .execute_graph(graph)
211            .map_err(GraphError::into_render_error)?;
212        let started = Instant::now();
213        super::pack::pack_yuv420p(&self.workers, &image, encoding, targets)?;
214        self.stats.render_micros.fetch_add(
215            u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX),
216            Ordering::Relaxed,
217        );
218        Ok(())
219    }
220
221    fn draw_kernel(
222        &self,
223        graph: &RenderGraph<'_>,
224        outputs: &[Option<Arc<CpuImage>>],
225        node: &KernelNode<'_>,
226    ) -> Result<CpuImage, GraphError> {
227        let entry = self.entry(node.effect).ok_or(RenderError::UnknownEffect)?;
228        let definition = &entry.definition;
229        if definition.kind != node.kind {
230            return Err(GraphError::EffectKindMismatch);
231        }
232        crate::runtime_effect::validate_runtime(node.runtime)?;
233        crate::runtime_effect::encode_params(&definition.params, node.params)?;
234        if node
235            .resources
236            .iter()
237            .any(|(name, _)| !definition.resources.iter().any(|spec| spec.name == name))
238        {
239            return Err(RenderError::InvalidEffectParams.into());
240        }
241        let mut resources: Vec<(&str, &CpuLut)> = Vec::with_capacity(definition.resources.len());
242        for spec in &definition.resources {
243            let id = node
244                .resources
245                .get(&spec.name)
246                .ok_or(RenderError::MissingEffectResource)?;
247            let lut = self
248                .resource(id)
249                .ok_or(RenderError::MissingEffectResource)?;
250            resources.push((spec.name.as_str(), lut));
251        }
252        if node
253            .inputs
254            .iter()
255            .any(|(name, _)| !definition.inputs.iter().any(|spec| spec.name == *name))
256        {
257            return Err(RenderError::InvalidEffectParams.into());
258        }
259        let mut inputs = Vec::with_capacity(definition.inputs.len());
260        for spec in &definition.inputs {
261            let (_, input) = node
262                .inputs
263                .iter()
264                .find(|(name, _)| *name == spec.name)
265                .ok_or(RenderError::MissingEffectInput)?;
266            inputs.push((spec.name.as_str(), output(outputs, *input)));
267        }
268        let transparent = CpuImage::transparent(graph.size);
269        let source_a = node.source_a.map_or(&transparent, |id| output(outputs, id));
270        let source_b = node.source_b.map_or(source_a, |id| output(outputs, id));
271        let context = KernelContext {
272            size: graph.size,
273            definition,
274            params: node.params,
275            runtime: node.runtime,
276            max_filter_samples: self.quality_controls().max_filter_samples,
277            source_a,
278            source_b,
279            inputs: &inputs,
280            resources: &resources,
281        };
282        let kernel = entry.kernel;
283        // Kernel bounds are in input pixels, which match the output only at output size. A source
284        // presented at another size is sampled in uv and can reach every pixel.
285        let matching = [source_a, source_b]
286            .into_iter()
287            .chain(inputs.iter().map(|(_, image)| *image))
288            .all(|image| image.size() == graph.size);
289        let bounds = if matching {
290            kernel.bounds(&context)
291        } else {
292            PixelBox::whole(graph.size)
293        }
294        .intersect(PixelBox::whole(graph.size));
295        if bounds.is_empty() {
296            return Ok(CpuImage::transparent(graph.size));
297        }
298        let width = bounds.width as usize;
299        let mut pixels = self.frame_pool.take(bounds.pixels());
300        self.workers.bands_with(
301            &mut pixels,
302            width,
303            || vec![[0.0_f32; 4]; width],
304            |row, first_row, band| {
305                row.fill([0.0; 4]);
306                for (index, line) in band.chunks_mut(width).enumerate() {
307                    #[allow(clippy::cast_possible_truncation)]
308                    let y = bounds.y + (first_row + index) as u32;
309                    kernel.row(&context, y, bounds.x, row);
310                    for (out, value) in line.iter_mut().zip(row.iter()) {
311                        *out = half::store4(*value);
312                    }
313                }
314            },
315        );
316        Ok(CpuImage::pooled(
317            graph.size,
318            bounds,
319            pixels,
320            &self.frame_pool,
321        ))
322    }
323
324    /// Result-cache lookups, as the GPU executor's `cached_results`.
325    fn cached_results(
326        &self,
327        graph: &RenderGraph<'_>,
328        order: &[RenderNodeId],
329        output: RenderNodeId,
330        blend_space: f32,
331    ) -> CachedResults {
332        let count = graph.nodes.len();
333        let mut cached = CachedResults {
334            hits: vec![None; count],
335            store: vec![None; count],
336            needed: vec![true; count],
337        };
338        if graph.intents.iter().all(Option::is_none) {
339            return cached;
340        }
341        let mut cache = self.lock_cache();
342        if !cache.enabled() {
343            return cached;
344        }
345        let eligibility = graph.eligibility();
346        let (mut hits, mut misses, mut ineligible) = (0, 0, 0);
347        cached.needed = vec![false; count];
348        cached.needed[output.index] = true;
349        for node in order.iter().rev() {
350            if !cached.needed[node.index] {
351                continue;
352            }
353            if let Some(intent) = graph.intents[node.index] {
354                if eligibility[node.index] == CacheEligibility::Eligible {
355                    let key = renderer_node_key(graph.cache_keys[node.index], blend_space);
356                    let pinned = intent == CacheIntent::Pin;
357                    if let Some(image) = cache.get(key, pinned) {
358                        cached.hits[node.index] = Some(image);
359                        hits += 1;
360                        continue;
361                    }
362                    cached.store[node.index] = Some((key, pinned));
363                    misses += 1;
364                } else if intent != CacheIntent::Bypass {
365                    ineligible += 1;
366                }
367            }
368            for input in graph.nodes[node.index].dependencies() {
369                cached.needed[input.index] = true;
370            }
371        }
372        let skipped = order
373            .iter()
374            .filter(|node| {
375                !cached.needed[node.index]
376                    && cached.hits[node.index].is_none()
377                    && !matches!(graph.nodes[node.index], RenderNode::Source(_))
378            })
379            .count() as u64;
380        cache.record(hits, misses, ineligible, skipped);
381        cached
382    }
383}
384
385struct CachedResults {
386    hits: Vec<Option<Arc<CpuImage>>>,
387    store: Vec<Option<(RenderCacheKey, bool)>>,
388    needed: Vec<bool>,
389}
390
391fn output(outputs: &[Option<Arc<CpuImage>>], node: RenderNodeId) -> &CpuImage {
392    outputs[node.index]
393        .as_deref()
394        .expect("compiled dependency has an output")
395}
396
397/// Drops the results `node` was the last to read.
398fn release_inputs(
399    graph: &RenderGraph<'_>,
400    last_use: &[Option<usize>],
401    outputs: &mut [Option<Arc<CpuImage>>],
402    node: RenderNodeId,
403    position: usize,
404) {
405    for dependency in graph.nodes[node.index].dependencies() {
406        if last_use[dependency.index] == Some(position) {
407            outputs[dependency.index] = None;
408        }
409    }
410}