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:
@@ -78,7 +78,6 @@ public class RenderExtractionSystem : ISystem
|
||||
ref var cameraQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_cameraQueryID);
|
||||
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>())
|
||||
{
|
||||
ref readonly var camRef = ref cam.Get();
|
||||
@@ -108,6 +107,7 @@ public class RenderExtractionSystem : ISystem
|
||||
ref readonly var meshInstance = ref meshInstances[i];
|
||||
if ((meshInstance.renderingLayerMask & camRef.renderingLayerMask) == 0u)
|
||||
{
|
||||
// Not in the same rendering layer, skip.
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ public class RenderExtractionSystem : ISystem
|
||||
var camPosition = camLtwRef.matrix.c3.xyz;
|
||||
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)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -3,39 +3,8 @@ using System.Runtime.InteropServices;
|
||||
|
||||
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 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 ROOT_PARAMETER_COUNT = 1;
|
||||
@@ -46,26 +15,28 @@ public struct PushConstantsData
|
||||
{
|
||||
public uint globalIndex;
|
||||
public uint viewIndex;
|
||||
public uint instanceIndex;
|
||||
public uint objectIndex;
|
||||
public uint instanceIndex;
|
||||
public uint materialIndex;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = 8)]
|
||||
[StructLayout(LayoutKind.Sequential, Size = 20)]
|
||||
public struct GlobalFrameData
|
||||
{
|
||||
public uint viewBufferIndex;
|
||||
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 float4x4 localToWorld;
|
||||
}
|
||||
|
||||
// The size should be 176 bytes (16-byte aligned)
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
public struct PerViewData
|
||||
{
|
||||
public float4x4 viewMatrix;
|
||||
@@ -77,15 +48,14 @@ public struct PerViewData
|
||||
public float4 screenSize; // xy: size, zw: 1/size
|
||||
};
|
||||
|
||||
// The size should be 96 bytes (16-byte aligned)
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
public struct PerObjectData
|
||||
{
|
||||
public float4x4 localToWorld;
|
||||
public float3 worldBoundsMin;
|
||||
public uint vertexBuffer;
|
||||
public float3 worldBoundsMax;
|
||||
public uint indexBuffer;
|
||||
|
||||
public uint meshletBuffer;
|
||||
public uint meshletVerticesBuffer;
|
||||
public uint meshletTrianglesBuffer;
|
||||
|
||||
@@ -54,7 +54,7 @@ public struct MeshletMeshData : IDisposable
|
||||
public UnsafeList<MeshletGroup> groups;
|
||||
public UnsafeList<MeshletHierarchyNode> hierarchyNodes;
|
||||
public UnsafeList<uint> meshletVertices;
|
||||
public UnsafeList<byte> meshletTriangles;
|
||||
public UnsafeList<uint> meshletTriangles;
|
||||
public int lodLevelCount;
|
||||
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.meshlets.IsCreated) data.meshlets = new UnsafeList<Meshlet>(64, 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
|
||||
{
|
||||
@@ -264,23 +264,27 @@ public struct Mesh : IResourceReleasable
|
||||
var meshlet = new Meshlet
|
||||
{
|
||||
vertexCount = (byte)cluster.vertexCount,
|
||||
triangleCount = (byte)(cluster.indexCount / 3),
|
||||
triangleCount = (byte)(cluster.localIndexCount / 3),
|
||||
vertexOffset = (uint)data.meshletVertices.Count,
|
||||
triangleOffset = (uint)data.meshletTriangles.Count,
|
||||
groupIndex = (uint)data.groups.Count - 1
|
||||
};
|
||||
data.meshlets.Add(meshlet);
|
||||
|
||||
// Add indices
|
||||
for (nuint j = 0; j < cluster.indexCount; j++)
|
||||
// Add unique vertices
|
||||
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)
|
||||
// Assuming 8-bit local indices for meshlets as per standard convention
|
||||
for (nuint j = 0; j < cluster.indexCount; j++)
|
||||
// Add local triangles (packed into uints)
|
||||
nuint triangleCount = cluster.localIndexCount / 3;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using Misaki.HighPerformance.Mathematics;
|
||||
|
||||
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
|
||||
{
|
||||
private readonly IGraphicsEngine _engine;
|
||||
@@ -185,12 +186,11 @@ public readonly unsafe ref struct RenderingContext
|
||||
MemoryType = ResourceMemoryType.Default,
|
||||
};
|
||||
// Ensure size is multiple of 4 for Raw buffer
|
||||
var trianglesSize = (uint)meshletData.meshletTriangles.Count;
|
||||
trianglesSize = (trianglesSize + 3u) & ~3u;
|
||||
var trianglesSize = (uint)meshletData.meshletTriangles.Count * sizeof(uint);
|
||||
var trianglesDesc = new BufferDesc
|
||||
{
|
||||
Size = trianglesSize,
|
||||
Stride = sizeof(byte),
|
||||
Stride = sizeof(uint),
|
||||
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
|
||||
MemoryType = ResourceMemoryType.Default,
|
||||
};
|
||||
@@ -208,7 +208,7 @@ public readonly unsafe ref struct RenderingContext
|
||||
// Padding for triangle data if needed
|
||||
if (trianglesSize > meshletData.meshletTriangles.Count)
|
||||
{
|
||||
var paddedData = new byte[trianglesSize];
|
||||
var paddedData = new uint[trianglesSize];
|
||||
meshletData.meshletTriangles.AsSpan().CopyTo(paddedData);
|
||||
_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);
|
||||
}
|
||||
|
||||
public void UpdateObjectData(Handle<Mesh> mesh, float4x4 localToWorld)
|
||||
public void UpdateObjectData(Handle<Mesh> mesh)
|
||||
{
|
||||
var r = _resourceManager.GetMeshReference(mesh);
|
||||
if (r.IsFailure)
|
||||
@@ -233,7 +233,6 @@ public readonly unsafe ref struct RenderingContext
|
||||
ref readonly var meshData = ref r.Value;
|
||||
var data = new PerObjectData
|
||||
{
|
||||
localToWorld = localToWorld,
|
||||
worldBoundsMin = meshData.BoundingBox.Min,
|
||||
worldBoundsMax = meshData.BoundingBox.Max,
|
||||
vertexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.VertexBuffer.AsResource()),
|
||||
|
||||
337
src/Runtime/Ghost.Graphics/Obsolete/MeshRenderPass.cs
Normal file
337
src/Runtime/Ghost.Graphics/Obsolete/MeshRenderPass.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ struct Meshlet
|
||||
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")]
|
||||
void MSMain(
|
||||
uint3 groupThreadID : SV_GroupThreadID,
|
||||
@@ -59,23 +59,12 @@ void MSMain(
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
uint triangleIndex = groupThreadID.x;
|
||||
uint baseOffset = m.triangleOffset + triangleIndex * 3;
|
||||
|
||||
// Load 4 bytes to get the 3 index bytes
|
||||
// Needs byte-aligned loading
|
||||
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);
|
||||
|
||||
// Load the packed 32-bit integer containing the 3 local indices
|
||||
uint packedIndices = meshletTrianglesBuffer.Load((m.triangleOffset + triangleIndex) * 4);
|
||||
|
||||
uint i0 = packedIndices & 0xFF;
|
||||
uint i1 = (packedIndices >> 8) & 0xFF;
|
||||
|
||||
@@ -28,6 +28,11 @@ internal struct RenderSystemDesc
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public IRenderPipelineSettings? InitialRenderPipelineSettings
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -180,7 +185,7 @@ public class RenderSystem : IDisposable
|
||||
_shutdownEvent = new AutoResetEvent(false);
|
||||
_resizeRequest = new ConcurrentDictionary<ISwapChain, uint2>();
|
||||
|
||||
_renderPipelineSettings = new GhostRenderPipelineSettings();
|
||||
_renderPipelineSettings = _config.InitialRenderPipelineSettings ?? new GhostRenderPipelineSettings();
|
||||
_renderPipeline = _renderPipelineSettings.CreatePipeline(this);
|
||||
|
||||
_isRunning = false;
|
||||
@@ -416,6 +421,7 @@ public class RenderSystem : IDisposable
|
||||
frameResource.Dispose();
|
||||
}
|
||||
|
||||
_renderPipeline.Dispose();
|
||||
_graphicsEngine.Dispose();
|
||||
_shutdownEvent.Dispose();
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#ifndef 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
|
||||
{
|
||||
BYTE_ADDRESS_BUFFER globalBuffer;
|
||||
BYTE_ADDRESS_BUFFER perViewBuffer;
|
||||
BYTE_ADDRESS_BUFFER perObjectBuffer;
|
||||
BYTE_ADDRESS_BUFFER perInstanceBuffer;
|
||||
BYTE_ADDRESS_BUFFER perMaterialBuffer;
|
||||
};
|
||||
|
||||
@@ -24,7 +25,6 @@ struct PerViewData
|
||||
|
||||
struct PerObjectData
|
||||
{
|
||||
float4x4 localToWorld;
|
||||
float3 worldBoundsMin;
|
||||
BYTE_ADDRESS_BUFFER vertexBuffer;
|
||||
float3 worldBoundsMax;
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace Ghost.Graphics.Utilities;
|
||||
internal struct Cluster : IDisposable
|
||||
{
|
||||
public UnsafeList<uint> indices;
|
||||
public UnsafeList<uint> uniqueVertices;
|
||||
public UnsafeList<byte> localIndices;
|
||||
public ClodBounds bounds;
|
||||
public nuint vertices;
|
||||
public int group;
|
||||
@@ -17,7 +19,9 @@ internal struct Cluster : IDisposable
|
||||
|
||||
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;
|
||||
/// <summary> Number of indices. </summary>
|
||||
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;
|
||||
/// <summary> Pointer to local triangle indices for this cluster. </summary>
|
||||
public byte* localIndices;
|
||||
/// <summary> Number of local indices. </summary>
|
||||
public nuint localIndexCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -244,13 +254,22 @@ public static unsafe class MeshletUtility
|
||||
{
|
||||
vertices = meshlet.vertex_count,
|
||||
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,
|
||||
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++)
|
||||
{
|
||||
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);
|
||||
@@ -377,7 +396,10 @@ public static unsafe class MeshletUtility
|
||||
: srcCluster.bounds,
|
||||
indices = (uint*)srcCluster.indices.GetUnsafePtr(),
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.RenderGraphModule;
|
||||
using Ghost.Graphics.RenderPipeline;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Misaki.HighPerformance.Mathematics;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ghost.Graphics.Test.RenderPasses;
|
||||
|
||||
@@ -18,6 +17,12 @@ public sealed class TestRenderPipelineSettings : IRenderPipelineSettings
|
||||
|
||||
public unsafe partial class TestRenderPipeline : IRenderPipeline
|
||||
{
|
||||
private class MeshletDebugPassData
|
||||
{
|
||||
public Identifier<RGTexture> backbuffer;
|
||||
public RenderList renderList;
|
||||
}
|
||||
|
||||
private readonly RenderGraph _renderGraph;
|
||||
private readonly RenderSystem _renderSystem;
|
||||
|
||||
@@ -28,13 +33,6 @@ public unsafe partial class TestRenderPipeline : IRenderPipeline
|
||||
Dispose();
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected void ThrowIfDisposed()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
|
||||
internal TestRenderPipeline(RenderSystem renderSystem)
|
||||
{
|
||||
_renderSystem = renderSystem;
|
||||
@@ -56,7 +54,6 @@ public unsafe partial class TestRenderPipeline : IRenderPipeline
|
||||
|
||||
// 1. Allocate and populate Instance Data buffer
|
||||
var instanceCount = request.opaqueRenderList.TotalRecordCount;
|
||||
|
||||
if (instanceCount == 0)
|
||||
{
|
||||
continue; // Nothing to render
|
||||
@@ -71,6 +68,7 @@ public unsafe partial class TestRenderPipeline : IRenderPipeline
|
||||
MemoryType = ResourceMemoryType.Upload, // Upload directly for simplicity in testing
|
||||
});
|
||||
|
||||
// TODO: Optimize by suballocation.
|
||||
var instanceBufferHandle = resourceManager.GetPooledResource(instanceBufferDesc);
|
||||
var instanceBufferResource = instanceBufferHandle.AsGraphicsBuffer();
|
||||
|
||||
@@ -137,6 +135,10 @@ public unsafe partial class TestRenderPipeline : IRenderPipeline
|
||||
{
|
||||
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.
|
||||
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()
|
||||
{
|
||||
if (_disposed)
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed partial class GraphicsTestWindow : Window
|
||||
|
||||
private bool _isFirstActivationHandled;
|
||||
|
||||
public unsafe GraphicsTestWindow()
|
||||
public GraphicsTestWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -43,7 +43,8 @@ public sealed partial class GraphicsTestWindow : Window
|
||||
_renderSystem = new RenderSystem(new RenderSystemDesc()
|
||||
{
|
||||
FrameBufferCount = 2,
|
||||
GraphicsAPI = GraphicsAPI.Direct3D12
|
||||
GraphicsAPI = GraphicsAPI.Direct3D12,
|
||||
InitialRenderPipelineSettings = new RenderPasses.TestRenderPipelineSettings()
|
||||
});
|
||||
|
||||
_swapChain = _renderSystem.GraphicsEngine.CreateSwapChain(new SwapChainDesc
|
||||
@@ -56,7 +57,6 @@ public sealed partial class GraphicsTestWindow : Window
|
||||
Target = SwapChainTarget.FromCompositionSurface(Panel)
|
||||
});
|
||||
|
||||
_renderSystem.RenderPipelineSettings = new RenderPasses.TestRenderPipelineSettings();
|
||||
_renderSystem.Start();
|
||||
|
||||
// ECS Setup
|
||||
@@ -76,7 +76,7 @@ public sealed partial class GraphicsTestWindow : Window
|
||||
|
||||
_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,
|
||||
nearClipPlane = 0.1f,
|
||||
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))
|
||||
});
|
||||
|
||||
// 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
|
||||
var meshEntity = _world.EntityManager.CreateEntity();
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
@@ -128,20 +110,23 @@ public sealed partial class GraphicsTestWindow : Window
|
||||
}
|
||||
|
||||
ctx.UploadMeshlets(meshHandle);
|
||||
ctx.UpdateObjectData(meshHandle, float4x4.identity);
|
||||
ctx.UpdateObjectData(meshHandle);
|
||||
|
||||
directCmd.End().ThrowIfFailed();
|
||||
_renderSystem.GraphicsEngine.Device.GraphicsQueue.Submit(directCmd);
|
||||
_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,
|
||||
renderingLayerMask = new RenderingLayerMask(uint.MaxValue),
|
||||
shadowCastingMode = Engine.ShadowCastingMode.On
|
||||
});
|
||||
|
||||
_world.EntityManager.AddComponent(meshEntity, new LocalToWorld
|
||||
_world.EntityManager.SetComponent(meshEntity, new LocalToWorld
|
||||
{
|
||||
matrix = float4x4.identity
|
||||
});
|
||||
@@ -200,10 +185,15 @@ public sealed partial class GraphicsTestWindow : Window
|
||||
|
||||
if (_renderSystem.CPUFenceValue < _renderSystem.GPUFenceValue + _renderSystem.MaxFrameLatency)
|
||||
{
|
||||
// TODO: In a real system, the camera target would be updated correctly.
|
||||
// For now, let's just make sure it renders to the correct back buffer.
|
||||
var queryID = new QueryBuilder().WithAll<Camera>().Build(_world);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user