diff --git a/engine/include/XCEngine/RHI/D3D12/D3D12DescriptorHeap.h b/engine/include/XCEngine/RHI/D3D12/D3D12DescriptorHeap.h index 16ef4c8a..6dfa8a89 100644 --- a/engine/include/XCEngine/RHI/D3D12/D3D12DescriptorHeap.h +++ b/engine/include/XCEngine/RHI/D3D12/D3D12DescriptorHeap.h @@ -33,6 +33,7 @@ public: GPUDescriptorHandle GetGPUDescriptorHandle(uint32_t index); uint32_t GetDescriptorCount() const override; DescriptorHeapType GetType() const override; + bool IsShaderVisible() const { return m_shaderVisible; } uint32_t GetDescriptorSize() const { return m_descriptorSize; } D3D12_CPU_DESCRIPTOR_HANDLE GetCPUDescriptorHandleForHeapStart() const; diff --git a/engine/include/XCEngine/RHI/D3D12/D3D12DescriptorSet.h b/engine/include/XCEngine/RHI/D3D12/D3D12DescriptorSet.h index 5a5a8e4b..8cadc62e 100644 --- a/engine/include/XCEngine/RHI/D3D12/D3D12DescriptorSet.h +++ b/engine/include/XCEngine/RHI/D3D12/D3D12DescriptorSet.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -13,6 +14,7 @@ namespace XCEngine { namespace RHI { class D3D12DescriptorHeap; +class D3D12Buffer; class D3D12DescriptorSet : public RHIDescriptorSet { public: @@ -41,6 +43,10 @@ public: uint32_t GetOffset() const { return m_offset; } uint32_t GetCount() const { return m_count; } D3D12DescriptorHeap* GetHeap() const { return m_heap; } + bool HasBindingType(DescriptorType type) const; + uint32_t GetFirstBindingOfType(DescriptorType type) const; + bool UploadConstantBuffer(); + D3D12_GPU_VIRTUAL_ADDRESS GetConstantBufferGPUAddress() const; private: D3D12DescriptorHeap* m_heap; @@ -50,7 +56,9 @@ private: DescriptorSetLayoutBinding* m_bindings; std::vector m_constantBufferData; bool m_constantBufferDirty = false; + std::unique_ptr m_constantBuffer; + uint64_t m_constantBufferCapacity = 0; }; } // namespace RHI -} // namespace XCEngine \ No newline at end of file +} // namespace XCEngine diff --git a/engine/include/XCEngine/RHI/D3D12/D3D12PipelineLayout.h b/engine/include/XCEngine/RHI/D3D12/D3D12PipelineLayout.h index a8c76f91..8301082f 100644 --- a/engine/include/XCEngine/RHI/D3D12/D3D12PipelineLayout.h +++ b/engine/include/XCEngine/RHI/D3D12/D3D12PipelineLayout.h @@ -29,12 +29,14 @@ public: uint32_t GetRootParameterIndex(uint32_t shaderRegister) const; bool HasRootParameter(uint32_t shaderRegister) const; + const RHIPipelineLayoutDesc& GetDesc() const { return m_desc; } private: bool InitializeInternal(D3D12Device* device, const RHIPipelineLayoutDesc& desc); ComPtr m_rootSignature; D3D12Device* m_device; + RHIPipelineLayoutDesc m_desc = {}; std::unordered_map m_registerToRootIndex; std::vector m_rootParameters; std::vector m_descriptorRanges; diff --git a/engine/src/RHI/D3D12/D3D12CommandList.cpp b/engine/src/RHI/D3D12/D3D12CommandList.cpp index 3e81dc71..fdffcc55 100644 --- a/engine/src/RHI/D3D12/D3D12CommandList.cpp +++ b/engine/src/RHI/D3D12/D3D12CommandList.cpp @@ -13,6 +13,20 @@ namespace XCEngine { namespace RHI { +namespace { + +bool HasDescriptorTableBindings(const D3D12DescriptorSet* descriptorSet) { + return descriptorSet != nullptr && + (descriptorSet->HasBindingType(DescriptorType::SRV) || + descriptorSet->HasBindingType(DescriptorType::UAV)); +} + +bool HasSamplerBindings(const D3D12DescriptorSet* descriptorSet) { + return descriptorSet != nullptr && descriptorSet->HasBindingType(DescriptorType::Sampler); +} + +} // namespace + D3D12CommandList::D3D12CommandList() : m_type(CommandQueueType::Direct) , m_currentTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST) @@ -164,7 +178,8 @@ void D3D12CommandList::SetGraphicsDescriptorSets( D3D12DescriptorSet* d3d12Set = static_cast(descriptorSets[i]); D3D12DescriptorHeap* heap = d3d12Set->GetHeap(); - if (heap != nullptr) { + if (heap != nullptr && heap->IsShaderVisible() && + (HasDescriptorTableBindings(d3d12Set) || HasSamplerBindings(d3d12Set))) { ID3D12DescriptorHeap* nativeHeap = heap->GetDescriptorHeap(); if (nativeHeap != nullptr && std::find(descriptorHeaps.begin(), descriptorHeaps.end(), nativeHeap) == descriptorHeaps.end()) { @@ -183,17 +198,28 @@ void D3D12CommandList::SetGraphicsDescriptorSets( } D3D12DescriptorSet* d3d12Set = static_cast(descriptorSets[i]); - D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle = d3d12Set->GetGPUHandle(); - - uint32_t rootIndex = firstSet + i; - if (d3d12Set->GetHeap() != nullptr) { - if (d3d12Set->GetHeap()->GetType() == DescriptorHeapType::CBV_SRV_UAV && d3d12Layout->HasRootParameter(100)) { - rootIndex = d3d12Layout->GetRootParameterIndex(100); - } else if (d3d12Set->GetHeap()->GetType() == DescriptorHeapType::Sampler && d3d12Layout->HasRootParameter(200)) { - rootIndex = d3d12Layout->GetRootParameterIndex(200); + if (d3d12Set->HasBindingType(DescriptorType::CBV)) { + const uint32_t cbvBinding = d3d12Set->GetFirstBindingOfType(DescriptorType::CBV); + if (cbvBinding != UINT32_MAX && + d3d12Layout->HasRootParameter(cbvBinding) && + d3d12Set->UploadConstantBuffer()) { + SetGraphicsRootConstantBufferView( + d3d12Layout->GetRootParameterIndex(cbvBinding), + d3d12Set->GetConstantBufferGPUAddress()); } } - SetGraphicsRootDescriptorTable(rootIndex, gpuHandle); + + D3D12DescriptorHeap* heap = d3d12Set->GetHeap(); + if (heap == nullptr || !heap->IsShaderVisible()) { + continue; + } + + D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle = d3d12Set->GetGPUHandle(); + if (heap->GetType() == DescriptorHeapType::CBV_SRV_UAV && HasDescriptorTableBindings(d3d12Set) && d3d12Layout->HasRootParameter(100)) { + SetGraphicsRootDescriptorTable(d3d12Layout->GetRootParameterIndex(100), gpuHandle); + } else if (heap->GetType() == DescriptorHeapType::Sampler && HasSamplerBindings(d3d12Set) && d3d12Layout->HasRootParameter(200)) { + SetGraphicsRootDescriptorTable(d3d12Layout->GetRootParameterIndex(200), gpuHandle); + } } } @@ -219,7 +245,8 @@ void D3D12CommandList::SetComputeDescriptorSets( D3D12DescriptorSet* d3d12Set = static_cast(descriptorSets[i]); D3D12DescriptorHeap* heap = d3d12Set->GetHeap(); - if (heap != nullptr) { + if (heap != nullptr && heap->IsShaderVisible() && + (HasDescriptorTableBindings(d3d12Set) || HasSamplerBindings(d3d12Set))) { ID3D12DescriptorHeap* nativeHeap = heap->GetDescriptorHeap(); if (nativeHeap != nullptr && std::find(descriptorHeaps.begin(), descriptorHeaps.end(), nativeHeap) == descriptorHeaps.end()) { @@ -238,17 +265,28 @@ void D3D12CommandList::SetComputeDescriptorSets( } D3D12DescriptorSet* d3d12Set = static_cast(descriptorSets[i]); - D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle = d3d12Set->GetGPUHandle(); - - uint32_t rootIndex = firstSet + i; - if (d3d12Set->GetHeap() != nullptr) { - if (d3d12Set->GetHeap()->GetType() == DescriptorHeapType::CBV_SRV_UAV && d3d12Layout->HasRootParameter(100)) { - rootIndex = d3d12Layout->GetRootParameterIndex(100); - } else if (d3d12Set->GetHeap()->GetType() == DescriptorHeapType::Sampler && d3d12Layout->HasRootParameter(200)) { - rootIndex = d3d12Layout->GetRootParameterIndex(200); + if (d3d12Set->HasBindingType(DescriptorType::CBV)) { + const uint32_t cbvBinding = d3d12Set->GetFirstBindingOfType(DescriptorType::CBV); + if (cbvBinding != UINT32_MAX && + d3d12Layout->HasRootParameter(cbvBinding) && + d3d12Set->UploadConstantBuffer()) { + m_commandList->SetComputeRootConstantBufferView( + d3d12Layout->GetRootParameterIndex(cbvBinding), + d3d12Set->GetConstantBufferGPUAddress()); } } - m_commandList->SetComputeRootDescriptorTable(rootIndex, gpuHandle); + + D3D12DescriptorHeap* heap = d3d12Set->GetHeap(); + if (heap == nullptr || !heap->IsShaderVisible()) { + continue; + } + + D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle = d3d12Set->GetGPUHandle(); + if (heap->GetType() == DescriptorHeapType::CBV_SRV_UAV && HasDescriptorTableBindings(d3d12Set) && d3d12Layout->HasRootParameter(100)) { + m_commandList->SetComputeRootDescriptorTable(d3d12Layout->GetRootParameterIndex(100), gpuHandle); + } else if (heap->GetType() == DescriptorHeapType::Sampler && HasSamplerBindings(d3d12Set) && d3d12Layout->HasRootParameter(200)) { + m_commandList->SetComputeRootDescriptorTable(d3d12Layout->GetRootParameterIndex(200), gpuHandle); + } } } diff --git a/engine/src/RHI/D3D12/D3D12DescriptorSet.cpp b/engine/src/RHI/D3D12/D3D12DescriptorSet.cpp index 9abcdd4a..44566e28 100644 --- a/engine/src/RHI/D3D12/D3D12DescriptorSet.cpp +++ b/engine/src/RHI/D3D12/D3D12DescriptorSet.cpp @@ -1,4 +1,5 @@ #include "XCEngine/RHI/D3D12/D3D12DescriptorSet.h" +#include "XCEngine/RHI/D3D12/D3D12Buffer.h" #include "XCEngine/RHI/D3D12/D3D12DescriptorHeap.h" #include "XCEngine/RHI/D3D12/D3D12ResourceView.h" #include "XCEngine/RHI/D3D12/D3D12Sampler.h" @@ -6,6 +7,15 @@ namespace XCEngine { namespace RHI { +namespace { + +uint64_t AlignConstantBufferSize(size_t size) { + const uint64_t minSize = size > 0 ? static_cast(size) : 1ull; + return (minSize + 255ull) & ~255ull; +} + +} // namespace + D3D12DescriptorSet::D3D12DescriptorSet() : m_heap(nullptr) , m_offset(0) @@ -39,6 +49,8 @@ void D3D12DescriptorSet::Shutdown() { m_offset = 0; m_count = 0; m_bindingCount = 0; + m_constantBuffer.reset(); + m_constantBufferCapacity = 0; if (m_bindings != nullptr) { delete[] m_bindings; m_bindings = nullptr; @@ -118,6 +130,7 @@ D3D12_GPU_DESCRIPTOR_HANDLE D3D12DescriptorSet::GetGPUHandle(uint32_t index) con } void D3D12DescriptorSet::WriteConstant(uint32_t binding, const void* data, size_t size, size_t offset) { + (void)binding; size_t requiredSize = offset + size; if (m_constantBufferData.size() < requiredSize) { m_constantBufferData.resize(requiredSize); @@ -126,5 +139,58 @@ void D3D12DescriptorSet::WriteConstant(uint32_t binding, const void* data, size_ m_constantBufferDirty = true; } +bool D3D12DescriptorSet::HasBindingType(DescriptorType type) const { + for (uint32_t i = 0; i < m_bindingCount; ++i) { + if (static_cast(m_bindings[i].type) == type) { + return true; + } + } + return false; +} + +uint32_t D3D12DescriptorSet::GetFirstBindingOfType(DescriptorType type) const { + for (uint32_t i = 0; i < m_bindingCount; ++i) { + if (static_cast(m_bindings[i].type) == type) { + return m_bindings[i].binding; + } + } + return UINT32_MAX; +} + +bool D3D12DescriptorSet::UploadConstantBuffer() { + if (!HasBindingType(DescriptorType::CBV) || m_heap == nullptr || m_heap->GetDevice() == nullptr) { + return false; + } + + const uint64_t alignedSize = AlignConstantBufferSize(m_constantBufferData.size()); + if (!m_constantBuffer || m_constantBufferCapacity < alignedSize) { + auto constantBuffer = std::make_unique(); + if (!constantBuffer->Initialize( + m_heap->GetDevice(), + alignedSize, + D3D12_RESOURCE_STATE_GENERIC_READ, + D3D12_HEAP_TYPE_UPLOAD)) { + return false; + } + + constantBuffer->SetBufferType(BufferType::Constant); + constantBuffer->SetStride(static_cast(alignedSize)); + m_constantBuffer = std::move(constantBuffer); + m_constantBufferCapacity = alignedSize; + m_constantBufferDirty = true; + } + + if (m_constantBufferDirty && !m_constantBufferData.empty()) { + m_constantBuffer->SetData(m_constantBufferData.data(), m_constantBufferData.size()); + m_constantBufferDirty = false; + } + + return m_constantBuffer != nullptr; +} + +D3D12_GPU_VIRTUAL_ADDRESS D3D12DescriptorSet::GetConstantBufferGPUAddress() const { + return m_constantBuffer ? m_constantBuffer->GetGPUVirtualAddress() : 0; +} + } // namespace RHI } // namespace XCEngine diff --git a/engine/src/RHI/D3D12/D3D12PipelineLayout.cpp b/engine/src/RHI/D3D12/D3D12PipelineLayout.cpp index 596a0492..ca2a344c 100644 --- a/engine/src/RHI/D3D12/D3D12PipelineLayout.cpp +++ b/engine/src/RHI/D3D12/D3D12PipelineLayout.cpp @@ -22,13 +22,14 @@ bool D3D12PipelineLayout::InitializeInternal(D3D12Device* device, const RHIPipel } m_device = device; + m_desc = desc; m_rootParameters.clear(); m_descriptorRanges.clear(); m_registerToRootIndex.clear(); const uint32_t rootParameterCount = - (desc.constantBufferCount > 0 ? 1u : 0u) + + desc.constantBufferCount + (desc.textureCount > 0 ? 1u : 0u) + (desc.samplerCount > 0 ? 1u : 0u); const uint32_t descriptorRangeCount = @@ -40,13 +41,10 @@ bool D3D12PipelineLayout::InitializeInternal(D3D12Device* device, const RHIPipel uint32_t rootIndex = 0; - if (desc.constantBufferCount > 0) { - D3D12_ROOT_PARAMETER param = D3D12RootSignature::Create32BitConstants( - 0, desc.constantBufferCount * 16, ShaderVisibility::All, 0); + for (uint32_t i = 0; i < desc.constantBufferCount; ++i) { + D3D12_ROOT_PARAMETER param = D3D12RootSignature::CreateCBV(i, ShaderVisibility::All, 0); m_rootParameters.push_back(param); - for (uint32_t i = 0; i < desc.constantBufferCount; ++i) { - m_registerToRootIndex[i] = rootIndex; - } + m_registerToRootIndex[i] = rootIndex; rootIndex++; } @@ -112,6 +110,7 @@ bool D3D12PipelineLayout::InitializeInternal(D3D12Device* device, const RHIPipel void D3D12PipelineLayout::Shutdown() { m_rootSignature.Reset(); + m_desc = {}; m_rootParameters.clear(); m_descriptorRanges.clear(); m_registerToRootIndex.clear(); diff --git a/engine/src/RHI/OpenGL/OpenGLDescriptorSet.cpp b/engine/src/RHI/OpenGL/OpenGLDescriptorSet.cpp index 1c2c2a77..a7a25221 100644 --- a/engine/src/RHI/OpenGL/OpenGLDescriptorSet.cpp +++ b/engine/src/RHI/OpenGL/OpenGLDescriptorSet.cpp @@ -140,10 +140,22 @@ void OpenGLDescriptorSet::Bind() { } glBindBuffer(GL_UNIFORM_BUFFER, m_constantBuffer); glBufferData(GL_UNIFORM_BUFFER, m_constantBufferData.size(), m_constantBufferData.data(), GL_DYNAMIC_DRAW); - glBindBufferBase(GL_UNIFORM_BUFFER, 0, m_constantBuffer); + glBindBuffer(GL_UNIFORM_BUFFER, 0); m_constantBufferDirty = false; } + if (m_constantBuffer != 0) { + for (const auto& binding : m_bindings) { + if (binding.type != DescriptorType::CBV) { + continue; + } + + for (uint32_t i = 0; i < binding.count; ++i) { + glBindBufferBase(GL_UNIFORM_BUFFER, binding.binding + i, m_constantBuffer); + } + } + } + for (size_t i = 0; i < m_bindings.size(); ++i) { const auto& binding = m_bindings[i]; @@ -170,6 +182,12 @@ void OpenGLDescriptorSet::Unbind() { for (size_t i = 0; i < m_bindings.size(); ++i) { const auto& binding = m_bindings[i]; + if (binding.type == DescriptorType::CBV) { + for (uint32_t j = 0; j < binding.count; ++j) { + glBindBufferBase(GL_UNIFORM_BUFFER, binding.binding + j, 0); + } + } + for (size_t j = 0; j < binding.textureUnits.size(); ++j) { uint32_t unit = binding.textureUnits[j]; diff --git a/tests/RHI/integration/CMakeLists.txt b/tests/RHI/integration/CMakeLists.txt index 14bba7de..6d5ffa6f 100644 --- a/tests/RHI/integration/CMakeLists.txt +++ b/tests/RHI/integration/CMakeLists.txt @@ -7,3 +7,4 @@ enable_testing() add_subdirectory(minimal) add_subdirectory(triangle) add_subdirectory(quad) +add_subdirectory(sphere) diff --git a/tests/RHI/integration/fixtures/RHIIntegrationFixture.cpp b/tests/RHI/integration/fixtures/RHIIntegrationFixture.cpp index e2a8fd3a..c77b4b88 100644 --- a/tests/RHI/integration/fixtures/RHIIntegrationFixture.cpp +++ b/tests/RHI/integration/fixtures/RHIIntegrationFixture.cpp @@ -156,7 +156,7 @@ void RHIIntegrationFixture::BeginRender() { Log("[TEST] BeginRender: backBufferIndex=%d", mCurrentBackBufferIndex); } -void RHIIntegrationFixture::SetRenderTargetForClear() { +void RHIIntegrationFixture::SetRenderTargetForClear(bool includeDepthStencil) { if (GetParam() == RHIType::D3D12) { Log("[TEST] SetRenderTargetForClear: D3D12 branch, mRTVs.size=%d, index=%d", (int)mRTVs.size(), mCurrentBackBufferIndex); @@ -167,7 +167,7 @@ void RHIIntegrationFixture::SetRenderTargetForClear() { d3d12CmdList->TransitionBarrier(backBuffer->GetResource(), ResourceStates::Present, ResourceStates::RenderTarget); RHIResourceView* rtv = mRTVs[mCurrentBackBufferIndex]; Log("[TEST] SetRenderTargetForClear: calling SetRenderTargets, rtv=%p", (void*)rtv); - mCommandList->SetRenderTargets(1, &rtv, nullptr); + mCommandList->SetRenderTargets(1, &rtv, includeDepthStencil ? mDSV : nullptr); Log("[TEST] SetRenderTargetForClear: done"); } else { Log("[TEST] SetRenderTargetForClear: skipped - condition failed"); diff --git a/tests/RHI/integration/fixtures/RHIIntegrationFixture.h b/tests/RHI/integration/fixtures/RHIIntegrationFixture.h index 5ee8a9fc..7b91a5d7 100644 --- a/tests/RHI/integration/fixtures/RHIIntegrationFixture.h +++ b/tests/RHI/integration/fixtures/RHIIntegrationFixture.h @@ -47,7 +47,7 @@ protected: HWND GetWindowHandle() const { return mWindow; } int GetCurrentBackBufferIndex() const { return mCurrentBackBufferIndex; } RHITexture* GetCurrentBackBuffer() { return mSwapChain ? mSwapChain->GetCurrentBackBuffer() : nullptr; } - void SetRenderTargetForClear(); + void SetRenderTargetForClear(bool includeDepthStencil = false); virtual void RenderFrame() {} diff --git a/tests/RHI/integration/sphere/CMakeLists.txt b/tests/RHI/integration/sphere/CMakeLists.txt new file mode 100644 index 00000000..6eb78628 --- /dev/null +++ b/tests/RHI/integration/sphere/CMakeLists.txt @@ -0,0 +1,59 @@ +cmake_minimum_required(VERSION 3.15) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + +project(rhi_integration_sphere) + +set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine) +set(PACKAGE_DIR ${CMAKE_SOURCE_DIR}/tests/opengl/package) + +get_filename_component(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../.. ABSOLUTE) + +add_executable(rhi_integration_sphere + main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../fixtures/RHIIntegrationFixture.cpp + ${PACKAGE_DIR}/src/glad.c +) + +target_include_directories(rhi_integration_sphere PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../fixtures + ${ENGINE_ROOT_DIR}/include + ${PACKAGE_DIR}/include + ${PROJECT_ROOT_DIR}/engine/src +) + +target_link_libraries(rhi_integration_sphere PRIVATE + d3d12 + dxgi + d3dcompiler + winmm + opengl32 + XCEngine + GTest::gtest +) + +target_compile_definitions(rhi_integration_sphere PRIVATE + UNICODE + _UNICODE + XCENGINE_SUPPORT_OPENGL + XCENGINE_SUPPORT_D3D12 +) + +add_custom_command(TARGET rhi_integration_sphere POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/Res + $/Res + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/tests/RHI/integration/compare_ppm.py + $/ + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm + $/GT.ppm + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll + $/ +) + +include(GoogleTest) +gtest_discover_tests(rhi_integration_sphere) diff --git a/tests/RHI/integration/sphere/GT.ppm b/tests/RHI/integration/sphere/GT.ppm new file mode 100644 index 00000000..37a204fe Binary files /dev/null and b/tests/RHI/integration/sphere/GT.ppm differ diff --git a/tests/RHI/integration/sphere/Res/Image/earth.png b/tests/RHI/integration/sphere/Res/Image/earth.png new file mode 100644 index 00000000..663081b5 Binary files /dev/null and b/tests/RHI/integration/sphere/Res/Image/earth.png differ diff --git a/tests/RHI/integration/sphere/main.cpp b/tests/RHI/integration/sphere/main.cpp new file mode 100644 index 00000000..dd39f3d0 --- /dev/null +++ b/tests/RHI/integration/sphere/main.cpp @@ -0,0 +1,645 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../fixtures/RHIIntegrationFixture.h" +#include "XCEngine/Core/Math/Matrix4.h" +#include "XCEngine/Core/Math/Vector3.h" +#include "XCEngine/Debug/ConsoleLogSink.h" +#include "XCEngine/Debug/Logger.h" +#include "XCEngine/RHI/RHIBuffer.h" +#include "XCEngine/RHI/RHIDescriptorPool.h" +#include "XCEngine/RHI/RHIDescriptorSet.h" +#include "XCEngine/RHI/RHIPipelineLayout.h" +#include "XCEngine/RHI/RHIPipelineState.h" +#include "XCEngine/RHI/RHIResourceView.h" +#include "XCEngine/RHI/RHISampler.h" +#include "XCEngine/RHI/RHITexture.h" + +using namespace XCEngine::Debug; +using namespace XCEngine::Math; +using namespace XCEngine::RHI; +using namespace XCEngine::RHI::Integration; + +namespace { + +struct Vertex { + float pos[4]; + float uv[2]; +}; + +struct MatrixBufferData { + Matrix4x4 projection; + Matrix4x4 view; + Matrix4x4 model; +}; + +constexpr float kSphereRadius = 1.0f; +constexpr int kSphereSegments = 32; +constexpr float kPi = 3.14159265358979323846f; + +std::filesystem::path GetExecutableDirectory() { + char exePath[MAX_PATH] = {}; + const DWORD length = GetModuleFileNameA(nullptr, exePath, MAX_PATH); + if (length == 0 || length >= MAX_PATH) { + return std::filesystem::current_path(); + } + + return std::filesystem::path(exePath).parent_path(); +} + +std::filesystem::path ResolveRuntimePath(const char* relativePath) { + return GetExecutableDirectory() / relativePath; +} + +void GenerateSphere(std::vector& vertices, std::vector& indices, float radius, int segments) { + vertices.clear(); + indices.clear(); + + segments = segments < 3 ? 3 : segments; + + for (int lat = 0; lat <= segments; ++lat) { + const float phi = kPi * static_cast(lat) / static_cast(segments); + const float sinPhi = sinf(phi); + const float cosPhi = cosf(phi); + + for (int lon = 0; lon <= segments; ++lon) { + const float theta = (kPi * 2.0f) * static_cast(lon) / static_cast(segments); + const float sinTheta = sinf(theta); + const float cosTheta = cosf(theta); + + Vertex vertex = {}; + vertex.pos[0] = radius * sinPhi * cosTheta; + vertex.pos[1] = radius * cosPhi; + vertex.pos[2] = radius * sinPhi * sinTheta; + vertex.pos[3] = 1.0f; + + vertex.uv[0] = static_cast(lon) / static_cast(segments); + vertex.uv[1] = static_cast(lat) / static_cast(segments); + + vertices.push_back(vertex); + } + } + + for (int lat = 0; lat < segments; ++lat) { + for (int lon = 0; lon < segments; ++lon) { + const uint32_t topLeft = static_cast(lat * (segments + 1) + lon); + const uint32_t topRight = topLeft + 1; + const uint32_t bottomLeft = static_cast((lat + 1) * (segments + 1) + lon); + const uint32_t bottomRight = bottomLeft + 1; + + indices.push_back(topLeft); + indices.push_back(bottomLeft); + indices.push_back(topRight); + + indices.push_back(topRight); + indices.push_back(bottomLeft); + indices.push_back(bottomRight); + } + } +} + +MatrixBufferData CreateMatrixBufferData() { + const float aspect = 1280.0f / 720.0f; + const Matrix4x4 projection = Matrix4x4::Perspective(45.0f * 3.141592f / 180.0f, aspect, 0.1f, 1000.0f); + const Matrix4x4 view = Matrix4x4::Identity(); + const Matrix4x4 model = Matrix4x4::Translation(Vector3(0.0f, 0.0f, 5.0f)); + + MatrixBufferData data = {}; + data.projection = projection.Transpose(); + data.view = view.Transpose(); + data.model = model.Transpose(); + return data; +} + +const char kSphereHlsl[] = R"( +Texture2D gDiffuseTexture : register(t0); +SamplerState gSampler : register(s0); + +cbuffer MatrixBuffer : register(b0) { + float4x4 gProjectionMatrix; + float4x4 gViewMatrix; + float4x4 gModelMatrix; +}; + +struct VSInput { + float4 position : POSITION; + float2 texcoord : TEXCOORD0; +}; + +struct PSInput { + float4 position : SV_POSITION; + float2 texcoord : TEXCOORD0; +}; + +PSInput MainVS(VSInput input) { + PSInput output; + float4 positionWS = mul(gModelMatrix, input.position); + float4 positionVS = mul(gViewMatrix, positionWS); + output.position = mul(gProjectionMatrix, positionVS); + output.texcoord = input.texcoord; + return output; +} + +float4 MainPS(PSInput input) : SV_TARGET { + return gDiffuseTexture.Sample(gSampler, input.texcoord); +} +)"; + +const char kSphereVertexShader[] = R"(#version 430 +layout(location = 0) in vec4 aPosition; +layout(location = 1) in vec2 aTexCoord; + +layout(std140, binding = 0) uniform MatrixBuffer { + mat4 gProjectionMatrix; + mat4 gViewMatrix; + mat4 gModelMatrix; +}; + +out vec2 vTexCoord; + +void main() { + vec4 positionWS = gModelMatrix * aPosition; + vec4 positionVS = gViewMatrix * positionWS; + gl_Position = gProjectionMatrix * positionVS; + vTexCoord = aTexCoord; +} +)"; + +const char kSphereFragmentShader[] = R"(#version 430 +layout(binding = 0) uniform sampler2D uTexture; + +in vec2 vTexCoord; + +layout(location = 0) out vec4 fragColor; + +void main() { + fragColor = texture(uTexture, vTexCoord); +} +)"; + +const char* GetScreenshotFilename(RHIType type) { + return type == RHIType::D3D12 ? "sphere_d3d12.ppm" : "sphere_opengl.ppm"; +} + +int GetComparisonThreshold(RHIType type) { + return type == RHIType::OpenGL ? 5 : 0; +} + +RHITexture* LoadSphereTexture(RHIDevice* device) { + const std::filesystem::path texturePath = ResolveRuntimePath("Res/Image/earth.png"); + const std::string texturePathString = texturePath.string(); + + stbi_set_flip_vertically_on_load(0); + + int width = 0; + int height = 0; + int channels = 0; + stbi_uc* pixels = stbi_load(texturePathString.c_str(), &width, &height, &channels, STBI_rgb_alpha); + if (pixels == nullptr) { + Log("[TEST] Failed to load sphere texture: %s", texturePathString.c_str()); + return nullptr; + } + + TextureDesc textureDesc = {}; + textureDesc.width = static_cast(width); + textureDesc.height = static_cast(height); + textureDesc.depth = 1; + textureDesc.mipLevels = 1; + textureDesc.arraySize = 1; + textureDesc.format = static_cast(Format::R8G8B8A8_UNorm); + textureDesc.textureType = static_cast(TextureType::Texture2D); + textureDesc.sampleCount = 1; + textureDesc.sampleQuality = 0; + textureDesc.flags = 0; + + RHITexture* texture = device->CreateTexture( + textureDesc, + pixels, + static_cast(width) * static_cast(height) * 4, + static_cast(width) * 4); + stbi_image_free(pixels); + return texture; +} + +GraphicsPipelineDesc CreateSpherePipelineDesc(RHIType type, RHIPipelineLayout* pipelineLayout) { + GraphicsPipelineDesc desc = {}; + desc.pipelineLayout = pipelineLayout; + desc.topologyType = static_cast(PrimitiveTopologyType::Triangle); + desc.renderTargetFormats[0] = static_cast(Format::R8G8B8A8_UNorm); + desc.depthStencilFormat = static_cast(Format::D24_UNorm_S8_UInt); + desc.sampleCount = 1; + + desc.rasterizerState.fillMode = static_cast(FillMode::Solid); + desc.rasterizerState.cullMode = static_cast(CullMode::None); + desc.rasterizerState.frontFace = static_cast(FrontFace::CounterClockwise); + desc.rasterizerState.depthClipEnable = true; + + desc.depthStencilState.depthTestEnable = true; + desc.depthStencilState.depthWriteEnable = true; + desc.depthStencilState.depthFunc = static_cast(ComparisonFunc::Less); + desc.depthStencilState.stencilEnable = false; + + InputElementDesc position = {}; + position.semanticName = "POSITION"; + position.semanticIndex = 0; + position.format = static_cast(Format::R32G32B32A32_Float); + position.inputSlot = 0; + position.alignedByteOffset = 0; + desc.inputLayout.elements.push_back(position); + + InputElementDesc texcoord = {}; + texcoord.semanticName = "TEXCOORD"; + texcoord.semanticIndex = 0; + texcoord.format = static_cast(Format::R32G32_Float); + texcoord.inputSlot = 0; + texcoord.alignedByteOffset = sizeof(float) * 4; + desc.inputLayout.elements.push_back(texcoord); + + if (type == RHIType::D3D12) { + desc.vertexShader.source.assign(kSphereHlsl, kSphereHlsl + strlen(kSphereHlsl)); + desc.vertexShader.sourceLanguage = ShaderLanguage::HLSL; + desc.vertexShader.entryPoint = L"MainVS"; + desc.vertexShader.profile = L"vs_5_0"; + + desc.fragmentShader.source.assign(kSphereHlsl, kSphereHlsl + strlen(kSphereHlsl)); + desc.fragmentShader.sourceLanguage = ShaderLanguage::HLSL; + desc.fragmentShader.entryPoint = L"MainPS"; + desc.fragmentShader.profile = L"ps_5_0"; + } else { + desc.vertexShader.source.assign(kSphereVertexShader, kSphereVertexShader + strlen(kSphereVertexShader)); + desc.vertexShader.sourceLanguage = ShaderLanguage::GLSL; + desc.vertexShader.profile = L"vs_4_30"; + + desc.fragmentShader.source.assign(kSphereFragmentShader, kSphereFragmentShader + strlen(kSphereFragmentShader)); + desc.fragmentShader.sourceLanguage = ShaderLanguage::GLSL; + desc.fragmentShader.profile = L"fs_4_30"; + } + + return desc; +} + +class SphereTest : public RHIIntegrationFixture { +protected: + void SetUp() override; + void TearDown() override; + void RenderFrame() override; + +private: + void InitializeSphereResources(); + void ShutdownSphereResources(); + + std::vector mVertices; + std::vector mIndices; + RHIBuffer* mVertexBuffer = nullptr; + RHIResourceView* mVertexBufferView = nullptr; + RHIBuffer* mIndexBuffer = nullptr; + RHIResourceView* mIndexBufferView = nullptr; + RHITexture* mTexture = nullptr; + RHIResourceView* mTextureView = nullptr; + RHISampler* mSampler = nullptr; + RHIDescriptorPool* mConstantPool = nullptr; + RHIDescriptorSet* mConstantSet = nullptr; + RHIDescriptorPool* mTexturePool = nullptr; + RHIDescriptorSet* mTextureSet = nullptr; + RHIDescriptorPool* mSamplerPool = nullptr; + RHIDescriptorSet* mSamplerSet = nullptr; + RHIPipelineLayout* mPipelineLayout = nullptr; + RHIPipelineState* mPipelineState = nullptr; +}; + +void SphereTest::SetUp() { + RHIIntegrationFixture::SetUp(); + InitializeSphereResources(); +} + +void SphereTest::TearDown() { + ShutdownSphereResources(); + RHIIntegrationFixture::TearDown(); +} + +void SphereTest::InitializeSphereResources() { + GenerateSphere(mVertices, mIndices, kSphereRadius, kSphereSegments); + ASSERT_FALSE(mVertices.empty()); + ASSERT_FALSE(mIndices.empty()); + + BufferDesc vertexBufferDesc = {}; + vertexBufferDesc.size = static_cast(mVertices.size() * sizeof(Vertex)); + vertexBufferDesc.stride = sizeof(Vertex); + vertexBufferDesc.bufferType = static_cast(BufferType::Vertex); + + mVertexBuffer = GetDevice()->CreateBuffer(vertexBufferDesc); + ASSERT_NE(mVertexBuffer, nullptr); + mVertexBuffer->SetData(mVertices.data(), mVertices.size() * sizeof(Vertex)); + mVertexBuffer->SetStride(sizeof(Vertex)); + mVertexBuffer->SetBufferType(BufferType::Vertex); + + ResourceViewDesc vertexViewDesc = {}; + vertexViewDesc.dimension = ResourceViewDimension::Buffer; + vertexViewDesc.structureByteStride = sizeof(Vertex); + mVertexBufferView = GetDevice()->CreateVertexBufferView(mVertexBuffer, vertexViewDesc); + ASSERT_NE(mVertexBufferView, nullptr); + + BufferDesc indexBufferDesc = {}; + indexBufferDesc.size = static_cast(mIndices.size() * sizeof(uint32_t)); + indexBufferDesc.stride = sizeof(uint32_t); + indexBufferDesc.bufferType = static_cast(BufferType::Index); + + mIndexBuffer = GetDevice()->CreateBuffer(indexBufferDesc); + ASSERT_NE(mIndexBuffer, nullptr); + mIndexBuffer->SetData(mIndices.data(), mIndices.size() * sizeof(uint32_t)); + mIndexBuffer->SetStride(sizeof(uint32_t)); + mIndexBuffer->SetBufferType(BufferType::Index); + + ResourceViewDesc indexViewDesc = {}; + indexViewDesc.dimension = ResourceViewDimension::Buffer; + indexViewDesc.format = static_cast(Format::R32_UInt); + mIndexBufferView = GetDevice()->CreateIndexBufferView(mIndexBuffer, indexViewDesc); + ASSERT_NE(mIndexBufferView, nullptr); + + mTexture = LoadSphereTexture(GetDevice()); + ASSERT_NE(mTexture, nullptr); + + ResourceViewDesc textureViewDesc = {}; + textureViewDesc.format = static_cast(Format::R8G8B8A8_UNorm); + textureViewDesc.dimension = ResourceViewDimension::Texture2D; + textureViewDesc.mipLevel = 0; + mTextureView = GetDevice()->CreateShaderResourceView(mTexture, textureViewDesc); + ASSERT_NE(mTextureView, nullptr); + + SamplerDesc samplerDesc = {}; + samplerDesc.filter = static_cast(FilterMode::Linear); + samplerDesc.addressU = static_cast(TextureAddressMode::Clamp); + samplerDesc.addressV = static_cast(TextureAddressMode::Clamp); + samplerDesc.addressW = static_cast(TextureAddressMode::Clamp); + samplerDesc.mipLodBias = 0.0f; + samplerDesc.maxAnisotropy = 1; + samplerDesc.comparisonFunc = static_cast(ComparisonFunc::Always); + samplerDesc.borderColorR = 0.0f; + samplerDesc.borderColorG = 0.0f; + samplerDesc.borderColorB = 0.0f; + samplerDesc.borderColorA = 0.0f; + samplerDesc.minLod = 0.0f; + samplerDesc.maxLod = 1000.0f; + mSampler = GetDevice()->CreateSampler(samplerDesc); + ASSERT_NE(mSampler, nullptr); + + DescriptorPoolDesc constantPoolDesc = {}; + constantPoolDesc.type = DescriptorHeapType::CBV_SRV_UAV; + constantPoolDesc.descriptorCount = 1; + constantPoolDesc.shaderVisible = false; + mConstantPool = GetDevice()->CreateDescriptorPool(constantPoolDesc); + ASSERT_NE(mConstantPool, nullptr); + + DescriptorSetLayoutBinding constantBinding = {}; + constantBinding.binding = 0; + constantBinding.type = static_cast(DescriptorType::CBV); + constantBinding.count = 1; + + DescriptorSetLayoutDesc constantLayoutDesc = {}; + constantLayoutDesc.bindings = &constantBinding; + constantLayoutDesc.bindingCount = 1; + + mConstantSet = mConstantPool->AllocateSet(constantLayoutDesc); + ASSERT_NE(mConstantSet, nullptr); + const MatrixBufferData matrixData = CreateMatrixBufferData(); + mConstantSet->WriteConstant(0, &matrixData, sizeof(matrixData)); + + DescriptorPoolDesc texturePoolDesc = {}; + texturePoolDesc.type = DescriptorHeapType::CBV_SRV_UAV; + texturePoolDesc.descriptorCount = 1; + texturePoolDesc.shaderVisible = true; + mTexturePool = GetDevice()->CreateDescriptorPool(texturePoolDesc); + ASSERT_NE(mTexturePool, nullptr); + + DescriptorSetLayoutBinding textureBinding = {}; + textureBinding.binding = 0; + textureBinding.type = static_cast(DescriptorType::SRV); + textureBinding.count = 1; + + DescriptorSetLayoutDesc textureLayoutDesc = {}; + textureLayoutDesc.bindings = &textureBinding; + textureLayoutDesc.bindingCount = 1; + + mTextureSet = mTexturePool->AllocateSet(textureLayoutDesc); + ASSERT_NE(mTextureSet, nullptr); + mTextureSet->Update(0, mTextureView); + + DescriptorPoolDesc samplerPoolDesc = {}; + samplerPoolDesc.type = DescriptorHeapType::Sampler; + samplerPoolDesc.descriptorCount = 1; + samplerPoolDesc.shaderVisible = true; + mSamplerPool = GetDevice()->CreateDescriptorPool(samplerPoolDesc); + ASSERT_NE(mSamplerPool, nullptr); + + DescriptorSetLayoutBinding samplerBinding = {}; + samplerBinding.binding = 0; + samplerBinding.type = static_cast(DescriptorType::Sampler); + samplerBinding.count = 1; + + DescriptorSetLayoutDesc samplerLayoutDesc = {}; + samplerLayoutDesc.bindings = &samplerBinding; + samplerLayoutDesc.bindingCount = 1; + + mSamplerSet = mSamplerPool->AllocateSet(samplerLayoutDesc); + ASSERT_NE(mSamplerSet, nullptr); + mSamplerSet->UpdateSampler(0, mSampler); + + RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.constantBufferCount = 1; + pipelineLayoutDesc.textureCount = 1; + pipelineLayoutDesc.samplerCount = 1; + mPipelineLayout = GetDevice()->CreatePipelineLayout(pipelineLayoutDesc); + ASSERT_NE(mPipelineLayout, nullptr); + + GraphicsPipelineDesc pipelineDesc = CreateSpherePipelineDesc(GetBackendType(), mPipelineLayout); + mPipelineState = GetDevice()->CreatePipelineState(pipelineDesc); + ASSERT_NE(mPipelineState, nullptr); + ASSERT_TRUE(mPipelineState->IsValid()); + + Log("[TEST] Sphere resources initialized for backend %d", static_cast(GetBackendType())); +} + +void SphereTest::ShutdownSphereResources() { + if (mPipelineState != nullptr) { + mPipelineState->Shutdown(); + delete mPipelineState; + mPipelineState = nullptr; + } + + if (mPipelineLayout != nullptr) { + mPipelineLayout->Shutdown(); + delete mPipelineLayout; + mPipelineLayout = nullptr; + } + + if (mConstantSet != nullptr) { + mConstantSet->Shutdown(); + delete mConstantSet; + mConstantSet = nullptr; + } + + if (mTextureSet != nullptr) { + mTextureSet->Shutdown(); + delete mTextureSet; + mTextureSet = nullptr; + } + + if (mSamplerSet != nullptr) { + mSamplerSet->Shutdown(); + delete mSamplerSet; + mSamplerSet = nullptr; + } + + if (mConstantPool != nullptr) { + mConstantPool->Shutdown(); + delete mConstantPool; + mConstantPool = nullptr; + } + + if (mTexturePool != nullptr) { + mTexturePool->Shutdown(); + delete mTexturePool; + mTexturePool = nullptr; + } + + if (mSamplerPool != nullptr) { + mSamplerPool->Shutdown(); + delete mSamplerPool; + mSamplerPool = nullptr; + } + + if (mSampler != nullptr) { + mSampler->Shutdown(); + delete mSampler; + mSampler = nullptr; + } + + if (mTextureView != nullptr) { + mTextureView->Shutdown(); + delete mTextureView; + mTextureView = nullptr; + } + + if (mTexture != nullptr) { + mTexture->Shutdown(); + delete mTexture; + mTexture = nullptr; + } + + if (mVertexBufferView != nullptr) { + mVertexBufferView->Shutdown(); + delete mVertexBufferView; + mVertexBufferView = nullptr; + } + + if (mIndexBufferView != nullptr) { + mIndexBufferView->Shutdown(); + delete mIndexBufferView; + mIndexBufferView = nullptr; + } + + if (mVertexBuffer != nullptr) { + mVertexBuffer->Shutdown(); + delete mVertexBuffer; + mVertexBuffer = nullptr; + } + + if (mIndexBuffer != nullptr) { + mIndexBuffer->Shutdown(); + delete mIndexBuffer; + mIndexBuffer = nullptr; + } +} + +void SphereTest::RenderFrame() { + ASSERT_NE(mPipelineState, nullptr); + ASSERT_NE(mPipelineLayout, nullptr); + ASSERT_NE(mConstantSet, nullptr); + ASSERT_NE(mTextureSet, nullptr); + ASSERT_NE(mSamplerSet, nullptr); + ASSERT_NE(mVertexBufferView, nullptr); + ASSERT_NE(mIndexBufferView, nullptr); + + RHICommandList* cmdList = GetCommandList(); + RHICommandQueue* cmdQueue = GetCommandQueue(); + ASSERT_NE(cmdList, nullptr); + ASSERT_NE(cmdQueue, nullptr); + + cmdList->Reset(); + SetRenderTargetForClear(true); + + Viewport viewport = { 0.0f, 0.0f, 1280.0f, 720.0f, 0.0f, 1.0f }; + Rect scissorRect = { 0, 0, 1280, 720 }; + cmdList->SetViewport(viewport); + cmdList->SetScissorRect(scissorRect); + cmdList->Clear(0.0f, 0.0f, 1.0f, 1.0f, 1 | 2); + + cmdList->SetPipelineState(mPipelineState); + RHIDescriptorSet* descriptorSets[] = { mConstantSet, mTextureSet, mSamplerSet }; + cmdList->SetGraphicsDescriptorSets(0, 3, descriptorSets, mPipelineLayout); + cmdList->SetPrimitiveTopology(PrimitiveTopology::TriangleList); + + RHIResourceView* vertexBuffers[] = { mVertexBufferView }; + uint64_t offsets[] = { 0 }; + uint32_t strides[] = { sizeof(Vertex) }; + cmdList->SetVertexBuffers(0, 1, vertexBuffers, offsets, strides); + cmdList->SetIndexBuffer(mIndexBufferView, 0); + cmdList->DrawIndexed(static_cast(mIndices.size())); + + EndRender(); + + cmdList->Close(); + void* cmdLists[] = { cmdList }; + cmdQueue->ExecuteCommandLists(1, cmdLists); +} + +TEST_P(SphereTest, RenderSphere) { + RHICommandQueue* cmdQueue = GetCommandQueue(); + RHISwapChain* swapChain = GetSwapChain(); + const int targetFrameCount = 30; + const char* screenshotFilename = GetScreenshotFilename(GetBackendType()); + const int comparisonThreshold = GetComparisonThreshold(GetBackendType()); + + for (int frameCount = 0; frameCount <= targetFrameCount; ++frameCount) { + if (frameCount > 0) { + cmdQueue->WaitForPreviousFrame(); + } + + Log("[TEST] Sphere MainLoop: frame %d", frameCount); + BeginRender(); + RenderFrame(); + + if (frameCount >= targetFrameCount) { + cmdQueue->WaitForIdle(); + Log("[TEST] Sphere MainLoop: frame %d reached, capturing %s", frameCount, screenshotFilename); + ASSERT_TRUE(TakeScreenshot(screenshotFilename)); + ASSERT_TRUE(CompareWithGoldenTemplate(screenshotFilename, "GT.ppm", static_cast(comparisonThreshold))); + Log("[TEST] Sphere MainLoop: frame %d compare passed", frameCount); + break; + } + + swapChain->Present(0, 0); + } +} + +} // namespace + +INSTANTIATE_TEST_SUITE_P(D3D12, SphereTest, ::testing::Values(RHIType::D3D12)); +INSTANTIATE_TEST_SUITE_P(OpenGL, SphereTest, ::testing::Values(RHIType::OpenGL)); + +GTEST_API_ int main(int argc, char** argv) { + Logger::Get().Initialize(); + Logger::Get().AddSink(std::make_unique()); + Logger::Get().SetMinimumLevel(LogLevel::Debug); + + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/RHI/unit/test_descriptor_set.cpp b/tests/RHI/unit/test_descriptor_set.cpp index 9b91e13a..a483e884 100644 --- a/tests/RHI/unit/test_descriptor_set.cpp +++ b/tests/RHI/unit/test_descriptor_set.cpp @@ -493,3 +493,57 @@ TEST_P(RHITestFixture, DescriptorSet_Update_UsesBindingNumberOnOpenGL) { pool->Shutdown(); delete pool; } + +TEST_P(RHITestFixture, DescriptorSet_BindConstantBuffer_UsesBindingNumberOnOpenGL) { + if (GetBackendType() != RHIType::OpenGL) { + GTEST_SKIP() << "OpenGL-specific constant buffer binding verification"; + } + + auto* openGLDevice = static_cast(GetDevice()); + ASSERT_NE(openGLDevice, nullptr); + ASSERT_TRUE(openGLDevice->MakeContextCurrent()); + + DescriptorPoolDesc poolDesc = {}; + poolDesc.type = DescriptorHeapType::CBV_SRV_UAV; + poolDesc.descriptorCount = 1; + poolDesc.shaderVisible = false; + + RHIDescriptorPool* pool = GetDevice()->CreateDescriptorPool(poolDesc); + ASSERT_NE(pool, nullptr); + + DescriptorSetLayoutBinding binding = {}; + binding.binding = 3; + binding.type = static_cast(DescriptorType::CBV); + binding.count = 1; + + DescriptorSetLayoutDesc layoutDesc = {}; + layoutDesc.bindings = &binding; + layoutDesc.bindingCount = 1; + + RHIDescriptorSet* set = pool->AllocateSet(layoutDesc); + ASSERT_NE(set, nullptr); + + const float matrixData[16] = { + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f, + }; + set->WriteConstant(3, matrixData, sizeof(matrixData)); + set->Bind(); + + GLint boundBuffer = 0; + glGetIntegeri_v(GL_UNIFORM_BUFFER_BINDING, 3, &boundBuffer); + EXPECT_NE(boundBuffer, 0); + + set->Unbind(); + + GLint unboundBuffer = -1; + glGetIntegeri_v(GL_UNIFORM_BUFFER_BINDING, 3, &unboundBuffer); + EXPECT_EQ(unboundBuffer, 0); + + set->Shutdown(); + delete set; + pool->Shutdown(); + delete pool; +} diff --git a/tests/RHI/unit/test_pipeline_layout.cpp b/tests/RHI/unit/test_pipeline_layout.cpp index b474504d..0f71fb8f 100644 --- a/tests/RHI/unit/test_pipeline_layout.cpp +++ b/tests/RHI/unit/test_pipeline_layout.cpp @@ -1,4 +1,5 @@ #include "fixtures/RHITestFixture.h" +#include "XCEngine/RHI/D3D12/D3D12PipelineLayout.h" #include "XCEngine/RHI/RHIPipelineLayout.h" #include "XCEngine/RHI/RHIDescriptorSet.h" @@ -151,4 +152,28 @@ TEST_P(RHITestFixture, PipelineLayout_DescriptorSetAllocation) { layout->Shutdown(); delete layout; -} \ No newline at end of file +} + +TEST_P(RHITestFixture, PipelineLayout_D3D12ConstantBuffers_MapToDistinctRootParameters) { + if (GetBackendType() != RHIType::D3D12) { + GTEST_SKIP() << "D3D12-specific root parameter verification"; + } + + RHIPipelineLayoutDesc desc = {}; + desc.constantBufferCount = 2; + desc.textureCount = 1; + desc.samplerCount = 1; + + RHIPipelineLayout* layout = GetDevice()->CreatePipelineLayout(desc); + ASSERT_NE(layout, nullptr); + + auto* d3d12Layout = static_cast(layout); + EXPECT_TRUE(d3d12Layout->HasRootParameter(0)); + EXPECT_TRUE(d3d12Layout->HasRootParameter(1)); + EXPECT_TRUE(d3d12Layout->HasRootParameter(100)); + EXPECT_TRUE(d3d12Layout->HasRootParameter(200)); + EXPECT_NE(d3d12Layout->GetRootParameterIndex(0), d3d12Layout->GetRootParameterIndex(1)); + + layout->Shutdown(); + delete layout; +} diff --git a/tests/TEST_SPEC.md b/tests/TEST_SPEC.md index ad71fc40..3dca6efd 100644 --- a/tests/TEST_SPEC.md +++ b/tests/TEST_SPEC.md @@ -1,6 +1,6 @@ # XCEngine 测试规范 -最后更新:2026-03-25 +最后更新:2026-03-26 ## 1. 目标 @@ -66,6 +66,7 @@ cmake --build build --config Debug --target rhi_unit_tests cmake --build build --config Debug --target rhi_integration_minimal cmake --build build --config Debug --target rhi_integration_triangle cmake --build build --config Debug --target rhi_integration_quad +cmake --build build --config Debug --target rhi_integration_sphere ``` ### 3.3 运行 @@ -116,6 +117,7 @@ RHI 当前分成四类测试: | 抽象层集成测试 | `rhi_integration_minimal` | | 抽象层集成测试 | `rhi_integration_triangle` | | 抽象层集成测试 | `rhi_integration_quad` | +| 抽象层集成测试 | `rhi_integration_sphere` | | D3D12 后端单元测试 | `rhi_d3d12_tests` | | OpenGL 后端单元测试 | `rhi_opengl_tests` | | D3D12 后端集成测试 | `d3d12_minimal_test` `d3d12_triangle_test` `d3d12_quad_test` `d3d12_sphere_test` | @@ -123,7 +125,7 @@ RHI 当前分成四类测试: 说明: -- 抽象层集成测试目前正式包含 `minimal`、`triangle` 与 `quad`。 +- 抽象层集成测试目前正式包含 `minimal`、`triangle`、`quad` 与 `sphere`。 ## 6. RHI 抽象层集成测试规范 @@ -146,6 +148,10 @@ tests/RHI/integration/ │ ├─ CMakeLists.txt │ ├─ GT.ppm │ └─ main.cpp +├─ sphere/ +│ ├─ CMakeLists.txt +│ ├─ GT.ppm +│ └─ main.cpp ├─ compare_ppm.py └─ CMakeLists.txt ``` @@ -164,6 +170,8 @@ tests/RHI/integration/ - `triangle_opengl.ppm` - `quad_d3d12.ppm` - `quad_opengl.ppm` + - `sphere_d3d12.ppm` + - `sphere_opengl.ppm` 5. 两个后端都必须与同一张 `GT.ppm` 做比对。 6. 新测试如果暴露抽象层缺口,应先补 RHI,再补测试。