resource stuff
This commit is contained in:
@@ -1,43 +1,209 @@
|
||||
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";
|
||||
private static readonly Dictionary<string, Resource> loadedResources = new Dictionary<string, Resource>();
|
||||
private const string RootName = "Resources";
|
||||
|
||||
public static T Load<T>(string resourcePath) where T : Resource
|
||||
// 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()
|
||||
{
|
||||
if (loadedResources.TryGetValue(resourcePath, out Resource resource))
|
||||
{ typeof(Shader), () => new Shader() }
|
||||
};
|
||||
|
||||
static ResourceManager()
|
||||
{
|
||||
_loaderThread = new Thread(LoaderWorkerLoop)
|
||||
{
|
||||
return (T)resource;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(Shader))
|
||||
{
|
||||
resource = new Shader();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Failed to create resource");
|
||||
}
|
||||
|
||||
Assert.Always(resource != null);
|
||||
resource.Path = resourcePath;
|
||||
resource.Load(StorageContainer.OpenStream(Path.Combine(AppContext.BaseDirectory, rootName, resourcePath) + ".bin"));
|
||||
|
||||
loadedResources.Add(resourcePath, resource);
|
||||
return (T)resource;
|
||||
}
|
||||
|
||||
public static void Unload(Resource 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
|
||||
{
|
||||
if (!loadedResources.ContainsKey(resource.Path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Guid id = GetId(resourcePath);
|
||||
return Retain<T>(id, resourcePath);
|
||||
}
|
||||
|
||||
resource.Unload();
|
||||
resource.Path = string.Empty;
|
||||
loadedResources.Remove(resource.Path);
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user