feat: implement asynchronous asset management system with texture streaming support
This commit is contained in:
@@ -3,5 +3,7 @@ using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Ghost.Editor")]
|
||||
[assembly: InternalsVisibleTo("Ghost.Editor.Core")]
|
||||
[assembly: InternalsVisibleTo("Ghost.UnitTest")]
|
||||
|
||||
|
||||
[assembly: EngineAssembly]
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
81
src/Runtime/Ghost.Engine/ResourceStreamingProcessor.cs
Normal file
81
src/Runtime/Ghost.Engine/ResourceStreamingProcessor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user