From 582b1ef819b86c8e12e81a27bc19501eff003233 Mon Sep 17 00:00:00 2001 From: Jorrit Rouwe Date: Fri, 10 Jan 2025 21:52:28 +0100 Subject: [PATCH] Implemented a Metal based renderer on macOS (#1450) --- .gitignore | 1 + Assets/Shaders/MTL/FontShader.metal | 39 ++++ Assets/Shaders/MTL/LineShader.metal | 30 +++ Assets/Shaders/MTL/TriangleShader.metal | 199 ++++++++++++++++++ Assets/Shaders/MTL/UIShader.metal | 41 ++++ Assets/Shaders/MTL/VertexConstants.h | 13 ++ Build/CMakeLists.txt | 2 +- Build/README.md | 3 +- Docs/ReleaseNotes.md | 3 +- TestFramework/Application/Application.cpp | 13 +- TestFramework/Input/MacOS/KeyboardMacOS.mm | 15 +- TestFramework/Renderer/DX12/RendererDX12.cpp | 7 + .../Renderer/MTL/FatalErrorIfFailedMTL.h | 11 + .../Renderer/MTL/FatalErrorIfFailedMTL.mm | 14 ++ TestFramework/Renderer/MTL/PipelineStateMTL.h | 32 +++ .../Renderer/MTL/PipelineStateMTL.mm | 163 ++++++++++++++ TestFramework/Renderer/MTL/PixelShaderMTL.h | 24 +++ .../Renderer/MTL/RenderInstancesMTL.h | 36 ++++ .../Renderer/MTL/RenderInstancesMTL.mm | 52 +++++ .../Renderer/MTL/RenderPrimitiveMTL.h | 40 ++++ .../Renderer/MTL/RenderPrimitiveMTL.mm | 74 +++++++ TestFramework/Renderer/MTL/RendererMTL.h | 46 ++++ TestFramework/Renderer/MTL/RendererMTL.mm | 181 ++++++++++++++++ TestFramework/Renderer/MTL/TextureMTL.h | 32 +++ TestFramework/Renderer/MTL/TextureMTL.mm | 98 +++++++++ TestFramework/Renderer/MTL/VertexShaderMTL.h | 24 +++ TestFramework/Renderer/Renderer.h | 3 + TestFramework/Renderer/VK/PipelineStateVK.cpp | 16 +- TestFramework/Renderer/VK/RendererVK.cpp | 7 + TestFramework/TestFramework.cmake | 61 +++++- TestFramework/Window/ApplicationWindowMacOS.h | 12 +- .../Window/ApplicationWindowMacOS.mm | 44 ++-- 32 files changed, 1272 insertions(+), 64 deletions(-) create mode 100644 Assets/Shaders/MTL/FontShader.metal create mode 100644 Assets/Shaders/MTL/LineShader.metal create mode 100644 Assets/Shaders/MTL/TriangleShader.metal create mode 100644 Assets/Shaders/MTL/UIShader.metal create mode 100644 Assets/Shaders/MTL/VertexConstants.h create mode 100644 TestFramework/Renderer/MTL/FatalErrorIfFailedMTL.h create mode 100644 TestFramework/Renderer/MTL/FatalErrorIfFailedMTL.mm create mode 100644 TestFramework/Renderer/MTL/PipelineStateMTL.h create mode 100644 TestFramework/Renderer/MTL/PipelineStateMTL.mm create mode 100644 TestFramework/Renderer/MTL/PixelShaderMTL.h create mode 100644 TestFramework/Renderer/MTL/RenderInstancesMTL.h create mode 100644 TestFramework/Renderer/MTL/RenderInstancesMTL.mm create mode 100644 TestFramework/Renderer/MTL/RenderPrimitiveMTL.h create mode 100644 TestFramework/Renderer/MTL/RenderPrimitiveMTL.mm create mode 100644 TestFramework/Renderer/MTL/RendererMTL.h create mode 100644 TestFramework/Renderer/MTL/RendererMTL.mm create mode 100644 TestFramework/Renderer/MTL/TextureMTL.h create mode 100644 TestFramework/Renderer/MTL/TextureMTL.mm create mode 100644 TestFramework/Renderer/MTL/VertexShaderMTL.h diff --git a/.gitignore b/.gitignore index fb7d73d13..3c39fa020 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /*.jor /detlog.txt /Assets/Shaders/VK/*.spv +/Assets/Shaders/MTL/*.metallib diff --git a/Assets/Shaders/MTL/FontShader.metal b/Assets/Shaders/MTL/FontShader.metal new file mode 100644 index 000000000..b605d99ba --- /dev/null +++ b/Assets/Shaders/MTL/FontShader.metal @@ -0,0 +1,39 @@ +#include + +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 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); +} diff --git a/Assets/Shaders/MTL/LineShader.metal b/Assets/Shaders/MTL/LineShader.metal new file mode 100644 index 000000000..d0326bc45 --- /dev/null +++ b/Assets/Shaders/MTL/LineShader.metal @@ -0,0 +1,30 @@ +#include + +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; +} diff --git a/Assets/Shaders/MTL/TriangleShader.metal b/Assets/Shaders/MTL/TriangleShader.metal new file mode 100644 index 000000000..f483e71d4 --- /dev/null +++ b/Assets/Shaders/MTL/TriangleShader.metal @@ -0,0 +1,199 @@ +#include + +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 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); +} diff --git a/Assets/Shaders/MTL/UIShader.metal b/Assets/Shaders/MTL/UIShader.metal new file mode 100644 index 000000000..35f65173e --- /dev/null +++ b/Assets/Shaders/MTL/UIShader.metal @@ -0,0 +1,41 @@ +#include + +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 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; +} diff --git a/Assets/Shaders/MTL/VertexConstants.h b/Assets/Shaders/MTL/VertexConstants.h new file mode 100644 index 000000000..abe13a816 --- /dev/null +++ b/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; +}; diff --git a/Build/CMakeLists.txt b/Build/CMakeLists.txt index e76b580ac..1b2451d67 100644 --- a/Build/CMakeLists.txt +++ b/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 diff --git a/Build/README.md b/Build/README.md index a2eb35068..895bf96c4 100644 --- a/Build/README.md +++ b/Build/README.md @@ -125,7 +125,7 @@ To implement your custom memory allocator override Allocate, Free, Reallocate, A
- Linux + Linux (Ubuntu)
  • Install clang (apt-get install clang)
  • Install cmake (apt-get install cmake)
  • @@ -152,7 +152,6 @@ To implement your custom memory allocator override Allocate, Free, Reallocate, A
    • Install XCode
    • Download CMake 3.23+ (https://cmake.org/download/)
    • -
    • If you want to build the Samples or JoltViewer, install the Vulkan SDK
    • Run: ./cmake_xcode_macos.sh
    • This will open XCode with a newly generated project
    • Build and run the project
    • diff --git a/Docs/ReleaseNotes.md b/Docs/ReleaseNotes.md index 1f411fa98..d88916c61 100644 --- a/Docs/ReleaseNotes.md +++ b/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. diff --git a/TestFramework/Application/Application.cpp b/TestFramework/Application/Application.cpp index 91055329f..45b99023f 100644 --- a/TestFramework/Application/Application.cpp +++ b/TestFramework/Application/Application.cpp @@ -13,11 +13,6 @@ #include #include #include -#ifdef JPH_ENABLE_VULKAN - #include -#elif defined(JPH_ENABLE_DIRECTX) - #include -#endif #ifdef JPH_PLATFORM_WINDOWS #include #include @@ -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 diff --git a/TestFramework/Input/MacOS/KeyboardMacOS.mm b/TestFramework/Input/MacOS/KeyboardMacOS.mm index 71604d28d..c18e93a25 100644 --- a/TestFramework/Input/MacOS/KeyboardMacOS.mm +++ b/TestFramework/Input/MacOS/KeyboardMacOS.mm @@ -76,14 +76,13 @@ - (KeyboardDelegate *)init:(KeyboardMacOS *)Keyboard { 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; } diff --git a/TestFramework/Renderer/DX12/RendererDX12.cpp b/TestFramework/Renderer/DX12/RendererDX12.cpp index fab8e9e30..e1f4578b7 100644 --- a/TestFramework/Renderer/DX12/RendererDX12.cpp +++ b/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 diff --git a/TestFramework/Renderer/MTL/FatalErrorIfFailedMTL.h b/TestFramework/Renderer/MTL/FatalErrorIfFailedMTL.h new file mode 100644 index 000000000..e38dbc9f8 --- /dev/null +++ b/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 + +/// Convert Metal error to readable text and alert +void FatalErrorIfFailed(NSError *inResult); + diff --git a/TestFramework/Renderer/MTL/FatalErrorIfFailedMTL.mm b/TestFramework/Renderer/MTL/FatalErrorIfFailedMTL.mm new file mode 100644 index 000000000..2e9bade10 --- /dev/null +++ b/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 + +#include +#include + +void FatalErrorIfFailed(NSError *inResult) +{ + if (inResult != nullptr) + FatalError("Metal error returned: %s", [[inResult localizedDescription] cStringUsingEncoding: NSUTF8StringEncoding]); +} diff --git a/TestFramework/Renderer/MTL/PipelineStateMTL.h b/TestFramework/Renderer/MTL/PipelineStateMTL.h new file mode 100644 index 000000000..31f6d3cc7 --- /dev/null +++ b/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 +#include +#include + +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 mVertexShader; + RefConst mPixelShader; + id mPipelineState; + id mDepthState; + MTLCullMode mCullMode; + MTLTriangleFillMode mFillMode; +}; diff --git a/TestFramework/Renderer/MTL/PipelineStateMTL.mm b/TestFramework/Renderer/MTL/PipelineStateMTL.mm new file mode 100644 index 000000000..43e6bc45a --- /dev/null +++ b/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 + +#include +#include +#include + +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(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 encoder = mRenderer->GetRenderEncoder(); + [encoder setRenderPipelineState: mPipelineState]; + [encoder setDepthStencilState: mDepthState]; + [encoder setCullMode: mCullMode]; + [encoder setTriangleFillMode: mFillMode]; +} diff --git a/TestFramework/Renderer/MTL/PixelShaderMTL.h b/TestFramework/Renderer/MTL/PixelShaderMTL.h new file mode 100644 index 000000000..4de8cdb01 --- /dev/null +++ b/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 + +#include + +/// Pixel shader handle for Metal +class PixelShaderMTL : public PixelShader +{ +public: + /// Constructor + PixelShaderMTL(id inFunction) : mFunction(inFunction) { } + virtual ~PixelShaderMTL() override { [mFunction release]; } + + /// Access to the function + id GetFunction() const { return mFunction; } + +private: + id mFunction; +}; diff --git a/TestFramework/Renderer/MTL/RenderInstancesMTL.h b/TestFramework/Renderer/MTL/RenderInstancesMTL.h new file mode 100644 index 000000000..30c89dd9c --- /dev/null +++ b/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 +#include + +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 mBuffer; + NSUInteger mBufferSize; + NSUInteger mInstanceSize; +}; diff --git a/TestFramework/Renderer/MTL/RenderInstancesMTL.mm b/TestFramework/Renderer/MTL/RenderInstancesMTL.mm new file mode 100644 index 000000000..1d2a61b81 --- /dev/null +++ b/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 + +#include +#include + +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 encoder = mRenderer->GetRenderEncoder(); + RenderPrimitiveMTL *prim = static_cast(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]; +} diff --git a/TestFramework/Renderer/MTL/RenderPrimitiveMTL.h b/TestFramework/Renderer/MTL/RenderPrimitiveMTL.h new file mode 100644 index 000000000..99070a24c --- /dev/null +++ b/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 +#include + +/// 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 mVertexBuffer; + id mIndexBuffer; +}; diff --git a/TestFramework/Renderer/MTL/RenderPrimitiveMTL.mm b/TestFramework/Renderer/MTL/RenderPrimitiveMTL.mm new file mode 100644 index 000000000..e987dcefb --- /dev/null +++ b/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 + +#include + +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 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]; +} diff --git a/TestFramework/Renderer/MTL/RendererMTL.h b/TestFramework/Renderer/MTL/RendererMTL.h new file mode 100644 index 000000000..03f3330f4 --- /dev/null +++ b/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 +#include + +#include + +/// 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 CreateTexture(const Surface *inSurface) override; + virtual Ref CreateVertexShader(const char *inName) override; + virtual Ref CreatePixelShader(const char *inName) override; + virtual unique_ptr 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 GetDevice() const { return mView.device; } + id GetRenderEncoder() const { return mRenderEncoder; } + +private: + MTKView * mView; + MTLRenderPassDescriptor * mShadowRenderPass; + Ref mShadowMap; + id mShaderLibrary; + id mCommandQueue; + id mCommandBuffer; + id mRenderEncoder; +}; diff --git a/TestFramework/Renderer/MTL/RendererMTL.mm b/TestFramework/Renderer/MTL/RendererMTL.mm new file mode 100644 index 000000000..1685e3459 --- /dev/null +++ b/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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +RendererMTL::~RendererMTL() +{ + [mCommandQueue release]; + [mShadowRenderPass release]; + [mShaderLibrary release]; +} + +void RendererMTL::Initialize(ApplicationWindow *inWindow) +{ + Renderer::Initialize(inWindow); + + mView = static_cast(inWindow)->GetMetalView(); + + id 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 RendererMTL::CreateTexture(const Surface *inSurface) +{ + return new TextureMTL(this, inSurface); +} + +Ref RendererMTL::CreateVertexShader(const char *inName) +{ + id function = [mShaderLibrary newFunctionWithName: [[[NSString alloc] initWithUTF8String: inName] autorelease]]; + if (function == nil) + FatalError("Vertex shader %s not found", inName); + return new VertexShaderMTL(function); +} + +Ref RendererMTL::CreatePixelShader(const char *inName) +{ + id function = [mShaderLibrary newFunctionWithName: [[[NSString alloc] initWithUTF8String: inName] autorelease]]; + if (function == nil) + FatalError("Pixel shader %s not found", inName); + return new PixelShaderMTL(function); +} + +unique_ptr 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(this, static_cast(inVertexShader), inInputDescription, inInputDescriptionCount, static_cast(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 diff --git a/TestFramework/Renderer/MTL/TextureMTL.h b/TestFramework/Renderer/MTL/TextureMTL.h new file mode 100644 index 000000000..475efc01b --- /dev/null +++ b/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 + +#include + +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 GetTexture() const { return mTexture; } + +private: + RendererMTL * mRenderer; + id mTexture; +}; + diff --git a/TestFramework/Renderer/MTL/TextureMTL.mm b/TestFramework/Renderer/MTL/TextureMTL.mm new file mode 100644 index 000000000..d7272664a --- /dev/null +++ b/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 + +#include +#include +#include +#include + +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 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]; +} diff --git a/TestFramework/Renderer/MTL/VertexShaderMTL.h b/TestFramework/Renderer/MTL/VertexShaderMTL.h new file mode 100644 index 000000000..256d2c6e1 --- /dev/null +++ b/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 + +#include + +/// Vertex shader handle for Metal +class VertexShaderMTL : public VertexShader +{ +public: + /// Constructor + VertexShaderMTL(id inFunction) : mFunction(inFunction) { } + virtual ~VertexShaderMTL() override { [mFunction release]; } + + /// Access to the function + id GetFunction() const { return mFunction; } + +private: + id mFunction; +}; diff --git a/TestFramework/Renderer/Renderer.h b/TestFramework/Renderer/Renderer.h index 114e797c2..33ae32ae2 100644 --- a/TestFramework/Renderer/Renderer.h +++ b/TestFramework/Renderer/Renderer.h @@ -94,6 +94,9 @@ class Renderer /// 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 { diff --git a/TestFramework/Renderer/VK/PipelineStateVK.cpp b/TestFramework/Renderer/VK/PipelineStateVK.cpp index 1f62e4e26..0c888902b 100644 --- a/TestFramework/Renderer/VK/PipelineStateVK.cpp +++ b/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; diff --git a/TestFramework/Renderer/VK/RendererVK.cpp b/TestFramework/Renderer/VK/RendererVK.cpp index d2b94eb30..1ce83f419 100644 --- a/TestFramework/Renderer/VK/RendererVK.cpp +++ b/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 diff --git a/TestFramework/TestFramework.cmake b/TestFramework/TestFramework.cmake index be22b0349..de45a15df 100644 --- a/TestFramework/TestFramework.cmake +++ b/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") diff --git a/TestFramework/Window/ApplicationWindowMacOS.h b/TestFramework/Window/ApplicationWindowMacOS.h index 3ae426a25..e71525211 100644 --- a/TestFramework/Window/ApplicationWindowMacOS.h +++ b/TestFramework/Window/ApplicationWindowMacOS.h @@ -7,8 +7,10 @@ #include #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 @@ class ApplicationWindowMacOS : public ApplicationWindow void OnMouseMoved(int inX, int inY) { mMouseMovedCallback(inX, inY); } protected: - CAMetalLayer * mMetalLayer = nullptr; + MTKView * mMetalView = nullptr; ApplicationWindow::RenderCallback mRenderCallback; MouseMovedCallback mMouseMovedCallback; }; diff --git a/TestFramework/Window/ApplicationWindowMacOS.mm b/TestFramework/Window/ApplicationWindowMacOS.mm index 6ffe21f07..521a508c9 100644 --- a/TestFramework/Window/ApplicationWindowMacOS.mm +++ b/TestFramework/Window/ApplicationWindowMacOS.mm @@ -19,8 +19,10 @@ @implementation MetalView - (MetalView *)init:(ApplicationWindowMacOS *)window { - [super initWithFrame: NSMakeRect(0, 0, window->GetWindowWidth(), window->GetWindowHeight()) device: MTLCreateSystemDefaultDevice()]; - + id 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 @@ - (bool)canBecomeKeyView 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 @@ -(void)applicationDidFinishLaunching:(NSNotification *)notification { // 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 @@ -(bool)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender @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 @@ -(bool)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { 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; +}