use std::sync::OnceLock;
const WEBGL2_MAX_TEXTURE_DIMENSION_2D: u32 = 2048;
const DESKTOP_MAX_TEXTURE_DIMENSION_2D: u32 = 8192;
const MAX_RESOURCE_TEXTURE_DIM: u32 = 4096;
const ATLAS_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DownlevelProfile {
Full,
WebGl2,
}
impl DownlevelProfile {
fn resolve(backend: wgpu::Backend) -> Self {
if backend == wgpu::Backend::Gl || downlevel_override_enabled() {
DownlevelProfile::WebGl2
} else {
DownlevelProfile::Full
}
}
}
fn downlevel_override_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
env_flag_enabled(
option_env!("FRUST_ENGINE_DOWNLEVEL"),
std::env::var("FRUST_ENGINE_DOWNLEVEL").ok(),
)
})
}
fn env_flag_enabled(compile_time: Option<&str>, runtime: Option<String>) -> bool {
fn is_set_non_zero(value: Option<&str>) -> bool {
matches!(value, Some(v) if v != "0")
}
is_set_non_zero(compile_time) || is_set_non_zero(runtime.as_deref())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TierCaps {
pub downlevel_flags: wgpu::DownlevelFlags,
pub adapter_name: String,
pub backend: wgpu::Backend,
pub max_texture_dimension_2d: u32,
pub max_texture_array_layers: u32,
pub max_bind_groups: u32,
pub max_uniform_buffer_binding_size: u32,
pub min_uniform_buffer_offset_alignment: u32,
pub max_vertex_attributes: u32,
pub has_storage_buffers: bool,
pub has_timestamp_query: bool,
pub transient_saves_memory: bool,
pub atlas_format: wgpu::TextureFormat,
pub resource_texture_dim: u32,
pub downlevel_profile: DownlevelProfile,
}
impl TierCaps {
pub fn probe(adapter: &wgpu::Adapter) -> Self {
let info = adapter.get_info();
let downlevel = adapter.get_downlevel_capabilities();
let limits = adapter.limits();
let features = adapter.features();
let downlevel_profile = DownlevelProfile::resolve(info.backend);
let limits = clamp_to_webgl2_defaults(limits, downlevel_profile);
let max_texture_dimension_2d = limits.max_texture_dimension_2d;
Self {
downlevel_flags: downlevel.flags,
adapter_name: info.name,
backend: info.backend,
max_texture_dimension_2d,
max_texture_array_layers: limits.max_texture_array_layers,
max_bind_groups: limits.max_bind_groups,
max_uniform_buffer_binding_size: saturating_u32(limits.max_uniform_buffer_binding_size),
min_uniform_buffer_offset_alignment: limits.min_uniform_buffer_offset_alignment,
max_vertex_attributes: limits.max_vertex_attributes,
has_storage_buffers: downlevel_profile == DownlevelProfile::Full
&& limits.max_storage_buffers_per_shader_stage > 0,
has_timestamp_query: features.contains(wgpu::Features::TIMESTAMP_QUERY),
transient_saves_memory: info.transient_saves_memory.unwrap_or(false),
atlas_format: ATLAS_FORMAT,
resource_texture_dim: max_texture_dimension_2d.min(MAX_RESOURCE_TEXTURE_DIM),
downlevel_profile,
}
}
pub fn fake(profile: DownlevelProfile) -> Self {
match profile {
DownlevelProfile::Full => Self {
downlevel_flags: wgpu::DownlevelFlags::all(),
adapter_name: "fake-full".to_string(),
backend: wgpu::Backend::Vulkan,
max_texture_dimension_2d: DESKTOP_MAX_TEXTURE_DIMENSION_2D,
max_texture_array_layers: 256,
max_bind_groups: 4,
max_uniform_buffer_binding_size: 64 << 10,
min_uniform_buffer_offset_alignment: 256,
max_vertex_attributes: 16,
has_storage_buffers: true,
has_timestamp_query: true,
transient_saves_memory: false,
atlas_format: ATLAS_FORMAT,
resource_texture_dim: DESKTOP_MAX_TEXTURE_DIMENSION_2D
.min(MAX_RESOURCE_TEXTURE_DIM),
downlevel_profile: DownlevelProfile::Full,
},
DownlevelProfile::WebGl2 => Self {
downlevel_flags: wgpu::DownlevelFlags::empty(),
adapter_name: "fake-webgl2".to_string(),
backend: wgpu::Backend::Gl,
max_texture_dimension_2d: WEBGL2_MAX_TEXTURE_DIMENSION_2D,
max_texture_array_layers: 256,
max_bind_groups: 4,
max_uniform_buffer_binding_size: 16 << 10,
min_uniform_buffer_offset_alignment: 256,
max_vertex_attributes: 16,
has_storage_buffers: false,
has_timestamp_query: false,
transient_saves_memory: false,
atlas_format: ATLAS_FORMAT,
resource_texture_dim: WEBGL2_MAX_TEXTURE_DIMENSION_2D.min(MAX_RESOURCE_TEXTURE_DIM),
downlevel_profile: DownlevelProfile::WebGl2,
},
}
}
}
fn clamp_to_webgl2_defaults(limits: wgpu::Limits, profile: DownlevelProfile) -> wgpu::Limits {
if profile != DownlevelProfile::WebGl2 {
return limits;
}
let webgl2 = wgpu::Limits::downlevel_webgl2_defaults();
wgpu::Limits {
max_texture_dimension_2d: limits
.max_texture_dimension_2d
.min(webgl2.max_texture_dimension_2d),
max_texture_array_layers: limits
.max_texture_array_layers
.min(webgl2.max_texture_array_layers),
max_bind_groups: limits.max_bind_groups.min(webgl2.max_bind_groups),
max_uniform_buffer_binding_size: limits
.max_uniform_buffer_binding_size
.min(webgl2.max_uniform_buffer_binding_size),
min_uniform_buffer_offset_alignment: limits
.min_uniform_buffer_offset_alignment
.max(webgl2.min_uniform_buffer_offset_alignment),
max_vertex_attributes: limits
.max_vertex_attributes
.min(webgl2.max_vertex_attributes),
..limits
}
}
fn saturating_u32(value: u64) -> u32 {
u32::try_from(value).unwrap_or(u32::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn full_profile_fake_reports_desktop_defaults() {
let caps = TierCaps::fake(DownlevelProfile::Full);
assert_eq!(caps.downlevel_profile, DownlevelProfile::Full);
assert_eq!(caps.backend, wgpu::Backend::Vulkan);
assert!(caps.has_storage_buffers);
assert_eq!(
caps.max_texture_dimension_2d,
DESKTOP_MAX_TEXTURE_DIMENSION_2D
);
assert_eq!(caps.resource_texture_dim, MAX_RESOURCE_TEXTURE_DIM);
}
#[test]
fn webgl2_profile_fake_reports_clamped_resource_dim_and_no_storage_buffers() {
let caps = TierCaps::fake(DownlevelProfile::WebGl2);
assert_eq!(caps.downlevel_profile, DownlevelProfile::WebGl2);
assert!(caps.resource_texture_dim <= 2048);
assert!(!caps.has_storage_buffers);
}
#[test]
fn resource_texture_dim_never_exceeds_the_shared_ceiling() {
let caps = TierCaps::fake(DownlevelProfile::Full);
assert!(caps.resource_texture_dim <= MAX_RESOURCE_TEXTURE_DIM);
}
#[test]
fn atlas_format_is_set_on_both_profiles() {
assert_eq!(
TierCaps::fake(DownlevelProfile::Full).atlas_format,
ATLAS_FORMAT
);
assert_eq!(
TierCaps::fake(DownlevelProfile::WebGl2).atlas_format,
ATLAS_FORMAT
);
}
#[test]
fn saturating_u32_clamps_a_too_large_u64() {
assert_eq!(saturating_u32(u64::MAX), u32::MAX);
assert_eq!(saturating_u32(64 << 10), 64 << 10);
}
#[test]
fn full_profile_clamp_is_a_no_op() {
let desktop = wgpu::Limits::defaults();
assert_eq!(
clamp_to_webgl2_defaults(desktop.clone(), DownlevelProfile::Full),
desktop
);
}
#[test]
fn webgl2_clamp_of_desktop_limits_matches_the_webgl2_fake_in_every_clamped_field() {
let desktop = wgpu::Limits::defaults();
let clamped = clamp_to_webgl2_defaults(desktop, DownlevelProfile::WebGl2);
let webgl2_fake = TierCaps::fake(DownlevelProfile::WebGl2);
assert_eq!(
clamped.max_texture_dimension_2d,
webgl2_fake.max_texture_dimension_2d
);
assert_eq!(
clamped.max_texture_array_layers,
webgl2_fake.max_texture_array_layers
);
assert_eq!(clamped.max_bind_groups, webgl2_fake.max_bind_groups);
assert_eq!(
saturating_u32(clamped.max_uniform_buffer_binding_size),
webgl2_fake.max_uniform_buffer_binding_size
);
assert_eq!(
clamped.min_uniform_buffer_offset_alignment,
webgl2_fake.min_uniform_buffer_offset_alignment
);
assert_eq!(
clamped.max_vertex_attributes,
webgl2_fake.max_vertex_attributes
);
}
#[test]
fn webgl2_clamp_never_lowers_the_alignment_floor_below_the_adapters_own_value() {
let stricter = wgpu::Limits {
min_uniform_buffer_offset_alignment: 512,
..wgpu::Limits::defaults()
};
let clamped = clamp_to_webgl2_defaults(stricter, DownlevelProfile::WebGl2);
assert_eq!(clamped.min_uniform_buffer_offset_alignment, 512);
}
#[test]
fn webgl2_clamp_never_raises_a_real_adapter_ceiling_past_its_own_hardware_limit() {
let constrained = wgpu::Limits {
max_texture_dimension_2d: 1024,
..wgpu::Limits::defaults()
};
let clamped = clamp_to_webgl2_defaults(constrained, DownlevelProfile::WebGl2);
assert_eq!(clamped.max_texture_dimension_2d, 1024);
}
#[test]
fn probe_downlevel_override_forces_storage_buffers_off_via_probe_semantics() {
let desktop = wgpu::Limits::defaults();
assert!(
desktop.max_storage_buffers_per_shader_stage > 0,
"fixture precondition"
);
let clamped = clamp_to_webgl2_defaults(desktop, DownlevelProfile::WebGl2);
assert!(clamped.max_storage_buffers_per_shader_stage > 0);
}
}