diff --git a/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs b/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs index bfb9851..94cade3 100644 --- a/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs +++ b/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs @@ -18,7 +18,7 @@ public struct DSLShaderError } } -internal static class DSLShaderCompiler +public static class DSLShaderCompiler { private static PipelineState MeragePipeline(PipelineSemantic? semantic, PipelineState parent) { @@ -141,21 +141,27 @@ internal static class DSLShaderCompiler var descriptor = new GraphicsShaderDescriptor { - name = semantics.name, - propertyBufferSize = propertyInfo.size, + Name = semantics.name, + PropertyBufferSize = propertyInfo.size, - shaderModel = semantics.shaderModel, - passes = passes + ShaderModel = semantics.shaderModel, + Passes = passes }; - for (var i = 0; i < descriptor.passes.Length; i++) + for (var i = 0; i < descriptor.Passes.Length; i++) { - descriptor.passes[i].shader = descriptor; + descriptor.Passes[i].shader = descriptor; } return descriptor; } + public static Result CompileGraphicsShader(Stream stream) + { + using var reader = new StreamReader(stream); + return CompileGraphicsShader(reader.ReadToEnd()); + } + public static Result CompileGraphicsShader(string shaderPath) { try @@ -163,7 +169,8 @@ internal static class DSLShaderCompiler var source = File.ReadAllText(shaderPath); // Use ANTLR4 parser - var shaderModels = AntlrShaderCompiler.ParseShaders(source, out var parseErrors); + var parseErrors = new List(); + var shaderModels = AntlrShaderCompiler.ParseShaders(source, parseErrors); if (parseErrors.Count != 0) { @@ -209,13 +216,20 @@ internal static class DSLShaderCompiler } } + public static Result CompileComputeShader(Stream stream) + { + using var reader = new StreamReader(stream); + return CompileComputeShader(reader.ReadToEnd()); + } + public static Result CompileComputeShader(string shaderPath) { try { var source = File.ReadAllText(shaderPath); - var shaderModels = AntlrShaderCompiler.ParseComputeShaders(source, out var parseErrors); + var parseErrors = new List(); + var shaderModels = AntlrShaderCompiler.ParseComputeShaders(source, parseErrors); if (parseErrors.Count != 0) { @@ -281,12 +295,12 @@ internal static class DSLShaderCompiler return new ComputeShaderDescriptor { - name = semantics.name, - propertyBufferSize = propertyInfo.size, - shaderModel = semantics.shaderModel, - shaderCodes = shaderCodes, - defines = semantics.defines?.ToArray() ?? Array.Empty(), - keywords = semantics.keywords?.ToArray() ?? Array.Empty() + Name = semantics.name, + PropertyBufferSize = propertyInfo.size, + ShaderModel = semantics.shaderModel, + ShaderCodes = shaderCodes, + Defines = semantics.defines?.ToArray() ?? Array.Empty(), + Keywords = semantics.keywords?.ToArray() ?? Array.Empty() }; } } diff --git a/src/Editor/Ghost.DSL/ShaderParser/AntlrShaderCompiler.cs b/src/Editor/Ghost.DSL/ShaderParser/AntlrShaderCompiler.cs index 9ab73a6..d718853 100644 --- a/src/Editor/Ghost.DSL/ShaderParser/AntlrShaderCompiler.cs +++ b/src/Editor/Ghost.DSL/ShaderParser/AntlrShaderCompiler.cs @@ -7,10 +7,8 @@ namespace Ghost.DSL.ShaderParser; public class AntlrShaderCompiler { - public static List ParseShaders(string source, out List errors) + public static List ParseShaders(string source, List errors) { - errors = new List(); - try { var inputStream = new AntlrInputStream(source); @@ -53,7 +51,7 @@ public class AntlrShaderCompiler } } - public static List ParseComputeShaders(string source, out List errors) + public static List ParseComputeShaders(string source, List errors) { errors = new List(); diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs index e27269e..2bffe1a 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs @@ -6,7 +6,7 @@ namespace Ghost.Editor.Core.AssetHandler; [AttributeUsage(AttributeTargets.Class)] public sealed class CustomAssetHandlerAttribute : Attribute { - public CustomAssetHandlerAttribute(string TypeID, string[] supportedExtensions, int version = 1) + public CustomAssetHandlerAttribute(string assetTypeID, string[] supportedExtensions, int version = 1) { } } @@ -33,14 +33,23 @@ public interface IAssetExportOptions; public interface IAssetHandler { - bool CanExport => false; AssetType RuntimeAssetType { get; } Guid EditorAssetTypeID { get; } IAssetSettings? CreateDefaultSettings(); - ValueTask> LoadAssetAsync(Stream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default); - ValueTask SaveAssetAsync(Stream targetStream, IAsset asset, CancellationToken token = default); - ValueTask ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default); - ValueTask ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default); + ValueTask> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default); + ValueTask SaveAssetAsync(FileStream targetStream, 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); +} + +public interface IPackableAssetHandler : IAssetHandler +{ + ValueTask PackAsync(FileStream assetStream, Stream 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/AssetHandler/AssetHandlerRegistry.cs index 45fa724..52d5bae 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandlerRegistry.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandlerRegistry.cs @@ -2,10 +2,6 @@ using Ghost.Engine; namespace Ghost.Editor.Core.AssetHandler; -/// -/// One-time scan at editor startup → two dictionaries. -/// All lookups are O(1) after construction. -/// public static class AssetHandlerRegistry { private static readonly Dictionary s_byExtension; @@ -21,10 +17,10 @@ public static class AssetHandlerRegistry s_versionByTypeId = new Dictionary(); } - public static void RegisterHandler(IAssetHandler handler, Guid typeId, ReadOnlySpan extensions, int version) + public static void RegisterHandler(IAssetHandler handler, Guid assetTypeId, ReadOnlySpan extensions, int version) { - s_byTypeId[typeId] = handler; - s_versionByTypeId[typeId] = version; + s_byTypeId[assetTypeId] = handler; + s_versionByTypeId[assetTypeId] = version; foreach (var ext in extensions) { @@ -46,13 +42,13 @@ public static class AssetHandlerRegistry return handler; } - public static IAssetHandler? GetByTypeId(Guid typeId) + public static IAssetHandler? GetByAssetTypeId(Guid typeId) { s_byTypeId.TryGetValue(typeId, out var handler); return handler; } - public static int GetVersionByTypeId(Guid typeId) + public static int GetVersionByAssetTypeId(Guid typeId) { s_versionByTypeId.TryGetValue(typeId, out var version); return version; @@ -63,7 +59,7 @@ public static class AssetHandlerRegistry return s_byExtension.Keys; } - public static AssetType GetAssetTypeByExtension(string extension) + public static AssetType GetRuntimeAssetTypeByExtension(string extension) { if (string.IsNullOrEmpty(extension)) { diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/MeshAssetHandler.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/MeshAssetHandler.cs new file mode 100644 index 0000000..93521c5 --- /dev/null +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/MeshAssetHandler.cs @@ -0,0 +1,380 @@ +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/ShaderAssetHandler.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/ShaderAssetHandler.cs new file mode 100644 index 0000000..f7a95a7 --- /dev/null +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/ShaderAssetHandler.cs @@ -0,0 +1,158 @@ +using Ghost.Core; +using Ghost.Core.Graphics; +using Ghost.DSL.ShaderCompiler; +using Ghost.Engine; +using System.Runtime.InteropServices; + +namespace Ghost.Editor.Core.AssetHandler; + +[Guid(GUID)] +public sealed partial class GraphicsShaderAsset : IAsset +{ + public const string GUID = "7BD4591C-B017-4814-AA0B-3F30EB3E727E"; + + public Guid ID + { + get; + } + + public IAssetSettings Settings + { + get; + } + + public Guid TypeID => typeof(GraphicsShaderAsset).GUID; + + public GraphicsShaderDescriptor Descriptor + { + get; + } + + internal GraphicsShaderAsset(GraphicsShaderDescriptor descriptor, Guid id) + { + ID = id; + Descriptor = descriptor; + } + + public void Dispose() + { + } +} + +[Guid(GUID)] +public sealed partial class ComputeShaderAsset : IAsset +{ + public const string GUID = "EA881979-CD8D-4088-B568-D42645F18C2A"; + + public Guid ID + { + get; + } + + public IAssetSettings Settings + { + get; + } + + public Guid TypeID => typeof(ComputeShaderAsset).GUID; + + public ComputeShaderDescriptor Descriptor + { + get; + } + + internal ComputeShaderAsset(ComputeShaderDescriptor descriptor, Guid id) + { + ID = id; + Descriptor = descriptor; + } + + public void Dispose() + { + } +} + +// Shader does not handle import/export via asset registry, it will handled by the hot reload system. +[CustomAssetHandler(GraphicsShaderAsset.GUID, [".gshdr"], 1)] +internal class GraphicsShaderAssetHandler : IPackableAssetHandler +{ + public AssetType RuntimeAssetType => AssetType.Shader; + public Guid EditorAssetTypeID => typeof(GraphicsShaderAsset).GUID; + + public IAssetSettings? CreateDefaultSettings() + { + return null; + } + + public async ValueTask> LoadAssetAsync(FileStream assetStream, 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); + if (result.IsFailure) + { + return Result.Failure(result.Message); + } + + return new GraphicsShaderAsset(result.Value, id); + } + catch (Exception ex) + { + return Result.Failure($"Failed to load shader asset: {ex.Message}"); + } + } + + public ValueTask SaveAssetAsync(FileStream targetStream, 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) + { + throw new NotImplementedException(); + } +} + +[CustomAssetHandler(ComputeShaderAsset.GUID, [".gcomp"], 1)] +internal class ComputeShaderAssetHandler : IPackableAssetHandler +{ + public AssetType RuntimeAssetType => AssetType.Shader; + public Guid EditorAssetTypeID => typeof(ComputeShaderAsset).GUID; + + public IAssetSettings? CreateDefaultSettings() + { + return null; + } + + public async ValueTask> LoadAssetAsync(FileStream assetStream, 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); + if (result.IsFailure) + { + return Result.Failure(result.Message); + } + + return new ComputeShaderAsset(result.Value, id); + } + catch (Exception ex) + { + return Result.Failure($"Failed to load shader asset: {ex.Message}"); + } + } + + public ValueTask SaveAssetAsync(FileStream targetStream, 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) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAssetHandler.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAssetHandler.cs index e866bbe..e2e8e52 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAssetHandler.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAssetHandler.cs @@ -1,9 +1,12 @@ using Ghost.Core; using Ghost.Engine; using Ghost.Graphics.RHI; -using ImageMagick; +using Ghost.StbI; 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; @@ -48,7 +51,7 @@ public enum MipmapFilter : uint } [Guid(GUID)] -public class TextureAsset : IAsset +public unsafe class TextureAsset : IAsset { public const string GUID = "27965FFF-860C-40EF-9123-1874D7DE9CDC"; @@ -57,7 +60,7 @@ public class TextureAsset : IAsset private readonly Guid _id; private readonly IAssetSettings _settings; - private readonly MagickImage _textureData; + private readonly IntPtr _textureData; private readonly uint _width; private readonly uint _height; private readonly uint _depth; @@ -65,17 +68,17 @@ public class TextureAsset : IAsset private readonly uint _dimension; public Guid ID => _id; - public Guid TypeID => s_typeID; + public Guid TypeID => typeof(TextureAsset).GUID; public IAssetSettings Settings => _settings; - public MagickImage TextureData => _textureData; + public IntPtr TextureData => _textureData; public uint Width => _width; public uint Height => _height; public uint Depth => _depth; public uint Dimension => _dimension; public uint ColorComponents => _colorComponents; - internal TextureAsset([OwnershipTransfer] MagickImage data, TextureContentHeader header, Guid id, IAssetSettings settings) + internal TextureAsset([OwnershipTransfer] IntPtr data, TextureContentHeader header, Guid id, IAssetSettings settings) { _id = id; _settings = settings; @@ -95,7 +98,7 @@ public class TextureAsset : IAsset public void Dispose() { - _textureData.Dispose(); + StbIApi.ImageFree((void*)_textureData); GC.SuppressFinalize(this); } } @@ -253,8 +256,19 @@ public class TextureAssetSettings : IAssetSettings } [CustomAssetHandler(TextureAsset.GUID, [".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr"], 1)] -internal class TextureAssetHandler : IAssetHandler +internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHandler { + internal struct TextureInfo + { + public IntPtr pixelData; + public int width; + public int height; + public int depth; + public int colorComponents; + public bool isHDR; + } + + public bool CanExport => false; public AssetType RuntimeAssetType => AssetType.Texture; public Guid EditorAssetTypeID => typeof(TextureAsset).GUID; @@ -291,23 +305,86 @@ internal class TextureAssetHandler : IAssetHandler return TextureDimension.Texture2D; } - public ValueTask> LoadAssetAsync(Stream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) + private static unsafe Result GetImageInfo(FileStream sourceStream) + { + using var mmf = MemoryMappedFile.CreateFromFile(sourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true); + 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); + + 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."); + } + + bitsPerChannel = StbIApi.Is16BitFromMemory(bufferSpan) > 0 ? 16 : 8; + + void* pPixels; + if (bitsPerChannel > 8) + { + pPixels = StbIApi.LoadfFromMemory(bufferSpan, &imageWidth, &imageHeight, &colorComponents, 4); + } + else + { + pPixels = StbIApi.LoadFromMemory(bufferSpan, &imageWidth, &imageHeight, &colorComponents, 4); + } + + return new TextureInfo + { + pixelData = (IntPtr)pPixels, + width = imageWidth, + height = imageHeight, + depth = bitsPerChannel, + colorComponents = colorComponents, + isHDR = isHDR, + }; + } + catch (Exception ex) + { + return Result.Failure($"Failed to get image info: {ex.Message}"); + } + finally + { + if (ptr != null) + { + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); + } + } + } + + public ValueTask> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) { try { - var image = new MagickImage(assetStream); + var infoResult = GetImageInfo(assetStream); + 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 = image.Width, - height = image.Height, - depth = image.Depth, - colorComponents = image.ChannelCount, + width = (uint)info.width, + height = (uint)info.height, + depth = (uint)info.depth, + colorComponents = (uint)info.colorComponents, dimension = (uint)GetTextureDimension(textureSettings) }; - return ValueTask.FromResult(Result.Success(new TextureAsset(image, contentHeader, id, textureSettings))); + return ValueTask.FromResult(Result.Success(new TextureAsset(info.pixelData, contentHeader, id, textureSettings))); } catch (Exception ex) { @@ -315,54 +392,109 @@ internal class TextureAssetHandler : IAssetHandler } } - public async ValueTask SaveAssetAsync(Stream targetStream, IAsset asset, CancellationToken token = default) + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private unsafe static 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) { if (asset is not TextureAsset textureAsset) { return Result.Failure("Asset type is not TextureAsset"); } - try + return await Task.Run(() => { - await textureAsset.TextureData.WriteAsync(targetStream, token); - return Result.Success(); - } - catch (Exception ex) - { - return Result.Failure(ex.Message); - } + var gcHandle = GCHandle.Alloc(targetStream, GCHandleType.Normal); + + try + { + var ext = Path.GetExtension(targetStream.Name); + + unsafe + { + switch (ext) + { + case ".png": + StbIApi.WritePngToFunc(&WriteCallback, (void*)GCHandle.ToIntPtr(gcHandle), (int)textureAsset.Width, (int)textureAsset.Height, (int)textureAsset.ColorComponents, (void*)textureAsset.TextureData, 0); + break; + + case ".jpg": + StbIApi.WriteJpgToFunc(&WriteCallback, (void*)GCHandle.ToIntPtr(gcHandle), (int)textureAsset.Width, (int)textureAsset.Height, (int)textureAsset.ColorComponents, (void*)textureAsset.TextureData, 90); + break; + + // TODO: Add support for other image formats + + default: + return Result.Failure($"Unsupported image format: {ext}"); + } + } + + return Result.Success(); + } + catch (Exception ex) + { + return Result.Failure(ex.Message); + } + finally + { + gcHandle.Free(); + } + }, token).ConfigureAwait(false); } - public async ValueTask ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) + public async ValueTask ImportAsync(FileStream sourceStream, FileStream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) { + if (sourceStream.Length > int.MaxValue) + { + return Result.Failure("Source stream is too large."); + } + try { - using var image = new MagickImage(sourceStream); - var pixels = image.GetPixelsUnsafe().GetAreaPointer(0, 0, image.Width, image.Height); - if (pixels == 0) + 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); + if (!infoResult.IsSuccess) { - return Result.Failure("Failed to retrieve pixel data from the source image."); + return Result.Failure(infoResult.Message); } - var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings(); - var (path, mip) = await TextureProcessor.CompressToCacheAsync(EditorApplication.CacheFolderPath, id, pixels, image.Width, image.Height, image.Depth, textureSettings, token) - .ConfigureAwait(false); + var info = infoResult.Value; + var result = await TextureProcessor.CompressToCacheAsync(EditorApplication.CacheFolderPath, id, + info, + textureSettings, token) + .ConfigureAwait(false); + + if (result.IsFailure) + { + return result; + } + + var (cachePath, mip) = result.Value; targetStream.Seek(0, SeekOrigin.Begin); - var contentHeader = new TextureContentHeader + var header = new TextureContentHeader { - width = image.Width, - height = image.Height, - depth = image.Depth, - colorComponents = image.ChannelCount, + width = (uint)info.width, + height = (uint)info.height, + depth = (uint)info.depth, + colorComponents = (uint)info.colorComponents, mipLevels = (uint)mip, dimension = (uint)GetTextureDimension(textureSettings) }; - targetStream.Write(MemoryMarshal.AsBytes(new Span(ref contentHeader))); + targetStream.Write(MemoryMarshal.AsBytes(new Span(ref header))); - await using var ddsStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + await using var ddsStream = new FileStream(cachePath, FileMode.Open, FileAccess.Read, FileShare.Read); await ddsStream.CopyToAsync(targetStream, token).ConfigureAwait(false); await targetStream.FlushAsync(token).ConfigureAwait(false); @@ -374,8 +506,13 @@ internal class TextureAssetHandler : IAssetHandler } } - public ValueTask ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default) + public ValueTask ExportAsync(FileStream assetStream, FileStream targetStream, 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) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs index e03a859..97f4700 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs @@ -1,3 +1,4 @@ +using Ghost.Core; using Ghost.Nvtt; using Misaki.HighPerformance.LowLevel; using System.IO.Hashing; @@ -24,27 +25,19 @@ internal static class TextureProcessor { private readonly string _outputPath; - private readonly nint _image; - private readonly uint _depth; - private readonly uint _width; - private readonly uint _height; + private readonly TextureAssetHandler.TextureInfo _textureInfo; private readonly TextureAssetSettings _settings; - private readonly TaskCompletionSource _completionSource; + private readonly TaskCompletionSource> _completionSource; - public int mipmapCount; + public Task> Task => _completionSource.Task; - public Task Task => _completionSource.Task; - - public NvttPipelineTask(string outputPath, nint image, uint width, uint height, uint depth, TextureAssetSettings settings) + public NvttPipelineTask(string outputPath, TextureAssetHandler.TextureInfo textureInfo, TextureAssetSettings settings) { _outputPath = outputPath; - _image = image; - _width = width; - _height = height; - _depth = depth; + _textureInfo = textureInfo; _settings = settings; - _completionSource = new TaskCompletionSource(); + _completionSource = new TaskCompletionSource>(); } public unsafe void Execute() @@ -54,15 +47,22 @@ internal static class TextureProcessor using var pOutOpts = new DisposablePtr(NvttOutputOptions.Create()); using var pCtx = new DisposablePtr(NvttContext.Create()); - var inputFormat = _depth > 8 - ? NvttInputFormat.NVTT_InputFormat_RGBA_32F - : NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below + var inputFormat = _textureInfo.colorComponents == 1 + ? NvttInputFormat.NVTT_InputFormat_R_32F + : _textureInfo.depth > 8 + ? NvttInputFormat.NVTT_InputFormat_RGBA_32F + : NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below - pSurface.Get()->SetImageData(inputFormat, (int)_width, (int)_height, 1, (void*)_image, NvttBoolean.NVTT_True, null); + 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)) + { + _completionSource.SetResult(Result.Failure("Failed to set image data for NVTT compression.")); + return; + } // 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 (_depth <= 8) + if (_textureInfo.colorComponents > 1 && _textureInfo.depth <= 8) { pSurface.Get()->Swizzle(2, 1, 0, 3, null); } @@ -101,7 +101,7 @@ internal static class TextureProcessor pSurface.Get()->PremultiplyAlpha(null); } - pCompOpts.Get()->SetFormat(SelectFormat(_settings)); + pCompOpts.Get()->SetFormat(SelectFormat(_settings, _textureInfo.isHDR)); pCompOpts.Get()->SetQuality(SelectQuality(_settings.Advanced.CompressionLevel)); if (_settings.Advanced.CutoutAlpha) @@ -117,6 +117,7 @@ internal static class TextureProcessor var nvttFilter = SelectMipmapFilter(_settings.Advanced.MipmapFilter); + int mipmapCount; if (!_settings.Advanced.GenerateMipmaps) { mipmapCount = 1; @@ -155,11 +156,13 @@ internal static class TextureProcessor } } - _completionSource.SetResult(); + _completionSource.SetResult(Result.Success(mipmapCount)); } } - public static async ValueTask<(string cachePath, int mipmapCount)> CompressToCacheAsync(string cachesFolderPath, Guid assetId, nint image, uint width, uint height, uint depth, TextureAssetSettings settings, CancellationToken cancellationToken) + public static async ValueTask> CompressToCacheAsync(string cachesFolderPath, Guid assetId, + TextureAssetHandler.TextureInfo textureInfo, + TextureAssetSettings settings, CancellationToken cancellationToken) { var settingsHash = ComputeSettingsHash(settings); var cacheFileName = $"texturecache_{assetId:N}_{settingsHash:X16}.dds"; @@ -202,21 +205,27 @@ internal static class TextureProcessor } ScheduleWork: - var workItem = new NvttPipelineTask(cachePath, image, width, height, depth, settings); + var workItem = new NvttPipelineTask(cachePath, textureInfo, settings); ThreadPool.UnsafeQueueUserWorkItem(workItem, true); - await workItem.Task.WaitAsync(cancellationToken).ConfigureAwait(false); + var result = await workItem.Task.WaitAsync(cancellationToken).ConfigureAwait(false); + if (result.IsFailure) + { + return Result.Failure(result.Message); + } - return (cachePath, workItem.mipmapCount); + return (cachePath, result.Value); } - private static NvttFormat SelectFormat(TextureAssetSettings settings) - => settings.Basic.TextureType switch - { - TextureType.Normal => NvttFormat.NVTT_Format_BC5, // RG normal map - TextureType.SingleChannel => NvttFormat.NVTT_Format_BC4, // single channel - TextureType.Lightmap => NvttFormat.NVTT_Format_BC6U, // HDR lightmap (unsigned) - _ => NvttFormat.NVTT_Format_BC7, // default color - }; + private static NvttFormat SelectFormat(TextureAssetSettings settings, bool isHDR) + => isHDR + ? NvttFormat.NVTT_Format_BC6U + : settings.Basic.TextureType switch + { + TextureType.Normal => NvttFormat.NVTT_Format_BC5, // RG normal map + TextureType.SingleChannel => NvttFormat.NVTT_Format_BC4, // single channel + TextureType.Lightmap => NvttFormat.NVTT_Format_BC6U, // HDR lightmap (unsigned) + _ => NvttFormat.NVTT_Format_BC7, // default color + }; private static NvttQuality SelectQuality(TextureCompressionLevel level) => level switch diff --git a/src/Editor/Ghost.Editor.Core/Contracts/IAssetRegistry.cs b/src/Editor/Ghost.Editor.Core/Contracts/IAssetRegistry.cs index 65543cd..a36af84 100644 --- a/src/Editor/Ghost.Editor.Core/Contracts/IAssetRegistry.cs +++ b/src/Editor/Ghost.Editor.Core/Contracts/IAssetRegistry.cs @@ -1,5 +1,6 @@ using Ghost.Core; using Ghost.Editor.Core.AssetHandler; +using Ghost.Editor.Core.Services; using Ghost.Engine.AssetLoader; namespace Ghost.Editor.Core.Contracts; @@ -40,12 +41,14 @@ public sealed class AssetChangedEventArgs : EventArgs public interface IAssetRegistry : IDisposable { + event EventHandler? OnAssetChanged; + event EventHandler? OnAssetImported; + + AssetCatalog GetAssetCatalog(); + string? GetAssetPath(Guid id); Guid GetAssetGuid(string assetPath); - event EventHandler? OnAssetChanged; - - ValueTask> ImportAssetAsync(string sourceFilePath, string targetAssetPath, CancellationToken token = default); ValueTask ReimportAssetAsync(Guid assetId, string sourceFilePath, CancellationToken token = default); ValueTask> LoadAssetAsync(Guid id, CancellationToken token = default); diff --git a/src/Editor/Ghost.Editor.Core/EditorApplication.cs b/src/Editor/Ghost.Editor.Core/EditorApplication.cs index 53dfe95..6cc41b7 100644 --- a/src/Editor/Ghost.Editor.Core/EditorApplication.cs +++ b/src/Editor/Ghost.Editor.Core/EditorApplication.cs @@ -55,6 +55,8 @@ public static class EditorApplication internal static void Initialize(IServiceProvider serviceProvider, string projectPath, string projectName) { + Environment.CurrentDirectory = projectPath; + s_serviceProvider = serviceProvider; s_currentProjectPath = projectPath; s_currentProjectName = projectName; diff --git a/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj b/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj index 8766dac..e2cdba7 100644 --- a/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj +++ b/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj @@ -15,7 +15,6 @@ - @@ -29,6 +28,8 @@ + + diff --git a/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs b/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs index 0752dce..b67cc76 100644 --- a/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs +++ b/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs @@ -7,7 +7,7 @@ namespace Ghost.Editor.Core.Services; /// Thread-safe SQLite-backed asset catalog. /// Replaces the in-memory dictionary approach with persistent storage. /// -internal sealed class AssetCatalog : IDisposable +public sealed partial class AssetCatalog : IDisposable { private readonly SqliteConnection _connection; private readonly Lock _writeLock = new(); @@ -101,10 +101,20 @@ internal sealed class AssetCatalog : IDisposable cmd.ExecuteNonQuery(); } + private static string ToUniversalPath(string path) + { + if (OperatingSystem.IsWindows()) + { + return Path.GetFullPath(path).Replace('\\', '/'); + } + + return path; + } + public Guid GetGuid(string sourcePath) { _cmdGetGuid.Parameters.Clear(); - _cmdGetGuid.Parameters.AddWithValue("@path", sourcePath); + _cmdGetGuid.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath)); var result = _cmdGetGuid.ExecuteScalar(); return result is byte[] bytes ? new Guid(bytes) : Guid.Empty; } @@ -122,7 +132,7 @@ internal sealed class AssetCatalog : IDisposable { _cmdUpsert.Parameters.Clear(); _cmdUpsert.Parameters.AddWithValue("@guid", meta.Guid.ToByteArray()); - _cmdUpsert.Parameters.AddWithValue("@path", sourcePath); + _cmdUpsert.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath)); _cmdUpsert.Parameters.AddWithValue("@handler_id", meta.HandlerTypeId?.ToByteArray() ?? (object)DBNull.Value); _cmdUpsert.Parameters.AddWithValue("@version", meta.HandlerVersion); _cmdUpsert.ExecuteNonQuery(); diff --git a/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs b/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs index ea2a0d7..703d55d 100644 --- a/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs +++ b/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs @@ -3,6 +3,7 @@ using Ghost.Core.Utilities; using Ghost.Editor.Core.AssetHandler; using Ghost.Editor.Core.Contracts; using System.Collections.Concurrent; +using System.IO.MemoryMappedFiles; namespace Ghost.Editor.Core.Services; @@ -22,6 +23,11 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable private readonly ConcurrentHashSet _dirtyAssets; public event EventHandler? OnAssetChanged; + public event EventHandler? OnAssetImported + { + add => _importCoordinator.OnImportCompleted += value; + remove => _importCoordinator.OnImportCompleted -= value; + } public AssetRegistry() { @@ -67,7 +73,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable if (meta != null) { var sourceRelative = AssetMetaIO.GetSourcePath(Path.GetRelativePath(EditorApplication.AssetsFolderPath, metaPath)); - _catalog.Upsert(meta, sourceRelative.Replace(Path.DirectorySeparatorChar, '/')); + _catalog.Upsert(meta, sourceRelative); foundGuids.Add(meta.Guid); } } @@ -84,7 +90,7 @@ 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).Replace(Path.DirectorySeparatorChar, '/'); + var relativePath = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.FullPath); if (_ignoreMetaWrites.TryRemove(e.FullPath, out _)) { @@ -114,7 +120,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable var changeType = AssetChangeType.None; if (e.ChangeType == WatcherChangeTypes.Created) { - await HandleNewSourceFileAsync(e.FullPath, relativePath); + await HandleNewSourceFileAsync(relativePath); changeType = AssetChangeType.Created; } else if (e.ChangeType == WatcherChangeTypes.Changed) @@ -144,14 +150,14 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable private void OnFileSystemRenameEvent(object sender, RenamedEventArgs e) { - var oldRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.OldFullPath).Replace(Path.DirectorySeparatorChar, '/'); - var newRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.FullPath).Replace(Path.DirectorySeparatorChar, '/'); + var oldRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.OldFullPath); + var newRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.FullPath); var guid = _catalog.GetGuid(oldRelative); if (guid != Guid.Empty) { _catalog.Remove(guid); - var metaFile = AssetMetaIO.GetMetaPath(e.FullPath); + var metaFile = AssetMetaIO.GetMetaPath(newRelative); if (File.Exists(metaFile)) { var meta = AssetMetaIO.ReadAsync(metaFile).AsTask().Result; @@ -165,12 +171,12 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable OnAssetChanged?.Invoke(this, new AssetChangedEventArgs(newRelative, oldRelative, AssetChangeType.Renamed)); } - private async Task HandleNewSourceFileAsync(string fullPath, string relativePath) + private async Task HandleNewSourceFileAsync(string relativePath) { var ext = Path.GetExtension(relativePath); var handler = AssetHandlerRegistry.GetByExtension(ext); - var metaPath = AssetMetaIO.GetMetaPath(fullPath); + var metaPath = AssetMetaIO.GetMetaPath(relativePath); if (File.Exists(metaPath)) { return; @@ -193,6 +199,11 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable await _importCoordinator.EnqueueAsync(new ImportJob(meta.Guid, relativePath, metaPath, ImportReason.NewAsset)); } + public AssetCatalog GetAssetCatalog() + { + return _catalog; + } + public string? GetAssetPath(Guid id) { return _catalog.GetSourcePath(id); @@ -200,7 +211,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable public Guid GetAssetGuid(string path) { - return _catalog.GetGuid(path.Replace(Path.DirectorySeparatorChar, '/')); + return _catalog.GetGuid(path); } public async ValueTask> ImportAssetAsync(string sourceFilePath, string targetAssetPath, CancellationToken token = default) @@ -208,17 +219,13 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable // Simple copy + wait for FSW or manually trigger? // Current requirement: "returns the new GUID immediately (import happens in background)" - var ext = Path.GetExtension(sourceFilePath); - var relativePath = targetAssetPath.Replace(Path.DirectorySeparatorChar, '/'); - var fullPath = Path.Combine(EditorApplication.AssetsFolderPath, relativePath); - - Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); - File.Copy(sourceFilePath, fullPath, true); + Directory.CreateDirectory(Path.GetDirectoryName(targetAssetPath)!); + File.Copy(sourceFilePath, targetAssetPath, true); // FSW will trigger but we can speed it up - await HandleNewSourceFileAsync(fullPath, relativePath); + await HandleNewSourceFileAsync(targetAssetPath); - var guid = _catalog.GetGuid(relativePath); + var guid = _catalog.GetGuid(targetAssetPath); return Result.Success(guid); } @@ -230,8 +237,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable return Result.Failure("Asset not found"); } - var fullPath = Path.Combine(EditorApplication.AssetsFolderPath, path); - var metaPath = AssetMetaIO.GetMetaPath(fullPath); + var metaPath = AssetMetaIO.GetMetaPath(path); await _importCoordinator.EnqueueAsync(new ImportJob(assetId, path, metaPath, ImportReason.ManualReimport), token); return Result.Success(); @@ -293,13 +299,14 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable return Result.Failure("Asset does not exist."); } - var handler = AssetHandlerRegistry.GetByTypeId(asset.TypeID); + var handler = AssetHandlerRegistry.GetByAssetTypeId(asset.TypeID); if (handler is null) { 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); } catch (Exception ex) diff --git a/src/Editor/Ghost.Editor.Core/Services/EditorContentProvider.cs b/src/Editor/Ghost.Editor.Core/Services/EditorContentProvider.cs index be10317..566fb0d 100644 --- a/src/Editor/Ghost.Editor.Core/Services/EditorContentProvider.cs +++ b/src/Editor/Ghost.Editor.Core/Services/EditorContentProvider.cs @@ -1,5 +1,6 @@ using Ghost.Core; using Ghost.Editor.Core.AssetHandler; +using Ghost.Editor.Core.Contracts; using Ghost.Engine; namespace Ghost.Editor.Core.Services; @@ -8,9 +9,9 @@ internal class EditorContentProvider : IContentProvider { private readonly AssetCatalog _catalog; - public EditorContentProvider(AssetCatalog catalog) + public EditorContentProvider(IAssetRegistry assetRegistry) { - _catalog = catalog; + _catalog = assetRegistry.GetAssetCatalog(); } public bool HasAsset(Guid guid) @@ -20,7 +21,7 @@ internal class EditorContentProvider : IContentProvider public Result OpenRead(Guid guid, CancellationToken token = default) { - var importedPath = Path.Combine(EditorApplication.LibraryImportsFolderPath, $"{guid:N}{ImportCoordinator.IMPORTED_EXTENSION}"); + var importedPath = ImportCoordinator.GetImportedAssetPath(guid); if (!File.Exists(importedPath)) { return Result.Failure($"Imported asset not found for GUID: {guid}"); @@ -37,7 +38,7 @@ internal class EditorContentProvider : IContentProvider public AssetType GetAssetType(Guid guid) { var handlerID = _catalog.GetHandlerTypeId(guid); - var handler = AssetHandlerRegistry.GetByTypeId(handlerID); + var handler = AssetHandlerRegistry.GetByAssetTypeId(handlerID); return handler?.RuntimeAssetType ?? AssetType.Unknown; } } diff --git a/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs b/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs index 94e0f7b..43d2dcf 100644 --- a/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs +++ b/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs @@ -24,7 +24,7 @@ internal readonly record struct ImportJob( ImportReason Reason ); -internal sealed class ImportCoordinator : IDisposable +internal sealed partial class ImportCoordinator : IDisposable { public const string IMPORTED_EXTENSION_NAME = "Imported"; public const string IMPORTED_EXTENSION = ".imported"; @@ -34,9 +34,7 @@ internal sealed class ImportCoordinator : IDisposable private readonly CancellationTokenSource _cts; private readonly Task[] _workers; - // In a real implementation, this event would be used to notify the UI/Rest of engine - // For now we just focus on the core logic - // public event EventHandler? OnAssetChanged; + public event EventHandler? OnImportCompleted; public ImportCoordinator(AssetCatalog catalog, int workerCount = 2) { @@ -76,9 +74,19 @@ internal sealed class ImportCoordinator : IDisposable } } - private async ValueTask ProcessImportAsync(ImportJob job, CancellationToken token) + public static string GetImportedAssetPath(Guid assetGuid) + { + var fileName = $"{assetGuid:N}{IMPORTED_EXTENSION}"; + var folderName = fileName.Substring(0, 2); + + var finalPath = Path.Combine(EditorApplication.LibraryImportsFolderPath, folderName, fileName); + Directory.CreateDirectory(finalPath); + + return finalPath; + } + + private static async ValueTask ProcessImportAsync(ImportJob job, CancellationToken token) { - var fullSourcePath = Path.Combine(EditorApplication.AssetsFolderPath, job.SourcePath); var meta = await AssetMetaIO.ReadAsync(job.MetaPath, token); if (meta is null) { @@ -87,27 +95,27 @@ internal sealed class ImportCoordinator : IDisposable } var handler = meta.HandlerTypeId.HasValue - ? AssetHandlerRegistry.GetByTypeId(meta.HandlerTypeId.Value) + ? AssetHandlerRegistry.GetByAssetTypeId(meta.HandlerTypeId.Value) : AssetHandlerRegistry.GetByExtension(Path.GetExtension(job.SourcePath)); - var contentHash = await ComputeFileHashAsync(fullSourcePath, token); + var contentHash = await ComputeFileHashAsync(job.SourcePath, token); var settingsHash = ComputeSettingsHash(meta.Settings); // Check if we can skip (if not a manual reimport) if (job.Reason != ImportReason.ManualReimport && meta.ContentHash == contentHash && meta.SettingsHash == settingsHash && - meta.HandlerVersion == AssetHandlerRegistry.GetVersionByTypeId(meta.HandlerTypeId ?? Guid.Empty)) + meta.HandlerVersion == AssetHandlerRegistry.GetVersionByAssetTypeId(meta.HandlerTypeId ?? Guid.Empty)) { return; } var importResult = Result.Success(); - if (handler is IAssetHandler importable) + if (handler is IImportableAssetHandler importable) { - var targetPath = Path.Combine(EditorApplication.LibraryImportsFolderPath, $"{job.AssetGuid:N}{IMPORTED_EXTENSION}"); + var targetPath = GetImportedAssetPath(job.AssetGuid); - await using var sourceStream = new FileStream(fullSourcePath, FileMode.Open, FileAccess.Read, FileShare.Read); + 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); @@ -117,7 +125,7 @@ internal sealed class ImportCoordinator : IDisposable { meta.ContentHash = contentHash; meta.SettingsHash = settingsHash; - meta.HandlerVersion = AssetHandlerRegistry.GetVersionByTypeId(meta.HandlerTypeId ?? Guid.Empty); + meta.HandlerVersion = AssetHandlerRegistry.GetVersionByAssetTypeId(meta.HandlerTypeId ?? Guid.Empty); meta.LastImportedUtc = DateTime.UtcNow; await AssetMetaIO.WriteAsync(job.MetaPath, meta, token); diff --git a/src/Editor/Ghost.Editor.Core/Utilities/ShaderCompilerUtility.cs b/src/Editor/Ghost.Editor.Core/Utilities/ShaderCompilerUtility.cs index 5f12f9a..080652f 100644 --- a/src/Editor/Ghost.Editor.Core/Utilities/ShaderCompilerUtility.cs +++ b/src/Editor/Ghost.Editor.Core/Utilities/ShaderCompilerUtility.cs @@ -130,7 +130,7 @@ internal static class ShaderCompilerUtility public static Result>> CompileComputeShader(this IShaderCompiler shaderCompiler, ComputeShaderDescriptor descriptor, ref readonly ShaderCompilationConfig additionalConfig, AllocationHandle allocationHandle) { - var fullDefines = CombineDefines(descriptor.defines, additionalConfig.defines); + var fullDefines = CombineDefines(descriptor.Defines, additionalConfig.defines); var config = new ShaderCompilationConfig { @@ -141,11 +141,11 @@ internal static class ShaderCompilerUtility stage = ShaderStage.ComputeShader, }; - var compiled = new UnsafeArray>(descriptor.shaderCodes.Length, allocationHandle); - for (int i = 0; i < descriptor.shaderCodes.Length; i++) + var compiled = new UnsafeArray>(descriptor.ShaderCodes.Length, allocationHandle); + for (int i = 0; i < descriptor.ShaderCodes.Length; i++) { - config.shaderCode = descriptor.shaderCodes[i].code; - config.entryPoint = descriptor.shaderCodes[i].entryPoint; + config.shaderCode = descriptor.ShaderCodes[i].code; + config.entryPoint = descriptor.ShaderCodes[i].entryPoint; var result = shaderCompiler.Compile(ref config, allocationHandle); if (result.IsFailure) diff --git a/src/Editor/Ghost.Editor/ActivationHandler.cs b/src/Editor/Ghost.Editor/ActivationHandler.cs index ca73e82..c230ece 100644 --- a/src/Editor/Ghost.Editor/ActivationHandler.cs +++ b/src/Editor/Ghost.Editor/ActivationHandler.cs @@ -1,3 +1,4 @@ +using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Utilities; using Ghost.Editor.Models; using Ghost.Engine; @@ -64,8 +65,14 @@ internal static class ActivationHandler }; AllocationManager.Initialize(opts); - - App.GetService(); + + var assetRegistry = App.GetService(); + var engineCore = App.GetService(); + + assetRegistry.OnAssetImported += (sender, e) => + { + engineCore.AssetManager.ReimportAsset(e); + }; return ValueTask.CompletedTask; } diff --git a/src/Editor/Ghost.Editor/Themes/Generic.xaml b/src/Editor/Ghost.Editor/Themes/Generic.xaml index 9d12ae4..b88fe1f 100644 --- a/src/Editor/Ghost.Editor/Themes/Generic.xaml +++ b/src/Editor/Ghost.Editor/Themes/Generic.xaml @@ -50,10 +50,102 @@ - + +