using System; using System.Runtime.InteropServices; using System.Threading; using CoreAnimation; using CoreGraphics; using Foundation; using OpenTK; using Metal; using UIKit; namespace MyMetalGame { public partial class GameViewController : UIViewController { struct Uniforms { public Matrix4 ModelviewProjectionMatrix; public Matrix4 NormalMatrix; } // The max number of command buffers in flight const int max_inflight_buffers = 3; // Max API memory buffer size const int max_bytes_per_frame = 1024 * 1024; float[] cubeVertexData = { // Data layout for each line below is: // positionX, positionY, positionZ, normalX, normalY, normalZ, 0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, -0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f }; // Layer CAMetalLayer metalLayer; bool layerSizeDidUpdate; MTLRenderPassDescriptor renderPassDescriptor; // Controller CADisplayLink timer; Semaphore inflightSemaphore; IMTLBuffer dynamicConstantBuffer; byte constantDataBufferIndex; // Renderer IMTLDevice device; IMTLCommandQueue commandQueue; IMTLLibrary defaultLibrary; IMTLRenderPipelineState pipelineState; IMTLBuffer vertexBuffer; IMTLDepthStencilState depthState; IMTLTexture depthTex; // Uniforms Matrix4 projectionMatrix; Matrix4 viewMatrix; Uniforms uniformBuffer; float rotation; public GameViewController (IntPtr handle) : base (handle) { } public override void ViewDidLoad () { base.ViewDidLoad (); constantDataBufferIndex = 0; inflightSemaphore = new Semaphore (max_inflight_buffers, max_inflight_buffers); SetupMetal (); LoadAssets (); timer = CADisplayLink.Create (Gameloop); timer.FrameInterval = 1; timer.AddToRunLoop (NSRunLoop.Main, NSRunLoop.NSDefaultRunLoopMode); } public override void DidReceiveMemoryWarning () { base.DidReceiveMemoryWarning (); // Dispose of any resources that can be recreated. } void SetupMetal () { // Find a usable device device = MTLDevice.SystemDefault; // Create a new command queue commandQueue = device.CreateCommandQueue (); // Load all the shader files with a metal file extension in the project NSError error; defaultLibrary = device.CreateLibrary ("default.metallib", out error); // Setup metal layer and add as sub layer to view metalLayer = new CAMetalLayer (); metalLayer.Device = device; metalLayer.PixelFormat = MTLPixelFormat.BGRA8Unorm; // Change this to NO if the compute encoder is used as the last pass on the drawable texture metalLayer.FramebufferOnly = true; // Add metal layer to the views layer hierarchy metalLayer.Frame = View.Layer.Frame; View.Layer.AddSublayer (metalLayer); View.Opaque = true; View.BackgroundColor = null; View.ContentScaleFactor = UIScreen.MainScreen.Scale; } void LoadAssets () { // Allocate one region of memory for the uniform buffer dynamicConstantBuffer = device.CreateBuffer (max_bytes_per_frame, 0); dynamicConstantBuffer.Label = "UniformBuffer"; // Load the fragment program into the library IMTLFunction fragmentProgram = defaultLibrary.CreateFunction ("lighting_fragment"); // Load the vertex program into the library IMTLFunction vertexProgram = defaultLibrary.CreateFunction ("lighting_vertex"); // Setup the vertex buffers vertexBuffer = device.CreateBuffer (cubeVertexData, (MTLResourceOptions)0); vertexBuffer.Label = "Vertices"; // Create a reusable pipeline state var pipelineStateDescriptor = new MTLRenderPipelineDescriptor { Label = "MyPipeline", SampleCount = 1, VertexFunction = vertexProgram, FragmentFunction = fragmentProgram, DepthAttachmentPixelFormat = MTLPixelFormat.Depth32Float }; pipelineStateDescriptor.ColorAttachments [0].PixelFormat = MTLPixelFormat.BGRA8Unorm; NSError error; pipelineState = device.CreateRenderPipelineState (pipelineStateDescriptor, out error); if (pipelineState == null) Console.WriteLine ("Failed to created pipeline state, error " + error); var depthStateDesc = new MTLDepthStencilDescriptor { DepthCompareFunction = MTLCompareFunction.Less, DepthWriteEnabled = true }; depthState = device.CreateDepthStencilState (depthStateDesc); } void SetupRenderPassDescriptorForTexture (IMTLTexture texture) { if (renderPassDescriptor == null) renderPassDescriptor = MTLRenderPassDescriptor.CreateRenderPassDescriptor (); renderPassDescriptor.ColorAttachments [0].Texture = texture; renderPassDescriptor.ColorAttachments [0].LoadAction = MTLLoadAction.Clear; renderPassDescriptor.ColorAttachments [0].ClearColor = new MTLClearColor (0.65f, 0.65f, 0.65f, 1.0f); renderPassDescriptor.ColorAttachments [0].StoreAction = MTLStoreAction.Store; if (depthTex == null || (depthTex.Width != texture.Width || depthTex.Height != texture.Height)) { // If we need a depth texture and don't have one, or if the depth texture we have is the wrong size // Then allocate one of the proper size MTLTextureDescriptor desc = MTLTextureDescriptor.CreateTexture2DDescriptor (MTLPixelFormat.Depth32Float, texture.Width, texture.Height, false); depthTex = device.CreateTexture (desc); depthTex.Label = "Depth"; renderPassDescriptor.DepthAttachment.Texture = depthTex; renderPassDescriptor.DepthAttachment.LoadAction = MTLLoadAction.Clear; renderPassDescriptor.DepthAttachment.ClearDepth = 1.0f; renderPassDescriptor.DepthAttachment.StoreAction = MTLStoreAction.DontCare; } } void Render () { inflightSemaphore.WaitOne (); Update (); // Create a new command buffer for each renderpass to the current drawable IMTLCommandBuffer commandBuffer = commandQueue.CommandBuffer (); commandBuffer.Label = "MyCommand"; // Obtain a drawable texture for this render pass and set up the renderpass descriptor for the command encoder to render into ICAMetalDrawable drawable = GetCurrentDrawable (); SetupRenderPassDescriptorForTexture (drawable.Texture); // Create a render command encoder so we can render into something IMTLRenderCommandEncoder renderEncoder = commandBuffer.CreateRenderCommandEncoder (renderPassDescriptor); renderEncoder.Label = "MyRenderEncoder"; renderEncoder.SetDepthStencilState (depthState); // Set context state renderEncoder.PushDebugGroup ("DrawCube"); renderEncoder.SetRenderPipelineState (pipelineState); renderEncoder.SetVertexBuffer (vertexBuffer, 0, 0); renderEncoder.SetVertexBuffer (dynamicConstantBuffer, (nuint)(Marshal.SizeOf (typeof(Uniforms)) * constantDataBufferIndex), 1); // Tell the render context we want to draw our primitives renderEncoder.DrawPrimitives (MTLPrimitiveType.Triangle, 0, 36, 1); renderEncoder.PopDebugGroup (); // We're done encoding commands renderEncoder.EndEncoding (); // Call the view's completion handler which is required by the view since it will signal its semaphore and set up the next buffer commandBuffer.AddCompletedHandler (buffer => { drawable.Dispose (); inflightSemaphore.Release (); }); // Schedule a present once the framebuffer is complete commandBuffer.PresentDrawable (drawable); // Finalize rendering here & push the command buffer to the GPU commandBuffer.Commit (); // The renderview assumes it can now increment the buffer index and that the previous index won't be touched until we cycle back around to the same index constantDataBufferIndex = (byte)((constantDataBufferIndex + 1) % max_inflight_buffers); } void Reshape () { // When reshape is called, update the view and projection matricies since this means the view orientation or size changed var aspect = (float)(View.Bounds.Size.Width / View.Bounds.Size.Height); projectionMatrix = CreateMatrixFromPerspective (65.0f * ((float)Math.PI / 180.0f), aspect, 0.1f, 100.0f); viewMatrix = Matrix4.Identity; } void Update () { var baseModel = Matrix4.Mult (CreateMatrixFromTranslation (0.0f, 0.0f, 5.0f), CreateMatrixFromRotation (rotation, 0.0f, 1.0f, 0.0f)); var baseMv = Matrix4.Mult (viewMatrix, baseModel); var modelViewMatrix = Matrix4.Mult (baseMv, CreateMatrixFromRotation (rotation, 1.0f, 1.0f, 1.0f)); uniformBuffer.NormalMatrix = Matrix4.Invert (Matrix4.Transpose (modelViewMatrix)); uniformBuffer.ModelviewProjectionMatrix = Matrix4.Transpose (Matrix4.Mult (projectionMatrix, modelViewMatrix)); // Copy uniformBuffer's content into dynamicConstantBuffer.Contents int rawsize = Marshal.SizeOf (typeof(Uniforms)); var rawdata = new byte[rawsize]; IntPtr ptr = Marshal.AllocHGlobal (rawsize); Marshal.StructureToPtr (uniformBuffer, ptr, false); Marshal.Copy (ptr, rawdata, 0, rawsize); Marshal.FreeHGlobal (ptr); Marshal.Copy (rawdata, 0, dynamicConstantBuffer.Contents + rawsize * constantDataBufferIndex, rawsize); rotation += 0.01f; } // The main game loop called by the CADisplayLine timer public void Gameloop () { if (layerSizeDidUpdate) { CGSize drawableSize = View.Bounds.Size; drawableSize.Width *= View.ContentScaleFactor; drawableSize.Height *= View.ContentScaleFactor; metalLayer.DrawableSize = drawableSize; Reshape (); layerSizeDidUpdate = false; } Render (); } // Called whenever view changes orientation or layout is changed public override void ViewDidLayoutSubviews () { base.ViewDidLayoutSubviews (); layerSizeDidUpdate = true; metalLayer.Frame = View.Layer.Frame; } public override bool ShouldAutorotate () { return true; } public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations () { if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone) return UIInterfaceOrientationMask.AllButUpsideDown; return UIInterfaceOrientationMask.All; } ICAMetalDrawable GetCurrentDrawable () { ICAMetalDrawable currentDrawable = null; while (currentDrawable == null) { currentDrawable = metalLayer.NextDrawable (); if (currentDrawable == null) Console.WriteLine ("CurrentDrawable is null"); } return currentDrawable; } static Matrix4 CreateMatrixFromPerspective (float fovY, float aspect, float nearZ, float farZ) { float yscale = 1.0f / (float)Math.Tan (fovY * 0.5f); float xscale = yscale / aspect; float q = farZ / (farZ - nearZ); var m = new Matrix4 { Row0 = new Vector4 (xscale, 0.0f, 0.0f, 0.0f), Row1 = new Vector4 (0.0f, yscale, 0.0f, 0.0f), Row2 = new Vector4 (0.0f, 0.0f, q, q * -nearZ), Row3 = new Vector4 (0.0f, 0.0f, 1.0f, 0.0f) }; return m; } static Matrix4 CreateMatrixFromTranslation (float x, float y, float z) { var m = Matrix4.Identity; m.Row0.W = x; m.Row1.W = y; m.Row2.W = z; m.Row3.W = 1.0f; return m; } static Matrix4 CreateMatrixFromRotation (float radians, float x, float y, float z) { Vector3 v = Vector3.Normalize (new Vector3 (x, y, z)); var cos = (float)Math.Cos (radians); var sin = (float)Math.Sin (radians); float cosp = 1.0f - cos; var m = new Matrix4 { Row0 = new Vector4 (cos + cosp * v.X * v.X, cosp * v.X * v.Y - v.Z * sin, cosp * v.X * v.Z + v.Y * sin, 0.0f), Row1 = new Vector4 (cosp * v.X * v.Y + v.Z * sin, cos + cosp * v.Y * v.Y, cosp * v.Y * v.Z - v.X * sin, 0.0f), Row2 = new Vector4 (cosp * v.X * v.Z - v.Y * sin, cosp * v.Y * v.Z + v.X * sin, cos + cosp * v.Z * v.Z, 0.0f), Row3 = new Vector4 (0.0f, 0.0f, 0.0f, 1.0f) }; return m; } } }