Partially fix panics when setting WGPU_SETTINGS_PRIO=webgl2 (#18113)

# Overview

Fixes https://github.com/bevyengine/bevy/issues/17869.

# Summary

`WGPU_SETTINGS_PRIO` changes various limits on `RenderDevice`. This is
useful to simulate platforms with lower limits.

However, some plugins only check the limits on `RenderAdapter` (the
actual GPU) - these limits are not affected by `WGPU_SETTINGS_PRIO`. So
the plugins try to use features that are unavailable and wgpu says "oh
no". See https://github.com/bevyengine/bevy/issues/17869 for details.

The PR adds various checks on `RenderDevice` limits. This is enough to
get most examples working, but some are not fixed (see below).

# Testing

- Tested native, with and without "WGPU_SETTINGS=webgl2".
Win10/Vulkan/Nvidia".
- Also WebGL. Win10/Chrome/Nvidia.

```
$env:WGPU_SETTINGS_PRIO = "webgl2"
cargo run --example testbed_3d
cargo run --example testbed_2d
cargo run --example testbed_ui
cargo run --example deferred_rendering
cargo run --example many_lights
cargo run --example order_independent_transparency # Still broken, see below.
cargo run --example occlusion_culling # Still broken, see below.
```

# Not Fixed

While testing I found a few other cases of limits being broken. 

"Compatibility" settings (WebGPU minimums) breaks native in various
examples.

```
$env:WGPU_SETTINGS_PRIO = "compatibility"
cargo run --example testbed_3d

  In Device::create_bind_group_layout, label = 'build mesh uniforms GPU early occlusion culling bind group layout'
    Too many bindings of type StorageBuffers in Stage ShaderStages(COMPUTE), limit is 8, count was 9. Check the limit `max_storage_buffers_per_shader_stage` passed to `Adapter::request_device`
```

`occlusion_culling` breaks fake webgl.
```
$env:WGPU_SETTINGS_PRIO = "webgl2"
cargo run --example occlusion_culling

thread '<unnamed>' panicked at C:\Projects\bevy\crates\bevy_render\src\render_resource\pipeline_cache.rs:555:28:
index out of bounds: the len is 0 but the index is 2
Encountered a panic in system `bevy_render::renderer::render_system`!
```

`occlusion_culling` breaks real webgl.

```
cargo run --example occlusion_culling --target wasm32-unknown-unknown

ERROR app: panicked at C:\Users\T\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\glow-0.16.0\src\web_sys.rs:4223:9:
Tex storage 2D multisample is not supported
```

OIT breaks fake webgl.

```
$env:WGPU_SETTINGS_PRIO = "webgl2"
cargo run --example order_independent_transparency

  In Device::create_bind_group, label = 'mesh_view_bind_group'
    Number of bindings in bind group descriptor (28) does not match the number of bindings defined in the bind group layout (25)

```

OIT breaks real webgl

```
cargo run --example order_independent_transparency --target wasm32-unknown-unknown

  In Device::create_render_pipeline, label = 'pbr_oit_mesh_pipeline'
    Error matching ShaderStages(FRAGMENT) shader requirements against the pipeline
      Shader global ResourceBinding { group: 0, binding: 34 } is not available in the pipeline layout
        Binding is missing from the pipeline layout
```
This commit is contained in:
Greeble 2025-03-09 20:14:27 +00:00 committed by GitHub
parent cbc931723e
commit 09ff7ce9f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 54 additions and 23 deletions

View File

@ -325,6 +325,18 @@ pub struct DownsampleDepthPipelines {
sampler: Sampler,
}
fn supports_compute_shaders(device: &RenderDevice, adapter: &RenderAdapter) -> bool {
adapter
.get_downlevel_capabilities()
.flags
.contains(DownlevelFlags::COMPUTE_SHADERS)
// Even if the adapter supports compute, we might be simulating a lack of
// compute via device limits (see `WgpuSettingsPriority::WebGL2` and
// `wgpu::Limits::downlevel_webgl2_defaults()`). This will have set all the
// `max_compute_*` limits to zero, so we arbitrarily pick one as a canary.
&& (device.limits().max_compute_workgroup_storage_size != 0)
}
/// Creates the [`DownsampleDepthPipelines`] if downsampling is supported on the
/// current platform.
fn create_downsample_depth_pipelines(
@ -346,11 +358,7 @@ fn create_downsample_depth_pipelines(
// If we don't have compute shaders, we can't invoke the downsample depth
// compute shader.
if !render_adapter
.get_downlevel_capabilities()
.flags
.contains(DownlevelFlags::COMPUTE_SHADERS)
{
if !supports_compute_shaders(&render_device, &render_adapter) {
return;
}

View File

@ -33,6 +33,9 @@ pub const OIT_RESOLVE_SHADER_HANDLE: Handle<Shader> =
/// Contains the render node used to run the resolve pass.
pub mod node;
/// Minimum required value of `wgpu::Limits::max_storage_buffers_per_shader_stage`.
pub const OIT_REQUIRED_STORAGE_BUFFERS: u32 = 2;
/// Plugin needed to resolve the Order Independent Transparency (OIT) buffer to the screen.
pub struct OitResolvePlugin;
impl Plugin for OitResolvePlugin {
@ -50,14 +53,11 @@ impl Plugin for OitResolvePlugin {
return;
};
if !render_app
.world()
.resource::<RenderAdapter>()
.get_downlevel_capabilities()
.flags
.contains(DownlevelFlags::FRAGMENT_WRITABLE_STORAGE)
{
warn!("OrderIndependentTransparencyPlugin not loaded. GPU lacks support: DownlevelFlags::FRAGMENT_WRITABLE_STORAGE.");
if !is_oit_supported(
render_app.world().resource::<RenderAdapter>(),
render_app.world().resource::<RenderDevice>(),
true,
) {
return;
}
@ -73,6 +73,34 @@ impl Plugin for OitResolvePlugin {
}
}
pub fn is_oit_supported(adapter: &RenderAdapter, device: &RenderDevice, warn: bool) -> bool {
if !adapter
.get_downlevel_capabilities()
.flags
.contains(DownlevelFlags::FRAGMENT_WRITABLE_STORAGE)
{
if warn {
warn!("OrderIndependentTransparencyPlugin not loaded. GPU lacks support: DownlevelFlags::FRAGMENT_WRITABLE_STORAGE.");
}
return false;
}
let max_storage_buffers_per_shader_stage = device.limits().max_storage_buffers_per_shader_stage;
if max_storage_buffers_per_shader_stage < OIT_REQUIRED_STORAGE_BUFFERS {
if warn {
warn!(
max_storage_buffers_per_shader_stage,
OIT_REQUIRED_STORAGE_BUFFERS,
"OrderIndependentTransparencyPlugin not loaded. RenderDevice lacks support: max_storage_buffers_per_shader_stage < OIT_REQUIRED_STORAGE_BUFFERS."
);
}
return false;
}
true
}
/// Bind group for the OIT resolve pass.
#[derive(Resource, Deref)]
pub struct OitResolveBindGroup(pub BindGroup);

View File

@ -1,7 +1,7 @@
use alloc::sync::Arc;
use bevy_core_pipeline::{
core_3d::ViewTransmissionTexture,
oit::{OitBuffers, OrderIndependentTransparencySettings},
oit::{resolve::is_oit_supported, OitBuffers, OrderIndependentTransparencySettings},
prepass::ViewPrepassTextures,
tonemapping::{
get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts,
@ -380,15 +380,10 @@ fn layout_entries(
// OIT
if layout_key.contains(MeshPipelineViewLayoutKey::OIT_ENABLED) {
// Check if the GPU supports writable storage buffers in the fragment shader
// If not, we can't use OIT, so we skip the OIT bindings.
// This is a hack to avoid errors on webgl -- the OIT plugin will warn the user that OIT
// is not supported on their platform, so we don't need to do it here.
if render_adapter
.get_downlevel_capabilities()
.flags
.contains(DownlevelFlags::FRAGMENT_WRITABLE_STORAGE)
{
// Check if we can use OIT. This is a hack to avoid errors on webgl --
// the OIT plugin will warn the user that OIT is not supported on their
// platform, so we don't need to do it here.
if is_oit_supported(render_adapter, render_device, false) {
entries = entries.extend_with_indices((
// oit_layers
(34, storage_buffer_sized(false, None)),