From 048ca7b36238c940d0579cd9ee917f9e664eed5c Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Mon, 27 Apr 2026 16:11:54 +0800 Subject: [PATCH] Bridge managed render funcs to command buffers --- ...ule_overview_rendergraph_srp_2026-04-17.md | 8 +- engine/include/XCEngine/Rendering/AGENTS.md | 14 +- .../Scripting/Mono/MonoScriptRuntime.h | 5 + .../src/Scripting/Mono/MonoScriptRuntime.cpp | 359 +++++++++++++++++- managed/GameScripts/RenderPipelineApiProbe.cs | 103 +++++ managed/XCEngine.ScriptCore/InternalCalls.cs | 9 +- .../Rendering/Core/CommandBuffer.cs | 25 ++ .../Graph/RenderGraphRasterPassBuilder.cs | 28 +- tests/fixtures/RenderTestRhiStubs.h | 11 +- tests/scripting/test_mono_script_runtime.cpp | 162 ++++++++ 10 files changed, 697 insertions(+), 27 deletions(-) diff --git a/docs/reference/rendering_module_overview_rendergraph_srp_2026-04-17.md b/docs/reference/rendering_module_overview_rendergraph_srp_2026-04-17.md index c2c12119..6aa3786c 100644 --- a/docs/reference/rendering_module_overview_rendergraph_srp_2026-04-17.md +++ b/docs/reference/rendering_module_overview_rendergraph_srp_2026-04-17.md @@ -2349,7 +2349,7 @@ std::unique_ptr ManagedScriptableRenderPipelineAsset::CreatePipe 应该给 managed 提供受控的 native 能力包装,比如: -1. 录制一个 raster/compute graph pass。 +1. 录制一个 raster/compute graph pass,并让 public `SetRenderFunc` 在 RenderGraph 执行期运行。 2. 访问当前 camera frame 的 source / target / blackboard。 3. 调用 native scene renderer 画 `Opaque / Skybox / Transparent`。 4. 调用 fullscreen / blit。 @@ -2359,6 +2359,12 @@ std::unique_ptr ManagedScriptableRenderPipelineAsset::CreatePipe > C# 负责组织,C++ 负责底层执行内核。 +2026-04-27 进展:`RenderGraphRasterPassBuilder.SetRenderFunc` 已不再在 recording/`Commit()` 时同步调用 +managed delegate。Mono bridge 现在会 retain render func delegate,在 native RenderGraph pass 执行时创建 +pass-scoped managed `CommandBuffer`,并把受支持的 command stream flush 到当前 `RHICommandList`。当前完成的 +第一条命令是 `CommandBuffer.ClearRenderTarget(Color)`;后续新增 command buffer API 必须沿用同一条链路补 +native command stream、RHI flush 和 scripting bridge executor 测试,不能只暴露 managed 空壳。 + ### 阶段 3:做一个最小可用的 managed forward pipeline 目标不是一步到位做 URP。 diff --git a/engine/include/XCEngine/Rendering/AGENTS.md b/engine/include/XCEngine/Rendering/AGENTS.md index cf117be8..e442749b 100644 --- a/engine/include/XCEngine/Rendering/AGENTS.md +++ b/engine/include/XCEngine/Rendering/AGENTS.md @@ -246,9 +246,10 @@ Managed RenderGraph 目前有意保持较小,但应维持 Unity 的现代形 - Public passes 应声明 reads、bindings、color attachments、depth attachment 和 render func,然后 `Commit`。 - Internal fullscreen helpers,例如 color scale、shader vector 和 final color,是 URP implementation details。 不要把它们作为 public `ScriptableRenderContext` shortcuts 暴露。 -- Managed `SetRenderFunc` 还不是完全 bridged native command buffer。它在 recording 时调用 managed render - func,并把声明的 IO 记录进 native graph execution。不要添加依赖 managed commands 之后在 native command - list 上运行的行为。 +- Managed `SetRenderFunc` 是 deferred RenderGraph execution callback。Mono bridge 必须 retain delegate, + 在 native graph pass 执行时创建 pass-scoped `CommandBuffer`,并把已支持的 managed command stream flush 到 + 当前 `RHICommandList`。当前公开且已测试的命令子集是 `CommandBuffer.ClearRenderTarget(Color)`,写向 pass + 的 primary color attachment;没有 native command stream 和 executor 测试的命令不要暴露成 public API。 - Public RenderGraph frame data 由同一个 renderer stage 内记录的所有 passes 共享。保持 `ContextContainer` 作为 stage frame data,而不是 pass-local scratch data。 - Fullscreen internal passes 需要有效 `sourceColorTexture` 和 `primaryColorTarget`。Multi-pass fullscreen chains @@ -316,7 +317,9 @@ Scene data 每个 camera frame 提取一次,然后由 pipeline 调整。 - Managed SRP 仍是 SRP v1 surface。Main scene drawing 通过 managed calls 记录,但由 native scene draw backends 执行。 -- Managed `CommandBuffer` 和 public `SetRenderFunc` 还不会执行 deferred native command lists。 +- Managed `CommandBuffer` 已能随 public `SetRenderFunc` 延迟到 RenderGraph 执行期运行,但当前只收口了 + `ClearRenderTarget(Color)` 这一条受测命令。它不是完整 Unity `CommandBuffer`;新增命令必须一起补 + native command stream、RHI flush 和 scripting bridge tests。 - Managed RenderGraph 当前暴露 raster authoring。Native graph 有 compute pass support,但 managed compute authoring 还未公开。 - `UniversalPostProcessBlock` 仍保留 post-process source promotion helper;实际 post-process stage 由 @@ -344,6 +347,9 @@ Scene data 每个 camera frame 提取一次,然后由 pipeline 调整。 support probe 和 recording 各自重建 pass queue 的重复事实源。 - Public managed RenderGraph raster authoring 已存在;internal fullscreen kernels 仍是 URP implementation details。 +- Public managed `SetRenderFunc` 已从 recording-time 调用改为 RenderGraph execution-time 调用; + native 通过 retained managed delegate 和 pass-scoped `CommandBuffer` bridge 执行已支持的 command stream, + 当前受测命令为 `CommandBuffer.ClearRenderTarget(Color)`。 - Public `ScriptableRenderPass.RecordRenderGraph(RenderGraph, ContextContainer)` 通过 `RenderingData`、 `CameraData`、`LightingData`、`ShadowData`、`EnvironmentData`、`FinalColorData` 和 `StageColorData` 接收真实 URP frame data。 diff --git a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h index c435dd1c..f670b382 100644 --- a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h @@ -97,6 +97,11 @@ public: MonoObject* GetExternalManagedObject(uint32_t gcHandle) const; uint32_t RetainExternalManagedObjectReference(MonoObject* managedObject); void ReleaseExternalManagedObject(uint32_t gcHandle); + void ReleaseRetainedManagedRenderFunc(uint32_t gcHandle); + bool InvokeManagedRenderGraphRasterFunc( + uint32_t gcHandle, + const char* passName, + uint64_t commandBufferHandle); bool IsScriptableRenderPipelineAssetObject(MonoObject* managedObject) const; bool TryEnsureManagedRenderPipelineAssetHandle( Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor& ioDescriptor); diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index 2afbb982..6f153546 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -31,6 +31,7 @@ #include "Rendering/RenderPipelineStageGraphContract.h" #include "Resources/BuiltinResources.h" #include "Core/Asset/ResourceManager.h" +#include "RHI/RHICommandList.h" #include "Scene/Scene.h" #include "Scripting/ScriptComponent.h" #include "Scripting/ScriptEngine.h" @@ -63,6 +64,8 @@ namespace { constexpr const char* kManagedRenderingNamespace = "XCEngine.Rendering"; +MonoScriptRuntime* GetActiveMonoScriptRuntime(); + struct MonoRootState { MonoDomain* rootDomain = nullptr; bool initialized = false; @@ -117,6 +120,7 @@ struct ManagedScriptableRenderContextState { Containers::String passName = {}; Rendering::FullscreenPassDesc passDesc = {}; bool usesManagedRenderFuncExecution = false; + uint32_t managedRenderFuncHandle = 0u; Rendering::RenderGraphTextureHandle sourceColorTexture = {}; std::vector readTextures = {}; std::vector readDepthTextures = {}; @@ -163,6 +167,21 @@ struct ManagedScriptableRenderPipelinePlanningContextState { Rendering::CameraFramePlan* plan = nullptr; }; +struct ManagedCommandBufferCommand { + enum class Kind { + ClearRenderTarget + }; + + Kind kind = Kind::ClearRenderTarget; + float color[4] = {}; +}; + +struct ManagedCommandBufferState { + uint64_t handle = 0; + const Rendering::RenderPassContext* passContext = nullptr; + std::vector commands = {}; +}; + uint64_t& GetManagedScriptableRenderContextNextHandle() { static uint64_t nextHandle = 1; return nextHandle; @@ -217,6 +236,108 @@ void UnregisterManagedScriptableRenderContextState(uint64_t handle) { GetManagedScriptableRenderContextRegistry().erase(handle); } +uint64_t& GetManagedCommandBufferNextHandle() { + static uint64_t nextHandle = 1; + return nextHandle; +} + +std::unordered_map& +GetManagedCommandBufferRegistry() { + static std::unordered_map registry; + return registry; +} + +uint64_t RegisterManagedCommandBufferState( + ManagedCommandBufferState& state) { + uint64_t handle = GetManagedCommandBufferNextHandle()++; + if (handle == 0u) { + handle = GetManagedCommandBufferNextHandle()++; + } + + state.handle = handle; + GetManagedCommandBufferRegistry()[handle] = &state; + return handle; +} + +ManagedCommandBufferState* FindManagedCommandBufferState( + uint64_t handle) { + if (handle == 0u) { + return nullptr; + } + + const auto it = GetManagedCommandBufferRegistry().find(handle); + return it != GetManagedCommandBufferRegistry().end() + ? it->second + : nullptr; +} + +void UnregisterManagedCommandBufferState(uint64_t handle) { + if (handle == 0u) { + return; + } + + GetManagedCommandBufferRegistry().erase(handle); +} + +void ReleaseManagedRenderFuncHandle(uint32_t& gcHandle) { + if (gcHandle == 0u) { + return; + } + + MonoScriptRuntime* const runtime = + GetActiveMonoScriptRuntime(); + if (runtime != nullptr) { + runtime->ReleaseExternalManagedObject(gcHandle); + } + gcHandle = 0u; +} + +void ReleaseManagedScriptableRenderContextRenderFuncHandles( + ManagedScriptableRenderContextState& state) { + for (auto& [rasterPassHandle, request] : + state.pendingRasterPassRequests) { + (void)rasterPassHandle; + ReleaseManagedRenderFuncHandle(request.managedRenderFuncHandle); + } + + for (ManagedScriptableRenderContextState::RasterPassRecordRequest& + request : state.rasterPassRequests) { + ReleaseManagedRenderFuncHandle(request.managedRenderFuncHandle); + } +} + +bool FlushManagedCommandBufferState( + const ManagedCommandBufferState& state) { + if (state.passContext == nullptr || + state.passContext->renderContext.commandList == nullptr) { + return false; + } + + RHI::RHICommandList* const commandList = + state.passContext->renderContext.commandList; + const std::vector& colorAttachments = + state.passContext->surface.GetColorAttachments(); + + for (const ManagedCommandBufferCommand& command : state.commands) { + switch (command.kind) { + case ManagedCommandBufferCommand::Kind::ClearRenderTarget: + if (colorAttachments.empty() || + colorAttachments[0] == nullptr) { + return false; + } + + commandList->ClearRenderTarget( + colorAttachments[0], + command.color); + break; + default: + return false; + } + } + + return true; +} + const Rendering::RenderCameraData& ResolveManagedScriptableRenderContextCameraData( const ManagedScriptableRenderContextState* state) { if (state != nullptr && @@ -491,9 +612,11 @@ bool RecordManagedRenderFuncRasterPass( std::vector textureBindings, Rendering::RenderGraphTextureHandle depthTarget, - const Containers::String& passName) { + const Containers::String& passName, + Rendering::RenderPassGraphBeginCallback executeCallback) { if (!ResolveManagedPrimaryColorTarget(colorTargets).IsValid() && - !depthTarget.IsValid()) { + !depthTarget.IsValid() || + !executeCallback) { return false; } @@ -526,9 +649,7 @@ bool RecordManagedRenderFuncRasterPass( return Rendering::RecordCallbackRasterRenderPass( renderGraphContext, io, - [](const Rendering::RenderPassContext&) { - return true; - }, + std::move(executeCallback), std::move(additionalReadTextures), std::move(additionalReadDepthTextures), std::move(textureBindings)); @@ -1964,20 +2085,91 @@ public: &result); UnregisterManagedScriptableRenderContextState(managedContextHandle); if (!invokeSucceeded) { + ReleaseManagedScriptableRenderContextRenderFuncHandles( + managedContextState); return false; } bool recorded = false; if (!(TryUnboxManagedBoolean(result, recorded) && recorded)) { + ReleaseManagedScriptableRenderContextRenderFuncHandles( + managedContextState); return false; } - return FlushManagedRasterPasses( - context, - managedContextState); + const bool flushed = + FlushManagedRasterPasses( + context, + managedContextState); + if (!flushed) { + ReleaseManagedScriptableRenderContextRenderFuncHandles( + managedContextState); + } + + return flushed; } private: + struct ManagedRenderFuncExecutionState { + std::shared_ptr + assetRuntime = nullptr; + uint32_t renderFuncHandle = 0u; + Containers::String passName = {}; + + ~ManagedRenderFuncExecutionState() { + ReleaseRenderFunc(); + } + + bool Execute(const Rendering::RenderPassContext& passContext) { + if (assetRuntime == nullptr || + !assetRuntime->IsRuntimeAlive() || + renderFuncHandle == 0u) { + return false; + } + + MonoScriptRuntime* const runtime = + assetRuntime->GetRuntime(); + if (runtime == nullptr) { + return false; + } + + ManagedCommandBufferState commandBufferState = {}; + commandBufferState.passContext = &passContext; + const uint64_t commandBufferHandle = + RegisterManagedCommandBufferState( + commandBufferState); + const bool invoked = + runtime->InvokeManagedRenderGraphRasterFunc( + renderFuncHandle, + passName.CStr(), + commandBufferHandle); + UnregisterManagedCommandBufferState( + commandBufferHandle); + + return invoked && + FlushManagedCommandBufferState( + commandBufferState); + } + + void ReleaseRenderFunc() { + if (renderFuncHandle == 0u) { + return; + } + + if (assetRuntime != nullptr && + assetRuntime->IsRuntimeAlive()) { + MonoScriptRuntime* const runtime = + assetRuntime->GetRuntime(); + if (runtime != nullptr) { + runtime->ReleaseRetainedManagedRenderFunc( + renderFuncHandle); + } + } + + renderFuncHandle = 0u; + } + }; + bool IsRuntimeAlive() const { return m_assetRuntime != nullptr && m_assetRuntime->IsRuntimeAlive() && @@ -2058,7 +2250,7 @@ private: bool FlushManagedRasterPasses( const Rendering::RenderPipelineStageRenderGraphContext& context, - const ManagedScriptableRenderContextState& managedContextState) { + ManagedScriptableRenderContextState& managedContextState) { if (managedContextState.rasterPassRequests.empty()) { return true; } @@ -2073,7 +2265,7 @@ private: for (size_t passIndex = 0u; passIndex < managedContextState.rasterPassRequests.size(); ++passIndex) { - const ManagedScriptableRenderContextState::RasterPassRecordRequest& + ManagedScriptableRenderContextState::RasterPassRecordRequest& request = managedContextState.rasterPassRequests[passIndex]; const Containers::String resolvedPassName = !request.passName.Empty() @@ -2084,6 +2276,25 @@ private: context.passName, passIndex); if (request.usesManagedRenderFuncExecution) { + const uint32_t renderFuncHandle = + std::exchange( + request.managedRenderFuncHandle, + 0u); + if (renderFuncHandle == 0u) { + return false; + } + + auto executionState = + std::make_shared(); + executionState->assetRuntime = m_assetRuntime; + executionState->renderFuncHandle = renderFuncHandle; + executionState->passName = resolvedPassName; + Rendering::RenderPassGraphBeginCallback executeCallback = + [executionState]( + const Rendering::RenderPassContext& passContext) { + return executionState != nullptr && + executionState->Execute(passContext); + }; if (!RecordManagedRenderFuncRasterPass( context, request.sourceColorTexture, @@ -2092,7 +2303,8 @@ private: request.readDepthTextures, request.textureBindings, request.depthTarget, - resolvedPassName)) { + resolvedPassName, + std::move(executeCallback))) { return false; } @@ -5106,6 +5318,8 @@ InternalCall_Rendering_ScriptableRenderContext_SetRasterPassColorScaleFullscreen return 0; } + ReleaseManagedRenderFuncHandle( + request->managedRenderFuncHandle); request->usesManagedRenderFuncExecution = false; request->passDesc = Rendering::FullscreenPassDesc::MakeColorScale( @@ -5140,6 +5354,8 @@ InternalCall_Rendering_ScriptableRenderContext_SetRasterPassShaderVectorFullscre return 0; } + ReleaseManagedRenderFuncHandle( + request->managedRenderFuncHandle); request->usesManagedRenderFuncExecution = false; request->passDesc = Rendering::FullscreenPassDesc::MakeShaderVector( @@ -5170,6 +5386,8 @@ InternalCall_Rendering_ScriptableRenderContext_SetRasterPassFinalColorFullscreen return 0; } + ReleaseManagedRenderFuncHandle( + request->managedRenderFuncHandle); request->usesManagedRenderFuncExecution = false; request->passDesc = Rendering::FullscreenPassDesc::MakeFinalColor( @@ -5183,7 +5401,8 @@ InternalCall_Rendering_ScriptableRenderContext_SetRasterPassFinalColorFullscreen mono_bool InternalCall_Rendering_ScriptableRenderContext_SetRasterPassManagedRenderFuncExecution( uint64_t nativeHandle, - uint64_t rasterPassHandle) { + uint64_t rasterPassHandle, + MonoObject* renderFunc) { ManagedScriptableRenderContextState* const state = FindManagedScriptableRenderContextState(nativeHandle); ManagedScriptableRenderContextState::RasterPassRecordRequest* @@ -5191,11 +5410,27 @@ InternalCall_Rendering_ScriptableRenderContext_SetRasterPassManagedRenderFuncExe FindPendingManagedRasterPassRecordRequest( state, rasterPassHandle); - if (request == nullptr) { + if (request == nullptr || + renderFunc == nullptr) { return 0; } + MonoScriptRuntime* const runtime = + GetActiveMonoScriptRuntime(); + if (runtime == nullptr) { + return 0; + } + + uint32_t renderFuncHandle = + runtime->RetainExternalManagedObjectReference(renderFunc); + if (renderFuncHandle == 0u) { + return 0; + } + + ReleaseManagedRenderFuncHandle( + request->managedRenderFuncHandle); request->usesManagedRenderFuncExecution = true; + request->managedRenderFuncHandle = renderFuncHandle; request->passDesc = {}; return 1; } @@ -5222,11 +5457,15 @@ mono_bool InternalCall_Rendering_ScriptableRenderContext_CommitRasterPass( state->pendingRasterPassRequests.erase(it); if (request.usesManagedRenderFuncExecution) { if (!HasManagedRasterWriteTarget(request)) { + ReleaseManagedRenderFuncHandle( + request.managedRenderFuncHandle); return 0; } } else if (!request.passDesc.IsValid() || !ResolveManagedPrimaryColorTarget(request.colorTargets) .IsValid()) { + ReleaseManagedRenderFuncHandle( + request.managedRenderFuncHandle); return 0; } @@ -5234,6 +5473,27 @@ mono_bool InternalCall_Rendering_ScriptableRenderContext_CommitRasterPass( return 1; } +mono_bool InternalCall_Rendering_CommandBuffer_ClearRenderTarget( + uint64_t nativeHandle, + XCEngine::Math::Color* color) { + ManagedCommandBufferState* const state = + FindManagedCommandBufferState(nativeHandle); + if (state == nullptr || + color == nullptr) { + return 0; + } + + ManagedCommandBufferCommand command = {}; + command.kind = + ManagedCommandBufferCommand::Kind::ClearRenderTarget; + command.color[0] = color->r; + command.color[1] = color->g; + command.color[2] = color->b; + command.color[3] = color->a; + state->commands.push_back(command); + return 1; +} + int32_t InternalCall_Rendering_ScriptableRenderContext_GetStageColorSource( uint64_t nativeHandle) { const ManagedScriptableRenderContextState* const state = @@ -6684,6 +6944,7 @@ void RegisterInternalCalls() { mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_SetRasterPassFinalColorFullscreenExecution", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_SetRasterPassFinalColorFullscreenExecution)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_SetRasterPassManagedRenderFuncExecution", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_SetRasterPassManagedRenderFuncExecution)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_CommitRasterPass", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_CommitRasterPass)); + mono_add_internal_call("XCEngine.InternalCalls::Rendering_CommandBuffer_ClearRenderTarget", reinterpret_cast(&InternalCall_Rendering_CommandBuffer_ClearRenderTarget)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_GetStageColorSource", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_GetStageColorSource)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_GetStageUsesGraphManagedOutputColor", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_GetStageUsesGraphManagedOutputColor)); mono_add_internal_call("XCEngine.InternalCalls::Rendering_ScriptableRenderContext_GetMainDirectionalShadowEnabled", reinterpret_cast(&InternalCall_Rendering_ScriptableRenderContext_GetMainDirectionalShadowEnabled)); @@ -8189,6 +8450,78 @@ void MonoScriptRuntime::ReleaseExternalManagedObject(uint32_t gcHandle) { DestroyExternalManagedObject(gcHandle); } +void MonoScriptRuntime::ReleaseRetainedManagedRenderFunc(uint32_t gcHandle) { + ReleaseExternalManagedObject(gcHandle); +} + +bool MonoScriptRuntime::InvokeManagedRenderGraphRasterFunc( + uint32_t gcHandle, + const char* passName, + uint64_t commandBufferHandle) { + if (!m_initialized || + gcHandle == 0u || + commandBufferHandle == 0u) { + return false; + } + + MonoObject* const renderFunc = + GetExternalManagedObject(gcHandle); + if (renderFunc == nullptr) { + return false; + } + + MonoClass* builderClass = nullptr; + if (!ResolveManagedClass( + m_settings.coreAssemblyName, + kManagedRenderingNamespace, + "RenderGraphRasterPassBuilder", + builderClass)) { + SetError( + "Managed class was not found: " + + m_settings.coreAssemblyName + "|" + + BuildFullClassName( + kManagedRenderingNamespace, + "RenderGraphRasterPassBuilder")); + return false; + } + + MonoMethod* const invokeMethod = + ResolveManagedMethod( + builderClass, + "InvokeManagedRenderFunc", + 3); + if (invokeMethod == nullptr) { + SetError( + "Managed RenderGraphRasterPassBuilder.InvokeManagedRenderFunc was not found."); + return false; + } + + MonoString* const managedPassName = + mono_string_new( + m_appDomain, + passName != nullptr ? passName : ""); + void* args[3] = { + renderFunc, + managedPassName, + &commandBufferHandle + }; + + MonoObject* exception = nullptr; + MonoObject* const result = + mono_runtime_invoke( + invokeMethod, + nullptr, + args, + &exception); + if (exception != nullptr) { + RecordException(exception); + return false; + } + + bool invoked = false; + return TryUnboxManagedBoolean(result, invoked) && invoked; +} + bool MonoScriptRuntime::IsScriptableRenderPipelineAssetObject( MonoObject* managedObject) const { return managedObject != nullptr && diff --git a/managed/GameScripts/RenderPipelineApiProbe.cs b/managed/GameScripts/RenderPipelineApiProbe.cs index 786437cf..7d7dc9fd 100644 --- a/managed/GameScripts/RenderPipelineApiProbe.cs +++ b/managed/GameScripts/RenderPipelineApiProbe.cs @@ -2607,6 +2607,83 @@ namespace Gameplay } } + internal static class ManagedCommandBufferRenderFuncProbeState + { + public static int RenderFuncCallCount; + public static int ClearSucceededCount; + public static Vector4 LastClearColor; + + public static void Reset() + { + RenderFuncCallCount = 0; + ClearSucceededCount = 0; + LastClearColor = new Vector4(0.0f, 0.0f, 0.0f, 0.0f); + } + } + + internal sealed class ManagedCommandBufferRenderFuncProbePipeline + : ScriptableRenderPipeline + { + protected override bool SupportsStageRenderGraph( + CameraFrameStage stage) + { + return stage == CameraFrameStage.MainScene; + } + + protected override bool RecordStageRenderGraph( + ScriptableRenderContext context) + { + if (context == null || + !context.primaryColorTarget.isValid) + { + return false; + } + + return context + .AddRasterPass( + "Managed.CommandBufferRenderFunc") + .SetColorAttachment( + context.primaryColorTarget) + .SetRenderFunc( + rasterContext => + { + Color clearColor = + new Color( + 0.20f, + 0.40f, + 0.60f, + 1.0f); + ManagedCommandBufferRenderFuncProbeState + .RenderFuncCallCount++; + ManagedCommandBufferRenderFuncProbeState + .LastClearColor = + new Vector4( + clearColor.r, + clearColor.g, + clearColor.b, + clearColor.a); + if (rasterContext != null && + rasterContext.cmd != null && + rasterContext.cmd.ClearRenderTarget( + clearColor)) + { + ManagedCommandBufferRenderFuncProbeState + .ClearSucceededCount++; + } + }) + .Commit(); + } + } + + public sealed class ManagedCommandBufferRenderFuncProbeAsset + : ScriptableRenderPipelineAsset + { + protected override ScriptableRenderPipeline CreatePipeline() + { + return new ManagedCommandBufferRenderFuncProbePipeline(); + } + } + internal sealed class ManagedRenderPipelineProbe : ProbeSceneRenderer { @@ -3338,6 +3415,32 @@ namespace Gameplay } } + public sealed class ManagedCommandBufferRenderFuncRuntimeSelectionProbe + : MonoBehaviour + { + public int ObservedRenderFuncCallCount; + public int ObservedClearSucceededCount; + public Vector4 ObservedLastClearColor; + + public void Start() + { + ManagedCommandBufferRenderFuncProbeState.Reset(); + } + + public void Update() + { + ObservedRenderFuncCallCount = + ManagedCommandBufferRenderFuncProbeState + .RenderFuncCallCount; + ObservedClearSucceededCount = + ManagedCommandBufferRenderFuncProbeState + .ClearSucceededCount; + ObservedLastClearColor = + ManagedCommandBufferRenderFuncProbeState + .LastClearColor; + } + } + public sealed class ManagedRenderContextCameraDataRuntimeSelectionProbe : MonoBehaviour { diff --git a/managed/XCEngine.ScriptCore/InternalCalls.cs b/managed/XCEngine.ScriptCore/InternalCalls.cs index 8c8a7408..2980ebdc 100644 --- a/managed/XCEngine.ScriptCore/InternalCalls.cs +++ b/managed/XCEngine.ScriptCore/InternalCalls.cs @@ -549,7 +549,8 @@ namespace XCEngine internal static extern bool Rendering_ScriptableRenderContext_SetRasterPassManagedRenderFuncExecution( ulong nativeHandle, - ulong rasterPassHandle); + ulong rasterPassHandle, + Action renderFunc); [MethodImpl(MethodImplOptions.InternalCall)] internal static extern bool @@ -557,6 +558,12 @@ namespace XCEngine ulong nativeHandle, ulong rasterPassHandle); + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool + Rendering_CommandBuffer_ClearRenderTarget( + ulong nativeHandle, + ref Color color); + [MethodImpl(MethodImplOptions.InternalCall)] internal static extern int Rendering_ScriptableRenderPipelinePlanningContext_GetRendererIndex( diff --git a/managed/XCEngine.ScriptCore/Rendering/Core/CommandBuffer.cs b/managed/XCEngine.ScriptCore/Rendering/Core/CommandBuffer.cs index 2981b61c..a7a0d6b3 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Core/CommandBuffer.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Core/CommandBuffer.cs @@ -1,3 +1,5 @@ +using XCEngine; + namespace XCEngine.Rendering { public sealed class CommandBuffer @@ -9,10 +11,33 @@ namespace XCEngine.Rendering public CommandBuffer( string name) + : this( + name, + 0ul) + { + } + + internal CommandBuffer( + string name, + ulong nativeHandle) { this.name = name ?? string.Empty; + m_nativeHandle = nativeHandle; } public string name { get; set; } + + public bool isValid => m_nativeHandle != 0ul; + + public bool ClearRenderTarget( + Color color) + { + return m_nativeHandle != 0ul && + InternalCalls.Rendering_CommandBuffer_ClearRenderTarget( + m_nativeHandle, + ref color); + } + + private readonly ulong m_nativeHandle; } } diff --git a/managed/XCEngine.ScriptCore/Rendering/Graph/RenderGraphRasterPassBuilder.cs b/managed/XCEngine.ScriptCore/Rendering/Graph/RenderGraphRasterPassBuilder.cs index 4c3d9737..584abdf5 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Graph/RenderGraphRasterPassBuilder.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Graph/RenderGraphRasterPassBuilder.cs @@ -373,17 +373,12 @@ namespace XCEngine.Rendering break; case RenderGraphRasterPassExecutionKind .ManagedRenderFunc: - // Managed command buffers are not bridged into native - // graph execution yet, so the render func is invoked - // during recording while native records the declared IO. - m_renderFunc( - new RenderGraphRasterContext( - new CommandBuffer(m_passName))); configuredExecution = InternalCalls .Rendering_ScriptableRenderContext_SetRasterPassManagedRenderFuncExecution( m_context.nativeHandle, - nativePassHandle); + nativePassHandle, + m_renderFunc); break; default: configuredExecution = false; @@ -501,5 +496,24 @@ namespace XCEngine.Rendering return false; } + + internal static bool InvokeManagedRenderFunc( + Action renderFunc, + string passName, + ulong commandBufferHandle) + { + if (renderFunc == null || + commandBufferHandle == 0ul) + { + return false; + } + + renderFunc( + new RenderGraphRasterContext( + new CommandBuffer( + passName, + commandBufferHandle))); + return true; + } } } diff --git a/tests/fixtures/RenderTestRhiStubs.h b/tests/fixtures/RenderTestRhiStubs.h index e5a5d46e..0099b08d 100644 --- a/tests/fixtures/RenderTestRhiStubs.h +++ b/tests/fixtures/RenderTestRhiStubs.h @@ -591,9 +591,16 @@ public: void ClearRenderTarget( XCEngine::RHI::RHIResourceView*, - const float[4], + const float color[4], uint32_t, const XCEngine::RHI::Rect*) override { + ++clearRenderTargetCalls; + if (color != nullptr) { + lastClearRenderTargetColor[0] = color[0]; + lastClearRenderTargetColor[1] = color[1]; + lastClearRenderTargetColor[2] = color[2]; + lastClearRenderTargetColor[3] = color[3]; + } } void ClearDepthStencil( @@ -616,6 +623,8 @@ public: size_t setPipelineStateCalls = 0u; size_t drawCalls = 0u; size_t drawIndexedCalls = 0u; + size_t clearRenderTargetCalls = 0u; + float lastClearRenderTargetColor[4] = {}; }; class TestRenderCommandQueue final : public XCEngine::RHI::RHICommandQueue { diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 3a3b2bc0..4736658c 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -4637,6 +4638,167 @@ TEST_F( recorder->Shutdown(); } +TEST_F( + MonoScriptRuntimeTest, + ManagedRenderFuncExecutesDuringRenderGraphAndFlushesCommandBuffer) { + Scene* runtimeScene = + CreateScene("ManagedCommandBufferRenderFuncScene"); + GameObject* selectionObject = + runtimeScene->CreateGameObject( + "ManagedCommandBufferRenderFuncSelection"); + ScriptComponent* selectionScript = + AddScript( + selectionObject, + "Gameplay", + "ManagedCommandBufferRenderFuncRuntimeSelectionProbe"); + ASSERT_NE(selectionScript, nullptr); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + int observedRenderFuncCallCount = -1; + int observedClearSucceededCount = -1; + ASSERT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "ObservedRenderFuncCallCount", + observedRenderFuncCallCount)); + ASSERT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "ObservedClearSucceededCount", + observedClearSucceededCount)); + ASSERT_EQ(observedRenderFuncCallCount, 0); + ASSERT_EQ(observedClearSucceededCount, 0); + + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor descriptor = { + "GameScripts", + "Gameplay", + "ManagedCommandBufferRenderFuncProbeAsset" + }; + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + std::unique_ptr recorder = + assetRuntime->CreateStageRecorder(); + ASSERT_NE(recorder, nullptr); + + TestRenderDevice device; + TestRenderCommandList commandList; + TestRenderCommandQueue commandQueue; + const XCEngine::Rendering::RenderContext renderContext = + CreateRenderContext( + device, + commandList, + commandQueue); + 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); + const XCEngine::Rendering::RenderGraphTextureHandle colorTarget = + graphBuilder.CreateTransientTexture( + "ManagedCommandBufferRenderFuncColor", + colorDesc); + + 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, + "ManagedCommandBufferRenderFunc", + XCEngine::Rendering::CameraFrameStage::MainScene, + renderContext, + sceneData, + surface, + nullptr, + nullptr, + XCEngine::RHI::ResourceStates::Common, + {}, + { colorTarget }, + {}, + {}, + &executionSucceeded, + &blackboard + }; + + ASSERT_TRUE(recorder->RecordStageRenderGraph(graphContext)) + << runtime->GetLastError(); + EXPECT_EQ(commandList.clearRenderTargetCalls, 0u); + + engine->OnUpdate(0.016f); + EXPECT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "ObservedRenderFuncCallCount", + observedRenderFuncCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "ObservedClearSucceededCount", + observedClearSucceededCount)); + EXPECT_EQ(observedRenderFuncCallCount, 0); + EXPECT_EQ(observedClearSucceededCount, 0); + + 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(), + "Managed.CommandBufferRenderFunc"); + + ASSERT_TRUE( + XCEngine::Rendering::RenderGraphExecutor::Execute( + compiledGraph, + renderContext, + &errorMessage)) + << errorMessage.CStr(); + + EXPECT_EQ(commandList.clearRenderTargetCalls, 1u); + EXPECT_NEAR(commandList.lastClearRenderTargetColor[0], 0.20f, 0.001f); + EXPECT_NEAR(commandList.lastClearRenderTargetColor[1], 0.40f, 0.001f); + EXPECT_NEAR(commandList.lastClearRenderTargetColor[2], 0.60f, 0.001f); + EXPECT_NEAR(commandList.lastClearRenderTargetColor[3], 1.0f, 0.001f); + + engine->OnUpdate(0.016f); + XCEngine::Math::Vector4 observedLastClearColor = {}; + EXPECT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "ObservedRenderFuncCallCount", + observedRenderFuncCallCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "ObservedClearSucceededCount", + observedClearSucceededCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + selectionScript, + "ObservedLastClearColor", + observedLastClearColor)); + EXPECT_EQ(observedRenderFuncCallCount, 1); + EXPECT_EQ(observedClearSucceededCount, 1); + ExpectVector4Near( + observedLastClearColor, + XCEngine::Math::Vector4(0.20f, 0.40f, 0.60f, 1.0f)); + + recorder->Shutdown(); +} + TEST_F( MonoScriptRuntimeTest, ManagedRenderPipelineAssetPlansFullscreenStagesFromRendererPassQueue) {