diff --git a/GameData/KSPCommunityFixes/KSPCommunityFixes.version b/GameData/KSPCommunityFixes/KSPCommunityFixes.version index a24dfae..d9c4cd4 100644 --- a/GameData/KSPCommunityFixes/KSPCommunityFixes.version +++ b/GameData/KSPCommunityFixes/KSPCommunityFixes.version @@ -2,7 +2,7 @@ "NAME": "KSPCommunityFixes", "URL": "https://raw.githubusercontent.com/KSPModdingLibs/KSPCommunityFixes/master/GameData/KSPCommunityFixes/KSPCommunityFixes.version", "DOWNLOAD": "https://github.com/KSPModdingLibs/KSPCommunityFixes/releases", - "VERSION": {"MAJOR": 1, "MINOR": 32, "PATCH": 1, "BUILD": 0}, + "VERSION": {"MAJOR": 1, "MINOR": 33, "PATCH": 0, "BUILD": 0}, "KSP_VERSION": {"MAJOR": 1, "MINOR": 12, "PATCH": 5}, "KSP_VERSION_MIN": {"MAJOR": 1, "MINOR": 8, "PATCH": 0}, "KSP_VERSION_MAX": {"MAJOR": 1, "MINOR": 12, "PATCH": 5} diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg index b8ecae2..75eddd6 100644 --- a/GameData/KSPCommunityFixes/Settings.cfg +++ b/GameData/KSPCommunityFixes/Settings.cfg @@ -371,6 +371,10 @@ KSP_COMMUNITY_FIXES // Can provide significant performance gains when having many mods using IMGUI heavily. IMGUIOptimization = true + // 3-4 times faster update of parts inter-collision state, significantly reduce stutter on + // docking, undocking, decoupling and joint failure events. + CollisionManagerFastUpdate = true + // ########################## // Modding // ########################## diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index 723b75c..fd15411 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -33,7 +33,7 @@ portable true bin\Release\ - TRACE + TRACE;ENABLE_PROFILER prompt 4 true @@ -125,6 +125,7 @@ + diff --git a/KSPCommunityFixes/Performance/CollisionManagerFastUpdate.cs b/KSPCommunityFixes/Performance/CollisionManagerFastUpdate.cs new file mode 100644 index 0000000..2bf80ce --- /dev/null +++ b/KSPCommunityFixes/Performance/CollisionManagerFastUpdate.cs @@ -0,0 +1,439 @@ +// see https://github.com/KSPModdingLibs/KSPCommunityFixes/issues/174 + +// Enable profiling or debug tools for this patch : +// #define CMFU_PROFILE +// #define CMFU_DEBUG + +using System; +using HarmonyLib; +using System.Collections.Generic; +using UnityEngine; + +#if CMFU_DEBUG || CMFU_PROFILE +using System; +using System.Diagnostics; +using System.IO; +using Debug = UnityEngine.Debug; +#endif + +namespace KSPCommunityFixes.Performance +{ + public class CollisionManagerFastUpdate : BasePatch + { + protected override Version VersionMin => new Version(1, 11, 0); + + protected override void ApplyPatches(List patches) + { + patches.Add( + new PatchInfo(PatchMethodType.Prefix, + AccessTools.Method(typeof(CollisionManager), nameof(CollisionManager.UpdatePartCollisionIgnores)), + this)); + } + + static bool CollisionManager_UpdatePartCollisionIgnores_Prefix(CollisionManager __instance) + { + List colliders = GetAllPartColliders(); + UpdatePartCollisionIgnoresFast(colliders); + return false; + } + + internal static void UpdatePartCollisionIgnoresFast(List colliders) + { +#if CMFU_DEBUG + fastResultsToVerify.Clear(); + int iterations = 0; + int pairs = 0; + int duplicates = 0; + int mismatches = 0; +#endif + int colliderCount = colliders.Count; + for (int i = 0; i < colliderCount; i++) + { + ColliderInfo colliderA = colliders[i]; + + for (int j = i + 1; j < colliderCount; j++) + { +#if CMFU_DEBUG + iterations++; +#endif + ColliderInfo colliderB = colliders[j]; + + if (colliderA.rigidBodyId == colliderB.rigidBodyId) + continue; + + bool ignore = colliderA.vesselId == colliderB.vesselId; + if (ignore && colliderA.sameVesselCollision && colliderB.sameVesselCollision && colliderA.partPersistentId != colliderB.partPersistentId) + ignore = false; + + Physics.IgnoreCollision(colliderA.collider, colliderB.collider, ignore); +#if CMFU_DEBUG + pairs++; + ColliderPair pair = new ColliderPair(colliderA.collider, colliderB.collider); + if (fastResultsToVerify.TryGetValue(pair, out bool otherIgnore)) + { + duplicates++; + if (otherIgnore != ignore) + mismatches++; + } + fastResultsToVerify[pair] = ignore; +#endif + } + } +#if CMFU_DEBUG + Debug.Log($"[CollisionManagerFastUpdate] [KSPCF results] Colliders : {colliderCount} - Iterations : {iterations} - Checked pairs : {pairs} - Duplicate pairs : {duplicates} - Ignore mismatches in duplicates : {mismatches}"); +#endif + } + + internal static List GetAllPartColliders() + { + bool hasLoadedEVAVessel = false; + int count = FlightGlobals.VesselsLoaded.Count; + while (count-- > 0) + { + if (FlightGlobals.VesselsLoaded[count].isEVA) + { + hasLoadedEVAVessel = true; + break; + } + } + + HashSet colliderHashSet = new HashSet(1000); + List colliders = new List(1000); + List colliderBuffer = new List(); + + foreach (Vessel vessel in FlightGlobals.VesselsLoaded) + { + int vesselId = vessel.GetInstanceID(); + for (int i = vessel.parts.Count; i-- > 0;) + { + Part part = vessel.parts[i]; + + part.partTransform.GetComponentsInChildren(hasLoadedEVAVessel, colliderBuffer); + for (int j = colliderBuffer.Count; j-- > 0;) + { + Collider collider = colliderBuffer[j]; + + if (colliderHashSet.Contains(collider)) + continue; + + if ((collider.gameObject.activeInHierarchy && collider.enabled) || (hasLoadedEVAVessel && (collider.CompareTag("Ladder") || collider.CompareTag("Airlock")))) + { + colliders.Add(new ColliderInfo(collider, vesselId, part.persistentId, part.sameVesselCollision)); + colliderHashSet.Add(collider); + } + } + } + } + + return colliders; + } + + internal struct ColliderInfo + { + public uint partPersistentId; + public int rigidBodyId; + public int vesselId; + public bool sameVesselCollision; + public Collider collider; + + public ColliderInfo(Collider collider, int vesselId, uint partPersistentId, bool sameVesselCollision) + { + this.collider = collider; + this.partPersistentId = partPersistentId; + this.sameVesselCollision = sameVesselCollision; + this.vesselId = vesselId; + rigidBodyId = collider.attachedRigidbody.IsNullOrDestroyed() ? 0 : collider.attachedRigidbody.GetInstanceID(); + } + } + +#if CMFU_DEBUG + internal static Dictionary fastResultsToVerify = new Dictionary(); + + internal static void VerifyPartCollisionIgnores() + { + Debug.Log("[CollisionManagerFastUpdate] : VERIFYING RESULTS..."); + + List colliders = GetAllPartColliders(); + UpdatePartCollisionIgnoresFast(colliders); + + Dictionary stockResultsToVerify = new Dictionary(); + int iterations = 0; + int pairs = 0; + int duplicates = 0; + int mismatches = 0; + int colliderCount = 0; + + + List allVesselColliders = CollisionManager.Instance.GetAllVesselColliders(); + + foreach (CollisionManager.VesselColliderList vesselColliderList in allVesselColliders) + foreach (CollisionManager.PartColliderList partColliderList in vesselColliderList.colliderList) + colliderCount += partColliderList.colliders.Count; + + int i = 0; + for (int count = allVesselColliders.Count; i < count; i++) + { + int j = i; + for (int count2 = allVesselColliders.Count; j < count2; j++) + { + List colliderList = allVesselColliders[i].colliderList; + List colliderList2 = allVesselColliders[j].colliderList; + bool flag = i == j; + int k = 0; + for (int count3 = colliderList.Count; k < count3; k++) + { + int l = (flag ? (k + 1) : 0); + for (int count4 = colliderList2.Count; l < count4; l++) + { + int m = 0; + for (int count5 = colliderList[k].colliders.Count; m < count5; m++) + { + int n = 0; + for (int count6 = colliderList2[l].colliders.Count; n < count6; n++) + { + iterations++; + Collider collider = colliderList[k].colliders[m]; + Collider collider2 = colliderList2[l].colliders[n]; + if (!(collider.attachedRigidbody == collider2.attachedRigidbody)) + { + bool ignore; + if ((ignore = flag) && colliderList[k].sameVesselCollision && colliderList2[l].sameVesselCollision && colliderList[k].partPersistentId != colliderList2[l].partPersistentId) + { + ignore = false; + } + + pairs++; + ColliderPair pair = new ColliderPair(collider, collider2); + if (stockResultsToVerify.TryGetValue(pair, out bool otherIgnore)) + { + duplicates++; + if (otherIgnore != ignore) + mismatches++; + } + stockResultsToVerify[pair] = ignore; + } + } + } + } + } + } + } + allVesselColliders.Clear(); + + Debug.Log($"[CollisionManagerFastUpdate] [Stock results] Colliders : {colliderCount} - Iterations : {iterations} - Checked pairs : {pairs} - Duplicate pairs : {duplicates} - Ignore mismatches in duplicates : {mismatches}"); + + int fastMissingPairs = 0; + int fastAdditionalPairs = 0; + int fastResultMismatchs = 0; + + foreach (KeyValuePair colliderPair in stockResultsToVerify) + { + if (fastResultsToVerify.TryGetValue(colliderPair.Key, out bool fastResult)) + { + if (fastResult != colliderPair.Value) + fastResultMismatchs++; + } + else + { + fastMissingPairs++; + } + } + + foreach (KeyValuePair colliderPair in fastResultsToVerify) + { + if (stockResultsToVerify.TryGetValue(colliderPair.Key, out bool stockResult)) + { + if (stockResult != colliderPair.Value) + fastResultMismatchs++; + } + else + { + fastAdditionalPairs++; + } + } + + Debug.Log($"[CollisionManagerFastUpdate] [Verify] Stock pairs : {stockResultsToVerify.Count} - KSPCF pairs : {fastResultsToVerify.Count} - KSPCF missing pairs : {fastMissingPairs} - KSPCF additional pairs : {fastAdditionalPairs} - Results mismatches : {fastResultMismatchs}"); + } + + internal struct ColliderPair + { + public Collider a; + public Collider b; + + public ColliderPair(Collider a, Collider b) + { + this.a = a; + this.b = b; + } + + public override int GetHashCode() => a.GetHashCode() + b.GetHashCode(); + public bool Equals(ColliderPair p) => (a == p.a && b == p.b) || (a == p.b && b == p.a); + public override bool Equals(object obj) => obj is ColliderPair other && Equals(other); + public static bool operator ==(ColliderPair lhs, ColliderPair rhs) => lhs.Equals(rhs); + public static bool operator !=(ColliderPair lhs, ColliderPair rhs) => !lhs.Equals(rhs); + } +#endif + } + +#if CMFU_DEBUG || CMFU_PROFILE + [KSPAddon(KSPAddon.Startup.Flight, false)] + public class CollisionManagerProfiler : MonoBehaviour + { + private bool fastIsCollecting; + private bool stockIsCollecting; + private bool fastRequireUpdate; + private bool stockRequireUpdate; + private Stopwatch fastSetupWatch = new Stopwatch(); + private Stopwatch fastMainWatch = new Stopwatch(); + private Stopwatch stockSetupWatch = new Stopwatch(); + private Stopwatch stockMainWatch = new Stopwatch(); + private int j = 0; + + private string profileFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "CollisionManagerProfiler.txt"); + + void Update() + { + if (fastIsCollecting) + { + if (j > 100) + { + fastIsCollecting = false; + double mainTime = fastMainWatch.Elapsed.TotalMilliseconds / 100.0; + double setupTime = fastSetupWatch.Elapsed.TotalMilliseconds / 100.0; + fastMainWatch.Reset(); + fastSetupWatch.Reset(); + if (!File.Exists(profileFilePath)) File.Create(profileFilePath); + using (StreamWriter sw = File.AppendText(profileFilePath)) + sw.WriteLine($"[KSPCF IMPL] {mainTime:F3} ms (setup = {setupTime:F3} ms)"); + } + else + { + fastRequireUpdate = true; + } + } + + if (stockIsCollecting) + { + if (j > 100) + { + stockIsCollecting = false; + double mainTime = stockMainWatch.Elapsed.TotalMilliseconds / 100.0; + double setupTime = stockSetupWatch.Elapsed.TotalMilliseconds / 100.0; + stockMainWatch.Reset(); + stockSetupWatch.Reset(); + + if (!File.Exists(profileFilePath)) File.Create(profileFilePath); + using (StreamWriter sw = File.AppendText(profileFilePath)) + sw.WriteLine($"[STOCK IMPL] {mainTime:F3} ms (setup = {setupTime:F3} ms)"); + } + else + { + stockRequireUpdate = true; + } + } + } + + void FixedUpdate() + { + if (fastRequireUpdate) + { + fastRequireUpdate = false; + j++; + fastMainWatch.Start(); + fastSetupWatch.Start(); + List colliders = CollisionManagerFastUpdate.GetAllPartColliders(); + fastSetupWatch.Stop(); + CollisionManagerFastUpdate.UpdatePartCollisionIgnoresFast(colliders); + fastMainWatch.Stop(); + } + + if (stockRequireUpdate) + { + stockRequireUpdate = false; + j++; + stockMainWatch.Start(); + stockSetupWatch.Start(); + List allVesselColliders = CollisionManager.Instance.GetAllVesselColliders(); + stockSetupWatch.Stop(); + UpdatePartCollisionIgnoresStock(allVesselColliders); + stockMainWatch.Stop(); + } + } + + void OnGUI() + { + if (GUI.Button(new Rect(10, 100, 150, 20), stockIsCollecting ? "Profiling stock..." : "Profile stock")) + { + if (!stockIsCollecting) + { + stockMainWatch.Reset(); + stockSetupWatch.Reset(); + stockIsCollecting = true; + j = 0; + } + } + + if (GUI.Button(new Rect(10, 120, 150, 20), fastIsCollecting ? "Profiling KSPCF..." : "Profile KSPCF")) + { + if (!fastIsCollecting) + { + fastMainWatch.Reset(); + fastSetupWatch.Reset(); + fastIsCollecting = true; + j = 0; + } + } + +#if CMFU_DEBUG + if (GUI.Button(new Rect(10, 140, 150, 20), "Verify results")) + { + CollisionManagerFastUpdate.VerifyPartCollisionIgnores(); + } +#endif + } + + static void UpdatePartCollisionIgnoresStock(List allVesselColliders) + { + int i = 0; + for (int count = allVesselColliders.Count; i < count; i++) + { + int j = i; + for (int count2 = allVesselColliders.Count; j < count2; j++) + { + List colliderList = allVesselColliders[i].colliderList; + List colliderList2 = allVesselColliders[j].colliderList; + bool flag = i == j; + int k = 0; + for (int count3 = colliderList.Count; k < count3; k++) + { + int l = (flag ? (k + 1) : 0); + for (int count4 = colliderList2.Count; l < count4; l++) + { + int m = 0; + for (int count5 = colliderList[k].colliders.Count; m < count5; m++) + { + int n = 0; + for (int count6 = colliderList2[l].colliders.Count; n < count6; n++) + { + Collider collider = colliderList[k].colliders[m]; + Collider collider2 = colliderList2[l].colliders[n]; + if (!(collider.attachedRigidbody == collider2.attachedRigidbody)) + { + bool ignore; + if ((ignore = flag) && colliderList[k].sameVesselCollision && colliderList2[l].sameVesselCollision && colliderList[k].partPersistentId != colliderList2[l].partPersistentId) + { + ignore = false; + } + Physics.IgnoreCollision(collider, collider2, ignore); + } + } + } + } + } + } + } + allVesselColliders.Clear(); + } + } +#endif + } diff --git a/KSPCommunityFixes/Properties/AssemblyInfo.cs b/KSPCommunityFixes/Properties/AssemblyInfo.cs index 0ddd8bc..84f99a7 100644 --- a/KSPCommunityFixes/Properties/AssemblyInfo.cs +++ b/KSPCommunityFixes/Properties/AssemblyInfo.cs @@ -30,7 +30,7 @@ // Revision // [assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.32.1.0")] +[assembly: AssemblyFileVersion("1.33.0.0")] -[assembly: KSPAssembly("KSPCommunityFixes", 1, 32, 1)] +[assembly: KSPAssembly("KSPCommunityFixes", 1, 33, 0)] [assembly: KSPAssemblyDependency("MultipleModulePartAPI", 1, 0, 0)] diff --git a/README.md b/README.md index be3bb08..1d54f29 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ User options are available from the "ESC" in-game settings menu :