2 Commits

Author SHA1 Message Date
7dac1e4437 refactor: rewrite scene streaming & shader bridge
This commit overhauls the scene management and streaming architecture to use a chunk-based asynchronous loading paradigm, and completely decouples the graphics runtime from editor compilation services to eliminate circular dependencies.

Shader Bridge Decoupling:
- Resolved circular runtime dependency between Ghost.Graphics and Ghost.Editor.Core.
- Shifted EditorShaderCompilerBridge from querying EngineCore to a decoupled event-driven model using IShaderCompilationBridge.
- Introduced custom stack-friendly delegate ShaderVariantCompiledHandler to handle ReadOnlySpan<ShaderByteCode> compilation buffers while maintaining zero-allocation constraints.
- Updated ShaderLibrary to self-manage bytecode caching and stale pipeline eviction by listening to compiler events.

Scene & Streaming Overhaul:
- Re-implemented Scene.cs with a high-performance two-phase asynchronous scene loading architecture.
- Replaced outdated per-entity component processing with chunk-level shared data management using the ISharedComponent paradigm for SceneID.
- Optimized ECS Query.cs and Archetype.cs to handle streaming chunk operations efficiently.
- Updated AssetManager, ResourceStreamingProcessor, and asset entries (SceneAssetEntry, MeshAssetEntry, TextureAssetEntry) to support the new streaming workflow.
2026-05-21 21:43:41 +09:00
e04c7eb6a7 Refactor ECS: split IComponent into Data/Shared types
Refactored ECS to distinguish IComponentData and ISharedComponent. Updated all component implementations and method constraints accordingly. Changed SceneID handling to use shared components and updated related APIs and tests. Fixed BufferReader pointer advancement and initialized EntityManager managed storage.
2026-05-18 10:51:02 +09:00
42 changed files with 553 additions and 390 deletions

2
.gitignore vendored
View File

@@ -13,7 +13,7 @@
AGENTS.md AGENTS.md
.opencode/ .opencode/
.code-review-graph/ .code-review-graph/
.github/instructions/ .antigravitycli/
ref/ ref/
docfx/ docfx/

View File

@@ -14,13 +14,13 @@ public readonly struct ComponentObject
} }
public ref T GetData<T>() public ref T GetData<T>()
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
return ref _world.EntityManager.GetComponent<T>(_entity); return ref _world.EntityManager.GetComponent<T>(_entity);
} }
public void SetData<T>(in T data) public void SetData<T>(in T data)
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
_world.EntityManager.SetComponent(_entity, data); _world.EntityManager.SetComponent(_entity, data);
} }

View File

@@ -31,20 +31,19 @@ public static class SceneGraphBuilder
foreach (var chunk in query.GetChunkIterator()) foreach (var chunk in query.GetChunkIterator())
{ {
var entities = chunk.GetEntities(); var entities = chunk.GetEntities();
var sceneIDs = chunk.GetComponentData<SceneID>(); var scene = chunk.GetSharedComponent<SceneID>();
for (var i = 0; i < chunk.EntityCount; i++) for (var i = 0; i < chunk.EntityCount; i++)
{ {
var s = sceneIDs[i].value; if (scene.value == Scene.INVALID_ID)
if (s == Scene.INVALID_ID)
{ {
continue; continue;
} }
if (!sceneMap.TryGetValue(s, out var list)) if (!sceneMap.TryGetValue(scene.value, out var list))
{ {
list = new List<Entity>(); list = new List<Entity>();
sceneMap[s] = list; sceneMap[scene.value] = list;
} }
list.Add(entities[i]); list.Add(entities[i]);

View File

@@ -3,11 +3,8 @@ using Ghost.Core.Graphics;
using Ghost.Editor.Core.Assets; using Ghost.Editor.Core.Assets;
using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Utilities; using Ghost.Editor.Core.Utilities;
using Ghost.Engine;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Microsoft.Extensions.DependencyInjection;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@@ -19,13 +16,13 @@ internal sealed class EditorShaderCompilerBridge : IShaderCompilationBridge
private readonly IAssetRegistry _assetRegistry; private readonly IAssetRegistry _assetRegistry;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IShaderCompiler _compiler; private readonly IShaderCompiler _compiler;
private EngineCore? _engineCore;
private readonly ConcurrentDictionary<ulong, Guid> _shaderIdToAssetId = new(); private readonly ConcurrentDictionary<ulong, Guid> _shaderIdToAssetId = new();
private readonly ConcurrentDictionary<Guid, Dictionary<int, string>[]> _assetKeywordMappings = new(); private readonly ConcurrentDictionary<Guid, Dictionary<int, string>[]> _assetKeywordMappings = new();
private Task? _shaderDictionaryPopulated; private Task? _shaderDictionaryPopulated;
public event Action<Key64<ShaderVariant>, ulong>? OnShaderVariantCompiled; public event ShaderVariantCompiledHandler? OnShaderVariantCompiled;
public event Action<ulong>? OnShaderInvalidated;
public EditorShaderCompilerBridge(IAssetRegistry assetRegistry, IServiceProvider serviceProvider) public EditorShaderCompilerBridge(IAssetRegistry assetRegistry, IServiceProvider serviceProvider)
{ {
@@ -50,13 +47,7 @@ internal sealed class EditorShaderCompilerBridge : IShaderCompilationBridge
_shaderIdToAssetId[nameHash] = guid; _shaderIdToAssetId[nameHash] = guid;
BuildKeywordMappings(result.Value, guid); BuildKeywordMappings(result.Value, guid);
_engineCore ??= _serviceProvider.GetService<EngineCore>(); OnShaderInvalidated?.Invoke(nameHash);
if (_engineCore != null)
{
var shaderLibrary = _engineCore.RenderSystem.ShaderLibrary;
var pipelineLibrary = _engineCore.RenderSystem.GraphicsEngine.PipelineLibrary;
shaderLibrary.InvalidateShaderCache(nameHash, pipelineLibrary);
}
} }
} }
} }
@@ -269,11 +260,6 @@ internal sealed class EditorShaderCompilerBridge : IShaderCompilationBridge
return Task.CompletedTask; return Task.CompletedTask;
} }
if (_engineCore == null)
{
return Task.CompletedTask;
}
using var compiled = compileResult.Value; using var compiled = compileResult.Value;
var stageCount = 0; var stageCount = 0;
@@ -298,11 +284,7 @@ internal sealed class EditorShaderCompilerBridge : IShaderCompilationBridge
byteCodes[idx++] = new ShaderByteCode { pCode = (byte*)compiled.psResult.GetUnsafePtr(), size = (ulong)compiled.psResult.Length }; byteCodes[idx++] = new ShaderByteCode { pCode = (byte*)compiled.psResult.GetUnsafePtr(), size = (ulong)compiled.psResult.Length };
} }
var shaderLibrary = _engineCore.RenderSystem.ShaderLibrary; OnShaderVariantCompiled?.Invoke(shaderId, passIndex, variantKey, new ReadOnlySpan<ShaderByteCode>(byteCodes, stageCount));
shaderLibrary.CacheCompiledResult(shaderId, passIndex, variantKey, new ReadOnlySpan<ShaderByteCode>(byteCodes, stageCount));
var (compiledHash, _) = shaderLibrary.GetCompiledHash(shaderId, passIndex, variantKey);
OnShaderVariantCompiled?.Invoke(variantKey, compiledHash);
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -331,11 +313,6 @@ internal sealed class EditorShaderCompilerBridge : IShaderCompilationBridge
return Task.CompletedTask; return Task.CompletedTask;
} }
if (_engineCore == null)
{
return Task.CompletedTask;
}
using var bytecodeArray = compileResult.Value; using var bytecodeArray = compileResult.Value;
var byteCode = new ShaderByteCode var byteCode = new ShaderByteCode
@@ -344,11 +321,7 @@ internal sealed class EditorShaderCompilerBridge : IShaderCompilationBridge
size = (ulong)bytecodeArray.Length size = (ulong)bytecodeArray.Length
}; };
var shaderLibrary = _engineCore.RenderSystem.ShaderLibrary; OnShaderVariantCompiled?.Invoke(shaderId, passIndex, variantKey, new ReadOnlySpan<ShaderByteCode>(ref byteCode));
shaderLibrary.CacheCompiledResult(shaderId, passIndex, variantKey, new ReadOnlySpan<ShaderByteCode>(ref byteCode));
var (compiledHash, _) = shaderLibrary.GetCompiledHash(shaderId, passIndex, variantKey);
OnShaderVariantCompiled?.Invoke(variantKey, compiledHash);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -29,7 +29,7 @@ public class EditorWorldService : IDisposable
{ {
var scene = SceneManager.CreateScene(); var scene = SceneManager.CreateScene();
var entity = EditorWorld.EntityManager.CreateEntity(); var entity = EditorWorld.EntityManager.CreateEntity();
EditorWorld.EntityManager.AddComponent(entity, new Engine.Components.SceneID EditorWorld.EntityManager.AddSharedComponent(entity, new Engine.Components.SceneID
{ {
value = scene.ID value = scene.ID
}); });

View File

@@ -179,20 +179,19 @@ public class SceneGraphSyncService
foreach (var chunk in query.GetChunkIterator()) foreach (var chunk in query.GetChunkIterator())
{ {
var entities = chunk.GetEntities(); var entities = chunk.GetEntities();
var sceneIDs = chunk.GetComponentData<SceneID>(); var scene = chunk.GetSharedComponent<SceneID>();
for (var i = 0; i < chunk.EntityCount; i++) for (var i = 0; i < chunk.EntityCount; i++)
{ {
var s = sceneIDs[i].value; if (scene.value == Scene.INVALID_ID)
if (s == Scene.INVALID_ID)
{ {
continue; continue;
} }
if (!sceneMap.TryGetValue(s, out var list)) if (!sceneMap.TryGetValue(scene.value, out var list))
{ {
list = new List<Entity>(); list = new List<Entity>();
sceneMap[s] = list; sceneMap[scene.value] = list;
} }
list.Add(entities[i]); list.Add(entities[i]);

View File

@@ -1,6 +1,5 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.SceneGraph;
using Ghost.Editor.Core.Utilities; using Ghost.Editor.Core.Utilities;
using Ghost.Engine; using Ghost.Engine;
using Ghost.Engine.Components; using Ghost.Engine.Components;
@@ -350,7 +349,7 @@ internal class SceneSerializationService : IDisposable
continue; continue;
} }
world.EntityManager.SetComponent(entity, new SceneID { value = activeScene.ID }); world.EntityManager.SetSharedComponent(entity, new SceneID { value = activeScene.ID });
var entityData = data.Entities[fileIndex]; var entityData = data.Entities[fileIndex];
ref var list = ref typeIds[fileIndex]; ref var list = ref typeIds[fileIndex];
@@ -421,17 +420,12 @@ internal class SceneSerializationService : IDisposable
#region Save Scene from Editor World #region Save Scene from Editor World
public unsafe Result SaveSceneFromEditorWorld(string filePath, Scene scene) public unsafe void SaveSceneFromEditorWorld(string filePath, Scene scene)
{ {
var world = _worldService.EditorWorld; var world = _worldService.EditorWorld;
using var scope = AllocationManager.CreateStackScope(); using var scope = AllocationManager.CreateStackScope();
using var sceneEntities = SceneManager.GetSceneEntities(scene, world, scope.AllocationHandle); using var sceneEntities = SceneManager.GetSceneEntities(world, scene, scope.AllocationHandle);
if (sceneEntities.Count == 0)
{
return Result.Failure("No entities found for the specified scene.");
}
var entities = new List<Entity>(sceneEntities.Count); var entities = new List<Entity>(sceneEntities.Count);
for (var i = 0; i < sceneEntities.Count; i++) for (var i = 0; i < sceneEntities.Count; i++)
@@ -511,8 +505,6 @@ internal class SceneSerializationService : IDisposable
writer.Flush(); writer.Flush();
File.WriteAllBytes(filePath, stream.ToArray()); File.WriteAllBytes(filePath, stream.ToArray());
return Result.Success();
} }
private static List<Entity> SortEntitiesByHierarchy(World world, List<Entity> entities) private static List<Entity> SortEntitiesByHierarchy(World world, List<Entity> entities)

View File

@@ -70,8 +70,6 @@ public partial class App : Application
services.AddSingleton<IContentProvider, EditorContentProvider>(); services.AddSingleton<IContentProvider, EditorContentProvider>();
services.AddSingleton<IShaderCompilationBridge, EditorShaderCompilerBridge>(); services.AddSingleton<IShaderCompilationBridge, EditorShaderCompilerBridge>();
services.AddSingleton<EngineCore>();
services.AddSingleton<EngineEditorViewModel>(); services.AddSingleton<EngineEditorViewModel>();
services.AddTransient<ContentBrowserViewModel>(); services.AddTransient<ContentBrowserViewModel>();

View File

@@ -50,4 +50,9 @@ internal partial class ContentBrowser
// Refresh the view model to show the new folder // Refresh the view model to show the new folder
viewModel.NavigateToDirectory(currentDir); viewModel.NavigateToDirectory(currentDir);
} }
[ContextMenuItem("project-browser", "Create/Asset/Scene")]
private static void CreateSceneAsset()
{
}
} }

View File

@@ -154,13 +154,10 @@ public interface IBufferReader
T Read<T>() T Read<T>()
where T : unmanaged; where T : unmanaged;
void ReadExactly<T>(Span<T> dst) void ReadExactly<T>(Span<T> dst)
where T : unmanaged; where T : unmanaged;
ReadOnlySpan<T> ReadSpan<T>(int length) ReadOnlySpan<T> ReadSpan<T>(int length)
where T : unmanaged; where T : unmanaged;
ReadOnlySpan<T> ReadToEnd<T>() ReadOnlySpan<T> ReadToEnd<T>()
where T : unmanaged; where T : unmanaged;
} }
@@ -211,7 +208,7 @@ public unsafe struct BufferReader : IBufferReader
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void ReadExactly<T>(Span<T> dst) public void ReadExactly<T>(Span<T> dst)
where T : unmanaged where T : unmanaged
{ {
var newAddr = _address + sizeof(T) * dst.Length; var newAddr = _address + sizeof(T) * dst.Length;
@@ -219,9 +216,9 @@ public unsafe struct BufferReader : IBufferReader
var src = new ReadOnlySpan<T>(_address, dst.Length); var src = new ReadOnlySpan<T>(_address, dst.Length);
src.CopyTo(dst); src.CopyTo(dst);
_address = newAddr;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<T> ReadSpan<T>(int length) public ReadOnlySpan<T> ReadSpan<T>(int length)
where T : unmanaged where T : unmanaged

View File

@@ -6,7 +6,7 @@ using Misaki.HighPerformance.Mathematics;
namespace Ghost.Engine.Components; namespace Ghost.Engine.Components;
public unsafe struct Camera : IComponent public unsafe struct Camera : IComponentData
{ {
public float nearClipPlane; public float nearClipPlane;
public float farClipPlane; public float farClipPlane;

View File

@@ -4,7 +4,7 @@ using Ghost.Graphics.Services;
namespace Ghost.Engine.Components; namespace Ghost.Engine.Components;
public struct GPUInstanceRef : IComponent public struct GPUInstanceRef : IComponentData
{ {
public uint gpuInstanceIndex; public uint gpuInstanceIndex;
public Identifier<MaterialPalette> materialPalette; public Identifier<MaterialPalette> materialPalette;

View File

@@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
namespace Ghost.Engine.Components; namespace Ghost.Engine.Components;
[HideEditor] [HideEditor]
public struct Hierarchy : IComponent public struct Hierarchy : IComponentData
{ {
public Entity parent; public Entity parent;
public Entity firstChild; public Entity firstChild;

View File

@@ -3,7 +3,7 @@ using Misaki.HighPerformance.Mathematics;
namespace Ghost.Engine.Components; namespace Ghost.Engine.Components;
public struct LocalToWorld : IComponent public struct LocalToWorld : IComponentData
{ {
public float4x4 matrix; public float4x4 matrix;
} }

View File

@@ -5,7 +5,7 @@ using Ghost.Graphics.Services;
namespace Ghost.Engine.Components; namespace Ghost.Engine.Components;
public struct MeshInstance : IComponent public struct MeshInstance : IComponentData
{ {
public Handle<Mesh> mesh; public Handle<Mesh> mesh;
public Identifier<MaterialPalette> materialPalette; public Identifier<MaterialPalette> materialPalette;

View File

@@ -66,52 +66,51 @@ public struct Scene : IEquatable<Scene>
} }
} }
/// <summary> public class LoadedSceneData : IDisposable
/// Manages scenes within a world.
/// </summary>
/// <remarks>
/// This is a minimal runtime representation. All metadata (like scene names)
/// should be stored in editor-only classes (SceneNode).
/// </remarks>
public static class SceneManager
{ {
internal struct LoadedSceneData : IDisposable public struct EntityData : IDisposable
{ {
internal struct EntityData : IDisposable public int fileLocalIndex;
{ public UnsafeList<Identifier<IComponent>> componentTypeIDs;
public int fileLocalIndex; public UnsafeList<(Identifier<IComponent> typeID, UnsafeArray<byte> data)> componentData;
public UnsafeList<Identifier<IComponent>> componentTypeIDs; public UnsafeList<(int componentDataIndex, UnsafeArray<int> fieldOffsets)> entityFields;
public UnsafeList<(Identifier<IComponent> typeID, UnsafeArray<byte> data)> componentData;
public UnsafeList<(int componentDataIndex, UnsafeArray<int> fieldOffsets)> entityFields;
public void Dispose()
{
componentTypeIDs.Dispose();
for (int i = 0; i < componentData.Count; i++)
{
componentData[i].data.Dispose();
}
componentData.Dispose();
for (int i = 0; i < entityFields.Count; i++)
{
entityFields[i].fieldOffsets.Dispose();
}
entityFields.Dispose();
}
}
public UnsafeArray<EntityData> entities;
public void Dispose() public void Dispose()
{ {
for (int i = 0; i < entities.Length; i++) componentTypeIDs.Dispose();
for (int i = 0; i < componentData.Count; i++)
{ {
entities[i].Dispose(); componentData[i].data.Dispose();
} }
entities.Dispose(); componentData.Dispose();
for (int i = 0; i < entityFields.Count; i++)
{
entityFields[i].fieldOffsets.Dispose();
}
entityFields.Dispose();
} }
} }
public UnsafeArray<EntityData> entities;
public void Dispose()
{
for (int i = 0; i < entities.Length; i++)
{
entities[i].Dispose();
}
entities.Dispose();
GC.SuppressFinalize(this);
}
}
/// <summary>
/// Manages scenes within a world.
/// </summary>
public static class SceneManager
{
private static ushort s_nextSceneID; private static ushort s_nextSceneID;
private static readonly Queue<ushort> s_recycledSceneIDs = new(); private static readonly Queue<ushort> s_recycledSceneIDs = new();
@@ -215,7 +214,10 @@ public static class SceneManager
else else
{ {
compData.Dispose(); compData.Dispose();
if (fieldCount > 0) fieldOffsets.Dispose(); if (fieldCount > 0)
{
fieldOffsets.Dispose();
}
} }
} }
@@ -242,23 +244,37 @@ public static class SceneManager
return ParseSceneData(header, ref reader, allocationHandle); return ParseSceneData(header, ref reader, allocationHandle);
} }
internal static unsafe Result<int> MaterializeScene(World world, ref readonly LoadedSceneData result, Scene scene) /// <summary>
/// Materializes the loaded scene data into actual entities in the world, setting their components and remapping entity references.
/// </summary>
/// <remarks>
/// This method create entities directly into the world. Must ensure it's the safe to perform such strcture changes before calling this method (e.g. not in the middle of a system update that might be iterating over entities).
/// </remarks>
/// <param name="world">The world into which to materialize the scene data.</param>
/// <param name="result">The loaded scene data.</param>
/// <param name="scene">The scene to which the entities belong.</param>
/// <param name="startEntityIndex">The index of the first entity to materialize.</param>
/// <param name="length">The number of entities to materialize.</param>
public static unsafe void MaterializeScene(World world, ref readonly LoadedSceneData result, Scene scene, int startEntityIndex, int length)
{ {
if (startEntityIndex < 0 || startEntityIndex + length > result.entities.Length)
{
Logger.Error($"Invalid entity index range for materialization: start={startEntityIndex}, length={length}, total={result.entities.Length}");
return;
}
using var scope = AllocationManager.CreateStackScope(); using var scope = AllocationManager.CreateStackScope();
using var forwardMap = new UnsafeHashMap<int, Entity>(result.entities.Length, scope.AllocationHandle); using var forwardMap = new UnsafeHashMap<int, Entity>(result.entities.Length, scope.AllocationHandle);
using var sharedCom = new SharedComponentSet(256, scope.AllocationHandle); using var sharedCom = new SharedComponentSet(256, scope.AllocationHandle);
// Create entities and set SceneID // Create entities and set SceneID
for (var i = 0; i < result.entities.Length; i++) for (var i = startEntityIndex; i < startEntityIndex + length; i++)
{ {
ref var pending = ref result.entities[i]; ref var pending = ref result.entities[i];
using var typeIds = new UnsafeList<Identifier<IComponent>>(pending.componentTypeIDs.Count + 1, scope.AllocationHandle); using var typeIds = new UnsafeList<Identifier<IComponent>>(pending.componentTypeIDs.Count + 1, scope.AllocationHandle);
typeIds.Add(ComponentTypeID<SceneID>.Value); typeIds.Add(ComponentTypeID<SceneID>.Value);
for (int j = 0; j < pending.componentTypeIDs.Count; j++) typeIds.AddRange(pending.componentTypeIDs);
{
typeIds.Add(pending.componentTypeIDs[j]);
}
sharedCom.With(new SceneID { value = scene.ID }); sharedCom.With(new SceneID { value = scene.ID });
@@ -270,11 +286,13 @@ public static class SceneManager
} }
// Set component data // Set component data
for (var i = 0; i < result.entities.Length; i++) for (var i = startEntityIndex; i < startEntityIndex + length; i++)
{ {
ref var pending = ref result.entities[i]; ref var pending = ref result.entities[i];
if (!forwardMap.TryGetValue(pending.fileLocalIndex, out var entity)) if (!forwardMap.TryGetValue(pending.fileLocalIndex, out var entity))
{
continue; continue;
}
for (var j = 0; j < pending.componentData.Count; j++) for (var j = 0; j < pending.componentData.Count; j++)
{ {
@@ -284,11 +302,13 @@ public static class SceneManager
} }
// Remap entity references // Remap entity references
for (var i = 0; i < result.entities.Length; i++) for (var i = startEntityIndex; i < startEntityIndex + length; i++)
{ {
ref var pending = ref result.entities[i]; ref var pending = ref result.entities[i];
if (!forwardMap.TryGetValue(pending.fileLocalIndex, out var entity)) if (!forwardMap.TryGetValue(pending.fileLocalIndex, out var entity))
{
continue; continue;
}
for (var j = 0; j < pending.entityFields.Count; j++) for (var j = 0; j < pending.entityFields.Count; j++)
{ {
@@ -297,7 +317,9 @@ public static class SceneManager
var pComponent = world.EntityManager.GetComponent(entity, compTypeID); var pComponent = world.EntityManager.GetComponent(entity, compTypeID);
if (pComponent == null) if (pComponent == null)
{
continue; continue;
}
for (var f = 0; f < fieldOffsets.Length; f++) for (var f = 0; f < fieldOffsets.Length; f++)
{ {
@@ -313,8 +335,6 @@ public static class SceneManager
} }
} }
} }
return Result.Success(result.entities.Length);
} }
/// <summary> /// <summary>
@@ -327,16 +347,13 @@ public static class SceneManager
var queryID = new QueryBuilder().WithAll<SceneID>().Build(world); var queryID = new QueryBuilder().WithAll<SceneID>().Build(world);
ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID); ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);
using var scope = AllocationManager.CreateStackScope();
using var ecb = new EntityCommandBuffer(512, scope.AllocationHandle);
// Iterate through all matching entities // Iterate through all matching entities
foreach (var chunk in query.GetChunkIterator()) foreach (var chunk in query.GetChunkIterator())
{ {
ref readonly var sceneID = ref chunk.GetSharedComponent<SceneID>(); ref readonly var sceneID = ref chunk.GetSharedComponent<SceneID>();
if (sceneID.value == scene.ID) if (sceneID.value == scene.ID)
{ {
ecb.DestroyEntities(chunk.GetEntities()); world.EntityManager.DestroyEntities(chunk.GetEntities());
} }
} }
@@ -350,7 +367,7 @@ public static class SceneManager
/// <param name="world">The world containing the entities.</param> /// <param name="world">The world containing the entities.</param>
/// <param name="entities">Span to store the entities.</param> /// <param name="entities">Span to store the entities.</param>
/// <returns>The number of entities written to the span.</returns> /// <returns>The number of entities written to the span.</returns>
public static UnsafeList<Entity> GetSceneEntities(Scene scene, World world, AllocationHandle handle) public static UnsafeList<Entity> GetSceneEntities(World world, Scene scene, AllocationHandle handle)
{ {
var queryID = new QueryBuilder().WithAll<SceneID>().Build(world); var queryID = new QueryBuilder().WithAll<SceneID>().Build(world);
ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID); ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);

View File

@@ -4,6 +4,7 @@ using Ghost.Graphics.RHI;
using Ghost.Graphics.Services; using Ghost.Graphics.Services;
using Misaki.HighPerformance.Jobs; using Misaki.HighPerformance.Jobs;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using TerraFX.Interop.Windows;
namespace Ghost.Engine.Streaming; namespace Ghost.Engine.Streaming;
@@ -53,7 +54,7 @@ internal static class AssetEntryFactory
} }
// TODO: Progress report // TODO: Progress report
internal abstract class AssetEntry internal abstract class AssetEntry : IAssetEntry
{ {
private readonly AssetManager _assetManager; private readonly AssetManager _assetManager;
private readonly ResourceManager _resourceManager; private readonly ResourceManager _resourceManager;
@@ -72,19 +73,27 @@ internal abstract class AssetEntry
protected ResourceManager ResourceManager => _resourceManager; protected ResourceManager ResourceManager => _resourceManager;
protected IResourceDatabase ResourceDatabase => _resourceDatabase; protected IResourceDatabase ResourceDatabase => _resourceDatabase;
public AssetManager AssetManager => _assetManager;
public Guid AssetId => _assetId; public Guid AssetId => _assetId;
public JobHandle LoadJobHandle => _loadJobHandle; public JobHandle LoadJobHandle => _loadJobHandle;
public AssetType AssetType => _assetType; public AssetType AssetType => _assetType;
public ReadOnlySpan<Guid> Dependencies => _dependencies; public ReadOnlySpan<Guid> Dependencies => _dependencies;
public int RefCount => Volatile.Read(ref _refCount); public int RefCount => Volatile.Read(ref _refCount);
public ref bool PendingReimport => ref _pendingReimport;
public ref int StateValue => ref _state; public ref int StateValue => ref _state;
public AssetState State public AssetState State
{ {
get => (AssetState)Volatile.Read(ref _state); get => (AssetState)Volatile.Read(ref _state);
set => Volatile.Write(ref _state, (int)value); set
{
Volatile.Write(ref _state, (int)value);
if (Volatile.Read(ref _state) == (int)AssetState.Ready)
{
if (Interlocked.CompareExchange(ref _pendingReimport, false, true))
{
_assetManager.ReimportAsset(_assetId); // re-queue
}
}
}
} }
protected AssetEntry(AssetManager manager, IResourceDatabase resourceDatabase, ResourceManager resourceManager, Guid assetId, AssetType assetType, Guid[] dependencies) protected AssetEntry(AssetManager manager, IResourceDatabase resourceDatabase, ResourceManager resourceManager, Guid assetId, AssetType assetType, Guid[] dependencies)
@@ -141,27 +150,35 @@ internal abstract class AssetEntry
return newRefCount; return newRefCount;
} }
public abstract Result OnLoadContent(Stream contentStream); public virtual void OnReleaseResource()
public abstract void OnReleaseResource();
}
internal abstract class ProcessableAssetEntry : AssetEntry
{
protected ProcessableAssetEntry(AssetManager manager, IResourceDatabase resourceDatabase, ResourceManager resourceManager, Guid assetId, AssetType assetType, Guid[] dependencies)
: base(manager, resourceDatabase, resourceManager, assetId, assetType, dependencies)
{ {
} }
public abstract Result<JobHandle> OnProcessing(object? context);
} }
internal abstract class UploadableAssetEntry : AssetEntry interface IAssetEntry
{ {
protected UploadableAssetEntry(AssetManager manager, IResourceDatabase resourceDatabase, ResourceManager resourceManager, Guid assetId, AssetType assetType, Guid[] dependencies) Guid AssetId { get; }
: base(manager, resourceDatabase, resourceManager, assetId, assetType, dependencies) AssetType AssetType { get; }
{ ReadOnlySpan<Guid> Dependencies { get; }
} int RefCount { get; }
AssetState State { get; set; }
public abstract Result OnRecordUploadCommands(ResourceStreamingContext context); void AddRef();
public abstract void OnUploadComplete(ResourceStreamingContext context); int Release();
}
internal interface ILoadableAssetEntry : IAssetEntry
{
Result OnLoadContent(Stream contentStream);
}
internal interface IProcessableAssetEntry : IAssetEntry
{
Result<JobHandle> OnProcessing();
}
internal interface IUploadableAssetEntry : IAssetEntry
{
Result OnRecordUploadCommands(ResourceStreamingContext context);
void OnUploadComplete(ResourceStreamingContext context);
} }

View File

@@ -53,12 +53,24 @@ internal struct LoadAssetJob : IJob
} }
using var stream = openResult.Value; using var stream = openResult.Value;
var result = entry.OnLoadContent(stream);
if (result.IsFailure) if (entry is ILoadableAssetEntry loadable)
{ {
entry.State = AssetState.Failed; var result = loadable.OnLoadContent(stream);
Logger.Error($"Failed to load asset {assetID}: {result.Message}");
return; if (result.IsFailure)
{
entry.State = AssetState.Failed;
Logger.Error($"Failed to load asset {assetID}: {result.Message}");
return;
}
}
entry.State = AssetState.Loaded;
if (!assetManager.StreamingProcessor.EnqueueForProcess(entry))
{
// This mean the asset don't need any further processing anymore.
entry.State = AssetState.Ready;
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -67,13 +79,6 @@ internal struct LoadAssetJob : IJob
Logger.Error($"Failed to load asset {assetID}: {ex.Message}"); Logger.Error($"Failed to load asset {assetID}: {ex.Message}");
return; return;
} }
entry.State = AssetState.Loaded;
if (!assetManager.StreamingProcessor.EnqueueForProcess(entry))
{
// This mean the asset don't need any further processing anymore.
entry.State = AssetState.Ready;
}
} }
} }
@@ -113,6 +118,24 @@ public partial class AssetManager : IDisposable
return _entries.TryRemove(guid, out var _); return _entries.TryRemove(guid, out var _);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private AssetEntry GetOrCreateEntry(Guid guid)
{
var entry = _entries.GetOrAdd(guid, static (id, self) =>
{
var type = self._contentProvider.GetAssetType(id);
var deps = self._contentProvider.GetDependencies(id);
var entry = AssetEntryFactory.CreateNewEntry(self, self._resourceDatabase, self._resourceManager, id, type, deps);
self.EnsureScheduled(entry);
return entry;
}, this);
entry.AddRef();
return entry;
}
private void EnsureScheduled(AssetEntry entry) private void EnsureScheduled(AssetEntry entry)
{ {
var previousState = Interlocked.CompareExchange(ref entry.StateValue, (int)AssetState.Scheduled, (int)AssetState.Unloaded); var previousState = Interlocked.CompareExchange(ref entry.StateValue, (int)AssetState.Scheduled, (int)AssetState.Unloaded);
@@ -188,24 +211,6 @@ public partial class AssetManager : IDisposable
entry.SetLoadJobHandle(handle); // Use low priority to avoid blocking main thread critical tasks like rendering and physics. entry.SetLoadJobHandle(handle); // Use low priority to avoid blocking main thread critical tasks like rendering and physics.
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private AssetEntry GetOrCreateEntry(Guid guid)
{
var entry = _entries.GetOrAdd(guid, static (id, self) =>
{
var type = self._contentProvider.GetAssetType(id);
var deps = self._contentProvider.GetDependencies(id);
var entry = AssetEntryFactory.CreateNewEntry(self, self._resourceDatabase, self._resourceManager, id, type, deps);
self.EnsureScheduled(entry);
return entry;
}, this);
entry.AddRef();
return entry;
}
internal void ReimportAsset(Guid guid) internal void ReimportAsset(Guid guid)
{ {
if (!_entries.TryGetValue(guid, out var entry)) if (!_entries.TryGetValue(guid, out var entry))
@@ -228,6 +233,19 @@ public partial class AssetManager : IDisposable
} }
} }
//public IResolveOperation ResolveAsset(Guid assetID)
//{
// if (assetID == Guid.Empty)
// {
// return null;
// }
// var entry = GetOrCreateEntry(assetID);
// entry.OnResolve();
// return new IResolveOperation;
//}
public void Dispose() public void Dispose()
{ {
foreach (var entry in _entries.Values) foreach (var entry in _entries.Values)

View File

@@ -86,7 +86,7 @@ public partial class AssetManager
} }
} }
internal unsafe class MeshAssetEntry : UploadableAssetEntry internal unsafe class MeshAssetEntry : AssetEntry, ILoadableAssetEntry, IUploadableAssetEntry
{ {
private Handle<Mesh> _actualHandle; private Handle<Mesh> _actualHandle;
private Handle<Mesh> _tempHandle; private Handle<Mesh> _tempHandle;
@@ -121,7 +121,12 @@ internal unsafe class MeshAssetEntry : UploadableAssetEntry
_actualHandle = resourceManager.RegisterMesh(ref mesh); _actualHandle = resourceManager.RegisterMesh(ref mesh);
} }
public override Result OnLoadContent(Stream contentStream) public override void OnReleaseResource()
{
ResourceManager.ReleaseMesh(_tempHandle);
}
public Result OnLoadContent(Stream contentStream)
{ {
bool ValidateRange(ulong offset, int count, uint stride) bool ValidateRange(ulong offset, int count, uint stride)
{ {
@@ -159,7 +164,7 @@ internal unsafe class MeshAssetEntry : UploadableAssetEntry
return Result.Failure("Mesh content contains an invalid material part range."); return Result.Failure("Mesh content contains an invalid material part range.");
} }
contentStream.Seek(0, SeekOrigin.Begin); contentStream.Position = 0;
_rawData = contentStream.ReadMemory(AllocationHandle.Persistent); _rawData = contentStream.ReadMemory(AllocationHandle.Persistent);
var pData = (byte*)_rawData.GetUnsafePtr(); var pData = (byte*)_rawData.GetUnsafePtr();
@@ -176,11 +181,6 @@ internal unsafe class MeshAssetEntry : UploadableAssetEntry
return Result.Success(); return Result.Success();
} }
public override void OnReleaseResource()
{
ResourceManager.ReleaseMesh(_tempHandle);
}
private static Handle<GPUBuffer> CreateBuffer(ResourceStreamingContext context, void* pData, int count, uint stride, BufferUsage usage, string name) private static Handle<GPUBuffer> CreateBuffer(ResourceStreamingContext context, void* pData, int count, uint stride, BufferUsage usage, string name)
{ {
var desc = new BufferDesc var desc = new BufferDesc
@@ -202,7 +202,7 @@ internal unsafe class MeshAssetEntry : UploadableAssetEntry
name); name);
} }
public override Result OnRecordUploadCommands(ResourceStreamingContext context) public Result OnRecordUploadCommands(ResourceStreamingContext context)
{ {
var vertexBuffer = CreateBuffer(context, _pVertices, _header.vertexCount, (uint)sizeof(Vertex), var vertexBuffer = CreateBuffer(context, _pVertices, _header.vertexCount, (uint)sizeof(Vertex),
BufferUsage.Vertex | BufferUsage.ShaderResource | BufferUsage.Raw, "Mesh_VertexBuffer"); BufferUsage.Vertex | BufferUsage.ShaderResource | BufferUsage.Raw, "Mesh_VertexBuffer");
@@ -298,7 +298,7 @@ internal unsafe class MeshAssetEntry : UploadableAssetEntry
return Result.Success(); return Result.Success();
} }
public override void OnUploadComplete(ResourceStreamingContext context) public void OnUploadComplete(ResourceStreamingContext context)
{ {
var (dstMeshRef, dstError) = context.ResourceManager.GetMeshReference(_actualHandle); var (dstMeshRef, dstError) = context.ResourceManager.GetMeshReference(_actualHandle);
var (srcMeshRef, srcError) = context.ResourceManager.GetMeshReference(_tempHandle); var (srcMeshRef, srcError) = context.ResourceManager.GetMeshReference(_tempHandle);

View File

@@ -11,28 +11,28 @@ internal class ResourceStreamingProcessor : IResourceStreamingProcessor
{ {
private const int MAX_UPLOADS_PER_FRAME = 8; private const int MAX_UPLOADS_PER_FRAME = 8;
private readonly ConcurrentQueue<ProcessableAssetEntry> _pendingProcess; private readonly ConcurrentQueue<IProcessableAssetEntry> _pendingProcess;
private readonly ConcurrentQueue<UploadableAssetEntry> _pendingUpload; private readonly ConcurrentQueue<IUploadableAssetEntry> _pendingUpload;
private readonly ConcurrentQueue<UploadableAssetEntry> _pendingFinalize; private readonly ConcurrentQueue<IUploadableAssetEntry> _pendingFinalize;
private ulong _pendingCopyFenceValue; private ulong _pendingCopyFenceValue;
public ResourceStreamingProcessor() public ResourceStreamingProcessor()
{ {
_pendingProcess = new ConcurrentQueue<ProcessableAssetEntry>(); _pendingProcess = new ConcurrentQueue<IProcessableAssetEntry>();
_pendingUpload = new ConcurrentQueue<UploadableAssetEntry>(); _pendingUpload = new ConcurrentQueue<IUploadableAssetEntry>();
_pendingFinalize = new ConcurrentQueue<UploadableAssetEntry>(); _pendingFinalize = new ConcurrentQueue<IUploadableAssetEntry>();
_pendingCopyFenceValue = 0; _pendingCopyFenceValue = 0;
} }
public bool EnqueueForProcess(AssetEntry entry) public bool EnqueueForProcess(AssetEntry entry)
{ {
if (entry is UploadableAssetEntry uploadable) if (entry is IUploadableAssetEntry uploadable)
{ {
_pendingUpload.Enqueue(uploadable); _pendingUpload.Enqueue(uploadable);
return true; return true;
} }
else if (entry is ProcessableAssetEntry processable) else if (entry is IProcessableAssetEntry processable)
{ {
_pendingProcess.Enqueue(processable); _pendingProcess.Enqueue(processable);
return true; return true;
@@ -48,7 +48,7 @@ internal class ResourceStreamingProcessor : IResourceStreamingProcessor
while (_pendingProcess.TryDequeue(out var entry)) while (_pendingProcess.TryDequeue(out var entry))
{ {
var result = entry.OnProcessing(context); var result = entry.OnProcessing();
if (result.IsFailure) if (result.IsFailure)
{ {
Logger.Error(result.Message); Logger.Error(result.Message);
@@ -74,12 +74,7 @@ internal class ResourceStreamingProcessor : IResourceStreamingProcessor
{ {
while (_pendingFinalize.TryDequeue(out var item)) while (_pendingFinalize.TryDequeue(out var item))
{ {
Volatile.Write(ref item.StateValue, (int)AssetState.Ready); item.State = AssetState.Ready;
if (Interlocked.CompareExchange(ref item.PendingReimport, false, true))
{
item.AssetManager.ReimportAsset(item.AssetId); // re-queue
}
item.OnUploadComplete(context); item.OnUploadComplete(context);
} }

View File

@@ -22,10 +22,40 @@ internal struct SceneContentHeader
public int entityCount; public int entityCount;
} }
// TODO: We should have a dedicated scene loading service. Maybe we should make our SceneManager as a service.
public partial class AssetManager public partial class AssetManager
{ {
public Result<JobHandle> LoadScene(World world, AssetRef<Scene> sceneAsset, SceneLoadingType loadingType) private struct LoadSceneJob : IJob
{
public SceneContentHeader header;
public Stream stream;
public LoadedSceneData loadedSceneData;
public readonly void Execute(ref readonly JobExecutionContext context)
{
try
{
var loadResult = SceneManager.ParseSceneData(header, stream, AllocationHandle.Persistent);
if (loadResult.IsFailure)
{
Logger.Error($"Failed to parse scene data: {loadResult.Message}");
return;
}
loadedSceneData.entities = loadResult.Value.entities;
}
catch (Exception ex)
{
Logger.Error($"Exception while loading scene: {ex}");
}
finally
{
stream.Dispose();
}
}
}
public unsafe Result<JobHandle> LoadScene(World world, AssetRef<Scene> sceneAsset, SceneLoadingType loadingType, ref LoadedSceneData? loadedSceneData)
{ {
if (!sceneAsset.IsValid) if (!sceneAsset.IsValid)
{ {
@@ -38,32 +68,50 @@ public partial class AssetManager
return Result.Failure($"Failed to open scene {sceneAsset.ID}: {openResult.Message}."); return Result.Failure($"Failed to open scene {sceneAsset.ID}: {openResult.Message}.");
} }
var stream = openResult.Value;
if (stream.Length < sizeof(SceneContentHeader))
{
stream.Dispose();
return Result.Failure("Invalid scene file size.");
}
var header = stream.Read<SceneContentHeader>();
if (header.magic != SceneContentHeader.MAGIC)
{
stream.Dispose();
return Result.Failure("Unexpected header format.");
}
if (header.version != SceneContentHeader.VERSION)
{
stream.Dispose();
return Result.Failure($"Not supported scene header version {header.version}.");
}
try try
{ {
using var stream = openResult.Value;
var header = stream.Read<SceneContentHeader>();
if (header.magic != SceneContentHeader.MAGIC)
{
return Result.Failure("Unexpected header format.");
}
if (header.version != SceneContentHeader.VERSION)
{
return Result.Failure($"Not supported scene header version {header.version}.");
}
if (loadingType == SceneLoadingType.Single) if (loadingType == SceneLoadingType.Single)
{ {
world.Reset(); world.Reset();
} }
var loadResult = SceneManager.ParseSceneData(header, stream, AllocationHandle.Persistent); loadedSceneData ??= new LoadedSceneData();
return JobHandle.Invalid; var entry = GetOrCreateEntry(sceneAsset.ID); // Purely to get the dependencies and ensure the asset is tracked, the actual loading is done in the job.
var job = new LoadSceneJob
{
header = header,
stream = stream,
loadedSceneData = loadedSceneData
};
return _jobScheduler.Schedule(in job, entry.LoadJobHandle);
} }
catch (Exception ex) catch (Exception ex)
{ {
stream.Dispose();
return Result.Failure(ex.Message); return Result.Failure(ex.Message);
} }
} }
@@ -73,29 +121,6 @@ internal class SceneAssetEntry : AssetEntry
{ {
public SceneAssetEntry(AssetManager manager, IResourceDatabase resourceDatabase, ResourceManager resourceManager, Guid assetId, Guid[] dependencies) public SceneAssetEntry(AssetManager manager, IResourceDatabase resourceDatabase, ResourceManager resourceManager, Guid assetId, Guid[] dependencies)
: base(manager, resourceDatabase, resourceManager, assetId, AssetType.Scene, dependencies) : base(manager, resourceDatabase, resourceManager, assetId, AssetType.Scene, dependencies)
{
// TODO: How can I get this? Ideally the public api will be something like SceneManager.LoadScene(World, Scene, SceneLoadingType).
// Should we handle the scene loading explicitly instead of auto loading on the first resolve?
// For example if we have a component called SceneStreamer{ SceneID a; SceneID b; }
// In save data, we convert the SceneID(ushort) to a asset gui, and convert it back during load. So at ResolveScene stage (before the file even been loaded), we need to call the SceneManager.CreateScene() and return the id immediately.
// Currently we store the world and loading type directly inside the asset entry, but actually that should not be bound with the asset itself, because we may load scene A along at the first time, then we load it additively at the second time.
// So, maybe the scene asset entry should only create a unique id from SceneManager.CreateScene() then resolve the scene file without loading it into world.
// Then we can load the scene into world using our job system, and user can decide to wait it immediatly (sync) or fire-and-forget (async).
// The workflow may be this:
// 1. Startup scene load, during resolve, see SceneStreamer has two SceneID fields (which still contains guid now), resolve this two scene via AssetManager. Get the id of those two scene immediately.
// 2. Background job load the scene into memory, parse the raw memory into the format that runtime understand. (Or maybe we do not load full memory, just check the header to see if it's valid?
// If the streamer type has 20 scenes, loading all 20 scenes into memory is very huge.).
// 3. The streamer called SceneManager.LoadScene(World, SceneID, SceneLoadingType) (example api, may not be this exactly). (Mybe we load the data into memory here every time when LoadScene is
// called? It will be fine right since load scene itself is a heavy opeartion and it's not am opeartion that will be performed per frame)
// 4. Background job load the scene into world by creating entities and setup components for those entities.
}
public override Result OnLoadContent(Stream contentStream)
{
return Result.Success();
}
public override void OnReleaseResource()
{ {
} }
} }

View File

@@ -57,7 +57,7 @@ public partial class AssetManager
} }
} }
internal unsafe class TextureAssetEntry : UploadableAssetEntry internal unsafe class TextureAssetEntry : AssetEntry, ILoadableAssetEntry, IUploadableAssetEntry
{ {
private Handle<GPUTexture> _actualHandle; private Handle<GPUTexture> _actualHandle;
private Handle<GPUTexture> _tempHandle; private Handle<GPUTexture> _tempHandle;
@@ -102,7 +102,12 @@ internal unsafe class TextureAssetEntry : UploadableAssetEntry
}; };
} }
public override Result OnLoadContent(Stream contentStream) public override void OnReleaseResource()
{
ResourceDatabase.ReleaseResource(_tempHandle.AsResource());
}
public Result OnLoadContent(Stream contentStream)
{ {
var header = contentStream.Read<TextureContentHeader>(); var header = contentStream.Read<TextureContentHeader>();
@@ -133,13 +138,7 @@ internal unsafe class TextureAssetEntry : UploadableAssetEntry
return Result.Success(); return Result.Success();
} }
public override void OnReleaseResource() public Result OnRecordUploadCommands(ResourceStreamingContext context)
{
ResourceDatabase.ReleaseResource(_tempHandle.AsResource());
}
public override Result OnRecordUploadCommands(ResourceStreamingContext context)
{ {
Logger.DebugAssert(_textureData.IsCreated); Logger.DebugAssert(_textureData.IsCreated);
@@ -161,7 +160,7 @@ internal unsafe class TextureAssetEntry : UploadableAssetEntry
return Result.Success(); return Result.Success();
} }
public override void OnUploadComplete(ResourceStreamingContext context) public void OnUploadComplete(ResourceStreamingContext context)
{ {
var actualHandle = context.ResourceDatabase.Replace(_actualHandle.AsResource(), _tempHandle.AsResource()); var actualHandle = context.ResourceDatabase.Replace(_actualHandle.AsResource(), _tempHandle.AsResource());
Logger.DebugAssert(actualHandle.IsValid); Logger.DebugAssert(actualHandle.IsValid);

View File

@@ -188,11 +188,11 @@ internal unsafe struct Archetype : IDisposable
} }
} }
private struct Edge //private struct Edge
{ //{
public int componentID; // public int componentID;
public int targetArchetype; // can't use Identifier<Archetype> because cycle causer // public int targetArchetype; // can't use Identifier<Archetype> because cycle causer
} //}
internal UnsafeBitSet _signature; internal UnsafeBitSet _signature;
internal UnsafeList<Chunk> _chunks; internal UnsafeList<Chunk> _chunks;
@@ -202,8 +202,8 @@ internal unsafe struct Archetype : IDisposable
internal UnsafeArray<SharedComponentLayout> _sharedLayouts; internal UnsafeArray<SharedComponentLayout> _sharedLayouts;
internal UnsafeList<ChunkGroup> _chunkGroups; internal UnsafeList<ChunkGroup> _chunkGroups;
private UnsafeList<Edge> _edgesAdd; private UnsafeHashMap<int, int> _edgesAdd;
private UnsafeList<Edge> _edgesRemove; private UnsafeHashMap<int, int> _edgesRemove;
// 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. // 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; internal int _cleanupEdge;
@@ -230,8 +230,8 @@ internal unsafe struct Archetype : IDisposable
_worldID = worldID; _worldID = worldID;
_chunks = new UnsafeList<Chunk>(4, AllocationHandle.Persistent); _chunks = new UnsafeList<Chunk>(4, AllocationHandle.Persistent);
_edgesAdd = new UnsafeList<Edge>(4, AllocationHandle.Persistent); _edgesAdd = new UnsafeHashMap<int, int>(4, AllocationHandle.Persistent);
_edgesRemove = new UnsafeList<Edge>(4, AllocationHandle.Persistent); _edgesRemove = new UnsafeHashMap<int, int>(4, AllocationHandle.Persistent);
if (componentIds.IsEmpty) if (componentIds.IsEmpty)
{ {
@@ -868,51 +868,25 @@ internal unsafe struct Archetype : IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEdgeAdd(Identifier<IComponent> componentID, Identifier<Archetype> targetArchetype) public void AddEdgeAdd(Identifier<IComponent> componentID, Identifier<Archetype> targetArchetype)
{ {
_edgesAdd.Add(new Edge _edgesAdd.TryAdd(componentID, targetArchetype);
{
componentID = componentID,
targetArchetype = targetArchetype
});
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Identifier<Archetype> GetEdgeAdd(Identifier<IComponent> componentID) public readonly Identifier<Archetype> GetEdgeAdd(Identifier<IComponent> componentID)
{ {
for (var i = 0; i < _edgesAdd.Count; i++) return _edgesAdd.GetValueOrDefault(componentID, -1);
{
var edge = _edgesAdd[i];
if (edge.componentID == componentID)
{
return edge.targetArchetype;
}
}
return Identifier<Archetype>.Invalid;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEdgeRemove(Identifier<IComponent> componentID, Identifier<Archetype> targetArchetype) public void AddEdgeRemove(Identifier<IComponent> componentID, Identifier<Archetype> targetArchetype)
{ {
_edgesRemove.Add(new Edge _edgesRemove.TryAdd(componentID, targetArchetype);
{
componentID = componentID,
targetArchetype = targetArchetype
});
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Identifier<Archetype> GetEdgeRemove(Identifier<IComponent> componentID) public readonly Identifier<Archetype> GetEdgeRemove(Identifier<IComponent> componentID)
{ {
for (var i = 0; i < _edgesRemove.Count; i++) return _edgesRemove.GetValueOrDefault(componentID, -1);
{
var edge = _edgesRemove[i];
if (edge.componentID == componentID)
{
return edge.targetArchetype;
}
}
return Identifier<Archetype>.Invalid;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -9,8 +9,9 @@ using System.Runtime.CompilerServices;
namespace Ghost.Entities; namespace Ghost.Entities;
public interface IComponent; public interface IComponent;
public interface IEnableableComponent : IComponent; public interface IComponentData : IComponent;
public interface ICleanupComponent : IComponent; public interface IEnableableComponent : IComponentData;
public interface ICleanupComponent : IComponentData;
public interface ISharedComponent : IComponent; public interface ISharedComponent : IComponent;
[AttributeUsage(AttributeTargets.Struct)] [AttributeUsage(AttributeTargets.Struct)]

View File

@@ -63,7 +63,7 @@ public unsafe struct EntityCommandBuffer : IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddComponent<T>(Entity entity, T component = default) public void AddComponent<T>(Entity entity, T component = default)
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
_writer.Write(ECBOpCode.AddComponent); _writer.Write(ECBOpCode.AddComponent);
_writer.Write(entity); _writer.Write(entity);
@@ -73,7 +73,7 @@ public unsafe struct EntityCommandBuffer : IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveComponent<T>(Entity entity) public void RemoveComponent<T>(Entity entity)
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
_writer.Write(ECBOpCode.RemoveComponent); _writer.Write(ECBOpCode.RemoveComponent);
_writer.Write(entity); _writer.Write(entity);
@@ -82,7 +82,7 @@ public unsafe struct EntityCommandBuffer : IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetComponent<T>(Entity entity, T component) public void SetComponent<T>(Entity entity, T component)
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
_writer.Write(ECBOpCode.SetComponent); _writer.Write(ECBOpCode.SetComponent);
_writer.Write(entity); _writer.Write(entity);

View File

@@ -566,7 +566,7 @@ public unsafe partial class EntityManager : IDisposable
/// <returns>The result status of the operation.</returns> /// <returns>The result status of the operation.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Error CreateSingleton<T>(T component = default) public Error CreateSingleton<T>(T component = default)
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
return CreateSingleton(ComponentTypeID<T>.Value, &component); return CreateSingleton(ComponentTypeID<T>.Value, &component);
} }
@@ -606,7 +606,7 @@ public unsafe partial class EntityManager : IDisposable
/// <returns>Reference to the component data. null ref if not found.</returns> /// <returns>Reference to the component data. null ref if not found.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T GetSingleton<T>() public ref T GetSingleton<T>()
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
var ptr = GetSingleton(ComponentTypeID<T>.Value); var ptr = GetSingleton(ComponentTypeID<T>.Value);
return ref *(T*)ptr; // This will return null ref if ptr is null. return ref *(T*)ptr; // This will return null ref if ptr is null.
@@ -761,7 +761,7 @@ public unsafe partial class EntityManager : IDisposable
/// <returns>The result status of the operation.</returns> /// <returns>The result status of the operation.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Error AddComponent<T>(Entity entity, T component = default) public Error AddComponent<T>(Entity entity, T component = default)
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
return AddComponent(entity, ComponentTypeID<T>.Value, &component); return AddComponent(entity, ComponentTypeID<T>.Value, &component);
} }
@@ -874,7 +874,7 @@ public unsafe partial class EntityManager : IDisposable
/// <returns>The result status of the operation.</returns> /// <returns>The result status of the operation.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Error RemoveComponent<T>(Entity entity) public Error RemoveComponent<T>(Entity entity)
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
return RemoveComponent(entity, ComponentTypeID<T>.Value); return RemoveComponent(entity, ComponentTypeID<T>.Value);
} }
@@ -908,7 +908,7 @@ public unsafe partial class EntityManager : IDisposable
/// <param name="component">The component data.</param> /// <param name="component">The component data.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Error SetComponent<T>(Entity entity, T component) public Error SetComponent<T>(Entity entity, T component)
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
return SetComponent(entity, ComponentTypeID<T>.Value, &component); return SetComponent(entity, ComponentTypeID<T>.Value, &component);
} }
@@ -939,7 +939,7 @@ public unsafe partial class EntityManager : IDisposable
/// <returns>Reference to the component data. null ref if not found.</returns> /// <returns>Reference to the component data. null ref if not found.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T GetComponent<T>(Entity entity) public ref T GetComponent<T>(Entity entity)
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
var ptr = GetComponent(entity, ComponentTypeID<T>.Value); var ptr = GetComponent(entity, ComponentTypeID<T>.Value);
return ref *(T*)ptr; // This will return null ref if ptr is null. return ref *(T*)ptr; // This will return null ref if ptr is null.

View File

@@ -8,7 +8,7 @@ namespace Ghost.Entities;
public interface IManagedComponent; public interface IManagedComponent;
public interface IManagedWrapper; public interface IManagedWrapper;
public readonly struct Managed<T> : IComponent, IManagedWrapper public readonly struct Managed<T> : IComponentData, IManagedWrapper
where T : IManagedComponent where T : IManagedComponent
{ {
public readonly int id; public readonly int id;
@@ -38,7 +38,7 @@ internal static class ManagedComponentRegistry
private static readonly List<ManagedComponentInfo> s_registeredComponents = new(); private static readonly List<ManagedComponentInfo> s_registeredComponents = new();
private static readonly Dictionary<IntPtr, int> s_typeHandleToID = new(); private static readonly Dictionary<IntPtr, int> s_typeHandleToID = new();
private static readonly Dictionary<string, int> s_nameToRuntimeID = new(); private static readonly Dictionary<string, int> s_nameToRuntimeID = new();
#if GHOST_SAFETY_CHECKS #if DEBUG || GHOST_EDITOR
internal static readonly Dictionary<int, Type> s_runtimeIDToType = new(); internal static readonly Dictionary<int, Type> s_runtimeIDToType = new();
#endif #endif
@@ -66,7 +66,7 @@ internal static class ManagedComponentRegistry
s_typeHandleToID[typeHandle] = newID; s_typeHandleToID[typeHandle] = newID;
s_nameToRuntimeID[stableName] = newID; s_nameToRuntimeID[stableName] = newID;
#if GHOST_SAFETY_CHECKS #if DEBUG || GHOST_EDITOR
s_runtimeIDToType[newID.Value] = typeof(T); s_runtimeIDToType[newID.Value] = typeof(T);
#endif #endif
@@ -137,7 +137,7 @@ public abstract class ScriptComponent : IManagedComponent
public Entity Entity => _entity; public Entity Entity => _entity;
protected ref T GetComponent<T>() protected ref T GetComponent<T>()
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
return ref _world.EntityManager.GetComponent<T>(_entity); return ref _world.EntityManager.GetComponent<T>(_entity);
} }
@@ -177,7 +177,7 @@ public abstract class ScriptComponent : IManagedComponent
public partial class EntityManager public partial class EntityManager
{ {
private IManagedComponentStorage[] _managedStorages; private IManagedComponentStorage[] _managedStorages = new IManagedComponentStorage[64];
internal IManagedComponentStorage[] ManagedStorages => _managedStorages; internal IManagedComponentStorage[] ManagedStorages => _managedStorages;

View File

@@ -29,13 +29,40 @@ internal struct EntityQueryMask : IDisposable, IEquatable<EntityQueryMask>
public override readonly int GetHashCode() public override readonly int GetHashCode()
{ {
var hash = 17; var hash = 17;
if (structuralAll.IsCreated) hash = hash * 23 + structuralAll.GetHashCode(); if (structuralAll.IsCreated)
if (structuralAbsent.IsCreated) hash = hash * 23 + structuralAbsent.GetHashCode(); {
if (structuralAny.IsCreated) hash = hash * 23 + structuralAny.GetHashCode(); hash = hash * 23 + structuralAll.GetHashCode();
if (requireEnabled.IsCreated) hash = hash * 23 + requireEnabled.GetHashCode(); }
if (requireDisabled.IsCreated) hash = hash * 23 + requireDisabled.GetHashCode();
if (rejectIfEnabled.IsCreated) hash = hash * 23 + rejectIfEnabled.GetHashCode(); if (structuralAbsent.IsCreated)
if (writeAccess.IsCreated) hash = hash * 23 + writeAccess.GetHashCode(); {
hash = hash * 23 + structuralAbsent.GetHashCode();
}
if (structuralAny.IsCreated)
{
hash = hash * 23 + structuralAny.GetHashCode();
}
if (requireEnabled.IsCreated)
{
hash = hash * 23 + requireEnabled.GetHashCode();
}
if (requireDisabled.IsCreated)
{
hash = hash * 23 + requireDisabled.GetHashCode();
}
if (rejectIfEnabled.IsCreated)
{
hash = hash * 23 + rejectIfEnabled.GetHashCode();
}
if (writeAccess.IsCreated)
{
hash = hash * 23 + writeAccess.GetHashCode();
}
return hash; return hash;
} }
@@ -153,7 +180,7 @@ public readonly unsafe ref struct ChunkView
/// <returns>true if the component of space T has changed since the specified version; otherwise, false.</returns> /// <returns>true if the component of space T has changed since the specified version; otherwise, false.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly bool HasChanged<T>(uint version) public readonly bool HasChanged<T>(uint version)
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
var layout = GetLayout(ComponentTypeID<T>.Value); var layout = GetLayout(ComponentTypeID<T>.Value);
return version < _pVersion[layout.versionIndex]; return version < _pVersion[layout.versionIndex];
@@ -188,7 +215,7 @@ public readonly unsafe ref struct ChunkView
/// <returns>The version number of the component space <typeparamref name="T"/>.</returns> /// <returns>The version number of the component space <typeparamref name="T"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly uint GetComponentVersion<T>() public readonly uint GetComponentVersion<T>()
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
return _pVersion[ComponentTypeID<T>.Value]; return _pVersion[ComponentTypeID<T>.Value];
} }
@@ -212,7 +239,7 @@ public readonly unsafe ref struct ChunkView
/// <exception cref="InvalidOperationException">Thrown if the specified component space is not present in the archetype.</exception> /// <exception cref="InvalidOperationException">Thrown if the specified component space is not present in the archetype.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<T> GetComponentData<T>() public ReadOnlySpan<T> GetComponentData<T>()
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
var layout = GetLayout(ComponentTypeID<T>.Value); var layout = GetLayout(ComponentTypeID<T>.Value);
var pComponentData = _pChunkData + layout.offset; var pComponentData = _pChunkData + layout.offset;
@@ -227,7 +254,7 @@ public readonly unsafe ref struct ChunkView
/// <exception cref="InvalidOperationException">Thrown if the specified component space is not present in the archetype.</exception> /// <exception cref="InvalidOperationException">Thrown if the specified component space is not present in the archetype.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<T> GetComponentDataRW<T>() public Span<T> GetComponentDataRW<T>()
where T : unmanaged, IComponent where T : unmanaged, IComponentData
{ {
var compId = ComponentTypeID<T>.Value; var compId = ComponentTypeID<T>.Value;
var layout = GetLayout(compId); var layout = GetLayout(compId);
@@ -289,7 +316,7 @@ public readonly unsafe ref struct ChunkView
var compID = ComponentTypeID<T>.Value; var compID = ComponentTypeID<T>.Value;
var layoutIndex = -1; var layoutIndex = -1;
for (int i = 0; i < _sharedLayouts.Length; i++) for (var i = 0; i < _sharedLayouts.Length; i++)
{ {
if (_sharedLayouts[i].componentID == compID.Value) if (_sharedLayouts[i].componentID == compID.Value)
{ {
@@ -467,6 +494,43 @@ public unsafe partial struct EntityQuery : IDisposable
return true; return true;
} }
private static bool RequiresEnableableFiltering(ref readonly Archetype archetype, ref readonly EntityQueryMask mask)
{
// If the query asks to filter by enabled/disabled state AND the archetype
// actually has that component marked as enableable (enableBitsOffset != -1), we must filter.
var it = mask.requireEnabled.GetIterator();
while (it.Next(out var id))
{
var layoutResult = archetype.GetLayout(id);
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
{
return true;
}
}
it = mask.requireDisabled.GetIterator();
while (it.Next(out var id))
{
var layoutResult = archetype.GetLayout(id);
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
{
return true;
}
}
it = mask.rejectIfEnabled.GetIterator();
while (it.Next(out var id))
{
var layoutResult = archetype.GetLayout(id);
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
{
return true;
}
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool CheckBit(byte* maskBase, int index) internal static bool CheckBit(byte* maskBase, int index)
{ {
@@ -513,15 +577,31 @@ public unsafe partial struct EntityQuery : IDisposable
{ {
var archetypeID = _matchingArchetypes[i]; var archetypeID = _matchingArchetypes[i];
ref var archetype = ref world.ComponentManager.GetArchetypeReference(archetypeID); ref var archetype = ref world.ComponentManager.GetArchetypeReference(archetypeID);
var requiresFiltering = RequiresEnableableFiltering(in archetype, in _mask);
for (var j = 0; j < archetype.ChunkCount; j++) for (var j = 0; j < archetype.ChunkCount; j++)
{ {
ref var chunk = ref archetype.GetChunkReference(j); ref var chunk = ref archetype.GetChunkReference(j);
if (chunk._count == 0)
for (var k = 0; k < chunk._count; k++)
{ {
if (IsEntityValid(chunk.GetUnsafePtr(), k, in archetype, in _mask)) continue;
}
if (!requiresFiltering)
{
// Fast path: all entities match
total += chunk._count;
}
else
{
// Slow path: check individual bits
for (var k = 0; k < chunk._count; k++)
{ {
total++; if (IsEntityValid(chunk.GetUnsafePtr(), k, in archetype, in _mask))
{
total++;
}
} }
} }
} }
@@ -530,6 +610,49 @@ public unsafe partial struct EntityQuery : IDisposable
return total; return total;
} }
public readonly bool HasMatchingEntity()
{
var world = World.GetWorld(_worldID);
if (world is null)
{
return false;
}
for (var i = 0; i < _matchingArchetypes.Count; i++)
{
var archetypeID = _matchingArchetypes[i];
ref var archetype = ref world.ComponentManager.GetArchetypeReference(archetypeID);
// Check ONCE per archetype if we actually need to loop entities
var requiresFiltering = RequiresEnableableFiltering(in archetype, in _mask);
for (var j = 0; j < archetype.ChunkCount; j++)
{
ref var chunk = ref archetype.GetChunkReference(j);
if (chunk._count == 0)
{
continue;
}
if (!requiresFiltering)
{
// No enablement constraints? Any entity in this chunk is a match!
return true;
}
for (var k = 0; k < chunk._count; k++)
{
if (IsEntityValid(chunk.GetUnsafePtr(), k, in archetype, in _mask))
{
return true;
}
}
}
}
return false;
}
public void Dispose() public void Dispose()
{ {
_mask.Dispose(); _mask.Dispose();

View File

@@ -65,7 +65,7 @@ public abstract class SystemBase : ISystem
{ {
var queryID = _requiredQueries[i]; var queryID = _requiredQueries[i];
ref var query = ref World.ComponentManager.GetEntityQueryReference(new Identifier<EntityQuery>(queryID)); ref var query = ref World.ComponentManager.GetEntityQueryReference(new Identifier<EntityQuery>(queryID));
if (query.CalculateEntityCount() == 0) if (!query.HasMatchingEntity())
{ {
return false; return false;
} }

View File

@@ -15,15 +15,15 @@ namespace Ghost.Graphics.D3D12;
internal sealed unsafe partial class D3D12ResourceAllocator internal sealed unsafe partial class D3D12ResourceAllocator
{ {
// NOTE: _MAX_BYTES may not be accurate, we need to verify it with feature level checks. // NOTE: MAX_BYTES may not be accurate, we need to verify it with feature level checks.
private const uint _MAX_BYTES = D3D12_REQ_RESOURCE_SIZE_IN_MEGABYTES_EXPRESSION_A_TERM * 1024u * 1024u; private const uint MAX_BYTES = D3D12_REQ_RESOURCE_SIZE_IN_MEGABYTES_EXPRESSION_A_TERM * 1024u * 1024u;
private const uint _MAX_TEXTURE2D_DIMENSION = 16384u; private const uint MAX_TEXTURE2D_DIMENSION = 16384u;
private const uint _MAX_TEXTURE3D_DIMENSION = 2048u; private const uint MAX_TEXTURE3D_DIMENSION = 2048u;
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CheckBufferSize(ulong sizeInBytes) private static void CheckBufferSize(ulong sizeInBytes)
{ {
if (sizeInBytes > _MAX_BYTES) if (sizeInBytes > MAX_BYTES)
{ {
throw new InvalidOperationException($"ERROR: Resource size too large for DirectX 12 (size {sizeInBytes})"); throw new InvalidOperationException($"ERROR: Resource size too large for DirectX 12 (size {sizeInBytes})");
} }
@@ -32,7 +32,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CheckTexture2DSize(uint width, uint height) private static void CheckTexture2DSize(uint width, uint height)
{ {
if (width > _MAX_TEXTURE2D_DIMENSION || height > _MAX_TEXTURE2D_DIMENSION) if (width > MAX_TEXTURE2D_DIMENSION || height > MAX_TEXTURE2D_DIMENSION)
{ {
throw new InvalidOperationException($"ERROR: Texture size too large for DirectX 12 (width {width}, height {height})"); throw new InvalidOperationException($"ERROR: Texture size too large for DirectX 12 (width {width}, height {height})");
} }
@@ -41,8 +41,8 @@ internal sealed unsafe partial class D3D12ResourceAllocator
internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
{ {
private const uint _UPLOAD_BATCH_SIZE = 64 * 1024 * 1024; // 64 MB private const uint UPLOAD_BATCH_SIZE = 64 * 1024 * 1024; // 64 MB
private const uint _MAX_RESOURCE_SIZE_TO_FIT_IN_UPLOAD_BATCH = 16 * 1024 * 1024; // 16 MB private const uint MAX_RESOURCE_SIZE_TO_FIT_IN_UPLOAD_BATCH = 16 * 1024 * 1024; // 16 MB
private UniquePtr<D3D12MA_Allocator> _d3d12MA; private UniquePtr<D3D12MA_Allocator> _d3d12MA;

View File

@@ -2,10 +2,8 @@ using Ghost.Core;
using Ghost.Core.Graphics; using Ghost.Core.Graphics;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Drawing; using System.Drawing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ghost.Graphics.RHI; namespace Ghost.Graphics.RHI;

View File

@@ -2,6 +2,14 @@ using Ghost.Core;
namespace Ghost.Graphics.RHI; namespace Ghost.Graphics.RHI;
public unsafe struct ShaderByteCode
{
public byte* pCode;
public ulong size;
}
public unsafe delegate void ShaderVariantCompiledHandler(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, ReadOnlySpan<ShaderByteCode> byteCodes);
public interface IShaderCompilationBridge : IDisposable public interface IShaderCompilationBridge : IDisposable
{ {
/// <summary> /// <summary>
@@ -11,7 +19,13 @@ public interface IShaderCompilationBridge : IDisposable
void RequestCompilation(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, LocalKeywordSet keywordMask); void RequestCompilation(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, LocalKeywordSet keywordMask);
/// <summary> /// <summary>
/// Event triggered when a shader variant has been successfully compiled and updated. /// Event triggered when a shader variant has been successfully compiled.
/// </summary> /// </summary>
event Action<Key64<ShaderVariant>, ulong> OnShaderVariantCompiled; event ShaderVariantCompiledHandler OnShaderVariantCompiled;
/// <summary>
/// Event triggered when a shader source has been imported or modified, requiring cache invalidation.
/// </summary>
event Action<ulong> OnShaderInvalidated;
} }

View File

@@ -207,7 +207,7 @@ public class RenderSystem : IDisposable
_resourceManager = new ResourceManager(_graphicsEngine.Device, _graphicsEngine.ResourceAllocator, _graphicsEngine.ResourceDatabase); _resourceManager = new ResourceManager(_graphicsEngine.Device, _graphicsEngine.ResourceAllocator, _graphicsEngine.ResourceDatabase);
_swapChainManager = new SwapChainManager(_graphicsEngine); _swapChainManager = new SwapChainManager(_graphicsEngine);
_shaderLibrary = new ShaderLibrary(desc.ShaderCompilationBridge, desc.ShaderCacheDirectory); _shaderLibrary = new ShaderLibrary(desc.ShaderCompilationBridge, _graphicsEngine.PipelineLibrary, desc.ShaderCacheDirectory);
_asyncCopyPipeline = new AsyncCopyPipeline(_graphicsEngine); _asyncCopyPipeline = new AsyncCopyPipeline(_graphicsEngine);
_renderThread = new Thread(RenderLoop) _renderThread = new Thread(RenderLoop)

View File

@@ -9,12 +9,6 @@ using System.Runtime.CompilerServices;
namespace Ghost.Graphics.Services; namespace Ghost.Graphics.Services;
internal unsafe struct ShaderByteCode
{
public byte* pCode;
public ulong size;
}
internal struct ShaderCache : IDisposable internal struct ShaderCache : IDisposable
{ {
public MemoryBlock byteCode; public MemoryBlock byteCode;
@@ -73,24 +67,32 @@ internal unsafe class ShaderLibrary : IDisposable
private readonly string _cacheDirectory; private readonly string _cacheDirectory;
private readonly IShaderCompilationBridge? _shaderCompilationBridge; private readonly IShaderCompilationBridge? _shaderCompilationBridge;
private readonly IPipelineLibrary? _pipelineLibrary;
internal ShaderLibrary(IShaderCompilationBridge? shaderCompilationBridge, string cacheDirectory) internal ShaderLibrary(IShaderCompilationBridge? shaderCompilationBridge, IPipelineLibrary? pipelineLibrary, string cacheDirectory)
{ {
_inMemoryCache = new UnsafeHashMap<ulong, CacheEntry>(16, AllocationHandle.Persistent); _inMemoryCache = new UnsafeHashMap<ulong, CacheEntry>(16, AllocationHandle.Persistent);
_variantToCompiledHash = new UnsafeHashMap<ulong, ulong>(16, AllocationHandle.Persistent); _variantToCompiledHash = new UnsafeHashMap<ulong, ulong>(16, AllocationHandle.Persistent);
_cacheDirectory = cacheDirectory; _cacheDirectory = cacheDirectory;
_shaderCompilationBridge = shaderCompilationBridge; _shaderCompilationBridge = shaderCompilationBridge;
_pipelineLibrary = pipelineLibrary;
if (_shaderCompilationBridge != null) if (_shaderCompilationBridge != null)
{ {
_shaderCompilationBridge.OnShaderVariantCompiled += OnVariantCompiled; _shaderCompilationBridge.OnShaderVariantCompiled += OnVariantCompiled;
_shaderCompilationBridge.OnShaderInvalidated += OnShaderInvalidated;
} }
} }
private void OnVariantCompiled(Key64<ShaderVariant> variantKey, ulong newCompiledHash) private void OnVariantCompiled(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, ReadOnlySpan<ShaderByteCode> byteCodes)
{ {
_variantToCompiledHash[variantKey] = newCompiledHash; CacheCompiledResult(shaderId, passIndex, variantKey, byteCodes);
}
private void OnShaderInvalidated(ulong shaderId)
{
InvalidateShaderCache(shaderId);
} }
private string GetShaderCacheFilePath(ulong hash) private string GetShaderCacheFilePath(ulong hash)
@@ -203,15 +205,20 @@ internal unsafe class ShaderLibrary : IDisposable
return Error.NotFound; return Error.NotFound;
} }
public void InvalidateShaderCache(ulong id, IPipelineLibrary pipelineLibrary) public void InvalidateShaderCache(ulong id)
{ {
if (_pipelineLibrary == null)
{
return;
}
if (_inMemoryCache.TryGetValue(id, out var entry)) if (_inMemoryCache.TryGetValue(id, out var entry))
{ {
for (int i = 0; i < entry.cache.Length; i++) for (int i = 0; i < entry.cache.Length; i++)
{ {
if (entry.cache[i].compiledHash != 0) if (entry.cache[i].compiledHash != 0)
{ {
pipelineLibrary.EvictStalePipelines(entry.cache[i].compiledHash); _pipelineLibrary.EvictStalePipelines(entry.cache[i].compiledHash);
} }
} }
entry.Dispose(); entry.Dispose();
@@ -237,6 +244,7 @@ internal unsafe class ShaderLibrary : IDisposable
if (_shaderCompilationBridge != null) if (_shaderCompilationBridge != null)
{ {
_shaderCompilationBridge.OnShaderVariantCompiled -= OnVariantCompiled; _shaderCompilationBridge.OnShaderVariantCompiled -= OnVariantCompiled;
_shaderCompilationBridge.OnShaderInvalidated -= OnShaderInvalidated;
} }
GC.SuppressFinalize(this); GC.SuppressFinalize(this);

View File

@@ -10,7 +10,6 @@ using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
using Windows.Devices.Geolocation;
namespace Ghost.Graphics.Test.Windows; namespace Ghost.Graphics.Test.Windows;
@@ -80,7 +79,7 @@ public sealed partial class GraphicsTestWindow : Window
group.AddSystem<CameraMovingSystem>(); group.AddSystem<CameraMovingSystem>();
group.SortSystems(); group.SortSystems();
_world.SystemManager.InitializeAll(default); _world.SystemManager.InitializeAll();
// Create Camera Entity // Create Camera Entity
@@ -107,7 +106,7 @@ public sealed partial class GraphicsTestWindow : Window
// Create Mesh Entity // Create Mesh Entity
//MeshBuilder.CreateCube(0.75f, default, Allocator.Persistent, out var vertices, out var indices); //MeshBuilder.CreateCube(0.75f, default, Allocator.Persistent, out var vertices, out var indices);
Utilities.MeshUtility.LoadMesh("F:/c/SimpleRayTracer/native/assets/bunny.obj", Allocator.Persistent, out var vertices, out var indices).ThrowIfFailed(); Utilities.MeshUtility.LoadMesh("F:/c/SimpleRayTracer/native/assets/bunny.obj", AllocationHandle.Persistent, out var vertices, out var indices).ThrowIfFailed();
// TODO: Put this to the beginning of the frame without creating another command buffer? // TODO: Put this to the beginning of the frame without creating another command buffer?
using var directCmd = _renderSystem.GraphicsEngine.CreateCommandBuffer(CommandBufferType.Graphics); using var directCmd = _renderSystem.GraphicsEngine.CreateCommandBuffer(CommandBufferType.Graphics);
@@ -144,7 +143,6 @@ public sealed partial class GraphicsTestWindow : Window
private void GraphicsTestWindow_Closed(object sender, WindowEventArgs e) private void GraphicsTestWindow_Closed(object sender, WindowEventArgs e)
{ {
#if false
try try
{ {
CompositionTarget.Rendering -= OnRendering; CompositionTarget.Rendering -= OnRendering;
@@ -172,7 +170,6 @@ public sealed partial class GraphicsTestWindow : Window
finally finally
{ {
} }
#endif
} }
private void SwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e) private void SwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e)

View File

@@ -7,8 +7,8 @@ namespace Ghost.UnitTest.ECS;
[DoNotParallelize] [DoNotParallelize]
public class EntityCommandBufferTests public class EntityCommandBufferTests
{ {
private struct CompA : IComponent { public int value; } private struct CompA : IComponentData { public int value; }
private struct CompB : IComponent { public int value; } private struct CompB : IComponentData { public int value; }
private World _world = null!; private World _world = null!;

View File

@@ -7,10 +7,10 @@ namespace Ghost.UnitTest.ECS;
[DoNotParallelize] [DoNotParallelize]
public class EntityQueryTests public class EntityQueryTests
{ {
private struct CompA : IComponent { public int value; } private struct CompA : IComponentData { public int value; }
private struct CompB : IComponent { public int value; } private struct CompB : IComponentData { public int value; }
private struct CompC : IComponent { public int value; } private struct CompC : IComponentData { public int value; }
private struct Tag : IComponent { } private struct Tag : IComponentData { }
private struct EnableableComp : IEnableableComponent { public int value; } private struct EnableableComp : IEnableableComponent { public int value; }
private World _world = null!; private World _world = null!;

View File

@@ -11,11 +11,11 @@ public class SharedComponentTests
{ {
// ── Test components ──────────────────────────────────────────────────────── // ── Test components ────────────────────────────────────────────────────────
private struct Tag : IComponent { } private struct Tag : IComponentData { }
private struct Tag2 : IComponent { } private struct Tag2 : IComponentData { }
private struct ScalarData : IComponent private struct ScalarData : IComponentData
{ {
public float value; public float value;
} }

View File

@@ -7,8 +7,8 @@ namespace Ghost.UnitTest.ECS;
[DoNotParallelize] [DoNotParallelize]
public class WorldTests public class WorldTests
{ {
private struct CompA : IComponent { public int value; } private struct CompA : IComponentData { public int value; }
private struct CompB : IComponent { public int value; } private struct CompB : IComponentData { public int value; }
private World _world = null!; private World _world = null!;

View File

@@ -31,16 +31,22 @@ public class ShaderLibraryTest
private class MockShaderCompilationBridge : IShaderCompilationBridge private class MockShaderCompilationBridge : IShaderCompilationBridge
{ {
public List<(ulong id, int passIndex, Key64<ShaderVariant> variantKey, LocalKeywordSet keywordMask)> Requests { get; } = new(); public List<(ulong id, int passIndex, Key64<ShaderVariant> variantKey, LocalKeywordSet keywordMask)> Requests { get; } = new();
public event Action<Key64<ShaderVariant>, ulong>? OnShaderVariantCompiled; public event ShaderVariantCompiledHandler? OnShaderVariantCompiled;
public event Action<ulong>? OnShaderInvalidated;
public void RequestCompilation(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, LocalKeywordSet keywordMask) public void RequestCompilation(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, LocalKeywordSet keywordMask)
{ {
Requests.Add((shaderId, passIndex, variantKey, keywordMask)); Requests.Add((shaderId, passIndex, variantKey, keywordMask));
} }
public void TriggerCompiled(Key64<ShaderVariant> variantKey, ulong newHash) public void TriggerCompiled(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, ReadOnlySpan<ShaderByteCode> byteCodes)
{ {
OnShaderVariantCompiled?.Invoke(variantKey, newHash); OnShaderVariantCompiled?.Invoke(shaderId, passIndex, variantKey, byteCodes);
}
public void TriggerInvalidated(ulong shaderId)
{
OnShaderInvalidated?.Invoke(shaderId);
} }
public void Dispose() public void Dispose()
@@ -64,8 +70,8 @@ public class ShaderLibraryTest
public unsafe void TestInvalidateShaderCache_EvictsPipelinesAndClearsCache() public unsafe void TestInvalidateShaderCache_EvictsPipelinesAndClearsCache()
{ {
// Arrange // Arrange
using var shaderLibrary = new ShaderLibrary(null, "TestShaderCache");
var mockPipelineLibrary = new MockPipelineLibrary(); var mockPipelineLibrary = new MockPipelineLibrary();
using var shaderLibrary = new ShaderLibrary(null, mockPipelineLibrary, "TestShaderCache");
ulong testShaderId = 12345; ulong testShaderId = 12345;
var testPassIndex = 0; var testPassIndex = 0;
@@ -99,7 +105,7 @@ public class ShaderLibraryTest
Assert.AreEqual(expectedHash, cachedResult.Value.compiledHash); Assert.AreEqual(expectedHash, cachedResult.Value.compiledHash);
// Act: Invalidate // Act: Invalidate
shaderLibrary.InvalidateShaderCache(testShaderId, mockPipelineLibrary); shaderLibrary.InvalidateShaderCache(testShaderId);
// Assert: EvictStalePipelines should be called // Assert: EvictStalePipelines should be called
Assert.HasCount(1, mockPipelineLibrary.EvictedHashes); Assert.HasCount(1, mockPipelineLibrary.EvictedHashes);
@@ -115,7 +121,7 @@ public class ShaderLibraryTest
{ {
// Arrange // Arrange
var mockBridge = new MockShaderCompilationBridge(); var mockBridge = new MockShaderCompilationBridge();
using var shaderLibrary = new ShaderLibrary(mockBridge, "TestShaderCache"); using var shaderLibrary = new ShaderLibrary(mockBridge, null, "TestShaderCache");
var testShaderId = 555UL; var testShaderId = 555UL;
var passIndex = 1; var passIndex = 1;
var variantKey = new Key64<ShaderVariant>(777); var variantKey = new Key64<ShaderVariant>(777);
@@ -133,28 +139,39 @@ public class ShaderLibraryTest
} }
[TestMethod] [TestMethod]
public void TestOnVariantCompiled_UpdatesHashCache() public unsafe void TestOnVariantCompiled_UpdatesHashCache()
{ {
// Arrange // Arrange
var mockBridge = new MockShaderCompilationBridge(); var mockBridge = new MockShaderCompilationBridge();
using var shaderLibrary = new ShaderLibrary(mockBridge, "TestShaderCache"); using var shaderLibrary = new ShaderLibrary(mockBridge, null, "TestShaderCache");
var variantKey = new Key64<ShaderVariant>(123); var variantKey = new Key64<ShaderVariant>(123);
ulong newHash = 0xABCDE;
var fakeData = new byte[] { 1, 2, 3, 4 };
var expectedHash = 0UL;
// Act // Act
mockBridge.TriggerCompiled(variantKey, newHash); fixed (byte* pData = fakeData)
{
var byteCode = new ShaderByteCode { pCode = pData, size = (ulong)fakeData.Length };
// Compute expected hash of bytecode
var dataSpan = new ReadOnlySpan<byte>(pData, fakeData.Length);
expectedHash = System.IO.Hashing.XxHash64.HashToUInt64(dataSpan);
mockBridge.TriggerCompiled(0, 0, variantKey, new ReadOnlySpan<ShaderByteCode>(ref byteCode));
}
// Assert // Assert
var result = shaderLibrary.GetCompiledHash(0, 0, variantKey); var result = shaderLibrary.GetCompiledHash(0, 0, variantKey);
Assert.IsTrue(result.IsSuccess); Assert.IsTrue(result.IsSuccess);
Assert.AreEqual(newHash, result.Value); Assert.AreEqual(expectedHash, result.Value);
} }
[TestMethod] [TestMethod]
public void TestGetCompiledCache_HandlesIndexOutOfBounds() public void TestGetCompiledCache_HandlesIndexOutOfBounds()
{ {
// Arrange // Arrange
using var shaderLibrary = new ShaderLibrary(null, "TestShaderCache"); using var shaderLibrary = new ShaderLibrary(null, null, "TestShaderCache");
var testShaderId = 111UL; var testShaderId = 111UL;
// Act // Act

View File

@@ -28,14 +28,14 @@ public class SceneGraphBuilderTests
private Entity CreateEntityWithScene(Scene scene) private Entity CreateEntityWithScene(Scene scene)
{ {
var entity = _world.EntityManager.CreateEntity(); var entity = _world.EntityManager.CreateEntity();
_world.EntityManager.AddComponent(entity, new SceneID { value = scene.ID }); _world.EntityManager.AddSharedComponent(entity, new SceneID { value = scene.ID });
return entity; return entity;
} }
private Entity CreateEntityWithSceneAndHierarchy(Scene scene, Entity parent) private Entity CreateEntityWithSceneAndHierarchy(Scene scene, Entity parent)
{ {
var entity = _world.EntityManager.CreateEntity(); var entity = _world.EntityManager.CreateEntity();
_world.EntityManager.AddComponent(entity, new SceneID { value = scene.ID }); _world.EntityManager.AddSharedComponent(entity, new SceneID { value = scene.ID });
_world.EntityManager.AddComponent(entity, new Hierarchy _world.EntityManager.AddComponent(entity, new Hierarchy
{ {
parent = Entity.Invalid, parent = Entity.Invalid,
@@ -163,7 +163,7 @@ public class SceneGraphBuilderTests
public void Build_InvalidSceneEntitiesAreExcluded() public void Build_InvalidSceneEntitiesAreExcluded()
{ {
var entity = _world.EntityManager.CreateEntity(); var entity = _world.EntityManager.CreateEntity();
_world.EntityManager.AddComponent(entity, new SceneID { value = Scene.INVALID_ID }); _world.EntityManager.AddSharedComponent(entity, new SceneID { value = Scene.INVALID_ID });
var nodes = SceneGraphBuilder.Build(_world); var nodes = SceneGraphBuilder.Build(_world);

View File

@@ -1,4 +1,3 @@
#if true
using Ghost.Core; using Ghost.Core;
using Ghost.Editor.Core; using Ghost.Editor.Core;
using Ghost.Editor.Core.Assets; using Ghost.Editor.Core.Assets;
@@ -75,7 +74,7 @@ public class SceneSerializationTests
{ {
var world = _worldService.EditorWorld; var world = _worldService.EditorWorld;
var entity = world.EntityManager.CreateEntity(); var entity = world.EntityManager.CreateEntity();
world.EntityManager.AddComponent(entity, new SceneID { value = scene.ID }); world.EntityManager.AddSharedComponent(entity, new SceneID { value = scene.ID });
return entity; return entity;
} }
@@ -83,7 +82,7 @@ public class SceneSerializationTests
{ {
var world = _worldService.EditorWorld; var world = _worldService.EditorWorld;
var entity = world.EntityManager.CreateEntity(); var entity = world.EntityManager.CreateEntity();
world.EntityManager.AddComponent(entity, new SceneID { value = scene.ID }); world.EntityManager.AddSharedComponent(entity, new SceneID { value = scene.ID });
world.EntityManager.AddComponent(entity, Hierarchy.Root); world.EntityManager.AddComponent(entity, Hierarchy.Root);
world.EntityManager.AddComponent(entity, new LocalToWorld()); world.EntityManager.AddComponent(entity, new LocalToWorld());
@@ -104,8 +103,8 @@ public class SceneSerializationTests
CreateEntityWithHierarchy(scene, Entity.Invalid); CreateEntityWithHierarchy(scene, Entity.Invalid);
var filePath = Path.Combine(_projectRoot, "TestScene.gscene"); var filePath = Path.Combine(_projectRoot, "TestScene.gscene");
var saveResult = _serializationService.SaveSceneFromEditorWorld(filePath, scene); _serializationService.SaveSceneFromEditorWorld(filePath, scene);
Assert.IsTrue(saveResult.IsSuccess, saveResult.Message);
Assert.IsTrue(File.Exists(filePath)); Assert.IsTrue(File.Exists(filePath));
var json = await File.ReadAllTextAsync(filePath, TestContext.CancellationToken); var json = await File.ReadAllTextAsync(filePath, TestContext.CancellationToken);
@@ -129,7 +128,7 @@ public class SceneSerializationTests
scene = loadResult.Value; scene = loadResult.Value;
using var scope = AllocationManager.CreateStackScope(); using var scope = AllocationManager.CreateStackScope();
using var entities = SceneManager.GetSceneEntities(scene, world, scope.AllocationHandle); using var entities = SceneManager.GetSceneEntities(world, scene, scope.AllocationHandle);
Assert.AreEqual(3, entities.Count, $"Expected 3 entities for scene {scene.ID} but found {entities.Count}"); Assert.AreEqual(3, entities.Count, $"Expected 3 entities for scene {scene.ID} but found {entities.Count}");
} }
@@ -145,8 +144,7 @@ public class SceneSerializationTests
var world = _worldService.EditorWorld; var world = _worldService.EditorWorld;
var filePath = Path.Combine(_projectRoot, "HierarchyScene.gscene"); var filePath = Path.Combine(_projectRoot, "HierarchyScene.gscene");
var saveResult = _serializationService.SaveSceneFromEditorWorld(filePath, scene); _serializationService.SaveSceneFromEditorWorld(filePath, scene);
Assert.IsTrue(saveResult.IsSuccess, saveResult.Message);
var data = await SceneSerializationService.DeserializeSceneFileAsync(filePath, TestContext.CancellationToken); var data = await SceneSerializationService.DeserializeSceneFileAsync(filePath, TestContext.CancellationToken);
Assert.IsNotNull(data); Assert.IsNotNull(data);
@@ -288,6 +286,7 @@ public class SceneSerializationTests
Assert.IsTrue(loadResult.IsSuccess, loadResult.Message); Assert.IsTrue(loadResult.IsSuccess, loadResult.Message);
var afterCount = 0; var afterCount = 0;
query = ref world.ComponentManager.GetEntityQueryReference(queryID);
foreach (var chunk in query.GetChunkIterator()) foreach (var chunk in query.GetChunkIterator())
{ {
afterCount += chunk.EntityCount; afterCount += chunk.EntityCount;
@@ -302,10 +301,9 @@ public class SceneSerializationTests
var scene = SceneManager.CreateScene(); var scene = SceneManager.CreateScene();
var filePath = Path.Combine(_projectRoot, "EmptyScene.gscene"); var filePath = Path.Combine(_projectRoot, "EmptyScene.gscene");
var saveResult = _serializationService.SaveSceneFromEditorWorld(filePath, scene); _serializationService.SaveSceneFromEditorWorld(filePath, scene);
Assert.IsTrue(saveResult.IsFailure, "Empty scene should fail to save.");
Assert.AreEqual("No entities found for the specified scene.", saveResult.Message); Assert.IsTrue(File.Exists(filePath));
} }
[TestMethod] [TestMethod]
@@ -396,7 +394,7 @@ public class SceneSerializationTests
var world = World.Create(entityCapacity: 64); var world = World.Create(entityCapacity: 64);
try try
{ {
Result<SceneManager.LoadedSceneData> sceneDataResult; Result<LoadedSceneData> sceneDataResult;
unsafe unsafe
{ {
fixed (byte* pBinary = binary) fixed (byte* pBinary = binary)
@@ -409,7 +407,7 @@ public class SceneSerializationTests
Assert.AreEqual(3, sceneDataResult.Value.entities.Count); Assert.AreEqual(3, sceneDataResult.Value.entities.Count);
using var sceneData = sceneDataResult.Value; using var sceneData = sceneDataResult.Value;
SceneManager.MaterializeScene(world, in sceneData, scene); SceneManager.MaterializeScene(world, in sceneData, scene, 0, sceneData.entities.Count);
var queryID = new QueryBuilder().WithAll<Hierarchy>().Build(world); var queryID = new QueryBuilder().WithAll<Hierarchy>().Build(world);
ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID); ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);
@@ -481,4 +479,3 @@ public class SceneSerializationTests
get; set; get; set;
} = null!; } = null!;
} }
#endif