crates/gpui/Cargo.toml 🔗
@@ -298,3 +298,7 @@ path = "examples/uniform_list.rs"
[[example]]
name = "window_shadow"
path = "examples/window_shadow.rs"
+
+[[example]]
+name = "metal_view"
+path = "examples/metal_view.rs"
Nate Butler created
crates/gpui/Cargo.toml | 4
crates/gpui/examples/metal_view.rs | 307 ++++++++++++++
crates/gpui/examples/metal_view_quad.rs | 355 +++++++++++++++++
crates/gpui/examples/metal_view_simple.rs | 198 +++++++++
crates/gpui/src/elements/metal_view.rs | 196 +++++++++
crates/gpui/src/elements/mod.rs | 5
crates/gpui/src/gpui.rs | 2
crates/gpui/src/platform/mac.rs | 2
crates/gpui/src/platform/mac/metal_render_pass.rs | 343 ++++++++++++++++
crates/gpui/src/platform/mac/metal_renderer.rs | 120 +++++
crates/gpui/src/scene.rs | 82 +++
crates/gpui/src/window.rs | 24 +
12 files changed, 1,625 insertions(+), 13 deletions(-)
@@ -298,3 +298,7 @@ path = "examples/uniform_list.rs"
[[example]]
name = "window_shadow"
path = "examples/window_shadow.rs"
+
+[[example]]
+name = "metal_view"
+path = "examples/metal_view.rs"
@@ -0,0 +1,307 @@
+use gpui::{Application, *};
+use std::sync::Arc;
+
+#[cfg(target_os = "macos")]
+use metal::{Device, MTLPrimitiveType, RenderCommandEncoderRef, RenderPipelineState, TextureRef};
+
+struct MetalViewExample {
+ #[cfg(target_os = "macos")]
+ pipeline_state: Option<RenderPipelineState>,
+ #[cfg(target_os = "macos")]
+ device: Option<Device>,
+}
+
+impl MetalViewExample {
+ fn new() -> Self {
+ Self {
+ #[cfg(target_os = "macos")]
+ pipeline_state: None,
+ #[cfg(target_os = "macos")]
+ device: None,
+ }
+ }
+
+ #[cfg(target_os = "macos")]
+ fn setup_metal(&mut self) {
+ // Create Metal device
+ let device = Device::system_default().expect("no Metal device");
+
+ // Create shader library from source
+ let shader_source = r#"
+ #include <metal_stdlib>
+ using namespace metal;
+
+ struct VertexOut {
+ float4 position [[position]];
+ float4 color;
+ };
+
+ vertex VertexOut vertex_main(uint vid [[vertex_id]]) {
+ VertexOut out;
+
+ // Create a rectangle using two triangles
+ // Triangle 1: top-left, top-right, bottom-left
+ // Triangle 2: top-right, bottom-right, bottom-left
+ float2 positions[6] = {
+ float2(-1.0, 1.0), // top-left
+ float2( 1.0, 1.0), // top-right
+ float2(-1.0, -1.0), // bottom-left
+ float2( 1.0, 1.0), // top-right
+ float2( 1.0, -1.0), // bottom-right
+ float2(-1.0, -1.0), // bottom-left
+ };
+
+ out.position = float4(positions[vid], 0.0, 1.0);
+ // Create a gradient color based on position
+ out.color = float4(
+ (positions[vid].x + 1.0) * 0.5, // Red based on X
+ (positions[vid].y + 1.0) * 0.5, // Green based on Y
+ 0.7, // Blue constant
+ 1.0 // Alpha
+ );
+
+ return out;
+ }
+
+ fragment float4 fragment_main(VertexOut in [[stage_in]]) {
+ return in.color;
+ }
+ "#;
+
+ let library = device
+ .new_library_with_source(shader_source, &metal::CompileOptions::new())
+ .expect("Failed to create shader library");
+
+ let vertex_function = library.get_function("vertex_main", None).unwrap();
+ let fragment_function = library.get_function("fragment_main", None).unwrap();
+
+ // Create pipeline state
+ let pipeline_descriptor = metal::RenderPipelineDescriptor::new();
+ pipeline_descriptor.set_vertex_function(Some(&vertex_function));
+ pipeline_descriptor.set_fragment_function(Some(&fragment_function));
+
+ // Configure color attachment
+ let color_attachment = pipeline_descriptor
+ .color_attachments()
+ .object_at(0)
+ .unwrap();
+ color_attachment.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm);
+
+ // Enable blending to work with GPUI's existing content
+ color_attachment.set_blending_enabled(true);
+ color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha);
+ color_attachment
+ .set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
+ color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
+ color_attachment
+ .set_destination_alpha_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
+
+ let pipeline_state = device
+ .new_render_pipeline_state(&pipeline_descriptor)
+ .expect("Failed to create pipeline state");
+
+ self.device = Some(device);
+ self.pipeline_state = Some(pipeline_state);
+ }
+
+ #[cfg(target_os = "macos")]
+ fn create_render_callback(&self) -> MetalRenderCallback {
+ let pipeline_state = self.pipeline_state.clone().unwrap();
+
+ Arc::new(
+ move |encoder: &RenderCommandEncoderRef,
+ _target: &TextureRef,
+ bounds: Bounds<Pixels>,
+ scale_factor: f32| {
+ // Set the pipeline state
+ encoder.set_render_pipeline_state(&pipeline_state);
+
+ // Set viewport to match element bounds
+ let viewport = metal::MTLViewport {
+ originX: bounds.origin.x.0 as f64 * scale_factor as f64,
+ originY: bounds.origin.y.0 as f64 * scale_factor as f64,
+ width: bounds.size.width.0 as f64 * scale_factor as f64,
+ height: bounds.size.height.0 as f64 * scale_factor as f64,
+ znear: 0.0,
+ zfar: 1.0,
+ };
+ encoder.set_viewport(viewport);
+
+ // Draw the rectangle (6 vertices for 2 triangles)
+ encoder.draw_primitives(MTLPrimitiveType::Triangle, 0, 6);
+ },
+ )
+ }
+}
+
+impl Render for MetalViewExample {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+ // Initialize Metal on first render if on macOS
+ #[cfg(target_os = "macos")]
+ if self.pipeline_state.is_none() {
+ self.setup_metal();
+ }
+
+ div()
+ .flex()
+ .bg(rgb(0x1e1e1e))
+ .size_full()
+ .justify_center()
+ .items_center()
+ .child(
+ div()
+ .flex_col()
+ .gap_4()
+ .child(
+ div().flex().justify_center().child(
+ div()
+ .child("Metal View Example")
+ .text_xl()
+ .text_color(rgb(0xffffff)),
+ ),
+ )
+ .child(
+ div()
+ .border_1()
+ .border_color(rgb(0x444444))
+ .rounded_md()
+ .overflow_hidden()
+ .child(
+ // The Metal view
+ #[cfg(target_os = "macos")]
+ {
+ let callback = self.create_render_callback();
+ metal_view()
+ .render_with_shared(callback)
+ .w(px(400.0))
+ .h(px(300.0))
+ .bg(rgb(0x000000))
+ },
+ #[cfg(not(target_os = "macos"))]
+ {
+ // Fallback for non-macOS platforms
+ div()
+ .w(px(400.0))
+ .h(px(300.0))
+ .bg(rgb(0x222222))
+ .flex()
+ .justify_center()
+ .items_center()
+ .child(
+ div()
+ .child("Metal rendering is only available on macOS")
+ .text_color(rgb(0x888888)),
+ )
+ },
+ ),
+ )
+ .child(
+ div().flex().justify_center().child(
+ div()
+ .child("A gradient rectangle rendered with custom Metal shaders")
+ .text_sm()
+ .text_color(rgb(0xaaaaaa)),
+ ),
+ ),
+ )
+ }
+}
+
+fn main() {
+ Application::new().run(|cx: &mut App| {
+ let _ = cx.open_window(
+ WindowOptions {
+ window_bounds: Some(WindowBounds::Windowed(Bounds {
+ origin: Point::new(px(100.0), px(100.0)),
+ size: Size {
+ width: px(600.0),
+ height: px(500.0),
+ },
+ })),
+ titlebar: Some(TitlebarOptions {
+ title: Some("Metal View Example".into()),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ |_window, cx| cx.new(|_cx| MetalViewExample::new()),
+ );
+ });
+}
+
+// Additional example: Using MetalView for more complex rendering
+#[cfg(target_os = "macos")]
+#[allow(dead_code)]
+mod advanced_example {
+ use super::*;
+ use std::sync::Mutex;
+
+ /// Example of a MetalView that renders an animated scene
+ pub struct AnimatedMetalView {
+ device: Device,
+ pipeline_state: RenderPipelineState,
+ frame_count: Arc<Mutex<f32>>,
+ }
+
+ impl AnimatedMetalView {
+ pub fn create_animated_renderer(&self) -> MetalRenderCallback {
+ let pipeline_state = self.pipeline_state.clone();
+ let frame_count = self.frame_count.clone();
+
+ Arc::new(
+ move |encoder: &RenderCommandEncoderRef,
+ _target: &TextureRef,
+ bounds: Bounds<Pixels>,
+ scale_factor: f32| {
+ // Update animation state
+ let mut count = frame_count.lock().unwrap();
+ *count += 0.01;
+ let time = *count;
+
+ // Set pipeline and viewport
+ encoder.set_render_pipeline_state(&pipeline_state);
+
+ let viewport = metal::MTLViewport {
+ originX: bounds.origin.x.0 as f64 * scale_factor as f64,
+ originY: bounds.origin.y.0 as f64 * scale_factor as f64,
+ width: bounds.size.width.0 as f64 * scale_factor as f64,
+ height: bounds.size.height.0 as f64 * scale_factor as f64,
+ znear: 0.0,
+ zfar: 1.0,
+ };
+ encoder.set_viewport(viewport);
+
+ // Pass time as a uniform
+ encoder.set_vertex_bytes(
+ 0,
+ std::mem::size_of::<f32>() as u64,
+ &time as *const f32 as *const _,
+ );
+
+ // Draw animated geometry
+ encoder.draw_primitives(MTLPrimitiveType::TriangleStrip, 0, 4);
+ },
+ )
+ }
+ }
+}
+
+// Example usage in a component:
+// ```rust
+// struct MyApp {
+// metal_renderer: Option<MetalRenderCallback>,
+// }
+//
+// impl Render for MyApp {
+// fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+// div()
+// .child(
+// metal_view()
+// .render_with(|encoder, target, bounds, scale_factor| {
+// // Your custom Metal rendering code here
+// })
+// .size_full()
+// )
+// }
+// }
+// ```
@@ -0,0 +1,355 @@
+use gpui::{prelude::*, *};
+use std::sync::Arc;
+
+#[cfg(target_os = "macos")]
+use metal::{Device, MTLPrimitiveType, RenderCommandEncoderRef, RenderPipelineState, TextureRef};
+
+struct MetalQuadExample {
+ #[cfg(target_os = "macos")]
+ pipeline_state: Option<RenderPipelineState>,
+ #[cfg(target_os = "macos")]
+ device: Option<Device>,
+}
+
+impl MetalQuadExample {
+ fn new() -> Self {
+ Self {
+ #[cfg(target_os = "macos")]
+ pipeline_state: None,
+ #[cfg(target_os = "macos")]
+ device: None,
+ }
+ }
+
+ #[cfg(target_os = "macos")]
+ fn setup_metal(&mut self) {
+ let device = Device::system_default().expect("no Metal device");
+
+ // Shader that properly handles viewport transformation
+ let shader_source = r#"
+ #include <metal_stdlib>
+ using namespace metal;
+
+ struct Uniforms {
+ float2 viewport_size;
+ };
+
+ struct VertexOut {
+ float4 position [[position]];
+ float4 color;
+ };
+
+ vertex VertexOut vertex_main(
+ uint vid [[vertex_id]],
+ constant Uniforms& uniforms [[buffer(0)]]
+ ) {
+ VertexOut out;
+
+ // Define a quad in pixel coordinates (0,0 to viewport_size)
+ float2 positions[6] = {
+ float2(0.0, 0.0), // top-left
+ float2(uniforms.viewport_size.x, 0.0), // top-right
+ float2(0.0, uniforms.viewport_size.y), // bottom-left
+ float2(uniforms.viewport_size.x, 0.0), // top-right
+ float2(uniforms.viewport_size.x, uniforms.viewport_size.y), // bottom-right
+ float2(0.0, uniforms.viewport_size.y), // bottom-left
+ };
+
+ // Transform from pixel coordinates to normalized device coordinates
+ float2 pos = positions[vid];
+ float2 ndc = (pos / uniforms.viewport_size) * 2.0 - 1.0;
+ ndc.y = -ndc.y; // Flip Y axis to match screen coordinates
+
+ out.position = float4(ndc, 0.0, 1.0);
+
+ // Create a nice gradient
+ float2 uv = pos / uniforms.viewport_size;
+ out.color = float4(
+ uv.x, // Red increases left to right
+ uv.y, // Green increases top to bottom
+ 1.0 - uv.x, // Blue decreases left to right
+ 1.0 // Full opacity
+ );
+
+ return out;
+ }
+
+ fragment float4 fragment_main(VertexOut in [[stage_in]]) {
+ return in.color;
+ }
+ "#;
+
+ let library = device
+ .new_library_with_source(shader_source, &metal::CompileOptions::new())
+ .expect("Failed to create shader library");
+
+ let vertex_function = library.get_function("vertex_main", None).unwrap();
+ let fragment_function = library.get_function("fragment_main", None).unwrap();
+
+ // Create pipeline state
+ let pipeline_descriptor = metal::RenderPipelineDescriptor::new();
+ pipeline_descriptor.set_vertex_function(Some(&vertex_function));
+ pipeline_descriptor.set_fragment_function(Some(&fragment_function));
+
+ let color_attachment = pipeline_descriptor
+ .color_attachments()
+ .object_at(0)
+ .unwrap();
+ color_attachment.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm);
+
+ // Enable blending
+ color_attachment.set_blending_enabled(true);
+ color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha);
+ color_attachment
+ .set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
+ color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
+ color_attachment
+ .set_destination_alpha_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
+
+ let pipeline_state = device
+ .new_render_pipeline_state(&pipeline_descriptor)
+ .expect("Failed to create pipeline state");
+
+ self.device = Some(device);
+ self.pipeline_state = Some(pipeline_state);
+ }
+
+ #[cfg(target_os = "macos")]
+ fn create_render_callback(&self) -> MetalRenderCallback {
+ let pipeline_state = self.pipeline_state.clone().unwrap();
+
+ Arc::new(
+ move |encoder: &RenderCommandEncoderRef,
+ _target: &TextureRef,
+ bounds: Bounds<Pixels>,
+ scale_factor: f32| {
+ // Set the pipeline state
+ encoder.set_render_pipeline_state(&pipeline_state);
+
+ // Set viewport to match element bounds
+ let viewport = metal::MTLViewport {
+ originX: bounds.origin.x.0 as f64 * scale_factor as f64,
+ originY: bounds.origin.y.0 as f64 * scale_factor as f64,
+ width: bounds.size.width.0 as f64 * scale_factor as f64,
+ height: bounds.size.height.0 as f64 * scale_factor as f64,
+ znear: 0.0,
+ zfar: 1.0,
+ };
+ encoder.set_viewport(viewport);
+
+ // Set scissor rectangle to clip to bounds
+ let scissor_rect = metal::MTLScissorRect {
+ x: (bounds.origin.x.0 * scale_factor) as u64,
+ y: (bounds.origin.y.0 * scale_factor) as u64,
+ width: (bounds.size.width.0 * scale_factor) as u64,
+ height: (bounds.size.height.0 * scale_factor) as u64,
+ };
+ encoder.set_scissor_rect(scissor_rect);
+
+ // Pass viewport size as uniform
+ #[repr(C)]
+ struct Uniforms {
+ viewport_size: [f32; 2],
+ }
+
+ let uniforms = Uniforms {
+ viewport_size: [
+ bounds.size.width.0 * scale_factor,
+ bounds.size.height.0 * scale_factor,
+ ],
+ };
+
+ encoder.set_vertex_bytes(
+ 0,
+ std::mem::size_of::<Uniforms>() as u64,
+ &uniforms as *const Uniforms as *const _,
+ );
+
+ // Draw the quad
+ encoder.draw_primitives(MTLPrimitiveType::Triangle, 0, 6);
+ },
+ )
+ }
+}
+
+impl Render for MetalQuadExample {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+ #[cfg(target_os = "macos")]
+ if self.pipeline_state.is_none() {
+ self.setup_metal();
+ }
+
+ div()
+ .flex()
+ .flex_col()
+ .bg(rgb(0x1e1e1e))
+ .size_full()
+ .p_8()
+ .gap_6()
+ .child(
+ div()
+ .child("Metal Quad Example")
+ .text_2xl()
+ .text_color(rgb(0xffffff)),
+ )
+ .child(
+ div()
+ .child("This example demonstrates proper coordinate handling in MetalView")
+ .text_color(rgb(0xaaaaaa)),
+ )
+ .child(
+ div()
+ .flex()
+ .gap_4()
+ .child(
+ div()
+ .flex_col()
+ .gap_2()
+ .flex_1()
+ .child(
+ div()
+ .child("Small MetalView (200x150)")
+ .text_sm()
+ .text_color(rgb(0xcccccc)),
+ )
+ .child(
+ div()
+ .border_1()
+ .border_color(rgb(0x444444))
+ .rounded_md()
+ .overflow_hidden()
+ .child(
+ #[cfg(target_os = "macos")]
+ {
+ let callback = self.create_render_callback();
+ metal_view()
+ .render_with_shared(callback)
+ .w(px(200.0))
+ .h(px(150.0))
+ .bg(rgb(0x000000))
+ },
+ #[cfg(not(target_os = "macos"))]
+ {
+ div()
+ .w(px(200.0))
+ .h(px(150.0))
+ .bg(rgb(0x222222))
+ .flex()
+ .items_center()
+ .justify_center()
+ .child(
+ div()
+ .child("Metal (macOS only)")
+ .text_color(rgb(0x666666)),
+ )
+ },
+ ),
+ ),
+ )
+ .child(
+ div()
+ .flex_col()
+ .gap_2()
+ .flex_1()
+ .child(
+ div()
+ .child("Large MetalView (400x300)")
+ .text_sm()
+ .text_color(rgb(0xcccccc)),
+ )
+ .child(
+ div()
+ .border_1()
+ .border_color(rgb(0x444444))
+ .rounded_md()
+ .overflow_hidden()
+ .child(
+ #[cfg(target_os = "macos")]
+ {
+ let callback = self.create_render_callback();
+ metal_view()
+ .render_with_shared(callback)
+ .w(px(400.0))
+ .h(px(300.0))
+ .bg(rgb(0x000000))
+ },
+ #[cfg(not(target_os = "macos"))]
+ {
+ div()
+ .w(px(400.0))
+ .h(px(300.0))
+ .bg(rgb(0x222222))
+ .flex()
+ .items_center()
+ .justify_center()
+ .child(
+ div()
+ .child("Metal (macOS only)")
+ .text_color(rgb(0x666666)),
+ )
+ },
+ ),
+ ),
+ ),
+ )
+ .child(
+ div().p_4().bg(rgb(0x2a2a2a)).rounded_md().child(
+ div()
+ .flex()
+ .flex_col()
+ .gap_2()
+ .child(
+ div()
+ .child("Key Features:")
+ .text_base()
+ .font_weight(FontWeight::SEMIBOLD)
+ .text_color(rgb(0xffffff)),
+ )
+ .child(
+ div()
+ .child("• Proper coordinate transformation from pixels to NDC")
+ .text_sm()
+ .text_color(rgb(0xaaaaaa)),
+ )
+ .child(
+ div()
+ .child("• Scissor rectangle to clip content to bounds")
+ .text_sm()
+ .text_color(rgb(0xaaaaaa)),
+ )
+ .child(
+ div()
+ .child("• Viewport size passed as uniform to shader")
+ .text_sm()
+ .text_color(rgb(0xaaaaaa)),
+ )
+ .child(
+ div()
+ .child("• Gradient fills entire MetalView bounds")
+ .text_sm()
+ .text_color(rgb(0xaaaaaa)),
+ ),
+ ),
+ )
+ }
+}
+
+fn main() {
+ Application::new().run(|cx: &mut App| {
+ let _ = cx.open_window(
+ WindowOptions {
+ window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
+ None,
+ size(px(900.0), px(600.0)),
+ cx,
+ ))),
+ titlebar: Some(TitlebarOptions {
+ title: Some("Metal Quad Example".into()),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ |_window, cx| cx.new(|_cx| MetalQuadExample::new()),
+ );
+ });
+}
@@ -0,0 +1,198 @@
+use gpui::{prelude::*, *};
+
+struct MetalViewSimpleExample {
+ show_metal_view: bool,
+}
+
+impl MetalViewSimpleExample {
+ fn new() -> Self {
+ Self {
+ show_metal_view: true,
+ }
+ }
+}
+
+impl Render for MetalViewSimpleExample {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .flex()
+ .flex_col()
+ .bg(rgb(0x1e1e1e))
+ .size_full()
+ .p_8()
+ .gap_4()
+ .child(
+ div()
+ .child("MetalView Simple Example")
+ .text_2xl()
+ .text_color(rgb(0xffffff)),
+ )
+ .child(
+ div()
+ .flex()
+ .gap_2()
+ .items_center()
+ .child(
+ div()
+ .id("toggle-button")
+ .px_3()
+ .py_1()
+ .bg(rgb(0x3b82f6))
+ .hover(|style| style.bg(rgb(0x2563eb)))
+ .rounded_md()
+ .cursor_pointer()
+ .on_click(cx.listener(|this, _event, _window, cx| {
+ this.show_metal_view = !this.show_metal_view;
+ cx.notify();
+ }))
+ .child(div().child("Toggle MetalView").text_color(rgb(0xffffff))),
+ )
+ .child(
+ div()
+ .child(format!(
+ "MetalView is: {}",
+ if self.show_metal_view {
+ "visible"
+ } else {
+ "hidden"
+ }
+ ))
+ .text_color(rgb(0xaaaaaa)),
+ ),
+ )
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .gap_4()
+ .p_4()
+ .bg(rgb(0x2a2a2a))
+ .rounded_md()
+ .child(
+ div()
+ .child("Container with MetalView")
+ .text_lg()
+ .text_color(rgb(0xffffff)),
+ )
+ .when(self.show_metal_view, |parent| {
+ parent.child(
+ div()
+ .border_2()
+ .border_color(rgb(0x444444))
+ .rounded_md()
+ .overflow_hidden()
+ .child(
+ #[cfg(target_os = "macos")]
+ {
+ metal_view()
+ .w_full()
+ .h(px(200.0))
+ .bg(rgb(0x1a1a1a))
+ .render_with(
+ |_encoder, _target, _bounds, _scale_factor| {
+ // This callback would contain custom Metal rendering code
+ // For now, it's just a placeholder
+ },
+ )
+ },
+ #[cfg(not(target_os = "macos"))]
+ {
+ div()
+ .w_full()
+ .h(px(200.0))
+ .bg(rgb(0x1a1a1a))
+ .flex()
+ .items_center()
+ .justify_center()
+ .child(
+ div()
+ .child("MetalView (macOS only)")
+ .text_color(rgb(0x666666)),
+ )
+ },
+ ),
+ )
+ })
+ .child(
+ div()
+ .flex()
+ .gap_4()
+ .child(
+ div().flex_1().p_3().bg(rgb(0x333333)).rounded_md().child(
+ div()
+ .child("Regular GPUI content")
+ .text_sm()
+ .text_color(rgb(0xcccccc)),
+ ),
+ )
+ .child(
+ div().flex_1().p_3().bg(rgb(0x333333)).rounded_md().child(
+ div()
+ .child("Can be mixed with MetalView")
+ .text_sm()
+ .text_color(rgb(0xcccccc)),
+ ),
+ ),
+ ),
+ )
+ .child(
+ div().mt_4().p_4().bg(rgb(0x2a2a2a)).rounded_md().child(
+ div()
+ .flex()
+ .flex_col()
+ .gap_2()
+ .child(
+ div()
+ .child("Notes:")
+ .text_base()
+ .font_weight(FontWeight::SEMIBOLD)
+ .text_color(rgb(0xffffff)),
+ )
+ .child(
+ div()
+ .child("• MetalView integrates with GPUI's layout system")
+ .text_sm()
+ .text_color(rgb(0xaaaaaa)),
+ )
+ .child(
+ div()
+ .child("• It can be styled with the same methods as other elements")
+ .text_sm()
+ .text_color(rgb(0xaaaaaa)),
+ )
+ .child(
+ div()
+ .child("• On macOS, it would render custom Metal content")
+ .text_sm()
+ .text_color(rgb(0xaaaaaa)),
+ )
+ .child(
+ div()
+ .child("• On other platforms, a fallback can be provided")
+ .text_sm()
+ .text_color(rgb(0xaaaaaa)),
+ ),
+ ),
+ )
+ }
+}
+
+fn main() {
+ Application::new().run(|cx: &mut App| {
+ let _ = cx.open_window(
+ WindowOptions {
+ window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
+ None,
+ size(px(800.0), px(600.0)),
+ cx,
+ ))),
+ titlebar: Some(TitlebarOptions {
+ title: Some("MetalView Simple Example".into()),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ |_window, cx| cx.new(|_cx| MetalViewSimpleExample::new()),
+ );
+ });
+}
@@ -0,0 +1,196 @@
+use crate::{
+ App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
+ Pixels, Style, StyleRefinement, Styled, Window,
+};
+use refineable::Refineable;
+use std::sync::Arc;
+
+#[cfg(target_os = "macos")]
+use metal::{RenderCommandEncoderRef, TextureRef};
+
+/// A callback for custom Metal rendering.
+///
+/// The callback receives:
+/// - command_encoder: The Metal command encoder to issue draw calls
+/// - target_texture: The texture to render into
+/// - bounds: The bounds of the element in pixels
+/// - scale_factor: The window's scale factor
+#[cfg(target_os = "macos")]
+pub type MetalRenderCallback =
+ Arc<dyn Fn(&RenderCommandEncoderRef, &TextureRef, Bounds<Pixels>, f32) + Send + Sync + 'static>;
+
+/// A view that allows custom Metal rendering.
+pub struct MetalView {
+ #[cfg(target_os = "macos")]
+ render_callback: Option<MetalRenderCallback>,
+ style: StyleRefinement,
+}
+
+/// Create a new Metal view element.
+pub fn metal_view() -> MetalView {
+ MetalView {
+ #[cfg(target_os = "macos")]
+ render_callback: None,
+ style: Default::default(),
+ }
+}
+
+impl MetalView {
+ /// Set the Metal render callback.
+ #[cfg(target_os = "macos")]
+ pub fn render_with<F>(mut self, callback: F) -> Self
+ where
+ F: Fn(&RenderCommandEncoderRef, &TextureRef, Bounds<Pixels>, f32) + Send + Sync + 'static,
+ {
+ self.render_callback = Some(Arc::new(callback));
+ self
+ }
+
+ /// Set the Metal render callback using a shared callback.
+ #[cfg(target_os = "macos")]
+ pub fn render_with_shared(mut self, callback: MetalRenderCallback) -> Self {
+ self.render_callback = Some(callback);
+ self
+ }
+}
+
+impl Element for MetalView {
+ type RequestLayoutState = ();
+ type PrepaintState = ();
+
+ fn id(&self) -> Option<ElementId> {
+ None
+ }
+
+ fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+ None
+ }
+
+ fn request_layout(
+ &mut self,
+ _global_id: Option<&GlobalElementId>,
+ _inspector_id: Option<&InspectorElementId>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> (LayoutId, Self::RequestLayoutState) {
+ let mut style = Style::default();
+ style.refine(&self.style);
+ let layout_id = window.request_layout(style, [], cx);
+ (layout_id, ())
+ }
+
+ fn prepaint(
+ &mut self,
+ _global_id: Option<&GlobalElementId>,
+ _inspector_id: Option<&InspectorElementId>,
+ _bounds: Bounds<Pixels>,
+ _request_layout: &mut Self::RequestLayoutState,
+ _window: &mut Window,
+ _cx: &mut App,
+ ) -> Self::PrepaintState {
+ }
+
+ fn paint(
+ &mut self,
+ _global_id: Option<&GlobalElementId>,
+ _inspector_id: Option<&InspectorElementId>,
+ bounds: Bounds<Pixels>,
+ _: &mut Self::RequestLayoutState,
+ _: &mut Self::PrepaintState,
+ window: &mut Window,
+ _: &mut App,
+ ) {
+ #[cfg(target_os = "macos")]
+ if let Some(render_callback) = &self.render_callback {
+ // TODO: This is a placeholder. In a real implementation, we would need to:
+ // 1. Register this Metal view with the window's rendering system
+ // 2. Ensure the callback is invoked during the Metal rendering pass
+ // 3. Handle proper clipping and transformation matrices
+ //
+ // For now, we'll store the callback and bounds in the window's custom render queue
+ window.paint_metal_view(bounds, render_callback.clone());
+ }
+ }
+}
+
+impl IntoElement for MetalView {
+ type Element = Self;
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+impl Styled for MetalView {
+ fn style(&mut self) -> &mut StyleRefinement {
+ &mut self.style
+ }
+}
+
+/// Extension trait for MetalView to provide platform-agnostic API
+pub trait MetalViewExt {
+ /// Set a placeholder render function for non-macOS platforms
+ fn render_placeholder<F>(self, callback: F) -> Self
+ where
+ F: Fn(Bounds<Pixels>) + Send + Sync + 'static;
+}
+
+impl MetalViewExt for MetalView {
+ fn render_placeholder<F>(self, _callback: F) -> Self
+ where
+ F: Fn(Bounds<Pixels>) + Send + Sync + 'static,
+ {
+ // On non-macOS platforms, this could render a placeholder
+ // or use a different rendering backend
+ self
+ }
+}
+
+#[cfg(target_os = "macos")]
+/// Helper functions for creating common Metal render callbacks
+pub mod helpers {
+ use super::*;
+ use metal::*;
+
+ /// Helper to create a simple colored rectangle Metal renderer
+ pub fn solid_color_renderer(r: f32, g: f32, b: f32, a: f32) -> MetalRenderCallback {
+ Arc::new(move |encoder, _texture, bounds, _scale_factor| {
+ // This is a simplified example. In practice, you would:
+ // 1. Create or reuse a render pipeline state
+ // 2. Set up vertex data for the bounds
+ // 3. Issue draw calls
+ // 4. Handle proper coordinate transformation
+
+ // For now, this is just a placeholder to show the API design
+ let _ = (encoder, bounds, r, g, b, a);
+ })
+ }
+
+ /// Helper to create a Metal renderer that draws a textured quad
+ pub fn textured_quad_renderer(texture: Texture) -> MetalRenderCallback {
+ Arc::new(move |encoder, _target, bounds, _scale_factor| {
+ // Similar to above, this would set up a textured quad rendering
+ let _ = (encoder, &texture, bounds);
+ })
+ }
+}
+
+// Example usage:
+// ```rust
+// use gpui::elements::{metal_view, MetalViewExt};
+//
+// #[cfg(target_os = "macos")]
+// let view = metal_view()
+// .render_with(|encoder, target, bounds, scale_factor| {
+// // Custom Metal rendering code here
+// // You have full access to Metal command encoder
+// })
+// .size_full();
+//
+// #[cfg(not(target_os = "macos"))]
+// let view = metal_view()
+// .render_placeholder(|bounds| {
+// // Fallback rendering for non-macOS platforms
+// })
+// .size_full();
+// ```
@@ -6,6 +6,9 @@ mod div;
mod image_cache;
mod img;
mod list;
+/// Metal-based custom rendering for macOS
+#[cfg(target_os = "macos")]
+pub mod metal_view;
mod surface;
mod svg;
mod text;
@@ -19,6 +22,8 @@ pub use div::*;
pub use image_cache::*;
pub use img::*;
pub use list::*;
+#[cfg(target_os = "macos")]
+pub use metal_view::*;
pub use surface::*;
pub use svg::*;
pub use text::*;
@@ -129,6 +129,8 @@ pub use assets::*;
pub use color::*;
pub use ctor::ctor;
pub use element::*;
+#[cfg(target_os = "macos")]
+pub use elements::metal_view::MetalRenderCallback;
pub use elements::*;
pub use executor::*;
pub use geometry::*;
@@ -9,6 +9,8 @@ mod screen_capture;
#[cfg(not(feature = "macos-blade"))]
mod metal_atlas;
+// #[cfg(not(feature = "macos-blade"))]
+// mod metal_render_pass;
#[cfg(not(feature = "macos-blade"))]
pub mod metal_renderer;
@@ -0,0 +1,343 @@
+use crate::{DevicePixels, PaintMetalView, PrimitiveBatch, ScaledPixels, Scene, Size};
+use metal::{
+ CommandBufferRef, CommandQueue, Device, MTLLoadAction, MTLStoreAction, RenderCommandEncoderRef,
+};
+
+/// Represents a single render command in the rendering pipeline
+#[derive(Debug)]
+pub enum RenderCommand<'a> {
+ /// Begin a new render pass with the specified configuration
+ BeginRenderPass { descriptor: RenderPassDescriptor },
+ /// Draw a batch of GPUI primitives
+ DrawPrimitives {
+ batch: PrimitiveBatch<'a>,
+ viewport_size: Size<DevicePixels>,
+ },
+ /// Execute custom Metal rendering
+ ExecuteMetalCallback {
+ metal_view: &'a PaintMetalView,
+ viewport_size: Size<DevicePixels>,
+ },
+ /// End the current render pass
+ EndRenderPass,
+}
+
+/// Configuration for a render pass
+#[derive(Clone, Debug)]
+pub struct RenderPassDescriptor {
+ pub texture: metal::Texture,
+ pub load_action: MTLLoadAction,
+ pub store_action: MTLStoreAction,
+ pub clear_color: metal::MTLClearColor,
+ pub viewport: metal::MTLViewport,
+}
+
+/// State that needs to be preserved across render pass breaks
+#[derive(Clone, Debug)]
+pub struct RenderState {
+ pub viewport: metal::MTLViewport,
+ pub blend_mode: Option<BlendMode>,
+ // Add other state that needs to be preserved
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum BlendMode {
+ Normal,
+ Multiply,
+ Screen,
+ // Add other blend modes as needed
+}
+
+/// Context provided to Metal render callbacks
+pub struct MetalRenderContext<'a> {
+ pub command_buffer: &'a CommandBufferRef,
+ pub drawable_texture: &'a metal::TextureRef,
+ pub viewport_size: Size<DevicePixels>,
+ pub device: &'a Device,
+ pub bounds: crate::Bounds<ScaledPixels>,
+ pub scale_factor: f32,
+}
+
+/// Manages the rendering pipeline with support for render pass breaks
+pub struct RenderPassManager {
+ device: Device,
+ command_queue: CommandQueue,
+ current_state: RenderState,
+}
+
+impl RenderPassManager {
+ pub fn new(device: Device, command_queue: CommandQueue) -> Self {
+ Self {
+ device,
+ command_queue,
+ current_state: RenderState {
+ viewport: metal::MTLViewport {
+ originX: 0.0,
+ originY: 0.0,
+ width: 0.0,
+ height: 0.0,
+ znear: 0.0,
+ zfar: 1.0,
+ },
+ blend_mode: None,
+ },
+ }
+ }
+
+ /// Convert a scene into a list of render commands
+ pub fn build_render_commands<'a>(
+ &self,
+ scene: &'a Scene,
+ drawable_texture: &metal::TextureRef,
+ viewport_size: Size<DevicePixels>,
+ is_opaque: bool,
+ ) -> Vec<RenderCommand<'a>> {
+ let mut commands = Vec::new();
+
+ // Initial render pass configuration
+ let alpha = if is_opaque { 1.0 } else { 0.0 };
+ let descriptor = RenderPassDescriptor {
+ texture: drawable_texture.to_owned(),
+ load_action: MTLLoadAction::Clear,
+ store_action: MTLStoreAction::Store,
+ clear_color: metal::MTLClearColor::new(0.0, 0.0, 0.0, alpha),
+ viewport: metal::MTLViewport {
+ originX: 0.0,
+ originY: 0.0,
+ width: i32::from(viewport_size.width) as f64,
+ height: i32::from(viewport_size.height) as f64,
+ znear: 0.0,
+ zfar: 1.0,
+ },
+ };
+
+ commands.push(RenderCommand::BeginRenderPass { descriptor });
+
+ // Process batches, inserting render pass breaks for MetalViews
+ let mut in_render_pass = true;
+
+ for batch in scene.batches() {
+ match batch {
+ #[cfg(target_os = "macos")]
+ PrimitiveBatch::MetalViews(metal_views) => {
+ // End current render pass
+ if in_render_pass {
+ commands.push(RenderCommand::EndRenderPass);
+ in_render_pass = false;
+ }
+
+ // Add commands for each MetalView
+ for metal_view in metal_views {
+ commands.push(RenderCommand::ExecuteMetalCallback {
+ metal_view,
+ viewport_size,
+ });
+ }
+ }
+ _ => {
+ // Ensure we're in a render pass
+ if !in_render_pass {
+ let descriptor = RenderPassDescriptor {
+ texture: drawable_texture.to_owned(),
+ load_action: MTLLoadAction::Load, // Load existing content
+ store_action: MTLStoreAction::Store,
+ clear_color: metal::MTLClearColor::new(0.0, 0.0, 0.0, 0.0),
+ viewport: self.current_state.viewport,
+ };
+ commands.push(RenderCommand::BeginRenderPass { descriptor });
+ in_render_pass = true;
+ }
+
+ // Add primitive drawing command
+ commands.push(RenderCommand::DrawPrimitives {
+ batch,
+ viewport_size,
+ });
+ }
+ }
+ }
+
+ // Ensure we end the final render pass
+ if in_render_pass {
+ commands.push(RenderCommand::EndRenderPass);
+ }
+
+ commands
+ }
+
+ /// Execute a list of render commands
+ pub fn execute_commands<F>(
+ &mut self,
+ commands: &[RenderCommand],
+ command_buffer: &CommandBufferRef,
+ drawable_texture: &metal::TextureRef,
+ mut draw_primitives: F,
+ ) -> Result<(), anyhow::Error>
+ where
+ F: FnMut(
+ PrimitiveBatch,
+ &RenderCommandEncoderRef,
+ Size<DevicePixels>,
+ ) -> Result<(), anyhow::Error>,
+ {
+ let mut current_encoder: Option<metal::RenderCommandEncoder> = None;
+
+ for command in commands {
+ match command {
+ RenderCommand::BeginRenderPass { descriptor } => {
+ // End any existing encoder
+ if let Some(encoder) = current_encoder.take() {
+ encoder.end_encoding();
+ }
+
+ // Create new render pass
+ let render_pass_descriptor = metal::RenderPassDescriptor::new();
+ let color_attachment = render_pass_descriptor
+ .color_attachments()
+ .object_at(0)
+ .unwrap();
+
+ color_attachment.set_texture(Some(&descriptor.texture));
+ color_attachment.set_load_action(descriptor.load_action);
+ color_attachment.set_store_action(descriptor.store_action);
+ color_attachment.set_clear_color(descriptor.clear_color);
+
+ let encoder =
+ command_buffer.new_render_command_encoder(&render_pass_descriptor);
+ encoder.set_viewport(descriptor.viewport);
+ self.current_state.viewport = descriptor.viewport;
+
+ current_encoder = Some(encoder);
+ }
+
+ RenderCommand::DrawPrimitives {
+ batch,
+ viewport_size,
+ } => {
+ if let Some(ref encoder) = current_encoder {
+ draw_primitives(*batch, encoder, *viewport_size)?;
+ }
+ }
+
+ RenderCommand::ExecuteMetalCallback {
+ metal_view,
+ viewport_size,
+ } => {
+ // End current encoder if any
+ if let Some(encoder) = current_encoder.take() {
+ encoder.end_encoding();
+ }
+
+ // Create context for the callback
+ let context = MetalRenderContext {
+ command_buffer,
+ drawable_texture,
+ viewport_size: *viewport_size,
+ device: &self.device,
+ bounds: metal_view.bounds.clone(),
+ scale_factor: 2.0, // TODO: Get actual scale factor
+ };
+
+ // Create a new render command encoder for the callback
+ let render_pass_descriptor = metal::RenderPassDescriptor::new();
+ let color_attachment = render_pass_descriptor
+ .color_attachments()
+ .object_at(0)
+ .unwrap();
+
+ color_attachment.set_texture(Some(drawable_texture));
+ color_attachment.set_load_action(MTLLoadAction::Load);
+ color_attachment.set_store_action(MTLStoreAction::Store);
+
+ let encoder =
+ command_buffer.new_render_command_encoder(&render_pass_descriptor);
+
+ // Invoke the callback
+ (metal_view.render_callback)(
+ &encoder,
+ drawable_texture,
+ context.bounds.into(),
+ context.scale_factor,
+ );
+
+ encoder.end_encoding();
+ }
+
+ RenderCommand::EndRenderPass => {
+ if let Some(encoder) = current_encoder.take() {
+ encoder.end_encoding();
+ }
+ }
+ }
+ }
+
+ // Ensure any remaining encoder is ended
+ if let Some(encoder) = current_encoder {
+ encoder.end_encoding();
+ }
+
+ Ok(())
+ }
+
+ /// Save the current render state
+ pub fn save_state(&self) -> RenderState {
+ self.current_state.clone()
+ }
+
+ /// Restore a previously saved render state
+ pub fn restore_state(&mut self, state: RenderState) {
+ self.current_state = state;
+ }
+}
+
+/// Builder for constructing render command lists
+pub struct RenderCommandBuilder<'a> {
+ commands: Vec<RenderCommand<'a>>,
+}
+
+impl<'a> RenderCommandBuilder<'a> {
+ pub fn new() -> Self {
+ Self {
+ commands: Vec::new(),
+ }
+ }
+
+ pub fn begin_render_pass(mut self, descriptor: RenderPassDescriptor) -> Self {
+ self.commands
+ .push(RenderCommand::BeginRenderPass { descriptor });
+ self
+ }
+
+ pub fn draw_primitives(
+ mut self,
+ batch: PrimitiveBatch<'a>,
+ viewport_size: Size<DevicePixels>,
+ ) -> Self {
+ self.commands.push(RenderCommand::DrawPrimitives {
+ batch,
+ viewport_size,
+ });
+ self
+ }
+
+ pub fn execute_metal_callback(
+ mut self,
+ metal_view: &'a PaintMetalView,
+ viewport_size: Size<DevicePixels>,
+ ) -> Self {
+ self.commands.push(RenderCommand::ExecuteMetalCallback {
+ metal_view,
+ viewport_size,
+ });
+ self
+ }
+
+ pub fn end_render_pass(mut self) -> Self {
+ self.commands.push(RenderCommand::EndRenderPass);
+ self
+ }
+
+ pub fn build(self) -> Vec<RenderCommand<'a>> {
+ self.commands
+ }
+}
@@ -2,7 +2,7 @@ use super::metal_atlas::MetalAtlas;
use crate::{
AtlasTextureId, AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask, DevicePixels,
MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch,
- Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline, point, size,
+ Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline, point, px, size,
};
use anyhow::{Context as _, Result};
use block::ConcreteBlock;
@@ -18,7 +18,7 @@ use core_video::{
pixel_buffer::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
};
use foreign_types::{ForeignType, ForeignTypeRef};
-use metal::{CAMetalLayer, CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
+use metal::{CAMetalLayer, MTLPixelFormat, MTLResourceOptions, NSRange};
use objc::{self, msg_send, sel, sel_impl};
use parking_lot::Mutex;
use smallvec::SmallVec;
@@ -97,7 +97,7 @@ pub(crate) struct MetalRenderer {
device: metal::Device,
layer: metal::MetalLayer,
presents_with_transaction: bool,
- command_queue: CommandQueue,
+ command_queue: metal::CommandQueue,
paths_rasterization_pipeline_state: metal::RenderPipelineState,
path_sprites_pipeline_state: metal::RenderPipelineState,
shadows_pipeline_state: metal::RenderPipelineState,
@@ -385,6 +385,7 @@ impl MetalRenderer {
)
.with_context(|| format!("rasterizing {} paths", scene.paths().len()))?;
+ // Create initial render pass
let render_pass_descriptor = metal::RenderPassDescriptor::new();
let color_attachment = render_pass_descriptor
.color_attachments()
@@ -396,7 +397,7 @@ impl MetalRenderer {
color_attachment.set_store_action(metal::MTLStoreAction::Store);
let alpha = if self.layer.is_opaque() { 1. } else { 0. };
color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha));
- let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
+ let mut command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
command_encoder.set_viewport(metal::MTLViewport {
originX: 0.0,
@@ -407,21 +408,53 @@ impl MetalRenderer {
zfar: 1.0,
});
+ let mut needs_new_encoder = false;
+
+ // Helper to create a continuation render encoder
+ let create_continuation_encoder = || {
+ let render_pass_descriptor = metal::RenderPassDescriptor::new();
+ let color_attachment = render_pass_descriptor
+ .color_attachments()
+ .object_at(0)
+ .unwrap();
+
+ color_attachment.set_texture(Some(drawable.texture()));
+ color_attachment.set_load_action(metal::MTLLoadAction::Load);
+ color_attachment.set_store_action(metal::MTLStoreAction::Store);
+
+ let encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
+ encoder.set_viewport(metal::MTLViewport {
+ originX: 0.0,
+ originY: 0.0,
+ width: i32::from(viewport_size.width) as f64,
+ height: i32::from(viewport_size.height) as f64,
+ znear: 0.0,
+ zfar: 1.0,
+ });
+ encoder
+ };
+
for batch in scene.batches() {
+ // Create a new encoder if needed
+ if needs_new_encoder {
+ command_encoder = create_continuation_encoder();
+ needs_new_encoder = false;
+ }
+
let ok = match batch {
PrimitiveBatch::Shadows(shadows) => self.draw_shadows(
shadows,
instance_buffer,
&mut instance_offset,
viewport_size,
- command_encoder,
+ &command_encoder,
),
PrimitiveBatch::Quads(quads) => self.draw_quads(
quads,
instance_buffer,
&mut instance_offset,
viewport_size,
- command_encoder,
+ &command_encoder,
),
PrimitiveBatch::Paths(paths) => self.draw_paths(
paths,
@@ -429,14 +462,14 @@ impl MetalRenderer {
instance_buffer,
&mut instance_offset,
viewport_size,
- command_encoder,
+ &command_encoder,
),
PrimitiveBatch::Underlines(underlines) => self.draw_underlines(
underlines,
instance_buffer,
&mut instance_offset,
viewport_size,
- command_encoder,
+ &command_encoder,
),
PrimitiveBatch::MonochromeSprites {
texture_id,
@@ -447,7 +480,7 @@ impl MetalRenderer {
instance_buffer,
&mut instance_offset,
viewport_size,
- command_encoder,
+ &command_encoder,
),
PrimitiveBatch::PolychromeSprites {
texture_id,
@@ -458,15 +491,72 @@ impl MetalRenderer {
instance_buffer,
&mut instance_offset,
viewport_size,
- command_encoder,
+ &command_encoder,
),
PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces(
surfaces,
instance_buffer,
&mut instance_offset,
viewport_size,
- command_encoder,
+ &command_encoder,
),
+ #[cfg(target_os = "macos")]
+ PrimitiveBatch::MetalViews(metal_views) => {
+ // End current render pass
+ command_encoder.end_encoding();
+
+ // Process each MetalView
+ for metal_view in metal_views {
+ // Create a render encoder for the callback
+ let render_pass_descriptor = metal::RenderPassDescriptor::new();
+ let color_attachment = render_pass_descriptor
+ .color_attachments()
+ .object_at(0)
+ .unwrap();
+
+ color_attachment.set_texture(Some(drawable.texture()));
+ color_attachment.set_load_action(metal::MTLLoadAction::Load);
+ color_attachment.set_store_action(metal::MTLStoreAction::Store);
+
+ let callback_encoder =
+ command_buffer.new_render_command_encoder(render_pass_descriptor);
+ callback_encoder.set_viewport(metal::MTLViewport {
+ originX: 0.0,
+ originY: 0.0,
+ width: i32::from(viewport_size.width) as f64,
+ height: i32::from(viewport_size.height) as f64,
+ znear: 0.0,
+ zfar: 1.0,
+ });
+
+ // Invoke the Metal rendering callback
+ let scale_factor = self.layer.contents_scale() as f32;
+ // Convert bounds from ScaledPixels to Pixels
+ let bounds = Bounds {
+ origin: point(
+ px(metal_view.bounds.origin.x.0 / scale_factor),
+ px(metal_view.bounds.origin.y.0 / scale_factor),
+ ),
+ size: size(
+ px(metal_view.bounds.size.width.0 / scale_factor),
+ px(metal_view.bounds.size.height.0 / scale_factor),
+ ),
+ };
+
+ (metal_view.render_callback)(
+ &callback_encoder,
+ drawable.texture(),
+ bounds,
+ scale_factor,
+ );
+
+ callback_encoder.end_encoding();
+ }
+
+ // Mark that we'll need a new encoder for subsequent primitives
+ needs_new_encoder = true;
+ true
+ }
};
if !ok {
@@ -484,7 +574,10 @@ impl MetalRenderer {
}
}
- command_encoder.end_encoding();
+ // End the encoder if we haven't already
+ if !needs_new_encoder {
+ command_encoder.end_encoding();
+ }
instance_buffer.metal_buffer.did_modify_range(NSRange {
location: 0,
@@ -1134,6 +1227,9 @@ impl MetalRenderer {
}
true
}
+
+ // Note: draw_metal_views is no longer needed as we handle MetalViews
+ // directly in draw_primitives with proper render pass management
}
fn build_pipeline_state(
@@ -27,6 +27,8 @@ pub(crate) struct Scene {
pub(crate) monochrome_sprites: Vec<MonochromeSprite>,
pub(crate) polychrome_sprites: Vec<PolychromeSprite>,
pub(crate) surfaces: Vec<PaintSurface>,
+ #[cfg(target_os = "macos")]
+ pub(crate) metal_views: Vec<PaintMetalView>,
}
impl Scene {
@@ -115,6 +117,11 @@ impl Scene {
surface.order = order;
self.surfaces.push(surface.clone());
}
+ #[cfg(target_os = "macos")]
+ Primitive::MetalView(metal_view) => {
+ metal_view.order = order;
+ self.metal_views.push(metal_view.clone());
+ }
}
self.paint_operations
.push(PaintOperation::Primitive(primitive));
@@ -140,6 +147,8 @@ impl Scene {
self.polychrome_sprites
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
self.surfaces.sort_by_key(|surface| surface.order);
+ #[cfg(target_os = "macos")]
+ self.metal_views.sort_by_key(|metal_view| metal_view.order);
}
#[cfg_attr(
@@ -172,6 +181,12 @@ impl Scene {
surfaces: &self.surfaces,
surfaces_start: 0,
surfaces_iter: self.surfaces.iter().peekable(),
+ #[cfg(target_os = "macos")]
+ metal_views: &self.metal_views,
+ #[cfg(target_os = "macos")]
+ metal_views_start: 0,
+ #[cfg(target_os = "macos")]
+ metal_views_iter: self.metal_views.iter().peekable(),
}
}
}
@@ -193,6 +208,8 @@ pub(crate) enum PrimitiveKind {
MonochromeSprite,
PolychromeSprite,
Surface,
+ #[cfg(target_os = "macos")]
+ MetalView,
}
pub(crate) enum PaintOperation {
@@ -210,6 +227,8 @@ pub(crate) enum Primitive {
MonochromeSprite(MonochromeSprite),
PolychromeSprite(PolychromeSprite),
Surface(PaintSurface),
+ #[cfg(target_os = "macos")]
+ MetalView(PaintMetalView),
}
impl Primitive {
@@ -222,6 +241,8 @@ impl Primitive {
Primitive::MonochromeSprite(sprite) => &sprite.bounds,
Primitive::PolychromeSprite(sprite) => &sprite.bounds,
Primitive::Surface(surface) => &surface.bounds,
+ #[cfg(target_os = "macos")]
+ Primitive::MetalView(metal_view) => &metal_view.bounds,
}
}
@@ -234,6 +255,8 @@ impl Primitive {
Primitive::MonochromeSprite(sprite) => &sprite.content_mask,
Primitive::PolychromeSprite(sprite) => &sprite.content_mask,
Primitive::Surface(surface) => &surface.content_mask,
+ #[cfg(target_os = "macos")]
+ Primitive::MetalView(metal_view) => &metal_view.content_mask,
}
}
}
@@ -267,13 +290,19 @@ struct BatchIterator<'a> {
surfaces: &'a [PaintSurface],
surfaces_start: usize,
surfaces_iter: Peekable<slice::Iter<'a, PaintSurface>>,
+ #[cfg(target_os = "macos")]
+ metal_views: &'a [PaintMetalView],
+ #[cfg(target_os = "macos")]
+ metal_views_start: usize,
+ #[cfg(target_os = "macos")]
+ metal_views_iter: Peekable<slice::Iter<'a, PaintMetalView>>,
}
impl<'a> Iterator for BatchIterator<'a> {
type Item = PrimitiveBatch<'a>;
fn next(&mut self) -> Option<Self::Item> {
- let mut orders_and_kinds = [
+ let mut orders_and_kinds = vec![
(
self.shadows_iter.peek().map(|s| s.order),
PrimitiveKind::Shadow,
@@ -297,6 +326,12 @@ impl<'a> Iterator for BatchIterator<'a> {
PrimitiveKind::Surface,
),
];
+
+ #[cfg(target_os = "macos")]
+ orders_and_kinds.push((
+ self.metal_views_iter.peek().map(|m| m.order),
+ PrimitiveKind::MetalView,
+ ));
orders_and_kinds.sort_by_key(|(order, kind)| (order.unwrap_or(u32::MAX), *kind));
let first = orders_and_kinds[0];
@@ -426,6 +461,23 @@ impl<'a> Iterator for BatchIterator<'a> {
&self.surfaces[surfaces_start..surfaces_end],
))
}
+ #[cfg(target_os = "macos")]
+ PrimitiveKind::MetalView => {
+ let metal_views_start = self.metal_views_start;
+ let mut metal_views_end = metal_views_start + 1;
+ self.metal_views_iter.next();
+ while self
+ .metal_views_iter
+ .next_if(|metal_view| (metal_view.order, batch_kind) < max_order_and_kind)
+ .is_some()
+ {
+ metal_views_end += 1;
+ }
+ self.metal_views_start = metal_views_end;
+ Some(PrimitiveBatch::MetalViews(
+ &self.metal_views[metal_views_start..metal_views_end],
+ ))
+ }
}
}
}
@@ -452,6 +504,8 @@ pub(crate) enum PrimitiveBatch<'a> {
sprites: &'a [PolychromeSprite],
},
Surfaces(&'a [PaintSurface]),
+ #[cfg(target_os = "macos")]
+ MetalViews(&'a [PaintMetalView]),
}
#[derive(Default, Debug, Clone)]
@@ -668,12 +722,38 @@ pub(crate) struct PaintSurface {
pub image_buffer: core_video::pixel_buffer::CVPixelBuffer,
}
+#[cfg(target_os = "macos")]
+#[derive(Clone)]
+pub(crate) struct PaintMetalView {
+ pub order: DrawOrder,
+ pub bounds: Bounds<ScaledPixels>,
+ pub content_mask: ContentMask<ScaledPixels>,
+ pub render_callback: crate::MetalRenderCallback,
+}
+
+impl Debug for PaintMetalView {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("PaintMetalView")
+ .field("order", &self.order)
+ .field("bounds", &self.bounds)
+ .field("content_mask", &self.content_mask)
+ .finish()
+ }
+}
+
impl From<PaintSurface> for Primitive {
fn from(surface: PaintSurface) -> Self {
Primitive::Surface(surface)
}
}
+#[cfg(target_os = "macos")]
+impl From<PaintMetalView> for Primitive {
+ fn from(metal_view: PaintMetalView) -> Self {
+ Primitive::MetalView(metal_view)
+ }
+}
+
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct PathId(pub(crate) usize);
@@ -2946,6 +2946,30 @@ impl Window {
});
}
+ /// Paint a custom Metal view.
+ ///
+ /// This method should only be called as part of the paint phase of element drawing.
+ #[cfg(target_os = "macos")]
+ pub fn paint_metal_view(
+ &mut self,
+ bounds: Bounds<Pixels>,
+ render_callback: crate::MetalRenderCallback,
+ ) {
+ use crate::PaintMetalView;
+
+ self.invalidator.debug_assert_paint();
+
+ let scale_factor = self.scale_factor();
+ let bounds = bounds.scale(scale_factor);
+ let content_mask = self.content_mask().scale(scale_factor);
+ self.next_frame.scene.insert_primitive(PaintMetalView {
+ order: 0,
+ bounds,
+ content_mask,
+ render_callback,
+ });
+ }
+
/// Removes an image from the sprite atlas.
pub fn drop_image(&mut self, data: Arc<RenderImage>) -> Result<()> {
for frame_index in 0..data.frame_count() {