//! [`Backend::Auto`](crate::Backend::Auto)'s per-comp choice between the CPU and the GPU //! compositor, from measured frame times. //! //! A light comp (a couple of small layers) composites faster on the CPU, which only touches //! the layers' bounds, than on the GPU, which works on whole frames and reads the result back; //! heavy comps (many layers, 3D, GPU effects) are faster on the GPU. Neither a layer count nor //! a pixel count predicts the crossover across machines, so Auto measures: per comp, output //! scale and path (readback / viewer display), it keeps an exponentially weighted average of //! the CPU and GPU frame times and renders on the faster one. Each side is warmed up first //! (its first frame, with layer cache misses and texture uploads, is not counted; the fastest of //! the next ones starts the average), and the slower side is re-measured every //! [`AutoPick::REPROBE`] frames so the choice follows the comp as it changes. use std::collections::HashMap; use std::sync::Mutex; use effectcraft_project::ItemId; /// Which frames share timing history: one comp at one output scale on one path. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct AutoKey { pub comp: ItemId, /// `RenderOpts::scale` bits. pub scale: u64, /// The viewer's display path (GPU frames stay on the device) rather than a readback. pub display: bool, } impl AutoKey { pub fn new(comp: ItemId, scale: f64, display: bool) -> AutoKey { AutoKey { comp, scale: scale.to_bits(), display } } } /// Timing history of one [`AutoKey`]. #[derive(Clone, Copy, Debug, Default)] pub struct AutoStats { /// Averaged ms/frame (`None` until a counted sample). pub cpu_ms: Option, pub gpu_ms: Option, /// Frames measured on each side (the first is the uncounted warm-up). pub cpu_frames: u32, pub gpu_frames: u32, /// Frames since the side not chosen was last measured. pub since_probe: u32, /// The GPU declined the last frame it was asked for (the CPU rendered it). pub declined: bool, } /// Per-comp CPU / GPU timing history for [`Backend::Auto`](crate::Backend::Auto) (held by the /// accelerator, see [`Accelerator::auto_pick`](crate::Accelerator::auto_pick)). #[derive(Default)] pub struct AutoPick { stats: Mutex>, } impl AutoPick { /// Frames measured on each side before choosing (the first is not counted). pub const WARMUP: u32 = 3; /// Weight of a new sample in the running average (slower / faster than the average). pub const ALPHA: f64 = 0.3; pub const ALPHA_DOWN: f64 = 0.5; /// Re-measure the side not chosen after this many frames. pub const REPROBE: u32 = 48; /// The CPU wins only when clearly faster (ties and noise stay on the GPU). pub const MARGIN: f64 = 0.85; /// Keys remembered before the history is reset. const MAX_KEYS: usize = 1024; /// Render the frame for `key` on the GPU? pub fn choose(&self, key: AutoKey) -> bool { let Ok(mut m) = self.stats.lock() else { return true }; if m.len() > Self::MAX_KEYS && !m.contains_key(&key) { m.clear(); } let s = m.entry(key).or_default(); // Warm up both sides, alternating, GPU first. let gpu_warm = s.declined || s.gpu_frames >= Self::WARMUP; if !gpu_warm && s.gpu_frames <= s.cpu_frames { return true; } if s.cpu_frames < Self::WARMUP { return false; } let gpu_faster = match (s.gpu_ms, s.cpu_ms) { (Some(g), Some(c)) => c >= g * Self::MARGIN, (g, _) => g.is_some(), }; // Periodically re-measure the slower side. if s.since_probe >= Self::REPROBE { s.since_probe = 0; return !gpu_faster; } s.since_probe += 1; gpu_faster } /// A frame for `key` took `ms` on the GPU (`gpu`) or the CPU. /// /// The warm-up keeps the fastest of its counted frames; after that the average follows /// improvements quickly and slow-downs gradually, and a single frame counts at most twice /// the average (a stall of the machine does not flip the choice for a whole re-probe /// period). pub fn record(&self, key: AutoKey, gpu: bool, ms: f64) { let Ok(mut m) = self.stats.lock() else { return }; let s = m.entry(key).or_default(); let (avg, frames) = if gpu { (&mut s.gpu_ms, &mut s.gpu_frames) } else { (&mut s.cpu_ms, &mut s.cpu_frames) }; *frames = frames.saturating_add(1); if gpu { s.declined = false; } if *frames < 2 { return; } *avg = Some(match *avg { None => ms, Some(a) if *frames <= Self::WARMUP => a.min(ms), Some(a) if ms < a => a + (ms - a) * Self::ALPHA_DOWN, Some(a) => a + (ms.min(2.0 * a) - a) * Self::ALPHA, }); } /// The GPU did not handle a frame for `key` (it rendered on the CPU instead). pub fn declined(&self, key: AutoKey) { if let Ok(mut m) = self.stats.lock() { let s = m.entry(key).or_default(); s.declined = true; s.gpu_ms = None; s.since_probe = 0; } } /// The history of `key`. pub fn stats(&self, key: AutoKey) -> Option { self.stats.lock().ok()?.get(&key).copied() } /// Forget all history (settings that change both paths' costs). pub fn reset(&self) { if let Ok(mut m) = self.stats.lock() { m.clear(); } } }