feat: 4× MSAA anti-aliasing for main drawing pipelines

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 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-04-08 00:52:38 +03:00
commit c12b30ad3d
3 changed files with 87 additions and 15 deletions

View file

@ -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×) | ✅ |
---

View file

@ -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<u32>,
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<WireGpu>,
gpu_hatches: Vec<HatchGpu>,
/// 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<u32>,
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<u32>) {
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<u32>) -> 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<u32>) -> wgpu::Texture
})
}
fn create_msaa_texture(
device: &wgpu::Device,
size: Size<u32>,
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)

View file

@ -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<usize>,
/// 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<Msg: std::fmt::Debug + Clone> shader::Program<Msg> 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<Msg: std::fmt::Debug + Clone> shader::Program<Msg> 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<u32>,
) {
pipeline.render(encoder, target, *clip);
pipeline.render(encoder, target, *clip, self.bg_color);
pipeline.viewcube.render(encoder, target, *clip);
}
}