Refactor instance update flow, asset registry, and texture IO

- Renamed AddInstanceRequest to UpdateInstanceRequest; unified add/update logic for GPU instances
- Introduced UpdateGPUInstanceSystem to handle changed MeshInstance components
- Replaced QueryBuilder.Create() with QueryBuilder.New() for consistency
- Switched versioning in ChunkView HasChanged/HasStructuralChanged to uint
- Added extension-to-AssetType mapping in AssetHandlerRegistry
- Changed TextureAssetHandler/Processor to use nint for image data
- Enhanced DDS cache: read mipmap count, handle invalid files
- Updated ProjectBrowserViewModel to use IAssetRegistry
- Upgraded Misaki.HighPerformance and System.IO.Hashing packages
- Set DependencyChainCapacity in JobSchedulerDesc
- Fixed instance buffer logic in GhostRenderPipeline
- Miscellaneous cleanups and namespace improvements
This commit is contained in:
2026-04-22 15:36:49 +09:00
parent cb4092179f
commit 884611181a
19 changed files with 150 additions and 55 deletions

View File

@@ -9,12 +9,14 @@ namespace Ghost.Editor.Core.AssetHandler;
public static class AssetHandlerRegistry
{
private static readonly Dictionary<string, IAssetHandler> s_byExtension;
private static readonly Dictionary<string, AssetType> s_typeByExtension;
private static readonly Dictionary<Guid, IAssetHandler> s_byTypeId;
private static readonly Dictionary<Guid, int> s_versionByTypeId;
static AssetHandlerRegistry()
{
s_byExtension = new Dictionary<string, IAssetHandler>(StringComparer.OrdinalIgnoreCase);
s_typeByExtension = new Dictionary<string, AssetType>(StringComparer.OrdinalIgnoreCase);
s_byTypeId = new Dictionary<Guid, IAssetHandler>();
s_versionByTypeId = new Dictionary<Guid, int>();
}
@@ -28,6 +30,7 @@ public static class AssetHandlerRegistry
{
var normalizedExt = ext.StartsWith('.') ? ext : "." + ext;
s_byExtension[normalizedExt] = handler;
s_typeByExtension[normalizedExt] = handler.TargetAssetType;
}
}
@@ -59,4 +62,15 @@ public static class AssetHandlerRegistry
{
return s_byExtension.Keys;
}
public static AssetType GetAssetTypeByExtension(string extension)
{
if (string.IsNullOrEmpty(extension))
{
return AssetType.Unknown;
}
var normalized = extension.StartsWith('.') ? extension : "." + extension;
return s_typeByExtension.GetValueOrDefault(normalized, AssetType.Unknown);
}
}

View File

@@ -1,10 +1,8 @@
using Ghost.Core;
using Ghost.Engine;
using Ghost.Engine.AssetLoader;
using Ghost.Graphics.RHI;
using ImageMagick;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.AssetHandler;
@@ -339,8 +337,8 @@ internal class TextureAssetHandler : IAssetHandler
try
{
using var image = new MagickImage(sourceStream);
var pixels = image.GetPixels().GetValues();
if (pixels == null)
var pixels = image.GetPixelsUnsafe().GetAreaPointer(0, 0, image.Width, image.Height);
if (pixels == 0)
{
return Result.Failure("Failed to retrieve pixel data from the source image.");
}

View File

@@ -1,6 +1,4 @@
using Ghost.Graphics.RHI;
using Ghost.Nvtt;
using ImageMagick;
using Misaki.HighPerformance.LowLevel;
using System.IO.Hashing;
using System.Runtime.CompilerServices;
@@ -26,7 +24,7 @@ internal static class TextureProcessor
{
private readonly string _outputPath;
private readonly float[] _image;
private readonly nint _image;
private readonly uint _depth;
private readonly uint _width;
private readonly uint _height;
@@ -38,7 +36,7 @@ internal static class TextureProcessor
public Task Task => _completionSource.Task;
public NvttPipelineTask(string outputPath, float[] image, uint width, uint height, uint depth, TextureAssetSettings settings)
public NvttPipelineTask(string outputPath, nint image, uint width, uint height, uint depth, TextureAssetSettings settings)
{
_outputPath = outputPath;
_image = image;
@@ -60,10 +58,7 @@ internal static class TextureProcessor
? NvttInputFormat.NVTT_InputFormat_RGBA_32F
: NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below
fixed (void* pData = _image)
{
pSurface.Get()->SetImageData(inputFormat, (int)_width, (int)_height, 1, pData, NvttBoolean.NVTT_True, null);
}
pSurface.Get()->SetImageData(inputFormat, (int)_width, (int)_height, 1, (void*)_image, NvttBoolean.NVTT_True, null);
// stb gives us RGBA byte order; NVTT BGRA_8UB reads it as BGRA,
// so channels R and B are swapped — fix with swizzle(2,1,0,3).
@@ -164,7 +159,7 @@ internal static class TextureProcessor
}
}
public static async ValueTask<(string cachePath, int mipmapCount)> CompressToCacheAsync(string cachesFolderPath, Guid assetId, float[] image, uint width, uint height, uint depth, TextureAssetSettings settings, CancellationToken cancellationToken)
public static async ValueTask<(string cachePath, int mipmapCount)> CompressToCacheAsync(string cachesFolderPath, Guid assetId, nint image, uint width, uint height, uint depth, TextureAssetSettings settings, CancellationToken cancellationToken)
{
var settingsHash = ComputeSettingsHash(settings);
var cacheFileName = $"texturecache_{assetId:N}_{settingsHash:X16}.dds";
@@ -176,15 +171,37 @@ internal static class TextureProcessor
if (File.Exists(cachePath))
{
// TODO: Implement mipmap count retrieval from existing cache file
return (cachePath, 0);
}
foreach (var stale in Directory.EnumerateFiles(cachesFolderPath, $"texturecache_{assetId:N}_*.dds"))
{
File.Delete(stale);
using var fs = new FileStream(cachePath, FileMode.Open, FileAccess.Read);
using var reader = new BinaryReader(fs);
if (reader.ReadUInt32() != 0x20534444)
{
File.Delete(cachePath);
goto ScheduleWork;
}
// Read dwFlags (Offset 8)
// Skip dwSize (4 bytes), then read dwFlags (4 bytes)
reader.BaseStream.Seek(4, SeekOrigin.Current);
var flags = reader.ReadUInt32();
// The DDSD_MIPMAPCOUNT flag is 0x00020000
var hasMipMapFlag = (flags & 0x00020000) != 0;
// Read dwMipMapCount (Offset 28)
reader.BaseStream.Seek(28, SeekOrigin.Begin);
var mipMapCount = reader.ReadUInt32();
// Return the correct count
// If the flag is missing, or the count says 0, there is still 1 main image.
if (!hasMipMapFlag || mipMapCount == 0)
{
return (cachePath, 1);
}
return (cachePath, (int)mipMapCount);
}
ScheduleWork:
var workItem = new NvttPipelineTask(cachePath, image, width, height, depth, settings);
ThreadPool.UnsafeQueueUserWorkItem(workItem, true);
await workItem.Task.WaitAsync(cancellationToken).ConfigureAwait(false);

View File

@@ -1,5 +1,4 @@
using Ghost.Editor.Core.AssetHandler;
using Ghost.Engine;
using Microsoft.Data.Sqlite;
namespace Ghost.Editor.Core.Services;

View File

@@ -10,7 +10,7 @@ namespace Ghost.Editor.ViewModels.Controls;
internal partial class ProjectBrowserViewModel : ObservableObject
{
private readonly IInspectorService _inspectorService;
// private readonly IAssetService _assetService;
private readonly IAssetRegistry _assetRegistry;
private readonly Dictionary<string, ExplorerItem> _pathToDirectoryItemMap = new();
private ExplorerItem? _selectedItem;
@@ -40,10 +40,10 @@ internal partial class ProjectBrowserViewModel : ObservableObject
get; set;
} = string.Empty;
public ProjectBrowserViewModel(IInspectorService inspectorService) // , IAssetService assetService)
public ProjectBrowserViewModel(IInspectorService inspectorService, IAssetRegistry assetRegistry)
{
_inspectorService = inspectorService;
// _assetService = assetService;
_assetRegistry = assetRegistry;
var assetsRootItem = new ExplorerItem(EditorApplication.ASSETS_FOLDER_NAME, Path.Combine(EditorApplication.ProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
LoadSubFolderRecursive(assetsRootItem);
@@ -109,7 +109,7 @@ internal partial class ProjectBrowserViewModel : ObservableObject
}
else
{
// _assetService.OpenAsset(SelectedItem.FullName);
// _assetRegistry.OpenAsset(SelectedItem.FullName);
return (null, 1);
}
}

View File

@@ -70,6 +70,7 @@
x:Name="ContentFrame"
Grid.Row="2"
IsNavigationStackEnabled="False" />-->
<!-- Edit View -->
<Grid Grid.Row="1" Background="{ThemeResource LayerFillColorDefaultBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -390,6 +391,7 @@
Margin="8,0"
PlaceholderText="Search components..." />
<!-- Components List -->
<ListView
Grid.Row="2"
Padding="4,2,0,2"
@@ -406,6 +408,7 @@
<TextBlock Text="Test" />
</ListView>
<!-- Component Properties for Selected Component -->
<ScrollView
Grid.Row="3"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"

View File

@@ -20,13 +20,13 @@
<ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.8" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="3.0.0" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.14">
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="3.1.0" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.3.3" />
<PackageReference Include="System.IO.Hashing" Version="10.0.6" />
<PackageReference Include="System.IO.Hashing" Version="10.0.7" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" />
</ItemGroup>

View File

@@ -21,6 +21,7 @@ public sealed partial class EngineCore : IDisposable
{
ThreadCount = Environment.ProcessorCount - 2, // We -2 here, one for main thread, one for render thread
ThreadPriority = ThreadPriority.Normal,
DependencyChainCapacity = 8192,
};
_jobScheduler = new JobScheduler(in desc);

View File

@@ -41,13 +41,13 @@ internal partial class GhostRenderPipeline
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;
var totalUpdateCount = ghostPayload.UpdateRequest.Count; // + ghostPayload.UpdateRequest.Count;
if (!ghostPayload.AddRequest.IsEmpty)
if (!ghostPayload.UpdateRequest.IsEmpty)
{
var addDesc = new BufferDesc
{
Size = (nuint)ghostPayload.AddRequest.Count * MemoryUtility.SizeOf<UpdateInstanceData>(),
Size = (nuint)ghostPayload.UpdateRequest.Count * MemoryUtility.SizeOf<UpdateInstanceData>(),
Stride = (uint)MemoryUtility.SizeOf<UpdateInstanceData>(),
Usage = BufferUsage.Structured | BufferUsage.ShaderResource,
HeapType = HeapType.Upload
@@ -57,7 +57,7 @@ internal partial class GhostRenderPipeline
var pAddData = (UpdateInstanceData*)resourceDatabase.MapResource(addBuffer.AsResource(), 0, null);
var i = 0;
while (ghostPayload.AddRequest.TryDequeue(out var addRequest))
while (ghostPayload.UpdateRequest.TryDequeue(out var addRequest))
{
var (mesh, error) = resourceManager.GetMeshReference(addRequest.meshInstance.mesh);
if (error.IsFailure)
@@ -95,7 +95,7 @@ internal partial class GhostRenderPipeline
{
var addDesc = new BufferDesc
{
Size = (nuint)ghostPayload.AddRequest.Count * MemoryUtility.SizeOf<RemoveInstanceData>(),
Size = (nuint)ghostPayload.UpdateRequest.Count * MemoryUtility.SizeOf<RemoveInstanceData>(),
Stride = (uint)MemoryUtility.SizeOf<RemoveInstanceData>(),
Usage = BufferUsage.Structured | BufferUsage.ShaderResource,
HeapType = HeapType.Upload

View File

@@ -10,7 +10,7 @@ namespace Ghost.Engine.RenderPipeline;
internal sealed class GhostRenderPayload : IRenderPayload
{
public struct AddInstanceRequest
public struct UpdateInstanceRequest
{
public MeshInstance meshInstance;
public float4x4 localToWorld;
@@ -27,7 +27,7 @@ internal sealed class GhostRenderPayload : IRenderPayload
private UnsafeList<RenderRequest> _renderRequests;
private readonly ConcurrentQueue<AddInstanceRequest> _addRequest;
private readonly ConcurrentQueue<UpdateInstanceRequest> _updateRequest;
private readonly ConcurrentQueue<RemoveInstanceRequest> _removeRequest;
private uint _instanceCountBefore;
@@ -35,7 +35,7 @@ internal sealed class GhostRenderPayload : IRenderPayload
public ReadOnlySpan<RenderRequest> RenderRequests => _renderRequests;
public ConcurrentQueue<AddInstanceRequest> AddRequest => _addRequest;
public ConcurrentQueue<UpdateInstanceRequest> UpdateRequest => _updateRequest;
public ConcurrentQueue<RemoveInstanceRequest> RemoveRequest => _removeRequest;
public uint InstanceCountBefore => _instanceCountBefore;
public uint InstanceCount => _instanceCount;
@@ -45,7 +45,7 @@ internal sealed class GhostRenderPayload : IRenderPayload
_renderPipeline = renderPipeline;
_renderRequests = new UnsafeList<RenderRequest>(4, Misaki.HighPerformance.LowLevel.Buffer.AllocationHandle.Persistent);
_addRequest = new ConcurrentQueue<AddInstanceRequest>();
_updateRequest = new ConcurrentQueue<UpdateInstanceRequest>();
_removeRequest = new ConcurrentQueue<RemoveInstanceRequest>();
}
@@ -59,10 +59,15 @@ internal sealed class GhostRenderPayload : IRenderPayload
{
var index = _renderPipeline.GPUScene.AddInstance();
_addRequest.Enqueue(new AddInstanceRequest { instanceId = index, localToWorld = ltw, meshInstance = meshInstance });
_updateRequest.Enqueue(new UpdateInstanceRequest { instanceId = index, localToWorld = ltw, meshInstance = meshInstance });
return index;
}
public void UpdateInstance(uint instanceId, float4x4 ltw, ref readonly MeshInstance meshInstance)
{
_updateRequest.Enqueue(new UpdateInstanceRequest { instanceId = instanceId, localToWorld = ltw, meshInstance = meshInstance });
}
public void RemoveInstance(uint instanceId)
{
var swapWithInstanceId = _renderPipeline.GPUScene.RemoveInstance(instanceId);
@@ -81,13 +86,13 @@ internal sealed class GhostRenderPayload : IRenderPayload
{
// 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);
Logger.DebugAssert(_instanceCount == _instanceCountBefore + (uint)_updateRequest.Count - (uint)_removeRequest.Count);
}
public void Reset()
{
_renderRequests.Clear();
_addRequest.Clear();
_updateRequest.Clear();
_removeRequest.Clear();
}

View File

@@ -7,8 +7,8 @@ using Misaki.HighPerformance.Utilities;
namespace Ghost.Engine.Systems;
[UpdateAfter<RemoveGPUInstanceSystem>]
[RenderPipelineSystem<GhostRenderPipelineSettings>]
[UpdateAfter<UpdateGPUInstanceSystem>]
internal class AddGPUInstanceSystem : SystemBase
{
private RenderSystem _renderSystem = null!;
@@ -19,7 +19,7 @@ internal class AddGPUInstanceSystem : SystemBase
{
_renderSystem = systemAPI.World.GetService<RenderSystem>();
_meshInstanceQueryID = QueryBuilder.Create()
_meshInstanceQueryID = QueryBuilder.New()
.WithAll<MeshInstance, LocalToWorld>()
.WithAbsent<GPUInstanceRef>()
.Build(systemAPI.World, true);

View File

@@ -18,7 +18,7 @@ internal class RemoveGPUInstanceSystem : SystemBase
{
_renderSystem = systemAPI.World.GetService<RenderSystem>();
_gpuInstanceQueryID = QueryBuilder.Create()
_gpuInstanceQueryID = QueryBuilder.New()
.WithAll<GPUInstanceRef>()
.WithAbsent<MeshInstance>()
.Build(systemAPI.World, true);
@@ -29,6 +29,7 @@ internal class RemoveGPUInstanceSystem : SystemBase
protected override void OnUpdate(ref readonly SystemAPI systemAPI)
{
var payload = (GhostRenderPayload)_renderSystem.GetCurrentFramePayload();
payload.BeginRecord();
ref var gpuInstanceQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_gpuInstanceQueryID);

View File

@@ -0,0 +1,56 @@
using Ghost.Core;
using Ghost.Engine.Components;
using Ghost.Engine.RenderPipeline;
using Ghost.Entities;
using Ghost.Graphics;
using Misaki.HighPerformance.Utilities;
namespace Ghost.Engine.Systems;
[RenderPipelineSystem<GhostRenderPipelineSettings>]
[UpdateAfter<RemoveGPUInstanceSystem>]
[UpdateBefore<AddGPUInstanceSystem>]
internal class UpdateGPUInstanceSystem : SystemBase
{
private RenderSystem _renderSystem = null!;
private Identifier<EntityQuery> _gpuInstanceQueryID;
protected override void OnInitialize(ref readonly SystemAPI systemAPI)
{
_renderSystem = systemAPI.World.GetService<RenderSystem>();
_gpuInstanceQueryID = QueryBuilder.New()
.WithAll<LocalToWorld, MeshInstance, GPUInstanceRef>()
.Build(systemAPI.World, true);
RequireQueryForUpdate(_gpuInstanceQueryID);
}
protected override void OnUpdate(ref readonly SystemAPI systemAPI)
{
var playload = (GhostRenderPayload)_renderSystem.GetCurrentFramePayload();
ref var instanceQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_gpuInstanceQueryID);
foreach (var chunk in instanceQuery.GetChunkIterator())
{
if (!chunk.HasChanged<MeshInstance>(LastSystemVersion))
{
continue;
}
var ltws = chunk.GetComponentData<LocalToWorld>();
var meshs = chunk.GetComponentData<MeshInstance>();
var gpuInstances = chunk.GetComponentData<GPUInstanceRef>();
for (var i = 0; i < chunk.EntityCount; i++)
{
ref readonly var ltw = ref ltws.GetElementUnsafe(i);
ref readonly var mesh = ref meshs.GetElementUnsafe(i);
ref readonly var instance = ref gpuInstances.GetElementUnsafe(i);
playload.UpdateInstance(instance.gpuSceneIndex, ltw.matrix, in mesh);
}
}
}
}

View File

@@ -130,7 +130,7 @@ public readonly unsafe ref struct ChunkView
/// <param name="version">The version number to compare against the component's current version. Must be greater than or equal to zero.</param>
/// <returns>true if the component's current version is less than or equal to the specified version; otherwise, false.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HasChanged(Identifier<IComponent> id, int version)
public bool HasChanged(Identifier<IComponent> id, uint version)
{
var layout = GetLayout(id);
return version < _pVersion[layout.versionIndex];
@@ -144,7 +144,7 @@ public readonly unsafe ref struct ChunkView
/// <param name="version">The version number to compare against the current version of the component.</param>
/// <returns>true if the component of space T has changed since the specified version; otherwise, false.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly bool HasChanged<T>(int version)
public readonly bool HasChanged<T>(uint version)
where T : unmanaged, IComponent
{
var layout = GetLayout(ComponentTypeID<T>.Value);
@@ -157,7 +157,7 @@ public readonly unsafe ref struct ChunkView
/// <param name="version">The version number to compare against the chunk's structural version.</param>
/// <returns>true if the chunk's structure has changed since the specified version; otherwise, false.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly bool HasStructuralChanged(int version)
public readonly bool HasStructuralChanged(uint version)
{
return version < _structuralVersion;
}
@@ -502,7 +502,7 @@ public ref partial struct QueryBuilder : IDisposable
_rw = new UnsafeList<Identifier<IComponent>>(4, _scope.AllocationHandle);
}
public static QueryBuilder Create()
public static QueryBuilder New()
{
return new QueryBuilder();
}

View File

@@ -28,11 +28,17 @@ public abstract class SystemBase : ISystem
{
private UnsafeList<int> _requiredQueries;
/// <summary>
/// Gets the world that the system is running on currently.
/// </summary>
public World World
{
get; init;
} = null!;
/// <summary>
/// Gets the last version that the system update.
/// </summary>
public uint LastSystemVersion
{
get; internal set;

View File

@@ -1,10 +1,5 @@
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;

View File

@@ -15,7 +15,7 @@ internal class CameraMovingSystem : ISystem
public void Initialize(ref readonly SystemAPI systemAPI)
{
_cameraQueryID = QueryBuilder.Create()
_cameraQueryID = QueryBuilder.New()
.WithAll<Camera, LocalToWorld>()
.Build(systemAPI.World, true);