From 1a918116211ba19e9d540b4ecee6557a30c62bf6 Mon Sep 17 00:00:00 2001 From: Misaki Date: Sat, 25 Apr 2026 18:23:21 +0900 Subject: [PATCH] Refactor asset pipeline to use file paths, improve import - Switched asset handler interfaces and implementations to use file paths instead of FileStreams for all operations. - Refactored mesh asset structure and parsing, moved meshlet logic to MeshProcessor, and introduced hierarchical MeshNode types. - Updated texture asset handling: switched to bits-per-channel, improved mipmap/cubemap generation, and SPMD HDRI support. - Updated shader asset handlers to use file paths and split code compilation logic. - Improved asset registry: added event debouncing, better path handling, and import time/hash tracking. - Added source generator for IAssetSettings registration to support polymorphic JSON serialization. - Updated dependencies and tests; various minor fixes and cleanups. --- .../ShaderCompiler/DSLShaderCompiler.cs | 34 +- .../AssetHandler/MeshAssetHandler.cs | 380 ------------------ .../{AssetHandler => Assets}/AssetHandler.cs | 14 +- .../AssetHandlerRegistry.cs | 18 +- .../{AssetHandler => Assets}/AssetMeta.cs | 37 +- .../Assets/MeshAssetHandler.cs | 248 ++++++++++++ .../Assets/MeshProcessor.Import.cs | 302 ++++++++++++++ .../Assets/MeshProcessor.Meshlet.cs} | 136 ++++++- .../ShaderAssetHandler.cs | 26 +- .../TextureAssetHandler.cs | 73 ++-- .../TextureProcessor.Compress.cs} | 244 ++++++++--- .../Assets/TextureProcessor.GGXMip.cs | 338 ++++++++++++++++ .../Contracts/IAssetRegistry.cs | 2 +- .../Services/AssetCatalog.cs | 34 +- .../Services/AssetRegistry.cs | 100 +++-- .../Services/EditorContentProvider.cs | 2 +- .../Services/ImportCoordinator.cs | 17 +- src/Editor/Ghost.Editor/ActivationHandler.cs | 12 +- .../Ghost.Editor/Models/ExplorerItem.cs | 2 +- .../Controls/ContentBrowserViewModel.cs | 10 +- src/Runtime/Ghost.Core/Ghost.Core.csproj | 1 + .../Ghost.Engine/AssetManager.Texture.cs | 12 +- .../AssetHandlerRegistrationGenerator.cs | 70 ++++ src/Runtime/Ghost.Graphics/Core/Mesh.cs | 153 ------- .../Ghost.Graphics/Core/RenderContext.cs | 2 +- src/Test/Ghost.MicroTest/Program.cs | 2 +- src/Test/Ghost.Shader.Test/Program.cs | 2 +- 27 files changed, 1523 insertions(+), 748 deletions(-) delete mode 100644 src/Editor/Ghost.Editor.Core/AssetHandler/MeshAssetHandler.cs rename src/Editor/Ghost.Editor.Core/{AssetHandler => Assets}/AssetHandler.cs (54%) rename src/Editor/Ghost.Editor.Core/{AssetHandler => Assets}/AssetHandlerRegistry.cs (83%) rename src/Editor/Ghost.Editor.Core/{AssetHandler => Assets}/AssetMeta.cs (76%) create mode 100644 src/Editor/Ghost.Editor.Core/Assets/MeshAssetHandler.cs create mode 100644 src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Import.cs rename src/{Runtime/Ghost.Graphics/Utilities/MeshletUtility.cs => Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs} (82%) rename src/Editor/Ghost.Editor.Core/{AssetHandler => Assets}/ShaderAssetHandler.cs (75%) rename src/Editor/Ghost.Editor.Core/{AssetHandler => Assets}/TextureAssetHandler.cs (83%) rename src/Editor/Ghost.Editor.Core/{AssetHandler/TextureProcessor.cs => Assets/TextureProcessor.Compress.cs} (50%) create mode 100644 src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.GGXMip.cs diff --git a/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs b/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs index 94cade3..3524960 100644 --- a/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs +++ b/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs @@ -159,18 +159,27 @@ public static class DSLShaderCompiler public static Result CompileGraphicsShader(Stream stream) { using var reader = new StreamReader(stream); - return CompileGraphicsShader(reader.ReadToEnd()); + return CompileGraphicsShaderCode(reader.ReadToEnd()); } public static Result CompileGraphicsShader(string shaderPath) + { + if (!File.Exists(shaderPath)) + { + return Result.Failure("Shader file not found: " + shaderPath); + } + + var code = File.ReadAllText(shaderPath); + return CompileGraphicsShaderCode(code); + } + + public static Result CompileGraphicsShaderCode(string shaderCode) { try { - var source = File.ReadAllText(shaderPath); - // Use ANTLR4 parser var parseErrors = new List(); - var shaderModels = AntlrShaderCompiler.ParseShaders(source, parseErrors); + var shaderModels = AntlrShaderCompiler.ParseShaders(shaderCode, parseErrors); if (parseErrors.Count != 0) { @@ -219,17 +228,26 @@ public static class DSLShaderCompiler public static Result CompileComputeShader(Stream stream) { using var reader = new StreamReader(stream); - return CompileComputeShader(reader.ReadToEnd()); + return CompileComputeShaderCode(reader.ReadToEnd()); } public static Result CompileComputeShader(string shaderPath) + { + if (!File.Exists(shaderPath)) + { + return Result.Failure("Shader file not found: " + shaderPath); + } + + var code = File.ReadAllText(shaderPath); + return CompileComputeShaderCode(code); + } + + public static Result CompileComputeShaderCode(string shaderCode) { try { - var source = File.ReadAllText(shaderPath); - var parseErrors = new List(); - var shaderModels = AntlrShaderCompiler.ParseComputeShaders(source, parseErrors); + var shaderModels = AntlrShaderCompiler.ParseComputeShaders(shaderCode, parseErrors); if (parseErrors.Count != 0) { diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/MeshAssetHandler.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/MeshAssetHandler.cs deleted file mode 100644 index 93521c5..0000000 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/MeshAssetHandler.cs +++ /dev/null @@ -1,380 +0,0 @@ -using Ghost.Core; -using Ghost.Engine; -using Ghost.Graphics.RHI; -using Ghost.Graphics.Utilities; -using Ghost.MeshOptimizer; -using Ghost.Ufbx; -using Misaki.HighPerformance.LowLevel; -using Misaki.HighPerformance.LowLevel.Buffer; -using Misaki.HighPerformance.LowLevel.Collections; -using Misaki.HighPerformance.LowLevel.Utilities; -using Misaki.HighPerformance.Mathematics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Text; - -namespace Ghost.Editor.Core.AssetHandler; - -public abstract class MeshAsset : IAsset -{ - - private UnsafeList _vertices; - private UnsafeList _indices; - - public Guid ID - { - get; - } - - public IAssetSettings Settings - { - get; - } - - public Guid TypeID => typeof(MeshAsset).GUID; - - public Span Vertices => _vertices.AsSpan(); - public Span Indices => _indices.AsSpan(); - - internal MeshAsset(ref UnsafeList vertices, ref UnsafeList indices, Guid id, MeshAssetSettings settings) - { - _vertices = vertices; - _indices = indices; - - ID = id; - Settings = settings; - } - - public void Dispose() - { - _vertices.Dispose(); - _indices.Dispose(); - } -} - -[Guid(GUID)] -public partial class FBXAsset : MeshAsset -{ - public const string GUID = "B99CA68E-EE7A-4822-BF1C-AA0A5120C36A"; - - internal FBXAsset(ref UnsafeList vertices, ref UnsafeList indices, Guid id, FbxAssetSettings settings) - : base(ref vertices, ref indices, id, settings) - { - } -} - - -public enum CoordinateAxis -{ - PositiveX, - PositiveY, - PositiveZ, - NegativeX, - NegativeY, - NegativeZ -} - -public enum VertexDataSource -{ - Imported, - Computed, - ComputedIfMissing -} - -public class MeshAssetSettings : IAssetSettings -{ - public VertexDataSource NormalDataSource - { - get; set; - } = VertexDataSource.ComputedIfMissing; - - public VertexDataSource TangentDataSource - { - get; set; - } = VertexDataSource.ComputedIfMissing; -} - -internal class ObjAssetSettings : MeshAssetSettings -{ - public CoordinateAxis ObjectUpAxis - { - get; set; - } = CoordinateAxis.PositiveY; - - public CoordinateAxis ObjectForwardAxis - { - get; set; - } = CoordinateAxis.NegativeZ; - - public CoordinateAxis ObjectRightAxis - { - get; set; - } = CoordinateAxis.PositiveX; - - public float UnitMeterScale - { - get; set; - } = 1.0f; -} - -internal class FbxAssetSettings : MeshAssetSettings -{ -} - -internal class MeshParsingWorkItem : IThreadPoolWorkItem -{ - private readonly string _filePath; - private readonly AllocationHandle _allocationHandle; - private readonly MeshAssetSettings _settings; - private readonly TaskCompletionSource _taskCompletionSource; - - public UnsafeList vertices; - public UnsafeList indices; - - public Task Task => _taskCompletionSource.Task; - - public MeshParsingWorkItem(string filePath, AllocationHandle allocationHandle, MeshAssetSettings settings) - { - _filePath = filePath; - _allocationHandle = allocationHandle; - _settings = settings; - _taskCompletionSource = new TaskCompletionSource(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float4 ComputeTangent(float3 t, float3 n, float3 b) - { - var proj = n * math.dot(n, t); - t = math.normalize(t - proj); - var w = math.dot(math.cross(n, t), b) < 0.0f ? -1.0f : 1.0f; - return new float4(t.xyz, w); - } - - private static ufbx_coordinate_axis ToUfbxCoordinateAxis(CoordinateAxis axis) - { - return axis switch - { - CoordinateAxis.PositiveX => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_POSITIVE_X, - CoordinateAxis.PositiveY => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_POSITIVE_Y, - CoordinateAxis.PositiveZ => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_POSITIVE_Z, - CoordinateAxis.NegativeX => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_NEGATIVE_X, - CoordinateAxis.NegativeY => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_NEGATIVE_Y, - CoordinateAxis.NegativeZ => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_NEGATIVE_Z, - _ => throw new ArgumentOutOfRangeException(nameof(axis), axis, null) - }; - } - - public unsafe void Execute() - { - if (!File.Exists(_filePath)) - { - _taskCompletionSource.SetResult(Result.Failure("Invalid file path.")); - return; - } - - if (!Path.GetExtension(_filePath).Equals(".obj", StringComparison.OrdinalIgnoreCase) - && !Path.GetExtension(_filePath).Equals(".fbx", StringComparison.OrdinalIgnoreCase)) - { - _taskCompletionSource.SetResult(Result.Failure("Unsupported file format. Only .obj and .fbx are supported.")); - return; - } - - var error = new ufbx_error(); - var load_Opts = new ufbx_load_opts - { - target_unit_meters = 1.0f, - target_axes = ufbx_coordinate_axes.left_handed_y_up, - // Force z-axis mirroring to correctly convert handedness to Left-Handed, - // while preserving correct left/right orientation when viewed from the front. - handedness_conversion_axis = ufbx_mirror_axis.UFBX_MIRROR_AXIS_Z, - space_conversion = ufbx_space_conversion.UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY, - }; - - if (_settings is ObjAssetSettings objSettings) - { - load_Opts.obj_axes = new ufbx_coordinate_axes - { - right = ToUfbxCoordinateAxis(objSettings.ObjectRightAxis), - up = ToUfbxCoordinateAxis(objSettings.ObjectUpAxis), - front = ToUfbxCoordinateAxis(objSettings.ObjectForwardAxis) - }; - - load_Opts.obj_unit_meters = objSettings.UnitMeterScale; - load_Opts.obj_search_mtl_by_filename = true; - } - - using var str = new UnsafeArray(Encoding.UTF8.GetByteCount(_filePath) + 1, AllocationHandle.FreeList); - var count = Encoding.UTF8.GetBytes(_filePath, str.AsSpan()); - str[count] = 0; - - using var scene = new DisposablePtr(ufbx_scene.LoadFile((sbyte*)str.GetUnsafePtr(), &load_Opts, &error)); - if (scene.Get() == null) - { - _taskCompletionSource.SetResult(Result.Failure(error.description.ToString())); - return; - } - - using var flatVertices = new UnsafeList(1024, AllocationHandle.FreeList); - - var missingNormals = false; - var missingTangents = false; - - for (var i = 0u; i < scene.Get()->nodes.count; i++) - { - var data = scene.Get()->nodes.data; - var node = scene.Get()->nodes.data[i]; - if (node->is_root) - { - continue; - } - - if (node->mesh != null) - { - var pMesh = node->mesh; - if (pMesh->num_faces == 0) - { - continue; - } - - var maxScratchIndices = (int)(pMesh->max_face_triangles * 3u); - - using var triIndicesArray = new UnsafeArray(maxScratchIndices, AllocationHandle.FreeList); - - for (var j = 0u; j < pMesh->num_faces; j++) - { - var face = pMesh->faces.data[j]; - - var numTris = UfbxApi.TriangulateFace(triIndicesArray.AsSpan(0, maxScratchIndices), pMesh, face); - - var totalIndices = numTris * 3; - for (var k = 0; k < totalIndices; k++) - { - var ufbxTopologyIndex = triIndicesArray[k]; - - var posIdx = pMesh->vertex_position.indices.data[ufbxTopologyIndex]; - var normIdx = pMesh->vertex_normal.exists ? pMesh->vertex_normal.indices.data[ufbxTopologyIndex] : uint.MaxValue; - var tanIdx = pMesh->vertex_tangent.exists ? pMesh->vertex_tangent.indices.data[ufbxTopologyIndex] : uint.MaxValue; - var uvIdx = pMesh->vertex_uv.exists ? pMesh->vertex_uv.indices.data[ufbxTopologyIndex] : uint.MaxValue; - var colIdx = pMesh->vertex_color.exists ? pMesh->vertex_color.indices.data[ufbxTopologyIndex] : uint.MaxValue; - var btanIdx = pMesh->vertex_bitangent.exists ? pMesh->vertex_bitangent.indices.data[ufbxTopologyIndex] : uint.MaxValue; - - var position = pMesh->vertex_position.values.data[posIdx]; - var normal = normIdx != uint.MaxValue ? pMesh->vertex_normal.values.data[normIdx] : default; - var uv = uvIdx != uint.MaxValue ? pMesh->vertex_uv.values.data[uvIdx] : default; - var color = colIdx != uint.MaxValue ? pMesh->vertex_color.values.data[colIdx] : default; - - var vertex = new Vertex - { - position = new float3(position.x, position.y, position.z), - normal = new float3(normal.x, normal.y, normal.z), - uv = new float2(uv.x, uv.y), - color = new Color128(color.x, color.y, color.z, color.w) - }; - - if (tanIdx != uint.MaxValue) - { - var mt = pMesh->vertex_tangent.values.data[tanIdx]; - var mb = btanIdx != uint.MaxValue ? pMesh->vertex_bitangent.values.data[btanIdx] : default; - - var t = new float3(mt.x, mt.y, mt.z); - var n = vertex.normal; - var b = btanIdx != uint.MaxValue ? new float3(mb.x, mb.y, mb.z) : math.cross(n, t); - vertex.tangent = ComputeTangent(t, n, b); - } - - var newIndex = (uint)flatVertices.Count; - - flatVertices.Add(vertex); - - if (!missingNormals) - { - missingNormals = normIdx == uint.MaxValue; - } - - if (!missingTangents) - { - missingTangents = tanIdx == uint.MaxValue || btanIdx == uint.MaxValue; - } - } - } - } - } - - var numIndices = (uint)flatVertices.Count; - - using var weldedIndices = new UnsafeArray((int)numIndices, AllocationHandle.FreeList); - using var cachedIndices = new UnsafeArray((int)numIndices, AllocationHandle.FreeList); - - var stream = new ufbx_vertex_stream - { - data = flatVertices.GetUnsafePtr(), - vertex_count = numIndices, - vertex_size = (nuint)sizeof(Vertex) - }; - - var numUniqueVertices = UfbxApi.GenerateIndices([stream], weldedIndices, null, &error); - if (numUniqueVertices == 0 && error.type != ufbx_error_type.UFBX_ERROR_NONE) - { - _taskCompletionSource.SetResult(Result.Failure($"Welding failed: {error.description}")); - return; - } - - MeshOptApi.OptimizeVertexCache((uint*)cachedIndices.GetUnsafePtr(), (uint*)weldedIndices.GetUnsafePtr(), numIndices, numUniqueVertices); - - vertices = new UnsafeList((int)numUniqueVertices, _allocationHandle); - indices = new UnsafeList((int)numIndices, _allocationHandle); - - var finalVertexCount = MeshOptApi.OptimizeVertexFetch(vertices.GetUnsafePtr(), (uint*)cachedIndices.GetUnsafePtr(), numIndices, flatVertices.GetUnsafePtr(), numIndices, (nuint)sizeof(Vertex)); - - vertices.UnsafeSetCount((int)finalVertexCount); - - MemoryUtility.MemCpy(indices.GetUnsafePtr(), cachedIndices.GetUnsafePtr(), numIndices * sizeof(uint)); - indices.UnsafeSetCount((int)numIndices); - - if (_settings.NormalDataSource == VertexDataSource.Computed || (_settings.NormalDataSource == VertexDataSource.ComputedIfMissing && missingNormals)) - { - MeshBuilder.ComputeNormal(vertices, indices); - } - - if (_settings.TangentDataSource == VertexDataSource.Computed || (_settings.TangentDataSource == VertexDataSource.ComputedIfMissing && missingTangents)) - { - MeshBuilder.ComputeTangents(vertices, indices); - } - - _taskCompletionSource.SetResult(Result.Success()); - } -} - -internal class FBXAssetHandler : IImportableAssetHandler -{ - public AssetType RuntimeAssetType => AssetType.Mesh; - - public Guid EditorAssetTypeID => typeof(FBXAsset).GUID; - - public bool CanExport => false; - - public IAssetSettings? CreateDefaultSettings() - { - throw new NotImplementedException(); - } - - public ValueTask> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) - { - throw new NotImplementedException(); - } - - public ValueTask SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default) - { - throw new NotImplementedException(); - } - - public ValueTask ImportAsync(FileStream sourceStream, FileStream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) - { - throw new NotImplementedException(); - } - - public ValueTask ExportAsync(FileStream assetStream, FileStream targetStream, IAssetExportOptions? options, CancellationToken token = default) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs b/src/Editor/Ghost.Editor.Core/Assets/AssetHandler.cs similarity index 54% rename from src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs rename to src/Editor/Ghost.Editor.Core/Assets/AssetHandler.cs index 2bffe1a..954780c 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/AssetHandler.cs @@ -1,7 +1,7 @@ using Ghost.Core; using Ghost.Engine; -namespace Ghost.Editor.Core.AssetHandler; +namespace Ghost.Editor.Core.Assets; [AttributeUsage(AttributeTargets.Class)] public sealed class CustomAssetHandlerAttribute : Attribute @@ -23,7 +23,7 @@ public interface IAsset : IDisposable get; } - public IAssetSettings Settings + public IAssetSettings? Settings { get; } @@ -38,18 +38,18 @@ public interface IAssetHandler IAssetSettings? CreateDefaultSettings(); - ValueTask> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default); - ValueTask SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default); + ValueTask> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default); + ValueTask SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default); } public interface IImportableAssetHandler : IAssetHandler { bool CanExport { get; } - ValueTask ImportAsync(FileStream sourceStream, FileStream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default); - ValueTask ExportAsync(FileStream assetStream, FileStream targetStream, IAssetExportOptions? options, CancellationToken token = default); + ValueTask ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default); + ValueTask ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default); } public interface IPackableAssetHandler : IAssetHandler { - ValueTask PackAsync(FileStream assetStream, Stream targetStream, CancellationToken token = default); + ValueTask PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default); } \ No newline at end of file diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandlerRegistry.cs b/src/Editor/Ghost.Editor.Core/Assets/AssetHandlerRegistry.cs similarity index 83% rename from src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandlerRegistry.cs rename to src/Editor/Ghost.Editor.Core/Assets/AssetHandlerRegistry.cs index 52d5bae..8beb733 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandlerRegistry.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/AssetHandlerRegistry.cs @@ -1,6 +1,6 @@ using Ghost.Engine; -namespace Ghost.Editor.Core.AssetHandler; +namespace Ghost.Editor.Core.Assets; public static class AssetHandlerRegistry { @@ -9,12 +9,16 @@ public static class AssetHandlerRegistry private static readonly Dictionary s_byTypeId; private static readonly Dictionary s_versionByTypeId; + private static readonly List<(Type Type, string Name)> s_iAssetSettingsTypes; + static AssetHandlerRegistry() { s_byExtension = new Dictionary(StringComparer.OrdinalIgnoreCase); s_typeByExtension = new Dictionary(StringComparer.OrdinalIgnoreCase); s_byTypeId = new Dictionary(); s_versionByTypeId = new Dictionary(); + + s_iAssetSettingsTypes = new List<(Type Type, string Name)>(); } public static void RegisterHandler(IAssetHandler handler, Guid assetTypeId, ReadOnlySpan extensions, int version) @@ -30,6 +34,11 @@ public static class AssetHandlerRegistry } } + public static void RegisterIAssetSettingsType(Type type, string name) + { + s_iAssetSettingsTypes.Add((type, name)); + } + public static IAssetHandler? GetByExtension(string extension) { if (string.IsNullOrEmpty(extension)) @@ -69,4 +78,9 @@ public static class AssetHandlerRegistry var normalized = extension.StartsWith('.') ? extension : "." + extension; return s_typeByExtension.GetValueOrDefault(normalized, AssetType.Unknown); } -} + + public static IReadOnlyCollection<(Type Type, string Name)> GetIAssetSettingsTypes() + { + return s_iAssetSettingsTypes; + } +} \ No newline at end of file diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetMeta.cs b/src/Editor/Ghost.Editor.Core/Assets/AssetMeta.cs similarity index 76% rename from src/Editor/Ghost.Editor.Core/AssetHandler/AssetMeta.cs rename to src/Editor/Ghost.Editor.Core/Assets/AssetMeta.cs index 6ee9d4f..e05a0a2 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetMeta.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/AssetMeta.cs @@ -1,18 +1,16 @@ -using Ghost.Engine; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; -namespace Ghost.Editor.Core.AssetHandler; +namespace Ghost.Editor.Core.Assets; /// /// Mark IAssetSettings for polymorphic serialization. /// Each handler type will register its own derived type. /// -[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] -[JsonDerivedType(typeof(DefaultAssetSettings), "Default")] public interface IAssetSettings; -public sealed class DefaultAssetSettings : IAssetSettings; +internal sealed class DefaultAssetSettings : IAssetSettings; /// /// Persisted as a JSON sidecar (.gmeta) next to every source asset. @@ -76,9 +74,36 @@ internal static class AssetMetaIO WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = { ConfigureAssetSettingsPolymorphism } + }, + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), + } }; + private static void ConfigureAssetSettingsPolymorphism(JsonTypeInfo typeInfo) + { + if (typeInfo.Type != typeof(IAssetSettings)) + { + return; + } + + typeInfo.PolymorphismOptions = new JsonPolymorphismOptions + { + TypeDiscriminatorPropertyName = "$type", + IgnoreUnrecognizedTypeDiscriminators = true, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor + }; + + foreach (var setting in AssetHandlerRegistry.GetIAssetSettingsTypes()) + { + typeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(setting.Type, setting.Name)); + } + } + public static async ValueTask ReadAsync(string metaPath, CancellationToken token = default) { if (!File.Exists(metaPath)) diff --git a/src/Editor/Ghost.Editor.Core/Assets/MeshAssetHandler.cs b/src/Editor/Ghost.Editor.Core/Assets/MeshAssetHandler.cs new file mode 100644 index 0000000..3c32cd5 --- /dev/null +++ b/src/Editor/Ghost.Editor.Core/Assets/MeshAssetHandler.cs @@ -0,0 +1,248 @@ +using Ghost.Core; +using Ghost.Engine; +using Ghost.Graphics.RHI; +using Misaki.HighPerformance.LowLevel.Collections; +using Misaki.HighPerformance.Mathematics; +using System.Runtime.InteropServices; + +namespace Ghost.Editor.Core.Assets; + +public class MeshNode : IDisposable +{ + public required string Name + { + get; set; + } + + public float4x4 LocalTransform + { + get; set; + } + + public MeshNode? Parent + { + get; init; + } + + public required IReadOnlyCollection Children + { + get; set; + } + + ~MeshNode() + { + Dispose(false); + } + + protected virtual void Dispose(bool disposing) + { + } + + public void Dispose() + { + foreach (var child in Children) + { + child.Dispose(); + } + + Dispose(true); + GC.SuppressFinalize(this); + } +} + +public class GeometryMeshNode : MeshNode +{ + private UnsafeList _vertices; + private UnsafeList _indices; + + public UnsafeList Vertices + { + get => _vertices; + set + { + _vertices.Dispose(); + _vertices = value; + } + } + + public UnsafeList Indices + { + get => _indices; + set + { + _indices.Dispose(); + _indices = value; + } + } + + public int MaterialIndex + { + get; set; + } + + protected override void Dispose(bool disposing) + { + _vertices.Dispose(); + _indices.Dispose(); + } +} + +public class LightMeshNode : MeshNode +{ + public float3 Color + { + get; set; + } + + public float Intensity + { + get; set; + } +} + +public abstract class MeshAsset : IAsset +{ + private MeshNode _root; + + public Guid ID + { + get; + } + + public IAssetSettings Settings + { + get; + } + + public Guid TypeID => typeof(MeshAsset).GUID; + + public MeshNode Root + { + get => _root; + set + { + _root?.Dispose(); + _root = value; + } + } + + internal MeshAsset(MeshNode root, Guid id, MeshAssetSettings settings) + { + _root = root; + + ID = id; + Settings = settings; + } + + public void Dispose() + { + _root?.Dispose(); + } +} + +[Guid(GUID)] +public partial class FBXAsset : MeshAsset +{ + public const string GUID = "B99CA68E-EE7A-4822-BF1C-AA0A5120C36A"; + + internal FBXAsset(MeshNode root, Guid id, FbxAssetSettings settings) + : base(root, id, settings) + { + } +} + +public enum CoordinateAxis +{ + PositiveX, + PositiveY, + PositiveZ, + NegativeX, + NegativeY, + NegativeZ +} + +public enum VertexDataSource +{ + Imported, + Computed, + ComputedIfMissing +} + +public class MeshAssetSettings : IAssetSettings +{ + public VertexDataSource NormalDataSource + { + get; set; + } = VertexDataSource.ComputedIfMissing; + + public VertexDataSource TangentDataSource + { + get; set; + } = VertexDataSource.ComputedIfMissing; + + public bool BuildMeshlets + { + get; set; + } = true; +} + +internal class ObjAssetSettings : MeshAssetSettings +{ + public CoordinateAxis ObjectUpAxis + { + get; set; + } = CoordinateAxis.PositiveY; + + public CoordinateAxis ObjectForwardAxis + { + get; set; + } = CoordinateAxis.NegativeZ; + + public CoordinateAxis ObjectRightAxis + { + get; set; + } = CoordinateAxis.PositiveX; + + public float UnitMeterScale + { + get; set; + } = 1.0f; +} + +internal class FbxAssetSettings : MeshAssetSettings +{ +} + +internal class FBXAssetHandler : IImportableAssetHandler +{ + public AssetType RuntimeAssetType => AssetType.Mesh; + + public Guid EditorAssetTypeID => typeof(FBXAsset).GUID; + + public bool CanExport => false; + + public IAssetSettings? CreateDefaultSettings() + { + throw new NotImplementedException(); + } + + public ValueTask> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public ValueTask SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public ValueTask ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public ValueTask ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Import.cs b/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Import.cs new file mode 100644 index 0000000..094da21 --- /dev/null +++ b/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Import.cs @@ -0,0 +1,302 @@ +using Ghost.Core; +using Ghost.Graphics.RHI; +using Ghost.Graphics.Utilities; +using Ghost.MeshOptimizer; +using Ghost.Ufbx; +using Misaki.HighPerformance.Jobs; +using Misaki.HighPerformance.LowLevel; +using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Collections; +using Misaki.HighPerformance.LowLevel.Utilities; +using Misaki.HighPerformance.Mathematics; +using System.Runtime.CompilerServices; +using System.Text; +using System.Xml.Linq; + +namespace Ghost.Editor.Core.Assets; + +internal unsafe class MeshParsingWorkItem : IJob +{ + private readonly string _filePath; + private readonly AllocationHandle _allocationHandle; + private readonly MeshAssetSettings _settings; + private readonly TaskCompletionSource> _taskCompletionSource; + + public UnsafeList vertices; + public UnsafeList indices; + + public Task> Task => _taskCompletionSource.Task; + + public MeshParsingWorkItem(string filePath, AllocationHandle allocationHandle, MeshAssetSettings settings) + { + _filePath = filePath; + _allocationHandle = allocationHandle; + _settings = settings; + _taskCompletionSource = new TaskCompletionSource>(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float4 ComputeTangent(float3 t, float3 n, float3 b) + { + var proj = n * math.dot(n, t); + t = math.normalize(t - proj); + var w = math.dot(math.cross(n, t), b) < 0.0f ? -1.0f : 1.0f; + return new float4(t.xyz, w); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ufbx_coordinate_axis ToUfbxCoordinateAxis(CoordinateAxis axis) + { + return axis switch + { + CoordinateAxis.PositiveX => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_POSITIVE_X, + CoordinateAxis.PositiveY => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_POSITIVE_Y, + CoordinateAxis.PositiveZ => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_POSITIVE_Z, + CoordinateAxis.NegativeX => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_NEGATIVE_X, + CoordinateAxis.NegativeY => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_NEGATIVE_Y, + CoordinateAxis.NegativeZ => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_NEGATIVE_Z, + _ => throw new ArgumentOutOfRangeException(nameof(axis), axis, null) + }; + } + + private GeometryMeshNode ParseGeometry(ufbx_mesh* pMesh) + { + var meshNode = new GeometryMeshNode + { + Name = pMesh->name.ToString(), + Children = Array.Empty(), + }; + + if (pMesh->num_faces == 0) + { + return meshNode; + } + + var missingNormals = false; + var missingTangents = false; + + using var flatVertices = new UnsafeList(1024, AllocationHandle.FreeList); + + var maxScratchIndices = (int)(pMesh->max_face_triangles * 3u); + + using var triIndicesArray = new UnsafeArray(maxScratchIndices, AllocationHandle.FreeList); + + for (var j = 0u; j < pMesh->num_faces; j++) + { + var face = pMesh->faces.data[j]; + + var numTris = UfbxApi.TriangulateFace(triIndicesArray.AsSpan(0, maxScratchIndices), pMesh, face); + + var totalIndices = numTris * 3; + for (var k = 0; k < totalIndices; k++) + { + var ufbxTopologyIndex = triIndicesArray[k]; + + var posIdx = pMesh->vertex_position.indices.data[ufbxTopologyIndex]; + var normIdx = pMesh->vertex_normal.exists ? pMesh->vertex_normal.indices.data[ufbxTopologyIndex] : uint.MaxValue; + var tanIdx = pMesh->vertex_tangent.exists ? pMesh->vertex_tangent.indices.data[ufbxTopologyIndex] : uint.MaxValue; + var uvIdx = pMesh->vertex_uv.exists ? pMesh->vertex_uv.indices.data[ufbxTopologyIndex] : uint.MaxValue; + var colIdx = pMesh->vertex_color.exists ? pMesh->vertex_color.indices.data[ufbxTopologyIndex] : uint.MaxValue; + var btanIdx = pMesh->vertex_bitangent.exists ? pMesh->vertex_bitangent.indices.data[ufbxTopologyIndex] : uint.MaxValue; + + var position = pMesh->vertex_position.values.data[posIdx]; + var normal = normIdx != uint.MaxValue ? pMesh->vertex_normal.values.data[normIdx] : default; + var uv = uvIdx != uint.MaxValue ? pMesh->vertex_uv.values.data[uvIdx] : default; + var color = colIdx != uint.MaxValue ? pMesh->vertex_color.values.data[colIdx] : default; + + var vertex = new Vertex + { + position = new float3(position.x, position.y, position.z), + normal = new float3(normal.x, normal.y, normal.z), + uv = new float2(uv.x, uv.y), + color = new Color128(color.x, color.y, color.z, color.w) + }; + + if (tanIdx != uint.MaxValue) + { + var mt = pMesh->vertex_tangent.values.data[tanIdx]; + var mb = btanIdx != uint.MaxValue ? pMesh->vertex_bitangent.values.data[btanIdx] : default; + + var t = new float3(mt.x, mt.y, mt.z); + var n = vertex.normal; + var b = btanIdx != uint.MaxValue ? new float3(mb.x, mb.y, mb.z) : math.cross(n, t); + vertex.tangent = ComputeTangent(t, n, b); + } + + var newIndex = (uint)flatVertices.Count; + + flatVertices.Add(vertex); + + if (!missingNormals) + { + missingNormals = normIdx == uint.MaxValue; + } + + if (!missingTangents) + { + missingTangents = tanIdx == uint.MaxValue || btanIdx == uint.MaxValue; + } + } + } + + var numIndices = (uint)flatVertices.Count; + + using var weldedIndices = new UnsafeArray((int)numIndices, AllocationHandle.FreeList); + using var cachedIndices = new UnsafeArray((int)numIndices, AllocationHandle.FreeList); + + var stream = new ufbx_vertex_stream + { + data = flatVertices.GetUnsafePtr(), + vertex_count = numIndices, + vertex_size = (nuint)sizeof(Vertex) + }; + + var error = new ufbx_error(); + var numUniqueVertices = UfbxApi.GenerateIndices([stream], weldedIndices, null, &error); + if (numUniqueVertices == 0 && error.type != ufbx_error_type.UFBX_ERROR_NONE) + { + return; + } + + MeshOptApi.OptimizeVertexCache((uint*)cachedIndices.GetUnsafePtr(), (uint*)weldedIndices.GetUnsafePtr(), numIndices, numUniqueVertices); + + vertices = new UnsafeList((int)numUniqueVertices, _allocationHandle); + indices = new UnsafeList((int)numIndices, _allocationHandle); + + var finalVertexCount = MeshOptApi.OptimizeVertexFetch(vertices.GetUnsafePtr(), (uint*)cachedIndices.GetUnsafePtr(), numIndices, flatVertices.GetUnsafePtr(), numIndices, (nuint)sizeof(Vertex)); + + vertices.UnsafeSetCount((int)finalVertexCount); + + MemoryUtility.MemCpy(indices.GetUnsafePtr(), cachedIndices.GetUnsafePtr(), numIndices * sizeof(uint)); + indices.UnsafeSetCount((int)numIndices); + + if (_settings.NormalDataSource == VertexDataSource.Computed || (_settings.NormalDataSource == VertexDataSource.ComputedIfMissing && missingNormals)) + { + MeshBuilder.ComputeNormal(vertices, indices); + } + + if (_settings.TangentDataSource == VertexDataSource.Computed || (_settings.TangentDataSource == VertexDataSource.ComputedIfMissing && missingTangents)) + { + MeshBuilder.ComputeTangents(vertices, indices); + } + } + + public void Execute(ref readonly JobExecutionContext context) + { + if (!File.Exists(_filePath)) + { + _taskCompletionSource.SetResult(Result.Failure("Invalid file path.")); + return; + } + + if (!Path.GetExtension(_filePath).Equals(".obj", StringComparison.OrdinalIgnoreCase) + && !Path.GetExtension(_filePath).Equals(".fbx", StringComparison.OrdinalIgnoreCase)) + { + _taskCompletionSource.SetResult(Result.Failure("Unsupported file format. Only .obj and .fbx are supported.")); + return; + } + + var error = new ufbx_error(); + var load_Opts = new ufbx_load_opts + { + target_unit_meters = 1.0f, + target_axes = ufbx_coordinate_axes.left_handed_y_up, + // Force z-axis mirroring to correctly convert handedness to Left-Handed, + // while preserving correct left/right orientation when viewed from the front. + handedness_conversion_axis = ufbx_mirror_axis.UFBX_MIRROR_AXIS_Z, + space_conversion = ufbx_space_conversion.UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY, + }; + + if (_settings is ObjAssetSettings objSettings) + { + load_Opts.obj_axes = new ufbx_coordinate_axes + { + right = ToUfbxCoordinateAxis(objSettings.ObjectRightAxis), + up = ToUfbxCoordinateAxis(objSettings.ObjectUpAxis), + front = ToUfbxCoordinateAxis(objSettings.ObjectForwardAxis) + }; + + load_Opts.obj_unit_meters = objSettings.UnitMeterScale; + load_Opts.obj_search_mtl_by_filename = true; + } + + using var str = new UnsafeArray(Encoding.UTF8.GetByteCount(_filePath) + 1, AllocationHandle.FreeList); + var count = Encoding.UTF8.GetBytes(_filePath, str.AsSpan()); + str[count] = 0; + + using var scene = new DisposablePtr(ufbx_scene.LoadFile((sbyte*)str.GetUnsafePtr(), &load_Opts, &error)); + if (scene.Get() == null) + { + _taskCompletionSource.SetResult(Result.Failure(error.description.ToString())); + return; + } + + using var flatVertices = new UnsafeList(1024, AllocationHandle.FreeList); + + var missingNormals = false; + var missingTangents = false; + + for (var i = 0u; i < scene.Get()->nodes.count; i++) + { + var data = scene.Get()->nodes.data; + var node = scene.Get()->nodes.data[i]; + if (node->is_root) + { + continue; + } + + if (node->mesh != null) + { + + } + } + + var numIndices = (uint)flatVertices.Count; + + using var weldedIndices = new UnsafeArray((int)numIndices, AllocationHandle.FreeList); + using var cachedIndices = new UnsafeArray((int)numIndices, AllocationHandle.FreeList); + + var stream = new ufbx_vertex_stream + { + data = flatVertices.GetUnsafePtr(), + vertex_count = numIndices, + vertex_size = (nuint)sizeof(Vertex) + }; + + var numUniqueVertices = UfbxApi.GenerateIndices([stream], weldedIndices, null, &error); + if (numUniqueVertices == 0 && error.type != ufbx_error_type.UFBX_ERROR_NONE) + { + _taskCompletionSource.SetResult(Result.Failure($"Welding failed: {error.description}")); + return; + } + + MeshOptApi.OptimizeVertexCache((uint*)cachedIndices.GetUnsafePtr(), (uint*)weldedIndices.GetUnsafePtr(), numIndices, numUniqueVertices); + + vertices = new UnsafeList((int)numUniqueVertices, _allocationHandle); + indices = new UnsafeList((int)numIndices, _allocationHandle); + + var finalVertexCount = MeshOptApi.OptimizeVertexFetch(vertices.GetUnsafePtr(), (uint*)cachedIndices.GetUnsafePtr(), numIndices, flatVertices.GetUnsafePtr(), numIndices, (nuint)sizeof(Vertex)); + + vertices.UnsafeSetCount((int)finalVertexCount); + + MemoryUtility.MemCpy(indices.GetUnsafePtr(), cachedIndices.GetUnsafePtr(), numIndices * sizeof(uint)); + indices.UnsafeSetCount((int)numIndices); + + if (_settings.NormalDataSource == VertexDataSource.Computed || (_settings.NormalDataSource == VertexDataSource.ComputedIfMissing && missingNormals)) + { + MeshBuilder.ComputeNormal(vertices, indices); + } + + if (_settings.TangentDataSource == VertexDataSource.Computed || (_settings.TangentDataSource == VertexDataSource.ComputedIfMissing && missingTangents)) + { + MeshBuilder.ComputeTangents(vertices, indices); + } + + _taskCompletionSource.SetResult(Result.Success()); + } +} + +public partial class MeshProcessor +{ + +} diff --git a/src/Runtime/Ghost.Graphics/Utilities/MeshletUtility.cs b/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs similarity index 82% rename from src/Runtime/Ghost.Graphics/Utilities/MeshletUtility.cs rename to src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs index 8cd69ad..12d69a2 100644 --- a/src/Runtime/Ghost.Graphics/Utilities/MeshletUtility.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs @@ -4,13 +4,16 @@ // TODO: This file should be moved to editor project since there is no reason we need to build meshlets and LOD at runtime. using Ghost.Core; +using Ghost.Graphics.Core; +using Ghost.Graphics.RHI; using Ghost.MeshOptimizer; using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.Mathematics; -using System.Diagnostics; +using Misaki.HighPerformance.Mathematics.Geometry; +using System.Runtime.CompilerServices; -namespace Ghost.Graphics.Utilities; +namespace Ghost.Editor.Core.Assets; internal struct Cluster : IDisposable { @@ -164,7 +167,7 @@ public unsafe delegate int ClodOutputDelegate(void* context, ClodGroup group, Re // FIX: UnsafeList and UnsafeArray are not same as std::vector. -public static unsafe class MeshletUtility +public static unsafe partial class MeshProcessor { private static ClodBounds ComputeBounds(ref readonly ClodMesh mesh, UnsafeList indices, float error) { @@ -651,7 +654,7 @@ public static unsafe class MeshletUtility bounds.error = Math.Max(bounds.error * config.simplifyErrorMergePrevious, error) + error * config.simplifyErrorMergeAdditive; var refined = OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback); - + for (var j = 0; j < groups[i].Count; j++) { clusters[groups[i][j]].Dispose(); @@ -691,4 +694,129 @@ public static unsafe class MeshletUtility return finalClusterCount; } + + public static void BuildMeshlets(MeshletMeshData* pMeshletData, ReadOnlyUnsafeCollection vertices, ReadOnlyUnsafeCollection indices) + { + Logger.DebugAssert(pMeshletData->meshletCount > 0, "Mesh must have vertices to build meshlets."); + + var config = new ClodConfig + { + maxVertices = 64, + minTriangles = 32, + maxTriangles = 124, + + partitionSpatial = true, + partitionSize = 16, + + clusterSpatial = false, + clusterSplitFactor = 2.0f, + + optimizeClusters = true, + optimizeClustersLevel = 1, + + simplifyRatio = 0.5f, + simplifyThreshold = 0.85f, + simplifyErrorMergePrevious = 1.0f, + simplifyErrorFactorSloppy = 2.0f, + simplifyPermissive = true, + simplifyFallbackPermissive = false, + simplifyFallbackSloppy = true, + }; + + var clodMesh = new ClodMesh + { + vertexPositions = (float*)Unsafe.AsPointer(in vertices[0].position), + vertexCount = (nuint)vertices.Count, + vertexPositionsStride = (nuint)sizeof(Vertex), + vertexAttributes = (float*)Unsafe.AsPointer(in vertices[0].normal), + vertexAttributesStride = (nuint)sizeof(Vertex), + indices = (uint*)indices.GetUnsafePtr(), + indexCount = (nuint)indices.Count, + attributeProtectMask = 0, // TODO: We need to protect UVs and other vertex attributes to ensure they are not altered during simplification. + }; + + Build(in config, in clodMesh, pMeshletData, MeshletOutputCallback); + + pMeshletData->meshletCount = pMeshletData->meshlets.IsCreated ? pMeshletData->meshlets.Count : 0; + + if (pMeshletData->groups.IsCreated && pMeshletData->groups.Count > 0) + { + var maxLodLevel = 0u; + for (var i = 0; i < pMeshletData->groups.Count; i++) + { + maxLodLevel = Math.Max(maxLodLevel, pMeshletData->groups[i].lodLevel); + } + + pMeshletData->lodLevelCount = (int)maxLodLevel + 1; + } + + pMeshletData->materialSlotCount = 1; + } + + private static int MeshletOutputCallback(void* context, ClodGroup group, ReadOnlyUnsafeCollection clusters) + { + var pMeshletData = (MeshletMeshData*)context; + + // Ensure lists are initialized + if (!pMeshletData->groups.IsCreated) pMeshletData->groups = new UnsafeList(16, AllocationHandle.Persistent); + if (!pMeshletData->meshlets.IsCreated) pMeshletData->meshlets = new UnsafeList(64, AllocationHandle.Persistent); + if (!pMeshletData->meshletVertices.IsCreated) pMeshletData->meshletVertices = new UnsafeList(128, AllocationHandle.Persistent); + if (!pMeshletData->meshletTriangles.IsCreated) pMeshletData->meshletTriangles = new UnsafeList(128, AllocationHandle.Persistent); + + var meshletGroup = new MeshletGroup + { + boundingSphere = new SphereBounds(group.simplified.center, group.simplified.radius), + boundingBox = new AABB(group.simplified.center - group.simplified.radius, group.simplified.center + group.simplified.radius), + parentError = group.simplified.error, + meshletStartIndex = (uint)pMeshletData->meshlets.Count, + meshletCount = (uint)clusters.Count, + lodLevel = (uint)group.depth + }; + pMeshletData->groups.Add(meshletGroup); + + for (var i = 0; i < clusters.Count; i++) + { + var cluster = clusters[i]; + + var meshlet = new Meshlet + { + boundingSphere = new SphereBounds(cluster.bounds.center, cluster.bounds.radius), + parentBoundingSphere = new SphereBounds(group.simplified.center, group.simplified.radius), + boundingBox = new AABB(cluster.bounds.center - cluster.bounds.radius, cluster.bounds.center + cluster.bounds.radius), + vertexCount = (byte)cluster.vertexCount, + triangleCount = (byte)(cluster.localIndexCount / 3), + vertexOffset = (uint)pMeshletData->meshletVertices.Count, + triangleOffset = (uint)pMeshletData->meshletTriangles.Count, + groupIndex = (uint)pMeshletData->groups.Count - 1, + clusterError = cluster.bounds.error, + parentError = group.simplified.error, + localMaterialIndex = 0, // TODO: support multiple materials + lodLevel = (byte)group.depth, + }; + pMeshletData->meshlets.Add(meshlet); + + // Add unique vertices + for (nuint j = 0; j < cluster.vertexCount; j++) + { + pMeshletData->meshletVertices.Add(cluster.uniqueVertices[j]); + } + // Add local triangles (packed into uints) + var triangleCount = cluster.localIndexCount / 3; + for (nuint j = 0; j < triangleCount; j++) + { + uint i0 = cluster.localIndices[j * 3 + 0]; + uint i1 = cluster.localIndices[j * 3 + 1]; + uint i2 = cluster.localIndices[j * 3 + 2]; + var packedTriangle = i0 | (i1 << 8) | (i2 << 16); + pMeshletData->meshletTriangles.Add(packedTriangle); + } + } + + return 0; + } + + public static void BuildClusterLodHierarchy() + { + // TODO: Implement a function that builds a cluster LOD hierarchy for a mesh, which can be used for efficient rendering of large meshes with varying levels of detail. + } } diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/ShaderAssetHandler.cs b/src/Editor/Ghost.Editor.Core/Assets/ShaderAssetHandler.cs similarity index 75% rename from src/Editor/Ghost.Editor.Core/AssetHandler/ShaderAssetHandler.cs rename to src/Editor/Ghost.Editor.Core/Assets/ShaderAssetHandler.cs index f7a95a7..8766c7b 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/ShaderAssetHandler.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/ShaderAssetHandler.cs @@ -4,7 +4,7 @@ using Ghost.DSL.ShaderCompiler; using Ghost.Engine; using System.Runtime.InteropServices; -namespace Ghost.Editor.Core.AssetHandler; +namespace Ghost.Editor.Core.Assets; [Guid(GUID)] public sealed partial class GraphicsShaderAsset : IAsset @@ -16,7 +16,7 @@ public sealed partial class GraphicsShaderAsset : IAsset get; } - public IAssetSettings Settings + public IAssetSettings? Settings { get; } @@ -49,7 +49,7 @@ public sealed partial class ComputeShaderAsset : IAsset get; } - public IAssetSettings Settings + public IAssetSettings? Settings { get; } @@ -84,13 +84,11 @@ internal class GraphicsShaderAssetHandler : IPackableAssetHandler return null; } - public async ValueTask> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) + public async ValueTask> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default) { try { - using var reader = new StreamReader(assetStream); - var shaderCode = await reader.ReadToEndAsync(token); - var result = DSLShaderCompiler.CompileGraphicsShader(shaderCode); + var result = DSLShaderCompiler.CompileGraphicsShader(assetPath); if (result.IsFailure) { return Result.Failure(result.Message); @@ -104,12 +102,12 @@ internal class GraphicsShaderAssetHandler : IPackableAssetHandler } } - public ValueTask SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default) + public ValueTask SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default) { return new ValueTask(Result.Failure("Saving shader assets is not supported yet as it's read-only. Please edit the shader source file directly if you need to modify it.")); } - public ValueTask PackAsync(FileStream assetStream, Stream targetStream, CancellationToken token = default) + public ValueTask PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default) { throw new NotImplementedException(); } @@ -126,13 +124,11 @@ internal class ComputeShaderAssetHandler : IPackableAssetHandler return null; } - public async ValueTask> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) + public async ValueTask> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default) { try { - using var reader = new StreamReader(assetStream); - var shaderCode = await reader.ReadToEndAsync(token); - var result = DSLShaderCompiler.CompileComputeShader(shaderCode); + var result = DSLShaderCompiler.CompileComputeShaderCode(assetPath); if (result.IsFailure) { return Result.Failure(result.Message); @@ -146,12 +142,12 @@ internal class ComputeShaderAssetHandler : IPackableAssetHandler } } - public ValueTask SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default) + public ValueTask SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default) { return new ValueTask(Result.Failure("Saving shader assets is not supported yet as it's read-only. Please edit the shader source file directly if you need to modify it.")); } - public ValueTask PackAsync(FileStream assetStream, Stream targetStream, CancellationToken token = default) + public ValueTask PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default) { throw new NotImplementedException(); } diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAssetHandler.cs b/src/Editor/Ghost.Editor.Core/Assets/TextureAssetHandler.cs similarity index 83% rename from src/Editor/Ghost.Editor.Core/AssetHandler/TextureAssetHandler.cs rename to src/Editor/Ghost.Editor.Core/Assets/TextureAssetHandler.cs index e2e8e52..c484072 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAssetHandler.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/TextureAssetHandler.cs @@ -6,9 +6,8 @@ using Misaki.HighPerformance.LowLevel; using System.IO.MemoryMappedFiles; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using TerraFX.Interop.Windows; -namespace Ghost.Editor.Core.AssetHandler; +namespace Ghost.Editor.Core.Assets; public enum TextureType : uint { @@ -86,7 +85,7 @@ public unsafe class TextureAsset : IAsset _textureData = data; _width = header.width; _height = header.height; - _depth = header.depth; + _depth = header.bpc; _dimension = header.dimension; _colorComponents = header.colorComponents; } @@ -160,11 +159,6 @@ public class TextureAssetSettings : IAssetSettings get; set; } = 0; // 0 means generate full mipmap levels. - public bool GammaCorrection - { - get; set; - } = true; - public bool PremultiplyAlpha { get; set; @@ -264,6 +258,7 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand public int width; public int height; public int depth; + public int bitsPerChannel; public int colorComponents; public bool isHDR; } @@ -305,33 +300,27 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand return TextureDimension.Texture2D; } - private static unsafe Result GetImageInfo(FileStream sourceStream) + private static unsafe Result GetImageInfo(string sourcePath, TextureAssetSettings settings) { - using var mmf = MemoryMappedFile.CreateFromFile(sourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true); + using var mmf = MemoryMappedFile.CreateFromFile(sourcePath, FileMode.Open); using var accessor = mmf.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); byte* ptr = null; try { - var ext = Path.GetExtension(sourceStream.Name); - var isHDR = ext.Equals(".hdr", StringComparison.OrdinalIgnoreCase); + var ext = Path.GetExtension(sourcePath); + var isHDR = ext.Equals(".hdr", StringComparison.OrdinalIgnoreCase) || settings.Basic.TextureShape == TextureShape.TextureCube; accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); int imageWidth, imageHeight, bitsPerChannel, colorComponents; - var bufferSpan = new ReadOnlySpan(ptr, (int)sourceStream.Length); - var code = StbIApi.InfoFromMemory(bufferSpan, &imageWidth, &imageHeight, &colorComponents); - if (code == 0) - { - return Result.Failure("Failed to read image info from memory."); - } - + var bufferSpan = new ReadOnlySpan(ptr, (int)accessor.Capacity); bitsPerChannel = StbIApi.Is16BitFromMemory(bufferSpan) > 0 ? 16 : 8; void* pPixels; - if (bitsPerChannel > 8) + if (isHDR || bitsPerChannel > 8) { pPixels = StbIApi.LoadfFromMemory(bufferSpan, &imageWidth, &imageHeight, &colorComponents, 4); } @@ -345,8 +334,9 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand pixelData = (IntPtr)pPixels, width = imageWidth, height = imageHeight, - depth = bitsPerChannel, - colorComponents = colorComponents, + depth = 1, + bitsPerChannel = bitsPerChannel, + colorComponents = 4, // We forced req_comp to 4 isHDR = isHDR, }; } @@ -363,25 +353,25 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand } } - public ValueTask> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) + public ValueTask> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default) { try { - var infoResult = GetImageInfo(assetStream); + var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings(); + var infoResult = GetImageInfo(assetPath, textureSettings); if (infoResult.IsFailure) { return ValueTask.FromResult(Result.Failure(infoResult.Message)); } var info = infoResult.Value; - var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings(); var contentHeader = new TextureContentHeader { width = (uint)info.width, height = (uint)info.height, - depth = (uint)info.depth, + bpc = (uint)info.bitsPerChannel, colorComponents = (uint)info.colorComponents, - dimension = (uint)GetTextureDimension(textureSettings) + dimension = (uint)GetTextureDimension(textureSettings), }; return ValueTask.FromResult(Result.Success(new TextureAsset(info.pixelData, contentHeader, id, textureSettings))); @@ -393,22 +383,24 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand } [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private unsafe static void WriteCallback(void* context, void* data, int size) + private static unsafe void WriteCallback(void* context, void* data, int size) { var stream = (Stream)GCHandle.FromIntPtr((IntPtr)context).Target!; var buffer = new ReadOnlySpan(data, size); stream.Write(buffer); } - public async ValueTask SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default) + public async ValueTask SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default) { if (asset is not TextureAsset textureAsset) { return Result.Failure("Asset type is not TextureAsset"); } + await using var targetStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None); return await Task.Run(() => { + // It will be safe here to pass the gc handle to c because c will not use it, c will only pass it back to c# in the callback, and we will free the handle after the write operation is done. var gcHandle = GCHandle.Alloc(targetStream, GCHandleType.Normal); try @@ -447,28 +439,24 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand }, token).ConfigureAwait(false); } - public async ValueTask ImportAsync(FileStream sourceStream, FileStream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) + public async ValueTask ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default) { - if (sourceStream.Length > int.MaxValue) + if (!File.Exists(sourcePath)) { - return Result.Failure("Source stream is too large."); + return Result.Failure("Source file does not exist."); } try { var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings(); - - using var mmf = MemoryMappedFile.CreateFromFile(sourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true); - using var accessor = mmf.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); - - var infoResult = GetImageInfo(sourceStream); + var infoResult = GetImageInfo(sourcePath, textureSettings); if (!infoResult.IsSuccess) { return Result.Failure(infoResult.Message); } var info = infoResult.Value; - var result = await TextureProcessor.CompressToCacheAsync(EditorApplication.CacheFolderPath, id, + var result = await TextureProcessor.GenerateMipAndCompressAsync(EditorApplication.CacheFolderPath, id, info, textureSettings, token) .ConfigureAwait(false); @@ -480,13 +468,12 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand var (cachePath, mip) = result.Value; - targetStream.Seek(0, SeekOrigin.Begin); - + await using var targetStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None); var header = new TextureContentHeader { width = (uint)info.width, height = (uint)info.height, - depth = (uint)info.depth, + bpc = (uint)info.bitsPerChannel, colorComponents = (uint)info.colorComponents, mipLevels = (uint)mip, dimension = (uint)GetTextureDimension(textureSettings) @@ -506,12 +493,12 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand } } - public ValueTask ExportAsync(FileStream assetStream, FileStream targetStream, IAssetExportOptions? options, CancellationToken token = default) + public ValueTask ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default) { return ValueTask.FromResult(Result.Failure("Exporting texture assets is not supported yet.")); } - public ValueTask PackAsync(FileStream assetStream, Stream targetStream, CancellationToken token = default) + public ValueTask PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default) { throw new NotImplementedException(); } diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs b/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.Compress.cs similarity index 50% rename from src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs rename to src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.Compress.cs index 97f4700..e6f4635 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.Compress.cs @@ -1,46 +1,42 @@ using Ghost.Core; +using Ghost.Engine; using Ghost.Nvtt; +using Misaki.HighPerformance.Jobs; using Misaki.HighPerformance.LowLevel; +using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Collections; +using Misaki.HighPerformance.Mathematics.SPMD; using System.IO.Hashing; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; -namespace Ghost.Editor.Core.AssetHandler; +namespace Ghost.Editor.Core.Assets; -/// -/// Drives the NVTT compression + mipmap pipeline for a single texture asset. -/// -/// Responsibilities: -/// 1. Accept raw decoded pixel bytes + settings. -/// 2. Determine the cache file path (CachesFolderPath/TextureCache/<guid>_<hash>.dds). -/// 3. If the cache is already valid (hash matches), skip compression. -/// 4. Otherwise run the full NVTT pipeline and write the DDS to the cache file. -/// -/// The caller owns opening/closing all streams; this class only takes spans and paths. -/// -internal static class TextureProcessor +internal static partial class TextureProcessor { private class NvttPipelineTask : IThreadPoolWorkItem { private readonly string _outputPath; private readonly TextureAssetHandler.TextureInfo _textureInfo; - private readonly TextureAssetSettings _settings; + private UnsafeArray _mipLevels; + private readonly TaskCompletionSource> _completionSource; public Task> Task => _completionSource.Task; - public NvttPipelineTask(string outputPath, TextureAssetHandler.TextureInfo textureInfo, TextureAssetSettings settings) + public NvttPipelineTask(string outputPath, TextureAssetHandler.TextureInfo textureInfo, TextureAssetSettings settings, UnsafeArray mipLevels) { _outputPath = outputPath; _textureInfo = textureInfo; _settings = settings; + _mipLevels = mipLevels; _completionSource = new TaskCompletionSource>(); } - public unsafe void Execute() + private unsafe Result RunMipGenCompressionPipeline() { using var pSurface = new DisposablePtr(NvttSurface.Create()); using var pCompOpts = new DisposablePtr(NvttCompressionOptions.Create()); @@ -49,20 +45,24 @@ internal static class TextureProcessor var inputFormat = _textureInfo.colorComponents == 1 ? NvttInputFormat.NVTT_InputFormat_R_32F - : _textureInfo.depth > 8 + : _textureInfo.bitsPerChannel > 8 ? NvttInputFormat.NVTT_InputFormat_RGBA_32F : NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below - var needUnsigned = _settings.Basic.TextureType == TextureType.Normal ? NvttBoolean.NVTT_True : NvttBoolean.NVTT_False; - if (pSurface.Get()->SetImageData(inputFormat, _textureInfo.width, _textureInfo.height, _textureInfo.depth, (void*)_textureInfo.pixelData, needUnsigned, null)) + var isNormal = _settings.Basic.TextureType == TextureType.Normal; + if (!pSurface.Get()->SetImageData(inputFormat, _textureInfo.width, _textureInfo.height, _textureInfo.depth, (void*)_textureInfo.pixelData, isNormal, null)) { - _completionSource.SetResult(Result.Failure("Failed to set image data for NVTT compression.")); - return; + return Result.Failure("Failed to set image data for NVTT compression."); + } + + if (isNormal) + { + pSurface.Get()->SetNormalMap(true); } // stb gives us RGBA byte order; NVTT BGRA_8UB reads it as BGRA, // so channels R and B are swapped — fix with swizzle(2,1,0,3). - if (_textureInfo.colorComponents > 1 && _textureInfo.depth <= 8) + if (_textureInfo.colorComponents > 1 && _textureInfo.bitsPerChannel <= 8) { pSurface.Get()->Swizzle(2, 1, 0, 3, null); } @@ -91,7 +91,7 @@ internal static class TextureProcessor pSurface.Get()->SetBorder(0f, 0f, 0f, 0f, null); } - if (_settings.Basic.IsSRGB && _settings.Advanced.GammaCorrection) + if (_settings.Basic.IsSRGB) { pSurface.Get()->ToLinearFromSrgb(null); } @@ -136,6 +136,10 @@ internal static class TextureProcessor pCtx.Get()->OutputHeader(pSurface.Get(), mipmapCount, pCompOpts.Get(), pOutOpts.Get()); using var pMip = new DisposablePtr(pSurface.Get()->Clone()); + if (pMip.Get() == null) + { + return Result.Failure("Failed to clone surface for mipmap generation."); + } for (var level = 0; level < mipmapCount; level++) { @@ -148,19 +152,110 @@ internal static class TextureProcessor _settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3, null); } - pCtx.Get()->Compress(pMip.Get(), 0, level, pCompOpts.Get(), pOutOpts.Get()); + using var compressMip = new DisposablePtr(pMip.Get()->Clone()); + if (_settings.Basic.IsSRGB) + { + compressMip.Get()->ToSrgb(null); + } + + if (!pCtx.Get()->Compress(compressMip.Get(), 0, level, pCompOpts.Get(), pOutOpts.Get())) + { + return Result.Failure("Failed to compress mipmap."); + } if (level + 1 < mipmapCount) { - pMip.Get()->BuildNextMipmapDefaults(nvttFilter, 1, null); + if (!pMip.Get()->BuildNextMipmapDefaults(nvttFilter, 1, null)) + { + return Result.Failure("Failed to build next mipmap."); + } } } - _completionSource.SetResult(Result.Success(mipmapCount)); + return Result.Success(mipmapCount); + } + + private unsafe Result RunCubeMapCompressionPipeline() + { + using var pCompOpts = new DisposablePtr(NvttCompressionOptions.Create()); + using var pOutOpts = new DisposablePtr(NvttOutputOptions.Create()); + using var pCtx = new DisposablePtr(NvttContext.Create()); + + pCompOpts.Get()->SetFormat(SelectFormat(_settings, _textureInfo.isHDR)); + pCompOpts.Get()->SetQuality(SelectQuality(_settings.Advanced.CompressionLevel)); + + pOutOpts.Get()->SetOutputHeader(true); + pOutOpts.Get()->SetSrgbFlag(_settings.Basic.IsSRGB); + pOutOpts.Get()->SetContainer(NvttContainer.NVTT_Container_DDS10); + pOutOpts.Get()->SetFileName(Encoding.UTF8.GetBytes(_outputPath)); + + pCtx.Get()->SetCudaAcceleration(NvttApi.IsCudaSupported()); + + int edgeLength; + using (var cubeSurface0 = new DisposablePtr(NvttCubeSurface.Create())) + using (var mip0Surf = new DisposablePtr(NvttSurface.Create())) + { + if (!mip0Surf.Get()->SetImageData(NvttInputFormat.NVTT_InputFormat_RGBA_32F, _mipLevels[0].width, _mipLevels[0].height, 1, _mipLevels[0].data.GetUnsafePtr(), false, null)) + { + return Result.Failure("Failed to set image data for NVTT compression."); + } + + cubeSurface0.Get()->Fold(mip0Surf.Get(), NvttCubeLayout.NVTT_CubeLayout_LatitudeLongitude); + edgeLength = cubeSurface0.Get()->EdgeLength(); + } + + pCtx.Get()->OutputHeaderData(NvttTextureType.NVTT_TextureType_Cube, edgeLength, edgeLength, 1, _mipLevels.Length, false, pCompOpts.Get(), pOutOpts.Get()); + + for (var level = 0; level < _mipLevels.Length; level++) + { + using var cubeSurface = new DisposablePtr(NvttCubeSurface.Create()); + using var mipSurf = new DisposablePtr(NvttSurface.Create()); + + mipSurf.Get()->SetImageData(NvttInputFormat.NVTT_InputFormat_RGBA_32F, _mipLevels[level].width, _mipLevels[level].height, 1, _mipLevels[level].data.GetUnsafePtr(), false, null); + cubeSurface.Get()->Fold(mipSurf.Get(), NvttCubeLayout.NVTT_CubeLayout_LatitudeLongitude); + + for (var face = 0; face < 6; face++) + { + var faceSurf = cubeSurface.Get()->Face(face); + if (_settings.Basic.IsSRGB) + { + faceSurf->ToSrgb(null); + } + + if (!pCtx.Get()->Compress(faceSurf, face, level, pCompOpts.Get(), pOutOpts.Get())) + { + return Result.Failure("Failed to compress mipmap."); + } + } + } + + return Result.Success(_mipLevels.Length); + } + + public void Execute() + { + Result finalResult; + try + { + if (_settings.Basic.TextureShape == TextureShape.TextureCube) + { + finalResult = RunCubeMapCompressionPipeline(); + } + else + { + finalResult = RunMipGenCompressionPipeline(); + } + } + catch (Exception ex) + { + finalResult = Result.Failure($"Compression threw an exception: {ex.Message}"); + } + + _completionSource.SetResult(finalResult); } } - public static async ValueTask> CompressToCacheAsync(string cachesFolderPath, Guid assetId, + public static async ValueTask> GenerateMipAndCompressAsync(string cachesFolderPath, Guid assetId, TextureAssetHandler.TextureInfo textureInfo, TextureAssetSettings settings, CancellationToken cancellationToken) { @@ -174,46 +269,79 @@ internal static class TextureProcessor if (File.Exists(cachePath)) { - using var fs = new FileStream(cachePath, FileMode.Open, FileAccess.Read); - using var reader = new BinaryReader(fs); - if (reader.ReadUInt32() != 0x20534444) + var isValid = false; + var mipMapCount = 1u; + var hasMipMapFlag = false; + + try + { + using var fs = new FileStream(cachePath, FileMode.Open, FileAccess.Read); + using var reader = new BinaryReader(fs); + if (reader.ReadUInt32() == 0x20534444) + { + reader.BaseStream.Seek(4, SeekOrigin.Current); + var flags = reader.ReadUInt32(); + hasMipMapFlag = (flags & 0x00020000) != 0; + + reader.BaseStream.Seek(28, SeekOrigin.Begin); + mipMapCount = reader.ReadUInt32(); + isValid = true; + } + } + catch + { + // Ignore read errors and regenerate + } + + if (isValid) + { + return (cachePath, (!hasMipMapFlag || mipMapCount == 0) ? 1 : (int)mipMapCount); + } + + try { File.Delete(cachePath); - goto ScheduleWork; } - - // Read dwFlags (Offset 8) - // Skip dwSize (4 bytes), then read dwFlags (4 bytes) - reader.BaseStream.Seek(4, SeekOrigin.Current); - var flags = reader.ReadUInt32(); - - // The DDSD_MIPMAPCOUNT flag is 0x00020000 - var hasMipMapFlag = (flags & 0x00020000) != 0; - - // Read dwMipMapCount (Offset 28) - reader.BaseStream.Seek(28, SeekOrigin.Begin); - var mipMapCount = reader.ReadUInt32(); - - // Return the correct count - // If the flag is missing, or the count says 0, there is still 1 main image. - if (!hasMipMapFlag || mipMapCount == 0) + catch { - return (cachePath, 1); + // Ignore deletion errors, maybe file is still locked or we have no permission. + // The pipeline will overwrite it. + } + } + + UnsafeArray mipLevels = default; + var scheduler = EditorApplication.GetService().JobScheduler; + + try + { + if (settings.Basic.TextureShape == TextureShape.TextureCube) + { + var handle = GenerateMipHDRI(scheduler, textureInfo, out mipLevels); + await scheduler.WaitAsync(handle, cancellationToken); } - return (cachePath, (int)mipMapCount); - } + var workItem = new NvttPipelineTask(cachePath, textureInfo, settings, mipLevels); + ThreadPool.UnsafeQueueUserWorkItem(workItem, true); + var result = await workItem.Task.WaitAsync(cancellationToken).ConfigureAwait(false); + if (result.IsFailure) + { + return Result.Failure(result.Message); + } - ScheduleWork: - var workItem = new NvttPipelineTask(cachePath, textureInfo, settings); - ThreadPool.UnsafeQueueUserWorkItem(workItem, true); - var result = await workItem.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - if (result.IsFailure) + return (cachePath, result.Value); + } + finally { - return Result.Failure(result.Message); - } + if (mipLevels.IsCreated) + { + var mipDisposeJob = new MipLevelDisposeJob + { + mipLevels = mipLevels, + }; - return (cachePath, result.Value); + scheduler.Schedule(in mipDisposeJob); + } + } } private static NvttFormat SelectFormat(TextureAssetSettings settings, bool isHDR) diff --git a/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.GGXMip.cs b/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.GGXMip.cs new file mode 100644 index 0000000..8052671 --- /dev/null +++ b/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.GGXMip.cs @@ -0,0 +1,338 @@ +using Ghost.Core; +using Misaki.HighPerformance.Jobs; +using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Collections; +using Misaki.HighPerformance.Mathematics.SPMD; +using System.Runtime.CompilerServices; +using static Misaki.HighPerformance.Mathematics.math; + +namespace Ghost.Editor.Core.Assets; + +internal static partial class TextureProcessor +{ + private const int _SAMPLE_COUNT = 1024; + + private struct MipLevel + { + public UnsafeArray data; + public int width; + public int height; + public int offset; + public float roughness; + } + + private unsafe struct GGXMipGenerationJobSPMD : IJobParallelFor + where TFloat : unmanaged, ISPMDLane + where TInt : unmanaged, ISPMDLane + { + public float* pImage; + public MipLevel* pMipLevels; + public float* pRadicalInverse_VdCLut; + public int imageWidth; + public int imageHeight; + public int numMipLevels; + public int channelCount; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector2 Hammersley(TFloat i, int N, float* lut) + { + var x = i / N; + var y = TFloat.Load(lut + (int)i[0]); + return MathV.Create(x, y); + } + + // GGX Importance Sampling + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector3 ImportanceSampleGGX(Vector2 Xi, Vector3 N, float roughness) + { + var a = roughness * roughness; // Disney remap roughness for better visual linearity + + var phi = 2.0f * PI * Xi.x; + + // Clamp the inside of the cosTheta Sqrt to prevent NaN on division precision edges + var cosThetaInner = TFloat.Max((1.0f - Xi.y) / (1.0f + (a * a - 1.0f) * Xi.y), TFloat.Zero); + var cosTheta = TFloat.Sqrt(cosThetaInner); + + // Clamp the inside of sinTheta to prevent sqrt of negative floating-point errors + var sinThetaInner = TFloat.Max(1.0f - cosTheta * cosTheta, TFloat.Zero); + var sinTheta = TFloat.Sqrt(sinThetaInner); + + // Spherical to Cartesian coordinates (Halfway vector) + var (sinPhi, cosPhi) = TFloat.SinCos(phi); + var H = MathV.Create(cosPhi * sinTheta, sinPhi * sinTheta, cosTheta); + + // Tangent space to World space + var mask = TFloat.Abs(N.z) < 0.999f; + var up = MathV.Select(mask, MathV.Create(0.0f, 0.0f, 1.0f), MathV.Create(1.0f, 0.0f, 0.0f)); + + var tangent = MathV.Normalize(MathV.Cross(up, N)); + var bitangent = MathV.Cross(N, tangent); + + var sampleVec = (tangent * H.x) + (bitangent * H.y) + (N * H.z); + return MathV.Normalize(sampleVec); + } + + // Maps a 3D direction vector to 2D equirectangular UVs + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector2 DirToEquirectangularUV(Vector3 dir) + { + var u = TFloat.Atan2(dir.z, dir.x); + var v = TFloat.Asin(dir.y); + + u = u / (2.0f * PI) + 0.5f; + v = v / PI + 0.5f; + return MathV.Create(u, v); + } + + // Samples the source HDR image using bilinear interpolation (simplified to nearest neighbor for brevity here) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector3 SampleEquirectangularMap(float* img, int w, int h, int c, Vector3 dir) + { + var uv = DirToEquirectangularUV(dir); + + // Nearest neighbor pixel coordinates + var px = (uv.x * (w - 1.0f)).Cast(); + var py = (uv.y * (h - 1.0f)).Cast(); + + // Clamp + px = TInt.Clamp(px, TInt.Zero, w - 1); + py = TInt.Clamp(py, TInt.Zero, h - 1); + + // Assuming float RGB array format + var idx = (py * w + px) * c; + return MathV.GatherVector3(img, idx.GetUnsafePtr(), 1); + } + + public void Execute(int loopIndex, ref readonly JobExecutionContext ctx) + { + var m = 0; + while (m < numMipLevels - 1 && loopIndex >= pMipLevels[m + 1].offset) + { + m++; + } + + var span = new ReadOnlySpan(pMipLevels, numMipLevels); + var pLevel = &pMipLevels[m]; + + var w = pLevel->width; + var h = pLevel->height; + var pData = pLevel->data; + + var local_i = loopIndex - pLevel->offset; + var x = local_i % w; + var y = local_i / w; + var u = (float)x / (w - 1); + var v = (float)y / (h - 1); + + var phi = (u - 0.5f) * 2.0f * PI; + var theta = (v - 0.5f) * PI; + + sincos(theta, out var sinTheta, out var cosTheta); + sincos(phi, out var sinPhi, out var cosPhi); + var N = float3(cosTheta * cosPhi, sinTheta, cosTheta * sinPhi); + N = normalize(N); + + // For split-sum, we assume View and Reflection directions equal the Normal + var V = N; + var R = N; + + var vN = MathV.Create( + TFloat.Create(N.x), + TFloat.Create(N.y), + TFloat.Create(N.z) + ); + + var vV = MathV.Create( + TFloat.Create(V.x), + TFloat.Create(V.y), + TFloat.Create(V.z) + ); + + var vPrefilteredColor = Vector3.Zero; + var vTotalWeight = TFloat.Zero; + + // Monte Carlo Integration Loop + + var vLuma = MathV.Create(0.2126f, 0.7152f, 0.0722f); + var dynamicSampleCount = (int)max(1.0f, _SAMPLE_COUNT * pLevel->roughness); + var dsc = TFloat.Create(dynamicSampleCount); + + for (var i = 0; i < dynamicSampleCount; i += TFloat.LaneWidth) + { + var laneIndices = TFloat.Sequence(i, 1.0f); + var validLaneMask = laneIndices < dsc; + + // Generate a Hammersley random sequence point + var Xi = Hammersley(laneIndices, dynamicSampleCount, pRadicalInverse_VdCLut); + + // Get the halfway vector based on GGX NDF + var H = ImportanceSampleGGX(Xi, vN, pLevel->roughness); + + // Calculate Light direction + var L = MathV.Reflect(-vV, H); + L = MathV.Normalize(L); + + var NdotL = TFloat.Max(MathV.Dot(vN, L), TFloat.Zero); + var sampleColor = SampleEquirectangularMap(pImage, imageWidth, imageHeight, channelCount, L); + + NdotL &= validLaneMask; + + // The Karis Average Weight: 1 / (1 + luma) + // A normal sky pixel (luma 1.0) gets a weight of 0.5. + // A sun pixel (luma 1000.0) gets a tiny weight of ~0.001, naturally suppressing it. + // This introduce bias, but significantly reduces fireflies without needing solid angle sampling or cdf inversion. + // And since this is a mip generation step, a little bias is acceptable for much better performance and stability. + var luma = MathV.Dot(sampleColor, vLuma); + var fireflyWeight = TFloat.One / (TFloat.One + luma); + var finalWeight = NdotL * fireflyWeight; + + vPrefilteredColor += sampleColor * finalWeight; + vTotalWeight += finalWeight; + } + + var totalWeight = 0.0f; + var prefilteredColor = float3(0, 0, 0); + + for (var i = 0; i < TFloat.LaneWidth; i++) + { + prefilteredColor.x += vPrefilteredColor.x[i]; + prefilteredColor.y += vPrefilteredColor.y[i]; + prefilteredColor.z += vPrefilteredColor.z[i]; + totalWeight += vTotalWeight[i]; + } + + // Average the result + if (totalWeight > 0.0f) + { + prefilteredColor *= 1.0f / totalWeight; + } + + // Write to output mip array + var out_idx = (y * w + x) * channelCount; + pData[out_idx] = prefilteredColor.x; + pData[out_idx + 1] = prefilteredColor.y; + pData[out_idx + 2] = prefilteredColor.z; + } + } + + private struct VdCLutDisposeJob : IJob + { + public UnsafeArray radicalInverse_VdCLut; + + public void Execute(ref readonly JobExecutionContext ctx) + { + radicalInverse_VdCLut.Dispose(); + } + } + + private struct MipLevelDisposeJob : IJob + { + public UnsafeArray mipLevels; + + public void Execute(ref readonly JobExecutionContext ctx) + { + for (var i = 0; i < mipLevels.Length; i++) + { + mipLevels[i].data.Dispose(); + } + + mipLevels.Dispose(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float RadicalInverse_VdC(uint bits) + { + bits = (bits << 16) | (bits >> 16); + bits = ((bits & 0x55555555u) << 1) | ((bits & 0xAAAAAAAAu) >> 1); + bits = ((bits & 0x33333333u) << 2) | ((bits & 0xCCCCCCCCu) >> 2); + bits = ((bits & 0x0F0F0F0Fu) << 4) | ((bits & 0xF0F0F0F0u) >> 4); + bits = ((bits & 0x00FF00FFu) << 8) | ((bits & 0xFF00FF00u) >> 8); + return bits * 2.3283064365386963e-10f; // bits / 0x100000000 + } + + private static JobHandle GenerateMipHDRI(JobScheduler scheduler, TextureAssetHandler.TextureInfo textureInfo, out UnsafeArray mipLevels) + { + Logger.DebugAssert(textureInfo.isHDR, "GenerateMipHDRI should only be called for HDR textures."); + + var totalMipLevels = (int)Math.Floor(Math.Log2(Math.Max(textureInfo.width, textureInfo.height))) + 1; + + mipLevels = new UnsafeArray(totalMipLevels, AllocationHandle.FreeList); + var radicalInverse_VdCLut = new UnsafeArray(_SAMPLE_COUNT, AllocationHandle.FreeList); + + for (var i = 0u; i < _SAMPLE_COUNT; i++) + { + radicalInverse_VdCLut[i] = RadicalInverse_VdC(i); + } + + int w, h; + var totalPixel = 0; + + for (var i = 0; i < totalMipLevels; i++) + { + w = Math.Max(1, textureInfo.width >> i); + h = Math.Max(1, textureInfo.height >> i); + + mipLevels[i] = new MipLevel + { + data = new UnsafeArray(w * h * textureInfo.colorComponents, AllocationHandle.FreeList), + width = w, + height = h, + offset = totalPixel, + roughness = (float)i / (totalMipLevels - 1) // Linear roughness from 0 to 1 across mip levels + }; + + totalPixel += w * h; + } + + JobHandle handle; + unsafe + { + if (WideLane.IsSupported) + { + var job = new GGXMipGenerationJobSPMD, WideLane> + { + pImage = (float*)textureInfo.pixelData, + pMipLevels = (MipLevel*)mipLevels.GetUnsafePtr(), + pRadicalInverse_VdCLut = (float*)radicalInverse_VdCLut.GetUnsafePtr(), + imageWidth = textureInfo.width, + imageHeight = textureInfo.height, + numMipLevels = totalMipLevels, + channelCount = textureInfo.colorComponents, + }; + + handle = scheduler.ScheduleParallelFor(in job, totalPixel, 64); + } + else + { + var job = new GGXMipGenerationJobSPMD, ScalarLane> + { + pImage = (float*)textureInfo.pixelData, + pMipLevels = (MipLevel*)mipLevels.GetUnsafePtr(), + pRadicalInverse_VdCLut = (float*)radicalInverse_VdCLut.GetUnsafePtr(), + imageWidth = textureInfo.width, + imageHeight = textureInfo.height, + numMipLevels = totalMipLevels, + channelCount = textureInfo.colorComponents, + }; + + handle = scheduler.ScheduleParallelFor(in job, totalPixel, 64); + } + } + + if (!handle.IsValid) + { + return JobHandle.Invalid; + } + + var disposeJob = new VdCLutDisposeJob + { + radicalInverse_VdCLut = radicalInverse_VdCLut + }; + + var disposeHandle = scheduler.Schedule(in disposeJob, handle); + Logger.DebugAssert(disposeHandle.IsValid, "Dispose job handle is invalid."); + + return disposeHandle; + } +} diff --git a/src/Editor/Ghost.Editor.Core/Contracts/IAssetRegistry.cs b/src/Editor/Ghost.Editor.Core/Contracts/IAssetRegistry.cs index a36af84..4358ab2 100644 --- a/src/Editor/Ghost.Editor.Core/Contracts/IAssetRegistry.cs +++ b/src/Editor/Ghost.Editor.Core/Contracts/IAssetRegistry.cs @@ -1,5 +1,5 @@ using Ghost.Core; -using Ghost.Editor.Core.AssetHandler; +using Ghost.Editor.Core.Assets; using Ghost.Editor.Core.Services; using Ghost.Engine.AssetLoader; diff --git a/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs b/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs index b67cc76..252429e 100644 --- a/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs +++ b/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs @@ -1,4 +1,4 @@ -using Ghost.Editor.Core.AssetHandler; +using Ghost.Editor.Core.Assets; using Microsoft.Data.Sqlite; namespace Ghost.Editor.Core.Services; @@ -17,9 +17,12 @@ public sealed partial class AssetCatalog : IDisposable private readonly SqliteCommand _cmdGetPath; private readonly SqliteCommand _cmdUpsert; private readonly SqliteCommand _cmdDelete; + private readonly SqliteCommand _cmdGetHandlerTypeId; private readonly SqliteCommand _cmdGetReferencers; private readonly SqliteCommand _cmdGetDependencies; + private readonly SqliteCommand _cmdGetImportedAt; + private readonly SqliteCommand _cmdInsertDep; private readonly SqliteCommand _cmdClearDeps; private readonly SqliteCommand _cmdEnumerate; @@ -48,16 +51,22 @@ public sealed partial class AssetCatalog : IDisposable _cmdGetGuid = CreateCommand("SELECT guid FROM assets WHERE source_path = @path"); _cmdGetPath = CreateCommand("SELECT source_path FROM assets WHERE guid = @guid"); _cmdGetHandlerTypeId = CreateCommand("SELECT handler_type_id FROM assets WHERE guid = @guid"); + _cmdGetImportedAt = CreateCommand("SELECT imported_at_ms FROM assets WHERE guid = @guid"); + _cmdUpsert = CreateCommand(@" - INSERT INTO assets (guid, source_path, handler_type_id, handler_version) - VALUES (@guid, @path, @handler_id, @version) + INSERT INTO assets (guid, source_path, handler_type_id, handler_version, content_hash, settings_hash, imported_at_ms) + VALUES (@guid, @path, @handler_id, @version, @content_hash, @settings_hash, @imported_at_ms) ON CONFLICT(guid) DO UPDATE SET source_path = excluded.source_path, handler_type_id = excluded.handler_type_id, - handler_version = excluded.handler_version"); + handler_version = excluded.handler_version, + content_hash = excluded.content_hash, + settings_hash = excluded.settings_hash, + imported_at_ms = excluded.imported_at_ms"); _cmdDelete = CreateCommand("DELETE FROM assets WHERE guid = @guid"); _cmdGetReferencers = CreateCommand("SELECT from_guid FROM dependencies WHERE to_guid = @guid"); _cmdGetDependencies = CreateCommand("SELECT to_guid FROM dependencies WHERE from_guid = @guid"); + _cmdInsertDep = CreateCommand("INSERT INTO dependencies (from_guid, to_guid) VALUES (@from, @to)"); _cmdClearDeps = CreateCommand("DELETE FROM dependencies WHERE from_guid = @guid"); _cmdEnumerate = CreateCommand("SELECT guid, source_path FROM assets"); @@ -135,6 +144,9 @@ public sealed partial class AssetCatalog : IDisposable _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.ExecuteNonQuery(); } } @@ -157,6 +169,20 @@ public sealed partial class AssetCatalog : IDisposable return result is byte[] bytes ? new Guid(bytes) : Guid.Empty; } + public DateTime? GetImportedAt(Guid guid) + { + _cmdGetImportedAt.Parameters.Clear(); + _cmdGetImportedAt.Parameters.AddWithValue("@guid", guid.ToByteArray()); + var result = _cmdGetImportedAt.ExecuteScalar(); + + if (result is long ticks) + { + return new DateTime(ticks, DateTimeKind.Utc); + } + + return null; + } + public void SetDependencies(Guid assetId, ReadOnlySpan dependencies) { lock (_writeLock) diff --git a/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs b/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs index 703d55d..c7002e5 100644 --- a/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs +++ b/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs @@ -1,9 +1,8 @@ using Ghost.Core; using Ghost.Core.Utilities; -using Ghost.Editor.Core.AssetHandler; +using Ghost.Editor.Core.Assets; using Ghost.Editor.Core.Contracts; using System.Collections.Concurrent; -using System.IO.MemoryMappedFiles; namespace Ghost.Editor.Core.Services; @@ -21,6 +20,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable private readonly ConcurrentDictionary _ignoreMetaWrites; private readonly ConcurrentHashSet _dirtyAssets; + private readonly ConcurrentDictionary _eventDebouncers; public event EventHandler? OnAssetChanged; public event EventHandler? OnAssetImported @@ -41,6 +41,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable _ignoreMetaWrites = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); _dirtyAssets = new ConcurrentHashSet(); + _eventDebouncers = new ConcurrentDictionary(); SyncCatalogWithDisk(); @@ -48,7 +49,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable { IncludeSubdirectories = true, EnableRaisingEvents = true, - NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.DirectoryName + NotifyFilter = NotifyFilters.LastWrite }; _watcher.Created += OnFileSystemEvent; @@ -72,7 +73,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable var meta = AssetMetaIO.ReadAsync(metaPath).AsTask().Result; if (meta != null) { - var sourceRelative = AssetMetaIO.GetSourcePath(Path.GetRelativePath(EditorApplication.AssetsFolderPath, metaPath)); + var sourceRelative = AssetMetaIO.GetSourcePath(Path.GetRelativePath(EditorApplication.ProjectPath, metaPath)); _catalog.Upsert(meta, sourceRelative); foundGuids.Add(meta.Guid); } @@ -90,57 +91,86 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable private async void OnFileSystemEvent(object sender, FileSystemEventArgs e) { var ext = Path.GetExtension(e.FullPath); - var relativePath = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.FullPath); - - if (_ignoreMetaWrites.TryRemove(e.FullPath, out _)) - { - return; - } if (ext is ".tmp" or ".gtemp") { return; } + if (_eventDebouncers.TryGetValue(e.FullPath, out var existingCts)) + { + existingCts.Cancel(); + existingCts.Dispose(); + } + + var cts = new CancellationTokenSource(); + _eventDebouncers[e.FullPath] = cts; + + try + { + // Add a small delay to group rapid sequential triggers together (250ms is usually sufficient) + await Task.Delay(250, cts.Token); + } + catch (TaskCanceledException) + { + // A newer event for this file interrupted us; abort this duplicate handling + return; + } + finally + { + if (_eventDebouncers.TryGetValue(e.FullPath, out var currentCts) && currentCts == cts) + { + _eventDebouncers.TryRemove(e.FullPath, out _); + cts.Dispose(); + } + } + + if (_ignoreMetaWrites.TryRemove(e.FullPath, out _)) + { + return; + } + + var relativePath = Path.GetRelativePath(EditorApplication.ProjectPath, e.FullPath); + var fileExists = File.Exists(e.FullPath); + if (ext == AssetMetaIO.META_EXTENSION) { - if (e.ChangeType == WatcherChangeTypes.Changed || e.ChangeType == WatcherChangeTypes.Created) + if (fileExists) { - var meta = AssetMetaIO.ReadAsync(e.FullPath).AsTask().Result; + var meta = await AssetMetaIO.ReadAsync(e.FullPath); if (meta != null) { _catalog.Upsert(meta, AssetMetaIO.GetSourcePath(relativePath)); - await _importCoordinator.EnqueueAsync(new ImportJob(meta.Guid, AssetMetaIO.GetSourcePath(relativePath), e.FullPath, ImportReason.SettingsChanged)); + await _importCoordinator.EnqueueAsync(new ImportJob(meta.Guid, AssetMetaIO.GetSourcePath(relativePath), relativePath, ImportReason.SettingsChanged)); } } - return; } var changeType = AssetChangeType.None; - if (e.ChangeType == WatcherChangeTypes.Created) + var guid = _catalog.GetGuid(relativePath); + + if (!fileExists) { - await HandleNewSourceFileAsync(relativePath); - changeType = AssetChangeType.Created; - } - else if (e.ChangeType == WatcherChangeTypes.Changed) - { - var guid = _catalog.GetGuid(relativePath); - if (guid != Guid.Empty) - { - await _importCoordinator.EnqueueAsync(new ImportJob(guid, relativePath, AssetMetaIO.GetMetaPath(e.FullPath), ImportReason.SourceChanged)); - changeType = AssetChangeType.Modified; - } - } - else if (e.ChangeType == WatcherChangeTypes.Deleted) - { - var guid = _catalog.GetGuid(relativePath); + // The file is no longer on disk. Wait safely completed. if (guid != Guid.Empty) { _catalog.Remove(guid); changeType = AssetChangeType.Deleted; } } + else if (guid == Guid.Empty) + { + // The file exists but isn't located inside our catalog yet -> Essentially a Creation + await HandleNewSourceFileAsync(relativePath); + changeType = AssetChangeType.Created; + } + else + { + // The file exists and is tracked in the catalog, but triggered an event -> Modification + await _importCoordinator.EnqueueAsync(new ImportJob(guid, relativePath, AssetMetaIO.GetMetaPath(relativePath), ImportReason.SourceChanged)); + changeType = AssetChangeType.Modified; + } if (changeType != AssetChangeType.None) { @@ -150,8 +180,8 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable private void OnFileSystemRenameEvent(object sender, RenamedEventArgs e) { - var oldRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.OldFullPath); - var newRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.FullPath); + var oldRelative = Path.GetRelativePath(EditorApplication.ProjectPath, e.OldFullPath); + var newRelative = Path.GetRelativePath(EditorApplication.ProjectPath, e.FullPath); var guid = _catalog.GetGuid(oldRelative); if (guid != Guid.Empty) @@ -276,8 +306,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable return Result.Failure("Meta file does not exist."); } - await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); - return await handler.LoadAssetAsync(stream, id, meta.Settings, token); + return await handler.LoadAssetAsync(path, id, meta.Settings, token); } catch (Exception ex) { @@ -305,9 +334,8 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable return Result.Failure("No Avaliable handler type."); } - await using var stream = new FileStream(path, FileMode.Open, FileAccess.Write, FileShare.None); // This will trigger the fsw and reimport automatically. - return await handler.SaveAssetAsync(stream, asset, token); + return await handler.SaveAssetAsync(path, asset, token); } catch (Exception ex) { diff --git a/src/Editor/Ghost.Editor.Core/Services/EditorContentProvider.cs b/src/Editor/Ghost.Editor.Core/Services/EditorContentProvider.cs index 566fb0d..fd0fa2d 100644 --- a/src/Editor/Ghost.Editor.Core/Services/EditorContentProvider.cs +++ b/src/Editor/Ghost.Editor.Core/Services/EditorContentProvider.cs @@ -1,5 +1,5 @@ using Ghost.Core; -using Ghost.Editor.Core.AssetHandler; +using Ghost.Editor.Core.Assets; using Ghost.Editor.Core.Contracts; using Ghost.Engine; diff --git a/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs b/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs index 43d2dcf..47bcfe5 100644 --- a/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs +++ b/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs @@ -1,5 +1,5 @@ using Ghost.Core; -using Ghost.Editor.Core.AssetHandler; +using Ghost.Editor.Core.Assets; using System.IO.Hashing; using System.Runtime.CompilerServices; using System.Text.Json; @@ -79,13 +79,14 @@ internal sealed partial class ImportCoordinator : IDisposable var fileName = $"{assetGuid:N}{IMPORTED_EXTENSION}"; var folderName = fileName.Substring(0, 2); - var finalPath = Path.Combine(EditorApplication.LibraryImportsFolderPath, folderName, fileName); - Directory.CreateDirectory(finalPath); + var importsFolder = Path.Combine(EditorApplication.LibraryImportsFolderPath, folderName); + var finalPath = Path.Combine(importsFolder, fileName); + Directory.CreateDirectory(importsFolder); return finalPath; } - private static async ValueTask ProcessImportAsync(ImportJob job, CancellationToken token) + private async ValueTask ProcessImportAsync(ImportJob job, CancellationToken token) { var meta = await AssetMetaIO.ReadAsync(job.MetaPath, token); if (meta is null) @@ -114,11 +115,7 @@ internal sealed partial class ImportCoordinator : IDisposable if (handler is IImportableAssetHandler importable) { var targetPath = GetImportedAssetPath(job.AssetGuid); - - await using var sourceStream = new FileStream(job.SourcePath, FileMode.Open, FileAccess.Read, FileShare.Read); - await using var targetStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None); - - importResult = await importable.ImportAsync(sourceStream, targetStream, job.AssetGuid, meta.Settings, token); + importResult = await importable.ImportAsync(job.SourcePath, targetPath, job.AssetGuid, meta.Settings, token); } if (importResult.IsSuccess) @@ -129,6 +126,8 @@ internal sealed partial class ImportCoordinator : IDisposable meta.LastImportedUtc = DateTime.UtcNow; await AssetMetaIO.WriteAsync(job.MetaPath, meta, token); + + OnImportCompleted?.Invoke(null, job.AssetGuid); } else { diff --git a/src/Editor/Ghost.Editor/ActivationHandler.cs b/src/Editor/Ghost.Editor/ActivationHandler.cs index c230ece..bd9cd21 100644 --- a/src/Editor/Ghost.Editor/ActivationHandler.cs +++ b/src/Editor/Ghost.Editor/ActivationHandler.cs @@ -66,13 +66,13 @@ internal static class ActivationHandler AllocationManager.Initialize(opts); - var assetRegistry = App.GetService(); - var engineCore = App.GetService(); + //var assetRegistry = App.GetService(); + //var engineCore = App.GetService(); - assetRegistry.OnAssetImported += (sender, e) => - { - engineCore.AssetManager.ReimportAsset(e); - }; + //assetRegistry.OnAssetImported += (sender, e) => + //{ + // engineCore.AssetManager.ReimportAsset(e); + //}; return ValueTask.CompletedTask; } diff --git a/src/Editor/Ghost.Editor/Models/ExplorerItem.cs b/src/Editor/Ghost.Editor/Models/ExplorerItem.cs index 6fd5d0d..6b59735 100644 --- a/src/Editor/Ghost.Editor/Models/ExplorerItem.cs +++ b/src/Editor/Ghost.Editor/Models/ExplorerItem.cs @@ -43,7 +43,7 @@ internal partial class ExplorerItem : ObservableObject public ExplorerItem(string name, string path, bool isDirectory, AssetType assetType = AssetType.Unknown) { Name = name; - Path = PathUtility.Normalize(path); + Path = path; IsDirectory = isDirectory; AssetType = assetType; diff --git a/src/Editor/Ghost.Editor/ViewModels/Controls/ContentBrowserViewModel.cs b/src/Editor/Ghost.Editor/ViewModels/Controls/ContentBrowserViewModel.cs index 9005ede..1c0ef9e 100644 --- a/src/Editor/Ghost.Editor/ViewModels/Controls/ContentBrowserViewModel.cs +++ b/src/Editor/Ghost.Editor/ViewModels/Controls/ContentBrowserViewModel.cs @@ -62,13 +62,13 @@ internal partial class ContentBrowserViewModel : ObservableObject private void OnAssetChanged(object? sender, AssetChangedEventArgs e) { - if (Path.GetExtension(e.AssetPath) == FileExtensions.META_FILE_EXTENSION) + if (e.AssetPath.EndsWith(FileExtensions.META_FILE_EXTENSION)) { return; } - var fullPath = PathUtility.Normalize(Path.Combine(EditorApplication.AssetsFolderPath, e.AssetPath)); - var dirPath = PathUtility.Normalize(Path.GetDirectoryName(fullPath)); + var fullPath = PathUtility.Normalize(e.AssetPath); + var dirPath = Path.GetDirectoryName(fullPath); if (string.Equals(dirPath, CurrentDirectoryPath, StringComparison.OrdinalIgnoreCase)) { @@ -78,7 +78,7 @@ internal partial class ContentBrowserViewModel : ObservableObject { if (e.ChangeType == AssetChangeType.Renamed && e.OldAssetPath != null) { - var oldFullPath = PathUtility.Normalize(Path.Combine(EditorApplication.AssetsFolderPath, e.OldAssetPath)); + var oldFullPath = PathUtility.Normalize(e.OldAssetPath); var oldItem = Files.FirstOrDefault(f => string.Equals(f.Path, oldFullPath, StringComparison.OrdinalIgnoreCase)); if (oldItem != null) Files.Remove(oldItem); } @@ -138,7 +138,7 @@ internal partial class ContentBrowserViewModel : ObservableObject foreach (var file in Directory.EnumerateFiles(path)) { - if (Path.GetExtension(file) == FileExtensions.META_FILE_EXTENSION) + if (file.EndsWith(FileExtensions.META_FILE_EXTENSION)) { continue; } diff --git a/src/Runtime/Ghost.Core/Ghost.Core.csproj b/src/Runtime/Ghost.Core/Ghost.Core.csproj index 25fe620..192f2f4 100644 --- a/src/Runtime/Ghost.Core/Ghost.Core.csproj +++ b/src/Runtime/Ghost.Core/Ghost.Core.csproj @@ -26,6 +26,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Runtime/Ghost.Engine/AssetManager.Texture.cs b/src/Runtime/Ghost.Engine/AssetManager.Texture.cs index f0358e2..4bbb43a 100644 --- a/src/Runtime/Ghost.Engine/AssetManager.Texture.cs +++ b/src/Runtime/Ghost.Engine/AssetManager.Texture.cs @@ -12,7 +12,7 @@ public struct TextureContentHeader { public uint width; public uint height; - public uint depth; + public uint bpc; public uint mipLevels; public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D public uint colorComponents; @@ -48,25 +48,25 @@ internal partial class AssetEntry }; } - private static TextureFormat GetTextureFormat(uint depth, uint colorComponents) + private static TextureFormat GetTextureFormat(uint bpc, uint colorComponents) { return colorComponents switch { - 1 => depth switch + 1 => bpc switch { 8 => TextureFormat.R8_UNorm, 16 => TextureFormat.R16_UNorm, 32 => TextureFormat.R32_UInt, _ => TextureFormat.Unknown, }, - 2 => depth switch + 2 => bpc switch { 8 => TextureFormat.R8G8_UNorm, 16 => TextureFormat.R16G16_UNorm, 32 => TextureFormat.R32G32_Float, _ => TextureFormat.Unknown, }, - 3 or 4 => depth switch + 3 or 4 => bpc switch { 8 => TextureFormat.R8G8B8A8_UNorm, 16 => TextureFormat.R16G16B16A16_Float, @@ -91,7 +91,7 @@ internal partial class AssetEntry Height = header.height, MipLevels = header.mipLevels, Slice = 1, - Format = GetTextureFormat(header.depth, header.colorComponents), + Format = GetTextureFormat(header.bpc, header.colorComponents), Dimension = (TextureDimension)header.dimension, Usage = TextureUsage.ShaderResource, }; diff --git a/src/Runtime/Ghost.Generator/AssetHandlerRegistrationGenerator.cs b/src/Runtime/Ghost.Generator/AssetHandlerRegistrationGenerator.cs index e0c301b..cac22a3 100644 --- a/src/Runtime/Ghost.Generator/AssetHandlerRegistrationGenerator.cs +++ b/src/Runtime/Ghost.Generator/AssetHandlerRegistrationGenerator.cs @@ -88,3 +88,73 @@ internal static partial class {registerTypeName} return null; } } + +[Generator] +internal class IAssetSettingsRegistrationGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var settingsCandidates = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (s, _) => s is ClassDeclarationSyntax, + transform: GetAssetSettingsSymbol) + .Where(symbol => symbol != null) + .Collect(); + context.RegisterSourceOutput(settingsCandidates, GenerateRegistrationCode); + } + + private void GenerateRegistrationCode(SourceProductionContext context, ImmutableArray array) + { + if (array.IsDefaultOrEmpty) + { + return; + } + + var sb = new System.Text.StringBuilder(); + + foreach (var iface in array) + { + sb.AppendLine($" global::Ghost.Editor.Core.AssetHandler.AssetHandlerRegistry.RegisterIAssetSettingsType(typeof({iface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}), \"{iface.Name}\");"); + } + + var registerTypeName = "g_iassetsettings_registeration"; + var code = $@"// + +[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] +internal static partial class {registerTypeName} +{{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterIAssetSettingsTypes() + {{ +{sb} + }} +}}"; + + context.AddSource($"{registerTypeName}.gen.cs", code); + } + + private INamedTypeSymbol GetAssetSettingsSymbol(GeneratorSyntaxContext context, CancellationToken token) + { + var classSyntax = (ClassDeclarationSyntax)context.Node; + if (context.SemanticModel.GetDeclaredSymbol(classSyntax) is not INamedTypeSymbol symbol) + { + return null; + } + + var iSettingsSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName("Ghost.Editor.Core.AssetHandler.IAssetSettings"); + if (iSettingsSymbol == null) + { + return null; + } + + foreach (var iface in symbol.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(iface, iSettingsSymbol)) + { + return symbol; + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/Runtime/Ghost.Graphics/Core/Mesh.cs b/src/Runtime/Ghost.Graphics/Core/Mesh.cs index 4fb862e..9ef37cc 100644 --- a/src/Runtime/Ghost.Graphics/Core/Mesh.cs +++ b/src/Runtime/Ghost.Graphics/Core/Mesh.cs @@ -6,7 +6,6 @@ using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics.Geometry; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Ghost.Graphics.Core; @@ -203,158 +202,6 @@ public struct Mesh : IResourceReleasable _meshletData.Dispose(); } - public unsafe void CookMeshlets() - { - if (_meshletData.meshlets.IsCreated) - { - _meshletData.meshlets.Dispose(); - } - - if (_meshletData.groups.IsCreated) - { - _meshletData.groups.Dispose(); - } - - if (_meshletData.hierarchyNodes.IsCreated) - { - _meshletData.hierarchyNodes.Dispose(); - } - - if (_meshletData.meshletVertices.IsCreated) - { - _meshletData.meshletVertices.Dispose(); - } - - if (_meshletData.meshletTriangles.IsCreated) - { - _meshletData.meshletTriangles.Dispose(); - } - - _meshletData.meshletCount = 0; - _meshletData.lodLevelCount = 0; - _meshletData.materialSlotCount = 0; - - // 1. Prepare Configuration - var config = new ClodConfig - { - maxVertices = 64, - minTriangles = 32, - maxTriangles = 124, - - partitionSpatial = true, - partitionSize = 16, - - clusterSpatial = false, - clusterSplitFactor = 2.0f, - - optimizeClusters = true, - optimizeClustersLevel = 1, - - simplifyRatio = 0.5f, - simplifyThreshold = 0.85f, - simplifyErrorMergePrevious = 1.0f, - simplifyErrorFactorSloppy = 2.0f, - simplifyPermissive = true, - simplifyFallbackPermissive = false, - simplifyFallbackSloppy = true, - }; - - // 2. Map Mesh to ClodMesh - var clodMesh = new ClodMesh - { - vertexPositions = (float*)Unsafe.AsPointer(ref _vertices[0].position), - vertexCount = (nuint)_vertices.Count, - vertexPositionsStride = (nuint)sizeof(Vertex), - vertexAttributes = (float*)Unsafe.AsPointer(ref _vertices[0].normal), - vertexAttributesStride = (nuint)sizeof(Vertex), - indices = (uint*)_indices.GetUnsafePtr(), - indexCount = (nuint)_indices.Count, - attributeProtectMask = 0, - }; - - // 3. Build - MeshletUtility.Build(in config, in clodMesh, Unsafe.AsPointer(ref this), MeshletOutputCallback); - - _meshletData.meshletCount = _meshletData.meshlets.IsCreated ? _meshletData.meshlets.Count : 0; - - if (_meshletData.groups.IsCreated && _meshletData.groups.Count > 0) - { - var maxLodLevel = 0u; - for (var i = 0; i < _meshletData.groups.Count; i++) - { - maxLodLevel = Math.Max(maxLodLevel, _meshletData.groups[i].lodLevel); - } - - _meshletData.lodLevelCount = (int)maxLodLevel + 1; - } - - _meshletData.materialSlotCount = 1; - } - - private static unsafe int MeshletOutputCallback(void* context, ClodGroup group, ReadOnlyUnsafeCollection clusters) - { - var mesh = (Mesh*)context; - - ref var data = ref mesh->_meshletData; - - // Ensure lists are initialized - if (!data.groups.IsCreated) data.groups = new UnsafeList(16, AllocationHandle.Persistent); - if (!data.meshlets.IsCreated) data.meshlets = new UnsafeList(64, AllocationHandle.Persistent); - if (!data.meshletVertices.IsCreated) data.meshletVertices = new UnsafeList(128, AllocationHandle.Persistent); - if (!data.meshletTriangles.IsCreated) data.meshletTriangles = new UnsafeList(128, AllocationHandle.Persistent); - - var meshletGroup = new MeshletGroup - { - boundingSphere = new SphereBounds(group.simplified.center, group.simplified.radius), - boundingBox = new AABB(group.simplified.center - group.simplified.radius, group.simplified.center + group.simplified.radius), - parentError = group.simplified.error, - meshletStartIndex = (uint)data.meshlets.Count, - meshletCount = (uint)clusters.Count, - lodLevel = (uint)group.depth - }; - data.groups.Add(meshletGroup); - - for (var i = 0; i < clusters.Count; i++) - { - var cluster = clusters[i]; - - var meshlet = new Meshlet - { - boundingSphere = new SphereBounds(cluster.bounds.center, cluster.bounds.radius), - parentBoundingSphere = new SphereBounds(group.simplified.center, group.simplified.radius), - boundingBox = new AABB(cluster.bounds.center - cluster.bounds.radius, cluster.bounds.center + cluster.bounds.radius), - vertexCount = (byte)cluster.vertexCount, - triangleCount = (byte)(cluster.localIndexCount / 3), - vertexOffset = (uint)data.meshletVertices.Count, - triangleOffset = (uint)data.meshletTriangles.Count, - groupIndex = (uint)data.groups.Count - 1, - clusterError = cluster.bounds.error, - parentError = group.simplified.error, - localMaterialIndex = 0, // TODO: support multiple materials - lodLevel = (byte)group.depth, - }; - data.meshlets.Add(meshlet); - - // Add unique vertices - for (nuint j = 0; j < cluster.vertexCount; j++) - { - data.meshletVertices.Add(cluster.uniqueVertices[j]); - } - // Add local triangles (packed into uints) - var triangleCount = cluster.localIndexCount / 3; - for (nuint j = 0; j < triangleCount; j++) - { - uint i0 = cluster.localIndices[j * 3 + 0]; - uint i1 = cluster.localIndices[j * 3 + 1]; - uint i2 = cluster.localIndices[j * 3 + 2]; - var packedTriangle = i0 | (i1 << 8) | (i2 << 16); - data.meshletTriangles.Add(packedTriangle); - } - } - - return 0; - } - public void ReleaseResource(IResourceDatabase database) { ReleaseCpuResources(); diff --git a/src/Runtime/Ghost.Graphics/Core/RenderContext.cs b/src/Runtime/Ghost.Graphics/Core/RenderContext.cs index 912b284..213365f 100644 --- a/src/Runtime/Ghost.Graphics/Core/RenderContext.cs +++ b/src/Runtime/Ghost.Graphics/Core/RenderContext.cs @@ -130,7 +130,7 @@ public readonly unsafe ref struct RenderContext if (staticMesh) { - meshData.CookMeshlets(); + //meshData.CookMeshlets(); UploadMeshlets(mesh); meshData.ReleaseCpuResources(); } diff --git a/src/Test/Ghost.MicroTest/Program.cs b/src/Test/Ghost.MicroTest/Program.cs index 1cb6a55..3bffb95 100644 --- a/src/Test/Ghost.MicroTest/Program.cs +++ b/src/Test/Ghost.MicroTest/Program.cs @@ -1,4 +1,4 @@ using Ghost.MicroTest; using Ghost.Test.Core; -TestRunner.Run(); \ No newline at end of file +TestRunner.Run(); \ No newline at end of file diff --git a/src/Test/Ghost.Shader.Test/Program.cs b/src/Test/Ghost.Shader.Test/Program.cs index 9dc0694..38bb74b 100644 --- a/src/Test/Ghost.Shader.Test/Program.cs +++ b/src/Test/Ghost.Shader.Test/Program.cs @@ -7,7 +7,7 @@ using System.Numerics; //return; #if true -var result = DSLShaderCompiler.CompileComputeShader("F:\\csharp\\GhostEngine\\src\\Runtime\\Ghost.Graphics\\TestCompute.gcomp"); +var result = DSLShaderCompiler.CompileComputeShaderCode("F:\\csharp\\GhostEngine\\src\\Runtime\\Ghost.Graphics\\TestCompute.gcomp"); if (result.IsFailure) { Console.WriteLine(result.Message);