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