diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index f089c62c..f1ec0aa5 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -334,6 +334,7 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderCameraData.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/CameraRenderRequest.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/VisibleRenderObject.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderPass.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderSceneExtractor.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderPipeline.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderPipelineAsset.h diff --git a/engine/include/XCEngine/Rendering/CameraRenderRequest.h b/engine/include/XCEngine/Rendering/CameraRenderRequest.h index 21f8c330..92243141 100644 --- a/engine/include/XCEngine/Rendering/CameraRenderRequest.h +++ b/engine/include/XCEngine/Rendering/CameraRenderRequest.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -19,6 +20,8 @@ struct CameraRenderRequest { RenderSurface surface; float cameraDepth = 0.0f; RenderClearFlags clearFlags = RenderClearFlags::All; + RenderPassSequence* preScenePasses = nullptr; + RenderPassSequence* postScenePasses = nullptr; bool IsValid() const { return scene != nullptr && diff --git a/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h b/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h index 2994a3dd..edd070d1 100644 --- a/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h +++ b/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -28,12 +29,17 @@ namespace Rendering { class RenderSurface; namespace Pipelines { +namespace Detail { +class BuiltinForwardOpaquePass; +} // namespace Detail class BuiltinForwardPipeline : public RenderPipeline { public: - BuiltinForwardPipeline() = default; + BuiltinForwardPipeline(); ~BuiltinForwardPipeline() override; + static RHI::InputLayoutDesc BuildInputLayout(); + bool Initialize(const RenderContext& context) override; void Shutdown() override; bool Render( @@ -42,6 +48,8 @@ public: const RenderSceneData& sceneData) override; private: + friend class Detail::BuiltinForwardOpaquePass; + struct OwnedDescriptorSet { RHI::RHIDescriptorPool* pool = nullptr; RHI::RHIDescriptorSet* set = nullptr; @@ -65,6 +73,7 @@ private: const Resources::Texture* ResolveTexture(const Resources::Material* material) const; RHI::RHIResourceView* ResolveTextureView(const VisibleRenderItem& visibleItem); + bool ExecuteForwardOpaquePass(const RenderPassContext& context); bool DrawVisibleItem( const RenderContext& context, const RenderSceneData& sceneData, @@ -85,6 +94,7 @@ private: RHI::RHISampler* m_sampler = nullptr; RHI::RHITexture* m_fallbackTexture = nullptr; RHI::RHIResourceView* m_fallbackTextureView = nullptr; + RenderPassSequence m_passSequence; }; } // namespace Pipelines diff --git a/engine/include/XCEngine/Rendering/RenderPass.h b/engine/include/XCEngine/Rendering/RenderPass.h new file mode 100644 index 00000000..945d7e85 --- /dev/null +++ b/engine/include/XCEngine/Rendering/RenderPass.h @@ -0,0 +1,90 @@ +#pragma once + +#include + +#include +#include +#include + +namespace XCEngine { +namespace Rendering { + +struct RenderSceneData; +class RenderSurface; + +struct RenderPassContext { + const RenderContext& renderContext; + const RenderSurface& surface; + const RenderSceneData& sceneData; +}; + +class RenderPass { +public: + virtual ~RenderPass() = default; + + virtual const char* GetName() const = 0; + + virtual bool Initialize(const RenderContext&) { + return true; + } + + virtual void Shutdown() { + } + + virtual bool Execute(const RenderPassContext& context) = 0; +}; + +class RenderPassSequence { +public: + void AddPass(std::unique_ptr pass) { + if (pass != nullptr) { + m_passes.push_back(std::move(pass)); + } + } + + size_t GetPassCount() const { + return m_passes.size(); + } + + bool Initialize(const RenderContext& context) { + for (const std::unique_ptr& pass : m_passes) { + if (pass == nullptr) { + continue; + } + + if (!pass->Initialize(context)) { + return false; + } + } + + return true; + } + + void Shutdown() { + for (auto passIt = m_passes.rbegin(); passIt != m_passes.rend(); ++passIt) { + if (*passIt != nullptr) { + (*passIt)->Shutdown(); + } + } + } + + bool Execute(const RenderPassContext& context) { + for (const std::unique_ptr& pass : m_passes) { + if (pass == nullptr) { + continue; + } + + if (!pass->Execute(context)) { + return false; + } + } + + return true; + } + +private: + std::vector> m_passes; +}; + +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/src/Rendering/CameraRenderer.cpp b/engine/src/Rendering/CameraRenderer.cpp index 56c24f54..a22ebfc0 100644 --- a/engine/src/Rendering/CameraRenderer.cpp +++ b/engine/src/Rendering/CameraRenderer.cpp @@ -51,7 +51,31 @@ bool CameraRenderer::Render( } sceneData.cameraData.clearFlags = request.clearFlags; - return m_pipeline->Render(request.context, request.surface, sceneData); + const RenderPassContext passContext = { + request.context, + request.surface, + sceneData + }; + + if (request.preScenePasses != nullptr) { + if (!request.preScenePasses->Initialize(request.context) || + !request.preScenePasses->Execute(passContext)) { + return false; + } + } + + if (!m_pipeline->Render(request.context, request.surface, sceneData)) { + return false; + } + + if (request.postScenePasses != nullptr) { + if (!request.postScenePasses->Initialize(request.context) || + !request.postScenePasses->Execute(passContext)) { + return false; + } + } + + return true; } } // namespace Rendering diff --git a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp index 1986cd33..72de6dfd 100644 --- a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp +++ b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp @@ -16,6 +16,35 @@ namespace XCEngine { namespace Rendering { namespace Pipelines { +namespace Detail { + +class BuiltinForwardOpaquePass final : public RenderPass { +public: + explicit BuiltinForwardOpaquePass(BuiltinForwardPipeline& pipeline) + : m_pipeline(pipeline) { + } + + const char* GetName() const override { + return "BuiltinForwardOpaquePass"; + } + + bool Initialize(const RenderContext& context) override { + return m_pipeline.EnsureInitialized(context); + } + + void Shutdown() override { + m_pipeline.DestroyPipelineResources(); + } + + bool Execute(const RenderPassContext& context) override { + return m_pipeline.ExecuteForwardOpaquePass(context); + } + +private: + BuiltinForwardPipeline& m_pipeline; +}; + +} // namespace Detail namespace { @@ -101,21 +130,7 @@ RHI::GraphicsPipelineDesc CreatePipelineDesc( pipelineDesc.sampleCount = 1; ApplyMaterialRenderState(material, pipelineDesc); - RHI::InputElementDesc position = {}; - position.semanticName = "POSITION"; - position.semanticIndex = 0; - position.format = static_cast(RHI::Format::R32G32B32A32_Float); - position.inputSlot = 0; - position.alignedByteOffset = 0; - pipelineDesc.inputLayout.elements.push_back(position); - - RHI::InputElementDesc texcoord = {}; - texcoord.semanticName = "TEXCOORD"; - texcoord.semanticIndex = 0; - texcoord.format = static_cast(RHI::Format::R32G32_Float); - texcoord.inputSlot = 0; - texcoord.alignedByteOffset = static_cast(offsetof(Resources::StaticMeshVertex, uv0)); - pipelineDesc.inputLayout.elements.push_back(texcoord); + pipelineDesc.inputLayout = BuiltinForwardPipeline::BuildInputLayout(); if (backendType == RHI::RHIType::D3D12) { pipelineDesc.vertexShader.source.assign( @@ -170,26 +185,66 @@ const Resources::Texture* FindMaterialTexture(const Resources::Material& materia } // namespace +BuiltinForwardPipeline::BuiltinForwardPipeline() { + m_passSequence.AddPass(std::make_unique(*this)); +} + BuiltinForwardPipeline::~BuiltinForwardPipeline() { Shutdown(); } +RHI::InputLayoutDesc BuiltinForwardPipeline::BuildInputLayout() { + RHI::InputLayoutDesc inputLayout = {}; + + RHI::InputElementDesc position = {}; + position.semanticName = "POSITION"; + position.semanticIndex = 0; + position.format = static_cast(RHI::Format::R32G32B32_Float); + position.inputSlot = 0; + position.alignedByteOffset = 0; + inputLayout.elements.push_back(position); + + RHI::InputElementDesc texcoord = {}; + texcoord.semanticName = "TEXCOORD"; + texcoord.semanticIndex = 0; + texcoord.format = static_cast(RHI::Format::R32G32_Float); + texcoord.inputSlot = 0; + texcoord.alignedByteOffset = static_cast(offsetof(Resources::StaticMeshVertex, uv0)); + inputLayout.elements.push_back(texcoord); + + return inputLayout; +} + bool BuiltinForwardPipeline::Initialize(const RenderContext& context) { - return EnsureInitialized(context); + return m_passSequence.Initialize(context); } void BuiltinForwardPipeline::Shutdown() { - DestroyPipelineResources(); + m_passSequence.Shutdown(); } bool BuiltinForwardPipeline::Render( const RenderContext& context, const RenderSurface& surface, const RenderSceneData& sceneData) { - if (!EnsureInitialized(context)) { + if (!Initialize(context)) { return false; } + const RenderPassContext passContext = { + context, + surface, + sceneData + }; + + return m_passSequence.Execute(passContext); +} + +bool BuiltinForwardPipeline::ExecuteForwardOpaquePass(const RenderPassContext& passContext) { + const RenderContext& context = passContext.renderContext; + const RenderSurface& surface = passContext.surface; + const RenderSceneData& sceneData = passContext.sceneData; + const std::vector& colorAttachments = surface.GetColorAttachments(); if (colorAttachments.empty()) { return false; diff --git a/tests/Rendering/unit/CMakeLists.txt b/tests/Rendering/unit/CMakeLists.txt index 57a53d09..47dd12f8 100644 --- a/tests/Rendering/unit/CMakeLists.txt +++ b/tests/Rendering/unit/CMakeLists.txt @@ -3,6 +3,8 @@ cmake_minimum_required(VERSION 3.15) project(XCEngine_RenderingUnitTests) set(RENDERING_UNIT_TEST_SOURCES + test_render_pass.cpp + test_builtin_forward_pipeline.cpp test_camera_scene_renderer.cpp test_render_scene_extractor.cpp ) diff --git a/tests/Rendering/unit/test_builtin_forward_pipeline.cpp b/tests/Rendering/unit/test_builtin_forward_pipeline.cpp new file mode 100644 index 00000000..a9be96e2 --- /dev/null +++ b/tests/Rendering/unit/test_builtin_forward_pipeline.cpp @@ -0,0 +1,29 @@ +#include + +#include +#include +#include + +using namespace XCEngine::Rendering::Pipelines; +using namespace XCEngine::Resources; +using namespace XCEngine::RHI; + +TEST(BuiltinForwardPipeline_Test, UsesFloat3PositionInputLayoutForStaticMeshVertices) { + const InputLayoutDesc inputLayout = BuiltinForwardPipeline::BuildInputLayout(); + + ASSERT_EQ(inputLayout.elements.size(), 2u); + + const InputElementDesc& position = inputLayout.elements[0]; + EXPECT_EQ(position.semanticName, "POSITION"); + EXPECT_EQ(position.semanticIndex, 0u); + EXPECT_EQ(position.format, static_cast(Format::R32G32B32_Float)); + EXPECT_EQ(position.inputSlot, 0u); + EXPECT_EQ(position.alignedByteOffset, 0u); + + const InputElementDesc& texcoord = inputLayout.elements[1]; + EXPECT_EQ(texcoord.semanticName, "TEXCOORD"); + EXPECT_EQ(texcoord.semanticIndex, 0u); + EXPECT_EQ(texcoord.format, static_cast(Format::R32G32_Float)); + EXPECT_EQ(texcoord.inputSlot, 0u); + EXPECT_EQ(texcoord.alignedByteOffset, static_cast(offsetof(StaticMeshVertex, uv0))); +} diff --git a/tests/Rendering/unit/test_camera_scene_renderer.cpp b/tests/Rendering/unit/test_camera_scene_renderer.cpp index c32e6f0d..24176f9f 100644 --- a/tests/Rendering/unit/test_camera_scene_renderer.cpp +++ b/tests/Rendering/unit/test_camera_scene_renderer.cpp @@ -7,6 +7,7 @@ #include #include +#include #include using namespace XCEngine::Components; @@ -25,6 +26,7 @@ struct MockPipelineState { RenderClearFlags lastClearFlags = RenderClearFlags::All; std::vector renderedCameras; std::vector renderedClearFlags; + std::vector eventLog; }; class MockPipeline final : public RenderPipeline { @@ -46,6 +48,7 @@ public: const RenderContext&, const RenderSurface& surface, const RenderSceneData& sceneData) override { + m_state->eventLog.push_back("pipeline"); ++m_state->renderCalls; m_state->lastSurfaceWidth = surface.GetWidth(); m_state->lastSurfaceHeight = surface.GetHeight(); @@ -61,6 +64,32 @@ private: std::shared_ptr m_state; }; +class TrackingPass final : public RenderPass { +public: + TrackingPass(std::shared_ptr state, const char* label) + : m_state(std::move(state)) + , m_label(label) { + } + + const char* GetName() const override { + return m_label; + } + + bool Initialize(const RenderContext&) override { + m_state->eventLog.push_back(std::string("init:") + m_label); + return true; + } + + bool Execute(const RenderPassContext&) override { + m_state->eventLog.push_back(m_label); + return true; + } + +private: + std::shared_ptr m_state; + const char* m_label = ""; +}; + RenderContext CreateValidContext() { RenderContext context; context.device = reinterpret_cast(1); @@ -105,6 +134,38 @@ TEST(CameraRenderer_Test, UsesOverrideCameraAndSurfaceSizeWhenSubmittingScene) { EXPECT_EQ(state->lastClearFlags, RenderClearFlags::None); } +TEST(CameraRenderer_Test, ExecutesInjectedPreAndPostPassSequencesAroundPipelineRender) { + Scene scene("CameraRendererPassScene"); + + GameObject* cameraObject = scene.CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + camera->SetPrimary(true); + camera->SetDepth(3.0f); + + auto state = std::make_shared(); + CameraRenderer renderer(std::make_unique(state)); + + RenderPassSequence prePasses; + prePasses.AddPass(std::make_unique(state, "pre")); + + RenderPassSequence postPasses; + postPasses.AddPass(std::make_unique(state, "post")); + + CameraRenderRequest request; + request.scene = &scene; + request.camera = camera; + request.context = CreateValidContext(); + request.surface = RenderSurface(320, 180); + request.cameraDepth = camera->GetDepth(); + request.preScenePasses = &prePasses; + request.postScenePasses = &postPasses; + + ASSERT_TRUE(renderer.Render(request)); + EXPECT_EQ( + state->eventLog, + (std::vector{ "init:pre", "pre", "pipeline", "init:post", "post" })); +} + TEST(SceneRenderer_Test, BuildsSingleExplicitRequestFromSelectedCamera) { Scene scene("SceneRendererRequestScene"); diff --git a/tests/Rendering/unit/test_render_pass.cpp b/tests/Rendering/unit/test_render_pass.cpp new file mode 100644 index 00000000..50ba5d8b --- /dev/null +++ b/tests/Rendering/unit/test_render_pass.cpp @@ -0,0 +1,125 @@ +#include + +#include +#include +#include + +#include +#include +#include + +using namespace XCEngine::Rendering; + +namespace { + +struct TrackingPassState { + std::vector initializeOrder; + std::vector executeOrder; + std::vector shutdownOrder; +}; + +class TrackingPass final : public RenderPass { +public: + TrackingPass( + std::string name, + TrackingPassState& state, + bool initializeResult = true, + bool executeResult = true) + : m_name(std::move(name)) + , m_state(state) + , m_initializeResult(initializeResult) + , m_executeResult(executeResult) { + } + + const char* GetName() const override { + return m_name.c_str(); + } + + bool Initialize(const RenderContext&) override { + m_state.initializeOrder.push_back(m_name); + return m_initializeResult; + } + + void Shutdown() override { + m_state.shutdownOrder.push_back(m_name); + } + + bool Execute(const RenderPassContext&) override { + m_state.executeOrder.push_back(m_name); + return m_executeResult; + } + +private: + std::string m_name; + TrackingPassState& m_state; + bool m_initializeResult = true; + bool m_executeResult = true; +}; + +RenderContext CreateValidContext() { + RenderContext context; + context.device = reinterpret_cast(1); + context.commandList = reinterpret_cast(1); + context.commandQueue = reinterpret_cast(1); + return context; +} + +} // namespace + +TEST(RenderPassSequence_Test, InitializesAndExecutesInInsertionOrderThenShutsDownInReverse) { + TrackingPassState state; + RenderPassSequence sequence; + sequence.AddPass(std::make_unique("SceneColor", state)); + sequence.AddPass(std::make_unique("GridOverlay", state)); + sequence.AddPass(std::make_unique("Outline", state)); + + ASSERT_EQ(sequence.GetPassCount(), 3u); + + const RenderContext context = CreateValidContext(); + ASSERT_TRUE(sequence.Initialize(context)); + + RenderSceneData sceneData; + const RenderSurface surface(1280, 720); + const RenderPassContext passContext = { + context, + surface, + sceneData + }; + + ASSERT_TRUE(sequence.Execute(passContext)); + sequence.Shutdown(); + + EXPECT_EQ( + state.initializeOrder, + (std::vector{ "SceneColor", "GridOverlay", "Outline" })); + EXPECT_EQ( + state.executeOrder, + (std::vector{ "SceneColor", "GridOverlay", "Outline" })); + EXPECT_EQ( + state.shutdownOrder, + (std::vector{ "Outline", "GridOverlay", "SceneColor" })); +} + +TEST(RenderPassSequence_Test, StopsExecutingWhenAPassFails) { + TrackingPassState state; + RenderPassSequence sequence; + sequence.AddPass(std::make_unique("SceneColor", state)); + sequence.AddPass(std::make_unique("SelectionMask", state, true, false)); + sequence.AddPass(std::make_unique("Outline", state)); + + const RenderContext context = CreateValidContext(); + ASSERT_TRUE(sequence.Initialize(context)); + + RenderSceneData sceneData; + const RenderSurface surface(640, 360); + const RenderPassContext passContext = { + context, + surface, + sceneData + }; + + EXPECT_FALSE(sequence.Execute(passContext)); + EXPECT_EQ( + state.executeOrder, + (std::vector{ "SceneColor", "SelectionMask" })); +}