3 Commits

Author SHA1 Message Date
a1c5ccf937 Refactor scene loading/materialization & asset streaming
- Overhauled scene loading with async, incremental, and cancellable operations using new types (SceneLoadStatus, SceneLoadOptions, etc.)
- Added thread-safe management of pending/loaded scenes and per-frame materialization budgeting
- Changed mesh header offsets from ulong to long; updated all related code to use stream positions directly
- Improved resource release logic for mesh/texture asset entries
- Refactored asset loading jobs and made reimport flag atomic
- Stream utility now always aligns memory blocks to 16 bytes
- Misc: fixed mock content headers, cleaned up code, and improved interface visibility
2026-05-22 18:21:16 +09:00
6b501efda0 Refactor and expand scene/entity hierarchy system
- SceneGraphBuilder: support initial entity names in tree
- EditorWorldService: full CRUD, events, scene graph rebuild
- SceneGraphSyncService: event-driven sync, node map, TryGetNode
- SceneSerializationService: serialize/deserialize names, sync
- Hierarchy UI: context menus, drag-and-drop, delete, no polling
- SceneManager: fix component type ID handling
- Add SceneGraphSync unit tests
- Remove obsolete asset handler/importer attributes
- Scene hierarchy is now reactive, robust, and testable
2026-05-22 17:55:25 +09:00
a40140cabd Refactor editor DI, update scene graph infra & deps
- Removed `[EditorInjectionAttribute]` and switched to direct service registration in `App.xaml.cs`
- Deleted `EditorIconSource` and moved icon handling to XAML
- Moved and enhanced `PathUtility` for path normalization and unique naming
- Renamed `SceneManager.UnloadScene` to `DestroyScene` for clarity
- Optimized `EntityQuery.Any()` with bitmask logic
- Improved `SceneGraphBuilder` and `SceneGraphSyncService` for better scene/entity handling
- Simplified entity field detection in `SceneSerializationService`
- Removed unused `TypeCache.Initialize()`
- Updated `ContentBrowser` to support scene asset creation and adjusted selection logic
- Upgraded NuGet package versions in project files
- Changed code generators to use `#if GHOST_EDITOR`
- Made `ShaderVariantCompiledHandler` safe
- Updated or removed scene graph planning docs to match new architecture
2026-05-22 15:32:30 +09:00
38 changed files with 1479 additions and 1084 deletions

View File

@@ -402,28 +402,28 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
using var stream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None); using var stream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None);
stream.Write(header); stream.Write(header);
header.vertexOffset = (ulong)stream.Position; header.vertexOffset = stream.Position;
await stream.WriteAsync<Vertex, UnsafeList<Vertex>>(geometry.Vertices, token); await stream.WriteAsync<Vertex, UnsafeList<Vertex>>(geometry.Vertices, token);
header.indexOffset = (ulong)stream.Position; header.indexOffset = stream.Position;
await stream.WriteAsync<uint, UnsafeList<uint>>(geometry.Indices, token); await stream.WriteAsync<uint, UnsafeList<uint>>(geometry.Indices, token);
header.materialPartOffset = (ulong)stream.Position; header.materialPartOffset = stream.Position;
WriteMaterialParts(stream, geometry.MaterialParts.AsSpan()); WriteMaterialParts(stream, geometry.MaterialParts.AsSpan());
header.meshletOffset = (ulong)stream.Position; header.meshletOffset = stream.Position;
await stream.WriteAsync<Meshlet, UnsafeList<Meshlet>>(meshletData.GetRef().meshlets, token); await stream.WriteAsync<Meshlet, UnsafeList<Meshlet>>(meshletData.GetRef().meshlets, token);
header.meshletGroupOffset = (ulong)stream.Position; header.meshletGroupOffset = stream.Position;
await stream.WriteAsync<MeshletGroup, UnsafeList<MeshletGroup>>(meshletData.GetRef().groups, token); await stream.WriteAsync<MeshletGroup, UnsafeList<MeshletGroup>>(meshletData.GetRef().groups, token);
header.meshletHierarchyNodeOffset = (ulong)stream.Position; header.meshletHierarchyNodeOffset = stream.Position;
await stream.WriteAsync<MeshletHierarchyNode, UnsafeList<MeshletHierarchyNode>>(meshletData.GetRef().hierarchyNodes, token); await stream.WriteAsync<MeshletHierarchyNode, UnsafeList<MeshletHierarchyNode>>(meshletData.GetRef().hierarchyNodes, token);
header.meshletVertexOffset = (ulong)stream.Position; header.meshletVertexOffset = stream.Position;
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.GetRef().meshletVertices, token); await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.GetRef().meshletVertices, token);
header.meshletTriangleOffset = (ulong)stream.Position; header.meshletTriangleOffset = stream.Position;
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.GetRef().meshletTriangles, token); await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.GetRef().meshletTriangles, token);
stream.Position = 0; stream.Position = 0;

View File

@@ -5,35 +5,6 @@ namespace Ghost.Editor.Core;
/// </summary> /// </summary>
public abstract class DiscoverableAttributeBase : Attribute; public abstract class DiscoverableAttributeBase : Attribute;
[AttributeUsage(AttributeTargets.Method)]
public class AssetOpenHandlerAttribute : DiscoverableAttributeBase
{
public string[] Extensions
{
get;
}
public AssetOpenHandlerAttribute(params string[] extensions)
{
Extensions = extensions.Select(e => e.StartsWith('.') ? e.ToLowerInvariant() : '.' + e.ToLowerInvariant()).ToArray();
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
internal class AssetImporterAttribute : DiscoverableAttributeBase
{
public string[] SupportedExtensions
{
get;
}
public AssetImporterAttribute(params string[] supportedExtensions)
{
SupportedExtensions = supportedExtensions;
}
}
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class)]
public class CustomEditorAttribute : DiscoverableAttributeBase public class CustomEditorAttribute : DiscoverableAttributeBase
{ {
@@ -48,32 +19,6 @@ public class CustomEditorAttribute : DiscoverableAttributeBase
} }
} }
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
public class EditorInjectionAttribute : DiscoverableAttributeBase
{
public enum ServiceLifetime
{
Singleton,
Transient,
}
public ServiceLifetime Lifetime
{
get;
}
public Type ImplementationType
{
get;
}
public EditorInjectionAttribute(ServiceLifetime lifetime, Type implementationType)
{
Lifetime = lifetime;
ImplementationType = implementationType;
}
}
[AttributeUsage(AttributeTargets.Method)] [AttributeUsage(AttributeTargets.Method)]
public sealed class ContextMenuItemAttribute : DiscoverableAttributeBase public sealed class ContextMenuItemAttribute : DiscoverableAttributeBase
{ {

View File

@@ -1,4 +1,4 @@
using Ghost.Core.Utilities; using Ghost.Editor.Core.Utilities;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;

View File

@@ -17,9 +17,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentIcons.WinUI" Version="2.1.326" /> <PackageReference Include="FluentIcons.WinUI" Version="2.1.326" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.8" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.0.1" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="2.1.3" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" /> <PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />
</ItemGroup> </ItemGroup>

View File

@@ -1,18 +0,0 @@
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Resources;
public static class EditorIconSource
{
public static readonly IconSource scene_24 = new FontIconSource
{
Glyph = "\uF159",
FontSize = 24
};
public static readonly IconSource entity_24 = new FontIconSource
{
Glyph = "\uF158",
FontSize = 24
};
}

View File

@@ -1,521 +0,0 @@
# SceneGraph Implementation Plan
## Summary of Design Decisions
| Decision | Choice |
|----------|--------|
| Hierarchy data model | Linked-list (`parent`/`firstChild`/`nextSibling`), efficient for ECS |
| Editor ↔ World relationship | Mirror — editor world holds ECS entities, SceneGraph reflects them as observable nodes |
| Transform representation | `LocalToWorld` matrix only (no separate PRS components) |
| Implementation order | Data structures → HierarchySystem → Mirror bridge → UI → Serialization |
---
## Phase 1: Fix SceneGraph Data Structures
**Goal:** Make `EntityNode`, `SceneNode`, and `SceneGraphNode` correct, complete, and constructible.
### 1.1 Enhance `SceneGraphNode`
**File:** `SceneGraphNode.cs`
- Add a `World` property so every node knows which world it belongs to.
- Make the constructor accept `World` and `string name`.
- Keep `ObservableObject` for MVVM binding.
- Keep `Children` as `ObservableCollection<SceneGraphNode>`.
```csharp
public abstract partial class SceneGraphNode : ObservableObject, IInspectable
{
public World World { get; }
[ObservableProperty]
public partial string Name { get; set; }
public ObservableCollection<SceneGraphNode> Children { get; } = new();
protected SceneGraphNode(World world, string name)
{
World = world;
Name = name;
}
// ... existing abstract members
}
```
### 1.2 Enhance `SceneNode`
**File:** `SceneNode.cs`
- Add a `Scene` field referencing the runtime `Scene` struct.
- Add a constructor: `SceneNode(World world, Scene scene, string name)`.
- Remove `XamlReader.Load()` template generation (Phase 4 will move to XAML resources).
```csharp
public sealed partial class SceneNode : SceneGraphNode
{
public Scene Scene { get; }
public SceneNode(World world, Scene scene, string name)
: base(world, name)
{
Scene = scene;
}
// icon, header, inspector, template
}
```
### 1.3 Enhance `EntityNode`
**File:** `EntityNode.cs`
- Add a constructor: `EntityNode(World world, Entity entity, string name)`.
- Remove `XamlReader.Load()` template.
- Entity reference is read-only after construction (immutable node identity).
```csharp
public sealed partial class EntityNode : SceneGraphNode
{
public Entity Entity { get; }
public EntityNode(World world, Entity entity, string name)
: base(world, name)
{
Entity = entity;
}
// icon, header, inspector, template
}
```
### 1.4 Add `SceneGraphBuilder`
**New file:** `SceneGraphBuilder.cs`
Used for the **initial full build** of the scene graph from an ECS `World`. After initial construction, incremental updates are handled by `SceneGraphSyncService` (Phase 3).
**Algorithm:**
1. Query all entities with `SceneID` component.
2. Group entities by `SceneID.scene.id`.
3. For each scene group:
- Create a `SceneNode` (name comes from editor metadata, not from runtime).
- Walk the `Hierarchy` component linked-list to build the tree:
- Root entities are those with `Hierarchy.parent == Entity.Invalid`.
- For each root, create an `EntityNode` and recursively visit `firstChild``nextSibling`.
- Entity names come from a naming system (either a component like `EntityName`, or editor-side metadata stored on the node).
4. Return `ObservableCollection<SceneGraphNode>` (the list of root scenes).
**Name resolution strategy:**
Names are stored directly on `EntityNode.Name`. During initial build, all entities get default names. During incremental sync, existing nodes (and their user-assigned names) are preserved because nodes are matched by `Entity` identity rather than recreated. See Phase 1.5 and 3.2 for details.
### 1.5 Entity Names (Editor-Only)
**No runtime component.** Entity names have no purpose in the runtime ECS. Names live purely on the editor side, stored directly on `EntityNode.Name`.
**Strategy:** Entity names persist naturally across syncs because `EntityNode` instances survive — the sync is incremental, not a full rebuild (see Phase 3.2). Names are read directly from `EntityNode.Name` for inspector display and serialization.
- New entities get a default name (e.g., `"Entity"`).
- User-renamed entities keep their name because the node instance is preserved.
- Destroyed entities have their node removed (names disappear with them).
- No dictionary, no cross-world identity concerns, no runtime footprint.
---
## Phase 2: HierarchySystem (Runtime)
**Goal:** Maintain the `Hierarchy` component's linked-list invariants.
**New file:** `Runtime/Ghost.Engine/Systems/HierarchySystem.cs`
### 2.1 Operations
The system exposes static helper methods (or instance methods on `EntityManager`) for hierarchy mutation. All mutations use `EntityCommandBuffer` internally to avoid structural changes mid-iteration.
| Operation | Description |
|-----------|-------------|
| `SetParent(child, parent)` | Links `child` as a child of `parent`. Updates `parent`, `firstChild`, `nextSibling` on both sides. |
| `RemoveParent(child)` | Unlinks `child` from its current parent. It becomes a root entity. |
| `DestroyEntity(entity)` | Also unlinks from parent, reparents children to grandparent (or makes them roots). |
### 2.2 `SetParent(Entity child, Entity parent)`
**Preconditions:**
- `child.IsValid` and `parent.IsValid`.
- Both entities exist in the world.
- Both have the `Hierarchy` component.
- `child != parent` (no self-parenting).
- `parent` is not a descendant of `child` (no cycles).
**Algorithm:**
1. If `child` already has a parent, call `RemoveParent(child)` first.
2. Get `ref childHierarchy`, `ref parentHierarchy`.
3. Set `childHierarchy.parent = parent`.
4. Set `childHierarchy.nextSibling = parentHierarchy.firstChild`.
5. Set `parentHierarchy.firstChild = child`.
### 2.3 `RemoveParent(Entity child)`
**Preconditions:** `child` has the `Hierarchy` component and has a parent.
**Algorithm:**
1. Get `ref childHierarchy`. Let `parent_entity = childHierarchy.parent`.
2. Get `ref parentHierarchy`.
3. Walk the sibling chain of `parentHierarchy.firstChild` to find and unlink `child`:
- If `firstChild == child`: set `firstChild = childHierarchy.nextSibling`.
- Else: walk `prev → current → nextSibling`, set `prev.nextSibling = childHierarchy.nextSibling`.
4. Set `childHierarchy.parent = Entity.Invalid`.
5. Set `childHierarchy.nextSibling = Entity.Invalid`.
### 2.4 Entity Destruction Cleanup
When an entity is destroyed via `EntityManager.DestroyEntity()`:
1. Get its `Hierarchy` component.
2. Call `RemoveParent(entity)`.
3. **Cascade destroy all children:** Walk `firstChild``nextSibling` and recursively destroy every descendant entity. Destroy children before destroying the parent to ensure correct cleanup order.
4. When saving/loading, this means saving an entity implies saving its entire subtree. Deleting an entity implies deleting its entire subtree.
### 2.5 Validation
Each method validates invariants in `DEBUG`/`GHOST_EDITOR` builds:
- No entity appears as its own ancestor (cycle detection via ancestor walk).
- `firstChild`/`parent`/`nextSibling` references are all valid entities or `Entity.Invalid`.
- No dangling references.
### 2.6 `HierarchySystem` as an `ISystem`
The system does NOT run every frame automatically. Parent/child mutations are explicit (called from editor commands or loading code). The system's `Update()` is a no-op. It exists as an `ISystem` so it can:
- Register its existence in the system graph.
- Be queried via `World.SystemManager`.
- Hold references to necessary queries.
Alternatively: make it a static utility class that takes `World` as a parameter. **Recommendation:** static utility class (`HierarchyUtility`) to avoid entity archetype cost for a no-op system. The `Hierarchy` component itself drives the tree shape.
---
## Phase 3: Mirror Bridge (Editor ↔ ECS sync)
**Goal:** Keep the `SceneGraphNode` tree in the editor synchronized with the ECS `World`.
### 3.1 `EditorWorldService`
**New file:** `Editor/Ghost.Editor.Core/Services/EditorWorldService.cs`
- Registers as a singleton via `[EditorInjection(ServiceLifetime.Singleton)]`.
- Creates the editor `World` on startup.
- Holds the `World` reference.
- Disposes the world on editor shutdown.
```csharp
[EditorInjection(ServiceLifetime.Singleton)]
public class EditorWorldService : IDisposable
{
public World EditorWorld { get; }
public EditorWorldService()
{
EditorWorld = World.Create(entityCapacity: 1024);
}
public void Dispose()
{
World.Destroy(EditorWorld.ID);
}
}
```
### 3.2 Scene Graph Sync Strategy
**Incremental sync via polling.** Full rebuild is avoided — existing nodes are matched by `Entity` identity and preserved across syncs. This keeps names, selection state, and expanded/collapsed state intact.
**Algorithm (runs on timer, e.g., every 100ms):**
1. Check `EditorWorld.Version` — if unchanged, skip.
2. Query all entities with `SceneID` component from the editor world.
3. Group by `SceneID.scene.id`.
4. **For each scene group:**
- Find or create the `SceneNode` in `RootNodes` (match by `Scene.ID`).
- Walk the `Hierarchy` linked-list of roots.
- **For each root entity:** find existing `EntityNode` in the tree by `Entity` identity. If found, keep the node (name, expansion, selection intact). If not found, create a new `EntityNode` with default name.
- **Recurse** into `firstChild``nextSibling`, matching existing nodes at each level.
5. **Remove stale nodes:** Any `EntityNode` not matched in step 4 is destroyed (entity no longer exists in world). Remove these nodes (and their subtrees) from the tree.
6. **Update hierarchy links:** If a matched entity's parent changed, move the `EntityNode` to its new parent's `Children` collection.
**Key benefits of incremental sync:**
- `EntityNode.Name` persists naturally — node instances survive.
- No dictionary, no name map.
- Selection and tree expansion state survive across syncs.
- Only affected subtrees change — minimal UI churn.
### 3.3 Incremental Sync Implementation
**New file:** `Editor/Ghost.Editor.Core/Services/SceneGraphSyncService.cs`
```csharp
[EditorInjection(ServiceLifetime.Singleton)]
public class SceneGraphSyncService
{
private readonly EditorWorldService _worldService;
private uint _lastSyncedVersion;
public ObservableCollection<SceneGraphNode> RootNodes { get; } = new();
public void Tick()
{
var currentVersion = _worldService.EditorWorld.Version;
if (currentVersion == _lastSyncedVersion)
return;
_lastSyncedVersion = currentVersion;
SyncScenesAndEntities(_worldService.EditorWorld);
}
private void SyncScenesAndEntities(World world)
{
// 1. Query all entities with SceneID
// 2. Group by scene ID
// 3. For each scene: match/create SceneNode, walk Hierarchy linked-list,
// match/create EntityNode, remove stale nodes, update Children links
}
}
```
### 3.4 Selection → Inspector Wiring
Already partially wired:
- `InspectorService.SetSelected(IInspectable, source)` exists.
- `SceneGraphNode : IInspectable` exists.
- Clicking a `TreeViewItem` sets selection → fires event → calls `SetSelected`.
**To complete:**
- In `Hierarchy.xaml.cs`, handle `TreeView.ItemInvoked` or selection changed.
- Call `InspectorService.SetSelected(node, this)`.
---
## Phase 4: TreeView UI
**Goal:** Replace `ListView` placeholder with a functional `TreeView` bound to the scene graph.
### 4.1 Replace `ListView` with `TreeView`
**File:** `Hierarchy.xaml`
Changes:
- Replace `<ListView>` with `<TreeView>`.
- Bind `ItemsSource` to `RootNodes` from `SceneGraphSyncService`.
- Use a `DataTemplateSelector` that picks `SceneNode.GetSceneHierarchyTemplate()` or `EntityNode.GetSceneHierarchyTemplate()` based on node type.
- Move templates to XAML resources (static `DataTemplate` in `Page.Resources` or a `ResourceDictionary`) instead of `XamlReader.Load()`.
**Template selection:**
```xml
<TreeView ItemsSource="{x:Bind ViewModel.RootNodes, Mode=OneWay}">
<TreeView.ItemTemplateSelector>
<local:SceneGraphTemplateSelector />
</TreeView.ItemTemplateSelector>
</TreeView>
```
### 4.2 Move Templates to XAML Resources
Create a `ResourceDictionary` (or put in `App.xaml` / `Hierarchy.xaml.Resources`):
```xml
<DataTemplate x:Key="SceneNodeTemplate" x:DataType="sg:SceneNode">
<TreeViewItem AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
IsExpanded="True"
ItemsSource="{x:Bind Children, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<FontIcon FontSize="14" Glyph="&#xF156;"/>
<TextBlock Margin="10,0,0,0" Text="{x:Bind Name, Mode=OneWay}"/>
</StackPanel>
</TreeViewItem>
</DataTemplate>
<DataTemplate x:Key="EntityNodeTemplate" x:DataType="sg:EntityNode">
<TreeViewItem AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
ItemsSource="{x:Bind Children, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<FontIcon FontSize="14" Glyph="&#xF158;"/>
<TextBlock Margin="5,0,0,0" Text="{x:Bind Name, Mode=OneWay}"/>
</StackPanel>
</TreeViewItem>
</DataTemplate>
```
### 4.3 DataTemplateSelector
**New file:** `Editor/Ghost.Editor/Views/Controls/SceneGraphTemplateSelector.cs`
```csharp
public class SceneGraphTemplateSelector : DataTemplateSelector
{
public DataTemplate SceneNodeTemplate { get; set; }
public DataTemplate EntityNodeTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item)
{
return item switch
{
SceneNode => SceneNodeTemplate,
EntityNode => EntityNodeTemplate,
_ => base.SelectTemplateCore(item)
};
}
}
```
### 4.4 Remove `GetSceneHierarchyTemplate()` from Node Classes
Once templates are in XAML resources (Phase 4.2), the `GetSceneHierarchyTemplate()` method on `SceneGraphNode` is no longer needed. Remove it from the abstract class and both subclasses. Do NOT remove it in Phase 1 — the method is still referenced until the XAML templates are ready.
### 4.5 Context Menu Support
Add `TreeViewItem.ContextFlyout` or right-click handling for:
- **Scene-level:** Create Entity, Rename Scene, Unload Scene, Save Scene.
- **Entity-level:** Create Child Entity, Delete Entity, Duplicate Entity, Rename Entity.
Create entity commands go through `EntityCommandBuffer` on the editor world.
### 4.6 Search/Filter
Wire the search `TextBox` to filter the TreeView:
- On text change, iterate `RootNodes` recursively.
- If a node or any descendant matches, show it. Otherwise collapse/hide.
- WinUI `TreeView` doesn't have built-in filtering, so implement a filtered copy of the tree or use `Visibility` toggling.
### 4.7 Drag & Drop Reparenting (Stretch Goal)
- Allow dragging an `EntityNode` onto another `EntityNode` to reparent.
- Uses WinUI drag-and-drop APIs.
- Calls `HierarchyUtility.SetParent()` on the editor world.
- Scene graph refreshes via sync.
---
## Phase 5: Serialization (JSON Editor / Binary Runtime)
### 5.1 File Local ID Scheme
When serializing a scene, entities are ordered (index in the list = file-local ID). All `Entity` references within components are serialized as file-local IDs, not global entity IDs.
**Rationale:** Global entity IDs are unpredictable across loads. File-local IDs are deterministic (they are just the list index) and remapped on load.
### 5.2 Serialization Format
**JSON (Editor) — in `Ghost.Editor.Core`:**
```json
{
"name": "MainScene",
"entities": [
{
"components": {
"Ghost.Engine.Components.Hierarchy": {
"parent": -1,
"firstChild": 1,
"nextSibling": -1
},
"Ghost.Engine.Components.LocalToWorld": {
"matrix": { "m00": 1.0, ... }
}
}
},
{
"components": { ... }
}
]
}
```
- Entity index in the array = file-local ID.
- `Entity` references (`parent`, `firstChild`, `nextSibling`) stored as `int` file-local IDs. `-1` = `Entity.Invalid`.
- Component types stored by their stable `FullName` string.
- Uses `System.Text.Json` with reflection (allowed in editor).
**Binary (Runtime) — in `Ghost.Engine`:**
- MemoryPack serialization of the same structure.
- Must be AOT-compatible (use source-generated formatters).
- Components are blittable, so MemoryPack handles them efficiently.
### 5.3 Save Algorithm
1. Get all entities with `SceneID == targetScene` via `SceneManager.GetSceneEntities()`.
2. Sort entities in a deterministic order (e.g., by hierarchy depth-first traversal for deterministic output).
3. Create a `fileLocalID → Entity` map (list index → entity).
4. Create a reverse `Entity → fileLocalID` map.
5. For each entity, serialize all components.
6. For any component field of type `Entity`, replace with the file-local ID (using the reverse map).
7. For `ManagedEntityRef` / script data, serialize via MemoryPack.
8. Write to file.
### 5.4 Load Algorithm
1. Deserialize JSON/binary → list of entity component data.
2. Allocate all entities in the target `World` (no components yet, or minimal archetype).
3. Build `fileLocalID → Entity` map (list index → new global entity).
4. For each entity:
- Add its components to the entity.
- For any `Entity`-typed field, look up the file-local ID in the map and replace with the new global entity.
5. Call `HierarchyUtility` to validate/repair hierarchy invariants if needed.
6. Return the count of loaded entities.
### 5.5 References to Other Scenes
Per the architecture plan, cross-scene references are not supported. If a component references an entity from another scene:
- On save: log a warning and serialize as `-1` (invalid).
- On load: the reference will be `Entity.Invalid`.
### 5.6 File Naming
Scene files use the `.gscene` extension (`g` = GhostEngine):
- `Assets/Scenes/{SceneName}.gscene.json` (editor JSON)
- `Assets/Scenes/{SceneName}.gscene` (runtime binary)
**Scene name resolution:**
- The scene's name derives from the file name (minus extension). E.g., `MyScene.gscene` → name is `"MyScene"`.
- For unsaved/new scenes: default name is `"NewScene"`.
- `SceneNode.Name` is set from the file name on load, and used as the save target on save.
---
## Component Checklist
| Phase | Task | File(s) |
|-------|------|---------|
| **1.1** | Enhance `SceneGraphNode` — add `World`, constructor | `SceneGraphNode.cs` |
| **1.2** | Enhance `SceneNode` — add `Scene` field, constructor | `SceneNode.cs` |
| **1.3** | Enhance `EntityNode` — add constructor, make usable | `EntityNode.cs` |
| **1.4** | Add `SceneGraphBuilder` | New: `SceneGraphBuilder.cs` |
| **1.5** | Entity name strategy (editor-only, no component) | See Section 1.4 |
| **2** | Add `HierarchyUtility` static class | New: `Runtime/.../HierarchyUtility.cs` |
| **3.1** | Add `EditorWorldService` | New: `Editor/.../EditorWorldService.cs` |
| **3.2-3** | Add `SceneGraphSyncService` | New: `Editor/.../SceneGraphSyncService.cs` |
| **4.1** | Replace `ListView` with `TreeView` in XAML | `Hierarchy.xaml` |
| **4.2** | Move templates to XAML resources | `Hierarchy.xaml` |
| **4.3** | Add `SceneGraphTemplateSelector` | New: `Views/Controls/SceneGraphTemplateSelector.cs` |
| **4.4** | Remove `GetSceneHierarchyTemplate()` methods | `SceneGraphNode.cs`, `.cs`, `.cs` |
| **4.5** | Context menu (create/delete entity) | `Hierarchy.xaml.cs` |
| **4.6** | Search/filter | `Hierarchy.xaml.cs` |
| **4.7** | Drag-drop reparenting (stretch) | `Hierarchy.xaml.cs` |
| **5.1-5** | Scene save/load with ID remapping | New files in `Serialization/` |
| **5.5** | MemoryPack source-gen formatters | `Ghost.Engine` |
---
## Resolved Questions
| # | Question | Decision |
|---|----------|----------|
| 1 | Entity name storage | **Editor-only** — names live on `EntityNode.Name` directly. Persist across syncs via incremental matching by `Entity` identity (no dictionary needed). No runtime component. |
| 2 | Orphan behavior on parent destroy | **Cascade destroy** all children recursively. |
| 3 | Remove `GetSceneHierarchyTemplate()` timing | **Phase 4** — keep until XAML templates are in place. |
| 4 | Scene name persistence | **File name** — name = file name minus `.gscene` extension. Unsaved scenes default to `"NewScene"`. |

View File

@@ -1,87 +0,0 @@
# Architecture Plan: Scene Graph and Scene Representation
The Scene Graph is a hierarchical structure that represents all the objects and entities within a 3D scene in the Ghost Editor.
## Scene Graph (Editor representation of runtime data)
There should be three main types of nodes in the Scene Graph for now:
1. **Scene Graph Node**: The base class for all nodes in the Scene Graph.
2. **Entity Node**: Represents an individual entity within a scene. Name stored here, not runtime component.
3. **Scene Node**: Represents a Scene object, which can contain multiple entities. Name stored here not runtime data.
### Editor World
Editor contains a different world compares to the runtime world. When user click the Play button, we will create a runtime world and load the scene data from the editor world to the runtime world.
This allows us to
1. Unload the runtime only systems like physics, rendering, etc when user stop playing.
2. Load editor only systems like gizmos, debug, etc when user stop playing.
3. Allow editor only entities like editor camera, editor lights, etc to exist in the editor world without affecting the runtime world.
### Editor Hierarchy
The Scene Graph should be represented as a tree structure in the editor (TreeView in WinUI 3), where:
- The top level nodes represents the loaded Scenes in the editor world.
- Levels below the Scene nodes represents the Entity nodes that belong to that scene.
- Each Entity node can have child Entity nodes representing parent-child relationships between entities.
An example hierarchy could look like this:
```
- Scene 1
- Entity A
- Entity B
- Entity C
- Scene 2
- Entity D
```
## Scene (The runtime representation)
A Scene is a collection of entities with SceneID component from a world that are grouped together. There can be multiple scenes in a world.
### Save a Scene
When save a scene, all entities with the SceneID component matching the scene's ID should be included in the saved data.
When an Entity references another Entity in the same scene, we should store the file local id instead of the global entity id.
For example, if Entity A (id: 10, 5th in scene) references Entity B (id: 20, 50th in scene) in the same scene, in the saved data for Entity A,
we should store 50 (the file local id) as the reference to Entity B instead of 20 (the global entity id).
> We does not allow cross-scene references for now because ideally it's not a good practice to have cross-scene references.
> We can use query or singleton pattern to access entities from other scenes if needed because they are in the same world.
### Load a Scene
When loading a scene, we need to reconstruct the entities and their relationships based on the saved data.
1. We allocate the entities in the world and assign them new global entity IDs.
2. We remap the file local IDs to the new global entity IDs and change the references accordingly.
For example if Entity A (file local id: 5) references Entity B (file local id: 50) in the saved data,
we need to find the new global entity IDs for both entities after loading and update the reference in Entity A to point to the new global entity ID of Entity B.
### Data format
The scene data should be stored in a structured format (JSON and binary) that includes:
- List of entities with their components and properties (Entities must in the order that file local id directly maps to the index in the list)
- References between entities using file local IDs
> The name of the saved scene file should match the name of the scene node in the editor.
JSON should only be used in the editor and JSON serialization/deserialization logic should also only exist in the editor codebase (Ghost.Editor.Core). Reflection is allowed here.
Binary format should be used in the runtime for better performance. The runtime codebase (Ghost.Engine) must be aot compatible.
Currently we strict the IComponent to must be unmanaged and blittable types.
However, we also support ManagedEntity and ManagedEntityRef with ScriptComponent to allow OOP like logic for common gameplay logic that DOD pattern is not suitable for.
Serializing/deserializing with those components will be tricky. We can use MemoryPack (already installed) for binary serialization/deserialization because it supports both unmanaged and managed types.
## What need to implement
- [ ] Scene type for the runtime representation if needed
- [ ] Scene Graph data structures (SceneNode, EntityNode)
- [ ] Editor World management (loading/unloading scenes, managing entities)
- [ ] Scene saving/loading logic with file local ID remapping
- [ ] Serialization/deserialization logic for scene data (JSON for editor, binary for runtime)
- [ ] UI integration for displaying and managing the Scene Graph in the editor with WinUI 3 TreeView

View File

@@ -1,12 +1,13 @@
using Ghost.Engine.Components; using Ghost.Engine.Components;
using Ghost.Engine.Core; using Ghost.Engine.Core;
using Ghost.Entities; using Ghost.Entities;
using System.Collections.Generic;
namespace Ghost.Editor.Core.SceneGraph; namespace Ghost.Editor.Core.SceneGraph;
public static class SceneGraphBuilder public static class SceneGraphBuilder
{ {
public static List<SceneNode> Build(World world) public static List<SceneNode> Build(World world, Dictionary<Entity, string>? initialNames = null)
{ {
var sceneNodes = new List<SceneNode>(); var sceneNodes = new List<SceneNode>();
var sceneEntities = GroupEntitiesByScene(world); var sceneEntities = GroupEntitiesByScene(world);
@@ -15,7 +16,7 @@ public static class SceneGraphBuilder
{ {
var sceneName = GetDefaultSceneName(scene); var sceneName = GetDefaultSceneName(scene);
var sceneNode = new SceneNode(world, new Scene(scene), sceneName); var sceneNode = new SceneNode(world, new Scene(scene), sceneName);
BuildEntityTree(entities, sceneNode); BuildEntityTree(entities, sceneNode, initialNames);
sceneNodes.Add(sceneNode); sceneNodes.Add(sceneNode);
} }
@@ -33,13 +34,13 @@ public static class SceneGraphBuilder
var entities = chunk.GetEntities(); var entities = chunk.GetEntities();
var scene = chunk.GetSharedComponent<SceneID>(); var scene = chunk.GetSharedComponent<SceneID>();
for (var i = 0; i < chunk.EntityCount; i++)
{
if (scene.value == Scene.INVALID_ID) if (scene.value == Scene.INVALID_ID)
{ {
continue; continue;
} }
for (var i = 0; i < chunk.EntityCount; i++)
{
if (!sceneMap.TryGetValue(scene.value, out var list)) if (!sceneMap.TryGetValue(scene.value, out var list))
{ {
list = new List<Entity>(); list = new List<Entity>();
@@ -53,7 +54,7 @@ public static class SceneGraphBuilder
return sceneMap; return sceneMap;
} }
private static void BuildEntityTree(List<Entity> entities, SceneGraphNode parentNode) private static void BuildEntityTree(List<Entity> entities, SceneGraphNode parentNode, Dictionary<Entity, string>? initialNames = null)
{ {
var entitySet = new HashSet<Entity>(entities); var entitySet = new HashSet<Entity>(entities);
var childrenByParent = new Dictionary<Entity, List<Entity>>(); var childrenByParent = new Dictionary<Entity, List<Entity>>();
@@ -82,13 +83,14 @@ public static class SceneGraphBuilder
foreach (var rootEntity in roots) foreach (var rootEntity in roots)
{ {
var entityNode = new EntityNode(parentNode.World, rootEntity, "Entity"); var name = initialNames != null && initialNames.TryGetValue(rootEntity, out var n) ? n : "Entity";
var entityNode = new EntityNode(parentNode.World, rootEntity, name);
parentNode.Children.Add(entityNode); parentNode.Children.Add(entityNode);
BuildSubtree(entityNode, childrenByParent); BuildSubtree(entityNode, childrenByParent, initialNames);
} }
} }
private static void BuildSubtree(EntityNode parentNode, Dictionary<Entity, List<Entity>> childrenByParent) private static void BuildSubtree(EntityNode parentNode, Dictionary<Entity, List<Entity>> childrenByParent, Dictionary<Entity, string>? initialNames = null)
{ {
if (!childrenByParent.TryGetValue(parentNode.Entity, out var childList)) if (!childrenByParent.TryGetValue(parentNode.Entity, out var childList))
{ {
@@ -100,9 +102,10 @@ public static class SceneGraphBuilder
{ {
foreach (var childEntity in childList) foreach (var childEntity in childList)
{ {
var childNode = new EntityNode(parentNode.World, childEntity, "Entity"); var name = initialNames != null && initialNames.TryGetValue(childEntity, out var n) ? n : "Entity";
var childNode = new EntityNode(parentNode.World, childEntity, name);
parentNode.Children.Add(childNode); parentNode.Children.Add(childNode);
BuildSubtree(childNode, childrenByParent); BuildSubtree(childNode, childrenByParent, initialNames);
} }
return; return;
@@ -113,9 +116,10 @@ public static class SceneGraphBuilder
{ {
if (childList.Contains(sibling)) if (childList.Contains(sibling))
{ {
var childNode = new EntityNode(parentNode.World, sibling, "Entity"); var name = initialNames != null && initialNames.TryGetValue(sibling, out var n) ? n : "Entity";
var childNode = new EntityNode(parentNode.World, sibling, name);
parentNode.Children.Add(childNode); parentNode.Children.Add(childNode);
BuildSubtree(childNode, childrenByParent); BuildSubtree(childNode, childrenByParent, initialNames);
} }
Hierarchy siblingHierarchy = default; Hierarchy siblingHierarchy = default;

View File

@@ -1,14 +1,17 @@
using Ghost.Core;
using Ghost.Editor.Core.SceneGraph; using Ghost.Editor.Core.SceneGraph;
using Ghost.Entities; using Ghost.Entities;
using Ghost.Engine;
using Ghost.Engine.Core; using Ghost.Engine.Core;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
namespace Ghost.Editor.Core.Services; namespace Ghost.Editor.Core.Services;
[EditorInjection(EditorInjectionAttribute.ServiceLifetime.Singleton, typeof(EditorWorldService))]
public class EditorWorldService : IDisposable public class EditorWorldService : IDisposable
{ {
private const int _DEFAULT_ENTITY_CAPACITY = 1024; private const int DEFAULT_ENTITY_CAPACITY = 1024;
public World EditorWorld public World EditorWorld
{ {
@@ -19,30 +22,209 @@ public class EditorWorldService : IDisposable
{ {
get; get;
} = new(); } = new();
public event Action<Entity, string, ushort>? EntityCreated;
public event Action<Entity>? EntityDestroyed;
public event Action<Entity, Entity, Entity>? EntityParentChanged; // (child, oldParent, newParent)
public event Action<Entity, string>? EntityNameChanged;
public event Action? SceneGraphRebuilt;
public EditorWorldService() public EditorWorldService()
{ {
EditorWorld = World.Create(entityCapacity: _DEFAULT_ENTITY_CAPACITY); EditorWorld = World.Create(entityCapacity: DEFAULT_ENTITY_CAPACITY);
}
public Entity CreateEntity(string name, ushort sceneID, Entity parent = default)
{
var entity = EditorWorld.EntityManager.CreateEntity();
EditorWorld.EntityManager.AddComponent(entity, new Engine.Components.Hierarchy
{
parent = Entity.Invalid,
firstChild = Entity.Invalid,
nextSibling = Entity.Invalid
});
EditorWorld.EntityManager.AddSharedComponent(entity, new Engine.Components.SceneID
{
value = sceneID
});
if (parent.IsValid)
{
HierarchyUtility.SetParent(EditorWorld, entity, parent);
}
EditorWorld.AdvanceVersion();
EntityCreated?.Invoke(entity, name, sceneID);
if (parent.IsValid)
{
EntityParentChanged?.Invoke(entity, Entity.Invalid, parent);
}
return entity;
}
public void DestroyEntity(Entity entity)
{
if (!entity.IsValid)
{
return;
}
DestroyEntityRecursive(entity);
EditorWorld.AdvanceVersion();
}
private void DestroyEntityRecursive(Entity entity)
{
if (EditorWorld.EntityManager.HasComponent<Engine.Components.Hierarchy>(entity))
{
ref var hierarchy = ref EditorWorld.EntityManager.GetComponent<Engine.Components.Hierarchy>(entity);
var child = hierarchy.firstChild;
while (child.IsValid)
{
ref var childHierarchy = ref EditorWorld.EntityManager.GetComponent<Engine.Components.Hierarchy>(child);
var next = childHierarchy.nextSibling;
DestroyEntityRecursive(child);
child = next;
}
}
HierarchyUtility.RemoveParent(EditorWorld, entity);
EditorWorld.EntityManager.DestroyEntity(entity);
EntityDestroyed?.Invoke(entity);
}
private void UpdateSceneIDRecursive(Entity entity, ushort sceneID)
{
if (EditorWorld.EntityManager.HasComponent<Engine.Components.SceneID>(entity))
{
EditorWorld.EntityManager.SetSharedComponent(entity, new Engine.Components.SceneID { value = sceneID });
}
if (EditorWorld.EntityManager.HasComponent<Engine.Components.Hierarchy>(entity))
{
ref var hierarchy = ref EditorWorld.EntityManager.GetComponent<Engine.Components.Hierarchy>(entity);
var child = hierarchy.firstChild;
while (child.IsValid)
{
ref var childHierarchy = ref EditorWorld.EntityManager.GetComponent<Engine.Components.Hierarchy>(child);
var next = childHierarchy.nextSibling;
UpdateSceneIDRecursive(child, sceneID);
child = next;
}
}
}
public void ChangeEntityScene(Entity entity, ushort sceneID)
{
if (!entity.IsValid)
{
return;
}
UpdateSceneIDRecursive(entity, sceneID);
EditorWorld.AdvanceVersion();
EntityParentChanged?.Invoke(entity, Entity.Invalid, Entity.Invalid);
}
public Error SetParent(Entity child, Entity parent)
{
if (!child.IsValid)
{
return Error.InvalidArgument;
}
Entity oldParent = Entity.Invalid;
if (EditorWorld.EntityManager.HasComponent<Engine.Components.Hierarchy>(child))
{
oldParent = EditorWorld.EntityManager.GetComponent<Engine.Components.Hierarchy>(child).parent;
}
Error err;
if (parent.IsValid)
{
err = HierarchyUtility.SetParent(EditorWorld, child, parent);
}
else
{
err = HierarchyUtility.RemoveParent(EditorWorld, child);
}
if (err == Error.None)
{
if (parent.IsValid && EditorWorld.EntityManager.HasComponent<Engine.Components.SceneID>(parent))
{
var locRes = EditorWorld.EntityManager.GetEntityLocation(parent);
if (locRes.IsSuccess)
{
ref var archetype = ref EditorWorld.ComponentManager.GetArchetypeReference(locRes.Value.archetypeID);
ref var chunk = ref archetype.GetChunkReference(locRes.Value.chunkIndex);
var chunkView = new ChunkView(in archetype, in chunk);
var parentSceneID = chunkView.GetSharedComponent<Engine.Components.SceneID>().value;
UpdateSceneIDRecursive(child, parentSceneID);
}
}
EditorWorld.AdvanceVersion();
EntityParentChanged?.Invoke(child, oldParent, parent);
}
return err;
}
public Error RemoveParent(Entity child)
{
return SetParent(child, Entity.Invalid);
}
public ushort GetEntitySceneID(Entity entity)
{
if (!entity.IsValid)
{
return Scene.INVALID_ID;
}
if (EditorWorld.EntityManager.HasComponent<Engine.Components.SceneID>(entity))
{
var locRes = EditorWorld.EntityManager.GetEntityLocation(entity);
if (locRes.IsSuccess)
{
ref var archetype = ref EditorWorld.ComponentManager.GetArchetypeReference(locRes.Value.archetypeID);
ref var chunk = ref archetype.GetChunkReference(locRes.Value.chunkIndex);
var chunkView = new ChunkView(in archetype, in chunk);
return chunkView.GetSharedComponent<Engine.Components.SceneID>().value;
}
}
return Scene.INVALID_ID;
}
public void RenameEntity(Entity entity, string newName)
{
if (!entity.IsValid)
{
return;
}
EntityNameChanged?.Invoke(entity, newName);
} }
public void CreateDefaultScene() public void CreateDefaultScene()
{ {
var scene = SceneManager.CreateScene(); var scene = SceneManager.CreateScene();
var entity = EditorWorld.EntityManager.CreateEntity(); CreateEntity("Entity", scene.ID);
EditorWorld.EntityManager.AddSharedComponent(entity, new Engine.Components.SceneID
{
value = scene.ID
});
} }
public void RebuildSceneGraph(Dictionary<Entity, string>? initialNames = null)
public void RebuildSceneGraph()
{ {
RootNodes.Clear(); RootNodes.Clear();
var sceneNodes = SceneGraphBuilder.Build(EditorWorld); var sceneNodes = SceneGraphBuilder.Build(EditorWorld, initialNames);
foreach (var node in sceneNodes) foreach (var node in sceneNodes)
{ {
RootNodes.Add(node); RootNodes.Add(node);
} }
SceneGraphRebuilt?.Invoke();
} }
public void Dispose() public void Dispose()

View File

@@ -1,53 +1,189 @@
using Ghost.Editor.Core.SceneGraph; using Ghost.Editor.Core.SceneGraph;
using Ghost.Engine;
using Ghost.Engine.Components; using Ghost.Engine.Components;
using Ghost.Engine.Core; using Ghost.Engine.Core;
using Ghost.Entities; using Ghost.Entities;
using System;
using System.Collections.Generic;
namespace Ghost.Editor.Core.Services; namespace Ghost.Editor.Core.Services;
[EditorInjection(EditorInjectionAttribute.ServiceLifetime.Singleton, typeof(SceneGraphSyncService))] public class SceneGraphSyncService : IDisposable
public class SceneGraphSyncService
{ {
private readonly EditorWorldService _worldService; private readonly EditorWorldService _worldService;
private uint _lastSyncedVersion; private readonly Dictionary<Entity, EntityNode> _nodeMap = new();
public SceneGraphSyncService(EditorWorldService worldService) public SceneGraphSyncService(EditorWorldService worldService)
{ {
_worldService = worldService; _worldService = worldService;
_worldService.EntityCreated += OnEntityCreated;
_worldService.EntityDestroyed += OnEntityDestroyed;
_worldService.EntityParentChanged += OnEntityParentChanged;
_worldService.EntityNameChanged += OnEntityNameChanged;
_worldService.SceneGraphRebuilt += OnSceneGraphRebuilt;
// Initialize node map from current root nodes
OnSceneGraphRebuilt();
} }
public bool Tick() public bool TryGetNode(Entity entity, out EntityNode node)
{ {
var currentVersion = _worldService.EditorWorld.Version; return _nodeMap.TryGetValue(entity, out node!);
if (currentVersion == _lastSyncedVersion) }
// Keep Tick as an empty stub returning false so we don't break Hierarchy.xaml.cs before we update it
public bool Tick()
{ {
return false; return false;
} }
_lastSyncedVersion = currentVersion; public void Dispose()
SyncScenesAndEntities(_worldService.EditorWorld); {
_worldService.EntityCreated -= OnEntityCreated;
_worldService.EntityDestroyed -= OnEntityDestroyed;
_worldService.EntityParentChanged -= OnEntityParentChanged;
_worldService.EntityNameChanged -= OnEntityNameChanged;
_worldService.SceneGraphRebuilt -= OnSceneGraphRebuilt;
}
private void OnSceneGraphRebuilt()
{
_nodeMap.Clear();
foreach (var sceneNode in _worldService.RootNodes)
{
PopulateNodeMapRecursive(sceneNode);
}
}
private void PopulateNodeMapRecursive(SceneGraphNode node)
{
if (node is EntityNode entityNode)
{
_nodeMap[entityNode.Entity] = entityNode;
}
foreach (var child in node.Children)
{
PopulateNodeMapRecursive(child);
}
}
private void OnEntityCreated(Entity entity, string name, ushort sceneID)
{
if (_nodeMap.ContainsKey(entity))
{
return;
}
var node = new EntityNode(_worldService.EditorWorld, entity, name);
_nodeMap[entity] = node;
// By default, add to the scene's root collection
var sceneNode = FindOrCreateSceneNode(sceneID);
sceneNode.Children.Add(node);
}
private void OnEntityDestroyed(Entity entity)
{
if (!_nodeMap.TryGetValue(entity, out var node))
{
return;
}
// Recursively remove from node map
RemoveNodeAndDescendantsRecursive(node);
// Remove from its parent's Children collection (or from RootNodes if it was a scene's root entity)
RemoveNodeFromParent(node);
}
private void RemoveNodeFromParent(EntityNode node)
{
foreach (var sceneNode in _worldService.RootNodes)
{
if (sceneNode.Children.Remove(node))
{
return;
}
if (RemoveNodeFromChildrenRecursive(sceneNode.Children, node))
{
return;
}
}
}
private bool RemoveNodeFromChildrenRecursive(System.Collections.ObjectModel.ObservableCollection<SceneGraphNode> children, EntityNode target)
{
foreach (var child in children)
{
if (child.Children.Remove(target))
{
return true; return true;
} }
private void SyncScenesAndEntities(World world) if (RemoveNodeFromChildrenRecursive(child.Children, target))
{ {
var sceneEntities = GroupEntitiesByScene(world); return true;
}
foreach (var (scene, entities) in sceneEntities)
{
var sceneNode = FindOrCreateSceneNode(world, scene, _worldService.RootNodes);
SyncEntityTree(sceneNode, entities);
RemoveStaleEntityNodes(sceneNode, entities);
} }
var activeScenes = new HashSet<ushort>(sceneEntities.Keys); return false;
RemoveStaleSceneNodes(_worldService.RootNodes, activeScenes);
} }
private static SceneNode FindOrCreateSceneNode(World world, ushort sceneID, System.Collections.ObjectModel.ObservableCollection<SceneNode> rootNodes) private void RemoveNodeAndDescendantsRecursive(EntityNode node)
{ {
foreach (var existing in rootNodes) _nodeMap.Remove(node.Entity);
foreach (var child in node.Children)
{
if (child is EntityNode childEntityNode)
{
RemoveNodeAndDescendantsRecursive(childEntityNode);
}
}
}
private void OnEntityParentChanged(Entity child, Entity oldParent, Entity newParent)
{
if (!_nodeMap.TryGetValue(child, out var childNode))
{
return;
}
// Remove from the old parent collection (wherever it currently is)
RemoveNodeFromParent(childNode);
// Add to the new parent collection (prepend at index 0 to match HierarchyUtility firstChild behavior)
if (newParent.IsValid && _nodeMap.TryGetValue(newParent, out var newParentNode))
{
newParentNode.Children.Insert(0, childNode);
}
else
{
// Add to the scene's root collection
if (_worldService.EditorWorld.EntityManager.HasComponent<SceneID>(child))
{
var sceneID = _worldService.GetEntitySceneID(child);
if (sceneID != Scene.INVALID_ID)
{
var sceneNode = FindOrCreateSceneNode(sceneID);
sceneNode.Children.Insert(0, childNode);
}
}
}
}
private void OnEntityNameChanged(Entity entity, string newName)
{
if (_nodeMap.TryGetValue(entity, out var node))
{
node.Name = newName;
}
}
private SceneNode FindOrCreateSceneNode(ushort sceneID)
{
foreach (var existing in _worldService.RootNodes)
{ {
if (existing.Scene.ID == sceneID) if (existing.Scene.ID == sceneID)
{ {
@@ -56,168 +192,8 @@ public class SceneGraphSyncService
} }
var sceneName = $"NewScene ({sceneID})"; var sceneName = $"NewScene ({sceneID})";
var newSceneNode = new SceneNode(world, new Scene(sceneID), sceneName); var newSceneNode = new SceneNode(_worldService.EditorWorld, new Scene(sceneID), sceneName);
rootNodes.Add(newSceneNode); _worldService.RootNodes.Add(newSceneNode);
return newSceneNode; return newSceneNode;
} }
private void SyncEntityTree(SceneGraphNode parentNode, List<Entity> entities)
{
var entitySet = new HashSet<Entity>(entities);
var children = new Dictionary<Entity, List<Entity>>();
var roots = new List<Entity>();
foreach (var entity in entities)
{
Hierarchy hierarchy = default;
var hasHierarchy = TryGetHierarchyComponent(parentNode.World, entity, ref hierarchy);
if (hasHierarchy && hierarchy.parent.IsValid && entitySet.Contains(hierarchy.parent))
{
if (!children.TryGetValue(hierarchy.parent, out var list))
{
list = new List<Entity>();
children[hierarchy.parent] = list;
}
list.Add(entity);
}
else
{
roots.Add(entity);
}
}
SyncExistingNodes(parentNode, roots, children);
}
private static void SyncExistingNodes(SceneGraphNode parentNode, List<Entity> roots, Dictionary<Entity, List<Entity>> children)
{
var existingNodeMap = new Dictionary<Entity, EntityNode>();
for (var i = parentNode.Children.Count - 1; i >= 0; i--)
{
if (parentNode.Children[i] is EntityNode entityNode)
{
if (!roots.Contains(entityNode.Entity) && !children.ContainsKey(entityNode.Entity))
{
parentNode.Children.RemoveAt(i);
}
else
{
existingNodeMap[entityNode.Entity] = entityNode;
}
}
}
for (var i = 0; i < roots.Count; i++)
{
var entity = roots[i];
if (existingNodeMap.TryGetValue(entity, out var existingNode))
{
existingNodeMap.Remove(entity);
if (i >= parentNode.Children.Count || parentNode.Children[i] != existingNode)
{
parentNode.Children.Remove(existingNode);
parentNode.Children.Insert(i, existingNode);
}
}
else
{
var newNode = new EntityNode(parentNode.World, entity, "Entity");
parentNode.Children.Insert(i, newNode);
existingNode = newNode;
}
if (children.TryGetValue(entity, out var childList))
{
SyncExistingNodes(existingNode, childList, children);
}
else
{
for (var j = existingNode.Children.Count - 1; j >= 0; j--)
{
if (existingNode.Children[j] is EntityNode)
{
existingNode.Children.RemoveAt(j);
}
}
}
}
}
private static void RemoveStaleEntityNodes(SceneGraphNode parentNode, List<Entity> entities)
{
var entitySet = new HashSet<Entity>(entities);
for (var i = parentNode.Children.Count - 1; i >= 0; i--)
{
if (parentNode.Children[i] is EntityNode entityNode && !entitySet.Contains(entityNode.Entity))
{
parentNode.Children.RemoveAt(i);
}
}
}
private static void RemoveStaleSceneNodes(System.Collections.ObjectModel.ObservableCollection<SceneNode> rootNodes, HashSet<ushort> activeScenes)
{
for (var i = rootNodes.Count - 1; i >= 0; i--)
{
if (!activeScenes.Contains(rootNodes[i].Scene.ID))
{
rootNodes.RemoveAt(i);
}
}
}
private static Dictionary<ushort, List<Entity>> GroupEntitiesByScene(World world)
{
var sceneMap = new Dictionary<ushort, List<Entity>>();
var queryID = new QueryBuilder().WithAll<SceneID>().Build(world);
ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);
foreach (var chunk in query.GetChunkIterator())
{
var entities = chunk.GetEntities();
var scene = chunk.GetSharedComponent<SceneID>();
for (var i = 0; i < chunk.EntityCount; i++)
{
if (scene.value == Scene.INVALID_ID)
{
continue;
}
if (!sceneMap.TryGetValue(scene.value, out var list))
{
list = new List<Entity>();
sceneMap[scene.value] = list;
}
list.Add(entities[i]);
}
}
return sceneMap;
}
private static unsafe bool TryGetHierarchyComponent(World world, Entity entity, ref Hierarchy hierarchy)
{
var location = world.EntityManager.GetEntityLocation(entity);
if (!location.IsSuccess)
{
return false;
}
ref var archetype = ref world.ComponentManager.GetArchetypeReference(location.Value.archetypeID);
var hierarchyID = ComponentTypeID<Hierarchy>.Value;
var pData = archetype.GetComponentData(location.Value.chunkIndex, location.Value.rowIndex, hierarchyID);
if (pData == null)
{
return false;
}
hierarchy = *(Hierarchy*)pData;
return true;
}
} }

View File

@@ -32,13 +32,18 @@ internal sealed class SceneSaveData
internal sealed class EntitySaveData internal sealed class EntitySaveData
{ {
public string Name
{
get; set;
} = "Entity";
public Dictionary<string, JsonElement> Components public Dictionary<string, JsonElement> Components
{ {
get; set; get; set;
} = new(); } = new();
} }
[EditorInjection(EditorInjectionAttribute.ServiceLifetime.Singleton, typeof(SceneSerializationService))] // TODO: Serialize shared components.
internal class SceneSerializationService : IDisposable internal class SceneSerializationService : IDisposable
{ {
private static readonly Dictionary<Type, FieldInfo[]> s_entityFieldsCache = new(); private static readonly Dictionary<Type, FieldInfo[]> s_entityFieldsCache = new();
@@ -66,11 +71,13 @@ internal class SceneSerializationService : IDisposable
private readonly EditorWorldService _worldService; private readonly EditorWorldService _worldService;
private readonly IAssetRegistry _assetRegistry; private readonly IAssetRegistry _assetRegistry;
private readonly SceneGraphSyncService _syncService;
public SceneSerializationService(EditorWorldService worldService, IAssetRegistry assetRegistry) public SceneSerializationService(EditorWorldService worldService, IAssetRegistry assetRegistry, SceneGraphSyncService syncService)
{ {
_worldService = worldService; _worldService = worldService;
_assetRegistry = assetRegistry; _assetRegistry = assetRegistry;
_syncService = syncService;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -84,11 +91,6 @@ internal class SceneSerializationService : IDisposable
return -1; return -1;
} }
private static bool IsEntityType(Type type)
{
return type == typeof(Entity);
}
private static FieldInfo[] GetEntityFields(Type type) private static FieldInfo[] GetEntityFields(Type type)
{ {
if (!s_entityFieldsCache.TryGetValue(type, out var fields)) if (!s_entityFieldsCache.TryGetValue(type, out var fields))
@@ -96,7 +98,7 @@ internal class SceneSerializationService : IDisposable
var list = new List<FieldInfo>(); var list = new List<FieldInfo>();
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
{ {
if (IsEntityType(field.FieldType)) if (field.FieldType == typeof(Entity))
{ {
list.Add(field); list.Add(field);
} }
@@ -266,6 +268,11 @@ internal class SceneSerializationService : IDisposable
{ {
var entityData = new EntitySaveData(); var entityData = new EntitySaveData();
if (entityElement.TryGetProperty("name", out var nameElement))
{
entityData.Name = nameElement.GetString() ?? "Entity";
}
if (entityElement.TryGetProperty("components", out var componentsElement)) if (entityElement.TryGetProperty("components", out var componentsElement))
{ {
foreach (var componentProperty in componentsElement.EnumerateObject()) foreach (var componentProperty in componentsElement.EnumerateObject())
@@ -296,13 +303,12 @@ internal class SceneSerializationService : IDisposable
var activeScene = SceneManager.CreateScene(); var activeScene = SceneManager.CreateScene();
var entityCount = data.Entities.Count; var entityCount = data.Entities.Count;
var forwardMap = new Dictionary<int, Entity>(entityCount);
if (entityCount == 0) if (entityCount == 0)
{ {
goto RebuildAndReturn; goto RebuildAndReturn;
} }
var forwardMap = new Dictionary<int, Entity>(entityCount);
var scope = AllocationManager.CreateStackScope(); var scope = AllocationManager.CreateStackScope();
var typeIds = new UnsafeArray<UnsafeList<Identifier<IComponent>>>(entityCount, scope.AllocationHandle); var typeIds = new UnsafeArray<UnsafeList<Identifier<IComponent>>>(entityCount, scope.AllocationHandle);
for (var i = 0; i < typeIds.Length; i++) for (var i = 0; i < typeIds.Length; i++)
@@ -352,12 +358,21 @@ internal class SceneSerializationService : IDisposable
world.EntityManager.SetSharedComponent(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];
var idx = 1;
foreach (var (typeName, componentElement) in entityData.Components) foreach (var (typeName, componentElement) in entityData.Components)
{ {
var compId = list[idx++]; var compId = ComponentRegistry.GetComponentIDByName(typeName);
if (compId.IsInvalid)
{
var type = TypeCache.GetTypes().FirstOrDefault(t => t.FullName == typeName);
if (type == null)
{
continue;
}
compId = ComponentRegistry.GetComponentIDByName(typeName);
}
if (compId.IsInvalid) if (compId.IsInvalid)
{ {
continue; continue;
@@ -392,7 +407,15 @@ internal class SceneSerializationService : IDisposable
} }
RebuildAndReturn: RebuildAndReturn:
_worldService.RebuildSceneGraph(); var initialNames = new Dictionary<Entity, string>();
for (var fileIndex = 0; fileIndex < entityCount; fileIndex++)
{
if (forwardMap.TryGetValue(fileIndex, out var entity))
{
initialNames[entity] = data.Entities[fileIndex].Name;
}
}
_worldService.RebuildSceneGraph(initialNames);
return activeScene; return activeScene;
} }
@@ -465,6 +488,14 @@ internal class SceneSerializationService : IDisposable
ref var archetype = ref world.ComponentManager.GetArchetypeReference(location.archetypeID); ref var archetype = ref world.ComponentManager.GetArchetypeReference(location.archetypeID);
writer.WriteStartObject(); writer.WriteStartObject();
var entityName = "Entity";
if (_syncService != null && _syncService.TryGetNode(entity, out var node))
{
entityName = node.Name;
}
writer.WriteString("name", entityName);
writer.WriteStartObject("components"); writer.WriteStartObject("components");
foreach (var layout in archetype._layouts) foreach (var layout in archetype._layouts)

View File

@@ -0,0 +1,37 @@
using System.Runtime.CompilerServices;
namespace Ghost.Editor.Core.Utilities;
public static class PathUtility
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Normalize(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
return Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetUniqueName(string path)
{
var directory = Path.GetDirectoryName(path);
directory ??= ".";
var fileName = Path.GetFileNameWithoutExtension(path);
var extension = Path.GetExtension(path);
var uniqueName = fileName;
var counter = 1;
while (File.Exists(Path.Combine(directory, uniqueName + extension)))
{
uniqueName = $"{fileName} ({counter++})";
}
return Path.Combine(directory, uniqueName + extension);
}
}

View File

@@ -85,12 +85,6 @@ public static class TypeCache
return dict; return dict;
} }
internal static void Initialize()
{
// Intentionally left blank.
// This method exists to force the static constructor to run.
}
internal static void Reload() internal static void Reload()
{ {
s_types = LoadTypes(); s_types = LoadTypes();

View File

@@ -53,8 +53,6 @@ public partial class App : Application
{ {
InitializeComponent(); InitializeComponent();
TypeCache.Initialize();
Host = Microsoft.Extensions.Hosting.Host. Host = Microsoft.Extensions.Hosting.Host.
CreateDefaultBuilder(). CreateDefaultBuilder().
UseContentRoot(AppContext.BaseDirectory). UseContentRoot(AppContext.BaseDirectory).
@@ -67,38 +65,17 @@ public partial class App : Application
services.AddSingleton<IInspectorService, InspectorService>(); services.AddSingleton<IInspectorService, InspectorService>();
services.AddSingleton<IPreviewService, PreviewService>(); services.AddSingleton<IPreviewService, PreviewService>();
services.AddSingleton<IAssetRegistry, AssetRegistry>(); services.AddSingleton<IAssetRegistry, AssetRegistry>();
services.AddSingleton<EditorWorldService>();
services.AddSingleton<SceneSerializationService>();
services.AddSingleton<SceneGraphSyncService>();
services.AddSingleton<IContentProvider, EditorContentProvider>(); services.AddSingleton<IContentProvider, EditorContentProvider>();
services.AddSingleton<IShaderCompilationBridge, EditorShaderCompilerBridge>(); services.AddSingleton<IShaderCompilationBridge, EditorShaderCompilerBridge>();
services.AddSingleton<EngineEditorViewModel>(); services.AddSingleton<EngineEditorViewModel>();
services.AddTransient<ContentBrowserViewModel>(); services.AddTransient<ContentBrowserViewModel>();
// TODO: Use source generators to generate this code at compile time instead of using reflection at runtime.
foreach (var type in TypeCache.GetTypes())
{
var data = type.GetCustomAttributesData().FirstOrDefault(a => a.AttributeType == typeof(EditorInjectionAttribute));
if (data is null)
{
continue;
}
var lifeTime = (EditorInjectionAttribute.ServiceLifetime)data.ConstructorArguments[0].Value!;
var implementationType = (Type)data.ConstructorArguments[1].Value!;
var serviceType = type.IsInterface ? type.AsType() : implementationType;
switch (lifeTime)
{
case EditorInjectionAttribute.ServiceLifetime.Singleton:
services.AddSingleton(serviceType, implementationType);
break;
case EditorInjectionAttribute.ServiceLifetime.Transient:
services.AddTransient(serviceType, implementationType);
break;
default:
break;
}
}
}) })
.Build(); .Build();

View File

@@ -38,10 +38,10 @@
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" /> <PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" /> <PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" /> <PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.7" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.8" />
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" /> <PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.0.1" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="2.1.3" />
<PackageReference Include="WinUIEx" Version="2.9.0" /> <PackageReference Include="WinUIEx" Version="2.9.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,11 +1,11 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Core.Utilities;
using Ghost.Editor.Core; using Ghost.Editor.Core;
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.Editor.Models; using Ghost.Editor.Models;
using Ghost.Engine; using Ghost.Engine;
using Ghost.Engine.Streaming;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;

View File

@@ -1,4 +1,7 @@
using Ghost.Editor.Core; using Ghost.Editor.Core;
using Ghost.Editor.Core.Services;
using Ghost.Editor.Core.Utilities;
using Ghost.Engine.Core;
namespace Ghost.Editor.Views.Controls; namespace Ghost.Editor.Views.Controls;
@@ -54,5 +57,24 @@ internal partial class ContentBrowser
[ContextMenuItem("project-browser", "Create/Asset/Scene")] [ContextMenuItem("project-browser", "Create/Asset/Scene")]
private static void CreateSceneAsset() private static void CreateSceneAsset()
{ {
var viewModel = LastFocused?.ViewModel;
if (viewModel is null)
{
return;
}
var currentDir = viewModel.CurrentDirectoryPath;
if (!Directory.Exists(currentDir))
{
return;
}
var newScenePath = PathUtility.GetUniqueName(Path.Combine(currentDir, "New Scene.gscene"));
var tempScene = SceneManager.CreateScene();
var sceneSerializationService = App.GetService<SceneSerializationService>();
sceneSerializationService.SaveSceneFromEditorWorld(newScenePath, tempScene);
SceneManager.DestroyScene(tempScene, App.GetService<EditorWorldService>().EditorWorld);
} }
} }

View File

@@ -47,13 +47,13 @@ internal sealed partial class ContentBrowser : UserControl
private void ProjectBrowser_Loaded(object sender, RoutedEventArgs e) private void ProjectBrowser_Loaded(object sender, RoutedEventArgs e)
{ {
_inspectorService.OnSelectionChanged += _inspectorService_OnSelectionChanged; //_inspectorService.OnSelectionChanged += _inspectorService_OnSelectionChanged;
GettingFocus += ProjectBrowser_GettingFocus; GettingFocus += ProjectBrowser_GettingFocus;
} }
private void ProjectBrowser_Unloaded(object sender, RoutedEventArgs e) private void ProjectBrowser_Unloaded(object sender, RoutedEventArgs e)
{ {
_inspectorService.OnSelectionChanged -= _inspectorService_OnSelectionChanged; //_inspectorService.OnSelectionChanged -= _inspectorService_OnSelectionChanged;
GettingFocus -= ProjectBrowser_GettingFocus; GettingFocus -= ProjectBrowser_GettingFocus;
if (LastFocused == this) if (LastFocused == this)
@@ -62,14 +62,14 @@ internal sealed partial class ContentBrowser : UserControl
} }
} }
private void _inspectorService_OnSelectionChanged(object? sender, InspectorSelectionChangedEventArgs e) //private void _inspectorService_OnSelectionChanged(object? sender, InspectorSelectionChangedEventArgs e)
{ //{
if (e.Source is not ContentBrowserViewModel) // if (e.Source is not ContentBrowserViewModel)
{ // {
PART_FilesView.DeselectAll(); // PART_FilesView.DeselectAll();
PART_DirectoriesView.SelectedNodes.Clear(); // PART_DirectoriesView.SelectedNodes.Clear();
} // }
} //}
private void PART_DirectoriesView_SelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args) private void PART_DirectoriesView_SelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args)
{ {
@@ -80,7 +80,7 @@ internal sealed partial class ContentBrowser : UserControl
_isUpdatingSelection = true; _isUpdatingSelection = true;
PART_FilesView.DeselectAll(); //PART_FilesView.DeselectAll();
if (args.AddedItems.Count > 0 && args.AddedItems[0] is ExplorerItem selectedItem) if (args.AddedItems.Count > 0 && args.AddedItems[0] is ExplorerItem selectedItem)
{ {
ViewModel.SelectedItem = selectedItem; ViewModel.SelectedItem = selectedItem;
@@ -99,7 +99,7 @@ internal sealed partial class ContentBrowser : UserControl
_isUpdatingSelection = true; _isUpdatingSelection = true;
PART_DirectoriesView.SelectedNodes.Clear(); //PART_DirectoriesView.SelectedNodes.Clear();
if (PART_FilesView.SelectedItem is ExplorerItem selectedItem) if (PART_FilesView.SelectedItem is ExplorerItem selectedItem)
{ {
ViewModel.SelectedItem = selectedItem; ViewModel.SelectedItem = selectedItem;

View File

@@ -20,6 +20,11 @@
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}" AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
IsExpanded="True" IsExpanded="True"
ItemsSource="{x:Bind Children, Mode=OneWay}"> ItemsSource="{x:Bind Children, Mode=OneWay}">
<TreeViewItem.ContextFlyout>
<MenuFlyout>
<MenuFlyoutItem Text="Create Entity" Click="OnCreateEntityClick" />
</MenuFlyout>
</TreeViewItem.ContextFlyout>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<FontIcon FontSize="14" Glyph="&#xF156;" /> <FontIcon FontSize="14" Glyph="&#xF156;" />
<TextBlock Margin="10,0,0,0" Text="{x:Bind Name, Mode=OneWay}" /> <TextBlock Margin="10,0,0,0" Text="{x:Bind Name, Mode=OneWay}" />
@@ -31,6 +36,12 @@
<TreeViewItem <TreeViewItem
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}" AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
ItemsSource="{x:Bind Children, Mode=OneWay}"> ItemsSource="{x:Bind Children, Mode=OneWay}">
<TreeViewItem.ContextFlyout>
<MenuFlyout>
<MenuFlyoutItem Text="Create Child" Click="OnCreateChildClick" />
<MenuFlyoutItem Text="Delete" Click="OnDeleteEntityClick" />
</MenuFlyout>
</TreeViewItem.ContextFlyout>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<FontIcon FontSize="14" Glyph="&#xF158;" /> <FontIcon FontSize="14" Glyph="&#xF158;" />
<TextBlock Margin="5,0,0,0" Text="{x:Bind Name, Mode=OneWay}" /> <TextBlock Margin="5,0,0,0" Text="{x:Bind Name, Mode=OneWay}" />
@@ -92,6 +103,13 @@
Grid.Row="1" Grid.Row="1"
Padding="4,2,0,2" Padding="4,2,0,2"
ItemTemplateSelector="{StaticResource SceneGraphTemplateSelector}" ItemTemplateSelector="{StaticResource SceneGraphTemplateSelector}"
SelectionMode="Single" /> SelectionMode="Single"
CanDragItems="True"
CanReorderItems="True"
AllowDrop="True"
DragItemsStarting="OnTreeViewDragItemsStarting"
DragOver="OnTreeViewDragOver"
Drop="OnTreeViewDrop"
KeyDown="OnTreeViewKeyDown" />
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -1,23 +1,29 @@
using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Services; using Ghost.Editor.Core.Services;
using Microsoft.UI.Dispatching; using Ghost.Editor.Core.SceneGraph;
using Ghost.Entities;
using Ghost.Engine;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using System;
namespace Ghost.Editor.Views.Controls; namespace Ghost.Editor.Views.Controls;
public sealed partial class Hierarchy : UserControl public sealed partial class Hierarchy : UserControl
{ {
private readonly SceneGraphSyncService _syncService;
private readonly IInspectorService _inspectorService; private readonly IInspectorService _inspectorService;
private readonly SceneGraphSyncService _syncService;
private readonly EditorWorldService _worldService; private readonly EditorWorldService _worldService;
private DispatcherQueueTimer? _syncTimer; private EntityNode? _draggedNode;
public Hierarchy() public Hierarchy()
{ {
InitializeComponent(); InitializeComponent();
_syncService = App.GetService<SceneGraphSyncService>();
_inspectorService = App.GetService<IInspectorService>(); _inspectorService = App.GetService<IInspectorService>();
_syncService = App.GetService<SceneGraphSyncService>();
_worldService = App.GetService<EditorWorldService>(); _worldService = App.GetService<EditorWorldService>();
SceneTreeView.ItemsSource = _worldService.RootNodes; SceneTreeView.ItemsSource = _worldService.RootNodes;
@@ -25,21 +31,9 @@ public sealed partial class Hierarchy : UserControl
SceneTreeView.ItemInvoked += OnTreeViewItemInvoked; SceneTreeView.ItemInvoked += OnTreeViewItemInvoked;
SceneTreeView.SelectionChanged += OnTreeViewSelectionChanged; SceneTreeView.SelectionChanged += OnTreeViewSelectionChanged;
_syncTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
_syncTimer.Interval = TimeSpan.FromMilliseconds(100);
_syncTimer.Tick += OnSyncTick;
_syncTimer.Start();
Unloaded += OnUnloaded; Unloaded += OnUnloaded;
} }
private void OnSyncTick(DispatcherQueueTimer sender, object args)
{
if (_syncService.Tick())
{
}
}
private void OnTreeViewItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args) private void OnTreeViewItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args)
{ {
if (args.InvokedItem is IInspectable inspectable) if (args.InvokedItem is IInspectable inspectable)
@@ -52,9 +46,150 @@ public sealed partial class Hierarchy : UserControl
{ {
} }
private void OnUnloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) private void OnTreeViewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == global::Windows.System.VirtualKey.Delete)
{
if (SceneTreeView.SelectedItem is EntityNode entityNode)
{
_worldService.DestroyEntity(entityNode.Entity);
e.Handled = true;
}
}
}
private void OnTreeViewDragItemsStarting(TreeView sender, TreeViewDragItemsStartingEventArgs args)
{
if (args.Items.Count > 0 && args.Items[0] is EntityNode entityNode)
{
_draggedNode = entityNode;
}
else
{
_draggedNode = null;
}
}
private void OnTreeViewDragOver(object sender, DragEventArgs e)
{
e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.None;
if (_draggedNode == null)
{
return;
}
var targetItem = GetAncestorTreeViewItem(e.OriginalSource as DependencyObject);
if (targetItem == null)
{
return;
}
var targetNode = targetItem.DataContext as SceneGraphNode;
if (targetNode == null)
{
return;
}
// 1. Can't drag onto itself
if (_draggedNode == targetNode)
{
return;
}
// 2. Can't drag onto a child of itself (cycle checking)
if (targetNode is EntityNode targetEntityNode)
{
if (HierarchyUtility.IsAncestor(_worldService.EditorWorld, targetEntityNode.Entity, _draggedNode.Entity))
{
return;
}
}
e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
}
private void OnTreeViewDrop(object sender, DragEventArgs e)
{
if (_draggedNode == null)
{
return;
}
var targetItem = GetAncestorTreeViewItem(e.OriginalSource as DependencyObject);
if (targetItem == null)
{
return;
}
var targetNode = targetItem.DataContext as SceneGraphNode;
if (targetNode == null)
{
return;
}
if (_draggedNode == targetNode)
{
return;
}
if (targetNode is EntityNode targetEntityNode)
{
if (!HierarchyUtility.IsAncestor(_worldService.EditorWorld, targetEntityNode.Entity, _draggedNode.Entity))
{
_worldService.SetParent(_draggedNode.Entity, targetEntityNode.Entity);
}
}
else if (targetNode is SceneNode sceneNode)
{
_worldService.RemoveParent(_draggedNode.Entity);
_worldService.ChangeEntityScene(_draggedNode.Entity, sceneNode.Scene.ID);
}
}
private void OnCreateEntityClick(object sender, RoutedEventArgs e)
{
if (sender is MenuFlyoutItem menuItem && menuItem.DataContext is SceneNode sceneNode)
{
_worldService.CreateEntity("Entity", sceneNode.Scene.ID);
}
}
private void OnCreateChildClick(object sender, RoutedEventArgs e)
{
if (sender is MenuFlyoutItem menuItem && menuItem.DataContext is EntityNode entityNode)
{
var sceneID = _worldService.GetEntitySceneID(entityNode.Entity);
if (sceneID != Ghost.Engine.Core.Scene.INVALID_ID)
{
_worldService.CreateEntity("Entity", sceneID, parent: entityNode.Entity);
}
}
}
private void OnDeleteEntityClick(object sender, RoutedEventArgs e)
{
if (sender is MenuFlyoutItem menuItem && menuItem.DataContext is EntityNode entityNode)
{
_worldService.DestroyEntity(entityNode.Entity);
}
}
private TreeViewItem? GetAncestorTreeViewItem(DependencyObject? current)
{
while (current != null)
{
if (current is TreeViewItem item)
{
return item;
}
current = VisualTreeHelper.GetParent(current);
}
return null;
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{ {
_syncTimer?.Stop();
SceneTreeView.ItemInvoked -= OnTreeViewItemInvoked; SceneTreeView.ItemInvoked -= OnTreeViewItemInvoked;
SceneTreeView.SelectionChanged -= OnTreeViewSelectionChanged; SceneTreeView.SelectionChanged -= OnTreeViewSelectionChanged;
Unloaded -= OnUnloaded; Unloaded -= OnUnloaded;

View File

@@ -1,17 +0,0 @@
using System.Runtime.CompilerServices;
namespace Ghost.Core.Utilities;
public static class PathUtility
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Normalize(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
return Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
}

View File

@@ -1,5 +1,6 @@
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts; using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -66,7 +67,8 @@ public static class StreamUtility
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static MemoryBlock ReadMemory(this Stream stream, long length, AllocationHandle allocationHandle) public static MemoryBlock ReadMemory(this Stream stream, long length, AllocationHandle allocationHandle)
{ {
var memory = new MemoryBlock((nuint)length, 16, allocationHandle); var alignedLength = MemoryUtility.AlignUp((nuint)length, 16);
var memory = new MemoryBlock(alignedLength, 16, allocationHandle);
// C# built-in collections use int for indexing, so we need to ensure that the buffer size does not exceed int.MaxValue // C# built-in collections use int for indexing, so we need to ensure that the buffer size does not exceed int.MaxValue
var maxChunkSize = (int)Math.Min(0x7fffffffL, length); var maxChunkSize = (int)Math.Min(0x7fffffffL, length);

View File

@@ -5,6 +5,7 @@ using Ghost.Engine.Streaming;
using Ghost.Entities; using Ghost.Entities;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using System.Collections.Concurrent;
using System.Text; using System.Text;
namespace Ghost.Engine.Core; namespace Ghost.Engine.Core;
@@ -106,15 +107,418 @@ public class LoadedSceneData : IDisposable
} }
} }
public enum SceneLoadStatus
{
Queued = 0,
WaitingForDependencies = 1,
Parsing = 2,
WaitingForMaterialization = 3,
Materializing = 4,
Completed = 5,
Failed = 6,
Canceled = 7,
}
public struct SceneLoadOptions
{
public bool DeferMaterialization;
public int MaxEntitiesPerFrame;
public int Priority;
public readonly bool AutoMaterialize => !DeferMaterialization;
}
public readonly struct SceneMaterializeBudget
{
public readonly int MaxScenes;
public readonly int MaxEntities;
public SceneMaterializeBudget(int maxEntities, int maxScenes = 0)
{
MaxEntities = maxEntities;
MaxScenes = maxScenes;
}
public static SceneMaterializeBudget Unlimited => new(int.MaxValue, int.MaxValue);
}
public sealed class SceneLoadOperation
{
private readonly PendingSceneLoad _pendingLoad;
internal PendingSceneLoad PendingLoad => _pendingLoad;
public SceneLoadStatus Status => _pendingLoad.Status;
public float Progress => _pendingLoad.Progress;
public Scene Scene => _pendingLoad.Scene;
public string? ErrorMessage => _pendingLoad.ErrorMessage;
public bool IsParsed => _pendingLoad.Status >= SceneLoadStatus.WaitingForMaterialization && _pendingLoad.Status < SceneLoadStatus.Failed;
public bool IsMaterialized => _pendingLoad.Status == SceneLoadStatus.Completed;
public bool IsCompleted => _pendingLoad.Status is SceneLoadStatus.Completed or SceneLoadStatus.Failed or SceneLoadStatus.Canceled;
internal SceneLoadOperation(PendingSceneLoad pendingLoad)
{
_pendingLoad = pendingLoad;
}
public void Cancel()
{
_pendingLoad.Cancel();
}
}
internal sealed class PendingSceneLoad : IDisposable
{
private enum MaterializePhase
{
NotStarted = 0,
CreateEntities = 1,
SetComponents = 2,
RemapEntityReferences = 3,
Completed = 4,
}
private readonly AssetEntry _sceneEntry;
private readonly SceneLoadOptions _options;
private LoadedSceneData? _loadedData;
private UnsafeArray<Entity> _fileLocalToRuntimeEntity;
private MaterializePhase _phase;
private int _nextCreateIndex;
private int _nextSetComponentIndex;
private int _nextRemapIndex;
private int _status;
private int _releasedSceneEntry;
private bool _singleResetApplied;
private bool _disposed;
public World World { get; }
public AssetRef<Scene> SceneAsset { get; }
public SceneLoadingType LoadingType { get; }
public Scene Scene { get; private set; }
public SceneLoadOptions Options => _options;
public SceneLoadStatus Status => (SceneLoadStatus)Volatile.Read(ref _status);
public string? ErrorMessage { get; private set; }
public bool IsTerminal => Status is SceneLoadStatus.Completed or SceneLoadStatus.Failed or SceneLoadStatus.Canceled;
public float Progress
{
get
{
var status = Status;
if (status == SceneLoadStatus.Completed)
{
return 1.0f;
}
var loadedData = _loadedData;
if (loadedData == null || !loadedData.entities.IsCreated || loadedData.entities.Length == 0)
{
return status switch
{
SceneLoadStatus.Queued => 0.0f,
SceneLoadStatus.WaitingForDependencies => 0.1f,
SceneLoadStatus.Parsing => 0.25f,
SceneLoadStatus.WaitingForMaterialization => 0.5f,
SceneLoadStatus.Materializing => 0.75f,
_ => 0.0f,
};
}
var entityCount = loadedData.entities.Length;
var completedEntities = Math.Min(entityCount * 3, _nextCreateIndex + _nextSetComponentIndex + _nextRemapIndex);
return 0.5f + (0.5f * completedEntities / (entityCount * 3));
}
}
public PendingSceneLoad(World world, AssetRef<Scene> sceneAsset, SceneLoadingType loadingType, SceneLoadOptions options, AssetEntry sceneEntry)
{
World = world;
SceneAsset = sceneAsset;
LoadingType = loadingType;
_options = options;
_sceneEntry = sceneEntry;
Scene = Scene.Invalid;
_status = (int)SceneLoadStatus.Queued;
}
public void SetStatus(SceneLoadStatus status)
{
Volatile.Write(ref _status, (int)status);
}
public void CompleteParsing(LoadedSceneData loadedData)
{
_loadedData = loadedData;
SetStatus(SceneLoadStatus.WaitingForMaterialization);
}
public void Fail(string message)
{
ErrorMessage = message;
SetStatus(SceneLoadStatus.Failed);
Dispose();
}
public void Cancel()
{
var status = Status;
if (status is SceneLoadStatus.Completed or SceneLoadStatus.Failed or SceneLoadStatus.Canceled)
{
return;
}
SetStatus(SceneLoadStatus.Canceled);
if (status >= SceneLoadStatus.WaitingForMaterialization)
{
Dispose();
}
}
public int Materialize(int maxEntities)
{
if (maxEntities <= 0 || IsTerminal)
{
return 0;
}
var loadedData = _loadedData;
if (loadedData == null)
{
return 0;
}
if (!_singleResetApplied && LoadingType == SceneLoadingType.Single)
{
SceneManager.ReleaseMaterializedSceneReferences(World);
World.Reset();
_singleResetApplied = true;
}
if (_phase == MaterializePhase.NotStarted)
{
Scene = SceneManager.CreateScene();
_fileLocalToRuntimeEntity = new UnsafeArray<Entity>(loadedData.entities.Length, AllocationHandle.Persistent);
_phase = MaterializePhase.CreateEntities;
SetStatus(SceneLoadStatus.Materializing);
}
var consumed = 0;
while (consumed < maxEntities && _phase != MaterializePhase.Completed)
{
switch (_phase)
{
case MaterializePhase.CreateEntities:
consumed += MaterializeCreateEntities(loadedData, maxEntities - consumed);
if (_nextCreateIndex >= loadedData.entities.Length)
{
_phase = MaterializePhase.SetComponents;
}
break;
case MaterializePhase.SetComponents:
consumed += MaterializeSetComponents(loadedData, maxEntities - consumed);
if (_nextSetComponentIndex >= loadedData.entities.Length)
{
_phase = MaterializePhase.RemapEntityReferences;
}
break;
case MaterializePhase.RemapEntityReferences:
consumed += MaterializeRemapEntityReferences(loadedData, maxEntities - consumed);
if (_nextRemapIndex >= loadedData.entities.Length)
{
CompleteMaterialization();
}
break;
}
}
return consumed;
}
private int MaterializeCreateEntities(LoadedSceneData loadedData, int maxEntities)
{
using var scope = AllocationManager.CreateStackScope();
using var sharedCom = new SharedComponentSet(256, scope.AllocationHandle);
var consumed = 0;
while (consumed < maxEntities && _nextCreateIndex < loadedData.entities.Length)
{
ref var pending = ref loadedData.entities[_nextCreateIndex];
using var typeIds = new UnsafeList<Identifier<IComponent>>(pending.componentTypeIDs.Count + 1, scope.AllocationHandle);
typeIds.Add(ComponentTypeID<SceneID>.Value);
for (var i = 0; i < pending.componentTypeIDs.Count; i++)
{
typeIds.Add(pending.componentTypeIDs[i]);
}
sharedCom.With(new SceneID { value = Scene.ID });
var set = new ComponentSetView(typeIds, sharedCom);
var entity = World.EntityManager.CreateEntity(set);
_fileLocalToRuntimeEntity[pending.fileLocalIndex] = entity;
sharedCom.Reset();
_nextCreateIndex++;
consumed++;
}
return consumed;
}
private unsafe int MaterializeSetComponents(LoadedSceneData loadedData, int maxEntities)
{
var consumed = 0;
while (consumed < maxEntities && _nextSetComponentIndex < loadedData.entities.Length)
{
ref var pending = ref loadedData.entities[_nextSetComponentIndex];
var entity = _fileLocalToRuntimeEntity[pending.fileLocalIndex];
if (entity.IsValid)
{
for (var i = 0; i < pending.componentData.Count; i++)
{
var (typeID, data) = pending.componentData[i];
World.EntityManager.SetComponent(entity, typeID, data.GetUnsafePtr());
}
}
_nextSetComponentIndex++;
consumed++;
}
return consumed;
}
private unsafe int MaterializeRemapEntityReferences(LoadedSceneData loadedData, int maxEntities)
{
var consumed = 0;
while (consumed < maxEntities && _nextRemapIndex < loadedData.entities.Length)
{
ref var pending = ref loadedData.entities[_nextRemapIndex];
var entity = _fileLocalToRuntimeEntity[pending.fileLocalIndex];
if (entity.IsValid)
{
for (var i = 0; i < pending.entityFields.Count; i++)
{
var (componentDataIndex, fieldOffsets) = pending.entityFields[i];
var compTypeID = pending.componentData[componentDataIndex].typeID;
var pComponent = World.EntityManager.GetComponent(entity, compTypeID);
if (pComponent == null)
{
continue;
}
for (var f = 0; f < fieldOffsets.Length; f++)
{
var fieldOffset = fieldOffsets[f];
var pField = (byte*)pComponent + fieldOffset;
var fileLocalIndex = *(int*)pField;
var remappedEntity = fileLocalIndex >= 0 && fileLocalIndex < _fileLocalToRuntimeEntity.Length ?
_fileLocalToRuntimeEntity[fileLocalIndex] :
Entity.Invalid;
*(Entity*)pField = remappedEntity;
}
}
}
_nextRemapIndex++;
consumed++;
}
return consumed;
}
private void CompleteMaterialization()
{
_phase = MaterializePhase.Completed;
SceneManager.RegisterMaterializedScene(this);
_loadedData?.Dispose();
_loadedData = null;
if (_fileLocalToRuntimeEntity.IsCreated)
{
_fileLocalToRuntimeEntity.Dispose();
}
SetStatus(SceneLoadStatus.Completed);
}
public void ReleaseMaterializedReference()
{
if (Interlocked.Exchange(ref _releasedSceneEntry, 1) == 0)
{
_sceneEntry.Release();
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
_loadedData?.Dispose();
_loadedData = null;
if (_fileLocalToRuntimeEntity.IsCreated)
{
_fileLocalToRuntimeEntity.Dispose();
}
if (Status != SceneLoadStatus.Completed)
{
ReleaseMaterializedReference();
}
_disposed = true;
}
}
/// <summary> /// <summary>
/// Manages scenes within a world. /// Manages scenes within a world.
/// </summary> /// </summary>
public static class SceneManager public static class SceneManager
{ {
private readonly struct SceneKey : IEquatable<SceneKey>
{
private readonly Identifier<World> _worldID;
private readonly ushort _sceneID;
public SceneKey(World world, Scene scene)
{
_worldID = world.ID;
_sceneID = scene.ID;
}
public bool Equals(SceneKey other)
{
return _worldID == other._worldID && _sceneID == other._sceneID;
}
public override bool Equals(object? obj)
{
return obj is SceneKey other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(_worldID, _sceneID);
}
}
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();
private static readonly Lock s_creationLock = new(); private static readonly Lock s_creationLock = new();
private static readonly Lock s_loadedScenesLock = new();
private static readonly ConcurrentQueue<PendingSceneLoad> s_pendingMaterialization = new();
private static readonly Dictionary<SceneKey, PendingSceneLoad> s_loadedScenes = new();
/// <summary> /// <summary>
/// Creates a new scene in the world. /// Creates a new scene in the world.
@@ -244,6 +648,77 @@ public static class SceneManager
return ParseSceneData(header, ref reader, allocationHandle); return ParseSceneData(header, ref reader, allocationHandle);
} }
internal static void EnqueuePendingScene(PendingSceneLoad pendingSceneLoad)
{
s_pendingMaterialization.Enqueue(pendingSceneLoad);
}
internal static void RegisterMaterializedScene(PendingSceneLoad pendingSceneLoad)
{
lock (s_loadedScenesLock)
{
s_loadedScenes[new SceneKey(pendingSceneLoad.World, pendingSceneLoad.Scene)] = pendingSceneLoad;
}
}
internal static void ReleaseMaterializedSceneReferences(World world)
{
lock (s_loadedScenesLock)
{
foreach (var (key, pendingLoad) in s_loadedScenes.ToArray())
{
if (pendingLoad.World != world)
{
continue;
}
pendingLoad.ReleaseMaterializedReference();
s_recycledSceneIDs.Enqueue(pendingLoad.Scene.ID);
s_loadedScenes.Remove(key);
}
}
}
public static void MaterializePendingScenes(World world, SceneMaterializeBudget budget = default)
{
var maxScenes = budget.MaxScenes > 0 ? budget.MaxScenes : int.MaxValue;
var remainingEntities = budget.MaxEntities > 0 ? budget.MaxEntities : int.MaxValue;
var pendingCount = s_pendingMaterialization.Count;
var processedScenes = 0;
for (var i = 0; i < pendingCount && processedScenes < maxScenes && remainingEntities > 0; i++)
{
if (!s_pendingMaterialization.TryDequeue(out var pendingLoad))
{
break;
}
if (pendingLoad.World != world || pendingLoad.IsTerminal)
{
if (!pendingLoad.IsTerminal)
{
s_pendingMaterialization.Enqueue(pendingLoad);
}
continue;
}
var sceneBudget = pendingLoad.Options.MaxEntitiesPerFrame > 0 ?
Math.Min(remainingEntities, pendingLoad.Options.MaxEntitiesPerFrame) :
remainingEntities;
var consumed = pendingLoad.Materialize(sceneBudget);
remainingEntities -= consumed;
if (!pendingLoad.IsTerminal)
{
s_pendingMaterialization.Enqueue(pendingLoad);
}
processedScenes++;
}
}
/// <summary> /// <summary>
/// Materializes the loaded scene data into actual entities in the world, setting their components and remapping entity references. /// Materializes the loaded scene data into actual entities in the world, setting their components and remapping entity references.
/// </summary> /// </summary>
@@ -274,7 +749,10 @@ public static class SceneManager
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);
if (pending.componentTypeIDs.Count > 0)
{
typeIds.AddRange(pending.componentTypeIDs); typeIds.AddRange(pending.componentTypeIDs);
}
sharedCom.With(new SceneID { value = scene.ID }); sharedCom.With(new SceneID { value = scene.ID });
@@ -340,9 +818,9 @@ public static class SceneManager
/// <summary> /// <summary>
/// Destroys all entities belonging to the specified scene. /// Destroys all entities belonging to the specified scene.
/// </summary> /// </summary>
/// <param name="scene">The scene to unload.</param> /// <param name="scene">The scene to destroy.</param>
/// <param name="world">The world containing the entities.</param> /// <param name="world">The world containing the entities.</param>
public static void UnloadScene(Scene scene, World world) public static void DestroyScene(Scene scene, World world)
{ {
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);
@@ -358,6 +836,15 @@ public static class SceneManager
} }
s_recycledSceneIDs.Enqueue(scene.ID); s_recycledSceneIDs.Enqueue(scene.ID);
lock (s_loadedScenesLock)
{
var key = new SceneKey(world, scene);
if (s_loadedScenes.Remove(key, out var pendingLoad))
{
pendingLoad.ReleaseMaterializedReference();
}
}
} }
/// <summary> /// <summary>

View File

@@ -4,7 +4,6 @@ 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;
@@ -68,7 +67,7 @@ internal abstract class AssetEntry : IAssetEntry
private int _refCount; private int _refCount;
private int _state; private int _state;
private bool _pendingReimport; private int _pendingReimport;
protected ResourceManager ResourceManager => _resourceManager; protected ResourceManager ResourceManager => _resourceManager;
protected IResourceDatabase ResourceDatabase => _resourceDatabase; protected IResourceDatabase ResourceDatabase => _resourceDatabase;
@@ -88,7 +87,7 @@ internal abstract class AssetEntry : IAssetEntry
Volatile.Write(ref _state, (int)value); Volatile.Write(ref _state, (int)value);
if (Volatile.Read(ref _state) == (int)AssetState.Ready) if (Volatile.Read(ref _state) == (int)AssetState.Ready)
{ {
if (Interlocked.CompareExchange(ref _pendingReimport, false, true)) if (Interlocked.Exchange(ref _pendingReimport, 0) == 1)
{ {
_assetManager.ReimportAsset(_assetId); // re-queue _assetManager.ReimportAsset(_assetId); // re-queue
} }
@@ -116,7 +115,7 @@ internal abstract class AssetEntry : IAssetEntry
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetPendingReimport() public void SetPendingReimport()
{ {
Volatile.Write(ref _pendingReimport, true); Volatile.Write(ref _pendingReimport, 1);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -155,7 +154,7 @@ internal abstract class AssetEntry : IAssetEntry
} }
} }
interface IAssetEntry internal interface IAssetEntry
{ {
Guid AssetId { get; } Guid AssetId { get; }
AssetType AssetType { get; } AssetType AssetType { get; }

View File

@@ -42,6 +42,17 @@ internal struct LoadAssetJob : IJob
entry.State = AssetState.Loading; entry.State = AssetState.Loading;
if (entry is not ILoadableAssetEntry loadable)
{
entry.State = AssetState.Loaded;
if (!assetManager.StreamingProcessor.EnqueueForProcess(entry))
{
entry.State = AssetState.Ready;
}
return;
}
try try
{ {
var openResult = assetManager.ContentProvider.OpenRead(entry.AssetId); var openResult = assetManager.ContentProvider.OpenRead(entry.AssetId);
@@ -53,18 +64,13 @@ internal struct LoadAssetJob : IJob
} }
using var stream = openResult.Value; using var stream = openResult.Value;
if (entry is ILoadableAssetEntry loadable)
{
var result = loadable.OnLoadContent(stream); var result = loadable.OnLoadContent(stream);
if (result.IsFailure) if (result.IsFailure)
{ {
entry.State = AssetState.Failed; entry.State = AssetState.Failed;
Logger.Error($"Failed to load asset {assetID}: {result.Message}"); Logger.Error($"Failed to load asset {assetID}: {result.Message}");
return; return;
} }
}
entry.State = AssetState.Loaded; entry.State = AssetState.Loaded;
if (!assetManager.StreamingProcessor.EnqueueForProcess(entry)) if (!assetManager.StreamingProcessor.EnqueueForProcess(entry))

View File

@@ -35,14 +35,14 @@ internal struct MeshContentHeader
public float3 boundsMin; public float3 boundsMin;
public float3 boundsMax; public float3 boundsMax;
public ulong vertexOffset; public long vertexOffset;
public ulong indexOffset; public long indexOffset;
public ulong materialPartOffset; public long materialPartOffset;
public ulong meshletOffset; public long meshletOffset;
public ulong meshletGroupOffset; public long meshletGroupOffset;
public ulong meshletHierarchyNodeOffset; public long meshletHierarchyNodeOffset;
public ulong meshletVertexOffset; public long meshletVertexOffset;
public ulong meshletTriangleOffset; public long meshletTriangleOffset;
} }
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
@@ -122,16 +122,20 @@ internal unsafe class MeshAssetEntry : AssetEntry, ILoadableAssetEntry, IUploada
} }
public override void OnReleaseResource() public override void OnReleaseResource()
{
ResourceManager.ReleaseMesh(_actualHandle);
if (_tempHandle.IsValid)
{ {
ResourceManager.ReleaseMesh(_tempHandle); ResourceManager.ReleaseMesh(_tempHandle);
} }
}
public Result OnLoadContent(Stream contentStream) public Result OnLoadContent(Stream contentStream)
{ {
bool ValidateRange(ulong offset, int count, uint stride) bool ValidateRange(long offset, int count, uint stride)
{ {
var size = (ulong)count * stride; var size = count * stride;
return offset <= _rawData.Size && size <= _rawData.Size - (nuint)offset; return offset <= contentStream.Length && size <= contentStream.Length - offset;
} }
var header = contentStream.Read<MeshContentHeader>(); var header = contentStream.Read<MeshContentHeader>();
@@ -330,6 +334,7 @@ internal unsafe class MeshAssetEntry : AssetEntry, ILoadableAssetEntry, IUploada
dstMesh.MeshletTrianglesBuffer = context.ResourceDatabase.Replace(temp.MeshletTrianglesBuffer.AsResource(), srcMesh.MeshletTrianglesBuffer.AsResource()).AsBuffer(); dstMesh.MeshletTrianglesBuffer = context.ResourceDatabase.Replace(temp.MeshletTrianglesBuffer.AsResource(), srcMesh.MeshletTrianglesBuffer.AsResource()).AsBuffer();
context.ResourceManager.ReleaseMesh(_tempHandle); context.ResourceManager.ReleaseMesh(_tempHandle);
_tempHandle = Handle<Mesh>.Invalid;
context.CommandBuffer.Barrier( context.CommandBuffer.Barrier(
BarrierDesc.Buffer(dstMesh.VertexBuffer, BarrierSync.VertexShading, BarrierAccess.VertexBuffer | BarrierAccess.ShaderResource), BarrierDesc.Buffer(dstMesh.VertexBuffer, BarrierSync.VertexShading, BarrierAccess.VertexBuffer | BarrierAccess.ShaderResource),
@@ -341,8 +346,6 @@ internal unsafe class MeshAssetEntry : AssetEntry, ILoadableAssetEntry, IUploada
BarrierDesc.Buffer(dstMesh.MeshletHierarchyBuffer, BarrierSync.AllShading, BarrierAccess.ShaderResource), BarrierDesc.Buffer(dstMesh.MeshletHierarchyBuffer, BarrierSync.AllShading, BarrierAccess.ShaderResource),
BarrierDesc.Buffer(dstMesh.MeshDataBuffer, BarrierSync.AllShading, BarrierAccess.ShaderResource)); BarrierDesc.Buffer(dstMesh.MeshDataBuffer, BarrierSync.AllShading, BarrierAccess.ShaderResource));
_actualHandle = Handle<Mesh>.Invalid;
_rawData.Dispose(); _rawData.Dispose();
_pVertices = null; _pVertices = null;
_pIndices = null; _pIndices = null;

View File

@@ -74,8 +74,8 @@ internal class ResourceStreamingProcessor : IResourceStreamingProcessor
{ {
while (_pendingFinalize.TryDequeue(out var item)) while (_pendingFinalize.TryDequeue(out var item))
{ {
item.State = AssetState.Ready;
item.OnUploadComplete(context); item.OnUploadComplete(context);
item.State = AssetState.Ready;
} }
_pendingCopyFenceValue = 0; _pendingCopyFenceValue = 0;

View File

@@ -29,24 +29,43 @@ public partial class AssetManager
public SceneContentHeader header; public SceneContentHeader header;
public Stream stream; public Stream stream;
public LoadedSceneData loadedSceneData; public PendingSceneLoad pendingSceneLoad;
public readonly void Execute(ref readonly JobExecutionContext context) public readonly void Execute(ref readonly JobExecutionContext context)
{ {
try try
{ {
var loadResult = SceneManager.ParseSceneData(header, stream, AllocationHandle.Persistent); if (pendingSceneLoad.Status == SceneLoadStatus.Canceled)
if (loadResult.IsFailure)
{ {
Logger.Error($"Failed to parse scene data: {loadResult.Message}"); pendingSceneLoad.Dispose();
return; return;
} }
loadedSceneData.entities = loadResult.Value.entities; pendingSceneLoad.SetStatus(SceneLoadStatus.Parsing);
var loadResult = SceneManager.ParseSceneData(header, stream, AllocationHandle.Persistent);
if (loadResult.IsFailure)
{
pendingSceneLoad.Fail(loadResult.Message ?? "Failed to parse scene data.");
return;
}
if (pendingSceneLoad.Status == SceneLoadStatus.Canceled)
{
loadResult.Value.Dispose();
pendingSceneLoad.Dispose();
return;
}
pendingSceneLoad.CompleteParsing(loadResult.Value);
if (pendingSceneLoad.Options.AutoMaterialize)
{
SceneManager.EnqueuePendingScene(pendingSceneLoad);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Error($"Exception while loading scene: {ex}"); pendingSceneLoad.Fail(ex.Message);
} }
finally finally
{ {
@@ -55,7 +74,7 @@ public partial class AssetManager
} }
} }
public unsafe Result<JobHandle> LoadScene(World world, AssetRef<Scene> sceneAsset, SceneLoadingType loadingType, ref LoadedSceneData? loadedSceneData) public unsafe Result<SceneLoadOperation> LoadScene(World world, AssetRef<Scene> sceneAsset, SceneLoadingType loadingType, SceneLoadOptions options = default)
{ {
if (!sceneAsset.IsValid) if (!sceneAsset.IsValid)
{ {
@@ -91,23 +110,19 @@ public partial class AssetManager
try try
{ {
if (loadingType == SceneLoadingType.Single)
{
world.Reset();
}
loadedSceneData ??= new LoadedSceneData();
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 entry = GetOrCreateEntry(sceneAsset.ID); // Purely to get the dependencies and ensure the asset is tracked, the actual loading is done in the job.
var pendingSceneLoad = new PendingSceneLoad(world, sceneAsset, loadingType, options, entry);
pendingSceneLoad.SetStatus(SceneLoadStatus.WaitingForDependencies);
var job = new LoadSceneJob var job = new LoadSceneJob
{ {
header = header, header = header,
stream = stream, stream = stream,
loadedSceneData = loadedSceneData pendingSceneLoad = pendingSceneLoad
}; };
return _jobScheduler.Schedule(in job, entry.LoadJobHandle); _jobScheduler.Schedule(in job, entry.LoadJobHandle);
return Result<SceneLoadOperation>.Success(new SceneLoadOperation(pendingSceneLoad));
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -103,9 +103,13 @@ internal unsafe class TextureAssetEntry : AssetEntry, ILoadableAssetEntry, IUplo
} }
public override void OnReleaseResource() public override void OnReleaseResource()
{
ResourceDatabase.ReleaseResource(_actualHandle.AsResource());
if (_tempHandle.IsValid)
{ {
ResourceDatabase.ReleaseResource(_tempHandle.AsResource()); ResourceDatabase.ReleaseResource(_tempHandle.AsResource());
} }
}
public Result OnLoadContent(Stream contentStream) public Result OnLoadContent(Stream contentStream)
{ {
@@ -167,8 +171,8 @@ internal unsafe class TextureAssetEntry : AssetEntry, ILoadableAssetEntry, IUplo
context.CommandBuffer.Barrier(BarrierDesc.Texture(actualHandle, BarrierSync.AllShading, BarrierAccess.ShaderResource, BarrierLayout.ShaderResource)); context.CommandBuffer.Barrier(BarrierDesc.Texture(actualHandle, BarrierSync.AllShading, BarrierAccess.ShaderResource, BarrierLayout.ShaderResource));
_actualHandle = Handle<GPUTexture>.Invalid; _actualHandle = actualHandle.AsTexture();
_tempHandle = actualHandle.AsTexture(); _tempHandle = Handle<GPUTexture>.Invalid;
_textureData.Dispose(); _textureData.Dispose();
} }
} }

View File

@@ -636,19 +636,74 @@ public unsafe partial struct EntityQuery : IDisposable
if (!requiresFiltering) if (!requiresFiltering)
{ {
// No enablement constraints? Any entity in this chunk is a match!
return true; return true;
} }
for (var k = 0; k < chunk._count; k++) var ulongCount = (chunk._count + 63) / 64;
var chunkHasMatch = false;
// Loop through 64 entities at a time
for (var block = 0; block < ulongCount; block++)
{ {
if (IsEntityValid(chunk.GetUnsafePtr(), k, in archetype, in _mask)) // Start assuming all 64 entities in this block are valid (ulong.MaxValue = 1111...)
// For the last block, mask out the garbage bits beyond chunk._count
var validMask = ulong.MaxValue;
var remaining = chunk._count - (block * 64);
if (remaining < 64)
{
validMask = (1UL << remaining) - 1UL;
}
// Intersect requireEnabled (Bits MUST be 1)
var it = _mask.requireEnabled.GetIterator();
while (it.Next(out var id) && validMask != 0)
{
var layoutResult = archetype.GetLayout(id);
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
{
var pMask = (ulong*)(chunk.GetUnsafePtr() + layoutResult.Value.enableBitsOffset);
validMask &= pMask[block];
}
}
// Intersect requireDisabled / rejectIfEnabled (Bits MUST be 0)
it = _mask.requireDisabled.GetIterator();
while (it.Next(out var id) && validMask != 0)
{
var layoutResult = archetype.GetLayout(id);
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
{
var pMask = (ulong*)(chunk.GetUnsafePtr() + layoutResult.Value.enableBitsOffset);
validMask &= ~pMask[block]; // Invert memory, because it must be 0!
}
}
// rejectIfEnabled behaves identically to requireDisabled
it = _mask.rejectIfEnabled.GetIterator();
while (it.Next(out var id) && validMask != 0)
{
var layoutResult = archetype.GetLayout(id);
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
{
var pMask = (ulong*)(chunk.GetUnsafePtr() + layoutResult.Value.enableBitsOffset);
validMask &= ~pMask[block];
}
}
// If validMask still has ANY bits set to 1, we found at least one matching entity.
if (validMask != 0)
{
chunkHasMatch = true;
break;
}
}
if (chunkHasMatch)
{ {
return true; return true;
} }
} }
} }
}
return false; return false;
} }

View File

@@ -86,7 +86,7 @@ internal class AssetHandlerRegistrationGenerator : IIncrementalGenerator
var registerTypeName = "g_assethandler_registeration"; var registerTypeName = "g_assethandler_registeration";
var code = $@"// <auto-generated /> var code = $@"// <auto-generated />
#if GHOST_SAFETY_CHECKS #if GHOST_EDITOR
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static partial class {registerTypeName} internal static partial class {registerTypeName}
{{ {{
@@ -159,7 +159,7 @@ internal class IAssetSettingsRegistrationGenerator : IIncrementalGenerator
var registerTypeName = "g_iassetsettings_registeration"; var registerTypeName = "g_iassetsettings_registeration";
var code = $@"// <auto-generated /> var code = $@"// <auto-generated />
#if GHOST_SAFETY_CHECKS #if GHOST_EDITOR
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static partial class {registerTypeName} internal static partial class {registerTypeName}
{{ {{

View File

@@ -200,7 +200,7 @@ namespace {info.TypeSymbol.ContainingNamespace.ToDisplayString()}
[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 4)] [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 4)]
{info.TypeSymbol.DeclaredAccessibility.ToString().ToLower()} partial struct {info.TypeSymbol.Name} {info.TypeSymbol.DeclaredAccessibility.ToString().ToLower()} partial struct {info.TypeSymbol.Name}
{{ {{
#if GHOST_SAFETY_CHECKS #if GHOST_EDITOR
public const string HLSL_SOURCE = @"" public const string HLSL_SOURCE = @""
struct {info.Name} struct {info.Name}
{{ {{
@@ -222,7 +222,7 @@ struct {info.Name}
var registerTypeName = "g_shaderproperty_registeration"; var registerTypeName = "g_shaderproperty_registeration";
var registerCode = $@"// <auto-generated/> var registerCode = $@"// <auto-generated/>
#if GHOST_SAFETY_CHECKS #if GHOST_EDITOR
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static partial class {registerTypeName} internal static partial class {registerTypeName}
{{ {{

View File

@@ -8,7 +8,7 @@ public unsafe struct ShaderByteCode
public ulong size; public ulong size;
} }
public unsafe delegate void ShaderVariantCompiledHandler(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, ReadOnlySpan<ShaderByteCode> byteCodes); public delegate void ShaderVariantCompiledHandler(ulong shaderId, int passIndex, Key64<ShaderVariant> variantKey, ReadOnlySpan<ShaderByteCode> byteCodes);
public interface IShaderCompilationBridge : IDisposable public interface IShaderCompilationBridge : IDisposable
{ {

View File

@@ -43,11 +43,11 @@
<ProjectCapability Include="Msix" /> <ProjectCapability Include="Msix" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.TestPlatform.TestHost" Version="18.4.0" /> <PackageReference Include="Microsoft.TestPlatform.TestHost" Version="18.5.1" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="2.1.3" />
<PackageReference Include="MSTest.TestAdapter" Version="4.2.1" /> <PackageReference Include="MSTest.TestAdapter" Version="4.2.3" />
<PackageReference Include="MSTest.TestFramework" Version="4.2.1" /> <PackageReference Include="MSTest.TestFramework" Version="4.2.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" /> <ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" />

View File

@@ -12,7 +12,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MSTest" Version="4.2.2" /> <PackageReference Include="MSTest" Version="4.2.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -36,6 +36,8 @@ internal class MockingContentProvider : IContentProvider
{ {
var header = new TextureContentHeader var header = new TextureContentHeader
{ {
magic = TextureContentHeader.MAGIC,
version = TextureContentHeader.VERSION,
width = width, width = width,
height = height, height = height,
bpc = 8, bpc = 8,
@@ -144,14 +146,14 @@ internal class MockingContentProvider : IContentProvider
}; };
stream.Write(header); stream.Write(header);
header.vertexOffset = (ulong)stream.Position; stream.Write(vertices); header.vertexOffset = stream.Position; stream.Write(vertices);
header.indexOffset = (ulong)stream.Position; stream.Write(indices); header.indexOffset = stream.Position; stream.Write(indices);
header.materialPartOffset = (ulong)stream.Position; stream.Write(materialParts); header.materialPartOffset = stream.Position; stream.Write(materialParts);
header.meshletOffset = (ulong)stream.Position; stream.Write(meshlets); header.meshletOffset = stream.Position; stream.Write(meshlets);
header.meshletGroupOffset = (ulong)stream.Position; stream.Write(groups); header.meshletGroupOffset = stream.Position; stream.Write(groups);
header.meshletHierarchyNodeOffset = (ulong)stream.Position; stream.Write(hierarchy); header.meshletHierarchyNodeOffset = stream.Position; stream.Write(hierarchy);
header.meshletVertexOffset = (ulong)stream.Position; stream.Write(meshletVertices); header.meshletVertexOffset = stream.Position; stream.Write(meshletVertices);
header.meshletTriangleOffset = (ulong)stream.Position; stream.Write(meshletTriangles); header.meshletTriangleOffset = stream.Position; stream.Write(meshletTriangles);
stream.Position = 0; stream.Position = 0;
stream.Write(header); stream.Write(header);

View File

@@ -0,0 +1,143 @@
using Ghost.Core;
using Ghost.Editor.Core.SceneGraph;
using Ghost.Editor.Core.Services;
using Ghost.Engine;
using Ghost.Engine.Components;
using Ghost.Engine.Core;
using Ghost.Entities;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
namespace Ghost.UnitTest;
[TestClass]
[DoNotParallelize]
public class SceneGraphSyncTests
{
private EditorWorldService _worldService = null!;
private SceneGraphSyncService _syncService = null!;
[TestInitialize]
public void Setup()
{
_worldService = new EditorWorldService();
_syncService = new SceneGraphSyncService(_worldService);
}
[TestCleanup]
public void Cleanup()
{
_syncService.Dispose();
_worldService.Dispose();
}
[TestMethod]
public void SceneGraph_InitialBuild_PopulatesTreeCorrectly()
{
var world = _worldService.EditorWorld;
var scene = SceneManager.CreateScene();
// Create parent and child
var parent = world.EntityManager.CreateEntity();
world.EntityManager.AddSharedComponent(parent, new SceneID { value = scene.ID });
world.EntityManager.AddComponent(parent, Hierarchy.Root);
var child = world.EntityManager.CreateEntity();
world.EntityManager.AddSharedComponent(child, new SceneID { value = scene.ID });
world.EntityManager.AddComponent(child, Hierarchy.Root);
HierarchyUtility.SetParent(world, child, parent);
var names = new Dictionary<Entity, string>
{
{ parent, "ParentEntity" },
{ child, "ChildEntity" }
};
_worldService.RebuildSceneGraph(names);
Assert.AreEqual(1, _worldService.RootNodes.Count);
var sceneNode = _worldService.RootNodes[0];
Assert.AreEqual(scene.ID, sceneNode.Scene.ID);
Assert.AreEqual(1, sceneNode.Children.Count);
var parentNode = (EntityNode)sceneNode.Children[0];
Assert.AreEqual("ParentEntity", parentNode.Name);
Assert.AreEqual(parent, parentNode.Entity);
Assert.AreEqual(1, parentNode.Children.Count);
var childNode = (EntityNode)parentNode.Children[0];
Assert.AreEqual("ChildEntity", childNode.Name);
Assert.AreEqual(child, childNode.Entity);
}
[TestMethod]
public void SceneGraph_CreateEntity_AppendsToRootAutomatically()
{
var scene = SceneManager.CreateScene();
var entity = _worldService.CreateEntity("NewEntity", scene.ID);
Assert.AreEqual(1, _worldService.RootNodes.Count);
var sceneNode = _worldService.RootNodes[0];
Assert.AreEqual(1, sceneNode.Children.Count);
var entityNode = (EntityNode)sceneNode.Children[0];
Assert.AreEqual("NewEntity", entityNode.Name);
Assert.AreEqual(entity, entityNode.Entity);
}
[TestMethod]
public void SceneGraph_DestroyEntity_RemovesFromTreeAutomatically()
{
var scene = SceneManager.CreateScene();
var entity = _worldService.CreateEntity("NewEntity", scene.ID);
var sceneNode = _worldService.RootNodes[0];
Assert.AreEqual(1, sceneNode.Children.Count);
_worldService.DestroyEntity(entity);
Assert.AreEqual(0, sceneNode.Children.Count);
}
[TestMethod]
public void SceneGraph_SetParent_MovesNodeInTree()
{
var scene = SceneManager.CreateScene();
var parent = _worldService.CreateEntity("Parent", scene.ID);
var child = _worldService.CreateEntity("Child", scene.ID);
var sceneNode = _worldService.RootNodes[0];
Assert.AreEqual(2, sceneNode.Children.Count);
var err = _worldService.SetParent(child, parent);
Assert.AreEqual(Error.None, err);
Assert.AreEqual(1, sceneNode.Children.Count);
var parentNode = (EntityNode)sceneNode.Children[0];
Assert.AreEqual("Parent", parentNode.Name);
Assert.AreEqual(1, parentNode.Children.Count);
var childNode = (EntityNode)parentNode.Children[0];
Assert.AreEqual("Child", childNode.Name);
Assert.AreEqual(child, childNode.Entity);
}
[TestMethod]
public void SceneGraph_RenameEntity_UpdatesNodeNameInstantly()
{
var scene = SceneManager.CreateScene();
var entity = _worldService.CreateEntity("OriginalName", scene.ID);
var sceneNode = _worldService.RootNodes[0];
var entityNode = (EntityNode)sceneNode.Children[0];
Assert.AreEqual("OriginalName", entityNode.Name);
_worldService.RenameEntity(entity, "NewName");
Assert.AreEqual("NewName", entityNode.Name);
}
}

View File

@@ -42,7 +42,8 @@ public class SceneSerializationTests
EditorApplication.Initialize(new EmptyServiceProvider(), _projectRoot, "SceneTest"); EditorApplication.Initialize(new EmptyServiceProvider(), _projectRoot, "SceneTest");
_worldService = new EditorWorldService(); _worldService = new EditorWorldService();
_serializationService = new SceneSerializationService(_worldService, null!); var syncService = new SceneGraphSyncService(_worldService);
_serializationService = new SceneSerializationService(_worldService, null!, syncService);
} }
[TestCleanup] [TestCleanup]