refactor(core): asset pipeline overhaul & dock removal

- Introduced IAsset interface and refactored asset loading/saving.
- Migrated TextureContentHeader to Ghost.Engine; updated usage.
- Rewrote AssetRegistry, AssetCatalog, ImportCoordinator for new asset flow.
- Added thread-safe ConcurrentHashSet utility.
- Improved EditorApplication folder management/init.
- Updated TextureAssetHandler/TextureProcessor for new import/export.
- Added EditorContentProvider for asset access.
- Updated AssetManager to use new AssetType enum; removed GCHandle.
- Removed all custom docking controls and templates.
- Deleted obsolete ViewModels/Pages (Console, Hierarchy, Inspector, Project).
- Renamed ProjectBrowser to ContentBrowser; updated references.
- Updated NuGet packages, Result conversions, and commit instructions.
- General cleanup: namespaces, dead code, structure.
This commit is contained in:
2026-04-21 23:20:29 +09:00
parent c249a389e3
commit cb4092179f
59 changed files with 700 additions and 2780 deletions

View File

@@ -1,15 +1,2 @@
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

@@ -20,7 +20,7 @@
<ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.8" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="2.0.0" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="3.0.0" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.14">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -81,7 +81,7 @@ public readonly struct Result
sb.AppendLine(result.Message);
}
}
if (sb.Length == 0)
{
return Success();
@@ -158,6 +158,7 @@ public readonly struct Result<T>
public static implicit operator Result<T>(T? data) => data is not null ? Success(data) : Failure(null);
public static implicit operator Result<T>(Result result) => result.IsSuccess ? Success(default!) : Failure(result.Message);
public static implicit operator Result(Result<T> result) => result.IsSuccess ? Result.Success() : Result.Failure(result.Message);
public static implicit operator bool(Result<T> result) => result.IsSuccess;
}

View File

@@ -0,0 +1,162 @@
using System.Collections;
namespace Ghost.Core.Utilities;
public class ConcurrentHashSet<T> : IDisposable
{
public struct Enumerator : IEnumerator<T>
{
private readonly ConcurrentHashSet<T> _set;
private readonly HashSet<T>.Enumerator _enumerator;
public Enumerator(ConcurrentHashSet<T> set)
{
_set = set;
_set._lock.EnterReadLock();
_enumerator = _set._hashSet.GetEnumerator();
}
public readonly T Current => _enumerator.Current;
readonly object? IEnumerator.Current => Current;
public void Dispose()
{
if (_set._lock.IsReadLockHeld)
{
_set._lock.ExitReadLock();
}
_enumerator.Dispose();
}
public bool MoveNext()
{
return _enumerator.MoveNext();
}
public void Reset()
{
}
}
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private readonly HashSet<T> _hashSet = new HashSet<T>();
public int Count
{
get
{
_lock.EnterReadLock();
try
{
return _hashSet.Count;
}
finally
{
if (_lock.IsReadLockHeld)
{
_lock.ExitReadLock();
}
}
}
}
public bool IsEmpty
{
get
{
_lock.EnterReadLock();
try
{
return _hashSet.Count == 0;
}
finally
{
if (_lock.IsReadLockHeld)
{
_lock.ExitReadLock();
}
}
}
}
~ConcurrentHashSet()
{
Dispose();
}
public Enumerator GetEnumerator()
{
return new Enumerator(this);
}
public bool Add(T item)
{
_lock.EnterWriteLock();
try
{
return _hashSet.Add(item);
}
finally
{
if (_lock.IsWriteLockHeld)
{
_lock.ExitWriteLock();
}
}
}
public bool Contains(T item)
{
_lock.EnterReadLock();
try
{
return _hashSet.Contains(item);
}
finally
{
if (_lock.IsReadLockHeld)
{
_lock.ExitReadLock();
}
}
}
public bool Remove(T item)
{
_lock.EnterWriteLock();
try
{
return _hashSet.Remove(item);
}
finally
{
if (_lock.IsWriteLockHeld)
{
_lock.ExitWriteLock();
}
}
}
public void Clear()
{
_lock.EnterWriteLock();
try
{
_hashSet.Clear();
}
finally
{
if (_lock.IsWriteLockHeld)
{
_lock.ExitWriteLock();
}
}
}
public void Dispose()
{
_lock.Dispose();
GC.SuppressFinalize(this);
}
}

View File

@@ -1,6 +1,8 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using MemoryHandle = System.Buffers.MemoryHandle;
namespace Ghost.Core.Utilities;
@@ -51,3 +53,47 @@ public unsafe class NativeMemoryManager<T> : MemoryManager<T>
{
}
}
public sealed class CastMemoryManager<TFrom, TTo> : MemoryManager<TTo>
where TFrom : struct
where TTo : struct
{
private readonly Memory<TFrom> _from;
private MemoryHandle _innerHandle;
public CastMemoryManager(Memory<TFrom> from)
{
_from = from;
}
public override Span<TTo> GetSpan()
{
return MemoryMarshal.Cast<TFrom, TTo>(_from.Span);
}
public override MemoryHandle Pin(int elementIndex = 0)
{
_innerHandle = _from.Pin();
unsafe
{
int byteOffset = elementIndex * Unsafe.SizeOf<TTo>();
void* pointer = (byte*)_innerHandle.Pointer + byteOffset;
return new MemoryHandle(pointer, default, this);
}
}
public override void Unpin()
{
_innerHandle.Dispose();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_innerHandle.Dispose();
}
}
}

View File

@@ -1,47 +1,7 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Runtime.InteropServices;
namespace Ghost.Engine.AssetLoader;
public abstract class Asset : IResourceReleasable
{
private bool _disposed;
public Guid ID
{
get;
}
public abstract AssetType Type
{
get;
}
protected Asset(Guid id)
{
ID = id;
}
protected virtual void Release(IResourceDatabase resourceDatabase)
{
}
public void ReleaseResource(IResourceDatabase database)
{
if (_disposed)
{
return;
}
Release(database);
_disposed = true;
}
}
public readonly struct AssetReference : IEquatable<AssetReference>
{
private readonly int _value;
@@ -84,71 +44,3 @@ public readonly struct AssetReference : IEquatable<AssetReference>
return !(left == right);
}
}
[StructLayout(LayoutKind.Sequential, Size = 64)] // Leave extra space for future expansion without breaking compatibility
public struct TextureContentHeader
{
public uint width;
public uint height;
public uint depth;
public uint mipLevels;
public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D
public uint colorComponents;
}
public class TextureAsset : Asset
{
private MemoryBlock _textureData;
private readonly uint _width;
private readonly uint _height;
private readonly uint _depth;
private readonly uint _colorComponents;
private readonly uint _mipLevels;
private readonly uint _dimension;
private Handle<GPUTexture> _textureHandle;
public override AssetType Type => AssetType.Texture;
public uint Width => _width;
public uint Height => _height;
public uint Depth => _depth;
public uint MipLevels => _mipLevels;
public uint Dimension => _dimension;
public uint ColorComponents => _colorComponents;
public Handle<GPUTexture> TextureHandle => _textureHandle;
internal TextureAsset([OwnershipTransfer] ref MemoryBlock data, TextureContentHeader header, Guid id)
: base(id)
{
_textureData = data;
_width = header.width;
_height = header.height;
_depth = header.depth;
_mipLevels = header.mipLevels;
_dimension = header.dimension;
_colorComponents = header.colorComponents;
}
internal void SetTextureHandle(Handle<GPUTexture> handle, bool disposeCPUData = true)
{
_textureHandle = handle;
if (disposeCPUData)
{
_textureData.Dispose();
}
}
public ReadOnlySpan<T> GeData<T>()
where T : unmanaged
{
return _textureData.AsSpan<T>();
}
protected override void Release(IResourceDatabase resourceDatabase)
{
_textureData.Dispose();
resourceDatabase.ReleaseResource(_textureHandle.AsResource());
}
}

View File

@@ -1,13 +1,23 @@
using Ghost.Core;
using Ghost.Core.Utilities;
using Ghost.Engine.AssetLoader;
using Ghost.Graphics;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Utilities;
using TerraFX.Interop.Windows;
using System.Runtime.InteropServices;
namespace Ghost.Engine;
[StructLayout(LayoutKind.Sequential, Size = 64)] // Leave extra space for future expansion without breaking compatibility
public struct TextureContentHeader
{
public uint width;
public uint height;
public uint depth;
public uint mipLevels;
public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D
public uint colorComponents;
}
internal partial class AssetEntry
{
private unsafe class TextureData

View File

@@ -11,11 +11,24 @@ using Misaki.HighPerformance.LowLevel.Utilities;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Engine;
public enum AssetState : byte
public enum AssetType
{
Texture = 0,
Mesh = 1,
Material = 2,
Shaders = 3,
Scene = 4,
Audio = 5,
Video = 6,
Json = 7,
Unknown = 32, // We are unlikely to have more than 32 asset types.
}
public enum AssetState
{
Unloaded = 0,
Scheduled = 1,
@@ -192,6 +205,11 @@ internal unsafe partial class AssetEntry
public void OnReleaseResource()
{
s_onReleaseResource[(int)_assetType]?.Invoke(this);
if (_rawData.IsCreated)
{
_rawData.Dispose();
}
}
}
@@ -199,7 +217,7 @@ internal struct LoadAssetJob : IJob
{
public Guid assetID;
public AssetType assetType;
public GCHandle assetManagerHandle;
public AssetManager assetManager;
private static Result LoadRawData(IContentProvider contentProvider, AssetEntry entry)
{
@@ -232,8 +250,6 @@ internal struct LoadAssetJob : IJob
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))
@@ -279,8 +295,6 @@ internal partial class AssetManager : IDisposable
private readonly ConcurrentDictionary<Guid, AssetEntry> _entries;
private GCHandle _selfHandle;
// TODO
private Handle<GPUTexture> _fallbackTexture;
private Handle<GPUTexture> _fallbackNormalMap;
@@ -300,7 +314,6 @@ internal partial class AssetManager : IDisposable
_jobScheduler = jobScheduler;
_entries = new ConcurrentDictionary<Guid, AssetEntry>();
_selfHandle = GCHandle.Alloc(this, GCHandleType.Normal);
}
internal bool TryGetEntry(Guid guid, [NotNullWhen(true)] out AssetEntry? entry)
@@ -378,10 +391,10 @@ internal partial class AssetManager : IDisposable
{
assetID = entry.AssetId,
assetType = entry.AssetType,
assetManagerHandle = _selfHandle,
assetManager = this,
};
entry.SetLoadJobHandle(_jobScheduler.Schedule(ref job, dependency, JobPriority.Low)); // Use low priority to avoid blocking main thread critical tasks like rendering and physics.
entry.SetLoadJobHandle(_jobScheduler.Schedule(ref job, JobPriority.Low, dependency)); // Use low priority to avoid blocking main thread critical tasks like rendering and physics.
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -409,18 +422,18 @@ internal partial class AssetManager : IDisposable
return;
}
if (entry.State is AssetState.Loading or AssetState.Loaded or AssetState.Uploading)
if (Interlocked.CompareExchange(ref entry.StateValue, (int)AssetState.Scheduled, (int)AssetState.Ready) == (int)AssetState.Ready)
{
// 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.
EnsureScheduled(entry);
}
else
{
entry.SetPendingReimport();
return;
}
// 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()
@@ -431,6 +444,5 @@ internal partial class AssetManager : IDisposable
}
_entries.Clear();
_selfHandle.Free();
}
}

View File

@@ -19,7 +19,6 @@ internal unsafe struct ChunkInfo
internal unsafe struct JobChunkBatch<TJob> : IJobParallelFor
where TJob : unmanaged, IJobChunk
{
public TJob userJob;
public ReadOnlyUnsafeCollection<ChunkInfo> chunkInfos;