From f4fb859972f28e442d3dde3d318430dfb3625c4e Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 17 Apr 2026 21:38:22 +0800 Subject: [PATCH] feat(scripting): add managed SRP runtime bridge --- docs/plan/SRP_Runtime_v1_2026-04-17.md | 306 ++++++++++++ .../Scripting/Mono/MonoScriptRuntime.h | 46 +- .../ScriptableRenderPipelineHost.cpp | 4 +- .../src/Scripting/Mono/MonoScriptRuntime.cpp | 471 +++++++++++++++++- managed/GameScripts/RenderPipelineApiProbe.cs | 31 ++ .../unit/test_camera_scene_renderer.cpp | 54 ++ 6 files changed, 908 insertions(+), 4 deletions(-) create mode 100644 docs/plan/SRP_Runtime_v1_2026-04-17.md diff --git a/docs/plan/SRP_Runtime_v1_2026-04-17.md b/docs/plan/SRP_Runtime_v1_2026-04-17.md new file mode 100644 index 00000000..91e7e7f4 --- /dev/null +++ b/docs/plan/SRP_Runtime_v1_2026-04-17.md @@ -0,0 +1,306 @@ +# SRP Runtime v1 实施计划 2026-04-17 + +## 1. 结论 + +现在**可以正式进入 SRP 主线**了。 + +但这里的“开始搭 SRP”,准确含义不是: + +1. 直接开做 `URP` 包层。 +2. 直接做延迟渲染。 +3. 直接把阴影、高斯、体积、后处理全部搬到 C#。 + +本阶段正确目标是: + +`Native RenderGraph / Planning / Execution` +`-> Managed SRP Runtime v1` +`-> Managed Forward Pipeline v1` +`-> 之后再开 URP-like Package` + +也就是说,下一阶段确实是搭 SRP,但搭的是 **SRP Runtime v1**,不是直接搭“最终版 URP”。 + +--- + +## 2. 为什么现在可以开始做 SRP + +当前 native 渲染层已经具备进入 SRP 主线的前提: + +1. `RenderGraph` 已经可用,不是空壳。 +2. `CameraFramePlan` 已经把每相机渲染组织稳定下来。 +3. `CameraFrameStage` 和 stage dispatch 已经把主链拆清楚了。 +4. `BuiltinForwardPipeline` 已经能把 `MainScene` 录进 render graph。 +5. `ScriptableRenderPipelineHost` 已经提供了 `stage recorder + fallback renderer` 的宿主接缝。 +6. 默认 pipeline 工厂已经能从 `managed or default asset` 入口创建渲染管线。 + +所以当前真正缺的已经不是 native 渲染骨架,而是: + +1. managed pipeline 在 runtime 里真实存在。 +2. native 能持有并调用 managed pipeline。 +3. managed 层拿到最小可用的渲染上下文。 + +换句话说,**native 这一段已经够了,下一刀就该切 SRP runtime**。 + +--- + +## 3. 本阶段目标 + +本阶段收口目标只有四个: + +1. native 能创建并持有真实 managed `ScriptableRenderPipelineAsset` / `ScriptableRenderPipeline` 实例。 +2. `ScriptableRenderPipelineHost` 能稳定回调到 managed pipeline 参与 `MainScene` stage graph 录制。 +3. managed 拿到一个最小可用的 `ScriptableRenderContext v1`。 +4. 跑通一条最小 managed forward pipeline 主链,同时保留 builtin forward fallback。 + +如果这四件事没有形成闭环,就不算进入 SRP 主线。 + +--- + +## 4. 本阶段明确不做的事 + +这一阶段**明确不做**下面这些内容: + +1. 不开 `URP-like package`。 +2. 不做 deferred pipeline。 +3. 不把阴影、后处理、高斯、体积的组织权全部搬到 C#。 +4. 不直接对外暴露原始 `RHI` 或原始 native internal type。 +5. 不追求第一版就完全对齐 Unity 的 `ScriptableRenderContext` 和 `CommandBuffer`。 + +原因很简单: + +当前根问题不是“缺某个 renderer feature”,而是“managed pipeline 运行时还没真正成立”。 + +--- + +## 5. 本阶段的正确边界 + +### 留在 C++ 的 + +1. `RHI` +2. `RenderGraph` +3. frame planning / stage dispatch / graph compile / execute +4. scene extraction / culling / frame data +5. builtin renderer 的底层执行实现 + +### 开始上移到 managed 的 + +1. pipeline asset 的实例化和生命周期 +2. pipeline 的主场景录制组织 +3. 受控的 render context 封装 +4. 后续 `URP-like` 包层需要依赖的 SRP runtime 接缝 + +本阶段的核心原则是: + +> C++ 继续负责渲染内核,C# 开始接管渲染组织。 + +--- + +## 6. 分阶段实施顺序 + +### Phase A: 建立通用 managed object runtime + +目标: + +1. 在 `MonoScriptRuntime` 中补齐“任意 managed 对象”的创建、持有、释放能力。 +2. 不再只围绕 `ScriptComponent`、`GameObject`、`Component` wrapper 打转。 +3. 能按类型描述创建 asset 实例和 pipeline 实例。 +4. 能稳定保存 `GCHandle` 或等价持有关系。 +5. 能稳定回调实例方法并正确处理异常。 + +这一阶段是整个 SRP runtime 的硬前提。 + +如果这一步不成立,后面一切 SRP API 都只是空壳。 + +### Phase B: 把 render pipeline asset 从“类型描述”升级成“真实 runtime 对象” + +当前 `GraphicsSettings.renderPipelineAssetType` 的语义还是: + +`把一个 C# 类型名写给 native` + +这一阶段需要升级成: + +1. native 能根据类型创建真实 managed asset 实例。 +2. managed asset 能创建真实 managed pipeline 实例。 +3. native 能持有 asset / pipeline 生命周期。 +4. `ManagedScriptableRenderPipelineAsset` 不再只是 descriptor-only 包装。 + +第一版允许继续用 `Type` 作为 bootstrap 入口,但 native 内部不能再停留在 descriptor 模型。 + +### Phase C: 实现真实的 managed render pipeline bridge + +目标: + +1. `ManagedRenderPipelineBridge` 由 `MonoScriptRuntime` 提供真实实现。 +2. `ManagedScriptableRenderPipelineAsset` 创建的 host 能绑定到真实 managed pipeline。 +3. `ScriptableRenderPipelineHost` 能调用 managed pipeline 的 stage 录制方法。 +4. bridge 不再只是测试用 mock seam。 + +这一阶段完成后,managed pipeline 才算真正“活过来”。 + +### Phase D: 定义 `ScriptableRenderContext v1` + +第一版不要追求大而全。 + +只需要提供最小可用能力: + +1. 查询当前 camera frame / stage 基本信息。 +2. 参与 `MainScene` render graph 录制。 +3. 调用 native scene renderer 绘制 `Opaque / Skybox / Transparent`。 +4. 访问最基础的 source / target / blackboard 语义。 +5. 预留 fullscreen / blit 接口,但不强求一步到位。 + +这一版不直接暴露 raw `RenderGraphTextureHandle` 和 raw `RenderSurface` 给用户脚本。 + +### Phase E: 跑通最小 managed forward pipeline v1 + +目标: + +1. 用 C# 创建一个 managed pipeline。 +2. managed pipeline 能声明支持 `MainScene` stage graph。 +3. managed pipeline 能在 `MainScene` 调 native forward scene renderer。 +4. editor / runtime 在切到 managed pipeline 后仍能正常渲染。 +5. builtin forward 仍可作为 fallback 路径保留。 + +这一阶段完成后,才算正式进入 “SRP 已经开始工作” 的状态。 + +--- + +## 7. 第一批要改的文件 + +### Native 侧核心落点 + +1. `engine/src/Scripting/Mono/MonoScriptRuntime.cpp` +2. `engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h` +3. `engine/src/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.cpp` +4. `engine/include/XCEngine/Rendering/Pipelines/ScriptableRenderPipelineHost.h` +5. `engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp` +6. `engine/src/Rendering/Internal/RenderPipelineFactory.cpp` + +### Managed 侧核心落点 + +1. `managed/XCEngine.ScriptCore/InternalCalls.cs` +2. `managed/XCEngine.ScriptCore/GraphicsSettings.cs` +3. `managed/XCEngine.ScriptCore/ScriptableRenderPipelineAsset.cs` +4. `managed/XCEngine.ScriptCore/ScriptableRenderPipeline.cs` +5. 新增 `managed/XCEngine.ScriptCore/ScriptableRenderContext.cs` +6. 可能新增最小 `managed/XCEngine.ScriptCore/Rendering/*` 辅助类型 + +### 测试与验证 + +1. `managed/GameScripts/RenderPipelineApiProbe.cs` +2. 新增 native / managed bridge smoke tests +3. editor 冒烟场景验证 + +--- + +## 8. 代码层面的实施拆分 + +建议按下面的提交粒度推进,而不是一口气乱改: + +### Commit 1: managed object runtime 基础设施 + +完成后应该能: + +1. 按类型创建普通 managed 对象。 +2. 持有并释放对象句柄。 +3. 回调实例方法。 + +### Commit 2: managed pipeline asset / pipeline 生命周期 + +完成后应该能: + +1. 从 `GraphicsSettings.renderPipelineAssetType` 生成真实 asset 实例。 +2. 从 asset 生成真实 pipeline 实例。 +3. 由 native 负责持有这两个对象。 + +### Commit 3: host 与 managed bridge 接通 + +完成后应该能: + +1. `ScriptableRenderPipelineHost` 绑定真实 managed stage recorder。 +2. `MainScene` 录制可回调到 managed pipeline。 + +### Commit 4: `ScriptableRenderContext v1` + +完成后应该能: + +1. managed 侧拿到最小上下文。 +2. managed 能调用 native scene renderer 画主场景阶段。 + +### Commit 5: managed forward pipeline v1 smoke path + +完成后应该能: + +1. 用 managed pipeline 跑通主场景。 +2. 与 builtin forward fallback 共存。 + +--- + +## 9. 关键风险 + +### 风险 1: Mono 生命周期和 GC handle 泄漏 + +这是第一风险。 + +如果 object runtime 持有策略不稳,后面渲染管线切换、domain reload、editor 热切换都会出问题。 + +### 风险 2: 把 host 做成“全局单例桥” + +如果 bridge 继续停留在粗糙全局状态,后面多实例、多场景、重建 pipeline 都会很难收口。 + +### 风险 3: 过早暴露原始 native 类型 + +如果第一版就把 raw graph/resource handle 暴露给 managed,API 很快会固化,后面很难收拾。 + +### 风险 4: builtin forward 与 managed forward 边界不清 + +如果 managed 直接依赖 `BuiltinForwardPipeline` 私有实现细节,后面就不是 SRP,而是“脚本包一层 builtin forward”。 + +--- + +## 10. 阶段验收标准 + +本阶段收口时,必须同时满足下面这些条件: + +1. native 里存在真实的 managed pipeline bridge,不再只是 mock seam。 +2. `GraphicsSettings` 能驱动创建真实 managed asset / pipeline 实例。 +3. `ScriptableRenderPipelineHost` 能稳定回调 managed pipeline 的 `MainScene` 录制。 +4. managed 侧存在最小 `ScriptableRenderContext v1`。 +5. 一条最小 managed forward pipeline 能真实跑出主场景。 +6. builtin forward fallback 路径仍然可用。 +7. editor 编译通过,基础冒烟渲染不崩。 + +只要这 7 条有一条不成立,这一阶段就不能算收口。 + +--- + +## 11. 本阶段完成后,下一阶段才做什么 + +等 `SRP Runtime v1` 收口之后,下一阶段才适合开始: + +1. `URP-like asset` +2. `RendererData` +3. `RendererFeature` +4. `RenderPassEvent` +5. 阴影 / 后处理 / 高斯 / 体积 等组织权逐步上移 + +也就是说: + +**现在可以开始搭 SRP。** + +但准确说法应该是: + +**现在开始搭的是 `SRP Runtime v1`,不是直接搭最终版 URP。** + +--- + +## 12. 下一刀从哪里下 + +下一步第一刀就应该下在: + +`MonoScriptRuntime` +`-> 通用 managed object runtime` +`-> 真实 ManagedRenderPipelineBridge` + +这是整个 SRP 主线最根上的卡点。 + +如果这里不先解决,后面无论做多少 `ScriptableRenderPipelineAsset`、`ScriptableRenderContext`、`RendererFeature`,都只是外壳。 diff --git a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h index 16a9e0b9..369d9d59 100644 --- a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,8 @@ namespace XCEngine { namespace Scripting { class ScriptComponent; +class MonoManagedRenderPipelineBridge; +class MonoManagedRenderPipelineStageRecorder; class MonoScriptRuntime : public IScriptRuntime { public: @@ -156,6 +159,11 @@ private: uint32_t gcHandle = 0; }; + struct ExternalManagedObjectData { + MonoClass* monoClass = nullptr; + uint32_t gcHandle = 0; + }; + void ResolveSettings(); bool InitializeRootDomain(); bool CreateAppDomain(); @@ -195,11 +203,43 @@ private: bool TryReadFieldValue(MonoObject* instance, const FieldMetadata& fieldMetadata, ScriptFieldValue& outValue) const; void ClearManagedInstances(); + void ClearExternalManagedObjects(); void ClearClassCache(); - bool InvokeManagedMethod(MonoObject* instance, MonoMethod* method, void** args = nullptr); + bool ResolveManagedClass( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className, + MonoClass*& outClass) const; + bool CreateExternalManagedObject( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className, + uint32_t& outHandle); + bool CreateExternalManagedObject( + MonoClass* monoClass, + uint32_t& outHandle); + uint32_t RetainExternalManagedObject(MonoObject* instance); + void DestroyExternalManagedObject(uint32_t gcHandle); + MonoObject* GetManagedObject(uint32_t gcHandle) const; + MonoMethod* ResolveManagedMethod( + MonoClass* monoClass, + const char* methodName, + int parameterCount) const; + MonoMethod* ResolveManagedMethod( + MonoObject* instance, + const char* methodName, + int parameterCount) const; + bool InvokeManagedMethod( + MonoObject* instance, + MonoMethod* method, + void** args = nullptr, + MonoObject** outReturnValue = nullptr); void RecordException(MonoObject* exception); void SetError(const std::string& error); + friend class MonoManagedRenderPipelineBridge; + friend class MonoManagedRenderPipelineStageRecorder; + Settings m_settings; Components::Scene* m_activeScene = nullptr; @@ -215,6 +255,8 @@ private: MonoClass* m_behaviourClass = nullptr; MonoClass* m_gameObjectClass = nullptr; MonoClass* m_monoBehaviourClass = nullptr; + MonoClass* m_scriptableRenderPipelineAssetClass = nullptr; + MonoClass* m_scriptableRenderPipelineClass = nullptr; MonoClass* m_serializeFieldAttributeClass = nullptr; MonoMethod* m_gameObjectConstructor = nullptr; MonoClassField* m_managedGameObjectUUIDField = nullptr; @@ -223,6 +265,8 @@ private: std::unordered_map m_classes; std::unordered_map m_instances; + std::unordered_map m_externalManagedObjects; + std::shared_ptr m_runtimeLifetimeToken; }; } // namespace Scripting diff --git a/engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp b/engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp index e90a9445..f9c8c8ce 100644 --- a/engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp +++ b/engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp @@ -109,7 +109,9 @@ bool ScriptableRenderPipelineHost::RecordStageRenderGraph( if (m_stageRecorder != nullptr && m_stageRecorder->SupportsStageRenderGraph(context.stage)) { - return m_stageRecorder->RecordStageRenderGraph(context); + if (m_stageRecorder->RecordStageRenderGraph(context)) { + return true; + } } return m_pipelineRenderer != nullptr && diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index c6e0356a..0a37c3cd 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -148,6 +148,272 @@ MonoScriptRuntime* GetActiveMonoScriptRuntime() { return dynamic_cast(ScriptEngine::Get().GetRuntime()); } +bool TryUnboxManagedBoolean(MonoObject* boxedValue, bool& outValue) { + outValue = false; + if (!boxedValue) { + return false; + } + + void* const rawValue = mono_object_unbox(boxedValue); + if (!rawValue) { + return false; + } + + outValue = (*static_cast(rawValue)) != 0; + return true; +} + +} // namespace + +class MonoManagedRenderPipelineStageRecorder final + : public Rendering::RenderPipelineStageRecorder { +public: + MonoManagedRenderPipelineStageRecorder( + MonoScriptRuntime* runtime, + std::weak_ptr runtimeLifetime, + Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor) + : m_runtime(runtime) + , m_runtimeLifetime(std::move(runtimeLifetime)) + , m_descriptor(std::move(descriptor)) { + } + + ~MonoManagedRenderPipelineStageRecorder() override { + Shutdown(); + } + + bool Initialize(const Rendering::RenderContext&) override { + return EnsureManagedPipeline(); + } + + void Shutdown() override { + ReleaseManagedObjects(); + m_supportsStageMethod = nullptr; + m_recordStageMethod = nullptr; + m_pipelineCreationAttempted = false; + } + + bool SupportsStageRenderGraph(Rendering::CameraFrameStage stage) const override { + if (!Rendering::SupportsCameraFramePipelineGraphRecording(stage) || + !EnsureManagedPipeline()) { + return false; + } + + MonoObject* const pipelineObject = + m_runtime->GetManagedObject(m_pipelineHandle); + MonoMethod* const method = ResolveSupportsStageMethod(pipelineObject); + if (!pipelineObject || !method) { + return false; + } + + int32_t managedStage = static_cast(stage); + void* args[1] = { &managedStage }; + MonoObject* result = nullptr; + if (!m_runtime->InvokeManagedMethod( + pipelineObject, + method, + args, + &result)) { + return false; + } + + bool supportsStage = false; + return TryUnboxManagedBoolean(result, supportsStage) && supportsStage; + } + + bool RecordStageRenderGraph( + const Rendering::RenderPipelineStageRenderGraphContext& context) override { + if (!EnsureManagedPipeline()) { + return false; + } + + MonoObject* const pipelineObject = + m_runtime->GetManagedObject(m_pipelineHandle); + MonoMethod* const method = ResolveRecordStageMethod(pipelineObject); + if (!pipelineObject || !method) { + return false; + } + + int32_t managedStage = static_cast(context.stage); + void* args[1] = { &managedStage }; + MonoObject* result = nullptr; + if (!m_runtime->InvokeManagedMethod( + pipelineObject, + method, + args, + &result)) { + return false; + } + + bool recorded = false; + return TryUnboxManagedBoolean(result, recorded) && recorded; + } + +private: + bool IsRuntimeAlive() const { + return m_runtime != nullptr && + !m_runtimeLifetime.expired() && + m_runtime->m_initialized; + } + + bool EnsureManagedPipeline() const { + if (m_pipelineHandle != 0) { + return true; + } + if (m_pipelineCreationAttempted || !IsRuntimeAlive() || + !m_descriptor.IsValid()) { + return false; + } + + m_pipelineCreationAttempted = true; + + MonoClass* assetClass = nullptr; + if (!m_runtime->ResolveManagedClass( + m_descriptor.assemblyName, + m_descriptor.namespaceName, + m_descriptor.className, + assetClass) || + assetClass == nullptr) { + return false; + } + + if (!IsMonoClassOrSubclass( + assetClass, + m_runtime->m_scriptableRenderPipelineAssetClass)) { + m_runtime->SetError( + "Managed render pipeline asset must derive from ScriptableRenderPipelineAsset: " + + m_descriptor.GetFullName() + "."); + return false; + } + + uint32_t assetHandle = 0; + if (!m_runtime->CreateExternalManagedObject(assetClass, assetHandle) || + assetHandle == 0) { + return false; + } + + MonoObject* const assetObject = m_runtime->GetManagedObject(assetHandle); + MonoMethod* const createPipelineMethod = + m_runtime->ResolveManagedMethod(assetObject, "CreatePipeline", 0); + if (!assetObject || !createPipelineMethod) { + m_runtime->DestroyExternalManagedObject(assetHandle); + return false; + } + + MonoObject* pipelineObject = nullptr; + if (!m_runtime->InvokeManagedMethod( + assetObject, + createPipelineMethod, + nullptr, + &pipelineObject) || + pipelineObject == nullptr) { + m_runtime->DestroyExternalManagedObject(assetHandle); + return false; + } + + if (!IsMonoClassOrSubclass( + mono_object_get_class(pipelineObject), + m_runtime->m_scriptableRenderPipelineClass)) { + m_runtime->SetError( + "Managed render pipeline asset returned a non-ScriptableRenderPipeline instance: " + + m_descriptor.GetFullName() + "."); + m_runtime->DestroyExternalManagedObject(assetHandle); + return false; + } + + const uint32_t pipelineHandle = + m_runtime->RetainExternalManagedObject(pipelineObject); + if (pipelineHandle == 0) { + m_runtime->DestroyExternalManagedObject(assetHandle); + return false; + } + + m_assetHandle = assetHandle; + m_pipelineHandle = pipelineHandle; + return true; + } + + void ReleaseManagedObjects() { + if (!IsRuntimeAlive()) { + m_assetHandle = 0; + m_pipelineHandle = 0; + return; + } + + if (m_pipelineHandle != 0) { + m_runtime->DestroyExternalManagedObject(m_pipelineHandle); + m_pipelineHandle = 0; + } + if (m_assetHandle != 0) { + m_runtime->DestroyExternalManagedObject(m_assetHandle); + m_assetHandle = 0; + } + } + + MonoMethod* ResolveSupportsStageMethod(MonoObject* pipelineObject) const { + if (m_supportsStageMethod == nullptr) { + m_supportsStageMethod = + m_runtime->ResolveManagedMethod( + pipelineObject, + "SupportsStageRenderGraph", + 1); + } + + return m_supportsStageMethod; + } + + MonoMethod* ResolveRecordStageMethod(MonoObject* pipelineObject) const { + if (m_recordStageMethod == nullptr) { + m_recordStageMethod = + m_runtime->ResolveManagedMethod( + pipelineObject, + "RecordStageRenderGraph", + 1); + } + + return m_recordStageMethod; + } + + MonoScriptRuntime* m_runtime = nullptr; + std::weak_ptr m_runtimeLifetime; + Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor m_descriptor; + mutable uint32_t m_assetHandle = 0; + mutable uint32_t m_pipelineHandle = 0; + mutable MonoMethod* m_supportsStageMethod = nullptr; + mutable MonoMethod* m_recordStageMethod = nullptr; + mutable bool m_pipelineCreationAttempted = false; +}; + +class MonoManagedRenderPipelineBridge final + : public Rendering::Pipelines::ManagedRenderPipelineBridge { +public: + MonoManagedRenderPipelineBridge( + MonoScriptRuntime* runtime, + std::weak_ptr runtimeLifetime) + : m_runtime(runtime) + , m_runtimeLifetime(std::move(runtimeLifetime)) { + } + + std::unique_ptr CreateStageRecorder( + const Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor& descriptor) const override { + if (m_runtime == nullptr || + m_runtimeLifetime.expired() || + !descriptor.IsValid()) { + return nullptr; + } + + return std::make_unique( + m_runtime, + m_runtimeLifetime, + descriptor); + } + +private: + MonoScriptRuntime* m_runtime = nullptr; + std::weak_ptr m_runtimeLifetime; +}; + +namespace { + ManagedComponentTypeInfo ResolveManagedComponentTypeInfo(MonoClass* monoClass) { ManagedComponentTypeInfo typeInfo; if (!monoClass) { @@ -2003,12 +2269,19 @@ bool MonoScriptRuntime::Initialize() { return false; } + m_runtimeLifetimeToken = std::make_shared(0); m_initialized = true; + Rendering::Pipelines::SetManagedRenderPipelineBridge( + std::make_shared( + this, + m_runtimeLifetimeToken)); return true; } void MonoScriptRuntime::Shutdown() { + Rendering::Pipelines::ClearManagedRenderPipelineBridge(); ClearManagedInstances(); + ClearExternalManagedObjects(); ClearClassCache(); m_coreAssembly = nullptr; @@ -2019,6 +2292,8 @@ void MonoScriptRuntime::Shutdown() { m_behaviourClass = nullptr; m_gameObjectClass = nullptr; m_monoBehaviourClass = nullptr; + m_scriptableRenderPipelineAssetClass = nullptr; + m_scriptableRenderPipelineClass = nullptr; m_serializeFieldAttributeClass = nullptr; m_gameObjectConstructor = nullptr; m_managedGameObjectUUIDField = nullptr; @@ -2030,6 +2305,7 @@ void MonoScriptRuntime::Shutdown() { m_activeScene = nullptr; GetInternalCallScene() = nullptr; GetInternalCallDeltaTime() = 0.0f; + m_runtimeLifetimeToken.reset(); m_initialized = false; } @@ -2614,6 +2890,24 @@ bool MonoScriptRuntime::DiscoverScriptClasses() { return false; } + m_scriptableRenderPipelineAssetClass = mono_class_from_name( + m_coreImage, + m_settings.baseNamespace.c_str(), + "ScriptableRenderPipelineAsset"); + if (!m_scriptableRenderPipelineAssetClass) { + SetError("Failed to locate the managed ScriptableRenderPipelineAsset base type."); + return false; + } + + m_scriptableRenderPipelineClass = mono_class_from_name( + m_coreImage, + m_settings.baseNamespace.c_str(), + "ScriptableRenderPipeline"); + if (!m_scriptableRenderPipelineClass) { + SetError("Failed to locate the managed ScriptableRenderPipeline base type."); + return false; + } + m_serializeFieldAttributeClass = mono_class_from_name( m_coreImage, m_settings.baseNamespace.c_str(), @@ -2917,6 +3211,117 @@ const MonoScriptRuntime::ClassMetadata* MonoScriptRuntime::FindClassMetadata( return it != m_classes.end() ? &it->second : nullptr; } +bool MonoScriptRuntime::ResolveManagedClass( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className, + MonoClass*& outClass) const { + outClass = nullptr; + if (!m_initialized || assemblyName.empty() || className.empty()) { + return false; + } + + MonoImage* image = nullptr; + if (assemblyName == m_settings.coreAssemblyName) { + image = m_coreImage; + } else if (assemblyName == m_settings.appAssemblyName) { + image = m_appImage; + } + + if (!image) { + return false; + } + + SetCurrentDomain(); + outClass = mono_class_from_name( + image, + namespaceName.c_str(), + className.c_str()); + return outClass != nullptr; +} + +bool MonoScriptRuntime::CreateExternalManagedObject( + const std::string& assemblyName, + const std::string& namespaceName, + const std::string& className, + uint32_t& outHandle) { + outHandle = 0; + + MonoClass* monoClass = nullptr; + if (!ResolveManagedClass( + assemblyName, + namespaceName, + className, + monoClass)) { + SetError( + "Managed class was not found: " + + assemblyName + "|" + + BuildFullClassName(namespaceName, className)); + return false; + } + + return CreateExternalManagedObject(monoClass, outHandle); +} + +bool MonoScriptRuntime::CreateExternalManagedObject( + MonoClass* monoClass, + uint32_t& outHandle) { + outHandle = 0; + if (!m_initialized || !monoClass) { + return false; + } + + SetCurrentDomain(); + + MonoObject* const instance = mono_object_new(m_appDomain, monoClass); + if (!instance) { + SetError( + "Mono failed to allocate a managed object for " + + BuildFullClassName( + SafeString(mono_class_get_namespace(monoClass)), + SafeString(mono_class_get_name(monoClass))) + "."); + return false; + } + + mono_runtime_object_init(instance); + outHandle = RetainExternalManagedObject(instance); + return outHandle != 0; +} + +uint32_t MonoScriptRuntime::RetainExternalManagedObject(MonoObject* instance) { + if (!m_initialized || !instance) { + return 0; + } + + SetCurrentDomain(); + + const uint32_t gcHandle = mono_gchandle_new(instance, false); + if (gcHandle == 0) { + return 0; + } + + m_externalManagedObjects[gcHandle] = ExternalManagedObjectData{ + mono_object_get_class(instance), + gcHandle + }; + return gcHandle; +} + +void MonoScriptRuntime::DestroyExternalManagedObject(uint32_t gcHandle) { + if (gcHandle == 0) { + return; + } + + const auto it = m_externalManagedObjects.find(gcHandle); + if (it == m_externalManagedObjects.end()) { + return; + } + + SetCurrentDomain(); + mono_gchandle_free(gcHandle); + m_externalManagedObjects.erase(it); +} + MonoScriptRuntime::InstanceData* MonoScriptRuntime::FindInstance(const ScriptRuntimeContext& context) { const auto it = m_instances.find(InstanceKey{context.gameObjectUUID, context.scriptComponentUUID}); return it != m_instances.end() ? &it->second : nullptr; @@ -2948,6 +3353,43 @@ MonoObject* MonoScriptRuntime::GetManagedObject(const InstanceData& instanceData return mono_gchandle_get_target(instanceData.gcHandle); } +MonoObject* MonoScriptRuntime::GetManagedObject(uint32_t gcHandle) const { + if (gcHandle == 0 || + m_externalManagedObjects.find(gcHandle) == m_externalManagedObjects.end()) { + return nullptr; + } + + SetCurrentDomain(); + return mono_gchandle_get_target(gcHandle); +} + +MonoMethod* MonoScriptRuntime::ResolveManagedMethod( + MonoClass* monoClass, + const char* methodName, + int parameterCount) const { + if (!monoClass || !methodName) { + return nullptr; + } + + SetCurrentDomain(); + return mono_class_get_method_from_name( + monoClass, + methodName, + parameterCount); +} + +MonoMethod* MonoScriptRuntime::ResolveManagedMethod( + MonoObject* instance, + const char* methodName, + int parameterCount) const { + return instance != nullptr + ? ResolveManagedMethod( + mono_object_get_class(instance), + methodName, + parameterCount) + : nullptr; +} + bool MonoScriptRuntime::ApplyContextFields(const ScriptRuntimeContext& context, MonoObject* instance) { if (!instance) { return false; @@ -3552,24 +3994,49 @@ void MonoScriptRuntime::ClearManagedInstances() { m_instances.clear(); } +void MonoScriptRuntime::ClearExternalManagedObjects() { + for (auto& [gcHandle, objectData] : m_externalManagedObjects) { + (void)objectData; + if (gcHandle != 0) { + mono_gchandle_free(gcHandle); + } + } + + m_externalManagedObjects.clear(); +} + void MonoScriptRuntime::ClearClassCache() { m_classes.clear(); } -bool MonoScriptRuntime::InvokeManagedMethod(MonoObject* instance, MonoMethod* method, void** args) { +bool MonoScriptRuntime::InvokeManagedMethod( + MonoObject* instance, + MonoMethod* method, + void** args, + MonoObject** outReturnValue) { if (!instance || !method) { + if (outReturnValue) { + *outReturnValue = nullptr; + } return false; } SetCurrentDomain(); MonoObject* exception = nullptr; - mono_runtime_invoke(method, instance, args, &exception); + MonoObject* returnValue = mono_runtime_invoke(method, instance, args, &exception); if (exception) { + if (outReturnValue) { + *outReturnValue = nullptr; + } RecordException(exception); return false; } + if (outReturnValue) { + *outReturnValue = returnValue; + } + return true; } diff --git a/managed/GameScripts/RenderPipelineApiProbe.cs b/managed/GameScripts/RenderPipelineApiProbe.cs index b3ae5001..f33d323b 100644 --- a/managed/GameScripts/RenderPipelineApiProbe.cs +++ b/managed/GameScripts/RenderPipelineApiProbe.cs @@ -10,6 +10,37 @@ namespace Gameplay { } + public sealed class ManagedRenderPipelineProbeAsset : ScriptableRenderPipelineAsset + { + public static int CreatePipelineCallCount; + + protected override ScriptableRenderPipeline CreatePipeline() + { + CreatePipelineCallCount++; + return new ManagedRenderPipelineProbe(); + } + } + + public sealed class ManagedRenderPipelineProbe : ScriptableRenderPipeline + { + public static int SupportsStageCallCount; + public static int RecordStageCallCount; + + protected override bool SupportsStageRenderGraph( + CameraFrameStage stage) + { + SupportsStageCallCount++; + return stage == CameraFrameStage.MainScene; + } + + protected override bool RecordStageRenderGraph( + CameraFrameStage stage) + { + RecordStageCallCount++; + return false; + } + } + public sealed class RenderPipelineApiProbe : MonoBehaviour { public bool InitialTypeWasNull; diff --git a/tests/Rendering/unit/test_camera_scene_renderer.cpp b/tests/Rendering/unit/test_camera_scene_renderer.cpp index 478cd818..d43e0373 100644 --- a/tests/Rendering/unit/test_camera_scene_renderer.cpp +++ b/tests/Rendering/unit/test_camera_scene_renderer.cpp @@ -4253,6 +4253,60 @@ TEST(ScriptableRenderPipelineHost_Test, PrefersStageRecorderBeforeFallbackRender EXPECT_EQ(replacementRecorderState->shutdownCalls, 1); } +TEST( + ScriptableRenderPipelineHost_Test, + FallsBackToRendererWhenStageRecorderDeclinesRecording) { + auto rendererState = std::make_shared(); + auto recorderState = std::make_shared(); + + rendererState->supportsMainSceneRenderGraph = true; + recorderState->supportsMainSceneRenderGraph = true; + recorderState->recordMainSceneResult = false; + + Pipelines::ScriptableRenderPipelineHost host( + std::make_unique(rendererState)); + host.SetStageRecorder( + std::make_unique(recorderState)); + + Scene scene("ScriptableRenderPipelineFallbackScene"); + GameObject* cameraObject = scene.CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + camera->SetPrimary(true); + camera->SetDepth(1.0f); + + const RenderContext context = CreateValidContext(); + const RenderSurface surface(800, 600); + RenderSceneData sceneData = + CreateSceneDataForCamera(scene, *camera, surface); + RenderGraph graph = {}; + RenderGraphBuilder graphBuilder(graph); + RenderGraphBlackboard blackboard = {}; + bool executionSucceeded = true; + const RenderPipelineStageRenderGraphContext graphContext = { + graphBuilder, + "MainScene", + CameraFrameStage::MainScene, + context, + sceneData, + surface, + nullptr, + nullptr, + XCEngine::RHI::ResourceStates::Common, + {}, + {}, + {}, + &executionSucceeded, + &blackboard + }; + + EXPECT_TRUE(host.SupportsStageRenderGraph(CameraFrameStage::MainScene)); + EXPECT_TRUE(host.RecordStageRenderGraph(graphContext)); + EXPECT_EQ(recorderState->recordMainSceneCalls, 1); + EXPECT_EQ(rendererState->recordMainSceneCalls, 1); + EXPECT_TRUE(recorderState->lastReceivedRenderGraphBlackboard); + EXPECT_TRUE(rendererState->lastReceivedRenderGraphBlackboard); +} + TEST(ScriptableRenderPipelineHost_Test, DirectHostDoesNotInstallBuiltinStandaloneStagePasses) { auto rendererState = std::make_shared(); Pipelines::ScriptableRenderPipelineHost host(