Refactor: variant-aware shader/material pipeline overhaul

Major architectural update to graphics/material/shader system:
- Introduced strongly-typed key structs (Key64/Key128) for passes, variants, and pipelines; removed legacy key types.
- Implemented robust hashing and key generation utilities for efficient variant and pipeline lookup/caching.
- Shader compiler now compiles/caches all keyword variants using new key system; includes handled as lists.
- Switched to push constant root signature for per-draw data; updated HLSL and C# codegen accordingly.
- Refactored Material, Shader, and Pass data structures for cache efficiency and variant support.
- Pipeline library and PSO management now use 128-bit keys and variant-specific caching.
- Replaced WorldNode with SceneNode in editor/scene graph; introduced ComponentManager for archetype/query management.
- Migrated math utilities to Misaki.HighPerformance.Mathematics; updated editor controls.
- Updated all HLSL and codegen for new buffer/push constant layouts and macros.
- Misc: project reference cleanup, D3D12 Work Graph support, doc updates, and code modernization.
This commit is contained in:
2026-01-09 22:25:37 +09:00
parent c9be05fc60
commit 6a041f75ba
93 changed files with 1926 additions and 1390 deletions

View File

@@ -2,162 +2,19 @@ using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.Graphics.Core;
using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Utilities;
using System.IO.Hashing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Graphics.RHI;
public readonly struct ShaderPassKey : IEquatable<ShaderPassKey>
{
public readonly ulong value;
public readonly struct ShaderVariant;
public readonly struct GraphicsPipeline;
public ShaderPassKey(ulong value)
{
this.value = value;
}
public ShaderPassKey(string passId)
{
var passIdSpan = passId.AsSpan();
value = XxHash3.HashToUInt64(MemoryMarshal.AsBytes(passIdSpan));
}
public override string ToString()
{
return value.ToString("X16");
}
public bool Equals(ShaderPassKey other)
{
return value == other.value;
}
public override int GetHashCode()
{
return value.GetHashCode();
}
public override bool Equals(object? obj)
{
return obj is ShaderPassKey key && Equals(key);
}
public static bool operator ==(ShaderPassKey left, ShaderPassKey right)
{
return left.Equals(right);
}
public static bool operator !=(ShaderPassKey left, ShaderPassKey right)
{
return !(left == right);
}
}
public readonly struct GraphicsPipelineKey
{
public const int KEY_STRING_LENGTH = 33; // 32 chars + null terminator
public readonly UInt128 value;
public GraphicsPipelineKey(UInt128 value)
{
this.value = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GraphicsPipelineKey Combine(MaterialPipelineKey materialKey, PassPipelineKey passKey)
{
// Order-sensitive 128-bit mix. Cheap and stable, avoids span hashing.
static ulong Mix64(ulong x)
{
x ^= x >> 30;
x *= 0xBF58476D1CE4E5B9ul;
x ^= x >> 27;
x *= 0x94D049BB133111EBul;
x ^= x >> 31;
return x;
}
unsafe static ulong GetLow(UInt128 value) => ((ulong*)&value)[0];
unsafe static ulong GetHigh(UInt128 value) => ((ulong*)&value)[1];
var mLo = GetLow(materialKey.value);
var mHi = GetHigh(materialKey.value);
var pLo = GetLow(passKey.value);
var pHi = GetHigh(passKey.value);
// Distinct constants + cross-feeding to reduce structural collisions.
var lo = Mix64(mLo ^ (pLo + 0x9E3779B97F4A7C15ul) ^ (mHi * 0xD6E8FEB86659FD93ul));
var hi = Mix64(mHi ^ (pHi + 0xC2B2AE3D27D4EB4Ful) ^ (pLo * 0x165667B19E3779F9ul));
return new GraphicsPipelineKey(new UInt128(lo, hi));
}
public Result GetString(Span<char> destination)
{
if (!value.TryFormat(destination, out var num, "X16"))
{
return Result.Failure("Failed to format GraphicsPipelineKey to string.");
}
// Just in case.
destination[num] = '\0';
return Result.Success();
}
public override string ToString()
{
return value.ToString("X16");
}
public override int GetHashCode()
{
return value.GetHashCode();
}
}
public readonly struct MaterialPipelineKey : IEquatable<MaterialPipelineKey>
public readonly struct PassPipelineHash : IEquatable<PassPipelineHash>
{
public readonly UInt128 value;
// TODO: Variants
public MaterialPipelineKey(ShaderPassKey passKey, PipelineState psoOptions)
{
// 32-bit packed key for states controlled by material / overrides.
// layout:
// 0..3 Blend (4 bits)
// 4..6 Cull (3 bits)
// 7..10 DeafaultState (4 bits)
// 11 ZWrite (1 bit)
// 12..15 ColorMask (4 bits)
var key = 0u;
key |= ((uint)psoOptions.Blend & 0xFu) << 0;
key |= ((uint)psoOptions.Cull & 0x7u) << 4;
key |= ((uint)psoOptions.ZTest & 0xFu) << 7;
key |= ((uint)psoOptions.ZWrite & 0x1u) << 11;
key |= ((uint)psoOptions.ColorMask & 0xFu) << 12;
value = new UInt128(passKey.value, key);
}
public bool Equals(MaterialPipelineKey other) => value == other.value;
public override bool Equals(object? obj) => obj is MaterialPipelineKey other && Equals(other);
public override int GetHashCode() => value.GetHashCode();
public static bool operator ==(MaterialPipelineKey left, MaterialPipelineKey right) => left.Equals(right);
public static bool operator !=(MaterialPipelineKey left, MaterialPipelineKey right) => !(left == right);
}
public readonly struct PassPipelineKey : IEquatable<PassPipelineKey>
{
public readonly UInt128 value;
public PassPipelineKey(ReadOnlySpan<TextureFormat> rtvFormats, TextureFormat dsvFormat)
public PassPipelineHash(ReadOnlySpan<TextureFormat> rtvFormats, TextureFormat dsvFormat)
{
if (rtvFormats.Length > 8)
{
@@ -177,17 +34,17 @@ public readonly struct PassPipelineKey : IEquatable<PassPipelineKey>
value = new UInt128(rtvPart, (ulong)dsvFormat);
}
public bool Equals(PassPipelineKey other) => value == other.value;
public override bool Equals(object? obj) => obj is PassPipelineKey other && Equals(other);
public bool Equals(PassPipelineHash other) => value == other.value;
public override bool Equals(object? obj) => obj is PassPipelineHash other && Equals(other);
public override int GetHashCode() => value.GetHashCode();
public static bool operator ==(PassPipelineKey left, PassPipelineKey right) => left.Equals(right);
public static bool operator !=(PassPipelineKey left, PassPipelineKey right) => !(left == right);
public static bool operator ==(PassPipelineHash left, PassPipelineHash right) => left.Equals(right);
public static bool operator !=(PassPipelineHash left, PassPipelineHash right) => !(left == right);
}
public ref struct GraphicsPSODescriptor
{
public ShaderPassKey PassId
public Key64<ShaderVariant> VariantKey
{
get; set;
}
@@ -248,7 +105,7 @@ public readonly struct CBufferInfo
get; init;
}
public IReadOnlyList<CBufferPropertyInfo> Properties
public IReadOnlyList<CBufferPropertyInfo>? Properties
{
get; init;
}

View File

@@ -104,7 +104,7 @@ public interface ICommandBuffer : IDisposable
/// Sets the pipeline state object
/// </summary>
/// <param name="pipelineKey">Pipeline state to set</param>
void SetPipelineState(GraphicsPipelineKey pipelineKey);
void SetPipelineState(Key128<GraphicsPipeline> pipelineKey);
/// <summary>
/// Sets the constant buffer view for the specified slot in the graphics pipeline.
@@ -135,6 +135,14 @@ public interface ICommandBuffer : IDisposable
/// <param name="topology">The primitive topology that determines how the input vertices are interpreted during rendering.</param>
void SetPrimitiveTopology(PrimitiveTopology topology);
/// <summary>
/// Sets a 32-bit constant value in the graphics root signature at the specified index.
/// </summary>
/// <param name="rootIndex">The zero-based index of the root parameter in the graphics root signature to set the constant for.</param>
/// <param name="constantBuffer">A read-only span containing the 32-bit constant values to set.</param>
/// <param name="offsetIn32Bits">The offset, in 32-bit values, from the start of the root parameter where the constants will be set.</param>
void SetGraphicsRoot32Constants(uint rootIndex, ReadOnlySpan<uint> constantBuffer, uint offsetIn32Bits = 0);
/// <summary>
/// Issues a non-indexed draw call.
/// </summary>

View File

@@ -22,6 +22,6 @@ public interface IPipelineLibrary : IDisposable
/// <param name="filePath">File path. If null, load default library.</param>
void InitializeLibrary(string? filePath);
void SaveLibraryToDisk(string filePath);
bool HasPipeline(GraphicsPipelineKey key);
Result<GraphicsPipelineKey> CompilePSO(ref readonly GraphicsPSODescriptor descriptor, ref readonly GraphicsCompiledResult compiled);
bool HasPipeline(Key128<GraphicsPipeline> key);
Result<Key128<GraphicsPipeline>> CompilePSO(ref readonly GraphicsPSODescriptor descriptor, ref readonly GraphicsCompiledResult compiled);
}

View File

@@ -9,6 +9,7 @@ public enum FeatureSupport
MeshShaders = 1 << 2,
SamplerFeedback = 1 << 3,
BindlessResources = 1 << 4,
WorkGraphs = 1 << 5,
}
/// <summary>
@@ -40,5 +41,8 @@ public interface IRenderDevice : IDisposable
get;
}
public FeatureSupport GetFeatureSupport();
public FeatureSupport FeatureSupport
{
get;
}
}

View File

@@ -1,3 +1,10 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.Core.Utilities;
using Ghost.Graphics.Core;
using System.IO.Hashing;
using System.Runtime.InteropServices;
namespace Ghost.Graphics.RHI;
internal static class RHIUtility
@@ -27,7 +34,7 @@ internal static class RHIUtility
var packed = false;
var planar = false;
var bpe = 0u;
//switch (Format)
//{
// case Format.BC1Typeless:
@@ -127,4 +134,49 @@ internal static class RHIUtility
slicePitch = rowPitch * height;
}
}
}
public static Key64<ShaderPass> CreateShaderPassKey(string passID)
{
var passIdSpan = passID.AsSpan();
return new Key64<ShaderPass>(XxHash3.HashToUInt64(MemoryMarshal.AsBytes(passIdSpan)));
}
public static Key64<ShaderVariant> CreateShaderVariantKey(Key64<ShaderPass> passKey, ref readonly LocalKeywordSet keywords)
{
var passHash = passKey.Value;
var keywordHash = keywords.GetHash64();
return new Key64<ShaderVariant>(Hash.Hash64(passHash, keywordHash));
}
public static unsafe Key128<GraphicsPipeline> CreateGraphicsPipelineKey(Key64<ShaderVariant> shaderVariantKey, PipelineState pipelineState, PassPipelineHash passKey)
{
// Order-sensitive 128-bit mix. Cheap and stable, avoids span hashing.
static ulong Mix64(ulong x)
{
x ^= x >> 30;
x *= 0xBF58476D1CE4E5B9ul;
x ^= x >> 27;
x *= 0x94D049BB133111EBul;
x ^= x >> 31;
return x;
}
var mLo = shaderVariantKey.Value;
var mHi = pipelineState.GetHashCode64();
var pPasskey = (ulong*)&passKey.value;
var pLo = pPasskey[0];
var pHi = pPasskey[1];
// Distinct constants + cross-feeding to reduce structural collisions.
var lo = Mix64(mLo ^ (pLo + 0x9E3779B97F4A7C15ul) ^ (mHi * 0xD6E8FEB86659FD93ul));
var hi = Mix64(mHi ^ (pHi + 0xC2B2AE3D27D4EB4Ful) ^ (pLo * 0x165667B19E3779F9ul));
return new Key128<GraphicsPipeline>(new UInt128(lo, hi));
}
public static bool TryGetString(this Key128<GraphicsPipeline> key, Span<char> destination)
{
return key.Value.TryFormat(destination, out var _, "X16");
}
}