feat(resource): refactor heap management & suballocation

Major overhaul of GPU resource/heap management:
- Replace resource pooling and upload buffer logic with transient heap/page-based suballocation in ResourceManager.
- Add support for suballocation and heap flags/types, with D3D12 helpers.
- Remove ICommandBuffer.UploadBuffer/UploadTexture; add UpdateSubResources and CopyBuffer, move upload logic to RenderingContext.
- Refactor D3D12ResourceAllocator/Database for suballocation, heap flags, and mapping.
- Standardize on Handle<GPUBuffer> usage.
- Update meshlet/mesh utilities for new allocation handles and memory pools.
- Refactor RenderGraph and docs to use "heap" terminology.
- Use cpuFrame/gpuFrame consistently for frame sync.
- Add s2h.hlsl, s2h_3d.hlsl, s2h_scatter.hlsl shader debug libs.
- Miscellaneous fixes, cleanup, and dependency updates.

BREAKING CHANGE: Resource pooling and upload APIs replaced with new heap/page-based suballocation system. Update all buffer/texture creation and upload code to use new ResourceManager and ICommandBuffer methods.
This commit is contained in:
2026-04-03 01:48:49 +09:00
parent d03eb659fa
commit 6321b36ef5
47 changed files with 2552 additions and 526 deletions

View File

@@ -11,16 +11,16 @@ namespace Ghost.Graphics.Core;
internal struct CBufferCache : IResourceReleasable
{
private UnsafeArray<byte> _cpuData;
private Handle<RHI.GPUBuffer> _gpuResource;
private Handle<GPUBuffer> _gpuResource;
private uint _size;
public readonly UnsafeArray<byte> CpuData => _cpuData;
public readonly Handle<RHI.GPUBuffer> GpuResource => _gpuResource;
public readonly Handle<GPUBuffer> GpuResource => _gpuResource;
public readonly uint Size => _size;
public readonly bool IsCreated => _size != 0 && _gpuResource.IsValid && _cpuData.IsCreated;
public CBufferCache(Handle<RHI.GPUBuffer> buffer, uint bufferSize)
public CBufferCache(Handle<GPUBuffer> buffer, uint bufferSize)
{
_size = bufferSize;
_cpuData = new UnsafeArray<byte>((int)bufferSize, Allocator.Persistent);
@@ -37,7 +37,7 @@ internal struct CBufferCache : IResourceReleasable
_cpuData.Dispose();
database.ReleaseResource(_gpuResource.AsResource());
_gpuResource = Handle<RHI.GPUBuffer>.Invalid;
_gpuResource = Handle<GPUBuffer>.Invalid;
_size = 0;
}
}

View File

@@ -1,7 +1,6 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Utilities;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.Mathematics;
@@ -138,7 +137,7 @@ public struct Mesh : IResourceReleasable
/// <summary>
/// Gets the handle to the vertex buffer on the GPU.
/// </summary>
public Handle<RHI.GPUBuffer> VertexBuffer
public Handle<GPUBuffer> VertexBuffer
{
get; internal set;
}
@@ -146,7 +145,7 @@ public struct Mesh : IResourceReleasable
/// <summary>
/// Gets the handle to the index buffer on the GPU.
/// </summary>
public Handle<RHI.GPUBuffer> IndexBuffer
public Handle<GPUBuffer> IndexBuffer
{
get; internal set;
}
@@ -154,7 +153,7 @@ public struct Mesh : IResourceReleasable
/// <summary>
/// Gets the handle to the meshlet buffer on the GPU.
/// </summary>
public Handle<RHI.GPUBuffer> MeshLetBuffer
public Handle<GPUBuffer> MeshLetBuffer
{
get; internal set;
}
@@ -162,7 +161,7 @@ public struct Mesh : IResourceReleasable
/// <summary>
/// Gets the handle to the meshlet vertices buffer on the GPU.
/// </summary>
public Handle<RHI.GPUBuffer> MeshletVerticesBuffer
public Handle<GPUBuffer> MeshletVerticesBuffer
{
get; internal set;
}
@@ -170,7 +169,7 @@ public struct Mesh : IResourceReleasable
/// <summary>
/// Gets the handle to the meshlet triangles buffer on the GPU.
/// </summary>
public Handle<RHI.GPUBuffer> MeshletTrianglesBuffer
public Handle<GPUBuffer> MeshletTrianglesBuffer
{
get; internal set;
}
@@ -178,12 +177,12 @@ public struct Mesh : IResourceReleasable
/// <summary>
/// Gets the handle to the mesh data buffer on the GPU.
/// </summary>
public Handle<RHI.GPUBuffer> ObjectDataBuffer
public Handle<GPUBuffer> ObjectDataBuffer
{
get; internal set;
}
internal Mesh(ReadOnlySpan<Vertex> vertices, ReadOnlySpan<uint> indices, Handle<RHI.GPUBuffer> vertexBuffer, Handle<RHI.GPUBuffer> indexBuffer)
internal Mesh(ReadOnlySpan<Vertex> vertices, ReadOnlySpan<uint> indices, Handle<GPUBuffer> vertexBuffer, Handle<GPUBuffer> indexBuffer)
{
Vertices = new UnsafeList<Vertex>(vertices.Length, Allocator.Persistent);
Indices = new UnsafeList<uint>(indices.Length, Allocator.Persistent);

View File

@@ -2,10 +2,11 @@ using Ghost.Core;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.Diagnostics;
namespace Ghost.Graphics.Core;
// TODO: Temporary rendering context for resource creation and data upload. We will refactor it later when we have a better understanding of the engine architecture.
// 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 RenderingContext
{
private readonly IGraphicsEngine _engine;
@@ -82,6 +83,49 @@ public readonly unsafe ref struct RenderingContext
ResourceDatabase.SetResourceBarrierData(resource, new ResourceBarrierData(newLayout, newAccess, newSync));
}
private void UploadBuffer<T>(Handle<GPUBuffer> buffer, params ReadOnlySpan<T> data)
where T : unmanaged
{
var r = _engine.ResourceDatabase.GetResourceDescription(buffer.AsResource());
if (r.IsFailure)
{
return;
}
Debug.Assert(r.Value.Type == ResourceType.Buffer);
var sizeInBytes = (nuint)(data.Length * sizeof(T));
var memoryType = r.Value.BufferDescription.MemoryType;
if (memoryType == ResourceMemoryType.Upload)
{
fixed (T* pData = data)
{
ResourceDatabase.Map(buffer.AsResource(), 0, null, null, pData, sizeInBytes);
}
}
else
{
//var uploadHandle = _resourceAllocator.CreateTempUploadBuffer(sizeInBytes, out var offset);
//var uploadResource = _resourceDatabase.GetResource(uploadHandle.AsResource());
var uploadDesc = new BufferDesc
{
Size = sizeInBytes,
Usage = BufferUsage.Upload,
MemoryType = ResourceMemoryType.Upload,
};
var uploadHandle = _resourceManager.CreateTransientBuffer(in uploadDesc);
fixed (T* pData = data)
{
ResourceDatabase.Map(uploadHandle.AsResource(), 0, null, null, pData, sizeInBytes);
}
_directCmd.CopyBuffer(buffer, uploadHandle, 0, 0, sizeInBytes);
}
}
public Handle<Mesh> CreateMesh(UnsafeList<Vertex> vertices, UnsafeList<uint> indices, bool staticMesh)
{
var mesh = _resourceManager.CreateMesh(vertices, indices);
@@ -98,8 +142,8 @@ public readonly unsafe ref struct RenderingContext
TransitionBarrier(vertexHandle, false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
TransitionBarrier(indexHandle, false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
_directCmd.UploadBuffer(meshData.VertexBuffer, meshData.Vertices.AsSpan());
_directCmd.UploadBuffer(meshData.IndexBuffer, meshData.Indices.AsSpan());
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);
@@ -147,8 +191,8 @@ public readonly unsafe ref struct RenderingContext
TransitionBarrier(vertexHandle, false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
TransitionBarrier(indexHandle, false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
_directCmd.UploadBuffer(meshRef.VertexBuffer, meshRef.Vertices.AsSpan());
_directCmd.UploadBuffer(meshRef.IndexBuffer, meshRef.Indices.AsSpan());
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);
@@ -204,9 +248,9 @@ public readonly unsafe ref struct RenderingContext
TransitionBarrier(meshRef.MeshletVerticesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
TransitionBarrier(meshRef.MeshletTrianglesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
_directCmd.UploadBuffer(meshRef.MeshLetBuffer, meshletData.meshlets.AsSpan());
_directCmd.UploadBuffer(meshRef.MeshletVerticesBuffer, meshletData.meshletVertices.AsSpan());
_directCmd.UploadBuffer(meshRef.MeshletTrianglesBuffer, meshletData.meshletTriangles.AsSpan());
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);
@@ -236,7 +280,7 @@ public readonly unsafe ref struct RenderingContext
var bufferHandle = meshData.ObjectDataBuffer.AsResource();
TransitionBarrier(bufferHandle, false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
_directCmd.UploadBuffer(meshData.ObjectDataBuffer, data);
UploadBuffer(meshData.ObjectDataBuffer, data);
TransitionBarrier(bufferHandle, false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.PixelShading | BarrierSync.NonPixelShading);
}
@@ -252,16 +296,29 @@ public readonly unsafe ref struct RenderingContext
public void UploadTexture<T>(Handle<GPUTexture> texture, ReadOnlySpan<T> data)
where T : unmanaged
{
var desc = ResourceDatabase.GetResourceDescription(texture.AsResource()).GetValueOrThrow();
//var size = ResourceAllocator.GetSizeInfo(desc).Size;
//if ((ulong)(data.Length * sizeof(T)) != ResourceAllocator.GetSizeInfo(desc).Size)
//{
// throw new ArgumentException("Data size does not match texture size.");
//}
var desc = ResourceDatabase.GetResourceDescription(texture.AsResource()).GetValueOrThrow();
desc.TextureDescription.Format.GetSurfaceInfo(desc.TextureDescription.Width, desc.TextureDescription.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,
MemoryType = ResourceMemoryType.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)
@@ -273,7 +330,7 @@ public readonly unsafe ref struct RenderingContext
slicePitch = slicePitch
};
_directCmd.UploadTexture(texture, subresourceData);
_directCmd.UpdateSubResources(texture.AsResource(), uploadHandle.AsResource(), subresourceData);
}
}
}

View File

@@ -5,7 +5,7 @@ using System.Diagnostics;
namespace Ghost.Graphics.RenderGraphModule;
/// <summary>
/// Main render graph class that manages resource allocation and pass execution.
/// Main render graph class that manages heap allocation and pass execution.
/// </summary>
public sealed class RenderGraph : IDisposable
{
@@ -125,7 +125,7 @@ public sealed class RenderGraph : IDisposable
/// </summary>
/// <param name="buffer">The external buffer handle.</param>
/// <returns>The identifier of the imported render graph buffer. Invalid if import fails.</returns>
public Identifier<RGBuffer> ImportBuffer(Handle<RHI.GPUBuffer> buffer, string name)
public Identifier<RGBuffer> ImportBuffer(Handle<GPUBuffer> buffer, string name)
{
var r = _resourceDatabase.GetResourceDescription(buffer.AsResource());
if (r.IsFailure)
@@ -178,7 +178,7 @@ public sealed class RenderGraph : IDisposable
}
/// <summary>
/// Compiles the render graph by culling unused passes and determining resource lifetimes.
/// Compiles the render graph by culling unused passes and determining heap lifetimes.
/// </summary>
public Error Compile(ViewState viewState)
{

View File

@@ -12,7 +12,7 @@ internal struct MemoryBlock
public bool isFree;
public int firstUsePass;
public int lastUsePass;
public int logicalResourceIndex; // Which logical resource is currently using this block
public int logicalResourceIndex; // Which logical heap is currently using this block
public MemoryBlock(ulong offset, ulong size)
{
@@ -267,7 +267,7 @@ internal sealed class ResourceAliasingManager
private readonly ResourceHeap _heap;
private readonly List<PlacedResource> _placedResources;
// Mapping from logical resource index to placed resource index
// Mapping from logical heap index to placed heap index
private readonly Dictionary<int, int> _logicalToPlaced;
private const ulong _DEFAULT_TEXTURE_ALIGNMENT = 65536; // 64KB
@@ -373,7 +373,7 @@ internal sealed class ResourceAliasingManager
_heap.size = peakMemoryUsage;
_heap.Reset();
// Allocate each logical resource in the heap
// Allocate each logical heap in the heap
foreach (var (logicalIndex, logicalResource) in logicalResources)
{
// TODO: Currently we are recalculating the aliasing candidates in the real allocation pass.
@@ -414,7 +414,7 @@ internal sealed class ResourceAliasingManager
}
// Second pass: Populate aliasedLogicalResources lists
// For each placed resource, find all OTHER placed resources at the same offset
// For each placed heap, find all OTHER placed resources at the same offset
for (var i = 0; i < _placedResources.Count; i++)
{
var placed = _placedResources[i];
@@ -432,7 +432,7 @@ internal sealed class ResourceAliasingManager
// Check if they're at the same offset
if (other.heapOffset == placed.heapOffset)
{
// Add the other's logical resource to this one's aliased list
// Add the other's logical heap to this one's aliased list
var otherLogicalIndex = other.aliasedLogicalResources[0]; // Each has exactly one at this point
if (!placed.aliasedLogicalResources.Contains(otherLogicalIndex))
{

View File

@@ -12,7 +12,7 @@ internal enum BarrierFlags
}
/// <summary>
/// Represents a resource barrier requirement that needs to be resolved at runtime.
/// Represents a heap barrier requirement that needs to be resolved at runtime.
/// </summary>
internal struct ResourceBarrier
{
@@ -55,7 +55,7 @@ internal struct ResourceBarrier
}
/// <summary>
/// Tracks the current state of a resource across passes during compilation.
/// Tracks the current state of a heap across passes during compilation.
/// </summary>
internal sealed class ResourceStateTracker
{
@@ -124,7 +124,7 @@ internal static class RenderGraphBarriers
}
/// <summary>
/// Inserts aliasing barriers when a placed resource is reused.
/// Inserts aliasing barriers when a placed heap is reused.
/// </summary>
private static void InsertAliasingBarriers(
RenderGraphPassBase pass,
@@ -148,20 +148,20 @@ internal static class RenderGraphBarriers
continue;
}
// Check if this is the first use of this logical resource
// Check if this is the first use of this logical heap
if (resource.firstUsePass == pass.index)
{
// Get the placed resource
// Get the placed heap
var placedIndex = aliasingManager.GetPlacedResourceIndex(id.Value);
if (placedIndex >= 0)
{
var placed = aliasingManager.GetPlacedResource(placedIndex);
// If this placed resource has multiple aliased resources,
// If this placed heap has multiple aliased resources,
// we need an aliasing barrier when switching between them
if (placed != null && placed.aliasedLogicalResources.Count > 1)
{
// Find the resource that used this placed memory most recently before this pass
// Find the heap that used this placed memory most recently before this pass
Identifier<RGResource> resourceBefore = default;
var mostRecentLastUse = -1;
@@ -169,10 +169,10 @@ internal static class RenderGraphBarriers
{
if (otherLogicalIndex != id.Value)
{
// Get resource by global index
// Get heap by global index
var otherResource = resources.GetResourceByIndex(otherLogicalIndex);
// Check if this resource finished before our resource starts
// Check if this heap finished before our heap starts
if (otherResource.lastUsePass < pass.index &&
otherResource.lastUsePass > mostRecentLastUse)
{
@@ -182,7 +182,7 @@ internal static class RenderGraphBarriers
}
}
// If we found a previous resource, insert aliasing barrier
// If we found a previous heap, insert aliasing barrier
if (mostRecentLastUse >= 0)
{
// Aliasing Requirement: Transition to Undefined, Sync with Predecessor
@@ -215,7 +215,7 @@ internal static class RenderGraphBarriers
List<CompiledBarrier> compiledBarriers,
RenderGraphResourceRegistry resources)
{
// Helper to add a compiled barrier for a resource transition
// Helper to add a compiled barrier for a heap transition
void AddTransition(Identifier<RGResource> id, ResourceBarrierData targetState)
{
var resource = resources.GetResource(id);

View File

@@ -22,7 +22,7 @@ public enum ResourceExtractionFlags : byte
{
None = 0,
/// <summary>
/// Releases the old resource after extraction.
/// Releases the old heap after extraction.
/// </summary>
ReleaseAfterExtract = 1 << 0,
}
@@ -36,19 +36,19 @@ public interface IRenderGraphBuilder : IDisposable
void AllowPassCulling(bool value);
/// <summary>
/// Creates a new texture resource based on the specified desc.
/// Creates a new texture heap based on the specified desc.
/// </summary>
/// <param name="desc">A structure that defines the properties and configuration of the texture to create.</param>
/// <param name="name">The name of the texture resource.</param>
/// <returns>An identifier for the newly created texture resource.</returns>
/// <param name="name">The name of the texture heap.</param>
/// <returns>An identifier for the newly created texture heap.</returns>
Identifier<RGTexture> CreateTexture(in RGTextureDesc desc, string name);
/// <summary>
/// Creates a new buffer resource based on the specified desc.
/// Creates a new buffer heap based on the specified desc.
/// </summary>
/// <param name="desc">A structure that defines the properties and configuration of the buffer to create.</param>
/// <param name="name">The name of the buffer resource.</param>
/// <returns>An identifier for the newly created buffer resource.</returns>
/// <param name="name">The name of the buffer heap.</param>
/// <returns>An identifier for the newly created buffer heap.</returns>
Identifier<RGBuffer> CreateBuffer(in BufferDesc desc, string name);
/// <summary>
@@ -69,17 +69,17 @@ public interface IRenderGraphBuilder : IDisposable
Identifier<RGBuffer> UseBuffer(Identifier<RGBuffer> buffer, AccessFlags accessMode);
/// <summary>
/// Extracts the actual texture resource associated with the given identifier for use in outside of the render graph execution context.
/// Extracts the actual texture heap associated with the given identifier for use in outside of the render graph execution context.
/// </summary>
/// <param name="src">The identifier of the texture to be extracted.</param>
/// <param name="dst">A handle to receive the actual GPU texture resource.</param>
/// <param name="dst">A handle to receive the actual GPU texture heap.</param>
void QueryTextureExtraction(Identifier<RGTexture> src, Handle<GPUTexture> dst, ResourceExtractionFlags flags = ResourceExtractionFlags.ReleaseAfterExtract);
/// <summary>
/// Extracts the actual buffer resource associated with the given identifier for use in outside of the render graph execution context.
/// Extracts the actual buffer heap associated with the given identifier for use in outside of the render graph execution context.
/// </summary>
/// <param name="src">The identifier of the buffer to be extracted.</param>
/// <param name="dst">A handle to receive the actual GPU buffer resource.</param>
/// <param name="dst">A handle to receive the actual GPU buffer heap.</param>
void QueryBufferExtraction(Identifier<RGBuffer> src, Handle<GPUBuffer> dst, ResourceExtractionFlags flags = ResourceExtractionFlags.ReleaseAfterExtract);
}
@@ -94,7 +94,7 @@ public interface IRasterRenderGraphBuilder : IRenderGraphBuilder
/// <summary>
/// Specifies that the given buffer will be used for random access operations with the specified access mode within the current context.
/// </summary>
/// <param name="buffer">An identifier for the buffer to be used for random access. Must reference a valid buffer resource.</param>
/// <param name="buffer">An identifier for the buffer to be used for random access. Must reference a valid buffer heap.</param>
/// <returns>An identifier for the buffer.</returns>
Identifier<RGBuffer> UseRandomAccessBuffer(Identifier<RGBuffer> buffer);
@@ -150,7 +150,7 @@ public interface IUnsafeRenderGraphBuilder : IRenderGraphBuilder
/// <summary>
/// Specifies that the given buffer will be used for random access operations with the specified access mode within the current context.
/// </summary>
/// <param name="buffer">An identifier for the buffer to be used for random access. Must reference a valid buffer resource.</param>
/// <param name="buffer">An identifier for the buffer to be used for random access. Must reference a valid buffer heap.</param>
/// <returns>An identifier for the buffer.</returns>
Identifier<RGBuffer> UseRandomAccessBuffer(Identifier<RGBuffer> buffer);

View File

@@ -16,16 +16,16 @@ internal sealed class CachedCompilation
// Culling decisions for each pass
public readonly List<bool> passCulledFlags = new(64);
// Physical resource aliasing mappings (logical index -> physical index)
// Physical heap aliasing mappings (logical index -> physical index)
public readonly Dictionary<int, int> logicalToPhysical = new(128);
// Placed resource metadata
// Placed heap metadata
public readonly List<PlacedResourceData> placedResources = new(32);
// Compiled barriers (stores only target states, queries before state from ResourceManager)
public readonly List<CompiledBarrier> compiledBarriers = new(128);
// Real gpu resource
// Real gpu heap
public readonly List<Handle<GPUResource>> backingResources = new(32);
// View state used for this compilation
@@ -44,7 +44,7 @@ internal sealed class CachedCompilation
}
/// <summary>
/// Placed resource data for caching.
/// Placed heap data for caching.
/// </summary>
internal struct PlacedResourceData
{

View File

@@ -4,7 +4,7 @@ using Ghost.Graphics.RHI;
namespace Ghost.Graphics.RenderGraphModule;
/// <summary>
/// Handles compilation of the render graph including pass culling, resource allocation,
/// Handles compilation of the render graph including pass culling, heap allocation,
/// barrier compilation, and cache management.
/// </summary>
internal sealed class RenderGraphCompiler
@@ -229,7 +229,7 @@ internal sealed class RenderGraphCompiler
{
Size = _aliasingManager.Heap.size + 64 * 1024, // Add 64KB padding to avoid potential overflows
Alignment = ResourceHeap.DEFAULT_ALIGNMENT,
HeapFlags = HeapFlags.AlowBufferAndTexture,
HeapFlags = HeapFlags.AllowAllBufferAndTexture,
HeapType = HeapType.Default
};

View File

@@ -12,10 +12,10 @@ public interface IRenderGraphContext
Handle<GPUResource> GetActualResource(Identifier<RGResource> resource);
Handle<GPUTexture> GetActualTexture(Identifier<RGTexture> texture);
Handle<RHI.GPUBuffer> GetActualBuffer(Identifier<RGBuffer> buffer);
Handle<GPUBuffer> GetActualBuffer(Identifier<RGBuffer> buffer);
Handle<GPUTexture> GetHistoryTexture(ReadOnlySpan<Identifier<RGTexture>> texture, int historyOffset);
Handle<RHI.GPUBuffer> GetHistoryBuffer(ReadOnlySpan<Identifier<RGBuffer>> buffer, int historyOffset);
Handle<GPUBuffer> GetHistoryBuffer(ReadOnlySpan<Identifier<RGBuffer>> buffer, int historyOffset);
ICommandBuffer GetCommandBufferUnsafe();
}
@@ -60,8 +60,8 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
private TextureFormat _dsvFormat;
private int _rtvCount;
private Handle<RHI.GPUBuffer> _activePerMaterialData;
private Handle<RHI.GPUBuffer> _activePerMeshData;
private Handle<GPUBuffer> _activePerMaterialData;
private Handle<GPUBuffer> _activePerMeshData;
private int _activeMeshIndexCount;
private uint _activeFrameBuffer;
@@ -120,9 +120,9 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
return _resources.GetResource(texture.AsResource()).backingResource.AsTexture();
}
public Handle<RHI.GPUBuffer> GetActualBuffer(Identifier<RGBuffer> buffer)
public Handle<GPUBuffer> GetActualBuffer(Identifier<RGBuffer> buffer)
{
return _resources.GetResource(buffer.AsResource()).backingResource.AsGraphicsBuffer();
return _resources.GetResource(buffer.AsResource()).backingResource.AsBuffer();
}
public Handle<GPUTexture> GetHistoryTexture(ReadOnlySpan<Identifier<RGTexture>> textures, int historyOffset)
@@ -141,11 +141,11 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
return GetActualTexture(textures[index]);
}
public Handle<RHI.GPUBuffer> GetHistoryBuffer(ReadOnlySpan<Identifier<RGBuffer>> buffers, int historyOffset)
public Handle<GPUBuffer> GetHistoryBuffer(ReadOnlySpan<Identifier<RGBuffer>> buffers, int historyOffset)
{
if (historyOffset < 0 || historyOffset >= buffers.Length)
{
return Handle<RHI.GPUBuffer>.Invalid;
return Handle<GPUBuffer>.Invalid;
}
var index = (int)(_frameIndex % buffers.Length) - historyOffset;
@@ -172,7 +172,7 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
var r = _resourceManager.GetMaterialReference(material);
if (r.IsFailure)
{
_activePerMaterialData = Handle<RHI.GPUBuffer>.Invalid;
_activePerMaterialData = Handle<GPUBuffer>.Invalid;
return;
}
@@ -185,7 +185,7 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
var shaderResult = _resourceManager.GetShaderReference(material.Shader);
if (shaderResult.IsFailure)
{
_activePerMaterialData = Handle<RHI.GPUBuffer>.Invalid;
_activePerMaterialData = Handle<GPUBuffer>.Invalid;
return;
}
@@ -230,7 +230,7 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
var r = _resourceManager.GetMeshReference(mesh);
if (r.IsFailure)
{
_activePerMeshData = Handle<RHI.GPUBuffer>.Invalid;
_activePerMeshData = Handle<GPUBuffer>.Invalid;
_activeMeshIndexCount = 0;
return;
}

View File

@@ -78,7 +78,7 @@ internal static class RenderGraphHasher
}
/// <summary>
/// Computes a hash of a texture resource's structural properties.
/// Computes a hash of a texture heap's structural properties.
/// For imported textures, hashes the backing handle.
/// For transient textures, hashes the descriptor (respecting size mode).
/// </summary>
@@ -94,7 +94,7 @@ internal static class RenderGraphHasher
// Hash imported flag
writer.Write(resource.isImported);
// For imported textures, hash the backing resource handle
// For imported textures, hash the backing heap handle
if (resource.isImported)
{
writer.Write(resource.backingResource.GetHashCode());

View File

@@ -204,7 +204,7 @@ internal sealed class RenderGraphNativePassBuilder
{
var laterPass = compiledPasses[passB];
// Build a set of render target resource IDs (color + depth)
// Build a set of render target heap IDs (color + depth)
var renderTargets = new HashSet<Identifier<RGResource>>();
for (var i = 0; i <= laterPass.maxColorIndex; i++)
{
@@ -241,7 +241,7 @@ internal sealed class RenderGraphNativePassBuilder
/// <summary>
/// Infers optimal load/store operations for all attachments in a native render pass.
/// Uses resource lifetime information to minimize memory bandwidth (critical for TBDR GPUs).
/// Uses heap lifetime information to minimize memory bandwidth (critical for TBDR GPUs).
/// </summary>
private void InferLoadStoreOps(NativeRenderPass nativePass)
{

View File

@@ -211,7 +211,7 @@ internal sealed class RenderGraphResourceRegistry
return new Identifier<RGTexture>(resource.index);
}
public Identifier<RGBuffer> ImportBuffer(ref readonly BufferDesc desc, Handle<RHI.GPUBuffer> buffer, string name)
public Identifier<RGBuffer> ImportBuffer(ref readonly BufferDesc desc, Handle<GPUBuffer> buffer, string name)
{
var resource = _pool.Rent<RenderGraphResource>();
resource.name = name;
@@ -256,7 +256,7 @@ internal sealed class RenderGraphResourceRegistry
}
/// <summary>
/// Gets resource by global index. Use this when iterating over all resources.
/// Gets heap by global index. Use this when iterating over all resources.
/// </summary>
public RenderGraphResource GetResourceByIndex(int index)
{

View File

@@ -163,7 +163,7 @@ public class RenderSystem : IDisposable
throw new NotSupportedException($"The specified graphics API '{desc.GraphicsAPI}' is not supported.");
}
_resourceManager = new ResourceManager(_graphicsEngine.ResourceAllocator, _graphicsEngine.ResourceDatabase);
_resourceManager = new ResourceManager(_graphicsEngine.Device, _graphicsEngine.ResourceAllocator, _graphicsEngine.ResourceDatabase);
_swapChainManager = new SwapChainManager(_graphicsEngine);
// Create frame resources for synchronization
@@ -246,7 +246,7 @@ public class RenderSystem : IDisposable
var flushFence = _graphicsEngine.Device.GraphicsQueue.Signal(_gpuFenceValue);
_graphicsEngine.Device.GraphicsQueue.WaitForValue(flushFence);
// Sync the current frame resource to this new fence to keep state consistent
// Sync the current frame heap to this new fence to keep state consistent
frameResource.FenceValue = flushFence;
foreach (var resource in _frameResources)
@@ -322,7 +322,7 @@ public class RenderSystem : IDisposable
}
// End the frame and present
_resourceManager.EndFrame(_cpuFenceValue);
_resourceManager.EndFrame(_gpuFenceValue);
r = _graphicsEngine.EndFrame(_gpuFenceValue);
if (r.IsFailure)
@@ -426,9 +426,11 @@ public class RenderSystem : IDisposable
_renderPipeline.Dispose();
_swapChainManager.Dispose();
_resourceManager.Dispose();
_graphicsEngine.Dispose();
_swapChainManager.Dispose();
_shutdownEvent.Dispose();
_disposed = true;

View File

@@ -0,0 +1,271 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.Diagnostics;
namespace Ghost.Graphics;
public partial class ResourceManager
{
private const ulong _DEFAULT_TRANSIENT_PAGE_SIZE = 16 * 1024 * 1024; // 16MB
private struct Page
{
public Handle<GPUResource> heap;
public ulong offset;
public HeapFlags heapFlags;
public HeapType heapType;
}
private struct RetiringPage
{
public Page page;
public ulong retireFrame;
}
private UnsafeList<Page> _activePages;
private UnsafeQueue<RetiringPage> _retiringPages;
private UnsafeList<Handle<GPUResource>> _oversizedTransientResources;
private void InitializePool()
{
_activePages = new UnsafeList<Page>(4, Allocator.Persistent);
_retiringPages = new UnsafeQueue<RetiringPage>(4, Allocator.Persistent);
_oversizedTransientResources = new UnsafeList<Handle<GPUResource>>(4, Allocator.Persistent);
}
private Error CreateNewActivePage(HeapType heapType, HeapFlags heapFlags)
{
var allocationDesc = new AllocationDesc
{
Size = _DEFAULT_TRANSIENT_PAGE_SIZE,
Alignment = 65536, // 64KB
HeapType = heapType,
HeapFlags = heapFlags,
};
var buffer = _resourceAllocator.Allocate(in allocationDesc, $"Page {_activePages.Count + _retiringPages.Count}");
if (buffer.IsInvalid)
{
return Error.OutOfMemory;
}
_activePages.Add(new Page
{
heap = buffer,
offset = 0,
heapFlags = heapFlags,
heapType = heapType,
});
return Error.None;
}
public Handle<GPUTexture> CreateTransientTexture(ref readonly TextureDesc desc, string? name = null)
{
var isRTOrDS = desc.Usage.HasFlag(TextureUsage.DepthStencil) || desc.Usage.HasFlag(TextureUsage.RenderTarget);
var size = _resourceAllocator.GetSizeInfo(ResourceDesc.Texture(desc));
if (size.Size > _DEFAULT_TRANSIENT_PAGE_SIZE)
{
var texHandle = _resourceAllocator.CreateTexture(in desc, name);
if (texHandle.IsValid)
{
_oversizedTransientResources.Add(texHandle.AsResource());
}
return texHandle;
}
var requiredHeapFlags = _renderDevice.FeatureSupport.HasFlag(FeatureSupport.AliasBuffersAndTextures) ?
HeapFlags.AllowAllBufferAndTexture :
isRTOrDS ? HeapFlags.AllowOnlyRTAndDS : HeapFlags.AllowOnlyTextures;
var foundPageIndex = -1;
var alignedOffset = 0UL;
for (var i = 0; i < _activePages.Count; i++)
{
ref var p = ref _activePages[i];
if (p.heapType != HeapType.Default)
{
continue;
}
if (p.heapFlags != requiredHeapFlags && p.heapFlags != HeapFlags.AllowAllBufferAndTexture)
{
continue;
}
var proposedOffset = (p.offset + (size.Alignment - 1)) & ~(size.Alignment - 1);
if (proposedOffset + size.Size <= _DEFAULT_TRANSIENT_PAGE_SIZE)
{
foundPageIndex = i;
alignedOffset = proposedOffset;
break;
}
}
if (foundPageIndex == -1)
{
var error = CreateNewActivePage(HeapType.Default, requiredHeapFlags);
if (error != Error.None)
{
Debug.Fail($"Failed to create a new page for transient texture: {error}");
return Handle<GPUTexture>.Invalid;
}
foundPageIndex = _activePages.Count - 1;
alignedOffset = 0;
}
ref var page = ref _activePages[foundPageIndex];
var handle = _resourceAllocator.CreateTexture(in desc, name, new CreationOptions
{
AllocationType = ResourceAllocationType.Suballocation,
Heap = page.heap,
Offset = alignedOffset,
});
page.offset = alignedOffset + size.Size;
return handle;
}
public Handle<GPUBuffer> CreateTransientBuffer(ref readonly BufferDesc desc, string? name = null)
{
var size = _resourceAllocator.GetSizeInfo(ResourceDesc.Buffer(desc));
if (size.Size > _DEFAULT_TRANSIENT_PAGE_SIZE)
{
var bufHandle = _resourceAllocator.CreateBuffer(in desc, name);
if (bufHandle.IsValid)
{
_oversizedTransientResources.Add(bufHandle.AsResource());
}
return bufHandle;
}
var requiredHeapType = desc.MemoryType switch
{
ResourceMemoryType.Upload => HeapType.Upload,
ResourceMemoryType.Readback => HeapType.Readback,
_ => HeapType.Default
};
var requiredHeapFlags = _renderDevice.FeatureSupport.HasFlag(FeatureSupport.AliasBuffersAndTextures) ?
HeapFlags.AllowAllBufferAndTexture : HeapFlags.AllowOnlyBuffers;
var foundPageIndex = -1;
var alignedOffset = 0UL;
for (var i = 0; i < _activePages.Count; i++)
{
ref var p = ref _activePages[i];
if (p.heapType != requiredHeapType)
{
continue;
}
if (p.heapFlags != requiredHeapFlags && p.heapFlags != HeapFlags.AllowAllBufferAndTexture)
{
continue;
}
var proposedOffset = (p.offset + (size.Alignment - 1)) & ~(size.Alignment - 1);
if (proposedOffset + size.Size <= _DEFAULT_TRANSIENT_PAGE_SIZE)
{
foundPageIndex = i;
alignedOffset = proposedOffset;
break;
}
}
if (foundPageIndex == -1)
{
var error = CreateNewActivePage(requiredHeapType, requiredHeapFlags);
if (error != Error.None)
{
Debug.Fail($"Failed to create a new page for transient buffer: {error}");
return Handle<GPUBuffer>.Invalid;
}
foundPageIndex = _activePages.Count - 1;
alignedOffset = 0;
}
ref var page = ref _activePages[foundPageIndex];
var handle = _resourceAllocator.CreateBuffer(in desc, name, new CreationOptions
{
AllocationType = ResourceAllocationType.Suballocation,
Heap = page.heap,
Offset = alignedOffset,
});
page.offset = alignedOffset + size.Size;
return handle;
}
private void EndFramePool(ulong gpuFrame)
{
for (var i = 0; i < _activePages.Count; i++)
{
ref var page = ref _activePages[i];
_retiringPages.Enqueue(new RetiringPage
{
page = page,
retireFrame = _cpuFrame
});
}
_activePages.Clear();
while (_retiringPages.TryPeek(out var retiringPage) && retiringPage.retireFrame <= gpuFrame)
{
_retiringPages.Dequeue();
// Reset the page for reuse
retiringPage.page.offset = 0;
_activePages.Add(retiringPage.page);
}
for (var i = 0; i < _oversizedTransientResources.Count; i++)
{
_resourceDatabase.ReleaseResource(_oversizedTransientResources[i]);
}
_oversizedTransientResources.Clear();
}
private void DisposePool()
{
foreach (var page in _activePages)
{
_resourceDatabase.ReleaseResourceImmediately(page.heap);
}
foreach (var page in _retiringPages)
{
_resourceDatabase.ReleaseResourceImmediately(page.page.heap);
}
foreach (var resource in _oversizedTransientResources)
{
_resourceDatabase.ReleaseResourceImmediately(resource);
}
_activePages.Dispose();
_retiringPages.Dispose();
_oversizedTransientResources.Dispose();
}
}

View File

@@ -8,7 +8,7 @@ using System.Diagnostics;
namespace Ghost.Graphics;
public sealed class ResourceManager : IDisposable
public sealed partial class ResourceManager : IDisposable
{
private readonly struct ResourceReturnEntry
{
@@ -22,6 +22,7 @@ public sealed class ResourceManager : IDisposable
}
}
private readonly IRenderDevice _renderDevice;
private readonly IResourceAllocator _resourceAllocator;
private readonly IResourceDatabase _resourceDatabase;
@@ -31,15 +32,13 @@ public sealed class ResourceManager : IDisposable
private readonly MaterialPaletteStore _materialPalettes;
private ulong _currentFrame;
private UnsafeHashMap<ResourceDesc, UnsafeQueue<Handle<GPUResource>>> _resourceCache;
private UnsafeQueue<ResourceReturnEntry> _resourceReturnQueue;
private ulong _cpuFrame;
private bool _disposed;
public ResourceManager(IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase)
public ResourceManager(IRenderDevice renderDevice, IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase)
{
_renderDevice = renderDevice;
_resourceAllocator = resourceAllocator;
_resourceDatabase = resourceDatabase;
@@ -48,9 +47,8 @@ public sealed class ResourceManager : IDisposable
_shaders = new UnsafeList<Shader>(16, Allocator.Persistent);
_materialPalettes = new MaterialPaletteStore();
_resourceCache = new UnsafeHashMap<ResourceDesc, UnsafeQueue<Handle<GPUResource>>>(32, Allocator.Persistent);
_resourceReturnQueue = new UnsafeQueue<ResourceReturnEntry>(32, Allocator.Persistent);
InitializePool();
}
~ResourceManager()
@@ -58,30 +56,16 @@ public sealed class ResourceManager : IDisposable
Dispose();
}
internal void BeginFrame(ulong currentFrame)
internal void BeginFrame(ulong cpuFrame)
{
Debug.Assert(!_disposed);
_currentFrame = currentFrame;
_cpuFrame = cpuFrame;
}
internal void EndFrame(ulong completedFrame)
internal void EndFrame(ulong gpuFrame)
{
Debug.Assert(!_disposed);
while (_resourceReturnQueue.TryPeek(out var entry) && entry.returnFrame <= completedFrame)
{
_resourceReturnQueue.Dequeue();
var result = _resourceDatabase.GetResourceDescription(entry.handle);
Debug.Assert(result.IsSuccess);
ref var queue = ref _resourceCache.GetValueRefOrAddDefault(result.Value, out var exist);
if (!exist)
{
queue = new UnsafeQueue<Handle<GPUResource>>(4, Allocator.Persistent);
}
queue.Enqueue(entry.handle);
}
EndFramePool(gpuFrame);
}
/// <summary>
@@ -198,7 +182,7 @@ public sealed class ResourceManager : IDisposable
}
/// <summary>
/// Releases the mesh resource associated with the specified handle, freeing any resources held by it. Includes both CPU and GPU resources.
/// Releases the mesh heap associated with the specified handle, freeing any resources held by it. Includes both CPU and GPU resources.
/// </summary>
/// <param name="handle">The handle of the mesh to release. Must refer to a mesh that was previously created and not already released.</param>
public void ReleaseMesh(Handle<Mesh> handle)
@@ -364,38 +348,6 @@ public sealed class ResourceManager : IDisposable
shader.ReleaseResource(_resourceDatabase);
}
public Handle<GPUResource> GetPooledResource(in ResourceDesc desc)
{
Debug.Assert(!_disposed);
ref var queue = ref _resourceCache.GetValueRef(desc, out var exist);
if (exist && queue.TryDequeue(out Handle<GPUResource> handle))
{
return handle;
}
handle = desc.Type switch
{
ResourceType.Buffer => _resourceAllocator.CreateBuffer(in desc.BufferDescription, "PooledBuffer").AsResource(),
ResourceType.Texture => _resourceAllocator.CreateTexture(in desc.TextureDescription, "PooledTexture").AsResource(),
_ => throw new ArgumentException("Invalid resource type.", nameof(desc)),
};
return handle;
}
public void ReturnPooledResource(Handle<GPUResource> handle)
{
Debug.Assert(!_disposed);
if (handle.IsInvalid)
{
return;
}
_resourceReturnQueue.Enqueue(new ResourceReturnEntry(handle, _currentFrame));
}
public void Dispose()
{
if (_disposed)
@@ -423,19 +375,7 @@ public sealed class ResourceManager : IDisposable
_shaders.Dispose();
_materialPalettes.Dispose();
foreach (var kvp in _resourceCache)
{
var queue = kvp.Value;
while (queue.TryDequeue(out var handle))
{
_resourceDatabase.ReleaseResource(handle);
}
queue.Dispose();
}
_resourceCache.Dispose();
_resourceReturnQueue.Dispose();
DisposePool();
_disposed = true;
GC.SuppressFinalize(this);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,354 @@
//////////////////////////////////////////////////////////////////////////
// Shader To Human (S2H) - HLSL/GLSL library for debugging shaders //
// Copyright (c) 2024-2025 Electronic Arts Inc. All rights reserved. //
//////////////////////////////////////////////////////////////////////////
// Example:
// #include "s2h.h"
// #include "s2h_3d.h"
// {
// struct Context3D context;
// // todo
// s2h_init(context);
// }
#ifndef S2H_3D_INCLUDE
#define S2H_3D_INCLUDE
struct Context3D
{
// ray origin
float3 ro;
// ray direction, normalized?
float3 rd;
// surfacePos = ro + rd * depth
float depth;
//
float4 dstColor;
};
//
void s2h_init(out Context3D context, float3 ro, float3 rd);
// @param thickness e.g. 0.09f
void s2h_drawLineWS(inout Context3D context, float3 from, float3 to, float4 color, float thickness);
// AABB: Axis Aligned Bounding Box
void s2h_drawAABB(inout Context3D context, float3 center, float3 halfSize, float4 color);
//
void s2h_drawArrowWS(inout Context3D context, float3 from, float3 to, float4 color, float thickness);
// @param worldFromObject aka objectToWorld
// @param r in world space units
void s2h_drawBasis(inout Context3D context, float4x4 worldFromObject, float r);
// @param radius e.g. 0.1f
void s2h_drawSphereWS(inout Context3D context, float3 pos, float4 color, float radius);
// 8x8 checker board with X (red) and Z (blue) around offset pointing up (Y+)
void s2h_drawCheckerBoard(inout Context3D context, float3 offset);
// infinitely far, +/- X/Z and horizon, useful to having some background for the user to orient themselves
void s2h_drawSkybox(inout Context3D context);
// implementation ----------------------------------------------------------------------
// @param ro ray origin
// @param ro ray direction
void s2h_init(out Context3D context, float3 ro, float3 rd)
{
context.ro = ro;
context.rd = rd;
context.depth = S2H_FLT_MAX;
context.dstColor = float4(0, 0, 0, 0);
}
// Inigo Quilez sphere ray intersection https://iquilezles.org/articles/intersectors
// sphere of size ra centered at point ce
float2 s2h_sphIntersect( in float3 ro, in float3 rd, in float3 ce, float ra )
{
float3 oc = ro - ce;
float b = dot( oc, rd );
float c = dot( oc, oc ) - ra*ra;
float h = b*b - c;
if( h<0.0 ) return float2(-1.0, -1.0); // no intersection
h = sqrt( h );
return float2( -b-h, -b+h );
}
// Inigo Quilez box ray intersection https://iquilezles.org/articles/intersectors
// axis aligned box centered at the origin, with size boxSize
float2 s2h_boxIntersection( in float3 ro, in float3 rd, float3 boxSize, out float3 outNormal )
{
float3 m = 1.0/rd; // can precompute if traversing a set of aligned boxes
float3 n = m*ro; // can precompute if traversing a set of aligned boxes
float3 k = abs(m)*boxSize;
float3 t1 = -n - k;
float3 t2 = -n + k;
float tN = max( max( t1.x, t1.y ), t1.z );
float tF = min( min( t2.x, t2.y ), t2.z );
if( tN>tF || tF<0.0) return float2(-1.0, -1.0); // no intersection
outNormal = (tN>0.0) ? step(float3(tN, tN, tN),t1) : // ro outside the box
step(t2,float3(tF, tF, tF)); // ro inside the box
outNormal *= -sign(rd);
return float2( tN, tF );
}
// Inigo Quilez box cylinder intersection https://iquilezles.org/articles/intersectors
// cylinder defined by extremes a and b, and radious ra
float4 s2h_cylIntersect( in float3 ro, in float3 rd, in float3 a, in float3 b, float ra )
{
float3 ba = b - a;
float3 oc = ro - a;
float baba = dot(ba,ba);
float bard = dot(ba,rd);
float baoc = dot(ba,oc);
float k2 = baba - bard*bard;
float k1 = baba*dot(oc,rd) - baoc*bard;
float k0 = baba*dot(oc,oc) - baoc*baoc - ra*ra*baba;
float h = k1*k1 - k2*k0;
if( h<0.0 ) return float4(-1.0, -1.0, -1.0, -1.0);//no intersection
h = sqrt(h);
float t = (-k1-h)/k2;
// body
float y = baoc + t*bard;
if( y>0.0 && y<baba ) return float4( t, (oc+t*rd - ba*y/baba)/ra );
// caps
t = ( ((y<0.0) ? 0.0 : baba) - baoc)/bard;
if( abs(k1+k2*t)<h )
{
return float4( t, ba*sign(y)/sqrt(baba) );
}
return float4(-1.0, -1.0, -1.0, -1.0);//no intersection
}
// normal at point p of cylinder (a,b,ra), see above
float3 s2h_cylNormal( in float3 p, in float3 a, in float3 b, float ra )
{
float3 pa = p - a;
float3 ba = b - a;
float baba = dot(ba,ba);
float paba = dot(pa,ba);
float h = dot(pa,ba)/baba;
return (pa - ba*h)/ra;
}
float s2h_dot2(float3 p)
{
return dot(p,p);
}
// cone defined by extremes pa and pb, and radious ra and rb
// Only one square root and one division is emplyed in the worst case. s2h_dot2(v) is dot(v,v)
// @param float4(t, normal)
float4 s2h_coneIntersect( in float3 ro, in float3 rd, in float3 pa, in float3 pb, in float ra, in float rb )
{
float3 ba = pb - pa;
float3 oa = ro - pa;
float3 ob = ro - pb;
float m0 = dot(ba,ba);
float m1 = dot(oa,ba);
float m2 = dot(rd,ba);
float m3 = dot(rd,oa);
float m5 = dot(oa,oa);
float m9 = dot(ob,ba);
// caps
if( m1<0.0 )
{
if( s2h_dot2(oa*m2-rd*m1)<(ra*ra*m2*m2) ) // delayed division
return float4(-m1/m2,-ba*rsqrt(m0));
}
else if( m9>0.0 )
{
float t = -m9/m2; // NOT delayed division
if( s2h_dot2(ob+rd*t)<(rb*rb) )
return float4(t,ba*rsqrt(m0));
}
// body
float rr = ra - rb;
float hy = m0 + rr*rr;
float k2 = m0*m0 - m2*m2*hy;
float k1 = m0*m0*m3 - m1*m2*hy + m0*ra*(rr*m2*1.0 );
float k0 = m0*m0*m5 - m1*m1*hy + m0*ra*(rr*m1*2.0 - m0*ra);
float h = k1*k1 - k2*k0;
if( h<0.0 ) return float4(-1.0, -1.0, -1.0, -1.0); //no intersection
float t = (-k1-sqrt(h))/k2;
float y = m1 + t*m2;
if( y<0.0 || y>m0 ) return float4(-1.0, -1.0, -1.0, -1.0); //no intersection
return float4(t, normalize(m0*(m0*(oa+t*rd)+rr*ba*ra)-ba*hy*y));
}
void s2h_drawAABB(inout Context3D context, float3 center, float3 halfSize, float4 color)
{
float3 normal;
float2 hit = s2h_boxIntersection(context.ro - center, context.rd, halfSize, normal);
if(hit.y > 0.0f && hit.x < context.depth)
{
context.depth = hit.x;
context.dstColor = color;
context.dstColor = lerp(context.dstColor, float4(normal * 0.5f + 0.5f, 1), 0.3f);
}
}
void s2h_drawLineWS(inout Context3D context, float3 from, float3 to, float4 color, float thickness)
{
float2 hit = s2h_cylIntersect(context.ro, context.rd, from, to, thickness).xy;
if(hit.x > 0.0 && hit.x < context.depth)
{
context.depth = hit.x;
float3 p = context.ro + context.depth * context.rd;
float3 normal = s2h_cylNormal(p, from, to, thickness);
// todo: refine shading
color.rgb = lerp(color.rgb, normal * 0.5f + 0.5f, 0.3f);
context.dstColor = color;
}
}
void s2h_drawArrowWS(inout Context3D context, float3 from, float3 to, float4 color, float thickness)
{
float4 hit = s2h_coneIntersect(context.ro, context.rd, from, to, thickness, 0.0f);
if(hit.x > 0.0 && hit.x < context.depth)
{
context.depth = hit.x;
float3 normal = hit.yzw;
// todo: refine shading
color.rgb = lerp(color.rgb, normal * 0.5 + 0.5, 0.3);
context.dstColor = color;
}
}
void s2h_drawBasis(inout Context3D context, float4x4 worldFromObject, float r)
{
float4 oHom = mul(worldFromObject, float4(0, 0, 0, 1));
float4 xHom = mul(worldFromObject, float4(r, 0, 0, 1));
float4 yHom = mul(worldFromObject, float4(0, r, 0, 1));
float4 zHom = mul(worldFromObject, float4(0, 0, r, 1));
float3 o = oHom.xyz / oHom.w;
float3 x = xHom.xyz / xHom.w;
float3 y = yHom.xyz / yHom.w;
float3 z = zHom.xyz / zHom.w;
s2h_drawArrowWS(context, o, x, float4(1, 0, 0, 1), 0.09f);
s2h_drawArrowWS(context, o, y, float4(0, 1, 0, 1), 0.09f);
s2h_drawArrowWS(context, o, z, float4(0, 0, 1, 1), 0.09f);
}
void s2h_drawSphereWS(inout Context3D context, float3 pos, float4 color, float radius)
{
float2 hit = s2h_sphIntersect(context.ro, context.rd, pos, radius);
if(hit.x > 0.0 && hit.x < context.depth)
{
float3 hitPos = context.ro + hit.x * context.rd;
float3 normal = normalize(hitPos - pos);
context.depth = hit.x;
// todo: refine shading
color.rgb = lerp(color.rgb, normal * 0.5 + 0.5, 0.3);
context.dstColor = color;
}
}
void s2h_drawCheckerBoard(inout Context3D context, float3 offset)
{
float3 pos = float3(0, -0.2, 0) + offset;
float3 size = float3(4.4, 0.2, 4.4);
float3 normal;
float2 hit = s2h_boxIntersection(context.ro - pos, context.rd, size, normal);
if(hit.y > 0.0f && hit.x < context.depth)
{
context.depth = hit.x;
float3 hitPos = context.ro + hit.x * context.rd;
float2 uv = hitPos.zx;
float value = 1.0f;
if(abs(uv.x) < 4.0f && abs(uv.y) < 4.0f)
value = frac(floor(uv.x) * 0.5f + floor(uv.y) * 0.5f) > 0.25f ? 0.4f : 0.6f;
context.dstColor = float4(value, value, value, 1);
if(abs(uv.x) < (4.0f - uv.y) * 0.1f && uv.y > 0.0f)
context.dstColor.rgb = float3(1,0,0);
if(abs(uv.y) < (4.0f - uv.x) * 0.1f && uv.x > 0.0f)
context.dstColor.rgb = float3(0,0,1);
if(dot(uv, uv) < 0.25f)
context.dstColor.rgb = float3(0,1,0);
context.dstColor = lerp(context.dstColor, float4(normal * 0.5f + 0.5f, 1), 0.3f);
}
}
void s2h_drawSkybox(inout Context3D context)
{
if(context.depth == S2H_FLT_MAX)
{
float3 d = context.rd;
float pi = 3.14159265f;
// assuming normalized rd
float2 uv = float2(-atan2(d.z, d.x) / pi + 1.0, acos(d.y) / pi);
float2 px = uv * float2(s2h_fontSize() * 8.0, s2h_fontSize() * 4.0);
float tileX = s2h_fontSize() * 4.0;
// 4*4 characters around the x axis
ContextGather ui;
s2h_init(ui, float2(frac(px.x / tileX + 0.5f) * tileX, px.y));
// horizon
ui.dstColor.rgb = float3(1,1,1) * saturate(1.0f - pow(abs(d.y), 0.2f));
ui.dstColor.a = 1.0f;
// grid
{
float2 gridXY = frac(ui.pxPos);
gridXY = min(gridXY, float2(1.0f, 1.0f) - gridXY);
// 0 .. 0.5
float grid = min(gridXY.x, gridXY.y);
ui.dstColor.rgb = lerp(ui.dstColor.rgb, float3(1,1,1), 0.07f * saturate(1.0f - grid * 30.0f));
}
bool xzAxis = abs(d.x) > abs(d.z);
bool posAxis = xzAxis ? (d.x > 0.0f) : (d.z > 0.0f);
s2h_setCursor(ui, float2(0, 12));
ui.textColor.rgb = xzAxis ? float3(1, 0, 0) : float3(0, 0, 1);
ui.textColor.a = 0.4f;
s2h_printTxt(ui, _SPACE);
s2h_printTxt(ui, posAxis ? _PLUS : _MINUS);
s2h_printTxt(ui, xzAxis ? _X : _Z);
context.dstColor = ui.dstColor;
}
}
void scene(inout Context3D context);
void sceneWithShadows(inout Context3D context)
{
scene(context);
float4 litScene = context.dstColor;
// shadow ray, experiment, todo: expose light direction
if(context.depth < S2H_FLT_MAX)
{
// 0..1
float visible;
{
const float bias = 0.001;
Context3D shadowContext;
s2h_init(shadowContext, context.ro + context.depth * context.rd, normalize(float3(1.0,3.0,2.0)));
shadowContext.ro += bias * shadowContext.rd;
scene(shadowContext);
visible = shadowContext.depth == S2H_FLT_MAX ? 1.0 : 0.0;
}
// shadows are grey, todo: expose ambient color
float shadowFactor = 0.5 - visible * 0.5;
context.dstColor.rgb = lerp(litScene.rgb, float3(0.0, 0.0, 0.0), shadowFactor);
}
}
#endif // S2H_3D_INCLUDE

View File

@@ -0,0 +1,245 @@
//////////////////////////////////////////////////////////////////////////
// Shader To Human (S2H) - HLSL/GLSL library for debugging shaders //
// Copyright (c) 2024-2025 Electronic Arts Inc. All rights reserved. //
//////////////////////////////////////////////////////////////////////////
// Example:
// #include "s2h.h"
// #include "s2h_scatter.h"
// {
// struct ContextScatter ui;
// s2h_init(ui);
// s2h_printTxt(ui, _A, _B);
// }
// void onGfxForAllScatter(int2 pxPos, float4 color)
// {
// g_computeOutput[pxPos] = color;
// }
#ifndef S2H_SCATTER_INCLUDE
#define S2H_SCATTER_INCLUDE
// documentation:
struct ContextScatter
{
// RGBA, alpha 1 is assumed to be opaque
float4 textColor;
// private, for internal use, might change --------
// in pixels
int2 pxCursor;
// window left top, set by s2h_init()
int pxLeftX;
// 1/2/3/4, call s2h_setScale()
int scale;
};
// first call this
void s2h_init(out ContextScatter ui);
// set text cursor position, next printLF() will reset to this x position
void s2h_setCursor(inout ContextScatter ui, float2 inpxLeftTop);
// @param scale 1:pixel perfect, 2:2x, 3:3x, ..
void s2h_setScale(inout ContextScatter ui, uint scale);
// e.g. ui.s2h_printTxt('I', ' ', 'a', 'm');
// @param a ascii character or 0
void s2h_printTxt(inout ContextScatter ui, uint a, uint b, uint c, uint d, uint e, uint f);
// jump to next line
void s2h_printLF(inout ContextScatter ui);
// @param value e.g. 123, 0
void s2h_printInt(inout ContextScatter ui, int value);
// print hexadecimal e.g. "0000aa34"
// @param value 32bit e.g. 0x123, 0xff00
void s2h_printHex(inout ContextScatter ui, uint value);
// @param output e.g. g_output from RWTexture2D<float3> g_output : register(u0, space0);
// @param pos in pixels from left top, left top of the printout
// @param value
void s2h_printFloat(inout ContextScatter ui, float value);
// block in a 8x8 character
void s2h_printBlock(inout ContextScatter ui, float4 color);
// circle in a 8x8 character
void s2h_printDisc(inout ContextScatter ui, float4 color);
// don't use directly
void s2h_printCharacter(inout ContextScatter ui, uint ascii);
// no AA
void s2h_drawCrosshair(inout ContextScatter ui, float2 pxCenter, float pxRadius, float4 color);
// implementation ----------------------------------------------------------------------
void s2h_init(out ContextScatter ui)
{
// white, opaque
ui.textColor = float4(1, 1, 1, 1);
ui.pxCursor = int2(0, 0);
ui.pxLeftX = ui.pxCursor.x;
ui.scale = 1;
}
void s2h_setCursor(inout ContextScatter ui, float2 inpxLeftTop)
{
ui.pxCursor = inpxLeftTop;
ui.pxLeftX = inpxLeftTop.x;
}
void s2h_setScale(inout ContextScatter ui, uint scale)
{
ui.scale = scale;
}
// implement this in your code
void onGfxForAllScatter(int2 pxPos, float4 color);
void s2h_printCharacter(inout ContextScatter ui, uint ascii)
{
[loop] for(int y = 0; y < 8 * ui.scale; ++y)
[loop] for(int x = 0; x < 8 * ui.scale; ++x)
if(s2h_fontLookup(ascii, int2(x, y) / ui.scale))
onGfxForAllScatter(ui.pxCursor + int2(x, y), ui.textColor);
ui.pxCursor.x += 8 * ui.scale;
}
void s2h_drawCrosshair(inout ContextScatter ui, float2 pxCenter, float pxRadius, float4 color)
{
// avoiding int math for better performance
onGfxForAllScatter(pxCenter, ui.textColor);
[loop] for(float i = 1; i < pxRadius; ++i)
{
onGfxForAllScatter(pxCenter + float2( i, 0), color);
onGfxForAllScatter(pxCenter+ float2(-i, 0), color);
onGfxForAllScatter(pxCenter+ float2( 0, i), color);
onGfxForAllScatter(pxCenter + float2( 0, -i), color);
}
}
void s2h_printTxt(inout ContextScatter ui, uint a)
{
s2h_printCharacter(ui, a);
}
// glsl has no default arguments to we implement multiple functions instead making porting easier
void s2h_printTxt(inout ContextScatter ui, uint a, uint b)
{ s2h_printTxt(ui, a); s2h_printCharacter(ui, b); }
void s2h_printTxt(inout ContextScatter ui, uint a, uint b, uint c)
{ s2h_printTxt(ui, a, b); s2h_printCharacter(ui, c); }
void s2h_printTxt(inout ContextScatter ui, uint a, uint b, uint c, uint d)
{ s2h_printTxt(ui, a, b, c); s2h_printCharacter(ui, d); }
void s2h_printTxt(inout ContextScatter ui, uint a, uint b, uint c, uint d, uint e)
{ s2h_printTxt(ui, a, b, c, d); s2h_printCharacter(ui, e); }
void s2h_printTxt(inout ContextScatter ui, uint a, uint b, uint c, uint d, uint e, uint f)
{ s2h_printTxt(ui, a, b, c, d, e); s2h_printCharacter(ui, f); }
void s2h_printLF(inout ContextScatter ui)
{
ui.pxCursor.x = ui.pxLeftX;
ui.pxCursor.y += 8 * ui.scale;
}
void s2h_printInt(inout ContextScatter ui, int value)
{
// leading '-'
if (value < 0)
{
s2h_printCharacter(ui, '-');
value = -value;
}
if (value == 0)
{
s2h_printCharacter(ui, '0');
return;
}
// move to right depending on number length
{
uint tmp = (uint)value;
while (tmp != 0u)
{
ui.pxCursor.x += 8 * ui.scale;
tmp /= 10u;
}
}
// digits
{
float backup = ui.pxCursor.x;
uint tmp = (uint)value;
while (tmp != 0u)
{
// 0..9
uint digit = tmp % 10u;
tmp /= 10u;
// go backwards
ui.pxCursor.x -= 8 * ui.scale;
s2h_printCharacter(ui, '0' + digit);
// counter +=8 from printCharacter ()
ui.pxCursor.x -= 8 * ui.scale;
}
ui.pxCursor.x = backup;
}
}
void s2h_printHex(inout ContextScatter ui, uint value)
{
// 8 nibbles
for(int i = 7; i >= 0; --i)
{
// 0..15
uint nibble = (value >> (i * 4)) & 0xf;
uint start = (nibble < 10) ? '0' : ('A' - 10);
s2h_printCharacter(ui, start + nibble);
}
}
void s2h_printFloat(inout ContextScatter ui, float value)
{
s2h_printInt(ui, int(value));
float fractional = frac(abs(value));
s2h_printCharacter(ui, '.');
uint digitCount = 3u;
// todo: unit tests, this is likely wrong at lower precision
// fractional digits
for(uint i = 0u; i < digitCount; ++i)
{
fractional *= 10.0f;
// 0..9
uint digit = uint(fractional);
fractional = frac(fractional);
s2h_printCharacter(ui, '0' + digit);
}
}
void s2h_printBlock(inout ContextScatter ui, float4 color)
{
[loop] for(int y = 0; y < 8 * ui.scale; ++y)
[loop] for(int x = 0; x < 8 * ui.scale; ++x)
{
float2 pxLocal = (float2(x, y) ) / ui.scale - float2(3.5f, 3.5f);
float mask = saturate(4 - max(abs(pxLocal.x), abs(pxLocal.y)));
if(mask)
onGfxForAllScatter(ui.pxCursor + int2(x,y), color);
}
ui.pxCursor.x += 8 * ui.scale;
}
void s2h_printDisc(inout ContextScatter ui, float4 color)
{
[loop] for(int y = 0; y < 8 * ui.scale; ++y)
[loop] for(int x = 0; x < 8 * ui.scale; ++x)
{
float2 pxLocal = (float2(x, y) ) / ui.scale - float2(3.5f, 3.5f);
float mask = saturate(4 - length(pxLocal));
if(mask)
onGfxForAllScatter(ui.pxCursor + int2(x,y), color);
}
ui.pxCursor.x += 8 * ui.scale;
}
#endif // S2H_SCATTER_INCLUDE

View File

@@ -1,4 +1,6 @@
using Ghost.Graphics.Core;
// Source: https://github.com/zeux/meshoptimizer/blob/master/demo/clusterlod.h
// Translated from C++ to C#.
using Ghost.MeshOptimizer;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
@@ -172,9 +174,9 @@ public static unsafe class MeshletUtility
};
}
private static ClodBounds MergeBounds(UnsafeList<Cluster> clusters, UnsafeList<int> group)
private static ClodBounds MergeBounds(UnsafeList<Cluster> clusters, UnsafeList<int> group, AllocationHandle handle)
{
using var boundsList = new UnsafeArray<ClodBounds>(group.Count, Allocator.FreeList);
using var boundsList = new UnsafeArray<ClodBounds>(group.Count, handle);
for (var j = 0; j < group.Count; j++)
{
boundsList[j] = (clusters[group[j]].bounds);
@@ -202,13 +204,13 @@ public static unsafe class MeshletUtility
};
}
private static UnsafeList<Cluster> Clusterize(ref readonly ClodConfig config, ref readonly ClodMesh mesh, uint* indices, nuint indexCount, Allocator allocator)
private static UnsafeList<Cluster> Clusterize(ref readonly ClodConfig config, ref readonly ClodMesh mesh, uint* indices, nuint indexCount, AllocationHandle handle)
{
var maxMeshlets = MeshOptApi.BuildMeshletsBound(indexCount, config.maxVertices, config.minTriangles);
using var meshlets = new UnsafeArray<meshopt_Meshlet>((int)maxMeshlets, Allocator.FreeList);
using var meshletVertices = new UnsafeArray<uint>((int)indexCount, Allocator.FreeList);
using var meshletTriangles = new UnsafeArray<byte>((int)indexCount, Allocator.FreeList);
using var meshlets = new UnsafeArray<meshopt_Meshlet>((int)maxMeshlets, handle);
using var meshletVertices = new UnsafeArray<uint>((int)indexCount, handle);
using var meshletTriangles = new UnsafeArray<byte>((int)indexCount, handle);
var pMeshlets = (meshopt_Meshlet*)meshlets.GetUnsafePtr();
var pMeshletVertices = (uint*)meshletVertices.GetUnsafePtr();
@@ -236,7 +238,7 @@ public static unsafe class MeshletUtility
);
}
var clusters = new UnsafeList<Cluster>((int)meshletCount, allocator);
var clusters = new UnsafeList<Cluster>((int)meshletCount, handle);
for (nuint i = 0; i < meshletCount; i++)
{
@@ -255,9 +257,9 @@ public static unsafe class MeshletUtility
var cluster = new Cluster
{
vertices = meshlet.vertex_count,
indices = new UnsafeList<uint>((int)(meshlet.triangle_count * 3), Allocator.FreeList),
uniqueVertices = new UnsafeList<uint>((int)meshlet.vertex_count, Allocator.FreeList),
localIndices = new UnsafeList<byte>((int)(meshlet.triangle_count * 3), Allocator.FreeList),
indices = new UnsafeList<uint>((int)(meshlet.triangle_count * 3), handle),
uniqueVertices = new UnsafeList<uint>((int)meshlet.vertex_count, handle),
localIndices = new UnsafeList<byte>((int)(meshlet.triangle_count * 3), handle),
group = -1,
refined = -1
};
@@ -324,16 +326,16 @@ public static unsafe class MeshletUtility
}
}
private static UnsafeList<UnsafeList<int>> Partition(ref readonly ClodConfig config, ref readonly ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> pending, UnsafeArray<uint> remap, Allocator allocator)
private static UnsafeList<UnsafeList<int>> Partition(ref readonly ClodConfig config, ref readonly ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> pending, UnsafeArray<uint> remap, AllocationHandle handle)
{
if (pending.Count <= (int)config.partitionSize)
{
var single = new UnsafeList<UnsafeList<int>>(1, allocator);
var pendingcpy = new UnsafeList<int>(pending.Count, Allocator.FreeList);
var single = new UnsafeList<UnsafeList<int>>(1, handle);
var pendingcpy = new UnsafeList<int>(pending.Count, handle);
pendingcpy.AddRange(pending.AsSpan());
single.Add(pendingcpy);
return single;
}
@@ -343,8 +345,8 @@ public static unsafe class MeshletUtility
totalIndexCount += (nuint)clusters[pending[i]].indices.Count;
}
using var clusterIndices = new UnsafeList<uint>((int)totalIndexCount, Allocator.FreeList);
using var clusterCounts = new UnsafeList<uint>(pending.Count, Allocator.FreeList);
using var clusterIndices = new UnsafeList<uint>((int)totalIndexCount, handle);
using var clusterCounts = new UnsafeList<uint>(pending.Count, handle);
nuint offset = 0;
for (var i = 0; i < pending.Count; i++)
@@ -359,7 +361,7 @@ public static unsafe class MeshletUtility
offset += (nuint)cluster.indices.Count;
}
using var clusterPart = new UnsafeArray<uint>(pending.Count, Allocator.FreeList);
using var clusterPart = new UnsafeArray<uint>(pending.Count, handle);
var partitionCount = MeshOptApi.PartitionClusters(
(uint*)clusterPart.GetUnsafePtr(),
@@ -373,10 +375,10 @@ public static unsafe class MeshletUtility
config.partitionSize
);
var partitions = new UnsafeList<UnsafeList<int>>((int)partitionCount, allocator);
var partitions = new UnsafeList<UnsafeList<int>>((int)partitionCount, handle);
for (nuint i = 0; i < partitionCount; i++)
{
partitions.Add(new UnsafeList<int>((int)(config.partitionSize + config.partitionSize / 3), allocator));
partitions.Add(new UnsafeList<int>((int)(config.partitionSize + config.partitionSize / 3), handle));
}
for (var i = 0; i < pending.Count; i++)
@@ -387,9 +389,9 @@ public static unsafe class MeshletUtility
return partitions;
}
private static int OutputGroup(ref readonly ClodConfig config, ref readonly ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> group, ClodBounds simplified, int depth, void* outputContext, ClodOutputDelegate? outputCallback)
private static int OutputGroup(ref readonly ClodConfig config, ref readonly ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> group, ClodBounds simplified, int depth, void* outputContext, ClodOutputDelegate? outputCallback, AllocationHandle handle)
{
using var groupClusters = new UnsafeList<ClodCluster>(group.Count, Allocator.FreeList);
using var groupClusters = new UnsafeList<ClodCluster>(group.Count, handle);
for (var i = 0; i < group.Count; i++)
{
@@ -423,35 +425,35 @@ public static unsafe class MeshletUtility
public uint id;
}
private static void SimplifyFallback(ref UnsafeArray<uint> lod, ref readonly ClodMesh mesh, ReadOnlyUnsafeCollection<uint> indices, ReadOnlyUnsafeCollection<byte> locks, nuint target_count, float* error)
private static void SimplifyFallback(ref UnsafeArray<uint> lod, ref readonly ClodMesh mesh, ReadOnlyUnsafeCollection<uint> indices, ReadOnlyUnsafeCollection<byte> locks, nuint target_count, float* error, AllocationHandle handle)
{
using var subset = new UnsafeArray<SloppyVertex>(indices.Count, Allocator.FreeList);
using var subset_locks = new UnsafeArray<byte>(indices.Count, Allocator.FreeList);
using var subset = new UnsafeArray<SloppyVertex>(indices.Count, handle);
using var subset_locks = new UnsafeArray<byte>(indices.Count, handle);
lod.Resize(indices.Count);
var positions_stride = mesh.vertexPositionsStride / sizeof(float);
var positions_stride = mesh.vertexPositionsStride / sizeof(float);
// deindex the mesh subset to avoid calling simplifySloppy on the entire vertex buffer (which is prohibitively expensive without sparsity)
for (var i = 0; i<indices.Count; ++i)
{
var v = indices[i];
Debug.Assert(v<mesh.vertexCount);
// deindex the mesh subset to avoid calling simplifySloppy on the entire vertex buffer (which is prohibitively expensive without sparsity)
for (var i = 0; i < indices.Count; ++i)
{
var v = indices[i];
Debug.Assert(v < mesh.vertexCount);
subset[i].x = mesh.vertexPositions[v * positions_stride + 0];
subset[i].y = mesh.vertexPositions[v * positions_stride + 1];
subset[i].z = mesh.vertexPositions[v * positions_stride + 2];
subset[i].id = v;
subset[i].y = mesh.vertexPositions[v * positions_stride + 1];
subset[i].z = mesh.vertexPositions[v * positions_stride + 2];
subset[i].id = v;
subset_locks[i] = locks[v];
lod[i] = (uint)i;
subset_locks[i] = locks[v];
lod[i] = (uint)i;
}
var newSize = MeshOptApi.SimplifySloppy((uint*)lod.GetUnsafePtr(), (uint*)lod.GetUnsafePtr(), (nuint)lod.Count, (float*)subset.GetUnsafePtr(), (nuint)subset.Count, (nuint)sizeof(SloppyVertex), (byte*)subset_locks.GetUnsafePtr(), target_count, float.MaxValue, error);
lod.Resize((int)newSize);
// convert error to absolute
* error *= MeshOptApi.SimplifyScale((float*)subset.GetUnsafePtr(), (nuint)subset.Count, (nuint)sizeof(SloppyVertex));
*error *= MeshOptApi.SimplifyScale((float*)subset.GetUnsafePtr(), (nuint)subset.Count, (nuint)sizeof(SloppyVertex));
// restore original vertex indices
for (var i = 0; i < lod.Count; ++i)
@@ -460,9 +462,9 @@ public static unsafe class MeshletUtility
}
}
public static UnsafeArray<uint> Simplify(ref readonly ClodConfig config, ref readonly ClodMesh mesh, ReadOnlyUnsafeCollection<uint> indices, ReadOnlyUnsafeCollection<byte> locks, nuint targetCount, float* error, Allocator allocator)
public static UnsafeArray<uint> Simplify(ref readonly ClodConfig config, ref readonly ClodMesh mesh, ReadOnlyUnsafeCollection<uint> indices, ReadOnlyUnsafeCollection<byte> locks, nuint targetCount, float* error, AllocationHandle handle)
{
var lod = new UnsafeArray<uint>(indices.Count, allocator);
var lod = new UnsafeArray<uint>(indices.Count, handle);
if (targetCount >= (nuint)indices.Count)
{
@@ -527,7 +529,7 @@ public static unsafe class MeshletUtility
if ((nuint)lod.Length > targetCount && config.simplifyFallbackSloppy)
{
SimplifyFallback(ref lod, in mesh, indices, locks, targetCount, error);
SimplifyFallback(ref lod, in mesh, indices, locks, targetCount, error, handle);
*error *= config.simplifyErrorFactorSloppy;
}
@@ -575,8 +577,13 @@ public static unsafe class MeshletUtility
{
Debug.Assert(mesh.vertexAttributesStride % sizeof(float) == 0, "vertexAttributesStride must be a multiple of sizeof(float)");
using var locks = new UnsafeArray<byte>((int)mesh.vertexCount, Allocator.FreeList, AllocationOption.Clear);
using var remap = new UnsafeArray<uint>((int)mesh.vertexCount, Allocator.FreeList);
using var pool = new MemoryPool<VirtualArena, VirtualArena.CreationOptions>(new VirtualArena.CreationOptions
{
reserveCapacity = 256 * 1024 * 1024
});
using var locks = new UnsafeArray<byte>((int)mesh.vertexCount, pool.AllocationHandle, AllocationOption.Clear); ;
using var remap = new UnsafeArray<uint>((int)mesh.vertexCount, pool.AllocationHandle);
MeshOptApi.GeneratePositionRemap((uint*)remap.GetUnsafePtr(), mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride);
@@ -599,14 +606,14 @@ public static unsafe class MeshletUtility
}
}
using var clusters = Clusterize(in config, in mesh, mesh.indices, mesh.indexCount, Allocator.FreeList);
using var clusters = Clusterize(in config, in mesh, mesh.indices, mesh.indexCount, pool.AllocationHandle);
for (var i = 0; i < clusters.Count; i++)
{
clusters[i].bounds = ComputeBounds(in mesh, clusters[i].indices, 0.0f);
}
using var pending = new UnsafeList<int>(clusters.Count, Allocator.FreeList);
using var pending = new UnsafeList<int>(clusters.Count, pool.AllocationHandle);
for (var i = 0; i < clusters.Count; i++)
{
pending.Add(i);
@@ -616,14 +623,14 @@ public static unsafe class MeshletUtility
while (pending.Count > 1)
{
using var groups = Partition(in config, in mesh, clusters, pending, remap, Allocator.FreeList);
using var groups = Partition(in config, in mesh, clusters, pending, remap, pool.AllocationHandle);
pending.Clear();
LockBoundary(locks, groups, clusters, remap, mesh.vertexLock);
for (var i = 0; i < groups.Count; i++)
{
using var merged = new UnsafeList<uint>(groups[i].Count * (int)config.maxTriangles * 3, Allocator.FreeList);
using var merged = new UnsafeList<uint>(groups[i].Count * (int)config.maxTriangles * 3, pool.AllocationHandle);
for (var j = 0; j < groups[i].Count; j++)
{
var clusterIndices = clusters[groups[i][j]].indices;
@@ -631,28 +638,28 @@ public static unsafe class MeshletUtility
}
var targetSize = (nuint)(merged.Count / 3 * config.simplifyRatio * 3.0f);
var bounds = MergeBounds(clusters, groups[i]);
var bounds = MergeBounds(clusters, groups[i], pool.AllocationHandle);
var error = 0.0f;
using var simplified = Simplify(in config, in mesh, merged.AsReadOnly(), locks.AsReadOnly(), targetSize, &error, Allocator.FreeList);
using var simplified = Simplify(in config, in mesh, merged.AsReadOnly(), locks.AsReadOnly(), targetSize, &error, pool.AllocationHandle);
if ((nuint)simplified.Length > (nuint)(merged.Count * config.simplifyThreshold))
{
bounds.error = float.MaxValue;
OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback);
OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback, pool.AllocationHandle);
continue;
}
bounds.error = Math.Max(bounds.error * config.simplifyErrorMergePrevious, error) + error * config.simplifyErrorMergeAdditive;
var refined = OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback);
var refined = OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback, pool.AllocationHandle);
for (var j = 0; j < groups[i].Count; j++)
{
clusters[groups[i][j]].Dispose();
}
using var split = Clusterize(in config, in mesh, (uint*)simplified.GetUnsafePtr(), (nuint)simplified.Length, Allocator.FreeList);
using var split = Clusterize(in config, in mesh, (uint*)simplified.GetUnsafePtr(), (nuint)simplified.Length, pool.AllocationHandle);
for (var j = 0; j < split.Count; j++)
{
split[j].refined = refined;
@@ -674,7 +681,7 @@ public static unsafe class MeshletUtility
{
var bounds = clusters[pending[0]].bounds;
bounds.error = float.MaxValue;
OutputGroup(in config, in mesh, clusters, pending, bounds, depth, outputContext, outputCallback);
OutputGroup(in config, in mesh, clusters, pending, bounds, depth, outputContext, outputCallback, pool.AllocationHandle);
}
var finalClusterCount = (nuint)clusters.Count;

View File

@@ -53,11 +53,11 @@ shader "MyShader/Standard"
MeshData meshData = LoadData<MeshData>(instanceData.meshBuffer, 0);
ByteAddressBuffer meshletBuffer = GET_BUFFER(meshData.meshletBuffer);
Meshlet m = meshletBuffer.Load<Meshlet>(groupID.x * sizeof(Meshlet));
Meshlet meshlet = meshletBuffer.Load<Meshlet>(groupID.x * sizeof(Meshlet));
s_Payload.meshletIndex = groupID.x;
uint lodLevel = (m.packedCounts >> 24) & 0xFFu;
uint lodLevel = (meshlet.packedCounts >> 24) & 0xFFu;
uint emitMeshlet = lodLevel == 0u ? 1u : 0u;
DispatchMesh(emitMeshlet, 1u, 1u, s_Payload);
}
@@ -74,10 +74,10 @@ shader "MyShader/Standard"
MeshData meshData = LoadData<MeshData>(instanceData.meshBuffer, 0);
ByteAddressBuffer meshletBuffer = GET_BUFFER(meshData.meshletBuffer);
Meshlet m = meshletBuffer.Load<Meshlet>(meshPayload.meshletIndex * sizeof(Meshlet));
Meshlet meshlet = meshletBuffer.Load<Meshlet>(meshPayload.meshletIndex * sizeof(Meshlet));
uint vertexCount = m.packedCounts & 0xFFu;
uint triangleCount = (m.packedCounts >> 8) & 0xFFu;
uint vertexCount = meshlet.packedCounts & 0xFFu;
uint triangleCount = (meshlet.packedCounts >> 8) & 0xFFu;
SetMeshOutputCounts(vertexCount, triangleCount);
ByteAddressBuffer meshletVerticesBuffer = GET_BUFFER(meshData.meshletVerticesBuffer);
@@ -86,7 +86,7 @@ shader "MyShader/Standard"
// Write vertex output
if (groupThreadID.x < vertexCount)
{
uint vertexIndex = meshletVerticesBuffer.Load((m.vertexOffset + groupThreadID.x) * 4);
uint vertexIndex = meshletVerticesBuffer.Load((meshlet.vertexOffset + groupThreadID.x) * 4);
ByteAddressBuffer vertices = GET_BUFFER(meshData.vertexBuffer);
Vertex v = vertices.Load<Vertex>(vertexIndex * sizeof(Vertex));
@@ -112,7 +112,7 @@ shader "MyShader/Standard"
uint triangleIndex = groupThreadID.x;
// Load the packed 32-bit integer containing the 3 local indices
uint packedIndices = meshletTrianglesBuffer.Load((m.triangleOffset + triangleIndex) * 4);
uint packedIndices = meshletTrianglesBuffer.Load((meshlet.triangleOffset + triangleIndex) * 4);
uint i0 = packedIndices & 0xFF;
uint i1 = (packedIndices >> 8) & 0xFF;