diff --git a/src/Editor/Ghost.Editor.Core/Assets/MeshAssetHandler.cs b/src/Editor/Ghost.Editor.Core/Assets/MeshAssetHandler.cs index cab823f..46ab991 100644 --- a/src/Editor/Ghost.Editor.Core/Assets/MeshAssetHandler.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/MeshAssetHandler.cs @@ -211,11 +211,6 @@ public class MeshAssetSettings : IAssetSettings { get; set; } = VertexDataSource.ComputedIfMissing; - - public bool BuildMeshlets - { - get; set; - } = true; } internal class ObjAssetSettings : MeshAssetSettings diff --git a/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs b/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs index 1d701a4..5a072a9 100644 --- a/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs @@ -1,8 +1,6 @@ // Source: https://github.com/zeux/meshoptimizer/blob/master/demo/clusterlod.h // Translated from C++ to C#. -// TODO: This file should be moved to editor project since there is no reason we need to build meshlets and LOD at runtime. - using Ghost.Core; using Ghost.Graphics.Core; using Ghost.Graphics.RHI; @@ -165,8 +163,6 @@ public unsafe struct ClodCluster /// public unsafe delegate int ClodOutputDelegate(void* context, ClodGroup group, ReadOnlyUnsafeCollection clusters); -// FIX: UnsafeList and UnsafeArray are not same as std::vector. - public static unsafe partial class MeshProcessor { private static ClodBounds ComputeBounds(ref readonly ClodMesh mesh, UnsafeList indices, float error) diff --git a/src/Editor/Ghost.Editor.Core/Assets/TextureAssetHandler.cs b/src/Editor/Ghost.Editor.Core/Assets/TextureAssetHandler.cs index c484072..97546c6 100644 --- a/src/Editor/Ghost.Editor.Core/Assets/TextureAssetHandler.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/TextureAssetHandler.cs @@ -406,17 +406,18 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand try { var ext = Path.GetExtension(targetStream.Name); + var result = 0; unsafe { switch (ext) { case ".png": - StbIApi.WritePngToFunc(&WriteCallback, (void*)GCHandle.ToIntPtr(gcHandle), (int)textureAsset.Width, (int)textureAsset.Height, (int)textureAsset.ColorComponents, (void*)textureAsset.TextureData, 0); + result = StbIApi.WritePngToFunc(&WriteCallback, (void*)GCHandle.ToIntPtr(gcHandle), (int)textureAsset.Width, (int)textureAsset.Height, (int)textureAsset.ColorComponents, (void*)textureAsset.TextureData, 0); break; case ".jpg": - StbIApi.WriteJpgToFunc(&WriteCallback, (void*)GCHandle.ToIntPtr(gcHandle), (int)textureAsset.Width, (int)textureAsset.Height, (int)textureAsset.ColorComponents, (void*)textureAsset.TextureData, 90); + result = StbIApi.WriteJpgToFunc(&WriteCallback, (void*)GCHandle.ToIntPtr(gcHandle), (int)textureAsset.Width, (int)textureAsset.Height, (int)textureAsset.ColorComponents, (void*)textureAsset.TextureData, 90); break; // TODO: Add support for other image formats @@ -426,7 +427,7 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand } } - return Result.Success(); + return result != 0 ? Result.Success() : Result.Failure("Failed to write image data."); } catch (Exception ex) { diff --git a/src/Runtime/Ghost.Engine/Components/GPUInstanceRef.cs b/src/Runtime/Ghost.Engine/Components/GPUInstanceRef.cs index 10bfab1..cfdac2d 100644 --- a/src/Runtime/Ghost.Engine/Components/GPUInstanceRef.cs +++ b/src/Runtime/Ghost.Engine/Components/GPUInstanceRef.cs @@ -1,8 +1,11 @@ +using Ghost.Core; using Ghost.Entities; +using Ghost.Graphics.Services; namespace Ghost.Engine.Components; public struct GPUInstanceRef : IComponent { - public uint gpuSceneIndex; + public uint gpuInstanceIndex; + public Identifier materialPalette; } diff --git a/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.UpdateGPUScene.cs b/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.UpdateGPUScene.cs index be1feb4..78fdc8a 100644 --- a/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.UpdateGPUScene.cs +++ b/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.UpdateGPUScene.cs @@ -27,7 +27,7 @@ internal partial class GhostRenderPipeline public float4x4 localToWorld; public uint instanceID; public uint meshBuffer; - public uint materialPalette; + public uint materialPaletteIndex; public uint renderingLayerMask; public uint shadowCastingMode; } @@ -71,7 +71,7 @@ internal partial class GhostRenderPipeline localToWorld = addRequest.localToWorld, instanceID = addRequest.instanceId, meshBuffer = resourceDatabase.GetBindlessIndex(mesh.Get().MeshDataBuffer.AsResource()), - materialPalette = (uint)addRequest.meshInstance.materialPalette.Value, + materialPaletteIndex = (uint)addRequest.meshInstance.materialPalette.Value, renderingLayerMask = addRequest.meshInstance.renderingLayerMask, shadowCastingMode = (uint)addRequest.meshInstance.shadowCastingMode }; diff --git a/src/Runtime/Ghost.Engine/Systems/AddGPUInstanceSystem.cs b/src/Runtime/Ghost.Engine/Systems/AddGPUInstanceSystem.cs index 4a8cdb2..257ccf0 100644 --- a/src/Runtime/Ghost.Engine/Systems/AddGPUInstanceSystem.cs +++ b/src/Runtime/Ghost.Engine/Systems/AddGPUInstanceSystem.cs @@ -46,7 +46,9 @@ internal class AddGPUInstanceSystem : SystemBase var entity = entities.GetElementUnsafe(i); var index = payload.AddInstance(localToWorld.matrix, in meshInstance); - systemAPI.World.EntityCommandBuffer.AddComponent(entity, new GPUInstanceRef { gpuSceneIndex = index }); + var materialPalette = meshInstance.materialPalette; + + systemAPI.World.EntityCommandBuffer.AddComponent(entity, new GPUInstanceRef { gpuInstanceIndex = index, materialPalette = materialPalette }); } } diff --git a/src/Runtime/Ghost.Engine/Systems/RemoveGPUInstanceSystem.cs b/src/Runtime/Ghost.Engine/Systems/RemoveGPUInstanceSystem.cs index 57b21e4..0e08683 100644 --- a/src/Runtime/Ghost.Engine/Systems/RemoveGPUInstanceSystem.cs +++ b/src/Runtime/Ghost.Engine/Systems/RemoveGPUInstanceSystem.cs @@ -43,7 +43,9 @@ internal class RemoveGPUInstanceSystem : SystemBase var gpuInstance = gpuInstanceRefs.GetElementUnsafe(i); var entity = entities.GetElementUnsafe(i); - payload.RemoveInstance(gpuInstance.gpuSceneIndex); + payload.RemoveInstance(gpuInstance.gpuInstanceIndex); + _renderSystem.ResourceManager.ReleaseMaterialPalette(gpuInstance.materialPalette); + systemAPI.World.EntityCommandBuffer.RemoveComponent(entity); } } diff --git a/src/Runtime/Ghost.Engine/Systems/UpdateGPUInstanceSystem.cs b/src/Runtime/Ghost.Engine/Systems/UpdateGPUInstanceSystem.cs index cbf57fe..24ab086 100644 --- a/src/Runtime/Ghost.Engine/Systems/UpdateGPUInstanceSystem.cs +++ b/src/Runtime/Ghost.Engine/Systems/UpdateGPUInstanceSystem.cs @@ -49,7 +49,7 @@ internal class UpdateGPUInstanceSystem : SystemBase ref readonly var mesh = ref meshs.GetElementUnsafe(i); ref readonly var instance = ref gpuInstances.GetElementUnsafe(i); - playload.UpdateInstance(instance.gpuSceneIndex, ltw.matrix, in mesh); + playload.UpdateInstance(instance.gpuInstanceIndex, ltw.matrix, in mesh); } } } diff --git a/src/Runtime/Ghost.Graphics.RHI/RootSignatureLayout.cs b/src/Runtime/Ghost.Graphics.RHI/RootSignatureLayout.cs index caadfb9..accc468 100644 --- a/src/Runtime/Ghost.Graphics.RHI/RootSignatureLayout.cs +++ b/src/Runtime/Ghost.Graphics.RHI/RootSignatureLayout.cs @@ -35,6 +35,8 @@ public struct FrameData { public uint instanceBuffer; public uint userBuffer; + public uint paletteOffsetBuffer; // bindless index into PaletteOffsetBuffer + public uint materialIndexBuffer; // bindless index into MaterialIndexBuffer } [StructLayout(LayoutKind.Sequential, Pack = 4)] @@ -42,7 +44,7 @@ public struct InstanceData { public float4x4 localToWorld; public uint meshBuffer; - public uint materialBuffer; + public uint materialPaletteIndex; // index into PaletteOffsetBuffer (from MaterialPaletteStore) } [StructLayout(LayoutKind.Sequential, Pack = 4)] @@ -68,4 +70,5 @@ public struct MeshData public uint meshletBuffer; public uint meshletVerticesBuffer; public uint meshletTrianglesBuffer; + public uint materialSlotCount; // number of material slots baked into this mesh's meshlets }; diff --git a/src/Runtime/Ghost.Graphics/Core/RenderContext.cs b/src/Runtime/Ghost.Graphics/Core/RenderContext.cs index 213365f..e639ca0 100644 --- a/src/Runtime/Ghost.Graphics/Core/RenderContext.cs +++ b/src/Runtime/Ghost.Graphics/Core/RenderContext.cs @@ -106,6 +106,43 @@ public readonly unsafe ref struct RenderContext } } + /// + /// Uploads a sub-range of data into an existing GPU buffer at the specified byte offset. + /// Used for incremental uploads (e.g. dirty palette ranges). + /// + public void UploadBufferRange(Handle buffer, ReadOnlySpan data, uint byteOffset) + where T : unmanaged + { + if (data.IsEmpty) + { + return; + } + + var sizeInBytes = (nuint)(data.Length * sizeof(T)); + + var uploadDesc = new BufferDesc + { + Size = sizeInBytes, + Usage = BufferUsage.Upload, + HeapType = HeapType.Upload, + }; + + var uploadHandle = ResourceManager.CreateTransientBuffer(in uploadDesc); + if (uploadHandle.IsInvalid) + { + throw new OutOfMemoryException("Failed to create upload buffer for range upload."); + } + + fixed (T* pData = data) + { + var mappedData = ResourceDatabase.MapResource(uploadHandle.AsResource(), 0, null); + MemoryUtility.MemCpy(mappedData, pData, sizeInBytes); + ResourceDatabase.UnmapResource(uploadHandle.AsResource(), 0, null); + } + + CommandBuffer.CopyBuffer(buffer, uploadHandle, byteOffset, 0, sizeInBytes); + } + public Handle CreateMesh(UnsafeList vertices, UnsafeList indices, bool staticMesh) { var mesh = ResourceManager.CreateMesh(vertices, indices); @@ -253,6 +290,7 @@ public readonly unsafe ref struct RenderContext meshletBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshLetBuffer.AsResource()), meshletVerticesBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshletVerticesBuffer.AsResource()), meshletTrianglesBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshletTrianglesBuffer.AsResource()), + materialSlotCount = (uint)meshData.MeshletData.materialSlotCount, }; var bufferHandle = meshData.MeshDataBuffer.AsResource(); diff --git a/src/Runtime/Ghost.Graphics/Services/MaterialPaletteStore.cs b/src/Runtime/Ghost.Graphics/Services/MaterialPaletteStore.cs index 0bfa13d..c062205 100644 --- a/src/Runtime/Ghost.Graphics/Services/MaterialPaletteStore.cs +++ b/src/Runtime/Ghost.Graphics/Services/MaterialPaletteStore.cs @@ -29,6 +29,8 @@ internal sealed class MaterialPaletteStore : IDisposable public int refCount; public ulong lookupHash; public int nextFree; + /// First element index in _materialIndices for this palette. + public int indicesOffset; public readonly bool IsAllocated => materials.IsCreated; public readonly bool IsActive => refCount > 0 && materials.IsCreated; @@ -44,6 +46,51 @@ internal sealed class MaterialPaletteStore : IDisposable private int _freeListHead; private bool _disposed; + // Deferred-release queue: slots are not reclaimed until the GPU has finished + // reading them (same pattern as ResourceManager.ResourceReturnEntry). + private struct PendingFreeSlot + { + public int slotIndex; + public ulong releaseFrame; + } + + private UnsafeQueue _pendingFreeSlots; + private ulong _currentFrame; + + // ── CPU-side GPU buffer mirrors ────────────────────────────────────────── + // Index 0 is always reserved (palette 0 = no palette / empty), so both + // lists are pre-seeded with one sentinel entry each. + + /// One uint per palette entry: base offset into . + private UnsafeList _paletteOffsets; + + /// Packed bindless CBuffer descriptor indices for all palettes, contiguous. + private UnsafeList _materialIndices; + + // Dirty ranges for incremental GPU upload. + private int _dirtyOffsetStart; + private int _dirtyOffsetEnd; + private int _dirtyIndicesStart; + private int _dirtyIndicesEnd; + private bool _gpuDirty; + + /// + /// Returns true if any palette data has changed since the last call. + /// + public bool IsGpuDirty => _gpuDirty; + + /// + /// Returns the CPU-side palette offset array (one uint per palette slot). + /// Only valid to read on the render thread after main-thread work is complete. + /// + public ReadOnlySpan PaletteOffsets => _paletteOffsets.AsSpan(); + + /// + /// Returns the CPU-side packed material bindless index array. + /// Entries are 0 until is called. + /// + public ReadOnlySpan MaterialIndices => _materialIndices.AsSpan(); + public MaterialPaletteStore(int initialCapacity = 16) { if (initialCapacity <= 0) @@ -54,6 +101,20 @@ internal sealed class MaterialPaletteStore : IDisposable _entries = new UnsafeList(initialCapacity + 1, AllocationHandle.Persistent); _lookup = new UnsafeHashMap(initialCapacity * 2, AllocationHandle.Persistent); _freeListHead = 0; + + _paletteOffsets = new UnsafeList(initialCapacity + 1, AllocationHandle.Persistent); + _materialIndices = new UnsafeList(initialCapacity * 4, AllocationHandle.Persistent); + _pendingFreeSlots = new UnsafeQueue(16, AllocationHandle.Persistent); + + // Slot 0 is reserved (empty palette). Seed both lists so indices stay in sync. + _paletteOffsets.Add(0); // palette 0 offset = 0 + _materialIndices.Add(0); // placeholder, never read for palette 0 + + _dirtyOffsetStart = int.MaxValue; + _dirtyOffsetEnd = 0; + _dirtyIndicesStart = int.MaxValue; + _dirtyIndicesEnd = 0; + _gpuDirty = false; } ~MaterialPaletteStore() @@ -145,6 +206,31 @@ internal sealed class MaterialPaletteStore : IDisposable } _lookup.Add(hash, index); + // Record where in _materialIndices this palette's slots begin. + newEntry.indicesOffset = _materialIndices.Count; + + // Ensure _paletteOffsets is large enough (index may be a recycled slot). + while (_paletteOffsets.Count <= index) + { + _paletteOffsets.Add(0); + } + + _paletteOffsets[index] = (uint)newEntry.indicesOffset; + + // Append placeholder indices (0 = unresolved). ResolveMaterialIndices + // will overwrite these with real bindless indices before GPU upload. + for (var i = 0; i < materials.Length; i++) + { + _materialIndices.Add(0); + } + + // Mark dirty ranges. + _dirtyOffsetStart = Math.Min(_dirtyOffsetStart, index); + _dirtyOffsetEnd = Math.Max(_dirtyOffsetEnd, index + 1); + _dirtyIndicesStart = Math.Min(_dirtyIndicesStart, newEntry.indicesOffset); + _dirtyIndicesEnd = Math.Max(_dirtyIndicesEnd, newEntry.indicesOffset + materials.Length); + _gpuDirty = true; + return index; } @@ -207,10 +293,96 @@ internal sealed class MaterialPaletteStore : IDisposable return; } + // Remove from CPU lookup immediately — the slot is logically dead. _lookup.Remove(entry.lookupHash); entry.materials.Clear(); - entry.nextFree = _freeListHead; - _freeListHead = paletteID; + + // Do NOT push to _freeListHead yet. The GPU may still be in-flight reading + // _paletteOffsets[slot] from previous frames. Queue for deferred reclaim. + _pendingFreeSlots.Enqueue(new PendingFreeSlot + { + slotIndex = paletteID, + releaseFrame = _currentFrame, + }); + } + + /// + /// Advances the frame counter and reclaims slots whose GPU reads have completed. + /// Must be called from ResourceManager.EndFrame(completedFrame) with the + /// same that was used to drain the resource return queue. + /// A slot is safe to reuse when completedFrame > releaseFrame, meaning + /// all GPU command buffers that could have read the old palette entry have retired. + /// + public void EndFrame(ulong currentFrame, ulong completedFrame) + { + _currentFrame = currentFrame; + + while (_pendingFreeSlots.TryPeek(out var pending) && pending.releaseFrame < completedFrame) + { + _pendingFreeSlots.Dequeue(); + ref var entry = ref _entries[pending.slotIndex]; + entry.nextFree = _freeListHead; + _freeListHead = pending.slotIndex; + } + } + + // ── GPU upload support ─────────────────────────────────────────────────── + + /// + /// Gets the dirty sub-ranges that need to be uploaded to the GPU. + /// + public void GetDirtyRanges( + out int offsetStart, out int offsetEnd, + out int indicesStart, out int indicesEnd) + { + offsetStart = _gpuDirty ? _dirtyOffsetStart : 0; + offsetEnd = _gpuDirty ? _dirtyOffsetEnd : 0; + indicesStart = _gpuDirty ? _dirtyIndicesStart : 0; + indicesEnd = _gpuDirty ? _dirtyIndicesEnd : 0; + } + + /// + /// Resolves every active palette's material handles to GPU bindless CBuffer indices. + /// Must be called on the render thread before uploading to GPU. + /// + /// + /// Delegate that maps a to its CBuffer bindless descriptor heap + /// index. Typically mat => resourceDatabase.GetBindlessIndex(material.CBuffer). + /// + /// The state object to pass to the resolveIndex delegate. + public void ResolveMaterialIndices(Func, object?, uint> resolveIndex, object? state) + { + if (!_gpuDirty) + { + return; + } + + for (var i = 1; i < _entries.Count; i++) + { + ref var entry = ref _entries[i]; + if (!entry.IsActive) + { + continue; + } + + var baseOffset = entry.indicesOffset; + for (var slot = 0; slot < entry.materials.Count; slot++) + { + _materialIndices[baseOffset + slot] = resolveIndex(entry.materials[slot], state); + } + } + } + + /// + /// Clears the dirty flag after GPU upload is complete. + /// + public void ClearDirty() + { + _dirtyOffsetStart = int.MaxValue; + _dirtyOffsetEnd = 0; + _dirtyIndicesStart = int.MaxValue; + _dirtyIndicesEnd = 0; + _gpuDirty = false; } public void Dispose() @@ -227,6 +399,9 @@ internal sealed class MaterialPaletteStore : IDisposable _entries.Dispose(); _lookup.Dispose(); + _paletteOffsets.Dispose(); + _materialIndices.Dispose(); + _pendingFreeSlots.Dispose(); _disposed = true; GC.SuppressFinalize(this); diff --git a/src/Runtime/Ghost.Graphics/Services/ResourceManager.cs b/src/Runtime/Ghost.Graphics/Services/ResourceManager.cs index 08d342b..875f100 100644 --- a/src/Runtime/Ghost.Graphics/Services/ResourceManager.cs +++ b/src/Runtime/Ghost.Graphics/Services/ResourceManager.cs @@ -9,6 +9,8 @@ namespace Ghost.Graphics.Services; public sealed partial class ResourceManager : IDisposable { + private const uint _PALETTE_BUFFER_INITIAL_CAPACITY = 64; + private readonly struct ResourceReturnEntry { public readonly Handle handle; @@ -32,6 +34,12 @@ public sealed partial class ResourceManager : IDisposable private readonly MaterialPaletteStore _materialPalettes; + // Persistent GPU buffers for the two-buffer material palette indirection. + private Handle _paletteOffsetBuffer; + private Handle _materialIndexBuffer; + private uint _paletteOffsetCapacity; + private uint _materialIndexCapacity; + // TODO: Any better way? System.Threading.Lock is very fast though, it use spin lock before entering kernel. // rw lock slim is an option but it has more overhead on read. Because more than 90% of the time we are reading, it may not be a good option. // Plus UnsafeSlotMap use jagged array internally, which means we can have concurrent read and write, but not add and remove, on different slots without any issue, so we only need to lock when writing to those slots. @@ -44,6 +52,18 @@ public sealed partial class ResourceManager : IDisposable private bool _disposed; + /// + /// Returns the bindless descriptor heap index for the palette offset GPU buffer. + /// Valid after the first call. + /// + public uint PaletteOffsetBufferBindlessIndex => _resourceDatabase.GetBindlessIndex(_paletteOffsetBuffer.AsResource()); + + /// + /// Returns the bindless descriptor heap index for the material index GPU buffer. + /// Valid after the first call. + /// + public uint MaterialIndexBufferBindlessIndex => _resourceDatabase.GetBindlessIndex(_materialIndexBuffer.AsResource()); + public ResourceManager(IRenderDevice renderDevice, IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase) { _renderDevice = renderDevice; @@ -61,6 +81,12 @@ public sealed partial class ResourceManager : IDisposable _materialWriteLock = new Lock(); _shaderWriteLock = new Lock(); _computeShaderWriteLock = new Lock(); + + // Create initial GPU palette buffers. These grow on demand in UploadMaterialPaletteData. + _paletteOffsetCapacity = _PALETTE_BUFFER_INITIAL_CAPACITY; + _materialIndexCapacity = _PALETTE_BUFFER_INITIAL_CAPACITY * 4; + _paletteOffsetBuffer = CreatePaletteBuffer(_paletteOffsetCapacity, "PaletteOffsetBuffer"); + _materialIndexBuffer = CreatePaletteBuffer(_materialIndexCapacity, "MaterialIndexBuffer"); } ~ResourceManager() @@ -77,6 +103,7 @@ public sealed partial class ResourceManager : IDisposable internal void EndFrame(ulong completedFrame) { Logger.DebugAssert(!_disposed); + _materialPalettes.EndFrame(_submittedFrame, completedFrame); EndFramePool(completedFrame); } @@ -336,7 +363,102 @@ public sealed partial class ResourceManager : IDisposable } /// - /// Releases a material palette reference previously returned by . + /// Resolves dirty material palette data and uploads it to the GPU. + /// Must be called once per frame on the render thread, before any draw calls. + /// Handles buffer growth with copy-on-resize semantics (same pattern as GPUScene). + /// + public void UploadMaterialPaletteData(RenderContext ctx) + { + Logger.DebugAssert(!_disposed); + + if (!_materialPalettes.IsGpuDirty) + { + return; + } + + // Resolve material handles → bindless CBuffer indices. + _materialPalettes.ResolveMaterialIndices(static (materialHandle, state) => + { + var self = (ResourceManager)state!; + var r = self.GetMaterialReference(materialHandle); + if (r.IsFailure || !r.Value._cBufferCache.IsCreated) + { + return 0u; + } + + return self._resourceDatabase.GetBindlessIndex(r.Value._cBufferCache.GpuResource.AsResource()); + }, this); + + var offsets = _materialPalettes.PaletteOffsets; + var indices = _materialPalettes.MaterialIndices; + + _materialPalettes.GetDirtyRanges( + out var offsetStart, out var offsetEnd, + out var indicesStart, out var indicesEnd); + + // ── Resize PaletteOffsetBuffer if needed ── + if ((uint)offsets.Length > _paletteOffsetCapacity) + { + var newCapacity = Math.Max(_paletteOffsetCapacity * 2, (uint)offsets.Length); + var newBuffer = CreatePaletteBuffer(newCapacity, "PaletteOffsetBuffer_Resized"); + + ctx.CommandBuffer.CopyBuffer(newBuffer, _paletteOffsetBuffer, 0, 0, _paletteOffsetCapacity * sizeof(uint)); + + _resourceDatabase.ReleaseResource(_paletteOffsetBuffer.AsResource()); + _paletteOffsetBuffer = newBuffer; + _paletteOffsetCapacity = newCapacity; + + // Full upload needed after resize. + offsetStart = 0; + offsetEnd = offsets.Length; + } + + // ── Resize MaterialIndexBuffer if needed ── + if ((uint)indices.Length > _materialIndexCapacity) + { + var newCapacity = Math.Max(_materialIndexCapacity * 2, (uint)indices.Length); + var newBuffer = CreatePaletteBuffer(newCapacity, "MaterialIndexBuffer_Resized"); + + ctx.CommandBuffer.CopyBuffer(newBuffer, _materialIndexBuffer, 0, 0, _materialIndexCapacity * sizeof(uint)); + + _resourceDatabase.ReleaseResource(_materialIndexBuffer.AsResource()); + _materialIndexBuffer = newBuffer; + _materialIndexCapacity = newCapacity; + + indicesStart = 0; + indicesEnd = indices.Length; + } + + // ── Upload dirty ranges ── + if (offsetEnd > offsetStart) + { + var dirtyOffsets = offsets.Slice(offsetStart, offsetEnd - offsetStart); + ctx.UploadBufferRange(_paletteOffsetBuffer, dirtyOffsets, (uint)(offsetStart * sizeof(uint))); + } + + if (indicesEnd > indicesStart) + { + var dirtyIndices = indices.Slice(indicesStart, indicesEnd - indicesStart); + ctx.UploadBufferRange(_materialIndexBuffer, dirtyIndices, (uint)(indicesStart * sizeof(uint))); + } + + _materialPalettes.ClearDirty(); + } + + private Handle CreatePaletteBuffer(uint capacity, string name) + { + var desc = new BufferDesc + { + Size = capacity * sizeof(uint), + Stride = sizeof(uint), + Usage = BufferUsage.Raw | BufferUsage.ShaderResource, + HeapType = HeapType.Default, + }; + return _resourceAllocator.CreateBuffer(in desc, name); + } + + /// + /// Releases the material palette associated with the specified palette ID. /// /// The palette index to release. public void ReleaseMaterialPalette(Identifier paletteID) @@ -468,6 +590,9 @@ public sealed partial class ResourceManager : IDisposable _shaders.Dispose(); _materialPalettes.Dispose(); + _resourceDatabase.ReleaseResource(_paletteOffsetBuffer.AsResource()); + _resourceDatabase.ReleaseResource(_materialIndexBuffer.AsResource()); + DisposePool(); _disposed = true; diff --git a/src/Runtime/Ghost.Graphics/Shaders/Includes/Common.hlsl b/src/Runtime/Ghost.Graphics/Shaders/Includes/Common.hlsl index c988551..e00ca7b 100644 --- a/src/Runtime/Ghost.Graphics/Shaders/Includes/Common.hlsl +++ b/src/Runtime/Ghost.Graphics/Shaders/Includes/Common.hlsl @@ -106,4 +106,22 @@ static inline T LoadData(BYTE_ADDRESS_BUFFER buffer, uint index) return buf.Load(index * sizeof(T)); } +/// Resolves a meshlet's local material index to a global bindless CBuffer descriptor index. +/// Uses the two-buffer indirection: PaletteOffsetBuffer → MaterialIndexBuffer → CBuffer. +/// paletteOffsetBuffer : from FrameData — one uint per palette, base offset into materialIndexBuffer +/// materialIndexBuffer : from FrameData — packed bindless CBuffer indices for all palettes +/// paletteIndex : per-instance value from InstanceData.materialPaletteIndex +/// localMaterialIndex : per-meshlet value from Meshlet.packedCounts byte 2 +static inline uint LoadMaterialBindlessIndex( + BYTE_ADDRESS_BUFFER paletteOffsetBuffer, + BYTE_ADDRESS_BUFFER materialIndexBuffer, + uint paletteIndex, + uint localMaterialIndex) +{ + ByteAddressBuffer offsets = GET_BUFFER(paletteOffsetBuffer); + ByteAddressBuffer indices = GET_BUFFER(materialIndexBuffer); + uint base = offsets.Load(paletteIndex * 4); + return indices.Load((base + localMaterialIndex) * 4); +} + #endif // GHOST_COMMON_HLSL diff --git a/src/Runtime/Ghost.Graphics/Shaders/Includes/Properties.hlsl b/src/Runtime/Ghost.Graphics/Shaders/Includes/Properties.hlsl index 0b5d387..a75a1d7 100644 --- a/src/Runtime/Ghost.Graphics/Shaders/Includes/Properties.hlsl +++ b/src/Runtime/Ghost.Graphics/Shaders/Includes/Properties.hlsl @@ -29,6 +29,8 @@ struct FrameData { BYTE_ADDRESS_BUFFER instanceBuffer; BYTE_ADDRESS_BUFFER userBuffer; + BYTE_ADDRESS_BUFFER paletteOffsetBuffer; // global PaletteOffsetBuffer + BYTE_ADDRESS_BUFFER materialIndexBuffer; // global MaterialIndexBuffer }; struct ViewData @@ -46,7 +48,7 @@ struct InstanceData { float4x4 localToWorld; BYTE_ADDRESS_BUFFER meshBuffer; - BYTE_ADDRESS_BUFFER materialBuffer; + uint materialPaletteIndex; // index into PaletteOffsetBuffer }; struct MeshData @@ -59,6 +61,7 @@ struct MeshData BYTE_ADDRESS_BUFFER meshletBuffer; BYTE_ADDRESS_BUFFER meshletVerticesBuffer; BYTE_ADDRESS_BUFFER meshletTrianglesBuffer; + uint materialSlotCount; }; #if defined(__GRAPHICS__) diff --git a/src/Test/Ghost.UnitTest/AssetSystem/AssertRegistryTest.cs b/src/Test/Ghost.UnitTest/AssetSystem/AssertRegistryTest.cs index 24d2055..8a07f07 100644 --- a/src/Test/Ghost.UnitTest/AssetSystem/AssertRegistryTest.cs +++ b/src/Test/Ghost.UnitTest/AssetSystem/AssertRegistryTest.cs @@ -1,4 +1,4 @@ -using Ghost.Editor.Core.AssetHandler; +using Ghost.Editor.Core.Assets; using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Services; diff --git a/src/Test/Ghost.UnitTest/AssetSystem/AssetCatalogTests.cs b/src/Test/Ghost.UnitTest/AssetSystem/AssetCatalogTests.cs index 5454022..03d82c5 100644 --- a/src/Test/Ghost.UnitTest/AssetSystem/AssetCatalogTests.cs +++ b/src/Test/Ghost.UnitTest/AssetSystem/AssetCatalogTests.cs @@ -1,4 +1,4 @@ -using Ghost.Editor.Core.AssetHandler; +using Ghost.Editor.Core.Assets; using Ghost.Editor.Core.Services; using Microsoft.Data.Sqlite; diff --git a/src/Test/Ghost.UnitTest/AssetSystem/AssetMetaTests.cs b/src/Test/Ghost.UnitTest/AssetSystem/AssetMetaTests.cs index 2a701da..378dfb9 100644 --- a/src/Test/Ghost.UnitTest/AssetSystem/AssetMetaTests.cs +++ b/src/Test/Ghost.UnitTest/AssetSystem/AssetMetaTests.cs @@ -1,5 +1,4 @@ -using Ghost.Editor.Core.AssetHandler; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Ghost.Editor.Core.Assets; namespace Ghost.UnitTest.AssetSystem; diff --git a/src/Test/Ghost.UnitTest/AssetSystem/ImportCoordinatorTests.cs b/src/Test/Ghost.UnitTest/AssetSystem/ImportCoordinatorTests.cs index 2377bc3..de0bab5 100644 --- a/src/Test/Ghost.UnitTest/AssetSystem/ImportCoordinatorTests.cs +++ b/src/Test/Ghost.UnitTest/AssetSystem/ImportCoordinatorTests.cs @@ -1,4 +1,4 @@ -using Ghost.Editor.Core.AssetHandler; +using Ghost.Editor.Core.Assets; using Ghost.Editor.Core.Services; using Microsoft.Data.Sqlite; diff --git a/src/ThridParty/Ghost.StbI/Wrapper/StbI.nativegen.cs b/src/ThridParty/Ghost.StbI/Wrapper/StbI.nativegen.cs index f5946cf..a96bc50 100644 --- a/src/ThridParty/Ghost.StbI/Wrapper/StbI.nativegen.cs +++ b/src/ThridParty/Ghost.StbI/Wrapper/StbI.nativegen.cs @@ -561,7 +561,7 @@ public unsafe partial struct StbIApi } /// - /// From: + /// From: /// [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public static int WritePngToFunc(delegate* unmanaged[Cdecl] func, void* context, int w, int h, int comp, void* data, int stride_in_bytes) @@ -577,7 +577,7 @@ public unsafe partial struct StbIApi } /// - /// From: + /// From: /// [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public static int WriteBmpToFunc(delegate* unmanaged[Cdecl] func, void* context, int w, int h, int comp, void* data)