Skip to main content

facett_graphview/
backend.rs

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