210 lines
6.8 KiB
C#
210 lines
6.8 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Threading;
|
|
|
|
namespace Nerfed.Runtime;
|
|
|
|
/// <summary>
|
|
/// A highly scalable, multithreaded resource manager that handles asynchronous asset
|
|
/// loading and automatic reference-counted memory management.
|
|
/// </summary>
|
|
public static class ResourceManager
|
|
{
|
|
private const string RootName = "Resources";
|
|
|
|
// Track resources by their Guid ID instead of simple strings.
|
|
private static readonly ConcurrentDictionary<Guid, Resource> _resourceCache = new();
|
|
|
|
// Mapping a string path to its runtime Guid identifier
|
|
private static readonly ConcurrentDictionary<string, Guid> _pathToGuid = new();
|
|
|
|
// Queues for background processing
|
|
private static readonly ConcurrentQueue<Resource> _loadQueue = new();
|
|
|
|
// Loader threads
|
|
private static readonly Thread _loaderThread;
|
|
private static bool _isRunning = true;
|
|
|
|
// A registry of how to create concrete Resource instances from a generic type without massive switch statements.
|
|
private static readonly Dictionary<Type, Func<Resource>> _resourceFactories = new()
|
|
{
|
|
{ typeof(Shader), () => new Shader() }
|
|
};
|
|
|
|
static ResourceManager()
|
|
{
|
|
_loaderThread = new Thread(LoaderWorkerLoop)
|
|
{
|
|
Name = "Nerfed Asset Loader",
|
|
IsBackground = true,
|
|
Priority = ThreadPriority.BelowNormal // Keeps CPU time focused on the main game loop
|
|
};
|
|
_loaderThread.Start();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Synchronously shuts down the loader thread when the engine closes.
|
|
/// </summary>
|
|
public static void Shutdown()
|
|
{
|
|
_isRunning = false;
|
|
_loaderThread.Join();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a new resource type factory so the manager knows how to instantiate it.
|
|
/// Example: RegisterResourceType<Texture>(() => new Texture());
|
|
/// </summary>
|
|
public static void RegisterResourceType<T>(Func<T> factory) where T : Resource
|
|
{
|
|
_resourceFactories[typeof(T)] = factory;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the Guid associated with a specific asset path, making an initial id pass if required.
|
|
/// In a fully baked engine, the Guid is known at compile time or baked in the map data.
|
|
/// </summary>
|
|
public static Guid GetId(string resourcePath)
|
|
{
|
|
return _pathToGuid.GetOrAdd(resourcePath, _ => Guid.NewGuid());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Begins an asynchronous load for a resource by its Guid.
|
|
/// In ECS systems, Entities should strictly prefer this overload over the string one.
|
|
/// </summary>
|
|
public static T Retain<T>(Guid id, string expectedPath) where T : Resource
|
|
{
|
|
var resource = _resourceCache.GetOrAdd(id, (assetId) =>
|
|
{
|
|
if (!_resourceFactories.TryGetValue(typeof(T), out var factory))
|
|
{
|
|
throw new Exception($"Failed to create resource. No factory registered for {typeof(T).Name}");
|
|
}
|
|
|
|
var newResource = factory();
|
|
newResource.Id = assetId;
|
|
// The path is still required so the background thread knows which file to open from disk.
|
|
newResource.Path = expectedPath;
|
|
newResource.State = ResourceState.Unloaded;
|
|
|
|
return newResource;
|
|
});
|
|
|
|
lock (resource)
|
|
{
|
|
resource.ReferenceCount++;
|
|
|
|
if (resource.State == ResourceState.Unloaded)
|
|
{
|
|
resource.State = ResourceState.Queued;
|
|
_loadQueue.Enqueue(resource);
|
|
}
|
|
}
|
|
|
|
return (T)resource;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Begins an asynchronous load utilizing the string path to find the matching Guid.
|
|
/// This should generally be avoided in tight ECS loops.
|
|
/// </summary>
|
|
public static T Retain<T>(string resourcePath) where T : Resource
|
|
{
|
|
Guid id = GetId(resourcePath);
|
|
return Retain<T>(id, resourcePath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the current loading state of a resource by its Guid without altering its reference count.
|
|
/// </summary>
|
|
public static ResourceState GetState(Guid id)
|
|
{
|
|
if (_resourceCache.TryGetValue(id, out var resource))
|
|
{
|
|
return resource.State;
|
|
}
|
|
return ResourceState.Unloaded;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decrements the reference count of a resource by its Guid.
|
|
/// </summary>
|
|
public static void Release(Guid id)
|
|
{
|
|
if (_resourceCache.TryGetValue(id, out var resource))
|
|
{
|
|
Release(resource);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decrements the reference count of a resource.
|
|
/// If the count reaches 0, the asset is automatically unloaded from memory.
|
|
/// </summary>
|
|
public static void Release(Resource resource)
|
|
{
|
|
if (resource == null) return;
|
|
|
|
lock (resource)
|
|
{
|
|
resource.ReferenceCount--;
|
|
|
|
if (resource.ReferenceCount <= 0)
|
|
{
|
|
// Fully unused! We should unload it safely.
|
|
if (resource.State == ResourceState.Loaded)
|
|
{
|
|
resource.Unload();
|
|
}
|
|
|
|
resource.State = ResourceState.Unloaded;
|
|
_resourceCache.TryRemove(resource.Id, out _);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Background thread loop that pulls from the queue and does the slow file I/O operations.
|
|
/// </summary>
|
|
private static void LoaderWorkerLoop()
|
|
{
|
|
while (_isRunning)
|
|
{
|
|
if (_loadQueue.TryDequeue(out var resource))
|
|
{
|
|
// Safety check: Was the resource released before we even got around to loading it?
|
|
if (resource.ReferenceCount <= 0)
|
|
{
|
|
resource.State = ResourceState.Unloaded;
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
resource.State = ResourceState.Loading;
|
|
string fullPath = Path.Combine(AppContext.BaseDirectory, RootName, resource.Id.ToString()) + ".bin";
|
|
|
|
// Do the slow synchronous disk read
|
|
using var stream = StorageContainer.OpenStream(fullPath);
|
|
resource.Load(stream);
|
|
|
|
resource.State = ResourceState.Loaded;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error($"Failed to background load asset '{resource.Path}': {e.Message}");
|
|
resource.State = ResourceState.Failed;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Sleep cleanly if queue is empty to avoid burning total CPU usage on an infinite while-loop
|
|
Thread.Sleep(10);
|
|
}
|
|
}
|
|
}
|
|
}
|