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.
357 lines
9.7 KiB
Markdown
357 lines
9.7 KiB
Markdown
# Ghost Shader Concept - High Performance Material System
|
|
|
|
A modern, high-performance material and shader system designed for maximum efficiency and flexibility. Built with data-oriented design principles inspired by Unity DOTS, Unreal Engine 5, and modern rendering engines.
|
|
|
|
## Architecture Overview
|
|
|
|
### Core Design Principles
|
|
|
|
1. **Data-Oriented Design**: Cache-friendly memory layouts for optimal performance
|
|
2. **Lock-Free Where Possible**: Concurrent collections and atomic operations minimize contention
|
|
3. **Zero-Allocation Hot Paths**: Struct-based keys and value types reduce GC pressure
|
|
4. **Compile-Time Variants**: Shader permutations compiled ahead or on-demand
|
|
5. **Batch-Friendly**: Automatic PSO batching for minimal state changes
|
|
|
|
---
|
|
|
|
## System Components
|
|
|
|
### 1. Keyword System (`ShaderKeyword.cs`, `KeywordSet.cs`)
|
|
|
|
**Keywords** enable/disable shader features at compile time, creating variants.
|
|
|
|
- **Global Keywords**: Engine-wide settings (HDR, shadow quality, platform features)
|
|
- **Local Keywords**: Per-material settings (normal mapping, alpha test, etc.)
|
|
|
|
**KeywordSet**: Compact bitset (256 global + 256 local keywords) using unsafe fixed buffers
|
|
- O(1) enable/disable/query operations
|
|
- Fast hash computation for variant key generation
|
|
- Supports merging global + local keywords
|
|
|
|
```csharp
|
|
var keywords = new KeywordSet();
|
|
keywords.Enable(alphaTestKeyword);
|
|
keywords.Enable(normalMapKeyword);
|
|
ulong hash = keywords.ComputeHash(); // For variant lookup
|
|
```
|
|
|
|
### 2. Shader Variant System (`ShaderKeys.cs`)
|
|
|
|
**ShaderVariantKey**: Uniquely identifies a compiled shader variant
|
|
- Combines shader program ID + keyword hash
|
|
- Used as cache key for `IShaderCompiler`
|
|
|
|
**GraphicsPipelineKey**: Uniquely identifies a complete PSO
|
|
- Combines shader variant + render state hash + pass ID
|
|
- Used as cache key for `IPipelineLibrary`
|
|
|
|
### 3. Render State (`RenderState.cs`)
|
|
|
|
Immutable, hashable pipeline state:
|
|
- Rasterizer (cull mode, fill mode, depth bias)
|
|
- Depth-Stencil (test/write enable, compare func, stencil ops)
|
|
- Blend State (per-RT blend factors and operations)
|
|
- Topology
|
|
|
|
```csharp
|
|
var state = RenderState.Default;
|
|
state.BlendEnable = true;
|
|
state.SrcBlend = BlendFactor.SrcAlpha;
|
|
ulong hash = state.ComputeHash();
|
|
```
|
|
|
|
### 4. Shader Programs (`ShaderProgram.cs`)
|
|
|
|
A **ShaderProgram** represents a complete shader with multiple passes.
|
|
|
|
**ShaderPass**: Single rendering pass with:
|
|
- Name and ID
|
|
- Default render state
|
|
- Entry point functions (vertex/pixel)
|
|
|
|
**Builder Pattern** for clean creation:
|
|
|
|
```csharp
|
|
var shader = new ShaderProgramBuilder()
|
|
.WithName("StandardPBR")
|
|
.AddPass("ForwardBase", RenderState.Default)
|
|
.AddPass("ShadowCaster", shadowState)
|
|
.DeclareKeywords(alphaTest, normalMap)
|
|
.Build();
|
|
```
|
|
|
|
### 5. Material Properties (`MaterialPropertyBlock.cs`)
|
|
|
|
**Thread-safe**, **linear memory layout** for GPU upload efficiency.
|
|
|
|
Supports:
|
|
- Scalars (float, int)
|
|
- Vectors (float2, float3, float4)
|
|
- Matrices (4x4)
|
|
- Textures (planned)
|
|
|
|
Properties are **16-byte aligned** for GPU compatibility and stored contiguously.
|
|
|
|
```csharp
|
|
var props = new MaterialPropertyBlock();
|
|
props.SetFloat("_Metallic", 0.5f);
|
|
props.SetVector4("_Color", 1, 0, 0, 1);
|
|
unsafe {
|
|
props.CopyTo(gpuBufferPtr, bufferSize);
|
|
}
|
|
```
|
|
|
|
### 6. Materials (`Material.cs`)
|
|
|
|
High-level material instance combining:
|
|
- **Shader program** reference
|
|
- **Property block** for per-material data
|
|
- **Local keywords** for variant selection
|
|
- **Per-pass render state overrides**
|
|
|
|
**Thread-safe** for property updates. **Dirty tracking** for efficient GPU updates.
|
|
|
|
```csharp
|
|
var material = new Material(shaderProgram);
|
|
material.SetFloat("_Metallic", 0.8f);
|
|
material.EnableKeyword(normalMapKeyword);
|
|
material.SetPassRenderState("ForwardBase", transparentState);
|
|
|
|
// Get pipeline key for rendering
|
|
var psoKey = material.GetPipelineKey(passIndex, globalKeywords);
|
|
```
|
|
|
|
**Cloning** for material instances:
|
|
```csharp
|
|
var clone = material.Clone(); // Deep copy of properties and state
|
|
```
|
|
|
|
### 7. Global State (`GlobalKeywordState.cs`)
|
|
|
|
Singleton managing **engine-wide keywords**.
|
|
- Thread-safe keyword enable/disable
|
|
- Version tracking for cache invalidation
|
|
- Automatic merging with local keywords during rendering
|
|
|
|
```csharp
|
|
GlobalKeywordState.Instance.EnableKeyword(hdrKeyword);
|
|
var keywords = GlobalKeywordState.Instance.GetKeywordSet();
|
|
```
|
|
|
|
### 8. Batch Renderer (`MaterialBatchRenderer.cs`)
|
|
|
|
**Core rendering system** that:
|
|
1. Groups draw calls by PSO (shader variant + render state + pass)
|
|
2. Ensures shader variants are compiled
|
|
3. Gets/creates PSOs from pipeline library
|
|
4. Returns sorted batches for minimal state changes
|
|
|
|
```csharp
|
|
var batches = batchRenderer.BatchDrawCalls(drawCalls);
|
|
foreach (var batch in batches) {
|
|
SetPipeline(batch.Pipeline);
|
|
foreach (var draw in batch.DrawCommands) {
|
|
// Upload material properties
|
|
// Issue draw call
|
|
}
|
|
}
|
|
```
|
|
|
|
**Async Warmup** for pre-compiling variants:
|
|
```csharp
|
|
await batchRenderer.WarmupVariantsAsync(shader, variantConfigs);
|
|
```
|
|
|
|
### 9. Material Pooling (`MaterialPool.cs`)
|
|
|
|
Object pool for material instances to reduce allocations.
|
|
|
|
```csharp
|
|
var material = pool.Rent(shaderProgram);
|
|
// Use material...
|
|
pool.Return(material);
|
|
```
|
|
|
|
---
|
|
|
|
## Performance Characteristics
|
|
|
|
### Memory Layout
|
|
- **KeywordSet**: 64 bytes (fixed size, stack-allocated)
|
|
- **RenderState**: ~60 bytes (stack-allocated)
|
|
- **MaterialPropertyBlock**: Variable, contiguous, 16-byte aligned
|
|
|
|
### Complexity
|
|
- **Keyword enable/disable**: O(1)
|
|
- **Hash computation**: O(1) - fixed iterations
|
|
- **Pipeline key generation**: O(1)
|
|
- **Batch sorting**: O(N log N) where N = unique PSOs (typically << draw calls)
|
|
|
|
### Concurrency
|
|
- **Lock-free**: Keyword queries, hash computation, key generation
|
|
- **Concurrent**: Variant compilation cache, PSO cache
|
|
- **Thread-safe**: Material property updates, global keyword changes
|
|
|
|
---
|
|
|
|
## Usage Example
|
|
|
|
```csharp
|
|
// 1. Setup
|
|
var registry = ShaderKeywordRegistry.Instance;
|
|
var normalMap = registry.GetOrRegister("NORMAL_MAP", KeywordScope.Local);
|
|
var hdr = registry.GetOrRegister("HDR", KeywordScope.Global);
|
|
|
|
GlobalKeywordState.Instance.EnableKeyword(hdr);
|
|
|
|
// 2. Create shader
|
|
var shader = new ShaderProgramBuilder()
|
|
.WithName("PBR")
|
|
.AddPass("Forward", RenderState.Default)
|
|
.AddPass("Shadow", shadowState)
|
|
.DeclareKeywords(normalMap)
|
|
.Build();
|
|
|
|
// 3. Create material
|
|
var material = new Material(shader);
|
|
material.SetVector4("_BaseColor", 1, 0, 0, 1);
|
|
material.SetFloat("_Metallic", 0.8f);
|
|
material.EnableKeyword(normalMap);
|
|
|
|
// 4. Render
|
|
var batchRenderer = new MaterialBatchRenderer(compiler, pipelineLib);
|
|
var batches = batchRenderer.BatchDrawCalls(drawCommands);
|
|
|
|
foreach (var batch in batches) {
|
|
commandList.SetPipeline(batch.Pipeline);
|
|
foreach (var draw in batch.DrawCommands) {
|
|
unsafe {
|
|
draw.Material.CopyPropertiesTo(cbufferPtr, cbufferSize);
|
|
}
|
|
commandList.DrawIndexed(draw.IndexCount, ...);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Advanced Features
|
|
|
|
### Per-Pass State Overrides
|
|
|
|
Materials can override render state per-pass:
|
|
|
|
```csharp
|
|
var transparentState = RenderState.Default;
|
|
transparentState.BlendEnable = true;
|
|
transparentState.SrcBlend = BlendFactor.SrcAlpha;
|
|
transparentState.DestBlend = BlendFactor.InvSrcAlpha;
|
|
|
|
material.SetPassRenderState("Forward", transparentState);
|
|
material.SetPassRenderState("Shadow", RenderState.Default); // Opaque shadow
|
|
```
|
|
|
|
### Shader Variant Warmup
|
|
|
|
Pre-compile common variants to avoid runtime hitches:
|
|
|
|
```csharp
|
|
var variants = new[] {
|
|
keywordSet1, // No features
|
|
keywordSet2, // Normal map only
|
|
keywordSet3, // Normal map + alpha test
|
|
// ...
|
|
};
|
|
|
|
await batchRenderer.WarmupVariantsAsync(shader, variants);
|
|
```
|
|
|
|
### Material Property Inheritance
|
|
|
|
Clone materials with shared base properties:
|
|
|
|
```csharp
|
|
var baseMaterial = new Material(shader);
|
|
baseMaterial.SetVector4("_BaseColor", 1, 1, 1, 1);
|
|
|
|
var redVariant = baseMaterial.Clone();
|
|
redVariant.SetVector4("_BaseColor", 1, 0, 0, 1);
|
|
|
|
var blueVariant = baseMaterial.Clone();
|
|
blueVariant.SetVector4("_BaseColor", 0, 0, 1, 1);
|
|
```
|
|
|
|
---
|
|
|
|
## Extension Points
|
|
|
|
### Custom Property Types
|
|
|
|
Extend `MaterialPropertyBlock` for custom data:
|
|
|
|
```csharp
|
|
public void SetCustomStruct<T>(string name, T value) where T : unmanaged
|
|
{
|
|
// Implementation
|
|
}
|
|
```
|
|
|
|
### Material Property Validation
|
|
|
|
Add validation in `Material` setters:
|
|
|
|
```csharp
|
|
public void SetFloat(string name, float value)
|
|
{
|
|
if (value < 0 || value > 1)
|
|
throw new ArgumentOutOfRangeException();
|
|
_propertyBlock.SetFloat(name, value);
|
|
}
|
|
```
|
|
|
|
### Custom Batching Strategies
|
|
|
|
Subclass or compose with `MaterialBatchRenderer`:
|
|
|
|
```csharp
|
|
public class DepthSortedBatchRenderer : MaterialBatchRenderer
|
|
{
|
|
public override MaterialBatch[] BatchDrawCalls(...)
|
|
{
|
|
var batches = base.BatchDrawCalls(...);
|
|
// Custom depth sorting logic
|
|
return batches;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Comparison to Other Engines
|
|
|
|
| Feature | Ghost | Unity URP | Unreal 5 | Godot 4 |
|
|
|---------|-------|-----------|----------|---------|
|
|
| Keyword System | Global + Local | Global + Local | Static + Dynamic | Static |
|
|
| Multi-pass | Native | SubShader | Material Functions | Multi-pass |
|
|
| Per-pass Override | ✓ | Limited | ✓ | ✓ |
|
|
| Variant Caching | Auto | Auto | Auto | Auto |
|
|
| Batch Optimization | PSO-based | SRP Batcher | Nanite/VSM | Clustered |
|
|
| Unsafe/Native | ✓ | ✓ (Jobs) | ✓ (C++) | Limited |
|
|
|
|
---
|
|
|
|
## Future Enhancements
|
|
|
|
1. **GPU-Driven Rendering**: Indirect draws, culling on GPU
|
|
2. **Material Graphs**: Node-based shader authoring
|
|
3. **Hot Reload**: Runtime shader recompilation
|
|
4. **Texture Support**: Bindless textures, virtual texturing
|
|
5. **Compute Shaders**: Material property animation on GPU
|
|
6. **Serialization**: Material asset loading/saving
|
|
|
|
---
|
|
|
|
## License
|
|
|
|
This is a concept/demonstration project. Adapt as needed for your engine.
|