Browse Source

Implement Vulkan renderer (#1420)

This is an alternative to the DirectX renderer and will be used to render the samples on other platforms than Windows.
Jorrit Rouwe 11 months ago
parent
commit
f55e0e31d0
69 changed files with 4093 additions and 1283 deletions
  1. 1 1
      .github/workflows/build.yml
  2. 1 0
      .gitignore
  3. 17 0
      Assets/Shaders/FontPixelShader.frag
  4. 2 0
      Assets/Shaders/FontPixelShader.hlsl
  5. 17 0
      Assets/Shaders/FontVertexShader.vert
  6. 10 0
      Assets/Shaders/LinePixelShader.frag
  7. 14 0
      Assets/Shaders/LineVertexShader.vert
  8. 6 0
      Assets/Shaders/TriangleDepthPixelShader.frag
  9. 31 0
      Assets/Shaders/TriangleDepthVertexShader.vert
  10. 110 0
      Assets/Shaders/TrianglePixelShader.frag
  11. 48 0
      Assets/Shaders/TriangleVertexShader.vert
  12. 13 0
      Assets/Shaders/UIPixelShader.frag
  13. 10 0
      Assets/Shaders/UIPixelShaderUntextured.frag
  14. 17 0
      Assets/Shaders/UIVertexShader.vert
  15. 7 0
      Assets/Shaders/VertexConstantsVK.h
  16. 3 0
      Build/CMakeLists.txt
  17. 16 1
      TestFramework/Application/Application.cpp
  18. 3 3
      TestFramework/Renderer/DX12/CommandQueueDX12.h
  19. 8 8
      TestFramework/Renderer/DX12/ConstantBufferDX12.cpp
  20. 6 6
      TestFramework/Renderer/DX12/ConstantBufferDX12.h
  21. 1 1
      TestFramework/Renderer/DX12/DescriptorHeapDX12.h
  22. 1 1
      TestFramework/Renderer/DX12/FatalErrorIfFailedDX12.cpp
  23. 1 1
      TestFramework/Renderer/DX12/FatalErrorIfFailedDX12.h
  24. 135 0
      TestFramework/Renderer/DX12/PipelineStateDX12.cpp
  25. 27 0
      TestFramework/Renderer/DX12/PipelineStateDX12.h
  26. 17 0
      TestFramework/Renderer/DX12/PixelShaderDX12.h
  27. 20 18
      TestFramework/Renderer/DX12/RenderInstancesDX12.cpp
  28. 37 0
      TestFramework/Renderer/DX12/RenderInstancesDX12.h
  29. 148 0
      TestFramework/Renderer/DX12/RenderPrimitiveDX12.cpp
  30. 45 0
      TestFramework/Renderer/DX12/RenderPrimitiveDX12.h
  31. 708 0
      TestFramework/Renderer/DX12/RendererDX12.cpp
  32. 111 0
      TestFramework/Renderer/DX12/RendererDX12.h
  33. 16 24
      TestFramework/Renderer/DX12/TextureDX12.cpp
  34. 32 0
      TestFramework/Renderer/DX12/TextureDX12.h
  35. 17 0
      TestFramework/Renderer/DX12/VertexShaderDX12.h
  36. 45 65
      TestFramework/Renderer/DebugRendererImp.cpp
  37. 5 18
      TestFramework/Renderer/DebugRendererImp.h
  38. 11 11
      TestFramework/Renderer/Font.cpp
  39. 1 0
      TestFramework/Renderer/Font.h
  40. 0 84
      TestFramework/Renderer/PipelineState.cpp
  41. 36 13
      TestFramework/Renderer/PipelineState.h
  42. 15 0
      TestFramework/Renderer/PixelShader.h
  43. 7 16
      TestFramework/Renderer/RenderInstances.h
  44. 0 121
      TestFramework/Renderer/RenderPrimitive.cpp
  45. 16 24
      TestFramework/Renderer/RenderPrimitive.h
  46. 18 705
      TestFramework/Renderer/Renderer.cpp
  47. 49 98
      TestFramework/Renderer/Renderer.h
  48. 5 20
      TestFramework/Renderer/Texture.h
  49. 35 0
      TestFramework/Renderer/VK/BufferVK.h
  50. 32 0
      TestFramework/Renderer/VK/ConstantBufferVK.cpp
  51. 30 0
      TestFramework/Renderer/VK/ConstantBufferVK.h
  52. 14 0
      TestFramework/Renderer/VK/FatalErrorIfFailedVK.cpp
  53. 11 0
      TestFramework/Renderer/VK/FatalErrorIfFailedVK.h
  54. 189 0
      TestFramework/Renderer/VK/PipelineStateVK.cpp
  55. 30 0
      TestFramework/Renderer/VK/PipelineStateVK.h
  56. 34 0
      TestFramework/Renderer/VK/PixelShaderVK.h
  57. 54 0
      TestFramework/Renderer/VK/RenderInstancesVK.cpp
  58. 35 0
      TestFramework/Renderer/VK/RenderInstancesVK.h
  59. 100 0
      TestFramework/Renderer/VK/RenderPrimitiveVK.cpp
  60. 44 0
      TestFramework/Renderer/VK/RenderPrimitiveVK.h
  61. 1112 0
      TestFramework/Renderer/VK/RendererVK.cpp
  62. 123 0
      TestFramework/Renderer/VK/RendererVK.h
  63. 180 0
      TestFramework/Renderer/VK/TextureVK.cpp
  64. 35 0
      TestFramework/Renderer/VK/TextureVK.h
  65. 34 0
      TestFramework/Renderer/VK/VertexShaderVK.h
  66. 15 0
      TestFramework/Renderer/VertexShader.h
  67. 101 24
      TestFramework/TestFramework.cmake
  68. 20 20
      TestFramework/UI/UIManager.cpp
  69. 1 0
      TestFramework/UI/UIManager.h

+ 1 - 1
.github/workflows/build.yml

@@ -193,7 +193,7 @@ jobs:
         update: true
     - name: Configure CMake
       working-directory: ${{github.workspace}}/Build
-      run: ./cmake_linux_mingw.sh ${{matrix.build_type}} g++ -DBUILD_SHARED_LIBS=${{matrix.shared_lib}}
+      run: ./cmake_linux_mingw.sh ${{matrix.build_type}} -DBUILD_SHARED_LIBS=${{matrix.shared_lib}}
     - name: Build
       run: cmake --build Build/MinGW_${{matrix.build_type}} -j $(nproc)
     - name: Test

+ 1 - 0
.gitignore

@@ -6,3 +6,4 @@
 /snapshot.bin
 /*.jor
 /detlog.txt
+/Assets/Shaders/*.spv

+ 17 - 0
Assets/Shaders/FontPixelShader.frag

@@ -0,0 +1,17 @@
+#version 450
+
+layout(set = 1, binding = 0) uniform sampler2D texSampler;
+
+layout(location = 0) in vec2 iTex;
+layout(location = 1) in vec4 iColor;
+
+layout(location = 0) out vec4 oColor;
+
+void main()
+{
+	float t = texture(texSampler, iTex).x;
+	if (t < 0.5)
+		discard;
+	
+	oColor = vec4(iColor.xyz, t);
+}

+ 2 - 0
Assets/Shaders/FontPixelShader.hlsl

@@ -18,6 +18,8 @@ PS_OUTPUT main(PS_INPUT In)
 	PS_OUTPUT Output;
 
 	float t = ShaderTexture.Sample(SampleType, In.Tex).r;
+	if (t < 0.5)
+		discard;
 
 	Output.RGBColor = float4(In.Color.rgb, t);
 

+ 17 - 0
Assets/Shaders/FontVertexShader.vert

@@ -0,0 +1,17 @@
+#version 450
+
+#include "VertexConstantsVK.h"
+
+layout(location = 0) in vec3 vPos;
+layout(location = 1) in vec2 vTex;
+layout(location = 2) in vec4 vCol;
+
+layout(location = 0) out vec2 oTex;
+layout(location = 1) out vec4 oColor;
+
+void main() 
+{
+	gl_Position = c.Projection * c.View * vec4(vPos, 1.0f);
+	oTex = vTex;
+	oColor = vCol;
+}

+ 10 - 0
Assets/Shaders/LinePixelShader.frag

@@ -0,0 +1,10 @@
+#version 450
+
+layout(location = 0) in vec4 iColor;
+
+layout(location = 0) out vec4 oColor;
+
+void main()
+{
+    oColor = iColor;
+}

+ 14 - 0
Assets/Shaders/LineVertexShader.vert

@@ -0,0 +1,14 @@
+#version 450
+
+#include "VertexConstantsVK.h"
+
+layout(location = 0) in vec3 iPosition;
+layout(location = 1) in vec4 iColor;
+
+layout(location = 0) out vec4 oColor;
+
+void main() 
+{
+    gl_Position = c.Projection * c.View * vec4(iPosition, 1.0);
+    oColor = iColor;
+}

+ 6 - 0
Assets/Shaders/TriangleDepthPixelShader.frag

@@ -0,0 +1,6 @@
+#version 450
+
+// We only write depth, so this shader does nothing
+void main()
+{
+}

+ 31 - 0
Assets/Shaders/TriangleDepthVertexShader.vert

@@ -0,0 +1,31 @@
+#version 450
+
+#include "VertexConstantsVK.h"
+
+layout(location = 0) in vec3 vPos;
+layout(location = 1) in vec3 vNorm;
+layout(location = 2) in vec2 vTex;
+layout(location = 3) in vec4 vCol;
+
+layout(location = 4) in mat4 iModel;
+layout(location = 8) in mat4 iModelInvTrans;
+layout(location = 12) in vec4 iCol;
+
+void main() 
+{
+	// Check if the alpha = 0
+	if (vCol.a * iCol.a == 0.0)
+	{
+		// Don't draw the triangle by moving it to an invalid location
+		gl_Position = vec4(0, 0, 0, 0);
+	}
+	else
+	{
+		// Transform the position from world space to homogeneous projection space for the light
+		vec4 pos = vec4(vPos, 1.0f);
+		pos = iModel * pos;
+		pos = c.LightView * pos;
+		pos = c.LightProjection * pos;
+		gl_Position = pos;
+	}
+}

+ 110 - 0
Assets/Shaders/TrianglePixelShader.frag

@@ -0,0 +1,110 @@
+#version 450
+
+layout(binding = 1) uniform PixelShaderConstantBuffer
+{
+	vec3	CameraPos;
+	vec3	LightPos;
+} c;
+
+layout(location = 0) in vec3 iNormal;
+layout(location = 1) in vec3 iWorldPos;
+layout(location = 2) in vec2 iTex;
+layout(location = 3) in vec4 iPositionL;
+layout(location = 4) in vec4 iColor;
+
+layout(location = 0) out vec4 oColor;
+
+layout(set = 1, binding = 0) uniform sampler2DShadow LightDepthSampler;
+
+void main()
+{
+	// Constants
+	float AmbientFactor = 0.3;
+	vec3 DiffuseColor = vec3(iColor.r, iColor.g, iColor.b);
+	vec3 SpecularColor = vec3(1, 1, 1);
+	float SpecularPower = 100.0;
+	float bias = 1.0e-7;
+
+	// Homogenize position in light space
+	vec3 position_l = iPositionL.xyz / iPositionL.w;
+
+	// Calculate dot product between direction to light and surface normal and clamp between [0, 1]
+	vec3 view_dir = normalize(c.CameraPos - iWorldPos);
+	vec3 world_to_light = c.LightPos - iWorldPos;
+	vec3 light_dir = normalize(world_to_light);
+	vec3 normal = normalize(iNormal);
+	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, 1);
+
+	// Calculate texture coordinates in light depth texture
+	vec2 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 (iColor.a > 0 // Alpha = 0 means don't receive shadows
+		&& tex_coord.x == clamp(tex_coord.x, 0, 1) && tex_coord.y == clamp(tex_coord.y, 0, 1))
+	{
+		// 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;
+		vec2 offsets[] = { 
+			vec2(-1.5 * width, -1.5 * height),
+			vec2(-0.5 * width, -1.5 * height),
+			vec2(0.5 * width, -1.5 * height),
+			vec2(1.5 * width, -1.5 * height),
+
+			vec2(-1.5 * width, -0.5 * height),
+			vec2(-0.5 * width, -0.5 * height),
+			vec2(0.5 * width, -0.5 * height),
+			vec2(1.5 * width, -0.5 * height),
+
+			vec2(-1.5 * width, 0.5 * height),
+			vec2(-0.5 * width, 0.5 * height),
+			vec2(0.5 * width, 0.5 * height),
+			vec2(1.5 * width, 0.5 * height),
+
+			vec2(-1.5 * width, 1.5 * height),
+			vec2(-0.5 * width, 1.5 * height),
+			vec2(0.5 * width, 1.5 * height),
+			vec2(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)
+		{
+			vec3 location_and_reference = vec3(tex_coord + offsets[i], light_depth);
+			shadow_factor += texture(LightDepthSampler, location_and_reference);
+		}
+		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, 1), SpecularPower) : 0.0;
+
+	// Apply procedural pattern based on the uv coordinates
+	bvec2 less_half = lessThan(iTex - floor(iTex), vec2(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
+	vec2 dx = dFdx(iTex), dy = dFdy(iTex);
+	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
+	oColor = vec4(clamp((AmbientFactor + diffuse * shadow_factor) * darken_factor * DiffuseColor + SpecularColor * specular * shadow_factor, 0, 1), 1);
+}

+ 48 - 0
Assets/Shaders/TriangleVertexShader.vert

@@ -0,0 +1,48 @@
+#version 450
+
+#include "VertexConstantsVK.h"
+
+layout(location = 0) in vec3 vPos;
+layout(location = 1) in vec3 vNorm;
+layout(location = 2) in vec2 vTex;
+layout(location = 3) in vec4 vCol;
+
+layout(location = 4) in mat4 iModel;
+layout(location = 8) in mat4 iModelInvTrans;
+layout(location = 12) in vec4 iCol;
+
+layout(location = 0) out vec3 oNormal;
+layout(location = 1) out vec3 oWorldPos;
+layout(location = 2) out vec2 oTex;
+layout(location = 3) out vec4 oPositionL;
+layout(location = 4) out vec4 oColor;
+
+void main() 
+{
+	// Get world position
+	vec4 pos = vec4(vPos, 1.0f);
+	vec4 world_pos = iModel * pos;
+
+	// Transform the position from world space to homogeneous projection space
+	vec4 proj_pos = c.View * world_pos;
+	proj_pos = c.Projection * proj_pos;
+	gl_Position = proj_pos;
+
+	// Transform the position from world space to projection space of the light
+	vec4 proj_lpos = c.LightView * world_pos;
+	proj_lpos = c.LightProjection * proj_lpos;
+	oPositionL = proj_lpos;
+
+	// output normal
+	vec4 norm = vec4(vNorm, 0.0f);
+	oNormal = normalize(iModelInvTrans * norm).xyz;
+
+	// output world position of the vertex
+	oWorldPos = world_pos.xyz;
+
+	// output texture coordinates
+	oTex = vTex;
+
+	// output color
+	oColor = vCol * iCol;
+}

+ 13 - 0
Assets/Shaders/UIPixelShader.frag

@@ -0,0 +1,13 @@
+#version 450
+
+layout(set = 1, binding = 0) uniform sampler2D texSampler;
+
+layout(location = 0) in vec4 iColor;
+layout(location = 1) in vec2 iTex;
+
+layout(location = 0) out vec4 oColor;
+
+void main()
+{
+	oColor = iColor * texture(texSampler, iTex);
+}

+ 10 - 0
Assets/Shaders/UIPixelShaderUntextured.frag

@@ -0,0 +1,10 @@
+#version 450
+
+layout(location = 0) in vec4 iColor;
+
+layout(location = 0) out vec4 oColor;
+
+void main()
+{
+	oColor = iColor;
+}

+ 17 - 0
Assets/Shaders/UIVertexShader.vert

@@ -0,0 +1,17 @@
+#version 450
+
+#include "VertexConstantsVK.h"
+
+layout(location = 0) in vec3 vPos;
+layout(location = 1) in vec2 vTex;
+layout(location = 2) in vec4 vCol;
+
+layout(location = 0) out vec4 oColor;
+layout(location = 1) out vec2 oTex;
+
+void main() 
+{
+	gl_Position = c.Projection * c.View * vec4(vPos, 1.0f);
+	oTex = vTex;
+	oColor = vCol;
+}

+ 7 - 0
Assets/Shaders/VertexConstantsVK.h

@@ -0,0 +1,7 @@
+layout(binding = 0) uniform VertexShaderConstantBuffer
+{
+	mat4	View;
+	mat4	Projection;
+	mat4	LightView;
+	mat4	LightProjection;
+} c;

+ 3 - 0
Build/CMakeLists.txt

@@ -106,6 +106,9 @@ include(CMakeDependentOption)
 # Windows Store only supports the DLL version
 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)
+
 # 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
 	if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")

+ 16 - 1
TestFramework/Application/Application.cpp

@@ -14,6 +14,11 @@
 #include <Jolt/RegisterTypes.h>
 #include <Renderer/DebugRendererImp.h>
 #include <crtdbg.h>
+#ifdef JPH_ENABLE_VULKAN
+	#include <Renderer/VK/RendererVK.h>
+#else
+	#include <Renderer/DX12/RendererDX12.h>
+#endif
 
 // Constructor
 Application::Application() :
@@ -52,7 +57,11 @@ Application::Application() :
 		DisableCustomMemoryHook dcmh;
 
 		// Create renderer
-		mRenderer = new Renderer;
+	#ifdef JPH_ENABLE_VULKAN
+		mRenderer = new RendererVK;
+	#else
+		mRenderer = new RendererDX12;
+	#endif
 		mRenderer->Initialize();
 
 		// Create font
@@ -226,6 +235,12 @@ void Application::Run()
 			// Start rendering
 			mRenderer->BeginFrame(mWorldCamera, GetWorldScale());
 
+			// Draw from light
+			static_cast<DebugRendererImp *>(mDebugRenderer)->DrawShadowPass();
+
+			// Start drawing normally
+			mRenderer->EndShadowPass();
+
 			// Draw debug information
 			static_cast<DebugRendererImp *>(mDebugRenderer)->Draw();
 

+ 3 - 3
TestFramework/Renderer/CommandQueue.h → TestFramework/Renderer/DX12/CommandQueueDX12.h

@@ -4,14 +4,14 @@
 
 #pragma once
 
-#include <Renderer/FatalErrorIfFailed.h>
+#include <Renderer/DX12/FatalErrorIfFailedDX12.h>
 
 /// Holds a number of DirectX operations with logic to wait for completion
-class CommandQueue
+class CommandQueueDX12
 {
 public:
 	/// Destructor
-										~CommandQueue()
+										~CommandQueueDX12()
 	{
 		WaitUntilFinished();
 

+ 8 - 8
TestFramework/Renderer/ConstantBuffer.cpp → TestFramework/Renderer/DX12/ConstantBufferDX12.cpp

@@ -4,24 +4,24 @@
 
 #include <TestFramework.h>
 
-#include <Renderer/ConstantBuffer.h>
-#include <Renderer/Renderer.h>
-#include <Renderer/FatalErrorIfFailed.h>
+#include <Renderer/DX12/ConstantBufferDX12.h>
+#include <Renderer/DX12/RendererDX12.h>
+#include <Renderer/DX12/FatalErrorIfFailedDX12.h>
 
-ConstantBuffer::ConstantBuffer(Renderer *inRenderer, uint64 inBufferSize) :
+ConstantBufferDX12::ConstantBufferDX12(RendererDX12 *inRenderer, uint64 inBufferSize) :
 	mRenderer(inRenderer)
 {
 	mBuffer = mRenderer->CreateD3DResourceOnUploadHeap(inBufferSize);
 	mBufferSize = inBufferSize;
 }
 
-ConstantBuffer::~ConstantBuffer()
+ConstantBufferDX12::~ConstantBufferDX12()
 {
 	if (mBuffer != nullptr)
 		mRenderer->RecycleD3DResourceOnUploadHeap(mBuffer.Get(), mBufferSize);
 }
 
-void *ConstantBuffer::MapInternal()
+void *ConstantBufferDX12::MapInternal()
 {
 	void *mapped_resource;
 	D3D12_RANGE range = { 0, 0 }; // We're not going to read
@@ -29,12 +29,12 @@ void *ConstantBuffer::MapInternal()
 	return mapped_resource;
 }
 
-void ConstantBuffer::Unmap()
+void ConstantBufferDX12::Unmap()
 {
 	mBuffer->Unmap(0, nullptr);
 }
 
-void ConstantBuffer::Bind(int inSlot)
+void ConstantBufferDX12::Bind(int inSlot)
 {
 	mRenderer->GetCommandList()->SetGraphicsRootConstantBufferView(inSlot, mBuffer->GetGPUVirtualAddress());
 }

+ 6 - 6
TestFramework/Renderer/ConstantBuffer.h → TestFramework/Renderer/DX12/ConstantBufferDX12.h

@@ -4,15 +4,15 @@
 
 #pragma once
 
-class Renderer;
+class RendererDX12;
 
 /// A binary blob that can be used to pass constants to a shader
-class ConstantBuffer
+class ConstantBufferDX12
 {
 public:
 	/// Constructor
-										ConstantBuffer(Renderer *inRenderer, uint64 inBufferSize);
-										~ConstantBuffer();
+										ConstantBufferDX12(RendererDX12 *inRenderer, uint64 inBufferSize);
+										~ConstantBufferDX12();
 
 	/// Map / unmap buffer (get pointer to data). This will discard all data in the buffer.
 	template <typename T> T *			Map()											{ return reinterpret_cast<T *>(MapInternal()); }
@@ -22,11 +22,11 @@ public:
 	void								Bind(int inSlot);
 
 private:
-	friend class Renderer;
+	friend class RendererDX12;
 
 	void *								MapInternal();
 
-	Renderer *							mRenderer;
+	RendererDX12 *						mRenderer;
 	ComPtr<ID3D12Resource>				mBuffer;
 	uint64								mBufferSize;
 };

+ 1 - 1
TestFramework/Renderer/DescriptorHeap.h → TestFramework/Renderer/DX12/DescriptorHeapDX12.h

@@ -5,7 +5,7 @@
 #pragma once
 
 /// DirectX descriptor heap, used to allocate handles for resources to bind them to shaders
-class DescriptorHeap
+class DescriptorHeapDX12
 {
 public:
 	/// Initialize the heap

+ 1 - 1
TestFramework/Renderer/FatalErrorIfFailed.cpp → TestFramework/Renderer/DX12/FatalErrorIfFailedDX12.cpp

@@ -6,7 +6,7 @@
 
 #include <system_error>
 
-#include <Renderer/FatalErrorIfFailed.h>
+#include <Renderer/DX12/FatalErrorIfFailedDX12.h>
 #include <Jolt/Core/StringTools.h>
 #include <Utils/Log.h>
 

+ 1 - 1
TestFramework/Renderer/FatalErrorIfFailed.h → TestFramework/Renderer/DX12/FatalErrorIfFailedDX12.h

@@ -4,6 +4,6 @@
 
 #pragma once
 
-/// Convert DirectX error codes to exceptions
+/// Convert DirectX error to readable text and alert
 void FatalErrorIfFailed(HRESULT inHResult);
 

+ 135 - 0
TestFramework/Renderer/DX12/PipelineStateDX12.cpp

@@ -0,0 +1,135 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/DX12/PipelineStateDX12.h>
+#include <Renderer/DX12/RendererDX12.h>
+#include <Renderer/DX12/VertexShaderDX12.h>
+#include <Renderer/DX12/PixelShaderDX12.h>
+#include <Renderer/DX12/FatalErrorIfFailedDX12.h>
+
+PipelineStateDX12::PipelineStateDX12(RendererDX12 *inRenderer, const VertexShaderDX12 *inVertexShader, const EInputDescription *inInputDescription, uint inInputDescriptionCount, const PixelShaderDX12 *inPixelShader, EDrawPass inDrawPass, EFillMode inFillMode, ETopology inTopology, EDepthTest inDepthTest, EBlendMode inBlendMode, ECullMode inCullMode) :
+	mRenderer(inRenderer)
+{
+	D3D12_PRIMITIVE_TOPOLOGY_TYPE topology = inTopology == ETopology::Triangle? D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE : D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE;
+
+	Array<D3D12_INPUT_ELEMENT_DESC> input_description;
+	uint vertex_offset = 0, instance_offset = 0;
+	for (uint i = 0; i < inInputDescriptionCount; ++i)
+		switch (inInputDescription[i])
+		{
+		case EInputDescription::Position:
+			input_description.push_back({ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, vertex_offset, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 });
+			vertex_offset += 3 * sizeof(float);
+			break;
+
+		case EInputDescription::Color:
+			input_description.push_back({ "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, vertex_offset, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 });
+			vertex_offset += 4 * sizeof(uint8);
+			break;
+
+		case EInputDescription::Normal:
+			input_description.push_back({ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, vertex_offset, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 });
+			vertex_offset += 3 * sizeof(float);
+			break;
+
+		case EInputDescription::TexCoord:
+			input_description.push_back({ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, vertex_offset, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 });
+			vertex_offset += 2 * sizeof(float);
+			break;
+
+		case EInputDescription::InstanceColor:
+			input_description.push_back({ "INSTANCE_COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 1, instance_offset, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 });
+			instance_offset += 4 * sizeof(uint8);
+			break;
+
+		case EInputDescription::InstanceTransform:
+		{
+			for (uint j = 0; j < 4; ++j)
+			{
+				input_description.push_back({ "INSTANCE_TRANSFORM", j, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, instance_offset, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 });
+				instance_offset += 4 * sizeof(float);
+			}
+			break;
+		}
+
+		case EInputDescription::InstanceInvTransform:
+		{
+			for (uint j = 0; j < 4; ++j)
+			{
+				input_description.push_back({ "INSTANCE_INV_TRANSFORM", j, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, instance_offset, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 });
+				instance_offset += 4 * sizeof(float);
+			}
+			break;
+		}
+	}
+
+	D3D12_GRAPHICS_PIPELINE_STATE_DESC pso_desc = {};
+	pso_desc.InputLayout = { input_description.data(), (UINT)input_description.size() };
+	pso_desc.pRootSignature = mRenderer->GetRootSignature();
+	pso_desc.VS = { inVertexShader->mShader->GetBufferPointer(), inVertexShader->mShader->GetBufferSize() };
+	pso_desc.PS = { inPixelShader->mShader->GetBufferPointer(), inPixelShader->mShader->GetBufferSize() };
+
+	pso_desc.RasterizerState.FillMode = inFillMode == EFillMode::Solid? D3D12_FILL_MODE_SOLID : D3D12_FILL_MODE_WIREFRAME;
+	pso_desc.RasterizerState.CullMode = inCullMode == ECullMode::Backface? D3D12_CULL_MODE_FRONT : D3D12_CULL_MODE_BACK; // DX uses left handed system so we reverse the options
+	pso_desc.RasterizerState.FrontCounterClockwise = FALSE;
+	pso_desc.RasterizerState.DepthBias = D3D12_DEFAULT_DEPTH_BIAS;
+	pso_desc.RasterizerState.DepthBiasClamp = D3D12_DEFAULT_DEPTH_BIAS_CLAMP;
+	pso_desc.RasterizerState.SlopeScaledDepthBias = D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS;
+	pso_desc.RasterizerState.DepthClipEnable = TRUE;
+	pso_desc.RasterizerState.MultisampleEnable = FALSE;
+	pso_desc.RasterizerState.AntialiasedLineEnable = FALSE;
+	pso_desc.RasterizerState.ForcedSampleCount = 0;
+	pso_desc.RasterizerState.ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF;
+
+	pso_desc.BlendState.AlphaToCoverageEnable = FALSE;
+	pso_desc.BlendState.IndependentBlendEnable = FALSE;
+
+	D3D12_RENDER_TARGET_BLEND_DESC &blend_desc = pso_desc.BlendState.RenderTarget[0];
+	blend_desc.LogicOpEnable = FALSE;
+	blend_desc.LogicOp = D3D12_LOGIC_OP_NOOP;
+	blend_desc.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
+	switch (inBlendMode)
+	{
+	case EBlendMode::Write:
+		blend_desc.BlendEnable = FALSE;
+		break;
+
+	case EBlendMode::AlphaBlend:
+		blend_desc.BlendEnable = TRUE;
+		blend_desc.SrcBlend = D3D12_BLEND_SRC_ALPHA;
+		blend_desc.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
+		blend_desc.BlendOp = D3D12_BLEND_OP_ADD;
+		blend_desc.SrcBlendAlpha = D3D12_BLEND_ZERO;
+		blend_desc.DestBlendAlpha = D3D12_BLEND_ZERO;
+		blend_desc.BlendOpAlpha = D3D12_BLEND_OP_ADD;
+		break;
+	}
+
+	pso_desc.DepthStencilState.DepthEnable = inDepthTest == EDepthTest::On? TRUE : FALSE;
+	pso_desc.DepthStencilState.DepthWriteMask = inDepthTest == EDepthTest::On? D3D12_DEPTH_WRITE_MASK_ALL : D3D12_DEPTH_WRITE_MASK_ZERO;
+	pso_desc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_GREATER;
+	pso_desc.DepthStencilState.StencilEnable = FALSE;
+
+	pso_desc.SampleMask = UINT_MAX;
+	pso_desc.PrimitiveTopologyType = topology;
+	pso_desc.NumRenderTargets = 1;
+	pso_desc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
+	pso_desc.DSVFormat = DXGI_FORMAT_D32_FLOAT;
+	pso_desc.SampleDesc.Count = 1;
+
+	FatalErrorIfFailed(mRenderer->GetDevice()->CreateGraphicsPipelineState(&pso_desc, IID_PPV_ARGS(&mPSO)));
+}
+
+PipelineStateDX12::~PipelineStateDX12()
+{
+	if (mPSO != nullptr)
+		mRenderer->RecycleD3DObject(mPSO.Get());
+}
+
+void PipelineStateDX12::Activate()
+{
+	mRenderer->GetCommandList()->SetPipelineState(mPSO.Get());
+}

+ 27 - 0
TestFramework/Renderer/DX12/PipelineStateDX12.h

@@ -0,0 +1,27 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/PipelineState.h>
+
+class RendererDX12;
+class VertexShaderDX12;
+class PixelShaderDX12;
+
+/// DirectX 12 pipeline state object
+class PipelineStateDX12 : public PipelineState
+{
+public:
+	/// Constructor
+										PipelineStateDX12(RendererDX12 *inRenderer, const VertexShaderDX12 *inVertexShader, const EInputDescription *inInputDescription, uint inInputDescriptionCount, const PixelShaderDX12 *inPixelShader, EDrawPass inDrawPass, EFillMode inFillMode, ETopology inTopology, EDepthTest inDepthTest, EBlendMode inBlendMode, ECullMode inCullMode);
+	virtual								~PipelineStateDX12() override;
+
+	/// Make this pipeline state active (any primitives rendered after this will use this state)
+	virtual void						Activate() override;
+
+private:
+	RendererDX12 *						mRenderer;
+	ComPtr<ID3D12PipelineState>			mPSO;
+};

+ 17 - 0
TestFramework/Renderer/DX12/PixelShaderDX12.h

@@ -0,0 +1,17 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/PixelShader.h>
+
+/// Pixel shader handle for DirectX
+class PixelShaderDX12 : public PixelShader
+{
+public:
+	/// Constructor
+							PixelShaderDX12(ComPtr<ID3DBlob> inShader)		: mShader(inShader) { }
+
+	ComPtr<ID3DBlob>		mShader;										///< The compiled shader
+};

+ 20 - 18
TestFramework/Renderer/RenderInstances.cpp → TestFramework/Renderer/DX12/RenderInstancesDX12.cpp

@@ -1,14 +1,14 @@
 // Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
-// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
 // SPDX-License-Identifier: MIT
 
 #include <TestFramework.h>
 
-#include <Renderer/RenderInstances.h>
-#include <Renderer/RenderPrimitive.h>
-#include <Renderer/FatalErrorIfFailed.h>
+#include <Renderer/DX12/RenderInstancesDX12.h>
+#include <Renderer/DX12/RenderPrimitiveDX12.h>
+#include <Renderer/DX12/FatalErrorIfFailedDX12.h>
 
-void RenderInstances::Clear()
+void RenderInstancesDX12::Clear()
 {
 	if (mInstanceBuffer != nullptr)
 		mRenderer->RecycleD3DResourceOnUploadHeap(mInstanceBuffer.Get(), mInstanceBufferSize);
@@ -18,7 +18,7 @@ void RenderInstances::Clear()
 	mInstanceSize = 0;
 }
 
-void RenderInstances::CreateBuffer(int inNumInstances, int inInstanceSize)
+void RenderInstancesDX12::CreateBuffer(int inNumInstances, int inInstanceSize)
 {
 	if (mInstanceBuffer == nullptr || mInstanceBufferSize < inNumInstances * inInstanceSize)
 	{
@@ -37,7 +37,7 @@ void RenderInstances::CreateBuffer(int inNumInstances, int inInstanceSize)
 	mInstanceSize = inInstanceSize;
 }
 
-void *RenderInstances::Lock()
+void *RenderInstancesDX12::Lock()
 {
 	uint32 *mapped_resource;
 	D3D12_RANGE range = { 0, 0 };
@@ -45,27 +45,29 @@ void *RenderInstances::Lock()
 	return mapped_resource;
 }
 
-void RenderInstances::Unlock()
+void RenderInstancesDX12::Unlock()
 {
 	mInstanceBuffer->Unmap(0, nullptr);
 }
 
-void RenderInstances::Draw(RenderPrimitive *inPrimitive, int inStartInstance, int inNumInstances) const
+void RenderInstancesDX12::Draw(RenderPrimitive *inPrimitive, int inStartInstance, int inNumInstances) const
 {
 	if (inNumInstances <= 0)
 		return;
 
+	RenderPrimitiveDX12 *primitive = static_cast<RenderPrimitiveDX12 *>(inPrimitive);
+
 	ID3D12GraphicsCommandList *command_list = mRenderer->GetCommandList();
 
 	// Set topology
-	command_list->IASetPrimitiveTopology(inPrimitive->mType);
+	command_list->IASetPrimitiveTopology(primitive->mType == PipelineState::ETopology::Triangle? D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST : D3D_PRIMITIVE_TOPOLOGY_LINELIST);
 
 	D3D12_VERTEX_BUFFER_VIEW vb_view[2];
 
 	// Vertex buffer
-	vb_view[0].BufferLocation = inPrimitive->mVtxBuffer->GetGPUVirtualAddress();
-	vb_view[0].StrideInBytes = inPrimitive->mVtxSize;
-	vb_view[0].SizeInBytes = inPrimitive->mNumVtxToDraw * inPrimitive->mVtxSize;
+	vb_view[0].BufferLocation = primitive->mVtxBuffer->GetGPUVirtualAddress();
+	vb_view[0].StrideInBytes = primitive->mVtxSize;
+	vb_view[0].SizeInBytes = primitive->mNumVtxToDraw * primitive->mVtxSize;
 
 	// Instances buffer
 	vb_view[1].BufferLocation = mInstanceBuffer->GetGPUVirtualAddress();
@@ -74,21 +76,21 @@ void RenderInstances::Draw(RenderPrimitive *inPrimitive, int inStartInstance, in
 
 	command_list->IASetVertexBuffers(0, 2, &vb_view[0]);
 
-	if (inPrimitive->mIdxBuffer == nullptr)
+	if (primitive->mIdxBuffer == nullptr)
 	{
 		// Draw instanced primitive
-		command_list->DrawInstanced(inPrimitive->mNumVtxToDraw, inNumInstances, 0, inStartInstance);
+		command_list->DrawInstanced(primitive->mNumVtxToDraw, inNumInstances, 0, inStartInstance);
 	}
 	else
 	{
 		// Set index buffer
 		D3D12_INDEX_BUFFER_VIEW ib_view;
-		ib_view.BufferLocation = inPrimitive->mIdxBuffer->GetGPUVirtualAddress();
-		ib_view.SizeInBytes = inPrimitive->mNumIdxToDraw * sizeof(uint32);
+		ib_view.BufferLocation = primitive->mIdxBuffer->GetGPUVirtualAddress();
+		ib_view.SizeInBytes = primitive->mNumIdxToDraw * sizeof(uint32);
 		ib_view.Format = DXGI_FORMAT_R32_UINT;
 		command_list->IASetIndexBuffer(&ib_view);
 
 		// Draw instanced primitive
-		command_list->DrawIndexedInstanced(inPrimitive->mNumIdxToDraw, inNumInstances, 0, 0, inStartInstance);
+		command_list->DrawIndexedInstanced(primitive->mNumIdxToDraw, inNumInstances, 0, 0, inStartInstance);
 	}
 }

+ 37 - 0
TestFramework/Renderer/DX12/RenderInstancesDX12.h

@@ -0,0 +1,37 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/DX12/RendererDX12.h>
+#include <Renderer/RenderInstances.h>
+
+class RenderPrimitive;
+
+/// DirectX 12 implementation of a render instances object
+class RenderInstancesDX12 : public RenderInstances
+{
+public:
+	/// Constructor
+							RenderInstancesDX12(RendererDX12 *inRenderer)										: mRenderer(inRenderer) { }
+	virtual					~RenderInstancesDX12() 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:
+	RendererDX12 *			mRenderer;
+
+	ComPtr<ID3D12Resource>	mInstanceBuffer;
+	int						mInstanceBufferSize = 0;
+	int						mInstanceSize = 0;
+};

+ 148 - 0
TestFramework/Renderer/DX12/RenderPrimitiveDX12.cpp

@@ -0,0 +1,148 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/DX12/RenderPrimitiveDX12.h>
+#include <Renderer/DX12/FatalErrorIfFailedDX12.h>
+
+void RenderPrimitiveDX12::ReleaseVertexBuffer()
+{
+	if (mVtxBuffer != nullptr)
+	{
+		if (mVtxBufferInUploadHeap)
+			mRenderer->RecycleD3DResourceOnUploadHeap(mVtxBuffer.Get(), mNumVtx * mVtxSize);
+		else
+			mRenderer->RecycleD3DObject(mVtxBuffer.Get());
+		mVtxBuffer = nullptr;
+	}
+
+	mVtxBufferInUploadHeap = false;
+	RenderPrimitive::ReleaseVertexBuffer();
+}
+
+void RenderPrimitiveDX12::ReleaseIndexBuffer()
+{
+	if (mIdxBuffer != nullptr)
+	{
+		if (mIdxBufferInUploadHeap)
+			mRenderer->RecycleD3DResourceOnUploadHeap(mIdxBuffer.Get(), mNumIdx * sizeof(uint32));
+		else
+			mRenderer->RecycleD3DObject(mIdxBuffer.Get());
+		mIdxBuffer = nullptr;
+	}
+
+	mIdxBufferInUploadHeap = false;
+	RenderPrimitive::ReleaseIndexBuffer();
+}
+
+void RenderPrimitiveDX12::CreateVertexBuffer(int inNumVtx, int inVtxSize, const void *inData)
+{
+	RenderPrimitive::CreateVertexBuffer(inNumVtx, inVtxSize, inData);
+
+	uint64 size = uint64(inNumVtx) * inVtxSize;
+
+	if (inData != nullptr)
+	{
+		// Data provided, assume the buffer is static so allocate it on the GPU
+		mVtxBuffer = mRenderer->CreateD3DResourceOnDefaultHeap(inData, size);
+		mVtxBufferInUploadHeap = false;
+	}
+	else
+	{
+		// No data provided, create a buffer that will be uploaded to the GPU every time it is used
+		mVtxBuffer = mRenderer->CreateD3DResourceOnUploadHeap(size);
+		mVtxBufferInUploadHeap = true;
+	}
+
+	JPH_IF_DEBUG(mVtxBuffer->SetName(L"Vertex Buffer");)
+}
+
+void *RenderPrimitiveDX12::LockVertexBuffer()
+{
+	void *mapped_resource;
+	D3D12_RANGE range = { 0, 0 };
+	FatalErrorIfFailed(mVtxBuffer->Map(0, &range, &mapped_resource));
+	return mapped_resource;
+}
+
+void RenderPrimitiveDX12::UnlockVertexBuffer()
+{
+	mVtxBuffer->Unmap(0, nullptr);
+}
+
+void RenderPrimitiveDX12::CreateIndexBuffer(int inNumIdx, const uint32 *inData)
+{
+	RenderPrimitive::CreateIndexBuffer(inNumIdx, inData);
+
+	uint64 size = uint64(inNumIdx) * sizeof(uint32);
+
+	if (inData != nullptr)
+	{
+		// Data provided, assume the buffer is static so allocate it on the GPU
+		mIdxBuffer = mRenderer->CreateD3DResourceOnDefaultHeap(inData, size);
+		mIdxBufferInUploadHeap = false;
+	}
+	else
+	{
+		// No data provided, create a buffer that will be uploaded to the GPU every time it is used
+		mIdxBuffer = mRenderer->CreateD3DResourceOnUploadHeap(size);
+		mIdxBufferInUploadHeap = true;
+	}
+
+	JPH_IF_DEBUG(mIdxBuffer->SetName(L"Index Buffer");)
+}
+
+uint32 *RenderPrimitiveDX12::LockIndexBuffer()
+{
+	uint32 *mapped_resource;
+	D3D12_RANGE range = { 0, 0 };
+	FatalErrorIfFailed(mIdxBuffer->Map(0, &range, (void **)&mapped_resource));
+	return mapped_resource;
+}
+
+void RenderPrimitiveDX12::UnlockIndexBuffer()
+{
+	mIdxBuffer->Unmap(0, nullptr);
+}
+
+void RenderPrimitiveDX12::Draw() const
+{
+	ID3D12GraphicsCommandList *command_list = mRenderer->GetCommandList();
+
+	// Set topology
+	command_list->IASetPrimitiveTopology(mType == PipelineState::ETopology::Triangle? D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST : D3D_PRIMITIVE_TOPOLOGY_LINELIST);
+
+	if (mIdxBuffer == nullptr)
+	{
+		// Set vertex buffer
+		D3D12_VERTEX_BUFFER_VIEW vb_view;
+		vb_view.BufferLocation = mVtxBuffer->GetGPUVirtualAddress();
+		vb_view.StrideInBytes = mVtxSize;
+		vb_view.SizeInBytes = mNumVtxToDraw * mVtxSize;
+		command_list->IASetVertexBuffers(0, 1, &vb_view);
+
+		// Draw the non indexed primitive
+		command_list->DrawInstanced(mNumVtxToDraw, 1, 0, 0);
+	}
+	else
+	{
+		// Set vertex buffer
+		D3D12_VERTEX_BUFFER_VIEW vb_view;
+		vb_view.BufferLocation = mVtxBuffer->GetGPUVirtualAddress();
+		vb_view.StrideInBytes = mVtxSize;
+		vb_view.SizeInBytes = mNumVtx * mVtxSize;
+		command_list->IASetVertexBuffers(0, 1, &vb_view);
+
+		// Set index buffer
+		D3D12_INDEX_BUFFER_VIEW ib_view;
+		ib_view.BufferLocation = mIdxBuffer->GetGPUVirtualAddress();
+		ib_view.SizeInBytes = mNumIdxToDraw * sizeof(uint32);
+		ib_view.Format = DXGI_FORMAT_R32_UINT;
+		command_list->IASetIndexBuffer(&ib_view);
+
+		// Draw indexed primitive
+		command_list->DrawIndexedInstanced(mNumIdxToDraw, 1, 0, 0, 0);
+	}
+}

+ 45 - 0
TestFramework/Renderer/DX12/RenderPrimitiveDX12.h

@@ -0,0 +1,45 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/RenderPrimitive.h>
+#include <Renderer/DX12/RendererDX12.h>
+
+/// DirectX 12 implementation of a render primitive
+class RenderPrimitiveDX12 : public RenderPrimitive
+{
+public:
+	/// Constructor
+							RenderPrimitiveDX12(RendererDX12 *inRenderer, PipelineState::ETopology inType)	: mRenderer(inRenderer), mType(inType) { }
+	virtual					~RenderPrimitiveDX12() 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 RenderInstancesDX12;
+
+	RendererDX12 *			mRenderer;
+
+	PipelineState::ETopology mType;
+
+	ComPtr<ID3D12Resource>	mVtxBuffer;
+	bool					mVtxBufferInUploadHeap = false;
+
+	ComPtr<ID3D12Resource>	mIdxBuffer;
+	bool					mIdxBufferInUploadHeap = false;
+};

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

@@ -0,0 +1,708 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/DX12/RendererDX12.h>
+#include <Renderer/DX12/RenderPrimitiveDX12.h>
+#include <Renderer/DX12/PipelineStateDX12.h>
+#include <Renderer/DX12/VertexShaderDX12.h>
+#include <Renderer/DX12/PixelShaderDX12.h>
+#include <Renderer/DX12/TextureDX12.h>
+#include <Renderer/DX12/RenderInstancesDX12.h>
+#include <Renderer/DX12/FatalErrorIfFailedDX12.h>
+#include <Jolt/Core/Profiler.h>
+#include <Utils/ReadData.h>
+#include <Utils/Log.h>
+
+#include <d3dcompiler.h>
+#ifdef JPH_DEBUG
+	#include <d3d12sdklayers.h>
+#endif
+
+RendererDX12::~RendererDX12()
+{
+	// Ensure that the GPU is no longer referencing resources that are about to be cleaned up by the destructor.
+	WaitForGpu();
+
+	// Don't add more stuff to the delay reference list
+	mIsExiting = true;
+
+	CloseHandle(mFenceEvent);
+}
+
+void RendererDX12::WaitForGpu()
+{
+	// Schedule a Signal command in the queue
+	UINT64 current_fence_value = mFenceValues[mFrameIndex];
+	FatalErrorIfFailed(mCommandQueue->Signal(mFence.Get(), current_fence_value));
+
+	// Wait until the fence has been processed
+	FatalErrorIfFailed(mFence->SetEventOnCompletion(current_fence_value, mFenceEvent));
+	WaitForSingleObjectEx(mFenceEvent, INFINITE, FALSE);
+
+	// Increment the fence value for all frames
+	for (uint n = 0; n < cFrameCount; ++n)
+		mFenceValues[n] = current_fence_value + 1;
+
+	// Release all used resources
+	for (Array<ComPtr<ID3D12Object>> &list : mDelayReleased)
+		list.clear();
+
+	// Anything that's not used yet can be removed, delayed objects are now available
+	mResourceCache.clear();
+	mDelayCached[mFrameIndex].swap(mResourceCache);
+}
+
+void RendererDX12::CreateRenderTargets()
+{
+	// Create render targets and views
+	for (uint n = 0; n < cFrameCount; ++n)
+	{
+		mRenderTargetViews[n] = mRTVHeap.Allocate();
+
+		FatalErrorIfFailed(mSwapChain->GetBuffer(n, IID_PPV_ARGS(&mRenderTargets[n])));
+		mDevice->CreateRenderTargetView(mRenderTargets[n].Get(), nullptr, mRenderTargetViews[n]);
+	}
+}
+
+void RendererDX12::CreateDepthBuffer()
+{
+	// Free any previous depth stencil view
+	if (mDepthStencilView.ptr != 0)
+		mDSVHeap.Free(mDepthStencilView);
+
+	// Free any previous depth stencil buffer
+	mDepthStencilBuffer.Reset();
+
+	// Allocate depth stencil buffer
+	D3D12_CLEAR_VALUE clear_value = {};
+	clear_value.Format = DXGI_FORMAT_D32_FLOAT;
+	clear_value.DepthStencil.Depth = 0.0f;
+	clear_value.DepthStencil.Stencil = 0;
+
+	D3D12_HEAP_PROPERTIES heap_properties = {};
+	heap_properties.Type = D3D12_HEAP_TYPE_DEFAULT;
+	heap_properties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
+	heap_properties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
+	heap_properties.CreationNodeMask = 1;
+	heap_properties.VisibleNodeMask = 1;
+
+	D3D12_RESOURCE_DESC depth_stencil_desc = {};
+	depth_stencil_desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
+	depth_stencil_desc.Alignment = 0;
+	depth_stencil_desc.Width = mWindowWidth;
+	depth_stencil_desc.Height = mWindowHeight;
+	depth_stencil_desc.DepthOrArraySize = 1;
+	depth_stencil_desc.MipLevels = 1;
+	depth_stencil_desc.Format = DXGI_FORMAT_D32_FLOAT;
+	depth_stencil_desc.SampleDesc.Count = 1;
+	depth_stencil_desc.SampleDesc.Quality = 0;
+	depth_stencil_desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
+	depth_stencil_desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
+
+	FatalErrorIfFailed(mDevice->CreateCommittedResource(&heap_properties, D3D12_HEAP_FLAG_NONE, &depth_stencil_desc, D3D12_RESOURCE_STATE_DEPTH_WRITE, &clear_value, IID_PPV_ARGS(&mDepthStencilBuffer)));
+
+	// Allocate depth stencil view
+	D3D12_DEPTH_STENCIL_VIEW_DESC depth_stencil_view_desc = {};
+	depth_stencil_view_desc.Format = DXGI_FORMAT_D32_FLOAT;
+	depth_stencil_view_desc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
+	depth_stencil_view_desc.Flags = D3D12_DSV_FLAG_NONE;
+
+	mDepthStencilView = mDSVHeap.Allocate();
+	mDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), &depth_stencil_view_desc, mDepthStencilView);
+}
+
+void RendererDX12::Initialize()
+{
+	Renderer::Initialize();
+
+#if defined(JPH_DEBUG)
+	// Enable the D3D12 debug layer
+	ComPtr<ID3D12Debug> debug_controller;
+	if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debug_controller))))
+		debug_controller->EnableDebugLayer();
+#endif
+
+	// Create DXGI factory
+	FatalErrorIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mDXGIFactory)));
+
+	// Find adapter
+	ComPtr<IDXGIAdapter1> adapter;
+
+	HRESULT result = E_FAIL;
+
+	// First check if we have the Windows 1803 IDXGIFactory6 interface
+	ComPtr<IDXGIFactory6> factory6;
+	if (SUCCEEDED(mDXGIFactory->QueryInterface(IID_PPV_ARGS(&factory6))))
+	{
+		for (UINT index = 0; DXGI_ERROR_NOT_FOUND != factory6->EnumAdapterByGpuPreference(index, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&adapter)); ++index)
+		{
+			DXGI_ADAPTER_DESC1 desc;
+			adapter->GetDesc1(&desc);
+
+			// We don't want software renderers
+			if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
+				continue;
+
+			// Check to see whether the adapter supports Direct3D 12
+			result = D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&mDevice));
+			if (SUCCEEDED(result))
+				break;
+		}
+	}
+	else
+	{
+		// Fall back to the older method that may not get the fastest GPU
+		for (UINT index = 0; DXGI_ERROR_NOT_FOUND != mDXGIFactory->EnumAdapters1(index, &adapter); ++index)
+		{
+			DXGI_ADAPTER_DESC1 desc;
+			adapter->GetDesc1(&desc);
+
+			// We don't want software renderers
+			if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
+				continue;
+
+			// Check to see whether the adapter supports Direct3D 12
+			result = D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&mDevice));
+			if (SUCCEEDED(result))
+				break;
+		}
+	}
+
+	// Check if we managed to obtain a device
+	FatalErrorIfFailed(result);
+
+#ifdef JPH_DEBUG
+	// Enable breaking on errors
+	ComPtr<ID3D12InfoQueue> info_queue;
+	if (SUCCEEDED(mDevice.As(&info_queue)))
+	{
+		info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_CORRUPTION, TRUE);
+		info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_ERROR, TRUE);
+		info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_WARNING, TRUE);
+
+		// Disable an error that triggers on Windows 11 with a hybrid graphic system
+		// See: https://stackoverflow.com/questions/69805245/directx-12-application-is-crashing-in-windows-11
+		D3D12_MESSAGE_ID hide[] =
+		{
+			D3D12_MESSAGE_ID_RESOURCE_BARRIER_MISMATCHING_COMMAND_LIST_TYPE,
+		};
+		D3D12_INFO_QUEUE_FILTER filter = { };
+		filter.DenyList.NumIDs = static_cast<UINT>( std::size( hide ) );
+		filter.DenyList.pIDList = hide;
+		info_queue->AddStorageFilterEntries( &filter );
+	}
+#endif // JPH_DEBUG
+
+	// Disable full screen transitions
+	FatalErrorIfFailed(mDXGIFactory->MakeWindowAssociation(mhWnd, DXGI_MWA_NO_ALT_ENTER));
+
+	// Create heaps
+	mRTVHeap.Init(mDevice.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_RTV, D3D12_DESCRIPTOR_HEAP_FLAG_NONE, 2);
+	mDSVHeap.Init(mDevice.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_DSV, D3D12_DESCRIPTOR_HEAP_FLAG_NONE, 4);
+	mSRVHeap.Init(mDevice.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE, 128);
+
+	// Create a command queue
+	D3D12_COMMAND_QUEUE_DESC queue_desc = {};
+	queue_desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
+	queue_desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
+	FatalErrorIfFailed(mDevice->CreateCommandQueue(&queue_desc, IID_PPV_ARGS(&mCommandQueue)));
+
+	// Create a command allocator for each frame
+	for (uint n = 0; n < cFrameCount; n++)
+		FatalErrorIfFailed(mDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&mCommandAllocators[n])));
+
+	// Describe and create the swap chain
+	DXGI_SWAP_CHAIN_DESC swap_chain_desc = {};
+	swap_chain_desc.BufferCount = cFrameCount;
+	swap_chain_desc.BufferDesc.Width = mWindowWidth;
+	swap_chain_desc.BufferDesc.Height = mWindowHeight;
+	swap_chain_desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
+	swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
+	swap_chain_desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
+	swap_chain_desc.OutputWindow = mhWnd;
+	swap_chain_desc.SampleDesc.Count = 1;
+	swap_chain_desc.Windowed = TRUE;
+
+	ComPtr<IDXGISwapChain> swap_chain;
+	FatalErrorIfFailed(mDXGIFactory->CreateSwapChain(mCommandQueue.Get(), &swap_chain_desc, &swap_chain));
+	FatalErrorIfFailed(swap_chain.As(&mSwapChain));
+	mFrameIndex = mSwapChain->GetCurrentBackBufferIndex();
+
+	CreateRenderTargets();
+
+	CreateDepthBuffer();
+
+	// Create a root signature suitable for all our shaders
+	D3D12_ROOT_PARAMETER params[3] = {};
+
+	// Mapping a constant buffer to slot 0 for the vertex shader
+	params[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;
+	params[0].Descriptor.ShaderRegister = 0;
+	params[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;
+
+	// Mapping a constant buffer to slot 1 in the pixel shader
+	params[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;
+	params[1].Descriptor.ShaderRegister = 1;
+	params[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
+
+	// Mapping a texture to slot 2 in the pixel shader
+	D3D12_DESCRIPTOR_RANGE range = {};
+	range.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
+	range.BaseShaderRegister = 2;
+	range.NumDescriptors = 1;
+	params[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
+	params[2].DescriptorTable.NumDescriptorRanges = 1;
+	params[2].DescriptorTable.pDescriptorRanges = &range;
+	params[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
+
+	D3D12_STATIC_SAMPLER_DESC samplers[3] = {};
+
+	// Sampler 0: Non-wrapping linear filtering
+	samplers[0].Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR;
+	samplers[0].AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
+	samplers[0].AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
+	samplers[0].AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
+	samplers[0].MipLODBias = 0.0f;
+	samplers[0].MaxAnisotropy = 1;
+	samplers[0].ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS;
+	samplers[0].BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK;
+	samplers[0].MinLOD = 0.0f;
+	samplers[0].MaxLOD = D3D12_FLOAT32_MAX;
+	samplers[0].ShaderRegister = 0;
+	samplers[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
+
+	// Sampler 1: Wrapping and linear filtering
+	samplers[1] = samplers[0];
+	samplers[1].AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
+	samplers[1].AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
+	samplers[1].AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
+	samplers[1].ShaderRegister = 1;
+
+	// Sampler 2: Point filtering, using SampleCmp mode to compare if sampled value >= reference value (for shadows)
+	samplers[2] = samplers[0];
+	samplers[2].Filter = D3D12_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT;
+	samplers[2].ComparisonFunc = D3D12_COMPARISON_FUNC_GREATER_EQUAL;
+	samplers[2].ShaderRegister = 2;
+
+	D3D12_ROOT_SIGNATURE_DESC root_signature_desc = {};
+	root_signature_desc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
+	root_signature_desc.NumParameters = ARRAYSIZE(params);
+	root_signature_desc.pParameters = params;
+	root_signature_desc.NumStaticSamplers = ARRAYSIZE(samplers);
+	root_signature_desc.pStaticSamplers = samplers;
+
+	ComPtr<ID3DBlob> signature;
+	ComPtr<ID3DBlob> error;
+	FatalErrorIfFailed(D3D12SerializeRootSignature(&root_signature_desc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error));
+	FatalErrorIfFailed(mDevice->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&mRootSignature)));
+
+	// Create the command list
+	FatalErrorIfFailed(mDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, mCommandAllocators[mFrameIndex].Get(), nullptr, IID_PPV_ARGS(&mCommandList)));
+
+	// Command lists are created in the recording state, but there is nothing to record yet. The main loop expects it to be closed, so close it now
+	FatalErrorIfFailed(mCommandList->Close());
+
+	// Create synchronization object
+	FatalErrorIfFailed(mDevice->CreateFence(mFenceValues[mFrameIndex], D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)));
+
+	// Increment fence value so we don't skip waiting the first time a command list is executed
+	mFenceValues[mFrameIndex]++;
+
+	// Create an event handle to use for frame synchronization
+	mFenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
+	if (mFenceEvent == nullptr)
+		FatalErrorIfFailed(HRESULT_FROM_WIN32(GetLastError()));
+
+	// Initialize the queue used to upload resources to the GPU
+	mUploadQueue.Initialize(mDevice.Get());
+
+	// Create constant buffer. One per frame to avoid overwriting the constant buffer while the GPU is still using it.
+	for (uint n = 0; n < cFrameCount; ++n)
+	{
+		mVertexShaderConstantBufferProjection[n] = CreateConstantBuffer(sizeof(VertexShaderConstantBuffer));
+		mVertexShaderConstantBufferOrtho[n] = CreateConstantBuffer(sizeof(VertexShaderConstantBuffer));
+		mPixelShaderConstantBuffer[n] = CreateConstantBuffer(sizeof(PixelShaderConstantBuffer));
+	}
+
+	// Create depth only texture (no color buffer, as seen from light)
+	mShadowMap = new TextureDX12(this, cShadowMapSize, cShadowMapSize);
+}
+
+void RendererDX12::OnWindowResize()
+{
+	Renderer::OnWindowResize();
+
+	// Wait for the previous frame to be rendered
+	WaitForGpu();
+
+	// Free the render targets and views to allow resizing the swap chain
+	for (uint n = 0; n < cFrameCount; ++n)
+	{
+		mRTVHeap.Free(mRenderTargetViews[n]);
+		mRenderTargets[n].Reset();
+	}
+
+	// Resize the swap chain buffers
+	FatalErrorIfFailed(mSwapChain->ResizeBuffers(cFrameCount, mWindowWidth, mWindowHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0));
+
+	// Back buffer index may have changed after the resize (it always seems to go to 0 again)
+	mFrameIndex = mSwapChain->GetCurrentBackBufferIndex();
+
+	// Since we may have switched frame index and we know everything is done, we need to update the fence value for our other frame as completed
+	for (uint n = 0; n < cFrameCount; ++n)
+		if (mFrameIndex != n)
+			mFenceValues[n] = mFence->GetCompletedValue();
+
+	// Recreate render targets
+	CreateRenderTargets();
+
+	// Recreate depth buffer
+	CreateDepthBuffer();
+}
+
+void RendererDX12::BeginFrame(const CameraState &inCamera, float inWorldScale)
+{
+	JPH_PROFILE_FUNCTION();
+
+	Renderer::BeginFrame(inCamera, inWorldScale);
+
+	// Reset command allocator
+	FatalErrorIfFailed(mCommandAllocators[mFrameIndex]->Reset());
+
+	// Reset command list
+	FatalErrorIfFailed(mCommandList->Reset(mCommandAllocators[mFrameIndex].Get(), nullptr));
+
+	// Set root signature
+	mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
+
+	// Set SRV heap
+	ID3D12DescriptorHeap *heaps[] = { mSRVHeap.Get() };
+	mCommandList->SetDescriptorHeaps(_countof(heaps), heaps);
+
+	// Indicate that the back buffer will be used as a render target.
+	D3D12_RESOURCE_BARRIER barrier;
+	barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
+	barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
+	barrier.Transition.pResource = mRenderTargets[mFrameIndex].Get();
+	barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
+	barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;
+	barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
+	mCommandList->ResourceBarrier(1, &barrier);
+
+	// Clear the back buffer.
+	const float blue[] = { 0.098f, 0.098f, 0.439f, 1.000f };
+	mCommandList->ClearRenderTargetView(mRenderTargetViews[mFrameIndex], blue, 0, nullptr);
+	mCommandList->ClearDepthStencilView(mDepthStencilView, D3D12_CLEAR_FLAG_DEPTH, 0.0f, 0, 0, nullptr);
+
+	// Set constants for vertex shader in projection mode
+	VertexShaderConstantBuffer *vs = mVertexShaderConstantBufferProjection[mFrameIndex]->Map<VertexShaderConstantBuffer>();
+	*vs = mVSBuffer;
+	mVertexShaderConstantBufferProjection[mFrameIndex]->Unmap();
+
+	// Set constants for vertex shader in ortho mode
+	vs = mVertexShaderConstantBufferOrtho[mFrameIndex]->Map<VertexShaderConstantBuffer>();
+	*vs = mVSBufferOrtho;
+	mVertexShaderConstantBufferOrtho[mFrameIndex]->Unmap();
+
+	// Switch to 3d projection mode
+	SetProjectionMode();
+
+	// Set constants for pixel shader
+	PixelShaderConstantBuffer *ps = mPixelShaderConstantBuffer[mFrameIndex]->Map<PixelShaderConstantBuffer>();
+	*ps = mPSBuffer;
+	mPixelShaderConstantBuffer[mFrameIndex]->Unmap();
+
+	// Set the pixel shader constant buffer data.
+	mPixelShaderConstantBuffer[mFrameIndex]->Bind(1);
+
+	// Start drawing the shadow pass
+	mShadowMap->SetAsRenderTarget(true);
+}
+
+void RendererDX12::EndShadowPass()
+{
+	JPH_PROFILE_FUNCTION();
+
+	// Finish drawing the shadow pass
+	mShadowMap->SetAsRenderTarget(false);
+
+	// Set the main back buffer as render target
+	mCommandList->OMSetRenderTargets(1, &mRenderTargetViews[mFrameIndex], FALSE, &mDepthStencilView);
+
+	// Set viewport
+	D3D12_VIEWPORT viewport = { 0.0f, 0.0f, static_cast<float>(mWindowWidth), static_cast<float>(mWindowHeight), 0.0f, 1.0f };
+	mCommandList->RSSetViewports(1, &viewport);
+
+	// Set scissor rect
+	D3D12_RECT scissor_rect = { 0, 0, static_cast<LONG>(mWindowWidth), static_cast<LONG>(mWindowHeight) };
+	mCommandList->RSSetScissorRects(1, &scissor_rect);
+}
+
+void RendererDX12::EndFrame()
+{
+	JPH_PROFILE_FUNCTION();
+
+	Renderer::EndFrame();
+
+	// Indicate that the back buffer will now be used to present.
+	D3D12_RESOURCE_BARRIER barrier;
+	barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
+	barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
+	barrier.Transition.pResource = mRenderTargets[mFrameIndex].Get();
+	barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
+	barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
+	barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
+	mCommandList->ResourceBarrier(1, &barrier);
+
+	// Close the command list
+	FatalErrorIfFailed(mCommandList->Close());
+
+	// Execute the command list
+	ID3D12CommandList* command_lists[] = { mCommandList.Get() };
+	mCommandQueue->ExecuteCommandLists(_countof(command_lists), command_lists);
+
+	// Present the frame
+	FatalErrorIfFailed(mSwapChain->Present(1, 0));
+
+	// Schedule a Signal command in the queue
+	UINT64 current_fence_value = mFenceValues[mFrameIndex];
+	FatalErrorIfFailed(mCommandQueue->Signal(mFence.Get(), current_fence_value));
+
+	// Update the frame index
+	mFrameIndex = mSwapChain->GetCurrentBackBufferIndex();
+
+	// If the next frame is not ready to be rendered yet, wait until it is ready
+	UINT64 completed_value = mFence->GetCompletedValue();
+	if (completed_value < mFenceValues[mFrameIndex])
+	{
+		FatalErrorIfFailed(mFence->SetEventOnCompletion(mFenceValues[mFrameIndex], mFenceEvent));
+		WaitForSingleObjectEx(mFenceEvent, INFINITE, FALSE);
+	}
+
+	// Release all used resources
+	mDelayReleased[mFrameIndex].clear();
+
+	// Anything that's not used yet can be removed, delayed objects are now available
+	mResourceCache.clear();
+	mDelayCached[mFrameIndex].swap(mResourceCache);
+
+	// Set the fence value for the next frame.
+	mFenceValues[mFrameIndex] = current_fence_value + 1;
+}
+
+
+void RendererDX12::SetProjectionMode()
+{
+	JPH_ASSERT(mInFrame);
+
+	mVertexShaderConstantBufferProjection[mFrameIndex]->Bind(0);
+}
+
+void RendererDX12::SetOrthoMode()
+{
+	JPH_ASSERT(mInFrame);
+
+	mVertexShaderConstantBufferOrtho[mFrameIndex]->Bind(0);
+}
+
+Ref<Texture> RendererDX12::CreateTexture(const Surface *inSurface)
+{
+	return new TextureDX12(this, inSurface);
+}
+
+Ref<VertexShader> RendererDX12::CreateVertexShader(const char *inFileName)
+{
+	UINT flags = D3DCOMPILE_ENABLE_STRICTNESS;
+#ifdef JPH_DEBUG
+	flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
+#endif
+
+	const D3D_SHADER_MACRO defines[] =
+	{
+		{ nullptr, nullptr }
+	};
+
+	// Read shader source file
+	Array<uint8> data = ReadData((String(inFileName) + ".hlsl").c_str());
+
+	// Compile source
+	ComPtr<ID3DBlob> shader_blob, error_blob;
+	HRESULT hr = D3DCompile(&data[0],
+							(uint)data.size(),
+							inFileName,
+							defines,
+							D3D_COMPILE_STANDARD_FILE_INCLUDE,
+							"main",
+							"vs_5_0",
+							flags,
+							0,
+							shader_blob.GetAddressOf(),
+							error_blob.GetAddressOf());
+	if (FAILED(hr))
+	{
+		// Throw error if compilation failed
+		if (error_blob)
+			OutputDebugStringA((const char *)error_blob->GetBufferPointer());
+		FatalError("Failed to compile vertex shader");
+	}
+
+	return new VertexShaderDX12(shader_blob);
+}
+
+Ref<PixelShader> RendererDX12::CreatePixelShader(const char *inFileName)
+{
+	UINT flags = D3DCOMPILE_ENABLE_STRICTNESS;
+#ifdef JPH_DEBUG
+	flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
+#endif
+
+	const D3D_SHADER_MACRO defines[] =
+	{
+		{ nullptr, nullptr }
+	};
+
+	// Read shader source file
+	Array<uint8> data = ReadData((String(inFileName) + ".hlsl").c_str());
+
+	// Compile source
+	ComPtr<ID3DBlob> shader_blob, error_blob;
+	HRESULT hr = D3DCompile(&data[0],
+							(uint)data.size(),
+							inFileName,
+							defines,
+							D3D_COMPILE_STANDARD_FILE_INCLUDE,
+							"main",
+							"ps_5_0",
+							flags,
+							0,
+							shader_blob.GetAddressOf(),
+							error_blob.GetAddressOf());
+	if (FAILED(hr))
+	{
+		// Throw error if compilation failed
+		if (error_blob)
+			OutputDebugStringA((const char *)error_blob->GetBufferPointer());
+		FatalError("Failed to compile pixel shader");
+	}
+
+	return new PixelShaderDX12(shader_blob);
+}
+
+unique_ptr<ConstantBufferDX12> RendererDX12::CreateConstantBuffer(uint inBufferSize)
+{
+	return make_unique<ConstantBufferDX12>(this, inBufferSize);
+}
+
+unique_ptr<PipelineState> RendererDX12::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<PipelineStateDX12>(this, static_cast<const VertexShaderDX12 *>(inVertexShader), inInputDescription, inInputDescriptionCount, static_cast<const PixelShaderDX12 *>(inPixelShader), inDrawPass, inFillMode, inTopology, inDepthTest, inBlendMode, inCullMode);
+}
+
+RenderPrimitive *RendererDX12::CreateRenderPrimitive(PipelineState::ETopology inType)
+{
+	return new RenderPrimitiveDX12(this, inType);
+}
+
+RenderInstances *RendererDX12::CreateRenderInstances()
+{
+	return new RenderInstancesDX12(this);
+}
+
+ComPtr<ID3D12Resource> RendererDX12::CreateD3DResource(D3D12_HEAP_TYPE inHeapType, D3D12_RESOURCE_STATES inResourceState, uint64 inSize)
+{
+	// Create a new resource
+	D3D12_RESOURCE_DESC desc;
+	desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
+	desc.Alignment = 0;
+	desc.Width = inSize;
+	desc.Height = 1;
+	desc.DepthOrArraySize = 1;
+	desc.MipLevels = 1;
+	desc.Format = DXGI_FORMAT_UNKNOWN;
+	desc.SampleDesc.Count = 1;
+	desc.SampleDesc.Quality = 0;
+	desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
+	desc.Flags = D3D12_RESOURCE_FLAG_NONE;
+
+	D3D12_HEAP_PROPERTIES heap_properties = {};
+	heap_properties.Type = inHeapType;
+	heap_properties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
+	heap_properties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
+	heap_properties.CreationNodeMask = 1;
+	heap_properties.VisibleNodeMask = 1;
+
+	ComPtr<ID3D12Resource> resource;
+	FatalErrorIfFailed(mDevice->CreateCommittedResource(&heap_properties, D3D12_HEAP_FLAG_NONE, &desc, inResourceState, nullptr, IID_PPV_ARGS(&resource)));
+	return resource;
+}
+
+void RendererDX12::CopyD3DResource(ID3D12Resource *inDest, const void *inSrc, uint64 inSize)
+{
+	// Copy data to destination buffer
+	void *data;
+	D3D12_RANGE range = { 0, 0 }; // We're not going to read
+	FatalErrorIfFailed(inDest->Map(0, &range, &data));
+	memcpy(data, inSrc, size_t(inSize));
+	inDest->Unmap(0, nullptr);
+}
+
+void RendererDX12::CopyD3DResource(ID3D12Resource *inDest, ID3D12Resource *inSrc, uint64 inSize)
+{
+	// Start a commandlist for the upload
+	ID3D12GraphicsCommandList *list = mUploadQueue.Start();
+
+	// Copy the data to the GPU
+	list->CopyBufferRegion(inDest, 0, inSrc, 0, inSize);
+
+	// Change the state of the resource to generic read
+	D3D12_RESOURCE_BARRIER barrier;
+	barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
+	barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
+	barrier.Transition.pResource = inDest;
+	barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
+	barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_GENERIC_READ;
+	barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
+	list->ResourceBarrier(1, &barrier);
+
+	// Wait for copying to finish
+	mUploadQueue.ExecuteAndWait();
+}
+
+ComPtr<ID3D12Resource> RendererDX12::CreateD3DResourceOnDefaultHeap(const void *inData, uint64 inSize)
+{
+	ComPtr<ID3D12Resource> upload = CreateD3DResourceOnUploadHeap(inSize);
+	ComPtr<ID3D12Resource> resource = CreateD3DResource(D3D12_HEAP_TYPE_DEFAULT, D3D12_RESOURCE_STATE_COMMON, inSize);
+	CopyD3DResource(upload.Get(), inData, inSize);
+	CopyD3DResource(resource.Get(), upload.Get(), inSize);
+	RecycleD3DResourceOnUploadHeap(upload.Get(), inSize);
+	return resource;
+}
+
+ComPtr<ID3D12Resource> RendererDX12::CreateD3DResourceOnUploadHeap(uint64 inSize)
+{
+	// Try cache first
+	ResourceCache::iterator i = mResourceCache.find(inSize);
+	if (i != mResourceCache.end() && !i->second.empty())
+	{
+		ComPtr<ID3D12Resource> resource = i->second.back();
+		i->second.pop_back();
+		return resource;
+	}
+
+	return CreateD3DResource(D3D12_HEAP_TYPE_UPLOAD, D3D12_RESOURCE_STATE_GENERIC_READ, inSize);
+}
+
+void RendererDX12::RecycleD3DResourceOnUploadHeap(ID3D12Resource *inResource, uint64 inSize)
+{
+	if (!mIsExiting)
+		mDelayCached[mFrameIndex][inSize].push_back(inResource);
+}
+
+void RendererDX12::RecycleD3DObject(ID3D12Object *inResource)
+{
+	if (!mIsExiting)
+		mDelayReleased[mFrameIndex].push_back(inResource);
+}

+ 111 - 0
TestFramework/Renderer/DX12/RendererDX12.h

@@ -0,0 +1,111 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Core/UnorderedMap.h>
+#include <Renderer/Renderer.h>
+#include <Renderer/DX12/CommandQueueDX12.h>
+#include <Renderer/DX12/DescriptorHeapDX12.h>
+#include <Renderer/DX12/ConstantBufferDX12.h>
+#include <Renderer/DX12/TextureDX12.h>
+
+/// DirectX 12 renderer
+class RendererDX12 : public Renderer
+{
+public:
+	/// Destructor
+	virtual							~RendererDX12() override;
+
+	// See: Renderer
+	virtual void					Initialize() 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 *inFileName) override;
+	virtual Ref<PixelShader>		CreatePixelShader(const char *inFileName) 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.GetPtr(); }
+	virtual void					OnWindowResize() override;
+
+	/// Create a constant buffer
+	unique_ptr<ConstantBufferDX12>	CreateConstantBuffer(uint inBufferSize);
+
+	/// Create a buffer on the default heap (usable for permanent buffers)
+	ComPtr<ID3D12Resource>			CreateD3DResourceOnDefaultHeap(const void *inData, uint64 inSize);
+
+	/// Create buffer on the upload heap (usable for temporary buffers).
+	ComPtr<ID3D12Resource>			CreateD3DResourceOnUploadHeap(uint64 inSize);
+
+	/// Recycle a buffer on the upload heap. This puts it back in a cache and will reuse it when it is certain the GPU is no longer referencing it.
+	void							RecycleD3DResourceOnUploadHeap(ID3D12Resource *inResource, uint64 inSize);
+
+	/// Keeps a reference to the resource until the current frame has finished
+	void							RecycleD3DObject(ID3D12Object *inResource);
+
+	/// Access to the most important DirectX structures
+	ID3D12Device *					GetDevice()							{ return mDevice.Get(); }
+	ID3D12RootSignature *			GetRootSignature()					{ return mRootSignature.Get(); }
+	ID3D12GraphicsCommandList *		GetCommandList()					{ JPH_ASSERT(mInFrame); return mCommandList.Get(); }
+	CommandQueueDX12 &				GetUploadQueue()					{ return mUploadQueue; }
+	DescriptorHeapDX12 &			GetDSVHeap()						{ return mDSVHeap; }
+	DescriptorHeapDX12 &			GetSRVHeap()						{ return mSRVHeap; }
+
+private:
+	// Wait for pending GPU work to complete
+	void							WaitForGpu();
+
+	// Create render targets and their views
+	void							CreateRenderTargets();
+
+	// Create a depth buffer for the back buffer
+	void							CreateDepthBuffer();
+
+	// Function to create a ID3D12Resource on specified heap with specified state
+	ComPtr<ID3D12Resource>			CreateD3DResource(D3D12_HEAP_TYPE inHeapType, D3D12_RESOURCE_STATES inResourceState, uint64 inSize);
+
+	// Copy CPU memory into a ID3D12Resource
+	void							CopyD3DResource(ID3D12Resource *inDest, const void *inSrc, uint64 inSize);
+
+	// Copy a CPU resource to a GPU resource
+	void							CopyD3DResource(ID3D12Resource *inDest, ID3D12Resource *inSrc, uint64 inSize);
+
+	// DirectX interfaces
+	ComPtr<IDXGIFactory4>			mDXGIFactory;
+	ComPtr<ID3D12Device>			mDevice;
+	DescriptorHeapDX12				mRTVHeap;							///< Render target view heap
+	DescriptorHeapDX12				mDSVHeap;							///< Depth stencil view heap
+	DescriptorHeapDX12				mSRVHeap;							///< Shader resource view heap
+	ComPtr<IDXGISwapChain3>			mSwapChain;
+	ComPtr<ID3D12Resource>			mRenderTargets[cFrameCount];		///< Two render targets (we're double buffering in order for the CPU to continue while the GPU is rendering)
+	D3D12_CPU_DESCRIPTOR_HANDLE		mRenderTargetViews[cFrameCount];	///< The two render views corresponding to the render targets
+	ComPtr<ID3D12Resource>			mDepthStencilBuffer;				///< The main depth buffer
+	D3D12_CPU_DESCRIPTOR_HANDLE		mDepthStencilView { 0 };			///< A view for binding the depth buffer
+	ComPtr<ID3D12CommandAllocator>	mCommandAllocators[cFrameCount];	///< Two command allocator lists (one per frame)
+	ComPtr<ID3D12CommandQueue>		mCommandQueue;						///< The command queue that will execute commands (there's only 1 since we want to finish rendering 1 frame before moving onto the next)
+	ComPtr<ID3D12GraphicsCommandList> mCommandList;						///< The command list
+	ComPtr<ID3D12RootSignature>		mRootSignature;						///< The root signature, we have a simple application so we only need 1, which is suitable for all our shaders
+	Ref<TextureDX12>				mShadowMap;							///< Used to render shadow maps
+	CommandQueueDX12				mUploadQueue;						///< Queue used to upload resources to GPU memory
+	unique_ptr<ConstantBufferDX12>	mVertexShaderConstantBufferProjection[cFrameCount];
+	unique_ptr<ConstantBufferDX12>	mVertexShaderConstantBufferOrtho[cFrameCount];
+	unique_ptr<ConstantBufferDX12>	mPixelShaderConstantBuffer[cFrameCount];
+
+	// Synchronization objects used to finish rendering and swapping before reusing a command queue
+	HANDLE							mFenceEvent;						///< Fence event to wait for the previous frame rendering to complete (in order to free 1 of the buffers)
+	ComPtr<ID3D12Fence>				mFence;								///< Fence object, used to signal the end of a frame
+	UINT64							mFenceValues[cFrameCount] = {};		///< Values that were used to signal completion of one of the two frames
+
+	using ResourceCache = UnorderedMap<uint64, Array<ComPtr<ID3D12Resource>>>;
+
+	ResourceCache					mResourceCache;						///< Cache items ready to be reused
+	ResourceCache					mDelayCached[cFrameCount];			///< List of reusable ID3D12Resources that are potentially referenced by the GPU so can be used only when the GPU finishes
+	Array<ComPtr<ID3D12Object>>		mDelayReleased[cFrameCount];		///< Objects that are potentially referenced by the GPU so can only be freed when the GPU finishes
+	bool							mIsExiting = false;					///< When exiting we don't want to add references too buffers
+};

+ 16 - 24
TestFramework/Renderer/Texture.cpp → TestFramework/Renderer/DX12/TextureDX12.cpp

@@ -1,28 +1,25 @@
 // Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
-// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
 // SPDX-License-Identifier: MIT
 
 #include <TestFramework.h>
 
-#include <Renderer/Texture.h>
+#include <Renderer/DX12/TextureDX12.h>
+#include <Renderer/DX12/RendererDX12.h>
+#include <Renderer/DX12/FatalErrorIfFailedDX12.h>
 #include <Image/BlitSurface.h>
-#include <Renderer/Renderer.h>
-#include <Renderer/FatalErrorIfFailed.h>
 
-Texture::Texture(Renderer *inRenderer, const Surface *inSurface) :
+TextureDX12::TextureDX12(RendererDX12 *inRenderer, const Surface *inSurface) :
+	Texture(inSurface->GetWidth(), inSurface->GetHeight()),
 	mRenderer(inRenderer)
 {
-	// Store dimensions
-	mWidth = inSurface->GetWidth();
-	mHeight = inSurface->GetHeight();
-
 	// Create description
 	D3D12_RESOURCE_DESC desc = {};
 	desc.MipLevels = 1;
 	ESurfaceFormat format = inSurface->GetFormat();
 	switch (format)
 	{
-	case ESurfaceFormat::A4L4:			desc.Format = DXGI_FORMAT_R8G8_UNORM;		break;
+	case ESurfaceFormat::A4L4:			desc.Format = DXGI_FORMAT_R8G8_UNORM;		format = ESurfaceFormat::A8L8;		break;
 	case ESurfaceFormat::L8:			desc.Format = DXGI_FORMAT_R8_UNORM;			break;
 	case ESurfaceFormat::A8:			desc.Format = DXGI_FORMAT_A8_UNORM;			break;
 	case ESurfaceFormat::A8L8:			desc.Format = DXGI_FORMAT_R8G8_UNORM;		break;
@@ -128,13 +125,10 @@ Texture::Texture(Renderer *inRenderer, const Surface *inSurface) :
 	inRenderer->RecycleD3DResourceOnUploadHeap(upload_resource.Get(), required_size);
 }
 
-Texture::Texture(Renderer *inRenderer, int inWidth, int inHeight) :
+TextureDX12::TextureDX12(RendererDX12 *inRenderer, int inWidth, int inHeight) :
+	Texture(inWidth, inHeight),
 	mRenderer(inRenderer)
 {
-	// Store dimensions
-	mWidth = inWidth;
-	mHeight = inHeight;
-
 	// Allocate depth stencil buffer
 	D3D12_CLEAR_VALUE clear_value = {};
 	clear_value.Format = DXGI_FORMAT_D32_FLOAT;
@@ -182,7 +176,7 @@ Texture::Texture(Renderer *inRenderer, int inWidth, int inHeight) :
 	inRenderer->GetDevice()->CreateShaderResourceView(mTexture.Get(), &srv_desc, mSRV);
 }
 
-Texture::~Texture()
+TextureDX12::~TextureDX12()
 {
 	if (mSRV.ptr != 0)
 		mRenderer->GetSRVHeap().Free(mSRV);
@@ -194,17 +188,12 @@ Texture::~Texture()
 		mRenderer->RecycleD3DObject(mTexture.Get());
 }
 
-void Texture::Bind(int inSlot) const
-{
-	mRenderer->GetCommandList()->SetGraphicsRootDescriptorTable(inSlot, mRenderer->GetSRVHeap().ConvertToGPUHandle(mSRV));
-}
-
-void Texture::ClearRenderTarget()
+void TextureDX12::Bind() const
 {
-	mRenderer->GetCommandList()->ClearDepthStencilView(mDSV, D3D12_CLEAR_FLAG_DEPTH, 0, 0, 0, nullptr);
+	mRenderer->GetCommandList()->SetGraphicsRootDescriptorTable(2 /* All shaders use slot 2 to bind their texture */, mRenderer->GetSRVHeap().ConvertToGPUHandle(mSRV));
 }
 
-void Texture::SetAsRenderTarget(bool inSet) const
+void TextureDX12::SetAsRenderTarget(bool inSet) const
 {
 	if (inSet)
 	{
@@ -228,6 +217,9 @@ void Texture::SetAsRenderTarget(bool inSet) const
 		// Set scissor rect
 		D3D12_RECT scissor_rect = { 0, 0, static_cast<LONG>(mWidth), static_cast<LONG>(mHeight) };
 		mRenderer->GetCommandList()->RSSetScissorRects(1, &scissor_rect);
+
+		// Clear the render target
+		mRenderer->GetCommandList()->ClearDepthStencilView(mDSV, D3D12_CLEAR_FLAG_DEPTH, 0, 0, 0, nullptr);
 	}
 	else
 	{

+ 32 - 0
TestFramework/Renderer/DX12/TextureDX12.h

@@ -0,0 +1,32 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/Texture.h>
+
+class RendererDX12;
+
+class TextureDX12 : public Texture
+{
+public:
+	/// Constructor, called by Renderer::CreateTextureDX12
+										TextureDX12(RendererDX12 *inRenderer, const Surface *inSurface);	// Create a normal TextureDX12
+										TextureDX12(RendererDX12 *inRenderer, int inWidth, int inHeight);	// Create a render target (depth only)
+	virtual								~TextureDX12() override;
+
+	/// Bind texture to the pixel shader
+	virtual void						Bind() const override;
+
+	/// Activate this texture as the current render target, used by RendererDX12::BeginFrame, EndShadowPass
+	void								SetAsRenderTarget(bool inSet) const;
+
+private:
+	RendererDX12 *						mRenderer;
+
+	ComPtr<ID3D12Resource>				mTexture;				///< The texture data
+
+	D3D12_CPU_DESCRIPTOR_HANDLE			mSRV { 0 };				///< Shader resource view to bind as texture
+	D3D12_CPU_DESCRIPTOR_HANDLE			mDSV { 0 };				///< Depth shader view to bind as render target
+};

+ 17 - 0
TestFramework/Renderer/DX12/VertexShaderDX12.h

@@ -0,0 +1,17 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/VertexShader.h>
+
+/// Vertex shader handle for DirectX
+class VertexShaderDX12 : public VertexShader
+{
+public:
+	/// Constructor
+							VertexShaderDX12(ComPtr<ID3DBlob> inShader)		: mShader(inShader) { }
+
+	ComPtr<ID3DBlob>		mShader;										///< The compiled shader
+};

+ 45 - 65
TestFramework/Renderer/DebugRendererImp.cpp

@@ -6,8 +6,6 @@
 
 #include <Renderer/DebugRendererImp.h>
 #include <Renderer/Renderer.h>
-#include <Renderer/RenderPrimitive.h>
-#include <Renderer/Texture.h>
 #include <Renderer/Font.h>
 
 #ifndef JPH_DEBUG_RENDERER
@@ -22,55 +20,46 @@ DebugRendererImp::DebugRendererImp(Renderer *inRenderer, const Font *inFont) :
 	mFont(inFont)
 {
 	// Create input layout for lines
-	const D3D12_INPUT_ELEMENT_DESC line_vertex_desc[] =
+	const PipelineState::EInputDescription line_vertex_desc[] =
 	{
-		{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
-		{ "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
+		PipelineState::EInputDescription::Position,
+		PipelineState::EInputDescription::Color
 	};
 
 	// Lines
-	ComPtr<ID3DBlob> vtx_line = mRenderer->CreateVertexShader("Assets/Shaders/LineVertexShader.hlsl");
-	ComPtr<ID3DBlob> pix_line = mRenderer->CreatePixelShader("Assets/Shaders/LinePixelShader.hlsl");
-	mLineState = mRenderer->CreatePipelineState(vtx_line.Get(), line_vertex_desc, ARRAYSIZE(line_vertex_desc), pix_line.Get(), D3D12_FILL_MODE_SOLID, D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE, PipelineState::EDepthTest::On, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
+	Ref<VertexShader> vtx_line = mRenderer->CreateVertexShader("Assets/Shaders/LineVertexShader");
+	Ref<PixelShader> pix_line = mRenderer->CreatePixelShader("Assets/Shaders/LinePixelShader");
+	mLineState = mRenderer->CreatePipelineState(vtx_line, line_vertex_desc, std::size(line_vertex_desc), pix_line, PipelineState::EDrawPass::Normal, PipelineState::EFillMode::Solid, PipelineState::ETopology::Line, PipelineState::EDepthTest::On, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
 
 	// Create input layout for triangles
-	const D3D12_INPUT_ELEMENT_DESC triangles_vertex_desc[] =
+	const PipelineState::EInputDescription triangles_vertex_desc[] =
 	{
-		{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
-		{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
-		{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
-		{ "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, 32, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
-		{ "INSTANCE_TRANSFORM", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 },
-		{ "INSTANCE_TRANSFORM", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 16, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 },
-		{ "INSTANCE_TRANSFORM", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 32, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 },
-		{ "INSTANCE_TRANSFORM", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 48, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 },
-		{ "INSTANCE_INV_TRANSFORM", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 64, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 },
-		{ "INSTANCE_INV_TRANSFORM", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 80, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 },
-		{ "INSTANCE_INV_TRANSFORM", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 96, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 },
-		{ "INSTANCE_INV_TRANSFORM", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 112, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 },
-		{ "INSTANCE_COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 1, 128, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 },
+		PipelineState::EInputDescription::Position,
+		PipelineState::EInputDescription::Normal,
+		PipelineState::EInputDescription::TexCoord,
+		PipelineState::EInputDescription::Color,
+		PipelineState::EInputDescription::InstanceTransform,
+		PipelineState::EInputDescription::InstanceInvTransform,
+		PipelineState::EInputDescription::InstanceColor
 	};
 
 	// Triangles
-	ComPtr<ID3DBlob> vtx_triangle = mRenderer->CreateVertexShader("Assets/Shaders/TriangleVertexShader.hlsl");
-	ComPtr<ID3DBlob> pix_triangle  = mRenderer->CreatePixelShader("Assets/Shaders/TrianglePixelShader.hlsl");
-	mTriangleStateBF = mRenderer->CreatePipelineState(vtx_triangle.Get(), triangles_vertex_desc, ARRAYSIZE(triangles_vertex_desc), pix_triangle.Get(), D3D12_FILL_MODE_SOLID, D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, PipelineState::EDepthTest::On, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
-	mTriangleStateFF = mRenderer->CreatePipelineState(vtx_triangle.Get(), triangles_vertex_desc, ARRAYSIZE(triangles_vertex_desc), pix_triangle.Get(), D3D12_FILL_MODE_SOLID, D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, PipelineState::EDepthTest::On, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::FrontFace);
-	mTriangleStateWire = mRenderer->CreatePipelineState(vtx_triangle.Get(), triangles_vertex_desc, ARRAYSIZE(triangles_vertex_desc), pix_triangle.Get(), D3D12_FILL_MODE_WIREFRAME, D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, PipelineState::EDepthTest::On, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
+	Ref<VertexShader> vtx_triangle = mRenderer->CreateVertexShader("Assets/Shaders/TriangleVertexShader");
+	Ref<PixelShader> pix_triangle  = mRenderer->CreatePixelShader("Assets/Shaders/TrianglePixelShader");
+	mTriangleStateBF = mRenderer->CreatePipelineState(vtx_triangle, triangles_vertex_desc, std::size(triangles_vertex_desc), pix_triangle, PipelineState::EDrawPass::Normal, PipelineState::EFillMode::Solid, PipelineState::ETopology::Triangle, PipelineState::EDepthTest::On, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
+	mTriangleStateFF = mRenderer->CreatePipelineState(vtx_triangle, triangles_vertex_desc, std::size(triangles_vertex_desc), pix_triangle, PipelineState::EDrawPass::Normal, PipelineState::EFillMode::Solid, PipelineState::ETopology::Triangle, PipelineState::EDepthTest::On, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::FrontFace);
+	mTriangleStateWire = mRenderer->CreatePipelineState(vtx_triangle, triangles_vertex_desc, std::size(triangles_vertex_desc), pix_triangle, PipelineState::EDrawPass::Normal, PipelineState::EFillMode::Wireframe, PipelineState::ETopology::Triangle, PipelineState::EDepthTest::On, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
 
 	// Shadow pass
-	ComPtr<ID3DBlob> vtx_shadow = mRenderer->CreateVertexShader("Assets/Shaders/TriangleDepthVertexShader.hlsl");
-	ComPtr<ID3DBlob> pix_shadow = mRenderer->CreatePixelShader("Assets/Shaders/TriangleDepthPixelShader.hlsl");
-	mShadowStateBF = mRenderer->CreatePipelineState(vtx_shadow.Get(), triangles_vertex_desc, ARRAYSIZE(triangles_vertex_desc), pix_shadow.Get(), D3D12_FILL_MODE_SOLID, D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, PipelineState::EDepthTest::On, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
-	mShadowStateFF = mRenderer->CreatePipelineState(vtx_shadow.Get(), triangles_vertex_desc, ARRAYSIZE(triangles_vertex_desc), pix_shadow.Get(), D3D12_FILL_MODE_SOLID, D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, PipelineState::EDepthTest::On, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::FrontFace);
-	mShadowStateWire = mRenderer->CreatePipelineState(vtx_shadow.Get(), triangles_vertex_desc, ARRAYSIZE(triangles_vertex_desc), pix_shadow.Get(), D3D12_FILL_MODE_WIREFRAME, D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, PipelineState::EDepthTest::On, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
-
-	// Create depth only texture (no color buffer, as seen from light)
-	mDepthTexture = mRenderer->CreateRenderTarget(4096, 4096);
+	Ref<VertexShader> vtx_shadow = mRenderer->CreateVertexShader("Assets/Shaders/TriangleDepthVertexShader");
+	Ref<PixelShader> pix_shadow = mRenderer->CreatePixelShader("Assets/Shaders/TriangleDepthPixelShader");
+	mShadowStateBF = mRenderer->CreatePipelineState(vtx_shadow, triangles_vertex_desc, std::size(triangles_vertex_desc), pix_shadow, PipelineState::EDrawPass::Shadow, PipelineState::EFillMode::Solid, PipelineState::ETopology::Triangle, PipelineState::EDepthTest::On, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
+	mShadowStateFF = mRenderer->CreatePipelineState(vtx_shadow, triangles_vertex_desc, std::size(triangles_vertex_desc), pix_shadow, PipelineState::EDrawPass::Shadow, PipelineState::EFillMode::Solid, PipelineState::ETopology::Triangle, PipelineState::EDepthTest::On, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::FrontFace);
+	mShadowStateWire = mRenderer->CreatePipelineState(vtx_shadow, triangles_vertex_desc, std::size(triangles_vertex_desc), pix_shadow, PipelineState::EDrawPass::Shadow, PipelineState::EFillMode::Wireframe, PipelineState::ETopology::Triangle, PipelineState::EDepthTest::On, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
 
 	// Create instances buffer
 	for (uint n = 0; n < Renderer::cFrameCount; ++n)
-		mInstancesBuffer[n] = new RenderInstances(mRenderer);
+		mInstancesBuffer[n] = mRenderer->CreateRenderInstances();
 
 	// Create empty batch
 	Vertex empty_vertex { Float3(0, 0, 0), Float3(1, 0, 0), Float2(0, 0), Color::sWhite };
@@ -100,7 +89,7 @@ DebugRenderer::Batch DebugRendererImp::CreateTriangleBatch(const Triangle *inTri
 	if (inTriangles == nullptr || inTriangleCount == 0)
 		return mEmptyBatch;
 
-	BatchImpl *primitive = new BatchImpl(mRenderer, D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+	RenderPrimitive *primitive = mRenderer->CreateRenderPrimitive(PipelineState::ETopology::Triangle);
 	primitive->CreateVertexBuffer(3 * inTriangleCount, sizeof(Vertex), inTriangles);
 
 	return primitive;
@@ -111,7 +100,7 @@ DebugRenderer::Batch DebugRendererImp::CreateTriangleBatch(const Vertex *inVerti
 	if (inVertices == nullptr || inVertexCount == 0 || inIndices == nullptr || inIndexCount == 0)
 		return mEmptyBatch;
 
-	BatchImpl *primitive = new BatchImpl(mRenderer, D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+	RenderPrimitive *primitive = mRenderer->CreateRenderPrimitive(PipelineState::ETopology::Triangle);
 	primitive->CreateVertexBuffer(inVertexCount, sizeof(Vertex), inVertices);
 	primitive->CreateIndexBuffer(inIndexCount, inIndices);
 
@@ -158,16 +147,14 @@ void DebugRendererImp::FinalizePrimitive()
 
 	if (mLockedPrimitive != nullptr)
 	{
-		BatchImpl *primitive = static_cast<BatchImpl *>(mLockedPrimitive.GetPtr());
-
 		// Unlock the primitive
-		primitive->UnlockVertexBuffer();
+		mLockedPrimitive->UnlockVertexBuffer();
 
 		// Set number of indices to draw
-		primitive->SetNumVtxToDraw(int(mLockedVertices - mLockedVerticesStart));
+		mLockedPrimitive->SetNumVtxToDraw(int(mLockedVertices - mLockedVerticesStart));
 
 		// Add to draw list
-		mTempPrimitives[new Geometry(mLockedPrimitive, mLockedPrimitiveBounds)].mInstances.push_back({ Mat44::sIdentity(), Mat44::sIdentity(), Color::sWhite, mLockedPrimitiveBounds, 1.0f });
+		mTempPrimitives[new Geometry(mLockedPrimitive.GetPtr(), mLockedPrimitiveBounds)].mInstances.push_back({ Mat44::sIdentity(), Mat44::sIdentity(), Color::sWhite, mLockedPrimitiveBounds, 1.0f });
 		++mNumInstances;
 
 		// Clear pointers
@@ -189,12 +176,11 @@ void DebugRendererImp::EnsurePrimitiveSpace(int inVtxSize)
 		FinalizePrimitive();
 
 		// Create new
-		BatchImpl *primitive = new BatchImpl(mRenderer, D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
-		primitive->CreateVertexBuffer(cVertexBufferSize, sizeof(Vertex));
-		mLockedPrimitive = primitive;
-
+		mLockedPrimitive = mRenderer->CreateRenderPrimitive(PipelineState::ETopology::Triangle);
+		mLockedPrimitive->CreateVertexBuffer(cVertexBufferSize, sizeof(Vertex));
+		
 		// Lock buffers
-		mLockedVerticesStart = mLockedVertices = (Vertex *)primitive->LockVertexBuffer();
+		mLockedVerticesStart = mLockedVertices = (Vertex *)mLockedPrimitive->LockVertexBuffer();
 		mLockedVerticesEnd = mLockedVertices + cVertexBufferSize;
 	}
 }
@@ -240,7 +226,7 @@ void DebugRendererImp::DrawInstances(const Geometry *inGeometry, const Array<int
 			int start_idx = next_start_idx;
 			next_start_idx = inStartIdx[lod + 1];
 			int num_instances = next_start_idx - start_idx;
-			instances_buffer->Draw(static_cast<BatchImpl *>(geometry_lods[lod].mTriangleBatch.GetPtr()), start_idx, num_instances);
+			instances_buffer->Draw(static_cast<RenderPrimitive *>(geometry_lods[lod].mTriangleBatch.GetPtr()), start_idx, num_instances);
 		}
 	}
 }
@@ -264,17 +250,17 @@ void DebugRendererImp::DrawLines()
 	// Draw the lines
 	if (!mLines.empty())
 	{
-		RenderPrimitive primitive(mRenderer, D3D_PRIMITIVE_TOPOLOGY_LINELIST);
-		primitive.CreateVertexBuffer((int)mLines.size() * 2, sizeof(Line) / 2);
-		void *data = primitive.LockVertexBuffer();
+		Ref<RenderPrimitive> primitive = mRenderer->CreateRenderPrimitive(PipelineState::ETopology::Line);
+		primitive->CreateVertexBuffer((int)mLines.size() * 2, sizeof(Line) / 2);
+		void *data = primitive->LockVertexBuffer();
 		memcpy(data, &mLines[0], mLines.size() * sizeof(Line));
-		primitive.UnlockVertexBuffer();
+		primitive->UnlockVertexBuffer();
 		mLineState->Activate();
-		primitive.Draw();
+		primitive->Draw();
 	}
 }
 
-void DebugRendererImp::DrawTriangles()
+void DebugRendererImp::DrawShadowPass()
 {
 	JPH_PROFILE_FUNCTION();
 
@@ -283,12 +269,6 @@ void DebugRendererImp::DrawTriangles()
 	// Finish the last primitive
 	FinalizePrimitive();
 
-	// Render to shadow map texture first
-	mRenderer->SetRenderTarget(mDepthTexture);
-
-	// Clear the shadow map texture to max depth
-	mDepthTexture->ClearRenderTarget();
-
 	// Get the camera and light frustum for culling
 	Vec3 camera_pos(mRenderer->GetCameraState().mPos - mRenderer->GetBaseOffset());
 	const Frustum &camera_frustum = mRenderer->GetCameraFrustum();
@@ -411,12 +391,12 @@ void DebugRendererImp::DrawTriangles()
 		for (InstanceMap::value_type &v : mWireframePrimitives)
 			DrawInstances(v.first, v.second.mLightStartIdx);
 	}
+}
 
-	// Switch to the main render target
-	mRenderer->SetRenderTarget(nullptr);
-
+void DebugRendererImp::DrawTriangles()
+{
 	// Bind the shadow map texture
-	mDepthTexture->Bind(2);
+	mRenderer->GetShadowMap()->Bind();
 
 	if (!mPrimitives.empty() || !mTempPrimitives.empty())
 	{

+ 5 - 18
TestFramework/Renderer/DebugRendererImp.h

@@ -16,8 +16,7 @@
 	#undef JPH_DEBUG_RENDERER_EXPORT
 #endif
 
-#include <Renderer/RenderPrimitive.h>
-#include <Renderer/RenderInstances.h>
+#include <Renderer/Renderer.h>
 #include <Jolt/Core/Mutex.h>
 #include <Jolt/Core/UnorderedMap.h>
 
@@ -41,6 +40,9 @@ public:
 	virtual void						DrawGeometry(RMat44Arg inModelMatrix, const AABox &inWorldSpaceBounds, float inLODScaleSq, ColorArg inModelColor, const GeometryRef &inGeometry, ECullMode inCullMode, ECastShadow inCastShadow, EDrawMode inDrawMode) override;
 	virtual void						DrawText3D(RVec3Arg inPosition, const string_view &inString, ColorArg inColor, float inHeight) override;
 
+	/// Draw all primitives from the light source
+	void								DrawShadowPass();
+
 	/// Draw all primitives that were added
 	void								Draw();
 
@@ -58,18 +60,6 @@ private:
 	void								ClearTriangles();
 	void								ClearTexts();
 
-	/// Implementation specific batch object
-	class BatchImpl : public RefTargetVirtual, public RenderPrimitive
-	{
-	public:
-		JPH_OVERRIDE_NEW_DELETE
-
-										BatchImpl(Renderer *inRenderer, D3D_PRIMITIVE_TOPOLOGY inType) : RenderPrimitive(inRenderer, inType) { }
-
-		virtual void					AddRef() override			{ RenderPrimitive::AddRef(); }
-		virtual void					Release() override			{ if (--mRefCount == 0) delete this; }
-	};
-
 	/// Finalize the current locked primitive and add it to the primitives to draw
 	void								FinalizePrimitive();
 
@@ -88,9 +78,6 @@ private:
 	unique_ptr<PipelineState>			mShadowStateFF;
 	unique_ptr<PipelineState>			mShadowStateWire;
 
-	/// The shadow buffer (depth buffer rendered from the light)
-	Ref<Texture>						mDepthTexture;
-
 	/// Lock that protects the triangle batches from being accessed from multiple threads
 	Mutex								mPrimitivesLock;
 
@@ -149,7 +136,7 @@ private:
 	Ref<RenderInstances>				mInstancesBuffer[Renderer::cFrameCount];
 
 	/// Primitive that is being built + its properties
-	Batch								mLockedPrimitive;
+	Ref<RenderPrimitive>				mLockedPrimitive;
 	Vertex *							mLockedVerticesStart = nullptr;
 	Vertex *							mLockedVertices = nullptr;
 	Vertex *							mLockedVerticesEnd = nullptr;

+ 11 - 11
TestFramework/Renderer/Font.cpp

@@ -196,20 +196,20 @@ Font::Create(const char *inFontName, int inCharHeight)
 	DeleteDC(dc);
 
 	// Create input layout
-	const D3D12_INPUT_ELEMENT_DESC vertex_desc[] =
+	const PipelineState::EInputDescription vertex_desc[] =
 	{
-		{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
-		{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
-		{ "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, 20, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
+		PipelineState::EInputDescription::Position,
+		PipelineState::EInputDescription::TexCoord,
+		PipelineState::EInputDescription::Color
 	};
 
 	// Load vertex shader
-	ComPtr<ID3DBlob> vtx = mRenderer->CreateVertexShader("Assets/Shaders/FontVertexShader.hlsl");
+	Ref<VertexShader> vtx = mRenderer->CreateVertexShader("Assets/Shaders/FontVertexShader");
 
 	// Load pixel shader
-	ComPtr<ID3DBlob> pix = mRenderer->CreatePixelShader("Assets/Shaders/FontPixelShader.hlsl");
+	Ref<PixelShader> pix = mRenderer->CreatePixelShader("Assets/Shaders/FontPixelShader");
 
-	mPipelineState = mRenderer->CreatePipelineState(vtx.Get(), vertex_desc, ARRAYSIZE(vertex_desc), pix.Get(), D3D12_FILL_MODE_SOLID, D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, PipelineState::EDepthTest::Off, PipelineState::EBlendMode::AlphaTest, PipelineState::ECullMode::Backface);
+	mPipelineState = mRenderer->CreatePipelineState(vtx, vertex_desc, std::size(vertex_desc), pix, PipelineState::EDrawPass::Normal, PipelineState::EFillMode::Solid, PipelineState::ETopology::Triangle, PipelineState::EDepthTest::Off, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
 
 	// Create texture
 	mTexture = mRenderer->CreateTexture(surface);
@@ -396,13 +396,13 @@ void Font::DrawText3D(Mat44Arg inTransform, const string_view &inText, ColorArg
 	if (inText.empty())
 		return;
 
-	RenderPrimitive primitive(mRenderer, D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
-	if (CreateString(inTransform, inText, inColor, primitive))
+	Ref<RenderPrimitive> primitive = mRenderer->CreateRenderPrimitive(PipelineState::ETopology::Triangle);
+	if (CreateString(inTransform, inText, inColor, *primitive))
 	{
-		mTexture->Bind(2);
+		mTexture->Bind();
 
 		mPipelineState->Activate();
 
-		primitive.Draw();
+		primitive->Draw();
 	}
 }

+ 1 - 0
TestFramework/Renderer/Font.h

@@ -10,6 +10,7 @@
 #include <Renderer/RenderPrimitive.h>
 #include <Renderer/Texture.h>
 #include <Renderer/PipelineState.h>
+#include <memory>
 
 class Renderer;
 class Texture;

+ 0 - 84
TestFramework/Renderer/PipelineState.cpp

@@ -1,84 +0,0 @@
-// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
-// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
-// SPDX-License-Identifier: MIT
-
-#include <TestFramework.h>
-
-#include <Renderer/PipelineState.h>
-#include <Renderer/Renderer.h>
-#include <Renderer/FatalErrorIfFailed.h>
-
-PipelineState::PipelineState(Renderer *inRenderer, ID3DBlob *inVertexShader, const D3D12_INPUT_ELEMENT_DESC *inInputDescription, uint inInputDescriptionCount, ID3DBlob *inPixelShader, D3D12_FILL_MODE inFillMode, D3D12_PRIMITIVE_TOPOLOGY_TYPE inTopology, EDepthTest inDepthTest, EBlendMode inBlendMode, ECullMode inCullMode) :
-	mRenderer(inRenderer)
-{
-	D3D12_GRAPHICS_PIPELINE_STATE_DESC pso_desc = {};
-	pso_desc.InputLayout = { inInputDescription, inInputDescriptionCount };
-	pso_desc.pRootSignature = mRenderer->GetRootSignature();
-	pso_desc.VS = { inVertexShader->GetBufferPointer(), inVertexShader->GetBufferSize() };
-	pso_desc.PS = { inPixelShader->GetBufferPointer(), inPixelShader->GetBufferSize() };
-
-	pso_desc.RasterizerState.FillMode = inFillMode;
-	pso_desc.RasterizerState.CullMode = inCullMode == ECullMode::Backface? D3D12_CULL_MODE_FRONT : D3D12_CULL_MODE_BACK; // DX uses left handed system so we reverse the options
-	pso_desc.RasterizerState.FrontCounterClockwise = FALSE;
-	pso_desc.RasterizerState.DepthBias = D3D12_DEFAULT_DEPTH_BIAS;
-	pso_desc.RasterizerState.DepthBiasClamp = D3D12_DEFAULT_DEPTH_BIAS_CLAMP;
-	pso_desc.RasterizerState.SlopeScaledDepthBias = D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS;
-	pso_desc.RasterizerState.DepthClipEnable = TRUE;
-	pso_desc.RasterizerState.MultisampleEnable = FALSE;
-	pso_desc.RasterizerState.AntialiasedLineEnable = FALSE;
-	pso_desc.RasterizerState.ForcedSampleCount = 0;
-	pso_desc.RasterizerState.ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF;
-
-	pso_desc.BlendState.AlphaToCoverageEnable = FALSE;
-	pso_desc.BlendState.IndependentBlendEnable = FALSE;
-
-	D3D12_RENDER_TARGET_BLEND_DESC &blend_desc = pso_desc.BlendState.RenderTarget[0];
-	blend_desc.LogicOpEnable = FALSE;
-	blend_desc.LogicOp = D3D12_LOGIC_OP_NOOP;
-	blend_desc.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
-	switch (inBlendMode)
-	{
-	case EBlendMode::Write:
-		blend_desc.BlendEnable = FALSE;
-		break;
-
-	case EBlendMode::AlphaTest:
-		pso_desc.BlendState.AlphaToCoverageEnable = TRUE;
-		[[fallthrough]];
-
-	case EBlendMode::AlphaBlend:
-		blend_desc.BlendEnable = TRUE;
-		blend_desc.SrcBlend = D3D12_BLEND_SRC_ALPHA;
-		blend_desc.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
-		blend_desc.BlendOp = D3D12_BLEND_OP_ADD;
-		blend_desc.SrcBlendAlpha = D3D12_BLEND_ZERO;
-		blend_desc.DestBlendAlpha = D3D12_BLEND_ZERO;
-		blend_desc.BlendOpAlpha = D3D12_BLEND_OP_ADD;
-		break;
-	}
-
-	pso_desc.DepthStencilState.DepthEnable = inDepthTest == EDepthTest::On? TRUE : FALSE;
-	pso_desc.DepthStencilState.DepthWriteMask = inDepthTest == EDepthTest::On? D3D12_DEPTH_WRITE_MASK_ALL : D3D12_DEPTH_WRITE_MASK_ZERO;
-	pso_desc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_GREATER;
-	pso_desc.DepthStencilState.StencilEnable = FALSE;
-
-	pso_desc.SampleMask = UINT_MAX;
-	pso_desc.PrimitiveTopologyType = inTopology;
-	pso_desc.NumRenderTargets = 1;
-	pso_desc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
-	pso_desc.DSVFormat = DXGI_FORMAT_D32_FLOAT;
-	pso_desc.SampleDesc.Count = 1;
-
-	FatalErrorIfFailed(mRenderer->GetDevice()->CreateGraphicsPipelineState(&pso_desc, IID_PPV_ARGS(&mPSO)));
-}
-
-PipelineState::~PipelineState()
-{
-	if (mPSO != nullptr)
-		mRenderer->RecycleD3DObject(mPSO.Get());
-}
-
-void PipelineState::Activate()
-{
-	mRenderer->GetCommandList()->SetPipelineState(mPSO.Get());
-}

+ 36 - 13
TestFramework/Renderer/PipelineState.h

@@ -4,12 +4,43 @@
 
 #pragma once
 
-class Renderer;
-
 /// Defines how primitives should be rendered
 class PipelineState
 {
 public:
+	/// Describes the input layout of the vertex shader
+	enum class EInputDescription
+	{
+		Position,						///< 3 float position
+		Color,							///< 4 uint8 color
+		Normal,							///< 3 float normal
+		TexCoord,						///< 2 float texture coordinate
+		InstanceColor,					///< 4 uint8 per instance color
+		InstanceTransform,				///< 4x4 float per instance transform
+		InstanceInvTransform,			///< 4x4 float per instance inverse transform
+	};
+
+	/// In which draw pass to use this pipeline state
+	enum class EDrawPass
+	{
+		Shadow,
+		Normal
+	};
+
+	/// The type of topology to emit
+	enum class ETopology
+	{
+		Triangle,
+		Line
+	};
+
+	/// Fill mode of the triangles
+	enum class EFillMode
+	{
+		Solid,
+		Wireframe
+	};
+
 	/// If depth write / depth test is on
 	enum class EDepthTest
 	{
@@ -22,7 +53,6 @@ public:
 	{
 		Write,
 		AlphaBlend,
-		AlphaTest,						///< Alpha blend with alpha test enabled
 	};
 
 	/// How to cull triangles
@@ -32,16 +62,9 @@ public:
 		FrontFace,
 	};
 
-	/// Constructor
-										PipelineState(Renderer *inRenderer, ID3DBlob *inVertexShader, const D3D12_INPUT_ELEMENT_DESC *inInputDescription, uint inInputDescriptionCount, ID3DBlob *inPixelShader, D3D12_FILL_MODE inFillMode, D3D12_PRIMITIVE_TOPOLOGY_TYPE inTopology, EDepthTest inDepthTest, EBlendMode inBlendMode, ECullMode inCullMode);
-										~PipelineState();
+	/// Destructor
+	virtual								~PipelineState() = default;
 
 	/// Make this pipeline state active (any primitives rendered after this will use this state)
-	void								Activate();
-
-private:
-	friend class Renderer;
-
-	Renderer *							mRenderer;
-	ComPtr<ID3D12PipelineState>			mPSO;
+	virtual void						Activate() = 0;
 };

+ 15 - 0
TestFramework/Renderer/PixelShader.h

@@ -0,0 +1,15 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Core/Reference.h>
+
+/// Pixel shader handle
+class PixelShader : public RefTarget<PixelShader>
+{
+public:
+	/// Destructor
+	virtual					~PixelShader() = default;
+};

+ 7 - 16
TestFramework/Renderer/RenderInstances.h

@@ -4,7 +4,6 @@
 
 #pragma once
 
-#include <Renderer/Renderer.h>
 #include <Jolt/Core/Reference.h>
 
 class RenderPrimitive;
@@ -13,25 +12,17 @@ class RenderPrimitive;
 class RenderInstances : public RefTarget<RenderInstances>
 {
 public:
-	/// Constructor
-							RenderInstances(Renderer *inRenderer)											: mRenderer(inRenderer) { }
-							~RenderInstances()																{ Clear(); }
+	/// Destructor
+	virtual					~RenderInstances() = default;
 
 	/// Erase all instance data
-	void					Clear();
+	virtual void			Clear() = 0;
 
 	/// Instance buffer management functions
-	void					CreateBuffer(int inNumInstances, int inInstanceSize);
-	void *					Lock();
-	void					Unlock();
+	virtual void			CreateBuffer(int inNumInstances, int inInstanceSize) = 0;
+	virtual void *			Lock() = 0;
+	virtual void			Unlock() = 0;
 
 	/// Draw the instances when context has been set by Renderer::BindShader
-	void					Draw(RenderPrimitive *inPrimitive, int inStartInstance, int inNumInstances) const;
-
-private:
-	Renderer *				mRenderer;
-
-	ComPtr<ID3D12Resource>	mInstanceBuffer;
-	int						mInstanceBufferSize = 0;
-	int						mInstanceSize = 0;
+	virtual void			Draw(RenderPrimitive *inPrimitive, int inStartInstance, int inNumInstances) const = 0;
 };

+ 0 - 121
TestFramework/Renderer/RenderPrimitive.cpp

@@ -5,39 +5,18 @@
 #include <TestFramework.h>
 
 #include <Renderer/RenderPrimitive.h>
-#include <Renderer/FatalErrorIfFailed.h>
 
 void RenderPrimitive::ReleaseVertexBuffer()
 {
-	if (mVtxBuffer != nullptr)
-	{
-		if (mVtxBufferInUploadHeap)
-			mRenderer->RecycleD3DResourceOnUploadHeap(mVtxBuffer.Get(), mNumVtx * mVtxSize);
-		else
-			mRenderer->RecycleD3DObject(mVtxBuffer.Get());
-		mVtxBuffer = nullptr;
-	}
-
 	mNumVtx = 0;
 	mNumVtxToDraw = 0;
 	mVtxSize = 0;
-	mVtxBufferInUploadHeap = false;
 }
 
 void RenderPrimitive::ReleaseIndexBuffer()
 {
-	if (mIdxBuffer != nullptr)
-	{
-		if (mIdxBufferInUploadHeap)
-			mRenderer->RecycleD3DResourceOnUploadHeap(mIdxBuffer.Get(), mNumIdx * sizeof(uint32));
-		else
-			mRenderer->RecycleD3DObject(mIdxBuffer.Get());
-		mIdxBuffer = nullptr;
-	}
-
 	mNumIdx = 0;
 	mNumIdxToDraw = 0;
-	mIdxBufferInUploadHeap = false;
 }
 
 void RenderPrimitive::Clear()
@@ -50,115 +29,15 @@ void RenderPrimitive::CreateVertexBuffer(int inNumVtx, int inVtxSize, const void
 {
 	ReleaseVertexBuffer();
 
-	uint64 size = uint64(inNumVtx) * inVtxSize;
-
-	if (inData != nullptr)
-	{
-		// Data provided, assume the buffer is static so allocate it on the GPU
-		mVtxBuffer = mRenderer->CreateD3DResourceOnDefaultHeap(inData, size);
-		mVtxBufferInUploadHeap = false;
-	}
-	else
-	{
-		// No data provided, create a buffer that will be uploaded to the GPU every time it is used
-		mVtxBuffer = mRenderer->CreateD3DResourceOnUploadHeap(size);
-		mVtxBufferInUploadHeap = true;
-	}
-
-	JPH_IF_DEBUG(mVtxBuffer->SetName(L"Vertex Buffer");)
-
 	mNumVtx = inNumVtx;
 	mNumVtxToDraw = inNumVtx;
 	mVtxSize = inVtxSize;
 }
 
-void *RenderPrimitive::LockVertexBuffer()
-{
-	void *mapped_resource;
-	D3D12_RANGE range = { 0, 0 };
-	FatalErrorIfFailed(mVtxBuffer->Map(0, &range, &mapped_resource));
-	return mapped_resource;
-}
-
-void RenderPrimitive::UnlockVertexBuffer()
-{
-	mVtxBuffer->Unmap(0, nullptr);
-}
-
 void RenderPrimitive::CreateIndexBuffer(int inNumIdx, const uint32 *inData)
 {
 	ReleaseIndexBuffer();
 
-	uint64 size = uint64(inNumIdx) * sizeof(uint32);
-
-	if (inData != nullptr)
-	{
-		// Data provided, assume the buffer is static so allocate it on the GPU
-		mIdxBuffer = mRenderer->CreateD3DResourceOnDefaultHeap(inData, size);
-		mIdxBufferInUploadHeap = false;
-	}
-	else
-	{
-		// No data provided, create a buffer that will be uploaded to the GPU every time it is used
-		mIdxBuffer = mRenderer->CreateD3DResourceOnUploadHeap(size);
-		mIdxBufferInUploadHeap = true;
-	}
-
-	JPH_IF_DEBUG(mIdxBuffer->SetName(L"Index Buffer");)
-
 	mNumIdx = inNumIdx;
 	mNumIdxToDraw = inNumIdx;
 }
-
-uint32 *RenderPrimitive::LockIndexBuffer()
-{
-	uint32 *mapped_resource;
-	D3D12_RANGE range = { 0, 0 };
-	FatalErrorIfFailed(mIdxBuffer->Map(0, &range, (void **)&mapped_resource));
-	return mapped_resource;
-}
-
-void RenderPrimitive::UnlockIndexBuffer()
-{
-	mIdxBuffer->Unmap(0, nullptr);
-}
-
-void RenderPrimitive::Draw() const
-{
-	ID3D12GraphicsCommandList *command_list = mRenderer->GetCommandList();
-
-	// Set topology
-	command_list->IASetPrimitiveTopology(mType);
-
-	if (mIdxBuffer == nullptr)
-	{
-		// Set vertex buffer
-		D3D12_VERTEX_BUFFER_VIEW vb_view;
-		vb_view.BufferLocation = mVtxBuffer->GetGPUVirtualAddress();
-		vb_view.StrideInBytes = mVtxSize;
-		vb_view.SizeInBytes = mNumVtxToDraw * mVtxSize;
-		command_list->IASetVertexBuffers(0, 1, &vb_view);
-
-		// Draw the non indexed primitive
-		command_list->DrawInstanced(mNumVtxToDraw, 1, 0, 0);
-	}
-	else
-	{
-		// Set vertex buffer
-		D3D12_VERTEX_BUFFER_VIEW vb_view;
-		vb_view.BufferLocation = mVtxBuffer->GetGPUVirtualAddress();
-		vb_view.StrideInBytes = mVtxSize;
-		vb_view.SizeInBytes = mNumVtx * mVtxSize;
-		command_list->IASetVertexBuffers(0, 1, &vb_view);
-
-		// Set index buffer
-		D3D12_INDEX_BUFFER_VIEW ib_view;
-		ib_view.BufferLocation = mIdxBuffer->GetGPUVirtualAddress();
-		ib_view.SizeInBytes = mNumIdxToDraw * sizeof(uint32);
-		ib_view.Format = DXGI_FORMAT_R32_UINT;
-		command_list->IASetIndexBuffer(&ib_view);
-
-		// Draw indexed primitive
-		command_list->DrawIndexedInstanced(mNumIdxToDraw, 1, 0, 0, 0);
-	}
-}

+ 16 - 24
TestFramework/Renderer/RenderPrimitive.h

@@ -4,16 +4,14 @@
 
 #pragma once
 
-#include <Renderer/Renderer.h>
 #include <Jolt/Core/Reference.h>
 
 /// Simple wrapper around vertex and index buffers
-class RenderPrimitive : public RefTarget<RenderPrimitive>
+class RenderPrimitive : public RefTarget<RenderPrimitive>, public RefTargetVirtual
 {
 public:
-	/// Constructor
-							RenderPrimitive(Renderer *inRenderer, D3D_PRIMITIVE_TOPOLOGY inType)			: mRenderer(inRenderer), mType(inType) { }
-							~RenderPrimitive()																{ Clear(); }
+	/// Destructor
+	virtual 				~RenderPrimitive() override = default;
 
 	/// Erase all primitive data
 	void					Clear();
@@ -22,41 +20,35 @@ public:
 	bool					IsEmpty() const																	{ return mNumVtx == 0 && mNumIdx == 0; }
 
 	/// Vertex buffer management functions
-	void					CreateVertexBuffer(int inNumVtx, int inVtxSize, const void *inData = nullptr);
-	void					ReleaseVertexBuffer();
-	void *					LockVertexBuffer();
-	void					UnlockVertexBuffer();
+	virtual void			CreateVertexBuffer(int inNumVtx, int inVtxSize, const void *inData = nullptr) = 0;
+	virtual void			ReleaseVertexBuffer();
+	virtual void *			LockVertexBuffer() = 0;
+	virtual void			UnlockVertexBuffer() = 0;
 	int						GetNumVtx() const																{ return mNumVtx; }
 	int						GetNumVtxToDraw() const															{ return mNumVtxToDraw; }
 	void					SetNumVtxToDraw(int inUsed)														{ mNumVtxToDraw = inUsed; }
 
 	/// Index buffer management functions
-	void					CreateIndexBuffer(int inNumIdx, const uint32 *inData = nullptr);
-	void					ReleaseIndexBuffer();
-	uint32 *				LockIndexBuffer();
-	void					UnlockIndexBuffer();
+	virtual void			CreateIndexBuffer(int inNumIdx, const uint32 *inData = nullptr) = 0;
+	virtual void			ReleaseIndexBuffer();
+	virtual uint32 *		LockIndexBuffer() = 0;
+	virtual void			UnlockIndexBuffer() = 0;
 	int						GetNumIdx() const																{ return mNumIdx; }
 	int						GetNumIdxToDraw() const															{ return mNumIdxToDraw; }
 	void					SetNumIdxToDraw(int inUsed)														{ mNumIdxToDraw = inUsed; }
 
 	/// Draw the primitive
-	void					Draw() const;
+	virtual void			Draw() const = 0;
 
-private:
-	friend class RenderInstances;
+	/// Implement RefTargetVirtual, so we can conveniently use this class as DebugRenderer::Batch
+	virtual void			AddRef() override																{ RefTarget<RenderPrimitive>::AddRef(); }
+	virtual void			Release() override																{ RefTarget<RenderPrimitive>::Release(); }
 
-	Renderer *				mRenderer;
-
-	D3D_PRIMITIVE_TOPOLOGY	mType;
-
-	ComPtr<ID3D12Resource>	mVtxBuffer;
+protected:
 	int						mNumVtx = 0;
 	int						mNumVtxToDraw = 0;
 	int						mVtxSize = 0;
-	bool					mVtxBufferInUploadHeap = false;
 
-	ComPtr<ID3D12Resource>	mIdxBuffer;
 	int						mNumIdx = 0;
 	int						mNumIdxToDraw = 0;
-	bool					mIdxBufferInUploadHeap = false;
 };

+ 18 - 705
TestFramework/Renderer/Renderer.cpp

@@ -5,34 +5,12 @@
 #include <TestFramework.h>
 
 #include <Renderer/Renderer.h>
-#include <Renderer/Texture.h>
-#include <Renderer/FatalErrorIfFailed.h>
-#include <Jolt/Core/Profiler.h>
-#include <Utils/ReadData.h>
 #include <Utils/Log.h>
 
-#include <d3dcompiler.h>
 #include <shellscalingapi.h>
-#ifdef JPH_DEBUG
-	#include <d3d12sdklayers.h>
-#endif
 
 static Renderer *sRenderer = nullptr;
 
-struct VertexShaderConstantBuffer
-{
-	Mat44		mView;
-	Mat44		mProjection;
-	Mat44		mLightView;
-	Mat44		mLightProjection;
-};
-
-struct PixelShaderConstantBuffer
-{
-	Vec4		mCameraPos;
-	Vec4		mLightPos;
-};
-
 //--------------------------------------------------------------------------------------
 // Called every time the application receives a message
 //--------------------------------------------------------------------------------------
@@ -63,99 +41,6 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l
 	return 0;
 }
 
-Renderer::~Renderer()
-{
-	// Ensure that the GPU is no longer referencing resources that are about to be cleaned up by the destructor.
-	WaitForGpu();
-
-	// Don't add more stuff to the delay reference list
-	mIsExiting = true;
-
-	CloseHandle(mFenceEvent);
-}
-
-void Renderer::WaitForGpu()
-{
-	// Schedule a Signal command in the queue
-	UINT64 current_fence_value = mFenceValues[mFrameIndex];
-	FatalErrorIfFailed(mCommandQueue->Signal(mFence.Get(), current_fence_value));
-
-	// Wait until the fence has been processed
-	FatalErrorIfFailed(mFence->SetEventOnCompletion(current_fence_value, mFenceEvent));
-	WaitForSingleObjectEx(mFenceEvent, INFINITE, FALSE);
-
-	// Increment the fence value for all frames
-	for (uint n = 0; n < cFrameCount; ++n)
-		mFenceValues[n] = current_fence_value + 1;
-
-	// Release all used resources
-	for (Array<ComPtr<ID3D12Object>> &list : mDelayReleased)
-		list.clear();
-
-	// Anything that's not used yet can be removed, delayed objects are now available
-	mResourceCache.clear();
-	mDelayCached[mFrameIndex].swap(mResourceCache);
-}
-
-void Renderer::CreateRenterTargets()
-{
-	// Create render targets and views
-	for (uint n = 0; n < cFrameCount; ++n)
-	{
-		mRenderTargetViews[n] = mRTVHeap.Allocate();
-
-		FatalErrorIfFailed(mSwapChain->GetBuffer(n, IID_PPV_ARGS(&mRenderTargets[n])));
-		mDevice->CreateRenderTargetView(mRenderTargets[n].Get(), nullptr, mRenderTargetViews[n]);
-	}
-}
-
-void Renderer::CreateDepthBuffer()
-{
-	// Free any previous depth stencil view
-	if (mDepthStencilView.ptr != 0)
-		mDSVHeap.Free(mDepthStencilView);
-
-	// Free any previous depth stencil buffer
-	mDepthStencilBuffer.Reset();
-
-	// Allocate depth stencil buffer
-	D3D12_CLEAR_VALUE clear_value = {};
-	clear_value.Format = DXGI_FORMAT_D32_FLOAT;
-	clear_value.DepthStencil.Depth = 0.0f;
-	clear_value.DepthStencil.Stencil = 0;
-
-	D3D12_HEAP_PROPERTIES heap_properties = {};
-	heap_properties.Type = D3D12_HEAP_TYPE_DEFAULT;
-	heap_properties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
-	heap_properties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
-	heap_properties.CreationNodeMask = 1;
-	heap_properties.VisibleNodeMask = 1;
-
-	D3D12_RESOURCE_DESC depth_stencil_desc = {};
-	depth_stencil_desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
-	depth_stencil_desc.Alignment = 0;
-	depth_stencil_desc.Width = mWindowWidth;
-	depth_stencil_desc.Height = mWindowHeight;
-	depth_stencil_desc.DepthOrArraySize = 1;
-	depth_stencil_desc.MipLevels = 1;
-	depth_stencil_desc.Format = DXGI_FORMAT_D32_FLOAT;
-	depth_stencil_desc.SampleDesc.Count = 1;
-	depth_stencil_desc.SampleDesc.Quality = 0;
-	depth_stencil_desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
-	depth_stencil_desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
-
-	FatalErrorIfFailed(mDevice->CreateCommittedResource(&heap_properties, D3D12_HEAP_FLAG_NONE, &depth_stencil_desc, D3D12_RESOURCE_STATE_DEPTH_WRITE, &clear_value, IID_PPV_ARGS(&mDepthStencilBuffer)));
-
-	// Allocate depth stencil view
-	D3D12_DEPTH_STENCIL_VIEW_DESC depth_stencil_view_desc = {};
-	depth_stencil_view_desc.Format = DXGI_FORMAT_D32_FLOAT;
-	depth_stencil_view_desc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
-	depth_stencil_view_desc.Flags = D3D12_DSV_FLAG_NONE;
-
-	mDepthStencilView = mDSVHeap.Allocate();
-	mDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), &depth_stencil_view_desc, mDepthStencilView);
-}
-
 void Renderer::Initialize()
 {
 	// Prevent this window from auto scaling
@@ -189,215 +74,6 @@ void Renderer::Initialize()
 	// Show window
 	ShowWindow(mhWnd, SW_SHOW);
 
-#if defined(JPH_DEBUG)
-	// Enable the D3D12 debug layer
-	ComPtr<ID3D12Debug> debug_controller;
-	if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debug_controller))))
-		debug_controller->EnableDebugLayer();
-#endif
-
-	// Create DXGI factory
-	FatalErrorIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mDXGIFactory)));
-
-	// Find adapter
-	ComPtr<IDXGIAdapter1> adapter;
-
-	HRESULT result = E_FAIL;
-
-	// First check if we have the Windows 1803 IDXGIFactory6 interface
-	ComPtr<IDXGIFactory6> factory6;
-	if (SUCCEEDED(mDXGIFactory->QueryInterface(IID_PPV_ARGS(&factory6))))
-	{
-		for (UINT index = 0; DXGI_ERROR_NOT_FOUND != factory6->EnumAdapterByGpuPreference(index, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&adapter)); ++index)
-		{
-			DXGI_ADAPTER_DESC1 desc;
-			adapter->GetDesc1(&desc);
-
-			// We don't want software renderers
-			if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
-				continue;
-
-			// Check to see whether the adapter supports Direct3D 12
-			result = D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&mDevice));
-			if (SUCCEEDED(result))
-				break;
-		}
-	}
-	else
-	{
-		// Fall back to the older method that may not get the fastest GPU
-		for (UINT index = 0; DXGI_ERROR_NOT_FOUND != mDXGIFactory->EnumAdapters1(index, &adapter); ++index)
-		{
-			DXGI_ADAPTER_DESC1 desc;
-			adapter->GetDesc1(&desc);
-
-			// We don't want software renderers
-			if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
-				continue;
-
-			// Check to see whether the adapter supports Direct3D 12
-			result = D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&mDevice));
-			if (SUCCEEDED(result))
-				break;
-		}
-	}
-
-	// Check if we managed to obtain a device
-	FatalErrorIfFailed(result);
-
-#ifdef JPH_DEBUG
-	// Enable breaking on errors
-	ComPtr<ID3D12InfoQueue> info_queue;
-	if (SUCCEEDED(mDevice.As(&info_queue)))
-	{
-		info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_CORRUPTION, TRUE);
-		info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_ERROR, TRUE);
-		info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_WARNING, TRUE);
-
-		// Disable an error that triggers on Windows 11 with a hybrid graphic system
-		// See: https://stackoverflow.com/questions/69805245/directx-12-application-is-crashing-in-windows-11
-		D3D12_MESSAGE_ID hide[] =
-		{
-			D3D12_MESSAGE_ID_RESOURCE_BARRIER_MISMATCHING_COMMAND_LIST_TYPE,
-		};
-		D3D12_INFO_QUEUE_FILTER filter = { };
-		filter.DenyList.NumIDs = static_cast<UINT>( std::size( hide ) );
-		filter.DenyList.pIDList = hide;
-		info_queue->AddStorageFilterEntries( &filter );
-	}
-#endif // JPH_DEBUG
-
-	// Disable full screen transitions
-	FatalErrorIfFailed(mDXGIFactory->MakeWindowAssociation(mhWnd, DXGI_MWA_NO_ALT_ENTER));
-
-	// Create heaps
-	mRTVHeap.Init(mDevice.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_RTV, D3D12_DESCRIPTOR_HEAP_FLAG_NONE, 2);
-	mDSVHeap.Init(mDevice.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_DSV, D3D12_DESCRIPTOR_HEAP_FLAG_NONE, 4);
-	mSRVHeap.Init(mDevice.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE, 128);
-
-	// Create a command queue
-	D3D12_COMMAND_QUEUE_DESC queue_desc = {};
-	queue_desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
-	queue_desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
-	FatalErrorIfFailed(mDevice->CreateCommandQueue(&queue_desc, IID_PPV_ARGS(&mCommandQueue)));
-
-	// Create a command allocator for each frame
-	for (uint n = 0; n < cFrameCount; n++)
-		FatalErrorIfFailed(mDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&mCommandAllocators[n])));
-
-	// Describe and create the swap chain
-	DXGI_SWAP_CHAIN_DESC swap_chain_desc = {};
-	swap_chain_desc.BufferCount = cFrameCount;
-	swap_chain_desc.BufferDesc.Width = mWindowWidth;
-	swap_chain_desc.BufferDesc.Height = mWindowHeight;
-	swap_chain_desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
-	swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
-	swap_chain_desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
-	swap_chain_desc.OutputWindow = mhWnd;
-	swap_chain_desc.SampleDesc.Count = 1;
-	swap_chain_desc.Windowed = TRUE;
-
-	ComPtr<IDXGISwapChain> swap_chain;
-	FatalErrorIfFailed(mDXGIFactory->CreateSwapChain(mCommandQueue.Get(), &swap_chain_desc, &swap_chain));
-	FatalErrorIfFailed(swap_chain.As(&mSwapChain));
-	mFrameIndex = mSwapChain->GetCurrentBackBufferIndex();
-
-	CreateRenterTargets();
-
-	CreateDepthBuffer();
-
-	// Create a root signature suitable for all our shaders
-	D3D12_ROOT_PARAMETER params[3] = {};
-
-	// Mapping a constant buffer to slot 0 for the vertex shader
-	params[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;
-	params[0].Descriptor.ShaderRegister = 0;
-	params[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;
-
-	// Mapping a constant buffer to slot 1 in the pixel shader
-	params[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;
-	params[1].Descriptor.ShaderRegister = 1;
-	params[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
-
-	// Mapping a texture to slot 2 in the pixel shader
-	D3D12_DESCRIPTOR_RANGE range = {};
-	range.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
-	range.BaseShaderRegister = 2;
-	range.NumDescriptors = 1;
-	params[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
-	params[2].DescriptorTable.NumDescriptorRanges = 1;
-	params[2].DescriptorTable.pDescriptorRanges = &range;
-	params[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
-
-	D3D12_STATIC_SAMPLER_DESC samplers[3] = {};
-
-	// Sampler 0: Non-wrapping linear filtering
-	samplers[0].Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR;
-	samplers[0].AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
-	samplers[0].AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
-	samplers[0].AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
-	samplers[0].MipLODBias = 0.0f;
-	samplers[0].MaxAnisotropy = 1;
-	samplers[0].ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS;
-	samplers[0].BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK;
-	samplers[0].MinLOD = 0.0f;
-	samplers[0].MaxLOD = D3D12_FLOAT32_MAX;
-	samplers[0].ShaderRegister = 0;
-	samplers[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
-
-	// Sampler 1: Wrapping and linear filtering
-	samplers[1] = samplers[0];
-	samplers[1].AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
-	samplers[1].AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
-	samplers[1].AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
-	samplers[1].ShaderRegister = 1;
-
-	// Sampler 2: Point filtering, using SampleCmp mode to compare if sampled value >= reference value (for shadows)
-	samplers[2] = samplers[0];
-	samplers[2].Filter = D3D12_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT;
-	samplers[2].ComparisonFunc = D3D12_COMPARISON_FUNC_GREATER_EQUAL;
-	samplers[2].ShaderRegister = 2;
-
-	D3D12_ROOT_SIGNATURE_DESC root_signature_desc = {};
-	root_signature_desc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
-	root_signature_desc.NumParameters = ARRAYSIZE(params);
-	root_signature_desc.pParameters = params;
-	root_signature_desc.NumStaticSamplers = ARRAYSIZE(samplers);
-	root_signature_desc.pStaticSamplers = samplers;
-
-	ComPtr<ID3DBlob> signature;
-	ComPtr<ID3DBlob> error;
-	FatalErrorIfFailed(D3D12SerializeRootSignature(&root_signature_desc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error));
-	FatalErrorIfFailed(mDevice->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&mRootSignature)));
-
-	// Create the command list
-	FatalErrorIfFailed(mDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, mCommandAllocators[mFrameIndex].Get(), nullptr, IID_PPV_ARGS(&mCommandList)));
-
-	// Command lists are created in the recording state, but there is nothing to record yet. The main loop expects it to be closed, so close it now
-	FatalErrorIfFailed(mCommandList->Close());
-
-	// Create synchronization object
-	FatalErrorIfFailed(mDevice->CreateFence(mFenceValues[mFrameIndex], D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)));
-
-	// Increment fence value so we don't skip waiting the first time a command list is executed
-	mFenceValues[mFrameIndex]++;
-
-	// Create an event handle to use for frame synchronization
-	mFenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
-	if (mFenceEvent == nullptr)
-		FatalErrorIfFailed(HRESULT_FROM_WIN32(GetLastError()));
-
-	// Initialize the queue used to upload resources to the GPU
-	mUploadQueue.Initialize(mDevice.Get());
-
-	// Create constant buffer. One per frame to avoid overwriting the constant buffer while the GPU is still using it.
-	for (uint n = 0; n < cFrameCount; ++n)
-	{
-		mVertexShaderConstantBufferProjection[n] = CreateConstantBuffer(sizeof(VertexShaderConstantBuffer));
-		mVertexShaderConstantBufferOrtho[n] = CreateConstantBuffer(sizeof(VertexShaderConstantBuffer));
-		mPixelShaderConstantBuffer[n] = CreateConstantBuffer(sizeof(PixelShaderConstantBuffer));
-	}
-
 	// Store global renderer now that we're done initializing
 	sRenderer = this;
 }
@@ -406,52 +82,23 @@ void Renderer::OnWindowResize()
 {
 	JPH_ASSERT(!mInFrame);
 
-	// Wait for the previous frame to be rendered
-	WaitForGpu();
-
 	// Get new window size
 	RECT rc;
 	GetClientRect(mhWnd, &rc);
 	mWindowWidth = max<LONG>(rc.right - rc.left, 8);
 	mWindowHeight = max<LONG>(rc.bottom - rc.top, 8);
-
-	// Free the render targets and views to allow resizing the swap chain
-	for (uint n = 0; n < cFrameCount; ++n)
-	{
-		mRTVHeap.Free(mRenderTargetViews[n]);
-		mRenderTargets[n].Reset();
-	}
-
-	// Resize the swap chain buffers
-	FatalErrorIfFailed(mSwapChain->ResizeBuffers(cFrameCount, mWindowWidth, mWindowHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0));
-
-	// Back buffer index may have changed after the resize (it always seems to go to 0 again)
-	mFrameIndex = mSwapChain->GetCurrentBackBufferIndex();
-
-	// Since we may have switched frame index and we know everything is done, we need to update the fence value for our other frame as completed
-	for (uint n = 0; n < cFrameCount; ++n)
-		if (mFrameIndex != n)
-			mFenceValues[n] = mFence->GetCompletedValue();
-
-	// Recreate render targets
-	CreateRenterTargets();
-
-	// Recreate depth buffer
-	CreateDepthBuffer();
 }
 
-static Mat44 sPerspectiveInfiniteReverseZ(float inFovY, float inAspect, float inNear)
+static Mat44 sPerspectiveInfiniteReverseZ(float inFovY, float inAspect, float inNear, float inYSign)
 {
 	float height = 1.0f / Tan(0.5f * inFovY);
 	float width = height / inAspect;
 
-	return Mat44(Vec4(width, 0.0f, 0.0f, 0.0f), Vec4(0.0f, height, 0.0f, 0.0f), Vec4(0.0f, 0.0f, 0, -1.0f), Vec4(0.0f, 0.0f, inNear, 0.0f));
+	return Mat44(Vec4(width, 0.0f, 0.0f, 0.0f), Vec4(0.0f, inYSign * height, 0.0f, 0.0f), Vec4(0.0f, 0.0f, 0.0f, -1.0f), Vec4(0.0f, 0.0f, inNear, 0.0f));
 }
 
 void Renderer::BeginFrame(const CameraState &inCamera, float inWorldScale)
 {
-	JPH_PROFILE_FUNCTION();
-
 	// Mark that we're in the frame
 	JPH_ASSERT(!mInFrame);
 	mInFrame = true;
@@ -459,37 +106,6 @@ void Renderer::BeginFrame(const CameraState &inCamera, float inWorldScale)
 	// Store state
 	mCameraState = inCamera;
 
-	// Reset command allocator
-	FatalErrorIfFailed(mCommandAllocators[mFrameIndex]->Reset());
-
-	// Reset command list
-	FatalErrorIfFailed(mCommandList->Reset(mCommandAllocators[mFrameIndex].Get(), nullptr));
-
-	// Set root signature
-	mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
-
-	// Set SRV heap
-	ID3D12DescriptorHeap *heaps[] = { mSRVHeap.Get() };
-	mCommandList->SetDescriptorHeaps(_countof(heaps), heaps);
-
-	// Indicate that the back buffer will be used as a render target.
-	D3D12_RESOURCE_BARRIER barrier;
-	barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
-	barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
-	barrier.Transition.pResource = mRenderTargets[mFrameIndex].Get();
-	barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
-	barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;
-	barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
-	mCommandList->ResourceBarrier(1, &barrier);
-
-	// Set the main back buffer as render target
-	SetRenderTarget(nullptr);
-
-	// Clear the back buffer.
-	const float blue[] = { 0.098f, 0.098f, 0.439f, 1.000f };
-	mCommandList->ClearRenderTargetView(mRenderTargetViews[mFrameIndex], blue, 0, nullptr);
-	mCommandList->ClearDepthStencilView(mDepthStencilView, D3D12_CLEAR_FLAG_DEPTH, 0.0f, 0, 0, nullptr);
-
 	// Light properties
 	Vec3 light_pos = inWorldScale * Vec3(250, 250, 250);
 	Vec3 light_tgt = Vec3::sZero();
@@ -499,346 +115,43 @@ void Renderer::BeginFrame(const CameraState &inCamera, float inWorldScale)
 	float light_near = 1.0f;
 
 	// Camera properties
+	Vec3 cam_pos = Vec3(inCamera.mPos - mBaseOffset);
 	float camera_fovy = inCamera.mFOVY;
 	float camera_aspect = static_cast<float>(GetWindowWidth()) / GetWindowHeight();
 	float camera_fovx = 2.0f * ATan(camera_aspect * Tan(0.5f * camera_fovy));
 	float camera_near = 0.01f * inWorldScale;
 
-	// Set constants for vertex shader in projection mode
-	VertexShaderConstantBuffer *vs = mVertexShaderConstantBufferProjection[mFrameIndex]->Map<VertexShaderConstantBuffer>();
+	// Calculate camera frustum
+	mCameraFrustum = Frustum(cam_pos, inCamera.mForward, inCamera.mUp, camera_fovx, camera_fovy, camera_near);
+
+	// Calculate light frustum
+	mLightFrustum = Frustum(light_pos, light_fwd, light_up, light_fov, light_fov, light_near);
 
 	// Camera projection and view
-	vs->mProjection = sPerspectiveInfiniteReverseZ(camera_fovy, camera_aspect, camera_near);
-	Vec3 cam_pos = Vec3(inCamera.mPos - mBaseOffset);
+	mVSBuffer.mProjection = sPerspectiveInfiniteReverseZ(camera_fovy, camera_aspect, camera_near, mPerspectiveYSign);
 	Vec3 tgt = cam_pos + inCamera.mForward;
-	vs->mView = Mat44::sLookAt(cam_pos, tgt, inCamera.mUp);
+	mVSBuffer.mView = Mat44::sLookAt(cam_pos, tgt, inCamera.mUp);
 
 	// Light projection and view
-	vs->mLightProjection = sPerspectiveInfiniteReverseZ(light_fov, 1.0f, light_near);
-	vs->mLightView = Mat44::sLookAt(light_pos, light_tgt, light_up);
-
-	mVertexShaderConstantBufferProjection[mFrameIndex]->Unmap();
-
-	// Set constants for vertex shader in ortho mode
-	vs = mVertexShaderConstantBufferOrtho[mFrameIndex]->Map<VertexShaderConstantBuffer>();
+	mVSBuffer.mLightProjection = sPerspectiveInfiniteReverseZ(light_fov, 1.0f, light_near, mPerspectiveYSign);
+	mVSBuffer.mLightView = Mat44::sLookAt(light_pos, light_tgt, light_up);
 
 	// Camera ortho projection and view
-	vs->mProjection = Mat44(Vec4(2.0f / mWindowWidth, 0.0f, 0.0f, 0.0f), Vec4(0.0f, -2.0f / mWindowHeight, 0.0f, 0.0f), Vec4(0.0f, 0.0f, -1.0f, 0.0f), Vec4(-1.0f, 1.0f, 0.0f, 1.0f));
-	vs->mView = Mat44::sIdentity();
+	mVSBufferOrtho.mProjection = Mat44(Vec4(2.0f / mWindowWidth, 0.0f, 0.0f, 0.0f), Vec4(0.0f, -mPerspectiveYSign * 2.0f / mWindowHeight, 0.0f, 0.0f), Vec4(0.0f, 0.0f, -1.0f, 0.0f), Vec4(-1.0f, mPerspectiveYSign * 1.0f, 0.0f, 1.0f));
+	mVSBufferOrtho.mView = Mat44::sIdentity();
 
 	// Light projection and view are unused in ortho mode
-	vs->mLightView = Mat44::sIdentity();
-	vs->mLightProjection = Mat44::sIdentity();
-
-	mVertexShaderConstantBufferOrtho[mFrameIndex]->Unmap();
-
-	// Switch to 3d projection mode
-	SetProjectionMode();
+	mVSBufferOrtho.mLightView = Mat44::sIdentity();
+	mVSBufferOrtho.mLightProjection = Mat44::sIdentity();
 
 	// Set constants for pixel shader
-	PixelShaderConstantBuffer *ps = mPixelShaderConstantBuffer[mFrameIndex]->Map<PixelShaderConstantBuffer>();
-	ps->mCameraPos = Vec4(cam_pos, 0);
-	ps->mLightPos = Vec4(light_pos, 0);
-	mPixelShaderConstantBuffer[mFrameIndex]->Unmap();
-
-	// Set the pixel shader constant buffer data.
-	mPixelShaderConstantBuffer[mFrameIndex]->Bind(1);
-
-	// Calculate camera frustum
-	mCameraFrustum = Frustum(cam_pos, inCamera.mForward, inCamera.mUp, camera_fovx, camera_fovy, camera_near);
-
-	// Calculate light frustum
-	mLightFrustum = Frustum(light_pos, light_fwd, light_up, light_fov, light_fov, light_near);
+	mPSBuffer.mCameraPos = Vec4(cam_pos, 0);
+	mPSBuffer.mLightPos = Vec4(light_pos, 0);
 }
 
 void Renderer::EndFrame()
 {
-	JPH_PROFILE_FUNCTION();
-
 	// Mark that we're no longer in the frame
 	JPH_ASSERT(mInFrame);
 	mInFrame = false;
-
-	// Indicate that the back buffer will now be used to present.
-	D3D12_RESOURCE_BARRIER barrier;
-	barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
-	barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
-	barrier.Transition.pResource = mRenderTargets[mFrameIndex].Get();
-	barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
-	barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
-	barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
-	mCommandList->ResourceBarrier(1, &barrier);
-
-	// Close the command list
-	FatalErrorIfFailed(mCommandList->Close());
-
-	// Execute the command list
-	ID3D12CommandList* command_lists[] = { mCommandList.Get() };
-	mCommandQueue->ExecuteCommandLists(_countof(command_lists), command_lists);
-
-	// Present the frame
-	FatalErrorIfFailed(mSwapChain->Present(1, 0));
-
-	// Schedule a Signal command in the queue
-	UINT64 current_fence_value = mFenceValues[mFrameIndex];
-	FatalErrorIfFailed(mCommandQueue->Signal(mFence.Get(), current_fence_value));
-
-	// Update the frame index
-	mFrameIndex = mSwapChain->GetCurrentBackBufferIndex();
-
-	// If the next frame is not ready to be rendered yet, wait until it is ready
-	UINT64 completed_value = mFence->GetCompletedValue();
-	if (completed_value < mFenceValues[mFrameIndex])
-	{
-		FatalErrorIfFailed(mFence->SetEventOnCompletion(mFenceValues[mFrameIndex], mFenceEvent));
-		WaitForSingleObjectEx(mFenceEvent, INFINITE, FALSE);
-	}
-
-	// Release all used resources
-	mDelayReleased[mFrameIndex].clear();
-
-	// Anything that's not used yet can be removed, delayed objects are now available
-	mResourceCache.clear();
-	mDelayCached[mFrameIndex].swap(mResourceCache);
-
-	// Set the fence value for the next frame.
-	mFenceValues[mFrameIndex] = current_fence_value + 1;
-}
-
-void Renderer::SetProjectionMode()
-{
-	JPH_ASSERT(mInFrame);
-
-	mVertexShaderConstantBufferProjection[mFrameIndex]->Bind(0);
-}
-
-void Renderer::SetOrthoMode()
-{
-	JPH_ASSERT(mInFrame);
-
-	mVertexShaderConstantBufferOrtho[mFrameIndex]->Bind(0);
-}
-
-Ref<Texture> Renderer::CreateTexture(const Surface *inSurface)
-{
-	return new Texture(this, inSurface);
-}
-
-Ref<Texture> Renderer::CreateRenderTarget(int inWidth, int inHeight)
-{
-	return new Texture(this, inWidth, inHeight);
-}
-
-void Renderer::SetRenderTarget(Texture *inRenderTarget)
-{
-	JPH_ASSERT(mInFrame);
-
-	// Unset the previous render target
-	if (mRenderTargetTexture != nullptr)
-		mRenderTargetTexture->SetAsRenderTarget(false);
-	mRenderTargetTexture = nullptr;
-
-	if (inRenderTarget == nullptr)
-	{
-		// Set the main back buffer as render target
-		mCommandList->OMSetRenderTargets(1, &mRenderTargetViews[mFrameIndex], FALSE, &mDepthStencilView);
-
-		// Set viewport
-		D3D12_VIEWPORT viewport = { 0.0f, 0.0f, static_cast<float>(mWindowWidth), static_cast<float>(mWindowHeight), 0.0f, 1.0f };
-		mCommandList->RSSetViewports(1, &viewport);
-
-		// Set scissor rect
-		D3D12_RECT scissor_rect = { 0, 0, static_cast<LONG>(mWindowWidth), static_cast<LONG>(mWindowHeight) };
-		mCommandList->RSSetScissorRects(1, &scissor_rect);
-	}
-	else
-	{
-		// Use the texture as render target
-		inRenderTarget->SetAsRenderTarget(true);
-		mRenderTargetTexture = inRenderTarget;
-	}
-}
-
-ComPtr<ID3DBlob> Renderer::CreateVertexShader(const char *inFileName)
-{
-	UINT flags = D3DCOMPILE_ENABLE_STRICTNESS;
-#ifdef JPH_DEBUG
-	flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
-#endif
-
-	const D3D_SHADER_MACRO defines[] =
-	{
-		{ nullptr, nullptr }
-	};
-
-	// Read shader source file
-	Array<uint8> data = ReadData(inFileName);
-
-	// Compile source
-	ComPtr<ID3DBlob> shader_blob, error_blob;
-	HRESULT hr = D3DCompile(&data[0],
-							(uint)data.size(),
-							inFileName,
-							defines,
-							D3D_COMPILE_STANDARD_FILE_INCLUDE,
-							"main",
-							"vs_5_0",
-							flags,
-							0,
-							shader_blob.GetAddressOf(),
-							error_blob.GetAddressOf());
-	if (FAILED(hr))
-	{
-		// Throw error if compilation failed
-		if (error_blob)
-			OutputDebugStringA((const char *)error_blob->GetBufferPointer());
-		FatalError("Failed to compile vertex shader");
-	}
-
-	return shader_blob;
-}
-
-ComPtr<ID3DBlob> Renderer::CreatePixelShader(const char *inFileName)
-{
-	UINT flags = D3DCOMPILE_ENABLE_STRICTNESS;
-#ifdef JPH_DEBUG
-	flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
-#endif
-
-	const D3D_SHADER_MACRO defines[] =
-	{
-		{ nullptr, nullptr }
-	};
-
-	// Read shader source file
-	Array<uint8> data = ReadData(inFileName);
-
-	// Compile source
-	ComPtr<ID3DBlob> shader_blob, error_blob;
-	HRESULT hr = D3DCompile(&data[0],
-							(uint)data.size(),
-							inFileName,
-							defines,
-							D3D_COMPILE_STANDARD_FILE_INCLUDE,
-							"main",
-							"ps_5_0",
-							flags,
-							0,
-							shader_blob.GetAddressOf(),
-							error_blob.GetAddressOf());
-	if (FAILED(hr))
-	{
-		// Throw error if compilation failed
-		if (error_blob)
-			OutputDebugStringA((const char *)error_blob->GetBufferPointer());
-		FatalError("Failed to compile pixel shader");
-	}
-
-	return shader_blob;
-}
-
-unique_ptr<ConstantBuffer> Renderer::CreateConstantBuffer(uint inBufferSize)
-{
-	return make_unique<ConstantBuffer>(this, inBufferSize);
-}
-
-unique_ptr<PipelineState> Renderer::CreatePipelineState(ID3DBlob *inVertexShader, const D3D12_INPUT_ELEMENT_DESC *inInputDescription, uint inInputDescriptionCount, ID3DBlob *inPixelShader, D3D12_FILL_MODE inFillMode, D3D12_PRIMITIVE_TOPOLOGY_TYPE inTopology, PipelineState::EDepthTest inDepthTest, PipelineState::EBlendMode inBlendMode, PipelineState::ECullMode inCullMode)
-{
-	return make_unique<PipelineState>(this, inVertexShader, inInputDescription, inInputDescriptionCount, inPixelShader, inFillMode, inTopology, inDepthTest, inBlendMode, inCullMode);
-}
-
-ComPtr<ID3D12Resource> Renderer::CreateD3DResource(D3D12_HEAP_TYPE inHeapType, D3D12_RESOURCE_STATES inResourceState, uint64 inSize)
-{
-	// Create a new resource
-	D3D12_RESOURCE_DESC desc;
-	desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
-	desc.Alignment = 0;
-	desc.Width = inSize;
-	desc.Height = 1;
-	desc.DepthOrArraySize = 1;
-	desc.MipLevels = 1;
-	desc.Format = DXGI_FORMAT_UNKNOWN;
-	desc.SampleDesc.Count = 1;
-	desc.SampleDesc.Quality = 0;
-	desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
-	desc.Flags = D3D12_RESOURCE_FLAG_NONE;
-
-	D3D12_HEAP_PROPERTIES heap_properties = {};
-	heap_properties.Type = inHeapType;
-	heap_properties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
-	heap_properties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
-	heap_properties.CreationNodeMask = 1;
-	heap_properties.VisibleNodeMask = 1;
-
-	ComPtr<ID3D12Resource> resource;
-	FatalErrorIfFailed(mDevice->CreateCommittedResource(&heap_properties, D3D12_HEAP_FLAG_NONE, &desc, inResourceState, nullptr, IID_PPV_ARGS(&resource)));
-	return resource;
-}
-
-void Renderer::CopyD3DResource(ID3D12Resource *inDest, const void *inSrc, uint64 inSize)
-{
-	// Copy data to destination buffer
-	void *data;
-	D3D12_RANGE range = { 0, 0 }; // We're not going to read
-	FatalErrorIfFailed(inDest->Map(0, &range, &data));
-	memcpy(data, inSrc, size_t(inSize));
-	inDest->Unmap(0, nullptr);
-}
-
-void Renderer::CopyD3DResource(ID3D12Resource *inDest, ID3D12Resource *inSrc, uint64 inSize)
-{
-	// Start a commandlist for the upload
-	ID3D12GraphicsCommandList *list = mUploadQueue.Start();
-
-	// Copy the data to the GPU
-	list->CopyBufferRegion(inDest, 0, inSrc, 0, inSize);
-
-	// Change the state of the resource to generic read
-	D3D12_RESOURCE_BARRIER barrier;
-	barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
-	barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
-	barrier.Transition.pResource = inDest;
-	barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
-	barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_GENERIC_READ;
-	barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
-	list->ResourceBarrier(1, &barrier);
-
-	// Wait for copying to finish
-	mUploadQueue.ExecuteAndWait();
-}
-
-ComPtr<ID3D12Resource> Renderer::CreateD3DResourceOnDefaultHeap(const void *inData, uint64 inSize)
-{
-	ComPtr<ID3D12Resource> upload = CreateD3DResourceOnUploadHeap(inSize);
-	ComPtr<ID3D12Resource> resource = CreateD3DResource(D3D12_HEAP_TYPE_DEFAULT, D3D12_RESOURCE_STATE_COMMON, inSize);
-	CopyD3DResource(upload.Get(), inData, inSize);
-	CopyD3DResource(resource.Get(), upload.Get(), inSize);
-	RecycleD3DResourceOnUploadHeap(upload.Get(), inSize);
-	return resource;
-}
-
-ComPtr<ID3D12Resource> Renderer::CreateD3DResourceOnUploadHeap(uint64 inSize)
-{
-	// Try cache first
-	ResourceCache::iterator i = mResourceCache.find(inSize);
-	if (i != mResourceCache.end() && !i->second.empty())
-	{
-		ComPtr<ID3D12Resource> resource = i->second.back();
-		i->second.pop_back();
-		return resource;
-	}
-
-	return CreateD3DResource(D3D12_HEAP_TYPE_UPLOAD, D3D12_RESOURCE_STATE_GENERIC_READ, inSize);
-}
-
-void Renderer::RecycleD3DResourceOnUploadHeap(ID3D12Resource *inResource, uint64 inSize)
-{
-	if (!mIsExiting)
-		mDelayCached[mFrameIndex][inSize].push_back(inResource);
-}
-
-void Renderer::RecycleD3DObject(ID3D12Object *inResource)
-{
-	if (!mIsExiting)
-		mDelayReleased[mFrameIndex].push_back(inResource);
 }

+ 49 - 98
TestFramework/Renderer/Renderer.h

@@ -4,13 +4,13 @@
 
 #pragma once
 
-#include <Jolt/Core/UnorderedMap.h>
 #include <Image/Surface.h>
 #include <Renderer/Frustum.h>
-#include <Renderer/ConstantBuffer.h>
 #include <Renderer/PipelineState.h>
-#include <Renderer/CommandQueue.h>
-#include <Renderer/DescriptorHeap.h>
+#include <Renderer/VertexShader.h>
+#include <Renderer/PixelShader.h>
+#include <Renderer/RenderPrimitive.h>
+#include <Renderer/RenderInstances.h>
 #include <memory>
 
 // Forward declares
@@ -32,13 +32,10 @@ class Renderer
 {
 public:
 	/// Destructor
-									~Renderer();
+	virtual							~Renderer() = default;
 
 	/// Initialize DirectX
-	void							Initialize();
-
-	/// Callback when the window resizes and the back buffer needs to be adjusted
-	void							OnWindowResize();
+	virtual void					Initialize();
 
 	/// Get window size
 	int								GetWindowWidth()					{ return mWindowWidth; }
@@ -47,42 +44,35 @@ public:
 	/// Access to the window handle
 	HWND							GetWindowHandle() const				{ return mhWnd; }
 
-	/// Access to the most important DirectX structures
-	ID3D12Device *					GetDevice()							{ return mDevice.Get(); }
-	ID3D12RootSignature *			GetRootSignature()					{ return mRootSignature.Get(); }
-	ID3D12GraphicsCommandList *		GetCommandList()					{ JPH_ASSERT(mInFrame); return mCommandList.Get(); }
-	CommandQueue &					GetUploadQueue()					{ return mUploadQueue; }
-	DescriptorHeap &				GetDSVHeap()						{ return mDSVHeap; }
-	DescriptorHeap &				GetSRVHeap()						{ return mSRVHeap; }
-
 	/// Start / end drawing a frame
-	void							BeginFrame(const CameraState &inCamera, float inWorldScale);
-	void							EndFrame();
+	virtual void					BeginFrame(const CameraState &inCamera, float inWorldScale);
+	virtual void					EndShadowPass() = 0;
+	virtual void					EndFrame();
 
 	/// Switch between orthographic and 3D projection mode
-	void							SetProjectionMode();
-	void							SetOrthoMode();
+	virtual void					SetProjectionMode() = 0;
+	virtual void					SetOrthoMode() = 0;
 
 	/// Create texture from an image surface
-	Ref<Texture>					CreateTexture(const Surface *inSurface);
-
-	/// Create a texture to render to (currently depth buffer only)
-	Ref<Texture>					CreateRenderTarget(int inWidth, int inHeight);
-
-	/// Change the render target to a texture. Use nullptr to set back to the main render target.
-	void							SetRenderTarget(Texture *inRenderTarget);
+	virtual Ref<Texture>			CreateTexture(const Surface *inSurface) = 0;
 
 	/// Compile a vertex shader
-	ComPtr<ID3DBlob>				CreateVertexShader(const char *inFileName);
+	virtual Ref<VertexShader>		CreateVertexShader(const char *inFileName) = 0;
 
 	/// Compile a pixel shader
-	ComPtr<ID3DBlob>				CreatePixelShader(const char *inFileName);
-
-	/// Create a constant buffer for the shader
-	unique_ptr<ConstantBuffer>		CreateConstantBuffer(uint inBufferSize);
+	virtual Ref<PixelShader>		CreatePixelShader(const char *inFileName) = 0;
 
 	/// Create pipeline state object that defines the complete state of how primitives should be rendered
-	unique_ptr<PipelineState>		CreatePipelineState(ID3DBlob *inVertexShader, const D3D12_INPUT_ELEMENT_DESC *inInputDescription, uint inInputDescriptionCount, ID3DBlob *inPixelShader, D3D12_FILL_MODE inFillMode, D3D12_PRIMITIVE_TOPOLOGY_TYPE inTopology, PipelineState::EDepthTest inDepthTest, PipelineState::EBlendMode inBlendMode, PipelineState::ECullMode inCullMode);
+	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) = 0;
+
+	/// Create a render primitive
+	virtual RenderPrimitive *		CreateRenderPrimitive(PipelineState::ETopology inType) = 0;
+
+	/// Create render instances object to allow drawing batches of objects
+	virtual RenderInstances *		CreateRenderInstances() = 0;
+
+	/// Get the shadow map texture
+	virtual Texture *				GetShadowMap() const = 0;
 
 	/// Get the camera state / frustum (only valid between BeginFrame() / EndFrame())
 	const CameraState &				GetCameraState() const				{ JPH_ASSERT(mInFrame); return mCameraState; }
@@ -98,80 +88,41 @@ public:
 	/// How many frames our pipeline is
 	static const uint				cFrameCount = 2;
 
+	/// Size of the shadow map will be cShadowMapSize x cShadowMapSize pixels
+	static const uint				cShadowMapSize = 4096;
+
 	/// Which frame is currently rendering (to keep track of which buffers are free to overwrite)
 	uint							GetCurrentFrameIndex() const		{ JPH_ASSERT(mInFrame); return mFrameIndex; }
 
-	/// Create a buffer on the default heap (usable for permanent buffers)
-	ComPtr<ID3D12Resource>			CreateD3DResourceOnDefaultHeap(const void *inData, uint64 inSize);
-
-	/// Create buffer on the upload heap (usable for temporary buffers).
-	ComPtr<ID3D12Resource>			CreateD3DResourceOnUploadHeap(uint64 inSize);
-
-	/// Recycle a buffer on the upload heap. This puts it back in a cache and will reuse it when it is certain the GPU is no longer referencing it.
-	void							RecycleD3DResourceOnUploadHeap(ID3D12Resource *inResource, uint64 inSize);
-
-	/// Keeps a reference to the resource until the current frame has finished
-	void							RecycleD3DObject(ID3D12Object *inResource);
-
-private:
-	// Wait for pending GPU work to complete
-	void							WaitForGpu();
-
-	// Create render targets and their views
-	void							CreateRenterTargets();
-
-	// Create a depth buffer for the back buffer
-	void							CreateDepthBuffer();
-
-	// Function to create a ID3D12Resource on specified heap with specified state
-	ComPtr<ID3D12Resource>			CreateD3DResource(D3D12_HEAP_TYPE inHeapType, D3D12_RESOURCE_STATES inResourceState, uint64 inSize);
-
-	// Copy CPU memory into a ID3D12Resource
-	void							CopyD3DResource(ID3D12Resource *inDest, const void *inSrc, uint64 inSize);
-
-	// Copy a CPU resource to a GPU resource
-	void							CopyD3DResource(ID3D12Resource *inDest, ID3D12Resource *inSrc, uint64 inSize);
+	/// Callback when the window resizes and the back buffer needs to be adjusted
+	virtual void					OnWindowResize();
+
+protected:
+	struct VertexShaderConstantBuffer
+	{
+		Mat44						mView;
+		Mat44						mProjection;
+		Mat44						mLightView;
+		Mat44						mLightProjection;
+	};
+
+	struct PixelShaderConstantBuffer
+	{
+		Vec4						mCameraPos;
+		Vec4						mLightPos;
+	};
 
 	HWND							mhWnd;
 	int								mWindowWidth = 1920;
 	int								mWindowHeight = 1080;
-	unique_ptr<ConstantBuffer>		mVertexShaderConstantBufferProjection[cFrameCount];
-	unique_ptr<ConstantBuffer>		mVertexShaderConstantBufferOrtho[cFrameCount];
-	unique_ptr<ConstantBuffer>		mPixelShaderConstantBuffer[cFrameCount];
+	float							mPerspectiveYSign = 1.0f;			///< Sign for the Y coordinate in the projection matrix (1 for DX, -1 for Vulkan)
 	bool							mInFrame = false;					///< If we're within a BeginFrame() / EndFrame() pair
 	CameraState						mCameraState;
 	RVec3							mBaseOffset { RVec3::sZero() };		///< Offset to subtract from the camera position to deal with large worlds
 	Frustum							mCameraFrustum;
 	Frustum							mLightFrustum;
-
-	// DirectX interfaces
-	ComPtr<IDXGIFactory4>			mDXGIFactory;
-	ComPtr<ID3D12Device>			mDevice;
-	DescriptorHeap					mRTVHeap;							///< Render target view heap
-	DescriptorHeap					mDSVHeap;							///< Depth stencil view heap
-	DescriptorHeap					mSRVHeap;							///< Shader resource view heap
-	ComPtr<IDXGISwapChain3>			mSwapChain;
-	ComPtr<ID3D12Resource>			mRenderTargets[cFrameCount];		///< Two render targets (we're double buffering in order for the CPU to continue while the GPU is rendering)
-	D3D12_CPU_DESCRIPTOR_HANDLE		mRenderTargetViews[cFrameCount];	///< The two render views corresponding to the render targets
-	ComPtr<ID3D12Resource>			mDepthStencilBuffer;				///< The main depth buffer
-	D3D12_CPU_DESCRIPTOR_HANDLE		mDepthStencilView { 0 };			///< A view for binding the depth buffer
-	ComPtr<ID3D12CommandAllocator>	mCommandAllocators[cFrameCount];	///< Two command allocator lists (one per frame)
-	ComPtr<ID3D12CommandQueue>		mCommandQueue;						///< The command queue that will execute commands (there's only 1 since we want to finish rendering 1 frame before moving onto the next)
-	ComPtr<ID3D12GraphicsCommandList> mCommandList;						///< The command list
-	ComPtr<ID3D12RootSignature>		mRootSignature;						///< The root signature, we have a simple application so we only need 1, which is suitable for all our shaders
-	Ref<Texture>					mRenderTargetTexture;				///< When rendering to a texture, this is the active texture
-	CommandQueue					mUploadQueue;						///< Queue used to upload resources to GPU memory
-
-	// Synchronization objects used to finish rendering and swapping before reusing a command queue
-	uint							mFrameIndex;						///< Current frame index (0 or 1)
-	HANDLE							mFenceEvent;						///< Fence event to wait for the previous frame rendering to complete (in order to free 1 of the buffers)
-	ComPtr<ID3D12Fence>				mFence;								///< Fence object, used to signal the end of a frame
-	UINT64							mFenceValues[cFrameCount] = {};		///< Values that were used to signal completion of one of the two frames
-
-	using ResourceCache = UnorderedMap<uint64, Array<ComPtr<ID3D12Resource>>>;
-
-	ResourceCache					mResourceCache;						///< Cache items ready to be reused
-	ResourceCache					mDelayCached[cFrameCount];			///< List of reusable ID3D12Resources that are potentially referenced by the GPU so can be used only when the GPU finishes
-	Array<ComPtr<ID3D12Object>>		mDelayReleased[cFrameCount];		///< Objects that are potentially referenced by the GPU so can only be freed when the GPU finishes
-	bool							mIsExiting = false;					///< When exiting we don't want to add references too buffers
+	uint							mFrameIndex = 0;					///< Current frame index (0 or 1)
+	VertexShaderConstantBuffer		mVSBuffer;
+	VertexShaderConstantBuffer		mVSBufferOrtho;
+	PixelShaderConstantBuffer		mPSBuffer;
 };

+ 5 - 20
TestFramework/Renderer/Texture.h

@@ -7,38 +7,23 @@
 #include <Jolt/Core/Reference.h>
 
 /// Forward declares
-class Renderer;
 class Surface;
 
 class Texture : public RefTarget<Texture>
 {
 public:
-	/// Constructor, called by Renderer::CreateTexture
-										Texture(Renderer *inRenderer, const Surface *inSurface);	// Create a normal texture
-										Texture(Renderer *inRenderer, int inWidth, int inHeight);	// Create a render target (depth only)
-										~Texture();
+	/// Constructor
+										Texture(int inWidth, int inHeight) : mWidth(inWidth), mHeight(inHeight) { }
+	virtual								~Texture() = default;
 
 	/// Get dimensions of texture
 	inline int							GetWidth() const		{ return mWidth; }
 	inline int							GetHeight() const		{ return mHeight; }
 
 	/// Bind texture to the pixel shader
-	void								Bind(int inSlot) const;
-
-	/// Clear this texture (only possible for render targets)
-	void								ClearRenderTarget();
-
-	/// Activate this texture as the current render target, called by Renderer::SetRenderTarget
-	void								SetAsRenderTarget(bool inSet) const;
-
-private:
-	Renderer *							mRenderer;
+	virtual void						Bind() const = 0;
 
+protected:
 	int									mWidth;
 	int									mHeight;
-
-	ComPtr<ID3D12Resource>				mTexture;				///< The texture data
-
-	D3D12_CPU_DESCRIPTOR_HANDLE			mSRV { 0 };				///< Shader resource view to bind as texture
-	D3D12_CPU_DESCRIPTOR_HANDLE			mDSV { 0 };				///< Depth shader view to bind as render target
 };

+ 35 - 0
TestFramework/Renderer/VK/BufferVK.h

@@ -0,0 +1,35 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <vulkan/vulkan.h>
+
+/// Simple wrapper class to manage a Vulkan buffer
+class BufferVK
+{
+public:
+	/// Free memory associated with a buffer
+	void						Free(VkDevice inDevice)
+	{
+		if (mBuffer != VK_NULL_HANDLE)
+		{
+			vkDestroyBuffer(inDevice, mBuffer, nullptr);
+			mBuffer = VK_NULL_HANDLE;
+		}
+
+		if (mMemory != VK_NULL_HANDLE)
+		{
+			vkFreeMemory(inDevice, mMemory, nullptr);
+			mMemory = VK_NULL_HANDLE;
+		}
+	}
+
+	VkBuffer					mBuffer = VK_NULL_HANDLE;
+	VkDeviceMemory				mMemory = VK_NULL_HANDLE;
+
+	VkBufferUsageFlags			mUsage;
+	VkMemoryPropertyFlags		mProperties;
+	VkDeviceSize				mSize = 0;
+};

+ 32 - 0
TestFramework/Renderer/VK/ConstantBufferVK.cpp

@@ -0,0 +1,32 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/VK/ConstantBufferVK.h>
+#include <Renderer/VK/RendererVK.h>
+#include <Renderer/VK/FatalErrorIfFailedVK.h>
+
+ConstantBufferVK::ConstantBufferVK(RendererVK *inRenderer, VkDeviceSize inBufferSize) :
+	mRenderer(inRenderer)
+{
+	mRenderer->CreateBuffer(inBufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, mBuffer);
+}
+
+ConstantBufferVK::~ConstantBufferVK()
+{
+	mRenderer->FreeBuffer(mBuffer);
+}
+
+void *ConstantBufferVK::MapInternal()
+{
+	void *data = nullptr;
+	FatalErrorIfFailed(vkMapMemory(mRenderer->GetDevice(), mBuffer.mMemory, 0, mBuffer.mSize, 0, &data));
+	return data;
+}
+
+void ConstantBufferVK::Unmap()
+{
+	vkUnmapMemory(mRenderer->GetDevice(), mBuffer.mMemory);
+}

+ 30 - 0
TestFramework/Renderer/VK/ConstantBufferVK.h

@@ -0,0 +1,30 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/VK/BufferVK.h>
+
+class RendererVK;
+
+/// A binary blob that can be used to pass constants to a shader
+class ConstantBufferVK
+{
+public:
+	/// Constructor
+										ConstantBufferVK(RendererVK *inRenderer, VkDeviceSize inBufferSize);
+										~ConstantBufferVK();
+
+	/// Map / unmap buffer (get pointer to data). This will discard all data in the buffer.
+	template <typename T> T *			Map()											{ return reinterpret_cast<T *>(MapInternal()); }
+	void								Unmap();
+
+	VkBuffer							GetBuffer() const								{ return mBuffer.mBuffer; }
+
+private:
+	void *								MapInternal();
+
+	RendererVK *						mRenderer;
+	BufferVK							mBuffer;
+};

+ 14 - 0
TestFramework/Renderer/VK/FatalErrorIfFailedVK.cpp

@@ -0,0 +1,14 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/VK/FatalErrorIfFailedVK.h>
+#include <Utils/Log.h>
+
+void FatalErrorIfFailed(VkResult inVkResult)
+{
+	if (inVkResult != VK_SUCCESS)
+		FatalError("Vulkan error returned: %d", inVkResult);
+}

+ 11 - 0
TestFramework/Renderer/VK/FatalErrorIfFailedVK.h

@@ -0,0 +1,11 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <vulkan/vulkan.h>
+
+/// Convert Vulkan error to readable text and alert
+void FatalErrorIfFailed(VkResult inVkResult);
+

+ 189 - 0
TestFramework/Renderer/VK/PipelineStateVK.cpp

@@ -0,0 +1,189 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/VK/PipelineStateVK.h>
+#include <Renderer/VK/RendererVK.h>
+#include <Renderer/VK/FatalErrorIfFailedVK.h>
+
+PipelineStateVK::PipelineStateVK(RendererVK *inRenderer, const VertexShaderVK *inVertexShader, const EInputDescription *inInputDescription, uint inInputDescriptionCount, const PixelShaderVK *inPixelShader, EDrawPass inDrawPass, EFillMode inFillMode, ETopology inTopology, EDepthTest inDepthTest, EBlendMode inBlendMode, ECullMode inCullMode) :
+	mRenderer(inRenderer),
+	mVertexShader(inVertexShader),
+	mPixelShader(inPixelShader)
+{
+	VkPipelineShaderStageCreateInfo shader_stages[] = { inVertexShader->mStageInfo, inPixelShader->mStageInfo };
+
+	// TODO: This doesn't follow the SPIR-V alignment rules
+	Array<VkVertexInputAttributeDescription> attribute_descriptions;
+	VkVertexInputAttributeDescription temp_vtx = { }, temp_instance = { };
+	temp_instance.binding = 1;
+	uint instance_alignment = 1;
+	for (uint i = 0; i < inInputDescriptionCount; ++i)
+		switch (inInputDescription[i])
+		{
+		case EInputDescription::Position:
+			temp_vtx.format = VK_FORMAT_R32G32B32_SFLOAT;
+			attribute_descriptions.push_back(temp_vtx);
+			temp_vtx.offset += 3 * sizeof(float);
+			break;
+
+		case EInputDescription::Color:
+			temp_vtx.format = VK_FORMAT_R8G8B8A8_UNORM;
+			attribute_descriptions.push_back(temp_vtx);
+			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);
+			temp_vtx.offset += 2 * sizeof(float);
+			break;
+
+		case EInputDescription::InstanceColor:
+			instance_alignment = max(instance_alignment, 4u);
+			temp_instance.format = VK_FORMAT_R8G8B8A8_UNORM;
+			attribute_descriptions.push_back(temp_instance);
+			temp_instance.offset += 4 * sizeof(uint8);
+			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;
+			for (int j = 0; j < 4; ++j)
+			{
+				attribute_descriptions.push_back(temp_instance);
+				temp_instance.offset += 4 * sizeof(float);
+			}
+			break;
+		}
+
+	for (uint32 i = 0; i < uint32(attribute_descriptions.size()); ++i)
+		attribute_descriptions[i].location = i;
+
+	VkVertexInputBindingDescription binding_description[2];
+	binding_description[0].binding = 0;
+	binding_description[0].stride = temp_vtx.offset;
+	binding_description[0].inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
+	binding_description[1].binding = 1;
+	binding_description[1].stride = AlignUp(temp_instance.offset, instance_alignment);
+	binding_description[1].inputRate = VK_VERTEX_INPUT_RATE_INSTANCE;
+
+	VkPipelineVertexInputStateCreateInfo vertex_input_info = {};
+	vertex_input_info.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
+	vertex_input_info.vertexBindingDescriptionCount = temp_instance.offset > 0? 2 : 1;
+	vertex_input_info.pVertexBindingDescriptions = binding_description;
+	vertex_input_info.vertexAttributeDescriptionCount = uint32(attribute_descriptions.size());
+	vertex_input_info.pVertexAttributeDescriptions = attribute_descriptions.data();
+
+	VkPipelineInputAssemblyStateCreateInfo input_assembly = {};
+	input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
+	input_assembly.topology = inTopology == ETopology::Triangle? VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST : VK_PRIMITIVE_TOPOLOGY_LINE_LIST;
+	input_assembly.primitiveRestartEnable = VK_FALSE;
+
+	VkPipelineViewportStateCreateInfo viewport_state = {};
+	viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
+	viewport_state.viewportCount = 1;
+	viewport_state.scissorCount = 1;
+
+	VkPipelineRasterizationStateCreateInfo rasterizer = {};
+	rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
+	rasterizer.depthClampEnable = VK_FALSE;
+	rasterizer.rasterizerDiscardEnable = VK_FALSE;
+	rasterizer.polygonMode = inFillMode == EFillMode::Solid? VK_POLYGON_MODE_FILL : VK_POLYGON_MODE_LINE;
+	rasterizer.lineWidth = 1.0f;
+	rasterizer.cullMode = inCullMode == ECullMode::Backface? VK_CULL_MODE_BACK_BIT : VK_CULL_MODE_FRONT_BIT;
+	rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
+	rasterizer.depthBiasEnable = VK_FALSE;
+
+	VkPipelineMultisampleStateCreateInfo multisampling = {};
+	multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
+	multisampling.sampleShadingEnable = VK_FALSE;
+	multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
+
+	VkPipelineDepthStencilStateCreateInfo depth_stencil = {};
+	depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
+	depth_stencil.depthTestEnable = inDepthTest == EDepthTest::On? VK_TRUE : VK_FALSE;
+	depth_stencil.depthWriteEnable = inDepthTest == EDepthTest::On? VK_TRUE : VK_FALSE;
+	depth_stencil.depthCompareOp = VK_COMPARE_OP_GREATER; // Reverse-Z, greater is closer
+
+	VkPipelineColorBlendAttachmentState color_blend_attachment = {};
+	color_blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
+	switch (inBlendMode)
+	{
+	case EBlendMode::Write:
+		color_blend_attachment.blendEnable = VK_FALSE;
+		break;
+
+	case EBlendMode::AlphaBlend:
+		color_blend_attachment.blendEnable = VK_TRUE;
+		color_blend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
+		color_blend_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
+		color_blend_attachment.colorBlendOp = VK_BLEND_OP_ADD;
+		color_blend_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
+		color_blend_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
+		color_blend_attachment.alphaBlendOp = VK_BLEND_OP_ADD;
+		break;
+	}
+
+	VkPipelineColorBlendStateCreateInfo color_blending = {};
+	color_blending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
+	color_blending.logicOpEnable = VK_FALSE;
+	color_blending.logicOp = VK_LOGIC_OP_COPY;
+	color_blending.attachmentCount = 1;
+	color_blending.pAttachments = &color_blend_attachment;
+
+	VkDynamicState dynamic_states[] = {
+		VK_DYNAMIC_STATE_VIEWPORT,
+		VK_DYNAMIC_STATE_SCISSOR
+	};
+	VkPipelineDynamicStateCreateInfo dynamic_state = {};
+	dynamic_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
+	dynamic_state.dynamicStateCount = std::size(dynamic_states);
+	dynamic_state.pDynamicStates = dynamic_states;
+
+	VkGraphicsPipelineCreateInfo pipeline_info = {};
+	pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
+	pipeline_info.stageCount = std::size(shader_stages);
+	pipeline_info.pStages = shader_stages;
+	pipeline_info.pVertexInputState = &vertex_input_info;
+	pipeline_info.pInputAssemblyState = &input_assembly;
+	pipeline_info.pViewportState = &viewport_state;
+	pipeline_info.pRasterizationState = &rasterizer;
+	pipeline_info.pMultisampleState = &multisampling;
+	pipeline_info.pDepthStencilState = &depth_stencil;
+	pipeline_info.pColorBlendState = &color_blending;
+	pipeline_info.pDynamicState = &dynamic_state;
+	pipeline_info.layout = mRenderer->GetPipelineLayout();
+	pipeline_info.renderPass = inDrawPass == EDrawPass::Normal? mRenderer->GetRenderPass() : mRenderer->GetRenderPassShadow();
+	FatalErrorIfFailed(vkCreateGraphicsPipelines(mRenderer->GetDevice(), VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &mGraphicsPipeline));
+}
+
+PipelineStateVK::~PipelineStateVK()
+{
+	vkDeviceWaitIdle(mRenderer->GetDevice());
+
+	vkDestroyPipeline(mRenderer->GetDevice(), mGraphicsPipeline, nullptr);
+}
+
+void PipelineStateVK::Activate()
+{
+	vkCmdBindPipeline(mRenderer->GetCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, mGraphicsPipeline);
+}

+ 30 - 0
TestFramework/Renderer/VK/PipelineStateVK.h

@@ -0,0 +1,30 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/PipelineState.h>
+#include <Renderer/VK/VertexShaderVK.h>
+#include <Renderer/VK/PixelShaderVK.h>
+
+class RendererVK;
+
+/// Vulkan pipeline state object
+class PipelineStateVK : public PipelineState
+{
+public:
+	/// Constructor
+										PipelineStateVK(RendererVK *inRenderer, const VertexShaderVK *inVertexShader, const EInputDescription *inInputDescription, uint inInputDescriptionCount, const PixelShaderVK *inPixelShader, EDrawPass inDrawPass, EFillMode inFillMode, ETopology inTopology, EDepthTest inDepthTest, EBlendMode inBlendMode, ECullMode inCullMode);
+	virtual								~PipelineStateVK() override;
+
+	/// Make this pipeline state active (any primitives rendered after this will use this state)
+	virtual void						Activate() override;
+
+private:
+	RendererVK *						mRenderer;
+	RefConst<VertexShaderVK>			mVertexShader;
+	RefConst<PixelShaderVK>				mPixelShader;
+
+	VkPipeline							mGraphicsPipeline;
+};

+ 34 - 0
TestFramework/Renderer/VK/PixelShaderVK.h

@@ -0,0 +1,34 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/PixelShader.h>
+
+#include <vulkan/vulkan.h>
+
+/// Pixel shader handle for Vulkan
+class PixelShaderVK : public PixelShader
+{
+public:
+	/// Constructor
+							PixelShaderVK(VkDevice inDevice, VkShaderModule inShaderModule) :
+		mDevice(inDevice),
+		mStageInfo()
+	{
+		mStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
+		mStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
+		mStageInfo.module = inShaderModule;
+		mStageInfo.pName = "main";
+	}
+
+	/// Destructor
+	virtual					~PixelShaderVK() override
+	{
+		vkDestroyShaderModule(mDevice, mStageInfo.module, nullptr);
+	}
+
+	VkDevice				mDevice;
+	VkPipelineShaderStageCreateInfo mStageInfo;
+};

+ 54 - 0
TestFramework/Renderer/VK/RenderInstancesVK.cpp

@@ -0,0 +1,54 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/VK/RenderInstancesVK.h>
+#include <Renderer/VK/RenderPrimitiveVK.h>
+#include <Renderer/VK/FatalErrorIfFailedVK.h>
+
+void RenderInstancesVK::Clear()
+{
+	mRenderer->FreeBuffer(mInstancesBuffer);
+}
+
+void RenderInstancesVK::CreateBuffer(int inNumInstances, int inInstanceSize)
+{
+	Clear();
+
+	mRenderer->CreateBuffer(VkDeviceSize(inNumInstances) * inInstanceSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, mInstancesBuffer);
+}
+
+void *RenderInstancesVK::Lock()
+{
+	void *data;
+	FatalErrorIfFailed(vkMapMemory(mRenderer->GetDevice(), mInstancesBuffer.mMemory, 0, mInstancesBuffer.mSize, 0, &data));
+	return data;
+}
+
+void RenderInstancesVK::Unlock()
+{
+	vkUnmapMemory(mRenderer->GetDevice(), mInstancesBuffer.mMemory);
+}
+
+void RenderInstancesVK::Draw(RenderPrimitive *inPrimitive, int inStartInstance, int inNumInstances) const
+{
+	VkCommandBuffer command_buffer = mRenderer->GetCommandBuffer();
+	RenderPrimitiveVK *primitive = static_cast<RenderPrimitiveVK *>(inPrimitive);
+
+	VkBuffer buffers[] = { primitive->mVertexBuffer.mBuffer, mInstancesBuffer.mBuffer };
+	VkDeviceSize offsets[] = { 0, 0 };
+	vkCmdBindVertexBuffers(command_buffer, 0, 2, buffers, offsets);
+
+	if (primitive->mIndexBuffer.mBuffer == VK_NULL_HANDLE)
+	{
+		vkCmdDraw(command_buffer, primitive->mNumVtxToDraw, inNumInstances, 0, inStartInstance);
+	}
+	else
+	{
+		vkCmdBindIndexBuffer(command_buffer, primitive->mIndexBuffer.mBuffer, 0, VK_INDEX_TYPE_UINT32);
+
+		vkCmdDrawIndexed(command_buffer, primitive->mNumIdxToDraw, inNumInstances, 0, 0, inStartInstance);
+	}
+}

+ 35 - 0
TestFramework/Renderer/VK/RenderInstancesVK.h

@@ -0,0 +1,35 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/VK/RendererVK.h>
+#include <Renderer/RenderInstances.h>
+
+class RenderPrimitive;
+
+/// Vulkan implementation of a render instances object
+class RenderInstancesVK : public RenderInstances
+{
+public:
+	/// Constructor
+							RenderInstancesVK(RendererVK *inRenderer)											: mRenderer(inRenderer) { }
+	virtual					~RenderInstancesVK() 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:
+	RendererVK *			mRenderer;
+
+	BufferVK				mInstancesBuffer;
+};

+ 100 - 0
TestFramework/Renderer/VK/RenderPrimitiveVK.cpp

@@ -0,0 +1,100 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/VK/RenderPrimitiveVK.h>
+#include <Renderer/VK/FatalErrorIfFailedVK.h>
+
+void RenderPrimitiveVK::ReleaseVertexBuffer()
+{
+	mRenderer->FreeBuffer(mVertexBuffer);
+	mVertexBufferDeviceLocal = false;
+
+	RenderPrimitive::ReleaseVertexBuffer();
+}
+
+void RenderPrimitiveVK::ReleaseIndexBuffer()
+{
+	mRenderer->FreeBuffer(mIndexBuffer);
+	mIndexBufferDeviceLocal = false;
+
+	RenderPrimitive::ReleaseIndexBuffer();
+}
+
+void RenderPrimitiveVK::CreateVertexBuffer(int inNumVtx, int inVtxSize, const void *inData)
+{
+	RenderPrimitive::CreateVertexBuffer(inNumVtx, inVtxSize, inData);
+
+	VkDeviceSize size = VkDeviceSize(inNumVtx) * inVtxSize;
+	if (inData != nullptr)
+	{
+		mRenderer->CreateDeviceLocalBuffer(inData, size, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, mVertexBuffer);
+		mVertexBufferDeviceLocal = true;
+	}
+	else
+		mRenderer->CreateBuffer(size, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, mVertexBuffer);
+}
+
+void *RenderPrimitiveVK::LockVertexBuffer()
+{
+	JPH_ASSERT(!mVertexBufferDeviceLocal);
+
+	void *data;
+	FatalErrorIfFailed(vkMapMemory(mRenderer->GetDevice(), mVertexBuffer.mMemory, 0, VkDeviceSize(mNumVtx) * mVtxSize, 0, &data));
+	return data;
+}
+
+void RenderPrimitiveVK::UnlockVertexBuffer()
+{
+	vkUnmapMemory(mRenderer->GetDevice(), mVertexBuffer.mMemory);
+}
+
+void RenderPrimitiveVK::CreateIndexBuffer(int inNumIdx, const uint32 *inData)
+{
+	RenderPrimitive::CreateIndexBuffer(inNumIdx, inData);
+
+	VkDeviceSize size = VkDeviceSize(inNumIdx) * sizeof(uint32);
+	if (inData != nullptr)
+	{
+		mRenderer->CreateDeviceLocalBuffer(inData, size, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, mIndexBuffer);
+		mIndexBufferDeviceLocal = true;
+	}
+	else
+		mRenderer->CreateBuffer(size, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, mIndexBuffer);
+}
+
+uint32 *RenderPrimitiveVK::LockIndexBuffer()
+{
+	JPH_ASSERT(!mIndexBufferDeviceLocal);
+
+	void *data;
+	vkMapMemory(mRenderer->GetDevice(), mIndexBuffer.mMemory, 0, VkDeviceSize(mNumIdx) * sizeof(uint32), 0, &data);
+	return reinterpret_cast<uint32 *>(data);
+}
+
+void RenderPrimitiveVK::UnlockIndexBuffer()
+{
+	vkUnmapMemory(mRenderer->GetDevice(), mIndexBuffer.mMemory);
+}
+
+void RenderPrimitiveVK::Draw() const
+{
+	VkCommandBuffer command_buffer = mRenderer->GetCommandBuffer();
+
+	VkBuffer vertex_buffers[] = { mVertexBuffer.mBuffer };
+	VkDeviceSize offsets[] = { 0 };
+	vkCmdBindVertexBuffers(command_buffer, 0, 1, vertex_buffers, offsets);
+
+	if (mIndexBuffer.mBuffer == VK_NULL_HANDLE)
+	{
+		vkCmdDraw(command_buffer, mNumVtxToDraw, 1, 0, 0);
+	}
+	else
+	{
+		vkCmdBindIndexBuffer(command_buffer, mIndexBuffer.mBuffer, 0, VK_INDEX_TYPE_UINT32);
+
+		vkCmdDrawIndexed(command_buffer, mNumIdxToDraw, 1, 0, 0, 0);
+	}
+}

+ 44 - 0
TestFramework/Renderer/VK/RenderPrimitiveVK.h

@@ -0,0 +1,44 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/RenderPrimitive.h>
+#include <Renderer/VK/RendererVK.h>
+#include <Renderer/VK/BufferVK.h>
+
+/// Vulkan implementation of a render primitive
+class RenderPrimitiveVK : public RenderPrimitive
+{
+public:
+	/// Constructor
+							RenderPrimitiveVK(RendererVK *inRenderer)										: mRenderer(inRenderer) { }
+	virtual					~RenderPrimitiveVK() 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 RenderInstancesVK;
+
+	RendererVK *			mRenderer;
+
+	BufferVK				mVertexBuffer;
+	bool					mVertexBufferDeviceLocal = false;
+
+	BufferVK				mIndexBuffer;
+	bool					mIndexBufferDeviceLocal = false;
+};

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

@@ -0,0 +1,1112 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/VK/RendererVK.h>
+#include <Renderer/VK/RenderPrimitiveVK.h>
+#include <Renderer/VK/RenderInstancesVK.h>
+#include <Renderer/VK/PipelineStateVK.h>
+#include <Renderer/VK/VertexShaderVK.h>
+#include <Renderer/VK/PixelShaderVK.h>
+#include <Renderer/VK/TextureVK.h>
+#include <Renderer/VK/FatalErrorIfFailedVK.h>
+#include <Utils/Log.h>
+#include <Utils/ReadData.h>
+#include <Jolt/Core/Profiler.h>
+#include <Jolt/Core/QuickSort.h>
+#include <Jolt/Core/RTTI.h>
+
+#include <vulkan/vulkan_win32.h>
+
+#ifdef JPH_DEBUG
+
+static VKAPI_ATTR VkBool32 VKAPI_CALL sVulkanDebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT inSeverity, [[maybe_unused]] VkDebugUtilsMessageTypeFlagsEXT inType, const VkDebugUtilsMessengerCallbackDataEXT *inCallbackData, [[maybe_unused]] void *inUserData)
+{
+	Trace("VK: %s", inCallbackData->pMessage);
+	JPH_ASSERT((inSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) == 0);
+	return VK_FALSE;
+}
+
+#endif // JPH_DEBUG
+
+RendererVK::~RendererVK()
+{
+	vkDeviceWaitIdle(mDevice);
+
+	// Destroy the shadow map
+	mShadowMap = nullptr;
+	vkDestroyFramebuffer(mDevice, mShadowFrameBuffer, nullptr);
+
+	// Release constant buffers
+	for (unique_ptr<ConstantBufferVK> &cb : mVertexShaderConstantBufferProjection)
+		cb = nullptr;
+	for (unique_ptr<ConstantBufferVK> &cb : mVertexShaderConstantBufferOrtho)
+		cb = nullptr;
+	for (unique_ptr<ConstantBufferVK> &cb : mPixelShaderConstantBuffer)
+		cb = nullptr;
+
+	// Free all buffers
+	for (BufferCache &bc : mFreedBuffers)
+		for (BufferCache::value_type &vt : bc)
+			for (BufferVK &bvk : vt.second)
+				bvk.Free(mDevice);
+	for (BufferCache::value_type &vt : mBufferCache)
+		for (BufferVK &bvk : vt.second)
+			bvk.Free(mDevice);
+
+	for (VkFence fence : mInFlightFences)
+		vkDestroyFence(mDevice, fence, nullptr);
+
+	for (VkSemaphore semaphore : mRenderFinishedSemaphores) 
+		vkDestroySemaphore(mDevice, semaphore, nullptr);
+	for (VkSemaphore semaphore : mImageAvailableSemaphores) 
+		vkDestroySemaphore(mDevice, semaphore, nullptr);
+
+	vkDestroyCommandPool(mDevice, mCommandPool, nullptr);
+
+	vkDestroyPipelineLayout(mDevice, mPipelineLayout, nullptr);
+
+	vkDestroyRenderPass(mDevice, mRenderPassShadow, nullptr);
+	vkDestroyRenderPass(mDevice, mRenderPass, nullptr);
+
+	vkDestroyDescriptorPool(mDevice, mDescriptorPool, nullptr);
+
+	vkDestroySampler(mDevice, mTextureSamplerShadow, nullptr);
+	vkDestroySampler(mDevice, mTextureSamplerRepeat, nullptr);
+
+	vkDestroyDescriptorSetLayout(mDevice, mDescriptorSetLayoutUBO, nullptr);
+	vkDestroyDescriptorSetLayout(mDevice, mDescriptorSetLayoutTexture, nullptr);
+
+	DestroySwapChain();
+
+	vkDestroySurfaceKHR(mInstance, mSurface, nullptr);
+
+	vkDestroyDevice(mDevice, nullptr);
+
+#ifdef _DEBUG
+	PFN_vkDestroyDebugUtilsMessengerEXT vkDestroyDebugUtilsMessengerEXT = (PFN_vkDestroyDebugUtilsMessengerEXT)(void *)vkGetInstanceProcAddr(mInstance, "vkDestroyDebugUtilsMessengerEXT");
+	if (vkDestroyDebugUtilsMessengerEXT != nullptr)
+		vkDestroyDebugUtilsMessengerEXT(mInstance, mDebugMessenger, nullptr);
+#endif
+
+	 vkDestroyInstance(mInstance, nullptr);
+}
+
+void RendererVK::Initialize()
+{
+	Renderer::Initialize();
+
+	// Flip the sign of the projection matrix
+	mPerspectiveYSign = -1.0f;
+
+	// Required instance extensions
+	Array<const char *> required_instance_extensions;
+	required_instance_extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME);
+	required_instance_extensions.push_back(VK_KHR_WIN32_SURFACE_EXTENSION_NAME);
+
+	// Required device extensions
+	Array<const char *> required_device_extensions;
+	required_device_extensions.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
+
+	// Query supported instance extensions
+	uint32 instance_extension_count = 0;
+	FatalErrorIfFailed(vkEnumerateInstanceExtensionProperties(nullptr, &instance_extension_count, nullptr));
+	Array<VkExtensionProperties> instance_extensions;
+	instance_extensions.resize(instance_extension_count);
+	FatalErrorIfFailed(vkEnumerateInstanceExtensionProperties(nullptr, &instance_extension_count, instance_extensions.data()));
+
+	// Query supported validation layers
+	uint32 validation_layer_count;
+	vkEnumerateInstanceLayerProperties(&validation_layer_count, nullptr);
+	Array<VkLayerProperties> validation_layers(validation_layer_count);
+	vkEnumerateInstanceLayerProperties(&validation_layer_count, validation_layers.data());
+
+	// Create Vulkan instance
+	VkInstanceCreateInfo instance_create_info = {};
+	instance_create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
+
+#ifdef JPH_DEBUG
+	// Enable validation layer if supported
+	const char *desired_validation_layers[] = { "VK_LAYER_KHRONOS_validation" };
+	for (const VkLayerProperties &p : validation_layers)
+		if (strcmp(desired_validation_layers[0], p.layerName) == 0)
+		{
+			instance_create_info.enabledLayerCount = 1;
+			instance_create_info.ppEnabledLayerNames = desired_validation_layers;
+			break;
+		}
+
+	// Setup debug messenger callback if the extension is supported
+	VkDebugUtilsMessengerCreateInfoEXT messenger_create_info = {};
+	for (const VkExtensionProperties &ext : instance_extensions)
+		if (strcmp(VK_EXT_DEBUG_UTILS_EXTENSION_NAME, ext.extensionName) == 0)
+		{
+			messenger_create_info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
+			messenger_create_info.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
+			messenger_create_info.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_DEVICE_ADDRESS_BINDING_BIT_EXT;
+			messenger_create_info.pfnUserCallback = sVulkanDebugCallback;
+			instance_create_info.pNext = &messenger_create_info;
+			required_instance_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
+			break;
+		}
+#endif
+
+	instance_create_info.enabledExtensionCount = (uint32)required_instance_extensions.size();
+	instance_create_info.ppEnabledExtensionNames = required_instance_extensions.data();
+	FatalErrorIfFailed(vkCreateInstance(&instance_create_info, nullptr, &mInstance));
+
+#ifdef JPH_DEBUG
+	// Finalize debug messenger callback
+	PFN_vkCreateDebugUtilsMessengerEXT vkCreateDebugUtilsMessengerEXT = (PFN_vkCreateDebugUtilsMessengerEXT)(std::uintptr_t)vkGetInstanceProcAddr(mInstance, "vkCreateDebugUtilsMessengerEXT");
+	if (vkCreateDebugUtilsMessengerEXT != nullptr)
+		FatalErrorIfFailed(vkCreateDebugUtilsMessengerEXT(mInstance, &messenger_create_info, nullptr, &mDebugMessenger));
+#endif
+
+	// Create surface
+	VkWin32SurfaceCreateInfoKHR surface_create_info = {};
+	surface_create_info.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
+	surface_create_info.hwnd = mhWnd;
+	surface_create_info.hinstance = GetModuleHandle(nullptr);
+	FatalErrorIfFailed(vkCreateWin32SurfaceKHR(mInstance, &surface_create_info, nullptr, &mSurface));
+
+	// Select device
+	uint32 device_count = 0;
+	FatalErrorIfFailed(vkEnumeratePhysicalDevices(mInstance, &device_count, nullptr));
+	Array<VkPhysicalDevice> devices;
+	devices.resize(device_count);
+	FatalErrorIfFailed(vkEnumeratePhysicalDevices(mInstance, &device_count, devices.data()));
+	struct Device
+	{
+		VkPhysicalDevice		mPhysicalDevice;
+		String					mName;
+		VkSurfaceFormatKHR		mFormat;
+		uint32					mGraphicsQueueIndex;
+		uint32					mPresentQueueIndex;
+		int						mScore;
+	};
+	Array<Device> available_devices;
+	for (VkPhysicalDevice device : devices)
+	{
+		// Get device properties
+		VkPhysicalDeviceProperties properties;
+		vkGetPhysicalDeviceProperties(device, &properties);
+
+		// Test if it is an appropriate type
+		int score = 0;
+		switch (properties.deviceType)
+		{
+		case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU:
+			score = 30;
+			break;
+		case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU:
+			score = 20;
+			break;
+		case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU:
+			score = 10;
+			break;
+		case VK_PHYSICAL_DEVICE_TYPE_OTHER:
+		case VK_PHYSICAL_DEVICE_TYPE_CPU:
+		case VK_PHYSICAL_DEVICE_TYPE_MAX_ENUM:
+			continue;
+		}
+
+		// Check if the device supports all our required extensions
+		uint32 device_extension_count;
+		vkEnumerateDeviceExtensionProperties(device, nullptr, &device_extension_count, nullptr);
+		Array<VkExtensionProperties> available_extensions;
+		available_extensions.resize(device_extension_count);
+		vkEnumerateDeviceExtensionProperties(device, nullptr, &device_extension_count, available_extensions.data());
+		int found_extensions = 0;
+		for (const char *required_device_extension : required_device_extensions)
+			for (const VkExtensionProperties &ext : available_extensions)
+				if (strcmp(required_device_extension, ext.extensionName) == 0)
+				{
+					found_extensions++;
+					break;
+				}
+		if (found_extensions != int(required_device_extensions.size()))
+			continue;
+
+		// Find the right queues
+		uint32 queue_family_count = 0;
+		vkGetPhysicalDeviceQueueFamilyProperties(device, &queue_family_count, nullptr);
+		Array<VkQueueFamilyProperties> queue_families;
+		queue_families.resize(queue_family_count);
+		vkGetPhysicalDeviceQueueFamilyProperties(device, &queue_family_count, queue_families.data());
+		uint32 graphics_queue = ~uint32(0);
+		uint32 present_queue = ~uint32(0);
+		for (uint32 i = 0; i < uint32(queue_families.size()); ++i)
+		{
+			if (queue_families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)
+				graphics_queue = i;
+
+			VkBool32 present_support = false;
+			vkGetPhysicalDeviceSurfaceSupportKHR(device, i, mSurface, &present_support);
+			if (present_support)
+				present_queue = i;
+
+			if (graphics_queue != ~uint32(0) && present_queue != ~uint32(0))
+				break;
+		}
+		if (graphics_queue == ~uint32(0) || present_queue == ~uint32(0))
+			continue;
+
+		// Select surface format
+		VkSurfaceFormatKHR selected_format = SelectFormat(device);
+		if (selected_format.format == VK_FORMAT_UNDEFINED)
+			continue;
+
+		// Add the device
+		available_devices.push_back({ device, properties.deviceName, selected_format, graphics_queue, present_queue, score });
+	}
+	if (available_devices.empty())
+		FatalError("No Vulkan device found!");
+	QuickSort(available_devices.begin(), available_devices.end(), [](const Device &inLHS, const Device &inRHS) {
+		return inLHS.mScore > inRHS.mScore;
+	});
+	const Device &selected_device = available_devices[0];
+	Trace("Selected device: %s", selected_device.mName.c_str());
+	mPhysicalDevice = selected_device.mPhysicalDevice;
+
+	// Get memory properties
+	vkGetPhysicalDeviceMemoryProperties(mPhysicalDevice, &mMemoryProperties);
+
+	// Create device
+	float queue_priority = 1.0f;
+	VkDeviceQueueCreateInfo queue_create_info[2] = {};
+	for (size_t i = 0; i < std::size(queue_create_info); ++i)
+	{
+		queue_create_info[i].sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
+		queue_create_info[i].queueCount = 1;
+		queue_create_info[i].pQueuePriorities = &queue_priority;
+	}
+	queue_create_info[0].queueFamilyIndex = selected_device.mGraphicsQueueIndex;
+	queue_create_info[1].queueFamilyIndex = selected_device.mPresentQueueIndex;
+	VkPhysicalDeviceFeatures device_features = {};
+	device_features.fillModeNonSolid = VK_TRUE;
+	VkDeviceCreateInfo device_create_info = {};
+	device_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
+	device_create_info.queueCreateInfoCount = selected_device.mGraphicsQueueIndex != selected_device.mPresentQueueIndex? 2 : 1;
+	device_create_info.pQueueCreateInfos = queue_create_info;
+	device_create_info.enabledLayerCount = instance_create_info.enabledLayerCount;
+	device_create_info.ppEnabledLayerNames = instance_create_info.ppEnabledLayerNames;
+	device_create_info.enabledExtensionCount = uint32(required_device_extensions.size());
+	device_create_info.ppEnabledExtensionNames = required_device_extensions.data();
+	device_create_info.pEnabledFeatures = &device_features;
+	FatalErrorIfFailed(vkCreateDevice(selected_device.mPhysicalDevice, &device_create_info, nullptr, &mDevice));
+
+	// Get the queues
+	mGraphicsQueueIndex = selected_device.mGraphicsQueueIndex;
+	mPresentQueueIndex = selected_device.mPresentQueueIndex;
+	vkGetDeviceQueue(mDevice, mGraphicsQueueIndex, 0, &mGraphicsQueue);
+	vkGetDeviceQueue(mDevice, mPresentQueueIndex, 0, &mPresentQueue);
+
+	VkCommandPoolCreateInfo pool_info = {};
+	pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
+	pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
+	pool_info.queueFamilyIndex = selected_device.mGraphicsQueueIndex;
+	FatalErrorIfFailed(vkCreateCommandPool(mDevice, &pool_info, nullptr, &mCommandPool));
+
+	VkCommandBufferAllocateInfo command_buffer_info = {};
+	command_buffer_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
+	command_buffer_info.commandPool = mCommandPool;
+	command_buffer_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
+	command_buffer_info.commandBufferCount = 1;
+	for (uint32 i = 0; i < cFrameCount; ++i)
+		FatalErrorIfFailed(vkAllocateCommandBuffers(mDevice, &command_buffer_info, &mCommandBuffers[i]));
+
+	VkSemaphoreCreateInfo semaphore_info = {};
+	semaphore_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
+	for (uint32 i = 0; i < cFrameCount; ++i)
+	{
+		FatalErrorIfFailed(vkCreateSemaphore(mDevice, &semaphore_info, nullptr, &mImageAvailableSemaphores[i]));
+		FatalErrorIfFailed(vkCreateSemaphore(mDevice, &semaphore_info, nullptr, &mRenderFinishedSemaphores[i]));
+	}
+
+	VkFenceCreateInfo fence_info = {};
+	fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
+	fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT;
+	for (uint32 i = 0; i < cFrameCount; ++i)
+		FatalErrorIfFailed(vkCreateFence(mDevice, &fence_info, nullptr, &mInFlightFences[i]));
+
+	// Create constant buffer. One per frame to avoid overwriting the constant buffer while the GPU is still using it.
+	for (uint n = 0; n < cFrameCount; ++n)
+	{
+		mVertexShaderConstantBufferProjection[n] = CreateConstantBuffer(sizeof(VertexShaderConstantBuffer));
+		mVertexShaderConstantBufferOrtho[n] = CreateConstantBuffer(sizeof(VertexShaderConstantBuffer));
+		mPixelShaderConstantBuffer[n] = CreateConstantBuffer(sizeof(PixelShaderConstantBuffer));
+	}
+
+	// Create descriptor set layout for the uniform buffers
+	VkDescriptorSetLayoutBinding ubo_layout_binding[2] = {};
+	ubo_layout_binding[0].binding = 0;
+	ubo_layout_binding[0].descriptorCount = 1;
+	ubo_layout_binding[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
+	ubo_layout_binding[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
+	ubo_layout_binding[1].binding = 1;
+	ubo_layout_binding[1].descriptorCount = 1;
+	ubo_layout_binding[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
+	ubo_layout_binding[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
+	VkDescriptorSetLayoutCreateInfo ubo_dsl = {};
+	ubo_dsl.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
+	ubo_dsl.bindingCount = std::size(ubo_layout_binding);
+	ubo_dsl.pBindings = ubo_layout_binding;
+	FatalErrorIfFailed(vkCreateDescriptorSetLayout(mDevice, &ubo_dsl, nullptr, &mDescriptorSetLayoutUBO));
+
+	// Create descriptor set layout for the texture binding
+	VkDescriptorSetLayoutBinding texture_layout_binding = {};
+	texture_layout_binding.binding = 0;
+	texture_layout_binding.descriptorCount = 1;
+	texture_layout_binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
+	texture_layout_binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
+	VkDescriptorSetLayoutCreateInfo texture_dsl = {};
+	texture_dsl.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
+	texture_dsl.bindingCount = 1;
+	texture_dsl.pBindings = &texture_layout_binding;
+	FatalErrorIfFailed(vkCreateDescriptorSetLayout(mDevice, &texture_dsl, nullptr, &mDescriptorSetLayoutTexture));
+
+	// Create pipeline layout
+	VkPipelineLayoutCreateInfo pipeline_layout = {};
+	VkDescriptorSetLayout layout_handles[] = { mDescriptorSetLayoutUBO, mDescriptorSetLayoutTexture };
+	pipeline_layout.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
+	pipeline_layout.setLayoutCount = std::size(layout_handles);
+	pipeline_layout.pSetLayouts = layout_handles;
+	pipeline_layout.pushConstantRangeCount = 0;
+	FatalErrorIfFailed(vkCreatePipelineLayout(mDevice, &pipeline_layout, nullptr, &mPipelineLayout));
+
+	// Create descriptor pool
+	VkDescriptorPoolSize descriptor_pool_size = {};
+	descriptor_pool_size.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
+	descriptor_pool_size.descriptorCount = cFrameCount;
+	VkDescriptorPoolCreateInfo descriptor_info = {};
+	descriptor_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
+	descriptor_info.poolSizeCount = 1;
+	descriptor_info.pPoolSizes = &descriptor_pool_size;
+	descriptor_info.maxSets = 256;
+	FatalErrorIfFailed(vkCreateDescriptorPool(mDevice, &descriptor_info, nullptr, &mDescriptorPool));
+
+	// Allocate descriptor sets for 3d rendering
+	Array<VkDescriptorSetLayout> layouts(cFrameCount, mDescriptorSetLayoutUBO);
+	VkDescriptorSetAllocateInfo descriptor_set_alloc_info = {};
+	descriptor_set_alloc_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
+	descriptor_set_alloc_info.descriptorPool = mDescriptorPool;
+	descriptor_set_alloc_info.descriptorSetCount = cFrameCount;
+	descriptor_set_alloc_info.pSetLayouts = layouts.data();
+	FatalErrorIfFailed(vkAllocateDescriptorSets(mDevice, &descriptor_set_alloc_info, mDescriptorSets));
+	for (uint i = 0; i < cFrameCount; i++)
+	{
+		VkDescriptorBufferInfo vs_buffer_info = {};
+		vs_buffer_info.buffer = mVertexShaderConstantBufferProjection[i]->GetBuffer();
+		vs_buffer_info.range = sizeof(VertexShaderConstantBuffer);
+
+		VkDescriptorBufferInfo ps_buffer_info = {};
+		ps_buffer_info.buffer = mPixelShaderConstantBuffer[i]->GetBuffer();
+		ps_buffer_info.range = sizeof(PixelShaderConstantBuffer);
+
+		VkWriteDescriptorSet descriptor_write[2] = {};
+		descriptor_write[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+		descriptor_write[0].dstSet = mDescriptorSets[i];
+		descriptor_write[0].dstBinding = 0;
+		descriptor_write[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
+		descriptor_write[0].descriptorCount = 1;
+		descriptor_write[0].pBufferInfo = &vs_buffer_info;
+		descriptor_write[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+		descriptor_write[1].dstSet = mDescriptorSets[i];
+		descriptor_write[1].dstBinding = 1;
+		descriptor_write[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
+		descriptor_write[1].descriptorCount = 1;
+		descriptor_write[1].pBufferInfo = &ps_buffer_info;
+		vkUpdateDescriptorSets(mDevice, 2, descriptor_write, 0, nullptr);
+	}
+
+	// Allocate descriptor sets for 2d rendering
+	FatalErrorIfFailed(vkAllocateDescriptorSets(mDevice, &descriptor_set_alloc_info, mDescriptorSetsOrtho));
+	for (uint i = 0; i < cFrameCount; i++)
+	{
+		VkDescriptorBufferInfo vs_buffer_info = {};
+		vs_buffer_info.buffer = mVertexShaderConstantBufferOrtho[i]->GetBuffer();
+		vs_buffer_info.range = sizeof(VertexShaderConstantBuffer);
+
+		VkWriteDescriptorSet descriptor_write = {};
+		descriptor_write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+		descriptor_write.dstSet = mDescriptorSetsOrtho[i];
+		descriptor_write.dstBinding = 0;
+		descriptor_write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
+		descriptor_write.descriptorCount = 1;
+		descriptor_write.pBufferInfo = &vs_buffer_info;
+		vkUpdateDescriptorSets(mDevice, 1, &descriptor_write, 0, nullptr);
+	}
+
+	// Create regular texture sampler
+	VkSamplerCreateInfo sampler_info = {};
+	sampler_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
+	sampler_info.magFilter = VK_FILTER_LINEAR;
+	sampler_info.minFilter = VK_FILTER_LINEAR;
+	sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+	sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+	sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+	sampler_info.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
+	sampler_info.unnormalizedCoordinates = VK_FALSE;
+	sampler_info.compareEnable = VK_FALSE;
+	sampler_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
+	FatalErrorIfFailed(vkCreateSampler(mDevice, &sampler_info, nullptr, &mTextureSamplerRepeat));
+
+	// Create sampler for shadow maps
+	sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+	sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+	sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+	sampler_info.compareEnable = VK_TRUE;
+	sampler_info.compareOp = VK_COMPARE_OP_GREATER_OR_EQUAL;
+	FatalErrorIfFailed(vkCreateSampler(mDevice, &sampler_info, nullptr, &mTextureSamplerShadow));
+
+	{
+		// Create shadow render pass
+		VkAttachmentDescription shadowmap_attachment = {};
+		shadowmap_attachment.format = FindDepthFormat();
+		shadowmap_attachment.samples = VK_SAMPLE_COUNT_1_BIT;
+		shadowmap_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
+		shadowmap_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
+		shadowmap_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
+		shadowmap_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
+		shadowmap_attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+		shadowmap_attachment.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+		VkAttachmentReference shadowmap_attachment_ref = {};
+		shadowmap_attachment_ref.attachment = 0;
+		shadowmap_attachment_ref.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
+		VkSubpassDescription subpass_shadow = {};
+		subpass_shadow.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
+		subpass_shadow.pDepthStencilAttachment = &shadowmap_attachment_ref;
+		VkSubpassDependency dependencies_shadow = {};
+		dependencies_shadow.srcSubpass = VK_SUBPASS_EXTERNAL;
+		dependencies_shadow.dstSubpass = 0;
+		dependencies_shadow.srcStageMask = VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
+		dependencies_shadow.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
+		dependencies_shadow.dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
+		dependencies_shadow.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
+		VkRenderPassCreateInfo render_pass_shadow = {};
+		render_pass_shadow.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
+		render_pass_shadow.attachmentCount = 1;
+		render_pass_shadow.pAttachments = &shadowmap_attachment;
+		render_pass_shadow.subpassCount = 1;
+		render_pass_shadow.pSubpasses = &subpass_shadow;
+		render_pass_shadow.dependencyCount = 1;
+		render_pass_shadow.pDependencies = &dependencies_shadow;
+		FatalErrorIfFailed(vkCreateRenderPass(mDevice, &render_pass_shadow, nullptr, &mRenderPassShadow));
+	}
+
+	// Create depth only texture (no color buffer, as seen from light)
+	mShadowMap = new TextureVK(this, cShadowMapSize, cShadowMapSize);
+
+	// Create frame buffer for the shadow pass
+	VkImageView attachments[] = { mShadowMap->GetImageView() };
+	VkFramebufferCreateInfo frame_buffer_info = {};
+	frame_buffer_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
+	frame_buffer_info.renderPass = mRenderPassShadow;
+	frame_buffer_info.attachmentCount = std::size(attachments);
+	frame_buffer_info.pAttachments = attachments;
+	frame_buffer_info.width = cShadowMapSize;
+	frame_buffer_info.height = cShadowMapSize;
+	frame_buffer_info.layers = 1;
+	FatalErrorIfFailed(vkCreateFramebuffer(mDevice, &frame_buffer_info, nullptr, &mShadowFrameBuffer));
+
+	{
+		// Create normal render pass
+		VkAttachmentDescription attachments_normal[2] = {};
+		VkAttachmentDescription &color_attachment = attachments_normal[0];
+		color_attachment.format = selected_device.mFormat.format;
+		color_attachment.samples = VK_SAMPLE_COUNT_1_BIT;
+		color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
+		color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
+		color_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
+		color_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
+		color_attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+		color_attachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
+		VkAttachmentReference color_attachment_ref = {};
+		color_attachment_ref.attachment = 0;
+		color_attachment_ref.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+		VkAttachmentDescription &depth_attachment = attachments_normal[1];
+		depth_attachment.format = FindDepthFormat();
+		depth_attachment.samples = VK_SAMPLE_COUNT_1_BIT;
+		depth_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
+		depth_attachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
+		depth_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
+		depth_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
+		depth_attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+		depth_attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
+		VkAttachmentReference depth_attachment_ref = {};
+		depth_attachment_ref.attachment = 1;
+		depth_attachment_ref.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
+		VkSubpassDescription subpass_normal = {};
+		subpass_normal.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
+		subpass_normal.colorAttachmentCount = 1;
+		subpass_normal.pColorAttachments = &color_attachment_ref;
+		subpass_normal.pDepthStencilAttachment = &depth_attachment_ref;
+		VkSubpassDependency dependencies_normal = {};
+		dependencies_normal.srcSubpass = VK_SUBPASS_EXTERNAL;
+		dependencies_normal.dstSubpass = 0;
+		dependencies_normal.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
+		dependencies_normal.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
+		dependencies_normal.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
+		dependencies_normal.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT;
+		VkRenderPassCreateInfo render_pass_normal = {};
+		render_pass_normal.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
+		render_pass_normal.attachmentCount = std::size(attachments_normal);
+		render_pass_normal.pAttachments = attachments_normal;
+		render_pass_normal.subpassCount = 1;
+		render_pass_normal.pSubpasses = &subpass_normal;
+		render_pass_normal.dependencyCount = 1;
+		render_pass_normal.pDependencies = &dependencies_normal;
+		FatalErrorIfFailed(vkCreateRenderPass(mDevice, &render_pass_normal, nullptr, &mRenderPass));
+	}
+
+	// Create the swap chain
+	CreateSwapChain(mPhysicalDevice);
+}
+
+VkSurfaceFormatKHR RendererVK::SelectFormat(VkPhysicalDevice inDevice)
+{
+	uint32 format_count;
+	vkGetPhysicalDeviceSurfaceFormatsKHR(inDevice, mSurface, &format_count, nullptr);
+	if (format_count == 0)
+		return { VK_FORMAT_UNDEFINED, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR };
+	Array<VkSurfaceFormatKHR> formats;
+	formats.resize(format_count);
+	vkGetPhysicalDeviceSurfaceFormatsKHR(inDevice, mSurface, &format_count, formats.data());
+
+	// Select BGRA8 UNORM format if available, otherwise the 1st format
+	for (const VkSurfaceFormatKHR &format : formats)
+		if (format.format == VK_FORMAT_B8G8R8A8_UNORM && format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
+			return format;
+	return formats[0];
+}
+
+VkFormat RendererVK::FindDepthFormat()
+{
+	VkFormat candidates[] = { VK_FORMAT_D32_SFLOAT, VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D24_UNORM_S8_UINT };
+	for (VkFormat format : candidates)
+	{
+		VkFormatProperties props;
+		vkGetPhysicalDeviceFormatProperties(mPhysicalDevice, format, &props);
+
+		if ((props.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) == VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)
+			return format;
+	}
+
+	FatalError("Failed to find format!");
+}
+
+void RendererVK::CreateSwapChain(VkPhysicalDevice inDevice)
+{
+	// Select the format
+	VkSurfaceFormatKHR format = SelectFormat(inDevice);
+	mSwapChainImageFormat = format.format;
+
+	// Determine swap chain extent
+	VkSurfaceCapabilitiesKHR capabilities;
+	vkGetPhysicalDeviceSurfaceCapabilitiesKHR(inDevice, mSurface, &capabilities);
+	mSwapChainExtent = capabilities.currentExtent;
+	if (mSwapChainExtent.width == UINT32_MAX || mSwapChainExtent.height == UINT32_MAX)
+		mSwapChainExtent = { uint32(mWindowWidth), uint32(mWindowHeight) };
+	mSwapChainExtent.width = Clamp(mSwapChainExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width);
+	mSwapChainExtent.height = Clamp(mSwapChainExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height);
+
+	// Early out if our window has been minimized
+	if (mSwapChainExtent.width == 0 || mSwapChainExtent.height == 0)
+		return;
+
+	// Create the swap chain
+	uint32 image_count = min(capabilities.minImageCount + 1, capabilities.maxImageCount);
+	VkSwapchainCreateInfoKHR swapchain_create_info = {};
+	swapchain_create_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
+	swapchain_create_info.surface = mSurface;
+	swapchain_create_info.minImageCount = image_count;
+	swapchain_create_info.imageFormat = format.format;
+	swapchain_create_info.imageColorSpace = format.colorSpace;
+	swapchain_create_info.imageExtent = mSwapChainExtent;
+	swapchain_create_info.imageArrayLayers = 1;
+	swapchain_create_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
+	uint32 queue_family_indices[] = { mGraphicsQueueIndex, mPresentQueueIndex };
+	if (mGraphicsQueueIndex != mPresentQueueIndex)
+	{
+		swapchain_create_info.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
+		swapchain_create_info.queueFamilyIndexCount = 2;
+		swapchain_create_info.pQueueFamilyIndices = queue_family_indices;
+	}
+	else
+	{
+		swapchain_create_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
+	}
+	swapchain_create_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
+	swapchain_create_info.preTransform = capabilities.currentTransform;
+	swapchain_create_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
+	swapchain_create_info.presentMode = VK_PRESENT_MODE_FIFO_KHR;
+	swapchain_create_info.clipped = VK_TRUE;
+	FatalErrorIfFailed(vkCreateSwapchainKHR(mDevice, &swapchain_create_info, nullptr, &mSwapChain));
+
+	// Get the swap chain images
+	mSwapChainImages.resize(image_count);
+	vkGetSwapchainImagesKHR(mDevice, mSwapChain, &image_count, mSwapChainImages.data());
+
+	// Create image views
+	mSwapChainImageViews.resize(image_count);
+	for (uint32 i = 0; i < image_count; ++i)
+		mSwapChainImageViews[i] = CreateImageView(mSwapChainImages[i], mSwapChainImageFormat, VK_IMAGE_ASPECT_COLOR_BIT);
+
+	// Create depth buffer
+	VkFormat depth_format = FindDepthFormat();
+	CreateImage(mSwapChainExtent.width, mSwapChainExtent.height, depth_format, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, mDepthImage, mDepthImageMemory);
+	mDepthImageView = CreateImageView(mDepthImage, depth_format, VK_IMAGE_ASPECT_DEPTH_BIT);
+
+	// Create frame buffers for the normal pass
+	mSwapChainFramebuffers.resize(image_count);
+	for (size_t i = 0; i < mSwapChainFramebuffers.size(); i++)
+	{
+		VkImageView attachments[] = { mSwapChainImageViews[i], mDepthImageView };
+		VkFramebufferCreateInfo frame_buffer_info = {};
+		frame_buffer_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
+		frame_buffer_info.renderPass = mRenderPass;
+		frame_buffer_info.attachmentCount = std::size(attachments);
+		frame_buffer_info.pAttachments = attachments;
+		frame_buffer_info.width = mSwapChainExtent.width;
+		frame_buffer_info.height = mSwapChainExtent.height;
+		frame_buffer_info.layers = 1;
+		FatalErrorIfFailed(vkCreateFramebuffer(mDevice, &frame_buffer_info, nullptr, &mSwapChainFramebuffers[i]));
+	}
+}
+
+void RendererVK::DestroySwapChain()
+{
+	// Destroy depth buffer
+	if (mDepthImageView != VK_NULL_HANDLE)
+	{
+		vkDestroyImageView(mDevice, mDepthImageView, nullptr);
+		vkDestroyImage(mDevice, mDepthImage, nullptr);
+		vkFreeMemory(mDevice, mDepthImageMemory, nullptr);
+	}
+
+	for (VkFramebuffer frame_buffer : mSwapChainFramebuffers)
+		vkDestroyFramebuffer(mDevice, frame_buffer, nullptr);
+	mSwapChainFramebuffers.clear();
+
+	for (VkImageView view : mSwapChainImageViews)
+		vkDestroyImageView(mDevice, view, nullptr);
+	mSwapChainImageViews.clear();
+
+	if (mSwapChain != nullptr)
+	{
+		vkDestroySwapchainKHR(mDevice, mSwapChain, nullptr);
+		mSwapChain = nullptr;
+	}
+}
+
+void RendererVK::OnWindowResize()
+{
+	Renderer::OnWindowResize();
+
+	vkDeviceWaitIdle(mDevice);
+	DestroySwapChain();
+	CreateSwapChain(mPhysicalDevice);
+}
+
+void RendererVK::BeginFrame(const CameraState &inCamera, float inWorldScale)
+{
+	JPH_PROFILE_FUNCTION();
+
+	Renderer::BeginFrame(inCamera, inWorldScale);
+
+	// If we have no swap chain, bail out
+	if (mSwapChain == nullptr)
+		return;
+
+	// Update frame index
+	mFrameIndex = (mFrameIndex + 1) % cFrameCount;
+
+	// Wait for this frame to complete
+	vkWaitForFences(mDevice, 1, &mInFlightFences[mFrameIndex], VK_TRUE, UINT64_MAX);
+
+	VkResult result = vkAcquireNextImageKHR(mDevice, mSwapChain, UINT64_MAX, mImageAvailableSemaphores[mFrameIndex], VK_NULL_HANDLE, &mImageIndex);
+	if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR)
+	{
+		vkDeviceWaitIdle(mDevice);
+		DestroySwapChain();
+		CreateSwapChain(mPhysicalDevice);
+		if (mSwapChain == nullptr)
+			return;
+		result = vkAcquireNextImageKHR(mDevice, mSwapChain, UINT64_MAX, mImageAvailableSemaphores[mFrameIndex], VK_NULL_HANDLE, &mImageIndex);
+	}
+	FatalErrorIfFailed(result);
+
+	// Free buffers that weren't used this frame
+	for (BufferCache::value_type &vt : mBufferCache)
+		for (BufferVK &bvk : vt.second)
+			bvk.Free(mDevice);
+	mBufferCache.clear();
+
+	// Recycle the buffers that were freed
+	mBufferCache.swap(mFreedBuffers[mFrameIndex]);
+	
+	vkResetFences(mDevice, 1, &mInFlightFences[mFrameIndex]);
+
+	VkCommandBuffer command_buffer = GetCommandBuffer();
+	FatalErrorIfFailed(vkResetCommandBuffer(command_buffer, 0));
+
+	VkCommandBufferBeginInfo command_buffer_begin_info = {};
+	command_buffer_begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
+	FatalErrorIfFailed(vkBeginCommandBuffer(command_buffer, &command_buffer_begin_info));
+
+	// Begin the shadow pass
+	VkClearValue clear_value;
+	clear_value.depthStencil = { 0.0f, 0 };
+	VkRenderPassBeginInfo render_pass_begin_info = {};
+	render_pass_begin_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
+	render_pass_begin_info.renderPass = mRenderPassShadow;
+	render_pass_begin_info.framebuffer = mShadowFrameBuffer;
+	render_pass_begin_info.renderArea.extent = { cShadowMapSize, cShadowMapSize };
+	render_pass_begin_info.clearValueCount = 1;
+	render_pass_begin_info.pClearValues = &clear_value;
+	vkCmdBeginRenderPass(command_buffer, &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
+
+	// Set constants for vertex shader in projection mode
+	VertexShaderConstantBuffer *vs = mVertexShaderConstantBufferProjection[mFrameIndex]->Map<VertexShaderConstantBuffer>();
+	*vs = mVSBuffer;
+	mVertexShaderConstantBufferProjection[mFrameIndex]->Unmap();
+
+	// Set constants for vertex shader in ortho mode
+	vs = mVertexShaderConstantBufferOrtho[mFrameIndex]->Map<VertexShaderConstantBuffer>();
+	*vs = mVSBufferOrtho;
+	mVertexShaderConstantBufferOrtho[mFrameIndex]->Unmap();
+
+	// Set constants for pixel shader
+	PixelShaderConstantBuffer *ps = mPixelShaderConstantBuffer[mFrameIndex]->Map<PixelShaderConstantBuffer>();
+	*ps = mPSBuffer;
+	mPixelShaderConstantBuffer[mFrameIndex]->Unmap();
+
+	// Set the view port and scissor rect to the shadow map size
+	UpdateViewPortAndScissorRect(cShadowMapSize, cShadowMapSize);
+
+	// Switch to 3d projection mode
+	SetProjectionMode();
+}
+
+void RendererVK::EndShadowPass()
+{
+	VkCommandBuffer command_buffer = GetCommandBuffer();
+
+	// End the shadow pass
+	vkCmdEndRenderPass(command_buffer);
+
+	// Begin the normal render pass
+	VkClearValue clear_values[2];
+	clear_values[0].color = {{ 0.098f, 0.098f, 0.439f, 1.000f }};
+	clear_values[1].depthStencil = { 0.0f, 0 }; // Reverse-Z clears to 0
+	VkRenderPassBeginInfo render_pass_begin_info = {};
+	render_pass_begin_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
+	render_pass_begin_info.renderPass = mRenderPass;
+	JPH_ASSERT(mImageIndex < mSwapChainFramebuffers.size());
+	render_pass_begin_info.framebuffer = mSwapChainFramebuffers[mImageIndex];
+	render_pass_begin_info.renderArea.extent = mSwapChainExtent;
+	render_pass_begin_info.clearValueCount = std::size(clear_values);
+	render_pass_begin_info.pClearValues = clear_values;
+	vkCmdBeginRenderPass(command_buffer, &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
+
+	// Set the view port and scissor rect to the screen size
+	UpdateViewPortAndScissorRect(mSwapChainExtent.width, mSwapChainExtent.height);
+}
+
+void RendererVK::EndFrame()
+{
+	JPH_PROFILE_FUNCTION();
+
+	// If we have no swap chain, bail out
+	if (mSwapChain == nullptr)
+	{
+		Renderer::EndFrame();
+		return;
+	}
+
+	VkCommandBuffer command_buffer = GetCommandBuffer();
+	vkCmdEndRenderPass(command_buffer);
+
+	FatalErrorIfFailed(vkEndCommandBuffer(command_buffer));
+
+	VkSemaphore wait_semaphores[] = { mImageAvailableSemaphores[mFrameIndex] };
+	VkSemaphore signal_semaphores[] = { mRenderFinishedSemaphores[mFrameIndex] };
+	VkPipelineStageFlags wait_stages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT };
+	VkSubmitInfo submit_info = {};
+	submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
+	submit_info.waitSemaphoreCount = 1;
+	submit_info.pWaitSemaphores = wait_semaphores;
+	submit_info.pWaitDstStageMask = wait_stages;
+	submit_info.commandBufferCount = 1;
+	submit_info.pCommandBuffers = &command_buffer;
+	submit_info.signalSemaphoreCount = 1;
+	submit_info.pSignalSemaphores = signal_semaphores;
+	FatalErrorIfFailed(vkQueueSubmit(mGraphicsQueue, 1, &submit_info, mInFlightFences[mFrameIndex]));
+
+	VkSwapchainKHR swap_chains[] = { mSwapChain };
+	VkPresentInfoKHR present_info = {};
+	present_info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
+	present_info.waitSemaphoreCount = 1;
+	present_info.pWaitSemaphores = signal_semaphores;
+	present_info.swapchainCount = 1;
+	present_info.pSwapchains = swap_chains;
+	present_info.pImageIndices = &mImageIndex;
+	vkQueuePresentKHR(mPresentQueue, &present_info);
+
+	Renderer::EndFrame();
+}
+
+void RendererVK::SetProjectionMode()
+{
+	JPH_ASSERT(mInFrame);
+
+	// Bind descriptor set for 3d rendering
+	vkCmdBindDescriptorSets(GetCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, mPipelineLayout, 0, 1, &mDescriptorSets[mFrameIndex], 0, nullptr);
+}
+
+void RendererVK::SetOrthoMode()
+{
+	JPH_ASSERT(mInFrame);
+
+	// Bind descriptor set for 2d rendering
+	vkCmdBindDescriptorSets(GetCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, mPipelineLayout, 0, 1, &mDescriptorSetsOrtho[mFrameIndex], 0, nullptr);
+}
+
+Ref<Texture> RendererVK::CreateTexture(const Surface *inSurface)
+{
+	return new TextureVK(this, inSurface);
+}
+
+Ref<VertexShader> RendererVK::CreateVertexShader(const char *inFileName)
+{
+	Array<uint8> data = ReadData((String(inFileName) + ".vert.spv").c_str());
+
+	VkShaderModuleCreateInfo create_info = {};
+	create_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
+	create_info.codeSize = data.size();
+	create_info.pCode = reinterpret_cast<const uint32 *>(data.data());
+	VkShaderModule shader_module;
+	FatalErrorIfFailed(vkCreateShaderModule(mDevice, &create_info, nullptr, &shader_module));
+
+	return new VertexShaderVK(mDevice, shader_module);
+}
+
+Ref<PixelShader> RendererVK::CreatePixelShader(const char *inFileName)
+{
+	Array<uint8> data = ReadData((String(inFileName) + ".frag.spv").c_str());
+
+	VkShaderModuleCreateInfo create_info = {};
+	create_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
+	create_info.codeSize = data.size();
+	create_info.pCode = reinterpret_cast<const uint32 *>(data.data());
+	VkShaderModule shader_module;
+	FatalErrorIfFailed(vkCreateShaderModule(mDevice, &create_info, nullptr, &shader_module));
+
+	return new PixelShaderVK(mDevice, shader_module);
+}
+
+unique_ptr<PipelineState> RendererVK::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<PipelineStateVK>(this, static_cast<const VertexShaderVK *>(inVertexShader), inInputDescription, inInputDescriptionCount, static_cast<const PixelShaderVK *>(inPixelShader), inDrawPass, inFillMode, inTopology, inDepthTest, inBlendMode, inCullMode);
+}
+
+RenderPrimitive *RendererVK::CreateRenderPrimitive(PipelineState::ETopology inType)
+{
+	return new RenderPrimitiveVK(this);
+}
+
+RenderInstances *RendererVK::CreateRenderInstances()
+{
+	return new RenderInstancesVK(this);
+}
+
+uint32 RendererVK::FindMemoryType(uint32 inTypeFilter, VkMemoryPropertyFlags inProperties)
+{
+	for (uint32 i = 0; i < mMemoryProperties.memoryTypeCount; i++)
+		if ((inTypeFilter & (1 << i))
+			&& (mMemoryProperties.memoryTypes[i].propertyFlags & inProperties) == inProperties)
+			return i;
+
+	FatalError("Failed to find memory type!");
+}
+
+void RendererVK::CreateBuffer(VkDeviceSize inSize, VkBufferUsageFlags inUsage, VkMemoryPropertyFlags inProperties, BufferVK &outBuffer)
+{
+	// Check the cache
+	BufferCache::iterator i = mBufferCache.find({ inSize, inUsage, inProperties });
+	if (i != mBufferCache.end() && !i->second.empty())
+	{
+		outBuffer = i->second.back();
+		i->second.pop_back();
+		return;
+	}
+
+	// Create a new buffer
+	outBuffer.mSize = inSize;
+	outBuffer.mUsage = inUsage;
+	outBuffer.mProperties = inProperties;
+
+	VkBufferCreateInfo create_info = {};
+	create_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
+	create_info.size = inSize;
+	create_info.usage = inUsage;
+	create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
+	FatalErrorIfFailed(vkCreateBuffer(mDevice, &create_info, nullptr, &outBuffer.mBuffer));
+
+	VkMemoryRequirements mem_requirements;
+	vkGetBufferMemoryRequirements(mDevice, outBuffer.mBuffer, &mem_requirements);
+
+	VkMemoryAllocateInfo alloc_info = {};
+	alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
+	alloc_info.allocationSize = mem_requirements.size;
+	alloc_info.memoryTypeIndex = FindMemoryType(mem_requirements.memoryTypeBits, inProperties);
+
+	FatalErrorIfFailed(vkAllocateMemory(mDevice, &alloc_info, nullptr, &outBuffer.mMemory));
+
+	vkBindBufferMemory(mDevice, outBuffer.mBuffer, outBuffer.mMemory, 0);
+}
+
+VkCommandBuffer RendererVK::StartTempCommandBuffer()
+{
+	VkCommandBufferAllocateInfo alloc_info = {};
+	alloc_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
+	alloc_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
+	alloc_info.commandPool = mCommandPool;
+	alloc_info.commandBufferCount = 1;
+
+	VkCommandBuffer command_buffer;
+	vkAllocateCommandBuffers(mDevice, &alloc_info, &command_buffer);
+
+	VkCommandBufferBeginInfo begin_info = {};
+	begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
+	begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
+	vkBeginCommandBuffer(command_buffer, &begin_info);
+
+	return command_buffer;
+}
+
+void RendererVK::EndTempCommandBuffer(VkCommandBuffer inCommandBuffer)
+{
+	vkEndCommandBuffer(inCommandBuffer);
+
+	VkSubmitInfo submit_info = {};
+	submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
+	submit_info.commandBufferCount = 1;
+	submit_info.pCommandBuffers = &inCommandBuffer;
+
+	vkQueueSubmit(mGraphicsQueue, 1, &submit_info, VK_NULL_HANDLE);
+	vkQueueWaitIdle(mGraphicsQueue); // Inefficient, but we only use this during initialization
+
+	vkFreeCommandBuffers(mDevice, mCommandPool, 1, &inCommandBuffer);
+}
+
+void RendererVK::CopyBuffer(VkBuffer inSrc, VkBuffer inDst, VkDeviceSize inSize)
+{
+	VkCommandBuffer command_buffer = StartTempCommandBuffer();
+
+	VkBufferCopy region = {};
+	region.size = inSize;
+	vkCmdCopyBuffer(command_buffer, inSrc, inDst, 1, &region);
+
+	EndTempCommandBuffer(command_buffer);
+}
+
+void RendererVK::CreateDeviceLocalBuffer(const void *inData, VkDeviceSize inSize, VkBufferUsageFlags inUsage, BufferVK &outBuffer)
+{
+	BufferVK staging_buffer;
+	CreateBuffer(inSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, staging_buffer);
+
+	void *data;
+	vkMapMemory(mDevice, staging_buffer.mMemory, 0, inSize, 0, &data);
+	memcpy(data, inData, (size_t)inSize);
+	vkUnmapMemory(mDevice, staging_buffer.mMemory);
+
+	CreateBuffer(inSize, inUsage | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, outBuffer);
+
+	CopyBuffer(staging_buffer.mBuffer, outBuffer.mBuffer, inSize);
+
+	FreeBuffer(staging_buffer);
+}
+
+void RendererVK::FreeBuffer(BufferVK &ioBuffer)
+{
+	if (ioBuffer.mBuffer != VK_NULL_HANDLE)
+	{
+		JPH_ASSERT(mFrameIndex < cFrameCount);
+		mFreedBuffers[mFrameIndex][{ ioBuffer.mSize, ioBuffer.mUsage, ioBuffer.mProperties }].push_back(ioBuffer);
+	}
+}
+
+unique_ptr<ConstantBufferVK> RendererVK::CreateConstantBuffer(VkDeviceSize inBufferSize)
+{
+	return make_unique<ConstantBufferVK>(this, inBufferSize);
+}
+
+VkImageView RendererVK::CreateImageView(VkImage inImage, VkFormat inFormat, VkImageAspectFlags inAspectFlags)
+{
+	VkImageViewCreateInfo view_info = {};
+	view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
+	view_info.image = inImage;
+	view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
+	view_info.format = inFormat;
+	view_info.subresourceRange.aspectMask = inAspectFlags;
+	view_info.subresourceRange.levelCount = 1;
+	view_info.subresourceRange.layerCount = 1;
+
+	VkImageView image_view;
+	FatalErrorIfFailed(vkCreateImageView(mDevice, &view_info, nullptr, &image_view));
+
+	return image_view;
+}
+
+void RendererVK::CreateImage(uint32 inWidth, uint32 inHeight, VkFormat inFormat, VkImageTiling inTiling, VkImageUsageFlags inUsage, VkMemoryPropertyFlags inProperties, VkImage &outImage, VkDeviceMemory &outMemory)
+{
+	VkImageCreateInfo image_info = {};
+	image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
+	image_info.imageType = VK_IMAGE_TYPE_2D;
+	image_info.extent.width = inWidth;
+	image_info.extent.height = inHeight;
+	image_info.extent.depth = 1;
+	image_info.mipLevels = 1;
+	image_info.arrayLayers = 1;
+	image_info.format = inFormat;
+	image_info.tiling = inTiling;
+	image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+	image_info.usage = inUsage;
+	image_info.samples = VK_SAMPLE_COUNT_1_BIT;
+	image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
+	FatalErrorIfFailed(vkCreateImage(mDevice, &image_info, nullptr, &outImage));
+
+	VkMemoryRequirements mem_requirements;
+	vkGetImageMemoryRequirements(mDevice, outImage, &mem_requirements);
+
+	VkMemoryAllocateInfo alloc_info = {};
+	alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
+	alloc_info.allocationSize = mem_requirements.size;
+	alloc_info.memoryTypeIndex = FindMemoryType(mem_requirements.memoryTypeBits, inProperties);
+	FatalErrorIfFailed(vkAllocateMemory(mDevice, &alloc_info, nullptr, &outMemory));
+
+	vkBindImageMemory(mDevice, outImage, outMemory, 0);
+}
+
+void RendererVK::UpdateViewPortAndScissorRect(uint32 inWidth, uint32 inHeight)
+{
+	VkCommandBuffer command_buffer = GetCommandBuffer();
+
+	// Update the view port rect
+	VkViewport viewport = {};
+	viewport.x = 0.0f;
+	viewport.y = 0.0f;
+	viewport.width = (float)inWidth;
+	viewport.height = (float)inHeight;
+	viewport.minDepth = 0.0f;
+	viewport.maxDepth = 1.0f;
+	vkCmdSetViewport(command_buffer, 0, 1, &viewport);
+
+	// Update the scissor rect
+	VkRect2D scissor = {};
+	scissor.extent = { inWidth, inHeight };
+	vkCmdSetScissor(command_buffer, 0, 1, &scissor);
+}

+ 123 - 0
TestFramework/Renderer/VK/RendererVK.h

@@ -0,0 +1,123 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/Renderer.h>
+#include <Renderer/VK/ConstantBufferVK.h>
+#include <Renderer/VK/TextureVK.h>
+#include <Jolt/Core/UnorderedMap.h>
+
+#include <vulkan/vulkan.h>
+
+/// Vulkan renderer
+class RendererVK : public Renderer
+{
+public:
+	/// Destructor
+	virtual							~RendererVK() override;
+
+	// See: Renderer
+	virtual void					Initialize() 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 *inFileName) override;
+	virtual Ref<PixelShader>		CreatePixelShader(const char *inFileName) 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.GetPtr(); }
+	virtual void					OnWindowResize() override;
+
+	VkDevice						GetDevice() const												{ return mDevice; }
+	VkDescriptorPool				GetDescriptorPool() const										{ return mDescriptorPool; }
+	VkDescriptorSetLayout			GetDescriptorSetLayoutTexture() const							{ return mDescriptorSetLayoutTexture; }
+	VkSampler						GetTextureSamplerRepeat() const									{ return mTextureSamplerRepeat; }
+	VkSampler						GetTextureSamplerShadow() const									{ return mTextureSamplerShadow; }
+	VkRenderPass					GetRenderPassShadow() const										{ return mRenderPassShadow; }
+	VkRenderPass					GetRenderPass() const											{ return mRenderPass; }
+	VkPipelineLayout				GetPipelineLayout() const										{ return mPipelineLayout; }
+	VkCommandBuffer					GetCommandBuffer()												{ JPH_ASSERT(mInFrame); return mCommandBuffers[mFrameIndex]; }
+	VkCommandBuffer					StartTempCommandBuffer();
+	void							EndTempCommandBuffer(VkCommandBuffer inCommandBuffer);
+	void							CreateBuffer(VkDeviceSize inSize, VkBufferUsageFlags inUsage, VkMemoryPropertyFlags inProperties, BufferVK &outBuffer);
+	void							CopyBuffer(VkBuffer inSrc, VkBuffer inDst, VkDeviceSize inSize);
+	void							CreateDeviceLocalBuffer(const void *inData, VkDeviceSize inSize, VkBufferUsageFlags inUsage, BufferVK &outBuffer);
+	void							FreeBuffer(BufferVK &ioBuffer);
+	unique_ptr<ConstantBufferVK>	CreateConstantBuffer(VkDeviceSize inBufferSize);
+	void							CreateImage(uint32 inWidth, uint32 inHeight, VkFormat inFormat, VkImageTiling inTiling, VkImageUsageFlags inUsage, VkMemoryPropertyFlags inProperties, VkImage &outImage, VkDeviceMemory &outMemory);
+	VkImageView						CreateImageView(VkImage inImage, VkFormat inFormat, VkImageAspectFlags inAspectFlags);
+	VkFormat						FindDepthFormat();
+
+private:
+	uint32							FindMemoryType(uint32 inTypeFilter, VkMemoryPropertyFlags inProperties);
+	VkSurfaceFormatKHR				SelectFormat(VkPhysicalDevice inDevice);
+	void							CreateSwapChain(VkPhysicalDevice inDevice);
+	void							DestroySwapChain();
+	void							UpdateViewPortAndScissorRect(uint32 inWidth, uint32 inHeight);
+
+	VkInstance						mInstance = VK_NULL_HANDLE;
+	VkDebugUtilsMessengerEXT		mDebugMessenger = VK_NULL_HANDLE;
+	VkPhysicalDevice				mPhysicalDevice = VK_NULL_HANDLE;
+	VkPhysicalDeviceMemoryProperties mMemoryProperties;
+	VkDevice						mDevice = VK_NULL_HANDLE;
+	uint32							mGraphicsQueueIndex = 0;
+	uint32							mPresentQueueIndex = 0;
+	VkQueue							mGraphicsQueue = VK_NULL_HANDLE;
+	VkQueue							mPresentQueue = VK_NULL_HANDLE;
+	VkSurfaceKHR					mSurface = VK_NULL_HANDLE;
+	VkSwapchainKHR					mSwapChain = VK_NULL_HANDLE;
+	Array<VkImage>					mSwapChainImages;
+	VkFormat						mSwapChainImageFormat;
+	VkExtent2D						mSwapChainExtent;
+	Array<VkImageView>				mSwapChainImageViews;
+	VkImage							mDepthImage = VK_NULL_HANDLE;
+	VkDeviceMemory					mDepthImageMemory = VK_NULL_HANDLE;
+	VkImageView						mDepthImageView = VK_NULL_HANDLE;
+	VkDescriptorSetLayout			mDescriptorSetLayoutUBO = VK_NULL_HANDLE;
+	VkDescriptorSetLayout			mDescriptorSetLayoutTexture = VK_NULL_HANDLE;
+	VkDescriptorPool				mDescriptorPool = VK_NULL_HANDLE;
+	VkDescriptorSet					mDescriptorSets[cFrameCount];
+	VkDescriptorSet					mDescriptorSetsOrtho[cFrameCount];
+	VkSampler						mTextureSamplerShadow = VK_NULL_HANDLE;	
+	VkSampler						mTextureSamplerRepeat = VK_NULL_HANDLE;	
+	VkRenderPass					mRenderPassShadow = VK_NULL_HANDLE;
+	VkRenderPass					mRenderPass = VK_NULL_HANDLE;
+	VkPipelineLayout				mPipelineLayout = VK_NULL_HANDLE;
+	VkFramebuffer					mShadowFrameBuffer = VK_NULL_HANDLE;
+	Array<VkFramebuffer>			mSwapChainFramebuffers;
+	uint32							mImageIndex = 0;
+	VkCommandPool					mCommandPool = VK_NULL_HANDLE;
+	VkCommandBuffer					mCommandBuffers[cFrameCount];
+	VkSemaphore						mImageAvailableSemaphores[cFrameCount];
+	VkSemaphore						mRenderFinishedSemaphores[cFrameCount];
+	VkFence							mInFlightFences[cFrameCount];
+	Ref<TextureVK>					mShadowMap;
+	unique_ptr<ConstantBufferVK>	mVertexShaderConstantBufferProjection[cFrameCount];
+	unique_ptr<ConstantBufferVK>	mVertexShaderConstantBufferOrtho[cFrameCount];
+	unique_ptr<ConstantBufferVK>	mPixelShaderConstantBuffer[cFrameCount];
+		
+	struct Key
+	{
+		bool						operator == (const Key &inRHS) const
+		{
+			return mSize == inRHS.mSize && mUsage == inRHS.mUsage && mProperties == inRHS.mProperties;
+		}
+
+		VkDeviceSize				mSize;
+		VkBufferUsageFlags			mUsage;
+		VkMemoryPropertyFlags		mProperties;
+	};
+
+	JPH_MAKE_HASH_STRUCT(Key, KeyHasher, t.mSize, t.mUsage, t.mProperties);
+
+	using BufferCache = UnorderedMap<Key, Array<BufferVK>, KeyHasher>;
+
+	BufferCache						mFreedBuffers[cFrameCount];
+	BufferCache						mBufferCache;
+};

+ 180 - 0
TestFramework/Renderer/VK/TextureVK.cpp

@@ -0,0 +1,180 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Renderer/VK/TextureVK.h>
+#include <Renderer/VK/RendererVK.h>
+#include <Renderer/VK/FatalErrorIfFailedVK.h>
+#include <Image/BlitSurface.h>
+
+TextureVK::TextureVK(RendererVK *inRenderer, const Surface *inSurface) :
+	Texture(inSurface->GetWidth(), inSurface->GetHeight()),
+	mRenderer(inRenderer)
+{
+	ESurfaceFormat format = inSurface->GetFormat();
+	VkFormat vk_format = VK_FORMAT_B8G8R8A8_UNORM;
+	switch (format)
+	{
+	case ESurfaceFormat::A4L4:			vk_format = VK_FORMAT_R8G8_UNORM;				format = ESurfaceFormat::A8L8; break;
+	case ESurfaceFormat::L8:			vk_format = VK_FORMAT_R8_UNORM;					break;
+	case ESurfaceFormat::A8:			vk_format = VK_FORMAT_A8_UNORM_KHR;				break;
+	case ESurfaceFormat::A8L8:			vk_format = VK_FORMAT_R8G8_UNORM;				break;
+	case ESurfaceFormat::R5G6B5:		vk_format = VK_FORMAT_B5G6R5_UNORM_PACK16;		break;
+	case ESurfaceFormat::X1R5G5B5:		vk_format = VK_FORMAT_B5G5R5A1_UNORM_PACK16;	format = ESurfaceFormat::A1R5G5B5; break;
+	case ESurfaceFormat::X4R4G4B4:		vk_format = VK_FORMAT_B4G4R4A4_UNORM_PACK16;	format = ESurfaceFormat::A4R4G4B4; break;
+	case ESurfaceFormat::A1R5G5B5:		vk_format = VK_FORMAT_B5G5R5A1_UNORM_PACK16;	break;
+	case ESurfaceFormat::A4R4G4B4:		vk_format = VK_FORMAT_B4G4R4A4_UNORM_PACK16;	break;
+	case ESurfaceFormat::R8G8B8:		vk_format = VK_FORMAT_B8G8R8_UNORM;				break;
+	case ESurfaceFormat::B8G8R8:		vk_format = VK_FORMAT_B8G8R8_UNORM;				break;
+	case ESurfaceFormat::X8R8G8B8:		vk_format = VK_FORMAT_B8G8R8A8_UNORM;			format = ESurfaceFormat::A8R8G8B8; break;
+	case ESurfaceFormat::X8B8G8R8:		vk_format = VK_FORMAT_B8G8R8A8_UNORM;			format = ESurfaceFormat::X8R8G8B8; break;
+	case ESurfaceFormat::A8R8G8B8:		vk_format = VK_FORMAT_B8G8R8A8_UNORM;			break;
+	case ESurfaceFormat::A8B8G8R8:		vk_format = VK_FORMAT_B8G8R8A8_UNORM;			format = ESurfaceFormat::A8R8G8B8; 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;
+	}
+
+	int bpp = surface->GetBytesPerPixel();
+	VkDeviceSize image_size = VkDeviceSize(mWidth) * mHeight * bpp;
+	VkDevice device = mRenderer->GetDevice();
+
+	BufferVK staging_buffer;
+	mRenderer->CreateBuffer(image_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, staging_buffer);
+
+	// Copy data to upload texture
+	surface->Lock(ESurfaceLockMode::Read);
+	void *data;
+	vkMapMemory(device, staging_buffer.mMemory, 0, image_size, 0, &data);
+	for (int y = 0; y < mHeight; ++y)
+		memcpy(reinterpret_cast<uint8 *>(data) + y * mWidth * bpp, surface->GetData() + y * surface->GetStride(), mWidth * bpp);
+	vkUnmapMemory(device, staging_buffer.mMemory);
+	surface->UnLock();
+
+	// Create destination image
+	mRenderer->CreateImage(mWidth, mHeight, vk_format, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, mImage, mImageMemory);
+
+	VkCommandBuffer command_buffer = mRenderer->StartTempCommandBuffer();
+
+	// Make the image suitable for transferring to
+	TransitionImageLayout(command_buffer, mImage, vk_format, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
+
+	// Copy the data to the destination image
+	VkBufferImageCopy region = {};
+	region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+	region.imageSubresource.layerCount = 1;
+	region.imageExtent = { uint32(mWidth), uint32(mHeight), 1 };
+	vkCmdCopyBufferToImage(command_buffer, staging_buffer.mBuffer, mImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
+
+	// Make the image suitable for sampling
+	TransitionImageLayout(command_buffer, mImage, vk_format, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
+
+	mRenderer->EndTempCommandBuffer(command_buffer);
+
+	// Destroy temporary buffer
+	mRenderer->FreeBuffer(staging_buffer);
+
+	CreateImageViewAndDescriptorSet(vk_format, VK_IMAGE_ASPECT_COLOR_BIT, mRenderer->GetTextureSamplerRepeat());
+}
+
+TextureVK::TextureVK(RendererVK *inRenderer, int inWidth, int inHeight) :
+	Texture(inWidth, inHeight),
+	mRenderer(inRenderer)
+{
+	VkFormat vk_format = mRenderer->FindDepthFormat();
+
+	// Create render target
+	mRenderer->CreateImage(mWidth, mHeight, vk_format, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, mImage, mImageMemory);
+
+	CreateImageViewAndDescriptorSet(vk_format, VK_IMAGE_ASPECT_DEPTH_BIT, mRenderer->GetTextureSamplerShadow());
+}
+
+TextureVK::~TextureVK()
+{
+	if (mImage != VK_NULL_HANDLE)
+	{
+		VkDevice device = mRenderer->GetDevice();
+
+		vkDeviceWaitIdle(device);
+
+		vkDestroyImageView(device, mImageView, nullptr);
+		vkDestroyImage(device, mImage, nullptr);
+		vkFreeMemory(device, mImageMemory, nullptr);
+	}
+}
+
+void TextureVK::Bind() const
+{
+	if (mDescriptorSet != VK_NULL_HANDLE)
+		vkCmdBindDescriptorSets(mRenderer->GetCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, mRenderer->GetPipelineLayout(), 1, 1, &mDescriptorSet, 0, nullptr);
+}
+
+void TextureVK::CreateImageViewAndDescriptorSet(VkFormat inFormat, VkImageAspectFlags inAspectFlags, VkSampler inSampler)
+{
+	VkDevice device = mRenderer->GetDevice();
+
+	// Create image view
+	mImageView = mRenderer->CreateImageView(mImage, inFormat, inAspectFlags);
+
+	// Allocate descriptor set for binding the texture
+	VkDescriptorSetLayout layout = mRenderer->GetDescriptorSetLayoutTexture();
+	VkDescriptorSetAllocateInfo descriptor_set_alloc_info = {};
+	descriptor_set_alloc_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
+	descriptor_set_alloc_info.descriptorPool = mRenderer->GetDescriptorPool();
+	descriptor_set_alloc_info.descriptorSetCount = 1;
+	descriptor_set_alloc_info.pSetLayouts = &layout;
+	FatalErrorIfFailed(vkAllocateDescriptorSets(device, &descriptor_set_alloc_info, &mDescriptorSet));
+
+	VkDescriptorImageInfo image_info = {};
+	image_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+	image_info.imageView = mImageView;
+	image_info.sampler = inSampler;
+
+	VkWriteDescriptorSet descriptor_write = {};
+	descriptor_write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+	descriptor_write.dstSet = mDescriptorSet;
+	descriptor_write.dstBinding = 0;
+	descriptor_write.dstArrayElement = 0;
+	descriptor_write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
+	descriptor_write.descriptorCount = 1;
+	descriptor_write.pImageInfo = &image_info;
+	vkUpdateDescriptorSets(device, 1, &descriptor_write, 0, nullptr);
+}
+
+void TextureVK::TransitionImageLayout(VkCommandBuffer inCommandBuffer, VkImage inImage, VkFormat inFormat, VkImageLayout inOldLayout, VkImageLayout inNewLayout)
+{
+	VkImageMemoryBarrier barrier = {};
+	barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
+	barrier.oldLayout = inOldLayout;
+	barrier.newLayout = inNewLayout;
+	barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
+	barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
+	barrier.image = inImage;
+	barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+	barrier.subresourceRange.levelCount = 1;
+	barrier.subresourceRange.layerCount = 1;
+
+	if (inOldLayout == VK_IMAGE_LAYOUT_UNDEFINED && inNewLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
+	{
+		barrier.srcAccessMask = 0;
+		barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
+		vkCmdPipelineBarrier(inCommandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
+	}
+	else if (inOldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && inNewLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
+	{
+		barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
+		barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
+		vkCmdPipelineBarrier(inCommandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
+	}
+}

+ 35 - 0
TestFramework/Renderer/VK/TextureVK.h

@@ -0,0 +1,35 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/Texture.h>
+
+#include <vulkan/vulkan.h>
+
+class RendererVK;
+
+class TextureVK : public Texture
+{
+public:
+	/// Constructor, called by Renderer::CreateTextureVK
+										TextureVK(RendererVK *inRenderer, const Surface *inSurface);	// Create a normal TextureVK
+										TextureVK(RendererVK *inRenderer, int inWidth, int inHeight);	// Create a render target (depth only)
+	virtual								~TextureVK() override;
+
+	/// Bind texture to the pixel shader
+	virtual void						Bind() const override;
+
+	VkImageView							GetImageView() const						{ return mImageView; }
+
+private:
+	void								CreateImageViewAndDescriptorSet(VkFormat inFormat, VkImageAspectFlags inAspectFlags, VkSampler inSampler);
+	void								TransitionImageLayout(VkCommandBuffer inCommandBuffer, VkImage inImage, VkFormat inFormat, VkImageLayout inOldLayout, VkImageLayout inNewLayout);
+
+	RendererVK *						mRenderer;
+	VkImage								mImage = VK_NULL_HANDLE;
+	VkDeviceMemory						mImageMemory = VK_NULL_HANDLE;
+	VkImageView							mImageView = VK_NULL_HANDLE;
+	VkDescriptorSet						mDescriptorSet = VK_NULL_HANDLE;
+};

+ 34 - 0
TestFramework/Renderer/VK/VertexShaderVK.h

@@ -0,0 +1,34 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Renderer/VertexShader.h>
+
+#include <vulkan/vulkan.h>
+
+/// Vertex shader handle for Vulkan
+class VertexShaderVK : public VertexShader
+{
+public:
+	/// Constructor
+							VertexShaderVK(VkDevice inDevice, VkShaderModule inShaderModule) :
+		mDevice(inDevice),
+		mStageInfo()
+	{
+		mStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
+		mStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
+		mStageInfo.module = inShaderModule;
+		mStageInfo.pName = "main";
+	}
+
+	/// Destructor
+	virtual					~VertexShaderVK() override
+	{
+		vkDestroyShaderModule(mDevice, mStageInfo.module, nullptr);
+	}
+
+	VkDevice				mDevice;
+	VkPipelineShaderStageCreateInfo mStageInfo;
+};

+ 15 - 0
TestFramework/Renderer/VertexShader.h

@@ -0,0 +1,15 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Core/Reference.h>
+
+/// Vertex shader handle
+class VertexShader : public RefTarget<VertexShader>
+{
+public:
+	/// Destructor
+	virtual					~VertexShader() = default;
+};

+ 101 - 24
TestFramework/TestFramework.cmake

@@ -24,27 +24,38 @@ set(TEST_FRAMEWORK_SRC_FILES
 	${TEST_FRAMEWORK_ROOT}/Input/Mouse.h
 	${TEST_FRAMEWORK_ROOT}/Math/Perlin.cpp
 	${TEST_FRAMEWORK_ROOT}/Math/Perlin.h
-	${TEST_FRAMEWORK_ROOT}/Renderer/ConstantBuffer.cpp
-	${TEST_FRAMEWORK_ROOT}/Renderer/ConstantBuffer.h
-	${TEST_FRAMEWORK_ROOT}/Renderer/CommandQueue.h
-	${TEST_FRAMEWORK_ROOT}/Renderer/DescriptorHeap.h
-	${TEST_FRAMEWORK_ROOT}/Renderer/FatalErrorIfFailed.cpp
-	${TEST_FRAMEWORK_ROOT}/Renderer/FatalErrorIfFailed.h
+	${TEST_FRAMEWORK_ROOT}/Renderer/DebugRendererImp.cpp
+	${TEST_FRAMEWORK_ROOT}/Renderer/DebugRendererImp.h
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/ConstantBufferDX12.cpp
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/ConstantBufferDX12.h
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/CommandQueueDX12.h
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/DescriptorHeapDX12.h
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/FatalErrorIfFailedDX12.cpp
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/FatalErrorIfFailedDX12.h
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/PipelineStateDX12.cpp
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/PipelineStateDX12.h
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/PixelShaderDX12.h
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/RendererDX12.cpp
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/RendererDX12.h
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/RenderInstancesDX12.cpp
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/RenderInstancesDX12.h
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/RenderPrimitiveDX12.cpp
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/RenderPrimitiveDX12.h
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/TextureDX12.cpp
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/TextureDX12.h
+	${TEST_FRAMEWORK_ROOT}/Renderer/DX12/VertexShaderDX12.h
 	${TEST_FRAMEWORK_ROOT}/Renderer/Font.cpp
 	${TEST_FRAMEWORK_ROOT}/Renderer/Font.h
 	${TEST_FRAMEWORK_ROOT}/Renderer/Frustum.h
-	${TEST_FRAMEWORK_ROOT}/Renderer/PipelineState.cpp
 	${TEST_FRAMEWORK_ROOT}/Renderer/PipelineState.h
+	${TEST_FRAMEWORK_ROOT}/Renderer/PixelShader.h
 	${TEST_FRAMEWORK_ROOT}/Renderer/Renderer.cpp
 	${TEST_FRAMEWORK_ROOT}/Renderer/Renderer.h
-	${TEST_FRAMEWORK_ROOT}/Renderer/RenderInstances.cpp
 	${TEST_FRAMEWORK_ROOT}/Renderer/RenderInstances.h
 	${TEST_FRAMEWORK_ROOT}/Renderer/RenderPrimitive.cpp
 	${TEST_FRAMEWORK_ROOT}/Renderer/RenderPrimitive.h
-	${TEST_FRAMEWORK_ROOT}/Renderer/Texture.cpp
 	${TEST_FRAMEWORK_ROOT}/Renderer/Texture.h
-	${TEST_FRAMEWORK_ROOT}/Renderer/DebugRendererImp.cpp
-	${TEST_FRAMEWORK_ROOT}/Renderer/DebugRendererImp.h
+	${TEST_FRAMEWORK_ROOT}/Renderer/VertexShader.h
 	${TEST_FRAMEWORK_ROOT}/TestFramework.cmake
 	${TEST_FRAMEWORK_ROOT}/TestFramework.h
 	${TEST_FRAMEWORK_ROOT}/UI/UIAnimation.cpp
@@ -83,27 +94,24 @@ set(TEST_FRAMEWORK_SRC_FILES
 	${TEST_FRAMEWORK_ROOT}/Utils/ReadData.h
 )
 
-# Group source files
-source_group(TREE ${TEST_FRAMEWORK_ROOT} FILES ${TEST_FRAMEWORK_SRC_FILES})
-
 # All shaders
 set(TEST_FRAMEWORK_SRC_FILES_SHADERS
 	${PHYSICS_REPO_ROOT}/Assets/Shaders/VertexConstants.h
 )
 
-# Vertex shaders
-set(TEST_FRAMEWORK_VERTEX_SHADERS
+# HLSL vertex shaders
+set(TEST_FRAMEWORK_HLSL_VERTEX_SHADERS
 	${PHYSICS_REPO_ROOT}/Assets/Shaders/FontVertexShader.hlsl
 	${PHYSICS_REPO_ROOT}/Assets/Shaders/LineVertexShader.hlsl
 	${PHYSICS_REPO_ROOT}/Assets/Shaders/TriangleDepthVertexShader.hlsl
 	${PHYSICS_REPO_ROOT}/Assets/Shaders/TriangleVertexShader.hlsl
 	${PHYSICS_REPO_ROOT}/Assets/Shaders/UIVertexShader.hlsl
 )
-set(TEST_FRAMEWORK_SRC_FILES_SHADERS ${TEST_FRAMEWORK_SRC_FILES_SHADERS} ${TEST_FRAMEWORK_VERTEX_SHADERS})
-set_source_files_properties(${TEST_FRAMEWORK_VERTEX_SHADERS} PROPERTIES VS_SHADER_FLAGS "/WX /T vs_5_0")
+set(TEST_FRAMEWORK_SRC_FILES_SHADERS ${TEST_FRAMEWORK_SRC_FILES_SHADERS} ${TEST_FRAMEWORK_HLSL_VERTEX_SHADERS})
+set_source_files_properties(${TEST_FRAMEWORK_HLSL_VERTEX_SHADERS} PROPERTIES VS_SHADER_FLAGS "/WX /T vs_5_0")
 
-# Pixel shaders
-set(TEST_FRAMEWORK_PIXEL_SHADERS
+# HLSL pixel shaders
+set(TEST_FRAMEWORK_HLSL_PIXEL_SHADERS
 	${PHYSICS_REPO_ROOT}/Assets/Shaders/FontPixelShader.hlsl
 	${PHYSICS_REPO_ROOT}/Assets/Shaders/LinePixelShader.hlsl
 	${PHYSICS_REPO_ROOT}/Assets/Shaders/TriangleDepthPixelShader.hlsl
@@ -111,14 +119,83 @@ set(TEST_FRAMEWORK_PIXEL_SHADERS
 	${PHYSICS_REPO_ROOT}/Assets/Shaders/UIPixelShader.hlsl
 	${PHYSICS_REPO_ROOT}/Assets/Shaders/UIPixelShaderUntextured.hlsl
 )
-set(TEST_FRAMEWORK_SRC_FILES_SHADERS ${TEST_FRAMEWORK_SRC_FILES_SHADERS} ${TEST_FRAMEWORK_PIXEL_SHADERS})
-set_source_files_properties(${TEST_FRAMEWORK_PIXEL_SHADERS} PROPERTIES VS_SHADER_FLAGS "/WX /T ps_5_0")
+set(TEST_FRAMEWORK_SRC_FILES_SHADERS ${TEST_FRAMEWORK_SRC_FILES_SHADERS} ${TEST_FRAMEWORK_HLSL_PIXEL_SHADERS})
+set_source_files_properties(${TEST_FRAMEWORK_HLSL_PIXEL_SHADERS} PROPERTIES VS_SHADER_FLAGS "/WX /T ps_5_0")
+
+# Include the Vulkan library
+find_package(Vulkan)
+if (Vulkan_FOUND)
+	# Vulkan source files
+	set(TEST_FRAMEWORK_SRC_FILES
+		${TEST_FRAMEWORK_SRC_FILES}
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/BufferVK.h
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/ConstantBufferVK.cpp
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/ConstantBufferVK.h
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/FatalErrorIfFailedVK.cpp
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/FatalErrorIfFailedVK.h
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/PipelineStateVK.cpp
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/PipelineStateVK.h
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/PixelShaderVK.h
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/RendererVK.cpp
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/RendererVK.h
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/RenderInstancesVK.cpp
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/RenderInstancesVK.h
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/RenderPrimitiveVK.cpp
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/RenderPrimitiveVK.h
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/TextureVK.cpp
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/TextureVK.h
+		${TEST_FRAMEWORK_ROOT}/Renderer/VK/VertexShaderVK.h
+	)
+
+	# GLSL headers
+	set(TEST_FRAMEWORK_SRC_FILES_SHADERS
+		${TEST_FRAMEWORK_SRC_FILES_SHADERS}
+		${PHYSICS_REPO_ROOT}/Assets/Shaders/VertexConstantsVK.h
+	)
+
+	# GLSL shaders
+	set(TEST_FRAMEWORK_GLSL_SHADERS
+		${PHYSICS_REPO_ROOT}/Assets/Shaders/FontVertexShader.vert
+		${PHYSICS_REPO_ROOT}/Assets/Shaders/LineVertexShader.vert
+		${PHYSICS_REPO_ROOT}/Assets/Shaders/TriangleDepthVertexShader.vert
+		${PHYSICS_REPO_ROOT}/Assets/Shaders/TriangleVertexShader.vert
+		${PHYSICS_REPO_ROOT}/Assets/Shaders/UIVertexShader.vert
+		${PHYSICS_REPO_ROOT}/Assets/Shaders/FontPixelShader.frag
+		${PHYSICS_REPO_ROOT}/Assets/Shaders/LinePixelShader.frag
+		${PHYSICS_REPO_ROOT}/Assets/Shaders/TriangleDepthPixelShader.frag
+		${PHYSICS_REPO_ROOT}/Assets/Shaders/TrianglePixelShader.frag
+		${PHYSICS_REPO_ROOT}/Assets/Shaders/UIPixelShader.frag
+		${PHYSICS_REPO_ROOT}/Assets/Shaders/UIPixelShaderUntextured.frag
+	)
+
+	# Compile GLSL shaders
+	foreach(SHADER ${TEST_FRAMEWORK_GLSL_SHADERS})
+		set(SPV_SHADER ${SHADER}.spv)
+		add_custom_command(OUTPUT ${SPV_SHADER}
+			COMMAND ${Vulkan_GLSLC_EXECUTABLE} ${SHADER} -o ${SPV_SHADER}
+			DEPENDS ${SHADER}
+			COMMENT "Compiling ${SHADER}")
+		list(APPEND TEST_FRAMEWORK_SPV_SHADERS ${SPV_SHADER})
+	endforeach()
+endif()
+
+# Group source files
+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})
+source_group(TREE ${PHYSICS_REPO_ROOT} FILES ${TEST_FRAMEWORK_SRC_FILES_SHADERS} ${TEST_FRAMEWORK_GLSL_SHADERS} ${TEST_FRAMEWORK_SPV_SHADERS})
 
 # Create TestFramework lib
-add_library(TestFramework STATIC ${TEST_FRAMEWORK_SRC_FILES} ${TEST_FRAMEWORK_SRC_FILES_SHADERS})
+add_library(TestFramework STATIC ${TEST_FRAMEWORK_SRC_FILES} ${TEST_FRAMEWORK_SRC_FILES_SHADERS} ${TEST_FRAMEWORK_SPV_SHADERS})
 target_include_directories(TestFramework PUBLIC ${TEST_FRAMEWORK_ROOT})
 target_link_libraries(TestFramework LINK_PUBLIC Jolt dxguid.lib dinput8.lib dxgi.lib d3d12.lib d3dcompiler.lib)
 target_precompile_headers(TestFramework PUBLIC ${TEST_FRAMEWORK_ROOT}/TestFramework.h)
+
+# Enable Vulkan for the TestFramework
+if (Vulkan_FOUND)
+	target_include_directories(TestFramework PUBLIC ${Vulkan_INCLUDE_DIRS})
+	target_link_libraries(TestFramework LINK_PUBLIC Jolt ${Vulkan_LIBRARIES})
+	if (JPH_ENABLE_VULKAN)
+		target_compile_definitions(TestFramework PRIVATE JPH_ENABLE_VULKAN)
+	endif()
+endif()

+ 20 - 20
TestFramework/UI/UIManager.cpp

@@ -29,22 +29,22 @@ UIManager::UIManager(Renderer *inRenderer) :
 	SetHeight(mRenderer->GetWindowHeight());
 
 	// Create input layout
-	const D3D12_INPUT_ELEMENT_DESC vertex_desc[] =
+	const PipelineState::EInputDescription vertex_desc[] =
 	{
-		{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
-		{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
-		{ "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, 20, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
+		PipelineState::EInputDescription::Position,
+		PipelineState::EInputDescription::TexCoord,
+		PipelineState::EInputDescription::Color
 	};
 
 	// Load vertex shader
-	ComPtr<ID3DBlob> vtx = mRenderer->CreateVertexShader("Assets/Shaders/UIVertexShader.hlsl");
+	Ref<VertexShader> vtx = mRenderer->CreateVertexShader("Assets/Shaders/UIVertexShader");
 
 	// Load pixel shader
-	ComPtr<ID3DBlob> pix_textured = mRenderer->CreatePixelShader("Assets/Shaders/UIPixelShader.hlsl");
-	ComPtr<ID3DBlob> pix_untextured = mRenderer->CreatePixelShader("Assets/Shaders/UIPixelShaderUntextured.hlsl");
+	Ref<PixelShader> pix_textured = mRenderer->CreatePixelShader("Assets/Shaders/UIPixelShader");
+	Ref<PixelShader> pix_untextured = mRenderer->CreatePixelShader("Assets/Shaders/UIPixelShaderUntextured");
 
-	mTextured = mRenderer->CreatePipelineState(vtx.Get(), vertex_desc, ARRAYSIZE(vertex_desc), pix_textured.Get(), D3D12_FILL_MODE_SOLID, D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, PipelineState::EDepthTest::Off, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
-	mUntextured = mRenderer->CreatePipelineState(vtx.Get(), vertex_desc, ARRAYSIZE(vertex_desc), pix_untextured.Get(), D3D12_FILL_MODE_SOLID, D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, PipelineState::EDepthTest::Off, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
+	mTextured = mRenderer->CreatePipelineState(vtx, vertex_desc, std::size(vertex_desc), pix_textured, PipelineState::EDrawPass::Normal, PipelineState::EFillMode::Solid, PipelineState::ETopology::Triangle, PipelineState::EDepthTest::Off, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
+	mUntextured = mRenderer->CreatePipelineState(vtx, vertex_desc, std::size(vertex_desc), pix_untextured, PipelineState::EDrawPass::Normal, PipelineState::EFillMode::Solid, PipelineState::ETopology::Triangle, PipelineState::EDepthTest::Off, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);
 }
 
 UIManager::~UIManager()
@@ -264,8 +264,8 @@ void UIManager::DrawQuad(int inX, int inY, int inWidth, int inHeight, const UITe
 	{
 		bool has_inner = inQuad.HasInnerPart();
 
-		RenderPrimitive primitive(mRenderer, D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
-		primitive.CreateVertexBuffer(has_inner? 9 * 6 : 6, sizeof(QuadVertex));
+		Ref<RenderPrimitive> primitive = mRenderer->CreateRenderPrimitive(PipelineState::ETopology::Triangle);
+		primitive->CreateVertexBuffer(has_inner? 9 * 6 : 6, sizeof(QuadVertex));
 
 		float w = float(inQuad.mTexture->GetWidth()), h = float(inQuad.mTexture->GetHeight());
 
@@ -277,7 +277,7 @@ void UIManager::DrawQuad(int inX, int inY, int inWidth, int inHeight, const UITe
 		tx1 /= w; ty1 /= h;
 		tx2 /= w; ty2 /= h;
 
-		QuadVertex *v = (QuadVertex *)primitive.LockVertexBuffer();
+		QuadVertex *v = (QuadVertex *)primitive->LockVertexBuffer();
 
 		if (has_inner)
 		{
@@ -312,20 +312,20 @@ void UIManager::DrawQuad(int inX, int inY, int inWidth, int inHeight, const UITe
 			sDrawQuad(v, x1, y1, x2, y2, tx1, ty1, tx2, ty2, inColor);
 		}
 
-		primitive.UnlockVertexBuffer();
-		inQuad.mTexture->Bind(2);
+		primitive->UnlockVertexBuffer();
+		inQuad.mTexture->Bind();
 		mTextured->Activate();
-		primitive.Draw();
+		primitive->Draw();
 	}
 	else
 	{
-		RenderPrimitive primitive(mRenderer, D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
-		primitive.CreateVertexBuffer(6, sizeof(QuadVertex));
-		QuadVertex *v = (QuadVertex *)primitive.LockVertexBuffer();
+		Ref<RenderPrimitive> primitive = mRenderer->CreateRenderPrimitive(PipelineState::ETopology::Triangle);
+		primitive->CreateVertexBuffer(6, sizeof(QuadVertex));
+		QuadVertex *v = (QuadVertex *)primitive->LockVertexBuffer();
 		sDrawQuad(v, x1, y1, x2, y2, 0, 0, 0, 0, inColor);
-		primitive.UnlockVertexBuffer();
+		primitive->UnlockVertexBuffer();
 		mUntextured->Activate();
-		primitive.Draw();
+		primitive->Draw();
 	}
 }
 

+ 1 - 0
TestFramework/UI/UIManager.h

@@ -6,6 +6,7 @@
 
 #include <UI/UIElement.h>
 #include <UI/UITexturedQuad.h>
+#include <Renderer/Renderer.h>
 #include <Renderer/PipelineState.h>
 #include <memory>