From d1e41949017c363d8f603df472e9d3216d7e88da Mon Sep 17 00:00:00 2001 From: gotmachine <24925209+gotmachine@users.noreply.github.com> Date: Sat, 1 Jun 2024 21:00:18 +0200 Subject: [PATCH 1/8] Temporary work, I need to break this down into two branches --- GameData/KSPCommunityFixes/Settings.cfg | 4 + KSPCommunityFixes/KSPCommunityFixes.csproj | 8 + KSPCommunityFixes/Library/MuParser.cs | 1165 +++++++++++++++++ KSPCommunityFixes/Library/ShaderHelpers.cs | 132 ++ KSPCommunityFixes/Library/StaticHelpers.cs | 5 + KSPCommunityFixes/Performance/FastLoader.cs | 402 +++++- .../Performance/FlightIntegratorPerf.cs | 554 ++++++++ .../Performance/GeneralPerfFixes.cs | 1142 ++++++++++++++++ 8 files changed, 3390 insertions(+), 22 deletions(-) create mode 100644 KSPCommunityFixes/Library/MuParser.cs create mode 100644 KSPCommunityFixes/Library/ShaderHelpers.cs create mode 100644 KSPCommunityFixes/Performance/FlightIntegratorPerf.cs create mode 100644 KSPCommunityFixes/Performance/GeneralPerfFixes.cs diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg index d5ceb7d1..4778a542 100644 --- a/GameData/KSPCommunityFixes/Settings.cfg +++ b/GameData/KSPCommunityFixes/Settings.cfg @@ -420,6 +420,10 @@ KSP_COMMUNITY_FIXES // state synchronization and caching solar panels scaled space raycasts results. OptimizedModuleRaycasts = true + GeneralPerfFixes = true + + FlightIntegratorPerf = true + // ########################## // Modding // ########################## diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index 5ccec49f..0a604987 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -98,6 +98,10 @@ + + + + @@ -126,7 +130,9 @@ + + @@ -138,6 +144,8 @@ + + diff --git a/KSPCommunityFixes/Library/MuParser.cs b/KSPCommunityFixes/Library/MuParser.cs new file mode 100644 index 00000000..f1556163 --- /dev/null +++ b/KSPCommunityFixes/Library/MuParser.cs @@ -0,0 +1,1165 @@ +using PartToolsLib; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using UnityEngine; +using UnityEngine.Rendering; + +namespace KSPCommunityFixes.Library +{ + // For reference on my 5800X3D/DDR4, when disk reading isn't a factor and for loading 485MB + // worth of models : + // - The stock parser has a throughput of 120 MB/s + // - The KSPCF parser has a throughput of 290 MB/s + // Roadblocks to further optimizations : + // - We can't avoid making a copy of the mesh data, avoiding that would require Unity accepting Span in the various Mesh.Set*() methods + // - For some mesh data, the mu data layout doesn't match a continuous array of structs, so we have to parse those structs one by one + // - Animation parsing is inerently slow due to two strings having to be parsed for every curve, plus other hard to overcome inefficiencies + // - Ideally, the dummy* data structures should avoid manipulating strings, would use dictionaries instead of lists, and could be reused instead of re-instantatied for every model. + // Overall, further optimization probably won't be beneficial anyway, we will almost always be bottlenecked by disk read speed. + + /// + /// Reimplementation of the stock mu model format parser (PartReader). Roughly 60% faster. + /// + internal class MuParser + { + private static readonly UTF8Encoding decoder = new UTF8Encoding(); + private static char[] charBuffer; + + private static int[] intBuffer; + private static Vector2[] vector2Buffer; + private static Vector3[] vector3Buffer; + private static Vector4[] vector4Buffer; + private static Color32[] color32Buffer; + private static Keyframe[][] keyFrameBuffers; + + private static List matDummies; + private static PartReader.TextureDummyList textureDummies; + private static List boneDummies; + + private static string modelDirectoryUrl; + private static byte[] data; + private static unsafe byte* dataPtr; + private static int dataLength; + private static int index; + + private static int version; + + /// + /// Parse a mu model into a GameObject hierarchy + /// + /// GameData relative path to the folder containing the model. For a model known by its , this will be the urlFile.parent.url value. + /// A byte array containing the raw model file data + /// The length of the data in the array. Ignored if zero. + public static unsafe GameObject Parse(string modelDirectoryUrl, byte[] data, int dataLength = 0) + { + if (matDummies == null) + matDummies = new List(); + + if (textureDummies == null) + textureDummies = new PartReader.TextureDummyList(); + + if (boneDummies == null) + boneDummies = new List(); + + GameObject model; + + GCHandle pinnedArray = GCHandle.Alloc(data, GCHandleType.Pinned); + + try + { + MuParser.modelDirectoryUrl = modelDirectoryUrl; + MuParser.data = data; + MuParser.dataLength = dataLength <= 0 ? data.Length : dataLength; + dataPtr = (byte*)pinnedArray.AddrOfPinnedObject(); + index = 0; + + if (ReadInt() != 76543) + throw new Exception("Invalid mu file"); + + version = ReadInt(); + SkipString(); + + model = ReadChild(null); + AffectSkinnedMeshRenderersBones(model); + } + catch (Exception e) + { + model = null; + Debug.LogError($"Model {modelDirectoryUrl} error: {e.Message}\n{e.StackTrace}"); + } + finally + { + pinnedArray.Free(); + MuParser.matDummies.Clear(); + MuParser.boneDummies.Clear(); + MuParser.textureDummies.Clear(); + MuParser.modelDirectoryUrl = null; + MuParser.data = null; + MuParser.dataPtr = null; + MuParser.dataLength = 0; + MuParser.index = 0; + MuParser.version = 0; + } + + return model; + } + + /// + /// Call this to release the memory used by the static buffers. + /// This is safe to use at any point. + /// + public static void ReleaseBuffers() + { + intBuffer = null; + vector2Buffer = null; + vector3Buffer = null; + vector4Buffer = null; + color32Buffer = null; + keyFrameBuffers = null; + charBuffer = null; + matDummies = null; + textureDummies = null; + boneDummies = null; + } + + #region Core methods + + private static GameObject ReadChild(Transform parent) + { + GameObject gameObject = new GameObject(ReadString()); + + gameObject.transform.parent = parent; + gameObject.transform.localPosition = ReadVector3(); + gameObject.transform.localRotation = ReadQuaternion(); + gameObject.transform.localScale = ReadVector3(); + + while (index < dataLength) + { + switch (ReadInt()) + { + case 0: + ReadChild(gameObject.transform); + break; + case 2: + ReadAnimation(gameObject); + break; + case 3: + ReadMeshCollider(gameObject); + break; + case 4: + ReadSphereCollider(gameObject); + break; + case 5: + ReadCapsuleCollider(gameObject); + break; + case 6: + ReadBoxCollider(gameObject); + break; + case 7: + ReadMeshFilter(gameObject); + break; + case 8: + ReadMeshRenderer(gameObject); + break; + case 9: + ReadSkinnedMeshRenderer(gameObject); + break; + case 10: + ReadMaterials(); + break; + case 12: + ReadTextures(gameObject); + break; + case 23: + ReadLight(gameObject); + break; + case 24: + ReadTagAndLayer(gameObject); + break; + case 25: + ReadMeshCollider2(gameObject); + break; + case 26: + ReadSphereCollider2(gameObject); + break; + case 27: + ReadCapsuleCollider2(gameObject); + break; + case 28: + ReadBoxCollider2(gameObject); + break; + case 29: + ReadWheelCollider(gameObject); + break; + case 30: + ReadCamera(gameObject); + break; + case 31: + ReadParticles(gameObject); + break; + case 1: + return gameObject; + } + } + return gameObject; + } + + private static void AffectSkinnedMeshRenderersBones(GameObject model) + { + if (boneDummies.Count > 0) + { + int i = 0; + for (int count = boneDummies.Count; i < count; i++) + { + Transform[] array = new Transform[boneDummies[i].bones.Count]; + int j = 0; + for (int count2 = boneDummies[i].bones.Count; j < count2; j++) + { + array[j] = FindChildByName(model.transform, boneDummies[i].bones[j]); + } + boneDummies[i].smr.bones = array; + } + } + } + + private static Transform FindChildByName(Transform parent, string name) + { + if (parent.name == name) + { + return parent; + } + foreach (Transform item in parent) + { + Transform transform = FindChildByName(item, name); + if (transform != null) + { + return transform; + } + } + return null; + } + + #endregion + + #region Component parsers + + private static void ReadAnimation(GameObject o) + { + Animation animation = o.AddComponent(); + int clipCount = ReadInt(); + bool isInvalid = false; + for (int i = 0; i < clipCount; i++) + { + AnimationClip animationClip = new AnimationClip(); + animationClip.legacy = true; + string clipName = ReadString(); + animationClip.localBounds = new Bounds(ReadVector3(), ReadVector3()); + animationClip.wrapMode = (WrapMode)ReadInt(); + + int curveCount = ReadInt(); + for (int j = 0; j < curveCount; j++) + { + string curvePath = ReadString(); + string curveProperty = ReadString(); + Type curveType = null; + switch (ReadInt()) + { + case 0: + curveType = typeof(Transform); + break; + case 1: + curveType = typeof(Material); + break; + case 2: + curveType = typeof(Light); + break; + case 3: + curveType = typeof(AudioSource); + break; + } + WrapMode preWrapMode = (WrapMode)ReadInt(); + WrapMode postWrapMode = (WrapMode)ReadInt(); + + int keyFrameCount = ReadInt(); + Keyframe[] keyFrames = GetKeyFrameBuffer(keyFrameCount); + for (int k = 0; k < keyFrameCount; k++) + keyFrames[k] = ReadKeyFrame(); + + AnimationCurve animationCurve = new AnimationCurve(keyFrames); + animationCurve.preWrapMode = preWrapMode; + animationCurve.postWrapMode = postWrapMode; + + if (clipName == null || curvePath == null || curveType == null || curveProperty == null) + { + isInvalid = true; + Debug.LogWarning($"{clipName ?? "Null clipName"} : {curvePath ?? "Null curvePath"}, {(curveType == null ? "Null curveType" : curveType.ToString())}, {curveProperty ?? "Null curveProperty"}"); + continue; + } + + animationClip.SetCurve(curvePath, curveType, curveProperty, animationCurve); + } + if (!isInvalid) + { + animation.AddClip(animationClip, clipName); + } + } + string defaultclipName = ReadString(); + if (defaultclipName != string.Empty && !isInvalid) + animation.clip = animation.GetClip(defaultclipName); + + animation.playAutomatically = ReadBool(); + } + + /// + /// Usually, the curve will have less than 10 keyframes, this method will return a + /// cached buffer instead of instantiatiating a new one in such cases. + /// + private static Keyframe[] GetKeyFrameBuffer(int keyFrameCount) + { + if (keyFrameBuffers == null) + { + keyFrameBuffers = new Keyframe[10][]; + keyFrameBuffers[0] = new Keyframe[0]; + keyFrameBuffers[1] = new Keyframe[1]; + keyFrameBuffers[2] = new Keyframe[2]; + keyFrameBuffers[3] = new Keyframe[3]; + keyFrameBuffers[4] = new Keyframe[4]; + keyFrameBuffers[5] = new Keyframe[5]; + keyFrameBuffers[6] = new Keyframe[6]; + keyFrameBuffers[7] = new Keyframe[7]; + keyFrameBuffers[8] = new Keyframe[8]; + keyFrameBuffers[9] = new Keyframe[9]; + } + + if (keyFrameCount < 10) + return keyFrameBuffers[keyFrameCount]; + + return new Keyframe[keyFrameCount]; + } + + private static void ReadMeshCollider(GameObject o) + { + MeshCollider meshCollider = o.AddComponent(); + SkipBool(); // this is actually the "convex" property, but it is always forced to true; + meshCollider.convex = true; + meshCollider.sharedMesh = ReadMesh(); + } + + private static void ReadSphereCollider(GameObject o) + { + SphereCollider sphereCollider = o.AddComponent(); + sphereCollider.radius = ReadFloat(); + sphereCollider.center = ReadVector3(); + } + + private static void ReadCapsuleCollider(GameObject o) + { + CapsuleCollider capsuleCollider = o.AddComponent(); + capsuleCollider.radius = ReadFloat(); + capsuleCollider.direction = ReadInt(); + capsuleCollider.center = ReadVector3(); + } + + private static void ReadBoxCollider(GameObject o) + { + BoxCollider boxCollider = o.AddComponent(); + boxCollider.size = ReadVector3(); + boxCollider.center = ReadVector3(); + } + + private static void ReadMeshFilter(GameObject o) + { + o.AddComponent().sharedMesh = ReadMesh(); + } + + private static void ReadMeshRenderer(GameObject o) + { + MeshRenderer meshRenderer = o.AddComponent(); + if (version >= 1) + { + meshRenderer.shadowCastingMode = ReadBool() ? ShadowCastingMode.On : ShadowCastingMode.Off; + meshRenderer.receiveShadows = ReadBool(); + } + int rendererCount = ReadInt(); + for (int i = 0; i < rendererCount; i++) + { + int materialCount = ReadInt(); + while (materialCount >= matDummies.Count) + matDummies.Add(new PartReader.MaterialDummy()); + + matDummies[materialCount].renderers.Add(meshRenderer); + } + } + + private static void ReadSkinnedMeshRenderer(GameObject o) + { + SkinnedMeshRenderer skinnedMeshRenderer = o.AddComponent(); + int rendererCount = ReadInt(); + for (int i = 0; i < rendererCount; i++) + { + int materialCount = ReadInt(); + while (materialCount >= matDummies.Count) + { + matDummies.Add(new PartReader.MaterialDummy()); + } + matDummies[materialCount].renderers.Add(skinnedMeshRenderer); + } + skinnedMeshRenderer.localBounds = new Bounds(ReadVector3(), ReadVector3()); + skinnedMeshRenderer.quality = (SkinQuality)ReadInt(); + skinnedMeshRenderer.updateWhenOffscreen = ReadBool(); + int num3 = ReadInt(); + + PartReader.BonesDummy bonesDummy = new PartReader.BonesDummy(); + bonesDummy.smr = skinnedMeshRenderer; + for (int j = 0; j < num3; j++) + { + bonesDummy.bones.Add(ReadString()); + } + boneDummies.Add(bonesDummy); + skinnedMeshRenderer.sharedMesh = ReadMesh(); + } + + private static void ReadMaterials() + { + int materialCount = ReadInt(); + for (int i = 0; i < materialCount; i++) + { + PartReader.MaterialDummy materialDummy = matDummies[i]; + Material material = version < 4 ? ReadMaterial() : ReadMaterial4(); + + for (int j = materialDummy.renderers.Count; j-- > 0;) + materialDummy.renderers[j].sharedMaterial = material; + + for (int j = materialDummy.particleEmitters.Count; j-- > 0;) + materialDummy.particleEmitters[j].material = material; + } + } + + private static Material ReadMaterial() + { + string name = ReadString(); + ShaderType shaderType = (ShaderType)ReadInt(); + Material material = new Material(ShaderHelpers.GetShader(shaderType)); + switch (shaderType) + { + default: + + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + break; + case ShaderType.Specular: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetColor(ShaderHelpers.SpecColorPropId, ReadColor()); + material.SetFloat(ShaderHelpers.ShininessPropId, ReadFloat()); + break; + case ShaderType.Bumped: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + ReadMaterialTexture(material, "_BumpMap", ShaderHelpers.BumpMapPropId); + break; + case ShaderType.BumpedSpecular: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + ReadMaterialTexture(material, "_BumpMap", ShaderHelpers.BumpMapPropId); + material.SetColor(ShaderHelpers.SpecColorPropId, ReadColor()); + material.SetFloat(ShaderHelpers.ShininessPropId, ReadFloat()); + break; + case ShaderType.Emissive: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + ReadMaterialTexture(material, "_Emissive", ShaderHelpers.EmissivePropId); + material.SetColor(PropertyIDs._EmissiveColor, ReadColor()); + break; + case ShaderType.EmissiveSpecular: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetColor(ShaderHelpers.SpecColorPropId, ReadColor()); + material.SetFloat(ShaderHelpers.ShininessPropId, ReadFloat()); + ReadMaterialTexture(material, "_Emissive", ShaderHelpers.EmissivePropId); + material.SetColor(PropertyIDs._EmissiveColor, ReadColor()); + break; + case ShaderType.EmissiveBumpedSpecular: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + ReadMaterialTexture(material, "_BumpMap", ShaderHelpers.BumpMapPropId); + material.SetColor(ShaderHelpers.SpecColorPropId, ReadColor()); + material.SetFloat(ShaderHelpers.ShininessPropId, ReadFloat()); + ReadMaterialTexture(material, "_Emissive", ShaderHelpers.EmissivePropId); + material.SetColor(PropertyIDs._EmissiveColor, ReadColor()); + break; + case ShaderType.AlphaCutout: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetFloat(ShaderHelpers.CutoffPropId, ReadFloat()); + break; + case ShaderType.AlphaCutoutBumped: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + ReadMaterialTexture(material, "_BumpMap", ShaderHelpers.BumpMapPropId); + material.SetFloat(ShaderHelpers.CutoffPropId, ReadFloat()); + break; + case ShaderType.Alpha: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + break; + case ShaderType.AlphaSpecular: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetFloat(ShaderHelpers.GlossPropId, ReadFloat()); + material.SetColor(ShaderHelpers.SpecColorPropId, ReadColor()); + material.SetFloat(ShaderHelpers.ShininessPropId, ReadFloat()); + break; + case ShaderType.AlphaUnlit: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetColor(ShaderHelpers.ColorPropId, ReadColor()); + break; + case ShaderType.Unlit: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetColor(ShaderHelpers.ColorPropId, ReadColor()); + break; + case ShaderType.ParticleAlpha: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetColor(ShaderHelpers.ColorPropId, ReadColor()); + material.SetFloat(ShaderHelpers.InvFadePropId, ReadFloat()); + break; + case ShaderType.ParticleAdditive: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetColor(ShaderHelpers.ColorPropId, ReadColor()); + material.SetFloat(ShaderHelpers.InvFadePropId, ReadFloat()); + break; + case ShaderType.BumpedSpecularMap: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + ReadMaterialTexture(material, "_BumpMap", ShaderHelpers.BumpMapPropId); + ReadMaterialTexture(material, "_SpecMap", ShaderHelpers.SpecMapPropId); + material.SetFloat(ShaderHelpers.SpecTintPropId, ReadFloat()); + material.SetFloat(ShaderHelpers.ShininessPropId, ReadFloat()); + break; + } + + material.name = name; + return material; + } + + private static Material ReadMaterial4() + { + string matName = ReadString(); + string shaderName = ReadString(); + int propertyCount = ReadInt(); + Material material = new Material(Shader.Find(shaderName)); + material.name = matName; + for (int i = 0; i < propertyCount; i++) + { + string text = ReadString(); + switch (ReadInt()) + { + case 0: + material.SetColor(text, ReadColor()); + break; + case 1: + material.SetVector(text, ReadVector4()); + break; + case 2: + material.SetFloat(text, ReadFloat()); + break; + case 3: + material.SetFloat(text, ReadFloat()); + break; + case 4: + ReadMaterialTexture(material, text, Shader.PropertyToID(text)); + break; + } + } + return material; + } + + private static void ReadMaterialTexture(Material mat, string textureName, int textureId) + { + // we would need to reimplement the whole texture/material dummy thing to get ride of + // having to use the texture name. Probably doesn't matter much... + textureDummies.AddTextureDummy(ReadInt(), mat, textureName); + mat.SetTextureScale(textureId, ReadVector2()); + mat.SetTextureOffset(textureId, ReadVector2()); + } + + private static void ReadTextures(GameObject o) + { + int texCount = ReadInt(); + if (texCount != textureDummies.Count) + { + Debug.LogError("TextureError: " + texCount + " " + textureDummies.Count); + return; + } + for (int i = 0; i < texCount; i++) + { + string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(ReadString()); + TextureType textureType = (TextureType)ReadInt(); + string url = modelDirectoryUrl + "/" + fileNameWithoutExtension; + Texture2D texture = GameDatabase.Instance.GetTexture(url, textureType == TextureType.NormalMap); + if (texture.IsNullOrDestroyed()) + { + Debug.LogError($"Texture '{url}' not found!"); + continue; + } + int j = 0; + for (int materialCount = textureDummies[i].Count; j < materialCount; j++) + { + PartReader.TextureMaterialDummy textureMaterialDummy = textureDummies[i][j]; + int k = 0; + for (int texPropCount = textureMaterialDummy.shaderName.Count; k < texPropCount; k++) + { + string texProperty = textureMaterialDummy.shaderName[k]; + textureMaterialDummy.material.SetTexture(texProperty, texture); + } + } + } + } + + private static void ReadLight(GameObject o) + { + Light light = o.AddComponent(); + + light.type = (LightType)ReadInt(); + light.intensity = ReadFloat(); + light.range = ReadFloat(); + light.color = ReadColor(); + light.cullingMask = ReadInt(); + + if (version > 1) + light.spotAngle = ReadFloat(); + } + + private static void ReadTagAndLayer(GameObject o) + { + o.tag = ReadString(); + o.layer = ReadInt(); + } + + private static void ReadMeshCollider2(GameObject o) + { + MeshCollider meshCollider = o.AddComponent(); + meshCollider.convex = true; + meshCollider.isTrigger = ReadBool(); + SkipBool(); // this is actually the "convex" property, but it is always forced to true; + meshCollider.sharedMesh = ReadMesh(); + } + + private static void ReadSphereCollider2(GameObject o) + { + SphereCollider sphereCollider = o.AddComponent(); + sphereCollider.isTrigger = ReadBool(); + sphereCollider.radius = ReadFloat(); + sphereCollider.center = ReadVector3(); + } + + private static void ReadCapsuleCollider2(GameObject o) + { + CapsuleCollider capsuleCollider = o.AddComponent(); + capsuleCollider.isTrigger = ReadBool(); + capsuleCollider.radius = ReadFloat(); + capsuleCollider.height = ReadFloat(); + capsuleCollider.direction = ReadInt(); + capsuleCollider.center = ReadVector3(); + } + + private static void ReadBoxCollider2(GameObject o) + { + BoxCollider boxCollider = o.AddComponent(); + boxCollider.isTrigger = ReadBool(); + boxCollider.size = ReadVector3(); + boxCollider.center = ReadVector3(); + } + + private static void ReadWheelCollider(GameObject o) + { + WheelCollider wheelCollider = o.AddComponent(); + wheelCollider.mass = ReadFloat(); + wheelCollider.radius = ReadFloat(); + wheelCollider.suspensionDistance = ReadFloat(); + wheelCollider.center = ReadVector3(); + wheelCollider.suspensionSpring = new JointSpring + { + spring = ReadFloat(), + damper = ReadFloat(), + targetPosition = ReadFloat() + }; + wheelCollider.forwardFriction = new WheelFrictionCurve + { + extremumSlip = ReadFloat(), + extremumValue = ReadFloat(), + asymptoteSlip = ReadFloat(), + asymptoteValue = ReadFloat(), + stiffness = ReadFloat() + }; + wheelCollider.sidewaysFriction = new WheelFrictionCurve + { + extremumSlip = ReadFloat(), + extremumValue = ReadFloat(), + asymptoteSlip = ReadFloat(), + asymptoteValue = ReadFloat(), + stiffness = ReadFloat() + }; + wheelCollider.enabled = false; + } + + private static void ReadCamera(GameObject o) + { + Camera camera = o.AddComponent(); + camera.clearFlags = (CameraClearFlags)ReadInt(); + camera.backgroundColor = ReadColor(); + camera.cullingMask = ReadInt(); + camera.orthographic = ReadBool(); + camera.fieldOfView = ReadFloat(); + camera.nearClipPlane = ReadFloat(); + camera.farClipPlane = ReadFloat(); + camera.depth = ReadFloat(); + camera.allowHDR = false; + camera.enabled = false; + } + + private static void ReadParticles(GameObject o) + { + KSPParticleEmitter kSPParticleEmitter = o.AddComponent(); + kSPParticleEmitter.emit = ReadBool(); + kSPParticleEmitter.shape = (KSPParticleEmitter.EmissionShape)ReadInt(); + kSPParticleEmitter.shape3D.x = ReadFloat(); + kSPParticleEmitter.shape3D.y = ReadFloat(); + kSPParticleEmitter.shape3D.z = ReadFloat(); + kSPParticleEmitter.shape2D.x = ReadFloat(); + kSPParticleEmitter.shape2D.y = ReadFloat(); + kSPParticleEmitter.shape1D = ReadFloat(); + kSPParticleEmitter.color = ReadColor(); + kSPParticleEmitter.useWorldSpace = ReadBool(); + kSPParticleEmitter.minSize = ReadFloat(); + kSPParticleEmitter.maxSize = ReadFloat(); + kSPParticleEmitter.minEnergy = ReadFloat(); + kSPParticleEmitter.maxEnergy = ReadFloat(); + kSPParticleEmitter.minEmission = ReadInt(); + kSPParticleEmitter.maxEmission = ReadInt(); + kSPParticleEmitter.worldVelocity.x = ReadFloat(); + kSPParticleEmitter.worldVelocity.y = ReadFloat(); + kSPParticleEmitter.worldVelocity.z = ReadFloat(); + kSPParticleEmitter.localVelocity.x = ReadFloat(); + kSPParticleEmitter.localVelocity.y = ReadFloat(); + kSPParticleEmitter.localVelocity.z = ReadFloat(); + kSPParticleEmitter.rndVelocity.x = ReadFloat(); + kSPParticleEmitter.rndVelocity.y = ReadFloat(); + kSPParticleEmitter.rndVelocity.z = ReadFloat(); + kSPParticleEmitter.emitterVelocityScale = ReadFloat(); + kSPParticleEmitter.angularVelocity = ReadFloat(); + kSPParticleEmitter.rndAngularVelocity = ReadFloat(); + kSPParticleEmitter.rndRotation = ReadBool(); + kSPParticleEmitter.doesAnimateColor = ReadBool(); + kSPParticleEmitter.colorAnimation = new Color[5]; + for (int i = 0; i < 5; i++) + { + kSPParticleEmitter.colorAnimation[i] = ReadColor(); + } + kSPParticleEmitter.worldRotationAxis.x = ReadFloat(); + kSPParticleEmitter.worldRotationAxis.y = ReadFloat(); + kSPParticleEmitter.worldRotationAxis.z = ReadFloat(); + kSPParticleEmitter.localRotationAxis.x = ReadFloat(); + kSPParticleEmitter.localRotationAxis.y = ReadFloat(); + kSPParticleEmitter.localRotationAxis.z = ReadFloat(); + kSPParticleEmitter.sizeGrow = ReadFloat(); + kSPParticleEmitter.rndForce.x = ReadFloat(); + kSPParticleEmitter.rndForce.y = ReadFloat(); + kSPParticleEmitter.rndForce.z = ReadFloat(); + kSPParticleEmitter.force.x = ReadFloat(); + kSPParticleEmitter.force.y = ReadFloat(); + kSPParticleEmitter.force.z = ReadFloat(); + kSPParticleEmitter.damping = ReadFloat(); + kSPParticleEmitter.castShadows = ReadBool(); + kSPParticleEmitter.recieveShadows = ReadBool(); + kSPParticleEmitter.lengthScale = ReadFloat(); + kSPParticleEmitter.velocityScale = ReadFloat(); + kSPParticleEmitter.maxParticleSize = ReadFloat(); + switch (ReadInt()) + { + default: + kSPParticleEmitter.particleRenderMode = ParticleSystemRenderMode.Billboard; + break; + case 3: + kSPParticleEmitter.particleRenderMode = ParticleSystemRenderMode.Stretch; + break; + case 4: + kSPParticleEmitter.particleRenderMode = ParticleSystemRenderMode.HorizontalBillboard; + break; + case 5: + kSPParticleEmitter.particleRenderMode = ParticleSystemRenderMode.VerticalBillboard; + break; + } + kSPParticleEmitter.uvAnimationXTile = ReadInt(); + kSPParticleEmitter.uvAnimationYTile = ReadInt(); + kSPParticleEmitter.uvAnimationCycles = ReadInt(); + int num = ReadInt(); + while (num >= matDummies.Count) + { + matDummies.Add(new PartReader.MaterialDummy()); + } + matDummies[num].particleEmitters.Add(kSPParticleEmitter); + } + + #endregion + + #region Mesh parsing + + private static Mesh ReadMesh() + { + Mesh mesh = new Mesh(); + EntryType entryType = (EntryType)ReadInt(); + if (entryType != EntryType.MeshStart) + { + Debug.LogError("Mesh Error"); + return null; + } + + int size = ReadInt(); + SkipInt(); + + int subMeshIndex = 0; + while ((entryType = (EntryType)ReadInt()) != EntryType.MeshEnd) + { + switch (entryType) + { + case EntryType.MeshVertexColors: + { + FillColor32Buffer(size); + mesh.SetColors(color32Buffer, 0, size); + + break; + } + case EntryType.MeshVerts: + { + FillVector3Buffer(size); + mesh.SetVertices(vector3Buffer, 0, size); + + break; + } + case EntryType.MeshUV: + { + FillVector2Buffer(size); + mesh.SetUVs(0, vector2Buffer, 0, size); + break; + } + case EntryType.MeshUV2: + { + FillVector2Buffer(size); + mesh.SetUVs(1, vector2Buffer, 0, size); + break; + } + case EntryType.MeshNormals: + { + FillVector3Buffer(size); + mesh.SetNormals(vector3Buffer, 0, size); + break; + } + case EntryType.MeshTangents: + { + FillVector4Buffer(size); + mesh.SetTangents(vector4Buffer, 0, size); + break; + } + case EntryType.MeshTriangles: + { + if (mesh.subMeshCount == subMeshIndex) + mesh.subMeshCount++; + + int triangleCount = ReadInt(); + FillIntBuffer(triangleCount); + mesh.SetTriangles(intBuffer, 0, triangleCount, subMeshIndex); + subMeshIndex++; + break; + } + case EntryType.MeshBoneWeights: + { + BoneWeight[] boneWeights = new BoneWeight[size]; + for (int i = 0; i < size; i++) + boneWeights[i] = ReadBoneWeight(); + + mesh.boneWeights = boneWeights; + break; + } + case EntryType.MeshBindPoses: + { + int bindPosesCount = ReadInt(); + Matrix4x4[] bindPoses = new Matrix4x4[bindPosesCount]; + for (int i = 0; i < bindPosesCount; i++) + bindPoses[i] = ReadMatrix4x4(); + + mesh.bindposes = bindPoses; + break; + } + } + } + mesh.RecalculateBounds(); + return mesh; + } + + // note : to fill mesh data, we take advantage of the fact that the various + // structs (int, vector3, etc) are sequentially packed in the raw data array. + // Ideally, we would pass the raw data array with a pointer offset to the various + // mesh.Set*() methods, but that would require those methods accepting a Span + // that we would built from an arbitrary pointer offset. + // So we fallback to a memcopy of the raw data to a buffer array that we then + // pass to the unity methods. + + private static unsafe void FillIntBuffer(int intCount) + { + int byteCount = intCount * 4; + int valIdx = Advance(byteCount); + + if (intBuffer == null || intBuffer.Length < intCount) + intBuffer = new int[(int)(intCount * 1.5)]; + + fixed (int* intBufferPtr = intBuffer) + Buffer.MemoryCopy(dataPtr + valIdx, intBufferPtr, byteCount, byteCount); + } + + private static unsafe void FillVector2Buffer(int vector2Count) + { + int byteCount = vector2Count * 8; + int valIdx = Advance(byteCount); + + if (vector2Buffer == null || vector2Buffer.Length < vector2Count) + vector2Buffer = new Vector2[(int)(vector2Count * 1.5)]; + + fixed (Vector2* vector2BufferPtr = vector2Buffer) + Buffer.MemoryCopy(dataPtr + valIdx, vector2BufferPtr, byteCount, byteCount); + } + + private static unsafe void FillVector3Buffer(int vector3Count) + { + int byteCount = vector3Count * 12; + int valIdx = Advance(byteCount); + + if (vector3Buffer == null || vector3Buffer.Length < vector3Count) + vector3Buffer = new Vector3[(int)(vector3Count * 1.5)]; + + fixed (Vector3* vector3BufferPtr = vector3Buffer) + Buffer.MemoryCopy(dataPtr + valIdx, vector3BufferPtr, byteCount, byteCount); + } + + private static unsafe void FillVector4Buffer(int vector4Count) + { + int byteCount = vector4Count * 16; + int valIdx = Advance(byteCount); + + if (vector4Buffer == null || vector4Buffer.Length < vector4Count) + vector4Buffer = new Vector4[(int)(vector4Count * 1.5)]; + + fixed (Vector4* vector4BufferPtr = vector4Buffer) + Buffer.MemoryCopy(dataPtr + valIdx, vector4BufferPtr, byteCount, byteCount); + } + + private static unsafe void FillColor32Buffer(int color32Count) + { + int byteCount = color32Count * 4; + int valIdx = Advance(byteCount); + + if (color32Buffer == null || color32Buffer.Length < color32Count) + color32Buffer = new Color32[(int)(color32Count * 1.5)]; + + fixed (Color32* color32BufferPtr = color32Buffer) + Buffer.MemoryCopy(dataPtr + valIdx, color32BufferPtr, byteCount, byteCount); + } + + + #endregion + + #region Base binary reader infrastructure + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Advance(int bytes) + { + int currentIndex = index; + int nextIndex = currentIndex + bytes; + if (nextIndex > dataLength) + ThrowEndOfDataException(); + + index = nextIndex; + return currentIndex; + } + + private static void ThrowEndOfDataException() + { + throw new InvalidOperationException("Unable to read beyond the end of the data"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe byte ReadByte() + { + int valIdx = Advance(1); + return *(dataPtr + valIdx); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SkipInt() + { + Advance(4); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe int ReadInt() + { + int valIdx = Advance(4); + return *(int*)(dataPtr + valIdx); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe float ReadFloat() + { + int valIdx = Advance(4); + return *(float*)(dataPtr + valIdx); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SkipBool() + { + Advance(1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe bool ReadBool() + { + int valIdx = Advance(1); + return *(dataPtr + valIdx) != 0; + } + + private static void SkipString() + { + Advance(Read7BitEncodedInt()); + } + + private static string ReadString() + { + int strByteLength = Read7BitEncodedInt(); + + if (strByteLength < 0) + throw new Exception("Invalid string length"); + + if (strByteLength == 0) + return string.Empty; + + ExpandCharBuffer(strByteLength); + + int currentPos = index; + Advance(strByteLength); + + int charCount = decoder.GetChars(data, currentPos, strByteLength, charBuffer, 0); + return new string(charBuffer, 0, charCount); + } + + private static void ExpandCharBuffer(int length) + { + if (charBuffer == null || charBuffer.Length < length) + charBuffer = new char[(int)(length * 1.5)]; + } + + private static int Read7BitEncodedInt() + { + int num = 0; + int num2 = 0; + byte b; + do + { + if (num2 == 35) + { + throw new FormatException("Too many bytes in what should have been a 7 bit encoded Int32."); + } + b = ReadByte(); + num |= (b & 0x7F) << num2; + num2 += 7; + } + while ((b & 0x80u) != 0); + return num; + } + + private static unsafe Vector2 ReadVector2() + { + int valIdx = Advance(8); + return *(Vector2*)(dataPtr + valIdx); + } + + private static unsafe Vector3 ReadVector3() + { + int valIdx = Advance(12); + return *(Vector3*)(dataPtr + valIdx); + } + + private static unsafe Vector4 ReadVector4() + { + int valIdx = Advance(16); + return *(Vector4*)(dataPtr + valIdx); + } + + private static unsafe Quaternion ReadQuaternion() + { + int valIdx = Advance(16); + return *(Quaternion*)(dataPtr + valIdx); + } + + private static unsafe Color ReadColor() + { + int valIdx = Advance(16); + return *(Color*)(dataPtr + valIdx); + } + + private static unsafe Color32 ReadColor32() + { + int valIdx = Advance(4); + return *(Color32*)(dataPtr + valIdx); + + Advance(4); + return new Color32(data[index - 4], data[index - 3], data[index - 2], data[index - 1]); + } + + private static unsafe BoneWeight ReadBoneWeight() + { + int valIdx = Advance(32); + // data isn't packed with the same layout as the struct, so we fallback to setting every field + return new BoneWeight() + { + boneIndex0 = *(int*)(dataPtr + valIdx), + weight0 = *(float*)(dataPtr + valIdx + 4), + boneIndex1 = *(int*)(dataPtr + valIdx + 8), + weight1 = *(float*)(dataPtr + valIdx + 12), + boneIndex2 = *(int*)(dataPtr + valIdx + 16), + weight2 = *(float*)(dataPtr + valIdx + 20), + boneIndex3 = *(int*)(dataPtr + valIdx + 24), + weight3 = *(float*)(dataPtr + valIdx + 28) + }; + } + + private static unsafe Matrix4x4 ReadMatrix4x4() + { + int valIdx = Advance(64); + // data isn't packed with the same layout as the struct, so we fallback to setting every field + return new Matrix4x4() + { + m00 = *(float*)(dataPtr + valIdx), + m01 = *(float*)(dataPtr + valIdx + 4), + m02 = *(float*)(dataPtr + valIdx + 8), + m03 = *(float*)(dataPtr + valIdx + 12), + m10 = *(float*)(dataPtr + valIdx + 16), + m11 = *(float*)(dataPtr + valIdx + 20), + m12 = *(float*)(dataPtr + valIdx + 24), + m13 = *(float*)(dataPtr + valIdx + 28), + m20 = *(float*)(dataPtr + valIdx + 32), + m21 = *(float*)(dataPtr + valIdx + 36), + m22 = *(float*)(dataPtr + valIdx + 40), + m23 = *(float*)(dataPtr + valIdx + 44), + m30 = *(float*)(dataPtr + valIdx + 48), + m31 = *(float*)(dataPtr + valIdx + 52), + m32 = *(float*)(dataPtr + valIdx + 56), + m33 = *(float*)(dataPtr + valIdx + 60) + }; + } + + private static unsafe Keyframe ReadKeyFrame() + { + // this is encoded as 4 floats (16 bytes), but there is 4 bytes of padding at the end + int valIdx = Advance(20); + return new Keyframe( + *(float*)(dataPtr + valIdx), + *(float*)(dataPtr + valIdx + 4), + *(float*)(dataPtr + valIdx + 8), + *(float*)(dataPtr + valIdx + 12)); + } + + #endregion + } +} diff --git a/KSPCommunityFixes/Library/ShaderHelpers.cs b/KSPCommunityFixes/Library/ShaderHelpers.cs new file mode 100644 index 00000000..a97be755 --- /dev/null +++ b/KSPCommunityFixes/Library/ShaderHelpers.cs @@ -0,0 +1,132 @@ +using PartToolsLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace KSPCommunityFixes.Library +{ + internal class ShaderHelpers + { + private static bool shadersPopulated = false; + + private static Shader _kspDiffuse; + private static Shader _kspSpecular; + private static Shader _kspBumped; + private static Shader _kspBumpedSpecular; + private static Shader _kspEmissiveDiffuse; + private static Shader _kspEmissiveSpecular; + private static Shader _kspEmissiveBumpedSpecular; + private static Shader _kspAlphaCutoff; + private static Shader _kspAlphaCutoffBumped; + private static Shader _kspAlphaTranslucent; + private static Shader _kspAlphaTranslucentSpecular; + private static Shader _kspAlphaUnlitTransparent; + private static Shader _kspUnlit; + private static Shader _kspParticlesAlphaBlended; + private static Shader _kspParticulesAdditive; + private static Shader _kspBumpedSpecularMapped; + + private static void PopulateShaders() + { + PopulateShader("KSP/Diffuse", ref _kspDiffuse); + PopulateShader("KSP/Specular", ref _kspSpecular); + PopulateShader("KSP/Bumped", ref _kspBumped); + PopulateShader("KSP/Bumped Specular", ref _kspBumpedSpecular); + PopulateShader("KSP/Emissive/Diffuse", ref _kspEmissiveDiffuse); + PopulateShader("KSP/Emissive/Specular", ref _kspEmissiveSpecular); + PopulateShader("KSP/Emissive/Bumped Specular", ref _kspEmissiveBumpedSpecular); + PopulateShader("KSP/Alpha/Cutoff", ref _kspAlphaCutoff); + PopulateShader("KSP/Alpha/Cutoff Bumped", ref _kspAlphaCutoffBumped); + PopulateShader("KSP/Alpha/Translucent", ref _kspAlphaTranslucent); + PopulateShader("KSP/Alpha/Translucent Specular", ref _kspAlphaTranslucentSpecular); + PopulateShader("KSP/Alpha/Unlit Transparent", ref _kspAlphaUnlitTransparent); + PopulateShader("KSP/Unlit", ref _kspUnlit); + PopulateShader("KSP/Particles/Alpha Blended", ref _kspParticlesAlphaBlended); + PopulateShader("KSP/Particles/Additive", ref _kspParticulesAdditive); + PopulateShader("KSP/Bumped Specular (Mapped)", ref _kspBumpedSpecularMapped); + shadersPopulated = true; + } + + private static void PopulateShader(string shaderName, ref Shader shaderRef) + { + shaderRef = Shader.Find(shaderName); + shaders[shaderName] = shaderRef; + } + + private static Dictionary shaders = new Dictionary(); + + public static Shader GetShader(ShaderType shaderType) + { + if (!shadersPopulated) + PopulateShaders(); + + switch (shaderType) + { + case ShaderType.Diffuse: + default: + return _kspDiffuse; + case ShaderType.Specular: + return _kspSpecular; + case ShaderType.Bumped: + return _kspBumped; + case ShaderType.BumpedSpecular: + return _kspBumpedSpecular; + case ShaderType.Emissive: + return _kspEmissiveDiffuse; + case ShaderType.EmissiveSpecular: + return _kspEmissiveSpecular; + case ShaderType.EmissiveBumpedSpecular: + return _kspEmissiveBumpedSpecular; + case ShaderType.AlphaCutout: + return _kspAlphaCutoff; + case ShaderType.AlphaCutoutBumped: + return _kspAlphaCutoffBumped; + case ShaderType.Alpha: + return _kspAlphaTranslucent; + case ShaderType.AlphaSpecular: + return _kspAlphaTranslucentSpecular; + case ShaderType.AlphaUnlit: + return _kspAlphaUnlitTransparent; + case ShaderType.Unlit: + return _kspUnlit; + case ShaderType.ParticleAlpha: + return _kspParticlesAlphaBlended; + case ShaderType.ParticleAdditive: + return _kspParticulesAdditive; + case ShaderType.BumpedSpecularMap: + return _kspBumpedSpecularMapped; + } + } + + public static Shader GetShader(string shaderName) + { + if (!shadersPopulated) + PopulateShaders(); + + if (!shaders.TryGetValue(shaderName, out Shader shader)) + { + shader = Shader.Find(shaderName); + if (shader.IsNotNullRef()) + shaders.Add(shaderName, shader); + } + + return shader; + } + + public static readonly int MainTexPropId = Shader.PropertyToID("_MainTex"); + public static readonly int BumpMapPropId = Shader.PropertyToID("_BumpMap"); + public static readonly int EmissivePropId = Shader.PropertyToID("_Emissive"); + public static readonly int SpecMapPropId = Shader.PropertyToID("_SpecMap"); + + public static readonly int SpecColorPropId = Shader.PropertyToID("_SpecColor"); + public static readonly int ShininessPropId = Shader.PropertyToID("_Shininess"); + public static readonly int CutoffPropId = Shader.PropertyToID("_Cutoff"); + public static readonly int GlossPropId = Shader.PropertyToID("_Gloss"); + public static readonly int ColorPropId = Shader.PropertyToID("_Color"); + public static readonly int SpecTintPropId = Shader.PropertyToID("_SpecTint"); + public static readonly int InvFadePropId = Shader.PropertyToID("_InvFade"); + } +} diff --git a/KSPCommunityFixes/Library/StaticHelpers.cs b/KSPCommunityFixes/Library/StaticHelpers.cs index ab45b73d..591b26da 100644 --- a/KSPCommunityFixes/Library/StaticHelpers.cs +++ b/KSPCommunityFixes/Library/StaticHelpers.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using HarmonyLib; using KSP.UI.TooltipTypes; +using PartToolsLib; using UnityEngine; namespace KSPCommunityFixes @@ -91,5 +93,8 @@ public static bool EditPartModuleKSPFieldAttributes(Type partModuleType, string /// /// The value. public static bool IsPowerOfTwo(uint value) => (value & (value - 1)) == 0 && value != 0; + + + } } diff --git a/KSPCommunityFixes/Performance/FastLoader.cs b/KSPCommunityFixes/Performance/FastLoader.cs index f26b41a4..ad570a5a 100644 --- a/KSPCommunityFixes/Performance/FastLoader.cs +++ b/KSPCommunityFixes/Performance/FastLoader.cs @@ -1,6 +1,7 @@ // #define DEBUG_TEXTURE_CACHE using DDSHeaders; +using Expansions; using HarmonyLib; using KSP.Localization; using KSPCommunityFixes.Library.Buffers; @@ -16,6 +17,7 @@ using System.Reflection.Emit; using System.Runtime.InteropServices; using System.Threading; +using KSPCommunityFixes.Library; using TMPro; using Unity.Collections; using UnityEngine; @@ -28,6 +30,93 @@ namespace KSPCommunityFixes.Performance { + [KSPAddon(KSPAddon.Startup.MainMenu, true)] + internal class KSPCFFastLoaderReport : MonoBehaviour + { + internal static float initialConfigLoadTime; + internal static Stopwatch wSecondConfigLoad = new Stopwatch(); + internal static Stopwatch wConfigTranslate = new Stopwatch(); + internal static Stopwatch wAssetsLoading = new Stopwatch(); + internal static Stopwatch wAudioLoading = new Stopwatch(); + internal static Stopwatch wTextureLoading = new Stopwatch(); + internal static Stopwatch wModelLoading = new Stopwatch(); + internal static Stopwatch wAssetBundleLoading = new Stopwatch(); + internal static Stopwatch wGamedatabaseLoading = new Stopwatch(); + internal static Stopwatch wBuiltInPartsCopy = new Stopwatch(); + internal static Stopwatch wPartConfigExtraction = new Stopwatch(); + internal static Stopwatch wPartCompilationLoading = new Stopwatch(); + internal static Stopwatch wInternalCompilationLoading = new Stopwatch(); + internal static Stopwatch wExpansionLoading = new Stopwatch(); + internal static Stopwatch wPSystemSetup = new Stopwatch(); + + internal static long audioBytesLoaded; + internal static int texturesLoaded; + internal static long texturesBytesLoaded; + internal static int modelsLoaded; + internal static long modelsBytesLoaded; + + void Start() + { + float totalLoadingTime = Time.realtimeSinceStartup; + int totalPartsLoaded = 0; + int totalModulesLoaded = 0; + foreach (AvailablePart availablePart in PartLoader.Instance.loadedParts) + { + if (availablePart.partPrefab.IsNotNullOrDestroyed()) + { + totalPartsLoaded++; + totalModulesLoaded += availablePart.partPrefab.modules.Count; + } + } + + int totalInternalsLoaded = PartLoader.Instance.internalParts.Count; + int totalInternalPropsLoaded = PartLoader.Instance.internalProps.Count; + + string log = + $"[KSPCF:FastLoader] {SystemInfo.processorType} | {SystemInfo.systemMemorySize} MB | {SystemInfo.graphicsDeviceName} ({SystemInfo.graphicsMemorySize} MB)\n" + + $"Total loading time to main menu : {totalLoadingTime:F3}s\n" + + $"- Configs and assemblies loaded in {initialConfigLoadTime:F3}s\n" + + $"- Configs reload done in {wSecondConfigLoad.Elapsed.TotalSeconds:F3}s\n" + + $"- Configs translated in {wConfigTranslate.Elapsed.TotalSeconds:F3}s\n" + + $"- {KSPCFFastLoader.loadedAssetCount} assets loaded in {wAssetsLoading.Elapsed.TotalSeconds:F3}s :\n" + + $" - {KSPCFFastLoader.audioFilesLoaded} audio assets ({StaticHelpers.HumanReadableBytes(audioBytesLoaded)}) in {wAudioLoading.Elapsed.TotalSeconds:F3}s\n" + + $" - {texturesLoaded} texture assets ({StaticHelpers.HumanReadableBytes(texturesBytesLoaded)}) in {wTextureLoading.Elapsed.TotalSeconds:F3}s, {StaticHelpers.HumanReadableBytes((long)(texturesBytesLoaded / wTextureLoading.Elapsed.TotalSeconds))}/s\n" + + $" - {modelsLoaded} model assets ({StaticHelpers.HumanReadableBytes(modelsBytesLoaded)}) in {wModelLoading.Elapsed.TotalSeconds:F3}s, {wModelLoading.Elapsed.TotalMilliseconds / modelsLoaded:F3} ms/model\n" + + $"- Asset bundles loaded in {wAssetBundleLoading.Elapsed.TotalSeconds:F3}s\n" + + $"- GameDatabase (configs, resources, traits, upgrades...) loaded in {wGamedatabaseLoading.Elapsed.TotalSeconds:F3}s\n" + + $"- Built-in parts copied in {wBuiltInPartsCopy.Elapsed.TotalSeconds:F3}s\n" + + $"- Part and internal configs extracted in {wPartConfigExtraction.Elapsed.TotalSeconds:F3}s\n" + + $"- {totalPartsLoaded} parts and {totalModulesLoaded} modules compiled in {wPartCompilationLoading.Elapsed.TotalSeconds:F3}s\n" + + $" ({totalModulesLoaded / (float)totalPartsLoaded:F1} modules/part, {wPartCompilationLoading.Elapsed.TotalMilliseconds / totalPartsLoaded:F3} ms/part, {wPartCompilationLoading.Elapsed.TotalMilliseconds / totalModulesLoaded:F3} ms/module)\n" + + $"- {totalInternalsLoaded} internal spaces and {totalInternalPropsLoaded} props compiled in {wInternalCompilationLoading.Elapsed.TotalSeconds:F3}s\n"; + + if (ExpansionsLoader.expansionsInfo.Count > 0) + log += $"- {ExpansionsLoader.expansionsInfo.Count} DLC ({ExpansionsLoader.expansionsInfo.Values.Join(info => info.DisplayName)}) loaded in {wExpansionLoading.Elapsed.TotalSeconds:F3}s\n"; + + log += + $"- Planetary system loaded in {wPSystemSetup.Elapsed.TotalSeconds:F3}s"; + + Debug.Log(log); + Debug.Log($"Texture queries : {KSPCFFastLoader.txcallCount}, slow path : {KSPCFFastLoader.txMissCount} ({KSPCFFastLoader.txMissCount / (float)KSPCFFastLoader.txcallCount:P2})"); + Debug.Log($"PartIcon compilation : {GeneralPerfFixes.watch.Elapsed.TotalMilliseconds:F3}"); + Destroy(gameObject); + } + } + + [KSPAddon(KSPAddon.Startup.PSystemSpawn, true)] + internal class KSPCFFastLoaderPSystemSetup : MonoBehaviour + { + internal static void PSystemManager_Awake_Prefix() + { + KSPCFFastLoaderReport.wPSystemSetup.Start(); + } + + void OnDestroy() + { + KSPCFFastLoaderReport.wPSystemSetup.Stop(); + } + } + [KSPAddon(KSPAddon.Startup.Instantly, true)] internal class KSPCFFastLoader : MonoBehaviour { @@ -58,8 +147,14 @@ internal class KSPCFFastLoader : MonoBehaviour // min amount of files to try to keep in memory, regardless of maxBufferSize private const int minFileRead = 10; - private static Harmony harmony; - private static string HarmonyID => typeof(KSPCFFastLoader).FullName; + private static Harmony persistentHarmony; + private static string PersistentHarmonyID => typeof(KSPCFFastLoader).FullName; + + private static Harmony assetAndPartLoaderHarmony; + private static string AssetAndPartLoaderHarmonyID => typeof(KSPCFFastLoader).FullName + "AssetAndPartLoader"; + + private static Harmony expansionsLoaderHarmony; + private static string ExpansionsLoaderHarmonyID => typeof(KSPCFFastLoader).FullName + "ExpansionsLoader"; public static KSPCFFastLoader loader; @@ -80,7 +175,12 @@ internal class KSPCFFastLoader : MonoBehaviour private Dictionary textureCacheData; private HashSet textureDataIds; private bool cacheUpdated = false; - + + internal static Dictionary modelsByUrl; + internal static Dictionary modelsByDirectoryUrl; + internal static Dictionary urlFilesByModel; + internal static Dictionary texturesByUrl; + private void Awake() { if (KSPCommunityFixes.KspVersion < new Version(1, 12, 3)) @@ -90,22 +190,52 @@ private void Awake() return; } + KSPCFFastLoaderReport.initialConfigLoadTime = Time.realtimeSinceStartup; + Debug.Log("[KSPCF] Injecting FastLoader..."); loader = this; IsPatchEnabled = true; - harmony = new Harmony(HarmonyID); + + persistentHarmony = new Harmony(PersistentHarmonyID); + + MethodInfo m_GameDatabase_GetModelPrefab = AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetModelPrefab)); + MethodInfo p_GameDatabase_GetModelPrefab = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_GetModelPrefab_Prefix)); + persistentHarmony.Patch(m_GameDatabase_GetModelPrefab, new HarmonyMethod(p_GameDatabase_GetModelPrefab)); + + MethodInfo m_GameDatabase_GetModelPrefabIn = AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetModelPrefabIn)); + MethodInfo p_GameDatabase_GetModelPrefabIn = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_GetModelPrefabIn_Prefix)); + persistentHarmony.Patch(m_GameDatabase_GetModelPrefabIn, new HarmonyMethod(p_GameDatabase_GetModelPrefabIn)); + + MethodInfo m_GameDatabase_GetModelFile = AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetModelFile), new []{typeof(GameObject)}); + MethodInfo p_GameDatabase_GetModelFile = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_GetModelFile_Prefix)); + persistentHarmony.Patch(m_GameDatabase_GetModelFile, new HarmonyMethod(p_GameDatabase_GetModelFile)); + + MethodInfo m_GameDatabase_GetTextureInfo = AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetTextureInfo)); + MethodInfo p_GameDatabase_GetTextureInfo = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_GetTextureInfo_Prefix)); + persistentHarmony.Patch(m_GameDatabase_GetTextureInfo, new HarmonyMethod(p_GameDatabase_GetTextureInfo)); + + MethodInfo m_GameDatabase_GetTexture = AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetTexture)); + MethodInfo p_GameDatabase_GetTexture = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_GetTexture_Prefix)); + persistentHarmony.Patch(m_GameDatabase_GetTexture, new HarmonyMethod(p_GameDatabase_GetTexture)); + + MethodInfo m_PSystemManager_Awake = AccessTools.Method(typeof(PSystemManager), nameof(PSystemManager.Awake)); + MethodInfo p_PSystemManager_Awake = AccessTools.Method(typeof(KSPCFFastLoaderPSystemSetup), nameof(KSPCFFastLoaderPSystemSetup.PSystemManager_Awake_Prefix)); + persistentHarmony.Patch(m_PSystemManager_Awake, new HarmonyMethod(p_PSystemManager_Awake)); + + assetAndPartLoaderHarmony = new Harmony(AssetAndPartLoaderHarmonyID); MethodInfo m_GameDatabase_SetupMainLoaders = AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.SetupMainLoaders)); MethodInfo t_GameDatabase_SetupMainLoaders = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_SetupMainLoaders_Prefix)); - harmony.Patch(m_GameDatabase_SetupMainLoaders, new HarmonyMethod(t_GameDatabase_SetupMainLoaders)); + assetAndPartLoaderHarmony.Patch(m_GameDatabase_SetupMainLoaders, new HarmonyMethod(t_GameDatabase_SetupMainLoaders)); MethodInfo m_GameDatabase_LoadAssetBundleObjects_MoveNext = AccessTools.EnumeratorMoveNext(AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.LoadAssetBundleObjects))); - MethodInfo t_GameDatabase_LoadAssetBundleObjects_MoveNext = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_LoadAssetBundleObjects_MoveNext_Prefix)); - harmony.Patch(m_GameDatabase_LoadAssetBundleObjects_MoveNext, new HarmonyMethod(t_GameDatabase_LoadAssetBundleObjects_MoveNext)); + MethodInfo pr_GameDatabase_LoadAssetBundleObjects_MoveNext = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_LoadAssetBundleObjects_MoveNext_Prefix)); + MethodInfo po_GameDatabase_LoadAssetBundleObjects_MoveNext = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_LoadAssetBundleObjects_MoveNext_Postfix)); + assetAndPartLoaderHarmony.Patch(m_GameDatabase_LoadAssetBundleObjects_MoveNext, new HarmonyMethod(pr_GameDatabase_LoadAssetBundleObjects_MoveNext), new HarmonyMethod(po_GameDatabase_LoadAssetBundleObjects_MoveNext)); MethodInfo m_PartLoader_StartLoad = AccessTools.Method(typeof(PartLoader), nameof(PartLoader.StartLoad)); MethodInfo t_PartLoader_StartLoad = AccessTools.Method(typeof(KSPCFFastLoader), nameof(PartLoader_StartLoad_Transpiler)); - harmony.Patch(m_PartLoader_StartLoad, null, null, new HarmonyMethod(t_PartLoader_StartLoad)); + assetAndPartLoaderHarmony.Patch(m_PartLoader_StartLoad, null, null, new HarmonyMethod(t_PartLoader_StartLoad)); PatchStartCoroutineInCoroutine(AccessTools.Method(typeof(PartLoader), nameof(PartLoader.CompileParts))); PatchStartCoroutineInCoroutine(AccessTools.Method(typeof(DragCubeSystem), nameof(DragCubeSystem.SetupDragCubeCoroutine), new[] { typeof(Part) })); @@ -114,7 +244,14 @@ private void Awake() // Fix for issue #114 : Drag cubes are incorrectly calculated with KSPCF 1.24.1 MethodInfo m_DragCubeSystem_RenderDragCubes_MoveNext = AccessTools.EnumeratorMoveNext(AccessTools.Method(typeof(DragCubeSystem), nameof(DragCubeSystem.RenderDragCubes))); MethodInfo m_DragCubeSystem_RenderDragCubes_MoveNext_Transpiler = AccessTools.Method(typeof(KSPCFFastLoader), nameof(DragCubeSystem_RenderDragCubes_MoveNext_Transpiler)); - harmony.Patch(m_DragCubeSystem_RenderDragCubes_MoveNext, null, null, new HarmonyMethod(m_DragCubeSystem_RenderDragCubes_MoveNext_Transpiler)); + assetAndPartLoaderHarmony.Patch(m_DragCubeSystem_RenderDragCubes_MoveNext, null, null, new HarmonyMethod(m_DragCubeSystem_RenderDragCubes_MoveNext_Transpiler)); + + expansionsLoaderHarmony = new Harmony(ExpansionsLoaderHarmonyID); + MethodInfo m_ExpansionsLoader_StartLoad = AccessTools.Method(typeof(ExpansionsLoader), nameof(PartLoader.StartLoad)); + MethodInfo p_ExpansionsLoader_StartLoad = AccessTools.Method(typeof(KSPCFFastLoader), nameof(ExpansionsLoader_StartLoad_Prefix)); + expansionsLoaderHarmony.Patch(m_ExpansionsLoader_StartLoad, new HarmonyMethod(p_ExpansionsLoader_StartLoad)); + GameEvents.OnExpansionSystemLoaded.Add(OnExpansionSystemLoaded); + GameEvents.OnGameDatabaseLoaded.Add(OnGameDatabaseLoaded); configPath = ConfigPath; textureCachePath = Path.Combine(ModPath, "PluginData", "TextureCache"); @@ -150,8 +287,8 @@ void OnDestroy() if (!IsPatchEnabled) return; - harmony.UnpatchAll(HarmonyID); - harmony = null; + assetAndPartLoaderHarmony.UnpatchAll(AssetAndPartLoaderHarmonyID); + assetAndPartLoaderHarmony = null; loader = null; } @@ -255,9 +392,37 @@ static bool GameDatabase_LoadAssetBundleObjects_MoveNext_Prefix(object __instanc return false; } + KSPCFFastLoaderReport.wAssetBundleLoading.Start(); return true; } + static void GameDatabase_LoadAssetBundleObjects_MoveNext_Postfix(object __instance, ref bool __result) + { + if (!__result) + { + KSPCFFastLoaderReport.wAssetBundleLoading.Stop(); + KSPCFFastLoaderReport.wGamedatabaseLoading.Start(); + } + } + + private void OnGameDatabaseLoaded() + { + KSPCFFastLoaderReport.wGamedatabaseLoading.Stop(); + GameEvents.OnGameDatabaseLoaded.Remove(OnGameDatabaseLoaded); + } + + + + + static void ExpansionsLoader_StartLoad_Prefix() => KSPCFFastLoaderReport.wExpansionLoading.Start(); + + private void OnExpansionSystemLoaded() + { + KSPCFFastLoaderReport.wExpansionLoading.Stop(); + expansionsLoaderHarmony.UnpatchAll(ExpansionsLoaderHarmonyID); + GameEvents.OnExpansionSystemLoaded.Remove(OnExpansionSystemLoaded); + } + #endregion #region Asset loader reimplementation (main coroutine) @@ -268,7 +433,7 @@ static bool GameDatabase_LoadAssetBundleObjects_MoveNext_Prefix(object __instanc static double ElapsedTime => Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency; static int totalAssetCount; - static int loadedAssetCount; + internal static int loadedAssetCount; /// /// Custom partial reimplementation of the stock GameDatabase.LoadObjects() coroutine @@ -290,10 +455,14 @@ static IEnumerator FastAssetLoader(List configFileTypes) // However, the full reload means mods can take the opportunity to generate configs/assets on // the fly from Awake() in a Startup.Instantly KSPAddon and have it being loaded. I've found // at least 2 mods doing that, so unfortunately this can't really be optimized... + KSPCFFastLoaderReport.wSecondConfigLoad.Restart(); gdb._root = new UrlDir(gdb.urlConfig.ToArray(), configFileTypes.ToArray()); + KSPCFFastLoaderReport.wSecondConfigLoad.Stop(); // Optimized version of GameDatabase.translateLoadedNodes() + KSPCFFastLoaderReport.wConfigTranslate.Restart(); TranslateLoadedNodes(gdb); + KSPCFFastLoaderReport.wConfigTranslate.Stop(); yield return null; gdb.progressTitle = "Waiting for PNGTextureCache opt-in..."; @@ -303,6 +472,7 @@ static IEnumerator FastAssetLoader(List configFileTypes) gdb.progressTitle = "Searching assets to load..."; yield return null; + KSPCFFastLoaderReport.wAssetsLoading.Restart(); double nextFrameTime = ElapsedTime + minFrameTimeD; // Files loaded by our custom loaders @@ -413,6 +583,7 @@ static IEnumerator FastAssetLoader(List configFileTypes) } gdb.progressTitle = "Loading sound assets..."; + KSPCFFastLoaderReport.wAudioLoading.Restart(); yield return null; // call non-stock audio loaders @@ -500,10 +671,13 @@ static IEnumerator FastAssetLoader(List configFileTypes) // start texture loading gdb.progressFraction = 0.25f; + KSPCFFastLoaderReport.wAudioLoading.Stop(); + KSPCFFastLoaderReport.wTextureLoading.Restart(); gdb.progressTitle = "Loading texture assets..."; yield return null; // call non-stock texture loaders + texturesByUrl = new Dictionary(allTextureFiles.Count); unsupportedFilesCount = unsupportedTextureFiles.Count; loadersCount = gdb.loadersTexture.Count; @@ -548,6 +722,7 @@ static IEnumerator FastAssetLoader(List configFileTypes) } // call our custom loader + yield return gdb.StartCoroutine(FilesLoader(textureAssets, allTextureFiles, "Loading texture asset")); // write texture cache json to disk @@ -556,10 +731,15 @@ static IEnumerator FastAssetLoader(List configFileTypes) // start model loading gdb.progressFraction = 0.75f; + KSPCFFastLoaderReport.wTextureLoading.Stop(); + KSPCFFastLoaderReport.wModelLoading.Start(); gdb.progressTitle = "Loading model assets..."; yield return null; // call non-stock model loaders + modelsByUrl = new Dictionary(allModelFiles.Count); + modelsByDirectoryUrl = new Dictionary(allModelFiles.Count); + urlFilesByModel = new Dictionary(allModelFiles.Count); unsupportedFilesCount = unsupportedModelFiles.Count; loadersCount = gdb.loadersModel.Count; @@ -606,11 +786,14 @@ static IEnumerator FastAssetLoader(List configFileTypes) // all done, do some cleanup arrayPool = null; + MuParser.ReleaseBuffers(); // stock stuff gdb.lastLoadTime = KSPUtil.SystemDateTime.DateTimeNow(); gdb.progressFraction = 1f; loadObjectsInProgress = false; + KSPCFFastLoaderReport.wModelLoading.Stop(); + KSPCFFastLoaderReport.wAssetsLoading.Stop(); } /// @@ -665,7 +848,8 @@ private static void TranslateLoadedNodes(GameDatabase gdb) #region Asset loader reimplementation (audio loader) static int concurrentAudioCoroutines; - static int audioFilesLoaded; + internal static int audioFilesLoaded; + /// /// Concurrent coroutines (read "multiple coroutines in the same frame") audio loader @@ -676,7 +860,9 @@ static IEnumerator AudioLoader(UrlFile urlFile) try { - string normalizedUri = KSPUtil.ApplicationFileProtocol + new FileInfo(urlFile.fullPath).FullName; + FileInfo fileInfo = new FileInfo(urlFile.fullPath); + KSPCFFastLoaderReport.audioBytesLoaded += fileInfo.Length; + string normalizedUri = KSPUtil.ApplicationFileProtocol + fileInfo.FullName; UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(normalizedUri, AudioType.UNKNOWN); yield return request.SendWebRequest(); while (!request.isDone) @@ -710,6 +896,7 @@ static IEnumerator AudioLoader(UrlFile urlFile) static int loadedBytes; static object lockObject = new object(); + /// /// Textures / models loader coroutine implementing threaded disk reads and framerate decoupling /// @@ -1041,6 +1228,9 @@ public void LoadAndDisposeMainThread() textureInfo.name = file.url; textureInfo.texture.name = file.url; Instance.databaseTexture.Add(textureInfo); + texturesByUrl[file.url] = textureInfo; + KSPCFFastLoaderReport.texturesBytesLoaded += dataLength; + KSPCFFastLoaderReport.texturesLoaded++; } } else if (file.fileType == FileType.Model) @@ -1074,6 +1264,13 @@ public void LoadAndDisposeMainThread() model.SetActive(false); Instance.databaseModel.Add(model); Instance.databaseModelFiles.Add(file); + modelsByUrl[file.url] = model; + // if multiple models in the same dir, we only add the first + // to ensure identical behavior as the GameDatabase.GetModelPrefabIn() method + modelsByDirectoryUrl.TryAdd(file.parent.url, model); + urlFilesByModel.Add(model, file); + KSPCFFastLoaderReport.modelsBytesLoaded += dataLength; + KSPCFFastLoaderReport.modelsLoaded++; } } } @@ -1475,6 +1672,7 @@ private TextureInfo LoadTRUECOLOR() return new TextureInfo(file, texture, isNormalMap, !isNormalMap, false); } + /* private static void InitPartReader() { if (PartReader.matDummies == null) @@ -1553,6 +1751,12 @@ private GameObject LoadMU() } return gameObject; } + */ + + private GameObject LoadMU() + { + return MuParser.Parse(file.parent.url, buffer, dataLength); + } private GameObject LoadDAE() { @@ -1711,8 +1915,6 @@ private static IEnumerable PartLoader_StartLoad_Transpiler(IEnu private static IEnumerator PartLoader_CompileAll() { - Stopwatch watch = Stopwatch.StartNew(); - PartLoader instance = PartLoader.Instance; if (instance._recompile) @@ -1721,6 +1923,8 @@ private static IEnumerator PartLoader_CompileAll() } instance.progressTitle = ""; instance.progressFraction = 0f; + KSPCFFastLoaderReport.wBuiltInPartsCopy.Restart(); + // copy the prebuilt parts (eva kerbals and flags) into the loaded part db for (int i = 0; i < instance.initialPartsLength; i++) { AvailablePart availablePart = new AvailablePart(instance.parts[i]); @@ -1751,10 +1955,13 @@ private static IEnumerator PartLoader_CompileAll() } instance.loadedParts.Add(availablePart); } + KSPCFFastLoaderReport.wBuiltInPartsCopy.Stop(); + KSPCFFastLoaderReport.wPartConfigExtraction.Restart(); UrlConfig[] configs = GameDatabase.Instance.GetConfigs("PART"); UrlConfig[] allPropNodes = GameDatabase.Instance.GetConfigs("PROP"); UrlConfig[] allSpaceNodes = GameDatabase.Instance.GetConfigs("INTERNAL"); UrlConfig[] configs2 = GameDatabase.Instance.GetConfigs("VARIANTTHEME"); + KSPCFFastLoaderReport.wPartConfigExtraction.Stop(); int num = configs.Length + allPropNodes.Length + allSpaceNodes.Length; instance.progressDelta = 1f / num; instance.InitializePartDatabase(); @@ -1762,12 +1969,15 @@ private static IEnumerator PartLoader_CompileAll() instance.APFinderByName.Clear(); instance.CompileVariantThemes(configs2); + KSPCFFastLoaderReport.wPartCompilationLoading.Restart(); PartCompilationInProgress = true; IEnumerator compilePartsEnumerator = FrameUnlockedCoroutine(instance.CompileParts(configs)); while (compilePartsEnumerator.MoveNext()) yield return null; PartCompilationInProgress = false; + KSPCFFastLoaderReport.wPartCompilationLoading.Stop(); + KSPCFFastLoaderReport.wInternalCompilationLoading.Restart(); IEnumerator compileInternalPropsEnumerator = FrameUnlockedCoroutine(instance.CompileInternalProps(allPropNodes)); while (compileInternalPropsEnumerator.MoveNext()) yield return null; @@ -1775,16 +1985,12 @@ private static IEnumerator PartLoader_CompileAll() IEnumerator compileInternalSpacesEnumerator = FrameUnlockedCoroutine(instance.CompileInternalSpaces(allSpaceNodes)); while (compileInternalSpacesEnumerator.MoveNext()) yield return null; + KSPCFFastLoaderReport.wInternalCompilationLoading.Stop(); Destroy(loader); instance.SavePartDatabase(); - Debug.Log($"PartLoader: {configs.Length} parts compiled"); - Debug.Log($"PartLoader: {allPropNodes.Length} internal props compiled"); - Debug.Log($"PartLoader: {allSpaceNodes.Length} internal spaces compiled"); - Debug.Log($"PartLoader: compilation took {watch.Elapsed.TotalSeconds:F3}s"); - instance._recompile = false; PartUpgradeManager.Handler.LinkUpgrades(); GameEvents.OnUpgradesLinked.Fire(); @@ -1804,7 +2010,7 @@ private static IEnumerator PartLoader_CompileAll() private static void PatchStartCoroutineInCoroutine(MethodInfo coroutine) { MethodInfo t_StartCoroutinePassThroughTranspiler = AccessTools.Method(typeof(KSPCFFastLoader), nameof(StartCoroutinePassThroughTranspiler)); - harmony.Patch(AccessTools.EnumeratorMoveNext(coroutine), null, null, new HarmonyMethod(t_StartCoroutinePassThroughTranspiler)); + assetAndPartLoaderHarmony.Patch(AccessTools.EnumeratorMoveNext(coroutine), null, null, new HarmonyMethod(t_StartCoroutinePassThroughTranspiler)); } /// @@ -2439,6 +2645,158 @@ public void Show() } + #endregion + + #region General perf patches + + static bool GameDatabase_GetModelPrefab_Prefix(GameDatabase __instance, string url, out GameObject __result) + { + if (url == null) + { + __result = null; + return false; + } + + if (!modelsByUrl.TryGetValue(url, out __result)) + { + // We need a fallback because models are also added from asset bundles, + // and because anyone could be adding models as the databaseModel list is public + List models = __instance.databaseModel; + for (int i = models.Count; i-- > 0;) + { + if (models[i].name == url) + { + __result = models[i]; + modelsByUrl.Add(url, __result); + break; + } + } + } + + return false; + } + + static bool GameDatabase_GetModelPrefabIn_Prefix(GameDatabase __instance, string url, out GameObject __result) + { + if (url == null) + { + __result = null; + return false; + } + + if (!modelsByDirectoryUrl.TryGetValue(url, out __result)) + { + // We need a fallback because models are also added from asset bundles, + // and because anyone could be adding models as the databaseModel list is public + List models = __instance.databaseModel; + for (int i = models.Count; i-- > 0;) + { + string modelName = models[i].name; + if (modelName.Substring(0, modelName.LastIndexOf('/')) == url) + { + __result = models[i]; + modelsByDirectoryUrl.Add(url, __result); + break; + } + } + } + + return false; + } + + static bool GameDatabase_GetModelFile_Prefix(GameDatabase __instance, GameObject modelPrefab, out UrlFile __result) + { + if (modelPrefab.IsNullRef()) + { + __result = null; + return false; + } + + if (!urlFilesByModel.TryGetValue(modelPrefab, out __result)) + { + // We need a fallback because models are also added from asset bundles, + // and because anyone could be adding models as the databaseModel list is public + List models = __instance.databaseModel; + for (int i = models.Count; i-- > 0;) + { + if (models[i] == modelPrefab) + { + __result = __instance.databaseModelFiles[i]; + urlFilesByModel.Add(modelPrefab, __result); + break; + } + } + } + + return false; + } + + static bool GameDatabase_GetTextureInfo_Prefix(GameDatabase __instance, string url, out TextureInfo __result) + { + txcallCount++; + if (url == null) + { + __result = null; + return false; + } + + if (__instance.flagSwaps.TryGetValue(url, out string newUrl)) + url = newUrl; + + if (!texturesByUrl.TryGetValue(url, out __result)) + { + for (int i = __instance.databaseTexture.Count; i-- > 0;) + { + if (string.Equals(url, __instance.databaseTexture[i].name, StringComparison.OrdinalIgnoreCase)) + { + __result = __instance.databaseTexture[i]; + texturesByUrl.Add(url, __result); + txMissCount++; + break; + } + } + } + return false; + } + + internal static int txcallCount; + internal static int txMissCount; + + static bool GameDatabase_GetTexture_Prefix(GameDatabase __instance, string url, bool asNormalMap, out Texture2D __result) + { + txcallCount++; + if (url == null) + { + __result = null; + return false; + } + + if (__instance.flagSwaps.TryGetValue(url, out string newUrl)) + url = newUrl; + + if (!texturesByUrl.TryGetValue(url, out TextureInfo textureInfo)) + { + for (int i = __instance.databaseTexture.Count; i-- > 0;) + { + if (string.Equals(url, __instance.databaseTexture[i].name, StringComparison.OrdinalIgnoreCase)) + { + + textureInfo = __instance.databaseTexture[i]; + texturesByUrl.Add(url, textureInfo); + txMissCount++; + break; + } + } + } + + if (textureInfo != null) + __result = asNormalMap ? textureInfo.normalMap : textureInfo.texture; + else + __result = null; + + return false; + } + #endregion } diff --git a/KSPCommunityFixes/Performance/FlightIntegratorPerf.cs b/KSPCommunityFixes/Performance/FlightIntegratorPerf.cs new file mode 100644 index 00000000..e16865e1 --- /dev/null +++ b/KSPCommunityFixes/Performance/FlightIntegratorPerf.cs @@ -0,0 +1,554 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using UnityEngine; +using static EdyCommonTools.RotationController; +using static EdyCommonTools.Spline; +using Debug = UnityEngine.Debug; + +namespace KSPCommunityFixes.Performance +{ + internal class FlightIntegratorPerf : BasePatch + { + private static Stopwatch updateOcclusionWatch = new Stopwatch(); + private static int callCount; + + protected override void ApplyPatches(List patches) + { + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateOcclusion)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Postfix, + AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateOcclusion)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateOcclusionSolar)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateOcclusionBody)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateMassStats)), + this)); + + // other offenders, in aero situations : + + // AddSurfaceDragDirection : 7% + // - could turn the curves (1.6%) into lookup tables + // - general float to double pass, not sure how practical due to working a lot with float-backed drag cubes, relevant at least for the Mathf.Pow() call + // - multiple InverseTransform calls, use the matrix instead + + // general optimization pass on UpdateOcclusionConvection() : 4.4% + + } + + // Avoid setting RigidBody.mass and RigidBody.centerOfMass for all parts on every update if they didn't change + // Setting these properties is quite costly on the PhysX side, especially centerOfMass (1% to 2% of the frame time + // depending on the situation), and centerOfMass should almost never change unless somthing is changing CoMOffset. + // Setting mass is less costly and will change relatively often but avoiding setting when unecessary is still a decent improvement. + // We also take the opportunity to make a few optimizations (faster null checks, inlined inner loop, using the PartResourceList + // backing list instead of going through the custom indexer...) + static bool FlightIntegrator_UpdateMassStats_Prefix(FlightIntegrator __instance) + { + List parts = __instance.vessel.parts; + for (int i = __instance.partCount; i-- > 0;) + { + Part part = parts[i]; + + List partResources = part._resources.dict.list; + float resourceMass = 0f; + double resourceThermalMass = 0.0; + for (int j = partResources.Count; j-- > 0;) + { + PartResource partResource = partResources[j]; + float resMass = (float)partResource.amount * partResource.info.density; + resourceMass += resMass; + resourceThermalMass += resMass * partResource.info._specificHeatCapacity; + } + + part.resourceMass = resourceMass; + part.resourceThermalMass = resourceThermalMass; + part.thermalMass = part.mass * __instance.cacheStandardSpecificHeatCapacity * part.thermalMassModifier + part.resourceThermalMass; + __instance.SetSkinThermalMass(part); + part.thermalMass = Math.Max(part.thermalMass - part.skinThermalMass, 0.1); + part.thermalMassReciprocal = 1.0 / part.thermalMass; + } + + for (int i = __instance.partCount; i-- > 0;) + { + Part part = parts[i]; + if (part.rb.IsNotNullOrDestroyed()) + { + bool shouldSetRBProperties = !part.packed; + float physicsMass = part.mass + part.resourceMass + __instance.GetPhysicslessChildMass(part); // don't use recursion, avoid the null check + physicsMass = Mathf.Clamp(physicsMass, part.partInfo.MinimumMass, Mathf.Abs(physicsMass)); + if (part.physicsMass != physicsMass) + { + part.physicsMass = physicsMass; + if (shouldSetRBProperties) + { + float rbMass = Mathf.Max(part.partInfo.MinimumRBMass, physicsMass); + if (part.servoRb.IsNotNullOrDestroyed()) + { + float halfRBMass = rbMass * 0.5f; + part.rb.mass = halfRBMass; + part.servoRb.mass = halfRBMass; + } + else + { + part.rb.mass = rbMass; + } + } + } + + if (shouldSetRBProperties && part.rb.centerOfMass != part.CoMOffset) + part.rb.centerOfMass = part.CoMOffset; + } + else + { + part.physicsMass = 0.0; + } + } + + return false; + } + + static void FlightIntegrator_UpdateOcclusion_Prefix(bool all) + { + if (!all) + { + callCount++; + updateOcclusionWatch.Start(); + } + } + + static void FlightIntegrator_UpdateOcclusion_Postfix() + { + updateOcclusionWatch.Stop(); + if (callCount % 100 == 0) + { + Debug.Log($"[UpdateOcclusion] {updateOcclusionWatch.Elapsed.TotalMilliseconds / callCount:F3}ms/call"); + } + } + + static bool FlightIntegrator_UpdateOcclusionSolar_Prefix(FlightIntegrator __instance) + { + FlightIntegrator fi = __instance; + List occlusionDataList = fi.occlusionSun; + OcclusionCylinder[] occluders = fi.occludersSun; + Vector3d velocity = fi.sunVector; + + bool requiresSort = false; + + if (fi.partThermalDataCount != fi.partThermalDataList.Count) + { + fi.recreateThermalGraph = true; + fi.CheckThermalGraph(); + } + + int lastPartIndex = fi.partThermalDataCount - 1; + int partIndex = fi.partThermalDataCount; + QuaternionDPointRotation velToUp = new QuaternionDPointRotation(FromToRotation(velocity, Vector3d.up)); + while (partIndex-- > 0) + { + OcclusionData occlusionDataToUpdate = occlusionDataList[partIndex]; + UpdateOcclusionData(occlusionDataToUpdate, velocity, velToUp); + if (!requiresSort && partIndex < lastPartIndex && occlusionDataList[partIndex + 1].maximumDot < occlusionDataToUpdate.maximumDot) + requiresSort = true; + } + + if (requiresSort) + occlusionDataList.Sort(); + + OcclusionData occlusionData = occlusionDataList[lastPartIndex]; + occlusionData.ptd.sunAreaMultiplier = 1.0; + occlusionData.sunCyl.Setup(occlusionData); + occluders[0] = occlusionData.sunCyl; + + // O(n²) [n = part count] situation here, so micro-optimizing the inner loop is critical. + int occluderCount = 1; + int index = lastPartIndex; + while (index-- > 0) + { + occlusionData = occlusionDataList[index]; + double minExtentsX = occlusionData.minExtents.x; + double minExtentsY = occlusionData.minExtents.y; + double maxExtentsX = occlusionData.maxExtents.x; + double maxExtentsY = occlusionData.maxExtents.y; + + double areaMultiplier = 1.0; + + for (int i = 0; i < occluderCount; i++) + { + // GetCylinderOcclusion + OcclusionCylinder occluder = occluders[i]; + double offsetX = occluder.offset.x; + double offsetY = occluder.offset.y; + double minX = offsetX + minExtentsX; + double minY = offsetY + minExtentsY; + double maxX = offsetX + maxExtentsX; + double maxY = offsetY + maxExtentsY; + double centralExtentX = occluder.extents.x; + double centralExtentY = occluder.extents.y; + double centralExtentXInv = -centralExtentX; + double centralExtentYInv = -centralExtentY; + + double mid = (maxX - minX) * (maxY - minY); + if (maxX >= centralExtentXInv && minX <= centralExtentX && maxY >= centralExtentYInv && minY <= centralExtentY && mid != 0.0) + { + double midX = Math.Min(centralExtentX, maxX) - Math.Max(centralExtentXInv, minX); + if (midX < 0.0) + midX = 0.0; + + double midY = Math.Min(centralExtentY, maxY) - Math.Max(centralExtentYInv, minY); + if (midY < 0.0) + midY = 0.0; + + double rectRect = midX * midY / mid; + if (double.IsNaN(rectRect)) // it could be nice to put that outside the inner loop + { + if (GameSettings.FI_LOG_TEMP_ERROR) + Debug.LogError("[FlightIntegrator]: For part " + occlusionData.ptd.part.name + ", rectRect is NaN"); + } + else + { + areaMultiplier -= rectRect; + } + } + + if (areaMultiplier < 0.001) + { + areaMultiplier = 0.0; + break; + } + } + + occlusionData.ptd.sunAreaMultiplier = areaMultiplier; + if (areaMultiplier > 0) + { + occlusionData.sunCyl.Setup(occlusionData); + occluders[occluderCount] = occlusionData.sunCyl; + occluderCount++; + } + } + + return false; + } + + static bool FlightIntegrator_UpdateOcclusionBody_Prefix(FlightIntegrator __instance) + { + FlightIntegrator fi = __instance; + List occlusionDataList = fi.occlusionBody; + OcclusionCylinder[] occluders = fi.occludersBody; + Vector3d velocity = -fi.vessel.upAxis; + + bool requiresSort = false; + + if (fi.partThermalDataCount != fi.partThermalDataList.Count) + { + fi.recreateThermalGraph = true; + fi.CheckThermalGraph(); + } + + int lastPartIndex = fi.partThermalDataCount - 1; + int partIndex = fi.partThermalDataCount; + QuaternionDPointRotation velToUp = new QuaternionDPointRotation(FromToRotation(velocity, Vector3d.up)); + while (partIndex-- > 0) + { + OcclusionData occlusionDataToUpdate = occlusionDataList[partIndex]; + UpdateOcclusionData(occlusionDataToUpdate, velocity, velToUp); + if (!requiresSort && partIndex < lastPartIndex && occlusionDataList[partIndex + 1].maximumDot < occlusionDataToUpdate.maximumDot) + requiresSort = true; + } + + if (requiresSort) + occlusionDataList.Sort(); + + OcclusionData occlusionData = occlusionDataList[lastPartIndex]; + occlusionData.ptd.bodyAreaMultiplier = 1.0; + occlusionData.bodyCyl.Setup(occlusionData); + occluders[0] = occlusionData.bodyCyl; + + int occluderCount = 1; + int index = lastPartIndex; + while (index-- > 0) + { + occlusionData = occlusionDataList[index]; + double minExtentsX = occlusionData.minExtents.x; + double minExtentsY = occlusionData.minExtents.y; + double maxExtentsX = occlusionData.maxExtents.x; + double maxExtentsY = occlusionData.maxExtents.y; + + double areaMultiplier = 1.0; + + for (int i = 0; i < occluderCount; i++) + { + // GetCylinderOcclusion + OcclusionCylinder occluder = occluders[i]; + double offsetX = occluder.offset.x; + double offsetY = occluder.offset.y; + double minX = offsetX + minExtentsX; + double minY = offsetY + minExtentsY; + double maxX = offsetX + maxExtentsX; + double maxY = offsetY + maxExtentsY; + double centralExtentX = occluder.extents.x; + double centralExtentY = occluder.extents.y; + + double mid = (maxX - minX) * (maxY - minY); + if (!(maxX < 0.0 - centralExtentX) && !(minX > centralExtentX) && !(maxY < 0.0 - centralExtentY) && !(minY > centralExtentY) && mid != 0.0) + { + double rectRect = Math.Max(0.0, Math.Min(centralExtentX, maxX) - Math.Max(0.0 - centralExtentX, minX)) * Math.Max(0.0, Math.Min(centralExtentY, maxY) - Math.Max(0.0 - centralExtentY, minY)) / mid; + if (double.IsNaN(rectRect)) + { + if (GameSettings.FI_LOG_TEMP_ERROR) + Debug.LogError("[FlightIntegrator]: For part " + occlusionData.ptd.part.name + ", rectRect is NaN"); + } + else + { + areaMultiplier -= rectRect; + } + } + + if (areaMultiplier < 0.001) + { + areaMultiplier = 0.0; + break; + } + } + + occlusionData.ptd.bodyAreaMultiplier = areaMultiplier; + if (areaMultiplier > 0) + { + occlusionData.bodyCyl.Setup(occlusionData); + occluders[occluderCount] = occlusionData.bodyCyl; + occluderCount++; + } + } + return false; + } + + // a lot of stuff is actually unused in OcclusionData + // boundsVertices (only used in the scope of OcclusionData.Update, we use local vars and inline stuff instead) + // projectedVertices, projectedDots : part of an alternative thermal thing that isn't activated / never called + // useDragArea is always true, so the involved code paths are never taken + static void UpdateOcclusionData(OcclusionData occlusionData, Vector3d velocity, QuaternionDPointRotation velToUp) + { + Part part = occlusionData.part; + + if (part.IsNullOrDestroyed() || part.partTransform.IsNullOrDestroyed()) + return; + + Vector3 center = occlusionData.part.DragCubes.WeightedCenter; + Vector3 size = occlusionData.part.DragCubes.WeightedSize; + + double cX = center.x; + double cY = center.y; + double cZ = center.z; + double eX = size.x * 0.5; + double eY = size.y * 0.5; + double eZ = size.z * 0.5; + double minX = cX - eX; + double minY = cY - eY; + double minZ = cZ - eZ; + double maxX = cX + eX; + double maxY = cY + eY; + double maxZ = cZ + eZ; + + Matrix4x4D localToWorldMatrix = (Matrix4x4D)part.partTransform.localToWorldMatrix; // 10% of the load. probably can't be avoided, but well... + + // 10% of the load is here, probably worth it to extract the matrix components and to manually inline (the MultiplyPoint3x4 method is **not** inlined) + Vector3d boundVert1 = MultiplyPoint3x4(localToWorldMatrix, minX, minY, minZ); + Vector3d boundVert2 = MultiplyPoint3x4(localToWorldMatrix, maxX, maxY, maxZ); + Vector3d boundVert3 = MultiplyPoint3x4(localToWorldMatrix, minX, minY, maxZ); + Vector3d boundVert4 = MultiplyPoint3x4(localToWorldMatrix, minX, maxY, minZ); + Vector3d boundVert5 = MultiplyPoint3x4(localToWorldMatrix, maxX, minY, minZ); + Vector3d boundVert6 = MultiplyPoint3x4(localToWorldMatrix, minX, maxY, maxZ); + Vector3d boundVert7 = MultiplyPoint3x4(localToWorldMatrix, maxX, minY, maxZ); + Vector3d boundVert8 = MultiplyPoint3x4(localToWorldMatrix, maxX, maxY, minZ); + + double minDot = double.MaxValue; + double maxDot = double.MinValue; + double minExtentX = double.MaxValue; + double minExtentY = double.MaxValue; + double maxExtentX = double.MinValue; + double maxExtentY = double.MinValue; + + FindDotMinMax(Vector3d.Dot(boundVert1, velocity), ref minDot, ref maxDot); + velToUp.RotatePointGetXZ(boundVert1, out double vertX, out double vertZ); + FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); + + FindDotMinMax(Vector3d.Dot(boundVert2, velocity), ref minDot, ref maxDot); + velToUp.RotatePointGetXZ(boundVert2, out vertX, out vertZ); + FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); + + FindDotMinMax(Vector3d.Dot(boundVert3, velocity), ref minDot, ref maxDot); + velToUp.RotatePointGetXZ(boundVert3, out vertX, out vertZ); + FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); + + FindDotMinMax(Vector3d.Dot(boundVert4, velocity), ref minDot, ref maxDot); + velToUp.RotatePointGetXZ(boundVert4, out vertX, out vertZ); + FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); + + FindDotMinMax(Vector3d.Dot(boundVert5, velocity), ref minDot, ref maxDot); + velToUp.RotatePointGetXZ(boundVert5, out vertX, out vertZ); + FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); + + FindDotMinMax(Vector3d.Dot(boundVert6, velocity), ref minDot, ref maxDot); + velToUp.RotatePointGetXZ(boundVert6, out vertX, out vertZ); + FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); + + FindDotMinMax(Vector3d.Dot(boundVert7, velocity), ref minDot, ref maxDot); + velToUp.RotatePointGetXZ(boundVert7, out vertX, out vertZ); + FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); + + FindDotMinMax(Vector3d.Dot(boundVert8, velocity), ref minDot, ref maxDot); + velToUp.RotatePointGetXZ(boundVert8, out vertX, out vertZ); + FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); + + Vector3d worldBoundsCenter = MultiplyPoint3x4(localToWorldMatrix, cX, cY, cZ); + occlusionData.centroidDot = Vector3d.Dot(worldBoundsCenter, velocity); + occlusionData.projectedCenter = worldBoundsCenter - occlusionData.centroidDot * velocity; + occlusionData.boundsCenter = new Vector3((float)cX, (float)cY, (float)cZ); + occlusionData.minimumDot = minDot; + occlusionData.maximumDot = maxDot; + occlusionData.minExtents = new Vector2((float)minExtentX, (float)minExtentY); // minExtents / maxExtents : ideally flatten into double fields + occlusionData.maxExtents = new Vector2((float)maxExtentX, (float)maxExtentY); + + occlusionData.extents = (occlusionData.maxExtents - occlusionData.minExtents) * 0.5f; // extents, center : ideally flatten into double fields + occlusionData.center = occlusionData.minExtents + occlusionData.extents; + + occlusionData.projectedArea = part.DragCubes.CrossSectionalArea; + occlusionData.invFineness = part.DragCubes.TaperDot; + occlusionData.maxWidthDepth = part.DragCubes.Depth; + + occlusionData.projectedRadius = Math.Sqrt(occlusionData.projectedArea / Math.PI); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void FindExtentsMinMax(double x, double z, ref double minExtentX, ref double minExtentY, ref double maxExtentX, ref double maxExtentY) + { + maxExtentX = Math.Max(maxExtentX, x); + maxExtentY = Math.Max(maxExtentY, z); + minExtentX = Math.Min(minExtentX, x); + minExtentY = Math.Min(minExtentY, z); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void FindDotMinMax(double dot, ref double minDot, ref double maxDot) + { + if (dot < minDot) + minDot = dot; + + if (dot > maxDot) + maxDot = dot; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector3d MultiplyPoint3x4(Matrix4x4D m, double x, double y, double z) + { + return new Vector3d( + m.m00 * x + m.m01 * y + m.m02 * z + m.m03, + m.m10 * x + m.m11 * y + m.m12 * z + m.m13, + m.m20 * x + m.m21 * y + m.m22 * z + m.m23); + } + + private static QuaternionD FromToRotation(Vector3d from, Vector3d to) + { + double d = Vector3d.Dot(from, to); + double qw = Math.Sqrt(from.sqrMagnitude * to.sqrMagnitude) + d; + double x, y, z, sqrMag; + if (qw < 1e-12) + { + // vectors are 180 degrees apart + x = from.x; + y = from.y; + z = -from.z; + sqrMag = x * x + y * y + z * z; + if (sqrMag != 1.0) + { + double invNorm = 1.0 / Math.Sqrt(sqrMag); + x *= invNorm; + y *= invNorm; + z *= invNorm; + } + return new QuaternionD(x, y, z, 0.0); + } + + Vector3d axis = Vector3d.Cross(from, to); + x = axis.x; + y = axis.y; + z = axis.z; + sqrMag = x * x + y * y + z * z + qw * qw; + if (sqrMag != 1.0) + { + double invNorm = 1.0 / Math.Sqrt(sqrMag); + x *= invNorm; + y *= invNorm; + z *= invNorm; + qw *= invNorm; + } + + return new QuaternionD(x, y, z, qw); + } + } + + internal struct QuaternionDPointRotation + { + private double qx2; + private double qy2; + private double qz2; + private double qx2x; + private double qy2y; + private double qz2z; + private double qy2x; + private double qz2x; + private double qz2y; + private double qx2w; + private double qy2w; + private double qz2w; + + public QuaternionDPointRotation(QuaternionD rotation) + { + qx2 = rotation.x * 2.0; + qy2 = rotation.y * 2.0; + qz2 = rotation.z * 2.0; + qx2x = rotation.x * qx2; + qy2y = rotation.y * qy2; + qz2z = rotation.z * qz2; + qy2x = rotation.x * qy2; + qz2x = rotation.x * qz2; + qz2y = rotation.y * qz2; + qx2w = rotation.w * qx2; + qy2w = rotation.w * qy2; + qz2w = rotation.w * qz2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RotatePointGetXZ(Vector3d point, out double x, out double z) + { + x = (1.0 - (qy2y + qz2z)) * point.x + (qy2x - qz2w) * point.y + (qz2x + qy2w) * point.z; + z = (qz2x - qy2w) * point.x + (qz2y + qx2w) * point.y + (1.0 - (qx2x + qy2y)) * point.z; + } + } + + public class KSPCFOcclusionData: OcclusionData + { + + // projecte + public KSPCFOcclusionData(PartThermalData data) : base(data) + { + + } + } +} diff --git a/KSPCommunityFixes/Performance/GeneralPerfFixes.cs b/KSPCommunityFixes/Performance/GeneralPerfFixes.cs new file mode 100644 index 00000000..e73608c7 --- /dev/null +++ b/KSPCommunityFixes/Performance/GeneralPerfFixes.cs @@ -0,0 +1,1142 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using HarmonyLib; +using KSP.UI.Screens; +using KSP.UI.Screens.Settings; +using KSPCommunityFixes.Library; +using TMPro.Examples; +using UnityEngine; +using UnityEngine.UIElements; +using VehiclePhysics; +using static System.Number; +using static GameDatabase; +using static Highlighting.Highlighter.RendererCache; +using static iT; +using static ProceduralSpaceObject; +using UObject = UnityEngine.Object; + +namespace KSPCommunityFixes.Performance +{ + internal class GeneralPerfFixes : BasePatch + { + internal static Stopwatch watch = new Stopwatch(); + + protected override void ApplyPatches(List patches) + { + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.PropertyGetter(typeof(FlightGlobals), nameof(FlightGlobals.fetch)), + this, nameof(FlightGlobals_fetch_Prefix))); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(PartLoader), nameof(PartLoader.CreatePartIcon)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(PartLoader), nameof(PartLoader.ApplyPartValue)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(BaseFieldList), nameof(BaseFieldList.CreateList)), + this)); + + //patches.Add(new PatchInfo( + // PatchMethodType.Postfix, + // AccessTools.Method(typeof(PartLoader), nameof(PartLoader.CreatePartIcon)), + // this)); + + //patches.Add(new PatchInfo( + // PatchMethodType.Prefix, + // AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetModelPrefab)), + // this)); + + //// texture getting patches would also benefit to model loading, but we need to run earlier... + //patches.Add(new PatchInfo( + // PatchMethodType.Prefix, + // AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetTextureInfo)), + // this)); + + //patches.Add(new PatchInfo( + // PatchMethodType.Prefix, + // AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetTexture)), + // this)); + + //patches.Add(new PatchInfo( + // PatchMethodType.Prefix, + // AccessTools.Method(typeof(PartModule), nameof(PartModule.ModularSetup)), + // this)); + + //patches.Add(new PatchInfo( + // PatchMethodType.Postfix, + // AccessTools.Method(typeof(PartModule), nameof(PartModule.ModularSetup)), + // this)); + } + + static bool FlightGlobals_fetch_Prefix(out FlightGlobals __result) + { + if (KSPCFFastLoader.PartCompilationInProgress) + { + __result = null; + return false; + } + + if (FlightGlobals._fetch.IsNullOrDestroyed()) + { + FlightGlobals._fetch = UObject.FindObjectOfType(); + } + + __result = FlightGlobals._fetch; + return false; + } + + static List componentBuffer = new List(); + static List iconHiddenComponentBuffer = new List(); + static List colliderObjectsToDestroy = new List(); + static List iconRenderers = new List(); + static List materialBuffer = new List(); + + private static bool shaderRefsAcquired; + private static Shader shader_ScreenSpaceMaskSpecular; + private static Shader shader_ScreenSpaceMaskBumpedSpecularTransparent; + private static Shader shader_ScreenSpaceMaskBumped; + private static Shader shader_ScreenSpaceMaskAlphaCutoffBackground; + private static Shader shader_ScreenSpaceMaskUnlit; + private static Shader shader_ScreenSpaceMask; + + private static Shader GetIconShader(string materialShaderName) + { + if (!shaderRefsAcquired) + { + shader_ScreenSpaceMaskSpecular = Shader.Find("KSP/ScreenSpaceMaskSpecular"); + shader_ScreenSpaceMaskBumpedSpecularTransparent = Shader.Find("KSP/ScreenSpaceMaskBumpedSpecular(Transparent)"); + shader_ScreenSpaceMaskBumped = Shader.Find("KSP/ScreenSpaceMaskBumped"); + shader_ScreenSpaceMaskAlphaCutoffBackground = Shader.Find("KSP/ScreenSpaceMaskAlphaCutoffBackground"); + shader_ScreenSpaceMaskUnlit = Shader.Find("KSP/ScreenSpaceMaskUnlit"); + shader_ScreenSpaceMask = Shader.Find("KSP/ScreenSpaceMask"); + shaderRefsAcquired = true; + } + + if (materialShaderName == "KSP/Bumped Specular (Mapped)") + return shader_ScreenSpaceMaskSpecular; + if (materialShaderName == "KSP/Bumped Specular (Transparent)") + return shader_ScreenSpaceMaskBumpedSpecularTransparent; + if (materialShaderName.Contains("Bumped")) + return shader_ScreenSpaceMaskBumped; + if (materialShaderName.Contains("KSP/Alpha/CutoffBackground")) + return shader_ScreenSpaceMaskAlphaCutoffBackground; + if (materialShaderName == "KSP/Unlit") + return shader_ScreenSpaceMaskUnlit; + + return shader_ScreenSpaceMask; + } + + //static void PartLoader_CreatePartIcon_Prefix() + //{ + // watch.Start(); + //} + + //static void PartLoader_CreatePartIcon_Postfix() + //{ + // watch.Stop(); + //} + + private static HashSet OnIconCreatePartModules; + + private static bool HasOnIconCreateModule(Part part) + { + if (OnIconCreatePartModules == null) + { + OnIconCreatePartModules = new HashSet(); + + foreach (Type type in AccessTools.AllTypes()) + { + if (type.IsSubclassOf(typeof(PartModule))) + { + if (type.GetMethod(nameof(PartModule.OnIconCreate), BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) != null) + { + OnIconCreatePartModules.Add(type); + } + } + } + } + + if (OnIconCreatePartModules.Count == 0) + return false; + + List modules = part.modules.modules; + for (int i = modules.Count; i-- > 0;) + { + if (OnIconCreatePartModules.Contains(modules[i].GetType())) + { + return true; + } + } + + return false; + } + + static bool PartLoader_CreatePartIcon_Prefix(GameObject newPart, out float iconScale, out GameObject __result) + { + watch.Start(); + newPart.SetActive(false); + GameObject partObject = UObject.Instantiate(newPart); + newPart.SetActive(true); + //partObject.SetActive(true); + Part part = partObject.GetComponent(); + + // only activate the part if a module requires it. + // probably not a good idea in the end : + // - some model setup might happen in Awake(), and we would miss those + // - the perf gains are not as good as I hoped. + if (HasOnIconCreateModule(part)) + { + partObject.SetActive(true); + } + + if (part.IsNotNullOrDestroyed()) + { + int i = 0; + for (int count = part.Modules.Count; i < count; i++) + part.Modules[i].OnIconCreate(); + } + + Bounds partBounds = default; + partObject.GetComponentsInChildren(false, componentBuffer); + try + { + for (int i = componentBuffer.Count; i-- > 0;) + { + Component c = componentBuffer[i]; + if (c is Part + || c is PartModule + || c is EffectBehaviour + || c is WheelCollider + || c is SmokeTrailControl + || c is FXPrefab + || c is ParticleSystem + || c is Light + || c is Animation + || c is DAE) + { + UObject.DestroyImmediate(c, false); + } + else if (!c.IsDestroyed() && (c is Renderer || c is MeshFilter)) + { + // we are adding renderers that will potentially be destroyed latter + if (!(c is MeshFilter)) + iconRenderers.Add((Renderer)c); + + if ((c is MeshRenderer || c is SkinnedMeshRenderer || c is MeshFilter) && c.gameObject.CompareTag("Icon_Hidden")) + { + c.gameObject.GetComponentsInChildren(false, iconHiddenComponentBuffer); + + for (int j = iconHiddenComponentBuffer.Count; j-- > 0;) + { + Component child = iconHiddenComponentBuffer[j]; + if (!child.IsDestroyed() && (child is MeshRenderer || child is SkinnedMeshRenderer || child is MeshFilter)) + UObject.DestroyImmediate(child, false); + } + + iconHiddenComponentBuffer.Clear(); + UObject.DestroyImmediate(c, false); + } + } + else if (c is Collider) + { + if (c.gameObject.name == "collider") + colliderObjectsToDestroy.Add(c.gameObject); + else + UObject.DestroyImmediate(c, false); + } + } + + for (int i = colliderObjectsToDestroy.Count; i-- > 0;) + UObject.DestroyImmediate(colliderObjectsToDestroy[i]); + + partObject.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity); + + bool first = true; + for (int i = iconRenderers.Count; i-- > 0;) + { + Renderer renderer = iconRenderers[i]; + if (renderer.IsDestroyed()) + continue; + + if (!(renderer is ParticleSystemRenderer)) + { + if (first) + { + first = false; + partBounds = renderer.bounds; + } + else + { + partBounds.Encapsulate(renderer.bounds); + } + } + + renderer.GetSharedMaterials(materialBuffer); + for (int j = materialBuffer.Count; j-- > 0;) + { + Material partMaterial = materialBuffer[j]; + if (partMaterial.IsNullOrDestroyed()) + continue; + + Material iconMaterial = new Material(GetIconShader(partMaterial.shader.name)); + iconMaterial.name = partMaterial.name; + iconMaterial.CopyPropertiesFromMaterial(partMaterial); + if (iconMaterial.HasProperty(ShaderHelpers.ColorPropId)) + { + Color originalColor = iconMaterial.color; + iconMaterial.SetColor(PropertyIDs._Color, new Color( + Mathf.Clamp(originalColor.r, 0.5f, 1f), + Mathf.Clamp(originalColor.g, 0.5f, 1f), + Mathf.Clamp(originalColor.b, 0.5f, 1f))); + } + else + { + iconMaterial.SetColor(PropertyIDs._Color, Color.white); + } + + iconMaterial.SetFloat(PropertyIDs._Multiplier, PartLoader.Instance.shaderMultiplier); + iconMaterial.SetFloat(PropertyIDs._MinX, 0f); + iconMaterial.SetFloat(PropertyIDs._MaxX, 1f); + iconMaterial.SetFloat(PropertyIDs._MinY, 0f); + iconMaterial.SetFloat(PropertyIDs._MaxY, 1f); + materialBuffer[j] = iconMaterial; + } + + if (materialBuffer.Count == 1) + renderer.sharedMaterial = materialBuffer[0]; + else + renderer.sharedMaterials = materialBuffer.ToArray(); + + materialBuffer.Clear(); + } + + } + finally + { + componentBuffer.Clear(); + iconHiddenComponentBuffer.Clear(); + colliderObjectsToDestroy.Clear(); + iconRenderers.Clear(); + materialBuffer.Clear(); + } + + Vector3 size = partBounds.size; + float x = Math.Abs(size.x); + float y = Math.Abs(size.y); + float z = Math.Abs(size.z); + iconScale = x > y ? x : y; + iconScale = iconScale > z ? iconScale : z; + iconScale = 1f / iconScale; + + GameObject iconObject = new GameObject(); + iconObject.name = partObject.name + " icon"; + partObject.transform.parent = iconObject.transform; + partObject.transform.localScale = Vector3.one * iconScale; + partObject.transform.localPosition = partBounds.center * (0f - iconScale); + iconObject.transform.parent = PartLoader.Instance.transform; + iconObject.SetActive(false); + partObject.SetActive(true); + + __result = iconObject; + watch.Stop(); + return false; + } + + private static Dictionary partFieldsSetters = new Dictionary(); + private static Dictionary compoundPartFieldsSetters = new Dictionary(); + private static Dictionary> derivedPartSetters; + + private abstract class PartFieldSetter + { + public abstract bool SetField(object instance, string value); + } + + private sealed class FloatSetter : PartFieldSetter + { + private Action setter; + + public FloatSetter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + float value = float.Parse(strValue); + setter(instance, value); + return true; + } + } + + private sealed class DoubleSetter : PartFieldSetter + { + private Action setter; + + public DoubleSetter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + double value = double.Parse(strValue); + setter(instance, value); + return true; + } + } + + private sealed class IntSetter : PartFieldSetter + { + private Action setter; + + public IntSetter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + int value = int.Parse(strValue); + setter(instance, value); + return true; + } + } + + private sealed class BoolSetter : PartFieldSetter + { + private Action setter; + + public BoolSetter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + bool value = bool.Parse(strValue); + setter(instance, value); + return true; + } + } + + private sealed class StringSetter : PartFieldSetter + { + private Action setter; + + public StringSetter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + setter(instance, strValue); + return true; + } + } + + private sealed class EnumSetter : PartFieldSetter + { + private Action setter; + private Type enumType; + + public EnumSetter(FieldInfo fieldInfo) + { + setter = CreateInstanceBoxedValueSetter(fieldInfo); + enumType = fieldInfo.FieldType; + } + + public override bool SetField(object instance, string strValue) + { + object value = Enum.Parse(enumType, strValue); + setter(instance, value); + return true; + } + } + + private sealed class Vector2Setter : PartFieldSetter + { + private Action setter; + + public Vector2Setter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + string[] array = strValue.Split(','); + if (array.Length < 2) + { + PDebug.Log($"WARNING: {strValue} is nor formatted properly! proper format for Vector2 is x,y"); + return false; + } + setter(instance, new Vector2(float.Parse(array[0]), float.Parse(array[1]))); + return true; + } + } + + private sealed class Vector3Setter : PartFieldSetter + { + private Action setter; + + public Vector3Setter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + string[] array = strValue.Split(','); + if (array.Length < 3) + { + PDebug.Log($"WARNING: {strValue} is nor formatted properly! proper format for Vector3 is x,y,z"); + return false; + } + setter(instance, new Vector3(float.Parse(array[0]), float.Parse(array[1]), float.Parse(array[2]))); + return true; + } + } + + private sealed class Vector4Setter : PartFieldSetter + { + private Action setter; + + public Vector4Setter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + string[] array = strValue.Split(','); + if (array.Length < 4) + { + PDebug.Log($"WARNING: {strValue} is nor formatted properly! proper format for Vector4 is x,y,z,w"); + return false; + } + setter(instance, new Vector4(float.Parse(array[0]), float.Parse(array[1]), float.Parse(array[2]), float.Parse(array[3]))); + return true; + } + } + + private sealed class QuaternionSetter : PartFieldSetter + { + private Action setter; + + public QuaternionSetter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + string[] array = strValue.Split(','); + if (array.Length < 4) + { + PDebug.Log($"WARNING: {strValue} is nor formatted properly! proper format for Quaternion is angle(deg),x,y,z"); + return false; + } + setter(instance, Quaternion.AngleAxis(float.Parse(array[0]), new Vector3(float.Parse(array[1]), float.Parse(array[2]), float.Parse(array[3])))); + return true; + } + } + + private sealed class DragModelTypeSetter : PartFieldSetter + { + public override bool SetField(object instance, string value) + { + Part part = (Part)instance; + switch (value.ToUpper()) + { + case "SPHERICAL": + part.dragModel = Part.DragModel.SPHERICAL; + break; + case "NONE": + case "OVERRIDE": + part.dragModel = Part.DragModel.NONE; + break; + case "CYLINDRICAL": + part.dragModel = Part.DragModel.CYLINDRICAL; + break; + case "CONIC": + part.dragModel = Part.DragModel.CONIC; + break; + default: + part.dragModel = Part.DragModel.CUBE; + break; + } + return true; + } + } + + private sealed class PartRendererBoundsIgnoreTypeSetter : PartFieldSetter + { + public override bool SetField(object instance, string value) + { + Part part = (Part)instance; + string[] splitted = value.Split(','); + part.partRendererBoundsIgnore = new List(); + for (int i = 0; i < splitted.Length; i++) + { + part.partRendererBoundsIgnore.Add(splitted[i].Trim()); + } + return true; + } + } + + private sealed class AnonymousSetter : PartFieldSetter + { + private Action setter; + //private Func parser; + private MethodInfo parserMethod; + private string[] valueBuffer = new string[1]; + + public AnonymousSetter(FieldInfo fieldInfo, MethodInfo parserMethod) + { + //parser = (Func)Delegate.CreateDelegate(typeof(Func), parserMethod); + this.parserMethod = parserMethod; + if (fieldInfo.FieldType.IsValueType) + setter = CreateInstanceBoxedValueSetter(fieldInfo); + else + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + //object value = parser(strValue); + valueBuffer[0] = strValue; + object value = parserMethod.Invoke(null, valueBuffer); + setter(instance, value); + return true; + } + } + + public static Action CreateInstanceSetter(FieldInfo field) + { + string methodName = field.ReflectedType.FullName + ".set_" + field.Name; + DynamicMethod setterMethod = new DynamicMethod(methodName, null, new Type[2] { typeof(object), typeof(T) }, true); + ILGenerator gen = setterMethod.GetILGenerator(); + gen.Emit(OpCodes.Ldarg_0); + gen.Emit(OpCodes.Ldarg_1); + gen.Emit(OpCodes.Stfld, field); + gen.Emit(OpCodes.Ret); + return (Action)setterMethod.CreateDelegate(typeof(Action)); + } + + public static Action CreateInstanceBoxedValueSetter(FieldInfo field) + { + string methodName = field.ReflectedType.FullName + ".set_" + field.Name; + DynamicMethod setterMethod = new DynamicMethod(methodName, null, new Type[2] { typeof(object), typeof(object) }, true); + ILGenerator gen = setterMethod.GetILGenerator(); + gen.Emit(OpCodes.Ldarg_0); + gen.Emit(OpCodes.Ldarg_1); + gen.Emit(OpCodes.Unbox_Any, field.FieldType); + gen.Emit(OpCodes.Stfld, field); + gen.Emit(OpCodes.Ret); + return (Action)setterMethod.CreateDelegate(typeof(Action)); + } + + private static bool partFieldDictionariesAreBuilt; + + public static void BuildPartFieldDictionaries() + { + Type partType = typeof(Part); + foreach (FieldInfo fieldInfo in partType.GetFields(BindingFlags.Instance | BindingFlags.Public)) + { + PartFieldSetter setter; + + string fieldName = fieldInfo.Name; + + if (fieldName == "dragModelType") + { + setter = new DragModelTypeSetter(); + } + else if (fieldName == "partRendererBoundsIgnore") + { + setter = new PartRendererBoundsIgnoreTypeSetter(); + } + else if (fieldName == "iconCenter" + || fieldName == "alphaCutoff" + || fieldName == "scale" + || fieldName == "texture" + || fieldName == "normalmap" + || fieldName == "name" + || fieldName == "specPower" + || fieldName == "rimFalloff" + || fieldName == "mesh" + || fieldName == "subcategory" + || fieldName == "module" + || fieldName == "exportScale" + || fieldName.StartsWith("node") + || fieldName.StartsWith("fx") + || fieldName.StartsWith("sound")) + { + // ignored field, just have a null entry + setter = null; + } + else + { + Type fieldType = fieldInfo.FieldType; + if (fieldType == typeof(float)) + { + setter = new FloatSetter(fieldInfo); + } + else if (fieldType == typeof(double)) + { + setter = new DoubleSetter(fieldInfo); + } + else if (fieldType == typeof(int)) + { + setter = new IntSetter(fieldInfo); + } + else if (fieldType == typeof(bool)) + { + setter = new BoolSetter(fieldInfo); + } + else if (fieldType == typeof(string)) + { + setter = new StringSetter(fieldInfo); + } + else if (fieldType == typeof(Vector2)) + { + setter = new Vector2Setter(fieldInfo); + } + else if (fieldType == typeof(Vector3)) + { + setter = new Vector3Setter(fieldInfo); + } + else if (fieldType == typeof(Vector4)) + { + setter = new Vector4Setter(fieldInfo); + } + else if (fieldType == typeof(Quaternion)) + { + setter = new QuaternionSetter(fieldInfo); + } + else if (fieldType.IsEnum) + { + setter = new EnumSetter(fieldInfo); + } + else + { + MethodInfo parseMethod = fieldType.GetMethod("Parse", new[] { typeof(string) }); + if (parseMethod == null) + { + setter = null; + } + else + { + setter = new AnonymousSetter(fieldInfo, parseMethod); + } + } + } + + partFieldsSetters[fieldInfo.Name] = setter; + } + + + //foreach (Type type in AccessTools.AllTypes()) + //{ + // if (type.IsSubclassOf(Part)) + // { + + // } + //} + } + + static bool PartLoader_ApplyPartValue_Prefix(Part part, ConfigNode.Value nodeValue, out bool __result) + { + if (!partFieldDictionariesAreBuilt) + { + partFieldDictionariesAreBuilt = true; + BuildPartFieldDictionaries(); + } + + string valueName = nodeValue.name; + if (partFieldsSetters.TryGetValue(valueName, out PartFieldSetter setter)) + { + if (setter == null) + { + __result = false; + return false; + } + + __result = setter.SetField(part, nodeValue.value); + return false; + } + + __result = false; + return false; + } + + static unsafe Vector2 ParseVector2(string value) + { + UnsafeString str = new UnsafeString(value); + for (int i = str.length; i-- > 0;) + { + if (str[i] == ',') + { + UnsafeString x = str.Substring(0, i); + UnsafeString y = str.Substring(i + 1); + Vector2 result = new Vector2(ParseSingle(x), ParseSingle(y)); + str.UnPin(); + return result; + } + } + + InvalidFormatException(); + return default; + } + + private static void InvalidFormatException() + { + throw new FormatException(); + } + + internal static NumberFormatInfo numberFormatInfo = NumberFormatInfo.CurrentInfo; + internal static NumberStyles numberStyleFloat = NumberStyles.Float | NumberStyles.AllowThousands; + + internal static unsafe float ParseSingle(UnsafeString str) + { + byte* stackBuffer = stackalloc byte[(int)(uint)NumberBuffer.NumberBufferBytes]; + NumberBuffer number = new NumberBuffer(stackBuffer); + double value2 = 0.0; + + if (!TryStringToNumber(str, numberStyleFloat, ref number, numberFormatInfo, parseDecimal: false)) + { + UnsafeString trimmedStr = str.Trim(); + if (trimmedStr.Equals(numberFormatInfo.PositiveInfinitySymbol)) + { + return float.PositiveInfinity; + } + if (trimmedStr.Equals(numberFormatInfo.NegativeInfinitySymbol)) + { + return float.NegativeInfinity; + } + if (trimmedStr.Equals(numberFormatInfo.NaNSymbol)) + { + return float.NaN; + } + throw new FormatException("Input string was not in a correct format."); + } + if (!NumberBufferToDouble(number.PackForNative(), ref value2)) + { + throw new OverflowException("Value was either too large or too small for a Single."); + } + float num = (float)value2; + if (float.IsInfinity(num)) + { + throw new OverflowException("Value was either too large or too small for a Single."); + } + return num; + } + + internal static unsafe bool TryStringToNumber(UnsafeString str, NumberStyles options, ref NumberBuffer number, NumberFormatInfo numfmt, bool parseDecimal) + { + char* ptr = str.ptr; + if (!ParseNumber(ref ptr, options, ref number, null, numfmt, parseDecimal) || (ptr - str.ptr < str.length && !TrailingZeros(str, (int)(ptr - str.ptr)))) + return false; + + return true; + } + + private static bool TrailingZeros(UnsafeString str, int index) + { + for (int i = index; i < str.length; i++) + if (str[i] != 0) + return false; + + return true; + } + + static bool BaseFieldList_CreateList_Prefix(BaseFieldList __instance, object instance) + { + // base code from BaseFieldList + BaseFieldList.ReflectedData reflectedAttributes = BaseFieldList.GetReflectedAttributes(instance.GetType(), __instance.ignoreUIControlWhenCreatingReflectedData); + + __instance._fields.Capacity = reflectedAttributes.fields.Count; + int i = 0; + for (int count = reflectedAttributes.fields.Count; i < count; i++) + { + BaseField val = new BaseField(reflectedAttributes.fieldAttributes[i], (FieldInfo)reflectedAttributes.fields[i], instance); + val.SetOriginalValue(); + __instance._fields.Add(val); + } + + // override from BaseFieldList : BaseFieldList + // ReflectedData reflectedAttributes = BaseFieldList.GetReflectedAttributes(instance.GetType(), __instance.ignoreUIControlWhenCreatingReflectedData); + int j = 0; + for (int count = reflectedAttributes.controls.Count; j < count; j++) + { + BaseField item = new BaseField(reflectedAttributes.controlAttributes[j], (FieldInfo)reflectedAttributes.fields[j], instance); + __instance._fields.Add(item); + } + + return false; + } + } + + public struct UnsafeString + { + public unsafe char* ptr; + public int length; + private GCHandle handle; + + private unsafe UnsafeString(char* ptr, int length, GCHandle handle) + { + this.ptr = ptr; + this.length = length; + this.handle = handle; + } + + public unsafe UnsafeString(string str) + { + handle = GCHandle.Alloc(str, GCHandleType.Pinned); + ptr = (char*)handle.AddrOfPinnedObject(); + length = str.Length; + } + + public unsafe UnsafeString(string str, int start, int length = 0) + { + handle = GCHandle.Alloc(str, GCHandleType.Pinned); + ptr = (char*)handle.AddrOfPinnedObject() + start; + this.length = length == 0 ? str.Length - start : length; + } + + public static unsafe UnsafeString Empty => new UnsafeString((char*)0, 0, new GCHandle()); + + public unsafe char this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if DEBUG + if (index < 0 || index >= length) + throw new IndexOutOfRangeException(); +#endif + return ptr[index]; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe UnsafeString Substring(int start, int length = 0) + { + if (start + length > length) + ArgumentOutOfRange(); + + return new UnsafeString( + ptr + start, + length == 0 ? this.length - start : length, + handle); + } + + private static void ArgumentOutOfRange() + { + throw new ArgumentOutOfRangeException(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe UnsafeString Trim() + { + int firstNonEmptyChar = 0; + while (firstNonEmptyChar < length && char.IsWhiteSpace(ptr[firstNonEmptyChar])) + firstNonEmptyChar++; + + if (firstNonEmptyChar == length) + return Empty; + + int lastNonEmptyChar = length - 1; + while (char.IsWhiteSpace(ptr[lastNonEmptyChar])) + lastNonEmptyChar--; + + return Substring(firstNonEmptyChar, lastNonEmptyChar - firstNonEmptyChar + 1); + } + + public void UnPin() + { + if (handle.IsAllocated) + handle.Free(); + } + + public override unsafe string ToString() + { + return new string(ptr, 0, length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe bool Equals(UnsafeString value) + { + if (ptr == value.ptr && length == value.length) + return true; + + if (length != value.length) + return false; + + return Equals(this, value); + } + + public static unsafe bool Equals(UnsafeString value1, UnsafeString value2) + { + int num = value1.length; + char* ptr1 = value1.ptr; + char* ptr2 = value2.ptr; + + if (Environment.Is64BitProcess) + { + while (num >= 12) + { + if (*(long*)ptr1 != *(long*)ptr2) + { + return false; + } + if (*(long*)(ptr1 + 4) != *(long*)(ptr2 + 4)) + { + return false; + } + if (*(long*)(ptr1 + 8) != *(long*)(ptr2 + 8)) + { + return false; + } + ptr1 += 12; + ptr2 += 12; + num -= 12; + } + } + else + { + while (num >= 10) + { + if (*(int*)ptr1 != *(int*)ptr2) + { + return false; + } + if (*(int*)(ptr1 + 2) != *(int*)(ptr2 + 2)) + { + return false; + } + if (*(int*)(ptr1 + 4) != *(int*)(ptr2 + 4)) + { + return false; + } + if (*(int*)(ptr1 + 6) != *(int*)(ptr2 + 6)) + { + return false; + } + if (*(int*)(ptr1 + 8) != *(int*)(ptr2 + 8)) + { + return false; + } + ptr1 += 10; + ptr2 += 10; + num -= 10; + } + } + while (num > 0 && *(int*)ptr1 == *(int*)ptr2) + { + ptr1 += 2; + ptr2 += 2; + num -= 2; + } + return num <= 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe bool Equals(string value) + { + fixed (char* strPtr = &value.m_firstChar) + if (ptr == strPtr && length == value.Length) + return true; + + if (length != value.Length) + return false; + + return Equals(this, value); + } + + public static unsafe bool Equals(UnsafeString value1, string value2) + { + int num = value1.length; + char* ptr1 = value1.ptr; + fixed (char* strPtr = &value2.m_firstChar) + { + char* ptr2 = strPtr; + if (Environment.Is64BitProcess) + { + while (num >= 12) + { + if (*(long*)ptr1 != *(long*)ptr2) + { + return false; + } + if (*(long*)(ptr1 + 4) != *(long*)(ptr2 + 4)) + { + return false; + } + if (*(long*)(ptr1 + 8) != *(long*)(ptr2 + 8)) + { + return false; + } + ptr1 += 12; + ptr2 += 12; + num -= 12; + } + } + else + { + while (num >= 10) + { + if (*(int*)ptr1 != *(int*)ptr2) + { + return false; + } + if (*(int*)(ptr1 + 2) != *(int*)(ptr2 + 2)) + { + return false; + } + if (*(int*)(ptr1 + 4) != *(int*)(ptr2 + 4)) + { + return false; + } + if (*(int*)(ptr1 + 6) != *(int*)(ptr2 + 6)) + { + return false; + } + if (*(int*)(ptr1 + 8) != *(int*)(ptr2 + 8)) + { + return false; + } + ptr1 += 10; + ptr2 += 10; + num -= 10; + } + } + while (num > 0 && *(int*)ptr1 == *(int*)ptr2) + { + ptr1 += 2; + ptr2 += 2; + num -= 2; + } + return num <= 0; + } + + + } + } +} From 9c580da6fcd6527e945ee4ef9b656b8234067d59 Mon Sep 17 00:00:00 2001 From: gotmachine <24925209+gotmachine@users.noreply.github.com> Date: Mon, 3 Jun 2024 13:16:07 +0200 Subject: [PATCH 2/8] Fixes and cleanup of FlightIntegratorPerf --- KSPCommunityFixes/KSPCommunityFixes.csproj | 1 + KSPCommunityFixes/Library/StaticHelpers.cs | 28 + .../Performance/FlightIntegratorPerf.cs | 554 ++++++++++++++---- 3 files changed, 472 insertions(+), 111 deletions(-) diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index 0a604987..86f3b1e0 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -225,6 +225,7 @@ + $(SolutionDir) diff --git a/KSPCommunityFixes/Library/StaticHelpers.cs b/KSPCommunityFixes/Library/StaticHelpers.cs index 591b26da..1254ac16 100644 --- a/KSPCommunityFixes/Library/StaticHelpers.cs +++ b/KSPCommunityFixes/Library/StaticHelpers.cs @@ -94,7 +94,35 @@ public static bool EditPartModuleKSPFieldAttributes(Type partModuleType, string /// The value. public static bool IsPowerOfTwo(uint value) => (value & (value - 1)) == 0 && value != 0; + public static Matrix4x4D ToMatrix4x4D(ref this Matrix4x4 m) + { + return new Matrix4x4D( + m.m00, m.m01, m.m02, m.m03, + m.m10, m.m11, m.m12, m.m13, + m.m20, m.m21, m.m22, m.m23, + m.m30, m.m31, m.m32, m.m33); + } + + public static Vector3d MultiplyVector(ref this Matrix4x4D m, Vector3d vector) + { + return new Vector3d( + m.m00 * vector.x + m.m01 * vector.y + m.m02 * vector.z, + m.m10 * vector.x + m.m11 * vector.y + m.m12 * vector.z, + m.m20 * vector.x + m.m21 * vector.y + m.m22 * vector.z); + } + /// + /// 3 times faster than using the + operator + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Add(ref this Vector3d v, Vector3d other) + { + v.x += other.x; + v.y += other.y; + v.z += other.z; + } } } diff --git a/KSPCommunityFixes/Performance/FlightIntegratorPerf.cs b/KSPCommunityFixes/Performance/FlightIntegratorPerf.cs index e16865e1..defa8acb 100644 --- a/KSPCommunityFixes/Performance/FlightIntegratorPerf.cs +++ b/KSPCommunityFixes/Performance/FlightIntegratorPerf.cs @@ -1,45 +1,38 @@ -using HarmonyLib; +// Enable cross checking our implementations results with the stock implementations results +// Warning : very log-spammy and performance destroying, don't leave this enabled if you don't need to. +// #define DEBUG_FLIGHTINTEGRATOR + +using HarmonyLib; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Runtime.CompilerServices; using UnityEngine; -using static EdyCommonTools.RotationController; -using static EdyCommonTools.Spline; using Debug = UnityEngine.Debug; namespace KSPCommunityFixes.Performance { internal class FlightIntegratorPerf : BasePatch { - private static Stopwatch updateOcclusionWatch = new Stopwatch(); - private static int callCount; - protected override void ApplyPatches(List patches) { patches.Add(new PatchInfo( PatchMethodType.Prefix, - AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateOcclusion)), - this)); - - patches.Add(new PatchInfo( - PatchMethodType.Postfix, - AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateOcclusion)), + AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateOcclusionSolar)), this)); patches.Add(new PatchInfo( PatchMethodType.Prefix, - AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateOcclusionSolar)), + AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateOcclusionBody)), this)); patches.Add(new PatchInfo( PatchMethodType.Prefix, - AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateOcclusionBody)), + AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateMassStats)), this)); patches.Add(new PatchInfo( PatchMethodType.Prefix, - AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateMassStats)), + AccessTools.Method(typeof(VesselPrecalculate), nameof(VesselPrecalculate.CalculatePhysicsStats)), this)); // other offenders, in aero situations : @@ -53,9 +46,373 @@ protected override void ApplyPatches(List patches) } + #region VesselPrecalculate.CalculatePhysicsStats optimizations + + /// + /// 40-60% faster than the stock method depending on the situation. + /// Hard to optimize further, a large chunk of the time is spent getting transform / rb properties (~40%) + /// and performing unavoidable double/float conversions (~10%). + /// + private static bool VesselPrecalculate_CalculatePhysicsStats_Prefix(VesselPrecalculate __instance) + { + Vessel vessel = __instance.vessel; + bool isMasslessOrNotLoaded = true; + + if (vessel.loaded) + { + int partCount = vessel.Parts.Count; + Transform vesselTransform = vessel.ReferenceTransform; + Matrix4x4 vesselInverseMatrixF = vesselTransform.worldToLocalMatrix; + Matrix4x4D vesselInverseMatrix = vesselInverseMatrixF.ToMatrix4x4D(); + QuaternionD vesselInverseRotation = QuaternionD.Inverse(vesselTransform.rotation); + Vector3d com = Vector3d.zero; + Vector3d velocity = Vector3d.zero; + Vector3d angularVelocity = Vector3d.zero; + double vesselMass = 0.0; + + if (vessel.packed && partCount > 0) + Physics.SyncTransforms(); + + VesselPrePartBufferEnsureCapacity(partCount); + int index = partCount; + int rbPartCount = 0; + while (index-- > 0) + { + Part part = vessel.parts[index]; + if (part.rb.IsNotNullOrDestroyed()) + { + Vector3d partPosition = part.partTransform.position; + QuaternionD partRotation = part.partTransform.rotation; + vesselPrePartBuffer[rbPartCount] = new PartVesselPreData(partPosition, partRotation, index); + rbPartCount++; +#if DEBUG_FLIGHTINTEGRATOR + double deviation = ((partPosition + partRotation * part.CoMOffset) - part.rb.worldCenterOfMass).magnitude; + if (deviation > 0.001) + Debug.LogWarning($"[KSPCF:FIPerf] KSPCF calculated WorldCenterOfMass is deviating from stock by {deviation:F3}m for part {part.partInfo.title} on vessel {vessel.GetDisplayName()}"); +#endif + double physicsMass = part.physicsMass; + // note : on flight scene load, the parts RBs center of mass won't be set until the vessel gets out of the packed + // state (see FI.UpdateMassStats()), it will initially be set to whatever PhysX has computed from the RB colliders. + // This result in an inconsistent vessel CoM (and all derived stats) being computed for several frames. + // For performance reasons we don't use rb.worldCenterOfMass, but instead re-compute it from Part.CoMOffset, but + // this also has the side effect of fixing those inconsistencies. + com.Add((partPosition + partRotation * part.CoMOffset) * physicsMass); + velocity.Add((Vector3d)part.rb.velocity * physicsMass); + angularVelocity.Add(vesselInverseRotation* part.rb.angularVelocity * physicsMass); + vesselMass += physicsMass; + } + } + + if (vesselMass > 0.0) + { + isMasslessOrNotLoaded = false; + vessel.totalMass = vesselMass; + double vesselMassRecip = 1.0 / vesselMass; + vessel.CoMD = com * vesselMassRecip; + vessel.rb_velocityD = velocity * vesselMassRecip; + vessel.velocityD = vessel.rb_velocityD + Krakensbane.GetFrameVelocity(); + vessel.CoM = vessel.CoMD; + vessel.localCoM = vesselInverseMatrix.MultiplyPoint3x4(vessel.CoMD); + vessel.rb_velocity = vessel.rb_velocityD; + vessel.angularVelocityD = angularVelocity * vesselMassRecip; + vessel.angularVelocity = vessel.angularVelocityD; + + if (vessel.angularVelocityD == Vector3d.zero && vessel.packed) + { + vessel.MOI.Zero(); + vessel.angularMomentum.Zero(); + } + else + { + InertiaTensor inertiaTensor = new InertiaTensor(); + for (int i = 0; i < rbPartCount; i++) + { + PartVesselPreData partPreData = vesselPrePartBuffer[i]; + Part part = vessel.parts[partPreData.partIndex]; + + // add part inertia tensor to vessel inertia tensor + Vector3d principalMoments = part.rb.inertiaTensor; + QuaternionD princAxesRot = vesselInverseRotation * partPreData.rotation * (QuaternionD)part.rb.inertiaTensorRotation; + inertiaTensor.AddPartInertiaTensor(principalMoments, princAxesRot); + + // add part mass and position contribution to vessel inertia tensor + double rbMass = Math.Max(part.partInfo.minimumRBMass, part.physicsMass); + // Note : the stock MoI code fails to account for the additional RB of servo parts. + // On servo parts, the part physicsMass is redistributed equally between the part RB and the servo RB, and since when + // computing the MoI, the stock code uses only the rb.mass, some mass will be unacounted for. Ideally we should do + // the full additional MoI calcs with the servo RB, but as a shorthand fix we just include the whole physicsMass instead + // of half of it like what stock would do. If we want to replicate exactely stock, uncomment those : + // if (part.servoRb.IsNotNullRef()) + // rbMass *= 0.5; + // Note 2 : another side effect of using Part.physicsMass instead of rb.mass is that mass will be correct on scene + // loads, before FI.UpdateMassStats() has run (when it hasn't run yet, rb.mass is set to 1 for all parts) + Vector3d partPosition = vesselInverseMatrix.MultiplyVector(partPreData.position - vessel.CoMD); + inertiaTensor.AddPartMass(rbMass, partPosition); + } + + vessel.MOI = inertiaTensor.MoI; + vessel.angularMomentum.x = (float)(inertiaTensor.m00 * vessel.angularVelocityD.x); + vessel.angularMomentum.y = (float)(inertiaTensor.m11 * vessel.angularVelocityD.y); + vessel.angularMomentum.z = (float)(inertiaTensor.m22 * vessel.angularVelocityD.z); + } + } + +#if DEBUG_FLIGHTINTEGRATOR + VerifyPhysicsStats(__instance); +#endif + } + + if (isMasslessOrNotLoaded) + { + if (vessel.packed) + { + if (vessel.LandedOrSplashed) + { + vessel.CoMD = __instance.worldSurfacePos + __instance.worldSurfaceRot * vessel.localCoM; + } + else + { + if (!vessel.orbitDriver.Ready) + { + vessel.orbitDriver.orbit.Init(); + vessel.orbitDriver.updateFromParameters(setPosition: false); + } + vessel.CoMD = vessel.mainBody.position + vessel.orbitDriver.pos; + } + } + else + { + vessel.CoMD = vessel.vesselTransform.TransformPoint(vessel.localCoM); + } + + vessel.CoM = vessel.CoMD; + + if (vessel.rootPart.IsNotNullOrDestroyed() && vessel.rootPart.rb.IsNotNullOrDestroyed()) + { + vessel.rb_velocity = vessel.rootPart.rb.GetPointVelocity(vessel.CoM); + vessel.rb_velocityD = vessel.rb_velocity; + vessel.velocityD = (Vector3d)vessel.rb_velocity + Krakensbane.GetFrameVelocity(); + vessel.angularVelocityD = (vessel.angularVelocity = Quaternion.Inverse(vessel.ReferenceTransform.rotation) * vessel.rootPart.rb.angularVelocity); + } + else + { + vessel.rb_velocity.Zero(); + vessel.rb_velocityD.Zero(); + vessel.velocityD.Zero(); + vessel.angularVelocity.Zero(); + vessel.angularVelocityD.Zero(); + } + vessel.MOI.Zero(); + vessel.angularMomentum.Zero(); + } + __instance.firstStatsRunComplete = true; + return false; + } + + private readonly struct PartVesselPreData + { + public readonly int partIndex; + public readonly Vector3d position; + public readonly QuaternionD rotation; + + public PartVesselPreData(Vector3d position, QuaternionD rotation, int partIndex) + { + this.position = position; + this.rotation = rotation; + this.partIndex = partIndex; + } + } + + private static void VesselPrePartBufferEnsureCapacity(int partCount) + { + if (vesselPrePartBuffer.Length < partCount) + vesselPrePartBuffer = new PartVesselPreData[(int)(partCount * 1.25)]; + } + + private static PartVesselPreData[] vesselPrePartBuffer = new PartVesselPreData[300]; + + private struct InertiaTensor + { + public double m00; + public double m11; + public double m22; + + public void AddPartInertiaTensor(Vector3d principalMoments, QuaternionD princAxesRot) + { + // inverse the princAxesRot quaternion + double invpx = -princAxesRot.x; + double invpy = -princAxesRot.y; + double invpz = -princAxesRot.z; + + // prepare inverse rotation + double ipx2 = invpx * 2.0; + double ipy2 = invpy * 2.0; + double ipz2 = invpz * 2.0; + double ipx2x = invpx * ipx2; + double ipy2y = invpy * ipy2; + double ipz2z = invpz * ipz2; + double ipy2x = invpx * ipy2; + double ipz2x = invpx * ipz2; + double ipz2y = invpy * ipz2; + double ipx2w = princAxesRot.w * ipx2; + double ipy2w = princAxesRot.w * ipy2; + double ipz2w = princAxesRot.w * ipz2; + + // inverse rotate column 0 + double ir0x = principalMoments.x * (1.0 - (ipy2y + ipz2z)); + double ir0y = principalMoments.y * (ipy2x + ipz2w); + double ir0z = principalMoments.z * (ipz2x - ipy2w); + + // inverse rotate column 1 + double ir1x = principalMoments.x * (ipy2x - ipz2w); + double ir1y = principalMoments.y * (1.0 - (ipx2x + ipz2z)); + double ir1z = principalMoments.z * (ipz2y + ipx2w); + + // inverse rotate column 2 + double ir2x = principalMoments.x * (ipz2x + ipy2w); + double ir2y = principalMoments.y * (ipz2y - ipx2w); + double ir2z = principalMoments.z * (1.0 - (ipx2x + ipy2y)); + + // prepare rotation + double qx2 = princAxesRot.x * 2.0; + double qy2 = princAxesRot.y * 2.0; + double qz2 = princAxesRot.z * 2.0; + double qx2x = princAxesRot.x * qx2; + double qy2y = princAxesRot.y * qy2; + double qz2z = princAxesRot.z * qz2; + double qy2x = princAxesRot.x * qy2; + double qz2x = princAxesRot.x * qz2; + double qz2y = princAxesRot.y * qz2; + double qx2w = princAxesRot.w * qx2; + double qy2w = princAxesRot.w * qy2; + double qz2w = princAxesRot.w * qz2; + + // rotate column 0 + m00 += (1.0 - (qy2y + qz2z)) * ir0x + (qy2x - qz2w) * ir0y + (qz2x + qy2w) * ir0z; + + // rotate column 1 + m11 += (qy2x + qz2w) * ir1x + (1.0 - (qx2x + qz2z)) * ir1y + (qz2y - qx2w) * ir1z; + + // rotate column 2 + m22 += (qz2x - qy2w) * ir2x + (qz2y + qx2w) * ir2y + (1.0 - (qx2x + qy2y)) * ir2z; + } + + public void AddPartMass(double partMass, Vector3d partPosition) + { + double massLever = partMass * partPosition.sqrMagnitude; + double invMass = -partMass; + + m00 += invMass * partPosition.x * partPosition.x + massLever; + m11 += invMass * partPosition.y * partPosition.y + massLever; + m22 += invMass * partPosition.z * partPosition.z + massLever; + } + + public Vector3 MoI => new Vector3((float)m00, (float)m11, (float)m22); + } + + private static void VerifyPhysicsStats(VesselPrecalculate vesselPre) + { + Vessel vessel = vesselPre.Vessel; + if (!vessel.loaded) + return; + + Transform referenceTransform = vessel.ReferenceTransform; + int partCount = vessel.Parts.Count; + QuaternionD vesselInverseRotation = QuaternionD.Inverse(referenceTransform.rotation); + Vector3d pCoM = Vector3d.zero; + Vector3d pVel = Vector3d.zero; + Vector3d pAngularVel = Vector3d.zero; + double vesselMass = 0.0; // vessel.totalMass + int index = partCount; + while (index-- > 0) + { + Part part = vessel.parts[index]; + if (part.rb != null) + { + double physicsMass = part.physicsMass; + pCoM += (Vector3d)part.rb.worldCenterOfMass * physicsMass; + Vector3d vector3d = (Vector3d)part.rb.velocity * physicsMass; + pVel += vector3d; + pAngularVel += vesselInverseRotation * part.rb.angularVelocity * physicsMass; + vesselMass += physicsMass; + } + } + if (vesselMass > 0.0) + { + double vesselMassRecip = 1.0 / vesselMass; + Vector3d vCoMD = pCoM * vesselMassRecip; // vessel.CoMD + Vector3d vRbVelD = pVel * vesselMassRecip; // vessel.rb_velocityD + Vector3d vAngVelD = pAngularVel * vesselMassRecip; // vessel.angularVelocityD + Vector3 vMoI = vessel.MOI; + if (vAngVelD == Vector3d.zero && vessel.packed) + { + vMoI.Zero(); + } + else + { + Matrix4x4 inertiaTensor = Matrix4x4.zero; + Matrix4x4 mIdentity = Matrix4x4.identity; + Matrix4x4 m2 = Matrix4x4.identity; + Matrix4x4 m3 = Matrix4x4.identity; + Quaternion vesselInverseRotationF = vesselInverseRotation; + for (int i = 0; i < partCount; i++) + { + Part part2 = vessel.parts[i]; + if (part2.rb != null) + { + KSPUtil.ToDiagonalMatrix2(part2.rb.inertiaTensor, ref mIdentity); + Quaternion partRot = vesselInverseRotationF * part2.transform.rotation * part2.rb.inertiaTensorRotation; + Quaternion invPartRot = Quaternion.Inverse(partRot); + Matrix4x4 mPart = Matrix4x4.TRS(Vector3.zero, partRot, Vector3.one); + Matrix4x4 invMPart = Matrix4x4.TRS(Vector3.zero, invPartRot, Vector3.one); + Matrix4x4 right = mPart * mIdentity * invMPart; + KSPUtil.Add(ref inertiaTensor, ref right); + Vector3 lever = referenceTransform.InverseTransformDirection(part2.rb.position - vCoMD); + KSPUtil.ToDiagonalMatrix2(part2.rb.mass * lever.sqrMagnitude, ref m2); + KSPUtil.Add(ref inertiaTensor, ref m2); + KSPUtil.OuterProduct2(lever, (0f - part2.rb.mass) * lever, ref m3); + KSPUtil.Add(ref inertiaTensor, ref m3); + } + } + vMoI = KSPUtil.Diag(inertiaTensor); + } + + string warnings = string.Empty; + + double vMassDiff = Math.Abs(vesselMass - vessel.totalMass); + if (vMassDiff > vesselMass / 1e6) + warnings += $"Mass diverging by {vMassDiff:F6} ({vMassDiff / (vesselMass > 0.0 ? vesselMass : 1.0):P5}) "; + + double vCoMDDiff = (vessel.CoMD - vCoMD).magnitude; + if (vCoMDDiff > vCoMD.magnitude / 1e6) + warnings += $"CoM diverging by {vCoMDDiff:F6} ({vCoMDDiff / (vCoMD.magnitude > 0.0 ? vCoMD.magnitude : 1.0):P5}) "; + + double vVelDiff = (vessel.rb_velocityD - vRbVelD).magnitude; + if (vVelDiff > vRbVelD.magnitude / 1e6) + warnings += $"Velocity diverging by {vVelDiff:F6} ({vVelDiff / (vRbVelD.magnitude > 0.0 ? vRbVelD.magnitude : 1.0):P5}) "; + + double vAngVelDDiff = (vessel.angularVelocityD - vAngVelD).magnitude; + if (vAngVelDDiff > vAngVelD.magnitude / 1e6) + warnings += $"Angular velocity diverging by {vAngVelDDiff:F6} ({vAngVelDDiff / (vAngVelD.magnitude > 0.0 ? vAngVelD.magnitude : 1.0):P5}) "; + + double vMoIDiff = (vessel.MOI - vMoI).magnitude; + if (vMoIDiff > vMoI.magnitude / 1e5) + warnings += $"MoI diverging by {vMoIDiff:F6} ({vMoIDiff / (vMoI.magnitude > 0.0 ? vMoI.magnitude : 1.0):P5}) "; + + if (warnings.Length > 0) + { + Debug.LogWarning($"[KSPCF:FIPerf] CalculatePhysicsStats : diverging stats for vessel {vessel.GetDisplayName()}\n{warnings}"); + } + } + } + + #endregion + + #region FlightIntegrator.UpdateMassStats optimizations + // Avoid setting RigidBody.mass and RigidBody.centerOfMass for all parts on every update if they didn't change // Setting these properties is quite costly on the PhysX side, especially centerOfMass (1% to 2% of the frame time - // depending on the situation), and centerOfMass should almost never change unless somthing is changing CoMOffset. + // depending on the situation), and centerOfMass should almost never change unless something is changing CoMOffset. // Setting mass is less costly and will change relatively often but avoiding setting when unecessary is still a decent improvement. // We also take the opportunity to make a few optimizations (faster null checks, inlined inner loop, using the PartResourceList // backing list instead of going through the custom indexer...) @@ -90,30 +447,32 @@ static bool FlightIntegrator_UpdateMassStats_Prefix(FlightIntegrator __instance) Part part = parts[i]; if (part.rb.IsNotNullOrDestroyed()) { - bool shouldSetRBProperties = !part.packed; - float physicsMass = part.mass + part.resourceMass + __instance.GetPhysicslessChildMass(part); // don't use recursion, avoid the null check + float physicsMass = part.mass + part.resourceMass + __instance.GetPhysicslessChildMass(part); // further optimization : don't use recursion, avoid the null check physicsMass = Mathf.Clamp(physicsMass, part.partInfo.MinimumMass, Mathf.Abs(physicsMass)); - if (part.physicsMass != physicsMass) + part.physicsMass = physicsMass; + + if (!part.packed) { - part.physicsMass = physicsMass; - if (shouldSetRBProperties) + float rbMass = Mathf.Max(part.partInfo.MinimumRBMass, physicsMass); + bool hasServoRB = part.servoRb.IsNotNullOrDestroyed(); + + if (hasServoRB) + rbMass *= 0.5f; + + // unfortunately, there is some internal fp manipulation when setting rb.mass + // resulting in tiny deltas between what we set and the value we read back. + if (Math.Abs(part.rb.mass - rbMass) > rbMass / 1e6f) { - float rbMass = Mathf.Max(part.partInfo.MinimumRBMass, physicsMass); - if (part.servoRb.IsNotNullOrDestroyed()) - { - float halfRBMass = rbMass * 0.5f; - part.rb.mass = halfRBMass; - part.servoRb.mass = halfRBMass; - } - else - { - part.rb.mass = rbMass; - } + part.rb.mass = rbMass; + if (hasServoRB) + part.servoRb.mass = rbMass; } - } - if (shouldSetRBProperties && part.rb.centerOfMass != part.CoMOffset) - part.rb.centerOfMass = part.CoMOffset; + // Either this doesn't happen with rb.centerOfMass, or the built-in Vector3 equality + // epsilon takes cares of it. I guess this might happen still if a large offset is defined... + if (part.rb.centerOfMass != part.CoMOffset) + part.rb.centerOfMass = part.CoMOffset; + } } else { @@ -124,23 +483,9 @@ static bool FlightIntegrator_UpdateMassStats_Prefix(FlightIntegrator __instance) return false; } - static void FlightIntegrator_UpdateOcclusion_Prefix(bool all) - { - if (!all) - { - callCount++; - updateOcclusionWatch.Start(); - } - } + #endregion - static void FlightIntegrator_UpdateOcclusion_Postfix() - { - updateOcclusionWatch.Stop(); - if (callCount % 100 == 0) - { - Debug.Log($"[UpdateOcclusion] {updateOcclusionWatch.Elapsed.TotalMilliseconds / callCount:F3}ms/call"); - } - } + #region FlightIntegrator.UpdateOcclusion optimizations static bool FlightIntegrator_UpdateOcclusionSolar_Prefix(FlightIntegrator __instance) { @@ -208,12 +553,10 @@ static bool FlightIntegrator_UpdateOcclusionSolar_Prefix(FlightIntegrator __inst if (maxX >= centralExtentXInv && minX <= centralExtentX && maxY >= centralExtentYInv && minY <= centralExtentY && mid != 0.0) { double midX = Math.Min(centralExtentX, maxX) - Math.Max(centralExtentXInv, minX); - if (midX < 0.0) - midX = 0.0; + if (midX < 0.0) midX = 0.0; double midY = Math.Min(centralExtentY, maxY) - Math.Max(centralExtentYInv, minY); - if (midY < 0.0) - midY = 0.0; + if (midY < 0.0) midY = 0.0; double rectRect = midX * midY / mid; if (double.IsNaN(rectRect)) // it could be nice to put that outside the inner loop @@ -365,17 +708,17 @@ static void UpdateOcclusionData(OcclusionData occlusionData, Vector3d velocity, double maxY = cY + eY; double maxZ = cZ + eZ; - Matrix4x4D localToWorldMatrix = (Matrix4x4D)part.partTransform.localToWorldMatrix; // 10% of the load. probably can't be avoided, but well... + Matrix4x4D localToWorldMatrix = (Matrix4x4D)part.partTransform.localToWorldMatrix; // 10% of the load. // 10% of the load is here, probably worth it to extract the matrix components and to manually inline (the MultiplyPoint3x4 method is **not** inlined) - Vector3d boundVert1 = MultiplyPoint3x4(localToWorldMatrix, minX, minY, minZ); - Vector3d boundVert2 = MultiplyPoint3x4(localToWorldMatrix, maxX, maxY, maxZ); - Vector3d boundVert3 = MultiplyPoint3x4(localToWorldMatrix, minX, minY, maxZ); - Vector3d boundVert4 = MultiplyPoint3x4(localToWorldMatrix, minX, maxY, minZ); - Vector3d boundVert5 = MultiplyPoint3x4(localToWorldMatrix, maxX, minY, minZ); - Vector3d boundVert6 = MultiplyPoint3x4(localToWorldMatrix, minX, maxY, maxZ); - Vector3d boundVert7 = MultiplyPoint3x4(localToWorldMatrix, maxX, minY, maxZ); - Vector3d boundVert8 = MultiplyPoint3x4(localToWorldMatrix, maxX, maxY, minZ); + Vector3d boundVert1 = MultiplyPoint3x4(ref localToWorldMatrix, minX, minY, minZ); + Vector3d boundVert2 = MultiplyPoint3x4(ref localToWorldMatrix, maxX, maxY, maxZ); + Vector3d boundVert3 = MultiplyPoint3x4(ref localToWorldMatrix, minX, minY, maxZ); + Vector3d boundVert4 = MultiplyPoint3x4(ref localToWorldMatrix, minX, maxY, minZ); + Vector3d boundVert5 = MultiplyPoint3x4(ref localToWorldMatrix, maxX, minY, minZ); + Vector3d boundVert6 = MultiplyPoint3x4(ref localToWorldMatrix, minX, maxY, maxZ); + Vector3d boundVert7 = MultiplyPoint3x4(ref localToWorldMatrix, maxX, minY, maxZ); + Vector3d boundVert8 = MultiplyPoint3x4(ref localToWorldMatrix, maxX, maxY, minZ); double minDot = double.MaxValue; double maxDot = double.MinValue; @@ -416,7 +759,7 @@ static void UpdateOcclusionData(OcclusionData occlusionData, Vector3d velocity, velToUp.RotatePointGetXZ(boundVert8, out vertX, out vertZ); FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); - Vector3d worldBoundsCenter = MultiplyPoint3x4(localToWorldMatrix, cX, cY, cZ); + Vector3d worldBoundsCenter = MultiplyPoint3x4(ref localToWorldMatrix, cX, cY, cZ); occlusionData.centroidDot = Vector3d.Dot(worldBoundsCenter, velocity); occlusionData.projectedCenter = worldBoundsCenter - occlusionData.centroidDot * velocity; occlusionData.boundsCenter = new Vector3((float)cX, (float)cY, (float)cZ); @@ -455,7 +798,7 @@ private static void FindDotMinMax(double dot, ref double minDot, ref double maxD } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector3d MultiplyPoint3x4(Matrix4x4D m, double x, double y, double z) + private static Vector3d MultiplyPoint3x4(ref Matrix4x4D m, double x, double y, double z) { return new Vector3d( m.m00 * x + m.m01 * y + m.m02 * z + m.m03, @@ -501,54 +844,43 @@ private static QuaternionD FromToRotation(Vector3d from, Vector3d to) return new QuaternionD(x, y, z, qw); } - } - internal struct QuaternionDPointRotation - { - private double qx2; - private double qy2; - private double qz2; - private double qx2x; - private double qy2y; - private double qz2z; - private double qy2x; - private double qz2x; - private double qz2y; - private double qx2w; - private double qy2w; - private double qz2w; - - public QuaternionDPointRotation(QuaternionD rotation) + internal struct QuaternionDPointRotation { - qx2 = rotation.x * 2.0; - qy2 = rotation.y * 2.0; - qz2 = rotation.z * 2.0; - qx2x = rotation.x * qx2; - qy2y = rotation.y * qy2; - qz2z = rotation.z * qz2; - qy2x = rotation.x * qy2; - qz2x = rotation.x * qz2; - qz2y = rotation.y * qz2; - qx2w = rotation.w * qx2; - qy2w = rotation.w * qy2; - qz2w = rotation.w * qz2; - } + private double qx2x; + private double qy2y; + private double qz2z; + private double qy2x; + private double qz2x; + private double qz2y; + private double qx2w; + private double qy2w; + private double qz2w; + + public QuaternionDPointRotation(QuaternionD rotation) + { + double qx2 = rotation.x * 2.0; + double qy2 = rotation.y * 2.0; + double qz2 = rotation.z * 2.0; + qx2x = rotation.x * qx2; + qy2y = rotation.y * qy2; + qz2z = rotation.z * qz2; + qy2x = rotation.x * qy2; + qz2x = rotation.x * qz2; + qz2y = rotation.y * qz2; + qx2w = rotation.w * qx2; + qy2w = rotation.w * qy2; + qz2w = rotation.w * qz2; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void RotatePointGetXZ(Vector3d point, out double x, out double z) - { - x = (1.0 - (qy2y + qz2z)) * point.x + (qy2x - qz2w) * point.y + (qz2x + qy2w) * point.z; - z = (qz2x - qy2w) * point.x + (qz2y + qx2w) * point.y + (1.0 - (qx2x + qy2y)) * point.z; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RotatePointGetXZ(Vector3d point, out double x, out double z) + { + x = (1.0 - (qy2y + qz2z)) * point.x + (qy2x - qz2w) * point.y + (qz2x + qy2w) * point.z; + z = (qz2x - qy2w) * point.x + (qz2y + qx2w) * point.y + (1.0 - (qx2x + qy2y)) * point.z; + } } - } - - public class KSPCFOcclusionData: OcclusionData - { - - // projecte - public KSPCFOcclusionData(PartThermalData data) : base(data) - { - } + #endregion } } From 845af0ab3e9471397b0ac7c0716afc1e3b837178 Mon Sep 17 00:00:00 2001 From: gotmachine <24925209+gotmachine@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:06:41 +0200 Subject: [PATCH 3/8] Preparing to rebase --- KSPCommunityFixes/KSPCommunityFixes.csproj | 1 - KSPCommunityFixes/Library/MuParser.cs | 5 +- .../Performance/FlightIntegratorPerf.cs | 886 ------------------ 3 files changed, 2 insertions(+), 890 deletions(-) delete mode 100644 KSPCommunityFixes/Performance/FlightIntegratorPerf.cs diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index 86f3b1e0..aec90fa9 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -144,7 +144,6 @@ - diff --git a/KSPCommunityFixes/Library/MuParser.cs b/KSPCommunityFixes/Library/MuParser.cs index f1556163..c73ff11c 100644 --- a/KSPCommunityFixes/Library/MuParser.cs +++ b/KSPCommunityFixes/Library/MuParser.cs @@ -10,8 +10,7 @@ namespace KSPCommunityFixes.Library { - // For reference on my 5800X3D/DDR4, when disk reading isn't a factor and for loading 485MB - // worth of models : + // For reference on my 5800X3D/DDR4, when disk reading isn't a factor and for loading 485MB worth of models : // - The stock parser has a throughput of 120 MB/s // - The KSPCF parser has a throughput of 290 MB/s // Roadblocks to further optimizations : @@ -344,7 +343,7 @@ private static Keyframe[] GetKeyFrameBuffer(int keyFrameCount) private static void ReadMeshCollider(GameObject o) { MeshCollider meshCollider = o.AddComponent(); - SkipBool(); // this is actually the "convex" property, but it is always forced to true; + SkipBool(); // this is actually the "convex" property, but it is always forced to true meshCollider.convex = true; meshCollider.sharedMesh = ReadMesh(); } diff --git a/KSPCommunityFixes/Performance/FlightIntegratorPerf.cs b/KSPCommunityFixes/Performance/FlightIntegratorPerf.cs deleted file mode 100644 index defa8acb..00000000 --- a/KSPCommunityFixes/Performance/FlightIntegratorPerf.cs +++ /dev/null @@ -1,886 +0,0 @@ -// Enable cross checking our implementations results with the stock implementations results -// Warning : very log-spammy and performance destroying, don't leave this enabled if you don't need to. -// #define DEBUG_FLIGHTINTEGRATOR - -using HarmonyLib; -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using UnityEngine; -using Debug = UnityEngine.Debug; - -namespace KSPCommunityFixes.Performance -{ - internal class FlightIntegratorPerf : BasePatch - { - protected override void ApplyPatches(List patches) - { - patches.Add(new PatchInfo( - PatchMethodType.Prefix, - AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateOcclusionSolar)), - this)); - - patches.Add(new PatchInfo( - PatchMethodType.Prefix, - AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateOcclusionBody)), - this)); - - patches.Add(new PatchInfo( - PatchMethodType.Prefix, - AccessTools.Method(typeof(FlightIntegrator), nameof(FlightIntegrator.UpdateMassStats)), - this)); - - patches.Add(new PatchInfo( - PatchMethodType.Prefix, - AccessTools.Method(typeof(VesselPrecalculate), nameof(VesselPrecalculate.CalculatePhysicsStats)), - this)); - - // other offenders, in aero situations : - - // AddSurfaceDragDirection : 7% - // - could turn the curves (1.6%) into lookup tables - // - general float to double pass, not sure how practical due to working a lot with float-backed drag cubes, relevant at least for the Mathf.Pow() call - // - multiple InverseTransform calls, use the matrix instead - - // general optimization pass on UpdateOcclusionConvection() : 4.4% - - } - - #region VesselPrecalculate.CalculatePhysicsStats optimizations - - /// - /// 40-60% faster than the stock method depending on the situation. - /// Hard to optimize further, a large chunk of the time is spent getting transform / rb properties (~40%) - /// and performing unavoidable double/float conversions (~10%). - /// - private static bool VesselPrecalculate_CalculatePhysicsStats_Prefix(VesselPrecalculate __instance) - { - Vessel vessel = __instance.vessel; - bool isMasslessOrNotLoaded = true; - - if (vessel.loaded) - { - int partCount = vessel.Parts.Count; - Transform vesselTransform = vessel.ReferenceTransform; - Matrix4x4 vesselInverseMatrixF = vesselTransform.worldToLocalMatrix; - Matrix4x4D vesselInverseMatrix = vesselInverseMatrixF.ToMatrix4x4D(); - QuaternionD vesselInverseRotation = QuaternionD.Inverse(vesselTransform.rotation); - Vector3d com = Vector3d.zero; - Vector3d velocity = Vector3d.zero; - Vector3d angularVelocity = Vector3d.zero; - double vesselMass = 0.0; - - if (vessel.packed && partCount > 0) - Physics.SyncTransforms(); - - VesselPrePartBufferEnsureCapacity(partCount); - int index = partCount; - int rbPartCount = 0; - while (index-- > 0) - { - Part part = vessel.parts[index]; - if (part.rb.IsNotNullOrDestroyed()) - { - Vector3d partPosition = part.partTransform.position; - QuaternionD partRotation = part.partTransform.rotation; - vesselPrePartBuffer[rbPartCount] = new PartVesselPreData(partPosition, partRotation, index); - rbPartCount++; -#if DEBUG_FLIGHTINTEGRATOR - double deviation = ((partPosition + partRotation * part.CoMOffset) - part.rb.worldCenterOfMass).magnitude; - if (deviation > 0.001) - Debug.LogWarning($"[KSPCF:FIPerf] KSPCF calculated WorldCenterOfMass is deviating from stock by {deviation:F3}m for part {part.partInfo.title} on vessel {vessel.GetDisplayName()}"); -#endif - double physicsMass = part.physicsMass; - // note : on flight scene load, the parts RBs center of mass won't be set until the vessel gets out of the packed - // state (see FI.UpdateMassStats()), it will initially be set to whatever PhysX has computed from the RB colliders. - // This result in an inconsistent vessel CoM (and all derived stats) being computed for several frames. - // For performance reasons we don't use rb.worldCenterOfMass, but instead re-compute it from Part.CoMOffset, but - // this also has the side effect of fixing those inconsistencies. - com.Add((partPosition + partRotation * part.CoMOffset) * physicsMass); - velocity.Add((Vector3d)part.rb.velocity * physicsMass); - angularVelocity.Add(vesselInverseRotation* part.rb.angularVelocity * physicsMass); - vesselMass += physicsMass; - } - } - - if (vesselMass > 0.0) - { - isMasslessOrNotLoaded = false; - vessel.totalMass = vesselMass; - double vesselMassRecip = 1.0 / vesselMass; - vessel.CoMD = com * vesselMassRecip; - vessel.rb_velocityD = velocity * vesselMassRecip; - vessel.velocityD = vessel.rb_velocityD + Krakensbane.GetFrameVelocity(); - vessel.CoM = vessel.CoMD; - vessel.localCoM = vesselInverseMatrix.MultiplyPoint3x4(vessel.CoMD); - vessel.rb_velocity = vessel.rb_velocityD; - vessel.angularVelocityD = angularVelocity * vesselMassRecip; - vessel.angularVelocity = vessel.angularVelocityD; - - if (vessel.angularVelocityD == Vector3d.zero && vessel.packed) - { - vessel.MOI.Zero(); - vessel.angularMomentum.Zero(); - } - else - { - InertiaTensor inertiaTensor = new InertiaTensor(); - for (int i = 0; i < rbPartCount; i++) - { - PartVesselPreData partPreData = vesselPrePartBuffer[i]; - Part part = vessel.parts[partPreData.partIndex]; - - // add part inertia tensor to vessel inertia tensor - Vector3d principalMoments = part.rb.inertiaTensor; - QuaternionD princAxesRot = vesselInverseRotation * partPreData.rotation * (QuaternionD)part.rb.inertiaTensorRotation; - inertiaTensor.AddPartInertiaTensor(principalMoments, princAxesRot); - - // add part mass and position contribution to vessel inertia tensor - double rbMass = Math.Max(part.partInfo.minimumRBMass, part.physicsMass); - // Note : the stock MoI code fails to account for the additional RB of servo parts. - // On servo parts, the part physicsMass is redistributed equally between the part RB and the servo RB, and since when - // computing the MoI, the stock code uses only the rb.mass, some mass will be unacounted for. Ideally we should do - // the full additional MoI calcs with the servo RB, but as a shorthand fix we just include the whole physicsMass instead - // of half of it like what stock would do. If we want to replicate exactely stock, uncomment those : - // if (part.servoRb.IsNotNullRef()) - // rbMass *= 0.5; - // Note 2 : another side effect of using Part.physicsMass instead of rb.mass is that mass will be correct on scene - // loads, before FI.UpdateMassStats() has run (when it hasn't run yet, rb.mass is set to 1 for all parts) - Vector3d partPosition = vesselInverseMatrix.MultiplyVector(partPreData.position - vessel.CoMD); - inertiaTensor.AddPartMass(rbMass, partPosition); - } - - vessel.MOI = inertiaTensor.MoI; - vessel.angularMomentum.x = (float)(inertiaTensor.m00 * vessel.angularVelocityD.x); - vessel.angularMomentum.y = (float)(inertiaTensor.m11 * vessel.angularVelocityD.y); - vessel.angularMomentum.z = (float)(inertiaTensor.m22 * vessel.angularVelocityD.z); - } - } - -#if DEBUG_FLIGHTINTEGRATOR - VerifyPhysicsStats(__instance); -#endif - } - - if (isMasslessOrNotLoaded) - { - if (vessel.packed) - { - if (vessel.LandedOrSplashed) - { - vessel.CoMD = __instance.worldSurfacePos + __instance.worldSurfaceRot * vessel.localCoM; - } - else - { - if (!vessel.orbitDriver.Ready) - { - vessel.orbitDriver.orbit.Init(); - vessel.orbitDriver.updateFromParameters(setPosition: false); - } - vessel.CoMD = vessel.mainBody.position + vessel.orbitDriver.pos; - } - } - else - { - vessel.CoMD = vessel.vesselTransform.TransformPoint(vessel.localCoM); - } - - vessel.CoM = vessel.CoMD; - - if (vessel.rootPart.IsNotNullOrDestroyed() && vessel.rootPart.rb.IsNotNullOrDestroyed()) - { - vessel.rb_velocity = vessel.rootPart.rb.GetPointVelocity(vessel.CoM); - vessel.rb_velocityD = vessel.rb_velocity; - vessel.velocityD = (Vector3d)vessel.rb_velocity + Krakensbane.GetFrameVelocity(); - vessel.angularVelocityD = (vessel.angularVelocity = Quaternion.Inverse(vessel.ReferenceTransform.rotation) * vessel.rootPart.rb.angularVelocity); - } - else - { - vessel.rb_velocity.Zero(); - vessel.rb_velocityD.Zero(); - vessel.velocityD.Zero(); - vessel.angularVelocity.Zero(); - vessel.angularVelocityD.Zero(); - } - vessel.MOI.Zero(); - vessel.angularMomentum.Zero(); - } - __instance.firstStatsRunComplete = true; - return false; - } - - private readonly struct PartVesselPreData - { - public readonly int partIndex; - public readonly Vector3d position; - public readonly QuaternionD rotation; - - public PartVesselPreData(Vector3d position, QuaternionD rotation, int partIndex) - { - this.position = position; - this.rotation = rotation; - this.partIndex = partIndex; - } - } - - private static void VesselPrePartBufferEnsureCapacity(int partCount) - { - if (vesselPrePartBuffer.Length < partCount) - vesselPrePartBuffer = new PartVesselPreData[(int)(partCount * 1.25)]; - } - - private static PartVesselPreData[] vesselPrePartBuffer = new PartVesselPreData[300]; - - private struct InertiaTensor - { - public double m00; - public double m11; - public double m22; - - public void AddPartInertiaTensor(Vector3d principalMoments, QuaternionD princAxesRot) - { - // inverse the princAxesRot quaternion - double invpx = -princAxesRot.x; - double invpy = -princAxesRot.y; - double invpz = -princAxesRot.z; - - // prepare inverse rotation - double ipx2 = invpx * 2.0; - double ipy2 = invpy * 2.0; - double ipz2 = invpz * 2.0; - double ipx2x = invpx * ipx2; - double ipy2y = invpy * ipy2; - double ipz2z = invpz * ipz2; - double ipy2x = invpx * ipy2; - double ipz2x = invpx * ipz2; - double ipz2y = invpy * ipz2; - double ipx2w = princAxesRot.w * ipx2; - double ipy2w = princAxesRot.w * ipy2; - double ipz2w = princAxesRot.w * ipz2; - - // inverse rotate column 0 - double ir0x = principalMoments.x * (1.0 - (ipy2y + ipz2z)); - double ir0y = principalMoments.y * (ipy2x + ipz2w); - double ir0z = principalMoments.z * (ipz2x - ipy2w); - - // inverse rotate column 1 - double ir1x = principalMoments.x * (ipy2x - ipz2w); - double ir1y = principalMoments.y * (1.0 - (ipx2x + ipz2z)); - double ir1z = principalMoments.z * (ipz2y + ipx2w); - - // inverse rotate column 2 - double ir2x = principalMoments.x * (ipz2x + ipy2w); - double ir2y = principalMoments.y * (ipz2y - ipx2w); - double ir2z = principalMoments.z * (1.0 - (ipx2x + ipy2y)); - - // prepare rotation - double qx2 = princAxesRot.x * 2.0; - double qy2 = princAxesRot.y * 2.0; - double qz2 = princAxesRot.z * 2.0; - double qx2x = princAxesRot.x * qx2; - double qy2y = princAxesRot.y * qy2; - double qz2z = princAxesRot.z * qz2; - double qy2x = princAxesRot.x * qy2; - double qz2x = princAxesRot.x * qz2; - double qz2y = princAxesRot.y * qz2; - double qx2w = princAxesRot.w * qx2; - double qy2w = princAxesRot.w * qy2; - double qz2w = princAxesRot.w * qz2; - - // rotate column 0 - m00 += (1.0 - (qy2y + qz2z)) * ir0x + (qy2x - qz2w) * ir0y + (qz2x + qy2w) * ir0z; - - // rotate column 1 - m11 += (qy2x + qz2w) * ir1x + (1.0 - (qx2x + qz2z)) * ir1y + (qz2y - qx2w) * ir1z; - - // rotate column 2 - m22 += (qz2x - qy2w) * ir2x + (qz2y + qx2w) * ir2y + (1.0 - (qx2x + qy2y)) * ir2z; - } - - public void AddPartMass(double partMass, Vector3d partPosition) - { - double massLever = partMass * partPosition.sqrMagnitude; - double invMass = -partMass; - - m00 += invMass * partPosition.x * partPosition.x + massLever; - m11 += invMass * partPosition.y * partPosition.y + massLever; - m22 += invMass * partPosition.z * partPosition.z + massLever; - } - - public Vector3 MoI => new Vector3((float)m00, (float)m11, (float)m22); - } - - private static void VerifyPhysicsStats(VesselPrecalculate vesselPre) - { - Vessel vessel = vesselPre.Vessel; - if (!vessel.loaded) - return; - - Transform referenceTransform = vessel.ReferenceTransform; - int partCount = vessel.Parts.Count; - QuaternionD vesselInverseRotation = QuaternionD.Inverse(referenceTransform.rotation); - Vector3d pCoM = Vector3d.zero; - Vector3d pVel = Vector3d.zero; - Vector3d pAngularVel = Vector3d.zero; - double vesselMass = 0.0; // vessel.totalMass - int index = partCount; - while (index-- > 0) - { - Part part = vessel.parts[index]; - if (part.rb != null) - { - double physicsMass = part.physicsMass; - pCoM += (Vector3d)part.rb.worldCenterOfMass * physicsMass; - Vector3d vector3d = (Vector3d)part.rb.velocity * physicsMass; - pVel += vector3d; - pAngularVel += vesselInverseRotation * part.rb.angularVelocity * physicsMass; - vesselMass += physicsMass; - } - } - if (vesselMass > 0.0) - { - double vesselMassRecip = 1.0 / vesselMass; - Vector3d vCoMD = pCoM * vesselMassRecip; // vessel.CoMD - Vector3d vRbVelD = pVel * vesselMassRecip; // vessel.rb_velocityD - Vector3d vAngVelD = pAngularVel * vesselMassRecip; // vessel.angularVelocityD - Vector3 vMoI = vessel.MOI; - if (vAngVelD == Vector3d.zero && vessel.packed) - { - vMoI.Zero(); - } - else - { - Matrix4x4 inertiaTensor = Matrix4x4.zero; - Matrix4x4 mIdentity = Matrix4x4.identity; - Matrix4x4 m2 = Matrix4x4.identity; - Matrix4x4 m3 = Matrix4x4.identity; - Quaternion vesselInverseRotationF = vesselInverseRotation; - for (int i = 0; i < partCount; i++) - { - Part part2 = vessel.parts[i]; - if (part2.rb != null) - { - KSPUtil.ToDiagonalMatrix2(part2.rb.inertiaTensor, ref mIdentity); - Quaternion partRot = vesselInverseRotationF * part2.transform.rotation * part2.rb.inertiaTensorRotation; - Quaternion invPartRot = Quaternion.Inverse(partRot); - Matrix4x4 mPart = Matrix4x4.TRS(Vector3.zero, partRot, Vector3.one); - Matrix4x4 invMPart = Matrix4x4.TRS(Vector3.zero, invPartRot, Vector3.one); - Matrix4x4 right = mPart * mIdentity * invMPart; - KSPUtil.Add(ref inertiaTensor, ref right); - Vector3 lever = referenceTransform.InverseTransformDirection(part2.rb.position - vCoMD); - KSPUtil.ToDiagonalMatrix2(part2.rb.mass * lever.sqrMagnitude, ref m2); - KSPUtil.Add(ref inertiaTensor, ref m2); - KSPUtil.OuterProduct2(lever, (0f - part2.rb.mass) * lever, ref m3); - KSPUtil.Add(ref inertiaTensor, ref m3); - } - } - vMoI = KSPUtil.Diag(inertiaTensor); - } - - string warnings = string.Empty; - - double vMassDiff = Math.Abs(vesselMass - vessel.totalMass); - if (vMassDiff > vesselMass / 1e6) - warnings += $"Mass diverging by {vMassDiff:F6} ({vMassDiff / (vesselMass > 0.0 ? vesselMass : 1.0):P5}) "; - - double vCoMDDiff = (vessel.CoMD - vCoMD).magnitude; - if (vCoMDDiff > vCoMD.magnitude / 1e6) - warnings += $"CoM diverging by {vCoMDDiff:F6} ({vCoMDDiff / (vCoMD.magnitude > 0.0 ? vCoMD.magnitude : 1.0):P5}) "; - - double vVelDiff = (vessel.rb_velocityD - vRbVelD).magnitude; - if (vVelDiff > vRbVelD.magnitude / 1e6) - warnings += $"Velocity diverging by {vVelDiff:F6} ({vVelDiff / (vRbVelD.magnitude > 0.0 ? vRbVelD.magnitude : 1.0):P5}) "; - - double vAngVelDDiff = (vessel.angularVelocityD - vAngVelD).magnitude; - if (vAngVelDDiff > vAngVelD.magnitude / 1e6) - warnings += $"Angular velocity diverging by {vAngVelDDiff:F6} ({vAngVelDDiff / (vAngVelD.magnitude > 0.0 ? vAngVelD.magnitude : 1.0):P5}) "; - - double vMoIDiff = (vessel.MOI - vMoI).magnitude; - if (vMoIDiff > vMoI.magnitude / 1e5) - warnings += $"MoI diverging by {vMoIDiff:F6} ({vMoIDiff / (vMoI.magnitude > 0.0 ? vMoI.magnitude : 1.0):P5}) "; - - if (warnings.Length > 0) - { - Debug.LogWarning($"[KSPCF:FIPerf] CalculatePhysicsStats : diverging stats for vessel {vessel.GetDisplayName()}\n{warnings}"); - } - } - } - - #endregion - - #region FlightIntegrator.UpdateMassStats optimizations - - // Avoid setting RigidBody.mass and RigidBody.centerOfMass for all parts on every update if they didn't change - // Setting these properties is quite costly on the PhysX side, especially centerOfMass (1% to 2% of the frame time - // depending on the situation), and centerOfMass should almost never change unless something is changing CoMOffset. - // Setting mass is less costly and will change relatively often but avoiding setting when unecessary is still a decent improvement. - // We also take the opportunity to make a few optimizations (faster null checks, inlined inner loop, using the PartResourceList - // backing list instead of going through the custom indexer...) - static bool FlightIntegrator_UpdateMassStats_Prefix(FlightIntegrator __instance) - { - List parts = __instance.vessel.parts; - for (int i = __instance.partCount; i-- > 0;) - { - Part part = parts[i]; - - List partResources = part._resources.dict.list; - float resourceMass = 0f; - double resourceThermalMass = 0.0; - for (int j = partResources.Count; j-- > 0;) - { - PartResource partResource = partResources[j]; - float resMass = (float)partResource.amount * partResource.info.density; - resourceMass += resMass; - resourceThermalMass += resMass * partResource.info._specificHeatCapacity; - } - - part.resourceMass = resourceMass; - part.resourceThermalMass = resourceThermalMass; - part.thermalMass = part.mass * __instance.cacheStandardSpecificHeatCapacity * part.thermalMassModifier + part.resourceThermalMass; - __instance.SetSkinThermalMass(part); - part.thermalMass = Math.Max(part.thermalMass - part.skinThermalMass, 0.1); - part.thermalMassReciprocal = 1.0 / part.thermalMass; - } - - for (int i = __instance.partCount; i-- > 0;) - { - Part part = parts[i]; - if (part.rb.IsNotNullOrDestroyed()) - { - float physicsMass = part.mass + part.resourceMass + __instance.GetPhysicslessChildMass(part); // further optimization : don't use recursion, avoid the null check - physicsMass = Mathf.Clamp(physicsMass, part.partInfo.MinimumMass, Mathf.Abs(physicsMass)); - part.physicsMass = physicsMass; - - if (!part.packed) - { - float rbMass = Mathf.Max(part.partInfo.MinimumRBMass, physicsMass); - bool hasServoRB = part.servoRb.IsNotNullOrDestroyed(); - - if (hasServoRB) - rbMass *= 0.5f; - - // unfortunately, there is some internal fp manipulation when setting rb.mass - // resulting in tiny deltas between what we set and the value we read back. - if (Math.Abs(part.rb.mass - rbMass) > rbMass / 1e6f) - { - part.rb.mass = rbMass; - if (hasServoRB) - part.servoRb.mass = rbMass; - } - - // Either this doesn't happen with rb.centerOfMass, or the built-in Vector3 equality - // epsilon takes cares of it. I guess this might happen still if a large offset is defined... - if (part.rb.centerOfMass != part.CoMOffset) - part.rb.centerOfMass = part.CoMOffset; - } - } - else - { - part.physicsMass = 0.0; - } - } - - return false; - } - - #endregion - - #region FlightIntegrator.UpdateOcclusion optimizations - - static bool FlightIntegrator_UpdateOcclusionSolar_Prefix(FlightIntegrator __instance) - { - FlightIntegrator fi = __instance; - List occlusionDataList = fi.occlusionSun; - OcclusionCylinder[] occluders = fi.occludersSun; - Vector3d velocity = fi.sunVector; - - bool requiresSort = false; - - if (fi.partThermalDataCount != fi.partThermalDataList.Count) - { - fi.recreateThermalGraph = true; - fi.CheckThermalGraph(); - } - - int lastPartIndex = fi.partThermalDataCount - 1; - int partIndex = fi.partThermalDataCount; - QuaternionDPointRotation velToUp = new QuaternionDPointRotation(FromToRotation(velocity, Vector3d.up)); - while (partIndex-- > 0) - { - OcclusionData occlusionDataToUpdate = occlusionDataList[partIndex]; - UpdateOcclusionData(occlusionDataToUpdate, velocity, velToUp); - if (!requiresSort && partIndex < lastPartIndex && occlusionDataList[partIndex + 1].maximumDot < occlusionDataToUpdate.maximumDot) - requiresSort = true; - } - - if (requiresSort) - occlusionDataList.Sort(); - - OcclusionData occlusionData = occlusionDataList[lastPartIndex]; - occlusionData.ptd.sunAreaMultiplier = 1.0; - occlusionData.sunCyl.Setup(occlusionData); - occluders[0] = occlusionData.sunCyl; - - // O(n²) [n = part count] situation here, so micro-optimizing the inner loop is critical. - int occluderCount = 1; - int index = lastPartIndex; - while (index-- > 0) - { - occlusionData = occlusionDataList[index]; - double minExtentsX = occlusionData.minExtents.x; - double minExtentsY = occlusionData.minExtents.y; - double maxExtentsX = occlusionData.maxExtents.x; - double maxExtentsY = occlusionData.maxExtents.y; - - double areaMultiplier = 1.0; - - for (int i = 0; i < occluderCount; i++) - { - // GetCylinderOcclusion - OcclusionCylinder occluder = occluders[i]; - double offsetX = occluder.offset.x; - double offsetY = occluder.offset.y; - double minX = offsetX + minExtentsX; - double minY = offsetY + minExtentsY; - double maxX = offsetX + maxExtentsX; - double maxY = offsetY + maxExtentsY; - double centralExtentX = occluder.extents.x; - double centralExtentY = occluder.extents.y; - double centralExtentXInv = -centralExtentX; - double centralExtentYInv = -centralExtentY; - - double mid = (maxX - minX) * (maxY - minY); - if (maxX >= centralExtentXInv && minX <= centralExtentX && maxY >= centralExtentYInv && minY <= centralExtentY && mid != 0.0) - { - double midX = Math.Min(centralExtentX, maxX) - Math.Max(centralExtentXInv, minX); - if (midX < 0.0) midX = 0.0; - - double midY = Math.Min(centralExtentY, maxY) - Math.Max(centralExtentYInv, minY); - if (midY < 0.0) midY = 0.0; - - double rectRect = midX * midY / mid; - if (double.IsNaN(rectRect)) // it could be nice to put that outside the inner loop - { - if (GameSettings.FI_LOG_TEMP_ERROR) - Debug.LogError("[FlightIntegrator]: For part " + occlusionData.ptd.part.name + ", rectRect is NaN"); - } - else - { - areaMultiplier -= rectRect; - } - } - - if (areaMultiplier < 0.001) - { - areaMultiplier = 0.0; - break; - } - } - - occlusionData.ptd.sunAreaMultiplier = areaMultiplier; - if (areaMultiplier > 0) - { - occlusionData.sunCyl.Setup(occlusionData); - occluders[occluderCount] = occlusionData.sunCyl; - occluderCount++; - } - } - - return false; - } - - static bool FlightIntegrator_UpdateOcclusionBody_Prefix(FlightIntegrator __instance) - { - FlightIntegrator fi = __instance; - List occlusionDataList = fi.occlusionBody; - OcclusionCylinder[] occluders = fi.occludersBody; - Vector3d velocity = -fi.vessel.upAxis; - - bool requiresSort = false; - - if (fi.partThermalDataCount != fi.partThermalDataList.Count) - { - fi.recreateThermalGraph = true; - fi.CheckThermalGraph(); - } - - int lastPartIndex = fi.partThermalDataCount - 1; - int partIndex = fi.partThermalDataCount; - QuaternionDPointRotation velToUp = new QuaternionDPointRotation(FromToRotation(velocity, Vector3d.up)); - while (partIndex-- > 0) - { - OcclusionData occlusionDataToUpdate = occlusionDataList[partIndex]; - UpdateOcclusionData(occlusionDataToUpdate, velocity, velToUp); - if (!requiresSort && partIndex < lastPartIndex && occlusionDataList[partIndex + 1].maximumDot < occlusionDataToUpdate.maximumDot) - requiresSort = true; - } - - if (requiresSort) - occlusionDataList.Sort(); - - OcclusionData occlusionData = occlusionDataList[lastPartIndex]; - occlusionData.ptd.bodyAreaMultiplier = 1.0; - occlusionData.bodyCyl.Setup(occlusionData); - occluders[0] = occlusionData.bodyCyl; - - int occluderCount = 1; - int index = lastPartIndex; - while (index-- > 0) - { - occlusionData = occlusionDataList[index]; - double minExtentsX = occlusionData.minExtents.x; - double minExtentsY = occlusionData.minExtents.y; - double maxExtentsX = occlusionData.maxExtents.x; - double maxExtentsY = occlusionData.maxExtents.y; - - double areaMultiplier = 1.0; - - for (int i = 0; i < occluderCount; i++) - { - // GetCylinderOcclusion - OcclusionCylinder occluder = occluders[i]; - double offsetX = occluder.offset.x; - double offsetY = occluder.offset.y; - double minX = offsetX + minExtentsX; - double minY = offsetY + minExtentsY; - double maxX = offsetX + maxExtentsX; - double maxY = offsetY + maxExtentsY; - double centralExtentX = occluder.extents.x; - double centralExtentY = occluder.extents.y; - - double mid = (maxX - minX) * (maxY - minY); - if (!(maxX < 0.0 - centralExtentX) && !(minX > centralExtentX) && !(maxY < 0.0 - centralExtentY) && !(minY > centralExtentY) && mid != 0.0) - { - double rectRect = Math.Max(0.0, Math.Min(centralExtentX, maxX) - Math.Max(0.0 - centralExtentX, minX)) * Math.Max(0.0, Math.Min(centralExtentY, maxY) - Math.Max(0.0 - centralExtentY, minY)) / mid; - if (double.IsNaN(rectRect)) - { - if (GameSettings.FI_LOG_TEMP_ERROR) - Debug.LogError("[FlightIntegrator]: For part " + occlusionData.ptd.part.name + ", rectRect is NaN"); - } - else - { - areaMultiplier -= rectRect; - } - } - - if (areaMultiplier < 0.001) - { - areaMultiplier = 0.0; - break; - } - } - - occlusionData.ptd.bodyAreaMultiplier = areaMultiplier; - if (areaMultiplier > 0) - { - occlusionData.bodyCyl.Setup(occlusionData); - occluders[occluderCount] = occlusionData.bodyCyl; - occluderCount++; - } - } - return false; - } - - // a lot of stuff is actually unused in OcclusionData - // boundsVertices (only used in the scope of OcclusionData.Update, we use local vars and inline stuff instead) - // projectedVertices, projectedDots : part of an alternative thermal thing that isn't activated / never called - // useDragArea is always true, so the involved code paths are never taken - static void UpdateOcclusionData(OcclusionData occlusionData, Vector3d velocity, QuaternionDPointRotation velToUp) - { - Part part = occlusionData.part; - - if (part.IsNullOrDestroyed() || part.partTransform.IsNullOrDestroyed()) - return; - - Vector3 center = occlusionData.part.DragCubes.WeightedCenter; - Vector3 size = occlusionData.part.DragCubes.WeightedSize; - - double cX = center.x; - double cY = center.y; - double cZ = center.z; - double eX = size.x * 0.5; - double eY = size.y * 0.5; - double eZ = size.z * 0.5; - double minX = cX - eX; - double minY = cY - eY; - double minZ = cZ - eZ; - double maxX = cX + eX; - double maxY = cY + eY; - double maxZ = cZ + eZ; - - Matrix4x4D localToWorldMatrix = (Matrix4x4D)part.partTransform.localToWorldMatrix; // 10% of the load. - - // 10% of the load is here, probably worth it to extract the matrix components and to manually inline (the MultiplyPoint3x4 method is **not** inlined) - Vector3d boundVert1 = MultiplyPoint3x4(ref localToWorldMatrix, minX, minY, minZ); - Vector3d boundVert2 = MultiplyPoint3x4(ref localToWorldMatrix, maxX, maxY, maxZ); - Vector3d boundVert3 = MultiplyPoint3x4(ref localToWorldMatrix, minX, minY, maxZ); - Vector3d boundVert4 = MultiplyPoint3x4(ref localToWorldMatrix, minX, maxY, minZ); - Vector3d boundVert5 = MultiplyPoint3x4(ref localToWorldMatrix, maxX, minY, minZ); - Vector3d boundVert6 = MultiplyPoint3x4(ref localToWorldMatrix, minX, maxY, maxZ); - Vector3d boundVert7 = MultiplyPoint3x4(ref localToWorldMatrix, maxX, minY, maxZ); - Vector3d boundVert8 = MultiplyPoint3x4(ref localToWorldMatrix, maxX, maxY, minZ); - - double minDot = double.MaxValue; - double maxDot = double.MinValue; - double minExtentX = double.MaxValue; - double minExtentY = double.MaxValue; - double maxExtentX = double.MinValue; - double maxExtentY = double.MinValue; - - FindDotMinMax(Vector3d.Dot(boundVert1, velocity), ref minDot, ref maxDot); - velToUp.RotatePointGetXZ(boundVert1, out double vertX, out double vertZ); - FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); - - FindDotMinMax(Vector3d.Dot(boundVert2, velocity), ref minDot, ref maxDot); - velToUp.RotatePointGetXZ(boundVert2, out vertX, out vertZ); - FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); - - FindDotMinMax(Vector3d.Dot(boundVert3, velocity), ref minDot, ref maxDot); - velToUp.RotatePointGetXZ(boundVert3, out vertX, out vertZ); - FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); - - FindDotMinMax(Vector3d.Dot(boundVert4, velocity), ref minDot, ref maxDot); - velToUp.RotatePointGetXZ(boundVert4, out vertX, out vertZ); - FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); - - FindDotMinMax(Vector3d.Dot(boundVert5, velocity), ref minDot, ref maxDot); - velToUp.RotatePointGetXZ(boundVert5, out vertX, out vertZ); - FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); - - FindDotMinMax(Vector3d.Dot(boundVert6, velocity), ref minDot, ref maxDot); - velToUp.RotatePointGetXZ(boundVert6, out vertX, out vertZ); - FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); - - FindDotMinMax(Vector3d.Dot(boundVert7, velocity), ref minDot, ref maxDot); - velToUp.RotatePointGetXZ(boundVert7, out vertX, out vertZ); - FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); - - FindDotMinMax(Vector3d.Dot(boundVert8, velocity), ref minDot, ref maxDot); - velToUp.RotatePointGetXZ(boundVert8, out vertX, out vertZ); - FindExtentsMinMax(vertX, vertZ, ref minExtentX, ref minExtentY, ref maxExtentX, ref maxExtentY); - - Vector3d worldBoundsCenter = MultiplyPoint3x4(ref localToWorldMatrix, cX, cY, cZ); - occlusionData.centroidDot = Vector3d.Dot(worldBoundsCenter, velocity); - occlusionData.projectedCenter = worldBoundsCenter - occlusionData.centroidDot * velocity; - occlusionData.boundsCenter = new Vector3((float)cX, (float)cY, (float)cZ); - occlusionData.minimumDot = minDot; - occlusionData.maximumDot = maxDot; - occlusionData.minExtents = new Vector2((float)minExtentX, (float)minExtentY); // minExtents / maxExtents : ideally flatten into double fields - occlusionData.maxExtents = new Vector2((float)maxExtentX, (float)maxExtentY); - - occlusionData.extents = (occlusionData.maxExtents - occlusionData.minExtents) * 0.5f; // extents, center : ideally flatten into double fields - occlusionData.center = occlusionData.minExtents + occlusionData.extents; - - occlusionData.projectedArea = part.DragCubes.CrossSectionalArea; - occlusionData.invFineness = part.DragCubes.TaperDot; - occlusionData.maxWidthDepth = part.DragCubes.Depth; - - occlusionData.projectedRadius = Math.Sqrt(occlusionData.projectedArea / Math.PI); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void FindExtentsMinMax(double x, double z, ref double minExtentX, ref double minExtentY, ref double maxExtentX, ref double maxExtentY) - { - maxExtentX = Math.Max(maxExtentX, x); - maxExtentY = Math.Max(maxExtentY, z); - minExtentX = Math.Min(minExtentX, x); - minExtentY = Math.Min(minExtentY, z); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void FindDotMinMax(double dot, ref double minDot, ref double maxDot) - { - if (dot < minDot) - minDot = dot; - - if (dot > maxDot) - maxDot = dot; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector3d MultiplyPoint3x4(ref Matrix4x4D m, double x, double y, double z) - { - return new Vector3d( - m.m00 * x + m.m01 * y + m.m02 * z + m.m03, - m.m10 * x + m.m11 * y + m.m12 * z + m.m13, - m.m20 * x + m.m21 * y + m.m22 * z + m.m23); - } - - private static QuaternionD FromToRotation(Vector3d from, Vector3d to) - { - double d = Vector3d.Dot(from, to); - double qw = Math.Sqrt(from.sqrMagnitude * to.sqrMagnitude) + d; - double x, y, z, sqrMag; - if (qw < 1e-12) - { - // vectors are 180 degrees apart - x = from.x; - y = from.y; - z = -from.z; - sqrMag = x * x + y * y + z * z; - if (sqrMag != 1.0) - { - double invNorm = 1.0 / Math.Sqrt(sqrMag); - x *= invNorm; - y *= invNorm; - z *= invNorm; - } - return new QuaternionD(x, y, z, 0.0); - } - - Vector3d axis = Vector3d.Cross(from, to); - x = axis.x; - y = axis.y; - z = axis.z; - sqrMag = x * x + y * y + z * z + qw * qw; - if (sqrMag != 1.0) - { - double invNorm = 1.0 / Math.Sqrt(sqrMag); - x *= invNorm; - y *= invNorm; - z *= invNorm; - qw *= invNorm; - } - - return new QuaternionD(x, y, z, qw); - } - - internal struct QuaternionDPointRotation - { - private double qx2x; - private double qy2y; - private double qz2z; - private double qy2x; - private double qz2x; - private double qz2y; - private double qx2w; - private double qy2w; - private double qz2w; - - public QuaternionDPointRotation(QuaternionD rotation) - { - double qx2 = rotation.x * 2.0; - double qy2 = rotation.y * 2.0; - double qz2 = rotation.z * 2.0; - qx2x = rotation.x * qx2; - qy2y = rotation.y * qy2; - qz2z = rotation.z * qz2; - qy2x = rotation.x * qy2; - qz2x = rotation.x * qz2; - qz2y = rotation.y * qz2; - qx2w = rotation.w * qx2; - qy2w = rotation.w * qy2; - qz2w = rotation.w * qz2; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void RotatePointGetXZ(Vector3d point, out double x, out double z) - { - x = (1.0 - (qy2y + qz2z)) * point.x + (qy2x - qz2w) * point.y + (qz2x + qy2w) * point.z; - z = (qz2x - qy2w) * point.x + (qz2y + qx2w) * point.y + (1.0 - (qx2x + qy2y)) * point.z; - } - } - - #endregion - } -} From b6ebe8b5aee19e07743ce10d338cbb78efc8a856 Mon Sep 17 00:00:00 2001 From: gotmachine <24925209+gotmachine@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:11:28 +0200 Subject: [PATCH 4/8] More cleanup of unused / not useful stuff --- KSPCommunityFixes/Library/ShaderHelpers.cs | 4 - KSPCommunityFixes/Library/StaticHelpers.cs | 31 --- .../Performance/GeneralPerfFixes.cs | 187 +++--------------- 3 files changed, 24 insertions(+), 198 deletions(-) diff --git a/KSPCommunityFixes/Library/ShaderHelpers.cs b/KSPCommunityFixes/Library/ShaderHelpers.cs index a97be755..3b11f4d9 100644 --- a/KSPCommunityFixes/Library/ShaderHelpers.cs +++ b/KSPCommunityFixes/Library/ShaderHelpers.cs @@ -1,9 +1,5 @@ using PartToolsLib; -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using UnityEngine; namespace KSPCommunityFixes.Library diff --git a/KSPCommunityFixes/Library/StaticHelpers.cs b/KSPCommunityFixes/Library/StaticHelpers.cs index 1254ac16..0ef08393 100644 --- a/KSPCommunityFixes/Library/StaticHelpers.cs +++ b/KSPCommunityFixes/Library/StaticHelpers.cs @@ -93,36 +93,5 @@ public static bool EditPartModuleKSPFieldAttributes(Type partModuleType, string /// /// The value. public static bool IsPowerOfTwo(uint value) => (value & (value - 1)) == 0 && value != 0; - - public static Matrix4x4D ToMatrix4x4D(ref this Matrix4x4 m) - { - return new Matrix4x4D( - m.m00, m.m01, m.m02, m.m03, - m.m10, m.m11, m.m12, m.m13, - m.m20, m.m21, m.m22, m.m23, - m.m30, m.m31, m.m32, m.m33); - } - - public static Vector3d MultiplyVector(ref this Matrix4x4D m, Vector3d vector) - { - return new Vector3d( - m.m00 * vector.x + m.m01 * vector.y + m.m02 * vector.z, - m.m10 * vector.x + m.m11 * vector.y + m.m12 * vector.z, - m.m20 * vector.x + m.m21 * vector.y + m.m22 * vector.z); - } - - /// - /// 3 times faster than using the + operator - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Add(ref this Vector3d v, Vector3d other) - { - v.x += other.x; - v.y += other.y; - v.z += other.z; - } - } } diff --git a/KSPCommunityFixes/Performance/GeneralPerfFixes.cs b/KSPCommunityFixes/Performance/GeneralPerfFixes.cs index e73608c7..1edd1959 100644 --- a/KSPCommunityFixes/Performance/GeneralPerfFixes.cs +++ b/KSPCommunityFixes/Performance/GeneralPerfFixes.cs @@ -1,27 +1,15 @@ -using System; +using HarmonyLib; +using KSPCommunityFixes.Library; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using HarmonyLib; -using KSP.UI.Screens; -using KSP.UI.Screens.Settings; -using KSPCommunityFixes.Library; -using TMPro.Examples; using UnityEngine; -using UnityEngine.UIElements; -using VehiclePhysics; using static System.Number; -using static GameDatabase; -using static Highlighting.Highlighter.RendererCache; -using static iT; -using static ProceduralSpaceObject; using UObject = UnityEngine.Object; namespace KSPCommunityFixes.Performance @@ -46,58 +34,11 @@ protected override void ApplyPatches(List patches) PatchMethodType.Prefix, AccessTools.Method(typeof(PartLoader), nameof(PartLoader.ApplyPartValue)), this)); - - patches.Add(new PatchInfo( - PatchMethodType.Prefix, - AccessTools.Method(typeof(BaseFieldList), nameof(BaseFieldList.CreateList)), - this)); - - //patches.Add(new PatchInfo( - // PatchMethodType.Postfix, - // AccessTools.Method(typeof(PartLoader), nameof(PartLoader.CreatePartIcon)), - // this)); - - //patches.Add(new PatchInfo( - // PatchMethodType.Prefix, - // AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetModelPrefab)), - // this)); - - //// texture getting patches would also benefit to model loading, but we need to run earlier... - //patches.Add(new PatchInfo( - // PatchMethodType.Prefix, - // AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetTextureInfo)), - // this)); - - //patches.Add(new PatchInfo( - // PatchMethodType.Prefix, - // AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetTexture)), - // this)); - - //patches.Add(new PatchInfo( - // PatchMethodType.Prefix, - // AccessTools.Method(typeof(PartModule), nameof(PartModule.ModularSetup)), - // this)); - - //patches.Add(new PatchInfo( - // PatchMethodType.Postfix, - // AccessTools.Method(typeof(PartModule), nameof(PartModule.ModularSetup)), - // this)); } static bool FlightGlobals_fetch_Prefix(out FlightGlobals __result) { - if (KSPCFFastLoader.PartCompilationInProgress) - { - __result = null; - return false; - } - - if (FlightGlobals._fetch.IsNullOrDestroyed()) - { - FlightGlobals._fetch = UObject.FindObjectOfType(); - } - - __result = FlightGlobals._fetch; + __result = FlightGlobals._fetch.IsNullOrDestroyed() ? null : FlightGlobals._fetch; return false; } @@ -142,78 +83,22 @@ private static Shader GetIconShader(string materialShaderName) return shader_ScreenSpaceMask; } - //static void PartLoader_CreatePartIcon_Prefix() - //{ - // watch.Start(); - //} - - //static void PartLoader_CreatePartIcon_Postfix() - //{ - // watch.Stop(); - //} - - private static HashSet OnIconCreatePartModules; - - private static bool HasOnIconCreateModule(Part part) - { - if (OnIconCreatePartModules == null) - { - OnIconCreatePartModules = new HashSet(); - - foreach (Type type in AccessTools.AllTypes()) - { - if (type.IsSubclassOf(typeof(PartModule))) - { - if (type.GetMethod(nameof(PartModule.OnIconCreate), BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) != null) - { - OnIconCreatePartModules.Add(type); - } - } - } - } - - if (OnIconCreatePartModules.Count == 0) - return false; - - List modules = part.modules.modules; - for (int i = modules.Count; i-- > 0;) - { - if (OnIconCreatePartModules.Contains(modules[i].GetType())) - { - return true; - } - } - - return false; - } - static bool PartLoader_CreatePartIcon_Prefix(GameObject newPart, out float iconScale, out GameObject __result) { watch.Start(); - newPart.SetActive(false); - GameObject partObject = UObject.Instantiate(newPart); - newPart.SetActive(true); - //partObject.SetActive(true); - Part part = partObject.GetComponent(); - - // only activate the part if a module requires it. - // probably not a good idea in the end : - // - some model setup might happen in Awake(), and we would miss those - // - the perf gains are not as good as I hoped. - if (HasOnIconCreateModule(part)) - { - partObject.SetActive(true); - } + GameObject iconPartObject = UObject.Instantiate(newPart); + iconPartObject.SetActive(true); + Part iconPart = iconPartObject.GetComponent(); - if (part.IsNotNullOrDestroyed()) + if (iconPart.IsNotNullOrDestroyed()) { int i = 0; - for (int count = part.Modules.Count; i < count; i++) - part.Modules[i].OnIconCreate(); + for (int count = iconPart.Modules.Count; i < count; i++) + iconPart.Modules[i].OnIconCreate(); } Bounds partBounds = default; - partObject.GetComponentsInChildren(false, componentBuffer); + iconPartObject.GetComponentsInChildren(false, componentBuffer); try { for (int i = componentBuffer.Count; i-- > 0;) @@ -265,7 +150,7 @@ static bool PartLoader_CreatePartIcon_Prefix(GameObject newPart, out float iconS for (int i = colliderObjectsToDestroy.Count; i-- > 0;) UObject.DestroyImmediate(colliderObjectsToDestroy[i]); - partObject.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity); + iconPartObject.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity); bool first = true; for (int i = iconRenderers.Count; i-- > 0;) @@ -301,9 +186,9 @@ static bool PartLoader_CreatePartIcon_Prefix(GameObject newPart, out float iconS { Color originalColor = iconMaterial.color; iconMaterial.SetColor(PropertyIDs._Color, new Color( - Mathf.Clamp(originalColor.r, 0.5f, 1f), - Mathf.Clamp(originalColor.g, 0.5f, 1f), - Mathf.Clamp(originalColor.b, 0.5f, 1f))); + originalColor.r < 0.5f ? 0.5f : originalColor.r, + originalColor.g < 0.5f ? 0.5f : originalColor.g, + originalColor.b < 0.5f ? 0.5f : originalColor.b)); } else { @@ -344,14 +229,13 @@ static bool PartLoader_CreatePartIcon_Prefix(GameObject newPart, out float iconS iconScale = iconScale > z ? iconScale : z; iconScale = 1f / iconScale; - GameObject iconObject = new GameObject(); - iconObject.name = partObject.name + " icon"; - partObject.transform.parent = iconObject.transform; - partObject.transform.localScale = Vector3.one * iconScale; - partObject.transform.localPosition = partBounds.center * (0f - iconScale); + GameObject iconObject = new GameObject(iconPartObject.name + " icon"); + iconPartObject.transform.parent = iconObject.transform; + iconPartObject.transform.localScale = Vector3.one * iconScale; + iconPartObject.transform.localPosition = partBounds.center * (0f - iconScale); iconObject.transform.parent = PartLoader.Instance.transform; iconObject.SetActive(false); - partObject.SetActive(true); + iconPartObject.SetActive(true); __result = iconObject; watch.Stop(); @@ -785,7 +669,10 @@ static bool PartLoader_ApplyPartValue_Prefix(Part part, ConfigNode.Value nodeVal __result = false; return false; } + } + public static class UnsafeStringParsing + { static unsafe Vector2 ParseVector2(string value) { UnsafeString str = new UnsafeString(value); @@ -853,7 +740,7 @@ internal static unsafe bool TryStringToNumber(UnsafeString str, NumberStyles opt char* ptr = str.ptr; if (!ParseNumber(ref ptr, options, ref number, null, numfmt, parseDecimal) || (ptr - str.ptr < str.length && !TrailingZeros(str, (int)(ptr - str.ptr)))) return false; - + return true; } @@ -865,32 +752,6 @@ private static bool TrailingZeros(UnsafeString str, int index) return true; } - - static bool BaseFieldList_CreateList_Prefix(BaseFieldList __instance, object instance) - { - // base code from BaseFieldList - BaseFieldList.ReflectedData reflectedAttributes = BaseFieldList.GetReflectedAttributes(instance.GetType(), __instance.ignoreUIControlWhenCreatingReflectedData); - - __instance._fields.Capacity = reflectedAttributes.fields.Count; - int i = 0; - for (int count = reflectedAttributes.fields.Count; i < count; i++) - { - BaseField val = new BaseField(reflectedAttributes.fieldAttributes[i], (FieldInfo)reflectedAttributes.fields[i], instance); - val.SetOriginalValue(); - __instance._fields.Add(val); - } - - // override from BaseFieldList : BaseFieldList - // ReflectedData reflectedAttributes = BaseFieldList.GetReflectedAttributes(instance.GetType(), __instance.ignoreUIControlWhenCreatingReflectedData); - int j = 0; - for (int count = reflectedAttributes.controls.Count; j < count; j++) - { - BaseField item = new BaseField(reflectedAttributes.controlAttributes[j], (FieldInfo)reflectedAttributes.fields[j], instance); - __instance._fields.Add(item); - } - - return false; - } } public struct UnsafeString From d97398679c1637bbec5d33b6248b57f05902ff1f Mon Sep 17 00:00:00 2001 From: gotmachine <24925209+gotmachine@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:05:45 +0200 Subject: [PATCH 5/8] State looks good enough to become a PR, I guess --- GameData/KSPCommunityFixes/Settings.cfg | 2 + KSPCommunityFixes/KSPCommunityFixes.csproj | 4 +- KSPCommunityFixes/Library/MuParser.cs | 3 - KSPCommunityFixes/Performance/FastLoader.cs | 92 +--- .../Performance/MinorPerfTweaks.cs | 17 +- ...GeneralPerfFixes.cs => PartParsingPerf.cs} | 430 ++---------------- 6 files changed, 74 insertions(+), 474 deletions(-) rename KSPCommunityFixes/Performance/{GeneralPerfFixes.cs => PartParsingPerf.cs} (65%) diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg index 84ffd5a7..8f2d4f99 100644 --- a/GameData/KSPCommunityFixes/Settings.cfg +++ b/GameData/KSPCommunityFixes/Settings.cfg @@ -459,6 +459,8 @@ KSP_COMMUNITY_FIXES // but this helps a bit in other cases as well. FloatingOriginPerf = true + PartParsingPerf = true + // ########################## // Modding // ########################## diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index 75de080a..e0d0ad43 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -1,4 +1,4 @@ - + @@ -175,7 +175,7 @@ - + diff --git a/KSPCommunityFixes/Library/MuParser.cs b/KSPCommunityFixes/Library/MuParser.cs index c73ff11c..63ecbfcb 100644 --- a/KSPCommunityFixes/Library/MuParser.cs +++ b/KSPCommunityFixes/Library/MuParser.cs @@ -1101,9 +1101,6 @@ private static unsafe Color32 ReadColor32() { int valIdx = Advance(4); return *(Color32*)(dataPtr + valIdx); - - Advance(4); - return new Color32(data[index - 4], data[index - 3], data[index - 2], data[index - 1]); } private static unsafe BoneWeight ReadBoneWeight() diff --git a/KSPCommunityFixes/Performance/FastLoader.cs b/KSPCommunityFixes/Performance/FastLoader.cs index 5a672e6b..114189ac 100644 --- a/KSPCommunityFixes/Performance/FastLoader.cs +++ b/KSPCommunityFixes/Performance/FastLoader.cs @@ -79,15 +79,16 @@ void Start() $"- Configs reload done in {wSecondConfigLoad.Elapsed.TotalSeconds:F3}s\n" + $"- Configs translated in {wConfigTranslate.Elapsed.TotalSeconds:F3}s\n" + $"- {KSPCFFastLoader.loadedAssetCount} assets loaded in {wAssetsLoading.Elapsed.TotalSeconds:F3}s :\n" + - $" - {KSPCFFastLoader.audioFilesLoaded} audio assets ({StaticHelpers.HumanReadableBytes(audioBytesLoaded)}) in {wAudioLoading.Elapsed.TotalSeconds:F3}s\n" + + $" - {KSPCFFastLoader.audioFilesLoaded} audio assets ({StaticHelpers.HumanReadableBytes(audioBytesLoaded)}) in {wAudioLoading.Elapsed.TotalSeconds:F3}s, {StaticHelpers.HumanReadableBytes((long)(audioBytesLoaded / wAudioLoading.Elapsed.TotalSeconds))}/s\n" + $" - {texturesLoaded} texture assets ({StaticHelpers.HumanReadableBytes(texturesBytesLoaded)}) in {wTextureLoading.Elapsed.TotalSeconds:F3}s, {StaticHelpers.HumanReadableBytes((long)(texturesBytesLoaded / wTextureLoading.Elapsed.TotalSeconds))}/s\n" + - $" - {modelsLoaded} model assets ({StaticHelpers.HumanReadableBytes(modelsBytesLoaded)}) in {wModelLoading.Elapsed.TotalSeconds:F3}s, {wModelLoading.Elapsed.TotalMilliseconds / modelsLoaded:F3} ms/model\n" + + $" - {modelsLoaded} model assets ({StaticHelpers.HumanReadableBytes(modelsBytesLoaded)}) in {wModelLoading.Elapsed.TotalSeconds:F3}s, {StaticHelpers.HumanReadableBytes((long)(modelsBytesLoaded / wModelLoading.Elapsed.TotalSeconds))}/s\n" + $"- Asset bundles loaded in {wAssetBundleLoading.Elapsed.TotalSeconds:F3}s\n" + $"- GameDatabase (configs, resources, traits, upgrades...) loaded in {wGamedatabaseLoading.Elapsed.TotalSeconds:F3}s\n" + $"- Built-in parts copied in {wBuiltInPartsCopy.Elapsed.TotalSeconds:F3}s\n" + $"- Part and internal configs extracted in {wPartConfigExtraction.Elapsed.TotalSeconds:F3}s\n" + $"- {totalPartsLoaded} parts and {totalModulesLoaded} modules compiled in {wPartCompilationLoading.Elapsed.TotalSeconds:F3}s\n" + - $" ({totalModulesLoaded / (float)totalPartsLoaded:F1} modules/part, {wPartCompilationLoading.Elapsed.TotalMilliseconds / totalPartsLoaded:F3} ms/part, {wPartCompilationLoading.Elapsed.TotalMilliseconds / totalModulesLoaded:F3} ms/module)\n" + + $" - {totalModulesLoaded / (float)totalPartsLoaded:F1} modules/part, {wPartCompilationLoading.Elapsed.TotalMilliseconds / totalPartsLoaded:F3} ms/part, {wPartCompilationLoading.Elapsed.TotalMilliseconds / totalModulesLoaded:F3} ms/module\n" + + $" - PartIcon compilation : {PartParsingPerf.iconCompilationWatch.Elapsed.TotalSeconds:F3}s\n" + $"- {totalInternalsLoaded} internal spaces and {totalInternalPropsLoaded} props compiled in {wInternalCompilationLoading.Elapsed.TotalSeconds:F3}s\n"; if (ExpansionsLoader.expansionsInfo.Count > 0) @@ -98,7 +99,6 @@ void Start() Debug.Log(log); Debug.Log($"Texture queries : {KSPCFFastLoader.txcallCount}, slow path : {KSPCFFastLoader.txMissCount} ({KSPCFFastLoader.txMissCount / (float)KSPCFFastLoader.txcallCount:P2})"); - Debug.Log($"PartIcon compilation : {GeneralPerfFixes.watch.Elapsed.TotalMilliseconds:F3}"); Destroy(gameObject); } } @@ -896,7 +896,6 @@ static IEnumerator AudioLoader(UrlFile urlFile) static int loadedBytes; static object lockObject = new object(); - /// /// Textures / models loader coroutine implementing threaded disk reads and framerate decoupling /// @@ -1018,8 +1017,6 @@ static void ReadAssetsThread(List files, Deque buffer) } } - - /// /// Asset wrapper class, actual implementation of the disk reader, individual texture/model formats loaders /// @@ -1672,87 +1669,6 @@ private TextureInfo LoadTRUECOLOR() return new TextureInfo(file, texture, isNormalMap, !isNormalMap, false); } - /* - private static void InitPartReader() - { - if (PartReader.matDummies == null) - PartReader.matDummies = new List(); - else - PartReader.matDummies.Clear(); - - if (PartReader.boneDummies == null) - PartReader.boneDummies = new List(); - else - PartReader.boneDummies.Clear(); - - if (PartReader.textureDummies == null) - PartReader.textureDummies = new PartReader.TextureDummyList(); - else - PartReader.textureDummies.Clear(); - } - - private static void CleanPartReader() - { - PartReader.matDummies.Clear(); - PartReader.boneDummies.Clear(); - PartReader.textureDummies.Clear(); - } - - private GameObject LoadMU() - { - InitPartReader(); - PartReader.file = file; - memoryStream = new MemoryStream(buffer, 0, dataLength); - binaryReader = new BinaryReader(memoryStream); - PartToolsLib.FileType fileType = (PartToolsLib.FileType)binaryReader.ReadInt32(); - PartReader.fileVersion = binaryReader.ReadInt32(); - _ = binaryReader.ReadString() + string.Empty; - if (fileType != PartToolsLib.FileType.ModelBinary) - { - SetError($"'{file.url}.mu' is an incorrect type."); - return null; - } - GameObject gameObject = null; - try - { - gameObject = PartReader.ReadChild(binaryReader, null); - if (PartReader.boneDummies.Count > 0) - { - int i = 0; - for (int count = PartReader.boneDummies.Count; i < count; i++) - { - Transform[] array = new Transform[PartReader.boneDummies[i].bones.Count]; - int j = 0; - for (int count2 = PartReader.boneDummies[i].bones.Count; j < count2; j++) - { - array[j] = PartReader.FindChildByName(gameObject.transform, PartReader.boneDummies[i].bones[j]); - } - PartReader.boneDummies[i].smr.bones = array; - } - } - if (PartReader.shaderFallback) - { - Renderer[] componentsInChildren = gameObject.GetComponentsInChildren(); - int k = 0; - for (int num = componentsInChildren.Length; k < num; k++) - { - Renderer renderer = componentsInChildren[k]; - int l = 0; - for (int num2 = renderer.sharedMaterials.Length; l < num2; l++) - { - renderer.sharedMaterials[l].shader = Shader.Find("KSP/Diffuse"); - } - } - } - } - finally - { - CleanPartReader(); - } - return gameObject; - } - */ - private GameObject LoadMU() { return MuParser.Parse(file.parent.url, buffer, dataLength); diff --git a/KSPCommunityFixes/Performance/MinorPerfTweaks.cs b/KSPCommunityFixes/Performance/MinorPerfTweaks.cs index f90a2457..a3117ba1 100644 --- a/KSPCommunityFixes/Performance/MinorPerfTweaks.cs +++ b/KSPCommunityFixes/Performance/MinorPerfTweaks.cs @@ -1,4 +1,5 @@ -using System; +using HarmonyLib; +using System; using System.Collections.Generic; using UnityEngine; @@ -10,11 +11,25 @@ internal class MinorPerfTweaks : BasePatch protected override void ApplyPatches() { + AddPatch(PatchType.Override, AccessTools.PropertyGetter(typeof(FlightGlobals), nameof(FlightGlobals.fetch)), nameof(FlightGlobals_fetch_Override)); + AddPatch(PatchType.Override, typeof(Part), nameof(Part.isKerbalEVA)); AddPatch(PatchType.Override, typeof(VolumeNormalizer), nameof(VolumeNormalizer.Update)); } + // When FlightGlobals._fetch is null/destroyed, the stock "fetch" getter fallback to a FindObjectOfType() + // call. This is extremly slow, and account for ~10% of the total loading time (7 seconds) of the total + // launch > main menu on stock + BDB install, due to being called during part compilation. + // The _fetch field is acquired from Awake() and set to null in OnDestroy(), so there is no real reason for this. + // The only behavior change I can think of would be something calling fetch in between the OnDestroy() + // call and the effective destruction of the native object. In any case, this can be qualified as a bug, + // as the flightglobal instance left accessible will be in quite invalid state. + private static FlightGlobals FlightGlobals_fetch_Override() + { + return FlightGlobals._fetch.IsNullOrDestroyed() ? null : FlightGlobals._fetch; + } + // Called (sometimes multiple times) in Part.FixedUpdate() public static bool Part_isKerbalEVA_Override(Part part) { diff --git a/KSPCommunityFixes/Performance/GeneralPerfFixes.cs b/KSPCommunityFixes/Performance/PartParsingPerf.cs similarity index 65% rename from KSPCommunityFixes/Performance/GeneralPerfFixes.cs rename to KSPCommunityFixes/Performance/PartParsingPerf.cs index 1edd1959..dcbb3e25 100644 --- a/KSPCommunityFixes/Performance/GeneralPerfFixes.cs +++ b/KSPCommunityFixes/Performance/PartParsingPerf.cs @@ -1,45 +1,23 @@ -using HarmonyLib; -using KSPCommunityFixes.Library; +using KSPCommunityFixes.Library; using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Reflection; using System.Reflection.Emit; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using UnityEngine; -using static System.Number; using UObject = UnityEngine.Object; namespace KSPCommunityFixes.Performance { - internal class GeneralPerfFixes : BasePatch + internal class PartParsingPerf : BasePatch { - internal static Stopwatch watch = new Stopwatch(); + internal static Stopwatch iconCompilationWatch = new Stopwatch(); - protected override void ApplyPatches(List patches) + protected override void ApplyPatches() { - patches.Add(new PatchInfo( - PatchMethodType.Prefix, - AccessTools.PropertyGetter(typeof(FlightGlobals), nameof(FlightGlobals.fetch)), - this, nameof(FlightGlobals_fetch_Prefix))); - - patches.Add(new PatchInfo( - PatchMethodType.Prefix, - AccessTools.Method(typeof(PartLoader), nameof(PartLoader.CreatePartIcon)), - this)); - - patches.Add(new PatchInfo( - PatchMethodType.Prefix, - AccessTools.Method(typeof(PartLoader), nameof(PartLoader.ApplyPartValue)), - this)); - } + AddPatch(PatchType.Prefix, typeof(PartLoader), nameof(PartLoader.CreatePartIcon)); - static bool FlightGlobals_fetch_Prefix(out FlightGlobals __result) - { - __result = FlightGlobals._fetch.IsNullOrDestroyed() ? null : FlightGlobals._fetch; - return false; + AddPatch(PatchType.Prefix, typeof(PartLoader), nameof(PartLoader.ApplyPartValue)); } static List componentBuffer = new List(); @@ -83,9 +61,9 @@ private static Shader GetIconShader(string materialShaderName) return shader_ScreenSpaceMask; } - static bool PartLoader_CreatePartIcon_Prefix(GameObject newPart, out float iconScale, out GameObject __result) + private static bool PartLoader_CreatePartIcon_Prefix(GameObject newPart, out float iconScale, out GameObject __result) { - watch.Start(); + iconCompilationWatch.Start(); GameObject iconPartObject = UObject.Instantiate(newPart); iconPartObject.SetActive(true); Part iconPart = iconPartObject.GetComponent(); @@ -236,15 +214,16 @@ static bool PartLoader_CreatePartIcon_Prefix(GameObject newPart, out float iconS iconObject.transform.parent = PartLoader.Instance.transform; iconObject.SetActive(false); iconPartObject.SetActive(true); - __result = iconObject; - watch.Stop(); + iconCompilationWatch.Stop(); return false; } - private static Dictionary partFieldsSetters = new Dictionary(); - private static Dictionary compoundPartFieldsSetters = new Dictionary(); - private static Dictionary> derivedPartSetters; + + + private static Dictionary partFieldsSetters = new Dictionary(300); + private static Dictionary compoundPartFieldsSetters = new Dictionary(300); + private static Dictionary> unknownPartFieldsSetters = new Dictionary>(); private abstract class PartFieldSetter { @@ -512,7 +491,7 @@ public override bool SetField(object instance, string strValue) } } - public static Action CreateInstanceSetter(FieldInfo field) + private static Action CreateInstanceSetter(FieldInfo field) { string methodName = field.ReflectedType.FullName + ".set_" + field.Name; DynamicMethod setterMethod = new DynamicMethod(methodName, null, new Type[2] { typeof(object), typeof(T) }, true); @@ -524,7 +503,7 @@ public static Action CreateInstanceSetter(FieldInfo field) return (Action)setterMethod.CreateDelegate(typeof(Action)); } - public static Action CreateInstanceBoxedValueSetter(FieldInfo field) + private static Action CreateInstanceBoxedValueSetter(FieldInfo field) { string methodName = field.ReflectedType.FullName + ".set_" + field.Name; DynamicMethod setterMethod = new DynamicMethod(methodName, null, new Type[2] { typeof(object), typeof(object) }, true); @@ -539,9 +518,30 @@ public static Action CreateInstanceBoxedValueSetter(FieldInfo fi private static bool partFieldDictionariesAreBuilt; - public static void BuildPartFieldDictionaries() + private static void BuildPartFieldDictionaries() + { + PopulatePartFieldDictionary(typeof(Part), partFieldsSetters); + PopulatePartFieldDictionary(typeof(CompoundPart), compoundPartFieldsSetters); + + foreach (AssemblyLoader.LoadedAssembly assembly in AssemblyLoader.loadedAssemblies.assemblies) + { + if (assembly.typesDictionary.TryGetValue(typeof(Part), out Dictionary partTypes)) + { + foreach (Type unknownPartType in partTypes.Values) + { + if (unknownPartType != typeof(Part) && unknownPartType != typeof(CompoundPart)) + { + Dictionary unknownTypeDict = new Dictionary(300); + unknownPartFieldsSetters.Add(unknownPartType, unknownTypeDict); + PopulatePartFieldDictionary(unknownPartType, unknownTypeDict); + } + } + } + } + } + + private static void PopulatePartFieldDictionary(Type partType, Dictionary typeSetterDict) { - Type partType = typeof(Part); foreach (FieldInfo fieldInfo in partType.GetFields(BindingFlags.Instance | BindingFlags.Public)) { PartFieldSetter setter; @@ -632,17 +632,8 @@ public static void BuildPartFieldDictionaries() } } - partFieldsSetters[fieldInfo.Name] = setter; + typeSetterDict[fieldInfo.Name] = setter; } - - - //foreach (Type type in AccessTools.AllTypes()) - //{ - // if (type.IsSubclassOf(Part)) - // { - - // } - //} } static bool PartLoader_ApplyPartValue_Prefix(Part part, ConfigNode.Value nodeValue, out bool __result) @@ -653,8 +644,17 @@ static bool PartLoader_ApplyPartValue_Prefix(Part part, ConfigNode.Value nodeVal BuildPartFieldDictionaries(); } + Type partType = part.GetType(); + Dictionary partFieldSetterDict; + if (partType == typeof(Part)) + partFieldSetterDict = partFieldsSetters; + else if (partType == typeof(CompoundPart)) + partFieldSetterDict = compoundPartFieldsSetters; + else if (!unknownPartFieldsSetters.TryGetValue(partType, out partFieldSetterDict)) + throw new Exception($"Unknown part type: '{partType}'"); + string valueName = nodeValue.name; - if (partFieldsSetters.TryGetValue(valueName, out PartFieldSetter setter)) + if (partFieldSetterDict.TryGetValue(valueName, out PartFieldSetter setter)) { if (setter == null) { @@ -670,334 +670,4 @@ static bool PartLoader_ApplyPartValue_Prefix(Part part, ConfigNode.Value nodeVal return false; } } - - public static class UnsafeStringParsing - { - static unsafe Vector2 ParseVector2(string value) - { - UnsafeString str = new UnsafeString(value); - for (int i = str.length; i-- > 0;) - { - if (str[i] == ',') - { - UnsafeString x = str.Substring(0, i); - UnsafeString y = str.Substring(i + 1); - Vector2 result = new Vector2(ParseSingle(x), ParseSingle(y)); - str.UnPin(); - return result; - } - } - - InvalidFormatException(); - return default; - } - - private static void InvalidFormatException() - { - throw new FormatException(); - } - - internal static NumberFormatInfo numberFormatInfo = NumberFormatInfo.CurrentInfo; - internal static NumberStyles numberStyleFloat = NumberStyles.Float | NumberStyles.AllowThousands; - - internal static unsafe float ParseSingle(UnsafeString str) - { - byte* stackBuffer = stackalloc byte[(int)(uint)NumberBuffer.NumberBufferBytes]; - NumberBuffer number = new NumberBuffer(stackBuffer); - double value2 = 0.0; - - if (!TryStringToNumber(str, numberStyleFloat, ref number, numberFormatInfo, parseDecimal: false)) - { - UnsafeString trimmedStr = str.Trim(); - if (trimmedStr.Equals(numberFormatInfo.PositiveInfinitySymbol)) - { - return float.PositiveInfinity; - } - if (trimmedStr.Equals(numberFormatInfo.NegativeInfinitySymbol)) - { - return float.NegativeInfinity; - } - if (trimmedStr.Equals(numberFormatInfo.NaNSymbol)) - { - return float.NaN; - } - throw new FormatException("Input string was not in a correct format."); - } - if (!NumberBufferToDouble(number.PackForNative(), ref value2)) - { - throw new OverflowException("Value was either too large or too small for a Single."); - } - float num = (float)value2; - if (float.IsInfinity(num)) - { - throw new OverflowException("Value was either too large or too small for a Single."); - } - return num; - } - - internal static unsafe bool TryStringToNumber(UnsafeString str, NumberStyles options, ref NumberBuffer number, NumberFormatInfo numfmt, bool parseDecimal) - { - char* ptr = str.ptr; - if (!ParseNumber(ref ptr, options, ref number, null, numfmt, parseDecimal) || (ptr - str.ptr < str.length && !TrailingZeros(str, (int)(ptr - str.ptr)))) - return false; - - return true; - } - - private static bool TrailingZeros(UnsafeString str, int index) - { - for (int i = index; i < str.length; i++) - if (str[i] != 0) - return false; - - return true; - } - } - - public struct UnsafeString - { - public unsafe char* ptr; - public int length; - private GCHandle handle; - - private unsafe UnsafeString(char* ptr, int length, GCHandle handle) - { - this.ptr = ptr; - this.length = length; - this.handle = handle; - } - - public unsafe UnsafeString(string str) - { - handle = GCHandle.Alloc(str, GCHandleType.Pinned); - ptr = (char*)handle.AddrOfPinnedObject(); - length = str.Length; - } - - public unsafe UnsafeString(string str, int start, int length = 0) - { - handle = GCHandle.Alloc(str, GCHandleType.Pinned); - ptr = (char*)handle.AddrOfPinnedObject() + start; - this.length = length == 0 ? str.Length - start : length; - } - - public static unsafe UnsafeString Empty => new UnsafeString((char*)0, 0, new GCHandle()); - - public unsafe char this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { -#if DEBUG - if (index < 0 || index >= length) - throw new IndexOutOfRangeException(); -#endif - return ptr[index]; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe UnsafeString Substring(int start, int length = 0) - { - if (start + length > length) - ArgumentOutOfRange(); - - return new UnsafeString( - ptr + start, - length == 0 ? this.length - start : length, - handle); - } - - private static void ArgumentOutOfRange() - { - throw new ArgumentOutOfRangeException(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe UnsafeString Trim() - { - int firstNonEmptyChar = 0; - while (firstNonEmptyChar < length && char.IsWhiteSpace(ptr[firstNonEmptyChar])) - firstNonEmptyChar++; - - if (firstNonEmptyChar == length) - return Empty; - - int lastNonEmptyChar = length - 1; - while (char.IsWhiteSpace(ptr[lastNonEmptyChar])) - lastNonEmptyChar--; - - return Substring(firstNonEmptyChar, lastNonEmptyChar - firstNonEmptyChar + 1); - } - - public void UnPin() - { - if (handle.IsAllocated) - handle.Free(); - } - - public override unsafe string ToString() - { - return new string(ptr, 0, length); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe bool Equals(UnsafeString value) - { - if (ptr == value.ptr && length == value.length) - return true; - - if (length != value.length) - return false; - - return Equals(this, value); - } - - public static unsafe bool Equals(UnsafeString value1, UnsafeString value2) - { - int num = value1.length; - char* ptr1 = value1.ptr; - char* ptr2 = value2.ptr; - - if (Environment.Is64BitProcess) - { - while (num >= 12) - { - if (*(long*)ptr1 != *(long*)ptr2) - { - return false; - } - if (*(long*)(ptr1 + 4) != *(long*)(ptr2 + 4)) - { - return false; - } - if (*(long*)(ptr1 + 8) != *(long*)(ptr2 + 8)) - { - return false; - } - ptr1 += 12; - ptr2 += 12; - num -= 12; - } - } - else - { - while (num >= 10) - { - if (*(int*)ptr1 != *(int*)ptr2) - { - return false; - } - if (*(int*)(ptr1 + 2) != *(int*)(ptr2 + 2)) - { - return false; - } - if (*(int*)(ptr1 + 4) != *(int*)(ptr2 + 4)) - { - return false; - } - if (*(int*)(ptr1 + 6) != *(int*)(ptr2 + 6)) - { - return false; - } - if (*(int*)(ptr1 + 8) != *(int*)(ptr2 + 8)) - { - return false; - } - ptr1 += 10; - ptr2 += 10; - num -= 10; - } - } - while (num > 0 && *(int*)ptr1 == *(int*)ptr2) - { - ptr1 += 2; - ptr2 += 2; - num -= 2; - } - return num <= 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe bool Equals(string value) - { - fixed (char* strPtr = &value.m_firstChar) - if (ptr == strPtr && length == value.Length) - return true; - - if (length != value.Length) - return false; - - return Equals(this, value); - } - - public static unsafe bool Equals(UnsafeString value1, string value2) - { - int num = value1.length; - char* ptr1 = value1.ptr; - fixed (char* strPtr = &value2.m_firstChar) - { - char* ptr2 = strPtr; - if (Environment.Is64BitProcess) - { - while (num >= 12) - { - if (*(long*)ptr1 != *(long*)ptr2) - { - return false; - } - if (*(long*)(ptr1 + 4) != *(long*)(ptr2 + 4)) - { - return false; - } - if (*(long*)(ptr1 + 8) != *(long*)(ptr2 + 8)) - { - return false; - } - ptr1 += 12; - ptr2 += 12; - num -= 12; - } - } - else - { - while (num >= 10) - { - if (*(int*)ptr1 != *(int*)ptr2) - { - return false; - } - if (*(int*)(ptr1 + 2) != *(int*)(ptr2 + 2)) - { - return false; - } - if (*(int*)(ptr1 + 4) != *(int*)(ptr2 + 4)) - { - return false; - } - if (*(int*)(ptr1 + 6) != *(int*)(ptr2 + 6)) - { - return false; - } - if (*(int*)(ptr1 + 8) != *(int*)(ptr2 + 8)) - { - return false; - } - ptr1 += 10; - ptr2 += 10; - num -= 10; - } - } - while (num > 0 && *(int*)ptr1 == *(int*)ptr2) - { - ptr1 += 2; - ptr2 += 2; - num -= 2; - } - return num <= 0; - } - - - } - } } From ca336a7c94d8ea6ccee1278fe98c0f5d3dfd5997 Mon Sep 17 00:00:00 2001 From: gotmachine <24925209+gotmachine@users.noreply.github.com> Date: Thu, 17 Oct 2024 10:06:59 +0200 Subject: [PATCH 6/8] Changed PartParsingPerf patches to be overrides instead of prefixes --- .../Performance/PartParsingPerf.cs | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/KSPCommunityFixes/Performance/PartParsingPerf.cs b/KSPCommunityFixes/Performance/PartParsingPerf.cs index dcbb3e25..51241b95 100644 --- a/KSPCommunityFixes/Performance/PartParsingPerf.cs +++ b/KSPCommunityFixes/Performance/PartParsingPerf.cs @@ -15,9 +15,9 @@ internal class PartParsingPerf : BasePatch protected override void ApplyPatches() { - AddPatch(PatchType.Prefix, typeof(PartLoader), nameof(PartLoader.CreatePartIcon)); + AddPatch(PatchType.Override, typeof(PartLoader), nameof(PartLoader.CreatePartIcon)); - AddPatch(PatchType.Prefix, typeof(PartLoader), nameof(PartLoader.ApplyPartValue)); + AddPatch(PatchType.Override, typeof(PartLoader), nameof(PartLoader.ApplyPartValue)); } static List componentBuffer = new List(); @@ -61,7 +61,7 @@ private static Shader GetIconShader(string materialShaderName) return shader_ScreenSpaceMask; } - private static bool PartLoader_CreatePartIcon_Prefix(GameObject newPart, out float iconScale, out GameObject __result) + private static GameObject PartLoader_CreatePartIcon_Override(PartLoader partLoader, GameObject newPart, out float iconScale) { iconCompilationWatch.Start(); GameObject iconPartObject = UObject.Instantiate(newPart); @@ -173,7 +173,7 @@ private static bool PartLoader_CreatePartIcon_Prefix(GameObject newPart, out flo iconMaterial.SetColor(PropertyIDs._Color, Color.white); } - iconMaterial.SetFloat(PropertyIDs._Multiplier, PartLoader.Instance.shaderMultiplier); + iconMaterial.SetFloat(PropertyIDs._Multiplier, partLoader.shaderMultiplier); iconMaterial.SetFloat(PropertyIDs._MinX, 0f); iconMaterial.SetFloat(PropertyIDs._MaxX, 1f); iconMaterial.SetFloat(PropertyIDs._MinY, 0f); @@ -211,16 +211,14 @@ private static bool PartLoader_CreatePartIcon_Prefix(GameObject newPart, out flo iconPartObject.transform.parent = iconObject.transform; iconPartObject.transform.localScale = Vector3.one * iconScale; iconPartObject.transform.localPosition = partBounds.center * (0f - iconScale); - iconObject.transform.parent = PartLoader.Instance.transform; + iconObject.transform.parent = partLoader.transform; iconObject.SetActive(false); iconPartObject.SetActive(true); - __result = iconObject; + //__result = iconObject; iconCompilationWatch.Stop(); - return false; + return iconObject; } - - private static Dictionary partFieldsSetters = new Dictionary(300); private static Dictionary compoundPartFieldsSetters = new Dictionary(300); private static Dictionary> unknownPartFieldsSetters = new Dictionary>(); @@ -636,7 +634,7 @@ private static void PopulatePartFieldDictionary(Type partType, Dictionary Date: Sat, 19 Oct 2024 13:07:45 +0200 Subject: [PATCH 7/8] Added [ManualPatch] attribute, allow to define a BasePatch that can be called manually. Main intended usage is for patches requiring to be applied before ModuleManagerPostLoad. --- KSPCommunityFixes/BasePatch.cs | 13 ++++++++++++- KSPCommunityFixes/KSPCommunityFixes.cs | 17 +++++++++-------- KSPCommunityFixes/KSPCommunityFixes.csproj | 2 +- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/KSPCommunityFixes/BasePatch.cs b/KSPCommunityFixes/BasePatch.cs index 0857f67f..59931504 100644 --- a/KSPCommunityFixes/BasePatch.cs +++ b/KSPCommunityFixes/BasePatch.cs @@ -43,6 +43,17 @@ internal class TranspileInDebugAttribute : Attribute { } + /// + /// When applied to a class derived from , the patch won't be automatically applied. + /// To apply the patch, call . Note that if that call happens before ModuleManager + /// has patched the configs (ie, before part compilation), must be overriden + /// to return , or the patch won't be applied. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + internal class ManualPatchAttribute : Attribute + { + } + public abstract class BasePatch { public static readonly string pluginData = "PluginData"; @@ -64,7 +75,7 @@ public static void Patch(Type patchType) if (!patch.IgnoreConfig && !KSPCommunityFixes.enabledPatches.Contains(patchType.Name)) { - Debug.Log($"[KSPCommunityFixes] Patch {patchType.Name} not applied (disabled in Settings.cfg)"); + Debug.Log($"[KSPCommunityFixes] Patch {patchType.Name} not applied (not enabled in Settings.cfg)"); return; } diff --git a/KSPCommunityFixes/KSPCommunityFixes.cs b/KSPCommunityFixes/KSPCommunityFixes.cs index 0853461c..4be9996b 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.cs +++ b/KSPCommunityFixes/KSPCommunityFixes.cs @@ -14,7 +14,6 @@ public class KSPCommunityFixes : MonoBehaviour public static string LOC_KSPCF_Title = "KSP Community Fixes"; - public static Harmony Harmony { get; private set; } public static HashSet enabledPatches = new HashSet(); @@ -50,6 +49,14 @@ public static Version KspVersion } } + static KSPCommunityFixes() + { + Harmony = new Harmony("KSPCommunityFixes"); +#if DEBUG + Harmony.DEBUG = true; +#endif + } + public static T GetPatchInstance() where T : BasePatch { if (!patchInstances.TryGetValue(typeof(T), out BasePatch instance)) @@ -72,11 +79,6 @@ void Start() DontDestroyOnLoad(this); } - Harmony = new Harmony("KSPCommunityFixes"); - -#if DEBUG - Harmony.DEBUG = true; -#endif LocalizationUtils.GenerateLocTemplateIfRequested(); LocalizationUtils.ParseLocalization(); @@ -113,10 +115,9 @@ public void MMPostLoadCallback() List patchesTypes = new List(); foreach (Type type in Assembly.GetAssembly(basePatchType).GetTypes()) { - if (!type.IsAbstract && type.IsSubclassOf(basePatchType)) + if (!type.IsAbstract && type.IsSubclassOf(basePatchType) && type.GetCustomAttribute() == null) { patchesTypes.Add(type); - } } diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index f6e1cc75..0b86c07a 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -1,4 +1,4 @@ - + From b5d6362b934e4e66ac4268414c1d018fb845d550 Mon Sep 17 00:00:00 2001 From: gotmachine <24925209+gotmachine@users.noreply.github.com> Date: Sat, 19 Oct 2024 15:35:38 +0200 Subject: [PATCH 8/8] - Splitted the GameDatabse.GetModel/GetTexture patches into a separate file / class. - Refactored those patches as a BasePatch using the new [ManualPatch] attribute (instead of patching manually with a separate harmony instance). - Patched a few additional texture/model getting methods. --- KSPCommunityFixes/KSPCommunityFixes.csproj | 1 + KSPCommunityFixes/Performance/FastLoader.cs | 182 +------------ .../Performance/GameDatabasePerf.cs | 241 ++++++++++++++++++ 3 files changed, 252 insertions(+), 172 deletions(-) create mode 100644 KSPCommunityFixes/Performance/GameDatabasePerf.cs diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index e0d0ad43..bd72a13c 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -164,6 +164,7 @@ + diff --git a/KSPCommunityFixes/Performance/FastLoader.cs b/KSPCommunityFixes/Performance/FastLoader.cs index 114189ac..714cb040 100644 --- a/KSPCommunityFixes/Performance/FastLoader.cs +++ b/KSPCommunityFixes/Performance/FastLoader.cs @@ -98,7 +98,7 @@ void Start() $"- Planetary system loaded in {wPSystemSetup.Elapsed.TotalSeconds:F3}s"; Debug.Log(log); - Debug.Log($"Texture queries : {KSPCFFastLoader.txcallCount}, slow path : {KSPCFFastLoader.txMissCount} ({KSPCFFastLoader.txMissCount / (float)KSPCFFastLoader.txcallCount:P2})"); + Debug.Log($"Texture queries : {GameDatabasePerf.txcallCount}, slow path : {GameDatabasePerf.txMissCount} ({GameDatabasePerf.txMissCount / (float)GameDatabasePerf.txcallCount:P2})"); Destroy(gameObject); } } @@ -196,27 +196,10 @@ private void Awake() loader = this; IsPatchEnabled = true; - persistentHarmony = new Harmony(PersistentHarmonyID); - - MethodInfo m_GameDatabase_GetModelPrefab = AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetModelPrefab)); - MethodInfo p_GameDatabase_GetModelPrefab = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_GetModelPrefab_Prefix)); - persistentHarmony.Patch(m_GameDatabase_GetModelPrefab, new HarmonyMethod(p_GameDatabase_GetModelPrefab)); - - MethodInfo m_GameDatabase_GetModelPrefabIn = AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetModelPrefabIn)); - MethodInfo p_GameDatabase_GetModelPrefabIn = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_GetModelPrefabIn_Prefix)); - persistentHarmony.Patch(m_GameDatabase_GetModelPrefabIn, new HarmonyMethod(p_GameDatabase_GetModelPrefabIn)); + // Patch the various GameDatabase.GetModel/GetTexture methods to use the FastLoader dictionaries + BasePatch.Patch(typeof(GameDatabasePerf)); - MethodInfo m_GameDatabase_GetModelFile = AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetModelFile), new []{typeof(GameObject)}); - MethodInfo p_GameDatabase_GetModelFile = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_GetModelFile_Prefix)); - persistentHarmony.Patch(m_GameDatabase_GetModelFile, new HarmonyMethod(p_GameDatabase_GetModelFile)); - - MethodInfo m_GameDatabase_GetTextureInfo = AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetTextureInfo)); - MethodInfo p_GameDatabase_GetTextureInfo = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_GetTextureInfo_Prefix)); - persistentHarmony.Patch(m_GameDatabase_GetTextureInfo, new HarmonyMethod(p_GameDatabase_GetTextureInfo)); - - MethodInfo m_GameDatabase_GetTexture = AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.GetTexture)); - MethodInfo p_GameDatabase_GetTexture = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_GetTexture_Prefix)); - persistentHarmony.Patch(m_GameDatabase_GetTexture, new HarmonyMethod(p_GameDatabase_GetTexture)); + persistentHarmony = new Harmony(PersistentHarmonyID); MethodInfo m_PSystemManager_Awake = AccessTools.Method(typeof(PSystemManager), nameof(PSystemManager.Awake)); MethodInfo p_PSystemManager_Awake = AccessTools.Method(typeof(KSPCFFastLoaderPSystemSetup), nameof(KSPCFFastLoaderPSystemSetup.PSystemManager_Awake_Prefix)); @@ -677,7 +660,12 @@ static IEnumerator FastAssetLoader(List configFileTypes) yield return null; // call non-stock texture loaders - texturesByUrl = new Dictionary(allTextureFiles.Count); + + // note : we could use the StringComparer.OrdinalIgnoreCase comparer as the dictionary key comparer, + // as this is the comparison that stock is doing. However, profiling show that casing mismatches rarely happen + // (never in stock, 0.22% of calls in a very heavily modded install with a bunch of part mods of varying quality) + // and the overhead of the OrdinalIgnoreCase comparer is offsetting the gains (but a small margin, but still). + texturesByUrl = new Dictionary(allTextureFiles.Count); unsupportedFilesCount = unsupportedTextureFiles.Count; loadersCount = gdb.loadersTexture.Count; @@ -2562,157 +2550,7 @@ public void Show() #endregion - #region General perf patches - - static bool GameDatabase_GetModelPrefab_Prefix(GameDatabase __instance, string url, out GameObject __result) - { - if (url == null) - { - __result = null; - return false; - } - - if (!modelsByUrl.TryGetValue(url, out __result)) - { - // We need a fallback because models are also added from asset bundles, - // and because anyone could be adding models as the databaseModel list is public - List models = __instance.databaseModel; - for (int i = models.Count; i-- > 0;) - { - if (models[i].name == url) - { - __result = models[i]; - modelsByUrl.Add(url, __result); - break; - } - } - } - - return false; - } - - static bool GameDatabase_GetModelPrefabIn_Prefix(GameDatabase __instance, string url, out GameObject __result) - { - if (url == null) - { - __result = null; - return false; - } - - if (!modelsByDirectoryUrl.TryGetValue(url, out __result)) - { - // We need a fallback because models are also added from asset bundles, - // and because anyone could be adding models as the databaseModel list is public - List models = __instance.databaseModel; - for (int i = models.Count; i-- > 0;) - { - string modelName = models[i].name; - if (modelName.Substring(0, modelName.LastIndexOf('/')) == url) - { - __result = models[i]; - modelsByDirectoryUrl.Add(url, __result); - break; - } - } - } - - return false; - } - static bool GameDatabase_GetModelFile_Prefix(GameDatabase __instance, GameObject modelPrefab, out UrlFile __result) - { - if (modelPrefab.IsNullRef()) - { - __result = null; - return false; - } - - if (!urlFilesByModel.TryGetValue(modelPrefab, out __result)) - { - // We need a fallback because models are also added from asset bundles, - // and because anyone could be adding models as the databaseModel list is public - List models = __instance.databaseModel; - for (int i = models.Count; i-- > 0;) - { - if (models[i] == modelPrefab) - { - __result = __instance.databaseModelFiles[i]; - urlFilesByModel.Add(modelPrefab, __result); - break; - } - } - } - - return false; - } - - static bool GameDatabase_GetTextureInfo_Prefix(GameDatabase __instance, string url, out TextureInfo __result) - { - txcallCount++; - if (url == null) - { - __result = null; - return false; - } - - if (__instance.flagSwaps.TryGetValue(url, out string newUrl)) - url = newUrl; - - if (!texturesByUrl.TryGetValue(url, out __result)) - { - for (int i = __instance.databaseTexture.Count; i-- > 0;) - { - if (string.Equals(url, __instance.databaseTexture[i].name, StringComparison.OrdinalIgnoreCase)) - { - __result = __instance.databaseTexture[i]; - texturesByUrl.Add(url, __result); - txMissCount++; - break; - } - } - } - return false; - } - - internal static int txcallCount; - internal static int txMissCount; - - static bool GameDatabase_GetTexture_Prefix(GameDatabase __instance, string url, bool asNormalMap, out Texture2D __result) - { - txcallCount++; - if (url == null) - { - __result = null; - return false; - } - - if (__instance.flagSwaps.TryGetValue(url, out string newUrl)) - url = newUrl; - - if (!texturesByUrl.TryGetValue(url, out TextureInfo textureInfo)) - { - for (int i = __instance.databaseTexture.Count; i-- > 0;) - { - if (string.Equals(url, __instance.databaseTexture[i].name, StringComparison.OrdinalIgnoreCase)) - { - - textureInfo = __instance.databaseTexture[i]; - texturesByUrl.Add(url, textureInfo); - txMissCount++; - break; - } - } - } - - if (textureInfo != null) - __result = asNormalMap ? textureInfo.normalMap : textureInfo.texture; - else - __result = null; - - return false; - } - - #endregion } #if DEBUG diff --git a/KSPCommunityFixes/Performance/GameDatabasePerf.cs b/KSPCommunityFixes/Performance/GameDatabasePerf.cs new file mode 100644 index 00000000..abdb7597 --- /dev/null +++ b/KSPCommunityFixes/Performance/GameDatabasePerf.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using static GameDatabase; +using static UrlDir; + +namespace KSPCommunityFixes.Performance +{ + // see https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/269 + // this patch is applied manually on launch from the FastLoader patch. + // It has no entry in settings.cfg and can't be disabled. + [ManualPatch] + internal class GameDatabasePerf : BasePatch + { + protected override bool IgnoreConfig => true; + + protected override void ApplyPatches() + { + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetModelPrefab)); + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetModelPrefabIn)); + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetModel)); + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetModelIn)); + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetModelFile), new[] { typeof(GameObject) }); + // we don't patch the GetModelFile(string) variant as it would require an additional dictionary, + // is unused in stock and very unlikely to ever be used by anyone. + + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetTextureInfo)); + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetTextureInfoIn)); + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetTexture)); + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetTextureIn)); + // For the same reasons, we don't patch GetTextureFile(string) + } + + static GameObject GameDatabase_GetModelPrefab_Override(GameDatabase gdb, string url) + { + if (url == null) + return null; + + if (!KSPCFFastLoader.modelsByUrl.TryGetValue(url, out GameObject result)) + { + // We need a fallback because models are also added from asset bundles, + // and because anyone could be adding models as the databaseModel list is public + List models = gdb.databaseModel; + for (int i = models.Count; i-- > 0;) + { + if (models[i].name == url) + { + result = models[i]; + KSPCFFastLoader.modelsByUrl.Add(url, result); + break; + } + } + } + + return result; + } + + static GameObject GameDatabase_GetModelPrefabIn_Override(GameDatabase gdb, string url) + { + if (url == null) + return null; + + if (!KSPCFFastLoader.modelsByDirectoryUrl.TryGetValue(url, out GameObject result)) + { + // We need a fallback because models are also added from asset bundles, + // and because anyone could be adding models as the databaseModel list is public + List models = gdb.databaseModel; + for (int i = models.Count; i-- > 0;) + { + string modelName = models[i].name; + if (modelName.Substring(0, modelName.LastIndexOf('/')) == url) + { + result = models[i]; + KSPCFFastLoader.modelsByDirectoryUrl.Add(url, result); + break; + } + } + } + + return result; + } + + static GameObject GameDatabase_GetModel_Override(GameDatabase gdb, string url) + { + GameObject prefab = GameDatabase_GetModelPrefab_Override(gdb, url); + if (prefab.IsNullRef()) + return null; + + return UnityEngine.Object.Instantiate(prefab); + } + + static GameObject GameDatabase_GetModelIn_Override(GameDatabase gdb, string url) + { + GameObject prefab = GameDatabase_GetModelPrefabIn_Override(gdb, url); + if (prefab.IsNullRef()) + return null; + + return UnityEngine.Object.Instantiate(prefab); + } + + static UrlFile GameDatabase_GetModelFile_Override(GameDatabase gdb, GameObject modelPrefab) + { + if (modelPrefab.IsNullRef()) + return null; + + if (!KSPCFFastLoader.urlFilesByModel.TryGetValue(modelPrefab, out UrlFile result)) + { + // We need a fallback because models are also added from asset bundles, + // and because anyone could be adding models as the databaseModel list is public + List models = gdb.databaseModel; + for (int i = models.Count; i-- > 0;) + { + if (models[i] == modelPrefab) + { + result = gdb.databaseModelFiles[i]; + KSPCFFastLoader.urlFilesByModel.Add(modelPrefab, result); + break; + } + } + } + + return result; + } + + internal static int txcallCount; + internal static int txMissCount; + + static TextureInfo GameDatabase_GetTextureInfo_Override(GameDatabase gdb, string url) + { + txcallCount++; + if (url == null) + return null; + + if (gdb.flagSwaps.TryGetValue(url, out string newUrl)) + url = newUrl; + + if (!KSPCFFastLoader.texturesByUrl.TryGetValue(url, out TextureInfo result)) + { + for (int i = gdb.databaseTexture.Count; i-- > 0;) + { + if (string.Equals(url, gdb.databaseTexture[i].name, StringComparison.OrdinalIgnoreCase)) + { + result = gdb.databaseTexture[i]; + KSPCFFastLoader.texturesByUrl.Add(url, result); + txMissCount++; + break; + } + } + } + return result; + } + + static TextureInfo GameDatabase_GetTextureInfoIn_Override(GameDatabase gdb, string url, string textureName) + { + txcallCount++; + if (url == null || textureName == null) + return null; + + url = url.Substring(0, url.LastIndexOf('/') + 1) + textureName; + + if (gdb.flagSwaps.TryGetValue(url, out string newUrl)) + url = newUrl; + + if (!KSPCFFastLoader.texturesByUrl.TryGetValue(url, out TextureInfo result)) + { + for (int i = gdb.databaseTexture.Count; i-- > 0;) + { + if (string.Equals(url, gdb.databaseTexture[i].name, StringComparison.OrdinalIgnoreCase)) + { + result = gdb.databaseTexture[i]; + KSPCFFastLoader.texturesByUrl.Add(url, result); + txMissCount++; + break; + } + } + } + + return result; + } + + static Texture2D GameDatabase_GetTexture_Override(GameDatabase gdb, string url, bool asNormalMap) + { + txcallCount++; + if (url == null) + return null; + + if (gdb.flagSwaps.TryGetValue(url, out string newUrl)) + url = newUrl; + + if (!KSPCFFastLoader.texturesByUrl.TryGetValue(url, out TextureInfo textureInfo)) + { + for (int i = gdb.databaseTexture.Count; i-- > 0;) + { + if (string.Equals(url, gdb.databaseTexture[i].name, StringComparison.OrdinalIgnoreCase)) + { + textureInfo = gdb.databaseTexture[i]; + KSPCFFastLoader.texturesByUrl.Add(url, textureInfo); + txMissCount++; + break; + } + } + } + + if (textureInfo == null) + return null; + + return asNormalMap ? textureInfo.normalMap : textureInfo.texture; + } + + static Texture2D GameDatabase_GetTextureIn_Override(GameDatabase gdb, string url, string textureName, bool asNormalMap) + { + txcallCount++; + if (url == null || textureName == null) + return null; + + url = url.Substring(0, url.LastIndexOf('/') + 1) + textureName; + + if (gdb.flagSwaps.TryGetValue(url, out string newUrl)) + url = newUrl; + + if (!KSPCFFastLoader.texturesByUrl.TryGetValue(url, out TextureInfo textureInfo)) + { + for (int i = gdb.databaseTexture.Count; i-- > 0;) + { + if (string.Equals(url, gdb.databaseTexture[i].name, StringComparison.OrdinalIgnoreCase)) + { + textureInfo = gdb.databaseTexture[i]; + KSPCFFastLoader.texturesByUrl.Add(url, textureInfo); + txMissCount++; + break; + } + } + } + + if (textureInfo == null) + return null; + + return asNormalMap ? textureInfo.normalMap : textureInfo.texture; + } + } +}