feat(graphics): improve rendering pipeline and docs
- Refactor D3D12 backend and RenderGraph module - Update graphics RHI and core rendering components - Add Random.hlsl shader include - Regenerate API documentation and update user guides
This commit is contained in:
69
doc/docs/components.md
Normal file
69
doc/docs/components.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Components Overview
|
||||
|
||||
In GhostEngine, components are strict, unmanaged data structures. They must implement the empty interface `IComponent` (or its derivations). This ensures AOT compatibility and zero GC allocation overhead when iterating over millions of entities.
|
||||
|
||||
## Defining a Component
|
||||
|
||||
To define a standard component, create an unmanaged struct implementing `IComponent`:
|
||||
|
||||
```csharp
|
||||
using Ghost.Entities;
|
||||
using System.Numerics;
|
||||
|
||||
public struct TransformComponent : IComponent
|
||||
{
|
||||
public Vector3 Position;
|
||||
public Quaternion Rotation;
|
||||
public Vector3 Scale;
|
||||
}
|
||||
```
|
||||
|
||||
## Enableable Components
|
||||
|
||||
Sometimes you need to toggle a component on or off without incurring the heavy structural cost of removing and re-adding the component from an entity. GhostEngine supports this via `IEnableableComponent`.
|
||||
|
||||
```csharp
|
||||
public struct Renderable : IEnableableComponent
|
||||
{
|
||||
public int MeshID;
|
||||
public int MaterialID;
|
||||
}
|
||||
```
|
||||
|
||||
When iterating over an archetype chunk, you can check the bitmask to see if an entity's enableable component is currently active:
|
||||
|
||||
```csharp
|
||||
// During a query chunk iteration:
|
||||
var renderables = chunkView.GetComponentData<Renderable>();
|
||||
var enableBits = chunkView.GetEnableBits<Renderable>();
|
||||
|
||||
for (int i = 0; i < chunkView.EntityCount; i++)
|
||||
{
|
||||
if (chunkView.IsComponentEnabled<Renderable>(i))
|
||||
{
|
||||
// Process active renderable
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Component Type IDs
|
||||
|
||||
The engine assigns a unique runtime ID to each unmanaged component type. You can retrieve this identifier using the `ComponentTypeID<T>.Value` generic cache:
|
||||
|
||||
```csharp
|
||||
Identifier<IComponent> id = ComponentTypeID<TransformComponent>.Value;
|
||||
```
|
||||
|
||||
This ID is heavily used under the hood for query masking, archetype matching, and memory offset calculations inside a chunk.
|
||||
|
||||
## RequireComponent Attribute
|
||||
|
||||
You can enforce dependencies between components using the `[RequireComponentAttribute<T>]`. While this is mostly semantic, it helps structure dependencies for complex systems.
|
||||
|
||||
```csharp
|
||||
[RequireComponent<TransformComponent>]
|
||||
public struct PhysicsCollider : IComponent
|
||||
{
|
||||
public float Radius;
|
||||
}
|
||||
```
|
||||
32
doc/docs/ecs-concepts.md
Normal file
32
doc/docs/ecs-concepts.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# ECS Concepts
|
||||
|
||||
GhostEngine's ECS implementation relies on several key concepts rooted in Data-Oriented Design. Understanding these concepts is essential to writing performant engine code and gameplay logic.
|
||||
|
||||
## World
|
||||
|
||||
A `World` encapsulates all data and logic for an independent ECS simulation. It owns three core managers:
|
||||
- **EntityManager:** Handles the creation, destruction, and structural modification of Entities.
|
||||
- **ComponentManager:** Manages Archetypes and EntityQueries, keeping track of component memory layouts and chunk allocations.
|
||||
- **SystemManager:** Manages the lifecycle and execution order of all Systems.
|
||||
|
||||
Multiple Worlds can exist simultaneously (e.g., one for logic, one for rendering, or server/client splits in multiplayer).
|
||||
|
||||
## Entity
|
||||
|
||||
An `Entity` in Ghost.Entities is not an object or a class; it is merely an integer ID (often accompanied by a version number). It serves as a lookup key to find a specific set of component data.
|
||||
|
||||
## Component
|
||||
|
||||
A `Component` is a pure, unmanaged data structure (`struct`). It implements `IComponent` or `IEnableableComponent`. Components contain no logic. By restricting components to unmanaged types, the engine ensures they can be packed tightly into continuous blocks of memory (Chunks), entirely avoiding Garbage Collection (GC) overhead and ensuring fast CPU cache lines.
|
||||
|
||||
## Archetype and Chunks
|
||||
|
||||
When you create an entity with a specific set of components (e.g., `Position` and `Velocity`), the ECS groups it into an **Archetype**. An Archetype represents a unique signature of components.
|
||||
|
||||
Entities of the same Archetype have their component data stored in **Chunks**. A Chunk is a fixed-size block of unmanaged memory (e.g., 16KB).
|
||||
|
||||
Instead of storing arrays of Entities containing objects (Array of Structures), the Chunk stores independent arrays for each component type (Structure of Arrays). This means when a system queries for `Position`, it receives a contiguous unmanaged memory `Span<Position>`, allowing the CPU to aggressively prefetch data and vectorize operations.
|
||||
|
||||
## System
|
||||
|
||||
A `System` provides the logic that transforms component data from one state to the next. Systems iterate over matching Archetypes using `EntityQuery` and process component data in bulk. Systems can be managed manually or grouped into a `SystemGroup` which topologically sorts execution order based on dependencies.
|
||||
88
doc/docs/ecs-workflows.md
Normal file
88
doc/docs/ecs-workflows.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# ECS Workflows
|
||||
|
||||
This document outlines standard daily workflows for working within GhostEngine's ECS.
|
||||
|
||||
## Creating Entity Queries
|
||||
|
||||
To read or modify data, you first build an `EntityQuery`. You use the `QueryBuilder` struct to define inclusion and exclusion constraints.
|
||||
|
||||
```csharp
|
||||
Identifier<EntityQuery> queryId = QueryBuilder.Create()
|
||||
.WithAll<Position>() // Must have Position
|
||||
.WithAny<Player, Enemy>() // Must have Player OR Enemy
|
||||
.WithNone<Dead>() // Must NOT have Dead structurally
|
||||
.WithDisabled<Renderable>() // Must have Renderable structurally, but it must be disabled
|
||||
.Build(world);
|
||||
```
|
||||
|
||||
Queries are hashed and cached internally by the `ComponentManager`. Building the same query mask twice will yield the same `Identifier<EntityQuery>`.
|
||||
|
||||
## Iterating Chunk Views
|
||||
|
||||
Once you have a query, use `GetChunkIterator()` to traverse the unmanaged memory directly. This returns a `ChunkView` struct.
|
||||
|
||||
Because the data is unmanaged, you can extract `Span<T>` and modify it with extreme speed.
|
||||
|
||||
```csharp
|
||||
ref var query = ref world.ComponentManager.GetEntityQueryReference(queryId);
|
||||
|
||||
foreach (var chunkView in query.GetChunkIterator())
|
||||
{
|
||||
// Get Read-Only Span
|
||||
ReadOnlySpan<Velocity> vels = chunkView.GetComponentData<Velocity>();
|
||||
|
||||
// Get Read-Write Span
|
||||
Span<Position> pos = chunkView.GetComponentDataRW<Position>();
|
||||
|
||||
// For loop for cache-aligned processing
|
||||
for (int i = 0; i < chunkView.EntityCount; i++)
|
||||
{
|
||||
pos[i].Value += vels[i].Value * dt;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** Accessing data using `GetComponentDataRW<T>()` will automatically bump the internal version numbers for that component type in that specific chunk, allowing other systems to detect changes efficiently.
|
||||
|
||||
## Entity Command Buffers (Structural Changes)
|
||||
|
||||
In ECS, structural changes (adding/removing components, destroying entities) reorganize chunk memory. Doing this while iterating over chunks invalidates the memory layout.
|
||||
|
||||
To safely defer structural changes, use an `EntityCommandBuffer`.
|
||||
|
||||
```csharp
|
||||
// Get the world's main ECB
|
||||
var ecb = world.EntityCommandBuffer;
|
||||
|
||||
foreach (var chunk in query.GetChunkIterator())
|
||||
{
|
||||
var entities = chunk.GetEntities();
|
||||
var healths = chunk.GetComponentData<Health>();
|
||||
|
||||
for (int i = 0; i < chunk.EntityCount; i++)
|
||||
{
|
||||
if (healths[i].Value <= 0)
|
||||
{
|
||||
// Defer the destruction until Playback() is called
|
||||
ecb.DestroyEntity(entities[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
At the end of the frame (or the end of the system group update), you must call:
|
||||
```csharp
|
||||
world.PlaybackEntityCommandBuffers();
|
||||
```
|
||||
|
||||
### Multi-threading ECBs
|
||||
|
||||
When using the `JobScheduler`, you can record deferred commands across multiple worker threads concurrently by requesting thread-local ECBs:
|
||||
|
||||
```csharp
|
||||
int workerIndex = JobScheduler.CurrentThreadIndex;
|
||||
var threadEcb = world.GetThreadLocalEntityCommandBuffer(workerIndex);
|
||||
|
||||
// Safely record structural changes from parallel jobs
|
||||
threadEcb.AddComponent<Dead>(entity);
|
||||
```
|
||||
@@ -1 +1,88 @@
|
||||
# Getting Started
|
||||
# Getting Started with Ghost.Entities
|
||||
|
||||
GhostEngine's Entity Component System (ECS) is a high-performance, Data-Oriented architecture designed for C# .NET 10. The ECS runtime is strictly AOT-compatible and focuses on unmanaged data structures to maximize cache locality and performance.
|
||||
|
||||
## Initialization
|
||||
|
||||
The core of the ECS is the `World`. A `World` contains an `EntityManager`, `ComponentManager`, and `SystemManager`.
|
||||
|
||||
To get started, you must create a `World`:
|
||||
|
||||
```csharp
|
||||
using Ghost.Entities;
|
||||
|
||||
// Create a new World with a default entity capacity of 16
|
||||
World world = World.Create();
|
||||
```
|
||||
|
||||
## Creating Entities and Adding Components
|
||||
|
||||
Entities are simply IDs that point to a combination of components. You use the `EntityManager` to create and modify them.
|
||||
|
||||
First, define your unmanaged components:
|
||||
|
||||
```csharp
|
||||
public struct Position : IComponent
|
||||
{
|
||||
public float X, Y, Z;
|
||||
}
|
||||
|
||||
public struct Velocity : IComponent
|
||||
{
|
||||
public float X, Y, Z;
|
||||
}
|
||||
```
|
||||
|
||||
Then, create entities and attach components:
|
||||
|
||||
```csharp
|
||||
using Ghost.Core;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
|
||||
// Creating a ComponentSet for fast archetype initialization
|
||||
using var scope = AllocationManager.CreateStackScope();
|
||||
var componentSet = new ComponentSet(
|
||||
scope.AllocationHandle,
|
||||
ComponentTypeID<Position>.Value,
|
||||
ComponentTypeID<Velocity>.Value
|
||||
);
|
||||
|
||||
// Create 1000 entities with both Position and Velocity
|
||||
world.EntityManager.CreateEntities(1000, componentSet);
|
||||
```
|
||||
|
||||
## Adding Systems and Updating
|
||||
|
||||
Systems contain the logic that operates on the component data. You add systems to the `SystemManager` and run them inside a loop.
|
||||
|
||||
```csharp
|
||||
// Get the DefaultSystemGroup to attach systems
|
||||
var group = world.SystemManager.GetSystem<DefaultSystemGroup>();
|
||||
|
||||
// Add custom systems
|
||||
group.AddSystem<MovementSystem>();
|
||||
|
||||
// Sort systems based on their dependencies
|
||||
group.SortSystems();
|
||||
|
||||
// Initialize all systems
|
||||
world.SystemManager.InitializeAll(new TimeData());
|
||||
|
||||
// Update loop
|
||||
while (running)
|
||||
{
|
||||
world.SystemManager.UpdateAll(timeData);
|
||||
world.PlaybackEntityCommandBuffers();
|
||||
}
|
||||
|
||||
// Cleanup at the end
|
||||
world.Dispose();
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
In GhostEngine ECS:
|
||||
1. **World** manages the lifecycle of your ECS data.
|
||||
2. **Components** (`IComponent`) are unmanaged data structures.
|
||||
3. **Entities** are logical containers combining those components into archetypes.
|
||||
4. **Systems** (`SystemBase`) execute logic over arrays of components using `EntityQuery`.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# Introduction
|
||||
53
doc/docs/performance.md
Normal file
53
doc/docs/performance.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Performance and Memory
|
||||
|
||||
GhostEngine's ECS is explicitly designed around maximum CPU efficiency and AOT compatibility.
|
||||
|
||||
## Structure of Arrays (SoA)
|
||||
|
||||
Traditional OOP patterns use Arrays of Structures (AoS), which causes cache misses when looping through objects to read a single field.
|
||||
|
||||
GhostEngine ECS uses Structure of Arrays (SoA) at the chunk level. A 16KB unmanaged `Chunk` separates components into continuous blocks. When a `System` iterates a `ChunkView` and calls `GetComponentData<Position>()`, it receives a contiguous `Span<Position>`.
|
||||
|
||||
Because modern CPUs pre-fetch continuous memory aggressively, an ECS query can be up to orders of magnitude faster than naive OOP equivalents.
|
||||
|
||||
## Benchmark Example
|
||||
|
||||
The internal `Ghost.Entities.Test` benchmarks demonstrate the raw speed comparison:
|
||||
|
||||
```csharp
|
||||
// OOP Approach (~4000ns)
|
||||
for (var i = 0; i < _gameObjects.Length; i++)
|
||||
{
|
||||
_gameObjects[i].Position += new Vector4(_dt, _dt, _dt, 0);
|
||||
}
|
||||
|
||||
// ECS Approach (~620ns)
|
||||
ref var query = ref _world.ComponentManager.GetEntityQueryReference(_queryIdentifier);
|
||||
foreach (var chunkView in query.GetChunkIterator())
|
||||
{
|
||||
var positions = chunkView.GetComponentDataRW<Position>();
|
||||
ref var address = ref MemoryMarshal.GetReference(positions);
|
||||
|
||||
for (var i = 0; i < positions.Length; i++)
|
||||
{
|
||||
Unsafe.Add(ref address, i).value += new Vector4(_dt, _dt, _dt, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Results for 1,000,000 Entities:**
|
||||
- Over **98.5% cache hits**
|
||||
- Approximately **4x to 6x faster** execution times
|
||||
- **0 Bytes** allocated per frame (Zero Garbage Collection)
|
||||
|
||||
## Versioning and Change Tracking
|
||||
|
||||
Chunks maintain atomic version counters. Whenever `GetComponentDataRW<T>()` is called, the chunk updates its component version to match the current `World.Version`.
|
||||
|
||||
Systems can rapidly bypass entire chunks without iterating them by checking `chunkView.HasChanged<T>(LastSystemVersion)`. This enables dirty-flag processing at near-zero cost.
|
||||
|
||||
## Unmanaged Constraints
|
||||
|
||||
To guarantee this performance, the engine strictly enforces unmanaged types.
|
||||
- You cannot put `string`, `class`, or managed arrays inside `IComponent`.
|
||||
- For bulk temporary allocations during system updates, you should use `VirtualStack.Scope` or `AllocationManager` alongside `UnsafeList<T>` to remain entirely off the .NET Garbage Collector heap.
|
||||
82
doc/docs/systems.md
Normal file
82
doc/docs/systems.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Systems Overview
|
||||
|
||||
Systems are where all your ECS logic lives. They interact with data by iterating over entities that match an `EntityQuery`.
|
||||
|
||||
## The `ISystem` Interface
|
||||
|
||||
At its core, a system implements `ISystem`, which provides three lifecycle methods:
|
||||
|
||||
```csharp
|
||||
public interface ISystem
|
||||
{
|
||||
void Initialize(ref readonly SystemAPI systemAPI);
|
||||
void Update(ref readonly SystemAPI systemAPI);
|
||||
void Cleanup(ref readonly SystemAPI systemAPI);
|
||||
}
|
||||
```
|
||||
|
||||
The `SystemAPI` struct provides access to the `World` and `TimeData` for the current frame.
|
||||
|
||||
## `SystemBase`
|
||||
|
||||
For most gameplay code, you should inherit from the abstract class `SystemBase`, which provides convenient wrappers and handles "Should Update" logic automatically.
|
||||
|
||||
```csharp
|
||||
public class MovementSystem : SystemBase
|
||||
{
|
||||
private Identifier<EntityQuery> _query;
|
||||
|
||||
protected override void OnInitialize(ref readonly SystemAPI systemAPI)
|
||||
{
|
||||
// Build a query for entities that have both Position and Velocity
|
||||
_query = new QueryBuilder()
|
||||
.WithAllRW<Position>()
|
||||
.WithAll<Velocity>()
|
||||
.Build(World);
|
||||
|
||||
// Tell the system base to ONLY run OnUpdate if this query has at least 1 entity
|
||||
RequireQueryForUpdate(_query);
|
||||
}
|
||||
|
||||
protected override void OnUpdate(ref readonly SystemAPI systemAPI)
|
||||
{
|
||||
float dt = systemAPI.Time.DeltaTime;
|
||||
|
||||
ref var query = ref World.ComponentManager.GetEntityQueryReference(_query);
|
||||
|
||||
foreach (var chunk in query.GetChunkIterator())
|
||||
{
|
||||
var positions = chunk.GetComponentDataRW<Position>();
|
||||
var velocities = chunk.GetComponentData<Velocity>();
|
||||
|
||||
for (int i = 0; i < chunk.EntityCount; i++)
|
||||
{
|
||||
positions[i].Value += velocities[i].Value * dt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Automatic Update Triggers
|
||||
|
||||
Notice the `RequireQueryForUpdate(_query)` call. `SystemBase` will skip calling `OnUpdate` if the query results are completely empty, saving processing time. `SystemBase` also invokes `OnStartRunning` and `OnStopRunning` when the system transitions between having matching entities and not.
|
||||
|
||||
## System Execution Order
|
||||
|
||||
By default, systems execute in the order they are added to a `SystemGroup`. To enforce execution order regardless of initialization sequence, use the `[UpdateAfter]` and `[UpdateBefore]` attributes.
|
||||
|
||||
```csharp
|
||||
[UpdateAfter(typeof(PhysicsSystem))]
|
||||
[UpdateBefore(typeof(RenderSystem))]
|
||||
public class MovementSystem : SystemBase
|
||||
{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
When you call `group.SortSystems()`, the `SystemGroup` uses Kahn's algorithm to perform a topological sort and resolves these dependencies. If circular dependencies are detected, the engine will throw an exception.
|
||||
|
||||
## System Groups
|
||||
|
||||
`SystemGroup`s implement `ISystem` themselves, meaning you can nest groups within groups to build complex hierarchical update loops (e.g. `FixedUpdateGroup`, `PresentationGroup`). GhostEngine provides a `DefaultSystemGroup` that is automatically instantiated by the `SystemManager`.
|
||||
@@ -1,4 +1,12 @@
|
||||
- name: Introduction
|
||||
href: introduction.md
|
||||
- name: Getting Started
|
||||
href: getting-started.md
|
||||
- name: Getting Started
|
||||
href: getting-started.md
|
||||
- name: ECS Concepts
|
||||
href: ecs-concepts.md
|
||||
- name: Components
|
||||
href: components.md
|
||||
- name: Systems
|
||||
href: systems.md
|
||||
- name: ECS Workflows
|
||||
href: ecs-workflows.md
|
||||
- name: Performance
|
||||
href: performance.md
|
||||
|
||||
Reference in New Issue
Block a user