diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg index d5ceb7d..5ba87e3 100644 --- a/GameData/KSPCommunityFixes/Settings.cfg +++ b/GameData/KSPCommunityFixes/Settings.cfg @@ -4,6 +4,9 @@ KSP_COMMUNITY_FIXES { + OnDemandPartTextures = true + + // ########################## // Major bugfixes // ########################## diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index 5ccec49..84d10ec 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -170,6 +170,7 @@ + diff --git a/KSPCommunityFixes/Performance/FastLoader.cs b/KSPCommunityFixes/Performance/FastLoader.cs index f26b41a..6563d0c 100644 --- a/KSPCommunityFixes/Performance/FastLoader.cs +++ b/KSPCommunityFixes/Performance/FastLoader.cs @@ -303,6 +303,8 @@ static IEnumerator FastAssetLoader(List configFileTypes) gdb.progressTitle = "Searching assets to load..."; yield return null; + OnDemandPartTextures.GetTextures(out HashSet allPartTextures); + double nextFrameTime = ElapsedTime + minFrameTimeD; // Files loaded by our custom loaders @@ -350,26 +352,29 @@ static IEnumerator FastAssetLoader(List configFileTypes) } break; case FileType.Texture: + + bool isOnDemand = allPartTextures.Contains(file.url); + switch (file.fileExtension) { case "dds": - textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureDDS)); + textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureDDS, isOnDemand)); break; case "jpg": case "jpeg": - textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureJPG)); + textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureJPG, isOnDemand)); break; case "mbm": - textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureMBM)); + textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureMBM, isOnDemand)); break; case "png": - textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TexturePNG)); + textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TexturePNG, isOnDemand)); break; case "tga": - textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureTGA)); + textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureTGA, isOnDemand)); break; case "truecolor": - textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureTRUECOLOR)); + textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureTRUECOLOR, isOnDemand)); break; default: unsupportedTextureFiles.Add(file); @@ -802,6 +807,12 @@ static void ReadAssetsThread(List files, Deque buffer) { foreach (RawAsset rawAsset in files) { + if (rawAsset.IsOnDemand) + { + buffer.AddToFront(rawAsset); + continue; + } + rawAsset.ReadFromDiskWorkerThread(); SpinWait spin = new SpinWait(); @@ -836,7 +847,7 @@ static void ReadAssetsThread(List files, Deque buffer) /// /// Asset wrapper class, actual implementation of the disk reader, individual texture/model formats loaders /// - private class RawAsset + internal class RawAsset { public enum AssetType { @@ -881,18 +892,21 @@ public enum Result private BinaryReader binaryReader; private Result result; private string resultMessage; + private bool isOnDemand; public UrlFile File => file; public Result State => result; public string Message => resultMessage; public int DataLength => dataLength; public string TypeName => assetTypeNames[(int)assetType]; + public bool IsOnDemand => isOnDemand; - public RawAsset(UrlFile file, AssetType assetType) + public RawAsset(UrlFile file, AssetType assetType, bool isOnDemand = false) { this.result = Result.Valid; this.file = file; this.assetType = assetType; + this.isOnDemand = isOnDemand; } private void SetError(string message) @@ -1002,35 +1016,49 @@ public void LoadAndDisposeMainThread() if (file.fileType == FileType.Texture) { TextureInfo textureInfo; - switch (assetType) + + if (isOnDemand) { - case AssetType.TextureDDS: - textureInfo = LoadDDS(); - break; - case AssetType.TextureJPG: - textureInfo = LoadJPG(); - break; - case AssetType.TextureMBM: - textureInfo = LoadMBM(); - break; - case AssetType.TexturePNG: - textureInfo = LoadPNG(); - break; - case AssetType.TexturePNGCached: - textureInfo = LoadPNGCached(); - break; - case AssetType.TextureTGA: - textureInfo = LoadTGA(); - break; - case AssetType.TextureTRUECOLOR: - textureInfo = LoadTRUECOLOR(); - break; - default: - SetError("Unknown texture format"); - return; + textureInfo = new OnDemandTextureInfo(file, assetType); } + else + { + switch (assetType) + { + case AssetType.TextureDDS: + textureInfo = LoadDDS(); + break; + case AssetType.TextureJPG: + textureInfo = LoadJPG(); + break; + case AssetType.TextureMBM: + textureInfo = LoadMBM(); + break; + case AssetType.TexturePNG: + textureInfo = LoadPNG(); + break; + case AssetType.TexturePNGCached: + textureInfo = LoadPNGCached(); + break; + case AssetType.TextureTGA: + textureInfo = LoadTGA(); + break; + case AssetType.TextureTRUECOLOR: + textureInfo = LoadTRUECOLOR(); + break; + default: + SetError("Unknown texture format"); + return; + } + + if (textureInfo.texture.IsNullOrDestroyed()) + result = Result.Failed; - if (result == Result.Failed || textureInfo == null || textureInfo.texture.IsNullOrDestroyed()) + textureInfo.texture.name = file.url; + textureInfo.name = file.url; + } + + if (result == Result.Failed || textureInfo == null) { result = Result.Failed; if (string.IsNullOrEmpty(resultMessage)) @@ -1038,8 +1066,6 @@ public void LoadAndDisposeMainThread() } else { - textureInfo.name = file.url; - textureInfo.texture.name = file.url; Instance.databaseTexture.Add(textureInfo); } } diff --git a/KSPCommunityFixes/Performance/OnDemandPartTextures.cs b/KSPCommunityFixes/Performance/OnDemandPartTextures.cs new file mode 100644 index 0000000..884fa8d --- /dev/null +++ b/KSPCommunityFixes/Performance/OnDemandPartTextures.cs @@ -0,0 +1,470 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using HarmonyLib; +using KSP.UI.Screens; +using KSPCommunityFixes.QoL; +using UnityEngine; +using static KSP.UI.Screens.Settings.SettingsSetup; +using static KSPCommunityFixes.Performance.KSPCFFastLoader; +using static KSPCommunityFixes.QoL.NoIVA; +using static ProceduralSpaceObject; + +namespace KSPCommunityFixes.Performance +{ + public class OnDemandPartTextures : BasePatch + { + internal static Dictionary> partsTextures; + internal static Dictionary prefabsData = new Dictionary(); + + protected override void ApplyPatches(List patches) + { + MethodInfo m_Instantiate = null; + foreach (MethodInfo methodInfo in typeof(UnityEngine.Object).GetMethods(BindingFlags.Public | BindingFlags.Static)) + { + if (methodInfo.IsGenericMethod + && methodInfo.Name == nameof(UnityEngine.Object.Instantiate) + && methodInfo.GetParameters().Length == 1) + { + m_Instantiate = methodInfo.MakeGenericMethod(typeof(UnityEngine.Object)); + break; + } + } + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + m_Instantiate, + this, nameof(Instantiate_Prefix))); + + patches.Add(new PatchInfo( + PatchMethodType.Postfix, + AccessTools.Method(typeof(PartLoader), nameof(PartLoader.ParsePart)), + this)); + } + + static void Instantiate_Prefix(UnityEngine.Object original) + { + if (original is Part part + && part.partInfo != null + && part.partInfo.partPrefab.IsNotNullRef() // skip instantiation of the special prefabs (kerbals, flag) during loading + && prefabsData.TryGetValue(part.partInfo, out PrefabData prefabData) + && !prefabData.areTexturesLoaded) + { + // load textures + // set them on the prefab + prefabData.areTexturesLoaded = true; + } + } + + static void PartLoader_ParsePart_Postfix(AvailablePart __result) + { + if (!partsTextures.TryGetValue(__result.name, out List textures)) + return; + + Transform modelTransform = __result.partPrefab.transform.Find("model"); + if (modelTransform == null) + return; + + PrefabData prefabData = null; + + foreach (Renderer renderer in modelTransform.GetComponentsInChildren(true)) + { + if (renderer is ParticleSystemRenderer || renderer.sharedMaterial.IsNullOrDestroyed()) + continue; + + Material material = renderer.sharedMaterial; + MaterialTextures materialTextures = null; + + if (material.HasProperty("_MainTex")) + { + Texture currentTex = material.GetTexture("_MainTex"); + if (currentTex.IsNotNullRef()) + { + string currentTexName = currentTex.name; + if (OnDemandTextureInfo.texturesByUrl.TryGetValue(currentTexName, out OnDemandTextureInfo textureInfo)) + { + if (prefabData == null) + prefabData = new PrefabData(); + + if (materialTextures == null) + materialTextures = new MaterialTextures(material); + + prefabData.materials.Add(materialTextures); + materialTextures.mainTex = textureInfo; + } + + } + } + + if (material.HasProperty("_BumpMap")) + { + Texture currentTex = material.GetTexture("_BumpMap"); + if (currentTex.IsNotNullRef()) + { + string currentTexName = currentTex.name; + if (OnDemandTextureInfo.texturesByUrl.TryGetValue(currentTexName, out OnDemandTextureInfo textureInfo)) + { + if (prefabData == null) + prefabData = new PrefabData(); + + if (materialTextures == null) + materialTextures = new MaterialTextures(material); + + prefabData.materials.Add(materialTextures); + materialTextures.bumpMap = textureInfo; + } + + } + } + + if (material.HasProperty("_Emissive")) + { + Texture currentTex = material.GetTexture("_Emissive"); + if (currentTex.IsNotNullRef()) + { + string currentTexName = currentTex.name; + if (OnDemandTextureInfo.texturesByUrl.TryGetValue(currentTexName, out OnDemandTextureInfo textureInfo)) + { + if (prefabData == null) + prefabData = new PrefabData(); + + if (materialTextures == null) + materialTextures = new MaterialTextures(material); + + prefabData.materials.Add(materialTextures); + materialTextures.emissive = textureInfo; + } + + } + } + + if (material.HasProperty("_SpecMap")) + { + Texture currentTex = material.GetTexture("_SpecMap"); + if (currentTex.IsNotNullRef()) + { + string currentTexName = currentTex.name; + if (OnDemandTextureInfo.texturesByUrl.TryGetValue(currentTexName, out OnDemandTextureInfo textureInfo)) + { + if (prefabData == null) + prefabData = new PrefabData(); + + if (materialTextures == null) + materialTextures = new MaterialTextures(material); + + prefabData.materials.Add(materialTextures); + materialTextures.specMap = textureInfo; + } + + } + } + } + + if (prefabData != null) + prefabsData[__result] = prefabData; + } + + private static readonly char[] textureSplitChars = { ':', ',', ';' }; + + public static void GetTextures(out HashSet allPartTextures) + { + Dictionary> modelParts = new Dictionary>(); + Dictionary> partTextureReplacements = new Dictionary>(); + + foreach (UrlDir.UrlConfig urlConfig in GameDatabase.Instance.root.AllConfigs) + { + if (urlConfig.type != "PART") + continue; + + bool hasModelInModelNode = false; + + string partName = urlConfig.config.GetValue("name"); + if (partName == null) + continue; + + partName = partName.Replace('_', '.'); + + foreach (ConfigNode partNode in urlConfig.config.nodes) + { + if (partNode.name == "MODEL") + { + List texturePaths = null; + foreach (ConfigNode.Value modelValue in partNode.values) + { + if (modelValue.name == "model") + { + hasModelInModelNode = true; + + if (!modelParts.TryGetValue(modelValue.value, out List parts)) + { + parts = new List(); + modelParts[modelValue.value] = parts; + } + + parts.Add(partName); + } + else if (modelValue.name == "texture") + { + string[] array = modelValue.value.Split(textureSplitChars, StringSplitOptions.RemoveEmptyEntries); + if (array.Length != 2) + continue; + + if (texturePaths == null) + texturePaths = new List(); + + texturePaths.Add(new TextureReplacement(array[0].Trim(), array[1].Trim())); + } + } + + if (texturePaths != null) + partTextureReplacements[partName] = texturePaths; + } + } + + if (!hasModelInModelNode) + { + foreach (UrlDir.UrlFile urlFile in urlConfig.parent.parent.files) + { + if (urlFile.fileExtension == "mu") + { + if (!modelParts.TryGetValue(urlFile.url, out List parts)) + { + parts = new List(); + modelParts[urlFile.url] = parts; + } + + parts.Add(partName); + break; // only first model found should be added + } + } + } + } + + partsTextures = new Dictionary>(500); + allPartTextures = new HashSet(500); + + HashSet allTextures = new HashSet(2000); + HashSet allReplacedTextures = new HashSet(500); + List texturePathsBuffer = new List(); + List textureFileNameBuffer = new List(); + + foreach (UrlDir.UrlFile urlFile in GameDatabase.Instance.root.AllFiles) + { + if (urlFile.fileType == UrlDir.FileType.Texture) + { + allTextures.Add(urlFile.url); + continue; + } + + if (urlFile.fileType != UrlDir.FileType.Model) + continue; + + if (!modelParts.TryGetValue(urlFile.url, out List parts)) + continue; + + foreach (string textureFile in MuParser.GetModelTextures(urlFile.fullPath)) + { + string textureFileName = Path.GetFileNameWithoutExtension(textureFile); + textureFileNameBuffer.Add(textureFileName); + texturePathsBuffer.Add(urlFile.parent.url + "/" + textureFileName); + } + + if (texturePathsBuffer.Count == 0) + { + modelParts.Remove(urlFile.url); + continue; + } + + foreach (string part in parts) + { + if (partTextureReplacements.TryGetValue(part, out List textureReplacements)) + { + foreach (TextureReplacement textureReplacement in textureReplacements) + { + for (int i = 0; i < textureFileNameBuffer.Count; i++) + { + if (textureReplacement.textureName == textureFileNameBuffer[i]) + { + allReplacedTextures.Add(texturePathsBuffer[i]); + texturePathsBuffer[i] = textureReplacement.replacementUrl; + } + } + } + } + + if (!partsTextures.TryGetValue(part, out List textures)) + { + textures = new List(texturePathsBuffer); + partsTextures[part] = textures; + } + else + { + textures.AddRange(texturePathsBuffer); + } + } + + texturePathsBuffer.Clear(); + textureFileNameBuffer.Clear(); + } + + List stringBuffer = new List(); + foreach (KeyValuePair> partTextures in partsTextures) + { + for (int i = partTextures.Value.Count; i-- > 0;) + { + string texture = partTextures.Value[i]; + if (!allTextures.Contains(texture)) + { + partTextures.Value.RemoveAt(i); + } + else + { + allPartTextures.Add(texture); + } + } + + if (partTextures.Value.Count == 0) + stringBuffer.Add(partTextures.Key); + } + + for (int i = stringBuffer.Count; i-- > 0;) + partsTextures.Remove(stringBuffer[i]); + + stringBuffer.Clear(); + foreach (string replacedTexture in allReplacedTextures) + if (allPartTextures.Contains(replacedTexture)) + stringBuffer.Add(replacedTexture); + + for (int i = stringBuffer.Count; i-- > 0;) + allReplacedTextures.Remove(stringBuffer[i]); + } + } + + + //[KSPAddon(KSPAddon.Startup.Instantly, true)] + public class OnDemandPartTexturesLoader// : MonoBehaviour + { + + + + + // 1 + // build a > dictionary + // 1.a. + // - Parse part configs + // - find MODEL > model reference + // - find MODEL > texture refernces + // - find ModulePartVariants > TEXTURE references + // - In all found model references : + // - find all texture references + // Patch Part.Awake(), after RelinkPrefab() + // - swap the model textures + + // overview of where textures can come from : + // - defined in the model(s) materials + // - PART > "mesh" : depreciated/unused + // - If no PART > MODEL node : the first found (as ordered in GameDatabase.databaseModel) *.mu model placed in the same directory as the part config (cfg.parent.parent.url) + // - If PART > MODEL node(s) : the model defined in the "model" value + // - not sure if it is possible to set a path to the texture in the model ? I don't think so, but... + // - defined as a texture replacement in the PART > MODEL node + // - the replacement is done on the prefab renderers sharedMaterial + // - as far as I can tell, this require (a potentially dummy) texture with the same name as what is backed in the model to be sitting next to the model in the same directory + + + } + + internal class TextureReplacement + { + public string textureName; + public string replacementUrl; + + public TextureReplacement(string textureName, string replacementUrl) + { + this.textureName = textureName; + this.replacementUrl = replacementUrl; + } + } + + internal class PrefabData + { + public bool areTexturesLoaded; + public List materials; + + public void LoadTextures() + { + + } + } + + internal class MaterialTextures + { + private Material material; + public OnDemandTextureInfo mainTex; + public OnDemandTextureInfo bumpMap; + public OnDemandTextureInfo emissive; + public OnDemandTextureInfo specMap; + + public MaterialTextures(Material material) + { + this.material = material; + } + + public void LoadTextures() + { + if (mainTex != null) + { + mainTex.Load(); + material.SetTexture("_MainTex", mainTex.texture); + } + + if (bumpMap != null) + { + bumpMap.Load(); + material.SetTexture("_BumpMap", bumpMap.texture); + } + + if (emissive != null) + { + emissive.Load(); + material.SetTexture("_Emissive", emissive.texture); + } + + if (specMap != null) + { + specMap.Load(); + material.SetTexture("_SpecMap", specMap.texture); + } + } + } + + internal class OnDemandTextureInfo : GameDatabase.TextureInfo + { + public static Dictionary texturesByUrl = new Dictionary(); + + private Texture2D dummyTexture; + + private bool isLoaded; + private RawAsset.AssetType textureType; + + public OnDemandTextureInfo(UrlDir.UrlFile file, RawAsset.AssetType textureType, bool isNormalMap = false, bool isReadable = false, bool isCompressed = false, Texture2D texture = null) + : base(file, texture, isNormalMap, isReadable, isCompressed) + { + name = file.url; + this.textureType = textureType; + dummyTexture = new Texture2D(1, 1, TextureFormat.ARGB32, false); + dummyTexture.Apply(false, true); + dummyTexture.name = name; + this.texture = dummyTexture; + texturesByUrl.Add(name, this); + } + + public void Load() + { + + } + } +}