Add sub-asset import and mesh asset support
- Implement sub-asset import for mesh/model assets with manifest generation and deterministic GUIDs - Extend AssetCatalog for sub-asset tracking and management - Update AssetRegistry and ImportCoordinator for sub-asset workflows - Add mesh asset parsing, GPU upload, and resource management - Update mesh data structures for meshlet groups/hierarchy and LODs - Improve tests for sub-asset import and mesh handling - Enhance mocks for mesh asset testing and resource mapping - Fix path handling and native DLL loading issues - Miscellaneous bug fixes and refactoring
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using Ghost.MicroTest;
|
||||
using Ghost.Test.Core;
|
||||
|
||||
TestRunner.Run<NvttBindingTest>();
|
||||
TestRunner.Run<UfbxBindingTest>();
|
||||
@@ -1,3 +1,4 @@
|
||||
using Ghost.Editor.Core;
|
||||
using Ghost.Editor.Core.Assets;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Ghost.Editor.Core.Services;
|
||||
@@ -7,16 +8,21 @@ namespace Ghost.UnitTest.AssetSystem;
|
||||
[TestClass]
|
||||
public class AssertRegistryTest
|
||||
{
|
||||
private string _assetsRoot = null!;
|
||||
private IAssetRegistry _registry = null!;
|
||||
|
||||
public TestContext TestContext
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
var testDir = Path.Combine(Path.GetTempPath(), "GhostEngineTests", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(testDir);
|
||||
|
||||
_assetsRoot = Path.Combine(testDir, "Assets");
|
||||
|
||||
EditorApplication.Initialize(null!, testDir, "Test");
|
||||
|
||||
_registry = new AssetRegistry();
|
||||
}
|
||||
|
||||
@@ -29,16 +35,20 @@ public class AssertRegistryTest
|
||||
[TestMethod]
|
||||
public async Task TestAssetRegistry_AutoImport()
|
||||
{
|
||||
var sourcePath = "test.text";
|
||||
var fullSourcePath = Path.Combine(_assetsRoot, sourcePath);
|
||||
await File.WriteAllBytesAsync(fullSourcePath, [1, 2, 3]);
|
||||
|
||||
await Task.Delay(1000); // Wait for FSW to trigger
|
||||
var sourcePath = "Assets/test.text";
|
||||
await File.WriteAllBytesAsync(sourcePath, [1, 2, 3], TestContext.CancellationToken);
|
||||
|
||||
var metaPath = AssetMetaIO.GetMetaPath(fullSourcePath);
|
||||
var metaPath = AssetMetaIO.GetMetaPath(sourcePath);
|
||||
|
||||
using var cts = new CancellationTokenSource(5000);
|
||||
while (!File.Exists(metaPath) && !cts.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(50, cts.Token);
|
||||
}
|
||||
|
||||
Assert.IsTrue(File.Exists(metaPath));
|
||||
|
||||
var meta = await AssetMetaIO.ReadAsync(metaPath);
|
||||
var meta = await AssetMetaIO.ReadAsync(metaPath, TestContext.CancellationToken);
|
||||
Assert.IsNotNull(meta);
|
||||
|
||||
var guid = _registry.GetAssetGuid(sourcePath);
|
||||
|
||||
@@ -49,7 +49,7 @@ public class AssetCatalogTests
|
||||
catalog.Upsert(meta, path);
|
||||
|
||||
Assert.AreEqual(guid, catalog.GetGuid(path));
|
||||
Assert.AreEqual(path, catalog.GetSourcePath(guid));
|
||||
Assert.AreEqual(Path.GetFullPath(path).Replace('\\', '/'), catalog.GetSourcePath(guid));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -68,4 +68,35 @@ public class AssetCatalogTests
|
||||
Assert.AreEqual(1, referencers.Count);
|
||||
Assert.AreEqual(asset1, referencers[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestAssetCatalog_VirtualSubAssets()
|
||||
{
|
||||
using var catalog = new AssetCatalog(_dbPath);
|
||||
var parent = Guid.NewGuid();
|
||||
var subMesh = Guid.NewGuid();
|
||||
var handlerTypeId = Guid.NewGuid();
|
||||
|
||||
catalog.Upsert(new AssetMeta { Guid = parent, HandlerTypeId = handlerTypeId, HandlerVersion = 1 }, "Props/kit.fbx");
|
||||
catalog.UpsertSubAsset(parent,
|
||||
new AssetMeta { Guid = subMesh, HandlerTypeId = handlerTypeId, HandlerVersion = 1 },
|
||||
"Props/kit.fbx#Mesh/Root/Crate",
|
||||
"Mesh",
|
||||
"Crate",
|
||||
"Root/Crate");
|
||||
catalog.SetDependencies(parent, stackalloc[] { subMesh });
|
||||
|
||||
Assert.AreEqual(subMesh, catalog.GetGuid("Props/kit.fbx#Mesh/Root/Crate"));
|
||||
var subAssets = catalog.GetSubAssets(parent);
|
||||
Assert.AreEqual(1, subAssets.Count);
|
||||
Assert.AreEqual(subMesh, subAssets[0].Guid);
|
||||
Assert.AreEqual(parent, subAssets[0].ParentGuid);
|
||||
Assert.AreEqual("Mesh", subAssets[0].Kind);
|
||||
Assert.AreEqual("Crate", subAssets[0].DisplayName);
|
||||
Assert.AreEqual("Root/Crate", subAssets[0].StablePath);
|
||||
|
||||
var dependencies = catalog.GetDependencies(parent);
|
||||
Assert.AreEqual(1, dependencies.Count);
|
||||
Assert.AreEqual(subMesh, dependencies[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public class AssetManagerTest
|
||||
};
|
||||
_jobScheduler = new JobScheduler(in schedulerDesc);
|
||||
|
||||
_assetManager = new AssetManager(_graphicsEngine.ResourceDatabase, _provider, _processor, _jobScheduler);
|
||||
_assetManager = new AssetManager(_graphicsEngine.ResourceDatabase, _resourceManager, _provider, _processor, _jobScheduler);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
@@ -109,4 +109,89 @@ public class AssetManagerTest
|
||||
Assert.AreEqual(BarrierLayout.ShaderResource, data.layout);
|
||||
Assert.AreEqual(BarrierSync.AllShading, data.sync);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task AssetManager_ResolveMeshThenBackgroundUpload()
|
||||
{
|
||||
var assetID = Guid.NewGuid();
|
||||
_provider.AddMockMesh(assetID, readDelayMs: Random.Shared.Next(10, 50));
|
||||
|
||||
var handle = _assetManager.ResolveMesh(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);
|
||||
|
||||
_processor.ProcessPendingUploads(ctx);
|
||||
|
||||
Assert.IsGreaterThanOrEqualTo((int)AssetState.Ready, entry.StateValue);
|
||||
Assert.IsTrue(_resourceManager.HasMesh(handle));
|
||||
|
||||
ref readonly var mesh = ref _resourceManager.GetMeshReference(handle).GetValueOrThrow();
|
||||
var (vertexBarrier, vertexError) = _graphicsEngine.ResourceDatabase.GetResourceBarrierData(mesh.VertexBuffer.AsResource());
|
||||
var (indexBarrier, indexError) = _graphicsEngine.ResourceDatabase.GetResourceBarrierData(mesh.IndexBuffer.AsResource());
|
||||
var (meshDataBarrier, meshDataError) = _graphicsEngine.ResourceDatabase.GetResourceBarrierData(mesh.MeshDataBuffer.AsResource());
|
||||
|
||||
Assert.AreEqual(Error.None, vertexError);
|
||||
Assert.AreEqual(Error.None, indexError);
|
||||
Assert.AreEqual(Error.None, meshDataError);
|
||||
Assert.IsTrue(vertexBarrier.access.HasFlag(BarrierAccess.VertexBuffer));
|
||||
Assert.IsTrue(indexBarrier.access.HasFlag(BarrierAccess.IndexBuffer));
|
||||
Assert.AreEqual(BarrierAccess.ShaderResource, meshDataBarrier.access);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task AssetManager_ReimportMeshKeepsStableHandle()
|
||||
{
|
||||
var assetID = Guid.NewGuid();
|
||||
_provider.AddMockMesh(assetID);
|
||||
|
||||
var handle = _assetManager.ResolveMesh(assetID);
|
||||
await Task.Delay(1000, TestContext.CancellationToken);
|
||||
|
||||
var ctx = new ResourceStreamingContext
|
||||
{
|
||||
ResourceManager = _resourceManager,
|
||||
ResourceDatabase = _graphicsEngine.ResourceDatabase,
|
||||
ResourceAllocator = _graphicsEngine.ResourceAllocator,
|
||||
CopyPipeline = _copyPipeline,
|
||||
GraphicsCommandBuffer = _commandBuffer,
|
||||
};
|
||||
|
||||
_processor.ProcessPendingUploads(ctx);
|
||||
_processor.ProcessPendingUploads(ctx);
|
||||
|
||||
Assert.IsTrue(_assetManager.TryGetEntry(assetID, out var entry));
|
||||
Assert.AreEqual(AssetState.Ready, entry.State);
|
||||
|
||||
_provider.AddMockMesh(assetID);
|
||||
_assetManager.ReimportAsset(assetID);
|
||||
await Task.Delay(1000, TestContext.CancellationToken);
|
||||
|
||||
_processor.ProcessPendingUploads(ctx);
|
||||
_processor.ProcessPendingUploads(ctx);
|
||||
|
||||
var reimportedHandle = _assetManager.ResolveMesh(assetID);
|
||||
Assert.AreEqual(handle, reimportedHandle);
|
||||
Assert.AreEqual(AssetState.Ready, entry.State);
|
||||
}
|
||||
}
|
||||
|
||||
145
src/Test/Ghost.UnitTest/AssetSystem/MeshAssetHandlerTests.cs
Normal file
145
src/Test/Ghost.UnitTest/AssetSystem/MeshAssetHandlerTests.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Editor.Core;
|
||||
using Ghost.Editor.Core.Assets;
|
||||
using Ghost.Editor.Core.Services;
|
||||
using Ghost.Engine;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Ghost.UnitTest.AssetSystem;
|
||||
|
||||
[TestClass]
|
||||
public class MeshAssetHandlerTests
|
||||
{
|
||||
private sealed class EmptyServiceProvider : IServiceProvider
|
||||
{
|
||||
public object? GetService(Type serviceType)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string _projectRoot = null!;
|
||||
private string _previousCurrentDirectory = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
AllocationManager.Initialize(AllocationManagerDesc.Default);
|
||||
|
||||
_previousCurrentDirectory = Environment.CurrentDirectory;
|
||||
_projectRoot = Path.Combine(Path.GetTempPath(), "GhostEngineTests", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(_projectRoot);
|
||||
EditorApplication.Initialize(new EmptyServiceProvider(), _projectRoot, "MeshImportTest");
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
AllocationManager.Dispose();
|
||||
Environment.CurrentDirectory = _previousCurrentDirectory;
|
||||
|
||||
if (Directory.Exists(_projectRoot))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(_projectRoot, true);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
if (Directory.Exists(_projectRoot))
|
||||
{
|
||||
Directory.Delete(_projectRoot, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task FBXAssetHandler_ImportsObjAsManifestAndMeshSubAssets()
|
||||
{
|
||||
var sourcePath = Path.Combine(EditorApplication.AssetsFolderPath, "kit.obj");
|
||||
await File.WriteAllTextAsync(sourcePath, CreateTwoObjectObj());
|
||||
|
||||
var parentGuid = Guid.NewGuid();
|
||||
var targetPath = ImportCoordinator.GetImportedAssetPath(parentGuid);
|
||||
var handler = new FBXAssetHandler();
|
||||
|
||||
var result = await handler.ImportWithSubAssetsAsync(sourcePath, targetPath, parentGuid, new ObjAssetSettings(), TestContext.CancellationToken);
|
||||
|
||||
if (result.IsFailure && result.Message?.Contains("Unable to load DLL", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
Assert.Inconclusive(result.Message);
|
||||
}
|
||||
|
||||
Assert.IsTrue(result.IsSuccess, result.Message);
|
||||
Assert.IsTrue(File.Exists(targetPath));
|
||||
Assert.IsGreaterThanOrEqualTo(result.Value.Length, 2);
|
||||
|
||||
foreach (var subAsset in result.Value)
|
||||
{
|
||||
Assert.AreEqual("Mesh", subAsset.Kind);
|
||||
Assert.IsTrue(subAsset.VirtualSourcePath.Contains("#Mesh/", StringComparison.Ordinal));
|
||||
|
||||
var meshPath = ImportCoordinator.GetImportedAssetPath(subAsset.Guid);
|
||||
Assert.IsTrue(File.Exists(meshPath));
|
||||
|
||||
await using var stream = new FileStream(meshPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var headerBytes = new byte[Marshal.SizeOf<MeshContentHeader>()];
|
||||
await stream.ReadExactlyAsync(headerBytes, TestContext.CancellationToken);
|
||||
|
||||
var header = MemoryMarshal.Read<MeshContentHeader>(headerBytes);
|
||||
Assert.AreEqual(MeshContentHeader.MAGIC, header.magic);
|
||||
Assert.AreEqual(MeshContentHeader.VERSION, header.version);
|
||||
Assert.IsGreaterThan(0u, header.vertexCount);
|
||||
Assert.IsGreaterThan(0u, header.indexCount);
|
||||
Assert.IsGreaterThan(0u, header.meshletCount);
|
||||
Assert.IsGreaterThan(0u, header.meshletGroupCount);
|
||||
Assert.IsGreaterThan(0u, header.meshletHierarchyNodeCount);
|
||||
}
|
||||
}
|
||||
|
||||
public TestContext TestContext
|
||||
{
|
||||
get; set;
|
||||
} = null!;
|
||||
|
||||
private static string CreateTwoObjectObj()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
AppendGrid(sb, "PropA", 0, 0);
|
||||
AppendGrid(sb, "PropB", 49, 1);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void AppendGrid(StringBuilder sb, string name, int vertexOffset, int z)
|
||||
{
|
||||
const int size = 6;
|
||||
sb.AppendLine($"o {name}");
|
||||
|
||||
for (var y = 0; y <= size; y++)
|
||||
{
|
||||
for (var x = 0; x <= size; x++)
|
||||
{
|
||||
sb.AppendLine($"v {x} {y} {z}");
|
||||
sb.AppendLine("vn 0 0 1");
|
||||
sb.AppendLine($"vt {x / (float)size} {y / (float)size}");
|
||||
}
|
||||
}
|
||||
|
||||
for (var y = 0; y < size; y++)
|
||||
{
|
||||
for (var x = 0; x < size; x++)
|
||||
{
|
||||
var i0 = vertexOffset + y * (size + 1) + x + 1;
|
||||
var i1 = i0 + 1;
|
||||
var i2 = i0 + size + 1;
|
||||
var i3 = i2 + 1;
|
||||
sb.AppendLine($"f {i0}/{i0}/{i0} {i1}/{i1}/{i1} {i2}/{i2}/{i2}");
|
||||
sb.AppendLine($"f {i1}/{i1}/{i1} {i3}/{i3}/{i3} {i2}/{i2}/{i2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Engine;
|
||||
using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Misaki.HighPerformance.Mathematics;
|
||||
using Misaki.HighPerformance.Mathematics.Geometry;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.UnitTest.MockingEnvironment;
|
||||
|
||||
@@ -59,6 +64,118 @@ internal class MockingContentProvider : IContentProvider
|
||||
});
|
||||
}
|
||||
|
||||
public void AddMockMesh(Guid guid, int readDelayMs = 0)
|
||||
{
|
||||
var vertices = new[]
|
||||
{
|
||||
new Vertex { position = new float3(0, 0, 0), normal = new float3(0, 1, 0), tangent = new float4(1, 0, 0, 1), uv = new float2(0, 0), color = new Color128(1, 1, 1, 1) },
|
||||
new Vertex { position = new float3(1, 0, 0), normal = new float3(0, 1, 0), tangent = new float4(1, 0, 0, 1), uv = new float2(1, 0), color = new Color128(1, 1, 1, 1) },
|
||||
new Vertex { position = new float3(0, 1, 0), normal = new float3(0, 1, 0), tangent = new float4(1, 0, 0, 1), uv = new float2(0, 1), color = new Color128(1, 1, 1, 1) },
|
||||
};
|
||||
var indices = new uint[] { 0, 1, 2 };
|
||||
var materialParts = new[]
|
||||
{
|
||||
new MeshContentMaterialPart { materialIndex = 0, indexStart = 0, indexCount = 3, vertexStart = 0, vertexCount = 3 }
|
||||
};
|
||||
var meshlets = new[]
|
||||
{
|
||||
new Meshlet
|
||||
{
|
||||
boundingSphere = new SphereBounds(new float3(0.5f, 0.5f, 0), 1.0f),
|
||||
parentBoundingSphere = new SphereBounds(new float3(0.5f, 0.5f, 0), 1.0f),
|
||||
boundingBox = new AABB(new float3(0, 0, 0), new float3(1, 1, 0)),
|
||||
vertexOffset = 0,
|
||||
triangleOffset = 0,
|
||||
groupIndex = 0,
|
||||
clusterError = 0,
|
||||
parentError = 0,
|
||||
vertexCount = 3,
|
||||
triangleCount = 1,
|
||||
localMaterialIndex = 0,
|
||||
lodLevel = 0,
|
||||
}
|
||||
};
|
||||
var groups = new[]
|
||||
{
|
||||
new MeshletGroup
|
||||
{
|
||||
boundingSphere = new SphereBounds(new float3(0.5f, 0.5f, 0), 1.0f),
|
||||
boundingBox = new AABB(new float3(0, 0, 0), new float3(1, 1, 0)),
|
||||
parentError = 0,
|
||||
meshletStartIndex = 0,
|
||||
meshletCount = 1,
|
||||
lodLevel = 0,
|
||||
}
|
||||
};
|
||||
var hierarchy = new[]
|
||||
{
|
||||
new MeshletHierarchyNode
|
||||
{
|
||||
minX = new float4(0, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity),
|
||||
minY = new float4(0, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity),
|
||||
minZ = new float4(0, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity),
|
||||
maxX = new float4(1, float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity),
|
||||
maxY = new float4(1, float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity),
|
||||
maxZ = new float4(0, float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity),
|
||||
maxParentError = new float4(0),
|
||||
nodeData = new uint4(0, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF),
|
||||
}
|
||||
};
|
||||
var meshletVertices = new uint[] { 0, 1, 2 };
|
||||
var meshletTriangles = new uint[] { 0 | (1u << 8) | (2u << 16) };
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
var header = new MeshContentHeader
|
||||
{
|
||||
magic = MeshContentHeader.MAGIC,
|
||||
version = MeshContentHeader.VERSION,
|
||||
vertexCount = (uint)vertices.Length,
|
||||
indexCount = (uint)indices.Length,
|
||||
materialPartCount = (uint)materialParts.Length,
|
||||
meshletCount = (uint)meshlets.Length,
|
||||
meshletGroupCount = (uint)groups.Length,
|
||||
meshletHierarchyNodeCount = (uint)hierarchy.Length,
|
||||
meshletVertexCount = (uint)meshletVertices.Length,
|
||||
meshletTriangleCount = (uint)meshletTriangles.Length,
|
||||
materialSlotCount = 1,
|
||||
lodLevelCount = 1,
|
||||
boundsMin = new float3(0, 0, 0),
|
||||
boundsMax = new float3(1, 1, 0),
|
||||
};
|
||||
|
||||
WriteStruct(stream, in header);
|
||||
header.vertexOffset = (ulong)stream.Position; WriteSpan(stream, vertices);
|
||||
header.indexOffset = (ulong)stream.Position; WriteSpan(stream, indices);
|
||||
header.materialPartOffset = (ulong)stream.Position; WriteSpan(stream, materialParts);
|
||||
header.meshletOffset = (ulong)stream.Position; WriteSpan(stream, meshlets);
|
||||
header.meshletGroupOffset = (ulong)stream.Position; WriteSpan(stream, groups);
|
||||
header.meshletHierarchyNodeOffset = (ulong)stream.Position; WriteSpan(stream, hierarchy);
|
||||
header.meshletVertexOffset = (ulong)stream.Position; WriteSpan(stream, meshletVertices);
|
||||
header.meshletTriangleOffset = (ulong)stream.Position; WriteSpan(stream, meshletTriangles);
|
||||
|
||||
stream.Position = 0;
|
||||
WriteStruct(stream, in header);
|
||||
|
||||
AddMockAsset(guid, new MockAssetData
|
||||
{
|
||||
type = AssetType.Mesh,
|
||||
data = stream.ToArray(),
|
||||
readDelayMs = readDelayMs
|
||||
});
|
||||
}
|
||||
|
||||
private static void WriteStruct<T>(Stream stream, ref readonly T value)
|
||||
where T : unmanaged
|
||||
{
|
||||
stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in value, 1)));
|
||||
}
|
||||
|
||||
private static void WriteSpan<T>(Stream stream, ReadOnlySpan<T> value)
|
||||
where T : unmanaged
|
||||
{
|
||||
stream.Write(MemoryMarshal.AsBytes(value));
|
||||
}
|
||||
|
||||
public AssetType GetAssetType(Guid guid)
|
||||
{
|
||||
return _assets.TryGetValue(guid, out var asset) ? asset.type : AssetType.Unknown;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.UnitTest.MockingEnvironment;
|
||||
|
||||
@@ -13,6 +14,7 @@ internal unsafe class MockingResourceDatabase : IResourceDatabase
|
||||
public string? name;
|
||||
public int refCount = 1;
|
||||
public bool isShared;
|
||||
public unsafe void* mappedData;
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<ulong, MockResourceRecord> _resources = new();
|
||||
@@ -118,9 +120,21 @@ internal unsafe class MockingResourceDatabase : IResourceDatabase
|
||||
|
||||
public void* MapResource(Handle<GPUResource> 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.");
|
||||
if (!_resources.TryGetValue(GetKey(handle), out var record))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
lock (record)
|
||||
{
|
||||
if (record.mappedData == null)
|
||||
{
|
||||
var size = record.desc.Type == ResourceType.Buffer ? Math.Max(1UL, record.desc.BufferDescriptor.Size) : 1UL;
|
||||
record.mappedData = NativeMemory.Alloc((nuint)size);
|
||||
}
|
||||
|
||||
return record.mappedData;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReleaseResource(Handle<GPUResource> handle)
|
||||
@@ -137,6 +151,12 @@ internal unsafe class MockingResourceDatabase : IResourceDatabase
|
||||
record.refCount--;
|
||||
if (record.refCount <= 0)
|
||||
{
|
||||
if (record.mappedData != null)
|
||||
{
|
||||
NativeMemory.Free(record.mappedData);
|
||||
record.mappedData = null;
|
||||
}
|
||||
|
||||
_resources.TryRemove(GetKey(handle), out _);
|
||||
}
|
||||
}
|
||||
@@ -207,6 +227,15 @@ internal unsafe class MockingResourceDatabase : IResourceDatabase
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var record in _resources.Values)
|
||||
{
|
||||
if (record.mappedData != null)
|
||||
{
|
||||
NativeMemory.Free(record.mappedData);
|
||||
record.mappedData = null;
|
||||
}
|
||||
}
|
||||
|
||||
_resources.Clear();
|
||||
_samplers.Clear();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user