sugarloaf 0.4.3

Sugarloaf is Rio rendering engine, designed to be multiplatform. It is based on WebGPU, Rust library for Desktops and WebAssembly for Web (JavaScript). This project is created and maintained for Rio terminal purposes but feel free to use it.
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
    }
}