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, Pixels};
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.coverage,
133                    scene,
134                    graph.size,
135                    blend_space,
136                )?),
137                RenderNode::Composite { layers, background } => {
138                    let inputs = layers
139                        .iter()
140                        .map(|layer| LayerInput {
141                            image: output(&outputs, layer.source),
142                            visual: layer.visual,
143                            blend_mode: layer.blend_mode,
144                        })
145                        .collect::<Vec<_>>();
146                    Arc::new(composite(
147                        &self.workers,
148                        &inputs,
149                        CompositeSettings {
150                            size: graph.size,
151                            background: *background,
152                            blend_space,
153                            max_filter_samples: quality.max_filter_samples,
154                        },
155                    )?)
156                }
157            };
158            if !matches!(graph.nodes[node.index], RenderNode::Source(_)) {
159                drawn += 1;
160            }
161            if let Some((key, pinned)) = cached.store[node.index] {
162                inserts.push((key, Arc::clone(&image), pinned));
163            }
164            outputs[node.index] = Some(image);
165            high_water = high_water.max(
166                outputs
167                    .iter()
168                    .flatten()
169                    .map(|image| image.retained_bytes())
170                    .sum(),
171            );
172            release_inputs(graph, &plan.last_use, &mut outputs, node, position);
173        }
174        let image = outputs[plan.output.index]
175            .take()
176            .ok_or(GraphError::SourceOutput)?;
177        if !inserts.is_empty() {
178            let mut cache = self.lock_cache();
179            for (key, image, pinned) in &inserts {
180                cache.insert(*key, image, *pinned);
181            }
182        }
183        self.stats.graphs.fetch_add(1, Ordering::Relaxed);
184        self.stats.nodes.fetch_add(drawn, Ordering::Relaxed);
185        self.stats.render_micros.fetch_add(
186            u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX),
187            Ordering::Relaxed,
188        );
189        self.stats
190            .image_high_water_bytes
191            .fetch_max(high_water, Ordering::Relaxed);
192        Ok(image)
193    }
194
195    /// Draws `graph` and packs it as YUV420 with `encoding` directly into the caller's planes; the
196    /// CPU counterpart of [`crate::Renderer::render_graph_yuv420p`].
197    ///
198    /// # Errors
199    /// [`Self::execute_graph`]'s errors, and [`RenderError::TextureReadback`] for an odd size or a
200    /// target too small.
201    pub fn render_graph_into_yuv420p(
202        &self,
203        graph: &RenderGraph<'_>,
204        encoding: YuvEncoding,
205        targets: &mut Yuv420pTargets<'_>,
206    ) -> Result<(), RenderError> {
207        let image = self
208            .execute_graph(graph)
209            .map_err(GraphError::into_render_error)?;
210        let started = Instant::now();
211        super::pack::pack_yuv420p(&self.workers, &image, encoding, targets)?;
212        self.stats.render_micros.fetch_add(
213            u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX),
214            Ordering::Relaxed,
215        );
216        Ok(())
217    }
218
219    fn draw_kernel(
220        &self,
221        graph: &RenderGraph<'_>,
222        outputs: &[Option<Arc<CpuImage>>],
223        node: &KernelNode<'_>,
224    ) -> Result<CpuImage, GraphError> {
225        let entry = self.entry(node.effect).ok_or(RenderError::UnknownEffect)?;
226        let definition = &entry.definition;
227        if definition.kind != node.kind {
228            return Err(GraphError::EffectKindMismatch);
229        }
230        crate::runtime_effect::validate_runtime(node.runtime)?;
231        crate::runtime_effect::encode_params(&definition.params, node.params)?;
232        if node
233            .resources
234            .iter()
235            .any(|(name, _)| !definition.resources.iter().any(|spec| spec.name == name))
236        {
237            return Err(RenderError::InvalidEffectParams.into());
238        }
239        let mut resources: Vec<(&str, &CpuLut)> = Vec::with_capacity(definition.resources.len());
240        for spec in &definition.resources {
241            let id = node
242                .resources
243                .get(&spec.name)
244                .ok_or(RenderError::MissingEffectResource)?;
245            let lut = self
246                .resource(id)
247                .ok_or(RenderError::MissingEffectResource)?;
248            resources.push((spec.name.as_str(), lut));
249        }
250        if node
251            .inputs
252            .iter()
253            .any(|(name, _)| !definition.inputs.iter().any(|spec| spec.name == *name))
254        {
255            return Err(RenderError::InvalidEffectParams.into());
256        }
257        let mut inputs = Vec::with_capacity(definition.inputs.len());
258        for spec in &definition.inputs {
259            let (_, input) = node
260                .inputs
261                .iter()
262                .find(|(name, _)| *name == spec.name)
263                .ok_or(RenderError::MissingEffectInput)?;
264            inputs.push((spec.name.as_str(), output(outputs, *input)));
265        }
266        let transparent = CpuImage::transparent(graph.size);
267        let source_a = node.source_a.map_or(&transparent, |id| output(outputs, id));
268        let source_b = node.source_b.map_or(source_a, |id| output(outputs, id));
269        let context = KernelContext {
270            size: graph.size,
271            definition,
272            params: node.params,
273            runtime: node.runtime,
274            max_filter_samples: self.quality_controls().max_filter_samples,
275            source_a,
276            source_b,
277            inputs: &inputs,
278            resources: &resources,
279        };
280        let kernel = entry.kernel;
281        // Kernel bounds are in input pixels, which match the output only at output size. A source
282        // presented at another size is sampled in uv and can reach every pixel.
283        let matching = [source_a, source_b]
284            .into_iter()
285            .chain(inputs.iter().map(|(_, image)| *image))
286            .all(|image| image.size() == graph.size);
287        let bounds = if matching {
288            kernel.bounds(&context)
289        } else {
290            PixelBox::whole(graph.size)
291        }
292        .intersect(PixelBox::whole(graph.size));
293        if bounds.is_empty() {
294            return Ok(CpuImage::transparent(graph.size));
295        }
296        let width = bounds.width as usize;
297        let mut pixels = vec![[0_u16; 4]; bounds.pixels()];
298        self.workers.bands(&mut pixels, width, |first_row, band| {
299            let mut row = vec![[0.0_f32; 4]; width];
300            for (index, line) in band.chunks_mut(width).enumerate() {
301                #[allow(clippy::cast_possible_truncation)]
302                let y = bounds.y + (first_row + index) as u32;
303                kernel.row(&context, y, bounds.x, &mut row);
304                for (out, value) in line.iter_mut().zip(&row) {
305                    *out = half::store4(*value);
306                }
307            }
308        });
309        Ok(CpuImage::new(graph.size, bounds, Pixels::Half(pixels)))
310    }
311
312    /// Result-cache lookups, as the GPU executor's `cached_results`.
313    fn cached_results(
314        &self,
315        graph: &RenderGraph<'_>,
316        order: &[RenderNodeId],
317        output: RenderNodeId,
318        blend_space: f32,
319    ) -> CachedResults {
320        let count = graph.nodes.len();
321        let mut cached = CachedResults {
322            hits: vec![None; count],
323            store: vec![None; count],
324            needed: vec![true; count],
325        };
326        if graph.intents.iter().all(Option::is_none) {
327            return cached;
328        }
329        let mut cache = self.lock_cache();
330        if !cache.enabled() {
331            return cached;
332        }
333        let eligibility = graph.eligibility();
334        let (mut hits, mut misses, mut ineligible) = (0, 0, 0);
335        cached.needed = vec![false; count];
336        cached.needed[output.index] = true;
337        for node in order.iter().rev() {
338            if !cached.needed[node.index] {
339                continue;
340            }
341            if let Some(intent) = graph.intents[node.index] {
342                if eligibility[node.index] == CacheEligibility::Eligible {
343                    let key = renderer_node_key(graph.cache_keys[node.index], blend_space);
344                    let pinned = intent == CacheIntent::Pin;
345                    if let Some(image) = cache.get(key, pinned) {
346                        cached.hits[node.index] = Some(image);
347                        hits += 1;
348                        continue;
349                    }
350                    cached.store[node.index] = Some((key, pinned));
351                    misses += 1;
352                } else if intent != CacheIntent::Bypass {
353                    ineligible += 1;
354                }
355            }
356            for input in graph.nodes[node.index].dependencies() {
357                cached.needed[input.index] = true;
358            }
359        }
360        let skipped = order
361            .iter()
362            .filter(|node| {
363                !cached.needed[node.index]
364                    && cached.hits[node.index].is_none()
365                    && !matches!(graph.nodes[node.index], RenderNode::Source(_))
366            })
367            .count() as u64;
368        cache.record(hits, misses, ineligible, skipped);
369        cached
370    }
371}
372
373struct CachedResults {
374    hits: Vec<Option<Arc<CpuImage>>>,
375    store: Vec<Option<(RenderCacheKey, bool)>>,
376    needed: Vec<bool>,
377}
378
379fn output(outputs: &[Option<Arc<CpuImage>>], node: RenderNodeId) -> &CpuImage {
380    outputs[node.index]
381        .as_deref()
382        .expect("compiled dependency has an output")
383}
384
385/// Drops the results `node` was the last to read.
386fn release_inputs(
387    graph: &RenderGraph<'_>,
388    last_use: &[Option<usize>],
389    outputs: &mut [Option<Arc<CpuImage>>],
390    node: RenderNodeId,
391    position: usize,
392) {
393    for dependency in graph.nodes[node.index].dependencies() {
394        if last_use[dependency.index] == Some(position) {
395            outputs[dependency.index] = None;
396        }
397    }
398}