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)
8.4 KiB
macOS M1 — renderer (Metal backend + GraphicsApi abstraction)
Scope: crates/ghostty-renderer only. Zig: renderer/Metal.zig, renderer/metal/*, the macOS parts of
generic.zig / Thread.zig / renderer.zig / shadertoy.zig.
GraphicsApi abstraction (graphics_api.rs)
Zig's GenericRenderer(comptime GraphicsAPI) duck-types its API; Rust has the GraphicsApi trait with
associated types (Target, Texture, Sampler, Buffer<T> (GAT), BufferRef, Pipeline, Shaders, GpuUniforms,
Frame, RenderPass<'a>), associated consts (CUSTOM_SHADER_TARGET, CUSTOM_SHADER_Y_IS_DOWN, SWAP_CHAIN_COUNT) and
operations (init_*, begin_frame, surface_size, present_last_target, draw_frame_start/end, loop_enter/exit, ...).
Helper traits: GpuBuffer, GpuTexture, GpuTarget, GpuShaders, GpuFrame, GpuRenderPass; shared value types
Attachment, AttachmentTarget, Draw, Step, Primitive, ImageTextureFormat, TextureKind, ApiError.
generic::Renderer<G: GraphicsApi = OpenGL>,image::{State,Image,Entry}<G>,thread::Thread<G = Api>.lib.rs:Api = metal::Metalontarget_vendor = "apple",opengl::OpenGLotherwise;Renderer = generic::Renderer<Api>(Backend::default_for_targetagrees, tested).- The OpenGL backend implements the trait in
opengl/api.rsby delegating to its (unchanged) inherent API, so apprt-gtk andtests/egl_headless.rskeep using the GL-typed API.opengl::render_pass::{Attachment,Step,Draw}are now aliases of the generic types;gl::Primitivere-exportsgraphics_api::Primitive. - CPU-side GPU structs shared by every API moved to
shader_types.rs(Uniformskeeps the std140 layout with bit-packedbools; Metal converts viaGraphicsApi::gpu_uniformstoMetalUniforms, whose bools are four bytes like the MSL struct). - Behaviour changes needed for asynchronous (Metal) frames, equal to Zig's
frame_sema/frameCompleted: the swap chain hands frame states back from the frame completion callback (SwapChainInner+Condvar);begin_frame(&self)no longer borrows the API;GpuFrame::complete(sync, completion)reports health through aFrameCompletion(inline for GL / sync Metal, on Metal's completion thread otherwise);display_unrealizedwaits for in-flight frames; font grid changes bump anatlas_epochso in-flight frames resync atlases. Renderer::display_realized<F>(loader)/last_frame_pixelsexist only forRenderer<OpenGL>; the generic hook isdisplay_realized_with(|api| ..).- GL:
OpenGL::last_targetis nowArc<Mutex<Option<Arc<Target>>>>(frames present without borrowing the API);present,present_last_target,clear_last_target,begin_frametake&self;last_target()returns a clone.
Metal (src/metal/, cfg(target_vendor = "apple"), objc2 0.6 + objc2-metal 0.3)
| Zig | Rust |
|---|---|
| Metal.zig | metal/mod.rs (Metal, ViewInfo, Metal::init(blending, &ViewInfo)) |
| metal/api.zig | metal_layout.rs (raw values, plain Rust, unit tested) + objc2_metal types |
| metal/buffer.zig | metal/buffer.rs |
| metal/Frame.zig | metal/frame.rs (command buffer, completion block via block2::RcBlock) |
| metal/IOSurfaceLayer.zig | metal/io_surface_layer.rs (define_class! CALayer subclass IOSurfaceLayer: display, actionForKey:; main-thread set_surface via dispatch2) |
| metal/Pipeline.zig | metal/pipeline.rs (vertex attributes from MetalVertex::ATTRIBUTES instead of reflection) |
| metal/RenderPass.zig | metal/render_pass.rs (buffer slots: vertex 0, uniforms 1, rest from 2) |
| metal/Sampler.zig, Target.zig, Texture.zig | metal/{sampler,target,texture}.rs (Target = IOSurface BGRA + Display P3 colorspace + MTLTexture) |
| metal/shaders.zig | metal/shaders.rs (+ shader_types, metal_layout::PIPELINE_DESCS) |
| shaders/shaders.metal | shaders/shaders.metal (verbatim) |
- The metallib: Zig precompiles
shaders.metalwithxcrun metal; we embed the source and compile it withnewLibraryWithSourceatShaders::init(same as post-process shaders already do). Entry points are looked up by name. - View attachment is done with
msg_send!on theNSView/UIView(no AppKit dependency): macOSsetLayer:thensetWantsLayer:, iOSaddSublayer:.setClipsToBounds:is guarded withrespondsToSelector:(NSView API since macOS 14). Metal::initexpects to run on the main thread with the view the apprt (L1) provides; the apprt buildsMetaland passes it toRenderer::new(opts, api);Thread::draw_on_app_threaddefaults to false on Apple targets.- iOS: compiled via
target_vendor = "apple"with the UIView branch; not checked here (only macOS targets are installed).
macOS parts of generic / thread
display_link.rs:CVDisplayLinkwrapper (objc2-core-video, macOS only). On other platformsDisplayLinkis uninhabited and the renderer holdsNone(Zig:DisplayLink = void).Renderercreates it whenwindow-vsyncis set (macOS),loop_enter(draw_now)installs the callback and starts it,loop_exitstops,set_focus/set_visiblestart/stop it,set_macos_display_id,has_vsync(= link running).loop_enteralso sets the Metal layer's display callback todraw_frame(true)(holding aWeak<Renderer>).thread.rs:pthread_setname_np("renderer"), QoS class (qos_for(visible, focused), tested on every platform), autorelease pool arounddrain_mailbox,Message::MacosDisplayId,thread_enter/exit,loop_enter/exitguards.generic.rs: withbackground-blur = macos-glass-*the background alpha is forced to 0 (macOS only).Renderer::{thread_enter,thread_exit,loop_enter,loop_exit,set_macos_display_id}are new.
Shadertoy MSL
shadertoy::msl_from_compiled (naga msl-out, enabled on all platforms so it is tested on Linux): entry point renamed to
ghostty_main (spirv-cross' main0 becomes main0_ in naga, which suffixes names ending in a digit), Globals at
[[buffer(1)]], iChannel0 at [[texture(0)]] / [[sampler(0)]] (what the Metal render pass binds), MSL 2.1.
Target::Msl no longer returns Unsupported. Known limitation: the unused iChannelTime float array has std140 stride 16
in the CPU struct but naga emits tightly packed float[4] (spirv-cross emits padded types); no Ghostty shader uses it.
Tests
- Linux (run):
metal_layout(resource options / texture usage bits, storage mode, max texture size, surface size clamp, pixel formats, vertex attribute tables, buffer slots, blend state,MetalUniformsoffsets == MSL struct, conversion from the std140 uniforms, shader function names exist inshaders.metal), shadertoyshadertoy_to_msl(port of "shadertoy to msl", +focus shader, +file loading),thread::qos_class_policy,backendtests,tests/mock_api.rs(generic renderer over a mock API with asynchronous triple-buffered completion, health reporting, unrealize waiting for in-flight frames). Totals: 97 unit + 9 EGL + 2 mock_api, all passing; the 9 EGL tests are unchanged apart from dropping an unneededmut. - macOS (compile-checked only):
metal/target.rs,render_pass.rs,frame.rsunit tests (pixel format fourcc, primitive mapping, health from command buffer status) compile with--all-targets. - Not ported: Zig has no tests in the Metal files other than
shadertoy to msl(ported) and theCellTextsize test (covered byshader_typeslayout tests).
Deviations / notes
Buffer::sync(Metal) allocates exactly double the required bytes; Zig multiplies by@sizeOf(T)again.Texture::replace_regionrejects too-short data (safe fn), Zig does not check.Imagevertex stays 48 bytes (GL layout padded to 16); Metal's stride issize_ofof that struct, offsets unchanged.ghostty-core'ssurface/mod.rsimportsghostty_renderer::Renderer(the platform alias) but builds anOpenGLAPI (GTK); on macOS that alias isRenderer<Metal>. Until L1 provides the embedded surface, core should importghostty_renderer::generic::Renderer(=Renderer<OpenGL>, identical type on Linux) socargo check --target *-apple-darwin -p ghostty-corepasses.Threadis generic over the API for this reason.- Cross-check recipe (no macOS SDK):
GETTEXT_DIR=/nonexistent(skip gettext-sys source build),CC_<target>= a wrapper aroundzig cc -target aarch64-macosforonig_sys(drop-arch/--target/-mmacosx-version-min),CARGO_BUILD_RUSTC_WRAPPER=(kache is picked up as cc wrapper),ZIG_GLOBAL_CACHE_DIRon a writable fs.