Skip to content

Commit

Permalink
Robust.Packaging system (space-wizards#3016)
Browse files Browse the repository at this point in the history
Co-authored-by: metalgearsloth <[email protected]>
  • Loading branch information
PJB3005 and metalgearsloth authored Sep 14, 2022
1 parent d87d6f7 commit 2065978
Show file tree
Hide file tree
Showing 32 changed files with 1,863 additions and 479 deletions.
203 changes: 18 additions & 185 deletions Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Utility;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Resources;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
Expand All @@ -20,27 +18,19 @@ namespace Robust.Client.ResourceManagement
/// </summary>
public sealed class RSIResource : BaseResource
{
private static readonly float[] OneArray = {1};

public override ResourcePath? Fallback => new("/Textures/error.rsi");

private static readonly JsonSerializerOptions SerializerOptions =
new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
AllowTrailingCommas = true
};

public RSI RSI { get; private set; } = default!;

/// <summary>
/// The minimum version of RSI we can load.
/// </summary>
public const uint MINIMUM_RSI_VERSION = 1;
public const uint MINIMUM_RSI_VERSION = RsiLoading.MINIMUM_RSI_VERSION;

/// <summary>
/// The maximum version of RSI we can load.
/// </summary>
public const uint MAXIMUM_RSI_VERSION = 1;
public const uint MAXIMUM_RSI_VERSION = RsiLoading.MAXIMUM_RSI_VERSION;

public override void Load(IResourceCache cache, ResourcePath path)
{
Expand All @@ -67,7 +57,12 @@ internal static void LoadTexture(IClyde clyde, LoadStepData loadStepData)

internal static void LoadPreTexture(IResourceCache cache, LoadStepData data)
{
var metadata = LoadRsiMetadata(cache, data.Path);
var manifestPath = data.Path / "meta.json";
RsiLoading.RsiMetadata metadata;
using (var manifestFile = cache.ContentFileRead(manifestPath))
{
metadata = RsiLoading.LoadRsiMetadata(manifestFile);
}

var stateCount = metadata.States.Length;
var toAtlas = new StateReg[stateCount];
Expand Down Expand Up @@ -127,7 +122,15 @@ internal static void LoadPreTexture(IResourceCache cache, LoadStepData data)
reg.Indices = foldedIndices;
reg.Offsets = callbackOffset;

var state = new RSI.State(frameSize, rsi, stateObject.StateId, stateObject.DirType, foldedDelays,
var dirType = stateObject.DirCount switch
{
1 => RSI.State.DirectionType.Dir1,
4 => RSI.State.DirectionType.Dir4,
8 => RSI.State.DirectionType.Dir8,
_ => throw new InvalidOperationException()
};

var state = new RSI.State(frameSize, rsi, stateObject.StateId, dirType, foldedDelays,
textures);
rsi.AddState(state);

Expand Down Expand Up @@ -225,104 +228,6 @@ internal void LoadFinish(IResourceCache cache, LoadStepData data)
}
}

private static RsiMetadata LoadRsiMetadata(IResourceCache cache, ResourcePath path)
{
var manifestPath = path / "meta.json";
RsiJsonMetadata? manifestJson;

using (var manifestFile = cache.ContentFileRead(manifestPath))
{
if (manifestFile.CanSeek && manifestFile.Length <= 4096)
{
// Most RSIs are actually tiny so if that's the case just load them into a stackalloc buffer.
// Avoids a ton of allocations with stream reader etc
// because System.Text.Json can process it directly.
Span<byte> buf = stackalloc byte[4096];
var totalRead = manifestFile.ReadToEnd(buf);
buf = buf[..totalRead];
buf = BomUtil.SkipBom(buf);

manifestJson = JsonSerializer.Deserialize<RsiJsonMetadata>(buf, SerializerOptions);
}
else
{
using var reader = new StreamReader(manifestFile);

string manifestContents = reader.ReadToEnd();
manifestJson = JsonSerializer.Deserialize<RsiJsonMetadata>(manifestContents, SerializerOptions);
}
}

if (manifestJson == null)
throw new RSILoadException($"Manifest JSON failed to deserialize!");

var size = manifestJson.Size;
var states = new StateMetadata[manifestJson.States.Length];

for (var stateI = 0; stateI < manifestJson.States.Length; stateI++)
{
var stateObject = manifestJson.States[stateI];
var stateName = stateObject.Name;
RSI.State.DirectionType directions;
int dirValue;

if (stateObject.Directions is { } dirVal)
{
dirValue = dirVal;
directions = dirVal switch
{
1 => RSI.State.DirectionType.Dir1,
4 => RSI.State.DirectionType.Dir4,
8 => RSI.State.DirectionType.Dir8,
_ => throw new RSILoadException($"Invalid direction for state '{stateName}': {dirValue}. Expected 1, 4 or 8")
};
}
else
{
dirValue = 1;
directions = RSI.State.DirectionType.Dir1;
}

// We can ignore selectors and flags for now,
// because they're not used yet!

// Get the lists of delays.
float[][] delays;
if (stateObject.Delays != null)
{
delays = stateObject.Delays;

if (delays.Length != dirValue)
{
throw new RSILoadException(
$"Direction frames list count ({dirValue}) does not match amount of delays specified ({delays.Length}) for state '{stateName}'.");
}

for (var i = 0; i < delays.Length; i++)
{
var delayList = delays[i];
if (delayList.Length == 0)
{
delays[i] = OneArray;
}
}
}
else
{
delays = new float[dirValue][];
// No delays specified, default to 1 frame per dir.
for (var i = 0; i < dirValue; i++)
{
delays[i] = OneArray;
}
}

states[stateI] = new StateMetadata(new RSI.StateId(stateName), directions, delays);
}

return new RsiMetadata(size, states);
}

/// <summary>
/// Folds a per-directional sets of animation delays
/// into an equivalent set of animation delays and indices that works for every direction.
Expand Down Expand Up @@ -492,77 +397,5 @@ internal struct StateReg
public Vector2i[][] Offsets;
public int TotalFrameCount;
}

internal sealed class RsiMetadata
{
public RsiMetadata(Vector2i size, StateMetadata[] states)
{
Size = size;
States = states;
}

public Vector2i Size { get; }
public StateMetadata[] States { get; }
}

internal sealed class StateMetadata
{
public StateMetadata(RSI.StateId stateId, RSI.State.DirectionType dirType, float[][] delays)
{
StateId = stateId;
DirType = dirType;
Delays = delays;

DebugTools.Assert(delays.Length == DirCount);
DebugTools.Assert(StateId.IsValid);
}

public RSI.StateId StateId { get; }
public RSI.State.DirectionType DirType { get; }

public int DirCount => DirType switch
{
RSI.State.DirectionType.Dir1 => 1,
RSI.State.DirectionType.Dir4 => 4,
RSI.State.DirectionType.Dir8 => 8,
_ => 1
};

public float[][] Delays { get; }
}

// To be directly deserialized.
[UsedImplicitly]
private sealed record RsiJsonMetadata(Vector2i Size, StateJsonMetadata[] States)
{
}

[UsedImplicitly]
private sealed record StateJsonMetadata(string Name, int? Directions, float[][]? Delays)
{
}
}

[Serializable]
[Virtual]
public class RSILoadException : Exception
{
public RSILoadException()
{
}

public RSILoadException(string message) : base(message)
{
}

public RSILoadException(string message, Exception inner) : base(message, inner)
{
}

protected RSILoadException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
}
}
}
30 changes: 4 additions & 26 deletions Robust.Client/Utility/ImageSharpExt.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
Expand Down Expand Up @@ -32,31 +33,13 @@ public static void Blit<T>(this Image<T> source, UIBox2i sourceRect,
Image<T> destination, Vector2i destinationOffset)
where T : unmanaged, IPixel<T>
{
// TODO: Bounds checks.

Blit(source.GetPixelSpan(), source.Width, sourceRect, destination, destinationOffset);
ImageOps.Blit(source, sourceRect, destination, destinationOffset);
}

public static void Blit<T>(this ReadOnlySpan<T> source, int sourceWidth, UIBox2i sourceRect,
Image<T> destination, Vector2i destinationOffset) where T : unmanaged, IPixel<T>
{
var dstSpan = destination.GetPixelSpan();
var dstWidth = destination.Width;
var srcHeight = sourceRect.Height;
var srcWidth = sourceRect.Width;

var (ox, oy) = destinationOffset;

for (var y = 0; y < srcHeight; y++)
{
var sourceRowOffset = sourceWidth * (y + sourceRect.Top) + sourceRect.Left;
var destRowOffset = dstWidth * (y + oy) + ox;

var srcRow = source[sourceRowOffset..(sourceRowOffset + srcWidth)];
var dstRow = dstSpan[destRowOffset..(destRowOffset + srcWidth)];

srcRow.CopyTo(dstRow);
}
ImageOps.Blit(source, sourceWidth, sourceRect, destination, destinationOffset);
}

/// <summary>
Expand All @@ -66,12 +49,7 @@ public static void Blit<T>(this ReadOnlySpan<T> source, int sourceWidth, UIBox2i
/// <exception cref="ArgumentException">Thrown if the image is not a single contiguous buffer.</exception>
public static Span<T> GetPixelSpan<T>(this Image<T> image) where T : unmanaged, IPixel<T>
{
if (!image.DangerousTryGetSinglePixelMemory(out var memory))
{
throw new ArgumentException("Image is not backed by a single buffer, cannot fetch span.");
}

return memory.Span;
return ImageOps.GetPixelSpan(image);
}
}
}
Expand Down
68 changes: 68 additions & 0 deletions Robust.Packaging/AssetProcessing/AssetFiles.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
namespace Robust.Packaging.AssetProcessing;

// TODO: Memory management strategies could be better.

/// <summary>
/// Represents a single file that is passed through the asset graph system.
/// </summary>
/// <seealso cref="AssetFileDisk"/>
/// <seealso cref="AssetFileMemory"/>
public abstract class AssetFile
{
/// <summary>
/// The destination path of the asset file in the VFS.
/// </summary>
public string Path { get; }

/// <summary>
/// Open the file for reading.
/// </summary>
public abstract Stream Open();

private protected AssetFile(string path)
{
Path = path;
}
}

/// <summary>
/// A file of which the contents are backed by disk storage.
/// Avoids pulling files into memory immediately over <see cref="AssetFileMemory"/>.
/// </summary>
/// <remarks>
/// Files passed in must be considered to be immutable.
/// They should not change underneath our feet, even after the asset graph is done processing.
/// </remarks>
public sealed class AssetFileDisk : AssetFile
{
public string DiskPath { get; }

public AssetFileDisk(string path, string diskPath) : base(path)
{
DiskPath = diskPath;
}

public override Stream Open()
{
return File.OpenRead(DiskPath);
}
}

/// <summary>
/// A file of which the contents are backed by memory.
/// </summary>
public sealed class AssetFileMemory : AssetFile
{
public byte[] Memory { get; }

public AssetFileMemory(string path, byte[] memory) : base(path)
{
Memory = memory;
}

public override Stream Open()
{
return new MemoryStream(Memory, false);
}
}

Loading

0 comments on commit 2065978

Please sign in to comment.