From 82183fb5330c03bba832f82f06cfe68bb949f93d Mon Sep 17 00:00:00 2001 From: siimav Date: Sun, 1 Sep 2024 22:51:55 +0300 Subject: [PATCH] Add common DragCubeTool, now with caching --- Source/ROUtils/Properties/AssemblyInfo.cs | 8 +- Source/ROUtils/ROUtils.csproj | 1 + Source/ROUtils/Utils/DragCubeTool.cs | 207 ++++++++++++++++++++++ Source/ROUtils/Utils/ModUtils.cs | 14 +- 4 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 Source/ROUtils/Utils/DragCubeTool.cs diff --git a/Source/ROUtils/Properties/AssemblyInfo.cs b/Source/ROUtils/Properties/AssemblyInfo.cs index a1e6684..1f2d9b3 100644 --- a/Source/ROUtils/Properties/AssemblyInfo.cs +++ b/Source/ROUtils/Properties/AssemblyInfo.cs @@ -33,13 +33,13 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.1.0")] // Don't change for every release +[assembly: AssemblyVersion("1.1.0.0")] // Don't change for every release #if CIBUILD [assembly: AssemblyFileVersion("@MAJOR@.@MINOR@.@PATCH@.@BUILD@")] [assembly: KSPAssembly("ROUtils", @MAJOR@, @MINOR@, @PATCH@)] #else -[assembly: AssemblyFileVersion("1.0.1.0")] -[assembly: KSPAssembly("ROUtils", 1, 0, 1)] +[assembly: AssemblyFileVersion("1.1.0.0")] +[assembly: KSPAssembly("ROUtils", 1, 1, 0)] #endif -[assembly: KSPAssemblyDependency("KSPCommunityFixes", 1, 22, 1)] \ No newline at end of file +[assembly: KSPAssemblyDependency("KSPCommunityFixes", 1, 22, 1)] diff --git a/Source/ROUtils/ROUtils.csproj b/Source/ROUtils/ROUtils.csproj index c916fd8..a635878 100644 --- a/Source/ROUtils/ROUtils.csproj +++ b/Source/ROUtils/ROUtils.csproj @@ -127,6 +127,7 @@ + diff --git a/Source/ROUtils/Utils/DragCubeTool.cs b/Source/ROUtils/Utils/DragCubeTool.cs new file mode 100644 index 0000000..6d885d3 --- /dev/null +++ b/Source/ROUtils/Utils/DragCubeTool.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.Profiling; + +namespace ROUtils +{ + /// + /// Common tool for various procedural part mods for generating drag cubes. + /// + public class DragCubeTool : MonoBehaviour + { + private static readonly Dictionary _cacheDict = new Dictionary(); + private static bool _statsRoutineStarted = false; + private static uint _cubesRenderedThisFrame = 0; + private static uint _cacheHitsThisFrame = 0; + private static long _elapsedTicks = 0; + + private string _shapeKey; + + /// + /// Globally enable of disable drag cube caching. + /// + public static bool UseCache { get; set; } = true; + /// + /// Whether to validate all cubes that got fetched from cache against freshly-rendered ones. + /// + public static bool ValidateCubes { get; set; } = false; + /// + /// Max number of items in cache. Once that number is reached then the cache is cleared entirely. + /// + public static uint MaxCacheSize { get; set; } = 5000; + + public Part Part { get; private set; } + + /// + /// Creates and assigns a drag cube for the given procedural part. + /// This process can have one to many frames of delay. + /// + /// Part to create drag cube for + /// Key that uniquely identifies the geometry of the part.Used in caching logic. Use null if no caching is desired. + /// + public static DragCubeTool UpdateDragCubes(Part p, string shapeKey = null) + { + var tool = p.GetComponent(); + if (tool == null) + { + tool = p.gameObject.AddComponent(); + tool.Part = p; + tool._shapeKey = shapeKey; + } + tool._shapeKey = shapeKey; + return tool; + } + + /// + /// Creates and assigns a drag cube for the given procedural part. + /// Use only when you know that the part is ready for drag cube rendering. Otherwise use UpdateDragCubes. + /// + /// Part to create drag cube for + /// Key that uniquely identifies the geometry of the part.Used in caching logic. Use null if no caching is desired. + /// Thrown when the part is not yet ready for drag cube rendering + public static void UpdateDragCubesImmediate(Part p, string shapeKey = null) + { + if (!Ready(p)) + throw new InvalidOperationException("Not ready for drag cube rendering yet"); + + UpdateCubes(p, shapeKey); + } + + public void FixedUpdate() + { + if (Ready()) + UpdateCubes(); + } + + public bool Ready() => Ready(Part); + + private static bool Ready(Part p) + { + if (HighLogic.LoadedSceneIsFlight) + return FlightGlobals.ready; + if (HighLogic.LoadedSceneIsEditor) + return p.localRoot == EditorLogic.RootPart && p.gameObject.layer != LayerMask.NameToLayer("TransparentFX"); + return true; + } + + private void UpdateCubes() + { + UpdateCubes(Part, _shapeKey); + Destroy(this); + } + + private static void UpdateCubes(Part p, string shapeKey = null) + { + if (ModUtils.IsFARInstalled) + p.SendMessage("GeometryPartModuleRebuildMeshData"); + + Profiler.BeginSample("UpdateCubes"); + long startTicks = System.Diagnostics.Stopwatch.GetTimestamp(); + if (!UseCache || shapeKey == null || !_cacheDict.TryGetValue(shapeKey, out DragCube dragCube)) + { + dragCube = DragCubeSystem.Instance.RenderProceduralDragCube(p); + _cubesRenderedThisFrame++; + + if (UseCache && shapeKey != null && PartLoader.Instance.IsReady()) + { + // Keep a pristine copy in cache. I.e the instance must not be be used by a part. + DragCube clonedCube = CloneCube(dragCube); + _cacheDict[shapeKey] = clonedCube; + } + } + else + { + _cacheHitsThisFrame++; + dragCube = CloneCube(dragCube); + if (ValidateCubes) + RunCubeValidation(p, dragCube, shapeKey); + } + + p.DragCubes.ClearCubes(); + p.DragCubes.Cubes.Add(dragCube); + p.DragCubes.ResetCubeWeights(); + p.DragCubes.ForceUpdate(true, true, false); + p.DragCubes.SetDragWeights(); + + _elapsedTicks += System.Diagnostics.Stopwatch.GetTimestamp() - startTicks; + Profiler.EndSample(); + + if (!_statsRoutineStarted) + p.StartCoroutine(StatsCoroutine()); + } + + private static IEnumerator StatsCoroutine() + { + _statsRoutineStarted = true; + yield return new WaitForEndOfFrame(); + _statsRoutineStarted = false; + + double timeMs = _elapsedTicks / (System.Diagnostics.Stopwatch.Frequency / 1000d); + Debug.Log($"[DragCubeTool] Rendered {_cubesRenderedThisFrame} cubes; fetched {_cacheHitsThisFrame} from cache; exec time: {timeMs:F1}ms"); + _cacheHitsThisFrame = 0; + _cubesRenderedThisFrame = 0; + _elapsedTicks = 0; + + if (_cacheDict.Count > MaxCacheSize) + { + Debug.Log($"[DragCubeTool] Cache limit reached ({_cacheDict.Count} / {MaxCacheSize}), emptying..."); + _cacheDict.Clear(); + } + } + + private static DragCube CloneCube(DragCube dragCube) + { + return new DragCube + { + area = dragCube.area, + drag = dragCube.drag, + depth = dragCube.depth, + dragModifiers = dragCube.dragModifiers, + center = dragCube.center, + size = dragCube.size, + name = dragCube.name + }; + } + + private static void RunCubeValidation(Part p, DragCube cacheCube, string shapeKey) + { + DragCube renderedCube = DragCubeSystem.Instance.RenderProceduralDragCube(p); + + // drag components randomly switch places so sort the arrays before comparing + var cacheSortedDrag = cacheCube.drag.OrderBy(v => v).ToArray(); + var renderSortedDrag = renderedCube.drag.OrderBy(v => v).ToArray(); + + if (!ArraysNearlyEqual(cacheCube.area, renderedCube.area, 0.005f) || + !ArraysNearlyEqual(cacheSortedDrag, renderSortedDrag, 0.05f) || + !ArraysNearlyEqual(cacheCube.depth, renderedCube.depth, 0.01f) || + !ArraysNearlyEqual(cacheCube.dragModifiers, renderedCube.dragModifiers, 0.005f) || + !VectorsNearlyEqual(cacheCube.center, renderedCube.center, 0.005f) || + !VectorsNearlyEqual(cacheCube.size, renderedCube.size, 0.005f)) + { + Debug.LogError($"[DragCubeTool] Mismatch in cached cube for part {p.partInfo.name}, key {shapeKey}:"); + Debug.LogError($"Cache: {cacheCube.SaveToString()}"); + Debug.LogError($"Renderd: {renderedCube.SaveToString()}"); + } + } + + private static bool ArraysNearlyEqual(float[] arr1, float[] arr2, float tolerance) + { + for (int i = 0; i < arr1.Length; i++) + { + float a = arr1[i]; + float b = arr2[i]; + if (Math.Abs(a - b) > tolerance) + return false; + } + return true; + } + + private static bool VectorsNearlyEqual(Vector3 v1, Vector3 v2, float tolerance) + { + return (v1 - v2).sqrMagnitude < tolerance * tolerance; + } + } +} diff --git a/Source/ROUtils/Utils/ModUtils.cs b/Source/ROUtils/Utils/ModUtils.cs index 20259c7..693ffdd 100644 --- a/Source/ROUtils/Utils/ModUtils.cs +++ b/Source/ROUtils/Utils/ModUtils.cs @@ -24,7 +24,19 @@ public static bool IsRP1Installed } } - + private static bool? _isFARInstalled; + public static bool IsFARInstalled + { + get + { + if (!_isFARInstalled.HasValue) + { + _isFARInstalled = AssemblyLoader.loadedAssemblies.Any(a => a.assembly.GetName().Name == "FerramAerospaceResearch"); + } + return _isFARInstalled.Value; + } + } + private static bool? _isTestFlightInstalled = null; private static bool? _isTestLiteInstalled = null;