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:
2026-04-28 18:22:09 +09:00
parent 631638f3fb
commit 0eaf7cd51d
19 changed files with 390 additions and 30 deletions

View File

@@ -211,11 +211,6 @@ public class MeshAssetSettings : IAssetSettings
{
get; set;
} = VertexDataSource.ComputedIfMissing;
public bool BuildMeshlets
{
get; set;
} = true;
}
internal class ObjAssetSettings : MeshAssetSettings

View File

@@ -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)

View File

@@ -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)
{

View File

@@ -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;
}

View File

@@ -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
};

View File

@@ -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 });
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
};

View File

@@ -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();

View File

@@ -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 &gt; 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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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__)

View File

@@ -1,4 +1,4 @@
using Ghost.Editor.Core.AssetHandler;
using Ghost.Editor.Core.Assets;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Services;

View File

@@ -1,4 +1,4 @@
using Ghost.Editor.Core.AssetHandler;
using Ghost.Editor.Core.Assets;
using Ghost.Editor.Core.Services;
using Microsoft.Data.Sqlite;

View File

@@ -1,5 +1,4 @@
using Ghost.Editor.Core.AssetHandler;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Ghost.Editor.Core.Assets;
namespace Ghost.UnitTest.AssetSystem;

View File

@@ -1,4 +1,4 @@
using Ghost.Editor.Core.AssetHandler;
using Ghost.Editor.Core.Assets;
using Ghost.Editor.Core.Services;
using Microsoft.Data.Sqlite;

View File

@@ -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)