From a615f78e727bcb62e7aac51dc2ebd7a256a50124 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Mon, 20 Apr 2026 15:03:45 +0800 Subject: [PATCH] feat(srp): formalize renderer contracts and project feature bridge --- ...项目侧自定义渲染管线打通计划_2026-04-20.md | 147 ++++++ ...P_RendererData主链正式化计划_2026-04-20.md | 233 +++++++++ managed/GameScripts/RenderPipelineApiProbe.cs | 488 ++++++++++++++++++ .../Core/ScriptableRenderPipelineAsset.cs | 5 + .../ColorScalePostProcessRendererFeature.cs | 2 +- .../Universal/RendererBackedRenderPipeline.cs | 2 +- .../RendererBackedRenderPipelineAsset.cs | 126 ++++- .../Rendering/Universal/ScriptableRenderer.cs | 2 +- .../Universal/ScriptableRendererData.cs | 15 + .../Universal/ScriptableRendererFeature.cs | 14 + .../Scripts/ProjectRenderPipelineProbe.cs | 92 ++++ tests/scripting/test_mono_script_runtime.cpp | 393 ++++++++++++++ .../test_project_script_assembly.cpp | 113 ++++ 13 files changed, 1604 insertions(+), 28 deletions(-) create mode 100644 docs/plan/SRP_项目侧自定义渲染管线打通计划_2026-04-20.md create mode 100644 docs/used/SRP_RendererData主链正式化计划_2026-04-20.md create mode 100644 project/Assets/Scripts/ProjectRenderPipelineProbe.cs diff --git a/docs/plan/SRP_项目侧自定义渲染管线打通计划_2026-04-20.md b/docs/plan/SRP_项目侧自定义渲染管线打通计划_2026-04-20.md new file mode 100644 index 00000000..8cc98eca --- /dev/null +++ b/docs/plan/SRP_项目侧自定义渲染管线打通计划_2026-04-20.md @@ -0,0 +1,147 @@ +# SRP 项目侧自定义渲染管线打通计划 2026-04-20 + +## 1. 阶段目标 + +上一阶段已经把这条 managed 主链正式化了: + +`ScriptableRenderPipelineAsset -> RendererData -> Renderer -> Feature -> Pass` + +并且已经锁住了: + +1. `renderer data dirty -> asset runtime version -> native runtime rebuild` +2. `request / frame plan / backend key / execution` 统一 renderer selection +3. feature/pass 生命周期与 builtin/custom feature 顺序契约 + +下一阶段不再继续整理内部边界,而是正式把“用户在项目脚本里自定义 SRP/URP”这条外部能力打通。 + +目标不是先做编辑器,而是先证明: + +1. `project/Assets` 里的 C# 脚本可以直接继承公开 SRP API +2. 项目脚本定义的 `RenderPipelineAsset / RendererData / RendererFeature / RenderPass` 能真正跑进 native runtime +3. 这条路径不是 probe 特例,而是后续做 Unity 风格 SRP/URP 的正式入口 + +--- + +## 2. 当前缺口 + +当前引擎虽然已经有: + +1. `XCEngine.ScriptCore.dll` +2. `XCEngine.RenderPipelines.Universal.dll` +3. managed render pipeline bridge +4. 项目脚本程序集 `project/Assets -> GameScripts.dll` + +但是目前 SRP 能力主要还是在 `managed/GameScripts/RenderPipelineApiProbe.cs` 里被验证,离“项目作者真正使用”还差一层正式化: + +1. 缺少项目侧自定义 renderer feature/pass 的正式验证 +2. 缺少项目侧自定义 renderer data / pipeline asset 的正式验证 +3. 缺少项目侧路径下的 invalidation / rebuild / release 契约验证 +4. 还没有把“引擎内置 probe”与“项目作者可直接照着写”的能力明确区分开 + +也就是说,现在 SRP core 已经成型,但“用户可用”这一步还没有打通。 + +--- + +## 3. 本阶段范围 + +本阶段只做“项目侧自定义 SRP 能力打通”,不做下面这些内容: + +1. 不做 editor inspector / asset 面板 +2. 不做新的渲染特性大项,比如 deferred / lightmap / SSAO +3. 不改 RenderGraph 的 C++ 层方向 +4. 不做 HDRP 风格的复杂内容体系 + +本阶段只做一件事: + +把 `project/Assets` 里的 C# 脚本真正变成可运行、可验证、可扩展的 SRP 入口。 + +--- + +## 4. 实施步骤 + +### Step 1:打通项目侧 custom renderer feature / pass + +目标: + +让项目脚本可以直接定义自己的 `ScriptableRendererFeature` 和 `ScriptableRenderPass`,并通过公开 API 参与主场景 / 后处理阶段录制。 + +预期改动点: + +1. `project/Assets/Scripts/*` +2. `tests/scripting/test_mono_script_runtime.cpp` +3. 如有必要,微调 `managed/XCEngine.ScriptCore/Rendering/*` 的公开面 + +需要锁住的结果: + +1. 项目脚本定义的 custom feature/pass 能被发现并运行 +2. 录制顺序与 stage 支持判断符合正式 SRP 链路 +3. 不依赖 probe 内部 helper 才能工作 + +### Step 2:打通项目侧 custom renderer data / pipeline asset + +目标: + +让项目脚本直接定义自己的 `RendererBackedRenderPipelineAsset` / `ScriptableRendererData` / `ScriptableRenderer`,并由 `GraphicsSettings.renderPipelineAsset` 正式选中。 + +预期改动点: + +1. `project/Assets/Scripts/*` +2. `tests/scripting/test_mono_script_runtime.cpp` +3. 如有必要,补 `managed/XCEngine.ScriptCore/Rendering/Universal/*` + +需要锁住的结果: + +1. 项目侧 asset 能经由 managed bridge materialize 成 native runtime +2. backend key / request / plan / execution 继续走统一主链 +3. 项目侧 renderer 不需要 engine 内部特判 + +### Step 3:锁住项目侧 invalidation / rebuild / release 契约 + +目标: + +把“项目作者改了 pipeline asset / renderer data / feature 配置之后,native runtime 能稳定重建”这件事正式化。 + +需要锁住的结果: + +1. project-side asset invalidation 会传播到 runtime version +2. project-side renderer data invalidation 会触发 renderer/pipeline rebuild +3. release 后不会残留旧 feature/pass/runtime cache + +### Step 4:阶段验证与收口 + +固定验证流程: + +1. 编译 `XCEditor` +2. 运行 `rendering_unit_tests` +3. 运行 `scripting_tests` +4. 运行旧版 `editor/bin/Debug/XCEngine.exe` 至少 10s +5. 检查新的 `editor/bin/Debug/editor.log` 中出现 `SceneReady` +6. plan 归档到 `docs/used` +7. 使用规范 commit message 提交并推送 + +--- + +## 5. 验收标准 + +完成后应满足: + +1. 项目脚本可以直接定义并运行自定义 SRP 组件 +2. engine package 提供的是正式 API,而不是只能靠测试 probe 才能走通 +3. 用户未来写自己的“轻量 URP 包”时,不需要再先改 C++ 主链 +4. 下一阶段可以继续往真正的 URP 内容层扩展,而不是继续补基础接缝 + +--- + +## 6. 阶段意义 + +这一步做完之后,引擎就不只是“内部已经有 SRP 骨架”,而是会进入: + +`SRP core 已可被项目脚本直接消费` + +这才是后面继续做 Unity 风格: + +1. 官方 URP 包层 +2. 用户自定义 RendererFeature +3. 更复杂的通用渲染特性 + +的真正起点。 diff --git a/docs/used/SRP_RendererData主链正式化计划_2026-04-20.md b/docs/used/SRP_RendererData主链正式化计划_2026-04-20.md new file mode 100644 index 00000000..88dac08e --- /dev/null +++ b/docs/used/SRP_RendererData主链正式化计划_2026-04-20.md @@ -0,0 +1,233 @@ +# SRP RendererData 主链正式化计划 2026-04-20 + +## 1. 阶段目标 + +上一阶段已经把: + +`ManagedRenderPipelineAssetRuntime -> native renderer backend registry` + +这条接缝补齐了,`managed backend key -> native backend asset` 不再是单点硬编码。 + +下一阶段不再补零散 seam,而是正式收紧 Unity 风格的 managed SRP 主链: + +`RenderPipelineAsset -> RendererData -> Renderer -> RendererFeature -> RenderPass` + +这一阶段的目标,是把当前已经存在但还不够“主线化”的 Universal/SRP 结构,收成一个稳定、清晰、可继续往 URP 演进的正式骨架。 + +--- + +## 2. 当前状态 + +当前已经具备的能力: + +1. C++ 侧已经有 `ManagedScriptableRenderPipelineAsset`,可以通过 Mono bridge 创建 managed asset runtime。 +2. managed 侧已经有: + - `ScriptableRenderPipelineAsset` + - `RendererBackedRenderPipelineAsset` + - `ScriptableRendererData` + - `ScriptableRenderer` + - `ScriptableRendererFeature` + - `ScriptableRenderPass` + - `UniversalRenderPipelineAsset` + - `UniversalRendererData` + - `UniversalRenderer` +3. 默认 Universal 路径已经能驱动 main scene / post process / final output 这些录制。 +4. renderer backend key 已经能通过 managed asset / renderer data 交给 native factory 解析。 + +也就是说,骨架已经不是从零开始了,问题不在“有没有”,而在“主链边界还没有彻底收紧”。 + +--- + +## 3. 当前主要问题 + +### 3.1 `RendererBackedRenderPipelineAsset` 的主链职责虽然存在,但还没有完全锁死 + +当前 asset 已经负责: + +1. 创建 pipeline +2. 解析 default renderer data / renderer +3. 转发 camera request 配置 +4. 转发 camera frame plan 配置 +5. 提供 backend key +6. 释放 renderer data runtime resources + +但是这条链的“正式契约”还不够清楚,很多行为只是现在恰好能跑,不代表后续扩展 renderer list / renderer selection / 多种 renderer data 时仍然稳定。 + +### 3.2 `ScriptableRendererData` 的 dirty / rebuild 语义还不够完整 + +当前 `ScriptableRendererData` 已经有: + +1. renderer instance cache +2. renderer feature cache +3. `SetDirty()` +4. runtime resource release + +但它的 dirty 更多还是“本地缓存失效”语义,尚未完全锁死: + +1. renderer data 失效后,asset 的 runtime resource version 是否必然变化 +2. native runtime 是否会稳定感知并重建 +3. renderer data / renderer / feature 的 release 顺序是否完全可预测 + +这一步如果不收紧,后面做真正的 URP renderer asset、feature authoring、甚至多 renderer 切换时,会不断出现“改了配置但 native 还拿着旧缓存”的问题。 + +### 3.3 default renderer 选择与 fallback 规则还不够正式 + +当前已经有: + +1. `rendererDataList` +2. `defaultRendererIndex` +3. invalid index fallback + +但还缺少更完整的主线约束: + +1. renderer data 为空时如何补默认项 +2. 多个 slot 指向同一 renderer data 时如何释放 +3. request / frame plan / backend key / renderer execution 是否始终走同一套 renderer selection + +### 3.4 builtin renderer 行为和自定义 feature 行为需要进一步统一 + +当前 `UniversalRenderer` 内部已经挂了 builtin scene feature,同时 `UniversalRendererData` 也能提供自定义 `rendererFeatures`。 + +这说明主线已经成型,但还需要进一步锁死: + +1. builtin feature 和用户 feature 的装配顺序 +2. pass queue 的稳定顺序 +3. invalidation 后 builtin/custom feature 是否都被正确重建 + +--- + +## 4. 本阶段要解决什么 + +这一阶段只做 SRP/URP 主链正式化,不做下面这些内容: + +1. 不做 deferred renderer +2. 不做新的 native renderer backend +3. 不做 editor 侧 renderer asset 检视器 +4. 不做新的大型图形效果 + +这一阶段只做一件事: + +把现有 `RendererBackedRenderPipelineAsset / ScriptableRendererData / UniversalRenderer` +这条链,收成一个后续可以稳定承接“真正 URP 包层演进”的正式主线。 + +`Render Graph` 仍然继续留在 C++ 层,这一阶段不改这个大方向。 + +--- + +## 5. 实施方案 + +### Step 1:正式化 asset 与 renderer data 的失效传播 + +目标: + +让 renderer data 的 dirty 不再只是 managed 本地缓存行为,而是正式影响 asset runtime version / native runtime rebuild 判定。 + +预期改动点: + +1. `managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs` +2. `managed/XCEngine.ScriptCore/Rendering/Universal/RendererBackedRenderPipelineAsset.cs` +3. `managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs` +4. 必要时补 `engine/src/Scripting/Mono/MonoScriptRuntime.cpp` 对应验证 + +要锁住的结果: + +1. 改 renderer data 配置会触发 asset 级 runtime resource version 变化 +2. native runtime 能稳定重建 renderer / pipeline 相关缓存 + +### Step 2:正式化 renderer selection 主链 + +目标: + +把“选哪个 renderer data / renderer”变成单一主链,不允许 request、frame plan、backend key、execution 各走各的。 + +预期改动点: + +1. `managed/XCEngine.ScriptCore/Rendering/Universal/RendererBackedRenderPipelineAsset.cs` +2. `managed/XCEngine.ScriptCore/Rendering/Universal/RendererBackedRenderPipeline.cs` +3. `managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderPipelineAsset.cs` + +要锁住的结果: + +1. default renderer resolve 规则唯一 +2. invalid index fallback 行为唯一 +3. `ConfigureCameraRenderRequest / ConfigureCameraFramePlan / GetPipelineRendererAssetKey / ResolveRenderer` + 始终基于同一 renderer selection + +### Step 3:正式化 renderer / feature / pass 的装配与释放边界 + +目标: + +让 `ScriptableRendererData -> ScriptableRenderer -> ScriptableRendererFeature -> ScriptableRenderPass` +这条运行时装配链可预测、可重建、可释放。 + +预期改动点: + +1. `managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs` +2. `managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRenderer.cs` +3. `managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererFeature.cs` +4. `managed/XCEngine.ScriptCore/Rendering/Universal/UniversalRenderer.cs` +5. 必要时相关 probe / test script + +要锁住的结果: + +1. builtin feature 与 custom feature 的创建顺序稳定 +2. pass queue 顺序稳定 +3. dirty/release 后 renderer、feature、pass 不残留旧实例 + +### Step 4:补齐测试,把主链锁死 + +需要重点补的测试方向: + +1. renderer data dirty 会触发 managed asset runtime rebuild +2. renderer selection 在 request / plan / execution / backend key 上保持一致 +3. duplicated renderer data slot 的 release 不重复 +4. builtin feature + custom feature 的装配顺序与 pass 顺序稳定 +5. runtime release 后再次访问会得到新实例而不是旧缓存 + +优先测试文件: + +1. `tests/scripting/test_mono_script_runtime.cpp` +2. `tests/Rendering/unit/test_camera_scene_renderer.cpp` +3. 如有必要补 managed probe: + `managed/GameScripts/RenderPipelineApiProbe.cs` + +### Step 5:阶段验证与收口 + +本阶段完成后,按固定流程验证: + +1. 编译 `XCEditor` +2. 运行 `rendering_unit_tests` +3. 运行 `scripting_tests` +4. 运行旧版 `editor/bin/Debug/XCEngine.exe` 冒烟至少 10s +5. 检查新的 `editor/bin/Debug/editor.log` 中出现 `SceneReady` +6. plan 归档到 `docs/used` +7. 使用规范 commit message 提交并推送 + +--- + +## 6. 验收标准 + +完成后应满足: + +1. `RendererBackedRenderPipelineAsset` 成为明确的 renderer selection 主入口 +2. `ScriptableRendererData` 的 dirty 能稳定传导到 asset/runtime rebuild +3. renderer、feature、pass 的创建/释放/重建边界清楚且有测试锁定 +4. Universal 默认路径和自定义 feature 路径都走同一套正式主链 +5. 后续再往上做真正的 URP 包层功能时,不需要继续补大量“临时接缝” + +--- + +## 7. 阶段完成后的意义 + +这一阶段完成之后,SRP 就不再只是“managed 侧能录几段 graph”的试运行状态,而会进入: + +`SRP core 已成型,URP 可以沿着正式主链继续搭` + +届时再往后做: + +1. 更完整的 Universal renderer data +2. 更正式的 renderer feature authoring +3. 真正的 URP 包层功能扩展 +4. 更进一步的项目级渲染管线资源化 + +都会顺很多。 diff --git a/managed/GameScripts/RenderPipelineApiProbe.cs b/managed/GameScripts/RenderPipelineApiProbe.cs index 942d31fa..43b4a79e 100644 --- a/managed/GameScripts/RenderPipelineApiProbe.cs +++ b/managed/GameScripts/RenderPipelineApiProbe.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using XCEngine; using XCEngine.Rendering; using XCEngine.Rendering.Universal; @@ -30,6 +31,34 @@ namespace Gameplay } } + internal static class ProbeRuntimeVersionUtility + { + private static readonly MethodInfo s_getRuntimeResourceVersionMethod = + typeof(ScriptableRenderPipelineAsset) + .GetMethod( + "GetRuntimeResourceVersionInstance", + BindingFlags.Instance | + BindingFlags.NonPublic); + + public static int GetRuntimeResourceVersion( + ScriptableRenderPipelineAsset asset) + { + if (asset == null || + s_getRuntimeResourceVersionMethod == null) + { + return 0; + } + + object version = + s_getRuntimeResourceVersionMethod.Invoke( + asset, + null); + return version is int resolvedVersion + ? resolvedVersion + : 0; + } + } + internal enum SceneInjectionKind { BeforeOpaque, @@ -378,6 +407,140 @@ namespace Gameplay } } + internal static class ManagedFeaturePassOrderProbeState + { + public static string RecordedOrder = string.Empty; + + public static void Reset() + { + RecordedOrder = string.Empty; + } + + public static void Append( + string token) + { + if (string.IsNullOrEmpty(token)) + { + return; + } + + if (!string.IsNullOrEmpty(RecordedOrder)) + { + RecordedOrder += ">"; + } + + RecordedOrder += token; + } + } + + internal sealed class ManagedFeaturePassOrderProbePass + : ScriptableRenderPass + { + private readonly string m_token; + + public ManagedFeaturePassOrderProbePass( + string token) + { + m_token = token ?? string.Empty; + renderPassEvent = RenderPassEvent.RenderOpaques; + } + + protected override bool RecordRenderGraph( + ScriptableRenderContext context, + RenderingData renderingData) + { + if (renderingData == null || + !renderingData.isMainSceneStage) + { + return false; + } + + ManagedFeaturePassOrderProbeState.Append(m_token); + return true; + } + } + + internal sealed class ManagedFeaturePassOrderBuiltinFeature + : ScriptableRendererFeature + { + private readonly ManagedFeaturePassOrderProbePass m_pass = + new ManagedFeaturePassOrderProbePass("Builtin"); + + public override void AddRenderPasses( + ScriptableRenderer renderer, + RenderingData renderingData) + { + if (renderer == null || + renderingData == null || + !renderingData.isMainSceneStage) + { + return; + } + + renderer.EnqueuePass(m_pass); + } + } + + internal sealed class ManagedFeaturePassOrderCustomFeature + : ScriptableRendererFeature + { + private readonly ManagedFeaturePassOrderProbePass m_pass; + + public ManagedFeaturePassOrderCustomFeature( + string token) + { + m_pass = + new ManagedFeaturePassOrderProbePass(token); + } + + public override void AddRenderPasses( + ScriptableRenderer renderer, + RenderingData renderingData) + { + if (renderer == null || + renderingData == null || + !renderingData.isMainSceneStage) + { + return; + } + + renderer.EnqueuePass(m_pass); + } + } + + internal sealed class ManagedFeaturePassOrderProbeRenderer + : ScriptableRenderer + { + public ManagedFeaturePassOrderProbeRenderer() + { + AddFeature( + new ManagedFeaturePassOrderBuiltinFeature()); + } + } + + internal sealed class ManagedFeaturePassOrderProbeRendererData + : ScriptableRendererData + { + protected override ScriptableRenderer CreateRenderer() + { + return new ManagedFeaturePassOrderProbeRenderer(); + } + + protected override ScriptableRendererFeature[] CreateRendererFeatures() + { + return new ScriptableRendererFeature[] + { + new ManagedFeaturePassOrderCustomFeature("CustomA"), + new ManagedFeaturePassOrderCustomFeature("CustomB") + }; + } + + protected override string GetPipelineRendererAssetKey() + { + return "BuiltinForward"; + } + } + internal sealed class CameraDataObservationPass : ScriptableRenderPass { @@ -892,6 +1055,74 @@ namespace Gameplay } } + internal sealed class ManagedRendererSelectionInactiveRenderer + : ScriptableRenderer + { + protected override bool SupportsRendererRecording( + RendererRecordingContext context) + { + return false; + } + + protected override bool RecordRenderer( + RendererRecordingContext context) + { + return false; + } + } + + internal sealed class ManagedRendererSelectionActiveRendererData + : ScriptableRendererData + { + protected override ScriptableRenderer CreateRenderer() + { + return new ProbeSceneRenderer(); + } + + protected override void ConfigureCameraRenderRequest( + CameraRenderRequestContext context) + { + if (HasDirectionalShadow(context)) + { + ClearDirectionalShadow(context); + } + } + + protected override void ConfigureCameraFramePlan( + ScriptableRenderPipelinePlanningContext context) + { + if (context == null || + context.IsStageRequested( + CameraFrameStage.PostProcess)) + { + return; + } + + context.RequestFullscreenStage( + CameraFrameStage.PostProcess, + CameraFrameColorSource.MainSceneColor); + } + + protected override string GetPipelineRendererAssetKey() + { + return "BuiltinForward"; + } + } + + internal sealed class ManagedRendererSelectionInactiveRendererData + : ScriptableRendererData + { + protected override ScriptableRenderer CreateRenderer() + { + return new ManagedRendererSelectionInactiveRenderer(); + } + + protected override string GetPipelineRendererAssetKey() + { + return "MissingBackend"; + } + } + internal sealed class ManagedRendererReuseProbeRendererData : ProbeRendererData { @@ -914,6 +1145,8 @@ namespace Gameplay internal static class ManagedRendererInvalidationProbeState { + public static int CreatePipelineCallCount; + public static int DisposePipelineCallCount; public static int CreateRendererCallCount; public static int SetupRendererCallCount; public static int CreateFeatureCallCount; @@ -923,6 +1156,8 @@ namespace Gameplay public static void Reset() { + CreatePipelineCallCount = 0; + DisposePipelineCallCount = 0; CreateRendererCallCount = 0; SetupRendererCallCount = 0; CreateFeatureCallCount = 0; @@ -932,6 +1167,30 @@ namespace Gameplay } } + internal sealed class ManagedRendererInvalidationProbePipeline + : RendererBackedRenderPipeline + { + public ManagedRendererInvalidationProbePipeline( + RendererBackedRenderPipelineAsset asset) + : base(asset) + { + ManagedRendererInvalidationProbeState + .CreatePipelineCallCount++; + } + + protected override void Dispose( + bool disposing) + { + if (disposing) + { + ManagedRendererInvalidationProbeState + .DisposePipelineCallCount++; + } + + base.Dispose(disposing); + } + } + internal sealed class ManagedRendererInvalidationProbeFeature : ScriptableRendererFeature { @@ -982,6 +1241,79 @@ namespace Gameplay } } + internal static class ManagedPersistentFeatureProbeState + { + public static int CreateRendererCallCount; + public static int CreateFeatureRuntimeCallCount; + public static int DisposeRendererCallCount; + public static int DisposeFeatureCallCount; + public static int InvalidateRendererCallCount; + + public static void Reset() + { + CreateRendererCallCount = 0; + CreateFeatureRuntimeCallCount = 0; + DisposeRendererCallCount = 0; + DisposeFeatureCallCount = 0; + InvalidateRendererCallCount = 0; + } + } + + internal sealed class ManagedPersistentFeatureProbeRendererFeature + : ScriptableRendererFeature + { + public override void Create() + { + ManagedPersistentFeatureProbeState + .CreateFeatureRuntimeCallCount++; + } + + protected override void ReleaseRuntimeResources() + { + ManagedPersistentFeatureProbeState + .DisposeFeatureCallCount++; + } + } + + internal sealed class ManagedPersistentFeatureProbeRenderer + : ProbeSceneRenderer + { + protected override void ReleaseRuntimeResources() + { + ManagedPersistentFeatureProbeState + .DisposeRendererCallCount++; + } + } + + internal sealed class ManagedPersistentFeatureProbeRendererData + : ProbeRendererData + { + private readonly ManagedPersistentFeatureProbeRendererFeature m_feature = + new ManagedPersistentFeatureProbeRendererFeature(); + + protected override ScriptableRenderer CreateProbeRenderer() + { + ManagedPersistentFeatureProbeState + .CreateRendererCallCount++; + return new ManagedPersistentFeatureProbeRenderer(); + } + + protected override ScriptableRendererFeature[] CreateRendererFeatures() + { + return new ScriptableRendererFeature[] + { + m_feature + }; + } + + public void InvalidateForTest() + { + ManagedPersistentFeatureProbeState + .InvalidateRendererCallCount++; + SetDirty(); + } + } + internal sealed class ManagedPlannedFullscreenRenderPipelineProbeRendererData : ProbeRendererData { @@ -1317,6 +1649,20 @@ namespace Gameplay } } + public sealed class ManagedFallbackRendererSelectionConsistencyProbeAsset + : RendererBackedRenderPipelineAsset + { + public ManagedFallbackRendererSelectionConsistencyProbeAsset() + { + rendererDataList = new ScriptableRendererData[] + { + new ManagedRendererSelectionActiveRendererData(), + new ManagedRendererSelectionInactiveRendererData() + }; + defaultRendererIndex = 7; + } + } + public sealed class ManagedRendererReuseProbeAsset : RendererBackedRenderPipelineAsset { @@ -1347,6 +1693,13 @@ namespace Gameplay }; } + protected override ScriptableRenderPipeline + CreateRendererBackedPipeline() + { + return new ManagedRendererInvalidationProbePipeline( + this); + } + public void InvalidateDefaultRendererForTest() { if (m_rendererData == null) @@ -1358,6 +1711,47 @@ namespace Gameplay } } + public sealed class ManagedPersistentFeatureProbeAsset + : RendererBackedRenderPipelineAsset + { + private readonly ManagedPersistentFeatureProbeRendererData + m_rendererData; + + public ManagedPersistentFeatureProbeAsset() + { + ManagedPersistentFeatureProbeState.Reset(); + m_rendererData = + new ManagedPersistentFeatureProbeRendererData(); + rendererDataList = new ScriptableRendererData[] + { + m_rendererData + }; + } + + public void InvalidateDefaultRendererForTest() + { + if (m_rendererData == null) + { + return; + } + + m_rendererData.InvalidateForTest(); + } + } + + public sealed class ManagedFeaturePassOrderProbeAsset + : RendererBackedRenderPipelineAsset + { + public ManagedFeaturePassOrderProbeAsset() + { + ManagedFeaturePassOrderProbeState.Reset(); + rendererDataList = new ScriptableRendererData[] + { + new ManagedFeaturePassOrderProbeRendererData() + }; + } + } + internal static class ManagedAssetInvalidationProbeState { public static int CreatePipelineCallCount; @@ -1735,12 +2129,16 @@ namespace Gameplay public sealed class ManagedRendererInvalidationObservationProbe : MonoBehaviour { + public int ObservedCreatePipelineCallCount; + public int ObservedDisposePipelineCallCount; public int ObservedCreateRendererCallCount; public int ObservedSetupRendererCallCount; public int ObservedCreateFeatureCallCount; public int ObservedDisposeRendererCallCount; public int ObservedDisposeFeatureCallCount; public int ObservedInvalidateRendererCallCount; + public int ObservedRuntimeResourceVersionBeforeInvalidation; + public int ObservedRuntimeResourceVersionAfterInvalidation; private bool m_requestedInvalidation; @@ -1754,10 +2152,24 @@ namespace Gameplay ManagedRendererInvalidationProbeState .CreateRendererCallCount > 0) { + ObservedRuntimeResourceVersionBeforeInvalidation = + ProbeRuntimeVersionUtility + .GetRuntimeResourceVersion( + selectedAsset); selectedAsset.InvalidateDefaultRendererForTest(); + ObservedRuntimeResourceVersionAfterInvalidation = + ProbeRuntimeVersionUtility + .GetRuntimeResourceVersion( + selectedAsset); m_requestedInvalidation = true; } + ObservedCreatePipelineCallCount = + ManagedRendererInvalidationProbeState + .CreatePipelineCallCount; + ObservedDisposePipelineCallCount = + ManagedRendererInvalidationProbeState + .DisposePipelineCallCount; ObservedCreateRendererCallCount = ManagedRendererInvalidationProbeState.CreateRendererCallCount; ObservedSetupRendererCallCount = @@ -1773,6 +2185,82 @@ namespace Gameplay } } + public sealed class ManagedPersistentFeatureRuntimeSelectionProbe + : MonoBehaviour + { + public void Start() + { + GraphicsSettings.renderPipelineAsset = + new ManagedPersistentFeatureProbeAsset(); + } + } + + public sealed class ManagedPersistentFeatureObservationProbe + : MonoBehaviour + { + public int ObservedCreateRendererCallCount; + public int ObservedCreateFeatureRuntimeCallCount; + public int ObservedDisposeRendererCallCount; + public int ObservedDisposeFeatureCallCount; + public int ObservedInvalidateRendererCallCount; + + private bool m_requestedInvalidation; + + public void Update() + { + ManagedPersistentFeatureProbeAsset selectedAsset = + GraphicsSettings.renderPipelineAsset + as ManagedPersistentFeatureProbeAsset; + if (!m_requestedInvalidation && + selectedAsset != null && + ManagedPersistentFeatureProbeState + .CreateFeatureRuntimeCallCount > 0) + { + selectedAsset.InvalidateDefaultRendererForTest(); + m_requestedInvalidation = true; + } + + ObservedCreateRendererCallCount = + ManagedPersistentFeatureProbeState + .CreateRendererCallCount; + ObservedCreateFeatureRuntimeCallCount = + ManagedPersistentFeatureProbeState + .CreateFeatureRuntimeCallCount; + ObservedDisposeRendererCallCount = + ManagedPersistentFeatureProbeState + .DisposeRendererCallCount; + ObservedDisposeFeatureCallCount = + ManagedPersistentFeatureProbeState + .DisposeFeatureCallCount; + ObservedInvalidateRendererCallCount = + ManagedPersistentFeatureProbeState + .InvalidateRendererCallCount; + } + } + + public sealed class ManagedFeaturePassOrderRuntimeSelectionProbe + : MonoBehaviour + { + public void Start() + { + GraphicsSettings.renderPipelineAsset = + new ManagedFeaturePassOrderProbeAsset(); + } + } + + public sealed class ManagedFeaturePassOrderObservationProbe + : MonoBehaviour + { + public string ObservedOrder = string.Empty; + + public void Update() + { + ObservedOrder = + ManagedFeaturePassOrderProbeState.RecordedOrder ?? + string.Empty; + } + } + public sealed class ManagedAssetInvalidationRuntimeSelectionProbe : MonoBehaviour { diff --git a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs index 1f79e276..d9983fd0 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs @@ -23,6 +23,7 @@ namespace XCEngine.Rendering internal int GetRuntimeResourceVersionInstance() { + SynchronizeRuntimeResourceVersion(); return m_runtimeResourceVersion; } @@ -55,6 +56,10 @@ namespace XCEngine.Rendering { } + protected virtual void SynchronizeRuntimeResourceVersion() + { + } + protected void SetDirty() { ReleaseRuntimeResources(); diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/ColorScalePostProcessRendererFeature.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/ColorScalePostProcessRendererFeature.cs index 0e36e835..9b0bb879 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/ColorScalePostProcessRendererFeature.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/ColorScalePostProcessRendererFeature.cs @@ -60,7 +60,7 @@ namespace XCEngine.Rendering.Universal if (m_pass == null) { - Create(); + CreateInstance(); } renderer.EnqueuePass(m_pass); diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/RendererBackedRenderPipeline.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/RendererBackedRenderPipeline.cs index 11a766a4..dd623e70 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/RendererBackedRenderPipeline.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/RendererBackedRenderPipeline.cs @@ -17,7 +17,7 @@ namespace XCEngine.Rendering.Universal RendererRecordingContext context) { return m_asset != null - ? m_asset.GetDefaultRenderer() + ? m_asset.ResolveSelectedRenderer() : null; } } diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/RendererBackedRenderPipelineAsset.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/RendererBackedRenderPipelineAsset.cs index 1009874c..91d9cdd7 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/RendererBackedRenderPipelineAsset.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/RendererBackedRenderPipelineAsset.cs @@ -10,6 +10,8 @@ namespace XCEngine.Rendering.Universal public ScriptableRendererData[] rendererDataList = Array.Empty(); public int defaultRendererIndex = 0; + private int m_rendererDataRuntimeStateHash; + private bool m_rendererDataRuntimeStateHashResolved; protected RendererBackedRenderPipelineAsset() { @@ -27,7 +29,7 @@ namespace XCEngine.Rendering.Universal new RendererCameraRequestContext(context)); ScriptableRendererData resolvedRendererData = - GetDefaultRendererData(); + ResolveSelectedRendererData(); if (resolvedRendererData != null) { resolvedRendererData @@ -40,7 +42,7 @@ namespace XCEngine.Rendering.Universal ScriptableRenderPipelinePlanningContext context) { ScriptableRendererData resolvedRendererData = - GetDefaultRendererData(); + ResolveSelectedRendererData(); if (resolvedRendererData != null) { resolvedRendererData @@ -52,7 +54,7 @@ namespace XCEngine.Rendering.Universal protected override string GetPipelineRendererAssetKey() { ScriptableRendererData resolvedRendererData = - GetDefaultRendererData(); + ResolveSelectedRendererData(); return resolvedRendererData != null ? resolvedRendererData .GetPipelineRendererAssetKeyInstance() @@ -64,6 +66,29 @@ namespace XCEngine.Rendering.Universal ReleaseRendererDataRuntimeResources(); } + protected override void SynchronizeRuntimeResourceVersion() + { + int runtimeStateHash = + ComputeRendererDataRuntimeStateHash(); + if (!m_rendererDataRuntimeStateHashResolved) + { + m_rendererDataRuntimeStateHash = + runtimeStateHash; + m_rendererDataRuntimeStateHashResolved = true; + return; + } + + if (runtimeStateHash == + m_rendererDataRuntimeStateHash) + { + return; + } + + m_rendererDataRuntimeStateHash = + runtimeStateHash; + SetDirty(); + } + protected virtual ScriptableRenderPipeline CreateRendererBackedPipeline() { @@ -83,37 +108,34 @@ namespace XCEngine.Rendering.Universal internal ScriptableRendererData GetDefaultRendererData() { - return GetRendererData(defaultRendererIndex); + return ResolveSelectedRendererData(); } internal ScriptableRenderer GetDefaultRenderer() { - return GetRenderer(defaultRendererIndex); + return ResolveSelectedRenderer(); + } + + internal ScriptableRendererData ResolveSelectedRendererData() + { + return ResolveRendererDataByResolvedIndex( + ResolveSelectedRendererIndex()); + } + + internal ScriptableRenderer ResolveSelectedRenderer() + { + ScriptableRendererData rendererData = + ResolveSelectedRendererData(); + return rendererData != null + ? rendererData.GetRendererInstance() + : null; } internal ScriptableRendererData GetRendererData( int rendererIndex) { - EnsureRendererDataList(); - if (rendererDataList.Length == 0) - { - return null; - } - - int resolvedRendererIndex = - ResolveRendererIndex(rendererIndex); - if (resolvedRendererIndex < 0) - { - return null; - } - - if (rendererDataList[resolvedRendererIndex] == null) - { - rendererDataList[resolvedRendererIndex] = - CreateDefaultRendererData(); - } - - return rendererDataList[resolvedRendererIndex]; + return ResolveRendererDataByResolvedIndex( + ResolveRendererIndex(rendererIndex)); } internal ScriptableRenderer GetRenderer( @@ -182,6 +204,60 @@ namespace XCEngine.Rendering.Universal return false; } + private int ComputeRendererDataRuntimeStateHash() + { + unchecked + { + int hash = 17; + hash = + (hash * 31) + + ResolveSelectedRendererIndex(); + if (rendererDataList == null) + { + return hash; + } + + hash = (hash * 31) + rendererDataList.Length; + for (int i = 0; i < rendererDataList.Length; ++i) + { + ScriptableRendererData rendererData = + rendererDataList[i]; + if (rendererData != null) + { + hash = + (hash * 31) + + rendererData + .GetRuntimeStateVersionInstance(); + } + } + + return hash; + } + } + + private ScriptableRendererData ResolveRendererDataByResolvedIndex( + int resolvedRendererIndex) + { + EnsureRendererDataList(); + if (resolvedRendererIndex < 0) + { + return null; + } + + if (rendererDataList[resolvedRendererIndex] == null) + { + rendererDataList[resolvedRendererIndex] = + CreateDefaultRendererData(); + } + + return rendererDataList[resolvedRendererIndex]; + } + + private int ResolveSelectedRendererIndex() + { + return ResolveRendererIndex(defaultRendererIndex); + } + private int ResolveRendererIndex( int rendererIndex) { diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRenderer.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRenderer.cs index e936ac4f..bfc1f0f6 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRenderer.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRenderer.cs @@ -69,7 +69,7 @@ namespace XCEngine.Rendering.Universal } m_features.Add(feature); - feature.Create(); + feature.CreateInstance(); } protected virtual void AddRenderPasses( diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs index f0e37651..0da54449 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererData.cs @@ -9,6 +9,7 @@ namespace XCEngine.Rendering.Universal private ScriptableRendererFeature[] m_rendererFeatures; private ScriptableRenderer m_rendererInstance; private bool m_rendererInvalidated; + private int m_runtimeStateVersion = 1; protected ScriptableRendererData() { @@ -60,6 +61,11 @@ namespace XCEngine.Rendering.Universal return GetPipelineRendererAssetKey(); } + internal int GetRuntimeStateVersionInstance() + { + return m_runtimeStateVersion; + } + internal void ReleaseRuntimeResourcesInstance() { ReleaseRendererSetupCache(); @@ -182,6 +188,15 @@ namespace XCEngine.Rendering.Universal { ReleaseRendererSetupCache(); m_rendererInvalidated = true; + unchecked + { + ++m_runtimeStateVersion; + } + + if (m_runtimeStateVersion <= 0) + { + m_runtimeStateVersion = 1; + } } protected void AddRendererFeature( diff --git a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererFeature.cs b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererFeature.cs index 7ef8e86d..9b1bdca4 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererFeature.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Universal/ScriptableRendererFeature.cs @@ -6,6 +6,7 @@ namespace XCEngine.Rendering.Universal public abstract class ScriptableRendererFeature { private bool m_disposed; + private bool m_runtimeCreated; protected ScriptableRendererFeature() { @@ -13,6 +14,18 @@ namespace XCEngine.Rendering.Universal public bool isActive { get; set; } = true; + internal void CreateInstance() + { + if (m_runtimeCreated) + { + return; + } + + m_disposed = false; + Create(); + m_runtimeCreated = true; + } + internal void ReleaseRuntimeResourcesInstance() { if (m_disposed) @@ -22,6 +35,7 @@ namespace XCEngine.Rendering.Universal ReleaseRuntimeResources(); m_disposed = true; + m_runtimeCreated = false; } public virtual void Create() diff --git a/project/Assets/Scripts/ProjectRenderPipelineProbe.cs b/project/Assets/Scripts/ProjectRenderPipelineProbe.cs new file mode 100644 index 00000000..e4239bf4 --- /dev/null +++ b/project/Assets/Scripts/ProjectRenderPipelineProbe.cs @@ -0,0 +1,92 @@ +using XCEngine; +using XCEngine.Rendering; +using XCEngine.Rendering.Universal; + +namespace ProjectScripts +{ + public sealed class ProjectPostProcessColorScalePass + : ScriptableRenderPass + { + public ProjectPostProcessColorScalePass() + { + renderPassEvent = + RenderPassEvent.BeforeRenderingPostProcessing; + } + + protected override bool RecordRenderGraph( + ScriptableRenderContext context, + RenderingData renderingData) + { + return context != null && + renderingData != null && + renderingData.isPostProcessStage && + RecordColorScaleFullscreenPass( + context, + new Vector4(1.15f, 0.95f, 1.05f, 1.0f)); + } + } + + public sealed class ProjectPostProcessColorScaleFeature + : ScriptableRendererFeature + { + private ProjectPostProcessColorScalePass m_pass; + + public override void Create() + { + m_pass = + new ProjectPostProcessColorScalePass(); + } + + public override void ConfigureCameraFramePlan( + ScriptableRenderPipelinePlanningContext context) + { + if (context == null || + context.IsStageRequested( + CameraFrameStage.PostProcess)) + { + return; + } + + context.RequestFullscreenStage( + CameraFrameStage.PostProcess, + CameraFrameColorSource.MainSceneColor); + } + + public override void AddRenderPasses( + ScriptableRenderer renderer, + RenderingData renderingData) + { + if (renderer == null || + renderingData == null || + !renderingData.isPostProcessStage) + { + return; + } + + if (m_pass == null) + { + Create(); + } + + renderer.EnqueuePass(m_pass); + } + } + + public sealed class ProjectUniversalFeaturePipelineAsset + : UniversalRenderPipelineAsset + { + public ProjectUniversalFeaturePipelineAsset() + { + rendererDataList = new ScriptableRendererData[] + { + new UniversalRendererData + { + rendererFeatures = new ScriptableRendererFeature[] + { + new ProjectPostProcessColorScaleFeature() + } + } + }; + } + } +} diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 95a1e021..186478a9 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -3082,6 +3082,164 @@ TEST_F( nullptr); } +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderPipelineBridgeUsesSameFallbackRendererAcrossBackendRequestPlanAndExecution) { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = { + "GameScripts", + "Gameplay", + "ManagedFallbackRendererSelectionConsistencyProbeAsset" + }; + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + + const std::shared_ptr + rendererAsset = assetRuntime->GetPipelineRendererAsset(); + ASSERT_NE(rendererAsset, nullptr); + + std::unique_ptr rendererPipeline = + rendererAsset->CreatePipeline(); + ASSERT_NE(rendererPipeline, nullptr); + EXPECT_NE( + dynamic_cast( + rendererPipeline.get()), + nullptr); + + Scene* runtimeScene = + CreateScene("ManagedFallbackRendererSelectionConsistencyScene"); + GameObject* cameraObject = runtimeScene->CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + ASSERT_NE(camera, nullptr); + camera->SetPrimary(true); + + GameObject* lightObject = runtimeScene->CreateGameObject("Light"); + auto* light = lightObject->AddComponent(); + ASSERT_NE(light, nullptr); + light->SetLightType(LightType::Directional); + light->SetCastsShadows(true); + + XCEngine::Rendering::Pipelines::ManagedScriptableRenderPipelineAsset + asset(descriptor); + + XCEngine::Rendering::CameraRenderRequest request = {}; + request.scene = runtimeScene; + request.camera = camera; + request.surface = XCEngine::Rendering::RenderSurface(64u, 64u); + XCEngine::Rendering::ApplyDefaultRenderPipelineAssetCameraRenderRequestPolicy( + request, + 0u, + 0u, + XCEngine::Rendering::DirectionalShadowPlanningSettings{}); + ASSERT_TRUE(request.directionalShadow.IsValid()); + + asset.ConfigureCameraRenderRequest( + request, + 0u, + 0u, + XCEngine::Rendering::DirectionalShadowPlanningSettings{}); + EXPECT_FALSE(request.directionalShadow.IsValid()); + + XCEngine::Rendering::CameraFramePlan plan = + XCEngine::Rendering::CameraFramePlan::FromRequest(request); + asset.ConfigureCameraFramePlan(plan); + EXPECT_TRUE( + plan.IsFullscreenStageRequested( + XCEngine::Rendering::CameraFrameStage::PostProcess)); + EXPECT_EQ( + plan.ResolveStageColorSource( + XCEngine::Rendering::CameraFrameStage::PostProcess), + XCEngine::Rendering::CameraFrameColorSource::MainSceneColor); + + std::unique_ptr pipeline = + asset.CreatePipeline(); + auto* host = + dynamic_cast( + pipeline.get()); + ASSERT_NE(host, nullptr); + ASSERT_NE(host->GetStageRecorder(), nullptr); + EXPECT_TRUE( + host->GetStageRecorder()->Initialize( + XCEngine::Rendering::RenderContext{})) + << runtime->GetLastError(); + EXPECT_TRUE( + host->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)) + << runtime->GetLastError(); + + XCEngine::Rendering::RenderGraph graph; + XCEngine::Rendering::RenderGraphBuilder graphBuilder(graph); + XCEngine::Rendering::RenderGraphTextureDesc colorDesc = {}; + colorDesc.width = 64u; + colorDesc.height = 64u; + colorDesc.format = + static_cast( + XCEngine::RHI::Format::R8G8B8A8_UNorm); + XCEngine::Rendering::RenderGraphTextureDesc depthDesc = colorDesc; + depthDesc.format = + static_cast( + XCEngine::RHI::Format::D32_Float); + const XCEngine::Rendering::RenderGraphTextureHandle colorTarget = + graphBuilder.CreateTransientTexture( + "ManagedFallbackSelectedMainSceneColor", + colorDesc); + const XCEngine::Rendering::RenderGraphTextureHandle depthTarget = + graphBuilder.CreateTransientTexture( + "ManagedFallbackSelectedMainSceneDepth", + depthDesc); + + const XCEngine::Rendering::RenderSceneData sceneData = {}; + const XCEngine::Rendering::RenderSurface surface(64u, 64u); + bool executionSucceeded = true; + XCEngine::Rendering::RenderGraphBlackboard blackboard = {}; + const XCEngine::Rendering::RenderPipelineStageRenderGraphContext graphContext = { + graphBuilder, + "ManagedFallbackSelectedMainScene", + XCEngine::Rendering::CameraFrameStage::MainScene, + {}, + sceneData, + surface, + nullptr, + nullptr, + XCEngine::RHI::ResourceStates::Common, + {}, + { colorTarget }, + depthTarget, + {}, + &executionSucceeded, + &blackboard + }; + + EXPECT_TRUE(host->GetStageRecorder()->RecordStageRenderGraph(graphContext)) + << runtime->GetLastError(); + + XCEngine::Rendering::CompiledRenderGraph compiledGraph = {}; + XCEngine::Containers::String errorMessage; + ASSERT_TRUE( + XCEngine::Rendering::RenderGraphCompiler::Compile( + graph, + compiledGraph, + &errorMessage)) + << errorMessage.CStr(); + ASSERT_EQ(compiledGraph.GetPassCount(), 3u); + EXPECT_STREQ( + compiledGraph.GetPassName(0).CStr(), + "ManagedFallbackSelectedMainScene.Opaque"); + EXPECT_STREQ( + compiledGraph.GetPassName(1).CStr(), + "ManagedFallbackSelectedMainScene.Skybox"); + EXPECT_STREQ( + compiledGraph.GetPassName(2).CStr(), + "ManagedFallbackSelectedMainScene.Transparent"); + + host->GetStageRecorder()->Shutdown(); +} + TEST_F( MonoScriptRuntimeTest, ManagedRenderPipelineBridgeFallsBackToDefaultSceneRecorderWhenBackendKeyIsUnknown) { @@ -3304,9 +3462,21 @@ TEST_F( int observedCreateRendererCallCount = 0; int observedSetupRendererCallCount = 0; int observedCreateFeatureCallCount = 0; + int observedCreatePipelineCallCount = 0; + int observedDisposePipelineCallCount = 0; int observedDisposeRendererCallCount = 0; int observedDisposeFeatureCallCount = 0; int observedInvalidateRendererCallCount = 0; + int observedRuntimeResourceVersionBeforeInvalidation = 0; + int observedRuntimeResourceVersionAfterInvalidation = 0; + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedCreatePipelineCallCount", + observedCreatePipelineCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedDisposePipelineCallCount", + observedDisposePipelineCallCount)); EXPECT_TRUE(runtime->TryGetFieldValue( observationScript, "ObservedCreateRendererCallCount", @@ -3331,17 +3501,130 @@ TEST_F( observationScript, "ObservedInvalidateRendererCallCount", observedInvalidateRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedRuntimeResourceVersionBeforeInvalidation", + observedRuntimeResourceVersionBeforeInvalidation)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedRuntimeResourceVersionAfterInvalidation", + observedRuntimeResourceVersionAfterInvalidation)); + EXPECT_EQ(observedCreatePipelineCallCount, 2); + EXPECT_EQ(observedDisposePipelineCallCount, 1); EXPECT_EQ(observedCreateRendererCallCount, 2); EXPECT_EQ(observedSetupRendererCallCount, 2); EXPECT_EQ(observedCreateFeatureCallCount, 2); EXPECT_EQ(observedDisposeRendererCallCount, 1); EXPECT_EQ(observedDisposeFeatureCallCount, 1); EXPECT_EQ(observedInvalidateRendererCallCount, 1); + EXPECT_GT(observedRuntimeResourceVersionBeforeInvalidation, 0); + EXPECT_EQ( + observedRuntimeResourceVersionAfterInvalidation, + observedRuntimeResourceVersionBeforeInvalidation + 1); recorder->Shutdown(); } +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderPipelineBridgeReleasesPersistentRendererFeatureAcrossRendererInvalidationAndAssetRuntimeRelease) { + Scene* runtimeScene = + CreateScene("ManagedPersistentFeatureLifecycleScene"); + GameObject* selectionObject = + runtimeScene->CreateGameObject( + "ManagedPersistentFeatureSelection"); + ScriptComponent* selectionScript = + AddScript( + selectionObject, + "Gameplay", + "ManagedPersistentFeatureRuntimeSelectionProbe"); + ASSERT_NE(selectionScript, nullptr); + GameObject* observationObject = + runtimeScene->CreateGameObject( + "ManagedPersistentFeatureObservation"); + ScriptComponent* observationScript = + AddScript( + observationObject, + "Gameplay", + "ManagedPersistentFeatureObservationProbe"); + ASSERT_NE(observationScript, nullptr); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = + XCEngine::Rendering::Pipelines::GetConfiguredManagedRenderPipelineAssetDescriptor(); + ASSERT_TRUE(descriptor.IsValid()); + ASSERT_NE(descriptor.managedAssetHandle, 0u); + EXPECT_EQ(descriptor.assemblyName, "GameScripts"); + EXPECT_EQ(descriptor.namespaceName, "Gameplay"); + EXPECT_EQ(descriptor.className, "ManagedPersistentFeatureProbeAsset"); + + { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + + std::unique_ptr + recorder = assetRuntime->CreateStageRecorder(); + ASSERT_NE(recorder, nullptr); + + const XCEngine::Rendering::RenderContext context = {}; + ASSERT_TRUE(recorder->Initialize(context)); + ASSERT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + + engine->OnUpdate(0.016f); + + ASSERT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + + recorder->Shutdown(); + } + + engine->OnUpdate(0.016f); + EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError(); + + int observedCreateRendererCallCount = 0; + int observedCreateFeatureRuntimeCallCount = 0; + int observedDisposeRendererCallCount = 0; + int observedDisposeFeatureCallCount = 0; + int observedInvalidateRendererCallCount = 0; + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedCreateRendererCallCount", + observedCreateRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedCreateFeatureRuntimeCallCount", + observedCreateFeatureRuntimeCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedDisposeRendererCallCount", + observedDisposeRendererCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedDisposeFeatureCallCount", + observedDisposeFeatureCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedInvalidateRendererCallCount", + observedInvalidateRendererCallCount)); + + EXPECT_EQ(observedCreateRendererCallCount, 2); + EXPECT_EQ(observedCreateFeatureRuntimeCallCount, 2); + EXPECT_EQ(observedDisposeRendererCallCount, 2); + EXPECT_EQ(observedDisposeFeatureCallCount, 2); + EXPECT_EQ(observedInvalidateRendererCallCount, 1); +} + TEST_F( MonoScriptRuntimeTest, ManagedRenderPipelineBridgeRebuildsPipelineAfterAssetInvalidation) { @@ -3532,6 +3815,116 @@ TEST_F( recorder->Shutdown(); } +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderPipelineBridgeKeepsBuiltinAndCustomFeaturePassOrderStable) { + Scene* runtimeScene = + CreateScene("ManagedFeaturePassOrderScene"); + GameObject* selectionObject = + runtimeScene->CreateGameObject( + "ManagedFeaturePassOrderSelection"); + ScriptComponent* selectionScript = + AddScript( + selectionObject, + "Gameplay", + "ManagedFeaturePassOrderRuntimeSelectionProbe"); + ASSERT_NE(selectionScript, nullptr); + GameObject* observationObject = + runtimeScene->CreateGameObject( + "ManagedFeaturePassOrderObservation"); + ScriptComponent* observationScript = + AddScript( + observationObject, + "Gameplay", + "ManagedFeaturePassOrderObservationProbe"); + ASSERT_NE(observationScript, nullptr); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = + XCEngine::Rendering::Pipelines::GetConfiguredManagedRenderPipelineAssetDescriptor(); + ASSERT_TRUE(descriptor.IsValid()); + ASSERT_NE(descriptor.managedAssetHandle, 0u); + EXPECT_EQ(descriptor.assemblyName, "GameScripts"); + EXPECT_EQ(descriptor.namespaceName, "Gameplay"); + EXPECT_EQ(descriptor.className, "ManagedFeaturePassOrderProbeAsset"); + + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + std::unique_ptr recorder = + assetRuntime->CreateStageRecorder(); + ASSERT_NE(recorder, nullptr); + + const XCEngine::Rendering::RenderContext renderContext = {}; + ASSERT_TRUE(recorder->Initialize(renderContext)); + ASSERT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + + XCEngine::Rendering::RenderGraph graph; + XCEngine::Rendering::RenderGraphBuilder graphBuilder(graph); + XCEngine::Rendering::RenderGraphTextureDesc colorDesc = {}; + colorDesc.width = 64u; + colorDesc.height = 64u; + colorDesc.format = + static_cast( + XCEngine::RHI::Format::R8G8B8A8_UNorm); + XCEngine::Rendering::RenderGraphTextureDesc depthDesc = colorDesc; + depthDesc.format = + static_cast( + XCEngine::RHI::Format::D32_Float); + const XCEngine::Rendering::RenderGraphTextureHandle colorTarget = + graphBuilder.CreateTransientTexture( + "ManagedFeaturePassOrderColor", + colorDesc); + const XCEngine::Rendering::RenderGraphTextureHandle depthTarget = + graphBuilder.CreateTransientTexture( + "ManagedFeaturePassOrderDepth", + depthDesc); + + const XCEngine::Rendering::RenderSceneData sceneData = {}; + const XCEngine::Rendering::RenderSurface surface(64u, 64u); + bool executionSucceeded = true; + XCEngine::Rendering::RenderGraphBlackboard blackboard = {}; + const XCEngine::Rendering::RenderPipelineStageRenderGraphContext graphContext = { + graphBuilder, + "ManagedFeaturePassOrder", + XCEngine::Rendering::CameraFrameStage::MainScene, + renderContext, + sceneData, + surface, + nullptr, + nullptr, + XCEngine::RHI::ResourceStates::Common, + {}, + { colorTarget }, + depthTarget, + {}, + &executionSucceeded, + &blackboard + }; + + EXPECT_TRUE(recorder->RecordStageRenderGraph(graphContext)); + + engine->OnUpdate(0.016f); + EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError(); + + std::string observedOrder; + EXPECT_TRUE(runtime->TryGetFieldValue( + observationScript, + "ObservedOrder", + observedOrder)); + EXPECT_EQ(observedOrder, "Builtin>CustomA>CustomB"); + + recorder->Shutdown(); +} + TEST_F( MonoScriptRuntimeTest, ManagedStageRecorderRecordsShaderVectorPostProcessThroughScriptableRenderContext) { diff --git a/tests/scripting/test_project_script_assembly.cpp b/tests/scripting/test_project_script_assembly.cpp index b97a7f7e..f78eff9a 100644 --- a/tests/scripting/test_project_script_assembly.cpp +++ b/tests/scripting/test_project_script_assembly.cpp @@ -1,5 +1,11 @@ #include +#include +#include +#include +#include +#include +#include #include #include @@ -146,4 +152,111 @@ TEST_F(ProjectScriptAssemblyTest, DiscoversProjectAssetMonoBehaviourClassesAndFi EXPECT_FLOAT_EQ(std::get(defaultValues[2].value), 2.5f); } +TEST_F(ProjectScriptAssemblyTest, DiscoversProjectAssetRenderPipelineAssetClasses) { + std::vector classes; + ASSERT_TRUE(runtime->TryGetAvailableRenderPipelineAssetClasses(classes)); + + EXPECT_NE( + std::find( + classes.begin(), + classes.end(), + ScriptClassDescriptor{ + "GameScripts", + "ProjectScripts", + "ProjectUniversalFeaturePipelineAsset"}), + classes.end()); +} + +TEST_F( + ProjectScriptAssemblyTest, + CreatesProjectAssetUniversalFeatureRuntimeThroughManagedBridge) { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = { + "GameScripts", + "ProjectScripts", + "ProjectUniversalFeaturePipelineAsset" + }; + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + EXPECT_NE(assetRuntime->GetPipelineRendererAsset(), nullptr); + + std::unique_ptr recorder = + assetRuntime->CreateStageRecorder(); + ASSERT_NE(recorder, nullptr); + + const XCEngine::Rendering::RenderContext renderContext = {}; + ASSERT_TRUE(recorder->Initialize(renderContext)); + EXPECT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + EXPECT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::PostProcess)); + + XCEngine::Rendering::RenderGraph graph; + XCEngine::Rendering::RenderGraphBuilder graphBuilder(graph); + XCEngine::Rendering::RenderGraphTextureDesc colorDesc = {}; + colorDesc.width = 64u; + colorDesc.height = 64u; + colorDesc.format = + static_cast( + XCEngine::RHI::Format::R8G8B8A8_UNorm); + const XCEngine::Rendering::RenderGraphTextureHandle sourceColor = + graphBuilder.ImportTexture( + "ProjectManagedPostProcessSource", + colorDesc, + reinterpret_cast(901), + {}); + const XCEngine::Rendering::RenderGraphTextureHandle outputColor = + graphBuilder.CreateTransientTexture( + "ProjectManagedPostProcessOutput", + colorDesc); + + const XCEngine::Rendering::RenderSceneData sceneData = {}; + const XCEngine::Rendering::RenderSurface surface(64u, 64u); + bool executionSucceeded = true; + XCEngine::Rendering::RenderGraphBlackboard blackboard = {}; + XCEngine::Rendering::EmplaceCameraFrameRenderGraphFrameData(blackboard) + .resources.mainScene.color = sourceColor; + const XCEngine::Rendering::RenderPipelineStageRenderGraphContext graphContext = { + graphBuilder, + "ProjectManagedPostProcess", + XCEngine::Rendering::CameraFrameStage::PostProcess, + renderContext, + sceneData, + surface, + nullptr, + nullptr, + XCEngine::RHI::ResourceStates::Common, + {}, + { outputColor }, + {}, + {}, + &executionSucceeded, + &blackboard + }; + + EXPECT_TRUE(recorder->RecordStageRenderGraph(graphContext)); + + XCEngine::Rendering::CompiledRenderGraph compiledGraph = {}; + XCEngine::Containers::String errorMessage; + ASSERT_TRUE( + XCEngine::Rendering::RenderGraphCompiler::Compile( + graph, + compiledGraph, + &errorMessage)) + << errorMessage.CStr(); + ASSERT_EQ(compiledGraph.GetPassCount(), 1u); + EXPECT_STREQ( + compiledGraph.GetPassName(0).CStr(), + "ProjectManagedPostProcess"); + + recorder->Shutdown(); +} + } // namespace