Render extraction system & ECS/graphics refactor

Introduced RenderExtractionSystem for entity-based render data extraction. Added MeshInstance and MeshPalette components with shadow casting support. Refactored QueryBuilder API, SharedComponentStore, and component registration for clarity and flexibility. Updated SystemManager and SystemGroup to use SystemAPI. Replaced RenderingConfig with GraphicsEngineDesc/RenderSystemDesc. RenderFrame now uses CPU/GPU fence values for sync. Removed Camera.cs in favor of ECS-based rendering. Improved Material, RenderingLayerMask, Mesh, and RenderList APIs. Updated package references and fixed naming, error handling, and disposal issues.
This commit is contained in:
2026-03-08 22:51:03 +09:00
parent bfe8588d76
commit 619720feee
26 changed files with 493 additions and 269 deletions

View File

@@ -23,7 +23,7 @@ internal struct ComponentInfo
public int size;
public int alignment;
public bool isEnableable;
public bool isShared;
public bool isSharedWarper;
}
/// <summary>
@@ -42,7 +42,9 @@ internal static class ComponentRegistry
private static readonly Dictionary<IntPtr, int> s_typeHandleToID = new();
private static readonly Dictionary<string, int> s_nameToRuntimeID = new();
#if DEBUG || GHOST_EDITOR
internal static readonly Dictionary<int, Type> s_runtimeIDToType = new();
#endif
public static unsafe Identifier<IComponent> GetOrRegisterComponentID<T>()
where T : unmanaged, IComponent
@@ -61,19 +63,21 @@ internal static class ComponentRegistry
var stableName = typeof(T).FullName ?? typeof(T).Name;
var info = new ComponentInfo
{
// stableName = new FixedText64(stableName),
// stableName = stableName,
id = newID,
size = sizeof(T),
alignment = (int)MemoryUtility.AlignOf<T>(),
isEnableable = typeof(IEnableableComponent).IsAssignableFrom(type),
//isShared = typeof(ISharedComponent).IsAssignableFrom(type),
isSharedWarper = typeof(ISharedWarper).IsAssignableFrom(type),
};
s_registeredComponents.Add(info);
s_typeHandleToID[typeHandle] = newID;
s_nameToRuntimeID[stableName] = newID;
#if DEBUG || GHOST_EDITOR
s_runtimeIDToType[newID.Value] = typeof(T);
#endif
return newID;
}
@@ -143,7 +147,7 @@ internal static class ComponentRegistry
}
}
public class ComponentManager : IDisposable
public partial class ComponentManager : IDisposable
{
private readonly World _world;

View File

@@ -24,7 +24,7 @@ public readonly struct Managed<T> : IComponent, IManagedWrapper
public static class ManagedComponemtnID<T>
where T : IManagedComponent
{
public static readonly Identifier<IManagedComponent> value = ManagedComponentRegistry.GetOrRegisterComponent<T>();
public static readonly Identifier<IManagedComponent> Value = ManagedComponentRegistry.GetOrRegisterComponent<T>();
}
internal struct ManagedComponentInfo
@@ -67,7 +67,7 @@ internal static class ManagedComponentRegistry
s_typeHandleToID[typeHandle] = newID;
s_nameToRuntimeID[stableName] = newID;
#if DEBUG || GHOST_EDITOR
s_runtimeIDToType[newID.value] = typeof(T);
s_runtimeIDToType[newID.Value] = typeof(T);
#endif
return newID;
@@ -109,7 +109,7 @@ internal class ManagedComponentStorage<T> : IManagedComponentStorage
{
private readonly SlotMap<T> _storage = new(16);
public Identifier<IManagedComponent> TypeID => ManagedComponemtnID<T>.value;
public Identifier<IManagedComponent> TypeID => ManagedComponemtnID<T>.Value;
public Managed<T> Add(T component)
{
@@ -177,20 +177,20 @@ public abstract class ScriptComponent : IManagedComponent
public partial class EntityManager
{
private IManagedComponentStorage[] _storages;
private IManagedComponentStorage[] _managedStorages;
internal IManagedComponentStorage[] Storages => _storages;
internal IManagedComponentStorage[] ManagedStorages => _managedStorages;
private ManagedComponentStorage<T> GetOrCreateManagedComponentStorage<T>()
where T : IManagedComponent
{
var id = ManagedComponemtnID<T>.value;
if (_storages == null || _storages.Length <= id.value)
var id = ManagedComponemtnID<T>.Value;
if (_managedStorages == null || _managedStorages.Length <= id.Value)
{
Array.Resize(ref _storages, id.value + 1);
Array.Resize(ref _managedStorages, id.Value + 1);
}
ref var storage = ref _storages[id.value];
ref var storage = ref _managedStorages[id.Value];
storage ??= new ManagedComponentStorage<T>();
return (ManagedComponentStorage<T>)storage;

View File

@@ -103,9 +103,9 @@ public readonly unsafe ref struct ChunkView
_layouts = archetype._layouts.AsReadOnly();
_layoutIndexLookup = archetype._componentIDToLayoutIndex.AsReadOnly();
_pChunkData = chunk.GetUnsafePtr();
_pVersion = chunk.GetVersionUnsafePtr();
_entityOffset = archetype.EntityIDsOffset;
_entityCount = chunk._count;
_pVersion = chunk.GetVersionUnsafePtr();
_structuralVersion = chunk._structuralVersion;
_currentVersion = World.GetWorldUncheck(archetype.WorldID).Version;
@@ -476,7 +476,7 @@ public unsafe partial struct EntityQuery : IDisposable
}
}
public ref partial struct QueryBuilder
public ref partial struct QueryBuilder : IDisposable
{
private readonly Stack.Scope _scope;
@@ -503,52 +503,57 @@ public ref partial struct QueryBuilder
_rw = new UnsafeList<Identifier<IComponent>>(4, _scope.AllocationHandle);
}
public static QueryBuilder Create()
{
return new QueryBuilder();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void FindMax(UnsafeList<Identifier<IComponent>> list, ref int max)
{
foreach (var id in list)
{
if (id.Value > max) max = id.Value;
max = Math.Max(max, id.Value);
}
}
public void WithAll(params Span<Identifier<IComponent>> componentIDs)
{
_all.AddRange(componentIDs, componentIDs.Length);
_all.AddRange(componentIDs);
}
public void WithAny(params Span<Identifier<IComponent>> componentIDs)
{
_any.AddRange(componentIDs, componentIDs.Length);
_any.AddRange(componentIDs);
}
public void WithAbsent(params Span<Identifier<IComponent>> componentIDs)
{
_absent.AddRange(componentIDs, componentIDs.Length);
_absent.AddRange(componentIDs);
}
public void WithNone(params Span<Identifier<IComponent>> componentIDs)
{
_none.AddRange(componentIDs, componentIDs.Length);
_none.AddRange(componentIDs);
}
public void WithDisabled(params Span<Identifier<IComponent>> componentIDs)
{
_disabled.AddRange(componentIDs, componentIDs.Length);
_disabled.AddRange(componentIDs);
}
public void WithPresent(params Span<Identifier<IComponent>> componentIDs)
{
_present.AddRange(componentIDs, componentIDs.Length);
_present.AddRange(componentIDs);
}
public void WithPresentRW(params Span<Identifier<IComponent>> componentIDs)
{
_present.AddRange(componentIDs, componentIDs.Length);
_rw.AddRange(componentIDs, componentIDs.Length);
_present.AddRange(componentIDs);
_rw.AddRange(componentIDs);
}
public Identifier<EntityQuery> Build(World world, Allocator allocator = Allocator.Persistent)
private void BuildQueryMask(AllocationHandle allocationHandle, out EntityQueryMask mask)
{
// 1. Calculate max component ID to size the BitSets
var maxID = 0;
@@ -560,16 +565,16 @@ public ref partial struct QueryBuilder
FindMax(_present, ref maxID);
// 2. Create the Mask
var mask = new EntityQueryMask
mask = new EntityQueryMask
{
structuralAll = new UnsafeBitSet(maxID + 1, allocator, AllocationOption.Clear),
structuralAny = new UnsafeBitSet(maxID + 1, allocator, AllocationOption.Clear),
structuralAbsent = new UnsafeBitSet(maxID + 1, allocator, AllocationOption.Clear),
requireEnabled = new UnsafeBitSet(maxID + 1, allocator, AllocationOption.Clear),
requireDisabled = new UnsafeBitSet(maxID + 1, allocator, AllocationOption.Clear),
rejectIfEnabled = new UnsafeBitSet(maxID + 1, allocator, AllocationOption.Clear),
structuralAll = new UnsafeBitSet(maxID + 1, allocationHandle, AllocationOption.Clear),
structuralAny = new UnsafeBitSet(maxID + 1, allocationHandle, AllocationOption.Clear),
structuralAbsent = new UnsafeBitSet(maxID + 1, allocationHandle, AllocationOption.Clear),
requireEnabled = new UnsafeBitSet(maxID + 1, allocationHandle, AllocationOption.Clear),
requireDisabled = new UnsafeBitSet(maxID + 1, allocationHandle, AllocationOption.Clear),
rejectIfEnabled = new UnsafeBitSet(maxID + 1, allocationHandle, AllocationOption.Clear),
writeAccess = new UnsafeBitSet(maxID + 1, allocator, AllocationOption.Clear),
writeAccess = new UnsafeBitSet(maxID + 1, allocationHandle, AllocationOption.Clear),
};
// 3. Fill BitSets
@@ -616,8 +621,30 @@ public ref partial struct QueryBuilder
{
mask.writeAccess.SetBit(id);
}
}
public EntityQuery BuildWithoutCache(World world, AllocationHandle allocationHandle, bool dispose = true)
{
BuildQueryMask(allocationHandle, out var mask);
var query = new EntityQuery(Identifier<EntityQuery>.Invalid, world.ID, ref mask);
for (var i = 0; i < world.ComponentManager.ArchetypeCount; i++)
{
query.AddArchetypeIfMatch(in world.ComponentManager.GetArchetypeReference(i));
}
if (dispose)
{
Dispose();
}
return query;
}
public Identifier<EntityQuery> Build(World world, bool dispose = true)
{
BuildQueryMask(AllocationManager.GetAllocationHandle(Allocator.Persistent), out var mask);
// 4. Ask World for the Query (Cached)
var maskHash = mask.GetHashCode();
var queryID = world.ComponentManager.GetEntityQueryIDByMaskHash(maskHash);
if (queryID.IsValid)
@@ -635,11 +662,26 @@ public ref partial struct QueryBuilder
queryID = world.ComponentManager.CreateEntityQuery(in mask, maskHash);
Return:
Dispose();
if (dispose)
{
Dispose();
}
return queryID;
}
private readonly void Dispose()
public void Clear()
{
_all.Clear();
_any.Clear();
_absent.Clear();
_none.Clear();
_disabled.Clear();
_present.Clear();
_rw.Clear();
}
public readonly void Dispose()
{
_scope.Dispose();
}

View File

@@ -1,56 +1,67 @@
#if false
using Ghost.Core;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Diagnostics;
namespace Ghost.Entities;
public interface ISharedComponent
public interface ISharedComponent;
public interface ISharedWarper
{
public int Index
{
get; set;
}
}
public struct Shared<T> : IComponent, ISharedWarper
where T : unmanaged, ISharedComponent
{
public int Index
{
get; set;
}
}
internal unsafe sealed class SharedComponentStore : IDisposable
{
private struct EntryInfo
{
public int RefCount;
public int HashCode;
public int Version;
public int NextFree; // free-list linkage (index)
public int refCount;
public int hashCode;
public int version;
public int nextFree; // free-list linkage (index)
}
private struct TypeStore : IDisposable
{
public int TypeSize;
public UnsafeList<byte> Data; // raw bytes, stride = TypeSize
public UnsafeList<EntryInfo> Infos; // parallel to Data entries (Entry 0 reserved)
public UnsafeHashMap<long, int> HashLookup; // (hashKey) -> entryIndex
public int FreeListHead; // head index, 0 means none (we'll use Infos[0].NextFree too if you prefer)
public int VersionCounter;
public int typeSize;
public UnsafeList<byte> data; // raw bytes, stride = TypeSize
public UnsafeList<EntryInfo> infos; // parallel to Data entries (Entry 0 reserved)
public UnsafeHashMap<long, int> hashLookup; // (hashKey) -> entryIndex
public int freeListHead; // head index, 0 means none (we'll use Infos[0].NextFree too if you prefer)
public int versionCounter;
public void Dispose()
{
Data.Dispose();
Infos.Dispose();
HashLookup.Dispose();
data.Dispose();
infos.Dispose();
hashLookup.Dispose();
}
}
private readonly UnsafeHashMap<int, TypeStore> _perType; // componentTypeId -> TypeStore
private UnsafeHashMap<int, TypeStore> _perType; // componentTypeId -> TypeStore
private bool _disposed;
public SharedComponentStore(int initialCapacity = 16)
{
_perType = new UnsafeHashMap<int, TypeStore>(initialCapacity, Allocator.Persistent);
}
public void Dispose()
~SharedComponentStore()
{
foreach (var kvp in _perType)
{
kvp.Value.Dispose();
}
_perType.Dispose();
Dispose();
}
public int InsertOrGet(int componentTypeId, int typeSize, void* data, int hashCode)
@@ -66,12 +77,12 @@ internal unsafe sealed class SharedComponentStore : IDisposable
// Combine (typeId, hash) into a single key; collisions handled by memcmp below.
var key = ((long)componentTypeId << 32) ^ (uint)hashCode;
if (store.HashLookup.TryGetValue(key, out var existingIndex))
if (store.hashLookup.TryGetValue(key, out var existingIndex))
{
var existingPtr = (byte*)store.Data.GetUnsafePtr() + (existingIndex * store.TypeSize);
if (new Span<byte>(existingPtr, store.TypeSize).SequenceEqual(new Span<byte>(data, store.TypeSize)))
var existingPtr = (byte*)store.data.GetUnsafePtr() + (existingIndex * store.typeSize);
if (new Span<byte>(existingPtr, store.typeSize).SequenceEqual(new Span<byte>(data, store.typeSize)))
{
((EntryInfo*)store.Infos.GetUnsafePtr())[existingIndex].RefCount++;
((EntryInfo*)store.infos.GetUnsafePtr())[existingIndex].refCount++;
return existingIndex;
}
// If collision: fall through to insert (you may want a secondary structure).
@@ -79,98 +90,167 @@ internal unsafe sealed class SharedComponentStore : IDisposable
int index = AllocateEntry(ref store);
var dst = (byte*)store.Data.GetUnsafePtr() + (index * store.TypeSize);
MemoryUtility.MemCpy(dst, data, (nuint)store.TypeSize);
var dst = (byte*)store.data.GetUnsafePtr() + (index * store.typeSize);
MemoryUtility.MemCpy(dst, data, (nuint)store.typeSize);
store.Infos[index] = new EntryInfo
store.infos[index] = new EntryInfo
{
RefCount = 1,
HashCode = hashCode,
Version = ++store.VersionCounter,
NextFree = -1
refCount = 1,
hashCode = hashCode,
version = ++store.versionCounter,
nextFree = -1
};
store.HashLookup[key] = index;
store.hashLookup[key] = index;
return index;
}
public void AddRef(int componentTypeId, int index)
public RefResult<T, Error> GetComponentData<T>(int componentTypeId, int index)
where T : unmanaged
{
if (index == 0) return;
ref var store = ref _perType[componentTypeId];
store.Infos[index].RefCount++;
if (index == 0)
{
return Error.InvalidArgument;
}
ref var store = ref _perType.GetValueRef(componentTypeId, out var exist);
if (!exist || index >= store.infos.Count)
{
return Error.NotFound;
}
var info = store.infos[index];
if (info.refCount <= 0)
{
return Error.NotFound;
}
var dataPtr = (byte*)store.data.GetUnsafePtr() + (index * store.typeSize);
return new RefResult<T, Error>(ref *(T*)dataPtr, Error.Success);
}
public Error AddRef(int componentTypeId, int index)
{
if (index == 0)
{
return Error.Success;
}
ref var store = ref _perType.GetValueRef(componentTypeId, out var exist);
if (!exist)
{
return Error.NotFound;
}
store.infos[index].refCount++;
return Error.Success;
}
public void Release(int componentTypeId, int index)
{
if (index == 0) return;
ref var store = ref _perType.GetValueByKey(componentTypeId);
if (index == 0)
{
return;
}
ref var info = ref store.Infos.Ptr[index];
info.RefCount--;
if (info.RefCount > 0) return;
ref var store = ref _perType.GetValueRef(componentTypeId, out var exist);
if (!exist)
{
return;
}
ref var info = ref store.infos[index];
info.refCount--;
if (info.refCount > 0)
{
return;
}
// Remove from hash lookup (best-effort; collisions require more robust handling)
long key = ((long)componentTypeId << 32) ^ (uint)info.HashCode;
store.HashLookup.Remove(key);
long key = ((long)componentTypeId << 32) ^ (uint)info.hashCode;
store.hashLookup.Remove(key);
// Push to free-list
info.NextFree = store.FreeListHead;
store.FreeListHead = index;
info.nextFree = store.freeListHead;
store.freeListHead = index;
}
public void* GetDataPtr(int componentTypeId, int index)
{
if (index == 0) return null;
ref var store = ref _perType.GetValueByKey(componentTypeId);
return (byte*)store.Data.Ptr + (index * store.TypeSize);
if (index == 0)
{
return null;
}
ref var store = ref _perType.GetValueRef(componentTypeId, out var exist);
if (!exist)
{
return null;
}
return (byte*)store.data.GetUnsafePtr() + (index * store.typeSize);
}
private ref TypeStore GetOrCreateTypeStore(int componentTypeId, int typeSize)
{
if (_perType.TryGetValue(componentTypeId, out var existing))
ref var existing = ref _perType.GetValueRef(componentTypeId, out var exist);
if (exist)
{
// UnsafeHashMap returns by value in some implementations; you may need a different pattern here.
// Adjust to your container API (e.g., TryGetValueRef).
return ref existing;
}
var store = new TypeStore
{
TypeSize = typeSize,
Data = new UnsafeList<byte>(typeSize * 16, Allocator.Persistent),
Infos = new UnsafeList<EntryInfo>(16, Allocator.Persistent),
HashLookup = new UnsafeHashMap<long, int>(16, Allocator.Persistent),
FreeListHead = 0,
VersionCounter = 0
typeSize = typeSize,
data = new UnsafeList<byte>(typeSize * 16, Allocator.Persistent),
infos = new UnsafeList<EntryInfo>(16, Allocator.Persistent),
hashLookup = new UnsafeHashMap<long, int>(16, Allocator.Persistent),
freeListHead = 0,
versionCounter = 0
};
// Create reserved default entry at index 0
store.Data.Resize(typeSize); // one element worth of bytes
store.Infos.Add(new EntryInfo { RefCount = int.MaxValue, HashCode = 0, Version = 0, NextFree = -1 });
store.data.Resize(typeSize); // one element worth of bytes
store.infos.Add(new EntryInfo { refCount = int.MaxValue, hashCode = 0, version = 0, nextFree = -1 });
_perType.Add(componentTypeId, store);
// NOTE: returning a ref requires a "get ref" API; adjust to your UnsafeHashMap capabilities.
return ref _perType.GetValueByKey(componentTypeId);
existing = ref _perType.GetValueRef(componentTypeId, out exist);
Debug.Assert(exist);
return ref existing;
}
private static int AllocateEntry(ref TypeStore store)
{
if (store.FreeListHead != 0)
if (store.freeListHead != 0)
{
int idx = store.FreeListHead;
store.FreeListHead = store.Infos[idx].NextFree;
store.Infos[idx].NextFree = -1;
int idx = store.freeListHead;
store.freeListHead = store.infos[idx].nextFree;
store.infos[idx].nextFree = -1;
return idx;
}
int newIndex = store.Infos.Count;
store.Infos.Add(default);
int newIndex = store.infos.Count;
store.infos.Add(default);
int newByteCount = (newIndex + 1) * store.TypeSize;
store.Data.Resize(newByteCount);
int newByteCount = (newIndex + 1) * store.typeSize;
store.data.Resize(newByteCount);
return newIndex;
}
}
#endif
public void Dispose()
{
foreach (var kvp in _perType)
{
kvp.Value.Dispose();
}
_perType.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
}
}

View File

@@ -8,15 +8,15 @@ public readonly ref struct SystemAPI
{
get; init;
}
public World World
{
get; init;
}
}
public interface ISystem
{
World World
{
get; init;
}
void Initialize(ref readonly SystemAPI systemAPI);
void Update(ref readonly SystemAPI systemAPI);
void Cleanup(ref readonly SystemAPI systemAPI);
@@ -277,11 +277,7 @@ public abstract class SystemGroup : ISystem
public void AddSystem<T>()
where T : ISystem, new()
{
_systems.Add(new T()
{
World = World
});
_systems.Add(new T());
_version++;
}
@@ -342,9 +338,7 @@ public abstract class SystemGroup : ISystem
}
}
public class DefaultSystemGroup : SystemGroup
{
}
public class DefaultSystemGroup : SystemGroup;
public class SystemManager
{
@@ -363,10 +357,7 @@ public class SystemManager
public void AddSystem<T>()
where T : ISystem, new()
{
_systems.Add(new T()
{
World = _world
});
_systems.Add(new T());
}
public T GetSystem<T>()
@@ -383,24 +374,42 @@ public class SystemManager
throw new InvalidOperationException($"System of type {typeof(T).FullName} not found in SystemManager.");
}
internal void InitializeAll(ref readonly SystemAPI systemAPI)
internal void InitializeAll(TimeData timeData)
{
var systemAPI = new SystemAPI
{
Time = timeData,
World = _world
};
foreach (var system in _systems)
{
system.Initialize(in systemAPI);
}
}
internal void UpdateAll(ref readonly SystemAPI systemAPI)
internal void UpdateAll(TimeData timeData)
{
var systemAPI = new SystemAPI
{
Time = timeData,
World = _world
};
foreach (var system in _systems)
{
system.Update(in systemAPI);
}
}
internal void CleanupAll(ref readonly SystemAPI systemAPI)
internal void CleanupAll(TimeData timeData)
{
var systemAPI = new SystemAPI
{
Time = timeData,
World = _world
};
foreach (var system in _systems)
{
system.Cleanup(in systemAPI);