Skip to content

Commit

Permalink
Add common DragCubeTool, now with caching
Browse files Browse the repository at this point in the history
  • Loading branch information
siimav committed Sep 4, 2024
1 parent 57562b4 commit 82183fb
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 5 deletions.
8 changes: 4 additions & 4 deletions Source/ROUtils/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
[assembly: KSPAssemblyDependency("KSPCommunityFixes", 1, 22, 1)]
1 change: 1 addition & 0 deletions Source/ROUtils/ROUtils.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
<Compile Include="CachedObject.cs" />
<Compile Include="SingletonHost.cs" />
<Compile Include="Utils\ContinuousLogger.cs" />
<Compile Include="Utils\DragCubeTool.cs" />
<Compile Include="Utils\DTUtils.cs" />
<Compile Include="Utils\HyperEdit_Utilities.cs" />
<Compile Include="Utils\KSPUtils.cs" />
Expand Down
207 changes: 207 additions & 0 deletions Source/ROUtils/Utils/DragCubeTool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Profiling;

namespace ROUtils
{
/// <summary>
/// Common tool for various procedural part mods for generating drag cubes.
/// </summary>
public class DragCubeTool : MonoBehaviour
{
private static readonly Dictionary<string, DragCube> _cacheDict = new Dictionary<string, DragCube>();
private static bool _statsRoutineStarted = false;
private static uint _cubesRenderedThisFrame = 0;
private static uint _cacheHitsThisFrame = 0;
private static long _elapsedTicks = 0;

private string _shapeKey;

/// <summary>
/// Globally enable of disable drag cube caching.
/// </summary>
public static bool UseCache { get; set; } = true;
/// <summary>
/// Whether to validate all cubes that got fetched from cache against freshly-rendered ones.
/// </summary>
public static bool ValidateCubes { get; set; } = false;
/// <summary>
/// Max number of items in cache. Once that number is reached then the cache is cleared entirely.
/// </summary>
public static uint MaxCacheSize { get; set; } = 5000;

public Part Part { get; private set; }

/// <summary>
/// Creates and assigns a drag cube for the given procedural part.
/// This process can have one to many frames of delay.
/// </summary>
/// <param name="p">Part to create drag cube for</param>
/// <param name="shapeKey">Key that uniquely identifies the geometry of the part.Used in caching logic. Use null if no caching is desired.</param>
/// <returns></returns>
public static DragCubeTool UpdateDragCubes(Part p, string shapeKey = null)
{
var tool = p.GetComponent<DragCubeTool>();
if (tool == null)
{
tool = p.gameObject.AddComponent<DragCubeTool>();
tool.Part = p;
tool._shapeKey = shapeKey;
}
tool._shapeKey = shapeKey;
return tool;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="p">Part to create drag cube for</param>
/// <param name="shapeKey">Key that uniquely identifies the geometry of the part.Used in caching logic. Use null if no caching is desired.</param>
/// <exception cref="InvalidOperationException">Thrown when the part is not yet ready for drag cube rendering</exception>
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;
}
}
}
14 changes: 13 additions & 1 deletion Source/ROUtils/Utils/ModUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

0 comments on commit 82183fb

Please sign in to comment.