resource stuff
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Nerfed.Builder.Meta;
|
||||
|
||||
namespace Nerfed.Builder;
|
||||
|
||||
@@ -57,15 +59,45 @@ public class Builder : IDisposable
|
||||
string outFile = $"{args.ResourceOutPath}/{relativeFile}{PathUtil.ImportedFileExtension}";
|
||||
|
||||
FileInfo inFileInfo = new FileInfo(inFile);
|
||||
FileInfo outFileInfo = new FileInfo(outFile);
|
||||
|
||||
if (!FileUtil.IsNewer(inFileInfo, outFileInfo))
|
||||
// =========================================================================
|
||||
// STEP 1: GUID META FILE SYNC
|
||||
// Ensure the source file has a backing .meta file generating its Guid
|
||||
// =========================================================================
|
||||
string metaFile = inFile + ".meta";
|
||||
AssetMeta metaData;
|
||||
|
||||
if (!File.Exists(metaFile))
|
||||
{
|
||||
// Generate a brand new meta file to track this asset permanently
|
||||
metaData = new AssetMeta(Guid.NewGuid());
|
||||
string json = JsonSerializer.Serialize(metaData, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(metaFile, json);
|
||||
Console.WriteLine($"[Meta] Generated new tracking ID '{metaData.Id}' for {relativeFile}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Load the existing guid
|
||||
metaData = JsonSerializer.Deserialize<AssetMeta>(File.ReadAllText(metaFile))!;
|
||||
}
|
||||
|
||||
// Change output file from Name.ext.bin -> /GUID.bin to completely anonymize the actual game package!
|
||||
string cacheOutFile = $"{args.ResourceOutPath}/{metaData.Id}.bin";
|
||||
FileInfo outFileInfo = new FileInfo(cacheOutFile);
|
||||
|
||||
// Rebuild if the source file changed, or if the meta file changed!
|
||||
FileInfo metaFileInfo = new FileInfo(metaFile);
|
||||
bool requiresCompile = !outFileInfo.Exists ||
|
||||
FileUtil.IsNewer(inFileInfo, outFileInfo) ||
|
||||
FileUtil.IsNewer(metaFileInfo, outFileInfo);
|
||||
|
||||
if (!requiresCompile)
|
||||
{
|
||||
// File has not changed since last build, no need to build this one.
|
||||
return;
|
||||
}
|
||||
|
||||
string outDir = Path.GetDirectoryName(outFile);
|
||||
string outDir = Path.GetDirectoryName(cacheOutFile)!;
|
||||
if (!Directory.Exists(outDir))
|
||||
{
|
||||
Directory.CreateDirectory(outDir);
|
||||
@@ -74,14 +106,14 @@ public class Builder : IDisposable
|
||||
string ext = Path.GetExtension(inFile).ToLower();
|
||||
if (importers.TryGetValue(ext, out IImporter importer))
|
||||
{
|
||||
importer.Import(inFile, outFile);
|
||||
importer.Import(inFile, cacheOutFile); // Compile source directly to hash.bin
|
||||
}
|
||||
else
|
||||
{
|
||||
rawFileImporter.Import(inFile, outFile);
|
||||
rawFileImporter.Import(inFile, cacheOutFile);
|
||||
}
|
||||
|
||||
Console.WriteLine(relativeFile);
|
||||
Console.WriteLine($"Compiled {relativeFile} -> {metaData.Id}.bin");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
|
||||
namespace Nerfed.Builder.Meta
|
||||
{
|
||||
/// <summary>
|
||||
/// Foundation for JSON-serialized metadata files (e.g. hero.png.meta)
|
||||
/// </summary>
|
||||
public class AssetMeta
|
||||
{
|
||||
/// <summary>
|
||||
/// The universally unique identifier for this asset, generated on first import.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The importer version. Useful to force re-imports if your engine updates how it parses textures.
|
||||
/// </summary>
|
||||
public int ImporterVersion { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Base constructor needed for JSON deserialization
|
||||
/// </summary>
|
||||
public AssetMeta() { }
|
||||
|
||||
public AssetMeta(Guid id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,10 +68,10 @@ public class GraphicsDevice : IDisposable
|
||||
|
||||
internal void LoadDefaultPipelines()
|
||||
{
|
||||
FullscreenVertexShader = ResourceManager.Load<Shader>("Shaders/Fullscreen.vert");
|
||||
VideoFragmentShader = ResourceManager.Load<Shader>("Shaders/Video.frag");
|
||||
TextVertexShader = ResourceManager.Load<Shader>("Shaders/Text.vert");
|
||||
TextFragmentShader = ResourceManager.Load<Shader>("Shaders/Text.frag");
|
||||
FullscreenVertexShader = ResourceManager.Retain<Shader>("Shaders/Fullscreen.vert");
|
||||
VideoFragmentShader = ResourceManager.Retain<Shader>("Shaders/Video.frag");
|
||||
TextVertexShader = ResourceManager.Retain<Shader>("Shaders/Text.vert");
|
||||
TextFragmentShader = ResourceManager.Retain<Shader>("Shaders/Text.frag");
|
||||
|
||||
VideoPipeline = new GraphicsPipeline(
|
||||
this,
|
||||
@@ -373,10 +373,10 @@ public class GraphicsDevice : IDisposable
|
||||
resources.Clear();
|
||||
}
|
||||
|
||||
ResourceManager.Unload(FullscreenVertexShader);
|
||||
ResourceManager.Unload(TextFragmentShader);
|
||||
ResourceManager.Unload(TextVertexShader);
|
||||
ResourceManager.Unload(VideoFragmentShader);
|
||||
ResourceManager.Release(FullscreenVertexShader);
|
||||
ResourceManager.Release(TextFragmentShader);
|
||||
ResourceManager.Release(TextVertexShader);
|
||||
ResourceManager.Release(VideoFragmentShader);
|
||||
}
|
||||
|
||||
Refresh.Refresh_DestroyDevice(Handle);
|
||||
|
||||
@@ -60,8 +60,8 @@ public class GuiController : IDisposable
|
||||
io.DisplaySize = new Vector2(mainWindow.Width, mainWindow.Height);
|
||||
io.DisplayFramebufferScale = Vector2.One;
|
||||
|
||||
imGuiVertexShader = ResourceManager.Load<Shader>("Shaders/ImGui.vert");
|
||||
imGuiFragmentShader = ResourceManager.Load<Shader>("Shaders/ImGui.frag");
|
||||
imGuiVertexShader = ResourceManager.Retain<Shader>("Shaders/ImGui.vert");
|
||||
imGuiFragmentShader = ResourceManager.Retain<Shader>("Shaders/ImGui.frag");
|
||||
|
||||
imGuiSampler = new Sampler(graphicsDevice, SamplerCreateInfo.LinearClamp);
|
||||
|
||||
@@ -630,8 +630,8 @@ public class GuiController : IDisposable
|
||||
fontTexture?.Dispose();
|
||||
imGuiVertexBuffer?.Dispose();
|
||||
imGuiIndexBuffer?.Dispose();
|
||||
ResourceManager.Unload(imGuiVertexShader);
|
||||
ResourceManager.Unload(imGuiFragmentShader);
|
||||
ResourceManager.Release(imGuiVertexShader);
|
||||
ResourceManager.Release(imGuiFragmentShader);
|
||||
imGuiPipeline?.Dispose();
|
||||
imGuiSampler?.Dispose();
|
||||
resourceUploader?.Dispose();
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
using System;
|
||||
namespace Nerfed.Runtime;
|
||||
|
||||
public enum ResourceState
|
||||
{
|
||||
Unloaded,
|
||||
Queued,
|
||||
Loading,
|
||||
Loaded,
|
||||
Failed
|
||||
}
|
||||
|
||||
public abstract class Resource
|
||||
{
|
||||
public Guid Id { get; internal set; }
|
||||
public string Path { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Natively tracks if the resource is currently in RAM/VRAM.
|
||||
/// </summary>
|
||||
public ResourceState State { get; internal set; } = ResourceState.Unloaded;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks how many entities or systems currently need this loaded.
|
||||
/// When it hits 0, the ResourceManager handles unloading natively.
|
||||
/// </summary>
|
||||
public int ReferenceCount { get; internal set; } = 0;
|
||||
|
||||
internal abstract void Load(Stream stream);
|
||||
internal abstract void Unload();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Nerfed.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Attach this component to an entity mapped to raw source-path strings.
|
||||
/// Useful for testing, hardcoded assets, or before full editor-guided GUID injection.
|
||||
/// </summary>
|
||||
public readonly record struct AssetReferenceComponent(Guid AssetId);
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed version of an asset reference, preventing the user from accidentally
|
||||
/// assigning a Shader GUID to a Texture component in the Editor.
|
||||
/// </summary>
|
||||
public readonly record struct TypedAssetReference<TRes>(Guid AssetId) where TRes : Resource;
|
||||
|
||||
/// <summary>
|
||||
/// Added to an entity by the AssetStreamingSystem when the physical resource is fully
|
||||
/// loaded in memory and ready to be used by the renderer or physics engine.
|
||||
/// </summary>
|
||||
public struct AssetLoadedTag { }
|
||||
@@ -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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(Shader))
|
||||
/// <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
|
||||
{
|
||||
resource = new Shader();
|
||||
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
|
||||
{
|
||||
throw new Exception("Failed to create resource");
|
||||
// Sleep cleanly if queue is empty to avoid burning total CPU usage on an infinite while-loop
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!loadedResources.ContainsKey(resource.Path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
resource.Unload();
|
||||
resource.Path = string.Empty;
|
||||
loadedResources.Remove(resource.Path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Nerfed.Runtime.Resources;
|
||||
|
||||
/// <summary>
|
||||
/// A sample component demonstrating how to use strongly-typed asset references
|
||||
/// in a realistic scenario where an entity requires multiple distinct resources.
|
||||
/// </summary>
|
||||
public struct SampleMeshVisualComponent
|
||||
{
|
||||
// The user safely assigns a Mesh GUID in the Editor inspector.
|
||||
public TypedAssetReference<Shader> VertexShader;
|
||||
|
||||
// The user safely assigns a Material GUID in the Editor inspector.
|
||||
public TypedAssetReference<Shader> FragmentShader;
|
||||
|
||||
public SampleMeshVisualComponent(Guid vertexId, Guid fragId) {
|
||||
VertexShader = new TypedAssetReference<Shader>(vertexId);
|
||||
FragmentShader = new TypedAssetReference<Shader>(fragId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using MoonTools.ECS;
|
||||
using Nerfed.Runtime.Scene.Streaming;
|
||||
using System;
|
||||
|
||||
namespace Nerfed.Runtime.Resources;
|
||||
|
||||
/// <summary>
|
||||
/// A typical rendering preparation system that natively resolves and requests
|
||||
/// asynchronous background loading for its own required assets, removing the
|
||||
/// need for a monolithic generic AssetStreaming manager.
|
||||
/// </summary>
|
||||
public class SampleRenderSystem : MoonTools.ECS.System
|
||||
{
|
||||
private readonly Filter _meshVisualsFilter;
|
||||
|
||||
public SampleRenderSystem(World world) : base(world) {
|
||||
_meshVisualsFilter = FilterBuilder
|
||||
.Include<SampleMeshVisualComponent>()
|
||||
// Always ignore chunk entities technically "unloading" from RAM
|
||||
.Exclude<ChunkUnloadPendingTag>()
|
||||
.Build();
|
||||
}
|
||||
|
||||
public override void Update(TimeSpan delta) {
|
||||
foreach(Entity entity in _meshVisualsFilter.Entities) {
|
||||
SampleMeshVisualComponent visualComp = Get<SampleMeshVisualComponent>(entity);
|
||||
|
||||
// 1. Resolve State
|
||||
ResourceState vertState = ResourceManager.GetState(visualComp.VertexShader.AssetId);
|
||||
ResourceState fragState = ResourceManager.GetState(visualComp.FragmentShader.AssetId);
|
||||
|
||||
// 2. Asynchronously request assets if they don't exist in memory yet
|
||||
if(vertState == ResourceState.Unloaded) {
|
||||
ResourceManager.Retain<Shader>(visualComp.VertexShader.AssetId, "Unknown/Path");
|
||||
}
|
||||
if(fragState == ResourceState.Unloaded) {
|
||||
ResourceManager.Retain<Shader>(visualComp.FragmentShader.AssetId, "Unknown/Path");
|
||||
}
|
||||
|
||||
// 3. Prevent rendering logic unless ALL strictly required assets are fully mapped
|
||||
bool isReadyToDraw = vertState == ResourceState.Loaded && fragState == ResourceState.Loaded;
|
||||
|
||||
if(isReadyToDraw) {
|
||||
// At this exact point, you can safely assume:
|
||||
// 1) The background loading threads are 100% finished processing these shaders.
|
||||
// 2) The GraphicsDevice can safely extract the native handle.
|
||||
|
||||
// e.g. GraphicsDevice.BindShader(visualComp.VertexShader.AssetId);
|
||||
// e.g. GraphicsDevice.DrawPolygons(...);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user