bevyengine / bevyengine/bevy

StandardMaterial causes long delays on Firefox with WebGL

Open
#18,142 5 comments 1 reaction 0 assignees View on GitHub
A-Rendering C-Performance D-Shaders O-WebGL2 S-Needs-Design X-Contentious
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 16h
Merged PRs (30d)
171

Description

## Bevy version

0.15.3

## Relevant system information

`AdapterInfo { name: "Apple M1, or similar", vendor: 4203, device: 0, device_type: IntegratedGpu, driver: "", driver_info: "WebGL 2.0", backend: Gl }`

Browser is Firefox 134.0, "WebGL 2 Driver WSI Info" in `about:support` says it's using CGL

## What you did

Open any Bevy project that uses `StandardMaterial` in Firefox with WebGL. This includes most of the WebGL examples on the Bevy official examples page like .

Alternatively, run any Bevy WebGL project that uses a custom material with a large uniform array of large structs:

main.rs

```rust
use bevy::{
prelude::*,
render::render_resource::{AsBindGroup, ShaderRef, ShaderType},
};

#[derive(Clone, Copy, Default, ShaderType)]
struct LargeStruct {
a: f32,
b: f32,
c: f32,
d: f32,
e: f32,
f: f32,
g: f32,
h: f32,
i: f32,
j: f32,
k: f32,
l: f32,
m: f32,
n: f32,
o: f32,
p: f32,
}

#[derive(AsBindGroup, Asset, Clone, TypePath)]
struct TestMaterial {
#[uniform(0)]
struct_array: [LargeStruct; 200],
}

impl Material for TestMaterial {
fn fragment_shader() -> ShaderRef {
ShaderRef::from("test.wgsl")
}
}

fn main() {
let mut app = App::new();
app.add_plugins((DefaultPlugins, MaterialPlugin::::default()))
.add_systems(Startup, setup)
.run();
}

fn setup(
mut commands: Commands,
mut meshes: ResMut>,
mut materials: ResMut>,
) {
commands.spawn((Camera3d::default(), Transform::from_xyz(0.0, 0.0, 10.0)));
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(4.0, 4.0, 4.0))),
MeshMaterial3d(materials.add(TestMaterial {
struct_array: [LargeStruct::default(); 200],
})),
));
}
```

test.wgsl

```wgsl
struct LargeStruct {
a: f32,
b: f32,
c: f32,
d: f32,
e: f32,
f: f32,
g: f32,
h: f32,
i: f32,
j: f32,
k: f32,
l: f32,
m: f32,
n: f32,
o: f32,
p: f32,
}

@group(2) @binding(0) var struct_array: array;

@fragment
fn fragment() -> @location(0) vec4f {
return vec4(struct_array[0].a, vec3(1.0));
}
```

## What went wrong

The browser takes several seconds to link the WebGL program. This blocks the main thread and causes a long delay when loading the page or whenever shaders are recompiled.

## Additional information

Any browser that uses OpenGL to implement WebGL on macOS runs into this problem. In addition to Firefox, this includes desktop Chrome/Safari whenever the WebGL backend is manually set to OpenGL. Profiling shows that most of the time is spent in calls to `glGetActiveUniform` or `glGetActiveUniformsiv`.

What seems to be going on is:

- While linking a WebGL program, the browser has to collect information about each active uniform with calls to `glGetActiveUniformsiv` and `glGetActiveUniform`. OpenGL treats each member of a struct as a different uniform resource (see [OpenGL introspection documentation](https://www.khronos.org/opengl/wiki/Program_Introspection#Naming>)), so large arrays of large structs can have a giant number of uniforms to query.
- The `StandardMaterial` PBR shader accesses `clusterable_objects.data` in [`pbr_lighting.wgsl`](https://github.com/bevyengine/bevy/blob/release-0.15.3/crates/bevy_pbr/src/render/pbr_lighting.wgsl#L450). When storage buffers are unavailable, this is a uniform array of 204 `ClusterableObject` structs, each with 11 members as defined in [`mesh_view_types.wgsl`](https://github.com/bevyengine/bevy/blob/release-0.15.3/crates/bevy_pbr/src/render/mesh_view_types.wgsl#L108). In [Firefox's implementation](https://github.com/mozilla/gecko-dev/blob/master/dom/canvas/WebGLContext.cpp#L2395), that makes 2244 uniforms for each of five calls to `glGetActiveUniformsiv` to check, plus 2244 calls to `glGetActiveUniform`.
- Parts of a uniform array can be excluded from the list of active uniforms if the shader compiler finds that they're unused, but when the array is in a uniform block, all its uniforms are apparently counted as active (this might be implementation dependent). Naga translates WGSL uniform buffers to GLSL ES uniform blocks, so all members of all 204 entries are queried if any one is accessed.
- The Apple OpenGL implementation is very slow at this for some reason, and the time it takes scales noticeably with the number of uniforms.

If possible, it might help to make space for fewer than 204 entries in `clusterable_objects` at first if there aren't too many point or spot lights in the scene, then somehow increase the capacity up to `MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS` if more lights are added at runtime. This might require recompiling shaders, but it would only affect platforms without storage buffer support.

Contributor guide

Open the contributing guide

Research direction

Start by reading crates/bevy_pbr/src/render/pbr_lighting.wgsl around the clusterable_objects access and the ClusterableObject definition in crates/bevy_pbr/src/render/mesh_view_types.wgsl. Reproduce the StandardMaterial example on Firefox WebGL, then trace the no-storage-buffer fallback and MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS. Done means substantially reducing shader-link delays on affected OpenGL-backed WebGL platforms without breaking scenes with additional point or spot lights.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
computer-graphics, performance, web-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.