Refactor rendering projects

This commit is contained in:
2026-02-24 20:08:26 +09:00
parent 93c58fa7fb
commit 30090f84ab
88 changed files with 1350 additions and 1136 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
namespace Ghost.Graphics.RHI;
public interface ICommandAllocator : IDisposable
{
void Reset();
}

View File

@@ -0,0 +1,204 @@
using Ghost.Core;
namespace Ghost.Graphics.RHI;
// TODO: Add ICommandAllocator support for thread local command buffers. We often use one allocator for multiple command buffers in a single frame.
/// <summary>
/// D3D12-style command buffer interface for recording rendering commands
/// </summary>
public interface ICommandBuffer : IDisposable
{
/// <summary>
/// Gets the space of the command buffer.
/// </summary>
CommandBufferType Type
{
get;
}
/// <summary>
/// Indicates whether the command buffer contains any recorded commands.
/// </summary>
bool IsEmpty
{
get;
}
string Name
{
get;
set;
}
/// <summary>
/// Begins recording commands into this command buffer
/// </summary>
void Begin(ICommandAllocator allocator);
/// <summary>
/// Ends recording commands and prepares for submission
/// </summary>
Result End();
/// <summary>
/// Sets the viewport for rendering
/// </summary>
/// <param name="viewport">Viewport to set</param>
void SetViewport(ViewportDesc viewport);
/// <summary>
/// Sets the scissor rectangle
/// </summary>
/// <param name="rect">Scissor rectangle to set</param>
void SetScissorRect(RectDesc rect);
/// <summary>
/// Sets the optional render targets and optional depth Target for subsequent rendering operations.
/// </summary>
/// <remarks>
/// To specify no render targets, provide an empty span for <paramref name="renderTargets"/>.
/// Use <see cref="Handle{Texture}.Invalid"/> for <paramref name="depthTarget"/> if no depth Target is required.
/// </remarks>
/// <param name="renderTargets">A read-only span of handles to textures that will be used as render targets.
/// The order of handles determines the order in which render targets are bound.</param>
/// <param name="depthTarget">A handle to the texture to be used as the depth Target. Specify a invalid handle if no depth Target is required.</param>
void SetRenderTargets(ReadOnlySpan<Handle<Texture>> renderTargets, Handle<Texture> depthTarget);
void ClearRenderTargetView(Handle<Texture> renderTarget, Color128 clearColor);
void ClearDepthStencilView(Handle<Texture> depthStencil, bool inlcludeDepth, bool includeStencil, float clearDepth = 1.0f, byte clearStencil = 0);
/// <summary>
/// Begins a render pass with the specified render Target
/// </summary>
/// <param name="rtDescs">Render Target descriptions</param>
/// <param name="depthDesc">Depth stencil description</param>
/// <param name="allowUAVWrites">Whether UAV writes are allowed during the render pass</param>
void BeginRenderPass(ReadOnlySpan<PassRenderTargetDesc> rtDescs, PassDepthStencilDesc depthDesc, bool allowUAVWrites = false);
/// <summary>
/// Ends the current render pass
/// </summary>
void EndRenderPass();
// TODO: Enhanced barriers.
/// <summary>
/// Inserts multiple resource barriers.
/// </summary>
/// <param name="barrierDescs">Resource barrier descriptions</param>
void ResourceBarrier(params ReadOnlySpan<BarrierDesc> barrierDescs);
/// <summary>
/// Sets the pipeline state object
/// </summary>
/// <param name="pipelineKey">Pipeline state to set</param>
void SetPipelineState(Key128<GraphicsPipeline> pipelineKey);
/// <summary>
/// Sets the constant buffer view for the specified slot in the graphics pipeline.
/// </summary>
/// <param name="slot">The zero-based index of the slot to bind the constant buffer view to.</param>
/// <param name="buffer">A graphics buffer to use as the constant buffer view.</param>
void SetConstantBufferView(uint slot, Handle<GraphicsBuffer> buffer);
/// <summary>
/// Binds a vertex buffer to the specified slot for subsequent draw calls.
/// </summary>
/// <param name="slot">The vertex buffer slot to bind to.</param>
/// <param name="buffer">The handle to the graphics buffer containing vertex data.</param>
/// <param name="offset">The Offset in bytes from the start of the buffer.</param>
void SetVertexBuffer(uint slot, Handle<GraphicsBuffer> buffer, ulong offset = 0);
/// <summary>
/// Binds an index buffer for indexed drawing.
/// </summary>
/// <param name="buffer">The handle to the graphics buffer containing index data.</param>
/// <param name="type">The space of indices (e.g., 16-bit or 32-bit).</param>
/// <param name="offset">The Offset in bytes from the start of the buffer.</param>
void SetIndexBuffer(Handle<GraphicsBuffer> buffer, IndexType type, ulong offset = 0);
/// <summary>
/// Sets the primitive topology to be used for subsequent drawing operations.
/// </summary>
/// <param name="topology">The primitive topology that determines how the input vertices are interpreted during rendering.</param>
void SetPrimitiveTopology(PrimitiveTopology topology);
/// <summary>
/// Sets a 32-bit constant value in the graphics root signature at the specified index.
/// </summary>
/// <param name="rootIndex">The zero-based index of the root parameter in the graphics root signature to set the constant for.</param>
/// <param name="constantBuffer">A read-only span containing the 32-bit constant values to set.</param>
/// <param name="offsetIn32Bits">The Offset, in 32-bit values, from the start of the root parameter where the constants will be set.</param>
void SetGraphicsRoot32Constants(uint rootIndex, ReadOnlySpan<uint> constantBuffer, uint offsetIn32Bits = 0);
/// <summary>
/// Issues a non-indexed draw call.
/// </summary>
/// <param name="vertexCount">Number of vertices to draw.</param>
/// <param name="instanceCount">Number of instances to draw.</param>
/// <param name="startVertex">Index of the first vertex to draw.</param>
/// <param name="startInstance">Index of the first instance to draw.</param>
void Draw(uint vertexCount, uint instanceCount = 1, uint startVertex = 0, uint startInstance = 0);
/// <summary>
/// Issues an indexed draw call.
/// </summary>
/// <param name="indexCount">Number of indices to draw.</param>
/// <param name="instanceCount">Number of instances to draw.</param>
/// <param name="startIndex">Index of the first index to draw.</param>
/// <param name="baseVertex">Value added to each index before indexing the vertex buffer.</param>
/// <param name="startInstance">Index of the first instance to draw.</param>
void DrawIndexed(uint indexCount, uint instanceCount = 1, uint startIndex = 0, int baseVertex = 0, uint startInstance = 0);
/// <summary>
/// Dispatches compute threads
/// </summary>
/// <param name="threadGroupCountX">Thread groups in X dimension</param>
/// <param name="threadGroupCountY">Thread groups in Y dimension</param>
/// <param name="threadGroupCountZ">Thread groups in Z dimension</param>
void DispatchCompute(uint threadGroupCountX, uint threadGroupCountY, uint threadGroupCountZ);
/// <summary>
/// Dispatches mesh shader threads
/// </summary>
/// <param name="threadGroupCountX">Thread groups in X dimension</param>
/// <param name="threadGroupCountY">Thread groups in Y dimension</param>
/// <param name="threadGroupCountZ">Thread groups in Z dimension</param>
void DispatchMesh(uint threadGroupCountX, uint threadGroupCountY, uint threadGroupCountZ);
/// <summary>
/// Dispatches ray tracing threads
/// </summary>
// TODO: This method is not supported yet.
void DispatchRay();
/// <summary>
/// Uploads the specified data to the buffer represented by the given handle.
/// </summary>
/// <typeparam name="T">The unmanaged Value space of the elements to upload to the buffer.</typeparam>
/// <param name="buffer">A handle to the buffer that will receive the uploaded data.</param>
/// <param name="data">A read-only span containing the data to upload to the buffer. The span must contain elements of space
/// <typeparamref name="T"/>.</param>
void UploadBuffer<T>(Handle<GraphicsBuffer> buffer, params ReadOnlySpan<T> data)
where T : unmanaged;
/// <summary>
/// Uploads texture data to the specified texture resource starting at the given subresource index.
/// </summary>
/// <param name="texture">The texture resource to which the subresource data will be uploaded. Must be a valid, initialized texture handle.</param>
/// <param name="subresources">A reference to the structure containing the subresource data to upload. The data must match the Format and layout expected by the texture.</param>
/// Must be greater than zero and not exceed the remaining subresources in the texture.</param>
void UploadTexture(Handle<Texture> texture, params ReadOnlySpan<SubResourceData> subresources);
/// <summary>
/// Copies a specified number of bytes from the source graphics buffer to the destination graphics buffer.
/// </summary>
/// <param name="dest">The handle to the destination graphics buffer where data will be written.</param>
/// <param name="src">The handle to the source graphics buffer from which data will be read.</param>
/// <param name="destOffset">The byte Offset in the destination buffer at which to begin writing. Must be zero or greater.</param>
/// <param name="srcOffset">The byte Offset in the source buffer at which to begin reading. Must be zero or greater.</param>
/// <param name="numBytes">The number of bytes to copy. If zero, copies the remaining bytes from the source buffer starting at <paramref name="srcOffset"/>.</param>
void CopyBuffer(Handle<GraphicsBuffer> dest, Handle<GraphicsBuffer> src, ulong destOffset = 0, ulong srcOffset = 0, ulong numBytes = 0);
}

View File

@@ -0,0 +1,51 @@
namespace Ghost.Graphics.RHI;
/// <summary>
/// Command queue interface
/// </summary>
public interface ICommandQueue : IDisposable
{
/// <summary>
/// Type of commands this queue can execute
/// </summary>
public CommandQueueType Type
{
get;
}
/// <summary>
/// Submits a single command buffer for execution
/// </summary>
/// <param name="commandBuffer">Command buffer to submit</param>
public void Submit(ICommandBuffer commandBuffer);
/// <summary>
/// Submits multiple command buffers for execution
/// </summary>
/// <param name="commandBuffers">Command buffers to submit</param>
public void Submit(params ReadOnlySpan<ICommandBuffer> commandBuffers);
/// <summary>
/// Signals a fence with the specified Value
/// </summary>
/// <param name="value">Value to signal</param>
/// <returns>The fence Value that was signaled</returns>
public ulong Signal(ulong value);
/// <summary>
/// Waits for the fence to reach the specified Value
/// </summary>
/// <param name="value">Value to wait for</param>
public void WaitForValue(ulong value);
/// <summary>
/// Gets the last completed fence Value
/// </summary>
/// <returns>Last completed fence Value</returns>
public ulong GetCompletedValue();
/// <summary>
/// Waits until all submitted commands have finished executing
/// </summary>
public void WaitIdle();
}

View File

@@ -0,0 +1,78 @@
using Ghost.Core;
namespace Ghost.Graphics.RHI;
public interface IGraphicsEngine : IDisposable
{
IRenderDevice Device
{
get;
}
IShaderCompiler ShaderCompiler
{
get;
}
IPipelineLibrary PipelineLibrary
{
get;
}
IResourceDatabase ResourceDatabase
{
get;
}
IResourceAllocator ResourceAllocator
{
get;
}
/// <summary>
/// Creates a new instance of a renderer for drawing graphical content.
/// </summary>
/// <returns>An object that implements the IRenderer interface, which can be used to render graphics.</returns>
IRenderer CreateRenderer();
/// <summary>
/// Removes the specified renderer from the collection of active renderers.
/// </summary>
/// <param name="renderer">The renderer instance to remove.</param>
void RemoveRenderer(IRenderer renderer);
/// <summary>
/// Removes all registered renderers from the collection.
/// </summary>
/// <remarks>Call this method to reset the renderer collection to an empty state. After calling this
/// method, no renderers will be available until new ones are added.</remarks>
void ClearRenderers();
/// <summary>
/// Creates a new command allocator for the specified command buffer space.
/// </summary>
/// <param name="type">The space of command buffer for which to create the allocator. The default is CommandBufferType.Graphics.</param>
/// <returns>An <see cref="ICommandAllocator"/> instance configured for the specified command buffer space.</returns>
ICommandAllocator CreateCommandAllocator(CommandBufferType type = CommandBufferType.Graphics);
/// <summary>
/// Creates a command buffer for recording rendering commands
/// </summary>
/// <param name="type">Type of command buffer to create</param>
/// <returns>A new command buffer instance</returns>
ICommandBuffer CreateCommandBuffer(CommandBufferType type = CommandBufferType.Graphics);
/// <summary>
/// Creates a swap chain for presentation
/// </summary>
/// <param name="desc">Swap chain description</param>
/// <returns>A new swap chain instance</returns>
ISwapChain CreateSwapChain(SwapChainDesc desc);
/// <summary>
/// Renders the current frame.
/// </summary>
/// <param name="commandAllocator">Command allocator to use for rendering</param>
/// <returns>Result of the rendering operation</returns>
Result RenderFrame(ICommandAllocator commandAllocator);
}

View File

@@ -0,0 +1,15 @@
using Ghost.Core;
namespace Ghost.Graphics.RHI;
public interface IPipelineLibrary : IDisposable
{
/// <summary>
/// Load pipeline library from disk.
/// </summary>
/// <param name="filePath">File path. If null, load default library.</param>
void InitializeLibrary(string? filePath);
void SaveLibraryToDisk(string filePath);
bool HasPipeline(Key128<GraphicsPipeline> key);
Result<Key128<GraphicsPipeline>> CompilePSO(ref readonly GraphicsPSODescriptor descriptor, ref readonly GraphicsCompiledResult compiled);
}

View File

@@ -0,0 +1,49 @@
namespace Ghost.Graphics.RHI;
[Flags]
public enum FeatureSupport
{
None = 0,
RayTracing = 1 << 0,
VariableRateShading = 1 << 1,
MeshShaders = 1 << 2,
SamplerFeedback = 1 << 3,
BindlessResources = 1 << 4,
WorkGraphs = 1 << 5,
AliasBuffersAndTextures = 1 << 6,
}
/// <summary>
/// D3D12-native render device interface for creating graphics resources
/// </summary>
public interface IRenderDevice : IDisposable
{
/// <summary>
/// Graphics command queue for rendering operations
/// </summary>
public ICommandQueue GraphicsQueue
{
get;
}
/// <summary>
/// Compute command queue for compute shader operations
/// </summary>
public ICommandQueue ComputeQueue
{
get;
}
/// <summary>
/// Copy command queue for data transfer operations
/// </summary>
public ICommandQueue CopyQueue
{
get;
}
public FeatureSupport FeatureSupport
{
get;
}
}

View File

@@ -0,0 +1,41 @@
using Ghost.Core;
namespace Ghost.Graphics.RHI;
public interface IRenderOutput
{
ViewportDesc Viewport
{
get; set;
}
RectDesc Scissor
{
get; set;
}
/// <summary>
/// Gets a handle to the current render target texture.
/// </summary>
/// <returns>A handle to the texture that is currently set as the render target.</returns>
Handle<Texture> GetRenderTarget();
/// <summary>
/// Begins a rendering operation using the specified command buffer. Typically this will include resource barriers,
/// </summary>
/// <param name="cmd">The command buffer that records rendering commands.</param>
///
void BeginRender(ICommandBuffer cmd);
/// <summary>
/// Finalizes the rendering process using the specified command buffer.
/// </summary>
/// <param name="cmd">The command buffer that contains the rendering commands to be finalized.</param>
void EndRender(ICommandBuffer cmd);
/// <summary>
/// Displays the current frame to the output device or screen.
/// </summary>
/// <remarks>Call this method after rendering operations to present the rendered content. The exact
/// behavior may depend on the underlying graphics implementation or device.</remarks>
void Present();
}

View File

@@ -0,0 +1,47 @@
using Misaki.HighPerformance.Mathematics;
namespace Ghost.Graphics.RHI;
public interface IFenceSynchronizer
{
uint CPUFenceValue
{
get;
}
uint GPUFenceValue
{
get;
}
uint FrameIndex
{
get;
}
uint MaxFrameLatency
{
get;
}
bool WaitForGPUReady(int timeOut = -1);
void SignalCPUReady();
void WaitIdle();
}
public interface IRenderSystem : IFenceSynchronizer, IDisposable
{
IGraphicsEngine GraphicsEngine
{
get;
}
bool IsRunning
{
get;
}
void Start();
void Stop();
void RequestSwapChainResize(ISwapChain swapChain, uint2 newSize);
}

View File

@@ -0,0 +1,37 @@
using Ghost.Core;
namespace Ghost.Graphics.RHI;
public readonly struct RenderContext
{
public ICommandBuffer CommandBuffer { get; init; }
}
/// <summary>
/// High-level renderer interface that uses RHI abstractions
/// </summary>
public interface IRenderer : IDisposable
{
/// <summary>
/// Gets or sets the render output target for this renderer.
/// </summary>
IRenderOutput? RenderOutput
{
get; set;
}
/// <summary>
/// The function that performs the actual rendering operations. Skip rendering if this is null.
/// </summary>
Func<RenderContext, Error>? RenderFunc
{
get; set;
}
/// <summary>
/// Renders a frame
/// </summary>
/// <param name="commandAllocator">Command allocator to use for rendering</param>
/// <returns>Result of the rendering operation</returns>
Result Render(ICommandAllocator commandAllocator);
}

View File

@@ -0,0 +1,140 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.RHI;
public enum ResourceAllocationType
{
Default,
Temporary,
Suballocation,
}
public struct CreationOptions
{
public ResourceAllocationType AllocationType
{
get; set;
}
public Handle<GPUResource> Heap
{
get; set;
}
public ulong Offset
{
get; set;
}
}
public enum HeapType
{
Default,
Upload,
Readback
}
public enum HeapFlags
{
None = 0,
AllowBuffers,
AllowTextures,
AllowRTAndDS,
AlowBufferAndTexture,
}
public struct AllocationDesc
{
public ulong Size
{
get; set;
}
public ulong Alignment
{
get; set;
}
public HeapType HeapType
{
get; set;
}
public HeapFlags HeapFlags
{
get; set;
}
}
public readonly struct ResourceSizeInfo
{
public ulong Size
{
get; init;
}
public ulong Alignment
{
get; init;
}
}
public interface IResourceAllocator : IDisposable
{
ResourceSizeInfo GetSizeInfo(ResourceDesc desc);
/// <summary>
/// Allocates a block of memory on the GPU
/// </summary>
/// <param name="desc">Allocation description</param>
/// <param name="name">Debug name of the allocation</param>
/// <returns>An <see cref="Handle{GPUResource}"/> point to the allocated memory</returns>
Handle<GPUResource> Allocate(ref readonly AllocationDesc desc, string name);
/// <summary>
/// Creates a texture resource
/// </summary>
/// <param name="desc">Texture description</param>
/// <param name="name">Debug name of the resource</param>
/// <param name="options">Additional options of the resource allocation</param>
/// <returns>An <see cref="Handle{Texture}"/> point to the resource</returns>
Handle<Texture> CreateTexture(ref readonly TextureDesc desc, string name, CreationOptions options = default);
/// <summary>
/// Creates a render Target for off-screen rendering
/// </summary>
/// <param name="desc">Render Target description</param>
/// <param name="name">Debug name of the resource</param>
/// <param name="options">Additional options of the resource allocation</param>
/// <returns>An <see cref="Handle{Texture}"/> point to the resource</returns>
Handle<Texture> CreateRenderTarget(ref readonly RenderTargetDesc desc, string name, CreationOptions options = default);
/// <summary>
/// Creates a buffer resource
/// </summary>
/// <param name="desc">Buffer description</param>
/// <param name="name">Debug name of the resource</param>
/// <param name="options">Additional options of the resource allocation</param>
/// <returns>An <see cref="Handle{GraphicsBuffer}"/> point to the resource</returns>
Handle<GraphicsBuffer> CreateBuffer(ref readonly BufferDesc desc, string name, CreationOptions options = default);
/// <summary>
/// Creates a temporary upload buffer of the specified size in bytes.
/// </summary>
/// <remarks>
/// This method has been optimized for frequent calls during frame updates. It efficiently manages memory to minimize fragmentation and overhead.
/// </remarks>
/// <param name="sizeInBytes">The size of the upload buffer to create, in bytes.</param>
/// <param name="offset">The offset within the upload buffer where the allocation begins.</param>
/// <returns>An <see cref="Handle{GraphicsBuffer}"/> pointing to the created upload buffer.</returns>
Handle<GraphicsBuffer> CreateTempUploadBuffer(ulong sizeInBytes, out ulong offset);
/// <summary>
/// Creates a new sampler object using the specified sampler description.
/// </summary>
/// <param name="desc">A read-only reference to a <see cref="SamplerDesc"/> structure that defines the properties of the sampler to be created.</param>
/// <returns>An <see cref="Identifier{Sampler}"/> that uniquely identifies the created sampler object.</returns>
Identifier<Sampler> CreateSampler(ref readonly SamplerDesc desc);
}

View File

@@ -0,0 +1,131 @@
using Ghost.Core;
namespace Ghost.Graphics.RHI;
public interface IResourceReleasable
{
/// <summary>
/// A method to release GPU resources.
/// </summary>
void ReleaseResource(IResourceDatabase database);
}
public struct ResourceBarrierData
{
public BarrierLayout layout;
public BarrierAccess access;
public BarrierSync sync;
public ResourceBarrierData(BarrierLayout layout, BarrierAccess access, BarrierSync sync)
{
this.layout = layout;
this.access = access;
this.sync = sync;
}
}
public enum BindlessAccess
{
ShaderResource,
ConstantBuffer,
UnorderedAccess,
}
// TODO: Consider adding methods for resource enumeration, statistics, and bulk operations.
// TODO: Consider adding async resource loading and streaming support.
// TODO: Mesh, Material, Shader management could be separated into their own interfaces for better modularity because they are not bound to specific graphics API.
public interface IResourceDatabase : IDisposable
{
/*
/// <summary>
/// Imports an external unmanaged resource and returns a handle for use within the resource management system.
/// </summary>
/// <typeparam name="T">The space of the unmanaged resource pointer to import.</typeparam>
/// <param name="resourcePtr">A pointer to the external unmanaged resource to be imported. Must remain valid for the duration of the resource's usage.</param>
/// <param name="initialState">The initial state to assign to the imported resource.</param>
/// <returns>A handle representing the imported resource, which can be used for subsequent operations.</returns>
unsafe Handle<GPUResource> ImportExternalResource<T>(T resourcePtr, ResourceState initialState, string? name = null)
where T : unmanaged;
*/
/// <summary>
/// Checks if a resource with the specified handle exists in the database.
/// </summary>
/// <param name="handle">The handle of the resource to check for existence.</param>
bool HasResource(Handle<GPUResource> handle);
/// <summary>
/// Retrieves the current barrier data of the specified resource.
/// </summary>
/// <param name="handle">The handle that uniquely identifies the resource.</param>
/// <returns>A ResourceBarrierData value representing the current barrier state.</returns>
Result<ResourceBarrierData, Error> GetResourceBarrierData(Handle<GPUResource> handle);
/// <summary>
/// Sets the barrier data of the specified resource handle.
/// </summary>
/// <param name="handle">The handle that identifies the resource.</param>
/// <param name="data">The new barrier data.</param>
/// <returns>An Error indicating the success or failure of the operation.</returns>
Error SetResourceBarrierData(Handle<GPUResource> handle, ResourceBarrierData data);
/// <summary>
/// Retrieves the description of a GPU resource associated with the specified handle.
/// </summary>
/// <param name="handle">A handle that identifies the GPU resource for which to obtain the description. Must reference a valid resource.</param>
/// <returns>A ResourceDesc structure containing details about the specified GPU resource.</returns>
Result<ResourceDesc, Error> GetResourceDescription(Handle<GPUResource> handle);
/// <summary>
/// Retrieves the bindless index associated with the specified GPU resource handle.
/// </summary>
/// <param name="handle">A handle to the GPU resource for which to obtain the bindless index. Must reference a valid, currently registered resource.</param>
/// <param name="access">The type of bindless access for which to obtain the index.</param>
/// <returns>The bindless index corresponding to the specified GPU resource handle. ~0 if the resource does not support bindless access or is not found.</returns>
uint GetBindlessIndex(Handle<GPUResource> handle, BindlessAccess access = BindlessAccess.ShaderResource);
/// <summary>
/// Retrieves the name of the GPU resource associated with the specified handle.
/// </summary>
/// <remarks>
/// You should only use this method in debug builds or inside engine editor.
/// </remarks>
/// <param name="handle">A handle to the GPU resource for which to obtain the name. Must reference a valid resource.</param>
/// <returns>The name of the GPU resource associated with the specified handle, or null if the resource does not have a name.</returns>
string? GetResourceName(Handle<GPUResource> handle);
/// <summary>
/// Releases the GPU resource associated with the specified handle, freeing any resources allocated to it.
/// </summary>
/// <param name="handle">The handle of the resource to be removed.</param>
void ScheduleReleaseResource(Handle<GPUResource> handle);
/// <summary>
/// Releases the GPU resource associated with the specified handle immediately, freeing any resources allocated to it.
/// </summary>
/// <param name="handle">The handle of the resource to be removed.</param>
void ReleaseResourceImmediately(Handle<GPUResource> handle);
/// <summary>
/// Retrieves an existing sampler identifier that matches the specified description, or creates a new one if none
/// exists.
/// </summary>
/// <param name="desc">A read-only reference to a <see cref="SamplerDesc"/> structure that defines the properties of the sampler to retrieve or create.</param>
/// <param name="id">An integer identifier to associate with the sampler.</param>
/// <returns>An <see cref="Identifier{Sampler}"/> representing the sampler that matches the specified description.
/// If a matching sampler does not exist, a new sampler is created and its identifier is returned.</returns>
Identifier<Sampler> CreateSampler(ref readonly SamplerDesc desc, int id);
/// <summary>
/// Determines whether a sampler with the specified identifier exists.
/// </summary>
/// <param name="id">The identifier of the sampler to check for existence.</param>
/// <returns>true if a sampler with the given identifier exists; otherwise, false.</returns>
bool TryGetSampler(ref readonly SamplerDesc desc, out Identifier<Sampler> id);
/// <summary>
/// Releases the sampler associated with the specified identifier and frees any resources allocated to it.
/// </summary>
/// <param name="id">The identifier of the sampler to release. Must reference a valid, existing sampler.</param>
void ReleaseSampler(Identifier<Sampler> id);
}

View File

@@ -0,0 +1,149 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.RHI;
public struct ShaderCompileResult : IDisposable
{
public UnsafeArray<byte> bytecode;
public ShaderReflectionData reflectionData;
public readonly bool IsCreated => bytecode.IsCreated;
public void Dispose()
{
bytecode.Dispose();
}
}
public struct GraphicsCompiledResult : IDisposable
{
public ShaderCompileResult tsResult;
public ShaderCompileResult msResult;
public ShaderCompileResult psResult;
public void Dispose()
{
tsResult.Dispose();
msResult.Dispose();
psResult.Dispose();
}
}
public ref struct ShaderCompilationConfig
{
public ReadOnlySpan<string> defines;
public ReadOnlySpan<string> includes;
public string shaderPath;
public string entryPoint;
public string? injectedCode;
public ShaderStage stage;
public CompilerTier tier;
public CompilerOptimizeLevel optimizeLevel;
public CompilerOption options;
}
public enum CompilerTier
{
Tier0,
Tier1,
Tier2
}
public enum CompilerOptimizeLevel
{
O0,
O1,
O2,
O3
}
[Flags]
public enum CompilerOption
{
None = 0,
KeepDebugInfo = 1 << 0,
KeepReflections = 1 << 1,
WarnAsError = 1 << 2,
SpirvCrossCompile = 1 << 3
}
public enum ShaderStage
{
TaskShader,
MeshShader,
PixelShader,
ComputeShader
}
public enum ShaderInputType
{
ConstantBuffer,
Texture,
Sampler,
UAV,
StructuredBuffer,
ByteAddressBuffer,
RWStructuredBuffer,
RWByteAddressBuffer
}
public struct ResourceBindingInfo
{
public string Name
{
get; set;
}
public ShaderInputType Type
{
get; set;
}
public uint BindPoint
{
get; set;
}
public uint BindCount
{
get; set;
}
public uint Space
{
get; set;
}
public uint Size
{
get; set;
}
public IReadOnlyList<CBufferPropertyInfo>? Properties
{
get; set;
}
}
public readonly struct ShaderReflectionData
{
public List<ResourceBindingInfo> ResourcesBindings
{
get;
}
public ShaderReflectionData()
{
ResourcesBindings = new List<ResourceBindingInfo>();
}
}
public interface IShaderCompiler : IDisposable
{
Result<ShaderCompileResult> Compile(ref readonly ShaderCompilationConfig config, Allocator allocator);
Result<GraphicsCompiledResult> CompilePass(ref readonly PassDescriptor descriptor, ref readonly ShaderCompilationConfig additionalConfig, Key64<ShaderVariant> key);
Result<GraphicsCompiledResult, Error> LoadCompiledCache(Key64<ShaderVariant> key);
}

View File

@@ -0,0 +1,73 @@
using Ghost.Core;
namespace Ghost.Graphics.RHI;
/// <summary>
/// Swap chain interface for presentation
/// </summary>
public interface ISwapChain : IDisposable
{
/// <summary>
/// Width of the swap chain back buffers
/// </summary>
uint Width
{
get;
}
/// <summary>
/// Height of the swap chain back buffers
/// </summary>
uint Height
{
get;
}
/// <summary>
/// Gets the horizontal scaling factor applied to the object. This is used for DPI scaling.
/// </summary>
float ScaleX
{
get;
}
/// <summary>
/// Gets the vertical scale factor applied to the object. This is used for DPI scaling.
/// </summary>
float ScaleY
{
get;
}
/// <summary>
/// Gets the current back buffer texture
/// </summary>
/// <returns>Current back buffer texture</returns>
Handle<Texture> GetCurrentBackBuffer();
/// <summary>
/// Gets all back buffer textures
/// </summary>
/// <returns>AlowBufferAndTexture back buffer textures</returns>
ReadOnlySpan<Handle<Texture>> GetBackBuffers();
/// <summary>
/// Presents the rendered frame
/// </summary>
/// <param name="vsync">Enable vertical synchronization</param>
void Present(bool vsync = true);
/// <summary>
/// Resizes the swap chain back buffers
/// </summary>
/// <param name="width">New Width</param>
/// <param name="height">New Height</param>
void Resize(uint width, uint height);
/// <summary>
/// Sets the horizontal and vertical scaling factors for the object.
/// </summary>
/// <param name="scaleX">The factor by which to scale the object along the X-axis.</param>
/// <param name="scaleY">The factor by which to scale the object along the Y-axis.</param>
void SetScale(float scaleX, float scaleY);
}

View File

@@ -0,0 +1,127 @@
using System.Runtime.Intrinsics;
using ElementType = uint;
namespace Ghost.Graphics.RHI;
public unsafe struct LocalKeywordSet
{
private const int _DATA_ARRAY_LENGTH = 4; // 4 * 32 = 128 bits
private const int _BITS_PER_ELEMENT = sizeof(ElementType) * 8;
private fixed ElementType _data[_DATA_ARRAY_LENGTH];
public void SetKeyword(int localIndex, bool enabled)
{
var index = localIndex / _BITS_PER_ELEMENT;
var bit = localIndex % _BITS_PER_ELEMENT;
if (enabled)
{
_data[index] |= (uint)(1 << bit);
}
else
{
_data[index] &= ~(uint)(1 << bit);
}
}
public bool IsKeywordEnabled(int localIndex)
{
var index = localIndex / _BITS_PER_ELEMENT;
var bit = localIndex % _BITS_PER_ELEMENT;
return (_data[index] & (uint)(1 << bit)) != 0;
}
public void Clear()
{
for (var i = 0; i < _DATA_ARRAY_LENGTH; i++)
{
_data[i] = 0;
}
}
public ulong GetHash64()
{
var hash = 14695981039346656037ul; // FNV Offset basis
for (var i = 0; i < _DATA_ARRAY_LENGTH; i++)
{
hash ^= _data[i];
hash *= 1099511628211ul; // FNV prime
}
return hash;
}
public override int GetHashCode()
{
var hash = 17;
for (var i = 0; i < _DATA_ARRAY_LENGTH; i++)
{
hash = hash * 31 + _data[i].GetHashCode();
}
return hash;
}
public static LocalKeywordSet operator |(in LocalKeywordSet a, in LocalKeywordSet b)
{
var result = default(LocalKeywordSet);
if (Vector128<ElementType>.IsSupported)
{
fixed (ElementType* pDataA = a._data)
fixed (ElementType* pDataB = b._data)
{
for (var i = 0; i < _DATA_ARRAY_LENGTH; i += Vector128<ElementType>.Count)
{
var elementOffset = (nuint)i;
var vecA = Vector128.LoadUnsafe(ref *pDataA, elementOffset);
var vecB = Vector128.LoadUnsafe(ref *pDataB, elementOffset);
var vecResult = Vector128.BitwiseOr(vecA, vecB);
vecResult.StoreUnsafe(ref result._data[0], elementOffset);
}
}
}
else
{
for (var i = 0; i < _DATA_ARRAY_LENGTH; i++)
{
result._data[i] = a._data[i] | b._data[i];
}
}
return result;
}
public static LocalKeywordSet operator &(in LocalKeywordSet a, in LocalKeywordSet b)
{
var result = default(LocalKeywordSet);
if (Vector128<ElementType>.IsSupported)
{
fixed (ElementType* pDataA = a._data)
fixed (ElementType* pDataB = b._data)
{
for (var i = 0; i < _DATA_ARRAY_LENGTH; i += Vector128<ElementType>.Count)
{
var elementOffset = (nuint)i;
var vecA = Vector128.LoadUnsafe(ref *pDataA, elementOffset);
var vecB = Vector128.LoadUnsafe(ref *pDataB, elementOffset);
var vecResult = Vector128.BitwiseAnd(vecA, vecB);
vecResult.StoreUnsafe(ref result._data[0], elementOffset);
}
}
}
else
{
for (var i = 0; i < _DATA_ARRAY_LENGTH; i++)
{
result._data[i] = a._data[i] & b._data[i];
}
}
return result;
}
}

View File

@@ -0,0 +1,183 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.Core.Utilities;
using System.IO.Hashing;
using System.Runtime.InteropServices;
namespace Ghost.Graphics.RHI;
public static class RHIUtility
{
public const int MAX_RENDER_TARGETS = 8;
public static uint GetBytesPerPixel(this TextureFormat format)
{
return format switch
{
TextureFormat.R8G8B8A8_UNorm => 4,
TextureFormat.B8G8R8A8_UNorm => 4,
TextureFormat.R16G16B16A16_Float => 8,
TextureFormat.R32G32B32A32_Float => 16,
TextureFormat.D24_UNorm_S8_UInt => 4,
TextureFormat.D32_Float => 4,
_ => throw new NotSupportedException($"Texture format {format} is not supported."),
};
}
public static uint GetTotalBytes(this TextureDesc desc)
{
return desc.Format.GetBytesPerPixel() * desc.Width * desc.Height * desc.Slice;
}
public static void GetSurfaceInfo(this TextureFormat format, uint width, uint height, out uint rowPitch, out uint slicePitch, out uint rowCount)
{
var bc = false;
var packed = false;
var planar = false;
var bpe = 0u;
//switch (Format)
//{
// case Format.BC1Typeless:
// case Format.BC1Unorm:
// case Format.BC1UnormSrgb:
// case Format.BC4Typeless:
// case Format.BC4Unorm:
// case Format.BC4Snorm:
// bc = true;
// bpe = 8;
// break;
// case Format.BC2Typeless:
// case Format.BC2Unorm:
// case Format.BC2UnormSrgb:
// case Format.BC3Typeless:
// case Format.BC3Unorm:
// case Format.BC3UnormSrgb:
// case Format.BC5Typeless:
// case Format.BC5Unorm:
// case Format.BC5Snorm:
// case Format.BC6HTypeless:
// case Format.BC6HUF16:
// case Format.BC6HSF16:
// case Format.BC7Typeless:
// case Format.BC7Unorm:
// case Format.BC7UnormSrgb:
// bc = true;
// bpe = 16;
// break;
// case Format.R8G8_B8G8Unorm:
// case Format.G8R8_G8B8Unorm:
// case Format.YUY2:
// packed = true;
// bpe = 4;
// break;
// case Format.Y210:
// case Format.Y216:
// packed = true;
// bpe = 8;
// break;
// case Format.NV12:
// case Format.Opaque420:
// case Format.P208:
// planar = true;
// bpe = 2;
// break;
// case Format.P010:
// case Format.P016:
// planar = true;
// bpe = 4;
// break;
// default:
// break;
//}
if (bc)
{
var numBlocksWide = 0u;
if (width > 0)
{
numBlocksWide = Math.Max(1u, (width + 3) / 4u);
}
var numBlocksHigh = 0u;
if (height > 0)
{
numBlocksHigh = Math.Max(1u, (height + 3) / 4u);
}
rowPitch = numBlocksWide * bpe;
rowCount = numBlocksHigh;
slicePitch = rowPitch * numBlocksHigh;
}
else if (packed)
{
rowPitch = ((width + 1u) >> 1) * bpe;
rowCount = height;
slicePitch = rowPitch * height;
}
else if (planar)
{
rowPitch = ((width + 1u) >> 1) * bpe;
slicePitch = (rowPitch * height) + ((rowPitch * height + 1) >> 1);
rowCount = height + ((height + 1u) >> 1);
}
else
{
var bpp = GetBytesPerPixel(format) * 8;
rowPitch = (width * bpp + 7) / 8; // round up to nearest byte
rowCount = height;
slicePitch = rowPitch * height;
}
}
public static Key64<ShaderPass> CreateShaderPassKey(string passID)
{
var passIdSpan = passID.AsSpan();
return new Key64<ShaderPass>(XxHash3.HashToUInt64(MemoryMarshal.AsBytes(passIdSpan)));
}
public static Key64<ShaderVariant> CreateShaderVariantKey(Key64<ShaderPass> passKey, ref readonly LocalKeywordSet keywords)
{
var passHash = passKey.Value;
var keywordHash = keywords.GetHash64();
return new Key64<ShaderVariant>(Hash.Hash64(passHash, keywordHash));
}
public static unsafe Key128<GraphicsPipeline> CreateGraphicsPipelineKey(Key64<ShaderVariant> shaderVariantKey, PipelineState pipelineState, PassPipelineHash passKey)
{
// Order-sensitive 128-bit mix. Cheap and stable, avoids span hashing.
static ulong Mix64(ulong x)
{
x ^= x >> 30;
x *= 0xBF58476D1CE4E5B9ul;
x ^= x >> 27;
x *= 0x94D049BB133111EBul;
x ^= x >> 31;
return x;
}
var mLo = shaderVariantKey.Value;
var mHi = pipelineState.GetHashCode64();
var pPasskey = (ulong*)&passKey.value;
var pLo = pPasskey[0];
var pHi = pPasskey[1];
// Distinct constants + cross-feeding to reduce structural collisions.
var lo = Mix64(mLo ^ (pLo + 0x9E3779B97F4A7C15ul) ^ (mHi * 0xD6E8FEB86659FD93ul));
var hi = Mix64(mHi ^ (pHi + 0xC2B2AE3D27D4EB4Ful) ^ (pLo * 0x165667B19E3779F9ul));
return new Key128<GraphicsPipeline>(new UInt128(lo, hi));
}
public static bool TryGetString(this Key128<GraphicsPipeline> key, Span<char> destination)
{
return key.Value.TryFormat(destination, out var _, "X16");
}
}

View File

@@ -0,0 +1,32 @@
using Ghost.Core;
namespace Ghost.Graphics.RHI;
public readonly struct GPUResource;
public readonly struct Texture;
public readonly struct GraphicsBuffer;
public readonly struct Sampler;
public static class ResourceHandleExtensions
{
public static Handle<GPUResource> AsResource(this Handle<Texture> texture)
{
return new Handle<GPUResource>(texture.ID, texture.Generation);
}
public static Handle<GPUResource> AsResource(this Handle<GraphicsBuffer> buffer)
{
return new Handle<GPUResource>(buffer.ID, buffer.Generation);
}
public static Handle<Texture> AsTexture(this Handle<GPUResource> resource)
{
return new Handle<Texture>(resource.ID, resource.Generation);
}
public static Handle<GraphicsBuffer> AsGraphicsBuffer(this Handle<GPUResource> resource)
{
return new Handle<GraphicsBuffer>(resource.ID, resource.Generation);
}
}

View File

@@ -0,0 +1,75 @@
using Misaki.HighPerformance.Mathematics;
using System.Runtime.InteropServices;
namespace Ghost.Graphics.RHI;
/// <summary>
/// The layout of the root signature is:
/// <list space="bullet">
/// <item>
/// Global buffer (b0)
/// </item>
/// <item>
/// Per-view buffer (b1)
/// </item>
/// <item>
/// Per-object buffer (b2)
/// </item>
/// <item>
/// Per-material buffer (b3)
/// </item>
/// <item>
/// Descriptor table for bindless textures (t0)
/// </item>
/// <item>
/// Descriptor table for bindless samplers (s0)
/// </item>
/// </list>
/// </summary>
public static class RootSignatureLayout
{
// public const int GLOBAL_BUFFER_SLOT = 0;
// public const int PER_VIEW_BUFFER_SLOT = 1;
// public const int PER_OBJECT_BUFFER_SLOT = 2;
// public const int PER_MATERIAL_BUFFER_SLOT = 3;
// public const int TEXTURE_HEAP_SLOT = 0;
// public const int SAMPLER_HEAP_SLOT = 0;
public const int PUSH_CONSTANT_SLOT = 0;
public const int ROOT_PARAMETER_COUNT = 1;
}
[StructLayout(LayoutKind.Sequential, Size = 16)]
public struct PushConstantsData
{
public uint globalIndex;
public uint viewIndex;
public uint objectIndex;
public uint materialIndex;
}
// The size should be 176 bytes (16-byte aligned)
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct PerViewData
{
public float4x4 viewMatrix;
public float4x4 projectionMatrix;
public float3 cameraPosition;
public float nearClip;
public float3 cameraDirection;
public float farClip;
public float4 screenSize; // xy: size, zw: 1/size
};
// The size should be 96 bytes (16-byte aligned)
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct PerObjectData
{
public float4x4 localToWorld;
public float3 worldBoundsMin;
public uint vertexBuffer;
public float3 worldBoundsMax;
public uint indexBuffer;
};