use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use super::image::CpuImage;
use super::kernels::{CpuKernel, CpuLut, kernel};
use super::workers::Workers;
use crate::result_cache::{CachedPicture, ResultCache};
use crate::{
BlendSpace, CacheInvalidation, EffectCatalog, EffectDef, EffectId, GraphError, Lut3dData,
RenderCacheKey, RenderControls, RenderError, RenderGraph, RenderQualityControls,
ResultCacheStats,
};
impl CachedPicture for Arc<CpuImage> {
fn cached_bytes(&self) -> u64 {
self.retained_bytes()
}
}
pub(crate) struct CatalogEntry {
pub(crate) definition: EffectDef,
pub(crate) reads_time: bool,
pub(crate) kernel: &'static dyn CpuKernel,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct CpuRenderStats {
pub graphs: u64,
pub nodes: u64,
pub render_micros: u64,
pub presented_sources: u64,
pub image_high_water_bytes: u64,
}
#[derive(Default)]
pub(crate) struct StatCounters {
pub(crate) graphs: AtomicU64,
pub(crate) nodes: AtomicU64,
pub(crate) render_micros: AtomicU64,
pub(crate) presented_sources: AtomicU64,
pub(crate) image_high_water_bytes: AtomicU64,
}
pub struct CpuRenderer {
pub(crate) workers: Workers,
pub(crate) blend_space: BlendSpace,
quality: Mutex<RenderQualityControls>,
cache: Mutex<ResultCache<Arc<CpuImage>>>,
catalog: HashMap<EffectId, CatalogEntry>,
internal: HashSet<EffectId>,
resources: HashMap<String, Arc<CpuLut>>,
pub(crate) coverage: Mutex<alpha2_geometry::raster::CoverageCache>,
pub(crate) stats: StatCounters,
frames: AtomicU64,
repeated: AtomicU64,
}
impl std::fmt::Debug for CpuRenderer {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("CpuRenderer")
.field("threads", &self.workers.threads())
.field("blend_space", &self.blend_space)
.finish_non_exhaustive()
}
}
impl CpuRenderer {
#[must_use]
pub fn new(threads: usize) -> Self {
let mut catalog = HashMap::new();
let mut internal = HashSet::new();
for (definition, is_internal) in crate::builtins::internal_effect_definitions()
.into_iter()
.map(|definition| (definition, true))
.chain(
crate::builtin_effect_definitions()
.into_iter()
.chain(crate::builtin_transition_definitions())
.map(|definition| (definition, false)),
)
{
let Some(kernel) = kernel(&definition.id, definition.version) else {
continue;
};
let id = EffectId::new(definition.id.clone(), definition.version);
if is_internal {
internal.insert(id.clone());
}
catalog.insert(
id,
CatalogEntry {
reads_time: crate::registry::body_reads_time(&definition.wgsl_body),
definition,
kernel,
},
);
}
Self {
workers: Workers::new(threads),
blend_space: BlendSpace::default(),
quality: Mutex::new(RenderQualityControls::default()),
cache: Mutex::new(ResultCache::default()),
catalog,
internal,
resources: HashMap::new(),
coverage: Mutex::new(alpha2_geometry::raster::CoverageCache::default()),
stats: StatCounters::default(),
frames: AtomicU64::new(0),
repeated: AtomicU64::new(0),
}
}
#[must_use]
pub const fn with_blend_space(mut self, blend_space: BlendSpace) -> Self {
self.blend_space = blend_space;
self
}
#[must_use]
pub const fn threads(&self) -> usize {
self.workers.threads()
}
#[must_use]
pub fn quality_controls(&self) -> RenderQualityControls {
*self.quality.lock().unwrap_or_else(PoisonError::into_inner)
}
#[must_use]
pub fn unsupported_builtins() -> Vec<(String, u32)> {
crate::builtin_effect_definitions()
.into_iter()
.chain(crate::builtin_transition_definitions())
.chain(crate::builtins::internal_effect_definitions())
.filter(|definition| kernel(&definition.id, definition.version).is_none())
.map(|definition| (definition.id, definition.version))
.collect()
}
pub fn set_result_cache_budget(&self, budget_bytes: u64) {
self.lock_cache().set_budget(budget_bytes);
}
#[must_use]
pub fn result_cache_stats(&self) -> ResultCacheStats {
self.lock_cache().stats()
}
pub fn clear_result_cache(&self) {
self.lock_cache().clear(CacheInvalidation::Explicit);
}
#[must_use]
pub fn stats(&self) -> CpuRenderStats {
CpuRenderStats {
graphs: self.stats.graphs.load(Ordering::Relaxed),
nodes: self.stats.nodes.load(Ordering::Relaxed),
render_micros: self.stats.render_micros.load(Ordering::Relaxed),
presented_sources: self.stats.presented_sources.load(Ordering::Relaxed),
image_high_water_bytes: self.stats.image_high_water_bytes.load(Ordering::Relaxed),
}
}
#[must_use]
pub fn frame_outcomes(&self) -> (u64, u64) {
(
self.frames.load(Ordering::Relaxed),
self.repeated.load(Ordering::Relaxed),
)
}
pub fn register_lut3d(
&mut self,
id: impl Into<String>,
data: &Lut3dData,
) -> Result<(), RenderError> {
let id = id.into();
if id.is_empty() || !data.is_valid(u32::MAX) {
return Err(RenderError::InvalidEffectResource);
}
let values = data
.values
.iter()
.map(|rgb| rgb.map(|value| half::f16::from_f32(value).to_f32()))
.collect();
self.resources.insert(
id,
Arc::new(CpuLut {
edge: data.edge_length,
domain_min: data.domain_min,
domain_max: data.domain_max,
values,
}),
);
self.lock_cache().clear(CacheInvalidation::EffectResources);
Ok(())
}
pub fn clear_effect_resources(&mut self) {
self.resources.clear();
self.lock_cache().clear(CacheInvalidation::EffectResources);
}
pub fn release_idle_memory(&self) -> u64 {
let mut cache = self.lock_cache();
let bytes = cache.stats().bytes;
cache.clear(CacheInvalidation::Explicit);
bytes
}
pub fn unregister_effect_resource(&mut self, id: &str) -> bool {
let removed = self.resources.remove(id).is_some();
if removed {
self.lock_cache().clear(CacheInvalidation::EffectResources);
}
removed
}
pub(crate) fn resource(&self, id: &str) -> Option<&Arc<CpuLut>> {
self.resources.get(id)
}
pub(crate) fn entry(&self, id: &EffectId) -> Option<&CatalogEntry> {
self.catalog.get(id)
}
pub(crate) fn lock_cache(&self) -> MutexGuard<'_, ResultCache<Arc<CpuImage>>> {
self.cache.lock().unwrap_or_else(PoisonError::into_inner)
}
}
impl EffectCatalog for CpuRenderer {
fn effect_definition(&self, id: &EffectId) -> Option<&EffectDef> {
self.catalog.get(id).map(|entry| &entry.definition)
}
fn effect_reads_time(&self, id: &EffectId) -> bool {
if let Some(passes) = crate::gaussian_blur::expanded_passes(id.as_str()) {
return passes
.iter()
.any(|pass| self.effect_reads_time(&EffectId::from_static(pass, 1)));
}
self.catalog.get(id).is_none_or(|entry| entry.reads_time)
}
fn project_effect_id(&self, id: &str, version: u32) -> Option<&EffectId> {
let key = EffectId::new(id.to_owned(), version);
self.catalog
.get_key_value(&key)
.map(|(id, _)| id)
.filter(|id| !self.internal.contains(*id))
}
}
impl RenderControls for CpuRenderer {
fn set_quality_controls(&self, max_filter_samples: u32, motion_blur_subframes: u32) {
let next = RenderQualityControls {
max_filter_samples: max_filter_samples
.clamp(1, RenderQualityControls::MAX_FILTER_SAMPLES),
motion_blur_subframes: motion_blur_subframes.max(1),
};
let mut quality = self.quality.lock().unwrap_or_else(PoisonError::into_inner);
if *quality != next {
*quality = next;
drop(quality);
self.lock_cache().clear(CacheInvalidation::Quality);
}
}
fn set_texture_budget_bytes(&self, _budget_bytes: u64) {}
fn content_epoch(&self) -> u64 {
self.lock_cache().clears()
}
fn record_frame_outcome(&self, presented: bool, repeated: bool) {
if presented {
self.frames.fetch_add(1, Ordering::Relaxed);
}
if repeated {
self.repeated.fetch_add(1, Ordering::Relaxed);
}
}
fn graph_cache_key(&self, graph: &RenderGraph<'_>) -> Result<RenderCacheKey, GraphError> {
let quality = self.quality_controls();
let plan = graph.compile_with_quality(quality)?;
Ok(crate::graph_cache::cpu_renderer_cache_key(
plan.cache_key,
graph.blend_space.unwrap_or(self.blend_space).shader_value(),
quality,
))
}
}