diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg index e6d4d44..c82c451 100644 --- a/GameData/KSPCommunityFixes/Settings.cfg +++ b/GameData/KSPCommunityFixes/Settings.cfg @@ -403,6 +403,10 @@ KSP_COMMUNITY_FIXES // Allow a min value of 0.02 instead of 0.03 for the "Max Physics Delta-Time Per Frame" main menu setting. LowerMinPhysicsDTPerFrame = true + // 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. + OptimizedModuleRaycasts = true + // ########################## // Modding // ########################## diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index 0b27596..7588997 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -169,6 +169,7 @@ + diff --git a/KSPCommunityFixes/Library/Extensions.cs b/KSPCommunityFixes/Library/Extensions.cs index d3e4422..0553322 100644 --- a/KSPCommunityFixes/Library/Extensions.cs +++ b/KSPCommunityFixes/Library/Extensions.cs @@ -28,5 +28,10 @@ public static string AssemblyQualifiedName(this object obj) Type type = obj.GetType(); return $"{type.Assembly.GetName().Name}:{type.Name}"; } + + public static bool IsPAWOpen(this Part part) + { + return part.PartActionWindow.IsNotNullOrDestroyed() && part.PartActionWindow.isActiveAndEnabled; + } } } diff --git a/KSPCommunityFixes/Performance/OptimizedModuleRaycasts.cs b/KSPCommunityFixes/Performance/OptimizedModuleRaycasts.cs new file mode 100644 index 0000000..4c402c5 --- /dev/null +++ b/KSPCommunityFixes/Performance/OptimizedModuleRaycasts.cs @@ -0,0 +1,152 @@ +using System; +using HarmonyLib; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace KSPCommunityFixes.Performance +{ + internal class OptimizedModuleRaycasts : BasePatch + { + private static readonly WaitForFixedUpdate waitForFixedUpdate = new WaitForFixedUpdate(); + private static bool partModulesSyncedOnceInFixedUpdate = false; + + protected override Version VersionMin => new Version(1, 12, 3); + + protected override void ApplyPatches(List patches) + { + patches.Add( + new PatchInfo(PatchMethodType.Transpiler, + AccessTools.Method(typeof(ModuleEngines), nameof(ModuleEngines.EngineExhaustDamage)), + this)); + + patches.Add( + new PatchInfo(PatchMethodType.Prefix, + AccessTools.Method(typeof(ModuleDeployableSolarPanel), nameof(ModuleDeployableSolarPanel.CalculateTrackingLOS)), + this)); + + KSPCommunityFixes.Instance.StartCoroutine(ResetSyncOnFixedEnd()); + } + + static IEnumerator ResetSyncOnFixedEnd() + { + while (true) + { + partModulesSyncedOnceInFixedUpdate = false; + lastVesselId = 0; + lastTrackingTransformId = 0; + yield return waitForFixedUpdate; + } + } + + static IEnumerable ModuleEngines_EngineExhaustDamage_Transpiler(IEnumerable instructions) + { + MethodInfo m_Physics_RayCast = AccessTools.Method(typeof(Physics), nameof(Physics.Raycast), new[] { typeof(Vector3), typeof(Vector3), typeof(RaycastHit).MakeByRefType(), typeof(float), typeof(int) }); + MethodInfo m_RaycastNoSync = AccessTools.Method(typeof(OptimizedModuleRaycasts), nameof(RaycastNoSync)); + + foreach (CodeInstruction instruction in instructions) + { + if (instruction.Calls(m_Physics_RayCast)) + { + instruction.operand = m_RaycastNoSync; + } + + yield return instruction; + } + } + + private static int lastVesselId; + private static int lastTrackingTransformId; + private static bool lastHasLoS; + private static string lastBlocker; + + private static bool ModuleDeployableSolarPanel_CalculateTrackingLOS_Prefix(ModuleDeployableSolarPanel __instance, Vector3 trackingDirection, ref string blocker, out bool __result) + { + if (__instance.part.ShieldedFromAirstream && __instance.applyShielding) + { + blocker = "aero shielding"; + __result = false; + return false; + } + + int trackingTransformId = __instance.trackingTransformLocal.GetInstanceID(); + int vesselId = __instance.vessel.GetInstanceID(); + if (lastTrackingTransformId == trackingTransformId && lastVesselId == vesselId) + { + if (!lastHasLoS) + { + __result = false; + blocker = lastBlocker; + return false; + } + } + else + { + lastTrackingTransformId = trackingTransformId; + lastVesselId = vesselId; + + Vector3 scaledVesselPos = ScaledSpace.LocalToScaledSpace(__instance.vessel.transform.position); + Vector3 scaledDirection = (ScaledSpace.LocalToScaledSpace(__instance.trackingTransformLocal.position) - scaledVesselPos).normalized; + + if (Physics.Raycast(scaledVesselPos, scaledDirection, out RaycastHit scaledHit, float.MaxValue, __instance.planetLayerMask) && scaledHit.transform.NotDestroyedRefNotEquals(__instance.trackingTransformScaled)) + { + __instance.hit = scaledHit; // just to ensure this is populated + lastBlocker = scaledHit.transform.gameObject.name; // allocates a string + blocker = lastBlocker; + lastHasLoS = false; + __result = false; + return false; + } + + lastHasLoS = true; + lastBlocker = null; + } + + Vector3 localPanelPos = __instance.secondaryTransform.position + trackingDirection * __instance.raycastOffset; + __result = !RaycastNoSync(localPanelPos, trackingDirection, out RaycastHit localhit, float.MaxValue, __instance.defaultLayerMask); + __instance.hit = localhit; // just to ensure this is populated + + if (!__result && __instance.part.IsPAWOpen() && localhit.transform.gameObject.IsNotNullOrDestroyed()) + { + GameObject hitObject = localhit.transform.gameObject; + if (!ReferenceEquals(hitObject.GetComponent(), null)) + { + blocker = ModuleDeployableSolarPanel.cacheAutoLOC_438839; + } + else + { + Part partUpwardsCached = FlightGlobals.GetPartUpwardsCached(hitObject); + if (partUpwardsCached.IsNotNullOrDestroyed()) + { + blocker = partUpwardsCached.partInfo.title; + } + else + { + string tag = hitObject.tag; // allocates a string + if (tag.Contains("KSC")) + blocker = ResearchAndDevelopment.GetMiniBiomedisplayNameByUnityTag(tag, true); + else + blocker = hitObject.name; + } + } + } + + return false; + } + + public static bool RaycastNoSync(Vector3 origin, Vector3 direction, out RaycastHit hitInfo, float maxDistance, int layerMask) + { + if (!partModulesSyncedOnceInFixedUpdate) + { + Physics.SyncTransforms(); + partModulesSyncedOnceInFixedUpdate = true; + } + + Physics.autoSyncTransforms = false; + bool result = Physics.defaultPhysicsScene.Raycast(origin, direction, out hitInfo, maxDistance, layerMask); + Physics.autoSyncTransforms = true; + return result; + } + } +} diff --git a/README.md b/README.md index 0a77aee..36740e5 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ User options are available from the "ESC" in-game settings menu :
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. #### API and modding tools - **MultipleModuleInPartAPI** [KSP 1.8.0 - 1.12.5]
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). @@ -189,6 +189,7 @@ If doing so in the `Debug` configuration and if your KSP install is modified to ### Changelog ##### 1.35.0 +- New KSP performance patch : [**OptimizedModuleRaycasts**](https://github.com/KSPModdingLibs/KSPCommunityFixes/issues/216) : 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. - **FastLoader** : Improved DDS loading performance by avoiding an extra copy of the DDS data ##### 1.34.1