Files
XCEngine/tests/Rendering/unit/test_camera_scene_renderer.cpp

2616 lines
113 KiB
C++
Raw Normal View History

#include <gtest/gtest.h>
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/RHI/RHICommandList.h>
#include <XCEngine/RHI/RHICommandQueue.h>
#include <XCEngine/RHI/RHIDevice.h>
#include <XCEngine/RHI/RHIResourceView.h>
#include <XCEngine/RHI/RHITexture.h>
2026-04-04 23:01:34 +08:00
#include <XCEngine/Core/Math/Vector4.h>
#include <XCEngine/Rendering/Execution/CameraRenderer.h>
#include <XCEngine/Rendering/RenderPipelineAsset.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/Rendering/Execution/SceneRenderer.h>
#include <XCEngine/Resources/Material/Material.h>
#include <XCEngine/Resources/Shader/ShaderKeywordTypes.h>
#include <XCEngine/Scene/Scene.h>
#include <memory>
#include <string>
#include <vector>
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();
2026-04-04 23:01:34 +08:00
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<CameraComponent*> renderedCameras;
std::vector<RenderClearFlags> renderedClearFlags;
std::vector<XCEngine::Math::Color> renderedClearColors;
std::vector<std::string> 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;
2026-04-04 23:01:34 +08:00
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;
2026-04-04 23:01:34 +08:00
XCEngine::RHI::Format lastShaderViewFormat = XCEngine::RHI::Format::Unknown;
};
class MockShadowTexture final : public XCEngine::RHI::RHITexture {
public:
MockShadowTexture(
std::shared_ptr<MockShadowAllocationState> 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<MockShadowAllocationState> 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<MockShadowAllocationState> 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 {
2026-04-04 23:01:34 +08:00
if (m_viewType == XCEngine::RHI::ResourceViewType::ShaderResource) {
++m_state->destroyShaderViewCalls;
} else if (m_viewType == XCEngine::RHI::ResourceViewType::RenderTarget) {
++m_state->destroyRenderTargetViewCalls;
2026-04-04 23:01:34 +08:00
} else {
++m_state->destroyDepthViewCalls;
}
}
void Shutdown() override {
2026-04-04 23:01:34 +08:00
if (m_viewType == XCEngine::RHI::ResourceViewType::ShaderResource) {
++m_state->shutdownShaderViewCalls;
} else if (m_viewType == XCEngine::RHI::ResourceViewType::RenderTarget) {
++m_state->shutdownRenderTargetViewCalls;
2026-04-04 23:01:34 +08:00
} 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<MockShadowAllocationState> 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<MockShadowAllocationState> 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<XCEngine::RHI::Format>(desc.format);
return new MockShadowTexture(
m_state,
desc.width,
desc.height,
static_cast<XCEngine::RHI::Format>(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<XCEngine::RHI::Format>(desc.format);
return new MockShadowView(
m_state,
XCEngine::RHI::ResourceViewType::RenderTarget,
static_cast<XCEngine::RHI::Format>(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<XCEngine::RHI::Format>(desc.format);
return new MockShadowView(
m_state,
XCEngine::RHI::ResourceViewType::DepthStencil,
static_cast<XCEngine::RHI::Format>(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*,
2026-04-04 23:01:34 +08:00
const XCEngine::RHI::ResourceViewDesc& desc) override {
++m_state->createShaderViewCalls;
m_state->lastShaderViewFormat = static_cast<XCEngine::RHI::Format>(desc.format);
return new MockShadowView(
m_state,
XCEngine::RHI::ResourceViewType::ShaderResource,
static_cast<XCEngine::RHI::Format>(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<MockShadowAllocationState> m_state;
XCEngine::RHI::RHICapabilities m_capabilities = {};
XCEngine::RHI::RHIDeviceInfo m_deviceInfo = {};
};
struct MockPipelineAssetState {
int createCalls = 0;
std::shared_ptr<MockPipelineState> lastCreatedPipelineState;
FinalColorSettings defaultFinalColorSettings = {};
};
class MockPipeline final : public RenderPipeline {
public:
explicit MockPipeline(std::shared_ptr<MockPipelineState> 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;
2026-04-04 23:01:34 +08:00
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<MockPipelineState> m_state;
};
class MockPipelineAsset final : public RenderPipelineAsset {
public:
explicit MockPipelineAsset(std::shared_ptr<MockPipelineAssetState> state)
: m_state(std::move(state)) {
}
std::unique_ptr<RenderPipeline> CreatePipeline() const override {
++m_state->createCalls;
m_state->lastCreatedPipelineState = std::make_shared<MockPipelineState>();
return std::make_unique<MockPipeline>(m_state->lastCreatedPipelineState);
}
FinalColorSettings GetDefaultFinalColorSettings() const override {
return m_state->defaultFinalColorSettings;
}
private:
std::shared_ptr<MockPipelineAssetState> m_state;
};
class MockObjectIdPass final : public RenderPass {
2026-04-01 16:44:11 +08:00
public:
MockObjectIdPass(
std::shared_ptr<MockPipelineState> state,
bool renderResult = true)
: m_state(std::move(state))
, m_renderResult(renderResult) {
}
const char* GetName() const override {
return "MockObjectIdPass";
}
bool Execute(const RenderPassContext&) override {
2026-04-01 16:44:11 +08:00
m_state->eventLog.push_back("objectId");
return m_renderResult;
}
void Shutdown() override {
m_state->eventLog.push_back("shutdown:objectId");
}
private:
std::shared_ptr<MockPipelineState> m_state;
bool m_renderResult = true;
};
class MockScenePass final : public RenderPass {
public:
MockScenePass(
std::shared_ptr<MockPipelineState> 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<MockPipelineState> m_state;
const char* m_label = "";
bool m_initializeResult = true;
bool m_executeResult = true;
};
class TrackingPass final : public RenderPass {
public:
TrackingPass(
std::shared_ptr<MockPipelineState> 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<MockPipelineState> 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<XCEngine::RHI::RHIDevice*>(1);
context.commandList = reinterpret_cast<XCEngine::RHI::RHICommandList*>(1);
context.commandQueue = reinterpret_cast<XCEngine::RHI::RHICommandQueue*>(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<XCEngine::RHI::RHIResourceView*>(1));
request.postProcess.sourceSurface = RenderSurface(256, 128);
request.postProcess.sourceSurface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(2));
request.postProcess.sourceColorView = reinterpret_cast<XCEngine::RHI::RHIResourceView*>(20);
request.postProcess.sourceColorState = XCEngine::RHI::ResourceStates::PixelShaderResource;
request.postProcess.destinationSurface = RenderSurface(512, 256);
request.postProcess.destinationSurface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(3));
request.finalOutput.sourceSurface = RenderSurface(512, 256);
request.finalOutput.sourceSurface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(4));
request.finalOutput.sourceColorView = reinterpret_cast<XCEngine::RHI::RHIResourceView*>(40);
request.finalOutput.sourceColorState = XCEngine::RHI::ResourceStates::PixelShaderResource;
request.finalOutput.destinationSurface = RenderSurface(640, 360);
request.finalOutput.destinationSurface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(5));
request.objectId.surface = RenderSurface(320, 180);
request.objectId.surface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(6));
request.objectId.surface.SetDepthAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(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<XCEngine::RHI::RHIResourceView*>(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<XCEngine::RHI::RHIResourceView*>(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<XCEngine::RHI::RHIResourceView*>(1));
request.sourceSurface.SetAutoTransitionEnabled(false);
request.sourceColorView = reinterpret_cast<XCEngine::RHI::RHIResourceView*>(2);
request.sourceColorState = XCEngine::RHI::ResourceStates::RenderTarget;
request.destinationSurface = RenderSurface(512, 256);
request.destinationSurface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(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<XCEngine::RHI::RHIResourceView*>(1));
request.sourceColorView = reinterpret_cast<XCEngine::RHI::RHIResourceView*>(2);
request.sourceColorState = XCEngine::RHI::ResourceStates::RenderTarget;
request.destinationSurface = RenderSurface(512, 256);
request.destinationSurface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(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<CameraComponent>();
primaryCamera->SetPrimary(true);
primaryCamera->SetDepth(10.0f);
GameObject* overrideCameraObject = scene.CreateGameObject("OverrideCamera");
auto* overrideCamera = overrideCameraObject->AddComponent<CameraComponent>();
overrideCamera->SetPrimary(false);
overrideCamera->SetDepth(-1.0f);
auto state = std::make_shared<MockPipelineState>();
CameraRenderer renderer(std::make_unique<MockPipeline>(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<CameraComponent>();
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<MockPipelineState>();
CameraRenderer renderer(std::make_unique<MockPipeline>(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<CameraComponent>();
camera->SetPrimary(true);
camera->SetSkyboxEnabled(true);
XCEngine::Resources::Material skyboxMaterial;
camera->SetSkyboxMaterial(&skyboxMaterial);
auto state = std::make_shared<MockPipelineState>();
CameraRenderer renderer(std::make_unique<MockPipeline>(state));
CameraRenderRequest request;
request.scene = &scene;
request.camera = camera;
request.context = CreateValidContext();
request.surface = RenderSurface(320, 200);
request.surface.SetDepthAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(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<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(3.0f);
auto state = std::make_shared<MockPipelineState>();
CameraRenderer renderer(std::make_unique<MockPipeline>(state));
RenderPassSequence prePasses;
prePasses.AddPass(std::make_unique<TrackingPass>(state, "pre"));
RenderPassSequence postPasses;
postPasses.AddPass(std::make_unique<TrackingPass>(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<std::string>{
"init:pre",
"pre",
"pipeline",
"init:post",
"post" }));
}
2026-04-01 16:44:11 +08:00
TEST(CameraRenderer_Test, ExecutesObjectIdPassBetweenPipelineAndPostPassesWhenRequested) {
Scene scene("CameraRendererObjectIdPassScene");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(3.0f);
auto state = std::make_shared<MockPipelineState>();
CameraRenderer renderer(
std::make_unique<MockPipeline>(state),
std::make_unique<MockObjectIdPass>(state));
RenderPassSequence prePasses;
prePasses.AddPass(std::make_unique<TrackingPass>(state, "pre"));
RenderPassSequence postPasses;
postPasses.AddPass(std::make_unique<TrackingPass>(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<XCEngine::RHI::RHIResourceView*>(1));
request.objectId.surface.SetDepthAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(2));
ASSERT_TRUE(renderer.Render(request));
EXPECT_EQ(
state->eventLog,
(std::vector<std::string>{
"init:pre",
"pre",
"pipeline",
"objectId",
"init:post",
"post" }));
2026-04-01 16:44:11 +08:00
}
TEST(CameraRenderer_Test, RoutesSceneColorThroughPostProcessAndFinalOutputStages) {
Scene scene("CameraRendererPostProcessScene");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(3.0f);
auto state = std::make_shared<MockPipelineState>();
CameraRenderer renderer(std::make_unique<MockPipeline>(state));
auto postProcessPass = std::make_unique<MockScenePass>(state, "postProcess");
MockScenePass* postProcessPassRaw = postProcessPass.get();
RenderPassSequence postProcessPasses;
postProcessPasses.AddPass(std::move(postProcessPass));
auto finalOutputPass = std::make_unique<MockScenePass>(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<XCEngine::RHI::RHIResourceView*>(1));
request.cameraDepth = camera->GetDepth();
request.postProcess.sourceSurface = RenderSurface(256, 128);
request.postProcess.sourceSurface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(2));
request.postProcess.sourceColorView = reinterpret_cast<XCEngine::RHI::RHIResourceView*>(20);
request.postProcess.destinationSurface = RenderSurface(512, 256);
request.postProcess.destinationSurface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(3));
request.postProcess.passes = &postProcessPasses;
request.finalOutput.sourceSurface = request.postProcess.destinationSurface;
request.finalOutput.sourceColorView = reinterpret_cast<XCEngine::RHI::RHIResourceView*>(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<XCEngine::RHI::RHIResourceView*>(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<XCEngine::RHI::RHIResourceView*>(30));
EXPECT_EQ(finalOutputPassRaw->lastSurfaceWidth, 800u);
EXPECT_EQ(finalOutputPassRaw->lastSurfaceHeight, 600u);
EXPECT_EQ(
state->eventLog,
(std::vector<std::string>{
"pipeline",
"init:postProcess",
"postProcess",
"init:finalOutput",
"finalOutput" }));
}
TEST(CameraRenderer_Test, ChainsMultiPassPostProcessThroughIntermediateSurface) {
Scene scene("CameraRendererMultiPassPostProcessScene");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(4.0f);
auto pipelineState = std::make_shared<MockPipelineState>();
auto allocationState = std::make_shared<MockShadowAllocationState>();
MockShadowDevice device(allocationState);
CameraRenderer renderer(std::make_unique<MockPipeline>(pipelineState));
auto firstPass = std::make_unique<MockScenePass>(pipelineState, "postProcessTint");
MockScenePass* firstPassRaw = firstPass.get();
auto secondPass = std::make_unique<MockScenePass>(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<std::string>{
"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<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(5.0f);
auto pipelineState = std::make_shared<MockPipelineState>();
auto allocationState = std::make_shared<MockShadowAllocationState>();
MockShadowDevice device(allocationState);
CameraRenderer renderer(std::make_unique<MockPipeline>(pipelineState));
auto postFirstPass = std::make_unique<MockScenePass>(pipelineState, "postA");
auto postSecondPass = std::make_unique<MockScenePass>(pipelineState, "postB");
MockScenePass* postSecondPassRaw = postSecondPass.get();
RenderPassSequence postProcessPasses;
postProcessPasses.AddPass(std::move(postFirstPass));
postProcessPasses.AddPass(std::move(postSecondPass));
auto finalFirstPass = std::make_unique<MockScenePass>(pipelineState, "finalA");
MockScenePass* finalFirstPassRaw = finalFirstPass.get();
auto finalSecondPass = std::make_unique<MockScenePass>(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<std::string>{
"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<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(3.0f);
auto state = std::make_shared<MockPipelineState>();
CameraRenderer renderer(
std::make_unique<MockPipeline>(state),
std::make_unique<MockObjectIdPass>(state));
auto shadowPass = std::make_unique<MockScenePass>(state, "shadowCaster");
renderer.SetShadowCasterPass(std::move(shadowPass));
auto depthPass = std::make_unique<MockScenePass>(state, "depthOnly");
renderer.SetDepthOnlyPass(std::move(depthPass));
RenderPassSequence prePasses;
prePasses.AddPass(std::make_unique<TrackingPass>(state, "pre"));
RenderPassSequence postProcessPasses;
postProcessPasses.AddPass(std::make_unique<TrackingPass>(state, "postProcess"));
RenderPassSequence finalOutputPasses;
finalOutputPasses.AddPass(std::make_unique<TrackingPass>(state, "finalOutput"));
RenderPassSequence postPasses;
postPasses.AddPass(std::make_unique<TrackingPass>(state, "post"));
RenderPassSequence overlayPasses;
overlayPasses.AddPass(std::make_unique<TrackingPass>(state, "overlay"));
CameraRenderRequest request;
request.scene = &scene;
request.camera = camera;
request.context = CreateValidContext();
request.surface = RenderSurface(320, 180);
request.surface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(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<XCEngine::RHI::RHIResourceView*>(1));
request.depthOnly.surface = RenderSurface(96, 48);
request.depthOnly.surface.SetDepthAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(2));
request.postProcess.sourceSurface = RenderSurface(256, 128);
request.postProcess.sourceSurface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(4));
request.postProcess.sourceColorView = reinterpret_cast<XCEngine::RHI::RHIResourceView*>(40);
request.postProcess.destinationSurface = RenderSurface(320, 180);
request.postProcess.destinationSurface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(5));
request.finalOutput.sourceSurface = request.postProcess.destinationSurface;
request.finalOutput.sourceColorView = reinterpret_cast<XCEngine::RHI::RHIResourceView*>(50);
request.finalOutput.destinationSurface = request.surface;
request.objectId.surface = RenderSurface(320, 180);
request.objectId.surface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(6));
request.objectId.surface.SetDepthAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(7));
ASSERT_TRUE(renderer.Render(request));
EXPECT_EQ(
state->eventLog,
(std::vector<std::string>{
"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<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(3.0f);
auto state = std::make_shared<MockPipelineState>();
CameraRenderer renderer(
std::make_unique<MockPipeline>(state),
std::make_unique<MockObjectIdPass>(state));
auto shadowPass = std::make_unique<MockScenePass>(state, "shadowCaster");
MockScenePass* shadowPassRaw = shadowPass.get();
renderer.SetShadowCasterPass(std::move(shadowPass));
auto depthPass = std::make_unique<MockScenePass>(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<XCEngine::RHI::RHIResourceView*>(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<XCEngine::RHI::RHIResourceView*>(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<std::string>{
"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<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(2.0f);
auto pipelineState = std::make_shared<MockPipelineState>();
auto allocationState = std::make_shared<MockShadowAllocationState>();
MockShadowDevice device(allocationState);
RenderContext context = CreateValidContext();
context.device = &device;
{
CameraRenderer renderer(
std::make_unique<MockPipeline>(pipelineState),
std::make_unique<MockObjectIdPass>(pipelineState));
auto shadowPass = std::make_unique<MockScenePass>(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<std::string>{
"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);
2026-04-04 23:01:34 +08:00
EXPECT_EQ(allocationState->shutdownShaderViewCalls, 1);
EXPECT_EQ(allocationState->shutdownTextureCalls, 1);
EXPECT_EQ(allocationState->destroyDepthViewCalls, 1);
2026-04-04 23:01:34 +08:00
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<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(2.0f);
auto pipelineState = std::make_shared<MockPipelineState>();
auto allocationState = std::make_shared<MockShadowAllocationState>();
MockShadowDevice device(allocationState);
RenderContext context = CreateValidContext();
context.device = &device;
CameraRenderer renderer(
std::make_unique<MockPipeline>(pipelineState),
std::make_unique<MockObjectIdPass>(pipelineState));
auto shadowPass = std::make_unique<MockScenePass>(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<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(2.0f);
auto pipelineState = std::make_shared<MockPipelineState>();
auto allocationState = std::make_shared<MockShadowAllocationState>();
MockShadowDevice device(allocationState);
RenderContext context = CreateValidContext();
context.device = &device;
{
CameraRenderer renderer(
std::make_unique<MockPipeline>(pipelineState),
std::make_unique<MockObjectIdPass>(pipelineState));
auto shadowPass = std::make_unique<MockScenePass>(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<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(2.0f);
auto pipelineState = std::make_shared<MockPipelineState>();
auto allocationState = std::make_shared<MockShadowAllocationState>();
MockShadowDevice device(allocationState);
RenderContext context = CreateValidContext();
context.device = &device;
{
CameraRenderer renderer(
std::make_unique<MockPipeline>(pipelineState),
std::make_unique<MockObjectIdPass>(pipelineState));
auto shadowPass = std::make_unique<MockScenePass>(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<std::string>{ "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<std::string>{
"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<CameraComponent>();
camera->SetPrimary(true);
auto state = std::make_shared<MockPipelineState>();
CameraRenderer renderer(std::make_unique<MockPipeline>(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<XCEngine::RHI::RHIResourceView*>(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<CameraComponent>();
camera->SetPrimary(true);
auto state = std::make_shared<MockPipelineState>();
CameraRenderer renderer(std::make_unique<MockPipeline>(state));
RenderPassSequence postProcessPasses;
postProcessPasses.AddPass(std::make_unique<TrackingPass>(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<XCEngine::RHI::RHIResourceView*>(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<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(2.0f);
auto state = std::make_shared<MockPipelineState>();
state->renderResult = false;
CameraRenderer renderer(std::make_unique<MockPipeline>(state));
RenderPassSequence prePasses;
prePasses.AddPass(std::make_unique<TrackingPass>(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<std::string>{ "init:pre", "pre", "pipeline" }));
}
2026-04-01 16:44:11 +08:00
TEST(CameraRenderer_Test, StopsRenderingWhenObjectIdPassFails) {
Scene scene("CameraRendererObjectIdFailureScene");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(2.0f);
auto state = std::make_shared<MockPipelineState>();
CameraRenderer renderer(
std::make_unique<MockPipeline>(state),
std::make_unique<MockObjectIdPass>(state, false));
RenderPassSequence prePasses;
prePasses.AddPass(std::make_unique<TrackingPass>(state, "pre"));
RenderPassSequence postPasses;
postPasses.AddPass(std::make_unique<TrackingPass>(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<XCEngine::RHI::RHIResourceView*>(1));
request.objectId.surface.SetDepthAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(2));
EXPECT_FALSE(renderer.Render(request));
EXPECT_EQ(
state->eventLog,
(std::vector<std::string>{ "init:pre", "pre", "pipeline", "objectId" }));
2026-04-01 16:44:11 +08:00
}
TEST(CameraRenderer_Test, ShutsDownSequencesWhenPostPassInitializationFails) {
Scene scene("CameraRendererPostPassInitFailureScene");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(4.0f);
auto state = std::make_shared<MockPipelineState>();
CameraRenderer renderer(std::make_unique<MockPipeline>(state));
RenderPassSequence prePasses;
prePasses.AddPass(std::make_unique<TrackingPass>(state, "pre"));
RenderPassSequence postPasses;
postPasses.AddPass(std::make_unique<TrackingPass>(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<std::string>{
"init:pre",
"pre",
"pipeline",
"init:post",
"shutdown:post" }));
}
TEST(SceneRenderer_Test, BuildsSortedRequestsForAllUsableCamerasAndHonorsOverrideCamera) {
Scene scene("SceneRendererRequestScene");
GameObject* lowCameraObject = scene.CreateGameObject("LowCamera");
auto* lowCamera = lowCameraObject->AddComponent<CameraComponent>();
lowCamera->SetPrimary(true);
lowCamera->SetDepth(1.0f);
GameObject* highCameraObject = scene.CreateGameObject("HighCamera");
auto* highCamera = highCameraObject->AddComponent<CameraComponent>();
highCamera->SetPrimary(true);
highCamera->SetDepth(5.0f);
2026-04-01 01:33:46 +08:00
highCamera->SetClearMode(CameraClearMode::None);
SceneRenderer renderer;
const RenderContext context = CreateValidContext();
const RenderSurface surface(320, 180);
const std::vector<CameraRenderRequest> 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);
2026-04-01 01:33:46 +08:00
EXPECT_EQ(defaultRequests[1].clearFlags, RenderClearFlags::None);
const std::vector<CameraRenderRequest> 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<CameraComponent>();
lateBaseCamera->SetDepth(10.0f);
lateBaseCamera->SetStackType(CameraStackType::Base);
GameObject* earlyBaseCameraObject = scene.CreateGameObject("EarlyBaseCamera");
auto* earlyBaseCamera = earlyBaseCameraObject->AddComponent<CameraComponent>();
earlyBaseCamera->SetDepth(1.0f);
earlyBaseCamera->SetStackType(CameraStackType::Base);
GameObject* overlayCameraObject = scene.CreateGameObject("OverlayCamera");
auto* overlayCamera = overlayCameraObject->AddComponent<CameraComponent>();
overlayCamera->SetDepth(-10.0f);
overlayCamera->SetStackType(CameraStackType::Overlay);
SceneRenderer renderer;
const std::vector<CameraRenderRequest> 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<CameraComponent>();
firstCamera->SetPrimary(false);
firstCamera->SetDepth(2.0f);
firstCamera->SetStackType(CameraStackType::Base);
GameObject* secondCameraObject = scene.CreateGameObject("SecondCamera");
auto* secondCamera = secondCameraObject->AddComponent<CameraComponent>();
secondCamera->SetPrimary(false);
secondCamera->SetDepth(2.0f);
secondCamera->SetStackType(CameraStackType::Base);
SceneRenderer renderer;
const std::vector<CameraRenderRequest> 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<CameraComponent>();
firstOverlay->SetDepth(1.0f);
firstOverlay->SetStackType(CameraStackType::Overlay);
GameObject* secondOverlayObject = scene.CreateGameObject("SecondOverlay");
auto* secondOverlay = secondOverlayObject->AddComponent<CameraComponent>();
secondOverlay->SetDepth(2.0f);
secondOverlay->SetStackType(CameraStackType::Overlay);
SceneRenderer renderer;
const std::vector<CameraRenderRequest> 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);
}
2026-04-01 01:33:46 +08:00
TEST(SceneRenderer_Test, HonorsExplicitOverrideCameraClearMode) {
Scene scene("SceneRendererOverrideClearModeScene");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(2.0f);
camera->SetClearMode(CameraClearMode::DepthOnly);
SceneRenderer renderer;
const std::vector<CameraRenderRequest> 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<CameraComponent>();
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<CameraRenderRequest> 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<CameraComponent>();
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<CameraRenderRequest> 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<CameraComponent>();
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<CameraRenderRequest> 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<CameraComponent>();
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<MockShadowAllocationState>();
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<CameraRenderRequest> 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<CameraComponent>();
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<MockPipelineAssetState>();
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<MockPipelineAsset>(assetState));
const std::vector<CameraRenderRequest> 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<CameraComponent>();
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<MockShadowAllocationState>();
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<MockPipelineAssetState>();
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<MockPipelineAsset>(assetState));
const std::vector<CameraRenderRequest> 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<CameraComponent>();
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<MockShadowAllocationState>();
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<CameraRenderRequest> 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<CameraComponent>();
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<MockShadowAllocationState>();
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<CameraRenderRequest> 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<CameraComponent>();
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<MockPipelineState>();
auto allocationState = std::make_shared<MockShadowAllocationState>();
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<MockPipeline>(pipelineState));
std::vector<CameraRenderRequest> 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<MockScenePass>(pipelineState, "postProcess"));
firstFrameRequests[0].postProcess.passes = &postProcessPasses;
ASSERT_TRUE(renderer.Render(firstFrameRequests));
const std::vector<CameraRenderRequest> 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<CameraComponent>();
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<MockPipelineState>();
auto allocationState = std::make_shared<MockShadowAllocationState>();
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<MockPipeline>(pipelineState));
std::vector<CameraRenderRequest> 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<MockScenePass>(pipelineState, "postProcess"));
RenderPassSequence finalOutputPasses;
finalOutputPasses.AddPass(std::make_unique<MockScenePass>(pipelineState, "finalOutput"));
firstFrameRequests[0].postProcess.passes = &postProcessPasses;
firstFrameRequests[0].finalOutput.passes = &finalOutputPasses;
ASSERT_TRUE(renderer.Render(firstFrameRequests));
const std::vector<CameraRenderRequest> 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<CameraComponent>();
camera->SetPrimary(true);
camera->SetViewportRect(XCEngine::Math::Rect(0.125f, 0.25f, 0.5f, 0.5f));
auto state = std::make_shared<MockPipelineState>();
CameraRenderer renderer(std::make_unique<MockPipeline>(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<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(2.0f);
auto initialState = std::make_shared<MockPipelineState>();
auto replacementState = std::make_shared<MockPipelineState>();
{
auto initialPipeline = std::make_unique<MockPipeline>(initialState);
MockPipeline* initialPipelineRaw = initialPipeline.get();
SceneRenderer renderer(std::move(initialPipeline));
EXPECT_EQ(renderer.GetPipeline(), initialPipelineRaw);
auto replacementPipeline = std::make_unique<MockPipeline>(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<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(2.0f);
auto initialAssetState = std::make_shared<MockPipelineAssetState>();
auto replacementAssetState = std::make_shared<MockPipelineAssetState>();
{
SceneRenderer renderer(std::make_shared<MockPipelineAsset>(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<MockPipelineAsset>(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<CameraComponent>();
farCamera->SetPrimary(true);
farCamera->SetDepth(10.0f);
GameObject* nearCameraObject = scene.CreateGameObject("NearCamera");
auto* nearCamera = nearCameraObject->AddComponent<CameraComponent>();
nearCamera->SetPrimary(false);
nearCamera->SetDepth(1.0f);
auto state = std::make_shared<MockPipelineState>();
SceneRenderer renderer(std::make_unique<MockPipeline>(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<CameraRenderRequest> 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<CameraComponent>();
firstCamera->SetPrimary(false);
firstCamera->SetDepth(2.0f);
GameObject* secondCameraObject = scene.CreateGameObject("SecondCamera");
auto* secondCamera = secondCameraObject->AddComponent<CameraComponent>();
secondCamera->SetPrimary(false);
secondCamera->SetDepth(2.0f);
auto state = std::make_shared<MockPipelineState>();
SceneRenderer renderer(std::make_unique<MockPipeline>(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<CameraRenderRequest> 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);
}