gpui: Enable MSAA to Path render for Anti-Aliasing (#22812)

Jason Lee created

Closes #20762

Release Notes:

- N/A

---

Enable MSAA for Anti-Aliasing to Path (`cx.paint_path`) for drawing a
better vector graphics.

```bash
cargo run -p gpui --example gradient --features macos-blade
cargo run -p gpui --example gradient

cargo run -p gpui --example painting --features macos-blade
cargo run -p gpui --example painting
```

**Before**

<img width="1089" alt="image"
src="https://github.com/user-attachments/assets/0ae7240f-4ba9-4ef5-896c-e436c1282770"
/>

**After**

<img width="944" alt="image"
src="https://github.com/user-attachments/assets/71a07ae8-be54-452c-aacc-b8cec1f810c0"
/>

## TODO

- [x] Support Metal and Blade.
- [x] Detect system support to set up sample count.
- [x] Fix extra lines between Path vertices wait #22808 to merge.

Ref https://github.com/kvark/blade/pull/213

Ask @kvark to review.

I am not sure if there is anything I missed. I modified it according to
the
[particle](https://github.com/kvark/blade/tree/main/examples/particle)
example of Blade project. But the difference is that after the first
MSAA render, I did not do it a second time, I tested it and found it was
not necessary.

Change summary

Cargo.lock                                       |  6 
Cargo.toml                                       |  6 
crates/gpui/src/platform/blade/blade_atlas.rs    | 49 ++++++++++++++
crates/gpui/src/platform/blade/blade_renderer.rs | 60 +++++++++++------
crates/gpui/src/platform/mac/metal_atlas.rs      | 22 ++++++
crates/gpui/src/platform/mac/metal_renderer.rs   | 27 ++++++-
6 files changed, 136 insertions(+), 34 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1839,7 +1839,7 @@ dependencies = [
 [[package]]
 name = "blade-graphics"
 version = "0.6.0"
-source = "git+https://github.com/kvark/blade?rev=091a8401033847bb9b6ace3fcf70448d069621c5#091a8401033847bb9b6ace3fcf70448d069621c5"
+source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f"
 dependencies = [
  "ash",
  "ash-window",
@@ -1871,7 +1871,7 @@ dependencies = [
 [[package]]
 name = "blade-macros"
 version = "0.3.0"
-source = "git+https://github.com/kvark/blade?rev=091a8401033847bb9b6ace3fcf70448d069621c5#091a8401033847bb9b6ace3fcf70448d069621c5"
+source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1881,7 +1881,7 @@ dependencies = [
 [[package]]
 name = "blade-util"
 version = "0.2.0"
-source = "git+https://github.com/kvark/blade?rev=091a8401033847bb9b6ace3fcf70448d069621c5#091a8401033847bb9b6ace3fcf70448d069621c5"
+source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f"
 dependencies = [
  "blade-graphics",
  "bytemuck",

Cargo.toml 🔗

@@ -375,9 +375,9 @@ async-watch = "0.3.1"
 async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
 base64 = "0.22"
 bitflags = "2.6.0"
-blade-graphics = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
-blade-macros = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
-blade-util = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
+blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
+blade-macros = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
+blade-util = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
 blake3 = "1.5.3"
 bytes = "1.0"
 cargo_metadata = "0.19"

crates/gpui/src/platform/blade/blade_atlas.rs 🔗

@@ -27,6 +27,7 @@ struct BladeAtlasState {
     tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
     initializations: Vec<AtlasTextureId>,
     uploads: Vec<PendingUpload>,
+    path_sample_count: u32,
 }
 
 #[cfg(gles)]
@@ -42,10 +43,11 @@ impl BladeAtlasState {
 pub struct BladeTextureInfo {
     pub size: gpu::Extent,
     pub raw_view: gpu::TextureView,
+    pub msaa_view: Option<gpu::TextureView>,
 }
 
 impl BladeAtlas {
-    pub(crate) fn new(gpu: &Arc<gpu::Context>) -> Self {
+    pub(crate) fn new(gpu: &Arc<gpu::Context>, path_sample_count: u32) -> Self {
         BladeAtlas(Mutex::new(BladeAtlasState {
             gpu: Arc::clone(gpu),
             upload_belt: BufferBelt::new(BufferBeltDescriptor {
@@ -57,6 +59,7 @@ impl BladeAtlas {
             tiles_by_key: Default::default(),
             initializations: Vec::new(),
             uploads: Vec::new(),
+            path_sample_count,
         }))
     }
 
@@ -106,6 +109,7 @@ impl BladeAtlas {
                 depth: 1,
             },
             raw_view: texture.raw_view,
+            msaa_view: texture.msaa_view,
         }
     }
 }
@@ -204,6 +208,39 @@ impl BladeAtlasState {
             }
         }
 
+        // We currently only enable MSAA for path textures.
+        let (msaa, msaa_view) = if self.path_sample_count > 1 && kind == AtlasTextureKind::Path {
+            let msaa = self.gpu.create_texture(gpu::TextureDesc {
+                name: "msaa path texture",
+                format,
+                size: gpu::Extent {
+                    width: size.width.into(),
+                    height: size.height.into(),
+                    depth: 1,
+                },
+                array_layer_count: 1,
+                mip_level_count: 1,
+                sample_count: self.path_sample_count,
+                dimension: gpu::TextureDimension::D2,
+                usage: gpu::TextureUsage::TARGET,
+            });
+
+            (
+                Some(msaa),
+                Some(self.gpu.create_texture_view(
+                    msaa,
+                    gpu::TextureViewDesc {
+                        name: "msaa texture view",
+                        format,
+                        dimension: gpu::ViewDimension::D2,
+                        subresources: &Default::default(),
+                    },
+                )),
+            )
+        } else {
+            (None, None)
+        };
+
         let raw = self.gpu.create_texture(gpu::TextureDesc {
             name: "atlas",
             format,
@@ -240,6 +277,8 @@ impl BladeAtlasState {
             format,
             raw,
             raw_view,
+            msaa,
+            msaa_view,
             live_atlas_keys: 0,
         };
 
@@ -354,6 +393,8 @@ struct BladeAtlasTexture {
     allocator: BucketedAtlasAllocator,
     raw: gpu::Texture,
     raw_view: gpu::TextureView,
+    msaa: Option<gpu::Texture>,
+    msaa_view: Option<gpu::TextureView>,
     format: gpu::TextureFormat,
     live_atlas_keys: u32,
 }
@@ -381,6 +422,12 @@ impl BladeAtlasTexture {
     fn destroy(&mut self, gpu: &gpu::Context) {
         gpu.destroy_texture(self.raw);
         gpu.destroy_texture_view(self.raw_view);
+        if let Some(msaa) = self.msaa {
+            gpu.destroy_texture(msaa);
+        }
+        if let Some(msaa_view) = self.msaa_view {
+            gpu.destroy_texture_view(msaa_view);
+        }
     }
 
     fn bytes_per_pixel(&self) -> u8 {

crates/gpui/src/platform/blade/blade_renderer.rs 🔗

@@ -7,16 +7,18 @@ use crate::{
     MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad,
     ScaledPixels, Scene, Shadow, Size, Underline,
 };
+use blade_graphics as gpu;
+use blade_util::{BufferBelt, BufferBeltDescriptor};
 use bytemuck::{Pod, Zeroable};
 use collections::HashMap;
 #[cfg(target_os = "macos")]
 use media::core_video::CVMetalTextureCache;
-
-use blade_graphics as gpu;
-use blade_util::{BufferBelt, BufferBeltDescriptor};
 use std::{mem, sync::Arc};
 
 const MAX_FRAME_TIME_MS: u32 = 10000;
+// Use 4x MSAA, all devices support it.
+// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount
+const PATH_SAMPLE_COUNT: u32 = 4;
 
 #[repr(C)]
 #[derive(Clone, Copy, Pod, Zeroable)]
@@ -208,7 +210,10 @@ impl BladePipelines {
                     blend: Some(gpu::BlendState::ADDITIVE),
                     write_mask: gpu::ColorWrites::default(),
                 }],
-                multisample_state: gpu::MultisampleState::default(),
+                multisample_state: gpu::MultisampleState {
+                    sample_count: PATH_SAMPLE_COUNT,
+                    ..Default::default()
+                },
             }),
             paths: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
                 name: "paths",
@@ -348,7 +353,7 @@ impl BladeRenderer {
             min_chunk_size: 0x1000,
             alignment: 0x40, // Vulkan `minStorageBufferOffsetAlignment` on Intel Xe
         });
-        let atlas = Arc::new(BladeAtlas::new(&context.gpu));
+        let atlas = Arc::new(BladeAtlas::new(&context.gpu, PATH_SAMPLE_COUNT));
         let atlas_sampler = context.gpu.create_sampler(gpu::SamplerDesc {
             name: "atlas",
             mag_filter: gpu::FilterMode::Linear,
@@ -497,27 +502,38 @@ impl BladeRenderer {
             };
 
             let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) };
-            let mut pass = self.command_encoder.render(
+            let frame_view = tex_info.raw_view;
+            let color_target = if let Some(msaa_view) = tex_info.msaa_view {
+                gpu::RenderTarget {
+                    view: msaa_view,
+                    init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack),
+                    finish_op: gpu::FinishOp::ResolveTo(frame_view),
+                }
+            } else {
+                gpu::RenderTarget {
+                    view: frame_view,
+                    init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack),
+                    finish_op: gpu::FinishOp::Store,
+                }
+            };
+
+            if let mut pass = self.command_encoder.render(
                 "paths",
                 gpu::RenderTargetSet {
-                    colors: &[gpu::RenderTarget {
-                        view: tex_info.raw_view,
-                        init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack),
-                        finish_op: gpu::FinishOp::Store,
-                    }],
+                    colors: &[color_target],
                     depth_stencil: None,
                 },
-            );
-
-            let mut encoder = pass.with(&self.pipelines.path_rasterization);
-            encoder.bind(
-                0,
-                &ShaderPathRasterizationData {
-                    globals,
-                    b_path_vertices: vertex_buf,
-                },
-            );
-            encoder.draw(0, vertices.len() as u32, 0, 1);
+            ) {
+                let mut encoder = pass.with(&self.pipelines.path_rasterization);
+                encoder.bind(
+                    0,
+                    &ShaderPathRasterizationData {
+                        globals,
+                        b_path_vertices: vertex_buf,
+                    },
+                );
+                encoder.draw(0, vertices.len() as u32, 0, 1);
+            }
         }
     }
 

crates/gpui/src/platform/mac/metal_atlas.rs 🔗

@@ -13,13 +13,14 @@ use std::borrow::Cow;
 pub(crate) struct MetalAtlas(Mutex<MetalAtlasState>);
 
 impl MetalAtlas {
-    pub(crate) fn new(device: Device) -> Self {
+    pub(crate) fn new(device: Device, path_sample_count: u32) -> Self {
         MetalAtlas(Mutex::new(MetalAtlasState {
             device: AssertSend(device),
             monochrome_textures: Default::default(),
             polychrome_textures: Default::default(),
             path_textures: Default::default(),
             tiles_by_key: Default::default(),
+            path_sample_count,
         }))
     }
 
@@ -27,6 +28,10 @@ impl MetalAtlas {
         self.0.lock().texture(id).metal_texture.clone()
     }
 
+    pub(crate) fn msaa_texture(&self, id: AtlasTextureId) -> Option<metal::Texture> {
+        self.0.lock().texture(id).msaa_texture.clone()
+    }
+
     pub(crate) fn allocate(
         &self,
         size: Size<DevicePixels>,
@@ -54,6 +59,7 @@ struct MetalAtlasState {
     polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
     path_textures: AtlasTextureList<MetalAtlasTexture>,
     tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
+    path_sample_count: u32,
 }
 
 impl PlatformAtlas for MetalAtlas {
@@ -176,6 +182,18 @@ impl MetalAtlasState {
         texture_descriptor.set_usage(usage);
         let metal_texture = self.device.new_texture(&texture_descriptor);
 
+        // We currently only enable MSAA for path textures.
+        let msaa_texture = if self.path_sample_count > 1 && kind == AtlasTextureKind::Path {
+            let mut descriptor = texture_descriptor.clone();
+            descriptor.set_texture_type(metal::MTLTextureType::D2Multisample);
+            descriptor.set_storage_mode(metal::MTLStorageMode::Private);
+            descriptor.set_sample_count(self.path_sample_count as _);
+            let msaa_texture = self.device.new_texture(&descriptor);
+            Some(msaa_texture)
+        } else {
+            None
+        };
+
         let texture_list = match kind {
             AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
             AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
@@ -191,6 +209,7 @@ impl MetalAtlasState {
             },
             allocator: etagere::BucketedAtlasAllocator::new(size.into()),
             metal_texture: AssertSend(metal_texture),
+            msaa_texture: AssertSend(msaa_texture),
             live_atlas_keys: 0,
         };
 
@@ -217,6 +236,7 @@ struct MetalAtlasTexture {
     id: AtlasTextureId,
     allocator: BucketedAtlasAllocator,
     metal_texture: AssertSend<metal::Texture>,
+    msaa_texture: AssertSend<Option<metal::Texture>>,
     live_atlas_keys: u32,
 }
 

crates/gpui/src/platform/mac/metal_renderer.rs 🔗

@@ -28,6 +28,9 @@ pub(crate) type PointF = crate::Point<f32>;
 const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
 #[cfg(feature = "runtime_shaders")]
 const SHADERS_SOURCE_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal"));
+// Use 4x MSAA, all devices support it.
+// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount
+const PATH_SAMPLE_COUNT: u32 = 4;
 
 pub type Context = Arc<Mutex<InstanceBufferPool>>;
 pub type Renderer = MetalRenderer;
@@ -170,6 +173,7 @@ impl MetalRenderer {
             "path_rasterization_vertex",
             "path_rasterization_fragment",
             MTLPixelFormat::R16Float,
+            PATH_SAMPLE_COUNT,
         );
         let path_sprites_pipeline_state = build_pipeline_state(
             &device,
@@ -229,7 +233,7 @@ impl MetalRenderer {
         );
 
         let command_queue = device.new_command_queue();
-        let sprite_atlas = Arc::new(MetalAtlas::new(device.clone()));
+        let sprite_atlas = Arc::new(MetalAtlas::new(device.clone(), PATH_SAMPLE_COUNT));
         let core_video_texture_cache =
             unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() };
 
@@ -531,10 +535,20 @@ impl MetalRenderer {
                 .unwrap();
 
             let texture = self.sprite_atlas.metal_texture(texture_id);
-            color_attachment.set_texture(Some(&texture));
-            color_attachment.set_load_action(metal::MTLLoadAction::Clear);
-            color_attachment.set_store_action(metal::MTLStoreAction::Store);
+            let msaa_texture = self.sprite_atlas.msaa_texture(texture_id);
+
+            if let Some(msaa_texture) = msaa_texture {
+                color_attachment.set_texture(Some(&msaa_texture));
+                color_attachment.set_resolve_texture(Some(&texture));
+                color_attachment.set_load_action(metal::MTLLoadAction::Clear);
+                color_attachment.set_store_action(metal::MTLStoreAction::MultisampleResolve);
+            } else {
+                color_attachment.set_texture(Some(&texture));
+                color_attachment.set_load_action(metal::MTLLoadAction::Clear);
+                color_attachment.set_store_action(metal::MTLStoreAction::Store);
+            }
             color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.));
+
             let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
             command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state);
             command_encoder.set_vertex_buffer(
@@ -1160,6 +1174,7 @@ fn build_path_rasterization_pipeline_state(
     vertex_fn_name: &str,
     fragment_fn_name: &str,
     pixel_format: metal::MTLPixelFormat,
+    path_sample_count: u32,
 ) -> metal::RenderPipelineState {
     let vertex_fn = library
         .get_function(vertex_fn_name, None)
@@ -1172,6 +1187,10 @@ fn build_path_rasterization_pipeline_state(
     descriptor.set_label(label);
     descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
     descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
+    if path_sample_count > 1 {
+        descriptor.set_raster_sample_count(path_sample_count as _);
+        descriptor.set_alpha_to_coverage_enabled(true);
+    }
     let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
     color_attachment.set_pixel_format(pixel_format);
     color_attachment.set_blending_enabled(true);