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:
@@ -8,26 +8,26 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
<DefineConstants>$(DefineConstants);MHP_ENABLE_SAFETY_CHECKS;MHP_ENABLE_MIMALLOC</DefineConstants>
|
<DefineConstants>$(DefineConstants);MHP_ENABLE_SAFETY_CHECKS;MHP_ENABLE_STACKTRACE;MHP_ENABLE_MIMALLOC;MHP_FASTMATH</DefineConstants>
|
||||||
<IsAotCompatible>True</IsAotCompatible>
|
<IsAotCompatible>True</IsAotCompatible>
|
||||||
<IsTrimmable>True</IsTrimmable>
|
<IsTrimmable>True</IsTrimmable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||||
<DefineConstants>$(DefineConstants);MHP_ENABLE_MIMALLOC</DefineConstants>
|
<DefineConstants>$(DefineConstants);MHP_ENABLE_MIMALLOC;MHP_FASTMATH</DefineConstants>
|
||||||
<IsAotCompatible>True</IsAotCompatible>
|
<IsAotCompatible>True</IsAotCompatible>
|
||||||
<IsTrimmable>True</IsTrimmable>
|
<IsTrimmable>True</IsTrimmable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Misaki.HighPerformance" Version="1.0.9" />
|
<PackageReference Include="Misaki.HighPerformance" Version="1.0.9" />
|
||||||
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="3.1.3" />
|
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="3.1.5" />
|
||||||
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.17">
|
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.17">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.3.3" />
|
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.3.3" />
|
||||||
<PackageReference Include="Misaki.HighPerformance.Mathematics.SPMD" Version="1.3.4" />
|
<PackageReference Include="Misaki.HighPerformance.Mathematics.SPMD" Version="1.3.5" />
|
||||||
<PackageReference Include="System.IO.Hashing" Version="10.0.7" />
|
<PackageReference Include="System.IO.Hashing" Version="10.0.7" />
|
||||||
<PackageReference Include="TerraFX.Interop.Mimalloc" Version="1.6.7.2" />
|
<PackageReference Include="TerraFX.Interop.Mimalloc" Version="1.6.7.2" />
|
||||||
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" />
|
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" />
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
using Ghost.Core.Utilities;
|
using Ghost.Core.Utilities;
|
||||||
using Ghost.Graphics;
|
using Ghost.Graphics;
|
||||||
using Ghost.Graphics.Core;
|
|
||||||
using Ghost.Graphics.RHI;
|
using Ghost.Graphics.RHI;
|
||||||
using Misaki.HighPerformance.Jobs;
|
using Misaki.HighPerformance.Jobs;
|
||||||
using Misaki.HighPerformance.LowLevel;
|
using Misaki.HighPerformance.LowLevel;
|
||||||
@@ -54,7 +53,7 @@ internal partial class AssetEntry
|
|||||||
{
|
{
|
||||||
private static readonly Action<AssetEntry>[] s_onCreation = new Action<AssetEntry>[(int)AssetType.Unknown + 1];
|
private static readonly Action<AssetEntry>[] s_onCreation = new Action<AssetEntry>[(int)AssetType.Unknown + 1];
|
||||||
private static readonly Func<AssetEntry, Result>[] s_onParseRawData = new Func<AssetEntry, Result>[(int)AssetType.Unknown + 1];
|
private static readonly Func<AssetEntry, Result>[] s_onParseRawData = new Func<AssetEntry, Result>[(int)AssetType.Unknown + 1];
|
||||||
private static readonly Action<AssetEntry, ResourceStreamingContext>[] s_onRecordUpload = new Action<AssetEntry, ResourceStreamingContext>[(int)AssetType.Unknown + 1];
|
private static readonly Func<AssetEntry, ResourceStreamingContext, Result>[] s_onRecordUpload = new Func<AssetEntry, ResourceStreamingContext, Result>[(int)AssetType.Unknown + 1];
|
||||||
private static readonly Action<AssetEntry, ResourceStreamingContext>[] s_onUploadComplete = new Action<AssetEntry, ResourceStreamingContext>[(int)AssetType.Unknown + 1];
|
private static readonly Action<AssetEntry, ResourceStreamingContext>[] s_onUploadComplete = new Action<AssetEntry, ResourceStreamingContext>[(int)AssetType.Unknown + 1];
|
||||||
private static readonly Action<AssetEntry>[] s_onReleaseResource = new Action<AssetEntry>[(int)AssetType.Unknown + 1];
|
private static readonly Action<AssetEntry>[] s_onReleaseResource = new Action<AssetEntry>[(int)AssetType.Unknown + 1];
|
||||||
|
|
||||||
@@ -184,9 +183,9 @@ internal unsafe partial class AssetEntry
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void OnRecordUploadCommands(ResourceStreamingContext context)
|
public Result OnRecordUploadCommands(ResourceStreamingContext context)
|
||||||
{
|
{
|
||||||
s_onRecordUpload[(int)_assetType]?.Invoke(this, context);
|
return s_onRecordUpload[(int)_assetType]?.Invoke(this, context) ?? Result.Failure("Unsupported asset type.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
@@ -417,10 +416,10 @@ internal partial class AssetManager : IDisposable
|
|||||||
|
|
||||||
if (Interlocked.CompareExchange(ref entry.StateValue, (int)AssetState.Scheduled, (int)AssetState.Ready) == (int)AssetState.Ready)
|
if (Interlocked.CompareExchange(ref entry.StateValue, (int)AssetState.Scheduled, (int)AssetState.Ready) == (int)AssetState.Ready)
|
||||||
{
|
{
|
||||||
// Entry is in Ready state — the old texture is valid and will remain visible.
|
// Entry is in Ready state - the old texture is valid and will remain visible.
|
||||||
// Go directly to Scheduled → Loading → Loaded → Uploading → Ready again.
|
// Go directly to Scheduled -> Loading -> Loaded -> Uploading -> Ready again.
|
||||||
// The swap cycle in RecordTextureUpload/OnTextureUploadComplete handles the
|
// The swap cycle in RecordTextureUpload/OnTextureUploadComplete handles the
|
||||||
// v1 → v2 transition exactly like the fallback → v1 transition.
|
// v1 to v2 transition exactly like the fallback to v1 transition.
|
||||||
EnsureScheduled(entry);
|
EnsureScheduled(entry);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -62,17 +62,26 @@ internal class ResourceStreamingProcessor : IResourceStreamingProcessor
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Record copy commands into cmdCopy
|
// Record copy commands into cmdCopy
|
||||||
entry.OnRecordUploadCommands(context);
|
if (entry.OnRecordUploadCommands(context).IsFailure)
|
||||||
|
{
|
||||||
|
Logger.Error($"Failed to record upload commands for asset {entry.AssetId}. Skipping upload.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
entry.State = AssetState.Uploading;
|
entry.State = AssetState.Uploading;
|
||||||
|
|
||||||
_pendingFinalize.Enqueue(entry);
|
_pendingFinalize.Enqueue(entry);
|
||||||
uploadCount++;
|
uploadCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = context.CopyPipeline.End();
|
|
||||||
|
|
||||||
// 3. Submit the batch
|
// 3. Submit the batch
|
||||||
if (uploadCount > 0 && result.IsSuccess)
|
if (context.CopyPipeline.End().IsFailure)
|
||||||
|
{
|
||||||
|
Logger.Error("Failed to submit copy command list for resource streaming.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadCount > 0)
|
||||||
{
|
{
|
||||||
_pendingCopyFenceValue = context.CopyPipeline.SignaledFenceValue();
|
_pendingCopyFenceValue = context.CopyPipeline.SignaledFenceValue();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ using System.Runtime.CompilerServices;
|
|||||||
[assembly: InternalsVisibleTo("Ghost.Editor")]
|
[assembly: InternalsVisibleTo("Ghost.Editor")]
|
||||||
[assembly: InternalsVisibleTo("Ghost.Editor.Core")]
|
[assembly: InternalsVisibleTo("Ghost.Editor.Core")]
|
||||||
[assembly: InternalsVisibleTo("Ghost.Graphics.Test")]
|
[assembly: InternalsVisibleTo("Ghost.Graphics.Test")]
|
||||||
[assembly: InternalsVisibleTo("Ghost.Graphics.Test-Winui")]
|
[assembly: InternalsVisibleTo("Ghost.UnitTest")]
|
||||||
|
|
||||||
[assembly: EngineAssembly]
|
[assembly: EngineAssembly]
|
||||||
@@ -3,7 +3,7 @@ using Ghost.Graphics.Services;
|
|||||||
|
|
||||||
namespace Ghost.Graphics;
|
namespace Ghost.Graphics;
|
||||||
|
|
||||||
internal ref struct ResourceStreamingContext
|
internal readonly struct ResourceStreamingContext
|
||||||
{
|
{
|
||||||
public required AsyncCopyPipeline CopyPipeline
|
public required AsyncCopyPipeline CopyPipeline
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -475,8 +475,12 @@ public class RenderSystem : IDisposable
|
|||||||
|
|
||||||
_renderPipeline.Dispose();
|
_renderPipeline.Dispose();
|
||||||
|
|
||||||
|
_fence.Dispose();
|
||||||
|
|
||||||
|
_shaderLibrary.Dispose();
|
||||||
_resourceManager.Dispose();
|
_resourceManager.Dispose();
|
||||||
_swapChainManager.Dispose();
|
_swapChainManager.Dispose();
|
||||||
|
_asyncCopyPipeline.Dispose();
|
||||||
|
|
||||||
_graphicsEngine.Dispose();
|
_graphicsEngine.Dispose();
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using Ghost.Graphics.RHI;
|
|||||||
|
|
||||||
namespace Ghost.Graphics.Services;
|
namespace Ghost.Graphics.Services;
|
||||||
|
|
||||||
public class AsyncCopyPipeline
|
public class AsyncCopyPipeline : IDisposable
|
||||||
{
|
{
|
||||||
private readonly IRenderDevice _device;
|
private readonly IRenderDevice _device;
|
||||||
|
|
||||||
@@ -26,6 +26,11 @@ public class AsyncCopyPipeline
|
|||||||
_fence.Name = "AsyncCopyPipeline_Fence";
|
_fence.Name = "AsyncCopyPipeline_Fence";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~AsyncCopyPipeline()
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
internal void Begin()
|
internal void Begin()
|
||||||
{
|
{
|
||||||
_commandAllocator.Reset();
|
_commandAllocator.Reset();
|
||||||
@@ -74,4 +79,13 @@ public class AsyncCopyPipeline
|
|||||||
{
|
{
|
||||||
return _fence.WaitForValueAsync(_fenceValue);
|
return _fence.WaitForValueAsync(_fenceValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_commandAllocator.Dispose();
|
||||||
|
_commandBuffer.Dispose();
|
||||||
|
_fence.Dispose();
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -592,6 +592,7 @@ public sealed partial class ResourceManager : IDisposable
|
|||||||
_meshes.Dispose();
|
_meshes.Dispose();
|
||||||
_materials.Dispose();
|
_materials.Dispose();
|
||||||
_shaders.Dispose();
|
_shaders.Dispose();
|
||||||
|
_computeShaders.Dispose();
|
||||||
_materialPalettes.Dispose();
|
_materialPalettes.Dispose();
|
||||||
|
|
||||||
_resourceDatabase.ReleaseResource(_paletteOffsetBuffer.AsResource());
|
_resourceDatabase.ReleaseResource(_paletteOffsetBuffer.AsResource());
|
||||||
|
|||||||
@@ -1,41 +1,56 @@
|
|||||||
|
using Ghost.Core;
|
||||||
using Ghost.Engine;
|
using Ghost.Engine;
|
||||||
|
using Ghost.Graphics;
|
||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Ghost.Graphics.Services;
|
||||||
using Ghost.UnitTest.MockingEnvironment;
|
using Ghost.UnitTest.MockingEnvironment;
|
||||||
using Misaki.HighPerformance.Jobs;
|
using Misaki.HighPerformance.Jobs;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
|
||||||
namespace Ghost.UnitTest.AssetSystem;
|
namespace Ghost.UnitTest.AssetSystem;
|
||||||
|
|
||||||
[TestClass]
|
[TestClass]
|
||||||
|
[DoNotParallelize]
|
||||||
public class AssetManagerTest
|
public class AssetManagerTest
|
||||||
{
|
{
|
||||||
private MockingResourceDatabase _resourceDatabase = null!;
|
private MockingGraphicsEngine _graphicsEngine = null!;
|
||||||
private MockingResourceAllocator _resourceAllocator = null!;
|
|
||||||
private MockingCommandBuffer _commandBuffer = null!;
|
private MockingCommandBuffer _commandBuffer = null!;
|
||||||
private MockingContentProvider _provider = null!;
|
private MockingContentProvider _provider = null!;
|
||||||
|
|
||||||
|
private AsyncCopyPipeline _copyPipeline = null!;
|
||||||
|
private ResourceManager _resourceManager = null!;
|
||||||
private ResourceStreamingProcessor _processor = null!;
|
private ResourceStreamingProcessor _processor = null!;
|
||||||
private JobScheduler _jobScheduler = null!;
|
private JobScheduler _jobScheduler = null!;
|
||||||
|
|
||||||
private AssetManager _assetManager = null!;
|
private AssetManager _assetManager = null!;
|
||||||
|
|
||||||
|
public TestContext TestContext
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
[TestInitialize]
|
[TestInitialize]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
_resourceDatabase = new MockingResourceDatabase();
|
AllocationManager.Initialize(AllocationManagerDesc.Default);
|
||||||
_resourceAllocator = new MockingResourceAllocator(_resourceDatabase);
|
|
||||||
_commandBuffer = new MockingCommandBuffer(_resourceDatabase);
|
_graphicsEngine = new MockingGraphicsEngine();
|
||||||
|
_commandBuffer = (MockingCommandBuffer)_graphicsEngine.CreateCommandBuffer();
|
||||||
_provider = new MockingContentProvider();
|
_provider = new MockingContentProvider();
|
||||||
|
|
||||||
|
_copyPipeline = new AsyncCopyPipeline(_graphicsEngine);
|
||||||
|
_resourceManager = new ResourceManager(_graphicsEngine.Device, _graphicsEngine.ResourceAllocator, _graphicsEngine.ResourceDatabase);
|
||||||
_processor = new ResourceStreamingProcessor();
|
_processor = new ResourceStreamingProcessor();
|
||||||
|
|
||||||
var schedulerDesc = new JobSchedulerDesc
|
var schedulerDesc = new JobSchedulerDesc
|
||||||
{
|
{
|
||||||
ThreadCount = 1,
|
ThreadCount = 1,
|
||||||
ThreadPriority = ThreadPriority.Normal,
|
ThreadPriority = ThreadPriority.Normal,
|
||||||
DependencyChainCapacity = 1024,
|
DependencyChainCapacity = 64,
|
||||||
};
|
};
|
||||||
_jobScheduler = new JobScheduler(in schedulerDesc);
|
_jobScheduler = new JobScheduler(in schedulerDesc);
|
||||||
|
|
||||||
_assetManager = new AssetManager(_resourceDatabase, _provider, _processor, _jobScheduler);
|
_assetManager = new AssetManager(_graphicsEngine.ResourceDatabase, _provider, _processor, _jobScheduler);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCleanup]
|
[TestCleanup]
|
||||||
@@ -43,18 +58,55 @@ public class AssetManagerTest
|
|||||||
{
|
{
|
||||||
_assetManager.Dispose();
|
_assetManager.Dispose();
|
||||||
_jobScheduler.Dispose();
|
_jobScheduler.Dispose();
|
||||||
|
_resourceManager.Dispose();
|
||||||
|
_copyPipeline.Dispose();
|
||||||
_commandBuffer.Dispose();
|
_commandBuffer.Dispose();
|
||||||
_resourceAllocator.Dispose();
|
_graphicsEngine.Dispose();
|
||||||
_resourceDatabase.Dispose();
|
|
||||||
|
AllocationManager.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void AssetManager_GetsAssetSuccessfully()
|
public async Task AssetManager_ResolveTextureThenBackgroundUpload()
|
||||||
{
|
{
|
||||||
var assetID = Guid.NewGuid();
|
var assetID = Guid.NewGuid();
|
||||||
_provider.AddMockTexture(assetID, readDelayMs: Random.Shared.Next(10, 50));
|
_provider.AddMockTexture(assetID, readDelayMs: Random.Shared.Next(10, 50));
|
||||||
|
|
||||||
var handle = _assetManager.ResolveTexture(assetID);
|
var handle = _assetManager.ResolveTexture(assetID);
|
||||||
Assert.IsTrue(handle.IsValid);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ internal class MockingCommandBuffer : ICommandBuffer
|
|||||||
{
|
{
|
||||||
private readonly IResourceDatabase _resourceDatabase;
|
private readonly IResourceDatabase _resourceDatabase;
|
||||||
|
|
||||||
private string _name = "MockCommandBuffer";
|
|
||||||
private bool _isEmpty = true;
|
private bool _isEmpty = true;
|
||||||
|
|
||||||
// Tracking properties for test assertions
|
// Tracking properties for test assertions
|
||||||
@@ -16,19 +15,22 @@ internal class MockingCommandBuffer : ICommandBuffer
|
|||||||
public int CopyCallCount { get; private set; }
|
public int CopyCallCount { get; private set; }
|
||||||
public int UpdateSubResourcesCount { get; private set; }
|
public int UpdateSubResourcesCount { get; private set; }
|
||||||
|
|
||||||
public CommandBufferType Type => default;
|
|
||||||
|
|
||||||
public bool IsEmpty => _isEmpty;
|
public bool IsEmpty => _isEmpty;
|
||||||
|
|
||||||
|
public CommandBufferType Type
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
public string Name
|
public string Name
|
||||||
{
|
{
|
||||||
get => _name;
|
get; set;
|
||||||
set => _name = value;
|
} = "MockingCommandBuffer";
|
||||||
}
|
|
||||||
|
|
||||||
public MockingCommandBuffer(IResourceDatabase resourceDatabase)
|
public MockingCommandBuffer(IResourceDatabase resourceDatabase, CommandBufferType type)
|
||||||
{
|
{
|
||||||
_resourceDatabase = resourceDatabase;
|
_resourceDatabase = resourceDatabase;
|
||||||
|
Type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Barrier(params scoped ReadOnlySpan<BarrierDesc> barrierDescs)
|
public void Barrier(params scoped ReadOnlySpan<BarrierDesc> barrierDescs)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/Test/Ghost.UnitTest/MockingEnvironment/MockingFence.cs
Normal file
52
src/Test/Ghost.UnitTest/MockingEnvironment/MockingFence.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ internal unsafe class MockingResourceDatabase : IResourceDatabase
|
|||||||
private int _samplerToken = 0;
|
private int _samplerToken = 0;
|
||||||
|
|
||||||
private static ulong GetKey(Handle<GPUResource> handle) => ((ulong)handle.Generation << 32) | (uint)handle.ID;
|
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)
|
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)
|
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)
|
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)
|
public Handle<GPUResource> Replace(Handle<GPUResource> dst, Handle<GPUResource> src)
|
||||||
{
|
{
|
||||||
// For tests, replacing means taking the new handle (src) and disposing the old (dst)
|
if (_resources.TryGetValue(GetKey(dst), out var recordDst) &&
|
||||||
ReleaseResource(dst);
|
_resources.TryGetValue(GetKey(src), out var recordSrc))
|
||||||
return src;
|
{
|
||||||
|
_resources[GetKey(dst)] = recordSrc;
|
||||||
|
_resources[GetKey(src)] = recordDst;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReleaseResource(src);
|
||||||
|
return dst;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Error SetResourceBarrierData(Handle<GPUResource> handle, ResourceBarrierData data)
|
public Error SetResourceBarrierData(Handle<GPUResource> handle, ResourceBarrierData data)
|
||||||
@@ -164,8 +169,10 @@ internal unsafe class MockingResourceDatabase : IResourceDatabase
|
|||||||
{
|
{
|
||||||
record.barrierData = data;
|
record.barrierData = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Error.None;
|
return Error.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Error.NotFound;
|
return Error.NotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user