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.
This commit is contained in:
2026-04-25 18:23:21 +09:00
parent 4757c0c91a
commit 1a91811621
27 changed files with 1523 additions and 748 deletions

View File

@@ -159,18 +159,27 @@ public static class DSLShaderCompiler
public static Result<GraphicsShaderDescriptor> CompileGraphicsShader(Stream stream) public static Result<GraphicsShaderDescriptor> CompileGraphicsShader(Stream stream)
{ {
using var reader = new StreamReader(stream); using var reader = new StreamReader(stream);
return CompileGraphicsShader(reader.ReadToEnd()); return CompileGraphicsShaderCode(reader.ReadToEnd());
} }
public static Result<GraphicsShaderDescriptor> CompileGraphicsShader(string shaderPath) public static Result<GraphicsShaderDescriptor> 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<GraphicsShaderDescriptor> CompileGraphicsShaderCode(string shaderCode)
{ {
try try
{ {
var source = File.ReadAllText(shaderPath);
// Use ANTLR4 parser // Use ANTLR4 parser
var parseErrors = new List<DSLShaderError>(); var parseErrors = new List<DSLShaderError>();
var shaderModels = AntlrShaderCompiler.ParseShaders(source, parseErrors); var shaderModels = AntlrShaderCompiler.ParseShaders(shaderCode, parseErrors);
if (parseErrors.Count != 0) if (parseErrors.Count != 0)
{ {
@@ -219,17 +228,26 @@ public static class DSLShaderCompiler
public static Result<ComputeShaderDescriptor> CompileComputeShader(Stream stream) public static Result<ComputeShaderDescriptor> CompileComputeShader(Stream stream)
{ {
using var reader = new StreamReader(stream); using var reader = new StreamReader(stream);
return CompileComputeShader(reader.ReadToEnd()); return CompileComputeShaderCode(reader.ReadToEnd());
} }
public static Result<ComputeShaderDescriptor> CompileComputeShader(string shaderPath) public static Result<ComputeShaderDescriptor> 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<ComputeShaderDescriptor> CompileComputeShaderCode(string shaderCode)
{ {
try try
{ {
var source = File.ReadAllText(shaderPath);
var parseErrors = new List<DSLShaderError>(); var parseErrors = new List<DSLShaderError>();
var shaderModels = AntlrShaderCompiler.ParseComputeShaders(source, parseErrors); var shaderModels = AntlrShaderCompiler.ParseComputeShaders(shaderCode, parseErrors);
if (parseErrors.Count != 0) if (parseErrors.Count != 0)
{ {

View File

@@ -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<Vertex> _vertices;
private UnsafeList<uint> _indices;
public Guid ID
{
get;
}
public IAssetSettings Settings
{
get;
}
public Guid TypeID => typeof(MeshAsset).GUID;
public Span<Vertex> Vertices => _vertices.AsSpan();
public Span<uint> Indices => _indices.AsSpan();
internal MeshAsset(ref UnsafeList<Vertex> vertices, ref UnsafeList<uint> 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<Vertex> vertices, ref UnsafeList<uint> 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<Result> _taskCompletionSource;
public UnsafeList<Vertex> vertices;
public UnsafeList<uint> indices;
public Task<Result> Task => _taskCompletionSource.Task;
public MeshParsingWorkItem(string filePath, AllocationHandle allocationHandle, MeshAssetSettings settings)
{
_filePath = filePath;
_allocationHandle = allocationHandle;
_settings = settings;
_taskCompletionSource = new TaskCompletionSource<Result>();
}
[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<byte>(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>(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<Vertex>(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<uint>(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<uint>((int)numIndices, AllocationHandle.FreeList);
using var cachedIndices = new UnsafeArray<uint>((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<Vertex>((int)numUniqueVertices, _allocationHandle);
indices = new UnsafeList<uint>((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<Result<IAsset>> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default)
{
throw new NotImplementedException();
}
public ValueTask<Result> SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default)
{
throw new NotImplementedException();
}
public ValueTask<Result> ImportAsync(FileStream sourceStream, FileStream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default)
{
throw new NotImplementedException();
}
public ValueTask<Result> ExportAsync(FileStream assetStream, FileStream targetStream, IAssetExportOptions? options, CancellationToken token = default)
{
throw new NotImplementedException();
}
}

View File

@@ -1,7 +1,7 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Engine; using Ghost.Engine;
namespace Ghost.Editor.Core.AssetHandler; namespace Ghost.Editor.Core.Assets;
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class)]
public sealed class CustomAssetHandlerAttribute : Attribute public sealed class CustomAssetHandlerAttribute : Attribute
@@ -23,7 +23,7 @@ public interface IAsset : IDisposable
get; get;
} }
public IAssetSettings Settings public IAssetSettings? Settings
{ {
get; get;
} }
@@ -38,18 +38,18 @@ public interface IAssetHandler
IAssetSettings? CreateDefaultSettings(); IAssetSettings? CreateDefaultSettings();
ValueTask<Result<IAsset>> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default); ValueTask<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default);
ValueTask<Result> SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default); ValueTask<Result> SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default);
} }
public interface IImportableAssetHandler : IAssetHandler public interface IImportableAssetHandler : IAssetHandler
{ {
bool CanExport { get; } bool CanExport { get; }
ValueTask<Result> ImportAsync(FileStream sourceStream, FileStream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default); ValueTask<Result> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default);
ValueTask<Result> ExportAsync(FileStream assetStream, FileStream targetStream, IAssetExportOptions? options, CancellationToken token = default); ValueTask<Result> ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default);
} }
public interface IPackableAssetHandler : IAssetHandler public interface IPackableAssetHandler : IAssetHandler
{ {
ValueTask<Result> PackAsync(FileStream assetStream, Stream targetStream, CancellationToken token = default); ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default);
} }

View File

@@ -1,6 +1,6 @@
using Ghost.Engine; using Ghost.Engine;
namespace Ghost.Editor.Core.AssetHandler; namespace Ghost.Editor.Core.Assets;
public static class AssetHandlerRegistry public static class AssetHandlerRegistry
{ {
@@ -9,12 +9,16 @@ public static class AssetHandlerRegistry
private static readonly Dictionary<Guid, IAssetHandler> s_byTypeId; private static readonly Dictionary<Guid, IAssetHandler> s_byTypeId;
private static readonly Dictionary<Guid, int> s_versionByTypeId; private static readonly Dictionary<Guid, int> s_versionByTypeId;
private static readonly List<(Type Type, string Name)> s_iAssetSettingsTypes;
static AssetHandlerRegistry() static AssetHandlerRegistry()
{ {
s_byExtension = new Dictionary<string, IAssetHandler>(StringComparer.OrdinalIgnoreCase); s_byExtension = new Dictionary<string, IAssetHandler>(StringComparer.OrdinalIgnoreCase);
s_typeByExtension = new Dictionary<string, AssetType>(StringComparer.OrdinalIgnoreCase); s_typeByExtension = new Dictionary<string, AssetType>(StringComparer.OrdinalIgnoreCase);
s_byTypeId = new Dictionary<Guid, IAssetHandler>(); s_byTypeId = new Dictionary<Guid, IAssetHandler>();
s_versionByTypeId = new Dictionary<Guid, int>(); s_versionByTypeId = new Dictionary<Guid, int>();
s_iAssetSettingsTypes = new List<(Type Type, string Name)>();
} }
public static void RegisterHandler(IAssetHandler handler, Guid assetTypeId, ReadOnlySpan<string> extensions, int version) public static void RegisterHandler(IAssetHandler handler, Guid assetTypeId, ReadOnlySpan<string> 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) public static IAssetHandler? GetByExtension(string extension)
{ {
if (string.IsNullOrEmpty(extension)) if (string.IsNullOrEmpty(extension))
@@ -69,4 +78,9 @@ public static class AssetHandlerRegistry
var normalized = extension.StartsWith('.') ? extension : "." + extension; var normalized = extension.StartsWith('.') ? extension : "." + extension;
return s_typeByExtension.GetValueOrDefault(normalized, AssetType.Unknown); return s_typeByExtension.GetValueOrDefault(normalized, AssetType.Unknown);
} }
}
public static IReadOnlyCollection<(Type Type, string Name)> GetIAssetSettingsTypes()
{
return s_iAssetSettingsTypes;
}
}

View File

@@ -1,18 +1,16 @@
using Ghost.Engine;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace Ghost.Editor.Core.AssetHandler; namespace Ghost.Editor.Core.Assets;
/// <summary> /// <summary>
/// Mark IAssetSettings for polymorphic serialization. /// Mark IAssetSettings for polymorphic serialization.
/// Each handler type will register its own derived type. /// Each handler type will register its own derived type.
/// </summary> /// </summary>
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(DefaultAssetSettings), "Default")]
public interface IAssetSettings; public interface IAssetSettings;
public sealed class DefaultAssetSettings : IAssetSettings; internal sealed class DefaultAssetSettings : IAssetSettings;
/// <summary> /// <summary>
/// Persisted as a JSON sidecar (.gmeta) next to every source asset. /// Persisted as a JSON sidecar (.gmeta) next to every source asset.
@@ -76,9 +74,36 @@ internal static class AssetMetaIO
WriteIndented = true, WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 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<AssetMeta?> ReadAsync(string metaPath, CancellationToken token = default) public static async ValueTask<AssetMeta?> ReadAsync(string metaPath, CancellationToken token = default)
{ {
if (!File.Exists(metaPath)) if (!File.Exists(metaPath))

View File

@@ -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<MeshNode> 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<Vertex> _vertices;
private UnsafeList<uint> _indices;
public UnsafeList<Vertex> Vertices
{
get => _vertices;
set
{
_vertices.Dispose();
_vertices = value;
}
}
public UnsafeList<uint> 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<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
{
throw new NotImplementedException();
}
public ValueTask<Result> SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default)
{
throw new NotImplementedException();
}
public ValueTask<Result> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
{
throw new NotImplementedException();
}
public ValueTask<Result> ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default)
{
throw new NotImplementedException();
}
}

View File

@@ -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<Result<MeshNode>> _taskCompletionSource;
public UnsafeList<Vertex> vertices;
public UnsafeList<uint> indices;
public Task<Result<MeshNode>> Task => _taskCompletionSource.Task;
public MeshParsingWorkItem(string filePath, AllocationHandle allocationHandle, MeshAssetSettings settings)
{
_filePath = filePath;
_allocationHandle = allocationHandle;
_settings = settings;
_taskCompletionSource = new TaskCompletionSource<Result<MeshNode>>();
}
[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<MeshNode>(),
};
if (pMesh->num_faces == 0)
{
return meshNode;
}
var missingNormals = false;
var missingTangents = false;
using var flatVertices = new UnsafeList<Vertex>(1024, AllocationHandle.FreeList);
var maxScratchIndices = (int)(pMesh->max_face_triangles * 3u);
using var triIndicesArray = new UnsafeArray<uint>(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<uint>((int)numIndices, AllocationHandle.FreeList);
using var cachedIndices = new UnsafeArray<uint>((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<Vertex>((int)numUniqueVertices, _allocationHandle);
indices = new UnsafeList<uint>((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<byte>(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>(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<Vertex>(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<uint>((int)numIndices, AllocationHandle.FreeList);
using var cachedIndices = new UnsafeArray<uint>((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<Vertex>((int)numUniqueVertices, _allocationHandle);
indices = new UnsafeList<uint>((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
{
}

View File

@@ -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. // 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.Core;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Ghost.MeshOptimizer; using Ghost.MeshOptimizer;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.Mathematics; 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 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. // 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<uint> indices, float error) private static ClodBounds ComputeBounds(ref readonly ClodMesh mesh, UnsafeList<uint> indices, float error)
{ {
@@ -651,7 +654,7 @@ public static unsafe class MeshletUtility
bounds.error = Math.Max(bounds.error * config.simplifyErrorMergePrevious, error) + error * config.simplifyErrorMergeAdditive; 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); var refined = OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback);
for (var j = 0; j < groups[i].Count; j++) for (var j = 0; j < groups[i].Count; j++)
{ {
clusters[groups[i][j]].Dispose(); clusters[groups[i][j]].Dispose();
@@ -691,4 +694,129 @@ public static unsafe class MeshletUtility
return finalClusterCount; return finalClusterCount;
} }
public static void BuildMeshlets(MeshletMeshData* pMeshletData, ReadOnlyUnsafeCollection<Vertex> vertices, ReadOnlyUnsafeCollection<uint> 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<ClodCluster> clusters)
{
var pMeshletData = (MeshletMeshData*)context;
// Ensure lists are initialized
if (!pMeshletData->groups.IsCreated) pMeshletData->groups = new UnsafeList<MeshletGroup>(16, AllocationHandle.Persistent);
if (!pMeshletData->meshlets.IsCreated) pMeshletData->meshlets = new UnsafeList<Meshlet>(64, AllocationHandle.Persistent);
if (!pMeshletData->meshletVertices.IsCreated) pMeshletData->meshletVertices = new UnsafeList<uint>(128, AllocationHandle.Persistent);
if (!pMeshletData->meshletTriangles.IsCreated) pMeshletData->meshletTriangles = new UnsafeList<uint>(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.
}
} }

View File

@@ -4,7 +4,7 @@ using Ghost.DSL.ShaderCompiler;
using Ghost.Engine; using Ghost.Engine;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.AssetHandler; namespace Ghost.Editor.Core.Assets;
[Guid(GUID)] [Guid(GUID)]
public sealed partial class GraphicsShaderAsset : IAsset public sealed partial class GraphicsShaderAsset : IAsset
@@ -16,7 +16,7 @@ public sealed partial class GraphicsShaderAsset : IAsset
get; get;
} }
public IAssetSettings Settings public IAssetSettings? Settings
{ {
get; get;
} }
@@ -49,7 +49,7 @@ public sealed partial class ComputeShaderAsset : IAsset
get; get;
} }
public IAssetSettings Settings public IAssetSettings? Settings
{ {
get; get;
} }
@@ -84,13 +84,11 @@ internal class GraphicsShaderAssetHandler : IPackableAssetHandler
return null; return null;
} }
public async ValueTask<Result<IAsset>> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) public async ValueTask<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
{ {
try try
{ {
using var reader = new StreamReader(assetStream); var result = DSLShaderCompiler.CompileGraphicsShader(assetPath);
var shaderCode = await reader.ReadToEndAsync(token);
var result = DSLShaderCompiler.CompileGraphicsShader(shaderCode);
if (result.IsFailure) if (result.IsFailure)
{ {
return Result.Failure(result.Message); return Result.Failure(result.Message);
@@ -104,12 +102,12 @@ internal class GraphicsShaderAssetHandler : IPackableAssetHandler
} }
} }
public ValueTask<Result> SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default) public ValueTask<Result> SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default)
{ {
return new ValueTask<Result>(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.")); return new ValueTask<Result>(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<Result> PackAsync(FileStream assetStream, Stream targetStream, CancellationToken token = default) public ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@@ -126,13 +124,11 @@ internal class ComputeShaderAssetHandler : IPackableAssetHandler
return null; return null;
} }
public async ValueTask<Result<IAsset>> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) public async ValueTask<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
{ {
try try
{ {
using var reader = new StreamReader(assetStream); var result = DSLShaderCompiler.CompileComputeShaderCode(assetPath);
var shaderCode = await reader.ReadToEndAsync(token);
var result = DSLShaderCompiler.CompileComputeShader(shaderCode);
if (result.IsFailure) if (result.IsFailure)
{ {
return Result.Failure(result.Message); return Result.Failure(result.Message);
@@ -146,12 +142,12 @@ internal class ComputeShaderAssetHandler : IPackableAssetHandler
} }
} }
public ValueTask<Result> SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default) public ValueTask<Result> SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default)
{ {
return new ValueTask<Result>(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.")); return new ValueTask<Result>(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<Result> PackAsync(FileStream assetStream, Stream targetStream, CancellationToken token = default) public ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@@ -6,9 +6,8 @@ using Misaki.HighPerformance.LowLevel;
using System.IO.MemoryMappedFiles; using System.IO.MemoryMappedFiles;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using TerraFX.Interop.Windows;
namespace Ghost.Editor.Core.AssetHandler; namespace Ghost.Editor.Core.Assets;
public enum TextureType : uint public enum TextureType : uint
{ {
@@ -86,7 +85,7 @@ public unsafe class TextureAsset : IAsset
_textureData = data; _textureData = data;
_width = header.width; _width = header.width;
_height = header.height; _height = header.height;
_depth = header.depth; _depth = header.bpc;
_dimension = header.dimension; _dimension = header.dimension;
_colorComponents = header.colorComponents; _colorComponents = header.colorComponents;
} }
@@ -160,11 +159,6 @@ public class TextureAssetSettings : IAssetSettings
get; set; get; set;
} = 0; // 0 means generate full mipmap levels. } = 0; // 0 means generate full mipmap levels.
public bool GammaCorrection
{
get; set;
} = true;
public bool PremultiplyAlpha public bool PremultiplyAlpha
{ {
get; set; get; set;
@@ -264,6 +258,7 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
public int width; public int width;
public int height; public int height;
public int depth; public int depth;
public int bitsPerChannel;
public int colorComponents; public int colorComponents;
public bool isHDR; public bool isHDR;
} }
@@ -305,33 +300,27 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
return TextureDimension.Texture2D; return TextureDimension.Texture2D;
} }
private static unsafe Result<TextureInfo> GetImageInfo(FileStream sourceStream) private static unsafe Result<TextureInfo> 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); using var accessor = mmf.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read);
byte* ptr = null; byte* ptr = null;
try try
{ {
var ext = Path.GetExtension(sourceStream.Name); var ext = Path.GetExtension(sourcePath);
var isHDR = ext.Equals(".hdr", StringComparison.OrdinalIgnoreCase); var isHDR = ext.Equals(".hdr", StringComparison.OrdinalIgnoreCase) || settings.Basic.TextureShape == TextureShape.TextureCube;
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
int imageWidth, imageHeight, bitsPerChannel, colorComponents; int imageWidth, imageHeight, bitsPerChannel, colorComponents;
var bufferSpan = new ReadOnlySpan<byte>(ptr, (int)sourceStream.Length); var bufferSpan = new ReadOnlySpan<byte>(ptr, (int)accessor.Capacity);
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; bitsPerChannel = StbIApi.Is16BitFromMemory(bufferSpan) > 0 ? 16 : 8;
void* pPixels; void* pPixels;
if (bitsPerChannel > 8) if (isHDR || bitsPerChannel > 8)
{ {
pPixels = StbIApi.LoadfFromMemory(bufferSpan, &imageWidth, &imageHeight, &colorComponents, 4); pPixels = StbIApi.LoadfFromMemory(bufferSpan, &imageWidth, &imageHeight, &colorComponents, 4);
} }
@@ -345,8 +334,9 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
pixelData = (IntPtr)pPixels, pixelData = (IntPtr)pPixels,
width = imageWidth, width = imageWidth,
height = imageHeight, height = imageHeight,
depth = bitsPerChannel, depth = 1,
colorComponents = colorComponents, bitsPerChannel = bitsPerChannel,
colorComponents = 4, // We forced req_comp to 4
isHDR = isHDR, isHDR = isHDR,
}; };
} }
@@ -363,25 +353,25 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
} }
} }
public ValueTask<Result<IAsset>> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) public ValueTask<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
{ {
try try
{ {
var infoResult = GetImageInfo(assetStream); var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings();
var infoResult = GetImageInfo(assetPath, textureSettings);
if (infoResult.IsFailure) if (infoResult.IsFailure)
{ {
return ValueTask.FromResult(Result<IAsset>.Failure(infoResult.Message)); return ValueTask.FromResult(Result<IAsset>.Failure(infoResult.Message));
} }
var info = infoResult.Value; var info = infoResult.Value;
var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings();
var contentHeader = new TextureContentHeader var contentHeader = new TextureContentHeader
{ {
width = (uint)info.width, width = (uint)info.width,
height = (uint)info.height, height = (uint)info.height,
depth = (uint)info.depth, bpc = (uint)info.bitsPerChannel,
colorComponents = (uint)info.colorComponents, colorComponents = (uint)info.colorComponents,
dimension = (uint)GetTextureDimension(textureSettings) dimension = (uint)GetTextureDimension(textureSettings),
}; };
return ValueTask.FromResult(Result.Success<IAsset>(new TextureAsset(info.pixelData, contentHeader, id, textureSettings))); return ValueTask.FromResult(Result.Success<IAsset>(new TextureAsset(info.pixelData, contentHeader, id, textureSettings)));
@@ -393,22 +383,24 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
} }
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] [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 stream = (Stream)GCHandle.FromIntPtr((IntPtr)context).Target!;
var buffer = new ReadOnlySpan<byte>(data, size); var buffer = new ReadOnlySpan<byte>(data, size);
stream.Write(buffer); stream.Write(buffer);
} }
public async ValueTask<Result> SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default) public async ValueTask<Result> SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default)
{ {
if (asset is not TextureAsset textureAsset) if (asset is not TextureAsset textureAsset)
{ {
return Result.Failure("Asset type is not 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(() => 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); var gcHandle = GCHandle.Alloc(targetStream, GCHandleType.Normal);
try try
@@ -447,28 +439,24 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
}, token).ConfigureAwait(false); }, token).ConfigureAwait(false);
} }
public async ValueTask<Result> ImportAsync(FileStream sourceStream, FileStream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) public async ValueTask<Result> 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 try
{ {
var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings(); var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings();
var infoResult = GetImageInfo(sourcePath, textureSettings);
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) if (!infoResult.IsSuccess)
{ {
return Result.Failure(infoResult.Message); return Result.Failure(infoResult.Message);
} }
var info = infoResult.Value; var info = infoResult.Value;
var result = await TextureProcessor.CompressToCacheAsync(EditorApplication.CacheFolderPath, id, var result = await TextureProcessor.GenerateMipAndCompressAsync(EditorApplication.CacheFolderPath, id,
info, info,
textureSettings, token) textureSettings, token)
.ConfigureAwait(false); .ConfigureAwait(false);
@@ -480,13 +468,12 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
var (cachePath, mip) = result.Value; 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 var header = new TextureContentHeader
{ {
width = (uint)info.width, width = (uint)info.width,
height = (uint)info.height, height = (uint)info.height,
depth = (uint)info.depth, bpc = (uint)info.bitsPerChannel,
colorComponents = (uint)info.colorComponents, colorComponents = (uint)info.colorComponents,
mipLevels = (uint)mip, mipLevels = (uint)mip,
dimension = (uint)GetTextureDimension(textureSettings) dimension = (uint)GetTextureDimension(textureSettings)
@@ -506,12 +493,12 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
} }
} }
public ValueTask<Result> ExportAsync(FileStream assetStream, FileStream targetStream, IAssetExportOptions? options, CancellationToken token = default) public ValueTask<Result> ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default)
{ {
return ValueTask.FromResult(Result.Failure("Exporting texture assets is not supported yet.")); return ValueTask.FromResult(Result.Failure("Exporting texture assets is not supported yet."));
} }
public ValueTask<Result> PackAsync(FileStream assetStream, Stream targetStream, CancellationToken token = default) public ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@@ -1,46 +1,42 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Engine;
using Ghost.Nvtt; using Ghost.Nvtt;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel; 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.IO.Hashing;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
namespace Ghost.Editor.Core.AssetHandler; namespace Ghost.Editor.Core.Assets;
/// <summary> internal static partial class TextureProcessor
/// 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 (<c>CachesFolderPath/TextureCache/&lt;guid&gt;_&lt;hash&gt;.dds</c>).
/// 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.
/// </summary>
internal static class TextureProcessor
{ {
private class NvttPipelineTask : IThreadPoolWorkItem private class NvttPipelineTask : IThreadPoolWorkItem
{ {
private readonly string _outputPath; private readonly string _outputPath;
private readonly TextureAssetHandler.TextureInfo _textureInfo; private readonly TextureAssetHandler.TextureInfo _textureInfo;
private readonly TextureAssetSettings _settings; private readonly TextureAssetSettings _settings;
private UnsafeArray<MipLevel> _mipLevels;
private readonly TaskCompletionSource<Result<int>> _completionSource; private readonly TaskCompletionSource<Result<int>> _completionSource;
public Task<Result<int>> Task => _completionSource.Task; public Task<Result<int>> Task => _completionSource.Task;
public NvttPipelineTask(string outputPath, TextureAssetHandler.TextureInfo textureInfo, TextureAssetSettings settings) public NvttPipelineTask(string outputPath, TextureAssetHandler.TextureInfo textureInfo, TextureAssetSettings settings, UnsafeArray<MipLevel> mipLevels)
{ {
_outputPath = outputPath; _outputPath = outputPath;
_textureInfo = textureInfo; _textureInfo = textureInfo;
_settings = settings; _settings = settings;
_mipLevels = mipLevels;
_completionSource = new TaskCompletionSource<Result<int>>(); _completionSource = new TaskCompletionSource<Result<int>>();
} }
public unsafe void Execute() private unsafe Result<int> RunMipGenCompressionPipeline()
{ {
using var pSurface = new DisposablePtr<NvttSurface>(NvttSurface.Create()); using var pSurface = new DisposablePtr<NvttSurface>(NvttSurface.Create());
using var pCompOpts = new DisposablePtr<NvttCompressionOptions>(NvttCompressionOptions.Create()); using var pCompOpts = new DisposablePtr<NvttCompressionOptions>(NvttCompressionOptions.Create());
@@ -49,20 +45,24 @@ internal static class TextureProcessor
var inputFormat = _textureInfo.colorComponents == 1 var inputFormat = _textureInfo.colorComponents == 1
? NvttInputFormat.NVTT_InputFormat_R_32F ? NvttInputFormat.NVTT_InputFormat_R_32F
: _textureInfo.depth > 8 : _textureInfo.bitsPerChannel > 8
? NvttInputFormat.NVTT_InputFormat_RGBA_32F ? NvttInputFormat.NVTT_InputFormat_RGBA_32F
: NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below : NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below
var needUnsigned = _settings.Basic.TextureType == TextureType.Normal ? NvttBoolean.NVTT_True : NvttBoolean.NVTT_False; var isNormal = _settings.Basic.TextureType == TextureType.Normal;
if (pSurface.Get()->SetImageData(inputFormat, _textureInfo.width, _textureInfo.height, _textureInfo.depth, (void*)_textureInfo.pixelData, needUnsigned, null)) 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 Result.Failure<int>("Failed to set image data for NVTT compression.");
return; }
if (isNormal)
{
pSurface.Get()->SetNormalMap(true);
} }
// stb gives us RGBA byte order; NVTT BGRA_8UB reads it as BGRA, // 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). // 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); pSurface.Get()->Swizzle(2, 1, 0, 3, null);
} }
@@ -91,7 +91,7 @@ internal static class TextureProcessor
pSurface.Get()->SetBorder(0f, 0f, 0f, 0f, null); pSurface.Get()->SetBorder(0f, 0f, 0f, 0f, null);
} }
if (_settings.Basic.IsSRGB && _settings.Advanced.GammaCorrection) if (_settings.Basic.IsSRGB)
{ {
pSurface.Get()->ToLinearFromSrgb(null); pSurface.Get()->ToLinearFromSrgb(null);
} }
@@ -136,6 +136,10 @@ internal static class TextureProcessor
pCtx.Get()->OutputHeader(pSurface.Get(), mipmapCount, pCompOpts.Get(), pOutOpts.Get()); pCtx.Get()->OutputHeader(pSurface.Get(), mipmapCount, pCompOpts.Get(), pOutOpts.Get());
using var pMip = new DisposablePtr<NvttSurface>(pSurface.Get()->Clone()); using var pMip = new DisposablePtr<NvttSurface>(pSurface.Get()->Clone());
if (pMip.Get() == null)
{
return Result.Failure("Failed to clone surface for mipmap generation.");
}
for (var level = 0; level < mipmapCount; level++) for (var level = 0; level < mipmapCount; level++)
{ {
@@ -148,19 +152,110 @@ internal static class TextureProcessor
_settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3, null); _settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3, null);
} }
pCtx.Get()->Compress(pMip.Get(), 0, level, pCompOpts.Get(), pOutOpts.Get()); using var compressMip = new DisposablePtr<NvttSurface>(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) 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<int> RunCubeMapCompressionPipeline()
{
using var pCompOpts = new DisposablePtr<NvttCompressionOptions>(NvttCompressionOptions.Create());
using var pOutOpts = new DisposablePtr<NvttOutputOptions>(NvttOutputOptions.Create());
using var pCtx = new DisposablePtr<NvttContext>(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>(NvttCubeSurface.Create()))
using (var mip0Surf = new DisposablePtr<NvttSurface>(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<int>("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>(NvttCubeSurface.Create());
using var mipSurf = new DisposablePtr<NvttSurface>(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<int> 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<Result<(string cachePath, int mipmapCount)>> CompressToCacheAsync(string cachesFolderPath, Guid assetId, public static async ValueTask<Result<(string cachePath, int mipmapCount)>> GenerateMipAndCompressAsync(string cachesFolderPath, Guid assetId,
TextureAssetHandler.TextureInfo textureInfo, TextureAssetHandler.TextureInfo textureInfo,
TextureAssetSettings settings, CancellationToken cancellationToken) TextureAssetSettings settings, CancellationToken cancellationToken)
{ {
@@ -174,46 +269,79 @@ internal static class TextureProcessor
if (File.Exists(cachePath)) if (File.Exists(cachePath))
{ {
using var fs = new FileStream(cachePath, FileMode.Open, FileAccess.Read); var isValid = false;
using var reader = new BinaryReader(fs); var mipMapCount = 1u;
if (reader.ReadUInt32() != 0x20534444) 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); File.Delete(cachePath);
goto ScheduleWork;
} }
catch
// 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)
{ {
return (cachePath, 1); // Ignore deletion errors, maybe file is still locked or we have no permission.
// The pipeline will overwrite it.
}
}
UnsafeArray<MipLevel> mipLevels = default;
var scheduler = EditorApplication.GetService<EngineCore>().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: return (cachePath, result.Value);
var workItem = new NvttPipelineTask(cachePath, textureInfo, settings); }
ThreadPool.UnsafeQueueUserWorkItem(workItem, true); finally
var result = await workItem.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
if (result.IsFailure)
{ {
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) private static NvttFormat SelectFormat(TextureAssetSettings settings, bool isHDR)

View File

@@ -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<float> data;
public int width;
public int height;
public int offset;
public float roughness;
}
private unsafe struct GGXMipGenerationJobSPMD<TFloat, TInt> : IJobParallelFor
where TFloat : unmanaged, ISPMDLane<TFloat, float>
where TInt : unmanaged, ISPMDLane<TInt, int>
{
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<TFloat, float> Hammersley(TFloat i, int N, float* lut)
{
var x = i / N;
var y = TFloat.Load(lut + (int)i[0]);
return MathV.Create<TFloat, float>(x, y);
}
// GGX Importance Sampling
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector3<TFloat, float> ImportanceSampleGGX(Vector2<TFloat, float> Xi, Vector3<TFloat, float> 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<TFloat, float>(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<TFloat, float>(0.0f, 0.0f, 1.0f), MathV.Create<TFloat, float>(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<TFloat, float> DirToEquirectangularUV(Vector3<TFloat, float> 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<TFloat, float>(u, v);
}
// Samples the source HDR image using bilinear interpolation (simplified to nearest neighbor for brevity here)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector3<TFloat, float> SampleEquirectangularMap(float* img, int w, int h, int c, Vector3<TFloat, float> dir)
{
var uv = DirToEquirectangularUV(dir);
// Nearest neighbor pixel coordinates
var px = (uv.x * (w - 1.0f)).Cast<TInt, int>();
var py = (uv.y * (h - 1.0f)).Cast<TInt, int>();
// 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<TFloat, float>(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<MipLevel>(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, float>(
TFloat.Create(N.x),
TFloat.Create(N.y),
TFloat.Create(N.z)
);
var vV = MathV.Create<TFloat, float>(
TFloat.Create(V.x),
TFloat.Create(V.y),
TFloat.Create(V.z)
);
var vPrefilteredColor = Vector3<TFloat, float>.Zero;
var vTotalWeight = TFloat.Zero;
// Monte Carlo Integration Loop
var vLuma = MathV.Create<TFloat, float>(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<float> radicalInverse_VdCLut;
public void Execute(ref readonly JobExecutionContext ctx)
{
radicalInverse_VdCLut.Dispose();
}
}
private struct MipLevelDisposeJob : IJob
{
public UnsafeArray<MipLevel> 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<MipLevel> 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<MipLevel>(totalMipLevels, AllocationHandle.FreeList);
var radicalInverse_VdCLut = new UnsafeArray<float>(_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<float>(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<float>, WideLane<int>>
{
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<float>, ScalarLane<int>>
{
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;
}
}

View File

@@ -1,5 +1,5 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Editor.Core.AssetHandler; using Ghost.Editor.Core.Assets;
using Ghost.Editor.Core.Services; using Ghost.Editor.Core.Services;
using Ghost.Engine.AssetLoader; using Ghost.Engine.AssetLoader;

View File

@@ -1,4 +1,4 @@
using Ghost.Editor.Core.AssetHandler; using Ghost.Editor.Core.Assets;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
namespace Ghost.Editor.Core.Services; namespace Ghost.Editor.Core.Services;
@@ -17,9 +17,12 @@ public sealed partial class AssetCatalog : IDisposable
private readonly SqliteCommand _cmdGetPath; private readonly SqliteCommand _cmdGetPath;
private readonly SqliteCommand _cmdUpsert; private readonly SqliteCommand _cmdUpsert;
private readonly SqliteCommand _cmdDelete; private readonly SqliteCommand _cmdDelete;
private readonly SqliteCommand _cmdGetHandlerTypeId; private readonly SqliteCommand _cmdGetHandlerTypeId;
private readonly SqliteCommand _cmdGetReferencers; private readonly SqliteCommand _cmdGetReferencers;
private readonly SqliteCommand _cmdGetDependencies; private readonly SqliteCommand _cmdGetDependencies;
private readonly SqliteCommand _cmdGetImportedAt;
private readonly SqliteCommand _cmdInsertDep; private readonly SqliteCommand _cmdInsertDep;
private readonly SqliteCommand _cmdClearDeps; private readonly SqliteCommand _cmdClearDeps;
private readonly SqliteCommand _cmdEnumerate; private readonly SqliteCommand _cmdEnumerate;
@@ -48,16 +51,22 @@ public sealed partial class AssetCatalog : IDisposable
_cmdGetGuid = CreateCommand("SELECT guid FROM assets WHERE source_path = @path"); _cmdGetGuid = CreateCommand("SELECT guid FROM assets WHERE source_path = @path");
_cmdGetPath = CreateCommand("SELECT source_path FROM assets WHERE guid = @guid"); _cmdGetPath = CreateCommand("SELECT source_path FROM assets WHERE guid = @guid");
_cmdGetHandlerTypeId = CreateCommand("SELECT handler_type_id 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(@" _cmdUpsert = CreateCommand(@"
INSERT INTO assets (guid, source_path, handler_type_id, handler_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) VALUES (@guid, @path, @handler_id, @version, @content_hash, @settings_hash, @imported_at_ms)
ON CONFLICT(guid) DO UPDATE SET ON CONFLICT(guid) DO UPDATE SET
source_path = excluded.source_path, source_path = excluded.source_path,
handler_type_id = excluded.handler_type_id, handler_type_id = excluded.handler_type_id,
handler_version = excluded.handler_version"); handler_version = excluded.handler_version,
content_hash = excluded.content_hash,
settings_hash = excluded.settings_hash,
imported_at_ms = excluded.imported_at_ms");
_cmdDelete = CreateCommand("DELETE FROM assets WHERE guid = @guid"); _cmdDelete = CreateCommand("DELETE FROM assets WHERE guid = @guid");
_cmdGetReferencers = CreateCommand("SELECT from_guid FROM dependencies WHERE to_guid = @guid"); _cmdGetReferencers = CreateCommand("SELECT from_guid FROM dependencies WHERE to_guid = @guid");
_cmdGetDependencies = CreateCommand("SELECT to_guid FROM dependencies WHERE from_guid = @guid"); _cmdGetDependencies = CreateCommand("SELECT to_guid FROM dependencies WHERE from_guid = @guid");
_cmdInsertDep = CreateCommand("INSERT INTO dependencies (from_guid, to_guid) VALUES (@from, @to)"); _cmdInsertDep = CreateCommand("INSERT INTO dependencies (from_guid, to_guid) VALUES (@from, @to)");
_cmdClearDeps = CreateCommand("DELETE FROM dependencies WHERE from_guid = @guid"); _cmdClearDeps = CreateCommand("DELETE FROM dependencies WHERE from_guid = @guid");
_cmdEnumerate = CreateCommand("SELECT guid, source_path FROM assets"); _cmdEnumerate = CreateCommand("SELECT guid, source_path FROM assets");
@@ -135,6 +144,9 @@ public sealed partial class AssetCatalog : IDisposable
_cmdUpsert.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath)); _cmdUpsert.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath));
_cmdUpsert.Parameters.AddWithValue("@handler_id", meta.HandlerTypeId?.ToByteArray() ?? (object)DBNull.Value); _cmdUpsert.Parameters.AddWithValue("@handler_id", meta.HandlerTypeId?.ToByteArray() ?? (object)DBNull.Value);
_cmdUpsert.Parameters.AddWithValue("@version", meta.HandlerVersion); _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(); _cmdUpsert.ExecuteNonQuery();
} }
} }
@@ -157,6 +169,20 @@ public sealed partial class AssetCatalog : IDisposable
return result is byte[] bytes ? new Guid(bytes) : Guid.Empty; 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<Guid> dependencies) public void SetDependencies(Guid assetId, ReadOnlySpan<Guid> dependencies)
{ {
lock (_writeLock) lock (_writeLock)

View File

@@ -1,9 +1,8 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Core.Utilities; using Ghost.Core.Utilities;
using Ghost.Editor.Core.AssetHandler; using Ghost.Editor.Core.Assets;
using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Contracts;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.IO.MemoryMappedFiles;
namespace Ghost.Editor.Core.Services; namespace Ghost.Editor.Core.Services;
@@ -21,6 +20,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
private readonly ConcurrentDictionary<string, bool> _ignoreMetaWrites; private readonly ConcurrentDictionary<string, bool> _ignoreMetaWrites;
private readonly ConcurrentHashSet<Guid> _dirtyAssets; private readonly ConcurrentHashSet<Guid> _dirtyAssets;
private readonly ConcurrentDictionary<string, CancellationTokenSource> _eventDebouncers;
public event EventHandler<AssetChangedEventArgs>? OnAssetChanged; public event EventHandler<AssetChangedEventArgs>? OnAssetChanged;
public event EventHandler<Guid>? OnAssetImported public event EventHandler<Guid>? OnAssetImported
@@ -41,6 +41,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
_ignoreMetaWrites = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase); _ignoreMetaWrites = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
_dirtyAssets = new ConcurrentHashSet<Guid>(); _dirtyAssets = new ConcurrentHashSet<Guid>();
_eventDebouncers = new ConcurrentDictionary<string, CancellationTokenSource>();
SyncCatalogWithDisk(); SyncCatalogWithDisk();
@@ -48,7 +49,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
{ {
IncludeSubdirectories = true, IncludeSubdirectories = true,
EnableRaisingEvents = true, EnableRaisingEvents = true,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.DirectoryName NotifyFilter = NotifyFilters.LastWrite
}; };
_watcher.Created += OnFileSystemEvent; _watcher.Created += OnFileSystemEvent;
@@ -72,7 +73,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
var meta = AssetMetaIO.ReadAsync(metaPath).AsTask().Result; var meta = AssetMetaIO.ReadAsync(metaPath).AsTask().Result;
if (meta != null) 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); _catalog.Upsert(meta, sourceRelative);
foundGuids.Add(meta.Guid); foundGuids.Add(meta.Guid);
} }
@@ -90,57 +91,86 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
private async void OnFileSystemEvent(object sender, FileSystemEventArgs e) private async void OnFileSystemEvent(object sender, FileSystemEventArgs e)
{ {
var ext = Path.GetExtension(e.FullPath); 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") if (ext is ".tmp" or ".gtemp")
{ {
return; 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 (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) if (meta != null)
{ {
_catalog.Upsert(meta, AssetMetaIO.GetSourcePath(relativePath)); _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; return;
} }
var changeType = AssetChangeType.None; var changeType = AssetChangeType.None;
if (e.ChangeType == WatcherChangeTypes.Created) var guid = _catalog.GetGuid(relativePath);
if (!fileExists)
{ {
await HandleNewSourceFileAsync(relativePath); // The file is no longer on disk. Wait safely completed.
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);
if (guid != Guid.Empty) if (guid != Guid.Empty)
{ {
_catalog.Remove(guid); _catalog.Remove(guid);
changeType = AssetChangeType.Deleted; 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) if (changeType != AssetChangeType.None)
{ {
@@ -150,8 +180,8 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
private void OnFileSystemRenameEvent(object sender, RenamedEventArgs e) private void OnFileSystemRenameEvent(object sender, RenamedEventArgs e)
{ {
var oldRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.OldFullPath); var oldRelative = Path.GetRelativePath(EditorApplication.ProjectPath, e.OldFullPath);
var newRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.FullPath); var newRelative = Path.GetRelativePath(EditorApplication.ProjectPath, e.FullPath);
var guid = _catalog.GetGuid(oldRelative); var guid = _catalog.GetGuid(oldRelative);
if (guid != Guid.Empty) if (guid != Guid.Empty)
@@ -276,8 +306,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
return Result.Failure("Meta file does not exist."); 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(path, id, meta.Settings, token);
return await handler.LoadAssetAsync(stream, id, meta.Settings, token);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -305,9 +334,8 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
return Result.Failure("No Avaliable handler type."); 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. // 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) catch (Exception ex)
{ {

View File

@@ -1,5 +1,5 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Editor.Core.AssetHandler; using Ghost.Editor.Core.Assets;
using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Contracts;
using Ghost.Engine; using Ghost.Engine;

View File

@@ -1,5 +1,5 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Editor.Core.AssetHandler; using Ghost.Editor.Core.Assets;
using System.IO.Hashing; using System.IO.Hashing;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
@@ -79,13 +79,14 @@ internal sealed partial class ImportCoordinator : IDisposable
var fileName = $"{assetGuid:N}{IMPORTED_EXTENSION}"; var fileName = $"{assetGuid:N}{IMPORTED_EXTENSION}";
var folderName = fileName.Substring(0, 2); var folderName = fileName.Substring(0, 2);
var finalPath = Path.Combine(EditorApplication.LibraryImportsFolderPath, folderName, fileName); var importsFolder = Path.Combine(EditorApplication.LibraryImportsFolderPath, folderName);
Directory.CreateDirectory(finalPath); var finalPath = Path.Combine(importsFolder, fileName);
Directory.CreateDirectory(importsFolder);
return finalPath; 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); var meta = await AssetMetaIO.ReadAsync(job.MetaPath, token);
if (meta is null) if (meta is null)
@@ -114,11 +115,7 @@ internal sealed partial class ImportCoordinator : IDisposable
if (handler is IImportableAssetHandler importable) if (handler is IImportableAssetHandler importable)
{ {
var targetPath = GetImportedAssetPath(job.AssetGuid); var targetPath = GetImportedAssetPath(job.AssetGuid);
importResult = await importable.ImportAsync(job.SourcePath, targetPath, job.AssetGuid, meta.Settings, token);
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);
} }
if (importResult.IsSuccess) if (importResult.IsSuccess)
@@ -129,6 +126,8 @@ internal sealed partial class ImportCoordinator : IDisposable
meta.LastImportedUtc = DateTime.UtcNow; meta.LastImportedUtc = DateTime.UtcNow;
await AssetMetaIO.WriteAsync(job.MetaPath, meta, token); await AssetMetaIO.WriteAsync(job.MetaPath, meta, token);
OnImportCompleted?.Invoke(null, job.AssetGuid);
} }
else else
{ {

View File

@@ -66,13 +66,13 @@ internal static class ActivationHandler
AllocationManager.Initialize(opts); AllocationManager.Initialize(opts);
var assetRegistry = App.GetService<IAssetRegistry>(); //var assetRegistry = App.GetService<IAssetRegistry>();
var engineCore = App.GetService<EngineCore>(); //var engineCore = App.GetService<EngineCore>();
assetRegistry.OnAssetImported += (sender, e) => //assetRegistry.OnAssetImported += (sender, e) =>
{ //{
engineCore.AssetManager.ReimportAsset(e); // engineCore.AssetManager.ReimportAsset(e);
}; //};
return ValueTask.CompletedTask; return ValueTask.CompletedTask;
} }

View File

@@ -43,7 +43,7 @@ internal partial class ExplorerItem : ObservableObject
public ExplorerItem(string name, string path, bool isDirectory, AssetType assetType = AssetType.Unknown) public ExplorerItem(string name, string path, bool isDirectory, AssetType assetType = AssetType.Unknown)
{ {
Name = name; Name = name;
Path = PathUtility.Normalize(path); Path = path;
IsDirectory = isDirectory; IsDirectory = isDirectory;
AssetType = assetType; AssetType = assetType;

View File

@@ -62,13 +62,13 @@ internal partial class ContentBrowserViewModel : ObservableObject
private void OnAssetChanged(object? sender, AssetChangedEventArgs e) 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; return;
} }
var fullPath = PathUtility.Normalize(Path.Combine(EditorApplication.AssetsFolderPath, e.AssetPath)); var fullPath = PathUtility.Normalize(e.AssetPath);
var dirPath = PathUtility.Normalize(Path.GetDirectoryName(fullPath)); var dirPath = Path.GetDirectoryName(fullPath);
if (string.Equals(dirPath, CurrentDirectoryPath, StringComparison.OrdinalIgnoreCase)) if (string.Equals(dirPath, CurrentDirectoryPath, StringComparison.OrdinalIgnoreCase))
{ {
@@ -78,7 +78,7 @@ internal partial class ContentBrowserViewModel : ObservableObject
{ {
if (e.ChangeType == AssetChangeType.Renamed && e.OldAssetPath != null) 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)); var oldItem = Files.FirstOrDefault(f => string.Equals(f.Path, oldFullPath, StringComparison.OrdinalIgnoreCase));
if (oldItem != null) Files.Remove(oldItem); if (oldItem != null) Files.Remove(oldItem);
} }
@@ -138,7 +138,7 @@ internal partial class ContentBrowserViewModel : ObservableObject
foreach (var file in Directory.EnumerateFiles(path)) foreach (var file in Directory.EnumerateFiles(path))
{ {
if (Path.GetExtension(file) == FileExtensions.META_FILE_EXTENSION) if (file.EndsWith(FileExtensions.META_FILE_EXTENSION))
{ {
continue; continue;
} }

View File

@@ -26,6 +26,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.3.3" /> <PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.3.3" />
<PackageReference Include="Misaki.HighPerformance.Mathematics.SPMD" Version="1.3.0" />
<PackageReference Include="System.IO.Hashing" Version="10.0.7" /> <PackageReference Include="System.IO.Hashing" Version="10.0.7" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" /> <PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" />
</ItemGroup> </ItemGroup>

View File

@@ -12,7 +12,7 @@ public struct TextureContentHeader
{ {
public uint width; public uint width;
public uint height; public uint height;
public uint depth; public uint bpc;
public uint mipLevels; public uint mipLevels;
public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D
public uint colorComponents; 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 return colorComponents switch
{ {
1 => depth switch 1 => bpc switch
{ {
8 => TextureFormat.R8_UNorm, 8 => TextureFormat.R8_UNorm,
16 => TextureFormat.R16_UNorm, 16 => TextureFormat.R16_UNorm,
32 => TextureFormat.R32_UInt, 32 => TextureFormat.R32_UInt,
_ => TextureFormat.Unknown, _ => TextureFormat.Unknown,
}, },
2 => depth switch 2 => bpc switch
{ {
8 => TextureFormat.R8G8_UNorm, 8 => TextureFormat.R8G8_UNorm,
16 => TextureFormat.R16G16_UNorm, 16 => TextureFormat.R16G16_UNorm,
32 => TextureFormat.R32G32_Float, 32 => TextureFormat.R32G32_Float,
_ => TextureFormat.Unknown, _ => TextureFormat.Unknown,
}, },
3 or 4 => depth switch 3 or 4 => bpc switch
{ {
8 => TextureFormat.R8G8B8A8_UNorm, 8 => TextureFormat.R8G8B8A8_UNorm,
16 => TextureFormat.R16G16B16A16_Float, 16 => TextureFormat.R16G16B16A16_Float,
@@ -91,7 +91,7 @@ internal partial class AssetEntry
Height = header.height, Height = header.height,
MipLevels = header.mipLevels, MipLevels = header.mipLevels,
Slice = 1, Slice = 1,
Format = GetTextureFormat(header.depth, header.colorComponents), Format = GetTextureFormat(header.bpc, header.colorComponents),
Dimension = (TextureDimension)header.dimension, Dimension = (TextureDimension)header.dimension,
Usage = TextureUsage.ShaderResource, Usage = TextureUsage.ShaderResource,
}; };

View File

@@ -88,3 +88,73 @@ internal static partial class {registerTypeName}
return null; 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<INamedTypeSymbol> 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 = $@"// <auto-generated />
[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;
}
}

View File

@@ -6,7 +6,6 @@ using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Mathematics.Geometry; using Misaki.HighPerformance.Mathematics.Geometry;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ghost.Graphics.Core; namespace Ghost.Graphics.Core;
@@ -203,158 +202,6 @@ public struct Mesh : IResourceReleasable
_meshletData.Dispose(); _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<ClodCluster> clusters)
{
var mesh = (Mesh*)context;
ref var data = ref mesh->_meshletData;
// Ensure lists are initialized
if (!data.groups.IsCreated) data.groups = new UnsafeList<MeshletGroup>(16, AllocationHandle.Persistent);
if (!data.meshlets.IsCreated) data.meshlets = new UnsafeList<Meshlet>(64, AllocationHandle.Persistent);
if (!data.meshletVertices.IsCreated) data.meshletVertices = new UnsafeList<uint>(128, AllocationHandle.Persistent);
if (!data.meshletTriangles.IsCreated) data.meshletTriangles = new UnsafeList<uint>(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) public void ReleaseResource(IResourceDatabase database)
{ {
ReleaseCpuResources(); ReleaseCpuResources();

View File

@@ -130,7 +130,7 @@ public readonly unsafe ref struct RenderContext
if (staticMesh) if (staticMesh)
{ {
meshData.CookMeshlets(); //meshData.CookMeshlets();
UploadMeshlets(mesh); UploadMeshlets(mesh);
meshData.ReleaseCpuResources(); meshData.ReleaseCpuResources();
} }

View File

@@ -1,4 +1,4 @@
using Ghost.MicroTest; using Ghost.MicroTest;
using Ghost.Test.Core; using Ghost.Test.Core;
TestRunner.Run<StbIBindingTest>(); TestRunner.Run<NvttBindingTest>();

View File

@@ -7,7 +7,7 @@ using System.Numerics;
//return; //return;
#if true #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) if (result.IsFailure)
{ {
Console.WriteLine(result.Message); Console.WriteLine(result.Message);