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

1393 lines
56 KiB
C++

#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>
#include <XCEngine/Core/Math/Vector4.h>
#include <XCEngine/Rendering/CameraRenderer.h>
#include <XCEngine/Rendering/ObjectIdPass.h>
#include <XCEngine/Rendering/RenderPipelineAsset.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/Rendering/SceneRenderer.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();
bool lastHasMainDirectionalShadow = false;
XCEngine::RHI::RHIResourceView* lastShadowMap = nullptr;
XCEngine::Math::Matrix4x4 lastShadowViewProjection = XCEngine::Math::Matrix4x4::Identity();
XCEngine::Math::Vector4 lastShadowParams = XCEngine::Math::Vector4::Zero();
std::vector<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 createDepthViewCalls = 0;
int shutdownDepthViewCalls = 0;
int destroyDepthViewCalls = 0;
int createShaderViewCalls = 0;
int shutdownShaderViewCalls = 0;
int destroyShaderViewCalls = 0;
uint32_t lastTextureWidth = 0;
uint32_t lastTextureHeight = 0;
XCEngine::RHI::Format lastTextureFormat = XCEngine::RHI::Format::Unknown;
XCEngine::RHI::Format lastDepthViewFormat = XCEngine::RHI::Format::Unknown;
XCEngine::RHI::Format lastShaderViewFormat = XCEngine::RHI::Format::Unknown;
};
class MockShadowTexture final : public XCEngine::RHI::RHITexture {
public:
MockShadowTexture(
std::shared_ptr<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 {
if (m_viewType == XCEngine::RHI::ResourceViewType::ShaderResource) {
++m_state->destroyShaderViewCalls;
} else {
++m_state->destroyDepthViewCalls;
}
}
void Shutdown() override {
if (m_viewType == XCEngine::RHI::ResourceViewType::ShaderResource) {
++m_state->shutdownShaderViewCalls;
} else {
++m_state->shutdownDepthViewCalls;
}
}
void* GetNativeHandle() override { return nullptr; }
bool IsValid() const override { return true; }
XCEngine::RHI::ResourceViewType GetViewType() const override { return m_viewType; }
XCEngine::RHI::ResourceViewDimension GetDimension() const override { return m_dimension; }
XCEngine::RHI::Format GetFormat() const override { return m_format; }
private:
std::shared_ptr<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&) override { return nullptr; }
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::RHITexture*,
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::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;
};
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;
m_state->lastHasMainDirectionalShadow = sceneData.lighting.HasMainDirectionalShadow();
m_state->lastShadowMap = sceneData.lighting.mainDirectionalShadow.shadowMap;
m_state->lastShadowViewProjection = sceneData.lighting.mainDirectionalShadow.viewProjection;
m_state->lastShadowParams = sceneData.lighting.mainDirectionalShadow.shadowParams;
m_state->renderedCameras.push_back(sceneData.camera);
m_state->renderedClearFlags.push_back(sceneData.cameraData.clearFlags);
m_state->renderedClearColors.push_back(sceneData.cameraData.clearColor);
return m_state->renderResult;
}
private:
std::shared_ptr<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);
}
private:
std::shared_ptr<MockPipelineAssetState> m_state;
};
class MockObjectIdPass final : public ObjectIdPass {
public:
MockObjectIdPass(
std::shared_ptr<MockPipelineState> state,
bool renderResult = true)
: m_state(std::move(state))
, m_renderResult(renderResult) {
}
bool Render(
const RenderContext&,
const RenderSurface&,
const RenderSceneData&) override {
m_state->eventLog.push_back("objectId");
return m_renderResult;
}
void Shutdown() override {
m_state->eventLog.push_back("shutdown:objectId");
}
private:
std::shared_ptr<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();
return m_executeResult;
}
void Shutdown() override {
m_state->eventLog.push_back(std::string("shutdown:") + m_label);
}
uint32_t lastViewportWidth = 0;
uint32_t lastViewportHeight = 0;
RenderClearFlags lastClearFlags = RenderClearFlags::All;
XCEngine::Math::Color lastClearColor = XCEngine::Math::Color::Black();
XCEngine::Math::Vector3 lastWorldPosition = XCEngine::Math::Vector3::Zero();
uint32_t lastSurfaceWidth = 0;
uint32_t lastSurfaceHeight = 0;
private:
std::shared_ptr<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&) override {
m_state->eventLog.push_back(m_label);
return m_executeResult;
}
void Shutdown() override {
m_state->eventLog.push_back(std::string("shutdown:") + m_label);
}
private:
std::shared_ptr<MockPipelineState> m_state;
const char* m_label = "";
bool m_initializeResult = true;
bool m_executeResult = true;
};
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(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, 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",
"shutdown:post",
"shutdown:pre" }));
}
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",
"shutdown:post",
"shutdown:pre" }));
}
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->lastShadowParams.x, 0.0015f);
EXPECT_FLOAT_EQ(pipelineState->lastShadowParams.y, 1.0f / 256.0f);
EXPECT_FLOAT_EQ(pipelineState->lastShadowParams.z, 1.0f / 128.0f);
EXPECT_FLOAT_EQ(pipelineState->lastShadowParams.w, 0.85f);
}
EXPECT_EQ(allocationState->shutdownDepthViewCalls, 1);
EXPECT_EQ(allocationState->shutdownShaderViewCalls, 1);
EXPECT_EQ(allocationState->shutdownTextureCalls, 1);
EXPECT_EQ(allocationState->destroyDepthViewCalls, 1);
EXPECT_EQ(allocationState->destroyShaderViewCalls, 1);
EXPECT_EQ(allocationState->destroyTextureCalls, 1);
}
TEST(CameraRenderer_Test, ReusesDirectionalShadowSurfaceWhenPlanMatches) {
Scene scene("CameraRendererDirectionalShadowReuseScene");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<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, 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, 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", "shutdown:pre" }));
}
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", "shutdown:pre" }));
}
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",
"shutdown:pre" }));
}
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);
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);
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);
}
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(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);
}