Skip to content

Commit

Permalink
Improved Asset Loading Queue
Browse files Browse the repository at this point in the history
  • Loading branch information
Digi committed Oct 10, 2024
1 parent af57552 commit 7f2f494
Show file tree
Hide file tree
Showing 47 changed files with 704 additions and 590 deletions.
9 changes: 9 additions & 0 deletions LevelImposter/AssetLoader/Audio/LoadableAudio.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using LevelImposter.Core;

namespace LevelImposter.AssetLoader;

public readonly struct LoadableAudio(string _id, IStreamable _streamable) : ICachable
{
public string ID => _id;
public IStreamable Streamable => _streamable;
}
43 changes: 43 additions & 0 deletions LevelImposter/AssetLoader/AudioLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using LevelImposter.Core;
using UnityEngine;

namespace LevelImposter.AssetLoader;

public class AudioLoader : AsyncQueue<LoadableAudio, AudioClip>
{
private AudioLoader()
{
}

public static AudioLoader Instance { get; } = new();

/// <summary>
/// Simplified shorthand to load an audioclip asynchronously.
/// </summary>
/// <param name="id">ID for cache</param>
/// <param name="streamable">Streamable with raw image data</param>
/// <param name="callback">Callback when the AudioClip is loaded</param>
public static void LoadAsync(string id, IStreamable streamable, Action<AudioClip> callback)
{
Instance.AddToQueue(
new LoadableAudio(id, streamable),
callback
);
}

protected override AudioClip Load(LoadableAudio loadable)
{
// Open the stream
var stream = loadable.Streamable.OpenStream();

// Load the sprite
var loadedFile = WAVLoader.Load(stream, loadable.ID);

// Close the stream
stream.Close();

// Return the loaded sprite
return loadedFile;
}
}
39 changes: 39 additions & 0 deletions LevelImposter/AssetLoader/Loaders/GIFLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.IO;
using LevelImposter.Core;
using UnityEngine;

namespace LevelImposter.AssetLoader;

public class GIFLoader
{
/// <summary>
/// Loads a GIF image from a stream.
/// </summary>
/// <param name="imgStream">Image stream to load from</param>
/// <param name="options">Options to apply</param>
/// <returns>A fully-loaded GIFFile containing the image data</returns>
public static LoadedGIF Load(
Stream imgStream,
LoadableSprite loadable)
{
// Create new file
var gifFile = new GIFFile(loadable.ID);
GCHandler.Register(gifFile);

// Append Options
var options = loadable.Options;
gifFile.SetPivot(options.Pivot);
// TODO: Allow pixel art in GIFs

// Load the GIF file from the stream
gifFile.Load(imgStream);

// Return the GIF file
return new LoadedGIF(gifFile.GetFrameSprite(0), gifFile);
}

public class LoadedGIF(Sprite _sprite, GIFFile _gifFile) : LoadedSprite(_sprite)
{
public GIFFile GIFFile => _gifFile;
}
}
92 changes: 92 additions & 0 deletions LevelImposter/AssetLoader/Loaders/PNGLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System.IO;
using Il2CppInterop.Runtime.Attributes;
using LevelImposter.Core;
using UnityEngine;

namespace LevelImposter.AssetLoader;

public class PNGLoader
{
/// <summary>
/// Loads a PNG/JPG image from a stream.
/// </summary>
/// <param name="imgStream">Raw PNG/JPG file stream</param>
/// <param name="loadable">Sprite options to apply</param>
/// <returns>A still UnityEngine.Sprite containing the image data</returns>
/// <exception cref="IOException">If the Stream fails to read image data</exception>
public static LoadedSprite Load(Stream imgStream, LoadableSprite loadable)
{
// Get All Data
var imageDataBuffer = new byte[imgStream.Length];
var readBytes = imgStream.Read(imageDataBuffer, 0, imageDataBuffer.Length);
if (readBytes != imageDataBuffer.Length)
throw new IOException("Failed to read all image data");

// Get Options
var options = loadable.Options;

// Create Texture
var sprite = ImageDataToSprite(
imageDataBuffer,
loadable.ID,
options.Pivot,
options.PixelArt
);

// Register in GC
GCHandler.Register(sprite);

// Create Loaded Sprite
return new LoadedSprite(sprite);
}

/// <summary>
/// Converts raw PNG/JPG bytes to a still sprite.
/// <para>
/// This is a relatively expensive operation and must be done on the main Unity thread.
/// Texture data is removed from CPU memory making the resulting Sprite non-readable.
/// </para>
/// </summary>
/// <param name="imgData">Raw PNG/JPG data in within IL2CPP memory</param>
/// <param name="name">Name of the resulting sprite objects</param>
/// <param name="pivot">Pivots the sprite by a Vector2. (Default: 0.5f, 0.5f)</param>
/// <param name="isPixelArt">Whether the image is pixel art or not. Disables Bilinear filtering. (Default: false)</param>
/// <returns>A Unity Sprite containing the resulting image data</returns>
[HideFromIl2Cpp]
public static Sprite ImageDataToSprite(
byte[] imgData,
string name = "CustomSprite",
Vector2? pivot = null,
bool isPixelArt = false)
{
// Generate Texture
Texture2D texture = new(1, 1, TextureFormat.RGBA32, false)
{
name = $"{name}_tex",
wrapMode = TextureWrapMode.Clamp,
filterMode = isPixelArt ? FilterMode.Point : FilterMode.Bilinear,
hideFlags = HideFlags.HideAndDontSave,
requestedMipmapLevel = 0
};
texture.LoadImage(imgData);

// Remove from CPU Memory
texture.Apply(false, true);

// Generate Sprite
var sprite = Sprite.Create(
texture,
new Rect(0, 0, texture.width, texture.height),
pivot ?? new Vector2(0.5f, 0.5f),
100.0f,
0,
SpriteMeshType.FullRect
);

// Set Sprite Flags
sprite.name = $"{name}_sprite";
sprite.hideFlags = HideFlags.DontUnloadUnusedAsset;

return sprite;
}
}
54 changes: 54 additions & 0 deletions LevelImposter/AssetLoader/Loaders/WAVLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.IO;
using LevelImposter.Core;
using UnityEngine;

namespace LevelImposter.AssetLoader;

public class WAVLoader
{
/// <summary>
/// Loads a WAV from a sound data object.
/// </summary>
/// <param name="soundData">Sound Data to load</param>
/// <returns>Sound data in the form of a Unity AudioClip</returns>
public static AudioClip? Load(LISound? soundData)
{
// Get Sound Data
if (soundData == null)
return null;

// Get Asset DB
var mapAssetDB = LIShipStatus.GetInstanceOrNull()?.CurrentMap?.mapAssetDB;

// Get Sound Stream
var soundDBElem = mapAssetDB?.Get(soundData.dataID);
if (soundDBElem == null)
return null;

// Get Sound Data
var stream = soundDBElem.OpenStream();
var audioClip = Load(stream, soundData.id.ToString());
stream.Close();

return audioClip;
}

/// <summary>
/// Loads a WAV from a raw stream.
/// </summary>
/// <param name="stream">Stream to loads from</param>
/// <param name="name">Name of the resulting object</param>
/// <returns>Sound data in the form of a Unity AudioClip</returns>
public static AudioClip Load(Stream stream, string name)
{
// Create a new WAV file
var wavFile = new WAVFile(name);
GCHandler.Register(wavFile);

// Load the WAV file from the stream
wavFile.Load(stream);

// Return the WAV file
return wavFile.GetClip();
}
}
101 changes: 101 additions & 0 deletions LevelImposter/AssetLoader/Queue/AsyncQueue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System;
using System.Collections;
using System.Collections.Generic;
using LevelImposter.Core;
using Reactor.Utilities;

namespace LevelImposter.AssetLoader;

/// <summary>
/// A queue that asynchronously loads items in the background using a coroutine.
/// </summary>
/// <typeparam name="TInput">Type used for input values (must be ICachable)</typeparam>
/// <typeparam name="TOutput">Type used for output values (must be ICachable)</typeparam>
public abstract class AsyncQueue<TInput, TOutput> where TInput : ICachable
{
private IEnumerator? _consumeQueueCoroutine;

public int QueueSize => Queue.Count;
public int CacheSize => Cache.Count;
protected Queue<QueuedItem> Queue { get; } = new();
protected ItemCache<TOutput> Cache { get; } = new();

/// <summary>
/// Adds an item to the queue.
/// </summary>
/// <param name="inputData">Input data needed to load the item</param>
/// <param name="onLoad">Called when the item is loaded in</param>
public void AddToQueue(TInput inputData, Action<TOutput> onLoad)
{
// Add the item to the queue
Queue.Enqueue(new QueuedItem(inputData, onLoad));

// Start consuming the queue if it's not already running
_consumeQueueCoroutine ??= Coroutines.Start(CoConsumeQueue());
}

/// <summary>
/// Clears the queue and cache.
/// </summary>
public void Clear()
{
Queue.Clear();
Cache.Clear();
}

/// <summary>
/// Called when an item is loaded.
/// </summary>
/// <param name="inputData">Input data used to load item</param>
/// <returns>Output data of the item</returns>
protected abstract TOutput Load(TInput inputData);

/// <summary>
/// Unity coroutine for consuming the queue.
/// </summary>
private IEnumerator CoConsumeQueue()
{
// Repeat until the queue is empty
while (Queue.Count > 0)
{
// Wait for the next available frame
yield return null;

// Continuously load items until the lag limit is reached
while (LagLimiter.ShouldContinue(20) && Queue.Count > 0)
{
// Get the next item in the queue
var queuedItem = Queue.Dequeue();

// Check if item is in cache
var output = Cache.Get(queuedItem.ID);
if (output == null)
{
// Load the item
output = Load(queuedItem.InputData);

// Add the item to the cache
Cache.Add(queuedItem.ID, output);
}

// Call the onLoad callback
queuedItem.OnLoad(output);
}
}

// Clear the coroutine
_consumeQueueCoroutine = null;
}

/// <summary>
/// Represents an item in the queue.
/// </summary>
/// <param name="inputData">Data used for input</param>
/// <param name="onLoad">Callback when the data is loaded in</param>
protected readonly struct QueuedItem(TInput inputData, Action<TOutput> onLoad)
{
public string ID => InputData.ID;
public TInput InputData { get; } = inputData;
public Action<TOutput> OnLoad { get; } = onLoad;
}
}
6 changes: 6 additions & 0 deletions LevelImposter/AssetLoader/Queue/ICachable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace LevelImposter.AssetLoader;

public interface ICachable
{
public string ID { get; }
}
24 changes: 24 additions & 0 deletions LevelImposter/AssetLoader/Queue/ItemCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.Generic;

namespace LevelImposter.AssetLoader;

public class ItemCache<T>
{
private readonly Dictionary<string, T> _cachedItems = new();
public int Count => _cachedItems.Count;

public void Add(string id, T asset)
{
_cachedItems[id] = asset;
}

public T? Get(string id)
{
return _cachedItems.GetValueOrDefault(id);
}

public void Clear()
{
_cachedItems.Clear();
}
}
Loading

0 comments on commit 7f2f494

Please sign in to comment.