forked from Misaki/GhostEngine
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:
229
Ghost.Shader.Concept/Material.cs
Normal file
229
Ghost.Shader.Concept/Material.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
namespace Ghost.Shader.Concept;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a material instance with properties, keywords, and per-pass overrides.
|
||||
/// Thread-safe for property updates, optimized for rendering.
|
||||
/// </summary>
|
||||
public sealed class Material : IDisposable
|
||||
{
|
||||
private readonly ShaderProgram _shaderProgram;
|
||||
private readonly MaterialPropertyBlock _propertyBlock;
|
||||
private KeywordSet _localKeywords;
|
||||
private readonly RenderState[] _passOverrides;
|
||||
private readonly object _lock = new();
|
||||
private bool _isDirty = true;
|
||||
|
||||
public ShaderProgram ShaderProgram => _shaderProgram;
|
||||
public bool IsDirty => _isDirty;
|
||||
|
||||
public Material(ShaderProgram shaderProgram)
|
||||
{
|
||||
_shaderProgram = shaderProgram;
|
||||
_propertyBlock = new MaterialPropertyBlock();
|
||||
_localKeywords = new KeywordSet();
|
||||
_passOverrides = new RenderState[shaderProgram.Passes.Length];
|
||||
|
||||
// Initialize pass overrides with shader defaults
|
||||
for (int i = 0; i < _passOverrides.Length; i++)
|
||||
{
|
||||
_passOverrides[i] = shaderProgram.Passes[i].RenderState;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_propertyBlock?.Dispose();
|
||||
}
|
||||
|
||||
#region Property Updates
|
||||
|
||||
public void SetFloat(string name, float value)
|
||||
{
|
||||
_propertyBlock.SetFloat(name, value);
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
public void SetVector2(string name, float x, float y)
|
||||
{
|
||||
_propertyBlock.SetVector2(name, x, y);
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
public void SetVector3(string name, float x, float y, float z)
|
||||
{
|
||||
_propertyBlock.SetVector3(name, x, y, z);
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
public void SetVector4(string name, float x, float y, float z, float w)
|
||||
{
|
||||
_propertyBlock.SetVector4(name, x, y, z, w);
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
public void SetInt(string name, int value)
|
||||
{
|
||||
_propertyBlock.SetInt(name, value);
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
public void SetMatrix4x4(string name, ReadOnlySpan<float> matrix)
|
||||
{
|
||||
_propertyBlock.SetMatrix4x4(name, matrix);
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
public bool TryGetFloat(string name, out float value)
|
||||
{
|
||||
return _propertyBlock.TryGetFloat(name, out value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Keyword Management
|
||||
|
||||
public void EnableKeyword(ShaderKeyword keyword)
|
||||
{
|
||||
if (keyword.Scope != KeywordScope.Local)
|
||||
throw new ArgumentException("Only local keywords can be set on materials", nameof(keyword));
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_localKeywords.Enable(keyword);
|
||||
MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public void DisableKeyword(ShaderKeyword keyword)
|
||||
{
|
||||
if (keyword.Scope != KeywordScope.Local)
|
||||
throw new ArgumentException("Only local keywords can be set on materials", nameof(keyword));
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_localKeywords.Disable(keyword);
|
||||
MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsKeywordEnabled(ShaderKeyword keyword)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _localKeywords.IsEnabled(keyword);
|
||||
}
|
||||
}
|
||||
|
||||
public KeywordSet GetLocalKeywords()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _localKeywords;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Pass State Overrides
|
||||
|
||||
public void SetPassRenderState(int passIndex, RenderState state)
|
||||
{
|
||||
if (passIndex < 0 || passIndex >= _passOverrides.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(passIndex));
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_passOverrides[passIndex] = state;
|
||||
MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetPassRenderState(string passName, RenderState state)
|
||||
{
|
||||
int index = _shaderProgram.GetPassIndex(passName);
|
||||
if (index < 0)
|
||||
throw new ArgumentException($"Pass '{passName}' not found in shader program", nameof(passName));
|
||||
|
||||
SetPassRenderState(index, state);
|
||||
}
|
||||
|
||||
public RenderState GetPassRenderState(int passIndex)
|
||||
{
|
||||
if (passIndex < 0 || passIndex >= _passOverrides.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(passIndex));
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
return _passOverrides[passIndex];
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Pipeline Key Generation
|
||||
|
||||
/// <summary>
|
||||
/// Generates a pipeline key for a specific pass, combining global and local keywords.
|
||||
/// </summary>
|
||||
public unsafe GraphicsPipelineKey GetPipelineKey(int passIndex, in KeywordSet globalKeywords)
|
||||
{
|
||||
if (passIndex < 0 || passIndex >= _passOverrides.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(passIndex));
|
||||
|
||||
KeywordSet combined;
|
||||
RenderState state;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
fixed (KeywordSet* pGlobal = &globalKeywords)
|
||||
fixed (KeywordSet* pLocal = &_localKeywords)
|
||||
{
|
||||
combined = KeywordSet.Merge(pGlobal, pLocal);
|
||||
}
|
||||
state = _passOverrides[passIndex];
|
||||
}
|
||||
|
||||
var variantKey = _shaderProgram.CreateVariantKey(combined);
|
||||
var pipelineKey = new GraphicsPipelineKey(
|
||||
variantKey,
|
||||
state.ComputeHash(),
|
||||
_shaderProgram.Passes[passIndex].PassId);
|
||||
|
||||
return pipelineKey;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public unsafe void CopyPropertiesTo(byte* destination, int maxSize)
|
||||
{
|
||||
_propertyBlock.CopyTo(destination, maxSize);
|
||||
}
|
||||
|
||||
public void ClearDirty()
|
||||
{
|
||||
_isDirty = false;
|
||||
}
|
||||
|
||||
private void MarkDirty()
|
||||
{
|
||||
_isDirty = true;
|
||||
}
|
||||
|
||||
public Material Clone()
|
||||
{
|
||||
var clone = new Material(_shaderProgram);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
clone._propertyBlock.CopyFrom(_propertyBlock);
|
||||
clone._localKeywords = _localKeywords;
|
||||
|
||||
for (int i = 0; i < _passOverrides.Length; i++)
|
||||
{
|
||||
clone._passOverrides[i] = _passOverrides[i];
|
||||
}
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user