1153 lines
43 KiB
Rust
1153 lines
43 KiB
Rust
use std::collections::HashMap;
|
|
use std::ffi::c_void;
|
|
use std::fs::File;
|
|
use std::io::Write;
|
|
use std::mem;
|
|
use std::ptr::NonNull;
|
|
use std::sync::Once;
|
|
|
|
use dispatch2::DispatchData;
|
|
use galaxyui_core::fonts::{self, canvas, RasterizedGlyph, SubpixelAlignment};
|
|
use galaxyui_core::platform::CapturedFrame;
|
|
use galaxyui_core::rendering::texture_cache::TextureCache;
|
|
use galaxyui_core::rendering::{self};
|
|
use galaxyui_core::scene::{CornerRadius, GlyphFade, GlyphKey, Icon, Image, Layer, Scene};
|
|
use objc2::rc::Retained;
|
|
use objc2::runtime::ProtocolObject;
|
|
use objc2_foundation::NSString;
|
|
use objc2_metal::{
|
|
MTLBlendFactor, MTLBlendOperation, MTLBuffer, MTLClearColor, MTLCommandBuffer,
|
|
MTLCommandEncoder, MTLCommandQueue, MTLDevice, MTLDrawable, MTLFunction, MTLIndexType,
|
|
MTLLibrary, MTLLoadAction, MTLOrigin, MTLPixelFormat, MTLPrimitiveType, MTLRegion,
|
|
MTLRenderCommandEncoder, MTLRenderPassDescriptor, MTLRenderPipelineDescriptor,
|
|
MTLRenderPipelineState, MTLResourceOptions, MTLScissorRect, MTLSize, MTLStoreAction,
|
|
MTLTexture, MTLTextureDescriptor, MTLViewport,
|
|
};
|
|
use objc2_quartz_core::CAMetalDrawable;
|
|
use pathfinder_color::{ColorF, ColorU};
|
|
use pathfinder_geometry::rect::{RectF, RectI};
|
|
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
|
|
|
use super::frame_capture::capture_frame;
|
|
use crate::platform::mac::rendering::renderer::Device;
|
|
use crate::platform::mac::window::WindowState;
|
|
use crate::rendering::atlas::{AllocatedRegion, TextureId};
|
|
use crate::rendering::{get_best_dash_gap, GlyphCache, GlyphRasterBoundsFn, RasterizeGlyphFn};
|
|
|
|
const METAL_LIB_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
|
|
static WRITE_LIB_TO_FILE: Once = Once::new();
|
|
|
|
/// A structure to help manage a single rendering pass.
|
|
struct RenderPass<'a> {
|
|
drawable: &'a ProtocolObject<dyn CAMetalDrawable>,
|
|
buffer: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
|
|
encoder: Retained<ProtocolObject<dyn MTLRenderCommandEncoder>>,
|
|
encoding_finished: bool,
|
|
}
|
|
|
|
impl<'a> RenderPass<'a> {
|
|
fn new(
|
|
command_queue: &ProtocolObject<dyn MTLCommandQueue>,
|
|
drawable: &'a ProtocolObject<dyn CAMetalDrawable>,
|
|
) -> Self {
|
|
let buffer = command_queue
|
|
.commandBuffer()
|
|
.expect("command queue should always vend a command buffer");
|
|
let encoder = buffer
|
|
.renderCommandEncoderWithDescriptor(&Self::create_descriptor(drawable))
|
|
.expect("command buffer should always vend a render command encoder");
|
|
Self {
|
|
drawable,
|
|
buffer,
|
|
encoder,
|
|
encoding_finished: false,
|
|
}
|
|
}
|
|
|
|
/// Finishes a render pass with optional frame capture.
|
|
///
|
|
/// If this is not called, the encoded commands will not be executed and the
|
|
/// drawable will not be updated.
|
|
///
|
|
/// Returns the captured frame data if capture was requested.
|
|
fn finish_with_capture(
|
|
mut self,
|
|
drawable_size: pathfinder_geometry::vector::Vector2F,
|
|
should_capture: bool,
|
|
presents_with_transaction: bool,
|
|
) -> Option<CapturedFrame> {
|
|
self.encoder.endEncoding();
|
|
self.encoding_finished = true;
|
|
|
|
// If we're able to do asynchronous presentation, do so - it allows us to avoid
|
|
// blocking on the GPU for the duration of the frame.
|
|
if !should_capture && !presents_with_transaction {
|
|
self.buffer
|
|
.presentDrawable(ProtocolObject::from_ref(self.drawable));
|
|
self.buffer.commit();
|
|
return None;
|
|
}
|
|
|
|
// Otherwise, commit the buffer and wait for it to complete before continuing.
|
|
self.buffer.commit();
|
|
self.buffer.waitUntilCompleted();
|
|
|
|
let capture = if should_capture {
|
|
let texture = self.drawable.texture();
|
|
capture_frame(&texture, drawable_size)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
self.drawable.present();
|
|
capture
|
|
}
|
|
|
|
/// Creates a descriptor for a pass that renders into the provided drawable.
|
|
fn create_descriptor(
|
|
drawable: &ProtocolObject<dyn CAMetalDrawable>,
|
|
) -> Retained<MTLRenderPassDescriptor> {
|
|
let descriptor = MTLRenderPassDescriptor::new();
|
|
|
|
// SAFETY: index 0 is always a valid color attachment slot for a CAMetalLayer's drawable.
|
|
let color_attachment = unsafe { descriptor.colorAttachments().objectAtIndexedSubscript(0) };
|
|
color_attachment.setTexture(Some(&drawable.texture()));
|
|
color_attachment.setLoadAction(MTLLoadAction::Clear);
|
|
color_attachment.setStoreAction(MTLStoreAction::Store);
|
|
color_attachment.setClearColor(MTLClearColor {
|
|
red: 0.,
|
|
green: 0.,
|
|
blue: 0.,
|
|
alpha: 0.,
|
|
});
|
|
|
|
descriptor
|
|
}
|
|
}
|
|
|
|
impl Drop for RenderPass<'_> {
|
|
fn drop(&mut self) {
|
|
// Make sure that `end_encoding()` is called, even if a panic occurs
|
|
// during rendering.
|
|
if !self.encoding_finished {
|
|
self.encoder.endEncoding();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A set of resources necessary for rendering that retain state across frames.
|
|
struct Resources {
|
|
draw_rects_pipeline_state: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
|
|
draw_images_pipeline_state: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
|
|
draw_glyphs_pipeline_state: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
|
|
quad_vertices: Retained<ProtocolObject<dyn MTLBuffer>>,
|
|
quad_indices: Retained<ProtocolObject<dyn MTLBuffer>>,
|
|
glyph_cache: GlyphCache<Retained<ProtocolObject<dyn MTLTexture>>>,
|
|
texture_cache: TextureCache<Retained<ProtocolObject<dyn MTLTexture>>>,
|
|
}
|
|
|
|
/// A structure that manages rendering scenes using a particular hardware
|
|
/// device.
|
|
pub struct Renderer {
|
|
resources: Resources,
|
|
command_queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
|
|
}
|
|
|
|
impl Renderer {
|
|
pub fn new(
|
|
device: &ProtocolObject<dyn MTLDevice>,
|
|
color_pixel_format: MTLPixelFormat,
|
|
glyph_config: rendering::GlyphConfig,
|
|
) -> Self {
|
|
let library = if cfg!(feature = "enable-metal-frame-capture") {
|
|
let temp_lib_path = std::env::temp_dir().join("shaders.metallib");
|
|
WRITE_LIB_TO_FILE.call_once(|| {
|
|
let mut file = File::create(&temp_lib_path).unwrap();
|
|
file.write_all(METAL_LIB_BYTES).unwrap();
|
|
});
|
|
let path = NSString::from_str(temp_lib_path.to_str().unwrap());
|
|
// `newLibraryWithURL:` is the non-deprecated replacement, but we
|
|
// load the shader library from a file path here.
|
|
#[allow(deprecated)]
|
|
let library = device.newLibraryWithFile_error(&path).unwrap();
|
|
library
|
|
} else {
|
|
let data = DispatchData::from_static_bytes(METAL_LIB_BYTES);
|
|
device.newLibraryWithData_error(&data).unwrap()
|
|
};
|
|
|
|
let rect_vertex_shader = library
|
|
.newFunctionWithName(&NSString::from_str("rect_vertex_shader"))
|
|
.unwrap();
|
|
let rect_fragment_shader = library
|
|
.newFunctionWithName(&NSString::from_str("rect_fragment_shader"))
|
|
.unwrap();
|
|
let rect_pipeline = Self::create_pipeline(
|
|
"Rects",
|
|
color_pixel_format,
|
|
&rect_vertex_shader,
|
|
&rect_fragment_shader,
|
|
);
|
|
let draw_rects_pipeline_state = device
|
|
.newRenderPipelineStateWithDescriptor_error(&rect_pipeline)
|
|
.unwrap();
|
|
|
|
let image_fragment_shader = library
|
|
.newFunctionWithName(&NSString::from_str("image_fragment_shader"))
|
|
.unwrap();
|
|
let image_pipeline = Self::create_pipeline(
|
|
"Images",
|
|
color_pixel_format,
|
|
&rect_vertex_shader,
|
|
&image_fragment_shader,
|
|
);
|
|
let draw_images_pipeline_state = device
|
|
.newRenderPipelineStateWithDescriptor_error(&image_pipeline)
|
|
.unwrap();
|
|
|
|
let glyph_vertex_shader = library
|
|
.newFunctionWithName(&NSString::from_str("glyph_vertex_shader"))
|
|
.unwrap();
|
|
let glyph_fragment_shader = library
|
|
.newFunctionWithName(&NSString::from_str("glyph_fragment_shader"))
|
|
.unwrap();
|
|
let glyph_pipeline = Self::create_pipeline(
|
|
"Glyphs",
|
|
color_pixel_format,
|
|
&glyph_vertex_shader,
|
|
&glyph_fragment_shader,
|
|
);
|
|
let draw_glyphs_pipeline_state = device
|
|
.newRenderPipelineStateWithDescriptor_error(&glyph_pipeline)
|
|
.unwrap();
|
|
|
|
let quad_vertices = new_metal_buffer(
|
|
device,
|
|
&[
|
|
shader::Vector2F::new(0., 0.),
|
|
shader::Vector2F::new(1., 0.),
|
|
shader::Vector2F::new(0., 1.),
|
|
shader::Vector2F::new(1., 1.),
|
|
],
|
|
MTLResourceOptions::StorageModeManaged,
|
|
);
|
|
|
|
let quad_indices = new_metal_buffer(
|
|
device,
|
|
&[0_u16, 1, 2, 2, 3, 1],
|
|
MTLResourceOptions::StorageModeManaged,
|
|
);
|
|
|
|
let glyph_cache = GlyphCache::new(glyph_config);
|
|
|
|
Self {
|
|
resources: Resources {
|
|
draw_rects_pipeline_state,
|
|
draw_images_pipeline_state,
|
|
draw_glyphs_pipeline_state,
|
|
quad_vertices,
|
|
quad_indices,
|
|
glyph_cache,
|
|
texture_cache: TextureCache::new(),
|
|
},
|
|
command_queue: device
|
|
.newCommandQueue()
|
|
.expect("device should always vend a command queue"),
|
|
}
|
|
}
|
|
|
|
fn create_pipeline(
|
|
label: &str,
|
|
color_pixel_format: MTLPixelFormat,
|
|
vertex_shader: &ProtocolObject<dyn MTLFunction>,
|
|
fragment_shader: &ProtocolObject<dyn MTLFunction>,
|
|
) -> Retained<MTLRenderPipelineDescriptor> {
|
|
let pipeline = MTLRenderPipelineDescriptor::new();
|
|
pipeline.setLabel(Some(&NSString::from_str(label)));
|
|
pipeline.setVertexFunction(Some(vertex_shader));
|
|
pipeline.setFragmentFunction(Some(fragment_shader));
|
|
|
|
// SAFETY: index 0 is always a valid color attachment slot for a render pipeline.
|
|
let attachment = unsafe { pipeline.colorAttachments().objectAtIndexedSubscript(0) };
|
|
attachment.setPixelFormat(color_pixel_format);
|
|
attachment.setBlendingEnabled(true);
|
|
attachment.setRgbBlendOperation(MTLBlendOperation::Add);
|
|
attachment.setAlphaBlendOperation(MTLBlendOperation::Add);
|
|
attachment.setSourceRGBBlendFactor(MTLBlendFactor::SourceAlpha);
|
|
attachment.setSourceAlphaBlendFactor(MTLBlendFactor::One);
|
|
attachment.setDestinationRGBBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
|
|
attachment.setDestinationAlphaBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
|
|
|
|
pipeline
|
|
}
|
|
|
|
fn render(
|
|
&mut self,
|
|
scene: &Scene,
|
|
ctx: &MetalDrawContext,
|
|
should_capture: bool,
|
|
presents_with_transaction: bool,
|
|
) -> Option<CapturedFrame> {
|
|
self.resources
|
|
.glyph_cache
|
|
.update_config(&scene.rendering_config().glyphs);
|
|
|
|
let render_pass = RenderPass::new(&self.command_queue, ctx.drawable);
|
|
|
|
Frame::new(scene, &render_pass.encoder, &mut self.resources, ctx).draw();
|
|
|
|
render_pass.finish_with_capture(
|
|
ctx.drawable_size,
|
|
should_capture,
|
|
presents_with_transaction,
|
|
)
|
|
}
|
|
}
|
|
|
|
/// A struct that manages rendering a single frame: the encoding of a scene into
|
|
/// a set of GPU draw calls to rasterize the scene description into a bitmap
|
|
/// image.
|
|
pub struct Frame<'a> {
|
|
scene: &'a Scene,
|
|
command_encoder: &'a ProtocolObject<dyn MTLRenderCommandEncoder>,
|
|
resources: &'a mut Resources,
|
|
ctx: &'a MetalDrawContext<'a>,
|
|
}
|
|
|
|
impl<'a> Frame<'a> {
|
|
fn new(
|
|
scene: &'a Scene,
|
|
command_encoder: &'a ProtocolObject<dyn MTLRenderCommandEncoder>,
|
|
resources: &'a mut Resources,
|
|
ctx: &'a MetalDrawContext<'a>,
|
|
) -> Self {
|
|
Self {
|
|
scene,
|
|
resources,
|
|
command_encoder,
|
|
ctx,
|
|
}
|
|
}
|
|
|
|
fn draw(&mut self) {
|
|
self.command_encoder.setViewport(MTLViewport {
|
|
originX: 0.0,
|
|
originY: 0.0,
|
|
width: self.ctx.drawable_size.x() as f64,
|
|
height: self.ctx.drawable_size.y() as f64,
|
|
znear: 0.0,
|
|
zfar: 1.0,
|
|
});
|
|
|
|
for layer in self.scene.layers() {
|
|
if let Some(bounds) = layer.clip_bounds {
|
|
// Make sure the scissor rect doesn't extend beyond the boundaries
|
|
// of the window, as required by the Metal API.
|
|
// API docs: https://developer.apple.com/documentation/metal/mtlrendercommandencoder/1515583-setscissorrect?language=objc
|
|
// Scissor test background reading: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/By_example/Basic_scissoring
|
|
let device_bounds = RectF::new(Vector2F::zero(), self.ctx.drawable_size);
|
|
let bounds = (bounds * self.scene.scale_factor()).intersection(device_bounds);
|
|
if let Some(intersection) = bounds {
|
|
self.command_encoder.setScissorRect(MTLScissorRect {
|
|
x: intersection.origin_x().round() as usize,
|
|
y: intersection.origin_y().round() as usize,
|
|
width: intersection.width().round() as usize,
|
|
height: intersection.height().round() as usize,
|
|
});
|
|
} else {
|
|
// The layer's clip bounds don't intersect the window bounds
|
|
// at all; we can skip drawing anything in this layer.
|
|
continue;
|
|
}
|
|
} else {
|
|
self.command_encoder.setScissorRect(MTLScissorRect {
|
|
x: 0_usize,
|
|
y: 0_usize,
|
|
width: self.ctx.drawable_size.x() as usize,
|
|
height: self.ctx.drawable_size.y() as usize,
|
|
});
|
|
}
|
|
self.draw_rects(layer);
|
|
self.draw_images(layer);
|
|
self.draw_glyphs(layer);
|
|
}
|
|
}
|
|
|
|
// Utility function to render image or icon in Metal.
|
|
fn render_image_or_icon(&mut self, image: Option<&Image>, icon: Option<&Icon>) {
|
|
let opacity;
|
|
let bounds;
|
|
let asset;
|
|
let is_icon;
|
|
let icon_color;
|
|
let ui_corner_radius;
|
|
|
|
if let Some(to_render) = image {
|
|
opacity = to_render.opacity;
|
|
bounds = to_render.bounds;
|
|
asset = &to_render.asset;
|
|
is_icon = false;
|
|
icon_color = ColorF::new(0.0, 0.0, 0.0, opacity).into();
|
|
ui_corner_radius = to_render.corner_radius;
|
|
} else {
|
|
let to_render = icon.unwrap();
|
|
opacity = to_render.opacity;
|
|
bounds = to_render.bounds;
|
|
asset = &to_render.asset;
|
|
is_icon = true;
|
|
icon_color = to_render.color.to_f32().into();
|
|
ui_corner_radius = CornerRadius::default();
|
|
}
|
|
|
|
let mut per_rect_uniforms = Vec::new();
|
|
let scale_factor = self.scene.scale_factor();
|
|
let bounds = bounds * scale_factor;
|
|
let min_dimension = f32::min(bounds.height(), bounds.width());
|
|
let corner_radius = crate::rendering::CornerRadius::from_ui_corner_radius(
|
|
ui_corner_radius,
|
|
scale_factor,
|
|
min_dimension,
|
|
);
|
|
per_rect_uniforms.push(shader::PerRectUniforms::new(
|
|
bounds.origin().into(),
|
|
bounds.size().into(),
|
|
corner_radius,
|
|
0.,
|
|
0.,
|
|
0.,
|
|
0.,
|
|
vec2f(0.0, 0.0).into(),
|
|
vec2f(1.0, 0.0).into(),
|
|
ColorF::new(0.0, 0.0, 0.0, opacity).into(),
|
|
ColorF::new(0.0, 0.0, 0.0, opacity).into(),
|
|
vec2f(0.0, 0.0).into(),
|
|
vec2f(1.0, 0.0).into(),
|
|
ColorU::transparent_black().to_f32().into(),
|
|
ColorU::transparent_black().to_f32().into(),
|
|
is_icon,
|
|
icon_color,
|
|
Vector2F::zero().into(),
|
|
ColorU::transparent_black().to_f32().into(),
|
|
0_f32,
|
|
0_f32,
|
|
0.,
|
|
vec2f(0.0, 0.0).into(),
|
|
));
|
|
let per_rect_uniforms_buffer = new_metal_buffer(
|
|
self.ctx.device,
|
|
&per_rect_uniforms,
|
|
MTLResourceOptions::StorageModeManaged,
|
|
);
|
|
|
|
let uniforms = shader::Uniforms::new(self.ctx.drawable_size.into());
|
|
let uniforms_ptr = NonNull::from(&uniforms).cast::<c_void>();
|
|
let uniforms_len = mem::size_of::<shader::Uniforms>();
|
|
|
|
// SAFETY: the per-rect uniform buffer and `uniforms` value outlive this encoded draw
|
|
// call, and the bound buffer/byte sizes and indices match the shader bindings.
|
|
unsafe {
|
|
self.command_encoder.setVertexBuffer_offset_atIndex(
|
|
Some(&per_rect_uniforms_buffer),
|
|
0,
|
|
1,
|
|
);
|
|
self.command_encoder
|
|
.setVertexBytes_length_atIndex(uniforms_ptr, uniforms_len, 2);
|
|
self.command_encoder
|
|
.setFragmentBytes_length_atIndex(uniforms_ptr, uniforms_len, 0);
|
|
}
|
|
|
|
let (_, texture) = self
|
|
.resources
|
|
.texture_cache
|
|
.get_or_insert_by_asset(asset, |asset| {
|
|
let width = asset.size().x() as usize;
|
|
let height = asset.size().y() as usize;
|
|
|
|
let texture_descriptor = MTLTextureDescriptor::new();
|
|
texture_descriptor.setPixelFormat(MTLPixelFormat::RGBA8Unorm);
|
|
// SAFETY: width/height come from a decoded asset and are within Metal limits.
|
|
unsafe {
|
|
texture_descriptor.setWidth(width);
|
|
texture_descriptor.setHeight(height);
|
|
}
|
|
let texture = self
|
|
.ctx
|
|
.device
|
|
.newTextureWithDescriptor(&texture_descriptor)
|
|
.expect("device should create an RGBA8 texture");
|
|
let region = MTLRegion {
|
|
origin: MTLOrigin { x: 0, y: 0, z: 0 },
|
|
size: MTLSize {
|
|
width,
|
|
height,
|
|
depth: 1,
|
|
},
|
|
};
|
|
|
|
let bytes_per_row: usize = 4 * width;
|
|
// SAFETY: rgba_bytes holds width*height*4 bytes laid out to match the region
|
|
// and row stride.
|
|
unsafe {
|
|
texture.replaceRegion_mipmapLevel_withBytes_bytesPerRow(
|
|
region,
|
|
0,
|
|
NonNull::new(asset.rgba_bytes().as_ptr() as *mut c_void)
|
|
.expect("asset rgba bytes pointer is non-null"),
|
|
bytes_per_row,
|
|
);
|
|
}
|
|
|
|
texture
|
|
});
|
|
|
|
// SAFETY: the bound texture and quad index buffer outlive this encoded draw call.
|
|
unsafe {
|
|
self.command_encoder
|
|
.setFragmentTexture_atIndex(Some(&**texture), 0);
|
|
|
|
self.command_encoder
|
|
.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset_instanceCount(
|
|
MTLPrimitiveType::Triangle,
|
|
6,
|
|
MTLIndexType::UInt16,
|
|
&self.resources.quad_indices,
|
|
0,
|
|
per_rect_uniforms.len(),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn draw_images(&mut self, layer: &Layer) {
|
|
if layer.images.is_empty() && layer.icons.is_empty() {
|
|
// It's a mac assertion error to create an empty metal buffer, so exit early
|
|
return;
|
|
}
|
|
|
|
self.command_encoder
|
|
.setRenderPipelineState(&self.resources.draw_images_pipeline_state);
|
|
// SAFETY: index 0 binds the shared quad vertex buffer, which outlives the draw calls.
|
|
unsafe {
|
|
self.command_encoder.setVertexBuffer_offset_atIndex(
|
|
Some(&self.resources.quad_vertices),
|
|
0,
|
|
0,
|
|
);
|
|
}
|
|
|
|
for image in &layer.images {
|
|
self.render_image_or_icon(Some(image), None);
|
|
}
|
|
|
|
// Another iteration for rendering icons.
|
|
for icon in &layer.icons {
|
|
self.render_image_or_icon(None, Some(icon));
|
|
}
|
|
}
|
|
|
|
fn draw_rects(&self, layer: &Layer) {
|
|
if layer.rects.is_empty() {
|
|
// It's a mac assertion error to create an empty metal buffer, so exit early
|
|
return;
|
|
}
|
|
|
|
self.command_encoder
|
|
.setRenderPipelineState(&self.resources.draw_rects_pipeline_state);
|
|
// SAFETY: index 0 binds the shared quad vertex buffer, which outlives the draw call.
|
|
unsafe {
|
|
self.command_encoder.setVertexBuffer_offset_atIndex(
|
|
Some(&self.resources.quad_vertices),
|
|
0,
|
|
0,
|
|
);
|
|
}
|
|
|
|
let mut per_rect_uniforms = Vec::new();
|
|
for rect in &layer.rects {
|
|
let scale_factor = self.scene.scale_factor();
|
|
let bounds = rect.bounds * scale_factor;
|
|
|
|
let dash = rect
|
|
.border
|
|
.dash
|
|
.map(|mut dash| {
|
|
dash.dash_length *= scale_factor;
|
|
dash.gap_length *= scale_factor;
|
|
dash
|
|
})
|
|
.unwrap_or_default();
|
|
let horizontal_gap = get_best_dash_gap(bounds.width(), dash);
|
|
let vertical_gap = get_best_dash_gap(bounds.height(), dash);
|
|
let dash_length = dash.dash_length;
|
|
let gap_lengths = Vector2F::new(horizontal_gap, vertical_gap);
|
|
|
|
if let Some(drop_shadow) = rect.drop_shadow {
|
|
let sigma = drop_shadow.blur_radius;
|
|
let padding = drop_shadow.spread_radius * self.scene.scale_factor();
|
|
let shadow_origin =
|
|
bounds.origin() + drop_shadow.offset * self.scene.scale_factor() - padding;
|
|
let shadow_size = bounds.size() + vec2f(2. * padding, 2. * padding);
|
|
|
|
let min_dimension = f32::min(shadow_size.x(), shadow_size.y());
|
|
|
|
let corner_radius = crate::rendering::CornerRadius::from_ui_corner_radius(
|
|
rect.corner_radius,
|
|
scale_factor,
|
|
min_dimension,
|
|
);
|
|
|
|
// For the drop shadow case, we pass in a rect with the bounds
|
|
// of the shadow and render that before rendering the actual rect.
|
|
per_rect_uniforms.push(shader::PerRectUniforms::new(
|
|
shadow_origin.into(),
|
|
shadow_size.into(),
|
|
corner_radius,
|
|
0_f32,
|
|
0_f32,
|
|
0_f32,
|
|
0_f32,
|
|
Vector2F::zero().into(),
|
|
Vector2F::zero().into(),
|
|
ColorU::transparent_black().to_f32().into(),
|
|
ColorU::transparent_black().to_f32().into(),
|
|
Vector2F::zero().into(),
|
|
Vector2F::zero().into(),
|
|
ColorU::transparent_black().to_f32().into(),
|
|
ColorU::transparent_black().to_f32().into(),
|
|
false,
|
|
ColorU::transparent_black().to_f32().into(),
|
|
(drop_shadow.offset * self.scene.scale_factor()).into(),
|
|
drop_shadow.color.to_f32().into(),
|
|
sigma * self.scene.scale_factor(),
|
|
padding,
|
|
dash_length,
|
|
gap_lengths.into(),
|
|
));
|
|
}
|
|
|
|
let min_dimension = f32::min(bounds.height(), bounds.width());
|
|
let corner_radius = crate::rendering::CornerRadius::from_ui_corner_radius(
|
|
rect.corner_radius,
|
|
scale_factor,
|
|
min_dimension,
|
|
);
|
|
|
|
per_rect_uniforms.push(shader::PerRectUniforms::new(
|
|
bounds.origin().into(),
|
|
bounds.size().into(),
|
|
corner_radius,
|
|
rect.border.top_width() * scale_factor,
|
|
rect.border.right_width() * scale_factor,
|
|
rect.border.bottom_width() * scale_factor,
|
|
rect.border.left_width() * scale_factor,
|
|
rect.background.start().into(),
|
|
rect.background.end().into(),
|
|
rect.background.start_color().to_f32().into(),
|
|
rect.background.end_color().to_f32().into(),
|
|
rect.border.color.start().into(),
|
|
rect.border.color.end().into(),
|
|
rect.border.color.start_color().to_f32().into(),
|
|
rect.border.color.end_color().to_f32().into(),
|
|
false,
|
|
ColorU::transparent_black().to_f32().into(),
|
|
Vector2F::zero().into(),
|
|
ColorU::transparent_black().to_f32().into(),
|
|
0_f32,
|
|
0_f32,
|
|
dash_length,
|
|
gap_lengths.into(),
|
|
));
|
|
}
|
|
let per_rect_uniforms_buffer = new_metal_buffer(
|
|
self.ctx.device,
|
|
&per_rect_uniforms,
|
|
MTLResourceOptions::StorageModeManaged,
|
|
);
|
|
|
|
let uniforms = shader::Uniforms::new(self.ctx.drawable_size.into());
|
|
let uniforms_ptr = NonNull::from(&uniforms).cast::<c_void>();
|
|
let uniforms_len = mem::size_of::<shader::Uniforms>();
|
|
|
|
// SAFETY: the per-rect uniform buffer and `uniforms` value outlive this encoded draw
|
|
// call, and the bound buffer/byte sizes and indices match the shader bindings.
|
|
unsafe {
|
|
self.command_encoder.setVertexBuffer_offset_atIndex(
|
|
Some(&per_rect_uniforms_buffer),
|
|
0,
|
|
1,
|
|
);
|
|
self.command_encoder
|
|
.setVertexBytes_length_atIndex(uniforms_ptr, uniforms_len, 2);
|
|
self.command_encoder
|
|
.setFragmentBytes_length_atIndex(uniforms_ptr, uniforms_len, 0);
|
|
|
|
self.command_encoder
|
|
.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset_instanceCount(
|
|
MTLPrimitiveType::Triangle,
|
|
6,
|
|
MTLIndexType::UInt16,
|
|
&self.resources.quad_indices,
|
|
0,
|
|
per_rect_uniforms.len(),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn draw_glyphs(&mut self, layer: &Layer) {
|
|
if layer.glyphs.is_empty() {
|
|
// It's a mac assertion error to create an empty metal buffer, so exit early
|
|
return;
|
|
}
|
|
|
|
self.command_encoder
|
|
.setRenderPipelineState(&self.resources.draw_glyphs_pipeline_state);
|
|
// SAFETY: index 0 binds the shared quad vertex buffer, which outlives the draw calls.
|
|
unsafe {
|
|
self.command_encoder.setVertexBuffer_offset_atIndex(
|
|
Some(&self.resources.quad_vertices),
|
|
0,
|
|
0,
|
|
);
|
|
}
|
|
|
|
let scale_factor = self.scene.scale_factor();
|
|
|
|
let mut texture_to_glyph: HashMap<TextureId, Vec<shader::PerGlyphUniforms>> =
|
|
HashMap::new();
|
|
for glyph in &layer.glyphs {
|
|
let glyph_position = glyph.position * scale_factor;
|
|
let subpixel_alignment = SubpixelAlignment::new(glyph_position);
|
|
|
|
match self.resources.glyph_cache.get(
|
|
glyph.glyph_key,
|
|
self.scene.scale_factor(),
|
|
subpixel_alignment,
|
|
&|atlas_size| create_new_texture_atlas(atlas_size, self.ctx.device),
|
|
&insert_glyph_into_texture,
|
|
&|glyph_key, scale, alignment| {
|
|
self.ctx.glyph_raster_bounds(glyph_key, scale, alignment)
|
|
},
|
|
&|glyph_key, scale, subpixel_alignment, glyph_config, format| {
|
|
self.ctx.rasterize_glyph(
|
|
glyph_key,
|
|
scale,
|
|
subpixel_alignment,
|
|
glyph_config,
|
|
format,
|
|
)
|
|
},
|
|
) {
|
|
Ok(Some(gto)) => {
|
|
let (fade_start, fade_end) = match &glyph.fade {
|
|
None => (&0.0, &-1.0),
|
|
Some(GlyphFade::Horizontal { start, end }) => (start, end),
|
|
};
|
|
|
|
// Adjust the horizontal position by the subpixel alignment
|
|
// so that we only shift the glyph over by the amount that
|
|
// isn't accounted for in the subpixel-rasterized glyph.
|
|
let glyph_position = glyph_position - subpixel_alignment.to_offset();
|
|
|
|
// Make sure to pass the glyph size in the atlas
|
|
// Not the size of the render bounds (which may be smaller)
|
|
// If you pass the render bounds as the size, the shader
|
|
// will try to sample from a smaller area than the size
|
|
// in the atlas, leading to artifacts.
|
|
let uv_region = gto.allocated_region.uv_region;
|
|
let uniform = shader::PerGlyphUniforms::new(
|
|
(glyph_position + gto.raster_bounds.origin()).into(),
|
|
gto.allocated_region.pixel_region.size().to_f32().into(),
|
|
uv_region.origin_x(),
|
|
uv_region.origin_y(),
|
|
uv_region.width(),
|
|
uv_region.height(),
|
|
fade_start * scale_factor,
|
|
fade_end * scale_factor,
|
|
glyph.color.to_f32().into(),
|
|
gto.is_emoji,
|
|
);
|
|
|
|
if let Some(per_glyph_uniforms) = texture_to_glyph.get_mut(>o.texture_id) {
|
|
per_glyph_uniforms.push(uniform);
|
|
} else {
|
|
texture_to_glyph.insert(gto.texture_id, vec![uniform]);
|
|
}
|
|
}
|
|
Ok(None) => {}
|
|
Err(_) => {
|
|
log::error!("Unable to get glyph out of glyph cache for glyph {glyph:?}");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if texture_to_glyph.is_empty() {
|
|
// Early exit if there are no glyphs to render, as it causes a debug assert
|
|
// failure in the metal code to create an empty metal buffer.
|
|
return;
|
|
}
|
|
|
|
for (texture_id, per_glyph_uniforms) in texture_to_glyph {
|
|
let per_glyph_uniforms_buffer = new_metal_buffer(
|
|
self.ctx.device,
|
|
&per_glyph_uniforms,
|
|
MTLResourceOptions::StorageModeManaged,
|
|
);
|
|
|
|
let uniforms = shader::Uniforms::new(self.ctx.drawable_size.into());
|
|
let uniforms_ptr = NonNull::from(&uniforms).cast::<c_void>();
|
|
let uniforms_len = mem::size_of::<shader::Uniforms>();
|
|
|
|
let texture = self
|
|
.resources
|
|
.glyph_cache
|
|
.texture(&texture_id)
|
|
.expect("texture ID should be in atlas");
|
|
|
|
// SAFETY: the per-glyph uniform buffer, `uniforms` value, bound texture, and quad
|
|
// index buffer outlive this encoded draw call, and the bound sizes/indices match the
|
|
// shader bindings.
|
|
unsafe {
|
|
self.command_encoder.setVertexBuffer_offset_atIndex(
|
|
Some(&per_glyph_uniforms_buffer),
|
|
0,
|
|
1,
|
|
);
|
|
self.command_encoder
|
|
.setVertexBytes_length_atIndex(uniforms_ptr, uniforms_len, 2);
|
|
self.command_encoder
|
|
.setFragmentTexture_atIndex(Some(&**texture), 0);
|
|
self.command_encoder
|
|
.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset_instanceCount(
|
|
MTLPrimitiveType::Triangle,
|
|
6,
|
|
MTLIndexType::UInt16,
|
|
&self.resources.quad_indices,
|
|
0,
|
|
per_glyph_uniforms.len(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for Frame<'_> {
|
|
fn drop(&mut self) {
|
|
self.resources.texture_cache.end_frame();
|
|
}
|
|
}
|
|
|
|
fn new_metal_buffer<T>(
|
|
device: &ProtocolObject<dyn MTLDevice>,
|
|
data: &[T],
|
|
options: MTLResourceOptions,
|
|
) -> Retained<ProtocolObject<dyn MTLBuffer>> {
|
|
// SAFETY: `data` points to `size_of_val(data)` initialized bytes; Metal copies them into the
|
|
// new buffer, so the pointer only needs to be valid for the duration of this call.
|
|
unsafe {
|
|
device.newBufferWithBytes_length_options(
|
|
NonNull::new(data.as_ptr() as *mut c_void).expect("buffer data pointer is non-null"),
|
|
std::mem::size_of_val(data),
|
|
options,
|
|
)
|
|
}
|
|
.expect("device should create a buffer")
|
|
}
|
|
|
|
mod shader {
|
|
#![allow(non_upper_case_globals)]
|
|
#![allow(non_camel_case_types)]
|
|
#![allow(non_snake_case)]
|
|
// Temporarily silence the warning coming from https://github.com/rust-lang/rust-bindgen/issues/1651
|
|
#![allow(unknown_lints)]
|
|
|
|
use pathfinder_color::ColorF;
|
|
use pathfinder_geometry::vector::{
|
|
Vector2F as PathfinderVector2F, Vector4F as PathfinderVector4F,
|
|
};
|
|
pub use shader_types::*;
|
|
|
|
mod shader_types {
|
|
// Bindgen deferences null pointers in generated test code, see:
|
|
// https://github.com/rust-lang/rust-bindgen/issues/1651
|
|
#![allow(deref_nullptr)]
|
|
include!(concat!(env!("OUT_DIR"), "/shader_types.rs"));
|
|
}
|
|
|
|
pub struct Vector2F(vector_float2);
|
|
pub struct Vector4F(vector_float4);
|
|
|
|
impl Vector2F {
|
|
pub fn new(x: f32, y: f32) -> Self {
|
|
let y = y.to_bits();
|
|
let mut vec = (y as vector_float2) << 32;
|
|
let x = x.to_bits();
|
|
vec |= x as vector_float2;
|
|
Self(vec)
|
|
}
|
|
}
|
|
|
|
impl From<PathfinderVector2F> for Vector2F {
|
|
fn from(vec: PathfinderVector2F) -> Self {
|
|
Self::new(vec.x(), vec.y())
|
|
}
|
|
}
|
|
|
|
impl Vector4F {
|
|
pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self {
|
|
let w = w.to_bits();
|
|
let mut vec = w as vector_float4;
|
|
vec <<= 32;
|
|
let z = z.to_bits();
|
|
vec |= z as vector_float4;
|
|
vec <<= 32;
|
|
let y = y.to_bits();
|
|
vec |= y as vector_float4;
|
|
vec <<= 32;
|
|
let x = x.to_bits();
|
|
vec |= x as vector_float4;
|
|
Self(vec)
|
|
}
|
|
}
|
|
|
|
impl From<PathfinderVector4F> for Vector4F {
|
|
fn from(vec: PathfinderVector4F) -> Self {
|
|
Self::new(vec.x(), vec.y(), vec.z(), vec.w())
|
|
}
|
|
}
|
|
|
|
impl From<ColorF> for Vector4F {
|
|
fn from(color: ColorF) -> Self {
|
|
Self::new(color.r(), color.g(), color.b(), color.a())
|
|
}
|
|
}
|
|
|
|
impl PerRectUniforms {
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn new(
|
|
origin: Vector2F,
|
|
size: Vector2F,
|
|
corner_radius: crate::rendering::CornerRadius,
|
|
border_top: f32,
|
|
border_right: f32,
|
|
border_bottom: f32,
|
|
border_left: f32,
|
|
background_start: Vector2F,
|
|
background_end: Vector2F,
|
|
background_start_color: Vector4F,
|
|
background_end_color: Vector4F,
|
|
border_start: Vector2F,
|
|
border_end: Vector2F,
|
|
border_start_color: Vector4F,
|
|
border_end_color: Vector4F,
|
|
is_icon: bool,
|
|
icon_color: Vector4F,
|
|
drop_shadow_offsets: Vector2F,
|
|
drop_shadow_color: Vector4F,
|
|
drop_shadow_sigma: f32,
|
|
drop_shadow_padding_factor: f32,
|
|
dash_length: f32,
|
|
gap_lengths: Vector2F,
|
|
) -> Self {
|
|
Self {
|
|
origin: origin.0,
|
|
size: size.0,
|
|
corner_radius_top_left: corner_radius.top_left,
|
|
corner_radius_top_right: corner_radius.top_right,
|
|
corner_radius_bottom_left: corner_radius.bottom_left,
|
|
corner_radius_bottom_right: corner_radius.bottom_right,
|
|
border_top,
|
|
border_right,
|
|
border_bottom,
|
|
border_left,
|
|
background_start: background_start.0,
|
|
background_end: background_end.0,
|
|
background_start_color: background_start_color.0,
|
|
background_end_color: background_end_color.0,
|
|
border_start: border_start.0,
|
|
border_end: border_end.0,
|
|
border_start_color: border_start_color.0,
|
|
border_end_color: border_end_color.0,
|
|
is_icon: is_icon as i32,
|
|
icon_color: icon_color.0,
|
|
drop_shadow_offsets: drop_shadow_offsets.0,
|
|
drop_shadow_color: drop_shadow_color.0,
|
|
drop_shadow_sigma,
|
|
drop_shadow_padding_factor,
|
|
dash_length,
|
|
gap_lengths: gap_lengths.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PerGlyphUniforms {
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn new(
|
|
origin: Vector2F,
|
|
size: Vector2F,
|
|
uv_left: f32,
|
|
uv_top: f32,
|
|
uv_width: f32,
|
|
uv_height: f32,
|
|
fade_start: f32,
|
|
fade_end: f32,
|
|
color: Vector4F,
|
|
is_emoji: bool,
|
|
) -> Self {
|
|
Self {
|
|
origin: origin.0,
|
|
size: size.0,
|
|
color: color.0,
|
|
uv_left,
|
|
uv_top,
|
|
uv_width,
|
|
uv_height,
|
|
fade_start,
|
|
fade_end,
|
|
is_emoji: is_emoji as i32,
|
|
__bindgen_padding_0: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Uniforms {
|
|
pub fn new(viewport_size: Vector2F) -> Self {
|
|
Self {
|
|
viewport_size: viewport_size.0,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) struct MetalDrawContext<'a> {
|
|
pub(super) device: &'a ProtocolObject<dyn MTLDevice>,
|
|
pub(super) drawable: &'a ProtocolObject<dyn CAMetalDrawable>,
|
|
pub(super) drawable_size: Vector2F,
|
|
rasterize_glyph_fn: &'a RasterizeGlyphFn<'a>,
|
|
glyph_raster_bounds_fn: &'a GlyphRasterBoundsFn<'a>,
|
|
}
|
|
|
|
impl MetalDrawContext<'_> {
|
|
pub(super) fn rasterize_glyph(
|
|
&self,
|
|
glyph_key: GlyphKey,
|
|
scale: Vector2F,
|
|
subpixel_alignment: SubpixelAlignment,
|
|
glyph_config: &rendering::GlyphConfig,
|
|
format: canvas::RasterFormat,
|
|
) -> anyhow::Result<RasterizedGlyph> {
|
|
(self.rasterize_glyph_fn)(glyph_key, scale, subpixel_alignment, glyph_config, format)
|
|
}
|
|
|
|
pub(super) fn glyph_raster_bounds(
|
|
&self,
|
|
glyph_key: GlyphKey,
|
|
scale: Vector2F,
|
|
glyph_config: &rendering::GlyphConfig,
|
|
) -> anyhow::Result<RectI> {
|
|
(self.glyph_raster_bounds_fn)(glyph_key, scale, glyph_config)
|
|
}
|
|
}
|
|
|
|
impl super::super::Renderer for Renderer {
|
|
fn render(&mut self, scene: &Scene, window: &WindowState, font_cache: &fonts::Cache) {
|
|
// SAFETY: `render` is called via `warp_update_layer`, which is only be invoked for
|
|
// windows created via Window::open() and always sets a non-`None` device.
|
|
#[allow(irrefutable_let_patterns)]
|
|
let Device::Metal(metal_device) = window
|
|
.device()
|
|
.expect("render is only called for a window that has a real display")
|
|
else {
|
|
log::error!("Metal renderer called with non-metal device");
|
|
return;
|
|
};
|
|
let metal_device: &ProtocolObject<dyn MTLDevice> = metal_device;
|
|
|
|
let metal_layer = window.metal_layer();
|
|
let presents_with_transaction = metal_layer.presentsWithTransaction();
|
|
let drawable = metal_layer
|
|
.nextDrawable()
|
|
.expect("CAMetalLayer with allowsNextDrawableTimeout disabled always vends a drawable");
|
|
|
|
let ctx = &MetalDrawContext {
|
|
device: metal_device,
|
|
drawable: &drawable,
|
|
drawable_size: window.physical_size(),
|
|
rasterize_glyph_fn: &|glyph_key, scale, subpixel_alignment, glyph_config, format| {
|
|
font_cache.rasterized_glyph(
|
|
glyph_key,
|
|
scale,
|
|
subpixel_alignment,
|
|
glyph_config,
|
|
format,
|
|
)
|
|
},
|
|
glyph_raster_bounds_fn: &|glyph_key, scale, alignment| {
|
|
font_cache.glyph_raster_bounds(glyph_key, scale, alignment)
|
|
},
|
|
};
|
|
|
|
let capture_callback = window.capture_callback.borrow_mut().take();
|
|
let should_capture = capture_callback.is_some();
|
|
let captured = Self::render(self, scene, ctx, should_capture, presents_with_transaction);
|
|
if let (Some(frame), Some(callback)) = (captured, capture_callback) {
|
|
callback(frame);
|
|
}
|
|
}
|
|
|
|
fn resize(&mut self, _window: &WindowState) {
|
|
// TODO(alokedesai): Backport the optimization to only set the size of surface when a
|
|
// window is resized to the Metal renderer.
|
|
}
|
|
}
|
|
|
|
/// Writes the bytes of the `glyph` into a region of the current texture identified by `region`.
|
|
fn insert_glyph_into_texture(
|
|
region: AllocatedRegion,
|
|
glyph: &RasterizedGlyph,
|
|
texture: &mut Retained<ProtocolObject<dyn MTLTexture>>,
|
|
) {
|
|
let region = MTLRegion {
|
|
origin: MTLOrigin {
|
|
x: region.pixel_region.origin_x() as usize,
|
|
y: region.pixel_region.origin_y() as usize,
|
|
z: 0,
|
|
},
|
|
size: MTLSize {
|
|
width: region.pixel_region.width() as usize,
|
|
height: region.pixel_region.height() as usize,
|
|
depth: 1,
|
|
},
|
|
};
|
|
|
|
let bytes_per_row: usize = 4 * (glyph.canvas.size.x() as usize);
|
|
// SAFETY: the glyph canvas holds at least `bytes_per_row * region.height` bytes laid out to
|
|
// match the destination region.
|
|
unsafe {
|
|
texture.replaceRegion_mipmapLevel_withBytes_bytesPerRow(
|
|
region,
|
|
0,
|
|
NonNull::new(glyph.canvas.pixels.as_slice().as_ptr() as *mut c_void)
|
|
.expect("glyph canvas pixel pointer is non-null"),
|
|
bytes_per_row,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Creates a new texture atlas for use in the cache.
|
|
fn create_new_texture_atlas(
|
|
atlas_size: usize,
|
|
device: &ProtocolObject<dyn MTLDevice>,
|
|
) -> Retained<ProtocolObject<dyn MTLTexture>> {
|
|
let texture_descriptor = MTLTextureDescriptor::new();
|
|
texture_descriptor.setPixelFormat(MTLPixelFormat::RGBA8Unorm);
|
|
// SAFETY: `atlas_size` is a fixed, valid texture dimension within Metal limits.
|
|
unsafe {
|
|
texture_descriptor.setWidth(atlas_size);
|
|
texture_descriptor.setHeight(atlas_size);
|
|
}
|
|
device
|
|
.newTextureWithDescriptor(&texture_descriptor)
|
|
.expect("device should create an atlas texture")
|
|
}
|