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

+110 -10
View File
@@ -12,7 +12,7 @@ use std::{io::Read, path::Path};
use bytemuck::{Pod, Zeroable};
use naga::{
back::glsl as glsl_out,
back::{glsl as glsl_out, msl as msl_out},
front::glsl as glsl_in,
valid::{Capabilities, ModuleInfo, ValidationFlags, Validator},
};
@@ -71,8 +71,7 @@ impl Default for Uniforms {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Target {
Glsl,
/// Metal is out of scope for the Rust port; converting to it fails with
/// [`Error::Unsupported`].
/// Metal Shading Language (naga's `msl-out` replaces spirv-cross).
Msl,
}
@@ -101,7 +100,7 @@ pub enum Error {
#[error("shader validation failed")]
ValidationFailed,
/// Writing the target GLSL failed (upstream: `error.SpvcFailed`).
#[error("GLSL generation failed: {0}")]
#[error("shader generation failed: {0}")]
SpvcFailed(String),
#[error("unsupported shader target")]
Unsupported,
@@ -171,7 +170,7 @@ pub fn load_from_file(path: &Path, target: Target) -> Result<String, Error> {
match target {
Target::Glsl => glsl_from_compiled(&compiled),
Target::Msl => Err(Error::Unsupported),
Target::Msl => msl_from_compiled(&compiled),
}
}
@@ -295,6 +294,86 @@ fn adapt_for_naga(full: &str) -> String {
s
}
/// The name of the fragment entry point in generated MSL. spirv-cross calls
/// it `main0` because `main` is reserved in MSL; naga would turn `main0` into
/// `main0_` (it suffixes names ending in a digit), so we use a name it keeps
/// as is. The Metal renderer looks the function up by this name.
pub const MSL_ENTRY_POINT: &str = "ghostty_main";
/// Convert a compiled shader to MSL (`mslFromSpv`).
///
/// The resource slots follow what spirv-cross produces with
/// `SPVC_COMPILER_OPTION_MSL_ENABLE_DECORATION_BINDING` for the shadertoy
/// prefix: the `Globals` uniform block at `[[buffer(1)]]` ("we need this to
/// properly locate the uniform block to index 1") and `iChannel0` as
/// `[[texture(0)]]` / `[[sampler(0)]]`, which is where the Metal render pass
/// binds them.
pub fn msl_from_compiled(compiled: &Compiled) -> Result<String, Error> {
let fail = |e: &dyn std::fmt::Display| {
log::warn!("spirv-cross error message={e}");
Error::SpvcFailed(e.to_string())
};
// Rename the entry point (info is indexed by entry point order, which
// doesn't change).
let mut module = compiled.module.clone();
for ep in &mut module.entry_points {
if ep.name == "main" {
ep.name = MSL_ENTRY_POINT.into();
}
}
let mut resources = msl_out::EntryPointResources::default();
let mut bind = |binding: u32, target: msl_out::BindTarget| {
resources
.resources
.insert(naga::ResourceBinding { group: 0, binding }, target);
};
// iChannel0 texture (the sampler is split off at binding 2, see
// `adapt_for_naga`).
bind(
0,
msl_out::BindTarget {
texture: Some(0),
..Default::default()
},
);
bind(
1,
msl_out::BindTarget {
buffer: Some(1),
..Default::default()
},
);
bind(
2,
msl_out::BindTarget {
sampler: Some(msl_out::BindSamplerTarget::Resource(0)),
..Default::default()
},
);
let mut options = msl_out::Options {
// Needed for `[[position]]` fragment inputs etc. on all supported
// macOS versions (the metallib is built for macOS 10.14 / MSL 2.1).
lang_version: (2, 1),
fake_missing_bindings: false,
..Default::default()
};
options
.per_entry_point_map
.insert(MSL_ENTRY_POINT.into(), resources);
let pipeline = msl_out::PipelineOptions {
entry_point: Some((naga::ShaderStage::Fragment, MSL_ENTRY_POINT.into())),
..Default::default()
};
let (msl, _info) = msl_out::write_string(&module, &compiled.info, &options, &pipeline)
.map_err(|e| fail(&e))?;
Ok(msl)
}
/// Convert a compiled shader to GLSL 430 core (`glslFromSpv`).
pub fn glsl_from_compiled(compiled: &Compiled) -> Result<String, Error> {
const GLSL_VERSION: u16 = 430;
@@ -383,17 +462,38 @@ mod tests {
}
#[test]
fn shadertoy_to_msl_unsupported() {
fn shadertoy_to_msl() {
// `shadertoy to msl`
let compiled = compile_shadertoy(TEST_CRT, None).unwrap();
assert_eq!(compiled.module.entry_points.len(), 1);
let msl = msl_from_compiled(&compiled).unwrap();
assert!(msl.contains("#include <metal_stdlib>"));
// The Metal renderer looks the fragment function up by this name.
assert!(
msl.contains("fragment ghostty_mainOutput ghostty_main("),
"{msl}"
);
// Uniform block at buffer 1, channel at texture 0 / sampler 0.
assert!(msl.contains("[[buffer(1)]]"), "{msl}");
assert!(msl.contains("[[texture(0)]]"), "{msl}");
assert!(msl.contains("[[sampler(0)]]"), "{msl}");
}
#[test]
fn shadertoy_focus_to_msl() {
let compiled = compile_shadertoy(TEST_FOCUS, None).unwrap();
let msl = msl_from_compiled(&compiled).unwrap();
assert!(msl.contains("ghostty_main("));
}
#[test]
fn load_from_file_msl() {
let dir = std::env::temp_dir().join(format!("ghostty-shadertoy-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("crt.glsl");
std::fs::write(&path, TEST_CRT).unwrap();
assert!(matches!(
load_from_file(&path, Target::Msl),
Err(Error::Unsupported)
));
let msl = load_from_file(&path, Target::Msl).unwrap();
assert!(msl.contains("ghostty_main("));
std::fs::remove_dir_all(&dir).ok();
}