Add rendering pass sequence scaffolding

This commit is contained in:
2026-03-30 02:22:17 +08:00
parent b489492af0
commit 2a31628db1
10 changed files with 420 additions and 20 deletions

View File

@@ -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

View File

@@ -1,5 +1,6 @@
#pragma once
#include <XCEngine/Rendering/RenderPass.h>
#include <XCEngine/Rendering/RenderCameraData.h>
#include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderSurface.h>
@@ -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 &&

View File

@@ -1,6 +1,7 @@
#pragma once
#include <XCEngine/Rendering/RenderMaterialUtility.h>
#include <XCEngine/Rendering/RenderPass.h>
#include <XCEngine/Rendering/RenderPipeline.h>
#include <XCEngine/Rendering/RenderResourceCache.h>
@@ -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

View File

@@ -0,0 +1,90 @@
#pragma once
#include <XCEngine/Rendering/RenderContext.h>
#include <cstddef>
#include <memory>
#include <vector>
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<RenderPass> 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<RenderPass>& 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<RenderPass>& pass : m_passes) {
if (pass == nullptr) {
continue;
}
if (!pass->Execute(context)) {
return false;
}
}
return true;
}
private:
std::vector<std::unique_ptr<RenderPass>> m_passes;
};
} // namespace Rendering
} // namespace XCEngine

View File

@@ -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

View File

@@ -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<uint32_t>(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<uint32_t>(RHI::Format::R32G32_Float);
texcoord.inputSlot = 0;
texcoord.alignedByteOffset = static_cast<uint32_t>(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<Detail::BuiltinForwardOpaquePass>(*this));
}
BuiltinForwardPipeline::~BuiltinForwardPipeline() {
Shutdown();
}
RHI::InputLayoutDesc BuiltinForwardPipeline::BuildInputLayout() {
RHI::InputLayoutDesc inputLayout = {};
RHI::InputElementDesc position = {};
position.semanticName = "POSITION";
position.semanticIndex = 0;
position.format = static_cast<uint32_t>(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<uint32_t>(RHI::Format::R32G32_Float);
texcoord.inputSlot = 0;
texcoord.alignedByteOffset = static_cast<uint32_t>(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<RHI::RHIResourceView*>& colorAttachments = surface.GetColorAttachments();
if (colorAttachments.empty()) {
return false;

View File

@@ -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
)

View File

@@ -0,0 +1,29 @@
#include <gtest/gtest.h>
#include <XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h>
#include <XCEngine/Resources/Mesh/Mesh.h>
#include <XCEngine/RHI/RHIEnums.h>
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<uint32_t>(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<uint32_t>(Format::R32G32_Float));
EXPECT_EQ(texcoord.inputSlot, 0u);
EXPECT_EQ(texcoord.alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, uv0)));
}

View File

@@ -7,6 +7,7 @@
#include <XCEngine/Scene/Scene.h>
#include <memory>
#include <string>
#include <vector>
using namespace XCEngine::Components;
@@ -25,6 +26,7 @@ struct MockPipelineState {
RenderClearFlags lastClearFlags = RenderClearFlags::All;
std::vector<CameraComponent*> renderedCameras;
std::vector<RenderClearFlags> renderedClearFlags;
std::vector<std::string> 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<MockPipelineState> m_state;
};
class TrackingPass final : public RenderPass {
public:
TrackingPass(std::shared_ptr<MockPipelineState> 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<MockPipelineState> m_state;
const char* m_label = "";
};
RenderContext CreateValidContext() {
RenderContext context;
context.device = reinterpret_cast<XCEngine::RHI::RHIDevice*>(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<CameraComponent>();
camera->SetPrimary(true);
camera->SetDepth(3.0f);
auto state = std::make_shared<MockPipelineState>();
CameraRenderer renderer(std::make_unique<MockPipeline>(state));
RenderPassSequence prePasses;
prePasses.AddPass(std::make_unique<TrackingPass>(state, "pre"));
RenderPassSequence postPasses;
postPasses.AddPass(std::make_unique<TrackingPass>(state, "post"));
CameraRenderRequest request;
request.scene = &scene;
request.camera = camera;
request.context = CreateValidContext();
request.surface = RenderSurface(320, 180);
request.cameraDepth = camera->GetDepth();
request.preScenePasses = &prePasses;
request.postScenePasses = &postPasses;
ASSERT_TRUE(renderer.Render(request));
EXPECT_EQ(
state->eventLog,
(std::vector<std::string>{ "init:pre", "pre", "pipeline", "init:post", "post" }));
}
TEST(SceneRenderer_Test, BuildsSingleExplicitRequestFromSelectedCamera) {
Scene scene("SceneRendererRequestScene");

View File

@@ -0,0 +1,125 @@
#include <gtest/gtest.h>
#include <XCEngine/Rendering/RenderPass.h>
#include <XCEngine/Rendering/RenderSceneExtractor.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <memory>
#include <string>
#include <vector>
using namespace XCEngine::Rendering;
namespace {
struct TrackingPassState {
std::vector<std::string> initializeOrder;
std::vector<std::string> executeOrder;
std::vector<std::string> 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<XCEngine::RHI::RHIDevice*>(1);
context.commandList = reinterpret_cast<XCEngine::RHI::RHICommandList*>(1);
context.commandQueue = reinterpret_cast<XCEngine::RHI::RHICommandQueue*>(1);
return context;
}
} // namespace
TEST(RenderPassSequence_Test, InitializesAndExecutesInInsertionOrderThenShutsDownInReverse) {
TrackingPassState state;
RenderPassSequence sequence;
sequence.AddPass(std::make_unique<TrackingPass>("SceneColor", state));
sequence.AddPass(std::make_unique<TrackingPass>("GridOverlay", state));
sequence.AddPass(std::make_unique<TrackingPass>("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<std::string>{ "SceneColor", "GridOverlay", "Outline" }));
EXPECT_EQ(
state.executeOrder,
(std::vector<std::string>{ "SceneColor", "GridOverlay", "Outline" }));
EXPECT_EQ(
state.shutdownOrder,
(std::vector<std::string>{ "Outline", "GridOverlay", "SceneColor" }));
}
TEST(RenderPassSequence_Test, StopsExecutingWhenAPassFails) {
TrackingPassState state;
RenderPassSequence sequence;
sequence.AddPass(std::make_unique<TrackingPass>("SceneColor", state));
sequence.AddPass(std::make_unique<TrackingPass>("SelectionMask", state, true, false));
sequence.AddPass(std::make_unique<TrackingPass>("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<std::string>{ "SceneColor", "SelectionMask" }));
}