Add high-performance material/shader system (Ghost.Shader.Concept)
Introduces a new Ghost.Shader.Concept project implementing a modern, data-oriented material and shader system with: - Global/local keyword bitsets (fast O(1) ops, 64 bytes) - Multi-pass shader program and per-pass render state overrides - Thread-safe, 16-byte aligned material property blocks - Material pooling to reduce GC pressure - Batch renderer for efficient PSO grouping and async variant warmup - Full demo (Program.cs) and extensive documentation (ARCHITECTURE.md, README.md, PROJECT_SUMMARY.md) - Minor integration: new enums, doc updates, and keyword handling in existing code No breaking changes to the existing engine; all new code is isolated. This serves as a reference implementation for high-performance, extensible material/shader architectures.
This commit is contained in:
161
Ghost.Shader.Concept/KeywordSet.cs
Normal file
161
Ghost.Shader.Concept/KeywordSet.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ghost.Shader.Concept;
|
||||
|
||||
/// <summary>
|
||||
/// Compact representation of enabled keywords using bitsets.
|
||||
/// Supports up to 256 global and 256 local keywords with O(1) operations.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public unsafe struct KeywordSet : IEquatable<KeywordSet>
|
||||
{
|
||||
private const int GlobalBits = 256;
|
||||
private const int LocalBits = 256;
|
||||
private const int GlobalLongs = GlobalBits / 64;
|
||||
private const int LocalLongs = LocalBits / 64;
|
||||
|
||||
private fixed ulong _globalBits[GlobalLongs];
|
||||
private fixed ulong _localBits[LocalLongs];
|
||||
|
||||
public void Enable(ShaderKeyword keyword)
|
||||
{
|
||||
fixed (ulong* global = _globalBits, local = _localBits)
|
||||
{
|
||||
if (keyword.Scope == KeywordScope.Global)
|
||||
SetBit(global, GlobalLongs, keyword.Id);
|
||||
else
|
||||
SetBit(local, LocalLongs, keyword.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public void Disable(ShaderKeyword keyword)
|
||||
{
|
||||
fixed (ulong* global = _globalBits, local = _localBits)
|
||||
{
|
||||
if (keyword.Scope == KeywordScope.Global)
|
||||
ClearBit(global, GlobalLongs, keyword.Id);
|
||||
else
|
||||
ClearBit(local, LocalLongs, keyword.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly bool IsEnabled(ShaderKeyword keyword)
|
||||
{
|
||||
fixed (ulong* global = _globalBits, local = _localBits)
|
||||
{
|
||||
if (keyword.Scope == KeywordScope.Global)
|
||||
return GetBit(global, GlobalLongs, keyword.Id);
|
||||
else
|
||||
return GetBit(local, LocalLongs, keyword.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
fixed (ulong* global = _globalBits, local = _localBits)
|
||||
{
|
||||
for (int i = 0; i < GlobalLongs; i++)
|
||||
global[i] = 0;
|
||||
for (int i = 0; i < LocalLongs; i++)
|
||||
local[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetGlobal(KeywordSet* other)
|
||||
{
|
||||
fixed (ulong* dest = _globalBits)
|
||||
{
|
||||
for (int i = 0; i < GlobalLongs; i++)
|
||||
dest[i] = other->_globalBits[i];
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLocal(KeywordSet* other)
|
||||
{
|
||||
fixed (ulong* dest = _localBits)
|
||||
{
|
||||
for (int i = 0; i < LocalLongs; i++)
|
||||
dest[i] = other->_localBits[i];
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe KeywordSet Merge(KeywordSet* a, KeywordSet* b)
|
||||
{
|
||||
KeywordSet result;
|
||||
KeywordSet* pResult = &result;
|
||||
|
||||
for (int i = 0; i < GlobalLongs; i++)
|
||||
pResult->_globalBits[i] = a->_globalBits[i] | b->_globalBits[i];
|
||||
for (int i = 0; i < LocalLongs; i++)
|
||||
pResult->_localBits[i] = a->_localBits[i] | b->_localBits[i];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public readonly ulong ComputeHash()
|
||||
{
|
||||
ulong hash = 0xcbf29ce484222325; // FNV-1a offset
|
||||
const ulong prime = 0x100000001b3;
|
||||
|
||||
fixed (ulong* global = _globalBits, local = _localBits)
|
||||
{
|
||||
for (int i = 0; i < GlobalLongs; i++)
|
||||
{
|
||||
hash ^= global[i];
|
||||
hash *= prime;
|
||||
}
|
||||
for (int i = 0; i < LocalLongs; i++)
|
||||
{
|
||||
hash ^= local[i];
|
||||
hash *= prime;
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
public readonly bool Equals(KeywordSet other)
|
||||
{
|
||||
fixed (ulong* thisGlobal = _globalBits, thisLocal = _localBits)
|
||||
{
|
||||
for (int i = 0; i < GlobalLongs; i++)
|
||||
if (thisGlobal[i] != other._globalBits[i])
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < LocalLongs; i++)
|
||||
if (thisLocal[i] != other._localBits[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public override readonly bool Equals(object? obj) => obj is KeywordSet other && Equals(other);
|
||||
public override readonly int GetHashCode() => (int)ComputeHash();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void SetBit(ulong* bits, int count, int index)
|
||||
{
|
||||
if (index < 0 || index >= count * 64) return;
|
||||
int longIndex = index / 64;
|
||||
int bitIndex = index % 64;
|
||||
bits[longIndex] |= (1UL << bitIndex);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ClearBit(ulong* bits, int count, int index)
|
||||
{
|
||||
if (index < 0 || index >= count * 64) return;
|
||||
int longIndex = index / 64;
|
||||
int bitIndex = index % 64;
|
||||
bits[longIndex] &= ~(1UL << bitIndex);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool GetBit(ulong* bits, int count, int index)
|
||||
{
|
||||
if (index < 0 || index >= count * 64) return false;
|
||||
int longIndex = index / 64;
|
||||
int bitIndex = index % 64;
|
||||
return (bits[longIndex] & (1UL << bitIndex)) != 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user