#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(); XCEngine::Math::Vector4 lastShadowParams = XCEngine::Math::Vector4::Zero(); std::vector renderedCameras; std::vector renderedClearFlags; std::vector renderedClearColors; std::vector eventLog; }; struct MockShadowAllocationState { int createTextureCalls = 0; int shutdownTextureCalls = 0; int destroyTextureCalls = 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 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 { ++m_state->destroyDepthViewCalls; } } void Shutdown() override { if (m_viewType == XCEngine::RHI::ResourceViewType::ShaderResource) { ++m_state->shutdownShaderViewCalls; } 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&) override { return nullptr; } 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::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::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; }; 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->lastShadowParams = sceneData.lighting.mainDirectionalShadow.shadowParams; 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); } private: std::shared_ptr m_state; }; class MockObjectIdPass final : public ObjectIdPass { public: MockObjectIdPass( std::shared_ptr state, bool renderResult = true) : m_state(std::move(state)) , m_renderResult(renderResult) { } bool Render( const RenderContext&, const RenderSurface&, const RenderSceneData&) override { m_state->eventLog.push_back("objectId"); return m_renderResult; } void Shutdown() override { m_state->eventLog.push_back("shutdown:objectId"); } private: std::shared_ptr m_state; bool m_renderResult = true; }; class 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(); 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; 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&) override { m_state->eventLog.push_back(m_label); 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; }; RenderContext CreateValidContext() { RenderContext context; context.device = reinterpret_cast(1); context.commandList = reinterpret_cast(1); context.commandQueue = reinterpret_cast(1); return context; } } // namespace 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, 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", "shutdown:post", "shutdown:pre" })); } TEST(CameraRenderer_Test, ExecutesObjectIdPassBetweenPipelineAndPostPassesWhenRequested) { Scene scene("CameraRendererObjectIdPassScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(3.0f); auto state = std::make_shared(); CameraRenderer renderer( std::make_unique(state), std::make_unique(state)); RenderPassSequence prePasses; prePasses.AddPass(std::make_unique(state, "pre")); RenderPassSequence postPasses; postPasses.AddPass(std::make_unique(state, "post")); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = CreateValidContext(); request.surface = RenderSurface(320, 180); request.cameraDepth = camera->GetDepth(); request.preScenePasses = &prePasses; request.postScenePasses = &postPasses; request.objectId.surface = RenderSurface(320, 180); request.objectId.surface.SetColorAttachment(reinterpret_cast(1)); request.objectId.surface.SetDepthAttachment(reinterpret_cast(2)); ASSERT_TRUE(renderer.Render(request)); EXPECT_EQ( state->eventLog, (std::vector{ "init:pre", "pre", "pipeline", "objectId", "init:post", "post", "shutdown:post", "shutdown:pre" })); } TEST(CameraRenderer_Test, 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->lastShadowParams.x, 0.0015f); EXPECT_FLOAT_EQ(pipelineState->lastShadowParams.y, 1.0f / 256.0f); EXPECT_FLOAT_EQ(pipelineState->lastShadowParams.z, 1.0f / 128.0f); EXPECT_FLOAT_EQ(pipelineState->lastShadowParams.w, 0.85f); } 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, 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, 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", "shutdown:pre" })); } TEST(CameraRenderer_Test, StopsRenderingWhenObjectIdPassFails) { Scene scene("CameraRendererObjectIdFailureScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetDepth(2.0f); auto state = std::make_shared(); CameraRenderer renderer( std::make_unique(state), std::make_unique(state, false)); RenderPassSequence prePasses; prePasses.AddPass(std::make_unique(state, "pre")); RenderPassSequence postPasses; postPasses.AddPass(std::make_unique(state, "post")); CameraRenderRequest request; request.scene = &scene; request.camera = camera; request.context = CreateValidContext(); request.surface = RenderSurface(320, 180); request.cameraDepth = camera->GetDepth(); request.preScenePasses = &prePasses; request.postScenePasses = &postPasses; request.objectId.surface = RenderSurface(320, 180); request.objectId.surface.SetColorAttachment(reinterpret_cast(1)); request.objectId.surface.SetDepthAttachment(reinterpret_cast(2)); EXPECT_FALSE(renderer.Render(request)); EXPECT_EQ( state->eventLog, (std::vector{ "init:pre", "pre", "pipeline", "objectId", "shutdown:pre" })); } TEST(CameraRenderer_Test, ShutsDownSequencesWhenPostPassInitializationFails) { Scene scene("CameraRendererPostPassInitFailureScene"); 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", "shutdown:pre" })); } 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(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); }