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:
@@ -3,10 +3,11 @@ using Ghost.Core.Graphics;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Graphics.Core;
|
||||
|
||||
public readonly struct ShaderPass : IResourceReleasable
|
||||
public readonly struct ShaderPass
|
||||
{
|
||||
public ShaderPassKey Identifier
|
||||
{
|
||||
@@ -18,8 +19,9 @@ public readonly struct ShaderPass : IResourceReleasable
|
||||
get; init;
|
||||
}
|
||||
|
||||
readonly void IResourceReleasable.ReleaseResource(IResourceDatabase database)
|
||||
public LocalKeywordSet.ReadOnly KeywordIDs
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,15 +35,43 @@ public partial struct Shader
|
||||
private static readonly Dictionary<string, int> s_propertyNameToID = new Dictionary<string, int>();
|
||||
private static int s_nextPropertyID = 0;
|
||||
|
||||
private static readonly Dictionary<string, int> s_keywordNameToID = new Dictionary<string, int>();
|
||||
private static int s_nextkeywordID = 0;
|
||||
|
||||
public static Identifier<ShaderPass> GetPassID(string passName)
|
||||
{
|
||||
return new Identifier<ShaderPass>(s_passNameToID.GetValueOrDefault(passName, s_nextPassID++));
|
||||
ref var id = ref CollectionsMarshal.GetValueRefOrAddDefault(s_passNameToID, passName, out var exists);
|
||||
if (!exists)
|
||||
{
|
||||
id = s_nextPassID++;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public static Identifier<ShaderProperty> GetPropertyID(string propertyName)
|
||||
{
|
||||
return new Identifier<ShaderProperty>(s_propertyNameToID.GetValueOrDefault(propertyName, s_nextPropertyID++));
|
||||
ref var id = ref CollectionsMarshal.GetValueRefOrAddDefault(s_propertyNameToID, propertyName, out var exists);
|
||||
if (!exists)
|
||||
{
|
||||
id = s_nextPropertyID++;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public static int GetKeywordID(string keywordName)
|
||||
{
|
||||
ref var id = ref CollectionsMarshal.GetValueRefOrAddDefault(s_keywordNameToID, keywordName, out var exists);
|
||||
if (!exists)
|
||||
{
|
||||
id = s_nextkeywordID++;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
// TODO: Global keywords
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,7 +81,8 @@ public partial struct Shader : IResourceReleasable, IIdentifierType
|
||||
{
|
||||
private readonly uint _cbufferSize;
|
||||
private UnsafeArray<ShaderPass> _shaderPasses;
|
||||
private UnsafeHashMap<int, int> _passLookup; // pass id to index
|
||||
private UnsafeHashMap<int, int> _passIDToLocal;
|
||||
private UnsafeHashMap<int, int> _keywordIDToLocal;
|
||||
|
||||
public readonly int PassCount => _shaderPasses.Count;
|
||||
public readonly uint CBufferSize => _cbufferSize;
|
||||
@@ -60,7 +91,8 @@ public partial struct Shader : IResourceReleasable, IIdentifierType
|
||||
{
|
||||
_cbufferSize = descriptor.cbufferSize;
|
||||
_shaderPasses = new UnsafeArray<ShaderPass>(descriptor.passes.Count, Allocator.Persistent);
|
||||
_passLookup = new UnsafeHashMap<int, int>(descriptor.passes.Count, Allocator.Persistent);
|
||||
_passIDToLocal = new UnsafeHashMap<int, int>(descriptor.passes.Count, Allocator.Persistent);
|
||||
_keywordIDToLocal = new UnsafeHashMap<int, int>(32, Allocator.Persistent);
|
||||
|
||||
for (var i = 0; i < descriptor.passes.Count; i++)
|
||||
{
|
||||
@@ -73,20 +105,60 @@ public partial struct Shader : IResourceReleasable, IIdentifierType
|
||||
}
|
||||
|
||||
var passKey = new ShaderPassKey(pass.Identifier);
|
||||
var keywords = default(LocalKeywordSet);
|
||||
|
||||
if (fullPass.keywords != null && fullPass.keywords.Count > 0)
|
||||
{
|
||||
var localKeywordIndex = 0;
|
||||
|
||||
for (var j = 0; j < fullPass.keywords.Count; j++)
|
||||
{
|
||||
var group = fullPass.keywords[j];
|
||||
if (group.keywords == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (group.space == KeywordSpace.Local)
|
||||
{
|
||||
foreach (var kw in group.keywords)
|
||||
{
|
||||
var kwID = GetKeywordID(kw);
|
||||
var idx = localKeywordIndex++;
|
||||
|
||||
keywords.SetKeyword(idx, true);
|
||||
_keywordIDToLocal.TryAdd(kwID, idx);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Global keywords
|
||||
}
|
||||
}
|
||||
|
||||
_shaderPasses[i] = new ShaderPass
|
||||
{
|
||||
Identifier = passKey,
|
||||
DeafaultState = fullPass.localPipeline
|
||||
DeafaultState = fullPass.localPipeline,
|
||||
KeywordIDs = keywords.AsReadOnly(),
|
||||
};
|
||||
|
||||
_passLookup[GetPassID(pass.Name)] = i;
|
||||
_passIDToLocal[GetPassID(pass.Name)] = (ushort)i;
|
||||
}
|
||||
}
|
||||
|
||||
internal int GetLocalKeywordIndex(int globalKeywordID)
|
||||
{
|
||||
if (_keywordIDToLocal.TryGetValue(globalKeywordID, out var localIndex))
|
||||
{
|
||||
return localIndex;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public readonly int GetPassIndex(Identifier<ShaderPass> passID)
|
||||
{
|
||||
if (_passLookup.TryGetValue(passID.Value, out var index))
|
||||
if (_passIDToLocal.TryGetValue(passID.Value, out var index))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
@@ -96,7 +168,7 @@ public partial struct Shader : IResourceReleasable, IIdentifierType
|
||||
|
||||
public readonly int GetPassIndex(string passName)
|
||||
{
|
||||
if (_passLookup.TryGetValue(GetPassID(passName), out var index))
|
||||
if (_passIDToLocal.TryGetValue(GetPassID(passName), out var index))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
@@ -104,14 +176,14 @@ public partial struct Shader : IResourceReleasable, IIdentifierType
|
||||
return -1;
|
||||
}
|
||||
|
||||
public readonly ShaderPass GetPass(int index)
|
||||
public readonly ref ShaderPass GetPassReference(int index)
|
||||
{
|
||||
return _shaderPasses[index];
|
||||
return ref _shaderPasses[index];
|
||||
}
|
||||
|
||||
public readonly Result<ShaderPass, ErrorStatus> TryGetPass(Identifier<ShaderPass> passID, out int passIndex)
|
||||
{
|
||||
if (_passLookup.TryGetValue(passID.Value, out var index))
|
||||
if (_passIDToLocal.TryGetValue(passID.Value, out var index))
|
||||
{
|
||||
passIndex = -1;
|
||||
return ErrorStatus.NotFound;
|
||||
@@ -124,6 +196,6 @@ public partial struct Shader : IResourceReleasable, IIdentifierType
|
||||
void IResourceReleasable.ReleaseResource(IResourceDatabase database)
|
||||
{
|
||||
_shaderPasses.Dispose();
|
||||
_passLookup.Dispose();
|
||||
_passIDToLocal.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user