#![cfg(feature = "run_test")] #![deny(unsafe_op_in_unsafe_fn)] use core::{cell::OnceCell, ptr::NonNull}; use std::sync::RwLock; use icrate::Foundation::NSString; use icrate::Metal::{ MTLBlitCommandEncoder, MTLClearColor, MTLResource, MTLTexture, MTLTextureDescriptor, MTLTextureUsagePixelFormatView, MTLTextureUsageRenderTarget, MTLTextureUsageShaderRead, }; use icrate::{ AppKit::{ NSApplication, NSApplicationActivationPolicyRegular, NSApplicationDelegate, NSBackingStoreBuffered, NSWindow, NSWindowStyleMaskClosable, NSWindowStyleMaskResizable, NSWindowStyleMaskTitled, }, Foundation::{ ns_string, MainThreadMarker, NSDate, NSNotification, NSObject, NSObjectProtocol, NSPoint, NSRect, NSSize, }, Metal::{ MTLCommandBuffer, MTLCommandEncoder, MTLCommandQueue, MTLCreateSystemDefaultDevice, MTLDevice, MTLDrawable, MTLLibrary, MTLPrimitiveTypeTriangle, MTLRenderCommandEncoder, MTLRenderPipelineDescriptor, MTLRenderPipelineState, }, MetalKit::{MTKView, MTKViewDelegate}, }; use librashader_common::Viewport; use librashader_presets::ShaderPreset; use librashader_runtime_mtl::FilterChainMetal; use objc2::{ declare_class, msg_send_id, mutability::MainThreadOnly, rc::Id, runtime::ProtocolObject, ClassType, DeclaredClass, }; #[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, } macro_rules! idcell { ($name:ident => $this:expr) => { $this.ivars().$name.set($name).expect(&format!( "ivar should not already be initialized: `{}`", stringify!($name) )); }; ($name:ident <= $this:expr) => { #[rustfmt::skip] let Some($name) = $this.ivars().$name.get() else { unreachable!( "ivar should be initialized: `{}`", stringify!($name) ) }; }; } // declare the desired instance variables struct Ivars { start_date: Id, command_queue: OnceCell>>, pipeline_state: OnceCell>>, filter_chain: OnceCell>, window: OnceCell>, } // declare the Objective-C class machinery declare_class!( struct Delegate; // SAFETY: // - The superclass NSObject does not have any subclassing requirements. // - Main thread only mutability is correct, since this is an application delegate. // - `Delegate` does not implement `Drop`. unsafe impl ClassType for Delegate { type Super = NSObject; type Mutability = MainThreadOnly; const NAME: &'static str = "Delegate"; } impl DeclaredClass for Delegate { type Ivars = Ivars; } unsafe impl NSObjectProtocol for 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) { let mtm = MainThreadMarker::from(self); // create the app window let window = { let content_rect = NSRect::new(NSPoint::new(0., 0.), NSSize::new(768., 768.)); let style = NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskTitled; let backing_store_type = NSBackingStoreBuffered; let flag = false; unsafe { NSWindow::initWithContentRect_styleMask_backing_defer( mtm.alloc(), content_rect, style, backing_store_type, flag, ) } }; // 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 frame_rect = window.frame(); unsafe { MTKView::initWithFrame_device(mtm.alloc(), frame_rect, Some(&device)) } }; // create the pipeline descriptor let pipeline_descriptor = MTLRenderPipelineDescriptor::new(); unsafe { pipeline_descriptor .colorAttachments() .objectAtIndexedSubscript(0) .setPixelFormat(mtk_view.colorPixelFormat()); } // compile the shaders let library = device .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."); // let preset = ShaderPreset::try_parse("./test/shaders_slang/crt/crt-lottes.slangp").unwrap(); let preset = ShaderPreset::try_parse("./test/shaders_slang/crt/crt-royale.slangp").unwrap(); // let preset = ShaderPreset::try_parse("./test/basic.slangp").unwrap(); let filter_chain = FilterChainMetal::load_from_preset( preset, &command_queue, None, ) .unwrap(); let filter_chain = RwLock::new(filter_chain); // configure the metal view delegate unsafe { let object = ProtocolObject::from_ref(self); mtk_view.setDelegate(Some(object)); } // configure the window window.setContentView(Some(&mtk_view)); window.center(); window.setTitle(ns_string!("metal example")); window.makeKeyAndOrderFront(None); // initialize the delegate state idcell!(command_queue => self); idcell!(pipeline_state => self); idcell!(filter_chain => self); idcell!(window => self); } } // define the delegate methods for the `MTKViewDelegate` protocol unsafe impl MTKViewDelegate for Delegate { #[method(drawInMTKView:)] #[allow(non_snake_case)] unsafe fn drawInMTKView(&self, mtk_view: &MTKView) { idcell!(command_queue <= self); idcell!(pipeline_state <= self); idcell!(filter_chain <= self); unsafe { mtk_view.setFramebufferOnly(false); mtk_view.setClearColor(MTLClearColor { red: 0.3, blue: 0.5, green: 0.3, alpha: 0.0, }); } // 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![mtk_view, currentDrawable]; // prepare for drawing let Some(current_drawable) = current_drawable 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; }; // compute the scene properties let scene_properties_data = &SceneProperties { // time: unsafe { self.ivars().start_date.timeIntervalSinceNow() } as f32, time: 0.0 }; // 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( scene_properties_bytes.cast::(), core::mem::size_of_val(scene_properties_data), 0, ) }; // 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 { encoder.setVertexBytes_length_atIndex( vertex_input_bytes.cast::(), core::mem::size_of_val(vertex_input_data), 1, ) }; // configure the encoder with the pipeline and draw the triangle encoder.setRenderPipelineState(pipeline_state); unsafe { encoder.drawPrimitives_vertexStart_vertexCount(MTLPrimitiveTypeTriangle, 0, 3) }; encoder.endEncoding(); unsafe { let mut filter_chain = filter_chain.write().unwrap(); let texture = pass_descriptor .colorAttachments() .objectAtIndexedSubscript(0) .texture() .unwrap(); let tex_desc = MTLTextureDescriptor::texture2DDescriptorWithPixelFormat_width_height_mipmapped( texture.pixelFormat(), texture.width(), texture.height(), false ); tex_desc.setUsage(MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead | MTLTextureUsagePixelFormatView); // tex_desc.setPixelFormat(MTLPixelFormatRGBA8Unorm); let frontbuffer = command_queue .device() .newTextureWithDescriptor(&tex_desc) .unwrap(); frontbuffer .setLabel(Some(&*NSString::from_str("librashader frontbuffer"))); let backbuffer = command_queue .device() .newTextureWithDescriptor(&tex_desc) .unwrap(); backbuffer .setLabel(Some(&*NSString::from_str("librashader backbuffer"))); let blit = command_buffer .blitCommandEncoder() .unwrap(); blit.copyFromTexture_toTexture(&texture, &frontbuffer); blit.endEncoding(); filter_chain.frame(&frontbuffer, &Viewport { x: 0.0, y: 0.0, mvp: None, output: &backbuffer }, &command_buffer, 1, None) .expect("frame"); // let rpass = MTLRenderPassDescriptor::new(); // rpass.colorAttachments() // .objectAtIndexedSubscript(0) // .setTexture(Some(&backbuffer)); // rpass.colorAttachments() // .objectAtIndexedSubscript(0) // .setLoadAction(MTLLoadActionClear); // rpass.colorAttachments() // .objectAtIndexedSubscript(0) // .setStoreAction(MTLStoreActionStore); // // let clear = command_buffer // .renderCommandEncoderWithDescriptor(&*rpass) // .unwrap(); // // // clear.endEncoding(); let blit = command_buffer .blitCommandEncoder() .unwrap(); blit.copyFromTexture_toTexture(&backbuffer, &texture); blit.endEncoding(); } // schedule the command buffer for display and commit 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"); } } ); impl Delegate { pub fn new(mtm: MainThreadMarker) -> Id { let this = mtm.alloc(); let this = this.set_ivars(Ivars { start_date: unsafe { NSDate::now() }, command_queue: OnceCell::default(), pipeline_state: OnceCell::default(), filter_chain: OnceCell::default(), window: OnceCell::default(), }); unsafe { msg_send_id![super(this), init] } } } fn main() { let mtm = MainThreadMarker::new().unwrap(); // configure the app let app = NSApplication::sharedApplication(mtm); app.setActivationPolicy(NSApplicationActivationPolicyRegular); // configure the application delegate let delegate = Delegate::new(mtm); let object = ProtocolObject::from_ref(&*delegate); app.setDelegate(Some(object)); // run the app unsafe { app.run() }; }