Skip to content

Commit c34743e

Browse files
committed
Gate VSM behind the high renderer tier
1 parent 3050fd7 commit c34743e

9 files changed

Lines changed: 151 additions & 37 deletions

docs/renderer-capability-tiers.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ because an unrelated optional feature is absent.
1313
<!-- BEGIN GENERATED CAPABILITY TIERS -->
1414
| Tier | Materials | Geometry | Shadows | GI | Reflections | AA | Textures | Path tracing | Minimum contract |
1515
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
16-
| baseline | Tier C per-material bind groups | CPU direct draws | Cascaded/VSM raster paths | Software SDF, probes, and SSGI | SSR, planar, and probe fallbacks | TAA/CAS/FXAA | Per-material resident textures | Disabled when this tier is forced | Active platform profile |
17-
| modern | Tier B deterministic paged arrays | CPU direct draws | Cascaded/VSM raster paths | Software SDF, probes, and SSGI | SSR, planar, and probe fallbacks | TAA/CAS/FXAA | Paged texture arrays/atlases | Disabled when this tier is forced | 16 texture-array layers; 8 sampled textures/stage |
18-
| high-end | Tier A descriptor-indexed global tables | GPU indirect when supported; CPU oracle fallback | Cascaded/VSM raster paths | Ray query when supported; software SDF/SSGI fallback | Ray query when supported; SSR/planar/probe fallback | TAA/CAS/FXAA | Descriptor-indexed texture/sampler arrays | Available only with ray query and required limits | Texture-binding arrays + non-uniform indexing; 2 array elements |
16+
| baseline | Tier C per-material bind groups | CPU direct draws | Cascaded raster shadows (VSM capability fallback) | Software SDF, probes, and SSGI | SSR, planar, and probe fallbacks | TAA/CAS/FXAA | Per-material resident textures | Disabled when this tier is forced | Active platform profile |
17+
| modern | Tier B deterministic paged arrays | CPU direct draws | Cascaded raster shadows (VSM capability fallback) | Software SDF, probes, and SSGI | SSR, planar, and probe fallbacks | TAA/CAS/FXAA | Paged texture arrays/atlases | Disabled when this tier is forced | 16 texture-array layers; 8 sampled textures/stage |
18+
| high-end | Tier A descriptor-indexed global tables | GPU indirect when supported; CPU oracle fallback | VSM page cache with cascaded fallback | Ray query when supported; software SDF/SSGI fallback | Ray query when supported; SSR/planar/probe fallback | TAA/CAS/FXAA | Descriptor-indexed texture/sampler arrays | Available only with ray query and required limits | Texture-binding arrays + non-uniform indexing; 2 array elements |
1919
<!-- END GENERATED CAPABILITY TIERS -->
2020

2121
This table describes the cross-system path contract after device creation.

docs/virtual-shadow-maps.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ The default path remains the established cascaded shadow map (CSM). When VSM
1111
is not requested, Bloom allocates no VSM textures or buffers and compiles the
1212
canonical shaders without VSM bindings or sampling branches.
1313

14+
VSM is also capability-gated at renderer startup. If `BLOOM_VSM=1` is present
15+
while `BLOOM_FORCE_RENDER_TIER=baseline` or `modern` selects a supported lower
16+
tier, Bloom compiles the same canonical CSM shaders and allocates no VSM or
17+
VSM-caster resources. The request remains visible in telemetry, together with
18+
`capability_eligible: false`, `enabled: false`, and
19+
`selection_reason: "lower-tier-csm-fallback"`. The public renderer capability
20+
report exposes the same state under `runtime_support.virtual_shadows`.
21+
1422
## Current contract
1523

1624
- Three directional levels, each with a 32 by 32 virtual address space.
@@ -95,7 +103,8 @@ quality and performance fallbacks, not failure states.
95103
Quality telemetry reports the VSM state under
96104
`renderer_paths.virtual_shadows`:
97105

98-
- `requested`, `active`, `fallback`, and `dynamic_fallback_mode`;
106+
- `requested`, `capability_eligible`, `enabled`, `active`,
107+
`selection_reason`, `fallback`, and `dynamic_fallback_mode`;
99108
- physical capacity and depth/metadata/total bytes;
100109
- receiver demand source and count;
101110
- receiver-bounds count and active marking backend;

native/shared/src/renderer/capabilities.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ pub const CAPABILITY_TIER_DEFINITIONS: [CapabilityTierDefinition; 3] = [
6262
min_sampled_textures: 0,
6363
material_bindings: "Tier C per-material bind groups",
6464
geometry_submission: "CPU direct draws",
65-
shadows: "Cascaded/VSM raster paths",
65+
shadows: "Cascaded raster shadows (VSM capability fallback)",
6666
gi: "Software SDF, probes, and SSGI",
6767
reflections: "SSR, planar, and probe fallbacks",
6868
anti_aliasing: "TAA/CAS/FXAA",
@@ -79,7 +79,7 @@ pub const CAPABILITY_TIER_DEFINITIONS: [CapabilityTierDefinition; 3] = [
7979
min_sampled_textures: 8,
8080
material_bindings: "Tier B deterministic paged arrays",
8181
geometry_submission: "CPU direct draws",
82-
shadows: "Cascaded/VSM raster paths",
82+
shadows: "Cascaded raster shadows (VSM capability fallback)",
8383
gi: "Software SDF, probes, and SSGI",
8484
reflections: "SSR, planar, and probe fallbacks",
8585
anti_aliasing: "TAA/CAS/FXAA",
@@ -97,7 +97,7 @@ pub const CAPABILITY_TIER_DEFINITIONS: [CapabilityTierDefinition; 3] = [
9797
min_sampled_textures: 0,
9898
material_bindings: "Tier A descriptor-indexed global tables",
9999
geometry_submission: "GPU indirect when supported; CPU oracle fallback",
100-
shadows: "Cascaded/VSM raster paths",
100+
shadows: "VSM page cache with cascaded fallback",
101101
gi: "Ray query when supported; software SDF/SSGI fallback",
102102
reflections: "Ray query when supported; SSR/planar/probe fallback",
103103
anti_aliasing: "TAA/CAS/FXAA",

native/shared/src/renderer/capability_api.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ impl Renderer {
163163
});
164164
out.push_str(",\"gpu_driven\":");
165165
out.push_str(&self.gpu_driven.report_json());
166+
out.push_str(",\"virtual_shadows\":");
167+
out.push_str(&self.shadow_map.virtual_map.report_json());
166168
out.push_str(",\"vsm_gpu_casters\":");
167169
out.push_str(&self.vsm_gpu_casters.report_json());
168170
out.push_str(",\"imported_refraction\":");

native/shared/src/renderer/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1832,6 +1832,14 @@ impl Renderer {
18321832
logical_width: u32,
18331833
logical_height: u32,
18341834
) -> Self {
1835+
// Optional renderer paths must observe the same accepted tier as
1836+
// device negotiation before selecting shader variants or resources.
1837+
// A forced baseline/modern run therefore stays on canonical CSM even
1838+
// when the high-tier VSM environment switch is also present.
1839+
let renderer_capabilities =
1840+
capabilities::RendererCapabilities::detect(device.features(), &device.limits());
1841+
crate::virtual_shadows::configure_capability_tier(renderer_capabilities.selected_tier);
1842+
18351843
// EN-063 — the format frames are RENDERED in, as opposed to the
18361844
// format the surface is CONFIGURED with. The tonemapper writes
18371845
// LINEAR values and relies on hardware encode-on-store into an sRGB

native/shared/src/virtual_shadow_report.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub(super) fn json(vsm: &DirectionalVirtualShadowMap) -> String {
3333
.map_or(0, |gpu| gpu.receiver_demand.in_flight());
3434
let gpu_overhead_bytes =
3535
page_table_bytes + render_staging_bytes + VSM_SAMPLING_PARAMS_BYTES + gpu_receiver_bytes;
36-
let (physical_capacity, physical_bytes, gpu_overhead_bytes, render_budget) = if vsm.requested {
36+
let (physical_capacity, physical_bytes, gpu_overhead_bytes, render_budget) = if vsm.enabled {
3737
(
3838
stats.capacity,
3939
vsm.cache.memory_bytes(),
@@ -45,7 +45,8 @@ pub(super) fn json(vsm: &DirectionalVirtualShadowMap) -> String {
4545
};
4646
format!(
4747
concat!(
48-
"{{\"requested\":{},\"active\":{},",
48+
"{{\"requested\":{},\"capability_eligible\":{},\"enabled\":{},\"active\":{},",
49+
"\"selection_reason\":\"{}\",",
4950
"\"projection\":\"camera-centered-page-snapped-clipmap\",",
5051
"\"fallback\":\"csm\",\"dynamic_fallback\":{},",
5152
"\"dynamic_fallback_mode\":\"{}\",\"dynamic_fallback_pages\":{},",
@@ -72,8 +73,11 @@ pub(super) fn json(vsm: &DirectionalVirtualShadowMap) -> String {
7273
"{{\"level\":1,\"resident\":{},\"dirty\":{}}},",
7374
"{{\"level\":2,\"resident\":{},\"dirty\":{}}}]}}"
7475
),
75-
vsm.requested,
76+
vsm.requested_by_user,
77+
vsm.capability_eligible,
78+
vsm.enabled,
7679
vsm.sampling_active,
80+
vsm.selection_reason,
7781
vsm.dynamic_global_fallback || vsm.dynamic_overlay_deferred_pages > 0,
7882
if vsm.dynamic_global_fallback {
7983
"whole-frame-csm"
@@ -109,7 +113,7 @@ pub(super) fn json(vsm: &DirectionalVirtualShadowMap) -> String {
109113
stats.clipmap_pages_dropped,
110114
vsm.pending.len(),
111115
render_budget,
112-
if !vsm.requested {
116+
if !vsm.enabled {
113117
"disabled"
114118
} else if vsm.receiver_demand_active {
115119
"receiver-bounds"
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use std::sync::OnceLock;
2+
3+
use crate::renderer::capabilities::RendererCapabilityTier;
4+
5+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6+
pub(super) struct VirtualShadowSelection {
7+
pub(super) requested_by_user: bool,
8+
pub(super) capability_eligible: bool,
9+
pub(super) enabled: bool,
10+
pub(super) reason: &'static str,
11+
}
12+
13+
static CONFIGURED_CAPABILITY_TIER: OnceLock<RendererCapabilityTier> = OnceLock::new();
14+
15+
pub(crate) fn configure_capability_tier(tier: RendererCapabilityTier) {
16+
let _ = CONFIGURED_CAPABILITY_TIER.set(tier);
17+
}
18+
19+
fn selection_for(
20+
requested_by_user: bool,
21+
tier: Option<RendererCapabilityTier>,
22+
) -> VirtualShadowSelection {
23+
let capability_eligible = tier.is_none_or(|tier| tier >= RendererCapabilityTier::HighEnd);
24+
let (enabled, reason) = match (requested_by_user, capability_eligible) {
25+
(false, _) => (false, "not-requested"),
26+
(true, false) => (false, "lower-tier-csm-fallback"),
27+
(true, true) => (true, "high-tier-vsm"),
28+
};
29+
VirtualShadowSelection {
30+
requested_by_user,
31+
capability_eligible,
32+
enabled,
33+
reason,
34+
}
35+
}
36+
37+
fn requested_by_user() -> bool {
38+
static REQUESTED: OnceLock<bool> = OnceLock::new();
39+
*REQUESTED.get_or_init(|| {
40+
std::env::var("BLOOM_VSM")
41+
.map(|value| {
42+
matches!(
43+
value.trim().to_ascii_lowercase().as_str(),
44+
"1" | "on" | "true" | "enabled"
45+
)
46+
})
47+
.unwrap_or(false)
48+
})
49+
}
50+
51+
pub(super) fn selection() -> VirtualShadowSelection {
52+
selection_for(
53+
requested_by_user(),
54+
CONFIGURED_CAPABILITY_TIER.get().copied(),
55+
)
56+
}
57+
58+
pub(crate) fn virtual_shadows_requested() -> bool {
59+
selection().enabled
60+
}
61+
62+
#[cfg(test)]
63+
mod tests {
64+
use super::*;
65+
66+
#[test]
67+
fn requested_vsm_is_enabled_on_high_tier_only() {
68+
let disabled = selection_for(false, Some(RendererCapabilityTier::HighEnd));
69+
assert!(!disabled.requested_by_user);
70+
assert!(disabled.capability_eligible);
71+
assert!(!disabled.enabled);
72+
assert_eq!(disabled.reason, "not-requested");
73+
74+
for tier in [
75+
RendererCapabilityTier::Baseline,
76+
RendererCapabilityTier::Modern,
77+
] {
78+
let fallback = selection_for(true, Some(tier));
79+
assert!(fallback.requested_by_user);
80+
assert!(!fallback.capability_eligible);
81+
assert!(!fallback.enabled);
82+
assert_eq!(fallback.reason, "lower-tier-csm-fallback");
83+
}
84+
85+
let high = selection_for(true, Some(RendererCapabilityTier::HighEnd));
86+
assert!(high.requested_by_user);
87+
assert!(high.capability_eligible);
88+
assert!(high.enabled);
89+
assert_eq!(high.reason, "high-tier-vsm");
90+
}
91+
}

native/shared/src/virtual_shadows.rs

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
//! CSM remains the fallback for absent, dirty, over-budget, or unrendered pages.
55
66
use std::collections::HashMap;
7-
use std::sync::OnceLock;
87
#[path = "directional_shadow_clipmap.rs"]
98
mod clipmap;
109
#[path = "virtual_shadow_gpu_receiver.rs"]
@@ -15,6 +14,11 @@ mod page_priority;
1514
mod receiver_demand;
1615
#[path = "virtual_shadow_report.rs"]
1716
mod report;
17+
#[path = "virtual_shadow_selection.rs"]
18+
mod selection;
19+
20+
use selection::selection as virtual_shadow_selection;
21+
pub(crate) use selection::{configure_capability_tier, virtual_shadows_requested};
1822

1923
pub const VSM_CLIP_LEVELS: u8 = 3;
2024
pub const VSM_VIRTUAL_PAGES_PER_AXIS: u16 = 32;
@@ -490,7 +494,10 @@ fn paint_debug_cell(
490494
/// performs no demand walk and allocates no GPU memory, so landing the
491495
/// foundation cannot change images, frame time, or residency.
492496
pub struct DirectionalVirtualShadowMap {
493-
requested: bool,
497+
requested_by_user: bool,
498+
capability_eligible: bool,
499+
enabled: bool,
500+
selection_reason: &'static str,
494501
sampling_active: bool,
495502
dynamic_global_fallback: bool,
496503
dynamic_overlay_pages: Vec<VirtualShadowPage>,
@@ -522,8 +529,8 @@ pub struct DirectionalVirtualShadowMap {
522529

523530
impl DirectionalVirtualShadowMap {
524531
pub fn new(device: &wgpu::Device, shadow_uniform_layout: &wgpu::BindGroupLayout) -> Self {
525-
let requested = virtual_shadows_requested();
526-
let requested_capacity = if requested {
532+
let selection = virtual_shadow_selection();
533+
let requested_capacity = if selection.enabled {
527534
env_u16(
528535
"BLOOM_VSM_PHYSICAL_PAGES",
529536
VSM_DEFAULT_PHYSICAL_PAGES,
@@ -547,16 +554,19 @@ impl DirectionalVirtualShadowMap {
547554
.min(VSM_MAX_PAGE_RENDER_BUDGET)
548555
.min(buffer_limited_budget.max(1));
549556
let render_budget = env_u16("BLOOM_VSM_PAGE_BUDGET", 8, 1, max_render_budget).into();
550-
let gpu = requested.then(|| {
557+
let gpu = selection.enabled.then(|| {
551558
GpuVirtualShadowResources::new(device, shadow_uniform_layout, capacity, render_budget)
552559
});
553-
let fallback_demand = if requested {
560+
let fallback_demand = if selection.enabled {
554561
centered_directional_demand(0)
555562
} else {
556563
Vec::new()
557564
};
558565
Self {
559-
requested,
566+
requested_by_user: selection.requested_by_user,
567+
capability_eligible: selection.capability_eligible,
568+
enabled: selection.enabled,
569+
selection_reason: selection.reason,
560570
sampling_active: false,
561571
dynamic_global_fallback: false,
562572
dynamic_overlay_pages: Vec::new(),
@@ -601,7 +611,7 @@ impl DirectionalVirtualShadowMap {
601611
self.frame = self.frame.wrapping_add(1).max(1);
602612
self.cache.begin_frame(self.frame);
603613
self.pending.clear();
604-
if !self.requested {
614+
if !self.enabled {
605615
return;
606616
}
607617
self.dynamic_overlay_rendered_pages = 0;
@@ -740,7 +750,7 @@ impl DirectionalVirtualShadowMap {
740750
self.receiver_demand.clear();
741751
self.receiver_bounds_signature = 0;
742752
self.receiver_demand_level_vps = None;
743-
self.receiver_marking_backend = if self.requested {
753+
self.receiver_marking_backend = if self.enabled {
744754
"center-fallback"
745755
} else {
746756
"disabled"
@@ -859,7 +869,7 @@ impl DirectionalVirtualShadowMap {
859869
if global_fallback {
860870
self.cache.invalidate_light(0);
861871
}
862-
let sampling_active = self.requested && self.gpu.is_some() && !global_fallback;
872+
let sampling_active = self.enabled && self.gpu.is_some() && !global_fallback;
863873
if !self.sampling_params_initialized
864874
|| sampling_active != self.sampling_active
865875
|| (sampling_active && self.sampling_level_vps != Some(level_vps))
@@ -889,7 +899,7 @@ impl DirectionalVirtualShadowMap {
889899
}
890900

891901
pub fn requested(&self) -> bool {
892-
self.requested
902+
self.enabled
893903
}
894904

895905
pub fn physical_page_view(&self, physical_page: u16) -> Option<&wgpu::TextureView> {
@@ -958,7 +968,7 @@ impl DirectionalVirtualShadowMap {
958968
}
959969

960970
pub fn debug_images(&self) -> Vec<(&'static str, u32, u32, Vec<u8>)> {
961-
if !self.requested {
971+
if !self.enabled {
962972
return Vec::new();
963973
}
964974
let (virtual_width, virtual_height, virtual_rgb) = self.cache.debug_virtual_rgb(0, 4);
@@ -1232,20 +1242,6 @@ pub(crate) fn directional_material_shader(source: String) -> String {
12321242
output
12331243
}
12341244

1235-
pub(crate) fn virtual_shadows_requested() -> bool {
1236-
static REQUESTED: OnceLock<bool> = OnceLock::new();
1237-
*REQUESTED.get_or_init(|| {
1238-
std::env::var("BLOOM_VSM")
1239-
.map(|value| {
1240-
matches!(
1241-
value.trim().to_ascii_lowercase().as_str(),
1242-
"1" | "on" | "true" | "enabled"
1243-
)
1244-
})
1245-
.unwrap_or(false)
1246-
})
1247-
}
1248-
12491245
fn env_u16(name: &str, default: u16, min: u16, max: u16) -> u16 {
12501246
std::env::var(name)
12511247
.ok()

native/shared/tests/device_negotiation.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ fn negotiated_headless_device_constructs_the_complete_renderer() {
3434
assert!(public["material_binding"]["selected_tier"].is_string());
3535
assert!(public["runtime_support"]["path_tracing"].is_boolean());
3636
assert!(public["runtime_support"]["gpu_driven"]["enabled"].is_boolean());
37+
assert!(public["runtime_support"]["virtual_shadows"]["requested"].is_boolean());
38+
assert!(public["runtime_support"]["virtual_shadows"]["capability_eligible"].is_boolean());
39+
assert!(public["runtime_support"]["virtual_shadows"]["enabled"].is_boolean());
40+
assert!(public["runtime_support"]["virtual_shadows"]["selection_reason"].is_string());
3741

3842
// Headless qualification has no swapchain to configure, but it is still
3943
// uncapped and must retain the requested mode for truthful telemetry.

0 commit comments

Comments
 (0)