mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-06-09 10:06:34 +02:00
83c2a1be11
* [Dependency] source generator No more reflection, no more codegen at runtime Also various changes to Roslyn helpers to make this easier to write. Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback. No fallback is available for [field: Dependency] properties, due to a Roslyn bug. Code Fixes exist. We love Roslyn * Apply dependencies generator changes to all code * Release notes * Preprocessor got hands * Handle nullable dependencies These are bad but gotta deal with it. * Apply suggestions from code review Co-authored-by: Moony <moony@hellomouse.net> * Fine, let's not use collection expressions --------- Co-authored-by: Moony <moony@hellomouse.net>
431 lines
16 KiB
C#
431 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
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.Collections;
|
|
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
|
|
{
|
|
[Dependency] private IClyde _clyde = null!;
|
|
public IClyde Clyde => _clyde;
|
|
[Dependency] private IResourceManager _manager = default!;
|
|
[Dependency] private IFontManager _fontManager = null!;
|
|
public IFontManager FontManager => _fontManager;
|
|
[Dependency] private ILogManager _logManager = default!;
|
|
[Dependency] private 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.LoadTextureParameters(_manager, data);
|
|
if (!data.LoadParameters.Preload)
|
|
{
|
|
data.Skip = true;
|
|
return;
|
|
}
|
|
|
|
TextureResource.LoadPreTextureData(_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 || data.Skip)
|
|
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;
|
|
var skipped = 0;
|
|
foreach (var data in texList)
|
|
{
|
|
if (data.Bad)
|
|
{
|
|
errors += 1;
|
|
continue;
|
|
}
|
|
|
|
if (data.Skip)
|
|
{
|
|
skipped += 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, {CountSkipped} skipped) in {LoadTime}",
|
|
texList.Length - skipped - errors,
|
|
errors,
|
|
skipped,
|
|
sw.Elapsed);
|
|
}
|
|
|
|
private void PreloadRsis(ISawmill sawmill)
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
var resList = GetTypeData<RSIResource>().Resources;
|
|
|
|
var foundRsiList = _manager.ContentFindFiles("/Textures/")
|
|
.Where(p => p.ToString().EndsWith(".rsi/meta.json"))
|
|
.Select(c => c.Directory);
|
|
|
|
var foundRsicList = _manager.ContentFindFiles("/Textures/")
|
|
.Where(p => p.Extension == "rsic")
|
|
.Select(c => c.WithExtension("rsi"));
|
|
|
|
var rsiList = foundRsiList
|
|
.Concat(foundRsicList)
|
|
.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;
|
|
}
|
|
});
|
|
|
|
var atlasLookup = rsiList.ToLookup(ShouldMetaAtlas);
|
|
var atlasList = atlasLookup[true].ToArray();
|
|
var nonAtlasList = atlasLookup[false].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?
|
|
|
|
// We now need to insert the RSIs into the atlas. This specific problem is 2BP|O|F - the items are oriented
|
|
// and cutting is free. The sorting is done by a slightly modified FFDH algorithm. The algorithm is exactly
|
|
// the same as the standard FFDH algorithm with one main difference: We create new "levels" above placed
|
|
// blocks. For example if the first block was 10x20, then the second was 10x10 units, we would create a
|
|
// 10x10 level above the second block that would be treated as a normal level. This increases the packing
|
|
// efficiency from ~85% to ~95% with very little extra computational effort. The algorithm appears to be
|
|
// ~97% effective for storing SS14s RSIs.
|
|
//
|
|
// Here are some more resources about the strip packing problem!
|
|
// - https://en.wikipedia.org/w/index.php?title=Strip_packing_problem&oldid=1263496949#First-fit_decreasing-height_(FFDH)
|
|
// - https://www.csc.liv.ac.uk/~epa/surveyhtml.html
|
|
// - https://www.dei.unipd.it/~fisch/ricop/tesi/tesi_dottorato_Lodi_1999.pdf
|
|
|
|
// The array must be sorted from biggest to smallest first.
|
|
Array.Sort(atlasList, (b, a) => a.AtlasSheet.Height.CompareTo(b.AtlasSheet.Height));
|
|
|
|
var maxSize = Math.Min(GL.GetInteger(GetPName.MaxTextureSize), _configurationManager.GetCVar(CVars.ResRSIAtlasSize));
|
|
|
|
// THIS IS NOT GUARANTEED TO HAVE ANY PARTICULARLY LOGICAL ORDERING.
|
|
// E.G you could have atlas 1 RSIs appear *before* you're done seeing atlas 2 RSIs.
|
|
var levels = new ValueList<Level>();
|
|
|
|
// List of all the image atlases.
|
|
var imageAtlases = new ValueList<Image<Rgba32>>();
|
|
|
|
// List of all the actual atlases.
|
|
var finalAtlases = new ValueList<OwnedTexture>();
|
|
|
|
// Number of total pixels in each atlas.
|
|
var finalPixels = new ValueList<int>();
|
|
|
|
// First we just find the location of all the RSIs in the atlas before actually placing them.
|
|
// This allows us to effectively determine how much space we need to allocate for the images.
|
|
var currentHeight = 0;
|
|
var currentAtlasIndex = 0;
|
|
foreach (var rsi in atlasList)
|
|
{
|
|
var insertHeight = rsi.AtlasSheet.Height;
|
|
var insertWidth = rsi.AtlasSheet.Width;
|
|
|
|
var found = false;
|
|
for (var i = 0; i < levels.Count && !found; i++)
|
|
{
|
|
var levelPosition = levels[i].Position;
|
|
var levelWidth = levels[i].Width;
|
|
var levelHeight = levels[i].Height;
|
|
|
|
// Check if it can fit in this level.
|
|
if (levelHeight < insertHeight || levelWidth + insertWidth > levels[i].MaxWidth)
|
|
continue;
|
|
|
|
found = true;
|
|
|
|
levels[i].Width += insertWidth;
|
|
rsi.AtlasOffset = levelPosition + new Vector2i(levelWidth, 0);
|
|
levels[i].RSIList.Add(rsi);
|
|
|
|
// Creating the extra "free" space above blocks that can be used for inserting more items.
|
|
// This differs from the FFDH spec which just ignores this space.
|
|
Debug.Assert(levelHeight >= insertHeight); // Must be true because the array needs to be sorted
|
|
if (levelHeight - insertHeight == 0)
|
|
continue;
|
|
|
|
var freeLevel = new Level
|
|
{
|
|
AtlasId = levels[i].AtlasId,
|
|
Position = levelPosition + new Vector2i(levelWidth, insertHeight),
|
|
Height = levelHeight - insertHeight,
|
|
Width = 0,
|
|
MaxWidth = insertWidth,
|
|
RSIList = [ ]
|
|
};
|
|
|
|
levels.Add(freeLevel);
|
|
}
|
|
|
|
if (found)
|
|
continue;
|
|
|
|
// Ran out of space, we need to move on to the next atlas.
|
|
// This also isn't in the normal FFDH algorithm (obviously) but its close enough.
|
|
if (currentHeight + insertHeight > maxSize)
|
|
{
|
|
imageAtlases.Add(new Image<Rgba32>(maxSize, currentHeight));
|
|
finalPixels.Add(0);
|
|
currentHeight = 0;
|
|
currentAtlasIndex++;
|
|
}
|
|
|
|
rsi.AtlasOffset = new Vector2i(0, currentHeight);
|
|
|
|
var newLevel = new Level
|
|
{
|
|
AtlasId = currentAtlasIndex,
|
|
Position = new Vector2i(0, currentHeight),
|
|
Height = insertHeight,
|
|
Width = insertWidth,
|
|
MaxWidth = maxSize,
|
|
RSIList = [ rsi ]
|
|
};
|
|
levels.Add(newLevel);
|
|
|
|
currentHeight += insertHeight;
|
|
}
|
|
|
|
// This allocation takes a long time.
|
|
imageAtlases.Add(new Image<Rgba32>(maxSize, currentHeight));
|
|
finalPixels.Add(0);
|
|
|
|
// Put all textures on the atlases
|
|
foreach (var level in levels)
|
|
{
|
|
foreach (var rsi in level.RSIList)
|
|
{
|
|
var box = new UIBox2i(0, 0, rsi.AtlasSheet.Width, rsi.AtlasSheet.Height);
|
|
|
|
rsi.AtlasSheet.Blit(box, imageAtlases[level.AtlasId], rsi.AtlasOffset);
|
|
finalPixels[level.AtlasId] += rsi.AtlasSheet.Width * rsi.AtlasSheet.Height;
|
|
}
|
|
}
|
|
|
|
// Finalize the atlases.
|
|
for (var i = 0; i < imageAtlases.Count; i++)
|
|
{
|
|
var atlasTexture = Clyde.LoadTextureFromImage(imageAtlases[i], $"Meta atlas {i}");
|
|
finalAtlases.Add(atlasTexture);
|
|
|
|
sawmill.Debug($"(Meta atlas {i}) - cropped utilization: {(float)finalPixels[i] / (maxSize * imageAtlases[i].Height):P2}, fill percentage: {(float)imageAtlases[i].Height / maxSize:P2}");
|
|
}
|
|
|
|
// Finally, reference the actual atlas from the RSIs.
|
|
foreach (var level in levels)
|
|
{
|
|
foreach (var rsi in level.RSIList)
|
|
{
|
|
rsi.AtlasTexture = finalAtlases[level.AtlasId];
|
|
}
|
|
}
|
|
|
|
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,
|
|
finalAtlases.Count,
|
|
nonAtlasList.Length,
|
|
errors,
|
|
sw.Elapsed);
|
|
}
|
|
|
|
private static bool ShouldMetaAtlas(RSIResource.LoadStepData rsi)
|
|
{
|
|
return rsi.MetaAtlas && rsi.LoadParameters == TextureLoadParameters.Default;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A "Level" to place boxes. Similar to FFDH levels, but with more parameters so we can fit in "free" levels
|
|
/// above placed boxes.
|
|
/// </summary>
|
|
internal sealed class Level
|
|
{
|
|
/// <summary>
|
|
/// Index of the atlas this is located.
|
|
/// </summary>
|
|
public required int AtlasId;
|
|
/// <summary>
|
|
/// Bottom left of the location for the RSIs.
|
|
/// </summary>
|
|
public required Vector2i Position;
|
|
/// <summary>
|
|
/// The current width of the level.
|
|
/// </summary>
|
|
/// <remarks>This can (and will) be 0. Will change.</remarks>
|
|
public required int Width;
|
|
/// <summary>
|
|
/// The current height of the level.
|
|
/// </summary>
|
|
/// <remarks>This value should never change.</remarks>
|
|
public required int Height;
|
|
/// <summary>
|
|
/// Maximum width of the level.
|
|
/// </summary>
|
|
public required int MaxWidth;
|
|
/// <summary>
|
|
/// List of all the RSIs stored in this level. RSIs are ordered from tallest to smallest per level.
|
|
/// </summary>
|
|
public required List<RSIResource.LoadStepData> RSIList;
|
|
}
|
|
}
|