fix: MSAA resolve no longer overwrites widget backgrounds
MSAA and depth textures are now sized to the shader widget's clip bounds instead of the full surface. The resolve writes to a matching clip-sized intermediate texture; a blit pass (blit.wgsl, viewport trick) then copies the result to the exact clip region of the surface target, leaving all other widgets untouched. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c12b30ad3d
commit
42048362e2
3 changed files with 235 additions and 47 deletions
|
|
@ -36,6 +36,13 @@ pub struct Pipeline {
|
||||||
depth_view: wgpu::TextureView,
|
depth_view: wgpu::TextureView,
|
||||||
/// 4× MSAA color buffer for the main drawing passes.
|
/// 4× MSAA color buffer for the main drawing passes.
|
||||||
msaa_view: wgpu::TextureView,
|
msaa_view: wgpu::TextureView,
|
||||||
|
/// Single-sample texture that receives the MSAA resolve result.
|
||||||
|
resolve_view: wgpu::TextureView,
|
||||||
|
/// Pipeline + resources for blitting the resolve texture to the surface target.
|
||||||
|
blit_pipeline: wgpu::RenderPipeline,
|
||||||
|
blit_bind_group_layout: wgpu::BindGroupLayout,
|
||||||
|
blit_sampler: wgpu::Sampler,
|
||||||
|
blit_bind_group: wgpu::BindGroup,
|
||||||
/// Cached texture format (needed to recreate MSAA / depth textures on resize).
|
/// Cached texture format (needed to recreate MSAA / depth textures on resize).
|
||||||
surface_format: wgpu::TextureFormat,
|
surface_format: wgpu::TextureFormat,
|
||||||
gpu_wires: Vec<WireGpu>,
|
gpu_wires: Vec<WireGpu>,
|
||||||
|
|
@ -355,6 +362,95 @@ impl Pipeline {
|
||||||
let init_size = Size::new(1, 1);
|
let init_size = Size::new(1, 1);
|
||||||
let msaa_view = create_msaa_texture(device, init_size, format)
|
let msaa_view = create_msaa_texture(device, init_size, format)
|
||||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
let resolve_tex = create_resolve_texture(device, init_size, format);
|
||||||
|
let resolve_view = resolve_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
// ── Blit pipeline (resolve texture → surface target) ──────────────
|
||||||
|
let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some("blit.shader"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
|
||||||
|
"../../shaders/blit.wgsl"
|
||||||
|
))),
|
||||||
|
});
|
||||||
|
|
||||||
|
let blit_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("blit.bgl"),
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Texture {
|
||||||
|
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||||
|
view_dimension: wgpu::TextureViewDimension::D2,
|
||||||
|
multisampled: false,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let blit_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("blit.pipeline_layout"),
|
||||||
|
bind_group_layouts: &[&blit_bgl],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("blit.pipeline"),
|
||||||
|
layout: Some(&blit_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &blit_shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[],
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
},
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &blit_shader,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format,
|
||||||
|
blend: None,
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
}),
|
||||||
|
multiview: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let blit_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
label: Some("blit.sampler"),
|
||||||
|
mag_filter: wgpu::FilterMode::Nearest,
|
||||||
|
min_filter: wgpu::FilterMode::Nearest,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let blit_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("blit.bind_group"),
|
||||||
|
layout: &blit_bgl,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&resolve_view),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::Sampler(&blit_sampler),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
wire_pipeline,
|
wire_pipeline,
|
||||||
|
|
@ -368,6 +464,11 @@ impl Pipeline {
|
||||||
depth_texture_size: Size::new(1, 1),
|
depth_texture_size: Size::new(1, 1),
|
||||||
depth_view,
|
depth_view,
|
||||||
msaa_view,
|
msaa_view,
|
||||||
|
resolve_view,
|
||||||
|
blit_pipeline,
|
||||||
|
blit_bind_group_layout: blit_bgl,
|
||||||
|
blit_sampler,
|
||||||
|
blit_bind_group,
|
||||||
surface_format: format,
|
surface_format: format,
|
||||||
gpu_wires: vec![],
|
gpu_wires: vec![],
|
||||||
gpu_hatches: vec![],
|
gpu_hatches: vec![],
|
||||||
|
|
@ -459,14 +560,8 @@ impl Pipeline {
|
||||||
timestamp_writes: None,
|
timestamp_writes: None,
|
||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
});
|
});
|
||||||
pass.set_viewport(
|
// MSAA texture is clip-bounds-sized, so viewport starts at (0, 0).
|
||||||
vp.x as f32,
|
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||||
vp.y as f32,
|
|
||||||
vp.width as f32,
|
|
||||||
vp.height as f32,
|
|
||||||
0.0,
|
|
||||||
1.0,
|
|
||||||
);
|
|
||||||
if !self.gpu_hatches.is_empty() {
|
if !self.gpu_hatches.is_empty() {
|
||||||
pass.set_pipeline(&self.hatch_pipeline);
|
pass.set_pipeline(&self.hatch_pipeline);
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
|
|
@ -502,14 +597,7 @@ impl Pipeline {
|
||||||
timestamp_writes: None,
|
timestamp_writes: None,
|
||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
});
|
});
|
||||||
pass.set_viewport(
|
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||||
vp.x as f32,
|
|
||||||
vp.y as f32,
|
|
||||||
vp.width as f32,
|
|
||||||
vp.height as f32,
|
|
||||||
0.0,
|
|
||||||
1.0,
|
|
||||||
);
|
|
||||||
pass.set_pipeline(&self.image_pipeline);
|
pass.set_pipeline(&self.image_pipeline);
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
for img in &self.gpu_images {
|
for img in &self.gpu_images {
|
||||||
|
|
@ -543,14 +631,7 @@ impl Pipeline {
|
||||||
timestamp_writes: None,
|
timestamp_writes: None,
|
||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
});
|
});
|
||||||
pass.set_viewport(
|
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||||
vp.x as f32,
|
|
||||||
vp.y as f32,
|
|
||||||
vp.width as f32,
|
|
||||||
vp.height as f32,
|
|
||||||
0.0,
|
|
||||||
1.0,
|
|
||||||
);
|
|
||||||
pass.set_pipeline(&self.mesh_pipeline);
|
pass.set_pipeline(&self.mesh_pipeline);
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
for mesh in &self.gpu_meshes {
|
for mesh in &self.gpu_meshes {
|
||||||
|
|
@ -586,14 +667,7 @@ impl Pipeline {
|
||||||
timestamp_writes: None,
|
timestamp_writes: None,
|
||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
});
|
});
|
||||||
pass.set_viewport(
|
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||||
vp.x as f32,
|
|
||||||
vp.y as f32,
|
|
||||||
vp.width as f32,
|
|
||||||
vp.height as f32,
|
|
||||||
0.0,
|
|
||||||
1.0,
|
|
||||||
);
|
|
||||||
pass.set_pipeline(&self.wire_pipeline);
|
pass.set_pipeline(&self.wire_pipeline);
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
for wire in &self.gpu_wires {
|
for wire in &self.gpu_wires {
|
||||||
|
|
@ -628,14 +702,7 @@ impl Pipeline {
|
||||||
timestamp_writes: None,
|
timestamp_writes: None,
|
||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
});
|
});
|
||||||
pass.set_viewport(
|
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||||
vp.x as f32,
|
|
||||||
vp.y as f32,
|
|
||||||
vp.width as f32,
|
|
||||||
vp.height as f32,
|
|
||||||
0.0,
|
|
||||||
1.0,
|
|
||||||
);
|
|
||||||
pass.set_pipeline(&self.hatch_pipeline);
|
pass.set_pipeline(&self.hatch_pipeline);
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
for wipeout in &self.gpu_wipeouts {
|
for wipeout in &self.gpu_wipeouts {
|
||||||
|
|
@ -645,16 +712,16 @@ impl Pipeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Resolve MSAA → target ─────────────────────────────────────────
|
// ── Resolve MSAA → clip-sized resolve texture ─────────────────────
|
||||||
// An empty render pass with resolve_target transfers the MSAA samples
|
// Both msaa_view and resolve_view are sized to clip_bounds, so the
|
||||||
// to the non-multisampled iced target surface.
|
// resolve does NOT touch any pixels outside the shader widget's area.
|
||||||
{
|
{
|
||||||
let _resolve = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
let _resolve = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
label: Some("msaa.resolve_pass"),
|
label: Some("msaa.resolve_pass"),
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
view: msaa,
|
view: msaa,
|
||||||
depth_slice: None,
|
depth_slice: None,
|
||||||
resolve_target: Some(target),
|
resolve_target: Some(&self.resolve_view),
|
||||||
ops: wgpu::Operations {
|
ops: wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Load,
|
load: wgpu::LoadOp::Load,
|
||||||
store: wgpu::StoreOp::Discard,
|
store: wgpu::StoreOp::Discard,
|
||||||
|
|
@ -666,6 +733,38 @@ impl Pipeline {
|
||||||
});
|
});
|
||||||
// No draw calls — the pass itself triggers the MSAA resolve.
|
// No draw calls — the pass itself triggers the MSAA resolve.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Blit resolve texture → surface target at clip_bounds position ──
|
||||||
|
// The viewport maps the full-screen NDC quad to exactly clip_bounds
|
||||||
|
// in the surface, leaving all other widgets untouched.
|
||||||
|
{
|
||||||
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("blit.render_pass"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: target,
|
||||||
|
depth_slice: None,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Load,
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
timestamp_writes: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
});
|
||||||
|
pass.set_viewport(
|
||||||
|
vp.x as f32,
|
||||||
|
vp.y as f32,
|
||||||
|
vp.width as f32,
|
||||||
|
vp.height as f32,
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
pass.set_pipeline(&self.blit_pipeline);
|
||||||
|
pass.set_bind_group(0, &self.blit_bind_group, &[]);
|
||||||
|
pass.draw(0..6, 0..1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) {
|
pub fn ensure_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) {
|
||||||
|
|
@ -674,6 +773,23 @@ impl Pipeline {
|
||||||
self.depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
self.depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
let msaa_tex = create_msaa_texture(device, size, self.surface_format);
|
let msaa_tex = create_msaa_texture(device, size, self.surface_format);
|
||||||
self.msaa_view = msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
self.msaa_view = msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
let resolve_tex = create_resolve_texture(device, size, self.surface_format);
|
||||||
|
let resolve_view = resolve_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
self.blit_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("blit.bind_group"),
|
||||||
|
layout: &self.blit_bind_group_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&resolve_view),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::Sampler(&self.blit_sampler),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
self.resolve_view = resolve_view;
|
||||||
self.depth_texture_size = size;
|
self.depth_texture_size = size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -696,6 +812,27 @@ fn create_depth_texture(device: &wgpu::Device, size: Size<u32>) -> wgpu::Texture
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_resolve_texture(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
size: Size<u32>,
|
||||||
|
format: wgpu::TextureFormat,
|
||||||
|
) -> wgpu::Texture {
|
||||||
|
device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("viewer.resolve_texture"),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: size.width.max(1),
|
||||||
|
height: size.height.max(1),
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
view_formats: &[],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn create_msaa_texture(
|
fn create_msaa_texture(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
size: Size<u32>,
|
size: Size<u32>,
|
||||||
|
|
|
||||||
|
|
@ -132,9 +132,16 @@ impl shader::Primitive for Primitive {
|
||||||
viewport: &Viewport,
|
viewport: &Viewport,
|
||||||
) {
|
) {
|
||||||
let phys = viewport.physical_size();
|
let phys = viewport.physical_size();
|
||||||
let size = Size::new(phys.width, phys.height);
|
let full_size = Size::new(phys.width, phys.height);
|
||||||
pipeline.ensure_depth_texture(device, size);
|
// MSAA and depth textures are sized to the shader widget's clip bounds,
|
||||||
pipeline.viewcube.ensure_depth_texture(device, size);
|
// not the full surface — so the MSAA resolve can't overwrite other widgets.
|
||||||
|
let scale = viewport.scale_factor() as f32;
|
||||||
|
let clip_size = Size::new(
|
||||||
|
(_bounds.width * scale).ceil() as u32,
|
||||||
|
(_bounds.height * scale).ceil() as u32,
|
||||||
|
);
|
||||||
|
pipeline.ensure_depth_texture(device, clip_size);
|
||||||
|
pipeline.viewcube.ensure_depth_texture(device, full_size);
|
||||||
pipeline.upload_uniforms(queue, &self.uniforms);
|
pipeline.upload_uniforms(queue, &self.uniforms);
|
||||||
pipeline.upload_hatches(device, &self.hatches);
|
pipeline.upload_hatches(device, &self.hatches);
|
||||||
pipeline.upload_wipeouts(device, &self.wipeout_hatches);
|
pipeline.upload_wipeouts(device, &self.wipeout_hatches);
|
||||||
|
|
|
||||||
44
src/shaders/blit.wgsl
Normal file
44
src/shaders/blit.wgsl
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
// blit.wgsl
|
||||||
|
// Copies the MSAA-resolved texture to the surface target.
|
||||||
|
// A full-screen NDC quad is drawn; the render pass viewport positions it
|
||||||
|
// exactly over the shader widget's clip bounds in the surface.
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
@location(0) uv: vec2<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
var<private> POSITIONS: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
|
||||||
|
vec2<f32>(-1.0, 1.0),
|
||||||
|
vec2<f32>(-1.0, -1.0),
|
||||||
|
vec2<f32>( 1.0, 1.0),
|
||||||
|
vec2<f32>(-1.0, -1.0),
|
||||||
|
vec2<f32>( 1.0, -1.0),
|
||||||
|
vec2<f32>( 1.0, 1.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// NDC Y is up; texture V is down — flip Y.
|
||||||
|
var<private> UVS: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
|
||||||
|
vec2<f32>(0.0, 0.0),
|
||||||
|
vec2<f32>(0.0, 1.0),
|
||||||
|
vec2<f32>(1.0, 0.0),
|
||||||
|
vec2<f32>(0.0, 1.0),
|
||||||
|
vec2<f32>(1.0, 1.0),
|
||||||
|
vec2<f32>(1.0, 0.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
out.position = vec4<f32>(POSITIONS[idx], 0.0, 1.0);
|
||||||
|
out.uv = UVS[idx];
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(0) var t_resolved: texture_2d<f32>;
|
||||||
|
@group(0) @binding(1) var s_resolved: sampler;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
return textureSample(t_resolved, s_resolved, in.uv);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue