From a93b6765cf3a26ff752662bdc85479a15116f444 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 --- .../ROUtils/ProceduralTools/DragCubeTool.cs | 427 ++++++++++++++++++ .../ProceduralTools/DragCubeToolEvents.cs | 21 + Source/ROUtils/Properties/AssemblyInfo.cs | 8 +- Source/ROUtils/ROUtils.csproj | 2 + Source/ROUtils/Utils/ModUtils.cs | 14 +- 5 files changed, 467 insertions(+), 5 deletions(-) create mode 100644 Source/ROUtils/ProceduralTools/DragCubeTool.cs create mode 100644 Source/ROUtils/ProceduralTools/DragCubeToolEvents.cs diff --git a/Source/ROUtils/ProceduralTools/DragCubeTool.cs b/Source/ROUtils/ProceduralTools/DragCubeTool.cs new file mode 100644 index 0000000..c455b33 --- /dev/null +++ b/Source/ROUtils/ProceduralTools/DragCubeTool.cs @@ -0,0 +1,427 @@ +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 readonly HashSet _inProgressMultiCubeRenderings = new HashSet(); + private static bool _statsRoutineStarted = false; + private static uint _cubesRenderedThisFrame = 0; + private static uint _cacheHitsThisFrame = 0; + private static long _elapsedTicks = 0; + + private string _shapeKey; + private Coroutine _multiCubeRoutine; + + /// + /// 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; + 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); + } + + internal static void ClearStaticState() + { + + _inProgressMultiCubeRenderings.Clear(); + _statsRoutineStarted = false; + _cubesRenderedThisFrame = 0; + _cacheHitsThisFrame = 0; + _elapsedTicks = 0; + } + + public void FixedUpdate() + { + if (Part == null) + { + // Somehow part can become null when doing cloning in symmetry + Destroy(this); + return; + } + + if (_multiCubeRoutine == null && 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() + { + // Assume a single animation module + IMultipleDragCube multiCube = Part.FindModuleImplementing(); + if (multiCube?.IsMultipleCubesActive ?? false) + { + Debug.Log($"[DragCubeTool] Need to render multiple cubes for {Part.partInfo.name}"); + if (!UseCache || _shapeKey == null || !TryUpdateMultiCubesFromCache(multiCube)) + { + // Render over multiple frames for animated parts + _multiCubeRoutine = StartCoroutine(UpdateMultiCubesRoutine()); + } + } + else + { + UpdateCubes(Part, _shapeKey); + Destroy(this); + } + } + + /// + /// If all cubes are cached then these can be assigned on the same frame. + /// + /// + /// + private bool TryUpdateMultiCubesFromCache(IMultipleDragCube multiCube) + { + long startTicks = System.Diagnostics.Stopwatch.GetTimestamp(); + List newCubeList = new List(); + foreach (string cName in multiCube.GetDragCubeNames()) + { + string shapeKey = $"{_shapeKey}${cName}"; + if (!_cacheDict.TryGetValue(shapeKey, out DragCube dragCube)) + { + _elapsedTicks += System.Diagnostics.Stopwatch.GetTimestamp() - startTicks; + return false; + } + else + { + _cacheHitsThisFrame++; + dragCube = CloneCube(dragCube); + newCubeList.Add(dragCube); + } + } + + if (ValidateCubes) + { + // Validation will run the multicube generation routine which will in turn assign these to the part as well + _elapsedTicks += System.Diagnostics.Stopwatch.GetTimestamp() - startTicks; + _multiCubeRoutine = StartCoroutine(MultiCubeValidationRoutine(newCubeList)); + } + else + { + AssignMultiCubes(newCubeList); + + _elapsedTicks += System.Diagnostics.Stopwatch.GetTimestamp() - startTicks; + + NotifyFARIfNeeded(Part); + EnsureStatsRoutineStarted(Part); + + Destroy(this); + } + + return true; + } + + private IEnumerator UpdateMultiCubesRoutine(bool isValidation = false) + { + long startTicks = System.Diagnostics.Stopwatch.GetTimestamp(); + Part p = Instantiate(Part, Vector3.zero, Quaternion.identity); + GameObject gameObject = p.gameObject; + bool hasVessel = p.GetComponent() != null; + if (hasVessel) + p.vessel.mapObject = null; + + DragCubeSystem.Instance.SetupPartForRender(p, gameObject); + IMultipleDragCube multiCube = p.FindModuleImplementing(); + + List newCubeList = new List(); + foreach (string cName in multiCube.GetDragCubeNames()) + { + string shapeKey = _shapeKey == null ? null : $"{_shapeKey}${cName}"; + bool alreadyInProgress = !_inProgressMultiCubeRenderings.Add(shapeKey); + if (!alreadyInProgress || isValidation) + { + try + { + Debug.Log($"[DragCubeTool] Rendering pos {cName}"); + multiCube.AssumeDragCubePosition(cName); + + _elapsedTicks += System.Diagnostics.Stopwatch.GetTimestamp() - startTicks; + EnsureStatsRoutineStarted(Part); + + // Animations do not propagate to the meshes immediately so need to wait for the next frame before rendering + yield return null; + + startTicks = System.Diagnostics.Stopwatch.GetTimestamp(); + + DragCube dragCube = DragCubeSystem.Instance.RenderProceduralDragCube(p); + dragCube.Name = cName; + newCubeList.Add(dragCube); + _cubesRenderedThisFrame++; + AddCubeToCache(dragCube, shapeKey); + } + finally + { + _inProgressMultiCubeRenderings.Remove(shapeKey); + } + } + else + { + // TODO: shouldn't instantiate a new part in this case. Kinda painful to implement though. + _elapsedTicks += System.Diagnostics.Stopwatch.GetTimestamp() - startTicks; + + DragCube dragCube; + do + { + Debug.Log($"[DragCubeTool] Rendering {cName} already in progress, waiting..."); + yield return null; + } + while (!_cacheDict.TryGetValue(shapeKey, out dragCube)); + + dragCube = CloneCube(dragCube); + startTicks = System.Diagnostics.Stopwatch.GetTimestamp(); + Debug.Log($"[DragCubeTool] Finished waiting for {cName}"); + newCubeList.Add(dragCube); + } + } + + gameObject.SetActive(false); + Destroy(gameObject); + if (hasVessel) + FlightCamera.fetch.CycleCameraHighlighter(); + + AssignMultiCubes(newCubeList); + + _elapsedTicks += System.Diagnostics.Stopwatch.GetTimestamp() - startTicks; + + NotifyFARIfNeeded(Part); + EnsureStatsRoutineStarted(Part); + + Destroy(this); + } + + private static void UpdateCubes(Part p, string shapeKey = null) + { + 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++; + AddCubeToCache(dragCube, shapeKey); + } + 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(); + + NotifyFARIfNeeded(p); + EnsureStatsRoutineStarted(p); + } + + private static void AddCubeToCache(DragCube dragCube, string shapeKey) + { + 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; + } + } + + private static void NotifyFARIfNeeded(Part p) + { + if (ModUtils.IsFARInstalled) + p.SendMessage("GeometryPartModuleRebuildMeshData"); + } + + private static void EnsureStatsRoutineStarted(Part p) + { + if (!_statsRoutineStarted && PartLoader.Instance.IsReady()) + 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 && _inProgressMultiCubeRenderings.Count == 0) + { + Debug.Log($"[DragCubeTool] Cache limit reached ({_cacheDict.Count} / {MaxCacheSize}), emptying..."); + _cacheDict.Clear(); + } + } + + private void AssignMultiCubes(List cubes) + { + if (Part.DragCubes.Cubes.Count == cubes.Count) + { + // Copy over weights from part cubes. Most likely these were already updated to reflect animation state. + foreach (DragCube c in cubes) + { + DragCube c2 = Part.DragCubes.GetCube(c.name); + if (c2 != null) + c.Weight = c2.Weight; + } + } + + Part.DragCubes.ClearCubes(); + Part.DragCubes.Cubes.AddRange(cubes); + Part.DragCubes.ForceUpdate(true, true, false); + Part.DragCubes.SetDragWeights(); + } + + 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); + RunCubeValidation(cacheCube, renderedCube, p, shapeKey); + } + + private IEnumerator MultiCubeValidationRoutine(List cacheCubeList) + { + yield return UpdateMultiCubesRoutine(isValidation: true); + + IMultipleDragCube multiCube = Part.FindModuleImplementing(); + string[] names = multiCube.GetDragCubeNames(); + if (names.Length != cacheCubeList.Count) + { + Debug.LogError($"[DragCubeTool] Cube count mismatch in MultiCubeValidationRoutine"); + yield break; + } + + for (int i = 0; i < names.Length; i++) + { + string cName = names[i]; + string shapeKey = $"{_shapeKey}${cName}"; + if (!_cacheDict.TryGetValue(shapeKey, out DragCube dragCube)) + { + // cache got cleared? + Debug.LogWarning($"[DragCubeTool] Failed to fetch {shapeKey} from cache in MultiCubeValidationRoutine"); + yield break; + } + + RunCubeValidation(cacheCubeList[i], dragCube, Part, shapeKey); + } + } + + private static void RunCubeValidation(DragCube cacheCube, DragCube renderedCube, Part p, string shapeKey) + { + // 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 (cacheCube.name != renderedCube.name || + !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/ProceduralTools/DragCubeToolEvents.cs b/Source/ROUtils/ProceduralTools/DragCubeToolEvents.cs new file mode 100644 index 0000000..d6bc6ad --- /dev/null +++ b/Source/ROUtils/ProceduralTools/DragCubeToolEvents.cs @@ -0,0 +1,21 @@ +using UnityEngine.SceneManagement; + +namespace ROUtils +{ + internal class DragCubeToolEvents : HostedSingleton + { + public DragCubeToolEvents(SingletonHost host) : base(host) + { + } + + public override void Awake() + { + SceneManager.sceneLoaded += SceneLoaded; + } + + private void SceneLoaded(Scene scene, LoadSceneMode mode) + { + DragCubeTool.ClearStaticState(); + } + } +} 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..9c555fa 100644 --- a/Source/ROUtils/ROUtils.csproj +++ b/Source/ROUtils/ROUtils.csproj @@ -122,11 +122,13 @@ + + 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;