From 92b966fe0da6c58eed915358d123315bd483ee1d Mon Sep 17 00:00:00 2001 From: Misaki Date: Wed, 21 Jan 2026 18:32:03 +0900 Subject: [PATCH] Render graph integration and resource management refactor Introduces a full-featured render graph system with pass culling, resource aliasing, and automatic barrier generation. Refactors resource and barrier APIs, improves error handling, and unifies result types. Renderer and render passes now use the new graph-based workflow. Updates shader includes, adds a blit shader, and improves HLSL parsing. Removes dynamic descriptor heaps in favor of persistent ones. Project file now includes the render graph module. Lays the foundation for advanced rendering features and improved memory efficiency. --- Ghost.Core/Logging.cs | 8 +- Ghost.Core/Result.cs | 56 +- Ghost.Core/Utilities/Win32Utility.cs | 8 +- Ghost.DSL/Grammar/GhostShaderLexer.g4 | 5 + Ghost.DSL/Grammar/GhostShaderParser.g4 | 11 +- Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs | 4 +- Ghost.Entities.Test/EntityQueryTest.cs | 2 +- Ghost.Entities/Archetype.cs | 6 +- Ghost.Entities/EntityQuery.JobChunk.cs | 7 +- Ghost.Entities/Query.cs | 12 +- .../EntityQuery.ComponentIterator.gen.cs | 64 +- .../EntityQuery.ComponentIterator.tt | 8 +- ...EntityQuery.EntityComponentIterator.gen.cs | 64 +- .../EntityQuery.EntityComponentIterator.tt | 8 +- .../Templates/EntityQuery.JobEntity.gen.cs | 56 +- .../Templates/EntityQuery.JobEntity.tt | 7 +- Ghost.Entities/World.cs | 9 +- .../Windows/GraphicsTestWindow.xaml.cs | 3 +- Ghost.Graphics/Contracts/IRenderPass.cs | 4 +- Ghost.Graphics/Core/{Color.cs => Common.cs} | 25 + Ghost.Graphics/Core/Material.cs | 37 +- Ghost.Graphics/Core/Mesh.cs | 8 +- Ghost.Graphics/Core/RenderOutput.cs | 4 +- Ghost.Graphics/Core/RenderingContext.cs | 106 +- Ghost.Graphics/Core/Vertex.cs | 28 - Ghost.Graphics/D3D12/D3D12CommandBuffer.cs | 189 ++- .../D3D12/D3D12DescriptorAllocator.cs | 122 +- Ghost.Graphics/D3D12/D3D12DescriptorHeap.cs | 96 +- Ghost.Graphics/D3D12/D3D12GraphicsEngine.cs | 8 +- Ghost.Graphics/D3D12/D3D12PipelineLibrary.cs | 50 +- Ghost.Graphics/D3D12/D3D12Renderer.cs | 47 +- .../D3D12/D3D12ResourceAllocator.cs | 139 +- Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs | 83 +- .../D3D12/Utilities/D3D12PipelineResource.cs | 10 +- .../D3D12/Utilities/D3D12Utility.cs | 4 +- Ghost.Graphics/Ghost.Graphics.csproj | 6 - Ghost.Graphics/RHI/Common.cs | 41 +- Ghost.Graphics/RHI/ICommandBuffer.cs | 23 +- Ghost.Graphics/RHI/IPipelineLibrary.cs | 11 - Ghost.Graphics/RHI/IResourceAllocator.cs | 2 +- Ghost.Graphics/RHI/IResourceDatabase.cs | 15 +- Ghost.Graphics/RHI/RHIUtility.cs | 2 + .../RenderGraphModule/RenderGraph.cs | 1386 +++++++++++++++++ .../RenderGraphModule/RenderGraphAliasing.cs | 543 +++++++ .../RenderGraphModule/RenderGraphBarriers.cs | 117 ++ .../RenderGraphBlackboard.cs | 62 + .../RenderGraphModule/RenderGraphBuilder.cs | 318 ++++ .../RenderGraphCompilationCache.cs | 154 ++ .../RenderGraphModule/RenderGraphContext.cs | 198 +++ .../RenderGraphNativePass.cs | 52 + .../RenderGraphModule/RenderGraphPass.cs | 172 ++ .../RenderGraphResourcePool.cs | 311 ++++ .../RenderGraphModule/RenderGraphTypes.cs | 500 ++++++ Ghost.Graphics/RenderPasses/MeshRenderPass.cs | 138 +- Ghost.Graphics/RenderPasses/ShaderCode.hlsl | 6 +- .../RenderPasses/SimpleRenderPipeline.cs | 5 + Ghost.Graphics/RenderSystem.cs | 15 +- Ghost.Graphics/Shaders/Blit.gsdef | 74 + Ghost.Graphics/Shaders/Includes/Color.hlsl | 12 + .../Shaders/Includes}/Common.hlsl | 0 .../Shaders/Includes}/Properties.hlsl | 2 +- GhostEngine.slnx | 1 - 62 files changed, 4843 insertions(+), 621 deletions(-) rename Ghost.Graphics/Core/{Color.cs => Common.cs} (77%) delete mode 100644 Ghost.Graphics/Core/Vertex.cs create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraph.cs create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraphAliasing.cs create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraphBarriers.cs create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraphBlackboard.cs create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraphBuilder.cs create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraphCompilationCache.cs create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraphContext.cs create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraphNativePass.cs create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraphPass.cs create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraphResourcePool.cs create mode 100644 Ghost.Graphics/RenderGraphModule/RenderGraphTypes.cs create mode 100644 Ghost.Graphics/RenderPasses/SimpleRenderPipeline.cs create mode 100644 Ghost.Graphics/Shaders/Blit.gsdef create mode 100644 Ghost.Graphics/Shaders/Includes/Color.hlsl rename {Ghost.DSL/BuiltIn => Ghost.Graphics/Shaders/Includes}/Common.hlsl (100%) rename {Ghost.DSL/BuiltIn => Ghost.Graphics/Shaders/Includes}/Properties.hlsl (90%) diff --git a/Ghost.Core/Logging.cs b/Ghost.Core/Logging.cs index 42b1337..814ec9a 100644 --- a/Ghost.Core/Logging.cs +++ b/Ghost.Core/Logging.cs @@ -69,9 +69,15 @@ public static class Logger private class LoggerImpl : ILogger { private readonly ObservableCollection _logs = new(); + private readonly ReadOnlyObservableCollection _readOnly; private readonly Lock _lock = new(); - public ReadOnlyObservableCollection Logs => new(_logs); + public ReadOnlyObservableCollection Logs => _readOnly; + + public LoggerImpl() + { + _readOnly = new ReadOnlyObservableCollection(_logs); + } public void Log(string message, LogLevel level) { diff --git a/Ghost.Core/Result.cs b/Ghost.Core/Result.cs index 3f31867..ab2d865 100644 --- a/Ghost.Core/Result.cs +++ b/Ghost.Core/Result.cs @@ -10,7 +10,7 @@ public readonly struct Result public readonly string? Message => _message; public readonly bool IsSuccess => _isSuccess; - public readonly bool IsFailure => !_isSuccess; + public readonly bool IsFailure => !IsSuccess; public Result(bool success, string? message = null) { @@ -65,12 +65,15 @@ public readonly struct Result private readonly string? _message; private readonly bool _isSuccess; + /// + /// Gets the value. Undefined if the result is a failure. + /// public T Value { get { #if DEBUG || GHOST_EDITOR - if (!_isSuccess) + if (IsFailure) { throw new InvalidOperationException($"Cannot access Value when Result is a failure. {_message}"); } @@ -81,7 +84,7 @@ public readonly struct Result public readonly string? Message => _message; public readonly bool IsSuccess => _isSuccess; - public readonly bool IsFailure => !_isSuccess; + public readonly bool IsFailure => !IsSuccess; public Result(bool success, T value, string? message = null) { @@ -136,14 +139,16 @@ public readonly struct Result { private readonly T _value; private readonly E _error; - private readonly bool _isSuccess; + /// + /// Gets the value. Undefined if the result is a failure. + /// public T Value { get { #if DEBUG || GHOST_EDITOR - if (!_isSuccess) + if (IsFailure) { throw new InvalidOperationException($"Cannot access Value when Result is a failure. Error: {_error}"); } @@ -153,37 +158,35 @@ public readonly struct Result } public E Error => _error; - public bool IsSuccess => _isSuccess; - public bool IsFailure => !_isSuccess; + public bool IsSuccess => EqualityComparer.Default.Equals(_error, default); + public bool IsFailure => !IsSuccess; - public Result(T value, E status, bool isSuccess) + public Result(T value, E status) { _value = value; _error = status; - _isSuccess = isSuccess; } public static Result Success(T value) { - return new Result(value, default, true); + return new Result(value, default); } public static Result Failure(E status) { - return new Result(default!, status, false); + return new Result(default!, status); } - public void Deconstruct(out bool success, out T value, out E status) + public void Deconstruct(out T value, out E status) { - success = IsSuccess; value = Value; status = Error; } public override string ToString() => $"Value: {_value}, Status: {_error}"; - public static implicit operator Result(T data) => new(data, default, true); - public static implicit operator Result(E status) => new(default!, status, false); + public static implicit operator Result(T data) => new(data, default); + public static implicit operator Result(E status) => new(default!, status); public static implicit operator bool(Result result) => result.IsSuccess; } @@ -191,15 +194,17 @@ public readonly ref struct RefResult where E : struct, Enum { private readonly ref T _value; - private readonly E _error; - private readonly bool _isSuccess; + private readonly E _error; + /// + /// Gets a reference to the value. Undefined if the result is a failure. + /// public ref T Value { get { #if DEBUG || GHOST_EDITOR - if (!_isSuccess) + if (IsFailure) { throw new InvalidOperationException($"Cannot access Value when Result is a failure. Error: {_error}"); } @@ -209,24 +214,23 @@ public readonly ref struct RefResult } public E Error => _error; - public bool IsSuccess => _isSuccess; - public bool IsFailure => !_isSuccess; + public bool IsSuccess => EqualityComparer.Default.Equals(_error, default); + public bool IsFailure => !IsSuccess; - public RefResult(ref T value, E error, bool isSuccess) + public RefResult(ref T value, E error) { _value = ref value; _error = error; - _isSuccess = isSuccess; } public static RefResult Success(ref T value) { - return new RefResult(ref value, default, true); + return new RefResult(ref value, default); } public static RefResult Failure(E error) { - return new RefResult(ref Unsafe.NullRef(), error, false); + return new RefResult(ref Unsafe.NullRef(), error); } public void Deconstruct(out bool success, out Ref value, out E status) @@ -238,8 +242,8 @@ public readonly ref struct RefResult public override string ToString() => $"Value: {_value}, Status: {_error}"; - public static implicit operator RefResult(Ref data) => new(ref data.Get(), default, true); - public static implicit operator RefResult(E error) => new(ref Unsafe.NullRef(), error, false); + public static implicit operator RefResult(Ref data) => new(ref data.Get(), default); + public static implicit operator RefResult(E error) => new(ref Unsafe.NullRef(), error); public static implicit operator bool(RefResult result) => result.IsSuccess; } diff --git a/Ghost.Core/Utilities/Win32Utility.cs b/Ghost.Core/Utilities/Win32Utility.cs index ad920ef..c396be7 100644 --- a/Ghost.Core/Utilities/Win32Utility.cs +++ b/Ghost.Core/Utilities/Win32Utility.cs @@ -2,10 +2,8 @@ using Misaki.HighPerformance.LowLevel; using System.ComponentModel; using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Runtime.Versioning; using TerraFX.Interop.Windows; -using TerraFX.Interop.WinRT; namespace Ghost.Core.Utilities; @@ -62,12 +60,10 @@ internal static unsafe partial class Win32Utility public static void Dispose(ref this UniquePtr uPtr) where T : unmanaged, IUnknown.Interface { - T* ptr = uPtr.Get(); + var ptr = uPtr.Detach(); if (ptr != null) { - uPtr = default; ptr->Release(); - //MemoryLeakException.ThrowIfRefCountNonZero(ptr->Release()); } } @@ -78,7 +74,7 @@ internal static unsafe partial class Win32Utility { return Result.Success(); } - + return Result.Failure($"{op} failed with code {hr}"); } diff --git a/Ghost.DSL/Grammar/GhostShaderLexer.g4 b/Ghost.DSL/Grammar/GhostShaderLexer.g4 index 2f4f714..f1568cd 100644 --- a/Ghost.DSL/Grammar/GhostShaderLexer.g4 +++ b/Ghost.DSL/Grammar/GhostShaderLexer.g4 @@ -17,6 +17,8 @@ LBRACE: '{'; RBRACE: '}'; LPAREN: '('; RPAREN: ')'; +LBRACK: '['; +RBRACK: ']'; SEMICOLON: ';'; COMMA: ','; EQUALS: '='; @@ -31,3 +33,6 @@ IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]*; WS: [ \t\r\n]+ -> skip; LINE_COMMENT: '//' ~[\r\n]* -> skip; BLOCK_COMMENT: '/*' .*? '*/' -> skip; + + +ANY_CHAR: . ; \ No newline at end of file diff --git a/Ghost.DSL/Grammar/GhostShaderParser.g4 b/Ghost.DSL/Grammar/GhostShaderParser.g4 index 5a26f98..607c834 100644 --- a/Ghost.DSL/Grammar/GhostShaderParser.g4 +++ b/Ghost.DSL/Grammar/GhostShaderParser.g4 @@ -75,11 +75,16 @@ keywordStatement: hlslBlock: HLSL LBRACE - hlslCode + hlslBody RBRACE; -hlslCode: - .*? ; // Capture everything inside hlsl block +// Recursively matches content, ensuring braces are balanced. +hlslBody: + ( + ~(LBRACE | RBRACE) // Match ANY token except open/close braces + | + LBRACE hlslBody RBRACE // Or match a nested block recursively + )*; shaderEntry: IDENTIFIER STRING_LITERAL COLON STRING_LITERAL SEMICOLON; diff --git a/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs b/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs index 75626c7..ba19056 100644 --- a/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs +++ b/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs @@ -264,7 +264,7 @@ internal static class DSLShaderCompiler #ifndef {fileDefine} #define {fileDefine} -#include ""F:/csharp/GhostEngine/Ghost.DSL/BuiltIn/Common.hlsl"""); +#include ""F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Includes/Common.hlsl"""); sb.Append(@" struct PerMaterialData @@ -303,7 +303,7 @@ struct PerMaterialData #ifndef GLOBALDATA_G_HLSL #define GLOBALDATA_G_HLSL -#include ""F:/csharp/GhostEngine/Ghost.DSL/BuiltIn/Common.hlsl"" +#include ""F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Includes/Common.hlsl"" struct GlobalData {"); diff --git a/Ghost.Entities.Test/EntityQueryTest.cs b/Ghost.Entities.Test/EntityQueryTest.cs index a28ee6b..79bc07a 100644 --- a/Ghost.Entities.Test/EntityQueryTest.cs +++ b/Ghost.Entities.Test/EntityQueryTest.cs @@ -53,7 +53,7 @@ public partial class EntityQueryTest : ITest _world.AdvanceVersion(); var testJob = new TestChunkQueryJob(); - var handle = query.ScheduleChunkParallel(testJob, 64, JobHandle.Invalid); + var handle = query.ScheduleChunkParallel(testJob, 1, JobHandle.Invalid); _jobScheduler.WaitComplete(handle); query.ForEach((e, ref t) => diff --git a/Ghost.Entities/Archetype.cs b/Ghost.Entities/Archetype.cs index 202934f..91b0640 100644 --- a/Ghost.Entities/Archetype.cs +++ b/Ghost.Entities/Archetype.cs @@ -64,13 +64,13 @@ internal unsafe sealed class ChunkDebugView } var views = new List(); - var r = World.GetWorld(worldID); - if (!r) + var world = World.GetWorld(worldID); + if (world is null) { return []; } - ref var archetype = ref r.Value.ComponentManager.GetArchetypeReference(archetypeID); + ref var archetype = ref world.ComponentManager.GetArchetypeReference(archetypeID); var it = archetype._signature.GetIterator(); while (it.Next(out var index)) { diff --git a/Ghost.Entities/EntityQuery.JobChunk.cs b/Ghost.Entities/EntityQuery.JobChunk.cs index fd04521..b1a2743 100644 --- a/Ghost.Entities/EntityQuery.JobChunk.cs +++ b/Ghost.Entities/EntityQuery.JobChunk.cs @@ -47,7 +47,12 @@ public unsafe partial struct EntityQuery public JobHandle ScheduleChunkParallel(TJob job, int batchSize, JobHandle dependency) where TJob : unmanaged, IJobChunk { - var world = World.GetWorld(_worldID).GetValueOrThrow(); + var world = World.GetWorld(_worldID); + if (world is null) + { + return JobHandle.Invalid; + } + if (world.JobScheduler == null) { throw new InvalidOperationException("The World has no JobScheduler assigned."); diff --git a/Ghost.Entities/Query.cs b/Ghost.Entities/Query.cs index 003ea0e..299ad10 100644 --- a/Ghost.Entities/Query.cs +++ b/Ghost.Entities/Query.cs @@ -434,7 +434,12 @@ public unsafe partial struct EntityQuery : IDisposable [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ChunkIterator GetChunkIterator() { - var world = World.GetWorld(_worldID).Value; + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + return new ChunkIterator(_matchingArchetypes.AsReadOnly(), world); } @@ -442,13 +447,12 @@ public unsafe partial struct EntityQuery : IDisposable public readonly int GetEntityCount() { var total = 0; - var r = World.GetWorld(_worldID); - if (r.IsFailure) + var world = World.GetWorld(_worldID); + if (world is null) { return 0; } - var world = r.Value; for(var i = 0; i < _matchingArchetypes.Count; i++) { var archetypeID = _matchingArchetypes[i]; diff --git a/Ghost.Entities/Templates/EntityQuery.ComponentIterator.gen.cs b/Ghost.Entities/Templates/EntityQuery.ComponentIterator.gen.cs index ead2e68..1e40e28 100644 --- a/Ghost.Entities/Templates/EntityQuery.ComponentIterator.gen.cs +++ b/Ghost.Entities/Templates/EntityQuery.ComponentIterator.gen.cs @@ -176,7 +176,13 @@ public unsafe partial struct EntityQuery public readonly ComponentIterator GetComponentIterator() where T0 : unmanaged, IComponent { - return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } public readonly ref struct ComponentIterator @@ -373,7 +379,13 @@ public unsafe partial struct EntityQuery where T0 : unmanaged, IComponent where T1 : unmanaged, IComponent { - return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } public readonly ref struct ComponentIterator @@ -580,7 +592,13 @@ public unsafe partial struct EntityQuery where T1 : unmanaged, IComponent where T2 : unmanaged, IComponent { - return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } public readonly ref struct ComponentIterator @@ -797,7 +815,13 @@ public unsafe partial struct EntityQuery where T2 : unmanaged, IComponent where T3 : unmanaged, IComponent { - return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } public readonly ref struct ComponentIterator @@ -1024,7 +1048,13 @@ public unsafe partial struct EntityQuery where T3 : unmanaged, IComponent where T4 : unmanaged, IComponent { - return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } public readonly ref struct ComponentIterator @@ -1261,7 +1291,13 @@ public unsafe partial struct EntityQuery where T4 : unmanaged, IComponent where T5 : unmanaged, IComponent { - return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } public readonly ref struct ComponentIterator @@ -1508,7 +1544,13 @@ public unsafe partial struct EntityQuery where T5 : unmanaged, IComponent where T6 : unmanaged, IComponent { - return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } public readonly ref struct ComponentIterator @@ -1765,7 +1807,13 @@ public unsafe partial struct EntityQuery where T6 : unmanaged, IComponent where T7 : unmanaged, IComponent { - return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new ComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } } diff --git a/Ghost.Entities/Templates/EntityQuery.ComponentIterator.tt b/Ghost.Entities/Templates/EntityQuery.ComponentIterator.tt index 4c932d2..2148c70 100644 --- a/Ghost.Entities/Templates/EntityQuery.ComponentIterator.tt +++ b/Ghost.Entities/Templates/EntityQuery.ComponentIterator.tt @@ -221,7 +221,13 @@ public unsafe partial struct EntityQuery public readonly ComponentIterator<<#= generics#>> GetComponentIterator<<#= generics#>>() <#= restrictions #> { - return new ComponentIterator<<#= generics#>>(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new ComponentIterator<<#= generics#>>(_matchingArchetypes.AsReadOnly(), _mask, world); } <# } #> diff --git a/Ghost.Entities/Templates/EntityQuery.EntityComponentIterator.gen.cs b/Ghost.Entities/Templates/EntityQuery.EntityComponentIterator.gen.cs index d0b0638..e04014b 100644 --- a/Ghost.Entities/Templates/EntityQuery.EntityComponentIterator.gen.cs +++ b/Ghost.Entities/Templates/EntityQuery.EntityComponentIterator.gen.cs @@ -199,7 +199,13 @@ public unsafe partial struct EntityQuery public readonly EntityComponentIterator GetEntityComponentIterator() where T0 : unmanaged, IComponent { - return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } public readonly ref struct EntityComponentIterator @@ -403,7 +409,13 @@ public unsafe partial struct EntityQuery where T0 : unmanaged, IComponent where T1 : unmanaged, IComponent { - return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } public readonly ref struct EntityComponentIterator @@ -617,7 +629,13 @@ public unsafe partial struct EntityQuery where T1 : unmanaged, IComponent where T2 : unmanaged, IComponent { - return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } public readonly ref struct EntityComponentIterator @@ -841,7 +859,13 @@ public unsafe partial struct EntityQuery where T2 : unmanaged, IComponent where T3 : unmanaged, IComponent { - return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } public readonly ref struct EntityComponentIterator @@ -1075,7 +1099,13 @@ public unsafe partial struct EntityQuery where T3 : unmanaged, IComponent where T4 : unmanaged, IComponent { - return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } public readonly ref struct EntityComponentIterator @@ -1319,7 +1349,13 @@ public unsafe partial struct EntityQuery where T4 : unmanaged, IComponent where T5 : unmanaged, IComponent { - return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } public readonly ref struct EntityComponentIterator @@ -1573,7 +1609,13 @@ public unsafe partial struct EntityQuery where T5 : unmanaged, IComponent where T6 : unmanaged, IComponent { - return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } public readonly ref struct EntityComponentIterator @@ -1837,7 +1879,13 @@ public unsafe partial struct EntityQuery where T6 : unmanaged, IComponent where T7 : unmanaged, IComponent { - return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new EntityComponentIterator(_matchingArchetypes.AsReadOnly(), _mask, world); } } diff --git a/Ghost.Entities/Templates/EntityQuery.EntityComponentIterator.tt b/Ghost.Entities/Templates/EntityQuery.EntityComponentIterator.tt index 9820a67..1dadc2b 100644 --- a/Ghost.Entities/Templates/EntityQuery.EntityComponentIterator.tt +++ b/Ghost.Entities/Templates/EntityQuery.EntityComponentIterator.tt @@ -222,7 +222,13 @@ public unsafe partial struct EntityQuery public readonly EntityComponentIterator<<#= generics#>> GetEntityComponentIterator<<#= generics#>>() <#= restrictions #> { - return new EntityComponentIterator<<#= generics#>>(_matchingArchetypes.AsReadOnly(), _mask, World.GetWorld(_worldID).GetValueOrThrow()); + var world = World.GetWorld(_worldID); + if (world is null) + { + return default; + } + + return new EntityComponentIterator<<#= generics#>>(_matchingArchetypes.AsReadOnly(), _mask, world); } <# } #> diff --git a/Ghost.Entities/Templates/EntityQuery.JobEntity.gen.cs b/Ghost.Entities/Templates/EntityQuery.JobEntity.gen.cs index 06728cd..6f28797 100644 --- a/Ghost.Entities/Templates/EntityQuery.JobEntity.gen.cs +++ b/Ghost.Entities/Templates/EntityQuery.JobEntity.gen.cs @@ -1095,7 +1095,12 @@ public unsafe partial struct EntityQuery where TJob : unmanaged, IJobEntity where T0 : unmanaged, IComponent { - var world = World.GetWorld(_worldID).GetValueOrThrow(); + var world = World.GetWorld(_worldID); + if (world is null) + { + return JobHandle.Invalid; + } + if (world.JobScheduler == null) { throw new InvalidOperationException("The World has no JobScheduler assigned."); @@ -1232,7 +1237,12 @@ public unsafe partial struct EntityQuery where T0 : unmanaged, IComponent where T1 : unmanaged, IComponent { - var world = World.GetWorld(_worldID).GetValueOrThrow(); + var world = World.GetWorld(_worldID); + if (world is null) + { + return JobHandle.Invalid; + } + if (world.JobScheduler == null) { throw new InvalidOperationException("The World has no JobScheduler assigned."); @@ -1396,7 +1406,12 @@ public unsafe partial struct EntityQuery where T1 : unmanaged, IComponent where T2 : unmanaged, IComponent { - var world = World.GetWorld(_worldID).GetValueOrThrow(); + var world = World.GetWorld(_worldID); + if (world is null) + { + return JobHandle.Invalid; + } + if (world.JobScheduler == null) { throw new InvalidOperationException("The World has no JobScheduler assigned."); @@ -1587,7 +1602,12 @@ public unsafe partial struct EntityQuery where T2 : unmanaged, IComponent where T3 : unmanaged, IComponent { - var world = World.GetWorld(_worldID).GetValueOrThrow(); + var world = World.GetWorld(_worldID); + if (world is null) + { + return JobHandle.Invalid; + } + if (world.JobScheduler == null) { throw new InvalidOperationException("The World has no JobScheduler assigned."); @@ -1805,7 +1825,12 @@ public unsafe partial struct EntityQuery where T3 : unmanaged, IComponent where T4 : unmanaged, IComponent { - var world = World.GetWorld(_worldID).GetValueOrThrow(); + var world = World.GetWorld(_worldID); + if (world is null) + { + return JobHandle.Invalid; + } + if (world.JobScheduler == null) { throw new InvalidOperationException("The World has no JobScheduler assigned."); @@ -2050,7 +2075,12 @@ public unsafe partial struct EntityQuery where T4 : unmanaged, IComponent where T5 : unmanaged, IComponent { - var world = World.GetWorld(_worldID).GetValueOrThrow(); + var world = World.GetWorld(_worldID); + if (world is null) + { + return JobHandle.Invalid; + } + if (world.JobScheduler == null) { throw new InvalidOperationException("The World has no JobScheduler assigned."); @@ -2322,7 +2352,12 @@ public unsafe partial struct EntityQuery where T5 : unmanaged, IComponent where T6 : unmanaged, IComponent { - var world = World.GetWorld(_worldID).GetValueOrThrow(); + var world = World.GetWorld(_worldID); + if (world is null) + { + return JobHandle.Invalid; + } + if (world.JobScheduler == null) { throw new InvalidOperationException("The World has no JobScheduler assigned."); @@ -2621,7 +2656,12 @@ public unsafe partial struct EntityQuery where T6 : unmanaged, IComponent where T7 : unmanaged, IComponent { - var world = World.GetWorld(_worldID).GetValueOrThrow(); + var world = World.GetWorld(_worldID); + if (world is null) + { + return JobHandle.Invalid; + } + if (world.JobScheduler == null) { throw new InvalidOperationException("The World has no JobScheduler assigned."); diff --git a/Ghost.Entities/Templates/EntityQuery.JobEntity.tt b/Ghost.Entities/Templates/EntityQuery.JobEntity.tt index b170cf1..26eeff8 100644 --- a/Ghost.Entities/Templates/EntityQuery.JobEntity.tt +++ b/Ghost.Entities/Templates/EntityQuery.JobEntity.tt @@ -125,7 +125,12 @@ public unsafe partial struct EntityQuery where TJob : unmanaged, IJobEntity<<#= generics #>> <#= restrictions #> { - var world = World.GetWorld(_worldID).GetValueOrThrow(); + var world = World.GetWorld(_worldID); + if (world is null) + { + return JobHandle.Invalid; + } + if (world.JobScheduler == null) { throw new InvalidOperationException("The World has no JobScheduler assigned."); diff --git a/Ghost.Entities/World.cs b/Ghost.Entities/World.cs index bec9e9a..8a4fda9 100644 --- a/Ghost.Entities/World.cs +++ b/Ghost.Entities/World.cs @@ -46,7 +46,7 @@ public partial class World } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static World GetWorldUncheck(Identifier id) + public static World GetWorldUncheck(Identifier id) { #if DEBUG || GHOST_EDITOR if (id.Value < 0 || id.Value >= s_worlds.Count) @@ -62,15 +62,14 @@ public partial class World } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Result GetWorld(Identifier id) + public static World? GetWorld(Identifier id) { if (id.Value < 0 || id.Value >= s_worlds.Count) { - return ErrorStatus.InvalidArgument; + return null; } - var world = s_worlds[id.Value]; - return world is null ? ErrorStatus.NotFound : world; + return s_worlds[id.Value]; } } diff --git a/Ghost.Graphics.Test/Windows/GraphicsTestWindow.xaml.cs b/Ghost.Graphics.Test/Windows/GraphicsTestWindow.xaml.cs index 50f0599..f15b71b 100644 --- a/Ghost.Graphics.Test/Windows/GraphicsTestWindow.xaml.cs +++ b/Ghost.Graphics.Test/Windows/GraphicsTestWindow.xaml.cs @@ -4,6 +4,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using Misaki.HighPerformance.Mathematics; +using static Ghost.Graphics.D3D12.D3D12ResourceDatabase; namespace Ghost.Graphics.Test.Windows; @@ -15,7 +16,7 @@ public sealed partial class GraphicsTestWindow : Window private bool _isFirstActivationHandled; - public GraphicsTestWindow() + public unsafe GraphicsTestWindow() { InitializeComponent(); diff --git a/Ghost.Graphics/Contracts/IRenderPass.cs b/Ghost.Graphics/Contracts/IRenderPass.cs index a79b4ff..5d5b0a3 100644 --- a/Ghost.Graphics/Contracts/IRenderPass.cs +++ b/Ghost.Graphics/Contracts/IRenderPass.cs @@ -1,4 +1,6 @@ +using Ghost.Core; using Ghost.Graphics.Core; +using Ghost.Graphics.RenderGraphModule; using Ghost.Graphics.RHI; namespace Ghost.Graphics.Contracts; @@ -6,6 +8,6 @@ namespace Ghost.Graphics.Contracts; public interface IRenderPass { void Initialize(ref readonly RenderingContext ctx); - void Execute(ref readonly RenderingContext ctx); + void Build(RenderGraph graph, Identifier backbuffer); void Cleanup(IResourceDatabase resourceDatabase); } diff --git a/Ghost.Graphics/Core/Color.cs b/Ghost.Graphics/Core/Common.cs similarity index 77% rename from Ghost.Graphics/Core/Color.cs rename to Ghost.Graphics/Core/Common.cs index 88c3141..11a0b21 100644 --- a/Ghost.Graphics/Core/Color.cs +++ b/Ghost.Graphics/Core/Common.cs @@ -1,6 +1,8 @@ +using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.Mathematics; using System.Drawing; using System.Runtime.InteropServices; +using TerraFX.Interop.DirectX; namespace Ghost.Graphics.Core; @@ -118,3 +120,26 @@ public struct Color128 : IEquatable return !(left == right); } } + + +[StructLayout(LayoutKind.Sequential)] +public struct Vertex +{ + public static class Semantic + { + public const DXGI_FORMAT ALIGNED_FORMAT = DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT; + public const int COUNT = 5; + + public static readonly FixedText32 Position = new("POSITION"); + public static readonly FixedText32 Normal = new("NORMAL"); + public static readonly FixedText32 Tangent = new("TANGENT"); + public static readonly FixedText32 Uv = new("TEXCOORD"); + public static readonly FixedText32 Color = new("COLOR"); + } + + public float4 position; + public float4 normal; + public float4 tangent; + public float4 uv; + public Color128 color; +} \ No newline at end of file diff --git a/Ghost.Graphics/Core/Material.cs b/Ghost.Graphics/Core/Material.cs index 0886179..2a8cfaa 100644 --- a/Ghost.Graphics/Core/Material.cs +++ b/Ghost.Graphics/Core/Material.cs @@ -61,6 +61,11 @@ public struct Material : IResourceReleasable public readonly Identifier Shader => _shader; public readonly bool IsDirty => _isDirty; + public int ActivePassIndex + { + get; set; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetDirty() { @@ -77,8 +82,13 @@ public struct Material : IResourceReleasable _cBufferCache.ReleaseResource(database); _shader = shaderId; - var shader = database.GetShaderReference(shaderId); + var r = database.GetShaderReference(shaderId); + if (r.IsFailure) + { + return r.Error; + } + ref readonly var shader = ref r.Value; if (_passPipelineOverride.Count < shader.PassCount) { if (!_passPipelineOverride.IsCreated) @@ -187,7 +197,13 @@ public struct Material : IResourceReleasable [MethodImpl(MethodImplOptions.AggressiveInlining)] public ErrorStatus SetKeyword(IResourceDatabase resourceDatabase, int keywordId, bool enabled) { - ref var shader = ref resourceDatabase.GetShaderReference(_shader); + var r = resourceDatabase.GetShaderReference(_shader); + if (r.IsFailure) + { + return r.Error; + } + + ref readonly var shader = ref r.Value; var localIndex = shader.GetLocalKeywordIndex(keywordId); if (localIndex == -1) { @@ -203,7 +219,13 @@ public struct Material : IResourceReleasable [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool IsKeywordEnabled(IResourceDatabase resourceDatabase, int keywordId) { - ref var shader = ref resourceDatabase.GetShaderReference(_shader); + var r = resourceDatabase.GetShaderReference(_shader); + if (r.IsFailure) + { + return false; + } + + ref readonly var shader = ref r.Value; var localIndex = shader.GetLocalKeywordIndex(keywordId); if (localIndex == -1) { @@ -216,13 +238,18 @@ public struct Material : IResourceReleasable [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly void UploadData(ICommandBuffer cmd, bool pixelOnlyResource = true) { - cmd.ResourceBarrier(_cBufferCache.GpuResource.AsResource(), ResourceState.CopyDest); + if (!_isDirty) + { + return; + } + + cmd.TransitionBarrier(_cBufferCache.GpuResource.AsResource(), ResourceState.CopyDest); cmd.UploadBuffer(_cBufferCache.GpuResource, _cBufferCache.CpuData.AsSpan()); var state = pixelOnlyResource ? ResourceState.PixelShaderResource : ResourceState.NonPixelShaderResource | ResourceState.PixelShaderResource; - cmd.ResourceBarrier(_cBufferCache.GpuResource.AsResource(), state); + cmd.TransitionBarrier(_cBufferCache.GpuResource.AsResource(), state); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Ghost.Graphics/Core/Mesh.cs b/Ghost.Graphics/Core/Mesh.cs index 9c9f53c..774eb0e 100644 --- a/Ghost.Graphics/Core/Mesh.cs +++ b/Ghost.Graphics/Core/Mesh.cs @@ -95,12 +95,6 @@ public struct Mesh : IResourceReleasable get; internal set; } - public Mesh() - { - VertexBuffer = Handle.Invalid; - IndexBuffer = Handle.Invalid; - } - internal Mesh(ReadOnlySpan vertices, ReadOnlySpan indices, Handle vertexBuffer, Handle indexBuffer) { Vertices = new UnsafeList(vertices.Length, Allocator.Persistent); @@ -119,7 +113,7 @@ public struct Mesh : IResourceReleasable _indices.Dispose(); } - void IResourceReleasable.ReleaseResource(IResourceDatabase database) + readonly void IResourceReleasable.ReleaseResource(IResourceDatabase database) { ReleaseCpuResources(); diff --git a/Ghost.Graphics/Core/RenderOutput.cs b/Ghost.Graphics/Core/RenderOutput.cs index 8ada196..295a844 100644 --- a/Ghost.Graphics/Core/RenderOutput.cs +++ b/Ghost.Graphics/Core/RenderOutput.cs @@ -33,12 +33,12 @@ internal class SwapChainRenderOutput : IRenderOutput public void BeginRender(ICommandBuffer cmd) { - cmd.ResourceBarrier(GetRenderTarget().AsResource(), ResourceState.Present, ResourceState.RenderTarget); + cmd.TransitionBarrier(GetRenderTarget().AsResource(), ResourceState.Present, ResourceState.RenderTarget); } public void EndRender(ICommandBuffer cmd) { - cmd.ResourceBarrier(GetRenderTarget().AsResource(), ResourceState.RenderTarget, ResourceState.Present); + cmd.TransitionBarrier(GetRenderTarget().AsResource(), ResourceState.RenderTarget, ResourceState.Present); } public void Present() diff --git a/Ghost.Graphics/Core/RenderingContext.cs b/Ghost.Graphics/Core/RenderingContext.cs index c79ecc0..de674f0 100644 --- a/Ghost.Graphics/Core/RenderingContext.cs +++ b/Ghost.Graphics/Core/RenderingContext.cs @@ -50,13 +50,18 @@ public readonly unsafe ref struct RenderingContext public Handle CreateMesh(UnsafeList vertices, UnsafeList indices, bool staticMesh) { var mesh = ResourceAllocator.CreateMesh(vertices, indices); - ref var meshData = ref ResourceDatabase.GetMeshReference(mesh); + var r = ResourceDatabase.GetMeshReference(mesh); + if (r.IsFailure) + { + return mesh; + } + ref readonly var meshData = ref r.Value; var vertexHandle = meshData.VertexBuffer.AsResource(); var indexHandle = meshData.IndexBuffer.AsResource(); - _directCmd.ResourceBarrier(vertexHandle, ResourceState.CopyDest); - _directCmd.ResourceBarrier(indexHandle, ResourceState.CopyDest); + _directCmd.TransitionBarrier(vertexHandle, ResourceState.CopyDest); + _directCmd.TransitionBarrier(indexHandle, ResourceState.CopyDest); _directCmd.UploadBuffer(meshData.VertexBuffer, meshData.Vertices.AsSpan()); _directCmd.UploadBuffer(meshData.IndexBuffer, meshData.Indices.AsSpan()); @@ -64,8 +69,8 @@ public readonly unsafe ref struct RenderingContext if (staticMesh) { meshData.ReleaseCpuResources(); - _directCmd.ResourceBarrier(vertexHandle, ResourceState.NonPixelShaderResource); - _directCmd.ResourceBarrier(indexHandle, ResourceState.NonPixelShaderResource); + _directCmd.TransitionBarrier(vertexHandle, ResourceState.NonPixelShaderResource); + _directCmd.TransitionBarrier(indexHandle, ResourceState.NonPixelShaderResource); } return mesh; @@ -91,16 +96,22 @@ public readonly unsafe ref struct RenderingContext /// Whether to mark the mesh as static. If it's true, the cpu buffer of the mesh will not be avaliable any more public void UploadMesh(Handle mesh, bool markMeshStatic) { - ref var meshRef = ref ResourceDatabase.GetMeshReference(mesh); + var r = ResourceDatabase.GetMeshReference(mesh); + if (r.IsFailure) + { + return; + } - _directCmd.ResourceBarrier(meshRef.VertexBuffer.AsResource(), ResourceState.CopyDest); - _directCmd.ResourceBarrier(meshRef.IndexBuffer.AsResource(), ResourceState.CopyDest); + ref readonly var meshRef = ref r.Value; + + _directCmd.TransitionBarrier(meshRef.VertexBuffer.AsResource(), ResourceState.CopyDest); + _directCmd.TransitionBarrier(meshRef.IndexBuffer.AsResource(), ResourceState.CopyDest); _directCmd.UploadBuffer(meshRef.VertexBuffer, meshRef.Vertices.AsSpan()); _directCmd.UploadBuffer(meshRef.IndexBuffer, meshRef.Indices.AsSpan()); - _directCmd.ResourceBarrier(meshRef.VertexBuffer.AsResource(), ResourceState.NonPixelShaderResource); - _directCmd.ResourceBarrier(meshRef.IndexBuffer.AsResource(), ResourceState.NonPixelShaderResource); + _directCmd.TransitionBarrier(meshRef.VertexBuffer.AsResource(), ResourceState.NonPixelShaderResource); + _directCmd.TransitionBarrier(meshRef.IndexBuffer.AsResource(), ResourceState.NonPixelShaderResource); if (markMeshStatic) { @@ -110,7 +121,13 @@ public readonly unsafe ref struct RenderingContext public void UpdateObjectData(Handle mesh, float4x4 localToWorld) { - ref var meshData = ref ResourceDatabase.GetMeshReference(mesh); + var r = ResourceDatabase.GetMeshReference(mesh); + if (r.IsFailure) + { + return; + } + + ref readonly var meshData = ref r.Value; var data = new PerObjectData { localToWorld = localToWorld, @@ -122,9 +139,9 @@ public readonly unsafe ref struct RenderingContext var bufferHandle = meshData.ObjectDataBuffer.AsResource(); - _directCmd.ResourceBarrier(bufferHandle, ResourceState.CopyDest); + _directCmd.TransitionBarrier(bufferHandle, ResourceState.CopyDest); _directCmd.UploadBuffer(meshData.ObjectDataBuffer, [data]); - _directCmd.ResourceBarrier(bufferHandle, ResourceState.NonPixelShaderResource | ResourceState.PixelShaderResource); + _directCmd.TransitionBarrier(bufferHandle, ResourceState.NonPixelShaderResource | ResourceState.PixelShaderResource); } public Handle CreateTexture(ref readonly TextureDesc desc, ReadOnlySpan data, string name) @@ -149,7 +166,7 @@ public readonly unsafe ref struct RenderingContext desc.TextureDescription.Format.GetSurfaceInfo(desc.TextureDescription.Width, desc.TextureDescription.Height, out var rowPitch, out var slicePitch, out _); - _directCmd.ResourceBarrier(texture.AsResource(), ResourceState.CopyDest); + _directCmd.TransitionBarrier(texture.AsResource(), ResourceState.CopyDest); fixed (T* pData = data) { @@ -163,65 +180,4 @@ public readonly unsafe ref struct RenderingContext _directCmd.UploadTexture(texture, [subresourceData]); } } - - // TODO: Ideally we should queue the draw call to our rendering system, and render it in a full rendering pipeline. - // This is just a place holder for now for testing purpose. - public void DispatchMesh(Handle mesh, Handle material, Identifier passID, uint numThreadsX) - { - ref var meshRef = ref ResourceDatabase.GetMeshReference(mesh); - ref var materialRef = ref ResourceDatabase.GetMaterialReference(material); - ref var shader = ref ResourceDatabase.GetShaderReference(materialRef.Shader); - - var passIndex = shader.GetPassIndex(passID); - if (passIndex == -1) - { - throw new InvalidOperationException("Shader pass not found in the material's shader."); - } - - ref var pass = ref shader.GetPassReference(passIndex); - - var passPipelineHash = new PassPipelineHash([TextureFormat.B8G8R8A8_UNorm], TextureFormat.Unknown); - var materialPipeline = materialRef.GetPassPipelineOverride(passIndex); - - // Mask out the keywords that are not used in this pass. - var variantMask = materialRef._keywordMask & pass.KeywordIDs; - var shaderVariantKey = RHIUtility.CreateShaderVariantKey(pass.Key, in variantMask); - - var pipelineKey = RHIUtility.CreateGraphicsPipelineKey(shaderVariantKey, materialPipeline, passPipelineHash); - - if (!_engine.PipelineLibrary.HasPipeline(pipelineKey)) - { - var r = _engine.ShaderCompiler.LoadCompiledCache(shaderVariantKey); - if (r.IsFailure) - { - throw new InvalidOperationException("Failed to load compiled shader cache for pipeline state object creation."); - } - - var psoDes = new GraphicsPSODescriptor - { - VariantKey = shaderVariantKey, - PipelineOption = materialRef.GetPassPipelineOverride(passIndex), - - RtvFormats = [TextureFormat.B8G8R8A8_UNorm], - DsvFormat = TextureFormat.Unknown, - }; - - var compiled = r.Value; - _engine.PipelineLibrary.CompilePSO(in psoDes, in compiled).GetValueOrThrow(); - } - - _directCmd.SetPipelineState(pipelineKey); - - var data = new PushConstantsData - { - objectIndex = _engine.ResourceDatabase.GetBindlessIndex(meshRef.ObjectDataBuffer.AsResource()), - materialIndex = _engine.ResourceDatabase.GetBindlessIndex(materialRef._cBufferCache.GpuResource.AsResource()), - }; - - var pushConstantSpan = new ReadOnlySpan(&data, sizeof(PushConstantsData) / sizeof(uint)); - _directCmd.SetGraphicsRoot32Constants(RootSignatureLayout.PUSH_CONSTANT_SLOT, pushConstantSpan); - - var threadGroupCountX = ((uint)meshRef.IndexCount + numThreadsX - 1) / numThreadsX; - _directCmd.DispatchMesh(threadGroupCountX, 1, 1); - } } diff --git a/Ghost.Graphics/Core/Vertex.cs b/Ghost.Graphics/Core/Vertex.cs deleted file mode 100644 index 4f15fa5..0000000 --- a/Ghost.Graphics/Core/Vertex.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Misaki.HighPerformance.LowLevel.Collections; -using Misaki.HighPerformance.Mathematics; -using System.Runtime.InteropServices; -using TerraFX.Interop.DirectX; - -namespace Ghost.Graphics.Core; - -[StructLayout(LayoutKind.Sequential)] -public struct Vertex -{ - public static class Semantic - { - public const DXGI_FORMAT ALIGNED_FORMAT = DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT; - public const int COUNT = 5; - - public static readonly FixedText32 position = new("POSITION"); - public static readonly FixedText32 normal = new("NORMAL"); - public static readonly FixedText32 tangent = new("TANGENT"); - public static readonly FixedText32 uv = new("TEXCOORD"); - public static readonly FixedText32 color = new("COLOR"); - } - - public float4 position; - public float4 normal; - public float4 tangent; - public float4 uv; - public Color128 color; -} diff --git a/Ghost.Graphics/D3D12/D3D12CommandBuffer.cs b/Ghost.Graphics/D3D12/D3D12CommandBuffer.cs index 5b4fd97..d712fc9 100644 --- a/Ghost.Graphics/D3D12/D3D12CommandBuffer.cs +++ b/Ghost.Graphics/D3D12/D3D12CommandBuffer.cs @@ -5,7 +5,6 @@ using Ghost.Graphics.D3D12.Utilities; using Ghost.Graphics.RHI; using Misaki.HighPerformance.LowLevel; using Misaki.HighPerformance.LowLevel.Utilities; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -115,7 +114,7 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer [MethodImpl(MethodImplOptions.AggressiveInlining)] #if DEBUG - [DoesNotReturn] + [System.Diagnostics.CodeAnalysis.DoesNotReturn] private static void RecordError(string cmdName, ErrorStatus status) #else private void RecordError(string cmdName, ErrorStatus status) @@ -206,51 +205,82 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer #endif IncrementCommandCount(); + if (barrierDescs.IsEmpty) + { + return; + } + var count = 0u; var pBarriers = stackalloc D3D12_RESOURCE_BARRIER[barrierDescs.Length]; for (var i = 0; i < barrierDescs.Length; i++) { var desc = barrierDescs[i]; - if (desc.StateBefore == desc.StateAfter) - { - continue; - } + D3D12_RESOURCE_BARRIER barrier = default; - if (!desc.Resource.IsValid) + switch (desc.type) { - RecordError(nameof(ResourceBarrier), ErrorStatus.InvalidArgument); - continue; - } + case BarrierType.Transition: + if (desc.transition.stateBefore == desc.transition.stateAfter) + { + continue; + } - var recordResult = _resourceDatabase.GetResourceRecord(desc.Resource); - if (recordResult.Error != ErrorStatus.None) - { - RecordError(nameof(ResourceBarrier), recordResult.Error); - continue; - } + var recordResult = _resourceDatabase.GetResourceRecord(desc.transition.resource); + if (recordResult.Error != ErrorStatus.None) + { + RecordError(nameof(TransitionBarrier), recordResult.Error); + continue; + } - ref var record = ref recordResult.Value; - if (record.state != desc.StateBefore) - { - RecordError(nameof(ResourceBarrier), ErrorStatus.InvalidState); - continue; - } + ref var record = ref recordResult.Value; + var stateBefore = desc.transition.stateBefore == ResourceState.Auto ? record.state : desc.transition.stateBefore; + + barrier = D3D12_RESOURCE_BARRIER.InitTransition(record.ResourcePtr, + stateBefore.ToD3D12States(), desc.transition.stateAfter.ToD3D12States()); - var barrier = D3D12_RESOURCE_BARRIER.InitTransition(record.ResourcePtr, - desc.StateBefore.ToD3D12States(), desc.StateAfter.ToD3D12States()); + record.state = desc.transition.stateAfter; + break; + + case BarrierType.Aliasing: + var recordBeforeResult = _resourceDatabase.GetResourceRecord(desc.aliasing.resourceBefore); + if (recordBeforeResult.Error != ErrorStatus.None) + { + RecordError(nameof(TransitionBarrier), recordBeforeResult.Error); + continue; + } + + var recordAfterResult = _resourceDatabase.GetResourceRecord(desc.aliasing.resourceAfter); + if (recordAfterResult.Error != ErrorStatus.None) + { + RecordError(nameof(TransitionBarrier), recordAfterResult.Error); + continue; + } + + barrier = D3D12_RESOURCE_BARRIER.InitAliasing( + recordBeforeResult.Value.ResourcePtr, + recordAfterResult.Value.ResourcePtr); + break; + case BarrierType.UAV: + var recordUavResult = _resourceDatabase.GetResourceRecord(desc.uav.resource); + if (recordUavResult.Error != ErrorStatus.None) + { + RecordError(nameof(TransitionBarrier), recordUavResult.Error); + continue; + } + + barrier = D3D12_RESOURCE_BARRIER.InitUAV(recordUavResult.Value.ResourcePtr); + break; + } pBarriers[count] = barrier; count++; - - // Update the resource state in the database - record.state = desc.StateAfter; } - + _commandList.Get()->ResourceBarrier(count, pBarriers); } - public void ResourceBarrier(Handle resource, ResourceState stateBefore, ResourceState stateAfter) + public void TransitionBarrier(Handle resource, ResourceState stateBefore, ResourceState stateAfter) { ThrowIfDisposed(); ThrowIfNotRecording(); @@ -270,7 +300,7 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer var recordResult = _resourceDatabase.GetResourceRecord(resource); if (recordResult.Error != ErrorStatus.None) { - RecordError(nameof(ResourceBarrier), recordResult.Error); + RecordError(nameof(TransitionBarrier), recordResult.Error); return; } @@ -282,7 +312,7 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer record.state = stateAfter; } - public void ResourceBarrier(Handle resource, ResourceState stateAfter) + public void TransitionBarrier(Handle resource, ResourceState stateAfter) { ThrowIfDisposed(); ThrowIfNotRecording(); @@ -297,7 +327,7 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer var recordResult = _resourceDatabase.GetResourceRecord(resource); if (recordResult.Error != ErrorStatus.None) { - RecordError(nameof(ResourceBarrier), recordResult.Error); + RecordError(nameof(TransitionBarrier), recordResult.Error); return; } @@ -314,6 +344,38 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer record.state = stateAfter; } + public void AliasBarrier(Handle resourceBefore, Handle resourceAfter) + { + ThrowIfDisposed(); + ThrowIfNotRecording(); +#if !DEBUG + if (_lastError.Status != ErrorStatus.None) + { + return; + } +#endif + IncrementCommandCount(); + + var recordBeforeResult = _resourceDatabase.GetResourceRecord(resourceBefore); + if (recordBeforeResult.Error != ErrorStatus.None) + { + RecordError(nameof(AliasBarrier), recordBeforeResult.Error); + return; + } + + var recordAfterResult = _resourceDatabase.GetResourceRecord(resourceAfter); + if (recordAfterResult.Error != ErrorStatus.None) + { + RecordError(nameof(AliasBarrier), recordAfterResult.Error); + return; + } + + var barrier = D3D12_RESOURCE_BARRIER.InitAliasing( + recordBeforeResult.Value.ResourcePtr, + recordAfterResult.Value.ResourcePtr); + _commandList.Get()->ResourceBarrier(1, &barrier); + } + public void SetRenderTargets(ReadOnlySpan> renderTargets, Handle depthTarget) { ThrowIfDisposed(); @@ -367,6 +429,69 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer _commandList.Get()->OMSetRenderTargets(rtvCount, pRtvHandles, FALSE, pDsvHandle); } + public void ClearRenderTargetView(Handle renderTarget, Color128 clearColor) + { + ThrowIfDisposed(); + ThrowIfNotRecording(); +#if !DEBUG + if (_lastError.Status != ErrorStatus.None) + { + return; + } +#endif + IncrementCommandCount(); + + var recordResult = _resourceDatabase.GetResourceRecord(renderTarget.AsResource()); + if (recordResult.Error != ErrorStatus.None) + { + RecordError(nameof(ClearRenderTargetView), recordResult.Error); + return; + } + + ref var record = ref recordResult.Value; + var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.rtv); + var color = stackalloc float[4] + { + clearColor.r, + clearColor.g, + clearColor.b, + clearColor.a + }; + + _commandList.Get()->ClearRenderTargetView(cpuHandle, color, 0, null); + } + + public void ClearDepthStencilView(Handle depthStencil, bool inlcudeDepth, bool includeStencil, float clearDepth = 1.0f, byte clearStencil = 0) + { + ThrowIfDisposed(); + ThrowIfNotRecording(); +#if !DEBUG + if (_lastError.Status != ErrorStatus.None) + { + return; + } +#endif + IncrementCommandCount(); + + var recordResult = _resourceDatabase.GetResourceRecord(depthStencil.AsResource()); + if (recordResult.Error != ErrorStatus.None) + { + RecordError(nameof(ClearDepthStencilView), recordResult.Error); + return; + } + + ref var record = ref recordResult.Value; + var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.dsv); + var flag = (inlcudeDepth ? D3D12_CLEAR_FLAG_DEPTH : 0) | (includeStencil ? D3D12_CLEAR_FLAG_STENCIL : 0); + + _commandList.Get()->ClearDepthStencilView(cpuHandle, + flag, + clearDepth, + clearStencil, + 0, + null); + } + public void BeginRenderPass(ReadOnlySpan rtDescs, PassDepthStencilDesc depthDesc, bool allowUAVWrites = false) { ThrowIfDisposed(); diff --git a/Ghost.Graphics/D3D12/D3D12DescriptorAllocator.cs b/Ghost.Graphics/D3D12/D3D12DescriptorAllocator.cs index f45cb4a..ad424f5 100644 --- a/Ghost.Graphics/D3D12/D3D12DescriptorAllocator.cs +++ b/Ghost.Graphics/D3D12/D3D12DescriptorAllocator.cs @@ -18,12 +18,12 @@ internal unsafe class D3D12DescriptorAllocator : IDisposable private bool _disposed; - public D3D12DescriptorAllocator(D3D12RenderDevice device, int initialRtvCount = 256, int initialDsvCount = 256, int initialSrvCount = 200_000, int initialSamplerCount = 256) + public D3D12DescriptorAllocator(D3D12RenderDevice device, int initialRtvCount = 512, int initialDsvCount = 512, int initialSrvCount = 200_000, int initialSamplerCount = 256) { - _rtvHeap = new D3D12DescriptorHeap("rtv", device, D3D12_DESCRIPTOR_HEAP_TYPE_RTV, initialRtvCount, initialRtvCount / 2); - _dsvHeap = new D3D12DescriptorHeap("dsv", device, D3D12_DESCRIPTOR_HEAP_TYPE_DSV, initialDsvCount, initialDsvCount / 2); - _cbvSrvUavHeap = new D3D12DescriptorHeap("srv", device, D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, initialSrvCount, initialSrvCount / 2); - _samplerHeap = new D3D12DescriptorHeap("sampler", device, D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER, initialSamplerCount, initialSamplerCount); + _rtvHeap = new D3D12DescriptorHeap("rtv", device, D3D12_DESCRIPTOR_HEAP_TYPE_RTV, initialRtvCount); + _dsvHeap = new D3D12DescriptorHeap("dsv", device, D3D12_DESCRIPTOR_HEAP_TYPE_DSV, initialDsvCount); + _cbvSrvUavHeap = new D3D12DescriptorHeap("srv", device, D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, initialSrvCount); + _samplerHeap = new D3D12DescriptorHeap("sampler", device, D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER, initialSamplerCount); } ~D3D12DescriptorAllocator() @@ -33,11 +33,11 @@ internal unsafe class D3D12DescriptorAllocator : IDisposable #region RTV Methods - public Identifier AllocateRTV(bool dynamic = false) + public Identifier AllocateRTV() { ObjectDisposedException.ThrowIf(_disposed, this); - var index = dynamic ? _rtvHeap.AllocateDescriptorDynamic() : _rtvHeap.AllocateDescriptor(); + var index = _rtvHeap.AllocateDescriptor(); if (index == -1) { throw new InvalidOperationException("Failed to allocate RTV descriptor"); @@ -46,11 +46,11 @@ internal unsafe class D3D12DescriptorAllocator : IDisposable return new Identifier(index); } - public Identifier[] AllocateRTVs(int count, bool dynamic = false) + public Identifier[] AllocateRTVs(int count) { ObjectDisposedException.ThrowIf(_disposed, this); - var baseIndex = dynamic ? _rtvHeap.AllocateDescriptorsDynamic(count) : _rtvHeap.AllocateDescriptors(count); + var baseIndex = _rtvHeap.AllocateDescriptors(count); if (baseIndex == -1) { throw new InvalidOperationException($"Failed to allocate {count} RTV descriptors"); @@ -91,39 +91,15 @@ internal unsafe class D3D12DescriptorAllocator : IDisposable } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void MakePersistent(Identifier descriptor) - { - ObjectDisposedException.ThrowIf(_disposed, this); - _rtvHeap.CopyToPersistentHeap(descriptor.Value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void MakePersistent(ReadOnlySpan> descriptors) - { - ObjectDisposedException.ThrowIf(_disposed, this); - foreach (var descriptor in descriptors) - { - MakePersistent(descriptor); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ResetRTVDynamicHeap() - { - ObjectDisposedException.ThrowIf(_disposed, this); - _rtvHeap.ResetDynamicHeap(); - } - #endregion #region DSV Methods - public Identifier AllocateDSV(bool dynamic = false) + public Identifier AllocateDSV() { ObjectDisposedException.ThrowIf(_disposed, this); - var index = dynamic ? _dsvHeap.AllocateDescriptorDynamic() : _dsvHeap.AllocateDescriptor(); + var index = _dsvHeap.AllocateDescriptor(); if (index == -1) { throw new InvalidOperationException("Failed to allocate DSV descriptor"); @@ -132,11 +108,11 @@ internal unsafe class D3D12DescriptorAllocator : IDisposable return new Identifier(index); } - public Identifier[] AllocateDSVs(int count, bool dynamic = false) + public Identifier[] AllocateDSVs(int count) { ObjectDisposedException.ThrowIf(_disposed, this); - var baseIndex = dynamic ? _dsvHeap.AllocateDescriptorsDynamic(count) : _dsvHeap.AllocateDescriptors(count); + var baseIndex = _dsvHeap.AllocateDescriptors(count); if (baseIndex == -1) { throw new InvalidOperationException($"Failed to allocate {count} DSV descriptors"); @@ -174,53 +150,28 @@ internal unsafe class D3D12DescriptorAllocator : IDisposable } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void MakePersistent(Identifier descriptor) - { - ObjectDisposedException.ThrowIf(_disposed, this); - _dsvHeap.CopyToPersistentHeap(descriptor.Value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void MakePersistent(ReadOnlySpan> descriptors) - { - ObjectDisposedException.ThrowIf(_disposed, this); - foreach (var descriptor in descriptors) - { - MakePersistent(descriptor); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ResetDSVDynamicHeap() - { - ObjectDisposedException.ThrowIf(_disposed, this); - _dsvHeap.ResetDynamicHeap(); - } - #endregion #region CBV_SRV_UAV Methods - public Identifier AllocateCbvSrvUav(bool dynamic = false) + public Identifier AllocateCbvSrvUav() { ObjectDisposedException.ThrowIf(_disposed, this); - var index = dynamic ? _cbvSrvUavHeap.AllocateDescriptorDynamic() : _cbvSrvUavHeap.AllocateDescriptor(); + var index = _cbvSrvUavHeap.AllocateDescriptor(); if (index == -1) { throw new InvalidOperationException("Failed to allocate CBV/SRV/UAV descriptor"); } - _cbvSrvUavHeap.CopyToShaderVisibleHeap(index); return new Identifier(index); } - public Identifier[] AllocateSRVs(int count, bool dynamic = false) + public Identifier[] AllocateSRVs(int count) { ObjectDisposedException.ThrowIf(_disposed, this); - var baseIndex = dynamic ? _cbvSrvUavHeap.AllocateDescriptorsDynamic(count) : _cbvSrvUavHeap.AllocateDescriptors(count); + var baseIndex = _cbvSrvUavHeap.AllocateDescriptors(count); if (baseIndex == -1) { throw new InvalidOperationException($"Failed to allocate {count} CBV/SRV/UAV descriptors"); @@ -233,10 +184,15 @@ internal unsafe class D3D12DescriptorAllocator : IDisposable descriptors[i] = new Identifier(index); } - _cbvSrvUavHeap.CopyToShaderVisibleHeap(baseIndex, count); return descriptors; } + public void CopyToShaderVisible(Identifier descriptor) + { + ObjectDisposedException.ThrowIf(_disposed, this); + _cbvSrvUavHeap.CopyToShaderVisibleHeap(descriptor.Value); + } + public D3D12_CPU_DESCRIPTOR_HANDLE GetCpuHandle(Identifier descriptor) { ObjectDisposedException.ThrowIf(_disposed, this); @@ -271,30 +227,6 @@ internal unsafe class D3D12DescriptorAllocator : IDisposable } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void MakePersistent(Identifier descriptor) - { - ObjectDisposedException.ThrowIf(_disposed, this); - _cbvSrvUavHeap.CopyToPersistentHeap(descriptor.Value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void MakePersistent(ReadOnlySpan> descriptors) - { - ObjectDisposedException.ThrowIf(_disposed, this); - foreach (var descriptor in descriptors) - { - MakePersistent(descriptor); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ResetCbvSrvUavDynamicHeap() - { - ObjectDisposedException.ThrowIf(_disposed, this); - _cbvSrvUavHeap.ResetDynamicHeap(); - } - #endregion #region Sampler Methods @@ -309,7 +241,6 @@ internal unsafe class D3D12DescriptorAllocator : IDisposable throw new InvalidOperationException("Failed to allocate Sampler descriptor"); } - _samplerHeap.CopyToShaderVisibleHeap(index); return new Identifier(index); } @@ -330,10 +261,15 @@ internal unsafe class D3D12DescriptorAllocator : IDisposable descriptors[i] = new Identifier(index); } - _samplerHeap.CopyToShaderVisibleHeap(baseIndex, count); return descriptors; } + public void CopyToShaderVisible(Identifier descriptor) + { + ObjectDisposedException.ThrowIf(_disposed, this); + _samplerHeap.CopyToShaderVisibleHeap(descriptor.Value); + } + public D3D12_CPU_DESCRIPTOR_HANDLE GetCpuHandle(Identifier descriptor) { ObjectDisposedException.ThrowIf(_disposed, this); diff --git a/Ghost.Graphics/D3D12/D3D12DescriptorHeap.cs b/Ghost.Graphics/D3D12/D3D12DescriptorHeap.cs index 897afc4..ea03c26 100644 --- a/Ghost.Graphics/D3D12/D3D12DescriptorHeap.cs +++ b/Ghost.Graphics/D3D12/D3D12DescriptorHeap.cs @@ -1,7 +1,7 @@ using Ghost.Core.Utilities; +using Ghost.Graphics.D3D12.Utilities; using Misaki.HighPerformance.LowLevel; using Misaki.HighPerformance.LowLevel.Collections; -using Misaki.HighPerformance.LowLevel.Utilities; using System.Diagnostics; using System.Numerics; using TerraFX.Interop.DirectX; @@ -22,10 +22,7 @@ internal unsafe struct D3D12DescriptorHeap : IDisposable private D3D12_CPU_DESCRIPTOR_HANDLE _startCpuHandleShaderVisible; private D3D12_GPU_DESCRIPTOR_HANDLE _startGpuHandleShaderVisible; private int _searchStart; - private UnsafeArray _allocatedDescriptors; - - private readonly int _dynamicHeapStart; - private int _currentDynamicOffset; + private UnsafeBitSet _allocatedDescriptors; private readonly Lock _lock = new(); @@ -57,7 +54,7 @@ internal unsafe struct D3D12DescriptorHeap : IDisposable public readonly ID3D12DescriptorHeap* Heap => _heap.Get(); public readonly ID3D12DescriptorHeap* ShaderVisibleHeap => _shaderVisibleHeap.Get(); - public D3D12DescriptorHeap(string name, D3D12RenderDevice device, D3D12_DESCRIPTOR_HEAP_TYPE type, int numDescriptors, int dynamicHeapStart) + public D3D12DescriptorHeap(string name, D3D12RenderDevice device, D3D12_DESCRIPTOR_HEAP_TYPE type, int numDescriptors) { numDescriptors = Math.Max(64, numDescriptors); @@ -68,16 +65,13 @@ internal unsafe struct D3D12DescriptorHeap : IDisposable ShaderVisible = type == D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV || type == D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER; Stride = device.NativeDevice.Get()->GetDescriptorHandleIncrementSize(type); - _dynamicHeapStart = Math.Clamp(dynamicHeapStart, 0, numDescriptors); - _currentDynamicOffset = 0; - var success = AllocateResources(numDescriptors); Debug.Assert(success); - _heap.Get()->SetName(name.AsSpan().GetUnsafePtr()); + _heap.Get()->SetName(name); if (ShaderVisible) { - _shaderVisibleHeap.Get()->SetName($"{name} Shader Visible".AsSpan().GetUnsafePtr()); + _shaderVisibleHeap.Get()->SetName($"{name} Shader Visible"); } } @@ -94,7 +88,7 @@ internal unsafe struct D3D12DescriptorHeap : IDisposable // Find a contiguous range of 'count' indices for which _allocatedDescriptors[index] is false for (var index = _searchStart; index < NumDescriptors; index++) { - if (_allocatedDescriptors[index]) + if (_allocatedDescriptors.IsSet(index)) { freeCount = 0; } @@ -111,15 +105,20 @@ internal unsafe struct D3D12DescriptorHeap : IDisposable } } - if (!found || foundIndex >= _dynamicHeapStart) + if (!found) { - Debug.Assert(false, "ERROR: Descriptor heap is full!"); - return _INVALID_DESCRIPTOR_INDEX; + foundIndex = NumDescriptors; + + if (!Grow(NumDescriptors + count)) + { + Debug.WriteLine("Error: Failed to grow descriptor heap."); + return _INVALID_DESCRIPTOR_INDEX; + } } for (var index = foundIndex; index < foundIndex + count; index++) { - _allocatedDescriptors[index] = true; + _allocatedDescriptors.SetBit(index); } NumAllocatedDescriptors += count; @@ -128,38 +127,6 @@ internal unsafe struct D3D12DescriptorHeap : IDisposable } } - public int AllocateDescriptorDynamic() => AllocateDescriptorsDynamic(1); - - public int AllocateDescriptorsDynamic(int count) - { - if (count <= 0) - { - throw new ArgumentOutOfRangeException(nameof(count), "Count must be greater than zero."); - } - - // NOTE: In dynamic allocation, we use arena-style allocation without freeing. - // We reset the Offset at the beginning of each frame instead. - - lock (_lock) - { - var baseIndex = _currentDynamicOffset + _dynamicHeapStart; - _currentDynamicOffset += count; - - var requiredSize = baseIndex + count; - if (requiredSize > NumDescriptors) - { - if (!Grow(requiredSize)) - { - Debug.Assert(false, "ERROR: Failed to grow a descriptor heap!"); - return _INVALID_DESCRIPTOR_INDEX; - } - } - - NumAllocatedDescriptors += count; - return baseIndex; - } - } - public void ReleaseDescriptor(int index) => ReleaseDescriptors(index, 1); public void ReleaseDescriptors(int baseIndex, int count = 1) @@ -174,24 +141,18 @@ internal unsafe struct D3D12DescriptorHeap : IDisposable return; } - if (baseIndex >= _dynamicHeapStart) - { - // Dynamic allocations are not released individually. - return; - } - lock (_lock) { for (var index = baseIndex; index < baseIndex + count; index++) { #if DEBUG || GHOST_EDITOR - if (!_allocatedDescriptors[index]) + if (!_allocatedDescriptors.IsSet(index)) { Debug.WriteLine("Error: Attempted to release an un-allocated descriptor"); } #endif - _allocatedDescriptors[index] = false; + _allocatedDescriptors.ClearBit(index); } NumAllocatedDescriptors -= count; @@ -203,14 +164,6 @@ internal unsafe struct D3D12DescriptorHeap : IDisposable } } - public void ResetDynamicHeap() - { - lock (_lock) - { - _currentDynamicOffset = 0; - } - } - public readonly D3D12_CPU_DESCRIPTOR_HANDLE GetCpuHandle(int index) { if (index < 0 || index >= NumDescriptors) @@ -251,19 +204,6 @@ internal unsafe struct D3D12DescriptorHeap : IDisposable return _startGpuHandleShaderVisible.Offset(index, Stride); } - public int CopyToPersistentHeap(int index, int count = 1) - { - if (index < _dynamicHeapStart) - { - return index; - } - - var newLocation = AllocateDescriptors(count); - _device.NativeDevice.Get()->CopyDescriptorsSimple((uint)count, GetCpuHandle(index), GetCpuHandle(newLocation), HeapType); - - return newLocation; - } - public readonly void CopyToShaderVisibleHeap(int index, int count = 1) { _device.NativeDevice.Get()->CopyDescriptorsSimple((uint)count, GetCpuHandleShaderVisible(index), GetCpuHandle(index), HeapType); @@ -296,7 +236,7 @@ internal unsafe struct D3D12DescriptorHeap : IDisposable if (!_allocatedDescriptors.IsCreated) { - _allocatedDescriptors = new UnsafeArray(numDescriptors, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent, Misaki.HighPerformance.LowLevel.Buffer.AllocationOption.Clear); + _allocatedDescriptors = new UnsafeBitSet(numDescriptors, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent, Misaki.HighPerformance.LowLevel.Buffer.AllocationOption.Clear); } else { diff --git a/Ghost.Graphics/D3D12/D3D12GraphicsEngine.cs b/Ghost.Graphics/D3D12/D3D12GraphicsEngine.cs index 1b4538a..edb40e2 100644 --- a/Ghost.Graphics/D3D12/D3D12GraphicsEngine.cs +++ b/Ghost.Graphics/D3D12/D3D12GraphicsEngine.cs @@ -128,9 +128,6 @@ internal class D3D12GraphicsEngine : IGraphicsEngine } _resourceAllocator.ReleaseTempResources(); - _descriptorAllocator.ResetCbvSrvUavDynamicHeap(); - _descriptorAllocator.ResetDSVDynamicHeap(); - _descriptorAllocator.ResetRTVDynamicHeap(); return r; } @@ -142,6 +139,11 @@ internal class D3D12GraphicsEngine : IGraphicsEngine return; } + foreach (var renderer in _renderers) + { + renderer.Dispose(); + } + _resourceAllocator.Dispose(); _pipelineLibrary.Dispose(); _resourceDatabase.Dispose(); diff --git a/Ghost.Graphics/D3D12/D3D12PipelineLibrary.cs b/Ghost.Graphics/D3D12/D3D12PipelineLibrary.cs index 26d7f85..626390a 100644 --- a/Ghost.Graphics/D3D12/D3D12PipelineLibrary.cs +++ b/Ghost.Graphics/D3D12/D3D12PipelineLibrary.cs @@ -59,35 +59,6 @@ internal unsafe class D3D12PipelineLibrary : IPipelineLibrary // NOTE: Since we are targeting SM 6.6, we can use ResourceDescriptorHeap and SamplerDescriptorHeap directly without needing to set up viewGroup tables. var rootParameters = stackalloc D3D12_ROOT_PARAMETER1[RootSignatureLayout.ROOT_PARAMETER_COUNT]; -#if false - rootParameters[0] = new D3D12_ROOT_PARAMETER1 - { - ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV, - ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL, - Descriptor = new D3D12_ROOT_DESCRIPTOR1(RootSignatureLayout.GLOBAL_BUFFER_SLOT, 0), // b0 - }; - - rootParameters[1] = new D3D12_ROOT_PARAMETER1 - { - ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV, - ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL, - Descriptor = new D3D12_ROOT_DESCRIPTOR1(RootSignatureLayout.PER_VIEW_BUFFER_SLOT, 0), // b1 - }; - - rootParameters[2] = new D3D12_ROOT_PARAMETER1 - { - ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV, - ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL, - Descriptor = new D3D12_ROOT_DESCRIPTOR1(RootSignatureLayout.PER_OBJECT_BUFFER_SLOT, 0), // b2 - }; - - rootParameters[3] = new D3D12_ROOT_PARAMETER1 - { - ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV, - ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL, - Descriptor = new D3D12_ROOT_DESCRIPTOR1(RootSignatureLayout.PER_MATERIAL_BUFFER_SLOT, 0), // b3 - }; -#else rootParameters[0] = new D3D12_ROOT_PARAMETER1 { ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS, @@ -99,7 +70,7 @@ internal unsafe class D3D12PipelineLibrary : IPipelineLibrary Num32BitValues = 4 // Global, View, Object, Material indices } }; -#endif + var rootSignatureDesc = new D3D12_ROOT_SIGNATURE_DESC1 { NumParameters = RootSignatureLayout.ROOT_PARAMETER_COUNT, @@ -175,9 +146,14 @@ internal unsafe class D3D12PipelineLibrary : IPipelineLibrary private static Result ValidateReflectionData(ShaderReflectionData reflectionData) { - if (reflectionData.ResourcesBindings.Count != RootSignatureLayout.ROOT_PARAMETER_COUNT) + if (reflectionData.ResourcesBindings.Count > RootSignatureLayout.ROOT_PARAMETER_COUNT) { - return Result.Failure($"Shader must use all {RootSignatureLayout.ROOT_PARAMETER_COUNT} constant buffer slots defined in the root signature."); + return Result.Failure($"Shader uses more root parameters than supported ({RootSignatureLayout.ROOT_PARAMETER_COUNT})."); + } + + if (reflectionData.ResourcesBindings.Count == 0) + { + return Result.Success(default(CBufferInfo)); } var rootConstant = reflectionData.ResourcesBindings[0]; @@ -275,11 +251,11 @@ internal unsafe class D3D12PipelineLibrary : IPipelineLibrary if (!_pipelineCache.ContainsKey(pipelineKey)) { - var result = ValidatePassReflectionData(in compiled); - if (result.IsFailure) - { - return Result.Failure(result.Message); - } + //var result = ValidatePassReflectionData(in compiled); + //if (result.IsFailure) + //{ + // return Result.Failure(result.Message); + //} var desc = new D3DX12_MESH_SHADER_PIPELINE_STATE_DESC { diff --git a/Ghost.Graphics/D3D12/D3D12Renderer.cs b/Ghost.Graphics/D3D12/D3D12Renderer.cs index 7d9b454..1a9bbb3 100644 --- a/Ghost.Graphics/D3D12/D3D12Renderer.cs +++ b/Ghost.Graphics/D3D12/D3D12Renderer.cs @@ -3,6 +3,7 @@ using Ghost.Graphics.RHI; using Ghost.Graphics.Core; using Ghost.Graphics.RenderPasses; using Ghost.Graphics.Contracts; +using Ghost.Graphics.RenderGraphModule; namespace Ghost.Graphics.D3D12; @@ -15,6 +16,7 @@ internal class D3D12Renderer : IRenderer private readonly D3D12ResourceDatabase _resourceDatabase; private readonly ICommandBuffer _commandBuffer; + private readonly RenderGraph _renderGraph; private uint _frameIndex; private bool _disposed; @@ -35,6 +37,7 @@ internal class D3D12Renderer : IRenderer _resourceDatabase = resourceDatabase; _commandBuffer = _graphicsEngine.CreateCommandBuffer(CommandBufferType.Graphics); + _renderGraph = new RenderGraph(_graphicsEngine); // NOTE: Testing only. _pass = new(); @@ -61,7 +64,7 @@ internal class D3D12Renderer : IRenderer _commandBuffer.Begin(commandAllocator); RenderOutput.BeginRender(_commandBuffer); - // NOTE: Temperary solution: render directly to the swap chain back buffer if available. + // NOTE: Temporary solution: render directly to the swap chain back buffer if available. // HACK: This is hard coded for testing purposes only. var error = RenderScene(target, RenderOutput.Viewport, RenderOutput.Scissor); @@ -87,30 +90,6 @@ internal class D3D12Renderer : IRenderer // TODO: A proper render graph integration. private ErrorStatus RenderScene(Handle target, ViewportDesc viewport, RectDesc rect) { - var clearColor = new Color128 { r = 1.0f, g = 0.0f, b = 1.0f, a = 1.0f }; - - Span rtDesc = - [ - new PassRenderTargetDesc - { - Texture = target, - ClearColor = clearColor, - LoadOp = AttachmentLoadOp.Clear, - StoreOp = AttachmentStoreOp.Store, - }, - ]; - - var depthDesc = new PassDepthStencilDesc - { - Texture = Handle.Invalid, - ClearDepth = 1.0f, - ClearStencil = 0, - DepthLoadOp = AttachmentLoadOp.Clear, - StencilLoadOp = AttachmentLoadOp.Clear, - DepthStoreOp = AttachmentStoreOp.Store, - StencilStoreOp = AttachmentStoreOp.Store, - }; - // NOTE: Testing only. var ctx = new RenderingContext(_graphicsEngine, _commandBuffer); if (_frameIndex == 0) @@ -118,14 +97,23 @@ internal class D3D12Renderer : IRenderer _pass.Initialize(ref ctx); } - _commandBuffer.BeginRenderPass(rtDesc, depthDesc, false); + //_commandBuffer.BeginRenderPass(rtDesc, depthDesc, false); _commandBuffer.SetViewport(viewport); _commandBuffer.SetScissorRect(rect); - // NOTE: Testing only. - _pass.Execute(ref ctx); + _renderGraph.Reset(); - _commandBuffer.EndRenderPass(); + var backBuffer = _renderGraph.ImportTexture(target, "Back Buffer"); + _pass.Build(_renderGraph, backBuffer); + + // Create view state from viewport + var viewState = new ViewState((uint)viewport.Width, (uint)viewport.Height); + + // Compile with view state + _renderGraph.Compile(in viewState); + _renderGraph.Execute(_commandBuffer); + + //_commandBuffer.EndRenderPass(); _frameIndex++; return ErrorStatus.None; @@ -140,6 +128,7 @@ internal class D3D12Renderer : IRenderer // NOTE: Testing only. _pass.Cleanup(_resourceDatabase); + _renderGraph.Dispose(); _commandBuffer.Dispose(); diff --git a/Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs b/Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs index 9e6fb99..d1cc9f3 100644 --- a/Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs +++ b/Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs @@ -9,6 +9,7 @@ using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Xml.Linq; using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -623,9 +624,9 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Handle TrackResource(D3D12MA_Allocation* allocation, D3D12_RESOURCE_STATES state, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string name, bool isTemp) + private Handle TrackAllocation(D3D12MA_Allocation* allocation, D3D12_RESOURCE_STATES state, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string name, bool isTemp) { - var handle = _resourceDatabase.AddAllocation(allocation, _fenceSynchronizer.CPUFenceValue, D3D12Utility.ToResourceState(state), resourceDescriptor, desc, name); + var handle = _resourceDatabase.AddAllocation(allocation, _fenceSynchronizer.CPUFenceValue, state.ToResourceState(), resourceDescriptor, desc, name); if (isTemp) { @@ -635,12 +636,11 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator return handle; } - private HRESULT CreateResource(D3D12MA_ALLOCATION_DESC* pAllocationDesc, D3D12_RESOURCE_DESC* pResourceDesc, D3D12_RESOURCE_STATES initialState, CreationOptions options, void** ppv) + private HRESULT CreateResource(D3D12MA_ALLOCATION_DESC* pAllocationDesc, D3D12_RESOURCE_DESC* pResourceDesc, D3D12_RESOURCE_STATES initialState, CreationOptions options, Guid* riid, void** ppv) { var hr = S.S_OK; - var iid = IID.IID_NULL; - if (options.AllocationType == ResourceAllocationType.RenderGraphTransient) + if (options.AllocationType == ResourceAllocationType.Suballocation) { // pAllocation should be the render graph Heap. ppvResource should be the out resource. var result = _resourceDatabase.GetResourceRecord(options.Heap); @@ -649,11 +649,12 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator return E.E_NOTFOUND; } - hr = _d3d12MA.Get()->CreateAliasingResource(result.Value.resource.allocation.Get(), options.Offset, pResourceDesc, initialState, null, &iid, ppv); + hr = _d3d12MA.Get()->CreateAliasingResource(result.Value.resource.allocation.Get(), options.Offset, pResourceDesc, initialState, null, riid, ppv); } else { - hr = _d3d12MA.Get()->CreateResource(pAllocationDesc, pResourceDesc, initialState, null, (D3D12MA_Allocation**)ppv, &iid, null); + var nuliid = IID.IID_NULL; + hr = _d3d12MA.Get()->CreateResource(pAllocationDesc, pResourceDesc, initialState, null, (D3D12MA_Allocation**)ppv, &nuliid, null); } return hr; @@ -695,7 +696,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator return Handle.Invalid; } - return TrackResource(alloc, D3D12_RESOURCE_STATE_COMMON, ResourceViewGroup.Invalid, default, name, false); + return TrackAllocation(alloc, D3D12_RESOURCE_STATE_COMMON, ResourceViewGroup.Invalid, default, name, false); } public Handle CreateTexture(ref readonly TextureDesc desc, string name, CreationOptions options = default) @@ -756,10 +757,26 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator }; var initialState = DetermineInitialTextureState(desc.Usage); - + var isSubAllocation = options.AllocationType == ResourceAllocationType.Suballocation; D3D12MA_Allocation* pAllocation = default; - if (CreateResource(&allocationDesc, &resourceDesc, initialState, options, (void**)&pAllocation).FAILED) + ID3D12Resource* pResource = default; + HRESULT hr; + + if (isSubAllocation) { + hr = CreateResource(&allocationDesc, &resourceDesc, initialState, options, __uuidof(pResource), (void**)&pResource); + } + else + { + hr = CreateResource(&allocationDesc, &resourceDesc, initialState, options, null, (void**)&pAllocation); + pResource = pAllocation->GetResource(); + } + + if (hr.FAILED) + { +#if DEBUG + Marshal.ThrowExceptionForHR(hr); +#endif return Handle.Invalid; } @@ -767,46 +784,55 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator var resourceDescriptor = ResourceViewGroup.Invalid; if (desc.Usage.HasFlag(TextureUsage.ShaderResource)) { - resourceDescriptor.srv = _descriptorAllocator.AllocateCbvSrvUav(isTemp); - // TODO: Maybe use non-shader-visible descriptor first then batch copy to shader-visible Heap later? - var cpuHandle = _descriptorAllocator.GetCpuHandleShaderVisible(resourceDescriptor.srv); + resourceDescriptor.srv = _descriptorAllocator.AllocateCbvSrvUav(); + var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.srv); var isCubeMap = desc.Dimension == TextureDimension.TextureCube || desc.Dimension == TextureDimension.TextureCubeArray; - var srvDesc = CreateTextureSrvDesc(pAllocation->GetResource(), mipLevels, desc.Slice, isCubeMap); + var srvDesc = CreateTextureSrvDesc(pResource, mipLevels, desc.Slice, isCubeMap); - _device.NativeDevice.Get()->CreateShaderResourceView(pAllocation->GetResource(), &srvDesc, cpuHandle); + _device.NativeDevice.Get()->CreateShaderResourceView(pResource, &srvDesc, cpuHandle); + _descriptorAllocator.CopyToShaderVisible(resourceDescriptor.srv); } if (desc.Usage.HasFlag(TextureUsage.RenderTarget)) { - resourceDescriptor.rtv = _descriptorAllocator.AllocateRTV(isTemp); + resourceDescriptor.rtv = _descriptorAllocator.AllocateRTV(); var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.rtv); - var rtvDesc = CreateRtvDesc(pAllocation->GetResource()); + var rtvDesc = CreateRtvDesc(pResource); - _device.NativeDevice.Get()->CreateRenderTargetView(pAllocation->GetResource(), &rtvDesc, cpuHandle); + _device.NativeDevice.Get()->CreateRenderTargetView(pResource, &rtvDesc, cpuHandle); } if (desc.Usage.HasFlag(TextureUsage.DepthStencil)) { - resourceDescriptor.dsv = _descriptorAllocator.AllocateDSV(isTemp); + resourceDescriptor.dsv = _descriptorAllocator.AllocateDSV(); var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.dsv); - var dsvDesc = CreateDsvDesc(pAllocation->GetResource()); + var dsvDesc = CreateDsvDesc(pResource); - _device.NativeDevice.Get()->CreateDepthStencilView(pAllocation->GetResource(), &dsvDesc, cpuHandle); + _device.NativeDevice.Get()->CreateDepthStencilView(pResource, &dsvDesc, cpuHandle); } if (desc.Usage.HasFlag(TextureUsage.UnorderedAccess)) { - resourceDescriptor.uav = _descriptorAllocator.AllocateCbvSrvUav(isTemp); - var cpuHandle = _descriptorAllocator.GetCpuHandleShaderVisible(resourceDescriptor.uav); - var uavDesc = CreateTextureUavDesc(pAllocation->GetResource()); + resourceDescriptor.uav = _descriptorAllocator.AllocateCbvSrvUav(); + var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.uav); + var uavDesc = CreateTextureUavDesc(pResource); - _device.NativeDevice.Get()->CreateUnorderedAccessView(pAllocation->GetResource(), null, &uavDesc, cpuHandle); + _device.NativeDevice.Get()->CreateUnorderedAccessView(pResource, null, &uavDesc, cpuHandle); + _descriptorAllocator.CopyToShaderVisible(resourceDescriptor.uav); } - var handle = TrackResource(pAllocation, initialState, resourceDescriptor, ResourceDesc.Texture(desc), name, isTemp); + Handle resource; + if (isSubAllocation) + { + resource = _resourceDatabase.ImportExternalResource(pResource, initialState.ToResourceState(), resourceDescriptor, name); + } + else + { + resource = TrackAllocation(pAllocation, initialState, resourceDescriptor, ResourceDesc.Texture(desc), name, isTemp); + } - return handle.AsTexture(); + return resource.AsTexture(); } public Handle CreateRenderTarget(ref readonly RenderTargetDesc desc, string name, CreationOptions options = default) @@ -839,21 +865,33 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator }; var initialState = DetermineInitialBufferState(desc.Usage, desc.MemoryType); - + var isSubAllocation = options.Heap.IsValid; D3D12MA_Allocation* pAllocation = default; - if (CreateResource(&allocationDesc, &resourceDesc, initialState, options, (void**)&pAllocation).FAILED) + ID3D12Resource* pResource = default; + HRESULT hr; + + if (isSubAllocation) + { + hr = CreateResource(&allocationDesc, &resourceDesc, initialState, options, __uuidof(pResource), (void**)&pResource); + } + else + { + hr = CreateResource(&allocationDesc, &resourceDesc, initialState, options, null, (void**)&pAllocation); + pResource = pAllocation->GetResource(); + } + + if (hr.FAILED) { return Handle.Invalid; } var isTemp = options.AllocationType == ResourceAllocationType.Temporary; var resourceDescriptor = ResourceViewGroup.Invalid; - var pResource = pAllocation->GetResource(); if (desc.Usage.HasFlag(BufferUsage.Constant)) { - resourceDescriptor.cbv = _descriptorAllocator.AllocateCbvSrvUav(isTemp); - var cpuHandle = _descriptorAllocator.GetCpuHandleShaderVisible(resourceDescriptor.cbv); + resourceDescriptor.cbv = _descriptorAllocator.AllocateCbvSrvUav(); + var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.cbv); var cbvDesc = new D3D12_CONSTANT_BUFFER_VIEW_DESC { BufferLocation = pResource->GetGPUVirtualAddress(), @@ -861,28 +899,40 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator }; _device.NativeDevice.Get()->CreateConstantBufferView(&cbvDesc, cpuHandle); + _descriptorAllocator.CopyToShaderVisible(resourceDescriptor.cbv); } if (desc.Usage.HasFlag(BufferUsage.ShaderResource)) { - resourceDescriptor.srv = _descriptorAllocator.AllocateCbvSrvUav(isTemp); - var cpuHandle = _descriptorAllocator.GetCpuHandleShaderVisible(resourceDescriptor.srv); - var srvDesc = CreateBufferSrvDesc(pAllocation->GetResource(), desc.Stride, isRaw); + resourceDescriptor.srv = _descriptorAllocator.AllocateCbvSrvUav(); + var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.srv); + var srvDesc = CreateBufferSrvDesc(pResource, desc.Stride, isRaw); _device.NativeDevice.Get()->CreateShaderResourceView(pResource, &srvDesc, cpuHandle); + _descriptorAllocator.CopyToShaderVisible(resourceDescriptor.srv); } if (desc.Usage.HasFlag(BufferUsage.UnorderedAccess)) { - resourceDescriptor.uav = _descriptorAllocator.AllocateCbvSrvUav(isTemp); - var cpuHandle = _descriptorAllocator.GetCpuHandleShaderVisible(resourceDescriptor.uav); - var uavDesc = CreateBufferUavDesc(pAllocation->GetResource(), desc.Stride, isRaw); + resourceDescriptor.uav = _descriptorAllocator.AllocateCbvSrvUav(); + var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.uav); + var uavDesc = CreateBufferUavDesc(pResource, desc.Stride, isRaw); _device.NativeDevice.Get()->CreateUnorderedAccessView(pResource, null, &uavDesc, cpuHandle); + _descriptorAllocator.CopyToShaderVisible(resourceDescriptor.uav); } - var handle = TrackResource(pAllocation, initialState, resourceDescriptor, ResourceDesc.Buffer(desc), name, isTemp); - return handle.AsGraphicsBuffer(); + Handle resource; + if (isSubAllocation) + { + resource = _resourceDatabase.ImportExternalResource(pResource, initialState.ToResourceState(), resourceDescriptor, name); + } + else + { + resource = TrackAllocation(pAllocation, initialState, resourceDescriptor, ResourceDesc.Buffer(desc), name, isTemp); + } + + return resource.AsGraphicsBuffer(); } public Handle CreateTempUploadBuffer(ulong sizeInBytes, out ulong offset) @@ -935,8 +985,9 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator }; var samplerDescriptor = _descriptorAllocator.AllocateSampler(); - var cpuHandle = _descriptorAllocator.GetCpuHandleShaderVisible(samplerDescriptor); + var cpuHandle = _descriptorAllocator.GetCpuHandle(samplerDescriptor); _device.NativeDevice.Get()->CreateSampler(&samplerDesc, cpuHandle); + _descriptorAllocator.CopyToShaderVisible(samplerDescriptor); return _resourceDatabase.CreateSampler(in desc, samplerDescriptor.Value); } @@ -1013,15 +1064,15 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator while (_tempResources.Count > 0) { var handle = _tempResources.Peek(); - ref var info = ref _resourceDatabase.GetResourceRecord(handle, out var exist); - if (!exist || !info.Allocated) + var r = _resourceDatabase.GetResourceRecord(handle); + if (r.IsFailure || !r.Value.Allocated) { // Resource already released or invalid, just dequeue _tempResources.Dequeue(); continue; } - if (info.cpuFenceValue > _fenceSynchronizer.GPUFenceValue) + if (r.Value.cpuFenceValue > _fenceSynchronizer.GPUFenceValue) { // Resource still in use by GPU, stop processing. // Since resources are enqueued in order, we can break here. diff --git a/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs b/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs index d56c1d4..9471de7 100644 --- a/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs +++ b/Ghost.Graphics/D3D12/D3D12ResourceDatabase.cs @@ -133,6 +133,13 @@ internal class D3D12ResourceDatabase : IResourceDatabase public unsafe Handle ImportExternalResource(ID3D12Resource* pResource, ResourceState initialState, ResourceViewGroup viewGroup, string? name = null) { ObjectDisposedException.ThrowIf(_disposed, this); + if (pResource == null) + { +#if DEBUG + System.Diagnostics.Debugger.Break(); +#endif + return Handle.Invalid; + } var id = _resources.Add(new ResourceRecord(pResource, initialState, viewGroup), out var generation); var handle = new Handle(id, generation); @@ -151,6 +158,13 @@ internal class D3D12ResourceDatabase : IResourceDatabase public unsafe Handle AddAllocation(D3D12MA_Allocation* allocation, uint cpuFenceValue, ResourceState initialState, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null) { ObjectDisposedException.ThrowIf(_disposed, this); + if (allocation == null) + { +#if DEBUG + System.Diagnostics.Debugger.Break(); +#endif + return Handle.Invalid; + } var id = _resources.Add(new ResourceRecord(allocation, cpuFenceValue, initialState, resourceDescriptor, desc), out var generation); var handle = new Handle(id, generation); @@ -159,7 +173,11 @@ internal class D3D12ResourceDatabase : IResourceDatabase if (!string.IsNullOrEmpty(name)) { allocation->SetName(name); - allocation->GetResource()->SetName(name); + var pResource = allocation->GetResource(); + if (pResource != null) + { + pResource->SetName(name); + } _resourceName[handle] = name; } #endif @@ -186,18 +204,10 @@ internal class D3D12ResourceDatabase : IResourceDatabase return RefResult.Success(ref info); } - public ref ResourceRecord GetResourceRecord(Handle handle, out bool exist) - { - ObjectDisposedException.ThrowIf(_disposed, this); - return ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out exist); - } - public SharedPtr GetResource(Handle handle) { - ObjectDisposedException.ThrowIf(_disposed, this); - var r = GetResourceRecord(handle); - if (r.Error != ErrorStatus.None) + if (r.IsFailure) { return null; } @@ -207,10 +217,8 @@ internal class D3D12ResourceDatabase : IResourceDatabase public Result GetResourceState(Handle handle) { - ObjectDisposedException.ThrowIf(_disposed, this); - var r = GetResourceRecord(handle); - if (!r) + if (r.IsFailure) { return r.Error; } @@ -218,25 +226,22 @@ internal class D3D12ResourceDatabase : IResourceDatabase return r.Value.state; } - public void SetResourceState(Handle handle, ResourceState state) + public ErrorStatus SetResourceState(Handle handle, ResourceState state) { - ObjectDisposedException.ThrowIf(_disposed, this); - - ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); - if (!exist || !info.Allocated) + var r = GetResourceRecord(handle); + if (r.IsFailure) { - throw new KeyNotFoundException($"Resource with handle {handle} not found."); + return r.Error; } - info.state = state; + r.Value.state = state; + return ErrorStatus.None; } public Result GetResourceDescription(Handle handle) { - ObjectDisposedException.ThrowIf(_disposed, this); - var r = GetResourceRecord(handle); - if (!r) + if (r.IsFailure) { return r.Error; } @@ -246,15 +251,13 @@ internal class D3D12ResourceDatabase : IResourceDatabase public uint GetBindlessIndex(Handle handle) { - ObjectDisposedException.ThrowIf(_disposed, this); - - ref var info = ref GetResourceRecord(handle, out var exist); - if (!exist || !info.Allocated) + var r = GetResourceRecord(handle); + if (r.IsFailure || !r.Value.Allocated) { return ~0u; } - return (uint)info.viewGroup.srv.Value; + return (uint)r.Value.viewGroup.srv.Value; } public string? GetResourceName(Handle handle) @@ -329,17 +332,15 @@ internal class D3D12ResourceDatabase : IResourceDatabase return _meshes.Contains(handle.ID, handle.Generation); } - public ref Mesh GetMeshReference(Handle handle) + public RefResult GetMeshReference(Handle handle) { - ObjectDisposedException.ThrowIf(_disposed, this); - ref var mesh = ref _meshes.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); if (!exist) { - throw new ArgumentOutOfRangeException(nameof(handle), $"Mesh {handle} is invalid."); + return ErrorStatus.NotFound; } - return ref mesh; + return RefResult.Success(ref mesh); } public void ReleaseMesh(Handle handle) @@ -370,17 +371,15 @@ internal class D3D12ResourceDatabase : IResourceDatabase return _materials.Contains(handle.ID, handle.Generation); } - public ref Material GetMaterialReference(Handle handle) + public RefResult GetMaterialReference(Handle handle) { - ObjectDisposedException.ThrowIf(_disposed, this); - ref var material = ref _materials.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); if (!exist) { - throw new ArgumentOutOfRangeException(nameof(handle), $"Material handle {handle} is invalid."); + return ErrorStatus.NotFound; } - return ref material; + return RefResult.Success(ref material); } public void ReleaseMaterial(Handle handle) @@ -412,16 +411,14 @@ internal class D3D12ResourceDatabase : IResourceDatabase return id.Value >= 0 && id.Value < _shaders.Count; } - public ref Shader GetShaderReference(Identifier id) + public RefResult GetShaderReference(Identifier id) { - ObjectDisposedException.ThrowIf(_disposed, this); - if (!HasShader(id)) { - throw new ArgumentOutOfRangeException(nameof(id), $"Shader id {id} is invalid."); + return ErrorStatus.NotFound; } - return ref _shaders[id.Value]; + return RefResult.Success(ref _shaders[id.Value]); } public void ReleaseShader(Identifier id) diff --git a/Ghost.Graphics/D3D12/Utilities/D3D12PipelineResource.cs b/Ghost.Graphics/D3D12/Utilities/D3D12PipelineResource.cs index 4a3444f..af52971 100644 --- a/Ghost.Graphics/D3D12/Utilities/D3D12PipelineResource.cs +++ b/Ghost.Graphics/D3D12/Utilities/D3D12PipelineResource.cs @@ -7,11 +7,11 @@ namespace Ghost.Graphics.D3D12.Utilities; internal unsafe static class D3D12PipelineResource { private readonly static D3D12_INPUT_ELEMENT_DESC[] s_inputElementDescs = [ - new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.position.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 0u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, - new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.normal.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 16u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, - new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.tangent.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 32u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, - new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.uv.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 48u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, - new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.color.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 64u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, + new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Position.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 0u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, + new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Normal.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 16u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, + new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Tangent.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 32u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, + new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Uv.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 48u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, + new D3D12_INPUT_ELEMENT_DESC{ SemanticName = (sbyte*)Vertex.Semantic.Color.GetUnsafePointer(), SemanticIndex = 0u, Format = Vertex.Semantic.ALIGNED_FORMAT, InputSlot = 0u, AlignedByteOffset = 64u, InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, InstanceDataStepRate = 0 }, ]; public const DXGI_FORMAT SWAP_CHAIN_BACK_BUFFER_FORMAT = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; diff --git a/Ghost.Graphics/D3D12/Utilities/D3D12Utility.cs b/Ghost.Graphics/D3D12/Utilities/D3D12Utility.cs index 2637a82..f26d60f 100644 --- a/Ghost.Graphics/D3D12/Utilities/D3D12Utility.cs +++ b/Ghost.Graphics/D3D12/Utilities/D3D12Utility.cs @@ -42,7 +42,7 @@ internal unsafe static class D3D12Utility D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_TEXTURE1D => TextureDimension.Texture2D, D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_TEXTURE2D => TextureDimension.Texture2D, D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_TEXTURE3D => TextureDimension.Texture3D, - _ => TextureDimension.Unknown, + _ => throw new NotSupportedException($"Resource dimension {dimension} is not supported."), }; } @@ -71,7 +71,7 @@ internal unsafe static class D3D12Utility DXGI_FORMAT_R32G32B32A32_FLOAT => TextureFormat.R32G32B32A32_Float, DXGI_FORMAT_D24_UNORM_S8_UINT => TextureFormat.D24_UNorm_S8_UInt, DXGI_FORMAT_D32_FLOAT => TextureFormat.D32_Float, - _ => TextureFormat.Unknown, + _ => throw new NotSupportedException($"DXGI format {format} is not supported.") }; } diff --git a/Ghost.Graphics/Ghost.Graphics.csproj b/Ghost.Graphics/Ghost.Graphics.csproj index 7d005e5..bd12bd6 100644 --- a/Ghost.Graphics/Ghost.Graphics.csproj +++ b/Ghost.Graphics/Ghost.Graphics.csproj @@ -16,12 +16,6 @@ True - - - - - - diff --git a/Ghost.Graphics/RHI/Common.cs b/Ghost.Graphics/RHI/Common.cs index 7405bb6..825de90 100644 --- a/Ghost.Graphics/RHI/Common.cs +++ b/Ghost.Graphics/RHI/Common.cs @@ -294,28 +294,41 @@ public struct PassDepthStencilDesc } +[StructLayout(LayoutKind.Explicit)] public struct BarrierDesc { - public Handle Resource + public struct barrierdesc_transition { - get; set; + public Handle resource; + public ResourceState stateBefore; + public ResourceState stateAfter; } - public ResourceState StateBefore + public struct barrierdesc_aliasing { - get; set; + public Handle resourceBefore; + public Handle resourceAfter; } - public ResourceState StateAfter + public struct barrierdesc_uav { - get; set; + public Handle resource; } + + [FieldOffset(0)] + public BarrierType type; + [FieldOffset(4)] + public barrierdesc_transition transition; + [FieldOffset(4)] + public barrierdesc_aliasing aliasing; + [FieldOffset(4)] + public barrierdesc_uav uav; } public struct ResourceDesc { [StructLayout(LayoutKind.Explicit)] - private struct resource_union + internal struct resource_union { [FieldOffset(0)] public TextureDesc textureDescription; @@ -323,7 +336,7 @@ public struct ResourceDesc public BufferDesc bufferDescription; } - private resource_union _desc; + internal resource_union _desc; public TextureDesc TextureDescription { @@ -774,9 +787,17 @@ public enum SwapChainTargetType } -[Flags] -public enum ResourceState +public enum BarrierType : int { + Transition, + Aliasing, + UAV +} + +[Flags] +public enum ResourceState : int +{ + Auto = -1, Common = 0, VertexAndConstantBuffer = 1 << 0, IndexBuffer = 1 << 1, diff --git a/Ghost.Graphics/RHI/ICommandBuffer.cs b/Ghost.Graphics/RHI/ICommandBuffer.cs index ed4f6b3..ec08cd3 100644 --- a/Ghost.Graphics/RHI/ICommandBuffer.cs +++ b/Ghost.Graphics/RHI/ICommandBuffer.cs @@ -66,6 +66,10 @@ public interface ICommandBuffer : IDisposable /// A handle to the texture to be used as the depth Target. Specify a invalid handle if no depth Target is required. void SetRenderTargets(ReadOnlySpan> renderTargets, Handle depthTarget); + void ClearRenderTargetView(Handle renderTarget, Color128 clearColor); + + void ClearDepthStencilView(Handle depthStencil, bool inlcudeDepth, bool includeStencil, float clearDepth = 1.0f, byte clearStencil = 0); + /// /// Begins a render pass with the specified render Target /// @@ -79,8 +83,10 @@ public interface ICommandBuffer : IDisposable /// void EndRenderPass(); + // TODO: Enhanced barriers. + /// - /// Inserts multiple resource barriers for state transitions. + /// Inserts multiple resource barriers. /// /// Resource barrier descriptions void ResourceBarrier(ReadOnlySpan barrierDescs); @@ -91,14 +97,21 @@ public interface ICommandBuffer : IDisposable /// A handle to the GPU resource to transition. /// The current state of the resource before the transition. /// The desired state of the resource after the transition. - void ResourceBarrier(Handle resource, ResourceState stateBefore, ResourceState stateAfter); + void TransitionBarrier(Handle resource, ResourceState stateBefore, ResourceState stateAfter); /// /// Inserts a resource barrier for state transitions. The current state is tracked internally. /// /// A handle to the GPU resource to transition. /// The desired state of the resource after the transition. - void ResourceBarrier(Handle resource, ResourceState stateAfter); + void TransitionBarrier(Handle resource, ResourceState stateAfter); + + /// + /// Inserts a barrier to ensure correct aliasing transitions between two GPU resources. + /// + /// A handle to the GPU resource representing the state before the aliasing transition + /// A handle to the GPU resource representing the state after the aliasing transition + void AliasBarrier(Handle resourceBefore, Handle resourceAfter); /// /// Sets the pipeline state object @@ -207,8 +220,8 @@ public interface ICommandBuffer : IDisposable /// /// Copies a specified number of bytes from the source graphics buffer to the destination graphics buffer. /// - /// The handle to the destination graphics buffer where data will be written. Cannot be null. - /// The handle to the source graphics buffer from which data will be read. Cannot be null. + /// The handle to the destination graphics buffer where data will be written. + /// The handle to the source graphics buffer from which data will be read. /// The byte Offset in the destination buffer at which to begin writing. Must be zero or greater. /// The byte Offset in the source buffer at which to begin reading. Must be zero or greater. /// The number of bytes to copy. If zero, copies the remaining bytes from the source buffer starting at . diff --git a/Ghost.Graphics/RHI/IPipelineLibrary.cs b/Ghost.Graphics/RHI/IPipelineLibrary.cs index 4a4f93a..3319e00 100644 --- a/Ghost.Graphics/RHI/IPipelineLibrary.cs +++ b/Ghost.Graphics/RHI/IPipelineLibrary.cs @@ -3,17 +3,6 @@ using Ghost.Graphics.Contracts; namespace Ghost.Graphics.RHI; -public interface IShaderPipeline -{ - /// - /// Pipeline space - /// - PipelineType Type - { - get; - } -} - public interface IPipelineLibrary : IDisposable { /// diff --git a/Ghost.Graphics/RHI/IResourceAllocator.cs b/Ghost.Graphics/RHI/IResourceAllocator.cs index 144ae57..1af5ada 100644 --- a/Ghost.Graphics/RHI/IResourceAllocator.cs +++ b/Ghost.Graphics/RHI/IResourceAllocator.cs @@ -9,7 +9,7 @@ public enum ResourceAllocationType { Default, Temporary, - RenderGraphTransient, + Suballocation, } public struct CreationOptions diff --git a/Ghost.Graphics/RHI/IResourceDatabase.cs b/Ghost.Graphics/RHI/IResourceDatabase.cs index 922af84..e46842a 100644 --- a/Ghost.Graphics/RHI/IResourceDatabase.cs +++ b/Ghost.Graphics/RHI/IResourceDatabase.cs @@ -46,7 +46,8 @@ public interface IResourceDatabase : IDisposable /// /// The handle that identifies the resource whose state will be updated. /// The new state to assign to the resource represented by . - void SetResourceState(Handle handle, ResourceState state); + /// An ErrorStatus indicating the success or failure of the operation. + ErrorStatus SetResourceState(Handle handle, ResourceState state); /// /// Retrieves the description of a GPU resource associated with the specified handle. @@ -113,8 +114,8 @@ public interface IResourceDatabase : IDisposable /// Returns a reference to the mesh associated with the specified handle. /// /// The handle of the mesh to retrieve. Must refer to a valid mesh; otherwise, the behavior is undefined. - /// A reference to the mesh corresponding to the specified handle. - ref Mesh GetMeshReference(Handle handle); + /// A result containing a reference to the mesh corresponding to the specified handle, or an error status if the handle is invalid. + RefResult GetMeshReference(Handle handle); /// /// Releases the mesh resource associated with the specified handle, freeing any resources held by it. Includes both CPU and GPU resources. @@ -140,8 +141,8 @@ public interface IResourceDatabase : IDisposable /// Gets a reference to the material associated with the specified handle. /// /// The handle of the material to retrieve. Must refer to a valid material. - /// A reference to the material corresponding to the specified handle. - ref Material GetMaterialReference(Handle handle); + /// A result containing a reference to the material corresponding to the specified handle, or an error status if the handle is invalid. + RefResult GetMaterialReference(Handle handle); /// /// Releases the material associated with the specified handle, making it available for reuse or disposal. @@ -167,8 +168,8 @@ public interface IResourceDatabase : IDisposable /// Returns a reference to the shader associated with the specified identifier. /// /// The identifier of the shader to retrieve. Must refer to a valid shader. - /// A reference to the shader corresponding to the specified identifier. - ref Shader GetShaderReference(Identifier id); + /// A result containing a reference to the shader corresponding to the specified identifier, or an error status if the identifier is invalid. + RefResult GetShaderReference(Identifier id); /// /// Releases the shader associated with the specified identifier, freeing any resources allocated to it. diff --git a/Ghost.Graphics/RHI/RHIUtility.cs b/Ghost.Graphics/RHI/RHIUtility.cs index bd42eb9..84bc454 100644 --- a/Ghost.Graphics/RHI/RHIUtility.cs +++ b/Ghost.Graphics/RHI/RHIUtility.cs @@ -9,6 +9,8 @@ namespace Ghost.Graphics.RHI; internal static class RHIUtility { + public const int MAX_RENDER_TARGETS = 8; + public static uint GetBytesPerPixel(this TextureFormat format) { return format switch diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraph.cs b/Ghost.Graphics/RenderGraphModule/RenderGraph.cs new file mode 100644 index 0000000..6901e55 --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraph.cs @@ -0,0 +1,1386 @@ +using Ghost.Core; +using Ghost.Graphics.Core; +using Ghost.Graphics.RHI; +using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Collections; +using System.IO.Hashing; +using TerraFX.Interop.Windows; + +namespace Ghost.Graphics.RenderGraphModule; + +/// +/// Main render graph class that manages resource allocation and pass execution. +/// +public sealed class RenderGraph : IDisposable +{ + private readonly IGraphicsEngine _graphicsEngine; + + private readonly RenderGraphObjectPool _objectPool; + private readonly RenderGraphResourceRegistry _resources; + + private readonly List _passes; + private readonly List _compiledPasses; + private readonly List _nativePasses; + + private readonly RenderGraphBuilder _builder; + private readonly ResourceAliasingManager _aliasingManager; + + private readonly Dictionary _resourceStates; + private readonly List _barriers; + + private readonly RenderGraphCompilationCache _compilationCache; + private readonly RenderGraphContext _context; + + private bool _compiled; + private Handle _resourceHeap; + private ViewState _currentViewState; + + public RenderGraphBlackboard Blackboard + { + get; + } + + public RenderGraph(IGraphicsEngine graphicsEngine) + { + _graphicsEngine = graphicsEngine; + + _objectPool = new RenderGraphObjectPool(); + _resources = new RenderGraphResourceRegistry(_objectPool); + + _passes = new List(32); + _compiledPasses = new List(32); + _nativePasses = new List(32); + + _builder = new RenderGraphBuilder(); + _aliasingManager = new ResourceAliasingManager(_objectPool); + + _resourceStates = new Dictionary(64); + _barriers = new List(128); + + _compilationCache = new RenderGraphCompilationCache(); + + _resourceHeap = Handle.Invalid; + _context = new RenderGraphContext( + _graphicsEngine.ResourceDatabase, + _graphicsEngine.PipelineLibrary, + _graphicsEngine.ShaderCompiler, + _resources + ); + + Blackboard = new RenderGraphBlackboard(); + } + + /// + /// Resets the render graph for a new frame. + /// Reuses existing allocations to minimize GC. + /// + public void Reset() + { + // Clear blackboard data + Blackboard.Clear(); + + // Reset resources but keep allocations + _resources.BeginFrame(); + + // Reset aliasing manager + _aliasingManager.BeginFrame(); + + // Clear resource states and barriers + _resourceStates.Clear(); + _barriers.Clear(); + + // Return passes to the pool and reset count + for (var i = 0; i < _passes.Count; i++) + { + var pass = _passes[i]; + pass.Reset(_objectPool); + } + + _passes.Clear(); + + // Clear compiled passes list + _compiledPasses.Clear(); + + // Return native passes to pool + for (var i = 0; i < _nativePasses.Count; i++) + { + _objectPool.Return(_nativePasses[i]); + } + _nativePasses.Clear(); + + _compiled = false; + } + + /// + /// Imports an external texture into the render graph. + /// + /// The external texture handle. + /// The identifier of the imported render graph texture. Invalid if import fails. + public Identifier ImportTexture(Handle texture, string name) + { + var r = _graphicsEngine.ResourceDatabase.GetResourceDescription(texture.AsResource()); + if (r.IsFailure) + { + return Identifier.Invalid; + } + + var desc = r.Value; + return _resources.ImportTexture(in desc._desc.textureDescription, texture, name); + } + + /// + /// Imports an external buffer into the render graph. + /// + /// The external buffer handle. + /// The identifier of the imported render graph buffer. Invalid if import fails. + public Identifier ImportBuffer(Handle buffer, string name) + { + var r = _graphicsEngine.ResourceDatabase.GetResourceDescription(buffer.AsResource()); + if (r.IsFailure) + { + return Identifier.Invalid; + } + + var desc = r.Value; + return _resources.ImportBuffer(in desc._desc.bufferDescription, buffer, name); + } + + public IRasterRenderGraphBuilder AddRasterRenderPass(string name, out TPassData passData) + where TPassData : class, new() + { + var renderPass = _objectPool.Rent>(); + renderPass.Init(_passes.Count, _objectPool.Rent(), name, RenderPassType.Raster); + passData = renderPass.passData; + + _passes.Add(renderPass); + + _builder.Init(this, renderPass, _resources); + return _builder; + } + + public IComputeRenderGraphBuilder AddComputeRenderPass(string name, out TPassData passData) + where TPassData : class, new() + { + var renderPass = _objectPool.Rent>(); + renderPass.Init(_passes.Count, _objectPool.Rent(), name, RenderPassType.Compute); + passData = renderPass.passData; + + _passes.Add(renderPass); + + _builder.Init(this, renderPass, _resources); + return _builder; + } + + public IUnsafeRenderGraphBuilder AddUnsafeRenderPass(string name, out TPassData passData) + where TPassData : class, new() + { + var renderPass = _objectPool.Rent>(); + renderPass.Init(_passes.Count, _objectPool.Rent(), name, RenderPassType.Unsafe); + passData = renderPass.passData; + + _passes.Add(renderPass); + + _builder.Init(this, renderPass, _resources); + return _builder; + } + + + private unsafe int ComputeTextureHash(byte* pData, int offset, Identifier texture) + { + if (texture.IsInvalid) + { + return offset; + } + + var resource = _resources.GetResource(texture.AsResource()); + + // Hash imported flag + *(pData + offset) = resource.isImported ? (byte)1 : (byte)0; + offset += sizeof(byte); + + // For imported textures, hash the backing resource handle + if (resource.isImported) + { + *(int*)(pData + offset) = resource.backingResource.GetHashCode(); + offset += sizeof(int); + return offset; + } + + var desc = resource.rgTextureDesc; + + // Hash format (structural) + *(TextureFormat*)(pData + offset) = desc.format; + offset += sizeof(TextureFormat); + + // Hash size mode (structural) + *(RGTextureSizeMode*)(pData + offset) = desc.sizeMode; + offset += sizeof(RGTextureSizeMode); + + // Hash size specification based on mode + if (desc.sizeMode == RGTextureSizeMode.Absolute) + { + // Absolute mode: hash actual dimensions + *(uint*)(pData + offset) = desc.width; + offset += sizeof(uint); + *(uint*)(pData + offset) = desc.height; + offset += sizeof(uint); + } + else + { + // Relative mode: hash scale factors (NOT resolved dimensions) + *(float*)(pData + offset) = desc.scaleX; + offset += sizeof(float); + *(float*)(pData + offset) = desc.scaleY; + offset += sizeof(float); + } + + // Hash other structural properties + *(TextureDimension*)(pData + offset) = desc.dimension; + offset += sizeof(TextureDimension); + *(uint*)(pData + offset) = desc.mipLevels; + offset += sizeof(uint); + *(TextureUsage*)(pData + offset) = desc.usage; + offset += sizeof(TextureUsage); + + *(bool*)(pData + offset) = desc.clearAtFirstUse; + offset += sizeof(bool); + *(bool*)(pData + offset) = desc.discardAtLastUse; + offset += sizeof(bool); + + return offset; + } + + + private unsafe ulong ComputeGraphHash() + { + using var scope = AllocationManager.CreateStackScope(); + var bufferPool = new UnsafeList(2048, scope.AllocationHandle); + var pData = (byte*)bufferPool.GetUnsafePtr(); + var offset = 0; + + // Hash pass count + *(int*)(pData + offset) = _passes.Count; + offset += sizeof(int); + + // Hash each pass structure (excluding names) + for (var i = 0; i < _passes.Count; i++) + { + var pass = _passes[i]; + + *(RenderPassType*)(pData + offset) = pass.type; + offset += sizeof(RenderPassType); + + *(bool*)(pData + offset) = pass.allowCulling; + offset += sizeof(bool); + + *(bool*)(pData + offset) = pass.asyncCompute; + offset += sizeof(bool); + + // Hash depth attachment + offset = ComputeTextureHash(pData, offset, pass.depthAccess.id); + + pData[offset] = (byte)pass.depthAccess.accessFlags; + offset += sizeof(AccessFlags); + + *(int*)(pData + offset) = pass.maxColorIndex; + offset += sizeof(int); + for (var j = 0; j <= pass.maxColorIndex; j++) + { + offset = ComputeTextureHash(pData, offset, pass.colorAccess[j].id); + + pData[offset] = (byte)pass.colorAccess[j].accessFlags; + offset += sizeof(AccessFlags); + } + + for (var j = 0; j < (int)RenderGraphResourceType.Count; j++) + { + var readList = pass.resourceReads[j]; + var writeList = pass.resourceWrites[j]; + var createList = pass.resourceCreates[j]; + + *(int*)(pData + offset) = readList.Count; + offset += sizeof(int); + for (var k = 0; k < readList.Count; k++) + { + *(int*)(pData + offset) = readList[k].Value; + offset += sizeof(int); + } + + *(int*)(pData + offset) = writeList.Count; + offset += sizeof(int); + for (var k = 0; k < writeList.Count; k++) + { + *(int*)(pData + offset) = writeList[k].Value; + offset += sizeof(int); + } + + *(int*)(pData + offset) = createList.Count; + offset += sizeof(int); + for (var k = 0; k < createList.Count; k++) + { + *(int*)(pData + offset) = createList[k].Value; + offset += sizeof(int); + } + + *(int*)(pData + offset) = pass.randomAccess.Count; + offset += sizeof(int); + for (var k = 0; k < pass.randomAccess.Count; k++) + { + *(int*)(pData + offset) = pass.randomAccess[k].Value; + offset += sizeof(int); + } + + // Hash buffer hints (important for correct barrier generation) + *(int*)(pData + offset) = pass.bufferHints.Count; + offset += sizeof(int); + foreach (var kvp in pass.bufferHints) + { + *(int*)(pData + offset) = kvp.Key; // Buffer resource ID + offset += sizeof(int); + *(int*)(pData + offset) = (int)kvp.Value; // BufferHint flags + offset += sizeof(int); + } + } + + *(int*)(pData + offset) = pass.GetRenderFuncHashCode(); + offset += sizeof(int); + } + + var span = new Span(pData, offset); + return XxHash64.HashToUInt64(span); + } + + /// + /// Compiles the render graph by culling unused passes and determining resource lifetimes. + /// + public void Compile(in ViewState viewState) + { + if (_compiled) + { + return; + } + + _currentViewState = viewState; + + // Resolve texture sizes before computing hash + _resources.ResolveTextureSizes(in viewState); + + var graphHash = ComputeGraphHash(); + if (_compilationCache.TryGetCached(graphHash, out var cached)) + { + // Check if view state changed + if (!cached.viewState.Equals(viewState)) + { + // View state changed - re-resolve sizes and recreate GPU resources + _resources.ResolveTextureSizes(in viewState); + RecreateResourcesForNewViewState(cached, viewState); + } + else + { + // Perfect cache hit - restore everything + RestoreFromCache(cached); + } + + _compiled = true; + return; + } + + _compiledPasses.Clear(); + + // Mark passes with side effects (writes to imported resources) + for (var i = 0; i < _passes.Count; i++) + { + var pass = _passes[i]; + + // Check if this pass writes to any imported resources + for (var j = 0; j < (int)RenderGraphResourceType.Count; j++) + { + var writeList = pass.resourceWrites[j]; + for (var k = 0; k < writeList.Count; k++) + { + var writeHandle = writeList[k]; + var resource = _resources.GetResource(writeHandle); + if (resource.isImported) + { + pass.hasSideEffects = true; + break; + } + } + } + } + + // Cull passes based on dependency analysis + // Mark all passes as culled initially + for (var i = 0; i < _passes.Count; i++) + { + _passes[i].culled = _passes[i].allowCulling && !_passes[i].hasSideEffects; + } + + // Traverse backwards from passes with side effects + for (var i = _passes.Count - 1; i >= 0; i--) + { + var pass = _passes[i]; + if (!pass.culled) + { + UnculDependencies(pass); + } + } + + // Build final pass list (only non-culled passes) + for (var i = 0; i < _passes.Count; i++) + { + var pass = _passes[i]; + if (!pass.culled) + { + _compiledPasses.Add(pass); + } + } + + _aliasingManager.AssignPhysicalResources(_resources, _passes.Count); + AllocateResource(); + + GenerateBarriers(); + BuildNativeRenderPasses(); + StoreInCache(graphHash); + + _compiled = true; + } + + private void AllocateResource() + { + if (_resourceHeap.IsValid) + { + foreach (var res in _resources.Resources) + { + if (res.isImported) + { + continue; + } + + _graphicsEngine.ResourceDatabase.ReleaseResource(res.backingResource); + } + + _graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap); + } + + if (_aliasingManager.Heap.size == 0) + { + return; + } + + var allocationDesc = new AllocationDesc + { + Size = _aliasingManager.Heap.size + 1024 * 1024, + Alignment = ResourceHeap.DEFAULT_ALIGNMENT, + HeapFlags = HeapFlags.AlowBufferAndTexture, + HeapType = HeapType.Default + }; + + _resourceHeap = _graphicsEngine.ResourceAllocator.Allocate(in allocationDesc, "RenderGraphResourceHeap"); + + for (var i = 0; i < _resources.Resources.Count; i++) + { + var placedIndex = _aliasingManager.GetPlacedResourceIndex(i); + var placed = _aliasingManager.GetPlacedResource(placedIndex); + if (placed == null) + { + continue; + } + + var res = _resources.Resources[i]; + var ops = new CreationOptions + { + AllocationType = ResourceAllocationType.Suballocation, + Heap = _resourceHeap, + Offset = placed.heapOffset, + }; + + if (res.type == RenderGraphResourceType.Texture) + { + var textureDesc = res.rgTextureDesc.ToTextureDesc(res.resolvedWidth, res.resolvedHeight); + res.backingResource = _graphicsEngine.ResourceAllocator.CreateTexture(in textureDesc, res.name, ops).AsResource(); + } + else if (res.type == RenderGraphResourceType.Buffer) + { + res.backingResource = _graphicsEngine.ResourceAllocator.CreateBuffer(in res.bufferDesc, res.name, ops).AsResource(); + } + else + { + throw new NotSupportedException(); + } + + _compilationCache.UpdateBackingResource(i, res.backingResource); + } + } + + /// + /// Recreates GPU resources when view state changes but compilation cache is valid. + /// + private void RecreateResourcesForNewViewState(CachedCompilation cached, in ViewState newViewState) + { + // Restore compilation results (passes, barriers, aliasing) + RestoreFromCache(cached); + AllocateResource(); + + cached.viewState = newViewState; + } + + /// + /// Restores the render graph state from cached compilation results. + /// + private void RestoreFromCache(CachedCompilation cached) + { + // Restore compiled pass list + _compiledPasses.Clear(); + for (var i = 0; i < cached.compiledPassIndices.Count; i++) + { + var passIndex = cached.compiledPassIndices[i]; + _compiledPasses.Add(_passes[passIndex]); + } + + // Restore culling flags + for (var i = 0; i < _passes.Count && i < cached.passCulledFlags.Count; i++) + { + _passes[i].culled = cached.passCulledFlags[i]; + } + + // Restore aliasing mappings (need to update ResourceAliasingManager) + _aliasingManager.RestoreFromCache(cached.logicalToPhysical, cached.placedResources); + + // Restore barriers (deep copy to avoid shared references) + _barriers.Clear(); + for (var i = 0; i < cached.barriers.Count; i++) + { + _barriers.Add(cached.barriers[i]); + } + + // Restore resource states + _resourceStates.Clear(); + foreach (var kvp in cached.resourceStates) + { + _resourceStates[kvp.Key] = kvp.Value; + } + + for (var i = 0; i < _resources.ResourceCount; i++) + { + var res = _resources.Resources[i]; + + if (!res.isImported) + { + res.backingResource = cached.backingResources[i]; + } + } + } + + /// + /// Stores current compilation results in the cache. + /// + private void StoreInCache(ulong graphHash) + { + var cacheData = new CachedCompilation(); + + // Store view state + cacheData.viewState = _currentViewState; + + // Store compiled pass indices + for (var i = 0; i < _compiledPasses.Count; i++) + { + cacheData.compiledPassIndices.Add(_compiledPasses[i].index); + } + + // Store culling flags for all passes + for (var i = 0; i < _passes.Count; i++) + { + cacheData.passCulledFlags.Add(_passes[i].culled); + } + + // Store aliasing mappings + _aliasingManager.StoreToCache(cacheData.logicalToPhysical, cacheData.placedResources); + + // Store barriers + for (var i = 0; i < _barriers.Count; i++) + { + cacheData.barriers.Add(_barriers[i]); + } + + // Store resource states + foreach (var kvp in _resourceStates) + { + cacheData.resourceStates[kvp.Key] = kvp.Value; + } + + for (var i = 0; i < _resources.ResourceCount; i++) + { + var res = _resources.Resources[i]; + cacheData.backingResources.Add(res.backingResource); + } + + _compilationCache.Store(graphHash, cacheData); + } + + private void UnculProducer(Identifier resource) + { + var res = _resources.GetResource(resource); + if (res.producerPass >= 0) + { + var producer = _passes[res.producerPass]; + if (producer.culled) + { + producer.culled = false; + UnculDependencies(producer); + } + } + } + + private void UnculDependencies(RenderGraphPassBase pass) + { + // Un-cull producers of read resources + for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) + { + var readList = pass.resourceReads[i]; + for (var j = 0; j < readList.Count; j++) + { + UnculProducer(readList[j]); + } + } + + // Un-cull producers of color attachments + for (var i = 0; i < pass.maxColorIndex; i++) + { + if (pass.colorAccess[i].id.IsValid) + { + UnculProducer(pass.colorAccess[i].id.AsResource()); + } + } + + // Un-cull producer of depth attachment + if (pass.depthAccess.id.IsValid) + { + UnculProducer(pass.depthAccess.id.AsResource()); + } + + // Un-cull producers of UAV resources (if not already in reads/writes) + for (var i = 0; i < pass.randomAccess.Count; i++) + { + UnculProducer(pass.randomAccess[i]); + } + } + + /// + /// Generates resource barriers for state transitions and aliasing. + /// + private void GenerateBarriers() + { + _barriers.Clear(); + _resourceStates.Clear(); + + // Process each compiled pass in order + for (var passIdx = 0; passIdx < _compiledPasses.Count; passIdx++) + { + var pass = _compiledPasses[passIdx]; + + // Insert aliasing barriers for resources that reuse physical memory + InsertAliasingBarriers(pass, passIdx); + + // Insert transition barriers for state changes + InsertTransitionBarriers(pass, passIdx); + } + } + + /// + /// Inserts aliasing barriers when a placed resource is reused. + /// + private void InsertAliasingBarriers(RenderGraphPassBase pass, int passIdx) + { + // Check all resources written by this pass (both textures and buffers) + for (var resType = 0; resType < (int)RenderGraphResourceType.Count; resType++) + { + var writeList = pass.resourceWrites[resType]; + for (var i = 0; i < writeList.Count; i++) + { + var id = writeList[i]; + var resource = _resources.GetResource(id); + + // Skip imported resources + if (resource.isImported) + { + continue; + } + + // Check if this is the first use of this logical resource + if (resource.firstUsePass == pass.index) + { + // Get the placed resource + var placedIndex = _aliasingManager.GetPlacedResourceIndex(id.Value); + if (placedIndex >= 0) + { + var placed = _aliasingManager.GetPlacedResource(placedIndex); + + // If this placed resource has multiple aliased resources, + // we need an aliasing barrier when switching between them + if (placed != null && placed.aliasedLogicalResources.Count > 1) + { + // Find the resource that used this placed memory most recently before this pass + Identifier resourceBefore = default; + var mostRecentLastUse = -1; + + foreach (var otherLogicalIndex in placed.aliasedLogicalResources) + { + if (otherLogicalIndex != id.Value) + { + // Get resource by global index + var otherResource = _resources.GetResourceByIndex(otherLogicalIndex); + + // Check if this resource finished before our resource starts + if (otherResource.lastUsePass < pass.index && + otherResource.lastUsePass > mostRecentLastUse) + { + mostRecentLastUse = otherResource.lastUsePass; + resourceBefore = new Identifier(otherLogicalIndex); + } + } + } + + // If we found a previous resource, insert aliasing barrier + if (mostRecentLastUse >= 0) + { + var barrier = ResourceBarrier.CreateAliasingBarrier( + resourceBefore, + id, + passIdx + ); + _barriers.Add(barrier); + } + } + } + } + } + } + } + + /// + /// Inserts transition barriers when a resource changes state. + /// + private void InsertTransitionBarriers(RenderGraphPassBase pass, int passIdx) + { + // Process reads (transition to appropriate state based on resource type and hints) + for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) + { + var readList = pass.resourceReads[i]; + for (var j = 0; j < readList.Count; j++) + { + var handle = readList[j]; + var state = GetBufferReadState(handle, pass, (RenderGraphResourceType)i); + InsertTransitionIfNeeded(handle, state, passIdx); + } + } + + switch (pass.type) + { + case RenderPassType.Raster: + for (var i = 0; i < pass.maxColorIndex; i++) + { + var access = pass.colorAccess[i]; + InsertTransitionIfNeeded(access.id.AsResource(), ResourceState.RenderTarget, passIdx); + } + + if (pass.depthAccess.id.IsValid) + { + var depthAccess = pass.depthAccess; + InsertTransitionIfNeeded(depthAccess.id.AsResource(), ResourceState.DepthWrite, passIdx); + } + + for (var i = 0; i < pass.randomAccess.Count; i++) + { + InsertTransitionIfNeeded(pass.randomAccess[i], ResourceState.UnorderedAccess, passIdx); + } + + break; + case RenderPassType.Compute: + for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) + { + var writeList = pass.resourceWrites[i]; + for (var j = 0; j < writeList.Count; j++) + { + var id = writeList[j]; + InsertTransitionIfNeeded(id, ResourceState.UnorderedAccess, passIdx); + } + } + + break; + case RenderPassType.Unsafe: + for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) + { + var writeList = pass.resourceWrites[i]; + for (var j = 0; j < writeList.Count; j++) + { + var id = writeList[j]; + InsertTransitionIfNeeded(id, ResourceState.RenderTarget, passIdx); + } + } + + for (var i = 0; i < pass.randomAccess.Count; i++) + { + InsertTransitionIfNeeded(pass.randomAccess[i], ResourceState.UnorderedAccess, passIdx); + } + break; + } + + } + + /// + /// Inserts a transition barrier if the resource state changes. + /// + private void InsertTransitionIfNeeded(Identifier resource, ResourceState newState, int passIdx) + { + if (!_resourceStates.TryGetValue(resource.Value, out var currentState)) + { + // First time seeing this resource, assume undefined + // currentState = ResourceState.Common; + var r = _graphicsEngine.ResourceDatabase.GetResourceState(_resources.GetResource(resource).backingResource); + currentState = r.IsSuccess ? r.Value : ResourceState.Common; + } + + if (currentState != newState) + { + var barrier = ResourceBarrier.CreateTransitionBarrier( + resource, + currentState, + newState, + passIdx + ); + _barriers.Add(barrier); + _resourceStates[resource.Value] = newState; + } + } + + /// + /// Determines the appropriate resource state for a buffer read operation based on usage hints. + /// + private static ResourceState GetBufferReadState(Identifier handle, RenderGraphPassBase pass, RenderGraphResourceType resourceType) + { + // Textures always use ShaderResource state + if (resourceType == RenderGraphResourceType.Texture) + { + return ResourceState.PixelShaderResource | ResourceState.NonPixelShaderResource; + } + + // Check for buffer-specific usage hints + if (pass.bufferHints.TryGetValue(handle.Value, out var hint)) + { + if (hint.HasFlag(BufferHint.IndirectArgument)) + { + return ResourceState.IndirectArgument; + } + } + + // Default: ByteAddressBuffer read (SRV) - matches bindless architecture + return ResourceState.PixelShaderResource | ResourceState.NonPixelShaderResource; + } + + /// + /// Builds native render passes by merging compatible consecutive raster passes. + /// Uses conservative merging: only merge passes with identical attachments and no barriers between them. + /// + private void BuildNativeRenderPasses() + { + // Clear previous native passes + for (var i = 0; i < _nativePasses.Count; i++) + { + _objectPool.Return(_nativePasses[i]); + } + _nativePasses.Clear(); + + NativeRenderPass? currentNativePass = null; + + for (var i = 0; i < _compiledPasses.Count; i++) + { + var pass = _compiledPasses[i]; + + // Only raster passes can be merged into native render passes + // Compute passes break the current native render pass + if (pass.type != RenderPassType.Raster) + { + // Close current native pass if open + if (currentNativePass != null) + { + _nativePasses.Add(currentNativePass); + currentNativePass = null; + } + continue; // Compute/Unsafe passes execute outside native render passes + } + + + // Check if we can merge with current native pass + if (currentNativePass != null && CanMergePasses(currentNativePass, pass, i)) + { + // Merge into existing native pass + currentNativePass.mergedPassIndices.Add(i); + currentNativePass.lastLogicalPass = i; + } + else + { + // Start new native pass + if (currentNativePass != null) + { + _nativePasses.Add(currentNativePass); + } + + currentNativePass = CreateNativePass(pass, i); + } + } + + // Add final native pass + if (currentNativePass != null) + { + _nativePasses.Add(currentNativePass); + } + + // Infer load/store operations for all native passes + for (var i = 0; i < _nativePasses.Count; i++) + { + InferLoadStoreOps(_nativePasses[i]); + } + } + + /// + /// Creates a new native render pass from a logical pass. + /// + private NativeRenderPass CreateNativePass(RenderGraphPassBase pass, int passIndex) + { + var nativePass = _objectPool.Rent(); + nativePass.Reset(); + + nativePass.index = _nativePasses.Count; + nativePass.mergedPassIndices.Add(passIndex); + nativePass.firstLogicalPass = passIndex; + nativePass.lastLogicalPass = passIndex; + nativePass.allowUAVWrites = pass.randomAccess.Count > 0; + + // Copy color attachments + nativePass.colorAttachmentCount = pass.maxColorIndex + 1; + for (var i = 0; i <= pass.maxColorIndex; i++) + { + var access = pass.colorAccess[i]; + nativePass.colorAttachments[i] = new RenderTargetInfo + { + texture = access.id, + access = access.accessFlags + }; + } + + // Copy depth attachment + if (!pass.depthAccess.id.IsInvalid) + { + nativePass.hasDepthAttachment = true; + nativePass.depthAttachment = new DepthStencilInfo + { + texture = pass.depthAccess.id, + access = pass.depthAccess.accessFlags + }; + } + + return nativePass; + } + + /// + /// Checks if a logical pass can be merged into an existing native render pass. + /// Conservative merging: only merge if attachments match and no barriers needed. + /// + private bool CanMergePasses(NativeRenderPass nativePass, RenderGraphPassBase pass, int passIndex) + { + // Don't merge if UAVs are involved (conservative) + if (pass.randomAccess.Count > 0 || nativePass.allowUAVWrites) + { + return false; + } + + // Check if attachment configuration matches + if (!AttachmentsMatch(nativePass, pass)) + { + return false; + } + + // Check if barriers are needed between last merged pass and this pass + if (RequiresBarrierBetweenPasses(nativePass.lastLogicalPass, passIndex)) + { + return false; + } + + return true; + } + + /// + /// Checks if the attachment configuration of a pass matches the native pass. + /// + private static bool AttachmentsMatch(NativeRenderPass nativePass, RenderGraphPassBase pass) + { + // Check color attachment count + if (nativePass.colorAttachmentCount != pass.maxColorIndex + 1) + { + return false; + } + + // Check each color attachment + for (var i = 0; i < nativePass.colorAttachmentCount; i++) + { + if (nativePass.colorAttachments[i].texture != pass.colorAccess[i].id) + { + return false; + } + } + + // Check depth attachment + if (nativePass.hasDepthAttachment != !pass.depthAccess.id.IsInvalid) + { + return false; + } + + if (nativePass.hasDepthAttachment && nativePass.depthAttachment.texture != pass.depthAccess.id) + { + return false; + } + + return true; + } + + /// + /// Checks if any barriers are required between two passes that would prevent merging. + /// Only barriers affecting render targets prevent merging; SRV barriers are fine. + /// + private bool RequiresBarrierBetweenPasses(int passA, int passB) + { + var laterPass = _compiledPasses[passB]; + + // Build a set of render target resource IDs (color + depth) + var renderTargets = new HashSet>(); + for (var i = 0; i <= laterPass.maxColorIndex; i++) + { + if (!laterPass.colorAccess[i].id.IsInvalid) + { + renderTargets.Add(laterPass.colorAccess[i].id.AsResource()); + } + } + if (!laterPass.depthAccess.id.IsInvalid) + { + renderTargets.Add(laterPass.depthAccess.id.AsResource()); + } + + // Check if any barriers for passB affect render targets + for (var i = 0; i < _barriers.Count; i++) + { + if (_barriers[i].PassIndex == passB) + { + // Only prevent merge if barrier affects a render target + if (renderTargets.Contains(_barriers[i].Resource)) + { + return true; // Barrier affects render target, cannot merge + } + } + + if (_barriers[i].PassIndex > passB) + { + break; // No more barriers for this pass + } + } + + return false; + } + + /// + /// Infers optimal load/store operations for all attachments in a native render pass. + /// Uses resource lifetime information to minimize memory bandwidth (critical for TBDR GPUs). + /// + private void InferLoadStoreOps(NativeRenderPass nativePass) + { + // Infer load/store ops for color attachments + for (var i = 0; i < nativePass.colorAttachmentCount; i++) + { + ref var attachment = ref nativePass.colorAttachments[i]; + var resource = _resources.GetResource(attachment.texture); + var flags = attachment.access; + + // ===== LOAD OP INFERENCE ===== + + // 1. First use + if (resource.firstUsePass == nativePass.firstLogicalPass) + { + // Clear at first use + if (resource.rgTextureDesc.clearAtFirstUse) + { + attachment.loadOp = AttachmentLoadOp.Clear; + attachment.clearColor = resource.rgTextureDesc.clearColor; + } + else + { + attachment.loadOp = AttachmentLoadOp.DontCare; + } + } + // 2. Discard flag: DontCare for performance + else if (flags.HasFlag(AccessFlags.Discard)) + { + attachment.loadOp = AttachmentLoadOp.DontCare; + } + // 3. Read flag: Must preserve existing contents + else if (flags.HasFlag(AccessFlags.Read)) + { + attachment.loadOp = AttachmentLoadOp.Load; + } + // 4. Continuation from previous pass + else + { + attachment.loadOp = AttachmentLoadOp.Load; + } + + // ===== STORE OP INFERENCE ===== + + // Last use: No one needs it after this native pass + if (resource.lastUsePass == nativePass.lastLogicalPass) + { + if (resource.rgTextureDesc.discardAtLastUse) + { + attachment.storeOp = AttachmentStoreOp.DontCare; + } + else + { + attachment.storeOp = AttachmentStoreOp.Store; + } + } + // Intermediate: Store for future passes + else + { + attachment.storeOp = AttachmentStoreOp.Store; + } + + } + + // Infer load/store ops for depth attachment + if (nativePass.hasDepthAttachment) + { + ref var attachment = ref nativePass.depthAttachment; + var resource = _resources.GetResource(attachment.texture); + var flags = attachment.access; + + // ===== LOAD OP INFERENCE ===== + + // 1. First Use + if (resource.firstUsePass == nativePass.firstLogicalPass) + { + // Clear at first use + if (resource.rgTextureDesc.clearAtFirstUse) + { + attachment.loadOp = AttachmentLoadOp.Clear; + attachment.clearDepth = resource.rgTextureDesc.clearDepth; + attachment.clearStencil = resource.rgTextureDesc.clearStencil; + } + else + { + attachment.loadOp = AttachmentLoadOp.DontCare; + } + } + // 2. Discard flag: DontCare for performance + else if (flags.HasFlag(AccessFlags.Discard)) + { + attachment.loadOp = AttachmentLoadOp.DontCare; + } + // 3. Read flag: Must preserve existing contents + else if (flags.HasFlag(AccessFlags.Read)) + { + attachment.loadOp = AttachmentLoadOp.Load; + } + // 4. Continuation from previous pass + else + { + attachment.loadOp = AttachmentLoadOp.Load; + } + + // ===== STORE OP INFERENCE ===== + + // Depth is commonly discarded (depth-only passes, intermediate depth) + if (resource.lastUsePass == nativePass.lastLogicalPass) + { + if (resource.rgTextureDesc.discardAtLastUse) + { + attachment.storeOp = AttachmentStoreOp.DontCare; + } + else + { + attachment.storeOp = AttachmentStoreOp.Store; + } + } + else + { + attachment.storeOp = AttachmentStoreOp.Store; + } + + } + } + + /// + /// Executes all compiled passes using native render passes where possible. + /// + public unsafe void Execute(ICommandBuffer cmd) + { + if (!_compiled) + { + throw new InvalidOperationException("Render graph must be compiled before execution. Call Compile(viewState) first."); + } + + var barrierIndex = 0; + var nativePassIndex = 0; + var logicalPassIndex = 0; + + _context.SetCommandBuffer(cmd); + + var pPassRTDescs = stackalloc PassRenderTargetDesc[8]; + var pRtFormats = stackalloc TextureFormat[8]; + + while (logicalPassIndex < _compiledPasses.Count) + { + var pass = _compiledPasses[logicalPassIndex]; + + // Check if this pass is part of a native render pass + if (pass.type == RenderPassType.Raster && nativePassIndex < _nativePasses.Count) + { + var nativePass = _nativePasses[nativePassIndex]; + + // Build barriers for ALL merged passes before beginning the native render pass + for (var i = 0; i < nativePass.mergedPassIndices.Count; i++) + { + var mergedPassIdx = nativePass.mergedPassIndices[i]; + ExecuteBarriersForPass(cmd, mergedPassIdx, ref barrierIndex); + } + + // Begin native render pass + for (var i = 0; i < nativePass.colorAttachmentCount; i++) + { + var attachment = nativePass.colorAttachments[i]; + pPassRTDescs[i] = new PassRenderTargetDesc + { + Texture = _resources.GetResource(attachment.texture).backingResource.AsTexture(), + ClearColor = attachment.clearColor, + LoadOp = attachment.loadOp, + StoreOp = attachment.storeOp + }; + } + + var depthDesc = new PassDepthStencilDesc + { + Texture = nativePass.hasDepthAttachment + ? _resources.GetResource(nativePass.depthAttachment.texture).backingResource.AsTexture() + : Handle.Invalid, + ClearDepth = nativePass.depthAttachment.clearDepth, + ClearStencil = nativePass.depthAttachment.clearStencil, + DepthLoadOp = nativePass.hasDepthAttachment + ? nativePass.depthAttachment.loadOp + : AttachmentLoadOp.DontCare, + DepthStoreOp = nativePass.hasDepthAttachment + ? nativePass.depthAttachment.storeOp + : AttachmentStoreOp.DontCare, + StencilLoadOp = nativePass.hasDepthAttachment + ? nativePass.depthAttachment.loadOp + : AttachmentLoadOp.DontCare, + StencilStoreOp = nativePass.hasDepthAttachment + ? nativePass.depthAttachment.storeOp + : AttachmentStoreOp.DontCare + }; + + cmd.BeginRenderPass(new Span(pPassRTDescs, nativePass.colorAttachmentCount), depthDesc); + + for (var i = 0; i < nativePass.colorAttachmentCount; i++) + { + var attachment = nativePass.colorAttachments[i]; + var resource = _resources.GetResource(attachment.texture); + pRtFormats[i] = resource.rgTextureDesc.format; + } + + var depthFormat = nativePass.hasDepthAttachment + ? _resources.GetResource(nativePass.depthAttachment.texture).rgTextureDesc.format + : TextureFormat.Unknown; + _context.SetRenderTargetFormats(new ReadOnlySpan(pRtFormats, nativePass.colorAttachmentCount), depthFormat); + + // Build all merged logical passes within this native render pass + for (var i = 0; i < nativePass.mergedPassIndices.Count; i++) + { + var mergedPassIdx = nativePass.mergedPassIndices[i]; + var mergedPass = _compiledPasses[mergedPassIdx]; + mergedPass.Execute(_context); + logicalPassIndex++; + } + + cmd.EndRenderPass(); + nativePassIndex++; + } + else + { + // Compute pass or standalone raster pass (not merged) or Unsafe pass + ExecuteBarriersForPass(cmd, logicalPassIndex, ref barrierIndex); + pass.Execute(_context); + logicalPassIndex++; + } + + } + } + + /// + /// Executes all barriers for a specific pass. + /// + private unsafe void ExecuteBarriersForPass(ICommandBuffer cmd, int passIndex, ref int barrierIndex) + { + var pBarrierDescs = stackalloc BarrierDesc[16]; // batch by 16 + var count = 0; + var hasRemain = false; + + Start: + while (barrierIndex < _barriers.Count && _barriers[barrierIndex].PassIndex == passIndex) + { + var barrier = _barriers[barrierIndex]; + + var desc = new BarrierDesc(); + desc.type = barrier.Type; + switch (desc.type) + { + case BarrierType.Transition: + desc.transition.resource = _resources.GetResource(barrier.Resource).backingResource; + desc.transition.stateBefore = barrier.StateBefore; + desc.transition.stateAfter = barrier.StateAfter; + break; + case BarrierType.Aliasing: + desc.aliasing.resourceBefore = _resources.GetResource(barrier.ResourceBefore).backingResource; + desc.aliasing.resourceAfter = _resources.GetResource(barrier.ResourceAfter).backingResource; + break; + case BarrierType.UAV: + desc.uav.resource = _resources.GetResource(barrier.Resource).backingResource; + break; + } + + pBarrierDescs[count] = desc; + count++; + + barrierIndex++; + + if (count == 16) + { + hasRemain = _barriers.Count > barrierIndex && _barriers[barrierIndex].PassIndex == passIndex; + break; + } + } + + if (count > 0) + { + cmd.ResourceBarrier(new ReadOnlySpan(pBarrierDescs, count)); + } + + if (hasRemain) + { + count = 0; + hasRemain = false; + goto Start; + } + } + + public void Dispose() + { + _graphicsEngine.ResourceDatabase.ReleaseResource(_resourceHeap); + } +} diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphAliasing.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphAliasing.cs new file mode 100644 index 0000000..4ff0fae --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphAliasing.cs @@ -0,0 +1,543 @@ +using Ghost.Core.Utilities; +using Ghost.Graphics.RHI; +using System.Runtime.InteropServices; + +namespace Ghost.Graphics.RenderGraphModule; + +/// +/// Represents a memory block within a heap. +/// +internal struct MemoryBlock +{ + public ulong offset; + public ulong size; + public bool isFree; + public int firstUsePass; + public int lastUsePass; + public int logicalResourceIndex; // Which logical resource is currently using this block + + public MemoryBlock(ulong offset, ulong size) + { + this.offset = offset; + this.size = size; + isFree = true; + firstUsePass = int.MaxValue; + lastUsePass = -1; + logicalResourceIndex = -1; + } + + public void Reset() + { + isFree = true; + firstUsePass = int.MaxValue; + lastUsePass = -1; + logicalResourceIndex = -1; + } +} + +/// +/// Represents a GPU memory heap for placed resources. +/// Supports D3D12-style heap tier 2 (buffers and textures can alias). +/// +internal sealed class ResourceHeap +{ + public int index; + public ulong size; + private readonly List _blocks = new(32); + + // D3D12 heap alignment requirement (64KB for MSAA textures, 4KB for others) + public const ulong DEFAULT_ALIGNMENT = 65536; // 64KB + + public ResourceHeap(int index, ulong initialSize = 16 * 1024 * 1024) // 16MB default + { + this.index = index; + this.size = initialSize; + + // Initially one large free block + _blocks.Add(new MemoryBlock(0, initialSize)); + } + + public void Reset() + { + _blocks.Clear(); + _blocks.Add(new MemoryBlock(0, size)); + } + + /// + /// Attempts to allocate a block of the requested size with proper alignment. + /// Uses best-fit algorithm with lifetime-aware allocation. + /// + public (bool success, ulong offset, MemoryBlock block) TryAllocate( + ulong requestedSize, + int firstUsePass, + int lastUsePass, + int logicalResourceIndex, + ulong alignment = DEFAULT_ALIGNMENT) + { + var alignedSize = AlignUp(requestedSize, alignment); + + var bestFitIndex = -1; + ulong bestFitOffset = 0; + var smallestWaste = ulong.MaxValue; + + // Find the best fit block that doesn't overlap with lifetime + var blockSpan = CollectionsMarshal.AsSpan(_blocks); + for (var i = 0; i < blockSpan.Length; i++) + { + ref var block = ref blockSpan[i]; + + // Try to find space within this block + var alignedOffset = AlignUp(block.offset, alignment); + var endOffset = alignedOffset + alignedSize; + + if (endOffset <= block.offset + block.size) + { + // Check if this offset range conflicts with ANY existing allocations + var canUseOffset = CanPlaceAtOffset(alignedOffset, alignedSize, firstUsePass, lastUsePass); + + if (canUseOffset) + { + var waste = block.size - alignedSize; + + if (waste < smallestWaste) + { + smallestWaste = waste; + bestFitIndex = i; + bestFitOffset = alignedOffset; + } + } + } + } + + if (bestFitIndex == -1) + { + return (false, 0, default); + } + + ref var bestFit = ref CollectionsMarshal.AsSpan(_blocks)[bestFitIndex]; + + // If the block is free, we need to split it + if (bestFit.isFree) + { + var remainingSize = (bestFit.offset + bestFit.size) - (bestFitOffset + alignedSize); + + // Update the current block to be allocated + bestFit.offset = bestFitOffset; + bestFit.size = alignedSize; + bestFit.isFree = false; + bestFit.firstUsePass = firstUsePass; + bestFit.lastUsePass = lastUsePass; + bestFit.logicalResourceIndex = logicalResourceIndex; + + // Create a new free block for the remaining space if there is any + if (remainingSize > 0) + { + var newBlock = new MemoryBlock(bestFitOffset + alignedSize, remainingSize); + _blocks.Insert(bestFitIndex + 1, newBlock); + } + } + else + { + // Block is already allocated but lifetime doesn't overlap, we can alias it + // Create a new aliased block at the same location + var aliasedBlock = new MemoryBlock(bestFitOffset, alignedSize) + { + isFree = false, + firstUsePass = firstUsePass, + lastUsePass = lastUsePass, + logicalResourceIndex = logicalResourceIndex + }; + + // Insert in sorted order by offset + var insertIndex = 0; + for (var i = 0; i < _blocks.Count; i++) + { + if (_blocks[i].offset > bestFitOffset) + { + break; + } + insertIndex = i + 1; + } + _blocks.Insert(insertIndex, aliasedBlock); + // Update bestFit to point to the newly inserted block + bestFit = ref CollectionsMarshal.AsSpan(_blocks)[insertIndex]; + } + + return (true, bestFitOffset, bestFit); + } + + /// + /// Checks if a resource can be placed at the given offset without lifetime conflicts. + /// Must check ALL blocks that overlap with this offset range. + /// + private bool CanPlaceAtOffset(ulong offset, ulong size, int firstUsePass, int lastUsePass) + { + var endOffset = offset + size; + + foreach (var block in _blocks) + { + // Skip free blocks - they don't have lifetime constraints + if (block.isFree) + { + continue; + } + + // Check if this block's memory range overlaps with our target range + var blockEnd = block.offset + block.size; + var memoryOverlap = !(offset >= blockEnd || endOffset <= block.offset); + + if (memoryOverlap) + { + // Memory ranges overlap, check if lifetimes also overlap + var lifetimeOverlap = !(firstUsePass > block.lastUsePass || lastUsePass < block.firstUsePass); + + if (lifetimeOverlap) + { + // Both memory AND lifetime overlap - cannot place here! + return false; + } + } + } + + return true; + } + + /// + /// Gets the total memory that would be used if no aliasing occurred. + /// + public ulong GetTotalAllocatedWithoutAliasing() + { + ulong total = 0; + foreach (var block in _blocks) + { + if (!block.isFree) + { + total += block.size; + } + } + + return total; + } + + /// + /// Gets the peak memory usage considering aliasing (max offset + size). + /// + public ulong GetPeakUsage() + { + ulong peak = 0; + foreach (var block in _blocks) + { + if (!block.isFree) + { + peak = Math.Max(peak, block.offset + block.size); + } + } + + return peak; + } + + private static ulong AlignUp(ulong value, ulong alignment) + { + return (value + alignment - 1) & ~(alignment - 1); + } +} + +/// +/// Represents a placed resource within a heap. +/// +internal sealed class PlacedResource +{ + public int index; + public RenderGraphResourceType type; + public ulong heapOffset; + public ulong sizeInBytes; + + // Lifetime tracking + public int firstUsePass = int.MaxValue; + public int lastUsePass = -1; + + // Aliasing tracking + public readonly List aliasedLogicalResources = new(4); + public MemoryBlock memoryBlock; + + public void Reset() + { + index = -1; + type = RenderGraphResourceType.Texture; + heapOffset = 0; + sizeInBytes = 0; + firstUsePass = int.MaxValue; + lastUsePass = -1; + aliasedLogicalResources.Clear(); + memoryBlock = default; + } + + public void UpdateLifetime(int passIndex) + { + firstUsePass = Math.Min(firstUsePass, passIndex); + lastUsePass = Math.Max(lastUsePass, passIndex); + } +} + +/// +/// Manages physical resource allocation and aliasing using heap-based allocation. +/// Supports D3D12 heap tier 2: buffers and textures can alias as long as lifetimes don't overlap. +/// +internal sealed class ResourceAliasingManager +{ + private readonly RenderGraphObjectPool _pool; + + private readonly ResourceHeap _heap; + private readonly List _placedResources; + // Mapping from logical resource index to placed resource index + private readonly Dictionary _logicalToPlaced; + + // D3D12 alignment constants + private const ulong _DEFAULT_TEXTURE_ALIGNMENT = 65536; // 64KB + private const ulong _DEFAULT_BUFFER_ALIGNMENT = 65536; // 64KB for D3D12 + + public ResourceHeap Heap => _heap; + + /// + /// Helper method to get the size of a resource + /// + private ulong GetResourceSize(RenderGraphResource resource) + { + if (resource.type == RenderGraphResourceType.Texture) + { + var textureDesc = resource.rgTextureDesc.ToTextureDesc(resource.resolvedWidth, resource.resolvedHeight); + return AlignUp(textureDesc.GetTotalBytes(), _DEFAULT_TEXTURE_ALIGNMENT); + } + else // Buffer + { + return resource.bufferDesc.Size; + } + } + + public ResourceAliasingManager(RenderGraphObjectPool pool) + { + _pool = pool; + _heap = new ResourceHeap(0); + _placedResources = new List(32); + _logicalToPlaced = new Dictionary(64); + } + + public void BeginFrame() + { + for (var i = 0; i < _placedResources.Count; i++) + { + _pool.Return(_placedResources[i]); + } + + _placedResources.Clear(); + _logicalToPlaced.Clear(); + + _heap.Reset(); + } + + /// + /// Assigns physical resources (placed resources) to logical resources using heap-based allocation. + /// This is the modern D3D12 approach: check if resource fits in a hole, not if it matches size/format. + /// Uses a two-pass algorithm: + /// 1. First pass: Simulate allocation to determine peak memory usage + /// 2. Second pass: Create a single heap of the peak size and do the real allocation + /// + public void AssignPhysicalResources(RenderGraphResourceRegistry registry, int passCount) + { + // Build list of all logical resources (both textures and buffers) with their lifetimes + var logicalResources = ListPool<(int index, RenderGraphResource resource)>.Rent(); + + // Iterate through all resources in unified list + for (var i = 0; i < registry.ResourceCount; i++) + { + var resource = registry.GetResourceByIndex(i); + if (!resource.isImported) // Don't alias imported resources + { + logicalResources.Add((resource.index, resource)); + } + } + + // Sort by size descending (larger resources first for better packing) + logicalResources.Sort((a, b) => + { + var sizeA = GetResourceSize(a.resource); + var sizeB = GetResourceSize(b.resource); + return sizeB.CompareTo(sizeA); // Descending + }); + + // ===== PASS 1: Simulate allocation to determine peak memory usage ===== + var simulationHeap = new ResourceHeap(0, ulong.MaxValue); // Unlimited size for simulation + foreach (var (logicalIndex, logicalResource) in logicalResources) + { + var size = GetResourceSize(logicalResource); + var alignment = logicalResource.type == RenderGraphResourceType.Texture + ? _DEFAULT_TEXTURE_ALIGNMENT + : _DEFAULT_BUFFER_ALIGNMENT; + + var (success, offset, block) = simulationHeap.TryAllocate( + size, + logicalResource.firstUsePass, + logicalResource.lastUsePass, + logicalIndex, + alignment); + + if (!success) + { + throw new InvalidOperationException("Simulation allocation failed - this should never happen with unlimited heap"); + } + } + + // Get peak usage from simulation + var peakMemoryUsage = simulationHeap.GetPeakUsage(); + + // Align peak usage to 64KB (D3D12 requirement) + peakMemoryUsage = AlignUp(peakMemoryUsage, _DEFAULT_TEXTURE_ALIGNMENT); + + // ===== PASS 2: Create a single heap of the peak size and do the real allocation ===== + _heap.size = peakMemoryUsage; + _heap.Reset(); + + // Allocate each logical resource in the heap + foreach (var (logicalIndex, logicalResource) in logicalResources) + { + var size = GetResourceSize(logicalResource); + var alignment = logicalResource.type == RenderGraphResourceType.Texture + ? _DEFAULT_TEXTURE_ALIGNMENT + : _DEFAULT_BUFFER_ALIGNMENT; + + var (success, offset, block) = _heap.TryAllocate( + size, + logicalResource.firstUsePass, + logicalResource.lastUsePass, + logicalIndex, + alignment); + + if (!success) + { + throw new InvalidOperationException("Real allocation failed - this should match simulation"); + } + + var assignedOffset = offset; + var assignedBlock = block; + + var assignedPlaced = _pool.Rent(); + assignedPlaced.index = _placedResources.Count; + assignedPlaced.type = logicalResource.type; + assignedPlaced.heapOffset = assignedOffset; + assignedPlaced.sizeInBytes = size; + assignedPlaced.firstUsePass = logicalResource.firstUsePass; + assignedPlaced.lastUsePass = logicalResource.lastUsePass; + assignedPlaced.memoryBlock = assignedBlock; + assignedPlaced.aliasedLogicalResources.Clear(); + assignedPlaced.aliasedLogicalResources.Add(logicalIndex); + + _placedResources.Add(assignedPlaced); + _logicalToPlaced[logicalIndex] = assignedPlaced.index; + } + + // Second pass: Populate aliasedLogicalResources lists + // For each placed resource, find all OTHER placed resources at the same offset + for (var i = 0; i < _placedResources.Count; i++) + { + var placed = _placedResources[i]; + + // Find all logical resources that share the same heap location + for (var j = 0; j < _placedResources.Count; j++) + { + if (i == j) + { + continue; // Skip self + } + + var other = _placedResources[j]; + + // Check if they're at the same offset + if (other.heapOffset == placed.heapOffset) + { + // Add the other's logical resource to this one's aliased list + var otherLogicalIndex = other.aliasedLogicalResources[0]; // Each has exactly one at this point + if (!placed.aliasedLogicalResources.Contains(otherLogicalIndex)) + { + placed.aliasedLogicalResources.Add(otherLogicalIndex); + } + } + } + } + + ListPool<(int index, RenderGraphResource resource)>.Return(logicalResources); + } + + public int GetPlacedResourceIndex(int logicalIndex) + { + return _logicalToPlaced.TryGetValue(logicalIndex, out var placedIndex) ? placedIndex : -1; + } + + public PlacedResource? GetPlacedResource(int placedIndex) + { + return placedIndex >= 0 && placedIndex < _placedResources.Count + ? _placedResources[placedIndex] + : null; + } + + private static ulong AlignUp(ulong value, ulong alignment) + { + return (value + alignment - 1) & ~(alignment - 1); + } + + /// + /// Restores aliasing state from cache. + /// + public void RestoreFromCache(Dictionary logicalToPlaced, List placedData) + { + _logicalToPlaced.Clear(); + foreach (var kvp in logicalToPlaced) + { + _logicalToPlaced[kvp.Key] = kvp.Value; + } + + // Restore placed resources + for (var i = 0; i < placedData.Count; i++) + { + var placed = _pool.Rent(); + + var data = placedData[i]; + placed.index = data.index; + placed.type = data.type; + placed.heapOffset = data.heapOffset; + placed.sizeInBytes = data.sizeInBytes; + placed.firstUsePass = data.firstUsePass; + placed.lastUsePass = data.lastUsePass; + + placed.aliasedLogicalResources.Clear(); + + _placedResources.Add(placed); + } + } + + /// + /// Stores current aliasing state to cache. + /// + public void StoreToCache(Dictionary outLogicalToPlaced, List outPlacedData) + { + outLogicalToPlaced.Clear(); + foreach (var kvp in _logicalToPlaced) + { + outLogicalToPlaced[kvp.Key] = kvp.Value; + } + + outPlacedData.Clear(); + for (var i = 0; i < _placedResources.Count; i++) + { + var placed = _placedResources[i]; + outPlacedData.Add(new PlacedResourceData + { + index = placed.index, + type = placed.type, + heapOffset = placed.heapOffset, + sizeInBytes = placed.sizeInBytes, + firstUsePass = placed.firstUsePass, + lastUsePass = placed.lastUsePass + }); + } + } +} diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphBarriers.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphBarriers.cs new file mode 100644 index 0000000..fe6ae16 --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphBarriers.cs @@ -0,0 +1,117 @@ +using Ghost.Core; +using Ghost.Graphics.RHI; +using System.Runtime.InteropServices; + +namespace Ghost.Graphics.RenderGraphModule; + +/// +/// Represents a resource barrier that needs to be inserted. +/// For D3D12 aliasing barriers: ResourceBefore is the old resource, ResourceAfter is the new resource. +/// +internal struct ResourceBarrier +{ + [StructLayout(LayoutKind.Explicit)] + private struct barrier_union + { + internal struct barrier_union_transition + { + public Identifier resource; + public ResourceState stateBefore; + public ResourceState stateAfter; + } + + internal struct barrier_union_aliasing + { + public Identifier resourceBefore; + public Identifier resourceAfter; + } + + // TODO: union can not have non-blittable types + + [FieldOffset(0)] + public barrier_union_transition transition; + [FieldOffset(0)] + public barrier_union_aliasing aliasing; + } + + private barrier_union _union; + + public BarrierType Type + { + get; init; + } + + public int PassIndex + { + get; init; + } + + // For Transition and UAV barriers + public readonly Identifier Resource => _union.transition.resource; + public readonly ResourceState StateBefore => _union.transition.stateBefore; + public readonly ResourceState StateAfter => _union.transition.stateAfter; + + // For Aliasing barriers (D3D12_RESOURCE_BARRIER::Aliasing) + public readonly Identifier ResourceBefore => _union.aliasing.resourceBefore; + public readonly Identifier ResourceAfter => _union.aliasing.resourceAfter; + + // Constructor for Aliasing barriers + public static ResourceBarrier CreateAliasingBarrier( + Identifier resourceBefore, + Identifier resourceAfter, + int passIndex) + { + return new ResourceBarrier + { + Type = BarrierType.Aliasing, + PassIndex = passIndex, + _union = new barrier_union + { + aliasing = new barrier_union.barrier_union_aliasing + { + resourceBefore = resourceBefore, + resourceAfter = resourceAfter + } + } + }; + } + + public static ResourceBarrier CreateTransitionBarrier( + Identifier resource, + ResourceState before, + ResourceState after, + int passIndex) + { + return new ResourceBarrier + { + Type = BarrierType.Transition, + PassIndex = passIndex, + _union = new barrier_union + { + transition = new barrier_union.barrier_union_transition + { + resource = resource, + stateBefore = before, + stateAfter = after + } + } + }; + } +} + +/// +/// Tracks the current state of a resource across passes. +/// +internal sealed class ResourceStateTracker +{ + public int resourceIndex; + public ResourceState currentState = ResourceState.Common; + public int lastAccessPass = -1; + + public void Reset() + { + resourceIndex = -1; + currentState = ResourceState.Common; + lastAccessPass = -1; + } +} diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphBlackboard.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphBlackboard.cs new file mode 100644 index 0000000..b7d3d55 --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphBlackboard.cs @@ -0,0 +1,62 @@ +namespace Ghost.Graphics.RenderGraphModule; + +/// +/// Blackboard for sharing data between render passes. +/// Uses a dictionary with type keys to store different pass data types. +/// Avoids allocations by reusing the same dictionary across frames. +/// +public sealed class RenderGraphBlackboard +{ + private readonly Dictionary _data = new(16); + + /// + /// Adds or updates pass data in the blackboard. + /// + public void Add(T data) + where T : class, IPassData + { + var type = typeof(T); + _data[type] = data; + } + + /// + /// Retrieves pass data from the blackboard. + /// + public T Get() + where T : class, IPassData + { + var type = typeof(T); + if (_data.TryGetValue(type, out var obj)) + { + return (T)obj; + } + + throw new KeyNotFoundException($"Pass data of type {type.Name} not found in blackboard"); + } + + /// + /// Tries to get pass data from the blackboard. + /// + public bool TryGet(out T? data) + where T : class, IPassData + { + var type = typeof(T); + if (_data.TryGetValue(type, out var obj)) + { + data = (T)obj; + return true; + } + + data = null; + return false; + } + + /// + /// Clears all data from the blackboard. + /// Does not deallocate the backing dictionary to avoid allocations. + /// + public void Clear() + { + _data.Clear(); + } +} diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphBuilder.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphBuilder.cs new file mode 100644 index 0000000..e578322 --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphBuilder.cs @@ -0,0 +1,318 @@ +using Ghost.Core; +using Ghost.Graphics.RHI; +using System.Diagnostics; + +namespace Ghost.Graphics.RenderGraphModule; + +[Flags] +public enum AccessFlags : byte +{ + None = 0, + Read = 1 << 0, + Write = 1 << 1, + Discard = 1 << 2, + + WriteAll = Write | Discard, + ReadWrite = Read | Write, +} + +public interface IRenderGraphBuilder : IDisposable +{ + /// + /// Enables or disables pass culling for the current context. + /// + /// A value indicating whether pass culling is allowed. + void AllowPassCulling(bool value); + + /// + /// Creates a new texture resource based on the specified desc. + /// + /// A structure that defines the properties and configuration of the texture to create. + /// The name of the texture resource. + /// An identifier for the newly created texture resource. + Identifier CreateTexture(in RGTextureDesc desc, string name); + + /// + /// Creates a new buffer resource based on the specified desc. + /// + /// A structure that defines the properties and configuration of the buffer to create. + /// The name of the buffer resource. + /// An identifier for the newly created buffer resource. + Identifier CreateBuffer(in BufferDesc desc, string name); + + /// + /// Registers the specified texture for use in the current render graph pass with the given access mode. + /// + /// The identifier of the texture to be used in the render graph pass. + /// The access mode specifying how the texture will be read or written during the pass. + /// An identifier for the texture. + Identifier UseTexture(Identifier texture, AccessFlags accessMode); + + /// + /// Registers the specified buffer for use in the current render graph pass with the given access mode. + /// + /// The identifier of the buffer to be used in the render graph pass. + /// The access mode specifying how the buffer will be read or written during the pass. + /// Optional hint about how the buffer will be used (e.g., IndirectArgument). Default is None (ByteAddressBuffer SRV). + /// An identifier for the buffer. + Identifier UseBuffer(Identifier buffer, AccessFlags accessMode, BufferHint hint = BufferHint.None); +} + +public interface IRasterRenderGraphBuilder : IRenderGraphBuilder +{ + /// + /// Binds a texture for random access operations within the current rendering pass. + /// + /// The identifier of the texture to be used for random access. + /// An identifier for the texture. + Identifier UseRandomAccessTexture(Identifier texture); + /// + /// Specifies that the given buffer will be used for random access operations with the specified access mode within the current context. + /// + /// An identifier for the buffer to be used for random access. Must reference a valid buffer resource. + /// An identifier for the buffer. + Identifier UseRandomAccessBuffer(Identifier buffer); + + /// + /// Sets the color attachment at the specified index to the given texture. + /// + /// The identifier of the texture to use as the color attachment. + /// The zero-based index of the color attachment to set. + /// Access flags. Default is Write (assumes partial update). Use WriteAll for fullscreen passes. + void SetColorAttachment(Identifier texture, int index, AccessFlags flags = AccessFlags.Write); + + /// + /// Sets the depth attachment for the current render pass using the specified texture. + /// + /// The identifier of the texture to use as the depth attachment. Cannot be null. + /// Access flags. Default is ReadWrite (assumes partial update). Use WriteAll for fullscreen passes. + void SetDepthAttachment(Identifier texture, AccessFlags flags = AccessFlags.ReadWrite); + + /// + /// Sets the function used to render a pass with the specified pass data and render context. + /// + /// The type of data associated with the render pass. + /// The delegate that defines the rendering logic for the pass. + void SetRenderFunc(Action renderFunc) + where TPassData : class, new(); +} + +public interface IComputeRenderGraphBuilder : IRenderGraphBuilder +{ + /// + /// Enables or disables asynchronous compute operations. + /// + /// true to enable asynchronous compute; otherwise, false. + void EnableAsyncCompute(bool value); + + /// + /// Sets the render function to be invoked during the compute rendering process. + /// + /// The type of the data object passed to the render function. + /// The delegate that defines the rendering logic to execute. + void SetRenderFunc(Action renderFunc) + where TPassData : class, new(); +} + +public interface IUnsafeRenderGraphBuilder : IRenderGraphBuilder +{ + /// + /// Binds a texture for random access operations within the current rendering pass. + /// + /// The identifier of the texture to be used for random access. + /// An identifier for the texture. + Identifier UseRandomAccessTexture(Identifier texture); + /// + /// Specifies that the given buffer will be used for random access operations with the specified access mode within the current context. + /// + /// An identifier for the buffer to be used for random access. Must reference a valid buffer resource. + /// An identifier for the buffer. + Identifier UseRandomAccessBuffer(Identifier buffer); + + /// + /// Sets the function used to render a pass with the specified pass data and render context. + /// + /// The type of data associated with the render pass. + /// The delegate that defines the rendering logic for the pass. + void SetRenderFunc(Action renderFunc) + where TPassData : class, new(); +} + +internal class RenderGraphBuilder : IRasterRenderGraphBuilder, IComputeRenderGraphBuilder, IUnsafeRenderGraphBuilder +{ + + private RenderGraph _graph = null!; + private RenderGraphPassBase _pass = null!; + private RenderGraphResourceRegistry _resources = null!; + private bool _disposed; + + internal void Init(RenderGraph graph, RenderGraphPassBase pass, RenderGraphResourceRegistry resources) + { + _graph = graph; + _pass = pass; + _resources = resources; + _disposed = false; + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + private Identifier UseResource(Identifier resource, AccessFlags accessFlags, RenderGraphResourceType type) + { + if (accessFlags.HasFlag(AccessFlags.Read)) + { + _pass.resourceReads[(int)type].Add(resource); + _resources.AddConsumer(resource, _pass.index); + } + + if (accessFlags.HasFlag(AccessFlags.Write)) + { + _pass.resourceWrites[(int)type].Add(resource); + _resources.SetProducer(resource, _pass.index); + } + + return resource; + } + + public void AllowPassCulling(bool value) + { + _pass.allowCulling = value; + } + + public void EnableAsyncCompute(bool value) + { + _pass.asyncCompute = value; + } + + public Identifier CreateTexture(in RGTextureDesc desc, string name) + { + ThrowIfDisposed(); + + var handle = _resources.CreateTexture(in desc, name); + _pass.resourceCreates[(int)RenderGraphResourceType.Texture].Add(handle.AsResource()); + _resources.SetProducer(handle.AsResource(), _pass.index); + return handle; + } + + public Identifier CreateBuffer(in BufferDesc desc, string name) + { + ThrowIfDisposed(); + + var handle = _resources.CreateBuffer(in desc, name); + _pass.resourceCreates[(int)RenderGraphResourceType.Buffer].Add(handle.AsResource()); + _resources.SetProducer(handle.AsResource(), _pass.index); + return handle; + } + + public Identifier UseTexture(Identifier texture, AccessFlags flags) + { + ThrowIfDisposed(); + return UseResource(texture.AsResource(), flags, RenderGraphResourceType.Texture).AsTexture(); + } + + public Identifier UseBuffer(Identifier buffer, AccessFlags flags, BufferHint hint = BufferHint.None) + { + ThrowIfDisposed(); + + // Store buffer hint if not None + if (hint != BufferHint.None) + { + _pass.bufferHints[buffer.AsResource().Value] = hint; + } + + return UseResource(buffer.AsResource(), flags, RenderGraphResourceType.Buffer).AsBuffer(); + } + + public Identifier UseRandomAccessTexture(Identifier texture) + { + ThrowIfDisposed(); + + var resource = texture.AsResource(); + UseResource(resource, AccessFlags.ReadWrite, RenderGraphResourceType.Texture); + _pass.randomAccess.Add(resource); + return texture; + } + + public Identifier UseRandomAccessBuffer(Identifier buffer) + { + ThrowIfDisposed(); + + var resource = buffer.AsResource(); + UseResource(resource, AccessFlags.ReadWrite, RenderGraphResourceType.Buffer); + _pass.randomAccess.Add(resource); + return buffer; + } + + public void SetColorAttachment(Identifier texture, int index, AccessFlags flags = AccessFlags.Write) + { + ThrowIfDisposed(); + + Debug.Assert(index >= 0 && index < _pass.colorAccess.Length, "Color attachment index out of range."); + + var id = UseTexture(texture, flags); + if (_pass.colorAccess[index].id == id || _pass.colorAccess[index].id.IsInvalid) + { + _pass.maxColorIndex = Math.Max(_pass.maxColorIndex, index); + _pass.colorAccess[index] = new TextureAccess(id, flags); + } + else + { + throw new InvalidOperationException($"Color attachment at index {index} is already set to a different texture."); + } + } + + public void SetDepthAttachment(Identifier texture, AccessFlags flags = AccessFlags.Write) + { + ThrowIfDisposed(); + + var id = UseTexture(texture, flags); + if (_pass.depthAccess.id == id || _pass.depthAccess.id.IsInvalid) + { + _pass.depthAccess = new TextureAccess(id, flags); + } + else + { + throw new InvalidOperationException("Depth attachment is already set to a different texture."); + } + } + + public void SetRenderFunc(Action renderFunc) + where TPassData : class, new() + { + ((RasterRenderGraphPass)_pass).renderFunc = renderFunc; + } + + public void SetRenderFunc(Action renderFunc) + where TPassData : class, new() + { + ((ComputeRenderGraphPass)_pass).renderFunc = renderFunc; + } + + public void SetRenderFunc(Action renderFunc) + where TPassData : class, new() + { + ((UnsafeRenderGraphPass)_pass).renderFunc = renderFunc; + } + + public void Dispose() + + { + if (_disposed) + { + return; + } + + if (!_pass.HasRenderFunc()) + { + throw new InvalidOperationException("RenderGraphBuilder must be disposed after setting up the render function."); + } + + _graph = null!; + _pass = null!; + _resources = null!; + + _disposed = true; + } +} diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphCompilationCache.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphCompilationCache.cs new file mode 100644 index 0000000..337d472 --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphCompilationCache.cs @@ -0,0 +1,154 @@ +using Ghost.Core; +using Ghost.Graphics.Core; +using Ghost.Graphics.RHI; +using System.Diagnostics.CodeAnalysis; + +namespace Ghost.Graphics.RenderGraphModule; + +/// +/// Represents cached compilation results for a render graph. +/// This avoids recompiling the graph when the structure hasn't changed. +/// +internal sealed class CachedCompilation +{ + // Compiled pass indices (indices into the _passes list) + public readonly List compiledPassIndices = new(64); + + // Culling decisions for each pass + public readonly List passCulledFlags = new(64); + + // Physical resource aliasing mappings (logical index -> physical index) + public readonly Dictionary logicalToPhysical = new(128); + + // Placed resource metadata + public readonly List placedResources = new(32); + + // Resource barriers + public readonly List barriers = new(128); + + // Resource state mappings (for barrier generation) + public readonly Dictionary resourceStates = new(128); + + // Real gpu resource + public readonly List> backingResources = new(32); + + // View state used for this compilation + public ViewState viewState; + + public void Clear() + { + compiledPassIndices.Clear(); + passCulledFlags.Clear(); + logicalToPhysical.Clear(); + placedResources.Clear(); + barriers.Clear(); + resourceStates.Clear(); + backingResources.Clear(); + viewState = default; + } +} + +/// +/// Placed resource data for caching. +/// +internal struct PlacedResourceData +{ + public int index; + public RenderGraphResourceType type; + public ulong heapOffset; + public ulong sizeInBytes; + public int firstUsePass; + public int lastUsePass; +} + +/// +/// Manages compilation caching for render graphs. +/// Stores compiled results and allows cache hits when graph structure is unchanged. +/// +internal sealed class RenderGraphCompilationCache +{ + private readonly CachedCompilation _cached = new(); + private ulong _cachedHash; + private bool _hasCachedData; + + // Statistics + public int CacheHits { get; private set; } + public int CacheMisses { get; private set; } + + /// + /// Attempts to retrieve cached compilation results. + /// + public bool TryGetCached(ulong hash, [MaybeNullWhen(false)] out CachedCompilation result) + { + if (_hasCachedData && _cachedHash == hash) + { + result = _cached; + CacheHits++; + return true; + } + + result = null; + CacheMisses++; + return false; + } + + /// + /// Stores compilation results in the cache. + /// + public void Store(ulong hash, CachedCompilation data) + { + _cachedHash = hash; + _hasCachedData = true; + + // Deep copy the data + _cached.Clear(); + + _cached.compiledPassIndices.AddRange(data.compiledPassIndices); + _cached.passCulledFlags.AddRange(data.passCulledFlags); + + foreach (var kvp in data.logicalToPhysical) + { + _cached.logicalToPhysical[kvp.Key] = kvp.Value; + } + + _cached.placedResources.AddRange(data.placedResources); + _cached.barriers.AddRange(data.barriers); + + foreach (var kvp in data.resourceStates) + { + _cached.resourceStates[kvp.Key] = kvp.Value; + } + + _cached.backingResources.AddRange(data.backingResources); + } + + /// + /// Invalidates the cache, forcing recompilation on next Compile(). + /// + public void Invalidate() + { + _hasCachedData = false; + _cachedHash = 0; + _cached.Clear(); + } + + public void UpdateBackingResource(int logicalIndex, Handle resource) + { + if (logicalIndex < 0 || logicalIndex >= _cached.backingResources.Count) + { + return; + } + + _cached.backingResources[logicalIndex] = resource; + } + + /// + /// Gets cache statistics for debugging. + /// + public (int hits, int misses, double hitRate) GetStatistics() + { + int total = CacheHits + CacheMisses; + double hitRate = total > 0 ? (double)CacheHits / total : 0.0; + return (CacheHits, CacheMisses, hitRate); + } +} diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphContext.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphContext.cs new file mode 100644 index 0000000..83375ae --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphContext.cs @@ -0,0 +1,198 @@ +using Ghost.Core; +using Ghost.Graphics.Contracts; +using Ghost.Graphics.Core; +using Ghost.Graphics.RHI; +using Misaki.HighPerformance.Mathematics; + +namespace Ghost.Graphics.RenderGraphModule; + +public interface IRenderGraphContext +{ + IResourceDatabase ResourceDatabase { get; } + + Handle GetActualResource(Identifier resource); + Handle GetActualTexture(Identifier texture); + Handle GetActualBuffer(Identifier buffer); +} + +public interface IRasterRenderContext : IRenderGraphContext +{ + int ActiveMeshIndexCount { get; } + + void SetActiveMaterial(Handle material); + void SetActiveMaterial(ref readonly Material material); + void SetActiveMesh(Handle mesh); + void SetActiveMesh(ref readonly Mesh mesh); + void DispatchMesh(uint3 threadGroupCount); +} + +public interface IComputeRenderContext : IRenderGraphContext +{ + void DispatchCompute(uint3 threadGroupCount); +} + +public interface IUnsafeRenderContext : IRasterRenderContext, IRenderGraphContext +{ + ICommandBuffer CommandBuffer { get; } +} + +internal sealed class RenderGraphContext : IRasterRenderContext, IComputeRenderContext, IUnsafeRenderContext +{ + private readonly IResourceDatabase _resourceDatabase; + private readonly IPipelineLibrary _pipelineLibrary; + private readonly IShaderCompiler _shaderCompiler; + private readonly RenderGraphResourceRegistry _resources; + + private ICommandBuffer _commandBuffer = null!; + + private readonly TextureFormat[] _rtvFormats; + private TextureFormat _dsvFormat; + + private Handle _activePerMaterialData; + private Handle _activePerMeshData; + private int _activeMeshIndexCount; + + public IResourceDatabase ResourceDatabase => _resourceDatabase; + + public int ActiveMeshIndexCount => _activeMeshIndexCount; + + public ICommandBuffer CommandBuffer => _commandBuffer; + + internal RenderGraphContext(IResourceDatabase resourceDatabase, IPipelineLibrary pipelineLibrary, IShaderCompiler shaderCompiler, RenderGraphResourceRegistry resources) + { + _resourceDatabase = resourceDatabase; + _pipelineLibrary = pipelineLibrary; + _shaderCompiler = shaderCompiler; + _resources = resources; + + _rtvFormats = new TextureFormat[RHIUtility.MAX_RENDER_TARGETS]; + _dsvFormat = TextureFormat.Unknown; + } + + internal void SetCommandBuffer(ICommandBuffer commandBuffer) + { + _commandBuffer = commandBuffer; + } + + internal void SetRenderTargetFormats(ReadOnlySpan rtvFormats, TextureFormat dsvFormat) + { + for (int i = 0; i < RHIUtility.MAX_RENDER_TARGETS; i++) + { + _rtvFormats[i] = i < rtvFormats.Length ? rtvFormats[i] : TextureFormat.Unknown; + } + + _dsvFormat = dsvFormat; + } + + public Handle GetActualResource(Identifier resource) + { + return _resources.GetResource(resource).backingResource; + } + + public Handle GetActualTexture(Identifier texture) + { + return _resources.GetResource(texture.AsResource()).backingResource.AsTexture(); + } + + public Handle GetActualBuffer(Identifier buffer) + { + return _resources.GetResource(buffer.AsResource()).backingResource.AsGraphicsBuffer(); + } + + public void SetActiveMaterial(Handle material) + { + var r = _resourceDatabase.GetMaterialReference(material); + if (r.IsFailure) + { + _activePerMaterialData = Handle.Invalid; + return; + } + + ref readonly var mat = ref r.Value; + SetActiveMaterial(in mat); + } + + public void SetActiveMaterial(ref readonly Material material) + { + var shaderResult = _resourceDatabase.GetShaderReference(material.Shader); + if (shaderResult.IsFailure) + { + _activePerMaterialData = Handle.Invalid; + return; + } + + ref readonly var shader = ref shaderResult.Value; + ref readonly var pass = ref shader.GetPassReference(material.ActivePassIndex); + + var passPipelineHash = new PassPipelineHash(_rtvFormats, _dsvFormat); + var materialPipeline = material.GetPassPipelineOverride(material.ActivePassIndex); + + // Mask out the keywords that are not used in this pass. + var variantMask = material._keywordMask & pass.KeywordIDs; + var shaderVariantKey = RHIUtility.CreateShaderVariantKey(pass.Key, in variantMask); + var pipelineKey = RHIUtility.CreateGraphicsPipelineKey(shaderVariantKey, materialPipeline, passPipelineHash); + + if (!_pipelineLibrary.HasPipeline(pipelineKey)) + { + var compiledCacheResult = _shaderCompiler.LoadCompiledCache(shaderVariantKey); + if (compiledCacheResult.IsFailure) + { + throw new InvalidOperationException("Failed to load compiled shader cache for pipeline state object creation."); + } + + var psoDes = new GraphicsPSODescriptor + { + VariantKey = shaderVariantKey, + PipelineOption = materialPipeline, + + RtvFormats = _rtvFormats, + DsvFormat = _dsvFormat, + }; + + var compiled = compiledCacheResult.Value; + _pipelineLibrary.CompilePSO(in psoDes, in compiled).GetValueOrThrow(); + } + + _activePerMaterialData = material._cBufferCache.GpuResource; + _commandBuffer.SetPipelineState(pipelineKey); + } + + public void SetActiveMesh(Handle mesh) + { + var r = _resourceDatabase.GetMeshReference(mesh); + if (r.IsFailure) + { + _activePerMeshData = Handle.Invalid; + _activeMeshIndexCount = 0; + return; + } + + ref readonly var meshRef = ref r.Value; + SetActiveMesh(in meshRef); + } + + public void SetActiveMesh(ref readonly Mesh mesh) + { + _activePerMeshData = mesh.ObjectDataBuffer; + _activeMeshIndexCount = mesh.IndexCount; + } + + public unsafe void DispatchMesh(uint3 threadGroupCount) + { + // TODO: Global and view constants + var data = new PushConstantsData + { + objectIndex = _resourceDatabase.GetBindlessIndex(_activePerMeshData.AsResource()), + materialIndex = _resourceDatabase.GetBindlessIndex(_activePerMaterialData.AsResource()), + }; + + var pushConstantSpan = new ReadOnlySpan(&data, sizeof(PushConstantsData) / sizeof(uint)); + _commandBuffer.SetGraphicsRoot32Constants(RootSignatureLayout.PUSH_CONSTANT_SLOT, pushConstantSpan); + _commandBuffer.DispatchMesh(threadGroupCount.x, threadGroupCount.y, threadGroupCount.z); + } + + public void DispatchCompute(uint3 threadGroupCount) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphNativePass.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphNativePass.cs new file mode 100644 index 0000000..6ce926c --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphNativePass.cs @@ -0,0 +1,52 @@ +using Ghost.Core; + +namespace Ghost.Graphics.RenderGraphModule; + +/// +/// Represents a native render pass that can contain multiple merged logical passes. +/// Maps to D3D12 BeginRenderPass/EndRenderPass or Vulkan vkCmdBeginRenderPass/vkCmdEndRenderPass. +/// +internal sealed class NativeRenderPass +{ + public int index; + + /// + /// Indices of logical passes merged into this native render pass. + /// + public readonly List mergedPassIndices = new(4); + + /// + /// Color attachments shared across all merged passes. + /// + public RenderTargetInfo[] colorAttachments = new RenderTargetInfo[8]; + public int colorAttachmentCount; + + /// + /// Depth-stencil attachment (optional). + /// + public DepthStencilInfo depthAttachment; + public bool hasDepthAttachment; + + /// + /// Range of logical passes included in this native pass. + /// + public int firstLogicalPass; + public int lastLogicalPass; + + /// + /// Whether UAV writes are allowed during this render pass. + /// + public bool allowUAVWrites; + + public void Reset() + { + index = -1; + mergedPassIndices.Clear(); + colorAttachmentCount = 0; + hasDepthAttachment = false; + depthAttachment = default; + firstLogicalPass = int.MaxValue; + lastLogicalPass = -1; + allowUAVWrites = false; + } +} diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphPass.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphPass.cs new file mode 100644 index 0000000..abd3a46 --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphPass.cs @@ -0,0 +1,172 @@ +using Ghost.Core; +using System.Runtime.CompilerServices; + +namespace Ghost.Graphics.RenderGraphModule; + +/// +/// Represents different types of render passes. +/// +public enum RenderPassType : byte +{ + Raster, + Compute, + Unsafe +} + + +/// +/// Base class for render passes. +/// Uses pooling to avoid allocations after the first frame. +/// +internal abstract class RenderGraphPassBase +{ + public string name = string.Empty; + public int index; + public RenderPassType type; + public bool allowCulling = true; + public bool asyncCompute; + + public TextureAccess depthAccess; + public TextureAccess[] colorAccess = new TextureAccess[8]; + public int maxColorIndex = -1; + + public List> randomAccess = new(8); + + // Resource dependencies + public readonly List>[] resourceReads = new List>[(int)RenderGraphResourceType.Count]; + public readonly List>[] resourceWrites = new List>[(int)RenderGraphResourceType.Count]; + public readonly List>[] resourceCreates = new List>[(int)RenderGraphResourceType.Count]; + + // Buffer usage hints (maps buffer resource ID to hint) + public readonly Dictionary bufferHints = new(8); + + // Execution state + public bool culled; + public bool hasSideEffects; + + public RenderGraphPassBase() + { + for (int i = 0; i < (int)RenderGraphResourceType.Count; i++) + { + resourceReads[i] = new List>(8); + resourceWrites[i] = new List>(4); + resourceCreates[i] = new List>(4); + } + } + + public abstract void Execute(RenderGraphContext context); + public abstract bool HasRenderFunc(); + public abstract int GetRenderFuncHashCode(); + + public virtual void Reset(RenderGraphObjectPool pool) + { + name = string.Empty; + index = -1; + type = RenderPassType.Raster; + allowCulling = true; + asyncCompute = false; + + depthAccess = default; + colorAccess.AsSpan().Clear(); + maxColorIndex = -1; + + randomAccess.Clear(); + + for (var i = 0; i < (int)RenderGraphResourceType.Count; i++) + { + resourceReads[i].Clear(); + resourceWrites[i].Clear(); + resourceCreates[i].Clear(); + } + + bufferHints.Clear(); + + culled = false; + hasSideEffects = false; + } +} + +internal abstract class RenderGraphPass : RenderGraphPassBase + where TPassData : class, new() +{ + public TPassData passData = null!; + public Action? renderFunc; + + public void Init(int index, TPassData passData, string name, RenderPassType type) + { + this.index = index; + this.passData = passData; + this.name = name; + this.type = type; + } + + public sealed override bool HasRenderFunc() + { + return renderFunc != null; + } + + public override int GetRenderFuncHashCode() + { + if (renderFunc == null) + { + return 0; + } + + var methodHashCode = RuntimeHelpers.GetHashCode(renderFunc.Method); + return renderFunc.Target == null ? methodHashCode : methodHashCode ^ RuntimeHelpers.GetHashCode(renderFunc.Target); // static deleget does not have target + } + + public override void Reset(RenderGraphObjectPool pool) + { + base.Reset(pool); + pool.Return(passData); + + passData = null!; + renderFunc = null; + } +} + +internal sealed class RasterRenderGraphPass : RenderGraphPass + where TPassData : class, new() +{ + public override void Execute(RenderGraphContext context) + { + renderFunc!(passData, context); + } + + public override void Reset(RenderGraphObjectPool pool) + { + base.Reset(pool); + pool.Return(this); + } +} + +internal sealed class ComputeRenderGraphPass : RenderGraphPass + where TPassData : class, new() +{ + public override void Execute(RenderGraphContext context) + { + renderFunc!(passData, context); + } + + public override void Reset(RenderGraphObjectPool pool) + { + base.Reset(pool); + pool.Return(this); + } +} + +internal sealed class UnsafeRenderGraphPass : RenderGraphPass + where TPassData : class, new() +{ + public override void Execute(RenderGraphContext context) + { + renderFunc!(passData, context); + } + + public override void Reset(RenderGraphObjectPool pool) + { + base.Reset(pool); + pool.Return(this); + } +} diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphResourcePool.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphResourcePool.cs new file mode 100644 index 0000000..6ebfec1 --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphResourcePool.cs @@ -0,0 +1,311 @@ +using Ghost.Core; +using Ghost.Graphics.Core; +using Ghost.Graphics.RHI; +using Misaki.HighPerformance.Buffer; + +namespace Ghost.Graphics.RenderGraphModule; + +/// +/// Object pool for reusing allocated objects across frames. +/// This is key to minimizing GC allocations after the first frame. +/// +internal sealed class RenderGraphObjectPool +{ + private static readonly List s_allocatedPools = new(); + + private class SharedObjectPoolBase + { + public SharedObjectPoolBase() { } + public virtual void Clear() { } + } + + private class SharedObjectPool : SharedObjectPoolBase where T : class, new() + { + private static readonly ObjectPool s_pool = AllocatePool(); + + private static ObjectPool AllocatePool() + { + var newPool = new ObjectPool(() => new T(), null); + // Storing instance to clear the static pool of the same type if needed + s_allocatedPools.Add(new SharedObjectPool()); + return newPool; + } + + /// + /// Clear the pool using SharedObjectPool instance. + /// + /// + public override void Clear() + { + s_pool.Reset(); + } + + /// + /// Rent a new instance from the pool. + /// + /// + // FIX: ObjectPool.Rent() has a critical bug that it will put the newly created object into the pool directly and give out the same instance again. + // This will cause multiple renters to get the same instance. + public static T Rent() => s_pool.Rent(); + + /// + /// Return an object to the pool. + /// + /// instance to release. + public static void Return(T toRelease) => s_pool.Return(toRelease); + } + + public T Rent() + where T : class, new() + { + return SharedObjectPool.Rent(); + } + + public void Return(T obj) + where T : class, new() + { + SharedObjectPool.Return(obj); + } + + public void Clear() + { + for (var i = 0; i < s_allocatedPools.Count; i++) + { + s_allocatedPools[i].Clear(); + } + } +} + +/// +/// Represents a resource in the render graph (texture or buffer). +/// +internal sealed class RenderGraphResource +{ + public string name = string.Empty; + + public int index; + public RenderGraphResourceType type; + + // Resource descriptors (only one is valid based on type) + public RGTextureDesc rgTextureDesc; + public BufferDesc bufferDesc; + + // Resolved dimensions (computed from rgTextureDesc + ViewState for textures) + public uint resolvedWidth; + public uint resolvedHeight; + + public bool isImported; + public int firstUsePass = -1; + public int lastUsePass = -1; + public int producerPass = -1; + public List consumerPasses = new(4); + public int refCount; + + public Handle backingResource = Handle.Invalid; + + public void Reset() + { + name = string.Empty; + + type = RenderGraphResourceType.Texture; + index = -1; + rgTextureDesc = default; + bufferDesc = default; + resolvedWidth = 0; + resolvedHeight = 0; + isImported = false; + firstUsePass = -1; + lastUsePass = -1; + producerPass = -1; + consumerPasses.Clear(); + refCount = 0; + } +} + +/// +/// Registry for managing all resources in the render graph. +/// Uses pooling to minimize allocations after the first frame. +/// Uses a single unified list for both textures and buffers with global indexing. +/// +internal sealed class RenderGraphResourceRegistry +{ + private readonly RenderGraphObjectPool _pool; + private readonly List _resources; + + internal IReadOnlyList Resources => _resources; + + public RenderGraphResourceRegistry(RenderGraphObjectPool pool) + { + _pool = pool; + _resources = new List(64); + } + + public int ResourceCount => _resources.Count; + public int TextureResourceCount + { + get + { + int count = 0; + for (int i = 0; i < _resources.Count; i++) + { + if (_resources[i].type == RenderGraphResourceType.Texture) + count++; + } + return count; + } + } + public int BufferResourceCount + { + get + { + int count = 0; + for (int i = 0; i < _resources.Count; i++) + { + if (_resources[i].type == RenderGraphResourceType.Buffer) + count++; + } + return count; + } + } + + public void BeginFrame() + { + // Return all resources to pool + for (var i = 0; i < _resources.Count; i++) + { + _pool.Return(_resources[i]); + } + + _resources.Clear(); + } + + public Identifier ImportTexture(ref readonly TextureDesc desc, Handle texture, string name) + { + var resource = _pool.Rent(); + resource.name = name; + resource.type = RenderGraphResourceType.Texture; + resource.index = _resources.Count; + resource.rgTextureDesc = RGTextureDesc.FromTextureDesc(in desc); + resource.isImported = true; + resource.backingResource = texture.AsResource(); + resource.resolvedWidth = desc.Width; + resource.resolvedHeight = desc.Height; + + _resources.Add(resource); + + return new Identifier(resource.index); + } + + public Identifier CreateTexture(ref readonly RGTextureDesc desc, string name) + { + var resource = _pool.Rent(); + resource.name = name; + resource.type = RenderGraphResourceType.Texture; + resource.index = _resources.Count; + resource.rgTextureDesc = desc; + resource.isImported = false; + + _resources.Add(resource); + + return new Identifier(resource.index); + } + + public Identifier ImportBuffer(ref readonly BufferDesc desc, Handle buffer, string name) + { + var resource = _pool.Rent(); + resource.name = name; + resource.type = RenderGraphResourceType.Buffer; + resource.index = _resources.Count; + resource.bufferDesc = desc; + resource.isImported = true; + resource.backingResource = buffer.AsResource(); + + _resources.Add(resource); + + return new Identifier(resource.index); + } + + public Identifier CreateBuffer(ref readonly BufferDesc desc, string name) + { + var resource = _pool.Rent(); + resource.name= name; + resource.type = RenderGraphResourceType.Buffer; + resource.index = _resources.Count; + resource.bufferDesc = desc; + resource.isImported = false; + + _resources.Add(resource); + + return new Identifier(resource.index); + } + + public RenderGraphResource GetResource(Identifier resource) + { + return _resources[resource.Value]; + } + + public RenderGraphResource GetResource(Identifier texture) + { + return _resources[texture.Value]; + } + + public RenderGraphResource GetResource(Identifier buffer) + { + return _resources[buffer.Value]; + } + + /// + /// Gets resource by global index. Use this when iterating over all resources. + /// + public RenderGraphResource GetResourceByIndex(int index) + { + return _resources[index]; + } + + public void SetProducer(Identifier resourceID, int passIndex) + { + var resource = GetResource(resourceID); + resource.producerPass = passIndex; + if (resource.firstUsePass < 0) + { + resource.firstUsePass = passIndex; + } + } + + public void AddConsumer(Identifier resourceID, int passIndex) + { + var resource = GetResource(resourceID); + resource.consumerPasses.Add(passIndex); + resource.lastUsePass = passIndex; + if (resource.firstUsePass < 0) + { + resource.firstUsePass = passIndex; + } + } + + /// + /// Resolves texture sizes based on current view state. + /// Must be called after all resources are created and before compilation. + /// + internal void ResolveTextureSizes(in ViewState viewState) + { + for (var i = 0; i < _resources.Count; i++) + { + var res = _resources[i]; + if (res.type != RenderGraphResourceType.Texture || res.isImported) + continue; + + var desc = res.rgTextureDesc; + if (desc.sizeMode == RGTextureSizeMode.Absolute) + { + res.resolvedWidth = desc.width; + res.resolvedHeight = desc.height; + } + else // Relative + { + res.resolvedWidth = (uint)(desc.scaleX * viewState.viewportWidth); + res.resolvedHeight = (uint)(desc.scaleY * viewState.viewportHeight); + } + } + } +} diff --git a/Ghost.Graphics/RenderGraphModule/RenderGraphTypes.cs b/Ghost.Graphics/RenderGraphModule/RenderGraphTypes.cs new file mode 100644 index 0000000..1b773fe --- /dev/null +++ b/Ghost.Graphics/RenderGraphModule/RenderGraphTypes.cs @@ -0,0 +1,500 @@ +using Ghost.Core; +using Ghost.Graphics.Core; +using Ghost.Graphics.RHI; +using System.Runtime.CompilerServices; + +namespace Ghost.Graphics.RenderGraphModule; + +internal enum RenderGraphResourceType : int +{ + Texture, + Buffer, + // AccelerationStructure, + Count +} + +/// +/// Specifies how texture dimensions are determined. +/// +public enum RGTextureSizeMode : byte +{ + /// + /// Fixed pixel dimensions (width, height). + /// + Absolute, + + /// + /// Scale relative to view state (scaleX * viewportWidth, scaleY * viewportHeight). + /// + Relative +} + +/// +/// View state information for resolving relative texture sizes. +/// +public struct ViewState : IEquatable +{ + public uint viewportWidth; + public uint viewportHeight; + + public ViewState(uint width, uint height) + { + viewportWidth = width; + viewportHeight = height; + } + + public readonly bool Equals(ViewState other) + { + return viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight; + } + + public override readonly bool Equals(object? obj) + { + return obj is ViewState other && Equals(other); + } + + public override readonly int GetHashCode() + { + return HashCode.Combine(viewportWidth, viewportHeight); + } + + public static bool operator ==(ViewState left, ViewState right) + { + return left.Equals(right); + } + + public static bool operator !=(ViewState left, ViewState right) + { + return !left.Equals(right); + } +} + +/// +/// Render graph texture descriptor with support for relative sizing and clear operations. +/// +public struct RGTextureDesc : IEquatable +{ + public RGTextureSizeMode sizeMode; + + // Size specification (union-like - only one set is used based on sizeMode) + public uint width; // For Absolute mode + public uint height; // For Absolute mode + public float scaleX; // For Relative mode + public float scaleY; // For Relative mode + + // Common texture properties + public TextureFormat format; + public TextureDimension dimension; + public uint mipLevels; + public uint slice; + public TextureUsage usage; + + public bool clearAtFirstUse; + public bool discardAtLastUse; + + // Clear operation support + public Color128 clearColor; + + public float clearDepth; + public byte clearStencil; + + /// + /// Creates a texture descriptor with absolute dimensions. + /// + public static RGTextureDesc Absolute( + uint width, + uint height, + TextureFormat format, + Color128 clearColor = default, + bool clearAtFirstUse = true, + bool discardAtLastUse = true, + TextureDimension dimension = TextureDimension.Texture2D, + uint mipLevels = 1, + uint slice = 1, + TextureUsage usage = TextureUsage.RenderTarget | TextureUsage.ShaderResource) + { + return new RGTextureDesc + { + sizeMode = RGTextureSizeMode.Absolute, + width = width, + height = height, + format = format, + clearColor = clearColor, + clearAtFirstUse = clearAtFirstUse, + discardAtLastUse = discardAtLastUse, + clearDepth = 1.0f, + clearStencil = 0, + dimension = dimension, + mipLevels = mipLevels, + slice = slice, + usage = usage + }; + } + + /// + /// Creates a texture descriptor with relative dimensions (uniform scale). + /// + public static RGTextureDesc Relative( + float scale, + TextureFormat format, + Color128 clearColor = default, + bool clearAtFirstUse = true, + bool discardAtLastUse = true, + TextureDimension dimension = TextureDimension.Texture2D, + uint mipLevels = 1, + uint slice = 1, + TextureUsage usage = TextureUsage.RenderTarget | TextureUsage.ShaderResource) + { + return new RGTextureDesc + { + sizeMode = RGTextureSizeMode.Relative, + scaleX = scale, + scaleY = scale, + format = format, + clearColor = clearColor, + clearAtFirstUse = clearAtFirstUse, + discardAtLastUse = discardAtLastUse, + clearDepth = 1.0f, + clearStencil = 0, + dimension = dimension, + mipLevels = mipLevels, + slice = slice, + usage = usage + }; + } + + /// + /// Creates a texture descriptor with relative dimensions (non-uniform scale). + /// + public static RGTextureDesc Relative( + float scaleX, + float scaleY, + TextureFormat format, + Color128 clearColor = default, + bool clearAtFirstUse = true, + bool discardAtLastUse = true, + TextureDimension dimension = TextureDimension.Texture2D, + uint mipLevels = 1, + uint slice = 1, + TextureUsage usage = TextureUsage.RenderTarget | TextureUsage.ShaderResource) + { + return new RGTextureDesc + { + sizeMode = RGTextureSizeMode.Relative, + scaleX = scaleX, + scaleY = scaleY, + format = format, + clearColor = clearColor, + clearAtFirstUse = clearAtFirstUse, + discardAtLastUse = discardAtLastUse, + clearDepth = 1.0f, + clearStencil = 0, + dimension = dimension, + mipLevels = mipLevels, + slice = slice, + usage = usage + }; + } + + + /// + /// Creates a depth texture descriptor with relative dimensions. + /// + public static RGTextureDesc RelativeDepth( + float scale, + float clearDepth = 1.0f, + byte clearStencil = 0, + bool clearAtFirstUse = true, + bool discardAtLastUse = true, + TextureFormat format = TextureFormat.D32_Float) + { + return new RGTextureDesc + { + sizeMode = RGTextureSizeMode.Relative, + scaleX = scale, + scaleY = scale, + format = format, + clearColor = default, + clearDepth = clearDepth, + clearStencil = clearStencil, + clearAtFirstUse = clearAtFirstUse, + discardAtLastUse = discardAtLastUse, + dimension = TextureDimension.Texture2D, + mipLevels = 1, + slice = 1, + usage = TextureUsage.DepthStencil | TextureUsage.ShaderResource + }; + } + + /// + /// Creates an RGTextureDesc from an RHI TextureDesc (for imported textures). + /// + public static RGTextureDesc FromTextureDesc(in TextureDesc desc) + { + return new RGTextureDesc + { + sizeMode = RGTextureSizeMode.Absolute, + width = desc.Width, + height = desc.Height, + format = desc.Format, + clearColor = default, + clearDepth = 1.0f, + clearStencil = 0, + clearAtFirstUse = false, + discardAtLastUse = false, + dimension = desc.Dimension, + mipLevels = desc.MipLevels, + slice = desc.Slice, + usage = desc.Usage + }; + } + + + /// + /// Converts to RHI TextureDesc using resolved dimensions. + /// + public readonly TextureDesc ToTextureDesc(uint resolvedWidth, uint resolvedHeight) + { + return new TextureDesc + { + Width = resolvedWidth, + Height = resolvedHeight, + Format = format, + Dimension = dimension, + MipLevels = mipLevels, + Slice = slice, + Usage = usage + }; + } + + public readonly bool Equals(RGTextureDesc other) + { + return sizeMode == other.sizeMode && + format == other.format && + dimension == other.dimension && + mipLevels == other.mipLevels && + slice == other.slice && + usage == other.usage && + clearAtFirstUse == other.clearAtFirstUse && + discardAtLastUse == other.discardAtLastUse && + (sizeMode == RGTextureSizeMode.Absolute + ? width == other.width && height == other.height + : scaleX == other.scaleX && scaleY == other.scaleY); + } + + + public override readonly bool Equals(object? obj) + { + return obj is RGTextureDesc other && Equals(other); + } + + public override readonly int GetHashCode() + { + if (sizeMode == RGTextureSizeMode.Absolute) + { + return HashCode.Combine(sizeMode, width, height, format, dimension, mipLevels, slice, usage); + } + else + { + return HashCode.Combine(sizeMode, scaleX, scaleY, format, dimension, mipLevels, slice, usage); + } + } + + public static bool operator ==(RGTextureDesc left, RGTextureDesc right) + { + return left.Equals(right); + } + + public static bool operator !=(RGTextureDesc left, RGTextureDesc right) + { + return !left.Equals(right); + } +} + +public struct RGResource; +public struct RGTexture; +public struct RGBuffer; + +public static class RGResourceExtensions +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Identifier AsResource(this Identifier texture) + { + return new Identifier(texture.Value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Identifier AsResource(this Identifier buffer) + { + return new Identifier(buffer.Value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Identifier AsTexture(this Identifier resource) + { + return new Identifier(resource.Value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Identifier AsBuffer(this Identifier resource) + { + return new Identifier(resource.Value); + } +} + +/// +/// Hints for how a buffer will be used in a pass. +/// Used to determine correct resource state transitions. +/// +[Flags] +public enum BufferHint +{ + /// + /// No special usage - buffer will be used as shader resource (SRV) or UAV based on AccessFlags. + /// + None = 0, + + /// + /// Buffer will be used as indirect argument buffer (ExecuteIndirect). + /// Requires ResourceState.IndirectArgument. + /// + IndirectArgument = 1 << 0, +} + +internal readonly struct TextureAccess +{ + public readonly Identifier id; + public readonly AccessFlags accessFlags; + + public TextureAccess(Identifier id, AccessFlags accessFlags) + { + this.id = id; + this.accessFlags = accessFlags; + } +} + +/// +/// Tracks buffer access information including usage hints. +/// +internal readonly struct BufferAccess +{ + public readonly Identifier id; + public readonly AccessFlags accessFlags; + public readonly BufferHint hint; + + public BufferAccess(Identifier id, AccessFlags accessFlags, BufferHint hint = BufferHint.None) + { + this.id = id; + this.accessFlags = accessFlags; + this.hint = hint; + } +} + +///// +///// Descriptor for creating a texture resource. +///// +//public readonly struct TextureDescriptor : IEquatable +//{ +// public readonly int width; +// public readonly int height; +// public readonly TextureFormat format; +// public readonly string name; + +// public TextureDescriptor(int width, int height, TextureFormat format, string name) +// { +// this.width = width; +// this.height = height; +// this.format = format; +// this.name = name; +// } + +// public readonly bool Equals(TextureDescriptor other) +// { +// return width == other.width && +// height == other.height && +// format == other.format && +// name == other.name; +// } + +// public override readonly bool Equals(object? obj) => obj is TextureDescriptor other && Equals(other); +// public override readonly int GetHashCode() => HashCode.Combine(width, height, format, name); + +// public static bool operator ==(TextureDescriptor left, TextureDescriptor right) +// { +// return left.Equals(right); +// } + +// public static bool operator !=(TextureDescriptor left, TextureDescriptor right) +// { +// return !(left == right); +// } +//} + +//public readonly struct BufferDescriptor : IEquatable +//{ +// public readonly uint sizeInBytes; +// public readonly uint stride; +// public readonly BufferUsage usage; +// public readonly string name; + +// public BufferDescriptor(uint sizeInBytes, uint stride, BufferUsage usage, string name) +// { +// this.sizeInBytes = sizeInBytes; +// this.stride = stride; +// this.usage = usage; +// this.name = name; +// } + +// public readonly bool Equals(BufferDescriptor other) +// { +// return sizeInBytes == other.sizeInBytes && +// stride == other.stride && +// usage == other.usage && +// name == other.name; +// } + +// public override readonly bool Equals(object? obj) => obj is BufferDescriptor other && Equals(other); +// public override readonly int GetHashCode() => HashCode.Combine(sizeInBytes, name); + +// public static bool operator ==(BufferDescriptor left, BufferDescriptor right) +// { +// return left.Equals(right); +// } + +// public static bool operator !=(BufferDescriptor left, BufferDescriptor right) +// { +// return !(left == right); +// } +//} + +/// +/// Base interface for pass data that can be stored in the blackboard. +/// +public interface IPassData +{ +} + +/// +/// Information about a render target attachment in a native render pass. +/// +internal struct RenderTargetInfo +{ + public Identifier texture; + public AccessFlags access; + public AttachmentLoadOp loadOp; + public AttachmentStoreOp storeOp; + public Color128 clearColor; +} + +/// +/// Information about a depth-stencil attachment in a native render pass. +/// +internal struct DepthStencilInfo +{ + public Identifier texture; + public AccessFlags access; + public AttachmentLoadOp loadOp; + public AttachmentStoreOp storeOp; + public float clearDepth; + public byte clearStencil; +} diff --git a/Ghost.Graphics/RenderPasses/MeshRenderPass.cs b/Ghost.Graphics/RenderPasses/MeshRenderPass.cs index f24ee9f..eb866cc 100644 --- a/Ghost.Graphics/RenderPasses/MeshRenderPass.cs +++ b/Ghost.Graphics/RenderPasses/MeshRenderPass.cs @@ -3,16 +3,32 @@ using Ghost.Core.Graphics; using Ghost.DSL.ShaderCompiler; using Ghost.Graphics.Contracts; using Ghost.Graphics.Core; +using Ghost.Graphics.RenderGraphModule; using Ghost.Graphics.RHI; using Ghost.Graphics.Utilities; using Misaki.HighPerformance.Image; using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Utilities; -using System.Diagnostics; using System.Runtime.InteropServices; namespace Ghost.Graphics.RenderPasses; +internal class MeshRenderPassData +{ + public Handle mesh; + public Handle material; + public Identifier renderTarget; +} + +internal class BlitPassData +{ + public Identifier source; + public Identifier destination; + + public Handle blitMaterial; + public Identifier sampler; +} + /// /// Simplified bindless mesh render pass using high-level bindless APIs with fully bindless vertex/index buffer access /// @@ -33,12 +49,23 @@ internal class MeshRenderPass : IRenderPass private readonly uint _padding3; } + [StructLayout(LayoutKind.Sequential)] + private struct ShaderProperties_Hidden_Blit + { + public uint mainTex; + public uint sampler_mainTex; + private readonly uint _padding1; + private readonly uint _padding2; + } + private Handle _mesh; private Identifier _shader; private Handle _material; private Handle[]? _textures; + private Identifier _sampler; - private Identifier _forwardPassID; + private Identifier _blitShader; + private Handle _blitMaterial; // Texture file paths for this demo private readonly string[] _textureFiles = [ @@ -77,8 +104,32 @@ internal class MeshRenderPass : IRenderPass } } + private void CompileBlitShader(ref readonly RenderingContext ctx) + { + var shaderDescriptor = DSLShaderCompiler.CompileShader("F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Blit.gsdef", "C:/Users/Misaki/Downloads/Archive").GetValueOrThrow(); + _blitShader = ctx.ResourceAllocator.CreateGraphicsShader(shaderDescriptor); + _blitMaterial = ctx.ResourceAllocator.CreateMaterial(_blitShader); + + var config = new ShaderCompilationConfig + { + optimizeLevel = CompilerOptimizeLevel.O3, + options = CompilerOption.KeepReflections, + tier = CompilerTier.Tier2 + }; + + var pass = shaderDescriptor.passes[0]; + var emptyKeywords = new LocalKeywordSet(); + var variantKey = RHIUtility.CreateShaderVariantKey( + RHIUtility.CreateShaderPassKey(pass.identifier), + in emptyKeywords); + + ctx.ShaderCompiler.CompilePass(in pass, in config, variantKey).GetValueOrThrow(); + } + public void Initialize(ref readonly RenderingContext ctx) { + CompileBlitShader(in ctx); + var shaderDescriptor = DSLShaderCompiler.CompileShader("F:/csharp/GhostEngine/Ghost.Graphics/test.gsdef", "C:/Users/Misaki/Downloads/Archive").GetValueOrThrow(); _shader = ctx.ResourceAllocator.CreateGraphicsShader(shaderDescriptor); @@ -108,8 +159,13 @@ internal class MeshRenderPass : IRenderPass } else { - ref var shaderRef = ref ctx.ResourceDatabase.GetShaderReference(_shader); + var shaderResult = ctx.ResourceDatabase.GetShaderReference(_shader); + if (shaderResult.IsFailure) + { + throw new InvalidOperationException("Failed to get shader reference."); + } + ref readonly var shaderRef = ref shaderResult.Value; foreach (var keyGroup in GetAllVariantCombination(pass.keywords)) { config.defines = keyGroup.Span; @@ -157,7 +213,7 @@ internal class MeshRenderPass : IRenderPass Usage = TextureUsage.ShaderResource, }; - _textures[i] = ctx.CreateTexture(in desc, imageData.AsSpan(), $"Texture_{i}"); + _textures[i] = ctx.CreateTexture(in desc, imageData.AsSpan(), $"Texture_{i}"); } var samplerDesc = new SamplerDesc @@ -169,9 +225,15 @@ internal class MeshRenderPass : IRenderPass MaxAnisotropy = 16, }; - var sampler = ctx.ResourceAllocator.CreateSampler(in samplerDesc); + _sampler = ctx.ResourceAllocator.CreateSampler(in samplerDesc); - ref var matRef = ref ctx.ResourceDatabase.GetMaterialReference(_material); + var meshResult = ctx.ResourceDatabase.GetMaterialReference(_material); + if (meshResult.IsFailure) + { + throw new InvalidOperationException("Failed to get material reference."); + } + + ref readonly var matRef = ref meshResult.Value; var matProps = new ShaderProperties_MyShader_Standard { color = new float4(1.0f, 1.0f, 1.0f, 1.0f), @@ -179,18 +241,72 @@ internal class MeshRenderPass : IRenderPass texture2 = ctx.ResourceDatabase.GetBindlessIndex(_textures[1].AsResource()), texture3 = ctx.ResourceDatabase.GetBindlessIndex(_textures[2].AsResource()), texture4 = ctx.ResourceDatabase.GetBindlessIndex(_textures[3].AsResource()), - tex_sampler = (uint)sampler.Value, + tex_sampler = (uint)_sampler.Value, }; matRef.SetPropertyCache(in matProps).ThrowIfFailed(); matRef.UploadData(ctx.DirectCommandBuffer); - - _forwardPassID = Shader.GetPassID("Forward"); } - public void Execute(ref readonly RenderingContext ctx) + public void Build(RenderGraph graph, Identifier backbuffer) { - ctx.DispatchMesh(_mesh, _material, _forwardPassID, 3); + Identifier renderTarget; + using (var builder = graph.AddRasterRenderPass("Mesh Render Pass", out var passData)) + { + passData.mesh = _mesh; + passData.material = _material; + + passData.renderTarget = builder.CreateTexture(RGTextureDesc.Relative(1.0f, TextureFormat.R8G8B8A8_UNorm), "Render Target"); + builder.SetColorAttachment(passData.renderTarget, 0); + + renderTarget = passData.renderTarget; + + builder.SetRenderFunc(static (data, ctx) => + { + ctx.SetActiveMaterial(data.material); + ctx.SetActiveMesh(data.mesh); + + var threadGroupCountX = ((uint)ctx.ActiveMeshIndexCount + 2u) / 3u; + ctx.DispatchMesh(new uint3(threadGroupCountX, 1u, 1u)); + }); + } + + // FIX: We can not upload the blit material properties during a native render pass. + using (var builder = graph.AddUnsafeRenderPass("Blit Pass", out var passData)) + { + passData.source = renderTarget; + passData.destination = backbuffer; + passData.blitMaterial = _blitMaterial; + passData.sampler = _sampler; + + builder.UseTexture(passData.source, AccessFlags.Read); + builder.UseTexture(passData.destination, AccessFlags.WriteAll); + + builder.SetRenderFunc(static (data, ctx) => + { + var r = ctx.ResourceDatabase.GetMaterialReference(data.blitMaterial); + if (r.IsFailure) + { + return; + } + + ref readonly var matRef = ref r.Value; + var blitProps = new ShaderProperties_Hidden_Blit + { + mainTex = ctx.ResourceDatabase.GetBindlessIndex(ctx.GetActualResource(data.source.AsResource())), + sampler_mainTex = (uint)data.sampler.Value, + }; + + matRef.SetPropertyCache(in blitProps).ThrowIfFailed(); + matRef.UploadData(ctx.CommandBuffer); + + ctx.CommandBuffer.SetRenderTargets([ctx.GetActualTexture(data.destination)], Handle.Invalid); + + ctx.SetActiveMaterial(data.blitMaterial); + ctx.SetActiveMesh(Handle.Invalid); + ctx.DispatchMesh(new uint3(1, 1, 1)); + }); + } } public void Cleanup(IResourceDatabase resourceDatabase) diff --git a/Ghost.Graphics/RenderPasses/ShaderCode.hlsl b/Ghost.Graphics/RenderPasses/ShaderCode.hlsl index 9230fd3..2975bb1 100644 --- a/Ghost.Graphics/RenderPasses/ShaderCode.hlsl +++ b/Ghost.Graphics/RenderPasses/ShaderCode.hlsl @@ -1,5 +1,5 @@ -#include "F:/csharp/GhostEngine/Ghost.DSL/BuiltIn/Properties.hlsl" -#include "F:/csharp/GhostEngine/Ghost.DSL/BuiltIn/Common.hlsl" +#include "F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Includes/Properties.hlsl" +#include "F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Includes/Common.hlsl" struct PixelInput { @@ -46,5 +46,5 @@ float4 PSMain(PixelInput input) : SV_TARGET float4 color4 = SAMPLE_TEXTURE2D(perMaterialData.texture4, perMaterialData.tex_sampler, input.uv.xy); float4 blendedColor = (color1 + color2 + color3 + color4) * 0.25f; - return perMaterialData.color * blendedColor; + return perMaterialData.color * blendedColor + input.color; } diff --git a/Ghost.Graphics/RenderPasses/SimpleRenderPipeline.cs b/Ghost.Graphics/RenderPasses/SimpleRenderPipeline.cs new file mode 100644 index 0000000..9673779 --- /dev/null +++ b/Ghost.Graphics/RenderPasses/SimpleRenderPipeline.cs @@ -0,0 +1,5 @@ +namespace Ghost.Graphics.RenderPasses; + +internal class SimpleRenderPipeline +{ +} \ No newline at end of file diff --git a/Ghost.Graphics/RenderSystem.cs b/Ghost.Graphics/RenderSystem.cs index 39f14ce..0688c5d 100644 --- a/Ghost.Graphics/RenderSystem.cs +++ b/Ghost.Graphics/RenderSystem.cs @@ -257,7 +257,20 @@ internal class RenderSystem : IRenderSystem if (!_resizeRequest.IsEmpty) { - WaitIdle(); + //WaitIdle(); + _gpuFenceValue++; + var flushFence = _graphicsEngine.Device.GraphicsQueue.Signal(_gpuFenceValue); + _graphicsEngine.Device.GraphicsQueue.WaitForValue(flushFence); + + // Sync the current frame resource to this new fence to keep state consistent + frameResource.FenceValue = flushFence; + + + foreach (var resource in _frameResources) + { + resource.CommandAllocator.Reset(); + } + foreach (var kvp in _resizeRequest) { var swapChain = kvp.Key; diff --git a/Ghost.Graphics/Shaders/Blit.gsdef b/Ghost.Graphics/Shaders/Blit.gsdef new file mode 100644 index 0000000..24c0142 --- /dev/null +++ b/Ghost.Graphics/Shaders/Blit.gsdef @@ -0,0 +1,74 @@ +shader "Hidden/Blit" +{ + properties + { + tex2d mainTex = { white }; + sampler sampler_mainTex; + } + + pass "Blit" + { + pipeline + { + ztest = always; + zwrite = off; + cull = off; + blend = opaque; + color_mask = all; + } + + includes + { + "F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Includes/Common.hlsl"; + "F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Includes/Color.hlsl"; + "F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Includes/Properties.hlsl"; + } + + hlsl + { + struct PSInput + { + float4 position : SV_POSITION; + float4 uv : TEXCOORD0; + }; + + [MESH_SHADER_THREADS(4)] + [OUTPUT_TRIANGLE_TOPOLOGY] + void MSMain( + uint gtid : SV_GroupThreadID, + out vertices PSInput verts[4], + out indices uint3 tris[2] + ) + { + SetMeshOutputCounts(4, 2); + + float2 uv = float2(gtid & 1, (gtid >> 1) & 1); + + verts[gtid].position = float4(uv * 2.0 - 1.0, 0.0, 1.0); + // verts[gtid].position.y *= -1.0; + verts[gtid].uv = float4(uv, 0.0, 0.0); + + if (gtid == 0) + { + tris[0] = uint3(0, 1, 2); // First triangle + tris[1] = uint3(1, 3, 2); // Second triangle + } + } + + float4 PSMain(PSInput input) : SV_TARGET + { + PerMaterialData perMaterialData = LoadData(g_PushConstantData.perMaterialBuffer, 0); + + float2 uv = input.uv.xy; + float4 color = SAMPLE_TEXTURE2D(perMaterialData.mainTex, perMaterialData.sampler_mainTex, uv); + #ifdef LINEAR_COLORSPACE + color = LinearToSRGB(color); + #endif + return color; + } + } + + mesh "hlsl_block" : "MSMain"; + pixel "hlsl_block" : "PSMain"; + } +} diff --git a/Ghost.Graphics/Shaders/Includes/Color.hlsl b/Ghost.Graphics/Shaders/Includes/Color.hlsl new file mode 100644 index 0000000..4602854 --- /dev/null +++ b/Ghost.Graphics/Shaders/Includes/Color.hlsl @@ -0,0 +1,12 @@ +#ifndef GHOST_COLOR_HLSL +#define GHOST_COLOR_HLSL + +float4 LinearToSRGB(float4 color) +{ + float3 srgb; + srgb = saturate(color.rgb); + srgb = pow(srgb, 1.0 / 2.2); + return float4(srgb, color.a); +} + +#endif // GHOST_COLOR_HLSL diff --git a/Ghost.DSL/BuiltIn/Common.hlsl b/Ghost.Graphics/Shaders/Includes/Common.hlsl similarity index 100% rename from Ghost.DSL/BuiltIn/Common.hlsl rename to Ghost.Graphics/Shaders/Includes/Common.hlsl diff --git a/Ghost.DSL/BuiltIn/Properties.hlsl b/Ghost.Graphics/Shaders/Includes/Properties.hlsl similarity index 90% rename from Ghost.DSL/BuiltIn/Properties.hlsl rename to Ghost.Graphics/Shaders/Includes/Properties.hlsl index 3999d90..fca72d4 100644 --- a/Ghost.DSL/BuiltIn/Properties.hlsl +++ b/Ghost.Graphics/Shaders/Includes/Properties.hlsl @@ -1,7 +1,7 @@ #ifndef BUILTIN_PROPERTIES_HLSL #define BUILTIN_PROPERTIES_HLSL -#include "F:/csharp/GhostEngine/Ghost.DSL/BuiltIn/Common.hlsl" +#include "F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Includes/Common.hlsl" struct PushConstantData { diff --git a/GhostEngine.slnx b/GhostEngine.slnx index 1ad6552..faf0973 100644 --- a/GhostEngine.slnx +++ b/GhostEngine.slnx @@ -33,7 +33,6 @@ -