Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/platform/mac/rendering/metal/renderer.rs
1use crate::rendering::atlas::{AllocatedRegion, TextureId};
2use crate::rendering::{get_best_dash_gap, GlyphCache, GlyphRasterBoundsFn, RasterizeGlyphFn};
3use strato_ui_core::{
4 fonts::{self, SubpixelAlignment},
5 rendering::{self, texture_cache::TextureCache},
6};
7 
8use super::frame_capture::capture_frame;
9use crate::platform::mac::rendering::renderer::Device;
10use crate::platform::mac::window::WindowState;
11use cocoa::base::id;
12use metal::{
13 Function, MTLBlendFactor, MTLBlendOperation, MTLIndexType, MTLPrimitiveType,
14 MTLResourceOptions, RenderPipelineDescriptor,
15};
16use objc::{msg_send, sel, sel_impl};
17use strato_ui_core::platform::CapturedFrame;
18 
19use pathfinder_color::{ColorF, ColorU};
20use pathfinder_geometry::{
21 rect::RectF,
22 vector::{vec2f, Vector2F},
23};
24use strato_ui_core::fonts::{canvas, RasterizedGlyph};
25use strato_ui_core::scene::{CornerRadius, GlyphFade, Icon, Image, Layer, Scene};
26 
27use std::collections::HashMap;
28 
29use pathfinder_geometry::rect::RectI;
30use std::{fs::File, mem, sync::Once};
31use std::{io::Write, os::raw::c_void};
32use strato_ui_core::scene::GlyphKey;
33 
34const METAL_LIB_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
35static WRITE_LIB_TO_FILE: Once = Once::new();
36 
37/// A structure to help manage a single rendering pass.
38struct RenderPass<'a> {
39 drawable: &'a metal::MetalDrawableRef,
40 buffer: &'a metal::CommandBufferRef,
41 encoder: &'a metal::RenderCommandEncoderRef,
42 encoding_finished: bool,
43}
44 
45impl<'a> RenderPass<'a> {
46 fn new(
47 command_queue: &'a mut metal::CommandQueue,
48 drawable: &'a metal::MetalDrawableRef,
49 ) -> Self {
50 let buffer = command_queue.new_command_buffer();
51 let encoder = buffer.new_render_command_encoder(Self::create_descriptor(drawable));
52 Self {
53 drawable,
54 buffer,
55 encoder,
56 encoding_finished: false,
57 }
58 }
59 
60 /// Finishes a render pass with optional frame capture.
61 ///
62 /// If this is not called, the encoded commands will not be executed and the
63 /// drawable will not be updated.
64 ///
65 /// Returns the captured frame data if capture was requested.
66 fn finish_with_capture(
67 mut self,
68 drawable_size: pathfinder_geometry::vector::Vector2F,
69 should_capture: bool,
70 ) -> Option<CapturedFrame> {
71 self.encoder.end_encoding();
72 
73 self.encoding_finished = true;
74 
75 self.buffer.commit();
76 
77 self.buffer.wait_until_completed();
78 
79 let captured = if should_capture {
80 let texture = self.drawable.texture();
81 capture_frame(texture, drawable_size)
82 } else {
83 None
84 };
85 
86 self.drawable.present();
87 captured
88 }
89 
90 /// Creates a descriptor for a pass that renders into the provided drawable.
91 fn create_descriptor(drawable: &metal::MetalDrawableRef) -> &metal::RenderPassDescriptorRef {
92 let descriptor = metal::RenderPassDescriptor::new();
93 
94 let color_attachment = descriptor.color_attachments().object_at(0).expect(
95 "should always be able to get a color attachment for a CAMetalLayer's drawable",
96 );
97 color_attachment.set_texture(Some(drawable.texture()));
98 color_attachment.set_load_action(metal::MTLLoadAction::Clear);
99 color_attachment.set_store_action(metal::MTLStoreAction::Store);
100 color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 0.));
101 
102 descriptor
103 }
104}
105 
106impl Drop for RenderPass<'_> {
107 fn drop(&mut self) {
108 // Make sure that `end_encoding()` is called, even if a panic occurs
109 // during rendering.
110 if !self.encoding_finished {
111 self.encoder.end_encoding();
112 }
113 }
114}
115 
116/// A set of resources necessary for rendering that retain state across frames.
117struct Resources {
118 draw_rects_pipeline_state: metal::RenderPipelineState,
119 draw_images_pipeline_state: metal::RenderPipelineState,
120 draw_glyphs_pipeline_state: metal::RenderPipelineState,
121 quad_vertices: metal::Buffer,
122 quad_indices: metal::Buffer,
123 glyph_cache: GlyphCache<metal::Texture>,
124 texture_cache: TextureCache<metal::Texture>,
125}
126 
127/// A structure that manages rendering scenes using a particular hardware
128/// device.
129pub struct Renderer {
130 resources: Resources,
131 command_queue: metal::CommandQueue,
132}
133 
134impl Renderer {
135 pub fn new(
136 device: &metal::Device,
137 color_pixel_format: metal::MTLPixelFormat,
138 glyph_config: rendering::GlyphConfig,
139 ) -> Self {
140 let library = if cfg!(feature = "enable-metal-frame-capture") {
141 let temp_lib_path = std::env::temp_dir().join("shaders.metallib");
142 WRITE_LIB_TO_FILE.call_once(|| {
143 let mut file = File::create(&temp_lib_path).unwrap();
144 file.write_all(METAL_LIB_BYTES).unwrap();
145 });
146 device.new_library_with_file(temp_lib_path).unwrap()
147 } else {
148 device.new_library_with_data(METAL_LIB_BYTES).unwrap()
149 };
150 
151 let rect_vertex_shader = library.get_function("rect_vertex_shader", None).unwrap();
152 let rect_fragment_shader = library.get_function("rect_fragment_shader", None).unwrap();
153 let rect_pipeline = Self::create_pipeline(
154 "Rects",
155 color_pixel_format,
156 &rect_vertex_shader,
157 &rect_fragment_shader,
158 );
159 let draw_rects_pipeline_state = device.new_render_pipeline_state(&rect_pipeline).unwrap();
160 
161 let image_fragment_shader = library.get_function("image_fragment_shader", None).unwrap();
162 let image_pipeline = Self::create_pipeline(
163 "Images",
164 color_pixel_format,
165 &rect_vertex_shader,
166 &image_fragment_shader,
167 );
168 let draw_images_pipeline_state = device.new_render_pipeline_state(&image_pipeline).unwrap();
169 
170 let glyph_vertex_shader = library.get_function("glyph_vertex_shader", None).unwrap();
171 let glyph_fragment_shader = library.get_function("glyph_fragment_shader", None).unwrap();
172 let glyph_pipeline = Self::create_pipeline(
173 "Glyphs",
174 color_pixel_format,
175 &glyph_vertex_shader,
176 &glyph_fragment_shader,
177 );
178 let draw_glyphs_pipeline_state = device.new_render_pipeline_state(&glyph_pipeline).unwrap();
179 
180 let quad_vertices = new_metal_buffer(
181 device,
182 &[
183 shader::Vector2F::new(0., 0.),
184 shader::Vector2F::new(1., 0.),
185 shader::Vector2F::new(0., 1.),
186 shader::Vector2F::new(1., 1.),
187 ],
188 MTLResourceOptions::StorageModeManaged,
189 );
190 
191 let quad_indices = new_metal_buffer(
192 device,
193 &[0_u16, 1, 2, 2, 3, 1],
194 MTLResourceOptions::StorageModeManaged,
195 );
196 
197 let glyph_cache = GlyphCache::new(glyph_config);
198 
199 Self {
200 resources: Resources {
201 draw_rects_pipeline_state,
202 draw_images_pipeline_state,
203 draw_glyphs_pipeline_state,
204 quad_vertices,
205 quad_indices,
206 glyph_cache,
207 texture_cache: TextureCache::new(),
208 },
209 command_queue: device.new_command_queue(),
210 }
211 }
212 
213 fn create_pipeline(
214 label: &str,
215 color_pixel_format: metal::MTLPixelFormat,
216 vertex_shader: &Function,
217 fragment_shader: &Function,
218 ) -> RenderPipelineDescriptor {
219 let pipeline = metal::RenderPipelineDescriptor::new();
220 pipeline.set_label(label);
221 pipeline.set_vertex_function(Some(vertex_shader));
222 pipeline.set_fragment_function(Some(fragment_shader));
223 
224 let attachment = pipeline.color_attachments().object_at(0).unwrap();
225 attachment.set_pixel_format(color_pixel_format);
226 attachment.set_blending_enabled(true);
227 attachment.set_rgb_blend_operation(MTLBlendOperation::Add);
228 attachment.set_alpha_blend_operation(MTLBlendOperation::Add);
229 attachment.set_source_rgb_blend_factor(MTLBlendFactor::SourceAlpha);
230 attachment.set_source_alpha_blend_factor(MTLBlendFactor::One);
231 attachment.set_destination_rgb_blend_factor(MTLBlendFactor::OneMinusSourceAlpha);
232 attachment.set_destination_alpha_blend_factor(MTLBlendFactor::OneMinusSourceAlpha);
233 
234 pipeline
235 }
236 
237 fn render(
238 &mut self,
239 scene: &Scene,
240 ctx: &MetalDrawContext,
241 should_capture: bool,
242 ) -> Option<CapturedFrame> {
243 self.resources
244 .glyph_cache
245 .update_config(&scene.rendering_config().glyphs);
246 
247 let render_pass = RenderPass::new(&mut self.command_queue, ctx.drawable);
248 
249 Frame::new(scene, render_pass.encoder, &mut self.resources, ctx).draw();
250 
251 render_pass.finish_with_capture(ctx.drawable_size, should_capture)
252 }
253}
254 
255/// A struct that manages rendering a single frame: the encoding of a scene into
256/// a set of GPU draw calls to rasterize the scene description into a bitmap
257/// image.
258pub struct Frame<'a> {
259 scene: &'a Scene,
260 command_encoder: &'a metal::RenderCommandEncoderRef,
261 resources: &'a mut Resources,
262 ctx: &'a MetalDrawContext<'a>,
263}
264 
265impl<'a> Frame<'a> {
266 fn new(
267 scene: &'a Scene,
268 command_encoder: &'a metal::RenderCommandEncoderRef,
269 resources: &'a mut Resources,
270 ctx: &'a MetalDrawContext<'a>,
271 ) -> Self {
272 Self {
273 scene,
274 resources,
275 command_encoder,
276 ctx,
277 }
278 }
279 
280 fn draw(&mut self) {
281 self.command_encoder.set_viewport(metal::MTLViewport {
282 originX: 0.0,
283 originY: 0.0,
284 width: self.ctx.drawable_size.x() as f64,
285 height: self.ctx.drawable_size.y() as f64,
286 znear: 0.0,
287 zfar: 1.0,
288 });
289 
290 for layer in self.scene.layers() {
291 if let Some(bounds) = layer.clip_bounds {
292 // Make sure the scissor rect doesn't extend beyond the boundaries
293 // of the window, as required by the Metal API.
294 // API docs: https://developer.apple.com/documentation/metal/mtlrendercommandencoder/1515583-setscissorrect?language=objc
295 // Scissor test background reading: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/By_example/Basic_scissoring
296 let device_bounds = RectF::new(Vector2F::zero(), self.ctx.drawable_size);
297 let bounds = (bounds * self.scene.scale_factor()).intersection(device_bounds);
298 if let Some(intersection) = bounds {
299 self.command_encoder
300 .set_scissor_rect(metal::MTLScissorRect {
301 x: intersection.origin_x().round() as u64,
302 y: intersection.origin_y().round() as u64,
303 width: intersection.width().round() as u64,
304 height: intersection.height().round() as u64,
305 });
306 } else {
307 // The layer's clip bounds don't intersect the window bounds
308 // at all; we can skip drawing anything in this layer.
309 continue;
310 }
311 } else {
312 self.command_encoder
313 .set_scissor_rect(metal::MTLScissorRect {
314 x: 0_u64,
315 y: 0_u64,
316 width: self.ctx.drawable_size.x() as u64,
317 height: self.ctx.drawable_size.y() as u64,
318 });
319 }
320 self.draw_rects(layer);
321 self.draw_images(layer);
322 self.draw_glyphs(layer);
323 }
324 }
325 
326 // Utility function to render image or icon in Metal.
327 fn render_image_or_icon(&mut self, image: Option<&Image>, icon: Option<&Icon>) {
328 let opacity;
329 let bounds;
330 let asset;
331 let is_icon;
332 let icon_color;
333 let ui_corner_radius;
334 
335 if let Some(to_render) = image {
336 opacity = to_render.opacity;
337 bounds = to_render.bounds;
338 asset = &to_render.asset;
339 is_icon = false;
340 icon_color = ColorF::new(0.0, 0.0, 0.0, opacity).into();
341 ui_corner_radius = to_render.corner_radius;
342 } else {
343 let to_render = icon.unwrap();
344 opacity = to_render.opacity;
345 bounds = to_render.bounds;
346 asset = &to_render.asset;
347 is_icon = true;
348 icon_color = to_render.color.to_f32().into();
349 ui_corner_radius = CornerRadius::default();
350 }
351 
352 let mut per_rect_uniforms = Vec::new();
353 let scale_factor = self.scene.scale_factor();
354 let bounds = bounds * scale_factor;
355 let min_dimension = f32::min(bounds.height(), bounds.width());
356 let corner_radius = crate::rendering::CornerRadius::from_ui_corner_radius(
357 ui_corner_radius,
358 scale_factor,
359 min_dimension,
360 );
361 per_rect_uniforms.push(shader::PerRectUniforms::new(
362 bounds.origin().into(),
363 bounds.size().into(),
364 corner_radius,
365 0.,
366 0.,
367 0.,
368 0.,
369 vec2f(0.0, 0.0).into(),
370 vec2f(1.0, 0.0).into(),
371 ColorF::new(0.0, 0.0, 0.0, opacity).into(),
372 ColorF::new(0.0, 0.0, 0.0, opacity).into(),
373 vec2f(0.0, 0.0).into(),
374 vec2f(1.0, 0.0).into(),
375 ColorU::transparent_black().to_f32().into(),
376 ColorU::transparent_black().to_f32().into(),
377 is_icon,
378 icon_color,
379 Vector2F::zero().into(),
380 ColorU::transparent_black().to_f32().into(),
381 0_f32,
382 0_f32,
383 0.,
384 vec2f(0.0, 0.0).into(),
385 ));
386 let per_rect_uniforms_buffer = new_metal_buffer(
387 self.ctx.device,
388 &per_rect_uniforms,
389 MTLResourceOptions::StorageModeManaged,
390 );
391 
392 self.command_encoder
393 .set_vertex_buffer(1, Some(per_rect_uniforms_buffer.as_ref()), 0);
394 
395 let uniforms = shader::Uniforms::new(self.ctx.drawable_size.into());
396 self.command_encoder.set_vertex_bytes(
397 2,
398 mem::size_of::<shader::Uniforms>() as u64,
399 [uniforms].as_ptr() as *const _,
400 );
401 self.command_encoder.set_fragment_bytes(
402 0,
403 mem::size_of::<shader::Uniforms>() as u64,
404 [uniforms].as_ptr() as *const _,
405 );
406 
407 let (_, texture) = self
408 .resources
409 .texture_cache
410 .get_or_insert_by_asset(asset, |asset| {
411 let width = asset.size().x() as u64;
412 let height = asset.size().y() as u64;
413 
414 let texture_descriptor = metal::TextureDescriptor::new();
415 texture_descriptor.set_pixel_format(metal::MTLPixelFormat::RGBA8Unorm);
416 texture_descriptor.set_width(width);
417 texture_descriptor.set_height(height);
418 let texture = self.ctx.device.new_texture(&texture_descriptor);
419 let region = metal::MTLRegion {
420 origin: metal::MTLOrigin { x: 0, y: 0, z: 0 },
421 size: metal::MTLSize {
422 width,
423 height,
424 depth: 1,
425 },
426 };
427 
428 let bytes_per_row: u64 = 4 * width;
429 texture.replace_region(
430 region,
431 0,
432 asset.rgba_bytes().as_ptr() as *const c_void,
433 bytes_per_row,
434 );
435 
436 texture
437 });
438 
439 self.command_encoder
440 .set_fragment_texture(0, Some(texture.as_ref()));
441 
442 self.command_encoder.draw_indexed_primitives_instanced(
443 MTLPrimitiveType::Triangle,
444 6,
445 MTLIndexType::UInt16,
446 self.resources.quad_indices.as_ref(),
447 0,
448 per_rect_uniforms.len() as u64,
449 );
450 }
451 
452 fn draw_images(&mut self, layer: &Layer) {
453 if layer.images.is_empty() && layer.icons.is_empty() {
454 // It's a mac assertion error to create an empty metal buffer, so exit early
455 return;
456 }
457 
458 self.command_encoder
459 .set_render_pipeline_state(&self.resources.draw_images_pipeline_state);
460 self.command_encoder
461 .set_vertex_buffer(0, Some(self.resources.quad_vertices.as_ref()), 0);
462 
463 for image in &layer.images {
464 self.render_image_or_icon(Some(image), None);
465 }
466 
467 // Another iteration for rendering icons.
468 for icon in &layer.icons {
469 self.render_image_or_icon(None, Some(icon));
470 }
471 }
472 
473 fn draw_rects(&self, layer: &Layer) {
474 if layer.rects.is_empty() {
475 // It's a mac assertion error to create an empty metal buffer, so exit early
476 return;
477 }
478 
479 self.command_encoder
480 .set_render_pipeline_state(&self.resources.draw_rects_pipeline_state);
481 self.command_encoder
482 .set_vertex_buffer(0, Some(self.resources.quad_vertices.as_ref()), 0);
483 
484 let mut per_rect_uniforms = Vec::new();
485 for rect in &layer.rects {
486 let scale_factor = self.scene.scale_factor();
487 let bounds = rect.bounds * scale_factor;
488 
489 let dash = rect
490 .border
491 .dash
492 .map(|mut dash| {
493 dash.dash_length *= scale_factor;
494 dash.gap_length *= scale_factor;
495 dash
496 })
497 .unwrap_or_default();
498 let horizontal_gap = get_best_dash_gap(bounds.width(), dash);
499 let vertical_gap = get_best_dash_gap(bounds.height(), dash);
500 let dash_length = dash.dash_length;
501 let gap_lengths = Vector2F::new(horizontal_gap, vertical_gap);
502 
503 if let Some(drop_shadow) = rect.drop_shadow {
504 let sigma = drop_shadow.blur_radius;
505 let padding = drop_shadow.spread_radius * self.scene.scale_factor();
506 let shadow_origin =
507 bounds.origin() + drop_shadow.offset * self.scene.scale_factor() - padding;
508 let shadow_size = bounds.size() + vec2f(2. * padding, 2. * padding);
509 
510 let min_dimension = f32::min(shadow_size.x(), shadow_size.y());
511 
512 let corner_radius = crate::rendering::CornerRadius::from_ui_corner_radius(
513 rect.corner_radius,
514 scale_factor,
515 min_dimension,
516 );
517 
518 // For the drop shadow case, we pass in a rect with the bounds
519 // of the shadow and render that before rendering the actual rect.
520 per_rect_uniforms.push(shader::PerRectUniforms::new(
521 shadow_origin.into(),
522 shadow_size.into(),
523 corner_radius,
524 0_f32,
525 0_f32,
526 0_f32,
527 0_f32,
528 Vector2F::zero().into(),
529 Vector2F::zero().into(),
530 ColorU::transparent_black().to_f32().into(),
531 ColorU::transparent_black().to_f32().into(),
532 Vector2F::zero().into(),
533 Vector2F::zero().into(),
534 ColorU::transparent_black().to_f32().into(),
535 ColorU::transparent_black().to_f32().into(),
536 false,
537 ColorU::transparent_black().to_f32().into(),
538 (drop_shadow.offset * self.scene.scale_factor()).into(),
539 drop_shadow.color.to_f32().into(),
540 sigma * self.scene.scale_factor(),
541 padding,
542 dash_length,
543 gap_lengths.into(),
544 ));
545 }
546 
547 let min_dimension = f32::min(bounds.height(), bounds.width());
548 let corner_radius = crate::rendering::CornerRadius::from_ui_corner_radius(
549 rect.corner_radius,
550 scale_factor,
551 min_dimension,
552 );
553 
554 per_rect_uniforms.push(shader::PerRectUniforms::new(
555 bounds.origin().into(),
556 bounds.size().into(),
557 corner_radius,
558 rect.border.top_width() * scale_factor,
559 rect.border.right_width() * scale_factor,
560 rect.border.bottom_width() * scale_factor,
561 rect.border.left_width() * scale_factor,
562 rect.background.start().into(),
563 rect.background.end().into(),
564 rect.background.start_color().to_f32().into(),
565 rect.background.end_color().to_f32().into(),
566 rect.border.color.start().into(),
567 rect.border.color.end().into(),
568 rect.border.color.start_color().to_f32().into(),
569 rect.border.color.end_color().to_f32().into(),
570 false,
571 ColorU::transparent_black().to_f32().into(),
572 Vector2F::zero().into(),
573 ColorU::transparent_black().to_f32().into(),
574 0_f32,
575 0_f32,
576 dash_length,
577 gap_lengths.into(),
578 ));
579 }
580 let per_rect_uniforms_buffer = new_metal_buffer(
581 self.ctx.device,
582 &per_rect_uniforms,
583 MTLResourceOptions::StorageModeManaged,
584 );
585 
586 self.command_encoder
587 .set_vertex_buffer(1, Some(per_rect_uniforms_buffer.as_ref()), 0);
588 
589 let uniforms = shader::Uniforms::new(self.ctx.drawable_size.into());
590 self.command_encoder.set_vertex_bytes(
591 2,
592 mem::size_of::<shader::Uniforms>() as u64,
593 [uniforms].as_ptr() as *const _,
594 );
595 self.command_encoder.set_fragment_bytes(
596 0,
597 mem::size_of::<shader::Uniforms>() as u64,
598 [uniforms].as_ptr() as *const _,
599 );
600 
601 self.command_encoder.draw_indexed_primitives_instanced(
602 MTLPrimitiveType::Triangle,
603 6,
604 MTLIndexType::UInt16,
605 self.resources.quad_indices.as_ref(),
606 0,
607 per_rect_uniforms.len() as u64,
608 );
609 }
610 
611 fn draw_glyphs(&mut self, layer: &Layer) {
612 if layer.glyphs.is_empty() {
613 // It's a mac assertion error to create an empty metal buffer, so exit early
614 return;
615 }
616 
617 self.command_encoder
618 .set_render_pipeline_state(&self.resources.draw_glyphs_pipeline_state);
619 self.command_encoder
620 .set_vertex_buffer(0, Some(self.resources.quad_vertices.as_ref()), 0);
621 
622 let scale_factor = self.scene.scale_factor();
623 
624 let mut texture_to_glyph: HashMap<TextureId, Vec<shader::PerGlyphUniforms>> =
625 HashMap::new();
626 for glyph in &layer.glyphs {
627 let glyph_position = glyph.position * scale_factor;
628 let subpixel_alignment = SubpixelAlignment::new(glyph_position);
629 
630 match self.resources.glyph_cache.get(
631 glyph.glyph_key,
632 self.scene.scale_factor(),
633 subpixel_alignment,
634 &|atlas_size| create_new_texture_atlas(atlas_size, self.ctx.device),
635 &insert_glyph_into_texture,
636 &|glyph_key, scale, alignment| {
637 self.ctx.glyph_raster_bounds(glyph_key, scale, alignment)
638 },
639 &|glyph_key, scale, subpixel_alignment, glyph_config, format| {
640 self.ctx.rasterize_glyph(
641 glyph_key,
642 scale,
643 subpixel_alignment,
644 glyph_config,
645 format,
646 )
647 },
648 ) {
649 Ok(Some(gto)) => {
650 let (fade_start, fade_end) = match &glyph.fade {
651 None => (&0.0, &-1.0),
652 Some(GlyphFade::Horizontal { start, end }) => (start, end),
653 };
654 
655 // Adjust the horizontal position by the subpixel alignment
656 // so that we only shift the glyph over by the amount that
657 // isn't accounted for in the subpixel-rasterized glyph.
658 let glyph_position = glyph_position - subpixel_alignment.to_offset();
659 
660 // Make sure to pass the glyph size in the atlas
661 // Not the size of the render bounds (which may be smaller)
662 // If you pass the render bounds as the size, the shader
663 // will try to sample from a smaller area than the size
664 // in the atlas, leading to artifacts.
665 let uv_region = gto.allocated_region.uv_region;
666 let uniform = shader::PerGlyphUniforms::new(
667 (glyph_position + gto.raster_bounds.origin()).into(),
668 gto.allocated_region.pixel_region.size().to_f32().into(),
669 uv_region.origin_x(),
670 uv_region.origin_y(),
671 uv_region.width(),
672 uv_region.height(),
673 fade_start * scale_factor,
674 fade_end * scale_factor,
675 glyph.color.to_f32().into(),
676 gto.is_emoji,
677 );
678 
679 if let Some(per_glyph_uniforms) = texture_to_glyph.get_mut(&gto.texture_id) {
680 per_glyph_uniforms.push(uniform);
681 } else {
682 texture_to_glyph.insert(gto.texture_id, vec![uniform]);
683 }
684 }
685 Ok(None) => {}
686 Err(_) => {
687 log::error!("Unable to get glyph out of glyph cache for glyph {glyph:?}");
688 return;
689 }
690 }
691 }
692 
693 if texture_to_glyph.is_empty() {
694 // Early exit if there are no glyphs to render, as it causes a debug assert
695 // failure in the metal code to create an empty metal buffer.
696 return;
697 }
698 
699 for (texture_id, per_glyph_uniforms) in texture_to_glyph {
700 let per_glyph_uniforms_buffer = new_metal_buffer(
701 self.ctx.device,
702 &per_glyph_uniforms,
703 MTLResourceOptions::StorageModeManaged,
704 );
705 
706 self.command_encoder
707 .set_vertex_buffer(1, Some(per_glyph_uniforms_buffer.as_ref()), 0);
708 
709 let uniforms = shader::Uniforms::new(self.ctx.drawable_size.into());
710 self.command_encoder.set_vertex_bytes(
711 2,
712 mem::size_of::<shader::Uniforms>() as u64,
713 [uniforms].as_ptr() as *const _,
714 );
715 
716 let texture = self
717 .resources
718 .glyph_cache
719 .texture(&texture_id)
720 .expect("texture ID should be in atlas");
721 
722 self.command_encoder.set_fragment_texture(0, Some(texture));
723 
724 self.command_encoder.draw_indexed_primitives_instanced(
725 MTLPrimitiveType::Triangle,
726 6,
727 MTLIndexType::UInt16,
728 self.resources.quad_indices.as_ref(),
729 0,
730 per_glyph_uniforms.len() as u64,
731 );
732 }
733 }
734}
735 
736impl Drop for Frame<'_> {
737 fn drop(&mut self) {
738 self.resources.texture_cache.end_frame();
739 }
740}
741 
742fn new_metal_buffer<T>(
743 device: &metal::Device,
744 data: &[T],
745 options: MTLResourceOptions,
746) -> metal::Buffer {
747 device.new_buffer_with_data(
748 data.as_ptr() as *const c_void,
749 std::mem::size_of_val(data) as u64,
750 options,
751 )
752}
753 
754mod shader {
755 #![allow(non_upper_case_globals)]
756 #![allow(non_camel_case_types)]
757 #![allow(non_snake_case)]
758 // Temporarily silence the warning coming from https://github.com/rust-lang/rust-bindgen/issues/1651
759 #![allow(unknown_lints)]
760 
761 use pathfinder_color::ColorF;
762 use pathfinder_geometry::vector::{
763 Vector2F as PathfinderVector2F, Vector4F as PathfinderVector4F,
764 };
765 pub use shader_types::*;
766 
767 mod shader_types {
768 // Bindgen deferences null pointers in generated test code, see:
769 // https://github.com/rust-lang/rust-bindgen/issues/1651
770 #![allow(deref_nullptr)]
771 include!(concat!(env!("OUT_DIR"), "/shader_types.rs"));
772 }
773 
774 pub struct Vector2F(vector_float2);
775 pub struct Vector4F(vector_float4);
776 
777 impl Vector2F {
778 pub fn new(x: f32, y: f32) -> Self {
779 let y = y.to_bits();
780 let mut vec = (y as vector_float2) << 32;
781 let x = x.to_bits();
782 vec |= x as vector_float2;
783 Self(vec)
784 }
785 }
786 
787 impl From<PathfinderVector2F> for Vector2F {
788 fn from(vec: PathfinderVector2F) -> Self {
789 Self::new(vec.x(), vec.y())
790 }
791 }
792 
793 impl Vector4F {
794 pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self {
795 let w = w.to_bits();
796 let mut vec = w as vector_float4;
797 vec <<= 32;
798 let z = z.to_bits();
799 vec |= z as vector_float4;
800 vec <<= 32;
801 let y = y.to_bits();
802 vec |= y as vector_float4;
803 vec <<= 32;
804 let x = x.to_bits();
805 vec |= x as vector_float4;
806 Self(vec)
807 }
808 }
809 
810 impl From<PathfinderVector4F> for Vector4F {
811 fn from(vec: PathfinderVector4F) -> Self {
812 Self::new(vec.x(), vec.y(), vec.z(), vec.w())
813 }
814 }
815 
816 impl From<ColorF> for Vector4F {
817 fn from(color: ColorF) -> Self {
818 Self::new(color.r(), color.g(), color.b(), color.a())
819 }
820 }
821 
822 impl PerRectUniforms {
823 #[allow(clippy::too_many_arguments)]
824 pub fn new(
825 origin: Vector2F,
826 size: Vector2F,
827 corner_radius: crate::rendering::CornerRadius,
828 border_top: f32,
829 border_right: f32,
830 border_bottom: f32,
831 border_left: f32,
832 background_start: Vector2F,
833 background_end: Vector2F,
834 background_start_color: Vector4F,
835 background_end_color: Vector4F,
836 border_start: Vector2F,
837 border_end: Vector2F,
838 border_start_color: Vector4F,
839 border_end_color: Vector4F,
840 is_icon: bool,
841 icon_color: Vector4F,
842 drop_shadow_offsets: Vector2F,
843 drop_shadow_color: Vector4F,
844 drop_shadow_sigma: f32,
845 drop_shadow_padding_factor: f32,
846 dash_length: f32,
847 gap_lengths: Vector2F,
848 ) -> Self {
849 Self {
850 origin: origin.0,
851 size: size.0,
852 corner_radius_top_left: corner_radius.top_left,
853 corner_radius_top_right: corner_radius.top_right,
854 corner_radius_bottom_left: corner_radius.bottom_left,
855 corner_radius_bottom_right: corner_radius.bottom_right,
856 border_top,
857 border_right,
858 border_bottom,
859 border_left,
860 background_start: background_start.0,
861 background_end: background_end.0,
862 background_start_color: background_start_color.0,
863 background_end_color: background_end_color.0,
864 border_start: border_start.0,
865 border_end: border_end.0,
866 border_start_color: border_start_color.0,
867 border_end_color: border_end_color.0,
868 is_icon: is_icon as i32,
869 icon_color: icon_color.0,
870 drop_shadow_offsets: drop_shadow_offsets.0,
871 drop_shadow_color: drop_shadow_color.0,
872 drop_shadow_sigma,
873 drop_shadow_padding_factor,
874 dash_length,
875 gap_lengths: gap_lengths.0,
876 }
877 }
878 }
879 
880 impl PerGlyphUniforms {
881 #[allow(clippy::too_many_arguments)]
882 pub fn new(
883 origin: Vector2F,
884 size: Vector2F,
885 uv_left: f32,
886 uv_top: f32,
887 uv_width: f32,
888 uv_height: f32,
889 fade_start: f32,
890 fade_end: f32,
891 color: Vector4F,
892 is_emoji: bool,
893 ) -> Self {
894 Self {
895 origin: origin.0,
896 size: size.0,
897 color: color.0,
898 uv_left,
899 uv_top,
900 uv_width,
901 uv_height,
902 fade_start,
903 fade_end,
904 is_emoji: is_emoji as i32,
905 __bindgen_padding_0: Default::default(),
906 }
907 }
908 }
909 
910 impl Uniforms {
911 pub fn new(viewport_size: Vector2F) -> Self {
912 Self {
913 viewport_size: viewport_size.0,
914 }
915 }
916 }
917}
918 
919pub(super) struct MetalDrawContext<'a> {
920 pub(super) device: &'a metal::Device,
921 pub(super) drawable: &'a metal::MetalDrawableRef,
922 pub(super) drawable_size: Vector2F,
923 rasterize_glyph_fn: &'a RasterizeGlyphFn<'a>,
924 glyph_raster_bounds_fn: &'a GlyphRasterBoundsFn<'a>,
925}
926 
927impl MetalDrawContext<'_> {
928 pub(super) fn rasterize_glyph(
929 &self,
930 glyph_key: GlyphKey,
931 scale: Vector2F,
932 subpixel_alignment: SubpixelAlignment,
933 glyph_config: &rendering::GlyphConfig,
934 format: canvas::RasterFormat,
935 ) -> anyhow::Result<RasterizedGlyph> {
936 (self.rasterize_glyph_fn)(glyph_key, scale, subpixel_alignment, glyph_config, format)
937 }
938 
939 pub(super) fn glyph_raster_bounds(
940 &self,
941 glyph_key: GlyphKey,
942 scale: Vector2F,
943 glyph_config: &rendering::GlyphConfig,
944 ) -> anyhow::Result<RectI> {
945 (self.glyph_raster_bounds_fn)(glyph_key, scale, glyph_config)
946 }
947}
948 
949impl super::super::Renderer for Renderer {
950 fn render(&mut self, scene: &Scene, window: &WindowState, font_cache: &fonts::Cache) {
951 // SAFETY: `render` is called via `warp_update_layer`, which is only be invoked for
952 // windows created via Window::open() and always sets a non-`None` device.
953 #[allow(irrefutable_let_patterns)]
954 let Device::Metal(metal_device) = window
955 .device()
956 .expect("render is only called for a window that has a real display")
957 else {
958 log::error!("Metal renderer called with non-metal device");
959 return;
960 };
961 
962 let drawable = unsafe {
963 let native_view = window.native_view();
964 let layer: id = msg_send![native_view, layer];
965 let drawable: &metal::MetalDrawableRef = msg_send![layer, nextDrawable];
966 drawable
967 };
968 
969 let ctx = &MetalDrawContext {
970 device: metal_device,
971 drawable,
972 drawable_size: window.physical_size(),
973 rasterize_glyph_fn: &|glyph_key, scale, subpixel_alignment, glyph_config, format| {
974 font_cache.rasterized_glyph(
975 glyph_key,
976 scale,
977 subpixel_alignment,
978 glyph_config,
979 format,
980 )
981 },
982 glyph_raster_bounds_fn: &|glyph_key, scale, alignment| {
983 font_cache.glyph_raster_bounds(glyph_key, scale, alignment)
984 },
985 };
986 
987 let capture_callback = window.capture_callback.borrow_mut().take();
988 let should_capture = capture_callback.is_some();
989 let captured = Self::render(self, scene, ctx, should_capture);
990 if let (Some(frame), Some(callback)) = (captured, capture_callback) {
991 callback(frame);
992 }
993 }
994 
995 fn resize(&mut self, _window: &WindowState) {
996 // TODO(alokedesai): Backport the optimization to only set the size of surface when a
997 // window is resized to the Metal renderer.
998 }
999}
1000 
1001/// Writes the bytes of the `glyph` into a region of the current texture identified by `region`.
1002fn insert_glyph_into_texture(
1003 region: AllocatedRegion,
1004 glyph: &RasterizedGlyph,
1005 texture: &mut metal::Texture,
1006) {
1007 let region = metal::MTLRegion {
1008 origin: metal::MTLOrigin {
1009 x: region.pixel_region.origin_x() as u64,
1010 y: region.pixel_region.origin_y() as u64,
1011 z: 0,
1012 },
1013 size: metal::MTLSize {
1014 width: region.pixel_region.width() as u64,
1015 height: region.pixel_region.height() as u64,
1016 depth: 1,
1017 },
1018 };
1019 
1020 let bytes_per_row: u64 = 4 * (glyph.canvas.size.x() as u64);
1021 texture.replace_region(
1022 region,
1023 0,
1024 glyph.canvas.pixels.as_slice().as_ptr() as *const c_void,
1025 bytes_per_row,
1026 );
1027}
1028 
1029/// Creates a new texture atlas for use in the cache.
1030fn create_new_texture_atlas(atlas_size: usize, device: &metal::Device) -> metal::Texture {
1031 let texture_descriptor = metal::TextureDescriptor::new();
1032 texture_descriptor.set_pixel_format(metal::MTLPixelFormat::RGBA8Unorm);
1033 texture_descriptor.set_width(atlas_size as u64);
1034 texture_descriptor.set_height(atlas_size as u64);
1035 device.new_texture(&texture_descriptor)
1036}
1037