xamarin-macios/msbuild/tests/MyMetalGame/GameViewController.cs

397 строки
13 KiB
C#

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<float> (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);
if (ObjCRuntime.Runtime.Arch == ObjCRuntime.Arch.SIMULATOR)
desc.StorageMode = MTLStorageMode.Private;
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;
}
}
}