From c0b670b0523d6b52c5fade8dffb14666129d2e2e Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Mon, 27 Apr 2026 19:35:17 +0800 Subject: [PATCH] Serialize managed SRP asset graphs --- engine/include/XCEngine/Rendering/AGENTS.md | 25 +- .../ManagedScriptableRenderPipelineAsset.h | 5 + .../Scripting/Mono/MonoScriptRuntime.h | 9 + .../src/Scripting/Mono/MonoScriptRuntime.cpp | 206 +++- managed/CMakeLists.txt | 1 + managed/GameScripts/RenderPipelineApiProbe.cs | 129 ++ .../Universal/ScriptableRendererFeature.cs | 2 +- .../Core/ScriptableRenderPipelineAsset.cs | 15 + .../ScriptableObjectSerializedGraph.cs | 1051 +++++++++++++++++ tests/scripting/test_mono_script_runtime.cpp | 150 +++ 10 files changed, 1568 insertions(+), 25 deletions(-) create mode 100644 managed/XCEngine.ScriptCore/ScriptableObjectSerializedGraph.cs diff --git a/engine/include/XCEngine/Rendering/AGENTS.md b/engine/include/XCEngine/Rendering/AGENTS.md index b8a02742..01401326 100644 --- a/engine/include/XCEngine/Rendering/AGENTS.md +++ b/engine/include/XCEngine/Rendering/AGENTS.md @@ -109,7 +109,8 @@ Unity 兼容的公开命名、对象所有权和扩展点。 - `GraphicsSettings.renderPipelineAsset == null` 表示 renderer 使用 engine default native pipeline selection。 - `GraphicsSettings.renderPipelineAsset != null` 表示 renderer 使用 render-pipeline asset reference selection path。 - `AssetRef` 是长期选择身份;managed descriptor/handle 只是运行时 materialization cache 和过渡 fallback。 + `AssetRef` 是长期选择身份;managed descriptor 中的 serialized ScriptableObject graph 和 handle 只是运行时 + materialization cache 和过渡 fallback。 - Runtime startup 不应静默指定 project default SRP asset。Project 或 editor policy 可以显式选择一个, 但 active rendering mode 必须能通过 `GraphicsSettings.renderPipelineAsset` 观察到。 - `GraphicsSettingsState` 同时保存 configured render-pipeline asset `AssetRef` 和 @@ -198,11 +199,16 @@ Native passes 仍用于 backend fallback、工具和 built-in rendering。 Managed SRP assets 通过 `GraphicsSettings.renderPipelineAsset` 选择,并通过 `ManagedScriptableRenderPipelineAsset` bridge。 -- `ManagedRenderPipelineAssetDescriptor` 标识 managed asset assembly、namespace、class、retained managed - object handle 和可选 `AssetRef`。它不再是 configured pipeline 的唯一身份;`GraphicsSettingsState` 中的 - configured `AssetRef` 才是 Unity 风格 asset selection root。 -- Descriptor-only selection 仍保留给测试、legacy runtime fallback 和尚未接入 asset serialization 的路径。 - 一旦已有 configured `AssetRef`,不要把 managed object handle 或 descriptor 当作长期 asset identity。 +- `ManagedRenderPipelineAssetDescriptor` 标识 managed asset assembly、namespace、class、可选 `AssetRef`、 + serialized ScriptableObject asset graph 和 retained managed object handle。它不再是 configured pipeline + 的唯一身份;`GraphicsSettingsState` 中的 configured `AssetRef` 才是 Unity 风格 asset selection root。 +- Descriptor-only selection 仍保留给测试、legacy runtime fallback 和尚未接入 asset import 的路径。没有 + `serializedAssetGraph` 时才允许按 class 默认构造 materialize;一旦 snapshot 存在,缺失或失效的 + managed object handle 必须从 snapshot 重建,不能退回 code-created default asset。 +- `GraphicsSettings.renderPipelineAsset = asset` 必须把当前 managed SRP asset graph snapshot 收进 descriptor。 + 该 snapshot 覆盖 `UniversalRenderPipelineAsset -> ScriptableRendererData -> ScriptableRendererFeature` + 以及 public/`[SerializeField]` serializable settings。Runtime cache 更新可以替换 handle,但不得丢失 + snapshot 或 configured `AssetRef`。 - `ManagedScriptableRenderPipelineAsset` 解析 `ManagedRenderPipelineAssetRuntime`,创建 `ScriptableRenderPipelineHost`,并把 request/plan/final-color policy calls 转发给 managed code。 - `ScriptableRenderPipelineHost` 组合 native backend asset 和可选 managed stage recorder。当 managed recorder @@ -339,8 +345,8 @@ Scene data 每个 camera frame 提取一次,然后由 pipeline 调整。 authoring 还未公开。 - `UniversalPostProcessBlock` 仍保留 post-process source promotion helper;实际 post-process stage 由 active pass queue 中的 features/passes 声明。 -- Render-pipeline selection 已切到 `AssetRef` 作为根身份,但 `UniversalRendererData`、features 和 - `ScriptableObject` 字段/子资产仍是 code-created objects,还不是完整 Unity 风格 serialized asset pipeline。 +- Render-pipeline selection 已切到 `AssetRef` 作为根身份,并且 managed SRP descriptor 已保存 + ScriptableObject graph snapshot;完整 editor/importer `.asset` 持久化和 sub-asset authoring 仍未完成。 - 当前 shadow support 是单个 main directional shadow path,没有 cascades。 - Graph compiler/executor 当前没有实现 pass culling 或 transient aliasing。 @@ -354,6 +360,9 @@ Scene data 每个 camera frame 提取一次,然后由 pipeline 调整。 `GraphicsSettingsState` 选择 managed SRP assets。 - Render-pipeline asset selection 已从 descriptor/managed handle 切到 `AssetRef` 根身份;descriptor 保留为 assembly/type/handle runtime cache,managed materialization 更新 cache 时保留 configured asset reference。 +- Managed SRP descriptor 已接入 serialized ScriptableObject graph snapshot。`GraphicsSettings` setter 保存 + 当前 asset/data/features 图,getter 和 `MonoManagedRenderPipelineAssetRuntime` 在 handle 缺失或失效时按 + snapshot 重建;无 snapshot 的 descriptor-only selection 才保留 class 默认构造 fallback。 - Managed SRP execution 由 `ScriptableRenderPipelineHost` 承载,它组合 native backend 和可选 managed stage recorder。 - Mono-backed SRP assets 使用 `DefaultNativeBackend` 做 scene drawing,并把 managed stages 记录到 native diff --git a/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h b/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h index c93f9324..3031e76c 100644 --- a/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h +++ b/engine/include/XCEngine/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.h @@ -33,6 +33,7 @@ struct ManagedRenderPipelineAssetDescriptor { std::string className; uint32_t managedAssetHandle = 0u; Resources::AssetRef assetRef = {}; + std::string serializedAssetGraph; bool IsValid() const { return !assemblyName.empty() && !className.empty(); @@ -46,6 +47,10 @@ struct ManagedRenderPipelineAssetDescriptor { return assetRef.IsValid(); } + bool HasSerializedAssetGraph() const { + return !serializedAssetGraph.empty(); + } + std::string GetFullName() const { return namespaceName.empty() ? className diff --git a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h index f670b382..8b116470 100644 --- a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h @@ -103,6 +103,12 @@ public: const char* passName, uint64_t commandBufferHandle); bool IsScriptableRenderPipelineAssetObject(MonoObject* managedObject) const; + bool TrySerializeManagedRenderPipelineAssetGraph( + MonoObject* managedObject, + std::string& outSerializedGraph); + bool TryCreateManagedRenderPipelineAssetFromSerializedGraph( + const std::string& serializedGraph, + uint32_t& outHandle); bool TryEnsureManagedRenderPipelineAssetHandle( Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor& ioDescriptor); @@ -266,6 +272,9 @@ private: bool CreateExternalManagedObject( MonoClass* monoClass, uint32_t& outHandle); + bool TryDeserializeManagedRenderPipelineAssetGraph( + const std::string& serializedGraph, + MonoObject*& outAsset); uint32_t RetainExternalManagedObject(MonoObject* instance); void DestroyExternalManagedObject(uint32_t gcHandle); MonoObject* CreateManagedScriptableRenderContext(uint64_t nativeHandle); diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index e3f7aeb6..817d16fe 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -2752,24 +2752,33 @@ bool MonoManagedRenderPipelineAssetRuntime::EnsureManagedAsset() const { ? mono_object_get_class(assetObject) : nullptr; if (assetClass == nullptr) { - m_runtime->SetError( - "Managed render pipeline asset handle is no longer valid: " + - m_descriptor.GetFullName() + "."); - return false; - } - - if (!IsMonoClassOrSubclass( - assetClass, - m_runtime->m_scriptableRenderPipelineAssetClass)) { + if (!m_descriptor.HasSerializedAssetGraph()) { + m_runtime->SetError( + "Managed render pipeline asset handle is no longer valid: " + + m_descriptor.GetFullName() + "."); + return false; + } + } else if (!IsMonoClassOrSubclass( + assetClass, + m_runtime->m_scriptableRenderPipelineAssetClass)) { m_runtime->SetError( "Managed render pipeline asset must derive from ScriptableRenderPipelineAsset: " + m_descriptor.GetFullName() + "."); return false; + } else { + m_assetHandle = m_descriptor.managedAssetHandle; + m_ownsManagedAssetHandle = false; + return true; } + } - m_assetHandle = m_descriptor.managedAssetHandle; - m_ownsManagedAssetHandle = false; - return true; + if (m_descriptor.HasSerializedAssetGraph()) { + m_ownsManagedAssetHandle = + m_runtime->TryCreateManagedRenderPipelineAssetFromSerializedGraph( + m_descriptor.serializedAssetGraph, + m_assetHandle) && + m_assetHandle != 0; + return m_ownsManagedAssetHandle; } MonoClass* assetClass = nullptr; @@ -4896,6 +4905,17 @@ void InternalCall_Rendering_SetRenderPipelineAsset(MonoObject* assetObject) { runtime != nullptr ? runtime->RetainExternalManagedObjectReference(assetObject) : 0u; + if (runtime != nullptr && + !runtime->TrySerializeManagedRenderPipelineAssetGraph( + assetObject, + descriptor.serializedAssetGraph)) { + if (descriptor.managedAssetHandle != 0u) { + runtime->ReleaseExternalManagedObject( + descriptor.managedAssetHandle); + } + return; + } + if (!descriptor.IsValid() || descriptor.managedAssetHandle == 0u) { if (runtime != nullptr && @@ -4930,11 +4950,13 @@ MonoObject* InternalCall_Rendering_GetRenderPipelineAsset() { return nullptr; } - if (descriptor.managedAssetHandle == 0u) { - if (!runtime->TryEnsureManagedRenderPipelineAssetHandle(descriptor)) { - return nullptr; - } + const uint32_t previousManagedAssetHandle = + descriptor.managedAssetHandle; + if (!runtime->TryEnsureManagedRenderPipelineAssetHandle(descriptor)) { + return nullptr; + } + if (descriptor.managedAssetHandle != previousManagedAssetHandle) { Rendering::GetGraphicsSettingsState() .UpdateConfiguredRenderPipelineAssetRuntimeDescriptor(descriptor); } @@ -8531,6 +8553,127 @@ bool MonoScriptRuntime::IsScriptableRenderPipelineAssetObject( m_scriptableRenderPipelineAssetClass); } +bool MonoScriptRuntime::TrySerializeManagedRenderPipelineAssetGraph( + MonoObject* managedObject, + std::string& outSerializedGraph) { + outSerializedGraph.clear(); + if (!m_initialized || + !IsScriptableRenderPipelineAssetObject(managedObject) || + m_scriptableRenderPipelineAssetClass == nullptr) { + return false; + } + + MonoMethod* const serializeMethod = + ResolveManagedMethod( + m_scriptableRenderPipelineAssetClass, + "SerializeAssetGraphInstance", + 1); + if (serializeMethod == nullptr) { + SetError( + "Managed ScriptableRenderPipelineAsset.SerializeAssetGraphInstance was not found."); + return false; + } + + SetCurrentDomain(); + + void* args[1] = { managedObject }; + MonoObject* exception = nullptr; + MonoObject* const result = + mono_runtime_invoke( + serializeMethod, + nullptr, + args, + &exception); + if (exception != nullptr) { + RecordException(exception); + return false; + } + + MonoString* const serializedGraph = + reinterpret_cast(result); + outSerializedGraph = MonoStringToUtf8(serializedGraph); + if (outSerializedGraph.empty()) { + SetError( + "Managed render pipeline asset graph serialization returned an empty snapshot."); + return false; + } + + return true; +} + +bool MonoScriptRuntime::TryDeserializeManagedRenderPipelineAssetGraph( + const std::string& serializedGraph, + MonoObject*& outAsset) { + outAsset = nullptr; + if (!m_initialized || + serializedGraph.empty() || + m_scriptableRenderPipelineAssetClass == nullptr) { + return false; + } + + MonoMethod* const deserializeMethod = + ResolveManagedMethod( + m_scriptableRenderPipelineAssetClass, + "DeserializeAssetGraphInstance", + 1); + if (deserializeMethod == nullptr) { + SetError( + "Managed ScriptableRenderPipelineAsset.DeserializeAssetGraphInstance was not found."); + return false; + } + + SetCurrentDomain(); + + MonoString* const managedSerializedGraph = + mono_string_new( + m_appDomain, + serializedGraph.c_str()); + void* args[1] = { managedSerializedGraph }; + MonoObject* exception = nullptr; + MonoObject* const result = + mono_runtime_invoke( + deserializeMethod, + nullptr, + args, + &exception); + if (exception != nullptr) { + RecordException(exception); + return false; + } + + if (!IsScriptableRenderPipelineAssetObject(result)) { + SetError( + "Managed render pipeline asset graph deserialization did not return a ScriptableRenderPipelineAsset."); + return false; + } + + outAsset = result; + return true; +} + +bool MonoScriptRuntime::TryCreateManagedRenderPipelineAssetFromSerializedGraph( + const std::string& serializedGraph, + uint32_t& outHandle) { + outHandle = 0u; + + MonoObject* assetObject = nullptr; + if (!TryDeserializeManagedRenderPipelineAssetGraph( + serializedGraph, + assetObject) || + assetObject == nullptr) { + return false; + } + + outHandle = RetainExternalManagedObject(assetObject); + if (outHandle == 0u) { + SetError( + "Managed render pipeline asset graph deserialization returned an object that could not be retained."); + return false; + } + + return true; +} + bool MonoScriptRuntime::TryEnsureManagedRenderPipelineAssetHandle( Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor& ioDescriptor) { if (!m_initialized || !ioDescriptor.IsValid()) { @@ -8547,6 +8690,37 @@ bool MonoScriptRuntime::TryEnsureManagedRenderPipelineAssetHandle( ioDescriptor.managedAssetHandle = 0u; } + if (ioDescriptor.HasSerializedAssetGraph()) { + if (!TryCreateManagedRenderPipelineAssetFromSerializedGraph( + ioDescriptor.serializedAssetGraph, + ioDescriptor.managedAssetHandle) || + ioDescriptor.managedAssetHandle == 0u) { + return false; + } + + MonoObject* const assetObject = + GetExternalManagedObject(ioDescriptor.managedAssetHandle); + MonoClass* const assetClass = + assetObject != nullptr + ? mono_object_get_class(assetObject) + : nullptr; + if (assetClass != nullptr) { + MonoImage* const image = + mono_class_get_image(assetClass); + ioDescriptor.assemblyName = + TrimAssemblyName( + SafeString( + image != nullptr + ? mono_image_get_name(image) + : nullptr)); + ioDescriptor.namespaceName = + SafeString(mono_class_get_namespace(assetClass)); + ioDescriptor.className = + SafeString(mono_class_get_name(assetClass)); + } + return true; + } + MonoClass* assetClass = nullptr; if (!ResolveManagedClass( ioDescriptor.assemblyName, diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index 71998a14..dcc6cd86 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -162,6 +162,7 @@ set(XCENGINE_SCRIPT_CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/MonoBehaviour.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Object.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/ScriptableObject.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/ScriptableObjectSerializedGraph.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Physics.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/PhysicsBodyType.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Quaternion.cs diff --git a/managed/GameScripts/RenderPipelineApiProbe.cs b/managed/GameScripts/RenderPipelineApiProbe.cs index 7d7dc9fd..8714a92d 100644 --- a/managed/GameScripts/RenderPipelineApiProbe.cs +++ b/managed/GameScripts/RenderPipelineApiProbe.cs @@ -3314,6 +3314,135 @@ namespace Gameplay } } + public sealed class SerializedUniversalRenderPipelineAssetGraphProbe + : MonoBehaviour + { + public int UpdateCount; + public bool ObservedAssetWasNull = true; + public int ObservedDefaultRendererIndex = -1; + public int ObservedRendererDataCount; + public int ObservedRendererFeatureCount; + public bool ObservedFeatureActive = true; + public string ObservedFeatureTypeName = string.Empty; + public Vector4 ObservedColorScale; + public bool ObservedShadowsSupportMainLight = true; + public Vector4 ObservedFinalColorScale; + + public void Start() + { + UniversalRenderPipelineAsset asset = + ScriptableObject + .CreateInstance(); + UniversalRendererData rendererData = + ScriptableObject + .CreateInstance(); + ColorScalePostProcessRendererFeature colorScaleFeature = + ScriptableObject + .CreateInstance(); + if (asset == null || + rendererData == null || + colorScaleFeature == null) + { + return; + } + + colorScaleFeature.colorScale = new Vector4( + 0.42f, + 0.51f, + 0.63f, + 1.0f); + colorScaleFeature.SetActive(false); + rendererData.rendererFeatures = + new ScriptableRendererFeature[] + { + colorScaleFeature + }; + asset.rendererDataList = + new ScriptableRendererData[] + { + rendererData + }; + asset.defaultRendererIndex = 0; + asset.shadows = + UniversalShadowSettings.CreateDefault(); + asset.shadows.supportsMainLightShadows = false; + asset.finalColor = + UniversalFinalColorSettings.CreateDefault(); + asset.finalColor.settings.finalColorScale = + new Vector4( + 0.90f, + 1.10f, + 1.20f, + 1.0f); + + GraphicsSettings.renderPipelineAsset = asset; + } + + public void Update() + { + ++UpdateCount; + if (UpdateCount < 2) + { + return; + } + + UniversalRenderPipelineAsset asset = + GraphicsSettings.renderPipelineAsset as + UniversalRenderPipelineAsset; + ObservedAssetWasNull = asset == null; + if (asset == null) + { + return; + } + + ObservedDefaultRendererIndex = + asset.defaultRendererIndex; + ObservedRendererDataCount = + asset.rendererDataList != null + ? asset.rendererDataList.Length + : 0; + ObservedShadowsSupportMainLight = + asset.shadows != null && + asset.shadows.supportsMainLightShadows; + ObservedFinalColorScale = + asset.finalColor != null + ? asset.finalColor.settings.finalColorScale + : new Vector4(); + + ScriptableRendererData rendererData = + asset.rendererDataList != null && + asset.rendererDataList.Length > 0 + ? asset.rendererDataList[0] + : null; + ObservedRendererFeatureCount = + rendererData != null && + rendererData.rendererFeatures != null + ? rendererData.rendererFeatures.Length + : 0; + + ScriptableRendererFeature feature = + rendererData != null && + rendererData.rendererFeatures != null && + rendererData.rendererFeatures.Length > 0 + ? rendererData.rendererFeatures[0] + : null; + ObservedFeatureTypeName = + feature != null + ? feature.GetType().FullName ?? string.Empty + : string.Empty; + ObservedFeatureActive = + feature != null && + feature.isActive; + + ColorScalePostProcessRendererFeature colorScaleFeature = + feature as ColorScalePostProcessRendererFeature; + ObservedColorScale = + colorScaleFeature != null + ? colorScaleFeature.colorScale + : new Vector4(); + } + } + public sealed class ScriptCoreUniversalShadowlessRendererFeatureRuntimeSelectionProbe : MonoBehaviour { diff --git a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs index 70319b6e..b9b6879e 100644 --- a/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs +++ b/managed/XCEngine.RenderPipelines.Universal/Rendering/Universal/ScriptableRendererFeature.cs @@ -10,7 +10,7 @@ namespace XCEngine.Rendering.Universal { private bool m_disposed; private bool m_runtimeCreated; - private bool m_isActive = true; + [SerializeField] private bool m_isActive = true; private int m_runtimeStateVersion = 1; private int m_runtimeStateHash; private bool m_runtimeStateHashResolved; diff --git a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs index 1e7748fb..e10fc7ab 100644 --- a/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs +++ b/managed/XCEngine.ScriptCore/Rendering/Core/ScriptableRenderPipelineAsset.cs @@ -10,6 +10,21 @@ namespace XCEngine.Rendering { } + internal static string SerializeAssetGraphInstance( + ScriptableRenderPipelineAsset asset) + { + return ScriptableObjectSerializedGraph.Serialize(asset); + } + + internal static ScriptableRenderPipelineAsset + DeserializeAssetGraphInstance( + string serializedAssetGraph) + { + return ScriptableObjectSerializedGraph + .Deserialize(serializedAssetGraph) as + ScriptableRenderPipelineAsset; + } + internal void ReleaseRuntimeResourcesInstance() { ReleaseRuntimeResources(); diff --git a/managed/XCEngine.ScriptCore/ScriptableObjectSerializedGraph.cs b/managed/XCEngine.ScriptCore/ScriptableObjectSerializedGraph.cs new file mode 100644 index 00000000..f84194e9 --- /dev/null +++ b/managed/XCEngine.ScriptCore/ScriptableObjectSerializedGraph.cs @@ -0,0 +1,1051 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace XCEngine +{ + internal static class ScriptableObjectSerializedGraph + { + private const string FormatTag = "XCEngine.ScriptableObjectGraph"; + private const int FormatVersion = 1; + private static readonly object MissingValue = new object(); + + private enum ValueKind : byte + { + Null = 0, + Boolean = 1, + Byte = 2, + SByte = 3, + Int16 = 4, + UInt16 = 5, + Int32 = 6, + UInt32 = 7, + Int64 = 8, + UInt64 = 9, + Single = 10, + Double = 11, + String = 12, + Enum = 13, + ObjectReference = 14, + Array = 15, + Struct = 16 + } + + public static string Serialize( + ScriptableObject root) + { + if (root == null) + { + return string.Empty; + } + + SerializationContext context = + new SerializationContext(root); + using (MemoryStream stream = new MemoryStream()) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + writer.Write(FormatTag); + writer.Write(FormatVersion); + writer.Write(context.rootId); + writer.Write(context.nodes.Count); + + for (int i = 0; i < context.nodes.Count; ++i) + { + context.WriteNode(writer, context.nodes[i]); + } + + writer.Flush(); + return Convert.ToBase64String(stream.ToArray()); + } + } + + public static ScriptableObject Deserialize( + string serializedGraph) + { + if (string.IsNullOrEmpty(serializedGraph)) + { + return null; + } + + try + { + byte[] bytes = Convert.FromBase64String(serializedGraph); + using (MemoryStream stream = new MemoryStream(bytes)) + using (BinaryReader reader = new BinaryReader(stream)) + { + string tag = reader.ReadString(); + int version = reader.ReadInt32(); + if (tag != FormatTag || + version != FormatVersion) + { + return null; + } + + int rootId = reader.ReadInt32(); + int nodeCount = reader.ReadInt32(); + if (rootId < 0 || + rootId >= nodeCount || + nodeCount < 0) + { + return null; + } + + List nodes = + new List(nodeCount); + for (int i = 0; i < nodeCount; ++i) + { + nodes.Add(ReadNode(reader)); + } + + for (int i = 0; i < nodes.Count; ++i) + { + Type type = ResolveType( + nodes[i].assemblyName, + nodes[i].typeName); + nodes[i].instance = + CreateObjectInstance(type); + } + + if (nodes[rootId].instance == null) + { + return null; + } + + for (int i = 0; i < nodes.Count; ++i) + { + ApplyFields( + nodes[i].instance, + nodes[i].fields, + nodes); + } + + return nodes[rootId].instance as ScriptableObject; + } + } + catch + { + return null; + } + } + + private sealed class SerializationContext + { + public readonly List nodes = new List(); + public readonly int rootId; + private readonly Dictionary m_nodeIds = + new Dictionary( + ReferenceEqualityComparer.Instance); + + public SerializationContext( + object root) + { + rootId = CollectObject(root); + } + + public void WriteNode( + BinaryWriter writer, + object node) + { + WriteType(writer, node.GetType()); + WriteFieldSet(writer, node, node.GetType()); + } + + private int CollectObject( + object value) + { + if (value == null) + { + return -1; + } + + int existingId; + if (m_nodeIds.TryGetValue(value, out existingId)) + { + return existingId; + } + + int id = nodes.Count; + m_nodeIds[value] = id; + nodes.Add(value); + + Type type = value.GetType(); + FieldInfo[] fields = GetSerializableFields(type); + for (int i = 0; i < fields.Length; ++i) + { + FieldInfo field = fields[i]; + object fieldValue = field.GetValue(value); + EnsureSerializableFieldValue( + field, + fieldValue); + CollectValue(fieldValue); + } + + return id; + } + + private void CollectValue( + object value) + { + if (value == null) + { + return; + } + + Type type = value.GetType(); + if (IsScalarType(type) || + type.IsEnum) + { + return; + } + + if (type.IsArray) + { + Array array = value as Array; + if (array == null) + { + return; + } + + for (int i = 0; i < array.Length; ++i) + { + object elementValue = array.GetValue(i); + EnsureSerializableArrayElementValue( + type.GetElementType(), + elementValue); + CollectValue(elementValue); + } + + return; + } + + if (type.IsValueType) + { + FieldInfo[] fields = GetSerializableFields(type); + for (int i = 0; i < fields.Length; ++i) + { + FieldInfo field = fields[i]; + object fieldValue = field.GetValue(value); + EnsureSerializableFieldValue( + field, + fieldValue); + CollectValue(fieldValue); + } + + return; + } + + if (CanSerializeReferenceType(type)) + { + CollectObject(value); + } + } + + private void WriteFieldSet( + BinaryWriter writer, + object instance, + Type type) + { + FieldInfo[] fields = GetSerializableFields(type); + writer.Write(fields.Length); + for (int i = 0; i < fields.Length; ++i) + { + FieldInfo field = fields[i]; + object fieldValue = field.GetValue(instance); + EnsureSerializableFieldValue( + field, + fieldValue); + WriteType(writer, field.DeclaringType); + writer.Write(field.Name); + WriteValue( + writer, + fieldValue, + field.FieldType); + } + } + + private void WriteValue( + BinaryWriter writer, + object value, + Type declaredType) + { + if (value == null) + { + writer.Write((byte)ValueKind.Null); + return; + } + + Type type = value.GetType(); + if (type == typeof(bool)) + { + writer.Write((byte)ValueKind.Boolean); + writer.Write((bool)value); + return; + } + + if (type == typeof(byte)) + { + writer.Write((byte)ValueKind.Byte); + writer.Write((byte)value); + return; + } + + if (type == typeof(sbyte)) + { + writer.Write((byte)ValueKind.SByte); + writer.Write((sbyte)value); + return; + } + + if (type == typeof(short)) + { + writer.Write((byte)ValueKind.Int16); + writer.Write((short)value); + return; + } + + if (type == typeof(ushort)) + { + writer.Write((byte)ValueKind.UInt16); + writer.Write((ushort)value); + return; + } + + if (type == typeof(int)) + { + writer.Write((byte)ValueKind.Int32); + writer.Write((int)value); + return; + } + + if (type == typeof(uint)) + { + writer.Write((byte)ValueKind.UInt32); + writer.Write((uint)value); + return; + } + + if (type == typeof(long)) + { + writer.Write((byte)ValueKind.Int64); + writer.Write((long)value); + return; + } + + if (type == typeof(ulong)) + { + writer.Write((byte)ValueKind.UInt64); + writer.Write((ulong)value); + return; + } + + if (type == typeof(float)) + { + writer.Write((byte)ValueKind.Single); + writer.Write((float)value); + return; + } + + if (type == typeof(double)) + { + writer.Write((byte)ValueKind.Double); + writer.Write((double)value); + return; + } + + if (type == typeof(string)) + { + writer.Write((byte)ValueKind.String); + writer.Write((string)value); + return; + } + + if (type.IsEnum) + { + writer.Write((byte)ValueKind.Enum); + WriteType(writer, type); + writer.Write(Convert.ToInt64( + value, + CultureInfo.InvariantCulture)); + return; + } + + if (type.IsArray) + { + Array array = value as Array; + writer.Write((byte)ValueKind.Array); + WriteType(writer, type.GetElementType()); + writer.Write(array != null ? array.Length : 0); + if (array == null) + { + return; + } + + Type elementType = type.GetElementType(); + for (int i = 0; i < array.Length; ++i) + { + object elementValue = array.GetValue(i); + EnsureSerializableArrayElementValue( + elementType, + elementValue); + WriteValue( + writer, + elementValue, + elementType); + } + + return; + } + + if (type.IsValueType) + { + writer.Write((byte)ValueKind.Struct); + WriteType(writer, type); + WriteFieldSet(writer, value, type); + return; + } + + writer.Write((byte)ValueKind.ObjectReference); + writer.Write(m_nodeIds[value]); + } + } + + private sealed class NodeRecord + { + public string assemblyName; + public string typeName; + public List fields; + public object instance; + } + + private sealed class FieldRecord + { + public string declaringAssemblyName; + public string declaringTypeName; + public string name; + public ValueRecord value; + } + + private sealed class ValueRecord + { + public ValueKind kind; + public object scalarValue; + public string assemblyName; + public string typeName; + public int objectId; + public List elements; + public List fields; + } + + private sealed class ReferenceEqualityComparer + : IEqualityComparer + { + public static readonly ReferenceEqualityComparer Instance = + new ReferenceEqualityComparer(); + + public new bool Equals( + object x, + object y) + { + return ReferenceEquals(x, y); + } + + public int GetHashCode( + object obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } + + private static NodeRecord ReadNode( + BinaryReader reader) + { + NodeRecord node = new NodeRecord(); + ReadType( + reader, + out node.assemblyName, + out node.typeName); + node.fields = ReadFieldSet(reader); + return node; + } + + private static List ReadFieldSet( + BinaryReader reader) + { + int fieldCount = reader.ReadInt32(); + List fields = + new List(fieldCount); + for (int i = 0; i < fieldCount; ++i) + { + FieldRecord field = new FieldRecord(); + ReadType( + reader, + out field.declaringAssemblyName, + out field.declaringTypeName); + field.name = reader.ReadString(); + field.value = ReadValue(reader); + fields.Add(field); + } + + return fields; + } + + private static ValueRecord ReadValue( + BinaryReader reader) + { + ValueRecord value = new ValueRecord(); + value.kind = (ValueKind)reader.ReadByte(); + switch (value.kind) + { + case ValueKind.Null: + break; + case ValueKind.Boolean: + value.scalarValue = reader.ReadBoolean(); + break; + case ValueKind.Byte: + value.scalarValue = reader.ReadByte(); + break; + case ValueKind.SByte: + value.scalarValue = reader.ReadSByte(); + break; + case ValueKind.Int16: + value.scalarValue = reader.ReadInt16(); + break; + case ValueKind.UInt16: + value.scalarValue = reader.ReadUInt16(); + break; + case ValueKind.Int32: + value.scalarValue = reader.ReadInt32(); + break; + case ValueKind.UInt32: + value.scalarValue = reader.ReadUInt32(); + break; + case ValueKind.Int64: + value.scalarValue = reader.ReadInt64(); + break; + case ValueKind.UInt64: + value.scalarValue = reader.ReadUInt64(); + break; + case ValueKind.Single: + value.scalarValue = reader.ReadSingle(); + break; + case ValueKind.Double: + value.scalarValue = reader.ReadDouble(); + break; + case ValueKind.String: + value.scalarValue = reader.ReadString(); + break; + case ValueKind.Enum: + ReadType( + reader, + out value.assemblyName, + out value.typeName); + value.scalarValue = reader.ReadInt64(); + break; + case ValueKind.ObjectReference: + value.objectId = reader.ReadInt32(); + break; + case ValueKind.Array: + ReadType( + reader, + out value.assemblyName, + out value.typeName); + int elementCount = reader.ReadInt32(); + value.elements = + new List(elementCount); + for (int i = 0; i < elementCount; ++i) + { + value.elements.Add(ReadValue(reader)); + } + break; + case ValueKind.Struct: + ReadType( + reader, + out value.assemblyName, + out value.typeName); + value.fields = ReadFieldSet(reader); + break; + } + + return value; + } + + private static void ApplyFields( + object target, + List fields, + List nodes) + { + if (target == null || + fields == null) + { + return; + } + + for (int i = 0; i < fields.Count; ++i) + { + FieldRecord fieldRecord = fields[i]; + FieldInfo field = ResolveField(fieldRecord); + if (field == null || + !ShouldSerializeField(field)) + { + continue; + } + + object value = ConvertValue( + fieldRecord.value, + field.FieldType, + nodes); + if (ReferenceEquals(value, MissingValue)) + { + continue; + } + + try + { + field.SetValue(target, value); + } + catch + { + } + } + } + + private static object ConvertValue( + ValueRecord value, + Type targetType, + List nodes) + { + if (value == null) + { + return MissingValue; + } + + switch (value.kind) + { + case ValueKind.Null: + return targetType != null && + targetType.IsValueType + ? Activator.CreateInstance(targetType) + : null; + case ValueKind.Boolean: + case ValueKind.Byte: + case ValueKind.SByte: + case ValueKind.Int16: + case ValueKind.UInt16: + case ValueKind.Int32: + case ValueKind.UInt32: + case ValueKind.Int64: + case ValueKind.UInt64: + case ValueKind.Single: + case ValueKind.Double: + case ValueKind.String: + return ConvertScalar( + value.scalarValue, + targetType); + case ValueKind.Enum: + Type enumType = + ResolveType( + value.assemblyName, + value.typeName) ?? + targetType; + return enumType != null && + enumType.IsEnum + ? Enum.ToObject( + enumType, + value.scalarValue) + : MissingValue; + case ValueKind.ObjectReference: + return value.objectId >= 0 && + value.objectId < nodes.Count + ? nodes[value.objectId].instance + : MissingValue; + case ValueKind.Array: + return ConvertArray(value, targetType, nodes); + case ValueKind.Struct: + return ConvertStruct(value, targetType, nodes); + default: + return MissingValue; + } + } + + private static object ConvertScalar( + object value, + Type targetType) + { + if (targetType == null || + value == null) + { + return value; + } + + if (targetType.IsEnum) + { + return Enum.ToObject(targetType, value); + } + + Type valueType = value.GetType(); + if (targetType.IsAssignableFrom(valueType)) + { + return value; + } + + try + { + return Convert.ChangeType( + value, + targetType, + CultureInfo.InvariantCulture); + } + catch + { + return MissingValue; + } + } + + private static object ConvertArray( + ValueRecord value, + Type targetType, + List nodes) + { + Type elementType = + ResolveType( + value.assemblyName, + value.typeName); + if (elementType == null && + targetType != null && + targetType.IsArray) + { + elementType = targetType.GetElementType(); + } + + if (elementType == null || + value.elements == null) + { + return MissingValue; + } + + Array array = Array.CreateInstance( + elementType, + value.elements.Count); + for (int i = 0; i < value.elements.Count; ++i) + { + object elementValue = ConvertValue( + value.elements[i], + elementType, + nodes); + if (!ReferenceEquals(elementValue, MissingValue)) + { + array.SetValue(elementValue, i); + } + } + + return array; + } + + private static object ConvertStruct( + ValueRecord value, + Type targetType, + List nodes) + { + Type type = + ResolveType( + value.assemblyName, + value.typeName) ?? + targetType; + if (type == null || + !type.IsValueType) + { + return MissingValue; + } + + object instance = Activator.CreateInstance(type); + ApplyFields(instance, value.fields, nodes); + return instance; + } + + private static object CreateObjectInstance( + Type type) + { + if (type == null || + type.IsAbstract) + { + return null; + } + + try + { + return Activator.CreateInstance( + type, + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic, + binder: null, + args: new object[0], + culture: CultureInfo.InvariantCulture); + } + catch + { + return null; + } + } + + private static FieldInfo ResolveField( + FieldRecord fieldRecord) + { + Type declaringType = + ResolveType( + fieldRecord.declaringAssemblyName, + fieldRecord.declaringTypeName); + return declaringType != null + ? declaringType.GetField( + fieldRecord.name, + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic | + BindingFlags.DeclaredOnly) + : null; + } + + private static void WriteType( + BinaryWriter writer, + Type type) + { + writer.Write( + type != null + ? type.Assembly.GetName().Name + : string.Empty); + writer.Write( + type != null + ? type.FullName ?? string.Empty + : string.Empty); + } + + private static void ReadType( + BinaryReader reader, + out string assemblyName, + out string typeName) + { + assemblyName = reader.ReadString(); + typeName = reader.ReadString(); + } + + private static Type ResolveType( + string assemblyName, + string typeName) + { + if (string.IsNullOrEmpty(typeName)) + { + return null; + } + + Type type = null; + if (!string.IsNullOrEmpty(assemblyName)) + { + type = Type.GetType( + typeName + ", " + assemblyName, + false); + if (type != null) + { + return type; + } + } + + Assembly[] assemblies = + AppDomain.CurrentDomain.GetAssemblies(); + for (int i = 0; i < assemblies.Length; ++i) + { + Assembly assembly = assemblies[i]; + if (!string.IsNullOrEmpty(assemblyName) && + assembly.GetName().Name != assemblyName) + { + continue; + } + + type = assembly.GetType(typeName, false); + if (type != null) + { + return type; + } + } + + return null; + } + + private static FieldInfo[] GetSerializableFields( + Type type) + { + List fields = new List(); + for (Type currentType = type; + currentType != null && + currentType != typeof(object); + currentType = currentType.BaseType) + { + FieldInfo[] declaredFields = + currentType.GetFields( + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic | + BindingFlags.DeclaredOnly); + for (int i = 0; i < declaredFields.Length; ++i) + { + FieldInfo field = declaredFields[i]; + if (ShouldSerializeField(field)) + { + fields.Add(field); + } + } + } + + fields.Sort(CompareFields); + return fields.ToArray(); + } + + private static int CompareFields( + FieldInfo lhs, + FieldInfo rhs) + { + string lhsType = + lhs.DeclaringType != null + ? lhs.DeclaringType.FullName + : string.Empty; + string rhsType = + rhs.DeclaringType != null + ? rhs.DeclaringType.FullName + : string.Empty; + int result = string.CompareOrdinal(lhsType, rhsType); + if (result != 0) + { + return result; + } + + result = lhs.MetadataToken.CompareTo(rhs.MetadataToken); + return result != 0 + ? result + : string.CompareOrdinal(lhs.Name, rhs.Name); + } + + private static bool ShouldSerializeField( + FieldInfo field) + { + if (field == null || + field.IsStatic || + field.IsLiteral || + field.IsInitOnly || + field.IsNotSerialized) + { + return false; + } + + return field.IsPublic || + Attribute.IsDefined( + field, + typeof(SerializeField), + true); + } + + private static bool CanSerializeValue( + Type declaredType, + object value) + { + Type type = value != null + ? value.GetType() + : declaredType; + if (type == null || + type.IsPointer || + type.IsGenericTypeDefinition) + { + return false; + } + + if (IsScalarType(type) || + type.IsEnum) + { + return true; + } + + if (type.IsArray) + { + Type elementType = type.GetElementType(); + return elementType != null && + CanSerializeValue(elementType, null); + } + + if (type.IsValueType) + { + return true; + } + + return CanSerializeReferenceType(type); + } + + private static void EnsureSerializableFieldValue( + FieldInfo field, + object value) + { + if (field != null && + !CanSerializeValue( + field.FieldType, + value)) + { + string ownerType = + field.DeclaringType != null + ? field.DeclaringType.FullName + : string.Empty; + throw new NotSupportedException( + "Unsupported serialized field type: " + + ownerType + "." + + field.Name + " (" + + field.FieldType.FullName + ")"); + } + } + + private static void EnsureSerializableArrayElementValue( + Type elementType, + object value) + { + if (!CanSerializeValue(elementType, value)) + { + throw new NotSupportedException( + "Unsupported serialized array element type: " + + (value != null + ? value.GetType().FullName + : elementType != null + ? elementType.FullName + : string.Empty)); + } + } + + private static bool CanSerializeReferenceType( + Type type) + { + return type != null && + type.IsClass && + type != typeof(string) && + type != typeof(Type) && + !typeof(Delegate).IsAssignableFrom(type) && + (type.Namespace == null || + !type.Namespace.StartsWith( + "System", + StringComparison.Ordinal)); + } + + private static bool IsScalarType( + Type type) + { + return type == typeof(bool) || + type == typeof(byte) || + type == typeof(sbyte) || + type == typeof(short) || + type == typeof(ushort) || + type == typeof(int) || + type == typeof(uint) || + type == typeof(long) || + type == typeof(ulong) || + type == typeof(float) || + type == typeof(double) || + type == typeof(string); + } + } +} diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 97074b51..fb1864f6 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -529,6 +529,156 @@ TEST_F( "Gameplay.RenderPipelineApiProbeAsset"); } +TEST_F( + MonoScriptRuntimeTest, + ManagedGraphicsSettingsMaterializesSerializedUniversalAssetGraphWithoutExistingHandle) { + Scene* runtimeScene = + CreateScene("SerializedUniversalRenderPipelineAssetGraphScene"); + GameObject* scriptObject = + runtimeScene->CreateGameObject( + "SerializedUniversalRenderPipelineAssetGraphProbe"); + ScriptComponent* script = + AddScript( + scriptObject, + "Gameplay", + "SerializedUniversalRenderPipelineAssetGraphProbe"); + ASSERT_NE(script, nullptr); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor + descriptor = + XCEngine::Rendering::Pipelines:: + GetConfiguredManagedRenderPipelineAssetDescriptor(); + ASSERT_TRUE(descriptor.IsValid()); + ASSERT_EQ( + descriptor.assemblyName, + "XCEngine.RenderPipelines.Universal"); + ASSERT_EQ( + descriptor.namespaceName, + "XCEngine.Rendering.Universal"); + ASSERT_EQ( + descriptor.className, + "UniversalRenderPipelineAsset"); + ASSERT_NE(descriptor.managedAssetHandle, 0u); + ASSERT_FALSE(descriptor.serializedAssetGraph.empty()); + + runtime->ReleaseExternalManagedObject(descriptor.managedAssetHandle); + descriptor.managedAssetHandle = 0u; + XCEngine::Rendering::Pipelines:: + SetConfiguredManagedRenderPipelineAssetDescriptor(descriptor); + + { + const auto bridge = + XCEngine::Rendering::Pipelines::GetManagedRenderPipelineBridge(); + ASSERT_NE(bridge, nullptr); + + std::shared_ptr + assetRuntime = bridge->CreateAssetRuntime(descriptor); + ASSERT_NE(assetRuntime, nullptr); + + XCEngine::Rendering::FinalColorSettings settings = {}; + ASSERT_TRUE(assetRuntime->TryGetDefaultFinalColorSettings(settings)); + EXPECT_FLOAT_EQ(settings.finalColorScale.x, 0.90f); + EXPECT_FLOAT_EQ(settings.finalColorScale.y, 1.10f); + EXPECT_FLOAT_EQ(settings.finalColorScale.z, 1.20f); + EXPECT_FLOAT_EQ(settings.finalColorScale.w, 1.0f); + + std::unique_ptr + recorder = assetRuntime->CreateStageRecorder(); + ASSERT_NE(recorder, nullptr); + const XCEngine::Rendering::RenderContext context = {}; + ASSERT_TRUE(recorder->Initialize(context)); + EXPECT_TRUE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::MainScene)); + EXPECT_FALSE( + recorder->SupportsStageRenderGraph( + XCEngine::Rendering::CameraFrameStage::PostProcess)); + recorder->Shutdown(); + } + + engine->OnUpdate(0.016f); + engine->OnUpdate(0.016f); + EXPECT_TRUE(runtime->GetLastError().empty()) << runtime->GetLastError(); + + const XCEngine::Rendering::Pipelines::ManagedRenderPipelineAssetDescriptor + resolvedDescriptor = + XCEngine::Rendering::Pipelines:: + GetConfiguredManagedRenderPipelineAssetDescriptor(); + EXPECT_NE(resolvedDescriptor.managedAssetHandle, 0u); + EXPECT_FALSE(resolvedDescriptor.serializedAssetGraph.empty()); + + int updateCount = 0; + bool observedAssetWasNull = true; + int observedDefaultRendererIndex = -1; + int observedRendererDataCount = 0; + int observedRendererFeatureCount = 0; + bool observedFeatureActive = true; + std::string observedFeatureTypeName; + XCEngine::Math::Vector4 observedColorScale = {}; + bool observedShadowsSupportMainLight = true; + XCEngine::Math::Vector4 observedFinalColorScale = {}; + EXPECT_TRUE(runtime->TryGetFieldValue(script, "UpdateCount", updateCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedAssetWasNull", + observedAssetWasNull)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedDefaultRendererIndex", + observedDefaultRendererIndex)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedRendererDataCount", + observedRendererDataCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedRendererFeatureCount", + observedRendererFeatureCount)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedFeatureActive", + observedFeatureActive)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedFeatureTypeName", + observedFeatureTypeName)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedColorScale", + observedColorScale)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedShadowsSupportMainLight", + observedShadowsSupportMainLight)); + EXPECT_TRUE(runtime->TryGetFieldValue( + script, + "ObservedFinalColorScale", + observedFinalColorScale)); + + EXPECT_GE(updateCount, 2); + EXPECT_FALSE(observedAssetWasNull); + EXPECT_EQ(observedDefaultRendererIndex, 0); + EXPECT_EQ(observedRendererDataCount, 1); + EXPECT_EQ(observedRendererFeatureCount, 1); + EXPECT_FALSE(observedFeatureActive); + EXPECT_EQ( + observedFeatureTypeName, + "XCEngine.Rendering.Universal.ColorScalePostProcessRendererFeature"); + EXPECT_FLOAT_EQ(observedColorScale.x, 0.42f); + EXPECT_FLOAT_EQ(observedColorScale.y, 0.51f); + EXPECT_FLOAT_EQ(observedColorScale.z, 0.63f); + EXPECT_FLOAT_EQ(observedColorScale.w, 1.0f); + EXPECT_FALSE(observedShadowsSupportMainLight); + EXPECT_FLOAT_EQ(observedFinalColorScale.x, 0.90f); + EXPECT_FLOAT_EQ(observedFinalColorScale.y, 1.10f); + EXPECT_FLOAT_EQ(observedFinalColorScale.z, 1.20f); + EXPECT_FLOAT_EQ(observedFinalColorScale.w, 1.0f); +} + TEST_F( MonoScriptRuntimeTest, RuntimeStopClearsManagedGraphicsSettingsSelection) {