Skip to main content

alpha2_gpu/
instance.rs

1//! The single builder for every `wgpu` instance in the process.
2//!
3//! All instances share one DX12 shader compiler and one set of flags. The compiler is frozen when
4//! the first instance is made, since mixing compilers would render different bytes.
5//!
6//! On Windows the builder drops `InstanceFlags::DEBUG`, which makes `wgpu` compile HLSL without
7//! optimizations, so debug and release builds match (`WGPU_DEBUG=1` restores it). Elsewhere the
8//! flag stays because on Vulkan it installs the debug messenger that logs validation messages.
9//! Every build drops `VALIDATION_INDIRECT_CALL`, since alpha2 issues no indirect calls.
10
11use std::path::PathBuf;
12use std::sync::OnceLock;
13use std::sync::atomic::{AtomicBool, Ordering};
14
15/// The DX12 shader compiler the process's instances use.
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub enum Dx12Compiler {
18    /// The FXC compiler built into Windows; nothing to ship, but slow on large shaders.
19    Fxc,
20    /// DXC loaded from `library` (`dxcompiler.dll`).
21    Dxc {
22        /// The `dxcompiler.dll` to load.
23        library: PathBuf,
24    },
25}
26
27/// The settings every instance of the process is made with.
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct InstanceSettings {
30    /// The DX12 shader compiler.
31    pub dx12_compiler: Dx12Compiler,
32}
33
34impl InstanceSettings {
35    /// DXC from a non-empty `ALPHA2_DXC_PATH`, otherwise FXC. Never `wgpu`'s `Auto`, which loads
36    /// any `dxcompiler.dll` on the DLL search path, including the working directory.
37    #[must_use]
38    pub fn from_environment() -> Self {
39        Self::from_lookup(|name| std::env::var_os(name))
40    }
41
42    fn from_lookup(lookup: impl Fn(&str) -> Option<std::ffi::OsString>) -> Self {
43        let dx12_compiler = match lookup("ALPHA2_DXC_PATH") {
44            Some(path) if !path.is_empty() => Dx12Compiler::Dxc {
45                library: PathBuf::from(path),
46            },
47            _ => Dx12Compiler::Fxc,
48        };
49        Self { dx12_compiler }
50    }
51}
52
53/// Why [`configure_instances`] refused new settings.
54#[derive(Clone, Debug, PartialEq, Eq)]
55#[non_exhaustive]
56pub enum InstanceSettingsError {
57    /// An instance was already made or settings were already configured.
58    AlreadyFrozen,
59}
60
61impl std::fmt::Display for InstanceSettingsError {
62    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            Self::AlreadyFrozen => formatter.write_str(
65                "the GPU instance settings are fixed once the first instance is made or settings \
66                 were configured",
67            ),
68        }
69    }
70}
71
72impl std::error::Error for InstanceSettingsError {}
73
74struct SettingsCell {
75    settings: OnceLock<InstanceSettings>,
76    frozen: AtomicBool,
77}
78
79impl SettingsCell {
80    const fn new() -> Self {
81        Self {
82            settings: OnceLock::new(),
83            frozen: AtomicBool::new(false),
84        }
85    }
86
87    fn configure(&self, settings: InstanceSettings) -> Result<(), InstanceSettingsError> {
88        if self.frozen.load(Ordering::Acquire) {
89            return Err(InstanceSettingsError::AlreadyFrozen);
90        }
91        self.settings
92            .set(settings)
93            .map_err(|_| InstanceSettingsError::AlreadyFrozen)
94    }
95
96    fn get(&self, environment: impl FnOnce() -> InstanceSettings) -> &InstanceSettings {
97        self.settings.get_or_init(environment)
98    }
99
100    fn freeze(&self, environment: impl FnOnce() -> InstanceSettings) -> &InstanceSettings {
101        let settings = self.get(environment);
102        self.frozen.store(true, Ordering::Release);
103        settings
104    }
105}
106
107static SETTINGS: SettingsCell = SettingsCell::new();
108
109/// Sets the settings every instance of this process is made with.
110///
111/// Call once before making any device. Otherwise the first instance uses
112/// [`InstanceSettings::from_environment`].
113///
114/// # Errors
115/// [`InstanceSettingsError::AlreadyFrozen`] once an instance was made or settings were set.
116pub fn configure_instances(settings: InstanceSettings) -> Result<(), InstanceSettingsError> {
117    SETTINGS.configure(settings)
118}
119
120/// The process's instance settings, taken from the environment if none were configured.
121#[must_use]
122pub fn instance_settings() -> &'static InstanceSettings {
123    SETTINGS.get(InstanceSettings::from_environment)
124}
125
126/// The descriptor for every instance on `backends`: the process DX12 compiler and flags, with
127/// `WGPU_*` environment variables applied on top as developer overrides.
128#[must_use]
129#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
130pub fn instance_descriptor(backends: wgpu::Backends) -> wgpu::InstanceDescriptor {
131    descriptor_for(
132        backends,
133        SETTINGS.freeze(InstanceSettings::from_environment),
134    )
135}
136
137fn descriptor_for(
138    backends: wgpu::Backends,
139    settings: &InstanceSettings,
140) -> wgpu::InstanceDescriptor {
141    let mut descriptor = wgpu::InstanceDescriptor::new_without_display_handle();
142    descriptor.backends = backends;
143    descriptor.flags =
144        wgpu::InstanceFlags::from_build_config() - wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL;
145    if cfg!(target_os = "windows") {
146        descriptor.flags -= wgpu::InstanceFlags::DEBUG;
147    }
148    descriptor.backend_options.dx12.shader_compiler = match &settings.dx12_compiler {
149        Dx12Compiler::Fxc => wgpu::Dx12Compiler::Fxc,
150        Dx12Compiler::Dxc { library } => wgpu::Dx12Compiler::DynamicDxc {
151            dxc_path: library.to_string_lossy().into_owned(),
152        },
153    };
154    descriptor.with_env()
155}
156
157/// A new instance on `backends` from [`instance_descriptor`], made within the acquisition turn.
158#[must_use]
159#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
160pub fn new_instance(backends: wgpu::Backends) -> wgpu::Instance {
161    let _turn = crate::serialize_acquisition();
162    keep(backends);
163    wgpu::Instance::new(instance_descriptor(backends))
164}
165
166/// Under `serial-acquisition`, keeps an instance on `backends` until the process exits unless a
167/// kept one already covers them; see [`crate::serialize_acquisition`]. The caller holds the turn.
168#[allow(clippy::missing_const_for_fn, unused_variables)]
169pub(crate) fn keep(backends: wgpu::Backends) {
170    #[cfg(all(feature = "serial-acquisition", not(target_arch = "wasm32")))]
171    {
172        use std::sync::{Mutex, PoisonError};
173        static KEPT: Mutex<Vec<(wgpu::Backends, wgpu::Instance)>> = Mutex::new(Vec::new());
174        let backends = wgpu::Backends::from_env().unwrap_or(backends);
175        let mut kept = KEPT.lock().unwrap_or_else(PoisonError::into_inner);
176        if kept.iter().any(|(covered, _)| covered.contains(backends)) {
177            return;
178        }
179        let instance = wgpu::Instance::new(instance_descriptor(backends));
180        kept.push((backends, instance));
181    }
182}
183
184/// The DX12 shader compiler a descriptor asks for, as reports name it.
185#[derive(Clone, Debug, PartialEq, Eq)]
186#[non_exhaustive]
187pub enum ShaderCompiler {
188    /// The descriptor enables no DX12 backend.
189    NotDx12,
190    /// FXC.
191    Fxc,
192    /// DXC from the named library.
193    Dxc {
194        /// The `dxcompiler.dll` path.
195        library: String,
196    },
197    /// DXC from the DLL search path (`WGPU_DX12_COMPILER=dxc`).
198    DxcFromSearchPath,
199    /// DXC linked into the binary (`WGPU_DX12_COMPILER=staticdxc`).
200    StaticDxc,
201    /// `wgpu`'s own choice (`WGPU_DX12_COMPILER=auto`).
202    Auto,
203}
204
205impl std::fmt::Display for ShaderCompiler {
206    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        match self {
208            Self::NotDx12 => Ok(()),
209            Self::Fxc => formatter.write_str("FXC"),
210            Self::Dxc { library } => write!(formatter, "DXC {library}"),
211            Self::DxcFromSearchPath => formatter.write_str("DXC from the DLL search path"),
212            Self::StaticDxc => formatter.write_str("static DXC"),
213            Self::Auto => formatter.write_str("auto"),
214        }
215    }
216}
217
218/// The DX12 shader compiler `descriptor` asks for, read after its environment overrides.
219#[must_use]
220pub fn shader_compiler_of(descriptor: &wgpu::InstanceDescriptor) -> ShaderCompiler {
221    if !descriptor.backends.contains(wgpu::Backends::DX12) {
222        return ShaderCompiler::NotDx12;
223    }
224    match &descriptor.backend_options.dx12.shader_compiler {
225        wgpu::Dx12Compiler::Fxc => ShaderCompiler::Fxc,
226        // `WGPU_DX12_COMPILER=dxc` names the bare file, which the DLL search path resolves.
227        wgpu::Dx12Compiler::DynamicDxc { dxc_path } if dxc_path == "dxcompiler.dll" => {
228            ShaderCompiler::DxcFromSearchPath
229        }
230        wgpu::Dx12Compiler::DynamicDxc { dxc_path } => ShaderCompiler::Dxc {
231            library: dxc_path.clone(),
232        },
233        wgpu::Dx12Compiler::StaticDxc => ShaderCompiler::StaticDxc,
234        wgpu::Dx12Compiler::Auto => ShaderCompiler::Auto,
235    }
236}
237
238/// The DX12 shader compiler this process's DX12 instances use.
239#[must_use]
240pub fn dx12_shader_compiler() -> ShaderCompiler {
241    shader_compiler_of(&instance_descriptor(wgpu::Backends::DX12))
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn settings_take_the_dxc_path_from_the_environment_and_otherwise_fxc() {
250        let unset = InstanceSettings::from_lookup(|_| None);
251        assert_eq!(unset.dx12_compiler, Dx12Compiler::Fxc);
252        let empty = InstanceSettings::from_lookup(|_| Some("".into()));
253        assert_eq!(empty.dx12_compiler, Dx12Compiler::Fxc);
254        let set = InstanceSettings::from_lookup(|name| {
255            (name == "ALPHA2_DXC_PATH").then(|| r"C:\alpha2\dxcompiler.dll".into())
256        });
257        assert_eq!(
258            set.dx12_compiler,
259            Dx12Compiler::Dxc {
260                library: PathBuf::from(r"C:\alpha2\dxcompiler.dll")
261            }
262        );
263    }
264
265    #[test]
266    fn the_descriptor_carries_the_compiler_and_drops_debug_on_windows_and_indirect_validation() {
267        // Skip when environment overrides would change the descriptor.
268        if std::env::var_os("WGPU_DX12_COMPILER").is_some()
269            || std::env::var_os("WGPU_DEBUG").is_some()
270            || std::env::var_os("WGPU_VALIDATION_INDIRECT_CALL").is_some()
271        {
272            return;
273        }
274        let dxc = InstanceSettings {
275            dx12_compiler: Dx12Compiler::Dxc {
276                library: PathBuf::from("/opt/dxc/dxcompiler.dll"),
277            },
278        };
279        let descriptor = descriptor_for(wgpu::Backends::DX12, &dxc);
280        assert_eq!(
281            descriptor.flags.contains(wgpu::InstanceFlags::DEBUG),
282            cfg!(debug_assertions) && !cfg!(target_os = "windows")
283        );
284        assert!(
285            !descriptor
286                .flags
287                .contains(wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL)
288        );
289        assert_eq!(
290            shader_compiler_of(&descriptor),
291            ShaderCompiler::Dxc {
292                library: "/opt/dxc/dxcompiler.dll".to_owned()
293            }
294        );
295        assert_eq!(
296            shader_compiler_of(&descriptor).to_string(),
297            "DXC /opt/dxc/dxcompiler.dll"
298        );
299        let fxc = InstanceSettings {
300            dx12_compiler: Dx12Compiler::Fxc,
301        };
302        assert_eq!(
303            shader_compiler_of(&descriptor_for(wgpu::Backends::DX12, &fxc)).to_string(),
304            "FXC"
305        );
306        if std::env::var_os("WGPU_BACKEND").is_none() {
307            assert_eq!(
308                shader_compiler_of(&descriptor_for(wgpu::Backends::VULKAN, &fxc)),
309                ShaderCompiler::NotDx12
310            );
311        }
312    }
313
314    #[test]
315    fn settings_cannot_change_after_the_first_instance() {
316        let fxc = || InstanceSettings {
317            dx12_compiler: Dx12Compiler::Fxc,
318        };
319        let cell = SettingsCell::new();
320        cell.freeze(fxc);
321        assert_eq!(
322            cell.configure(fxc()),
323            Err(InstanceSettingsError::AlreadyFrozen)
324        );
325        let configured = SettingsCell::new();
326        assert_eq!(configured.configure(fxc()), Ok(()));
327        assert_eq!(
328            configured.configure(fxc()),
329            Err(InstanceSettingsError::AlreadyFrozen)
330        );
331        assert_eq!(configured.freeze(|| unreachable!()), &fxc());
332    }
333}