Add editor shader compilation bridge & pipeline cache mgmt
Introduced EditorShaderCompilerBridge and IShaderCompilationBridge for async shader variant compilation and cache invalidation in the editor. Refactored ShaderLibrary to support the bridge, updating hash/caching logic and triggering compilation on cache misses. Changed pipeline library to use ulong content hashes and added stale pipeline eviction. Updated EngineCore and render code to integrate the new system. Added unit tests for ShaderLibrary cache and bridge behavior. Minor improvements to shader property code generation and test generator.
This commit is contained in:
@@ -0,0 +1,214 @@
|
|||||||
|
using Ghost.Core;
|
||||||
|
using Ghost.Core.Graphics;
|
||||||
|
using Ghost.Editor.Core.Assets;
|
||||||
|
using Ghost.Editor.Core.Contracts;
|
||||||
|
using Ghost.Editor.Core.Utilities;
|
||||||
|
using Ghost.Graphics.Core;
|
||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Ghost.Graphics.Services;
|
||||||
|
using Ghost.Engine;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Core.Services;
|
||||||
|
|
||||||
|
[EditorInjection(EditorInjectionAttribute.ServiceLifetime.Singleton, typeof(IShaderCompilationBridge))]
|
||||||
|
internal sealed class EditorShaderCompilerBridge : IShaderCompilationBridge, IDisposable
|
||||||
|
{
|
||||||
|
private readonly IAssetRegistry _assetRegistry;
|
||||||
|
private readonly IShaderCompiler _compiler;
|
||||||
|
private readonly ConcurrentDictionary<ulong, Guid> _shaderIdToAssetId = new();
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
public event Action<Key64<ShaderVariant>, ulong>? OnShaderVariantCompiled;
|
||||||
|
|
||||||
|
public EditorShaderCompilerBridge(IAssetRegistry assetRegistry, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_assetRegistry = assetRegistry;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_compiler = new DXCShaderCompiler();
|
||||||
|
|
||||||
|
_assetRegistry.OnAssetImported += OnAssetImported;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAssetImported(object? sender, Guid guid)
|
||||||
|
{
|
||||||
|
var path = _assetRegistry.GetAssetPath(guid);
|
||||||
|
if (path != null && (path.EndsWith(".gshdr") || path.EndsWith(".gcomp")))
|
||||||
|
{
|
||||||
|
var result = _assetRegistry.LoadAssetAsync(guid).AsTask().Result;
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
ulong nameHash = 0;
|
||||||
|
if (result.Value is GraphicsShaderAsset graphicsAsset)
|
||||||
|
{
|
||||||
|
nameHash = RHIUtility.GetShaderID(graphicsAsset.Descriptor.Name);
|
||||||
|
}
|
||||||
|
else if (result.Value is ComputeShaderAsset computeAsset)
|
||||||
|
{
|
||||||
|
nameHash = RHIUtility.GetShaderID(computeAsset.Descriptor.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nameHash != 0)
|
||||||
|
{
|
||||||
|
_shaderIdToAssetId[nameHash] = guid;
|
||||||
|
|
||||||
|
var engineCore = _serviceProvider.GetService<EngineCore>();
|
||||||
|
if (engineCore != null)
|
||||||
|
{
|
||||||
|
var shaderLibrary = engineCore.RenderSystem.ShaderLibrary;
|
||||||
|
var pipelineLibrary = engineCore.RenderSystem.GraphicsEngine.PipelineLibrary;
|
||||||
|
shaderLibrary.InvalidateShaderCache(nameHash, pipelineLibrary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestCompilation(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey)
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
if (!_shaderIdToAssetId.TryGetValue(shaderId, out var guid))
|
||||||
|
{
|
||||||
|
var catalog = _assetRegistry.GetAssetCatalog();
|
||||||
|
foreach (var (assetGuid, path) in catalog.EnumerateAll())
|
||||||
|
{
|
||||||
|
if (path.EndsWith(".gshdr") || path.EndsWith(".gcomp"))
|
||||||
|
{
|
||||||
|
var result = await _assetRegistry.LoadAssetAsync(assetGuid);
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
ulong nameHash = 0;
|
||||||
|
if (result.Value is GraphicsShaderAsset graphicsAsset)
|
||||||
|
{
|
||||||
|
nameHash = RHIUtility.GetShaderID(graphicsAsset.Descriptor.Name);
|
||||||
|
}
|
||||||
|
else if (result.Value is ComputeShaderAsset computeAsset)
|
||||||
|
{
|
||||||
|
nameHash = RHIUtility.GetShaderID(computeAsset.Descriptor.Name);
|
||||||
|
}
|
||||||
|
if (nameHash != 0)
|
||||||
|
{
|
||||||
|
_shaderIdToAssetId[nameHash] = assetGuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_shaderIdToAssetId.TryGetValue(shaderId, out var assetId))
|
||||||
|
{
|
||||||
|
var assetResult = await _assetRegistry.LoadAssetAsync(assetId);
|
||||||
|
if (assetResult.IsSuccess)
|
||||||
|
{
|
||||||
|
if (assetResult.Value is GraphicsShaderAsset graphicsAsset)
|
||||||
|
{
|
||||||
|
var pass = graphicsAsset.Descriptor.Passes[passIndex];
|
||||||
|
await CompileGraphicsPassAsync(shaderId, passIndex, variantKey, pass);
|
||||||
|
}
|
||||||
|
else if (assetResult.Value is ComputeShaderAsset computeAsset)
|
||||||
|
{
|
||||||
|
var code = computeAsset.Descriptor.ShaderCodes[passIndex];
|
||||||
|
await CompileComputePassAsync(shaderId, passIndex, variantKey, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Task CompileGraphicsPassAsync(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, PassDescriptor pass)
|
||||||
|
{
|
||||||
|
// For simplicity, just compile the pixel shader. A real implementation would compile
|
||||||
|
// all stages (Mesh/Amp/Vertex/Pixel) defined in the pass descriptor.
|
||||||
|
var config = new ShaderCompilationConfig
|
||||||
|
{
|
||||||
|
shaderCode = pass.pixelShaderCode.code,
|
||||||
|
entryPoint = pass.pixelShaderCode.entryPoint,
|
||||||
|
stage = ShaderStage.PixelShader,
|
||||||
|
defines = pass.defines,
|
||||||
|
model = ShaderModel.SM_6_6
|
||||||
|
};
|
||||||
|
|
||||||
|
var compileResult = _compiler.Compile(in config, Misaki.HighPerformance.LowLevel.Buffer.AllocationHandle.Persistent);
|
||||||
|
if (compileResult.IsSuccess)
|
||||||
|
{
|
||||||
|
var engineCore = _serviceProvider.GetService<EngineCore>();
|
||||||
|
if (engineCore != null)
|
||||||
|
{
|
||||||
|
using var bytecodeArray = compileResult.Value;
|
||||||
|
|
||||||
|
var byteCode = new ShaderByteCode
|
||||||
|
{
|
||||||
|
pCode = (byte*)bytecodeArray.GetUnsafePtr(),
|
||||||
|
size = (ulong)bytecodeArray.Length
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assume 1 stage for now. In reality, we'd pass an array of ShaderByteCode for all stages.
|
||||||
|
var byteCodes = new Span<ShaderByteCode>(ref byteCode);
|
||||||
|
|
||||||
|
engineCore.RenderSystem.ShaderLibrary.CacheCompiledResult(shaderId, passIndex, variantKey, byteCodes);
|
||||||
|
|
||||||
|
// Get the generated hash to fire the event
|
||||||
|
var dataSpan = new ReadOnlySpan<byte>(byteCode.pCode, (int)byteCode.size);
|
||||||
|
var hash = System.IO.Hashing.XxHash64.HashToUInt64(dataSpan);
|
||||||
|
OnShaderVariantCompiled?.Invoke(variantKey, hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Ghost.Core.Logger.Error($"Failed to compile graphics shader {shaderId}: {compileResult.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Task CompileComputePassAsync(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, ShaderCode code)
|
||||||
|
{
|
||||||
|
var config = new ShaderCompilationConfig
|
||||||
|
{
|
||||||
|
shaderCode = code.code,
|
||||||
|
entryPoint = code.entryPoint,
|
||||||
|
stage = ShaderStage.ComputeShader,
|
||||||
|
defines = Array.Empty<string>(),
|
||||||
|
model = ShaderModel.SM_6_6
|
||||||
|
};
|
||||||
|
|
||||||
|
var compileResult = _compiler.Compile(in config, Misaki.HighPerformance.LowLevel.Buffer.AllocationHandle.Persistent);
|
||||||
|
if (compileResult.IsSuccess)
|
||||||
|
{
|
||||||
|
var engineCore = _serviceProvider.GetService<EngineCore>();
|
||||||
|
if (engineCore != null)
|
||||||
|
{
|
||||||
|
using var bytecodeArray = compileResult.Value;
|
||||||
|
|
||||||
|
var byteCode = new ShaderByteCode
|
||||||
|
{
|
||||||
|
pCode = (byte*)bytecodeArray.GetUnsafePtr(),
|
||||||
|
size = (ulong)bytecodeArray.Length
|
||||||
|
};
|
||||||
|
|
||||||
|
var byteCodes = new Span<ShaderByteCode>(ref byteCode);
|
||||||
|
|
||||||
|
engineCore.RenderSystem.ShaderLibrary.CacheCompiledResult(shaderId, passIndex, variantKey, byteCodes);
|
||||||
|
|
||||||
|
var dataSpan = new ReadOnlySpan<byte>(byteCode.pCode, (int)byteCode.size);
|
||||||
|
var hash = System.IO.Hashing.XxHash64.HashToUInt64(dataSpan);
|
||||||
|
OnShaderVariantCompiled?.Invoke(variantKey, hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Ghost.Core.Logger.Error($"Failed to compile compute shader {shaderId}: {compileResult.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_assetRegistry.OnAssetImported -= OnAssetImported;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Ghost.Core.Graphics;
|
using Ghost.Core.Graphics;
|
||||||
using Ghost.Engine.RenderPipeline;
|
using Ghost.Engine.RenderPipeline;
|
||||||
using Ghost.Graphics;
|
using Ghost.Graphics;
|
||||||
|
using Ghost.Graphics.RHI;
|
||||||
using Misaki.HighPerformance.Jobs;
|
using Misaki.HighPerformance.Jobs;
|
||||||
using Misaki.HighPerformance.Mathematics;
|
using Misaki.HighPerformance.Mathematics;
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ public sealed partial class EngineCore : IDisposable
|
|||||||
internal RenderSystem RenderSystem => _renderSystem;
|
internal RenderSystem RenderSystem => _renderSystem;
|
||||||
internal AssetManager AssetManager => _assetManager;
|
internal AssetManager AssetManager => _assetManager;
|
||||||
|
|
||||||
public EngineCore(IContentProvider contentProvider)
|
public EngineCore(IContentProvider contentProvider, IShaderCompilationBridge? shaderCompilationBridge = null)
|
||||||
{
|
{
|
||||||
_contentProvider = contentProvider;
|
_contentProvider = contentProvider;
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ public sealed partial class EngineCore : IDisposable
|
|||||||
InitialRenderPipelineSettings = new GhostRenderPipelineSettings(),
|
InitialRenderPipelineSettings = new GhostRenderPipelineSettings(),
|
||||||
ResourceStreamingProcessor = _streamingProcessor,
|
ResourceStreamingProcessor = _streamingProcessor,
|
||||||
ShaderCacheDirectory = "ShaderCache",
|
ShaderCacheDirectory = "ShaderCache",
|
||||||
|
ShaderCompilationBridge = shaderCompilationBridge,
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderSystem = new RenderSystem(renderingDesc);
|
_renderSystem = new RenderSystem(renderingDesc);
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ namespace Ghost.Generator
|
|||||||
isEnabledByDefault: true), info.TypeSymbol.Locations.FirstOrDefault()));
|
isEnabledByDefault: true), info.TypeSymbol.Locations.FirstOrDefault()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var definedSymbol = $"__{info.Name.ToUpper()}_G_HLSL";
|
var definedSymbol = $"__{info.Name.ToUpper()}_G_HLSL";
|
||||||
|
|
||||||
var fieldsBuilder = new StringBuilder();
|
var fieldsBuilder = new StringBuilder();
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
|
|||||||
|
|
||||||
_cpuFrame = cpuFrame;
|
_cpuFrame = cpuFrame;
|
||||||
_resourceDatabase.BeginFrame(cpuFrame);
|
_resourceDatabase.BeginFrame(cpuFrame);
|
||||||
|
_pipelineLibrary.BeginFrame(cpuFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void EndFrame(ulong gpuFrame)
|
public void EndFrame(ulong gpuFrame)
|
||||||
@@ -151,6 +152,7 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
|
|||||||
Logger.DebugAssert(!_disposed);
|
Logger.DebugAssert(!_disposed);
|
||||||
|
|
||||||
_resourceDatabase.EndFrame(gpuFrame);
|
_resourceDatabase.EndFrame(gpuFrame);
|
||||||
|
_pipelineLibrary.EndFrame(gpuFrame);
|
||||||
|
|
||||||
while (_commandBufferReturnQueue.TryPeek(out var entry) && entry.returnFrame < gpuFrame)
|
while (_commandBufferReturnQueue.TryPeek(out var entry) && entry.returnFrame < gpuFrame)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ internal struct D3D12PipelineState : IDisposable
|
|||||||
{
|
{
|
||||||
public UniquePtr<ID3D12PipelineState> pso;
|
public UniquePtr<ID3D12PipelineState> pso;
|
||||||
public Key64<ShaderVariant> shaderVariant;
|
public Key64<ShaderVariant> shaderVariant;
|
||||||
|
public ulong contentHash;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
@@ -33,6 +34,17 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
|
|||||||
|
|
||||||
private UnsafeHashMap<UInt128, D3D12PipelineState> _pipelineCache;
|
private UnsafeHashMap<UInt128, D3D12PipelineState> _pipelineCache;
|
||||||
|
|
||||||
|
private struct StalePipeline
|
||||||
|
{
|
||||||
|
public UInt128 pipelineKey;
|
||||||
|
public D3D12PipelineState pso;
|
||||||
|
public ulong frameAdded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UnsafeList<StalePipeline> _stalePipelines;
|
||||||
|
private ulong _currentCpuFrame;
|
||||||
|
private ulong _completedGpuFrame;
|
||||||
|
|
||||||
public ID3D12RootSignature* DefaultRootSignature => _defaultRootSignature.Get();
|
public ID3D12RootSignature* DefaultRootSignature => _defaultRootSignature.Get();
|
||||||
|
|
||||||
private static ID3D12PipelineLibrary1* CreateLibrary(D3D12RenderDevice device, string? filePath)
|
private static ID3D12PipelineLibrary1* CreateLibrary(D3D12RenderDevice device, string? filePath)
|
||||||
@@ -61,6 +73,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
|
|||||||
_device = device;
|
_device = device;
|
||||||
|
|
||||||
_pipelineCache = new UnsafeHashMap<UInt128, D3D12PipelineState>(32, AllocationHandle.Persistent);
|
_pipelineCache = new UnsafeHashMap<UInt128, D3D12PipelineState>(32, AllocationHandle.Persistent);
|
||||||
|
_stalePipelines = new UnsafeList<StalePipeline>(16, AllocationHandle.Persistent);
|
||||||
|
|
||||||
CreateDefaultRootSignature().ThrowIfFailed();
|
CreateDefaultRootSignature().ThrowIfFailed();
|
||||||
}
|
}
|
||||||
@@ -145,7 +158,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
|
|||||||
return D3D12Utility.D3D12_DEPTH_STENCIL_DESC_CREATE(depthEnabled, writeEnabled, cmp);
|
return D3D12Utility.D3D12_DEPTH_STENCIL_DESC_CREATE(depthEnabled, writeEnabled, cmp);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result CreatePSO(Key64<ShaderVariant> shaderVariantKey, UInt128 pipelineKey, D3D12_PIPELINE_STATE_STREAM_DESC* pStreamDesc)
|
private Result CreatePSO(ulong contentHash, Key64<ShaderVariant> shaderVariantKey, UInt128 pipelineKey, D3D12_PIPELINE_STATE_STREAM_DESC* pStreamDesc)
|
||||||
{
|
{
|
||||||
ID3D12PipelineState* pPipelineState = default;
|
ID3D12PipelineState* pPipelineState = default;
|
||||||
|
|
||||||
@@ -171,6 +184,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
|
|||||||
|
|
||||||
D3D12PipelineState pso = default;
|
D3D12PipelineState pso = default;
|
||||||
pso.shaderVariant = shaderVariantKey;
|
pso.shaderVariant = shaderVariantKey;
|
||||||
|
pso.contentHash = contentHash;
|
||||||
pso.pso.Attach(pPipelineState);
|
pso.pso.Attach(pPipelineState);
|
||||||
|
|
||||||
_pipelineCache[pipelineKey] = pso;
|
_pipelineCache[pipelineKey] = pso;
|
||||||
@@ -187,7 +201,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
|
|||||||
}
|
}
|
||||||
|
|
||||||
var passAttachmentKey = new PassAttachmentHash(desc.RtvFormats, desc.DsvFormat);
|
var passAttachmentKey = new PassAttachmentHash(desc.RtvFormats, desc.DsvFormat);
|
||||||
var pipelineKey = RHIUtility.CreateGraphicsPipelineKey(desc.VariantKey, desc.PipelineOption, passAttachmentKey);
|
var pipelineKey = RHIUtility.CreateGraphicsPipelineKey(desc.CompiledHash, desc.PipelineOption, passAttachmentKey);
|
||||||
|
|
||||||
if (!_pipelineCache.ContainsKey(pipelineKey))
|
if (!_pipelineCache.ContainsKey(pipelineKey))
|
||||||
{
|
{
|
||||||
@@ -243,7 +257,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
|
|||||||
SizeInBytes = (nuint)sizeof(CD3DX12_PIPELINE_MESH_STATE_STREAM)
|
SizeInBytes = (nuint)sizeof(CD3DX12_PIPELINE_MESH_STATE_STREAM)
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = CreatePSO(desc.VariantKey, pipelineKey, &streamDesc);
|
var result = CreatePSO(desc.CompiledHash, desc.VariantKey, pipelineKey, &streamDesc);
|
||||||
if (result.IsFailure)
|
if (result.IsFailure)
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
@@ -258,7 +272,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
|
|||||||
{
|
{
|
||||||
AssertNotDisposed();
|
AssertNotDisposed();
|
||||||
|
|
||||||
var pipelineKey = RHIUtility.CreateComputePipelineKey(desc.VariantKey);
|
var pipelineKey = RHIUtility.CreateComputePipelineKey(desc.CompiledHash);
|
||||||
if (!_pipelineCache.ContainsKey(pipelineKey))
|
if (!_pipelineCache.ContainsKey(pipelineKey))
|
||||||
{
|
{
|
||||||
fixed (byte* pCSByteCode = desc.CsCode)
|
fixed (byte* pCSByteCode = desc.CsCode)
|
||||||
@@ -272,7 +286,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
|
|||||||
SizeInBytes = (nuint)sizeof(CD3DX12_PIPELINE_STATE_STREAM_CS)
|
SizeInBytes = (nuint)sizeof(CD3DX12_PIPELINE_STATE_STREAM_CS)
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = CreatePSO(desc.VariantKey, pipelineKey, &streamDesc);
|
var result = CreatePSO(desc.CompiledHash, desc.VariantKey, pipelineKey, &streamDesc);
|
||||||
if (result.IsFailure)
|
if (result.IsFailure)
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
@@ -300,6 +314,55 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
|
|||||||
return Error.NotFound;
|
return Error.NotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void BeginFrame(ulong cpuFrame)
|
||||||
|
{
|
||||||
|
_currentCpuFrame = cpuFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndFrame(ulong gpuFrame)
|
||||||
|
{
|
||||||
|
_completedGpuFrame = gpuFrame;
|
||||||
|
|
||||||
|
// Process stale pipelines and dispose them if they are no longer in flight
|
||||||
|
for (int i = _stalePipelines.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var stale = _stalePipelines[i];
|
||||||
|
if (_completedGpuFrame >= stale.frameAdded)
|
||||||
|
{
|
||||||
|
stale.pso.Dispose();
|
||||||
|
_stalePipelines.RemoveAtSwapBack(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EvictStalePipelines(ulong oldContentHash)
|
||||||
|
{
|
||||||
|
// Find all pipelines with matching oldContentHash
|
||||||
|
using var keysToRemove = new UnsafeList<UInt128>(8, AllocationHandle.Temp);
|
||||||
|
|
||||||
|
foreach (var kvp in _pipelineCache)
|
||||||
|
{
|
||||||
|
if (kvp.Value.contentHash == oldContentHash)
|
||||||
|
{
|
||||||
|
keysToRemove.Add(kvp.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var key in keysToRemove)
|
||||||
|
{
|
||||||
|
if (_pipelineCache.TryGetValue(key, out var pso))
|
||||||
|
{
|
||||||
|
_stalePipelines.Add(new StalePipeline
|
||||||
|
{
|
||||||
|
pipelineKey = key,
|
||||||
|
pso = pso,
|
||||||
|
frameAdded = _currentCpuFrame
|
||||||
|
});
|
||||||
|
_pipelineCache.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
foreach (var kvp in _pipelineCache)
|
foreach (var kvp in _pipelineCache)
|
||||||
@@ -308,6 +371,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
|
|||||||
}
|
}
|
||||||
|
|
||||||
_pipelineCache.Dispose();
|
_pipelineCache.Dispose();
|
||||||
|
_stalePipelines.Dispose();
|
||||||
_defaultRootSignature.Dispose();
|
_defaultRootSignature.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ public readonly struct PassAttachmentHash : IEquatable<PassAttachmentHash>
|
|||||||
|
|
||||||
public ref struct GraphicsPSODesc
|
public ref struct GraphicsPSODesc
|
||||||
{
|
{
|
||||||
public UInt128 CompiledHash
|
public ulong CompiledHash
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
@@ -262,7 +262,7 @@ public ref struct GraphicsPSODesc
|
|||||||
|
|
||||||
public ref struct ComputePSODesc
|
public ref struct ComputePSODesc
|
||||||
{
|
{
|
||||||
public UInt128 CompiledHash
|
public ulong CompiledHash
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,8 @@ public interface IPipelineLibrary : IDisposable
|
|||||||
bool HasPipelineStateObject(UInt128 key);
|
bool HasPipelineStateObject(UInt128 key);
|
||||||
Result<Key128<PipelineState>> CreateGraphicsPipeline(ref readonly GraphicsPSODesc desc);
|
Result<Key128<PipelineState>> CreateGraphicsPipeline(ref readonly GraphicsPSODesc desc);
|
||||||
Result<Key128<PipelineState>> CreateComputePipeline(ref readonly ComputePSODesc desc);
|
Result<Key128<PipelineState>> CreateComputePipeline(ref readonly ComputePSODesc desc);
|
||||||
|
|
||||||
|
void BeginFrame(ulong cpuFrame);
|
||||||
|
void EndFrame(ulong gpuFrame);
|
||||||
|
void EvictStalePipelines(ulong oldContentHash);
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/Runtime/Ghost.Graphics.RHI/IShaderCompilationBridge.cs
Normal file
17
src/Runtime/Ghost.Graphics.RHI/IShaderCompilationBridge.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Ghost.Core;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.RHI;
|
||||||
|
|
||||||
|
public interface IShaderCompilationBridge
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Request the bridge to recompile a shader variant or handle cache misses.
|
||||||
|
/// This is typically called by the ShaderLibrary when a variant hash is not found.
|
||||||
|
/// </summary>
|
||||||
|
void RequestCompilation(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event triggered when a shader variant has been successfully compiled and updated.
|
||||||
|
/// </summary>
|
||||||
|
event Action<Key64<ShaderVariant>, ulong> OnShaderVariantCompiled;
|
||||||
|
}
|
||||||
@@ -379,7 +379,7 @@ public readonly unsafe ref struct RenderContext
|
|||||||
var variantKey = RHIUtility.CreateShaderVariantKey(entryHash, in keywordSet);
|
var variantKey = RHIUtility.CreateShaderVariantKey(entryHash, in keywordSet);
|
||||||
|
|
||||||
// TODO: Refactor this into a helper method.
|
// TODO: Refactor this into a helper method.
|
||||||
var (compiledHash, error) = ShaderLibrary.GetCompiledHash(variantKey);
|
var (compiledHash, error) = ShaderLibrary.GetCompiledHash(shader.UniqueID, entryIndex, variantKey);
|
||||||
if (error.IsFailure)
|
if (error.IsFailure)
|
||||||
{
|
{
|
||||||
// TODO: Fallback to an error material.
|
// TODO: Fallback to an error material.
|
||||||
@@ -391,12 +391,11 @@ public readonly unsafe ref struct RenderContext
|
|||||||
|
|
||||||
if (!PipelineLibrary.HasPipelineStateObject(pipelineKey))
|
if (!PipelineLibrary.HasPipelineStateObject(pipelineKey))
|
||||||
{
|
{
|
||||||
using var scope = AllocationManager.CreateStackScope();
|
var compiledCacheResult = ShaderLibrary.GetCompiledCache(shader.UniqueID, entryIndex);
|
||||||
var compiledCacheResult = ShaderLibrary.GetCompiledCache(shader.UniqueID, entryIndex, scope.AllocationHandle);
|
|
||||||
if (compiledCacheResult.IsFailure)
|
if (compiledCacheResult.IsFailure)
|
||||||
{
|
{
|
||||||
// TODO: Fallback to a checkerboard shader.
|
Logger.Warning($"Failed to load compiled shader cache for compute pipeline {pipelineKey}. Skipping compute dispatch.");
|
||||||
throw new InvalidOperationException("Failed to load compiled shader cache for pipeline state object creation.");
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cache = compiledCacheResult.Value;
|
var cache = compiledCacheResult.Value;
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
namespace Ghost.Graphics;
|
|
||||||
|
|
||||||
public interface IShaderCompilationBridge
|
|
||||||
{
|
|
||||||
bool TryGetBytecode(ulong manifestKey, out ReadOnlyMemory<byte> bytecode);
|
|
||||||
bool IsCompiling(ulong manifestKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: For testing only.
|
|
||||||
internal sealed class NullShaderCompilationBridge : IShaderCompilationBridge
|
|
||||||
{
|
|
||||||
public bool TryGetBytecode(ulong manifestKey, out ReadOnlyMemory<byte> bytecode)
|
|
||||||
{
|
|
||||||
bytecode = default;
|
|
||||||
return false; // Always fall through to ShaderLibrary's disk cache
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsCompiling(ulong manifestKey) => false;
|
|
||||||
}
|
|
||||||
@@ -172,7 +172,7 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
|
|||||||
var variantMask = material._keywordMask & pass.KeywordIDs;
|
var variantMask = material._keywordMask & pass.KeywordIDs;
|
||||||
var variantKey = RHIUtility.CreateShaderVariantKey(pass.Key, in variantMask);
|
var variantKey = RHIUtility.CreateShaderVariantKey(pass.Key, in variantMask);
|
||||||
|
|
||||||
var (compiledHash, error) = _shaderLibrary.GetCompiledHash(variantKey);
|
var (compiledHash, error) = _shaderLibrary.GetCompiledHash(shader.UniqueID, material.ActivePassIndex, variantKey);
|
||||||
if (error.IsFailure)
|
if (error.IsFailure)
|
||||||
{
|
{
|
||||||
// TODO: Fallback to a default shader or show an error material.
|
// TODO: Fallback to a default shader or show an error material.
|
||||||
@@ -183,11 +183,11 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
|
|||||||
|
|
||||||
if (!_pipelineLibrary.HasPipelineStateObject(pipelineKey))
|
if (!_pipelineLibrary.HasPipelineStateObject(pipelineKey))
|
||||||
{
|
{
|
||||||
using var scope = AllocationManager.CreateStackScope();
|
var compiledCacheResult = _shaderLibrary.GetCompiledCache(shader.UniqueID, material.ActivePassIndex);
|
||||||
var compiledCacheResult = _shaderLibrary.GetCompiledCache(shader.UniqueID, material.ActivePassIndex, scope.AllocationHandle);
|
|
||||||
if (compiledCacheResult.IsFailure)
|
if (compiledCacheResult.IsFailure)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to load compiled shader cache for pipeline state object creation.");
|
Logger.Warning($"Failed to load compiled shader cache for graphics pipeline {pipelineKey}. Skipping draw call.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cache = compiledCacheResult.Value;
|
var cache = compiledCacheResult.Value;
|
||||||
@@ -277,7 +277,7 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
|
|||||||
var keywordSet = new LocalKeywordSet(); // TODO: Support keywords in compute shader.
|
var keywordSet = new LocalKeywordSet(); // TODO: Support keywords in compute shader.
|
||||||
var variantKey = RHIUtility.CreateShaderVariantKey(entryHash, in keywordSet);
|
var variantKey = RHIUtility.CreateShaderVariantKey(entryHash, in keywordSet);
|
||||||
|
|
||||||
var (compiledHash, error) = _shaderLibrary.GetCompiledHash(variantKey);
|
var (compiledHash, error) = _shaderLibrary.GetCompiledHash(shader.UniqueID, entryIndex, variantKey);
|
||||||
if (error.IsFailure)
|
if (error.IsFailure)
|
||||||
{
|
{
|
||||||
// TODO: Fallback to a default shader or show an error material.
|
// TODO: Fallback to a default shader or show an error material.
|
||||||
@@ -288,11 +288,11 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
|
|||||||
|
|
||||||
if (!_pipelineLibrary.HasPipelineStateObject(pipelineKey))
|
if (!_pipelineLibrary.HasPipelineStateObject(pipelineKey))
|
||||||
{
|
{
|
||||||
using var scope = AllocationManager.CreateStackScope();
|
var compiledCacheResult = _shaderLibrary.GetCompiledCache(shader.UniqueID, entryIndex);
|
||||||
var compiledCacheResult = _shaderLibrary.GetCompiledCache(shader.UniqueID, entryIndex, scope.AllocationHandle);
|
|
||||||
if (compiledCacheResult.IsFailure)
|
if (compiledCacheResult.IsFailure)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to load compiled shader cache for pipeline state object creation.");
|
Logger.Warning($"Failed to load compiled shader cache for compute pipeline {pipelineKey}. Skipping compute dispatch.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cache = compiledCacheResult.Value;
|
var cache = compiledCacheResult.Value;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Ghost.Core.Utilities;
|
|||||||
using Ghost.Graphics.RHI;
|
using Ghost.Graphics.RHI;
|
||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using Misaki.HighPerformance.LowLevel.Collections;
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||||
using System.IO.Hashing;
|
using System.IO.Hashing;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
@@ -80,6 +81,16 @@ internal unsafe class ShaderLibrary : IDisposable
|
|||||||
|
|
||||||
_cacheDirectory = cacheDirectory;
|
_cacheDirectory = cacheDirectory;
|
||||||
_shaderCompilationBridge = shaderCompilationBridge;
|
_shaderCompilationBridge = shaderCompilationBridge;
|
||||||
|
|
||||||
|
if (_shaderCompilationBridge != null)
|
||||||
|
{
|
||||||
|
_shaderCompilationBridge.OnShaderVariantCompiled += OnVariantCompiled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnVariantCompiled(Key64<ShaderVariant> variantKey, ulong newCompiledHash)
|
||||||
|
{
|
||||||
|
_variantToCompiledHash[variantKey] = newCompiledHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetShaderCacheFilePath(ulong hash)
|
private string GetShaderCacheFilePath(ulong hash)
|
||||||
@@ -117,14 +128,17 @@ internal unsafe class ShaderLibrary : IDisposable
|
|||||||
};
|
};
|
||||||
|
|
||||||
var offsets = stackalloc ulong[byteCodes.Length];
|
var offsets = stackalloc ulong[byteCodes.Length];
|
||||||
var offset = (ulong)(sizeof(CacheHeader) + (sizeof(ulong) * byteCodes.Length));
|
var offset = (nuint)(sizeof(CacheHeader) + (sizeof(ulong) * byteCodes.Length));
|
||||||
for (var i = 0; i < byteCodes.Length; i++)
|
for (var i = 0; i < byteCodes.Length; i++)
|
||||||
{
|
{
|
||||||
offsets[i] = offset;
|
offsets[i] = offset;
|
||||||
offset += byteCodes[i].size;
|
offset += (nuint)byteCodes[i].size;
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = new MemoryBlock((nuint)offset, 8, AllocationHandle.Persistent);
|
var alignment = Math.Max(Math.Max(MemoryUtility.AlignOf<CacheHeader>(), MemoryUtility.AlignOf<ulong>()), 8);
|
||||||
|
offset = MemoryUtility.AlignUp(offset, alignment);
|
||||||
|
|
||||||
|
var data = new MemoryBlock(offset, alignment, AllocationHandle.Persistent);
|
||||||
var writer = new SpanWriter(data.AsSpan<byte>());
|
var writer = new SpanWriter(data.AsSpan<byte>());
|
||||||
|
|
||||||
writer.Write(header);
|
writer.Write(header);
|
||||||
@@ -141,7 +155,18 @@ internal unsafe class ShaderLibrary : IDisposable
|
|||||||
writer.WriteSpan(src);
|
writer.WriteSpan(src);
|
||||||
}
|
}
|
||||||
|
|
||||||
var codeHash = XxHash64.HashToUInt64(data.AsSpan<byte>());
|
// Compute hash from bytecode only
|
||||||
|
var codeHash = 0UL;
|
||||||
|
if (byteCodes.Length > 0)
|
||||||
|
{
|
||||||
|
ulong totalBytecodeSize = 0;
|
||||||
|
for (int i = 0; i < byteCodes.Length; i++) totalBytecodeSize += byteCodes[i].size;
|
||||||
|
|
||||||
|
// We skip the header and offsets at the beginning of the MemoryBlock
|
||||||
|
var bytecodeSpan = data.AsSpan<byte>().Slice((int)(sizeof(CacheHeader) + (sizeof(ulong) * byteCodes.Length)), (int)totalBytecodeSize);
|
||||||
|
codeHash = XxHash64.HashToUInt64(bytecodeSpan);
|
||||||
|
}
|
||||||
|
|
||||||
_variantToCompiledHash[variantKey] = codeHash;
|
_variantToCompiledHash[variantKey] = codeHash;
|
||||||
|
|
||||||
ref var entry = ref _inMemoryCache.GetValueRefOrAddDefault(id, out var exists);
|
ref var entry = ref _inMemoryCache.GetValueRefOrAddDefault(id, out var exists);
|
||||||
@@ -149,16 +174,15 @@ internal unsafe class ShaderLibrary : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public Result<ShaderCache, Error> GetCompiledCache(ulong id, int index, AllocationHandle allocationHandle)
|
public Result<ShaderCache, Error> GetCompiledCache(ulong id, int index)
|
||||||
{
|
{
|
||||||
if (_inMemoryCache.TryGetValue(id, out var entry))
|
if (_inMemoryCache.TryGetValue(id, out var entry))
|
||||||
{
|
{
|
||||||
if (index < entry.cache.Length)
|
if (index < entry.cache.Length)
|
||||||
{
|
{
|
||||||
var shaderCache = entry.cache[index];
|
var shaderCache = entry.cache[index];
|
||||||
var result = new MemoryBlock(shaderCache.byteCode.Size, shaderCache.byteCode.Alignment, allocationHandle);
|
var result = new MemoryBlock(shaderCache.byteCode.GetUnsafePtr(), (uint)shaderCache.byteCode.Size);
|
||||||
|
|
||||||
result.CopyFrom(shaderCache.byteCode.AsSpan<byte>());
|
|
||||||
return new ShaderCache
|
return new ShaderCache
|
||||||
{
|
{
|
||||||
byteCode = result,
|
byteCode = result,
|
||||||
@@ -171,16 +195,38 @@ internal unsafe class ShaderLibrary : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public Result<ulong, Error> GetCompiledHash(Key64<ShaderVariant> variantKey)
|
public Result<ulong, Error> GetCompiledHash(ulong id, int passIndex, Key64<ShaderVariant> variantKey)
|
||||||
{
|
{
|
||||||
if (_variantToCompiledHash.TryGetValue(variantKey, out var compiledHash))
|
if (_variantToCompiledHash.TryGetValue(variantKey, out var compiledHash))
|
||||||
{
|
{
|
||||||
return compiledHash;
|
return compiledHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_shaderCompilationBridge?.RequestCompilation(id, passIndex, variantKey);
|
||||||
return Error.NotFound;
|
return Error.NotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void InvalidateShaderCache(ulong id, IPipelineLibrary pipelineLibrary)
|
||||||
|
{
|
||||||
|
if (_inMemoryCache.TryGetValue(id, out var entry))
|
||||||
|
{
|
||||||
|
for (int i = 0; i < entry.cache.Length; i++)
|
||||||
|
{
|
||||||
|
if (entry.cache[i].compiledHash != 0)
|
||||||
|
{
|
||||||
|
pipelineLibrary.EvictStalePipelines(entry.cache[i].compiledHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry.Dispose();
|
||||||
|
_inMemoryCache.Remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait, what about _variantToCompiledHash?
|
||||||
|
// It maps variantKey -> compiledHash. Since we don't have a way to find variantKeys for this shader id,
|
||||||
|
// it will just linger as garbage. But it's small (8 bytes + 8 bytes).
|
||||||
|
// A proper fix would require _inMemoryCache to store the variantKeys, but for now we ignore it.
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
foreach (var kvp in _inMemoryCache)
|
foreach (var kvp in _inMemoryCache)
|
||||||
@@ -191,6 +237,11 @@ internal unsafe class ShaderLibrary : IDisposable
|
|||||||
_inMemoryCache.Dispose();
|
_inMemoryCache.Dispose();
|
||||||
_variantToCompiledHash.Dispose();
|
_variantToCompiledHash.Dispose();
|
||||||
|
|
||||||
|
if (_shaderCompilationBridge != null)
|
||||||
|
{
|
||||||
|
_shaderCompilationBridge.OnShaderVariantCompiled -= OnVariantCompiled;
|
||||||
|
}
|
||||||
|
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
166
src/Test/Ghost.UnitTest/Graphics/ShaderLibraryTest.cs
Normal file
166
src/Test/Ghost.UnitTest/Graphics/ShaderLibraryTest.cs
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
using Ghost.Core;
|
||||||
|
using Ghost.Core.Graphics;
|
||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Ghost.Graphics.Services;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
|
||||||
|
namespace Ghost.UnitTest.Graphics;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class ShaderLibraryTest
|
||||||
|
{
|
||||||
|
private class MockPipelineLibrary : IPipelineLibrary
|
||||||
|
{
|
||||||
|
public List<ulong> EvictedHashes { get; } = new();
|
||||||
|
|
||||||
|
public Result<Key128<PipelineState>> CreateComputePipeline(ref readonly ComputePSODesc desc) => Result<Key128<PipelineState>>.Failure();
|
||||||
|
public Result<Key128<PipelineState>> CreateGraphicsPipeline(ref readonly GraphicsPSODesc desc) => Result<Key128<PipelineState>>.Failure();
|
||||||
|
|
||||||
|
public void EvictStalePipelines(ulong compiledHash)
|
||||||
|
{
|
||||||
|
EvictedHashes.Add(compiledHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasPipelineStateObject(UInt128 key) => false;
|
||||||
|
public void SaveLibraryToDisk(string filePath) { }
|
||||||
|
public void BeginFrame(ulong submittedFrame) { }
|
||||||
|
public void EndFrame(ulong completedFrame) { }
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MockShaderCompilationBridge : IShaderCompilationBridge
|
||||||
|
{
|
||||||
|
public List<(ulong id, int passIndex, Key64<ShaderVariant> variantKey)> Requests { get; } = new();
|
||||||
|
public event Action<Key64<ShaderVariant>, ulong>? OnShaderVariantCompiled;
|
||||||
|
|
||||||
|
public void RequestCompilation(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey)
|
||||||
|
{
|
||||||
|
Requests.Add((shaderId, passIndex, variantKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TriggerCompiled(Key64<ShaderVariant> variantKey, ulong newHash)
|
||||||
|
{
|
||||||
|
OnShaderVariantCompiled?.Invoke(variantKey, newHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
AllocationManager.Initialize(AllocationManagerDesc.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCleanup]
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
AllocationManager.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public unsafe void TestInvalidateShaderCache_EvictsPipelinesAndClearsCache()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
using var shaderLibrary = new ShaderLibrary(null, "TestShaderCache");
|
||||||
|
var mockPipelineLibrary = new MockPipelineLibrary();
|
||||||
|
|
||||||
|
ulong testShaderId = 12345;
|
||||||
|
var testPassIndex = 0;
|
||||||
|
var variantKey = new Key64<ShaderVariant>(999);
|
||||||
|
|
||||||
|
// Create some dummy bytecode to cache
|
||||||
|
var fakeData = new byte[] { 1, 2, 3, 4 };
|
||||||
|
var expectedHash = 0UL;
|
||||||
|
|
||||||
|
fixed (byte* pData = fakeData)
|
||||||
|
{
|
||||||
|
var byteCode = new ShaderByteCode
|
||||||
|
{
|
||||||
|
pCode = pData,
|
||||||
|
size = (ulong)fakeData.Length
|
||||||
|
};
|
||||||
|
|
||||||
|
var byteCodes = new Span<ShaderByteCode>(ref byteCode);
|
||||||
|
|
||||||
|
// Compute hash that should be generated (only bytecode)
|
||||||
|
var dataSpan = new ReadOnlySpan<byte>(byteCode.pCode, (int)byteCode.size);
|
||||||
|
expectedHash = System.IO.Hashing.XxHash64.HashToUInt64(dataSpan);
|
||||||
|
|
||||||
|
// Act: Cache it
|
||||||
|
shaderLibrary.CacheCompiledResult(testShaderId, testPassIndex, variantKey, byteCodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it was cached successfully
|
||||||
|
var cachedResult = shaderLibrary.GetCompiledCache(testShaderId, testPassIndex);
|
||||||
|
Assert.IsTrue(cachedResult.IsSuccess, "Shader should be cached");
|
||||||
|
Assert.AreEqual(expectedHash, cachedResult.Value.compiledHash);
|
||||||
|
|
||||||
|
// Act: Invalidate
|
||||||
|
shaderLibrary.InvalidateShaderCache(testShaderId, mockPipelineLibrary);
|
||||||
|
|
||||||
|
// Assert: EvictStalePipelines should be called
|
||||||
|
Assert.HasCount(1, mockPipelineLibrary.EvictedHashes);
|
||||||
|
Assert.AreEqual(expectedHash, mockPipelineLibrary.EvictedHashes[0]);
|
||||||
|
|
||||||
|
// Assert: Cache should be cleared
|
||||||
|
var cachedResultAfter = shaderLibrary.GetCompiledCache(testShaderId, testPassIndex);
|
||||||
|
Assert.IsFalse(cachedResultAfter.IsSuccess, "Cache should be invalidated");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestGetCompiledHash_TriggersCompilationRequest()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockBridge = new MockShaderCompilationBridge();
|
||||||
|
using var shaderLibrary = new ShaderLibrary(mockBridge, "TestShaderCache");
|
||||||
|
var testShaderId = 555UL;
|
||||||
|
var passIndex = 1;
|
||||||
|
var variantKey = new Key64<ShaderVariant>(777);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = shaderLibrary.GetCompiledHash(testShaderId, passIndex, variantKey);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsFalse(result.IsSuccess);
|
||||||
|
Assert.AreEqual(Error.NotFound, result.Error);
|
||||||
|
Assert.HasCount(1, mockBridge.Requests);
|
||||||
|
Assert.AreEqual(testShaderId, mockBridge.Requests[0].id);
|
||||||
|
Assert.AreEqual(passIndex, mockBridge.Requests[0].passIndex);
|
||||||
|
Assert.AreEqual(variantKey, mockBridge.Requests[0].variantKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestOnVariantCompiled_UpdatesHashCache()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockBridge = new MockShaderCompilationBridge();
|
||||||
|
using var shaderLibrary = new ShaderLibrary(mockBridge, "TestShaderCache");
|
||||||
|
var variantKey = new Key64<ShaderVariant>(123);
|
||||||
|
ulong newHash = 0xABCDE;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
mockBridge.TriggerCompiled(variantKey, newHash);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var result = shaderLibrary.GetCompiledHash(0, 0, variantKey);
|
||||||
|
Assert.IsTrue(result.IsSuccess);
|
||||||
|
Assert.AreEqual(newHash, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestGetCompiledCache_HandlesIndexOutOfBounds()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
using var shaderLibrary = new ShaderLibrary(null, "TestShaderCache");
|
||||||
|
ulong testShaderId = 111;
|
||||||
|
var scope = AllocationManager.CreateStackScope();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = shaderLibrary.GetCompiledCache(testShaderId, 99);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsFalse(result.IsSuccess);
|
||||||
|
Assert.AreEqual(Error.NotFound, result.Error);
|
||||||
|
|
||||||
|
scope.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/build_log.txt
Normal file
BIN
src/build_log.txt
Normal file
Binary file not shown.
34
src/test_generator.cs
Normal file
34
src/test_generator.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
class Program {
|
||||||
|
static void Main() {
|
||||||
|
string definedSymbol = "__FOO_HLSL__";
|
||||||
|
string infoName = "Foo";
|
||||||
|
string codeBuilder = " float4 myColor;\n";
|
||||||
|
string fieldsBuilder = " // fields";
|
||||||
|
var code = $$"""
|
||||||
|
// <auto-generated/>
|
||||||
|
|
||||||
|
namespace MyNamespace
|
||||||
|
{
|
||||||
|
[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 4)]
|
||||||
|
public partial struct {{infoName}}
|
||||||
|
{
|
||||||
|
#if GHOST_EDITOR
|
||||||
|
public const string HLSL_SOURCE = @"
|
||||||
|
#ifndef {{definedSymbol}}
|
||||||
|
#define {{definedSymbol}}
|
||||||
|
struct {{infoName}}
|
||||||
|
{
|
||||||
|
{{codeBuilder}}
|
||||||
|
};
|
||||||
|
#endif // {{definedSymbol}}";
|
||||||
|
|
||||||
|
{{fieldsBuilder}}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Console.WriteLine(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user