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

4505 lines
192 KiB
C++

#include <gtest/gtest.h>
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/LightComponent.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/Execution/CameraRenderer.h>
#include <XCEngine/Rendering/Execution/RenderPipelineHost.h>
#include <XCEngine/Rendering/Graph/RenderGraph.h>
#include <XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h>
#include <XCEngine/Rendering/Pipelines/ScriptableRenderPipelineHost.h>
#include <XCEngine/Rendering/RenderPassGraphContract.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 "Rendering/Execution/Internal/CameraFrameGraph/SurfaceResolver.h"
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>
using namespace XCEngine::Components;
using namespace XCEngine::Rendering;
namespace {
const RenderSurface* ResolveStageOutputSurface(
CameraFrameStage stage,
const CameraFramePlan& plan) {
return ResolveCameraFrameStageOutputSurface(
stage,
plan,
DirectionalShadowExecutionState{});
}
CameraFrameStageSourceBinding ResolveStageSourceBinding(
CameraFrameStage stage,
const CameraFramePlan& plan) {
return ResolveCameraFrameStageSourceBinding(stage, plan);
}
struct MockPipelineState {
int initializeCalls = 0;
int shutdownCalls = 0;
int configureRenderSceneDataCalls = 0;
int configureDirectionalShadowExecutionStateCalls = 0;
int renderCalls = 0;
int recordMainSceneCalls = 0;
int executeRecordedMainSceneCalls = 0;
bool renderResult = true;
bool supportsMainSceneRenderGraph = false;
bool recordMainSceneResult = true;
bool lastSurfaceAutoTransitionEnabled = true;
uint32_t lastSurfaceWidth = 0;
uint32_t lastSurfaceHeight = 0;
int32_t lastRenderAreaX = 0;
int32_t lastRenderAreaY = 0;
int32_t lastRenderAreaWidth = 0;
int32_t lastRenderAreaHeight = 0;
uint32_t lastCameraViewportWidth = 0;
uint32_t lastCameraViewportHeight = 0;
CameraComponent* lastCamera = nullptr;
size_t lastVisibleItemCount = 0;
RenderClearFlags lastClearFlags = RenderClearFlags::All;
XCEngine::Math::Color lastClearColor = XCEngine::Math::Color::Black();
bool lastHasMainDirectionalShadow = false;
XCEngine::RHI::RHIResourceView* lastShadowMap = nullptr;
XCEngine::Math::Matrix4x4 lastShadowViewProjection = XCEngine::Math::Matrix4x4::Identity();
RenderDirectionalShadowMapMetrics lastShadowMapMetrics = {};
RenderDirectionalShadowSamplingData lastShadowSampling = {};
bool lastHasMainDirectionalShadowKeyword = false;
RenderEnvironmentMode lastEnvironmentMode = RenderEnvironmentMode::None;
bool lastHasSkybox = false;
const XCEngine::Resources::Material* lastSkyboxMaterial = nullptr;
bool lastRecordedMainSceneShadowHandleValid = false;
bool lastReceivedRenderGraphBlackboard = false;
bool lastBlackboardMainSceneColorValid = false;
bool lastBlackboardMainSceneDepthValid = false;
bool lastBlackboardMainDirectionalShadowValid = false;
bool lastBlackboardObjectIdColorValid = false;
std::function<void(const CameraFramePlan&, RenderSceneData&)> configureRenderSceneData = {};
std::function<bool(
const CameraFramePlan&,
const DirectionalShadowSurfaceAllocation&,
DirectionalShadowExecutionState&)> configureDirectionalShadowExecutionState = {};
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;
int createShaderViewCalls = 0;
int shutdownShaderViewCalls = 0;
int destroyShaderViewCalls = 0;
uint32_t lastTextureWidth = 0;
uint32_t lastTextureHeight = 0;
XCEngine::RHI::Format lastTextureFormat = XCEngine::RHI::Format::Unknown;
XCEngine::RHI::Format lastRenderTargetViewFormat = XCEngine::RHI::Format::Unknown;
XCEngine::RHI::Format lastDepthViewFormat = XCEngine::RHI::Format::Unknown;
XCEngine::RHI::Format lastShaderViewFormat = XCEngine::RHI::Format::Unknown;
};
class MockShadowTexture final : public XCEngine::RHI::RHITexture {
public:
MockShadowTexture(
std::shared_ptr<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 if (m_viewType == XCEngine::RHI::ResourceViewType::RenderTarget) {
++m_state->destroyRenderTargetViewCalls;
} else {
++m_state->destroyDepthViewCalls;
}
}
void Shutdown() override {
if (m_viewType == XCEngine::RHI::ResourceViewType::ShaderResource) {
++m_state->shutdownShaderViewCalls;
} else if (m_viewType == XCEngine::RHI::ResourceViewType::RenderTarget) {
++m_state->shutdownRenderTargetViewCalls;
} else {
++m_state->shutdownDepthViewCalls;
}
}
void* GetNativeHandle() override { return nullptr; }
bool IsValid() const override { return true; }
XCEngine::RHI::ResourceViewType GetViewType() const override { return m_viewType; }
XCEngine::RHI::ResourceViewDimension GetDimension() const override { return m_dimension; }
XCEngine::RHI::Format GetFormat() const override { return m_format; }
private:
std::shared_ptr<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*,
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 = {};
};
class MockNoopCommandList final : public XCEngine::RHI::RHICommandList {
public:
void Shutdown() override {}
void Reset() override {}
void Close() override {}
void TransitionBarrier(
XCEngine::RHI::RHIResourceView*,
XCEngine::RHI::ResourceStates,
XCEngine::RHI::ResourceStates) override {
}
void BeginRenderPass(
XCEngine::RHI::RHIRenderPass*,
XCEngine::RHI::RHIFramebuffer*,
const XCEngine::RHI::Rect&,
uint32_t,
const XCEngine::RHI::ClearValue*) override {
}
void EndRenderPass() override {}
void SetShader(XCEngine::RHI::RHIShader*) override {}
void SetPipelineState(XCEngine::RHI::RHIPipelineState*) override {}
void SetGraphicsDescriptorSets(
uint32_t,
uint32_t,
XCEngine::RHI::RHIDescriptorSet**,
XCEngine::RHI::RHIPipelineLayout*) override {
}
void SetComputeDescriptorSets(
uint32_t,
uint32_t,
XCEngine::RHI::RHIDescriptorSet**,
XCEngine::RHI::RHIPipelineLayout*) override {
}
void SetPrimitiveTopology(XCEngine::RHI::PrimitiveTopology) override {}
void SetViewport(const XCEngine::RHI::Viewport&) override {}
void SetViewports(uint32_t, const XCEngine::RHI::Viewport*) override {}
void SetScissorRect(const XCEngine::RHI::Rect&) override {}
void SetScissorRects(uint32_t, const XCEngine::RHI::Rect*) override {}
void SetRenderTargets(
uint32_t,
XCEngine::RHI::RHIResourceView**,
XCEngine::RHI::RHIResourceView*) override {
}
void SetStencilRef(uint8_t) override {}
void SetBlendFactor(const float[4]) override {}
void SetVertexBuffers(
uint32_t,
uint32_t,
XCEngine::RHI::RHIResourceView**,
const uint64_t*,
const uint32_t*) override {
}
void SetIndexBuffer(XCEngine::RHI::RHIResourceView*, uint64_t) override {}
void Draw(uint32_t, uint32_t, uint32_t, uint32_t) override {}
void DrawIndexed(uint32_t, uint32_t, uint32_t, int32_t, uint32_t) override {}
void Clear(float, float, float, float, uint32_t) override {}
void ClearRenderTarget(
XCEngine::RHI::RHIResourceView*,
const float[4],
uint32_t,
const XCEngine::RHI::Rect*) override {
}
void ClearDepthStencil(
XCEngine::RHI::RHIResourceView*,
float,
uint8_t,
uint32_t,
const XCEngine::RHI::Rect*) override {
}
void CopyResource(XCEngine::RHI::RHIResourceView*, XCEngine::RHI::RHIResourceView*) override {}
void Dispatch(uint32_t, uint32_t, uint32_t) override {}
};
class MockNoopCommandQueue final : public XCEngine::RHI::RHICommandQueue {
public:
void Shutdown() override {}
void ExecuteCommandLists(uint32_t, void**) override {}
void Signal(XCEngine::RHI::RHIFence*, uint64_t) override {}
void Wait(XCEngine::RHI::RHIFence*, uint64_t) override {}
uint64_t GetCompletedValue() override { return 0u; }
void WaitForIdle() override {}
XCEngine::RHI::CommandQueueType GetType() const override {
return XCEngine::RHI::CommandQueueType::Direct;
}
uint64_t GetTimestampFrequency() const override { return 0u; }
void* GetNativeHandle() override { return nullptr; }
void WaitForPreviousFrame() override {}
uint64_t GetCurrentFrame() const override { return 0u; }
};
struct MockPipelineAssetState {
int createCalls = 0;
int configureCameraRenderRequestCalls = 0;
int configureCameraFramePlanCalls = 0;
bool createNullPipeline = false;
std::shared_ptr<MockPipelineState> lastCreatedPipelineState;
FinalColorSettings defaultFinalColorSettings = {};
std::function<void(
CameraRenderRequest&,
size_t,
size_t,
const DirectionalShadowPlanningSettings&)> configureCameraRenderRequest = {};
std::function<void(CameraFramePlan&)> configureCameraFramePlan = {};
};
struct MockStageRecorderState {
int initializeCalls = 0;
int shutdownCalls = 0;
int recordMainSceneCalls = 0;
bool supportsMainSceneRenderGraph = false;
bool recordMainSceneResult = true;
bool lastReceivedRenderGraphBlackboard = false;
};
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 ConfigureRenderSceneData(
const CameraFramePlan& plan,
RenderSceneData& sceneData) const override {
++m_state->configureRenderSceneDataCalls;
if (m_state->configureRenderSceneData) {
m_state->configureRenderSceneData(plan, sceneData);
return;
}
RenderPipeline::ConfigureRenderSceneData(plan, sceneData);
}
bool ConfigureDirectionalShadowExecutionState(
const CameraFramePlan& plan,
const DirectionalShadowSurfaceAllocation& shadowAllocation,
DirectionalShadowExecutionState& shadowState) const override {
++m_state->configureDirectionalShadowExecutionStateCalls;
if (m_state->configureDirectionalShadowExecutionState) {
return m_state->configureDirectionalShadowExecutionState(
plan,
shadowAllocation,
shadowState);
}
return RenderPipeline::ConfigureDirectionalShadowExecutionState(
plan,
shadowAllocation,
shadowState);
}
void Shutdown() override {
++m_state->shutdownCalls;
ShutdownCameraFrameStandalonePasses();
}
bool SupportsStageRenderGraph(CameraFrameStage stage) const override {
return SupportsCameraFramePipelineGraphRecording(stage) &&
m_state->supportsMainSceneRenderGraph;
}
bool RecordStageRenderGraph(
const RenderPipelineStageRenderGraphContext& context) override {
++m_state->recordMainSceneCalls;
m_state->lastReceivedRenderGraphBlackboard = context.blackboard != nullptr;
if (const CameraFrameRenderGraphResources* frameResources =
TryGetCameraFrameRenderGraphResources(context.blackboard)) {
m_state->lastBlackboardMainSceneColorValid =
frameResources->mainScene.color.IsValid();
m_state->lastBlackboardMainSceneDepthValid =
frameResources->mainScene.depth.IsValid();
m_state->lastBlackboardMainDirectionalShadowValid =
frameResources->mainDirectionalShadow.IsValid();
m_state->lastBlackboardObjectIdColorValid =
frameResources->objectId.color.IsValid();
m_state->lastRecordedMainSceneShadowHandleValid =
frameResources->mainDirectionalShadow.IsValid();
}
if (!m_state->recordMainSceneResult) {
return false;
}
const RenderSceneData* const sceneData = &context.sceneData;
RenderSurface surface = context.surfaceTemplate;
surface.SetAutoTransitionEnabled(false);
const std::vector<RenderGraphTextureHandle> colorTargets = context.colorTargets;
const RenderGraphTextureHandle depthTarget = context.depthTarget;
const CameraFrameRenderGraphResources* const frameResources =
TryGetCameraFrameRenderGraphResources(context.blackboard);
const RenderGraphTextureHandle mainDirectionalShadowTexture =
frameResources != nullptr
? frameResources->mainDirectionalShadow
: RenderGraphTextureHandle{};
bool* const executionSucceeded = context.executionSucceeded;
const std::shared_ptr<MockPipelineState> state = m_state;
context.graphBuilder.AddRasterPass(
context.passName,
[state,
surface,
sceneData,
colorTargets,
depthTarget,
mainDirectionalShadowTexture,
executionSucceeded](
RenderGraphPassBuilder& passBuilder) {
for (RenderGraphTextureHandle colorTarget : colorTargets) {
if (colorTarget.IsValid()) {
passBuilder.WriteTexture(colorTarget);
}
}
if (depthTarget.IsValid()) {
passBuilder.WriteDepthTexture(depthTarget);
}
if (mainDirectionalShadowTexture.IsValid()) {
passBuilder.ReadTexture(mainDirectionalShadowTexture);
}
passBuilder.SetExecuteCallback(
[state,
surface,
sceneData,
executionSucceeded](
const RenderGraphExecutionContext&) {
if (executionSucceeded != nullptr && !(*executionSucceeded)) {
return;
}
state->eventLog.push_back("pipelineGraph");
++state->executeRecordedMainSceneCalls;
state->lastSurfaceAutoTransitionEnabled =
surface.IsAutoTransitionEnabled();
state->lastSurfaceWidth = surface.GetWidth();
state->lastSurfaceHeight = surface.GetHeight();
const XCEngine::Math::RectInt renderArea = surface.GetRenderArea();
state->lastRenderAreaX = renderArea.x;
state->lastRenderAreaY = renderArea.y;
state->lastRenderAreaWidth = renderArea.width;
state->lastRenderAreaHeight = renderArea.height;
state->lastCamera = sceneData->camera;
state->lastCameraViewportWidth = sceneData->cameraData.viewportWidth;
state->lastCameraViewportHeight = sceneData->cameraData.viewportHeight;
state->lastVisibleItemCount = sceneData->visibleItems.size();
state->lastClearFlags = sceneData->cameraData.clearFlags;
state->lastClearColor = sceneData->cameraData.clearColor;
state->lastHasMainDirectionalShadow =
sceneData->lighting.HasMainDirectionalShadow();
state->lastShadowMap = sceneData->lighting.mainDirectionalShadow.shadowMap;
state->lastShadowViewProjection =
sceneData->lighting.mainDirectionalShadow.viewProjection;
state->lastShadowMapMetrics =
sceneData->lighting.mainDirectionalShadow.mapMetrics;
state->lastShadowSampling =
sceneData->lighting.mainDirectionalShadow.sampling;
state->lastHasMainDirectionalShadowKeyword =
XCEngine::Resources::ShaderKeywordSetContains(
sceneData->globalShaderKeywords,
"XC_MAIN_LIGHT_SHADOWS");
state->lastEnvironmentMode = sceneData->environment.mode;
state->lastHasSkybox = sceneData->environment.HasSkybox();
state->lastSkyboxMaterial =
sceneData->environment.materialSkybox.material;
state->renderedCameras.push_back(sceneData->camera);
state->renderedClearFlags.push_back(sceneData->cameraData.clearFlags);
state->renderedClearColors.push_back(sceneData->cameraData.clearColor);
if (executionSucceeded != nullptr) {
*executionSucceeded = state->renderResult;
}
});
});
return true;
}
bool Render(
const RenderContext&,
const RenderSurface& surface,
const RenderSceneData& sceneData) override {
m_state->eventLog.push_back("pipeline");
++m_state->renderCalls;
m_state->lastSurfaceAutoTransitionEnabled = surface.IsAutoTransitionEnabled();
m_state->lastSurfaceWidth = surface.GetWidth();
m_state->lastSurfaceHeight = surface.GetHeight();
const XCEngine::Math::RectInt renderArea = surface.GetRenderArea();
m_state->lastRenderAreaX = renderArea.x;
m_state->lastRenderAreaY = renderArea.y;
m_state->lastRenderAreaWidth = renderArea.width;
m_state->lastRenderAreaHeight = renderArea.height;
m_state->lastCamera = sceneData.camera;
m_state->lastCameraViewportWidth = sceneData.cameraData.viewportWidth;
m_state->lastCameraViewportHeight = sceneData.cameraData.viewportHeight;
m_state->lastVisibleItemCount = sceneData.visibleItems.size();
m_state->lastClearFlags = sceneData.cameraData.clearFlags;
m_state->lastClearColor = sceneData.cameraData.clearColor;
m_state->lastHasMainDirectionalShadow = sceneData.lighting.HasMainDirectionalShadow();
m_state->lastShadowMap = sceneData.lighting.mainDirectionalShadow.shadowMap;
m_state->lastShadowViewProjection = sceneData.lighting.mainDirectionalShadow.viewProjection;
m_state->lastShadowMapMetrics = sceneData.lighting.mainDirectionalShadow.mapMetrics;
m_state->lastShadowSampling = sceneData.lighting.mainDirectionalShadow.sampling;
m_state->lastHasMainDirectionalShadowKeyword =
XCEngine::Resources::ShaderKeywordSetContains(
sceneData.globalShaderKeywords,
"XC_MAIN_LIGHT_SHADOWS");
m_state->lastEnvironmentMode = sceneData.environment.mode;
m_state->lastHasSkybox = sceneData.environment.HasSkybox();
m_state->lastSkyboxMaterial = sceneData.environment.materialSkybox.material;
m_state->renderedCameras.push_back(sceneData.camera);
m_state->renderedClearFlags.push_back(sceneData.cameraData.clearFlags);
m_state->renderedClearColors.push_back(sceneData.cameraData.clearColor);
return m_state->renderResult;
}
private:
std::shared_ptr<MockPipelineState> m_state;
};
class MockStageRecorder final : public RenderPipelineStageRecorder {
public:
explicit MockStageRecorder(std::shared_ptr<MockStageRecorderState> state)
: m_state(std::move(state)) {
}
bool Initialize(const RenderContext&) override {
++m_state->initializeCalls;
return true;
}
void Shutdown() override {
++m_state->shutdownCalls;
}
bool SupportsStageRenderGraph(CameraFrameStage stage) const override {
return SupportsCameraFramePipelineGraphRecording(stage) &&
m_state->supportsMainSceneRenderGraph;
}
bool RecordStageRenderGraph(
const RenderPipelineStageRenderGraphContext& context) override {
++m_state->recordMainSceneCalls;
m_state->lastReceivedRenderGraphBlackboard = context.blackboard != nullptr;
return m_state->recordMainSceneResult;
}
private:
std::shared_ptr<MockStageRecorderState> m_state;
};
template <typename PassT, typename... Args>
PassT* InstallStandaloneStagePass(
RenderPipeline& pipeline,
CameraFrameStage stage,
Args&&... args) {
auto pass = std::make_unique<PassT>(std::forward<Args>(args)...);
PassT* const passRaw = pass.get();
pipeline.SetCameraFrameStandalonePass(stage, std::move(pass));
return passRaw;
}
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;
if (m_state->createNullPipeline) {
m_state->lastCreatedPipelineState.reset();
return nullptr;
}
m_state->lastCreatedPipelineState = std::make_shared<MockPipelineState>();
return std::make_unique<MockPipeline>(m_state->lastCreatedPipelineState);
}
FinalColorSettings GetDefaultFinalColorSettings() const override {
return m_state->defaultFinalColorSettings;
}
void ConfigureCameraRenderRequest(
CameraRenderRequest& request,
size_t renderedBaseCameraCount,
size_t renderedRequestCount,
const DirectionalShadowPlanningSettings& directionalShadowSettings) const override {
++m_state->configureCameraRenderRequestCalls;
if (m_state->configureCameraRenderRequest) {
m_state->configureCameraRenderRequest(
request,
renderedBaseCameraCount,
renderedRequestCount,
directionalShadowSettings);
return;
}
RenderPipelineAsset::ConfigureCameraRenderRequest(
request,
renderedBaseCameraCount,
renderedRequestCount,
directionalShadowSettings);
}
void ConfigureCameraFramePlan(CameraFramePlan& plan) const override {
++m_state->configureCameraFramePlanCalls;
if (m_state->configureCameraFramePlan) {
m_state->configureCameraFramePlan(plan);
return;
}
RenderPipelineAsset::ConfigureCameraFramePlan(plan);
}
private:
std::shared_ptr<MockPipelineAssetState> m_state;
};
struct MockManagedRenderPipelineBridgeState {
int createStageRecorderCalls = 0;
Pipelines::ManagedRenderPipelineAssetDescriptor lastDescriptor = {};
std::shared_ptr<MockStageRecorderState> lastCreatedStageRecorderState;
};
class MockManagedRenderPipelineBridge final
: public Pipelines::ManagedRenderPipelineBridge {
public:
explicit MockManagedRenderPipelineBridge(
std::shared_ptr<MockManagedRenderPipelineBridgeState> state)
: m_state(std::move(state)) {
}
std::unique_ptr<RenderPipelineStageRecorder> CreateStageRecorder(
const Pipelines::ManagedRenderPipelineAssetDescriptor& descriptor) const override {
++m_state->createStageRecorderCalls;
m_state->lastDescriptor = descriptor;
m_state->lastCreatedStageRecorderState =
std::make_shared<MockStageRecorderState>();
return std::make_unique<MockStageRecorder>(
m_state->lastCreatedStageRecorderState);
}
private:
std::shared_ptr<MockManagedRenderPipelineBridgeState> m_state;
};
class MockObjectIdPass final : public RenderPass {
public:
MockObjectIdPass(
std::shared_ptr<MockPipelineState> state,
bool renderResult = true,
bool supportsRenderGraph = false)
: m_state(std::move(state))
, m_renderResult(renderResult)
, m_supportsRenderGraph(supportsRenderGraph) {
}
const char* GetName() const override {
return "MockObjectIdPass";
}
bool SupportsRenderGraph() const override {
return m_supportsRenderGraph;
}
bool RecordRenderGraph(const RenderPassRenderGraphContext& context) override {
m_state->eventLog.push_back("record:objectId");
return RecordColorDepthRasterPass(
*this,
context);
}
bool Execute(const RenderPassContext& context) override {
m_state->eventLog.push_back("objectId");
lastSurfaceAutoTransitionEnabled = context.surface.IsAutoTransitionEnabled();
lastSurfaceWidth = context.surface.GetRenderAreaWidth();
lastSurfaceHeight = context.surface.GetRenderAreaHeight();
return m_renderResult;
}
void Shutdown() override {
m_state->eventLog.push_back("shutdown:objectId");
}
private:
std::shared_ptr<MockPipelineState> m_state;
bool m_renderResult = true;
bool m_supportsRenderGraph = false;
public:
bool lastSurfaceAutoTransitionEnabled = true;
uint32_t lastSurfaceWidth = 0;
uint32_t lastSurfaceHeight = 0;
};
class MockScenePass final : public RenderPass {
public:
MockScenePass(
std::shared_ptr<MockPipelineState> state,
const char* label,
bool initializeResult = true,
bool executeResult = true,
bool supportsRenderGraph = false)
: m_state(std::move(state))
, m_label(label)
, m_initializeResult(initializeResult)
, m_executeResult(executeResult)
, m_supportsRenderGraph(supportsRenderGraph) {
}
const char* GetName() const override {
return m_label;
}
bool SupportsRenderGraph() const override {
return m_supportsRenderGraph;
}
bool RecordRenderGraph(const RenderPassRenderGraphContext& context) override {
m_state->eventLog.push_back(std::string("record:") + m_label);
return RecordColorDepthRasterPass(
*this,
context);
}
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();
lastSurfaceAutoTransitionEnabled = context.surface.IsAutoTransitionEnabled();
lastHasSourceSurface = context.sourceSurface != nullptr;
if (context.sourceSurface != nullptr) {
lastSourceSurfaceWidth = context.sourceSurface->GetRenderAreaWidth();
lastSourceSurfaceHeight = context.sourceSurface->GetRenderAreaHeight();
lastSourceSurfaceAutoTransitionEnabled =
context.sourceSurface->IsAutoTransitionEnabled();
}
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 lastSurfaceAutoTransitionEnabled = true;
bool lastHasSourceSurface = false;
uint32_t lastSourceSurfaceWidth = 0;
uint32_t lastSourceSurfaceHeight = 0;
bool lastSourceSurfaceAutoTransitionEnabled = true;
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;
bool m_supportsRenderGraph = false;
};
class TrackingPass final : public RenderPass {
public:
TrackingPass(
std::shared_ptr<MockPipelineState> state,
const char* label,
bool initializeResult = true,
bool executeResult = true,
bool supportsRenderGraph = false)
: m_state(std::move(state))
, m_label(label)
, m_initializeResult(initializeResult)
, m_executeResult(executeResult)
, m_supportsRenderGraph(supportsRenderGraph) {
}
const char* GetName() const override {
return m_label;
}
bool SupportsRenderGraph() const override {
return m_supportsRenderGraph;
}
bool RecordRenderGraph(const RenderPassRenderGraphContext& context) override {
m_state->eventLog.push_back(std::string("record:") + m_label);
lastRecordedSourceColorTextureValid = context.sourceColorTexture.IsValid();
lastRecordedRenderGraphBlackboard = context.blackboard != nullptr;
if (const CameraFrameRenderGraphResources* frameResources =
TryGetCameraFrameRenderGraphResources(context.blackboard)) {
lastRecordedBlackboardMainSceneColorValid =
frameResources->mainScene.color.IsValid();
lastRecordedBlackboardPostProcessColorValid =
frameResources->postProcess.color.IsValid();
} else {
lastRecordedBlackboardMainSceneColorValid = false;
lastRecordedBlackboardPostProcessColorValid = false;
}
return RecordDeclaredRasterPass(
*this,
context);
}
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();
lastSurfaceAutoTransitionEnabled = context.surface.IsAutoTransitionEnabled();
lastHasSourceSurface = context.sourceSurface != nullptr;
if (context.sourceSurface != nullptr) {
lastSourceSurfaceWidth = context.sourceSurface->GetRenderAreaWidth();
lastSourceSurfaceHeight = context.sourceSurface->GetRenderAreaHeight();
lastSourceSurfaceAutoTransitionEnabled =
context.sourceSurface->IsAutoTransitionEnabled();
}
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;
bool m_supportsRenderGraph = false;
public:
uint32_t lastSurfaceWidth = 0;
uint32_t lastSurfaceHeight = 0;
bool lastSurfaceAutoTransitionEnabled = true;
bool lastHasSourceSurface = false;
uint32_t lastSourceSurfaceWidth = 0;
uint32_t lastSourceSurfaceHeight = 0;
bool lastSourceSurfaceAutoTransitionEnabled = true;
XCEngine::RHI::RHIResourceView* lastSourceColorView = nullptr;
XCEngine::RHI::ResourceStates lastSourceColorState = XCEngine::RHI::ResourceStates::Common;
bool lastRecordedSourceColorTextureValid = false;
bool lastRecordedRenderGraphBlackboard = false;
bool lastRecordedBlackboardMainSceneColorValid = false;
bool lastRecordedBlackboardPostProcessColorValid = false;
};
RenderContext CreateValidContext() {
static MockNoopCommandList s_commandList;
static MockNoopCommandQueue s_commandQueue;
RenderContext context;
context.device = reinterpret_cast<XCEngine::RHI::RHIDevice*>(1);
context.commandList = &s_commandList;
context.commandQueue = &s_commandQueue;
return context;
}
RenderSceneData CreateSceneDataForCamera(
Scene&,
CameraComponent& camera,
const RenderSurface& surface) {
RenderSceneData sceneData = {};
sceneData.camera = &camera;
sceneData.cameraData.viewportWidth = surface.GetRenderAreaWidth();
sceneData.cameraData.viewportHeight = surface.GetRenderAreaHeight();
sceneData.cameraData.clearFlags = RenderClearFlags::All;
return sceneData;
}
} // 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");
const CameraFramePlan plan = CameraFramePlan::FromRequest(request);
EXPECT_TRUE(plan.HasFrameStage(CameraFrameStage::PreScenePasses));
EXPECT_TRUE(plan.HasFrameStage(CameraFrameStage::ShadowCaster));
EXPECT_TRUE(plan.HasFrameStage(CameraFrameStage::DepthOnly));
EXPECT_TRUE(plan.HasFrameStage(CameraFrameStage::MainScene));
EXPECT_TRUE(plan.HasFrameStage(CameraFrameStage::PostProcess));
EXPECT_TRUE(plan.HasFrameStage(CameraFrameStage::FinalOutput));
EXPECT_TRUE(plan.HasFrameStage(CameraFrameStage::ObjectId));
EXPECT_TRUE(plan.HasFrameStage(CameraFrameStage::PostScenePasses));
EXPECT_TRUE(plan.HasFrameStage(CameraFrameStage::OverlayPasses));
EXPECT_EQ(plan.GetPassSequence(CameraFrameStage::PreScenePasses), &prePasses);
EXPECT_EQ(plan.GetPassSequence(CameraFrameStage::PostProcess), &postProcessPasses);
EXPECT_EQ(plan.GetPassSequence(CameraFrameStage::FinalOutput), &finalOutputPasses);
EXPECT_EQ(plan.GetPassSequence(CameraFrameStage::PostScenePasses), &postPasses);
EXPECT_EQ(plan.GetPassSequence(CameraFrameStage::OverlayPasses), &overlayPasses);
EXPECT_EQ(plan.GetFullscreenStagePlan(CameraFrameStage::PostProcess), &plan.colorChain.postProcess);
EXPECT_EQ(plan.GetFullscreenStagePlan(CameraFrameStage::FinalOutput), &plan.colorChain.finalOutput);
EXPECT_EQ(plan.GetFullscreenStagePlan(CameraFrameStage::MainScene), nullptr);
EXPECT_EQ(plan.GetFullscreenPassRequest(CameraFrameStage::PostProcess), &plan.postProcess);
EXPECT_EQ(plan.GetFullscreenPassRequest(CameraFrameStage::FinalOutput), &plan.finalOutput);
EXPECT_EQ(plan.GetFullscreenPassRequest(CameraFrameStage::MainScene), nullptr);
EXPECT_EQ(plan.GetScenePassRequest(CameraFrameStage::ShadowCaster), &plan.shadowCaster);
EXPECT_EQ(plan.GetScenePassRequest(CameraFrameStage::DepthOnly), &plan.request.depthOnly);
EXPECT_EQ(plan.GetScenePassRequest(CameraFrameStage::MainScene), nullptr);
EXPECT_EQ(plan.GetObjectIdRequest(CameraFrameStage::ObjectId), &plan.request.objectId);
EXPECT_EQ(plan.GetObjectIdRequest(CameraFrameStage::MainScene), nullptr);
EXPECT_EQ(plan.GetSharedStageOutputSurface(CameraFrameStage::PreScenePasses), &plan.GetMainSceneSurface());
EXPECT_EQ(plan.GetSharedStageOutputSurface(CameraFrameStage::OverlayPasses), &plan.GetFinalCompositedSurface());
EXPECT_EQ(plan.GetSharedStageOutputSurface(CameraFrameStage::PostProcess), nullptr);
EXPECT_TRUE(plan.RequiresIntermediateSceneColor());
EXPECT_EQ(plan.GetMainSceneSurface().GetRenderAreaWidth(), 256u);
EXPECT_EQ(plan.GetMainSceneSurface().GetRenderAreaHeight(), 128u);
EXPECT_EQ(plan.GetFinalCompositedSurface().GetRenderAreaWidth(), 640u);
EXPECT_EQ(plan.GetFinalCompositedSurface().GetRenderAreaHeight(), 360u);
const RenderSurface* const postProcessOutput =
ResolveStageOutputSurface(CameraFrameStage::PostProcess, plan);
const CameraFrameStageSourceBinding postProcessSource =
ResolveStageSourceBinding(CameraFrameStage::PostProcess, plan);
ASSERT_NE(postProcessOutput, nullptr);
EXPECT_EQ(postProcessOutput->GetRenderAreaWidth(), 512u);
ASSERT_NE(postProcessSource.sourceSurface, nullptr);
EXPECT_EQ(postProcessSource.sourceSurface->GetRenderAreaWidth(), 256u);
EXPECT_EQ(
postProcessSource.sourceColorView,
reinterpret_cast<XCEngine::RHI::RHIResourceView*>(20));
EXPECT_EQ(
postProcessSource.sourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
const CameraFrameStageSourceBinding finalOutputSource =
ResolveStageSourceBinding(CameraFrameStage::FinalOutput, plan);
ASSERT_NE(finalOutputSource.sourceSurface, nullptr);
EXPECT_EQ(finalOutputSource.sourceSurface->GetRenderAreaWidth(), 512u);
EXPECT_EQ(
finalOutputSource.sourceColorView,
reinterpret_cast<XCEngine::RHI::RHIResourceView*>(40));
EXPECT_EQ(
finalOutputSource.sourceColorState,
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(CameraFramePlan::FromRequest(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(CameraFramePlan::FromRequest(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(CameraFramePlan::FromRequest(request)));
EXPECT_TRUE(state->lastHasSkybox);
EXPECT_EQ(state->lastEnvironmentMode, RenderEnvironmentMode::MaterialSkybox);
EXPECT_EQ(state->lastSkyboxMaterial, &skyboxMaterial);
}
TEST(CameraRenderer_Test, UsesPipelineSceneDataConfigurationHook) {
Scene scene("CameraRendererSceneDataHookScene");
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>();
state->configureRenderSceneData = [](
const CameraFramePlan&,
RenderSceneData& sceneData) {
sceneData.environment = {};
sceneData.globalShaderKeywords = {};
};
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(CameraFramePlan::FromRequest(request)));
EXPECT_EQ(state->configureRenderSceneDataCalls, 1);
EXPECT_FALSE(state->lastHasSkybox);
EXPECT_EQ(state->lastEnvironmentMode, RenderEnvironmentMode::None);
EXPECT_EQ(state->lastSkyboxMaterial, nullptr);
}
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(CameraFramePlan::FromRequest(request)));
EXPECT_EQ(
state->eventLog,
(std::vector<std::string>{
"init:pre",
"pre",
"pipeline",
"init:post",
"post" }));
}
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>();
auto pipeline = std::make_unique<MockPipeline>(state);
MockObjectIdPass* objectIdPassRaw =
InstallStandaloneStagePass<MockObjectIdPass>(
*pipeline,
CameraFrameStage::ObjectId,
state);
CameraRenderer renderer(std::move(pipeline));
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(CameraFramePlan::FromRequest(request)));
EXPECT_EQ(
state->eventLog,
(std::vector<std::string>{
"init:pre",
"pre",
"pipeline",
"objectId",
"init:post",
"post" }));
ASSERT_NE(objectIdPassRaw, nullptr);
EXPECT_FALSE(objectIdPassRaw->lastSurfaceAutoTransitionEnabled);
EXPECT_EQ(objectIdPassRaw->lastSurfaceWidth, 320u);
EXPECT_EQ(objectIdPassRaw->lastSurfaceHeight, 180u);
}
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(CameraFramePlan::FromRequest(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_FALSE(postProcessPassRaw->lastSourceSurfaceAutoTransitionEnabled);
EXPECT_EQ(
postProcessPassRaw->lastSourceColorView,
reinterpret_cast<XCEngine::RHI::RHIResourceView*>(20));
EXPECT_EQ(
postProcessPassRaw->lastSourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
EXPECT_EQ(postProcessPassRaw->lastSurfaceWidth, 512u);
EXPECT_EQ(postProcessPassRaw->lastSurfaceHeight, 256u);
EXPECT_FALSE(postProcessPassRaw->lastSurfaceAutoTransitionEnabled);
ASSERT_NE(finalOutputPassRaw, nullptr);
EXPECT_TRUE(finalOutputPassRaw->lastHasSourceSurface);
EXPECT_EQ(finalOutputPassRaw->lastSourceSurfaceWidth, 512u);
EXPECT_EQ(finalOutputPassRaw->lastSourceSurfaceHeight, 256u);
EXPECT_FALSE(finalOutputPassRaw->lastSourceSurfaceAutoTransitionEnabled);
EXPECT_EQ(
finalOutputPassRaw->lastSourceColorView,
reinterpret_cast<XCEngine::RHI::RHIResourceView*>(30));
EXPECT_EQ(
finalOutputPassRaw->lastSourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
EXPECT_EQ(finalOutputPassRaw->lastSurfaceWidth, 800u);
EXPECT_EQ(finalOutputPassRaw->lastSurfaceHeight, 600u);
EXPECT_FALSE(finalOutputPassRaw->lastSurfaceAutoTransitionEnabled);
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(CameraFramePlan::FromRequest(request)));
ASSERT_NE(firstPassRaw, nullptr);
EXPECT_TRUE(firstPassRaw->lastHasSourceSurface);
EXPECT_EQ(firstPassRaw->lastSourceSurfaceWidth, 256u);
EXPECT_EQ(firstPassRaw->lastSourceSurfaceHeight, 128u);
EXPECT_FALSE(firstPassRaw->lastSourceSurfaceAutoTransitionEnabled);
EXPECT_EQ(firstPassRaw->lastSourceColorView, sourceColorShaderView);
EXPECT_EQ(
firstPassRaw->lastSourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
EXPECT_EQ(firstPassRaw->lastSurfaceWidth, 512u);
EXPECT_EQ(firstPassRaw->lastSurfaceHeight, 256u);
EXPECT_FALSE(firstPassRaw->lastSurfaceAutoTransitionEnabled);
ASSERT_NE(secondPassRaw, nullptr);
EXPECT_TRUE(secondPassRaw->lastHasSourceSurface);
EXPECT_EQ(secondPassRaw->lastSourceSurfaceWidth, 512u);
EXPECT_EQ(secondPassRaw->lastSourceSurfaceHeight, 256u);
EXPECT_FALSE(secondPassRaw->lastSourceSurfaceAutoTransitionEnabled);
EXPECT_NE(secondPassRaw->lastSourceColorView, nullptr);
EXPECT_NE(secondPassRaw->lastSourceColorView, sourceColorShaderView);
EXPECT_EQ(
secondPassRaw->lastSourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
EXPECT_EQ(secondPassRaw->lastSurfaceWidth, 512u);
EXPECT_EQ(secondPassRaw->lastSurfaceHeight, 256u);
EXPECT_FALSE(secondPassRaw->lastSurfaceAutoTransitionEnabled);
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(CameraFramePlan::FromRequest(request)));
ASSERT_NE(postSecondPassRaw, nullptr);
EXPECT_EQ(postSecondPassRaw->lastSourceSurfaceWidth, 512u);
EXPECT_EQ(postSecondPassRaw->lastSourceSurfaceHeight, 256u);
EXPECT_FALSE(postSecondPassRaw->lastSourceSurfaceAutoTransitionEnabled);
EXPECT_EQ(
postSecondPassRaw->lastSourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
EXPECT_FALSE(postSecondPassRaw->lastSurfaceAutoTransitionEnabled);
ASSERT_NE(finalFirstPassRaw, nullptr);
EXPECT_EQ(finalFirstPassRaw->lastSourceSurfaceWidth, 512u);
EXPECT_EQ(finalFirstPassRaw->lastSourceSurfaceHeight, 256u);
EXPECT_FALSE(finalFirstPassRaw->lastSourceSurfaceAutoTransitionEnabled);
EXPECT_EQ(finalFirstPassRaw->lastSourceColorView, finalOutputSource);
EXPECT_EQ(
finalFirstPassRaw->lastSourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
EXPECT_FALSE(finalFirstPassRaw->lastSurfaceAutoTransitionEnabled);
ASSERT_NE(finalSecondPassRaw, nullptr);
EXPECT_EQ(finalSecondPassRaw->lastSourceSurfaceWidth, 800u);
EXPECT_EQ(finalSecondPassRaw->lastSourceSurfaceHeight, 600u);
EXPECT_FALSE(finalSecondPassRaw->lastSourceSurfaceAutoTransitionEnabled);
EXPECT_NE(finalSecondPassRaw->lastSourceColorView, nullptr);
EXPECT_NE(finalSecondPassRaw->lastSourceColorView, finalOutputSource);
EXPECT_EQ(
finalSecondPassRaw->lastSourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
EXPECT_FALSE(finalSecondPassRaw->lastSurfaceAutoTransitionEnabled);
EXPECT_EQ(allocationState->createTextureCalls, 2);
EXPECT_EQ(allocationState->createRenderTargetViewCalls, 2);
EXPECT_EQ(allocationState->createShaderViewCalls, 2);
EXPECT_EQ(allocationState->shutdownTextureCalls, 2);
EXPECT_EQ(allocationState->shutdownRenderTargetViewCalls, 2);
EXPECT_EQ(allocationState->shutdownShaderViewCalls, 2);
EXPECT_EQ(allocationState->destroyTextureCalls, 2);
EXPECT_EQ(allocationState->destroyRenderTargetViewCalls, 2);
EXPECT_EQ(allocationState->destroyShaderViewCalls, 2);
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, RecordsGraphCapableFullscreenSequencePassesInStageOrder) {
Scene scene("CameraRendererGraphFullscreenSequences");
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<TrackingPass>(pipelineState, "postA", true, true, true);
auto postSecondPass = std::make_unique<TrackingPass>(pipelineState, "postB", true, true, true);
TrackingPass* postSecondPassRaw = postSecondPass.get();
RenderPassSequence postProcessPasses;
postProcessPasses.AddPass(std::move(postFirstPass));
postProcessPasses.AddPass(std::move(postSecondPass));
auto finalFirstPass = std::make_unique<TrackingPass>(pipelineState, "finalA", true, true, true);
TrackingPass* finalFirstPassRaw = finalFirstPass.get();
auto finalSecondPass = std::make_unique<TrackingPass>(pipelineState, "finalB", true, true, true);
TrackingPass* 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(CameraFramePlan::FromRequest(request)));
ASSERT_NE(postSecondPassRaw, nullptr);
EXPECT_TRUE(postSecondPassRaw->lastHasSourceSurface);
EXPECT_EQ(postSecondPassRaw->lastSourceSurfaceWidth, 512u);
EXPECT_EQ(postSecondPassRaw->lastSourceSurfaceHeight, 256u);
EXPECT_EQ(
postSecondPassRaw->lastSourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
ASSERT_NE(finalFirstPassRaw, nullptr);
EXPECT_TRUE(finalFirstPassRaw->lastHasSourceSurface);
EXPECT_EQ(finalFirstPassRaw->lastSourceSurfaceWidth, 512u);
EXPECT_EQ(finalFirstPassRaw->lastSourceSurfaceHeight, 256u);
EXPECT_EQ(finalFirstPassRaw->lastSourceColorView, finalOutputSource);
EXPECT_EQ(
finalFirstPassRaw->lastSourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
ASSERT_NE(finalSecondPassRaw, nullptr);
EXPECT_TRUE(finalSecondPassRaw->lastHasSourceSurface);
EXPECT_EQ(finalSecondPassRaw->lastSourceSurfaceWidth, 800u);
EXPECT_EQ(finalSecondPassRaw->lastSourceSurfaceHeight, 600u);
EXPECT_NE(finalSecondPassRaw->lastSourceColorView, nullptr);
EXPECT_NE(finalSecondPassRaw->lastSourceColorView, finalOutputSource);
EXPECT_EQ(
finalSecondPassRaw->lastSourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
EXPECT_EQ(
pipelineState->eventLog,
(std::vector<std::string>{
"record:postA",
"record:postB",
"record:finalA",
"record:finalB",
"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>();
auto pipeline = std::make_unique<MockPipeline>(state);
InstallStandaloneStagePass<MockObjectIdPass>(
*pipeline,
CameraFrameStage::ObjectId,
state);
InstallStandaloneStagePass<MockScenePass>(
*pipeline,
CameraFrameStage::ShadowCaster,
state,
"shadowCaster");
InstallStandaloneStagePass<MockScenePass>(
*pipeline,
CameraFrameStage::DepthOnly,
state,
"depthOnly");
CameraRenderer renderer(std::move(pipeline));
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(CameraFramePlan::FromRequest(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, RecordsGraphCapableSinglePassSequenceStagesInDocumentedOrder) {
Scene scene("CameraRendererGraphSinglePassSequenceStages");
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 prePass = std::make_unique<TrackingPass>(state, "pre", true, true, true);
TrackingPass* prePassRaw = prePass.get();
RenderPassSequence prePasses;
prePasses.AddPass(std::move(prePass));
auto postProcessPass = std::make_unique<TrackingPass>(state, "postProcess", true, true, true);
TrackingPass* postProcessPassRaw = postProcessPass.get();
RenderPassSequence postProcessPasses;
postProcessPasses.AddPass(std::move(postProcessPass));
auto finalOutputPass = std::make_unique<TrackingPass>(state, "finalOutput", true, true, true);
TrackingPass* finalOutputPassRaw = finalOutputPass.get();
RenderPassSequence finalOutputPasses;
finalOutputPasses.AddPass(std::move(finalOutputPass));
auto postPass = std::make_unique<TrackingPass>(state, "post", true, true, true);
TrackingPass* postPassRaw = postPass.get();
RenderPassSequence postPasses;
postPasses.AddPass(std::move(postPass));
auto overlayPass = std::make_unique<TrackingPass>(state, "overlay", true, true, true);
TrackingPass* overlayPassRaw = overlayPass.get();
RenderPassSequence overlayPasses;
overlayPasses.AddPass(std::move(overlayPass));
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.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;
ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request)));
EXPECT_EQ(
state->eventLog,
(std::vector<std::string>{
"record:pre",
"record:postProcess",
"record:finalOutput",
"record:post",
"record:overlay",
"init:pre",
"pre",
"pipeline",
"init:postProcess",
"postProcess",
"init:finalOutput",
"finalOutput",
"init:post",
"post",
"init:overlay",
"overlay" }));
ASSERT_NE(prePassRaw, nullptr);
EXPECT_TRUE(prePassRaw->lastHasSourceSurface == false);
EXPECT_FALSE(prePassRaw->lastSurfaceAutoTransitionEnabled);
ASSERT_NE(postProcessPassRaw, nullptr);
EXPECT_TRUE(postProcessPassRaw->lastHasSourceSurface);
EXPECT_EQ(postProcessPassRaw->lastSourceSurfaceWidth, 256u);
EXPECT_EQ(postProcessPassRaw->lastSourceSurfaceHeight, 128u);
EXPECT_FALSE(postProcessPassRaw->lastSourceSurfaceAutoTransitionEnabled);
EXPECT_EQ(postProcessPassRaw->lastSourceColorView, reinterpret_cast<XCEngine::RHI::RHIResourceView*>(40));
EXPECT_EQ(
postProcessPassRaw->lastSourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
EXPECT_FALSE(postProcessPassRaw->lastSurfaceAutoTransitionEnabled);
ASSERT_NE(finalOutputPassRaw, nullptr);
EXPECT_TRUE(finalOutputPassRaw->lastHasSourceSurface);
EXPECT_EQ(finalOutputPassRaw->lastSourceSurfaceWidth, 320u);
EXPECT_EQ(finalOutputPassRaw->lastSourceSurfaceHeight, 180u);
EXPECT_FALSE(finalOutputPassRaw->lastSourceSurfaceAutoTransitionEnabled);
EXPECT_EQ(finalOutputPassRaw->lastSourceColorView, reinterpret_cast<XCEngine::RHI::RHIResourceView*>(50));
EXPECT_EQ(
finalOutputPassRaw->lastSourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
EXPECT_FALSE(finalOutputPassRaw->lastSurfaceAutoTransitionEnabled);
ASSERT_NE(postPassRaw, nullptr);
EXPECT_FALSE(postPassRaw->lastSurfaceAutoTransitionEnabled);
ASSERT_NE(overlayPassRaw, nullptr);
EXPECT_FALSE(overlayPassRaw->lastSurfaceAutoTransitionEnabled);
}
TEST(CameraRenderer_Test, ResolvesGraphManagedFullscreenSequenceSourcesFromFrameResources) {
Scene scene("CameraRendererGraphManagedFullscreenSources");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(3.0f);
auto state = std::make_shared<MockPipelineState>();
state->supportsMainSceneRenderGraph = true;
CameraRenderer renderer(std::make_unique<MockPipeline>(state));
auto allocationState = std::make_shared<MockShadowAllocationState>();
MockShadowDevice device(allocationState);
MockShadowView colorView(
allocationState,
XCEngine::RHI::ResourceViewType::RenderTarget,
XCEngine::RHI::Format::R8G8B8A8_UNorm,
XCEngine::RHI::ResourceViewDimension::Texture2D);
MockShadowView depthView(
allocationState,
XCEngine::RHI::ResourceViewType::DepthStencil,
XCEngine::RHI::Format::D24_UNorm_S8_UInt,
XCEngine::RHI::ResourceViewDimension::Texture2D);
auto postProcessPass = std::make_unique<TrackingPass>(state, "postProcess", true, true, true);
TrackingPass* postProcessPassRaw = postProcessPass.get();
RenderPassSequence postProcessPasses;
postProcessPasses.AddPass(std::move(postProcessPass));
auto finalOutputPass = std::make_unique<TrackingPass>(state, "finalOutput", true, true, true);
TrackingPass* finalOutputPassRaw = finalOutputPass.get();
RenderPassSequence finalOutputPasses;
finalOutputPasses.AddPass(std::move(finalOutputPass));
CameraRenderRequest request;
request.scene = &scene;
request.camera = camera;
request.context = CreateValidContext();
request.context.device = &device;
request.surface = RenderSurface(320, 180);
request.surface.SetColorAttachment(&colorView);
request.surface.SetDepthAttachment(&depthView);
request.cameraDepth = camera->GetDepth();
request.postProcess.passes = &postProcessPasses;
request.finalOutput.passes = &finalOutputPasses;
request.finalOutput.destinationSurface = request.surface;
CameraFramePlan plan = CameraFramePlan::FromRequest(request);
plan.colorChain.usesGraphManagedSceneColor = true;
plan.ConfigureGraphManagedSceneSurface();
plan.colorChain.postProcess.usesGraphManagedOutputColor = true;
plan.colorChain.postProcess.source = CameraFrameColorSource::MainSceneColor;
plan.colorChain.finalOutput.source = CameraFrameColorSource::PostProcessColor;
ASSERT_TRUE(plan.IsPostProcessStageValid());
ASSERT_TRUE(plan.IsFinalOutputStageValid());
const bool renderResult = renderer.Render(plan);
EXPECT_TRUE(renderResult);
EXPECT_EQ(state->recordMainSceneCalls, 1);
EXPECT_EQ(state->executeRecordedMainSceneCalls, 1);
EXPECT_EQ(
state->eventLog,
(std::vector<std::string>{
"record:postProcess",
"record:finalOutput",
"pipelineGraph",
"init:postProcess",
"postProcess",
"init:finalOutput",
"finalOutput" }));
ASSERT_NE(postProcessPassRaw, nullptr);
EXPECT_TRUE(postProcessPassRaw->lastRecordedRenderGraphBlackboard);
EXPECT_TRUE(postProcessPassRaw->lastRecordedSourceColorTextureValid);
EXPECT_TRUE(postProcessPassRaw->lastRecordedBlackboardMainSceneColorValid);
EXPECT_NE(postProcessPassRaw->lastSourceColorView, nullptr);
EXPECT_TRUE(postProcessPassRaw->lastHasSourceSurface);
EXPECT_EQ(postProcessPassRaw->lastSourceSurfaceWidth, 320u);
EXPECT_EQ(postProcessPassRaw->lastSourceSurfaceHeight, 180u);
EXPECT_FALSE(postProcessPassRaw->lastSourceSurfaceAutoTransitionEnabled);
EXPECT_EQ(
postProcessPassRaw->lastSourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
ASSERT_NE(finalOutputPassRaw, nullptr);
EXPECT_TRUE(finalOutputPassRaw->lastRecordedRenderGraphBlackboard);
EXPECT_TRUE(finalOutputPassRaw->lastRecordedSourceColorTextureValid);
EXPECT_TRUE(finalOutputPassRaw->lastRecordedBlackboardPostProcessColorValid);
EXPECT_NE(finalOutputPassRaw->lastSourceColorView, nullptr);
EXPECT_TRUE(finalOutputPassRaw->lastHasSourceSurface);
EXPECT_EQ(finalOutputPassRaw->lastSourceSurfaceWidth, 320u);
EXPECT_EQ(finalOutputPassRaw->lastSourceSurfaceHeight, 180u);
EXPECT_FALSE(finalOutputPassRaw->lastSourceSurfaceAutoTransitionEnabled);
EXPECT_EQ(
finalOutputPassRaw->lastSourceColorState,
XCEngine::RHI::ResourceStates::PixelShaderResource);
}
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>();
auto pipeline = std::make_unique<MockPipeline>(state);
InstallStandaloneStagePass<MockObjectIdPass>(
*pipeline,
CameraFrameStage::ObjectId,
state);
MockScenePass* shadowPassRaw =
InstallStandaloneStagePass<MockScenePass>(
*pipeline,
CameraFrameStage::ShadowCaster,
state,
"shadowCaster");
MockScenePass* depthPassRaw =
InstallStandaloneStagePass<MockScenePass>(
*pipeline,
CameraFrameStage::DepthOnly,
state,
"depthOnly");
CameraRenderer renderer(std::move(pipeline));
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(CameraFramePlan::FromRequest(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_FALSE(shadowPassRaw->lastSurfaceAutoTransitionEnabled);
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_FALSE(depthPassRaw->lastSurfaceAutoTransitionEnabled);
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, RecordsGraphCapableStandalonePassStagesBeforeExecution) {
Scene scene("CameraRendererGraphStandaloneStages");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(3.0f);
auto state = std::make_shared<MockPipelineState>();
auto pipeline = std::make_unique<MockPipeline>(state);
InstallStandaloneStagePass<MockObjectIdPass>(
*pipeline,
CameraFrameStage::ObjectId,
state,
true,
true);
MockScenePass* shadowPassRaw =
InstallStandaloneStagePass<MockScenePass>(
*pipeline,
CameraFrameStage::ShadowCaster,
state,
"shadowCaster",
true,
true,
true);
MockScenePass* depthPassRaw =
InstallStandaloneStagePass<MockScenePass>(
*pipeline,
CameraFrameStage::DepthOnly,
state,
"depthOnly",
true,
true,
true);
CameraRenderer renderer(std::move(pipeline));
CameraRenderRequest request;
request.scene = &scene;
request.camera = camera;
request.context = CreateValidContext();
request.surface = RenderSurface(320, 180);
request.surface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(1));
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);
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(CameraFramePlan::FromRequest(request)));
EXPECT_EQ(
state->eventLog,
(std::vector<std::string>{
"record:shadowCaster",
"record:depthOnly",
"record:objectId",
"init:shadowCaster",
"shadowCaster",
"init:depthOnly",
"depthOnly",
"pipeline",
"objectId" }));
ASSERT_NE(shadowPassRaw, nullptr);
EXPECT_EQ(shadowPassRaw->lastViewportWidth, 128u);
EXPECT_EQ(shadowPassRaw->lastViewportHeight, 64u);
EXPECT_FALSE(shadowPassRaw->lastSurfaceAutoTransitionEnabled);
EXPECT_EQ(shadowPassRaw->lastWorldPosition, XCEngine::Math::Vector3(7.0f, 8.0f, 9.0f));
ASSERT_NE(depthPassRaw, nullptr);
EXPECT_EQ(depthPassRaw->lastViewportWidth, 96u);
EXPECT_EQ(depthPassRaw->lastViewportHeight, 48u);
EXPECT_FALSE(depthPassRaw->lastSurfaceAutoTransitionEnabled);
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, UsesGraphManagedSurfaceForMainSceneWhenOutputAttachmentsExist) {
Scene scene("CameraRendererGraphManagedMainScene");
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));
CameraRenderRequest request;
request.scene = &scene;
request.camera = camera;
request.context = CreateValidContext();
request.surface = RenderSurface(320, 180);
request.surface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(1));
request.surface.SetDepthAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(2));
request.cameraDepth = camera->GetDepth();
ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request)));
EXPECT_EQ(state->renderCalls, 1);
EXPECT_FALSE(state->lastSurfaceAutoTransitionEnabled);
EXPECT_EQ(state->lastSurfaceWidth, 320u);
EXPECT_EQ(state->lastSurfaceHeight, 180u);
}
TEST(CameraRenderer_Test, UsesPipelineRecordedMainSceneWhenSupported) {
Scene scene("CameraRendererPipelineRecordedMainScene");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(3.0f);
auto state = std::make_shared<MockPipelineState>();
state->supportsMainSceneRenderGraph = true;
CameraRenderer renderer(std::make_unique<MockPipeline>(state));
CameraRenderRequest request;
request.scene = &scene;
request.camera = camera;
request.context = CreateValidContext();
request.surface = RenderSurface(320, 180);
request.surface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(1));
request.surface.SetDepthAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(2));
request.cameraDepth = camera->GetDepth();
ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request)));
EXPECT_EQ(state->recordMainSceneCalls, 1);
EXPECT_EQ(state->executeRecordedMainSceneCalls, 1);
EXPECT_EQ(state->renderCalls, 0);
EXPECT_EQ(state->eventLog, (std::vector<std::string>{ "pipelineGraph" }));
EXPECT_TRUE(state->lastReceivedRenderGraphBlackboard);
EXPECT_TRUE(state->lastBlackboardMainSceneColorValid);
EXPECT_TRUE(state->lastBlackboardMainSceneDepthValid);
EXPECT_FALSE(state->lastBlackboardMainDirectionalShadowValid);
EXPECT_FALSE(state->lastBlackboardObjectIdColorValid);
EXPECT_FALSE(state->lastSurfaceAutoTransitionEnabled);
EXPECT_EQ(state->lastSurfaceWidth, 320u);
EXPECT_EQ(state->lastSurfaceHeight, 180u);
}
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;
{
auto pipeline = std::make_unique<MockPipeline>(pipelineState);
InstallStandaloneStagePass<MockObjectIdPass>(
*pipeline,
CameraFrameStage::ObjectId,
pipelineState);
MockScenePass* shadowPassRaw =
InstallStandaloneStagePass<MockScenePass>(
*pipeline,
CameraFrameStage::ShadowCaster,
pipelineState,
"shadowCaster");
CameraRenderer renderer(std::move(pipeline));
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(CameraFramePlan::FromRequest(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_FALSE(shadowPassRaw->lastSurfaceAutoTransitionEnabled);
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.0010f);
EXPECT_FLOAT_EQ(pipelineState->lastShadowMapMetrics.inverseMapSize.x, 1.0f / 256.0f);
EXPECT_FLOAT_EQ(pipelineState->lastShadowMapMetrics.inverseMapSize.y, 1.0f / 128.0f);
EXPECT_FLOAT_EQ(pipelineState->lastShadowSampling.settings.shadowStrength, 0.85f);
EXPECT_TRUE(pipelineState->lastHasMainDirectionalShadowKeyword);
}
EXPECT_EQ(allocationState->shutdownDepthViewCalls, 1);
EXPECT_EQ(allocationState->shutdownShaderViewCalls, 1);
EXPECT_EQ(allocationState->shutdownTextureCalls, 1);
EXPECT_EQ(allocationState->destroyDepthViewCalls, 1);
EXPECT_EQ(allocationState->destroyShaderViewCalls, 1);
EXPECT_EQ(allocationState->destroyTextureCalls, 1);
}
TEST(CameraRenderer_Test, AllowsPipelineToOverrideAutoAllocatedDirectionalShadowExecutionState) {
Scene scene("CameraRendererDirectionalShadowExecutionPolicyScene");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(2.0f);
auto pipelineState = std::make_shared<MockPipelineState>();
pipelineState->configureDirectionalShadowExecutionState =
[](
const CameraFramePlan& plan,
const DirectionalShadowSurfaceAllocation& shadowAllocation,
DirectionalShadowExecutionState& shadowState) {
ApplyDefaultRenderPipelineDirectionalShadowExecutionPolicy(
plan,
shadowAllocation,
shadowState);
shadowState.shadowCasterRequest.clearFlags = RenderClearFlags::All;
shadowState.shadowCasterRequest.hasCameraDataOverride = true;
shadowState.shadowCasterRequest.cameraDataOverride =
plan.directionalShadow.cameraData;
shadowState.shadowCasterRequest.cameraDataOverride.worldPosition =
XCEngine::Math::Vector3(7.0f, 8.0f, 9.0f);
shadowState.shadowData.sampling.settings.shadowStrength = 0.25f;
return true;
};
auto allocationState = std::make_shared<MockShadowAllocationState>();
MockShadowDevice device(allocationState);
RenderContext context = CreateValidContext();
context.device = &device;
{
auto pipeline = std::make_unique<MockPipeline>(pipelineState);
InstallStandaloneStagePass<MockObjectIdPass>(
*pipeline,
CameraFrameStage::ObjectId,
pipelineState);
MockScenePass* shadowPassRaw =
InstallStandaloneStagePass<MockScenePass>(
*pipeline,
CameraFrameStage::ShadowCaster,
pipelineState,
"shadowCaster");
CameraRenderer renderer(std::move(pipeline));
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);
ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(request)));
EXPECT_EQ(
pipelineState->eventLog,
(std::vector<std::string>{
"init:shadowCaster",
"shadowCaster",
"pipeline" }));
EXPECT_EQ(
pipelineState->configureDirectionalShadowExecutionStateCalls,
1);
EXPECT_EQ(shadowPassRaw->lastClearFlags, RenderClearFlags::All);
EXPECT_EQ(
shadowPassRaw->lastWorldPosition,
XCEngine::Math::Vector3(7.0f, 8.0f, 9.0f));
EXPECT_FLOAT_EQ(
pipelineState->lastShadowSampling.settings.shadowStrength,
0.25f);
}
}
TEST(CameraRenderer_Test, PassesShadowDependencyToPipelineRecordedMainScene) {
Scene scene("CameraRendererPipelineRecordedShadowDependency");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(2.0f);
auto pipelineState = std::make_shared<MockPipelineState>();
pipelineState->supportsMainSceneRenderGraph = true;
auto allocationState = std::make_shared<MockShadowAllocationState>();
MockShadowDevice device(allocationState);
RenderContext context = CreateValidContext();
context.device = &device;
auto pipeline = std::make_unique<MockPipeline>(pipelineState);
InstallStandaloneStagePass<MockObjectIdPass>(
*pipeline,
CameraFrameStage::ObjectId,
pipelineState);
InstallStandaloneStagePass<MockScenePass>(
*pipeline,
CameraFrameStage::ShadowCaster,
pipelineState,
"shadowCaster");
CameraRenderer renderer(std::move(pipeline));
CameraRenderRequest request;
request.scene = &scene;
request.camera = camera;
request.context = context;
request.surface = RenderSurface(320, 180);
request.surface.SetColorAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(1));
request.surface.SetDepthAttachment(reinterpret_cast<XCEngine::RHI::RHIResourceView*>(2));
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(CameraFramePlan::FromRequest(request)));
EXPECT_EQ(pipelineState->recordMainSceneCalls, 1);
EXPECT_EQ(pipelineState->executeRecordedMainSceneCalls, 1);
EXPECT_TRUE(pipelineState->lastRecordedMainSceneShadowHandleValid);
EXPECT_TRUE(pipelineState->lastReceivedRenderGraphBlackboard);
EXPECT_TRUE(pipelineState->lastBlackboardMainSceneColorValid);
EXPECT_TRUE(pipelineState->lastBlackboardMainSceneDepthValid);
EXPECT_TRUE(pipelineState->lastBlackboardMainDirectionalShadowValid);
EXPECT_FALSE(pipelineState->lastBlackboardObjectIdColorValid);
EXPECT_EQ(
pipelineState->eventLog,
(std::vector<std::string>{
"init:shadowCaster",
"shadowCaster",
"pipelineGraph" }));
}
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;
auto pipeline = std::make_unique<MockPipeline>(pipelineState);
InstallStandaloneStagePass<MockObjectIdPass>(
*pipeline,
CameraFrameStage::ObjectId,
pipelineState);
InstallStandaloneStagePass<MockScenePass>(
*pipeline,
CameraFrameStage::ShadowCaster,
pipelineState,
"shadowCaster");
CameraRenderer renderer(std::move(pipeline));
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(CameraFramePlan::FromRequest(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(CameraFramePlan::FromRequest(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;
{
auto pipeline = std::make_unique<MockPipeline>(pipelineState);
InstallStandaloneStagePass<MockObjectIdPass>(
*pipeline,
CameraFrameStage::ObjectId,
pipelineState);
InstallStandaloneStagePass<MockScenePass>(
*pipeline,
CameraFrameStage::ShadowCaster,
pipelineState,
"shadowCaster");
CameraRenderer renderer(std::move(pipeline));
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(CameraFramePlan::FromRequest(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(CameraFramePlan::FromRequest(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;
{
auto pipeline = std::make_unique<MockPipeline>(pipelineState);
InstallStandaloneStagePass<MockObjectIdPass>(
*pipeline,
CameraFrameStage::ObjectId,
pipelineState);
InstallStandaloneStagePass<MockScenePass>(
*pipeline,
CameraFrameStage::ShadowCaster,
pipelineState,
"shadowCaster");
CameraRenderer renderer(std::move(pipeline));
CameraRenderRequest request;
request.scene = &scene;
request.camera = camera;
request.context = context;
request.surface = RenderSurface(320, 180);
request.cameraDepth = camera->GetDepth();
ASSERT_TRUE(renderer.Render(CameraFramePlan::FromRequest(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(CameraFramePlan::FromRequest(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(CameraFramePlan::FromRequest(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(CameraFramePlan::FromRequest(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(CameraFramePlan::FromRequest(request)));
EXPECT_EQ(
state->eventLog,
(std::vector<std::string>{ "init:pre", "pre", "pipeline" }));
}
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>();
auto pipeline = std::make_unique<MockPipeline>(state);
InstallStandaloneStagePass<MockObjectIdPass>(
*pipeline,
CameraFrameStage::ObjectId,
state,
false);
CameraRenderer renderer(std::move(pipeline));
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(CameraFramePlan::FromRequest(request)));
EXPECT_EQ(
state->eventLog,
(std::vector<std::string>{ "init:pre", "pre", "pipeline", "objectId" }));
}
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(CameraFramePlan::FromRequest(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);
highCamera->SetClearMode(CameraClearMode::None);
SceneRenderer renderer;
const RenderContext context = CreateValidContext();
const RenderSurface surface(320, 180);
const std::vector<CameraFramePlan> defaultPlans =
renderer.BuildFramePlans(scene, nullptr, context, surface);
ASSERT_EQ(defaultPlans.size(), 2u);
EXPECT_EQ(defaultPlans[0].request.camera, lowCamera);
EXPECT_EQ(defaultPlans[0].request.cameraDepth, 1.0f);
EXPECT_EQ(defaultPlans[0].request.clearFlags, RenderClearFlags::All);
EXPECT_EQ(defaultPlans[0].request.surface.GetWidth(), 320u);
EXPECT_EQ(defaultPlans[0].request.surface.GetHeight(), 180u);
EXPECT_EQ(defaultPlans[1].request.camera, highCamera);
EXPECT_EQ(defaultPlans[1].request.cameraDepth, 5.0f);
EXPECT_EQ(defaultPlans[1].request.clearFlags, RenderClearFlags::None);
const std::vector<CameraFramePlan> overridePlans =
renderer.BuildFramePlans(scene, lowCamera, context, surface);
ASSERT_EQ(overridePlans.size(), 1u);
EXPECT_EQ(overridePlans[0].request.camera, lowCamera);
EXPECT_EQ(overridePlans[0].request.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<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, nullptr, CreateValidContext(), RenderSurface(640, 360));
ASSERT_EQ(plans.size(), 3u);
EXPECT_EQ(plans[0].request.camera, earlyBaseCamera);
EXPECT_EQ(plans[0].request.cameraStackOrder, 0u);
EXPECT_EQ(plans[0].request.clearFlags, RenderClearFlags::All);
EXPECT_EQ(plans[1].request.camera, lateBaseCamera);
EXPECT_EQ(plans[1].request.cameraStackOrder, 0u);
EXPECT_EQ(plans[1].request.clearFlags, RenderClearFlags::Depth);
EXPECT_EQ(plans[2].request.camera, overlayCamera);
EXPECT_EQ(plans[2].request.cameraStackOrder, 1u);
EXPECT_EQ(plans[2].request.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<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, nullptr, CreateValidContext(), RenderSurface(640, 360));
ASSERT_EQ(plans.size(), 2u);
EXPECT_EQ(plans[0].request.camera, firstCamera);
EXPECT_EQ(plans[1].request.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<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, nullptr, CreateValidContext(), RenderSurface(320, 180));
ASSERT_EQ(plans.size(), 2u);
EXPECT_EQ(plans[0].request.camera, firstOverlay);
EXPECT_EQ(plans[0].request.clearFlags, RenderClearFlags::All);
EXPECT_EQ(plans[1].request.camera, secondOverlay);
EXPECT_EQ(plans[1].request.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<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, camera, CreateValidContext(), RenderSurface(640, 360));
ASSERT_EQ(plans.size(), 1u);
EXPECT_EQ(plans[0].request.camera, camera);
EXPECT_EQ(plans[0].request.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<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, nullptr, CreateValidContext(), RenderSurface(800, 600));
ASSERT_EQ(plans.size(), 1u);
const XCEngine::Math::RectInt renderArea = plans[0].request.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<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, nullptr, CreateValidContext(), surface);
ASSERT_EQ(plans.size(), 1u);
const XCEngine::Math::RectInt renderArea = plans[0].request.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<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, nullptr, CreateValidContext(), surface);
ASSERT_EQ(plans.size(), 1u);
const XCEngine::Math::RectInt renderArea = plans[0].request.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<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, nullptr, context, surface);
ASSERT_EQ(plans.size(), 1u);
const CameraFramePlan& plan = plans[0];
EXPECT_TRUE(plan.postProcess.IsRequested());
EXPECT_TRUE(plan.IsPostProcessStageValid());
EXPECT_NE(plan.postProcess.passes, nullptr);
ASSERT_EQ(plan.postProcess.passes->GetPassCount(), 2u);
EXPECT_TRUE(plan.UsesGraphManagedSceneColor());
EXPECT_FALSE(plan.UsesGraphManagedOutputColor(CameraFrameStage::PostProcess));
EXPECT_EQ(
plan.ResolveStageColorSource(CameraFrameStage::PostProcess),
CameraFrameColorSource::MainSceneColor);
EXPECT_EQ(plan.postProcess.destinationSurface.GetColorAttachments()[0], backBufferColorView);
EXPECT_EQ(plan.postProcess.destinationSurface.GetDepthAttachment(), depthView);
EXPECT_EQ(
plan.postProcess.destinationSurface.GetDepthStateBefore(),
XCEngine::RHI::ResourceStates::Common);
EXPECT_EQ(
plan.postProcess.destinationSurface.GetDepthStateAfter(),
XCEngine::RHI::ResourceStates::PixelShaderResource);
EXPECT_EQ(plan.GetMainSceneSurface().GetDepthAttachment(), depthView);
EXPECT_EQ(
plan.GetMainSceneSurface().GetDepthStateBefore(),
XCEngine::RHI::ResourceStates::Common);
EXPECT_EQ(
plan.GetMainSceneSurface().GetDepthStateAfter(),
XCEngine::RHI::ResourceStates::PixelShaderResource);
EXPECT_EQ(plan.GetMainSceneSurface().GetWidth(), 800u);
EXPECT_EQ(plan.GetMainSceneSurface().GetHeight(), 600u);
const XCEngine::Math::RectInt sourceRenderArea = plan.GetMainSceneSurface().GetRenderArea();
EXPECT_EQ(sourceRenderArea.x, 200);
EXPECT_EQ(sourceRenderArea.y, 75);
EXPECT_EQ(sourceRenderArea.width, 400);
EXPECT_EQ(sourceRenderArea.height, 375);
const CameraFrameStageSourceBinding postProcessSource =
ResolveStageSourceBinding(CameraFrameStage::PostProcess, plan);
EXPECT_EQ(postProcessSource.sourceSurface, nullptr);
EXPECT_EQ(postProcessSource.sourceColorView, nullptr);
EXPECT_EQ(
postProcessSource.sourceColorState,
XCEngine::RHI::ResourceStates::Common);
EXPECT_EQ(allocationState->createTextureCalls, 0);
EXPECT_EQ(allocationState->createRenderTargetViewCalls, 0);
EXPECT_EQ(allocationState->createShaderViewCalls, 0);
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<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, nullptr, CreateValidContext(), RenderSurface(640, 360));
ASSERT_EQ(plans.size(), 1u);
const CameraFramePlan& plan = plans[0];
EXPECT_TRUE(plan.finalColorPolicy.hasPipelineDefaults);
EXPECT_TRUE(plan.finalColorPolicy.hasCameraOverrides);
EXPECT_EQ(
plan.finalColorPolicy.outputTransferMode,
FinalColorOutputTransferMode::LinearToSRGB);
EXPECT_EQ(
plan.finalColorPolicy.exposureMode,
FinalColorExposureMode::Fixed);
EXPECT_FLOAT_EQ(plan.finalColorPolicy.exposureValue, 1.8f);
EXPECT_FLOAT_EQ(plan.finalColorPolicy.finalColorScale.x, 0.95f);
EXPECT_FALSE(plan.finalOutput.IsRequested());
}
TEST(SceneRenderer_Test, UsesPipelineAssetCameraFramePlanConfigurationHook) {
Scene scene("SceneRendererPlanConfigurationHookScene");
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 assetState = std::make_shared<MockPipelineAssetState>();
assetState->configureCameraFramePlan = [](
CameraFramePlan& plan) {
plan.ClearOwnedPostProcessSequence();
plan.ClearOwnedFinalOutputSequence();
plan.finalColorPolicy = {};
plan.postProcess = {};
plan.finalOutput = {};
plan.colorChain = {};
};
SceneRenderer renderer(std::make_shared<MockPipelineAsset>(assetState));
const std::vector<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, nullptr, CreateValidContext(), RenderSurface(640, 360));
ASSERT_EQ(plans.size(), 1u);
EXPECT_EQ(assetState->configureCameraFramePlanCalls, 1);
const CameraFramePlan& plan = plans[0];
EXPECT_FALSE(plan.finalColorPolicy.hasPipelineDefaults);
EXPECT_FALSE(plan.finalColorPolicy.hasCameraOverrides);
EXPECT_FALSE(plan.postProcess.IsRequested());
EXPECT_FALSE(plan.finalOutput.IsRequested());
}
TEST(SceneRenderer_Test, UsesPipelineAssetCameraRenderRequestConfigurationHook) {
Scene scene("SceneRendererRequestConfigurationHookScene");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(2.0f);
GameObject* shadowLightObject = scene.CreateGameObject("ShadowLight");
auto* shadowLight = shadowLightObject->AddComponent<LightComponent>();
shadowLight->SetLightType(LightType::Directional);
shadowLight->SetCastsShadows(true);
auto assetState = std::make_shared<MockPipelineAssetState>();
assetState->configureCameraRenderRequest =
[](
CameraRenderRequest& request,
size_t renderedBaseCameraCount,
size_t renderedRequestCount,
const DirectionalShadowPlanningSettings& directionalShadowSettings) {
ApplyDefaultRenderPipelineAssetCameraRenderRequestPolicy(
request,
renderedBaseCameraCount,
renderedRequestCount,
directionalShadowSettings);
request.directionalShadow = {};
};
SceneRenderer renderer(std::make_shared<MockPipelineAsset>(assetState));
const std::vector<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, nullptr, CreateValidContext(), RenderSurface(640, 360));
ASSERT_EQ(plans.size(), 1u);
EXPECT_EQ(assetState->configureCameraRenderRequestCalls, 1);
EXPECT_FALSE(plans[0].directionalShadow.IsValid());
}
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<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, nullptr, context, surface);
ASSERT_EQ(plans.size(), 1u);
const CameraFramePlan& plan = plans[0];
EXPECT_FALSE(plan.postProcess.IsRequested());
EXPECT_TRUE(plan.finalOutput.IsRequested());
EXPECT_TRUE(plan.IsFinalOutputStageValid());
ASSERT_NE(plan.finalOutput.passes, nullptr);
EXPECT_EQ(plan.finalOutput.passes->GetPassCount(), 1u);
EXPECT_TRUE(plan.UsesGraphManagedSceneColor());
EXPECT_EQ(
plan.ResolveStageColorSource(CameraFrameStage::FinalOutput),
CameraFrameColorSource::MainSceneColor);
EXPECT_EQ(plan.finalOutput.destinationSurface.GetColorAttachments()[0], backBufferColorView);
EXPECT_EQ(plan.finalOutput.destinationSurface.GetDepthAttachment(), depthView);
EXPECT_EQ(
plan.finalOutput.destinationSurface.GetDepthStateBefore(),
XCEngine::RHI::ResourceStates::Common);
EXPECT_EQ(
plan.finalOutput.destinationSurface.GetDepthStateAfter(),
XCEngine::RHI::ResourceStates::PixelShaderResource);
EXPECT_EQ(plan.GetMainSceneSurface().GetDepthAttachment(), depthView);
EXPECT_EQ(
plan.GetMainSceneSurface().GetDepthStateBefore(),
XCEngine::RHI::ResourceStates::Common);
EXPECT_EQ(
plan.GetMainSceneSurface().GetDepthStateAfter(),
XCEngine::RHI::ResourceStates::PixelShaderResource);
EXPECT_EQ(plan.GetMainSceneSurface().GetWidth(), 800u);
EXPECT_EQ(plan.GetMainSceneSurface().GetHeight(), 600u);
const CameraFrameStageSourceBinding finalOutputSource =
ResolveStageSourceBinding(CameraFrameStage::FinalOutput, plan);
EXPECT_EQ(finalOutputSource.sourceSurface, nullptr);
EXPECT_EQ(finalOutputSource.sourceColorView, nullptr);
EXPECT_EQ(
finalOutputSource.sourceColorState,
XCEngine::RHI::ResourceStates::Common);
EXPECT_EQ(allocationState->createTextureCalls, 0);
EXPECT_EQ(allocationState->createRenderTargetViewCalls, 0);
EXPECT_EQ(allocationState->createShaderViewCalls, 0);
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<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, nullptr, context, surface);
ASSERT_EQ(plans.size(), 1u);
const CameraFramePlan& plan = plans[0];
EXPECT_TRUE(plan.postProcess.IsRequested());
EXPECT_TRUE(plan.finalOutput.IsRequested());
EXPECT_TRUE(plan.IsPostProcessStageValid());
EXPECT_TRUE(plan.IsFinalOutputStageValid());
ASSERT_NE(plan.postProcess.passes, nullptr);
ASSERT_NE(plan.finalOutput.passes, nullptr);
EXPECT_EQ(plan.postProcess.passes->GetPassCount(), 1u);
EXPECT_EQ(plan.finalOutput.passes->GetPassCount(), 1u);
EXPECT_TRUE(plan.UsesGraphManagedSceneColor());
EXPECT_TRUE(plan.UsesGraphManagedOutputColor(CameraFrameStage::PostProcess));
EXPECT_EQ(
plan.ResolveStageColorSource(CameraFrameStage::PostProcess),
CameraFrameColorSource::MainSceneColor);
EXPECT_EQ(
plan.ResolveStageColorSource(CameraFrameStage::FinalOutput),
CameraFrameColorSource::PostProcessColor);
const CameraFrameStageSourceBinding postProcessSource =
ResolveStageSourceBinding(CameraFrameStage::PostProcess, plan);
const RenderSurface* const postProcessOutput =
ResolveStageOutputSurface(CameraFrameStage::PostProcess, plan);
const CameraFrameStageSourceBinding finalOutputSource =
ResolveStageSourceBinding(CameraFrameStage::FinalOutput, plan);
EXPECT_EQ(postProcessSource.sourceSurface, nullptr);
EXPECT_EQ(postProcessOutput, nullptr);
EXPECT_EQ(finalOutputSource.sourceSurface, nullptr);
EXPECT_EQ(plan.finalOutput.destinationSurface.GetColorAttachments()[0], backBufferColorView);
EXPECT_EQ(plan.finalOutput.destinationSurface.GetDepthAttachment(), depthView);
EXPECT_EQ(
plan.finalOutput.destinationSurface.GetDepthStateBefore(),
XCEngine::RHI::ResourceStates::Common);
EXPECT_EQ(
plan.finalOutput.destinationSurface.GetDepthStateAfter(),
XCEngine::RHI::ResourceStates::PixelShaderResource);
const XCEngine::Math::RectInt postProcessSourceArea = plan.GetMainSceneSurface().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 = plan.GetMainSceneSurface().GetRenderArea();
EXPECT_EQ(finalOutputSourceArea.x, 200);
EXPECT_EQ(finalOutputSourceArea.y, 75);
EXPECT_EQ(finalOutputSourceArea.width, 400);
EXPECT_EQ(finalOutputSourceArea.height, 375);
EXPECT_EQ(postProcessSource.sourceColorView, nullptr);
EXPECT_EQ(finalOutputSource.sourceColorView, nullptr);
EXPECT_EQ(
postProcessSource.sourceColorState,
XCEngine::RHI::ResourceStates::Common);
EXPECT_EQ(
finalOutputSource.sourceColorState,
XCEngine::RHI::ResourceStates::Common);
EXPECT_EQ(allocationState->createTextureCalls, 0);
EXPECT_EQ(allocationState->createRenderTargetViewCalls, 0);
EXPECT_EQ(allocationState->createShaderViewCalls, 0);
delete depthView;
delete backBufferColorView;
}
TEST(SceneRenderer_Test, ReturnedPlansOwnGeneratedFullscreenSequences) {
Scene scene("SceneRendererOwnsGeneratedFullscreenSequencesScene");
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);
CameraFramePlan preservedPlan = {};
{
SceneRenderer renderer;
const std::vector<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, nullptr, context, surface);
ASSERT_EQ(plans.size(), 1u);
ASSERT_TRUE(static_cast<bool>(plans[0].GetOwnedPostProcessSequence()));
ASSERT_TRUE(static_cast<bool>(plans[0].GetOwnedFinalOutputSequence()));
preservedPlan = plans[0];
}
ASSERT_TRUE(static_cast<bool>(preservedPlan.GetOwnedPostProcessSequence()));
ASSERT_TRUE(static_cast<bool>(preservedPlan.GetOwnedFinalOutputSequence()));
ASSERT_NE(preservedPlan.postProcess.passes, nullptr);
ASSERT_NE(preservedPlan.finalOutput.passes, nullptr);
EXPECT_EQ(
preservedPlan.postProcess.passes,
preservedPlan.GetOwnedPostProcessSequence().get());
EXPECT_EQ(
preservedPlan.finalOutput.passes,
preservedPlan.GetOwnedFinalOutputSequence().get());
EXPECT_EQ(preservedPlan.postProcess.passes->GetPassCount(), 1u);
EXPECT_EQ(preservedPlan.finalOutput.passes->GetPassCount(), 1u);
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<CameraFramePlan> plans =
renderer.BuildFramePlans(scene, nullptr, context, surface);
ASSERT_EQ(plans.size(), 1u);
const CameraFramePlan& plan = plans[0];
EXPECT_EQ(plan.request.surface.GetSampleCount(), 4u);
EXPECT_FALSE(plan.postProcess.IsRequested());
EXPECT_FALSE(plan.finalOutput.IsRequested());
EXPECT_EQ(allocationState->createTextureCalls, 0);
EXPECT_EQ(allocationState->createRenderTargetViewCalls, 0);
EXPECT_EQ(allocationState->createShaderViewCalls, 0);
delete depthView;
delete backBufferColorView;
}
TEST(SceneRenderer_Test, KeepsMainSceneGraphManagedWhenPostProcessIsEnabledAcrossFrames) {
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>();
pipelineState->supportsMainSceneRenderGraph = true;
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<CameraFramePlan> firstFramePlans =
renderer.BuildFramePlans(scene, nullptr, context, surface);
ASSERT_EQ(firstFramePlans.size(), 1u);
CameraFramePlan firstFramePlan = firstFramePlans[0];
EXPECT_TRUE(firstFramePlan.UsesGraphManagedSceneColor());
EXPECT_EQ(
firstFramePlan.ResolveStageColorSource(CameraFrameStage::PostProcess),
CameraFrameColorSource::MainSceneColor);
EXPECT_EQ(
firstFramePlan.GetMainSceneSurface().GetColorStateBefore(),
XCEngine::RHI::ResourceStates::Common);
RenderPassSequence postProcessPasses;
postProcessPasses.AddPass(std::make_unique<MockScenePass>(pipelineState, "postProcess"));
firstFramePlans[0].postProcess.passes = &postProcessPasses;
ASSERT_TRUE(renderer.Render(firstFramePlans));
const int createTextureCallsAfterFirstRender = allocationState->createTextureCalls;
const int createRenderTargetViewCallsAfterFirstRender =
allocationState->createRenderTargetViewCalls;
const int createShaderViewCallsAfterFirstRender = allocationState->createShaderViewCalls;
const std::vector<CameraFramePlan> secondFramePlans =
renderer.BuildFramePlans(scene, nullptr, context, surface);
ASSERT_EQ(secondFramePlans.size(), 1u);
const CameraFramePlan secondFramePlan = secondFramePlans[0];
EXPECT_TRUE(secondFramePlan.UsesGraphManagedSceneColor());
EXPECT_EQ(
secondFramePlan.ResolveStageColorSource(CameraFrameStage::PostProcess),
CameraFrameColorSource::MainSceneColor);
EXPECT_EQ(
secondFramePlan.GetMainSceneSurface().GetColorStateBefore(),
XCEngine::RHI::ResourceStates::Common);
EXPECT_EQ(allocationState->createTextureCalls, createTextureCallsAfterFirstRender);
EXPECT_EQ(
allocationState->createRenderTargetViewCalls,
createRenderTargetViewCallsAfterFirstRender);
EXPECT_EQ(allocationState->createShaderViewCalls, createShaderViewCallsAfterFirstRender);
delete depthView;
delete backBufferColorView;
}
TEST(SceneRenderer_Test, KeepsPostProcessOutputGraphManagedWhenFinalOutputIsEnabledAcrossFrames) {
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>();
pipelineState->supportsMainSceneRenderGraph = true;
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<CameraFramePlan> firstFramePlans =
renderer.BuildFramePlans(scene, nullptr, context, surface);
ASSERT_EQ(firstFramePlans.size(), 1u);
CameraFramePlan firstFramePlan = firstFramePlans[0];
EXPECT_TRUE(firstFramePlans[0].postProcess.IsRequested());
EXPECT_TRUE(firstFramePlans[0].finalOutput.IsRequested());
EXPECT_TRUE(firstFramePlan.UsesGraphManagedSceneColor());
EXPECT_TRUE(firstFramePlan.UsesGraphManagedOutputColor(CameraFrameStage::PostProcess));
EXPECT_EQ(
firstFramePlan.ResolveStageColorSource(CameraFrameStage::PostProcess),
CameraFrameColorSource::MainSceneColor);
EXPECT_EQ(
firstFramePlan.ResolveStageColorSource(CameraFrameStage::FinalOutput),
CameraFrameColorSource::PostProcessColor);
EXPECT_EQ(
firstFramePlan.GetMainSceneSurface().GetColorStateBefore(),
XCEngine::RHI::ResourceStates::Common);
EXPECT_EQ(
ResolveStageOutputSurface(CameraFrameStage::PostProcess, firstFramePlan),
nullptr);
RenderPassSequence postProcessPasses;
postProcessPasses.AddPass(std::make_unique<MockScenePass>(pipelineState, "postProcess"));
RenderPassSequence finalOutputPasses;
finalOutputPasses.AddPass(std::make_unique<MockScenePass>(pipelineState, "finalOutput"));
firstFramePlans[0].postProcess.passes = &postProcessPasses;
firstFramePlans[0].finalOutput.passes = &finalOutputPasses;
ASSERT_TRUE(renderer.Render(firstFramePlans));
const int createTextureCallsAfterFirstRender = allocationState->createTextureCalls;
const int createRenderTargetViewCallsAfterFirstRender =
allocationState->createRenderTargetViewCalls;
const int createShaderViewCallsAfterFirstRender = allocationState->createShaderViewCalls;
const std::vector<CameraFramePlan> secondFramePlans =
renderer.BuildFramePlans(scene, nullptr, context, surface);
ASSERT_EQ(secondFramePlans.size(), 1u);
const CameraFramePlan secondFramePlan = secondFramePlans[0];
EXPECT_TRUE(secondFramePlan.UsesGraphManagedSceneColor());
EXPECT_TRUE(secondFramePlan.UsesGraphManagedOutputColor(CameraFrameStage::PostProcess));
EXPECT_EQ(
secondFramePlan.ResolveStageColorSource(CameraFrameStage::PostProcess),
CameraFrameColorSource::MainSceneColor);
EXPECT_EQ(
secondFramePlan.ResolveStageColorSource(CameraFrameStage::FinalOutput),
CameraFrameColorSource::PostProcessColor);
EXPECT_EQ(
secondFramePlan.GetMainSceneSurface().GetColorStateBefore(),
XCEngine::RHI::ResourceStates::Common);
EXPECT_EQ(
ResolveStageOutputSurface(CameraFrameStage::PostProcess, secondFramePlan),
nullptr);
EXPECT_EQ(
ResolveStageSourceBinding(CameraFrameStage::FinalOutput, secondFramePlan).sourceSurface,
nullptr);
EXPECT_EQ(allocationState->createTextureCalls, createTextureCallsAfterFirstRender);
EXPECT_EQ(
allocationState->createRenderTargetViewCalls,
createRenderTargetViewCallsAfterFirstRender);
EXPECT_EQ(allocationState->createShaderViewCalls, createShaderViewCallsAfterFirstRender);
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(CameraFramePlan::FromRequest(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(RenderPipelineHost_Test, BuildsFramePlansFromRequestsUsingPipelineAssetDefaults) {
Scene scene("RenderPipelineHostBuildPlans");
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;
camera->SetFinalColorOverrides(cameraOverrides);
auto assetState = std::make_shared<MockPipelineAssetState>();
assetState->defaultFinalColorSettings.outputTransferMode =
FinalColorOutputTransferMode::LinearToSRGB;
assetState->defaultFinalColorSettings.exposureMode =
FinalColorExposureMode::Fixed;
assetState->defaultFinalColorSettings.exposureValue = 1.25f;
CameraRenderRequest request = {};
request.scene = &scene;
request.camera = camera;
request.context = CreateValidContext();
request.surface = RenderSurface(640, 360);
request.cameraDepth = camera->GetDepth();
request.clearFlags = RenderClearFlags::All;
RenderPipelineHost host(std::make_shared<MockPipelineAsset>(assetState));
const std::vector<CameraFramePlan> plans =
host.BuildFramePlans({ request });
ASSERT_EQ(plans.size(), 1u);
const CameraFramePlan& plan = plans[0];
EXPECT_TRUE(plan.finalColorPolicy.hasPipelineDefaults);
EXPECT_TRUE(plan.finalColorPolicy.hasCameraOverrides);
EXPECT_EQ(
plan.finalColorPolicy.outputTransferMode,
FinalColorOutputTransferMode::LinearToSRGB);
EXPECT_EQ(
plan.finalColorPolicy.exposureMode,
FinalColorExposureMode::Fixed);
EXPECT_FLOAT_EQ(plan.finalColorPolicy.exposureValue, 1.8f);
}
TEST(RenderPipelineHost_Test, ForwardsPipelineLifetimeAndRenderCallsToCameraRenderer) {
Scene scene("RenderPipelineHostScene");
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();
RenderPipelineHost host(std::move(initialPipeline));
EXPECT_EQ(host.GetPipeline(), initialPipelineRaw);
auto replacementPipeline = std::make_unique<MockPipeline>(replacementState);
MockPipeline* replacementPipelineRaw = replacementPipeline.get();
host.SetPipeline(std::move(replacementPipeline));
EXPECT_EQ(initialState->shutdownCalls, 1);
EXPECT_EQ(host.GetPipeline(), replacementPipelineRaw);
CameraRenderRequest request = {};
request.scene = &scene;
request.camera = camera;
request.context = CreateValidContext();
request.surface = RenderSurface(800, 600);
request.cameraDepth = camera->GetDepth();
request.clearFlags = RenderClearFlags::All;
const std::vector<CameraFramePlan> plans =
host.BuildFramePlans({ request });
ASSERT_EQ(plans.size(), 1u);
ASSERT_TRUE(host.Render(plans));
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(RenderPipelineHost_Test, SortsManualFramePlansByDepthBeforeRendering) {
Scene scene("RenderPipelineHostManualRequests");
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>();
RenderPipelineHost host(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<CameraFramePlan> plans =
host.BuildFramePlans({ farRequest, nearRequest });
ASSERT_TRUE(host.Render(plans));
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(ScriptableRenderPipelineHost_Test, ForwardsRendererLifetimeAndFrameRendering) {
Scene scene("ScriptableRenderPipelineHostScene");
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 initialRenderer = std::make_unique<MockPipeline>(initialState);
MockPipeline* initialRendererRaw = initialRenderer.get();
Pipelines::ScriptableRenderPipelineHost host(std::move(initialRenderer));
EXPECT_EQ(host.GetPipelineRenderer(), initialRendererRaw);
auto replacementRenderer = std::make_unique<MockPipeline>(replacementState);
MockPipeline* replacementRendererRaw = replacementRenderer.get();
host.SetPipelineRenderer(std::move(replacementRenderer));
EXPECT_EQ(initialState->shutdownCalls, 1);
EXPECT_EQ(host.GetPipelineRenderer(), replacementRendererRaw);
CameraRenderRequest request = {};
request.scene = &scene;
request.camera = camera;
request.context = CreateValidContext();
request.surface = RenderSurface(800, 600);
request.cameraDepth = camera->GetDepth();
request.clearFlags = RenderClearFlags::All;
RenderSceneData sceneData =
CreateSceneDataForCamera(scene, *camera, request.surface);
ASSERT_TRUE(host.Render(
request.context,
request.surface,
sceneData));
EXPECT_EQ(replacementState->initializeCalls, 1);
EXPECT_EQ(replacementState->renderCalls, 1);
EXPECT_EQ(replacementState->lastSurfaceWidth, 800u);
EXPECT_EQ(replacementState->lastSurfaceHeight, 600u);
EXPECT_EQ(replacementState->lastCamera, camera);
ASSERT_TRUE(host.Render(FrameExecutionContext(
request.context,
request.surface,
sceneData)));
EXPECT_EQ(replacementState->initializeCalls, 1);
EXPECT_EQ(replacementState->renderCalls, 2);
replacementState->supportsMainSceneRenderGraph = true;
RenderGraph graph = {};
RenderGraphBuilder graphBuilder(graph);
RenderGraphBlackboard blackboard = {};
bool executionSucceeded = true;
RenderSceneData graphSceneData =
CreateSceneDataForCamera(scene, *camera, request.surface);
const RenderPipelineStageRenderGraphContext graphContext = {
graphBuilder,
"MainScene",
CameraFrameStage::MainScene,
request.context,
graphSceneData,
request.surface,
nullptr,
nullptr,
XCEngine::RHI::ResourceStates::Common,
{},
{},
{},
&executionSucceeded,
&blackboard
};
EXPECT_TRUE(host.SupportsStageRenderGraph(CameraFrameStage::MainScene));
EXPECT_TRUE(host.RecordStageRenderGraph(graphContext));
EXPECT_EQ(replacementState->initializeCalls, 1);
EXPECT_EQ(replacementState->recordMainSceneCalls, 1);
}
EXPECT_EQ(initialState->shutdownCalls, 1);
EXPECT_EQ(replacementState->shutdownCalls, 1);
}
TEST(ScriptableRenderPipelineHost_Test, PrefersStageRecorderBeforeFallbackRenderer) {
auto rendererState = std::make_shared<MockPipelineState>();
auto initialRecorderState = std::make_shared<MockStageRecorderState>();
auto replacementRecorderState = std::make_shared<MockStageRecorderState>();
rendererState->supportsMainSceneRenderGraph = true;
initialRecorderState->supportsMainSceneRenderGraph = true;
replacementRecorderState->supportsMainSceneRenderGraph = true;
{
Pipelines::ScriptableRenderPipelineHost host(
std::make_unique<MockPipeline>(rendererState));
auto initialRecorder =
std::make_unique<MockStageRecorder>(initialRecorderState);
MockStageRecorder* initialRecorderRaw = initialRecorder.get();
host.SetStageRecorder(std::move(initialRecorder));
EXPECT_EQ(host.GetStageRecorder(), initialRecorderRaw);
auto replacementRecorder =
std::make_unique<MockStageRecorder>(replacementRecorderState);
MockStageRecorder* replacementRecorderRaw = replacementRecorder.get();
host.SetStageRecorder(std::move(replacementRecorder));
EXPECT_EQ(initialRecorderState->shutdownCalls, 1);
EXPECT_EQ(host.GetStageRecorder(), replacementRecorderRaw);
Scene scene("ScriptableRenderPipelineRecorderScene");
GameObject* cameraObject = scene.CreateGameObject("Camera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(1.0f);
const RenderContext context = CreateValidContext();
const RenderSurface surface(800, 600);
RenderSceneData sceneData =
CreateSceneDataForCamera(scene, *camera, surface);
RenderGraph graph = {};
RenderGraphBuilder graphBuilder(graph);
RenderGraphBlackboard blackboard = {};
bool executionSucceeded = true;
const RenderPipelineStageRenderGraphContext graphContext = {
graphBuilder,
"MainScene",
CameraFrameStage::MainScene,
context,
sceneData,
surface,
nullptr,
nullptr,
XCEngine::RHI::ResourceStates::Common,
{},
{},
{},
&executionSucceeded,
&blackboard
};
EXPECT_TRUE(host.SupportsStageRenderGraph(CameraFrameStage::MainScene));
EXPECT_TRUE(host.RecordStageRenderGraph(graphContext));
EXPECT_EQ(rendererState->initializeCalls, 1);
EXPECT_EQ(replacementRecorderState->initializeCalls, 1);
EXPECT_EQ(replacementRecorderState->recordMainSceneCalls, 1);
EXPECT_TRUE(host.RecordStageRenderGraph(graphContext));
EXPECT_EQ(rendererState->initializeCalls, 1);
EXPECT_EQ(replacementRecorderState->initializeCalls, 1);
EXPECT_EQ(replacementRecorderState->recordMainSceneCalls, 2);
EXPECT_TRUE(replacementRecorderState->lastReceivedRenderGraphBlackboard);
EXPECT_EQ(rendererState->recordMainSceneCalls, 0);
}
EXPECT_EQ(rendererState->shutdownCalls, 1);
EXPECT_EQ(replacementRecorderState->shutdownCalls, 1);
}
TEST(ScriptableRenderPipelineHost_Test, DirectHostDoesNotInstallBuiltinStandaloneStagePasses) {
auto rendererState = std::make_shared<MockPipelineState>();
Pipelines::ScriptableRenderPipelineHost host(
std::make_unique<MockPipeline>(rendererState));
EXPECT_EQ(host.GetCameraFrameStandalonePass(CameraFrameStage::ObjectId), nullptr);
EXPECT_EQ(host.GetCameraFrameStandalonePass(CameraFrameStage::DepthOnly), nullptr);
EXPECT_EQ(host.GetCameraFrameStandalonePass(CameraFrameStage::ShadowCaster), nullptr);
}
TEST(ScriptableRenderPipelineHostAsset_Test, CreatesHostFromRendererAssetAndForwardsDefaults) {
auto assetState = std::make_shared<MockPipelineAssetState>();
assetState->defaultFinalColorSettings.outputTransferMode =
FinalColorOutputTransferMode::LinearToSRGB;
assetState->defaultFinalColorSettings.exposureMode =
FinalColorExposureMode::Fixed;
assetState->defaultFinalColorSettings.exposureValue = 1.5f;
Pipelines::ScriptableRenderPipelineHostAsset asset(
std::make_shared<MockPipelineAsset>(assetState));
EXPECT_EQ(
asset.GetDefaultFinalColorSettings().outputTransferMode,
FinalColorOutputTransferMode::LinearToSRGB);
EXPECT_EQ(
asset.GetDefaultFinalColorSettings().exposureMode,
FinalColorExposureMode::Fixed);
EXPECT_FLOAT_EQ(
asset.GetDefaultFinalColorSettings().exposureValue,
1.5f);
std::unique_ptr<RenderPipeline> pipeline = asset.CreatePipeline();
ASSERT_NE(pipeline, nullptr);
auto* host =
dynamic_cast<Pipelines::ScriptableRenderPipelineHost*>(pipeline.get());
ASSERT_NE(host, nullptr);
EXPECT_NE(host->GetPipelineRenderer(), nullptr);
EXPECT_EQ(assetState->createCalls, 1);
EXPECT_NE(host->GetCameraFrameStandalonePass(CameraFrameStage::ObjectId), nullptr);
EXPECT_NE(host->GetCameraFrameStandalonePass(CameraFrameStage::DepthOnly), nullptr);
EXPECT_NE(host->GetCameraFrameStandalonePass(CameraFrameStage::ShadowCaster), nullptr);
}
TEST(ManagedScriptableRenderPipelineAsset_Test, CreatesHostWithStageRecorderFromManagedBridge) {
Pipelines::ClearManagedRenderPipelineBridge();
const Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedRenderPipelineProbeAsset"
};
auto bridgeState = std::make_shared<MockManagedRenderPipelineBridgeState>();
Pipelines::SetManagedRenderPipelineBridge(
std::make_shared<MockManagedRenderPipelineBridge>(bridgeState));
{
Pipelines::ManagedScriptableRenderPipelineAsset asset(descriptor);
std::unique_ptr<RenderPipeline> pipeline = asset.CreatePipeline();
ASSERT_NE(pipeline, nullptr);
auto* host =
dynamic_cast<Pipelines::ScriptableRenderPipelineHost*>(pipeline.get());
ASSERT_NE(host, nullptr);
EXPECT_NE(host->GetPipelineRenderer(), nullptr);
EXPECT_NE(host->GetStageRecorder(), nullptr);
EXPECT_EQ(bridgeState->createStageRecorderCalls, 1);
EXPECT_EQ(bridgeState->lastDescriptor.assemblyName, "GameScripts");
EXPECT_EQ(bridgeState->lastDescriptor.namespaceName, "Gameplay");
EXPECT_EQ(bridgeState->lastDescriptor.className, "ManagedRenderPipelineProbeAsset");
ASSERT_NE(bridgeState->lastCreatedStageRecorderState, nullptr);
bridgeState->lastCreatedStageRecorderState->supportsMainSceneRenderGraph = true;
EXPECT_TRUE(host->SupportsStageRenderGraph(CameraFrameStage::MainScene));
}
Pipelines::ClearManagedRenderPipelineBridge();
}
TEST(CameraRenderer_Test, DefaultPipelineAssetUsesManagedSelectionWhenPresent) {
const Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedRenderPipelineProbeAsset"
};
Pipelines::SetManagedRenderPipelineAssetDescriptor(descriptor);
CameraRenderer renderer;
auto* asset =
dynamic_cast<const Pipelines::ManagedScriptableRenderPipelineAsset*>(
renderer.GetPipelineAsset());
ASSERT_NE(asset, nullptr);
EXPECT_EQ(asset->GetDescriptor().assemblyName, "GameScripts");
EXPECT_EQ(asset->GetDescriptor().namespaceName, "Gameplay");
EXPECT_EQ(asset->GetDescriptor().className, "ManagedRenderPipelineProbeAsset");
EXPECT_NE(
dynamic_cast<Pipelines::ScriptableRenderPipelineHost*>(renderer.GetPipeline()),
nullptr);
Pipelines::ClearManagedRenderPipelineAssetDescriptor();
}
TEST(CameraRenderer_Test, RebindsToResolvedDefaultAssetWhenPreferredAssetCannotCreatePipeline) {
const Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = {
"GameScripts",
"Gameplay",
"ManagedRenderPipelineProbeAsset"
};
Pipelines::SetManagedRenderPipelineAssetDescriptor(descriptor);
auto failingAssetState = std::make_shared<MockPipelineAssetState>();
failingAssetState->createNullPipeline = true;
CameraRenderer renderer(std::make_shared<MockPipelineAsset>(failingAssetState));
EXPECT_EQ(failingAssetState->createCalls, 1);
EXPECT_NE(renderer.GetPipeline(), nullptr);
EXPECT_NE(
dynamic_cast<Pipelines::ScriptableRenderPipelineHost*>(renderer.GetPipeline()),
nullptr);
auto* asset =
dynamic_cast<const Pipelines::ManagedScriptableRenderPipelineAsset*>(
renderer.GetPipelineAsset());
ASSERT_NE(asset, nullptr);
EXPECT_EQ(asset->GetDescriptor().assemblyName, "GameScripts");
EXPECT_EQ(asset->GetDescriptor().namespaceName, "Gameplay");
EXPECT_EQ(asset->GetDescriptor().className, "ManagedRenderPipelineProbeAsset");
Pipelines::ClearManagedRenderPipelineAssetDescriptor();
}
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<CameraFramePlan> plans = {
CameraFramePlan::FromRequest(farRequest),
CameraFramePlan::FromRequest(nearRequest)
};
ASSERT_TRUE(renderer.Render(plans));
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<CameraFramePlan> plans = {
CameraFramePlan::FromRequest(secondRequest),
CameraFramePlan::FromRequest(firstRequest)
};
ASSERT_TRUE(renderer.Render(plans));
ASSERT_EQ(state->renderedCameras.size(), 2u);
EXPECT_EQ(state->renderedCameras[0], secondCamera);
EXPECT_EQ(state->renderedCameras[1], firstCamera);
}