Files
RobustToolbox/Robust.Client/ResourceManagement/ResourceCache.Preload.cs
Pieter-Jan Briers ae1051e813 Cache non-existence of ResourceCache TryGetResource.
Many patterns (both in engine and content) make use of regular TryGetResource returning null. The problem is that if the resource doesn't exist, it won't be cached and the code attempts to load it from disk *every single time*.

For example, opening an inventory in SS14 would hang the client for ages on some UI themes due to the UITheme texture fallback system constantly trying to load a texture that doesn't exist.
2024-06-19 22:50:09 +02:00

289 lines
10 KiB
C#

using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.Audio;
using Robust.Client.Graphics;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace Robust.Client.ResourceManagement
{
internal partial class ResourceCache
{
[field: Dependency] public IClyde Clyde { get; } = default!;
[field: Dependency] public IAudioInternal ClydeAudio { get; } = default!;
[Dependency] private readonly IResourceManager _manager = default!;
[field: Dependency] public IFontManager FontManager { get; } = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
public void PreloadTextures()
{
var sawmill = _logManager.GetSawmill("res.preload");
if (!_configurationManager.GetCVar(CVars.ResTexturePreloadingEnabled))
{
sawmill.Debug($"Skipping texture preloading due to CVar value.");
return;
}
PreloadTextures(sawmill);
PreloadRsis(sawmill);
}
private void PreloadTextures(ISawmill sawmill)
{
sawmill.Debug("Preloading textures...");
var sw = Stopwatch.StartNew();
var resList = GetTypeData<TextureResource>().Resources;
var texList = _manager.ContentFindFiles("/Textures/")
// Skip PNG files inside RSIs.
.Where(p => p.Extension == "png" && !p.ToString().Contains(".rsi/") && !resList.ContainsKey(p))
.Select(p => new TextureResource.LoadStepData {Path = p})
.ToArray();
Parallel.ForEach(texList, data =>
{
try
{
TextureResource.LoadPreTexture(_manager, data);
}
catch (Exception e)
{
// Mark failed loads as bad and skip them in the next few stages.
// Avoids any silly array resizing or similar.
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
data.Bad = true;
}
});
foreach (var data in texList)
{
if (data.Bad)
continue;
try
{
TextureResource.LoadTexture(Clyde, data);
}
catch (Exception e)
{
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
data.Bad = true;
}
}
var errors = 0;
foreach (var data in texList)
{
if (data.Bad)
{
errors += 1;
continue;
}
try
{
var texResource = new TextureResource();
texResource.LoadFinish(this, data);
resList[data.Path] = texResource;
}
catch (Exception e)
{
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
data.Bad = true;
errors += 1;
}
}
sawmill.Debug(
"Preloaded {CountLoaded} textures ({CountErrored} errored) in {LoadTime}",
texList.Length,
errors,
sw.Elapsed);
}
private void PreloadRsis(ISawmill sawmill)
{
var sw = Stopwatch.StartNew();
var resList = GetTypeData<RSIResource>().Resources;
var rsiList = _manager.ContentFindFiles("/Textures/")
.Where(p => p.ToString().EndsWith(".rsi/meta.json"))
.Select(c => c.Directory)
.Where(p => !resList.ContainsKey(p))
.Select(p => new RSIResource.LoadStepData {Path = p})
.ToArray();
Parallel.ForEach(rsiList, data =>
{
try
{
RSIResource.LoadPreTexture(_manager, data);
}
catch (Exception e)
{
// Mark failed loads as bad and skip them in the next few stages.
// Avoids any silly array resizing or similar.
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
data.Bad = true;
}
});
// Do not meta-atlas RSIs with custom load parameters.
var atlasList = rsiList.Where(x => x.LoadParameters == TextureLoadParameters.Default).ToArray();
var nonAtlasList = rsiList.Where(x => x.LoadParameters != TextureLoadParameters.Default).ToArray();
foreach (var data in nonAtlasList)
{
if (data.Bad)
continue;
try
{
RSIResource.LoadTexture(Clyde, data);
}
catch (Exception e)
{
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
data.Bad = true;
}
}
// This combines individual RSI atlases into larger atlases to reduce draw batches. currently this is a VERY
// lazy bundling and is not at all compact, its basically an atlas of RSI atlases. Really what this should
// try to do is to have each RSI write directly to the atlas, rather than having each RSI write to its own
// sub-atlas first.
//
// Also if the max texture size is too small, such that there needs to be more than one atlas, then each
// atlas should somehow try to group things by draw-depth & frequency to minimize batches? But currently
// everything fits onto a single 8k x 8k image so as long as the computer can manage that, it should be
// fine.
// TODO allow RSIs to opt out (useful for very big & rare RSIs)
// TODO combine with (non-rsi) texture atlas?
Array.Sort(atlasList, (b, a) => (b.AtlasSheet?.Height ?? 0).CompareTo(a.AtlasSheet?.Height ?? 0));
// Each RSI sub atlas has a different size.
// Even if we iterate through them once to estimate total area, I have NFI how to sanely estimate an optimal square-texture size.
// So fuck it, just default to letting it be as large as it needs to and crop it as needed?
var maxSize = Math.Min(GL.GetInteger(GetPName.MaxTextureSize), _configurationManager.GetCVar(CVars.ResRSIAtlasSize));
var sheet = new Image<Rgba32>(maxSize, maxSize);
var deltaY = 0;
Vector2i offset = default;
int finalized = -1;
int atlasCount = 0;
for (int i = 0; i < atlasList.Length; i++)
{
var rsi = atlasList[i];
if (rsi.Bad)
continue;
DebugTools.Assert(rsi.AtlasSheet.Width < sheet.Width);
DebugTools.Assert(rsi.AtlasSheet.Height < sheet.Height);
if (offset.X + rsi.AtlasSheet.Width > sheet.Width)
{
offset.X = 0;
offset.Y += deltaY;
}
if (offset.Y + rsi.AtlasSheet.Height > sheet.Height)
{
FinalizeMetaAtlas(i-1, sheet);
sheet = new Image<Rgba32>(maxSize, maxSize);
deltaY = 0;
offset = default;
}
deltaY = Math.Max(deltaY, rsi.AtlasSheet.Height);
var box = new UIBox2i(0, 0, rsi.AtlasSheet.Width, rsi.AtlasSheet.Height);
rsi.AtlasSheet.Blit(box, sheet, offset);
rsi.AtlasOffset = offset;
offset.X += rsi.AtlasSheet.Width;
}
var height = offset.Y + deltaY;
var croppedSheet = new Image<Rgba32>(maxSize, height);
sheet.Blit(new UIBox2i(0, 0, maxSize, height), croppedSheet, default);
FinalizeMetaAtlas(atlasList.Length - 1, croppedSheet);
void FinalizeMetaAtlas(int toIndex, Image<Rgba32> sheet)
{
var atlas = Clyde.LoadTextureFromImage(sheet);
for (int i = finalized + 1; i <= toIndex; i++)
{
var rsi = atlasList[i];
rsi.AtlasTexture = atlas;
}
finalized = toIndex;
atlasCount++;
}
Parallel.ForEach(rsiList, data =>
{
if (data.Bad)
return;
try
{
RSIResource.LoadPostTexture(data);
}
catch (Exception e)
{
data.Bad = true;
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
}
});
var errors = 0;
foreach (var data in rsiList)
{
if (data.Bad)
{
errors += 1;
continue;
}
try
{
var rsiRes = new RSIResource();
rsiRes.LoadFinish(this, data);
resList[data.Path] = rsiRes;
}
catch (Exception e)
{
sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}");
data.Bad = true;
errors += 1;
}
}
sawmill.Debug(
"Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountNotAtlas} not atlassed, {CountErrored} errored) in {LoadTime}",
rsiList.Length,
atlasCount,
nonAtlasList.Length,
errors,
sw.Elapsed);
}
}
}