Skip to main content

alpha2_render/cpu/
renderer.rs

1//! The CPU renderer: its catalog of built-ins, its controls, and its
2//! result cache.
3
4use std::collections::{HashMap, HashSet};
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
7
8use super::image::CpuImage;
9use super::kernels::{CpuKernel, CpuLut, kernel};
10use super::workers::Workers;
11use crate::result_cache::{CachedPicture, ResultCache};
12use crate::{
13    BlendSpace, CacheInvalidation, EffectCatalog, EffectDef, EffectId, GraphError, Lut3dData,
14    RenderCacheKey, RenderControls, RenderError, RenderGraph, RenderQualityControls,
15    ResultCacheStats,
16};
17
18impl CachedPicture for Arc<CpuImage> {
19    fn cached_bytes(&self) -> u64 {
20        self.retained_bytes()
21    }
22}
23
24/// One built-in the CPU renderer draws.
25pub(crate) struct CatalogEntry {
26    pub(crate) definition: EffectDef,
27    pub(crate) reads_time: bool,
28    pub(crate) kernel: &'static dyn CpuKernel,
29}
30
31/// Counts of what the CPU renderer has drawn, for metrics.
32#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
33pub struct CpuRenderStats {
34    /// Graphs drawn.
35    pub graphs: u64,
36    /// Nodes drawn (not found in the result cache).
37    pub nodes: u64,
38    /// Time spent drawing graphs, in microseconds.
39    pub render_micros: u64,
40    /// Sources presented: decoded frames converted to pictures.
41    pub presented_sources: u64,
42    /// Bytes of the largest picture set one graph held at once.
43    pub image_high_water_bytes: u64,
44}
45
46#[derive(Default)]
47pub(crate) struct StatCounters {
48    pub(crate) graphs: AtomicU64,
49    pub(crate) nodes: AtomicU64,
50    pub(crate) render_micros: AtomicU64,
51    pub(crate) presented_sources: AtomicU64,
52    pub(crate) image_high_water_bytes: AtomicU64,
53}
54
55/// A renderer that draws [`RenderGraph`]s on the CPU, with no GPU and no
56/// graphics driver.
57///
58/// It draws every built-in effect, transition and pass of the GPU
59/// [`crate::Renderer`], to the same contract: the same graph, the same
60/// stages, and pictures stored as the GPU's half-float intermediate stores
61/// them. Effects registered at run time as WGSL have no CPU implementation;
62/// a graph that draws one fails with [`RenderError::UnknownEffect`].
63///
64/// Its output is the same for any thread count and on every target.
65pub struct CpuRenderer {
66    pub(crate) workers: Workers,
67    pub(crate) blend_space: BlendSpace,
68    quality: Mutex<RenderQualityControls>,
69    cache: Mutex<ResultCache<Arc<CpuImage>>>,
70    catalog: HashMap<EffectId, CatalogEntry>,
71    internal: HashSet<EffectId>,
72    resources: HashMap<String, Arc<CpuLut>>,
73    /// Vector coverage kept between frames, as the GPU's vector pass keeps it.
74    pub(crate) coverage: Mutex<alpha2_geometry::raster::CoverageCache>,
75    pub(crate) stats: StatCounters,
76    frames: AtomicU64,
77    repeated: AtomicU64,
78}
79
80impl std::fmt::Debug for CpuRenderer {
81    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        formatter
83            .debug_struct("CpuRenderer")
84            .field("threads", &self.workers.threads())
85            .field("blend_space", &self.blend_space)
86            .finish_non_exhaustive()
87    }
88}
89
90impl CpuRenderer {
91    /// A CPU renderer drawing on `threads` threads (one draws on the
92    /// calling thread), with every built-in registered.
93    #[must_use]
94    pub fn new(threads: usize) -> Self {
95        let mut catalog = HashMap::new();
96        let mut internal = HashSet::new();
97        for (definition, is_internal) in crate::builtins::internal_effect_definitions()
98            .into_iter()
99            .map(|definition| (definition, true))
100            .chain(
101                crate::builtin_effect_definitions()
102                    .into_iter()
103                    .chain(crate::builtin_transition_definitions())
104                    .map(|definition| (definition, false)),
105            )
106        {
107            let Some(kernel) = kernel(&definition.id, definition.version) else {
108                continue;
109            };
110            let id = EffectId::new(definition.id.clone(), definition.version);
111            if is_internal {
112                internal.insert(id.clone());
113            }
114            catalog.insert(
115                id,
116                CatalogEntry {
117                    reads_time: crate::registry::body_reads_time(&definition.wgsl_body),
118                    definition,
119                    kernel,
120                },
121            );
122        }
123        Self {
124            workers: Workers::new(threads),
125            blend_space: BlendSpace::default(),
126            quality: Mutex::new(RenderQualityControls::default()),
127            cache: Mutex::new(ResultCache::default()),
128            catalog,
129            internal,
130            resources: HashMap::new(),
131            coverage: Mutex::new(alpha2_geometry::raster::CoverageCache::default()),
132            stats: StatCounters::default(),
133            frames: AtomicU64::new(0),
134            repeated: AtomicU64::new(0),
135        }
136    }
137
138    /// The renderer with composites blending in `blend_space` unless a
139    /// graph sets its own.
140    #[must_use]
141    pub const fn with_blend_space(mut self, blend_space: BlendSpace) -> Self {
142        self.blend_space = blend_space;
143        self
144    }
145
146    /// Threads the renderer draws on.
147    #[must_use]
148    pub const fn threads(&self) -> usize {
149        self.workers.threads()
150    }
151
152    /// The quality controls passes take now.
153    #[must_use]
154    pub fn quality_controls(&self) -> RenderQualityControls {
155        *self.quality.lock().unwrap_or_else(PoisonError::into_inner)
156    }
157
158    /// Every built-in effect and transition the GPU registers that this
159    /// renderer cannot draw, as `(id, version)`. Empty: every built-in has a
160    /// CPU kernel.
161    #[must_use]
162    pub fn unsupported_builtins() -> Vec<(String, u32)> {
163        crate::builtin_effect_definitions()
164            .into_iter()
165            .chain(crate::builtin_transition_definitions())
166            .chain(crate::builtins::internal_effect_definitions())
167            .filter(|definition| kernel(&definition.id, definition.version).is_none())
168            .map(|definition| (definition.id, definition.version))
169            .collect()
170    }
171
172    /// Keeps up to `budget_bytes` of drawn node results for reuse across
173    /// graphs, as [`crate::Renderer::set_result_cache_budget`] does.
174    pub fn set_result_cache_budget(&self, budget_bytes: u64) {
175        self.lock_cache().set_budget(budget_bytes);
176    }
177
178    /// The result cache's counters.
179    #[must_use]
180    pub fn result_cache_stats(&self) -> ResultCacheStats {
181        self.lock_cache().stats()
182    }
183
184    /// Drops every cached result.
185    pub fn clear_result_cache(&self) {
186        self.lock_cache().clear(CacheInvalidation::Explicit);
187    }
188
189    /// What the renderer has drawn so far.
190    #[must_use]
191    pub fn stats(&self) -> CpuRenderStats {
192        CpuRenderStats {
193            graphs: self.stats.graphs.load(Ordering::Relaxed),
194            nodes: self.stats.nodes.load(Ordering::Relaxed),
195            render_micros: self.stats.render_micros.load(Ordering::Relaxed),
196            presented_sources: self.stats.presented_sources.load(Ordering::Relaxed),
197            image_high_water_bytes: self.stats.image_high_water_bytes.load(Ordering::Relaxed),
198        }
199    }
200
201    /// Frames presented and frames that repeated the one before, as
202    /// [`RenderControls::record_frame_outcome`] counted them.
203    #[must_use]
204    pub fn frame_outcomes(&self) -> (u64, u64) {
205        (
206            self.frames.load(Ordering::Relaxed),
207            self.repeated.load(Ordering::Relaxed),
208        )
209    }
210
211    /// Registers or replaces the 3D lookup table `id`, as
212    /// [`crate::Renderer::register_lut3d`] does.
213    ///
214    /// # Errors
215    /// Returns [`RenderError::InvalidEffectResource`] for an empty identity
216    /// or an invalid cube.
217    pub fn register_lut3d(
218        &mut self,
219        id: impl Into<String>,
220        data: &Lut3dData,
221    ) -> Result<(), RenderError> {
222        let id = id.into();
223        if id.is_empty() || !data.is_valid(u32::MAX) {
224            return Err(RenderError::InvalidEffectResource);
225        }
226        // Stored as the GPU's half-float texture stores it.
227        let values = data
228            .values
229            .iter()
230            .map(|rgb| rgb.map(|value| half::f16::from_f32(value).to_f32()))
231            .collect();
232        self.resources.insert(
233            id,
234            Arc::new(CpuLut {
235                edge: data.edge_length,
236                domain_min: data.domain_min,
237                domain_max: data.domain_max,
238                values,
239            }),
240        );
241        self.lock_cache().clear(CacheInvalidation::EffectResources);
242        Ok(())
243    }
244
245    /// Removes every resource and clears the cached results.
246    pub fn clear_effect_resources(&mut self) {
247        self.resources.clear();
248        self.lock_cache().clear(CacheInvalidation::EffectResources);
249    }
250
251    /// Lets go of the cached results, which are kept only for reuse, and
252    /// returns the bytes they held.
253    pub fn release_idle_memory(&self) -> u64 {
254        let mut cache = self.lock_cache();
255        let bytes = cache.stats().bytes;
256        cache.clear(CacheInvalidation::Explicit);
257        bytes
258    }
259
260    /// Removes the resource `id` and returns whether it existed.
261    pub fn unregister_effect_resource(&mut self, id: &str) -> bool {
262        let removed = self.resources.remove(id).is_some();
263        if removed {
264            self.lock_cache().clear(CacheInvalidation::EffectResources);
265        }
266        removed
267    }
268
269    pub(crate) fn resource(&self, id: &str) -> Option<&Arc<CpuLut>> {
270        self.resources.get(id)
271    }
272
273    pub(crate) fn entry(&self, id: &EffectId) -> Option<&CatalogEntry> {
274        self.catalog.get(id)
275    }
276
277    pub(crate) fn lock_cache(&self) -> MutexGuard<'_, ResultCache<Arc<CpuImage>>> {
278        self.cache.lock().unwrap_or_else(PoisonError::into_inner)
279    }
280}
281
282impl EffectCatalog for CpuRenderer {
283    fn effect_definition(&self, id: &EffectId) -> Option<&EffectDef> {
284        self.catalog.get(id).map(|entry| &entry.definition)
285    }
286
287    fn effect_reads_time(&self, id: &EffectId) -> bool {
288        if let Some(passes) = crate::gaussian_blur::expanded_passes(id.as_str()) {
289            return passes
290                .iter()
291                .any(|pass| self.effect_reads_time(&EffectId::from_static(pass, 1)));
292        }
293        self.catalog.get(id).is_none_or(|entry| entry.reads_time)
294    }
295
296    fn project_effect_id(&self, id: &str, version: u32) -> Option<&EffectId> {
297        let key = EffectId::new(id.to_owned(), version);
298        self.catalog
299            .get_key_value(&key)
300            .map(|(id, _)| id)
301            .filter(|id| !self.internal.contains(*id))
302    }
303}
304
305impl RenderControls for CpuRenderer {
306    fn set_quality_controls(&self, max_filter_samples: u32, motion_blur_subframes: u32) {
307        let next = RenderQualityControls {
308            max_filter_samples: max_filter_samples
309                .clamp(1, RenderQualityControls::MAX_FILTER_SAMPLES),
310            motion_blur_subframes: motion_blur_subframes.max(1),
311        };
312        let mut quality = self.quality.lock().unwrap_or_else(PoisonError::into_inner);
313        if *quality != next {
314            *quality = next;
315            drop(quality);
316            // Results drawn with other controls are other pictures.
317            self.lock_cache().clear(CacheInvalidation::Quality);
318        }
319    }
320
321    /// The CPU renderer keeps no pool of pictures to budget: a picture's
322    /// memory goes back to the allocator when its last reader is done.
323    fn set_texture_budget_bytes(&self, _budget_bytes: u64) {}
324
325    fn content_epoch(&self) -> u64 {
326        self.lock_cache().clears()
327    }
328
329    fn record_frame_outcome(&self, presented: bool, repeated: bool) {
330        if presented {
331            self.frames.fetch_add(1, Ordering::Relaxed);
332        }
333        if repeated {
334            self.repeated.fetch_add(1, Ordering::Relaxed);
335        }
336    }
337
338    fn graph_cache_key(&self, graph: &RenderGraph<'_>) -> Result<RenderCacheKey, GraphError> {
339        let quality = self.quality_controls();
340        let plan = graph.compile_with_quality(quality)?;
341        Ok(crate::graph_cache::cpu_renderer_cache_key(
342            plan.cache_key,
343            graph.blend_space.unwrap_or(self.blend_space).shader_value(),
344            quality,
345        ))
346    }
347}