test: close renderer phase boundary

This commit is contained in:
2026-04-02 15:03:31 +08:00
parent ec7a15d85b
commit 617b11f801
4 changed files with 320 additions and 42 deletions

View File

@@ -0,0 +1,164 @@
# Renderer阶段收口说明
## 1. 目标
本文用于正式收口当前 Renderer 阶段,明确:
- 本阶段已经完成什么
- 哪些能力已经进入稳定边界
- 哪些事项明确延期到下一阶段
- 后续开发不应再继续把新功能塞回本阶段
当前收口日期:`2026-04-02`
---
## 2. 本阶段已完成能力
### 2.1 Renderer 主体边界
当前已经形成稳定分层:
- `RHI` 负责后端抽象与资源/命令执行
- `Rendering` 负责场景提取、camera request、pipeline、builtin pass
- `Editor` 负责 viewport 宿主、输入、overlay、编辑态请求装配
关键点:
- `CameraRenderer` 已经承担统一 camera 渲染执行职责
- `SceneRenderer` 已经承担 scene -> camera request 的组织职责
- editor scene viewport 不再自己拼装 renderer 执行逻辑
### 2.2 内建后处理边界
本阶段内建编辑态后处理已经收敛为 renderer 自己的通用请求能力:
- `BuiltinPostProcessRequest`
- `BuiltinPostProcessPassPlan`
- `BuiltinPostProcessPassSequenceBuilder`
这意味着:
- renderer 公共接口不再暴露 `SceneView` 专有命名
- grid / selection outline / debug mask 已归入 renderer 侧 builtin post-process 能力
- editor 只负责“是否启用、传什么数据、把哪些 render target 绑定进 request”
### 2.3 Editor Scene Viewport 接入
当前 editor scene viewport 已具备:
- renderer 离屏输出接入
- object-id 帧输出接入
- CPU picking 回退链路
- selection outline
- infinite grid
- built-in post-process 请求装配
其中:
- grid 和 outline 仍然服务于 editor scene viewport
- 但执行入口已经下沉到 renderer
- editor 只保留宿主与编辑器语义
### 2.4 自动化测试体系
当前已经具备稳定回归闸门:
- `tests/Rendering/unit`
- `tests/Rendering/integration`
- `tests/Editor`
- `rendering_phase_regression`
当前阶段收口依赖的关键验证包括:
- renderer unit tests
- editor tests
- 全 rendering integration 场景
- `XCEditor` smoke launch
---
## 3. 本阶段稳定边界
以下内容从现在开始视为本阶段稳定边界:
1. renderer 公共请求以 `CameraRenderRequest` 为核心,而不是 editor 自定义执行入口。
2. editor scene viewport 的内建后处理数据由 editor 组装,但 pass 执行由 renderer 负责。
3. builtin post-process 的公共语义是 renderer 语义,不是 `SceneView` 语义。
4. rendering regression 失败时,优先视为阶段回归,而不是“可接受的小问题”。
---
## 4. 本阶段明确延期项
以下事项明确不再继续塞入本阶段,转入下一阶段:
### 4.1 真正的多 pass / render graph 框架
当前已有 pass sequence 与 builtin post-process但这还不是完整的 renderer 多 pass 架构。
延期内容:
- renderer 级 render graph
- 更正式的 pass phase / event 模型
- 更通用的资源读写依赖管理
### 4.2 GPU Object ID 正式方案
当前 editor selection 相关链路已经能工作,但还不是最终方案。
延期内容:
- renderer 内正式 object-id pass/attachment 规范化
- editor picking 从 CPU fallback 继续向 GPU object-id 正式方案收敛
- editor/game shared picking contract
### 4.3 Gizmo 最终渲染体系
当前 gizmo 与 scene viewport 已经能工作,但不属于本阶段 renderer 收口范围。
延期内容:
- 更成熟的 gizmo 渲染架构
- 更统一的 gizmo draw pass / picking / overlay 体系
- 与后续 renderer 多 pass 的正式对接
### 4.4 C# SRP 对接
当前 renderer 的职责边界已经为 SRP 预留好了方向,但本阶段不做真正脚本化 pipeline 落地。
延期内容:
- `RenderPipelineAsset` / `RenderPipeline` 脚本绑定
- `ScriptableRenderContext`
- `CommandBuffer`
- renderer 与脚本侧的正式桥接层
---
## 5. 阶段退出标准
当前阶段只有在以下条件全部满足时才视为完成:
1. renderer/editor 边界中不再存在新的 `SceneView` 语义向 renderer 公共接口泄漏。
2. scene viewport 的 builtin post-process 组合链路具备稳定自动化回归覆盖。
3. `rendering_phase_regression` 保持通过。
4. 新功能开发转入下一阶段,不再回头污染本阶段边界。
截至本文落地时,这些退出标准已经满足。
---
## 6. 下一阶段入口
Renderer 下一阶段应当正式转向:
- renderer 内更完整的多 pass / phase 模型
- editor/game shared render feature 契约
- object-id 正式化
- 为后续 C# SRP 搭建真正可扩展的 renderer 接口
一句话总结:
- 当前阶段已经把“Renderer 从 RHI 之上独立出来,并接通 editor scene viewport”这件事做完
- 下一阶段不该继续修补这一层,而应开始建设更正式的 renderer 扩展框架

View File

@@ -1,11 +1,14 @@
#pragma once
#include "IViewportHostService.h"
#include "ViewportHostRenderTargets.h"
#include <XCEngine/Core/Math/Color.h>
#include <XCEngine/Rendering/Passes/BuiltinInfiniteGridPass.h>
#include <XCEngine/Rendering/CameraRenderRequest.h>
#include <string>
#include <vector>
namespace XCEngine {
namespace Editor {
@@ -112,6 +115,52 @@ inline ViewportRenderFallbackPolicy BuildGameViewportRenderFailurePolicy(
return policy;
}
struct SceneViewportBuiltinPostProcessBuildResult {
Rendering::BuiltinPostProcessRequest request = {};
const char* warningStatusText = nullptr;
};
inline Rendering::Passes::InfiniteGridPassData BuildSceneViewportGridPassData(
const SceneViewportOverlayData& overlay) {
Rendering::Passes::InfiniteGridPassData data = {};
data.valid = overlay.valid;
data.cameraPosition = overlay.cameraPosition;
data.cameraForward = overlay.cameraForward;
data.cameraRight = overlay.cameraRight;
data.cameraUp = overlay.cameraUp;
data.verticalFovDegrees = overlay.verticalFovDegrees;
data.nearClipPlane = overlay.nearClipPlane;
data.farClipPlane = overlay.farClipPlane;
data.orbitDistance = overlay.orbitDistance;
return data;
}
inline SceneViewportBuiltinPostProcessBuildResult BuildSceneViewportBuiltinPostProcess(
const SceneViewportOverlayData& overlay,
const std::vector<uint64_t>& selectedObjectIds,
bool hasObjectIdShaderView,
bool debugSelectionMask = false) {
SceneViewportBuiltinPostProcessBuildResult result = {};
if (!overlay.valid) {
return result;
}
result.request.gridPassData = BuildSceneViewportGridPassData(overlay);
result.request.selectedObjectIds = selectedObjectIds;
result.request.outlineStyle = {};
result.request.outlineStyle.outlineColor = Math::Color(1.0f, 0.4f, 0.0f, 1.0f);
result.request.outlineStyle.outlineWidthPixels = 2.0f;
result.request.outlineStyle.debugSelectionMask = debugSelectionMask;
if (!selectedObjectIds.empty() &&
!debugSelectionMask &&
!hasObjectIdShaderView) {
result.warningStatusText = "Scene object id shader view is unavailable";
}
return result;
}
inline void ApplySceneViewportRenderRequestSetup(
const ViewportRenderTargets& targets,
const Rendering::BuiltinPostProcessRequest* builtinPostProcess,

View File

@@ -37,21 +37,6 @@ namespace {
constexpr bool kDebugSceneSelectionMask = false;
Rendering::Passes::InfiniteGridPassData BuildInfiniteGridPassData(
const SceneViewportOverlayData& overlay) {
Rendering::Passes::InfiniteGridPassData data = {};
data.valid = overlay.valid;
data.cameraPosition = overlay.cameraPosition;
data.cameraForward = overlay.cameraForward;
data.cameraRight = overlay.cameraRight;
data.cameraUp = overlay.cameraUp;
data.verticalFovDegrees = overlay.verticalFovDegrees;
data.nearClipPlane = overlay.nearClipPlane;
data.farClipPlane = overlay.farClipPlane;
data.orbitDistance = overlay.orbitDistance;
return data;
}
Math::Vector3 GetSceneViewportOrientationAxisVector(SceneViewportOrientationAxis axis) {
switch (axis) {
case SceneViewportOrientationAxis::PositiveX:
@@ -410,30 +395,6 @@ private:
policy.clearColor.a);
}
void BuildSceneViewBuiltinPostProcessRequest(
ViewportEntry& entry,
const SceneViewportOverlayData& overlay,
const std::vector<uint64_t>& selectedObjectIds,
Rendering::BuiltinPostProcessRequest& outRequest) {
if (!overlay.valid) {
outRequest = {};
return;
}
outRequest.gridPassData = BuildInfiniteGridPassData(overlay);
outRequest.selectedObjectIds = selectedObjectIds;
outRequest.outlineStyle = {};
outRequest.outlineStyle.outlineColor = Math::Color(1.0f, 0.4f, 0.0f, 1.0f);
outRequest.outlineStyle.outlineWidthPixels = 2.0f;
outRequest.outlineStyle.debugSelectionMask = kDebugSceneSelectionMask;
if (!selectedObjectIds.empty() &&
!kDebugSceneSelectionMask &&
entry.renderTargets.objectIdShaderView == nullptr) {
SetViewportStatusIfEmpty(entry.statusText, "Scene object id shader view is unavailable");
}
}
void BuildSceneViewportRenderState(
ViewportEntry& entry,
IEditorContext& context,
@@ -446,11 +407,16 @@ private:
}
outState.selectedObjectIds = context.GetSelectionManager().GetSelectedEntities();
BuildSceneViewBuiltinPostProcessRequest(
entry,
const SceneViewportBuiltinPostProcessBuildResult builtinPostProcess =
BuildSceneViewportBuiltinPostProcess(
outState.overlay,
outState.selectedObjectIds,
outState.builtinPostProcess);
entry.renderTargets.objectIdShaderView != nullptr,
kDebugSceneSelectionMask);
outState.builtinPostProcess = builtinPostProcess.request;
if (builtinPostProcess.warningStatusText != nullptr) {
SetViewportStatusIfEmpty(entry.statusText, builtinPostProcess.warningStatusText);
}
}
bool RenderSceneViewportEntry(

View File

@@ -10,12 +10,14 @@ namespace {
using XCEngine::Editor::ApplySceneViewportRenderRequestSetup;
using XCEngine::Editor::ApplyViewportFailureStatus;
using XCEngine::Editor::BuildGameViewportRenderFailurePolicy;
using XCEngine::Editor::BuildSceneViewportBuiltinPostProcess;
using XCEngine::Editor::BuildSceneViewportRenderFailurePolicy;
using XCEngine::Editor::BuildViewportRenderTargetUnavailablePolicy;
using XCEngine::Editor::GameViewportRenderFailure;
using XCEngine::Editor::MarkGameViewportRenderSuccess;
using XCEngine::Editor::MarkSceneViewportRenderSuccess;
using XCEngine::Editor::SceneViewportRenderFailure;
using XCEngine::Editor::SceneViewportOverlayData;
using XCEngine::Editor::ViewportRenderTargets;
using XCEngine::RHI::Format;
using XCEngine::RHI::RHIResourceView;
@@ -76,6 +78,20 @@ public:
}
};
SceneViewportOverlayData CreateValidOverlay() {
SceneViewportOverlayData overlay = {};
overlay.valid = true;
overlay.cameraPosition = XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f);
overlay.cameraForward = XCEngine::Math::Vector3::Forward();
overlay.cameraRight = XCEngine::Math::Vector3::Right();
overlay.cameraUp = XCEngine::Math::Vector3::Up();
overlay.verticalFovDegrees = 70.0f;
overlay.nearClipPlane = 0.1f;
overlay.farClipPlane = 500.0f;
overlay.orbitDistance = 9.0f;
return overlay;
}
TEST(ViewportRenderFlowUtilsTest, BuildFailurePoliciesExposeExpectedStatusAndClearBehavior) {
const auto targetUnavailable = BuildViewportRenderTargetUnavailablePolicy();
EXPECT_STREQ(targetUnavailable.statusText, "Viewport render target is unavailable");
@@ -125,6 +141,58 @@ TEST(ViewportRenderFlowUtilsTest, ApplyViewportFailureStatusRespectsSetIfEmptyBe
EXPECT_EQ(statusText, "No active scene");
}
TEST(ViewportRenderFlowUtilsTest, BuildSceneViewportBuiltinPostProcessReturnsEmptyRequestWhenOverlayIsInvalid) {
const auto result = BuildSceneViewportBuiltinPostProcess({}, {}, true, false);
EXPECT_FALSE(result.request.IsRequested());
EXPECT_EQ(result.warningStatusText, nullptr);
}
TEST(ViewportRenderFlowUtilsTest, BuildSceneViewportBuiltinPostProcessConfiguresGridAndOutlineDefaults) {
const auto result = BuildSceneViewportBuiltinPostProcess(
CreateValidOverlay(),
{ 7u, 11u },
true,
false);
EXPECT_TRUE(result.request.IsRequested());
EXPECT_TRUE(result.request.gridPassData.valid);
EXPECT_EQ(result.request.selectedObjectIds.size(), 2u);
EXPECT_EQ(result.request.selectedObjectIds[0], 7u);
EXPECT_EQ(result.request.selectedObjectIds[1], 11u);
EXPECT_FLOAT_EQ(result.request.outlineStyle.outlineColor.r, 1.0f);
EXPECT_FLOAT_EQ(result.request.outlineStyle.outlineColor.g, 0.4f);
EXPECT_FLOAT_EQ(result.request.outlineStyle.outlineColor.b, 0.0f);
EXPECT_FLOAT_EQ(result.request.outlineStyle.outlineWidthPixels, 2.0f);
EXPECT_FALSE(result.request.outlineStyle.debugSelectionMask);
EXPECT_EQ(result.warningStatusText, nullptr);
}
TEST(ViewportRenderFlowUtilsTest, BuildSceneViewportBuiltinPostProcessReportsMissingObjectIdShaderViewForSelectionOutline) {
const auto result = BuildSceneViewportBuiltinPostProcess(
CreateValidOverlay(),
{ 42u },
false,
false);
EXPECT_TRUE(result.request.IsRequested());
EXPECT_TRUE(result.request.gridPassData.valid);
EXPECT_EQ(result.request.selectedObjectIds.size(), 1u);
EXPECT_STREQ(result.warningStatusText, "Scene object id shader view is unavailable");
}
TEST(ViewportRenderFlowUtilsTest, BuildSceneViewportBuiltinPostProcessDoesNotWarnWhenDebugMaskDisablesSelectionOutlineFallback) {
const auto result = BuildSceneViewportBuiltinPostProcess(
CreateValidOverlay(),
{ 42u },
false,
true);
EXPECT_TRUE(result.request.IsRequested());
EXPECT_TRUE(result.request.outlineStyle.debugSelectionMask);
EXPECT_EQ(result.warningStatusText, nullptr);
}
TEST(ViewportRenderFlowUtilsTest, ApplySceneRenderRequestSetupAttachesOptionalPassesAndObjectIdSurface) {
DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt);
DummyResourceView objectIdView(ResourceViewType::RenderTarget);
@@ -194,6 +262,37 @@ TEST(ViewportRenderFlowUtilsTest, ApplySceneRenderRequestSetupSkipsUnavailableOp
EXPECT_FALSE(request.builtinPostProcess.IsRequested());
}
TEST(ViewportRenderFlowUtilsTest, ApplySceneRenderRequestSetupPreservesBuiltinGridFallbackWhenObjectIdShaderViewIsUnavailable) {
DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt);
DummyResourceView objectIdView(ResourceViewType::RenderTarget);
ViewportRenderTargets targets = {};
targets.width = 800;
targets.height = 600;
targets.depthView = &depthView;
targets.objectIdView = &objectIdView;
const auto builtinPostProcess = BuildSceneViewportBuiltinPostProcess(
CreateValidOverlay(),
{ 99u },
false,
false);
XCEngine::Rendering::CameraRenderRequest request = {};
request.surface = RenderSurface(800, 600);
ApplySceneViewportRenderRequestSetup(
targets,
&builtinPostProcess.request,
nullptr,
request);
EXPECT_TRUE(request.builtinPostProcess.IsRequested());
EXPECT_EQ(request.builtinPostProcess.objectIdTextureView, nullptr);
EXPECT_EQ(request.builtinPostProcess.selectedObjectIds.size(), 1u);
EXPECT_TRUE(request.objectId.IsRequested());
}
TEST(ViewportRenderFlowUtilsTest, MarkSceneRenderSuccessMovesTargetsToShaderResourceState) {
DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt);
DummyResourceView objectIdView(ResourceViewType::RenderTarget);