From e7fedfd35ab7514a4d130cb7a864d00507cc7119 Mon Sep 17 00:00:00 2001 From: Misaki Date: Sat, 2 May 2026 22:54:58 +0900 Subject: [PATCH] Update asset system for deferred allocation & add unit tests Modernize Misaki.HighPerformance dependencies. Refactor texture asset creation to use deferred resource slots via CreateEmpty(). Remove fallback resource fields and update texture resolution logic. Add CreateEmpty() to resource database interfaces. Introduce comprehensive unit tests with mocks for asset management. Enable unsafe code in tests. --- src/Runtime/Ghost.Core/Ghost.Core.csproj | 8 +- .../Ghost.Engine/AssetManager.Texture.cs | 4 +- src/Runtime/Ghost.Engine/AssetManager.cs | 17 +- .../ResourceStreamingProcessor.cs | 1 - .../D3D12ResourceDatabase.cs | 9 + .../Ghost.Graphics.RHI/IResourceDatabase.cs | 6 + .../AssetSystem/AssetManagerTest.cs | 60 +++++ src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj | 1 + .../MockingCommandBuffer.cs | 193 ++++++++++++++++ .../MockingContentProvider.cs | 95 ++++++++ .../MockingResourceAllocator.cs | 56 +++++ .../MockingResourceDatabase.cs | 206 ++++++++++++++++++ 12 files changed, 637 insertions(+), 19 deletions(-) create mode 100644 src/Test/Ghost.UnitTest/AssetSystem/AssetManagerTest.cs create mode 100644 src/Test/Ghost.UnitTest/MockingEnvironment/MockingCommandBuffer.cs create mode 100644 src/Test/Ghost.UnitTest/MockingEnvironment/MockingContentProvider.cs create mode 100644 src/Test/Ghost.UnitTest/MockingEnvironment/MockingResourceAllocator.cs create mode 100644 src/Test/Ghost.UnitTest/MockingEnvironment/MockingResourceDatabase.cs diff --git a/src/Runtime/Ghost.Core/Ghost.Core.csproj b/src/Runtime/Ghost.Core/Ghost.Core.csproj index d734a92..955be26 100644 --- a/src/Runtime/Ghost.Core/Ghost.Core.csproj +++ b/src/Runtime/Ghost.Core/Ghost.Core.csproj @@ -20,14 +20,14 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Runtime/Ghost.Engine/AssetManager.Texture.cs b/src/Runtime/Ghost.Engine/AssetManager.Texture.cs index 4bbb43a..afeb04f 100644 --- a/src/Runtime/Ghost.Engine/AssetManager.Texture.cs +++ b/src/Runtime/Ghost.Engine/AssetManager.Texture.cs @@ -34,7 +34,7 @@ internal partial class AssetEntry { // This will create a new slot in the database, but not allocation any GPU resource. // Everything in the slot will have the same value as the fallback texture, expect the slot will be marked as shared. - var handle = e._resourceDatabase.CreateShared(e._assetManager.FallbackTexture.AsResource()).AsTexture(); + var handle = e._resourceDatabase.CreateEmpty().AsTexture(); e.SetStorage(handle); }; @@ -155,7 +155,7 @@ internal partial class AssetManager { if (assetID == Guid.Empty) { - return _fallbackTexture; + return Handle.Invalid; } var entry = GetOrCreateEntry(assetID); diff --git a/src/Runtime/Ghost.Engine/AssetManager.cs b/src/Runtime/Ghost.Engine/AssetManager.cs index 770b754..22047c1 100644 --- a/src/Runtime/Ghost.Engine/AssetManager.cs +++ b/src/Runtime/Ghost.Engine/AssetManager.cs @@ -295,17 +295,9 @@ internal partial class AssetManager : IDisposable private readonly ConcurrentDictionary _entries; - // TODO - private Handle _fallbackTexture; - private Handle _fallbackNormalMap; - private Handle _fallbackMesh; - private Handle _fallbackMaterial; - public IContentProvider ContentProvider => _contentProvider; public ResourceStreamingProcessor StreamingProcessor => _streamingProcessor; - public Handle FallbackTexture => _fallbackTexture; - internal AssetManager(IResourceDatabase resourceDatabase, IContentProvider contentProvider, ResourceStreamingProcessor streamingProcessor, JobScheduler jobScheduler) { _resourceDatabase = resourceDatabase; @@ -378,10 +370,10 @@ internal partial class AssetManager : IDisposable for (var i = list.Count - 1; i >= 0; i--) { // This should create the entry and schedule the job on those assets does not have any dependency first. - var handle = GetOrCreateEntry(list[i]).LoadJobHandle; - Logger.DebugAssert(handle.IsValid); + var depHandle = GetOrCreateEntry(list[i]).LoadJobHandle; + Logger.DebugAssert(depHandle.IsValid); - depHandles.Add(handle); + depHandles.Add(depHandle); } dependency = _jobScheduler.CombineDependencies(depHandles); @@ -394,7 +386,8 @@ internal partial class AssetManager : IDisposable assetManager = this, }; - entry.SetLoadJobHandle(_jobScheduler.Schedule(ref job, JobPriority.Low, dependency)); // Use low priority to avoid blocking main thread critical tasks like rendering and physics. + var handle = _jobScheduler.Schedule(ref job, JobPriority.Low, dependency); + entry.SetLoadJobHandle(handle); // Use low priority to avoid blocking main thread critical tasks like rendering and physics. } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Runtime/Ghost.Engine/ResourceStreamingProcessor.cs b/src/Runtime/Ghost.Engine/ResourceStreamingProcessor.cs index 5db87b7..37bcafd 100644 --- a/src/Runtime/Ghost.Engine/ResourceStreamingProcessor.cs +++ b/src/Runtime/Ghost.Engine/ResourceStreamingProcessor.cs @@ -1,6 +1,5 @@ using Ghost.Core; using Ghost.Graphics; -using SharpCompress.Common; using System.Collections.Concurrent; namespace Ghost.Engine; diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs index 61f8155..069d922 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs @@ -442,6 +442,15 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase } } + public Handle CreateEmpty() + { + lock (_writeLock) + { + var id = _resources.Add(default, out var generation); + return new Handle(id, generation); + } + } + public void* MapResource(Handle handle, uint subResource, ResourceRange? readRange) { var r = GetResourceRecord(handle); diff --git a/src/Runtime/Ghost.Graphics.RHI/IResourceDatabase.cs b/src/Runtime/Ghost.Graphics.RHI/IResourceDatabase.cs index 0bde9b6..2eb8694 100644 --- a/src/Runtime/Ghost.Graphics.RHI/IResourceDatabase.cs +++ b/src/Runtime/Ghost.Graphics.RHI/IResourceDatabase.cs @@ -142,6 +142,12 @@ public unsafe interface IResourceDatabase : IDisposable /// The handle to the newly created shared resource. Handle CreateShared(Handle src); + /// + /// Creates a slot in the resource database that contains no GPU resource, and returns a handle to that slot. This can be used as a placeholder for a resource that will be created or assigned later, allowing for deferred resource creation and management. + /// + /// The handle to the newly created empty resource slot. + Handle CreateEmpty(); + /// /// Maps a subresource of a GPU resource for CPU access, specifying read and write ranges. /// diff --git a/src/Test/Ghost.UnitTest/AssetSystem/AssetManagerTest.cs b/src/Test/Ghost.UnitTest/AssetSystem/AssetManagerTest.cs new file mode 100644 index 0000000..4f27c5f --- /dev/null +++ b/src/Test/Ghost.UnitTest/AssetSystem/AssetManagerTest.cs @@ -0,0 +1,60 @@ +using Ghost.Engine; +using Ghost.UnitTest.MockingEnvironment; +using Misaki.HighPerformance.Jobs; + +namespace Ghost.UnitTest.AssetSystem; + +[TestClass] +public class AssetManagerTest +{ + private MockingResourceDatabase _resourceDatabase = null!; + private MockingResourceAllocator _resourceAllocator = null!; + private MockingCommandBuffer _commandBuffer = null!; + private MockingContentProvider _provider = null!; + + private ResourceStreamingProcessor _processor = null!; + private JobScheduler _jobScheduler = null!; + + private AssetManager _assetManager = null!; + + [TestInitialize] + public void Setup() + { + _resourceDatabase = new MockingResourceDatabase(); + _resourceAllocator = new MockingResourceAllocator(_resourceDatabase); + _commandBuffer = new MockingCommandBuffer(_resourceDatabase); + _provider = new MockingContentProvider(); + + _processor = new ResourceStreamingProcessor(); + + var schedulerDesc = new JobSchedulerDesc + { + ThreadCount = 1, + ThreadPriority = ThreadPriority.Normal, + DependencyChainCapacity = 1024, + }; + _jobScheduler = new JobScheduler(in schedulerDesc); + + _assetManager = new AssetManager(_resourceDatabase, _provider, _processor, _jobScheduler); + } + + [TestCleanup] + public void Cleanup() + { + _assetManager.Dispose(); + _jobScheduler.Dispose(); + _commandBuffer.Dispose(); + _resourceAllocator.Dispose(); + _resourceDatabase.Dispose(); + } + + [TestMethod] + public void AssetManager_GetsAssetSuccessfully() + { + var assetID = Guid.NewGuid(); + _provider.AddMockTexture(assetID, readDelayMs: Random.Shared.Next(10, 50)); + + var handle = _assetManager.ResolveTexture(assetID); + Assert.IsTrue(handle.IsValid); + } +} diff --git a/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj b/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj index 1a6cf36..31f656a 100644 --- a/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj +++ b/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj @@ -7,6 +7,7 @@ enable x64;x86;ARM64 win-x86;win-x64;win-arm64 + True diff --git a/src/Test/Ghost.UnitTest/MockingEnvironment/MockingCommandBuffer.cs b/src/Test/Ghost.UnitTest/MockingEnvironment/MockingCommandBuffer.cs new file mode 100644 index 0000000..67778fc --- /dev/null +++ b/src/Test/Ghost.UnitTest/MockingEnvironment/MockingCommandBuffer.cs @@ -0,0 +1,193 @@ +using Ghost.Core; +using Ghost.Core.Graphics; +using Ghost.Graphics.RHI; + +namespace Ghost.UnitTest.MockingEnvironment; + +internal class MockingCommandBuffer : ICommandBuffer +{ + private readonly IResourceDatabase _resourceDatabase; + + private string _name = "MockCommandBuffer"; + private bool _isEmpty = true; + + // Tracking properties for test assertions + public int DrawCallCount { get; private set; } + public int CopyCallCount { get; private set; } + public int UpdateSubResourcesCount { get; private set; } + + public CommandBufferType Type => default; + + public bool IsEmpty => _isEmpty; + + public string Name + { + get => _name; + set => _name = value; + } + + public MockingCommandBuffer(IResourceDatabase resourceDatabase) + { + _resourceDatabase = resourceDatabase; + } + + public void Barrier(params scoped ReadOnlySpan barrierDescs) + { + _isEmpty = false; + lock (this) + { + foreach (var desc in barrierDescs) + { + var data = new ResourceBarrierData + { + access = desc.AccessAfter, + layout = desc.LayoutAfter, + sync = desc.SyncAfter + }; + + _resourceDatabase.SetResourceBarrierData(desc.Resource, data); + } + } + } + + public void Begin(ICommandAllocator allocator) + { + _isEmpty = true; + DrawCallCount = 0; + CopyCallCount = 0; + UpdateSubResourcesCount = 0; + } + + public void BeginRenderPass(ReadOnlySpan rtDescs, ref readonly PassDepthStencilDesc depthDesc, bool allowUAVWrites = false) + { + _isEmpty = false; + } + + public void ClearDepthStencilView(Handle depthStencil, bool inlcludeDepth, bool includeStencil, float clearDepth = 1, byte clearStencil = 0) + { + _isEmpty = false; + } + + public void ClearRenderTargetView(Handle renderTarget, Color128 clearColor) + { + _isEmpty = false; + } + + public void CopyBuffer(Handle dest, Handle src, ulong destOffset = 0, ulong srcOffset = 0, ulong numBytes = 0) + { + _isEmpty = false; + CopyCallCount++; + } + + public void CopyTexture(Handle dst, TextureRegion? dstRegion, Handle src, TextureRegion? srcRegion) + { + _isEmpty = false; + CopyCallCount++; + } + + public void DispatchCompute(uint threadGroupCountX, uint threadGroupCountY, uint threadGroupCountZ) + { + _isEmpty = false; + } + + public void DispatchGraph(ref readonly DispatchGraphDesc desc) + { + _isEmpty = false; + } + + public void DispatchMesh(uint threadGroupCountX, uint threadGroupCountY, uint threadGroupCountZ) + { + _isEmpty = false; + } + + public void DispatchRay() + { + _isEmpty = false; + } + + public void Dispose() + { + } + + public void Draw(uint vertexCount, uint instanceCount = 1, uint startVertex = 0, uint startInstance = 0) + { + _isEmpty = false; + DrawCallCount++; + } + + public void DrawIndexed(uint indexCount, uint instanceCount = 1, uint startIndex = 0, int baseVertex = 0, uint startInstance = 0) + { + _isEmpty = false; + DrawCallCount++; + } + + public Result End() + { + return Result.Success(); + } + + public void EndRenderPass() + { + } + + public void ExecuteIndirect(ICommandSignature commandSignature, Handle argumentBuffer, ulong argumentOffset, Handle countBuffer, ulong countBufferOffset) + { + _isEmpty = false; + } + + public void SetConstantBufferView(uint slot, Handle buffer) + { + _isEmpty = false; + } + + public void SetGraphicsRoot32Constants(uint rootIndex, ReadOnlySpan constantBuffer, uint offsetIn32Bits = 0) + { + _isEmpty = false; + } + + public void SetIndexBuffer(Handle buffer, IndexType type, ulong offset = 0) + { + _isEmpty = false; + } + + public void SetPipelineState(Key128 pipelineKey) + { + _isEmpty = false; + } + + public void SetPrimitiveTopology(PrimitiveTopology topology) + { + _isEmpty = false; + } + + public void SetProgram(ref readonly SetProgramDesc desc) + { + _isEmpty = false; + } + + public void SetRenderTargets(ReadOnlySpan> renderTargets, Handle depthTarget) + { + _isEmpty = false; + } + + public void SetScissorRect(ScissorRectDesc rect) + { + _isEmpty = false; + } + + public void SetVertexBuffer(uint slot, Handle buffer, ulong offset = 0) + { + _isEmpty = false; + } + + public void SetViewport(ViewportDesc viewport) + { + _isEmpty = false; + } + + public void UpdateSubResources(Handle resource, Handle intermediate, params scoped ReadOnlySpan subResources) + { + _isEmpty = false; + UpdateSubResourcesCount++; + } +} diff --git a/src/Test/Ghost.UnitTest/MockingEnvironment/MockingContentProvider.cs b/src/Test/Ghost.UnitTest/MockingEnvironment/MockingContentProvider.cs new file mode 100644 index 0000000..450d734 --- /dev/null +++ b/src/Test/Ghost.UnitTest/MockingEnvironment/MockingContentProvider.cs @@ -0,0 +1,95 @@ +using Ghost.Core; +using Ghost.Engine; +using System.Collections.Concurrent; + +namespace Ghost.UnitTest.MockingEnvironment; + +internal class MockingContentProvider : IContentProvider +{ + public class MockAssetData + { + public AssetType type; + public byte[] data = Array.Empty(); + public Guid[] dependencies = Array.Empty(); + + // This is crucial for multi-threaded testing: we can inject random or fixed + // delays to ensure our locking and state machines actually get stressed. + public int readDelayMs = 0; + } + + private readonly ConcurrentDictionary _assets = new(); + + public void AddMockAsset(Guid guid, MockAssetData data) + { + _assets[guid] = data; + } + + /// + /// Helper method to create a valid dummy texture byte stream that the AssetEntry can parse. + /// + public unsafe void AddMockTexture(Guid guid, uint width = 4, uint height = 4, int readDelayMs = 0) + { + var header = new TextureContentHeader + { + width = width, + height = height, + bpc = 8, + mipLevels = 1, + dimension = 2, // Texture2D + colorComponents = 4 + }; + + // Header size is strictly 64 bytes due to [StructLayout(LayoutKind.Sequential, Size = 64)] + var headerSize = 64; + var pixelDataSize = width * height * 4; + + var buffer = new byte[headerSize + pixelDataSize]; + + fixed (byte* pBuffer = buffer) + { + *(TextureContentHeader*)pBuffer = header; + // The rest of the array remains 0 (black/transparent pixels) which is fine for tests + } + + AddMockAsset(guid, new MockAssetData + { + type = AssetType.Texture, + data = buffer, + readDelayMs = readDelayMs + }); + } + + public AssetType GetAssetType(Guid guid) + { + return _assets.TryGetValue(guid, out var asset) ? asset.type : AssetType.Unknown; + } + + public Guid[] GetDependencies(Guid guid) + { + return _assets.TryGetValue(guid, out var asset) ? asset.dependencies : Array.Empty(); + } + + public bool HasAsset(Guid guid) + { + return _assets.ContainsKey(guid); + } + + public Result OpenRead(Guid guid, CancellationToken token = default) + { + if (_assets.TryGetValue(guid, out var asset)) + { + if (asset.readDelayMs > 0) + { + // Inject our simulated I/O latency to widen race condition windows. + // In a real multi-threaded test, this forces the executing thread to yield + // and lets other threads interact with the AssetManager in the meantime. + Thread.Sleep(asset.readDelayMs); + } + + // Return a fast, in-memory stream representing our file + return Result.Success(new MemoryStream(asset.data, writable: false)); + } + + return Result.Failure($"Mock asset {guid} not found."); + } +} diff --git a/src/Test/Ghost.UnitTest/MockingEnvironment/MockingResourceAllocator.cs b/src/Test/Ghost.UnitTest/MockingEnvironment/MockingResourceAllocator.cs new file mode 100644 index 0000000..900a478 --- /dev/null +++ b/src/Test/Ghost.UnitTest/MockingEnvironment/MockingResourceAllocator.cs @@ -0,0 +1,56 @@ +using Ghost.Core; +using Ghost.Graphics.RHI; + +namespace Ghost.UnitTest.MockingEnvironment; + +internal class MockingResourceAllocator : IResourceAllocator +{ + private readonly MockingResourceDatabase _database; + + public MockingResourceAllocator(MockingResourceDatabase database) + { + _database = database; + } + + public Handle Allocate(ref readonly AllocationDesc desc, string? name = null) + { + var barrier = new ResourceBarrierData { layout = BarrierLayout.Common, access = BarrierAccess.NoAccess, sync = BarrierSync.None }; + // Passing a mock buffer desc for raw allocation representation + var bufferDesc = new BufferDesc { Size = desc.Size, Usage = BufferUsage.None }; + return _database.AddMockResource(ResourceDesc.Buffer(bufferDesc), barrier, name); + } + + public Handle CreateBuffer(ref readonly BufferDesc desc, string? name = null, CreationOptions options = default) + { + var barrier = new ResourceBarrierData { layout = BarrierLayout.Undefined, access = BarrierAccess.Common, sync = BarrierSync.None }; + var handle = _database.AddMockResource(ResourceDesc.Buffer(desc), barrier, name); + return handle.AsBuffer(); + } + + public Identifier CreateSampler(ref readonly SamplerDesc desc) + { + return _database.AddSampler(in desc, 1); + } + + public Handle CreateTexture(ref readonly TextureDesc desc, string? name = null, CreationOptions options = default, AdditionalTextureDesc additionalDesc = default) + { + var barrier = new ResourceBarrierData { layout = BarrierLayout.Common, access = BarrierAccess.Common, sync = BarrierSync.None }; + var handle = _database.AddMockResource(ResourceDesc.Texture(desc), barrier, name); + return handle.AsTexture(); + } + + public ResourceSizeInfo GetSizeInfo(ResourceDesc desc) + { + return new ResourceSizeInfo + { + Size = 1048576, // 1MB mock + Alignment = 65536, // 64KB aligned + Offset = 0 + }; + } + + public void Dispose() + { + // Handled by dependency injection usually. + } +} diff --git a/src/Test/Ghost.UnitTest/MockingEnvironment/MockingResourceDatabase.cs b/src/Test/Ghost.UnitTest/MockingEnvironment/MockingResourceDatabase.cs new file mode 100644 index 0000000..e7b309e --- /dev/null +++ b/src/Test/Ghost.UnitTest/MockingEnvironment/MockingResourceDatabase.cs @@ -0,0 +1,206 @@ +using Ghost.Core; +using Ghost.Graphics.RHI; +using System.Collections.Concurrent; + +namespace Ghost.UnitTest.MockingEnvironment; + +internal unsafe class MockingResourceDatabase : IResourceDatabase +{ + internal class MockResourceRecord + { + public ResourceDesc desc; + public ResourceBarrierData barrierData; + public string? name; + public int refCount = 1; + public bool isShared; + } + + private readonly ConcurrentDictionary _resources = new(); + private readonly ConcurrentDictionary, SamplerDesc> _samplers = new(); + private int _nextToken = 0; + private int _samplerToken = 0; + + private static ulong GetKey(Handle handle) => ((ulong)handle.Generation << 32) | (uint)handle.ID; + private static ulong GetKey(Handle handle) where T : unmanaged => ((ulong)handle.Generation << 32) | (uint)handle.ID; + + public Handle AddMockResource(ResourceDesc desc, ResourceBarrierData barrierData, string? name) + { + var id = Interlocked.Increment(ref _nextToken); + var generation = 1; + var handle = new Handle(id, generation); + + _resources.TryAdd(GetKey(handle), new MockResourceRecord + { + desc = desc, + barrierData = barrierData, + name = name + }); + + return handle; + } + + public Identifier AddSampler(ref readonly SamplerDesc desc, int id) + { + var newId = new Identifier(id); + _samplers.TryAdd(newId, desc); + return newId; + } + + public Handle CreateShared(Handle src) + { + if (_resources.TryGetValue(GetKey(src), out var record)) + { + lock (record) + { + record.refCount++; + record.isShared = true; + } + + // To simulate sharing, we create a new handle mapping to the same conceptual resource. + // For simplicity, we just clone the dict entry with a new ID + var id = Interlocked.Increment(ref _nextToken); + var generation = 1; + var handle = new Handle(id, generation); + + _resources.TryAdd(GetKey(handle), record); + return handle; + } + + return Handle.Invalid; + } + + public Handle CreateEmpty() + { + var id = Interlocked.Increment(ref _nextToken); + var generation = 1; + var handle = new Handle(id, generation); + + _resources.TryAdd(GetKey(handle), new MockResourceRecord()); + + return handle; + } + + public uint GetBindlessIndex(Handle handle, BindlessAccess access = BindlessAccess.ShaderResource) + { + // Mock bindless index + return (uint)handle.ID; + } + + public ulong GetIntermediateResourceSize(Handle resource, uint firstSubResource, uint numSubResources) + { + return 1024 * 1024; // Mock size 1MB + } + + public Result GetResourceBarrierData(Handle handle) + { + if (_resources.TryGetValue(GetKey(handle), out var record)) + return record.barrierData; + return Error.NotFound; + } + + public Result GetResourceDescription(Handle handle) + { + if (_resources.TryGetValue(GetKey(handle), out var record)) + return record.desc; + return Error.NotFound; + } + + public string? GetResourceName(Handle handle) + { + if (_resources.TryGetValue(GetKey(handle), out var record)) + return record.name; + return null; + } + + public bool HasResource(Handle handle) + { + return _resources.ContainsKey(GetKey(handle)); + } + + public void* MapResource(Handle handle, uint subResource, ResourceRange? readRange) + { + // Real pointers are tricky in mocks unless native mem is allocated. + // Usually unit tests don't do CPU readbacks directly on the raw pointer unless necessary. + throw new NotSupportedException("MapResource is not supported in MockingResourceDatabase. Use a custom mechanism for tests."); + } + + public void ReleaseResource(Handle handle) + { + ReleaseResourceImmediately(handle); // Simplified for testing + } + + public void ReleaseResourceImmediately(Handle handle) + { + if (_resources.TryGetValue(GetKey(handle), out var record)) + { + lock (record) + { + record.refCount--; + if (record.refCount <= 0) + { + _resources.TryRemove(GetKey(handle), out _); + } + } + } + } + + public void ReleaseSampler(Identifier id) + { + _samplers.TryRemove(id, out _); + } + + public Handle Replace(Handle dst, Handle src) + { + // For tests, replacing means taking the new handle (src) and disposing the old (dst) + ReleaseResource(dst); + return src; + } + + public Error SetResourceBarrierData(Handle handle, ResourceBarrierData data) + { + if (_resources.TryGetValue(GetKey(handle), out var record)) + { + lock (record) + { + record.barrierData = data; + } + return Error.None; + } + return Error.NotFound; + } + + public Error Swap(Handle handleA, Handle handleB) + { + if (_resources.TryGetValue(GetKey(handleA), out var recordA) && + _resources.TryGetValue(GetKey(handleB), out var recordB)) + { + _resources[GetKey(handleA)] = recordB; + _resources[GetKey(handleB)] = recordA; + return Error.None; + } + return Error.NotFound; + } + + public bool TryGetSampler(ref readonly SamplerDesc desc, out Identifier id) + { + foreach (var kvp in _samplers) + { + // Simple generic mock check + id = kvp.Key; + return true; + } + id = default; + return false; + } + + public Error UnmapResource(Handle handle, uint subResource, ResourceRange? writtenRange) + { + return Error.None; + } + + public void Dispose() + { + _resources.Clear(); + _samplers.Clear(); + } +}