Start on metal rendering infrastructure

Nathan Sobo created

Change summary

Cargo.lock                                  |  43 +----
gpui/Cargo.toml                             |   4 
gpui/build.rs                               |  67 ++++++++
gpui/src/platform/mac/mod.rs                |   1 
gpui/src/platform/mac/renderer.rs           |  63 ++++++++
gpui/src/platform/mac/shaders/shaders.h     |  17 ++
gpui/src/platform/mac/shaders/shaders.metal |  30 ++++
gpui/src/platform/mac/window.rs             | 167 ++++++++++++++++++----
8 files changed, 318 insertions(+), 74 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -336,7 +336,7 @@ dependencies = [
  "cocoa-foundation",
  "core-foundation",
  "core-graphics",
- "foreign-types 0.3.2",
+ "foreign-types",
  "libc",
  "objc",
 ]
@@ -351,7 +351,7 @@ dependencies = [
  "block",
  "core-foundation",
  "core-graphics-types",
- "foreign-types 0.3.2",
+ "foreign-types",
  "libc",
  "objc",
 ]
@@ -396,7 +396,7 @@ dependencies = [
  "bitflags",
  "core-foundation",
  "core-graphics-types",
- "foreign-types 0.3.2",
+ "foreign-types",
  "libc",
 ]
 
@@ -408,7 +408,7 @@ checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b"
 dependencies = [
  "bitflags",
  "core-foundation",
- "foreign-types 0.3.2",
+ "foreign-types",
  "libc",
 ]
 
@@ -420,7 +420,7 @@ checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25"
 dependencies = [
  "core-foundation",
  "core-graphics",
- "foreign-types 0.3.2",
+ "foreign-types",
  "libc",
 ]
 
@@ -616,28 +616,7 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
 dependencies = [
- "foreign-types-shared 0.1.1",
-]
-
-[[package]]
-name = "foreign-types"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
-dependencies = [
- "foreign-types-macros",
- "foreign-types-shared 0.3.0",
-]
-
-[[package]]
-name = "foreign-types-macros"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63f713f8b2aa9e24fec85b0e290c56caee12e3b6ae0aeeda238a75b28251afd6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "foreign-types-shared",
 ]
 
 [[package]]
@@ -646,12 +625,6 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
 
-[[package]]
-name = "foreign-types-shared"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7684cf33bb7f28497939e8c7cf17e3e4e3b8d9a0080ffa4f8ae2f515442ee855"
-
 [[package]]
 name = "freetype"
 version = "0.7.0"
@@ -773,7 +746,7 @@ dependencies = [
  "core-text",
  "ctor",
  "font-kit",
- "foreign-types 0.5.0",
+ "foreign-types",
  "log",
  "metal",
  "num_cpus",
@@ -924,7 +897,7 @@ dependencies = [
  "bitflags",
  "block",
  "cocoa-foundation",
- "foreign-types 0.3.2",
+ "foreign-types",
  "log",
  "objc",
 ]

gpui/Cargo.toml 🔗

@@ -29,7 +29,7 @@ core-foundation = "0.9"
 core-graphics = "0.22.2"
 core-text = "19.2"
 font-kit = {git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1"}
-foreign-types = "0.5"
+foreign-types = "0.3"
 log = "0.4"
-metal = "0.21"
+metal = "0.21.0"
 objc = "0.2"

gpui/build.rs 🔗

@@ -1,8 +1,14 @@
-use std::{env, path::PathBuf};
+use std::{
+    env,
+    path::PathBuf,
+    process::{self, Command},
+};
 
 fn main() {
     generate_dispatch_bindings();
     compile_context_predicate_parser();
+    compile_metal_shaders();
+    generate_shader_bindings();
 }
 
 fn generate_dispatch_bindings() {
@@ -20,7 +26,7 @@ fn generate_dispatch_bindings() {
     let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
     bindings
         .write_to_file(out_path.join("dispatch_sys.rs"))
-        .expect("couldn't write bindings");
+        .expect("couldn't write dispatch bindings");
 }
 
 fn compile_context_predicate_parser() {
@@ -33,3 +39,60 @@ fn compile_context_predicate_parser() {
         .file(parser_c)
         .compile("tree_sitter_context_predicate");
 }
+
+const SHADER_HEADER_PATH: &'static str = "./src/platform/mac/shaders/shaders.h";
+
+fn compile_metal_shaders() {
+    let shader_path = "./src/platform/mac/shaders/shaders.metal";
+    let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air");
+    let metallib_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib");
+
+    println!("cargo:rerun-if-changed={}", SHADER_HEADER_PATH);
+    println!("cargo:rerun-if-changed={}", shader_path);
+
+    let output = Command::new("xcrun")
+        .args(&["-sdk", "macosx", "metal", "-c", shader_path, "-o"])
+        .arg(&air_output_path)
+        .output()
+        .unwrap();
+
+    if !output.status.success() {
+        eprintln!(
+            "metal shader compilation failed:\n{}",
+            String::from_utf8_lossy(&output.stderr)
+        );
+        process::exit(1);
+    }
+
+    let output = Command::new("xcrun")
+        .args(&["-sdk", "macosx", "metallib"])
+        .arg(air_output_path)
+        .arg("-o")
+        .arg(metallib_output_path)
+        .output()
+        .unwrap();
+
+    if !output.status.success() {
+        eprintln!(
+            "metallib compilation failed:\n{}",
+            String::from_utf8_lossy(&output.stderr)
+        );
+        process::exit(1);
+    }
+}
+
+fn generate_shader_bindings() {
+    let bindings = bindgen::Builder::default()
+        .header(SHADER_HEADER_PATH)
+        .whitelist_type("GPUIQuadInputIndex")
+        .whitelist_type("GPUIQuad")
+        .whitelist_type("GPUIQuadUniforms")
+        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
+        .generate()
+        .expect("unable to generate bindings");
+
+    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+    bindings
+        .write_to_file(out_path.join("shaders.rs"))
+        .expect("couldn't write shader bindings");
+}

gpui/src/platform/mac/renderer.rs 🔗

@@ -0,0 +1,63 @@
+use anyhow::{anyhow, Result};
+
+use crate::Scene;
+
+use super::window::RenderContext;
+
+const SHADERS_METALLIB: &'static [u8] =
+    include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
+
+pub struct Renderer {
+    quad_pipeline_state: metal::RenderPipelineState,
+}
+
+impl Renderer {
+    pub fn new(device: &metal::DeviceRef, pixel_format: metal::MTLPixelFormat) -> Result<Self> {
+        let library = device
+            .new_library_with_data(SHADERS_METALLIB)
+            .map_err(|message| anyhow!("error building metal library: {}", message))?;
+
+        Ok(Self {
+            quad_pipeline_state: build_pipeline_state(
+                device,
+                &library,
+                "quad",
+                "quad_vertex",
+                "quad_fragment",
+                pixel_format,
+            )?,
+        })
+    }
+
+    pub fn render(&mut self, scene: &Scene, ctx: RenderContext) {}
+}
+
+fn build_pipeline_state(
+    device: &metal::DeviceRef,
+    library: &metal::LibraryRef,
+    label: &str,
+    vertex_fn_name: &str,
+    fragment_fn_name: &str,
+    pixel_format: metal::MTLPixelFormat,
+) -> Result<metal::RenderPipelineState> {
+    let vertex_fn = library
+        .get_function(vertex_fn_name, None)
+        .map_err(|message| anyhow!("error locating vertex function: {}", message))?;
+    let fragment_fn = library
+        .get_function(fragment_fn_name, None)
+        .map_err(|message| anyhow!("error locating fragment function: {}", message))?;
+
+    let mut descriptor = metal::RenderPipelineDescriptor::new();
+    descriptor.set_label(label);
+    descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
+    descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
+    descriptor
+        .color_attachments()
+        .object_at(0)
+        .unwrap()
+        .set_pixel_format(pixel_format);
+
+    device
+        .new_render_pipeline_state(&descriptor)
+        .map_err(|message| anyhow!("could not create render pipeline state: {}", message))
+}

gpui/src/platform/mac/shaders/shaders.h 🔗

@@ -0,0 +1,17 @@
+#include <simd/simd.h>
+
+typedef enum {
+    GPUIQuadInputIndexVertices = 0,
+    GPUIQuadInputIndexQuads = 1,
+    GPUIQuadInputIndexUniforms = 2,
+} GPUIQuadInputIndex;
+
+typedef struct {
+    vector_float2 origin;
+    vector_float2 size;
+    vector_float4 background_color;
+} GPUIQuad;
+
+typedef struct {
+    vector_float2 viewport_size;
+} GPUIQuadUniforms;

gpui/src/platform/mac/shaders/shaders.metal 🔗

@@ -0,0 +1,30 @@
+#include <metal_stdlib>
+#include "shaders.h"
+
+using namespace metal;
+
+struct QuadFragmentInput {
+    float4 position [[position]];
+    GPUIQuad quad;
+};
+
+vertex QuadFragmentInput quad_vertex(
+    uint unit_vertex_id [[vertex_id]],
+    uint quad_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(GPUIQuadInputIndexVertices)]],
+    constant GPUIQuad *quads [[buffer(GPUIQuadInputIndexQuads)]],
+    constant GPUIQuadUniforms *uniforms [[buffer(GPUIQuadInputIndexUniforms)]]
+) {
+    float2 unit_vertex = unit_vertices[unit_vertex_id];
+    GPUIQuad quad = quads[quad_id];
+    float4 position = float4((unit_vertex * quad.size + quad.origin) / (uniforms->viewport_size / 2.0), 0.0, 1.0);
+
+    return QuadFragmentInput {
+        position,
+        quad,
+    };
+}
+
+fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]]) {
+    return input.quad.background_color;
+}

gpui/src/platform/mac/window.rs 🔗

@@ -12,13 +12,16 @@ use cocoa::{
     },
     base::{id, nil},
     foundation::{NSAutoreleasePool, NSSize, NSString},
+    quartzcore::AutoresizingMask,
 };
 use ctor::ctor;
+use foreign_types::ForeignType as _;
+use metal::{MTLClearColor, MTLLoadAction, MTLStoreAction};
 use objc::{
     class,
     declare::ClassDecl,
     msg_send,
-    runtime::{Class, Object, Sel, BOOL, NO, YES},
+    runtime::{Class, Object, Protocol, Sel, BOOL, NO, YES},
     sel, sel_impl,
 };
 use pathfinder_geometry::vector::vec2f;
@@ -31,13 +34,12 @@ use std::{
     time::{Duration, Instant},
 };
 
-use super::geometry::RectFExt;
+use super::{geometry::RectFExt, renderer::Renderer};
 
 const WINDOW_STATE_IVAR: &'static str = "windowState";
 
 static mut WINDOW_CLASS: *const Class = ptr::null();
 static mut VIEW_CLASS: *const Class = ptr::null();
-static mut DELEGATE_CLASS: *const Class = ptr::null();
 
 #[ctor]
 unsafe fn build_classes() {
@@ -63,7 +65,9 @@ unsafe fn build_classes() {
     VIEW_CLASS = {
         let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap();
         decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
+
         decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel));
+
         decl.add_method(
             sel!(keyDown:),
             handle_view_event as extern "C" fn(&Object, Sel, id),
@@ -84,20 +88,25 @@ unsafe fn build_classes() {
             sel!(scrollWheel:),
             handle_view_event as extern "C" fn(&Object, Sel, id),
         );
-        decl.register()
-    };
 
-    DELEGATE_CLASS = {
-        let mut decl = ClassDecl::new("GPUIWindowDelegate", class!(NSObject)).unwrap();
+        decl.add_protocol(Protocol::get("CALayerDelegate").unwrap());
         decl.add_method(
-            sel!(dealloc),
-            dealloc_delegate as extern "C" fn(&Object, Sel),
+            sel!(makeBackingLayer),
+            make_backing_layer as extern "C" fn(&Object, Sel) -> id,
+        );
+        decl.add_method(
+            sel!(viewDidChangeBackingProperties),
+            view_did_change_backing_properties as extern "C" fn(&Object, Sel),
+        );
+        decl.add_method(
+            sel!(setFrameSize:),
+            set_frame_size as extern "C" fn(&Object, Sel, NSSize),
         );
-        decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
         decl.add_method(
-            sel!(windowDidResize:),
-            window_did_resize as extern "C" fn(&Object, Sel, id),
+            sel!(displayLayer:),
+            display_layer as extern "C" fn(&Object, Sel, id),
         );
+
         decl.register()
     };
 }
@@ -110,6 +119,17 @@ struct WindowState {
     resize_callback: RefCell<Option<Box<dyn FnMut(Vector2F, f32)>>>,
     synthetic_drag_counter: Cell<usize>,
     executor: Rc<executor::Foreground>,
+    scene_to_render: RefCell<Option<Scene>>,
+    renderer: RefCell<Renderer>,
+    command_queue: metal::CommandQueue,
+    device: metal::Device,
+    layer: id,
+}
+
+pub struct RenderContext<'a> {
+    pub drawable_size: Vector2F,
+    pub device: &'a metal::Device,
+    pub command_encoder: &'a metal::RenderCommandEncoderRef,
 }
 
 impl Window {
@@ -117,6 +137,8 @@ impl Window {
         options: platform::WindowOptions,
         executor: Rc<executor::Foreground>,
     ) -> Result<Self> {
+        const PIXEL_FORMAT: metal::MTLPixelFormat = metal::MTLPixelFormat::BGRA8Unorm;
+
         unsafe {
             let pool = NSAutoreleasePool::new(nil);
 
@@ -138,12 +160,20 @@ impl Window {
                 return Err(anyhow!("window returned nil from initializer"));
             }
 
-            let delegate: id = msg_send![DELEGATE_CLASS, alloc];
-            let delegate = delegate.init();
-            if native_window == nil {
-                return Err(anyhow!("delegate returned nil from initializer"));
-            }
-            native_window.setDelegate_(delegate);
+            let device = metal::Device::system_default()
+                .ok_or_else(|| anyhow!("could not find default metal device"))?;
+
+            let layer: id = msg_send![class!(CAMetalLayer), layer];
+            let _: () = msg_send![layer, setDevice: device.as_ptr()];
+            let _: () = msg_send![layer, setPixelFormat: PIXEL_FORMAT];
+            let _: () = msg_send![layer, setAllowsNextDrawableTimeout: NO];
+            let _: () = msg_send![layer, setNeedsDisplayOnBoundsChange: YES];
+            let _: () = msg_send![layer, setPresentsWithTransaction: YES];
+            let _: () = msg_send![
+                layer,
+                setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE
+                    | AutoresizingMask::HEIGHT_SIZABLE
+            ];
 
             let native_view: id = msg_send![VIEW_CLASS, alloc];
             let native_view = NSView::init(native_view);
@@ -157,6 +187,11 @@ impl Window {
                 resize_callback: RefCell::new(None),
                 synthetic_drag_counter: Cell::new(0),
                 executor,
+                scene_to_render: Default::default(),
+                renderer: RefCell::new(Renderer::new(&device, PIXEL_FORMAT)?),
+                command_queue: device.new_command_queue(),
+                device,
+                layer,
             }));
 
             (*native_window).set_ivar(
@@ -167,10 +202,6 @@ impl Window {
                 WINDOW_STATE_IVAR,
                 Rc::into_raw(window.0.clone()) as *const c_void,
             );
-            (*delegate).set_ivar(
-                WINDOW_STATE_IVAR,
-                Rc::into_raw(window.0.clone()) as *const c_void,
-            );
 
             if let Some(title) = options.title.as_ref() {
                 native_window.setTitle_(NSString::alloc(nil).init_str(title));
@@ -237,7 +268,10 @@ impl platform::Window for Window {
     }
 
     fn render_scene(&self, scene: Scene) {
-        log::info!("render scene");
+        *self.0.scene_to_render.borrow_mut() = Some(scene);
+        unsafe {
+            let _: () = msg_send![self.0.native_window.contentView(), setNeedsDisplay: YES];
+        }
     }
 }
 
@@ -293,14 +327,6 @@ extern "C" fn dealloc_view(this: &Object, _: Sel) {
     }
 }
 
-extern "C" fn dealloc_delegate(this: &Object, _: Sel) {
-    unsafe {
-        let raw: *mut c_void = *this.get_ivar(WINDOW_STATE_IVAR);
-        Rc::from_raw(raw as *mut WindowState);
-        let () = msg_send![super(this, class!(NSObject)), dealloc];
-    }
-}
-
 extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
     let window = unsafe { window_state(this) };
 
@@ -329,14 +355,85 @@ extern "C" fn send_event(this: &Object, _: Sel, native_event: id) {
     }
 }
 
-extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
+extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
     let window = unsafe { window_state(this) };
-    let size = window.size();
-    let scale_factor = window.scale_factor();
+    window.layer
+}
+
+extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
+    let window;
+    unsafe {
+        window = window_state(this);
+        let _: () = msg_send![window.layer, setContentsScale: window.scale_factor() as f64];
+    }
+
     if let Some(callback) = window.resize_callback.borrow_mut().as_mut() {
+        let size = window.size();
+        let scale_factor = window.scale_factor();
         callback(size, scale_factor);
+    };
+}
+
+extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
+    let window;
+    unsafe {
+        window = window_state(this);
+        if window.size() == vec2f(size.width as f32, size.height as f32) {
+            return;
+        }
+
+        let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size];
+
+        let scale_factor = window.scale_factor() as f64;
+        let drawable_size: NSSize = NSSize {
+            width: size.width * scale_factor,
+            height: size.height * scale_factor,
+        };
+        let _: () = msg_send![window.layer, setDrawableSize: drawable_size];
+    }
+
+    if let Some(callback) = window.resize_callback.borrow_mut().as_mut() {
+        let size = window.size();
+        let scale_factor = window.scale_factor();
+        callback(size, scale_factor);
+    };
+}
+
+extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
+    unsafe {
+        let window = window_state(this);
+
+        if let Some(scene) = window.scene_to_render.borrow_mut().take() {
+            let drawable: &metal::MetalDrawableRef = msg_send![window.layer, nextDrawable];
+
+            let render_pass_descriptor = metal::RenderPassDescriptor::new();
+            let color_attachment = render_pass_descriptor
+                .color_attachments()
+                .object_at(0)
+                .unwrap();
+            color_attachment.set_texture(Some(drawable.texture()));
+            color_attachment.set_load_action(MTLLoadAction::Clear);
+            color_attachment.set_store_action(MTLStoreAction::Store);
+            color_attachment.set_clear_color(MTLClearColor::new(0., 0., 0., 1.));
+
+            let command_buffer = window.command_queue.new_command_buffer();
+            let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
+
+            window.renderer.borrow_mut().render(
+                &scene,
+                RenderContext {
+                    drawable_size: window.size() * window.scale_factor(),
+                    device: &window.device,
+                    command_encoder,
+                },
+            );
+
+            command_encoder.end_encoding();
+            command_buffer.commit();
+            command_buffer.wait_until_completed();
+            drawable.present();
+        };
     }
-    drop(window);
 }
 
 fn schedule_synthetic_drag(window_state: &Rc<WindowState>, position: Vector2F) {