diff --git a/docs/plan/Renderer阶段收口说明.md b/docs/plan/Renderer阶段收口说明.md new file mode 100644 index 00000000..44fdd05a --- /dev/null +++ b/docs/plan/Renderer阶段收口说明.md @@ -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 扩展框架 diff --git a/editor/src/Viewport/ViewportHostRenderFlowUtils.h b/editor/src/Viewport/ViewportHostRenderFlowUtils.h index 8907ed7c..6e50e6bb 100644 --- a/editor/src/Viewport/ViewportHostRenderFlowUtils.h +++ b/editor/src/Viewport/ViewportHostRenderFlowUtils.h @@ -1,11 +1,14 @@ #pragma once +#include "IViewportHostService.h" #include "ViewportHostRenderTargets.h" #include +#include #include #include +#include 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& 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, diff --git a/editor/src/Viewport/ViewportHostService.h b/editor/src/Viewport/ViewportHostService.h index 84d6c63d..3efdde77 100644 --- a/editor/src/Viewport/ViewportHostService.h +++ b/editor/src/Viewport/ViewportHostService.h @@ -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& 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( diff --git a/tests/editor/test_viewport_render_flow_utils.cpp b/tests/editor/test_viewport_render_flow_utils.cpp index e86a8160..b3f28d18 100644 --- a/tests/editor/test_viewport_render_flow_utils.cpp +++ b/tests/editor/test_viewport_render_flow_utils.cpp @@ -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);