1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
use crate::sugarloaf::{SugarloafWindow, SugarloafWindowSize};
use ::objc_rs::runtime::Object;
use ::objc_rs::{msg_send, sel, sel_impl};
use core_graphics::color_space::{kCGColorSpaceDisplayP3, CGColorSpace};
use core_graphics_types::geometry::CGSize;
use metal::foreign_types::ForeignType;
use metal::{
CommandBuffer, CommandQueue, Device, MTLGPUFamily, MTLPixelFormat, MetalDrawable,
MetalLayer, RenderCommandEncoder, Texture,
};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
pub struct MetalContext {
pub device: Device,
pub command_queue: CommandQueue,
pub layer: MetalLayer,
pub size: SugarloafWindowSize,
pub scale: f32,
pub supports_f16: bool,
}
pub struct MetalTexture {
pub texture: Texture,
pub drawable: MetalDrawable,
}
pub struct MetalCommandEncoder {
pub command_buffer: CommandBuffer,
}
pub struct MetalRenderPass<'a> {
pub encoder: RenderCommandEncoder,
_phantom: std::marker::PhantomData<&'a ()>,
}
impl MetalContext {
// type Device = Device;
// type Queue = CommandQueue;
// type Surface = MetalLayer;
// type TextureFormat = MTLPixelFormat;
// type CommandEncoder = MetalCommandEncoder;
// type RenderPass<'a> = MetalRenderPass<'a>;
// type Texture = MetalTexture;
// type TextureView = Texture;
// type Buffer = Buffer;
// type BindGroup = (); // Metal doesn't have bind groups, we'll use argument buffers
// type RenderPipeline = RenderPipelineState;
// type ComputePipeline = ComputePipelineState;
// type Sampler = SamplerState;
pub fn new(sugarloaf_window: SugarloafWindow) -> Self {
let device = Device::system_default().expect("Failed to create Metal device");
let command_queue = device.new_command_queue();
let size = sugarloaf_window.size;
let scale = sugarloaf_window.scale;
// Create Metal layer.
//
// Plain `BGRA8Unorm` + DisplayP3 colorspace tag — matches ghostty's
// default `alpha-blending = native` on macOS (see
// `ghostty/src/renderer/Metal.zig:208-211` and `Target.zig:44-60`).
// No `_sRGB` suffix: we don't want Metal to apply a transfer curve
// on read/write, so alpha blending stays in gamma-encoded space —
// which is what Apple's native widgets do and what keeps text
// weight identical to Terminal.app / ghostty. The fragment shaders
// compensate by emitting *already* sRGB-encoded DisplayP3 values
// (`prepare_output_rgb`), and the clear color goes through the
// same encode chain on the Rust side (see `sugarloaf.rs`).
//
// The DisplayP3 colorspace tag controls which *primaries* stored
// values represent on the display; we pick it unconditionally
// because it's the widest gamut we can present without HDR. The
// `[window] colorspace` config controls how input values are
// *interpreted* (sRGB vs P3 vs Rec.2020) via the gamut-conversion
// matrix in `renderer.metal`.
let layer = MetalLayer::new();
layer.set_device(&device);
layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
// Triple buffering: allow 3 drawables in flight so CPU/GPU/compositor
// can pipeline. `next_drawable` is the natural backpressure — it
// blocks the main thread once 3 drawables are out, capping how far
// ahead the CPU can run. Default is 2; ghostty/zed both bump this
// to 3 to match the standard Apple sample pattern.
layer.set_maximum_drawable_count(3);
// Disable the 1-second wait timeout on `next_drawable` — under load
// the default behaviour is to time out and return nil, which would
// turn into a dropped frame. Matches zed's
// `gpui_macos/src/metal_renderer.rs:162`.
unsafe {
let layer_obj = layer.as_ptr() as *mut Object;
let _: () = msg_send![layer_obj, setAllowsNextDrawableTimeout: false];
}
// CAMetalLayer's `colorspace` setter isn't wrapped by the `metal`
// crate yet, so we go through the Obj-C runtime directly. The CG
// setter retains internally (standard CA property behaviour), but
// we also `mem::forget` our owning handle to be fully defensive —
// we create exactly one colorspace at startup, so a one-time leak
// here is harmless and avoids a dangling pointer if Apple ever
// changes the retention semantics.
if let Some(cs) =
CGColorSpace::create_with_name(unsafe { kCGColorSpaceDisplayP3 })
{
unsafe {
let layer_obj = layer.as_ptr() as *mut Object;
let cs_ptr = cs.as_ptr() as *mut Object;
let _: () = msg_send![layer_obj, setColorspace: cs_ptr];
let applied: *mut Object = msg_send![layer_obj, colorspace];
if applied.is_null() {
tracing::warn!(
"CAMetalLayer.colorspace setter returned null — \
rendering will stay in the default sRGB colorspace"
);
} else {
tracing::info!("CAMetalLayer colorspace set to Display P3");
}
std::mem::forget(cs);
}
} else {
tracing::warn!("Failed to create Display P3 CGColorSpace");
}
// Default to opaque so the macOS compositor can take its
// fast path on windows without configured transparency. The
// host (rio) flips this to `false` via
// `Sugarloaf::set_window_opaque` when `config.window.opacity
// < 1` or a background blur style is on.
layer.set_opaque(true);
layer.set_presents_with_transaction(false);
// Use CGSize from core_graphics_types
let drawable_size = CGSize {
width: size.width as f64,
height: size.height as f64,
};
layer.set_drawable_size(drawable_size);
layer.set_contents_scale(scale as f64);
// Attach layer to window
unsafe {
match sugarloaf_window.window_handle().unwrap().as_raw() {
RawWindowHandle::AppKit(handle) => {
let ns_view = handle.ns_view.as_ptr() as *mut Object;
let _: () = msg_send![ns_view, setLayer: layer.as_ref()];
let _: () = msg_send![ns_view, setWantsLayer: true];
}
_ => panic!("Metal backend only supports macOS AppKit windows"),
}
}
// Check for f16 support (Metal generally supports it on modern hardware)
let supports_f16 = device.supports_family(MTLGPUFamily::Apple1);
tracing::info!("Metal device created: {:?}", device.name());
MetalContext {
device,
command_queue,
layer,
size: SugarloafWindowSize {
width: size.width,
height: size.height,
},
scale,
supports_f16,
}
}
#[inline]
pub fn resize(&mut self, width: u32, height: u32) {
self.size.width = width as f32;
self.size.height = height as f32;
let drawable_size = CGSize {
width: width as f64,
height: height as f64,
};
self.layer.set_drawable_size(drawable_size);
}
#[inline]
pub fn set_scale(&mut self, scale: f32) {
self.scale = scale;
}
/// Toggle the CAMetalLayer's opaque flag. `true` (default) lets the
/// macOS compositor take its opaque-window fast path; `false` is
/// required for `window.opacity < 1` or background blur to render
/// translucent. Cheap (one Cocoa property write); safe to call
/// every config reload.
#[inline]
pub fn set_layer_opaque(&self, opaque: bool) {
self.layer.set_opaque(opaque);
}
#[inline]
pub fn get_current_texture(&self) -> Result<MetalTexture, String> {
if let Some(drawable) = self.layer.next_drawable() {
let texture = drawable.texture().to_owned();
Ok(MetalTexture {
texture,
drawable: drawable.to_owned(),
})
} else {
Err("Failed to get next drawable".to_string())
}
}
// fn create_command_encoder(&self) -> Self::CommandEncoder {
// let command_buffer = self.command_queue.new_command_buffer().to_owned();
// MetalCommandEncoder { command_buffer }
// }
pub fn supports_f16(&self) -> bool {
self.supports_f16
}
}