test: close renderer phase boundary
This commit is contained in:
164
docs/plan/Renderer阶段收口说明.md
Normal file
164
docs/plan/Renderer阶段收口说明.md
Normal 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 扩展框架
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user