Files
GhostEngine/src/Runtime/Ghost.Entities/SharedComponent.cs

257 lines
6.9 KiB
C#

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 ISharedWrapper
{
int Index
{
get; set;
}
}
public struct Shared<T> : IComponent, ISharedWrapper
where T : unmanaged, ISharedComponent
{
public int Index
{
get; set;
}
}
internal sealed unsafe class SharedComponentStore : IDisposable
{
private struct EntryInfo
{
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 void Dispose()
{
data.Dispose();
infos.Dispose();
hashLookup.Dispose();
}
}
private UnsafeHashMap<int, TypeStore> _perType; // componentTypeId -> TypeStore
private bool _disposed;
public SharedComponentStore(int initialCapacity = 16)
{
_perType = new UnsafeHashMap<int, TypeStore>(initialCapacity, AllocationHandle.Persistent);
}
~SharedComponentStore()
{
Dispose();
}
public int InsertOrGet(int componentTypeId, int typeSize, void* data, int hashCode)
{
// Reserve index 0 for "default"
if (data == null)
{
return 0;
}
ref var store = ref GetOrCreateTypeStore(componentTypeId, typeSize);
// 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))
{
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++;
return existingIndex;
}
// If collision: fall through to insert (you may want a secondary structure).
}
var index = AllocateEntry(ref store);
var dst = (byte*)store.data.GetUnsafePtr() + (index * store.typeSize);
MemoryUtility.MemCpy(dst, data, (nuint)store.typeSize);
store.infos[index] = new EntryInfo
{
refCount = 1,
hashCode = hashCode,
version = ++store.versionCounter,
nextFree = -1
};
store.hashLookup[key] = index;
return index;
}
public RefResult<T, Error> GetComponentData<T>(int componentTypeId, int index)
where T : unmanaged
{
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.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)
var key = ((long)componentTypeId << 32) ^ (uint)info.hashCode;
store.hashLookup.Remove(key);
// Push to free-list
info.nextFree = store.freeListHead;
store.freeListHead = index;
}
public void* GetDataPtr(int componentTypeId, int index)
{
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)
{
ref var existing = ref _perType.GetValueRef(componentTypeId, out var exist);
if (exist)
{
return ref existing;
}
var store = new TypeStore
{
typeSize = typeSize,
data = new UnsafeList<byte>(typeSize * 16, AllocationHandle.Persistent),
infos = new UnsafeList<EntryInfo>(16, AllocationHandle.Persistent),
hashLookup = new UnsafeHashMap<long, int>(16, AllocationHandle.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 });
_perType.Add(componentTypeId, store);
existing = ref _perType.GetValueRef(componentTypeId, out exist);
Logger.DebugAssert(exist);
return ref existing;
}
private static int AllocateEntry(ref TypeStore store)
{
if (store.freeListHead != 0)
{
var idx = store.freeListHead;
store.freeListHead = store.infos[idx].nextFree;
store.infos[idx].nextFree = -1;
return idx;
}
var newIndex = store.infos.Count;
store.infos.Add(default);
var newByteCount = (newIndex + 1) * store.typeSize;
store.data.Resize(newByteCount);
return newIndex;
}
public void Dispose()
{
foreach (var kvp in _perType)
{
kvp.Value.Dispose();
}
_perType.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
}
}