Refactor asset streaming, error handling, and unit tests

- Add new compile-time constants and update package versions
- Refactor AssetEntry upload logic to return Result and propagate errors
- Enhance error handling in ResourceStreamingProcessor uploads
- Make ResourceStreamingContext a readonly struct
- Implement IDisposable and finalizers for resource cleanup
- Overhaul AssetManagerTest with async tests and improved mocks
- Add mock implementations for graphics interfaces for testing
- Refactor MockingCommandBuffer and MockingResourceDatabase for better simulation
- Update internals visibility for unit testing
This commit is contained in:
2026-05-03 17:05:52 +09:00
parent e7fedfd35a
commit d2bf2f12a2
16 changed files with 346 additions and 40 deletions

View File

@@ -1,41 +1,56 @@
using Ghost.Core;
using Ghost.Engine;
using Ghost.Graphics;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Ghost.UnitTest.MockingEnvironment;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Buffer;
namespace Ghost.UnitTest.AssetSystem;
[TestClass]
[DoNotParallelize]
public class AssetManagerTest
{
private MockingResourceDatabase _resourceDatabase = null!;
private MockingResourceAllocator _resourceAllocator = null!;
private MockingGraphicsEngine _graphicsEngine = null!;
private MockingCommandBuffer _commandBuffer = null!;
private MockingContentProvider _provider = null!;
private AsyncCopyPipeline _copyPipeline = null!;
private ResourceManager _resourceManager = null!;
private ResourceStreamingProcessor _processor = null!;
private JobScheduler _jobScheduler = null!;
private AssetManager _assetManager = null!;
public TestContext TestContext
{
get; set;
}
[TestInitialize]
public void Setup()
{
_resourceDatabase = new MockingResourceDatabase();
_resourceAllocator = new MockingResourceAllocator(_resourceDatabase);
_commandBuffer = new MockingCommandBuffer(_resourceDatabase);
AllocationManager.Initialize(AllocationManagerDesc.Default);
_graphicsEngine = new MockingGraphicsEngine();
_commandBuffer = (MockingCommandBuffer)_graphicsEngine.CreateCommandBuffer();
_provider = new MockingContentProvider();
_copyPipeline = new AsyncCopyPipeline(_graphicsEngine);
_resourceManager = new ResourceManager(_graphicsEngine.Device, _graphicsEngine.ResourceAllocator, _graphicsEngine.ResourceDatabase);
_processor = new ResourceStreamingProcessor();
var schedulerDesc = new JobSchedulerDesc
{
ThreadCount = 1,
ThreadPriority = ThreadPriority.Normal,
DependencyChainCapacity = 1024,
DependencyChainCapacity = 64,
};
_jobScheduler = new JobScheduler(in schedulerDesc);
_assetManager = new AssetManager(_resourceDatabase, _provider, _processor, _jobScheduler);
_assetManager = new AssetManager(_graphicsEngine.ResourceDatabase, _provider, _processor, _jobScheduler);
}
[TestCleanup]
@@ -43,18 +58,55 @@ public class AssetManagerTest
{
_assetManager.Dispose();
_jobScheduler.Dispose();
_resourceManager.Dispose();
_copyPipeline.Dispose();
_commandBuffer.Dispose();
_resourceAllocator.Dispose();
_resourceDatabase.Dispose();
_graphicsEngine.Dispose();
AllocationManager.Dispose();
}
[TestMethod]
public void AssetManager_GetsAssetSuccessfully()
public async Task AssetManager_ResolveTextureThenBackgroundUpload()
{
var assetID = Guid.NewGuid();
_provider.AddMockTexture(assetID, readDelayMs: Random.Shared.Next(10, 50));
var handle = _assetManager.ResolveTexture(assetID);
Assert.IsTrue(handle.IsValid);
Assert.IsTrue(_assetManager.TryGetEntry(assetID, out var entry));
Assert.IsGreaterThanOrEqualTo((int)AssetState.Scheduled, entry.StateValue);
await Task.Delay(1000, TestContext.CancellationToken);
Assert.IsGreaterThanOrEqualTo((int)AssetState.Loaded, entry.StateValue);
var ctx = new ResourceStreamingContext
{
ResourceManager = _resourceManager,
ResourceDatabase = _graphicsEngine.ResourceDatabase,
ResourceAllocator = _graphicsEngine.ResourceAllocator,
CopyPipeline = _copyPipeline,
GraphicsCommandBuffer = _commandBuffer,
};
_processor.ProcessPendingUploads(ctx);
await Task.Delay(1000, TestContext.CancellationToken);
Assert.IsGreaterThanOrEqualTo((int)AssetState.Uploading, entry.StateValue);
// Trigger the completion of the upload and the transition to shader resource state.
_processor.ProcessPendingUploads(ctx);
Assert.IsGreaterThanOrEqualTo((int)AssetState.Ready, entry.StateValue);
var (data, error) = _graphicsEngine.ResourceDatabase.GetResourceBarrierData(handle.AsResource());
Assert.AreEqual(Error.None, error);
Assert.AreEqual(BarrierAccess.ShaderResource, data.access);
Assert.AreEqual(BarrierLayout.ShaderResource, data.layout);
Assert.AreEqual(BarrierSync.AllShading, data.sync);
}
}

View File

@@ -0,0 +1,19 @@
using Ghost.Graphics.RHI;
namespace Ghost.UnitTest.MockingEnvironment;
internal class MockingCommandAllocator : ICommandAllocator
{
public string Name
{
get; set;
} = "MockCommandAllocator";
public void Reset()
{
}
public void Dispose()
{
}
}

View File

@@ -8,7 +8,6 @@ internal class MockingCommandBuffer : ICommandBuffer
{
private readonly IResourceDatabase _resourceDatabase;
private string _name = "MockCommandBuffer";
private bool _isEmpty = true;
// Tracking properties for test assertions
@@ -16,19 +15,22 @@ internal class MockingCommandBuffer : ICommandBuffer
public int CopyCallCount { get; private set; }
public int UpdateSubResourcesCount { get; private set; }
public CommandBufferType Type => default;
public bool IsEmpty => _isEmpty;
public CommandBufferType Type
{
get;
}
public string Name
{
get => _name;
set => _name = value;
}
get; set;
} = "MockingCommandBuffer";
public MockingCommandBuffer(IResourceDatabase resourceDatabase)
public MockingCommandBuffer(IResourceDatabase resourceDatabase, CommandBufferType type)
{
_resourceDatabase = resourceDatabase;
Type = type;
}
public void Barrier(params scoped ReadOnlySpan<BarrierDesc> barrierDescs)

View File

@@ -0,0 +1,48 @@
using Ghost.Graphics.RHI;
using System.Diagnostics;
namespace Ghost.UnitTest.MockingEnvironment;
internal class MockingCommandQueue : ICommandQueue
{
public CommandQueueType Type
{
get;
}
public string Name
{
get; set;
} = "MockCommandQueue";
public MockingCommandQueue(CommandQueueType type)
{
Type = type;
}
public ulong Signal(IFence fence, ulong value)
{
var mockingFence = fence as MockingFence;
Debug.Assert(mockingFence != null);
mockingFence.Signal(value);
return value;
}
public void Submit(ICommandBuffer commandBuffer)
{
}
public void Submit(params scoped ReadOnlySpan<ICommandBuffer> commandBuffers)
{
}
public void Wait(IFence fence, ulong value)
{
Thread.Sleep(Random.Shared.Next(10, 50));
}
public void Dispose()
{
}
}

View File

@@ -0,0 +1,52 @@
using Ghost.Graphics.RHI;
namespace Ghost.UnitTest.MockingEnvironment;
internal class MockingFence : IFence
{
private readonly AutoResetEvent _fenceEvent;
private ulong _currentValue;
public ulong CompletedValue => _currentValue;
public nint WaitHandle => _fenceEvent.SafeWaitHandle.DangerousGetHandle();
public string Name
{
get; set;
} = "MockingFence";
public MockingFence(ulong initialValue)
{
_fenceEvent = new AutoResetEvent(false);
_currentValue = initialValue;
}
public void Signal(ulong value)
{
if (value > _currentValue)
{
_currentValue = value;
_fenceEvent.Set();
}
}
public void WaitForValue(ulong value)
{
if (value > _currentValue)
{
_fenceEvent.WaitOne();
}
}
public Task WaitForValueAsync(ulong value)
{
return Task.Run(() => { WaitForValue(value); });
}
public void Dispose()
{
_fenceEvent.Dispose();
}
}

View File

@@ -0,0 +1,72 @@
using Ghost.Graphics.RHI;
using System;
using System.Collections.Generic;
using System.Text;
namespace Ghost.UnitTest.MockingEnvironment;
internal class MockingGraphicsEngine : IGraphicsEngine
{
private readonly MockingRenderDevice _renderDevice;
private readonly MockingResourceDatabase _resourceDatabase;
private readonly MockingResourceAllocator _resourceAllocator;
public IRenderDevice Device => _renderDevice;
public IPipelineLibrary PipelineLibrary => throw new NotImplementedException();
public IResourceDatabase ResourceDatabase => _resourceDatabase;
public IResourceAllocator ResourceAllocator => _resourceAllocator;
public MockingGraphicsEngine()
{
_renderDevice = new MockingRenderDevice();
_resourceDatabase = new MockingResourceDatabase();
_resourceAllocator = new MockingResourceAllocator(_resourceDatabase);
}
public void BeginFrame(ulong submittedFrame)
{
}
public ICommandAllocator CreateCommandAllocator(CommandBufferType type = CommandBufferType.Graphics)
{
return new MockingCommandAllocator();
}
public ICommandBuffer CreateCommandBuffer(CommandBufferType type = CommandBufferType.Graphics)
{
return new MockingCommandBuffer(_resourceDatabase, type);
}
public IFence CreateFence(ulong initialValue = 0)
{
return new MockingFence(initialValue);
}
public ISwapChain CreateSwapChain(SwapChainDesc desc)
{
throw new NotImplementedException();
}
public void EndFrame(ulong completedFrame)
{
}
public ICommandBuffer GetPooledCommandBuffer(CommandBufferType type = CommandBufferType.Graphics)
{
return new MockingCommandBuffer(_resourceDatabase, type);
}
public void ReturnPooledCommandBuffer(ICommandBuffer commandBuffer)
{
}
public void Dispose()
{
_resourceAllocator.Dispose();
_resourceDatabase.Dispose();
_renderDevice.Dispose();
}
}

View File

@@ -0,0 +1,27 @@
using Ghost.Graphics.RHI;
namespace Ghost.UnitTest.MockingEnvironment;
internal class MockingRenderDevice : IRenderDevice
{
private readonly MockingCommandQueue _commandQueue = new MockingCommandQueue(CommandQueueType.Graphics);
private readonly MockingCommandQueue _computeCommandQueue = new MockingCommandQueue(CommandQueueType.Compute);
private readonly MockingCommandQueue _copyCommandQueue = new MockingCommandQueue(CommandQueueType.Copy);
public ICommandQueue GraphicsQueue => _commandQueue;
public ICommandQueue ComputeQueue => _computeCommandQueue;
public ICommandQueue CopyQueue => _copyCommandQueue;
public FeatureSupport FeatureSupport => (FeatureSupport)~0;
public string Name
{
get; set;
} = "MockingRenderDevice";
public void Dispose()
{
}
}

View File

@@ -21,7 +21,6 @@ internal unsafe class MockingResourceDatabase : IResourceDatabase
private int _samplerToken = 0;
private static ulong GetKey(Handle<GPUResource> handle) => ((ulong)handle.Generation << 32) | (uint)handle.ID;
private static ulong GetKey<T>(Handle<T> handle) where T : unmanaged => ((ulong)handle.Generation << 32) | (uint)handle.ID;
public Handle<GPUResource> AddMockResource(ResourceDesc desc, ResourceBarrierData barrierData, string? name)
{
@@ -88,7 +87,7 @@ internal unsafe class MockingResourceDatabase : IResourceDatabase
public ulong GetIntermediateResourceSize(Handle<GPUResource> resource, uint firstSubResource, uint numSubResources)
{
return 1024 * 1024; // Mock size 1MB
return 0; // For testing, we can return 0 because we don't actually allocate memory.
}
public Result<ResourceBarrierData, Error> GetResourceBarrierData(Handle<GPUResource> handle)
@@ -151,9 +150,15 @@ internal unsafe class MockingResourceDatabase : IResourceDatabase
public Handle<GPUResource> Replace(Handle<GPUResource> dst, Handle<GPUResource> src)
{
// For tests, replacing means taking the new handle (src) and disposing the old (dst)
ReleaseResource(dst);
return src;
if (_resources.TryGetValue(GetKey(dst), out var recordDst) &&
_resources.TryGetValue(GetKey(src), out var recordSrc))
{
_resources[GetKey(dst)] = recordSrc;
_resources[GetKey(src)] = recordDst;
}
ReleaseResource(src);
return dst;
}
public Error SetResourceBarrierData(Handle<GPUResource> handle, ResourceBarrierData data)
@@ -164,8 +169,10 @@ internal unsafe class MockingResourceDatabase : IResourceDatabase
{
record.barrierData = data;
}
return Error.None;
}
return Error.NotFound;
}