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}