Преглед на файлове

Implemented a Metal based renderer on macOS (#1450)

Jorrit Rouwe преди 8 месеца
родител
ревизия
582b1ef819
променени са 32 файла, в които са добавени 1272 реда и са изтрити 64 реда
  1. 1 0
      .gitignore
  2. 39 0
      Assets/Shaders/MTL/FontShader.metal
  3. 30 0
      Assets/Shaders/MTL/LineShader.metal
  4. 199 0
      Assets/Shaders/MTL/TriangleShader.metal
  5. 41 0
      Assets/Shaders/MTL/UIShader.metal
  6. 13 0
      Assets/Shaders/MTL/VertexConstants.h
  7. 1 1
      Build/CMakeLists.txt
  8. 1 2
      Build/README.md
  9. 2 1
      Docs/ReleaseNotes.md
  10. 1 12
      TestFramework/Application/Application.cpp
  11. 7 8
      TestFramework/Input/MacOS/KeyboardMacOS.mm
  12. 7 0
      TestFramework/Renderer/DX12/RendererDX12.cpp
  13. 11 0
      TestFramework/Renderer/MTL/FatalErrorIfFailedMTL.h
  14. 14 0
      TestFramework/Renderer/MTL/FatalErrorIfFailedMTL.mm
  15. 32 0
      TestFramework/Renderer/MTL/PipelineStateMTL.h
  16. 163 0
      TestFramework/Renderer/MTL/PipelineStateMTL.mm
  17. 24 0
      TestFramework/Renderer/MTL/PixelShaderMTL.h
  18. 36 0
      TestFramework/Renderer/MTL/RenderInstancesMTL.h
  19. 52 0
      TestFramework/Renderer/MTL/RenderInstancesMTL.mm
  20. 40 0
      TestFramework/Renderer/MTL/RenderPrimitiveMTL.h
  21. 74 0
      TestFramework/Renderer/MTL/RenderPrimitiveMTL.mm
  22. 46 0
      TestFramework/Renderer/MTL/RendererMTL.h
  23. 181 0
      TestFramework/Renderer/MTL/RendererMTL.mm
  24. 32 0
      TestFramework/Renderer/MTL/TextureMTL.h
  25. 98 0
      TestFramework/Renderer/MTL/TextureMTL.mm
  26. 24 0
      TestFramework/Renderer/MTL/VertexShaderMTL.h
  27. 3 0
      TestFramework/Renderer/Renderer.h
  28. 1 15
      TestFramework/Renderer/VK/PipelineStateVK.cpp
  29. 7 0
      TestFramework/Renderer/VK/RendererVK.cpp
  30. 51 10
      TestFramework/TestFramework.cmake
  31. 9 3
      TestFramework/Window/ApplicationWindowMacOS.h
  32. 32 12
      TestFramework/Window/ApplicationWindowMacOS.mm

+ 1 - 0
.gitignore

@@ -7,3 +7,4 @@
 /*.jor
 /detlog.txt
 /Assets/Shaders/VK/*.spv
+/Assets/Shaders/MTL/*.metallib

+ 39 - 0
Assets/Shaders/MTL/FontShader.metal

@@ -0,0 +1,39 @@
+#include <metal_stdlib>
+
+using namespace metal;
+
+#include "VertexConstants.h"
+
+constexpr sampler alphaTextureSampler(mag_filter::linear, min_filter::linear);
+
+struct FontVertex
+{
+	float3		vPos [[attribute(0)]];
+	float2		vTex [[attribute(1)]];
+	uchar4		vCol [[attribute(2)]];
+};
+
+struct FontOut
+{
+    float4 		oPosition [[position]];
+    float2 		oTex;
+    float4 		oColor;
+};
+
+vertex FontOut FontVertexShader(FontVertex vert [[stage_in]], constant VertexShaderConstantBuffer *constants [[buffer(2)]])
+{
+    FontOut out;
+    out.oPosition = constants->Projection * constants->View * float4(vert.vPos, 1.0);
+    out.oTex = vert.vTex;
+    out.oColor = float4(vert.vCol) / 255.0;
+    return out;
+}
+
+fragment float4 FontPixelShader(FontOut in [[stage_in]], texture2d<float> alphaTexture [[texture(0)]])
+{
+	const float4 sample = alphaTexture.sample(alphaTextureSampler, in.oTex);
+	if (sample.x < 0.5)
+		discard_fragment();
+
+    return float4(in.oColor.xyz, sample.x);
+}

+ 30 - 0
Assets/Shaders/MTL/LineShader.metal

@@ -0,0 +1,30 @@
+#include <metal_stdlib>
+
+using namespace metal;
+
+#include "VertexConstants.h"
+
+struct LineVertex
+{
+	float3	iPosition [[attribute(0)]];
+	uchar4	iColor [[attribute(1)]];
+};
+
+struct LineOut
+{
+    float4 	oPosition [[position]];
+    float4 	oColor;
+};
+
+vertex LineOut LineVertexShader(LineVertex vert [[stage_in]], constant VertexShaderConstantBuffer *constants [[buffer(2)]])
+{
+    LineOut out;
+	out.oPosition = constants->Projection * constants->View * float4(vert.iPosition, 1.0);
+    out.oColor = float4(vert.iColor) / 255.0;
+    return out;
+}
+
+fragment float4 LinePixelShader(LineOut in [[stage_in]])
+{
+    return in.oColor;
+}

+ 199 - 0
Assets/Shaders/MTL/TriangleShader.metal

@@ -0,0 +1,199 @@
+#include <metal_stdlib>
+
+using namespace metal;
+
+#include "VertexConstants.h"
+
+constexpr sampler depthSampler(mag_filter::nearest, min_filter::nearest);
+
+struct Vertex
+{
+	float3		vPos [[attribute(0)]];
+	float3		vNorm [[attribute(1)]];
+	float2		vTex [[attribute(2)]];
+	uchar4		vCol [[attribute(3)]];
+	float4		iModel0 [[attribute(4)]];
+	float4		iModel1 [[attribute(5)]];
+	float4		iModel2 [[attribute(6)]];
+	float4		iModel3 [[attribute(7)]];
+	float4		iModelInvTrans0 [[attribute(8)]];
+	float4		iModelInvTrans1 [[attribute(9)]];
+	float4		iModelInvTrans2 [[attribute(10)]];
+	float4		iModelInvTrans3 [[attribute(11)]];
+	uchar4		iCol [[attribute(12)]];
+};
+
+struct TriangleOut
+{
+	float4		oPosition [[position]];
+	float3		oNormal;
+	float3		oWorldPos;
+	float2		oTex;
+	float4		oPositionL;
+	float4		oColor;
+};
+
+vertex TriangleOut TriangleVertexShader(Vertex vert [[stage_in]], constant VertexShaderConstantBuffer *constants [[buffer(2)]])
+{
+	TriangleOut out;
+
+	// Convert input matrices
+	float4x4 iModel(vert.iModel0, vert.iModel1, vert.iModel2, vert.iModel3);
+	float4x4 iModelInvTrans(vert.iModelInvTrans0, vert.iModelInvTrans1, vert.iModelInvTrans2, vert.iModelInvTrans3);
+
+	// Get world position
+	float4 pos = float4(vert.vPos, 1.0f);
+	float4 world_pos = iModel * pos;
+
+	// Transform the position from world space to homogeneous projection space
+	float4 proj_pos = constants->View * world_pos;
+	proj_pos = constants->Projection * proj_pos;
+	out.oPosition = proj_pos;
+
+	// Transform the position from world space to projection space of the light
+	float4 proj_lpos = constants->LightView * world_pos;
+	proj_lpos = constants->LightProjection * proj_lpos;
+	out.oPositionL = proj_lpos;
+
+	// output normal
+	float4 norm = float4(vert.vNorm, 0.0f);
+	out.oNormal = normalize(iModelInvTrans * norm).xyz;
+
+	// output world position of the vertex
+	out.oWorldPos = world_pos.xyz;
+
+	// output texture coordinates
+	out.oTex = vert.vTex;
+
+	// output color
+	out.oColor = float4(vert.vCol) * float4(vert.iCol) / (255.0 * 255.0);
+
+	return out;
+}
+
+fragment float4 TrianglePixelShader(TriangleOut vert [[stage_in]], constant PixelShaderConstantBuffer *constants, texture2d<float> depthTexture [[texture(0)]])
+{
+	// Constants
+	float AmbientFactor = 0.3;
+	float3 DiffuseColor = float3(vert.oColor.r, vert.oColor.g, vert.oColor.b);
+	float3 SpecularColor = float3(1, 1, 1);
+	float SpecularPower = 100.0;
+	float bias = 1.0e-7;
+
+	// Homogenize position in light space
+	float3 position_l = vert.oPositionL.xyz / vert.oPositionL.w;
+
+	// Calculate dot product between direction to light and surface normal and clamp between [0, 1]
+	float3 view_dir = normalize(constants->CameraPos - vert.oWorldPos);
+	float3 world_to_light = constants->LightPos - vert.oWorldPos;
+	float3 light_dir = normalize(world_to_light);
+	float3 normal = normalize(vert.oNormal);
+	if (dot(view_dir, normal) < 0) // If we're viewing the triangle from the back side, flip the normal to get the correct lighting
+		normal = -normal;
+	float normal_dot_light_dir = clamp(dot(normal, light_dir), 0.0, 1.0);
+
+	// Calculate texture coordinates in light depth texture
+	float2 tex_coord;
+	tex_coord.x = position_l.x / 2.0 + 0.5;
+	tex_coord.y = -position_l.y / 2.0 + 0.5;
+
+	// Check that the texture coordinate is inside the depth texture, if not we don't know if it is lit or not so we assume lit
+	float shadow_factor = 1.0;
+	if (vert.oColor.a > 0 // Alpha = 0 means don't receive shadows
+		&& tex_coord.x == clamp(tex_coord.x, 0.0, 1.0) && tex_coord.y == clamp(tex_coord.y, 0.0, 1.0))
+	{
+		// Modify shadow bias according to the angle between the normal and the light dir
+		float modified_bias = bias * tan(acos(normal_dot_light_dir));
+		modified_bias = min(modified_bias, 10.0 * bias);
+		
+		// Get texture size
+		float width = 1.0 / 4096;
+		float height = 1.0 / 4096;
+
+		// Samples to take
+		uint num_samples = 16;
+		float2 offsets[] = { 
+			float2(-1.5 * width, -1.5 * height),
+			float2(-0.5 * width, -1.5 * height),
+			float2(0.5 * width, -1.5 * height),
+			float2(1.5 * width, -1.5 * height),
+
+			float2(-1.5 * width, -0.5 * height),
+			float2(-0.5 * width, -0.5 * height),
+			float2(0.5 * width, -0.5 * height),
+			float2(1.5 * width, -0.5 * height),
+
+			float2(-1.5 * width, 0.5 * height),
+			float2(-0.5 * width, 0.5 * height),
+			float2(0.5 * width, 0.5 * height),
+			float2(1.5 * width, 0.5 * height),
+
+			float2(-1.5 * width, 1.5 * height),
+			float2(-0.5 * width, 1.5 * height),
+			float2(0.5 * width, 1.5 * height),
+			float2(1.5 * width, 1.5 * height),
+		};
+
+		// Calculate depth of this pixel relative to the light
+		float light_depth = position_l.z + modified_bias;
+
+		// Sample shadow factor
+		shadow_factor = 0.0;
+		for (uint i = 0; i < num_samples; ++i)
+			shadow_factor += depthTexture.sample(depthSampler, tex_coord + offsets[i]).x <= light_depth? 1.0 : 0.0;
+		shadow_factor /= num_samples;
+	}
+
+	// Calculate diffuse and specular
+	float diffuse = normal_dot_light_dir;
+	float specular = diffuse > 0.0? pow(clamp(-dot(reflect(light_dir, normal), view_dir), 0.0, 1.0), SpecularPower) : 0.0;
+
+	// Apply procedural pattern based on the uv coordinates
+	bool2 less_half = (vert.oTex - floor(vert.oTex)) < float2(0.5, 0.5);
+	float darken_factor = less_half.r ^ less_half.g? 0.5 : 1.0;
+
+	// Fade out checkerboard pattern when it tiles too often
+	float2 dx = dfdx(vert.oTex), dy = dfdy(vert.oTex);
+	float texel_distance = sqrt(dot(dx, dx) + dot(dy, dy));
+	darken_factor = mix(darken_factor, 0.75, clamp(5.0 * texel_distance - 1.5, 0.0, 1.0));
+
+	// Calculate color
+	return float4(clamp((AmbientFactor + diffuse * shadow_factor) * darken_factor * DiffuseColor + SpecularColor * specular * shadow_factor, 0, 1), 1);
+}
+
+struct DepthOut
+{
+	float4		oPosition [[position]];
+};
+
+vertex DepthOut TriangleDepthVertexShader(Vertex vert [[stage_in]], constant VertexShaderConstantBuffer *constants [[buffer(2)]])
+{
+	DepthOut out;
+
+	// Check if the alpha = 0
+	if (vert.vCol.a * vert.iCol.a == 0.0)
+	{
+		// Don't draw the triangle by moving it to an invalid location
+		out.oPosition = float4(0, 0, 0, 0);
+	}
+	else
+	{
+		// Convert input matrix
+		float4x4 iModel(vert.iModel0, vert.iModel1, vert.iModel2, vert.iModel3);
+
+		// Transform the position from world space to homogeneous projection space for the light
+		float4 pos = float4(vert.vPos, 1.0f);
+		pos = iModel * pos;
+		pos = constants->LightView * pos;
+		pos = constants->LightProjection * pos;
+		out.oPosition = pos;
+	}
+
+	return out;
+}
+
+fragment float4 TriangleDepthPixelShader(DepthOut in [[stage_in]])
+{
+	// We only write depth, so this shader does nothing
+	return float4(0.0, 0.0, 0.0, 1.0);
+}

+ 41 - 0
Assets/Shaders/MTL/UIShader.metal

@@ -0,0 +1,41 @@
+#include <metal_stdlib>
+
+using namespace metal;
+
+#include "VertexConstants.h"
+
+constexpr sampler uiTextureSampler(mag_filter::linear, min_filter::linear);
+
+struct UIVertex
+{
+	float3		vPos [[attribute(0)]];
+	float2		vTex [[attribute(1)]];
+	uchar4		vCol [[attribute(2)]];
+};
+
+struct UIOut
+{
+    float4 		oPosition [[position]];
+    float2 		oTex;
+    float4 		oColor;
+};
+
+vertex UIOut UIVertexShader(UIVertex vert [[stage_in]], constant VertexShaderConstantBuffer *constants [[buffer(2)]])
+{
+    UIOut out;
+    out.oPosition = constants->Projection * constants->View * float4(vert.vPos, 1.0);
+    out.oTex = vert.vTex;
+    out.oColor = float4(vert.vCol) / 255.0;
+    return out;
+}
+
+fragment float4 UIPixelShader(UIOut in [[stage_in]], texture2d<float> uiTexture [[texture(0)]])
+{
+	const float4 sample = uiTexture.sample(uiTextureSampler, in.oTex);
+    return sample * in.oColor;
+}
+
+fragment float4 UIPixelShaderUntextured(UIOut in [[stage_in]])
+{
+    return in.oColor;
+}

+ 13 - 0
Assets/Shaders/MTL/VertexConstants.h

@@ -0,0 +1,13 @@
+struct VertexShaderConstantBuffer
+{
+	float4x4 	View;				// view matrix
+	float4x4 	Projection;		// projection matrix
+	float4x4 	LightView;			// view matrix of the light
+	float4x4 	LightProjection;	// projection matrix of the light
+};
+
+struct PixelShaderConstantBuffer
+{
+	float3		CameraPos;
+	float3		LightPos;
+};

+ 1 - 1
Build/CMakeLists.txt

@@ -107,7 +107,7 @@ include(CMakeDependentOption)
 cmake_dependent_option(USE_STATIC_MSVC_RUNTIME_LIBRARY "Use the static MSVC runtime library" ON "MSVC;NOT WINDOWS_STORE" OFF)
 
 # Enable Vulkan instead of DirectX
-cmake_dependent_option(JPH_ENABLE_VULKAN "Enable Vulkan" OFF "WIN32" ON)
+cmake_dependent_option(JPH_ENABLE_VULKAN "Enable Vulkan" ON "LINUX" OFF)
 
 # Determine which configurations exist
 if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) # Only do this when we're at the top level, see: https://gitlab.kitware.com/cmake/cmake/-/issues/24181

+ 1 - 2
Build/README.md

@@ -125,7 +125,7 @@ To implement your custom memory allocator override Allocate, Free, Reallocate, A
 </details>
 
 <details>
-	<summary>Linux</summary>
+	<summary>Linux (Ubuntu)</summary>
 	<ul>
 		<li>Install clang (apt-get install clang)</li>
 		<li>Install cmake (apt-get install cmake)</li>
@@ -152,7 +152,6 @@ To implement your custom memory allocator override Allocate, Free, Reallocate, A
 	<ul>
 		<li>Install XCode</li>
 		<li>Download CMake 3.23+ (https://cmake.org/download/)</li>
-		<li>If you want to build the Samples or JoltViewer, install the <a href="https://vulkan.lunarg.com/sdk/home#mac">Vulkan SDK</a></li>
 		<li>Run: ./cmake_xcode_macos.sh</li>
 		<li>This will open XCode with a newly generated project</li>
 		<li>Build and run the project</li>

+ 2 - 1
Docs/ReleaseNotes.md

@@ -16,7 +16,8 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * Added binary serialization to `SkeletalAnimation`.
 * Added support for RISC-V, LoongArch and PowerPC (Little Endian) CPUs.
 * Added the ability to add a sub shape at a specified index in a MutableCompoundShape rather than at the end.
-* The Samples and JoltViewer can run on Linux/macOS using Vulkan now. Make sure to install the Vulkan SDK before compiling (see: Build/ubuntu24_install_vulkan_sdk.sh or [download](https://vulkan.lunarg.com/sdk/home) the SDK).
+* The Samples and JoltViewer can run on Linux using Vulkan. Make sure to install the Vulkan SDK before compiling (see: Build/ubuntu24_install_vulkan_sdk.sh).
+* The Samples and JoltViewer can run on macOS using Metal.
 * Added STLLocalAllocator which is an allocator that can be used in e.g. the Array class. It keeps a fixed size buffer for N elements and only when it runs out of space falls back to the heap.
 * Added support for CharacterVirtual to override the inner rigid body ID. This can be used to make the simulation deterministic in e.g. client/server setups.
 * Added OnContactPersisted, OnContactRemoved, OnCharacterContactPersisted and OnCharacterContactRemoved functions on CharacterContactListener to better match the interface of ContactListener.

+ 1 - 12
TestFramework/Application/Application.cpp

@@ -13,11 +13,6 @@
 #include <Jolt/Core/Factory.h>
 #include <Jolt/RegisterTypes.h>
 #include <Renderer/DebugRendererImp.h>
-#ifdef JPH_ENABLE_VULKAN
-	#include <Renderer/VK/RendererVK.h>
-#elif defined(JPH_ENABLE_DIRECTX)
-	#include <Renderer/DX12/RendererDX12.h>
-#endif
 #ifdef JPH_PLATFORM_WINDOWS
 	#include <crtdbg.h>
 	#include <Input/Win/KeyboardWin.h>
@@ -84,13 +79,7 @@ Application::Application([[maybe_unused]] const String &inCommandLine) :
 		mWindow->Initialize();
 
 		// Create renderer
-	#ifdef JPH_ENABLE_VULKAN
-		mRenderer = new RendererVK;
-	#elif defined(JPH_ENABLE_DIRECTX)
-		mRenderer = new RendererDX12;
-	#else
-		#error No renderer defined
-	#endif
+		mRenderer = Renderer::sCreate();
 		mRenderer->Initialize(mWindow);
 
 		// Create font

+ 7 - 8
TestFramework/Input/MacOS/KeyboardMacOS.mm

@@ -76,14 +76,13 @@ static EKey sToKey(GCKeyCode inValue)
 {
 	mKeyboard = Keyboard;
 
-    [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown handler:^NSEvent *(NSEvent *event) {
-        // Ignore all keystrokes except Command-Q (Quit).
-        if ((event.modifierFlags & NSEventModifierFlagCommand) && [event.charactersIgnoringModifiers isEqual:@"q"]) {
-            return event;
-        } else {
-            return nil;
-        }
-    }];
+	[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown handler:^NSEvent *(NSEvent *event) {
+		// Ignore all keystrokes except Command-Q (Quit).
+		if ((event.modifierFlags & NSEventModifierFlagCommand) && [event.charactersIgnoringModifiers isEqual:@"q"])
+			return event;
+		else
+			return nil;
+	}];
 
 	return self;
 }

+ 7 - 0
TestFramework/Renderer/DX12/RendererDX12.cpp

@@ -707,3 +707,10 @@ void RendererDX12::RecycleD3DObject(ID3D12Object *inResource)
 	if (!mIsExiting)
 		mDelayReleased[mFrameIndex].push_back(inResource);
 }
+
+#ifndef JPH_ENABLE_VULKAN
+Renderer *Renderer::sCreate()
+{
+	return new RendererDX12;
+}
+#endif

+ 11 - 0
TestFramework/Renderer/MTL/FatalErrorIfFailedMTL.h

@@ -0,0 +1,11 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#import <MetalKit/MetalKit.h>
+
+/// Convert Metal error to readable text and alert
+void FatalErrorIfFailed(NSError *inResult);
+

+ 14 - 0
TestFramework/Renderer/MTL/FatalErrorIfFailedMTL.mm

@@ -0,0 +1,14 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/MTL/FatalErrorIfFailedMTL.h>
+#include <Utils/Log.h>
+
+void FatalErrorIfFailed(NSError *inResult)
+{
+	if (inResult != nullptr)
+		FatalError("Metal error returned: %s", [[inResult localizedDescription] cStringUsingEncoding: NSUTF8StringEncoding]);
+}

+ 32 - 0
TestFramework/Renderer/MTL/PipelineStateMTL.h

@@ -0,0 +1,32 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/PipelineState.h>
+#include <Renderer/MTL/VertexShaderMTL.h>
+#include <Renderer/MTL/PixelShaderMTL.h>
+
+class RendererMTL;
+
+/// Metal pipeline state object
+class PipelineStateMTL : public PipelineState
+{
+public:
+	/// Constructor
+										PipelineStateMTL(RendererMTL *inRenderer, const VertexShaderMTL *inVertexShader, const EInputDescription *inInputDescription, uint inInputDescriptionCount, const PixelShaderMTL *inPixelShader, EDrawPass inDrawPass, EFillMode inFillMode, ETopology inTopology, EDepthTest inDepthTest, EBlendMode inBlendMode, ECullMode inCullMode);
+	virtual								~PipelineStateMTL() override;
+
+	/// Make this pipeline state active (any primitives rendered after this will use this state)
+	virtual void						Activate() override;
+
+private:
+	RendererMTL *						mRenderer;
+	RefConst<VertexShaderMTL>			mVertexShader;
+	RefConst<PixelShaderMTL>			mPixelShader;
+	id<MTLRenderPipelineState> 			mPipelineState;
+	id<MTLDepthStencilState>			mDepthState;
+	MTLCullMode							mCullMode;
+	MTLTriangleFillMode					mFillMode;
+};

+ 163 - 0
TestFramework/Renderer/MTL/PipelineStateMTL.mm

@@ -0,0 +1,163 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/MTL/PipelineStateMTL.h>
+#include <Renderer/MTL/RendererMTL.h>
+#include <Renderer/MTL/FatalErrorIfFailedMTL.h>
+
+PipelineStateMTL::PipelineStateMTL(RendererMTL *inRenderer, const VertexShaderMTL *inVertexShader, const EInputDescription *inInputDescription, uint inInputDescriptionCount, const PixelShaderMTL *inPixelShader, EDrawPass inDrawPass, EFillMode inFillMode, ETopology inTopology, EDepthTest inDepthTest, EBlendMode inBlendMode, ECullMode inCullMode) :
+	mRenderer(inRenderer),
+	mVertexShader(inVertexShader),
+	mPixelShader(inPixelShader)
+{
+	// Create a vertex descriptor
+	MTLVertexDescriptor *vertex_descriptor = [[MTLVertexDescriptor alloc] init];
+	uint vertex_offset = 0;
+	uint instance_offset = 0, instance_alignment = 4;
+	uint index = 0;
+	for (uint i = 0; i < inInputDescriptionCount; ++i)
+		switch (inInputDescription[i])
+		{
+		case EInputDescription::Position:
+		case EInputDescription::Normal:
+			vertex_descriptor.attributes[index].format = MTLVertexFormatFloat3;
+			vertex_descriptor.attributes[index].offset = vertex_offset;
+			vertex_descriptor.attributes[index].bufferIndex = 0;
+			vertex_offset += 3 * sizeof(float);
+			++index;
+			break;
+
+		case EInputDescription::Color:
+			vertex_descriptor.attributes[index].format = MTLVertexFormatUChar4;
+			vertex_descriptor.attributes[index].offset = vertex_offset;
+			vertex_descriptor.attributes[index].bufferIndex = 0;
+			vertex_offset += 4 * sizeof(uint8);
+			++index;
+			break;
+
+		case EInputDescription::TexCoord:
+			vertex_descriptor.attributes[index].format = MTLVertexFormatFloat2;
+			vertex_descriptor.attributes[index].offset = vertex_offset;
+			vertex_descriptor.attributes[index].bufferIndex = 0;
+			vertex_offset += 2 * sizeof(float);
+			++index;
+			break;
+
+		case EInputDescription::InstanceColor:
+			vertex_descriptor.attributes[index].format = MTLVertexFormatUChar4;
+			vertex_descriptor.attributes[index].offset = instance_offset;
+			vertex_descriptor.attributes[index].bufferIndex = 1;
+			instance_offset += 4 * sizeof(uint8);
+			++index;
+			break;
+
+		case EInputDescription::InstanceTransform:
+		case EInputDescription::InstanceInvTransform:
+			instance_alignment = max(instance_alignment, 16u);
+			instance_offset = AlignUp(instance_offset, 16u);
+			for (int j = 0; j < 4; ++j)
+			{
+				vertex_descriptor.attributes[index].format = MTLVertexFormatFloat4;
+				vertex_descriptor.attributes[index].offset = instance_offset;
+				vertex_descriptor.attributes[index].bufferIndex = 1;
+				instance_offset += 4 * sizeof(float);
+				++index;
+			}
+			break;
+		}
+
+	// Configure layouts
+	vertex_descriptor.layouts[0].stride = vertex_offset;
+	vertex_descriptor.layouts[0].stepRate = 1;
+	vertex_descriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
+
+	if (instance_offset > 0)
+	{
+		vertex_descriptor.layouts[1].stride = AlignUp(instance_offset, instance_alignment);
+		vertex_descriptor.layouts[1].stepRate = 1;
+		vertex_descriptor.layouts[1].stepFunction = MTLVertexStepFunctionPerInstance;
+	}
+
+	// Create the pipeline descriptor
+	MTLRenderPipelineDescriptor *descriptor = [[MTLRenderPipelineDescriptor alloc] init];
+	descriptor.vertexFunction = inVertexShader->GetFunction();
+	descriptor.fragmentFunction = inPixelShader->GetFunction();
+	descriptor.vertexDescriptor = vertex_descriptor;
+	switch (inDrawPass)
+	{
+	case EDrawPass::Shadow:
+		descriptor.depthAttachmentPixelFormat = static_cast<TextureMTL *>(mRenderer->GetShadowMap())->GetTexture().pixelFormat;
+		break;
+
+	case EDrawPass::Normal:
+		descriptor.colorAttachments[0].pixelFormat = mRenderer->GetView().colorPixelFormat;
+		switch (inBlendMode)
+		{
+		case EBlendMode::Write:
+			descriptor.colorAttachments[0].blendingEnabled = NO;
+			break;
+
+		case EBlendMode::AlphaBlend:
+			descriptor.colorAttachments[0].blendingEnabled = YES;
+			descriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
+			descriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
+			descriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
+			descriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorZero;
+			descriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorZero;
+			descriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
+			break;
+		}
+		descriptor.depthAttachmentPixelFormat = mRenderer->GetView().depthStencilPixelFormat;
+	}
+
+	NSError *error = nullptr;
+	mPipelineState = [mRenderer->GetDevice() newRenderPipelineStateWithDescriptor: descriptor error: &error];
+	FatalErrorIfFailed(error);
+	[descriptor release];
+	[vertex_descriptor release];
+
+	// Create depth descriptor
+	MTLDepthStencilDescriptor *depth_descriptor = [[MTLDepthStencilDescriptor new] init];
+	if (inDepthTest == EDepthTest::On)
+	{
+		depth_descriptor.depthCompareFunction = MTLCompareFunctionGreater;
+		depth_descriptor.depthWriteEnabled = YES;
+	}
+	else
+	{
+		depth_descriptor.depthCompareFunction = MTLCompareFunctionAlways;
+		depth_descriptor.depthWriteEnabled = NO;
+	}
+	mDepthState = [mRenderer->GetDevice() newDepthStencilStateWithDescriptor: depth_descriptor];
+	[depth_descriptor release];
+
+	// Determine cull mode
+	if (inCullMode == ECullMode::FrontFace)
+		mCullMode = MTLCullModeFront;
+	else
+		mCullMode = MTLCullModeBack;
+
+	// Determine fill mode
+	if (inFillMode == EFillMode::Solid)
+		mFillMode = MTLTriangleFillModeFill;
+	else
+		mFillMode = MTLTriangleFillModeLines;
+}
+
+PipelineStateMTL::~PipelineStateMTL()
+{
+	[mPipelineState release];
+	[mDepthState release];
+}
+
+void PipelineStateMTL::Activate()
+{
+	id<MTLRenderCommandEncoder> encoder = mRenderer->GetRenderEncoder();
+	[encoder setRenderPipelineState: mPipelineState];
+	[encoder setDepthStencilState: mDepthState];
+	[encoder setCullMode: mCullMode];
+	[encoder setTriangleFillMode: mFillMode];
+}

+ 24 - 0
TestFramework/Renderer/MTL/PixelShaderMTL.h

@@ -0,0 +1,24 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/PixelShader.h>
+
+#include <MetalKit/MetalKit.h>
+
+/// Pixel shader handle for Metal
+class PixelShaderMTL : public PixelShader
+{
+public:
+	/// Constructor
+							PixelShaderMTL(id<MTLFunction> inFunction) : mFunction(inFunction) { }
+	virtual					~PixelShaderMTL() override { [mFunction release]; }
+
+	/// Access to the function
+	id<MTLFunction>			GetFunction() const				{ return mFunction; }
+
+private:
+	id<MTLFunction>			mFunction;
+};

+ 36 - 0
TestFramework/Renderer/MTL/RenderInstancesMTL.h

@@ -0,0 +1,36 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/MTL/RendererMTL.h>
+#include <Renderer/RenderInstances.h>
+
+class RenderPrimitive;
+
+/// Metal implementation of a render instances object
+class RenderInstancesMTL : public RenderInstances
+{
+public:
+	/// Constructor
+							RenderInstancesMTL(RendererMTL *inRenderer)											: mRenderer(inRenderer) { }
+	virtual					~RenderInstancesMTL() override														{ Clear(); }
+
+	/// Erase all instance data
+	virtual void			Clear() override;
+
+	/// Instance buffer management functions
+	virtual void			CreateBuffer(int inNumInstances, int inInstanceSize) override;
+	virtual void *			Lock() override;
+	virtual void			Unlock() override;
+
+	/// Draw the instances when context has been set by Renderer::BindShader
+	virtual void			Draw(RenderPrimitive *inPrimitive, int inStartInstance, int inNumInstances) const override;
+
+private:
+	RendererMTL *			mRenderer;
+	id<MTLBuffer>			mBuffer;
+	NSUInteger				mBufferSize;
+	NSUInteger				mInstanceSize;
+};

+ 52 - 0
TestFramework/Renderer/MTL/RenderInstancesMTL.mm

@@ -0,0 +1,52 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/MTL/RenderInstancesMTL.h>
+#include <Renderer/MTL/RenderPrimitiveMTL.h>
+
+void RenderInstancesMTL::Clear()
+{
+	[mBuffer release];
+	mBuffer = nil;
+}
+
+void RenderInstancesMTL::CreateBuffer(int inNumInstances, int inInstanceSize)
+{
+	mInstanceSize = NSUInteger(inInstanceSize);
+	NSUInteger size = mInstanceSize * inNumInstances;
+	if (mBuffer == nullptr || mBufferSize < size)
+	{
+		Clear();
+
+		mBuffer = [mRenderer->GetView().device newBufferWithLength: size options: MTLResourceCPUCacheModeDefaultCache | MTLResourceStorageModeShared | MTLResourceHazardTrackingModeTracked];
+		mBufferSize = size;
+	}
+}
+
+void *RenderInstancesMTL::Lock()
+{
+	return mBuffer.contents;
+}
+
+void RenderInstancesMTL::Unlock()
+{
+}
+
+void RenderInstancesMTL::Draw(RenderPrimitive *inPrimitive, int inStartInstance, int inNumInstances) const
+{
+	if (inNumInstances <= 0)
+		return;
+
+	id<MTLRenderCommandEncoder> encoder = mRenderer->GetRenderEncoder();
+	RenderPrimitiveMTL *prim = static_cast<RenderPrimitiveMTL *>(inPrimitive);
+
+	[encoder setVertexBuffer: prim->mVertexBuffer offset: 0 atIndex: 0];
+	[encoder setVertexBuffer: mBuffer offset: mInstanceSize * inStartInstance atIndex: 1];
+	if (prim->mIndexBuffer == nil)
+		[encoder drawPrimitives: prim->mPrimitiveType vertexStart: 0 vertexCount: prim->mNumVtxToDraw instanceCount: inNumInstances];
+	else
+		[encoder drawIndexedPrimitives: prim->mPrimitiveType indexCount: prim->mNumIdxToDraw indexType: MTLIndexTypeUInt32 indexBuffer: prim->mIndexBuffer indexBufferOffset: 0 instanceCount: inNumInstances];
+}

+ 40 - 0
TestFramework/Renderer/MTL/RenderPrimitiveMTL.h

@@ -0,0 +1,40 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/RenderPrimitive.h>
+#include <Renderer/MTL/RendererMTL.h>
+
+/// Metal implementation of a render primitive
+class RenderPrimitiveMTL : public RenderPrimitive
+{
+public:
+	/// Constructor
+							RenderPrimitiveMTL(RendererMTL *inRenderer, MTLPrimitiveType inType)			: mRenderer(inRenderer), mPrimitiveType(inType) { }
+	virtual					~RenderPrimitiveMTL() override													{ Clear(); }
+
+	/// Vertex buffer management functions
+	virtual void			CreateVertexBuffer(int inNumVtx, int inVtxSize, const void *inData = nullptr) override;
+	virtual void			ReleaseVertexBuffer() override;
+	virtual void *			LockVertexBuffer() override;
+	virtual void			UnlockVertexBuffer() override;
+
+	/// Index buffer management functions
+	virtual void			CreateIndexBuffer(int inNumIdx, const uint32 *inData = nullptr) override;
+	virtual void			ReleaseIndexBuffer() override;
+	virtual uint32 *		LockIndexBuffer() override;
+	virtual void			UnlockIndexBuffer() override;
+
+	/// Draw the primitive
+	virtual void			Draw() const override;
+
+private:
+	friend class 			RenderInstancesMTL;
+	
+	RendererMTL *			mRenderer;
+	MTLPrimitiveType		mPrimitiveType;
+	id<MTLBuffer>			mVertexBuffer;
+	id<MTLBuffer>			mIndexBuffer;
+};

+ 74 - 0
TestFramework/Renderer/MTL/RenderPrimitiveMTL.mm

@@ -0,0 +1,74 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/MTL/RenderPrimitiveMTL.h>
+
+void RenderPrimitiveMTL::ReleaseVertexBuffer()
+{
+	[mVertexBuffer release];
+	mVertexBuffer = nil;
+
+	RenderPrimitive::ReleaseVertexBuffer();
+}
+
+void RenderPrimitiveMTL::ReleaseIndexBuffer()
+{
+	[mIndexBuffer release];
+	mIndexBuffer = nil;
+
+	RenderPrimitive::ReleaseIndexBuffer();
+}
+
+void RenderPrimitiveMTL::CreateVertexBuffer(int inNumVtx, int inVtxSize, const void *inData)
+{
+	RenderPrimitive::CreateVertexBuffer(inNumVtx, inVtxSize, inData);
+
+	NSUInteger size = NSUInteger(inNumVtx) * inVtxSize;
+	if (inData != nullptr)
+		mVertexBuffer = [mRenderer->GetDevice() newBufferWithBytes: inData length: size options: MTLResourceCPUCacheModeDefaultCache | MTLResourceStorageModeShared | MTLResourceHazardTrackingModeTracked];
+	else
+		mVertexBuffer = [mRenderer->GetDevice() newBufferWithLength: size options: MTLResourceCPUCacheModeDefaultCache | MTLResourceStorageModeShared | MTLResourceHazardTrackingModeTracked];
+}
+
+void *RenderPrimitiveMTL::LockVertexBuffer()
+{
+	return mVertexBuffer.contents;
+}
+
+void RenderPrimitiveMTL::UnlockVertexBuffer()
+{
+}
+
+void RenderPrimitiveMTL::CreateIndexBuffer(int inNumIdx, const uint32 *inData)
+{
+	RenderPrimitive::CreateIndexBuffer(inNumIdx, inData);
+
+	NSUInteger size = NSUInteger(inNumIdx) * sizeof(uint32);
+	if (inData != nullptr)
+		mIndexBuffer = [mRenderer->GetDevice() newBufferWithBytes: inData length: size options: MTLResourceCPUCacheModeDefaultCache | MTLResourceStorageModeManaged | MTLResourceHazardTrackingModeTracked];
+	else
+		mIndexBuffer = [mRenderer->GetDevice() newBufferWithLength: size options: MTLResourceCPUCacheModeDefaultCache | MTLResourceStorageModeShared | MTLResourceHazardTrackingModeTracked];
+}
+
+uint32 *RenderPrimitiveMTL::LockIndexBuffer()
+{
+	return (uint32 *)mIndexBuffer.contents;
+}
+
+void RenderPrimitiveMTL::UnlockIndexBuffer()
+{
+}
+
+void RenderPrimitiveMTL::Draw() const
+{
+	id<MTLRenderCommandEncoder> encoder = mRenderer->GetRenderEncoder();
+
+	[encoder setVertexBuffer: mVertexBuffer offset: 0 atIndex: 0];
+	if (mIndexBuffer == nil)
+		[encoder drawPrimitives: mPrimitiveType vertexStart: 0 vertexCount: mNumVtxToDraw];
+	else
+		[encoder drawIndexedPrimitives: mPrimitiveType indexCount: mNumIdxToDraw indexType: MTLIndexTypeUInt32 indexBuffer: mIndexBuffer indexBufferOffset: 0];
+}

+ 46 - 0
TestFramework/Renderer/MTL/RendererMTL.h

@@ -0,0 +1,46 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/Renderer.h>
+#include <Renderer/MTL/TextureMTL.h>
+
+#include <MetalKit/MetalKit.h>
+
+/// Metal renderer
+class RendererMTL : public Renderer
+{
+public:
+	virtual 						~RendererMTL() override;
+	
+	// See: Renderer
+	virtual void					Initialize(ApplicationWindow *inWindow) override;
+	virtual void					BeginFrame(const CameraState &inCamera, float inWorldScale) override;
+	virtual void					EndShadowPass() override;
+	virtual void					EndFrame() override;
+	virtual void					SetProjectionMode() override;
+	virtual void					SetOrthoMode() override;
+	virtual Ref<Texture>			CreateTexture(const Surface *inSurface) override;
+	virtual Ref<VertexShader>		CreateVertexShader(const char *inName) override;
+	virtual Ref<PixelShader>		CreatePixelShader(const char *inName) override;
+	virtual unique_ptr<PipelineState> CreatePipelineState(const VertexShader *inVertexShader, const PipelineState::EInputDescription *inInputDescription, uint inInputDescriptionCount, const PixelShader *inPixelShader, PipelineState::EDrawPass inDrawPass, PipelineState::EFillMode inFillMode, PipelineState::ETopology inTopology, PipelineState::EDepthTest inDepthTest, PipelineState::EBlendMode inBlendMode, PipelineState::ECullMode inCullMode) override;
+	virtual RenderPrimitive *		CreateRenderPrimitive(PipelineState::ETopology inType) override;
+	virtual RenderInstances *		CreateRenderInstances() override;
+	virtual Texture *				GetShadowMap() const override									{ return mShadowMap; }
+	virtual void					OnWindowResize() override										{ }
+
+	MTKView *						GetView() const													{ return mView; }
+	id<MTLDevice>					GetDevice() const												{ return mView.device; }
+	id<MTLRenderCommandEncoder>		GetRenderEncoder() const										{ return mRenderEncoder; }
+
+private:
+	MTKView *						mView;
+	MTLRenderPassDescriptor *		mShadowRenderPass;
+	Ref<TextureMTL>					mShadowMap;
+	id<MTLLibrary>					mShaderLibrary;
+	id<MTLCommandQueue>				mCommandQueue;
+	id<MTLCommandBuffer> 			mCommandBuffer;
+	id<MTLRenderCommandEncoder>		mRenderEncoder;
+};

+ 181 - 0
TestFramework/Renderer/MTL/RendererMTL.mm

@@ -0,0 +1,181 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/MTL/RendererMTL.h>
+#include <Renderer/MTL/RenderPrimitiveMTL.h>
+#include <Renderer/MTL/RenderInstancesMTL.h>
+#include <Renderer/MTL/PipelineStateMTL.h>
+#include <Renderer/MTL/VertexShaderMTL.h>
+#include <Renderer/MTL/PixelShaderMTL.h>
+#include <Renderer/MTL/TextureMTL.h>
+#include <Renderer/MTL/FatalErrorIfFailedMTL.h>
+#include <Window/ApplicationWindowMacOS.h>
+#include <Utils/Log.h>
+#include <Jolt/Core/Profiler.h>
+
+RendererMTL::~RendererMTL()
+{
+	[mCommandQueue release];
+	[mShadowRenderPass release];
+	[mShaderLibrary release];
+}
+
+void RendererMTL::Initialize(ApplicationWindow *inWindow)
+{
+	Renderer::Initialize(inWindow);
+
+	mView = static_cast<ApplicationWindowMacOS *>(inWindow)->GetMetalView();
+
+	id<MTLDevice> device = GetDevice();
+
+	// Load the shader library containing all shaders for the test framework
+	NSError *error = nullptr;
+	NSURL *url = [NSURL URLWithString: @"Assets/Shaders/MTL/Shaders.metallib"];
+	mShaderLibrary = [device newLibraryWithURL: url error: &error];
+	FatalErrorIfFailed(error);
+
+	// Create depth only texture (no color buffer, as seen from light)
+	mShadowMap = new TextureMTL(this, cShadowMapSize, cShadowMapSize);
+
+	// Create render pass descriptor for shadow pass
+	mShadowRenderPass = [[MTLRenderPassDescriptor alloc] init];
+	mShadowRenderPass.depthAttachment.texture = mShadowMap->GetTexture();
+	mShadowRenderPass.depthAttachment.loadAction = MTLLoadActionClear;
+	mShadowRenderPass.depthAttachment.storeAction = MTLStoreActionStore;
+	mShadowRenderPass.depthAttachment.clearDepth = 0.0f;
+
+	// Create the command queue
+	mCommandQueue = [device newCommandQueue];
+}
+
+void RendererMTL::BeginFrame(const CameraState &inCamera, float inWorldScale)
+{
+	JPH_PROFILE_FUNCTION();
+
+	Renderer::BeginFrame(inCamera, inWorldScale);
+
+	// Update frame index
+	mFrameIndex = (mFrameIndex + 1) % cFrameCount;
+
+	// Create a new command buffer
+	mCommandBuffer = [mCommandQueue commandBuffer];
+
+	// Create shadow render encoder
+	mRenderEncoder = [mCommandBuffer renderCommandEncoderWithDescriptor: mShadowRenderPass];
+
+	// Set viewport to size of shadow map
+	[mRenderEncoder setViewport: (MTLViewport){ 0.0, 0.0, double(cShadowMapSize), double(cShadowMapSize), 0.0, 1.0 }];
+
+	// Set pixel shader constants
+	[mRenderEncoder setFragmentBytes: &mPSBuffer length: sizeof(mPSBuffer) atIndex: 0];
+
+	// Counter clockwise is default winding order
+	[mRenderEncoder setFrontFacingWinding: MTLWindingCounterClockwise];
+
+	// Start with projection mode
+	SetProjectionMode();
+}
+
+void RendererMTL::EndShadowPass()
+{
+	// Finish the shadow encoder
+	[mRenderEncoder endEncoding];
+	mRenderEncoder = nil;
+
+	// Get the descriptor for the main window
+	MTLRenderPassDescriptor *render_pass_descriptor = mView.currentRenderPassDescriptor;
+	if (render_pass_descriptor == nullptr)
+		return;
+
+	// Create render encoder
+	mRenderEncoder = [mCommandBuffer renderCommandEncoderWithDescriptor: render_pass_descriptor];
+
+	// Set viewport
+	[mRenderEncoder setViewport: (MTLViewport){ 0.0, 0.0, double(mWindow->GetWindowWidth()), double(mWindow->GetWindowHeight()), 0.0, 1.0 }];
+
+	// Set pixel shader constants
+	[mRenderEncoder setFragmentBytes: &mPSBuffer length: sizeof(mPSBuffer) atIndex: 0];
+
+	// Counter clockwise is default winding order
+	[mRenderEncoder setFrontFacingWinding: MTLWindingCounterClockwise];
+
+	// Start with projection mode
+	SetProjectionMode();
+}
+
+void RendererMTL::EndFrame()
+{
+	JPH_PROFILE_FUNCTION();
+
+	// Finish the encoder
+	[mRenderEncoder endEncoding];
+	mRenderEncoder = nil;
+
+	// Schedule a present
+	[mCommandBuffer presentDrawable: mView.currentDrawable];
+
+	// Commit the command buffer
+	[mCommandBuffer commit];
+
+	Renderer::EndFrame();
+}
+
+void RendererMTL::SetProjectionMode()
+{
+	JPH_ASSERT(mInFrame);
+
+	[mRenderEncoder setVertexBytes: &mVSBuffer length: sizeof(mVSBuffer) atIndex: 2];
+}
+
+void RendererMTL::SetOrthoMode()
+{
+	JPH_ASSERT(mInFrame);
+
+	[mRenderEncoder setVertexBytes: &mVSBufferOrtho length: sizeof(mVSBufferOrtho) atIndex: 2];
+}
+
+Ref<Texture> RendererMTL::CreateTexture(const Surface *inSurface)
+{
+	return new TextureMTL(this, inSurface);
+}
+
+Ref<VertexShader> RendererMTL::CreateVertexShader(const char *inName)
+{
+	id<MTLFunction> function = [mShaderLibrary newFunctionWithName: [[[NSString alloc] initWithUTF8String: inName] autorelease]];
+	if (function == nil)
+		FatalError("Vertex shader %s not found", inName);
+	return new VertexShaderMTL(function);
+}
+
+Ref<PixelShader> RendererMTL::CreatePixelShader(const char *inName)
+{
+	id<MTLFunction> function = [mShaderLibrary newFunctionWithName: [[[NSString alloc] initWithUTF8String: inName] autorelease]];
+	if (function == nil)
+		FatalError("Pixel shader %s not found", inName);
+	return new PixelShaderMTL(function);
+}
+
+unique_ptr<PipelineState> RendererMTL::CreatePipelineState(const VertexShader *inVertexShader, const PipelineState::EInputDescription *inInputDescription, uint inInputDescriptionCount, const PixelShader *inPixelShader, PipelineState::EDrawPass inDrawPass, PipelineState::EFillMode inFillMode, PipelineState::ETopology inTopology, PipelineState::EDepthTest inDepthTest, PipelineState::EBlendMode inBlendMode, PipelineState::ECullMode inCullMode)
+{
+	return make_unique<PipelineStateMTL>(this, static_cast<const VertexShaderMTL *>(inVertexShader), inInputDescription, inInputDescriptionCount, static_cast<const PixelShaderMTL *>(inPixelShader), inDrawPass, inFillMode, inTopology, inDepthTest, inBlendMode, inCullMode);
+}
+
+RenderPrimitive *RendererMTL::CreateRenderPrimitive(PipelineState::ETopology inType)
+{
+	return new RenderPrimitiveMTL(this, inType == PipelineState::ETopology::Line? MTLPrimitiveTypeLine : MTLPrimitiveTypeTriangle);
+}
+
+RenderInstances *RendererMTL::CreateRenderInstances()
+{
+	return new RenderInstancesMTL(this);
+}
+
+#ifndef JPH_ENABLE_VULKAN
+Renderer *Renderer::sCreate()
+{
+	return new RendererMTL;
+}
+#endif

+ 32 - 0
TestFramework/Renderer/MTL/TextureMTL.h

@@ -0,0 +1,32 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/Texture.h>
+
+#include <MetalKit/MetalKit.h>
+
+class RendererMTL;
+
+/// Metal texture
+class TextureMTL : public Texture
+{
+public:
+	/// Constructor, called by Renderer::CreateTextureMTL
+										TextureMTL(RendererMTL *inRenderer, const Surface *inSurface);	// Create a normal Texture
+										TextureMTL(RendererMTL *inRenderer, int inWidth, int inHeight);	// Create a render target (depth only)
+	virtual								~TextureMTL() override;
+
+	/// Bind texture to the pixel shader
+	virtual void						Bind() const override;
+
+	/// Access to the metal texture
+	id<MTLTexture>						GetTexture() const					{ return mTexture; }
+
+private:
+	RendererMTL *						mRenderer;
+	id<MTLTexture> 						mTexture;
+};
+

+ 98 - 0
TestFramework/Renderer/MTL/TextureMTL.mm

@@ -0,0 +1,98 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/MTL/TextureMTL.h>
+#include <Renderer/MTL/RendererMTL.h>
+#include <Renderer/MTL/FatalErrorIfFailedMTL.h>
+#include <Image/BlitSurface.h>
+
+TextureMTL::TextureMTL(RendererMTL *inRenderer, const Surface *inSurface) :
+	Texture(inSurface->GetWidth(), inSurface->GetHeight()),
+	mRenderer(inRenderer)
+{
+	ESurfaceFormat format = inSurface->GetFormat();
+	MTLPixelFormat mt_format = MTLPixelFormatBGRA8Unorm;
+	switch (format)
+	{
+	case ESurfaceFormat::A4L4:
+	case ESurfaceFormat::A8L8:
+	case ESurfaceFormat::A4R4G4B4:
+	case ESurfaceFormat::R8G8B8:
+	case ESurfaceFormat::B8G8R8:
+	case ESurfaceFormat::X8R8G8B8:
+	case ESurfaceFormat::X8B8G8R8:
+	case ESurfaceFormat::A8R8G8B8:
+	case ESurfaceFormat::A8B8G8R8:		mt_format = MTLPixelFormatBGRA8Unorm;			format = ESurfaceFormat::A8R8G8B8; break;
+	case ESurfaceFormat::L8:			mt_format = MTLPixelFormatR8Unorm;				break;
+	case ESurfaceFormat::A8:			mt_format = MTLPixelFormatA8Unorm;				break;
+	case ESurfaceFormat::R5G6B5:
+	case ESurfaceFormat::X1R5G5B5:
+	case ESurfaceFormat::X4R4G4B4:		mt_format = MTLPixelFormatB5G6R5Unorm;			format = ESurfaceFormat::R5G6B5; break;
+	case ESurfaceFormat::A1R5G5B5:		mt_format = MTLPixelFormatA1BGR5Unorm;			break;
+	case ESurfaceFormat::Invalid:
+	default:							JPH_ASSERT(false);								break;
+	}
+
+	// Blit the surface to another temporary surface if the format changed
+	const Surface *surface = inSurface;
+	Ref<Surface> tmp;
+	if (format != inSurface->GetFormat())
+	{
+		tmp = new SoftwareSurface(mWidth, mHeight, format);
+		BlitSurface(inSurface, tmp);
+		surface = tmp;
+	}
+
+	// Create descriptor
+	MTLTextureDescriptor *descriptor = [[MTLTextureDescriptor alloc] init];
+	descriptor.textureType = MTLTextureType2D;
+	descriptor.usage = MTLTextureUsageShaderRead;
+	descriptor.pixelFormat = mt_format;
+	descriptor.width = mWidth;
+	descriptor.height = mHeight;
+	descriptor.storageMode = MTLStorageModeManaged;
+
+	MTLRegion region =
+	{
+		{ 0, 0, 0 },
+		{ NSUInteger(mWidth), NSUInteger(mHeight), 1}
+	};
+
+	// Create texture
+	mTexture = [inRenderer->GetDevice() newTextureWithDescriptor: descriptor];
+	surface->Lock(ESurfaceLockMode::Read);
+	[mTexture replaceRegion: region mipmapLevel:0 withBytes: surface->GetData() bytesPerRow: surface->GetStride()];
+	surface->UnLock();
+
+	[descriptor release];
+}
+
+TextureMTL::TextureMTL(RendererMTL *inRenderer, int inWidth, int inHeight) :
+	Texture(inWidth, inHeight),
+	mRenderer(inRenderer)
+{
+	MTLTextureDescriptor *descriptor = [[MTLTextureDescriptor alloc] init];
+	descriptor.textureType = MTLTextureType2D;
+	descriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;
+	descriptor.pixelFormat = MTLPixelFormatDepth32Float;
+	descriptor.width = mWidth;
+	descriptor.height = mHeight;
+	descriptor.storageMode = MTLStorageModePrivate;
+
+	mTexture = [inRenderer->GetDevice() newTextureWithDescriptor: descriptor];
+	
+	[descriptor release];
+}
+
+TextureMTL::~TextureMTL()
+{
+	[mTexture release];
+}
+
+void TextureMTL::Bind() const
+{
+	[mRenderer->GetRenderEncoder() setFragmentTexture: mTexture atIndex: 0];
+}

+ 24 - 0
TestFramework/Renderer/MTL/VertexShaderMTL.h

@@ -0,0 +1,24 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/VertexShader.h>
+
+#include <MetalKit/MetalKit.h>
+
+/// Vertex shader handle for Metal
+class VertexShaderMTL : public VertexShader
+{
+public:
+	/// Constructor
+							VertexShaderMTL(id<MTLFunction> inFunction) : mFunction(inFunction) { }
+	virtual					~VertexShaderMTL() override { [mFunction release]; }
+
+	/// Access to the function
+	id<MTLFunction>			GetFunction() const				{ return mFunction; }
+
+private:
+	id<MTLFunction>			mFunction;
+};

+ 3 - 0
TestFramework/Renderer/Renderer.h

@@ -94,6 +94,9 @@ public:
 	/// Callback when the window resizes and the back buffer needs to be adjusted
 	virtual void					OnWindowResize() = 0;
 
+	/// Create a platform specific Renderer instance
+	static Renderer *				sCreate();
+
 protected:
 	struct VertexShaderConstantBuffer
 	{

+ 1 - 15
TestFramework/Renderer/VK/PipelineStateVK.cpp

@@ -24,6 +24,7 @@ PipelineStateVK::PipelineStateVK(RendererVK *inRenderer, const VertexShaderVK *i
 		switch (inInputDescription[i])
 		{
 		case EInputDescription::Position:
+		case EInputDescription::Normal:
 			temp_vtx.format = VK_FORMAT_R32G32B32_SFLOAT;
 			attribute_descriptions.push_back(temp_vtx);
 			temp_vtx.offset += 3 * sizeof(float);
@@ -35,12 +36,6 @@ PipelineStateVK::PipelineStateVK(RendererVK *inRenderer, const VertexShaderVK *i
 			temp_vtx.offset += 4 * sizeof(uint8);
 			break;
 
-		case EInputDescription::Normal:
-			temp_vtx.format = VK_FORMAT_R32G32B32_SFLOAT;
-			attribute_descriptions.push_back(temp_vtx);
-			temp_vtx.offset += 3 * sizeof(float);
-			break;
-
 		case EInputDescription::TexCoord:
 			temp_vtx.format = VK_FORMAT_R32G32_SFLOAT;
 			attribute_descriptions.push_back(temp_vtx);
@@ -55,15 +50,6 @@ PipelineStateVK::PipelineStateVK(RendererVK *inRenderer, const VertexShaderVK *i
 			break;
 
 		case EInputDescription::InstanceTransform:
-			instance_alignment = max(instance_alignment, 16u);
-			temp_instance.format = VK_FORMAT_R32G32B32A32_SFLOAT;
-			for (int j = 0; j < 4; ++j)
-			{
-				attribute_descriptions.push_back(temp_instance);
-				temp_instance.offset += 4 * sizeof(float);
-			}
-			break;
-
 		case EInputDescription::InstanceInvTransform:
 			instance_alignment = max(instance_alignment, 16u);
 			temp_instance.format = VK_FORMAT_R32G32B32A32_SFLOAT;

+ 7 - 0
TestFramework/Renderer/VK/RendererVK.cpp

@@ -1267,3 +1267,10 @@ void RendererVK::UpdateViewPortAndScissorRect(uint32 inWidth, uint32 inHeight)
 	scissor.extent = { inWidth, inHeight };
 	vkCmdSetScissor(command_buffer, 0, 1, &scissor);
 }
+
+#ifdef JPH_ENABLE_VULKAN
+Renderer *Renderer::sCreate()
+{
+	return new RendererVK;
+}
+#endif

+ 51 - 10
TestFramework/TestFramework.cmake

@@ -1,6 +1,6 @@
 # Find Vulkan
 find_package(Vulkan)
-if (NOT CROSS_COMPILE_ARM AND (Vulkan_FOUND OR WIN32))
+if (NOT CROSS_COMPILE_ARM AND (Vulkan_FOUND OR WIN32 OR ("${CMAKE_SYSTEM_NAME}" MATCHES "Darwin")))
 	# We have Vulkan/DirectX so we can compile TestFramework
 	set(TEST_FRAMEWORK_AVAILABLE TRUE)
 
@@ -112,12 +112,10 @@ if (NOT CROSS_COMPILE_ARM AND (Vulkan_FOUND OR WIN32))
 			${TEST_FRAMEWORK_ROOT}/Window/ApplicationWindowWin.h
 		)
 
-		# All shaders
+		# HLSL vertex shaders
 		set(TEST_FRAMEWORK_SRC_FILES_SHADERS
 			${PHYSICS_REPO_ROOT}/Assets/Shaders/DX/VertexConstants.h
 		)
-
-		# HLSL vertex shaders
 		set(TEST_FRAMEWORK_HLSL_VERTEX_SHADERS
 			${PHYSICS_REPO_ROOT}/Assets/Shaders/DX/FontVertexShader.hlsl
 			${PHYSICS_REPO_ROOT}/Assets/Shaders/DX/LineVertexShader.hlsl
@@ -158,6 +156,20 @@ if (NOT CROSS_COMPILE_ARM AND (Vulkan_FOUND OR WIN32))
 		# macOS source files
 		set(TEST_FRAMEWORK_SRC_FILES
 			${TEST_FRAMEWORK_SRC_FILES}
+			${TEST_FRAMEWORK_ROOT}/Renderer/MTL/FatalErrorIfFailedMTL.mm
+			${TEST_FRAMEWORK_ROOT}/Renderer/MTL/FatalErrorIfFailedMTL.h
+			${TEST_FRAMEWORK_ROOT}/Renderer/MTL/PipelineStateMTL.mm
+			${TEST_FRAMEWORK_ROOT}/Renderer/MTL/PipelineStateMTL.h
+			${TEST_FRAMEWORK_ROOT}/Renderer/MTL/PixelShaderMTL.h
+			${TEST_FRAMEWORK_ROOT}/Renderer/MTL/RendererMTL.mm
+			${TEST_FRAMEWORK_ROOT}/Renderer/MTL/RendererMTL.h
+			${TEST_FRAMEWORK_ROOT}/Renderer/MTL/RenderInstancesMTL.mm
+			${TEST_FRAMEWORK_ROOT}/Renderer/MTL/RenderInstancesMTL.h
+			${TEST_FRAMEWORK_ROOT}/Renderer/MTL/RenderPrimitiveMTL.mm
+			${TEST_FRAMEWORK_ROOT}/Renderer/MTL/RenderPrimitiveMTL.h
+			${TEST_FRAMEWORK_ROOT}/Renderer/MTL/TextureMTL.mm
+			${TEST_FRAMEWORK_ROOT}/Renderer/MTL/TextureMTL.h
+			${TEST_FRAMEWORK_ROOT}/Renderer/MTL/VertexShaderMTL.h
 			${TEST_FRAMEWORK_ROOT}/Input/MacOS/KeyboardMacOS.mm
 			${TEST_FRAMEWORK_ROOT}/Input/MacOS/KeyboardMacOS.h
 			${TEST_FRAMEWORK_ROOT}/Input/MacOS/MouseMacOS.mm
@@ -165,6 +177,35 @@ if (NOT CROSS_COMPILE_ARM AND (Vulkan_FOUND OR WIN32))
 			${TEST_FRAMEWORK_ROOT}/Window/ApplicationWindowMacOS.mm
 			${TEST_FRAMEWORK_ROOT}/Window/ApplicationWindowMacOS.h
 		)
+
+		# Metal shaders
+		set(TEST_FRAMEWORK_SRC_FILES_SHADERS
+			${PHYSICS_REPO_ROOT}/Assets/Shaders/MTL/VertexConstants.h
+		)
+		set(TEST_FRAMEWORK_METAL_SHADERS
+			${PHYSICS_REPO_ROOT}/Assets/Shaders/MTL/FontShader.metal
+			${PHYSICS_REPO_ROOT}/Assets/Shaders/MTL/LineShader.metal
+			${PHYSICS_REPO_ROOT}/Assets/Shaders/MTL/TriangleShader.metal
+			${PHYSICS_REPO_ROOT}/Assets/Shaders/MTL/UIShader.metal
+		)
+
+		# Compile Metal shaders
+		foreach(SHADER ${TEST_FRAMEWORK_METAL_SHADERS})
+			cmake_path(GET SHADER FILENAME AIR_SHADER)
+			set(AIR_SHADER "${CMAKE_CURRENT_BINARY_DIR}/${AIR_SHADER}.air")
+			add_custom_command(OUTPUT ${AIR_SHADER}
+				COMMAND xcrun -sdk macosx metal -c ${SHADER} -o ${AIR_SHADER}
+				DEPENDS ${SHADER}
+				COMMENT "Compiling ${SHADER}")
+			list(APPEND TEST_FRAMEWORK_AIR_SHADERS ${AIR_SHADER})
+		endforeach()
+
+		# Link Metal shaders
+		set(TEST_FRAMEWORK_METAL_LIB ${PHYSICS_REPO_ROOT}/Assets/Shaders/MTL/Shaders.metallib)
+		add_custom_command(OUTPUT ${TEST_FRAMEWORK_METAL_LIB}
+			COMMAND xcrun -sdk macosx metallib -o ${TEST_FRAMEWORK_METAL_LIB} ${TEST_FRAMEWORK_AIR_SHADERS}
+			DEPENDS ${TEST_FRAMEWORK_AIR_SHADERS}
+			COMMENT "Linking shaders")
 	endif()
 
 	# Include the Vulkan library
@@ -196,8 +237,6 @@ if (NOT CROSS_COMPILE_ARM AND (Vulkan_FOUND OR WIN32))
 			${TEST_FRAMEWORK_SRC_FILES_SHADERS}
 			${PHYSICS_REPO_ROOT}/Assets/Shaders/VK/VertexConstants.h
 		)
-
-		# GLSL shaders
 		set(TEST_FRAMEWORK_GLSL_SHADERS
 			${PHYSICS_REPO_ROOT}/Assets/Shaders/VK/FontVertexShader.vert
 			${PHYSICS_REPO_ROOT}/Assets/Shaders/VK/LineVertexShader.vert
@@ -227,10 +266,13 @@ if (NOT CROSS_COMPILE_ARM AND (Vulkan_FOUND OR WIN32))
 	source_group(TREE ${TEST_FRAMEWORK_ROOT} FILES ${TEST_FRAMEWORK_SRC_FILES})
 
 	# Group shader files
-	source_group(TREE ${PHYSICS_REPO_ROOT} FILES ${TEST_FRAMEWORK_SRC_FILES_SHADERS} ${TEST_FRAMEWORK_GLSL_SHADERS} ${TEST_FRAMEWORK_SPV_SHADERS})
+	source_group(TREE ${PHYSICS_REPO_ROOT} FILES ${TEST_FRAMEWORK_SRC_FILES_SHADERS} ${TEST_FRAMEWORK_GLSL_SHADERS} ${TEST_FRAMEWORK_METAL_SHADERS})
+
+	# Group intermediate files
+	source_group(Intermediate FILES ${TEST_FRAMEWORK_SPV_SHADERS} ${TEST_FRAMEWORK_METAL_LIB})
 
 	# Create TestFramework lib
-	add_library(TestFramework STATIC ${TEST_FRAMEWORK_SRC_FILES} ${TEST_FRAMEWORK_SRC_FILES_SHADERS} ${TEST_FRAMEWORK_SPV_SHADERS})
+	add_library(TestFramework STATIC ${TEST_FRAMEWORK_SRC_FILES} ${TEST_FRAMEWORK_SRC_FILES_SHADERS} ${TEST_FRAMEWORK_GLSL_SHADERS} ${TEST_FRAMEWORK_SPV_SHADERS} ${TEST_FRAMEWORK_METAL_SHADERS} ${TEST_FRAMEWORK_METAL_LIB})
 	target_include_directories(TestFramework PUBLIC ${TEST_FRAMEWORK_ROOT})
 	target_precompile_headers(TestFramework PUBLIC ${TEST_FRAMEWORK_ROOT}/TestFramework.h)
 
@@ -245,7 +287,6 @@ if (NOT CROSS_COMPILE_ARM AND (Vulkan_FOUND OR WIN32))
 	if (WIN32)
 		# Windows configuration
 		target_link_libraries(TestFramework LINK_PUBLIC Jolt dxguid.lib dinput8.lib dxgi.lib d3d12.lib d3dcompiler.lib shcore.lib)
-		target_compile_definitions(TestFramework PRIVATE JPH_ENABLE_DIRECTX)
 	endif()
 	if (LINUX)
 		# Linux configuration
@@ -254,7 +295,7 @@ if (NOT CROSS_COMPILE_ARM AND (Vulkan_FOUND OR WIN32))
 	if ("${CMAKE_SYSTEM_NAME}" MATCHES "Darwin")
 		# macOS configuration
 		target_link_libraries(TestFramework LINK_PUBLIC Jolt "-framework Cocoa -framework Metal -framework MetalKit -framework GameController")
-		
+
 		# Ignore PCH files for .mm files
 		foreach(SRC_FILE ${TEST_FRAMEWORK_SRC_FILES})
 			if (SRC_FILE MATCHES "\.mm")

+ 9 - 3
TestFramework/Window/ApplicationWindowMacOS.h

@@ -7,8 +7,10 @@
 #include <Window/ApplicationWindow.h>
 
 #ifdef __OBJC__
+@class MTKView;
 @class CAMetalLayer;
 #else
+typedef void MTKView;
 typedef void CAMetalLayer;
 #endif
 
@@ -16,11 +18,15 @@ typedef void CAMetalLayer;
 class ApplicationWindowMacOS : public ApplicationWindow
 {
 public:
+	/// Destructor
+	virtual							~ApplicationWindowMacOS() override;
+
 	/// Initialize the window
 	virtual void					Initialize() override;
 
-	/// Access to the metal layer
-	CAMetalLayer *					GetMetalLayer() const					{ return mMetalLayer; }
+	/// Access to the metal objects
+	MTKView *						GetMetalView() const					{ return mMetalView; }
+	CAMetalLayer *					GetMetalLayer() const;
 
 	/// Enter the main loop and keep rendering frames until the window is closed
 	virtual void					MainLoop(RenderCallback inRenderCallback) override;
@@ -34,7 +40,7 @@ public:
 	void							OnMouseMoved(int inX, int inY)			{ mMouseMovedCallback(inX, inY); }
 
 protected:
-	CAMetalLayer *					mMetalLayer = nullptr;
+	MTKView *						mMetalView = nullptr;
 	ApplicationWindow::RenderCallback mRenderCallback;
 	MouseMovedCallback				mMouseMovedCallback;
 };

+ 32 - 12
TestFramework/Window/ApplicationWindowMacOS.mm

@@ -19,8 +19,10 @@
 
 - (MetalView *)init:(ApplicationWindowMacOS *)window
 {
-	[super initWithFrame: NSMakeRect(0, 0, window->GetWindowWidth(), window->GetWindowHeight()) device: MTLCreateSystemDefaultDevice()];
-	
+	id<MTLDevice> device = MTLCreateSystemDefaultDevice();
+	self = [super initWithFrame: NSMakeRect(0, 0, window->GetWindowWidth(), window->GetWindowHeight()) device: device];
+	[device release];
+
 	mWindow = window;
 	
 	self.delegate = self;
@@ -38,20 +40,24 @@
 	return YES;
 }
 
-- (BOOL)isFlipped {
-    return YES;
+- (BOOL)isFlipped
+{
+	return YES;
 }
 
 - (void)mouseMoved:(NSEvent *)event
 {
-    NSPoint locationInView = [self convertPoint:event.locationInWindow fromView:nil];
-    NSPoint locationInBacking = [self convertPointToBacking:locationInView];
-	mWindow->OnMouseMoved(locationInBacking.x, -locationInBacking.y);
+	NSPoint location_in_view = [self convertPoint: event.locationInWindow fromView: nil];
+	NSPoint location_in_backing = [self convertPointToBacking: location_in_view];
+	mWindow->OnMouseMoved(location_in_backing.x, -location_in_backing.y);
 }
 
 - (void)drawInMTKView:(MTKView *)view
 {
-	mWindow->RenderCallback();
+	@autoreleasepool
+	{
+		mWindow->RenderCallback();
+	}
 }
 
 - (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size
@@ -71,8 +77,8 @@
 {
 	// Add the Quit button to the first menu item on the toolbar
 	NSMenu *app_menu = [[NSApp mainMenu] itemAtIndex: 0].submenu;
-	NSMenuItem *quit_item = [[NSMenuItem alloc] initWithTitle:@"Quit" action:@selector(terminate:) keyEquivalent:@"q"];
-	[app_menu addItem:quit_item];
+	NSMenuItem *quit_item = [[NSMenuItem alloc] initWithTitle: @"Quit" action: @selector(terminate:) keyEquivalent: @"q"];
+	[app_menu addItem: quit_item];
 }
 
 -(bool)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
@@ -83,11 +89,19 @@
 
 @end
 
+ApplicationWindowMacOS::~ApplicationWindowMacOS()
+{
+	[mMetalView release];
+}
+
 void ApplicationWindowMacOS::Initialize()
 {
 	// Create metal view
 	MetalView *view = [[MetalView alloc] init: this];
-	mMetalLayer = (CAMetalLayer *)view.layer;
+	view.clearColor = MTLClearColorMake(0.098f, 0.098f, 0.439f, 1.000f);
+	view.depthStencilPixelFormat = MTLPixelFormatDepth32Float;
+	view.clearDepth = 0.0f;
+	mMetalView = view;
 
 	// Create window
 	NSWindow *window = [[NSWindow alloc] initWithContentRect: NSMakeRect(0, 0, mWindowWidth, mWindowHeight)
@@ -108,7 +122,13 @@ void ApplicationWindowMacOS::MainLoop(ApplicationWindow::RenderCallback inRender
 	{
 		NSApplication *app = [NSApplication sharedApplication];
 		AppDelegate *delegate = [[AppDelegate alloc] init];
-		[app setDelegate:delegate];
+		[app setDelegate: delegate];
 		[app run];
+		[delegate release];
 	}
 }
+
+CAMetalLayer *ApplicationWindowMacOS::GetMetalLayer() const
+{
+	return (CAMetalLayer *)mMetalView.layer;
+}