From 12d9d94138d0e987a00ff5956279f1dcdeec84a2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 4 Dec 2024 13:30:56 +0100 Subject: [PATCH] Add support for curves in C++ scripting api #2546 --- Source/Engine/Animations/Curve.cs | 84 +++++++++++++++++++ Source/Engine/Animations/Curve.h | 24 ++++-- Source/Engine/Level/Actors/Spline.cpp | 4 +- .../Bindings/BindingsGenerator.CSharp.cs | 20 +++-- .../Bindings/BindingsGenerator.Cpp.cs | 22 +++-- 5 files changed, 131 insertions(+), 23 deletions(-) diff --git a/Source/Engine/Animations/Curve.cs b/Source/Engine/Animations/Curve.cs index 7632653bc..4c47dbb6e 100644 --- a/Source/Engine/Animations/Curve.cs +++ b/Source/Engine/Animations/Curve.cs @@ -1,8 +1,10 @@ // Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. +using FlaxEngine.Interop; using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace FlaxEngine @@ -454,6 +456,40 @@ namespace FlaxEngine time = end; } } + + /// + /// Raw memory copy (used by scripting bindings - see MarshalAs tag). + /// + /// The keyframes array. + /// The raw keyframes data. + protected static unsafe byte[] MarshalKeyframes(Keyframe[] keyframes) + { + if (keyframes == null || keyframes.Length == 0) + return null; + var keyframeSize = Unsafe.SizeOf(); + var result = new byte[keyframes.Length * keyframeSize]; + fixed (byte* resultPtr = result) + { + var keyframesHandle = ManagedHandle.Alloc(keyframes, GCHandleType.Pinned); + var keyframesPtr = Marshal.UnsafeAddrOfPinnedArrayElement(keyframes, 0); + Buffer.MemoryCopy((void*)keyframesPtr, resultPtr, (uint)result.Length, (uint)result.Length); + keyframesHandle.Free(); + } + return result; + } + + /// + /// Raw memory copy (used by scripting bindings - see MarshalAs tag). + /// + /// The raw keyframes data. + /// The keyframes array. + protected static unsafe Keyframe[] MarshalKeyframes(byte[] raw) where Keyframe : struct + { + if (raw == null || raw.Length == 0) + return null; + fixed (byte* rawPtr = raw) + return MemoryMarshal.Cast(new Span(rawPtr, raw.Length)).ToArray(); + } } /// @@ -709,6 +745,30 @@ namespace FlaxEngine leftKey = Mathf.Max(0, start - 1); rightKey = Mathf.Min(start, Keyframes.Length - 1); } + + /// + /// Raw memory copy (used by scripting bindings - see MarshalAs tag). + /// + /// The curve to copy. + /// The raw keyframes data. + public static unsafe implicit operator byte[](LinearCurve curve) + { + if (curve == null) + return null; + return MarshalKeyframes(curve.Keyframes); + } + + /// + /// Raw memory copy (used by scripting bindings - see MarshalAs tag). + /// + /// The raw keyframes data. + /// The curve. + public static unsafe implicit operator LinearCurve(byte[] raw) + { + if (raw == null || raw.Length == 0) + return null; + return new LinearCurve(MarshalKeyframes(raw)); + } } /// @@ -1000,5 +1060,29 @@ namespace FlaxEngine leftKey = Mathf.Max(0, start - 1); rightKey = Mathf.Min(start, Keyframes.Length - 1); } + + /// + /// Raw memory copy (used by scripting bindings - see MarshalAs tag). + /// + /// The curve to copy. + /// The raw keyframes data. + public static unsafe implicit operator byte[](BezierCurve curve) + { + if (curve == null) + return null; + return MarshalKeyframes(curve.Keyframes); + } + + /// + /// Raw memory copy (used by scripting bindings - see MarshalAs tag). + /// + /// The raw keyframes data. + /// The curve. + public static unsafe implicit operator BezierCurve(byte[] raw) + { + if (raw == null || raw.Length == 0) + return null; + return new BezierCurve(MarshalKeyframes(raw)); + } } } diff --git a/Source/Engine/Animations/Curve.h b/Source/Engine/Animations/Curve.h index b259dae66..e3e0be33e 100644 --- a/Source/Engine/Animations/Curve.h +++ b/Source/Engine/Animations/Curve.h @@ -501,7 +501,7 @@ protected: /// An animation spline represented by a set of keyframes, each representing an endpoint of a curve. /// template> -class Curve : public CurveBase +API_CLASS(InBuild, Template, MarshalAs=Span) class Curve : public CurveBase { public: typedef CurveBase Base; @@ -763,28 +763,42 @@ public: } return true; } + + // Raw memory copy (used by scripting bindings - see MarshalAs tag). + Curve& operator=(const Span& raw) + { + ASSERT((raw.Length() % sizeof(KeyFrame)) == 0); + const int32 count = raw.Length() / sizeof(KeyFrame); + _keyframes.Resize(count, false); + Platform::MemoryCopy(_keyframes.Get(), raw.Get(), sizeof(KeyFrame) * count); + return *this; + } + operator Span() + { + return Span((const byte*)_keyframes.Get(), _keyframes.Count() * sizeof(KeyFrame)); + } }; /// /// An animation spline represented by a set of keyframes, each representing a value point. /// template -using StepCurve = Curve>; +API_TYPEDEF() using StepCurve = Curve>; /// /// An animation spline represented by a set of keyframes, each representing an endpoint of a linear curve. /// template -using LinearCurve = Curve>; +API_TYPEDEF() using LinearCurve = Curve>; /// /// An animation spline represented by a set of keyframes, each representing an endpoint of a cubic hermite curve. /// template -using HermiteCurve = Curve>; +API_TYPEDEF() using HermiteCurve = Curve>; /// /// An animation spline represented by a set of keyframes, each representing an endpoint of Bezier curve. /// template -using BezierCurve = Curve>; +API_TYPEDEF() using BezierCurve = Curve>; diff --git a/Source/Engine/Level/Actors/Spline.cpp b/Source/Engine/Level/Actors/Spline.cpp index 8dd79c8ae..bc46c3da1 100644 --- a/Source/Engine/Level/Actors/Spline.cpp +++ b/Source/Engine/Level/Actors/Spline.cpp @@ -478,9 +478,7 @@ void Spline::GetKeyframes(MArray* data) void Spline::SetKeyframes(MArray* data) { - const int32 count = MCore::Array::GetLength(data); - Curve.GetKeyframes().Resize(count, false); - Platform::MemoryCopy(Curve.GetKeyframes().Get(), MCore::Array::GetAddress(data), sizeof(Keyframe) * count); + Curve = Span((const byte*)MCore::Array::GetAddress(data), MCore::Array::GetLength(data)); UpdateSpline(); } diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs index 551417ebf..5d03d0489 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs @@ -20,12 +20,12 @@ namespace Flax.Build.Bindings private static readonly Dictionary CSharpAdditionalCodeCache = new Dictionary(); #if USE_NETCORE private static readonly TypeInfo CSharpEventBindReturn = new TypeInfo("void"); - private static readonly List CSharpEventBindParams = new List() { new FunctionInfo.ParameterInfo() { Name = "bind", Type = new TypeInfo("bool") } }; + private static readonly List CSharpEventBindParams = new List { new FunctionInfo.ParameterInfo { Name = "bind", Type = new TypeInfo("bool") } }; #endif public static event Action GenerateCSharpTypeInternals; - internal static readonly Dictionary CSharpNativeToManagedBasicTypes = new Dictionary() + internal static readonly Dictionary CSharpNativeToManagedBasicTypes = new Dictionary { // Language types { "bool", "bool" }, @@ -46,7 +46,7 @@ namespace Flax.Build.Bindings { "double", "double" }, }; - internal static readonly Dictionary CSharpNativeToManagedDefault = new Dictionary() + internal static readonly Dictionary CSharpNativeToManagedDefault = new Dictionary { // Engine types { "String", "string" }, @@ -632,11 +632,11 @@ namespace Flax.Build.Bindings else if (returnValueType == "object[]") returnMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.SystemObjectArrayMarshaller))"; else if (functionInfo.ReturnType.Type == "Array" || functionInfo.ReturnType.Type == "Span" || functionInfo.ReturnType.Type == "DataContainer" || functionInfo.ReturnType.Type == "BytesContainer" || returnNativeType == "Array") - returnMarshalType = $"MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = nameof(__returnCount))"; + returnMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = nameof(__returnCount))"; else if (functionInfo.ReturnType.Type == "Dictionary") - returnMarshalType = $"MarshalUsing(typeof(FlaxEngine.Interop.DictionaryMarshaller<,>), ConstantElementCount = 0)"; + returnMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.DictionaryMarshaller<,>), ConstantElementCount = 0)"; else if (returnValueType == "byte[]") - returnMarshalType = $"MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = \"__returnCount\")"; + returnMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = \"__returnCount\")"; else if (returnValueType == "bool[]") { // Boolean arrays does not support custom marshalling for some unknown reason @@ -691,11 +691,15 @@ namespace Flax.Build.Bindings parameterMarshalType += ", In"; // The usage of 'LibraryImportAttribute' does not follow recommendations. It is recommended to use explicit '[In]' and '[Out]' attributes on array parameters. } else if (parameterInfo.Type.Type == "Dictionary") - parameterMarshalType = $"MarshalUsing(typeof(FlaxEngine.Interop.DictionaryMarshaller<,>), ConstantElementCount = 0)"; + parameterMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.DictionaryMarshaller<,>), ConstantElementCount = 0)"; else if (nativeType == "bool") parameterMarshalType = "MarshalAs(UnmanagedType.U1)"; else if (nativeType == "char") parameterMarshalType = "MarshalAs(UnmanagedType.I2)"; + else if (nativeType.EndsWith("[]")) + { + parameterMarshalType = $"MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>))"; + } if (!string.IsNullOrEmpty(parameterMarshalType)) contents.Append($"[{parameterMarshalType}] "); @@ -730,7 +734,7 @@ namespace Flax.Build.Bindings if (parameterInfo.Type.Type == "Array" || parameterInfo.Type.Type == "Span" || parameterInfo.Type.Type == "DataContainer" || parameterInfo.Type.Type == "BytesContainer") parameterMarshalType = $"MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = \"{parameterInfo.Name}Count\")"; else if (parameterInfo.Type.Type == "Dictionary") - parameterMarshalType = $"MarshalUsing(typeof(FlaxEngine.Interop.DictionaryMarshaller<,>), ConstantElementCount = 0)"; + parameterMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.DictionaryMarshaller<,>), ConstantElementCount = 0)"; } if (nativeType == "System.Type") parameterMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.SystemTypeMarshaller))"; diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 138291177..a189c363a 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -980,6 +980,10 @@ namespace Flax.Build.Bindings UseReferenceForResult = UsePassByReference(buildData, functionInfo.ReturnType, caller), CustomParameters = new List(), }; + var returnType = functionInfo.ReturnType; + var returnApiType = FindApiTypeInfo(buildData, returnType, caller); + if (returnApiType != null && returnApiType.MarshalAs != null) + returnType = returnApiType.MarshalAs; bool returnTypeIsContainer = false; var returnValueConvert = GenerateCppWrapperNativeToManaged(buildData, functionInfo.ReturnType, caller, out var returnValueType, functionInfo); @@ -999,7 +1003,7 @@ namespace Flax.Build.Bindings }); } #if USE_NETCORE - else if (functionInfo.ReturnType.Type == "Array" || functionInfo.ReturnType.Type == "Span" || functionInfo.ReturnType.Type == "DataContainer" || functionInfo.ReturnType.Type == "BitArray" || functionInfo.ReturnType.Type == "BytesContainer") + else if (returnType.Type == "Array" || returnType.Type == "Span" || returnType.Type == "DataContainer" || returnType.Type == "BitArray" || returnType.Type == "BytesContainer") { returnTypeIsContainer = true; functionInfo.Glue.CustomParameters.Add(new FunctionInfo.ParameterInfo @@ -1173,7 +1177,7 @@ namespace Flax.Build.Bindings { callBegin += "*__resultAsRef = "; } - else if (!functionInfo.ReturnType.IsVoid) + else if (!returnType.IsVoid) { if (useInlinedReturn) callBegin += "return "; @@ -1186,7 +1190,7 @@ namespace Flax.Build.Bindings if (returnTypeIsContainer) { callReturnCount = indent; - if (functionInfo.ReturnType.Type == "Span" || functionInfo.ReturnType.Type == "BytesContainer") + if (returnType.Type == "Span" || returnType.Type == "BytesContainer") callReturnCount += "*__returnCount = {0}.Length();"; else callReturnCount += "*__returnCount = {0}.Count();"; @@ -1278,7 +1282,8 @@ namespace Flax.Build.Bindings #if USE_NETCORE if (!string.IsNullOrEmpty(callReturnCount)) { - contents.Append(indent).Append("const auto& __callTemp = ").Append(string.Format(callFormat, call, callParams)).Append(";").AppendLine(); + var tempVar = returnTypeIsContainer && returnType != functionInfo.ReturnType ? $"{returnType} __callTemp = " : "const auto& __callTemp = "; + contents.Append(indent).Append(tempVar).Append(string.Format(callFormat, call, callParams)).Append(";").AppendLine(); call = "__callTemp"; contents.Append(string.Format(callReturnCount, call)); contents.AppendLine(); @@ -1357,7 +1362,7 @@ namespace Flax.Build.Bindings } } - if (!useInlinedReturn && !functionInfo.Glue.UseReferenceForResult && !functionInfo.ReturnType.IsVoid) + if (!useInlinedReturn && !functionInfo.Glue.UseReferenceForResult && !returnType.IsVoid) { contents.Append(indent).Append("return __result;").AppendLine(); } @@ -1817,6 +1822,10 @@ namespace Flax.Build.Bindings // Add includes to properly compile bindings (eg. SoftObjectReference) GenerateCppAddFileReference(buildData, caller, typeInfo, apiTypeInfo); + // TODO: find a better way to reference other include files for types that have separate serialization header + if (typeInfo.Type.EndsWith("Curve") && typeInfo.GenericArgs != null) + CppIncludeFilesList.Add("Engine/Animations/CurveSerialization.h"); + return false; } @@ -3126,13 +3135,12 @@ namespace Flax.Build.Bindings // Includes header.Clear(); CppReferencesFiles.Remove(null); - CppIncludeFilesList.Clear(); foreach (var fileInfo in CppReferencesFiles) CppIncludeFilesList.Add(fileInfo.Name); CppIncludeFilesList.AddRange(CppIncludeFiles); CppIncludeFilesList.Sort(); if (CppIncludeFilesList.Remove("Engine/Serialization/Serialization.h")) - CppIncludeFilesList.Add("Engine/Serialization/Serialization.h"); + CppIncludeFilesList.Add("Engine/Serialization/Serialization.h"); // Include serialization header as the last one to properly handle specialization of custom types serialization foreach (var path in CppIncludeFilesList) header.AppendFormat("#include \"{0}\"", path).AppendLine(); contents.Insert(headerPos, header.ToString());