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,7 +49,14 @@ public interface IImportableAssetHandler : IAssetHandler
|
|||||||
ValueTask<Result> ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default);
|
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
|
public interface IPackableAssetHandler : IAssetHandler
|
||||||
{
|
{
|
||||||
ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default);
|
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_NAME = "gmeta";
|
||||||
public const string META_EXTENSION = ".gmeta";
|
public const string META_EXTENSION = ".gmeta";
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions s_options = new()
|
internal static readonly JsonSerializerOptions s_options = new()
|
||||||
{
|
{
|
||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
@@ -136,6 +136,7 @@ internal static class AssetMetaIO
|
|||||||
}
|
}
|
||||||
|
|
||||||
File.Move(tempPath, metaPath);
|
File.Move(tempPath, metaPath);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetMetaPath(string sourceFilePath)
|
public static string GetMetaPath(string sourceFilePath)
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
using Ghost.Engine;
|
using Ghost.Engine;
|
||||||
|
using Ghost.Editor.Core.Services;
|
||||||
|
using Ghost.Graphics.Core;
|
||||||
using Ghost.Graphics.RHI;
|
using Ghost.Graphics.RHI;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using Misaki.HighPerformance.LowLevel.Collections;
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
using Misaki.HighPerformance.Mathematics;
|
using Misaki.HighPerformance.Mathematics;
|
||||||
|
using Misaki.HighPerformance.Mathematics.Geometry;
|
||||||
|
using System.IO.Hashing;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Assets;
|
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
|
public abstract class MeshAsset : IAsset
|
||||||
{
|
{
|
||||||
private MeshNode _root;
|
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;
|
public AssetType RuntimeAssetType => AssetType.Mesh;
|
||||||
|
|
||||||
@@ -250,31 +392,326 @@ internal class FBXAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
|||||||
|
|
||||||
public IAssetSettings? CreateDefaultSettings()
|
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)
|
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)
|
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)
|
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>
|
/// </summary>
|
||||||
public sealed partial class AssetCatalog : IDisposable
|
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 SqliteConnection _connection;
|
||||||
private readonly Lock _writeLock = new();
|
private readonly Lock _writeLock = new();
|
||||||
|
|
||||||
@@ -26,6 +28,8 @@ public sealed partial class AssetCatalog : IDisposable
|
|||||||
private readonly SqliteCommand _cmdInsertDep;
|
private readonly SqliteCommand _cmdInsertDep;
|
||||||
private readonly SqliteCommand _cmdClearDeps;
|
private readonly SqliteCommand _cmdClearDeps;
|
||||||
private readonly SqliteCommand _cmdEnumerate;
|
private readonly SqliteCommand _cmdEnumerate;
|
||||||
|
private readonly SqliteCommand _cmdEnumerateSubAssets;
|
||||||
|
private readonly SqliteCommand _cmdDeleteSubAssetsForParent;
|
||||||
|
|
||||||
public AssetCatalog(string dbPath)
|
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");
|
_cmdGetImportedAt = CreateCommand("SELECT imported_at_ms FROM assets WHERE guid = @guid");
|
||||||
|
|
||||||
_cmdUpsert = CreateCommand(@"
|
_cmdUpsert = CreateCommand(@"
|
||||||
INSERT INTO assets (guid, source_path, handler_type_id, handler_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)
|
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
|
ON CONFLICT(guid) DO UPDATE SET
|
||||||
source_path = excluded.source_path,
|
source_path = excluded.source_path,
|
||||||
handler_type_id = excluded.handler_type_id,
|
handler_type_id = excluded.handler_type_id,
|
||||||
handler_version = excluded.handler_version,
|
handler_version = excluded.handler_version,
|
||||||
content_hash = excluded.content_hash,
|
content_hash = excluded.content_hash,
|
||||||
settings_hash = excluded.settings_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");
|
_cmdDelete = CreateCommand("DELETE FROM assets WHERE guid = @guid");
|
||||||
_cmdGetReferencers = CreateCommand("SELECT from_guid FROM dependencies WHERE to_guid = @guid");
|
_cmdGetReferencers = CreateCommand("SELECT from_guid FROM dependencies WHERE to_guid = @guid");
|
||||||
_cmdGetDependencies = CreateCommand("SELECT to_guid FROM dependencies WHERE from_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)");
|
_cmdInsertDep = CreateCommand("INSERT INTO dependencies (from_guid, to_guid) VALUES (@from, @to)");
|
||||||
_cmdClearDeps = CreateCommand("DELETE FROM dependencies WHERE from_guid = @guid");
|
_cmdClearDeps = CreateCommand("DELETE FROM dependencies WHERE from_guid = @guid");
|
||||||
_cmdEnumerate = CreateCommand("SELECT guid, source_path FROM assets");
|
_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)
|
private SqliteCommand CreateCommand(string sql)
|
||||||
@@ -90,9 +100,14 @@ public sealed partial class AssetCatalog : IDisposable
|
|||||||
handler_version INTEGER NOT NULL DEFAULT 0,
|
handler_version INTEGER NOT NULL DEFAULT 0,
|
||||||
content_hash TEXT,
|
content_hash TEXT,
|
||||||
settings_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 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 (
|
CREATE TABLE IF NOT EXISTS dependencies (
|
||||||
from_guid BLOB(16) NOT NULL REFERENCES assets(guid) ON DELETE CASCADE,
|
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);";
|
CREATE INDEX IF NOT EXISTS idx_labels_label ON labels(label);";
|
||||||
cmd.ExecuteNonQuery();
|
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)
|
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("@content_hash", meta.ContentHash ?? (object)DBNull.Value);
|
||||||
_cmdUpsert.Parameters.AddWithValue("@settings_hash", meta.SettingsHash ?? (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("@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();
|
_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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_cmdGetGuid.Dispose();
|
_cmdGetGuid.Dispose();
|
||||||
@@ -257,6 +370,8 @@ public sealed partial class AssetCatalog : IDisposable
|
|||||||
_cmdInsertDep.Dispose();
|
_cmdInsertDep.Dispose();
|
||||||
_cmdClearDeps.Dispose();
|
_cmdClearDeps.Dispose();
|
||||||
_cmdEnumerate.Dispose();
|
_cmdEnumerate.Dispose();
|
||||||
|
_cmdEnumerateSubAssets.Dispose();
|
||||||
|
_cmdDeleteSubAssetsForParent.Dispose();
|
||||||
_connection.Dispose();
|
_connection.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
|||||||
|
|
||||||
foreach (var (guid, path) in _catalog.EnumerateAll())
|
foreach (var (guid, path) in _catalog.EnumerateAll())
|
||||||
{
|
{
|
||||||
|
if (path.Contains('#', StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!foundGuids.Contains(guid))
|
if (!foundGuids.Contains(guid))
|
||||||
{
|
{
|
||||||
_catalog.Remove(guid);
|
_catalog.Remove(guid);
|
||||||
@@ -130,51 +135,58 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var relativePath = Path.GetRelativePath(EditorApplication.ProjectPath, e.FullPath);
|
try
|
||||||
var fileExists = File.Exists(e.FullPath);
|
|
||||||
|
|
||||||
if (ext == AssetMetaIO.META_EXTENSION)
|
|
||||||
{
|
{
|
||||||
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 (fileExists)
|
||||||
if (meta != null)
|
|
||||||
{
|
{
|
||||||
_catalog.Upsert(meta, AssetMetaIO.GetSourcePath(relativePath));
|
var meta = await AssetMetaIO.ReadAsync(e.FullPath);
|
||||||
await _importCoordinator.EnqueueAsync(new ImportJob(meta.Guid, AssetMetaIO.GetSourcePath(relativePath), relativePath, ImportReason.SettingsChanged));
|
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;
|
else if (guid == Guid.Empty)
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
// The file exists but isn't located inside our catalog yet -> Essentially a Creation
|
||||||
changeType = AssetChangeType.Deleted;
|
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
|
Logger.Error(ex);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,10 +112,23 @@ internal sealed partial class ImportCoordinator : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
var importResult = Result.Success();
|
var importResult = Result.Success();
|
||||||
|
ImportedSubAsset[] subAssets = Array.Empty<ImportedSubAsset>();
|
||||||
if (handler is IImportableAssetHandler importable)
|
if (handler is IImportableAssetHandler importable)
|
||||||
{
|
{
|
||||||
var targetPath = GetImportedAssetPath(job.AssetGuid);
|
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)
|
if (importResult.IsSuccess)
|
||||||
@@ -127,6 +140,36 @@ internal sealed partial class ImportCoordinator : IDisposable
|
|||||||
|
|
||||||
await AssetMetaIO.WriteAsync(job.MetaPath, meta, token);
|
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);
|
OnImportCompleted?.Invoke(null, job.AssetGuid);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<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>
|
<IsAotCompatible>True</IsAotCompatible>
|
||||||
<IsTrimmable>True</IsTrimmable>
|
<IsTrimmable>True</IsTrimmable>
|
||||||
</PropertyGroup>
|
</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;
|
||||||
using Ghost.Core.Utilities;
|
using Ghost.Core.Utilities;
|
||||||
using Ghost.Graphics;
|
using Ghost.Graphics;
|
||||||
|
using Ghost.Graphics.Services;
|
||||||
using Ghost.Graphics.RHI;
|
using Ghost.Graphics.RHI;
|
||||||
using Misaki.HighPerformance.Jobs;
|
using Misaki.HighPerformance.Jobs;
|
||||||
using Misaki.HighPerformance.LowLevel;
|
using Misaki.HighPerformance.LowLevel;
|
||||||
@@ -60,6 +61,7 @@ internal partial class AssetEntry
|
|||||||
static AssetEntry()
|
static AssetEntry()
|
||||||
{
|
{
|
||||||
RegisterTextureCallback();
|
RegisterTextureCallback();
|
||||||
|
RegisterMeshCallback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@ internal unsafe partial class AssetEntry
|
|||||||
|
|
||||||
private readonly AssetManager _assetManager;
|
private readonly AssetManager _assetManager;
|
||||||
private readonly IResourceDatabase _resourceDatabase;
|
private readonly IResourceDatabase _resourceDatabase;
|
||||||
|
private readonly ResourceManager _resourceManager;
|
||||||
|
|
||||||
private readonly Guid _assetId;
|
private readonly Guid _assetId;
|
||||||
private readonly AssetType _assetType;
|
private readonly AssetType _assetType;
|
||||||
@@ -101,10 +104,11 @@ internal unsafe partial class AssetEntry
|
|||||||
set => Volatile.Write(ref _state, (int)value);
|
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;
|
_assetManager = manager;
|
||||||
_resourceDatabase = resourceDatabase;
|
_resourceDatabase = resourceDatabase;
|
||||||
|
_resourceManager = resourceManager;
|
||||||
|
|
||||||
_assetId = assetId;
|
_assetId = assetId;
|
||||||
_assetType = assetType;
|
_assetType = assetType;
|
||||||
@@ -288,6 +292,7 @@ internal struct LoadAssetJob : IJob
|
|||||||
internal partial class AssetManager : IDisposable
|
internal partial class AssetManager : IDisposable
|
||||||
{
|
{
|
||||||
private readonly IResourceDatabase _resourceDatabase;
|
private readonly IResourceDatabase _resourceDatabase;
|
||||||
|
private readonly ResourceManager _resourceManager;
|
||||||
private readonly IContentProvider _contentProvider;
|
private readonly IContentProvider _contentProvider;
|
||||||
private readonly ResourceStreamingProcessor _streamingProcessor;
|
private readonly ResourceStreamingProcessor _streamingProcessor;
|
||||||
private readonly JobScheduler _jobScheduler;
|
private readonly JobScheduler _jobScheduler;
|
||||||
@@ -297,9 +302,10 @@ internal partial class AssetManager : IDisposable
|
|||||||
public IContentProvider ContentProvider => _contentProvider;
|
public IContentProvider ContentProvider => _contentProvider;
|
||||||
public ResourceStreamingProcessor StreamingProcessor => _streamingProcessor;
|
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;
|
_resourceDatabase = resourceDatabase;
|
||||||
|
_resourceManager = resourceManager;
|
||||||
_contentProvider = contentProvider;
|
_contentProvider = contentProvider;
|
||||||
_streamingProcessor = streamingProcessor;
|
_streamingProcessor = streamingProcessor;
|
||||||
_jobScheduler = jobScheduler;
|
_jobScheduler = jobScheduler;
|
||||||
@@ -320,9 +326,13 @@ internal partial class AssetManager : IDisposable
|
|||||||
|
|
||||||
private void EnsureScheduled(AssetEntry entry)
|
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.
|
// 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 type = self._contentProvider.GetAssetType(id);
|
||||||
var deps = self._contentProvider.GetDependencies(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);
|
self.EnsureScheduled(entry);
|
||||||
return entry;
|
return entry;
|
||||||
@@ -420,6 +430,7 @@ internal partial class AssetManager : IDisposable
|
|||||||
// Go directly to Scheduled -> Loading -> Loaded -> Uploading -> Ready again.
|
// Go directly to Scheduled -> Loading -> Loaded -> Uploading -> Ready again.
|
||||||
// The swap cycle in RecordTextureUpload/OnTextureUploadComplete handles the
|
// The swap cycle in RecordTextureUpload/OnTextureUploadComplete handles the
|
||||||
// v1 to v2 transition exactly like the fallback to v1 transition.
|
// v1 to v2 transition exactly like the fallback to v1 transition.
|
||||||
|
entry.SetLoadJobHandle(JobHandle.Invalid);
|
||||||
EnsureScheduled(entry);
|
EnsureScheduled(entry);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public sealed partial class EngineCore : IDisposable
|
|||||||
};
|
};
|
||||||
|
|
||||||
_renderSystem = new RenderSystem(renderingDesc);
|
_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()
|
public void Dispose()
|
||||||
@@ -56,4 +56,4 @@ public sealed partial class EngineCore : IDisposable
|
|||||||
_renderSystem.Dispose();
|
_renderSystem.Dispose();
|
||||||
_jobScheduler.Dispose();
|
_jobScheduler.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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (symbol.IsAbstract)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var iSettingsSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName("Ghost.Editor.Core.Assets.IAssetSettings");
|
var iSettingsSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName("Ghost.Editor.Core.Assets.IAssetSettings");
|
||||||
if (iSettingsSymbol == null)
|
if (iSettingsSymbol == null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -70,5 +70,9 @@ public struct MeshData
|
|||||||
public uint meshletBuffer;
|
public uint meshletBuffer;
|
||||||
public uint meshletVerticesBuffer;
|
public uint meshletVerticesBuffer;
|
||||||
public uint meshletTrianglesBuffer;
|
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
|
public uint materialSlotCount; // number of material slots baked into this mesh's meshlets
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -185,6 +185,22 @@ public struct Mesh : IResourceReleasable
|
|||||||
get; internal set;
|
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>
|
/// <summary>
|
||||||
/// Gets the handle to the mesh data buffer on the GPU.
|
/// Gets the handle to the mesh data buffer on the GPU.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -193,6 +209,19 @@ public struct Mesh : IResourceReleasable
|
|||||||
get; internal set;
|
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()
|
public void ReleaseCpuResources()
|
||||||
{
|
{
|
||||||
_vertices.Dispose();
|
_vertices.Dispose();
|
||||||
@@ -209,6 +238,8 @@ public struct Mesh : IResourceReleasable
|
|||||||
database.ReleaseResource(MeshLetBuffer.AsResource());
|
database.ReleaseResource(MeshLetBuffer.AsResource());
|
||||||
database.ReleaseResource(MeshletVerticesBuffer.AsResource());
|
database.ReleaseResource(MeshletVerticesBuffer.AsResource());
|
||||||
database.ReleaseResource(MeshletTrianglesBuffer.AsResource());
|
database.ReleaseResource(MeshletTrianglesBuffer.AsResource());
|
||||||
|
database.ReleaseResource(MeshletGroupBuffer.AsResource());
|
||||||
|
database.ReleaseResource(MeshletHierarchyBuffer.AsResource());
|
||||||
database.ReleaseResource(MeshDataBuffer.AsResource());
|
database.ReleaseResource(MeshDataBuffer.AsResource());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,22 +254,44 @@ public readonly unsafe ref struct RenderContext
|
|||||||
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
|
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
|
||||||
HeapType = HeapType.Default,
|
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.MeshLetBuffer = ResourceAllocator.CreateBuffer(in meshletDesc, "Meshlets");
|
||||||
meshRef.MeshletVerticesBuffer = ResourceAllocator.CreateBuffer(in verticesDesc, "MeshletVertices");
|
meshRef.MeshletVerticesBuffer = ResourceAllocator.CreateBuffer(in verticesDesc, "MeshletVertices");
|
||||||
meshRef.MeshletTrianglesBuffer = ResourceAllocator.CreateBuffer(in trianglesDesc, "MeshletTriangles");
|
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.MeshLetBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
|
||||||
TransitionBarrier(meshRef.MeshletVerticesBuffer.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.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.MeshLetBuffer, meshletData.meshlets.AsSpan());
|
||||||
UploadBuffer(meshRef.MeshletVerticesBuffer, meshletData.meshletVertices.AsSpan());
|
UploadBuffer(meshRef.MeshletVerticesBuffer, meshletData.meshletVertices.AsSpan());
|
||||||
UploadBuffer(meshRef.MeshletTrianglesBuffer, meshletData.meshletTriangles.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.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.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.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)
|
public void UpdateObjectData(Handle<Mesh> mesh)
|
||||||
@@ -290,6 +312,10 @@ public readonly unsafe ref struct RenderContext
|
|||||||
meshletBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshLetBuffer.AsResource()),
|
meshletBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshLetBuffer.AsResource()),
|
||||||
meshletVerticesBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshletVerticesBuffer.AsResource()),
|
meshletVerticesBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshletVerticesBuffer.AsResource()),
|
||||||
meshletTrianglesBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshletTrianglesBuffer.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,
|
materialSlotCount = (uint)meshData.MeshletData.materialSlotCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Ghost.Graphics.Core;
|
|||||||
using Ghost.Graphics.RHI;
|
using Ghost.Graphics.RHI;
|
||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using Misaki.HighPerformance.LowLevel.Collections;
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
using Misaki.HighPerformance.Mathematics.Geometry;
|
||||||
|
|
||||||
namespace Ghost.Graphics.Services;
|
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>
|
/// <summary>
|
||||||
/// Creates a new material instance using the specified shader.
|
/// Creates a new material instance using the specified shader.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ struct MeshData
|
|||||||
BYTE_ADDRESS_BUFFER meshletBuffer;
|
BYTE_ADDRESS_BUFFER meshletBuffer;
|
||||||
BYTE_ADDRESS_BUFFER meshletVerticesBuffer;
|
BYTE_ADDRESS_BUFFER meshletVerticesBuffer;
|
||||||
BYTE_ADDRESS_BUFFER meshletTrianglesBuffer;
|
BYTE_ADDRESS_BUFFER meshletTrianglesBuffer;
|
||||||
|
BYTE_ADDRESS_BUFFER meshletGroupBuffer;
|
||||||
|
BYTE_ADDRESS_BUFFER meshletHierarchyBuffer;
|
||||||
|
uint meshletCount;
|
||||||
|
uint lodLevelCount;
|
||||||
uint materialSlotCount;
|
uint materialSlotCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ public static unsafe class MeshBuilder
|
|||||||
public static void ComputeTangents(Span<Vertex> vertices, Span<uint> indices)
|
public static void ComputeTangents(Span<Vertex> vertices, Span<uint> indices)
|
||||||
{
|
{
|
||||||
using var scope = AllocationManager.CreateStackScope();
|
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)
|
for (var i = 0; i < indices.Length; i += 3)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Ghost.MicroTest;
|
using Ghost.MicroTest;
|
||||||
using Ghost.Test.Core;
|
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.Assets;
|
||||||
using Ghost.Editor.Core.Contracts;
|
using Ghost.Editor.Core.Contracts;
|
||||||
using Ghost.Editor.Core.Services;
|
using Ghost.Editor.Core.Services;
|
||||||
@@ -7,16 +8,21 @@ namespace Ghost.UnitTest.AssetSystem;
|
|||||||
[TestClass]
|
[TestClass]
|
||||||
public class AssertRegistryTest
|
public class AssertRegistryTest
|
||||||
{
|
{
|
||||||
private string _assetsRoot = null!;
|
|
||||||
private IAssetRegistry _registry = null!;
|
private IAssetRegistry _registry = null!;
|
||||||
|
|
||||||
|
public TestContext TestContext
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
[TestInitialize]
|
[TestInitialize]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
var testDir = Path.Combine(Path.GetTempPath(), "GhostEngineTests", Guid.NewGuid().ToString());
|
var testDir = Path.Combine(Path.GetTempPath(), "GhostEngineTests", Guid.NewGuid().ToString());
|
||||||
Directory.CreateDirectory(testDir);
|
Directory.CreateDirectory(testDir);
|
||||||
|
|
||||||
_assetsRoot = Path.Combine(testDir, "Assets");
|
EditorApplication.Initialize(null!, testDir, "Test");
|
||||||
|
|
||||||
_registry = new AssetRegistry();
|
_registry = new AssetRegistry();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,16 +35,20 @@ public class AssertRegistryTest
|
|||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task TestAssetRegistry_AutoImport()
|
public async Task TestAssetRegistry_AutoImport()
|
||||||
{
|
{
|
||||||
var sourcePath = "test.text";
|
var sourcePath = "Assets/test.text";
|
||||||
var fullSourcePath = Path.Combine(_assetsRoot, sourcePath);
|
await File.WriteAllBytesAsync(sourcePath, [1, 2, 3], TestContext.CancellationToken);
|
||||||
await File.WriteAllBytesAsync(fullSourcePath, [1, 2, 3]);
|
|
||||||
|
|
||||||
await Task.Delay(1000); // Wait for FSW to trigger
|
|
||||||
|
|
||||||
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));
|
Assert.IsTrue(File.Exists(metaPath));
|
||||||
|
|
||||||
var meta = await AssetMetaIO.ReadAsync(metaPath);
|
var meta = await AssetMetaIO.ReadAsync(metaPath, TestContext.CancellationToken);
|
||||||
Assert.IsNotNull(meta);
|
Assert.IsNotNull(meta);
|
||||||
|
|
||||||
var guid = _registry.GetAssetGuid(sourcePath);
|
var guid = _registry.GetAssetGuid(sourcePath);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public class AssetCatalogTests
|
|||||||
catalog.Upsert(meta, path);
|
catalog.Upsert(meta, path);
|
||||||
|
|
||||||
Assert.AreEqual(guid, catalog.GetGuid(path));
|
Assert.AreEqual(guid, catalog.GetGuid(path));
|
||||||
Assert.AreEqual(path, catalog.GetSourcePath(guid));
|
Assert.AreEqual(Path.GetFullPath(path).Replace('\\', '/'), catalog.GetSourcePath(guid));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -68,4 +68,35 @@ public class AssetCatalogTests
|
|||||||
Assert.AreEqual(1, referencers.Count);
|
Assert.AreEqual(1, referencers.Count);
|
||||||
Assert.AreEqual(asset1, referencers[0]);
|
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);
|
_jobScheduler = new JobScheduler(in schedulerDesc);
|
||||||
|
|
||||||
_assetManager = new AssetManager(_graphicsEngine.ResourceDatabase, _provider, _processor, _jobScheduler);
|
_assetManager = new AssetManager(_graphicsEngine.ResourceDatabase, _resourceManager, _provider, _processor, _jobScheduler);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCleanup]
|
[TestCleanup]
|
||||||
@@ -109,4 +109,89 @@ public class AssetManagerTest
|
|||||||
Assert.AreEqual(BarrierLayout.ShaderResource, data.layout);
|
Assert.AreEqual(BarrierLayout.ShaderResource, data.layout);
|
||||||
Assert.AreEqual(BarrierSync.AllShading, data.sync);
|
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.Core;
|
||||||
using Ghost.Engine;
|
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.Collections.Concurrent;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ghost.UnitTest.MockingEnvironment;
|
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)
|
public AssetType GetAssetType(Guid guid)
|
||||||
{
|
{
|
||||||
return _assets.TryGetValue(guid, out var asset) ? asset.type : AssetType.Unknown;
|
return _assets.TryGetValue(guid, out var asset) ? asset.type : AssetType.Unknown;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
using Ghost.Graphics.RHI;
|
using Ghost.Graphics.RHI;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ghost.UnitTest.MockingEnvironment;
|
namespace Ghost.UnitTest.MockingEnvironment;
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ internal unsafe class MockingResourceDatabase : IResourceDatabase
|
|||||||
public string? name;
|
public string? name;
|
||||||
public int refCount = 1;
|
public int refCount = 1;
|
||||||
public bool isShared;
|
public bool isShared;
|
||||||
|
public unsafe void* mappedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<ulong, MockResourceRecord> _resources = new();
|
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)
|
public void* MapResource(Handle<GPUResource> handle, uint subResource, ResourceRange? readRange)
|
||||||
{
|
{
|
||||||
// Real pointers are tricky in mocks unless native mem is allocated.
|
if (!_resources.TryGetValue(GetKey(handle), out var record))
|
||||||
// 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.");
|
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)
|
public void ReleaseResource(Handle<GPUResource> handle)
|
||||||
@@ -137,6 +151,12 @@ internal unsafe class MockingResourceDatabase : IResourceDatabase
|
|||||||
record.refCount--;
|
record.refCount--;
|
||||||
if (record.refCount <= 0)
|
if (record.refCount <= 0)
|
||||||
{
|
{
|
||||||
|
if (record.mappedData != null)
|
||||||
|
{
|
||||||
|
NativeMemory.Free(record.mappedData);
|
||||||
|
record.mappedData = null;
|
||||||
|
}
|
||||||
|
|
||||||
_resources.TryRemove(GetKey(handle), out _);
|
_resources.TryRemove(GetKey(handle), out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,6 +227,15 @@ internal unsafe class MockingResourceDatabase : IResourceDatabase
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
foreach (var record in _resources.Values)
|
||||||
|
{
|
||||||
|
if (record.mappedData != null)
|
||||||
|
{
|
||||||
|
NativeMemory.Free(record.mappedData);
|
||||||
|
record.mappedData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_resources.Clear();
|
_resources.Clear();
|
||||||
_samplers.Clear();
|
_samplers.Clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ public partial class Api
|
|||||||
// NOTE: Currently only support Windows.
|
// NOTE: Currently only support Windows.
|
||||||
if (libraryName == "dxcompiler")
|
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;
|
return dxcHandle;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public partial class Api
|
|||||||
OperatingSystem.IsMacOS() ? ".dylib" : "";
|
OperatingSystem.IsMacOS() ? ".dylib" : "";
|
||||||
|
|
||||||
var arch = Environment.Is64BitProcess ? "x64" : "x86";
|
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));
|
return NativeLibrary.Load(Path.Combine(nativeDllDir, libraryName + ext));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public partial class Api
|
|||||||
OperatingSystem.IsMacOS() ? ".dylib" : "";
|
OperatingSystem.IsMacOS() ? ".dylib" : "";
|
||||||
|
|
||||||
var arch = Environment.Is64BitProcess ? "x64" : "x86";
|
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));
|
return NativeLibrary.Load(Path.Combine(nativeDllDir, libraryName + ext));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public partial class Api
|
|||||||
OperatingSystem.IsMacOS() ? ".dylib" : "";
|
OperatingSystem.IsMacOS() ? ".dylib" : "";
|
||||||
|
|
||||||
var arch = Environment.Is64BitProcess ? "x64" : "x86";
|
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));
|
return NativeLibrary.Load(Path.Combine(nativeDllDir, libraryName + ext));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ public partial class Api
|
|||||||
OperatingSystem.IsMacOS() ? ".dylib" : "";
|
OperatingSystem.IsMacOS() ? ".dylib" : "";
|
||||||
|
|
||||||
var arch = Environment.Is64BitProcess ? "x64" : "x86";
|
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));
|
return NativeLibrary.Load(Path.Combine(nativeDllDir, libraryName + ext));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user