feat(meshlet): refactor meshlet pipeline and add render pass

Refactor meshlet data structures to use packed uint triangle indices, update meshlet cooking and upload logic, and align HLSL mesh shader. Add MeshRenderPass with bindless rendering and blit support. Improve RenderExtractionSystem, RootSignatureLayout, and TestRenderPipeline. Update GraphicsTestWindow for new pipeline and meshlet logic. Includes code cleanups and comments.
This commit is contained in:
2026-03-25 13:13:03 +09:00
parent 7860e5e341
commit b729ca86f5
12 changed files with 455 additions and 120 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@
*.userosscache *.userosscache
*.sln.docstates *.sln.docstates
AGENTS.md AGENTS.md
ref/
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs *.userprefs

View File

@@ -78,7 +78,6 @@ public class RenderExtractionSystem : ISystem
ref var cameraQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_cameraQueryID); ref var cameraQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_cameraQueryID);
ref var meshQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_meshQueryID); ref var meshQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_meshQueryID);
// TODO: We should extract the render record for each camera because different cameras may have different culling results.
foreach (var (cam, camLtw) in cameraQuery.GetComponentIterator<Camera, LocalToWorld>()) foreach (var (cam, camLtw) in cameraQuery.GetComponentIterator<Camera, LocalToWorld>())
{ {
ref readonly var camRef = ref cam.Get(); ref readonly var camRef = ref cam.Get();
@@ -108,6 +107,7 @@ public class RenderExtractionSystem : ISystem
ref readonly var meshInstance = ref meshInstances[i]; ref readonly var meshInstance = ref meshInstances[i];
if ((meshInstance.renderingLayerMask & camRef.renderingLayerMask) == 0u) if ((meshInstance.renderingLayerMask & camRef.renderingLayerMask) == 0u)
{ {
// Not in the same rendering layer, skip.
continue; continue;
} }
@@ -117,6 +117,7 @@ public class RenderExtractionSystem : ISystem
var camPosition = camLtwRef.matrix.c3.xyz; var camPosition = camLtwRef.matrix.c3.xyz;
var distance = math.distance(meshPosition, camPosition); var distance = math.distance(meshPosition, camPosition);
// TODO: Use bounding sphere or AABB for better culling. Currently it just uses the pivot point which can cause popping when the pivot is far from the actual geometry.
if (distance < camRef.nearClipPlane || distance > camRef.farClipPlane) if (distance < camRef.nearClipPlane || distance > camRef.farClipPlane)
{ {
continue; continue;

View File

@@ -3,39 +3,8 @@ using System.Runtime.InteropServices;
namespace Ghost.Graphics.RHI; namespace Ghost.Graphics.RHI;
/// <summary>
/// The layout of the root signature is:
/// <list space="bullet">
/// <item>
/// Global buffer (b0)
/// </item>
/// <item>
/// Per-view buffer (b1)
/// </item>
/// <item>
/// Per-object buffer (b2)
/// </item>
/// <item>
/// Per-material buffer (b3)
/// </item>
/// <item>
/// Descriptor table for bindless textures (t0)
/// </item>
/// <item>
/// Descriptor table for bindless samplers (s0)
/// </item>
/// </list>
/// </summary>
public static class RootSignatureLayout public static class RootSignatureLayout
{ {
// public const int GLOBAL_BUFFER_SLOT = 0;
// public const int PER_VIEW_BUFFER_SLOT = 1;
// public const int PER_OBJECT_BUFFER_SLOT = 2;
// public const int PER_MATERIAL_BUFFER_SLOT = 3;
// public const int TEXTURE_HEAP_SLOT = 0;
// public const int SAMPLER_HEAP_SLOT = 0;
public const int PUSH_CONSTANT_SLOT = 0; public const int PUSH_CONSTANT_SLOT = 0;
public const int ROOT_PARAMETER_COUNT = 1; public const int ROOT_PARAMETER_COUNT = 1;
@@ -46,26 +15,28 @@ public struct PushConstantsData
{ {
public uint globalIndex; public uint globalIndex;
public uint viewIndex; public uint viewIndex;
public uint instanceIndex;
public uint objectIndex; public uint objectIndex;
public uint instanceIndex;
public uint materialIndex; public uint materialIndex;
} }
[StructLayout(LayoutKind.Sequential, Size = 8)] [StructLayout(LayoutKind.Sequential, Size = 20)]
public struct GlobalFrameData public struct GlobalFrameData
{ {
public uint viewBufferIndex; public uint viewBufferIndex;
public uint instanceBufferIndex; public uint instanceBufferIndex;
public uint viewBufferCount;
public uint instanceBufferCount;
public uint userBufferIndex;
} }
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct InstanceData public struct InstanceData
{ {
public float4x4 localToWorld; public float4x4 localToWorld;
} }
// The size should be 176 bytes (16-byte aligned) [StructLayout(LayoutKind.Sequential, Pack = 4)]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct PerViewData public struct PerViewData
{ {
public float4x4 viewMatrix; public float4x4 viewMatrix;
@@ -77,15 +48,14 @@ public struct PerViewData
public float4 screenSize; // xy: size, zw: 1/size public float4 screenSize; // xy: size, zw: 1/size
}; };
// The size should be 96 bytes (16-byte aligned) [StructLayout(LayoutKind.Sequential, Pack = 4)]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct PerObjectData public struct PerObjectData
{ {
public float4x4 localToWorld;
public float3 worldBoundsMin; public float3 worldBoundsMin;
public uint vertexBuffer; public uint vertexBuffer;
public float3 worldBoundsMax; public float3 worldBoundsMax;
public uint indexBuffer; public uint indexBuffer;
public uint meshletBuffer; public uint meshletBuffer;
public uint meshletVerticesBuffer; public uint meshletVerticesBuffer;
public uint meshletTrianglesBuffer; public uint meshletTrianglesBuffer;

View File

@@ -54,7 +54,7 @@ public struct MeshletMeshData : IDisposable
public UnsafeList<MeshletGroup> groups; public UnsafeList<MeshletGroup> groups;
public UnsafeList<MeshletHierarchyNode> hierarchyNodes; public UnsafeList<MeshletHierarchyNode> hierarchyNodes;
public UnsafeList<uint> meshletVertices; public UnsafeList<uint> meshletVertices;
public UnsafeList<byte> meshletTriangles; public UnsafeList<uint> meshletTriangles;
public int lodLevelCount; public int lodLevelCount;
public int materialSlotCount; public int materialSlotCount;
@@ -247,7 +247,7 @@ public struct Mesh : IResourceReleasable
if (!data.groups.IsCreated) data.groups = new UnsafeList<MeshletGroup>(16, Allocator.Persistent); if (!data.groups.IsCreated) data.groups = new UnsafeList<MeshletGroup>(16, Allocator.Persistent);
if (!data.meshlets.IsCreated) data.meshlets = new UnsafeList<Meshlet>(64, Allocator.Persistent); if (!data.meshlets.IsCreated) data.meshlets = new UnsafeList<Meshlet>(64, Allocator.Persistent);
if (!data.meshletVertices.IsCreated) data.meshletVertices = new UnsafeList<uint>(128, Allocator.Persistent); if (!data.meshletVertices.IsCreated) data.meshletVertices = new UnsafeList<uint>(128, Allocator.Persistent);
if (!data.meshletTriangles.IsCreated) data.meshletTriangles = new UnsafeList<byte>(128, Allocator.Persistent); if (!data.meshletTriangles.IsCreated) data.meshletTriangles = new UnsafeList<uint>(128, Allocator.Persistent);
var meshletGroup = new MeshletGroup var meshletGroup = new MeshletGroup
{ {
@@ -264,23 +264,27 @@ public struct Mesh : IResourceReleasable
var meshlet = new Meshlet var meshlet = new Meshlet
{ {
vertexCount = (byte)cluster.vertexCount, vertexCount = (byte)cluster.vertexCount,
triangleCount = (byte)(cluster.indexCount / 3), triangleCount = (byte)(cluster.localIndexCount / 3),
vertexOffset = (uint)data.meshletVertices.Count, vertexOffset = (uint)data.meshletVertices.Count,
triangleOffset = (uint)data.meshletTriangles.Count, triangleOffset = (uint)data.meshletTriangles.Count,
groupIndex = (uint)data.groups.Count - 1 groupIndex = (uint)data.groups.Count - 1
}; };
data.meshlets.Add(meshlet); data.meshlets.Add(meshlet);
// Add indices // Add unique vertices
for (nuint j = 0; j < cluster.indexCount; j++) for (nuint j = 0; j < cluster.vertexCount; j++)
{ {
data.meshletVertices.Add(cluster.indices[j]); data.meshletVertices.Add(cluster.uniqueVertices[j]);
} }
// Add triangles (packed indices or byte offsets) // Add local triangles (packed into uints)
// Assuming 8-bit local indices for meshlets as per standard convention nuint triangleCount = cluster.localIndexCount / 3;
for (nuint j = 0; j < cluster.indexCount; j++) for (nuint j = 0; j < triangleCount; j++)
{ {
data.meshletTriangles.Add((byte)j); uint i0 = cluster.localIndices[j * 3 + 0];
uint i1 = cluster.localIndices[j * 3 + 1];
uint i2 = cluster.localIndices[j * 3 + 2];
uint packedTriangle = i0 | (i1 << 8) | (i2 << 16);
data.meshletTriangles.Add(packedTriangle);
} }
} }

View File

@@ -7,6 +7,7 @@ using Misaki.HighPerformance.Mathematics;
namespace Ghost.Graphics.Core; 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.
public readonly unsafe ref struct RenderingContext public readonly unsafe ref struct RenderingContext
{ {
private readonly IGraphicsEngine _engine; private readonly IGraphicsEngine _engine;
@@ -185,12 +186,11 @@ public readonly unsafe ref struct RenderingContext
MemoryType = ResourceMemoryType.Default, MemoryType = ResourceMemoryType.Default,
}; };
// Ensure size is multiple of 4 for Raw buffer // Ensure size is multiple of 4 for Raw buffer
var trianglesSize = (uint)meshletData.meshletTriangles.Count; var trianglesSize = (uint)meshletData.meshletTriangles.Count * sizeof(uint);
trianglesSize = (trianglesSize + 3u) & ~3u;
var trianglesDesc = new BufferDesc var trianglesDesc = new BufferDesc
{ {
Size = trianglesSize, Size = trianglesSize,
Stride = sizeof(byte), Stride = sizeof(uint),
Usage = BufferUsage.Raw | BufferUsage.ShaderResource, Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
MemoryType = ResourceMemoryType.Default, MemoryType = ResourceMemoryType.Default,
}; };
@@ -208,7 +208,7 @@ public readonly unsafe ref struct RenderingContext
// Padding for triangle data if needed // Padding for triangle data if needed
if (trianglesSize > meshletData.meshletTriangles.Count) if (trianglesSize > meshletData.meshletTriangles.Count)
{ {
var paddedData = new byte[trianglesSize]; var paddedData = new uint[trianglesSize];
meshletData.meshletTriangles.AsSpan().CopyTo(paddedData); meshletData.meshletTriangles.AsSpan().CopyTo(paddedData);
_directCmd.UploadBuffer(meshRef.MeshletTrianglesBuffer, paddedData.AsSpan()); _directCmd.UploadBuffer(meshRef.MeshletTrianglesBuffer, paddedData.AsSpan());
} }
@@ -222,7 +222,7 @@ public readonly unsafe ref struct RenderingContext
TransitionBarrier(meshRef.MeshletTrianglesBuffer.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, float4x4 localToWorld) public void UpdateObjectData(Handle<Mesh> mesh)
{ {
var r = _resourceManager.GetMeshReference(mesh); var r = _resourceManager.GetMeshReference(mesh);
if (r.IsFailure) if (r.IsFailure)
@@ -233,7 +233,6 @@ public readonly unsafe ref struct RenderingContext
ref readonly var meshData = ref r.Value; ref readonly var meshData = ref r.Value;
var data = new PerObjectData var data = new PerObjectData
{ {
localToWorld = localToWorld,
worldBoundsMin = meshData.BoundingBox.Min, worldBoundsMin = meshData.BoundingBox.Min,
worldBoundsMax = meshData.BoundingBox.Max, worldBoundsMax = meshData.BoundingBox.Max,
vertexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.VertexBuffer.AsResource()), vertexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.VertexBuffer.AsResource()),

View File

@@ -0,0 +1,337 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.DSL.ShaderCompiler;
using Ghost.Graphics.Core;
using Ghost.Graphics.Core.Contracts;
using Ghost.Graphics.RenderGraphModule;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Utilities;
using Misaki.HighPerformance.Image;
using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Utilities;
using System.Runtime.InteropServices;
namespace Ghost.Graphics;
internal class MeshRenderPassData
{
public Handle<Mesh> mesh;
public Handle<Material> material;
public Identifier<RGTexture> renderTarget;
}
internal class BlitPassData
{
public Identifier<RGTexture> source;
public Identifier<RGTexture> destination;
public Handle<Material> blitMaterial;
public Identifier<Sampler> sampler;
}
/// <summary>
/// Simplified bindless mesh render pass using high-level bindless APIs with fully bindless vertex/index buffer access
/// </summary>
internal class MeshRenderPass : IRenderPass
{
[StructLayout(LayoutKind.Sequential)]
private struct ShaderProperties_MyShader_Standard
{
public float4 color;
public uint texture1;
public uint texture2;
public uint texture3;
public uint texture4;
public uint tex_sampler;
private readonly uint _padding1;
private readonly uint _padding2;
private readonly uint _padding3;
}
[StructLayout(LayoutKind.Sequential)]
private struct ShaderProperties_Hidden_Blit
{
public uint mainTex;
public uint sampler_mainTex;
private readonly uint _padding1;
private readonly uint _padding2;
}
private Handle<Mesh> _mesh;
private Identifier<Shader> _shader;
private Handle<Material> _material;
private Handle<Texture>[]? _textures;
private Identifier<Sampler> _sampler;
private Identifier<Shader> _blitShader;
private Handle<Material> _blitMaterial;
// Texture file paths for this demo
private readonly string[] _textureFiles = [
"C:/Users/Misaki/Downloads/Im/Icon.png",
"C:/Users/Misaki/Downloads/Im/Backdrop.jpg",
"C:/Users/Misaki/Downloads/Im/101167591_p0.png",
"C:/Users/Misaki/Downloads/Im/yande.re 1134666 blue_archive nakamasa_ichika sugarhigh.jpg"
];
private static IEnumerable<ReadOnlyMemory<string>> GetAllVariantCombination(KeywordsGroup[] keywordsGroups)
{
if (keywordsGroups.Length == 0)
{
yield return ReadOnlyMemory<string>.Empty;
yield break;
}
var firstGroup = keywordsGroups[0];
var remainingGroups = keywordsGroups[1..];
foreach (var combination in GetAllVariantCombination(remainingGroups))
{
yield return combination;
}
foreach (var keyword in firstGroup.keywords)
{
foreach (var combination in GetAllVariantCombination(remainingGroups))
{
var array = new string[combination.Length + 1];
array[0] = keyword;
combination.Span.CopyTo(array.AsSpan(1));
yield return array;
}
}
}
private void CompileBlitShader(ref readonly RenderingContext ctx)
{
var shaderDescriptor = DSLShaderCompiler.CompileShader("F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/Shaders/Blit.gshdr", "C:/Users/Misaki/Downloads/Archive").GetValueOrThrow();
_blitShader = ctx.ResourceManager.CreateGraphicsShader(shaderDescriptor);
_blitMaterial = ctx.ResourceManager.CreateMaterial(_blitShader);
var config = new ShaderCompilationConfig
{
optimizeLevel = CompilerOptimizeLevel.O3,
options = CompilerOption.KeepReflections,
tier = CompilerTier.Tier2
};
var pass = shaderDescriptor.passes[0];
var emptyKeywords = new LocalKeywordSet();
var variantKey = RHIUtility.CreateShaderVariantKey(
RHIUtility.CreateShaderPassKey(pass.identifier),
in emptyKeywords);
ctx.ShaderCompiler.CompilePass(in pass, in config, variantKey).GetValueOrThrow();
}
public void Initialize(ref readonly RenderingContext ctx)
{
CompileBlitShader(in ctx);
var shaderDescriptor = DSLShaderCompiler.CompileShader("F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/test.gshdr", "C:/Users/Misaki/Downloads/Archive").GetValueOrThrow();
_shader = ctx.ResourceManager.CreateGraphicsShader(shaderDescriptor);
_material = ctx.ResourceManager.CreateMaterial(_shader);
for (var i = 0; i < shaderDescriptor.passes.Length; i++)
{
ref var pass = ref shaderDescriptor.passes[i];
var config = new ShaderCompilationConfig
{
optimizeLevel = CompilerOptimizeLevel.O3,
options = CompilerOption.KeepReflections,
tier = CompilerTier.Tier2
};
// TODO: Ideally, in editor mode, we compile a single variant when it's needed during rendering. Before the compilation is done, we fallback to a special "compilation in progress" shader.
// During the build process, we can precompile all the variants and store them in the cache for fast loading in runtime.
// After the compilation, we should store the compiled result in the disk cache even in editor mode. This allows us to avoid recompiling the same variant, same code hash and same version) multiple times.
if (pass.keywords.Length == 0)
{
var emptyKeywords = new LocalKeywordSet();
var variantKey = RHIUtility.CreateShaderVariantKey(
RHIUtility.CreateShaderPassKey(pass.identifier),
in emptyKeywords);
ctx.ShaderCompiler.CompilePass(in pass, in config, variantKey).GetValueOrThrow();
}
else
{
var shaderResult = ctx.ResourceManager.GetShaderReference(_shader);
if (shaderResult.IsFailure)
{
throw new InvalidOperationException("Failed to get shader reference.");
}
ref readonly var shaderRef = ref shaderResult.Value;
foreach (var keyGroup in GetAllVariantCombination(pass.keywords))
{
config.defines = keyGroup.Span;
var keywordsSet = new LocalKeywordSet();
foreach (var key in keyGroup.Span)
{
var localIndex = shaderRef.GetLocalKeywordIndex(Shader.GetKeywordID(key));
if (localIndex == -1)
{
continue;
}
keywordsSet.SetKeyword(localIndex, true);
}
var variantKey = RHIUtility.CreateShaderVariantKey(
RHIUtility.CreateShaderPassKey(pass.identifier),
in keywordsSet);
ctx.ShaderCompiler.CompilePass(in pass, in config, variantKey).GetValueOrThrow();
}
}
}
MeshBuilder.CreateCube(0.75f, default, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent, out var vertices, out var indices);
_mesh = ctx.CreateMesh(vertices, indices, true);
// Cook meshlets for the mesh
var meshRef = ctx.ResourceManager.GetMeshReference(_mesh);
if (meshRef.IsSuccess)
{
meshRef.Value.CookMeshlets();
}
ctx.UploadMeshlets(_mesh);
ctx.UpdateObjectData(_mesh);
_textures = new Handle<Texture>[_textureFiles.Length];
for (var i = 0; i < _textureFiles.Length; i++)
{
using var stream = File.OpenRead(_textureFiles[i]);
using var imageData = ImageResult.FromStream(stream, ColorComponents.RGBA);
var desc = new TextureDesc
{
Width = imageData.Width,
Height = imageData.Height,
Dimension = TextureDimension.Texture2D,
Format = TextureFormat.R8G8B8A8_UNorm,
MipLevels = 1,
Slice = 1,
Usage = TextureUsage.ShaderResource,
};
_textures[i] = ctx.CreateTexture<byte>(in desc, imageData.AsSpan(), $"Texture_{i}");
}
var samplerDesc = new SamplerDesc
{
AddressU = TextureAddressMode.Repeat,
AddressV = TextureAddressMode.Repeat,
AddressW = TextureAddressMode.Repeat,
FilterMode = TextureFilterMode.Bilinear,
MaxAnisotropy = 16,
};
_sampler = ctx.ResourceAllocator.CreateSampler(in samplerDesc);
var meshResult = ctx.ResourceManager.GetMaterialReference(_material);
if (meshResult.IsFailure)
{
throw new InvalidOperationException("Failed to get material reference.");
}
ref var matRef = ref meshResult.Value;
var matProps = new ShaderProperties_MyShader_Standard
{
color = new float4(1.0f, 1.0f, 1.0f, 1.0f),
texture1 = ctx.ResourceDatabase.GetBindlessIndex(_textures[0].AsResource()),
texture2 = ctx.ResourceDatabase.GetBindlessIndex(_textures[1].AsResource()),
texture3 = ctx.ResourceDatabase.GetBindlessIndex(_textures[2].AsResource()),
texture4 = ctx.ResourceDatabase.GetBindlessIndex(_textures[3].AsResource()),
tex_sampler = (uint)_sampler.Value,
};
matRef.SetPropertyCache(in matProps).ThrowIfFailed();
matRef.UploadData(ctx.DirectCommandBuffer, ctx.ResourceDatabase);
}
public void Build(RenderGraph graph, Identifier<RGTexture> backbuffer)
{
Identifier<RGTexture> renderTarget;
using (var builder = graph.AddRasterRenderPass<MeshRenderPassData>("Mesh Render Pass", out var passData))
{
passData.mesh = _mesh;
passData.material = _material;
passData.renderTarget = builder.CreateTexture(RGTextureDesc.Relative(1.0f, TextureFormat.R8G8B8A8_UNorm), "Render Target");
builder.SetColorAttachment(passData.renderTarget, 0);
renderTarget = passData.renderTarget;
builder.SetRenderFunc<MeshRenderPassData>(static (data, ctx) =>
{
ctx.SetActiveMaterial(data.material);
ctx.SetActiveMesh(data.mesh);
var threadGroupCountX = ((uint)ctx.ActiveMeshIndexCount + 2u) / 3u;
ctx.DispatchMesh(new uint3(threadGroupCountX, 1u, 1u));
});
}
using (var builder = graph.AddUnsafeRenderPass<BlitPassData>("Blit Pass", out var passData))
{
passData.source = renderTarget;
passData.destination = backbuffer;
passData.blitMaterial = _blitMaterial;
passData.sampler = _sampler;
builder.UseTexture(passData.source, AccessFlags.Read);
builder.UseTexture(passData.destination, AccessFlags.WriteAll);
builder.SetRenderFunc<BlitPassData>(static (data, ctx) =>
{
var r = ctx.ResourceManager.GetMaterialReference(data.blitMaterial);
if (r.IsFailure)
{
return;
}
ref var matRef = ref r.Value;
var blitProps = new ShaderProperties_Hidden_Blit
{
mainTex = ctx.ResourceDatabase.GetBindlessIndex(ctx.GetActualResource(data.source.AsResource())),
sampler_mainTex = (uint)data.sampler.Value,
};
matRef.SetPropertyCache(in blitProps).ThrowIfFailed();
matRef.UploadData(ctx.CommandBuffer, ctx.ResourceDatabase);
ctx.CommandBuffer.SetRenderTargets([ctx.GetActualTexture(data.destination)], Handle<Texture>.Invalid);
ctx.SetActiveMaterial(data.blitMaterial);
ctx.SetActiveMesh(Handle<Mesh>.Invalid); // Generate a full-screen triangle dynamically in mesh shader.
ctx.DispatchMesh(new uint3(1, 1, 1));
});
}
}
public void Cleanup(ResourceManager resourceManager, IResourceDatabase resourceDatabase)
{
resourceManager.ReleaseMaterial(_blitMaterial);
resourceManager.ReleaseMaterial(_material);
resourceManager.ReleaseShader(_shader);
resourceManager.ReleaseMesh(_mesh);
resourceDatabase.ReleaseSampler(_sampler);
if (_textures != null)
{
foreach (var texture in _textures)
{
resourceDatabase.ReleaseResource(texture.AsResource());
}
}
}
}

View File

@@ -20,7 +20,7 @@ struct Meshlet
uint packedCounts; // byte vertexCount, byte triangleCount, byte localMaterialIndex, byte lodLevel uint packedCounts; // byte vertexCount, byte triangleCount, byte localMaterialIndex, byte lodLevel
}; };
[numthreads(64, 1, 1)] // 64 threads for max 64 vertices and up to 124 triangles [numthreads(128, 1, 1)] // 128 threads to cover max 64 vertices and 124 triangles
[outputtopology("triangle")] [outputtopology("triangle")]
void MSMain( void MSMain(
uint3 groupThreadID : SV_GroupThreadID, uint3 groupThreadID : SV_GroupThreadID,
@@ -59,23 +59,12 @@ void MSMain(
} }
// Write triangle output (1 thread processes 1 triangle) // Write triangle output (1 thread processes 1 triangle)
// We could pack 3 indices in a uint or just use byte offset
// In our CPU code, we packed it as individual bytes, so 3 bytes per triangle.
// For 124 triangles, we have 372 bytes.
if (groupThreadID.x < triangleCount) if (groupThreadID.x < triangleCount)
{ {
uint triangleIndex = groupThreadID.x; uint triangleIndex = groupThreadID.x;
uint baseOffset = m.triangleOffset + triangleIndex * 3;
// Load 4 bytes to get the 3 index bytes // Load the packed 32-bit integer containing the 3 local indices
// Needs byte-aligned loading uint packedIndices = meshletTrianglesBuffer.Load((m.triangleOffset + triangleIndex) * 4);
uint wordOffset = baseOffset & ~3;
uint shift = (baseOffset & 3) * 8;
uint packedIndices1 = meshletTrianglesBuffer.Load(wordOffset);
uint packedIndices2 = meshletTrianglesBuffer.Load(wordOffset + 4);
uint64_t combined = ((uint64_t)packedIndices2 << 32) | packedIndices1;
uint packedIndices = (uint)(combined >> shift);
uint i0 = packedIndices & 0xFF; uint i0 = packedIndices & 0xFF;
uint i1 = (packedIndices >> 8) & 0xFF; uint i1 = (packedIndices >> 8) & 0xFF;

View File

@@ -28,6 +28,11 @@ internal struct RenderSystemDesc
{ {
get; set; get; set;
} }
public IRenderPipelineSettings? InitialRenderPipelineSettings
{
get; set;
}
} }
/// <summary> /// <summary>
@@ -180,7 +185,7 @@ public class RenderSystem : IDisposable
_shutdownEvent = new AutoResetEvent(false); _shutdownEvent = new AutoResetEvent(false);
_resizeRequest = new ConcurrentDictionary<ISwapChain, uint2>(); _resizeRequest = new ConcurrentDictionary<ISwapChain, uint2>();
_renderPipelineSettings = new GhostRenderPipelineSettings(); _renderPipelineSettings = _config.InitialRenderPipelineSettings ?? new GhostRenderPipelineSettings();
_renderPipeline = _renderPipelineSettings.CreatePipeline(this); _renderPipeline = _renderPipelineSettings.CreatePipeline(this);
_isRunning = false; _isRunning = false;
@@ -416,6 +421,7 @@ public class RenderSystem : IDisposable
frameResource.Dispose(); frameResource.Dispose();
} }
_renderPipeline.Dispose();
_graphicsEngine.Dispose(); _graphicsEngine.Dispose();
_shutdownEvent.Dispose(); _shutdownEvent.Dispose();

View File

@@ -1,13 +1,14 @@
#ifndef BUILTIN_PROPERTIES_HLSL #ifndef BUILTIN_PROPERTIES_HLSL
#define BUILTIN_PROPERTIES_HLSL #define BUILTIN_PROPERTIES_HLSL
#include "F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/Shaders/Includes/Common.hlsl" #include "F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/Shaders/Includes/Common.hlsl"
struct PushConstantData struct PushConstantData
{ {
BYTE_ADDRESS_BUFFER globalBuffer; BYTE_ADDRESS_BUFFER globalBuffer;
BYTE_ADDRESS_BUFFER perViewBuffer; BYTE_ADDRESS_BUFFER perViewBuffer;
BYTE_ADDRESS_BUFFER perObjectBuffer; BYTE_ADDRESS_BUFFER perObjectBuffer;
BYTE_ADDRESS_BUFFER perInstanceBuffer;
BYTE_ADDRESS_BUFFER perMaterialBuffer; BYTE_ADDRESS_BUFFER perMaterialBuffer;
}; };
@@ -24,7 +25,6 @@ struct PerViewData
struct PerObjectData struct PerObjectData
{ {
float4x4 localToWorld;
float3 worldBoundsMin; float3 worldBoundsMin;
BYTE_ADDRESS_BUFFER vertexBuffer; BYTE_ADDRESS_BUFFER vertexBuffer;
float3 worldBoundsMax; float3 worldBoundsMax;

View File

@@ -10,6 +10,8 @@ namespace Ghost.Graphics.Utilities;
internal struct Cluster : IDisposable internal struct Cluster : IDisposable
{ {
public UnsafeList<uint> indices; public UnsafeList<uint> indices;
public UnsafeList<uint> uniqueVertices;
public UnsafeList<byte> localIndices;
public ClodBounds bounds; public ClodBounds bounds;
public nuint vertices; public nuint vertices;
public int group; public int group;
@@ -17,7 +19,9 @@ internal struct Cluster : IDisposable
public void Dispose() public void Dispose()
{ {
indices.Dispose(); if (indices.IsCreated) indices.Dispose();
if (uniqueVertices.IsCreated) uniqueVertices.Dispose();
if (localIndices.IsCreated) localIndices.Dispose();
} }
} }
@@ -136,8 +140,14 @@ public unsafe struct ClodCluster
public uint* indices; public uint* indices;
/// <summary> Number of indices. </summary> /// <summary> Number of indices. </summary>
public nuint indexCount; public nuint indexCount;
/// <summary> Number of vertices in the cluster. </summary> /// <summary> Pointer to unique vertices for this cluster. </summary>
public uint* uniqueVertices;
/// <summary> Number of unique vertices in the cluster. </summary>
public nuint vertexCount; public nuint vertexCount;
/// <summary> Pointer to local triangle indices for this cluster. </summary>
public byte* localIndices;
/// <summary> Number of local indices. </summary>
public nuint localIndexCount;
} }
/// <summary> /// <summary>
@@ -244,13 +254,22 @@ public static unsafe class MeshletUtility
{ {
vertices = meshlet.vertex_count, vertices = meshlet.vertex_count,
indices = new UnsafeList<uint>((int)(meshlet.triangle_count * 3), Allocator.Persistent), indices = new UnsafeList<uint>((int)(meshlet.triangle_count * 3), Allocator.Persistent),
uniqueVertices = new UnsafeList<uint>((int)meshlet.vertex_count, Allocator.Persistent),
localIndices = new UnsafeList<byte>((int)(meshlet.triangle_count * 3), Allocator.Persistent),
group = -1, group = -1,
refined = -1 refined = -1
}; };
for (nuint j = 0; j < meshlet.vertex_count; j++)
{
cluster.uniqueVertices.Add(pMeshletVertices[meshlet.vertex_offset + j]);
}
for (nuint j = 0; j < meshlet.triangle_count * 3; j++) for (nuint j = 0; j < meshlet.triangle_count * 3; j++)
{ {
cluster.indices.Add(pMeshletVertices[meshlet.vertex_offset + pMeshletTriangles[meshlet.triangle_offset + j]]); byte localIdx = pMeshletTriangles[meshlet.triangle_offset + j];
cluster.localIndices.Add(localIdx);
cluster.indices.Add(pMeshletVertices[meshlet.vertex_offset + localIdx]);
} }
clusters.Add(cluster); clusters.Add(cluster);
@@ -377,7 +396,10 @@ public static unsafe class MeshletUtility
: srcCluster.bounds, : srcCluster.bounds,
indices = (uint*)srcCluster.indices.GetUnsafePtr(), indices = (uint*)srcCluster.indices.GetUnsafePtr(),
indexCount = (nuint)srcCluster.indices.Count, indexCount = (nuint)srcCluster.indices.Count,
vertexCount = srcCluster.vertices uniqueVertices = (uint*)srcCluster.uniqueVertices.GetUnsafePtr(),
vertexCount = srcCluster.vertices,
localIndices = (byte*)srcCluster.localIndices.GetUnsafePtr(),
localIndexCount = (nuint)srcCluster.localIndices.Count
}); });
} }

View File

@@ -1,10 +1,9 @@
using Ghost.Core;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.RenderGraphModule; using Ghost.Graphics.RenderGraphModule;
using Ghost.Graphics.RenderPipeline; using Ghost.Graphics.RenderPipeline;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Ghost.Graphics.Test.RenderPasses; namespace Ghost.Graphics.Test.RenderPasses;
@@ -18,6 +17,12 @@ public sealed class TestRenderPipelineSettings : IRenderPipelineSettings
public unsafe partial class TestRenderPipeline : IRenderPipeline public unsafe partial class TestRenderPipeline : IRenderPipeline
{ {
private class MeshletDebugPassData
{
public Identifier<RGTexture> backbuffer;
public RenderList renderList;
}
private readonly RenderGraph _renderGraph; private readonly RenderGraph _renderGraph;
private readonly RenderSystem _renderSystem; private readonly RenderSystem _renderSystem;
@@ -28,13 +33,6 @@ public unsafe partial class TestRenderPipeline : IRenderPipeline
Dispose(); Dispose();
} }
[Conditional("DEBUG")]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
internal TestRenderPipeline(RenderSystem renderSystem) internal TestRenderPipeline(RenderSystem renderSystem)
{ {
_renderSystem = renderSystem; _renderSystem = renderSystem;
@@ -56,7 +54,6 @@ public unsafe partial class TestRenderPipeline : IRenderPipeline
// 1. Allocate and populate Instance Data buffer // 1. Allocate and populate Instance Data buffer
var instanceCount = request.opaqueRenderList.TotalRecordCount; var instanceCount = request.opaqueRenderList.TotalRecordCount;
if (instanceCount == 0) if (instanceCount == 0)
{ {
continue; // Nothing to render continue; // Nothing to render
@@ -71,6 +68,7 @@ public unsafe partial class TestRenderPipeline : IRenderPipeline
MemoryType = ResourceMemoryType.Upload, // Upload directly for simplicity in testing MemoryType = ResourceMemoryType.Upload, // Upload directly for simplicity in testing
}); });
// TODO: Optimize by suballocation.
var instanceBufferHandle = resourceManager.GetPooledResource(instanceBufferDesc); var instanceBufferHandle = resourceManager.GetPooledResource(instanceBufferDesc);
var instanceBufferResource = instanceBufferHandle.AsGraphicsBuffer(); var instanceBufferResource = instanceBufferHandle.AsGraphicsBuffer();
@@ -137,6 +135,10 @@ public unsafe partial class TestRenderPipeline : IRenderPipeline
{ {
request.renderFunc(in ctx, in request); request.renderFunc(in ctx, in request);
} }
else
{
var backBuffer = _renderGraph.ImportTexture(request.colorTarget, "BackBuffer", clearAtFirstUse: false, discardAtLastUse: false);
}
// We must enqueue a return for the pooled resources so they are freed next frame. // We must enqueue a return for the pooled resources so they are freed next frame.
resourceManager.ReturnPooledResource(instanceBufferHandle); resourceManager.ReturnPooledResource(instanceBufferHandle);
@@ -145,6 +147,20 @@ public unsafe partial class TestRenderPipeline : IRenderPipeline
} }
} }
private void MeshletDebugPass(Identifier<RGTexture> backbuffer, RenderList renderList)
{
using (var builder = _renderGraph.AddRasterRenderPass<MeshletDebugPassData>("Meshlet Debug Pass", out var passData))
{
passData.renderList = renderList;
builder.SetColorAttachment(backbuffer, 0);
builder.SetRenderFunc<MeshletDebugPassData>(static (data, ctx)=>
{
});
}
}
public void Dispose() public void Dispose()
{ {
if (_disposed) if (_disposed)

View File

@@ -22,7 +22,7 @@ public sealed partial class GraphicsTestWindow : Window
private bool _isFirstActivationHandled; private bool _isFirstActivationHandled;
public unsafe GraphicsTestWindow() public GraphicsTestWindow()
{ {
InitializeComponent(); InitializeComponent();
@@ -43,7 +43,8 @@ public sealed partial class GraphicsTestWindow : Window
_renderSystem = new RenderSystem(new RenderSystemDesc() _renderSystem = new RenderSystem(new RenderSystemDesc()
{ {
FrameBufferCount = 2, FrameBufferCount = 2,
GraphicsAPI = GraphicsAPI.Direct3D12 GraphicsAPI = GraphicsAPI.Direct3D12,
InitialRenderPipelineSettings = new RenderPasses.TestRenderPipelineSettings()
}); });
_swapChain = _renderSystem.GraphicsEngine.CreateSwapChain(new SwapChainDesc _swapChain = _renderSystem.GraphicsEngine.CreateSwapChain(new SwapChainDesc
@@ -56,7 +57,6 @@ public sealed partial class GraphicsTestWindow : Window
Target = SwapChainTarget.FromCompositionSurface(Panel) Target = SwapChainTarget.FromCompositionSurface(Panel)
}); });
_renderSystem.RenderPipelineSettings = new RenderPasses.TestRenderPipelineSettings();
_renderSystem.Start(); _renderSystem.Start();
// ECS Setup // ECS Setup
@@ -76,7 +76,7 @@ public sealed partial class GraphicsTestWindow : Window
_world.EntityManager.SetComponent(cameraEntity, new Camera _world.EntityManager.SetComponent(cameraEntity, new Camera
{ {
colorTarget = _swapChain.GetCurrentBackBuffer(), // TODO: This should be updated every frame to the current back buffer. colorTarget = _swapChain.GetCurrentBackBuffer(), // NOTE: This should be updated every frame to the current back buffer.
depthTarget = Handle<Texture>.Invalid, depthTarget = Handle<Texture>.Invalid,
nearClipPlane = 0.1f, nearClipPlane = 0.1f,
farClipPlane = 1000.0f, farClipPlane = 1000.0f,
@@ -91,33 +91,15 @@ public sealed partial class GraphicsTestWindow : Window
matrix = float4x4.TRS(new float3(0.0f, 0.0f, -5.0f), quaternion.identity, new float3(1.0f, 1.0f, 1.0f)) matrix = float4x4.TRS(new float3(0.0f, 0.0f, -5.0f), quaternion.identity, new float3(1.0f, 1.0f, 1.0f))
}); });
// var cameraEntity = _world.EntityManager.CreateEntity();
// _world.EntityManager.AddComponent(cameraEntity, new Camera
// {
// colorTarget = _swapChain.GetCurrentBackBuffer(),
// depthTarget = Handle<Texture>.Invalid,
// nearClipPlane = 0.1f,
// farClipPlane = 1000.0f,
// focalLength = 50.0f,
// sensorSize = new float2(36.0f, 24.0f),
// gateFit = GateFit.Fill,
// renderingLayerMask = new RenderingLayerMask(uint.MaxValue),
// });
//
// _world.EntityManager.AddComponent(cameraEntity, new LocalToWorld
// {
// matrix = float4x4.TRS(new float3(0.0f, 0.0f, -5.0f), quaternion.identity, new float3(1.0f, 1.0f, 1.0f))
// });
// Create Mesh Entity // Create Mesh Entity
var meshEntity = _world.EntityManager.CreateEntity();
MeshBuilder.CreateCube(0.75f, default, Allocator.Persistent, out var vertices, out var indices); MeshBuilder.CreateCube(0.75f, default, Allocator.Persistent, out var vertices, out var indices);
var directCmd = _renderSystem.GraphicsEngine.CreateCommandBuffer(CommandBufferType.Graphics); // TODO: Put this to the beginning of the frame without createing another command buffer?
using var directCmd = _renderSystem.GraphicsEngine.CreateCommandBuffer(CommandBufferType.Graphics);
var ctx = new RenderingContext(_renderSystem.GraphicsEngine, _renderSystem.ResourceManager, directCmd); var ctx = new RenderingContext(_renderSystem.GraphicsEngine, _renderSystem.ResourceManager, directCmd);
directCmd.Begin(_renderSystem.GraphicsEngine.CreateCommandAllocator(CommandBufferType.Graphics)); using var cmdAllocator = _renderSystem.GraphicsEngine.CreateCommandAllocator(CommandBufferType.Graphics);
directCmd.Begin(cmdAllocator);
var meshHandle = ctx.CreateMesh(vertices, indices, true); var meshHandle = ctx.CreateMesh(vertices, indices, true);
@@ -128,20 +110,23 @@ public sealed partial class GraphicsTestWindow : Window
} }
ctx.UploadMeshlets(meshHandle); ctx.UploadMeshlets(meshHandle);
ctx.UpdateObjectData(meshHandle, float4x4.identity); ctx.UpdateObjectData(meshHandle);
directCmd.End().ThrowIfFailed(); directCmd.End().ThrowIfFailed();
_renderSystem.GraphicsEngine.Device.GraphicsQueue.Submit(directCmd); _renderSystem.GraphicsEngine.Device.GraphicsQueue.Submit(directCmd);
_renderSystem.GraphicsEngine.Device.GraphicsQueue.WaitIdle(); _renderSystem.GraphicsEngine.Device.GraphicsQueue.WaitIdle();
_world.EntityManager.AddComponent(meshEntity, new MeshInstance
var meshSet = new ComponentSet(scope.AllocationHandle, ComponentTypeID<MeshInstance>.Value, ComponentTypeID<LocalToWorld>.Value);
var meshEntity = _world.EntityManager.CreateEntity(meshSet);
_world.EntityManager.SetComponent(meshEntity, new MeshInstance
{ {
mesh = meshHandle, mesh = meshHandle,
renderingLayerMask = new RenderingLayerMask(uint.MaxValue), renderingLayerMask = new RenderingLayerMask(uint.MaxValue),
shadowCastingMode = Engine.ShadowCastingMode.On shadowCastingMode = Engine.ShadowCastingMode.On
}); });
_world.EntityManager.AddComponent(meshEntity, new LocalToWorld _world.EntityManager.SetComponent(meshEntity, new LocalToWorld
{ {
matrix = float4x4.identity matrix = float4x4.identity
}); });
@@ -200,10 +185,15 @@ public sealed partial class GraphicsTestWindow : Window
if (_renderSystem.CPUFenceValue < _renderSystem.GPUFenceValue + _renderSystem.MaxFrameLatency) if (_renderSystem.CPUFenceValue < _renderSystem.GPUFenceValue + _renderSystem.MaxFrameLatency)
{ {
// TODO: In a real system, the camera target would be updated correctly. var queryID = new QueryBuilder().WithAll<Camera>().Build(_world);
// For now, let's just make sure it renders to the correct back buffer. ref var query = ref _world.ComponentManager.GetEntityQueryReference(queryID);
_world.SystemManager.UpdateAll(default); // This runs RenderExtractionSystem, extracting data and queueing RenderRequests foreach (ref var cam in query.GetComponentIterator<Camera>())
{
cam.colorTarget = _swapChain.GetCurrentBackBuffer();
}
_world.SystemManager.UpdateAll(default);
_renderSystem.SignalCPUReady(); _renderSystem.SignalCPUReady();
} }
} }