teksilo_render/instance.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! The flags every wgpu instance in this workspace is built with.
5//!
6//! Two instances exist: the one [`crate::test_support`] opens for offscreen
7//! work, and the one `teksilo-platform` opens for windows. They have to agree
8//! about this, so the rule lives here, in the crate both can reach, rather
9//! than at either call site.
10
11/// The `InstanceFlags` to build a wgpu instance with: wgpu's own defaults,
12/// read from the environment, minus `VALIDATION_INDIRECT_CALL`.
13///
14/// That flag is set in **release** builds too — it comes from
15/// `InstanceFlags::from_build_config`'s non-debug branch, so it is not a
16/// debug-only cost. It makes `Device::new` build a set of compute and render
17/// pipelines that validate the arguments of indirect draws. This renderer
18/// issues no indirect draws at all — not one `draw_indirect`,
19/// `dispatch_indirect` or `multi_draw_*` anywhere in the workspace — so those
20/// pipelines check nothing we will ever submit.
21///
22/// That alone would only be wasted startup work. The reason the flag is
23/// cleared is that building those pipelines is also a way for device creation
24/// to *fail*, and failing there is not survivable.
25/// `wgpu_core::device::resource::Device::new` creates the hal device, then its
26/// `empty_bgl` — which registers a bind-group layout with the Vulkan backend's
27/// `DescriptorAllocator` — and only then calls `IndirectValidation::new(..)?`.
28/// A driver that cannot build them takes that `?`, and the early return drops
29/// the hal device *without* unregistering `empty_bgl`, because hal objects are
30/// not RAII and need an explicit destroy. `Drop for DescriptorAllocator` then
31/// finds a non-empty bucket and panics — "buckets are not empty, at least one
32/// BGL has not been unregistered" — from an ordinary, non-unwinding drop, so
33/// its own `thread::panicking()` guard does not suppress it.
34///
35/// The process therefore dies *inside* `request_device`, and neither caller
36/// can do anything about it there: a backend search cannot search past a
37/// panic, and an offscreen renderer cannot fall back to a software adapter.
38/// Reported from the field on an older Windows 10 machine where an app never
39/// opened a window, and confirmed there by setting
40/// `WGPU_VALIDATION_INDIRECT_CALL=0`, which let the window open. D3D12 has no
41/// `DescriptorAllocator` and never runs the assertion, which is why forcing
42/// `WGPU_BACKEND=dx12` looked like a graphics fix when it was really a way of
43/// not reaching this code.
44///
45/// `WGPU_VALIDATION_INDIRECT_CALL` is honoured in both directions, so the flag
46/// stays reachable for anyone debugging wgpu itself.
47#[must_use]
48pub fn instance_flags() -> wgpu::InstanceFlags {
49 without_unused_indirect_validation(
50 wgpu::InstanceFlags::from_env_or_default(),
51 std::env::var_os("WGPU_VALIDATION_INDIRECT_CALL").is_some(),
52 )
53}
54
55/// [`instance_flags`]'s decision, as a pure function of its two inputs.
56///
57/// Split out because the alternative is a test that writes a process-wide
58/// environment variable: unsafe since the 2024 edition, and racy against every
59/// other test in the binary regardless.
60///
61/// `asked_for` is whether the variable is *present*, not whether it is true.
62/// wgpu's `with_env` has already read its value into `base` by this point — it
63/// sets the flag for any value but `0`, and clears it for `0` — so all this
64/// has to decide is whether the user expressed an opinion at all. Testing the
65/// resulting bit instead cannot tell "wgpu's default" from "a developer asked
66/// for it", and testing the value again would re-implement `with_env`.
67fn without_unused_indirect_validation(
68 base: wgpu::InstanceFlags,
69 asked_for: bool,
70) -> wgpu::InstanceFlags {
71 if asked_for {
72 return base;
73 }
74 let mut flags = base;
75 flags.remove(wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL);
76 flags
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 const INDIRECT: wgpu::InstanceFlags = wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL;
84
85 #[test]
86 fn an_unset_variable_clears_the_flag_wgpu_defaults_to() {
87 // What a release build gets: `from_build_config` sets the flag and
88 // nobody asked for it.
89 assert!(!without_unused_indirect_validation(INDIRECT, false).contains(INDIRECT));
90 }
91
92 #[test]
93 fn an_explicit_request_is_honoured_in_both_directions() {
94 // `with_env` has already applied the value, so the flag as it arrives
95 // is the answer — set for `WGPU_VALIDATION_INDIRECT_CALL=1`, cleared
96 // for `=0`. Clearing a `=0` again would be right by luck; re-setting a
97 // `=1` would silently ignore the developer.
98 assert!(without_unused_indirect_validation(INDIRECT, true).contains(INDIRECT));
99 assert!(
100 !without_unused_indirect_validation(wgpu::InstanceFlags::empty(), true)
101 .contains(INDIRECT)
102 );
103 }
104
105 #[test]
106 fn no_other_flag_is_touched() {
107 let base = wgpu::InstanceFlags::DEBUG | wgpu::InstanceFlags::VALIDATION | INDIRECT;
108 let got = without_unused_indirect_validation(base, false);
109 assert_eq!(got, base.difference(INDIRECT));
110 }
111}