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
/// 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;
+ }
+ }
+}