From 6927b4b380dca5fe4de5916d030f9dd8653fbeca Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Wed, 1 Apr 2026 16:44:11 +0800 Subject: [PATCH] feat: add gpu object id scene picking --- editor/src/Viewport/ViewportHostService.h | 320 ++++++++++++- engine/CMakeLists.txt | 4 + .../XCEngine/Rendering/CameraRenderRequest.h | 18 + .../XCEngine/Rendering/CameraRenderer.h | 7 + .../XCEngine/Rendering/ObjectIdEncoding.h | 32 ++ .../include/XCEngine/Rendering/ObjectIdPass.h | 24 + .../Rendering/Passes/BuiltinObjectIdPass.h | 58 +++ engine/src/Rendering/CameraRenderer.cpp | 41 +- .../Rendering/Passes/BuiltinObjectIdPass.cpp | 420 ++++++++++++++++++ .../unit/test_camera_scene_renderer.cpp | 109 +++++ 10 files changed, 1030 insertions(+), 3 deletions(-) create mode 100644 engine/include/XCEngine/Rendering/ObjectIdEncoding.h create mode 100644 engine/include/XCEngine/Rendering/ObjectIdPass.h create mode 100644 engine/include/XCEngine/Rendering/Passes/BuiltinObjectIdPass.h create mode 100644 engine/src/Rendering/Passes/BuiltinObjectIdPass.cpp diff --git a/editor/src/Viewport/ViewportHostService.h b/editor/src/Viewport/ViewportHostService.h index 5233c718..b038b07e 100644 --- a/editor/src/Viewport/ViewportHostService.h +++ b/editor/src/Viewport/ViewportHostService.h @@ -15,18 +15,23 @@ #include #include +#include #include #include #include #include #include +#include #include #include #include #include +#include #include +#include #include +#include #include #include #include @@ -74,6 +79,191 @@ inline void SetViewportStatusIfEmpty(std::string& statusText, const char* messag } } +inline uint32_t ClampViewportPixelCoordinate(float value, uint32_t extent) { + if (extent == 0) { + return 0; + } + + const float maxCoordinate = static_cast(extent - 1u); + const float clamped = (std::max)(0.0f, (std::min)(value, maxCoordinate)); + return static_cast(std::floor(clamped)); +} + +inline bool WaitForD3D12QueueIdle( + ID3D12Device* device, + ID3D12CommandQueue* commandQueue) { + if (device == nullptr || commandQueue == nullptr) { + return false; + } + + ID3D12Fence* fence = nullptr; + if (FAILED(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)))) { + return false; + } + + HANDLE fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (fenceEvent == nullptr) { + fence->Release(); + return false; + } + + constexpr UINT64 kFenceValue = 1; + const HRESULT signalResult = commandQueue->Signal(fence, kFenceValue); + if (FAILED(signalResult)) { + CloseHandle(fenceEvent); + fence->Release(); + return false; + } + + if (fence->GetCompletedValue() < kFenceValue) { + if (FAILED(fence->SetEventOnCompletion(kFenceValue, fenceEvent))) { + CloseHandle(fenceEvent); + fence->Release(); + return false; + } + WaitForSingleObject(fenceEvent, INFINITE); + } + + CloseHandle(fenceEvent); + fence->Release(); + return true; +} + +inline bool ReadBackD3D12TexturePixel( + RHI::D3D12Device& device, + RHI::D3D12CommandQueue& commandQueue, + RHI::D3D12Texture& texture, + RHI::ResourceStates sourceState, + uint32_t pixelX, + uint32_t pixelY, + std::array& outRgba) { + ID3D12Device* nativeDevice = device.GetDevice(); + ID3D12CommandQueue* nativeCommandQueue = commandQueue.GetCommandQueue(); + ID3D12Resource* sourceResource = texture.GetResource(); + if (nativeDevice == nullptr || + nativeCommandQueue == nullptr || + sourceResource == nullptr || + pixelX >= texture.GetWidth() || + pixelY >= texture.GetHeight()) { + return false; + } + + ID3D12CommandAllocator* commandAllocator = nullptr; + if (FAILED(nativeDevice->CreateCommandAllocator( + D3D12_COMMAND_LIST_TYPE_DIRECT, + IID_PPV_ARGS(&commandAllocator)))) { + return false; + } + + ID3D12GraphicsCommandList* commandList = nullptr; + if (FAILED(nativeDevice->CreateCommandList( + 0, + D3D12_COMMAND_LIST_TYPE_DIRECT, + commandAllocator, + nullptr, + IID_PPV_ARGS(&commandList)))) { + commandAllocator->Release(); + return false; + } + + D3D12_HEAP_PROPERTIES heapProperties = {}; + heapProperties.Type = D3D12_HEAP_TYPE_READBACK; + + constexpr UINT kRowPitch = D3D12_TEXTURE_DATA_PITCH_ALIGNMENT; + D3D12_RESOURCE_DESC readbackDesc = {}; + readbackDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + readbackDesc.Width = kRowPitch; + readbackDesc.Height = 1; + readbackDesc.DepthOrArraySize = 1; + readbackDesc.MipLevels = 1; + readbackDesc.SampleDesc.Count = 1; + readbackDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + + ID3D12Resource* readbackBuffer = nullptr; + if (FAILED(nativeDevice->CreateCommittedResource( + &heapProperties, + D3D12_HEAP_FLAG_NONE, + &readbackDesc, + D3D12_RESOURCE_STATE_COPY_DEST, + nullptr, + IID_PPV_ARGS(&readbackBuffer)))) { + commandList->Release(); + commandAllocator->Release(); + return false; + } + + D3D12_RESOURCE_BARRIER barrier = {}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Transition.pResource = sourceResource; + barrier.Transition.StateBefore = RHI::ToD3D12(sourceState); + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE; + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + commandList->ResourceBarrier(1, &barrier); + + D3D12_TEXTURE_COPY_LOCATION srcLocation = {}; + srcLocation.pResource = sourceResource; + srcLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + srcLocation.SubresourceIndex = 0; + + D3D12_RESOURCE_DESC sourceDesc = sourceResource->GetDesc(); + D3D12_TEXTURE_COPY_LOCATION dstLocation = {}; + dstLocation.pResource = readbackBuffer; + dstLocation.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + dstLocation.PlacedFootprint.Offset = 0; + dstLocation.PlacedFootprint.Footprint.Format = sourceDesc.Format; + dstLocation.PlacedFootprint.Footprint.Width = 1; + dstLocation.PlacedFootprint.Footprint.Height = 1; + dstLocation.PlacedFootprint.Footprint.Depth = 1; + dstLocation.PlacedFootprint.Footprint.RowPitch = kRowPitch; + + D3D12_BOX sourceBox = {}; + sourceBox.left = pixelX; + sourceBox.top = pixelY; + sourceBox.front = 0; + sourceBox.right = pixelX + 1u; + sourceBox.bottom = pixelY + 1u; + sourceBox.back = 1; + commandList->CopyTextureRegion(&dstLocation, 0, 0, 0, &srcLocation, &sourceBox); + + barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE; + barrier.Transition.StateAfter = RHI::ToD3D12(sourceState); + commandList->ResourceBarrier(1, &barrier); + + if (FAILED(commandList->Close())) { + readbackBuffer->Release(); + commandList->Release(); + commandAllocator->Release(); + return false; + } + + ID3D12CommandList* commandLists[] = { commandList }; + nativeCommandQueue->ExecuteCommandLists(1, commandLists); + if (!WaitForD3D12QueueIdle(nativeDevice, nativeCommandQueue)) { + readbackBuffer->Release(); + commandList->Release(); + commandAllocator->Release(); + return false; + } + + void* mappedData = nullptr; + D3D12_RANGE readRange = { 0, 4 }; + if (FAILED(readbackBuffer->Map(0, &readRange, &mappedData))) { + readbackBuffer->Release(); + commandList->Release(); + commandAllocator->Release(); + return false; + } + + std::memcpy(outRgba.data(), mappedData, outRgba.size()); + D3D12_RANGE writeRange = { 0, 0 }; + readbackBuffer->Unmap(0, &writeRange); + + readbackBuffer->Release(); + commandList->Release(); + commandAllocator->Release(); + return true; +} + Math::Vector3 GetSceneViewportOrientationAxisVector(SceneViewportOrientationAxis axis) { switch (axis) { case SceneViewportOrientationAxis::PositiveX: @@ -110,6 +300,7 @@ public: } m_sceneViewCamera = {}; + m_sceneViewLastRenderContext = {}; m_device = nullptr; m_backend = nullptr; m_sceneGridPass.Shutdown(); @@ -201,6 +392,16 @@ public: return 0; } + ViewportEntry& entry = GetEntry(EditorViewportKind::Scene); + uint64_t objectIdEntity = 0; + if (TryPickSceneViewEntityWithObjectId( + entry, + viewportSize, + viewportMousePosition, + objectIdEntity)) { + return objectIdEntity; + } + SceneViewportPickRequest request = {}; request.scene = scene; request.overlay = GetSceneViewOverlayData(); @@ -255,6 +456,7 @@ public: } EnsureSceneRenderer(); + m_sceneViewLastRenderContext = renderContext; const auto* scene = context.GetSceneManager().GetScene(); for (ViewportEntry& entry : m_entries) { @@ -286,11 +488,15 @@ private: RHI::RHITexture* selectionMaskTexture = nullptr; RHI::RHIResourceView* selectionMaskView = nullptr; RHI::RHIResourceView* selectionMaskShaderView = nullptr; + RHI::RHITexture* objectIdTexture = nullptr; + RHI::RHIResourceView* objectIdView = nullptr; D3D12_CPU_DESCRIPTOR_HANDLE imguiCpuHandle = {}; D3D12_GPU_DESCRIPTOR_HANDLE imguiGpuHandle = {}; ImTextureID textureId = {}; RHI::ResourceStates colorState = RHI::ResourceStates::Common; RHI::ResourceStates selectionMaskState = RHI::ResourceStates::Common; + RHI::ResourceStates objectIdState = RHI::ResourceStates::Common; + bool hasValidObjectIdFrame = false; std::string statusText; }; @@ -458,6 +664,22 @@ private: return entry.selectionMaskShaderView != nullptr; } + bool CreateSceneViewportObjectIdResources(ViewportEntry& entry) { + const RHI::TextureDesc objectIdDesc = + BuildViewportTextureDesc(entry.width, entry.height, RHI::Format::R8G8B8A8_UNorm); + entry.objectIdTexture = m_device->CreateTexture(objectIdDesc); + if (entry.objectIdTexture == nullptr) { + return false; + } + + const RHI::ResourceViewDesc objectIdViewDesc = + BuildViewportTextureViewDesc(RHI::Format::R8G8B8A8_UNorm); + entry.objectIdView = m_device->CreateRenderTargetView( + entry.objectIdTexture, + objectIdViewDesc); + return entry.objectIdView != nullptr; + } + bool CreateViewportTextureDescriptor(ViewportEntry& entry) { m_backend->AllocateTextureDescriptor(&entry.imguiCpuHandle, &entry.imguiGpuHandle); if (entry.imguiCpuHandle.ptr == 0 || entry.imguiGpuHandle.ptr == 0) { @@ -493,7 +715,9 @@ private: (entry.kind != EditorViewportKind::Scene || (entry.selectionMaskTexture != nullptr && entry.selectionMaskView != nullptr && - entry.selectionMaskShaderView != nullptr)) && + entry.selectionMaskShaderView != nullptr && + entry.objectIdTexture != nullptr && + entry.objectIdView != nullptr)) && entry.textureId != ImTextureID{}) { return true; } @@ -519,12 +743,20 @@ private: return false; } + if (entry.kind == EditorViewportKind::Scene && + !CreateSceneViewportObjectIdResources(entry)) { + DestroyViewportResources(entry); + return false; + } + if (!CreateViewportTextureDescriptor(entry)) { DestroyViewportResources(entry); return false; } entry.colorState = RHI::ResourceStates::Common; entry.selectionMaskState = RHI::ResourceStates::Common; + entry.objectIdState = RHI::ResourceStates::Common; + entry.hasValidObjectIdFrame = false; return true; } @@ -546,6 +778,15 @@ private: return surface; } + Rendering::RenderSurface BuildObjectIdSurface(const ViewportEntry& entry) const { + Rendering::RenderSurface surface(entry.width, entry.height); + surface.SetColorAttachment(entry.objectIdView); + surface.SetDepthAttachment(entry.depthView); + surface.SetColorStateBefore(entry.objectIdState); + surface.SetColorStateAfter(RHI::ResourceStates::PixelShaderResource); + return surface; + } + void AddSceneSelectionMaskPass( ViewportEntry& entry, const Rendering::RenderSurface& selectionMaskSurface, @@ -802,6 +1043,7 @@ private: if (!EnsureSceneViewCamera()) { entry.statusText = "Scene view camera is unavailable"; ClearViewport(entry, renderContext, 0.18f, 0.07f, 0.07f, 1.0f); + entry.hasValidObjectIdFrame = false; return false; } @@ -810,6 +1052,7 @@ private: if (scene == nullptr) { entry.statusText = "No active scene"; ClearViewport(entry, renderContext, 0.07f, 0.08f, 0.10f, 1.0f); + entry.hasValidObjectIdFrame = false; return false; } @@ -821,6 +1064,7 @@ private: if (requests.empty()) { SetViewportStatusIfEmpty(entry.statusText, "Scene renderer failed"); ClearViewport(entry, renderContext, 0.18f, 0.07f, 0.07f, 1.0f); + entry.hasValidObjectIdFrame = false; return false; } @@ -828,13 +1072,21 @@ private: requests[0].postScenePasses = &sceneState.postPasses; } + if (entry.objectIdView != nullptr) { + requests[0].objectId.surface = BuildObjectIdSurface(entry); + requests[0].objectId.surface.SetRenderArea(requests[0].surface.GetRenderArea()); + } + if (!m_sceneRenderer->Render(requests)) { SetViewportStatusIfEmpty(entry.statusText, "Scene renderer failed"); ClearViewport(entry, renderContext, 0.18f, 0.07f, 0.07f, 1.0f); + entry.hasValidObjectIdFrame = false; return false; } entry.colorState = RHI::ResourceStates::PixelShaderResource; + entry.objectIdState = RHI::ResourceStates::PixelShaderResource; + entry.hasValidObjectIdFrame = requests[0].objectId.IsRequested(); return true; } @@ -859,6 +1111,7 @@ private: if (!m_sceneRenderer->Render(*scene, nullptr, renderContext, surface)) { entry.statusText = "Scene renderer failed"; ClearViewport(entry, renderContext, 0.18f, 0.07f, 0.07f, 1.0f); + entry.hasValidObjectIdFrame = false; return false; } @@ -914,6 +1167,56 @@ private: RHI::ResourceStates::RenderTarget, RHI::ResourceStates::PixelShaderResource); entry.colorState = RHI::ResourceStates::PixelShaderResource; + entry.hasValidObjectIdFrame = false; + } + + bool TryPickSceneViewEntityWithObjectId( + ViewportEntry& entry, + const ImVec2& viewportSize, + const ImVec2& viewportMousePosition, + uint64_t& outEntityId) { + outEntityId = 0; + + if (m_device == nullptr || + m_sceneViewLastRenderContext.commandQueue == nullptr || + entry.objectIdTexture == nullptr || + !entry.hasValidObjectIdFrame || + viewportSize.x <= 1.0f || + viewportSize.y <= 1.0f || + viewportMousePosition.x < 0.0f || + viewportMousePosition.y < 0.0f || + viewportMousePosition.x > viewportSize.x || + viewportMousePosition.y > viewportSize.y) { + return false; + } + + auto* commandQueue = + static_cast(m_sceneViewLastRenderContext.commandQueue); + auto* objectIdTexture = static_cast(entry.objectIdTexture); + if (commandQueue == nullptr || objectIdTexture == nullptr) { + return false; + } + + const uint32_t pixelX = ClampViewportPixelCoordinate(viewportMousePosition.x, entry.width); + const uint32_t pixelY = ClampViewportPixelCoordinate(viewportMousePosition.y, entry.height); + std::array rgba = {}; + if (!ReadBackD3D12TexturePixel( + *m_device, + *commandQueue, + *objectIdTexture, + entry.objectIdState, + pixelX, + pixelY, + rgba)) { + return false; + } + + outEntityId = static_cast(Rendering::DecodeObjectIdFromColor( + rgba[0], + rgba[1], + rgba[2], + rgba[3])); + return true; } void DestroyViewportResources(ViewportEntry& entry) { @@ -939,6 +1242,18 @@ private: entry.selectionMaskTexture = nullptr; } + if (entry.objectIdView != nullptr) { + entry.objectIdView->Shutdown(); + delete entry.objectIdView; + entry.objectIdView = nullptr; + } + + if (entry.objectIdTexture != nullptr) { + entry.objectIdTexture->Shutdown(); + delete entry.objectIdTexture; + entry.objectIdTexture = nullptr; + } + if (entry.depthView != nullptr) { entry.depthView->Shutdown(); delete entry.depthView; @@ -970,11 +1285,14 @@ private: entry.textureId = {}; entry.colorState = RHI::ResourceStates::Common; entry.selectionMaskState = RHI::ResourceStates::Common; + entry.objectIdState = RHI::ResourceStates::Common; + entry.hasValidObjectIdFrame = false; } UI::ImGuiBackendBridge* m_backend = nullptr; RHI::D3D12Device* m_device = nullptr; std::unique_ptr m_sceneRenderer; + Rendering::RenderContext m_sceneViewLastRenderContext = {}; std::array m_entries = {}; SceneViewCameraState m_sceneViewCamera; SceneViewportInfiniteGridPass m_sceneGridPass; diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index f1ec0aa5..a90f0f74 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -341,9 +341,13 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderSurface.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderResourceCache.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/CameraRenderer.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/ObjectIdEncoding.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/ObjectIdPass.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/SceneRenderer.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/Passes/BuiltinObjectIdPass.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/CameraRenderer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Passes/BuiltinObjectIdPass.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/RenderSurface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/RenderSceneExtractor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/RenderResourceCache.cpp diff --git a/engine/include/XCEngine/Rendering/CameraRenderRequest.h b/engine/include/XCEngine/Rendering/CameraRenderRequest.h index 48685bc6..b58911cc 100644 --- a/engine/include/XCEngine/Rendering/CameraRenderRequest.h +++ b/engine/include/XCEngine/Rendering/CameraRenderRequest.h @@ -13,11 +13,29 @@ class Scene; namespace Rendering { +struct ObjectIdRenderRequest { + RenderSurface surface; + + bool IsRequested() const { + return !surface.GetColorAttachments().empty(); + } + + bool IsValid() const { + const std::vector& colorAttachments = surface.GetColorAttachments(); + return !colorAttachments.empty() && + colorAttachments[0] != nullptr && + surface.GetDepthAttachment() != nullptr && + surface.GetRenderAreaWidth() > 0 && + surface.GetRenderAreaHeight() > 0; + } +}; + struct CameraRenderRequest { const Components::Scene* scene = nullptr; Components::CameraComponent* camera = nullptr; RenderContext context; RenderSurface surface; + ObjectIdRenderRequest objectId; float cameraDepth = 0.0f; uint8_t cameraStackOrder = 0; RenderClearFlags clearFlags = RenderClearFlags::All; diff --git a/engine/include/XCEngine/Rendering/CameraRenderer.h b/engine/include/XCEngine/Rendering/CameraRenderer.h index e5b96ed3..4ecae49b 100644 --- a/engine/include/XCEngine/Rendering/CameraRenderer.h +++ b/engine/include/XCEngine/Rendering/CameraRenderer.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -21,12 +22,17 @@ public: CameraRenderer(); explicit CameraRenderer(std::unique_ptr pipeline); explicit CameraRenderer(std::shared_ptr pipelineAsset); + CameraRenderer( + std::unique_ptr pipeline, + std::unique_ptr objectIdPass); ~CameraRenderer(); void SetPipeline(std::unique_ptr pipeline); void SetPipelineAsset(std::shared_ptr pipelineAsset); + void SetObjectIdPass(std::unique_ptr objectIdPass); RenderPipeline* GetPipeline() const { return m_pipeline.get(); } const RenderPipelineAsset* GetPipelineAsset() const { return m_pipelineAsset.get(); } + ObjectIdPass* GetObjectIdPass() const { return m_objectIdPass.get(); } bool Render(const CameraRenderRequest& request); @@ -36,6 +42,7 @@ private: RenderSceneExtractor m_sceneExtractor; std::shared_ptr m_pipelineAsset; std::unique_ptr m_pipeline; + std::unique_ptr m_objectIdPass; }; } // namespace Rendering diff --git a/engine/include/XCEngine/Rendering/ObjectIdEncoding.h b/engine/include/XCEngine/Rendering/ObjectIdEncoding.h new file mode 100644 index 00000000..af2cc951 --- /dev/null +++ b/engine/include/XCEngine/Rendering/ObjectIdEncoding.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include + +namespace XCEngine { +namespace Rendering { + +inline uint32_t EncodeObjectIdToUInt32(uint64_t objectId) { + return static_cast(objectId & 0xFFFFFFFFull); +} + +inline Math::Vector4 EncodeObjectIdToColor(uint64_t objectId) { + const uint32_t encodedId = EncodeObjectIdToUInt32(objectId); + constexpr float kInv255 = 1.0f / 255.0f; + return Math::Vector4( + static_cast((encodedId >> 0) & 0xFFu) * kInv255, + static_cast((encodedId >> 8) & 0xFFu) * kInv255, + static_cast((encodedId >> 16) & 0xFFu) * kInv255, + static_cast((encodedId >> 24) & 0xFFu) * kInv255); +} + +inline uint32_t DecodeObjectIdFromColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { + return static_cast(r) | + (static_cast(g) << 8u) | + (static_cast(b) << 16u) | + (static_cast(a) << 24u); +} + +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/include/XCEngine/Rendering/ObjectIdPass.h b/engine/include/XCEngine/Rendering/ObjectIdPass.h new file mode 100644 index 00000000..27e10ee7 --- /dev/null +++ b/engine/include/XCEngine/Rendering/ObjectIdPass.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +namespace XCEngine { +namespace Rendering { + +class ObjectIdPass { +public: + virtual ~ObjectIdPass() = default; + + virtual bool Render( + const RenderContext& context, + const RenderSurface& surface, + const RenderSceneData& sceneData) = 0; + + virtual void Shutdown() { + } +}; + +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/include/XCEngine/Rendering/Passes/BuiltinObjectIdPass.h b/engine/include/XCEngine/Rendering/Passes/BuiltinObjectIdPass.h new file mode 100644 index 00000000..a7e3993e --- /dev/null +++ b/engine/include/XCEngine/Rendering/Passes/BuiltinObjectIdPass.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include + +#include + +namespace XCEngine { +namespace Rendering { +namespace Passes { + +class BuiltinObjectIdPass final : public ObjectIdPass { +public: + ~BuiltinObjectIdPass() override; + + bool Render( + const RenderContext& context, + const RenderSurface& surface, + const RenderSceneData& sceneData) override; + + void Shutdown() override; + +private: + struct PerObjectConstants { + Math::Matrix4x4 projection = Math::Matrix4x4::Identity(); + Math::Matrix4x4 view = Math::Matrix4x4::Identity(); + Math::Matrix4x4 model = Math::Matrix4x4::Identity(); + Math::Vector4 objectIdColor = Math::Vector4::Zero(); + }; + + struct OwnedDescriptorSet { + RHI::RHIDescriptorPool* pool = nullptr; + RHI::RHIDescriptorSet* set = nullptr; + }; + + bool EnsureInitialized(const RenderContext& context); + bool CreateResources(const RenderContext& context); + void DestroyResources(); + + RHI::RHIDescriptorSet* GetOrCreatePerObjectSet(uint64_t objectId); + void DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet); + bool DrawVisibleItem( + const RenderContext& context, + const RenderSceneData& sceneData, + const VisibleRenderItem& visibleItem); + + RHI::RHIDevice* m_device = nullptr; + RHI::RHIType m_backendType = RHI::RHIType::D3D12; + RHI::RHIPipelineLayout* m_pipelineLayout = nullptr; + RHI::RHIPipelineState* m_pipelineState = nullptr; + RenderResourceCache m_resourceCache; + std::unordered_map m_perObjectSets; +}; + +} // namespace Passes +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/src/Rendering/CameraRenderer.cpp b/engine/src/Rendering/CameraRenderer.cpp index d41d72eb..6e2bd013 100644 --- a/engine/src/Rendering/CameraRenderer.cpp +++ b/engine/src/Rendering/CameraRenderer.cpp @@ -1,5 +1,6 @@ #include "Rendering/CameraRenderer.h" +#include "Rendering/Passes/BuiltinObjectIdPass.h" #include "Rendering/Pipelines/BuiltinForwardPipeline.h" #include "Rendering/RenderPipelineAsset.h" #include "Rendering/RenderSurface.h" @@ -58,12 +59,23 @@ CameraRenderer::CameraRenderer() } CameraRenderer::CameraRenderer(std::unique_ptr pipeline) - : m_pipelineAsset(nullptr) { + : CameraRenderer(std::move(pipeline), std::make_unique()) { +} + +CameraRenderer::CameraRenderer( + std::unique_ptr pipeline, + std::unique_ptr objectIdPass) + : m_pipelineAsset(nullptr) + , m_objectIdPass(std::move(objectIdPass)) { + if (m_objectIdPass == nullptr) { + m_objectIdPass = std::make_unique(); + } ResetPipeline(std::move(pipeline)); } CameraRenderer::CameraRenderer(std::shared_ptr pipelineAsset) - : m_pipelineAsset(std::move(pipelineAsset)) { + : m_pipelineAsset(std::move(pipelineAsset)) + , m_objectIdPass(std::make_unique()) { SetPipelineAsset(m_pipelineAsset); } @@ -71,6 +83,9 @@ CameraRenderer::~CameraRenderer() { if (m_pipeline) { m_pipeline->Shutdown(); } + if (m_objectIdPass != nullptr) { + m_objectIdPass->Shutdown(); + } } void CameraRenderer::SetPipeline(std::unique_ptr pipeline) { @@ -83,6 +98,17 @@ void CameraRenderer::SetPipelineAsset(std::shared_ptr ResetPipeline(CreatePipelineFromAsset(m_pipelineAsset)); } +void CameraRenderer::SetObjectIdPass(std::unique_ptr objectIdPass) { + if (m_objectIdPass != nullptr) { + m_objectIdPass->Shutdown(); + } + + m_objectIdPass = std::move(objectIdPass); + if (m_objectIdPass == nullptr) { + m_objectIdPass = std::make_unique(); + } +} + void CameraRenderer::ResetPipeline(std::unique_ptr pipeline) { if (m_pipeline != nullptr) { m_pipeline->Shutdown(); @@ -105,6 +131,10 @@ bool CameraRenderer::Render( request.surface.GetRenderAreaHeight() == 0) { return false; } + if (request.objectId.IsRequested() && + !request.objectId.IsValid()) { + return false; + } RenderSceneData sceneData = m_sceneExtractor.ExtractForCamera( *request.scene, @@ -140,6 +170,13 @@ bool CameraRenderer::Render( return false; } + if (request.objectId.IsRequested() && + (m_objectIdPass == nullptr || + !m_objectIdPass->Render(request.context, request.objectId.surface, sceneData))) { + ShutdownPassSequence(request.preScenePasses, preScenePassesInitialized); + return false; + } + bool postScenePassesInitialized = false; if (!InitializePassSequence( request.postScenePasses, diff --git a/engine/src/Rendering/Passes/BuiltinObjectIdPass.cpp b/engine/src/Rendering/Passes/BuiltinObjectIdPass.cpp new file mode 100644 index 00000000..81f34bd8 --- /dev/null +++ b/engine/src/Rendering/Passes/BuiltinObjectIdPass.cpp @@ -0,0 +1,420 @@ +#include "Rendering/Passes/BuiltinObjectIdPass.h" + +#include "Components/GameObject.h" +#include "RHI/RHICommandList.h" +#include "RHI/RHIPipelineLayout.h" +#include "RHI/RHIPipelineState.h" +#include "Resources/Mesh/Mesh.h" + +#include +#include + +namespace XCEngine { +namespace Rendering { +namespace Passes { + +namespace { + +const char kBuiltinObjectIdHlsl[] = R"( +cbuffer PerObjectConstants : register(b0) { + float4x4 gProjectionMatrix; + float4x4 gViewMatrix; + float4x4 gModelMatrix; + float4 gObjectIdColor; +}; + +struct VSInput { + float3 position : POSITION; +}; + +struct PSInput { + float4 position : SV_POSITION; +}; + +PSInput MainVS(VSInput input) { + PSInput output; + float4 positionWS = mul(gModelMatrix, float4(input.position, 1.0)); + float4 positionVS = mul(gViewMatrix, positionWS); + output.position = mul(gProjectionMatrix, positionVS); + return output; +} + +float4 MainPS(PSInput input) : SV_TARGET { + return gObjectIdColor; +} +)"; + +const char kBuiltinObjectIdVertexShader[] = R"(#version 430 +layout(location = 0) in vec3 aPosition; + +layout(std140, binding = 0) uniform PerObjectConstants { + mat4 gProjectionMatrix; + mat4 gViewMatrix; + mat4 gModelMatrix; + vec4 gObjectIdColor; +}; + +void main() { + vec4 positionWS = gModelMatrix * vec4(aPosition, 1.0); + vec4 positionVS = gViewMatrix * positionWS; + gl_Position = gProjectionMatrix * positionVS; +} +)"; + +const char kBuiltinObjectIdFragmentShader[] = R"(#version 430 +layout(std140, binding = 0) uniform PerObjectConstants { + mat4 gProjectionMatrix; + mat4 gViewMatrix; + mat4 gModelMatrix; + vec4 gObjectIdColor; +}; + +layout(location = 0) out vec4 fragColor; + +void main() { + fragColor = gObjectIdColor; +} +)"; + +RHI::InputLayoutDesc BuildInputLayout() { + RHI::InputLayoutDesc inputLayout = {}; + + RHI::InputElementDesc position = {}; + position.semanticName = "POSITION"; + position.semanticIndex = 0; + position.format = static_cast(RHI::Format::R32G32B32_Float); + position.inputSlot = 0; + position.alignedByteOffset = static_cast(offsetof(Resources::StaticMeshVertex, position)); + inputLayout.elements.push_back(position); + return inputLayout; +} + +RHI::GraphicsPipelineDesc CreatePipelineDesc( + RHI::RHIType backendType, + RHI::RHIPipelineLayout* pipelineLayout) { + RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = pipelineLayout; + pipelineDesc.topologyType = static_cast(RHI::PrimitiveTopologyType::Triangle); + pipelineDesc.renderTargetCount = 1; + pipelineDesc.renderTargetFormats[0] = static_cast(RHI::Format::R8G8B8A8_UNorm); + pipelineDesc.depthStencilFormat = static_cast(RHI::Format::D24_UNorm_S8_UInt); + pipelineDesc.sampleCount = 1; + pipelineDesc.inputLayout = BuildInputLayout(); + + pipelineDesc.rasterizerState.fillMode = static_cast(RHI::FillMode::Solid); + pipelineDesc.rasterizerState.cullMode = static_cast(RHI::CullMode::None); + pipelineDesc.rasterizerState.frontFace = static_cast(RHI::FrontFace::CounterClockwise); + pipelineDesc.rasterizerState.depthClipEnable = true; + + pipelineDesc.blendState.blendEnable = false; + pipelineDesc.blendState.colorWriteMask = static_cast(RHI::ColorWriteMask::All); + + pipelineDesc.depthStencilState.depthTestEnable = true; + pipelineDesc.depthStencilState.depthWriteEnable = false; + pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::LessEqual); + + if (backendType == RHI::RHIType::D3D12) { + pipelineDesc.vertexShader.source.assign( + kBuiltinObjectIdHlsl, + kBuiltinObjectIdHlsl + std::strlen(kBuiltinObjectIdHlsl)); + pipelineDesc.vertexShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.vertexShader.entryPoint = L"MainVS"; + pipelineDesc.vertexShader.profile = L"vs_5_0"; + + pipelineDesc.fragmentShader.source.assign( + kBuiltinObjectIdHlsl, + kBuiltinObjectIdHlsl + std::strlen(kBuiltinObjectIdHlsl)); + pipelineDesc.fragmentShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.fragmentShader.entryPoint = L"MainPS"; + pipelineDesc.fragmentShader.profile = L"ps_5_0"; + } else { + pipelineDesc.vertexShader.source.assign( + kBuiltinObjectIdVertexShader, + kBuiltinObjectIdVertexShader + std::strlen(kBuiltinObjectIdVertexShader)); + pipelineDesc.vertexShader.sourceLanguage = RHI::ShaderLanguage::GLSL; + pipelineDesc.vertexShader.profile = L"vs_4_30"; + + pipelineDesc.fragmentShader.source.assign( + kBuiltinObjectIdFragmentShader, + kBuiltinObjectIdFragmentShader + std::strlen(kBuiltinObjectIdFragmentShader)); + pipelineDesc.fragmentShader.sourceLanguage = RHI::ShaderLanguage::GLSL; + pipelineDesc.fragmentShader.profile = L"fs_4_30"; + } + + return pipelineDesc; +} + +} // namespace + +BuiltinObjectIdPass::~BuiltinObjectIdPass() { + Shutdown(); +} + +bool BuiltinObjectIdPass::Render( + const RenderContext& context, + const RenderSurface& surface, + const RenderSceneData& sceneData) { + if (!context.IsValid()) { + return false; + } + + const std::vector& colorAttachments = surface.GetColorAttachments(); + if (colorAttachments.empty() || colorAttachments[0] == nullptr || surface.GetDepthAttachment() == nullptr) { + return false; + } + + const Math::RectInt renderArea = surface.GetRenderArea(); + if (renderArea.width <= 0 || renderArea.height <= 0) { + return false; + } + + if (!EnsureInitialized(context)) { + return false; + } + + RHI::RHICommandList* commandList = context.commandList; + RHI::RHIResourceView* renderTarget = colorAttachments[0]; + + if (surface.IsAutoTransitionEnabled()) { + commandList->TransitionBarrier( + renderTarget, + surface.GetColorStateBefore(), + RHI::ResourceStates::RenderTarget); + } + + commandList->SetRenderTargets(1, &renderTarget, surface.GetDepthAttachment()); + + const RHI::Viewport viewport = { + static_cast(renderArea.x), + static_cast(renderArea.y), + static_cast(renderArea.width), + static_cast(renderArea.height), + 0.0f, + 1.0f + }; + const RHI::Rect scissorRect = { + renderArea.x, + renderArea.y, + renderArea.x + renderArea.width, + renderArea.y + renderArea.height + }; + const float clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + const RHI::Rect clearRects[] = { scissorRect }; + + commandList->SetViewport(viewport); + commandList->SetScissorRect(scissorRect); + commandList->ClearRenderTarget(renderTarget, clearColor, 1, clearRects); + commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList); + commandList->SetPipelineState(m_pipelineState); + + for (const VisibleRenderItem& visibleItem : sceneData.visibleItems) { + DrawVisibleItem(context, sceneData, visibleItem); + } + + if (surface.IsAutoTransitionEnabled()) { + commandList->TransitionBarrier( + renderTarget, + RHI::ResourceStates::RenderTarget, + surface.GetColorStateAfter()); + } + + return true; +} + +void BuiltinObjectIdPass::Shutdown() { + DestroyResources(); +} + +bool BuiltinObjectIdPass::EnsureInitialized(const RenderContext& context) { + if (!context.IsValid()) { + return false; + } + + if (m_pipelineLayout != nullptr && + m_pipelineState != nullptr && + m_device == context.device && + m_backendType == context.backendType) { + return true; + } + + DestroyResources(); + return CreateResources(context); +} + +bool BuiltinObjectIdPass::CreateResources(const RenderContext& context) { + m_device = context.device; + m_backendType = context.backendType; + + RHI::DescriptorSetLayoutBinding constantBinding = {}; + constantBinding.binding = 0; + constantBinding.type = static_cast(RHI::DescriptorType::CBV); + constantBinding.count = 1; + + RHI::DescriptorSetLayoutDesc constantLayout = {}; + constantLayout.bindings = &constantBinding; + constantLayout.bindingCount = 1; + + RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = &constantLayout; + pipelineLayoutDesc.setLayoutCount = 1; + m_pipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc); + if (m_pipelineLayout == nullptr) { + DestroyResources(); + return false; + } + + m_pipelineState = m_device->CreatePipelineState(CreatePipelineDesc(m_backendType, m_pipelineLayout)); + if (m_pipelineState == nullptr || !m_pipelineState->IsValid()) { + if (m_pipelineState != nullptr) { + m_pipelineState->Shutdown(); + delete m_pipelineState; + m_pipelineState = nullptr; + } + DestroyResources(); + return false; + } + + return true; +} + +void BuiltinObjectIdPass::DestroyResources() { + m_resourceCache.Shutdown(); + + for (auto& descriptorSetEntry : m_perObjectSets) { + DestroyOwnedDescriptorSet(descriptorSetEntry.second); + } + m_perObjectSets.clear(); + + if (m_pipelineState != nullptr) { + m_pipelineState->Shutdown(); + delete m_pipelineState; + m_pipelineState = nullptr; + } + + if (m_pipelineLayout != nullptr) { + m_pipelineLayout->Shutdown(); + delete m_pipelineLayout; + m_pipelineLayout = nullptr; + } + + m_device = nullptr; + m_backendType = RHI::RHIType::D3D12; +} + +RHI::RHIDescriptorSet* BuiltinObjectIdPass::GetOrCreatePerObjectSet(uint64_t objectId) { + const auto existing = m_perObjectSets.find(objectId); + if (existing != m_perObjectSets.end()) { + return existing->second.set; + } + + RHI::DescriptorPoolDesc poolDesc = {}; + poolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV; + poolDesc.descriptorCount = 1; + poolDesc.shaderVisible = false; + + OwnedDescriptorSet descriptorSet = {}; + descriptorSet.pool = m_device->CreateDescriptorPool(poolDesc); + if (descriptorSet.pool == nullptr) { + return nullptr; + } + + RHI::DescriptorSetLayoutBinding binding = {}; + binding.binding = 0; + binding.type = static_cast(RHI::DescriptorType::CBV); + binding.count = 1; + + RHI::DescriptorSetLayoutDesc layout = {}; + layout.bindings = &binding; + layout.bindingCount = 1; + descriptorSet.set = descriptorSet.pool->AllocateSet(layout); + if (descriptorSet.set == nullptr) { + DestroyOwnedDescriptorSet(descriptorSet); + return nullptr; + } + + const auto result = m_perObjectSets.emplace(objectId, descriptorSet); + return result.first->second.set; +} + +void BuiltinObjectIdPass::DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet) { + if (descriptorSet.set != nullptr) { + descriptorSet.set->Shutdown(); + delete descriptorSet.set; + descriptorSet.set = nullptr; + } + + if (descriptorSet.pool != nullptr) { + descriptorSet.pool->Shutdown(); + delete descriptorSet.pool; + descriptorSet.pool = nullptr; + } +} + +bool BuiltinObjectIdPass::DrawVisibleItem( + const RenderContext& context, + const RenderSceneData& sceneData, + const VisibleRenderItem& visibleItem) { + if (visibleItem.mesh == nullptr || visibleItem.gameObject == nullptr) { + return false; + } + + const RenderResourceCache::CachedMesh* cachedMesh = + m_resourceCache.GetOrCreateMesh(m_device, visibleItem.mesh); + if (cachedMesh == nullptr || cachedMesh->vertexBufferView == nullptr) { + return false; + } + + RHI::RHICommandList* commandList = context.commandList; + + RHI::RHIResourceView* vertexBuffers[] = { cachedMesh->vertexBufferView }; + const uint64_t offsets[] = { 0 }; + const uint32_t strides[] = { cachedMesh->vertexStride }; + commandList->SetVertexBuffers(0, 1, vertexBuffers, offsets, strides); + if (cachedMesh->indexBufferView != nullptr) { + commandList->SetIndexBuffer(cachedMesh->indexBufferView, 0); + } + + const uint64_t objectId = visibleItem.gameObject->GetID(); + RHI::RHIDescriptorSet* constantSet = GetOrCreatePerObjectSet(objectId); + if (constantSet == nullptr) { + return false; + } + + const PerObjectConstants constants = { + sceneData.cameraData.projection, + sceneData.cameraData.view, + visibleItem.localToWorld.Transpose(), + EncodeObjectIdToColor(objectId) + }; + constantSet->WriteConstant(0, &constants, sizeof(constants)); + + RHI::RHIDescriptorSet* descriptorSets[] = { constantSet }; + commandList->SetGraphicsDescriptorSets(0, 1, descriptorSets, m_pipelineLayout); + + if (visibleItem.hasSection) { + const Containers::Array& sections = visibleItem.mesh->GetSections(); + if (visibleItem.sectionIndex >= sections.Size()) { + return false; + } + + const Resources::MeshSection& section = sections[visibleItem.sectionIndex]; + if (cachedMesh->indexBufferView != nullptr && section.indexCount > 0) { + commandList->DrawIndexed(section.indexCount, 1, section.startIndex, 0, 0); + } else if (section.vertexCount > 0) { + commandList->Draw(section.vertexCount, 1, section.baseVertex, 0); + } + return true; + } + + if (cachedMesh->indexBufferView != nullptr && cachedMesh->indexCount > 0) { + commandList->DrawIndexed(cachedMesh->indexCount, 1, 0, 0, 0); + } else if (cachedMesh->vertexCount > 0) { + commandList->Draw(cachedMesh->vertexCount, 1, 0, 0); + } + + return true; +} + +} // namespace Passes +} // namespace Rendering +} // namespace XCEngine diff --git a/tests/Rendering/unit/test_camera_scene_renderer.cpp b/tests/Rendering/unit/test_camera_scene_renderer.cpp index 3f5b3090..e64085ad 100644 --- a/tests/Rendering/unit/test_camera_scene_renderer.cpp +++ b/tests/Rendering/unit/test_camera_scene_renderer.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -100,6 +101,32 @@ private: std::shared_ptr m_state; }; +class MockObjectIdPass final : public ObjectIdPass { +public: + MockObjectIdPass( + std::shared_ptr state, + bool renderResult = true) + : m_state(std::move(state)) + , m_renderResult(renderResult) { + } + + bool Render( + const RenderContext&, + const RenderSurface&, + const RenderSceneData&) override { + m_state->eventLog.push_back("objectId"); + return m_renderResult; + } + + void Shutdown() override { + m_state->eventLog.push_back("shutdown:objectId"); + } + +private: + std::shared_ptr m_state; + bool m_renderResult = true; +}; + class TrackingPass final : public RenderPass { public: TrackingPass( @@ -227,6 +254,51 @@ TEST(CameraRenderer_Test, ExecutesInjectedPreAndPostPassSequencesAroundPipelineR "shutdown:pre" })); } +TEST(CameraRenderer_Test, ExecutesObjectIdPassBetweenPipelineAndPostPassesWhenRequested) { + Scene scene("CameraRendererObjectIdPassScene"); + + GameObject* cameraObject = scene.CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + camera->SetPrimary(true); + camera->SetDepth(3.0f); + + auto state = std::make_shared(); + CameraRenderer renderer( + std::make_unique(state), + std::make_unique(state)); + + RenderPassSequence prePasses; + prePasses.AddPass(std::make_unique(state, "pre")); + + RenderPassSequence postPasses; + postPasses.AddPass(std::make_unique(state, "post")); + + CameraRenderRequest request; + request.scene = &scene; + request.camera = camera; + request.context = CreateValidContext(); + request.surface = RenderSurface(320, 180); + request.cameraDepth = camera->GetDepth(); + request.preScenePasses = &prePasses; + request.postScenePasses = &postPasses; + request.objectId.surface = RenderSurface(320, 180); + request.objectId.surface.SetColorAttachment(reinterpret_cast(1)); + request.objectId.surface.SetDepthAttachment(reinterpret_cast(2)); + + ASSERT_TRUE(renderer.Render(request)); + EXPECT_EQ( + state->eventLog, + (std::vector{ + "init:pre", + "pre", + "pipeline", + "objectId", + "init:post", + "post", + "shutdown:post", + "shutdown:pre" })); +} + TEST(CameraRenderer_Test, ShutsDownInitializedPassesWhenPipelineRenderFails) { Scene scene("CameraRendererFailureScene"); @@ -256,6 +328,43 @@ TEST(CameraRenderer_Test, ShutsDownInitializedPassesWhenPipelineRenderFails) { (std::vector{ "init:pre", "pre", "pipeline", "shutdown:pre" })); } +TEST(CameraRenderer_Test, StopsRenderingWhenObjectIdPassFails) { + Scene scene("CameraRendererObjectIdFailureScene"); + + GameObject* cameraObject = scene.CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + camera->SetPrimary(true); + camera->SetDepth(2.0f); + + auto state = std::make_shared(); + CameraRenderer renderer( + std::make_unique(state), + std::make_unique(state, false)); + + RenderPassSequence prePasses; + prePasses.AddPass(std::make_unique(state, "pre")); + + RenderPassSequence postPasses; + postPasses.AddPass(std::make_unique(state, "post")); + + CameraRenderRequest request; + request.scene = &scene; + request.camera = camera; + request.context = CreateValidContext(); + request.surface = RenderSurface(320, 180); + request.cameraDepth = camera->GetDepth(); + request.preScenePasses = &prePasses; + request.postScenePasses = &postPasses; + request.objectId.surface = RenderSurface(320, 180); + request.objectId.surface.SetColorAttachment(reinterpret_cast(1)); + request.objectId.surface.SetDepthAttachment(reinterpret_cast(2)); + + EXPECT_FALSE(renderer.Render(request)); + EXPECT_EQ( + state->eventLog, + (std::vector{ "init:pre", "pre", "pipeline", "objectId", "shutdown:pre" })); +} + TEST(CameraRenderer_Test, ShutsDownSequencesWhenPostPassInitializationFails) { Scene scene("CameraRendererPostPassInitFailureScene");