diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index dbbb6534f..58232601e 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -36,6 +36,13 @@ pub struct Pipeline { depth_view: wgpu::TextureView, /// 4× MSAA color buffer for the main drawing passes. 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). surface_format: wgpu::TextureFormat, gpu_wires: Vec, @@ -355,6 +362,95 @@ impl Pipeline { let init_size = Size::new(1, 1); let msaa_view = create_msaa_texture(device, init_size, format) .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 { wire_pipeline, @@ -368,6 +464,11 @@ impl Pipeline { depth_texture_size: Size::new(1, 1), depth_view, msaa_view, + resolve_view, + blit_pipeline, + blit_bind_group_layout: blit_bgl, + blit_sampler, + blit_bind_group, surface_format: format, gpu_wires: vec![], gpu_hatches: vec![], @@ -459,14 +560,8 @@ impl Pipeline { 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, - ); + // MSAA texture is clip-bounds-sized, so viewport starts at (0, 0). + pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0); if !self.gpu_hatches.is_empty() { pass.set_pipeline(&self.hatch_pipeline); pass.set_bind_group(0, &self.uniform_bind_group, &[]); @@ -502,14 +597,7 @@ impl Pipeline { 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_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0); pass.set_pipeline(&self.image_pipeline); pass.set_bind_group(0, &self.uniform_bind_group, &[]); for img in &self.gpu_images { @@ -543,14 +631,7 @@ impl Pipeline { 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_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0); pass.set_pipeline(&self.mesh_pipeline); pass.set_bind_group(0, &self.uniform_bind_group, &[]); for mesh in &self.gpu_meshes { @@ -586,14 +667,7 @@ impl Pipeline { 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_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0); pass.set_pipeline(&self.wire_pipeline); pass.set_bind_group(0, &self.uniform_bind_group, &[]); for wire in &self.gpu_wires { @@ -628,14 +702,7 @@ impl Pipeline { 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_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0); pass.set_pipeline(&self.hatch_pipeline); pass.set_bind_group(0, &self.uniform_bind_group, &[]); for wipeout in &self.gpu_wipeouts { @@ -645,16 +712,16 @@ impl Pipeline { } } - // ── Resolve MSAA → target ───────────────────────────────────────── - // An empty render pass with resolve_target transfers the MSAA samples - // to the non-multisampled iced target surface. + // ── Resolve MSAA → clip-sized resolve texture ───────────────────── + // Both msaa_view and resolve_view are sized to clip_bounds, so the + // resolve does NOT touch any pixels outside the shader widget's area. { let _resolve = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("msaa.resolve_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: msaa, depth_slice: None, - resolve_target: Some(target), + resolve_target: Some(&self.resolve_view), ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Discard, @@ -666,6 +733,38 @@ impl Pipeline { }); // 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) { @@ -674,6 +773,23 @@ impl Pipeline { self.depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default()); let msaa_tex = create_msaa_texture(device, size, self.surface_format); 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; } } @@ -696,6 +812,27 @@ fn create_depth_texture(device: &wgpu::Device, size: Size) -> wgpu::Texture }) } +fn create_resolve_texture( + device: &wgpu::Device, + size: Size, + 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( device: &wgpu::Device, size: Size, diff --git a/src/scene/render.rs b/src/scene/render.rs index 32e0dddac..112172264 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -132,9 +132,16 @@ impl shader::Primitive for Primitive { viewport: &Viewport, ) { let phys = viewport.physical_size(); - let size = Size::new(phys.width, phys.height); - pipeline.ensure_depth_texture(device, size); - pipeline.viewcube.ensure_depth_texture(device, size); + let full_size = Size::new(phys.width, phys.height); + // MSAA and depth textures are sized to the shader widget's clip bounds, + // 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_hatches(device, &self.hatches); pipeline.upload_wipeouts(device, &self.wipeout_hatches); diff --git a/src/shaders/blit.wgsl b/src/shaders/blit.wgsl new file mode 100644 index 000000000..de405dc4d --- /dev/null +++ b/src/shaders/blit.wgsl @@ -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, + @location(0) uv: vec2, +} + +var POSITIONS: array, 6> = array, 6>( + vec2(-1.0, 1.0), + vec2(-1.0, -1.0), + vec2( 1.0, 1.0), + vec2(-1.0, -1.0), + vec2( 1.0, -1.0), + vec2( 1.0, 1.0), +); + +// NDC Y is up; texture V is down — flip Y. +var UVS: array, 6> = array, 6>( + vec2(0.0, 0.0), + vec2(0.0, 1.0), + vec2(1.0, 0.0), + vec2(0.0, 1.0), + vec2(1.0, 1.0), + vec2(1.0, 0.0), +); + +@vertex +fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput { + var out: VertexOutput; + out.position = vec4(POSITIONS[idx], 0.0, 1.0); + out.uv = UVS[idx]; + return out; +} + +@group(0) @binding(0) var t_resolved: texture_2d; +@group(0) @binding(1) var s_resolved: sampler; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return textureSample(t_resolved, s_resolved, in.uv); +}