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:
@@ -49,6 +49,13 @@ public interface IImportableAssetHandler : IAssetHandler
|
||||
ValueTask<Result> ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default);
|
||||
}
|
||||
|
||||
public readonly record struct ImportedSubAsset(Guid Guid, string Kind, string DisplayName, string StablePath, string VirtualSourcePath, Guid HandlerTypeId);
|
||||
|
||||
public interface ISubAssetImportableAssetHandler : IImportableAssetHandler
|
||||
{
|
||||
ValueTask<Result<ImportedSubAsset[]>> ImportWithSubAssetsAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default);
|
||||
}
|
||||
|
||||
public interface IPackableAssetHandler : IAssetHandler
|
||||
{
|
||||
ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default);
|
||||
|
||||
@@ -69,7 +69,7 @@ internal static class AssetMetaIO
|
||||
public const string META_EXTENSION_NAME = "gmeta";
|
||||
public const string META_EXTENSION = ".gmeta";
|
||||
|
||||
private static readonly JsonSerializerOptions s_options = new()
|
||||
internal static readonly JsonSerializerOptions s_options = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
@@ -136,6 +136,7 @@ internal static class AssetMetaIO
|
||||
}
|
||||
|
||||
File.Move(tempPath, metaPath);
|
||||
|
||||
}
|
||||
|
||||
public static string GetMetaPath(string sourceFilePath)
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Engine;
|
||||
using Ghost.Editor.Core.Services;
|
||||
using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using Misaki.HighPerformance.Mathematics;
|
||||
using Misaki.HighPerformance.Mathematics.Geometry;
|
||||
using System.IO.Hashing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Ghost.Editor.Core.Assets;
|
||||
|
||||
@@ -132,6 +140,139 @@ public class LightMeshNode : MeshNode
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ModelManifest
|
||||
{
|
||||
public Guid AssetId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ModelManifestNode Root
|
||||
{
|
||||
get; set;
|
||||
} = new ModelManifestNode();
|
||||
|
||||
public List<ModelManifestSubAsset> Meshes
|
||||
{
|
||||
get; set;
|
||||
} = new List<ModelManifestSubAsset>();
|
||||
|
||||
public List<ModelManifestMetadata> Metadata
|
||||
{
|
||||
get; set;
|
||||
} = new List<ModelManifestMetadata>();
|
||||
}
|
||||
|
||||
public sealed class ModelManifestNode
|
||||
{
|
||||
public string Name
|
||||
{
|
||||
get; set;
|
||||
} = string.Empty;
|
||||
|
||||
public string StablePath
|
||||
{
|
||||
get; set;
|
||||
} = string.Empty;
|
||||
|
||||
public float4x4 LocalTransform
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public Guid MeshGuid
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<ModelManifestNode> Children
|
||||
{
|
||||
get; set;
|
||||
} = new List<ModelManifestNode>();
|
||||
}
|
||||
|
||||
public sealed class ModelManifestSubAsset
|
||||
{
|
||||
public Guid Guid
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get; set;
|
||||
} = string.Empty;
|
||||
|
||||
public string StablePath
|
||||
{
|
||||
get; set;
|
||||
} = string.Empty;
|
||||
|
||||
public int MaterialSlotCount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int VertexCount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int IndexCount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ModelManifestMetadata
|
||||
{
|
||||
public string Kind
|
||||
{
|
||||
get; set;
|
||||
} = string.Empty;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get; set;
|
||||
} = string.Empty;
|
||||
|
||||
public string StablePath
|
||||
{
|
||||
get; set;
|
||||
} = string.Empty;
|
||||
}
|
||||
|
||||
internal sealed class ImportedModelAsset : IAsset
|
||||
{
|
||||
public Guid ID
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public Guid TypeID => typeof(FBXAsset).GUID;
|
||||
|
||||
public IAssetSettings? Settings
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public ModelManifest Manifest
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public ImportedModelAsset(Guid id, IAssetSettings? settings, ModelManifest manifest)
|
||||
{
|
||||
ID = id;
|
||||
Settings = settings;
|
||||
Manifest = manifest;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class MeshAsset : IAsset
|
||||
{
|
||||
private MeshNode _root;
|
||||
@@ -240,7 +381,8 @@ internal class FbxAssetSettings : MeshAssetSettings
|
||||
{
|
||||
}
|
||||
|
||||
internal class FBXAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
||||
[CustomAssetHandler(FBXAsset.GUID, [".fbx", ".obj"], 1)]
|
||||
internal class FBXAssetHandler : ISubAssetImportableAssetHandler, IPackableAssetHandler
|
||||
{
|
||||
public AssetType RuntimeAssetType => AssetType.Mesh;
|
||||
|
||||
@@ -250,31 +392,326 @@ internal class FBXAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
||||
|
||||
public IAssetSettings? CreateDefaultSettings()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return new FbxAssetSettings();
|
||||
}
|
||||
|
||||
public ValueTask<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
||||
public async ValueTask<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
var importedPath = ImportCoordinator.GetImportedAssetPath(id);
|
||||
if (!File.Exists(importedPath))
|
||||
{
|
||||
return Result.Failure<IAsset>("Imported model manifest does not exist.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var stream = new FileStream(importedPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var manifest = await JsonSerializer.DeserializeAsync<ModelManifest>(stream, cancellationToken: token).ConfigureAwait(false);
|
||||
return manifest != null
|
||||
? Result.Success<IAsset>(new ImportedModelAsset(id, settings, manifest))
|
||||
: Result.Failure<IAsset>("Failed to deserialize model manifest.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure<IAsset>(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask<Result> SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return ValueTask.FromResult(Result.Failure("Saving model assets is not supported yet."));
|
||||
}
|
||||
|
||||
public ValueTask<Result> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
||||
public async ValueTask<Result> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return await ImportWithSubAssetsAsync(sourcePath, targetPath, id, settings, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask<Result<ImportedSubAsset[]>> ImportWithSubAssetsAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
||||
{
|
||||
if (!File.Exists(sourcePath))
|
||||
{
|
||||
return Result.Failure<ImportedSubAsset[]>("Source file does not exist.");
|
||||
}
|
||||
|
||||
var meshSettings = ResolveSettings(sourcePath, settings);
|
||||
var root = new MeshNode();
|
||||
try
|
||||
{
|
||||
var parseJob = new MeshParsingJob(root, sourcePath, AllocationHandle.Persistent, meshSettings);
|
||||
var context = default(Misaki.HighPerformance.Jobs.JobExecutionContext);
|
||||
parseJob.Execute(in context);
|
||||
|
||||
var manifest = new ModelManifest
|
||||
{
|
||||
AssetId = id,
|
||||
};
|
||||
|
||||
var importedSubAssets = new List<ImportedSubAsset>();
|
||||
manifest.Root = await WriteNodeAsync(id, sourcePath, root, string.Empty, manifest, importedSubAssets, token).ConfigureAwait(false);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
|
||||
await using (var stream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(stream, manifest, cancellationToken: token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return importedSubAssets.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure<ImportedSubAsset[]>($"Failed to import mesh asset: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
root.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask<Result> ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return ValueTask.FromResult(Result.Failure("Exporting model assets is not supported yet."));
|
||||
}
|
||||
|
||||
public ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return ValueTask.FromResult(Result.Failure("Packing model assets is not supported yet."));
|
||||
}
|
||||
|
||||
private static MeshAssetSettings ResolveSettings(string sourcePath, IAssetSettings? settings)
|
||||
{
|
||||
if (settings is MeshAssetSettings meshSettings)
|
||||
{
|
||||
return meshSettings;
|
||||
}
|
||||
|
||||
return Path.GetExtension(sourcePath).Equals(".obj", StringComparison.OrdinalIgnoreCase)
|
||||
? new ObjAssetSettings()
|
||||
: new FbxAssetSettings();
|
||||
}
|
||||
|
||||
private static async ValueTask<ModelManifestNode> WriteNodeAsync(
|
||||
Guid parentGuid,
|
||||
string sourcePath,
|
||||
MeshNode node,
|
||||
string parentPath,
|
||||
ModelManifest manifest,
|
||||
List<ImportedSubAsset> importedSubAssets,
|
||||
CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var stablePath = string.IsNullOrEmpty(parentPath)
|
||||
? SanitizePathSegment(node.Name)
|
||||
: $"{parentPath}/{SanitizePathSegment(node.Name)}";
|
||||
|
||||
var manifestNode = new ModelManifestNode
|
||||
{
|
||||
Name = node.Name,
|
||||
StablePath = stablePath,
|
||||
LocalTransform = node.LocalTransform,
|
||||
};
|
||||
|
||||
if (node is GeometryMeshNode geometry)
|
||||
{
|
||||
var meshGuid = CreateDeterministicSubAssetGuid(parentGuid, "Mesh", stablePath);
|
||||
var meshPath = ImportCoordinator.GetImportedAssetPath(meshGuid);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(meshPath)!);
|
||||
|
||||
var meshInfo = await WriteMeshContentAsync(meshPath, geometry, token).ConfigureAwait(false);
|
||||
manifestNode.MeshGuid = meshGuid;
|
||||
|
||||
manifest.Meshes.Add(new ModelManifestSubAsset
|
||||
{
|
||||
Guid = meshGuid,
|
||||
Name = node.Name,
|
||||
StablePath = stablePath,
|
||||
MaterialSlotCount = meshInfo.materialSlotCount,
|
||||
VertexCount = geometry.Vertices.Count,
|
||||
IndexCount = geometry.Indices.Count,
|
||||
});
|
||||
|
||||
importedSubAssets.Add(new ImportedSubAsset(
|
||||
meshGuid,
|
||||
"Mesh",
|
||||
node.Name,
|
||||
stablePath,
|
||||
$"{sourcePath}#Mesh/{stablePath}",
|
||||
typeof(FBXAsset).GUID));
|
||||
}
|
||||
else if (node is LightMeshNode)
|
||||
{
|
||||
manifest.Metadata.Add(new ModelManifestMetadata
|
||||
{
|
||||
Kind = "Light",
|
||||
Name = node.Name,
|
||||
StablePath = stablePath,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var child in node.Children)
|
||||
{
|
||||
manifestNode.Children.Add(await WriteNodeAsync(parentGuid, sourcePath, child, stablePath, manifest, importedSubAssets, token).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
return manifestNode;
|
||||
}
|
||||
|
||||
private static ValueTask<(int materialSlotCount, int lodLevelCount)> WriteMeshContentAsync(string targetPath, GeometryMeshNode geometry, CancellationToken token)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var meshletData = new MeshletMeshData();
|
||||
try
|
||||
{
|
||||
MeshProcessor.BuildMeshlets(&meshletData, geometry.Vertices.AsReadOnly(), geometry.Indices.AsReadOnly(), geometry.MaterialParts.AsSpan());
|
||||
MeshProcessor.BuildClusterLodHierarchy(&meshletData);
|
||||
|
||||
var bounds = ComputeBounds(geometry.Vertices);
|
||||
var header = new MeshContentHeader
|
||||
{
|
||||
magic = MeshContentHeader.MAGIC,
|
||||
version = MeshContentHeader.VERSION,
|
||||
vertexCount = (uint)geometry.Vertices.Count,
|
||||
indexCount = (uint)geometry.Indices.Count,
|
||||
materialPartCount = (uint)geometry.MaterialParts.Length,
|
||||
meshletCount = (uint)meshletData.meshlets.Count,
|
||||
meshletGroupCount = (uint)meshletData.groups.Count,
|
||||
meshletHierarchyNodeCount = (uint)meshletData.hierarchyNodes.Count,
|
||||
meshletVertexCount = (uint)meshletData.meshletVertices.Count,
|
||||
meshletTriangleCount = (uint)meshletData.meshletTriangles.Count,
|
||||
materialSlotCount = (uint)meshletData.materialSlotCount,
|
||||
lodLevelCount = (uint)meshletData.lodLevelCount,
|
||||
boundsMin = bounds.Min,
|
||||
boundsMax = bounds.Max,
|
||||
};
|
||||
|
||||
using var stream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
WriteStruct(stream, in header);
|
||||
|
||||
header.vertexOffset = (ulong)stream.Position;
|
||||
WriteSpan(stream, geometry.Vertices.AsSpan());
|
||||
|
||||
header.indexOffset = (ulong)stream.Position;
|
||||
WriteSpan(stream, geometry.Indices.AsSpan());
|
||||
|
||||
header.materialPartOffset = (ulong)stream.Position;
|
||||
WriteMaterialParts(stream, geometry.MaterialParts.AsSpan());
|
||||
|
||||
header.meshletOffset = (ulong)stream.Position;
|
||||
WriteSpan(stream, meshletData.meshlets.AsSpan());
|
||||
|
||||
header.meshletGroupOffset = (ulong)stream.Position;
|
||||
WriteSpan(stream, meshletData.groups.AsSpan());
|
||||
|
||||
header.meshletHierarchyNodeOffset = (ulong)stream.Position;
|
||||
WriteSpan(stream, meshletData.hierarchyNodes.AsSpan());
|
||||
|
||||
header.meshletVertexOffset = (ulong)stream.Position;
|
||||
WriteSpan(stream, meshletData.meshletVertices.AsSpan());
|
||||
|
||||
header.meshletTriangleOffset = (ulong)stream.Position;
|
||||
WriteSpan(stream, meshletData.meshletTriangles.AsSpan());
|
||||
|
||||
stream.Position = 0;
|
||||
WriteStruct(stream, in header);
|
||||
stream.Flush();
|
||||
|
||||
return ValueTask.FromResult((meshletData.materialSlotCount, meshletData.lodLevelCount));
|
||||
}
|
||||
finally
|
||||
{
|
||||
meshletData.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static AABB ComputeBounds(UnsafeList<Vertex> vertices)
|
||||
{
|
||||
var min = new float3(float.MaxValue);
|
||||
var max = new float3(float.MinValue);
|
||||
for (var i = 0; i < vertices.Count; i++)
|
||||
{
|
||||
var p = vertices[i].position;
|
||||
min = math.min(min, p);
|
||||
max = math.max(max, p);
|
||||
}
|
||||
|
||||
return new AABB(min, max);
|
||||
}
|
||||
|
||||
private static Guid CreateDeterministicSubAssetGuid(Guid parentGuid, string kind, string stablePath)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes($"{parentGuid:N}:{kind}:{stablePath}");
|
||||
Span<byte> hash = stackalloc byte[16];
|
||||
var hashValue = XxHash128.HashToUInt128(bytes);
|
||||
Unsafe.WriteUnaligned(ref hash[0], hashValue);
|
||||
|
||||
hash[6] = (byte)((hash[6] & 0x0F) | 0x50);
|
||||
hash[8] = (byte)((hash[8] & 0x3F) | 0x80);
|
||||
return new Guid(hash);
|
||||
}
|
||||
|
||||
private static string SanitizePathSegment(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return "Node";
|
||||
}
|
||||
|
||||
var chars = value.ToCharArray();
|
||||
for (var i = 0; i < chars.Length; i++)
|
||||
{
|
||||
if (chars[i] == '/' || chars[i] == '\\' || chars[i] == '#')
|
||||
{
|
||||
chars[i] = '_';
|
||||
}
|
||||
}
|
||||
|
||||
return new string(chars);
|
||||
}
|
||||
|
||||
private static void WriteMaterialParts(Stream stream, ReadOnlySpan<MaterialPartInfo> parts)
|
||||
{
|
||||
if (parts.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Span<MeshContentMaterialPart> buffer = parts.Length <= 64
|
||||
? stackalloc MeshContentMaterialPart[parts.Length]
|
||||
: new MeshContentMaterialPart[parts.Length];
|
||||
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
{
|
||||
buffer[i] = new MeshContentMaterialPart
|
||||
{
|
||||
materialIndex = parts[i].materialIndex,
|
||||
indexStart = parts[i].indexStart,
|
||||
indexCount = parts[i].indexCount,
|
||||
vertexStart = parts[i].vertexStart,
|
||||
vertexCount = parts[i].vertexCount,
|
||||
};
|
||||
}
|
||||
|
||||
WriteSpan(stream, buffer);
|
||||
}
|
||||
|
||||
private static void WriteStruct<T>(Stream stream, ref readonly T value)
|
||||
where T : unmanaged
|
||||
{
|
||||
var span = MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in value, 1));
|
||||
stream.Write(span);
|
||||
}
|
||||
|
||||
private static void WriteSpan<T>(Stream stream, ReadOnlySpan<T> value)
|
||||
where T : unmanaged
|
||||
{
|
||||
if (value.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
stream.Write(MemoryMarshal.AsBytes(value));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ namespace Ghost.Editor.Core.Services;
|
||||
/// </summary>
|
||||
public sealed partial class AssetCatalog : IDisposable
|
||||
{
|
||||
public readonly record struct SubAssetInfo(Guid Guid, Guid ParentGuid, string Kind, string DisplayName, string StablePath, string SourcePath, Guid HandlerTypeId);
|
||||
|
||||
private readonly SqliteConnection _connection;
|
||||
private readonly Lock _writeLock = new();
|
||||
|
||||
@@ -26,6 +28,8 @@ public sealed partial class AssetCatalog : IDisposable
|
||||
private readonly SqliteCommand _cmdInsertDep;
|
||||
private readonly SqliteCommand _cmdClearDeps;
|
||||
private readonly SqliteCommand _cmdEnumerate;
|
||||
private readonly SqliteCommand _cmdEnumerateSubAssets;
|
||||
private readonly SqliteCommand _cmdDeleteSubAssetsForParent;
|
||||
|
||||
public AssetCatalog(string dbPath)
|
||||
{
|
||||
@@ -54,15 +58,19 @@ public sealed partial class AssetCatalog : IDisposable
|
||||
_cmdGetImportedAt = CreateCommand("SELECT imported_at_ms FROM assets WHERE guid = @guid");
|
||||
|
||||
_cmdUpsert = CreateCommand(@"
|
||||
INSERT INTO assets (guid, source_path, handler_type_id, handler_version, content_hash, settings_hash, imported_at_ms)
|
||||
VALUES (@guid, @path, @handler_id, @version, @content_hash, @settings_hash, @imported_at_ms)
|
||||
INSERT INTO assets (guid, source_path, handler_type_id, handler_version, content_hash, settings_hash, imported_at_ms, parent_guid, subasset_kind, display_name, stable_path)
|
||||
VALUES (@guid, @path, @handler_id, @version, @content_hash, @settings_hash, @imported_at_ms, @parent_guid, @subasset_kind, @display_name, @stable_path)
|
||||
ON CONFLICT(guid) DO UPDATE SET
|
||||
source_path = excluded.source_path,
|
||||
handler_type_id = excluded.handler_type_id,
|
||||
handler_version = excluded.handler_version,
|
||||
content_hash = excluded.content_hash,
|
||||
settings_hash = excluded.settings_hash,
|
||||
imported_at_ms = excluded.imported_at_ms");
|
||||
imported_at_ms = excluded.imported_at_ms,
|
||||
parent_guid = excluded.parent_guid,
|
||||
subasset_kind = excluded.subasset_kind,
|
||||
display_name = excluded.display_name,
|
||||
stable_path = excluded.stable_path");
|
||||
_cmdDelete = CreateCommand("DELETE FROM assets WHERE guid = @guid");
|
||||
_cmdGetReferencers = CreateCommand("SELECT from_guid FROM dependencies WHERE to_guid = @guid");
|
||||
_cmdGetDependencies = CreateCommand("SELECT to_guid FROM dependencies WHERE from_guid = @guid");
|
||||
@@ -70,6 +78,8 @@ public sealed partial class AssetCatalog : IDisposable
|
||||
_cmdInsertDep = CreateCommand("INSERT INTO dependencies (from_guid, to_guid) VALUES (@from, @to)");
|
||||
_cmdClearDeps = CreateCommand("DELETE FROM dependencies WHERE from_guid = @guid");
|
||||
_cmdEnumerate = CreateCommand("SELECT guid, source_path FROM assets");
|
||||
_cmdEnumerateSubAssets = CreateCommand("SELECT guid, parent_guid, subasset_kind, display_name, stable_path, source_path, handler_type_id FROM assets WHERE parent_guid = @parent_guid ORDER BY stable_path");
|
||||
_cmdDeleteSubAssetsForParent = CreateCommand("DELETE FROM assets WHERE parent_guid = @parent_guid");
|
||||
}
|
||||
|
||||
private SqliteCommand CreateCommand(string sql)
|
||||
@@ -90,9 +100,14 @@ public sealed partial class AssetCatalog : IDisposable
|
||||
handler_version INTEGER NOT NULL DEFAULT 0,
|
||||
content_hash TEXT,
|
||||
settings_hash TEXT,
|
||||
imported_at_ms INTEGER
|
||||
imported_at_ms INTEGER,
|
||||
parent_guid BLOB(16),
|
||||
subasset_kind TEXT,
|
||||
display_name TEXT,
|
||||
stable_path TEXT
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_assets_path ON assets(source_path);
|
||||
CREATE INDEX IF NOT EXISTS idx_assets_parent ON assets(parent_guid);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dependencies (
|
||||
from_guid BLOB(16) NOT NULL REFERENCES assets(guid) ON DELETE CASCADE,
|
||||
@@ -108,6 +123,28 @@ public sealed partial class AssetCatalog : IDisposable
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_labels_label ON labels(label);";
|
||||
cmd.ExecuteNonQuery();
|
||||
|
||||
TryAddColumn("assets", "parent_guid", "BLOB(16)");
|
||||
TryAddColumn("assets", "subasset_kind", "TEXT");
|
||||
TryAddColumn("assets", "display_name", "TEXT");
|
||||
TryAddColumn("assets", "stable_path", "TEXT");
|
||||
|
||||
using var indexCmd = _connection.CreateCommand();
|
||||
indexCmd.CommandText = "CREATE INDEX IF NOT EXISTS idx_assets_parent ON assets(parent_guid);";
|
||||
indexCmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private void TryAddColumn(string tableName, string columnName, string columnType)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = $"ALTER TABLE {tableName} ADD COLUMN {columnName} {columnType};";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
catch (SqliteException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static string ToUniversalPath(string path)
|
||||
@@ -147,6 +184,30 @@ public sealed partial class AssetCatalog : IDisposable
|
||||
_cmdUpsert.Parameters.AddWithValue("@content_hash", meta.ContentHash ?? (object)DBNull.Value);
|
||||
_cmdUpsert.Parameters.AddWithValue("@settings_hash", meta.SettingsHash ?? (object)DBNull.Value);
|
||||
_cmdUpsert.Parameters.AddWithValue("@imported_at_ms", meta.LastImportedUtc?.Ticks ?? (object)DBNull.Value);
|
||||
_cmdUpsert.Parameters.AddWithValue("@parent_guid", DBNull.Value);
|
||||
_cmdUpsert.Parameters.AddWithValue("@subasset_kind", DBNull.Value);
|
||||
_cmdUpsert.Parameters.AddWithValue("@display_name", DBNull.Value);
|
||||
_cmdUpsert.Parameters.AddWithValue("@stable_path", DBNull.Value);
|
||||
_cmdUpsert.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpsertSubAsset(Guid parentGuid, AssetMeta meta, string sourcePath, string kind, string displayName, string stablePath)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
_cmdUpsert.Parameters.Clear();
|
||||
_cmdUpsert.Parameters.AddWithValue("@guid", meta.Guid.ToByteArray());
|
||||
_cmdUpsert.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath));
|
||||
_cmdUpsert.Parameters.AddWithValue("@handler_id", meta.HandlerTypeId?.ToByteArray() ?? (object)DBNull.Value);
|
||||
_cmdUpsert.Parameters.AddWithValue("@version", meta.HandlerVersion);
|
||||
_cmdUpsert.Parameters.AddWithValue("@content_hash", meta.ContentHash ?? (object)DBNull.Value);
|
||||
_cmdUpsert.Parameters.AddWithValue("@settings_hash", meta.SettingsHash ?? (object)DBNull.Value);
|
||||
_cmdUpsert.Parameters.AddWithValue("@imported_at_ms", meta.LastImportedUtc?.Ticks ?? (object)DBNull.Value);
|
||||
_cmdUpsert.Parameters.AddWithValue("@parent_guid", parentGuid.ToByteArray());
|
||||
_cmdUpsert.Parameters.AddWithValue("@subasset_kind", kind);
|
||||
_cmdUpsert.Parameters.AddWithValue("@display_name", displayName);
|
||||
_cmdUpsert.Parameters.AddWithValue("@stable_path", stablePath);
|
||||
_cmdUpsert.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
@@ -245,6 +306,58 @@ public sealed partial class AssetCatalog : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public List<SubAssetInfo> GetSubAssets(Guid parentGuid)
|
||||
{
|
||||
_cmdEnumerateSubAssets.Parameters.Clear();
|
||||
_cmdEnumerateSubAssets.Parameters.AddWithValue("@parent_guid", parentGuid.ToByteArray());
|
||||
|
||||
using var reader = _cmdEnumerateSubAssets.ExecuteReader();
|
||||
var list = new List<SubAssetInfo>();
|
||||
while (reader.Read())
|
||||
{
|
||||
list.Add(new SubAssetInfo(
|
||||
new Guid((byte[])reader[0]),
|
||||
new Guid((byte[])reader[1]),
|
||||
reader.GetString(2),
|
||||
reader.GetString(3),
|
||||
reader.GetString(4),
|
||||
reader.GetString(5),
|
||||
new Guid((byte[])reader[6])));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public void RemoveSubAssetsExcept(Guid parentGuid, ReadOnlySpan<Guid> keepGuids)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
if (keepGuids.Length == 0)
|
||||
{
|
||||
_cmdDeleteSubAssetsForParent.Parameters.Clear();
|
||||
_cmdDeleteSubAssetsForParent.Parameters.AddWithValue("@parent_guid", parentGuid.ToByteArray());
|
||||
_cmdDeleteSubAssetsForParent.ExecuteNonQuery();
|
||||
return;
|
||||
}
|
||||
|
||||
var keep = new HashSet<Guid>();
|
||||
for (var i = 0; i < keepGuids.Length; i++)
|
||||
{
|
||||
keep.Add(keepGuids[i]);
|
||||
}
|
||||
|
||||
foreach (var subAsset in GetSubAssets(parentGuid))
|
||||
{
|
||||
if (!keep.Contains(subAsset.Guid))
|
||||
{
|
||||
_cmdDelete.Parameters.Clear();
|
||||
_cmdDelete.Parameters.AddWithValue("@guid", subAsset.Guid.ToByteArray());
|
||||
_cmdDelete.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cmdGetGuid.Dispose();
|
||||
@@ -257,6 +370,8 @@ public sealed partial class AssetCatalog : IDisposable
|
||||
_cmdInsertDep.Dispose();
|
||||
_cmdClearDeps.Dispose();
|
||||
_cmdEnumerate.Dispose();
|
||||
_cmdEnumerateSubAssets.Dispose();
|
||||
_cmdDeleteSubAssetsForParent.Dispose();
|
||||
_connection.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,11 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
|
||||
foreach (var (guid, path) in _catalog.EnumerateAll())
|
||||
{
|
||||
if (path.Contains('#', StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!foundGuids.Contains(guid))
|
||||
{
|
||||
_catalog.Remove(guid);
|
||||
@@ -130,51 +135,58 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
var relativePath = Path.GetRelativePath(EditorApplication.ProjectPath, e.FullPath);
|
||||
var fileExists = File.Exists(e.FullPath);
|
||||
|
||||
if (ext == AssetMetaIO.META_EXTENSION)
|
||||
try
|
||||
{
|
||||
if (fileExists)
|
||||
var relativePath = Path.GetRelativePath(EditorApplication.ProjectPath, e.FullPath);
|
||||
var fileExists = File.Exists(e.FullPath);
|
||||
|
||||
if (ext == AssetMetaIO.META_EXTENSION)
|
||||
{
|
||||
var meta = await AssetMetaIO.ReadAsync(e.FullPath);
|
||||
if (meta != null)
|
||||
if (fileExists)
|
||||
{
|
||||
_catalog.Upsert(meta, AssetMetaIO.GetSourcePath(relativePath));
|
||||
await _importCoordinator.EnqueueAsync(new ImportJob(meta.Guid, AssetMetaIO.GetSourcePath(relativePath), relativePath, ImportReason.SettingsChanged));
|
||||
var meta = await AssetMetaIO.ReadAsync(e.FullPath);
|
||||
if (meta != null)
|
||||
{
|
||||
_catalog.Upsert(meta, AssetMetaIO.GetSourcePath(relativePath));
|
||||
await _importCoordinator.EnqueueAsync(new ImportJob(meta.Guid, AssetMetaIO.GetSourcePath(relativePath), relativePath, ImportReason.SettingsChanged));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var changeType = AssetChangeType.None;
|
||||
var guid = _catalog.GetGuid(relativePath);
|
||||
|
||||
if (!fileExists)
|
||||
{
|
||||
// The file is no longer on disk. Wait safely completed.
|
||||
if (guid != Guid.Empty)
|
||||
{
|
||||
_catalog.Remove(guid);
|
||||
changeType = AssetChangeType.Deleted;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var changeType = AssetChangeType.None;
|
||||
var guid = _catalog.GetGuid(relativePath);
|
||||
|
||||
if (!fileExists)
|
||||
{
|
||||
// The file is no longer on disk. Wait safely completed.
|
||||
if (guid != Guid.Empty)
|
||||
else if (guid == Guid.Empty)
|
||||
{
|
||||
_catalog.Remove(guid);
|
||||
changeType = AssetChangeType.Deleted;
|
||||
// The file exists but isn't located inside our catalog yet -> Essentially a Creation
|
||||
await HandleNewSourceFileAsync(relativePath);
|
||||
changeType = AssetChangeType.Created;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The file exists and is tracked in the catalog, but triggered an event -> Modification
|
||||
await _importCoordinator.EnqueueAsync(new ImportJob(guid, relativePath, AssetMetaIO.GetMetaPath(relativePath), ImportReason.SourceChanged));
|
||||
changeType = AssetChangeType.Modified;
|
||||
}
|
||||
|
||||
if (changeType != AssetChangeType.None)
|
||||
{
|
||||
OnAssetChanged?.Invoke(this, new AssetChangedEventArgs(relativePath, null, changeType));
|
||||
}
|
||||
}
|
||||
else if (guid == Guid.Empty)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// The file exists but isn't located inside our catalog yet -> Essentially a Creation
|
||||
await HandleNewSourceFileAsync(relativePath);
|
||||
changeType = AssetChangeType.Created;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The file exists and is tracked in the catalog, but triggered an event -> Modification
|
||||
await _importCoordinator.EnqueueAsync(new ImportJob(guid, relativePath, AssetMetaIO.GetMetaPath(relativePath), ImportReason.SourceChanged));
|
||||
changeType = AssetChangeType.Modified;
|
||||
}
|
||||
|
||||
if (changeType != AssetChangeType.None)
|
||||
{
|
||||
OnAssetChanged?.Invoke(this, new AssetChangedEventArgs(relativePath, null, changeType));
|
||||
Logger.Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,10 +112,23 @@ internal sealed partial class ImportCoordinator : IDisposable
|
||||
}
|
||||
|
||||
var importResult = Result.Success();
|
||||
ImportedSubAsset[] subAssets = Array.Empty<ImportedSubAsset>();
|
||||
if (handler is IImportableAssetHandler importable)
|
||||
{
|
||||
var targetPath = GetImportedAssetPath(job.AssetGuid);
|
||||
importResult = await importable.ImportAsync(job.SourcePath, targetPath, job.AssetGuid, meta.Settings, token);
|
||||
if (importable is ISubAssetImportableAssetHandler subAssetImportable)
|
||||
{
|
||||
var subAssetResult = await subAssetImportable.ImportWithSubAssetsAsync(job.SourcePath, targetPath, job.AssetGuid, meta.Settings, token);
|
||||
importResult = subAssetResult;
|
||||
if (subAssetResult.IsSuccess)
|
||||
{
|
||||
subAssets = subAssetResult.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
importResult = await importable.ImportAsync(job.SourcePath, targetPath, job.AssetGuid, meta.Settings, token);
|
||||
}
|
||||
}
|
||||
|
||||
if (importResult.IsSuccess)
|
||||
@@ -127,6 +140,36 @@ internal sealed partial class ImportCoordinator : IDisposable
|
||||
|
||||
await AssetMetaIO.WriteAsync(job.MetaPath, meta, token);
|
||||
|
||||
if (subAssets.Length > 0)
|
||||
{
|
||||
var dependencies = new Guid[subAssets.Length];
|
||||
for (var i = 0; i < subAssets.Length; i++)
|
||||
{
|
||||
var subAsset = subAssets[i];
|
||||
dependencies[i] = subAsset.Guid;
|
||||
|
||||
var subMeta = new AssetMeta
|
||||
{
|
||||
Guid = subAsset.Guid,
|
||||
HandlerTypeId = subAsset.HandlerTypeId,
|
||||
HandlerVersion = meta.HandlerVersion,
|
||||
ContentHash = contentHash,
|
||||
SettingsHash = settingsHash,
|
||||
LastImportedUtc = meta.LastImportedUtc,
|
||||
};
|
||||
|
||||
_catalog.UpsertSubAsset(job.AssetGuid, subMeta, subAsset.VirtualSourcePath, subAsset.Kind, subAsset.DisplayName, subAsset.StablePath);
|
||||
}
|
||||
|
||||
_catalog.RemoveSubAssetsExcept(job.AssetGuid, dependencies);
|
||||
_catalog.SetDependencies(job.AssetGuid, dependencies);
|
||||
}
|
||||
else if (handler is ISubAssetImportableAssetHandler)
|
||||
{
|
||||
_catalog.RemoveSubAssetsExcept(job.AssetGuid, ReadOnlySpan<Guid>.Empty);
|
||||
_catalog.SetDependencies(job.AssetGuid, ReadOnlySpan<Guid>.Empty);
|
||||
}
|
||||
|
||||
OnImportCompleted?.Invoke(null, job.AssetGuid);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DefineConstants>$(DefineConstants);MHP_ENABLE_SAFETY_CHECKS;MHP_ENABLE_MIMALLOC;MHP_FASTMATH</DefineConstants>
|
||||
<DefineConstants>$(DefineConstants);MHP_ENABLE_SAFETY_CHECKS;MHP_ENABLE_MIMALLOC;MHP_FASTMATH;MHP_ENABLE_STACKTRACE</DefineConstants>
|
||||
<IsAotCompatible>True</IsAotCompatible>
|
||||
<IsTrimmable>True</IsTrimmable>
|
||||
</PropertyGroup>
|
||||
|
||||
286
src/Runtime/Ghost.Engine/AssetManager.Mesh.cs
Normal file
286
src/Runtime/Ghost.Engine/AssetManager.Mesh.cs
Normal file
@@ -0,0 +1,286 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Graphics;
|
||||
using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Ghost.Graphics.Utilities;
|
||||
using Misaki.HighPerformance.Mathematics.Geometry;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ghost.Engine;
|
||||
|
||||
internal unsafe partial class AssetEntry
|
||||
{
|
||||
private sealed unsafe class MeshParsedData
|
||||
{
|
||||
public MeshContentHeader header;
|
||||
public byte* pVertices;
|
||||
public byte* pIndices;
|
||||
public byte* pMeshlets;
|
||||
public byte* pMeshletGroups;
|
||||
public byte* pMeshletHierarchyNodes;
|
||||
public byte* pMeshletVertices;
|
||||
public byte* pMeshletTriangles;
|
||||
}
|
||||
|
||||
private static void RegisterMeshCallback()
|
||||
{
|
||||
s_onCreation[(int)AssetType.Mesh] = static (e) =>
|
||||
{
|
||||
var handle = e._resourceManager.CreateEmptyMesh();
|
||||
e.SetStorage(handle);
|
||||
};
|
||||
|
||||
s_onParseRawData[(int)AssetType.Mesh] = static (e) => e.ParseMeshData();
|
||||
s_onRecordUpload[(int)AssetType.Mesh] = static (e, ctx) => e.RecordMeshUpload(ctx);
|
||||
s_onUploadComplete[(int)AssetType.Mesh] = static (e, ctx) => e.OnMeshUploadComplete(ctx);
|
||||
s_onReleaseResource[(int)AssetType.Mesh] = static (e) =>
|
||||
{
|
||||
var handle = e.GetStorage<Handle<Mesh>>();
|
||||
if (handle.IsValid)
|
||||
{
|
||||
e._resourceManager.ReleaseMesh(handle);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Result ParseMeshData()
|
||||
{
|
||||
var pData = (byte*)_rawData.GetUnsafePtr();
|
||||
Logger.DebugAssert(pData != null);
|
||||
|
||||
if (_rawData.Size < (nuint)sizeof(MeshContentHeader))
|
||||
{
|
||||
return Result.Failure("Mesh content is smaller than the header.");
|
||||
}
|
||||
|
||||
var header = Unsafe.ReadUnaligned<MeshContentHeader>(pData);
|
||||
if (header.magic != MeshContentHeader.MAGIC || header.version != MeshContentHeader.VERSION)
|
||||
{
|
||||
return Result.Failure("Unsupported mesh content format.");
|
||||
}
|
||||
|
||||
if (header.vertexCount == 0 || header.indexCount == 0 ||
|
||||
header.meshletCount == 0 || header.meshletGroupCount == 0 ||
|
||||
header.meshletHierarchyNodeCount == 0 || header.meshletVertexCount == 0 ||
|
||||
header.meshletTriangleCount == 0)
|
||||
{
|
||||
return Result.Failure("Mesh content is missing required geometry or meshlet data.");
|
||||
}
|
||||
|
||||
if (!ValidateRange(header.vertexOffset, header.vertexCount, (uint)sizeof(Vertex)) ||
|
||||
!ValidateRange(header.indexOffset, header.indexCount, sizeof(uint)) ||
|
||||
!ValidateRange(header.meshletOffset, header.meshletCount, (uint)sizeof(Meshlet)) ||
|
||||
!ValidateRange(header.meshletGroupOffset, header.meshletGroupCount, (uint)sizeof(MeshletGroup)) ||
|
||||
!ValidateRange(header.meshletHierarchyNodeOffset, header.meshletHierarchyNodeCount, (uint)sizeof(MeshletHierarchyNode)) ||
|
||||
!ValidateRange(header.meshletVertexOffset, header.meshletVertexCount, sizeof(uint)) ||
|
||||
!ValidateRange(header.meshletTriangleOffset, header.meshletTriangleCount, sizeof(uint)))
|
||||
{
|
||||
return Result.Failure("Mesh content contains an invalid data range.");
|
||||
}
|
||||
|
||||
if (header.materialPartCount > 0 && !ValidateRange(header.materialPartOffset, header.materialPartCount, (uint)sizeof(MeshContentMaterialPart)))
|
||||
{
|
||||
return Result.Failure("Mesh content contains an invalid material part range.");
|
||||
}
|
||||
|
||||
_parsedObject = new MeshParsedData
|
||||
{
|
||||
header = header,
|
||||
pVertices = pData + header.vertexOffset,
|
||||
pIndices = pData + header.indexOffset,
|
||||
pMeshlets = pData + header.meshletOffset,
|
||||
pMeshletGroups = pData + header.meshletGroupOffset,
|
||||
pMeshletHierarchyNodes = pData + header.meshletHierarchyNodeOffset,
|
||||
pMeshletVertices = pData + header.meshletVertexOffset,
|
||||
pMeshletTriangles = pData + header.meshletTriangleOffset,
|
||||
};
|
||||
|
||||
return Result.Success();
|
||||
|
||||
bool ValidateRange(ulong offset, uint count, uint stride)
|
||||
{
|
||||
var size = (ulong)count * stride;
|
||||
return offset <= _rawData.Size && size <= _rawData.Size - (nuint)offset;
|
||||
}
|
||||
}
|
||||
|
||||
private Result RecordMeshUpload(ResourceStreamingContext context)
|
||||
{
|
||||
if (_parsedObject is not MeshParsedData data)
|
||||
{
|
||||
return Result.Failure("Mesh parse data is missing.");
|
||||
}
|
||||
|
||||
ref readonly var header = ref data.header;
|
||||
|
||||
var vertexBuffer = CreateBuffer(context, data.pVertices, header.vertexCount, (uint)sizeof(Vertex),
|
||||
BufferUsage.Vertex | BufferUsage.ShaderResource | BufferUsage.Raw, "Mesh_VertexBuffer");
|
||||
var indexBuffer = CreateBuffer(context, data.pIndices, header.indexCount, sizeof(uint),
|
||||
BufferUsage.Index | BufferUsage.ShaderResource | BufferUsage.Raw, "Mesh_IndexBuffer");
|
||||
var meshletBuffer = CreateBuffer(context, data.pMeshlets, header.meshletCount, (uint)sizeof(Meshlet),
|
||||
BufferUsage.Raw | BufferUsage.ShaderResource, "Mesh_Meshlets");
|
||||
var meshletVerticesBuffer = CreateBuffer(context, data.pMeshletVertices, header.meshletVertexCount, sizeof(uint),
|
||||
BufferUsage.Raw | BufferUsage.ShaderResource, "Mesh_MeshletVertices");
|
||||
var meshletTrianglesBuffer = CreateBuffer(context, data.pMeshletTriangles, header.meshletTriangleCount, sizeof(uint),
|
||||
BufferUsage.Raw | BufferUsage.ShaderResource, "Mesh_MeshletTriangles");
|
||||
var meshletGroupBuffer = CreateBuffer(context, data.pMeshletGroups, header.meshletGroupCount, (uint)sizeof(MeshletGroup),
|
||||
BufferUsage.Raw | BufferUsage.ShaderResource, "Mesh_MeshletGroups");
|
||||
var meshletHierarchyBuffer = CreateBuffer(context, data.pMeshletHierarchyNodes, header.meshletHierarchyNodeCount, (uint)sizeof(MeshletHierarchyNode),
|
||||
BufferUsage.Raw | BufferUsage.ShaderResource, "Mesh_MeshletHierarchy");
|
||||
|
||||
if (vertexBuffer.IsInvalid || indexBuffer.IsInvalid || meshletBuffer.IsInvalid ||
|
||||
meshletVerticesBuffer.IsInvalid || meshletTrianglesBuffer.IsInvalid ||
|
||||
meshletGroupBuffer.IsInvalid || meshletHierarchyBuffer.IsInvalid)
|
||||
{
|
||||
return Result.Failure("Failed to create one or more mesh GPU buffers.");
|
||||
}
|
||||
|
||||
var meshData = new MeshData
|
||||
{
|
||||
worldBoundsMin = header.boundsMin,
|
||||
worldBoundsMax = header.boundsMax,
|
||||
vertexBuffer = context.ResourceDatabase.GetBindlessIndex(vertexBuffer.AsResource()),
|
||||
indexBuffer = context.ResourceDatabase.GetBindlessIndex(indexBuffer.AsResource()),
|
||||
meshletBuffer = context.ResourceDatabase.GetBindlessIndex(meshletBuffer.AsResource()),
|
||||
meshletVerticesBuffer = context.ResourceDatabase.GetBindlessIndex(meshletVerticesBuffer.AsResource()),
|
||||
meshletTrianglesBuffer = context.ResourceDatabase.GetBindlessIndex(meshletTrianglesBuffer.AsResource()),
|
||||
meshletGroupBuffer = context.ResourceDatabase.GetBindlessIndex(meshletGroupBuffer.AsResource()),
|
||||
meshletHierarchyBuffer = context.ResourceDatabase.GetBindlessIndex(meshletHierarchyBuffer.AsResource()),
|
||||
meshletCount = header.meshletCount,
|
||||
lodLevelCount = header.lodLevelCount,
|
||||
materialSlotCount = header.materialSlotCount,
|
||||
};
|
||||
|
||||
var meshDataBufferDesc = new BufferDesc
|
||||
{
|
||||
Size = (ulong)sizeof(MeshData),
|
||||
Stride = (uint)sizeof(MeshData),
|
||||
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
|
||||
HeapType = HeapType.Default,
|
||||
};
|
||||
|
||||
var meshDataBuffer = RenderingUtility.CreateBuffer(
|
||||
context.ResourceManager,
|
||||
context.ResourceDatabase,
|
||||
context.ResourceAllocator,
|
||||
context.CopyPipeline.GetCommandBuffer(),
|
||||
&meshData,
|
||||
(nuint)sizeof(MeshData),
|
||||
in meshDataBufferDesc,
|
||||
"Mesh_MeshDataBuffer");
|
||||
|
||||
if (meshDataBuffer.IsInvalid)
|
||||
{
|
||||
return Result.Failure("Failed to create mesh data buffer.");
|
||||
}
|
||||
|
||||
var newHandle = context.ResourceManager.CreateUploadedMesh(
|
||||
vertexBuffer,
|
||||
indexBuffer,
|
||||
meshletBuffer,
|
||||
meshletVerticesBuffer,
|
||||
meshletTrianglesBuffer,
|
||||
meshletGroupBuffer,
|
||||
meshletHierarchyBuffer,
|
||||
meshDataBuffer,
|
||||
(int)header.vertexCount,
|
||||
(int)header.indexCount,
|
||||
(int)header.meshletCount,
|
||||
(int)header.lodLevelCount,
|
||||
(int)header.materialSlotCount,
|
||||
new AABB(header.boundsMin, header.boundsMax));
|
||||
|
||||
if (newHandle.IsInvalid)
|
||||
{
|
||||
return Result.Failure("Failed to register uploaded mesh.");
|
||||
}
|
||||
|
||||
var oldHandle = GetStorage<Handle<Mesh>>();
|
||||
SetStorage((oldHandle, newHandle));
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
private static Handle<GPUBuffer> CreateBuffer(ResourceStreamingContext context, void* pData, uint count, uint stride, BufferUsage usage, string name)
|
||||
{
|
||||
var desc = new BufferDesc
|
||||
{
|
||||
Size = (ulong)count * stride,
|
||||
Stride = stride,
|
||||
Usage = usage,
|
||||
HeapType = HeapType.Default,
|
||||
};
|
||||
|
||||
return RenderingUtility.CreateBuffer(
|
||||
context.ResourceManager,
|
||||
context.ResourceDatabase,
|
||||
context.ResourceAllocator,
|
||||
context.CopyPipeline.GetCommandBuffer(),
|
||||
pData,
|
||||
(nuint)desc.Size,
|
||||
in desc,
|
||||
name);
|
||||
}
|
||||
|
||||
private void OnMeshUploadComplete(ResourceStreamingContext context)
|
||||
{
|
||||
var (oldHandle, newHandle) = GetStorage<(Handle<Mesh>, Handle<Mesh>)>();
|
||||
var actualHandle = context.ResourceManager.ReplaceMesh(oldHandle, newHandle);
|
||||
if (actualHandle.IsInvalid)
|
||||
{
|
||||
SetStorage(oldHandle);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = context.ResourceManager.GetMeshReference(actualHandle);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
ref readonly var mesh = ref result.Value;
|
||||
context.GraphicsCommandBuffer.Barrier(
|
||||
BarrierDesc.Buffer(mesh.VertexBuffer.AsResource(), BarrierSync.VertexShading, BarrierAccess.VertexBuffer | BarrierAccess.ShaderResource),
|
||||
BarrierDesc.Buffer(mesh.IndexBuffer.AsResource(), BarrierSync.IndexInput, BarrierAccess.IndexBuffer | BarrierAccess.ShaderResource),
|
||||
BarrierDesc.Buffer(mesh.MeshLetBuffer.AsResource(), BarrierSync.AllShading, BarrierAccess.ShaderResource),
|
||||
BarrierDesc.Buffer(mesh.MeshletVerticesBuffer.AsResource(), BarrierSync.AllShading, BarrierAccess.ShaderResource),
|
||||
BarrierDesc.Buffer(mesh.MeshletTrianglesBuffer.AsResource(), BarrierSync.AllShading, BarrierAccess.ShaderResource),
|
||||
BarrierDesc.Buffer(mesh.MeshletGroupBuffer.AsResource(), BarrierSync.AllShading, BarrierAccess.ShaderResource),
|
||||
BarrierDesc.Buffer(mesh.MeshletHierarchyBuffer.AsResource(), BarrierSync.AllShading, BarrierAccess.ShaderResource),
|
||||
BarrierDesc.Buffer(mesh.MeshDataBuffer.AsResource(), BarrierSync.AllShading, BarrierAccess.ShaderResource));
|
||||
}
|
||||
|
||||
SetStorage(actualHandle);
|
||||
|
||||
_rawData.Dispose();
|
||||
_parsedObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class AssetManager
|
||||
{
|
||||
public Handle<Mesh> ResolveMesh(Guid assetID)
|
||||
{
|
||||
if (assetID == Guid.Empty)
|
||||
{
|
||||
return Handle<Mesh>.Invalid;
|
||||
}
|
||||
|
||||
var entry = GetOrCreateEntry(assetID);
|
||||
Logger.DebugAssert(entry.AssetType == AssetType.Mesh);
|
||||
|
||||
return entry.GetStorage<Handle<Mesh>>();
|
||||
}
|
||||
|
||||
public int ReleaseMesh(Guid assetID)
|
||||
{
|
||||
if (assetID == Guid.Empty)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!_entries.TryGetValue(assetID, out var entry) || entry.AssetType != AssetType.Mesh)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return entry.Release();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Core.Utilities;
|
||||
using Ghost.Graphics;
|
||||
using Ghost.Graphics.Services;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Misaki.HighPerformance.Jobs;
|
||||
using Misaki.HighPerformance.LowLevel;
|
||||
@@ -60,6 +61,7 @@ internal partial class AssetEntry
|
||||
static AssetEntry()
|
||||
{
|
||||
RegisterTextureCallback();
|
||||
RegisterMeshCallback();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +74,7 @@ internal unsafe partial class AssetEntry
|
||||
|
||||
private readonly AssetManager _assetManager;
|
||||
private readonly IResourceDatabase _resourceDatabase;
|
||||
private readonly ResourceManager _resourceManager;
|
||||
|
||||
private readonly Guid _assetId;
|
||||
private readonly AssetType _assetType;
|
||||
@@ -101,10 +104,11 @@ internal unsafe partial class AssetEntry
|
||||
set => Volatile.Write(ref _state, (int)value);
|
||||
}
|
||||
|
||||
public AssetEntry(AssetManager manager, IResourceDatabase resourceDatabase, Guid assetId, AssetType assetType, Guid[] dependencies)
|
||||
public AssetEntry(AssetManager manager, IResourceDatabase resourceDatabase, ResourceManager resourceManager, Guid assetId, AssetType assetType, Guid[] dependencies)
|
||||
{
|
||||
_assetManager = manager;
|
||||
_resourceDatabase = resourceDatabase;
|
||||
_resourceManager = resourceManager;
|
||||
|
||||
_assetId = assetId;
|
||||
_assetType = assetType;
|
||||
@@ -288,6 +292,7 @@ internal struct LoadAssetJob : IJob
|
||||
internal partial class AssetManager : IDisposable
|
||||
{
|
||||
private readonly IResourceDatabase _resourceDatabase;
|
||||
private readonly ResourceManager _resourceManager;
|
||||
private readonly IContentProvider _contentProvider;
|
||||
private readonly ResourceStreamingProcessor _streamingProcessor;
|
||||
private readonly JobScheduler _jobScheduler;
|
||||
@@ -297,9 +302,10 @@ internal partial class AssetManager : IDisposable
|
||||
public IContentProvider ContentProvider => _contentProvider;
|
||||
public ResourceStreamingProcessor StreamingProcessor => _streamingProcessor;
|
||||
|
||||
internal AssetManager(IResourceDatabase resourceDatabase, IContentProvider contentProvider, ResourceStreamingProcessor streamingProcessor, JobScheduler jobScheduler)
|
||||
internal AssetManager(IResourceDatabase resourceDatabase, ResourceManager resourceManager, IContentProvider contentProvider, ResourceStreamingProcessor streamingProcessor, JobScheduler jobScheduler)
|
||||
{
|
||||
_resourceDatabase = resourceDatabase;
|
||||
_resourceManager = resourceManager;
|
||||
_contentProvider = contentProvider;
|
||||
_streamingProcessor = streamingProcessor;
|
||||
_jobScheduler = jobScheduler;
|
||||
@@ -320,9 +326,13 @@ internal partial class AssetManager : IDisposable
|
||||
|
||||
private void EnsureScheduled(AssetEntry entry)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref entry.StateValue, (int)AssetState.Scheduled, (int)AssetState.Unloaded) != (int)AssetState.Unloaded)
|
||||
var previousState = Interlocked.CompareExchange(ref entry.StateValue, (int)AssetState.Scheduled, (int)AssetState.Unloaded);
|
||||
if (previousState != (int)AssetState.Unloaded)
|
||||
{
|
||||
return;
|
||||
if (previousState != (int)AssetState.Scheduled || entry.LoadJobHandle.IsValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Can this be jobified? If the dependency tree is not deep, it should be fine to do it in main thread, otherwise we might need to schedule a job to do it.
|
||||
@@ -397,7 +407,7 @@ internal partial class AssetManager : IDisposable
|
||||
var type = self._contentProvider.GetAssetType(id);
|
||||
var deps = self._contentProvider.GetDependencies(id);
|
||||
|
||||
var entry = new AssetEntry(self, self._resourceDatabase, id, type, deps);
|
||||
var entry = new AssetEntry(self, self._resourceDatabase, self._resourceManager, id, type, deps);
|
||||
|
||||
self.EnsureScheduled(entry);
|
||||
return entry;
|
||||
@@ -420,6 +430,7 @@ internal partial class AssetManager : IDisposable
|
||||
// Go directly to Scheduled -> Loading -> Loaded -> Uploading -> Ready again.
|
||||
// The swap cycle in RecordTextureUpload/OnTextureUploadComplete handles the
|
||||
// v1 to v2 transition exactly like the fallback to v1 transition.
|
||||
entry.SetLoadJobHandle(JobHandle.Invalid);
|
||||
EnsureScheduled(entry);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -47,7 +47,7 @@ public sealed partial class EngineCore : IDisposable
|
||||
};
|
||||
|
||||
_renderSystem = new RenderSystem(renderingDesc);
|
||||
_assetManager = new AssetManager(_renderSystem.GraphicsEngine.ResourceDatabase, _contentProvider, _streamingProcessor, _jobScheduler);
|
||||
_assetManager = new AssetManager(_renderSystem.GraphicsEngine.ResourceDatabase, _renderSystem.ResourceManager, _contentProvider, _streamingProcessor, _jobScheduler);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
47
src/Runtime/Ghost.Engine/MeshContent.cs
Normal file
47
src/Runtime/Ghost.Engine/MeshContent.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Misaki.HighPerformance.Mathematics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Engine;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct MeshContentHeader
|
||||
{
|
||||
public const uint MAGIC = 0x48534D47; // GMSH
|
||||
public const uint VERSION = 1;
|
||||
|
||||
public uint magic;
|
||||
public uint version;
|
||||
|
||||
public uint vertexCount;
|
||||
public uint indexCount;
|
||||
public uint materialPartCount;
|
||||
public uint meshletCount;
|
||||
public uint meshletGroupCount;
|
||||
public uint meshletHierarchyNodeCount;
|
||||
public uint meshletVertexCount;
|
||||
public uint meshletTriangleCount;
|
||||
public uint materialSlotCount;
|
||||
public uint lodLevelCount;
|
||||
|
||||
public float3 boundsMin;
|
||||
public float3 boundsMax;
|
||||
|
||||
public ulong vertexOffset;
|
||||
public ulong indexOffset;
|
||||
public ulong materialPartOffset;
|
||||
public ulong meshletOffset;
|
||||
public ulong meshletGroupOffset;
|
||||
public ulong meshletHierarchyNodeOffset;
|
||||
public ulong meshletVertexOffset;
|
||||
public ulong meshletTriangleOffset;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct MeshContentMaterialPart
|
||||
{
|
||||
public int materialIndex;
|
||||
public int indexStart;
|
||||
public int indexCount;
|
||||
public int vertexStart;
|
||||
public int vertexCount;
|
||||
}
|
||||
@@ -141,6 +141,11 @@ internal static partial class {registerTypeName}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (symbol.IsAbstract)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var iSettingsSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName("Ghost.Editor.Core.Assets.IAssetSettings");
|
||||
if (iSettingsSymbol == null)
|
||||
{
|
||||
|
||||
@@ -70,5 +70,9 @@ public struct MeshData
|
||||
public uint meshletBuffer;
|
||||
public uint meshletVerticesBuffer;
|
||||
public uint meshletTrianglesBuffer;
|
||||
public uint meshletGroupBuffer;
|
||||
public uint meshletHierarchyBuffer;
|
||||
public uint meshletCount;
|
||||
public uint lodLevelCount;
|
||||
public uint materialSlotCount; // number of material slots baked into this mesh's meshlets
|
||||
};
|
||||
|
||||
@@ -185,6 +185,22 @@ public struct Mesh : IResourceReleasable
|
||||
get; internal set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the handle to the meshlet group buffer on the GPU.
|
||||
/// </summary>
|
||||
public Handle<GPUBuffer> MeshletGroupBuffer
|
||||
{
|
||||
get; internal set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the handle to the meshlet hierarchy buffer on the GPU.
|
||||
/// </summary>
|
||||
public Handle<GPUBuffer> MeshletHierarchyBuffer
|
||||
{
|
||||
get; internal set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the handle to the mesh data buffer on the GPU.
|
||||
/// </summary>
|
||||
@@ -193,6 +209,19 @@ public struct Mesh : IResourceReleasable
|
||||
get; internal set;
|
||||
}
|
||||
|
||||
internal void SetMeshletSummary(int meshletCount, int lodLevelCount, int materialSlotCount)
|
||||
{
|
||||
_meshletData.meshletCount = meshletCount;
|
||||
_meshletData.lodLevelCount = lodLevelCount;
|
||||
_meshletData.materialSlotCount = materialSlotCount;
|
||||
}
|
||||
|
||||
internal void SetCounts(int vertexCount, int indexCount)
|
||||
{
|
||||
VertexCount = vertexCount;
|
||||
IndexCount = indexCount;
|
||||
}
|
||||
|
||||
public void ReleaseCpuResources()
|
||||
{
|
||||
_vertices.Dispose();
|
||||
@@ -209,6 +238,8 @@ public struct Mesh : IResourceReleasable
|
||||
database.ReleaseResource(MeshLetBuffer.AsResource());
|
||||
database.ReleaseResource(MeshletVerticesBuffer.AsResource());
|
||||
database.ReleaseResource(MeshletTrianglesBuffer.AsResource());
|
||||
database.ReleaseResource(MeshletGroupBuffer.AsResource());
|
||||
database.ReleaseResource(MeshletHierarchyBuffer.AsResource());
|
||||
database.ReleaseResource(MeshDataBuffer.AsResource());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,22 +254,44 @@ public readonly unsafe ref struct RenderContext
|
||||
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
|
||||
HeapType = HeapType.Default,
|
||||
};
|
||||
var groupsDesc = new BufferDesc
|
||||
{
|
||||
Size = (uint)(meshletData.groups.Count * sizeof(MeshletGroup)),
|
||||
Stride = (uint)sizeof(MeshletGroup),
|
||||
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
|
||||
HeapType = HeapType.Default,
|
||||
};
|
||||
var hierarchyDesc = new BufferDesc
|
||||
{
|
||||
Size = (uint)(meshletData.hierarchyNodes.Count * sizeof(MeshletHierarchyNode)),
|
||||
Stride = (uint)sizeof(MeshletHierarchyNode),
|
||||
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
|
||||
HeapType = HeapType.Default,
|
||||
};
|
||||
|
||||
meshRef.MeshLetBuffer = ResourceAllocator.CreateBuffer(in meshletDesc, "Meshlets");
|
||||
meshRef.MeshletVerticesBuffer = ResourceAllocator.CreateBuffer(in verticesDesc, "MeshletVertices");
|
||||
meshRef.MeshletTrianglesBuffer = ResourceAllocator.CreateBuffer(in trianglesDesc, "MeshletTriangles");
|
||||
meshRef.MeshletGroupBuffer = ResourceAllocator.CreateBuffer(in groupsDesc, "MeshletGroups");
|
||||
meshRef.MeshletHierarchyBuffer = ResourceAllocator.CreateBuffer(in hierarchyDesc, "MeshletHierarchy");
|
||||
|
||||
TransitionBarrier(meshRef.MeshLetBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
|
||||
TransitionBarrier(meshRef.MeshletVerticesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
|
||||
TransitionBarrier(meshRef.MeshletTrianglesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
|
||||
TransitionBarrier(meshRef.MeshletGroupBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
|
||||
TransitionBarrier(meshRef.MeshletHierarchyBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
|
||||
|
||||
UploadBuffer(meshRef.MeshLetBuffer, meshletData.meshlets.AsSpan());
|
||||
UploadBuffer(meshRef.MeshletVerticesBuffer, meshletData.meshletVertices.AsSpan());
|
||||
UploadBuffer(meshRef.MeshletTrianglesBuffer, meshletData.meshletTriangles.AsSpan());
|
||||
UploadBuffer(meshRef.MeshletGroupBuffer, meshletData.groups.AsSpan());
|
||||
UploadBuffer(meshRef.MeshletHierarchyBuffer, meshletData.hierarchyNodes.AsSpan());
|
||||
|
||||
TransitionBarrier(meshRef.MeshLetBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.NonPixelShading | BarrierSync.PixelShading);
|
||||
TransitionBarrier(meshRef.MeshletVerticesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.NonPixelShading | BarrierSync.PixelShading);
|
||||
TransitionBarrier(meshRef.MeshletTrianglesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.NonPixelShading | BarrierSync.PixelShading);
|
||||
TransitionBarrier(meshRef.MeshletGroupBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.NonPixelShading | BarrierSync.PixelShading);
|
||||
TransitionBarrier(meshRef.MeshletHierarchyBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.NonPixelShading | BarrierSync.PixelShading);
|
||||
}
|
||||
|
||||
public void UpdateObjectData(Handle<Mesh> mesh)
|
||||
@@ -290,6 +312,10 @@ public readonly unsafe ref struct RenderContext
|
||||
meshletBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshLetBuffer.AsResource()),
|
||||
meshletVerticesBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshletVerticesBuffer.AsResource()),
|
||||
meshletTrianglesBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshletTrianglesBuffer.AsResource()),
|
||||
meshletGroupBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshletGroupBuffer.AsResource()),
|
||||
meshletHierarchyBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshletHierarchyBuffer.AsResource()),
|
||||
meshletCount = (uint)meshData.MeshletData.meshletCount,
|
||||
lodLevelCount = (uint)meshData.MeshletData.lodLevelCount,
|
||||
materialSlotCount = (uint)meshData.MeshletData.materialSlotCount,
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using Misaki.HighPerformance.Mathematics.Geometry;
|
||||
|
||||
namespace Ghost.Graphics.Services;
|
||||
|
||||
@@ -165,6 +166,79 @@ public sealed partial class ResourceManager : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public Handle<Mesh> CreateEmptyMesh(string? name = null)
|
||||
{
|
||||
Logger.DebugAssert(!_disposed);
|
||||
|
||||
lock (_meshWriteLock)
|
||||
{
|
||||
var id = _meshes.Add(new Mesh(), out var generation);
|
||||
return new Handle<Mesh>(id, generation);
|
||||
}
|
||||
}
|
||||
|
||||
public Handle<Mesh> CreateUploadedMesh(
|
||||
Handle<GPUBuffer> vertexBuffer,
|
||||
Handle<GPUBuffer> indexBuffer,
|
||||
Handle<GPUBuffer> meshletBuffer,
|
||||
Handle<GPUBuffer> meshletVerticesBuffer,
|
||||
Handle<GPUBuffer> meshletTrianglesBuffer,
|
||||
Handle<GPUBuffer> meshletGroupBuffer,
|
||||
Handle<GPUBuffer> meshletHierarchyBuffer,
|
||||
Handle<GPUBuffer> meshDataBuffer,
|
||||
int vertexCount,
|
||||
int indexCount,
|
||||
int meshletCount,
|
||||
int lodLevelCount,
|
||||
int materialSlotCount,
|
||||
AABB boundingBox)
|
||||
{
|
||||
Logger.DebugAssert(!_disposed);
|
||||
|
||||
var mesh = new Mesh
|
||||
{
|
||||
VertexBuffer = vertexBuffer,
|
||||
IndexBuffer = indexBuffer,
|
||||
MeshLetBuffer = meshletBuffer,
|
||||
MeshletVerticesBuffer = meshletVerticesBuffer,
|
||||
MeshletTrianglesBuffer = meshletTrianglesBuffer,
|
||||
MeshletGroupBuffer = meshletGroupBuffer,
|
||||
MeshletHierarchyBuffer = meshletHierarchyBuffer,
|
||||
MeshDataBuffer = meshDataBuffer,
|
||||
BoundingBox = boundingBox,
|
||||
};
|
||||
mesh.SetCounts(vertexCount, indexCount);
|
||||
mesh.SetMeshletSummary(meshletCount, lodLevelCount, materialSlotCount);
|
||||
|
||||
lock (_meshWriteLock)
|
||||
{
|
||||
var id = _meshes.Add(mesh, out var generation);
|
||||
return new Handle<Mesh>(id, generation);
|
||||
}
|
||||
}
|
||||
|
||||
public Handle<Mesh> ReplaceMesh(Handle<Mesh> dst, Handle<Mesh> src)
|
||||
{
|
||||
Logger.DebugAssert(!_disposed);
|
||||
|
||||
lock (_meshWriteLock)
|
||||
{
|
||||
ref var dstMesh = ref _meshes.GetElementReferenceAt(dst.ID, dst.Generation, out var dstExists);
|
||||
ref var srcMesh = ref _meshes.GetElementReferenceAt(src.ID, src.Generation, out var srcExists);
|
||||
if (!dstExists || !srcExists)
|
||||
{
|
||||
return Handle<Mesh>.Invalid;
|
||||
}
|
||||
|
||||
var oldMesh = dstMesh;
|
||||
dstMesh = srcMesh;
|
||||
_meshes.Remove(src.ID, src.Generation);
|
||||
|
||||
oldMesh.ReleaseResource(_resourceDatabase);
|
||||
return dst;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new material instance using the specified shader.
|
||||
/// </summary>
|
||||
|
||||
@@ -61,6 +61,10 @@ struct MeshData
|
||||
BYTE_ADDRESS_BUFFER meshletBuffer;
|
||||
BYTE_ADDRESS_BUFFER meshletVerticesBuffer;
|
||||
BYTE_ADDRESS_BUFFER meshletTrianglesBuffer;
|
||||
BYTE_ADDRESS_BUFFER meshletGroupBuffer;
|
||||
BYTE_ADDRESS_BUFFER meshletHierarchyBuffer;
|
||||
uint meshletCount;
|
||||
uint lodLevelCount;
|
||||
uint materialSlotCount;
|
||||
};
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ public static unsafe class MeshBuilder
|
||||
public static void ComputeTangents(Span<Vertex> vertices, Span<uint> indices)
|
||||
{
|
||||
using var scope = AllocationManager.CreateStackScope();
|
||||
var bitangents = new UnsafeArray<float3>(vertices.Length, scope.AllocationHandle, AllocationOption.Clear);
|
||||
using var bitangents = new UnsafeArray<float3>(vertices.Length, scope.AllocationHandle, AllocationOption.Clear);
|
||||
|
||||
for (var i = 0; i < indices.Length; i += 3)
|
||||
{
|
||||
|
||||
@@ -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]);
|
||||
var sourcePath = "Assets/test.text";
|
||||
await File.WriteAllBytesAsync(sourcePath, [1, 2, 3], TestContext.CancellationToken);
|
||||
|
||||
await Task.Delay(1000); // Wait for FSW to trigger
|
||||
var metaPath = AssetMetaIO.GetMetaPath(sourcePath);
|
||||
|
||||
using var cts = new CancellationTokenSource(5000);
|
||||
while (!File.Exists(metaPath) && !cts.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(50, cts.Token);
|
||||
}
|
||||
|
||||
var metaPath = AssetMetaIO.GetMetaPath(fullSourcePath);
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ public partial class Api
|
||||
// NOTE: Currently only support Windows.
|
||||
if (libraryName == "dxcompiler")
|
||||
{
|
||||
NativeLibrary.TryLoad("runtimes/win-x64/native/dxil.dll", out _);
|
||||
NativeLibrary.TryLoad(Path.Combine(AppContext.BaseDirectory, "runtimes", "win-x64", "native", "dxil.dll"), out _);
|
||||
|
||||
if (NativeLibrary.TryLoad("runtimes/win-x64/native/dxcompiler.dll", out var dxcHandle))
|
||||
if (NativeLibrary.TryLoad(Path.Combine(AppContext.BaseDirectory, "runtimes", "win-x64", "native", "dxcompiler.dll"), out var dxcHandle))
|
||||
{
|
||||
return dxcHandle;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public partial class Api
|
||||
OperatingSystem.IsMacOS() ? ".dylib" : "";
|
||||
|
||||
var arch = Environment.Is64BitProcess ? "x64" : "x86";
|
||||
var nativeDllDir = Path.Combine("./runtimes", platform + "-" + arch, "native");
|
||||
var nativeDllDir = Path.Combine(AppContext.BaseDirectory, "runtimes", platform + "-" + arch, "native");
|
||||
|
||||
return NativeLibrary.Load(Path.Combine(nativeDllDir, libraryName + ext));
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ public partial class Api
|
||||
OperatingSystem.IsMacOS() ? ".dylib" : "";
|
||||
|
||||
var arch = Environment.Is64BitProcess ? "x64" : "x86";
|
||||
var nativeDllDir = Path.Combine("./runtimes", platform + "-" + arch, "native");
|
||||
var nativeDllDir = Path.Combine(AppContext.BaseDirectory, "runtimes", platform + "-" + arch, "native");
|
||||
|
||||
return NativeLibrary.Load(Path.Combine(nativeDllDir, libraryName + ext));
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ public partial class Api
|
||||
OperatingSystem.IsMacOS() ? ".dylib" : "";
|
||||
|
||||
var arch = Environment.Is64BitProcess ? "x64" : "x86";
|
||||
var nativeDllDir = Path.Combine("./runtimes", platform + "-" + arch, "native");
|
||||
var nativeDllDir = Path.Combine(AppContext.BaseDirectory, "runtimes", platform + "-" + arch, "native");
|
||||
|
||||
return NativeLibrary.Load(Path.Combine(nativeDllDir, libraryName + ext));
|
||||
});
|
||||
|
||||
@@ -22,8 +22,7 @@ public partial class Api
|
||||
OperatingSystem.IsMacOS() ? ".dylib" : "";
|
||||
|
||||
var arch = Environment.Is64BitProcess ? "x64" : "x86";
|
||||
var nativeDllDir = Path.Combine("./runtimes", platform + "-" + arch, "native");
|
||||
|
||||
var nativeDllDir = Path.Combine(AppContext.BaseDirectory, "runtimes", platform + "-" + arch, "native");
|
||||
return NativeLibrary.Load(Path.Combine(nativeDllDir, libraryName + ext));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user