Skip to main content

qualia_core_db/wgsl_forge/
backend.rs

1//! Automatic native→fallback backend selection (plan §2).
2//!
3//! Plan §2 requires: *"If a target-specific native backend (e.g., PTX or MSL)
4//! fails to initialize on the host, the Forge must automatically fall back to the
5//! next available compilation target (e.g., SPIR-V or WGSL) to ensure the compute
6//! pipeline remains operational, albeit at a potentially reduced performance
7//! tier."*
8//!
9//! This module provides that policy as a **pure, deterministic** function so it is
10//! unit-testable with a mock availability predicate — no GPU or toolchain is
11//! required to exercise it. The execution/validation pipeline calls it to pick a
12//! runnable backend; *source emission* (`shader generate --target ptx`) is
13//! deliberately NOT routed through here (an explicit emission target must still
14//! emit that target's source).
15
16use super::emit::TargetBackend;
17
18/// The ordered native→portable fallback chain for `preferred`, starting with
19/// `preferred` itself and ending at the universal WGSL fallback.
20///
21/// Two families converge on the same universal tail:
22/// - NVIDIA native (`Ptx` / `CudaC`) → WGSL. There is no portable SPIR-V tier for
23///   the CUDA driver path (PTX is consumed by `cudarc`, not by a wgpu/SPIR-V
24///   pipeline), so a failed CUDA init drops straight to the wgpu/WGSL path.
25/// - GPU-shading-language native (`Msl` / `Hlsl`) → SPIR-V → WGSL. These compile
26///   into a wgpu pipeline, so binary SPIR-V (emitted from the same WGSL via naga's
27///   `spv-out`) is a meaningful intermediate tier before the WGSL source fallback.
28/// - `Spirv` → WGSL.
29/// - `Wgsl` is already the universal fallback; its chain is just `[Wgsl]`.
30fn fallback_chain(preferred: TargetBackend) -> &'static [TargetBackend] {
31    match preferred {
32        TargetBackend::Ptx => &[TargetBackend::Ptx, TargetBackend::Wgsl],
33        TargetBackend::CudaC => &[TargetBackend::CudaC, TargetBackend::Wgsl],
34        TargetBackend::Msl => &[
35            TargetBackend::Msl,
36            TargetBackend::Spirv,
37            TargetBackend::Wgsl,
38        ],
39        TargetBackend::Hlsl => &[
40            TargetBackend::Hlsl,
41            TargetBackend::Spirv,
42            TargetBackend::Wgsl,
43        ],
44        TargetBackend::Spirv => &[TargetBackend::Spirv, TargetBackend::Wgsl],
45        TargetBackend::Wgsl => &[TargetBackend::Wgsl],
46    }
47}
48
49/// Resolve which backend the *execution/validation* pipeline should actually use,
50/// given a `preferred` target and a predicate reporting whether a given backend's
51/// native toolchain is available on this host.
52///
53/// Policy (plan §2):
54/// - If `preferred` is available, use it unchanged: returns `(preferred, None)`.
55/// - Otherwise walk [`fallback_chain`] to the first available tier and return
56///   `(chosen, Some(note))`, where `note` explains the downgrade.
57/// - `Wgsl` is **always** considered available — it is the universal fallback and
58///   needs no native toolchain (naga compiles it in-process), so this function
59///   never fails to resolve a backend. The predicate is therefore not consulted
60///   for `Wgsl`; whatever it returns for `Wgsl` is ignored.
61///
62/// The function is pure and deterministic: identical inputs (including predicate
63/// behaviour) yield identical output, with no I/O or global state. This is what
64/// makes it unit-testable against a mock predicate.
65pub fn resolve_execution_backend(
66    preferred: TargetBackend,
67    native_available: impl Fn(TargetBackend) -> bool,
68) -> (TargetBackend, Option<String>) {
69    // The preferred backend wins outright when its toolchain is present (WGSL is
70    // always present).
71    if preferred == TargetBackend::Wgsl || native_available(preferred) {
72        return (preferred, None);
73    }
74
75    for &candidate in fallback_chain(preferred) {
76        if candidate == preferred {
77            continue; // already established as unavailable above
78        }
79        // WGSL is the universal fallback and is always available; any other tier
80        // must pass the availability predicate.
81        if candidate == TargetBackend::Wgsl || native_available(candidate) {
82            let note = format!(
83                "native backend {preferred:?} unavailable on this host; \
84                 falling back to {candidate:?} (reduced performance tier, plan §2)"
85            );
86            return (candidate, Some(note));
87        }
88    }
89
90    // Unreachable in practice: every chain ends in Wgsl, which is always
91    // available. Kept as a total, deterministic fallback rather than a panic.
92    (
93        TargetBackend::Wgsl,
94        Some(format!(
95            "native backend {preferred:?} unavailable; falling back to Wgsl (plan §2)"
96        )),
97    )
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    /// Build a mock predicate that reports the listed backends as available. WGSL
105    /// availability is irrelevant (the resolver treats it as always available), so
106    /// callers need not include it.
107    fn available(set: &'static [TargetBackend]) -> impl Fn(TargetBackend) -> bool {
108        move |t| set.contains(&t)
109    }
110
111    #[test]
112    fn preferred_available_yields_no_downgrade() {
113        // PTX present → use PTX, no note.
114        let (chosen, note) =
115            resolve_execution_backend(TargetBackend::Ptx, available(&[TargetBackend::Ptx]));
116        assert_eq!(chosen, TargetBackend::Ptx);
117        assert!(note.is_none(), "available preferred must not downgrade");
118
119        // MSL present → use MSL, no note.
120        let (chosen, note) =
121            resolve_execution_backend(TargetBackend::Msl, available(&[TargetBackend::Msl]));
122        assert_eq!(chosen, TargetBackend::Msl);
123        assert!(note.is_none());
124    }
125
126    #[test]
127    fn wgsl_is_always_available_even_if_predicate_denies_it() {
128        // Predicate denies everything (including WGSL); WGSL preferred still resolves.
129        let (chosen, note) = resolve_execution_backend(TargetBackend::Wgsl, available(&[]));
130        assert_eq!(chosen, TargetBackend::Wgsl);
131        assert!(note.is_none());
132    }
133
134    #[test]
135    fn ptx_unavailable_falls_to_wgsl_with_note() {
136        // No native toolchains present → PTX drops straight to WGSL (no SPIR-V tier
137        // on the CUDA driver path).
138        let (chosen, note) = resolve_execution_backend(TargetBackend::Ptx, available(&[]));
139        assert_eq!(chosen, TargetBackend::Wgsl);
140        let note = note.expect("a downgrade must be reported");
141        assert!(
142            note.contains("Ptx"),
143            "note must name the unavailable backend: {note}"
144        );
145        assert!(
146            note.contains("Wgsl"),
147            "note must name the chosen backend: {note}"
148        );
149    }
150
151    #[test]
152    fn cuda_c_unavailable_falls_to_wgsl_with_note() {
153        let (chosen, note) = resolve_execution_backend(TargetBackend::CudaC, available(&[]));
154        assert_eq!(chosen, TargetBackend::Wgsl);
155        assert!(note.unwrap().contains("CudaC"));
156    }
157
158    #[test]
159    fn msl_unavailable_prefers_spirv_when_available() {
160        // MSL absent but SPIR-V available → choose SPIR-V (the intermediate tier),
161        // NOT WGSL.
162        let (chosen, note) =
163            resolve_execution_backend(TargetBackend::Msl, available(&[TargetBackend::Spirv]));
164        assert_eq!(chosen, TargetBackend::Spirv);
165        let note = note.expect("a downgrade must be reported");
166        assert!(note.contains("Msl"));
167        assert!(note.contains("Spirv"));
168    }
169
170    #[test]
171    fn msl_unavailable_falls_to_wgsl_when_spirv_also_unavailable() {
172        // MSL absent and SPIR-V absent → fall all the way to WGSL.
173        let (chosen, note) = resolve_execution_backend(TargetBackend::Msl, available(&[]));
174        assert_eq!(chosen, TargetBackend::Wgsl);
175        assert!(note.unwrap().contains("Wgsl"));
176    }
177
178    #[test]
179    fn hlsl_unavailable_prefers_spirv_then_wgsl() {
180        // HLSL absent, SPIR-V present → SPIR-V.
181        let (chosen, _) =
182            resolve_execution_backend(TargetBackend::Hlsl, available(&[TargetBackend::Spirv]));
183        assert_eq!(chosen, TargetBackend::Spirv);
184
185        // HLSL absent, SPIR-V absent → WGSL.
186        let (chosen, _) = resolve_execution_backend(TargetBackend::Hlsl, available(&[]));
187        assert_eq!(chosen, TargetBackend::Wgsl);
188    }
189
190    #[test]
191    fn resolution_is_deterministic() {
192        // Identical inputs yield identical outputs across repeated calls.
193        let first = resolve_execution_backend(TargetBackend::Hlsl, available(&[]));
194        let second = resolve_execution_backend(TargetBackend::Hlsl, available(&[]));
195        assert_eq!(first, second);
196    }
197}