From ce4b5eefae413492bcb7c5f046175db2b6c3daee Mon Sep 17 00:00:00 2001 From: silvanshade Date: Tue, 22 Aug 2023 15:36:37 -0600 Subject: [PATCH 1/5] Add a template for a metal example --- crates/icrate/Cargo.toml | 25 ++++ crates/icrate/examples/metal.rs | 250 ++++++++++++++++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 crates/icrate/examples/metal.rs diff --git a/crates/icrate/Cargo.toml b/crates/icrate/Cargo.toml index 897cb31b7..dc6001a4d 100644 --- a/crates/icrate/Cargo.toml +++ b/crates/icrate/Cargo.toml @@ -78,6 +78,12 @@ required-features = [ "unstable-example-browser" ] +[[example]] +name = "metal" +required-features = [ + "unstable-example-metal" +] + [features] default = ["std", "apple"] @@ -163,6 +169,25 @@ unstable-example-browser = [ "WebKit_WKNavigation", "WebKit_WKWebView", ] +unstable-example-metal = [ + "apple", + "AppKit", + "AppKit_NSWindow", + "Foundation", + "Foundation_NSCoder", + "Foundation_NSError", + "Foundation_NSNotification", + "Foundation_NSString", + "Metal", + "Metal_MTLCompileOptions", + "Metal_MTLRenderPassDescriptor", + "Metal_MTLRenderPipelineColorAttachmentDescriptor", + "Metal_MTLRenderPipelineColorAttachmentDescriptorArray", + "Metal_MTLRenderPipelineDescriptor", + "MetalKit", + "MetalKit_MTKView", + "Metal_MTLRenderPassDescriptor", +] # Helps with CI unstable-frameworks-all = ["unstable-frameworks-ios", "unstable-frameworks-macos-13"] diff --git a/crates/icrate/examples/metal.rs b/crates/icrate/examples/metal.rs new file mode 100644 index 000000000..b5ba8662f --- /dev/null +++ b/crates/icrate/examples/metal.rs @@ -0,0 +1,250 @@ +#![deny(unsafe_op_in_unsafe_fn)] + +use core::ptr::NonNull; + +use icrate::{ + ns_string, + AppKit::{ + NSApplication, NSApplicationActivationPolicyRegular, NSApplicationDelegate, + NSBackingStoreBuffered, NSWindow, NSWindowStyleMaskClosable, NSWindowStyleMaskResizable, + NSWindowStyleMaskTitled, + }, + Foundation::{NSNotification, NSObject, NSObjectProtocol, NSPoint, NSRect, NSSize, NSString}, + Metal::{ + MTLCommandBuffer, MTLCommandEncoder, MTLCommandQueue, MTLCreateSystemDefaultDevice, + MTLDevice, MTLDrawable, MTLLibrary, MTLPrimitiveTypeTriangle, MTLRenderCommandEncoder, + MTLRenderPipelineDescriptor, MTLRenderPipelineState, + }, + MetalKit::{MTKView, MTKViewDelegate}, +}; +use objc2::{ + declare::{Ivar, IvarDrop}, + declare_class, msg_send, msg_send_id, + mutability::InteriorMutable, + rc::Id, + runtime::ProtocolObject, + ClassType, +}; + +// declare the Objective-C class machinery +declare_class!( + // declare the delegate class with our instance variables + struct Delegate { + device: IvarDrop>, "_device">, + command_queue: IvarDrop>, "_command_queue">, + pipeline_state: IvarDrop>, "_pipeline_state">, + window: IvarDrop, "_window">, + mtk_view: IvarDrop, "_mtk_view">, + } + mod ivars; + + // declare the class type + unsafe impl ClassType for Delegate { + type Super = NSObject; + type Mutability = InteriorMutable; + const NAME: &'static str = "Delegate"; + } + + // define the Delegate methods (e.g., initializer) + unsafe impl Delegate { + #[method(initWithShaders:)] + #[allow(non_snake_case)] + unsafe fn __initWithShaders(this: *mut Self, shaders: &NSString) -> Option> { + let this: Option<&mut Self> = msg_send![super(this), init]; + + // get the default device + let device = { + let ptr = unsafe { MTLCreateSystemDefaultDevice() }; + unsafe { Id::retain(ptr) }.expect("Failed to get default system device.") + }; + + // create the command queue + let command_queue = device + .newCommandQueue() + .expect("Failed to create a command queue."); + + // create the app window + let window = { + let this = NSWindow::alloc(); + let content_rect = NSRect::new(NSPoint::new(0., 0.), NSSize::new(1024., 768.)); + let style = NSWindowStyleMaskClosable + | NSWindowStyleMaskResizable + | NSWindowStyleMaskTitled; + let backing_store_type = NSBackingStoreBuffered; + let flag = false; + unsafe { + NSWindow::initWithContentRect_styleMask_backing_defer( + this, + content_rect, + style, + backing_store_type, + flag, + ) + } + }; + + // create the metal view + let mtk_view = { + let this = MTKView::alloc(); + let frame_rect = unsafe { window.frame() }; + unsafe { MTKView::initWithFrame_device(this, frame_rect, Some(&device)) } + }; + + // create the pipeline descriptor + let pipeline_descriptor = MTLRenderPipelineDescriptor::new(); + + unsafe { + pipeline_descriptor + .colorAttachments() + .objectAtIndexedSubscript(0) + .setPixelFormat(mtk_view.colorPixelFormat()); + } + + // create the pipeline state + let library = device + .newLibraryWithSource_options_error(shaders, None) + .expect("Failed to create a library."); + + let vertex_function = library.newFunctionWithName(ns_string!("vertex_main")); + pipeline_descriptor.setVertexFunction(vertex_function.as_deref()); + + let fragment_function = library.newFunctionWithName(ns_string!("fragment_main")); + pipeline_descriptor.setFragmentFunction(fragment_function.as_deref()); + + let pipeline_state = device + .newRenderPipelineStateWithDescriptor_error(&pipeline_descriptor) + .expect("Failed to create a pipeline state."); + + this.map(|this| { + Ivar::write(&mut this.device, device); + Ivar::write(&mut this.command_queue, command_queue); + Ivar::write(&mut this.pipeline_state, pipeline_state); + Ivar::write(&mut this.window, window); + Ivar::write(&mut this.mtk_view, mtk_view); + NonNull::from(this) + }) + } + } + + // define the delegate methods for the `NSApplicationDelegate` protocol + unsafe impl NSApplicationDelegate for Delegate { + #[method(applicationDidFinishLaunching:)] + #[allow(non_snake_case)] + unsafe fn applicationDidFinishLaunching(&self, _notification: &NSNotification) { + // configure the metal view delegate + unsafe { + let object = ProtocolObject::from_ref(self); + self.mtk_view.setDelegate(Some(object)); + } + + // configure the window + unsafe { + self.window.setContentView(Some(&self.mtk_view)); + self.window.center(); + self.window.setTitle(ns_string!("metal example")); + self.window.makeKeyAndOrderFront(None); + } + } + } + + // define the delegate methods for the `MTKViewDelegate` protocol + unsafe impl MTKViewDelegate for Delegate { + #[method(drawInMTKView:)] + #[allow(non_snake_case)] + unsafe fn drawInMTKView(&self, _view: &MTKView) { + let Some(command_buffer) = self.command_queue.commandBuffer() else { return; }; + let Some(pass_descriptor) = (unsafe { self.mtk_view.currentRenderPassDescriptor() }) else { return; }; + let Some(encoder) = command_buffer.renderCommandEncoderWithDescriptor(&pass_descriptor) else { return; }; + + #[rustfmt::skip] + let vertex_data: &mut [f32] = &mut [ + -0.5, -0.5, 0. , 1. , 0. , 0. , + 0.5, -0.5, 0. , 0. , 1. , 0. , + 0. , 0.5, 0. , 0. , 0. , 1. , + ]; + let vertex_bytes = unsafe { + NonNull::new_unchecked(vertex_data.as_mut_ptr().cast::()) + }; + unsafe { + encoder.setVertexBytes_length_atIndex( + vertex_bytes, + vertex_data.len() * core::mem::size_of::(), + 0, + ) + }; + + encoder.setRenderPipelineState(&self.pipeline_state); + unsafe { + encoder.drawPrimitives_vertexStart_vertexCount(MTLPrimitiveTypeTriangle, 0, 3) + }; + encoder.endEncoding(); + + // FIXME: icrate `MTKView` doesn't have a generated binding for `currentDrawable` yet + // (because it needs a definition of `CAMetalDrawable`, which we don't support yet) so + // we have to use a raw `msg_send_id` call here instead. + let current_drawable: Id> = + msg_send_id![&*self.mtk_view, currentDrawable]; + + command_buffer.presentDrawable(¤t_drawable); + + command_buffer.commit(); + } + + #[method(mtkView:drawableSizeWillChange:)] + #[allow(non_snake_case)] + unsafe fn mtkView_drawableSizeWillChange(&self, _view: &MTKView, _size: NSSize) { + println!("mtkView_drawableSizeWillChange"); + } + } +); + +unsafe impl NSObjectProtocol for Delegate {} + +#[cfg(target_os = "macos")] +impl Delegate { + pub fn init_with_shaders(shaders: &NSString) -> Id { + unsafe { msg_send_id![Self::alloc(), initWithShaders: shaders] } + } +} + +fn main() { + let app = unsafe { NSApplication::sharedApplication() }; + unsafe { app.setActivationPolicy(NSApplicationActivationPolicyRegular) }; + + let shaders = ns_string!( + r#" + #include + using namespace metal; + struct VertexIn { + packed_float3 position; + packed_float3 color; + }; + struct VertexOut { + float4 position [[position]]; + float4 color; + }; + vertex VertexOut vertex_main(device const VertexIn *vertices [[buffer(0)]], + uint vertexId [[vertex_id]]) { + VertexOut out; + out.position = float4(vertices[vertexId].position, 1); + out.color = float4(vertices[vertexId].color, 1); + return out; + } + fragment float4 fragment_main(VertexOut in [[stage_in]]) { + return in.color; + } + "# + ); + + // initialize the delegate + let delegate = Delegate::init_with_shaders(shaders); + + // configure the application delegate + unsafe { + let object = ProtocolObject::from_ref(&*delegate); + app.setDelegate(Some(object)) + }; + + // run the app + unsafe { app.run() }; +} From 779dd38e21f81c8e71cdefe095b9e65f825fc029 Mon Sep 17 00:00:00 2001 From: silvanshade Date: Wed, 23 Aug 2023 17:12:24 -0600 Subject: [PATCH 2/5] Fix to handle `currentDrawable` being nullable --- crates/icrate/examples/metal.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/icrate/examples/metal.rs b/crates/icrate/examples/metal.rs index b5ba8662f..40bde3c30 100644 --- a/crates/icrate/examples/metal.rs +++ b/crates/icrate/examples/metal.rs @@ -152,6 +152,13 @@ declare_class!( #[method(drawInMTKView:)] #[allow(non_snake_case)] unsafe fn drawInMTKView(&self, _view: &MTKView) { + // FIXME: icrate `MTKView` doesn't have a generated binding for `currentDrawable` yet + // (because it needs a definition of `CAMetalDrawable`, which we don't support yet) so + // we have to use a raw `msg_send_id` call here instead. + let current_drawable: Option>> = + msg_send_id![&*self.mtk_view, currentDrawable]; + + let Some(current_drawable) = current_drawable else { return; }; let Some(command_buffer) = self.command_queue.commandBuffer() else { return; }; let Some(pass_descriptor) = (unsafe { self.mtk_view.currentRenderPassDescriptor() }) else { return; }; let Some(encoder) = command_buffer.renderCommandEncoderWithDescriptor(&pass_descriptor) else { return; }; @@ -179,12 +186,6 @@ declare_class!( }; encoder.endEncoding(); - // FIXME: icrate `MTKView` doesn't have a generated binding for `currentDrawable` yet - // (because it needs a definition of `CAMetalDrawable`, which we don't support yet) so - // we have to use a raw `msg_send_id` call here instead. - let current_drawable: Id> = - msg_send_id![&*self.mtk_view, currentDrawable]; - command_buffer.presentDrawable(¤t_drawable); command_buffer.commit(); From c7f024080a9004fd12b40537bf378587468e217c Mon Sep 17 00:00:00 2001 From: PicoLeaf <85674246+PicoLeaf@users.noreply.github.com> Date: Sun, 27 Aug 2023 21:58:32 +0200 Subject: [PATCH 3/5] Update the metal example to make the triangle spin --- crates/icrate/examples/metal.rs | 112 ++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 36 deletions(-) diff --git a/crates/icrate/examples/metal.rs b/crates/icrate/examples/metal.rs index 40bde3c30..a694df873 100644 --- a/crates/icrate/examples/metal.rs +++ b/crates/icrate/examples/metal.rs @@ -9,11 +9,14 @@ use icrate::{ NSBackingStoreBuffered, NSWindow, NSWindowStyleMaskClosable, NSWindowStyleMaskResizable, NSWindowStyleMaskTitled, }, - Foundation::{NSNotification, NSObject, NSObjectProtocol, NSPoint, NSRect, NSSize, NSString}, + Foundation::{ + NSDate, NSNotification, NSObject, NSObjectProtocol, NSPoint, NSRect, NSSize, NSString, + }, Metal::{ - MTLCommandBuffer, MTLCommandEncoder, MTLCommandQueue, MTLCreateSystemDefaultDevice, - MTLDevice, MTLDrawable, MTLLibrary, MTLPrimitiveTypeTriangle, MTLRenderCommandEncoder, - MTLRenderPipelineDescriptor, MTLRenderPipelineState, + MTLArgumentEncoder, MTLCommandBuffer, MTLCommandEncoder, MTLCommandQueue, + MTLCreateSystemDefaultDevice, MTLDevice, MTLDrawable, MTLFunction, MTLLibrary, + MTLPrimitiveTypeTriangle, MTLRenderCommandEncoder, MTLRenderPipelineDescriptor, + MTLRenderPipelineState, }, MetalKit::{MTKView, MTKViewDelegate}, }; @@ -33,8 +36,10 @@ declare_class!( device: IvarDrop>, "_device">, command_queue: IvarDrop>, "_command_queue">, pipeline_state: IvarDrop>, "_pipeline_state">, + pipeline_descriptor: IvarDrop, "_pipeline_descriptor">, window: IvarDrop, "_window">, mtk_view: IvarDrop, "_mtk_view">, + start_date: IvarDrop, "_start_date">, } mod ivars; @@ -45,6 +50,27 @@ declare_class!( const NAME: &'static str = "Delegate"; } + // define the delegate methods for the `NSApplicationDelegate` protocol + unsafe impl NSApplicationDelegate for Delegate { + #[method(applicationDidFinishLaunching:)] + #[allow(non_snake_case)] + unsafe fn applicationDidFinishLaunching(&self, _notification: &NSNotification) { + // configure the metal view delegate + unsafe { + let object = ProtocolObject::from_ref(self); + self.mtk_view.setDelegate(Some(object)); + } + + // configure the window + unsafe { + self.window.setContentView(Some(&self.mtk_view)); + self.window.center(); + self.window.setTitle(ns_string!("metal example")); + self.window.makeKeyAndOrderFront(None); + } + } + } + // define the Delegate methods (e.g., initializer) unsafe impl Delegate { #[method(initWithShaders:)] @@ -66,7 +92,7 @@ declare_class!( // create the app window let window = { let this = NSWindow::alloc(); - let content_rect = NSRect::new(NSPoint::new(0., 0.), NSSize::new(1024., 768.)); + let content_rect = NSRect::new(NSPoint::new(0., 0.), NSSize::new(768., 768.)); let style = NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskTitled; @@ -119,34 +145,15 @@ declare_class!( Ivar::write(&mut this.device, device); Ivar::write(&mut this.command_queue, command_queue); Ivar::write(&mut this.pipeline_state, pipeline_state); + Ivar::write(&mut this.pipeline_descriptor, pipeline_descriptor); Ivar::write(&mut this.window, window); Ivar::write(&mut this.mtk_view, mtk_view); + Ivar::write(&mut this.start_date, unsafe { NSDate::now() }); NonNull::from(this) }) } } - // define the delegate methods for the `NSApplicationDelegate` protocol - unsafe impl NSApplicationDelegate for Delegate { - #[method(applicationDidFinishLaunching:)] - #[allow(non_snake_case)] - unsafe fn applicationDidFinishLaunching(&self, _notification: &NSNotification) { - // configure the metal view delegate - unsafe { - let object = ProtocolObject::from_ref(self); - self.mtk_view.setDelegate(Some(object)); - } - - // configure the window - unsafe { - self.window.setContentView(Some(&self.mtk_view)); - self.window.center(); - self.window.setTitle(ns_string!("metal example")); - self.window.makeKeyAndOrderFront(None); - } - } - } - // define the delegate methods for the `MTKViewDelegate` protocol unsafe impl MTKViewDelegate for Delegate { #[method(drawInMTKView:)] @@ -165,10 +172,11 @@ declare_class!( #[rustfmt::skip] let vertex_data: &mut [f32] = &mut [ - -0.5, -0.5, 0. , 1. , 0. , 0. , - 0.5, -0.5, 0. , 0. , 1. , 0. , - 0. , 0.5, 0. , 0. , 0. , 1. , + -f32::sqrt(3.0) / 4.0, -0.25, 0. , 1. , 0. , 0. , + f32::sqrt(3.0) / 4.0, -0.25, 0. , 0. , 1. , 0. , + 0. , 0.5, 0. , 0. , 0. , 1. , ]; + let vertex_bytes = unsafe { NonNull::new_unchecked(vertex_data.as_mut_ptr().cast::()) }; @@ -180,21 +188,45 @@ declare_class!( ) }; + let argument_encoder = unsafe { + self.pipeline_descriptor + .vertexFunction() + .unwrap() + .newArgumentEncoderWithBufferIndex(1) + }; + + let argument_buffer = self + .device + .newBufferWithLength_options(argument_encoder.encodedLength(), 0) + .unwrap(); + + unsafe { + argument_encoder.setArgumentBuffer_offset(Some(&*argument_buffer), 0); + }; + + let time = unsafe { + (argument_encoder.constantDataAtIndex(0).as_ptr() as *mut f32) + .as_mut() + .unwrap() + }; + + *time = unsafe { self.start_date.timeIntervalSinceNow() as f32 }; + + unsafe { encoder.setVertexBuffer_offset_atIndex(Some(&*argument_buffer), 0, 1) }; + encoder.setRenderPipelineState(&self.pipeline_state); unsafe { encoder.drawPrimitives_vertexStart_vertexCount(MTLPrimitiveTypeTriangle, 0, 3) }; encoder.endEncoding(); - command_buffer.presentDrawable(¤t_drawable); - command_buffer.commit(); } #[method(mtkView:drawableSizeWillChange:)] #[allow(non_snake_case)] unsafe fn mtkView_drawableSizeWillChange(&self, _view: &MTKView, _size: NSSize) { - println!("mtkView_drawableSizeWillChange"); + // println!("mtkView_drawableSizeWillChange"); } } ); @@ -216,19 +248,27 @@ fn main() { r#" #include using namespace metal; + struct FragmentShaderArguments { + float time [[id(0)]]; + }; struct VertexIn { packed_float3 position; packed_float3 color; }; + struct VertexOut { float4 position [[position]]; float4 color; }; - vertex VertexOut vertex_main(device const VertexIn *vertices [[buffer(0)]], - uint vertexId [[vertex_id]]) { + vertex VertexOut vertex_main( + device const VertexIn *vertices [[buffer(0)]], + device const FragmentShaderArguments & arg [[buffer(1)]], + uint vertexId [[vertex_id]] + ) { VertexOut out; - out.position = float4(vertices[vertexId].position, 1); - out.color = float4(vertices[vertexId].color, 1); + VertexIn vert = vertices[vertexId]; + out.position = float4(float2x2(cos(arg.time), -sin(arg.time), sin(arg.time), cos(arg.time))*vert.position.xy, vert.position.z, 1); + out.color = float4(vert.color, 1); return out; } fragment float4 fragment_main(VertexOut in [[stage_in]]) { From 6c5e6d8bca4313d050f68149af6f36eaf21941b9 Mon Sep 17 00:00:00 2001 From: PicoLeaf <85674246+PicoLeaf@users.noreply.github.com> Date: Sun, 27 Aug 2023 22:03:18 +0200 Subject: [PATCH 4/5] Add NSDate to the metal example --- crates/icrate/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/icrate/Cargo.toml b/crates/icrate/Cargo.toml index dc6001a4d..9cca4043a 100644 --- a/crates/icrate/Cargo.toml +++ b/crates/icrate/Cargo.toml @@ -178,6 +178,7 @@ unstable-example-metal = [ "Foundation_NSError", "Foundation_NSNotification", "Foundation_NSString", + "Foundation_NSDate", "Metal", "Metal_MTLCompileOptions", "Metal_MTLRenderPassDescriptor", From 851465609481a6e9ffc0af535dc2749ef514dc06 Mon Sep 17 00:00:00 2001 From: silvanshade Date: Sun, 27 Aug 2023 14:56:12 -0600 Subject: [PATCH 5/5] Refactoring --- crates/header-translator/src/output.rs | 1 + crates/icrate/Cargo.toml | 1 + crates/icrate/examples/metal.rs | 345 +++++++++++++++---------- 3 files changed, 213 insertions(+), 134 deletions(-) diff --git a/crates/header-translator/src/output.rs b/crates/header-translator/src/output.rs index f274a56fc..bb33e8f36 100644 --- a/crates/header-translator/src/output.rs +++ b/crates/header-translator/src/output.rs @@ -39,6 +39,7 @@ impl Output { let mut macos_10_13_features: BTreeSet = vec![ "unstable-frameworks-macos-10-7".into(), "unstable-example-delegate".into(), + "unstable-example-metal".into(), "unstable-example-nspasteboard".into(), "unstable-example-speech_synthesis".into(), ] diff --git a/crates/icrate/Cargo.toml b/crates/icrate/Cargo.toml index 9cca4043a..ea05a01d6 100644 --- a/crates/icrate/Cargo.toml +++ b/crates/icrate/Cargo.toml @@ -5311,6 +5311,7 @@ unstable-frameworks-macos-10-13 = [ "Metal_all", "PhotoKit_all", "unstable-example-delegate", + "unstable-example-metal", "unstable-example-nspasteboard", "unstable-example-speech_synthesis", "unstable-frameworks-macos-10-7", diff --git a/crates/icrate/examples/metal.rs b/crates/icrate/examples/metal.rs index a694df873..90408c21b 100644 --- a/crates/icrate/examples/metal.rs +++ b/crates/icrate/examples/metal.rs @@ -1,22 +1,20 @@ #![deny(unsafe_op_in_unsafe_fn)] -use core::ptr::NonNull; +use core::{cell::RefCell, ptr::NonNull}; use icrate::{ - ns_string, AppKit::{ NSApplication, NSApplicationActivationPolicyRegular, NSApplicationDelegate, NSBackingStoreBuffered, NSWindow, NSWindowStyleMaskClosable, NSWindowStyleMaskResizable, NSWindowStyleMaskTitled, }, Foundation::{ - NSDate, NSNotification, NSObject, NSObjectProtocol, NSPoint, NSRect, NSSize, NSString, + ns_string, NSDate, NSNotification, NSObject, NSObjectProtocol, NSPoint, NSRect, NSSize, }, Metal::{ - MTLArgumentEncoder, MTLCommandBuffer, MTLCommandEncoder, MTLCommandQueue, - MTLCreateSystemDefaultDevice, MTLDevice, MTLDrawable, MTLFunction, MTLLibrary, - MTLPrimitiveTypeTriangle, MTLRenderCommandEncoder, MTLRenderPipelineDescriptor, - MTLRenderPipelineState, + MTLCommandBuffer, MTLCommandEncoder, MTLCommandQueue, MTLCreateSystemDefaultDevice, + MTLDevice, MTLDrawable, MTLLibrary, MTLPrimitiveTypeTriangle, MTLRenderCommandEncoder, + MTLRenderPipelineDescriptor, MTLRenderPipelineState, }, MetalKit::{MTKView, MTKViewDelegate}, }; @@ -29,17 +27,91 @@ use objc2::{ ClassType, }; +#[rustfmt::skip] +const SHADERS: &str = r#" + #include + + struct SceneProperties { + float time; + }; + + struct VertexInput { + metal::packed_float3 position; + metal::packed_float3 color; + }; + + struct VertexOutput { + metal::float4 position [[position]]; + metal::float4 color; + }; + + vertex VertexOutput vertex_main( + device const SceneProperties& properties [[buffer(0)]], + device const VertexInput* vertices [[buffer(1)]], + uint vertex_idx [[vertex_id]] + ) { + VertexOutput out; + VertexInput in = vertices[vertex_idx]; + out.position = + metal::float4( + metal::float2x2( + metal::cos(properties.time), -metal::sin(properties.time), + metal::sin(properties.time), metal::cos(properties.time) + ) * in.position.xy, + in.position.z, + 1); + out.color = metal::float4(in.color, 1); + return out; + } + + fragment metal::float4 fragment_main(VertexOutput in [[stage_in]]) { + return in.color; + } +"#; + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct SceneProperties { + pub time: f32, +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct VertexInput { + pub position: Position, + pub color: Color, +} + +#[derive(Copy, Clone)] +// NOTE: this has the same ABI as `MTLPackedFloat3` +#[repr(C)] +pub struct Position { + pub x: f32, + pub y: f32, + pub z: f32, +} + +#[derive(Copy, Clone)] +// NOTE: this has the same ABI as `MTLPackedFloat3` +#[repr(C)] +pub struct Color { + pub r: f32, + pub g: f32, + pub b: f32, +} + +type IdCell = Box>>>; + // declare the Objective-C class machinery declare_class!( // declare the delegate class with our instance variables + #[rustfmt::skip] // FIXME: rustfmt breaks the macro parsing apparently struct Delegate { - device: IvarDrop>, "_device">, - command_queue: IvarDrop>, "_command_queue">, - pipeline_state: IvarDrop>, "_pipeline_state">, - pipeline_descriptor: IvarDrop, "_pipeline_descriptor">, - window: IvarDrop, "_window">, - mtk_view: IvarDrop, "_mtk_view">, start_date: IvarDrop, "_start_date">, + command_queue: IvarDrop>, "_command_queue">, + pipeline_state: IvarDrop>, "_pipeline_state">, + window: IvarDrop, "_window">, + mtk_view: IvarDrop, "_mtk_view">, } mod ivars; @@ -50,45 +122,28 @@ declare_class!( const NAME: &'static str = "Delegate"; } - // define the delegate methods for the `NSApplicationDelegate` protocol - unsafe impl NSApplicationDelegate for Delegate { - #[method(applicationDidFinishLaunching:)] - #[allow(non_snake_case)] - unsafe fn applicationDidFinishLaunching(&self, _notification: &NSNotification) { - // configure the metal view delegate - unsafe { - let object = ProtocolObject::from_ref(self); - self.mtk_view.setDelegate(Some(object)); - } - - // configure the window - unsafe { - self.window.setContentView(Some(&self.mtk_view)); - self.window.center(); - self.window.setTitle(ns_string!("metal example")); - self.window.makeKeyAndOrderFront(None); - } - } - } - // define the Delegate methods (e.g., initializer) unsafe impl Delegate { - #[method(initWithShaders:)] + #[method(init)] #[allow(non_snake_case)] - unsafe fn __initWithShaders(this: *mut Self, shaders: &NSString) -> Option> { + unsafe fn init(this: *mut Self) -> Option> { let this: Option<&mut Self> = msg_send![super(this), init]; + this.map(|this| { + Ivar::write(&mut this.start_date, unsafe { NSDate::now() }); + Ivar::write(&mut this.command_queue, IdCell::default()); + Ivar::write(&mut this.pipeline_state, IdCell::default()); + Ivar::write(&mut this.window, IdCell::default()); + Ivar::write(&mut this.mtk_view, IdCell::default()); + NonNull::from(this) + }) + } + } - // get the default device - let device = { - let ptr = unsafe { MTLCreateSystemDefaultDevice() }; - unsafe { Id::retain(ptr) }.expect("Failed to get default system device.") - }; - - // create the command queue - let command_queue = device - .newCommandQueue() - .expect("Failed to create a command queue."); - + // define the delegate methods for the `NSApplicationDelegate` protocol + unsafe impl NSApplicationDelegate for Delegate { + #[method(applicationDidFinishLaunching:)] + #[allow(non_snake_case)] + unsafe fn applicationDidFinishLaunching(&self, _notification: &NSNotification) { // create the app window let window = { let this = NSWindow::alloc(); @@ -109,6 +164,17 @@ declare_class!( } }; + // get the default device + let device = { + let ptr = unsafe { MTLCreateSystemDefaultDevice() }; + unsafe { Id::retain(ptr) }.expect("Failed to get default system device.") + }; + + // create the command queue + let command_queue = device + .newCommandQueue() + .expect("Failed to create a command queue."); + // create the metal view let mtk_view = { let this = MTKView::alloc(); @@ -126,31 +192,43 @@ declare_class!( .setPixelFormat(mtk_view.colorPixelFormat()); } - // create the pipeline state + // compile the shaders let library = device - .newLibraryWithSource_options_error(shaders, None) + .newLibraryWithSource_options_error(ns_string!(SHADERS), None) .expect("Failed to create a library."); + // configure the vertex shader let vertex_function = library.newFunctionWithName(ns_string!("vertex_main")); pipeline_descriptor.setVertexFunction(vertex_function.as_deref()); + // configure the fragment shader let fragment_function = library.newFunctionWithName(ns_string!("fragment_main")); pipeline_descriptor.setFragmentFunction(fragment_function.as_deref()); + // create the pipeline state let pipeline_state = device .newRenderPipelineStateWithDescriptor_error(&pipeline_descriptor) .expect("Failed to create a pipeline state."); - this.map(|this| { - Ivar::write(&mut this.device, device); - Ivar::write(&mut this.command_queue, command_queue); - Ivar::write(&mut this.pipeline_state, pipeline_state); - Ivar::write(&mut this.pipeline_descriptor, pipeline_descriptor); - Ivar::write(&mut this.window, window); - Ivar::write(&mut this.mtk_view, mtk_view); - Ivar::write(&mut this.start_date, unsafe { NSDate::now() }); - NonNull::from(this) - }) + // configure the metal view delegate + unsafe { + let object = ProtocolObject::from_ref(self); + mtk_view.setDelegate(Some(object)); + } + + // configure the window + unsafe { + window.setContentView(Some(&mtk_view)); + window.center(); + window.setTitle(ns_string!("metal example")); + window.makeKeyAndOrderFront(None); + } + + // initialize the delegate state + self.command_queue.borrow_mut().replace(command_queue); + self.pipeline_state.borrow_mut().replace(pipeline_state); + self.window.borrow_mut().replace(window); + self.mtk_view.borrow_mut().replace(mtk_view); } } @@ -159,66 +237,98 @@ declare_class!( #[method(drawInMTKView:)] #[allow(non_snake_case)] unsafe fn drawInMTKView(&self, _view: &MTKView) { + let command_queue = self.command_queue.borrow(); + let Some(command_queue) = command_queue.as_ref() else { return; }; + + let mtk_view = self.mtk_view.borrow(); + let Some(mtk_view) = mtk_view.as_ref() else { return; }; + + let pipeline_state = self.pipeline_state.borrow(); + let Some(pipeline_state) = pipeline_state.as_ref() else { return; }; + // FIXME: icrate `MTKView` doesn't have a generated binding for `currentDrawable` yet // (because it needs a definition of `CAMetalDrawable`, which we don't support yet) so // we have to use a raw `msg_send_id` call here instead. let current_drawable: Option>> = - msg_send_id![&*self.mtk_view, currentDrawable]; + msg_send_id![mtk_view, currentDrawable]; + // prepare for drawing let Some(current_drawable) = current_drawable else { return; }; - let Some(command_buffer) = self.command_queue.commandBuffer() else { return; }; - let Some(pass_descriptor) = (unsafe { self.mtk_view.currentRenderPassDescriptor() }) else { return; }; + let Some(command_buffer) = command_queue.commandBuffer() else { return; }; + let Some(pass_descriptor) = (unsafe { mtk_view.currentRenderPassDescriptor() }) else { return; }; let Some(encoder) = command_buffer.renderCommandEncoderWithDescriptor(&pass_descriptor) else { return; }; - #[rustfmt::skip] - let vertex_data: &mut [f32] = &mut [ - -f32::sqrt(3.0) / 4.0, -0.25, 0. , 1. , 0. , 0. , - f32::sqrt(3.0) / 4.0, -0.25, 0. , 0. , 1. , 0. , - 0. , 0.5, 0. , 0. , 0. , 1. , - ]; - - let vertex_bytes = unsafe { - NonNull::new_unchecked(vertex_data.as_mut_ptr().cast::()) + // compute the scene properties + let scene_properties_data = &SceneProperties { + time: unsafe { self.start_date.timeIntervalSinceNow() } as f32, }; + // write the scene properties to the vertex shader argument buffer at index 0 + let scene_properties_bytes = NonNull::from(scene_properties_data); unsafe { encoder.setVertexBytes_length_atIndex( - vertex_bytes, - vertex_data.len() * core::mem::size_of::(), + scene_properties_bytes.cast::(), + core::mem::size_of_val(scene_properties_data), 0, ) }; - let argument_encoder = unsafe { - self.pipeline_descriptor - .vertexFunction() - .unwrap() - .newArgumentEncoderWithBufferIndex(1) - }; - - let argument_buffer = self - .device - .newBufferWithLength_options(argument_encoder.encodedLength(), 0) - .unwrap(); - + // compute the triangle geometry + let vertex_input_data: &[VertexInput] = &[ + VertexInput { + position: Position { + x: -f32::sqrt(3.0) / 4.0, + y: -0.25, + z: 0., + }, + color: Color { + r: 1., + g: 0., + b: 0., + }, + }, + VertexInput { + position: Position { + x: f32::sqrt(3.0) / 4.0, + y: -0.25, + z: 0., + }, + color: Color { + r: 0., + g: 1., + b: 0., + }, + }, + VertexInput { + position: Position { + x: 0., + y: 0.5, + z: 0., + }, + color: Color { + r: 0., + g: 0., + b: 1., + }, + }, + ]; + // write the triangle geometry to the vertex shader argument buffer at index 1 + let vertex_input_bytes = NonNull::from(vertex_input_data); unsafe { - argument_encoder.setArgumentBuffer_offset(Some(&*argument_buffer), 0); - }; - - let time = unsafe { - (argument_encoder.constantDataAtIndex(0).as_ptr() as *mut f32) - .as_mut() - .unwrap() + encoder.setVertexBytes_length_atIndex( + vertex_input_bytes.cast::(), + core::mem::size_of_val(vertex_input_data), + 1, + ) }; - - *time = unsafe { self.start_date.timeIntervalSinceNow() as f32 }; - unsafe { encoder.setVertexBuffer_offset_atIndex(Some(&*argument_buffer), 0, 1) }; - - encoder.setRenderPipelineState(&self.pipeline_state); + // configure the encoder with the pipeline and draw the triangle + encoder.setRenderPipelineState(pipeline_state); unsafe { encoder.drawPrimitives_vertexStart_vertexCount(MTLPrimitiveTypeTriangle, 0, 3) }; encoder.endEncoding(); + + // schedule the command buffer for display and commit command_buffer.presentDrawable(¤t_drawable); command_buffer.commit(); } @@ -233,52 +343,19 @@ declare_class!( unsafe impl NSObjectProtocol for Delegate {} -#[cfg(target_os = "macos")] impl Delegate { - pub fn init_with_shaders(shaders: &NSString) -> Id { - unsafe { msg_send_id![Self::alloc(), initWithShaders: shaders] } + pub fn new() -> Id { + unsafe { msg_send_id![Self::alloc(), init] } } } fn main() { + // configure the app let app = unsafe { NSApplication::sharedApplication() }; unsafe { app.setActivationPolicy(NSApplicationActivationPolicyRegular) }; - let shaders = ns_string!( - r#" - #include - using namespace metal; - struct FragmentShaderArguments { - float time [[id(0)]]; - }; - struct VertexIn { - packed_float3 position; - packed_float3 color; - }; - - struct VertexOut { - float4 position [[position]]; - float4 color; - }; - vertex VertexOut vertex_main( - device const VertexIn *vertices [[buffer(0)]], - device const FragmentShaderArguments & arg [[buffer(1)]], - uint vertexId [[vertex_id]] - ) { - VertexOut out; - VertexIn vert = vertices[vertexId]; - out.position = float4(float2x2(cos(arg.time), -sin(arg.time), sin(arg.time), cos(arg.time))*vert.position.xy, vert.position.z, 1); - out.color = float4(vert.color, 1); - return out; - } - fragment float4 fragment_main(VertexOut in [[stage_in]]) { - return in.color; - } - "# - ); - // initialize the delegate - let delegate = Delegate::init_with_shaders(shaders); + let delegate = Delegate::new(); // configure the application delegate unsafe {