feat(macos): port Ghostty's macOS code paths (Metal, CoreText, Darwin)

Ports the macOS-specific Zig code behind cfg(target_os = "macos"):
- renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend
  (all renderer/metal/* files, shaders.metal verbatim, runtime library
  compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga;
  OpenGL remains the backend elsewhere.
- font: CoreText face, shaper and discovery with build.rs backend
  selection (coretext default; coretext-freetype / coretext-harfbuzz /
  coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple.
- input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes.
- os: macos, mach, cf_release_thread, NSProcessInfo args and macOS
  branches; config: macOS defaults, keybinds, Application Support paths.
- termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit
  handling, os_log logging.

Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings
clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot
run here. The embedded runtime/C API (embedded.zig) is the next step.

Also includes the cross-platform integration edits shared by the crash,
verification, Windows and macOS work (Cargo target-gating, thread
crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg
gates), a cfg fix so ghostty-config builds on wasm32, and an allow for
the intentional recursion in the crash overflow test.

Test Plan:
- cargo test --workspace (3224 passed)
- cargo clippy --workspace --all-targets -- -D warnings
- cargo check --target aarch64-apple-darwin -p ghostty-renderer (with
  the documented cross env)
This commit is contained in:
ddidderr committed 2026-10-12 06:02:39 +02:00
1 parent 76771bed0d
commit bab49ee596
124 files changed
+14936 -1242

No files matched your search

@@ -0,0 +1,168 @@
//! Wrapper for handling render pipelines (`metal/Pipeline.zig`).
use objc2::{rc::Retained, runtime::ProtocolObject};
use objc2_foundation::NSString;
use objc2_metal::{
MTLBlendFactor,
MTLBlendOperation,
MTLDevice as _,
MTLLibrary,
MTLPixelFormat,
MTLRenderPipelineDescriptor,
MTLRenderPipelineState,
MTLVertexDescriptor,
MTLVertexFormat,
MTLVertexStepFunction,
};
use super::{Device, Error};
use crate::metal_layout::{
MetalVertex,
PREMULTIPLIED_ALPHA,
PixelFormat,
VertexAttribute,
VertexStepFunction,
};
/// Describes a color attachment.
#[derive(Debug, Clone, Copy)]
pub struct Attachment {
pub pixel_format: PixelFormat,
pub blending_enabled: bool,
}
/// Options for initializing a render pipeline.
pub struct Options<'a> {
/// MTLDevice
pub device: &'a Device,
/// Name of the vertex function
pub vertex_fn: &'a str,
/// Name of the fragment function
pub fragment_fn: &'a str,
/// MTLLibrary to get the vertex function from
pub vertex_library: &'a ProtocolObject<dyn MTLLibrary>,
/// MTLLibrary to get the fragment function from
pub fragment_library: &'a ProtocolObject<dyn MTLLibrary>,
/// Vertex step function
pub step_fn: VertexStepFunction,
/// Info about the color attachments used by this render pipeline.
pub attachments: &'a [Attachment],
}
/// A render pipeline state.
pub struct Pipeline {
/// MTLRenderPipelineState
state: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
}
// SAFETY: MTLRenderPipelineState objects are immutable and thread-safe.
unsafe impl Send for Pipeline {}
// SAFETY: see above.
unsafe impl Sync for Pipeline {}
impl Pipeline {
/// The underlying MTLRenderPipelineState.
pub fn state(&self) -> &ProtocolObject<dyn MTLRenderPipelineState> {
&self.state
}
/// Create a pipeline whose vertex attributes are those of `V`.
pub fn init<V: MetalVertex>(opts: &Options<'_>) -> Result<Pipeline, Error> {
Self::init_impl(Some((V::ATTRIBUTES, size_of::<V>())), opts)
}
/// Create a pipeline without a vertex descriptor.
pub fn init_no_attributes(opts: &Options<'_>) -> Result<Pipeline, Error> {
Self::init_impl(None, opts)
}
fn init_impl(
vertex_attributes: Option<(&[VertexAttribute], usize)>,
opts: &Options<'_>,
) -> Result<Pipeline, Error> {
// Create our descriptor
let desc = MTLRenderPipelineDescriptor::new();
// Get our vertex and fragment functions and add them to the descriptor.
{
let name = NSString::from_str(opts.vertex_fn);
let func_vert = opts
.vertex_library
.newFunctionWithName(&name)
.ok_or_else(|| Error::MissingFunction(opts.vertex_fn.to_owned()))?;
desc.setVertexFunction(Some(&func_vert));
}
{
let name = NSString::from_str(opts.fragment_fn);
let func_frag = opts
.fragment_library
.newFunctionWithName(&name)
.ok_or_else(|| Error::MissingFunction(opts.fragment_fn.to_owned()))?;
desc.setFragmentFunction(Some(&func_frag));
}
// If we have vertex attributes, create and add a vertex descriptor.
if let Some((attributes, stride)) = vertex_attributes {
let vertex_desc = MTLVertexDescriptor::new();
// Our attributes are the fields of the input
let attrs = vertex_desc.attributes();
for (i, a) in attributes.iter().enumerate() {
// SAFETY: indexing the attribute array with a valid index
// (the descriptor array has 31 slots); offsets and buffer
// index are plain numbers validated by pipeline creation.
unsafe {
let attr = attrs.objectAtIndexedSubscript(i);
attr.setFormat(MTLVertexFormat(a.format as usize));
attr.setOffset(a.offset);
attr.setBufferIndex(0);
}
}
// The layout describes how and when we fetch the next vertex input.
let layouts = vertex_desc.layouts();
// SAFETY: buffer layout 0 exists; the stride is a plain number.
unsafe {
let layout = layouts.objectAtIndexedSubscript(0);
layout.setStepFunction(MTLVertexStepFunction(opts.step_fn as usize));
layout.setStride(stride);
}
desc.setVertexDescriptor(Some(&vertex_desc));
}
// Set our color attachment
let attachments = desc.colorAttachments();
for (i, at) in opts.attachments.iter().enumerate() {
// SAFETY: color attachment indexes are validated by Metal; the
// pipelines use a single attachment.
let attachment = unsafe { attachments.objectAtIndexedSubscript(i) };
attachment.setPixelFormat(MTLPixelFormat(at.pixel_format as usize));
attachment.setBlendingEnabled(at.blending_enabled);
// We always use premultiplied alpha blending for now.
if at.blending_enabled {
let b = PREMULTIPLIED_ALPHA;
attachment.setRgbBlendOperation(MTLBlendOperation(b.rgb_operation as usize));
attachment.setAlphaBlendOperation(MTLBlendOperation(b.alpha_operation as usize));
attachment.setSourceRGBBlendFactor(MTLBlendFactor(b.source_rgb as usize));
attachment.setSourceAlphaBlendFactor(MTLBlendFactor(b.source_alpha as usize));
attachment.setDestinationRGBBlendFactor(MTLBlendFactor(b.destination_rgb as usize));
attachment
.setDestinationAlphaBlendFactor(MTLBlendFactor(b.destination_alpha as usize));
}
}
// Make our state
let state = opts
.device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|err| Error::from_ns_error(&err))?;
Ok(Pipeline { state })
}
}