1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
//! Opt-in renderer features picked at construction time.
//!
//! Flags gate clusters of always-on infrastructure that not every
//! library consumer needs. Each defaults to `false`, so library
//! consumers (tools / 2D-with-some-3D / minimal viewers) pay zero
//! overhead for features they don't use. Game-side and editor builds
//! opt in explicitly via [`crate::AwsmRendererBuilder::with_features`].
/// Tri-state toggle for renderer capabilities whose availability
/// depends on hardware / browser support.
///
/// - `Auto` (default): capability-detect at device creation; the
/// builder probes the adapter and resolves to true/false.
/// - `On`: force-enable, asserting the path is supported. Bypasses
/// detection. Use when you have out-of-band knowledge that the
/// device supports it (or to bisect adapter-detection bugs).
/// - `Off`: force-disable, opting into the portable fallback path
/// even on devices that support the optimized path. Use to test
/// the fallback path on a supported device, or to side-step a
/// device-driver bug in the optimized path.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum FeatureToggle {
/// Capability-detect at device creation time.
#[default]
Auto,
/// Force-enable.
On,
/// Force-disable.
Off,
}
impl FeatureToggle {
/// Resolves the toggle against a runtime capability probe.
///
/// `Auto` falls through to `capability`. `On` returns `true`
/// regardless. `Off` returns `false` regardless. The resolved
/// boolean is what the renderer's allocation and pipeline-
/// selection logic actually consults.
pub fn resolve(self, capability: bool) -> bool {
match self {
FeatureToggle::Auto => capability,
FeatureToggle::On => true,
FeatureToggle::Off => false,
}
}
}
/// Per-renderer feature gates picked at construction time.
///
/// Toggling a gate after `build()` requires a renderer rebuild — the
/// `Option`-shaped owning fields on `AwsmRenderer` (gated buffers /
/// textures / render passes) are populated once based on the active
/// feature set.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RendererFeatures {
/// Enable GPU-driven culling: HZB build, occlusion cull,
/// `IndirectDrawArgs` compaction, and the `drawIndirect` geometry
/// path. Required for the GPU pipeline to filter visible meshes
/// on-device. At the 10K-mesh tier this is a 30–50% frame-time
/// win; below ~500 meshes the always-on dispatch + per-frame CPU
/// upload nets out to a small loss. The adaptive
/// [`RendererOptimizationPolicy`] (default `Auto`) handles that
/// trade automatically, so keeping this `true` is the right
/// default for editors and games once any mesh batching ramps up.
///
/// [`RendererOptimizationPolicy`]: crate::optimization_policy::RendererOptimizationPolicy
pub gpu_culling: bool,
/// Enable projection decals. Allocates `decal_color` (~16 MB at
/// 4K) + `decal_classify_buffers` (~17 MB at 4K) up-front and
/// dispatches the classify + decal compute + composite passes
/// whenever `Decals::len() > 0`. When `false`,
/// `insert_decal()` returns [`AwsmDecalError::FeatureNotEnabled`]
/// and none of the decal resources are allocated.
///
/// [`AwsmDecalError::FeatureNotEnabled`]: crate::decals::AwsmDecalError::FeatureNotEnabled
pub decals: bool,
/// Enable the GPU per-mesh pixel-coverage producer that feeds the
/// CPU [`MeshCoverage`] table via an async readback. Consumers of
/// that table (skin-skip, cheap-material LOD) are currently
/// parked, so the producer pays for nothing in the default case —
/// hence opt-in. When `false`, [`MeshCoverage::is_below_threshold`]
/// returns `false` for every mesh, which means any consumer falls
/// back to its "above threshold / use the expensive variant" path.
///
/// Flip on if you're wiring up your own consumer (or finishing
/// the parked ones). Allocates a counts buffer (`4 B × mesh
/// slot count`, grow-by-2) + a same-sized CPU-mappable readback
/// buffer; per-frame cost is one compute dispatch at the
/// visibility resolution plus one `copyBufferToBuffer` and a
/// `mapAsync` round-trip on a future frame.
///
/// [`MeshCoverage`]: crate::coverage::MeshCoverage
/// [`MeshCoverage::is_below_threshold`]: crate::coverage::MeshCoverage::is_below_threshold
pub coverage_lod: bool,
/// Enable GPU mesh-picking ([`AwsmRenderer::pick`]). When `false`
/// (the default), `AwsmRenderer.picker` is `None`, the two
/// picker compute pipelines never compile, the picker bind-group
/// layouts aren't registered, and [`AwsmRenderer::pick`] returns
/// [`PickResult::Disabled`]. Editor builds set this to `true`;
/// game / library builds that don't need click-to-select pay
/// zero cost.
///
/// Picker has 2 compute shader variants (multisampled true/false) + 2 compute pipelines + 2 bind-group layouts. On warm-Metal that's a few task-ticks worth of work skipped at startup; on cold-Dawn it's one compile wave saved.
///
/// [`AwsmRenderer::pick`]: crate::AwsmRenderer::pick
/// [`PickResult::Disabled`]: crate::picker::PickResult::Disabled
pub picking: bool,
/// Whether to use the WebGPU `indirect-first-instance` feature for
/// the non-instanced geometry pass's drawIndirect path.
///
/// When **enabled**, the compaction shader writes the per-mesh
/// slot index into `IndirectDrawArgs.first_instance`, and the
/// vertex shader's `geometry_mesh_metas[instance_index]` storage-
/// array lookup resolves to that slot. One shared bind group
/// services every non-instanced draw — no per-draw `setBindGroup`.
///
/// When **disabled** (portable fallback), the non-instanced path
/// uses the same uniform-with-dynamic-offset binding the instanced
/// path uses: the CPU calls `setBindGroup(2, ..., &[meta_offset])`
/// per draw, the args buffer's `first_instance` stays at 0, and
/// the storage-array binding is omitted from the shader. The GPU
/// culling benefit (compaction setting `instance_count` to 0/1)
/// is preserved — only the bind-group sharing is lost.
///
/// Browser support is limited (Firefox: none; Chrome desktop:
/// Linux-Intel only as of mid-2026). The default is `Auto`, which
/// resolves to true on adapters that expose the feature and false
/// on those that don't. Both paths are independently optimized;
/// neither is a "degraded" mode.
pub indirect_first_instance: FeatureToggle,
}
impl RendererFeatures {
/// Reads the resolved value of [`Self::indirect_first_instance`].
///
/// Only meaningful after the renderer builder has resolved `Auto`
/// against the device's capability. Before resolution, `Auto`
/// returns `false` from this helper — which means callers outside
/// the `build()` flow see "feature off" until the resolution step
/// has run. Inside the renderer the builder replaces `Auto` with
/// `On` or `Off` early in `build()`, so all downstream reads land
/// on a deterministic boolean.
pub fn indirect_first_instance_enabled(&self) -> bool {
self.indirect_first_instance.resolve(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_features_are_all_off() {
let features = RendererFeatures::default();
assert!(
!features.gpu_culling,
"gpu_culling must default to false so library consumers pay no cost"
);
assert!(
!features.decals,
"decals must default to false so library consumers pay no cost"
);
assert!(
!features.coverage_lod,
"coverage_lod must default to false so library consumers pay no cost"
);
assert!(
!features.picking,
"picking must default to false so library consumers pay no cost"
);
assert_eq!(
features.indirect_first_instance,
FeatureToggle::Auto,
"indirect_first_instance must default to Auto — capability detection at build time"
);
}
#[test]
fn feature_toggle_resolves_correctly() {
assert!(FeatureToggle::Auto.resolve(true), "Auto follows capability");
assert!(
!FeatureToggle::Auto.resolve(false),
"Auto follows capability"
);
assert!(FeatureToggle::On.resolve(true), "On ignores capability");
assert!(FeatureToggle::On.resolve(false), "On ignores capability");
assert!(!FeatureToggle::Off.resolve(true), "Off ignores capability");
assert!(!FeatureToggle::Off.resolve(false), "Off ignores capability");
}
#[test]
fn features_clone_independently() {
let mut a = RendererFeatures::default();
let b = a.clone();
a.gpu_culling = true;
assert_ne!(a, b);
assert!(!b.gpu_culling);
}
}