From 5ec8f01e448899ca86a0792462946e1678f8a987 Mon Sep 17 00:00:00 2001 From: Niels Eppenhof Date: Sun, 30 Aug 2026 03:23:01 +0200 Subject: [PATCH 1/6] DX12 prep --- EppoEngine/Source/Platform/ComPtr.h | 13 +++++++++ .../Platform/Vulkan/DeviceManagerVK.cpp | 16 +++++------ .../Source/Platform/Vulkan/DeviceManagerVK.h | 5 ++-- .../Source/Platform/Vulkan/VulkanShader.cpp | 28 ++++++++----------- .../Renderer/{ => Buffer}/IndexBuffer.cpp | 4 +-- .../Renderer/{ => Buffer}/IndexBuffer.h | 0 .../Renderer/{ => Buffer}/StorageBuffer.cpp | 2 +- .../Renderer/{ => Buffer}/StorageBuffer.h | 0 .../Renderer/{ => Buffer}/UniformBuffer.cpp | 2 +- .../Renderer/{ => Buffer}/UniformBuffer.h | 0 .../Renderer/{ => Buffer}/VertexBuffer.cpp | 4 +-- .../Renderer/{ => Buffer}/VertexBuffer.h | 0 .../Source/Renderer/DescriptorManager.h | 4 +-- EppoEngine/Source/Renderer/DeviceManager.cpp | 7 ----- EppoEngine/Source/Renderer/DeviceManager.h | 1 - EppoEngine/Source/Renderer/Mesh.h | 4 +-- EppoEngine/Source/Renderer/RenderPass.cpp | 4 +-- EppoEngine/Source/Renderer/SceneRenderer.h | 4 +-- 18 files changed, 48 insertions(+), 50 deletions(-) create mode 100644 EppoEngine/Source/Platform/ComPtr.h rename EppoEngine/Source/Renderer/{ => Buffer}/IndexBuffer.cpp (99%) rename EppoEngine/Source/Renderer/{ => Buffer}/IndexBuffer.h (100%) rename EppoEngine/Source/Renderer/{ => Buffer}/StorageBuffer.cpp (97%) rename EppoEngine/Source/Renderer/{ => Buffer}/StorageBuffer.h (100%) rename EppoEngine/Source/Renderer/{ => Buffer}/UniformBuffer.cpp (97%) rename EppoEngine/Source/Renderer/{ => Buffer}/UniformBuffer.h (100%) rename EppoEngine/Source/Renderer/{ => Buffer}/VertexBuffer.cpp (99%) rename EppoEngine/Source/Renderer/{ => Buffer}/VertexBuffer.h (100%) diff --git a/EppoEngine/Source/Platform/ComPtr.h b/EppoEngine/Source/Platform/ComPtr.h new file mode 100644 index 00000000..f0560abc --- /dev/null +++ b/EppoEngine/Source/Platform/ComPtr.h @@ -0,0 +1,13 @@ +#pragma once + +// clang-format off +#if defined(EP_PLATFORM_WINDOWS) + #include + template + using ComPtr = Microsoft::WRL::ComPtr; +#else + #include + template + using ComPtr = CComPtr; +#endif +// clang-format on diff --git a/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.cpp b/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.cpp index 85aaa687..596e9445 100644 --- a/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.cpp +++ b/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.cpp @@ -48,14 +48,6 @@ namespace Eppo vkDestroyInstance(m_Instance, nullptr); } - auto DeviceManagerVK::GetDevice() const -> nvrhi::IDevice* - { - if (m_ValidationLayer) - return m_ValidationLayer; - - return m_Device; - } - auto DeviceManagerVK::BeginFrame() -> bool { return m_Swapchain->BeginFrame(); @@ -66,6 +58,14 @@ namespace Eppo return m_Swapchain->Present(); } + auto DeviceManagerVK::GetDevice() const -> nvrhi::IDevice* + { + if (m_ValidationLayer) + return m_ValidationLayer; + + return m_Device; + } + auto DeviceManagerVK::CreateVulkanInstance() -> void { // Create instance diff --git a/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.h b/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.h index 4033229c..8711af24 100644 --- a/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.h +++ b/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.h @@ -15,14 +15,12 @@ namespace Eppo class DeviceManagerVK : public DeviceManager { public: - DeviceManagerVK(const Ref& window, const DeviceParams& params); + explicit DeviceManagerVK(const Ref& window, const DeviceParams& params); ~DeviceManagerVK() override = default; auto Init() -> void override; auto Shutdown() -> void override; - [[nodiscard]] auto GetDevice() const -> nvrhi::IDevice* override; - auto BeginFrame() -> bool override; auto Present() -> bool override; @@ -31,6 +29,7 @@ namespace Eppo [[nodiscard]] auto GetCurrentBackBufferIndex() const -> uint32_t override { return m_Swapchain->GetCurrentBackBufferIndex(); } [[nodiscard]] auto GetBackBufferCount() const -> uint32_t override { return m_Swapchain->GetImageCount(); } auto GetCurrentSwapchainImage() -> const SwapchainImage& override { return m_Swapchain->GetCurrentSwapchainImage(); } + [[nodiscard]] auto GetDevice() const -> nvrhi::IDevice* override; [[nodiscard]] constexpr auto GetVulkanInstance() const -> VkInstance { return m_Instance; } [[nodiscard]] constexpr auto GetPhysicalDevice() const -> const ScopedPtr& { return m_PhysicalDevice; } diff --git a/EppoEngine/Source/Platform/Vulkan/VulkanShader.cpp b/EppoEngine/Source/Platform/Vulkan/VulkanShader.cpp index 4d11f04b..ae60af9a 100644 --- a/EppoEngine/Source/Platform/Vulkan/VulkanShader.cpp +++ b/EppoEngine/Source/Platform/Vulkan/VulkanShader.cpp @@ -1,17 +1,11 @@ #include "pch.h" #include "Platform/Vulkan/VulkanShader.h" +#include "Platform/ComPtr.h" #include "Renderer/DeviceManager.h" -#if defined(EP_PLATFORM_WINDOWS) - #include -#else - #include -#endif - #include -#include #include #include @@ -74,7 +68,7 @@ namespace Eppo return E_FAIL; } - CComPtr blob; + ComPtr blob; if (FAILED(m_Utils->CreateBlob(source->data(), static_cast(source->size()), DXC_CP_UTF8, &blob))) return E_FAIL; @@ -298,8 +292,8 @@ namespace Eppo auto VulkanShader::Compile(const nvrhi::ShaderType type) -> bool { // Create compiler - CComPtr utils; - CComPtr compiler; + ComPtr utils; + ComPtr compiler; if (FAILED(DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&utils))) || FAILED(DxcCreateInstance(CLSID_DxcCompiler, IID_PPV_ARGS(&compiler)))) { @@ -310,11 +304,11 @@ namespace Eppo // Create include handler. A packed shader gets the pack-backed one and never the default: // the default reads from disk, which a deployed game has none of. const bool packed = !m_Specification.Source.empty(); - PackedIncludeHandler packedIncludeHandler(utils, m_Specification.Includes); - CComPtr diskIncludeHandler; + PackedIncludeHandler packedIncludeHandler(utils.Get(), m_Specification.Includes); + ComPtr diskIncludeHandler; if (!packed) utils->CreateDefaultIncludeHandler(&diskIncludeHandler); - IDxcIncludeHandler* includeHandler = packed ? static_cast(&packedIncludeHandler) : diskIncludeHandler.p; + IDxcIncludeHandler* includeHandler = packed ? static_cast(&packedIncludeHandler) : diskIncludeHandler.Get(); // Command line args for compiler. A packed shader is named relative to the virtual Resources/Shaders // root, so DXC hands its #include paths to the handler the way the pack keys them. @@ -365,14 +359,14 @@ namespace Eppo }; // Execute compiler - CComPtr result; + ComPtr result; if (FAILED(compiler->Compile(&srcBuffer, args, _countof(args), includeHandler, IID_PPV_ARGS(&result))) || !result) { Log::Error("Invoking the compiler for shader '{}' failed!", m_Specification.Name); return false; } - CComPtr errors = nullptr; + ComPtr errors; result->GetOutput(DXC_OUT_ERRORS, IID_PPV_ARGS(&errors), nullptr); if (errors != nullptr && errors->GetStringLength() != 0) @@ -389,8 +383,8 @@ namespace Eppo } // Save shader binary - CComPtr binary = nullptr; - CComPtr binaryName = nullptr; + ComPtr binary; + ComPtr binaryName; result->GetOutput(DXC_OUT_OBJECT, IID_PPV_ARGS(&binary), &binaryName); if (binary == nullptr) diff --git a/EppoEngine/Source/Renderer/IndexBuffer.cpp b/EppoEngine/Source/Renderer/Buffer/IndexBuffer.cpp similarity index 99% rename from EppoEngine/Source/Renderer/IndexBuffer.cpp rename to EppoEngine/Source/Renderer/Buffer/IndexBuffer.cpp index 6f94ba75..fb062c7f 100644 --- a/EppoEngine/Source/Renderer/IndexBuffer.cpp +++ b/EppoEngine/Source/Renderer/Buffer/IndexBuffer.cpp @@ -1,5 +1,5 @@ #include "pch.h" -#include "Renderer/IndexBuffer.h" +#include "Renderer/Buffer/IndexBuffer.h" #include "Renderer/DeviceManager.h" #include "Renderer/Mesh.h" @@ -240,4 +240,4 @@ namespace Eppo } } } -} \ No newline at end of file +} diff --git a/EppoEngine/Source/Renderer/IndexBuffer.h b/EppoEngine/Source/Renderer/Buffer/IndexBuffer.h similarity index 100% rename from EppoEngine/Source/Renderer/IndexBuffer.h rename to EppoEngine/Source/Renderer/Buffer/IndexBuffer.h diff --git a/EppoEngine/Source/Renderer/StorageBuffer.cpp b/EppoEngine/Source/Renderer/Buffer/StorageBuffer.cpp similarity index 97% rename from EppoEngine/Source/Renderer/StorageBuffer.cpp rename to EppoEngine/Source/Renderer/Buffer/StorageBuffer.cpp index 357de282..3ad713a1 100644 --- a/EppoEngine/Source/Renderer/StorageBuffer.cpp +++ b/EppoEngine/Source/Renderer/Buffer/StorageBuffer.cpp @@ -1,5 +1,5 @@ #include "pch.h" -#include "Renderer/StorageBuffer.h" +#include "Renderer/Buffer/StorageBuffer.h" #include "Renderer/DeviceManager.h" diff --git a/EppoEngine/Source/Renderer/StorageBuffer.h b/EppoEngine/Source/Renderer/Buffer/StorageBuffer.h similarity index 100% rename from EppoEngine/Source/Renderer/StorageBuffer.h rename to EppoEngine/Source/Renderer/Buffer/StorageBuffer.h diff --git a/EppoEngine/Source/Renderer/UniformBuffer.cpp b/EppoEngine/Source/Renderer/Buffer/UniformBuffer.cpp similarity index 97% rename from EppoEngine/Source/Renderer/UniformBuffer.cpp rename to EppoEngine/Source/Renderer/Buffer/UniformBuffer.cpp index c7419538..f7550ec4 100644 --- a/EppoEngine/Source/Renderer/UniformBuffer.cpp +++ b/EppoEngine/Source/Renderer/Buffer/UniformBuffer.cpp @@ -1,5 +1,5 @@ #include "pch.h" -#include "Renderer/UniformBuffer.h" +#include "Renderer/Buffer/UniformBuffer.h" #include "Renderer/DeviceManager.h" diff --git a/EppoEngine/Source/Renderer/UniformBuffer.h b/EppoEngine/Source/Renderer/Buffer/UniformBuffer.h similarity index 100% rename from EppoEngine/Source/Renderer/UniformBuffer.h rename to EppoEngine/Source/Renderer/Buffer/UniformBuffer.h diff --git a/EppoEngine/Source/Renderer/VertexBuffer.cpp b/EppoEngine/Source/Renderer/Buffer/VertexBuffer.cpp similarity index 99% rename from EppoEngine/Source/Renderer/VertexBuffer.cpp rename to EppoEngine/Source/Renderer/Buffer/VertexBuffer.cpp index c5951e1b..9faff253 100644 --- a/EppoEngine/Source/Renderer/VertexBuffer.cpp +++ b/EppoEngine/Source/Renderer/Buffer/VertexBuffer.cpp @@ -1,5 +1,5 @@ #include "pch.h" -#include "Renderer/VertexBuffer.h" +#include "Renderer/Buffer/VertexBuffer.h" #include "Renderer/DeviceManager.h" #include "Renderer/Mesh.h" @@ -328,4 +328,4 @@ namespace Eppo } } } -} \ No newline at end of file +} diff --git a/EppoEngine/Source/Renderer/VertexBuffer.h b/EppoEngine/Source/Renderer/Buffer/VertexBuffer.h similarity index 100% rename from EppoEngine/Source/Renderer/VertexBuffer.h rename to EppoEngine/Source/Renderer/Buffer/VertexBuffer.h diff --git a/EppoEngine/Source/Renderer/DescriptorManager.h b/EppoEngine/Source/Renderer/DescriptorManager.h index fcb3e066..d19b27bf 100644 --- a/EppoEngine/Source/Renderer/DescriptorManager.h +++ b/EppoEngine/Source/Renderer/DescriptorManager.h @@ -1,9 +1,9 @@ #pragma once +#include "Renderer/Buffer/StorageBuffer.h" +#include "Renderer/Buffer/UniformBuffer.h" #include "Renderer/DeviceManager.h" #include "Renderer/Image.h" -#include "Renderer/StorageBuffer.h" -#include "Renderer/UniformBuffer.h" #include diff --git a/EppoEngine/Source/Renderer/DeviceManager.cpp b/EppoEngine/Source/Renderer/DeviceManager.cpp index a5fd393f..85df632c 100644 --- a/EppoEngine/Source/Renderer/DeviceManager.cpp +++ b/EppoEngine/Source/Renderer/DeviceManager.cpp @@ -16,7 +16,6 @@ namespace Eppo { EP_ASSERT(params.API != RendererAPI::None, "No renderer api selected!"); #if !defined(EP_PLATFORM_WINDOWS) - EP_ASSERT(params.API != RendererAPI::DX11, "DX11 renderer api selected on a non windows target!"); EP_ASSERT(params.API != RendererAPI::DX12, "DX12 renderer api selected on a non windows target!"); #endif EP_ASSERT(params.MaxFramesInFlight >= 2); @@ -24,12 +23,6 @@ namespace Eppo switch (params.API) { #if defined(EP_PLATFORM_WINDOWS) - case RendererAPI::DX11: - { - EP_ASSERT(false, "Currently we do not support DX11!"); - break; - } - case RendererAPI::DX12: { EP_ASSERT(false, "Currently we do not support DX12!"); diff --git a/EppoEngine/Source/Renderer/DeviceManager.h b/EppoEngine/Source/Renderer/DeviceManager.h index b2a5135e..10f31719 100644 --- a/EppoEngine/Source/Renderer/DeviceManager.h +++ b/EppoEngine/Source/Renderer/DeviceManager.h @@ -13,7 +13,6 @@ namespace Eppo enum class RendererAPI { None, - DX11, DX12, Vulkan, }; diff --git a/EppoEngine/Source/Renderer/Mesh.h b/EppoEngine/Source/Renderer/Mesh.h index 8529b9fb..233947fe 100644 --- a/EppoEngine/Source/Renderer/Mesh.h +++ b/EppoEngine/Source/Renderer/Mesh.h @@ -1,11 +1,11 @@ #pragma once #include "Asset/Asset.h" +#include "Renderer/Buffer/IndexBuffer.h" +#include "Renderer/Buffer/VertexBuffer.h" #include "Renderer/Image.h" -#include "Renderer/IndexBuffer.h" #include "Renderer/Material.h" #include "Renderer/Sampler.h" -#include "Renderer/VertexBuffer.h" #include diff --git a/EppoEngine/Source/Renderer/RenderPass.cpp b/EppoEngine/Source/Renderer/RenderPass.cpp index 1bd57571..1b9dece3 100644 --- a/EppoEngine/Source/Renderer/RenderPass.cpp +++ b/EppoEngine/Source/Renderer/RenderPass.cpp @@ -1,13 +1,13 @@ #include "pch.h" #include "Renderer/RenderPass.h" +#include "Renderer/Buffer/StorageBuffer.h" +#include "Renderer/Buffer/UniformBuffer.h" #include "Renderer/DescriptorManager.h" #include "Renderer/DeviceManager.h" #include "Renderer/Image.h" #include "Renderer/Renderer.h" #include "Renderer/Sampler.h" -#include "Renderer/StorageBuffer.h" -#include "Renderer/UniformBuffer.h" namespace Eppo { diff --git a/EppoEngine/Source/Renderer/SceneRenderer.h b/EppoEngine/Source/Renderer/SceneRenderer.h index 0aceb3b9..c7ebd610 100644 --- a/EppoEngine/Source/Renderer/SceneRenderer.h +++ b/EppoEngine/Source/Renderer/SceneRenderer.h @@ -1,13 +1,13 @@ #pragma once +#include "Renderer/Buffer/StorageBuffer.h" +#include "Renderer/Buffer/UniformBuffer.h" #include "Renderer/Camera/EditorCamera.h" #include "Renderer/Camera/SceneCamera.h" #include "Renderer/Mesh.h" #include "Renderer/RenderCommandBuffer.h" #include "Renderer/RenderPass.h" #include "Renderer/Sampler.h" -#include "Renderer/StorageBuffer.h" -#include "Renderer/UniformBuffer.h" #include "Scene/Entity.h" #include "Scene/Scene.h" From 6d927e8f5df9174c2501b7f96ce1275517fd54fe Mon Sep 17 00:00:00 2001 From: Niels Eppenhof Date: Sun, 30 Aug 2026 03:36:45 +0200 Subject: [PATCH 2/6] Linux fix for ComPtr --- EppoEngine/Source/Platform/ComPtr.h | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/EppoEngine/Source/Platform/ComPtr.h b/EppoEngine/Source/Platform/ComPtr.h index f0560abc..3dc1d23a 100644 --- a/EppoEngine/Source/Platform/ComPtr.h +++ b/EppoEngine/Source/Platform/ComPtr.h @@ -8,6 +8,32 @@ #else #include template - using ComPtr = CComPtr; + class ComPtr : public CComPtr + { + using Base = CComPtr; + public: + using Base::Base; + using Base::operator=; + + ComPtr() noexcept = default; + + T* Get() const noexcept { return this->p; } + + T* const* GetAddressOf() const noexcept { return &this->p; } + T** GetAddressOf() noexcept + { + assert(this->p == nullptr); + return &this->p; + } + + T** ReleaseAndGetAddressOf() noexcept + { + this->Release(); + return &this->p; + } + + void Reset() noexcept { this->Release(); } + explicit operator bool() const noexcept { return this->p != nullptr; } + }; #endif // clang-format on From fb2d53c70cb4852878a69bdaa13c935281a09b44 Mon Sep 17 00:00:00 2001 From: Niels Eppenhof Date: Sun, 30 Aug 2026 03:44:46 +0200 Subject: [PATCH 3/6] Fix CI --- .../Platform/Vulkan/DeviceManagerVK.cpp | 13 +- .../Source/Platform/Vulkan/LogicalDevice.cpp | 4 +- EppoEngine/Source/Platform/Vulkan/Vulkan.h | 6 - .../Source/Renderer/RenderPass.cpp | 144 +++++++++--------- 4 files changed, 81 insertions(+), 86 deletions(-) diff --git a/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.cpp b/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.cpp index 596e9445..68fe7841 100644 --- a/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.cpp +++ b/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.cpp @@ -42,7 +42,7 @@ namespace Eppo m_LogicalDevice = nullptr; m_PhysicalDevice = nullptr; - if (g_EnableValidationLayers) + if (s_EnableValidationLayers) DestroyDebugUtilsMessengerEXT(m_Instance, m_DebugMessenger, nullptr); vkDestroyInstance(m_Instance, nullptr); @@ -84,9 +84,8 @@ namespace Eppo const std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); -#if !defined(EP_DIST) - m_Params.RequiredVulkanInstanceExtensions.emplace_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); -#endif + if (s_EnableValidationLayers) + m_Params.RequiredVulkanInstanceExtensions.emplace_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); for (const auto& extension : extensions) m_Params.RequiredVulkanInstanceExtensions.emplace_back(extension); @@ -106,7 +105,7 @@ namespace Eppo }; VkDebugUtilsMessengerCreateInfoEXT debugMessengerInfo{}; - if (g_EnableValidationLayers) + if (s_EnableValidationLayers) { instanceInfo.enabledLayerCount = static_cast(g_ValidationLayers.size()); instanceInfo.ppEnabledLayerNames = g_ValidationLayers.data(); @@ -127,7 +126,7 @@ namespace Eppo VK_CHECK(vkCreateInstance(&instanceInfo, nullptr, &m_Instance), "Failed to create vulkan instance!"); EP_ASSERT(m_Instance); - if (g_EnableValidationLayers) + if (s_EnableValidationLayers) { VK_CHECK( CreateDebugUtilsMessengerEXT(m_Instance, &debugMessengerInfo, nullptr, &m_DebugMessenger), @@ -162,7 +161,7 @@ namespace Eppo m_Device = nvrhi::vulkan::createDevice(deviceDesc); - if (g_EnableValidationLayers) + if (s_EnableValidationLayers) m_ValidationLayer = nvrhi::validation::createValidationLayer(m_Device); } } diff --git a/EppoEngine/Source/Platform/Vulkan/LogicalDevice.cpp b/EppoEngine/Source/Platform/Vulkan/LogicalDevice.cpp index 7b0b134a..e4423ba6 100644 --- a/EppoEngine/Source/Platform/Vulkan/LogicalDevice.cpp +++ b/EppoEngine/Source/Platform/Vulkan/LogicalDevice.cpp @@ -1,6 +1,8 @@ #include "pch.h" #include "Platform/Vulkan/LogicalDevice.h" +#include "Renderer/DeviceManager.h" + namespace Eppo { LogicalDevice::LogicalDevice(const ScopedPtr& physicalDevice) @@ -89,7 +91,7 @@ namespace Eppo .ppEnabledExtensionNames = g_DeviceExtensions.data(), }; - if (g_EnableValidationLayers) + if (s_EnableValidationLayers) { deviceInfo.enabledLayerCount = static_cast(g_ValidationLayers.size()); deviceInfo.ppEnabledLayerNames = g_ValidationLayers.data(); diff --git a/EppoEngine/Source/Platform/Vulkan/Vulkan.h b/EppoEngine/Source/Platform/Vulkan/Vulkan.h index 3a628e9e..e49020e7 100644 --- a/EppoEngine/Source/Platform/Vulkan/Vulkan.h +++ b/EppoEngine/Source/Platform/Vulkan/Vulkan.h @@ -8,12 +8,6 @@ namespace Eppo if (fn != VK_SUCCESS) \ Log::Error(LogSource::Vulkan, msg); -#if !defined(EP_DIST) - constexpr bool g_EnableValidationLayers = true; -#else - constexpr bool g_EnableValidationLayers = false; -#endif - constexpr std::array g_ValidationLayers = { "VK_LAYER_KHRONOS_validation" }; constexpr std::array g_DeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME, VK_KHR_MAINTENANCE_1_EXTENSION_NAME, VK_GOOGLE_HLSL_FUNCTIONALITY_1_EXTENSION_NAME, VK_GOOGLE_USER_TYPE_EXTENSION_NAME, diff --git a/EppoEngineTesting/Source/Renderer/RenderPass.cpp b/EppoEngineTesting/Source/Renderer/RenderPass.cpp index c0e97a06..50d6c21d 100644 --- a/EppoEngineTesting/Source/Renderer/RenderPass.cpp +++ b/EppoEngineTesting/Source/Renderer/RenderPass.cpp @@ -1,6 +1,8 @@ #include "TestSupport/EppoTest.h" #include "TestSupport/AppHarness.h" +#include "Renderer/Buffer/StorageBuffer.h" +#include "Renderer/Buffer/UniformBuffer.h" #include "Renderer/DescriptorManager.h" #include "Renderer/DeviceManager.h" #include "Renderer/Framebuffer.h" @@ -10,36 +12,33 @@ #include "Renderer/Renderer.h" #include "Renderer/Sampler.h" #include "Renderer/Shader.h" -#include "Renderer/StorageBuffer.h" -#include "Renderer/UniformBuffer.h" using namespace Eppo; namespace { - auto MakeGeometryPipeline() -> Ref - { - const auto& renderer = DeviceManager::Get()->GetRenderer(); - - const FramebufferSpecification framebufferSpec{ - .Width = 256, - .Height = 256, - .Attachments = { nvrhi::Format::RGBA8_UNORM, nvrhi::Format::RGBA16_FLOAT, nvrhi::Format::RGBA16_FLOAT, - nvrhi::Format::D32 }, - .DebugName = "Framebuffer RenderPassTest", - }; - - const auto framebuffer = CreateRef(framebufferSpec); - - const PipelineSpecification pipelineSpec{ - .Shader = renderer->GetShader("geometry"), - .CullMode = nvrhi::RasterCullMode::Front, - .DepthTestEnable = true, - .DepthWriteEnable = true, - }; - - return CreateRef(pipelineSpec, framebuffer->GetFramebuffer()->getFramebufferInfo()); - } + auto MakeGeometryPipeline() -> Ref + { + const auto& renderer = DeviceManager::Get()->GetRenderer(); + + const FramebufferSpecification framebufferSpec{ + .Width = 256, + .Height = 256, + .Attachments = { nvrhi::Format::RGBA8_UNORM, nvrhi::Format::RGBA16_FLOAT, nvrhi::Format::RGBA16_FLOAT, nvrhi::Format::D32 }, + .DebugName = "Framebuffer RenderPassTest", + }; + + const auto framebuffer = CreateRef(framebufferSpec); + + const PipelineSpecification pipelineSpec{ + .Shader = renderer->GetShader("geometry"), + .CullMode = nvrhi::RasterCullMode::Front, + .DepthTestEnable = true, + .DepthWriteEnable = true, + }; + + return CreateRef(pipelineSpec, framebuffer->GetFramebuffer()->getFramebufferInfo()); + } auto MakeCompositePipeline() -> Ref { @@ -64,9 +63,10 @@ namespace auto MakeSharedBindingPipeline() -> Ref { - const auto shader = Shader::Create(ShaderSpecification{ - .Name = "SharedLogicalBindingRenderPassTest", - .Source = R"( + const auto shader = Shader::Create( + ShaderSpecification{ + .Name = "SharedLogicalBindingRenderPassTest", + .Source = R"( struct Input { float3 Position : POSITION0; @@ -90,7 +90,8 @@ float4 PSMain() : SV_Target return float4(1.0, 1.0, 1.0, 1.0); } )", - }); + } + ); const FramebufferSpecification framebufferSpec{ .Width = 256, @@ -110,11 +111,7 @@ return float4(1.0, 1.0, 1.0, 1.0); ); } - auto SetGeometryInputs( - RenderPass& pass, - const Ref& camera, - const Ref& instances - ) -> void + auto SetGeometryInputs(RenderPass& pass, const Ref& camera, const Ref& instances) -> void { const auto drawData = CreateRef(80, 80, "TestSB Draw Data"); const auto materialData = CreateRef(96, 96, "TestSB Material Data"); @@ -124,9 +121,8 @@ return float4(1.0, 1.0, 1.0, 1.0); pass.SetInput(0, 2, materialData); } - [[nodiscard]] auto FindBinding( - const nvrhi::BindingSetDesc& desc, const uint32_t slot, const nvrhi::ResourceType type - ) -> const nvrhi::BindingSetItem* + [[nodiscard]] auto FindBinding(const nvrhi::BindingSetDesc& desc, const uint32_t slot, const nvrhi::ResourceType type) + -> const nvrhi::BindingSetItem* { const auto it = std::ranges::find_if( desc.bindings, @@ -140,39 +136,43 @@ return float4(1.0, 1.0, 1.0, 1.0); [[nodiscard]] auto MakeTestImage() -> Ref { - return Image::Create(ImageSpecification{ - .ImageFormat = nvrhi::Format::RGBA8_UNORM, - .Width = 4, - .Height = 4, - .DebugName = "Image RenderPassTest", - }); + return Image::Create( + ImageSpecification{ + .ImageFormat = nvrhi::Format::RGBA8_UNORM, + .Width = 4, + .Height = 4, + .DebugName = "Image RenderPassTest", + } + ); } } TEST(Renderer, RenderPass_ConstructionStoresSpecification) { - if (!Testing::AppHarness::IsAvailable()) - return; + if (!Testing::AppHarness::IsAvailable()) + return; - const RenderPass pass(RenderPassSpecification{ - .Name = "TestPass", - .Pipeline = MakeGeometryPipeline(), - .ClearColorOnLoad = true, - .ClearDepthOnLoad = false, - }); + const RenderPass pass( + RenderPassSpecification{ + .Name = "TestPass", + .Pipeline = MakeGeometryPipeline(), + .ClearColorOnLoad = true, + .ClearDepthOnLoad = false, + } + ); - EXPECT_EQ(std::string("TestPass"), pass.GetName()); + EXPECT_EQ(std::string("TestPass"), pass.GetName()); EXPECT_TRUE(pass.GetSpecification().Pipeline); - EXPECT_TRUE(pass.GetSpecification().ClearColorOnLoad); - EXPECT_TRUE(!pass.GetSpecification().ClearDepthOnLoad); + EXPECT_TRUE(pass.GetSpecification().ClearColorOnLoad); + EXPECT_TRUE(!pass.GetSpecification().ClearDepthOnLoad); } TEST(Renderer, RenderPass_DefaultConstructionHasNoPipelineOrBindingSets) { - const RenderPass pass; + const RenderPass pass; - EXPECT_TRUE(!pass.GetPipeline()); - EXPECT_TRUE(pass.GetBindingSets().empty()); + EXPECT_TRUE(!pass.GetPipeline()); + EXPECT_TRUE(pass.GetBindingSets().empty()); } TEST(Renderer, RenderPass_StatisticsAreOwnedAndMutable) @@ -188,26 +188,26 @@ TEST(Renderer, RenderPass_StatisticsAreOwnedAndMutable) TEST(Renderer, RenderPass_BakeMergesBoundAndBindlessSetsWithoutGaps) { - if (!Testing::AppHarness::IsAvailable()) - return; + if (!Testing::AppHarness::IsAvailable()) + return; - const auto pipeline = MakeGeometryPipeline(); - const auto camera = CreateRef(4096, "TestCB Camera"); - const auto instances = CreateRef(sizeof(glm::mat4), 4096, "TestSSBO Instances"); + const auto pipeline = MakeGeometryPipeline(); + const auto camera = CreateRef(4096, "TestCB Camera"); + const auto instances = CreateRef(sizeof(glm::mat4), 4096, "TestSSBO Instances"); - RenderPass pass(RenderPassSpecification{ .Name = "Geometry", .Pipeline = pipeline }); + RenderPass pass(RenderPassSpecification{ .Name = "Geometry", .Pipeline = pipeline }); SetGeometryInputs(pass, camera, instances); - pass.Bake(); + pass.Bake(); - const auto& descriptorManager = DeviceManager::Get()->GetRenderer()->GetDescriptorManager(); - const auto& bindingSets = pass.GetBindingSets(); + const auto& descriptorManager = DeviceManager::Get()->GetRenderer()->GetDescriptorManager(); + const auto& bindingSets = pass.GetBindingSets(); - EXPECT_EQ(3u, static_cast(bindingSets.size())); - EXPECT_TRUE(bindingSets[0] != nullptr); - EXPECT_TRUE(bindingSets[0]->getDesc() != nullptr); - EXPECT_EQ(1ul, bindingSets[0]->GetRefCount()); - EXPECT_TRUE(bindingSets[1] == descriptorManager->GetResourceDT().Get()); - EXPECT_TRUE(bindingSets[2] == descriptorManager->GetSamplerDT().Get()); + EXPECT_EQ(3u, static_cast(bindingSets.size())); + EXPECT_TRUE(bindingSets[0] != nullptr); + EXPECT_TRUE(bindingSets[0]->getDesc() != nullptr); + EXPECT_EQ(1ul, bindingSets[0]->GetRefCount()); + EXPECT_TRUE(bindingSets[1] == descriptorManager->GetResourceDT().Get()); + EXPECT_TRUE(bindingSets[2] == descriptorManager->GetSamplerDT().Get()); } TEST(Renderer, RenderPass_BakeDerivesPushConstantsFromShaderReflection) From 866b036529a648a97c649209b59a5541514c012d Mon Sep 17 00:00:00 2001 From: Niels Eppenhof Date: Sun, 30 Aug 2026 04:00:00 +0200 Subject: [PATCH 4/6] Fix undefined variable --- EppoEngine/Source/Renderer/DeviceManager.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/EppoEngine/Source/Renderer/DeviceManager.h b/EppoEngine/Source/Renderer/DeviceManager.h index 10f31719..113e816d 100644 --- a/EppoEngine/Source/Renderer/DeviceManager.h +++ b/EppoEngine/Source/Renderer/DeviceManager.h @@ -10,6 +10,12 @@ namespace Eppo { class DeviceManagerVK; +#if !defined(EP_DIST) + constexpr bool s_EnableValidationLayers = true; +#else + constexpr bool s_EnableValidationLayers = false; +#endif + enum class RendererAPI { None, From a1d42962353e4ed4b08df0ea7dacff6a29621a44 Mon Sep 17 00:00:00 2001 From: Niels Eppenhof Date: Thu, 3 Sep 2026 08:22:53 +0200 Subject: [PATCH 5/6] Added DX12 Shader, Swapchain, GpuProfiler and DeviceManager --- Dependencies/Ports/nvrhi/portfile.cmake | 4 +- Dependencies/Ports/nvrhi/vcpkg.json | 3 +- EppoEditor/Resources/Shaders/geometry.hlsl | 1 + EppoEditor/Resources/Shaders/imgui.hlsl | 2 +- EppoEditor/Resources/Shaders/shadowDepth.hlsl | 1 + EppoEngine/Source/Core/Application.cpp | 4 +- EppoEngine/Source/Core/Application.h | 1 + EppoEngine/Source/Core/Log.cpp | 3 + EppoEngine/Source/Core/Log.h | 28 +- EppoEngine/Source/ImGui/ImGuiLayer.cpp | 38 +- EppoEngine/Source/ImGui/ImGuiRenderer.cpp | 11 - EppoEngine/Source/ImGui/ImGuiRenderer.h | 10 +- EppoEngine/Source/Platform/DX12/DX12.h | 23 ++ .../Source/Platform/DX12/DX12GpuProfiler.cpp | 39 ++ .../Source/Platform/DX12/DX12GpuProfiler.h | 21 + .../Source/Platform/DX12/DX12Shader.cpp | 357 +++++++++++++++++ EppoEngine/Source/Platform/DX12/DX12Shader.h | 15 + .../Source/Platform/DX12/DX12Swapchain.cpp | 228 +++++++++++ .../Source/Platform/DX12/DX12Swapchain.h | 33 ++ .../Platform/DX12/DeviceManagerDX12.cpp | 145 +++++++ .../Source/Platform/DX12/DeviceManagerDX12.h | 42 ++ .../Platform/Vulkan/DeviceManagerVK.cpp | 19 +- .../Source/Platform/Vulkan/DeviceManagerVK.h | 11 +- .../Platform/Vulkan/VulkanGpuProfiler.cpp | 8 +- .../Source/Platform/Vulkan/VulkanShader.cpp | 324 ---------------- .../Source/Platform/Vulkan/VulkanShader.h | 4 +- .../{Swapchain.cpp => VulkanSwapchain.cpp} | 66 ++-- .../Vulkan/{Swapchain.h => VulkanSwapchain.h} | 43 +- EppoEngine/Source/Renderer/DeviceManager.cpp | 9 +- EppoEngine/Source/Renderer/DeviceManager.h | 31 +- EppoEngine/Source/Renderer/GpuProfiler.cpp | 23 +- EppoEngine/Source/Renderer/GpuProfiler.h | 20 +- EppoEngine/Source/Renderer/Image.cpp | 16 +- .../Source/Renderer/RenderCommandBuffer.cpp | 1 + EppoEngine/Source/Renderer/Renderer.cpp | 6 + EppoEngine/Source/Renderer/Renderer.h | 1 + EppoEngine/Source/Renderer/Shader.cpp | 366 +++++++++++++++++- EppoEngine/Source/Renderer/Shader.h | 2 + EppoEngine/Source/Renderer/Swapchain.cpp | 36 ++ EppoEngine/Source/Renderer/Swapchain.h | 50 +++ EppoEngine/premake5.lua | 1 + .../Source/Renderer/DescriptorManager.cpp | 15 +- .../Source/Renderer/DeviceManager.cpp | 3 + .../Source/Renderer/GpuProfiler.cpp | 99 +++++ .../Source/Renderer/SceneRendering.cpp | 5 + EppoEngineTesting/Source/Renderer/Shader.cpp | 19 +- .../Source/Renderer/Swapchain.cpp | 52 +++ .../Source/TestSupport/AppHarness.cpp | 15 + Scripts/Premake/Testing.lua | 9 +- vcpkg.json | 2 +- 50 files changed, 1761 insertions(+), 504 deletions(-) create mode 100644 EppoEngine/Source/Platform/DX12/DX12.h create mode 100644 EppoEngine/Source/Platform/DX12/DX12GpuProfiler.cpp create mode 100644 EppoEngine/Source/Platform/DX12/DX12GpuProfiler.h create mode 100644 EppoEngine/Source/Platform/DX12/DX12Shader.cpp create mode 100644 EppoEngine/Source/Platform/DX12/DX12Shader.h create mode 100644 EppoEngine/Source/Platform/DX12/DX12Swapchain.cpp create mode 100644 EppoEngine/Source/Platform/DX12/DX12Swapchain.h create mode 100644 EppoEngine/Source/Platform/DX12/DeviceManagerDX12.cpp create mode 100644 EppoEngine/Source/Platform/DX12/DeviceManagerDX12.h rename EppoEngine/Source/Platform/Vulkan/{Swapchain.cpp => VulkanSwapchain.cpp} (87%) rename EppoEngine/Source/Platform/Vulkan/{Swapchain.h => VulkanSwapchain.h} (57%) create mode 100644 EppoEngine/Source/Renderer/Swapchain.cpp create mode 100644 EppoEngine/Source/Renderer/Swapchain.h create mode 100644 EppoEngineTesting/Source/Renderer/GpuProfiler.cpp create mode 100644 EppoEngineTesting/Source/Renderer/Swapchain.cpp diff --git a/Dependencies/Ports/nvrhi/portfile.cmake b/Dependencies/Ports/nvrhi/portfile.cmake index f9802842..ff5fc5df 100644 --- a/Dependencies/Ports/nvrhi/portfile.cmake +++ b/Dependencies/Ports/nvrhi/portfile.cmake @@ -1,8 +1,8 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO NVIDIA-RTX/NVRHI - REF 54100464714de88a5a5059d25808f5ccb914ad7d - SHA512 56d5de1cc0840e29d8df976a5fe7b13d676c110ba24c09ff5e0caaa73f4aa56cc78d2ec2c31b1cb8da9f5b099c8b8598410792f8343a77ba928da28ba8146b1f + REF 2867a51e30a22aaea30c9c91b6b8bbc05cb48938 + SHA512 800a6bd106bba983ed98f71140a65ef6bf48502db3dbd9efd32814aad3822207018484195adc8a577f8688654ffd94e64846de043f906161f138e2aab04a7bdc HEAD_REF main PATCHES fix-vcpkg-deps.patch diff --git a/Dependencies/Ports/nvrhi/vcpkg.json b/Dependencies/Ports/nvrhi/vcpkg.json index 96b28839..abb6ace0 100644 --- a/Dependencies/Ports/nvrhi/vcpkg.json +++ b/Dependencies/Ports/nvrhi/vcpkg.json @@ -1,7 +1,6 @@ { "name": "nvrhi", - "version-date": "2026-02-26", - "port-version": 1, + "version-date": "2026-06-01", "description": "NVIDIA Rendering Hardware Interface abstraction layer for D3D11, D3D12, and Vulkan", "homepage": "https://github.com/NVIDIA-RTX/NVRHI", "license": "MIT", diff --git a/EppoEditor/Resources/Shaders/geometry.hlsl b/EppoEditor/Resources/Shaders/geometry.hlsl index afffd3d0..440d3d19 100644 --- a/EppoEditor/Resources/Shaders/geometry.hlsl +++ b/EppoEditor/Resources/Shaders/geometry.hlsl @@ -36,6 +36,7 @@ struct DrawData float4x4 Transform; uint InstanceOffset; uint MaterialIndex; + uint2 Padding; }; StructuredBuffer uDrawData : register(t1, space0); diff --git a/EppoEditor/Resources/Shaders/imgui.hlsl b/EppoEditor/Resources/Shaders/imgui.hlsl index fd0dc971..a4f65a22 100644 --- a/EppoEditor/Resources/Shaders/imgui.hlsl +++ b/EppoEditor/Resources/Shaders/imgui.hlsl @@ -13,7 +13,7 @@ struct PushConstants float2 Translate; }; PUSH_CONSTANTS -ConstantBuffer uPC : register(b0, space1); +ConstantBuffer uPC : register(b0, space0); Texture2D uTexture : register(t0, space0); SamplerState uSampler : register(s0, space0); diff --git a/EppoEditor/Resources/Shaders/shadowDepth.hlsl b/EppoEditor/Resources/Shaders/shadowDepth.hlsl index f1ea144a..caf95385 100644 --- a/EppoEditor/Resources/Shaders/shadowDepth.hlsl +++ b/EppoEditor/Resources/Shaders/shadowDepth.hlsl @@ -74,6 +74,7 @@ struct DrawData float4x4 Transform; uint InstanceOffset; uint MaterialIndex; + uint2 Padding; }; StructuredBuffer uDrawData : register(t1, space0); diff --git a/EppoEngine/Source/Core/Application.cpp b/EppoEngine/Source/Core/Application.cpp index 7fa138f6..0a38e26a 100644 --- a/EppoEngine/Source/Core/Application.cpp +++ b/EppoEngine/Source/Core/Application.cpp @@ -33,9 +33,9 @@ namespace Eppo } ); - // Create device manager (dx11/dx12/vk) + // Create device manager (dx12/vk) const DeviceParams deviceParams{ - .API = RendererAPI::Vulkan, + .API = m_Params.RendererAPI, .VSync = m_Params.VSync, }; diff --git a/EppoEngine/Source/Core/Application.h b/EppoEngine/Source/Core/Application.h index 7883124a..3d110932 100644 --- a/EppoEngine/Source/Core/Application.h +++ b/EppoEngine/Source/Core/Application.h @@ -32,6 +32,7 @@ namespace Eppo struct ApplicationParams { CommandLineArgs Args; + RendererAPI RendererAPI = RendererAPI::DX12; std::string Title = "EppoEngine"; uint32_t Width = 1600; diff --git a/EppoEngine/Source/Core/Log.cpp b/EppoEngine/Source/Core/Log.cpp index 128369ed..37970624 100644 --- a/EppoEngine/Source/Core/Log.cpp +++ b/EppoEngine/Source/Core/Log.cpp @@ -11,6 +11,7 @@ namespace Eppo Ref Log::s_GlfwLogger = nullptr; Ref Log::s_ScriptLogger = nullptr; Ref Log::s_VulkanLogger = nullptr; + Ref Log::s_DX12Logger = nullptr; auto Log::Init() -> void { @@ -40,6 +41,8 @@ namespace Eppo s_ScriptLogger->set_level(spdlog::level::trace); s_VulkanLogger = CreateRef("Vulkan", sinks); s_VulkanLogger->set_level(spdlog::level::trace); + s_DX12Logger = CreateRef("DX12", sinks); + s_DX12Logger->set_level(spdlog::level::trace); spdlog::set_default_logger(s_CoreLogger); s_CoreLogger->set_pattern("%^[%T.%e] [%n]: %v%$"); diff --git a/EppoEngine/Source/Core/Log.h b/EppoEngine/Source/Core/Log.h index 0e04dd76..71e0588c 100644 --- a/EppoEngine/Source/Core/Log.h +++ b/EppoEngine/Source/Core/Log.h @@ -19,7 +19,8 @@ namespace Eppo Core, Glfw, Script, - Vulkan + Vulkan, + DX12, }; class Log @@ -60,6 +61,12 @@ namespace Eppo break; } + case LogSource::DX12: + { + s_DX12Logger->trace(fmt, std::forward(args)...); + break; + } + default: { s_CoreLogger->trace(fmt, std::forward(args)...); @@ -97,6 +104,12 @@ namespace Eppo break; } + case LogSource::DX12: + { + s_DX12Logger->info(fmt, std::forward(args)...); + break; + } + default: { s_CoreLogger->info(fmt, std::forward(args)...); @@ -134,6 +147,12 @@ namespace Eppo break; } + case LogSource::DX12: + { + s_DX12Logger->warn(fmt, std::forward(args)...); + break; + } + default: { s_CoreLogger->warn(fmt, std::forward(args)...); @@ -171,6 +190,12 @@ namespace Eppo break; } + case LogSource::DX12: + { + s_DX12Logger->error(fmt, std::forward(args)...); + break; + } + default: { s_CoreLogger->error(fmt, std::forward(args)...); @@ -186,6 +211,7 @@ namespace Eppo static std::shared_ptr s_GlfwLogger; static std::shared_ptr s_ScriptLogger; static std::shared_ptr s_VulkanLogger; + static std::shared_ptr s_DX12Logger; }; } diff --git a/EppoEngine/Source/ImGui/ImGuiLayer.cpp b/EppoEngine/Source/ImGui/ImGuiLayer.cpp index caab83a5..41e520f6 100644 --- a/EppoEngine/Source/ImGui/ImGuiLayer.cpp +++ b/EppoEngine/Source/ImGui/ImGuiLayer.cpp @@ -4,10 +4,6 @@ #include "Core/Application.h" #include "Renderer/DeviceManager.h" -// TODO: TEMPORARY -#include "Platform/Vulkan/DeviceManagerVK.h" -#include "Platform/Vulkan/Swapchain.h" - #include #include #include @@ -16,14 +12,6 @@ namespace Eppo { static bool s_FontFallbackWarningLogged = false; - struct ImGuiViewportData - { - bool WindowOwned = false; - bool FrameAcquired = false; - Ref Swapchain = nullptr; - ScopedPtr Renderer = nullptr; - }; - namespace { // Cohesive dark theme with a pumpkin-orange (#E8641C) accent. Applied on top @@ -196,7 +184,7 @@ namespace Eppo { EP_PROFILE_FN("ImGuiLayer::Render") - const auto& dm = static_pointer_cast(DeviceManager::Get()); + const auto& dm = DeviceManager::Get(); ImGui::Render(); m_ImGuiRenderer->RenderToSwapchain(ImGui::GetMainViewport(), dm->GetSwapchain(), m_ClearMainSwapchainTarget); @@ -224,9 +212,6 @@ namespace Eppo auto ImGuiLayer::InitPlatformInterface() -> void { ImGuiPlatformIO& platformIO = ImGui::GetPlatformIO(); - if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) - EP_ASSERT(platformIO.Platform_CreateVkSurface != nullptr); - platformIO.Renderer_CreateWindow = ImGuiRenderer_CreateWindow; platformIO.Renderer_DestroyWindow = ImGuiRenderer_DestroyWindow; platformIO.Renderer_SetWindowSize = ImGuiRenderer_SetWindowSize; @@ -238,23 +223,18 @@ namespace Eppo { EP_PROFILE_FN("ImGuiLayer::ImGuiRenderer_CreateWindow") - const auto& dm = static_pointer_cast(DeviceManager::Get()); - const auto& platformIO = ImGui::GetPlatformIO(); + const auto& dm = DeviceManager::Get(); const auto data = IM_NEW(ImGuiViewportData)(); viewport->RendererUserData = data; - VkSurfaceKHR surface = nullptr; - VkInstance instance = dm->GetVulkanInstance(); - VK_CHECK( - platformIO.Platform_CreateVkSurface(viewport, reinterpret_cast(instance), nullptr, reinterpret_cast(&surface)), - "Failed to create vk surface for ImGui!" - ); + auto* glfwWindow = static_cast(viewport->PlatformHandle); + EP_ASSERT(glfwWindow, "ImGui platform window was created without a native window handle!"); - data->Swapchain = CreateScopedPtr(surface); - data->Swapchain->CreateSwapchain(static_cast(viewport->Size.x), static_cast(viewport->Size.y)); + data->Swapchain = dm->CreateSwapchain( + glfwWindow, static_cast(viewport->Size.x), static_cast(viewport->Size.y) + ); data->Renderer = CreateScopedPtr(); - data->WindowOwned = true; } auto ImGuiLayer::ImGuiRenderer_DestroyWindow(ImGuiViewport* viewport) -> void @@ -271,6 +251,10 @@ namespace Eppo EP_PROFILE_FN("ImGuiLayer::ImGuiRenderer_SetWindowSize") const auto* vd = static_cast(viewport->RendererUserData); + + // The render pass cache holds references to swapchain back buffers, which must be + // released before the swapchain recreates them + vd->Renderer->Resize(); vd->Swapchain->Resize(static_cast(size.x), static_cast(size.y)); } diff --git a/EppoEngine/Source/ImGui/ImGuiRenderer.cpp b/EppoEngine/Source/ImGui/ImGuiRenderer.cpp index 5fbc0207..dce86293 100644 --- a/EppoEngine/Source/ImGui/ImGuiRenderer.cpp +++ b/EppoEngine/Source/ImGui/ImGuiRenderer.cpp @@ -5,23 +5,12 @@ #include "Renderer/DeviceManager.h" #include "Renderer/Renderer.h" -// TODO: TEMPORARY -#include "Platform/Vulkan/Swapchain.h" - #include #include #include namespace Eppo { - struct ImGuiViewportData - { - bool WindowOwned = false; - bool FrameAcquired = false; - Ref Swapchain = nullptr; - ScopedPtr Renderer = nullptr; - }; - ImGuiRenderer::ImGuiRenderer() { const auto& dm = DeviceManager::Get(); diff --git a/EppoEngine/Source/ImGui/ImGuiRenderer.h b/EppoEngine/Source/ImGui/ImGuiRenderer.h index 8e957c4d..3715060e 100644 --- a/EppoEngine/Source/ImGui/ImGuiRenderer.h +++ b/EppoEngine/Source/ImGui/ImGuiRenderer.h @@ -3,6 +3,7 @@ #include "Renderer/Pipeline.h" #include "Renderer/RenderCommandBuffer.h" #include "Renderer/RenderPass.h" +#include "Renderer/Swapchain.h" #include #include @@ -11,7 +12,14 @@ namespace Eppo { - class Swapchain; + class ImGuiRenderer; + + struct ImGuiViewportData + { + bool FrameAcquired = false; + Ref Swapchain = nullptr; + ScopedPtr Renderer = nullptr; + }; class ImGuiRenderer { diff --git a/EppoEngine/Source/Platform/DX12/DX12.h b/EppoEngine/Source/Platform/DX12/DX12.h new file mode 100644 index 00000000..eb09ae43 --- /dev/null +++ b/EppoEngine/Source/Platform/DX12/DX12.h @@ -0,0 +1,23 @@ +#pragma once + +#if defined(EP_PLATFORM_WINDOWS) + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + + #include "Platform/ComPtr.h" + + #include + #include + +namespace Eppo +{ + #define DX_CHECK(fn, msg) \ + if (const HRESULT result__ = (fn); FAILED(result__)) \ + { \ + Log::Error(LogSource::DX12, "{} (HRESULT 0x{:08X})", msg, static_cast(result__)); \ + EP_ASSERT(false, msg); \ + } +} + +#endif diff --git a/EppoEngine/Source/Platform/DX12/DX12GpuProfiler.cpp b/EppoEngine/Source/Platform/DX12/DX12GpuProfiler.cpp new file mode 100644 index 00000000..c90a2f16 --- /dev/null +++ b/EppoEngine/Source/Platform/DX12/DX12GpuProfiler.cpp @@ -0,0 +1,39 @@ +#include "pch.h" +#include "Platform/DX12/DX12GpuProfiler.h" + +#include "Platform/DX12/DeviceManagerDX12.h" + +namespace Eppo +{ + DX12GpuProfiler::DX12GpuProfiler() + { +#if defined(TRACY_ENABLE) + const auto dm = std::static_pointer_cast(DeviceManager::Get()); + m_Context = TracyD3D12Context(dm->GetDxDevice(), dm->GetGraphicsQueue()); +#endif + } + + DX12GpuProfiler::~DX12GpuProfiler() + { +#if defined(TRACY_ENABLE) + if (m_Context) + TracyD3D12Destroy(m_Context); +#endif + } + + auto DX12GpuProfiler::Collect([[maybe_unused]] const Ref& commandBuffer) -> void + { +#if defined(TRACY_ENABLE) + TracyD3D12Collect(m_Context); +#endif + } + + auto DX12GpuProfiler::GetNativeContext() const -> void* + { +#if defined(TRACY_ENABLE) + return m_Context; +#else + return nullptr; +#endif + } +} diff --git a/EppoEngine/Source/Platform/DX12/DX12GpuProfiler.h b/EppoEngine/Source/Platform/DX12/DX12GpuProfiler.h new file mode 100644 index 00000000..df8d6608 --- /dev/null +++ b/EppoEngine/Source/Platform/DX12/DX12GpuProfiler.h @@ -0,0 +1,21 @@ +#pragma once + +#include "Renderer/GpuProfiler.h" + +namespace Eppo +{ + class DX12GpuProfiler final : public GpuProfiler + { + public: + DX12GpuProfiler(); + ~DX12GpuProfiler() override; + + auto Collect(const Ref& commandBuffer) -> void override; + [[nodiscard]] auto GetNativeContext() const -> void* override; + + private: +#if defined(TRACY_ENABLE) + TracyD3D12Ctx m_Context = nullptr; +#endif + }; +} diff --git a/EppoEngine/Source/Platform/DX12/DX12Shader.cpp b/EppoEngine/Source/Platform/DX12/DX12Shader.cpp new file mode 100644 index 00000000..39b9110b --- /dev/null +++ b/EppoEngine/Source/Platform/DX12/DX12Shader.cpp @@ -0,0 +1,357 @@ +#include "pch.h" +#include "Platform/DX12/DX12Shader.h" + +#include "Platform/ComPtr.h" + +#include +#include +#include + +#include + +namespace Eppo +{ + namespace + { + auto DxilTypeToNvrhiType(const std::string& semantic, const D3D12_SIGNATURE_PARAMETER_DESC& type) -> nvrhi::Format + { + const uint32_t components = std::popcount(static_cast(type.Mask)); + + bool packed = semantic.substr(0, 5) == "COLOR"; + + switch (type.ComponentType) + { + case D3D_REGISTER_COMPONENT_FLOAT32: + { + if (components == 1) + return packed ? nvrhi::Format::R8_UNORM : nvrhi::Format::R32_FLOAT; + if (components == 2) + return packed ? nvrhi::Format::RG8_UNORM : nvrhi::Format::RG32_FLOAT; + if (components == 3) + return nvrhi::Format::RGB32_FLOAT; + if (components == 4) + return packed ? nvrhi::Format::RGBA8_UNORM : nvrhi::Format::RGBA32_FLOAT; + + EP_ASSERT(false); + return nvrhi::Format::UNKNOWN; + } + + case D3D_REGISTER_COMPONENT_UINT32: + return nvrhi::Format::R32_UINT; + + default: + { + EP_ASSERT(false); + return nvrhi::Format::UNKNOWN; + } + } + } + } + + DX12Shader::DX12Shader(ShaderSpecification spec) + : Shader(std::move(spec)) + { + if (!CompileOrGetCache()) + { + Log::Error("Shader '{}' could not be compiled!", m_Specification.Name); + EP_ASSERT(false, "Shader compilation failed!"); + return; + } + + CreateShaderHandles(); + + Log::Info("=================================="); + Log::Info("===== Shader Reflection Data ====="); + Log::Info("=================================="); + Log::Info("Name: {}", m_Specification.Name); + + for (const auto& [type, bytes] : m_ShaderBytes) + { + if (!Reflect(type)) + { + EP_ASSERT(false, "Shader reflection failed!"); + return; + } + } + + if (m_ShaderBytes.contains(nvrhi::ShaderType::Vertex)) + CreateInputLayout(); + + Log::Info("=================================="); + + CreateBindingLayout(); + + m_IsLoaded.store(true, std::memory_order_release); + } + + auto DX12Shader::Reflect(const nvrhi::ShaderType type) -> bool + { + ComPtr utils; + ComPtr container; + ComPtr blob; + ComPtr reflection; + UINT32 part = 0; + const auto& bytes = m_ShaderBytes.at(type); + if (FAILED(DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&utils))) || + FAILED(DxcCreateInstance(CLSID_DxcContainerReflection, IID_PPV_ARGS(&container))) || + FAILED(utils->CreateBlob(bytes.data(), static_cast(bytes.size()), DXC_CP_ACP, &blob)) || + FAILED(container->Load(blob.Get())) || + FAILED(container->FindFirstPartKind(DXC_PART_DXIL, &part)) || + FAILED(container->GetPartReflection(part, IID_PPV_ARGS(&reflection)))) + { + Log::Error("Could not reflect shader '{}'!", m_Specification.Name); + return false; + } + + D3D12_SHADER_DESC shaderDesc{}; + if (FAILED(reflection->GetDesc(&shaderDesc))) + { + Log::Error("Could not read reflection data for shader '{}'!", m_Specification.Name); + return false; + } + + std::unordered_map> resources; + for (uint32_t index = 0; index < shaderDesc.BoundResources; index++) + { + D3D12_SHADER_INPUT_BIND_DESC resource{}; + if (FAILED(reflection->GetResourceBindingDesc(index, &resource))) + { + Log::Error("Could not read resource {} for shader '{}'!", index, m_Specification.Name); + return false; + } + resources[resource.Type].push_back(resource); + } + + Log::Info("Stage: {}", nvrhi::utils::ShaderStageToString(type)); + + if (shaderDesc.InputParameters != 0 && type == nvrhi::ShaderType::Vertex) + { + Log::Info("\tInputs:"); + + for (uint32_t index = 0; index < shaderDesc.InputParameters; index++) + { + D3D12_SIGNATURE_PARAMETER_DESC resource{}; + if (FAILED(reflection->GetInputParameterDesc(index, &resource))) + { + Log::Error("Could not read input {} for shader '{}'!", index, m_Specification.Name); + return false; + } + if (resource.SystemValueType != D3D_NAME_UNDEFINED) + continue; + + auto& input = m_ShaderInputs.emplace_back(); + input.Name = resource.SemanticName; + input.Location = resource.Register; + input.Type = DxilTypeToNvrhiType(resource.SemanticName, resource); + input.Offset = m_InputAttributeStride; + + m_InputAttributeStride += Utils::NvrhiFormatSize(input.Type); + + Log::Info("\t\tName: {}", input.Name); + Log::Info("\t\tLocation: {}", input.Location); + Log::Info("\t\tType: {}", nvrhi::utils::FormatToString(input.Type)); + } + + std::ranges::sort( + m_ShaderInputs, std::ranges::less{}, + [](const ShaderInputAttribute& input) -> uint32_t + { + return input.Location; + } + ); + } + + for (const auto& resource : resources[D3D_SIT_CBUFFER]) + { + if (std::string_view(resource.Name) != "uPC") + continue; + + const auto constantBuffer = reflection->GetConstantBufferByName(resource.Name); + D3D12_SHADER_BUFFER_DESC bufferDesc{}; + if (FAILED(constantBuffer->GetDesc(&bufferDesc)) || resource.BindPoint != 0 || resource.Space != 0) + { + Log::Error("Could not reflect push constants at b0, space0 for shader '{}'!", m_Specification.Name); + return false; + } + + uint32_t pushConstantSize = 0; + for (uint32_t index = 0; index < bufferDesc.Variables; index++) + { + D3D12_SHADER_VARIABLE_DESC variableDesc{}; + if (FAILED(constantBuffer->GetVariableByIndex(index)->GetDesc(&variableDesc))) + { + Log::Error("Could not read push constant {} for shader '{}'!", index, m_Specification.Name); + return false; + } + pushConstantSize = std::max(pushConstantSize, variableDesc.StartOffset + variableDesc.Size); + } + + m_PushConstants.Binding = 0; + if (pushConstantSize > m_PushConstants.Size) + m_PushConstants.Size = pushConstantSize; + m_PushConstants.Stage = m_HasPushConstants ? nvrhi::ShaderType::All : type; + m_HasPushConstants = true; + } + + if (!resources[D3D_SIT_CBUFFER].empty()) + { + Log::Info("Found {} uniform_buffers", resources[D3D_SIT_CBUFFER].size()); + + for (const auto& resource : resources[D3D_SIT_CBUFFER]) + { + if (std::string_view(resource.Name) == "uPC") + continue; + + const uint32_t set = resource.Space; + const uint32_t binding = resource.BindPoint; + + bool bindingExists = false; + if (m_ShaderResources.contains(set)) + { + for (auto& setResource : m_ShaderResources.at(set)) + { + if (resource.Name == setResource.Name && binding == setResource.Binding) + { + setResource.Stage = (setResource.Stage | type); + bindingExists = true; + break; + } + } + } + + if (!bindingExists) + { + ShaderResourceBinding& shaderResource = m_ShaderResources[set].emplace_back(); + shaderResource.Name = resource.Name; + shaderResource.Binding = binding; + shaderResource.Stage = type; + shaderResource.Type = nvrhi::ResourceType::ConstantBuffer; + + Log::Info("\t\tName: {}", shaderResource.Name); + Log::Info("\t\tBinding: {} (set: {})", shaderResource.Binding, set); + Log::Info("\t\tType: {}", nvrhi::utils::ResourceTypeToString(shaderResource.Type)); + } + } + } + + if (!resources[D3D_SIT_TEXTURE].empty()) + { + Log::Info("Found {} separate_images", resources[D3D_SIT_TEXTURE].size()); + + for (const auto& resource : resources[D3D_SIT_TEXTURE]) + { + const uint32_t set = resource.Space; + const uint32_t binding = resource.BindPoint; + + const uint32_t arraySize = resource.BindCount; + + bool bindingExists = false; + if (m_ShaderResources.contains(set)) + { + for (auto& setResource : m_ShaderResources.at(set)) + { + if (resource.Name == setResource.Name && binding == setResource.Binding) + { + setResource.Stage = (setResource.Stage | type); + bindingExists = true; + break; + } + } + } + + if (!bindingExists) + { + ShaderResourceBinding& shaderResource = m_ShaderResources[set].emplace_back(); + shaderResource.Name = resource.Name; + shaderResource.Binding = binding; + shaderResource.ArraySize = arraySize; + shaderResource.Stage = type; + shaderResource.Type = nvrhi::ResourceType::Texture_SRV; + + Log::Info("\t\tName: {}", shaderResource.Name); + Log::Info("\t\tBinding: {} (set: {})", shaderResource.Binding, set); + Log::Info("\t\tType: {}", nvrhi::utils::ResourceTypeToString(shaderResource.Type)); + } + } + } + + if (!resources[D3D_SIT_SAMPLER].empty()) + { + Log::Info("Found {} separate_samplers", resources[D3D_SIT_SAMPLER].size()); + + for (const auto& resource : resources[D3D_SIT_SAMPLER]) + { + const uint32_t set = resource.Space; + const uint32_t binding = resource.BindPoint; + + bool bindingExists = false; + if (m_ShaderResources.contains(set)) + { + for (auto& setResource : m_ShaderResources.at(set)) + { + if (resource.Name == setResource.Name && binding == setResource.Binding) + { + setResource.Stage = (setResource.Stage | type); + bindingExists = true; + break; + } + } + } + + if (!bindingExists) + { + ShaderResourceBinding& shaderResource = m_ShaderResources[set].emplace_back(); + shaderResource.Name = resource.Name; + shaderResource.Binding = binding; + shaderResource.Stage = type; + shaderResource.Type = nvrhi::ResourceType::Sampler; + + Log::Info("\t\tName: {}", shaderResource.Name); + Log::Info("\t\tBinding: {} (set: {})", shaderResource.Binding, set); + Log::Info("\t\tType: {}", nvrhi::utils::ResourceTypeToString(shaderResource.Type)); + } + } + } + + if (!resources[D3D_SIT_STRUCTURED].empty()) + { + Log::Info("Found {} storage_buffers", resources[D3D_SIT_STRUCTURED].size()); + + for (const auto& resource : resources[D3D_SIT_STRUCTURED]) + { + const uint32_t set = resource.Space; + const uint32_t binding = resource.BindPoint; + + bool bindingExists = false; + if (m_ShaderResources.contains(set)) + { + for (auto& setResource : m_ShaderResources.at(set)) + { + if (resource.Name == setResource.Name && binding == setResource.Binding) + { + setResource.Stage = (setResource.Stage | type); + bindingExists = true; + break; + } + } + } + + if (!bindingExists) + { + ShaderResourceBinding& shaderResource = m_ShaderResources[set].emplace_back(); + shaderResource.Name = resource.Name; + shaderResource.Binding = binding; + shaderResource.Stage = type; + shaderResource.Type = nvrhi::ResourceType::StructuredBuffer_SRV; + + Log::Info("\t\tName: {}", shaderResource.Name); + Log::Info("\t\tBinding: {} (set: {})", shaderResource.Binding, set); + Log::Info("\t\tType: {}", nvrhi::utils::ResourceTypeToString(shaderResource.Type)); + } + } + } + Log::Trace("Found {} sampled_images", 0); + Log::Trace("Found {} storage_images", resources[D3D_SIT_UAV_RWTYPED].size()); + return true; + } +} diff --git a/EppoEngine/Source/Platform/DX12/DX12Shader.h b/EppoEngine/Source/Platform/DX12/DX12Shader.h new file mode 100644 index 00000000..6d00c02b --- /dev/null +++ b/EppoEngine/Source/Platform/DX12/DX12Shader.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Renderer/Shader.h" + +namespace Eppo +{ + class DX12Shader : public Shader + { + public: + explicit DX12Shader(ShaderSpecification spec); + + private: + auto Reflect(nvrhi::ShaderType type) -> bool; + }; +} diff --git a/EppoEngine/Source/Platform/DX12/DX12Swapchain.cpp b/EppoEngine/Source/Platform/DX12/DX12Swapchain.cpp new file mode 100644 index 00000000..d3eb4534 --- /dev/null +++ b/EppoEngine/Source/Platform/DX12/DX12Swapchain.cpp @@ -0,0 +1,228 @@ +#include "pch.h" +#include "Platform/DX12/DX12Swapchain.h" + +#include "Platform/DX12/DeviceManagerDX12.h" +#include "Renderer/Image.h" +#include "Renderer/Renderer.h" + +#define GLFW_EXPOSE_NATIVE_WIN32 +#include +#include + +namespace Eppo +{ + namespace + { + auto NvrhiFormatToDxgi(const nvrhi::Format format) -> DXGI_FORMAT + { + switch (format) + { + case nvrhi::Format::RGBA8_UNORM: + return DXGI_FORMAT_R8G8B8A8_UNORM; + case nvrhi::Format::BGRA8_UNORM: + return DXGI_FORMAT_B8G8R8A8_UNORM; + case nvrhi::Format::SRGBA8_UNORM: + return DXGI_FORMAT_R8G8B8A8_UNORM_SRGB; + case nvrhi::Format::SBGRA8_UNORM: + return DXGI_FORMAT_B8G8R8A8_UNORM_SRGB; + default: + return DXGI_FORMAT_UNKNOWN; + } + } + } + + DX12Swapchain::DX12Swapchain(GLFWwindow* window) + : Swapchain(window) + { + m_WindowHandle = glfwGetWin32Window(window); + EP_ASSERT(m_WindowHandle); + } + + DX12Swapchain::~DX12Swapchain() + { + // Viewport swapchains are destroyed while the device is still alive + DeviceManager::Get()->WaitIdle(); + m_Images.clear(); + } + + auto DX12Swapchain::BeginFrame() -> bool + { + EP_PROFILE_FN("Swapchain::BeginFrame"); + EP_ASSERT(!m_FrameActive, "BeginFrame was called while a swapchain frame is already active!"); + + const auto [width, height] = GetWindowFramebufferSize(); + if (width == 0 || height == 0) + return false; + + if (width != m_Width || height != m_Height) + m_ResizePending = true; + + if (m_ResizePending) + Resize(); + + const auto& dm = DeviceManager::Get(); + auto& frame = m_FrameSyncData.at(m_CurrentFrameIndex); + if (frame.InFlight) + { + dm->GetDevice()->waitEventQuery(frame.CompletionQuery); + dm->GetDevice()->resetEventQuery(frame.CompletionQuery); + frame.InFlight = false; + } + + m_SwapchainImageIndex = m_Swapchain->GetCurrentBackBufferIndex(); + m_FrameActive = true; + return true; + } + + auto DX12Swapchain::Present() -> bool + { + EP_PROFILE_FN("Swapchain::Present"); + EP_ASSERT(m_FrameActive, "Present was called without an active swapchain frame!"); + + const auto& dm = std::static_pointer_cast(DeviceManager::Get()); + nvrhi::d3d12::IDevice* dxNvrhiDevice = dm->GetDevice()->getNativeObject(nvrhi::ObjectTypes::Nvrhi_D3D12_Device); + const DeviceParams& params = dm->GetParams(); + + const UINT syncInterval = params.VSync ? 1 : 0; + DXGI_SWAP_CHAIN_DESC1 desc{}; + DX_CHECK(m_Swapchain->GetDesc1(&desc), "Failed to get swapchain description!"); + const UINT presentFlags = !params.VSync && (desc.Flags & DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING) != 0 ? DXGI_PRESENT_ALLOW_TEARING : 0; + + const HRESULT result = m_Swapchain->Present(syncInterval, presentFlags); + + dxNvrhiDevice->executeCommandLists(nullptr, 0); + + auto& frame = m_FrameSyncData.at(m_CurrentFrameIndex); + dm->GetDevice()->setEventQuery(frame.CompletionQuery, nvrhi::CommandQueue::Graphics); + frame.InFlight = true; + + m_FrameActive = false; + m_CurrentFrameIndex = (m_CurrentFrameIndex + 1) % m_MaxFramesInFlight; + + if (SUCCEEDED(result)) + return true; + + Log::Error(LogSource::DX12, "Failed to present a swapchain image: HRESULT 0x{:08X}", static_cast(result)); + return false; + } + + auto DX12Swapchain::CreateSwapchain(uint32_t width, uint32_t height) -> void + { + const auto& dm = std::static_pointer_cast(DeviceManager::Get()); + const DeviceParams& params = dm->GetParams(); + + if (width == 0 || height == 0) + { + const auto extent = GetWindowFramebufferSize(); + m_Width = extent.Width; + m_Height = extent.Height; + } + else + { + m_Width = width; + m_Height = height; + } + + m_Format = NvrhiFormatToDxgi(params.SwapchainFormat); + EP_ASSERT(m_Format != DXGI_FORMAT_UNKNOWN); + + if (m_Swapchain) + { + m_Images.clear(); + + DXGI_SWAP_CHAIN_DESC1 desc{}; + DX_CHECK(m_Swapchain->GetDesc1(&desc), "Failed to get swapchain description!"); + DX_CHECK( + m_Swapchain->ResizeBuffers(desc.BufferCount, m_Width, m_Height, DXGI_FORMAT_UNKNOWN, desc.Flags), + "Failed to resize swapchain buffers!" + ); + } + else + { + BOOL allowTearing = FALSE; + if (!params.VSync) + dm->GetFactory()->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, &allowTearing, sizeof(allowTearing)); + + const DXGI_SWAP_CHAIN_DESC1 desc{ + .Width = m_Width, + .Height = m_Height, + .Format = m_Format, + .Stereo = FALSE, + .SampleDesc = { .Count = 1, .Quality = 0 }, + .BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT, + .BufferCount = std::max(2u, params.MaxFramesInFlight), + .Scaling = DXGI_SCALING_STRETCH, + .SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD, + .AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED, + .Flags = allowTearing == TRUE ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0u, + }; + + ComPtr swapchain; + DX_CHECK( + dm->GetFactory()->CreateSwapChainForHwnd(dm->GetGraphicsQueue(), m_WindowHandle, &desc, nullptr, nullptr, &swapchain), + "Failed to create IDXGISwapChain!" + ); + EP_ASSERT(swapchain); + DX_CHECK(swapchain.As(&m_Swapchain), "Failed to query IDXGISwapChain4!"); + } + + DXGI_SWAP_CHAIN_DESC1 scDesc{}; + DX_CHECK(m_Swapchain->GetDesc1(&scDesc), "Failed to get swapchain description!"); + m_MaxFramesInFlight = std::min(params.MaxFramesInFlight, scDesc.BufferCount); + + if (m_FrameSyncData.size() != m_MaxFramesInFlight) + { + m_FrameSyncData.clear(); + m_FrameSyncData.resize(m_MaxFramesInFlight); + + for (auto& frame : m_FrameSyncData) + { + frame.CompletionQuery = dm->GetDevice()->createEventQuery(); + EP_ASSERT(frame.CompletionQuery != nullptr, "Failed to create frame completion query."); + frame.InFlight = false; + } + } + else + { + for (auto& frame : m_FrameSyncData) + { + if (frame.InFlight) + dm->GetDevice()->resetEventQuery(frame.CompletionQuery); + frame.InFlight = false; + } + } + + m_CurrentFrameIndex = 0; + m_FrameActive = false; + + for (uint32_t i = 0; i < scDesc.BufferCount; i++) + { + ComPtr backBuffer; + DX_CHECK(m_Swapchain->GetBuffer(i, IID_PPV_ARGS(&backBuffer)), "Failed to get swapchain back buffer!"); + + auto& image = m_Images.emplace_back(); + image.NativeImage = backBuffer.Get(); + + ImageSpecification imageSpec{ + .ImageFormat = params.SwapchainFormat, + .Width = m_Width, + .Height = m_Height, + .IsRenderTarget = true, + .InitialState = nvrhi::ResourceStates::Present, + .DebugName = std::format("Swapchain Image {}", i), + }; + + FramebufferSpecification framebufferSpec{ + .Width = m_Width, + .Height = m_Height, + .SwapchainTarget = true, + .SwapchainImage = Image::Create(imageSpec, image.NativeImage), + .DebugName = std::format("Swapchain Framebuffer {}", i), + }; + + image.Framebuffer = CreateRef(framebufferSpec); + } + + m_SwapchainImageIndex = m_Swapchain->GetCurrentBackBufferIndex(); + } +} diff --git a/EppoEngine/Source/Platform/DX12/DX12Swapchain.h b/EppoEngine/Source/Platform/DX12/DX12Swapchain.h new file mode 100644 index 00000000..d1dbfafe --- /dev/null +++ b/EppoEngine/Source/Platform/DX12/DX12Swapchain.h @@ -0,0 +1,33 @@ +#pragma once + +#include "Platform/DX12/DX12.h" +#include "Renderer/DeviceManager.h" +#include "Renderer/Swapchain.h" + +namespace Eppo +{ + class DX12Swapchain : public Swapchain + { + public: + explicit DX12Swapchain(GLFWwindow* window); + ~DX12Swapchain() override; + + auto BeginFrame() -> bool override; + auto Present() -> bool override; + + auto CreateSwapchain(uint32_t width = 0, uint32_t height = 0) -> void override; + + private: + struct FrameSync + { + nvrhi::EventQueryHandle CompletionQuery = nullptr; + bool InFlight = false; + }; + + std::vector m_FrameSyncData; + HWND m_WindowHandle = nullptr; + ComPtr m_Swapchain; + + DXGI_FORMAT m_Format = DXGI_FORMAT_UNKNOWN; + }; +} diff --git a/EppoEngine/Source/Platform/DX12/DeviceManagerDX12.cpp b/EppoEngine/Source/Platform/DX12/DeviceManagerDX12.cpp new file mode 100644 index 00000000..04088548 --- /dev/null +++ b/EppoEngine/Source/Platform/DX12/DeviceManagerDX12.cpp @@ -0,0 +1,145 @@ +#include "pch.h" +#include "Platform/DX12/DeviceManagerDX12.h" + +#include "Platform/DX12/DX12Swapchain.h" +#include "Renderer/GpuProfiler.h" + +namespace Eppo +{ + namespace + { + auto SelectHardwareAdapter(IDXGIFactory6* factory, IDXGIAdapter1** outAdapter) -> void + { + *outAdapter = nullptr; + ComPtr adapter; + + auto probeAdapter = [&adapter](IDXGIAdapter1* candidate) -> bool + { + DXGI_ADAPTER_DESC1 desc; + candidate->GetDesc1(&desc); + + if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) + return false; + + return SUCCEEDED(D3D12CreateDevice(candidate, D3D_FEATURE_LEVEL_12_1, __uuidof(ID3D12Device), nullptr)); + }; + + for (UINT i = 0; + SUCCEEDED(factory->EnumAdapterByGpuPreference(i, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&adapter))); ++i) + { + if (probeAdapter(adapter.Get())) + break; + } + + if (adapter == nullptr) + { + for (UINT i = 0; SUCCEEDED(factory->EnumAdapters1(i, &adapter)); ++i) + { + if (probeAdapter(adapter.Get())) + break; + } + } + + *outAdapter = adapter.Detach(); + } + } + + DeviceManagerDX12::DeviceManagerDX12(const Ref& window, const DeviceParams& params) + : DeviceManager(window, params) + { + UINT dxgiFactoryFlags = 0; + + if (s_EnableValidationLayers) + { + ComPtr debugController; + if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)))) + { + debugController->EnableDebugLayer(); + dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG; + } + } + + DX_CHECK(CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&m_Factory)), "Failed to create IDXGIFactory6!"); + EP_ASSERT(m_Factory); + + ComPtr hardwareAdapter; + SelectHardwareAdapter(m_Factory.Get(), &hardwareAdapter); + EP_ASSERT(hardwareAdapter); + + DX_CHECK( + D3D12CreateDevice(hardwareAdapter.Get(), D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(&m_DxDevice)), "Failed to create ID3D12Device!" + ); + EP_ASSERT(m_DxDevice); + + // Command queues + D3D12_COMMAND_QUEUE_DESC queueDesc{ + .Type = D3D12_COMMAND_LIST_TYPE_DIRECT, + .Flags = D3D12_COMMAND_QUEUE_FLAG_NONE, + }; + DX_CHECK(m_DxDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_GraphicsQueue)), "Failed to create graphics command queue!"); + + if (m_Params.EnableComputeQueue) + { + queueDesc.Type = D3D12_COMMAND_LIST_TYPE_COMPUTE; + DX_CHECK(m_DxDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_ComputeQueue)), "Failed to create compute command queue!"); + } + + if (m_Params.EnableTransferQueue) + { + queueDesc.Type = D3D12_COMMAND_LIST_TYPE_COPY; + DX_CHECK(m_DxDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_TransferQueue)), "Failed to create transfer command queue!"); + } + + CreateNvrhiDevice(); + } + + auto DeviceManagerDX12::Init() -> void + { + m_Swapchain = CreateSwapchain(m_Window->GetNative(), 0, 0); + } + + auto DeviceManagerDX12::Shutdown() -> void + { + WaitIdle(); + GpuProfiler::Shutdown(); + + m_Renderer = nullptr; + m_Swapchain = nullptr; + + m_Device->runGarbageCollection(); + m_Device = nullptr; + m_ValidationLayer = nullptr; + } + + auto DeviceManagerDX12::CreateSwapchain(GLFWwindow* window, const uint32_t width, const uint32_t height) -> Ref + { + Ref swapchain = CreateRef(window); + swapchain->CreateSwapchain(width, height); + return swapchain; + } + + auto DeviceManagerDX12::GetDevice() const -> nvrhi::IDevice* + { + if (m_ValidationLayer) + return m_ValidationLayer; + + return m_Device; + } + + auto DeviceManagerDX12::CreateNvrhiDevice() -> void + { + const nvrhi::d3d12::DeviceDesc deviceDesc{ + .errorCB = &m_MessageCallback, + .pDevice = m_DxDevice.Get(), + .pGraphicsCommandQueue = m_GraphicsQueue.Get(), + .pComputeCommandQueue = m_ComputeQueue.Get(), + .pCopyCommandQueue = m_TransferQueue.Get(), + .enableHeapDirectlyIndexed = true, + }; + + m_Device = nvrhi::d3d12::createDevice(deviceDesc); + + if (s_EnableValidationLayers) + m_ValidationLayer = nvrhi::validation::createValidationLayer(m_Device); + } +} diff --git a/EppoEngine/Source/Platform/DX12/DeviceManagerDX12.h b/EppoEngine/Source/Platform/DX12/DeviceManagerDX12.h new file mode 100644 index 00000000..005a26f9 --- /dev/null +++ b/EppoEngine/Source/Platform/DX12/DeviceManagerDX12.h @@ -0,0 +1,42 @@ +#pragma once + +#include "Platform/DX12/DX12.h" +#include "Renderer/DeviceManager.h" + +#include +#include +#include + +namespace Eppo +{ + class DeviceManagerDX12 : public DeviceManager + { + public: + explicit DeviceManagerDX12(const Ref& window, const DeviceParams& params); + ~DeviceManagerDX12() override = default; + + auto Init() -> void override; + auto Shutdown() -> void override; + + auto CreateSwapchain(GLFWwindow* window, uint32_t width, uint32_t height) -> Ref override; + + [[nodiscard]] auto GetDevice() const -> nvrhi::IDevice* override; + + [[nodiscard]] auto GetDxDevice() const -> ID3D12Device* { return m_DxDevice.Get(); } + [[nodiscard]] auto GetFactory() const -> IDXGIFactory6* { return m_Factory.Get(); } + [[nodiscard]] auto GetGraphicsQueue() const -> ID3D12CommandQueue* { return m_GraphicsQueue.Get(); } + + private: + auto CreateNvrhiDevice() -> void; + + private: + nvrhi::d3d12::DeviceHandle m_Device = nullptr; + nvrhi::DeviceHandle m_ValidationLayer = nullptr; + + ComPtr m_DxDevice; + ComPtr m_Factory; + ComPtr m_ComputeQueue; + ComPtr m_GraphicsQueue; + ComPtr m_TransferQueue; + }; +} diff --git a/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.cpp b/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.cpp index 68fe7841..cd30159b 100644 --- a/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.cpp +++ b/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.cpp @@ -2,6 +2,7 @@ #include "Platform/Vulkan/DeviceManagerVK.h" #include "Platform/Vulkan/Vulkan.h" +#include "Platform/Vulkan/VulkanSwapchain.h" #include "Renderer/GpuProfiler.h" #include @@ -19,12 +20,7 @@ namespace Eppo auto DeviceManagerVK::Init() -> void { - VkSurfaceKHR surface = nullptr; - VK_CHECK(glfwCreateWindowSurface(m_Instance, m_Window->GetNative(), nullptr, &surface), "Failed to create window surface!"); - EP_ASSERT(surface); - - m_Swapchain = CreateRef(surface); - m_Swapchain->CreateSwapchain(); + m_Swapchain = CreateSwapchain(m_Window->GetNative(), 0, 0); } auto DeviceManagerVK::Shutdown() -> void @@ -48,14 +44,11 @@ namespace Eppo vkDestroyInstance(m_Instance, nullptr); } - auto DeviceManagerVK::BeginFrame() -> bool - { - return m_Swapchain->BeginFrame(); - } - - auto DeviceManagerVK::Present() -> bool + auto DeviceManagerVK::CreateSwapchain(GLFWwindow* window, const uint32_t width, const uint32_t height) -> Ref { - return m_Swapchain->Present(); + Ref swapchain = CreateRef(window); + swapchain->CreateSwapchain(width, height); + return swapchain; } auto DeviceManagerVK::GetDevice() const -> nvrhi::IDevice* diff --git a/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.h b/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.h index 8711af24..3beeda6d 100644 --- a/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.h +++ b/EppoEngine/Source/Platform/Vulkan/DeviceManagerVK.h @@ -2,7 +2,6 @@ #include "Platform/Vulkan/LogicalDevice.h" #include "Platform/Vulkan/PhysicalDevice.h" -#include "Platform/Vulkan/Swapchain.h" #include "Platform/Vulkan/Vulkan.h" #include "Renderer/DeviceManager.h" @@ -21,20 +20,13 @@ namespace Eppo auto Init() -> void override; auto Shutdown() -> void override; - auto BeginFrame() -> bool override; - auto Present() -> bool override; + auto CreateSwapchain(GLFWwindow* window, uint32_t width, uint32_t height) -> Ref override; - [[nodiscard]] virtual auto GetCurrentFrameIndex() const -> uint32_t override { return m_Swapchain->GetCurrentFrameIndex(); } - [[nodiscard]] virtual auto GetMaxFramesInFlight() const -> uint32_t override { return m_Swapchain->GetMaxFramesInFlight(); } - [[nodiscard]] auto GetCurrentBackBufferIndex() const -> uint32_t override { return m_Swapchain->GetCurrentBackBufferIndex(); } - [[nodiscard]] auto GetBackBufferCount() const -> uint32_t override { return m_Swapchain->GetImageCount(); } - auto GetCurrentSwapchainImage() -> const SwapchainImage& override { return m_Swapchain->GetCurrentSwapchainImage(); } [[nodiscard]] auto GetDevice() const -> nvrhi::IDevice* override; [[nodiscard]] constexpr auto GetVulkanInstance() const -> VkInstance { return m_Instance; } [[nodiscard]] constexpr auto GetPhysicalDevice() const -> const ScopedPtr& { return m_PhysicalDevice; } [[nodiscard]] constexpr auto GetLogicalDevice() const -> const ScopedPtr& { return m_LogicalDevice; } - [[nodiscard]] constexpr auto GetSwapchain() const -> const Ref& { return m_Swapchain; } private: auto CreateVulkanInstance() -> void; @@ -49,6 +41,5 @@ namespace Eppo ScopedPtr m_PhysicalDevice = nullptr; ScopedPtr m_LogicalDevice = nullptr; - Ref m_Swapchain = nullptr; }; } diff --git a/EppoEngine/Source/Platform/Vulkan/VulkanGpuProfiler.cpp b/EppoEngine/Source/Platform/Vulkan/VulkanGpuProfiler.cpp index 131359be..cb4e739e 100644 --- a/EppoEngine/Source/Platform/Vulkan/VulkanGpuProfiler.cpp +++ b/EppoEngine/Source/Platform/Vulkan/VulkanGpuProfiler.cpp @@ -14,13 +14,13 @@ namespace Eppo const VkPhysicalDevice physicalDevice = dm->GetPhysicalDevice()->GetNative(); const VkDevice device = dm->GetLogicalDevice()->GetNative(); const VkQueue graphicsQueue = dm->GetLogicalDevice()->GetGraphicsQueue(); - const auto graphicsFamily = static_cast(dm->GetPhysicalDevice()->GetQueueFamilyIndices().Graphics); + const auto& indices = dm->GetPhysicalDevice()->GetQueueFamilyIndices(); // TracyVkContext owns and re-begins its setup buffer, so give it a dedicated one from a reset-capable pool, not an nvrhi list. const VkCommandPoolCreateInfo poolInfo{ .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT | VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, - .queueFamilyIndex = graphicsFamily, + .queueFamilyIndex = static_cast(indices.Graphics), }; VkCommandPool setupPool = nullptr; VK_CHECK(vkCreateCommandPool(device, &poolInfo, nullptr, &setupPool), "Failed to create Tracy setup command pool!"); @@ -52,8 +52,8 @@ namespace Eppo auto VulkanGpuProfiler::Collect([[maybe_unused]] const Ref& commandBuffer) -> void { #if defined(TRACY_ENABLE) - const auto cmd = static_cast( - commandBuffer->GetCommandList()->getNativeObject(nvrhi::ObjectTypes::VK_CommandBuffer)); + const auto cmd = + static_cast(commandBuffer->GetCommandList()->getNativeObject(nvrhi::ObjectTypes::VK_CommandBuffer)); TracyVkCollect(m_Context, cmd); #endif } diff --git a/EppoEngine/Source/Platform/Vulkan/VulkanShader.cpp b/EppoEngine/Source/Platform/Vulkan/VulkanShader.cpp index ae60af9a..85b7de94 100644 --- a/EppoEngine/Source/Platform/Vulkan/VulkanShader.cpp +++ b/EppoEngine/Source/Platform/Vulkan/VulkanShader.cpp @@ -1,11 +1,6 @@ #include "pch.h" #include "Platform/Vulkan/VulkanShader.h" -#include "Platform/ComPtr.h" -#include "Renderer/DeviceManager.h" - -#include - #include #include @@ -13,150 +8,6 @@ namespace Eppo { namespace { - struct ShaderStage - { - nvrhi::ShaderType Type; - const wchar_t* TargetProfile; - const char* CacheSuffix; - }; - - constexpr std::array s_ShaderStages{ - ShaderStage{ .Type = nvrhi::ShaderType::Vertex, .TargetProfile = L"vs_6_6", .CacheSuffix = "vert" }, - ShaderStage{ .Type = nvrhi::ShaderType::Pixel, .TargetProfile = L"ps_6_6", .CacheSuffix = "frag" }, - }; - - auto FindStage(const nvrhi::ShaderType type) -> const ShaderStage& - { - const auto it = std::ranges::find(s_ShaderStages, type, &ShaderStage::Type); - EP_ASSERT(it != s_ShaderStages.end()); - return *it; - } - - auto ShaderStageSuffix(const nvrhi::ShaderType type) -> std::string - { - return FindStage(type).CacheSuffix; - } - - auto DetectStages(const std::string& source) -> std::vector - { - std::vector stages; - for (const auto& stage : s_ShaderStages) - if (source.find(Utils::ShaderEntryPoint(stage.Type)) != std::string::npos) - stages.push_back(stage.Type); - return stages; - } - - // Resolves #includes strictly from a packed game. A deployed runtime has no shader files on disk, - // and must not acquire any: an include that is not in the pack fails the compile instead. - class PackedIncludeHandler final : public IDxcIncludeHandler - { - public: - PackedIncludeHandler(IDxcUtils* utils, const std::map& includes) - : m_Utils(utils), m_Includes(includes) - {} - - auto STDMETHODCALLTYPE LoadSource(LPCWSTR filename, IDxcBlob** includeSource) -> HRESULT override - { - if (!includeSource) - return E_INVALIDARG; - *includeSource = nullptr; - - const auto* source = Find(std::filesystem::path(filename).lexically_normal().generic_string()); - if (!source) - { - Log::Error("Packed shader include '{}' is not in the game package.", std::filesystem::path(filename).generic_string()); - return E_FAIL; - } - - ComPtr blob; - if (FAILED(m_Utils->CreateBlob(source->data(), static_cast(source->size()), DXC_CP_UTF8, &blob))) - return E_FAIL; - - *includeSource = blob.Detach(); - return S_OK; - } - - auto STDMETHODCALLTYPE QueryInterface(REFIID riid, void** object) -> HRESULT override - { - if (!object) - return E_INVALIDARG; - - if (riid == __uuidof(IDxcIncludeHandler) || riid == __uuidof(IUnknown)) - { - *object = static_cast(this); - AddRef(); - return S_OK; - } - - *object = nullptr; - return E_NOINTERFACE; - } - - // Scoped to a single Compile call, so reference counting has nothing to manage. - auto STDMETHODCALLTYPE AddRef() -> ULONG override { return 1; } - auto STDMETHODCALLTYPE Release() -> ULONG override { return 1; } - - private: - // DXC resolves an include against the includer's name, so it may arrive prefixed. Keys are relative - // to Resources/Shaders; take the longest matching suffix, since a shorter one can match a different file. - [[nodiscard]] auto Find(const std::string& requested) const -> const std::string* - { - if (const auto it = m_Includes.find(requested); it != m_Includes.end()) - return &it->second; - - const std::string* match = nullptr; - size_t matched = 0; - for (const auto& [path, source] : m_Includes) - { - if (requested.size() <= path.size() || !requested.ends_with(path) || - requested.at(requested.size() - path.size() - 1) != '/') - continue; - - if (path.size() > matched) - { - match = &source; - matched = path.size(); - } - } - - return match; - } - - IDxcUtils* m_Utils = nullptr; - const std::map& m_Includes; - }; - - // Includes are part of a shader's compiled result, so the cache key has to cover them too. Lengths are - // folded in as well, otherwise a boundary can shift between two entries without changing the hash. - auto HashSource(const std::string& source, const std::map& includes) -> std::string - { - std::string combined = std::format("{}:{}", source.size(), source); - for (const auto& [path, includeSource] : includes) - combined += std::format("{}:{}{}:{}", path.size(), path, includeSource.size(), includeSource); - - return std::to_string(Hash::GenerateFnv(combined)); - } - - // Kept in step with the include walk in ProjectExporter::Export: if the two sets diverge, the editor - // and the deployed game hash the same shader differently and every shipped game recompiles on launch. - auto ReadIncludesFromDisk() -> std::map - { - const auto shadersDirectory = FS::GetResourcesDirectory() / "Shaders"; - if (!FS::Exists(shadersDirectory)) - return {}; - - std::map includes; - for (const auto& entry : std::filesystem::recursive_directory_iterator(shadersDirectory)) - { - if (!entry.is_regular_file() || entry.path().extension() != ".hlsli") - continue; - - includes.emplace(std::filesystem::relative(entry.path(), shadersDirectory).generic_string(), FS::ReadText(entry.path())); - } - - return includes; - } - auto SpirvTypeToNvrhiType(const std::string& semantic, const spirv_cross::SPIRType& type) -> nvrhi::Format { using spirv_cross::SPIRType; @@ -171,7 +22,6 @@ namespace Eppo return packed ? nvrhi::Format::R8_UNORM : nvrhi::Format::R32_FLOAT; if (type.vecsize == 2) return packed ? nvrhi::Format::RG8_UNORM : nvrhi::Format::RG32_FLOAT; - ; if (type.vecsize == 3) return nvrhi::Format::RGB32_FLOAT; if (type.vecsize == 4) @@ -226,180 +76,6 @@ namespace Eppo m_IsLoaded.store(true, std::memory_order_release); } - auto VulkanShader::CompileOrGetCache() -> bool - { - // A packed source is all a packed shader may read, along with its packed includes; it must not reach - // the filesystem for either. (The SPIR-V cache below is still on disk, but it is this shader's own - // output, keyed by a hash of the source.) - if (!m_Specification.Source.empty()) - { - m_ShaderSource = m_Specification.Source; - } - else - { - const std::filesystem::path sourcePath = FS::GetResourcesDirectory() / "Shaders" / std::format("{}.hlsl", m_Specification.Name); - m_ShaderSource = FS::ReadText(sourcePath); - m_Specification.Includes = ReadIncludesFromDisk(); - } - - const std::vector stages = DetectStages(m_ShaderSource); - if (stages.empty()) - { - Log::Error("Shader '{}' defines no known stage entry points.", m_Specification.Name); - return false; - } - - // One source compiles to several stage binaries, so a single hash of that source keys them all. - const std::string hash = HashSource(m_ShaderSource, m_Specification.Includes); - const std::filesystem::path shaderHashPath = FS::GetShaderCacheDirectory() / std::format("{}.hash", m_Specification.Name); - - bool verified = FS::Exists(shaderHashPath) && FS::ReadText(shaderHashPath) == hash; - for (const auto type : stages) - { - const std::filesystem::path shaderBinaryPath = - FS::GetShaderCacheDirectory() / std::format("{}.{}.spv", m_Specification.Name, ShaderStageSuffix(type)); - if (!FS::Exists(shaderBinaryPath)) - verified = false; - } - - if (verified) - { - Log::Info("Loading shader cache for '{}'", m_Specification.Name); - - for (const auto type : stages) - { - const std::filesystem::path shaderBinaryPath = - FS::GetShaderCacheDirectory() / std::format("{}.{}.spv", m_Specification.Name, ShaderStageSuffix(type)); - m_ShaderBytes[type] = FS::ReadBytes(shaderBinaryPath); - } - - return true; - } - - Log::Info("Compiling shader '{}'", m_Specification.Name); - - for (const auto type : stages) - { - if (!Compile(type)) - return false; - } - - FS::WriteText(shaderHashPath, hash, true); - - return true; - } - - auto VulkanShader::Compile(const nvrhi::ShaderType type) -> bool - { - // Create compiler - ComPtr utils; - ComPtr compiler; - if (FAILED(DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&utils))) || - FAILED(DxcCreateInstance(CLSID_DxcCompiler, IID_PPV_ARGS(&compiler)))) - { - Log::Error("Could not create the DXC compiler; is dxcompiler.dll present next to the executable?"); - return false; - } - - // Create include handler. A packed shader gets the pack-backed one and never the default: - // the default reads from disk, which a deployed game has none of. - const bool packed = !m_Specification.Source.empty(); - PackedIncludeHandler packedIncludeHandler(utils.Get(), m_Specification.Includes); - ComPtr diskIncludeHandler; - if (!packed) - utils->CreateDefaultIncludeHandler(&diskIncludeHandler); - IDxcIncludeHandler* includeHandler = packed ? static_cast(&packedIncludeHandler) : diskIncludeHandler.Get(); - - // Command line args for compiler. A packed shader is named relative to the virtual Resources/Shaders - // root, so DXC hands its #include paths to the handler the way the pack keys them. - const auto shaderFilename = std::format("{}.hlsl", m_Specification.Name); - const std::wstring shaderPath = packed ? std::filesystem::path(shaderFilename).wstring() - : std::filesystem::path(FS::GetResourcesDirectory() / "Shaders" / shaderFilename).wstring(); - const std::wstring binaryPath = - std::filesystem::path(FS::GetShaderCacheDirectory() / std::format("{}.{}.spv", m_Specification.Name, ShaderStageSuffix(type))) - .wstring(); - - const std::string entryPointNarrow = Utils::ShaderEntryPoint(type); - const std::wstring entryPoint(entryPointNarrow.begin(), entryPointNarrow.end()); - LPCWSTR args[] = { L"-E", - entryPoint.c_str(), - L"-T", - FindStage(type).TargetProfile, - L"-spirv", - L"-fspv-target-env=vulkan1.3", - L"-fvk-t-shift", - L"0", - L"0", - L"-fvk-s-shift", - L"128", - L"0", - L"-fvk-b-shift", - L"256", - L"0", - L"-fvk-u-shift", - L"384", - L"0", - L"-fspv-reflect", - L"-fvk-bind-resource-heap", - L"0", - L"1", - L"-fvk-bind-sampler-heap", - L"0", - L"2", - L"-D", - L"TARGET_VULKAN", - shaderPath.c_str(), - L"-Fo", - binaryPath.c_str() }; - - DxcBuffer srcBuffer{ - .Ptr = m_ShaderSource.c_str(), - .Size = m_ShaderSource.size(), - .Encoding = DXC_CP_UTF8, - }; - - // Execute compiler - ComPtr result; - if (FAILED(compiler->Compile(&srcBuffer, args, _countof(args), includeHandler, IID_PPV_ARGS(&result))) || !result) - { - Log::Error("Invoking the compiler for shader '{}' failed!", m_Specification.Name); - return false; - } - - ComPtr errors; - result->GetOutput(DXC_OUT_ERRORS, IID_PPV_ARGS(&errors), nullptr); - - if (errors != nullptr && errors->GetStringLength() != 0) - { - Log::Error("Compiler returned with errors: \n{}", errors->GetStringPointer()); - - HRESULT status; - result->GetStatus(&status); - if (FAILED(status)) - { - Log::Error("Compiling shader '{}' failed due to errors!", m_Specification.Name); - return false; - } - } - - // Save shader binary - ComPtr binary; - ComPtr binaryName; - result->GetOutput(DXC_OUT_OBJECT, IID_PPV_ARGS(&binary), &binaryName); - - if (binary == nullptr) - { - Log::Error("Compiling shader '{}' produced no binary!", m_Specification.Name); - return false; - } - - const char* pBinary = static_cast(binary->GetBufferPointer()); - FS::WriteBytes(binaryPath, pBinary, binary->GetBufferSize(), true); - m_ShaderBytes[type] = std::vector(pBinary, pBinary + binary->GetBufferSize()); - - return true; - } - auto VulkanShader::Reflect(nvrhi::ShaderType type) -> void { const spirv_cross::Compiler compiler(reinterpret_cast(m_ShaderBytes.at(type).data()), m_ShaderBytes.at(type).size() / 4); diff --git a/EppoEngine/Source/Platform/Vulkan/VulkanShader.h b/EppoEngine/Source/Platform/Vulkan/VulkanShader.h index bdc0dee8..f09960dc 100644 --- a/EppoEngine/Source/Platform/Vulkan/VulkanShader.h +++ b/EppoEngine/Source/Platform/Vulkan/VulkanShader.h @@ -10,8 +10,6 @@ namespace Eppo explicit VulkanShader(ShaderSpecification spec); private: - auto CompileOrGetCache() -> bool; - auto Compile(nvrhi::ShaderType type) -> bool; auto Reflect(nvrhi::ShaderType type) -> void; }; -} \ No newline at end of file +} diff --git a/EppoEngine/Source/Platform/Vulkan/Swapchain.cpp b/EppoEngine/Source/Platform/Vulkan/VulkanSwapchain.cpp similarity index 87% rename from EppoEngine/Source/Platform/Vulkan/Swapchain.cpp rename to EppoEngine/Source/Platform/Vulkan/VulkanSwapchain.cpp index 672b02ad..4f8b1655 100644 --- a/EppoEngine/Source/Platform/Vulkan/Swapchain.cpp +++ b/EppoEngine/Source/Platform/Vulkan/VulkanSwapchain.cpp @@ -1,9 +1,9 @@ #include "pch.h" -#include "Platform/Vulkan/Swapchain.h" +#include "Platform/Vulkan/VulkanSwapchain.h" -#include "Core/Application.h" #include "Platform/Vulkan/DeviceManagerVK.h" #include "Renderer/Image.h" +#include "Renderer/Renderer.h" #include #include @@ -30,11 +30,14 @@ namespace Eppo } } - Swapchain::Swapchain(const VkSurfaceKHR surface) - : m_Surface(surface) + VulkanSwapchain::VulkanSwapchain(GLFWwindow* window) + : Swapchain(window) { const auto& dm = std::static_pointer_cast(DeviceManager::Get()); + VK_CHECK(glfwCreateWindowSurface(dm->GetVulkanInstance(), window, nullptr, &m_Surface), "Failed to create window surface!"); + EP_ASSERT(m_Surface); + // Get swapchain support details auto [capabilities, formats, presentModes] = QuerySwapchainSupportDetails(); m_SurfaceFormat = SelectSurfaceFormat(formats); @@ -43,8 +46,11 @@ namespace Eppo m_Extent = SelectExtent(capabilities); } - Swapchain::~Swapchain() + VulkanSwapchain::~VulkanSwapchain() { + // Viewport swapchains are destroyed while the device is still alive + DeviceManager::Get()->WaitIdle(); + const auto& dm = std::static_pointer_cast(DeviceManager::Get()); VkDevice device = dm->GetLogicalDevice()->GetNative(); @@ -55,16 +61,24 @@ namespace Eppo for (auto& frame : m_FrameSyncData) vkDestroySemaphore(device, frame.AcquireSemaphore, nullptr); + m_FrameSyncData.clear(); vkDestroySwapchainKHR(device, m_Swapchain, nullptr); vkDestroySurfaceKHR(dm->GetVulkanInstance(), m_Surface, nullptr); } - auto Swapchain::BeginFrame() -> bool + auto VulkanSwapchain::BeginFrame() -> bool { EP_PROFILE_FN("Swapchain::BeginFrame"); EP_ASSERT(!m_FrameActive, "BeginFrame was called while a swapchain frame is already active!"); + const auto [width, height] = GetWindowFramebufferSize(); + if (width == 0 || height == 0) + return false; + + if (width != m_Width || height != m_Height) + m_ResizePending = true; + const auto& dm = std::static_pointer_cast(DeviceManager::Get()); VkDevice device = dm->GetLogicalDevice()->GetNative(); nvrhi::vulkan::IDevice* vkNvrhiDevice = dm->GetDevice()->getNativeObject(nvrhi::ObjectTypes::Nvrhi_VK_Device); @@ -72,7 +86,7 @@ namespace Eppo constexpr uint32_t maxAttempts = 3; VkResult result; - for (uint32_t attempt = 0; attempt < maxAttempts; attempt++) // switch to ++attempt + for (uint32_t attempt = 0; attempt < maxAttempts; attempt++) { if (m_ResizePending) Resize(); @@ -85,7 +99,8 @@ namespace Eppo frame.InFlight = false; } - result = vkAcquireNextImageKHR(device, m_Swapchain, UINT64_MAX, frame.AcquireSemaphore, nullptr, &m_SwapchainImageIndex); + const VkSemaphore acquireSemaphore = frame.AcquireSemaphore; + result = vkAcquireNextImageKHR(device, m_Swapchain, UINT64_MAX, acquireSemaphore, nullptr, &m_SwapchainImageIndex); if (result == VK_ERROR_OUT_OF_DATE_KHR) { @@ -101,7 +116,7 @@ namespace Eppo return false; } - vkNvrhiDevice->queueWaitForSemaphore(nvrhi::CommandQueue::Graphics, frame.AcquireSemaphore, 0); + vkNvrhiDevice->queueWaitForSemaphore(nvrhi::CommandQueue::Graphics, acquireSemaphore, 0); m_FrameActive = true; return true; } @@ -110,7 +125,7 @@ namespace Eppo return false; } - auto Swapchain::Present() -> bool + auto VulkanSwapchain::Present() -> bool { EP_PROFILE_FN("Swapchain::Present"); EP_ASSERT(m_FrameActive, "Present was called without an active swapchain frame!"); @@ -152,15 +167,13 @@ namespace Eppo return false; } - auto Swapchain::CreateSwapchain(uint32_t width, uint32_t height) -> void + auto VulkanSwapchain::CreateSwapchain(uint32_t width, uint32_t height) -> void { const auto& dm = std::static_pointer_cast(DeviceManager::Get()); VkDevice device = dm->GetLogicalDevice()->GetNative(); if (m_Swapchain) { - VK_CHECK(vkDeviceWaitIdle(device), "Failed to wait for vulkan device"); - for (const VkSemaphore semaphore : m_PresentSemaphores) vkDestroySemaphore(device, semaphore, nullptr); m_PresentSemaphores.clear(); @@ -177,6 +190,9 @@ namespace Eppo else m_Extent = { width, height }; + m_Width = m_Extent.width; + m_Height = m_Extent.height; + VkSwapchainCreateInfoKHR swapchainInfo{ .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, .surface = m_Surface, @@ -225,9 +241,9 @@ namespace Eppo for (auto& frame : m_FrameSyncData) { - VK_CHECK( - vkCreateSemaphore(device, &semaphoreInfo, nullptr, &frame.AcquireSemaphore), "Failed to create acquire semaphore!" - ); + VkSemaphore acquireSemaphore = nullptr; + VK_CHECK(vkCreateSemaphore(device, &semaphoreInfo, nullptr, &acquireSemaphore), "Failed to create acquire semaphore!"); + frame.AcquireSemaphore = acquireSemaphore; frame.CompletionQuery = dm->GetDevice()->createEventQuery(); EP_ASSERT(frame.CompletionQuery != nullptr, "Failed to create frame completion query."); frame.InFlight = false; @@ -278,15 +294,7 @@ namespace Eppo } } - auto Swapchain::Resize(uint32_t width, uint32_t height) -> void - { - const auto device = DeviceManager::Get()->GetDevice(); - - device->waitForIdle(); - CreateSwapchain(width, height); - } - - auto Swapchain::QuerySwapchainSupportDetails() const -> SwapchainSupportDetails + auto VulkanSwapchain::QuerySwapchainSupportDetails() const -> SwapchainSupportDetails { const auto& dm = std::static_pointer_cast(DeviceManager::Get()); const auto& physicalDevice = dm->GetPhysicalDevice(); @@ -336,7 +344,7 @@ namespace Eppo return details; } - auto Swapchain::SelectSurfaceFormat(const std::vector& surfaceFormats) -> VkSurfaceFormatKHR + auto VulkanSwapchain::SelectSurfaceFormat(const std::vector& surfaceFormats) -> VkSurfaceFormatKHR { EP_ASSERT(!surfaceFormats.empty(), "The Vulkan device reported no supported surface formats."); @@ -360,7 +368,7 @@ namespace Eppo return surfaceFormats.front(); } - auto Swapchain::SelectPresentMode(const std::vector& presentModes, const bool vsync) -> VkPresentModeKHR + auto VulkanSwapchain::SelectPresentMode(const std::vector& presentModes, const bool vsync) -> VkPresentModeKHR { if (vsync) return VK_PRESENT_MODE_FIFO_KHR; @@ -374,13 +382,13 @@ namespace Eppo return VK_PRESENT_MODE_FIFO_KHR; } - auto Swapchain::SelectExtent(const VkSurfaceCapabilitiesKHR& capabilities) const -> VkExtent2D + auto VulkanSwapchain::SelectExtent(const VkSurfaceCapabilitiesKHR& capabilities) const -> VkExtent2D { VkExtent2D extent = capabilities.currentExtent; if (capabilities.currentExtent.width == UINT32_MAX) { - const auto [width, height] = Application::Get().GetWindow()->GetFramebufferSize(); + const auto [width, height] = GetWindowFramebufferSize(); extent = { .width = width, .height = height }; extent.width = std::clamp(extent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); diff --git a/EppoEngine/Source/Platform/Vulkan/Swapchain.h b/EppoEngine/Source/Platform/Vulkan/VulkanSwapchain.h similarity index 57% rename from EppoEngine/Source/Platform/Vulkan/Swapchain.h rename to EppoEngine/Source/Platform/Vulkan/VulkanSwapchain.h index 3831c7a7..fdf06435 100644 --- a/EppoEngine/Source/Platform/Vulkan/Swapchain.h +++ b/EppoEngine/Source/Platform/Vulkan/VulkanSwapchain.h @@ -2,6 +2,7 @@ #include "Platform/Vulkan/Vulkan.h" #include "Renderer/DeviceManager.h" +#include "Renderer/Swapchain.h" namespace Eppo { @@ -12,23 +13,16 @@ namespace Eppo std::vector PresentModes; }; - class Swapchain + class VulkanSwapchain : public Swapchain { public: - Swapchain(VkSurfaceKHR surface); - ~Swapchain(); + explicit VulkanSwapchain(GLFWwindow* window); + ~VulkanSwapchain() override; - auto BeginFrame() -> bool; - auto Present() -> bool; + auto BeginFrame() -> bool override; + auto Present() -> bool override; - auto CreateSwapchain(uint32_t width = 0, uint32_t height = 0) -> void; - auto Resize(uint32_t width = 0, uint32_t height = 0) -> void; - - auto GetCurrentFrameIndex() const -> uint32_t { return m_CurrentFrameIndex; } - auto GetMaxFramesInFlight() const -> uint32_t { return m_MaxFramesInFlight; } - auto GetCurrentBackBufferIndex() const -> uint32_t { return m_SwapchainImageIndex; } - auto GetImageCount() const -> uint32_t { return static_cast(m_Images.size()); } - auto GetCurrentSwapchainImage() -> const SwapchainImage& { return m_Images.at(m_SwapchainImageIndex); } + auto CreateSwapchain(uint32_t width = 0, uint32_t height = 0) -> void override; private: [[nodiscard]] auto QuerySwapchainSupportDetails() const -> SwapchainSupportDetails; @@ -37,16 +31,6 @@ namespace Eppo [[nodiscard]] auto SelectExtent(const VkSurfaceCapabilitiesKHR& capabilities) const -> VkExtent2D; private: - VkSwapchainKHR m_Swapchain = nullptr; - VkSurfaceKHR m_Surface = nullptr; - - std::vector m_Images; - - VkFormat m_Format = VK_FORMAT_UNDEFINED; - VkExtent2D m_Extent{}; - VkPresentModeKHR m_PresentMode = VK_PRESENT_MODE_FIFO_KHR; - VkSurfaceFormatKHR m_SurfaceFormat; - struct FrameSync { VkSemaphore AcquireSemaphore = nullptr; @@ -55,11 +39,14 @@ namespace Eppo }; std::vector m_FrameSyncData; + VkSwapchainKHR m_Swapchain = nullptr; + VkSurfaceKHR m_Surface = nullptr; + + VkFormat m_Format = VK_FORMAT_UNDEFINED; + VkExtent2D m_Extent{}; + VkPresentModeKHR m_PresentMode = VK_PRESENT_MODE_FIFO_KHR; + VkSurfaceFormatKHR m_SurfaceFormat{}; + std::vector m_PresentSemaphores; - bool m_FrameActive = false; - bool m_ResizePending = false; - uint32_t m_CurrentFrameIndex = 0; // Index into m_FrameSyncData - uint32_t m_MaxFramesInFlight = 1; - uint32_t m_SwapchainImageIndex = 0; // Index into m_Images }; } diff --git a/EppoEngine/Source/Renderer/DeviceManager.cpp b/EppoEngine/Source/Renderer/DeviceManager.cpp index 85df632c..ced94918 100644 --- a/EppoEngine/Source/Renderer/DeviceManager.cpp +++ b/EppoEngine/Source/Renderer/DeviceManager.cpp @@ -5,6 +5,10 @@ #include "Platform/Vulkan/DeviceManagerVK.h" #include "Renderer/GpuProfiler.h" +#if defined(EP_PLATFORM_WINDOWS) + #include "Platform/DX12/DeviceManagerDX12.h" +#endif + namespace Eppo { auto DeviceManager::Get() -> Ref @@ -24,10 +28,7 @@ namespace Eppo { #if defined(EP_PLATFORM_WINDOWS) case RendererAPI::DX12: - { - EP_ASSERT(false, "Currently we do not support DX12!"); - break; - } + return CreateScopedPtr(window, params); #endif case RendererAPI::Vulkan: diff --git a/EppoEngine/Source/Renderer/DeviceManager.h b/EppoEngine/Source/Renderer/DeviceManager.h index 113e816d..82b09c7d 100644 --- a/EppoEngine/Source/Renderer/DeviceManager.h +++ b/EppoEngine/Source/Renderer/DeviceManager.h @@ -3,12 +3,16 @@ #include "Core/Window.h" #include "Renderer/Framebuffer.h" #include "Renderer/Renderer.h" +#include "Renderer/Swapchain.h" #include +struct GLFWwindow; + namespace Eppo { class DeviceManagerVK; + class DeviceManagerDX12; #if !defined(EP_DIST) constexpr bool s_EnableValidationLayers = true; @@ -57,12 +61,6 @@ namespace Eppo } }; - struct SwapchainImage - { - void* NativeImage = nullptr; - Ref Framebuffer = nullptr; - }; - struct DeviceParams { RendererAPI API = RendererAPI::Vulkan; @@ -92,20 +90,24 @@ namespace Eppo virtual auto Shutdown() -> void = 0; // Frame - virtual auto BeginFrame() -> bool = 0; - virtual auto Present() -> bool = 0; + auto BeginFrame() -> bool { return m_Swapchain->BeginFrame(); } + auto Present() -> bool { return m_Swapchain->Present(); } [[nodiscard]] auto WaitIdle() const -> bool; // Renderer auto InitRenderer() -> void; [[nodiscard]] constexpr auto GetRenderer() const -> const ScopedPtr& { return m_Renderer; } - // Swapchain/Nvrhi device - [[nodiscard]] virtual auto GetCurrentFrameIndex() const -> uint32_t = 0; - [[nodiscard]] virtual auto GetMaxFramesInFlight() const -> uint32_t = 0; - [[nodiscard]] virtual auto GetCurrentBackBufferIndex() const -> uint32_t = 0; - [[nodiscard]] virtual auto GetBackBufferCount() const -> uint32_t = 0; - virtual auto GetCurrentSwapchainImage() -> const SwapchainImage& = 0; + // Swapchain + virtual auto CreateSwapchain(GLFWwindow* window, uint32_t width = 0, uint32_t height = 0) -> Ref = 0; + [[nodiscard]] auto GetSwapchain() const -> const Ref& { return m_Swapchain; } + [[nodiscard]] auto GetCurrentFrameIndex() const -> uint32_t { return m_Swapchain->GetCurrentFrameIndex(); } + [[nodiscard]] auto GetMaxFramesInFlight() const -> uint32_t { return m_Swapchain->GetMaxFramesInFlight(); } + [[nodiscard]] auto GetCurrentBackBufferIndex() const -> uint32_t { return m_Swapchain->GetCurrentBackBufferIndex(); } + [[nodiscard]] auto GetBackBufferCount() const -> uint32_t { return m_Swapchain->GetImageCount(); } + [[nodiscard]] auto GetCurrentSwapchainImage() const -> const SwapchainImage& { return m_Swapchain->GetCurrentSwapchainImage(); } + + // Nvrhi device [[nodiscard]] virtual auto GetDevice() const -> nvrhi::IDevice* = 0; // Device Manager @@ -119,6 +121,7 @@ namespace Eppo DeviceParams m_Params; ScopedPtr m_Renderer = nullptr; Ref m_Window = nullptr; + Ref m_Swapchain = nullptr; NvrhiMessageCallback m_MessageCallback; }; diff --git a/EppoEngine/Source/Renderer/GpuProfiler.cpp b/EppoEngine/Source/Renderer/GpuProfiler.cpp index fa81d8a9..9795b78b 100644 --- a/EppoEngine/Source/Renderer/GpuProfiler.cpp +++ b/EppoEngine/Source/Renderer/GpuProfiler.cpp @@ -2,6 +2,11 @@ #include "Renderer/GpuProfiler.h" #include "Platform/Vulkan/VulkanGpuProfiler.h" +#include "Renderer/DeviceManager.h" + +#if defined(EP_PLATFORM_WINDOWS) + #include "Platform/DX12/DX12GpuProfiler.h" +#endif namespace Eppo { @@ -10,8 +15,22 @@ namespace Eppo auto GpuProfiler::Init() -> void { EP_ASSERT(!s_Instance, "GpuProfiler is already initialized!"); - // Vulkan is the only backend today; a D3D12 profiler would branch here on the active RendererAPI. - s_Instance = CreateScopedPtr(); + switch (DeviceManager::Get()->GetParams().API) + { +#if defined(EP_PLATFORM_WINDOWS) + case RendererAPI::DX12: + s_Instance = CreateScopedPtr(); + break; +#endif + + case RendererAPI::Vulkan: + s_Instance = CreateScopedPtr(); + break; + + default: + EP_ASSERT(false, "No renderer api selected!"); + break; + } } auto GpuProfiler::Shutdown() -> void diff --git a/EppoEngine/Source/Renderer/GpuProfiler.h b/EppoEngine/Source/Renderer/GpuProfiler.h index 8db87f49..7a96b60a 100644 --- a/EppoEngine/Source/Renderer/GpuProfiler.h +++ b/EppoEngine/Source/Renderer/GpuProfiler.h @@ -10,13 +10,15 @@ #include #include + #if defined(EP_PLATFORM_WINDOWS) + #include + #endif #endif namespace Eppo { class RenderCommandBuffer; - // Backend-neutral facade; GetNativeContext stays void* so a D3D12 backend can slot in beside Vulkan. class GpuProfiler { public: @@ -39,17 +41,17 @@ namespace Eppo private: static ScopedPtr s_Instance; }; -} -// TracyVkZone is an RAII/source-location macro, so it must expand at the call site rather than behind the interface. + // TracyVkZone is an RAII/source-location macro, so it must expand at the call site rather than behind the interface. #if defined(TRACY_ENABLE) - #define EP_GPU_ZONE(commandBuffer, name) \ - TracyVkZone(static_cast(::Eppo::GpuProfiler::Get()->GetNativeContext()), \ - static_cast((commandBuffer)->GetCommandList()->getNativeObject( \ - nvrhi::ObjectTypes::VK_CommandBuffer)), \ - name) - #define EP_GPU_COLLECT(commandBuffer) ::Eppo::GpuProfiler::Get()->Collect(commandBuffer) + #define EP_GPU_ZONE(commandBuffer, name) \ + TracyVkZone( \ + static_cast(GpuProfiler::Get()->GetNativeContext()), \ + static_cast((commandBuffer)->GetCommandList()->getNativeObject(nvrhi::ObjectTypes::VK_CommandBuffer)), name \ + ) + #define EP_GPU_COLLECT(commandBuffer) GpuProfiler::Get()->Collect(commandBuffer) #else #define EP_GPU_ZONE(commandBuffer, name) #define EP_GPU_COLLECT(commandBuffer) #endif +} diff --git a/EppoEngine/Source/Renderer/Image.cpp b/EppoEngine/Source/Renderer/Image.cpp index 447c6ac4..71538633 100644 --- a/EppoEngine/Source/Renderer/Image.cpp +++ b/EppoEngine/Source/Renderer/Image.cpp @@ -156,10 +156,18 @@ namespace Eppo .keepInitialState = spec.AutomaticStateTracking, }; - if (dm->GetParams().API == RendererAPI::Vulkan) - m_Texture = device->createHandleForNativeTexture(nvrhi::ObjectTypes::VK_Image, nvrhi::Object(existingImage), textureDesc); - else - EP_ASSERT(false); + switch (dm->GetParams().API) + { + case RendererAPI::Vulkan: + m_Texture = device->createHandleForNativeTexture(nvrhi::ObjectTypes::VK_Image, nvrhi::Object(existingImage), textureDesc); + break; + case RendererAPI::DX12: + m_Texture = + device->createHandleForNativeTexture(nvrhi::ObjectTypes::D3D12_Resource, nvrhi::Object(existingImage), textureDesc); + break; + default: + EP_ASSERT(false, "Unsupported renderer api!"); + } m_MipLevels = m_Texture->getDesc().mipLevels; m_Stride = GetStride(m_Texture->getDesc().format); diff --git a/EppoEngine/Source/Renderer/RenderCommandBuffer.cpp b/EppoEngine/Source/Renderer/RenderCommandBuffer.cpp index f027eb98..b8b2b1ac 100644 --- a/EppoEngine/Source/Renderer/RenderCommandBuffer.cpp +++ b/EppoEngine/Source/Renderer/RenderCommandBuffer.cpp @@ -88,6 +88,7 @@ namespace Eppo m_ActiveCommandList = nullptr; m_ActiveTimerQuery = nullptr; m_ActiveFrameIndex = UINT32_MAX; + m_GraphicsState = {}; } auto RenderCommandBuffer::BeginTimerQuery(const std::string& name) -> void diff --git a/EppoEngine/Source/Renderer/Renderer.cpp b/EppoEngine/Source/Renderer/Renderer.cpp index 853af28e..12b15f5c 100644 --- a/EppoEngine/Source/Renderer/Renderer.cpp +++ b/EppoEngine/Source/Renderer/Renderer.cpp @@ -141,6 +141,12 @@ namespace Eppo commandBuffer->GetCommandList()->clearState(); } + auto Renderer::ReleaseSwapchainResources() -> void + { + m_CompositePasses.clear(); + m_CompositeFramebuffers.clear(); + } + auto Renderer::CompositeToSwapchain(const Ref& image) -> void { EP_PROFILE_FN("Renderer::CompositeToSwapchain"); diff --git a/EppoEngine/Source/Renderer/Renderer.h b/EppoEngine/Source/Renderer/Renderer.h index af5cd6c9..41f09f87 100644 --- a/EppoEngine/Source/Renderer/Renderer.h +++ b/EppoEngine/Source/Renderer/Renderer.h @@ -28,6 +28,7 @@ namespace Eppo static auto BeginRenderPass(const Ref& commandBuffer, const Ref& renderPass) -> void; static auto EndRenderPass(const Ref& commandBuffer) -> void; auto CompositeToSwapchain(const Ref& image) -> void; + auto ReleaseSwapchainResources() -> void; [[nodiscard]] auto GetShader(const std::string& name) const -> Ref; [[nodiscard]] auto GetSampler(const SamplerSpecification& specification) -> Ref; diff --git a/EppoEngine/Source/Renderer/Shader.cpp b/EppoEngine/Source/Renderer/Shader.cpp index 14a05f02..e03a8bf4 100644 --- a/EppoEngine/Source/Renderer/Shader.cpp +++ b/EppoEngine/Source/Renderer/Shader.cpp @@ -1,13 +1,181 @@ #include "pch.h" #include "Renderer/Shader.h" +#include "Platform/ComPtr.h" #include "Platform/Vulkan/VulkanShader.h" #include "Renderer/DescriptorManager.h" #include "Renderer/DeviceManager.h" #include "Renderer/Renderer.h" +#if defined(EP_PLATFORM_WINDOWS) + #include "Platform/DX12/DX12Shader.h" +#endif + +#include + namespace Eppo { + namespace + { + struct ShaderStage + { + nvrhi::ShaderType Type; + const wchar_t* TargetProfile; + const char* CacheSuffix; + }; + + constexpr std::array s_ShaderStages{ + ShaderStage{ .Type = nvrhi::ShaderType::Vertex, .TargetProfile = L"vs_6_6", .CacheSuffix = "vert" }, + ShaderStage{ .Type = nvrhi::ShaderType::Pixel, .TargetProfile = L"ps_6_6", .CacheSuffix = "frag" }, + }; + + auto FindStage(const nvrhi::ShaderType type) -> const ShaderStage& + { + const auto it = std::ranges::find(s_ShaderStages, type, &ShaderStage::Type); + EP_ASSERT(it != s_ShaderStages.end()); + return *it; + } + + auto ShaderStageSuffix(const nvrhi::ShaderType type) -> std::string + { + return FindStage(type).CacheSuffix; + } + + [[nodiscard]] auto ShaderBinaryExtension(const RendererAPI api) -> const char* + { + switch (api) + { + case RendererAPI::DX12: + return "dxil"; + case RendererAPI::Vulkan: + return "spv"; + default: + EP_ASSERT(false, "No renderer API selected for shader compilation!"); + return ""; + } + } + + auto DetectStages(const std::string& source) -> std::vector + { + std::vector stages; + for (const auto& stage : s_ShaderStages) + if (source.find(Utils::ShaderEntryPoint(stage.Type)) != std::string::npos) + stages.push_back(stage.Type); + return stages; + } + + // Resolves #includes strictly from a packed game. A deployed runtime has no shader files on disk, + // and must not acquire any: an include that is not in the pack fails the compile instead. + class PackedIncludeHandler final : public IDxcIncludeHandler + { + public: + PackedIncludeHandler(IDxcUtils* utils, const std::map& includes) + : m_Utils(utils), m_Includes(includes) + {} + + auto STDMETHODCALLTYPE LoadSource(LPCWSTR filename, IDxcBlob** includeSource) -> HRESULT override + { + if (!includeSource) + return E_INVALIDARG; + *includeSource = nullptr; + + const auto* source = Find(std::filesystem::path(filename).lexically_normal().generic_string()); + if (!source) + { + Log::Error("Packed shader include '{}' is not in the game package.", std::filesystem::path(filename).generic_string()); + return E_FAIL; + } + + ComPtr blob; + if (FAILED(m_Utils->CreateBlob(source->data(), static_cast(source->size()), DXC_CP_UTF8, &blob))) + return E_FAIL; + + *includeSource = blob.Detach(); + return S_OK; + } + + auto STDMETHODCALLTYPE QueryInterface(REFIID riid, void** object) -> HRESULT override + { + if (!object) + return E_INVALIDARG; + + if (riid == __uuidof(IDxcIncludeHandler) || riid == __uuidof(IUnknown)) + { + *object = static_cast(this); + AddRef(); + return S_OK; + } + + *object = nullptr; + return E_NOINTERFACE; + } + + // Scoped to a single Compile call, so reference counting has nothing to manage. + auto STDMETHODCALLTYPE AddRef() -> ULONG override { return 1; } + auto STDMETHODCALLTYPE Release() -> ULONG override { return 1; } + + private: + // DXC resolves an include against the includer's name, so it may arrive prefixed. Keys are relative + // to Resources/Shaders; take the longest matching suffix, since a shorter one can match a different file. + [[nodiscard]] auto Find(const std::string& requested) const -> const std::string* + { + if (const auto it = m_Includes.find(requested); it != m_Includes.end()) + return &it->second; + + const std::string* match = nullptr; + size_t matched = 0; + for (const auto& [path, source] : m_Includes) + { + if (requested.size() <= path.size() || !requested.ends_with(path) || + requested.at(requested.size() - path.size() - 1) != '/') + continue; + + if (path.size() > matched) + { + match = &source; + matched = path.size(); + } + } + + return match; + } + + IDxcUtils* m_Utils = nullptr; + const std::map& m_Includes; + }; + + // Includes are part of a shader's compiled result, so the cache key has to cover them too. Lengths are + // folded in as well, otherwise a boundary can shift between two entries without changing the hash. + auto HashSource(const std::string& source, const std::map& includes) -> std::string + { + std::string combined = std::format("{}:{}", source.size(), source); + for (const auto& [path, includeSource] : includes) + combined += std::format("{}:{}{}:{}", path.size(), path, includeSource.size(), includeSource); + + return std::to_string(Hash::GenerateFnv(combined)); + } + + // Kept in step with the include walk in ProjectExporter::Export: if the two sets diverge, the editor + // and the deployed game hash the same shader differently and every shipped game recompiles on launch. + auto ReadIncludesFromDisk() -> std::map + { + const auto shadersDirectory = FS::GetResourcesDirectory() / "Shaders"; + if (!FS::Exists(shadersDirectory)) + return {}; + + std::map includes; + for (const auto& entry : std::filesystem::recursive_directory_iterator(shadersDirectory)) + { + if (!entry.is_regular_file() || entry.path().extension() != ".hlsli") + continue; + + includes.emplace(std::filesystem::relative(entry.path(), shadersDirectory).generic_string(), FS::ReadText(entry.path())); + } + + return includes; + } + } + namespace Utils { auto NvrhiFormatSize(nvrhi::Format format) -> uint32_t @@ -60,10 +228,201 @@ namespace Eppo { Log::Info("Loading shader '{}'", m_Specification.Name); - EP_ASSERT(DeviceManager::Get()->GetParams().API == RendererAPI::Vulkan); + EP_ASSERT(DeviceManager::Get()->GetParams().API != RendererAPI::None); EP_ASSERT(!m_Specification.IsCompute); } + auto Shader::CompileOrGetCache() -> bool + { + // A packed source is all a packed shader may read, along with its packed includes; it must not reach + // the filesystem for either. (The shader cache below is still on disk, but it is this shader's own + // output, keyed by a hash of the source.) + if (!m_Specification.Source.empty()) + { + m_ShaderSource = m_Specification.Source; + } + else + { + const std::filesystem::path sourcePath = FS::GetResourcesDirectory() / "Shaders" / std::format("{}.hlsl", m_Specification.Name); + m_ShaderSource = FS::ReadText(sourcePath); + m_Specification.Includes = ReadIncludesFromDisk(); + } + + const std::vector stages = DetectStages(m_ShaderSource); + if (stages.empty()) + { + Log::Error("Shader '{}' defines no known stage entry points.", m_Specification.Name); + return false; + } + + // One source compiles to several stage binaries, so a single hash of that source keys them all. + const std::string hash = HashSource(m_ShaderSource, m_Specification.Includes); + const auto extension = ShaderBinaryExtension(DeviceManager::Get()->GetParams().API); + const std::filesystem::path shaderHashPath = + FS::GetShaderCacheDirectory() / std::format("{}.{}.hash", m_Specification.Name, extension); + + bool verified = FS::Exists(shaderHashPath) && FS::ReadText(shaderHashPath) == hash; + for (const auto type : stages) + { + const std::filesystem::path shaderBinaryPath = + FS::GetShaderCacheDirectory() / std::format("{}.{}.{}", m_Specification.Name, ShaderStageSuffix(type), extension); + if (!FS::Exists(shaderBinaryPath)) + verified = false; + } + + if (verified) + { + Log::Info("Loading shader cache for '{}'", m_Specification.Name); + + for (const auto type : stages) + { + const std::filesystem::path shaderBinaryPath = + FS::GetShaderCacheDirectory() / std::format("{}.{}.{}", m_Specification.Name, ShaderStageSuffix(type), extension); + m_ShaderBytes[type] = FS::ReadBytes(shaderBinaryPath); + } + + return true; + } + + Log::Info("Compiling shader '{}'", m_Specification.Name); + + for (const auto type : stages) + { + if (!Compile(type)) + return false; + } + + FS::WriteText(shaderHashPath, hash, true); + + return true; + } + + auto Shader::Compile(const nvrhi::ShaderType type) -> bool + { + // Create compiler + ComPtr utils; + ComPtr compiler; + if (FAILED(DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&utils))) || + FAILED(DxcCreateInstance(CLSID_DxcCompiler, IID_PPV_ARGS(&compiler)))) + { + Log::Error("Could not create the DXC compiler; is dxcompiler.dll present next to the executable?"); + return false; + } + + // Create include handler. A packed shader gets the pack-backed one and never the default: + // the default reads from disk, which a deployed game has none of. + const bool packed = !m_Specification.Source.empty(); + PackedIncludeHandler packedIncludeHandler(utils.Get(), m_Specification.Includes); + ComPtr diskIncludeHandler; + if (!packed) + utils->CreateDefaultIncludeHandler(&diskIncludeHandler); + IDxcIncludeHandler* includeHandler = packed ? static_cast(&packedIncludeHandler) : diskIncludeHandler.Get(); + + // Command line args for compiler. A packed shader is named relative to the virtual Resources/Shaders + // root, so DXC hands its #include paths to the handler the way the pack keys them. + const auto shaderFilename = std::format("{}.hlsl", m_Specification.Name); + const std::wstring shaderPath = packed ? std::filesystem::path(shaderFilename).wstring() + : std::filesystem::path(FS::GetResourcesDirectory() / "Shaders" / shaderFilename).wstring(); + const auto extension = ShaderBinaryExtension(DeviceManager::Get()->GetParams().API); + const std::wstring binaryPath = + std::filesystem::path( + FS::GetShaderCacheDirectory() / std::format("{}.{}.{}", m_Specification.Name, ShaderStageSuffix(type), extension) + ) + .wstring(); + + const std::string entryPointNarrow = Utils::ShaderEntryPoint(type); + const std::wstring entryPoint(entryPointNarrow.begin(), entryPointNarrow.end()); + std::vector args{ L"-E", entryPoint.c_str(), L"-T", FindStage(type).TargetProfile }; + switch (DeviceManager::Get()->GetParams().API) + { + case RendererAPI::Vulkan: + args.insert( + args.end(), + { + L"-spirv", + L"-fspv-target-env=vulkan1.3", + L"-fvk-t-shift", + L"0", + L"0", + L"-fvk-s-shift", + L"128", + L"0", + L"-fvk-b-shift", + L"256", + L"0", + L"-fvk-u-shift", + L"384", + L"0", + L"-fspv-reflect", + L"-fvk-bind-resource-heap", + L"0", + L"1", + L"-fvk-bind-sampler-heap", + L"0", + L"2", + L"-D", + L"TARGET_VULKAN", + } + ); + break; + case RendererAPI::DX12: + args.insert(args.end(), { L"-D", L"TARGET_DX12" }); + break; + default: + EP_ASSERT(false, "No renderer api selected!"); + return false; + } + args.insert(args.end(), { shaderPath.c_str(), L"-Fo", binaryPath.c_str() }); + + DxcBuffer srcBuffer{ + .Ptr = m_ShaderSource.c_str(), + .Size = m_ShaderSource.size(), + .Encoding = DXC_CP_UTF8, + }; + + // Execute compiler + ComPtr result; + if (FAILED(compiler->Compile(&srcBuffer, args.data(), static_cast(args.size()), includeHandler, IID_PPV_ARGS(&result))) || + !result) + { + Log::Error("Invoking the compiler for shader '{}' failed!", m_Specification.Name); + return false; + } + + ComPtr errors; + result->GetOutput(DXC_OUT_ERRORS, IID_PPV_ARGS(&errors), nullptr); + + if (errors != nullptr && errors->GetStringLength() != 0) + { + Log::Error("Compiler returned with errors: \n{}", errors->GetStringPointer()); + + HRESULT status; + result->GetStatus(&status); + if (FAILED(status)) + { + Log::Error("Compiling shader '{}' failed due to errors!", m_Specification.Name); + return false; + } + } + + // Save shader binary + ComPtr binary; + ComPtr binaryName; + result->GetOutput(DXC_OUT_OBJECT, IID_PPV_ARGS(&binary), &binaryName); + + if (binary == nullptr) + { + Log::Error("Compiling shader '{}' produced no binary!", m_Specification.Name); + return false; + } + + const char* pBinary = static_cast(binary->GetBufferPointer()); + FS::WriteBytes(binaryPath, pBinary, binary->GetBufferSize(), true); + m_ShaderBytes[type] = std::vector(pBinary, pBinary + binary->GetBufferSize()); + + return true; + } + auto Shader::GetShaderHandle(const nvrhi::ShaderType type) -> nvrhi::ShaderHandle { if (const auto it = m_ShaderHandles.find(type); it != m_ShaderHandles.end()) @@ -75,6 +434,11 @@ namespace Eppo { switch (const auto& dm = DeviceManager::Get(); dm->GetParams().API) { +#if defined(EP_PLATFORM_WINDOWS) + case RendererAPI::DX12: + return CreateRef(std::move(spec)); +#endif + case RendererAPI::Vulkan: return CreateRef(std::move(spec)); diff --git a/EppoEngine/Source/Renderer/Shader.h b/EppoEngine/Source/Renderer/Shader.h index 0e87a182..d36fc960 100644 --- a/EppoEngine/Source/Renderer/Shader.h +++ b/EppoEngine/Source/Renderer/Shader.h @@ -74,6 +74,8 @@ namespace Eppo static auto Create(ShaderSpecification spec) -> Ref; protected: + auto CompileOrGetCache() -> bool; + auto Compile(nvrhi::ShaderType type) -> bool; auto CreateShaderHandles() -> void; auto CreateInputLayout() -> void; auto CreateBindingLayout() -> void; diff --git a/EppoEngine/Source/Renderer/Swapchain.cpp b/EppoEngine/Source/Renderer/Swapchain.cpp new file mode 100644 index 00000000..0a73421f --- /dev/null +++ b/EppoEngine/Source/Renderer/Swapchain.cpp @@ -0,0 +1,36 @@ +#include "pch.h" +#include "Renderer/Swapchain.h" + +#include "Renderer/DeviceManager.h" +#include "Renderer/Renderer.h" + +#include + +namespace Eppo +{ + Swapchain::Swapchain(GLFWwindow* window) + : m_Window(window) + { + EP_ASSERT(window); + } + + auto Swapchain::Resize(const uint32_t width, const uint32_t height) -> void + { + const auto device = DeviceManager::Get()->GetDevice(); + + device->waitForIdle(); + DeviceManager::Get()->GetRenderer()->ReleaseSwapchainResources(); + device->runGarbageCollection(); + CreateSwapchain(width, height); + m_ResizePending = false; + } + + auto Swapchain::GetWindowFramebufferSize() const -> std::pair + { + int width = 0; + int height = 0; + glfwGetFramebufferSize(m_Window, &width, &height); + + return { static_cast(width), static_cast(height) }; + } +} diff --git a/EppoEngine/Source/Renderer/Swapchain.h b/EppoEngine/Source/Renderer/Swapchain.h new file mode 100644 index 00000000..1622c468 --- /dev/null +++ b/EppoEngine/Source/Renderer/Swapchain.h @@ -0,0 +1,50 @@ +#pragma once + +#include "Renderer/Framebuffer.h" + +struct GLFWwindow; + +namespace Eppo +{ + struct SwapchainImage + { + void* NativeImage = nullptr; + Ref Framebuffer = nullptr; + }; + + class Swapchain + { + public: + virtual ~Swapchain() = default; + + virtual auto BeginFrame() -> bool = 0; + virtual auto Present() -> bool = 0; + auto Resize(uint32_t width = 0, uint32_t height = 0) -> void; + + [[nodiscard]] constexpr auto GetCurrentFrameIndex() const -> uint32_t { return m_CurrentFrameIndex; } + [[nodiscard]] constexpr auto GetMaxFramesInFlight() const -> uint32_t { return m_MaxFramesInFlight; } + [[nodiscard]] constexpr auto GetCurrentBackBufferIndex() const -> uint32_t { return m_SwapchainImageIndex; } + [[nodiscard]] auto GetImageCount() const -> uint32_t { return static_cast(m_Images.size()); } + [[nodiscard]] auto GetCurrentSwapchainImage() const -> const SwapchainImage& { return m_Images.at(m_SwapchainImageIndex); } + [[nodiscard]] constexpr auto GetWidth() const -> uint32_t { return m_Width; } + [[nodiscard]] constexpr auto GetHeight() const -> uint32_t { return m_Height; } + + protected: + explicit Swapchain(GLFWwindow* window); + + [[nodiscard]] auto GetWindowFramebufferSize() const -> std::pair; + virtual auto CreateSwapchain(uint32_t width, uint32_t height) -> void = 0; + + protected: + GLFWwindow* m_Window = nullptr; + std::vector m_Images; + + bool m_FrameActive = false; + bool m_ResizePending = false; + uint32_t m_CurrentFrameIndex = 0; // Index into frame synchronization data + uint32_t m_MaxFramesInFlight = 1; + uint32_t m_SwapchainImageIndex = 0; // Index into m_Images + uint32_t m_Width = 0; + uint32_t m_Height = 0; + }; +} diff --git a/EppoEngine/premake5.lua b/EppoEngine/premake5.lua index 561f4e82..986826c3 100644 --- a/EppoEngine/premake5.lua +++ b/EppoEngine/premake5.lua @@ -69,6 +69,7 @@ project "EppoEngine" filter "system:linux" defines { "EP_PLATFORM_LINUX", "__EMULATE_UUID" } pic "On" + removefiles { "Source/Platform/DX12/**" } filter {} diff --git a/EppoEngineTesting/Source/Renderer/DescriptorManager.cpp b/EppoEngineTesting/Source/Renderer/DescriptorManager.cpp index 2fd24514..60a19d5a 100644 --- a/EppoEngineTesting/Source/Renderer/DescriptorManager.cpp +++ b/EppoEngineTesting/Source/Renderer/DescriptorManager.cpp @@ -50,8 +50,9 @@ TEST(Renderer, DescriptorManager_RegisterResourceIncreasesNextFreeSlot) const auto& manager = CreateRef(); EP_REQUIRE(manager); - const auto& dm = DeviceManager::Get(); - const auto& image = dm->GetCurrentSwapchainImage().Framebuffer->GetFinalImage(); + const auto image = Image::Create(ImageSpecification{ + .ImageFormat = nvrhi::Format::RGBA8_UNORM, .Width = 1, .Height = 1, + }); EP_REQUIRE(image); const auto& resourceHeap = manager->GetResourceHeap(); @@ -87,8 +88,9 @@ TEST(Renderer, DescriptorManager_RegisterResourceReturnsValidHandle) const auto& manager = CreateRef(); EP_REQUIRE(manager); - const auto& dm = DeviceManager::Get(); - const auto& image = dm->GetCurrentSwapchainImage().Framebuffer->GetFinalImage(); + const auto image = Image::Create(ImageSpecification{ + .ImageFormat = nvrhi::Format::RGBA8_UNORM, .Width = 1, .Height = 1, + }); EP_REQUIRE(image); const auto handle = manager->Register(image); @@ -303,7 +305,7 @@ TEST(Renderer, DescriptorManager_SamplerLayoutIsMutableSampler) EXPECT_TRUE(layout->getBindlessDesc()->layoutType == nvrhi::BindlessLayoutDesc::LayoutType::MutableSampler); } -TEST(Renderer, DescriptorManager_DescriptorTableCapacityMatchesLayoutMaxCapacity) +TEST(Renderer, DescriptorManager_DescriptorTableCapacityCoversAllocatedSlots) { if (!Testing::AppHarness::IsAvailable()) return; @@ -317,7 +319,8 @@ TEST(Renderer, DescriptorManager_DescriptorTableCapacityMatchesLayoutMaxCapacity const auto& layout = resourceHeap->BindingLayout; EP_REQUIRE(layout); - EXPECT_EQ(layout->getBindlessDesc()->maxCapacity, resourceHeap->DescriptorTable->getCapacity()); + EXPECT_GE(resourceHeap->DescriptorTable->getCapacity(), resourceHeap->Capacity); + EXPECT_LE(resourceHeap->DescriptorTable->getCapacity(), layout->getBindlessDesc()->maxCapacity); } TEST(Renderer, DescriptorManager_HandleReleasesToOwningManager) diff --git a/EppoEngineTesting/Source/Renderer/DeviceManager.cpp b/EppoEngineTesting/Source/Renderer/DeviceManager.cpp index 669142c5..932c9aa8 100644 --- a/EppoEngineTesting/Source/Renderer/DeviceManager.cpp +++ b/EppoEngineTesting/Source/Renderer/DeviceManager.cpp @@ -23,6 +23,9 @@ TEST(Renderer, PhysicalDevice_ReportsAllRequiredFeatures) return; const auto& dm = DeviceManager::Get(); + if (dm->GetParams().API != RendererAPI::Vulkan) + GTEST_SKIP() << "This case checks Vulkan physical-device features."; + const auto* deviceManager = dynamic_cast(dm.get()); EP_REQUIRE(deviceManager); EP_REQUIRE(deviceManager->GetPhysicalDevice()); diff --git a/EppoEngineTesting/Source/Renderer/GpuProfiler.cpp b/EppoEngineTesting/Source/Renderer/GpuProfiler.cpp new file mode 100644 index 00000000..1cc7c39a --- /dev/null +++ b/EppoEngineTesting/Source/Renderer/GpuProfiler.cpp @@ -0,0 +1,99 @@ +#include "TestSupport/EppoTest.h" +#include "TestSupport/AppHarness.h" +#include "TestSupport/TestContext.h" + +#include "Platform/Vulkan/VulkanGpuProfiler.h" +#include "Renderer/DeviceManager.h" +#include "Renderer/GpuProfiler.h" +#include "Renderer/RenderCommandBuffer.h" +#include "Renderer/Renderer.h" + +#if defined(EP_PLATFORM_WINDOWS) + #include "Platform/DX12/DX12GpuProfiler.h" + #include "Platform/DX12/DeviceManagerDX12.h" + #include +#endif + +using namespace Eppo; + +TEST(Renderer, GpuProfiler_UsesSelectedBackend) +{ + ASSERT_TRUE(Testing::AppHarness::IsAvailable()); + + const auto* profiler = GpuProfiler::Get(); + ASSERT_NE(nullptr, profiler); +#if defined(TRACY_ENABLE) + EXPECT_NE(nullptr, profiler->GetNativeContext()); +#else + EXPECT_EQ(nullptr, profiler->GetNativeContext()); +#endif + + switch (DeviceManager::Get()->GetParams().API) + { + case RendererAPI::Vulkan: + EXPECT_NE(nullptr, dynamic_cast(profiler)); + break; +#if defined(EP_PLATFORM_WINDOWS) + case RendererAPI::DX12: + EXPECT_NE(nullptr, dynamic_cast(profiler)); + break; +#endif + default: + FAIL() << "No supported GPU profiler backend selected."; + } +} + +TEST(Renderer, GpuProfiler_NestedZonesSurviveFrameReuseAndCollection) +{ + Testing::TestContext context; + ASSERT_TRUE(context.IsAvailable()); + + const auto deviceManager = DeviceManager::Get(); +#if defined(EP_PLATFORM_WINDOWS) + ComPtr infoQueue; + if (s_EnableValidationLayers && deviceManager->GetParams().API == RendererAPI::DX12) + { + const auto dxDeviceManager = std::static_pointer_cast(deviceManager); + ASSERT_TRUE(SUCCEEDED(dxDeviceManager->GetDxDevice()->QueryInterface(IID_PPV_ARGS(&infoQueue)))); + } + const auto firstMessage = infoQueue ? infoQueue->GetNumStoredMessagesAllowedByRetrievalFilter() : 0; +#endif + + const auto commandBuffer = CreateRef(); + const auto frameCount = deviceManager->GetMaxFramesInFlight() * 4; + uint32_t submittedFrames = 0; + context.AdvanceFrames(frameCount, [&](float) -> void + { + Renderer::Submit([&]() -> void + { + commandBuffer->Begin(); + { + EP_GPU_ZONE(commandBuffer, "ProfilerOuterZone") + { + EP_GPU_ZONE(commandBuffer, "ProfilerInnerZone") + } + } + EP_GPU_COLLECT(commandBuffer); + commandBuffer->End(); + commandBuffer->Submit(); + submittedFrames++; + }); + }); + + ASSERT_TRUE(deviceManager->WaitIdle()); + EXPECT_EQ(frameCount, submittedFrames); +#if defined(EP_PLATFORM_WINDOWS) + if (infoQueue) + { + for (auto index = firstMessage; index < infoQueue->GetNumStoredMessagesAllowedByRetrievalFilter(); index++) + { + SIZE_T size = 0; + ASSERT_TRUE(SUCCEEDED(infoQueue->GetMessage(index, nullptr, &size))); + std::vector storage(size); + auto* message = reinterpret_cast(storage.data()); + ASSERT_TRUE(SUCCEEDED(infoQueue->GetMessage(index, message, &size))); + EXPECT_GT(message->Severity, D3D12_MESSAGE_SEVERITY_ERROR) << message->pDescription; + } + } +#endif +} diff --git a/EppoEngineTesting/Source/Renderer/SceneRendering.cpp b/EppoEngineTesting/Source/Renderer/SceneRendering.cpp index 4bab58c4..5d740904 100644 --- a/EppoEngineTesting/Source/Renderer/SceneRendering.cpp +++ b/EppoEngineTesting/Source/Renderer/SceneRendering.cpp @@ -1074,6 +1074,11 @@ TEST(Renderer, SceneRenderer_RoughIblSuppressesHighFrequencyFireflies) // nvrhi command list, which double-began/re-submitted the buffer and tripped Vulkan validation. TEST(Renderer, VulkanGpuProfiler_Construction_EmitsNoVulkanValidationErrors) { + if (!Testing::AppHarness::IsAvailable()) + return; + if (DeviceManager::Get()->GetParams().API != RendererAPI::Vulkan) + GTEST_SKIP() << "This case checks Vulkan profiler construction."; + Testing::TestContext ctx; if (!ctx.IsAvailable()) return; diff --git a/EppoEngineTesting/Source/Renderer/Shader.cpp b/EppoEngineTesting/Source/Renderer/Shader.cpp index 8dfbac0d..6abfd8df 100644 --- a/EppoEngineTesting/Source/Renderer/Shader.cpp +++ b/EppoEngineTesting/Source/Renderer/Shader.cpp @@ -30,8 +30,9 @@ namespace // A packed shader must compile fresh: a cache hit from an earlier run would bypass include resolution entirely. auto DiscardShaderCache(const std::string& name) -> void { - FS::RemoveAll(FS::GetShaderCacheDirectory() / std::format("{}.vert.spv", name)); - FS::RemoveAll(FS::GetShaderCacheDirectory() / std::format("{}.hash", name)); + const auto extension = DeviceManager::Get()->GetParams().API == RendererAPI::DX12 ? "dxil" : "spv"; + FS::RemoveAll(FS::GetShaderCacheDirectory() / std::format("{}.vert.{}", name, extension)); + FS::RemoveAll(FS::GetShaderCacheDirectory() / std::format("{}.{}.hash", name, extension)); } auto CheckMeshVertexLayout(const Ref& shader) -> void @@ -49,7 +50,7 @@ namespace EP_REQUIRE(attribute != nullptr); EXPECT_EQ(static_cast(sizeof(Vertex)), attribute->elementStride); - if (attribute->name.ends_with("TANGENT0")) + if (attribute->name.ends_with("TANGENT0") || attribute->name == "TANGENT") { foundTangent = true; EXPECT_TRUE(attribute->format == nvrhi::Format::RGBA32_FLOAT); @@ -158,7 +159,7 @@ TEST(Renderer, Shader_ShadowDepthReflectsMeshLayoutAndBindings) EXPECT_TRUE(HasResource(shadowDepth, 0, 1, nvrhi::ResourceType::StructuredBuffer_SRV)); EXPECT_TRUE(HasResource(shadowDepth, 0, 2, nvrhi::ResourceType::StructuredBuffer_SRV)); EP_REQUIRE(shadowDepth->HasPushConstants()); - EXPECT_EQ(sizeof(uint32_t), shadowDepth->GetPushConstants().Size); + EXPECT_EQ(3 * sizeof(uint32_t), shadowDepth->GetPushConstants().Size); EXPECT_TRUE(HasResource(renderer->GetShader("geometry"), 0, 2, nvrhi::ResourceType::ConstantBuffer)); } @@ -231,3 +232,13 @@ TEST(Renderer, Shader_WithoutStaticBindingsStillProducesGaplessLayouts) EXPECT_TRUE(layouts.contains(1)); EXPECT_TRUE(layouts.contains(2)); } + +TEST(Renderer, Shader_ImGuiReflectsPushConstantsSeparatelyFromConstantBuffers) +{ + ASSERT_TRUE(Testing::AppHarness::IsAvailable()); + const auto& shader = DeviceManager::Get()->GetRenderer()->GetShader("imgui"); + ASSERT_TRUE(shader->HasPushConstants()); + EXPECT_EQ(0u, shader->GetPushConstants().Binding); + EXPECT_EQ(16u, shader->GetPushConstants().Size); + EXPECT_FALSE(HasResource(shader, 0, 0, nvrhi::ResourceType::ConstantBuffer)); +} diff --git a/EppoEngineTesting/Source/Renderer/Swapchain.cpp b/EppoEngineTesting/Source/Renderer/Swapchain.cpp new file mode 100644 index 00000000..ad0e88ea --- /dev/null +++ b/EppoEngineTesting/Source/Renderer/Swapchain.cpp @@ -0,0 +1,52 @@ +#include "TestSupport/EppoTest.h" +#include "TestSupport/AppHarness.h" +#include "TestSupport/TestContext.h" + +#include "Renderer/DeviceManager.h" +#include "Renderer/Image.h" +#include "Renderer/Renderer.h" +#include "Renderer/Swapchain.h" + +#include + +using namespace Eppo; + +TEST(Renderer, Swapchain_ResizePreservesFrameCycleUntilNextResize) +{ + Testing::TestContext context; + ASSERT_TRUE(context.IsAvailable()); + + const auto* application = Testing::AppHarness::Get(); + const auto& swapchain = DeviceManager::Get()->GetSwapchain(); + auto* window = application->GetWindow()->GetNative(); + int originalWidth = 0; + int originalHeight = 0; + glfwGetWindowSize(window, &originalWidth, &originalHeight); + + const auto image = Image::Create(ImageSpecification{ .ImageFormat = nvrhi::Format::RGBA8_UNORM, .Width = 32, .Height = 32 }); + context.AdvanceFrames(swapchain->GetMaxFramesInFlight() + 1, [&](float) -> void + { + DeviceManager::Get()->GetRenderer()->CompositeToSwapchain(image); + }); + glfwSetWindowSize(window, originalWidth - 64, originalHeight - 32); + context.AdvanceFrames(1); + + const auto [width, height] = application->GetWindow()->GetFramebufferSize(); + EXPECT_EQ(width, swapchain->GetWidth()); + EXPECT_EQ(height, swapchain->GetHeight()); + const WeakRef framebuffer = swapchain->GetCurrentSwapchainImage().Framebuffer; + auto expectedFrameIndex = swapchain->GetCurrentFrameIndex(); + const auto frameCount = swapchain->GetMaxFramesInFlight() * 3; + for (uint32_t frame = 0; frame < frameCount; frame++) + { + context.AdvanceFrames(1); + expectedFrameIndex = (expectedFrameIndex + 1) % swapchain->GetMaxFramesInFlight(); + EXPECT_EQ(expectedFrameIndex, swapchain->GetCurrentFrameIndex()); + EXPECT_FALSE(framebuffer.expired()); + EXPECT_LT(swapchain->GetCurrentBackBufferIndex(), swapchain->GetImageCount()); + } + + glfwSetWindowSize(window, originalWidth, originalHeight); + context.AdvanceFrames(1); + EXPECT_TRUE(framebuffer.expired()); +} diff --git a/EppoEngineTesting/Source/TestSupport/AppHarness.cpp b/EppoEngineTesting/Source/TestSupport/AppHarness.cpp index 24029420..eef1563e 100644 --- a/EppoEngineTesting/Source/TestSupport/AppHarness.cpp +++ b/EppoEngineTesting/Source/TestSupport/AppHarness.cpp @@ -5,6 +5,8 @@ #include "Renderer/DeviceManager.h" #include +#include +#include namespace Eppo::Testing { @@ -31,6 +33,19 @@ namespace Eppo::Testing if (!s_BootAttempted) { s_BootAttempted = true; + if (const auto* renderer = std::getenv("EPPO_TEST_RENDERER")) + { + const std::string_view rendererName(renderer); + if (rendererName == "Vulkan") + params.RendererAPI = RendererAPI::Vulkan; + else if (rendererName == "DX12") + params.RendererAPI = RendererAPI::DX12; + else + { + Log::Error("Unknown test renderer '{}'. Expected Vulkan or DX12.", rendererName); + return nullptr; + } + } try { s_App = std::make_unique(std::move(params)); diff --git a/Scripts/Premake/Testing.lua b/Scripts/Premake/Testing.lua index 6552ece1..ebc400db 100644 --- a/Scripts/Premake/Testing.lua +++ b/Scripts/Premake/Testing.lua @@ -31,8 +31,15 @@ function WriteCTestFiles() for _, suite in ipairs(suites) do manifest:write(string.format('add_test(%s "%s" "--gtest_filter=%s.*")\n', suite[1], testExecutable, suite[1])) manifest:write(string.format( - 'set_tests_properties(%s PROPERTIES WORKING_DIRECTORY "%s" LABELS "%s")\n', + 'set_tests_properties(%s PROPERTIES WORKING_DIRECTORY "%s" LABELS "%s" ENVIRONMENT "EPPO_TEST_RENDERER=Vulkan")\n', suite[1], workingDirectory, suite[2])) + if os.target() == "windows" and suite[2] == "graphical" then + local dx12Suite = suite[1] .. "DX12" + manifest:write(string.format('add_test(%s "%s" "--gtest_filter=%s.*")\n', dx12Suite, testExecutable, suite[1])) + manifest:write(string.format( + 'set_tests_properties(%s PROPERTIES WORKING_DIRECTORY "%s" LABELS "graphical;dx12" ENVIRONMENT "EPPO_TEST_RENDERER=DX12")\n', + dx12Suite, workingDirectory)) + end end manifest:close() end diff --git a/vcpkg.json b/vcpkg.json index 82408475..3a0b8991 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -35,7 +35,7 @@ }, { "name": "nvrhi", - "version>=": "2026-02-26" + "version>=": "2026-06-01" }, { "name": "spdlog", From e38ea837487fddaed77c18d269339787381fb79e Mon Sep 17 00:00:00 2001 From: Niels Eppenhof Date: Thu, 3 Sep 2026 22:02:38 +0200 Subject: [PATCH 6/6] Fix DX Swapchain compile error on CI --- EppoEngine/Source/Platform/DX12/DX12Swapchain.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EppoEngine/Source/Platform/DX12/DX12Swapchain.cpp b/EppoEngine/Source/Platform/DX12/DX12Swapchain.cpp index d3eb4534..ffee88b2 100644 --- a/EppoEngine/Source/Platform/DX12/DX12Swapchain.cpp +++ b/EppoEngine/Source/Platform/DX12/DX12Swapchain.cpp @@ -113,9 +113,9 @@ namespace Eppo if (width == 0 || height == 0) { - const auto extent = GetWindowFramebufferSize(); - m_Width = extent.Width; - m_Height = extent.Height; + const auto [width, height] = GetWindowFramebufferSize(); + m_Width = width; + m_Height = height; } else {