Compare commits

...

3 Commits

Author SHA1 Message Date
Nate Butler
a0d47fda83 WIP fix animation 2025-06-29 14:32:33 -04:00
Nate Butler
9c8737f643 Update metal view example 2025-06-29 14:01:23 -04:00
Nate Butler
ac471813fc Just vibes for now. Not sure it'll go much further. 2025-06-29 12:27:00 -04:00
11 changed files with 1029 additions and 13 deletions

7
Cargo.lock generated
View File

@@ -7019,6 +7019,12 @@ dependencies = [
"gix-validate 0.9.4",
]
[[package]]
name = "glam"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945"
[[package]]
name = "glob"
version = "0.3.2"
@@ -7168,6 +7174,7 @@ dependencies = [
"font-kit",
"foreign-types 0.5.0",
"futures 0.3.31",
"glam",
"gpui_macros",
"http_client",
"image",

View File

@@ -87,6 +87,7 @@ collections.workspace = true
ctor.workspace = true
derive_more.workspace = true
etagere = "0.2"
glam = "0.24"
futures.workspace = true
gpui_macros.workspace = true
http_client = { optional = true, workspace = true }
@@ -298,3 +299,7 @@ path = "examples/uniform_list.rs"
[[example]]
name = "window_shadow"
path = "examples/window_shadow.rs"
[[example]]
name = "metal_view"
path = "examples/metal_view.rs"

View File

@@ -0,0 +1,254 @@
use gpui::{prelude::*, *};
use std::sync::Arc;
use std::time::Instant;
#[cfg(target_os = "macos")]
use metal::{Device, MTLPrimitiveType, RenderCommandEncoderRef, RenderPipelineState, TextureRef};
struct MetalViewExample {
start_time: Instant,
#[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,
start_time: Instant::now(),
}
}
#[cfg(target_os = "macos")]
fn setup_metal(&mut self) {
let device = Device::system_default().expect("no Metal device");
// Simplified shader for debugging
let shader_source = r#"
#include <metal_stdlib>
using namespace metal;
struct Uniforms {
float time;
};
struct VertexOut {
float4 position [[position]];
float4 color;
};
vertex VertexOut vertex_main(
uint vid [[vertex_id]],
constant Uniforms& uniforms [[buffer(0)]]
) {
VertexOut out;
// Define triangle vertices in normalized device coordinates
float2 positions[3] = {
float2( 0.0, 0.5), // Top
float2(-0.5, -0.5), // Bottom left
float2( 0.5, -0.5) // Bottom right
};
float3 colors[3] = {
float3(1.0, 0.0, 0.0), // Red
float3(0.0, 1.0, 0.0), // Green
float3(0.0, 0.0, 1.0) // Blue
};
// Apply rotation
float2 pos = positions[vid];
float c = cos(uniforms.time);
float s = sin(uniforms.time);
float2 rotated = float2(
pos.x * c - pos.y * s,
pos.x * s + pos.y * c
);
out.position = float4(rotated, 0.0, 1.0);
out.color = float4(colors[vid], 1.0);
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 - no vertex descriptor needed for vertex_id based rendering
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);
// Note: Depth testing is not enabled for now as it requires proper depth buffer setup
// in the GPUI rendering pipeline
// 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, time_delta: f32) -> 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 time as uniform
let time = time_delta * 2.0; // Scale for reasonable rotation speed
#[repr(C)]
struct Uniforms {
time: f32,
}
let uniforms = Uniforms { time };
encoder.set_vertex_bytes(
0,
std::mem::size_of::<Uniforms>() as u64,
&uniforms as *const Uniforms as *const _,
);
// Draw triangle using vertex_id - no vertex buffer needed
encoder.draw_primitives(MTLPrimitiveType::Triangle, 0, 3);
},
)
}
}
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();
}
// Request animation frame
window.request_animation_frame();
div()
.flex()
.flex_col()
.bg(rgb(0x1e1e1e))
.size_full()
.p_8()
.gap_6()
.child(
div()
.child("Metal View Element")
.text_2xl()
.text_color(rgb(0xffffff)),
)
.child(
div()
.child("While GPUI normally handles all Metal rendering for you, the metal_view() element gives you direct access to write custom Metal shaders and GPU drawing commands")
.text_color(rgb(0xaaaaaa)),
)
.child(
div()
.child("This example shows a rotating 3D cube - the 'Hello World' of 3D graphics programming")
.text_sm()
.text_color(rgb(0x888888)),
)
.child(div().overflow_hidden().child(
#[cfg(target_os = "macos")]
{
let elapsed = self.start_time.elapsed().as_secs_f32();
let callback = self.create_render_callback(elapsed);
metal_view()
.render_with_shared(callback)
.w(px(600.0))
.h(px(400.0))
.bg(rgb(0x000000))
},
#[cfg(not(target_os = "macos"))]
{
div()
.w(px(600.0))
.h(px(400.0))
.bg(rgb(0x222222))
.flex()
.items_center()
.justify_center()
.child(div().child("Metal (macOS only)").text_color(rgb(0x666666)))
},
))
}
}
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 View Element".into()),
..Default::default()
}),
..Default::default()
},
|_window, cx| cx.new(|_cx| MetalViewExample::new()),
);
cx.activate(false);
});
}

View File

@@ -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();
// ```

View File

@@ -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::*;

View File

@@ -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::*;

View File

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

View File

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

View File

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

View File

@@ -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 {
@@ -41,6 +43,8 @@ impl Scene {
self.monochrome_sprites.clear();
self.polychrome_sprites.clear();
self.surfaces.clear();
#[cfg(target_os = "macos")]
self.metal_views.clear();
}
#[cfg_attr(
@@ -115,6 +119,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 +149,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 +183,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 +210,8 @@ pub(crate) enum PrimitiveKind {
MonochromeSprite,
PolychromeSprite,
Surface,
#[cfg(target_os = "macos")]
MetalView,
}
pub(crate) enum PaintOperation {
@@ -210,6 +229,8 @@ pub(crate) enum Primitive {
MonochromeSprite(MonochromeSprite),
PolychromeSprite(PolychromeSprite),
Surface(PaintSurface),
#[cfg(target_os = "macos")]
MetalView(PaintMetalView),
}
impl Primitive {
@@ -222,6 +243,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 +257,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 +292,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 +328,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 +463,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 +506,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 +724,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);

View File

@@ -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() {