Skip to repository content1800 lines · 67.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:46:52.684Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
metal_renderer.rs
1use crate::metal_atlas::MetalAtlas;
2use anyhow::Result;
3use block::ConcreteBlock;
4use cocoa::{
5 base::{NO, YES},
6 foundation::{NSSize, NSUInteger},
7 quartzcore::AutoresizingMask,
8};
9use gpui::{
10 AtlasTextureId, Background, Bounds, ContentMask, DevicePixels, MonochromeSprite, PaintSurface,
11 Path, Point, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size,
12 Surface, Underline, point, size,
13};
14#[cfg(any(test, feature = "test-support"))]
15use image::RgbaImage;
16
17use core_foundation::base::TCFType;
18use core_video::{
19 metal_texture::CVMetalTextureGetTexture, metal_texture_cache::CVMetalTextureCache,
20 pixel_buffer::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
21};
22use foreign_types::{ForeignType, ForeignTypeRef};
23use metal::{
24 CAMetalLayer, CommandQueue, MTLGPUFamily, MTLPixelFormat, MTLResourceOptions, NSRange,
25 RenderPassColorAttachmentDescriptorRef,
26};
27use objc::{self, msg_send, sel, sel_impl};
28use parking_lot::Mutex;
29
30use std::{cell::Cell, ffi::c_void, mem, ptr, sync::Arc};
31
32// Exported to metal
33pub(crate) type PointF = gpui::Point<f32>;
34
35#[cfg(not(feature = "runtime_shaders"))]
36const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
37#[cfg(feature = "runtime_shaders")]
38const SHADERS_SOURCE_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal"));
39// Use 4x MSAA, all devices support it.
40// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount
41const PATH_SAMPLE_COUNT: u32 = 4;
42
43pub(crate) type Context = Arc<Mutex<InstanceBufferPool>>;
44pub(crate) type Renderer = MetalRenderer;
45
46pub(crate) unsafe fn new_renderer(
47 context: self::Context,
48 _native_window: *mut c_void,
49 _native_view: *mut c_void,
50 _bounds: gpui::Size<f32>,
51 transparent: bool,
52) -> Renderer {
53 MetalRenderer::new(context, transparent)
54}
55
56pub(crate) struct InstanceBufferPool {
57 buffer_size: usize,
58 buffers: Vec<metal::Buffer>,
59}
60
61impl Default for InstanceBufferPool {
62 fn default() -> Self {
63 Self {
64 buffer_size: 2 * 1024 * 1024,
65 buffers: Vec::new(),
66 }
67 }
68}
69
70pub(crate) struct InstanceBuffer {
71 metal_buffer: metal::Buffer,
72 size: usize,
73}
74
75impl InstanceBufferPool {
76 pub(crate) fn reset(&mut self, buffer_size: usize) {
77 self.buffer_size = buffer_size;
78 self.buffers.clear();
79 }
80
81 pub(crate) fn acquire(
82 &mut self,
83 device: &metal::Device,
84 unified_memory: bool,
85 ) -> InstanceBuffer {
86 let buffer = self.buffers.pop().unwrap_or_else(|| {
87 let options = if unified_memory {
88 MTLResourceOptions::StorageModeShared
89 // Buffers are write only which can benefit from the combined cache
90 // https://developer.apple.com/documentation/metal/mtlresourceoptions/cpucachemodewritecombined
91 | MTLResourceOptions::CPUCacheModeWriteCombined
92 } else {
93 MTLResourceOptions::StorageModeManaged
94 };
95
96 device.new_buffer(self.buffer_size as u64, options)
97 });
98 InstanceBuffer {
99 metal_buffer: buffer,
100 size: self.buffer_size,
101 }
102 }
103
104 pub(crate) fn release(&mut self, buffer: InstanceBuffer) {
105 if buffer.size == self.buffer_size {
106 self.buffers.push(buffer.metal_buffer)
107 }
108 }
109}
110
111pub(crate) struct MetalRenderer {
112 device: metal::Device,
113 layer: Option<metal::MetalLayer>,
114 is_apple_gpu: bool,
115 is_unified_memory: bool,
116 presents_with_transaction: bool,
117 /// For headless rendering, tracks whether output should be opaque
118 opaque: bool,
119 command_queue: CommandQueue,
120 paths_rasterization_pipeline_state: metal::RenderPipelineState,
121 path_sprites_pipeline_state: metal::RenderPipelineState,
122 shadows_pipeline_state: metal::RenderPipelineState,
123 quads_pipeline_state: metal::RenderPipelineState,
124 underlines_pipeline_state: metal::RenderPipelineState,
125 monochrome_sprites_pipeline_state: metal::RenderPipelineState,
126 polychrome_sprites_pipeline_state: metal::RenderPipelineState,
127 surfaces_pipeline_state: metal::RenderPipelineState,
128 unit_vertices: metal::Buffer,
129 #[allow(clippy::arc_with_non_send_sync)]
130 instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>,
131 sprite_atlas: Arc<MetalAtlas>,
132 core_video_texture_cache: core_video::metal_texture_cache::CVMetalTextureCache,
133 path_intermediate_texture: Option<metal::Texture>,
134 path_intermediate_msaa_texture: Option<metal::Texture>,
135 path_sample_count: u32,
136 /// Offscreen render target reused across `render_scene` calls when
137 /// rendering headlessly without reading pixels back.
138 #[cfg(any(test, feature = "test-support"))]
139 headless_render_target: Option<metal::Texture>,
140}
141
142#[repr(C)]
143pub struct PathRasterizationVertex {
144 pub xy_position: Point<ScaledPixels>,
145 pub st_position: Point<f32>,
146 pub color: Background,
147 pub bounds: Bounds<ScaledPixels>,
148}
149
150impl MetalRenderer {
151 /// Creates a new MetalRenderer with a CAMetalLayer for window-based rendering.
152 pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>, transparent: bool) -> Self {
153 let device = Self::create_device();
154
155 let layer = metal::MetalLayer::new();
156 layer.set_device(&device);
157 layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
158 // Support direct-to-display rendering if the window is not transparent
159 // https://developer.apple.com/documentation/metal/managing-your-game-window-for-metal-in-macos
160 layer.set_opaque(!transparent);
161 layer.set_maximum_drawable_count(3);
162 // Allow texture reading for visual tests (captures screenshots without ScreenCaptureKit)
163 #[cfg(any(test, feature = "test-support"))]
164 layer.set_framebuffer_only(false);
165 unsafe {
166 let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
167 let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
168 let _: () = msg_send![
169 &*layer,
170 setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE
171 | AutoresizingMask::HEIGHT_SIZABLE
172 ];
173 }
174
175 Self::new_internal(device, Some(layer), !transparent, instance_buffer_pool)
176 }
177
178 /// Creates a new headless MetalRenderer for offscreen rendering without a window.
179 ///
180 /// This renderer can render scenes to images without requiring a CAMetalLayer,
181 /// window, or AppKit. Use `render_scene_to_image()` to render scenes.
182 #[cfg(any(test, feature = "test-support"))]
183 pub fn new_headless(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>) -> Self {
184 let device = Self::create_device();
185 Self::new_internal(device, None, true, instance_buffer_pool)
186 }
187
188 fn create_device() -> metal::Device {
189 // Prefer low‐power integrated GPUs on Intel Mac. On Apple
190 // Silicon, there is only ever one GPU, so this is equivalent to
191 // `metal::Device::system_default()`.
192 if let Some(d) = metal::Device::all()
193 .into_iter()
194 .min_by_key(|d| (d.is_removable(), !d.is_low_power()))
195 {
196 d
197 } else {
198 // For some reason `all()` can return an empty list, see https://github.com/zed-industries/zed/issues/37689
199 // In that case, we fall back to the system default device.
200 log::error!(
201 "Unable to enumerate Metal devices; attempting to use system default device"
202 );
203 metal::Device::system_default().unwrap_or_else(|| {
204 log::error!("unable to access a compatible graphics device");
205 std::process::exit(1);
206 })
207 }
208 }
209
210 fn new_internal(
211 device: metal::Device,
212 layer: Option<metal::MetalLayer>,
213 opaque: bool,
214 instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>,
215 ) -> Self {
216 #[cfg(feature = "runtime_shaders")]
217 let library = device
218 .new_library_with_source(&SHADERS_SOURCE_FILE, &metal::CompileOptions::new())
219 .expect("error building metal library");
220 #[cfg(not(feature = "runtime_shaders"))]
221 let library = device
222 .new_library_with_data(SHADERS_METALLIB)
223 .expect("error building metal library");
224
225 fn to_float2_bits(point: PointF) -> u64 {
226 let mut output = point.y.to_bits() as u64;
227 output <<= 32;
228 output |= point.x.to_bits() as u64;
229 output
230 }
231
232 // Shared memory can be used only if CPU and GPU share the same memory space.
233 // https://developer.apple.com/documentation/metal/setting-resource-storage-modes
234 let is_unified_memory = device.has_unified_memory();
235 // Apple GPU families support memoryless textures, which can significantly reduce
236 // memory usage by keeping render targets in on-chip tile memory instead of
237 // allocating backing store in system memory.
238 // https://developer.apple.com/documentation/metal/mtlgpufamily
239 let is_apple_gpu = device.supports_family(MTLGPUFamily::Apple1);
240
241 let unit_vertices = [
242 to_float2_bits(point(0., 0.)),
243 to_float2_bits(point(1., 0.)),
244 to_float2_bits(point(0., 1.)),
245 to_float2_bits(point(0., 1.)),
246 to_float2_bits(point(1., 0.)),
247 to_float2_bits(point(1., 1.)),
248 ];
249 let unit_vertices = device.new_buffer_with_data(
250 unit_vertices.as_ptr() as *const c_void,
251 mem::size_of_val(&unit_vertices) as u64,
252 if is_unified_memory {
253 MTLResourceOptions::StorageModeShared
254 | MTLResourceOptions::CPUCacheModeWriteCombined
255 } else {
256 MTLResourceOptions::StorageModeManaged
257 },
258 );
259
260 let paths_rasterization_pipeline_state = build_path_rasterization_pipeline_state(
261 &device,
262 &library,
263 "paths_rasterization",
264 "path_rasterization_vertex",
265 "path_rasterization_fragment",
266 MTLPixelFormat::BGRA8Unorm,
267 PATH_SAMPLE_COUNT,
268 );
269 let path_sprites_pipeline_state = build_path_sprite_pipeline_state(
270 &device,
271 &library,
272 "path_sprites",
273 "path_sprite_vertex",
274 "path_sprite_fragment",
275 MTLPixelFormat::BGRA8Unorm,
276 );
277 let shadows_pipeline_state = build_pipeline_state(
278 &device,
279 &library,
280 "shadows",
281 "shadow_vertex",
282 "shadow_fragment",
283 MTLPixelFormat::BGRA8Unorm,
284 );
285 let quads_pipeline_state = build_pipeline_state(
286 &device,
287 &library,
288 "quads",
289 "quad_vertex",
290 "quad_fragment",
291 MTLPixelFormat::BGRA8Unorm,
292 );
293 let underlines_pipeline_state = build_pipeline_state(
294 &device,
295 &library,
296 "underlines",
297 "underline_vertex",
298 "underline_fragment",
299 MTLPixelFormat::BGRA8Unorm,
300 );
301 let monochrome_sprites_pipeline_state = build_pipeline_state(
302 &device,
303 &library,
304 "monochrome_sprites",
305 "monochrome_sprite_vertex",
306 "monochrome_sprite_fragment",
307 MTLPixelFormat::BGRA8Unorm,
308 );
309 let polychrome_sprites_pipeline_state = build_pipeline_state(
310 &device,
311 &library,
312 "polychrome_sprites",
313 "polychrome_sprite_vertex",
314 "polychrome_sprite_fragment",
315 MTLPixelFormat::BGRA8Unorm,
316 );
317 let surfaces_pipeline_state = build_pipeline_state(
318 &device,
319 &library,
320 "surfaces",
321 "surface_vertex",
322 "surface_fragment",
323 MTLPixelFormat::BGRA8Unorm,
324 );
325
326 let command_queue = device.new_command_queue();
327 let sprite_atlas = Arc::new(MetalAtlas::new(device.clone(), is_apple_gpu));
328 let core_video_texture_cache =
329 CVMetalTextureCache::new(None, device.clone(), None).unwrap();
330
331 Self {
332 device,
333 layer,
334 presents_with_transaction: false,
335 is_apple_gpu,
336 is_unified_memory,
337 opaque,
338 command_queue,
339 paths_rasterization_pipeline_state,
340 path_sprites_pipeline_state,
341 shadows_pipeline_state,
342 quads_pipeline_state,
343 underlines_pipeline_state,
344 monochrome_sprites_pipeline_state,
345 polychrome_sprites_pipeline_state,
346 surfaces_pipeline_state,
347 unit_vertices,
348 instance_buffer_pool,
349 sprite_atlas,
350 core_video_texture_cache,
351 path_intermediate_texture: None,
352 path_intermediate_msaa_texture: None,
353 path_sample_count: PATH_SAMPLE_COUNT,
354 #[cfg(any(test, feature = "test-support"))]
355 headless_render_target: None,
356 }
357 }
358
359 pub fn layer(&self) -> Option<&metal::MetalLayerRef> {
360 self.layer.as_ref().map(|l| l.as_ref())
361 }
362
363 pub fn layer_ptr(&self) -> *mut CAMetalLayer {
364 self.layer
365 .as_ref()
366 .map(|l| l.as_ptr())
367 .unwrap_or(ptr::null_mut())
368 }
369
370 pub fn sprite_atlas(&self) -> &Arc<MetalAtlas> {
371 &self.sprite_atlas
372 }
373
374 pub fn set_presents_with_transaction(&mut self, presents_with_transaction: bool) {
375 self.presents_with_transaction = presents_with_transaction;
376 if let Some(layer) = &self.layer {
377 layer.set_presents_with_transaction(presents_with_transaction);
378 }
379 }
380
381 pub fn update_drawable_size(&mut self, size: Size<DevicePixels>) {
382 if let Some(layer) = &self.layer {
383 let ns_size = NSSize {
384 width: size.width.0 as f64,
385 height: size.height.0 as f64,
386 };
387 unsafe {
388 let _: () = msg_send![
389 layer.as_ref(),
390 setDrawableSize: ns_size
391 ];
392 }
393 }
394 self.update_path_intermediate_textures(size);
395 }
396
397 fn update_path_intermediate_textures(&mut self, size: Size<DevicePixels>) {
398 // We are uncertain when this happens, but sometimes size can be 0 here. Most likely before
399 // the layout pass on window creation. Zero-sized texture creation causes SIGABRT.
400 // https://github.com/zed-industries/zed/issues/36229
401 if size.width.0 <= 0 || size.height.0 <= 0 {
402 self.path_intermediate_texture = None;
403 self.path_intermediate_msaa_texture = None;
404 return;
405 }
406
407 let texture_descriptor = metal::TextureDescriptor::new();
408 texture_descriptor.set_width(size.width.0 as u64);
409 texture_descriptor.set_height(size.height.0 as u64);
410 texture_descriptor.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm);
411 texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
412 texture_descriptor
413 .set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead);
414 self.path_intermediate_texture = Some(self.device.new_texture(&texture_descriptor));
415
416 if self.path_sample_count > 1 {
417 // https://developer.apple.com/documentation/metal/choosing-a-resource-storage-mode-for-apple-gpus
418 // Rendering MSAA textures are done in a single pass, so we can use memory-less storage on Apple Silicon
419 let storage_mode = if self.is_apple_gpu {
420 metal::MTLStorageMode::Memoryless
421 } else {
422 metal::MTLStorageMode::Private
423 };
424
425 let msaa_descriptor = texture_descriptor;
426 msaa_descriptor.set_texture_type(metal::MTLTextureType::D2Multisample);
427 msaa_descriptor.set_storage_mode(storage_mode);
428 msaa_descriptor.set_sample_count(self.path_sample_count as _);
429 self.path_intermediate_msaa_texture = Some(self.device.new_texture(&msaa_descriptor));
430 } else {
431 self.path_intermediate_msaa_texture = None;
432 }
433 }
434
435 pub fn update_transparency(&mut self, transparent: bool) {
436 self.opaque = !transparent;
437 if let Some(layer) = &self.layer {
438 layer.set_opaque(!transparent);
439 }
440 }
441
442 pub fn destroy(&self) {
443 // nothing to do
444 }
445
446 pub fn draw(&mut self, scene: &Scene) {
447 let layer = match &self.layer {
448 Some(l) => l.clone(),
449 None => {
450 log::error!(
451 "draw() called on headless renderer - use render_scene_to_image() instead"
452 );
453 return;
454 }
455 };
456 let viewport_size = layer.drawable_size();
457 let viewport_size: Size<DevicePixels> = size(
458 (viewport_size.width.ceil() as i32).into(),
459 (viewport_size.height.ceil() as i32).into(),
460 );
461 let drawable = if let Some(drawable) = layer.next_drawable() {
462 drawable
463 } else {
464 log::error!(
465 "failed to retrieve next drawable, drawable size: {:?}",
466 viewport_size
467 );
468 return;
469 };
470
471 loop {
472 let mut instance_buffer = self
473 .instance_buffer_pool
474 .lock()
475 .acquire(&self.device, self.is_unified_memory);
476
477 let command_buffer =
478 self.draw_primitives(scene, &mut instance_buffer, drawable, viewport_size);
479
480 match command_buffer {
481 Ok(command_buffer) => {
482 let instance_buffer_pool = self.instance_buffer_pool.clone();
483 let instance_buffer = Cell::new(Some(instance_buffer));
484 let block = ConcreteBlock::new(move |_| {
485 if let Some(instance_buffer) = instance_buffer.take() {
486 instance_buffer_pool.lock().release(instance_buffer);
487 }
488 });
489 let block = block.copy();
490 command_buffer.add_completed_handler(&block);
491
492 if self.presents_with_transaction {
493 command_buffer.commit();
494 command_buffer.wait_until_scheduled();
495 drawable.present();
496 } else {
497 command_buffer.present_drawable(drawable);
498 command_buffer.commit();
499 }
500 return;
501 }
502 Err(err) => {
503 log::error!(
504 "failed to render: {}. retrying with larger instance buffer size",
505 err
506 );
507 let mut instance_buffer_pool = self.instance_buffer_pool.lock();
508 let buffer_size = instance_buffer_pool.buffer_size;
509 if buffer_size >= 256 * 1024 * 1024 {
510 log::error!("instance buffer size grew too large: {}", buffer_size);
511 break;
512 }
513 instance_buffer_pool.reset(buffer_size * 2);
514 log::info!(
515 "increased instance buffer size to {}",
516 instance_buffer_pool.buffer_size
517 );
518 }
519 }
520 }
521 }
522
523 /// Renders the scene to a texture and returns the pixel data as an RGBA image.
524 /// This does not present the frame to screen - useful for visual testing
525 /// where we want to capture what would be rendered without displaying it.
526 ///
527 /// Note: This requires a layer-backed renderer. For headless rendering,
528 /// use `render_scene_to_image()` instead.
529 #[cfg(any(test, feature = "test-support"))]
530 pub fn render_to_image(&mut self, scene: &Scene) -> Result<RgbaImage> {
531 let layer = self
532 .layer
533 .clone()
534 .ok_or_else(|| anyhow::anyhow!("render_to_image requires a layer-backed renderer"))?;
535 let viewport_size = layer.drawable_size();
536 let viewport_size: Size<DevicePixels> = size(
537 (viewport_size.width.ceil() as i32).into(),
538 (viewport_size.height.ceil() as i32).into(),
539 );
540 let drawable = layer
541 .next_drawable()
542 .ok_or_else(|| anyhow::anyhow!("Failed to get drawable for render_to_image"))?;
543
544 loop {
545 let mut instance_buffer = self
546 .instance_buffer_pool
547 .lock()
548 .acquire(&self.device, self.is_unified_memory);
549
550 let command_buffer =
551 self.draw_primitives(scene, &mut instance_buffer, drawable, viewport_size);
552
553 match command_buffer {
554 Ok(command_buffer) => {
555 let instance_buffer_pool = self.instance_buffer_pool.clone();
556 let instance_buffer = Cell::new(Some(instance_buffer));
557 let block = ConcreteBlock::new(move |_| {
558 if let Some(instance_buffer) = instance_buffer.take() {
559 instance_buffer_pool.lock().release(instance_buffer);
560 }
561 });
562 let block = block.copy();
563 command_buffer.add_completed_handler(&block);
564
565 // Commit and wait for completion without presenting
566 command_buffer.commit();
567 command_buffer.wait_until_completed();
568
569 // Read pixels from the texture
570 let texture = drawable.texture();
571 let width = texture.width() as u32;
572 let height = texture.height() as u32;
573 let bytes_per_row = width as usize * 4;
574 let buffer_size = height as usize * bytes_per_row;
575
576 let mut pixels = vec![0u8; buffer_size];
577
578 let region = metal::MTLRegion {
579 origin: metal::MTLOrigin { x: 0, y: 0, z: 0 },
580 size: metal::MTLSize {
581 width: width as u64,
582 height: height as u64,
583 depth: 1,
584 },
585 };
586
587 texture.get_bytes(
588 pixels.as_mut_ptr() as *mut std::ffi::c_void,
589 bytes_per_row as u64,
590 region,
591 0,
592 );
593
594 // Convert BGRA to RGBA (swap B and R channels)
595 for chunk in pixels.chunks_exact_mut(4) {
596 chunk.swap(0, 2);
597 }
598
599 return RgbaImage::from_raw(width, height, pixels).ok_or_else(|| {
600 anyhow::anyhow!("Failed to create RgbaImage from pixel data")
601 });
602 }
603 Err(err) => {
604 log::error!(
605 "failed to render: {}. retrying with larger instance buffer size",
606 err
607 );
608 let mut instance_buffer_pool = self.instance_buffer_pool.lock();
609 let buffer_size = instance_buffer_pool.buffer_size;
610 if buffer_size >= 256 * 1024 * 1024 {
611 anyhow::bail!("instance buffer size grew too large: {}", buffer_size);
612 }
613 instance_buffer_pool.reset(buffer_size * 2);
614 log::info!(
615 "increased instance buffer size to {}",
616 instance_buffer_pool.buffer_size
617 );
618 }
619 }
620 }
621 }
622
623 /// Renders a scene to an image without requiring a window or CAMetalLayer.
624 ///
625 /// This is the primary method for headless rendering. It creates an offscreen
626 /// texture, renders the scene to it, and returns the pixel data as an RGBA image.
627 #[cfg(any(test, feature = "test-support"))]
628 pub fn render_scene_to_image(
629 &mut self,
630 scene: &Scene,
631 size: Size<DevicePixels>,
632 ) -> Result<RgbaImage> {
633 if size.width.0 <= 0 || size.height.0 <= 0 {
634 anyhow::bail!("Invalid size for render_scene_to_image: {:?}", size);
635 }
636
637 // Update path intermediate textures for this size
638 self.update_path_intermediate_textures(size);
639
640 // Create an offscreen texture as render target
641 let texture_descriptor = metal::TextureDescriptor::new();
642 texture_descriptor.set_width(size.width.0 as u64);
643 texture_descriptor.set_height(size.height.0 as u64);
644 texture_descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
645 texture_descriptor
646 .set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead);
647 texture_descriptor.set_storage_mode(metal::MTLStorageMode::Managed);
648 let target_texture = self.device.new_texture(&texture_descriptor);
649
650 loop {
651 let mut instance_buffer = self
652 .instance_buffer_pool
653 .lock()
654 .acquire(&self.device, self.is_unified_memory);
655
656 let command_buffer =
657 self.draw_primitives_to_texture(scene, &mut instance_buffer, &target_texture, size);
658
659 match command_buffer {
660 Ok(command_buffer) => {
661 let instance_buffer_pool = self.instance_buffer_pool.clone();
662 let instance_buffer = Cell::new(Some(instance_buffer));
663 let block = ConcreteBlock::new(move |_| {
664 if let Some(instance_buffer) = instance_buffer.take() {
665 instance_buffer_pool.lock().release(instance_buffer);
666 }
667 });
668 let block = block.copy();
669 command_buffer.add_completed_handler(&block);
670
671 // On discrete GPUs (non-unified memory), Managed textures
672 // require an explicit blit synchronize before the CPU can
673 // read back the rendered data. Without this, get_bytes
674 // returns stale zeros.
675 if !self.is_unified_memory {
676 let blit = command_buffer.new_blit_command_encoder();
677 blit.synchronize_resource(&target_texture);
678 blit.end_encoding();
679 }
680
681 // Commit and wait for completion
682 command_buffer.commit();
683 command_buffer.wait_until_completed();
684
685 // Read pixels from the texture
686 let width = size.width.0 as u32;
687 let height = size.height.0 as u32;
688 let bytes_per_row = width as usize * 4;
689 let buffer_size = height as usize * bytes_per_row;
690
691 let mut pixels = vec![0u8; buffer_size];
692
693 let region = metal::MTLRegion {
694 origin: metal::MTLOrigin { x: 0, y: 0, z: 0 },
695 size: metal::MTLSize {
696 width: width as u64,
697 height: height as u64,
698 depth: 1,
699 },
700 };
701
702 target_texture.get_bytes(
703 pixels.as_mut_ptr() as *mut std::ffi::c_void,
704 bytes_per_row as u64,
705 region,
706 0,
707 );
708
709 // Convert BGRA to RGBA (swap B and R channels)
710 for chunk in pixels.chunks_exact_mut(4) {
711 chunk.swap(0, 2);
712 }
713
714 return RgbaImage::from_raw(width, height, pixels).ok_or_else(|| {
715 anyhow::anyhow!("Failed to create RgbaImage from pixel data")
716 });
717 }
718 Err(err) => {
719 log::error!(
720 "failed to render: {}. retrying with larger instance buffer size",
721 err
722 );
723 let mut instance_buffer_pool = self.instance_buffer_pool.lock();
724 let buffer_size = instance_buffer_pool.buffer_size;
725 if buffer_size >= 256 * 1024 * 1024 {
726 anyhow::bail!("instance buffer size grew too large: {}", buffer_size);
727 }
728 instance_buffer_pool.reset(buffer_size * 2);
729 log::info!(
730 "increased instance buffer size to {}",
731 instance_buffer_pool.buffer_size
732 );
733 }
734 }
735 }
736 }
737
738 /// Renders a scene to a reused offscreen texture without reading pixels
739 /// back or blocking on GPU completion.
740 ///
741 /// This mirrors the CPU cost of presenting a frame to a window (scene
742 /// encoding, instance buffer writes, command submission) and is used by
743 /// headless benchmark rendering, where the produced pixels are never
744 /// inspected.
745 #[cfg(any(test, feature = "test-support"))]
746 pub fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()> {
747 if size.width.0 <= 0 || size.height.0 <= 0 {
748 anyhow::bail!("Invalid size for render_scene: {:?}", size);
749 }
750
751 self.update_path_intermediate_textures(size);
752
753 let needs_new_target = self.headless_render_target.as_ref().is_none_or(|texture| {
754 texture.width() != size.width.0 as u64 || texture.height() != size.height.0 as u64
755 });
756 if needs_new_target {
757 let texture_descriptor = metal::TextureDescriptor::new();
758 texture_descriptor.set_width(size.width.0 as u64);
759 texture_descriptor.set_height(size.height.0 as u64);
760 texture_descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
761 texture_descriptor.set_usage(
762 metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead,
763 );
764 texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
765 self.headless_render_target = Some(self.device.new_texture(&texture_descriptor));
766 }
767 let target_texture = self
768 .headless_render_target
769 .clone()
770 .expect("just ensured the render target exists");
771
772 loop {
773 let mut instance_buffer = self
774 .instance_buffer_pool
775 .lock()
776 .acquire(&self.device, self.is_unified_memory);
777
778 let command_buffer =
779 self.draw_primitives_to_texture(scene, &mut instance_buffer, &target_texture, size);
780
781 match command_buffer {
782 Ok(command_buffer) => {
783 let instance_buffer_pool = self.instance_buffer_pool.clone();
784 let instance_buffer = Cell::new(Some(instance_buffer));
785 let block = ConcreteBlock::new(move |_| {
786 if let Some(instance_buffer) = instance_buffer.take() {
787 instance_buffer_pool.lock().release(instance_buffer);
788 }
789 });
790 let block = block.copy();
791 command_buffer.add_completed_handler(&block);
792
793 // Commit without waiting, mirroring presentation to a real
794 // window where the CPU doesn't block on the GPU.
795 command_buffer.commit();
796 return Ok(());
797 }
798 Err(err) => {
799 log::error!(
800 "failed to render: {}. retrying with larger instance buffer size",
801 err
802 );
803 let mut instance_buffer_pool = self.instance_buffer_pool.lock();
804 let buffer_size = instance_buffer_pool.buffer_size;
805 if buffer_size >= 256 * 1024 * 1024 {
806 anyhow::bail!("instance buffer size grew too large: {}", buffer_size);
807 }
808 instance_buffer_pool.reset(buffer_size * 2);
809 log::info!(
810 "increased instance buffer size to {}",
811 instance_buffer_pool.buffer_size
812 );
813 }
814 }
815 }
816 }
817
818 fn draw_primitives(
819 &mut self,
820 scene: &Scene,
821 instance_buffer: &mut InstanceBuffer,
822 drawable: &metal::MetalDrawableRef,
823 viewport_size: Size<DevicePixels>,
824 ) -> Result<metal::CommandBuffer> {
825 self.draw_primitives_to_texture(scene, instance_buffer, drawable.texture(), viewport_size)
826 }
827
828 fn draw_primitives_to_texture(
829 &mut self,
830 scene: &Scene,
831 instance_buffer: &mut InstanceBuffer,
832 texture: &metal::TextureRef,
833 viewport_size: Size<DevicePixels>,
834 ) -> Result<metal::CommandBuffer> {
835 let command_queue = self.command_queue.clone();
836 let command_buffer = command_queue.new_command_buffer();
837 let alpha = if self.opaque { 1. } else { 0. };
838 let mut instance_offset = 0;
839
840 let mut command_encoder = new_command_encoder_for_texture(
841 command_buffer,
842 texture,
843 viewport_size,
844 |color_attachment| {
845 color_attachment.set_load_action(metal::MTLLoadAction::Clear);
846 color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha));
847 },
848 );
849
850 for batch in scene.batches() {
851 let ok = match batch {
852 PrimitiveBatch::Shadows(range) => self.draw_shadows(
853 &scene.shadows[range],
854 instance_buffer,
855 &mut instance_offset,
856 viewport_size,
857 command_encoder,
858 ),
859 PrimitiveBatch::Quads(range) => self.draw_quads(
860 &scene.quads[range],
861 instance_buffer,
862 &mut instance_offset,
863 viewport_size,
864 command_encoder,
865 ),
866 PrimitiveBatch::Paths(range) => {
867 let paths = &scene.paths[range];
868 command_encoder.end_encoding();
869
870 let did_draw = self.draw_paths_to_intermediate(
871 paths,
872 instance_buffer,
873 &mut instance_offset,
874 viewport_size,
875 command_buffer,
876 );
877
878 command_encoder = new_command_encoder_for_texture(
879 command_buffer,
880 texture,
881 viewport_size,
882 |color_attachment| {
883 color_attachment.set_load_action(metal::MTLLoadAction::Load);
884 },
885 );
886
887 if did_draw {
888 self.draw_paths_from_intermediate(
889 paths,
890 instance_buffer,
891 &mut instance_offset,
892 viewport_size,
893 command_encoder,
894 )
895 } else {
896 false
897 }
898 }
899 PrimitiveBatch::Underlines(range) => self.draw_underlines(
900 &scene.underlines[range],
901 instance_buffer,
902 &mut instance_offset,
903 viewport_size,
904 command_encoder,
905 ),
906 PrimitiveBatch::MonochromeSprites { texture_id, range } => self
907 .draw_monochrome_sprites(
908 texture_id,
909 &scene.monochrome_sprites[range],
910 instance_buffer,
911 &mut instance_offset,
912 viewport_size,
913 command_encoder,
914 ),
915 PrimitiveBatch::PolychromeSprites { texture_id, range } => self
916 .draw_polychrome_sprites(
917 texture_id,
918 &scene.polychrome_sprites[range],
919 instance_buffer,
920 &mut instance_offset,
921 viewport_size,
922 command_encoder,
923 ),
924 PrimitiveBatch::Surfaces(range) => self.draw_surfaces(
925 &scene.surfaces[range],
926 instance_buffer,
927 &mut instance_offset,
928 viewport_size,
929 command_encoder,
930 ),
931 PrimitiveBatch::SubpixelSprites { .. } => unreachable!(),
932 };
933 if !ok {
934 command_encoder.end_encoding();
935 anyhow::bail!(
936 "scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
937 scene.paths.len(),
938 scene.shadows.len(),
939 scene.quads.len(),
940 scene.underlines.len(),
941 scene.monochrome_sprites.len(),
942 scene.polychrome_sprites.len(),
943 scene.surfaces.len(),
944 );
945 }
946 }
947
948 command_encoder.end_encoding();
949
950 if !self.is_unified_memory {
951 // Sync the instance buffer to the GPU
952 instance_buffer.metal_buffer.did_modify_range(NSRange {
953 location: 0,
954 length: instance_offset as NSUInteger,
955 });
956 }
957
958 Ok(command_buffer.to_owned())
959 }
960
961 fn draw_paths_to_intermediate(
962 &self,
963 paths: &[Path<ScaledPixels>],
964 instance_buffer: &mut InstanceBuffer,
965 instance_offset: &mut usize,
966 viewport_size: Size<DevicePixels>,
967 command_buffer: &metal::CommandBufferRef,
968 ) -> bool {
969 if paths.is_empty() {
970 return true;
971 }
972 let Some(intermediate_texture) = &self.path_intermediate_texture else {
973 return false;
974 };
975
976 let render_pass_descriptor = metal::RenderPassDescriptor::new();
977 let color_attachment = render_pass_descriptor
978 .color_attachments()
979 .object_at(0)
980 .unwrap();
981 color_attachment.set_load_action(metal::MTLLoadAction::Clear);
982 color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 0.));
983
984 if let Some(msaa_texture) = &self.path_intermediate_msaa_texture {
985 color_attachment.set_texture(Some(msaa_texture));
986 color_attachment.set_resolve_texture(Some(intermediate_texture));
987 color_attachment.set_store_action(metal::MTLStoreAction::MultisampleResolve);
988 } else {
989 color_attachment.set_texture(Some(intermediate_texture));
990 color_attachment.set_store_action(metal::MTLStoreAction::Store);
991 }
992
993 let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
994 command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state);
995
996 align_offset(instance_offset);
997 let mut vertices = Vec::new();
998 for path in paths {
999 vertices.extend(path.vertices.iter().map(|v| PathRasterizationVertex {
1000 xy_position: v.xy_position,
1001 st_position: v.st_position,
1002 color: path.color,
1003 bounds: path.bounds.intersect(&path.content_mask.bounds),
1004 }));
1005 }
1006 let vertices_bytes_len = mem::size_of_val(vertices.as_slice());
1007 let next_offset = *instance_offset + vertices_bytes_len;
1008 if next_offset > instance_buffer.size {
1009 command_encoder.end_encoding();
1010 return false;
1011 }
1012 command_encoder.set_vertex_buffer(
1013 PathRasterizationInputIndex::Vertices as u64,
1014 Some(&instance_buffer.metal_buffer),
1015 *instance_offset as u64,
1016 );
1017 command_encoder.set_vertex_bytes(
1018 PathRasterizationInputIndex::ViewportSize as u64,
1019 mem::size_of_val(&viewport_size) as u64,
1020 &viewport_size as *const Size<DevicePixels> as *const _,
1021 );
1022 command_encoder.set_fragment_buffer(
1023 PathRasterizationInputIndex::Vertices as u64,
1024 Some(&instance_buffer.metal_buffer),
1025 *instance_offset as u64,
1026 );
1027 let buffer_contents =
1028 unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) };
1029 unsafe {
1030 ptr::copy_nonoverlapping(
1031 vertices.as_ptr() as *const u8,
1032 buffer_contents,
1033 vertices_bytes_len,
1034 );
1035 }
1036 command_encoder.draw_primitives(
1037 metal::MTLPrimitiveType::Triangle,
1038 0,
1039 vertices.len() as u64,
1040 );
1041 *instance_offset = next_offset;
1042
1043 command_encoder.end_encoding();
1044 true
1045 }
1046
1047 fn draw_shadows(
1048 &self,
1049 shadows: &[Shadow],
1050 instance_buffer: &mut InstanceBuffer,
1051 instance_offset: &mut usize,
1052 viewport_size: Size<DevicePixels>,
1053 command_encoder: &metal::RenderCommandEncoderRef,
1054 ) -> bool {
1055 if shadows.is_empty() {
1056 return true;
1057 }
1058 align_offset(instance_offset);
1059
1060 command_encoder.set_render_pipeline_state(&self.shadows_pipeline_state);
1061 command_encoder.set_vertex_buffer(
1062 ShadowInputIndex::Vertices as u64,
1063 Some(&self.unit_vertices),
1064 0,
1065 );
1066 command_encoder.set_vertex_buffer(
1067 ShadowInputIndex::Shadows as u64,
1068 Some(&instance_buffer.metal_buffer),
1069 *instance_offset as u64,
1070 );
1071 command_encoder.set_fragment_buffer(
1072 ShadowInputIndex::Shadows as u64,
1073 Some(&instance_buffer.metal_buffer),
1074 *instance_offset as u64,
1075 );
1076
1077 command_encoder.set_vertex_bytes(
1078 ShadowInputIndex::ViewportSize as u64,
1079 mem::size_of_val(&viewport_size) as u64,
1080 &viewport_size as *const Size<DevicePixels> as *const _,
1081 );
1082
1083 let shadow_bytes_len = mem::size_of_val(shadows);
1084 let buffer_contents =
1085 unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) };
1086
1087 let next_offset = *instance_offset + shadow_bytes_len;
1088 if next_offset > instance_buffer.size {
1089 return false;
1090 }
1091
1092 unsafe {
1093 ptr::copy_nonoverlapping(
1094 shadows.as_ptr() as *const u8,
1095 buffer_contents,
1096 shadow_bytes_len,
1097 );
1098 }
1099
1100 command_encoder.draw_primitives_instanced(
1101 metal::MTLPrimitiveType::Triangle,
1102 0,
1103 6,
1104 shadows.len() as u64,
1105 );
1106 *instance_offset = next_offset;
1107 true
1108 }
1109
1110 fn draw_quads(
1111 &self,
1112 quads: &[Quad],
1113 instance_buffer: &mut InstanceBuffer,
1114 instance_offset: &mut usize,
1115 viewport_size: Size<DevicePixels>,
1116 command_encoder: &metal::RenderCommandEncoderRef,
1117 ) -> bool {
1118 if quads.is_empty() {
1119 return true;
1120 }
1121 align_offset(instance_offset);
1122
1123 command_encoder.set_render_pipeline_state(&self.quads_pipeline_state);
1124 command_encoder.set_vertex_buffer(
1125 QuadInputIndex::Vertices as u64,
1126 Some(&self.unit_vertices),
1127 0,
1128 );
1129 command_encoder.set_vertex_buffer(
1130 QuadInputIndex::Quads as u64,
1131 Some(&instance_buffer.metal_buffer),
1132 *instance_offset as u64,
1133 );
1134 command_encoder.set_fragment_buffer(
1135 QuadInputIndex::Quads as u64,
1136 Some(&instance_buffer.metal_buffer),
1137 *instance_offset as u64,
1138 );
1139
1140 command_encoder.set_vertex_bytes(
1141 QuadInputIndex::ViewportSize as u64,
1142 mem::size_of_val(&viewport_size) as u64,
1143 &viewport_size as *const Size<DevicePixels> as *const _,
1144 );
1145
1146 let quad_bytes_len = mem::size_of_val(quads);
1147 let buffer_contents =
1148 unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) };
1149
1150 let next_offset = *instance_offset + quad_bytes_len;
1151 if next_offset > instance_buffer.size {
1152 return false;
1153 }
1154
1155 unsafe {
1156 ptr::copy_nonoverlapping(quads.as_ptr() as *const u8, buffer_contents, quad_bytes_len);
1157 }
1158
1159 command_encoder.draw_primitives_instanced(
1160 metal::MTLPrimitiveType::Triangle,
1161 0,
1162 6,
1163 quads.len() as u64,
1164 );
1165 *instance_offset = next_offset;
1166 true
1167 }
1168
1169 fn draw_paths_from_intermediate(
1170 &self,
1171 paths: &[Path<ScaledPixels>],
1172 instance_buffer: &mut InstanceBuffer,
1173 instance_offset: &mut usize,
1174 viewport_size: Size<DevicePixels>,
1175 command_encoder: &metal::RenderCommandEncoderRef,
1176 ) -> bool {
1177 let Some(first_path) = paths.first() else {
1178 return true;
1179 };
1180
1181 let Some(ref intermediate_texture) = self.path_intermediate_texture else {
1182 return false;
1183 };
1184
1185 command_encoder.set_render_pipeline_state(&self.path_sprites_pipeline_state);
1186 command_encoder.set_vertex_buffer(
1187 SpriteInputIndex::Vertices as u64,
1188 Some(&self.unit_vertices),
1189 0,
1190 );
1191 command_encoder.set_vertex_bytes(
1192 SpriteInputIndex::ViewportSize as u64,
1193 mem::size_of_val(&viewport_size) as u64,
1194 &viewport_size as *const Size<DevicePixels> as *const _,
1195 );
1196
1197 command_encoder.set_fragment_texture(
1198 SpriteInputIndex::AtlasTexture as u64,
1199 Some(intermediate_texture),
1200 );
1201
1202 // When copying paths from the intermediate texture to the drawable,
1203 // each pixel must only be copied once, in case of transparent paths.
1204 //
1205 // If all paths have the same draw order, then their bounds are all
1206 // disjoint, so we can copy each path's bounds individually. If this
1207 // batch combines different draw orders, we perform a single copy
1208 // for a minimal spanning rect.
1209 let sprites;
1210 if paths.last().unwrap().order == first_path.order {
1211 sprites = paths
1212 .iter()
1213 .map(|path| PathSprite {
1214 bounds: path.clipped_bounds(),
1215 })
1216 .collect();
1217 } else {
1218 let mut bounds = first_path.clipped_bounds();
1219 for path in paths.iter().skip(1) {
1220 bounds = bounds.union(&path.clipped_bounds());
1221 }
1222 sprites = vec![PathSprite { bounds }];
1223 }
1224
1225 align_offset(instance_offset);
1226 let sprite_bytes_len = mem::size_of_val(sprites.as_slice());
1227 let next_offset = *instance_offset + sprite_bytes_len;
1228 if next_offset > instance_buffer.size {
1229 return false;
1230 }
1231
1232 command_encoder.set_vertex_buffer(
1233 SpriteInputIndex::Sprites as u64,
1234 Some(&instance_buffer.metal_buffer),
1235 *instance_offset as u64,
1236 );
1237
1238 let buffer_contents =
1239 unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) };
1240 unsafe {
1241 ptr::copy_nonoverlapping(
1242 sprites.as_ptr() as *const u8,
1243 buffer_contents,
1244 sprite_bytes_len,
1245 );
1246 }
1247
1248 command_encoder.draw_primitives_instanced(
1249 metal::MTLPrimitiveType::Triangle,
1250 0,
1251 6,
1252 sprites.len() as u64,
1253 );
1254 *instance_offset = next_offset;
1255
1256 true
1257 }
1258
1259 fn draw_underlines(
1260 &self,
1261 underlines: &[Underline],
1262 instance_buffer: &mut InstanceBuffer,
1263 instance_offset: &mut usize,
1264 viewport_size: Size<DevicePixels>,
1265 command_encoder: &metal::RenderCommandEncoderRef,
1266 ) -> bool {
1267 if underlines.is_empty() {
1268 return true;
1269 }
1270 align_offset(instance_offset);
1271
1272 command_encoder.set_render_pipeline_state(&self.underlines_pipeline_state);
1273 command_encoder.set_vertex_buffer(
1274 UnderlineInputIndex::Vertices as u64,
1275 Some(&self.unit_vertices),
1276 0,
1277 );
1278 command_encoder.set_vertex_buffer(
1279 UnderlineInputIndex::Underlines as u64,
1280 Some(&instance_buffer.metal_buffer),
1281 *instance_offset as u64,
1282 );
1283 command_encoder.set_fragment_buffer(
1284 UnderlineInputIndex::Underlines as u64,
1285 Some(&instance_buffer.metal_buffer),
1286 *instance_offset as u64,
1287 );
1288
1289 command_encoder.set_vertex_bytes(
1290 UnderlineInputIndex::ViewportSize as u64,
1291 mem::size_of_val(&viewport_size) as u64,
1292 &viewport_size as *const Size<DevicePixels> as *const _,
1293 );
1294
1295 let underline_bytes_len = mem::size_of_val(underlines);
1296 let buffer_contents =
1297 unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) };
1298
1299 let next_offset = *instance_offset + underline_bytes_len;
1300 if next_offset > instance_buffer.size {
1301 return false;
1302 }
1303
1304 unsafe {
1305 ptr::copy_nonoverlapping(
1306 underlines.as_ptr() as *const u8,
1307 buffer_contents,
1308 underline_bytes_len,
1309 );
1310 }
1311
1312 command_encoder.draw_primitives_instanced(
1313 metal::MTLPrimitiveType::Triangle,
1314 0,
1315 6,
1316 underlines.len() as u64,
1317 );
1318 *instance_offset = next_offset;
1319 true
1320 }
1321
1322 fn draw_monochrome_sprites(
1323 &self,
1324 texture_id: AtlasTextureId,
1325 sprites: &[MonochromeSprite],
1326 instance_buffer: &mut InstanceBuffer,
1327 instance_offset: &mut usize,
1328 viewport_size: Size<DevicePixels>,
1329 command_encoder: &metal::RenderCommandEncoderRef,
1330 ) -> bool {
1331 if sprites.is_empty() {
1332 return true;
1333 }
1334 align_offset(instance_offset);
1335
1336 let sprite_bytes_len = mem::size_of_val(sprites);
1337 let buffer_contents =
1338 unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) };
1339
1340 let next_offset = *instance_offset + sprite_bytes_len;
1341 if next_offset > instance_buffer.size {
1342 return false;
1343 }
1344
1345 let texture = self.sprite_atlas.metal_texture(texture_id);
1346 let texture_size = size(
1347 DevicePixels(texture.width() as i32),
1348 DevicePixels(texture.height() as i32),
1349 );
1350 command_encoder.set_render_pipeline_state(&self.monochrome_sprites_pipeline_state);
1351 command_encoder.set_vertex_buffer(
1352 SpriteInputIndex::Vertices as u64,
1353 Some(&self.unit_vertices),
1354 0,
1355 );
1356 command_encoder.set_vertex_buffer(
1357 SpriteInputIndex::Sprites as u64,
1358 Some(&instance_buffer.metal_buffer),
1359 *instance_offset as u64,
1360 );
1361 command_encoder.set_vertex_bytes(
1362 SpriteInputIndex::ViewportSize as u64,
1363 mem::size_of_val(&viewport_size) as u64,
1364 &viewport_size as *const Size<DevicePixels> as *const _,
1365 );
1366 command_encoder.set_vertex_bytes(
1367 SpriteInputIndex::AtlasTextureSize as u64,
1368 mem::size_of_val(&texture_size) as u64,
1369 &texture_size as *const Size<DevicePixels> as *const _,
1370 );
1371 command_encoder.set_fragment_buffer(
1372 SpriteInputIndex::Sprites as u64,
1373 Some(&instance_buffer.metal_buffer),
1374 *instance_offset as u64,
1375 );
1376 command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
1377
1378 unsafe {
1379 ptr::copy_nonoverlapping(
1380 sprites.as_ptr() as *const u8,
1381 buffer_contents,
1382 sprite_bytes_len,
1383 );
1384 }
1385
1386 command_encoder.draw_primitives_instanced(
1387 metal::MTLPrimitiveType::Triangle,
1388 0,
1389 6,
1390 sprites.len() as u64,
1391 );
1392 *instance_offset = next_offset;
1393 true
1394 }
1395
1396 fn draw_polychrome_sprites(
1397 &self,
1398 texture_id: AtlasTextureId,
1399 sprites: &[PolychromeSprite],
1400 instance_buffer: &mut InstanceBuffer,
1401 instance_offset: &mut usize,
1402 viewport_size: Size<DevicePixels>,
1403 command_encoder: &metal::RenderCommandEncoderRef,
1404 ) -> bool {
1405 if sprites.is_empty() {
1406 return true;
1407 }
1408 align_offset(instance_offset);
1409
1410 let texture = self.sprite_atlas.metal_texture(texture_id);
1411 let texture_size = size(
1412 DevicePixels(texture.width() as i32),
1413 DevicePixels(texture.height() as i32),
1414 );
1415 command_encoder.set_render_pipeline_state(&self.polychrome_sprites_pipeline_state);
1416 command_encoder.set_vertex_buffer(
1417 SpriteInputIndex::Vertices as u64,
1418 Some(&self.unit_vertices),
1419 0,
1420 );
1421 command_encoder.set_vertex_buffer(
1422 SpriteInputIndex::Sprites as u64,
1423 Some(&instance_buffer.metal_buffer),
1424 *instance_offset as u64,
1425 );
1426 command_encoder.set_vertex_bytes(
1427 SpriteInputIndex::ViewportSize as u64,
1428 mem::size_of_val(&viewport_size) as u64,
1429 &viewport_size as *const Size<DevicePixels> as *const _,
1430 );
1431 command_encoder.set_vertex_bytes(
1432 SpriteInputIndex::AtlasTextureSize as u64,
1433 mem::size_of_val(&texture_size) as u64,
1434 &texture_size as *const Size<DevicePixels> as *const _,
1435 );
1436 command_encoder.set_fragment_buffer(
1437 SpriteInputIndex::Sprites as u64,
1438 Some(&instance_buffer.metal_buffer),
1439 *instance_offset as u64,
1440 );
1441 command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
1442
1443 let sprite_bytes_len = mem::size_of_val(sprites);
1444 let buffer_contents =
1445 unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) };
1446
1447 let next_offset = *instance_offset + sprite_bytes_len;
1448 if next_offset > instance_buffer.size {
1449 return false;
1450 }
1451
1452 unsafe {
1453 ptr::copy_nonoverlapping(
1454 sprites.as_ptr() as *const u8,
1455 buffer_contents,
1456 sprite_bytes_len,
1457 );
1458 }
1459
1460 command_encoder.draw_primitives_instanced(
1461 metal::MTLPrimitiveType::Triangle,
1462 0,
1463 6,
1464 sprites.len() as u64,
1465 );
1466 *instance_offset = next_offset;
1467 true
1468 }
1469
1470 fn draw_surfaces(
1471 &mut self,
1472 surfaces: &[PaintSurface],
1473 instance_buffer: &mut InstanceBuffer,
1474 instance_offset: &mut usize,
1475 viewport_size: Size<DevicePixels>,
1476 command_encoder: &metal::RenderCommandEncoderRef,
1477 ) -> bool {
1478 command_encoder.set_render_pipeline_state(&self.surfaces_pipeline_state);
1479 command_encoder.set_vertex_buffer(
1480 SurfaceInputIndex::Vertices as u64,
1481 Some(&self.unit_vertices),
1482 0,
1483 );
1484 command_encoder.set_vertex_bytes(
1485 SurfaceInputIndex::ViewportSize as u64,
1486 mem::size_of_val(&viewport_size) as u64,
1487 &viewport_size as *const Size<DevicePixels> as *const _,
1488 );
1489
1490 for surface in surfaces {
1491 let texture_size = size(
1492 DevicePixels::from(surface.image_buffer.get_width() as i32),
1493 DevicePixels::from(surface.image_buffer.get_height() as i32),
1494 );
1495
1496 assert_eq!(
1497 surface.image_buffer.get_pixel_format(),
1498 kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
1499 );
1500
1501 let y_texture = self
1502 .core_video_texture_cache
1503 .create_texture_from_image(
1504 surface.image_buffer.as_concrete_TypeRef(),
1505 None,
1506 MTLPixelFormat::R8Unorm,
1507 surface.image_buffer.get_width_of_plane(0),
1508 surface.image_buffer.get_height_of_plane(0),
1509 0,
1510 )
1511 .unwrap();
1512 let cb_cr_texture = self
1513 .core_video_texture_cache
1514 .create_texture_from_image(
1515 surface.image_buffer.as_concrete_TypeRef(),
1516 None,
1517 MTLPixelFormat::RG8Unorm,
1518 surface.image_buffer.get_width_of_plane(1),
1519 surface.image_buffer.get_height_of_plane(1),
1520 1,
1521 )
1522 .unwrap();
1523
1524 align_offset(instance_offset);
1525 let next_offset = *instance_offset + mem::size_of::<Surface>();
1526 if next_offset > instance_buffer.size {
1527 return false;
1528 }
1529
1530 command_encoder.set_vertex_buffer(
1531 SurfaceInputIndex::Surfaces as u64,
1532 Some(&instance_buffer.metal_buffer),
1533 *instance_offset as u64,
1534 );
1535 command_encoder.set_vertex_bytes(
1536 SurfaceInputIndex::TextureSize as u64,
1537 mem::size_of_val(&texture_size) as u64,
1538 &texture_size as *const Size<DevicePixels> as *const _,
1539 );
1540 // let y_texture = y_texture.get_texture().unwrap().
1541 command_encoder.set_fragment_texture(SurfaceInputIndex::YTexture as u64, unsafe {
1542 let texture = CVMetalTextureGetTexture(y_texture.as_concrete_TypeRef());
1543 Some(metal::TextureRef::from_ptr(texture as *mut _))
1544 });
1545 command_encoder.set_fragment_texture(SurfaceInputIndex::CbCrTexture as u64, unsafe {
1546 let texture = CVMetalTextureGetTexture(cb_cr_texture.as_concrete_TypeRef());
1547 Some(metal::TextureRef::from_ptr(texture as *mut _))
1548 });
1549
1550 unsafe {
1551 let buffer_contents = (instance_buffer.metal_buffer.contents() as *mut u8)
1552 .add(*instance_offset)
1553 as *mut SurfaceBounds;
1554 ptr::write(
1555 buffer_contents,
1556 SurfaceBounds {
1557 bounds: surface.bounds,
1558 content_mask: surface.content_mask,
1559 },
1560 );
1561 }
1562
1563 command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6);
1564 *instance_offset = next_offset;
1565 }
1566 true
1567 }
1568}
1569
1570fn new_command_encoder_for_texture<'a>(
1571 command_buffer: &'a metal::CommandBufferRef,
1572 texture: &'a metal::TextureRef,
1573 viewport_size: Size<DevicePixels>,
1574 configure_color_attachment: impl Fn(&RenderPassColorAttachmentDescriptorRef),
1575) -> &'a metal::RenderCommandEncoderRef {
1576 let render_pass_descriptor = metal::RenderPassDescriptor::new();
1577 let color_attachment = render_pass_descriptor
1578 .color_attachments()
1579 .object_at(0)
1580 .unwrap();
1581 color_attachment.set_texture(Some(texture));
1582 color_attachment.set_store_action(metal::MTLStoreAction::Store);
1583 configure_color_attachment(color_attachment);
1584
1585 let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
1586 command_encoder.set_viewport(metal::MTLViewport {
1587 originX: 0.0,
1588 originY: 0.0,
1589 width: i32::from(viewport_size.width) as f64,
1590 height: i32::from(viewport_size.height) as f64,
1591 znear: 0.0,
1592 zfar: 1.0,
1593 });
1594 command_encoder
1595}
1596
1597fn build_pipeline_state(
1598 device: &metal::DeviceRef,
1599 library: &metal::LibraryRef,
1600 label: &str,
1601 vertex_fn_name: &str,
1602 fragment_fn_name: &str,
1603 pixel_format: metal::MTLPixelFormat,
1604) -> metal::RenderPipelineState {
1605 let vertex_fn = library
1606 .get_function(vertex_fn_name, None)
1607 .expect("error locating vertex function");
1608 let fragment_fn = library
1609 .get_function(fragment_fn_name, None)
1610 .expect("error locating fragment function");
1611
1612 let descriptor = metal::RenderPipelineDescriptor::new();
1613 descriptor.set_label(label);
1614 descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
1615 descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
1616 let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
1617 color_attachment.set_pixel_format(pixel_format);
1618 color_attachment.set_blending_enabled(true);
1619 color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
1620 color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
1621 color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha);
1622 color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
1623 color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
1624 color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One);
1625
1626 device
1627 .new_render_pipeline_state(&descriptor)
1628 .expect("could not create render pipeline state")
1629}
1630
1631fn build_path_sprite_pipeline_state(
1632 device: &metal::DeviceRef,
1633 library: &metal::LibraryRef,
1634 label: &str,
1635 vertex_fn_name: &str,
1636 fragment_fn_name: &str,
1637 pixel_format: metal::MTLPixelFormat,
1638) -> metal::RenderPipelineState {
1639 let vertex_fn = library
1640 .get_function(vertex_fn_name, None)
1641 .expect("error locating vertex function");
1642 let fragment_fn = library
1643 .get_function(fragment_fn_name, None)
1644 .expect("error locating fragment function");
1645
1646 let descriptor = metal::RenderPipelineDescriptor::new();
1647 descriptor.set_label(label);
1648 descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
1649 descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
1650 let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
1651 color_attachment.set_pixel_format(pixel_format);
1652 color_attachment.set_blending_enabled(true);
1653 color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
1654 color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
1655 color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One);
1656 color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
1657 color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
1658 color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One);
1659
1660 device
1661 .new_render_pipeline_state(&descriptor)
1662 .expect("could not create render pipeline state")
1663}
1664
1665fn build_path_rasterization_pipeline_state(
1666 device: &metal::DeviceRef,
1667 library: &metal::LibraryRef,
1668 label: &str,
1669 vertex_fn_name: &str,
1670 fragment_fn_name: &str,
1671 pixel_format: metal::MTLPixelFormat,
1672 path_sample_count: u32,
1673) -> metal::RenderPipelineState {
1674 let vertex_fn = library
1675 .get_function(vertex_fn_name, None)
1676 .expect("error locating vertex function");
1677 let fragment_fn = library
1678 .get_function(fragment_fn_name, None)
1679 .expect("error locating fragment function");
1680
1681 let descriptor = metal::RenderPipelineDescriptor::new();
1682 descriptor.set_label(label);
1683 descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
1684 descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
1685 if path_sample_count > 1 {
1686 descriptor.set_raster_sample_count(path_sample_count as _);
1687 descriptor.set_alpha_to_coverage_enabled(false);
1688 }
1689 let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
1690 color_attachment.set_pixel_format(pixel_format);
1691 color_attachment.set_blending_enabled(true);
1692 color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
1693 color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
1694 color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One);
1695 color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
1696 color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
1697 color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
1698
1699 device
1700 .new_render_pipeline_state(&descriptor)
1701 .expect("could not create render pipeline state")
1702}
1703
1704// Align to multiples of 256 make Metal happy.
1705fn align_offset(offset: &mut usize) {
1706 *offset = (*offset).div_ceil(256) * 256;
1707}
1708
1709#[repr(C)]
1710enum ShadowInputIndex {
1711 Vertices = 0,
1712 Shadows = 1,
1713 ViewportSize = 2,
1714}
1715
1716#[repr(C)]
1717enum QuadInputIndex {
1718 Vertices = 0,
1719 Quads = 1,
1720 ViewportSize = 2,
1721}
1722
1723#[repr(C)]
1724enum UnderlineInputIndex {
1725 Vertices = 0,
1726 Underlines = 1,
1727 ViewportSize = 2,
1728}
1729
1730#[repr(C)]
1731enum SpriteInputIndex {
1732 Vertices = 0,
1733 Sprites = 1,
1734 ViewportSize = 2,
1735 AtlasTextureSize = 3,
1736 AtlasTexture = 4,
1737}
1738
1739#[repr(C)]
1740enum SurfaceInputIndex {
1741 Vertices = 0,
1742 Surfaces = 1,
1743 ViewportSize = 2,
1744 TextureSize = 3,
1745 YTexture = 4,
1746 CbCrTexture = 5,
1747}
1748
1749#[repr(C)]
1750enum PathRasterizationInputIndex {
1751 Vertices = 0,
1752 ViewportSize = 1,
1753}
1754
1755#[derive(Clone, Debug, Eq, PartialEq)]
1756#[repr(C)]
1757pub struct PathSprite {
1758 pub bounds: Bounds<ScaledPixels>,
1759}
1760
1761#[derive(Clone, Debug, Eq, PartialEq)]
1762#[repr(C)]
1763pub struct SurfaceBounds {
1764 pub bounds: Bounds<ScaledPixels>,
1765 pub content_mask: ContentMask<ScaledPixels>,
1766}
1767
1768#[cfg(any(test, feature = "test-support"))]
1769pub struct MetalHeadlessRenderer {
1770 renderer: MetalRenderer,
1771}
1772
1773#[cfg(any(test, feature = "test-support"))]
1774impl MetalHeadlessRenderer {
1775 pub fn new() -> Self {
1776 let instance_buffer_pool = Arc::new(Mutex::new(InstanceBufferPool::default()));
1777 let renderer = MetalRenderer::new_headless(instance_buffer_pool);
1778 Self { renderer }
1779 }
1780}
1781
1782#[cfg(any(test, feature = "test-support"))]
1783impl gpui::PlatformHeadlessRenderer for MetalHeadlessRenderer {
1784 fn render_scene_to_image(
1785 &mut self,
1786 scene: &Scene,
1787 size: Size<DevicePixels>,
1788 ) -> anyhow::Result<image::RgbaImage> {
1789 self.renderer.render_scene_to_image(scene, size)
1790 }
1791
1792 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> anyhow::Result<()> {
1793 self.renderer.render_scene(scene, size)
1794 }
1795
1796 fn sprite_atlas(&self) -> Arc<dyn gpui::PlatformAtlas> {
1797 self.renderer.sprite_atlas().clone()
1798 }
1799}
1800