#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace XCEngine::Components; using namespace XCEngine::Rendering; namespace { struct MockPipelineState { int initializeCalls = 0; int shutdownCalls = 0; int renderCalls = 0; bool renderResult = true; uint32_t lastSurfaceWidth = 0; uint32_t lastSurfaceHeight = 0; int32_t lastRenderAreaX = 0; int32_t lastRenderAreaY = 0; int32_t lastRenderAreaWidth = 0; int32_t lastRenderAreaHeight = 0; uint32_t lastCameraViewportWidth = 0; uint32_t lastCameraViewportHeight = 0; CameraComponent* lastCamera = nullptr; size_t lastVisibleItemCount = 0; RenderClearFlags lastClearFlags = RenderClearFlags::All; XCEngine::Math::Color lastClearColor = XCEngine::Math::Color::Black(); bool lastHasMainDirectionalShadow = false; XCEngine::RHI::RHIResourceView* lastShadowMap = nullptr; XCEngine::Math::Matrix4x4 lastShadowViewProjection = XCEngine::Math::Matrix4x4::Identity(); RenderDirectionalShadowMapMetrics lastShadowMapMetrics = {}; RenderDirectionalShadowSamplingData lastShadowSampling = {}; bool lastHasMainDirectionalShadowKeyword = false; RenderEnvironmentMode lastEnvironmentMode = RenderEnvironmentMode::None; bool lastHasSkybox = false; const XCEngine::Resources::Material* lastSkyboxMaterial = nullptr; std::vector renderedCameras; std::vector renderedClearFlags; std::vector renderedClearColors; std::vector eventLog; }; struct MockShadowAllocationState { int createTextureCalls = 0; int shutdownTextureCalls = 0; int destroyTextureCalls = 0; int createRenderTargetViewCalls = 0; int shutdownRenderTargetViewCalls = 0; int destroyRenderTargetViewCalls = 0; int createDepthViewCalls = 0; int shutdownDepthViewCalls = 0; int destroyDepthViewCalls = 0; int createShaderViewCalls = 0; int shutdownShaderViewCalls = 0; int destroyShaderViewCalls = 0; uint32_t lastTextureWidth = 0; uint32_t lastTextureHeight = 0; XCEngine::RHI::Format lastTextureFormat = XCEngine::RHI::Format::Unknown; XCEngine::RHI::Format lastRenderTargetViewFormat = XCEngine::RHI::Format::Unknown; XCEngine::RHI::Format lastDepthViewFormat = XCEngine::RHI::Format::Unknown; XCEngine::RHI::Format lastShaderViewFormat = XCEngine::RHI::Format::Unknown; }; class MockShadowTexture final : public XCEngine::RHI::RHITexture { public: MockShadowTexture( std::shared_ptr state, uint32_t width, uint32_t height, XCEngine::RHI::Format format) : m_state(std::move(state)) , m_width(width) , m_height(height) , m_format(format) { } ~MockShadowTexture() override { ++m_state->destroyTextureCalls; } uint32_t GetWidth() const override { return m_width; } uint32_t GetHeight() const override { return m_height; } uint32_t GetDepth() const override { return 1; } uint32_t GetMipLevels() const override { return 1; } XCEngine::RHI::Format GetFormat() const override { return m_format; } XCEngine::RHI::TextureType GetTextureType() const override { return XCEngine::RHI::TextureType::Texture2D; } XCEngine::RHI::ResourceStates GetState() const override { return m_stateValue; } void SetState(XCEngine::RHI::ResourceStates state) override { m_stateValue = state; } void* GetNativeHandle() override { return nullptr; } const std::string& GetName() const override { return m_name; } void SetName(const std::string& name) override { m_name = name; } void Shutdown() override { ++m_state->shutdownTextureCalls; } private: std::shared_ptr m_state; uint32_t m_width = 0; uint32_t m_height = 0; XCEngine::RHI::Format m_format = XCEngine::RHI::Format::Unknown; XCEngine::RHI::ResourceStates m_stateValue = XCEngine::RHI::ResourceStates::DepthWrite; std::string m_name; }; class MockShadowView final : public XCEngine::RHI::RHIResourceView { public: MockShadowView( std::shared_ptr state, XCEngine::RHI::ResourceViewType viewType, XCEngine::RHI::Format format, XCEngine::RHI::ResourceViewDimension dimension) : m_state(std::move(state)) , m_viewType(viewType) , m_format(format) , m_dimension(dimension) { } ~MockShadowView() override { if (m_viewType == XCEngine::RHI::ResourceViewType::ShaderResource) { ++m_state->destroyShaderViewCalls; } else if (m_viewType == XCEngine::RHI::ResourceViewType::RenderTarget) { ++m_state->destroyRenderTargetViewCalls; } else { ++m_state->destroyDepthViewCalls; } } void Shutdown() override { if (m_viewType == XCEngine::RHI::ResourceViewType::ShaderResource) { ++m_state->shutdownShaderViewCalls; } else if (m_viewType == XCEngine::RHI::ResourceViewType::RenderTarget) { ++m_state->shutdownRenderTargetViewCalls; } else { ++m_state->shutdownDepthViewCalls; } } void* GetNativeHandle() override { return nullptr; } bool IsValid() const override { return true; } XCEngine::RHI::ResourceViewType GetViewType() const override { return m_viewType; } XCEngine::RHI::ResourceViewDimension GetDimension() const override { return m_dimension; } XCEngine::RHI::Format GetFormat() const override { return m_format; } private: std::shared_ptr m_state; XCEngine::RHI::ResourceViewType m_viewType = XCEngine::RHI::ResourceViewType::DepthStencil; XCEngine::RHI::Format m_format = XCEngine::RHI::Format::Unknown; XCEngine::RHI::ResourceViewDimension m_dimension = XCEngine::RHI::ResourceViewDimension::Unknown; }; class MockShadowDevice final : public XCEngine::RHI::RHIDevice { public: explicit MockShadowDevice(std::shared_ptr state) : m_state(std::move(state)) { } bool Initialize(const XCEngine::RHI::RHIDeviceDesc&) override { return true; } void Shutdown() override {} XCEngine::RHI::RHIBuffer* CreateBuffer(const XCEngine::RHI::BufferDesc&) override { return nullptr; } XCEngine::RHI::RHITexture* CreateTexture(const XCEngine::RHI::TextureDesc& desc) override { ++m_state->createTextureCalls; m_state->lastTextureWidth = desc.width; m_state->lastTextureHeight = desc.height; m_state->lastTextureFormat = static_cast(desc.format); return new MockShadowTexture( m_state, desc.width, desc.height, static_cast(desc.format)); } XCEngine::RHI::RHITexture* CreateTexture( const XCEngine::RHI::TextureDesc& desc, const void*, size_t, uint32_t) override { return CreateTexture(desc); } XCEngine::RHI::RHISwapChain* CreateSwapChain( const XCEngine::RHI::SwapChainDesc&, XCEngine::RHI::RHICommandQueue*) override { return nullptr; } XCEngine::RHI::RHICommandList* CreateCommandList(const XCEngine::RHI::CommandListDesc&) override { return nullptr; } XCEngine::RHI::RHICommandQueue* CreateCommandQueue(const XCEngine::RHI::CommandQueueDesc&) override { return nullptr; } XCEngine::RHI::RHIShader* CreateShader(const XCEngine::RHI::ShaderCompileDesc&) override { return nullptr; } XCEngine::RHI::RHIPipelineState* CreatePipelineState(const XCEngine::RHI::GraphicsPipelineDesc&) override { return nullptr; } XCEngine::RHI::RHIPipelineLayout* CreatePipelineLayout(const XCEngine::RHI::RHIPipelineLayoutDesc&) override { return nullptr; } XCEngine::RHI::RHIFence* CreateFence(const XCEngine::RHI::FenceDesc&) override { return nullptr; } XCEngine::RHI::RHISampler* CreateSampler(const XCEngine::RHI::SamplerDesc&) override { return nullptr; } XCEngine::RHI::RHIRenderPass* CreateRenderPass( uint32_t, const XCEngine::RHI::AttachmentDesc*, const XCEngine::RHI::AttachmentDesc*) override { return nullptr; } XCEngine::RHI::RHIFramebuffer* CreateFramebuffer( XCEngine::RHI::RHIRenderPass*, uint32_t, uint32_t, uint32_t, XCEngine::RHI::RHIResourceView**, XCEngine::RHI::RHIResourceView*) override { return nullptr; } XCEngine::RHI::RHIDescriptorPool* CreateDescriptorPool(const XCEngine::RHI::DescriptorPoolDesc&) override { return nullptr; } XCEngine::RHI::RHIDescriptorSet* CreateDescriptorSet( XCEngine::RHI::RHIDescriptorPool*, const XCEngine::RHI::DescriptorSetLayoutDesc&) override { return nullptr; } XCEngine::RHI::RHIResourceView* CreateVertexBufferView( XCEngine::RHI::RHIBuffer*, const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; } XCEngine::RHI::RHIResourceView* CreateIndexBufferView( XCEngine::RHI::RHIBuffer*, const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; } XCEngine::RHI::RHIResourceView* CreateRenderTargetView( XCEngine::RHI::RHITexture*, const XCEngine::RHI::ResourceViewDesc& desc) override { ++m_state->createRenderTargetViewCalls; m_state->lastRenderTargetViewFormat = static_cast(desc.format); return new MockShadowView( m_state, XCEngine::RHI::ResourceViewType::RenderTarget, static_cast(desc.format), desc.dimension); } XCEngine::RHI::RHIResourceView* CreateDepthStencilView( XCEngine::RHI::RHITexture*, const XCEngine::RHI::ResourceViewDesc& desc) override { ++m_state->createDepthViewCalls; m_state->lastDepthViewFormat = static_cast(desc.format); return new MockShadowView( m_state, XCEngine::RHI::ResourceViewType::DepthStencil, static_cast(desc.format), desc.dimension); } XCEngine::RHI::RHIResourceView* CreateShaderResourceView( XCEngine::RHI::RHIBuffer*, const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; } XCEngine::RHI::RHIResourceView* CreateShaderResourceView( XCEngine::RHI::RHITexture*, const XCEngine::RHI::ResourceViewDesc& desc) override { ++m_state->createShaderViewCalls; m_state->lastShaderViewFormat = static_cast(desc.format); return new MockShadowView( m_state, XCEngine::RHI::ResourceViewType::ShaderResource, static_cast(desc.format), desc.dimension); } XCEngine::RHI::RHIResourceView* CreateUnorderedAccessView( XCEngine::RHI::RHIBuffer*, const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; } XCEngine::RHI::RHIResourceView* CreateUnorderedAccessView( XCEngine::RHI::RHITexture*, const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; } const XCEngine::RHI::RHICapabilities& GetCapabilities() const override { return m_capabilities; } const XCEngine::RHI::RHIDeviceInfo& GetDeviceInfo() const override { return m_deviceInfo; } void* GetNativeDevice() override { return nullptr; } private: std::shared_ptr m_state; XCEngine::RHI::RHICapabilities m_capabilities = {}; XCEngine::RHI::RHIDeviceInfo m_deviceInfo = {}; }; struct MockPipelineAssetState { int createCalls = 0; std::shared_ptr lastCreatedPipelineState; FinalColorSettings defaultFinalColorSettings = {}; }; class MockPipeline final : public RenderPipeline { public: explicit MockPipeline(std::shared_ptr state) : m_state(std::move(state)) { } bool Initialize(const RenderContext&) override { ++m_state->initializeCalls; return true; } void Shutdown() override { ++m_state->shutdownCalls; } bool Render( const RenderContext&, const RenderSurface& surface, const RenderSceneData& sceneData) override { m_state->eventLog.push_back("pipeline"); ++m_state->renderCalls; m_state->lastSurfaceWidth = surface.GetWidth(); m_state->lastSurfaceHeight = surface.GetHeight(); const XCEngine::Math::RectInt renderArea = surface.GetRenderArea(); m_state->lastRenderAreaX = renderArea.x; m_state->lastRenderAreaY = renderArea.y; m_state->lastRenderAreaWidth = renderArea.width; m_state->lastRenderAreaHeight = renderArea.height; m_state->lastCamera = sceneData.camera; m_state->lastCameraViewportWidth = sceneData.cameraData.viewportWidth; m_state->lastCameraViewportHeight = sceneData.cameraData.viewportHeight; m_state->lastVisibleItemCount = sceneData.visibleItems.size(); m_state->lastClearFlags = sceneData.cameraData.clearFlags; m_state->lastClearColor = sceneData.cameraData.clearColor; m_state->lastHasMainDirectionalShadow = sceneData.lighting.HasMainDirectionalShadow(); m_state->lastShadowMap = sceneData.lighting.mainDirectionalShadow.shadowMap; m_state->lastShadowViewProjection = sceneData.lighting.mainDirectionalShadow.viewProjection; m_state->lastShadowMapMetrics = sceneData.lighting.mainDirectionalShadow.mapMetrics; m_state->lastShadowSampling = sceneData.lighting.mainDirectionalShadow.sampling; m_state->lastHasMainDirectionalShadowKeyword = XCEngine::Resources::ShaderKeywordSetContains( sceneData.globalShaderKeywords, "XC_MAIN_LIGHT_SHADOWS"); m_state->lastEnvironmentMode = sceneData.environment.mode; m_state->lastHasSkybox = sceneData.environment.HasSkybox(); m_state->lastSkyboxMaterial = sceneData.environment.materialSkybox.material; m_state->renderedCameras.push_back(sceneData.camera); m_state->renderedClearFlags.push_back(sceneData.cameraData.clearFlags); m_state->renderedClearColors.push_back(sceneData.cameraData.clearColor); return m_state->renderResult; } private: std::shared_ptr m_state; }; class MockPipelineAsset final : public RenderPipelineAsset { public: explicit MockPipelineAsset(std::shared_ptr state) : m_state(std::move(state)) { } std::unique_ptr CreatePipeline() const override { ++m_state->createCalls; m_state->lastCreatedPipelineState = std::make_shared(); return std::make_unique(m_state->lastCreatedPipelineState); } FinalColorSettings GetDefaultFinalColorSettings() const override { return m_state->defaultFinalColorSettings; } private: std::shared_ptr m_state; }; class MockObjectIdPass final : public RenderPass { public: MockObjectIdPass( std::shared_ptr state, bool renderResult = true) : m_state(std::move(state)) , m_renderResult(renderResult) { } const char* GetName() const override { return "MockObjectIdPass"; } bool Execute(const RenderPassContext&) 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 MockScenePass final : public RenderPass { public: MockScenePass( std::shared_ptr state, const char* label, bool initializeResult = true, bool executeResult = true) : m_state(std::move(state)) , m_label(label) , m_initializeResult(initializeResult) , m_executeResult(executeResult) { } const char* GetName() const override { return m_label; } bool Initialize(const RenderContext&) override { m_state->eventLog.push_back(std::string("init:") + m_label); return m_initializeResult; } bool Execute(const RenderPassContext& context) override { m_state->eventLog.push_back(m_label); lastViewportWidth = context.sceneData.cameraData.viewportWidth; lastViewportHeight = context.sceneData.cameraData.viewportHeight; lastClearFlags = context.sceneData.cameraData.clearFlags; lastClearColor = context.sceneData.cameraData.clearColor; lastWorldPosition = context.sceneData.cameraData.worldPosition; lastSurfaceWidth = context.surface.GetRenderAreaWidth(); lastSurfaceHeight = context.surface.GetRenderAreaHeight(); lastHasSourceSurface = context.sourceSurface != nullptr; if (context.sourceSurface != nullptr) { lastSourceSurfaceWidth = context.sourceSurface->GetRenderAreaWidth(); lastSourceSurfaceHeight = context.sourceSurface->GetRenderAreaHeight(); } lastSourceColorView = context.sourceColorView; lastSourceColorState = context.sourceColorState; return m_executeResult; } void Shutdown() override { m_state->eventLog.push_back(std::string("shutdown:") + m_label); } uint32_t lastViewportWidth = 0; uint32_t lastViewportHeight = 0; RenderClearFlags lastClearFlags = RenderClearFlags::All; XCEngine::Math::Color lastClearColor = XCEngine::Math::Color::Black(); XCEngine::Math::Vector3 lastWorldPosition = XCEngine::Math::Vector3::Zero(); uint32_t lastSurfaceWidth = 0; uint32_t lastSurfaceHeight = 0; bool lastHasSourceSurface = false; uint32_t lastSourceSurfaceWidth = 0; uint32_t lastSourceSurfaceHeight = 0; XCEngine::RHI::RHIResourceView* lastSourceColorView = nullptr; XCEngine::RHI::ResourceStates lastSourceColorState = XCEngine::RHI::ResourceStates::Common; private: std::shared_ptr m_state; const char* m_label = ""; bool m_initializeResult = true; bool m_executeResult = true; }; class TrackingPass final : public RenderPass { public: TrackingPass( std::shared_ptr state, const char* label, bool initializeResult = true, bool executeResult = true) : m_state(std::move(state)) , m_label(label) , m_initializeResult(initializeResult) , m_executeResult(executeResult) { } const char* GetName() const override { return m_label; } bool Initialize(const RenderContext&) override { m_state->eventLog.push_back(std::string("init:") + m_label); return m_initializeResult; } bool Execute(const RenderPassContext& context) override { m_state->eventLog.push_back(m_label); lastSurfaceWidth = context.surface.GetRenderAreaWidth(); lastSurfaceHeight = context.surface.GetRenderAreaHeight(); lastHasSourceSurface = context.sourceSurface != nullptr; if (context.sourceSurface != nullptr) { lastSourceSurfaceWidth = context.sourceSurface->GetRenderAreaWidth(); lastSourceSurfaceHeight = context.sourceSurface->GetRenderAreaHeight(); } lastSourceColorView = context.sourceColorView; lastSourceColorState = context.sourceColorState; return m_executeResult; } void Shutdown() override { m_state->eventLog.push_back(std::string("shutdown:") + m_label); } private: std::shared_ptr m_state; const char* m_label = ""; bool m_initializeResult = true; bool m_executeResult = true; public: uint32_t lastSurfaceWidth = 0; uint32_t lastSurfaceHeight = 0; bool lastHasSourceSurface = false; uint32_t lastSourceSurfaceWidth = 0; uint32_t lastSourceSurfaceHeight = 0; XCEngine::RHI::RHIResourceView* lastSourceColorView = nullptr; XCEngine::RHI::ResourceStates lastSourceColorState = XCEngine::RHI::ResourceStates::Common; }; RenderContext CreateValidContext() { RenderContext context; context.device = reinterpret_cast(1); context.commandList = reinterpret_cast(1); context.commandQueue = reinterpret_cast(1); return context; } } // namespace TEST(CameraRenderRequest_Test, ReportsFormalFrameStageContract) { CameraRenderRequest request; RenderPassSequence prePasses; RenderPassSequence postProcessPasses; RenderPassSequence finalOutputPasses; RenderPassSequence postPasses; RenderPassSequence overlayPasses; request.preScenePasses = &prePasses; request.postProcess.passes = &postProcessPasses; request.finalOutput.passes = &finalOutputPasses; request.postScenePasses = &postPasses; request.overlayPasses = &overlayPasses; request.directionalShadow.enabled = true; request.directionalShadow.mapWidth = 128; request.directionalShadow.mapHeight = 64; request.directionalShadow.cameraData.viewportWidth = 128; request.directionalShadow.cameraData.viewportHeight = 64; request.depthOnly.surface = RenderSurface(96, 48); request.depthOnly.surface.SetDepthAttachment(reinterpret_cast(1)); request.postProcess.sourceSurface = RenderSurface(256, 128); request.postProcess.sourceSurface.SetColorAttachment(reinterpret_cast(2)); request.postProcess.sourceColorView = reinterpret_cast(20); request.postProcess.sourceColorState = XCEngine::RHI::ResourceStates::PixelShaderResource; request.postProcess.destinationSurface = RenderSurface(512, 256); request.postProcess.destinationSurface.SetColorAttachment(reinterpret_cast(3)); request.finalOutput.sourceSurface = RenderSurface(512, 256); request.finalOutput.sourceSurface.SetColorAttachment(reinterpret_cast(4)); request.finalOutput.sourceColorView = reinterpret_cast(40); request.finalOutput.sourceColorState = XCEngine::RHI::ResourceStates::PixelShaderResource; request.finalOutput.destinationSurface = RenderSurface(640, 360); request.finalOutput.destinationSurface.SetColorAttachment(reinterpret_cast(5)); request.objectId.surface = RenderSurface(320, 180); request.objectId.surface.SetColorAttachment(reinterpret_cast(6)); request.objectId.surface.SetDepthAttachment(reinterpret_cast(7)); ASSERT_EQ(kOrderedCameraFrameStages.size(), 9u); EXPECT_EQ(kOrderedCameraFrameStages[0].stage, CameraFrameStage::PreScenePasses); EXPECT_EQ(kOrderedCameraFrameStages[1].stage, CameraFrameStage::ShadowCaster); EXPECT_EQ(kOrderedCameraFrameStages[2].stage, CameraFrameStage::DepthOnly); EXPECT_EQ(kOrderedCameraFrameStages[3].stage, CameraFrameStage::MainScene); EXPECT_EQ(kOrderedCameraFrameStages[4].stage, CameraFrameStage::PostProcess); EXPECT_EQ(kOrderedCameraFrameStages[5].stage, CameraFrameStage::FinalOutput); EXPECT_EQ(kOrderedCameraFrameStages[6].stage, CameraFrameStage::ObjectId); EXPECT_EQ(kOrderedCameraFrameStages[7].stage, CameraFrameStage::PostScenePasses); EXPECT_EQ(kOrderedCameraFrameStages[8].stage, CameraFrameStage::OverlayPasses); EXPECT_STREQ(GetCameraFrameStageName(CameraFrameStage::MainScene), "MainScene"); EXPECT_STREQ(GetCameraFrameStageName(CameraFrameStage::PostProcess), "PostProcess"); EXPECT_STREQ(GetCameraFrameStageName(CameraFrameStage::FinalOutput), "FinalOutput"); EXPECT_STREQ(GetCameraFrameStageName(CameraFrameStage::OverlayPasses), "OverlayPasses"); EXPECT_TRUE(request.HasFrameStage(CameraFrameStage::PreScenePasses)); EXPECT_TRUE(request.HasFrameStage(CameraFrameStage::ShadowCaster)); EXPECT_TRUE(request.HasFrameStage(CameraFrameStage::DepthOnly)); EXPECT_TRUE(request.HasFrameStage(CameraFrameStage::MainScene)); EXPECT_TRUE(request.HasFrameStage(CameraFrameStage::PostProcess)); EXPECT_TRUE(request.HasFrameStage(CameraFrameStage::FinalOutput)); EXPECT_TRUE(request.HasFrameStage(CameraFrameStage::ObjectId)); EXPECT_TRUE(request.HasFrameStage(CameraFrameStage::PostScenePasses)); EXPECT_TRUE(request.HasFrameStage(CameraFrameStage::OverlayPasses)); EXPECT_EQ(request.GetPassSequence(CameraFrameStage::PreScenePasses), &prePasses); EXPECT_EQ(request.GetPassSequence(CameraFrameStage::PostProcess), &postProcessPasses); EXPECT_EQ(request.GetPassSequence(CameraFrameStage::FinalOutput), &finalOutputPasses); EXPECT_EQ(request.GetPassSequence(CameraFrameStage::PostScenePasses), &postPasses); EXPECT_EQ(request.GetPassSequence(CameraFrameStage::OverlayPasses), &overlayPasses); EXPECT_EQ(request.GetScenePassRequest(CameraFrameStage::ShadowCaster), &request.shadowCaster); EXPECT_EQ(request.GetScenePassRequest(CameraFrameStage::DepthOnly), &request.depthOnly); EXPECT_EQ(request.GetScenePassRequest(CameraFrameStage::MainScene), nullptr); EXPECT_EQ(request.GetObjectIdRequest(CameraFrameStage::ObjectId), &request.objectId); EXPECT_EQ(request.GetObjectIdRequest(CameraFrameStage::MainScene), nullptr); EXPECT_TRUE(request.RequiresIntermediateSceneColor()); EXPECT_EQ(request.GetMainSceneSurface().GetRenderAreaWidth(), 256u); EXPECT_EQ(request.GetMainSceneSurface().GetRenderAreaHeight(), 128u); EXPECT_EQ(request.GetFinalCompositedSurface().GetRenderAreaWidth(), 640u); EXPECT_EQ(request.GetFinalCompositedSurface().GetRenderAreaHeight(), 360u); ASSERT_NE(request.GetOutputSurface(CameraFrameStage::PostProcess), nullptr); EXPECT_EQ(request.GetOutputSurface(CameraFrameStage::PostProcess)->GetRenderAreaWidth(), 512u); ASSERT_NE(request.GetSourceSurface(CameraFrameStage::PostProcess), nullptr); EXPECT_EQ(request.GetSourceSurface(CameraFrameStage::PostProcess)->GetRenderAreaWidth(), 256u); EXPECT_EQ( request.GetSourceColorView(CameraFrameStage::PostProcess), reinterpret_cast(20)); EXPECT_EQ( request.GetSourceColorState(CameraFrameStage::PostProcess), XCEngine::RHI::ResourceStates::PixelShaderResource); ASSERT_NE(request.GetSourceSurface(CameraFrameStage::FinalOutput), nullptr); EXPECT_EQ(request.GetSourceSurface(CameraFrameStage::FinalOutput)->GetRenderAreaWidth(), 512u); EXPECT_EQ( request.GetSourceColorView(CameraFrameStage::FinalOutput), reinterpret_cast(40)); EXPECT_EQ( request.GetSourceColorState(CameraFrameStage::FinalOutput), XCEngine::RHI::ResourceStates::PixelShaderResource); } TEST(CameraRenderRequest_Test, RejectsFullscreenRequestWithoutReadableSourceStateWhenAutoTransitionIsDisabled) { RenderPassSequence passes; FullscreenPassRenderRequest request = {}; request.sourceSurface = RenderSurface(256, 128); request.sourceSurface.SetColorAttachment(reinterpret_cast(1)); request.sourceSurface.SetAutoTransitionEnabled(false); request.sourceColorView = reinterpret_cast(2); request.sourceColorState = XCEngine::RHI::ResourceStates::RenderTarget; request.destinationSurface = RenderSurface(512, 256); request.destinationSurface.SetColorAttachment(reinterpret_cast(3)); request.passes = &passes; EXPECT_FALSE(request.IsValid()); request.sourceColorState = XCEngine::RHI::ResourceStates::PixelShaderResource; EXPECT_TRUE(request.IsValid()); } TEST(CameraRenderRequest_Test, AcceptsAutoTransitionedFullscreenSourceAndRejectsMultisampledSource) { RenderPassSequence passes; FullscreenPassRenderRequest request = {}; request.sourceSurface = RenderSurface(256, 128); request.sourceSurface.SetColorAttachment(reinterpret_cast(1)); request.sourceColorView = reinterpret_cast(2); request.sourceColorState = XCEngine::RHI::ResourceStates::RenderTarget; request.destinationSurface = RenderSurface(512, 256); request.destinationSurface.SetColorAttachment(reinterpret_cast(3)); request.passes = &passes; EXPECT_TRUE(request.IsValid()); request.sourceSurface.SetSampleDesc(4u, 0u); EXPECT_FALSE(request.IsValid()); } TEST(CameraRenderer_Test, UsesOverrideCameraAndSurfaceSizeWhenSubmittingScene) { Scene scene("CameraRendererScene"); GameObject* primaryCameraObject = scene.CreateGameObject("PrimaryCamera"); auto* primaryCamera = primaryCameraObject->AddComponent(); primaryCamera->SetPrimary(true); primaryCamera->SetDepth(10.0f); GameObject* overrideCameraObject = scene.CreateGameObject("OverrideCamera"); auto* overrideCamera = overrideCameraObject->AddComponent(); overrideCamera->SetPrimary(false); overrideCamera->SetDepth(-1.0f); auto state = std::make_shared(); CameraRenderer renderer(std::make_unique(state)); CameraRenderRequest request; request.scene = &scene; request.camera = overrideCamera; request.context = CreateValidContext(); request.surface = RenderSurface(640, 480); request.cameraDepth = overrideCamera->GetDepth(); request.clearFlags = RenderClearFlags::None; ASSERT_TRUE(renderer.Render(request)); EXPECT_EQ(state->renderCalls, 1); EXPECT_EQ(state->lastSurfaceWidth, 640u); EXPECT_EQ(state->lastSurfaceHeight, 480u); EXPECT_EQ(state->lastRenderAreaX, 0); EXPECT_EQ(state->lastRenderAreaY, 0); EXPECT_EQ(state->lastRenderAreaWidth, 640); EXPECT_EQ(state->lastRenderAreaHeight, 480); EXPECT_EQ(state->lastCameraViewportWidth, 640u); EXPECT_EQ(state->lastCameraViewportHeight, 480u); EXPECT_EQ(state->lastCamera, overrideCamera); EXPECT_NE(state->lastCamera, primaryCamera); EXPECT_EQ(state->lastVisibleItemCount, 0u); EXPECT_EQ(state->lastClearFlags, RenderClearFlags::None); EXPECT_FLOAT_EQ(state->lastClearColor.r, overrideCamera->GetClearColor().r); EXPECT_FLOAT_EQ(state->lastClearColor.g, overrideCamera->GetClearColor().g); EXPECT_FLOAT_EQ(state->lastClearColor.b, overrideCamera->GetClearColor().b); EXPECT_FLOAT_EQ(state->lastClearColor.a, overrideCamera->GetClearColor().a); } TEST(CameraRenderer_Test, AppliesRequestClearColorOverrideToSceneData) { Scene scene("CameraRendererClearColorOverrideScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(1.0f); camera->SetClearColor(XCEngine::Math::Color(0.05f, 0.10f, 0.15f, 1.0f)); auto state = std::make_shared(); CameraRenderer renderer(std::make_unique(state)); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = CreateValidContext(); request.surface = RenderSurface(320, 200); request.hasClearColorOverride = true; request.clearColorOverride = XCEngine::Math::Color(0.27f, 0.27f, 0.27f, 1.0f); ASSERT_TRUE(renderer.Render(request)); EXPECT_FLOAT_EQ(state->lastClearColor.r, 0.27f); EXPECT_FLOAT_EQ(state->lastClearColor.g, 0.27f); EXPECT_FLOAT_EQ(state->lastClearColor.b, 0.27f); EXPECT_FLOAT_EQ(state->lastClearColor.a, 1.0f); } TEST(CameraRenderer_Test, PromotesSkyboxMaterialIntoEnvironmentFrameData) { Scene scene("CameraRendererSkyboxMaterialScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetSkyboxEnabled(true); XCEngine::Resources::Material skyboxMaterial; camera->SetSkyboxMaterial(&skyboxMaterial); auto state = std::make_shared(); CameraRenderer renderer(std::make_unique(state)); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = CreateValidContext(); request.surface = RenderSurface(320, 200); request.surface.SetDepthAttachment(reinterpret_cast(1)); ASSERT_TRUE(renderer.Render(request)); EXPECT_TRUE(state->lastHasSkybox); EXPECT_EQ(state->lastEnvironmentMode, RenderEnvironmentMode::MaterialSkybox); EXPECT_EQ(state->lastSkyboxMaterial, &skyboxMaterial); } TEST(CameraRenderer_Test, ExecutesInjectedPreAndPostPassSequencesAroundPipelineRender) { Scene scene("CameraRendererPassScene"); 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)); 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; ASSERT_TRUE(renderer.Render(request)); EXPECT_EQ( state->eventLog, (std::vector{ "init:pre", "pre", "pipeline", "init:post", "post" })); } 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" })); } TEST(CameraRenderer_Test, RoutesSceneColorThroughPostProcessAndFinalOutputStages) { Scene scene("CameraRendererPostProcessScene"); 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)); auto postProcessPass = std::make_unique(state, "postProcess"); MockScenePass* postProcessPassRaw = postProcessPass.get(); RenderPassSequence postProcessPasses; postProcessPasses.AddPass(std::move(postProcessPass)); auto finalOutputPass = std::make_unique(state, "finalOutput"); MockScenePass* finalOutputPassRaw = finalOutputPass.get(); RenderPassSequence finalOutputPasses; finalOutputPasses.AddPass(std::move(finalOutputPass)); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = CreateValidContext(); request.surface = RenderSurface(800, 600); request.surface.SetColorAttachment(reinterpret_cast(1)); request.cameraDepth = camera->GetDepth(); request.postProcess.sourceSurface = RenderSurface(256, 128); request.postProcess.sourceSurface.SetColorAttachment(reinterpret_cast(2)); request.postProcess.sourceColorView = reinterpret_cast(20); request.postProcess.destinationSurface = RenderSurface(512, 256); request.postProcess.destinationSurface.SetColorAttachment(reinterpret_cast(3)); request.postProcess.passes = &postProcessPasses; request.finalOutput.sourceSurface = request.postProcess.destinationSurface; request.finalOutput.sourceColorView = reinterpret_cast(30); request.finalOutput.destinationSurface = request.surface; request.finalOutput.passes = &finalOutputPasses; ASSERT_TRUE(renderer.Render(request)); EXPECT_EQ(state->lastSurfaceWidth, 256u); EXPECT_EQ(state->lastSurfaceHeight, 128u); EXPECT_EQ(state->lastCameraViewportWidth, 256u); EXPECT_EQ(state->lastCameraViewportHeight, 128u); ASSERT_NE(postProcessPassRaw, nullptr); EXPECT_TRUE(postProcessPassRaw->lastHasSourceSurface); EXPECT_EQ(postProcessPassRaw->lastSourceSurfaceWidth, 256u); EXPECT_EQ(postProcessPassRaw->lastSourceSurfaceHeight, 128u); EXPECT_EQ( postProcessPassRaw->lastSourceColorView, reinterpret_cast(20)); EXPECT_EQ(postProcessPassRaw->lastSurfaceWidth, 512u); EXPECT_EQ(postProcessPassRaw->lastSurfaceHeight, 256u); ASSERT_NE(finalOutputPassRaw, nullptr); EXPECT_TRUE(finalOutputPassRaw->lastHasSourceSurface); EXPECT_EQ(finalOutputPassRaw->lastSourceSurfaceWidth, 512u); EXPECT_EQ(finalOutputPassRaw->lastSourceSurfaceHeight, 256u); EXPECT_EQ( finalOutputPassRaw->lastSourceColorView, reinterpret_cast(30)); EXPECT_EQ(finalOutputPassRaw->lastSurfaceWidth, 800u); EXPECT_EQ(finalOutputPassRaw->lastSurfaceHeight, 600u); EXPECT_EQ( state->eventLog, (std::vector{ "pipeline", "init:postProcess", "postProcess", "init:finalOutput", "finalOutput" })); } TEST(CameraRenderer_Test, ChainsMultiPassPostProcessThroughIntermediateSurface) { Scene scene("CameraRendererMultiPassPostProcessScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(4.0f); auto pipelineState = std::make_shared(); auto allocationState = std::make_shared(); MockShadowDevice device(allocationState); CameraRenderer renderer(std::make_unique(pipelineState)); auto firstPass = std::make_unique(pipelineState, "postProcessTint"); MockScenePass* firstPassRaw = firstPass.get(); auto secondPass = std::make_unique(pipelineState, "postProcessComposite"); MockScenePass* secondPassRaw = secondPass.get(); RenderPassSequence postProcessPasses; postProcessPasses.AddPass(std::move(firstPass)); postProcessPasses.AddPass(std::move(secondPass)); auto* sourceColorAttachment = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::RenderTarget, XCEngine::RHI::Format::R8G8B8A8_UNorm, XCEngine::RHI::ResourceViewDimension::Texture2D); auto* sourceColorShaderView = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::ShaderResource, XCEngine::RHI::Format::R8G8B8A8_UNorm, XCEngine::RHI::ResourceViewDimension::Texture2D); auto* destinationColorAttachment = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::RenderTarget, XCEngine::RHI::Format::R8G8B8A8_UNorm, XCEngine::RHI::ResourceViewDimension::Texture2D); RenderContext context = CreateValidContext(); context.device = &device; CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = context; request.surface = RenderSurface(512, 256); request.surface.SetColorAttachment(destinationColorAttachment); request.cameraDepth = camera->GetDepth(); request.postProcess.sourceSurface = RenderSurface(256, 128); request.postProcess.sourceSurface.SetColorAttachment(sourceColorAttachment); request.postProcess.sourceColorView = sourceColorShaderView; request.postProcess.destinationSurface = RenderSurface(512, 256); request.postProcess.destinationSurface.SetColorAttachment(destinationColorAttachment); request.postProcess.passes = &postProcessPasses; ASSERT_TRUE(renderer.Render(request)); ASSERT_NE(firstPassRaw, nullptr); EXPECT_TRUE(firstPassRaw->lastHasSourceSurface); EXPECT_EQ(firstPassRaw->lastSourceSurfaceWidth, 256u); EXPECT_EQ(firstPassRaw->lastSourceSurfaceHeight, 128u); EXPECT_EQ(firstPassRaw->lastSourceColorView, sourceColorShaderView); EXPECT_EQ(firstPassRaw->lastSurfaceWidth, 512u); EXPECT_EQ(firstPassRaw->lastSurfaceHeight, 256u); ASSERT_NE(secondPassRaw, nullptr); EXPECT_TRUE(secondPassRaw->lastHasSourceSurface); EXPECT_EQ(secondPassRaw->lastSourceSurfaceWidth, 512u); EXPECT_EQ(secondPassRaw->lastSourceSurfaceHeight, 256u); EXPECT_NE(secondPassRaw->lastSourceColorView, nullptr); EXPECT_NE(secondPassRaw->lastSourceColorView, sourceColorShaderView); EXPECT_EQ(secondPassRaw->lastSurfaceWidth, 512u); EXPECT_EQ(secondPassRaw->lastSurfaceHeight, 256u); EXPECT_EQ(allocationState->createTextureCalls, 1); EXPECT_EQ(allocationState->createRenderTargetViewCalls, 1); EXPECT_EQ(allocationState->createShaderViewCalls, 1); EXPECT_EQ( pipelineState->eventLog, (std::vector{ "pipeline", "init:postProcessTint", "init:postProcessComposite", "postProcessTint", "postProcessComposite" })); delete destinationColorAttachment; delete sourceColorShaderView; delete sourceColorAttachment; } TEST(CameraRenderer_Test, KeepsPostProcessAndFinalOutputScratchSurfacesIndependentPerStage) { Scene scene("CameraRendererIndependentFullscreenStagesScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(5.0f); auto pipelineState = std::make_shared(); auto allocationState = std::make_shared(); MockShadowDevice device(allocationState); CameraRenderer renderer(std::make_unique(pipelineState)); auto postFirstPass = std::make_unique(pipelineState, "postA"); auto postSecondPass = std::make_unique(pipelineState, "postB"); MockScenePass* postSecondPassRaw = postSecondPass.get(); RenderPassSequence postProcessPasses; postProcessPasses.AddPass(std::move(postFirstPass)); postProcessPasses.AddPass(std::move(postSecondPass)); auto finalFirstPass = std::make_unique(pipelineState, "finalA"); MockScenePass* finalFirstPassRaw = finalFirstPass.get(); auto finalSecondPass = std::make_unique(pipelineState, "finalB"); MockScenePass* finalSecondPassRaw = finalSecondPass.get(); RenderPassSequence finalOutputPasses; finalOutputPasses.AddPass(std::move(finalFirstPass)); finalOutputPasses.AddPass(std::move(finalSecondPass)); auto* sourceColorAttachment = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::RenderTarget, XCEngine::RHI::Format::R8G8B8A8_UNorm, XCEngine::RHI::ResourceViewDimension::Texture2D); auto* sourceColorShaderView = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::ShaderResource, XCEngine::RHI::Format::R8G8B8A8_UNorm, XCEngine::RHI::ResourceViewDimension::Texture2D); auto* postProcessDestination = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::RenderTarget, XCEngine::RHI::Format::R8G8B8A8_UNorm, XCEngine::RHI::ResourceViewDimension::Texture2D); auto* finalOutputSource = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::ShaderResource, XCEngine::RHI::Format::R8G8B8A8_UNorm, XCEngine::RHI::ResourceViewDimension::Texture2D); auto* finalOutputDestination = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::RenderTarget, XCEngine::RHI::Format::R8G8B8A8_UNorm, XCEngine::RHI::ResourceViewDimension::Texture2D); RenderContext context = CreateValidContext(); context.device = &device; CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = context; request.surface = RenderSurface(800, 600); request.surface.SetColorAttachment(finalOutputDestination); request.cameraDepth = camera->GetDepth(); request.postProcess.sourceSurface = RenderSurface(256, 128); request.postProcess.sourceSurface.SetColorAttachment(sourceColorAttachment); request.postProcess.sourceColorView = sourceColorShaderView; request.postProcess.destinationSurface = RenderSurface(512, 256); request.postProcess.destinationSurface.SetColorAttachment(postProcessDestination); request.postProcess.passes = &postProcessPasses; request.finalOutput.sourceSurface = request.postProcess.destinationSurface; request.finalOutput.sourceColorView = finalOutputSource; request.finalOutput.destinationSurface = RenderSurface(800, 600); request.finalOutput.destinationSurface.SetColorAttachment(finalOutputDestination); request.finalOutput.passes = &finalOutputPasses; ASSERT_TRUE(renderer.Render(request)); ASSERT_NE(postSecondPassRaw, nullptr); EXPECT_EQ(postSecondPassRaw->lastSourceSurfaceWidth, 512u); EXPECT_EQ(postSecondPassRaw->lastSourceSurfaceHeight, 256u); ASSERT_NE(finalFirstPassRaw, nullptr); EXPECT_EQ(finalFirstPassRaw->lastSourceSurfaceWidth, 512u); EXPECT_EQ(finalFirstPassRaw->lastSourceSurfaceHeight, 256u); EXPECT_EQ(finalFirstPassRaw->lastSourceColorView, finalOutputSource); ASSERT_NE(finalSecondPassRaw, nullptr); EXPECT_EQ(finalSecondPassRaw->lastSourceSurfaceWidth, 800u); EXPECT_EQ(finalSecondPassRaw->lastSourceSurfaceHeight, 600u); EXPECT_NE(finalSecondPassRaw->lastSourceColorView, nullptr); EXPECT_NE(finalSecondPassRaw->lastSourceColorView, finalOutputSource); EXPECT_EQ(allocationState->createTextureCalls, 2); EXPECT_EQ(allocationState->createRenderTargetViewCalls, 2); EXPECT_EQ(allocationState->createShaderViewCalls, 2); EXPECT_EQ(allocationState->shutdownTextureCalls, 0); EXPECT_EQ(allocationState->shutdownRenderTargetViewCalls, 0); EXPECT_EQ(allocationState->shutdownShaderViewCalls, 0); EXPECT_EQ( pipelineState->eventLog, (std::vector{ "pipeline", "init:postA", "init:postB", "postA", "postB", "init:finalA", "init:finalB", "finalA", "finalB" })); delete finalOutputDestination; delete finalOutputSource; delete postProcessDestination; delete sourceColorShaderView; delete sourceColorAttachment; } TEST(CameraRenderer_Test, ExecutesFormalFrameStagesInDocumentedOrder) { Scene scene("CameraRendererFormalFrameStageScene"); 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)); auto shadowPass = std::make_unique(state, "shadowCaster"); renderer.SetShadowCasterPass(std::move(shadowPass)); auto depthPass = std::make_unique(state, "depthOnly"); renderer.SetDepthOnlyPass(std::move(depthPass)); RenderPassSequence prePasses; prePasses.AddPass(std::make_unique(state, "pre")); RenderPassSequence postProcessPasses; postProcessPasses.AddPass(std::make_unique(state, "postProcess")); RenderPassSequence finalOutputPasses; finalOutputPasses.AddPass(std::make_unique(state, "finalOutput")); RenderPassSequence postPasses; postPasses.AddPass(std::make_unique(state, "post")); RenderPassSequence overlayPasses; overlayPasses.AddPass(std::make_unique(state, "overlay")); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = CreateValidContext(); request.surface = RenderSurface(320, 180); request.surface.SetColorAttachment(reinterpret_cast(3)); request.cameraDepth = camera->GetDepth(); request.preScenePasses = &prePasses; request.postProcess.passes = &postProcessPasses; request.finalOutput.passes = &finalOutputPasses; request.postScenePasses = &postPasses; request.overlayPasses = &overlayPasses; request.shadowCaster.surface = RenderSurface(128, 64); request.shadowCaster.surface.SetDepthAttachment(reinterpret_cast(1)); request.depthOnly.surface = RenderSurface(96, 48); request.depthOnly.surface.SetDepthAttachment(reinterpret_cast(2)); request.postProcess.sourceSurface = RenderSurface(256, 128); request.postProcess.sourceSurface.SetColorAttachment(reinterpret_cast(4)); request.postProcess.sourceColorView = reinterpret_cast(40); request.postProcess.destinationSurface = RenderSurface(320, 180); request.postProcess.destinationSurface.SetColorAttachment(reinterpret_cast(5)); request.finalOutput.sourceSurface = request.postProcess.destinationSurface; request.finalOutput.sourceColorView = reinterpret_cast(50); request.finalOutput.destinationSurface = request.surface; request.objectId.surface = RenderSurface(320, 180); request.objectId.surface.SetColorAttachment(reinterpret_cast(6)); request.objectId.surface.SetDepthAttachment(reinterpret_cast(7)); ASSERT_TRUE(renderer.Render(request)); EXPECT_EQ( state->eventLog, (std::vector{ "init:pre", "pre", "init:shadowCaster", "shadowCaster", "init:depthOnly", "depthOnly", "pipeline", "init:postProcess", "postProcess", "init:finalOutput", "finalOutput", "objectId", "init:post", "post", "init:overlay", "overlay" })); } TEST(CameraRenderer_Test, ExecutesShadowCasterAndDepthOnlyRequestsBeforeMainPipeline) { Scene scene("CameraRendererDepthAndShadowScene"); 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)); auto shadowPass = std::make_unique(state, "shadowCaster"); MockScenePass* shadowPassRaw = shadowPass.get(); renderer.SetShadowCasterPass(std::move(shadowPass)); auto depthPass = std::make_unique(state, "depthOnly"); MockScenePass* depthPassRaw = depthPass.get(); renderer.SetDepthOnlyPass(std::move(depthPass)); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = CreateValidContext(); request.surface = RenderSurface(320, 180); request.cameraDepth = camera->GetDepth(); request.shadowCaster.surface = RenderSurface(128, 64); request.shadowCaster.surface.SetDepthAttachment(reinterpret_cast(2)); request.shadowCaster.hasCameraDataOverride = true; request.shadowCaster.cameraDataOverride.worldPosition = XCEngine::Math::Vector3(7.0f, 8.0f, 9.0f); request.depthOnly.surface = RenderSurface(96, 48); request.depthOnly.surface.SetDepthAttachment(reinterpret_cast(4)); request.depthOnly.hasClearColorOverride = true; request.depthOnly.clearColorOverride = XCEngine::Math::Color(0.3f, 0.2f, 0.1f, 1.0f); ASSERT_TRUE(renderer.Render(request)); EXPECT_EQ( state->eventLog, (std::vector{ "init:shadowCaster", "shadowCaster", "init:depthOnly", "depthOnly", "pipeline" })); EXPECT_EQ(shadowPassRaw->lastViewportWidth, 128u); EXPECT_EQ(shadowPassRaw->lastViewportHeight, 64u); EXPECT_EQ(shadowPassRaw->lastSurfaceWidth, 128u); EXPECT_EQ(shadowPassRaw->lastSurfaceHeight, 64u); EXPECT_EQ(shadowPassRaw->lastClearFlags, RenderClearFlags::Depth); EXPECT_EQ(shadowPassRaw->lastWorldPosition, XCEngine::Math::Vector3(7.0f, 8.0f, 9.0f)); EXPECT_EQ(depthPassRaw->lastViewportWidth, 96u); EXPECT_EQ(depthPassRaw->lastViewportHeight, 48u); EXPECT_EQ(depthPassRaw->lastClearFlags, RenderClearFlags::Depth); EXPECT_FLOAT_EQ(depthPassRaw->lastClearColor.r, 0.3f); EXPECT_FLOAT_EQ(depthPassRaw->lastClearColor.g, 0.2f); EXPECT_FLOAT_EQ(depthPassRaw->lastClearColor.b, 0.1f); EXPECT_FLOAT_EQ(depthPassRaw->lastClearColor.a, 1.0f); } TEST(CameraRenderer_Test, AutoAllocatesDirectionalShadowSurfaceFromShadowPlan) { Scene scene("CameraRendererAutoDirectionalShadowScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); auto pipelineState = std::make_shared(); auto allocationState = std::make_shared(); MockShadowDevice device(allocationState); RenderContext context = CreateValidContext(); context.device = &device; { CameraRenderer renderer( std::make_unique(pipelineState), std::make_unique(pipelineState)); auto shadowPass = std::make_unique(pipelineState, "shadowCaster"); MockScenePass* shadowPassRaw = shadowPass.get(); renderer.SetShadowCasterPass(std::move(shadowPass)); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = context; request.surface = RenderSurface(320, 180); request.cameraDepth = camera->GetDepth(); request.directionalShadow.enabled = true; request.directionalShadow.mapWidth = 256; request.directionalShadow.mapHeight = 128; request.directionalShadow.cameraData.viewportWidth = 256; request.directionalShadow.cameraData.viewportHeight = 128; request.directionalShadow.cameraData.clearFlags = RenderClearFlags::Depth; request.directionalShadow.cameraData.worldPosition = XCEngine::Math::Vector3(3.0f, 4.0f, 5.0f); request.directionalShadow.cameraData.viewProjection = XCEngine::Math::Matrix4x4::Translation(XCEngine::Math::Vector3(11.0f, 12.0f, 13.0f)); ASSERT_TRUE(renderer.Render(request)); EXPECT_EQ( pipelineState->eventLog, (std::vector{ "init:shadowCaster", "shadowCaster", "pipeline" })); EXPECT_EQ(shadowPassRaw->lastViewportWidth, 256u); EXPECT_EQ(shadowPassRaw->lastViewportHeight, 128u); EXPECT_EQ(shadowPassRaw->lastSurfaceWidth, 256u); EXPECT_EQ(shadowPassRaw->lastSurfaceHeight, 128u); EXPECT_EQ(shadowPassRaw->lastClearFlags, RenderClearFlags::Depth); EXPECT_EQ(shadowPassRaw->lastWorldPosition, XCEngine::Math::Vector3(3.0f, 4.0f, 5.0f)); EXPECT_EQ(allocationState->createTextureCalls, 1); EXPECT_EQ(allocationState->createDepthViewCalls, 1); EXPECT_EQ(allocationState->createShaderViewCalls, 1); EXPECT_EQ(allocationState->lastTextureWidth, 256u); EXPECT_EQ(allocationState->lastTextureHeight, 128u); EXPECT_EQ(allocationState->lastTextureFormat, XCEngine::RHI::Format::D32_Float); EXPECT_EQ(allocationState->lastDepthViewFormat, XCEngine::RHI::Format::D32_Float); EXPECT_EQ(allocationState->lastShaderViewFormat, XCEngine::RHI::Format::Unknown); EXPECT_EQ(allocationState->shutdownDepthViewCalls, 0); EXPECT_EQ(allocationState->shutdownShaderViewCalls, 0); EXPECT_EQ(allocationState->shutdownTextureCalls, 0); EXPECT_EQ(allocationState->destroyDepthViewCalls, 0); EXPECT_EQ(allocationState->destroyShaderViewCalls, 0); EXPECT_EQ(allocationState->destroyTextureCalls, 0); EXPECT_TRUE(pipelineState->lastHasMainDirectionalShadow); EXPECT_NE(pipelineState->lastShadowMap, nullptr); EXPECT_FLOAT_EQ(pipelineState->lastShadowViewProjection.m[0][3], 11.0f); EXPECT_FLOAT_EQ(pipelineState->lastShadowViewProjection.m[1][3], 12.0f); EXPECT_FLOAT_EQ(pipelineState->lastShadowViewProjection.m[2][3], 13.0f); EXPECT_FLOAT_EQ(pipelineState->lastShadowSampling.settings.receiverDepthBias, 0.0015f); EXPECT_FLOAT_EQ(pipelineState->lastShadowMapMetrics.inverseMapSize.x, 1.0f / 256.0f); EXPECT_FLOAT_EQ(pipelineState->lastShadowMapMetrics.inverseMapSize.y, 1.0f / 128.0f); EXPECT_FLOAT_EQ(pipelineState->lastShadowSampling.settings.shadowStrength, 0.85f); EXPECT_TRUE(pipelineState->lastHasMainDirectionalShadowKeyword); } EXPECT_EQ(allocationState->shutdownDepthViewCalls, 1); EXPECT_EQ(allocationState->shutdownShaderViewCalls, 1); EXPECT_EQ(allocationState->shutdownTextureCalls, 1); EXPECT_EQ(allocationState->destroyDepthViewCalls, 1); EXPECT_EQ(allocationState->destroyShaderViewCalls, 1); EXPECT_EQ(allocationState->destroyTextureCalls, 1); } TEST(CameraRenderer_Test, ReusesDirectionalShadowSurfaceWhenPlanMatches) { Scene scene("CameraRendererDirectionalShadowReuseScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); auto pipelineState = std::make_shared(); auto allocationState = std::make_shared(); MockShadowDevice device(allocationState); RenderContext context = CreateValidContext(); context.device = &device; CameraRenderer renderer( std::make_unique(pipelineState), std::make_unique(pipelineState)); auto shadowPass = std::make_unique(pipelineState, "shadowCaster"); renderer.SetShadowCasterPass(std::move(shadowPass)); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = context; request.surface = RenderSurface(320, 180); request.cameraDepth = camera->GetDepth(); request.directionalShadow.enabled = true; request.directionalShadow.mapWidth = 256; request.directionalShadow.mapHeight = 128; request.directionalShadow.cameraData.viewportWidth = 256; request.directionalShadow.cameraData.viewportHeight = 128; request.directionalShadow.cameraData.clearFlags = RenderClearFlags::Depth; ASSERT_TRUE(renderer.Render(request)); XCEngine::RHI::RHIResourceView* firstShadowMap = pipelineState->lastShadowMap; ASSERT_NE(firstShadowMap, nullptr); EXPECT_EQ(allocationState->createTextureCalls, 1); EXPECT_EQ(allocationState->createDepthViewCalls, 1); EXPECT_EQ(allocationState->createShaderViewCalls, 1); EXPECT_EQ(allocationState->shutdownDepthViewCalls, 0); EXPECT_EQ(allocationState->destroyTextureCalls, 0); ASSERT_TRUE(renderer.Render(request)); EXPECT_EQ(allocationState->createTextureCalls, 1); EXPECT_EQ(allocationState->createDepthViewCalls, 1); EXPECT_EQ(allocationState->createShaderViewCalls, 1); EXPECT_EQ(allocationState->shutdownDepthViewCalls, 0); EXPECT_EQ(allocationState->shutdownShaderViewCalls, 0); EXPECT_EQ(allocationState->shutdownTextureCalls, 0); EXPECT_EQ(allocationState->destroyDepthViewCalls, 0); EXPECT_EQ(allocationState->destroyShaderViewCalls, 0); EXPECT_EQ(allocationState->destroyTextureCalls, 0); EXPECT_EQ(pipelineState->lastShadowMap, firstShadowMap); } TEST(CameraRenderer_Test, RecreatesDirectionalShadowSurfaceWhenPlanSizeChanges) { Scene scene("CameraRendererDirectionalShadowResizeScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); auto pipelineState = std::make_shared(); auto allocationState = std::make_shared(); MockShadowDevice device(allocationState); RenderContext context = CreateValidContext(); context.device = &device; { CameraRenderer renderer( std::make_unique(pipelineState), std::make_unique(pipelineState)); auto shadowPass = std::make_unique(pipelineState, "shadowCaster"); renderer.SetShadowCasterPass(std::move(shadowPass)); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = context; request.surface = RenderSurface(320, 180); request.cameraDepth = camera->GetDepth(); request.directionalShadow.enabled = true; request.directionalShadow.mapWidth = 256; request.directionalShadow.mapHeight = 128; request.directionalShadow.cameraData.viewportWidth = 256; request.directionalShadow.cameraData.viewportHeight = 128; request.directionalShadow.cameraData.clearFlags = RenderClearFlags::Depth; ASSERT_TRUE(renderer.Render(request)); XCEngine::RHI::RHIResourceView* firstShadowMap = pipelineState->lastShadowMap; ASSERT_NE(firstShadowMap, nullptr); request.directionalShadow.mapWidth = 512; request.directionalShadow.mapHeight = 256; request.directionalShadow.cameraData.viewportWidth = 512; request.directionalShadow.cameraData.viewportHeight = 256; ASSERT_TRUE(renderer.Render(request)); EXPECT_EQ(allocationState->createTextureCalls, 2); EXPECT_EQ(allocationState->createDepthViewCalls, 2); EXPECT_EQ(allocationState->createShaderViewCalls, 2); EXPECT_EQ(allocationState->shutdownDepthViewCalls, 1); EXPECT_EQ(allocationState->shutdownShaderViewCalls, 1); EXPECT_EQ(allocationState->shutdownTextureCalls, 1); EXPECT_EQ(allocationState->destroyDepthViewCalls, 1); EXPECT_EQ(allocationState->destroyShaderViewCalls, 1); EXPECT_EQ(allocationState->destroyTextureCalls, 1); EXPECT_NE(pipelineState->lastShadowMap, firstShadowMap); EXPECT_EQ(allocationState->lastTextureWidth, 512u); EXPECT_EQ(allocationState->lastTextureHeight, 256u); EXPECT_EQ(allocationState->lastTextureFormat, XCEngine::RHI::Format::D32_Float); } EXPECT_EQ(allocationState->shutdownDepthViewCalls, 2); EXPECT_EQ(allocationState->shutdownShaderViewCalls, 2); EXPECT_EQ(allocationState->shutdownTextureCalls, 2); EXPECT_EQ(allocationState->destroyDepthViewCalls, 2); EXPECT_EQ(allocationState->destroyShaderViewCalls, 2); EXPECT_EQ(allocationState->destroyTextureCalls, 2); } TEST(CameraRenderer_Test, EnablesDirectionalShadowSurfaceAfterInitialFrameWithoutShadows) { Scene scene("CameraRendererDirectionalShadowToggleScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); auto pipelineState = std::make_shared(); auto allocationState = std::make_shared(); MockShadowDevice device(allocationState); RenderContext context = CreateValidContext(); context.device = &device; { CameraRenderer renderer( std::make_unique(pipelineState), std::make_unique(pipelineState)); auto shadowPass = std::make_unique(pipelineState, "shadowCaster"); renderer.SetShadowCasterPass(std::move(shadowPass)); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = context; request.surface = RenderSurface(320, 180); request.cameraDepth = camera->GetDepth(); ASSERT_TRUE(renderer.Render(request)); EXPECT_EQ(pipelineState->eventLog, (std::vector{ "pipeline" })); EXPECT_FALSE(pipelineState->lastHasMainDirectionalShadow); EXPECT_EQ(pipelineState->lastShadowMap, nullptr); EXPECT_EQ(allocationState->createTextureCalls, 0); EXPECT_EQ(allocationState->createDepthViewCalls, 0); EXPECT_EQ(allocationState->createShaderViewCalls, 0); pipelineState->eventLog.clear(); request.directionalShadow.enabled = true; request.directionalShadow.mapWidth = 256; request.directionalShadow.mapHeight = 128; request.directionalShadow.cameraData.viewportWidth = 256; request.directionalShadow.cameraData.viewportHeight = 128; request.directionalShadow.cameraData.clearFlags = RenderClearFlags::Depth; ASSERT_TRUE(renderer.Render(request)); EXPECT_EQ( pipelineState->eventLog, (std::vector{ "init:shadowCaster", "shadowCaster", "pipeline" })); EXPECT_TRUE(pipelineState->lastHasMainDirectionalShadow); EXPECT_NE(pipelineState->lastShadowMap, nullptr); EXPECT_EQ(allocationState->createTextureCalls, 1); EXPECT_EQ(allocationState->createDepthViewCalls, 1); EXPECT_EQ(allocationState->createShaderViewCalls, 1); EXPECT_EQ(allocationState->shutdownDepthViewCalls, 0); EXPECT_EQ(allocationState->shutdownShaderViewCalls, 0); EXPECT_EQ(allocationState->shutdownTextureCalls, 0); } EXPECT_EQ(allocationState->shutdownDepthViewCalls, 1); EXPECT_EQ(allocationState->shutdownShaderViewCalls, 1); EXPECT_EQ(allocationState->shutdownTextureCalls, 1); EXPECT_EQ(allocationState->destroyDepthViewCalls, 1); EXPECT_EQ(allocationState->destroyShaderViewCalls, 1); EXPECT_EQ(allocationState->destroyTextureCalls, 1); } TEST(CameraRenderer_Test, StopsRenderingWhenShadowCasterRequestIsInvalid) { Scene scene("CameraRendererInvalidShadowScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); auto state = std::make_shared(); CameraRenderer renderer(std::make_unique(state)); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = CreateValidContext(); request.surface = RenderSurface(320, 180); request.shadowCaster.surface = RenderSurface(64, 64); request.shadowCaster.surface.SetColorAttachment(reinterpret_cast(1)); EXPECT_FALSE(renderer.Render(request)); EXPECT_TRUE(state->eventLog.empty()); } TEST(CameraRenderer_Test, StopsRenderingWhenPostProcessRequestIsInvalid) { Scene scene("CameraRendererInvalidPostProcessScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); auto state = std::make_shared(); CameraRenderer renderer(std::make_unique(state)); RenderPassSequence postProcessPasses; postProcessPasses.AddPass(std::make_unique(state, "postProcess")); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = CreateValidContext(); request.surface = RenderSurface(320, 180); request.postProcess.passes = &postProcessPasses; request.postProcess.sourceSurface = RenderSurface(256, 128); request.postProcess.destinationSurface = RenderSurface(320, 180); request.postProcess.destinationSurface.SetColorAttachment(reinterpret_cast(1)); EXPECT_FALSE(renderer.Render(request)); EXPECT_TRUE(state->eventLog.empty()); } TEST(CameraRenderer_Test, ShutsDownInitializedPassesWhenPipelineRenderFails) { Scene scene("CameraRendererFailureScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); auto state = std::make_shared(); state->renderResult = false; CameraRenderer renderer(std::make_unique(state)); RenderPassSequence prePasses; prePasses.AddPass(std::make_unique(state, "pre")); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = CreateValidContext(); request.surface = RenderSurface(320, 180); request.cameraDepth = camera->GetDepth(); request.preScenePasses = &prePasses; EXPECT_FALSE(renderer.Render(request)); EXPECT_EQ( state->eventLog, (std::vector{ "init:pre", "pre", "pipeline" })); } 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" })); } TEST(CameraRenderer_Test, ShutsDownSequencesWhenPostPassInitializationFails) { Scene scene("CameraRendererPostPassInitFailureScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(4.0f); auto state = std::make_shared(); CameraRenderer renderer(std::make_unique(state)); RenderPassSequence prePasses; prePasses.AddPass(std::make_unique(state, "pre")); RenderPassSequence postPasses; postPasses.AddPass(std::make_unique(state, "post", false, true)); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = CreateValidContext(); request.surface = RenderSurface(512, 512); request.cameraDepth = camera->GetDepth(); request.preScenePasses = &prePasses; request.postScenePasses = &postPasses; EXPECT_FALSE(renderer.Render(request)); EXPECT_EQ( state->eventLog, (std::vector{ "init:pre", "pre", "pipeline", "init:post", "shutdown:post" })); } TEST(SceneRenderer_Test, BuildsSortedRequestsForAllUsableCamerasAndHonorsOverrideCamera) { Scene scene("SceneRendererRequestScene"); GameObject* lowCameraObject = scene.CreateGameObject("LowCamera"); auto* lowCamera = lowCameraObject->AddComponent(); lowCamera->SetPrimary(true); lowCamera->SetDepth(1.0f); GameObject* highCameraObject = scene.CreateGameObject("HighCamera"); auto* highCamera = highCameraObject->AddComponent(); highCamera->SetPrimary(true); highCamera->SetDepth(5.0f); highCamera->SetClearMode(CameraClearMode::None); SceneRenderer renderer; const RenderContext context = CreateValidContext(); const RenderSurface surface(320, 180); const std::vector defaultRequests = renderer.BuildRenderRequests(scene, nullptr, context, surface); ASSERT_EQ(defaultRequests.size(), 2u); EXPECT_EQ(defaultRequests[0].camera, lowCamera); EXPECT_EQ(defaultRequests[0].cameraDepth, 1.0f); EXPECT_EQ(defaultRequests[0].clearFlags, RenderClearFlags::All); EXPECT_EQ(defaultRequests[0].surface.GetWidth(), 320u); EXPECT_EQ(defaultRequests[0].surface.GetHeight(), 180u); EXPECT_EQ(defaultRequests[1].camera, highCamera); EXPECT_EQ(defaultRequests[1].cameraDepth, 5.0f); EXPECT_EQ(defaultRequests[1].clearFlags, RenderClearFlags::None); const std::vector overrideRequests = renderer.BuildRenderRequests(scene, lowCamera, context, surface); ASSERT_EQ(overrideRequests.size(), 1u); EXPECT_EQ(overrideRequests[0].camera, lowCamera); EXPECT_EQ(overrideRequests[0].clearFlags, RenderClearFlags::All); } TEST(SceneRenderer_Test, RendersBaseCamerasBeforeOverlayCamerasAndResolvesAutoClearPerStackType) { Scene scene("SceneRendererCameraStackScene"); GameObject* lateBaseCameraObject = scene.CreateGameObject("LateBaseCamera"); auto* lateBaseCamera = lateBaseCameraObject->AddComponent(); lateBaseCamera->SetDepth(10.0f); lateBaseCamera->SetStackType(CameraStackType::Base); GameObject* earlyBaseCameraObject = scene.CreateGameObject("EarlyBaseCamera"); auto* earlyBaseCamera = earlyBaseCameraObject->AddComponent(); earlyBaseCamera->SetDepth(1.0f); earlyBaseCamera->SetStackType(CameraStackType::Base); GameObject* overlayCameraObject = scene.CreateGameObject("OverlayCamera"); auto* overlayCamera = overlayCameraObject->AddComponent(); overlayCamera->SetDepth(-10.0f); overlayCamera->SetStackType(CameraStackType::Overlay); SceneRenderer renderer; const std::vector requests = renderer.BuildRenderRequests(scene, nullptr, CreateValidContext(), RenderSurface(640, 360)); ASSERT_EQ(requests.size(), 3u); EXPECT_EQ(requests[0].camera, earlyBaseCamera); EXPECT_EQ(requests[0].cameraStackOrder, 0u); EXPECT_EQ(requests[0].clearFlags, RenderClearFlags::All); EXPECT_EQ(requests[1].camera, lateBaseCamera); EXPECT_EQ(requests[1].cameraStackOrder, 0u); EXPECT_EQ(requests[1].clearFlags, RenderClearFlags::Depth); EXPECT_EQ(requests[2].camera, overlayCamera); EXPECT_EQ(requests[2].cameraStackOrder, 1u); EXPECT_EQ(requests[2].clearFlags, RenderClearFlags::Depth); } TEST(SceneRenderer_Test, PreservesSceneTraversalOrderForEqualPriorityCameras) { Scene scene("SceneRendererStableSceneOrder"); GameObject* firstCameraObject = scene.CreateGameObject("FirstCamera"); auto* firstCamera = firstCameraObject->AddComponent(); firstCamera->SetPrimary(false); firstCamera->SetDepth(2.0f); firstCamera->SetStackType(CameraStackType::Base); GameObject* secondCameraObject = scene.CreateGameObject("SecondCamera"); auto* secondCamera = secondCameraObject->AddComponent(); secondCamera->SetPrimary(false); secondCamera->SetDepth(2.0f); secondCamera->SetStackType(CameraStackType::Base); SceneRenderer renderer; const std::vector requests = renderer.BuildRenderRequests(scene, nullptr, CreateValidContext(), RenderSurface(640, 360)); ASSERT_EQ(requests.size(), 2u); EXPECT_EQ(requests[0].camera, firstCamera); EXPECT_EQ(requests[1].camera, secondCamera); } TEST(SceneRenderer_Test, FallsBackToColorClearForFirstOverlayCameraWhenNoBaseCameraExists) { Scene scene("SceneRendererOverlayOnlyScene"); GameObject* firstOverlayObject = scene.CreateGameObject("FirstOverlay"); auto* firstOverlay = firstOverlayObject->AddComponent(); firstOverlay->SetDepth(1.0f); firstOverlay->SetStackType(CameraStackType::Overlay); GameObject* secondOverlayObject = scene.CreateGameObject("SecondOverlay"); auto* secondOverlay = secondOverlayObject->AddComponent(); secondOverlay->SetDepth(2.0f); secondOverlay->SetStackType(CameraStackType::Overlay); SceneRenderer renderer; const std::vector requests = renderer.BuildRenderRequests(scene, nullptr, CreateValidContext(), RenderSurface(320, 180)); ASSERT_EQ(requests.size(), 2u); EXPECT_EQ(requests[0].camera, firstOverlay); EXPECT_EQ(requests[0].clearFlags, RenderClearFlags::All); EXPECT_EQ(requests[1].camera, secondOverlay); EXPECT_EQ(requests[1].clearFlags, RenderClearFlags::Depth); } TEST(SceneRenderer_Test, HonorsExplicitOverrideCameraClearMode) { Scene scene("SceneRendererOverrideClearModeScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); camera->SetClearMode(CameraClearMode::DepthOnly); SceneRenderer renderer; const std::vector requests = renderer.BuildRenderRequests(scene, camera, CreateValidContext(), RenderSurface(640, 360)); ASSERT_EQ(requests.size(), 1u); EXPECT_EQ(requests[0].camera, camera); EXPECT_EQ(requests[0].clearFlags, RenderClearFlags::Depth); } TEST(SceneRenderer_Test, ResolvesNormalizedCameraViewportRectToPerRequestRenderArea) { Scene scene("SceneRendererViewportRectScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); camera->SetViewportRect(XCEngine::Math::Rect(0.25f, 0.1f, 0.5f, 0.4f)); SceneRenderer renderer; const std::vector requests = renderer.BuildRenderRequests(scene, nullptr, CreateValidContext(), RenderSurface(800, 600)); ASSERT_EQ(requests.size(), 1u); const XCEngine::Math::RectInt renderArea = requests[0].surface.GetRenderArea(); EXPECT_EQ(renderArea.x, 200); EXPECT_EQ(renderArea.y, 60); EXPECT_EQ(renderArea.width, 400); EXPECT_EQ(renderArea.height, 240); } TEST(SceneRenderer_Test, ComposesCameraViewportRectWithinExistingSurfaceRenderArea) { Scene scene("SceneRendererNestedViewportRectScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); camera->SetViewportRect(XCEngine::Math::Rect(0.25f, 0.1f, 0.5f, 0.4f)); RenderSurface surface(800, 600); surface.SetRenderArea(XCEngine::Math::RectInt(100, 50, 400, 300)); SceneRenderer renderer; const std::vector requests = renderer.BuildRenderRequests(scene, nullptr, CreateValidContext(), surface); ASSERT_EQ(requests.size(), 1u); const XCEngine::Math::RectInt renderArea = requests[0].surface.GetRenderArea(); EXPECT_EQ(renderArea.x, 200); EXPECT_EQ(renderArea.y, 80); EXPECT_EQ(renderArea.width, 200); EXPECT_EQ(renderArea.height, 120); } TEST(SceneRenderer_Test, PreservesExistingSurfaceRenderAreaForFullViewportCamera) { Scene scene("SceneRendererFullViewportNestedSurfaceScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); camera->SetViewportRect(XCEngine::Math::Rect(0.0f, 0.0f, 1.0f, 1.0f)); RenderSurface surface(1024, 768); surface.SetRenderArea(XCEngine::Math::RectInt(80, 120, 320, 240)); SceneRenderer renderer; const std::vector requests = renderer.BuildRenderRequests(scene, nullptr, CreateValidContext(), surface); ASSERT_EQ(requests.size(), 1u); const XCEngine::Math::RectInt renderArea = requests[0].surface.GetRenderArea(); EXPECT_EQ(renderArea.x, 80); EXPECT_EQ(renderArea.y, 120); EXPECT_EQ(renderArea.width, 320); EXPECT_EQ(renderArea.height, 240); } TEST(SceneRenderer_Test, BuildsCameraColorScalePostProcessRequestFromCameraPassStack) { Scene scene("SceneRendererCameraPostProcessScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); camera->SetViewportRect(XCEngine::Math::Rect(0.25f, 0.125f, 0.5f, 0.625f)); camera->SetPostProcessPasses({ XCEngine::Rendering::CameraPostProcessPassDesc::MakeColorScale( XCEngine::Math::Vector4(1.0f, 0.75f, 0.75f, 1.0f)), XCEngine::Rendering::CameraPostProcessPassDesc::MakeColorScale( XCEngine::Math::Vector4(0.55f, 0.95f, 1.1f, 1.0f)) }); auto allocationState = std::make_shared(); MockShadowDevice device(allocationState); auto* backBufferColorView = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::RenderTarget, XCEngine::RHI::Format::R8G8B8A8_UNorm, XCEngine::RHI::ResourceViewDimension::Texture2D); auto* depthView = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::DepthStencil, XCEngine::RHI::Format::D24_UNorm_S8_UInt, XCEngine::RHI::ResourceViewDimension::Texture2D); RenderContext context = CreateValidContext(); context.device = &device; RenderSurface surface(800, 600); surface.SetColorAttachment(backBufferColorView); surface.SetDepthAttachment(depthView); surface.SetDepthStateBefore(XCEngine::RHI::ResourceStates::Common); surface.SetDepthStateAfter(XCEngine::RHI::ResourceStates::PixelShaderResource); SceneRenderer renderer; const std::vector requests = renderer.BuildRenderRequests(scene, nullptr, context, surface); ASSERT_EQ(requests.size(), 1u); const CameraRenderRequest& request = requests[0]; EXPECT_TRUE(request.postProcess.IsRequested()); EXPECT_TRUE(request.postProcess.IsValid()); EXPECT_NE(request.postProcess.passes, nullptr); ASSERT_EQ(request.postProcess.passes->GetPassCount(), 2u); EXPECT_EQ(request.postProcess.destinationSurface.GetColorAttachments()[0], backBufferColorView); EXPECT_EQ(request.postProcess.destinationSurface.GetDepthAttachment(), depthView); EXPECT_EQ( request.postProcess.destinationSurface.GetDepthStateBefore(), XCEngine::RHI::ResourceStates::Common); EXPECT_EQ( request.postProcess.destinationSurface.GetDepthStateAfter(), XCEngine::RHI::ResourceStates::PixelShaderResource); EXPECT_EQ(request.postProcess.sourceSurface.GetDepthAttachment(), depthView); EXPECT_EQ( request.postProcess.sourceSurface.GetDepthStateBefore(), XCEngine::RHI::ResourceStates::Common); EXPECT_EQ( request.postProcess.sourceSurface.GetDepthStateAfter(), XCEngine::RHI::ResourceStates::PixelShaderResource); EXPECT_EQ(request.postProcess.sourceSurface.GetWidth(), 800u); EXPECT_EQ(request.postProcess.sourceSurface.GetHeight(), 600u); const XCEngine::Math::RectInt sourceRenderArea = request.postProcess.sourceSurface.GetRenderArea(); EXPECT_EQ(sourceRenderArea.x, 200); EXPECT_EQ(sourceRenderArea.y, 75); EXPECT_EQ(sourceRenderArea.width, 400); EXPECT_EQ(sourceRenderArea.height, 375); EXPECT_NE(request.postProcess.sourceColorView, nullptr); EXPECT_NE(request.postProcess.sourceColorView, backBufferColorView); EXPECT_EQ( request.postProcess.sourceColorState, XCEngine::RHI::ResourceStates::PixelShaderResource); ASSERT_FALSE(request.postProcess.sourceSurface.GetColorAttachments().empty()); EXPECT_NE(request.postProcess.sourceSurface.GetColorAttachments()[0], backBufferColorView); EXPECT_EQ(allocationState->createTextureCalls, 1); EXPECT_EQ(allocationState->createRenderTargetViewCalls, 1); EXPECT_EQ(allocationState->createShaderViewCalls, 1); delete depthView; delete backBufferColorView; } TEST(SceneRenderer_Test, ResolvesFinalColorPolicyFromPipelineDefaultsAndCameraOverrides) { Scene scene("SceneRendererFinalColorPolicyScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); FinalColorOverrideSettings cameraOverrides = {}; cameraOverrides.overrideExposureValue = true; cameraOverrides.exposureValue = 1.8f; cameraOverrides.overrideFinalColorScale = true; cameraOverrides.finalColorScale = XCEngine::Math::Vector4(0.95f, 0.9f, 0.85f, 1.0f); camera->SetFinalColorOverrides(cameraOverrides); auto assetState = std::make_shared(); assetState->defaultFinalColorSettings.outputTransferMode = FinalColorOutputTransferMode::LinearToSRGB; assetState->defaultFinalColorSettings.exposureMode = FinalColorExposureMode::Fixed; assetState->defaultFinalColorSettings.exposureValue = 1.25f; assetState->defaultFinalColorSettings.finalColorScale = XCEngine::Math::Vector4(1.0f, 0.95f, 0.9f, 1.0f); SceneRenderer renderer(std::make_shared(assetState)); const std::vector requests = renderer.BuildRenderRequests(scene, nullptr, CreateValidContext(), RenderSurface(640, 360)); ASSERT_EQ(requests.size(), 1u); const CameraRenderRequest& request = requests[0]; EXPECT_TRUE(request.finalColorPolicy.hasPipelineDefaults); EXPECT_TRUE(request.finalColorPolicy.hasCameraOverrides); EXPECT_EQ( request.finalColorPolicy.outputTransferMode, FinalColorOutputTransferMode::LinearToSRGB); EXPECT_EQ( request.finalColorPolicy.exposureMode, FinalColorExposureMode::Fixed); EXPECT_FLOAT_EQ(request.finalColorPolicy.exposureValue, 1.8f); EXPECT_FLOAT_EQ(request.finalColorPolicy.finalColorScale.x, 0.95f); EXPECT_FALSE(request.finalOutput.IsRequested()); } TEST(SceneRenderer_Test, BuildsFinalOutputRequestFromResolvedFinalColorPolicy) { Scene scene("SceneRendererFinalOutputScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); FinalColorOverrideSettings cameraOverrides = {}; cameraOverrides.overrideExposureMode = true; cameraOverrides.exposureMode = FinalColorExposureMode::Fixed; cameraOverrides.overrideExposureValue = true; cameraOverrides.exposureValue = 1.6f; cameraOverrides.overrideFinalColorScale = true; cameraOverrides.finalColorScale = XCEngine::Math::Vector4(0.95f, 0.9f, 0.85f, 1.0f); camera->SetFinalColorOverrides(cameraOverrides); auto allocationState = std::make_shared(); MockShadowDevice device(allocationState); auto* backBufferColorView = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::RenderTarget, XCEngine::RHI::Format::R8G8B8A8_UNorm, XCEngine::RHI::ResourceViewDimension::Texture2D); auto* depthView = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::DepthStencil, XCEngine::RHI::Format::D24_UNorm_S8_UInt, XCEngine::RHI::ResourceViewDimension::Texture2D); auto assetState = std::make_shared(); assetState->defaultFinalColorSettings.outputTransferMode = FinalColorOutputTransferMode::LinearToSRGB; RenderContext context = CreateValidContext(); context.device = &device; RenderSurface surface(800, 600); surface.SetColorAttachment(backBufferColorView); surface.SetDepthAttachment(depthView); surface.SetDepthStateBefore(XCEngine::RHI::ResourceStates::Common); surface.SetDepthStateAfter(XCEngine::RHI::ResourceStates::PixelShaderResource); SceneRenderer renderer(std::make_shared(assetState)); const std::vector requests = renderer.BuildRenderRequests(scene, nullptr, context, surface); ASSERT_EQ(requests.size(), 1u); const CameraRenderRequest& request = requests[0]; EXPECT_FALSE(request.postProcess.IsRequested()); EXPECT_TRUE(request.finalOutput.IsRequested()); EXPECT_TRUE(request.finalOutput.IsValid()); ASSERT_NE(request.finalOutput.passes, nullptr); EXPECT_EQ(request.finalOutput.passes->GetPassCount(), 1u); EXPECT_EQ(request.finalOutput.destinationSurface.GetColorAttachments()[0], backBufferColorView); EXPECT_EQ(request.finalOutput.destinationSurface.GetDepthAttachment(), depthView); EXPECT_EQ( request.finalOutput.destinationSurface.GetDepthStateBefore(), XCEngine::RHI::ResourceStates::Common); EXPECT_EQ( request.finalOutput.destinationSurface.GetDepthStateAfter(), XCEngine::RHI::ResourceStates::PixelShaderResource); EXPECT_EQ(request.finalOutput.sourceSurface.GetDepthAttachment(), depthView); EXPECT_EQ( request.finalOutput.sourceSurface.GetDepthStateBefore(), XCEngine::RHI::ResourceStates::Common); EXPECT_EQ( request.finalOutput.sourceSurface.GetDepthStateAfter(), XCEngine::RHI::ResourceStates::PixelShaderResource); EXPECT_EQ(request.finalOutput.sourceSurface.GetWidth(), 800u); EXPECT_EQ(request.finalOutput.sourceSurface.GetHeight(), 600u); EXPECT_NE(request.finalOutput.sourceColorView, nullptr); EXPECT_NE(request.finalOutput.sourceColorView, backBufferColorView); EXPECT_EQ( request.finalOutput.sourceColorState, XCEngine::RHI::ResourceStates::PixelShaderResource); ASSERT_FALSE(request.finalOutput.sourceSurface.GetColorAttachments().empty()); EXPECT_NE(request.finalOutput.sourceSurface.GetColorAttachments()[0], backBufferColorView); EXPECT_EQ(allocationState->createTextureCalls, 1); EXPECT_EQ(allocationState->createRenderTargetViewCalls, 1); EXPECT_EQ(allocationState->createShaderViewCalls, 1); delete depthView; delete backBufferColorView; } TEST(SceneRenderer_Test, RoutesPostProcessIntoIntermediateSurfaceBeforeFinalOutput) { Scene scene("SceneRendererPostProcessFinalOutputScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); camera->SetViewportRect(XCEngine::Math::Rect(0.25f, 0.125f, 0.5f, 0.625f)); camera->SetPostProcessPasses({ XCEngine::Rendering::CameraPostProcessPassDesc::MakeColorScale( XCEngine::Math::Vector4(1.0f, 0.75f, 0.75f, 1.0f)) }); FinalColorOverrideSettings cameraOverrides = {}; cameraOverrides.overrideOutputTransferMode = true; cameraOverrides.outputTransferMode = FinalColorOutputTransferMode::LinearToSRGB; cameraOverrides.overrideFinalColorScale = true; cameraOverrides.finalColorScale = XCEngine::Math::Vector4(0.95f, 0.9f, 0.85f, 1.0f); camera->SetFinalColorOverrides(cameraOverrides); auto allocationState = std::make_shared(); MockShadowDevice device(allocationState); auto* backBufferColorView = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::RenderTarget, XCEngine::RHI::Format::R8G8B8A8_UNorm, XCEngine::RHI::ResourceViewDimension::Texture2D); auto* depthView = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::DepthStencil, XCEngine::RHI::Format::D24_UNorm_S8_UInt, XCEngine::RHI::ResourceViewDimension::Texture2D); RenderContext context = CreateValidContext(); context.device = &device; RenderSurface surface(800, 600); surface.SetColorAttachment(backBufferColorView); surface.SetDepthAttachment(depthView); surface.SetDepthStateBefore(XCEngine::RHI::ResourceStates::Common); surface.SetDepthStateAfter(XCEngine::RHI::ResourceStates::PixelShaderResource); SceneRenderer renderer; const std::vector requests = renderer.BuildRenderRequests(scene, nullptr, context, surface); ASSERT_EQ(requests.size(), 1u); const CameraRenderRequest& request = requests[0]; EXPECT_TRUE(request.postProcess.IsRequested()); EXPECT_TRUE(request.finalOutput.IsRequested()); ASSERT_NE(request.postProcess.passes, nullptr); ASSERT_NE(request.finalOutput.passes, nullptr); EXPECT_EQ(request.postProcess.passes->GetPassCount(), 1u); EXPECT_EQ(request.finalOutput.passes->GetPassCount(), 1u); ASSERT_FALSE(request.postProcess.sourceSurface.GetColorAttachments().empty()); ASSERT_FALSE(request.postProcess.destinationSurface.GetColorAttachments().empty()); ASSERT_FALSE(request.finalOutput.sourceSurface.GetColorAttachments().empty()); EXPECT_NE( request.postProcess.sourceSurface.GetColorAttachments()[0], request.postProcess.destinationSurface.GetColorAttachments()[0]); EXPECT_EQ( request.finalOutput.sourceSurface.GetColorAttachments()[0], request.postProcess.destinationSurface.GetColorAttachments()[0]); EXPECT_EQ(request.finalOutput.destinationSurface.GetColorAttachments()[0], backBufferColorView); EXPECT_EQ(request.postProcess.sourceSurface.GetDepthAttachment(), depthView); EXPECT_EQ( request.postProcess.sourceSurface.GetDepthStateBefore(), XCEngine::RHI::ResourceStates::Common); EXPECT_EQ( request.postProcess.sourceSurface.GetDepthStateAfter(), XCEngine::RHI::ResourceStates::PixelShaderResource); EXPECT_EQ(request.postProcess.destinationSurface.GetDepthAttachment(), nullptr); EXPECT_EQ(request.finalOutput.sourceSurface.GetDepthAttachment(), nullptr); EXPECT_EQ(request.finalOutput.destinationSurface.GetDepthAttachment(), depthView); EXPECT_EQ( request.finalOutput.destinationSurface.GetDepthStateBefore(), XCEngine::RHI::ResourceStates::Common); EXPECT_EQ( request.finalOutput.destinationSurface.GetDepthStateAfter(), XCEngine::RHI::ResourceStates::PixelShaderResource); const XCEngine::Math::RectInt postProcessSourceArea = request.postProcess.sourceSurface.GetRenderArea(); EXPECT_EQ(postProcessSourceArea.x, 200); EXPECT_EQ(postProcessSourceArea.y, 75); EXPECT_EQ(postProcessSourceArea.width, 400); EXPECT_EQ(postProcessSourceArea.height, 375); const XCEngine::Math::RectInt finalOutputSourceArea = request.finalOutput.sourceSurface.GetRenderArea(); EXPECT_EQ(finalOutputSourceArea.x, 200); EXPECT_EQ(finalOutputSourceArea.y, 75); EXPECT_EQ(finalOutputSourceArea.width, 400); EXPECT_EQ(finalOutputSourceArea.height, 375); EXPECT_NE(request.postProcess.sourceColorView, nullptr); EXPECT_NE(request.finalOutput.sourceColorView, nullptr); EXPECT_NE(request.postProcess.sourceColorView, request.finalOutput.sourceColorView); EXPECT_EQ( request.postProcess.sourceColorState, XCEngine::RHI::ResourceStates::PixelShaderResource); EXPECT_EQ( request.finalOutput.sourceColorState, XCEngine::RHI::ResourceStates::PixelShaderResource); EXPECT_EQ(allocationState->createTextureCalls, 2); EXPECT_EQ(allocationState->createRenderTargetViewCalls, 2); EXPECT_EQ(allocationState->createShaderViewCalls, 2); delete depthView; delete backBufferColorView; } TEST(SceneRenderer_Test, DoesNotBuildFullscreenStagesForMultisampledMainSceneSurface) { Scene scene("SceneRendererMultisampledFullscreenStageScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); camera->SetPostProcessPasses({ XCEngine::Rendering::CameraPostProcessPassDesc::MakeColorScale( XCEngine::Math::Vector4(1.0f, 0.75f, 0.75f, 1.0f)) }); FinalColorOverrideSettings cameraOverrides = {}; cameraOverrides.overrideOutputTransferMode = true; cameraOverrides.outputTransferMode = FinalColorOutputTransferMode::LinearToSRGB; camera->SetFinalColorOverrides(cameraOverrides); auto allocationState = std::make_shared(); MockShadowDevice device(allocationState); auto* backBufferColorView = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::RenderTarget, XCEngine::RHI::Format::R8G8B8A8_UNorm, XCEngine::RHI::ResourceViewDimension::Texture2D); auto* depthView = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::DepthStencil, XCEngine::RHI::Format::D24_UNorm_S8_UInt, XCEngine::RHI::ResourceViewDimension::Texture2D); RenderContext context = CreateValidContext(); context.device = &device; RenderSurface surface(800, 600); surface.SetColorAttachment(backBufferColorView); surface.SetDepthAttachment(depthView); surface.SetSampleDesc(4u, 0u); SceneRenderer renderer; const std::vector requests = renderer.BuildRenderRequests(scene, nullptr, context, surface); ASSERT_EQ(requests.size(), 1u); const CameraRenderRequest& request = requests[0]; EXPECT_EQ(request.surface.GetSampleCount(), 4u); EXPECT_FALSE(request.postProcess.IsRequested()); EXPECT_FALSE(request.finalOutput.IsRequested()); EXPECT_EQ(allocationState->createTextureCalls, 0); EXPECT_EQ(allocationState->createRenderTargetViewCalls, 0); EXPECT_EQ(allocationState->createShaderViewCalls, 0); delete depthView; delete backBufferColorView; } TEST(SceneRenderer_Test, ReusesTrackedSceneColorStateAcrossFramesWhenPostProcessIsEnabled) { Scene scene("SceneRendererTrackedSceneColorStateScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); camera->SetPostProcessPasses({ XCEngine::Rendering::CameraPostProcessPassDesc::MakeColorScale( XCEngine::Math::Vector4(1.0f, 0.9f, 0.8f, 1.0f)) }); auto pipelineState = std::make_shared(); auto allocationState = std::make_shared(); MockShadowDevice device(allocationState); RenderContext context = CreateValidContext(); context.device = &device; auto* backBufferColorView = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::RenderTarget, XCEngine::RHI::Format::R8G8B8A8_UNorm, XCEngine::RHI::ResourceViewDimension::Texture2D); auto* depthView = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::DepthStencil, XCEngine::RHI::Format::D24_UNorm_S8_UInt, XCEngine::RHI::ResourceViewDimension::Texture2D); RenderSurface surface(800, 600); surface.SetColorAttachment(backBufferColorView); surface.SetDepthAttachment(depthView); SceneRenderer renderer(std::make_unique(pipelineState)); std::vector firstFrameRequests = renderer.BuildRenderRequests(scene, nullptr, context, surface); ASSERT_EQ(firstFrameRequests.size(), 1u); EXPECT_EQ( firstFrameRequests[0].GetMainSceneSurface().GetColorStateBefore(), XCEngine::RHI::ResourceStates::Common); RenderPassSequence postProcessPasses; postProcessPasses.AddPass(std::make_unique(pipelineState, "postProcess")); firstFrameRequests[0].postProcess.passes = &postProcessPasses; ASSERT_TRUE(renderer.Render(firstFrameRequests)); const std::vector secondFrameRequests = renderer.BuildRenderRequests(scene, nullptr, context, surface); ASSERT_EQ(secondFrameRequests.size(), 1u); EXPECT_EQ( secondFrameRequests[0].GetMainSceneSurface().GetColorStateBefore(), XCEngine::RHI::ResourceStates::PixelShaderResource); EXPECT_EQ( secondFrameRequests[0].GetMainSceneSurface().GetColorStateAfter(), XCEngine::RHI::ResourceStates::PixelShaderResource); delete depthView; delete backBufferColorView; } TEST(SceneRenderer_Test, ReusesTrackedPostProcessOutputStateAcrossFramesWhenFinalOutputIsEnabled) { Scene scene("SceneRendererTrackedPostProcessOutputStateScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); camera->SetPostProcessPasses({ XCEngine::Rendering::CameraPostProcessPassDesc::MakeColorScale( XCEngine::Math::Vector4(1.0f, 0.9f, 0.8f, 1.0f)) }); FinalColorOverrideSettings finalColorOverrides = {}; finalColorOverrides.overrideOutputTransferMode = true; finalColorOverrides.outputTransferMode = FinalColorOutputTransferMode::LinearToSRGB; camera->SetFinalColorOverrides(finalColorOverrides); auto pipelineState = std::make_shared(); auto allocationState = std::make_shared(); MockShadowDevice device(allocationState); RenderContext context = CreateValidContext(); context.device = &device; auto* backBufferColorView = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::RenderTarget, XCEngine::RHI::Format::R8G8B8A8_UNorm, XCEngine::RHI::ResourceViewDimension::Texture2D); auto* depthView = new MockShadowView( allocationState, XCEngine::RHI::ResourceViewType::DepthStencil, XCEngine::RHI::Format::D24_UNorm_S8_UInt, XCEngine::RHI::ResourceViewDimension::Texture2D); RenderSurface surface(800, 600); surface.SetColorAttachment(backBufferColorView); surface.SetDepthAttachment(depthView); SceneRenderer renderer(std::make_unique(pipelineState)); std::vector firstFrameRequests = renderer.BuildRenderRequests(scene, nullptr, context, surface); ASSERT_EQ(firstFrameRequests.size(), 1u); EXPECT_TRUE(firstFrameRequests[0].postProcess.IsRequested()); EXPECT_TRUE(firstFrameRequests[0].finalOutput.IsRequested()); EXPECT_EQ( firstFrameRequests[0].GetMainSceneSurface().GetColorStateBefore(), XCEngine::RHI::ResourceStates::Common); EXPECT_EQ( firstFrameRequests[0].postProcess.destinationSurface.GetColorStateBefore(), XCEngine::RHI::ResourceStates::Common); RenderPassSequence postProcessPasses; postProcessPasses.AddPass(std::make_unique(pipelineState, "postProcess")); RenderPassSequence finalOutputPasses; finalOutputPasses.AddPass(std::make_unique(pipelineState, "finalOutput")); firstFrameRequests[0].postProcess.passes = &postProcessPasses; firstFrameRequests[0].finalOutput.passes = &finalOutputPasses; ASSERT_TRUE(renderer.Render(firstFrameRequests)); const std::vector secondFrameRequests = renderer.BuildRenderRequests(scene, nullptr, context, surface); ASSERT_EQ(secondFrameRequests.size(), 1u); EXPECT_EQ( secondFrameRequests[0].GetMainSceneSurface().GetColorStateBefore(), XCEngine::RHI::ResourceStates::PixelShaderResource); EXPECT_EQ( secondFrameRequests[0].postProcess.destinationSurface.GetColorStateBefore(), XCEngine::RHI::ResourceStates::PixelShaderResource); EXPECT_EQ( secondFrameRequests[0].finalOutput.sourceSurface.GetColorStateBefore(), XCEngine::RHI::ResourceStates::PixelShaderResource); delete depthView; delete backBufferColorView; } TEST(CameraRenderer_Test, UsesResolvedRenderAreaForCameraViewportDimensions) { Scene scene("CameraRendererViewportRectScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetViewportRect(XCEngine::Math::Rect(0.125f, 0.25f, 0.5f, 0.5f)); auto state = std::make_shared(); CameraRenderer renderer(std::make_unique(state)); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = CreateValidContext(); request.surface = RenderSurface(640, 480); request.surface.SetRenderArea(XCEngine::Math::RectInt(80, 120, 320, 240)); ASSERT_TRUE(renderer.Render(request)); EXPECT_EQ(state->lastRenderAreaX, 80); EXPECT_EQ(state->lastRenderAreaY, 120); EXPECT_EQ(state->lastRenderAreaWidth, 320); EXPECT_EQ(state->lastRenderAreaHeight, 240); EXPECT_EQ(state->lastCameraViewportWidth, 320u); EXPECT_EQ(state->lastCameraViewportHeight, 240u); } TEST(SceneRenderer_Test, ForwardsPipelineLifetimeAndRenderCallsToCameraRenderer) { Scene scene("SceneRendererScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); auto initialState = std::make_shared(); auto replacementState = std::make_shared(); { auto initialPipeline = std::make_unique(initialState); MockPipeline* initialPipelineRaw = initialPipeline.get(); SceneRenderer renderer(std::move(initialPipeline)); EXPECT_EQ(renderer.GetPipeline(), initialPipelineRaw); auto replacementPipeline = std::make_unique(replacementState); MockPipeline* replacementPipelineRaw = replacementPipeline.get(); renderer.SetPipeline(std::move(replacementPipeline)); EXPECT_EQ(initialState->shutdownCalls, 1); EXPECT_EQ(renderer.GetPipeline(), replacementPipelineRaw); const RenderSurface surface(800, 600); ASSERT_TRUE(renderer.Render(scene, nullptr, CreateValidContext(), surface)); EXPECT_EQ(replacementState->renderCalls, 1); EXPECT_EQ(replacementState->lastSurfaceWidth, 800u); EXPECT_EQ(replacementState->lastSurfaceHeight, 600u); EXPECT_EQ(replacementState->lastCamera, camera); } EXPECT_EQ(initialState->shutdownCalls, 1); EXPECT_EQ(replacementState->shutdownCalls, 1); } TEST(SceneRenderer_Test, CreatesPipelineInstancesFromPipelineAssetsAndShutsDownReplacedPipelines) { Scene scene("SceneRendererAssetScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); auto initialAssetState = std::make_shared(); auto replacementAssetState = std::make_shared(); { SceneRenderer renderer(std::make_shared(initialAssetState)); ASSERT_NE(renderer.GetPipeline(), nullptr); ASSERT_NE(renderer.GetPipelineAsset(), nullptr); EXPECT_EQ(initialAssetState->createCalls, 1); const RenderSurface surface(800, 600); ASSERT_TRUE(renderer.Render(scene, nullptr, CreateValidContext(), surface)); ASSERT_NE(initialAssetState->lastCreatedPipelineState, nullptr); EXPECT_EQ(initialAssetState->lastCreatedPipelineState->renderCalls, 1); EXPECT_EQ(initialAssetState->lastCreatedPipelineState->lastCamera, camera); renderer.SetPipelineAsset(std::make_shared(replacementAssetState)); ASSERT_NE(initialAssetState->lastCreatedPipelineState, nullptr); EXPECT_EQ(initialAssetState->lastCreatedPipelineState->shutdownCalls, 1); EXPECT_EQ(replacementAssetState->createCalls, 1); ASSERT_TRUE(renderer.Render(scene, nullptr, CreateValidContext(), surface)); ASSERT_NE(replacementAssetState->lastCreatedPipelineState, nullptr); EXPECT_EQ(replacementAssetState->lastCreatedPipelineState->renderCalls, 1); EXPECT_EQ(replacementAssetState->lastCreatedPipelineState->lastCamera, camera); } ASSERT_NE(replacementAssetState->lastCreatedPipelineState, nullptr); EXPECT_EQ(replacementAssetState->lastCreatedPipelineState->shutdownCalls, 1); } TEST(SceneRenderer_Test, SortsManualCameraRequestsByDepthBeforeRendering) { Scene scene("SceneRendererManualRequests"); GameObject* farCameraObject = scene.CreateGameObject("FarCamera"); auto* farCamera = farCameraObject->AddComponent(); farCamera->SetPrimary(true); farCamera->SetDepth(10.0f); GameObject* nearCameraObject = scene.CreateGameObject("NearCamera"); auto* nearCamera = nearCameraObject->AddComponent(); nearCamera->SetPrimary(false); nearCamera->SetDepth(1.0f); auto state = std::make_shared(); SceneRenderer renderer(std::make_unique(state)); CameraRenderRequest farRequest; farRequest.scene = &scene; farRequest.camera = farCamera; farRequest.context = CreateValidContext(); farRequest.surface = RenderSurface(800, 600); farRequest.cameraDepth = farCamera->GetDepth(); farRequest.cameraStackOrder = 1; farRequest.clearFlags = RenderClearFlags::None; CameraRenderRequest nearRequest = farRequest; nearRequest.camera = nearCamera; nearRequest.cameraDepth = nearCamera->GetDepth(); nearRequest.cameraStackOrder = 0; nearRequest.clearFlags = RenderClearFlags::Depth; const std::vector requests = { farRequest, nearRequest }; ASSERT_TRUE(renderer.Render(requests)); ASSERT_EQ(state->renderedCameras.size(), 2u); ASSERT_EQ(state->renderedClearFlags.size(), 2u); EXPECT_EQ(state->renderedCameras[0], nearCamera); EXPECT_EQ(state->renderedClearFlags[0], RenderClearFlags::Depth); EXPECT_EQ(state->renderedCameras[1], farCamera); EXPECT_EQ(state->renderedClearFlags[1], RenderClearFlags::None); } TEST(SceneRenderer_Test, PreservesManualSubmissionOrderForEqualPriorityRequests) { Scene scene("SceneRendererManualSubmissionOrder"); GameObject* firstCameraObject = scene.CreateGameObject("FirstCamera"); auto* firstCamera = firstCameraObject->AddComponent(); firstCamera->SetPrimary(false); firstCamera->SetDepth(2.0f); GameObject* secondCameraObject = scene.CreateGameObject("SecondCamera"); auto* secondCamera = secondCameraObject->AddComponent(); secondCamera->SetPrimary(false); secondCamera->SetDepth(2.0f); auto state = std::make_shared(); SceneRenderer renderer(std::make_unique(state)); CameraRenderRequest firstRequest; firstRequest.scene = &scene; firstRequest.camera = firstCamera; firstRequest.context = CreateValidContext(); firstRequest.surface = RenderSurface(800, 600); firstRequest.cameraDepth = 2.0f; firstRequest.cameraStackOrder = 0; firstRequest.clearFlags = RenderClearFlags::All; CameraRenderRequest secondRequest = firstRequest; secondRequest.camera = secondCamera; const std::vector requests = { secondRequest, firstRequest }; ASSERT_TRUE(renderer.Render(requests)); ASSERT_EQ(state->renderedCameras.size(), 2u); EXPECT_EQ(state->renderedCameras[0], secondCamera); EXPECT_EQ(state->renderedCameras[1], firstCamera); }