Refactor material palette system with GPU indirection
Major overhaul of material palette management: - Added two-buffer indirection (PaletteOffsetBuffer, MaterialIndexBuffer) for GPU material lookup, with incremental upload and resizing. - MaterialPaletteStore now tracks dirty ranges, supports deferred slot reclamation, and exposes CPU-side arrays for upload. - ResourceManager manages persistent GPU buffers and uploads only dirty subranges per frame. - Updated HLSL and C# structs to use palette indices. - Refactored systems/components to use new palette index and release logic. - Added RenderContext.UploadBufferRange for partial uploads. Minor: Fixed StbIApi interop signatures, updated test namespaces, and performed code cleanups.
This commit is contained in:
@@ -211,11 +211,6 @@ public class MeshAssetSettings : IAssetSettings
|
||||
{
|
||||
get; set;
|
||||
} = VertexDataSource.ComputedIfMissing;
|
||||
|
||||
public bool BuildMeshlets
|
||||
{
|
||||
get; set;
|
||||
} = true;
|
||||
}
|
||||
|
||||
internal class ObjAssetSettings : MeshAssetSettings
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
public unsafe delegate int ClodOutputDelegate(void* context, ClodGroup group, ReadOnlyUnsafeCollection<ClodCluster> 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<uint> indices, float error)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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> materialPalette;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<GPUInstanceRef>(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -106,6 +106,43 @@ public readonly unsafe ref struct RenderContext
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public void UploadBufferRange<T>(Handle<GPUBuffer> buffer, ReadOnlySpan<T> 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<Mesh> CreateMesh(UnsafeList<Vertex> vertices, UnsafeList<uint> 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();
|
||||
|
||||
@@ -29,6 +29,8 @@ internal sealed class MaterialPaletteStore : IDisposable
|
||||
public int refCount;
|
||||
public ulong lookupHash;
|
||||
public int nextFree;
|
||||
/// <summary> First element index in _materialIndices for this palette. </summary>
|
||||
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<PendingFreeSlot> _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.
|
||||
|
||||
/// <summary> One uint per palette entry: base offset into <see cref="_materialIndices"/>. </summary>
|
||||
private UnsafeList<uint> _paletteOffsets;
|
||||
|
||||
/// <summary> Packed bindless CBuffer descriptor indices for all palettes, contiguous. </summary>
|
||||
private UnsafeList<uint> _materialIndices;
|
||||
|
||||
// Dirty ranges for incremental GPU upload.
|
||||
private int _dirtyOffsetStart;
|
||||
private int _dirtyOffsetEnd;
|
||||
private int _dirtyIndicesStart;
|
||||
private int _dirtyIndicesEnd;
|
||||
private bool _gpuDirty;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if any palette data has changed since the last <see cref="ClearDirty"/> call.
|
||||
/// </summary>
|
||||
public bool IsGpuDirty => _gpuDirty;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<uint> PaletteOffsets => _paletteOffsets.AsSpan();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the CPU-side packed material bindless index array.
|
||||
/// Entries are 0 until <see cref="ResolveMaterialIndices"/> is called.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<uint> MaterialIndices => _materialIndices.AsSpan();
|
||||
|
||||
public MaterialPaletteStore(int initialCapacity = 16)
|
||||
{
|
||||
if (initialCapacity <= 0)
|
||||
@@ -54,6 +101,20 @@ internal sealed class MaterialPaletteStore : IDisposable
|
||||
_entries = new UnsafeList<Entry>(initialCapacity + 1, AllocationHandle.Persistent);
|
||||
_lookup = new UnsafeHashMap<ulong, int>(initialCapacity * 2, AllocationHandle.Persistent);
|
||||
_freeListHead = 0;
|
||||
|
||||
_paletteOffsets = new UnsafeList<uint>(initialCapacity + 1, AllocationHandle.Persistent);
|
||||
_materialIndices = new UnsafeList<uint>(initialCapacity * 4, AllocationHandle.Persistent);
|
||||
_pendingFreeSlots = new UnsafeQueue<PendingFreeSlot>(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,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances the frame counter and reclaims slots whose GPU reads have completed.
|
||||
/// Must be called from <c>ResourceManager.EndFrame(completedFrame)</c> with the
|
||||
/// same <paramref name="completedFrame"/> that was used to drain the resource return queue.
|
||||
/// A slot is safe to reuse when <c>completedFrame > releaseFrame</c>, meaning
|
||||
/// all GPU command buffers that could have read the old palette entry have retired.
|
||||
/// </summary>
|
||||
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 ───────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dirty sub-ranges that need to be uploaded to the GPU.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves every active palette's material handles to GPU bindless CBuffer indices.
|
||||
/// Must be called on the render thread before uploading to GPU.
|
||||
/// </summary>
|
||||
/// <param name="resolveIndex">
|
||||
/// Delegate that maps a <see cref="Handle{Material}"/> to its CBuffer bindless descriptor heap
|
||||
/// index. Typically <c>mat => resourceDatabase.GetBindlessIndex(material.CBuffer)</c>.
|
||||
/// </param>
|
||||
/// <param name="state"> The state object to pass to the resolveIndex delegate. </param>
|
||||
public void ResolveMaterialIndices(Func<Handle<Material>, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the dirty flag after GPU upload is complete.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
@@ -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<GPUResource> 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<GPUBuffer> _paletteOffsetBuffer;
|
||||
private Handle<GPUBuffer> _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;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the bindless descriptor heap index for the palette offset GPU buffer.
|
||||
/// Valid after the first <see cref="UploadMaterialPaletteData"/> call.
|
||||
/// </summary>
|
||||
public uint PaletteOffsetBufferBindlessIndex => _resourceDatabase.GetBindlessIndex(_paletteOffsetBuffer.AsResource());
|
||||
|
||||
/// <summary>
|
||||
/// Returns the bindless descriptor heap index for the material index GPU buffer.
|
||||
/// Valid after the first <see cref="UploadMaterialPaletteData"/> call.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases a material palette reference previously returned by <see cref="GetOrCreateMaterialPalette(ReadOnlySpan{Handle{Material}})"/>.
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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<GPUBuffer> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the material palette associated with the specified palette ID.
|
||||
/// </summary>
|
||||
/// <param name="paletteID">The palette index to release.</param>
|
||||
public void ReleaseMaterialPalette(Identifier<MaterialPalette> 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;
|
||||
|
||||
@@ -106,4 +106,22 @@ static inline T LoadData(BYTE_ADDRESS_BUFFER buffer, uint index)
|
||||
return buf.Load<T>(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
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Ghost.Editor.Core.AssetHandler;
|
||||
using Ghost.Editor.Core.Assets;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Ghost.Editor.Core.Services;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Ghost.Editor.Core.AssetHandler;
|
||||
using Ghost.Editor.Core.Assets;
|
||||
using Ghost.Editor.Core.Services;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Ghost.Editor.Core.AssetHandler;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Ghost.Editor.Core.Assets;
|
||||
|
||||
namespace Ghost.UnitTest.AssetSystem;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Ghost.Editor.Core.AssetHandler;
|
||||
using Ghost.Editor.Core.Assets;
|
||||
using Ghost.Editor.Core.Services;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
|
||||
@@ -561,7 +561,7 @@ public unsafe partial struct StbIApi
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// From: <see cref="Api.stbi_write_png_to_func(delegate* unmanaged[Cdecl]<void*, void*, int, void>, void*, int, int, int, void*, int)" />
|
||||
/// From: <see cref="Api.stbi_write_png_to_func(delegate* unmanaged[Cdecl]{void*, void*, int, void}, void*, int, int, int, void*, int)" />
|
||||
/// </summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public static int WritePngToFunc(delegate* unmanaged[Cdecl]<void*, void*, int, void> func, void* context, int w, int h, int comp, void* data, int stride_in_bytes)
|
||||
@@ -577,7 +577,7 @@ public unsafe partial struct StbIApi
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// From: <see cref="Api.stbi_write_bmp_to_func(delegate* unmanaged[Cdecl]<void*, void*, int, void>, void*, int, int, int, void*)" />
|
||||
/// From: <see cref="Api.stbi_write_bmp_to_func(delegate* unmanaged[Cdecl]{void*, void*, int, void}, void*, int, int, int, void*)" />
|
||||
/// </summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public static int WriteBmpToFunc(delegate* unmanaged[Cdecl]<void*, void*, int, void> func, void* context, int w, int h, int comp, void* data)
|
||||
|
||||
Reference in New Issue
Block a user