neovide 0.16.1

Neovide: No Nonsense Neovim Gui
use std::{rc::Rc, sync::Arc};

use objc2::{rc::Retained, runtime::ProtocolObject};
use objc2_app_kit::NSColorSpace;
use objc2_core_foundation::{CGFloat, CGSize};
use objc2_metal::{
    MTLCommandBuffer, MTLCommandQueue, MTLCreateSystemDefaultDevice, MTLDevice, MTLDrawable,
    MTLTexture,
};
use objc2_quartz_core::{CAMetalDrawable, CAMetalLayer};
use skia_safe::{
    Canvas, ColorSpace, ColorType, Surface, SurfaceProps, SurfacePropsFlags,
    gpu::{
        self, DirectContext, SurfaceOrigin,
        mtl::{BackendContext, TextureInfo},
        surfaces::wrap_backend_render_target,
    },
};
use winit::{event_loop::EventLoopProxy, window::Window};

use crate::{
    platform::macos::get_ns_window,
    profiling::tracy_gpu_zone,
    renderer::{RendererSettings, SkiaRenderer, VSync},
    window::EventPayload,
};

use super::Settings;

struct MetalDrawableSurface {
    pub drawable: Retained<ProtocolObject<dyn CAMetalDrawable>>,
    pub surface: Surface,
}

impl MetalDrawableSurface {
    fn new(
        drawable: Retained<ProtocolObject<dyn CAMetalDrawable>>,
        context: &mut DirectContext,
        settings: &Settings,
    ) -> MetalDrawableSurface {
        tracy_gpu_zone!("MetalDrawableSurface.new");

        let texture = drawable.texture();
        let texture_info = unsafe { TextureInfo::new(Retained::as_ptr(&texture).cast()) };
        let backend_render_target = gpu::backend_render_targets::make_mtl(
            (texture.width() as i32, texture.height() as i32),
            &texture_info,
        );

        let render_settings = settings.get::<RendererSettings>();

        let surface_props = SurfaceProps::new_with_text_properties(
            SurfacePropsFlags::default(),
            render_settings.pixel_geometry.into(),
            render_settings.text_contrast,
            render_settings.text_gamma,
        );

        let surface = wrap_backend_render_target(
            context,
            &backend_render_target,
            SurfaceOrigin::TopLeft,
            ColorType::BGRA8888,
            ColorSpace::new_srgb(),
            Some(surface_props).as_ref(),
        )
        .expect("Failed to create skia surface with metal drawable.");

        MetalDrawableSurface { drawable, surface }
    }

    fn mtl_drawable(&self) -> &ProtocolObject<dyn MTLDrawable> {
        self.drawable.as_ref()
    }
}

pub struct MetalSkiaRenderer {
    window: Rc<Window>,
    _device: Retained<ProtocolObject<dyn MTLDevice>>,
    command_queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
    metal_layer: Retained<CAMetalLayer>,
    _backend: BackendContext,
    context: DirectContext,
    metal_drawable_surface: Option<MetalDrawableSurface>,
    settings: Arc<Settings>,
}

impl MetalSkiaRenderer {
    pub fn new(window: Rc<Window>, srgb: bool, vsync: bool, settings: Arc<Settings>) -> Self {
        log::info!("Initialize MetalSkiaRenderer...");

        let draw_size = window.inner_size();
        let ns_window = get_ns_window(&window);

        ns_window.setColorSpace(Some(
            if srgb { NSColorSpace::sRGBColorSpace() } else { NSColorSpace::deviceRGBColorSpace() }
                .as_ref(),
        ));

        let device =
            MTLCreateSystemDefaultDevice().expect("Failed to create Metal system default device.");
        let metal_layer = {
            let metal_layer = CAMetalLayer::new();
            metal_layer.setDevice(Some(&device));
            metal_layer.setPresentsWithTransaction(false);
            metal_layer.setFramebufferOnly(false);
            metal_layer.setDisplaySyncEnabled(vsync);
            metal_layer.setOpaque(false);

            let ns_view = ns_window.contentView().unwrap();
            ns_view.setWantsLayer(true);
            ns_view.setLayer(Some(&metal_layer));

            metal_layer
                .setDrawableSize(CGSize::new(draw_size.width as f64, draw_size.height as f64));
            metal_layer
        };

        let command_queue = device.newCommandQueue().expect("Failed to create command queue.");

        let backend = unsafe {
            BackendContext::new(
                Retained::as_ptr(&device).cast(),
                Retained::as_ptr(&command_queue).cast(),
            )
        };

        let context = gpu::direct_contexts::make_metal(&backend, None).unwrap();

        MetalSkiaRenderer {
            window,
            _device: device,
            metal_layer,
            command_queue,
            _backend: backend,
            context,
            metal_drawable_surface: None,
            settings,
        }
    }

    fn move_to_next_frame(&mut self) {
        tracy_gpu_zone!("move_to_next_frame");

        let drawable = {
            self.metal_layer.nextDrawable().expect("Failed to get next drawable of metal layer.")
        };

        self.metal_drawable_surface =
            Some(MetalDrawableSurface::new(drawable, &mut self.context, &self.settings));
    }
}

impl SkiaRenderer for MetalSkiaRenderer {
    fn window(&self) -> Rc<Window> {
        Rc::clone(&self.window)
    }

    fn flush(&mut self) {
        tracy_gpu_zone!("flush");

        self.context.flush_and_submit();
    }

    fn swap_buffers(&mut self) {
        tracy_gpu_zone!("swap buffers");

        let command_buffer =
            self.command_queue.commandBuffer().expect("Failed to create command buffer.");
        command_buffer.presentDrawable(
            self.metal_drawable_surface.as_mut().expect("No drawable surface now.").mtl_drawable(),
        );
        command_buffer.commit();

        self.metal_drawable_surface = None;
    }

    fn canvas(&mut self) -> &Canvas {
        tracy_gpu_zone!("canvas");

        self.move_to_next_frame();

        self.metal_drawable_surface
            .as_mut()
            .expect("Not metal drawable surface now.")
            .surface
            .canvas()
    }

    fn resize(&mut self) {
        tracy_gpu_zone!("resize");

        let window_size = self.window.inner_size();
        self.metal_layer.setDrawableSize(CGSize::new(
            window_size.width as CGFloat,
            window_size.height as CGFloat,
        ));

        self.window.request_redraw();
    }

    fn create_vsync(&self, _proxy: EventLoopProxy<EventPayload>) -> VSync {
        VSync::MacosMetal()
    }
}