using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Threading; namespace Nerfed.Runtime; /// /// A highly scalable, multithreaded resource manager that handles asynchronous asset /// loading and automatic reference-counted memory management. /// public static class ResourceManager { private const string RootName = "Resources"; // Track resources by their Guid ID instead of simple strings. private static readonly ConcurrentDictionary _resourceCache = new(); // Mapping a string path to its runtime Guid identifier private static readonly ConcurrentDictionary _pathToGuid = new(); // Queues for background processing private static readonly ConcurrentQueue _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> _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(); } /// /// Synchronously shuts down the loader thread when the engine closes. /// public static void Shutdown() { _isRunning = false; _loaderThread.Join(); } /// /// Registers a new resource type factory so the manager knows how to instantiate it. /// Example: RegisterResourceType(() => new Texture()); /// public static void RegisterResourceType(Func factory) where T : Resource { _resourceFactories[typeof(T)] = factory; } /// /// 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. /// public static Guid GetId(string resourcePath) { return _pathToGuid.GetOrAdd(resourcePath, _ => Guid.NewGuid()); } /// /// Begins an asynchronous load for a resource by its Guid. /// In ECS systems, Entities should strictly prefer this overload over the string one. /// public static T Retain(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; } /// /// Begins an asynchronous load utilizing the string path to find the matching Guid. /// This should generally be avoided in tight ECS loops. /// public static T Retain(string resourcePath) where T : Resource { Guid id = GetId(resourcePath); return Retain(id, resourcePath); } /// /// Gets the current loading state of a resource by its Guid without altering its reference count. /// public static ResourceState GetState(Guid id) { if (_resourceCache.TryGetValue(id, out var resource)) { return resource.State; } return ResourceState.Unloaded; } /// /// Decrements the reference count of a resource by its Guid. /// public static void Release(Guid id) { if (_resourceCache.TryGetValue(id, out var resource)) { Release(resource); } } /// /// Decrements the reference count of a resource. /// If the count reaches 0, the asset is automatically unloaded from memory. /// 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 _); } } } /// /// Background thread loop that pulls from the queue and does the slow file I/O operations. /// 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); } } } }