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}