From 4d00d07df1f3d921aeb1fb630c8d9a1789e6b602 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Jul 2025 14:39:24 -0700 Subject: [PATCH] Render paths to a single fixed-size MSAA texture (#34992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is another attempt to solve the same problem as https://github.com/zed-industries/zed/pull/29718, while avoiding the regression on Intel GPUs. ### Background Currently, on main, all paths are first rendered to an intermediate "atlas" texture, similar to what we use for rendering glyphs, but with multi-sample antialiasing enabled. They are then drawn into our actual frame buffer in a separate pass, via the "path sprite" shaders. Notably, the intermediate texture acts as an "atlas" - the paths are laid out in a non-overlapping way, so that each path could be copied to an arbitrary position in the final scene. This non-overlapping approach makes a lot sense for Glyphs (which are frequently re-used in multiple places within a frame, and even across frames), but paths do not have these properties. * we clear the atlas every frame * we rasterize each path separately. there is no deduping. The problem with our current approach is that the path atlas textures can end up using lots of VRAM if the scene contains many paths. This is more of a problem in other apps that use GPUI than it is in Zed, but I do think it's an issue for Zed as well. On Windows, I have hit some crashes related to GPU memory. In https://github.com/zed-industries/zed/pull/29718, @sunli829 simplified path rendering to just draw directly to the frame buffer, and enabled msaa for the whole frame buffer. But apparently this doesn't work well on Intel GPUs because MSAA is slow on those GPUs. So we reverted that PR. ### Solution With this PR, we rasterize paths to an intermediate texture with MSAA. But rather than treating this intermediate texture like an *atlas* (growing it in order to allocate non-overlapping rectangles for every path), we simply use a single fixed-size, color texture that is the same size as thew viewport. In this texture, we rasterize the paths in their final screen position, allowing them to overlap. Then we simply blit them from the resolved texture to the frame buffer. ### To do * [x] Implement for Metal * [x] Implement for Blade * [x] Fix content masking for paths * [x] Fix rendering of partially transparent paths * [x] Verify that this performs well on Intel GPUs (help @notpeter 🙏 ) * [ ] Profile and optimize Release Notes: - N/A --------- Co-authored-by: Junkui Zhang <364772080@qq.com> --- crates/gpui/build.rs | 1 + crates/gpui/examples/painting.rs | 218 +++++- crates/gpui/examples/paths_bench.rs | 92 +++ crates/gpui/src/platform.rs | 1 - crates/gpui/src/platform/blade/blade_atlas.rs | 97 +-- .../gpui/src/platform/blade/blade_renderer.rs | 711 +++++++++++------- crates/gpui/src/platform/blade/shaders.wgsl | 78 +- crates/gpui/src/platform/mac/metal_atlas.rs | 56 +- .../gpui/src/platform/mac/metal_renderer.rs | 520 +++++++------ crates/gpui/src/platform/mac/shaders.metal | 138 ++-- crates/gpui/src/platform/test/window.rs | 2 +- crates/gpui/src/scene.rs | 13 +- 12 files changed, 1131 insertions(+), 796 deletions(-) create mode 100644 crates/gpui/examples/paths_bench.rs diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index aed439744044574c87e8873e0d06f1c5cc68ec26..7ab44a73f5532f3a37fd07f16a384535b5909485 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -128,6 +128,7 @@ mod macos { "AtlasTile".into(), "PathRasterizationInputIndex".into(), "PathVertex_ScaledPixels".into(), + "PathRasterizationVertex".into(), "ShadowInputIndex".into(), "Shadow".into(), "QuadInputIndex".into(), diff --git a/crates/gpui/examples/painting.rs b/crates/gpui/examples/painting.rs index ff4b64cbda124733bc9f2a93c350ec3134759a5e..668aed23772d32a84a81cc0648d6b60dd05e21cf 100644 --- a/crates/gpui/examples/painting.rs +++ b/crates/gpui/examples/painting.rs @@ -1,11 +1,12 @@ use gpui::{ Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder, PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowOptions, canvas, - div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size, + div, linear_color_stop, linear_gradient, point, prelude::*, px, quad, rgb, size, }; struct PaintingViewer { default_lines: Vec<(Path, Background)>, + background_quads: Vec<(Bounds, Background)>, lines: Vec>>, start: Point, dashed: bool, @@ -16,12 +17,148 @@ impl PaintingViewer { fn new(_window: &mut Window, _cx: &mut Context) -> Self { let mut lines = vec![]; + // Black squares beneath transparent paths. + let background_quads = vec![ + ( + Bounds { + origin: point(px(70.), px(70.)), + size: size(px(40.), px(40.)), + }, + gpui::black().into(), + ), + ( + Bounds { + origin: point(px(170.), px(70.)), + size: size(px(40.), px(40.)), + }, + gpui::black().into(), + ), + ( + Bounds { + origin: point(px(270.), px(70.)), + size: size(px(40.), px(40.)), + }, + gpui::black().into(), + ), + ( + Bounds { + origin: point(px(370.), px(70.)), + size: size(px(40.), px(40.)), + }, + gpui::black().into(), + ), + ( + Bounds { + origin: point(px(450.), px(50.)), + size: size(px(80.), px(80.)), + }, + gpui::black().into(), + ), + ]; + + // 50% opaque red path that extends across black quad. + let mut builder = PathBuilder::fill(); + builder.move_to(point(px(50.), px(50.))); + builder.line_to(point(px(130.), px(50.))); + builder.line_to(point(px(130.), px(130.))); + builder.line_to(point(px(50.), px(130.))); + builder.close(); + let path = builder.build().unwrap(); + let mut red = rgb(0xFF0000); + red.a = 0.5; + lines.push((path, red.into())); + + // 50% opaque blue path that extends across black quad. + let mut builder = PathBuilder::fill(); + builder.move_to(point(px(150.), px(50.))); + builder.line_to(point(px(230.), px(50.))); + builder.line_to(point(px(230.), px(130.))); + builder.line_to(point(px(150.), px(130.))); + builder.close(); + let path = builder.build().unwrap(); + let mut blue = rgb(0x0000FF); + blue.a = 0.5; + lines.push((path, blue.into())); + + // 50% opaque green path that extends across black quad. + let mut builder = PathBuilder::fill(); + builder.move_to(point(px(250.), px(50.))); + builder.line_to(point(px(330.), px(50.))); + builder.line_to(point(px(330.), px(130.))); + builder.line_to(point(px(250.), px(130.))); + builder.close(); + let path = builder.build().unwrap(); + let mut green = rgb(0x00FF00); + green.a = 0.5; + lines.push((path, green.into())); + + // 50% opaque black path that extends across black quad. + let mut builder = PathBuilder::fill(); + builder.move_to(point(px(350.), px(50.))); + builder.line_to(point(px(430.), px(50.))); + builder.line_to(point(px(430.), px(130.))); + builder.line_to(point(px(350.), px(130.))); + builder.close(); + let path = builder.build().unwrap(); + let mut black = rgb(0x000000); + black.a = 0.5; + lines.push((path, black.into())); + + // Two 50% opaque red circles overlapping - center should be darker red + let mut builder = PathBuilder::fill(); + let center = point(px(530.), px(85.)); + let radius = px(30.); + builder.move_to(point(center.x + radius, center.y)); + builder.arc_to( + point(radius, radius), + px(0.), + false, + false, + point(center.x - radius, center.y), + ); + builder.arc_to( + point(radius, radius), + px(0.), + false, + false, + point(center.x + radius, center.y), + ); + builder.close(); + let path = builder.build().unwrap(); + let mut red1 = rgb(0xFF0000); + red1.a = 0.5; + lines.push((path, red1.into())); + + let mut builder = PathBuilder::fill(); + let center = point(px(570.), px(85.)); + let radius = px(30.); + builder.move_to(point(center.x + radius, center.y)); + builder.arc_to( + point(radius, radius), + px(0.), + false, + false, + point(center.x - radius, center.y), + ); + builder.arc_to( + point(radius, radius), + px(0.), + false, + false, + point(center.x + radius, center.y), + ); + builder.close(); + let path = builder.build().unwrap(); + let mut red2 = rgb(0xFF0000); + red2.a = 0.5; + lines.push((path, red2.into())); + // draw a Rust logo let mut builder = lyon::path::Path::svg_builder(); lyon::extra::rust_logo::build_logo_path(&mut builder); // move down the Path let mut builder: PathBuilder = builder.into(); - builder.translate(point(px(10.), px(100.))); + builder.translate(point(px(10.), px(200.))); builder.scale(0.9); let path = builder.build().unwrap(); lines.push((path, gpui::black().into())); @@ -30,10 +167,10 @@ impl PaintingViewer { let mut builder = PathBuilder::fill(); builder.add_polygon( &[ - point(px(150.), px(200.)), - point(px(200.), px(125.)), - point(px(200.), px(175.)), - point(px(250.), px(100.)), + point(px(150.), px(300.)), + point(px(200.), px(225.)), + point(px(200.), px(275.)), + point(px(250.), px(200.)), ], false, ); @@ -42,17 +179,17 @@ impl PaintingViewer { // draw a ⭐ let mut builder = PathBuilder::fill(); - builder.move_to(point(px(350.), px(100.))); - builder.line_to(point(px(370.), px(160.))); - builder.line_to(point(px(430.), px(160.))); - builder.line_to(point(px(380.), px(200.))); - builder.line_to(point(px(400.), px(260.))); - builder.line_to(point(px(350.), px(220.))); - builder.line_to(point(px(300.), px(260.))); - builder.line_to(point(px(320.), px(200.))); - builder.line_to(point(px(270.), px(160.))); - builder.line_to(point(px(330.), px(160.))); - builder.line_to(point(px(350.), px(100.))); + builder.move_to(point(px(350.), px(200.))); + builder.line_to(point(px(370.), px(260.))); + builder.line_to(point(px(430.), px(260.))); + builder.line_to(point(px(380.), px(300.))); + builder.line_to(point(px(400.), px(360.))); + builder.line_to(point(px(350.), px(320.))); + builder.line_to(point(px(300.), px(360.))); + builder.line_to(point(px(320.), px(300.))); + builder.line_to(point(px(270.), px(260.))); + builder.line_to(point(px(330.), px(260.))); + builder.line_to(point(px(350.), px(200.))); let path = builder.build().unwrap(); lines.push(( path, @@ -66,7 +203,7 @@ impl PaintingViewer { // draw linear gradient let square_bounds = Bounds { - origin: point(px(450.), px(100.)), + origin: point(px(450.), px(200.)), size: size(px(200.), px(80.)), }; let height = square_bounds.size.height; @@ -96,31 +233,31 @@ impl PaintingViewer { // draw a pie chart let center = point(px(96.), px(96.)); - let pie_center = point(px(775.), px(155.)); + let pie_center = point(px(775.), px(255.)); let segments = [ ( - point(px(871.), px(155.)), - point(px(747.), px(63.)), + point(px(871.), px(255.)), + point(px(747.), px(163.)), rgb(0x1374e9), ), ( - point(px(747.), px(63.)), - point(px(679.), px(163.)), + point(px(747.), px(163.)), + point(px(679.), px(263.)), rgb(0xe13527), ), ( - point(px(679.), px(163.)), - point(px(754.), px(249.)), + point(px(679.), px(263.)), + point(px(754.), px(349.)), rgb(0x0751ce), ), ( - point(px(754.), px(249.)), - point(px(854.), px(210.)), + point(px(754.), px(349.)), + point(px(854.), px(310.)), rgb(0x209742), ), ( - point(px(854.), px(210.)), - point(px(871.), px(155.)), + point(px(854.), px(310.)), + point(px(871.), px(255.)), rgb(0xfbc10a), ), ]; @@ -140,11 +277,11 @@ impl PaintingViewer { .with_line_width(1.) .with_line_join(lyon::path::LineJoin::Bevel); let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options)); - builder.move_to(point(px(40.), px(320.))); + builder.move_to(point(px(40.), px(420.))); for i in 1..50 { builder.line_to(point( px(40.0 + i as f32 * 10.0), - px(320.0 + (i as f32 * 10.0).sin() * 40.0), + px(420.0 + (i as f32 * 10.0).sin() * 40.0), )); } let path = builder.build().unwrap(); @@ -152,6 +289,7 @@ impl PaintingViewer { Self { default_lines: lines.clone(), + background_quads, lines: vec![], start: point(px(0.), px(0.)), dashed: false, @@ -185,6 +323,7 @@ fn button( impl Render for PaintingViewer { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let default_lines = self.default_lines.clone(); + let background_quads = self.background_quads.clone(); let lines = self.lines.clone(); let dashed = self.dashed; @@ -221,6 +360,19 @@ impl Render for PaintingViewer { canvas( move |_, _, _| {}, move |_, _, window, _| { + // First draw background quads + for (bounds, color) in background_quads.iter() { + window.paint_quad(quad( + *bounds, + px(0.), + *color, + px(0.), + gpui::transparent_black(), + Default::default(), + )); + } + + // Then draw the default paths on top for (path, color) in default_lines { window.paint_path(path, color); } @@ -303,6 +455,10 @@ fn main() { |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)), ) .unwrap(); + cx.on_window_closed(|cx| { + cx.quit(); + }) + .detach(); cx.activate(true); }); } diff --git a/crates/gpui/examples/paths_bench.rs b/crates/gpui/examples/paths_bench.rs new file mode 100644 index 0000000000000000000000000000000000000000..a801889ae869ea7c08dce1362036b1d29c4daf36 --- /dev/null +++ b/crates/gpui/examples/paths_bench.rs @@ -0,0 +1,92 @@ +use gpui::{ + Application, Background, Bounds, ColorSpace, Context, Path, PathBuilder, Pixels, Render, + TitlebarOptions, Window, WindowBounds, WindowOptions, canvas, div, linear_color_stop, + linear_gradient, point, prelude::*, px, rgb, size, +}; + +const DEFAULT_WINDOW_WIDTH: Pixels = px(1024.0); +const DEFAULT_WINDOW_HEIGHT: Pixels = px(768.0); + +struct PaintingViewer { + default_lines: Vec<(Path, Background)>, + _painting: bool, +} + +impl PaintingViewer { + fn new(_window: &mut Window, _cx: &mut Context) -> Self { + let mut lines = vec![]; + + // draw a lightening bolt ⚡ + for _ in 0..2000 { + // draw a ⭐ + let mut builder = PathBuilder::fill(); + builder.move_to(point(px(350.), px(100.))); + builder.line_to(point(px(370.), px(160.))); + builder.line_to(point(px(430.), px(160.))); + builder.line_to(point(px(380.), px(200.))); + builder.line_to(point(px(400.), px(260.))); + builder.line_to(point(px(350.), px(220.))); + builder.line_to(point(px(300.), px(260.))); + builder.line_to(point(px(320.), px(200.))); + builder.line_to(point(px(270.), px(160.))); + builder.line_to(point(px(330.), px(160.))); + builder.line_to(point(px(350.), px(100.))); + let path = builder.build().unwrap(); + lines.push(( + path, + linear_gradient( + 180., + linear_color_stop(rgb(0xFACC15), 0.7), + linear_color_stop(rgb(0xD56D0C), 1.), + ) + .color_space(ColorSpace::Oklab), + )); + } + + Self { + default_lines: lines, + _painting: false, + } + } +} + +impl Render for PaintingViewer { + fn render(&mut self, window: &mut Window, _: &mut Context) -> impl IntoElement { + window.request_animation_frame(); + let lines = self.default_lines.clone(); + div().size_full().child( + canvas( + move |_, _, _| {}, + move |_, _, window, _| { + for (path, color) in lines { + window.paint_path(path, color); + } + }, + ) + .size_full(), + ) + } +} + +fn main() { + Application::new().run(|cx| { + cx.open_window( + WindowOptions { + titlebar: Some(TitlebarOptions { + title: Some("Vulkan".into()), + ..Default::default() + }), + focus: true, + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT), + cx, + ))), + ..Default::default() + }, + |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)), + ) + .unwrap(); + cx.activate(true); + }); +} diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 6f227f1d077e96337c82ad7eba9b1d0fd9c7dfc0..1e72d2386807b83b2f71e5d89309f8e75eb8132b 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -809,7 +809,6 @@ pub(crate) struct AtlasTextureId { pub(crate) enum AtlasTextureKind { Monochrome = 0, Polychrome = 1, - Path = 2, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] diff --git a/crates/gpui/src/platform/blade/blade_atlas.rs b/crates/gpui/src/platform/blade/blade_atlas.rs index 78ba52056a9dce1fb4a497ac257d96f6e1e2bd5c..74500ebf8324e4747122ac425388bc122953185e 100644 --- a/crates/gpui/src/platform/blade/blade_atlas.rs +++ b/crates/gpui/src/platform/blade/blade_atlas.rs @@ -10,8 +10,6 @@ use etagere::BucketedAtlasAllocator; use parking_lot::Mutex; use std::{borrow::Cow, ops, sync::Arc}; -pub(crate) const PATH_TEXTURE_FORMAT: gpu::TextureFormat = gpu::TextureFormat::R16Float; - pub(crate) struct BladeAtlas(Mutex); struct PendingUpload { @@ -27,7 +25,6 @@ struct BladeAtlasState { tiles_by_key: FxHashMap, initializations: Vec, uploads: Vec, - path_sample_count: u32, } #[cfg(gles)] @@ -41,13 +38,11 @@ impl BladeAtlasState { } pub struct BladeTextureInfo { - pub size: gpu::Extent, pub raw_view: gpu::TextureView, - pub msaa_view: Option, } impl BladeAtlas { - pub(crate) fn new(gpu: &Arc, path_sample_count: u32) -> Self { + pub(crate) fn new(gpu: &Arc) -> Self { BladeAtlas(Mutex::new(BladeAtlasState { gpu: Arc::clone(gpu), upload_belt: BufferBelt::new(BufferBeltDescriptor { @@ -59,7 +54,6 @@ impl BladeAtlas { tiles_by_key: Default::default(), initializations: Vec::new(), uploads: Vec::new(), - path_sample_count, })) } @@ -67,27 +61,6 @@ impl BladeAtlas { self.0.lock().destroy(); } - pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) { - let mut lock = self.0.lock(); - let textures = &mut lock.storage[texture_kind]; - for texture in textures.iter_mut() { - texture.clear(); - } - } - - /// Allocate a rectangle and make it available for rendering immediately (without waiting for `before_frame`) - pub fn allocate_for_rendering( - &self, - size: Size, - texture_kind: AtlasTextureKind, - gpu_encoder: &mut gpu::CommandEncoder, - ) -> AtlasTile { - let mut lock = self.0.lock(); - let tile = lock.allocate(size, texture_kind); - lock.flush_initializations(gpu_encoder); - tile - } - pub fn before_frame(&self, gpu_encoder: &mut gpu::CommandEncoder) { let mut lock = self.0.lock(); lock.flush(gpu_encoder); @@ -101,15 +74,8 @@ impl BladeAtlas { pub fn get_texture_info(&self, id: AtlasTextureId) -> BladeTextureInfo { let lock = self.0.lock(); let texture = &lock.storage[id]; - let size = texture.allocator.size(); BladeTextureInfo { - size: gpu::Extent { - width: size.width as u32, - height: size.height as u32, - depth: 1, - }, raw_view: texture.raw_view, - msaa_view: texture.msaa_view, } } } @@ -200,48 +166,8 @@ impl BladeAtlasState { format = gpu::TextureFormat::Bgra8UnormSrgb; usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE; } - AtlasTextureKind::Path => { - format = PATH_TEXTURE_FORMAT; - usage = gpu::TextureUsage::COPY - | gpu::TextureUsage::RESOURCE - | gpu::TextureUsage::TARGET; - } } - // 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, - external: None, - }); - - ( - 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, @@ -279,8 +205,6 @@ impl BladeAtlasState { format, raw, raw_view, - msaa, - msaa_view, live_atlas_keys: 0, }; @@ -340,7 +264,6 @@ impl BladeAtlasState { struct BladeAtlasStorage { monochrome_textures: AtlasTextureList, polychrome_textures: AtlasTextureList, - path_textures: AtlasTextureList, } impl ops::Index for BladeAtlasStorage { @@ -349,7 +272,6 @@ impl ops::Index for BladeAtlasStorage { match kind { crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, - crate::AtlasTextureKind::Path => &self.path_textures, } } } @@ -359,7 +281,6 @@ impl ops::IndexMut for BladeAtlasStorage { match kind { crate::AtlasTextureKind::Monochrome => &mut self.monochrome_textures, crate::AtlasTextureKind::Polychrome => &mut self.polychrome_textures, - crate::AtlasTextureKind::Path => &mut self.path_textures, } } } @@ -370,7 +291,6 @@ impl ops::Index for BladeAtlasStorage { let textures = match id.kind { crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, - crate::AtlasTextureKind::Path => &self.path_textures, }; textures[id.index as usize].as_ref().unwrap() } @@ -384,9 +304,6 @@ impl BladeAtlasStorage { for mut texture in self.polychrome_textures.drain().flatten() { texture.destroy(gpu); } - for mut texture in self.path_textures.drain().flatten() { - texture.destroy(gpu); - } } } @@ -395,17 +312,11 @@ struct BladeAtlasTexture { allocator: BucketedAtlasAllocator, raw: gpu::Texture, raw_view: gpu::TextureView, - msaa: Option, - msaa_view: Option, format: gpu::TextureFormat, live_atlas_keys: u32, } impl BladeAtlasTexture { - fn clear(&mut self) { - self.allocator.clear(); - } - fn allocate(&mut self, size: Size) -> Option { let allocation = self.allocator.allocate(size.into())?; let tile = AtlasTile { @@ -424,12 +335,6 @@ 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 { diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index cac47434ae308f7de7123baf26527ccb0da3321d..2e18d2be222dc561d4494ac68db6b5b8d00abed2 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/crates/gpui/src/platform/blade/blade_renderer.rs @@ -1,24 +1,19 @@ // Doing `if let` gives you nice scoping with passes/encoders #![allow(irrefutable_let_patterns)] -use super::{BladeAtlas, BladeContext, PATH_TEXTURE_FORMAT}; +use super::{BladeAtlas, BladeContext}; use crate::{ - AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask, DevicePixels, GpuSpecs, - MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad, - ScaledPixels, Scene, Shadow, Size, Underline, + Background, Bounds, DevicePixels, GpuSpecs, MonochromeSprite, Path, Point, 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 std::{mem, sync::Arc}; +use std::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 DEFAULT_PATH_SAMPLE_COUNT: u32 = 4; #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] @@ -114,8 +109,15 @@ struct ShaderSurfacesData { #[repr(C)] struct PathSprite { bounds: Bounds, +} + +#[derive(Clone, Debug)] +#[repr(C)] +struct PathRasterizationVertex { + xy_position: Point, + st_position: Point, color: Background, - tile: AtlasTile, + bounds: Bounds, } struct BladePipelines { @@ -144,10 +146,7 @@ impl BladePipelines { shader.check_struct_size::(); shader.check_struct_size::(); shader.check_struct_size::(); - assert_eq!( - mem::size_of::>(), - shader.get_struct_size("PathVertex") as usize, - ); + shader.check_struct_size::(); shader.check_struct_size::(); shader.check_struct_size::(); shader.check_struct_size::(); @@ -205,9 +204,16 @@ impl BladePipelines { }, depth_stencil: None, fragment: Some(shader.at("fs_path_rasterization")), + // The original implementation was using ADDITIVE blende mode, + // I don't know why + // color_targets: &[gpu::ColorTargetState { + // format: PATH_TEXTURE_FORMAT, + // blend: Some(gpu::BlendState::ADDITIVE), + // write_mask: gpu::ColorWrites::default(), + // }], color_targets: &[gpu::ColorTargetState { - format: PATH_TEXTURE_FORMAT, - blend: Some(gpu::BlendState::ADDITIVE), + format: surface_info.format, + blend: Some(gpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), write_mask: gpu::ColorWrites::default(), }], multisample_state: gpu::MultisampleState { @@ -226,7 +232,14 @@ impl BladePipelines { }, depth_stencil: None, fragment: Some(shader.at("fs_path")), - color_targets, + color_targets: &[gpu::ColorTargetState { + format: surface_info.format, + blend: Some(gpu::BlendState { + color: gpu::BlendComponent::OVER, + alpha: gpu::BlendComponent::ADDITIVE, + }), + write_mask: gpu::ColorWrites::default(), + }], multisample_state: gpu::MultisampleState::default(), }), underlines: gpu.create_render_pipeline(gpu::RenderPipelineDesc { @@ -317,12 +330,15 @@ pub struct BladeRenderer { last_sync_point: Option, pipelines: BladePipelines, instance_belt: BufferBelt, - path_tiles: HashMap, atlas: Arc, atlas_sampler: gpu::Sampler, #[cfg(target_os = "macos")] core_video_texture_cache: CVMetalTextureCache, path_sample_count: u32, + path_intermediate_texture: gpu::Texture, + path_intermediate_texture_view: gpu::TextureView, + path_intermediate_msaa_texture: Option, + path_intermediate_msaa_texture_view: Option, } impl BladeRenderer { @@ -352,21 +368,43 @@ impl BladeRenderer { let path_sample_count = std::env::var("ZED_PATH_SAMPLE_COUNT") .ok() .and_then(|v| v.parse().ok()) - .unwrap_or(DEFAULT_PATH_SAMPLE_COUNT); + .or_else(|| { + [4, 2, 1] + .into_iter() + .find(|count| context.gpu.supports_texture_sample_count(*count)) + }) + .unwrap_or(1); let pipelines = BladePipelines::new(&context.gpu, surface.info(), path_sample_count); let instance_belt = BufferBelt::new(BufferBeltDescriptor { memory: gpu::Memory::Shared, min_chunk_size: 0x1000, alignment: 0x40, // Vulkan `minStorageBufferOffsetAlignment` on Intel Xe }); - let atlas = Arc::new(BladeAtlas::new(&context.gpu, path_sample_count)); + let atlas = Arc::new(BladeAtlas::new(&context.gpu)); let atlas_sampler = context.gpu.create_sampler(gpu::SamplerDesc { - name: "atlas", + name: "path rasterization sampler", mag_filter: gpu::FilterMode::Linear, min_filter: gpu::FilterMode::Linear, ..Default::default() }); + let (path_intermediate_texture, path_intermediate_texture_view) = + create_path_intermediate_texture( + &context.gpu, + surface.info().format, + config.size.width, + config.size.height, + ); + let (path_intermediate_msaa_texture, path_intermediate_msaa_texture_view) = + create_msaa_texture_if_needed( + &context.gpu, + surface.info().format, + config.size.width, + config.size.height, + path_sample_count, + ) + .unzip(); + #[cfg(target_os = "macos")] let core_video_texture_cache = unsafe { CVMetalTextureCache::new( @@ -383,12 +421,15 @@ impl BladeRenderer { last_sync_point: None, pipelines, instance_belt, - path_tiles: HashMap::default(), atlas, atlas_sampler, #[cfg(target_os = "macos")] core_video_texture_cache, path_sample_count, + path_intermediate_texture, + path_intermediate_texture_view, + path_intermediate_msaa_texture, + path_intermediate_msaa_texture_view, }) } @@ -441,6 +482,35 @@ impl BladeRenderer { self.surface_config.size = gpu_size; self.gpu .reconfigure_surface(&mut self.surface, self.surface_config); + self.gpu.destroy_texture(self.path_intermediate_texture); + self.gpu + .destroy_texture_view(self.path_intermediate_texture_view); + if let Some(msaa_texture) = self.path_intermediate_msaa_texture { + self.gpu.destroy_texture(msaa_texture); + } + if let Some(msaa_view) = self.path_intermediate_msaa_texture_view { + self.gpu.destroy_texture_view(msaa_view); + } + let (path_intermediate_texture, path_intermediate_texture_view) = + create_path_intermediate_texture( + &self.gpu, + self.surface.info().format, + gpu_size.width, + gpu_size.height, + ); + self.path_intermediate_texture = path_intermediate_texture; + self.path_intermediate_texture_view = path_intermediate_texture_view; + let (path_intermediate_msaa_texture, path_intermediate_msaa_texture_view) = + create_msaa_texture_if_needed( + &self.gpu, + self.surface.info().format, + gpu_size.width, + gpu_size.height, + self.path_sample_count, + ) + .unzip(); + self.path_intermediate_msaa_texture = path_intermediate_msaa_texture; + self.path_intermediate_msaa_texture_view = path_intermediate_msaa_texture_view; } } @@ -491,76 +561,63 @@ impl BladeRenderer { } #[profiling::function] - fn rasterize_paths(&mut self, paths: &[Path]) { - self.path_tiles.clear(); - let mut vertices_by_texture_id = HashMap::default(); - - for path in paths { - let clipped_bounds = path - .bounds - .intersect(&path.content_mask.bounds) - .map_origin(|origin| origin.floor()) - .map_size(|size| size.ceil()); - let tile = self.atlas.allocate_for_rendering( - clipped_bounds.size.map(Into::into), - AtlasTextureKind::Path, - &mut self.command_encoder, - ); - vertices_by_texture_id - .entry(tile.texture_id) - .or_insert(Vec::new()) - .extend(path.vertices.iter().map(|vertex| PathVertex { - xy_position: vertex.xy_position - clipped_bounds.origin - + tile.bounds.origin.map(Into::into), - st_position: vertex.st_position, - content_mask: ContentMask { - bounds: tile.bounds.map(Into::into), - }, - })); - self.path_tiles.insert(path.id, tile); + fn draw_paths_to_intermediate( + &mut self, + paths: &[Path], + width: f32, + height: f32, + ) { + self.command_encoder + .init_texture(self.path_intermediate_texture); + if let Some(msaa_texture) = self.path_intermediate_msaa_texture { + self.command_encoder.init_texture(msaa_texture); } - for (texture_id, vertices) in vertices_by_texture_id { - let tex_info = self.atlas.get_texture_info(texture_id); + let target = if let Some(msaa_view) = self.path_intermediate_msaa_texture_view { + gpu::RenderTarget { + view: msaa_view, + init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack), + finish_op: gpu::FinishOp::ResolveTo(self.path_intermediate_texture_view), + } + } else { + gpu::RenderTarget { + view: self.path_intermediate_texture_view, + init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack), + finish_op: gpu::FinishOp::Store, + } + }; + if let mut pass = self.command_encoder.render( + "rasterize paths", + gpu::RenderTargetSet { + colors: &[target], + depth_stencil: None, + }, + ) { let globals = GlobalParams { - viewport_size: [tex_info.size.width as f32, tex_info.size.height as f32], + viewport_size: [width, height], premultiplied_alpha: 0, pad: 0, }; - + let mut encoder = pass.with(&self.pipelines.path_rasterization); + + let mut vertices = Vec::new(); + for path in paths { + vertices.extend(path.vertices.iter().map(|v| PathRasterizationVertex { + xy_position: v.xy_position, + st_position: v.st_position, + color: path.color, + bounds: path.bounds.intersect(&path.content_mask.bounds), + })); + } let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) }; - 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: &[color_target], - depth_stencil: None, + encoder.bind( + 0, + &ShaderPathRasterizationData { + globals, + b_path_vertices: vertex_buf, }, - ) { - 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); - } + ); + encoder.draw(0, vertices.len() as u32, 0, 1); } } @@ -572,12 +629,20 @@ impl BladeRenderer { self.gpu.destroy_command_encoder(&mut self.command_encoder); self.pipelines.destroy(&self.gpu); self.gpu.destroy_surface(&mut self.surface); + self.gpu.destroy_texture(self.path_intermediate_texture); + self.gpu + .destroy_texture_view(self.path_intermediate_texture_view); + if let Some(msaa_texture) = self.path_intermediate_msaa_texture { + self.gpu.destroy_texture(msaa_texture); + } + if let Some(msaa_view) = self.path_intermediate_msaa_texture_view { + self.gpu.destroy_texture_view(msaa_view); + } } pub fn draw(&mut self, scene: &Scene) { self.command_encoder.start(); self.atlas.before_frame(&mut self.command_encoder); - self.rasterize_paths(scene.paths()); let frame = { profiling::scope!("acquire frame"); @@ -597,7 +662,7 @@ impl BladeRenderer { pad: 0, }; - if let mut pass = self.command_encoder.render( + let mut pass = self.command_encoder.render( "main", gpu::RenderTargetSet { colors: &[gpu::RenderTarget { @@ -607,209 +672,235 @@ impl BladeRenderer { }], depth_stencil: None, }, - ) { - profiling::scope!("render pass"); - for batch in scene.batches() { - match batch { - PrimitiveBatch::Quads(quads) => { - let instance_buf = - unsafe { self.instance_belt.alloc_typed(quads, &self.gpu) }; - let mut encoder = pass.with(&self.pipelines.quads); - encoder.bind( - 0, - &ShaderQuadsData { - globals, - b_quads: instance_buf, - }, - ); - encoder.draw(0, 4, 0, quads.len() as u32); - } - PrimitiveBatch::Shadows(shadows) => { - let instance_buf = - unsafe { self.instance_belt.alloc_typed(shadows, &self.gpu) }; - let mut encoder = pass.with(&self.pipelines.shadows); - encoder.bind( - 0, - &ShaderShadowsData { - globals, - b_shadows: instance_buf, - }, - ); - encoder.draw(0, 4, 0, shadows.len() as u32); - } - PrimitiveBatch::Paths(paths) => { - let mut encoder = pass.with(&self.pipelines.paths); - // todo(linux): group by texture ID - for path in paths { - let tile = &self.path_tiles[&path.id]; - let tex_info = self.atlas.get_texture_info(tile.texture_id); - let origin = path.bounds.intersect(&path.content_mask.bounds).origin; - let sprites = [PathSprite { - bounds: Bounds { - origin: origin.map(|p| p.floor()), - size: tile.bounds.size.map(Into::into), - }, - color: path.color, - tile: (*tile).clone(), - }]; + ); - let instance_buf = - unsafe { self.instance_belt.alloc_typed(&sprites, &self.gpu) }; - encoder.bind( - 0, - &ShaderPathsData { - globals, - t_sprite: tex_info.raw_view, - s_sprite: self.atlas_sampler, - b_path_sprites: instance_buf, - }, - ); - encoder.draw(0, 4, 0, sprites.len() as u32); + profiling::scope!("render pass"); + for batch in scene.batches() { + match batch { + PrimitiveBatch::Quads(quads) => { + let instance_buf = unsafe { self.instance_belt.alloc_typed(quads, &self.gpu) }; + let mut encoder = pass.with(&self.pipelines.quads); + encoder.bind( + 0, + &ShaderQuadsData { + globals, + b_quads: instance_buf, + }, + ); + encoder.draw(0, 4, 0, quads.len() as u32); + } + PrimitiveBatch::Shadows(shadows) => { + let instance_buf = + unsafe { self.instance_belt.alloc_typed(shadows, &self.gpu) }; + let mut encoder = pass.with(&self.pipelines.shadows); + encoder.bind( + 0, + &ShaderShadowsData { + globals, + b_shadows: instance_buf, + }, + ); + encoder.draw(0, 4, 0, shadows.len() as u32); + } + PrimitiveBatch::Paths(paths) => { + let Some(first_path) = paths.first() else { + continue; + }; + drop(pass); + self.draw_paths_to_intermediate( + paths, + self.surface_config.size.width as f32, + self.surface_config.size.height as f32, + ); + pass = self.command_encoder.render( + "main", + gpu::RenderTargetSet { + colors: &[gpu::RenderTarget { + view: frame.texture_view(), + init_op: gpu::InitOp::Load, + finish_op: gpu::FinishOp::Store, + }], + depth_stencil: None, + }, + ); + let mut encoder = pass.with(&self.pipelines.paths); + // When copying paths from the intermediate texture to the drawable, + // each pixel must only be copied once, in case of transparent paths. + // + // If all paths have the same draw order, then their bounds are all + // disjoint, so we can copy each path's bounds individually. If this + // batch combines different draw orders, we perform a single copy + // for a minimal spanning rect. + let sprites = if paths.last().unwrap().order == first_path.order { + paths + .iter() + .map(|path| PathSprite { + bounds: path.bounds, + }) + .collect() + } else { + let mut bounds = first_path.bounds; + for path in paths.iter().skip(1) { + bounds = bounds.union(&path.bounds); } - } - PrimitiveBatch::Underlines(underlines) => { - let instance_buf = - unsafe { self.instance_belt.alloc_typed(underlines, &self.gpu) }; - let mut encoder = pass.with(&self.pipelines.underlines); - encoder.bind( - 0, - &ShaderUnderlinesData { - globals, - b_underlines: instance_buf, - }, - ); - encoder.draw(0, 4, 0, underlines.len() as u32); - } - PrimitiveBatch::MonochromeSprites { - texture_id, - sprites, - } => { - let tex_info = self.atlas.get_texture_info(texture_id); - let instance_buf = - unsafe { self.instance_belt.alloc_typed(sprites, &self.gpu) }; - let mut encoder = pass.with(&self.pipelines.mono_sprites); - encoder.bind( - 0, - &ShaderMonoSpritesData { - globals, - t_sprite: tex_info.raw_view, - s_sprite: self.atlas_sampler, - b_mono_sprites: instance_buf, - }, - ); - encoder.draw(0, 4, 0, sprites.len() as u32); - } - PrimitiveBatch::PolychromeSprites { - texture_id, - sprites, - } => { - let tex_info = self.atlas.get_texture_info(texture_id); - let instance_buf = - unsafe { self.instance_belt.alloc_typed(sprites, &self.gpu) }; - let mut encoder = pass.with(&self.pipelines.poly_sprites); - encoder.bind( - 0, - &ShaderPolySpritesData { - globals, - t_sprite: tex_info.raw_view, - s_sprite: self.atlas_sampler, - b_poly_sprites: instance_buf, - }, - ); - encoder.draw(0, 4, 0, sprites.len() as u32); - } - PrimitiveBatch::Surfaces(surfaces) => { - let mut _encoder = pass.with(&self.pipelines.surfaces); - - for surface in surfaces { - #[cfg(not(target_os = "macos"))] - { - let _ = surface; - continue; - }; - - #[cfg(target_os = "macos")] - { - let (t_y, t_cb_cr) = unsafe { - use core_foundation::base::TCFType as _; - use std::ptr; - - assert_eq!( + vec![PathSprite { bounds }] + }; + let instance_buf = + unsafe { self.instance_belt.alloc_typed(&sprites, &self.gpu) }; + encoder.bind( + 0, + &ShaderPathsData { + globals, + t_sprite: self.path_intermediate_texture_view, + s_sprite: self.atlas_sampler, + b_path_sprites: instance_buf, + }, + ); + encoder.draw(0, 4, 0, sprites.len() as u32); + } + PrimitiveBatch::Underlines(underlines) => { + let instance_buf = + unsafe { self.instance_belt.alloc_typed(underlines, &self.gpu) }; + let mut encoder = pass.with(&self.pipelines.underlines); + encoder.bind( + 0, + &ShaderUnderlinesData { + globals, + b_underlines: instance_buf, + }, + ); + encoder.draw(0, 4, 0, underlines.len() as u32); + } + PrimitiveBatch::MonochromeSprites { + texture_id, + sprites, + } => { + let tex_info = self.atlas.get_texture_info(texture_id); + let instance_buf = + unsafe { self.instance_belt.alloc_typed(sprites, &self.gpu) }; + let mut encoder = pass.with(&self.pipelines.mono_sprites); + encoder.bind( + 0, + &ShaderMonoSpritesData { + globals, + t_sprite: tex_info.raw_view, + s_sprite: self.atlas_sampler, + b_mono_sprites: instance_buf, + }, + ); + encoder.draw(0, 4, 0, sprites.len() as u32); + } + PrimitiveBatch::PolychromeSprites { + texture_id, + sprites, + } => { + let tex_info = self.atlas.get_texture_info(texture_id); + let instance_buf = + unsafe { self.instance_belt.alloc_typed(sprites, &self.gpu) }; + let mut encoder = pass.with(&self.pipelines.poly_sprites); + encoder.bind( + 0, + &ShaderPolySpritesData { + globals, + t_sprite: tex_info.raw_view, + s_sprite: self.atlas_sampler, + b_poly_sprites: instance_buf, + }, + ); + encoder.draw(0, 4, 0, sprites.len() as u32); + } + PrimitiveBatch::Surfaces(surfaces) => { + let mut _encoder = pass.with(&self.pipelines.surfaces); + + for surface in surfaces { + #[cfg(not(target_os = "macos"))] + { + let _ = surface; + continue; + }; + + #[cfg(target_os = "macos")] + { + let (t_y, t_cb_cr) = unsafe { + use core_foundation::base::TCFType as _; + use std::ptr; + + assert_eq!( surface.image_buffer.get_pixel_format(), core_video::pixel_buffer::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange ); - let y_texture = self - .core_video_texture_cache - .create_texture_from_image( - surface.image_buffer.as_concrete_TypeRef(), - ptr::null(), - metal::MTLPixelFormat::R8Unorm, - surface.image_buffer.get_width_of_plane(0), - surface.image_buffer.get_height_of_plane(0), - 0, - ) - .unwrap(); - let cb_cr_texture = self - .core_video_texture_cache - .create_texture_from_image( - surface.image_buffer.as_concrete_TypeRef(), - ptr::null(), - metal::MTLPixelFormat::RG8Unorm, - surface.image_buffer.get_width_of_plane(1), - surface.image_buffer.get_height_of_plane(1), - 1, - ) - .unwrap(); - ( - gpu::TextureView::from_metal_texture( - &objc2::rc::Retained::retain( - foreign_types::ForeignTypeRef::as_ptr( - y_texture.as_texture_ref(), - ) - as *mut objc2::runtime::ProtocolObject< - dyn objc2_metal::MTLTexture, - >, + let y_texture = self + .core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + ptr::null(), + metal::MTLPixelFormat::R8Unorm, + surface.image_buffer.get_width_of_plane(0), + surface.image_buffer.get_height_of_plane(0), + 0, + ) + .unwrap(); + let cb_cr_texture = self + .core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + ptr::null(), + metal::MTLPixelFormat::RG8Unorm, + surface.image_buffer.get_width_of_plane(1), + surface.image_buffer.get_height_of_plane(1), + 1, + ) + .unwrap(); + ( + gpu::TextureView::from_metal_texture( + &objc2::rc::Retained::retain( + foreign_types::ForeignTypeRef::as_ptr( + y_texture.as_texture_ref(), ) - .unwrap(), - gpu::TexelAspects::COLOR, - ), - gpu::TextureView::from_metal_texture( - &objc2::rc::Retained::retain( - foreign_types::ForeignTypeRef::as_ptr( - cb_cr_texture.as_texture_ref(), - ) - as *mut objc2::runtime::ProtocolObject< - dyn objc2_metal::MTLTexture, - >, + as *mut objc2::runtime::ProtocolObject< + dyn objc2_metal::MTLTexture, + >, + ) + .unwrap(), + gpu::TexelAspects::COLOR, + ), + gpu::TextureView::from_metal_texture( + &objc2::rc::Retained::retain( + foreign_types::ForeignTypeRef::as_ptr( + cb_cr_texture.as_texture_ref(), ) - .unwrap(), - gpu::TexelAspects::COLOR, - ), - ) - }; - - _encoder.bind( - 0, - &ShaderSurfacesData { - globals, - surface_locals: SurfaceParams { - bounds: surface.bounds.into(), - content_mask: surface.content_mask.bounds.into(), - }, - t_y, - t_cb_cr, - s_surface: self.atlas_sampler, + as *mut objc2::runtime::ProtocolObject< + dyn objc2_metal::MTLTexture, + >, + ) + .unwrap(), + gpu::TexelAspects::COLOR, + ), + ) + }; + + _encoder.bind( + 0, + &ShaderSurfacesData { + globals, + surface_locals: SurfaceParams { + bounds: surface.bounds.into(), + content_mask: surface.content_mask.bounds.into(), }, - ); + t_y, + t_cb_cr, + s_surface: self.atlas_sampler, + }, + ); - _encoder.draw(0, 4, 0, 1); - } + _encoder.draw(0, 4, 0, 1); } } } } } + drop(pass); self.command_encoder.present(frame); let sync_point = self.gpu.submit(&mut self.command_encoder); @@ -817,9 +908,79 @@ impl BladeRenderer { profiling::scope!("finish"); self.instance_belt.flush(&sync_point); self.atlas.after_frame(&sync_point); - self.atlas.clear_textures(AtlasTextureKind::Path); self.wait_for_gpu(); self.last_sync_point = Some(sync_point); } } + +fn create_path_intermediate_texture( + gpu: &gpu::Context, + format: gpu::TextureFormat, + width: u32, + height: u32, +) -> (gpu::Texture, gpu::TextureView) { + let texture = gpu.create_texture(gpu::TextureDesc { + name: "path intermediate", + format, + size: gpu::Extent { + width, + height, + depth: 1, + }, + array_layer_count: 1, + mip_level_count: 1, + sample_count: 1, + dimension: gpu::TextureDimension::D2, + usage: gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE | gpu::TextureUsage::TARGET, + external: None, + }); + let texture_view = gpu.create_texture_view( + texture, + gpu::TextureViewDesc { + name: "path intermediate view", + format, + dimension: gpu::ViewDimension::D2, + subresources: &Default::default(), + }, + ); + (texture, texture_view) +} + +fn create_msaa_texture_if_needed( + gpu: &gpu::Context, + format: gpu::TextureFormat, + width: u32, + height: u32, + sample_count: u32, +) -> Option<(gpu::Texture, gpu::TextureView)> { + if sample_count <= 1 { + return None; + } + let texture_msaa = gpu.create_texture(gpu::TextureDesc { + name: "path intermediate msaa", + format, + size: gpu::Extent { + width, + height, + depth: 1, + }, + array_layer_count: 1, + mip_level_count: 1, + sample_count, + dimension: gpu::TextureDimension::D2, + usage: gpu::TextureUsage::TARGET, + external: None, + }); + let texture_view_msaa = gpu.create_texture_view( + texture_msaa, + gpu::TextureViewDesc { + name: "path intermediate msaa view", + format, + dimension: gpu::ViewDimension::D2, + subresources: &Default::default(), + }, + ); + + Some((texture_msaa, texture_view_msaa)) +} diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index 0b34a0eea32fd492b5a82055e591bf22d593f136..b1ffb1812effa24e674e0238fcc6ddef7dc0f882 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -924,16 +924,19 @@ fn fs_shadow(input: ShadowVarying) -> @location(0) vec4 { // --- path rasterization --- // -struct PathVertex { +struct PathRasterizationVertex { xy_position: vec2, st_position: vec2, - content_mask: Bounds, + color: Background, + bounds: Bounds, } -var b_path_vertices: array; + +var b_path_vertices: array; struct PathRasterizationVarying { @builtin(position) position: vec4, @location(0) st_position: vec2, + @location(1) vertex_id: u32, //TODO: use `clip_distance` once Naga supports it @location(3) clip_distances: vec4, } @@ -945,40 +948,54 @@ fn vs_path_rasterization(@builtin(vertex_index) vertex_id: u32) -> PathRasteriza var out = PathRasterizationVarying(); out.position = to_device_position_impl(v.xy_position); out.st_position = v.st_position; - out.clip_distances = distance_from_clip_rect_impl(v.xy_position, v.content_mask); + out.vertex_id = vertex_id; + out.clip_distances = distance_from_clip_rect_impl(v.xy_position, v.bounds); return out; } @fragment -fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) f32 { +fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) vec4 { let dx = dpdx(input.st_position); let dy = dpdy(input.st_position); if (any(input.clip_distances < vec4(0.0))) { - return 0.0; + return vec4(0.0); } - let gradient = 2.0 * input.st_position.xx * vec2(dx.x, dy.x) - vec2(dx.y, dy.y); - let f = input.st_position.x * input.st_position.x - input.st_position.y; - let distance = f / length(gradient); - return saturate(0.5 - distance); + let v = b_path_vertices[input.vertex_id]; + let background = v.color; + let bounds = v.bounds; + + var alpha: f32; + if (length(vec2(dx.x, dy.x)) < 0.001) { + // If the gradient is too small, return a solid color. + alpha = 1.0; + } else { + let gradient = 2.0 * input.st_position.xx * vec2(dx.x, dy.x) - vec2(dx.y, dy.y); + let f = input.st_position.x * input.st_position.x - input.st_position.y; + let distance = f / length(gradient); + alpha = saturate(0.5 - distance); + } + let gradient_color = prepare_gradient_color( + background.tag, + background.color_space, + background.solid, + background.colors, + ); + let color = gradient_color(background, input.position.xy, bounds, + gradient_color.solid, gradient_color.color0, gradient_color.color1); + return vec4(color.rgb * color.a * alpha, color.a * alpha); } // --- paths --- // struct PathSprite { bounds: Bounds, - color: Background, - tile: AtlasTile, } var b_path_sprites: array; struct PathVarying { @builtin(position) position: vec4, - @location(0) tile_position: vec2, - @location(1) @interpolate(flat) instance_id: u32, - @location(2) @interpolate(flat) color_solid: vec4, - @location(3) @interpolate(flat) color0: vec4, - @location(4) @interpolate(flat) color1: vec4, + @location(0) texture_coords: vec2, } @vertex @@ -986,33 +1003,22 @@ fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); let sprite = b_path_sprites[instance_id]; // Don't apply content mask because it was already accounted for when rasterizing the path. + let device_position = to_device_position(unit_vertex, sprite.bounds); + // For screen-space intermediate texture, convert screen position to texture coordinates + let screen_position = sprite.bounds.origin + unit_vertex * sprite.bounds.size; + let texture_coords = screen_position / globals.viewport_size; var out = PathVarying(); - out.position = to_device_position(unit_vertex, sprite.bounds); - out.tile_position = to_tile_position(unit_vertex, sprite.tile); - out.instance_id = instance_id; + out.position = device_position; + out.texture_coords = texture_coords; - let gradient = prepare_gradient_color( - sprite.color.tag, - sprite.color.color_space, - sprite.color.solid, - sprite.color.colors - ); - out.color_solid = gradient.solid; - out.color0 = gradient.color0; - out.color1 = gradient.color1; return out; } @fragment fn fs_path(input: PathVarying) -> @location(0) vec4 { - let sample = textureSample(t_sprite, s_sprite, input.tile_position).r; - let mask = 1.0 - abs(1.0 - sample % 2.0); - let sprite = b_path_sprites[input.instance_id]; - let background = sprite.color; - let color = gradient_color(background, input.position.xy, sprite.bounds, - input.color_solid, input.color0, input.color1); - return blend_color(color, mask); + let sample = textureSample(t_sprite, s_sprite, input.texture_coords); + return sample; } // --- underlines --- // diff --git a/crates/gpui/src/platform/mac/metal_atlas.rs b/crates/gpui/src/platform/mac/metal_atlas.rs index 366f2dcc3ca5b0227a790ef7c25375891ab62504..5d2d8e63e06a1ea6251c1fd2edf461eeeedec612 100644 --- a/crates/gpui/src/platform/mac/metal_atlas.rs +++ b/crates/gpui/src/platform/mac/metal_atlas.rs @@ -13,53 +13,25 @@ use std::borrow::Cow; pub(crate) struct MetalAtlas(Mutex); impl MetalAtlas { - pub(crate) fn new(device: Device, path_sample_count: u32) -> Self { + pub(crate) fn new(device: Device) -> 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, })) } pub(crate) fn metal_texture(&self, id: AtlasTextureId) -> metal::Texture { self.0.lock().texture(id).metal_texture.clone() } - - pub(crate) fn msaa_texture(&self, id: AtlasTextureId) -> Option { - self.0.lock().texture(id).msaa_texture.clone() - } - - pub(crate) fn allocate( - &self, - size: Size, - texture_kind: AtlasTextureKind, - ) -> Option { - self.0.lock().allocate(size, texture_kind) - } - - pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) { - let mut lock = self.0.lock(); - let textures = match texture_kind { - AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, - AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, - AtlasTextureKind::Path => &mut lock.path_textures, - }; - for texture in textures.iter_mut() { - texture.clear(); - } - } } struct MetalAtlasState { device: AssertSend, monochrome_textures: AtlasTextureList, polychrome_textures: AtlasTextureList, - path_textures: AtlasTextureList, tiles_by_key: FxHashMap, - path_sample_count: u32, } impl PlatformAtlas for MetalAtlas { @@ -94,7 +66,6 @@ impl PlatformAtlas for MetalAtlas { let textures = match id.kind { AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, - AtlasTextureKind::Path => &mut lock.polychrome_textures, }; let Some(texture_slot) = textures @@ -128,7 +99,6 @@ impl MetalAtlasState { let textures = match texture_kind { AtlasTextureKind::Monochrome => &mut self.monochrome_textures, AtlasTextureKind::Polychrome => &mut self.polychrome_textures, - AtlasTextureKind::Path => &mut self.path_textures, }; if let Some(tile) = textures @@ -173,31 +143,14 @@ impl MetalAtlasState { pixel_format = metal::MTLPixelFormat::BGRA8Unorm; usage = metal::MTLTextureUsage::ShaderRead; } - AtlasTextureKind::Path => { - pixel_format = metal::MTLPixelFormat::R16Float; - usage = metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead; - } } texture_descriptor.set_pixel_format(pixel_format); 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, - AtlasTextureKind::Path => &mut self.path_textures, }; let index = texture_list.free_list.pop(); @@ -209,7 +162,6 @@ impl MetalAtlasState { }, allocator: etagere::BucketedAtlasAllocator::new(size.into()), metal_texture: AssertSend(metal_texture), - msaa_texture: AssertSend(msaa_texture), live_atlas_keys: 0, }; @@ -226,7 +178,6 @@ impl MetalAtlasState { let textures = match id.kind { crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, - crate::AtlasTextureKind::Path => &self.path_textures, }; textures[id.index as usize].as_ref().unwrap() } @@ -236,15 +187,10 @@ struct MetalAtlasTexture { id: AtlasTextureId, allocator: BucketedAtlasAllocator, metal_texture: AssertSend, - msaa_texture: AssertSend>, live_atlas_keys: u32, } impl MetalAtlasTexture { - fn clear(&mut self) { - self.allocator.clear(); - } - fn allocate(&mut self, size: Size) -> Option { let allocation = self.allocator.allocate(size.into())?; let tile = AtlasTile { diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 3cdc2dd2cf42ea7c2a92152893679aa930466869..fb5cb852d656e8d07354e44b103422afe261f12e 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -1,27 +1,30 @@ use super::metal_atlas::MetalAtlas; use crate::{ - AtlasTextureId, AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask, DevicePixels, - MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, - Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline, point, size, + AtlasTextureId, Background, Bounds, ContentMask, DevicePixels, MonochromeSprite, PaintSurface, + Path, Point, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, + Surface, Underline, point, size, }; -use anyhow::{Context as _, Result}; +use anyhow::Result; use block::ConcreteBlock; use cocoa::{ base::{NO, YES}, foundation::{NSSize, NSUInteger}, quartzcore::AutoresizingMask, }; -use collections::HashMap; + use core_foundation::base::TCFType; use core_video::{ metal_texture::CVMetalTextureGetTexture, metal_texture_cache::CVMetalTextureCache, pixel_buffer::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, }; use foreign_types::{ForeignType, ForeignTypeRef}; -use metal::{CAMetalLayer, CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange}; +use metal::{ + CAMetalLayer, CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange, + RenderPassColorAttachmentDescriptorRef, +}; use objc::{self, msg_send, sel, sel_impl}; use parking_lot::Mutex; -use smallvec::SmallVec; + use std::{cell::Cell, ffi::c_void, mem, ptr, sync::Arc}; // Exported to metal @@ -111,6 +114,17 @@ pub(crate) struct MetalRenderer { instance_buffer_pool: Arc>, sprite_atlas: Arc, core_video_texture_cache: core_video::metal_texture_cache::CVMetalTextureCache, + path_intermediate_texture: Option, + path_intermediate_msaa_texture: Option, + path_sample_count: u32, +} + +#[repr(C)] +pub struct PathRasterizationVertex { + pub xy_position: Point, + pub st_position: Point, + pub color: Background, + pub bounds: Bounds, } impl MetalRenderer { @@ -175,10 +189,10 @@ impl MetalRenderer { "paths_rasterization", "path_rasterization_vertex", "path_rasterization_fragment", - MTLPixelFormat::R16Float, + MTLPixelFormat::BGRA8Unorm, PATH_SAMPLE_COUNT, ); - let path_sprites_pipeline_state = build_pipeline_state( + let path_sprites_pipeline_state = build_path_sprite_pipeline_state( &device, &library, "path_sprites", @@ -236,7 +250,7 @@ impl MetalRenderer { ); let command_queue = device.new_command_queue(); - let sprite_atlas = Arc::new(MetalAtlas::new(device.clone(), PATH_SAMPLE_COUNT)); + let sprite_atlas = Arc::new(MetalAtlas::new(device.clone())); let core_video_texture_cache = CVMetalTextureCache::new(None, device.clone(), None).unwrap(); @@ -257,6 +271,9 @@ impl MetalRenderer { instance_buffer_pool, sprite_atlas, core_video_texture_cache, + path_intermediate_texture: None, + path_intermediate_msaa_texture: None, + path_sample_count: PATH_SAMPLE_COUNT, } } @@ -289,6 +306,31 @@ impl MetalRenderer { setDrawableSize: size ]; } + let device_pixels_size = Size { + width: DevicePixels(size.width as i32), + height: DevicePixels(size.height as i32), + }; + self.update_path_intermediate_textures(device_pixels_size); + } + + fn update_path_intermediate_textures(&mut self, size: Size) { + let texture_descriptor = metal::TextureDescriptor::new(); + texture_descriptor.set_width(size.width.0 as u64); + texture_descriptor.set_height(size.height.0 as u64); + texture_descriptor.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm); + texture_descriptor + .set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead); + self.path_intermediate_texture = Some(self.device.new_texture(&texture_descriptor)); + + if self.path_sample_count > 1 { + let mut msaa_descriptor = texture_descriptor.clone(); + msaa_descriptor.set_texture_type(metal::MTLTextureType::D2Multisample); + msaa_descriptor.set_storage_mode(metal::MTLStorageMode::Private); + msaa_descriptor.set_sample_count(self.path_sample_count as _); + self.path_intermediate_msaa_texture = Some(self.device.new_texture(&msaa_descriptor)); + } else { + self.path_intermediate_msaa_texture = None; + } } pub fn update_transparency(&self, _transparent: bool) { @@ -374,38 +416,18 @@ impl MetalRenderer { ) -> Result { let command_queue = self.command_queue.clone(); let command_buffer = command_queue.new_command_buffer(); - let mut instance_offset = 0; - - let path_tiles = self - .rasterize_paths( - scene.paths(), - instance_buffer, - &mut instance_offset, - command_buffer, - ) - .with_context(|| format!("rasterizing {} paths", scene.paths().len()))?; - - let render_pass_descriptor = metal::RenderPassDescriptor::new(); - let color_attachment = render_pass_descriptor - .color_attachments() - .object_at(0) - .unwrap(); - - color_attachment.set_texture(Some(drawable.texture())); - color_attachment.set_load_action(metal::MTLLoadAction::Clear); - color_attachment.set_store_action(metal::MTLStoreAction::Store); let alpha = if self.layer.is_opaque() { 1. } else { 0. }; - color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha)); - let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); + let mut instance_offset = 0; - command_encoder.set_viewport(metal::MTLViewport { - originX: 0.0, - originY: 0.0, - width: i32::from(viewport_size.width) as f64, - height: i32::from(viewport_size.height) as f64, - znear: 0.0, - zfar: 1.0, - }); + let mut command_encoder = new_command_encoder( + command_buffer, + drawable, + viewport_size, + |color_attachment| { + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha)); + }, + ); for batch in scene.batches() { let ok = match batch { @@ -414,29 +436,53 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ), PrimitiveBatch::Quads(quads) => self.draw_quads( quads, instance_buffer, &mut instance_offset, viewport_size, - command_encoder, - ), - PrimitiveBatch::Paths(paths) => self.draw_paths( - paths, - &path_tiles, - instance_buffer, - &mut instance_offset, - viewport_size, - command_encoder, + &command_encoder, ), + PrimitiveBatch::Paths(paths) => { + command_encoder.end_encoding(); + + let did_draw = self.draw_paths_to_intermediate( + paths, + instance_buffer, + &mut instance_offset, + viewport_size, + command_buffer, + ); + + command_encoder = new_command_encoder( + command_buffer, + drawable, + viewport_size, + |color_attachment| { + color_attachment.set_load_action(metal::MTLLoadAction::Load); + }, + ); + + if did_draw { + self.draw_paths_from_intermediate( + paths, + instance_buffer, + &mut instance_offset, + viewport_size, + &command_encoder, + ) + } else { + false + } + } PrimitiveBatch::Underlines(underlines) => self.draw_underlines( underlines, instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ), PrimitiveBatch::MonochromeSprites { texture_id, @@ -447,7 +493,7 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ), PrimitiveBatch::PolychromeSprites { texture_id, @@ -458,17 +504,16 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ), PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces( surfaces, instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ), }; - if !ok { command_encoder.end_encoding(); anyhow::bail!( @@ -493,104 +538,90 @@ impl MetalRenderer { Ok(command_buffer.to_owned()) } - fn rasterize_paths( + fn draw_paths_to_intermediate( &self, paths: &[Path], instance_buffer: &mut InstanceBuffer, instance_offset: &mut usize, + viewport_size: Size, command_buffer: &metal::CommandBufferRef, - ) -> Option> { - self.sprite_atlas.clear_textures(AtlasTextureKind::Path); - - let mut tiles = HashMap::default(); - let mut vertices_by_texture_id = HashMap::default(); - for path in paths { - let clipped_bounds = path.bounds.intersect(&path.content_mask.bounds); - - let tile = self - .sprite_atlas - .allocate(clipped_bounds.size.map(Into::into), AtlasTextureKind::Path)?; - vertices_by_texture_id - .entry(tile.texture_id) - .or_insert(Vec::new()) - .extend(path.vertices.iter().map(|vertex| PathVertex { - xy_position: vertex.xy_position - clipped_bounds.origin - + tile.bounds.origin.map(Into::into), - st_position: vertex.st_position, - content_mask: ContentMask { - bounds: tile.bounds.map(Into::into), - }, - })); - tiles.insert(path.id, tile); + ) -> bool { + if paths.is_empty() { + return true; } + let Some(intermediate_texture) = &self.path_intermediate_texture else { + return false; + }; - for (texture_id, vertices) in vertices_by_texture_id { - align_offset(instance_offset); - let vertices_bytes_len = mem::size_of_val(vertices.as_slice()); - let next_offset = *instance_offset + vertices_bytes_len; - if next_offset > instance_buffer.size { - return None; - } - - let render_pass_descriptor = metal::RenderPassDescriptor::new(); - let color_attachment = render_pass_descriptor - .color_attachments() - .object_at(0) - .unwrap(); - - let texture = self.sprite_atlas.metal_texture(texture_id); - 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 render_pass_descriptor = metal::RenderPassDescriptor::new(); + let color_attachment = render_pass_descriptor + .color_attachments() + .object_at(0) + .unwrap(); + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 0.)); - 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( - PathRasterizationInputIndex::Vertices as u64, - Some(&instance_buffer.metal_buffer), - *instance_offset as u64, - ); - let texture_size = Size { - width: DevicePixels::from(texture.width()), - height: DevicePixels::from(texture.height()), - }; - command_encoder.set_vertex_bytes( - PathRasterizationInputIndex::AtlasTextureSize as u64, - mem::size_of_val(&texture_size) as u64, - &texture_size as *const Size as *const _, - ); + if let Some(msaa_texture) = &self.path_intermediate_msaa_texture { + color_attachment.set_texture(Some(msaa_texture)); + color_attachment.set_resolve_texture(Some(intermediate_texture)); + color_attachment.set_store_action(metal::MTLStoreAction::MultisampleResolve); + } else { + color_attachment.set_texture(Some(intermediate_texture)); + color_attachment.set_store_action(metal::MTLStoreAction::Store); + } - let buffer_contents = unsafe { - (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) - }; - unsafe { - ptr::copy_nonoverlapping( - vertices.as_ptr() as *const u8, - buffer_contents, - vertices_bytes_len, - ); - } + 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.draw_primitives( - metal::MTLPrimitiveType::Triangle, - 0, - vertices.len() as u64, - ); + align_offset(instance_offset); + let mut vertices = Vec::new(); + for path in paths { + vertices.extend(path.vertices.iter().map(|v| PathRasterizationVertex { + xy_position: v.xy_position, + st_position: v.st_position, + color: path.color, + bounds: path.bounds.intersect(&path.content_mask.bounds), + })); + } + let vertices_bytes_len = mem::size_of_val(vertices.as_slice()); + let next_offset = *instance_offset + vertices_bytes_len; + if next_offset > instance_buffer.size { command_encoder.end_encoding(); - *instance_offset = next_offset; + return false; } + command_encoder.set_vertex_buffer( + PathRasterizationInputIndex::Vertices as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_vertex_bytes( + PathRasterizationInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + command_encoder.set_fragment_buffer( + PathRasterizationInputIndex::Vertices as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + unsafe { + ptr::copy_nonoverlapping( + vertices.as_ptr() as *const u8, + buffer_contents, + vertices_bytes_len, + ); + } + command_encoder.draw_primitives( + metal::MTLPrimitiveType::Triangle, + 0, + vertices.len() as u64, + ); + *instance_offset = next_offset; - Some(tiles) + command_encoder.end_encoding(); + true } fn draw_shadows( @@ -715,18 +746,21 @@ impl MetalRenderer { true } - fn draw_paths( + fn draw_paths_from_intermediate( &self, paths: &[Path], - tiles_by_path_id: &HashMap, instance_buffer: &mut InstanceBuffer, instance_offset: &mut usize, viewport_size: Size, command_encoder: &metal::RenderCommandEncoderRef, ) -> bool { - if paths.is_empty() { + let Some(ref first_path) = paths.first() else { return true; - } + }; + + let Some(ref intermediate_texture) = self.path_intermediate_texture else { + return false; + }; command_encoder.set_render_pipeline_state(&self.path_sprites_pipeline_state); command_encoder.set_vertex_buffer( @@ -740,88 +774,65 @@ impl MetalRenderer { &viewport_size as *const Size as *const _, ); - let mut prev_texture_id = None; - let mut sprites = SmallVec::<[_; 1]>::new(); - let mut paths_and_tiles = paths - .iter() - .map(|path| (path, tiles_by_path_id.get(&path.id).unwrap())) - .peekable(); + command_encoder.set_fragment_texture( + SpriteInputIndex::AtlasTexture as u64, + Some(intermediate_texture), + ); - loop { - if let Some((path, tile)) = paths_and_tiles.peek() { - if prev_texture_id.map_or(true, |texture_id| texture_id == tile.texture_id) { - prev_texture_id = Some(tile.texture_id); - let origin = path.bounds.intersect(&path.content_mask.bounds).origin; - sprites.push(PathSprite { - bounds: Bounds { - origin: origin.map(|p| p.floor()), - size: tile.bounds.size.map(Into::into), - }, - color: path.color, - tile: (*tile).clone(), - }); - paths_and_tiles.next(); - continue; - } + // When copying paths from the intermediate texture to the drawable, + // each pixel must only be copied once, in case of transparent paths. + // + // If all paths have the same draw order, then their bounds are all + // disjoint, so we can copy each path's bounds individually. If this + // batch combines different draw orders, we perform a single copy + // for a minimal spanning rect. + let sprites; + if paths.last().unwrap().order == first_path.order { + sprites = paths + .iter() + .map(|path| PathSprite { + bounds: path.bounds, + }) + .collect(); + } else { + let mut bounds = first_path.bounds; + for path in paths.iter().skip(1) { + bounds = bounds.union(&path.bounds); } + sprites = vec![PathSprite { bounds }]; + } - if sprites.is_empty() { - break; - } else { - align_offset(instance_offset); - let texture_id = prev_texture_id.take().unwrap(); - let texture: metal::Texture = self.sprite_atlas.metal_texture(texture_id); - let texture_size = size( - DevicePixels(texture.width() as i32), - DevicePixels(texture.height() as i32), - ); - - command_encoder.set_vertex_buffer( - SpriteInputIndex::Sprites as u64, - Some(&instance_buffer.metal_buffer), - *instance_offset as u64, - ); - command_encoder.set_vertex_bytes( - SpriteInputIndex::AtlasTextureSize as u64, - mem::size_of_val(&texture_size) as u64, - &texture_size as *const Size as *const _, - ); - command_encoder.set_fragment_buffer( - SpriteInputIndex::Sprites as u64, - Some(&instance_buffer.metal_buffer), - *instance_offset as u64, - ); - command_encoder - .set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); + align_offset(instance_offset); + let sprite_bytes_len = mem::size_of_val(sprites.as_slice()); + let next_offset = *instance_offset + sprite_bytes_len; + if next_offset > instance_buffer.size { + return false; + } - let sprite_bytes_len = mem::size_of_val(sprites.as_slice()); - let next_offset = *instance_offset + sprite_bytes_len; - if next_offset > instance_buffer.size { - return false; - } + command_encoder.set_vertex_buffer( + SpriteInputIndex::Sprites as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); - let buffer_contents = unsafe { - (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) - }; + let buffer_contents = + unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) }; + unsafe { + ptr::copy_nonoverlapping( + sprites.as_ptr() as *const u8, + buffer_contents, + sprite_bytes_len, + ); + } - unsafe { - ptr::copy_nonoverlapping( - sprites.as_ptr() as *const u8, - buffer_contents, - sprite_bytes_len, - ); - } + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + sprites.len() as u64, + ); + *instance_offset = next_offset; - command_encoder.draw_primitives_instanced( - metal::MTLPrimitiveType::Triangle, - 0, - 6, - sprites.len() as u64, - ); - *instance_offset = next_offset; - sprites.clear(); - } - } true } @@ -1136,6 +1147,33 @@ impl MetalRenderer { } } +fn new_command_encoder<'a>( + command_buffer: &'a metal::CommandBufferRef, + drawable: &'a metal::MetalDrawableRef, + viewport_size: Size, + configure_color_attachment: impl Fn(&RenderPassColorAttachmentDescriptorRef), +) -> &'a metal::RenderCommandEncoderRef { + let render_pass_descriptor = metal::RenderPassDescriptor::new(); + let color_attachment = render_pass_descriptor + .color_attachments() + .object_at(0) + .unwrap(); + color_attachment.set_texture(Some(drawable.texture())); + color_attachment.set_store_action(metal::MTLStoreAction::Store); + configure_color_attachment(color_attachment); + + let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); + command_encoder.set_viewport(metal::MTLViewport { + originX: 0.0, + originY: 0.0, + width: i32::from(viewport_size.width) as f64, + height: i32::from(viewport_size.height) as f64, + znear: 0.0, + zfar: 1.0, + }); + command_encoder +} + fn build_pipeline_state( device: &metal::DeviceRef, library: &metal::LibraryRef, @@ -1170,6 +1208,40 @@ fn build_pipeline_state( .expect("could not create render pipeline state") } +fn build_path_sprite_pipeline_state( + device: &metal::DeviceRef, + library: &metal::LibraryRef, + label: &str, + vertex_fn_name: &str, + fragment_fn_name: &str, + pixel_format: metal::MTLPixelFormat, +) -> metal::RenderPipelineState { + let vertex_fn = library + .get_function(vertex_fn_name, None) + .expect("error locating vertex function"); + let fragment_fn = library + .get_function(fragment_fn_name, None) + .expect("error locating fragment function"); + + let descriptor = metal::RenderPipelineDescriptor::new(); + descriptor.set_label(label); + descriptor.set_vertex_function(Some(vertex_fn.as_ref())); + descriptor.set_fragment_function(Some(fragment_fn.as_ref())); + let color_attachment = descriptor.color_attachments().object_at(0).unwrap(); + color_attachment.set_pixel_format(pixel_format); + color_attachment.set_blending_enabled(true); + color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add); + color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add); + color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); + color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One); + + device + .new_render_pipeline_state(&descriptor) + .expect("could not create render pipeline state") +} + fn build_path_rasterization_pipeline_state( device: &metal::DeviceRef, library: &metal::LibraryRef, @@ -1192,7 +1264,7 @@ fn build_path_rasterization_pipeline_state( 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); + descriptor.set_alpha_to_coverage_enabled(false); } let color_attachment = descriptor.color_attachments().object_at(0).unwrap(); color_attachment.set_pixel_format(pixel_format); @@ -1201,8 +1273,8 @@ fn build_path_rasterization_pipeline_state( color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add); color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One); color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One); - color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::One); - color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); + color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); device .new_render_pipeline_state(&descriptor) @@ -1257,15 +1329,13 @@ enum SurfaceInputIndex { #[repr(C)] enum PathRasterizationInputIndex { Vertices = 0, - AtlasTextureSize = 1, + ViewportSize = 1, } #[derive(Clone, Debug, Eq, PartialEq)] #[repr(C)] pub struct PathSprite { pub bounds: Bounds, - pub color: Background, - pub tile: AtlasTile, } #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index 64ebb1e22b3b2645f61af308dd832a80ef4eda52..f9d5bdbf4c4ae1fa6ce098463ce63701a7019bbc 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders.metal @@ -701,107 +701,117 @@ fragment float4 polychrome_sprite_fragment( struct PathRasterizationVertexOutput { float4 position [[position]]; float2 st_position; + uint vertex_id [[flat]]; float clip_rect_distance [[clip_distance]][4]; }; struct PathRasterizationFragmentInput { float4 position [[position]]; float2 st_position; + uint vertex_id [[flat]]; }; vertex PathRasterizationVertexOutput path_rasterization_vertex( - uint vertex_id [[vertex_id]], - constant PathVertex_ScaledPixels *vertices - [[buffer(PathRasterizationInputIndex_Vertices)]], - constant Size_DevicePixels *atlas_size - [[buffer(PathRasterizationInputIndex_AtlasTextureSize)]]) { - PathVertex_ScaledPixels v = vertices[vertex_id]; + uint vertex_id [[vertex_id]], + constant PathRasterizationVertex *vertices [[buffer(PathRasterizationInputIndex_Vertices)]], + constant Size_DevicePixels *atlas_size [[buffer(PathRasterizationInputIndex_ViewportSize)]] +) { + PathRasterizationVertex v = vertices[vertex_id]; float2 vertex_position = float2(v.xy_position.x, v.xy_position.y); - float2 viewport_size = float2(atlas_size->width, atlas_size->height); + float4 position = float4( + vertex_position * float2(2. / atlas_size->width, -2. / atlas_size->height) + float2(-1., 1.), + 0., + 1. + ); return PathRasterizationVertexOutput{ - float4(vertex_position / viewport_size * float2(2., -2.) + - float2(-1., 1.), - 0., 1.), + position, float2(v.st_position.x, v.st_position.y), - {v.xy_position.x - v.content_mask.bounds.origin.x, - v.content_mask.bounds.origin.x + v.content_mask.bounds.size.width - - v.xy_position.x, - v.xy_position.y - v.content_mask.bounds.origin.y, - v.content_mask.bounds.origin.y + v.content_mask.bounds.size.height - - v.xy_position.y}}; + vertex_id, + { + v.xy_position.x - v.bounds.origin.x, + v.bounds.origin.x + v.bounds.size.width - v.xy_position.x, + v.xy_position.y - v.bounds.origin.y, + v.bounds.origin.y + v.bounds.size.height - v.xy_position.y + } + }; } -fragment float4 path_rasterization_fragment(PathRasterizationFragmentInput input - [[stage_in]]) { +fragment float4 path_rasterization_fragment( + PathRasterizationFragmentInput input [[stage_in]], + constant PathRasterizationVertex *vertices [[buffer(PathRasterizationInputIndex_Vertices)]] +) { float2 dx = dfdx(input.st_position); float2 dy = dfdy(input.st_position); - float2 gradient = float2((2. * input.st_position.x) * dx.x - dx.y, - (2. * input.st_position.x) * dy.x - dy.y); - float f = (input.st_position.x * input.st_position.x) - input.st_position.y; - float distance = f / length(gradient); - float alpha = saturate(0.5 - distance); - return float4(alpha, 0., 0., 1.); + + PathRasterizationVertex v = vertices[input.vertex_id]; + Background background = v.color; + Bounds_ScaledPixels path_bounds = v.bounds; + float alpha; + if (length(float2(dx.x, dy.x)) < 0.001) { + alpha = 1.0; + } else { + float2 gradient = float2( + (2. * input.st_position.x) * dx.x - dx.y, + (2. * input.st_position.x) * dy.x - dy.y + ); + float f = (input.st_position.x * input.st_position.x) - input.st_position.y; + float distance = f / length(gradient); + alpha = saturate(0.5 - distance); + } + + GradientColor gradient_color = prepare_fill_color( + background.tag, + background.color_space, + background.solid, + background.colors[0].color, + background.colors[1].color + ); + + float4 color = fill_color( + background, + input.position.xy, + path_bounds, + gradient_color.solid, + gradient_color.color0, + gradient_color.color1 + ); + return float4(color.rgb * color.a * alpha, alpha * color.a); } struct PathSpriteVertexOutput { float4 position [[position]]; - float2 tile_position; - uint sprite_id [[flat]]; - float4 solid_color [[flat]]; - float4 color0 [[flat]]; - float4 color1 [[flat]]; + float2 texture_coords; }; vertex PathSpriteVertexOutput path_sprite_vertex( - uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]], - constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]], - constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], - constant Size_DevicePixels *viewport_size - [[buffer(SpriteInputIndex_ViewportSize)]], - constant Size_DevicePixels *atlas_size - [[buffer(SpriteInputIndex_AtlasTextureSize)]]) { - + uint unit_vertex_id [[vertex_id]], + uint sprite_id [[instance_id]], + constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]], + constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], + constant Size_DevicePixels *viewport_size [[buffer(SpriteInputIndex_ViewportSize)]] +) { float2 unit_vertex = unit_vertices[unit_vertex_id]; PathSprite sprite = sprites[sprite_id]; // Don't apply content mask because it was already accounted for when // rasterizing the path. float4 device_position = to_device_position(unit_vertex, sprite.bounds, viewport_size); - float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size); - GradientColor gradient = prepare_fill_color( - sprite.color.tag, - sprite.color.color_space, - sprite.color.solid, - sprite.color.colors[0].color, - sprite.color.colors[1].color - ); + float2 screen_position = float2(sprite.bounds.origin.x, sprite.bounds.origin.y) + unit_vertex * float2(sprite.bounds.size.width, sprite.bounds.size.height); + float2 texture_coords = screen_position / float2(viewport_size->width, viewport_size->height); return PathSpriteVertexOutput{ device_position, - tile_position, - sprite_id, - gradient.solid, - gradient.color0, - gradient.color1 + texture_coords }; } fragment float4 path_sprite_fragment( - PathSpriteVertexOutput input [[stage_in]], - constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], - texture2d atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) { - constexpr sampler atlas_texture_sampler(mag_filter::linear, - min_filter::linear); - float4 sample = - atlas_texture.sample(atlas_texture_sampler, input.tile_position); - float mask = 1. - abs(1. - fmod(sample.r, 2.)); - PathSprite sprite = sprites[input.sprite_id]; - Background background = sprite.color; - float4 color = fill_color(background, input.position.xy, sprite.bounds, - input.solid_color, input.color0, input.color1); - color.a *= mask; - return color; + PathSpriteVertexOutput input [[stage_in]], + texture2d intermediate_texture [[texture(SpriteInputIndex_AtlasTexture)]] +) { + constexpr sampler intermediate_texture_sampler(mag_filter::linear, min_filter::linear); + return intermediate_texture.sample(intermediate_texture_sampler, input.texture_coords); } struct SurfaceVertexOutput { diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 1b88415d3b6f57f90643a54742f5312e9fa2ec97..e15bd7aeecec5932eb6386bd47d168eda906dd63 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -341,7 +341,7 @@ impl PlatformAtlas for TestAtlas { crate::AtlasTile { texture_id: AtlasTextureId { index: texture_id, - kind: crate::AtlasTextureKind::Path, + kind: crate::AtlasTextureKind::Monochrome, }, tile_id: TileId(tile_id), padding: 0, diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 4eaef64afa1d0d888d93dceca07569136edb0d8e..ec8d720cdfaf84a521d585b581c80a2dbe2ff6f7 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -43,17 +43,6 @@ impl Scene { self.surfaces.clear(); } - #[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) - )] - pub fn paths(&self) -> &[Path] { - &self.paths - } - pub fn len(&self) -> usize { self.paint_operations.len() } @@ -681,7 +670,7 @@ pub(crate) struct PathId(pub(crate) usize); #[derive(Clone, Debug)] pub struct Path { pub(crate) id: PathId, - order: DrawOrder, + pub(crate) order: DrawOrder, pub(crate) bounds: Bounds

, pub(crate) content_mask: ContentMask

, pub(crate) vertices: Vec>,