From b8af6e8c3ab6437bfb825b9deef741459c2ae738 Mon Sep 17 00:00:00 2001 From: Misaki Date: Mon, 2 Mar 2026 19:06:19 +0900 Subject: [PATCH] Added RenderPipelineBase --- src/Runtime/Ghost.Core/Ghost.Core.csproj | 2 +- src/Runtime/Ghost.Engine/Components/Camera.cs | 34 ++++++ src/Runtime/Ghost.Entities/Component.cs | 10 +- .../Ghost.Graphics.D3D12/D3D12RenderDevice.cs | 2 +- src/Runtime/Ghost.Graphics/Core/RenderList.cs | 70 ++++++++++- .../Ghost.Graphics/Core/RenderRequest.cs | 13 +- .../Ghost.Graphics/Core/RenderingLayerMask.cs | 33 ++++++ .../RenderPasses/SimpleRenderPipeline.cs | 5 - .../RenderPipeline/GhostRenderPipeline.cs | 8 +- .../RenderPipeline/IRenderPipeline.cs | 9 -- .../RenderPipeline/RenderPipeline.cs | 70 +++++++++++ src/Runtime/Ghost.Graphics/ResourceManager.cs | 2 +- .../Ghost.Graphics/SwapChainManager.cs | 112 ++++++++++++++++++ 13 files changed, 338 insertions(+), 32 deletions(-) create mode 100644 src/Runtime/Ghost.Engine/Components/Camera.cs create mode 100644 src/Runtime/Ghost.Graphics/Core/RenderingLayerMask.cs delete mode 100644 src/Runtime/Ghost.Graphics/RenderPasses/SimpleRenderPipeline.cs delete mode 100644 src/Runtime/Ghost.Graphics/RenderPipeline/IRenderPipeline.cs create mode 100644 src/Runtime/Ghost.Graphics/RenderPipeline/RenderPipeline.cs create mode 100644 src/Runtime/Ghost.Graphics/SwapChainManager.cs diff --git a/src/Runtime/Ghost.Core/Ghost.Core.csproj b/src/Runtime/Ghost.Core/Ghost.Core.csproj index a99677c..d44b3e3 100644 --- a/src/Runtime/Ghost.Core/Ghost.Core.csproj +++ b/src/Runtime/Ghost.Core/Ghost.Core.csproj @@ -22,7 +22,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Runtime/Ghost.Engine/Components/Camera.cs b/src/Runtime/Ghost.Engine/Components/Camera.cs new file mode 100644 index 0000000..e8d4c6a --- /dev/null +++ b/src/Runtime/Ghost.Engine/Components/Camera.cs @@ -0,0 +1,34 @@ +using Ghost.Core; +using Ghost.Entities; +using Ghost.Graphics.Core; +using Ghost.Graphics.RHI; +using Misaki.HighPerformance.Mathematics; + +namespace Ghost.Engine.Components; + +[RequireComponent] +public unsafe struct Camera : IComponent +{ + public float nearClipPlane; + public float farClipPlane; + + public float2 sensorSize; + public GateFit gateFit; + public float iso; + public float shutterSpeed; + public float aperture; + public float focalLength; + public float focusDistance; + + public RenderingLayerMask renderingLayerMask; + + public int swapChainIndex; // The index of the swap chain to render to. -1 means render to rt only. + public int priority; + + public Handle colorTarget; + public Handle depthTarget; + // TODO: Add more render targets like motion vector, etc. + + // Custim render function. If it's not null, the render system will call this function instead of the default render pipeline. + public delegate* renderFunc; +} diff --git a/src/Runtime/Ghost.Entities/Component.cs b/src/Runtime/Ghost.Entities/Component.cs index 29e21e9..dc555bd 100644 --- a/src/Runtime/Ghost.Entities/Component.cs +++ b/src/Runtime/Ghost.Entities/Component.cs @@ -6,12 +6,14 @@ using System.Runtime.CompilerServices; namespace Ghost.Entities; -public interface IComponent -{ -} +public interface IComponent; +public interface IEnableableComponent : IComponent; -public interface IEnableableComponent : IComponent +[AttributeUsage(AttributeTargets.Struct)] +public class RequireComponentAttribute : Attribute + where T : unmanaged, IComponent { + public Type RequiredType => typeof(T); } internal struct ComponentInfo diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12RenderDevice.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12RenderDevice.cs index b51a382..8a43785 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12RenderDevice.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12RenderDevice.cs @@ -116,7 +116,7 @@ internal unsafe class D3D12RenderDevice : IRenderDevice support |= FeatureSupport.BindlessResources; } - if (options.ResourceHeapTier == D3D12_RESOURCE_HEAP_TIER.D3D12_RESOURCE_HEAP_TIER_2) + if (options.ResourceHeapTier == D3D12_RESOURCE_HEAP_TIER_2) { support |= FeatureSupport.AliasBuffersAndTextures; } diff --git a/src/Runtime/Ghost.Graphics/Core/RenderList.cs b/src/Runtime/Ghost.Graphics/Core/RenderList.cs index 1c61b3e..1a2c0df 100644 --- a/src/Runtime/Ghost.Graphics/Core/RenderList.cs +++ b/src/Runtime/Ghost.Graphics/Core/RenderList.cs @@ -12,7 +12,7 @@ public record struct RenderRecord public struct RenderList : IDisposable { - public unsafe ref struct Reader + public unsafe ref struct Enumerator { private readonly UnsafeList* pList; private readonly int length; @@ -20,7 +20,7 @@ public struct RenderList : IDisposable private int _listIndex; private int _itemIndex; - internal Reader(RenderList List) + internal Enumerator(RenderList List) { pList = (UnsafeList*)List._threadLocalRecords.GetUnsafePtr(); length = List._threadLocalRecords.Length; @@ -59,8 +59,16 @@ public struct RenderList : IDisposable private UnsafeArray> _threadLocalRecords; + public readonly int ThreadLocalCount => _threadLocalRecords.Length; + public readonly bool IsCreated => _threadLocalRecords.IsCreated; + public RenderList(int maxLevelOfConcurrency, int capacity, AllocationHandle allocationHandle) { + if (maxLevelOfConcurrency <= 0) + { + throw new ArgumentOutOfRangeException(nameof(maxLevelOfConcurrency), "Max level of concurrency must be greater than zero."); + } + _threadLocalRecords = new UnsafeArray>(maxLevelOfConcurrency, allocationHandle); for (int i = 0; i < maxLevelOfConcurrency; i++) @@ -74,16 +82,70 @@ public struct RenderList : IDisposable { } - public readonly Reader GetEnumerator() + private readonly void ThrowIfNotCreated() { - return new Reader(this); + if (!IsCreated) + { + throw new InvalidOperationException("RenderList is not created."); + } + } + + public readonly Enumerator GetEnumerator() + { + ThrowIfNotCreated(); + return new Enumerator(this); } public readonly void Add(RenderRecord record, int threadIndex) { + ThrowIfNotCreated(); _threadLocalRecords[threadIndex].Add(record); } + public readonly ReadOnlyUnsafeCollection GetThreadLocalRecords(int threadIndex) + { + ThrowIfNotCreated(); + return _threadLocalRecords[threadIndex].AsReadOnly(); + } + + public readonly void Clear() + { + ThrowIfNotCreated(); + for (int i = 0; i < _threadLocalRecords.Length; i++) + { + _threadLocalRecords[i].Clear(); + } + } + + public readonly void ClearThreadLocal(int threadIndex) + { + ThrowIfNotCreated(); + _threadLocalRecords[threadIndex].Clear(); + } + + public readonly void Append(RenderList other) + { + if (!IsCreated || !other.IsCreated) + { + throw new InvalidOperationException("Both RenderLists must be created before appending."); + } + + var maxConcurrency = Math.Min(_threadLocalRecords.Length, other._threadLocalRecords.Length); + for (int i = 0; i < maxConcurrency; i++) + { + _threadLocalRecords[i].AddRange(other._threadLocalRecords[i].AsSpan()); + } + + if (other._threadLocalRecords.Length > _threadLocalRecords.Length) + { + // Add remaining records from other lists to the first list if other has more thread local lists than this + for (int i = _threadLocalRecords.Length; i < other._threadLocalRecords.Length; i++) + { + _threadLocalRecords[0].AddRange(other._threadLocalRecords[i].AsSpan()); + } + } + } + public void Dispose() { for (int i = 0; i < _threadLocalRecords.Length; i++) diff --git a/src/Runtime/Ghost.Graphics/Core/RenderRequest.cs b/src/Runtime/Ghost.Graphics/Core/RenderRequest.cs index 64d901b..15a86f9 100644 --- a/src/Runtime/Ghost.Graphics/Core/RenderRequest.cs +++ b/src/Runtime/Ghost.Graphics/Core/RenderRequest.cs @@ -5,6 +5,15 @@ using System.Runtime.InteropServices; namespace Ghost.Graphics.Core; +public enum GateFit : uint +{ + Vertical, + Horizontal, + Fill, + Overscan, + None +} + [StructLayout(LayoutKind.Sequential, Pack = 4)] public struct Frustum { @@ -33,6 +42,7 @@ public struct Frustum public float3 corner7; } +// Since we are using ByteAddressBuffer in hlsl, we don't need to care about the 16 bytes alignment of the data like in CBuffer. [StructLayout(LayoutKind.Sequential, Pack = 4)] public struct RenderView { @@ -46,13 +56,14 @@ public struct RenderView public float farClipPlane; public float2 sensorSize; + public GateFit gateFit; public float iso; public float shutterSpeed; public float aperture; public float focalLength; public float focusDistance; - public uint renderingLayerMask; + public RenderingLayerMask renderingLayerMask; } public unsafe struct RenderRequest diff --git a/src/Runtime/Ghost.Graphics/Core/RenderingLayerMask.cs b/src/Runtime/Ghost.Graphics/Core/RenderingLayerMask.cs new file mode 100644 index 0000000..827d985 --- /dev/null +++ b/src/Runtime/Ghost.Graphics/Core/RenderingLayerMask.cs @@ -0,0 +1,33 @@ +using System.Diagnostics; + +namespace Ghost.Graphics.Core; + +public struct RenderingLayerMask +{ + private static readonly Dictionary _layerNameToBit = new (32); + private static readonly Dictionary _bitToLayerName = new (32); + + internal static void SetLayerName(int layerIndex, string name) + { + Debug.Assert(layerIndex >= 0 && layerIndex < 32, "Layer index must be between 0 and 31."); + + var bit = 1u << layerIndex; + _layerNameToBit[name] = bit; + _bitToLayerName[bit] = name; + } + + public static uint GetLayerBit(string name) + { + if (_layerNameToBit.TryGetValue(name, out var bit)) + { + return bit; + } + + return ~0u; + } + + public uint value; + + public static implicit operator uint(RenderingLayerMask mask) => mask.value; + public static implicit operator RenderingLayerMask(uint value) => new RenderingLayerMask { value = value }; +} diff --git a/src/Runtime/Ghost.Graphics/RenderPasses/SimpleRenderPipeline.cs b/src/Runtime/Ghost.Graphics/RenderPasses/SimpleRenderPipeline.cs deleted file mode 100644 index 9673779..0000000 --- a/src/Runtime/Ghost.Graphics/RenderPasses/SimpleRenderPipeline.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Ghost.Graphics.RenderPasses; - -internal class SimpleRenderPipeline -{ -} \ No newline at end of file diff --git a/src/Runtime/Ghost.Graphics/RenderPipeline/GhostRenderPipeline.cs b/src/Runtime/Ghost.Graphics/RenderPipeline/GhostRenderPipeline.cs index f7bb9f1..4c5cbd0 100644 --- a/src/Runtime/Ghost.Graphics/RenderPipeline/GhostRenderPipeline.cs +++ b/src/Runtime/Ghost.Graphics/RenderPipeline/GhostRenderPipeline.cs @@ -3,13 +3,9 @@ using Ghost.Graphics.RHI; namespace Ghost.Graphics.RenderPipeline; -public partial class GhostRenderPipeline : IRenderPipeline +public partial class GhostRenderPipeline : RenderPipelineBase { - public void Render(RenderContext ctx, ReadOnlySpan requests) - { - } - - public void Dispose() + public override void Render(RenderContext ctx, ReadOnlySpan requests) { } } diff --git a/src/Runtime/Ghost.Graphics/RenderPipeline/IRenderPipeline.cs b/src/Runtime/Ghost.Graphics/RenderPipeline/IRenderPipeline.cs deleted file mode 100644 index 5e10fc0..0000000 --- a/src/Runtime/Ghost.Graphics/RenderPipeline/IRenderPipeline.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ghost.Graphics.Core; -using Ghost.Graphics.RHI; - -namespace Ghost.Graphics.RenderPipeline; - -public interface IRenderPipeline : IDisposable -{ - void Render(RenderContext ctx, ReadOnlySpan requests); -} diff --git a/src/Runtime/Ghost.Graphics/RenderPipeline/RenderPipeline.cs b/src/Runtime/Ghost.Graphics/RenderPipeline/RenderPipeline.cs new file mode 100644 index 0000000..0fe15e2 --- /dev/null +++ b/src/Runtime/Ghost.Graphics/RenderPipeline/RenderPipeline.cs @@ -0,0 +1,70 @@ +using Ghost.Graphics.Core; +using Ghost.Graphics.RHI; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ghost.Graphics.RenderPipeline; + +public interface IRenderPipeline : IDisposable +{ + void AddRenderList(RenderList renderList, string key); + + void Render(RenderContext ctx, ReadOnlySpan requests); +} + +public abstract class RenderPipelineBase : IRenderPipeline +{ + protected readonly Dictionary _renderLists = new(); + + private bool _disposed; + + ~RenderPipelineBase() + { + Dispose(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + public void AddRenderList(RenderList renderList, string key) + { + ThrowIfDisposed(); + + ref var existingList = ref CollectionsMarshal.GetValueRefOrAddDefault(_renderLists, key, out var exists); + if (!exists) + { + existingList = renderList; + } + else + { + existingList.Append(renderList); + } + } + + public abstract void Render(RenderContext ctx, ReadOnlySpan requests); + + public void Dispose() + { + if (_disposed) + { + return; + } + + Dispose(true); + + foreach (var list in _renderLists.Values) + { + list.Dispose(); + } + + _disposed = true; + GC.SuppressFinalize(this); + } + + public virtual void Dispose(bool disposing) + { + } +} diff --git a/src/Runtime/Ghost.Graphics/ResourceManager.cs b/src/Runtime/Ghost.Graphics/ResourceManager.cs index b2ad0ce..2dd7cb3 100644 --- a/src/Runtime/Ghost.Graphics/ResourceManager.cs +++ b/src/Runtime/Ghost.Graphics/ResourceManager.cs @@ -320,4 +320,4 @@ internal sealed class ResourceManager : IResourceManager, IDisposable _disposed = true; GC.SuppressFinalize(this); } -} \ No newline at end of file +} diff --git a/src/Runtime/Ghost.Graphics/SwapChainManager.cs b/src/Runtime/Ghost.Graphics/SwapChainManager.cs new file mode 100644 index 0000000..f3c4065 --- /dev/null +++ b/src/Runtime/Ghost.Graphics/SwapChainManager.cs @@ -0,0 +1,112 @@ +using Ghost.Graphics.RHI; +using System.Diagnostics.CodeAnalysis; + +namespace Ghost.Graphics; + +internal sealed class SwapChainRecord +{ + private int _refCount; + + public ISwapChain SwapChain { get; } + + public SwapChainRecord(ISwapChain swapChain) + { + SwapChain = swapChain; + _refCount = 1; + } + + public bool TryAddRef() + { + while (true) + { + int current = Volatile.Read(ref _refCount); + if (current == 0) return false; // It's dead, let it go. + + if (Interlocked.CompareExchange(ref _refCount, current + 1, current) == current) + { + return true; // Successfully atomically incremented + } + } + } + + public bool ReleaseRef() + { + while (true) + { + int current = Volatile.Read(ref _refCount); + if (current == 0) + { + return false; + } + + if (Interlocked.CompareExchange(ref _refCount, current - 1, current) == current) + { + return (current - 1) == 0; + } + } + } +} + +internal class SwapChainManager +{ + public const int MAX_SWAP_CHAINS = 8; + private readonly IGraphicsEngine _graphicsEngine; + private readonly SwapChainRecord?[] _swapChains = new SwapChainRecord?[MAX_SWAP_CHAINS]; + + public SwapChainManager(IGraphicsEngine graphicsEngine) + { + _graphicsEngine = graphicsEngine; + } + + public ISwapChain EnsureSwapChain(int index, SwapChainDesc desc) + { + while (true) + { + var record = Volatile.Read(ref _swapChains[index]); + + if (record != null) + { + if (record.TryAddRef()) return record.SwapChain; + + Thread.Yield(); + continue; + } + + var newRecord = new SwapChainRecord(_graphicsEngine.CreateSwapChain(desc)); + var previous = Interlocked.CompareExchange(ref _swapChains[index], newRecord, null); + + if (previous == null) + { + return newRecord.SwapChain; + } + else + { + newRecord.SwapChain.Dispose(); + } + } + } + + public bool TryGetSwapChain(int index, [MaybeNullWhen(false)] out ISwapChain swapChain) + { + var record = Volatile.Read(ref _swapChains[index]); + if (record != null && record.TryAddRef()) + { + swapChain = record.SwapChain; + return true; + } + + swapChain = null; + return false; + } + + public void ReleaseSwapChain(int index) + { + var record = Volatile.Read(ref _swapChains[index]); + + if (record != null && record.ReleaseRef()) + { + record.SwapChain.Dispose(); + Interlocked.CompareExchange(ref _swapChains[index], null, record); + } + } +}