Render paths to a single fixed-size MSAA texture (#34992)

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>
This commit is contained in:
Max Brunsfeld 2025-07-25 14:39:24 -07:00 committed by GitHub
parent bf8e4272bc
commit 4d00d07df1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1134 additions and 799 deletions

View file

@ -128,6 +128,7 @@ mod macos {
"AtlasTile".into(),
"PathRasterizationInputIndex".into(),
"PathVertex_ScaledPixels".into(),
"PathRasterizationVertex".into(),
"ShadowInputIndex".into(),
"Shadow".into(),
"QuadInputIndex".into(),

View file

@ -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<Pixels>, Background)>,
background_quads: Vec<(Bounds<Pixels>, Background)>,
lines: Vec<Vec<Point<Pixels>>>,
start: Point<Pixels>,
dashed: bool,
@ -16,12 +17,148 @@ impl PaintingViewer {
fn new(_window: &mut Window, _cx: &mut Context<Self>) -> 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<Self>) -> 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);
});
}

View file

@ -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<Pixels>, Background)>,
_painting: bool,
}
impl PaintingViewer {
fn new(_window: &mut Window, _cx: &mut Context<Self>) -> 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<Self>) -> 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);
});
}

View file

@ -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)]

View file

@ -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<BladeAtlasState>);
struct PendingUpload {
@ -27,7 +25,6 @@ struct BladeAtlasState {
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
initializations: Vec<AtlasTextureId>,
uploads: Vec<PendingUpload>,
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<gpu::TextureView>,
}
impl BladeAtlas {
pub(crate) fn new(gpu: &Arc<gpu::Context>, path_sample_count: u32) -> Self {
pub(crate) fn new(gpu: &Arc<gpu::Context>) -> 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<DevicePixels>,
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<BladeAtlasTexture>,
polychrome_textures: AtlasTextureList<BladeAtlasTexture>,
path_textures: AtlasTextureList<BladeAtlasTexture>,
}
impl ops::Index<AtlasTextureKind> for BladeAtlasStorage {
@ -349,7 +272,6 @@ impl ops::Index<AtlasTextureKind> 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<AtlasTextureKind> 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<AtlasTextureId> 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<gpu::Texture>,
msaa_view: Option<gpu::TextureView>,
format: gpu::TextureFormat,
live_atlas_keys: u32,
}
impl BladeAtlasTexture {
fn clear(&mut self) {
self.allocator.clear();
}
fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
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 {

View file

@ -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<ScaledPixels>,
}
#[derive(Clone, Debug)]
#[repr(C)]
struct PathRasterizationVertex {
xy_position: Point<ScaledPixels>,
st_position: Point<f32>,
color: Background,
tile: AtlasTile,
bounds: Bounds<ScaledPixels>,
}
struct BladePipelines {
@ -144,10 +146,7 @@ impl BladePipelines {
shader.check_struct_size::<SurfaceParams>();
shader.check_struct_size::<Quad>();
shader.check_struct_size::<Shadow>();
assert_eq!(
mem::size_of::<PathVertex<ScaledPixels>>(),
shader.get_struct_size("PathVertex") as usize,
);
shader.check_struct_size::<PathRasterizationVertex>();
shader.check_struct_size::<PathSprite>();
shader.check_struct_size::<Underline>();
shader.check_struct_size::<MonochromeSprite>();
@ -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<gpu::SyncPoint>,
pipelines: BladePipelines,
instance_belt: BufferBelt,
path_tiles: HashMap<PathId, AtlasTile>,
atlas: Arc<BladeAtlas>,
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<gpu::Texture>,
path_intermediate_msaa_texture_view: Option<gpu::TextureView>,
}
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<ScaledPixels>]) {
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<ScaledPixels>],
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 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,
},
) {
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 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) };
encoder.bind(
0,
&ShaderPathRasterizationData {
globals,
b_path_vertices: vertex_buf,
},
);
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);
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;
};
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;
#[cfg(target_os = "macos")]
{
let (t_y, t_cb_cr) = unsafe {
use core_foundation::base::TCFType as _;
use std::ptr;
assert_eq!(
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,
>,
)
.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,
>,
)
.unwrap(),
gpu::TexelAspects::COLOR,
),
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,
>,
)
.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,
>,
)
.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.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))
}

View file

@ -924,16 +924,19 @@ fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> {
// --- path rasterization --- //
struct PathVertex {
struct PathRasterizationVertex {
xy_position: vec2<f32>,
st_position: vec2<f32>,
content_mask: Bounds,
color: Background,
bounds: Bounds,
}
var<storage, read> b_path_vertices: array<PathVertex>;
var<storage, read> b_path_vertices: array<PathRasterizationVertex>;
struct PathRasterizationVarying {
@builtin(position) position: vec4<f32>,
@location(0) st_position: vec2<f32>,
@location(1) vertex_id: u32,
//TODO: use `clip_distance` once Naga supports it
@location(3) clip_distances: vec4<f32>,
}
@ -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<f32> {
let dx = dpdx(input.st_position);
let dy = dpdy(input.st_position);
if (any(input.clip_distances < vec4<f32>(0.0))) {
return 0.0;
return vec4<f32>(0.0);
}
let gradient = 2.0 * input.st_position.xx * vec2<f32>(dx.x, dy.x) - vec2<f32>(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<f32>(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<f32>(dx.x, dy.x) - vec2<f32>(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<f32>(color.rgb * color.a * alpha, color.a * alpha);
}
// --- paths --- //
struct PathSprite {
bounds: Bounds,
color: Background,
tile: AtlasTile,
}
var<storage, read> b_path_sprites: array<PathSprite>;
struct PathVarying {
@builtin(position) position: vec4<f32>,
@location(0) tile_position: vec2<f32>,
@location(1) @interpolate(flat) instance_id: u32,
@location(2) @interpolate(flat) color_solid: vec4<f32>,
@location(3) @interpolate(flat) color0: vec4<f32>,
@location(4) @interpolate(flat) color1: vec4<f32>,
@location(0) texture_coords: vec2<f32>,
}
@vertex
@ -986,33 +1003,22 @@ fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
let unit_vertex = vec2<f32>(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<f32> {
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 --- //

View file

@ -13,53 +13,25 @@ use std::borrow::Cow;
pub(crate) struct MetalAtlas(Mutex<MetalAtlasState>);
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<metal::Texture> {
self.0.lock().texture(id).msaa_texture.clone()
}
pub(crate) fn allocate(
&self,
size: Size<DevicePixels>,
texture_kind: AtlasTextureKind,
) -> Option<AtlasTile> {
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<Device>,
monochrome_textures: AtlasTextureList<MetalAtlasTexture>,
polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
path_textures: AtlasTextureList<MetalAtlasTexture>,
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
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<metal::Texture>,
msaa_texture: AssertSend<Option<metal::Texture>>,
live_atlas_keys: u32,
}
impl MetalAtlasTexture {
fn clear(&mut self) {
self.allocator.clear();
}
fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
let allocation = self.allocator.allocate(size.into())?;
let tile = AtlasTile {

View file

@ -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<Mutex<InstanceBufferPool>>,
sprite_atlas: Arc<MetalAtlas>,
core_video_texture_cache: core_video::metal_texture_cache::CVMetalTextureCache,
path_intermediate_texture: Option<metal::Texture>,
path_intermediate_msaa_texture: Option<metal::Texture>,
path_sample_count: u32,
}
#[repr(C)]
pub struct PathRasterizationVertex {
pub xy_position: Point<ScaledPixels>,
pub st_position: Point<f32>,
pub color: Background,
pub bounds: Bounds<ScaledPixels>,
}
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<DevicePixels>) {
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<metal::CommandBuffer> {
let command_queue = self.command_queue.clone();
let command_buffer = command_queue.new_command_buffer();
let alpha = if self.layer.is_opaque() { 1. } else { 0. };
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);
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<ScaledPixels>],
instance_buffer: &mut InstanceBuffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_buffer: &metal::CommandBufferRef,
) -> Option<HashMap<PathId, AtlasTile>> {
self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
) -> bool {
if paths.is_empty() {
return true;
}
let Some(intermediate_texture) = &self.path_intermediate_texture else {
return false;
};
let mut tiles = HashMap::default();
let mut vertices_by_texture_id = HashMap::default();
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.));
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 command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state);
align_offset(instance_offset);
let mut vertices = Vec::new();
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);
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),
}));
}
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 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<DevicePixels> as *const _,
);
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,
);
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<DevicePixels> 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<ScaledPixels>],
tiles_by_path_id: &HashMap<PathId, AtlasTile>,
instance_buffer: &mut InstanceBuffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
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<DevicePixels> 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;
}
}
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<DevicePixels> 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));
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 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,
);
}
command_encoder.draw_primitives_instanced(
metal::MTLPrimitiveType::Triangle,
0,
6,
sprites.len() as u64,
);
*instance_offset = next_offset;
sprites.clear();
// 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 }];
}
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;
}
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) };
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;
true
}
@ -1136,6 +1147,33 @@ impl MetalRenderer {
}
}
fn new_command_encoder<'a>(
command_buffer: &'a metal::CommandBufferRef,
drawable: &'a metal::MetalDrawableRef,
viewport_size: Size<DevicePixels>,
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<ScaledPixels>,
pub color: Background,
pub tile: AtlasTile,
}
#[derive(Clone, Debug, Eq, PartialEq)]

View file

@ -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<float> 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<float> 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 {

View file

@ -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,

View file

@ -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<ScaledPixels>] {
&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<P: Clone + Debug + Default + PartialEq> {
pub(crate) id: PathId,
order: DrawOrder,
pub(crate) order: DrawOrder,
pub(crate) bounds: Bounds<P>,
pub(crate) content_mask: ContentMask<P>,
pub(crate) vertices: Vec<PathVertex<P>>,