feat: implement asynchronous asset management system with texture streaming support

This commit is contained in:
2026-04-20 01:09:59 +09:00
parent 4f5556ee1b
commit ed00f205b0
64 changed files with 1385 additions and 1157 deletions

View File

@@ -0,0 +1,15 @@
namespace Ghost.Core;
public enum AssetType : byte
{
Texture = 0,
Mesh = 1,
Material = 2,
Shaders = 3,
Audio = 4,
Scene = 5,
Video = 6,
Json = 7,
Unknown = 64,
}

View File

@@ -19,9 +19,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.7" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="1.6.1" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.13">
<PackageReference Include="Misaki.HighPerformance" Version="1.0.8" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="2.0.0" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.14">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -1,5 +1,3 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
@@ -14,7 +12,7 @@ public enum LogLevel
Debug
}
public class LogMessage
public readonly struct LogMessage
{
public LogLevel Level
{
@@ -55,34 +53,21 @@ public class LogMessage
}
}
public sealed class LogCollection : ReadOnlyObservableCollection<LogMessage>
{
public LogCollection(ObservableCollection<LogMessage> list)
: base(list)
{
}
public event NotifyCollectionChangedEventHandler? LogChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
base.OnCollectionChanged(args);
LogChanged?.Invoke(this, args);
}
}
public interface ILogger
{
LogCollection Logs
IReadOnlyCollection<LogMessage> Logs
{
get;
}
public bool CaptureStackTrace
bool CaptureStackTrace
{
get; set;
}
event Action<LogMessage> OnLogAdded;
event Action OnLogsCleared;
void Log(string message, LogLevel level);
void Log(Exception exception);
void Assert(bool condition, string message);
@@ -94,42 +79,48 @@ public static class Logger
// TODO: Add file logging.
private class LoggerImpl : ILogger
{
private readonly ObservableCollection<LogMessage> _logs = new();
private readonly LogCollection _readOnly;
private readonly Lock _lock = new();
private readonly List<LogMessage> _logs = new List<LogMessage>();
private readonly Lock _lock = new Lock();
public LogCollection Logs => _readOnly;
public IReadOnlyCollection<LogMessage> Logs => _logs;
public bool CaptureStackTrace
{
get; set;
} = true;
public LoggerImpl()
{
_readOnly = new LogCollection(_logs);
}
public event Action<LogMessage>? OnLogAdded;
public event Action? OnLogsCleared;
[StackTraceHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(string message, LogLevel level)
{
lock (_lock)
{
var stackTrace = CaptureStackTrace ? new StackTrace(true).ToString() : null;
_logs.Add(new LogMessage(level, message, stackTrace));
var logMessage = new LogMessage(level, message, stackTrace);
_logs.Add(logMessage);
OnLogAdded?.Invoke(logMessage);
}
}
[StackTraceHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(Exception exception)
{
lock (_lock)
{
_logs.Add(new LogMessage(LogLevel.Error, exception.Message, exception.StackTrace));
var logMessage = new LogMessage(LogLevel.Error, exception.Message, exception.StackTrace);
_logs.Add(logMessage);
OnLogAdded?.Invoke(logMessage);
}
}
[StackTraceHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Assert(bool condition, string message)
{
if (!condition)
@@ -138,11 +129,13 @@ public static class Logger
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Clear(bool includeFile = false)
{
lock (_lock)
{
_logs.Clear();
OnLogsCleared?.Invoke();
}
}
}
@@ -150,7 +143,7 @@ public static class Logger
private static readonly LoggerImpl s_logger = new LoggerImpl();
public static ILogger Impl => s_logger;
public static LogCollection Logs => s_logger.Logs;
public static IReadOnlyCollection<LogMessage> Logs => s_logger.Logs;
[StackTraceHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -67,6 +67,8 @@ public unsafe ref struct SpanWriter
readonly get => _position;
set => _position = value;
}
public readonly int RemainingBytes => _buffer.Length - _position;
public SpanWriter(Span<byte> buffer)
{
@@ -154,7 +156,7 @@ public unsafe struct BufferReader
public unsafe ref struct SpanReader
{
private readonly Span<byte> _buffer;
private readonly ReadOnlySpan<byte> _buffer;
private int _position;
public int Position
@@ -163,7 +165,9 @@ public unsafe ref struct SpanReader
set => _position = value;
}
public SpanReader(Span<byte> buffer)
public readonly int RemainingBytes => _buffer.Length - _position;
public SpanReader(ReadOnlySpan<byte> buffer)
{
_buffer = buffer;
_position = 0;
@@ -172,7 +176,7 @@ public unsafe ref struct SpanReader
public T Read<T>()
where T : unmanaged
{
var value = Unsafe.ReadUnaligned<T>(ref _buffer[_position]);
var value = Unsafe.ReadUnaligned<T>(in _buffer[_position]);
_position += Unsafe.SizeOf<T>();
return value;
}

View File

@@ -3,5 +3,7 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ghost.Editor")]
[assembly: InternalsVisibleTo("Ghost.Editor.Core")]
[assembly: InternalsVisibleTo("Ghost.UnitTest")]
[assembly: EngineAssembly]

View File

@@ -1,42 +0,0 @@
using Ghost.Core;
using Ghost.Core.Utilities;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Runtime.InteropServices;
namespace Ghost.Engine.AssetLoader;
internal sealed class TextureLoader : IRuntimeAssetLoader
{
public static readonly AssetType AssetType = AssetType.Texture;
public async ValueTask<Result<Asset>> LoadAsync(Stream cookedData, Guid id, CancellationToken token)
{
var header = new TextureContentHeader();
cookedData.ReadExactly(MemoryMarshal.AsBytes(new Span<TextureContentHeader>(ref header)));
var alignment = header.depth switch
{
8 => MemoryUtility.AlignOf<byte>(),
16 => MemoryUtility.AlignOf<ushort>(),
32 => MemoryUtility.AlignOf<float>(),
_ => MemoryUtility.AlignOf<float>()
};
var data = new MemoryBlock((nuint)(cookedData.Length - cookedData.Position), alignment, AllocationHandle.Persistent);
// C# built-in collections use int for indexing, so we need to ensure that the buffer size does not exceed int.MaxValue
var maxBufferSize = (int)Math.Min(0x7effffffu, header.width * header.height * header.depth / 8u * header.colorComponents);
var offset = 0u;
while (offset < data.Size)
{
using var memoryManager = NativeMemoryManager<byte>.FromMemoryBlock(data, (int)offset, maxBufferSize);
await cookedData.ReadExactlyAsync(memoryManager.Memory, token);
offset += (uint)memoryManager.Memory.Length;
}
return new TextureAsset(ref data, header, id);
}
}

View File

@@ -1,104 +1,146 @@
using Ghost.Core;
using Ghost.Core.Utilities;
using Ghost.Engine.AssetLoader;
using Ghost.Graphics;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Utilities;
using Misaki.HighPerformance.LowLevel.Buffer;
using TerraFX.Interop.Windows;
namespace Ghost.Engine;
public partial class AssetManager
internal partial class AssetEntry
{
private partial class AssetEntry
private unsafe class TextureData
{
private static TextureFormat GetTextureFormat(uint depth, uint colorComponents)
public TextureDesc desc;
public TextureContentHeader header;
public byte* pData;
public nuint dataSize;
}
private static void RegisterTextureCallback()
{
s_onCreation[(int)AssetType.Texture] = static (e) =>
{
return colorComponents switch
// This will create a new slot in the database, but not allocation any GPU resource.
// Everything in the slot will have the same value as the fallback texture, expect the slot will be marked as shared.
var handle = e._resourceDatabase.CreateShared(e._assetManager.FallbackTexture.AsResource()).AsTexture();
e.SetStorage(handle);
};
s_onParseRawData[(int)AssetType.Texture] = static (e) => e.ParseTextureData();
s_onRecordUpload[(int)AssetType.Texture] = static (e, ctx) => e.RecordTextureUpload(ctx);
s_onUploadComplete[(int)AssetType.Texture] = static (e, ctx) => e.OnTextureUploadComplete(ctx);
s_onReleaseResource[(int)AssetType.Texture] = static (e) =>
{
var handle = e.GetStorage<Handle<GPUTexture>>();
e._resourceDatabase.ReleaseResource(handle.AsResource());
};
}
private static TextureFormat GetTextureFormat(uint depth, uint colorComponents)
{
return colorComponents switch
{
1 => depth switch
{
1 => depth switch
{
8 => TextureFormat.R8_UNorm,
16 => TextureFormat.R16_UNorm,
32 => TextureFormat.R32_UInt,
_ => TextureFormat.Unknown,
},
2 => depth switch
{
8 => TextureFormat.R8G8_UNorm,
16 => TextureFormat.R16G16_UNorm,
32 => TextureFormat.R32G32_Float,
_ => TextureFormat.Unknown,
},
3 or 4 => depth switch
{
8 => TextureFormat.R8G8B8A8_UNorm,
16 => TextureFormat.R16G16B16A16_Float,
32 => TextureFormat.R32G32B32A32_Float,
_ => TextureFormat.Unknown,
},
8 => TextureFormat.R8_UNorm,
16 => TextureFormat.R16_UNorm,
32 => TextureFormat.R32_UInt,
_ => TextureFormat.Unknown,
};
}
private unsafe Result RecordTextureUpload(ICommandBuffer commandBuffer)
{
var pData = (byte*)_rawData.GetUnsafePtr();
var reader = new BufferReader(pData, _rawData.Size);
var header = reader.Read<TextureContentHeader>();
var textureDesc = new TextureDesc
},
2 => depth switch
{
Width = header.width,
Height = header.height,
MipLevels = header.mipLevels,
Slice = 1,
Format = GetTextureFormat(header.depth, header.colorComponents),
Dimension = (TextureDimension)header.dimension,
Usage = TextureUsage.ShaderResource,
};
var newHandle = RenderingUtility.CreateTexture(
ResourceManager,
ResourceDatabase,
ResourceAllocator,
commandBuffer,
reader.CurrentAddress,
reader.RemainingBytes,
in textureDesc);
if (newHandle.IsInvalid)
8 => TextureFormat.R8G8_UNorm,
16 => TextureFormat.R16G16_UNorm,
32 => TextureFormat.R32G32_Float,
_ => TextureFormat.Unknown,
},
3 or 4 => depth switch
{
return Result.Failure("Failed to create GPU texture.");
}
var oldHandle = GetStorage<Handle<GPUTexture>>();
SetStorage((oldHandle, newHandle));
return Result.Success();
}
private void OnTextureUploadComplete()
{
var (oldHandle, newHandle) = GetStorage<(Handle<GPUTexture>, Handle<GPUTexture>)>();
ResourceDatabase.Swap(oldHandle.AsResource(), newHandle.AsResource());
ResourceDatabase.ReleaseResource(newHandle.AsResource()); // releases old fallback slot
SetStorage((oldHandle, Handle<GPUTexture>.Invalid)); // Old handle is now the new handle, and the old fallback slot is released. Use Invalid handle to clear second slot.
_rawData.Dispose();
_rawData = default;
}
8 => TextureFormat.R8G8B8A8_UNorm,
16 => TextureFormat.R16G16B16A16_Float,
32 => TextureFormat.R32G32B32A32_Float,
_ => TextureFormat.Unknown,
},
_ => TextureFormat.Unknown,
};
}
private Handle<GPUTexture> AllocateTextureHandle()
private unsafe Result ParseTextureData()
{
// This will create a new slot in the database, but not allocation any GPU resource.
// Everything in the slot will have the same value as the fallback texture, expect the slot will be marked as shared.
return _resourceDatabase.CreateShared(_fallbackTexture.AsResource()).AsTexture();
var pData = (byte*)_rawData.GetUnsafePtr();
Logger.DebugAssert(pData != null);
var reader = new BufferReader(pData, _rawData.Size);
var header = reader.Read<TextureContentHeader>();
var textureDesc = new TextureDesc
{
Width = header.width,
Height = header.height,
MipLevels = header.mipLevels,
Slice = 1,
Format = GetTextureFormat(header.depth, header.colorComponents),
Dimension = (TextureDimension)header.dimension,
Usage = TextureUsage.ShaderResource,
};
// Will the gc be fine here?
var textureData = new TextureData
{
desc = textureDesc,
header = header,
pData = reader.CurrentAddress,
dataSize = reader.RemainingBytes,
};
_parsedObject = textureData;
return Result.Success();
}
private unsafe Result RecordTextureUpload(ResourceStreamingContext context)
{
var textureData = _parsedObject as TextureData;
Logger.DebugAssert(textureData != null);
var newHandle = RenderingUtility.CreateTexture(
context.ResourceManager,
context.ResourceDatabase,
context.ResourceAllocator,
context.CopyPipeline.GetCommandBuffer(),
textureData.pData,
textureData.dataSize,
in textureData.desc);
if (newHandle.IsInvalid)
{
return Result.Failure("Failed to create GPU texture.");
}
var oldHandle = GetStorage<Handle<GPUTexture>>();
SetStorage((oldHandle, newHandle));
return Result.Success();
}
private void OnTextureUploadComplete(ResourceStreamingContext context)
{
var (oldHandle, newHandle) = GetStorage<(Handle<GPUTexture>, Handle<GPUTexture>)>();
var actualHandle = context.ResourceDatabase.Replace(oldHandle.AsResource(), newHandle.AsResource());
context.GraphicsCommandBuffer.Barrier(BarrierDesc.Texture(oldHandle.AsResource(), BarrierSync.AllShading, BarrierAccess.ShaderResource, BarrierLayout.ShaderResource));
SetStorage((actualHandle, Handle<GPUTexture>.Invalid));
_rawData.Dispose();
_parsedObject = null;
}
}
internal partial class AssetManager
{
public Handle<GPUTexture> ResolveTexture(Guid assetID)
{
if (assetID == Guid.Empty)
@@ -111,4 +153,19 @@ public partial class AssetManager
return entry.GetStorage<Handle<GPUTexture>>();
}
public int ReleaseTexture(Guid assetID)
{
if (assetID == Guid.Empty)
{
return 0;
}
if (!_entries.TryGetValue(assetID, out var entry) || entry.AssetType != AssetType.Texture)
{
return 0;
}
return entry.Release();
}
}

View File

@@ -1,16 +1,17 @@
using Ghost.Core;
using Ghost.Core.Utilities;
using Ghost.Graphics;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Engine;
@@ -25,20 +26,7 @@ public enum AssetState : byte
Failed = 6,
}
public enum AssetType : byte
{
Texture = 0,
Mesh = 1,
Material = 2,
Audio = 3,
Scene = 4,
Video = 5,
Json = 6,
Unknown = 255,
}
internal interface IContentProvider
public interface IContentProvider
{
bool HasAsset(Guid guid);
@@ -49,258 +37,249 @@ internal interface IContentProvider
AssetType GetAssetType(Guid guid);
}
// TODO: Support DirectStorage.
public partial class AssetManager : IDisposable
internal partial class AssetEntry
{
private unsafe partial class AssetEntry : IDisposable
private static readonly Action<AssetEntry>[] s_onCreation = new Action<AssetEntry>[(int)AssetType.Unknown + 1];
private static readonly Func<AssetEntry, Result>[] s_onParseRawData = new Func<AssetEntry, Result>[(int)AssetType.Unknown + 1];
private static readonly Action<AssetEntry, ResourceStreamingContext>[] s_onRecordUpload = new Action<AssetEntry, ResourceStreamingContext>[(int)AssetType.Unknown + 1];
private static readonly Action<AssetEntry, ResourceStreamingContext>[] s_onUploadComplete = new Action<AssetEntry, ResourceStreamingContext>[(int)AssetType.Unknown + 1];
private static readonly Action<AssetEntry>[] s_onReleaseResource = new Action<AssetEntry>[(int)AssetType.Unknown + 1];
static AssetEntry()
{
public struct __storage
{
public fixed byte data[64];
}
RegisterTextureCallback();
}
}
private readonly AssetManager _assetManager;
private Guid _assetId;
private __storage _storage;
private MemoryBlock _rawData;
private JobHandle _loadJobHandle;
private AssetType _assetType;
private int _refCount;
private int _state;
private ResourceManager ResourceManager => _assetManager._resourceManager;
private IResourceDatabase ResourceDatabase => _assetManager._resourceDatabase;
private IResourceAllocator ResourceAllocator => _assetManager._resourceAllocator;
public Guid AssetId => _assetId;
public MemoryBlock RawData => _rawData;
public JobHandle LoadJobHandle => _loadJobHandle;
public AssetType AssetType => _assetType;
public int RefCount => Volatile.Read(ref _refCount);
public AssetState State
{
get => (AssetState)Volatile.Read(ref _state);
set => Volatile.Write(ref _state, (int)value);
}
public AssetEntry(AssetManager manager, Guid assetId, AssetType assetType)
{
_assetManager = manager;
_assetId = assetId;
_assetType = assetType;
_refCount = 1;
switch (assetType)
{
case AssetType.Texture:
SetStorage(manager.AllocateTextureHandle());
break;
case AssetType.Mesh:
break;
case AssetType.Material:
break;
case AssetType.Audio:
break;
case AssetType.Scene:
break;
case AssetType.Video:
break;
case AssetType.Json:
break;
case AssetType.Unknown:
default:
break;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetStorage<T>(T asset)
where T : unmanaged
{
Unsafe.WriteUnaligned(ref _storage.data[0], asset);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T GetStorage<T>()
where T : unmanaged
{
return Unsafe.ReadUnaligned<T>(ref _storage.data[0]);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetRawData([OwnershipTransfer] ref MemoryBlock data)
{
_rawData = data;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetLoadJobHandle(JobHandle handle)
{
_loadJobHandle = handle;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddRef()
{
Interlocked.Increment(ref _refCount);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Release()
{
var newRefCount = Interlocked.Decrement(ref _refCount);
Debug.Assert(newRefCount >= 0, "Reference count should not be negative");
if (newRefCount == 0)
{
Dispose();
}
return newRefCount;
}
public void OnRecordUploadCommands(ICommandBuffer commandBuffer)
{
switch (_assetType)
{
case AssetType.Texture:
RecordTextureUpload(commandBuffer);
break;
case AssetType.Mesh:
break;
case AssetType.Material:
break;
case AssetType.Audio:
break;
case AssetType.Scene:
break;
case AssetType.Video:
break;
case AssetType.Json:
break;
case AssetType.Unknown:
break;
default:
break;
}
}
public void OnUploadComplete()
{
switch (_assetType)
{
case AssetType.Texture:
OnTextureUploadComplete();
break;
case AssetType.Mesh:
break;
case AssetType.Material:
break;
case AssetType.Audio:
break;
case AssetType.Scene:
break;
case AssetType.Video:
break;
case AssetType.Json:
break;
case AssetType.Unknown:
break;
default:
break;
}
Volatile.Write(ref _state, (int)AssetState.Ready);
}
public void Dispose()
{
var handle = GetStorage<Handle<GPUTexture>>();
ResourceDatabase.ReleaseResource(handle.AsResource());
_assetManager.RemoveEntry(_assetId);
}
internal unsafe partial class AssetEntry
{
private struct Storage
{
public fixed byte data[64];
}
private struct LoadAssetJob : IJob
{
public Guid assetID;
public AssetType assetType;
private static Result LoadRawData(IContentProvider contentProvider, AssetEntry entry)
{
try
{
using var stream = contentProvider.OpenRead(entry.AssetId).GetValueOrThrow();
var data = new MemoryBlock((nuint)stream.Length, MemoryUtility.AlignOf<IntPtr>(), AllocationHandle.Persistent);
// C# built-in collections use int for indexing, so we need to ensure that the buffer size does not exceed int.MaxValue
var maxChunkSize = (int)Math.Min(0x7fffffffu, data.Size);
var offset = 0u;
while (offset < data.Size)
{
using var mem = NativeMemoryManager<byte>.FromMemoryBlock(data, (int)offset, maxChunkSize);
stream.ReadExactly(mem.Memory.Span);
offset += (uint)mem.Memory.Length;
}
entry.SetRawData(ref data);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure(ex.Message);
}
}
public void Execute(ref readonly JobExecutionContext ctx)
{
var assetManager = ctx.State as AssetManager;
Debug.Assert(assetManager is not null);
Debug.Assert(assetManager._contentProvider.GetAssetType(assetID) == assetType);
if (!assetManager._entries.TryGetValue(assetID, out var entry))
{
Logger.Error($"Asset entry not found for {assetID}");
return;
}
entry.State = AssetState.Loading;
var result = LoadRawData(assetManager._contentProvider, entry);
if (result.IsFailure)
{
entry.State = AssetState.Failed;
Logger.Error($"Failed to load asset {assetID}: {result.Message}");
return;
}
entry.State = AssetState.Loaded;
}
}
private const int _MAX_UPLOADS_PER_FRAME = 8;
private readonly IContentProvider _contentProvider;
private readonly ResourceManager _resourceManager;
private readonly IResourceAllocator _resourceAllocator;
private readonly AssetManager _assetManager;
private readonly IResourceDatabase _resourceDatabase;
private readonly AsyncCopyPipeline _copyPipeline; // Upload via copy queue.
private readonly Guid _assetId;
private readonly AssetType _assetType;
private readonly Guid[] _dependencies;
private Storage _storage;
private MemoryBlock _rawData;
private object? _parsedObject;
private JobHandle _loadJobHandle;
private int _refCount;
private int _state;
private bool _pendingReimport;
public Guid AssetId => _assetId;
public MemoryBlock RawData => _rawData;
public JobHandle LoadJobHandle => _loadJobHandle;
public AssetType AssetType => _assetType;
public ReadOnlySpan<Guid> Dependencies => _dependencies;
public int RefCount => Volatile.Read(ref _refCount);
public ref int StateValue => ref _state;
public AssetState State
{
get => (AssetState)Volatile.Read(ref _state);
set => Volatile.Write(ref _state, (int)value);
}
public AssetEntry(AssetManager manager, IResourceDatabase resourceDatabase, Guid assetId, AssetType assetType, Guid[] dependencies)
{
_assetManager = manager;
_resourceDatabase = resourceDatabase;
_assetId = assetId;
_assetType = assetType;
_dependencies = dependencies;
s_onCreation[(int)_assetType]?.Invoke(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetStorage<T>(T asset)
where T : unmanaged
{
Unsafe.WriteUnaligned(ref _storage.data[0], asset);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T GetStorage<T>()
where T : unmanaged
{
return Unsafe.ReadUnaligned<T>(ref _storage.data[0]);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetRawData([OwnershipTransfer] ref MemoryBlock data)
{
_rawData = data;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetLoadJobHandle(JobHandle handle)
{
_loadJobHandle = handle;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetPendingReimport()
{
Volatile.Write(ref _pendingReimport, true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddRef()
{
Interlocked.Increment(ref _refCount);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Release()
{
Logger.DebugAssert(State == AssetState.Ready);
var newRefCount = Interlocked.Decrement(ref _refCount);
Logger.DebugAssert(newRefCount >= 0, "Reference count should not be negative");
if (newRefCount == 0)
{
_assetManager.RemoveEntry(_assetId);
OnReleaseResource();
foreach (var dep in _dependencies)
{
if (_assetManager.TryGetEntry(dep, out var entry))
{
entry.Release();
}
}
}
return newRefCount;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Result OnParseRawData()
{
return s_onParseRawData[(int)_assetType]?.Invoke(this) ?? Result.Failure("Unsupported asset type.");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void OnRecordUploadCommands(ResourceStreamingContext context)
{
s_onRecordUpload[(int)_assetType]?.Invoke(this, context);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void OnUploadComplete(ResourceStreamingContext context)
{
s_onUploadComplete[(int)_assetType]?.Invoke(this, context);
Volatile.Write(ref _state, (int)AssetState.Ready);
if (Interlocked.CompareExchange(ref _pendingReimport, false, true))
{
_assetManager.InvalidateAsset(_assetId); // re-queue
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void OnReleaseResource()
{
s_onReleaseResource[(int)_assetType]?.Invoke(this);
}
}
internal struct LoadAssetJob : IJob
{
public Guid assetID;
public AssetType assetType;
public GCHandle assetManagerHandle;
private static Result LoadRawData(IContentProvider contentProvider, AssetEntry entry)
{
try
{
using var stream = contentProvider.OpenRead(entry.AssetId).GetValueOrThrow();
var data = new MemoryBlock((nuint)stream.Length, MemoryUtility.AlignOf<IntPtr>(), AllocationHandle.Persistent);
// C# built-in collections use int for indexing, so we need to ensure that the buffer size does not exceed int.MaxValue
var maxChunkSize = (int)Math.Min(0x7fffffffu, data.Size);
var offset = 0u;
while (offset < data.Size)
{
using var mem = NativeMemoryManager<byte>.FromMemoryBlock(data, (int)offset, maxChunkSize);
stream.ReadExactly(mem.Memory.Span);
offset += (uint)mem.Memory.Length;
}
entry.SetRawData(ref data);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure(ex.Message);
}
}
public readonly void Execute(ref readonly JobExecutionContext ctx)
{
var assetManager = assetManagerHandle.Target as AssetManager;
Logger.DebugAssert(assetManager is not null);
if (!assetManager.TryGetEntry(assetID, out var entry))
{
Logger.Error($"Asset entry not found for {assetID}");
return;
}
Logger.DebugAssert(entry.AssetType == assetType);
Logger.DebugAssert(entry.State == AssetState.Scheduled);
entry.State = AssetState.Loading;
var result = LoadRawData(assetManager.ContentProvider, entry);
if (result.IsFailure)
{
entry.State = AssetState.Failed;
Logger.Error($"Failed to load asset {assetID}: {result.Message}");
return;
}
result = entry.OnParseRawData();
if (result.IsFailure)
{
entry.State = AssetState.Failed;
Logger.Error($"Failed to parse asset {assetID}: {result.Message}");
return;
}
entry.State = AssetState.Loaded;
assetManager.StreamingProcessor.EnqueueForUpload(entry);
}
}
// TODO: Support DirectStorage.
internal partial class AssetManager : IDisposable
{
private readonly IResourceDatabase _resourceDatabase;
private readonly IContentProvider _contentProvider;
private readonly ResourceStreamingProcessor _streamingProcessor;
private readonly JobScheduler _jobScheduler;
private readonly ConcurrentDictionary<Guid, AssetEntry> _entries;
private readonly ConcurrentQueue<AssetEntry> _pendingFinalize;
private ulong _pendingCopyFenceValue;
private readonly ConcurrentDictionary<Guid, AssetEntry> _entries;
private GCHandle _selfHandle;
// TODO
private Handle<GPUTexture> _fallbackTexture;
@@ -308,57 +287,56 @@ public partial class AssetManager : IDisposable
private Handle<Mesh> _fallbackMesh;
private Handle<Material> _fallbackMaterial;
internal AssetManager(IContentProvider contentProvider, ResourceManager resourceManager, IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase, AsyncCopyPipeline uploadBatch)
public IContentProvider ContentProvider => _contentProvider;
public ResourceStreamingProcessor StreamingProcessor => _streamingProcessor;
public Handle<GPUTexture> FallbackTexture => _fallbackTexture;
internal AssetManager(IResourceDatabase resourceDatabase, IContentProvider contentProvider, ResourceStreamingProcessor streamingProcessor, JobScheduler jobScheduler)
{
_contentProvider = contentProvider;
_resourceManager = resourceManager;
_resourceAllocator = resourceAllocator;
_resourceDatabase = resourceDatabase;
_copyPipeline = uploadBatch;
_contentProvider = contentProvider;
_streamingProcessor = streamingProcessor;
_jobScheduler = jobScheduler;
var desc = new JobSchedulerDesc
{
ThreadCount = Environment.ProcessorCount < 8 ? 1 : 2,
ThreadPriority = ThreadPriority.BelowNormal,
State = this,
};
_jobScheduler = new JobScheduler(in desc);
_entries = new ConcurrentDictionary<Guid, AssetEntry>();
_pendingFinalize = new ConcurrentQueue<AssetEntry>();
_selfHandle = GCHandle.Alloc(this, GCHandleType.Normal);
}
internal bool TryGetEntry(Guid guid, [NotNullWhen(true)] out AssetEntry? entry)
{
return _entries.TryGetValue(guid, out entry);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool RemoveEntry(Guid guid)
internal bool RemoveEntry(Guid guid)
{
return _entries.TryRemove(guid, out _);
return _entries.TryRemove(guid, out var entry);
}
private void EnsureScheduled(AssetEntry entry)
{
if ((int)entry.State >= (int)AssetState.Scheduled)
if (Interlocked.CompareExchange(ref entry.StateValue, (int)AssetState.Scheduled, (int)AssetState.Unloaded) != (int)AssetState.Unloaded)
{
return;
}
// Resolve dependencies (in-memory manifest/catalog lookup — instant)
var deps = _contentProvider.GetDependencies(entry.AssetId);
// TODO: Can this be jobified? If the dependency tree is not deep, it should be fine to do it in main thread, otherwise we might need to schedule a job to do it.
var dependency = JobHandle.Invalid;
if (deps.Length > 0)
if (entry.Dependencies.Length > 0)
{
// Avoid stack overflow for deep dependency tree like a whole scene.
// Avoid stack overflow for deep dependency tree like a scene.
// Stack allocator here is fine, because it use virtual memory and has 32 mb capacity per thread.
using var scope = AllocationManager.CreateStackScope();
using var list = new UnsafeList<Guid>(deps.Length * 2, scope.AllocationHandle);
using var stack = new UnsafeStack<Guid>(deps.Length * 2, scope.AllocationHandle);
using var visited = new UnsafeHashSet<Guid>(deps.Length * 2, scope.AllocationHandle);
using var list = new UnsafeList<Guid>(entry.Dependencies.Length * 2, scope.AllocationHandle);
using var stack = new UnsafeStack<Guid>(entry.Dependencies.Length * 2, scope.AllocationHandle);
using var visited = new UnsafeHashSet<Guid>(entry.Dependencies.Length * 2, scope.AllocationHandle);
for (var i = 0; i < deps.Length; i++)
for (var i = 0; i < entry.Dependencies.Length; i++)
{
stack.Push(deps[i]);
stack.Push(entry.Dependencies[i]);
}
while (stack.TryPop(out var guid))
@@ -388,7 +366,7 @@ public partial class AssetManager : IDisposable
{
// This should create the entry and schedule the job on those assets does not have any dependency first.
var handle = GetOrCreateEntry(list[i]).LoadJobHandle;
Debug.Assert(handle.IsValid);
Logger.DebugAssert(handle.IsValid);
depHandles.Add(handle);
}
@@ -400,85 +378,59 @@ public partial class AssetManager : IDisposable
{
assetID = entry.AssetId,
assetType = entry.AssetType,
assetManagerHandle = _selfHandle,
};
entry.SetLoadJobHandle(_jobScheduler.Schedule(ref job, dependency));
entry.SetLoadJobHandle(_jobScheduler.Schedule(ref job, dependency, JobPriority.Low)); // Use low priority to avoid blocking main thread critical tasks like rendering and physics.
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private AssetEntry GetOrCreateEntry(Guid guid)
{
return _entries.GetOrAdd(guid, static (id, self) =>
var entry = _entries.GetOrAdd(guid, static (id, self) =>
{
var entry = new AssetEntry(self, id, self._contentProvider.GetAssetType(id))
{
State = AssetState.Scheduled
};
var type = self._contentProvider.GetAssetType(id);
var deps = self._contentProvider.GetDependencies(id);
var entry = new AssetEntry(self, self._resourceDatabase, id, type, deps);
self.EnsureScheduled(entry);
return entry;
}, this);
entry.AddRef();
return entry;
}
// NOTE: Render thread only.
internal void ProcessPendingUploads()
public void InvalidateAsset(Guid guid)
{
// 1. If there's a pending copy batch from last frame, check its fence
if (_pendingCopyFenceValue > 0 && _copyPipeline.CurrentFenceValue() >= _pendingCopyFenceValue)
{
while (_pendingFinalize.TryDequeue(out var item))
{
item.OnUploadComplete();
}
_pendingCopyFenceValue = 0;
}
if (_pendingCopyFenceValue > 0)
if (!_entries.TryGetValue(guid, out var entry))
{
return;
}
// 2. Collect entries that are in state == Loaded (I/O done, not yet uploaded)
// Cap per frame to avoid stalling (e.g., max 8 textures per frame)
_copyPipeline.Begin();
var cmdCopy = _copyPipeline.GetCommandBuffer();
var uploadCount = 0;
foreach (var (guid, entry) in _entries)
if (entry.State is AssetState.Loading or AssetState.Loaded or AssetState.Uploading)
{
if (entry.State != AssetState.Loaded)
{
continue;
}
if (uploadCount >= _MAX_UPLOADS_PER_FRAME)
{
break;
}
// Record copy commands into cmdCopy
entry.OnRecordUploadCommands(cmdCopy);
entry.State = AssetState.Uploading;
_pendingFinalize.Enqueue(entry);
uploadCount++;
entry.SetPendingReimport();
return;
}
// 3. Submit the batch
if (uploadCount > 0)
{
var result = _copyPipeline.End();
if (result.IsSuccess)
{
_pendingCopyFenceValue = _copyPipeline.SignaledFenceValue();
}
}
// Entry is in Ready state — the old texture is valid and will remain visible.
// Go directly to Scheduled → Loading → Loaded → Uploading → Ready again.
// The swap cycle in RecordTextureUpload/OnTextureUploadComplete handles the
// v1 → v2 transition exactly like the fallback → v1 transition.
entry.State = AssetState.Scheduled;
EnsureScheduled(entry);
}
public void Dispose()
{
throw new NotImplementedException();
foreach (var entry in _entries.Values)
{
entry.OnReleaseResource();
}
_entries.Clear();
_selfHandle.Free();
}
}

View File

@@ -6,29 +6,42 @@ namespace Ghost.Engine;
public sealed partial class EngineCore : IDisposable
{
private readonly IContentProvider _contentProvider;
private readonly JobScheduler _jobScheduler;
private readonly ResourceStreamingProcessor _streamingProcessor;
private readonly RenderSystem _renderSystem;
private readonly AssetManager _assetManager;
public JobScheduler JobScheduler => _jobScheduler;
public RenderSystem RenderSystem => _renderSystem;
public EngineCore()
public EngineCore(IContentProvider contentProvider)
{
_jobScheduler = new JobScheduler(Environment.ProcessorCount - 2); // We -2 here, one for main thread, one for render thread
_contentProvider = contentProvider;
var renderingConfig = new RenderSystemDesc
var desc = new JobSchedulerDesc
{
ThreadCount = Environment.ProcessorCount - 2, // We -2 here, one for main thread, one for render thread
ThreadPriority = ThreadPriority.Normal,
};
_jobScheduler = new JobScheduler(in desc);
_streamingProcessor = new ResourceStreamingProcessor();
var renderingDesc = new RenderSystemDesc
{
FrameBufferCount = 2,
GraphicsAPI = GraphicsAPI.Direct3D12,
InitialRenderPipelineSettings = new GhostRenderPipelineSettings(),
ResourceStreamingProcessor = _streamingProcessor,
ShaderCacheDirectory = "ShaderCache",
};
_renderSystem = new RenderSystem(renderingConfig);
_renderSystem = new RenderSystem(renderingDesc);
_assetManager = new AssetManager(_renderSystem.GraphicsEngine.ResourceDatabase, _contentProvider, _streamingProcessor, _jobScheduler);
}
public void Dispose()
{
_assetManager.Dispose();
_renderSystem.Dispose();
_jobScheduler.Dispose();
}

View File

@@ -55,7 +55,6 @@ internal unsafe class GPUScene : IDisposable
Dispose();
}
// NOTE: This is not thread safe.
public void ResizeIfNeeded(ICommandBuffer cmd)
{
if (_requiredResize == 0)
@@ -107,6 +106,8 @@ internal unsafe class GPUScene : IDisposable
// Return the last index. We will swap the last instance data with the removed index on gpu to keep the buffer compact.
var last = Interlocked.Decrement(ref _instanceCount);
Logger.DebugAssert(last >= 0);
return last;
}

View File

@@ -1,5 +1,6 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.Core.Utilities;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
@@ -13,28 +14,47 @@ namespace Ghost.Engine.RenderPipeline;
public partial struct UpdateGPUSceneShaderProperty
{
public uint gpuSceneBuffer;
public uint addBuffer;
public uint addCount;
public uint updateBuffer;
public uint updateCount;
public uint removeBuffer;
public uint removeCount;
}
internal partial class GhostRenderPipeline
{
private static unsafe Handle<GPUBuffer> CreateAddInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase, out int count)
private struct UpdateInstanceData
{
public float4x4 localToWorld;
public uint instanceID;
public uint meshBuffer;
public uint materialPalette;
public uint renderingLayerMask;
public uint shadowCastingMode;
}
private struct RemoveInstanceData
{
public uint instanceID;
public uint swapWithInstanceID;
}
private static unsafe Handle<GPUBuffer> CreateUpdateInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase, out int count)
{
// TODO: This should also include update requests like transform update, material update, etc.
var totalUpdateCount = ghostPayload.AddRequest.Count; // + ghostPayload.UpdateRequest.Count;
if (!ghostPayload.AddRequest.IsEmpty)
{
var addDesc = new BufferDesc
{
Size = (nuint)ghostPayload.AddRequest.Count * MemoryUtility.SizeOf<AddInstanceData>(),
Stride = (uint)MemoryUtility.SizeOf<AddInstanceData>(),
Size = (nuint)ghostPayload.AddRequest.Count * MemoryUtility.SizeOf<UpdateInstanceData>(),
Stride = (uint)MemoryUtility.SizeOf<UpdateInstanceData>(),
Usage = BufferUsage.Structured | BufferUsage.ShaderResource,
HeapType = HeapType.Upload
};
var addBuffer = resourceManager.CreateTransientBuffer(in addDesc, "Add Instance Buffer");
var pAddData = (AddInstanceData*)resourceDatabase.MapResource(addBuffer.AsResource(), 0, null);
var pAddData = (UpdateInstanceData*)resourceDatabase.MapResource(addBuffer.AsResource(), 0, null);
var i = 0;
while (ghostPayload.AddRequest.TryDequeue(out var addRequest))
@@ -46,7 +66,7 @@ internal partial class GhostRenderPipeline
continue;
}
pAddData[i] = new AddInstanceData
pAddData[i] = new UpdateInstanceData
{
localToWorld = addRequest.localToWorld,
instanceID = addRequest.instanceId,
@@ -106,14 +126,16 @@ internal partial class GhostRenderPipeline
return default;
}
public void UpdateGPUScene(RenderContext ctx, GhostRenderPayload payload)
private void UpdateGPUScene(RenderContext ctx, GhostRenderPayload payload)
{
var addBuffer = CreateAddInstanceBuffer(payload, ctx.ResourceManager, ctx.ResourceDatabase, out var addCount);
_gpuScene.ResizeIfNeeded(ctx.CommandBuffer);
var updateBuffer = CreateUpdateInstanceBuffer(payload, ctx.ResourceManager, ctx.ResourceDatabase, out var updateCount);
var removeBuffer = CreateRemoveInstanceBuffer(payload, ctx.ResourceManager, ctx.ResourceDatabase, out var removeCount);
if (addCount <= 0 && removeCount <= 0)
if (updateCount <= 0 && removeCount <= 0)
{
Logger.DebugAssert(addBuffer.IsInvalid && removeBuffer.IsInvalid, "Buffers should be invalid when there are no updates.");
Logger.DebugAssert(updateBuffer.IsInvalid && removeBuffer.IsInvalid, "Buffers should be invalid when there are no updates.");
return; // No updates needed
}
@@ -126,8 +148,8 @@ internal partial class GhostRenderPipeline
var property = new UpdateGPUSceneShaderProperty
{
gpuSceneBuffer = ctx.ResourceDatabase.GetBindlessIndex(_gpuScene.SceneBuffer.AsResource(), BindlessAccess.UnorderedAccess),
addBuffer = ctx.ResourceDatabase.GetBindlessIndex(addBuffer.AsResource()),
addCount = (uint)addCount,
updateBuffer = ctx.ResourceDatabase.GetBindlessIndex(updateBuffer.AsResource()),
updateCount = (uint)updateCount,
removeBuffer = ctx.ResourceDatabase.GetBindlessIndex(removeBuffer.AsResource()),
removeCount = (uint)removeCount
};

View File

@@ -2,28 +2,11 @@ using Ghost.Core;
using Ghost.Graphics;
using Ghost.Graphics.Core;
using Ghost.Graphics.RenderGraphModule;
using Misaki.HighPerformance.Mathematics;
namespace Ghost.Engine.RenderPipeline;
internal partial class GhostRenderPipeline : IRenderPipeline
{
private struct AddInstanceData
{
public float4x4 localToWorld;
public uint instanceID;
public uint meshBuffer;
public uint materialPalette;
public uint renderingLayerMask;
public uint shadowCastingMode;
}
private struct RemoveInstanceData
{
public uint instanceID;
public uint swapWithInstanceID;
}
private readonly RenderSystem _renderSystem;
private readonly RenderGraph _renderGraph;
@@ -39,7 +22,7 @@ internal partial class GhostRenderPipeline : IRenderPipeline
_gpuScene = new GPUScene(renderSystem.GraphicsEngine.ResourceAllocator, renderSystem.GraphicsEngine.ResourceDatabase, 102_400u); // 102.4k objects should be enough for now
}
public void Render(RenderContext ctx, int frameIndex, IRenderPayload payload)
public void Render(RenderContext ctx, int frameIndex, IRenderPayload payload, ReadOnlySpan<byte> resourceUpdateCommands)
{
var ghostPayload = (GhostRenderPayload)payload;

View File

@@ -1,3 +1,4 @@
using Ghost.Core;
using Ghost.Engine.Components;
using Ghost.Graphics;
using Ghost.Graphics.Core;
@@ -26,16 +27,17 @@ internal sealed class GhostRenderPayload : IRenderPayload
private UnsafeList<RenderRequest> _renderRequests;
// TODO: Consider using a more efficient data structure for these queues, such as a lock-free ring buffer or a custom concurrent queue implementation.
private readonly ConcurrentQueue<AddInstanceRequest> _addRequest;
private readonly ConcurrentQueue<RemoveInstanceRequest> _removeRequest;
private uint _instanceCountBefore;
private uint _instanceCount;
public ReadOnlySpan<RenderRequest> RenderRequests => _renderRequests;
public ConcurrentQueue<AddInstanceRequest> AddRequest => _addRequest;
public ConcurrentQueue<RemoveInstanceRequest> RemoveRequest => _removeRequest;
public uint InstanceCountBefore => _instanceCountBefore;
public uint InstanceCount => _instanceCount;
public GhostRenderPayload(GhostRenderPipeline renderPipeline)
@@ -70,10 +72,16 @@ internal sealed class GhostRenderPayload : IRenderPayload
}
}
public void BeginRecord()
{
_instanceCountBefore = _renderPipeline.GPUScene.InstanceCount;
}
public void EndRecord()
{
// We capture the count here to prevent that main thread continues to add more requests for next frame while the render thread is still processing current frame's requests.
_instanceCount = _renderPipeline.GPUScene.InstanceCount;
Logger.DebugAssert(_instanceCount == _instanceCountBefore + (uint)_addRequest.Count - (uint)_removeRequest.Count);
}
public void Reset()

View File

@@ -0,0 +1,81 @@
using Ghost.Core;
using Ghost.Graphics;
using SharpCompress.Common;
using System.Collections.Concurrent;
namespace Ghost.Engine;
internal class ResourceStreamingProcessor : IResourceStreamingProcessor
{
private const int _MAX_UPLOADS_PER_FRAME = 8;
private readonly ConcurrentQueue<AssetEntry> _pendingUpload;
private readonly ConcurrentQueue<AssetEntry> _pendingFinalize;
private ulong _pendingCopyFenceValue;
public ResourceStreamingProcessor()
{
_pendingUpload = new ConcurrentQueue<AssetEntry>();
_pendingFinalize = new ConcurrentQueue<AssetEntry>();
_pendingCopyFenceValue = 0;
}
public void EnqueueForUpload(AssetEntry entry)
{
_pendingUpload.Enqueue(entry);
}
public void ProcessPendingUploads(ResourceStreamingContext context)
{
// 1. If there's a pending copy batch from last frame, check its fence
if (_pendingCopyFenceValue > 0 && context.CopyPipeline.CurrentFenceValue() >= _pendingCopyFenceValue)
{
while (_pendingFinalize.TryDequeue(out var item))
{
item.OnUploadComplete(context);
}
_pendingCopyFenceValue = 0;
}
if (_pendingCopyFenceValue > 0)
{
return;
}
// 2. Collect entries that are in state == Loaded (I/O done, not yet uploaded)
// Cap per frame to avoid stalling (e.g., max 8 textures per frame)
if (_pendingUpload.IsEmpty)
{
return;
}
context.CopyPipeline.Begin();
var uploadCount = 0;
while (uploadCount < _MAX_UPLOADS_PER_FRAME && _pendingUpload.TryDequeue(out var entry))
{
if (entry.State != AssetState.Loaded)
{
Logger.Warning($"Asset {entry.AssetId} is in state {entry.State}, expected Loaded. Skipping upload.");
continue;
}
// Record copy commands into cmdCopy
entry.OnRecordUploadCommands(context);
entry.State = AssetState.Uploading;
_pendingFinalize.Enqueue(entry);
uploadCount++;
}
var result = context.CopyPipeline.End();
// 3. Submit the batch
if (uploadCount > 0 && result.IsSuccess)
{
_pendingCopyFenceValue = context.CopyPipeline.SignaledFenceValue();
}
}
}

View File

@@ -49,7 +49,7 @@ internal sealed unsafe class ChunkDebugView
private static object[] GetItems(ref readonly Chunk chunk)
{
#if !(DEBUG || GHOST_EDITOR)
#if !DEBUG
return [];
#else
var pData = chunk.GetUnsafePtr();
@@ -185,7 +185,7 @@ internal unsafe struct Archetype : IDisposable
private int _maxComponentID;
private int _entityIdsOffset;
// -1 means no cleanup component, 0 means haven't computed yet (since 0 is the empty archetype), positive value means the archetype id of the cleanup edge.
// 0 means no cleanup component (since 0 is the empty archetype), -1 means haven't computed yet, positive value means the archetype id of the cleanup edge.
internal int _cleanupEdge;
public readonly Identifier<Archetype> ID => _id;
@@ -248,7 +248,7 @@ internal unsafe struct Archetype : IDisposable
}
}
if (cleanupCount == 0)
if (cleanupCount > 0)
{
_cleanupEdge = -1;
}

View File

@@ -291,14 +291,15 @@ public unsafe partial class EntityManager : IDisposable
ref var archetype = ref _world.ComponentManager.GetArchetypeReference(location.archetypeID);
if (archetype._cleanupEdge < 0)
// 0 means no cleanup component (since 0 is the empty archetype), -1 means haven't computed yet, positive value means the archetype id of the cleanup edge.
if (archetype._cleanupEdge == 0)
{
return DestroyEntity_Internal(entity, location);
}
else
{
Identifier<Archetype> newArcID = default;
if (archetype._cleanupEdge == 0)
if (archetype._cleanupEdge < 0)
{
ref var signature = ref archetype._signature;

View File

@@ -7,11 +7,6 @@
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
<IsTrimmable>True</IsTrimmable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
<IsTrimmable>True</IsTrimmable>

View File

@@ -65,7 +65,7 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
_device = new D3D12RenderDevice();
_descriptorAllocator = new D3D12DescriptorAllocator(_device);
_resourceDatabase = new D3D12ResourceDatabase(_descriptorAllocator);
_resourceDatabase = new D3D12ResourceDatabase(_device, _descriptorAllocator);
_pipelineLibrary = new D3D12PipelineLibrary(_device);
_resourceAllocator = new D3D12ResourceAllocator(_device, _descriptorAllocator, _resourceDatabase, _pipelineLibrary);

View File

@@ -37,405 +37,6 @@ internal sealed unsafe partial class D3D12ResourceAllocator
throw new InvalidOperationException($"ERROR: Texture size too large for DirectX 12 (width {width}, height {height})");
}
}
private static D3D12_SHADER_RESOURCE_VIEW_DESC CreateTextureSrvDesc(ID3D12Resource* pResource, uint mipLevels, uint arraySize, bool isCubeMap, TextureFormat originalFormat)
{
var resourceDesc = pResource->GetDesc();
var srvDesc = new D3D12_SHADER_RESOURCE_VIEW_DESC
{
Format = resourceDesc.Format,
Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING
};
if (originalFormat == TextureFormat.D32_Float)
{
srvDesc.Format = DXGI_FORMAT_R32_FLOAT;
}
else if (originalFormat == TextureFormat.D24_UNorm_S8_UInt)
{
srvDesc.Format = DXGI_FORMAT_R24_UNORM_X8_TYPELESS;
}
switch (resourceDesc.Dimension)
{
case D3D12_RESOURCE_DIMENSION_TEXTURE1D:
if (resourceDesc.DepthOrArraySize > 1)
{
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE1DARRAY;
srvDesc.Texture1DArray = new D3D12_TEX1D_ARRAY_SRV
{
MipLevels = mipLevels,
ArraySize = arraySize,
};
}
else
{
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE1D;
srvDesc.Texture1D = new D3D12_TEX1D_SRV
{
MipLevels = mipLevels,
};
}
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE2D:
if (resourceDesc.DepthOrArraySize > 1)
{
if (isCubeMap)
{
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBEARRAY;
srvDesc.TextureCubeArray = new D3D12_TEXCUBE_ARRAY_SRV
{
MipLevels = mipLevels,
NumCubes = arraySize / 6,
};
}
else
{
srvDesc.ViewDimension = resourceDesc.SampleDesc.Count > 1 ? D3D12_SRV_DIMENSION_TEXTURE2DMSARRAY : D3D12_SRV_DIMENSION_TEXTURE2DARRAY;
srvDesc.Texture2DArray = new D3D12_TEX2D_ARRAY_SRV
{
MipLevels = mipLevels,
ArraySize = arraySize,
};
}
}
else
{
if (isCubeMap)
{
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE;
srvDesc.TextureCube = new D3D12_TEXCUBE_SRV
{
MipLevels = mipLevels,
};
}
else
{
srvDesc.ViewDimension = resourceDesc.SampleDesc.Count > 1 ? D3D12_SRV_DIMENSION_TEXTURE2DMS : D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D = new D3D12_TEX2D_SRV
{
MipLevels = mipLevels,
};
}
}
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE3D:
srvDesc.Texture3D = new D3D12_TEX3D_SRV
{
MipLevels = mipLevels,
};
break;
default:
throw new ArgumentException($"Unsupported texture dimension for SRV: {resourceDesc.Dimension}");
}
return srvDesc;
}
private static D3D12_SHADER_RESOURCE_VIEW_DESC CreateBufferSrvDesc(ID3D12Resource* pResource, uint stride, bool isRaw)
{
var resourceDesc = pResource->GetDesc();
var srvDesc = new D3D12_SHADER_RESOURCE_VIEW_DESC
{
ViewDimension = D3D12_SRV_DIMENSION_BUFFER,
Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING
};
if (isRaw)
{
srvDesc.Format = DXGI_FORMAT_R32_TYPELESS;
srvDesc.Buffer.FirstElement = 0;
srvDesc.Buffer.NumElements = (uint)(resourceDesc.Width / 4u);
srvDesc.Buffer.StructureByteStride = 0;
srvDesc.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_RAW;
}
else // Assumes Structured
{
srvDesc.Format = resourceDesc.Format;
srvDesc.Buffer.FirstElement = 0;
srvDesc.Buffer.NumElements = (uint)(resourceDesc.Width / stride);
srvDesc.Buffer.StructureByteStride = stride;
srvDesc.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_NONE;
}
return srvDesc;
}
private static D3D12_RENDER_TARGET_VIEW_DESC CreateRtvDesc(ID3D12Resource* pResource, uint mipSlice = 0, uint firstArraySlice = 0, uint planeSlice = 0)
{
var resourceDesc = pResource->GetDesc();
var rtvDesc = new D3D12_RENDER_TARGET_VIEW_DESC();
switch (resourceDesc.Dimension)
{
case D3D12_RESOURCE_DIMENSION_BUFFER:
rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_BUFFER;
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE1D:
rtvDesc.ViewDimension = resourceDesc.DepthOrArraySize > 1 ? D3D12_RTV_DIMENSION_TEXTURE1DARRAY : D3D12_RTV_DIMENSION_TEXTURE1D;
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE2D:
if (resourceDesc.SampleDesc.Count > 1)
{
rtvDesc.ViewDimension = resourceDesc.DepthOrArraySize > 1 ? D3D12_RTV_DIMENSION_TEXTURE2DMSARRAY : D3D12_RTV_DIMENSION_TEXTURE2DMS;
}
else
{
rtvDesc.ViewDimension = resourceDesc.DepthOrArraySize > 1 ? D3D12_RTV_DIMENSION_TEXTURE2DARRAY : D3D12_RTV_DIMENSION_TEXTURE2D;
}
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE3D:
rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE3D;
break;
default:
throw new ArgumentException($"Unsupported texture dimension for SRV: {resourceDesc.Dimension}");
}
rtvDesc.Format = resourceDesc.Format;
var isArray =
rtvDesc.ViewDimension == D3D12_RTV_DIMENSION_TEXTURE2DARRAY ||
rtvDesc.ViewDimension == D3D12_RTV_DIMENSION_TEXTURE2DMSARRAY;
var arraySize = 1u;
if (isArray)
{
arraySize = resourceDesc.ArraySize() - firstArraySlice;
}
switch (rtvDesc.ViewDimension)
{
case D3D12_RTV_DIMENSION_BUFFER:
rtvDesc.Buffer.FirstElement = firstArraySlice;
rtvDesc.Buffer.NumElements = arraySize;
break;
case D3D12_RTV_DIMENSION_TEXTURE1D:
rtvDesc.Texture1D.MipSlice = mipSlice;
break;
case D3D12_RTV_DIMENSION_TEXTURE1DARRAY:
rtvDesc.Texture1DArray.MipSlice = mipSlice;
rtvDesc.Texture1DArray.FirstArraySlice = firstArraySlice;
rtvDesc.Texture1DArray.ArraySize = arraySize;
break;
case D3D12_RTV_DIMENSION_TEXTURE2D:
rtvDesc.Texture2D.MipSlice = mipSlice;
rtvDesc.Texture2D.PlaneSlice = planeSlice;
break;
case D3D12_RTV_DIMENSION_TEXTURE2DARRAY:
rtvDesc.Texture2DArray.MipSlice = mipSlice;
rtvDesc.Texture2DArray.FirstArraySlice = firstArraySlice;
rtvDesc.Texture2DArray.ArraySize = arraySize;
rtvDesc.Texture2DArray.PlaneSlice = planeSlice;
break;
case D3D12_RTV_DIMENSION_TEXTURE2DMS:
break;
case D3D12_RTV_DIMENSION_TEXTURE2DMSARRAY:
rtvDesc.Texture2DMSArray.FirstArraySlice = firstArraySlice;
rtvDesc.Texture2DMSArray.ArraySize = arraySize;
break;
case D3D12_RTV_DIMENSION_TEXTURE3D:
rtvDesc.Texture3D.MipSlice = mipSlice;
rtvDesc.Texture3D.FirstWSlice = firstArraySlice;
rtvDesc.Texture3D.WSize = arraySize;
break;
default:
throw new ArgumentException($"Unsupported RTV dimension: {rtvDesc.ViewDimension}");
}
return rtvDesc;
}
private static D3D12_DEPTH_STENCIL_VIEW_DESC CreateDsvDesc(ID3D12Resource* pResource, uint mipSlice = 0, uint firstArraySlice = 0, D3D12_DSV_FLAGS flags = D3D12_DSV_FLAG_NONE, TextureFormat originalFormat = TextureFormat.Unknown)
{
var resourceDesc = pResource->GetDesc();
var dsvDesc = new D3D12_DEPTH_STENCIL_VIEW_DESC
{
Flags = flags,
};
switch (resourceDesc.Dimension)
{
case D3D12_RESOURCE_DIMENSION_TEXTURE1D:
dsvDesc.ViewDimension = resourceDesc.DepthOrArraySize > 1 ? D3D12_DSV_DIMENSION_TEXTURE1DARRAY : D3D12_DSV_DIMENSION_TEXTURE1D;
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE2D:
if (resourceDesc.SampleDesc.Count > 1)
{
dsvDesc.ViewDimension = resourceDesc.DepthOrArraySize > 1 ? D3D12_DSV_DIMENSION_TEXTURE2DMSARRAY : D3D12_DSV_DIMENSION_TEXTURE2DMS;
}
else
{
dsvDesc.ViewDimension = resourceDesc.DepthOrArraySize > 1 ? D3D12_DSV_DIMENSION_TEXTURE2DARRAY : D3D12_DSV_DIMENSION_TEXTURE2D;
}
break;
}
dsvDesc.Format = originalFormat == TextureFormat.Unknown ? resourceDesc.Format : originalFormat.ToDXGIFormat();
var isArray =
dsvDesc.ViewDimension == D3D12_DSV_DIMENSION_TEXTURE2DARRAY ||
dsvDesc.ViewDimension == D3D12_DSV_DIMENSION_TEXTURE2DMSARRAY;
var arraySize = 1u;
if (isArray)
{
arraySize = resourceDesc.ArraySize() - firstArraySlice;
}
switch (dsvDesc.ViewDimension)
{
case D3D12_DSV_DIMENSION_TEXTURE1D:
dsvDesc.Texture1D.MipSlice = mipSlice;
break;
case D3D12_DSV_DIMENSION_TEXTURE1DARRAY:
dsvDesc.Texture1DArray.MipSlice = mipSlice;
dsvDesc.Texture1DArray.FirstArraySlice = firstArraySlice;
dsvDesc.Texture1DArray.ArraySize = arraySize;
break;
case D3D12_DSV_DIMENSION_TEXTURE2D:
dsvDesc.Texture2D.MipSlice = mipSlice;
break;
case D3D12_DSV_DIMENSION_TEXTURE2DARRAY:
dsvDesc.Texture2DArray.MipSlice = mipSlice;
dsvDesc.Texture2DArray.FirstArraySlice = firstArraySlice;
dsvDesc.Texture2DArray.ArraySize = arraySize;
break;
case D3D12_DSV_DIMENSION_TEXTURE2DMS:
break;
case D3D12_DSV_DIMENSION_TEXTURE2DMSARRAY:
dsvDesc.Texture2DMSArray.FirstArraySlice = firstArraySlice;
dsvDesc.Texture2DMSArray.ArraySize = arraySize;
break;
default:
break;
}
return dsvDesc;
}
private static D3D12_UNORDERED_ACCESS_VIEW_DESC CreateTextureUavDesc(ID3D12Resource* pResource, uint mipSlice = 0, uint firstArraySlice = 0, uint planeSlice = 0)
{
var resourceDesc = pResource->GetDesc();
var uavDesc = new D3D12_UNORDERED_ACCESS_VIEW_DESC
{
Format = resourceDesc.Format
};
switch (resourceDesc.Dimension)
{
case D3D12_RESOURCE_DIMENSION_TEXTURE1D:
if (resourceDesc.DepthOrArraySize > 1)
{
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE1DARRAY;
uavDesc.Texture1DArray = new D3D12_TEX1D_ARRAY_UAV
{
MipSlice = mipSlice,
FirstArraySlice = firstArraySlice,
ArraySize = resourceDesc.ArraySize() - firstArraySlice
};
}
else
{
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE1D;
uavDesc.Texture1D = new D3D12_TEX1D_UAV
{
MipSlice = mipSlice
};
}
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE2D:
if (resourceDesc.DepthOrArraySize > 1)
{
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2DARRAY;
uavDesc.Texture2DArray = new D3D12_TEX2D_ARRAY_UAV
{
MipSlice = mipSlice,
FirstArraySlice = firstArraySlice,
ArraySize = resourceDesc.ArraySize() - firstArraySlice,
PlaneSlice = planeSlice
};
}
else
{
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D;
uavDesc.Texture2D = new D3D12_TEX2D_UAV
{
MipSlice = mipSlice,
PlaneSlice = planeSlice
};
}
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE3D:
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE3D;
uavDesc.Texture3D = new D3D12_TEX3D_UAV
{
MipSlice = mipSlice,
FirstWSlice = firstArraySlice,
WSize = resourceDesc.Depth() - firstArraySlice
};
break;
case D3D12_RESOURCE_DIMENSION_BUFFER:
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_BUFFER;
uavDesc.Buffer = new D3D12_BUFFER_UAV
{
FirstElement = 0,
NumElements = (uint)(resourceDesc.Width / 4), // Assuming R32_TYPELESS RAW
StructureByteStride = 0,
Flags = D3D12_BUFFER_UAV_FLAG_RAW
};
break;
default:
throw new ArgumentException($"Unsupported texture dimension for UAV: {resourceDesc.Dimension}");
}
return uavDesc;
}
private static D3D12_UNORDERED_ACCESS_VIEW_DESC CreateBufferUavDesc(ID3D12Resource* pResource, uint stride, bool isRaw)
{
var resourceDesc = pResource->GetDesc();
var uavDesc = new D3D12_UNORDERED_ACCESS_VIEW_DESC
{
ViewDimension = D3D12_UAV_DIMENSION_BUFFER,
};
if (isRaw)
{
uavDesc.Format = DXGI_FORMAT_R32_TYPELESS;
uavDesc.Buffer.FirstElement = 0;
uavDesc.Buffer.NumElements = (uint)(resourceDesc.Width / 4u);
uavDesc.Buffer.StructureByteStride = 0;
uavDesc.Buffer.Flags = D3D12_BUFFER_UAV_FLAG_RAW;
}
else // Assumes Structured
{
uavDesc.Format = resourceDesc.Format;
uavDesc.Buffer.FirstElement = 0;
uavDesc.Buffer.NumElements = (uint)(resourceDesc.Width / stride);
uavDesc.Buffer.StructureByteStride = stride;
uavDesc.Buffer.Flags = D3D12_BUFFER_UAV_FLAG_NONE;
}
return uavDesc;
}
}
internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
@@ -606,47 +207,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
return Handle<GPUTexture>.Invalid;
}
var resourceDescriptor = ResourceViewGroup.Invalid;
if (desc.Usage.HasFlag(TextureUsage.ShaderResource))
{
resourceDescriptor.srv = _descriptorAllocator.AllocateCbvSrvUav();
var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.srv);
var isCubeMap = desc.Dimension == TextureDimension.TextureCube || desc.Dimension == TextureDimension.TextureCubeArray;
var srvDesc = CreateTextureSrvDesc(pResource, resourceDesc.MipLevels, resourceDesc.DepthOrArraySize, isCubeMap, desc.Format);
_device.NativeObject.Get()->CreateShaderResourceView(pResource, &srvDesc, cpuHandle);
_descriptorAllocator.CopyToShaderVisible(resourceDescriptor.srv);
}
if (desc.Usage.HasFlag(TextureUsage.RenderTarget))
{
resourceDescriptor.rtv = _descriptorAllocator.AllocateRTV();
var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.rtv);
var rtvDesc = CreateRtvDesc(pResource);
_device.NativeObject.Get()->CreateRenderTargetView(pResource, &rtvDesc, cpuHandle);
}
if (desc.Usage.HasFlag(TextureUsage.DepthStencil))
{
resourceDescriptor.dsv = _descriptorAllocator.AllocateDSV();
var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.dsv);
var dsvDesc = CreateDsvDesc(pResource, 0, 0, D3D12_DSV_FLAG_NONE, desc.Format);
_device.NativeObject.Get()->CreateDepthStencilView(pResource, &dsvDesc, cpuHandle);
}
if (desc.Usage.HasFlag(TextureUsage.UnorderedAccess))
{
resourceDescriptor.uav = _descriptorAllocator.AllocateCbvSrvUav();
var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.uav);
var uavDesc = CreateTextureUavDesc(pResource);
_device.NativeObject.Get()->CreateUnorderedAccessView(pResource, null, &uavDesc, cpuHandle);
_descriptorAllocator.CopyToShaderVisible(resourceDescriptor.uav);
}
var resourceDescriptor = D3D12Utility.CreateResourceDescriptor(_device, _descriptorAllocator, ResourceDesc.Texture(desc), pResource);
var barrierData = new ResourceBarrierData
{
layout = BarrierLayout.Common,
@@ -707,42 +268,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
return Handle<GPUBuffer>.Invalid;
}
var resourceDescriptor = ResourceViewGroup.Invalid;
if (desc.Usage.HasFlag(BufferUsage.Constant))
{
resourceDescriptor.cbv = _descriptorAllocator.AllocateCbvSrvUav();
var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.cbv);
var cbvDesc = new D3D12_CONSTANT_BUFFER_VIEW_DESC
{
BufferLocation = pResource->GetGPUVirtualAddress(),
SizeInBytes = (uint)resourceDesc.Width,
};
_device.NativeObject.Get()->CreateConstantBufferView(&cbvDesc, cpuHandle);
_descriptorAllocator.CopyToShaderVisible(resourceDescriptor.cbv);
}
if (desc.Usage.HasFlag(BufferUsage.ShaderResource))
{
resourceDescriptor.srv = _descriptorAllocator.AllocateCbvSrvUav();
var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.srv);
var srvDesc = CreateBufferSrvDesc(pResource, desc.Stride, isRaw);
_device.NativeObject.Get()->CreateShaderResourceView(pResource, &srvDesc, cpuHandle);
_descriptorAllocator.CopyToShaderVisible(resourceDescriptor.srv);
}
if (desc.Usage.HasFlag(BufferUsage.UnorderedAccess))
{
resourceDescriptor.uav = _descriptorAllocator.AllocateCbvSrvUav();
var cpuHandle = _descriptorAllocator.GetCpuHandle(resourceDescriptor.uav);
var uavDesc = CreateBufferUavDesc(pResource, desc.Stride, isRaw);
_device.NativeObject.Get()->CreateUnorderedAccessView(pResource, null, &uavDesc, cpuHandle);
_descriptorAllocator.CopyToShaderVisible(resourceDescriptor.uav);
}
var resourceDescriptor = D3D12Utility.CreateResourceDescriptor(_device, _descriptorAllocator, ResourceDesc.Buffer(desc), pResource);
var barrierData = new ResourceBarrierData
{
layout = BarrierLayout.Undefined,

View File

@@ -104,6 +104,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
}
}
private readonly D3D12RenderDevice _device;
private readonly D3D12DescriptorAllocator _descriptorAllocator;
private UnsafeSlotMap<ResourceRecord> _resources;
@@ -121,8 +122,9 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
public ResourceBarrierData globalBarrier;
public D3D12ResourceDatabase(D3D12DescriptorAllocator descriptorAllocator)
public D3D12ResourceDatabase(D3D12RenderDevice device, D3D12DescriptorAllocator descriptorAllocator)
{
_device = device;
_descriptorAllocator = descriptorAllocator;
_resources = new UnsafeSlotMap<ResourceRecord>(64, AllocationHandle.Persistent, AllocationOption.Clear);
@@ -367,19 +369,56 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
{
ref var recordA = ref _resources.GetElementReferenceAt(handleA.ID, handleA.Generation, out var existA);
ref var recordB = ref _resources.GetElementReferenceAt(handleB.ID, handleB.Generation, out var existB);
if (!existA || !existB)
{
return Error.NotFound;
}
// ViewGroups are pinned to their slots — save before swap
var viewA = recordA.viewGroup;
var viewB = recordB.viewGroup;
var temp = recordA;
recordA = recordB;
recordB = temp;
// Restore viewGroups to their original slots
recordA.viewGroup = viewA;
recordB.viewGroup = viewB;
D3D12Utility.CreateResourceDescriptor(_device, _descriptorAllocator, recordA.desc, recordA.ResourcePtr, viewA);
D3D12Utility.CreateResourceDescriptor(_device, _descriptorAllocator, recordB.desc, recordB.ResourcePtr, viewB);
return Error.None;
}
public Handle<GPUResource> Replace(Handle<GPUResource> dst, Handle<GPUResource> src)
{
ref var recordDst = ref _resources.GetElementReferenceAt(dst.ID, dst.Generation, out var existDst);
ref var recordSrc = ref _resources.GetElementReferenceAt(src.ID, src.Generation, out var existSrc);
if (!existDst || !existSrc)
{
return Handle<GPUResource>.Invalid;
}
var dstView = recordDst.viewGroup;
var srcView = recordSrc.viewGroup;
var temp = recordDst;
recordDst = recordSrc;
recordSrc = temp;
// Pin viewGroups back
recordDst.viewGroup = dstView;
recordSrc.viewGroup = srcView;
// Update dst's descriptor to point to the new resource
D3D12Utility.CreateResourceDescriptor(_device, _descriptorAllocator, recordDst.desc, recordDst.ResourcePtr, dstView);
ReleaseResource(src);
return dst;
}
public Handle<GPUResource> CreateShared(Handle<GPUResource> src)
{
if (src.IsInvalid)

View File

@@ -486,7 +486,7 @@ internal static unsafe class D3D12Utility
public static D3D12_RESOURCE_DESC ToD3D12ResourceDesc(this in TextureDesc desc)
{
var dxgiFormat = desc.Format.ToDXGIFormat();
if (desc.Usage.HasFlag(TextureUsage.DepthStencil) && desc.Usage.HasFlag(TextureUsage.ShaderResource))
{
if (dxgiFormat == DXGI_FORMAT_D32_FLOAT)
@@ -795,4 +795,496 @@ internal static unsafe class D3D12Utility
}
public static D3D12_DEPTH_STENCILOP_DESC D3D12_DEPTH_STENCILOP_DESC_DEFAULT => D3D12_DEPTH_STENCILOP_DESC_CREATE(D3D12_STENCIL_OP_KEEP, D3D12_STENCIL_OP_KEEP, D3D12_STENCIL_OP_KEEP, D3D12_COMPARISON_FUNC_ALWAYS);
public static D3D12_SHADER_RESOURCE_VIEW_DESC CreateTextureSrvDesc(ID3D12Resource* pResource, uint mipLevels, uint arraySize, bool isCubeMap, TextureFormat originalFormat)
{
var resourceDesc = pResource->GetDesc();
var srvDesc = new D3D12_SHADER_RESOURCE_VIEW_DESC
{
Format = resourceDesc.Format,
Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING
};
if (originalFormat == TextureFormat.D32_Float)
{
srvDesc.Format = DXGI_FORMAT_R32_FLOAT;
}
else if (originalFormat == TextureFormat.D24_UNorm_S8_UInt)
{
srvDesc.Format = DXGI_FORMAT_R24_UNORM_X8_TYPELESS;
}
switch (resourceDesc.Dimension)
{
case D3D12_RESOURCE_DIMENSION_TEXTURE1D:
if (resourceDesc.DepthOrArraySize > 1)
{
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE1DARRAY;
srvDesc.Texture1DArray = new D3D12_TEX1D_ARRAY_SRV
{
MipLevels = mipLevels,
ArraySize = arraySize,
};
}
else
{
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE1D;
srvDesc.Texture1D = new D3D12_TEX1D_SRV
{
MipLevels = mipLevels,
};
}
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE2D:
if (resourceDesc.DepthOrArraySize > 1)
{
if (isCubeMap)
{
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBEARRAY;
srvDesc.TextureCubeArray = new D3D12_TEXCUBE_ARRAY_SRV
{
MipLevels = mipLevels,
NumCubes = arraySize / 6,
};
}
else
{
srvDesc.ViewDimension = resourceDesc.SampleDesc.Count > 1 ? D3D12_SRV_DIMENSION_TEXTURE2DMSARRAY : D3D12_SRV_DIMENSION_TEXTURE2DARRAY;
srvDesc.Texture2DArray = new D3D12_TEX2D_ARRAY_SRV
{
MipLevels = mipLevels,
ArraySize = arraySize,
};
}
}
else
{
if (isCubeMap)
{
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE;
srvDesc.TextureCube = new D3D12_TEXCUBE_SRV
{
MipLevels = mipLevels,
};
}
else
{
srvDesc.ViewDimension = resourceDesc.SampleDesc.Count > 1 ? D3D12_SRV_DIMENSION_TEXTURE2DMS : D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D = new D3D12_TEX2D_SRV
{
MipLevels = mipLevels,
};
}
}
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE3D:
srvDesc.Texture3D = new D3D12_TEX3D_SRV
{
MipLevels = mipLevels,
};
break;
default:
throw new ArgumentException($"Unsupported texture dimension for SRV: {resourceDesc.Dimension}");
}
return srvDesc;
}
public static D3D12_SHADER_RESOURCE_VIEW_DESC CreateBufferSrvDesc(ID3D12Resource* pResource, uint stride, bool isRaw)
{
var resourceDesc = pResource->GetDesc();
var srvDesc = new D3D12_SHADER_RESOURCE_VIEW_DESC
{
ViewDimension = D3D12_SRV_DIMENSION_BUFFER,
Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING
};
if (isRaw)
{
srvDesc.Format = DXGI_FORMAT_R32_TYPELESS;
srvDesc.Buffer.FirstElement = 0;
srvDesc.Buffer.NumElements = (uint)(resourceDesc.Width / 4u);
srvDesc.Buffer.StructureByteStride = 0;
srvDesc.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_RAW;
}
else // Assumes Structured
{
srvDesc.Format = resourceDesc.Format;
srvDesc.Buffer.FirstElement = 0;
srvDesc.Buffer.NumElements = (uint)(resourceDesc.Width / stride);
srvDesc.Buffer.StructureByteStride = stride;
srvDesc.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_NONE;
}
return srvDesc;
}
public static D3D12_RENDER_TARGET_VIEW_DESC CreateRtvDesc(ID3D12Resource* pResource, uint mipSlice = 0, uint firstArraySlice = 0, uint planeSlice = 0)
{
var resourceDesc = pResource->GetDesc();
var rtvDesc = new D3D12_RENDER_TARGET_VIEW_DESC();
switch (resourceDesc.Dimension)
{
case D3D12_RESOURCE_DIMENSION_BUFFER:
rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_BUFFER;
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE1D:
rtvDesc.ViewDimension = resourceDesc.DepthOrArraySize > 1 ? D3D12_RTV_DIMENSION_TEXTURE1DARRAY : D3D12_RTV_DIMENSION_TEXTURE1D;
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE2D:
if (resourceDesc.SampleDesc.Count > 1)
{
rtvDesc.ViewDimension = resourceDesc.DepthOrArraySize > 1 ? D3D12_RTV_DIMENSION_TEXTURE2DMSARRAY : D3D12_RTV_DIMENSION_TEXTURE2DMS;
}
else
{
rtvDesc.ViewDimension = resourceDesc.DepthOrArraySize > 1 ? D3D12_RTV_DIMENSION_TEXTURE2DARRAY : D3D12_RTV_DIMENSION_TEXTURE2D;
}
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE3D:
rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE3D;
break;
default:
throw new ArgumentException($"Unsupported texture dimension for SRV: {resourceDesc.Dimension}");
}
rtvDesc.Format = resourceDesc.Format;
var isArray =
rtvDesc.ViewDimension == D3D12_RTV_DIMENSION_TEXTURE2DARRAY ||
rtvDesc.ViewDimension == D3D12_RTV_DIMENSION_TEXTURE2DMSARRAY;
var arraySize = 1u;
if (isArray)
{
arraySize = resourceDesc.ArraySize() - firstArraySlice;
}
switch (rtvDesc.ViewDimension)
{
case D3D12_RTV_DIMENSION_BUFFER:
rtvDesc.Buffer.FirstElement = firstArraySlice;
rtvDesc.Buffer.NumElements = arraySize;
break;
case D3D12_RTV_DIMENSION_TEXTURE1D:
rtvDesc.Texture1D.MipSlice = mipSlice;
break;
case D3D12_RTV_DIMENSION_TEXTURE1DARRAY:
rtvDesc.Texture1DArray.MipSlice = mipSlice;
rtvDesc.Texture1DArray.FirstArraySlice = firstArraySlice;
rtvDesc.Texture1DArray.ArraySize = arraySize;
break;
case D3D12_RTV_DIMENSION_TEXTURE2D:
rtvDesc.Texture2D.MipSlice = mipSlice;
rtvDesc.Texture2D.PlaneSlice = planeSlice;
break;
case D3D12_RTV_DIMENSION_TEXTURE2DARRAY:
rtvDesc.Texture2DArray.MipSlice = mipSlice;
rtvDesc.Texture2DArray.FirstArraySlice = firstArraySlice;
rtvDesc.Texture2DArray.ArraySize = arraySize;
rtvDesc.Texture2DArray.PlaneSlice = planeSlice;
break;
case D3D12_RTV_DIMENSION_TEXTURE2DMS:
break;
case D3D12_RTV_DIMENSION_TEXTURE2DMSARRAY:
rtvDesc.Texture2DMSArray.FirstArraySlice = firstArraySlice;
rtvDesc.Texture2DMSArray.ArraySize = arraySize;
break;
case D3D12_RTV_DIMENSION_TEXTURE3D:
rtvDesc.Texture3D.MipSlice = mipSlice;
rtvDesc.Texture3D.FirstWSlice = firstArraySlice;
rtvDesc.Texture3D.WSize = arraySize;
break;
default:
throw new ArgumentException($"Unsupported RTV dimension: {rtvDesc.ViewDimension}");
}
return rtvDesc;
}
public static D3D12_DEPTH_STENCIL_VIEW_DESC CreateDsvDesc(ID3D12Resource* pResource, uint mipSlice = 0, uint firstArraySlice = 0, D3D12_DSV_FLAGS flags = D3D12_DSV_FLAG_NONE, TextureFormat originalFormat = TextureFormat.Unknown)
{
var resourceDesc = pResource->GetDesc();
var dsvDesc = new D3D12_DEPTH_STENCIL_VIEW_DESC
{
Flags = flags,
};
switch (resourceDesc.Dimension)
{
case D3D12_RESOURCE_DIMENSION_TEXTURE1D:
dsvDesc.ViewDimension = resourceDesc.DepthOrArraySize > 1 ? D3D12_DSV_DIMENSION_TEXTURE1DARRAY : D3D12_DSV_DIMENSION_TEXTURE1D;
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE2D:
if (resourceDesc.SampleDesc.Count > 1)
{
dsvDesc.ViewDimension = resourceDesc.DepthOrArraySize > 1 ? D3D12_DSV_DIMENSION_TEXTURE2DMSARRAY : D3D12_DSV_DIMENSION_TEXTURE2DMS;
}
else
{
dsvDesc.ViewDimension = resourceDesc.DepthOrArraySize > 1 ? D3D12_DSV_DIMENSION_TEXTURE2DARRAY : D3D12_DSV_DIMENSION_TEXTURE2D;
}
break;
}
dsvDesc.Format = originalFormat == TextureFormat.Unknown ? resourceDesc.Format : originalFormat.ToDXGIFormat();
var isArray =
dsvDesc.ViewDimension == D3D12_DSV_DIMENSION_TEXTURE2DARRAY ||
dsvDesc.ViewDimension == D3D12_DSV_DIMENSION_TEXTURE2DMSARRAY;
var arraySize = 1u;
if (isArray)
{
arraySize = resourceDesc.ArraySize() - firstArraySlice;
}
switch (dsvDesc.ViewDimension)
{
case D3D12_DSV_DIMENSION_TEXTURE1D:
dsvDesc.Texture1D.MipSlice = mipSlice;
break;
case D3D12_DSV_DIMENSION_TEXTURE1DARRAY:
dsvDesc.Texture1DArray.MipSlice = mipSlice;
dsvDesc.Texture1DArray.FirstArraySlice = firstArraySlice;
dsvDesc.Texture1DArray.ArraySize = arraySize;
break;
case D3D12_DSV_DIMENSION_TEXTURE2D:
dsvDesc.Texture2D.MipSlice = mipSlice;
break;
case D3D12_DSV_DIMENSION_TEXTURE2DARRAY:
dsvDesc.Texture2DArray.MipSlice = mipSlice;
dsvDesc.Texture2DArray.FirstArraySlice = firstArraySlice;
dsvDesc.Texture2DArray.ArraySize = arraySize;
break;
case D3D12_DSV_DIMENSION_TEXTURE2DMS:
break;
case D3D12_DSV_DIMENSION_TEXTURE2DMSARRAY:
dsvDesc.Texture2DMSArray.FirstArraySlice = firstArraySlice;
dsvDesc.Texture2DMSArray.ArraySize = arraySize;
break;
default:
break;
}
return dsvDesc;
}
public static D3D12_UNORDERED_ACCESS_VIEW_DESC CreateTextureUavDesc(ID3D12Resource* pResource, uint mipSlice = 0, uint firstArraySlice = 0, uint planeSlice = 0)
{
var resourceDesc = pResource->GetDesc();
var uavDesc = new D3D12_UNORDERED_ACCESS_VIEW_DESC
{
Format = resourceDesc.Format
};
switch (resourceDesc.Dimension)
{
case D3D12_RESOURCE_DIMENSION_TEXTURE1D:
if (resourceDesc.DepthOrArraySize > 1)
{
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE1DARRAY;
uavDesc.Texture1DArray = new D3D12_TEX1D_ARRAY_UAV
{
MipSlice = mipSlice,
FirstArraySlice = firstArraySlice,
ArraySize = resourceDesc.ArraySize() - firstArraySlice
};
}
else
{
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE1D;
uavDesc.Texture1D = new D3D12_TEX1D_UAV
{
MipSlice = mipSlice
};
}
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE2D:
if (resourceDesc.DepthOrArraySize > 1)
{
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2DARRAY;
uavDesc.Texture2DArray = new D3D12_TEX2D_ARRAY_UAV
{
MipSlice = mipSlice,
FirstArraySlice = firstArraySlice,
ArraySize = resourceDesc.ArraySize() - firstArraySlice,
PlaneSlice = planeSlice
};
}
else
{
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D;
uavDesc.Texture2D = new D3D12_TEX2D_UAV
{
MipSlice = mipSlice,
PlaneSlice = planeSlice
};
}
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE3D:
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE3D;
uavDesc.Texture3D = new D3D12_TEX3D_UAV
{
MipSlice = mipSlice,
FirstWSlice = firstArraySlice,
WSize = resourceDesc.Depth() - firstArraySlice
};
break;
case D3D12_RESOURCE_DIMENSION_BUFFER:
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_BUFFER;
uavDesc.Buffer = new D3D12_BUFFER_UAV
{
FirstElement = 0,
NumElements = (uint)(resourceDesc.Width / 4), // Assuming R32_TYPELESS RAW
StructureByteStride = 0,
Flags = D3D12_BUFFER_UAV_FLAG_RAW
};
break;
default:
throw new ArgumentException($"Unsupported texture dimension for UAV: {resourceDesc.Dimension}");
}
return uavDesc;
}
public static D3D12_UNORDERED_ACCESS_VIEW_DESC CreateBufferUavDesc(ID3D12Resource* pResource, uint stride, bool isRaw)
{
var resourceDesc = pResource->GetDesc();
var uavDesc = new D3D12_UNORDERED_ACCESS_VIEW_DESC
{
ViewDimension = D3D12_UAV_DIMENSION_BUFFER,
};
if (isRaw)
{
uavDesc.Format = DXGI_FORMAT_R32_TYPELESS;
uavDesc.Buffer.FirstElement = 0;
uavDesc.Buffer.NumElements = (uint)(resourceDesc.Width / 4u);
uavDesc.Buffer.StructureByteStride = 0;
uavDesc.Buffer.Flags = D3D12_BUFFER_UAV_FLAG_RAW;
}
else // Assumes Structured
{
uavDesc.Format = resourceDesc.Format;
uavDesc.Buffer.FirstElement = 0;
uavDesc.Buffer.NumElements = (uint)(resourceDesc.Width / stride);
uavDesc.Buffer.StructureByteStride = stride;
uavDesc.Buffer.Flags = D3D12_BUFFER_UAV_FLAG_NONE;
}
return uavDesc;
}
public static ResourceViewGroup CreateResourceDescriptor(D3D12RenderDevice device, D3D12DescriptorAllocator descriptorAllocator, in ResourceDesc desc, ID3D12Resource* pResource, ResourceViewGroup originalGroup = default)
{
var resourceDescriptor = new ResourceViewGroup();
var resourceDesc = pResource->GetDesc();
if (desc.Type == ResourceType.Texture)
{
ref readonly var textureDesc = ref desc.TextureDescriptor;
if (textureDesc.Usage.HasFlag(TextureUsage.ShaderResource))
{
resourceDescriptor.srv = originalGroup.srv.IsValid ? originalGroup.srv : descriptorAllocator.AllocateCbvSrvUav();
var cpuHandle = descriptorAllocator.GetCpuHandle(resourceDescriptor.srv);
var isCubeMap = textureDesc.Dimension == TextureDimension.TextureCube || textureDesc.Dimension == TextureDimension.TextureCubeArray;
var srvDesc = CreateTextureSrvDesc(pResource, resourceDesc.MipLevels, resourceDesc.DepthOrArraySize, isCubeMap, textureDesc.Format);
device.NativeObject.Get()->CreateShaderResourceView(pResource, &srvDesc, cpuHandle);
descriptorAllocator.CopyToShaderVisible(resourceDescriptor.srv);
}
if (textureDesc.Usage.HasFlag(TextureUsage.RenderTarget))
{
resourceDescriptor.rtv = originalGroup.rtv.IsValid ? originalGroup.rtv : descriptorAllocator.AllocateRTV();
var cpuHandle = descriptorAllocator.GetCpuHandle(resourceDescriptor.rtv);
var rtvDesc = CreateRtvDesc(pResource);
device.NativeObject.Get()->CreateRenderTargetView(pResource, &rtvDesc, cpuHandle);
}
if (textureDesc.Usage.HasFlag(TextureUsage.DepthStencil))
{
resourceDescriptor.dsv = originalGroup.dsv.IsValid ? originalGroup.dsv : descriptorAllocator.AllocateDSV();
var cpuHandle = descriptorAllocator.GetCpuHandle(resourceDescriptor.dsv);
var dsvDesc = CreateDsvDesc(pResource, 0, 0, D3D12_DSV_FLAG_NONE, textureDesc.Format);
device.NativeObject.Get()->CreateDepthStencilView(pResource, &dsvDesc, cpuHandle);
}
if (textureDesc.Usage.HasFlag(TextureUsage.UnorderedAccess))
{
resourceDescriptor.uav = originalGroup.uav.IsValid ? originalGroup.uav : descriptorAllocator.AllocateCbvSrvUav();
var cpuHandle = descriptorAllocator.GetCpuHandle(resourceDescriptor.uav);
var uavDesc = CreateTextureUavDesc(pResource);
device.NativeObject.Get()->CreateUnorderedAccessView(pResource, null, &uavDesc, cpuHandle);
descriptorAllocator.CopyToShaderVisible(resourceDescriptor.uav);
}
}
else
{
ref readonly var bufferDesc = ref desc.BufferDescriptor;
var isRaw = bufferDesc.Usage.HasFlag(BufferUsage.Raw);
if (bufferDesc.Usage.HasFlag(BufferUsage.Constant))
{
resourceDescriptor.cbv = originalGroup.cbv.IsValid ? originalGroup.cbv : descriptorAllocator.AllocateCbvSrvUav();
var cpuHandle = descriptorAllocator.GetCpuHandle(resourceDescriptor.cbv);
var cbvDesc = new D3D12_CONSTANT_BUFFER_VIEW_DESC
{
BufferLocation = pResource->GetGPUVirtualAddress(),
SizeInBytes = (uint)resourceDesc.Width,
};
device.NativeObject.Get()->CreateConstantBufferView(&cbvDesc, cpuHandle);
descriptorAllocator.CopyToShaderVisible(resourceDescriptor.cbv);
}
if (bufferDesc.Usage.HasFlag(BufferUsage.ShaderResource))
{
resourceDescriptor.srv = originalGroup.srv.IsValid ? originalGroup.srv : descriptorAllocator.AllocateCbvSrvUav();
var cpuHandle = descriptorAllocator.GetCpuHandle(resourceDescriptor.srv);
var srvDesc = CreateBufferSrvDesc(pResource, bufferDesc.Stride, isRaw);
device.NativeObject.Get()->CreateShaderResourceView(pResource, &srvDesc, cpuHandle);
descriptorAllocator.CopyToShaderVisible(resourceDescriptor.srv);
}
if (bufferDesc.Usage.HasFlag(BufferUsage.UnorderedAccess))
{
resourceDescriptor.uav = originalGroup.uav.IsValid ? originalGroup.uav : descriptorAllocator.AllocateCbvSrvUav();
var cpuHandle = descriptorAllocator.GetCpuHandle(resourceDescriptor.uav);
var uavDesc = CreateBufferUavDesc(pResource, bufferDesc.Stride, isRaw);
device.NativeObject.Get()->CreateUnorderedAccessView(pResource, null, &uavDesc, cpuHandle);
descriptorAllocator.CopyToShaderVisible(resourceDescriptor.uav);
}
}
return resourceDescriptor;
}
}

View File

@@ -122,6 +122,14 @@ public unsafe interface IResourceDatabase : IDisposable
/// <returns>An Error indicating the success or failure of the swap operation.</returns>
Error Swap(Handle<GPUResource> handleA, Handle<GPUResource> handleB);
/// <summary>
/// Replaces the GPU resource associated with the destination handle with the resource associated with the source handle, and release the original resource of the destination handle.
/// </summary>
/// <param name="dst">The handle to the destination resource.</param>
/// <param name="src">The handle to the source resource.</param>
/// <returns>The handle to the replaced resource.</returns>
Handle<GPUResource> Replace(Handle<GPUResource> dst, Handle<GPUResource> src);
/// <summary>
/// Creates a new GPU resource that is a share of the specified source resource, including all its properties and data.
/// The new resource will have the same description and content as the source resource, but will be a distinct entity in the resource database with its own handle.

View File

@@ -12,24 +12,34 @@ namespace Ghost.Graphics.Core;
// TODO: Temporary rendering context for heap creation and data upload. We will refactor it later when we have a better understanding of the engine architecture.
public readonly unsafe ref struct RenderContext
{
private readonly ResourceManager _resourceManager;
private readonly ShaderLibrary _shaderLibrary;
private readonly IGraphicsEngine _engine;
private readonly ICommandBuffer _commandBuffer;
public ICommandBuffer CommandBuffer => _commandBuffer;
public ResourceManager ResourceManager => _resourceManager;
public IResourceAllocator ResourceAllocator => _engine.ResourceAllocator;
public IResourceDatabase ResourceDatabase => _engine.ResourceDatabase;
public IPipelineLibrary PipelineLibrary => _engine.PipelineLibrary;
internal RenderContext(ResourceManager resourceManager, ShaderLibrary shaderLibrary, IGraphicsEngine engine, ICommandBuffer cmd)
public required ICommandBuffer CommandBuffer
{
_resourceManager = resourceManager;
_shaderLibrary = shaderLibrary;
_engine = engine;
_commandBuffer = cmd;
get; init;
}
public required ResourceManager ResourceManager
{
get; init;
}
public required IResourceAllocator ResourceAllocator
{
get; init;
}
public required IResourceDatabase ResourceDatabase
{
get; init;
}
public required IPipelineLibrary PipelineLibrary
{
get; init;
}
internal ShaderLibrary ShaderLibrary
{
get; init;
}
private void TransitionBarrier(Handle<GPUResource> resource, bool isTexture, BarrierLayout newLayout, BarrierAccess newAccess, BarrierSync newSync)
@@ -44,13 +54,13 @@ public readonly unsafe ref struct RenderContext
desc = BarrierDesc.Buffer(resource, newSync, newAccess);
}
_commandBuffer.Barrier(desc);
CommandBuffer.Barrier(desc);
}
public void UploadBuffer<T>(Handle<GPUBuffer> buffer, params ReadOnlySpan<T> data)
where T : unmanaged
{
var r = _engine.ResourceDatabase.GetResourceDescription(buffer.AsResource());
var r = ResourceDatabase.GetResourceDescription(buffer.AsResource());
if (r.IsFailure)
{
return;
@@ -65,9 +75,9 @@ public readonly unsafe ref struct RenderContext
{
fixed (T* pData = data)
{
var mappedData = _engine.ResourceDatabase.MapResource(buffer.AsResource(), 0, null);
var mappedData = ResourceDatabase.MapResource(buffer.AsResource(), 0, null);
MemoryUtility.MemCpy(mappedData, pData, sizeInBytes);
_engine.ResourceDatabase.UnmapResource(buffer.AsResource(), 0, null);
ResourceDatabase.UnmapResource(buffer.AsResource(), 0, null);
}
}
else
@@ -79,7 +89,7 @@ public readonly unsafe ref struct RenderContext
HeapType = HeapType.Upload,
};
var uploadHandle = _resourceManager.CreateTransientBuffer(in uploadDesc);
var uploadHandle = ResourceManager.CreateTransientBuffer(in uploadDesc);
if (uploadHandle.IsInvalid)
{
throw new OutOfMemoryException("Failed to create upload buffer for buffer data.");
@@ -87,19 +97,19 @@ public readonly unsafe ref struct RenderContext
fixed (T* pData = data)
{
var mappedData = _engine.ResourceDatabase.MapResource(uploadHandle.AsResource(), 0, null);
var mappedData = ResourceDatabase.MapResource(uploadHandle.AsResource(), 0, null);
MemoryUtility.MemCpy(mappedData, pData, sizeInBytes);
_engine.ResourceDatabase.UnmapResource(uploadHandle.AsResource(), 0, null);
ResourceDatabase.UnmapResource(uploadHandle.AsResource(), 0, null);
}
_commandBuffer.CopyBuffer(buffer, uploadHandle, 0, 0, sizeInBytes);
CommandBuffer.CopyBuffer(buffer, uploadHandle, 0, 0, sizeInBytes);
}
}
public Handle<Mesh> CreateMesh(UnsafeList<Vertex> vertices, UnsafeList<uint> indices, bool staticMesh)
{
var mesh = _resourceManager.CreateMesh(vertices, indices);
var r = _resourceManager.GetMeshReference(mesh);
var mesh = ResourceManager.CreateMesh(vertices, indices);
var r = ResourceManager.GetMeshReference(mesh);
if (r.IsFailure)
{
return mesh;
@@ -146,7 +156,7 @@ public readonly unsafe ref struct RenderContext
/// <param name="markMeshStatic">Whether to mark the mesh as static. If it's true, the cpu buffer of the mesh will not be avaliable any more</param>
public void UploadMesh(Handle<Mesh> mesh, bool markMeshStatic)
{
var r = _resourceManager.GetMeshReference(mesh);
var r = ResourceManager.GetMeshReference(mesh);
if (r.IsFailure)
{
return;
@@ -173,7 +183,7 @@ public readonly unsafe ref struct RenderContext
public void UploadMeshlets(Handle<Mesh> mesh)
{
var r = _resourceManager.GetMeshReference(mesh);
var r = ResourceManager.GetMeshReference(mesh);
if (r.IsFailure)
{
return;
@@ -208,9 +218,9 @@ public readonly unsafe ref struct RenderContext
HeapType = HeapType.Default,
};
meshRef.MeshLetBuffer = _engine.ResourceAllocator.CreateBuffer(in meshletDesc, "Meshlets");
meshRef.MeshletVerticesBuffer = _engine.ResourceAllocator.CreateBuffer(in verticesDesc, "MeshletVertices");
meshRef.MeshletTrianglesBuffer = _engine.ResourceAllocator.CreateBuffer(in trianglesDesc, "MeshletTriangles");
meshRef.MeshLetBuffer = ResourceAllocator.CreateBuffer(in meshletDesc, "Meshlets");
meshRef.MeshletVerticesBuffer = ResourceAllocator.CreateBuffer(in verticesDesc, "MeshletVertices");
meshRef.MeshletTrianglesBuffer = ResourceAllocator.CreateBuffer(in trianglesDesc, "MeshletTriangles");
TransitionBarrier(meshRef.MeshLetBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
TransitionBarrier(meshRef.MeshletVerticesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
@@ -227,7 +237,7 @@ public readonly unsafe ref struct RenderContext
public void UpdateObjectData(Handle<Mesh> mesh)
{
var r = _resourceManager.GetMeshReference(mesh);
var r = ResourceManager.GetMeshReference(mesh);
if (r.IsFailure)
{
return;
@@ -238,11 +248,11 @@ public readonly unsafe ref struct RenderContext
{
worldBoundsMin = meshData.BoundingBox.Min,
worldBoundsMax = meshData.BoundingBox.Max,
vertexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.VertexBuffer.AsResource()),
indexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.IndexBuffer.AsResource()),
meshletBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.MeshLetBuffer.AsResource()),
meshletVerticesBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.MeshletVerticesBuffer.AsResource()),
meshletTrianglesBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.MeshletTrianglesBuffer.AsResource()),
vertexBuffer = ResourceDatabase.GetBindlessIndex(meshData.VertexBuffer.AsResource()),
indexBuffer = ResourceDatabase.GetBindlessIndex(meshData.IndexBuffer.AsResource()),
meshletBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshLetBuffer.AsResource()),
meshletVerticesBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshletVerticesBuffer.AsResource()),
meshletTrianglesBuffer = ResourceDatabase.GetBindlessIndex(meshData.MeshletTrianglesBuffer.AsResource()),
};
var bufferHandle = meshData.MeshDataBuffer.AsResource();
@@ -275,7 +285,7 @@ public readonly unsafe ref struct RenderContext
HeapType = HeapType.Upload,
};
var uploadHandle = _resourceManager.CreateTransientBuffer(in uploadDesc);
var uploadHandle = ResourceManager.CreateTransientBuffer(in uploadDesc);
if (uploadHandle.IsInvalid)
{
throw new OutOfMemoryException("Failed to create upload buffer for texture data.");
@@ -292,7 +302,7 @@ public readonly unsafe ref struct RenderContext
slicePitch = slicePitch
};
_commandBuffer.UpdateSubResources(texture.AsResource(), uploadHandle.AsResource(), subresourceData);
CommandBuffer.UpdateSubResources(texture.AsResource(), uploadHandle.AsResource(), subresourceData);
}
}
@@ -305,7 +315,7 @@ public readonly unsafe ref struct RenderContext
var variantKey = RHIUtility.CreateShaderVariantKey(entryHash, in keywordSet);
// TODO: Refactor this into a helper method.
var (compiledHash, error) = _shaderLibrary.GetCompiledHash(variantKey);
var (compiledHash, error) = ShaderLibrary.GetCompiledHash(variantKey);
if (error.IsFailure)
{
// TODO: Fallback to an error material.
@@ -318,7 +328,7 @@ public readonly unsafe ref struct RenderContext
if (!PipelineLibrary.HasPipelineStateObject(pipelineKey))
{
using var scope = AllocationManager.CreateStackScope();
var compiledCacheResult = _shaderLibrary.GetCompiledCache(shader.UniqueID, entryIndex, scope.AllocationHandle);
var compiledCacheResult = ShaderLibrary.GetCompiledCache(shader.UniqueID, entryIndex, scope.AllocationHandle);
if (compiledCacheResult.IsFailure)
{
// TODO: Fallback to a checkerboard shader.
@@ -341,7 +351,7 @@ public readonly unsafe ref struct RenderContext
PipelineLibrary.CreateComputePipeline(in psoDes).GetValueOrThrow();
}
_commandBuffer.SetPipelineState(pipelineKey);
CommandBuffer.SetPipelineState(pipelineKey);
var propertySpan = MemoryMarshal.AsBytes(new ReadOnlySpan<T>(in property));
@@ -374,7 +384,7 @@ public readonly unsafe ref struct RenderContext
propertyBuffer = ResourceDatabase.GetBindlessIndex(properyBuffer.AsResource()),
};
_commandBuffer.SetGraphicsRoot32Constants(0, pushConstant.AsUInts());
_commandBuffer.DispatchCompute(threadGroupCount.x, threadGroupCount.y, threadGroupCount.z);
CommandBuffer.SetGraphicsRoot32Constants(0, pushConstant.AsUInts());
CommandBuffer.DispatchCompute(threadGroupCount.x, threadGroupCount.y, threadGroupCount.z);
}
}

View File

@@ -0,0 +1,42 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Graphics;
internal ref struct ResourceStreamingContext
{
public required AsyncCopyPipeline CopyPipeline
{
get; init;
}
public required ResourceManager ResourceManager
{
get; init;
}
public required IResourceDatabase ResourceDatabase
{
get; init;
}
public required IResourceAllocator ResourceAllocator
{
get; init;
}
public required ICommandBuffer GraphicsCommandBuffer
{
get; init;
}
}
internal interface IResourceStreamingProcessor
{
void ProcessPendingUploads(ResourceStreamingContext context);
}

View File

@@ -31,6 +31,11 @@ internal readonly struct RenderSystemDesc
get; init;
}
public required IResourceStreamingProcessor ResourceStreamingProcessor
{
get; init;
}
public required string ShaderCacheDirectory
{
get; init;
@@ -85,12 +90,16 @@ public class RenderSystem : IDisposable
}
}
private readonly RenderSystemDesc _config;
private readonly IResourceStreamingProcessor _streamingProcessor;
private IRenderPipelineSettings _renderPipelineSettings;
private IRenderPipeline _renderPipeline;
private readonly IGraphicsEngine _graphicsEngine;
private readonly ResourceManager _resourceManager;
private readonly SwapChainManager _swapChainManager;
private readonly ShaderLibrary _shaderLibrary;
private readonly AsyncCopyPipeline _asyncCopyPipeline;
private readonly IFence _fence;
private readonly FrameResource[] _frameResources;
@@ -99,9 +108,6 @@ public class RenderSystem : IDisposable
private readonly ConcurrentDictionary<ISwapChain, uint2> _resizeRequest;
private IRenderPipelineSettings _renderPipelineSettings;
private IRenderPipeline _renderPipeline;
private ulong _cpuFenceValue;
private ulong _submittedFenceValue;
@@ -113,13 +119,14 @@ public class RenderSystem : IDisposable
public IGraphicsEngine GraphicsEngine => _graphicsEngine;
public ResourceManager ResourceManager => _resourceManager;
public SwapChainManager SwapChainManager => _swapChainManager;
public AsyncCopyPipeline AsyncCopyPipeline => _asyncCopyPipeline;
public bool IsRunning => _isRunning;
public ulong CPUFenceValue => _cpuFenceValue;
public ulong SubmittedFenceValue => _submittedFenceValue;
public uint MaxFrameLatency => _config.FrameBufferCount;
public int MaxFrameLatency => _frameResources.Length;
public IRenderPipelineSettings RenderPipelineSettings
{
@@ -152,8 +159,6 @@ public class RenderSystem : IDisposable
internal RenderSystem(RenderSystemDesc desc)
{
_config = desc;
var engineDesc = new GraphicsEngineDesc
{
FrameBufferCount = desc.FrameBufferCount
@@ -178,9 +183,8 @@ public class RenderSystem : IDisposable
throw new NotSupportedException($"The specified graphics API '{desc.GraphicsAPI}' is not supported.");
}
_resourceManager = new ResourceManager(_graphicsEngine.Device, _graphicsEngine.ResourceAllocator, _graphicsEngine.ResourceDatabase);
_swapChainManager = new SwapChainManager(_graphicsEngine);
_shaderLibrary = new ShaderLibrary(desc.ShaderCompilationBridge, desc.ShaderCacheDirectory);
_streamingProcessor = desc.ResourceStreamingProcessor;
_renderPipelineSettings = desc.InitialRenderPipelineSettings;
_fence = _graphicsEngine.CreateFence(0);
// Create frame resources for synchronization
@@ -195,6 +199,17 @@ public class RenderSystem : IDisposable
};
}
_renderPipeline = _renderPipelineSettings.CreatePipeline(this);
for (var i = 0; i < _frameResources.Length; i++)
{
_frameResources[i].RenderPayload = _renderPipelineSettings.CreatePayload(this, _renderPipeline);
}
_resourceManager = new ResourceManager(_graphicsEngine.Device, _graphicsEngine.ResourceAllocator, _graphicsEngine.ResourceDatabase);
_swapChainManager = new SwapChainManager(_graphicsEngine);
_shaderLibrary = new ShaderLibrary(desc.ShaderCompilationBridge, desc.ShaderCacheDirectory);
_asyncCopyPipeline = new AsyncCopyPipeline(_graphicsEngine);
_renderThread = new Thread(RenderLoop)
{
IsBackground = true,
@@ -205,14 +220,6 @@ public class RenderSystem : IDisposable
_shutdownEvent = new AutoResetEvent(false);
_resizeRequest = new ConcurrentDictionary<ISwapChain, uint2>();
_renderPipelineSettings = _config.InitialRenderPipelineSettings;
_renderPipeline = _renderPipelineSettings.CreatePipeline(this);
for (var i = 0; i < _frameResources.Length; i++)
{
_frameResources[i].RenderPayload = _renderPipelineSettings.CreatePayload(this, _renderPipeline);
}
_isRunning = false;
_disposed = false;
}
@@ -239,7 +246,7 @@ public class RenderSystem : IDisposable
while (_isRunning)
{
var frameIndex = (int)(_submittedFenceValue % _config.FrameBufferCount);
var frameIndex = (int)(_submittedFenceValue % (ulong)_frameResources.Length);
ref var frameResource = ref _frameResources[frameIndex];
try
@@ -297,10 +304,27 @@ public class RenderSystem : IDisposable
try
{
cmd.Begin(frameResource.CommandAllocator);
var streamingContext = new ResourceStreamingContext
{
CopyPipeline = _asyncCopyPipeline,
GraphicsCommandBuffer = cmd,
ResourceAllocator = _graphicsEngine.ResourceAllocator,
ResourceDatabase = _graphicsEngine.ResourceDatabase,
ResourceManager = _resourceManager
};
_streamingProcessor.ProcessPendingUploads(streamingContext);
var ctx = new RenderContext(_resourceManager, _shaderLibrary, _graphicsEngine, cmd);
_renderPipeline.Render(ctx, frameIndex, frameResource.RenderPayload);
var renderContext = new RenderContext
{
CommandBuffer = cmd,
PipelineLibrary = _graphicsEngine.PipelineLibrary,
ResourceAllocator = _graphicsEngine.ResourceAllocator,
ResourceDatabase = _graphicsEngine.ResourceDatabase,
ResourceManager = _resourceManager,
ShaderLibrary = _shaderLibrary,
};
_renderPipeline.Render(renderContext, frameIndex, frameResource.RenderPayload);
_swapChainManager.TransitionToPresent(cmd);
// End recording commands and submit
@@ -369,7 +393,7 @@ public class RenderSystem : IDisposable
{
Logger.DebugAssert(!_disposed, "Cannot signal CPU ready on a disposed RenderSystem.");
var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
var eventIndex = (int)(_cpuFenceValue % (ulong)_frameResources.Length);
ref var frameResource = ref _frameResources[eventIndex];
frameResource.CpuReadyEvent.Set();
@@ -386,14 +410,14 @@ public class RenderSystem : IDisposable
{
Logger.DebugAssert(!_disposed, "Cannot acquire CPU frame on a disposed RenderSystem.");
var requiredGpuFence = _cpuFenceValue < _config.FrameBufferCount ? 0 : _cpuFenceValue - _config.FrameBufferCount + 1;
var requiredGpuFence = _cpuFenceValue < (ulong)_frameResources.Length ? 0 : _cpuFenceValue - (ulong)_frameResources.Length + 1;
if (requiredGpuFence > 0 && _fence.CompletedValue < requiredGpuFence)
{
return false;
}
var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
var eventIndex = (int)(_cpuFenceValue % (ulong)_frameResources.Length);
ref var frameResource = ref _frameResources[eventIndex];
return true;
@@ -409,7 +433,7 @@ public class RenderSystem : IDisposable
return true;
}
var eventIndex = (int)((submittedFenceValue - 1) % _config.FrameBufferCount);
var eventIndex = (int)((submittedFenceValue - 1) % (ulong)_frameResources.Length);
return _frameResources[eventIndex].GpuReadyEvent.WaitOne(timeOut);
}
@@ -429,7 +453,7 @@ public class RenderSystem : IDisposable
{
Logger.DebugAssert(!_disposed, "Cannot get current frame payload from a disposed RenderSystem.");
var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
var eventIndex = (int)(_cpuFenceValue % (ulong)_frameResources.Length);
ref var frameResource = ref _frameResources[eventIndex];
return frameResource.RenderPayload;

View File

@@ -4,7 +4,6 @@ using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.Diagnostics;
namespace Ghost.Graphics.Services;