4 Commits

Author SHA1 Message Date
1cc65e8218 refactor(shader): rewrite editor shader compilation bridge with keyword resolution
- Add AssetCatalog.EnumerateByTypes for filtered SQL queries
- Thread LocalKeywordSet through IShaderCompilationBridge API so the
  bridge can resolve keyword bitmask to string defines at compile time
- Eager/lazy popluation of shader-id-to-asset-id map eliminating
  full catalog scan per compilation miss
- Build keyword mapping from PassDescriptor groups to reconstruct
  localIndex -> keywordName in the bridge
- Use CompileShaderPass extension for multi-stage AS/MS/PS compilation
  with correct ShaderModel from descriptor
- Remove double-load of shader asset in compilation flow
- Update test mock to match new interface signature
2026-05-08 19:36:24 +09:00
d0076c852f 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.
2026-05-08 17:06:37 +09:00
ba8694ed0c Add shader property reflection and resource handles
Refactor shader property system to support runtime reflection via ShaderPropertyType and ShaderPropertyFieldInfo. Introduce strongly-typed Texture2D/3D and Buffer handle structs. Update ShaderPropertiesGenerator to emit field metadata and register it. Move mesh content structs to AssetManager.Mesh.cs and mark as internal. Update DSLShaderCompiler and registry for new property API. Remove obsolete files and clean up namespaces. Add sample TestShaderProperty struct.
2026-05-08 15:37:30 +09:00
80e820a858 Add Editor configs, refactor test core, DXC test
Added Debug_Editor/Release_Editor configs to all projects and solution. Refactored test utilities into Ghost.TestCore and updated references. Introduced DXCBindingTest for shader compilation. Updated conditional compilation to use GHOST_EDITOR. Improved platform mappings and performed minor code cleanup.
2026-05-08 13:59:36 +09:00
67 changed files with 1345 additions and 464 deletions

View File

@@ -0,0 +1,8 @@
<Project>
<PropertyGroup Condition="'$(Configuration)' == 'Debug_Editor'">
<DefineConstants>$(DefineConstants);DEBUG;GHOST_EDITOR;MHP_ENABLE_SAFETY_CHECKS;</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release_Editor'">
<DefineConstants>$(DefineConstants);GHOST_EDITOR;MHP_ENABLE_SAFETY_CHECKS;</DefineConstants>
</PropertyGroup>
</Project>

View File

@@ -4,6 +4,7 @@
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -101,7 +101,7 @@ public static class DSLShaderCompiler
var pass = semantics.passes![i]; var pass = semantics.passes![i];
var localPipeline = MeragePipeline(pass.localPipeline, PipelineState.Default); var localPipeline = MeragePipeline(pass.localPipeline, PipelineState.Default);
var result = BuildFinalShaderCode(pass.amplificationShader.shaderPath, pass.includes.AsSpan(), pass.hlsl, propertyInfo.code); var result = BuildFinalShaderCode(pass.amplificationShader.shaderPath, pass.includes.AsSpan(), pass.hlsl, propertyInfo.Code);
if (result.IsFailure) if (result.IsFailure)
{ {
return Result.Failure($"Failed to build shader code for pass '{pass.name}': {result.Message}"); return Result.Failure($"Failed to build shader code for pass '{pass.name}': {result.Message}");
@@ -109,7 +109,7 @@ public static class DSLShaderCompiler
var amplificationShaderCode = new ShaderCode { code = result.Value, entryPoint = pass.amplificationShader.entry }; var amplificationShaderCode = new ShaderCode { code = result.Value, entryPoint = pass.amplificationShader.entry };
result = BuildFinalShaderCode(pass.meshShader.shaderPath, pass.includes.AsSpan(), pass.hlsl, propertyInfo.code); result = BuildFinalShaderCode(pass.meshShader.shaderPath, pass.includes.AsSpan(), pass.hlsl, propertyInfo.Code);
if (result.IsFailure) if (result.IsFailure)
{ {
return Result.Failure($"Failed to build shader code for pass '{pass.name}': {result.Message}"); return Result.Failure($"Failed to build shader code for pass '{pass.name}': {result.Message}");
@@ -117,7 +117,7 @@ public static class DSLShaderCompiler
var meshShaderCode = new ShaderCode { code = result.Value, entryPoint = pass.meshShader.entry }; var meshShaderCode = new ShaderCode { code = result.Value, entryPoint = pass.meshShader.entry };
result = BuildFinalShaderCode(pass.pixelShader.shaderPath, pass.includes.AsSpan(), pass.hlsl, propertyInfo.code); result = BuildFinalShaderCode(pass.pixelShader.shaderPath, pass.includes.AsSpan(), pass.hlsl, propertyInfo.Code);
if (result.IsFailure) if (result.IsFailure)
{ {
return Result.Failure($"Failed to build shader code for pass '{pass.name}': {result.Message}"); return Result.Failure($"Failed to build shader code for pass '{pass.name}': {result.Message}");
@@ -142,7 +142,7 @@ public static class DSLShaderCompiler
var descriptor = new GraphicsShaderDescriptor var descriptor = new GraphicsShaderDescriptor
{ {
Name = semantics.name, Name = semantics.name,
PropertyBufferSize = propertyInfo.size, PropertyBufferSize = propertyInfo.Size,
ShaderModel = semantics.shaderModel, ShaderModel = semantics.shaderModel,
Passes = passes Passes = passes
@@ -302,7 +302,7 @@ public static class DSLShaderCompiler
var shaderCodes = new ShaderCode[semantics.entryPoints.Count]; var shaderCodes = new ShaderCode[semantics.entryPoints.Count];
for (var i = 0; i < shaderCodes.Length; i++) for (var i = 0; i < shaderCodes.Length; i++)
{ {
var result = BuildFinalShaderCode(semantics.entryPoints[i].shaderPath, semantics.includes.AsSpan(), semantics.hlsl, propertyInfo.code); var result = BuildFinalShaderCode(semantics.entryPoints[i].shaderPath, semantics.includes.AsSpan(), semantics.hlsl, propertyInfo.Code);
if (result.IsFailure) if (result.IsFailure)
{ {
return Result.Failure($"Failed to build shader code for entry point '{semantics.entryPoints[i].entry}': {result.Message}"); return Result.Failure($"Failed to build shader code for entry point '{semantics.entryPoints[i].entry}': {result.Message}");
@@ -314,7 +314,7 @@ public static class DSLShaderCompiler
return new ComputeShaderDescriptor return new ComputeShaderDescriptor
{ {
Name = semantics.name, Name = semantics.name,
PropertyBufferSize = propertyInfo.size, PropertyBufferSize = propertyInfo.Size,
ShaderModel = semantics.shaderModel, ShaderModel = semantics.shaderModel,
ShaderCodes = shaderCodes, ShaderCodes = shaderCodes,
Defines = semantics.defines?.ToArray() ?? Array.Empty<string>(), Defines = semantics.defines?.ToArray() ?? Array.Empty<string>(),

View File

@@ -10,8 +10,7 @@
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion> <SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<!-- in .net 10, field keyword is not preview anymore, but we are still waiting roslyn team to update their code analyzer packages --> <Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
<langversion>preview</langversion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Content Remove="Assets\MeshNode.cs" /> <Content Remove="Assets\MeshNode.cs" />

View File

@@ -89,6 +89,7 @@ public sealed partial class AssetCatalog
); );
CREATE UNIQUE INDEX IF NOT EXISTS idx_assets_path ON assets(source_path); CREATE UNIQUE INDEX IF NOT EXISTS idx_assets_path ON assets(source_path);
CREATE INDEX IF NOT EXISTS idx_assets_parent ON assets(parent_guid); CREATE INDEX IF NOT EXISTS idx_assets_parent ON assets(parent_guid);
CREATE INDEX IF NOT EXISTS idx_assets_type_id ON assets(asset_type_id);
CREATE TABLE IF NOT EXISTS dependencies ( CREATE TABLE IF NOT EXISTS dependencies (
from_guid BLOB(16) NOT NULL REFERENCES assets(guid) ON DELETE CASCADE, from_guid BLOB(16) NOT NULL REFERENCES assets(guid) ON DELETE CASCADE,
@@ -272,7 +273,7 @@ public sealed partial class AssetCatalog
{ {
using var connection = OpenConnection(); using var connection = OpenConnection();
using var cmd = connection.CreateCommand(); using var cmd = connection.CreateCommand();
cmd.CommandText = SqlEnumerate; cmd.CommandText = SqlEnumerate;
using var reader = cmd.ExecuteReader(); using var reader = cmd.ExecuteReader();
while (reader.Read()) while (reader.Read())
@@ -281,6 +282,33 @@ public sealed partial class AssetCatalog
} }
} }
public IEnumerable<Guid> EnumerateByTypes(params Guid[] assetTypeIds)
{
if (assetTypeIds.Length == 0)
{
yield break;
}
using var connection = OpenConnection();
using var cmd = connection.CreateCommand();
var parameterNames = new List<string>(assetTypeIds.Length);
for (int i = 0; i < assetTypeIds.Length; i++)
{
string paramName = $"@typeId{i}";
parameterNames.Add(paramName);
cmd.Parameters.AddWithValue(paramName, assetTypeIds[i].ToByteArray());
}
cmd.CommandText = $"SELECT guid FROM assets WHERE asset_type_id IN ({string.Join(", ", parameterNames)})";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
yield return new Guid((byte[])reader[0]);
}
}
public List<SubAssetInfo> GetSubAssets(Guid parentGuid) public List<SubAssetInfo> GetSubAssets(Guid parentGuid)
{ {
using var connection = OpenConnection(); using var connection = OpenConnection();

View File

@@ -0,0 +1,361 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.Editor.Core.Assets;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Utilities;
using Ghost.Engine;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Microsoft.Extensions.DependencyInjection;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
namespace Ghost.Editor.Core.Services;
internal sealed class EditorShaderCompilerBridge : IShaderCompilationBridge
{
private readonly IAssetRegistry _assetRegistry;
private readonly IServiceProvider _serviceProvider;
private readonly IShaderCompiler _compiler;
private readonly ConcurrentDictionary<ulong, Guid> _shaderIdToAssetId = new();
private readonly ConcurrentDictionary<Guid, Dictionary<int, string>[]> _assetKeywordMappings = new();
private Task? _shaderDictionaryPopulated;
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)
{
var nameHash = ExtractNameHash(result.Value);
if (nameHash != 0)
{
_shaderIdToAssetId[nameHash] = guid;
BuildKeywordMappings(result.Value, guid);
var engineCore = _serviceProvider.GetService<EngineCore>();
if (engineCore != null)
{
var shaderLibrary = engineCore.RenderSystem.ShaderLibrary;
var pipelineLibrary = engineCore.RenderSystem.GraphicsEngine.PipelineLibrary;
shaderLibrary.InvalidateShaderCache(nameHash, pipelineLibrary);
}
}
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong ExtractNameHash(IAsset asset)
{
if (asset is GraphicsShaderAsset graphicsAsset)
{
return RHIUtility.GetShaderID(graphicsAsset.Descriptor.Name);
}
if (asset is ComputeShaderAsset computeAsset)
{
return RHIUtility.GetShaderID(computeAsset.Descriptor.Name);
}
return 0;
}
private Task EnsureShaderDictionaryPopulatedAsync()
{
var existing = Volatile.Read(ref _shaderDictionaryPopulated);
if (existing != null)
{
return existing;
}
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var original = Interlocked.CompareExchange(ref _shaderDictionaryPopulated, tcs.Task, null);
if (original != null)
{
return original;
}
Task.Run(async () =>
{
try
{
var catalog = _assetRegistry.GetAssetCatalog();
var assetGuids = catalog.EnumerateByTypes(typeof(GraphicsShaderAsset).GUID, typeof(ComputeShaderAsset).GUID);
foreach (var assetGuid in assetGuids)
{
var result = await _assetRegistry.LoadAssetAsync(assetGuid);
if (result.IsSuccess)
{
var nameHash = ExtractNameHash(result.Value);
if (nameHash != 0)
{
_shaderIdToAssetId[nameHash] = assetGuid;
BuildKeywordMappings(result.Value, assetGuid);
}
}
}
tcs.SetResult();
}
catch (Exception ex)
{
tcs.SetException(ex);
}
});
return tcs.Task;
}
private void BuildKeywordMappings(IAsset asset, Guid assetId)
{
if (asset is GraphicsShaderAsset graphicsAsset)
{
var passes = graphicsAsset.Descriptor.Passes;
var mappings = new Dictionary<int, string>[passes.Length];
for (var i = 0; i < passes.Length; i++)
{
mappings[i] = BuildKeywordMappingFromGroups(passes[i].keywords);
}
_assetKeywordMappings[assetId] = mappings;
}
else if (asset is ComputeShaderAsset computeAsset)
{
var entryCount = computeAsset.Descriptor.ShaderCodes.Length;
var mappings = new Dictionary<int, string>[entryCount];
var sharedMapping = BuildKeywordMappingFromGroups(computeAsset.Descriptor.Keywords);
for (var i = 0; i < entryCount; i++)
{
mappings[i] = sharedMapping;
}
_assetKeywordMappings[assetId] = mappings;
}
}
private static Dictionary<int, string> BuildKeywordMappingFromGroups(KeywordsGroup[] groups)
{
var mapping = new Dictionary<int, string>();
var localIndex = 0;
foreach (var group in groups)
{
if (group.keywords == null)
{
continue;
}
if (group.space != KeywordSpace.Local)
{
continue;
}
foreach (var kw in group.keywords)
{
mapping[localIndex++] = kw;
}
}
return mapping;
}
private static string[] BuildVariantDefines(LocalKeywordSet keywordMask, Dictionary<int, string>? keywordMapping)
{
if (keywordMapping == null || keywordMapping.Count == 0)
{
return Array.Empty<string>();
}
var defines = new List<string>(keywordMapping.Count);
foreach (var (localIndex, keywordName) in keywordMapping)
{
if (keywordMask.IsKeywordEnabled(localIndex))
{
defines.Add(keywordName);
}
}
return defines.ToArray();
}
private static ReadOnlySpan<string> CombineDefines(ReadOnlySpan<string> staticDefines, ReadOnlySpan<string> variantDefines)
{
if (variantDefines.Length == 0)
{
return staticDefines;
}
if (staticDefines.Length == 0)
{
return variantDefines;
}
var combined = new string[staticDefines.Length + variantDefines.Length];
staticDefines.CopyTo(combined);
variantDefines.CopyTo(combined.AsSpan(staticDefines.Length));
return combined;
}
public void RequestCompilation(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, LocalKeywordSet keywordMask)
{
Task.Run(async () =>
{
await EnsureShaderDictionaryPopulatedAsync();
if (!_shaderIdToAssetId.TryGetValue(shaderId, out var assetId))
{
return;
}
var assetResult = await _assetRegistry.LoadAssetAsync(assetId);
if (assetResult.IsFailure)
{
return;
}
Dictionary<int, string>? keywordMapping = null;
if (_assetKeywordMappings.TryGetValue(assetId, out var mappings) && passIndex < mappings.Length)
{
keywordMapping = mappings[passIndex];
}
if (assetResult.Value is GraphicsShaderAsset graphicsAsset)
{
var pass = graphicsAsset.Descriptor.Passes[passIndex];
await CompileGraphicsPassAsync(shaderId, passIndex, variantKey, keywordMask, pass, graphicsAsset.Descriptor.ShaderModel, keywordMapping);
}
else if (assetResult.Value is ComputeShaderAsset computeAsset)
{
await CompileComputePassAsync(shaderId, passIndex, variantKey, keywordMask, computeAsset.Descriptor, passIndex, keywordMapping);
}
});
}
private unsafe Task CompileGraphicsPassAsync(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, LocalKeywordSet keywordMask, PassDescriptor descriptor, ShaderModel shaderModel, Dictionary<int, string>? keywordMapping)
{
var variantDefines = BuildVariantDefines(keywordMask, keywordMapping);
var additionalConfig = new ShaderCompilationConfig
{
defines = variantDefines,
model = shaderModel,
optimizeLevel = CompilerOptimizeLevel.O3,
options = CompilerOption.None
};
var compileResult = _compiler.CompileShaderPass(ref descriptor, ref additionalConfig, AllocationHandle.Persistent);
if (compileResult.IsFailure)
{
Ghost.Core.Logger.Error($"Failed to compile graphics shader {shaderId}: {compileResult.Message}");
return Task.CompletedTask;
}
var engineCore = _serviceProvider.GetService<EngineCore>();
if (engineCore == null)
{
return Task.CompletedTask;
}
using var compiled = compileResult.Value;
var stageCount = 0;
if (compiled.asResult.IsCreated) stageCount++;
if (compiled.msResult.IsCreated) stageCount++;
if (compiled.psResult.IsCreated) stageCount++;
var byteCodes = stackalloc ShaderByteCode[stageCount];
var idx = 0;
if (compiled.asResult.IsCreated)
{
byteCodes[idx++] = new ShaderByteCode { pCode = (byte*)compiled.asResult.GetUnsafePtr(), size = (ulong)compiled.asResult.Length };
}
if (compiled.msResult.IsCreated)
{
byteCodes[idx++] = new ShaderByteCode { pCode = (byte*)compiled.msResult.GetUnsafePtr(), size = (ulong)compiled.msResult.Length };
}
if (compiled.psResult.IsCreated)
{
byteCodes[idx++] = new ShaderByteCode { pCode = (byte*)compiled.psResult.GetUnsafePtr(), size = (ulong)compiled.psResult.Length };
}
var shaderLibrary = engineCore.RenderSystem.ShaderLibrary;
shaderLibrary.CacheCompiledResult(shaderId, passIndex, variantKey, new ReadOnlySpan<ShaderByteCode>(byteCodes, stageCount));
var (compiledHash, _) = shaderLibrary.GetCompiledHash(shaderId, passIndex, variantKey);
OnShaderVariantCompiled?.Invoke(variantKey, compiledHash);
return Task.CompletedTask;
}
private unsafe Task CompileComputePassAsync(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, LocalKeywordSet keywordMask, ComputeShaderDescriptor descriptor, int entryIndex, Dictionary<int, string>? keywordMapping)
{
var variantDefines = BuildVariantDefines(keywordMask, keywordMapping);
var fullDefines = CombineDefines(descriptor.Defines, variantDefines);
var code = descriptor.ShaderCodes[entryIndex];
var config = new ShaderCompilationConfig
{
shaderCode = code.code,
entryPoint = code.entryPoint,
stage = ShaderStage.ComputeShader,
defines = fullDefines,
model = descriptor.ShaderModel,
optimizeLevel = CompilerOptimizeLevel.O3,
options = CompilerOption.None
};
var compileResult = _compiler.Compile(ref config, AllocationHandle.Persistent);
if (compileResult.IsFailure)
{
Ghost.Core.Logger.Error($"Failed to compile compute shader {shaderId}: {compileResult.Message}");
return Task.CompletedTask;
}
var engineCore = _serviceProvider.GetService<EngineCore>();
if (engineCore == null)
{
return Task.CompletedTask;
}
using var bytecodeArray = compileResult.Value;
var byteCode = new ShaderByteCode
{
pCode = (byte*)bytecodeArray.GetUnsafePtr(),
size = (ulong)bytecodeArray.Length
};
var shaderLibrary = engineCore.RenderSystem.ShaderLibrary;
shaderLibrary.CacheCompiledResult(shaderId, passIndex, variantKey, new ReadOnlySpan<ShaderByteCode>(ref byteCode));
var (compiledHash, _) = shaderLibrary.GetCompiledHash(shaderId, passIndex, variantKey);
OnShaderVariantCompiled?.Invoke(variantKey, compiledHash);
return Task.CompletedTask;
}
public void Dispose()
{
_assetRegistry.OnAssetImported -= OnAssetImported;
}
}

View File

@@ -7,6 +7,7 @@ using Ghost.Editor.ViewModels.Controls;
using Ghost.Editor.ViewModels.Windows; using Ghost.Editor.ViewModels.Windows;
using Ghost.Editor.Views.Windows; using Ghost.Editor.Views.Windows;
using Ghost.Engine; using Ghost.Engine;
using Ghost.Graphics.RHI;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
@@ -66,6 +67,7 @@ public partial class App : Application
services.AddSingleton<IPreviewService, PreviewService>(); services.AddSingleton<IPreviewService, PreviewService>();
services.AddSingleton<IAssetRegistry, AssetRegistry>(); services.AddSingleton<IAssetRegistry, AssetRegistry>();
services.AddSingleton<IContentProvider, EditorContentProvider>(); services.AddSingleton<IContentProvider, EditorContentProvider>();
services.AddSingleton<IShaderCompilationBridge, EditorShaderCompilerBridge>();
services.AddSingleton<EngineCore>(); services.AddSingleton<EngineCore>();

View File

@@ -8,8 +8,7 @@
<PublishProfile>win-$(Platform).pubxml</PublishProfile> <PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI> <UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling> <EnableMsixTooling>true</EnableMsixTooling>
<!-- in .net 10, field keyword is not preview anymore, but we are still waiting roslyn team to update their code analyzer packages --> <Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
<langversion>preview</langversion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="Views\Controls\Hierarchy.xaml" /> <None Remove="Views\Controls\Hierarchy.xaml" />
@@ -217,15 +216,18 @@
<!-- Publish Properties --> <!-- Publish Properties -->
<PropertyGroup> <PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> <PublishReadyToRun Condition="'$(Configuration)'=='Debug_Editor'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun> <PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion> <SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<PublishAot>False</PublishAot>
<PublishTrimmed>False</PublishTrimmed>
<RootNamespace>Ghost.Editor</RootNamespace> <RootNamespace>Ghost.Editor</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release_Editor|x64'">
<Optimize>True</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release_Editor|ARM64'">
<Optimize>True</Optimize>
</PropertyGroup>
</Project> </Project>

View File

@@ -1,5 +1,9 @@
<Solution> <Solution>
<Configurations> <Configurations>
<BuildType Name="Debug" />
<BuildType Name="Debug_Editor" />
<BuildType Name="Release" />
<BuildType Name="Release_Editor" />
<Platform Name="ARM64" /> <Platform Name="ARM64" />
<Platform Name="x64" /> <Platform Name="x64" />
<Platform Name="x86" /> <Platform Name="x86" />
@@ -9,12 +13,18 @@
<Project Path="Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj"> <Project Path="Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj">
<Platform Solution="*|ARM64" Project="ARM64" /> <Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" /> <Platform Solution="*|x64" Project="x64" />
<Platform Solution="*|x86" Project="x86" /> <Platform Solution="Debug_Editor|x86" Project="x64" />
<Platform Solution="Debug|x86" Project="x86" />
<Platform Solution="Release_Editor|x86" Project="x64" />
<Platform Solution="Release|x86" Project="x86" />
</Project> </Project>
<Project Path="Editor/Ghost.Editor/Ghost.Editor.csproj"> <Project Path="Editor/Ghost.Editor/Ghost.Editor.csproj">
<Platform Solution="*|ARM64" Project="ARM64" /> <Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" /> <Platform Solution="*|x64" Project="x64" />
<Platform Solution="*|x86" Project="x86" /> <Platform Solution="Debug_Editor|x86" Project="x64" />
<Platform Solution="Debug|x86" Project="x86" />
<Platform Solution="Release_Editor|x86" Project="x64" />
<Platform Solution="Release|x86" Project="x86" />
<Deploy /> <Deploy />
</Project> </Project>
</Folder> </Folder>
@@ -45,12 +55,13 @@
</Project> </Project>
<Project Path="Test/Ghost.MicroTest/Ghost.MicroTest.csproj" Id="8c8ffa4b-e1e4-46a1-9221-7b508a109edd" /> <Project Path="Test/Ghost.MicroTest/Ghost.MicroTest.csproj" Id="8c8ffa4b-e1e4-46a1-9221-7b508a109edd" />
<Project Path="Test/Ghost.Shader.Test/Ghost.Shader.Test.csproj" /> <Project Path="Test/Ghost.Shader.Test/Ghost.Shader.Test.csproj" />
<Project Path="Test/Ghost.Test.Core/Ghost.Test.Core.csproj" /> <Project Path="Test/Ghost.TestCore/Ghost.TestCore.csproj" />
<Project Path="Test/Ghost.UnitTest/Ghost.UnitTest.csproj" Id="4da45668-456b-4dcc-acd8-6bfe154e6837"> <Project Path="Test/Ghost.UnitTest/Ghost.UnitTest.csproj" Id="4da45668-456b-4dcc-acd8-6bfe154e6837">
<Platform Solution="*|ARM64" Project="ARM64" /> <Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" /> <Platform Solution="*|x64" Project="x64" />
<Platform Solution="*|x86" Project="x86" /> <Platform Solution="*|x86" Project="x86" />
<Deploy /> <Deploy Solution="Debug|*" />
<Deploy Solution="Release|*" />
</Project> </Project>
</Folder> </Folder>
<Folder Name="/Tools/"> <Folder Name="/Tools/">

View File

@@ -5,15 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup> <Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>$(DefineConstants);MHP_ENABLE_SAFETY_CHECKS;MHP_ENABLE_MIMALLOC;MHP_FASTMATH</DefineConstants>
<IsAotCompatible>True</IsAotCompatible>
<IsTrimmable>True</IsTrimmable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DefineConstants>$(DefineConstants);MHP_ENABLE_MIMALLOC;MHP_FASTMATH</DefineConstants> <DefineConstants>$(DefineConstants);MHP_ENABLE_MIMALLOC;MHP_FASTMATH</DefineConstants>
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
<IsTrimmable>True</IsTrimmable> <IsTrimmable>True</IsTrimmable>

View File

@@ -0,0 +1,36 @@
using System.Runtime.InteropServices;
namespace Ghost.Core.Graphics;
[StructLayout(LayoutKind.Sequential)]
public readonly struct Texture2DHandle
{
public readonly uint DescriptorIndex;
public Texture2DHandle(uint descriptorIndex)
{
DescriptorIndex = descriptorIndex;
}
}
[StructLayout(LayoutKind.Sequential)]
public readonly struct Texture3DHandle
{
public readonly uint DescriptorIndex;
public Texture3DHandle(uint descriptorIndex)
{
DescriptorIndex = descriptorIndex;
}
}
[StructLayout(LayoutKind.Sequential)]
public readonly struct BufferHandle
{
public readonly uint DescriptorIndex;
public BufferHandle(uint descriptorIndex)
{
DescriptorIndex = descriptorIndex;
}
}

View File

@@ -1,20 +1,69 @@
namespace Ghost.Core.Graphics; namespace Ghost.Core.Graphics;
public enum ShaderPropertyType
{
Unknown,
Float,
Float2,
Float3,
Float4,
Int,
UInt,
Float4x4,
Texture2D,
Texture3D,
Buffer
}
public readonly struct ShaderPropertyFieldInfo
{
public string Name
{
get; init;
}
public ShaderPropertyType Type
{
get; init;
}
public int Offset
{
get; init;
}
}
#if DEBUG || GHOST_EDITOR #if DEBUG || GHOST_EDITOR
public struct ShaderPropertyInfo public struct ShaderPropertyInfo
{ {
public string shaderName; public string ShaderName
public string code; {
public uint size; get; init;
}
public string Code
{
get; init;
}
public uint Size
{
get; init;
}
public ShaderPropertyFieldInfo[] Fields
{
get; init;
}
} }
public static class ShaderPropertiesRegistry public static class ShaderPropertiesRegistry
{ {
private static readonly Dictionary<string, ShaderPropertyInfo> s_nameToCode = new Dictionary<string, ShaderPropertyInfo>(StringComparer.Ordinal); private static readonly Dictionary<string, ShaderPropertyInfo> s_nameToCode = new Dictionary<string, ShaderPropertyInfo>(StringComparer.Ordinal);
public static void Register(string name, string code, uint size) public static void Register(string name, string code, uint size, ShaderPropertyFieldInfo[] fields)
{ {
s_nameToCode[name] = new ShaderPropertyInfo { shaderName = name, code = code, size = size }; s_nameToCode[name] = new ShaderPropertyInfo { ShaderName = name, Code = code, Size = size, Fields = fields };
} }
public static bool TryGetInfo(string name, out ShaderPropertyInfo info) public static bool TryGetInfo(string name, out ShaderPropertyInfo info)

View File

@@ -123,22 +123,19 @@ public unsafe partial struct TempJobAllocator : IAllocator
#endif #endif
} }
public int AdvanceFrame() public void AdvanceFrame()
{ {
var allocations = Interlocked.Exchange(ref _allocationsPerFrame[_currentFrameIndex], 0);
_currentFrameCount++; _currentFrameCount++;
_currentFrameIndex = _currentFrameCount % _FRAME_LATENCY; _currentFrameIndex = _currentFrameCount % _FRAME_LATENCY;
(_pArena + _currentFrameIndex)->Reset(); (_pArena + _currentFrameIndex)->Reset();
#if MHP_ENABLE_SAFETY_CHECKS #if MHP_ENABLE_SAFETY_CHECKS
if (_allocationsPerFrame[_currentFrameIndex] != 0) var allocations = Interlocked.Exchange(ref _allocationsPerFrame[_currentFrameIndex], 0);
if (allocations != 0)
{ {
Logger.Error($"TempJobAllocator: Detected {_allocationsPerFrame[_currentFrameIndex]} leaked allocations from frame {_currentFrameCount - _FRAME_LATENCY}."); Logger.Error($"TempJobAllocator: Detected {allocations} leaked allocations from frame {_currentFrameCount - _FRAME_LATENCY}.");
} }
#endif #endif
return allocations;
} }
} }

View File

@@ -3,11 +3,56 @@ using Ghost.Graphics;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Ghost.Graphics.Utilities; using Ghost.Graphics.Utilities;
using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Mathematics.Geometry; using Misaki.HighPerformance.Mathematics.Geometry;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Engine; namespace Ghost.Engine;
[StructLayout(LayoutKind.Sequential)]
internal struct MeshContentHeader
{
public const uint MAGIC = 0x48534D47; // GMSH
public const uint VERSION = 1;
public uint magic;
public uint version;
public uint vertexCount;
public uint indexCount;
public uint materialPartCount;
public uint meshletCount;
public uint meshletGroupCount;
public uint meshletHierarchyNodeCount;
public uint meshletVertexCount;
public uint meshletTriangleCount;
public uint materialSlotCount;
public uint lodLevelCount;
public float3 boundsMin;
public float3 boundsMax;
public ulong vertexOffset;
public ulong indexOffset;
public ulong materialPartOffset;
public ulong meshletOffset;
public ulong meshletGroupOffset;
public ulong meshletHierarchyNodeOffset;
public ulong meshletVertexOffset;
public ulong meshletTriangleOffset;
}
[StructLayout(LayoutKind.Sequential)]
internal struct MeshContentMaterialPart
{
public int materialIndex;
public int indexStart;
public int indexCount;
public int vertexStart;
public int vertexCount;
}
internal unsafe partial class AssetEntry internal unsafe partial class AssetEntry
{ {
private sealed unsafe class MeshParsedData private sealed unsafe class MeshParsedData
@@ -225,6 +270,7 @@ internal unsafe partial class AssetEntry
private void OnMeshUploadComplete(ResourceStreamingContext context) private void OnMeshUploadComplete(ResourceStreamingContext context)
{ {
var (oldHandle, newHandle) = GetStorage<(Handle<Mesh>, Handle<Mesh>)>(); var (oldHandle, newHandle) = GetStorage<(Handle<Mesh>, Handle<Mesh>)>();
// FIX: Do not reaplce the mesh, replace the underlying buffers and data instead because we are using persistent gpu scene. Replacing the mesh does not update the gpu scene at all.
var actualHandle = context.ResourceManager.ReplaceMesh(oldHandle, newHandle); var actualHandle = context.ResourceManager.ReplaceMesh(oldHandle, newHandle);
if (actualHandle.IsInvalid) if (actualHandle.IsInvalid)
{ {

View File

@@ -1,6 +1,9 @@
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;
namespace Ghost.Engine; namespace Ghost.Engine;
@@ -23,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;
@@ -44,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);
@@ -57,3 +61,11 @@ public sealed partial class EngineCore : IDisposable
_jobScheduler.Dispose(); _jobScheduler.Dispose();
} }
} }
[GenerateShaderProperty("TestShader")]
public partial struct TestShaderProperty
{
public Texture2DHandle texture;
public uint someValue;
public float3 otherValue;
}

View File

@@ -5,14 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup> <Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
<IsTrimmable>True</IsTrimmable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
<IsTrimmable>True</IsTrimmable> <IsTrimmable>True</IsTrimmable>
</PropertyGroup> </PropertyGroup>

View File

@@ -1,47 +0,0 @@
using Misaki.HighPerformance.Mathematics;
using System.Runtime.InteropServices;
namespace Ghost.Engine;
[StructLayout(LayoutKind.Sequential)]
public struct MeshContentHeader
{
public const uint MAGIC = 0x48534D47; // GMSH
public const uint VERSION = 1;
public uint magic;
public uint version;
public uint vertexCount;
public uint indexCount;
public uint materialPartCount;
public uint meshletCount;
public uint meshletGroupCount;
public uint meshletHierarchyNodeCount;
public uint meshletVertexCount;
public uint meshletTriangleCount;
public uint materialSlotCount;
public uint lodLevelCount;
public float3 boundsMin;
public float3 boundsMax;
public ulong vertexOffset;
public ulong indexOffset;
public ulong materialPartOffset;
public ulong meshletOffset;
public ulong meshletGroupOffset;
public ulong meshletHierarchyNodeOffset;
public ulong meshletVertexOffset;
public ulong meshletTriangleOffset;
}
[StructLayout(LayoutKind.Sequential)]
public struct MeshContentMaterialPart
{
public int materialIndex;
public int indexStart;
public int indexCount;
public int vertexStart;
public int vertexCount;
}

View File

@@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Ghost.Engine;
internal class TestSetup : IDisposable
{
public void Dispose()
{
throw new NotImplementedException();
}
}

View File

@@ -5,6 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">

View File

@@ -2,7 +2,6 @@ using Ghost.Core;
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 Misaki.HighPerformance.LowLevel.Utilities;
using System.Diagnostics;
namespace Ghost.Entities; namespace Ghost.Entities;

View File

@@ -86,15 +86,18 @@ internal class AssetHandlerRegistrationGenerator : IIncrementalGenerator
var registerTypeName = "g_assethandler_registeration"; var registerTypeName = "g_assethandler_registeration";
var code = $@"// <auto-generated /> var code = $@"// <auto-generated />
#if GHOST_EDITOR
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static partial class {registerTypeName} internal static partial class {registerTypeName}
{{ {{
[global::System.Runtime.CompilerServices.ModuleInitializer] [global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void RegisterAssetHandlers() internal static void RegisterAssetHandlers()
{{ {{
{sb} {sb}
}} }}
}}"; }}
#endif";
context.AddSource($"{registerTypeName}.gen.cs", code); context.AddSource($"{registerTypeName}.gen.cs", code);
} }
@@ -156,6 +159,7 @@ internal class IAssetSettingsRegistrationGenerator : IIncrementalGenerator
var registerTypeName = "g_iassetsettings_registeration"; var registerTypeName = "g_iassetsettings_registeration";
var code = $@"// <auto-generated /> var code = $@"// <auto-generated />
#if GHOST_EDITOR
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static partial class {registerTypeName} internal static partial class {registerTypeName}
{{ {{
@@ -164,7 +168,8 @@ internal static partial class {registerTypeName}
{{ {{
{sb} {sb}
}} }}
}}"; }}
#endif";
context.AddSource($"{registerTypeName}.gen.cs", code); context.AddSource($"{registerTypeName}.gen.cs", code);
} }

View File

@@ -1,229 +0,0 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace Ghost.Generator
{
[Generator]
public class ComponentSerializationGenerator : IIncrementalGenerator
{
private string GetJsonWriteCall(ITypeSymbol type, string fieldName)
{
switch (type.SpecialType)
{
case SpecialType.System_Byte:
case SpecialType.System_SByte:
case SpecialType.System_Int16:
case SpecialType.System_Int32:
case SpecialType.System_Int64:
case SpecialType.System_Single:
case SpecialType.System_Double:
case SpecialType.System_Decimal:
case SpecialType.System_UInt64:
case SpecialType.System_UInt16:
case SpecialType.System_UInt32:
case SpecialType.System_IntPtr:
case SpecialType.System_UIntPtr:
return $@"writer.WriteNumber(""{fieldName}"", value.{fieldName});";
case SpecialType.System_Boolean:
return $@"writer.WriteBoolean(""{fieldName}"", value.{fieldName});";
case SpecialType.System_Char:
return $@"writer.WriteString(""{fieldName}"", [value.{fieldName}]);";
case SpecialType.System_String:
return $@"writer.WriteString(""{fieldName}"", value.{fieldName});";
}
return $@"writer.WritePropertyName(""{fieldName}""); global::System.Text.Json.JsonSerializer.Serialize(writer, value.{fieldName}, options);";
}
private string GetJsonReadCall(ITypeSymbol type)
{
switch (type.SpecialType)
{
case SpecialType.System_Byte: return "reader.GetByte()";
case SpecialType.System_SByte: return "reader.GetSByte()";
case SpecialType.System_Int16: return "reader.GetInt16()";
case SpecialType.System_Int32: return "reader.GetInt32()";
case SpecialType.System_Int64: return "reader.GetInt64()";
case SpecialType.System_Single: return "reader.GetSingle()";
case SpecialType.System_Double: return "reader.GetDouble()";
case SpecialType.System_Decimal: return "reader.GetDecimal()";
case SpecialType.System_UInt16: return "reader.GetUInt16()";
case SpecialType.System_UInt32: return "reader.GetUInt32()";
case SpecialType.System_UInt64: return "reader.GetUInt64()";
// Note: the size of IntPtr and UIntPtr varies by platform, we use Int64/UInt64 to ensure compatibility
case SpecialType.System_IntPtr: return "reader.GetInt64()";
case SpecialType.System_UIntPtr: return "reader.GetUInt64()";
case SpecialType.System_Boolean: return "reader.GetBoolean()";
case SpecialType.System_Char: return "reader.GetString()[0]";
case SpecialType.System_String: return "reader.GetString()";
}
return $"global::System.Text.Json.JsonSerializer.Deserialize<{type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(ref reader, options)";
}
private void GenerateJsonSerializer(SourceProductionContext context, ImmutableArray<INamedTypeSymbol> symbols)
{
var sbWrites = new StringBuilder();
var sbReads = new StringBuilder();
foreach (var symbol in symbols)
{
var namespaceName = symbol.ContainingNamespace.ToDisplayString();
var structName = symbol.Name;
var fullTypeName = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
// 1. Build Field Logic (Same as before)
sbWrites.Clear();
sbReads.Clear();
var fields = symbol.GetMembers()
.OfType<IFieldSymbol>()
.Where(f => !f.IsStatic && f.DeclaredAccessibility == Accessibility.Public);
foreach (var field in fields)
{
// Note: GetJsonWriteCall returns a string ending with ";"
var writeCall = GetJsonWriteCall(field.Type, field.Name);
if (writeCall != null)
{
sbWrites.Append(" "); // Indentation
sbWrites.AppendLine(writeCall);
}
var readCall = GetJsonReadCall(field.Type);
if (readCall != null)
{
// Note the double quotes ""{field.Name}"" for the case string
sbReads.Append(" "); // Indentation
sbReads.AppendLine($@"case ""{field.Name}"": result.{field.Name} = {readCall}; break;");
}
}
// 2. The Main Template using $@
// Watch the double braces {{ }} and double quotes "" ""
var sourceCode = $@"// <auto-generated/>
#nullable enable
namespace {namespaceName}
{{
public unsafe static class {structName}_Serializer
{{
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void Init()
{{
var id = Ghost.Entities.ComponentTypeID<{fullTypeName}>.Value;
Ghost.Engine.IO.ComponentSerializerRegistry.Register(id, SerializeBinaryUnsafe, SerializeJsonUnsafe);
}}
// ---------------------------------------------------------
// BINARY (Fast Path)
// ---------------------------------------------------------
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static unsafe void SerializeBinaryUnsafe(global::System.IO.BinaryWriter writer, void* value)
{{
writer.Write(new global::System.ReadOnlySpan<byte>(value, sizeof({fullTypeName})));
}}
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static void SerializeBinary(this global::System.IO.BinaryWriter writer, ref {fullTypeName} value)
{{
unsafe {{ writer.Write(new global::System.ReadOnlySpan<byte>(global::System.Runtime.CompilerServices.Unsafe.AsPointer(ref value), sizeof({fullTypeName}))); }}
}}
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static void DeserializeBinary(this global::System.IO.BinaryReader reader, ref {fullTypeName} value)
{{
unsafe {{ reader.Read(new global::System.Span<byte>(global::System.Runtime.CompilerServices.Unsafe.AsPointer(ref value), sizeof({fullTypeName}))); }}
}}
// ---------------------------------------------------------
// JSON WRITE
// ---------------------------------------------------------
public static unsafe void SerializeJsonUnsafe(System.Text.Json.Utf8JsonWriter writer, void* ptr, System.Text.Json.JsonSerializerOptions? options)
{{
SerializeJson(writer, ref *( {fullTypeName}*)ptr, options);
}}
public static void SerializeJson(this global::System.Text.Json.Utf8JsonWriter writer, ref {fullTypeName} value, global::System.Text.Json.JsonSerializerOptions? options)
{{
writer.WriteStartObject();
{sbWrites}
writer.WriteEndObject();
}}
// ---------------------------------------------------------
// JSON READ
// ---------------------------------------------------------
public static {fullTypeName} DeserializeJson(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Text.Json.JsonSerializerOptions? options)
{{
var result = default({fullTypeName});
if (reader.TokenType != global::System.Text.Json.JsonTokenType.StartObject) throw new global::System.Text.Json.JsonException();
while (reader.Read())
{{
if (reader.TokenType == global::System.Text.Json.JsonTokenType.EndObject) return result;
if (reader.TokenType != global::System.Text.Json.JsonTokenType.PropertyName) throw new global::System.Text.Json.JsonException();
var propName = reader.GetString();
reader.Read();
switch (propName)
{{
{sbReads}
default: reader.Skip(); break;
}}
}}
return result;
}}
}}
}}";
context.AddSource($"{structName}.Serializer.gen.cs", sourceCode);
}
}
public void Initialize(IncrementalGeneratorInitializationContext context)
{
return;
var componentCandidates = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (syntaxNode, _) => syntaxNode is StructDeclarationSyntax,
transform: (ctx, _) =>
{
var structSyntax = (StructDeclarationSyntax)ctx.Node;
if (!(ctx.SemanticModel.GetDeclaredSymbol(structSyntax) is INamedTypeSymbol symbol))
{
return null;
}
var compilation = ctx.SemanticModel.Compilation;
var iComponentSymbol = compilation.GetTypeByMetadataName("Ghost.Entities.IComponent");
if (iComponentSymbol == null)
{
return null;
}
foreach (var iface in symbol.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(iface, iComponentSymbol))
{
return symbol;
}
}
return null;
})
.Where(symbol => symbol != null)
.Collect();
context.RegisterSourceOutput(componentCandidates, GenerateJsonSerializer);
}
}
}

View File

@@ -4,6 +4,7 @@
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>14.0</LangVersion> <LangVersion>14.0</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -90,9 +90,13 @@ 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();
fieldsBuilder.AppendLine($" public static readonly global::Ghost.Core.Graphics.ShaderPropertyFieldInfo[] ReflectionData = new global::Ghost.Core.Graphics.ShaderPropertyFieldInfo[]");
fieldsBuilder.AppendLine(" {");
var fields = info.TypeSymbol.GetMembers().OfType<IFieldSymbol>(); var fields = info.TypeSymbol.GetMembers().OfType<IFieldSymbol>();
foreach (var field in fields) foreach (var field in fields)
{ {
@@ -103,6 +107,7 @@ namespace Ghost.Generator
var hlslTypeAttribute = field.GetAttributes().FirstOrDefault(ad => ad.AttributeClass?.ToDisplayString() == "Ghost.Engine.Utilities.GenerateAsHLSLTypeAttribute"); var hlslTypeAttribute = field.GetAttributes().FirstOrDefault(ad => ad.AttributeClass?.ToDisplayString() == "Ghost.Engine.Utilities.GenerateAsHLSLTypeAttribute");
var hlslType = string.Empty; var hlslType = string.Empty;
var shaderPropType = "global::Ghost.Core.Graphics.ShaderPropertyType.Unknown";
if (hlslTypeAttribute == null) if (hlslTypeAttribute == null)
{ {
@@ -110,34 +115,51 @@ namespace Ghost.Generator
{ {
case SpecialType.System_Single: case SpecialType.System_Single:
hlslType = "float"; hlslType = "float";
shaderPropType = "global::Ghost.Core.Graphics.ShaderPropertyType.Float";
break; break;
case SpecialType.System_Int32: case SpecialType.System_Int32:
hlslType = "int"; hlslType = "int";
shaderPropType = "global::Ghost.Core.Graphics.ShaderPropertyType.Int";
break; break;
case SpecialType.System_UInt32: case SpecialType.System_UInt32:
hlslType = "uint"; hlslType = "uint";
shaderPropType = "global::Ghost.Core.Graphics.ShaderPropertyType.UInt";
break; break;
default: default:
var typeName = field.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var typeName = field.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
switch (typeName) switch (typeName)
{ {
case "global::System.Numerics.Vector2": case "global::System.Numerics.Vector2" or "global::Misaki.HighPerformance.Mathematics.float2":
hlslType = "float2"; hlslType = "float2";
shaderPropType = "global::Ghost.Core.Graphics.ShaderPropertyType.Float2";
break; break;
case "global::System.Numerics.Vector3": case "global::System.Numerics.Vector3" or "global::Misaki.HighPerformance.Mathematics.float3":
hlslType = "float3"; hlslType = "float3";
shaderPropType = "global::Ghost.Core.Graphics.ShaderPropertyType.Float3";
break; break;
case "global::System.Numerics.Vector4": case "global::System.Numerics.Vector4" or "global::Misaki.HighPerformance.Mathematics.float4":
hlslType = "float4"; hlslType = "float4";
shaderPropType = "global::Ghost.Core.Graphics.ShaderPropertyType.Float4";
break; break;
case "global::System.Numerics.Matrix3x3": case "global::System.Numerics.Matrix4x4" or "global::Misaki.HighPerformance.Mathematics.float4x4":
hlslType = "float3x3";
break;
case "global::System.Numerics.Matrix4x4":
hlslType = "float4x4"; hlslType = "float4x4";
shaderPropType = "global::Ghost.Core.Graphics.ShaderPropertyType.Float4x4";
break; break;
case "global::System.Numerics.Quaternion" or "global::Misaki.HighPerformance.Mathematics.quaternion": case "global::System.Numerics.Quaternion" or "global::Misaki.HighPerformance.Mathematics.quaternion":
hlslType = "float4"; hlslType = "float4";
shaderPropType = "global::Ghost.Core.Graphics.ShaderPropertyType.Float4";
break;
case "global::Ghost.Core.Graphics.Texture2DHandle":
hlslType = "uint";
shaderPropType = "global::Ghost.Core.Graphics.ShaderPropertyType.Texture2D";
break;
case "global::Ghost.Core.Graphics.Texture3DHandle":
hlslType = "uint";
shaderPropType = "global::Ghost.Core.Graphics.ShaderPropertyType.Texture3D";
break;
case "global::Ghost.Core.Graphics.BufferHandle":
hlslType = "uint";
shaderPropType = "global::Ghost.Core.Graphics.ShaderPropertyType.Buffer";
break; break;
case var _ when typeName.StartsWith("global::Misaki.HighPerformance.Mathematics."): case var _ when typeName.StartsWith("global::Misaki.HighPerformance.Mathematics."):
hlslType = typeName.Substring("global::Misaki.HighPerformance.Mathematics.".Length); hlslType = typeName.Substring("global::Misaki.HighPerformance.Mathematics.".Length);
@@ -167,8 +189,12 @@ namespace Ghost.Generator
} }
codeBuilder.AppendLine($" {hlslType} {field.Name};"); codeBuilder.AppendLine($" {hlslType} {field.Name};");
fieldsBuilder.AppendLine($" new global::Ghost.Core.Graphics.ShaderPropertyFieldInfo {{ Name = \"{field.Name}\", Type = {shaderPropType}, Offset = (int)global::System.Runtime.InteropServices.Marshal.OffsetOf<{info.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(\"{field.Name}\") }},");
} }
fieldsBuilder.AppendLine(" };");
var code = $@"// <auto-generated/> var code = $@"// <auto-generated/>
namespace {info.TypeSymbol.ContainingNamespace.ToDisplayString()} namespace {info.TypeSymbol.ContainingNamespace.ToDisplayString()}
@@ -176,7 +202,7 @@ namespace {info.TypeSymbol.ContainingNamespace.ToDisplayString()}
[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 4)] [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 4)]
{info.TypeSymbol.DeclaredAccessibility.ToString().ToLower()} partial struct {info.TypeSymbol.Name} {info.TypeSymbol.DeclaredAccessibility.ToString().ToLower()} partial struct {info.TypeSymbol.Name}
{{ {{
#if DEBUG || GHOST_EDITOR #if GHOST_EDITOR
public const string HLSL_SOURCE = @"" public const string HLSL_SOURCE = @""
# ifndef {definedSymbol} # ifndef {definedSymbol}
# define {definedSymbol} # define {definedSymbol}
@@ -185,31 +211,33 @@ struct {info.Name}
{codeBuilder} {codeBuilder}
}}; }};
# endif // {definedSymbol}""; # endif // {definedSymbol}"";
}}
{fieldsBuilder}
#endif #endif
}}
}}"; }}";
context.AddSource($"{info.TypeSymbol.Name}_HLSL.gen.cs", code); context.AddSource($"{info.TypeSymbol.Name}_HLSL.gen.cs", code);
codeBuilder.Clear(); codeBuilder.Clear();
var typeFullName = info.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var typeFullName = info.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
registerBuilder.AppendLine($@" global::Ghost.Core.Graphics.ShaderPropertiesRegistry.Register(""{info.ShaderName}"", {typeFullName}.HLSL_SOURCE, (uint)sizeof({typeFullName}));"); registerBuilder.AppendLine($@" global::Ghost.Core.Graphics.ShaderPropertiesRegistry.Register(""{info.ShaderName}"", {typeFullName}.HLSL_SOURCE, (uint)sizeof({typeFullName}), {typeFullName}.ReflectionData);");
} }
var registerTypeName = "g_shaderproperty_registeration"; var registerTypeName = "g_shaderproperty_registeration";
var registerCode = $@"// <auto-generated/> var registerCode = $@"// <auto-generated/>
#if GHOST_EDITOR
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static partial class {registerTypeName} internal static partial class {registerTypeName}
{{ {{
#if DEBUG || GHOST_EDITOR
[global::System.Runtime.CompilerServices.ModuleInitializer] [global::System.Runtime.CompilerServices.ModuleInitializer]
internal unsafe static void RegisterShaderProperties() internal unsafe static void RegisterShaderProperties()
{{ {{
{registerBuilder} {registerBuilder}
}} }}
#endif }}
}}"; #endif";
context.AddSource($"{registerTypeName}.gen.cs", registerCode); context.AddSource($"{registerTypeName}.gen.cs", registerCode);
} }

View File

@@ -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)
{ {

View File

@@ -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();
} }
} }

View File

@@ -6,14 +6,9 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PublishAot>true</PublishAot> <PublishAot>true</PublishAot>
</PropertyGroup> <Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
<IsTrimmable>True</IsTrimmable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -179,7 +179,7 @@ public readonly struct ShaderPass
get; init; get; init;
} }
public LocalKeywordSet KeywordIDs public LocalKeywordSet DefinedKeywords
{ {
get; init; get; init;
} }
@@ -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;
} }

View File

@@ -5,14 +5,9 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup> <Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
<IsTrimmable>True</IsTrimmable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -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);
} }

View File

@@ -0,0 +1,17 @@
using Ghost.Core;
namespace Ghost.Graphics.RHI;
public interface IShaderCompilationBridge : IDisposable
{
/// <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, LocalKeywordSet keywordMask);
/// <summary>
/// Event triggered when a shader variant has been successfully compiled and updated.
/// </summary>
event Action<Key64<ShaderVariant>, ulong> OnShaderVariantCompiled;
}

View File

@@ -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, keywordSet);
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;

View File

@@ -119,7 +119,7 @@ public partial struct Shader : IResourceReleasable
{ {
Key = RHIUtility.GetPassID(_nameHash, i), Key = RHIUtility.GetPassID(_nameHash, i),
DefaultState = pass.localPipeline, DefaultState = pass.localPipeline,
KeywordIDs = keywords, DefinedKeywords = keywords,
}; };
_passIDToLocal[GetPassID(pass.name)] = (ushort)i; _passIDToLocal[GetPassID(pass.name)] = (ushort)i;

View File

@@ -5,16 +5,17 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
<IsAotCompatible>True</IsAotCompatible>
<IsTrimmable>True</IsTrimmable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <DebugType>embedded</DebugType>
<IsTrimmable>True</IsTrimmable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -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;
}

View File

@@ -169,10 +169,10 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
var materialPipeline = material.GetPassPipelineOverride(material.ActivePassIndex); var materialPipeline = material.GetPassPipelineOverride(material.ActivePassIndex);
// Mask out the keywords that are not used in this pass. // Mask out the keywords that are not used in this pass.
var variantMask = material._keywordMask & pass.KeywordIDs; var variantMask = material._keywordMask & pass.DefinedKeywords;
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, variantMask);
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, keywordSet);
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;

View File

@@ -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, LocalKeywordSet keywordMask = default)
{ {
if (_variantToCompiledHash.TryGetValue(variantKey, out var compiledHash)) if (_variantToCompiledHash.TryGetValue(variantKey, out var compiledHash))
{ {
return compiledHash; return compiledHash;
} }
_shaderCompilationBridge?.RequestCompilation(id, passIndex, variantKey, keywordMask);
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);
} }
} }

View File

@@ -1,4 +1,4 @@
using Ghost.Test.Core; using Ghost.TestCore;
using Misaki.HighPerformance.Jobs; using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;

View File

@@ -6,6 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -15,7 +16,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Entities\Ghost.Entities.csproj" /> <ProjectReference Include="..\..\Runtime\Ghost.Entities\Ghost.Entities.csproj" />
<ProjectReference Include="..\..\Test\Ghost.Test.Core\Ghost.Test.Core.csproj" /> <ProjectReference Include="..\..\Test\Ghost.TestCore\Ghost.TestCore.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,4 +1,4 @@
using Ghost.Test.Core; using Ghost.TestCore;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;

View File

@@ -1,4 +1,4 @@
using Ghost.Test.Core; using Ghost.TestCore;
namespace Ghost.Entities.Test; namespace Ghost.Entities.Test;

View File

@@ -11,6 +11,7 @@
<UseWinUI>true</UseWinUI> <UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling> <EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -50,7 +51,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" /> <ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" />
<ProjectReference Include="..\..\Test\Ghost.Test.Core\Ghost.Test.Core.csproj" /> <ProjectReference Include="..\..\Test\Ghost.TestCore\Ghost.TestCore.csproj" />
<ProjectReference Include="..\..\Runtime\Ghost.Graphics\Ghost.Graphics.csproj" /> <ProjectReference Include="..\..\Runtime\Ghost.Graphics\Ghost.Graphics.csproj" />
<ProjectReference Include="..\..\Editor\Ghost.DSL\Ghost.DSL.csproj" /> <ProjectReference Include="..\..\Editor\Ghost.DSL\Ghost.DSL.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.Ufbx\Ghost.Ufbx.csproj" /> <ProjectReference Include="..\..\ThridParty\Ghost.Ufbx\Ghost.Ufbx.csproj" />
@@ -68,6 +69,7 @@
<!-- Publish Properties --> <!-- Publish Properties -->
<PropertyGroup> <PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> <PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)'=='Debug_Editor'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun> <PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion> <SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
@@ -76,10 +78,19 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|x86'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|x64'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|ARM64'">
<DebugType>embedded</DebugType>
</PropertyGroup>
</Project> </Project>

View File

@@ -0,0 +1,171 @@
using Ghost.DXC;
using Ghost.TestCore;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static Ghost.DXC.UUID;
namespace Ghost.MicroTest;
internal unsafe class DXCBindingTest : ITest
{
private static ReadOnlySpan<byte> ShaderCode => @"
struct VSInput
{
float3 position : POSITION;
float3 normal : NORMAL;
};
struct PSInput
{
float4 position : SV_POSITION;
float3 normal : NORMAL;
};
PSInput main(VSInput input)
{
PSInput output;
output.position = float4(input.position, 1.0);
output.normal = input.normal;
return output;
}"u8;
private IDxcCompiler3* _compiler;
private IDxcUtils* _utils;
public void Setup()
{
IDxcCompiler3* pCompiler = default;
IDxcUtils* pUtils = default;
var hr = Api.DxcCreateInstance((Guid*)Unsafe.AsPointer(in Api.CLSID_DxcCompiler), __uuidof(pCompiler), (void**)&pCompiler);
if (hr < 0)
{
throw new InvalidOperationException($"Failed to create DXC compiler instance. HRESULT: 0x{hr:X8}");
}
hr = Api.DxcCreateInstance((Guid*)Unsafe.AsPointer(in Api.CLSID_DxcUtils), __uuidof(pUtils), (void**)&pUtils);
if (hr < 0)
{
pCompiler->Release();
throw new InvalidOperationException($"Failed to create DXC utils instance. HRESULT: 0x{hr:X8}");
}
_compiler = pCompiler;
_utils = pUtils;
}
private static List<string> GetCompilerArguments()
{
return new List<string>
{
"-T", "vs_6_6", // Target profile (vs_6_6 for vertex shader)
"-E", "main", // Entry point
"-HV", "2021", // HLSL version 2021
"-enable-16bit-types" // Enable 16-bit types
};
}
public void Run()
{
IDxcIncludeHandler* includeHandler = default;
IDxcBlobEncoding* sourceBlob = default;
try
{
var hr = _utils->CreateDefaultIncludeHandler(&includeHandler);
Assert.AreEqual(0, hr, $"Failed to create default include handler. HRESULT: 0x{hr:X8}");
fixed (byte* pCode = ShaderCode)
{
hr = _utils->CreateBlobFromPinned(pCode, (uint)ShaderCode.Length, Api.DXC_CP_UTF8, &sourceBlob);
Assert.AreEqual(0, hr, $"Failed to create blob from shader code. HRESULT: 0x{hr:X8}");
}
var argsArray = GetCompilerArguments();
var argPtrs = stackalloc char*[argsArray.Count];
for (var i = 0; i < argsArray.Count; i++)
{
argPtrs[i] = (char*)Marshal.StringToHGlobalUni(argsArray[i]);
}
IDxcResult* result = default;
IDxcBlob* bytecodeBlob = default;
try
{
// Compile shader
var buffer = new DxcBuffer
{
Ptr = sourceBlob->GetBufferPointer(),
Size = sourceBlob->GetBufferSize(),
Encoding = Api.DXC_CP_UTF8
};
hr = _compiler->Compile(&buffer, argPtrs, (uint)argsArray.Count, includeHandler, __uuidof(result), (void**)&result);
Assert.AreEqual(0, hr, $"Failed to compile shader. HRESULT: 0x{hr:X8}");
// Check compilation result
int hrStatus;
result->GetStatus(&hrStatus);
if (hrStatus < 0)
{
// Get error messages
IDxcBlobEncoding* pErrorBlob = default;
result->GetErrorBuffer(&pErrorBlob);
if (pErrorBlob != null)
{
var errorMessage = Marshal.PtrToStringUTF8((IntPtr)pErrorBlob->GetBufferPointer());
pErrorBlob->Release();
throw new InvalidOperationException($"DXC shader compilation failed:\n{errorMessage}");
}
else
{
throw new InvalidOperationException("DXC shader compilation failed with unknown error.");
}
}
// Get compiled bytecode
hr = result->GetResult(&bytecodeBlob);
Assert.AreEqual(0, hr, $"Failed to get compiled shader bytecode. HRESULT: 0x{hr:X8}");
var bytecodeSize = bytecodeBlob->GetBufferSize();
Assert.IsTrue(bytecodeSize > 0, "Compiled shader bytecode is empty.");
}
finally
{
if (result != null)
{
result->Release();
}
if (bytecodeBlob != null)
{
bytecodeBlob->Release();
}
for (var i = 0; i < argsArray.Count; i++)
{
Marshal.FreeHGlobal((nint)argPtrs[i]);
}
}
}
finally
{
if (includeHandler != null)
{
includeHandler->Release();
}
if (sourceBlob != null)
{
sourceBlob->Release();
}
}
}
public void Cleanup()
{
_compiler->Release();
_utils->Release();
}
}

View File

@@ -7,6 +7,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PublishAot>True</PublishAot> <PublishAot>True</PublishAot>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -15,7 +16,8 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Core\Ghost.Core.csproj" /> <ProjectReference Include="..\..\Runtime\Ghost.Core\Ghost.Core.csproj" />
<ProjectReference Include="..\..\Test\Ghost.Test.Core\Ghost.Test.Core.csproj" /> <ProjectReference Include="..\Ghost.TestCore\Ghost.TestCore.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.DXC\Ghost.DXC.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" /> <ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.StbI\Ghost.StbI.csproj" /> <ProjectReference Include="..\..\ThridParty\Ghost.StbI\Ghost.StbI.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.Ufbx\Ghost.Ufbx.csproj" /> <ProjectReference Include="..\..\ThridParty\Ghost.Ufbx\Ghost.Ufbx.csproj" />

View File

@@ -1,5 +1,5 @@
using Ghost.Nvtt; using Ghost.Nvtt;
using Ghost.Test.Core; using Ghost.TestCore;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;

View File

@@ -1,4 +1,4 @@
using Ghost.MicroTest; using Ghost.MicroTest;
using Ghost.Test.Core; using Ghost.TestCore;
TestRunner.Run<UfbxBindingTest>(); TestRunner.Run<DXCBindingTest>();

View File

@@ -1,5 +1,5 @@
using Ghost.StbI; using Ghost.StbI;
using Ghost.Test.Core; using Ghost.TestCore;
namespace Ghost.MicroTest; namespace Ghost.MicroTest;

View File

@@ -1,4 +1,4 @@
using Ghost.Test.Core; using Ghost.TestCore;
using Ghost.Ufbx; using Ghost.Ufbx;
namespace Ghost.MicroTest; namespace Ghost.MicroTest;

View File

@@ -6,6 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<PublishAot>True</PublishAot> <PublishAot>True</PublishAot>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,28 +0,0 @@
namespace Ghost.Test.Core;
public class TestRunner
{
public static void Run<T>()
where T : ITest, new()
{
var test = new T();
test.Setup();
test.Run();
test.Cleanup();
}
public static void Run<T>(int iteration)
where T : ITest, new()
{
var test = new T();
test.Setup();
iteration = iteration < 1 ? 1 : iteration;
for (var i = 0; i < iteration; i++)
{
test.Run();
}
test.Cleanup();
}
}

View File

@@ -0,0 +1,33 @@
namespace Ghost.TestCore;
public static class Assert
{
public static void AreEqual<T>(T expected, T actual, string message = "")
{
if (!EqualityComparer<T>.Default.Equals(expected, actual))
{
throw new AssertFailedException($"Assert.AreEqual failed. Expected: {expected}, Actual: {actual}. {message}");
}
}
public static void IsTrue(bool condition, string message = "")
{
if (!condition)
{
throw new AssertFailedException($"Assert.IsTrue failed. {message}");
}
}
public static void IsFalse(bool condition, string message = "")
{
if (condition)
{
throw new AssertFailedException($"Assert.IsFalse failed. {message}");
}
}
public static void Fail(string message = "")
{
throw new AssertFailedException($"Assert.Fail: {message}");
}
}

View File

@@ -0,0 +1,17 @@
namespace Ghost.TestCore;
[Serializable]
internal class AssertFailedException : Exception
{
public AssertFailedException()
{
}
public AssertFailedException(string? message) : base(message)
{
}
public AssertFailedException(string? message, Exception? innerException) : base(message, innerException)
{
}
}

View File

@@ -4,6 +4,7 @@
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -1,4 +1,4 @@
namespace Ghost.Test.Core; namespace Ghost.TestCore;
public interface ITest public interface ITest
{ {

View File

@@ -0,0 +1,49 @@
namespace Ghost.TestCore;
public class TestRunner
{
public static void Run<T>()
where T : ITest, new()
{
var test = new T();
try
{
test.Setup();
test.Run();
test.Cleanup();
}
catch (Exception ex)
{
Console.WriteLine($"Test failed with exception: {ex.Message}");
}
Console.WriteLine("Test completed.");
}
public static void Run<T>(int iteration)
where T : ITest, new()
{
var test = new T();
var i = 0;
try
{
test.Setup();
iteration = iteration < 1 ? 1 : iteration;
for (i = 0; i < iteration; i++)
{
test.Run();
}
test.Cleanup();
}
catch (Exception ex)
{
Console.WriteLine($"Test failed at iteration {i} with exception: {ex.Message}");
}
Console.WriteLine($"Test completed after {iteration} iterations.");
}
}

View File

@@ -46,7 +46,7 @@ public class ImportCoordinatorTests
[TestMethod] [TestMethod]
public async Task TestImportCoordinator_BasicImport() public async Task TestImportCoordinator_BasicImport()
{ {
using var catalog = new AssetCatalog(_dbPath); var catalog = new AssetCatalog(_dbPath);
using var coordinator = new ImportCoordinator(catalog); using var coordinator = new ImportCoordinator(catalog);
var assetGuid = Guid.NewGuid(); var assetGuid = Guid.NewGuid();

View File

@@ -8,6 +8,7 @@
<Platforms>x64;x86;ARM64</Platforms> <Platforms>x64;x86;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View 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, LocalKeywordSet keywordMask)> Requests { get; } = new();
public event Action<Key64<ShaderVariant>, ulong>? OnShaderVariantCompiled;
public void RequestCompilation(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, LocalKeywordSet keywordMask)
{
Requests.Add((shaderId, passIndex, variantKey, keywordMask));
}
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();
}
}

View File

@@ -5,12 +5,17 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
</PropertyGroup> </PropertyGroup>

View File

@@ -4,12 +4,17 @@
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
</PropertyGroup> </PropertyGroup>

View File

@@ -5,12 +5,17 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
</PropertyGroup> </PropertyGroup>

View File

@@ -5,12 +5,17 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
</PropertyGroup> </PropertyGroup>

View File

@@ -5,12 +5,17 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
</PropertyGroup> </PropertyGroup>

View File

@@ -5,12 +5,17 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
</PropertyGroup> </PropertyGroup>

View File

@@ -5,6 +5,7 @@
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

BIN
src/build_log.txt Normal file

Binary file not shown.

34
src/test_generator.cs Normal file
View 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);
}
}