From c12b30ad3de114020929f441a1c05e2ec7b755b9 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 8 Apr 2026 00:52:38 +0300 Subject: [PATCH] =?UTF-8?q?feat:=204=C3=97=20MSAA=20anti-aliasing=20for=20?= =?UTF-8?q?main=20drawing=20pipelines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All main render pipelines (wire, hatch, image, mesh) now use 4× multisampling. Drawing passes render to a dedicated MSAA color buffer; a final empty resolve pass transfers the anti-aliased result to the iced surface target. The ViewCube renders post-resolve at 1× to avoid pipeline incompatibility. Depth buffer also upgraded to 4× MSAA. Co-Authored-By: Claude Sonnet 4.6 --- ROADMAP.md | 2 +- src/scene/pipeline/mod.rs | 89 +++++++++++++++++++++++++++++++++------ src/scene/render.rs | 11 ++++- 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index a83254e5..152061f6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -42,7 +42,7 @@ Durum simgeleri: ✅ Tamamlandı · 🔧 Kısmen yapıldı · ⬜ Yapılmadı | 2.11 | UCS simgesi (XYZ tripod) | ✅ | | 2.12 | Solid3D / 3DSOLID tessellation (truck pipeline) | ✅ | | 2.13 | Region / Body / Wire / Silhouette entity render | ⬜ | -| 2.14 | Anti-aliasing / MSAA seçeneği | ⬜ | +| 2.14 | Anti-aliasing / MSAA (4×) | ✅ | --- diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index abdc007e..dbbb6534 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -20,6 +20,9 @@ use crate::scene::image_model::ImageModel; use crate::scene::mesh_model::MeshModel; use crate::scene::wire_model::WireModel; +/// MSAA sample count for the main drawing pipelines. +const MSAA_SAMPLES: u32 = 4; + pub struct Pipeline { wire_pipeline: wgpu::RenderPipeline, hatch_pipeline: wgpu::RenderPipeline, @@ -31,6 +34,10 @@ pub struct Pipeline { image_bgl1: wgpu::BindGroupLayout, depth_texture_size: Size, depth_view: wgpu::TextureView, + /// 4× MSAA color buffer for the main drawing passes. + msaa_view: wgpu::TextureView, + /// Cached texture format (needed to recreate MSAA / depth textures on resize). + surface_format: wgpu::TextureFormat, gpu_wires: Vec, gpu_hatches: Vec, /// Wipeout fills — rendered after wires in a separate pass. @@ -113,7 +120,7 @@ impl Pipeline { bias: wgpu::DepthBiasState::default(), }), multisample: wgpu::MultisampleState { - count: 1, + count: MSAA_SAMPLES, mask: !0, alpha_to_coverage_enabled: false, }, @@ -182,7 +189,7 @@ impl Pipeline { bias: wgpu::DepthBiasState::default(), }), multisample: wgpu::MultisampleState { - count: 1, + count: MSAA_SAMPLES, mask: !0, alpha_to_coverage_enabled: false, }, @@ -236,7 +243,7 @@ impl Pipeline { bias: wgpu::DepthBiasState::default(), }), multisample: wgpu::MultisampleState { - count: 1, + count: MSAA_SAMPLES, mask: !0, alpha_to_coverage_enabled: false, }, @@ -325,7 +332,7 @@ impl Pipeline { bias: wgpu::DepthBiasState::default(), }), multisample: wgpu::MultisampleState { - count: 1, + count: MSAA_SAMPLES, mask: !0, alpha_to_coverage_enabled: false, }, @@ -345,6 +352,10 @@ impl Pipeline { let viewcube = ViewCubePipeline::new(device, queue, format); + let init_size = Size::new(1, 1); + let msaa_view = create_msaa_texture(device, init_size, format) + .create_view(&wgpu::TextureViewDescriptor::default()); + Self { wire_pipeline, hatch_pipeline, @@ -356,6 +367,8 @@ impl Pipeline { image_bgl1, depth_texture_size: Size::new(1, 1), depth_view, + msaa_view, + surface_format: format, gpu_wires: vec![], gpu_hatches: vec![], gpu_wipeouts: vec![], @@ -414,19 +427,24 @@ impl Pipeline { encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, clip_bounds: Rectangle, + bg_color: [f32; 4], ) { let vp = clip_bounds; + let msaa = &self.msaa_view; + let [r, g, b, a] = bg_color; + let clear_color = wgpu::Color { r: r as f64, g: g as f64, b: b as f64, a: a as f64 }; // ── Pass 1: hatch fills ──────────────────────────────────────────── { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("hatch.render_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, + view: msaa, depth_slice: None, resolve_target: None, ops: wgpu::Operations { - load: wgpu::LoadOp::Load, + // Clear MSAA to background color on the first pass. + load: wgpu::LoadOp::Clear(clear_color), store: wgpu::StoreOp::Store, }, })], @@ -465,7 +483,7 @@ impl Pipeline { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("image.render_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, + view: msaa, depth_slice: None, resolve_target: None, ops: wgpu::Operations { @@ -506,7 +524,7 @@ impl Pipeline { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("mesh.render_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, + view: msaa, depth_slice: None, resolve_target: None, ops: wgpu::Operations { @@ -549,7 +567,7 @@ impl Pipeline { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("wire.render_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, + view: msaa, depth_slice: None, resolve_target: None, ops: wgpu::Operations { @@ -591,7 +609,7 @@ impl Pipeline { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("wipeout.render_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, + view: msaa, depth_slice: None, resolve_target: None, ops: wgpu::Operations { @@ -626,12 +644,36 @@ impl Pipeline { pass.draw(0..6, 0..1); } } + + // ── Resolve MSAA → target ───────────────────────────────────────── + // An empty render pass with resolve_target transfers the MSAA samples + // to the non-multisampled iced target surface. + { + 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), + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Discard, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + // No draw calls — the pass itself triggers the MSAA resolve. + } } pub fn ensure_depth_texture(&mut self, device: &wgpu::Device, size: Size) { if self.depth_texture_size != size { - let tex = create_depth_texture(device, size); - self.depth_view = tex.create_view(&wgpu::TextureViewDescriptor::default()); + let depth_tex = create_depth_texture(device, size); + 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()); self.depth_texture_size = size; } } @@ -646,7 +688,7 @@ fn create_depth_texture(device: &wgpu::Device, size: Size) -> wgpu::Texture depth_or_array_layers: 1, }, mip_level_count: 1, - sample_count: 1, + sample_count: MSAA_SAMPLES, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Depth32Float, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, @@ -654,6 +696,27 @@ fn create_depth_texture(device: &wgpu::Device, size: Size) -> wgpu::Texture }) } +fn create_msaa_texture( + device: &wgpu::Device, + size: Size, + format: wgpu::TextureFormat, +) -> wgpu::Texture { + device.create_texture(&wgpu::TextureDescriptor { + label: Some("viewer.msaa_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: MSAA_SAMPLES, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }) +} + impl iced::widget::shader::Pipeline for Pipeline { fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self { Self::new(device, queue, format) diff --git a/src/scene/render.rs b/src/scene/render.rs index 45325518..32e0ddda 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -36,6 +36,8 @@ pub struct Primitive { /// Used by the ViewCube pipeline — no gimbal lock. pub(super) cam_rotation: Mat4, pub(super) hover_region: Option, + /// Background color used to clear the MSAA buffer at the start of each frame. + pub(super) bg_color: [f32; 4], } // ── shader::Program impl ────────────────────────────────────────────────── @@ -59,6 +61,12 @@ impl shader::Program for Scene { } all_wires.extend(self.preview_wires.iter().cloned()); + let bg_color = if self.current_layout == "Model" { + self.bg_color + } else { + self.paper_bg_color + }; + Primitive { wires: all_wires, hatches: self.synced_hatch_models(), @@ -68,6 +76,7 @@ impl shader::Program for Scene { uniforms: Uniforms::new(&cam, bounds), cam_rotation: cam.view_rotation_mat(), hover_region: state.hover_region, + bg_color, } } @@ -149,7 +158,7 @@ impl shader::Primitive for Primitive { target: &iced::wgpu::TextureView, clip: &Rectangle, ) { - pipeline.render(encoder, target, *clip); + pipeline.render(encoder, target, *clip, self.bg_color); pipeline.viewcube.render(encoder, target, *clip); } }