Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions GameData/KSPCommunityFixes/Localization/en-us.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ Localization
#KSPCF_KSPCFFastLoader_PopupL4 = You can change this setting later in the in-game settings menu
#KSPCF_KSPCFFastLoader_PopupL5 = Do you want to enable this optimization ?

// TextureStreaming

#KSPCF_TextureStreaming_StreamingEnabledTitle = Texture streaming
#KSPCF_TextureStreaming_StreamingEnabledTooltip = Stream part textures from files on disk as they are needed. Ensures VRAM usage for part and IVA textures stays within the budget that you set.
#KSPCF_TextureStreaming_StreamingBudgetTitle = Streaming memory budget (MB)
#KSPCF_TextureStreaming_StreamingBudgetTooltip = The amount of memory that is allocated to store streaming textures. Unity will automatically load and unload parts of textures to stay under this limit.
#KSPCF_TextureStreaming_F_StreamingBudgetValue = <<1>> MB

// LowerMinPhysicsDTPerFrame

#KSPCF_LowerMinPhysicsDTPerFrame_SettingsTooltip = How the game handle lag in CPU bound situations.\nMostly relevant with large part count vessels.\n\nLower value :\nHigher and smoother FPS, but game time might advance slower than real time.\n\nHigher value :\nLower and choppier FPS, but game time will advance closer to real time.
Expand Down
4 changes: 4 additions & 0 deletions GameData/KSPCommunityFixes/Settings.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,10 @@ KSP_COMMUNITY_FIXES
// completes. Significantly reduces stutter when a ship with cargo bays crashes.
CargoBayPerf = true

// Stream part and IVA textures from disk on demand using Unity's mipmap streaming, keeping their
// VRAM footprint within a configurable budget. Can be toggled and tuned from the KSPCF in-game settings.
TextureStreaming = true

// ##########################
// Modding
// ##########################
Expand Down
63 changes: 59 additions & 4 deletions KSPCommunityFixes/Internal/PatchSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class PatchSettings : BasePatch
private static AltimeterHorizontalPosition altimeterPatch;
private static DisableManeuverTool maneuverToolPatch;
private static OptionalMakingHistoryDLCFeatures disableMHPatch;
private static TextureStreaming textureStreamingPatch;

protected override void ApplyPatches()
{
Expand All @@ -38,6 +39,10 @@ protected override void ApplyPatches()
if (disableMHPatch != null)
entryCount++;

textureStreamingPatch = KSPCommunityFixes.GetPatchInstance<TextureStreaming>();
if (textureStreamingPatch != null)
entryCount += 2;

// NoIVA is always enabled
entryCount++;
}
Expand All @@ -49,8 +54,10 @@ static void GameplaySettingsScreen_DrawMiniSettings_Postfix(ref DialogGUIBase[]

int count = __result.Length;

// +1 for the KSPCF title box; entryCount already accounts for every added row (the streaming
// patch contributes 2, see ApplyPatches).
DialogGUIBase[] modifiedResult = new DialogGUIBase[count + entryCount + 1];

for (int i = 0; i < count; i++)
modifiedResult[i] = __result[i];

Expand All @@ -76,7 +83,7 @@ static void GameplaySettingsScreen_DrawMiniSettings_Postfix(ref DialogGUIBase[]
if (maneuverToolPatch != null)
{
DialogGUIToggle toggle = new DialogGUIToggle(DisableManeuverTool.enableManeuverTool,
() => (!DisableManeuverTool.enableManeuverTool)
() => (!DisableManeuverTool.enableManeuverTool)
? Localizer.Format("#autoLOC_6001071") //"Disabled"
: Localizer.Format("#autoLOC_6001072"), //"Enabled"
DisableManeuverTool.OnToggleApp, 150f);
Expand All @@ -91,7 +98,7 @@ static void GameplaySettingsScreen_DrawMiniSettings_Postfix(ref DialogGUIBase[]

if (altimeterPatch != null)
{
DialogGUISlider slider = new DialogGUISlider(() => AltimeterHorizontalPosition.altimeterPosition, 0f, 1f, wholeNumbers: false, 200f, 20f, delegate(float f)
DialogGUISlider slider = new DialogGUISlider(() => AltimeterHorizontalPosition.altimeterPosition, 0f, 1f, wholeNumbers: false, 200f, 20f, delegate (float f)
{
AltimeterHorizontalPosition.altimeterPosition = f;
AltimeterHorizontalPosition.SetTopFramePosition();
Expand All @@ -112,6 +119,46 @@ static void GameplaySettingsScreen_DrawMiniSettings_Postfix(ref DialogGUIBase[]
new DialogGUILabel(NoIVA.LOC_SettingsTitle, 150f), noIVAslider, valueLabel, new DialogGUIFlexibleSpace());
count++;

if (textureStreamingPatch != null)
{
DialogGUIToggle streamingToggle = new(
TextureStreaming.MipmapStreamingEnabled,
() => (!TextureStreaming.MipmapStreamingEnabled)
? Localizer.Format("#autoLOC_6001071") //"Disabled"
: Localizer.Format("#autoLOC_6001072"), //"Enabled"
b => TextureStreaming.MipmapStreamingEnabled = b, 150f);
streamingToggle.tooltipText = TextureStreaming.LOC_StreamingEnabledTooltip;

modifiedResult[count] = new DialogGUIHorizontalLayout(
TextAnchor.MiddleLeft,
new DialogGUILabel(TextureStreaming.LOC_StreamingEnabledTitle, 150f),
streamingToggle,
new DialogGUIFlexibleSpace());
count++;

// Streaming memory budget, in MB (0 .. total VRAM). Only interactable while streaming is on.
float budgetMax = Math.Max(1024, SystemInfo.graphicsMemorySize);
DialogGUISlider budgetSlider = new(
() => TextureStreaming.MipmapStreamingBudgetMb,
0f,
budgetMax,
wholeNumbers: true,
128f,
20f,
budget => TextureStreaming.MipmapStreamingBudgetMb = (int)budget);
budgetSlider.tooltipText = TextureStreaming.LOC_StreamingBudgetTooltip;
budgetSlider.OptionInteractableCondition = () => TextureStreaming.MipmapStreamingEnabled;
DialogGUILabel budgetValue = new(() => Localizer.Format(TextureStreaming.LOC_F_StreamingBudgetValue, TextureStreaming.MipmapStreamingBudgetMb));

modifiedResult[count] = new DialogGUIHorizontalLayout(
TextAnchor.MiddleLeft,
new DialogGUILabel(TextureStreaming.LOC_StreamingBudgetTitle, 150f),
budgetSlider,
budgetValue,
new DialogGUIFlexibleSpace());
count++;
}

__result = modifiedResult;
}

Expand All @@ -133,12 +180,20 @@ static void GameplaySettingsScreen_ApplySettings_Postfix()

if (altimeterPatch != null)
{
ConfigNode node = new ConfigNode();
ConfigNode node = new();
node.AddValue(nameof(AltimeterHorizontalPosition.altimeterPosition), AltimeterHorizontalPosition.altimeterPosition);
SaveData<AltimeterHorizontalPosition>(node);
}

NoIVA.SaveSettings();

if (textureStreamingPatch != null)
{
ConfigNode streamingNode = new();
streamingNode.AddValue(nameof(TextureStreaming.MipmapStreamingEnabled), TextureStreaming.MipmapStreamingEnabled);
streamingNode.AddValue(nameof(TextureStreaming.MipmapStreamingBudgetMb), TextureStreaming.MipmapStreamingBudgetMb);
SaveData<TextureStreaming>(streamingNode);
}
}
}
}
4 changes: 4 additions & 0 deletions KSPCommunityFixes/Library/Model/ModelInstructions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using KSPCommunityFixes.Library;
using KSPCommunityFixes.Performance;
using PartToolsLib;
using UnityEngine;
using UnityEngine.Rendering;
Expand Down Expand Up @@ -167,7 +168,10 @@ public void Execute(UnityEngine.Object[] locals)
if (tex.IsNullOrDestroyed())
Debug.LogError($"Texture '{t.Url}' not found!");
else
{
mat.SetTexture(t.Name, tex);
KSPCFFastLoader.ReleaseToStreaming(tex);
}
}
}
}
Expand Down
30 changes: 12 additions & 18 deletions KSPCommunityFixes/Library/TextureBundle/TextureBundleBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace KSPCommunityFixes.Library.TextureBundle
{
/// <summary>
/// Builds a minimal UnityFS bundle wrapping a single streamed <c>Texture2D</c> and the
/// <c>AssetBundle</c> that references it, where the texture's pixel data lives in an existing
/// DDS file on disk. The generated bundle carries only ~1&#160;KB of metadata: the texture's
/// <c>m_StreamData.path</c> is the absolute path of the DDS file and <c>m_StreamData.offset</c>
/// is its data offset, so Unity opens the file and streams the compressed pixels itself. Pure
/// CPU work; safe to call from a background thread.
/// </summary>
/// <remarks>
/// The whole prefix is written into a single <see cref="BundleBufferWriter"/>: the UnityFS
/// framing (<see cref="BundleWriter"/>), the serialized-file framing
/// (<see cref="SerializedFileWriter"/>) and the two object bodies written here by hand. The
/// bodies reproduce the exact field order, sizes and alignment padding of Unity 2019.4's own
/// layout — the same layout the embedded type tree encodes.
///
/// <para>Borrowed from KSPTextureLoader
/// (../KSPTextureLoader/src/KSPTextureLoader/Format/Bundle/TextureBundleBuilder.cs), stripped
/// to the classic Texture2D + external-file path.</para>
/// A builder for UnityFS bundles that contain <see cref="Texture2D" />s.
/// The special bit is that these bundles refer to the actual contents of
/// the dds files on disk instead of storing them directly.
/// </remarks>
internal static class TextureBundleBuilder
{
Expand Down Expand Up @@ -67,6 +54,8 @@ public sealed class TextureRequest

/// <summary>Whether Unity should keep a CPU-side copy of the pixels.</summary>
public bool Readable;

public bool StreamingMipmaps;
}

/// <summary>The built bundle prefix plus the name to request from it.</summary>
Expand Down Expand Up @@ -104,6 +93,8 @@ public readonly struct TextureEntry

public readonly bool Readable;

public readonly bool StreamingMipmaps;

/// <summary>Absolute path of the DDS file the pixels are streamed from.</summary>
public readonly string ExternalPath;

Expand All @@ -121,6 +112,7 @@ public TextureEntry(
int format,
int colorSpace,
bool readable,
bool streamingMipmaps,
string externalPath,
long externalOffset,
long pixelsLength)
Expand All @@ -132,6 +124,7 @@ public TextureEntry(
Format = format;
ColorSpace = colorSpace;
Readable = readable;
StreamingMipmaps = streamingMipmaps;
ExternalPath = externalPath;
ExternalOffset = externalOffset;
PixelsLength = pixelsLength;
Expand Down Expand Up @@ -271,6 +264,7 @@ public static byte[] BuildMany(
Format = e.Format,
ColorSpace = e.ColorSpace,
Readable = e.Readable,
StreamingMipmaps = e.StreamingMipmaps,
};

file.BeginObject(w, ref slots[i + 1]);
Expand Down Expand Up @@ -331,7 +325,7 @@ long streamSize
w.WriteBool(req.Readable); // m_IsReadable
w.WriteBool(false); // m_IgnoreMasterTextureLimit
w.WriteBool(false); // m_IsPreProcessed
w.WriteBool(false); // m_StreamingMipmaps
w.WriteBool(req.StreamingMipmaps); // m_StreamingMipmaps
w.Align(4);
w.WriteInt32(0); // m_StreamingMipmapsPriority
w.Align(4);
Expand Down
75 changes: 73 additions & 2 deletions KSPCommunityFixes/Performance/FastLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,17 @@ private void Awake()
assetAndPartLoaderHarmony.Patch(m_PartLoader_StartLoad, null, null, new HarmonyMethod(t_PartLoader_StartLoad));

PatchStartCoroutineInCoroutine(AccessTools.Method(typeof(PartLoader), nameof(PartLoader.CompileParts)));

// These enable texture streaming for any texture used in a model,
// for a part, or for an IVA.
MethodInfo m_PartLoader_ReplaceTextures = AccessTools.Method(typeof(PartLoader), "ReplaceTextures");
MethodInfo po_PartLoader_ReplaceTextures = AccessTools.Method(typeof(KSPCFFastLoader), nameof(PartLoader_ReplaceTextures_Postfix));
assetAndPartLoaderHarmony.Patch(m_PartLoader_ReplaceTextures, null, new HarmonyMethod(po_PartLoader_ReplaceTextures));

MethodInfo m_PartLoader_ReplacePartTexture = AccessTools.Method(typeof(PartLoader), "ReplacePartTexture");
MethodInfo po_PartLoader_ReplacePartTexture = AccessTools.Method(typeof(KSPCFFastLoader), nameof(PartLoader_ReplacePartTexture_Postfix));
assetAndPartLoaderHarmony.Patch(m_PartLoader_ReplacePartTexture, null, new HarmonyMethod(po_PartLoader_ReplacePartTexture));

PatchStartCoroutineInCoroutine(AccessTools.Method(typeof(DragCubeSystem), nameof(DragCubeSystem.SetupDragCubeCoroutine), new[] { typeof(Part) }));
PatchStartCoroutineInCoroutine(AccessTools.Method(typeof(DragCubeSystem), nameof(DragCubeSystem.RenderDragCubesCoroutine)));

Expand Down Expand Up @@ -1149,6 +1160,7 @@ private static BundleClassification ClassifyBundleRequest(TextureLoadRequest req
req.File.url,
hdr.Width, hdr.Height, hdr.MipCount,
hdr.ClassicTextureFormat, hdr.ColorSpace, readable: false,
EligibleForStreaming(hdr),
sourcePath, hdr.DataOffset, hdr.StreamedSize);
return new BundleClassification(entry, new BundleItem { Request = req, IsNormalMap = isNormalMap });
}
Expand Down Expand Up @@ -1269,6 +1281,11 @@ private static IEnumerator InsertBundledTextures(
{
req.Result = new TextureInfo(req.File, tex, item.IsNormalMap, isReadable: false, isCompressed: true);
req.Status = TextureLoadRequest.State.Ready;

// Disable mipmap streaming if enabled. We'll re-enable it on a case-by-case
// basis when textures are used in parts or models.
if (tex.streamingMipmaps)
tex.requestedMipmapLevel = 0;
}
else
{
Expand All @@ -1286,6 +1303,41 @@ private static IEnumerator InsertBundledTextures(
}
#endregion

#region Mipmap streaming

/// <summary>
/// Enable mipmpa streaming for <paramref name="tex"/>.
/// </summary>
/// <param name="tex"></param>
internal static void ReleaseToStreaming(Texture tex)
{
if (tex is Texture2D t2d && t2d.streamingMipmaps)
t2d.ClearRequestedMipmapLevel();
}

private static void PartLoader_ReplaceTextures_Postfix(List<TextureInfo> newTextures)
{
if (newTextures == null)
return;

for (int i = 0; i < newTextures.Count; i++)
{
TextureInfo ti = newTextures[i];
if (ti == null)
continue;
ReleaseToStreaming(ti.texture);
ReleaseToStreaming(ti.normalMap);
}
}

private static void PartLoader_ReplacePartTexture_Postfix(UrlConfig urlConfig, string textureName, bool normalMap)
{
string url = urlConfig.parent.parent.url + "/textures/" + Path.GetFileNameWithoutExtension(textureName);
ReleaseToStreaming(GameDatabase.Instance.GetTexture(url, normalMap));
}

#endregion

#region Model bundle loader

private readonly struct QueueWriteGuard<T>(BlockingCollection<T> queue) : IDisposable
Expand Down Expand Up @@ -1472,6 +1524,8 @@ AssetBundleCreateRequest bundleRequest
yield break;
}

group.Bundle = bundle;

var request = bundle.LoadAllAssetsAsync();
request.priority = -100;
yield return request;
Expand Down Expand Up @@ -2404,6 +2458,19 @@ private static bool IsBlockAligned(GraphicsFormat format, int width, int height)
return width % blockWidth == 0 && height % blockHeight == 0;
}

// Textures smaller than 4KB are too small to be worth streaming.
private const long MinStreamingBytes = 4 * 1024;

// Attempting to load a compressed mipmap whose size is not a multiple of the
// DDS block size (4x4) causes unity to crash. To avoid this we need to
// only include textures whose mipmaps are the correct size.
private static bool EligibleForStreaming(in DDSPreparedHeader hdr) =>
hdr.StreamedSize >= MinStreamingBytes
&& IsBlockAligned(
hdr.Format,
Math.Max(1, hdr.Width >> TextureStreaming.MaxStreamingMipReduction),
Math.Max(1, hdr.Height >> TextureStreaming.MaxStreamingMipReduction));

// The number of mip levels Unity allocates for a full mip chain.
private static int ComputeMipCount(int width, int height)
{
Expand Down Expand Up @@ -3718,10 +3785,14 @@ private static void SetOptIn(bool optIn, ref bool? choosed)
loader.userOptInChoiceDone = true;
textureCacheEnabled = optIn;
choosed = true;
SaveConfig();
}

private static void SaveConfig()
{
ConfigNode config = new ConfigNode();
config.AddValue(nameof(userOptInChoiceDone), true);
config.AddValue(nameof(textureCacheEnabled), optIn);
config.AddValue(nameof(userOptInChoiceDone), loader.IsNotNullOrDestroyed() && loader.userOptInChoiceDone);
config.AddValue(nameof(textureCacheEnabled), textureCacheEnabled);

string pluginDataPath = Path.Combine(ModPath, "PluginData");
if (!Directory.Exists(pluginDataPath))
Expand Down
Loading