From 1d747f3975a288f7ba932f16b30d91590de54943 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Fri, 3 Apr 2026 09:27:00 +0800 Subject: [PATCH 1/4] Fix dof and background motion vectors on webgpu --- .../src/prepass/background_motion_vectors.rs | 8 +++++++- crates/bevy_post_process/src/dof/dof.wgsl | 4 ++-- crates/bevy_post_process/src/dof/mod.rs | 9 ++------- examples/3d/skybox.rs | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs b/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs index 8ac7fd7e2b5cf..4913ac2df25c7 100644 --- a/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs +++ b/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs @@ -150,6 +150,12 @@ impl SpecializedRenderPipeline for BackgroundMotionVectorsPipeline { type Key = BackgroundMotionVectorsPipelineKey; fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mut targets = prepass_target_descriptors(key.normal_prepass, true, false); + if let Some(normal) = &mut targets[0] { + // We don't write normal, set it empty to avoid WebGPU validation error. + // It's a bug that wgpu doesn't validate this, see https://github.com/gfx-rs/wgpu/issues/9147 + normal.write_mask = bevy_render::render_resource::ColorWrites::empty(); + } RenderPipelineDescriptor { label: Some("background_motion_vectors_pipeline".into()), layout: vec![self.bind_group_layout.clone()], @@ -168,7 +174,7 @@ impl SpecializedRenderPipeline for BackgroundMotionVectorsPipeline { }, fragment: Some(FragmentState { shader: self.fragment_shader.clone(), - targets: prepass_target_descriptors(key.normal_prepass, true, false), + targets, ..default() }), ..default() diff --git a/crates/bevy_post_process/src/dof/dof.wgsl b/crates/bevy_post_process/src/dof/dof.wgsl index 7dc32dc3cde1b..563b0af031644 100644 --- a/crates/bevy_post_process/src/dof/dof.wgsl +++ b/crates/bevy_post_process/src/dof/dof.wgsl @@ -212,7 +212,7 @@ fn gaussian_vertical(in: FullscreenVertexOutput) -> @location(0) vec4 { // │ // │ @fragment -fn bokeh_pass_0(in: FullscreenVertexOutput) -> DualOutput { +fn bokeh_pass_a(in: FullscreenVertexOutput) -> DualOutput { let coc = calculate_circle_of_confusion(in.position); let vertical = box_blur_a(in.position, coc, vec2(0.0, 1.0)); let diagonal = box_blur_a(in.position, coc, vec2(COS_NEG_FRAC_PI_6, SIN_NEG_FRAC_PI_6)); @@ -232,7 +232,7 @@ fn bokeh_pass_0(in: FullscreenVertexOutput) -> DualOutput { // • #ifdef DUAL_INPUT @fragment -fn bokeh_pass_1(in: FullscreenVertexOutput) -> @location(0) vec4 { +fn bokeh_pass_b(in: FullscreenVertexOutput) -> @location(0) vec4 { let coc = calculate_circle_of_confusion(in.position); let output_0 = box_blur_a(in.position, coc, vec2(COS_NEG_FRAC_PI_6, SIN_NEG_FRAC_PI_6)); let output_1 = box_blur_b(in.position, coc, vec2(COS_NEG_FRAC_PI_5_6, SIN_NEG_FRAC_PI_5_6)); diff --git a/crates/bevy_post_process/src/dof/mod.rs b/crates/bevy_post_process/src/dof/mod.rs index 7cd15a4121667..fb9ebdc503fa2 100644 --- a/crates/bevy_post_process/src/dof/mod.rs +++ b/crates/bevy_post_process/src/dof/mod.rs @@ -124,8 +124,6 @@ pub enum DepthOfFieldMode { /// /// For more information, see [Wikipedia's article on *bokeh*]. /// - /// This doesn't work on WebGPU. - /// /// [Wikipedia's article on *bokeh*]: https://en.wikipedia.org/wiki/Bokeh Bokeh, @@ -135,9 +133,6 @@ pub enum DepthOfFieldMode { /// aesthetically pleasing but requires less video memory bandwidth. /// /// This is the default. - /// - /// This works on native and WebGPU. - /// If targeting native platforms, consider using [`DepthOfFieldMode::Bokeh`] instead. #[default] Gaussian, } @@ -644,8 +639,8 @@ impl SpecializedRenderPipeline for DepthOfFieldPipeline { entry_point: Some(match key.pass { DofPass::GaussianHorizontal => "gaussian_horizontal".into(), DofPass::GaussianVertical => "gaussian_vertical".into(), - DofPass::BokehPass0 => "bokeh_pass_0".into(), - DofPass::BokehPass1 => "bokeh_pass_1".into(), + DofPass::BokehPass0 => "bokeh_pass_a".into(), + DofPass::BokehPass1 => "bokeh_pass_b".into(), }), targets, }), diff --git a/examples/3d/skybox.rs b/examples/3d/skybox.rs index ed2f8e6edbae4..2057fbc202a10 100644 --- a/examples/3d/skybox.rs +++ b/examples/3d/skybox.rs @@ -1,6 +1,6 @@ //! Load a cubemap texture onto a cube like a skybox and cycle through different compressed texture formats -#[cfg(not(target_arch = "wasm32"))] +#[cfg(any(feature = "webgpu", not(target_arch = "wasm32")))] use bevy::anti_alias::taa::TemporalAntiAliasing; use bevy::{ @@ -73,7 +73,7 @@ fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(( Camera3d::default(), Msaa::Off, - #[cfg(not(target_arch = "wasm32"))] + #[cfg(any(feature = "webgpu", not(target_arch = "wasm32")))] TemporalAntiAliasing::default(), ScreenSpaceAmbientOcclusion::default(), Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y), From 5300c8d8125e14f4a35b91c479fa8526e83a493b Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Fri, 24 Apr 2026 21:50:19 +0800 Subject: [PATCH 2/4] Remove unrelated comment --- .../bevy_core_pipeline/src/prepass/background_motion_vectors.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs b/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs index 4913ac2df25c7..9f706145fc6fe 100644 --- a/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs +++ b/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs @@ -153,7 +153,6 @@ impl SpecializedRenderPipeline for BackgroundMotionVectorsPipeline { let mut targets = prepass_target_descriptors(key.normal_prepass, true, false); if let Some(normal) = &mut targets[0] { // We don't write normal, set it empty to avoid WebGPU validation error. - // It's a bug that wgpu doesn't validate this, see https://github.com/gfx-rs/wgpu/issues/9147 normal.write_mask = bevy_render::render_resource::ColorWrites::empty(); } RenderPipelineDescriptor { From 6adb2068f5336ae16a9886728591aea8dc1e7a1f Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Mon, 27 Apr 2026 12:57:10 +0800 Subject: [PATCH 3/4] Set writeMask except location 1 to empty and update comments --- .../src/prepass/background_motion_vectors.rs | 13 ++++++++++--- crates/bevy_post_process/src/dof/mod.rs | 2 ++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs b/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs index 9f706145fc6fe..15d3c6155be64 100644 --- a/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs +++ b/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs @@ -151,10 +151,17 @@ impl SpecializedRenderPipeline for BackgroundMotionVectorsPipeline { fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { let mut targets = prepass_target_descriptors(key.normal_prepass, true, false); - if let Some(normal) = &mut targets[0] { - // We don't write normal, set it empty to avoid WebGPU validation error. - normal.write_mask = bevy_render::render_resource::ColorWrites::empty(); + // The shader only outputs to attachment at location 1, set write mask of the other attachments to empty + // to avoid WebGPU validation error "Color target has no corresponding fragment stage output but writeMask is not zero". + for target in + targets + .iter_mut() + .enumerate() + .filter_map(|(i, t)| if i != 1 { None } else { t.as_mut() }) + { + target.write_mask = bevy_render::render_resource::ColorWrites::empty(); } + RenderPipelineDescriptor { label: Some("background_motion_vectors_pipeline".into()), layout: vec![self.bind_group_layout.clone()], diff --git a/crates/bevy_post_process/src/dof/mod.rs b/crates/bevy_post_process/src/dof/mod.rs index 4851c577f3b7c..0644b7c499188 100644 --- a/crates/bevy_post_process/src/dof/mod.rs +++ b/crates/bevy_post_process/src/dof/mod.rs @@ -637,6 +637,8 @@ impl SpecializedRenderPipeline for DepthOfFieldPipeline { entry_point: Some(match key.pass { DofPass::GaussianHorizontal => "gaussian_horizontal".into(), DofPass::GaussianVertical => "gaussian_vertical".into(), + // Entry point names that end with number don't work on wasm. Perhaps `naga_oil` bug. + // See DofPass::BokehPass0 => "bokeh_pass_a".into(), DofPass::BokehPass1 => "bokeh_pass_b".into(), }), From edb4d1100bec1d983a0324f88a3790a5a5b00f8c Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Mon, 27 Apr 2026 23:44:55 +0800 Subject: [PATCH 4/4] Fix filter condition --- .../bevy_core_pipeline/src/prepass/background_motion_vectors.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs b/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs index 15d3c6155be64..8b1e43c55ef3b 100644 --- a/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs +++ b/crates/bevy_core_pipeline/src/prepass/background_motion_vectors.rs @@ -157,7 +157,7 @@ impl SpecializedRenderPipeline for BackgroundMotionVectorsPipeline { targets .iter_mut() .enumerate() - .filter_map(|(i, t)| if i != 1 { None } else { t.as_mut() }) + .filter_map(|(i, t)| if i == 1 { None } else { t.as_mut() }) { target.write_mask = bevy_render::render_resource::ColorWrites::empty(); }