Remove old SceneGraph
This commit is contained in:
@@ -24,4 +24,12 @@
|
||||
<ProjectReference Include="..\Ghost.Graphics\Ghost.Graphics.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Services\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MemoryPack" Version="1.21.4" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Engine.Components;
|
||||
using Ghost.Engine.Core;
|
||||
using Ghost.Entities;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
|
||||
namespace Ghost.Engine.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Handles binary serialization and deserialization of scenes for AOT-compatible runtime use.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Binary format provides fast, compact scene loading suitable for AOT compilation.
|
||||
/// Uses direct memory copying for component data without reflection.
|
||||
/// </remarks>
|
||||
public static unsafe class SceneBinarySerializer
|
||||
{
|
||||
private const int MAGIC_NUMBER = 0x47534345; // "GSCE" (Ghost Scene)
|
||||
private const int VERSION = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Saves a scene to a binary file.
|
||||
/// </summary>
|
||||
/// <param name="world">The world containing the entities.</param>
|
||||
/// <param name="sceneID">The scene ID to save.</param>
|
||||
/// <param name="filePath">The path to save the scene file.</param>
|
||||
public static void SaveScene(World world, short sceneID, string filePath)
|
||||
{
|
||||
using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
|
||||
using var writer = new BinaryWriter(stream);
|
||||
using var context = SerializationContext.Create();
|
||||
|
||||
// Write header
|
||||
writer.Write(MAGIC_NUMBER);
|
||||
writer.Write(VERSION);
|
||||
writer.Write(sceneID);
|
||||
|
||||
// Query all entities with the specified SceneID
|
||||
var queryID = new QueryBuilder()
|
||||
.WithAll<SceneID>()
|
||||
.Build(world);
|
||||
|
||||
var entities = new List<Entity>();
|
||||
|
||||
world.ComponentManager.GetEntityQueryReference(queryID).ForEach<SceneID>((Entity entity, ref SceneID sceneIDComponent) =>
|
||||
{
|
||||
if (sceneIDComponent.id == sceneID)
|
||||
{
|
||||
entities.Add(entity);
|
||||
}
|
||||
});
|
||||
|
||||
// Write entity count
|
||||
writer.Write(entities.Count);
|
||||
|
||||
// Allocate buffer for zero-filled component data (reused across loop iterations)
|
||||
const int MaxComponentSize = 4096; // Reasonable max size for most components
|
||||
var zeroBuffer = stackalloc byte[MaxComponentSize];
|
||||
MemoryUtility.MemSet(zeroBuffer, 0, MaxComponentSize);
|
||||
|
||||
// Write each entity
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
var fileId = context.RegisterEntityForSerialization(entity);
|
||||
|
||||
// Write entity file ID
|
||||
writer.Write(fileId);
|
||||
|
||||
// Get entity location
|
||||
var locationResult = world.EntityManager.GetEntityLocation(entity);
|
||||
if (locationResult.Error != Error.None)
|
||||
{
|
||||
// Write 0 components for invalid entity
|
||||
writer.Write(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
var location = locationResult.Value;
|
||||
ref var archetype = ref world.ComponentManager.GetArchetypeReference(location.archetypeID);
|
||||
|
||||
// Write component count
|
||||
writer.Write(archetype._layouts.Count);
|
||||
|
||||
// Write each component
|
||||
foreach (var layout in archetype._layouts)
|
||||
{
|
||||
// Write component type ID
|
||||
writer.Write((int)layout.componentID);
|
||||
|
||||
// Write component size
|
||||
writer.Write(layout.size);
|
||||
|
||||
// Get component data pointer
|
||||
var pComponentData = archetype.GetComponentData(location.chunkIndex, location.rowIndex, layout.componentID);
|
||||
if (pComponentData == null)
|
||||
{
|
||||
// Write zero-filled data if component not found
|
||||
if (layout.size > MaxComponentSize)
|
||||
{
|
||||
throw new InvalidOperationException($"Component size {layout.size} exceeds maximum buffer size {MaxComponentSize}");
|
||||
}
|
||||
writer.Write(new ReadOnlySpan<byte>(zeroBuffer, layout.size));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write component data directly
|
||||
writer.Write(new ReadOnlySpan<byte>(pComponentData, layout.size));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a scene from a binary file into the specified world.
|
||||
/// </summary>
|
||||
/// <param name="world">The world to load the scene into.</param>
|
||||
/// <param name="filePath">The path to the scene file.</param>
|
||||
/// <param name="newSceneID">The new scene ID to assign to loaded entities.</param>
|
||||
/// <returns>The number of entities loaded.</returns>
|
||||
public static int LoadScene(World world, string filePath, short newSceneID)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException($"Scene file not found: {filePath}");
|
||||
}
|
||||
|
||||
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||
using var reader = new BinaryReader(stream);
|
||||
using var context = SerializationContext.Create();
|
||||
|
||||
// Read and validate header
|
||||
var magic = reader.ReadInt32();
|
||||
if (magic != MAGIC_NUMBER)
|
||||
{
|
||||
throw new InvalidDataException("Invalid scene file format.");
|
||||
}
|
||||
|
||||
var version = reader.ReadInt32();
|
||||
if (version != VERSION)
|
||||
{
|
||||
throw new InvalidDataException($"Unsupported scene file version: {version}");
|
||||
}
|
||||
|
||||
var savedSceneID = reader.ReadInt16();
|
||||
|
||||
// Read entity count
|
||||
var entityCount = reader.ReadInt32();
|
||||
|
||||
// Pass 1: Create all entities and build ID mapping
|
||||
var fileIdToEntity = new Dictionary<int, Entity>(entityCount);
|
||||
var entityComponents = new List<(int fileId, List<(Identifier<IComponent> componentID, int size, byte[] data)> components)>(entityCount);
|
||||
|
||||
for (var i = 0; i < entityCount; i++)
|
||||
{
|
||||
var fileId = reader.ReadInt32();
|
||||
var componentCount = reader.ReadInt32();
|
||||
|
||||
var components = new List<(Identifier<IComponent> componentID, int size, byte[] data)>(componentCount);
|
||||
|
||||
// Read component data
|
||||
for (var j = 0; j < componentCount; j++)
|
||||
{
|
||||
var componentID = new Identifier<IComponent>(reader.ReadInt32());
|
||||
var size = reader.ReadInt32();
|
||||
var data = reader.ReadBytes(size);
|
||||
|
||||
components.Add((componentID, size, data));
|
||||
}
|
||||
|
||||
entityComponents.Add((fileId, components));
|
||||
|
||||
// Create entity
|
||||
var entity = world.EntityManager.CreateEntity();
|
||||
fileIdToEntity[fileId] = entity;
|
||||
context.RegisterEntity(fileId, entity);
|
||||
|
||||
// Add SceneID component
|
||||
world.EntityManager.AddComponent(entity, new SceneID { id = newSceneID });
|
||||
}
|
||||
|
||||
// Pass 2: Add components to entities (with automatic entity reference remapping)
|
||||
foreach (var (fileId, components) in entityComponents)
|
||||
{
|
||||
if (!fileIdToEntity.TryGetValue(fileId, out var entity))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var (componentID, size, data) in components)
|
||||
{
|
||||
// Skip SceneID as we already added it
|
||||
if (componentID == ComponentTypeID<SceneID>.Value)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fixed (byte* pData = data)
|
||||
{
|
||||
// Remap Entity references in the component data
|
||||
RemapEntityReferences(pData, componentID, context);
|
||||
|
||||
// Add component
|
||||
world.EntityManager.AddComponent(entity, componentID, pData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fileIdToEntity.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remaps Entity references within component data.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is a simple implementation that checks if the component contains Entity fields.
|
||||
/// For Hierarchy, it remaps parent, firstChild, and nextSibling fields.
|
||||
/// </remarks>
|
||||
private static void RemapEntityReferences(byte* pComponentData, Identifier<IComponent> componentID, SerializationContext context)
|
||||
{
|
||||
// Check if this is the Hierarchy component
|
||||
if (componentID == ComponentTypeID<Hierarchy>.Value)
|
||||
{
|
||||
var hierarchy = (Hierarchy*)pComponentData;
|
||||
|
||||
// Remap parent
|
||||
if (hierarchy->parent.IsValid && context.TryGetFileId(hierarchy->parent, out var parentFileId))
|
||||
{
|
||||
if (context.TryGetEntity(parentFileId, out var newParent))
|
||||
{
|
||||
hierarchy->parent = newParent;
|
||||
}
|
||||
}
|
||||
|
||||
// Remap firstChild
|
||||
if (hierarchy->firstChild.IsValid && context.TryGetFileId(hierarchy->firstChild, out var firstChildFileId))
|
||||
{
|
||||
if (context.TryGetEntity(firstChildFileId, out var newFirstChild))
|
||||
{
|
||||
hierarchy->firstChild = newFirstChild;
|
||||
}
|
||||
}
|
||||
|
||||
// Remap nextSibling
|
||||
if (hierarchy->nextSibling.IsValid && context.TryGetFileId(hierarchy->nextSibling, out var nextSiblingFileId))
|
||||
{
|
||||
if (context.TryGetEntity(nextSiblingFileId, out var newNextSibling))
|
||||
{
|
||||
hierarchy->nextSibling = newNextSibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add remapping for other components with Entity fields
|
||||
// This could be automated using source generators in the future
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
using Ghost.Entities;
|
||||
|
||||
namespace Ghost.Engine.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a thread-safe context for Entity ID remapping during deserialization.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class manages the mapping between file-local entity IDs (0, 1, 2...)
|
||||
/// and runtime entity IDs during scene deserialization. The context is scoped
|
||||
/// to the current async operation using AsyncLocal storage.
|
||||
/// </remarks>
|
||||
public sealed class SerializationContext : IDisposable
|
||||
{
|
||||
private static readonly AsyncLocal<SerializationContext?> s_current = new();
|
||||
|
||||
private readonly Dictionary<int, Entity> _fileIdToEntity = new();
|
||||
private readonly Dictionary<Entity, int> _entityToFileId = new();
|
||||
private int _nextFileId = 0;
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current serialization context for this async operation.
|
||||
/// </summary>
|
||||
public static SerializationContext? Current => s_current.Value;
|
||||
|
||||
private SerializationContext()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and activates a new serialization context for the current async scope.
|
||||
/// </summary>
|
||||
/// <returns>A new serialization context. Must be disposed when done.</returns>
|
||||
public static SerializationContext Create()
|
||||
{
|
||||
if (s_current.Value != null)
|
||||
{
|
||||
throw new InvalidOperationException("A serialization context is already active in this scope.");
|
||||
}
|
||||
|
||||
var context = new SerializationContext();
|
||||
s_current.Value = context;
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an entity mapping for deserialization.
|
||||
/// </summary>
|
||||
/// <param name="fileId">The file-local entity ID.</param>
|
||||
/// <param name="runtimeEntity">The runtime entity.</param>
|
||||
public void RegisterEntity(int fileId, Entity runtimeEntity)
|
||||
{
|
||||
_fileIdToEntity[fileId] = runtimeEntity;
|
||||
_entityToFileId[runtimeEntity] = fileId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a runtime entity and assigns it the next available file ID for serialization.
|
||||
/// </summary>
|
||||
/// <param name="runtimeEntity">The runtime entity to register.</param>
|
||||
/// <returns>The assigned file-local ID.</returns>
|
||||
public int RegisterEntityForSerialization(Entity runtimeEntity)
|
||||
{
|
||||
if (!_entityToFileId.TryGetValue(runtimeEntity, out var fileId))
|
||||
{
|
||||
fileId = _nextFileId++;
|
||||
_entityToFileId[runtimeEntity] = fileId;
|
||||
_fileIdToEntity[fileId] = runtimeEntity;
|
||||
}
|
||||
|
||||
return fileId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the runtime entity for a file-local ID.
|
||||
/// </summary>
|
||||
/// <param name="fileId">The file-local entity ID.</param>
|
||||
/// <param name="entity">The runtime entity if found.</param>
|
||||
/// <returns>True if the entity was found, false otherwise.</returns>
|
||||
public bool TryGetEntity(int fileId, out Entity entity)
|
||||
{
|
||||
return _fileIdToEntity.TryGetValue(fileId, out entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the file-local ID for a runtime entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The runtime entity.</param>
|
||||
/// <param name="fileId">The file-local ID if found.</param>
|
||||
/// <returns>True if the file ID was found, false otherwise.</returns>
|
||||
public bool TryGetFileId(Entity entity, out int fileId)
|
||||
{
|
||||
return _entityToFileId.TryGetValue(entity, out fileId);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
s_current.Value = null;
|
||||
_fileIdToEntity.Clear();
|
||||
_entityToFileId.Clear();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
using Ghost.Engine.Components;
|
||||
using Ghost.Engine.Core;
|
||||
using Ghost.Engine.IO;
|
||||
using Ghost.Entities;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Engine.Services;
|
||||
|
||||
public enum SceneLoadMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Unloads all currently loaded scenes before loading the new scene.
|
||||
/// </summary>
|
||||
Single,
|
||||
|
||||
/// <summary>
|
||||
/// Loads the scene additively without unloading existing scenes.
|
||||
/// </summary>
|
||||
Additive
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manages scene loading, unloading, and saving operations using binary serialization.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This runtime scene manager uses binary serialization for AOT compatibility.
|
||||
/// For editor JSON serialization, use EditorSceneManager in Ghost.Editor.Core.
|
||||
/// </remarks>
|
||||
public static class SceneManager
|
||||
{
|
||||
private static readonly Dictionary<short, Scene> s_loadedScenes = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all currently loaded scenes.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<Scene> LoadedScenes => s_loadedScenes.Values;
|
||||
|
||||
/// <summary>
|
||||
/// Loads a scene from a binary file into the specified world.
|
||||
/// </summary>
|
||||
/// <param name="world">The world to load the scene into.</param>
|
||||
/// <param name="filePath">The path to the scene file.</param>
|
||||
/// <param name="loadMode">The load mode (Single or Additive).</param>
|
||||
/// <returns>The loaded scene.</returns>
|
||||
public static Scene LoadScene(World world, string filePath, SceneLoadMode loadMode = SceneLoadMode.Single)
|
||||
{
|
||||
if (loadMode == SceneLoadMode.Single)
|
||||
{
|
||||
// Unload all currently loaded scenes in this world
|
||||
var scenesToUnload = s_loadedScenes.Values.Where(s => s.World == world).ToList();
|
||||
foreach (var scene in scenesToUnload)
|
||||
{
|
||||
UnloadScene(scene);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a new scene ID for this load
|
||||
var sceneName = Path.GetFileNameWithoutExtension(filePath);
|
||||
var newScene = new Scene(world, sceneName);
|
||||
|
||||
// Load the scene data using binary serialization
|
||||
SceneBinarySerializer.LoadScene(world, filePath, newScene.ID);
|
||||
|
||||
// Register the loaded scene
|
||||
s_loadedScenes[newScene.ID] = newScene;
|
||||
|
||||
return newScene;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a scene to a binary file.
|
||||
/// </summary>
|
||||
/// <param name="scene">The scene to save.</param>
|
||||
/// <param name="filePath">The path to save the scene file.</param>
|
||||
public static void SaveScene(Scene scene, string filePath)
|
||||
{
|
||||
SceneBinarySerializer.SaveScene(scene.World, scene.ID, filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unloads a scene, destroying all entities belonging to it.
|
||||
/// </summary>
|
||||
/// <param name="scene">The scene to unload.</param>
|
||||
public static void UnloadScene(Scene scene)
|
||||
{
|
||||
if (!s_loadedScenes.ContainsKey(scene.ID))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Query all entities with the scene's ID
|
||||
var queryID = new QueryBuilder()
|
||||
.WithAll<SceneID>()
|
||||
.Build(scene.World);
|
||||
|
||||
var entitiesToDestroy = new List<Entity>();
|
||||
|
||||
scene.World.ComponentManager.GetEntityQueryReference(queryID).ForEach<SceneID>((Entity entity, ref SceneID sceneIDComponent) =>
|
||||
{
|
||||
if (sceneIDComponent.id == scene.ID)
|
||||
{
|
||||
entitiesToDestroy.Add(entity);
|
||||
}
|
||||
});
|
||||
|
||||
// Destroy all entities in this scene
|
||||
scene.World.EntityManager.DestroyEntities(CollectionsMarshal.AsSpan(entitiesToDestroy));
|
||||
|
||||
// Remove from loaded scenes
|
||||
s_loadedScenes.Remove(scene.ID);
|
||||
|
||||
// Dispose the scene handle
|
||||
scene.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unloads all scenes in the specified world.
|
||||
/// </summary>
|
||||
/// <param name="world">The world whose scenes to unload.</param>
|
||||
public static void UnloadAllScenes(World world)
|
||||
{
|
||||
var scenesToUnload = s_loadedScenes.Values.Where(s => s.World == world).ToList();
|
||||
foreach (var scene in scenesToUnload)
|
||||
{
|
||||
UnloadScene(scene);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a loaded scene by its ID.
|
||||
/// </summary>
|
||||
/// <param name="sceneID">The scene ID to find.</param>
|
||||
/// <param name="scene">The found scene, or null if not loaded.</param>
|
||||
/// <returns>True if the scene was found, false otherwise.</returns>
|
||||
public static bool TryGetScene(short sceneID, out Scene? scene)
|
||||
{
|
||||
return s_loadedScenes.TryGetValue(sceneID, out scene);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user