Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Faster FindModelTransform*() and FindHeirarchyTransform*() #255

Merged
merged 3 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions GameData/KSPCommunityFixes/Settings.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,9 @@ KSP_COMMUNITY_FIXES
// state synchronization and caching solar panels scaled space raycasts results.
OptimizedModuleRaycasts = true

// Faster and minimal GC allocation replacements for Part FindModelTransform*() and FindHeirarchyTransform*()
FasterPartFindTransform = true

// General micro-optimization of FlightIntegrator and VesselPrecalculate, significantely increase
// framerate in large part count situations.
FlightPerf = true
Expand Down
1 change: 1 addition & 0 deletions KSPCommunityFixes/KSPCommunityFixes.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
<Compile Include="Performance\DisableHiddenPortraits.cs" />
<Compile Include="Performance\DisableMapUpdateInFlight.cs" />
<Compile Include="Performance\DragCubeGeneration.cs" />
<Compile Include="Performance\FasterPartFindTransform.cs" />
<Compile Include="Performance\FastLoader.cs" />
<Compile Include="Performance\FlightPerf.cs" />
<Compile Include="Performance\IMGUIOptimization.cs" />
Expand Down
290 changes: 290 additions & 0 deletions KSPCommunityFixes/Performance/FasterPartFindTransform.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
// Faster, and minimal GC alloc relacements for the Part FindModelTransform* and FindHeirarchyTransform* methods.
// Roughly 4 times faster on average for callers in a stock install, but this can go up to 20x faster on deep hierarchies.
// The main trick is to use Transform.Find() instead of checking the Transform.name property (which will allocate a new string)
// for every transform in the hierarchy. There are a few quirks with Transform.Find() requiring additional care, but this is
// overall much faster and eliminate GC allocations almost entirely.
// The methods are used quite a bit in stock, and massively over the modding ecosystem, so this can account to significant
// gains overall : https://github.com/search?q=FindModelTransform+OR+FindModelTransforms+language%3AC%23&type=code

// Uncomment to enable verification that kspcf results are matching stock, and stopwatch based perf comparison. Very logspammy.
// #define FPFT_DEBUG_PROFILE

using HarmonyLib;
using System;
using System.Collections.Generic;
using UnityEngine;

namespace KSPCommunityFixes.Performance
{
internal class FasterPartFindTransform : BasePatch
{
protected override Version VersionMin => new Version(1, 12, 3);

protected override void ApplyPatches(List<PatchInfo> patches)
{
#if !FPFT_DEBUG_PROFILE
patches.Add(new PatchInfo(
PatchMethodType.Prefix,
AccessTools.Method(typeof(Part), nameof(Part.FindHeirarchyTransform)),
this));

patches.Add(new PatchInfo(
PatchMethodType.Prefix,
AccessTools.Method(typeof(Part), nameof(Part.FindHeirarchyTransforms)),
this));

patches.Add(new PatchInfo(
PatchMethodType.Prefix,
AccessTools.Method(typeof(Part), nameof(Part.FindModelTransform)),
this));

patches.Add(new PatchInfo(
PatchMethodType.Prefix,
AccessTools.Method(typeof(Part), nameof(Part.FindModelTransforms)),
this));
#else
patches.Add(new PatchInfo(
PatchMethodType.Prefix,
AccessTools.Method(typeof(Part), nameof(Part.FindModelTransform)),
this, nameof(Part_FindModelTransform_Prefix_Debug)));

patches.Add(new PatchInfo(
PatchMethodType.Prefix,
AccessTools.Method(typeof(Part), nameof(Part.FindModelTransforms)),
this, nameof(Part_FindModelTransforms_Prefix_Debug)));
#endif
}

static bool Part_FindModelTransform_Prefix(Part __instance, string childName, out Transform __result)
{
// We need to check for the empty case because calling myTransform.Find("") will return myTransform...
if (string.IsNullOrEmpty(childName))
{
__result = null;
return false;
}

Transform model = __instance.partTransform.Find("model");

if (model.IsNullRef())
{
__result = null;
return false;
}

// special case where the method is called to find the model transform itself
if (childName == "model")
{
__result = model;
return false;
}

// Transform.Find() will treat '/' as a hierarchy path separator.
// In that (hopefully very rare) case we fall back to the stock method.
if (childName.IndexOf('/') != -1)
{
__result = null;
return true;
}
__result = FindChildRecursive(model, childName);
return false;
}

static bool Part_FindHeirarchyTransform_Prefix(Transform parent, string childName, out Transform __result)
{
// We need to check for the empty case because calling myTransform.Find("") will return myTransform...
if (parent.IsNullOrDestroyed() || string.IsNullOrEmpty(childName))
{
__result = null;
return false;
}

if (parent.gameObject.name == childName)
{
__result = parent;
return false;
}

// Transform.Find() will treat '/' as a hierarchy path separator.
// In that (hopefully very rare) case we fall back to the stock method.
if (childName.IndexOf('/') != -1)
{
__result = null;
return true;
}

__result = FindChildRecursive(parent, childName);
return false;
}

static Transform FindChildRecursive(Transform parent, string childName)
{
Transform child = parent.Find(childName);
if (child.IsNotNullRef())
return child;

int childCount = parent.childCount;
for (int i = 0; i < childCount; i++)
{
child = FindChildRecursive(parent.GetChild(i), childName);
if (child.IsNotNullRef())
return child;
}

return null;
}

static readonly List<Transform> transformBuffer = new List<Transform>();

static bool Part_FindModelTransforms_Prefix(Part __instance, string childName, out Transform[] __result)
{
try
{
// We need to check for the empty case because calling myTransform.Find("") will return myTransform...
if (string.IsNullOrEmpty(childName))
{
__result = Array.Empty<Transform>();
return false;
}

// Transform.Find() will treat '/' as a hierarchy path separator.
// In that (hopefully very rare) case we fall back to the stock method.
if (childName.IndexOf('/') != -1)
{
__result = null;
return true;
}

Transform model = __instance.partTransform.Find("model");

if (model.IsNullRef())
{
__result = Array.Empty<Transform>();
return false;
}

// special case where the method is called to find the model transform itself
if (childName == "model")
transformBuffer.Add(model);

FindChildsRecursive(model, childName, transformBuffer);
__result = transformBuffer.ToArray();
}
finally
{
transformBuffer.Clear();
}

return false;
}

static bool Part_FindHeirarchyTransforms_Prefix(Transform parent, string childName, List<Transform> tList)
{
// We need to check for the empty case because calling myTransform.Find("") will return myTransform...
if (parent.IsNullOrDestroyed() || string.IsNullOrEmpty(childName))
return false;

// Transform.Find() will treat '/' as a hierarchy path separator.
// In that (hopefully very rare) case we fall back to the stock method.
if (childName.IndexOf('/') != -1)
return true;

if (parent.gameObject.name == childName)
tList.Add(parent);

FindChildsRecursive(parent, childName, tList);
return false;
}

static void FindChildsRecursive(Transform parent, string childName, List<Transform> results)
{
int childCount = parent.childCount;
if (childCount == 0)
return;

Transform matchingChild = parent.Find(childName);

if (matchingChild.IsNotNullRef())
{
if (childCount == 1)
{
results.Add(matchingChild);
FindChildsRecursive(matchingChild, childName, results);
}
else
{
for (int i = 0; i < childCount; i++)
{
Transform child = parent.GetChild(i);
if (child.name == childName)
results.Add(child);

FindChildsRecursive(child, childName, results);
}
}
}
else
{
for (int i = 0; i < childCount; i++)
FindChildsRecursive(parent.GetChild(i), childName, results);
}
}

#if FPFT_DEBUG_PROFILE
private static System.Diagnostics.Stopwatch findModel_Stock = new System.Diagnostics.Stopwatch();
private static System.Diagnostics.Stopwatch findModel_KSPCF = new System.Diagnostics.Stopwatch();
private static int findModelMismatches = 0;
private static System.Diagnostics.Stopwatch findModels_Stock = new System.Diagnostics.Stopwatch();
private static System.Diagnostics.Stopwatch findModels_KSPCF = new System.Diagnostics.Stopwatch();
private static int findModelsMismatches = 0;

static bool Part_FindModelTransform_Prefix_Debug(Part __instance, string childName, out Transform __result)
{
findModel_Stock.Start();
__result = Part.FindHeirarchyTransform(__instance.partTransform.Find("model"), childName);
findModel_Stock.Stop();

findModel_KSPCF.Start();
if (Part_FindModelTransform_Prefix(__instance, childName, out Transform kspcfResult))
kspcfResult = Part.FindHeirarchyTransform(__instance.partTransform.Find("model"), childName);
findModel_KSPCF.Stop();

if (__result != kspcfResult)
{
findModelMismatches++;
Part_FindModelTransform_Prefix(__instance, childName, out kspcfResult);
}

Debug.Log($"FindModel [Stock: {findModel_Stock.Elapsed.TotalMilliseconds:F0} ms] [KSPCF: {findModel_KSPCF.Elapsed.TotalMilliseconds:F0} ms] [Mistmatches:{findModelMismatches}]");
return false;
}

static bool Part_FindModelTransforms_Prefix_Debug(Part __instance, string childName, out Transform[] __result)
{
findModels_Stock.Start();
List<Transform> list = new List<Transform>();
Part.FindHeirarchyTransforms(__instance.partTransform.Find("model"), childName, list);
__result = list.ToArray();
findModels_Stock.Stop();

findModels_KSPCF.Start();
if (Part_FindModelTransforms_Prefix(__instance, childName, out Transform[] kspcfResult))
{
list = new List<Transform>();
Part.FindHeirarchyTransforms(__instance.partTransform.Find("model"), childName, list);
kspcfResult = list.ToArray();
}
findModels_KSPCF.Stop();

if (!System.Linq.Enumerable.SequenceEqual(__result, kspcfResult))
{
findModelsMismatches++;
Part_FindModelTransforms_Prefix(__instance, childName, out kspcfResult);
}

Debug.Log($"[FindModels Stock: {findModels_Stock.Elapsed.TotalMilliseconds:F0} ms] [KSPCF: {findModels_KSPCF.Elapsed.TotalMilliseconds:F0} ms] [Mistmatches:{findModelsMismatches}]");
return false;
}
#endif
}
}
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ User options are available from the "ESC" in-game settings menu :<br/><img src="
- [**CollisionManagerFastUpdate**](https://github.com/KSPModdingLibs/KSPCommunityFixes/issues/174) [KSP 1.11.0 - 1.12.5]<br/>3-4 times faster update of parts inter-collision state, significantly reduce stutter on docking, undocking, decoupling and joint failure events.
- [**LowerMinPhysicsDTPerFrame**](https://github.com/KSPModdingLibs/KSPCommunityFixes/issues/175) [KSP 1.12.3 - 1.12.5]<br/>Allow a min value of 0.02 instead of 0.03 for the "Max Physics Delta-Time Per Frame" main menu setting. This allows for higher and smoother framerate at the expense of the game lagging behind real time.
- [**OptimizedModuleRaycasts**](https://github.com/KSPModdingLibs/KSPCommunityFixes/issues/216) [KSP 1.12.3 - 1.12.5]<br/>Improve engine exhaust damage and solar panel line of sight raycasts performance by avoiding extra physics state synchronization and caching solar panels scaled space raycasts results.
- [**FasterPartFindTransform**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/255) [KSP 1.12.3 - 1.12.5]<br/>Faster, and minimal GC alloc relacements for the Part FindModelTransform* and FindHeirarchyTransform* methods.

#### API and modding tools
- **MultipleModuleInPartAPI** [KSP 1.8.0 - 1.12.5]<br/>This API allow other plugins to implement PartModules that can exist in multiple occurrence in a single part and won't suffer "module indexing mismatch" persistent data losses following part configuration changes. [See documentation on the wiki](https://github.com/KSPModdingLibs/KSPCommunityFixes/wiki/MultipleModuleInPartAPI).
Expand Down Expand Up @@ -193,8 +194,9 @@ If doing so in the `Debug` configuration and if your KSP install is modified to

### Changelog

##### 1.36.0
- **SceneLoadSpeedBoost** : Reduce scene switch times further by forcing them to happen synchronously.
##### vNext
- New KSP performance patch : [**FasterPartFindTransform**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/255) [KSP 1.12.3 - 1.12.5] : Faster, and minimal GC alloc relacements for the Part FindModelTransform* and FindHeirarchyTransform* methods.
- New KSP performance patch : [**ForceSyncSceneSwitch**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/250) [KSP 1.12.0 - 1.12.5] : Forces all scene transitions to happen synchronously. Benefits scene transition time by reducing asset cleanup run count from 3 to 1.

##### 1.35.2
- **FastLoader** : Fixed a regression introduced in 1.35.1, causing PNG normal maps to be generated with empty mipmaps.
Expand Down
Loading