Files
GhostEngine/src/Runtime/Ghost.Graphics/Core/RenderContext.cs
Misaki 0eaf7cd51d 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.
2026-04-28 18:22:09 +09:00

429 lines
17 KiB
C#

using Ghost.Core;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.Mathematics;
using System.Runtime.InteropServices;
namespace Ghost.Graphics.Core;
// TODO: Temporary rendering context for heap creation and data upload. We will refactor it later when we have a better understanding of the engine architecture.
public readonly unsafe ref struct RenderContext
{
public required ICommandBuffer CommandBuffer
{
get; init;
}
public required ResourceManager ResourceManager
{
get; init;
}
public required IResourceAllocator ResourceAllocator
{
get; init;
}
public required IResourceDatabase ResourceDatabase
{
get; init;
}
public required IPipelineLibrary PipelineLibrary
{
get; init;
}
internal ShaderLibrary ShaderLibrary
{
get; init;
}
private void TransitionBarrier(Handle<GPUResource> resource, bool isTexture, BarrierLayout newLayout, BarrierAccess newAccess, BarrierSync newSync)
{
BarrierDesc desc;
if (isTexture)
{
desc = BarrierDesc.Texture(resource, newSync, newAccess, newLayout);
}
else
{
desc = BarrierDesc.Buffer(resource, newSync, newAccess);
}
CommandBuffer.Barrier(desc);
}
public void UploadBuffer<T>(Handle<GPUBuffer> buffer, params ReadOnlySpan<T> data)
where T : unmanaged
{
var r = ResourceDatabase.GetResourceDescription(buffer.AsResource());
if (r.IsFailure)
{
return;
}
Logger.DebugAssert(r.Value.Type == ResourceType.Buffer);
var sizeInBytes = (nuint)(data.Length * sizeof(T));
var memoryType = r.Value.BufferDescriptor.HeapType;
if (memoryType == HeapType.Upload)
{
fixed (T* pData = data)
{
var mappedData = ResourceDatabase.MapResource(buffer.AsResource(), 0, null);
MemoryUtility.MemCpy(mappedData, pData, sizeInBytes);
ResourceDatabase.UnmapResource(buffer.AsResource(), 0, null);
}
}
else
{
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 buffer data.");
}
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, 0, 0, sizeInBytes);
}
}
/// <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);
var r = ResourceManager.GetMeshReference(mesh);
if (r.IsFailure)
{
return mesh;
}
ref var meshData = ref r.Value;
var vertexHandle = meshData.VertexBuffer.AsResource();
var indexHandle = meshData.IndexBuffer.AsResource();
TransitionBarrier(vertexHandle, false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
TransitionBarrier(indexHandle, false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
UploadBuffer(meshData.VertexBuffer, meshData.Vertices.AsSpan());
UploadBuffer(meshData.IndexBuffer, meshData.Indices.AsSpan());
TransitionBarrier(vertexHandle, false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.VertexShading);
TransitionBarrier(indexHandle, false, BarrierLayout.Undefined, BarrierAccess.IndexBuffer, BarrierSync.IndexInput);
if (staticMesh)
{
//meshData.CookMeshlets();
UploadMeshlets(mesh);
meshData.ReleaseCpuResources();
}
return mesh;
}
public Handle<Mesh> CreateMesh(ReadOnlySpan<Vertex> vertices, ReadOnlySpan<uint> indices, bool staticMesh)
{
var vertexList = new UnsafeList<Vertex>(vertices.Length, AllocationHandle.Persistent);
var indexList = new UnsafeList<uint>(indices.Length, AllocationHandle.Persistent);
vertexList.CopyFrom(vertices);
indexList.CopyFrom(indices);
return CreateMesh(vertexList, indexList, staticMesh);
}
/// <summary>
/// Uploads the mesh data to the GPU.
/// </summary>
/// <param name="mesh">The handle point to the mesh buffer</param>
/// <param name="markMeshStatic">Whether to mark the mesh as static. If it's true, the cpu buffer of the mesh will not be avaliable any more</param>
public void UploadMesh(Handle<Mesh> mesh, bool markMeshStatic)
{
var r = ResourceManager.GetMeshReference(mesh);
if (r.IsFailure)
{
return;
}
ref var meshRef = ref r.Value;
var vertexHandle = meshRef.VertexBuffer.AsResource();
var indexHandle = meshRef.IndexBuffer.AsResource();
TransitionBarrier(vertexHandle, false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
TransitionBarrier(indexHandle, false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
UploadBuffer(meshRef.VertexBuffer, meshRef.Vertices.AsSpan());
UploadBuffer(meshRef.IndexBuffer, meshRef.Indices.AsSpan());
TransitionBarrier(vertexHandle, false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.VertexShading);
TransitionBarrier(indexHandle, false, BarrierLayout.Undefined, BarrierAccess.IndexBuffer, BarrierSync.IndexInput);
if (markMeshStatic)
{
meshRef.ReleaseCpuResources();
}
}
public void UploadMeshlets(Handle<Mesh> mesh)
{
var r = ResourceManager.GetMeshReference(mesh);
if (r.IsFailure)
{
return;
}
ref var meshRef = ref r.Value;
ref readonly var meshletData = ref meshRef.MeshletData;
if (!meshletData.meshlets.IsCreated || meshletData.meshlets.Count == 0) return;
var meshletDesc = new BufferDesc
{
Size = (uint)(meshletData.meshlets.Count * sizeof(Meshlet)),
Stride = (uint)sizeof(Meshlet),
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
HeapType = HeapType.Default,
};
var verticesDesc = new BufferDesc
{
Size = (uint)(meshletData.meshletVertices.Count * sizeof(uint)),
Stride = sizeof(uint),
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
HeapType = HeapType.Default,
};
// Ensure size is multiple of 4 for Raw buffer
var trianglesSize = (uint)meshletData.meshletTriangles.Count * sizeof(uint);
var trianglesDesc = new BufferDesc
{
Size = trianglesSize,
Stride = sizeof(uint),
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
HeapType = HeapType.Default,
};
meshRef.MeshLetBuffer = ResourceAllocator.CreateBuffer(in meshletDesc, "Meshlets");
meshRef.MeshletVerticesBuffer = ResourceAllocator.CreateBuffer(in verticesDesc, "MeshletVertices");
meshRef.MeshletTrianglesBuffer = ResourceAllocator.CreateBuffer(in trianglesDesc, "MeshletTriangles");
TransitionBarrier(meshRef.MeshLetBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
TransitionBarrier(meshRef.MeshletVerticesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
TransitionBarrier(meshRef.MeshletTrianglesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
UploadBuffer(meshRef.MeshLetBuffer, meshletData.meshlets.AsSpan());
UploadBuffer(meshRef.MeshletVerticesBuffer, meshletData.meshletVertices.AsSpan());
UploadBuffer(meshRef.MeshletTrianglesBuffer, meshletData.meshletTriangles.AsSpan());
TransitionBarrier(meshRef.MeshLetBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.NonPixelShading | BarrierSync.PixelShading);
TransitionBarrier(meshRef.MeshletVerticesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.NonPixelShading | BarrierSync.PixelShading);
TransitionBarrier(meshRef.MeshletTrianglesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.NonPixelShading | BarrierSync.PixelShading);
}
public void UpdateObjectData(Handle<Mesh> mesh)
{
var r = ResourceManager.GetMeshReference(mesh);
if (r.IsFailure)
{
return;
}
ref readonly var meshData = ref r.Value;
var data = new MeshData
{
worldBoundsMin = meshData.BoundingBox.Min,
worldBoundsMax = meshData.BoundingBox.Max,
vertexBuffer = ResourceDatabase.GetBindlessIndex(meshData.VertexBuffer.AsResource()),
indexBuffer = ResourceDatabase.GetBindlessIndex(meshData.IndexBuffer.AsResource()),
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();
TransitionBarrier(bufferHandle, false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
UploadBuffer(meshData.MeshDataBuffer, data);
TransitionBarrier(bufferHandle, false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.PixelShading | BarrierSync.NonPixelShading);
}
public Handle<GPUTexture> CreateTexture<T>(ref readonly TextureDesc desc, ReadOnlySpan<T> data, string name)
where T : unmanaged
{
var handle = ResourceAllocator.CreateTexture(in desc, name);
UploadTexture(handle, data);
return handle;
}
public void UploadTexture<T>(Handle<GPUTexture> texture, ReadOnlySpan<T> data)
where T : unmanaged
{
var desc = ResourceDatabase.GetResourceDescription(texture.AsResource()).GetValueOrThrow();
desc.TextureDescriptor.Format.GetSurfaceInfo(desc.TextureDescriptor.Width, desc.TextureDescriptor.Height, out var rowPitch, out var slicePitch, out _);
var requiredSize = ResourceDatabase.GetIntermediateResourceSize(texture.AsResource(), 0, 1);
var uploadDesc = new BufferDesc
{
Size = requiredSize,
Usage = BufferUsage.Upload,
HeapType = HeapType.Upload,
};
var uploadHandle = ResourceManager.CreateTransientBuffer(in uploadDesc);
if (uploadHandle.IsInvalid)
{
throw new OutOfMemoryException("Failed to create upload buffer for texture data.");
}
TransitionBarrier(texture.AsResource(), true, BarrierLayout.CopyDest, BarrierAccess.CopyDest, BarrierSync.Copy);
fixed (T* pData = data)
{
var subresourceData = new SubResourceData
{
pData = pData,
rowPitch = rowPitch,
slicePitch = slicePitch
};
CommandBuffer.UpdateSubResources(texture.AsResource(), uploadHandle.AsResource(), subresourceData);
}
}
public void DispatchCompute<T>(Handle<ComputeShader> compute, int entryIndex, ref readonly LocalKeywordSet keywordSet, ref readonly T property, uint3 threadGroupCount)
where T : unmanaged
{
ref var shader = ref ResourceManager.GetComputeShaderReference(compute).GetValueOrThrow();
var entryHash = shader.GetEntryID(entryIndex);
var variantKey = RHIUtility.CreateShaderVariantKey(entryHash, in keywordSet);
// TODO: Refactor this into a helper method.
var (compiledHash, error) = ShaderLibrary.GetCompiledHash(variantKey);
if (error.IsFailure)
{
// TODO: Fallback to an error material.
Logger.Debug($"No compiled shader found for compute shader {shader.UniqueID} with entry point {entryIndex} and keywords {keywordSet}.");
return;
}
var pipelineKey = RHIUtility.CreateComputePipelineKey(compiledHash);
if (!PipelineLibrary.HasPipelineStateObject(pipelineKey))
{
using var scope = AllocationManager.CreateStackScope();
var compiledCacheResult = ShaderLibrary.GetCompiledCache(shader.UniqueID, entryIndex, scope.AllocationHandle);
if (compiledCacheResult.IsFailure)
{
// TODO: Fallback to a checkerboard shader.
throw new InvalidOperationException("Failed to load compiled shader cache for pipeline state object creation.");
}
var cache = compiledCacheResult.Value;
Logger.DebugAssert(cache.compiledHash == compiledHash);
ShaderLibrary.ParseCacheData(cache.byteCode, out _, out var byteCodeOffsets, out var byteCodes);
Logger.DebugAssert(byteCodeOffsets.Length == 1);
var psoDes = new ComputePSODesc
{
CompiledHash = compiledHash,
VariantKey = variantKey,
CsCode = byteCodes.Slice((int)byteCodeOffsets[0]),
};
PipelineLibrary.CreateComputePipeline(in psoDes).GetValueOrThrow();
}
CommandBuffer.SetPipelineState(pipelineKey);
var propertySpan = MemoryMarshal.AsBytes(new ReadOnlySpan<T>(in property));
// TODO: Placed resource has 64k alignment requirement, which can waste lots of memory. We can allocate a large buffer and slice it for each dispatch to avoid this issue.
var propertyBufferDesc = new BufferDesc
{
Size = (uint)propertySpan.Length,
Stride = (uint)sizeof(T),
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
HeapType = HeapType.Upload,
};
var properyBuffer = ResourceManager.CreateTransientBuffer(in propertyBufferDesc);
var mappedData = ResourceDatabase.MapResource(properyBuffer.AsResource(), 0, null);
Logger.DebugAssert(mappedData != null, "Failed to map property buffer.");
fixed (byte* pData = propertySpan)
{
MemoryUtility.MemCpy(mappedData, pData, (nuint)propertySpan.Length);
}
error = ResourceDatabase.UnmapResource(properyBuffer.AsResource(), 0, null);
Logger.DebugAssert(error.IsSuccess, $"Failed to unmap property buffer: {error}.");
var pushConstant = new PushConstantsData
{
// TODO: Support frame and view buffer.
frameBuffer = 0,
viewBuffer = 0,
propertyBuffer = ResourceDatabase.GetBindlessIndex(properyBuffer.AsResource()),
};
CommandBuffer.SetGraphicsRoot32Constants(0, pushConstant.AsUInts());
CommandBuffer.DispatchCompute(threadGroupCount.x, threadGroupCount.y, threadGroupCount.z);
}
}