Update rendering architecture and resource management

Added a new `Ref<T>` struct for reference semantics.
Added the `RenderGraph` system for managing rendering passes.
Added the `RenderTexture` class for encapsulating GPU resources.
Added `GraphicsBuffer` class for effective GPU resource management.
Changed `CommandList` methods from public to internal for visibility control.
Changed `IRenderPass` interface from internal to public for accessibility.
Changed `GetData<T>()` in `ComponentObject.cs` to return `CompRef<T>`.
Changed `GetComponent<T>()` in `EntityManager.cs` to return `CompRef<T>`.
Changed `GetSingleton<T>()` in `World.cs` to use `CompRef<T>`.
Changed `IQueryTypeParameter` to use `CompRef<T>` for consistency.
Changed `QueryItem<T0>` and related structs to use `CompRef<T>`.
Changed `Material` class to support bindless textures.
Changed `Shader` class to support bindless rendering.
Changed `Mesh` class to support bindless vertex and index buffer access.
Updated documentation to reflect the new bindless rendering architecture.
This commit is contained in:
2025-08-01 21:34:48 +09:00
parent 1284bb17de
commit eafbfb2fa1
43 changed files with 3845 additions and 2183 deletions

View File

@@ -1,264 +1,84 @@
using Ghost.Graphics.D3D12;
using Ghost.Graphics.D3D12;
using Misaki.HighPerformance.Image;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Runtime.InteropServices;
using Win32;
using Win32.Graphics.Direct3D12;
using Win32.Graphics.Dxgi.Common;
namespace Ghost.Graphics.Data;
public unsafe class Texture2D : IDisposable
/// <summary>
/// Unified texture implementation that supports both bindless and regular descriptor table binding
/// for use with SM 6.6 ResourceDescriptorHeap access and traditional frame buffer textures
/// </summary>
public unsafe class Texture2D : Texture
{
private UnsafeArray<byte> _cpuData;
private readonly GraphicsResource _resource;
private readonly ShaderResourceDescriptor _srvDescriptor;
private bool _disposed;
public uint Width
{
get;
}
public uint Height
{
get;
}
public Format Format
{
get;
}
public uint BytesPerPixel
{
get;
}
private UnTypedArray _cpuData;
public uint Pitch
{
get;
}
public uint DataSize
private Texture2D(uint width, uint height, Format format, in TextureHandle handle, in ReadOnlySpan<byte> cpuData, BindlessDescriptor bindlessDescriptor)
: base(width, height, format, in handle, bindlessDescriptor)
{
get;
}
internal ShaderResourceDescriptor SRVDescriptor => _srvDescriptor;
public GraphicsResource? Resource => _resource;
public ReadOnlySpan<byte> CpuData => _cpuData.AsSpan();
public Texture2D(uint width, uint height, Span<byte> data, Format format)
{
Width = width;
Height = height;
Format = format;
BytesPerPixel = GetBytesPerPixel(format);
Pitch = width * BytesPerPixel;
DataSize = Pitch * height;
// Initialize CPU-side data
_cpuData = new((int)DataSize, Allocator.Persistent);
if (!data.IsEmpty)
uint alignment;
if (BytesPerPixel > 4)
{
if (data.Length != DataSize)
{
throw new ArgumentException($"Data size mismatch. Expected {DataSize} bytes, got {data.Length} bytes.");
}
data.CopyTo(_cpuData.AsSpan());
alignment = (uint)MemoryUtilities.AlignOf<float>();
}
else
{
alignment = (uint)MemoryUtilities.AlignOf<byte>();
}
_resource = CreateGpuResource();
_srvDescriptor = CreateShaderResourceView();
}
private static uint GetBytesPerPixel(Format format)
{
return format switch
_cpuData = new UnTypedArray((uint)Size, alignment, ref AllocationManager.PersistentHandle);
if (!cpuData.IsEmpty)
{
Format.R8G8B8A8Unorm => 4,
Format.R8G8B8A8UnormSrgb => 4,
Format.B8G8R8A8Unorm => 4,
Format.B8G8R8A8UnormSrgb => 4,
Format.R8G8B8A8Uint => 4,
Format.R8G8B8A8Sint => 4,
Format.R8G8B8A8Snorm => 4,
Format.R8G8Unorm => 2,
Format.R8G8Uint => 2,
Format.R8G8Sint => 2,
Format.R8G8Snorm => 2,
Format.R8Unorm => 1,
Format.R8Uint => 1,
Format.R8Sint => 1,
Format.R8Snorm => 1,
Format.A8Unorm => 1,
Format.R16G16B16A16Float => 8,
Format.R16G16B16A16Unorm => 8,
Format.R16G16B16A16Uint => 8,
Format.R16G16B16A16Sint => 8,
Format.R16G16B16A16Snorm => 8,
Format.R32G32B32A32Float => 16,
Format.R32G32B32A32Uint => 16,
Format.R32G32B32A32Sint => 16,
Format.R32G32B32Float => 12,
Format.R32G32B32Uint => 12,
Format.R32G32B32Sint => 12,
Format.R32G32Float => 8,
Format.R32G32Uint => 8,
Format.R32G32Sint => 8,
Format.R32Float => 4,
Format.R32Uint => 4,
Format.R32Sint => 4,
Format.R16G16Float => 4,
Format.R16G16Unorm => 4,
Format.R16G16Uint => 4,
Format.R16G16Sint => 4,
Format.R16G16Snorm => 4,
Format.R16Float => 2,
Format.R16Unorm => 2,
Format.R16Uint => 2,
Format.R16Sint => 2,
Format.R16Snorm => 2,
_ => throw new NotSupportedException($"Format {format} is not supported.")
};
}
private GraphicsResource CreateGpuResource()
{
var heapProperties = new HeapProperties(HeapType.Default);
var resourceDesc = ResourceDescription.Tex2D(Format, Width, Height);
ComPtr<ID3D12Resource> textureResource = default;
GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr->CreateCommittedResource(
&heapProperties,
HeapFlags.None,
&resourceDesc,
ResourceStates.Common,
null,
__uuidof<ID3D12Resource>(),
textureResource.GetVoidAddressOf()
);
return new(textureResource.Move());
}
private ShaderResourceDescriptor CreateShaderResourceView()
{
var srvDescriptor = GraphicsPipeline.DescriptorAllocator.AllocateSRV();
// Create the actual SRV
var srvDesc = new ShaderResourceViewDescription
{
Format = Format,
ViewDimension = SrvDimension.Texture2D,
Texture2D = new Texture2DSrv { MipLevels = 1 },
Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING
};
GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr->CreateShaderResourceView(_resource.NativeResource.Ptr, &srvDesc, srvDescriptor.CpuHandle);
return srvDescriptor;
}
private void CopyBufferToTexture(CommandList cmd, GraphicsResource srcBuffer, GraphicsResource dstTexture)
{
// Calculate the proper footprint for the placed subresource
var resourceDesc = dstTexture.NativeResource.Ptr->GetDesc();
PlacedSubresourceFootprint footprint = new()
{
Footprint = new SubresourceFootprint
if ((uint)cpuData.Length > Size)
{
Format = Format,
Width = Width,
Height = Height,
Depth = 1,
RowPitch = (uint)((Pitch + 255) & ~255) // Align to 256 bytes
throw new ArgumentException($"Data size mismatch. Expected {Size} bytes, got {cpuData.Length} bytes.");
}
};
var srcLocation = new TextureCopyLocation(srcBuffer.NativeResource.Ptr, footprint);
var dstLocation = new TextureCopyLocation(dstTexture.NativeResource.Ptr, 0);
_cpuData.CopyFrom(cpuData);
}
}
cmd.NativeCommandList.Ptr->CopyTextureRegion(&dstLocation, 0, 0, 0, &srcLocation, null);
public static Texture2D Create(uint width, uint height, in ReadOnlySpan<byte> data, Format format)
{
var handle = GraphicsPipeline.ResourceAllocator.CreateTexture2D(width, height, 0, format);
var bindlessDescriptor = CreateBindlessShaderResourceView(handle.ResourceHandle.GetAllocation().Resource, format);
return new Texture2D(width, height, format, in handle, in data, bindlessDescriptor);
}
public static Texture2D FromFile(string filePath)
{
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using var image = ImageResult.FromStream(stream, ColorComponents.RGBA);
return Create(image.Width, image.Height, image.AsSpan(), Format.R8G8B8A8Unorm);
}
/// <summary>
/// Sets the entire texture data on the CPU side.
/// </summary>
/// <param name="data">The texture data to set</param>
public void SetData(ReadOnlySpan<byte> data)
public void SetData<T>(ReadOnlySpan<T> data)
where T : unmanaged
{
ObjectDisposedException.ThrowIf(_disposed, this);
ThrowIfDisposed();
if (data.Length != DataSize)
if ((uint)data.Length > Size)
{
throw new ArgumentException($"Data size mismatch. Expected {DataSize} bytes, got {data.Length} bytes.");
throw new ArgumentException($"Data size mismatch. Expected {Size} bytes, got {data.Length} bytes.");
}
data.CopyTo(_cpuData.AsSpan());
}
/// <summary>
/// Sets the texture data for a specific region on the CPU side.
/// </summary>
/// <param name="data">The texture data to set</param>
/// <param name="x">Starting X coordinate</param>
/// <param name="y">Starting Y coordinate</param>
/// <param name="width">Width of the region</param>
/// <param name="height">Height of the region</param>
public void SetData(ReadOnlySpan<byte> data, uint x, uint y, uint width, uint height)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (x + width > Width || y + height > Height)
{
throw new ArgumentException("Region extends beyond texture bounds.");
}
var expectedSize = width * height * BytesPerPixel;
if (data.Length != expectedSize)
{
throw new ArgumentException($"Data size mismatch. Expected {expectedSize} bytes, got {data.Length} bytes.");
}
for (uint row = 0; row < height; row++)
{
var srcOffset = (int)(row * width * BytesPerPixel);
var dstOffset = (int)((y + row) * Pitch + x * BytesPerPixel);
var rowSize = (int)(width * BytesPerPixel);
data.Slice(srcOffset, rowSize).CopyTo(_cpuData.AsSpan().Slice(dstOffset, rowSize));
}
}
/// <summary>
/// Sets a single pixel value on the CPU side.
/// </summary>
/// <param name="x">X coordinate of the pixel</param>
/// <param name="y">Y coordinate of the pixel</param>
/// <param name="color">The color data for the pixel</param>
public void SetPixel(uint x, uint y, ReadOnlySpan<byte> color)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (x >= Width || y >= Height)
{
throw new ArgumentException("Pixel coordinates are outside texture bounds.");
}
if (color.Length != BytesPerPixel)
{
throw new ArgumentException($"Color data size mismatch. Expected {BytesPerPixel} bytes, got {color.Length} bytes.");
}
var offset = (int)(y * Pitch + x * BytesPerPixel);
color.CopyTo(_cpuData.AsSpan().Slice(offset, (int)BytesPerPixel));
_cpuData.CopyFrom(data);
}
/// <summary>
@@ -268,7 +88,8 @@ public unsafe class Texture2D : IDisposable
/// <param name="x">X coordinate of the pixel</param>
/// <param name="y">Y coordinate of the pixel</param>
/// <param name="color">The color value</param>
public void SetPixel<T>(uint x, uint y, T color) where T : unmanaged
public void SetPixel<T>(uint x, uint y, T color)
where T : unmanaged
{
if (sizeof(T) != BytesPerPixel)
{
@@ -279,6 +100,52 @@ public unsafe class Texture2D : IDisposable
SetPixel(x, y, colorSpan);
}
/// <summary>
/// Sets a single pixel value on the CPU side.
/// </summary>
/// <param name="x">X coordinate of the pixel</param>
/// <param name="y">Y coordinate of the pixel</param>
/// <param name="color">The color data for the pixel</param>
public void SetPixel(uint x, uint y, ReadOnlySpan<byte> color)
{
ThrowIfDisposed();
if (x >= Width || y >= Height)
{
throw new ArgumentException("Pixel coordinates are outside texture bounds.");
}
if (color.Length != BytesPerPixel)
{
throw new ArgumentException($"Color data size mismatch. Expected {BytesPerPixel} bytes, got {color.Length} bytes.");
}
var offset = y * Pitch + x * BytesPerPixel;
_cpuData.CopyFrom(color, 0u, offset, BytesPerPixel);
}
/// <summary>
/// Gets a single pixel value as a generic color type.
/// </summary>
/// <typeparam name="T">The color type (e.g., uint for RGBA32)</typeparam>
/// <param name="x">X coordinate of the pixel</param>
/// <param name="y">Y coordinate of the pixel</param>
/// <returns>The pixel color value</returns>
public T GetPixel<T>(uint x, uint y)
where T : unmanaged
{
if (sizeof(T) != BytesPerPixel)
{
throw new ArgumentException($"Color type size mismatch. Expected {BytesPerPixel} bytes, got {sizeof(T)} bytes.");
}
var pixelData = GetPixel(x, y);
fixed (byte* pPixel = pixelData)
{
return *(T*)pPixel;
}
}
/// <summary>
/// Gets a single pixel value from the CPU side data.
/// </summary>
@@ -287,7 +154,7 @@ public unsafe class Texture2D : IDisposable
/// <returns>The pixel color data</returns>
public ReadOnlySpan<byte> GetPixel(uint x, uint y)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ThrowIfDisposed();
if (x >= Width || y >= Height)
{
@@ -298,30 +165,12 @@ public unsafe class Texture2D : IDisposable
return _cpuData.AsSpan().Slice(offset, (int)BytesPerPixel);
}
/// <summary>
/// Gets a single pixel value as a generic color type.
/// </summary>
/// <typeparam name="T">The color type (e.g., uint for RGBA32)</typeparam>
/// <param name="x">X coordinate of the pixel</param>
/// <param name="y">Y coordinate of the pixel</param>
/// <returns>The pixel color value</returns>
public T GetPixel<T>(uint x, uint y) where T : unmanaged
{
if (sizeof(T) != BytesPerPixel)
{
throw new ArgumentException($"Color type size mismatch. Expected {BytesPerPixel} bytes, got {sizeof(T)} bytes.");
}
var pixelData = GetPixel(x, y);
return MemoryMarshal.Read<T>(pixelData);
}
/// <summary>
/// Uploads the CPU-side texture data to the GPU resource.
/// </summary>
public void UploadTextureData()
{
ObjectDisposedException.ThrowIf(_disposed, this);
ThrowIfDisposed();
Format.GetSurfaceInfo((int)Width, (int)Height, out var rowPitch, out var slicePitch);
var initData = new SubresourceData()
@@ -331,27 +180,15 @@ public unsafe class Texture2D : IDisposable
SlicePitch = slicePitch
};
using var uploadBatch = new ResourceUploadBatch();
uploadBatch.Begin();
uploadBatch.Transition(_resource, ResourceStates.Common, ResourceStates.CopyDest);
uploadBatch.Upload(_resource, 0, &initData, 1);
uploadBatch.Transition(_resource, ResourceStates.CopyDest, ResourceStates.PixelShaderResource);
uploadBatch.WaitForCompletion(uploadBatch.End());
var uploadBatch = GraphicsPipeline.UploadBatch;
uploadBatch.Transition(NativeResource, ResourceStates.Common, ResourceStates.CopyDest);
uploadBatch.Upload(NativeResource, 0, &initData, 1);
uploadBatch.Transition(NativeResource, ResourceStates.CopyDest, ResourceStates.PixelShaderResource);
}
public void Dispose()
public override void Dispose()
{
if (_disposed)
{
return;
}
base.Dispose();
_cpuData.Dispose();
_resource.Dispose();
GraphicsPipeline.DescriptorAllocator.ReleaseSRV(_srvDescriptor);
_disposed = true;
GC.SuppressFinalize(this);
}
}