Skip to main content

alpha2_render/cpu/
execute.rs

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