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

+78 -61
View File
@@ -5,6 +5,7 @@
//! closure (see [`OpenGL::prepare_context`]) by the apprt once the context is
//! current, and everything afterwards goes through the [`Gl`] handle.
pub mod api;
pub mod buffer;
pub mod frame;
#[allow(unsafe_code)]
@@ -16,7 +17,10 @@ pub mod shaders;
pub mod target;
pub mod texture;
use std::{ffi::c_void, sync::Arc};
use std::{
ffi::c_void,
sync::{Arc, Mutex, PoisonError},
};
use ghostty_font::atlas::{Atlas, Format as AtlasFormat};
@@ -32,6 +36,7 @@ use self::gl::{
Wrap,
};
pub use self::{
api::GlFrame,
buffer::Buffer,
frame::Frame,
gl::Gl,
@@ -43,8 +48,6 @@ pub use self::{
};
use crate::shadertoy;
pub type GraphicsApi = OpenGL;
/// Custom shaders are converted to GLSL for OpenGL.
pub const CUSTOM_SHADER_TARGET: shadertoy::Target = shadertoy::Target::Glsl;
/// The fragCoord for OpenGL shaders is +Y = up.
@@ -85,27 +88,54 @@ pub struct OpenGL {
pub blending: AlphaBlending,
/// The most recently presented target, in case we need to present it again.
last_target: Option<Arc<Target>>,
/// Shared with in-flight [`Frame`]s, which record what they present.
last_target: LastTarget,
}
/// Pixel format for image texture options.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageTextureFormat {
/// 1 byte per pixel grayscale.
Gray,
/// 4 bytes per pixel RGBA.
Rgba,
/// 4 bytes per pixel BGRA.
Bgra,
/// Slot holding the most recently presented target.
pub type LastTarget = Arc<Mutex<Option<Arc<Target>>>>;
/// Present `target` by blitting it to the default framebuffer, remembering it
/// in `last_target` in case we need to repeat it.
fn present_target(gl: &Gl, last_target: &LastTarget, target: &Arc<Target>) -> Result<(), Error> {
// In order to present a target we blit it to the default framebuffer.
// We disable GL_FRAMEBUFFER_SRGB while doing this blit, otherwise the
// values may be linearized as they're copied, but even though the draw
// framebuffer has a linear internal format, the values in it should be
// sRGB, not linear!
gl.disable(glow::FRAMEBUFFER_SRGB)?;
// Bind the target for reading.
let fbobind = gl.bind_framebuffer(target.framebuffer(), FramebufferTarget::Read);
// Blit
let (w, h) = (target.width as i32, target.height as i32);
gl.blit_framebuffer(
(0, 0, w, h),
(0, 0, w, h),
glow::COLOR_BUFFER_BIT,
glow::NEAREST,
);
gl.unbind_framebuffer(fbobind);
if let Err(err) = gl.enable(glow::FRAMEBUFFER_SRGB) {
log::error!("Error re-enabling GL_FRAMEBUFFER_SRGB, err={err}");
}
// Keep track of this target in case we need to repeat it.
*last_target.lock().unwrap_or_else(PoisonError::into_inner) = Some(Arc::clone(target));
Ok(())
}
impl ImageTextureFormat {
fn to_pixel_format(self) -> PixelFormat {
match self {
ImageTextureFormat::Gray => PixelFormat::Red,
ImageTextureFormat::Rgba => PixelFormat::Rgba,
ImageTextureFormat::Bgra => PixelFormat::Bgra,
}
pub use crate::graphics_api::ImageTextureFormat;
/// The GL pixel format of an [`ImageTextureFormat`].
fn to_pixel_format(format: ImageTextureFormat) -> PixelFormat {
match format {
ImageTextureFormat::Gray => PixelFormat::Red,
ImageTextureFormat::Rgba => PixelFormat::Rgba,
ImageTextureFormat::Bgra => PixelFormat::Bgra,
}
}
@@ -116,7 +146,7 @@ impl OpenGL {
OpenGL {
gl: None,
blending,
last_target: None,
last_target: LastTarget::default(),
}
}
@@ -126,7 +156,7 @@ impl OpenGL {
OpenGL {
gl: Some(gl),
blending,
last_target: None,
last_target: LastTarget::default(),
}
}
@@ -247,42 +277,18 @@ impl OpenGL {
}
/// Present the provided target.
pub fn present(&mut self, target: &Arc<Target>) -> Result<(), Error> {
let gl = self.gl_or_err()?.clone();
// In order to present a target we blit it to the default framebuffer.
// We disable GL_FRAMEBUFFER_SRGB while doing this blit, otherwise the
// values may be linearized as they're copied, but even though the draw
// framebuffer has a linear internal format, the values in it should be
// sRGB, not linear!
gl.disable(glow::FRAMEBUFFER_SRGB)?;
// Bind the target for reading.
let fbobind = gl.bind_framebuffer(target.framebuffer(), FramebufferTarget::Read);
// Blit
let (w, h) = (target.width as i32, target.height as i32);
gl.blit_framebuffer(
(0, 0, w, h),
(0, 0, w, h),
glow::COLOR_BUFFER_BIT,
glow::NEAREST,
);
gl.unbind_framebuffer(fbobind);
if let Err(err) = gl.enable(glow::FRAMEBUFFER_SRGB) {
log::error!("Error re-enabling GL_FRAMEBUFFER_SRGB, err={err}");
}
// Keep track of this target in case we need to repeat it.
self.last_target = Some(Arc::clone(target));
Ok(())
pub fn present(&self, target: &Arc<Target>) -> Result<(), Error> {
present_target(self.gl_or_err()?, &self.last_target, target)
}
/// Present the last presented target again.
pub fn present_last_target(&mut self) -> Result<(), Error> {
if let Some(target) = self.last_target.clone() {
pub fn present_last_target(&self) -> Result<(), Error> {
let last = self
.last_target
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
if let Some(target) = last {
self.present(&target)?;
}
Ok(())
@@ -290,14 +296,25 @@ impl OpenGL {
/// The most recently presented target, if any (also set when the blit to
/// the default framebuffer is a no-op, e.g. on a surfaceless context).
pub fn last_target(&self) -> Option<&Arc<Target>> {
self.last_target.as_ref()
pub fn last_target(&self) -> Option<Arc<Target>> {
self.last_target
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
/// Drop the remembered last-presented target (call when the target it
/// refers to is being replaced, e.g. on resize).
pub fn clear_last_target(&mut self) {
self.last_target = None;
pub fn clear_last_target(&self) {
*self
.last_target
.lock()
.unwrap_or_else(PoisonError::into_inner) = None;
}
/// The slot [`Frame`]s record the target they present in.
pub(crate) fn last_target_slot(&self) -> LastTarget {
Arc::clone(&self.last_target)
}
/// Returns the options to use when constructing buffers.
@@ -357,7 +374,7 @@ impl OpenGL {
srgb: bool,
) -> texture::Options {
texture::Options {
format: format.to_pixel_format(),
format: to_pixel_format(format),
internal_format: if srgb {
InternalFormat::Srgba
} else {
@@ -404,7 +421,7 @@ impl OpenGL {
/// Begin a frame. The target is presented via this API when the frame is
/// completed (see [`Frame::complete`]).
pub fn begin_frame(&mut self, target: Arc<Target>) -> Frame<'_> {
pub fn begin_frame(&self, target: Arc<Target>) -> Frame {
Frame::begin(frame::Options::default(), self, target)
}
}