Skip to main content

alpha2_render/cpu/
renderer.rs

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