Skip to main content

facett_core/render/
backend.rs

1//! **Runtime backend selection** — the galileo-style Renderer backend-swap policy,
2//! lifted into the L0 kernel from `facett-graphview::backend` so map skins and the
3//! graph engine decide identically. (graphview will re-export this in Phase C; this
4//! milestone just adds it to core without breaking graphview, which keeps its own
5//! copy.)
6//!
7//! vello is *not* GPU-only: the same scene rasterizes on the GPU (the L0 SDF wgpu
8//! pipeline / `vello` on wgpu) when a usable adapter exists, and on the CPU
9//! ([`super::cpu::CpuRenderer`] / `vello_cpu`, multithreaded SIMD) when it doesn't.
10//! The caller probes once and renders through whichever [`Backend`] [`decide`]
11//! returns.
12
13/// What a hardware probe found. The caller fills this (e.g. from a wgpu adapter
14/// enumeration, an env override, or a forced-CPU test flag); the *policy* — turning
15/// a probe into a pick — lives in [`decide`] so every surface decides identically.
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17pub struct GpuProbe {
18    /// A usable GPU adapter was found (a real wgpu device can be created).
19    pub usable_gpu: bool,
20    /// The caller forces the CPU path regardless (CI determinism, golden PNGs,
21    /// `FACETT_RENDER_CPU=1`, a known-bad driver allowlist hit).
22    pub force_cpu: bool,
23}
24
25impl GpuProbe {
26    /// A probe asserting a usable GPU is present.
27    pub fn gpu() -> Self {
28        Self { usable_gpu: true, force_cpu: false }
29    }
30    /// A probe with no GPU (the fallback path).
31    pub fn cpu_only() -> Self {
32        Self { usable_gpu: false, force_cpu: false }
33    }
34    /// A probe that reads the `FACETT_RENDER_CPU` env override on top of a
35    /// detected-GPU flag. The standard wiring a host uses.
36    pub fn from_env(usable_gpu: bool) -> Self {
37        Self { usable_gpu, force_cpu: std::env::var_os("FACETT_RENDER_CPU").is_some() }
38    }
39}
40
41/// The chosen rasterizer.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum Backend {
44    /// The L0 SDF wgpu pipeline (and the `vello` L1 overlay) on a wgpu device —
45    /// GPU rasterization, the fast path for large scenes. Wired behind the core
46    /// `wgpu` feature; see [`super::gpu`].
47    GpuVello,
48    /// `vello_cpu` — multithreaded SIMD software rasterization. Always available
49    /// (it ships transitively inside epaint 0.34). The reference path here.
50    CpuVello,
51}
52
53impl Backend {
54    pub fn is_gpu(self) -> bool {
55        matches!(self, Backend::GpuVello)
56    }
57    /// Human label for a status line / about box.
58    pub fn label(self) -> &'static str {
59        match self {
60            Backend::GpuVello => "L0 SDF wgpu (GPU)",
61            Backend::CpuVello => "vello_cpu (SIMD/threads)",
62        }
63    }
64}
65
66/// THE picker: turn a [`GpuProbe`] into a [`Backend`]. GPU when usable and not
67/// forced off **and** the `wgpu` feature is compiled in; CPU otherwise. Keeping
68/// this pure (probe in, enum out) makes the policy trivially testable.
69pub fn decide(probe: GpuProbe) -> Backend {
70    let want_gpu = probe.usable_gpu && !probe.force_cpu;
71    // The GPU path only exists when compiled with `wgpu`; without it, any probe
72    // resolves to CPU. This is what lets a default build stay GPU-dep-free while
73    // the seam is real.
74    if want_gpu && cfg!(feature = "wgpu") {
75        Backend::GpuVello
76    } else {
77        Backend::CpuVello
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn cpu_only_probe_always_picks_cpu() {
87        assert_eq!(decide(GpuProbe::cpu_only()), Backend::CpuVello);
88    }
89
90    #[test]
91    fn force_cpu_overrides_a_present_gpu() {
92        let p = GpuProbe { usable_gpu: true, force_cpu: true };
93        assert_eq!(decide(p), Backend::CpuVello);
94    }
95
96    #[test]
97    fn gpu_probe_picks_gpu_only_when_feature_on() {
98        let got = decide(GpuProbe::gpu());
99        if cfg!(feature = "wgpu") {
100            assert_eq!(got, Backend::GpuVello);
101        } else {
102            assert_eq!(got, Backend::CpuVello);
103        }
104    }
105}