Added new RHI abstraction layer;
Added new console debug page to UnitTest;
This commit is contained in:
@@ -39,4 +39,24 @@ public readonly struct TypeHandle
|
|||||||
{
|
{
|
||||||
return Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(Value));
|
return Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static implicit operator TypeHandle(IntPtr value)
|
||||||
|
{
|
||||||
|
return new TypeHandle(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator IntPtr(TypeHandle handle)
|
||||||
|
{
|
||||||
|
return handle.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator TypeHandle(Type type)
|
||||||
|
{
|
||||||
|
return Get(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator Type?(TypeHandle handle)
|
||||||
|
{
|
||||||
|
return handle.ToType();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,11 @@ internal unsafe sealed partial class ComponentDataView : Control
|
|||||||
for (var i = 0; i < fields.Length; i++)
|
for (var i = 0; i < fields.Length; i++)
|
||||||
{
|
{
|
||||||
var field = fields[i];
|
var field = fields[i];
|
||||||
var component = _world.ComponentStorage.ComponentPools[TypeHandle.Get(_componentType)].Get(_entity);
|
if (!_world.ComponentStorage.TryGetPool(TypeHandle.Get(_componentType), out var pool))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var component = pool.Get(_entity);
|
||||||
var propertyField = PropertyField.Create(field.Name, field, component);
|
var propertyField = PropertyField.Create(field.Name, field, component);
|
||||||
|
|
||||||
_propertyFields[i] = propertyField;
|
_propertyFields[i] = propertyField;
|
||||||
|
|||||||
@@ -104,12 +104,18 @@ internal class WorldNodeSerializer : JsonConverter<WorldNode>
|
|||||||
|
|
||||||
writer.WriteObject(Property.COMPONENTS, () =>
|
writer.WriteObject(Property.COMPONENTS, () =>
|
||||||
{
|
{
|
||||||
foreach (var kvp in value.World.ComponentStorage.ComponentPools)
|
for (var i = 0; i < value.World.ComponentStorage.ComponentPools.Count; i++)
|
||||||
{
|
{
|
||||||
var type = kvp.Key.ToType() ?? throw new Exception($"Type {kvp.Key} not found.");
|
var pool = value.World.ComponentStorage.ComponentPools[i];
|
||||||
|
if (pool == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = value.World.ComponentStorage.GetComponentPoolType(i).GetType();
|
||||||
var typeName = type.AssemblyQualifiedName ?? type.Name;
|
var typeName = type.AssemblyQualifiedName ?? type.Name;
|
||||||
|
|
||||||
writer.WriteArray(typeName, kvp.Value.Enumerate(), data =>
|
writer.WriteArray(typeName, pool.Enumerate(), data =>
|
||||||
{
|
{
|
||||||
writer.WriteObject(() =>
|
writer.WriteObject(() =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Ghost.Core.Attributes;
|
using Ghost.Core.Attributes;
|
||||||
using Ghost.Entities;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Utilities;
|
namespace Ghost.Editor.Core.Utilities;
|
||||||
@@ -34,29 +33,4 @@ public static class TypeCache
|
|||||||
{
|
{
|
||||||
return _types;
|
return _types;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static class ComponentTypeCache
|
|
||||||
{
|
|
||||||
private static readonly Type?[][] _componentTypes;
|
|
||||||
|
|
||||||
static ComponentTypeCache()
|
|
||||||
{
|
|
||||||
_componentTypes = new Type[World.WorldCount][];
|
|
||||||
for (var i = 0; i < World.WorldCount; i++)
|
|
||||||
{
|
|
||||||
var world = World.GetWorld(i);
|
|
||||||
var typeHandles = world.ComponentStorage.ComponentPools.Keys;
|
|
||||||
_componentTypes[i] = typeHandles.Select(handle => handle.ToType()).ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Type?[] GetComponentTypes(int worldIndex)
|
|
||||||
{
|
|
||||||
if (worldIndex < 0 || worldIndex >= _componentTypes.Length)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(worldIndex), "Invalid world index.");
|
|
||||||
}
|
|
||||||
return _componentTypes[worldIndex];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -217,8 +217,8 @@ internal class ScriptComponentPool : IComponentPool<ScriptComponent>
|
|||||||
private Dictionary<Entity, List<ScriptComponent>>? _scriptComponents;
|
private Dictionary<Entity, List<ScriptComponent>>? _scriptComponents;
|
||||||
private List<ScriptComponent>? _executionList;
|
private List<ScriptComponent>? _executionList;
|
||||||
|
|
||||||
internal Dictionary<Entity, List<ScriptComponent>>? ScriptComponents => _scriptComponents;
|
internal IReadOnlyDictionary<Entity, List<ScriptComponent>>? ScriptComponents => _scriptComponents;
|
||||||
internal List<ScriptComponent>? ExecutionList => _executionList;
|
internal IReadOnlyList<ScriptComponent>? ExecutionList => _executionList;
|
||||||
|
|
||||||
public bool IsInitialized => _scriptComponents != null;
|
public bool IsInitialized => _scriptComponents != null;
|
||||||
public int Count => _scriptComponents?.Keys.Count ?? 0;
|
public int Count => _scriptComponents?.Keys.Count ?? 0;
|
||||||
@@ -461,10 +461,22 @@ internal class ScriptComponentPool : IComponentPool<ScriptComponent>
|
|||||||
}
|
}
|
||||||
|
|
||||||
[SkipLocalsInit]
|
[SkipLocalsInit]
|
||||||
internal readonly struct ComponentStorage : IDisposable
|
internal struct ComponentStorage : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Dictionary<TypeHandle, IComponentPool> _componentPools = new();
|
private static int s_nextId = 0;
|
||||||
private readonly Dictionary<TypeHandle, BitSet> _componentEntityMasks = new();
|
private static class TypeID<T>
|
||||||
|
{
|
||||||
|
public static readonly int value = s_nextId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _currentCapacity = 16;
|
||||||
|
|
||||||
|
private IComponentPool?[] _componentPools = new IComponentPool[16];
|
||||||
|
private BitSet?[] _componentEntityMasks = new BitSet[16];
|
||||||
|
|
||||||
|
private readonly Dictionary<TypeHandle, int> _typeIDMap = new(16);
|
||||||
|
private readonly Dictionary<int, TypeHandle> _typeHandleMap = new(16);
|
||||||
|
|
||||||
private readonly ScriptComponentPool _scriptComponentPool = new();
|
private readonly ScriptComponentPool _scriptComponentPool = new();
|
||||||
|
|
||||||
private readonly World _world;
|
private readonly World _world;
|
||||||
@@ -474,92 +486,225 @@ internal readonly struct ComponentStorage : IDisposable
|
|||||||
_world = world;
|
_world = world;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Dictionary<TypeHandle, IComponentPool> ComponentPools => _componentPools;
|
internal readonly IReadOnlyList<IComponentPool?> ComponentPools => _componentPools;
|
||||||
internal Dictionary<TypeHandle, BitSet> ComponentEntityMasks => _componentEntityMasks;
|
internal readonly IReadOnlyList<BitSet?> ComponentEntityMasks => _componentEntityMasks;
|
||||||
internal ScriptComponentPool ScriptComponentPool => _scriptComponentPool;
|
internal readonly ScriptComponentPool ScriptComponentPool => _scriptComponentPool;
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool TryGetPool(TypeHandle typeHandle, [MaybeNullWhen(false)] out IComponentPool pool)
|
private readonly int GetTypeID(TypeHandle typeHandle)
|
||||||
{
|
{
|
||||||
return _componentPools.TryGetValue(typeHandle, out pool);
|
if (_typeIDMap.TryGetValue(typeHandle, out var id))
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof(TypeID<>).MakeGenericType(typeHandle!)
|
||||||
|
.GetField("value", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public)
|
||||||
|
?.GetValue(null) as int? ?? -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool TryGetPool(Type type, [MaybeNullWhen(false)] out IComponentPool pool)
|
private readonly int GetTypeID<T>()
|
||||||
|
{
|
||||||
|
return TypeID<T>.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void Resize(int newCapacity)
|
||||||
|
{
|
||||||
|
Array.Resize(ref _componentPools, newCapacity);
|
||||||
|
Array.Resize(ref _componentEntityMasks, newCapacity);
|
||||||
|
_currentCapacity = newCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
internal readonly TypeHandle GetComponentPoolType(int poolIndex)
|
||||||
|
{
|
||||||
|
if (poolIndex < 0 || poolIndex >= _currentCapacity)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(poolIndex), "Invalid pool index.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return _typeHandleMap[poolIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public readonly bool TryGetPool(TypeHandle typeHandle, [NotNullWhen(true)] out IComponentPool? pool)
|
||||||
|
{
|
||||||
|
var result = _typeIDMap.TryGetValue(typeHandle, out var id);
|
||||||
|
if (!result || id >= _currentCapacity)
|
||||||
|
{
|
||||||
|
pool = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pool = _componentPools[id];
|
||||||
|
return pool != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public readonly bool TryGetPool(Type type, [NotNullWhen(true)] out IComponentPool? pool)
|
||||||
{
|
{
|
||||||
return TryGetPool(TypeHandle.Get(type), out pool);
|
return TryGetPool(TypeHandle.Get(type), out pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool TryGetPool<T>([MaybeNullWhen(false)] out ComponentPool<T> pool)
|
public readonly bool TryGetPool<T>([NotNullWhen(true)] out ComponentPool<T>? pool)
|
||||||
where T : unmanaged, IComponentData
|
where T : unmanaged, IComponentData
|
||||||
{
|
{
|
||||||
var result = TryGetPool(TypeHandle.Get<T>(), out var obj);
|
var id = TypeID<T>.value;
|
||||||
pool = (ComponentPool<T>?)obj ?? default;
|
if (id >= _currentCapacity)
|
||||||
return result;
|
{
|
||||||
|
pool = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pool = (ComponentPool<T>?)_componentPools[id];
|
||||||
|
return pool != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IComponentPool GetOrCreateComponentPool(Type type)
|
||||||
|
{
|
||||||
|
var typeHandle = TypeHandle.Get(type);
|
||||||
|
if (_typeIDMap.TryGetValue(typeHandle, out var id))
|
||||||
|
{
|
||||||
|
if (id < _currentCapacity && _componentPools[id] is IComponentPool existingPool)
|
||||||
|
{
|
||||||
|
return existingPool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
id = GetTypeID(typeHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id >= _currentCapacity)
|
||||||
|
{
|
||||||
|
Resize(_currentCapacity * 2);
|
||||||
|
_typeIDMap[typeHandle] = id;
|
||||||
|
_typeHandleMap[id] = typeHandle;
|
||||||
|
}
|
||||||
|
else if (_componentPools[id] is IComponentPool existingPool)
|
||||||
|
{
|
||||||
|
return existingPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pool = Activator.CreateInstance(typeof(ComponentPool<>).MakeGenericType(type)) as IComponentPool
|
||||||
|
?? throw new InvalidOperationException($"Failed to create component pool for type {type.FullName}");
|
||||||
|
|
||||||
|
_componentPools[id] = pool;
|
||||||
|
|
||||||
|
return pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComponentPool<T> GetOrCreateComponentPool<T>()
|
public ComponentPool<T> GetOrCreateComponentPool<T>()
|
||||||
where T : unmanaged, IComponentData
|
where T : unmanaged, IComponentData
|
||||||
{
|
{
|
||||||
var key = TypeHandle.Get<T>();
|
var id = TypeID<T>.value;
|
||||||
if (!_componentPools.TryGetValue(key, out var obj))
|
var typeHandle = TypeHandle.Get<T>();
|
||||||
|
|
||||||
|
if (id >= _currentCapacity)
|
||||||
{
|
{
|
||||||
var pool = new ComponentPool<T>(16);
|
Resize(_currentCapacity * 2);
|
||||||
_componentPools[key] = pool;
|
_typeIDMap[typeHandle] = id;
|
||||||
return pool;
|
_typeHandleMap[id] = typeHandle;
|
||||||
|
}
|
||||||
|
else if (_componentPools[id] is ComponentPool<T> existingPool)
|
||||||
|
{
|
||||||
|
return existingPool;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (ComponentPool<T>)obj;
|
var pool = new ComponentPool<T>();
|
||||||
|
_componentPools[id] = pool;
|
||||||
|
|
||||||
|
return pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool TryGetMask(TypeHandle typeHandle, [MaybeNullWhen(false)] out BitSet bitSet)
|
public readonly bool TryGetMask(TypeHandle typeHandle, [NotNullWhen(true)] out BitSet? bitSet)
|
||||||
{
|
{
|
||||||
return _componentEntityMasks.TryGetValue(typeHandle, out bitSet);
|
if (!_typeIDMap.TryGetValue(typeHandle, out var id)
|
||||||
|
|| id >= _currentCapacity)
|
||||||
|
{
|
||||||
|
bitSet = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bitSet = _componentEntityMasks[id];
|
||||||
|
return bitSet != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool TryGetMask(Type type, [MaybeNullWhen(false)] out BitSet bitSet)
|
public readonly bool TryGetMask<T>([NotNullWhen(true)] out BitSet? bitSet)
|
||||||
{
|
|
||||||
return TryGetMask(TypeHandle.Get(type), out bitSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public bool TryGetMask<T>([MaybeNullWhen(false)] out BitSet bitSet)
|
|
||||||
where T : unmanaged, IComponentData
|
where T : unmanaged, IComponentData
|
||||||
{
|
{
|
||||||
return TryGetMask(TypeHandle.Get<T>(), out bitSet);
|
return TryGetMask(TypeHandle.Get<T>(), out bitSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BitSet GetOrCreateMask(TypeHandle typeHandle)
|
public BitSet GetOrCreateMask<T>()
|
||||||
|
where T : unmanaged, IComponentData
|
||||||
{
|
{
|
||||||
if (!_componentEntityMasks.TryGetValue(typeHandle, out var mask))
|
var typeHandle = TypeHandle.Get<T>();
|
||||||
|
if (!_typeIDMap.TryGetValue(typeHandle, out var id))
|
||||||
{
|
{
|
||||||
mask = new BitSet();
|
id = GetTypeID<T>();
|
||||||
_componentEntityMasks[typeHandle] = mask;
|
_typeIDMap[typeHandle] = id;
|
||||||
|
_typeHandleMap[id] = typeHandle;
|
||||||
}
|
}
|
||||||
return mask;
|
|
||||||
|
if (id >= _currentCapacity)
|
||||||
|
{
|
||||||
|
Resize(_currentCapacity * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var set = ref _componentEntityMasks[id];
|
||||||
|
set ??= new BitSet();
|
||||||
|
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BitSet GetOrCreateMask(Type type)
|
||||||
|
{
|
||||||
|
var typeHandle = TypeHandle.Get(type);
|
||||||
|
if (!_typeIDMap.TryGetValue(typeHandle, out var id))
|
||||||
|
{
|
||||||
|
id = GetTypeID(typeHandle);
|
||||||
|
_typeIDMap[typeHandle] = id;
|
||||||
|
_typeHandleMap[id] = typeHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id >= _currentCapacity)
|
||||||
|
{
|
||||||
|
Resize(_currentCapacity * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var set = ref _componentEntityMasks[id];
|
||||||
|
set ??= new BitSet();
|
||||||
|
|
||||||
|
return set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void RebuildExecutionList()
|
public readonly void RebuildExecutionList()
|
||||||
{
|
{
|
||||||
_scriptComponentPool.RebuildExecutionList();
|
_scriptComponentPool.RebuildExecutionList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(Entity entity)
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public readonly void Remove(Entity entity)
|
||||||
{
|
{
|
||||||
_scriptComponentPool.Remove(entity);
|
_scriptComponentPool.Remove(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public readonly void Dispose()
|
||||||
{
|
{
|
||||||
foreach (var pool in _componentPools.Values)
|
foreach (var pool in _componentPools)
|
||||||
{
|
{
|
||||||
pool.Dispose();
|
pool?.Dispose();
|
||||||
}
|
}
|
||||||
_componentPools.Clear();
|
|
||||||
|
Array.Clear(_componentPools);
|
||||||
_scriptComponentPool.Dispose();
|
_scriptComponentPool.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,6 +44,7 @@ public readonly struct EntityManager : IDisposable
|
|||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
internal readonly void AddEntityInternal(Entity entity)
|
internal readonly void AddEntityInternal(Entity entity)
|
||||||
{
|
{
|
||||||
_entities.Add(entity);
|
_entities.Add(entity);
|
||||||
@@ -87,18 +88,19 @@ public readonly struct EntityManager : IDisposable
|
|||||||
return _entities[entity.ID].Generation == entity.Generation;
|
return _entities[entity.ID].Generation == entity.Generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly void AddComponent(Entity entity, IComponentData component, Type type)
|
/// <summary>
|
||||||
|
/// Adds a component of the specified type to the given entity.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method use reflection to determine the type of the component being added. Use generic as much as possible.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="entity">The entity to which the component will be added.</param>
|
||||||
|
/// <param name="component">The component data to associate with the entity.</param>
|
||||||
|
/// <param name="componentType">The type of the component being added. This must match the type of <paramref name="component"/>.</param>
|
||||||
|
public readonly void AddComponent(Entity entity, IComponentData component, Type componentType)
|
||||||
{
|
{
|
||||||
var typeHandle = TypeHandle.Get(type);
|
_world.ComponentStorage.GetOrCreateComponentPool(componentType).Add(entity, component);
|
||||||
ref var pool = ref CollectionsMarshal.GetValueRefOrAddDefault(_world.ComponentStorage.ComponentPools, typeHandle, out var exists);
|
_world.ComponentStorage.GetOrCreateMask(componentType).SetBit(entity.ID);
|
||||||
if (!exists)
|
|
||||||
{
|
|
||||||
var poolType = typeof(ComponentPool<>).MakeGenericType(type);
|
|
||||||
pool = (IComponentPool)(Activator.CreateInstance(poolType) ?? throw new InvalidOperationException($"Failed to create component pool for type {type}."));
|
|
||||||
}
|
|
||||||
|
|
||||||
pool!.Add(entity, component);
|
|
||||||
_world.ComponentStorage.GetOrCreateMask(typeHandle).SetBit(entity.ID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -112,7 +114,7 @@ public readonly struct EntityManager : IDisposable
|
|||||||
where T : unmanaged, IComponentData
|
where T : unmanaged, IComponentData
|
||||||
{
|
{
|
||||||
_world.ComponentStorage.GetOrCreateComponentPool<T>().Add(entity, component);
|
_world.ComponentStorage.GetOrCreateComponentPool<T>().Add(entity, component);
|
||||||
_world.ComponentStorage.GetOrCreateMask(TypeHandle.Get<T>()).SetBit(entity.ID);
|
_world.ComponentStorage.GetOrCreateMask<T>().SetBit(entity.ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -134,7 +136,7 @@ public readonly struct EntityManager : IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_world.ComponentStorage.GetOrCreateMask(TypeHandle.Get<T>()).ClearBit(entity.ID);
|
_world.ComponentStorage.GetOrCreateMask<T>().ClearBit(entity.ID);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -219,8 +221,13 @@ public readonly struct EntityManager : IDisposable
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public readonly IEnumerable<IComponentData> GetComponents(Entity entity)
|
public readonly IEnumerable<IComponentData> GetComponents(Entity entity)
|
||||||
{
|
{
|
||||||
foreach (var pool in _world.ComponentStorage.ComponentPools.Values)
|
foreach (var pool in _world.ComponentStorage.ComponentPools)
|
||||||
{
|
{
|
||||||
|
if (pool == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (pool.Has(entity))
|
if (pool.Has(entity))
|
||||||
{
|
{
|
||||||
yield return pool.Get(entity);
|
yield return pool.Get(entity);
|
||||||
@@ -241,11 +248,17 @@ public readonly struct EntityManager : IDisposable
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public readonly IEnumerable<(TypeHandle, IntPtr)> GetComponentsUnsafe(Entity entity)
|
public readonly IEnumerable<(TypeHandle, IntPtr)> GetComponentsUnsafe(Entity entity)
|
||||||
{
|
{
|
||||||
foreach (var (typeHandle, pool) in _world.ComponentStorage.ComponentPools)
|
for (var i = 0; i < _world.ComponentStorage.ComponentPools.Count; i++)
|
||||||
{
|
{
|
||||||
|
var pool = _world.ComponentStorage.ComponentPools[i];
|
||||||
|
if (pool == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (pool.Has(entity))
|
if (pool.Has(entity))
|
||||||
{
|
{
|
||||||
yield return (typeHandle, pool.GetUnsafe(entity));
|
yield return (_world.ComponentStorage.GetComponentPoolType(i), pool.GetUnsafe(entity));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,26 @@
|
|||||||
|
|
||||||
namespace Ghost.Entities.Systems;
|
namespace Ghost.Entities.Systems;
|
||||||
|
|
||||||
internal class SystemDependencyBuilder(List<Type> allSystemTypes)
|
internal class SystemDependencyBuilder
|
||||||
{
|
{
|
||||||
private Dictionary<Type, List<Type>> _dependencies = new();
|
private readonly Dictionary<Type, List<Type>> _dependencies = new();
|
||||||
|
private readonly List<Type> _systemTypes;
|
||||||
|
|
||||||
|
public SystemDependencyBuilder(List<Type> allSystemTypes)
|
||||||
|
{
|
||||||
|
_systemTypes = allSystemTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a dependency graph for all system types that implement the <see cref="ISystem"/> interface.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This method analyzes all system types and their dependencies, as defined by the <see
|
||||||
|
/// cref="DependsOnAttribute"/>. It validates that each system type is a concrete implementation of <see
|
||||||
|
/// cref="ISystem"/> and constructs a mapping of each system type to its direct dependencies.</remarks>
|
||||||
|
/// <exception cref="ArgumentException">Thrown if a type in <c>allSystemTypes</c> is not a concrete implementation of <see cref="ISystem"/>.</exception>
|
||||||
public void BuildDependencyGraph()
|
public void BuildDependencyGraph()
|
||||||
{
|
{
|
||||||
foreach (var systemType in allSystemTypes)
|
foreach (var systemType in _systemTypes)
|
||||||
{
|
{
|
||||||
if (!typeof(ISystem).IsAssignableFrom(systemType) || systemType.IsAbstract || systemType.IsInterface)
|
if (!typeof(ISystem).IsAssignableFrom(systemType) || systemType.IsAbstract || systemType.IsInterface)
|
||||||
{
|
{
|
||||||
@@ -45,7 +58,7 @@ internal class SystemDependencyBuilder(List<Type> allSystemTypes)
|
|||||||
foreach (var dependencyType in directDependencies)
|
foreach (var dependencyType in directDependencies)
|
||||||
{
|
{
|
||||||
// Ensure the dependency is a registered system type
|
// Ensure the dependency is a registered system type
|
||||||
if (!allSystemTypes.Contains(dependencyType))
|
if (!_systemTypes.Contains(dependencyType))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"System {systemType.Name} depends on unregistered system {dependencyType.Name}.");
|
throw new InvalidOperationException($"System {systemType.Name} depends on unregistered system {dependencyType.Name}.");
|
||||||
}
|
}
|
||||||
@@ -66,12 +79,12 @@ internal class SystemDependencyBuilder(List<Type> allSystemTypes)
|
|||||||
/// <exception cref="InvalidOperationException">Thrown if a circular dependency is detected.</exception>"
|
/// <exception cref="InvalidOperationException">Thrown if a circular dependency is detected.</exception>"
|
||||||
public List<Type> BuildExecutionOrder()
|
public List<Type> BuildExecutionOrder()
|
||||||
{
|
{
|
||||||
var executionOrder = new List<Type>(allSystemTypes.Count);
|
var executionOrder = new List<Type>(_systemTypes.Count);
|
||||||
var visited = new HashSet<Type>(); // Tracks visited nodes in the current DFS path (for cycle detection)
|
var visited = new HashSet<Type>(); // Tracks visited nodes in the current DFS path (for cycle detection)
|
||||||
var permanentMark = new HashSet<Type>(); // Tracks nodes whose dependencies have been fully resolved
|
var permanentMark = new HashSet<Type>(); // Tracks nodes whose dependencies have been fully resolved
|
||||||
|
|
||||||
// Initialize dependencies for all registered systems, even those without explicit attributes
|
// Initialize dependencies for all registered systems, even those without explicit attributes
|
||||||
foreach (var sysType in allSystemTypes)
|
foreach (var sysType in _systemTypes)
|
||||||
{
|
{
|
||||||
if (!_dependencies.ContainsKey(sysType))
|
if (!_dependencies.ContainsKey(sysType))
|
||||||
{
|
{
|
||||||
@@ -79,7 +92,7 @@ internal class SystemDependencyBuilder(List<Type> allSystemTypes)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var systemType in allSystemTypes)
|
foreach (var systemType in _systemTypes)
|
||||||
{
|
{
|
||||||
if (!permanentMark.Contains(systemType))
|
if (!permanentMark.Contains(systemType))
|
||||||
{
|
{
|
||||||
|
|||||||
134
Ghost.Graphics/D3D12/D3D12Buffer.cs
Normal file
134
Ghost.Graphics/D3D12/D3D12Buffer.cs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Win32;
|
||||||
|
using Win32.Graphics.Direct3D12;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.D3D12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12 implementation of buffer interface
|
||||||
|
/// </summary>
|
||||||
|
internal unsafe class D3D12Buffer : IBuffer
|
||||||
|
{
|
||||||
|
private ComPtr<ID3D12Resource> _resource;
|
||||||
|
private ResourceState _currentState;
|
||||||
|
private void* _mappedPtr;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public BufferUsage Usage
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public ulong Size
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public ResourceState CurrentState => _currentState;
|
||||||
|
|
||||||
|
public ID3D12Resource* NativeResource => _resource.Get();
|
||||||
|
|
||||||
|
public D3D12Buffer(ComPtr<ID3D12Device14> device, BufferDesc desc)
|
||||||
|
{
|
||||||
|
Usage = desc.Usage;
|
||||||
|
Size = desc.Size;
|
||||||
|
_currentState = ResourceState.Common;
|
||||||
|
|
||||||
|
CreateBuffer(device, desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateBuffer(ComPtr<ID3D12Device14> device, BufferDesc desc)
|
||||||
|
{
|
||||||
|
var resourceDesc = new ResourceDescription
|
||||||
|
{
|
||||||
|
Dimension = ResourceDimension.Buffer,
|
||||||
|
Alignment = 0,
|
||||||
|
Width = desc.Size,
|
||||||
|
Height = 1,
|
||||||
|
DepthOrArraySize = 1,
|
||||||
|
MipLevels = 1,
|
||||||
|
Format = Win32.Graphics.Dxgi.Common.Format.Unknown,
|
||||||
|
SampleDesc = new Win32.Graphics.Dxgi.Common.SampleDescription(1, 0),
|
||||||
|
Layout = TextureLayout.RowMajor,
|
||||||
|
Flags = ConvertBufferUsage(desc.Usage)
|
||||||
|
};
|
||||||
|
|
||||||
|
var heapProps = new HeapProperties
|
||||||
|
{
|
||||||
|
Type = ConvertMemoryType(desc.MemoryType),
|
||||||
|
CPUPageProperty = CpuPageProperty.Unknown,
|
||||||
|
MemoryPoolPreference = MemoryPool.Unknown,
|
||||||
|
CreationNodeMask = 1,
|
||||||
|
VisibleNodeMask = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
var initialState = desc.MemoryType switch
|
||||||
|
{
|
||||||
|
MemoryType.Upload => Win32.Graphics.Direct3D12.ResourceStates.GenericRead,
|
||||||
|
MemoryType.Readback => Win32.Graphics.Direct3D12.ResourceStates.CopyDest,
|
||||||
|
_ => Win32.Graphics.Direct3D12.ResourceStates.Common
|
||||||
|
};
|
||||||
|
|
||||||
|
device.Get()->CreateCommittedResource(
|
||||||
|
&heapProps,
|
||||||
|
HeapFlags.None,
|
||||||
|
&resourceDesc,
|
||||||
|
initialState,
|
||||||
|
null,
|
||||||
|
__uuidof<ID3D12Resource>(),
|
||||||
|
_resource.GetVoidAddressOf());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void* Map()
|
||||||
|
{
|
||||||
|
if (_mappedPtr != null)
|
||||||
|
return _mappedPtr;
|
||||||
|
|
||||||
|
var range = new Win32.Graphics.Direct3D12.Range { Begin = 0, End = 0 };
|
||||||
|
|
||||||
|
fixed (void** ptr = &_mappedPtr)
|
||||||
|
{
|
||||||
|
_resource.Get()->Map(0, &range, ptr);
|
||||||
|
}
|
||||||
|
return _mappedPtr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Unmap()
|
||||||
|
{
|
||||||
|
if (_mappedPtr != null)
|
||||||
|
{
|
||||||
|
_resource.Get()->Unmap(0, null);
|
||||||
|
_mappedPtr = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HeapType ConvertMemoryType(MemoryType memoryType)
|
||||||
|
{
|
||||||
|
return memoryType switch
|
||||||
|
{
|
||||||
|
MemoryType.Default => HeapType.Default,
|
||||||
|
MemoryType.Upload => HeapType.Upload,
|
||||||
|
MemoryType.Readback => HeapType.Readback,
|
||||||
|
_ => throw new ArgumentException($"Unknown memory type: {memoryType}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResourceFlags ConvertBufferUsage(BufferUsage usage)
|
||||||
|
{
|
||||||
|
var flags = ResourceFlags.None;
|
||||||
|
|
||||||
|
if ((usage & BufferUsage.Raw) != 0)
|
||||||
|
flags |= ResourceFlags.AllowUnorderedAccess;
|
||||||
|
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Unmap();
|
||||||
|
_resource.Dispose();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
162
Ghost.Graphics/D3D12/D3D12CommandBuffer.cs
Normal file
162
Ghost.Graphics/D3D12/D3D12CommandBuffer.cs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
using Ghost.Graphics.Data;
|
||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Win32;
|
||||||
|
using Win32.Graphics.Direct3D12;
|
||||||
|
using Win32.Numerics;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.D3D12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12 implementation of command buffer interface
|
||||||
|
/// </summary>
|
||||||
|
internal unsafe class D3D12CommandBuffer : ICommandBuffer
|
||||||
|
{
|
||||||
|
private ComPtr<ID3D12CommandAllocator> _allocator;
|
||||||
|
private ComPtr<ID3D12GraphicsCommandList10> _commandList;
|
||||||
|
private readonly CommandBufferType _type;
|
||||||
|
private bool _isRecording;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public ID3D12GraphicsCommandList10* NativeCommandList => _commandList.Get();
|
||||||
|
|
||||||
|
public D3D12CommandBuffer(ComPtr<ID3D12Device14> device, CommandBufferType type)
|
||||||
|
{
|
||||||
|
_type = type;
|
||||||
|
var commandListType = ConvertCommandBufferType(type);
|
||||||
|
|
||||||
|
device.Get()->CreateCommandAllocator(commandListType, __uuidof<ID3D12CommandAllocator>(), _allocator.GetVoidAddressOf());
|
||||||
|
device.Get()->CreateCommandList(0u, commandListType, _allocator.Get(), null, __uuidof<ID3D12GraphicsCommandList10>(), _commandList.GetVoidAddressOf());
|
||||||
|
|
||||||
|
// Command lists are created in recording state, so close it
|
||||||
|
_commandList.Get()->Close();
|
||||||
|
_isRecording = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Begin()
|
||||||
|
{
|
||||||
|
if (_isRecording)
|
||||||
|
throw new InvalidOperationException("Command buffer is already recording");
|
||||||
|
|
||||||
|
_allocator.Get()->Reset();
|
||||||
|
_commandList.Get()->Reset(_allocator.Get(), null);
|
||||||
|
_isRecording = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void End()
|
||||||
|
{
|
||||||
|
if (!_isRecording)
|
||||||
|
throw new InvalidOperationException("Command buffer is not recording");
|
||||||
|
|
||||||
|
_commandList.Get()->Close();
|
||||||
|
_isRecording = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BeginRenderPass(IRenderTarget renderTarget, Color128 clearColor)
|
||||||
|
{
|
||||||
|
// TODO: Implement render pass begin
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndRenderPass()
|
||||||
|
{
|
||||||
|
// TODO: Implement render pass end
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetViewport(ViewportDesc viewport)
|
||||||
|
{
|
||||||
|
var d3d12Viewport = new Viewport(viewport.Width, viewport.Height, viewport.X, viewport.Y, viewport.MinDepth, viewport.MaxDepth);
|
||||||
|
_commandList.Get()->RSSetViewports(1, &d3d12Viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetScissorRect(RectDesc rect)
|
||||||
|
{
|
||||||
|
var d3d12Rect = new Rect(rect.Left, rect.Top, rect.Right, rect.Bottom);
|
||||||
|
_commandList.Get()->RSSetScissorRects(1, &d3d12Rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResourceBarrier(IResource resource, ResourceState before, ResourceState after)
|
||||||
|
{
|
||||||
|
if (resource is D3D12Texture d3d12Texture)
|
||||||
|
{
|
||||||
|
_commandList.Get()->ResourceBarrierTransition(d3d12Texture.NativeResource,
|
||||||
|
ConvertResourceState(before), ConvertResourceState(after));
|
||||||
|
}
|
||||||
|
else if (resource is D3D12Buffer d3d12Buffer)
|
||||||
|
{
|
||||||
|
_commandList.Get()->ResourceBarrierTransition(d3d12Buffer.NativeResource,
|
||||||
|
ConvertResourceState(before), ConvertResourceState(after));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Resource must be a D3D12 resource", nameof(resource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetGraphicsRootSignature(IRootSignature rootSignature)
|
||||||
|
{
|
||||||
|
// TODO: Implement root signature setting
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetPipelineState(IPipelineState pipelineState)
|
||||||
|
{
|
||||||
|
// TODO: Implement pipeline state setting
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDescriptorHeaps(IDescriptorHeap[] heaps)
|
||||||
|
{
|
||||||
|
// TODO: Implement descriptor heap setting
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawIndexedInstanced(uint indexCount, uint instanceCount = 1, uint startIndex = 0, int baseVertex = 0, uint startInstance = 0)
|
||||||
|
{
|
||||||
|
_commandList.Get()->DrawIndexedInstanced(indexCount, instanceCount, startIndex, baseVertex, startInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispatch(uint threadGroupCountX, uint threadGroupCountY = 1, uint threadGroupCountZ = 1)
|
||||||
|
{
|
||||||
|
_commandList.Get()->Dispatch(threadGroupCountX, threadGroupCountY, threadGroupCountZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CommandListType ConvertCommandBufferType(CommandBufferType type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
CommandBufferType.Graphics => CommandListType.Direct,
|
||||||
|
CommandBufferType.Compute => CommandListType.Compute,
|
||||||
|
CommandBufferType.Copy => CommandListType.Copy,
|
||||||
|
_ => throw new ArgumentException($"Unknown command buffer type: {type}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Win32.Graphics.Direct3D12.ResourceStates ConvertResourceState(ResourceState state)
|
||||||
|
{
|
||||||
|
return state switch
|
||||||
|
{
|
||||||
|
ResourceState.Common or ResourceState.Present => Win32.Graphics.Direct3D12.ResourceStates.Common,
|
||||||
|
ResourceState.VertexAndConstantBuffer => Win32.Graphics.Direct3D12.ResourceStates.VertexAndConstantBuffer,
|
||||||
|
ResourceState.IndexBuffer => Win32.Graphics.Direct3D12.ResourceStates.IndexBuffer,
|
||||||
|
ResourceState.RenderTarget => Win32.Graphics.Direct3D12.ResourceStates.RenderTarget,
|
||||||
|
ResourceState.UnorderedAccess => Win32.Graphics.Direct3D12.ResourceStates.UnorderedAccess,
|
||||||
|
ResourceState.DepthWrite => Win32.Graphics.Direct3D12.ResourceStates.DepthWrite,
|
||||||
|
ResourceState.DepthRead => Win32.Graphics.Direct3D12.ResourceStates.DepthRead,
|
||||||
|
ResourceState.PixelShaderResource => Win32.Graphics.Direct3D12.ResourceStates.PixelShaderResource,
|
||||||
|
ResourceState.CopyDest => Win32.Graphics.Direct3D12.ResourceStates.CopyDest,
|
||||||
|
ResourceState.CopySource => Win32.Graphics.Direct3D12.ResourceStates.CopySource,
|
||||||
|
_ => throw new ArgumentException($"Unknown resource state: {state}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
_commandList.Dispose();
|
||||||
|
_allocator.Dispose();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
120
Ghost.Graphics/D3D12/D3D12CommandQueue.cs
Normal file
120
Ghost.Graphics/D3D12/D3D12CommandQueue.cs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Win32;
|
||||||
|
using Win32.Graphics.Direct3D12;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.D3D12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12 implementation of command queue interface
|
||||||
|
/// </summary>
|
||||||
|
internal unsafe class D3D12CommandQueue : ICommandQueue
|
||||||
|
{
|
||||||
|
private ComPtr<ID3D12CommandQueue> _queue;
|
||||||
|
private ComPtr<ID3D12Fence1> _fence;
|
||||||
|
private readonly AutoResetEvent _fenceEvent;
|
||||||
|
private ulong _fenceValue;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public CommandQueueType Type { get; }
|
||||||
|
public ID3D12CommandQueue* NativeQueue => _queue.Get();
|
||||||
|
|
||||||
|
public D3D12CommandQueue(ComPtr<ID3D12Device14> device, CommandQueueType type)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
_fenceEvent = new AutoResetEvent(false);
|
||||||
|
_fenceValue = 0;
|
||||||
|
|
||||||
|
var queueDesc = new CommandQueueDescription
|
||||||
|
{
|
||||||
|
Type = ConvertCommandQueueType(type),
|
||||||
|
Priority = (int)CommandQueuePriority.Normal,
|
||||||
|
Flags = CommandQueueFlags.None,
|
||||||
|
};
|
||||||
|
|
||||||
|
fixed (void* queuePtr = &_queue)
|
||||||
|
{
|
||||||
|
device.Get()->CreateCommandQueue(&queueDesc, __uuidof<ID3D12CommandQueue>(), (void**)queuePtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
device.Get()->CreateFence(0, FenceFlags.None, __uuidof<ID3D12Fence1>(), _fence.GetVoidAddressOf());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Submit(ICommandBuffer commandBuffer)
|
||||||
|
{
|
||||||
|
if (commandBuffer is D3D12CommandBuffer d3d12CommandBuffer)
|
||||||
|
{
|
||||||
|
var commandList = d3d12CommandBuffer.NativeCommandList;
|
||||||
|
var commandListPtr = (ID3D12CommandList*)commandList;
|
||||||
|
_queue.Get()->ExecuteCommandLists(1, &commandListPtr);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Command buffer must be a D3D12CommandBuffer", nameof(commandBuffer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Submit(ICommandBuffer[] commandBuffers)
|
||||||
|
{
|
||||||
|
var commandLists = stackalloc ID3D12CommandList*[commandBuffers.Length];
|
||||||
|
|
||||||
|
for (int i = 0; i < commandBuffers.Length; i++)
|
||||||
|
{
|
||||||
|
if (commandBuffers[i] is D3D12CommandBuffer d3d12CommandBuffer)
|
||||||
|
{
|
||||||
|
commandLists[i] = (ID3D12CommandList*)d3d12CommandBuffer.NativeCommandList;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Command buffer at index {i} must be a D3D12CommandBuffer", nameof(commandBuffers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_queue.Get()->ExecuteCommandLists((uint)commandBuffers.Length, commandLists);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong Signal(ulong value)
|
||||||
|
{
|
||||||
|
_fenceValue = value;
|
||||||
|
_queue.Get()->Signal((ID3D12Fence*)_fence.Get(), _fenceValue);
|
||||||
|
return _fenceValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WaitForValue(ulong value)
|
||||||
|
{
|
||||||
|
if (_fence.Get()->GetCompletedValue() < value)
|
||||||
|
{
|
||||||
|
var handle = new Handle((void*)_fenceEvent.SafeWaitHandle.DangerousGetHandle());
|
||||||
|
if (_fence.Get()->SetEventOnCompletion(value, handle).Success)
|
||||||
|
{
|
||||||
|
_fenceEvent.WaitOne();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong GetCompletedValue()
|
||||||
|
{
|
||||||
|
return _fence.Get()->GetCompletedValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CommandListType ConvertCommandQueueType(CommandQueueType type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
CommandQueueType.Graphics => CommandListType.Direct,
|
||||||
|
CommandQueueType.Compute => CommandListType.Compute,
|
||||||
|
CommandQueueType.Copy => CommandListType.Copy,
|
||||||
|
_ => throw new ArgumentException($"Unknown command queue type: {type}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
_fenceEvent?.Dispose();
|
||||||
|
_fence.Dispose();
|
||||||
|
_queue.Dispose();
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
88
Ghost.Graphics/D3D12/D3D12DescriptorAllocator.cs
Normal file
88
Ghost.Graphics/D3D12/D3D12DescriptorAllocator.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Win32;
|
||||||
|
using Win32.Graphics.Direct3D12;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.D3D12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12 implementation of descriptor allocator interface
|
||||||
|
/// </summary>
|
||||||
|
internal unsafe class D3D12DescriptorAllocator : IDescriptorAllocator
|
||||||
|
{
|
||||||
|
private readonly DescriptorAllocator _internalAllocator;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public D3D12DescriptorAllocator(ComPtr<ID3D12Device14> device)
|
||||||
|
{
|
||||||
|
_internalAllocator = new DescriptorAllocator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DescriptorHandle AllocateRTV()
|
||||||
|
{
|
||||||
|
var rtvDescriptor = _internalAllocator.AllocateRTV();
|
||||||
|
return new DescriptorHandle(rtvDescriptor.Index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DescriptorHandle[] AllocateRTVs(uint count)
|
||||||
|
{
|
||||||
|
var rtvDescriptors = _internalAllocator.AllocateRTVs(count);
|
||||||
|
return rtvDescriptors.Select(desc => new DescriptorHandle(desc.Index)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DescriptorHandle AllocateDSV()
|
||||||
|
{
|
||||||
|
var dsvDescriptor = _internalAllocator.AllocateDSV();
|
||||||
|
return new DescriptorHandle(dsvDescriptor.Index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DescriptorHandle AllocateSRV()
|
||||||
|
{
|
||||||
|
var srvDescriptor = _internalAllocator.AllocateSRV();
|
||||||
|
return new DescriptorHandle(srvDescriptor.Index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DescriptorHandle AllocateSampler()
|
||||||
|
{
|
||||||
|
var samplerDescriptor = _internalAllocator.AllocateSampler();
|
||||||
|
return new DescriptorHandle(samplerDescriptor.Index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DescriptorHandle AllocateBindless()
|
||||||
|
{
|
||||||
|
var bindlessDescriptor = _internalAllocator.AllocateBindless();
|
||||||
|
return new DescriptorHandle(bindlessDescriptor.Index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReleaseRTV(DescriptorHandle handle)
|
||||||
|
{
|
||||||
|
// TODO: Convert back to internal descriptor and release
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReleaseDSV(DescriptorHandle handle)
|
||||||
|
{
|
||||||
|
// TODO: Convert back to internal descriptor and release
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReleaseSRV(DescriptorHandle handle)
|
||||||
|
{
|
||||||
|
// TODO: Convert back to internal descriptor and release
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReleaseSampler(DescriptorHandle handle)
|
||||||
|
{
|
||||||
|
// TODO: Convert back to internal descriptor and release
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReleaseBindless(DescriptorHandle handle)
|
||||||
|
{
|
||||||
|
// TODO: Convert back to internal descriptor and release
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
_internalAllocator?.Dispose();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
123
Ghost.Graphics/D3D12/D3D12RenderDevice.cs
Normal file
123
Ghost.Graphics/D3D12/D3D12RenderDevice.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
using Ghost.Core;
|
||||||
|
using Ghost.Graphics.Data;
|
||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Win32;
|
||||||
|
using Win32.Graphics.Direct3D;
|
||||||
|
using Win32.Graphics.Direct3D12;
|
||||||
|
using Win32.Graphics.Dxgi;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.D3D12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12 implementation of the render device interface
|
||||||
|
/// </summary>
|
||||||
|
internal unsafe class D3D12RenderDevice : IRenderDevice
|
||||||
|
{
|
||||||
|
private ComPtr<IDXGIFactory7> _dxgiFactory;
|
||||||
|
private ComPtr<ID3D12Device14> _device;
|
||||||
|
private ComPtr<IDXGIAdapter1> _adapter;
|
||||||
|
|
||||||
|
private D3D12CommandQueue _graphicsQueue;
|
||||||
|
private D3D12CommandQueue _computeQueue;
|
||||||
|
private D3D12CommandQueue _copyQueue;
|
||||||
|
private D3D12DescriptorAllocator _descriptorAllocator;
|
||||||
|
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public ICommandQueue GraphicsQueue => _graphicsQueue;
|
||||||
|
public ICommandQueue ComputeQueue => _computeQueue;
|
||||||
|
public ICommandQueue CopyQueue => _copyQueue;
|
||||||
|
public IDescriptorAllocator DescriptorAllocator => _descriptorAllocator;
|
||||||
|
|
||||||
|
public ConstPtr<ID3D12Device14> NativeDevice => new(_device.Get());
|
||||||
|
public ConstPtr<IDXGIFactory7> DXGIFactory => new(_dxgiFactory.Get());
|
||||||
|
public ConstPtr<IDXGIAdapter1> Adapter => new(_adapter.Get());
|
||||||
|
|
||||||
|
public D3D12RenderDevice()
|
||||||
|
{
|
||||||
|
InitializeDevice();
|
||||||
|
|
||||||
|
_graphicsQueue = new D3D12CommandQueue(_device, CommandQueueType.Graphics);
|
||||||
|
_computeQueue = new D3D12CommandQueue(_device, CommandQueueType.Compute);
|
||||||
|
_copyQueue = new D3D12CommandQueue(_device, CommandQueueType.Copy);
|
||||||
|
|
||||||
|
_descriptorAllocator = new D3D12DescriptorAllocator(_device);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeDevice()
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
CreateDXGIFactory2(true, __uuidof<IDXGIFactory7>(), _dxgiFactory.GetVoidAddressOf());
|
||||||
|
#else
|
||||||
|
CreateDXGIFactory2(false, __uuidof<IDXGIFactory7>(), _dxgiFactory.GetVoidAddressOf());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using ComPtr<IDXGIAdapter1> adapter = default;
|
||||||
|
|
||||||
|
for (uint adapterIndex = 0;
|
||||||
|
_dxgiFactory.Get()->EnumAdapterByGpuPreference(adapterIndex, GpuPreference.HighPerformance, __uuidof<IDXGIAdapter1>(), adapter.ReleaseAndGetVoidAddressOf()).Success;
|
||||||
|
adapterIndex++)
|
||||||
|
{
|
||||||
|
AdapterDescription1 desc = default;
|
||||||
|
adapter.Get()->GetDesc1(&desc);
|
||||||
|
|
||||||
|
// Don't select the Basic Render Driver adapter.
|
||||||
|
if ((desc.Flags & AdapterFlags.Software) != AdapterFlags.None)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (D3D12CreateDevice((IUnknown*)adapter.Get(), FeatureLevel.Level_12_0, __uuidof<ID3D12Device14>(), _device.GetVoidAddressOf()).Success)
|
||||||
|
{
|
||||||
|
_adapter = adapter.Move();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_device.Get() == null)
|
||||||
|
{
|
||||||
|
throw new PlatformNotSupportedException("Cannot create ID3D12Device with feature level 12.0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommandBuffer CreateCommandBuffer(CommandBufferType type = CommandBufferType.Graphics)
|
||||||
|
{
|
||||||
|
return new D3D12CommandBuffer(_device, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISwapChain CreateSwapChain(SwapChainDesc desc)
|
||||||
|
{
|
||||||
|
return new D3D12SwapChain(_dxgiFactory, _graphicsQueue.NativeQueue, desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IRenderTarget CreateRenderTarget(RenderTargetDesc desc)
|
||||||
|
{
|
||||||
|
return new D3D12RenderTarget(_device, _descriptorAllocator, desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITexture CreateTexture(TextureDesc desc)
|
||||||
|
{
|
||||||
|
return new D3D12Texture(_device, desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IBuffer CreateBuffer(BufferDesc desc)
|
||||||
|
{
|
||||||
|
return new D3D12Buffer(_device, desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
_descriptorAllocator?.Dispose();
|
||||||
|
_graphicsQueue?.Dispose();
|
||||||
|
_computeQueue?.Dispose();
|
||||||
|
_copyQueue?.Dispose();
|
||||||
|
|
||||||
|
_device.Reset();
|
||||||
|
_dxgiFactory.Dispose();
|
||||||
|
_adapter.Dispose();
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Ghost.Graphics/D3D12/D3D12RenderTarget.cs
Normal file
40
Ghost.Graphics/D3D12/D3D12RenderTarget.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Win32;
|
||||||
|
using Win32.Graphics.Direct3D12;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.D3D12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12 implementation of render target interface
|
||||||
|
/// Supports either color OR depth rendering, not both
|
||||||
|
/// </summary>
|
||||||
|
internal unsafe class D3D12RenderTarget : IRenderTarget
|
||||||
|
{
|
||||||
|
private readonly D3D12Texture _target;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public uint Width { get; }
|
||||||
|
public uint Height { get; }
|
||||||
|
public RenderTargetType Type { get; }
|
||||||
|
public ITexture Target => _target;
|
||||||
|
|
||||||
|
public D3D12RenderTarget(ComPtr<ID3D12Device14> device, D3D12DescriptorAllocator descriptorAllocator, RenderTargetDesc desc)
|
||||||
|
{
|
||||||
|
Width = desc.Width;
|
||||||
|
Height = desc.Height;
|
||||||
|
Type = desc.Type;
|
||||||
|
|
||||||
|
// Create the target texture based on type
|
||||||
|
var usage = Type == RenderTargetType.Color ? TextureUsage.RenderTarget : TextureUsage.DepthStencil;
|
||||||
|
var textureDesc = new TextureDesc(desc.Width, desc.Height, desc.Format, 1, usage);
|
||||||
|
_target = new D3D12Texture(device, textureDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
_target?.Dispose();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
222
Ghost.Graphics/D3D12/D3D12Renderer.cs
Normal file
222
Ghost.Graphics/D3D12/D3D12Renderer.cs
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Ghost.Graphics.Contracts;
|
||||||
|
using Ghost.Graphics.Data;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.D3D12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12 implementation of the renderer interface using RHI abstractions
|
||||||
|
/// </summary>
|
||||||
|
public unsafe class D3D12Renderer : IRenderer
|
||||||
|
{
|
||||||
|
private struct FrameResource : IDisposable
|
||||||
|
{
|
||||||
|
public ICommandBuffer CommandBuffer;
|
||||||
|
public ulong FenceValue;
|
||||||
|
|
||||||
|
public FrameResource(IRenderDevice device)
|
||||||
|
{
|
||||||
|
CommandBuffer = device.CreateCommandBuffer();
|
||||||
|
FenceValue = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
CommandBuffer?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IRenderDevice _device;
|
||||||
|
private readonly ICommandQueue _commandQueue;
|
||||||
|
private readonly FrameResource[] _frameResources;
|
||||||
|
private uint _frameIndex;
|
||||||
|
|
||||||
|
private IRenderTarget? _renderTarget;
|
||||||
|
private ISwapChain? _swapChain;
|
||||||
|
|
||||||
|
private readonly Lock _lock = new();
|
||||||
|
private uint _pendingWidth;
|
||||||
|
private uint _pendingHeight;
|
||||||
|
private bool _resizeRequested;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
// TODO: Add render passes support
|
||||||
|
// private ImmutableArray<IRenderPass> _renderPasses;
|
||||||
|
|
||||||
|
public D3D12Renderer(IRenderDevice device)
|
||||||
|
{
|
||||||
|
_device = device;
|
||||||
|
_commandQueue = device.GraphicsQueue;
|
||||||
|
|
||||||
|
// Create frame resources for double buffering
|
||||||
|
_frameResources = new FrameResource[2];
|
||||||
|
for (int i = 0; i < _frameResources.Length; i++)
|
||||||
|
{
|
||||||
|
_frameResources[i] = new FrameResource(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRenderTarget(IRenderTarget? renderTarget)
|
||||||
|
{
|
||||||
|
_renderTarget = renderTarget;
|
||||||
|
_swapChain = null; // Clear swap chain when using render target
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSwapChain(ISwapChain? swapChain)
|
||||||
|
{
|
||||||
|
_swapChain = swapChain;
|
||||||
|
_renderTarget = null; // Clear render target when using swap chain
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestResize(uint width, uint height)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_pendingWidth == width && _pendingHeight == height)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_resizeRequested = true;
|
||||||
|
_pendingWidth = width;
|
||||||
|
_pendingHeight = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExecutePendingResize()
|
||||||
|
{
|
||||||
|
if (!_resizeRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
uint newWidth, newHeight;
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
newWidth = _pendingWidth;
|
||||||
|
newHeight = _pendingHeight;
|
||||||
|
_resizeRequested = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for GPU to complete
|
||||||
|
WaitIdle();
|
||||||
|
|
||||||
|
// Resize swap chain if present
|
||||||
|
_swapChain?.Resize(newWidth, newHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Render()
|
||||||
|
{
|
||||||
|
ExecutePendingResize();
|
||||||
|
|
||||||
|
// Get current frame resource
|
||||||
|
var frameIndex = _frameIndex % (uint)_frameResources.Length;
|
||||||
|
ref var frame = ref _frameResources[frameIndex];
|
||||||
|
|
||||||
|
// Wait for this frame resource to be available
|
||||||
|
if (frame.FenceValue > 0)
|
||||||
|
{
|
||||||
|
_commandQueue.WaitForValue(frame.FenceValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin command recording
|
||||||
|
frame.CommandBuffer.Begin();
|
||||||
|
|
||||||
|
if (_renderTarget != null)
|
||||||
|
{
|
||||||
|
RenderToTarget(_renderTarget, frame.CommandBuffer);
|
||||||
|
}
|
||||||
|
else if (_swapChain != null)
|
||||||
|
{
|
||||||
|
RenderToSwapChain(_swapChain, frame.CommandBuffer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No render target - skip rendering
|
||||||
|
frame.CommandBuffer.End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// End command recording
|
||||||
|
frame.CommandBuffer.End();
|
||||||
|
|
||||||
|
// Submit commands
|
||||||
|
_commandQueue.Submit(frame.CommandBuffer);
|
||||||
|
|
||||||
|
// Present if using swap chain
|
||||||
|
_swapChain?.Present();
|
||||||
|
|
||||||
|
// Signal fence for this frame
|
||||||
|
frame.FenceValue = _commandQueue.Signal(++_frameIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderToTarget(IRenderTarget target, ICommandBuffer cmd)
|
||||||
|
{
|
||||||
|
var clearColor = new Color128 { r = 1.0f, g = 0.0f, b = 1.0f, a = 1.0f };
|
||||||
|
|
||||||
|
cmd.BeginRenderPass(target, clearColor);
|
||||||
|
|
||||||
|
var viewport = new ViewportDesc(target.Width, target.Height);
|
||||||
|
var scissor = new RectDesc(0, 0, (int)target.Width, (int)target.Height);
|
||||||
|
|
||||||
|
cmd.SetViewport(viewport);
|
||||||
|
cmd.SetScissorRect(scissor);
|
||||||
|
|
||||||
|
// TODO: Execute render passes
|
||||||
|
// foreach (var pass in _renderPasses)
|
||||||
|
// {
|
||||||
|
// pass.Execute(cmd);
|
||||||
|
// }
|
||||||
|
|
||||||
|
cmd.EndRenderPass();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderToSwapChain(ISwapChain swapChain, ICommandBuffer cmd)
|
||||||
|
{
|
||||||
|
var backBuffer = swapChain.GetCurrentBackBuffer();
|
||||||
|
|
||||||
|
// Transition back buffer to render target
|
||||||
|
cmd.ResourceBarrier(backBuffer, ResourceState.Present, ResourceState.RenderTarget);
|
||||||
|
|
||||||
|
// Create temporary render target for back buffer
|
||||||
|
// TODO: This should be cached/reused
|
||||||
|
var renderTarget = CreateBackBufferRenderTarget(backBuffer);
|
||||||
|
|
||||||
|
RenderToTarget(renderTarget, cmd);
|
||||||
|
|
||||||
|
// Transition back buffer to present
|
||||||
|
cmd.ResourceBarrier(backBuffer, ResourceState.RenderTarget, ResourceState.Present);
|
||||||
|
|
||||||
|
renderTarget.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IRenderTarget CreateBackBufferRenderTarget(ITexture backBuffer)
|
||||||
|
{
|
||||||
|
// TODO: Create render target from back buffer texture
|
||||||
|
// This is a simplified implementation
|
||||||
|
var desc = RenderTargetDesc.Color(backBuffer.Width, backBuffer.Height, backBuffer.Format);
|
||||||
|
return _device.CreateRenderTarget(desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WaitIdle()
|
||||||
|
{
|
||||||
|
// Wait for all frame resources to complete
|
||||||
|
foreach (ref var frame in _frameResources.AsSpan())
|
||||||
|
{
|
||||||
|
if (frame.FenceValue > 0)
|
||||||
|
{
|
||||||
|
_commandQueue.WaitForValue(frame.FenceValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
WaitIdle();
|
||||||
|
|
||||||
|
foreach (ref var frame in _frameResources.AsSpan())
|
||||||
|
{
|
||||||
|
frame.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
174
Ghost.Graphics/D3D12/D3D12SwapChain.cs
Normal file
174
Ghost.Graphics/D3D12/D3D12SwapChain.cs
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
using Ghost.Graphics.Contracts;
|
||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Ghost.Graphics.D3D12.Utilities;
|
||||||
|
using Win32;
|
||||||
|
using Win32.Graphics.Direct3D12;
|
||||||
|
using Win32.Graphics.Dxgi;
|
||||||
|
using Win32.Graphics.Dxgi.Common;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.D3D12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12 implementation of swap chain interface
|
||||||
|
/// </summary>
|
||||||
|
internal unsafe class D3D12SwapChain : ISwapChain
|
||||||
|
{
|
||||||
|
private ComPtr<IDXGISwapChain4> _swapChain;
|
||||||
|
private readonly D3D12Texture[] _backBuffers;
|
||||||
|
private uint _currentBackBufferIndex;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public uint Width { get; private set; }
|
||||||
|
public uint Height { get; private set; }
|
||||||
|
public uint BufferCount { get; }
|
||||||
|
|
||||||
|
public D3D12SwapChain(ComPtr<IDXGIFactory7> factory, ID3D12CommandQueue* commandQueue, SwapChainDesc desc)
|
||||||
|
{
|
||||||
|
_backBuffers = new D3D12Texture[desc.BufferCount];
|
||||||
|
|
||||||
|
Width = desc.Width;
|
||||||
|
Height = desc.Height;
|
||||||
|
BufferCount = desc.BufferCount;
|
||||||
|
|
||||||
|
CreateSwapChain(factory, commandQueue, desc);
|
||||||
|
CreateBackBuffers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateSwapChain(ComPtr<IDXGIFactory7> factory, ID3D12CommandQueue* commandQueue, SwapChainDesc desc)
|
||||||
|
{
|
||||||
|
var swapChainDesc = new SwapChainDescription1
|
||||||
|
{
|
||||||
|
Width = desc.Width,
|
||||||
|
Height = desc.Height,
|
||||||
|
Format = ConvertTextureFormat(desc.Format),
|
||||||
|
SampleDesc = new SampleDescription(1, 0),
|
||||||
|
BufferUsage = Usage.BackBuffer | Usage.RenderTargetOutput,
|
||||||
|
BufferCount = desc.BufferCount,
|
||||||
|
Scaling = Scaling.Stretch,
|
||||||
|
SwapEffect = SwapEffect.FlipDiscard,
|
||||||
|
AlphaMode = AlphaMode.Ignore,
|
||||||
|
Flags = SwapChainFlags.AllowTearing,
|
||||||
|
Stereo = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
using ComPtr<IDXGISwapChain1> tempSwapChain = default;
|
||||||
|
|
||||||
|
switch (desc.Target.Type)
|
||||||
|
{
|
||||||
|
case SwapChainTargetType.Composition:
|
||||||
|
factory.Get()->CreateSwapChainForComposition((IUnknown*)commandQueue, &swapChainDesc, null, tempSwapChain.GetAddressOf());
|
||||||
|
|
||||||
|
// Set the composition surface
|
||||||
|
if (desc.Target.CompositionSurface != null)
|
||||||
|
{
|
||||||
|
var swapChainPanelNative = ISwapChainPanelNative.FromSwapChainPanel(desc.Target.CompositionSurface);
|
||||||
|
swapChainPanelNative.SetSwapChain((IntPtr)tempSwapChain.Get());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SwapChainTargetType.WindowHandle:
|
||||||
|
var swapChainFullscreenDesc = new SwapChainFullscreenDescription
|
||||||
|
{
|
||||||
|
Windowed = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
factory.Get()->CreateSwapChainForHwnd(
|
||||||
|
(IUnknown*)commandQueue,
|
||||||
|
desc.Target.WindowHandle,
|
||||||
|
&swapChainDesc,
|
||||||
|
&swapChainFullscreenDesc,
|
||||||
|
null,
|
||||||
|
tempSwapChain.GetAddressOf());
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentException("Unsupported swap chain target type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tempSwapChain.Get()->QueryInterface(__uuidof<IDXGISwapChain4>(), _swapChain.GetVoidAddressOf()).Failure)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to create IDXGISwapChain4 interface.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentBackBufferIndex = _swapChain.Get()->GetCurrentBackBufferIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateBackBuffers()
|
||||||
|
{
|
||||||
|
for (uint i = 0; i < BufferCount; i++)
|
||||||
|
{
|
||||||
|
ComPtr<ID3D12Resource> backBuffer = default;
|
||||||
|
_swapChain.Get()->GetBuffer(i, __uuidof<ID3D12Resource>(), backBuffer.GetVoidAddressOf());
|
||||||
|
backBuffer.Get()->SetName($"SwapChain_BackBuffer_{i}");
|
||||||
|
|
||||||
|
_backBuffers[i] = new D3D12Texture(backBuffer.Move(), Width, Height, TextureFormat.B8G8R8A8_UNorm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITexture GetCurrentBackBuffer()
|
||||||
|
{
|
||||||
|
_currentBackBufferIndex = _swapChain.Get()->GetCurrentBackBufferIndex();
|
||||||
|
return _backBuffers[_currentBackBufferIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Present(bool vsync = true)
|
||||||
|
{
|
||||||
|
var presentFlags = PresentFlags.None;
|
||||||
|
var syncInterval = vsync ? 1u : 0u;
|
||||||
|
|
||||||
|
if (_swapChain.Get()->Present(syncInterval, presentFlags).Failure)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to present swap chain.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Resize(uint width, uint height)
|
||||||
|
{
|
||||||
|
if (Width == width && Height == height)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Release old back buffers
|
||||||
|
for (int i = 0; i < _backBuffers.Length; i++)
|
||||||
|
{
|
||||||
|
_backBuffers[i]?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize the swap chain
|
||||||
|
if (_swapChain.Get()->ResizeBuffers(BufferCount, width, height, Format.B8G8R8A8Unorm, SwapChainFlags.AllowTearing).Failure)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to resize swap chain buffers.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
|
||||||
|
// Recreate back buffers
|
||||||
|
CreateBackBuffers();
|
||||||
|
_currentBackBufferIndex = _swapChain.Get()->GetCurrentBackBufferIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Format ConvertTextureFormat(TextureFormat format)
|
||||||
|
{
|
||||||
|
return format switch
|
||||||
|
{
|
||||||
|
TextureFormat.R8G8B8A8_UNorm => Format.R8G8B8A8Unorm,
|
||||||
|
TextureFormat.B8G8R8A8_UNorm => Format.B8G8R8A8Unorm,
|
||||||
|
TextureFormat.R16G16B16A16_Float => Format.R16G16B16A16Float,
|
||||||
|
TextureFormat.R32G32B32A32_Float => Format.R32G32B32A32Float,
|
||||||
|
_ => throw new ArgumentException($"Unsupported texture format: {format}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
for (int i = 0; i < _backBuffers.Length; i++)
|
||||||
|
{
|
||||||
|
_backBuffers[i]?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_swapChain.Dispose();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
135
Ghost.Graphics/D3D12/D3D12Texture.cs
Normal file
135
Ghost.Graphics/D3D12/D3D12Texture.cs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Win32;
|
||||||
|
using Win32.Graphics.Direct3D12;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.D3D12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12 implementation of texture interface
|
||||||
|
/// </summary>
|
||||||
|
internal unsafe class D3D12Texture : ITexture
|
||||||
|
{
|
||||||
|
private ComPtr<ID3D12Resource> _resource;
|
||||||
|
private ResourceState _currentState;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public uint Width { get; }
|
||||||
|
public uint Height { get; }
|
||||||
|
public TextureFormat Format { get; }
|
||||||
|
public uint MipLevels { get; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public ulong Size { get; }
|
||||||
|
public ResourceState CurrentState => _currentState;
|
||||||
|
|
||||||
|
public ID3D12Resource* NativeResource => _resource.Get();
|
||||||
|
|
||||||
|
public D3D12Texture(ComPtr<ID3D12Resource> resource, uint width, uint height, TextureFormat format, uint mipLevels = 1)
|
||||||
|
{
|
||||||
|
_resource = resource.Move();
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
Format = format;
|
||||||
|
MipLevels = mipLevels;
|
||||||
|
_currentState = ResourceState.Common;
|
||||||
|
|
||||||
|
var desc = _resource.Get()->GetDesc();
|
||||||
|
Size = (ulong)(desc.Width * desc.Height * GetBytesPerPixel(format));
|
||||||
|
}
|
||||||
|
|
||||||
|
public D3D12Texture(ComPtr<ID3D12Device14> device, TextureDesc desc)
|
||||||
|
{
|
||||||
|
Width = desc.Width;
|
||||||
|
Height = desc.Height;
|
||||||
|
Format = desc.Format;
|
||||||
|
MipLevels = desc.MipLevels;
|
||||||
|
_currentState = ResourceState.Common;
|
||||||
|
|
||||||
|
CreateTexture(device, desc);
|
||||||
|
Size = (ulong)(Width * Height * GetBytesPerPixel(Format));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateTexture(ComPtr<ID3D12Device14> device, TextureDesc desc)
|
||||||
|
{
|
||||||
|
var resourceDesc = new ResourceDescription
|
||||||
|
{
|
||||||
|
Dimension = ResourceDimension.Texture2D,
|
||||||
|
Alignment = 0,
|
||||||
|
Width = desc.Width,
|
||||||
|
Height = desc.Height,
|
||||||
|
DepthOrArraySize = 1,
|
||||||
|
MipLevels = (ushort)desc.MipLevels,
|
||||||
|
Format = ConvertTextureFormat(desc.Format),
|
||||||
|
SampleDesc = new Win32.Graphics.Dxgi.Common.SampleDescription(1, 0),
|
||||||
|
Layout = TextureLayout.Unknown,
|
||||||
|
Flags = ConvertTextureUsage(desc.Usage)
|
||||||
|
};
|
||||||
|
|
||||||
|
var heapProps = new HeapProperties
|
||||||
|
{
|
||||||
|
Type = HeapType.Default,
|
||||||
|
CPUPageProperty = CpuPageProperty.Unknown,
|
||||||
|
MemoryPoolPreference = MemoryPool.Unknown,
|
||||||
|
CreationNodeMask = 1,
|
||||||
|
VisibleNodeMask = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
device.Get()->CreateCommittedResource(
|
||||||
|
&heapProps,
|
||||||
|
HeapFlags.None,
|
||||||
|
&resourceDesc,
|
||||||
|
Win32.Graphics.Direct3D12.ResourceStates.Common,
|
||||||
|
null,
|
||||||
|
__uuidof<ID3D12Resource>(),
|
||||||
|
_resource.GetVoidAddressOf());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Win32.Graphics.Dxgi.Common.Format ConvertTextureFormat(TextureFormat format)
|
||||||
|
{
|
||||||
|
return format switch
|
||||||
|
{
|
||||||
|
TextureFormat.R8G8B8A8_UNorm => Win32.Graphics.Dxgi.Common.Format.R8G8B8A8Unorm,
|
||||||
|
TextureFormat.B8G8R8A8_UNorm => Win32.Graphics.Dxgi.Common.Format.B8G8R8A8Unorm,
|
||||||
|
TextureFormat.R16G16B16A16_Float => Win32.Graphics.Dxgi.Common.Format.R16G16B16A16Float,
|
||||||
|
TextureFormat.R32G32B32A32_Float => Win32.Graphics.Dxgi.Common.Format.R32G32B32A32Float,
|
||||||
|
TextureFormat.D24_UNorm_S8_UInt => Win32.Graphics.Dxgi.Common.Format.D24UnormS8Uint,
|
||||||
|
TextureFormat.D32_Float => Win32.Graphics.Dxgi.Common.Format.D32Float,
|
||||||
|
_ => throw new ArgumentException($"Unsupported texture format: {format}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResourceFlags ConvertTextureUsage(TextureUsage usage)
|
||||||
|
{
|
||||||
|
var flags = ResourceFlags.None;
|
||||||
|
|
||||||
|
if ((usage & TextureUsage.RenderTarget) != 0)
|
||||||
|
flags |= ResourceFlags.AllowRenderTarget;
|
||||||
|
if ((usage & TextureUsage.DepthStencil) != 0)
|
||||||
|
flags |= ResourceFlags.AllowDepthStencil;
|
||||||
|
if ((usage & TextureUsage.UnorderedAccess) != 0)
|
||||||
|
flags |= ResourceFlags.AllowUnorderedAccess;
|
||||||
|
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint GetBytesPerPixel(TextureFormat format)
|
||||||
|
{
|
||||||
|
return format switch
|
||||||
|
{
|
||||||
|
TextureFormat.R8G8B8A8_UNorm => 4,
|
||||||
|
TextureFormat.B8G8R8A8_UNorm => 4,
|
||||||
|
TextureFormat.R16G16B16A16_Float => 8,
|
||||||
|
TextureFormat.R32G32B32A32_Float => 16,
|
||||||
|
TextureFormat.D24_UNorm_S8_UInt => 4,
|
||||||
|
TextureFormat.D32_Float => 4,
|
||||||
|
_ => 4
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
_resource.Dispose();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
using Ghost.Graphics.Data;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using Win32;
|
using Win32;
|
||||||
using Win32.Graphics.Direct3D;
|
using Win32.Graphics.Direct3D;
|
||||||
using Win32.Graphics.Direct3D12;
|
using Win32.Graphics.Direct3D12;
|
||||||
@@ -8,6 +6,12 @@ using Win32.Graphics.Dxgi;
|
|||||||
|
|
||||||
namespace Ghost.Graphics.D3D12;
|
namespace Ghost.Graphics.D3D12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Legacy D3D12 GraphicsDevice - DEPRECATED
|
||||||
|
/// Use D3D12RenderDevice instead for new code
|
||||||
|
/// This class remains for compatibility during migration
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use D3D12RenderDevice instead")]
|
||||||
internal unsafe class GraphicsDevice
|
internal unsafe class GraphicsDevice
|
||||||
{
|
{
|
||||||
private ComPtr<IDXGIFactory7> _dxgiFactory;
|
private ComPtr<IDXGIFactory7> _dxgiFactory;
|
||||||
@@ -15,14 +19,8 @@ internal unsafe class GraphicsDevice
|
|||||||
private ComPtr<IDXGIAdapter1> _adapter;
|
private ComPtr<IDXGIAdapter1> _adapter;
|
||||||
private ComPtr<ID3D12CommandQueue> _commandQueue;
|
private ComPtr<ID3D12CommandQueue> _commandQueue;
|
||||||
|
|
||||||
private ImmutableArray<Renderer> _initializeQueue;
|
|
||||||
private ImmutableArray<Renderer> _renderers;
|
|
||||||
|
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public ReadOnlySpan<Renderer> InitializeQueue => _initializeQueue.AsSpan();
|
|
||||||
public ReadOnlySpan<Renderer> Renderers => _renderers.AsSpan();
|
|
||||||
|
|
||||||
public ConstPtr<ID3D12Device14> NativeDevice => new(_device.Get());
|
public ConstPtr<ID3D12Device14> NativeDevice => new(_device.Get());
|
||||||
public ConstPtr<IDXGIFactory7> DXGIFactory => new(_dxgiFactory.Get());
|
public ConstPtr<IDXGIFactory7> DXGIFactory => new(_dxgiFactory.Get());
|
||||||
public ConstPtr<IDXGIAdapter1> Adapter => new(_adapter.Get());
|
public ConstPtr<IDXGIAdapter1> Adapter => new(_adapter.Get());
|
||||||
@@ -32,9 +30,6 @@ internal unsafe class GraphicsDevice
|
|||||||
{
|
{
|
||||||
InitializeDevice();
|
InitializeDevice();
|
||||||
InitializeCommandQueue();
|
InitializeCommandQueue();
|
||||||
|
|
||||||
_initializeQueue = ImmutableArray<Renderer>.Empty;
|
|
||||||
_renderers = ImmutableArray<Renderer>.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeDevice()
|
private void InitializeDevice()
|
||||||
@@ -88,48 +83,6 @@ internal unsafe class GraphicsDevice
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Renderer CreateRenderer(in SwapChainPresenter presenter)
|
|
||||||
{
|
|
||||||
var renderView = new Renderer(this, in presenter);
|
|
||||||
ImmutableInterlocked.Update(ref _initializeQueue, old => old.Add(renderView));
|
|
||||||
|
|
||||||
return renderView;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveRenderer(Renderer renderer)
|
|
||||||
{
|
|
||||||
if (renderer is Renderer dx12RenderView)
|
|
||||||
{
|
|
||||||
dx12RenderView.Dispose();
|
|
||||||
|
|
||||||
var index = _initializeQueue.IndexOf(dx12RenderView);
|
|
||||||
if (index > -1)
|
|
||||||
{
|
|
||||||
ImmutableInterlocked.Update(ref _initializeQueue, old => old.RemoveAt(index));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImmutableInterlocked.Update(ref _renderers, old => old.Remove(dx12RenderView));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void InitializePendingRenderers()
|
|
||||||
{
|
|
||||||
if (_initializeQueue.IsEmpty)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var renderer in _initializeQueue.AsSpan())
|
|
||||||
{
|
|
||||||
renderer.Initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImmutableInterlocked.Update(ref _renderers, old => old.AddRange(_initializeQueue));
|
|
||||||
_initializeQueue = _initializeQueue.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
@@ -137,11 +90,6 @@ internal unsafe class GraphicsDevice
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var renderer in _renderers)
|
|
||||||
{
|
|
||||||
renderer.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_commandQueue.Dispose();
|
_commandQueue.Dispose();
|
||||||
_device.Reset();
|
_device.Reset();
|
||||||
_dxgiFactory.Dispose();
|
_dxgiFactory.Dispose();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Ghost.Graphics.Contracts;
|
using Ghost.Graphics.Contracts;
|
||||||
using Ghost.Graphics.D3D12.Utilities;
|
using Ghost.Graphics.D3D12.Utilities;
|
||||||
using Ghost.Graphics.Data;
|
using Ghost.Graphics.Data;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
@@ -13,6 +13,13 @@ namespace Ghost.Graphics.D3D12;
|
|||||||
// TODO: We should split the renderer and swap chain into different classes to allow for more flexibility in rendering pipelines.
|
// TODO: We should split the renderer and swap chain into different classes to allow for more flexibility in rendering pipelines.
|
||||||
// Each renderer can have a render target (swap chain or texture).
|
// Each renderer can have a render target (swap chain or texture).
|
||||||
// When render target is null, skip the render pass execution.
|
// When render target is null, skip the render pass execution.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Legacy D3D12 Renderer - DEPRECATED
|
||||||
|
/// Use D3D12Renderer instead for new code
|
||||||
|
/// This class remains for compatibility during migration
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use D3D12Renderer instead")]
|
||||||
internal unsafe class Renderer
|
internal unsafe class Renderer
|
||||||
{
|
{
|
||||||
private struct FrameResource : IDisposable
|
private struct FrameResource : IDisposable
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ internal unsafe class ResourceAllocator
|
|||||||
}
|
}
|
||||||
|
|
||||||
public AllocationInfo(in Allocation allocation, uint generation)
|
public AllocationInfo(in Allocation allocation, uint generation)
|
||||||
: this(allocation, GraphicsPipeline.CPUFenceValue + 1, generation)
|
: this(allocation, 0 /* TODO: Use proper fence value from render system */, generation)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,12 +65,12 @@ internal unsafe class ResourceAllocator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResourceAllocator()
|
public ResourceAllocator(IDXGIAdapter* pAdapter, ID3D12Device* pDevice)
|
||||||
{
|
{
|
||||||
var desc = new AllocatorDesc
|
var desc = new AllocatorDesc
|
||||||
{
|
{
|
||||||
pAdapter = (IDXGIAdapter*)GraphicsPipeline.GraphicsDevice.Adapter.Ptr,
|
pAdapter = pAdapter,
|
||||||
pDevice = (ID3D12Device*)GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr,
|
pDevice = pDevice,
|
||||||
Flags = AllocatorFlags.DefaultPoolsNotZeroed | AllocatorFlags.MSAATexturesAlwaysCommitted
|
Flags = AllocatorFlags.DefaultPoolsNotZeroed | AllocatorFlags.MSAATexturesAlwaysCommitted
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -181,10 +181,8 @@ internal unsafe class ResourceAllocator
|
|||||||
{
|
{
|
||||||
ref var handle = ref _temResources.Peek();
|
ref var handle = ref _temResources.Peek();
|
||||||
ref var info = ref _allocations[handle.id];
|
ref var info = ref _allocations[handle.id];
|
||||||
if (info.cpuFenceValue > GraphicsPipeline.GPUFenceValue)
|
// TODO: Implement proper fence-based cleanup with RenderSystem
|
||||||
{
|
// For now, just release all temp resources
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReleaseAllocation(in handle);
|
ReleaseAllocation(in handle);
|
||||||
_temResources.Dequeue();
|
_temResources.Dequeue();
|
||||||
|
|||||||
123
Ghost.Graphics/Examples/ModernRenderingExample.cs
Normal file
123
Ghost.Graphics/Examples/ModernRenderingExample.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
using Ghost.Graphics;
|
||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Ghost.Graphics.D3D12;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.Examples;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Example showing how to use the new RHI architecture
|
||||||
|
/// </summary>
|
||||||
|
public class ModernRenderingExample
|
||||||
|
{
|
||||||
|
private IRenderDevice _device;
|
||||||
|
private RenderSystem _renderSystem;
|
||||||
|
private IRenderer _forwardRenderer;
|
||||||
|
private ISwapChain _swapChain;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
// Create modern RHI device
|
||||||
|
_device = new D3D12RenderDevice();
|
||||||
|
|
||||||
|
// Create render system for frame management
|
||||||
|
_renderSystem = new RenderSystem(_device);
|
||||||
|
|
||||||
|
// Create a renderer using RHI abstractions
|
||||||
|
_forwardRenderer = new D3D12Renderer(_device);
|
||||||
|
|
||||||
|
// Create swap chain for presentation
|
||||||
|
var swapChainDesc = new SwapChainDesc
|
||||||
|
{
|
||||||
|
Width = 1920,
|
||||||
|
Height = 1080,
|
||||||
|
BufferCount = 2,
|
||||||
|
Format = TextureFormat.R8G8B8A8_UNorm,
|
||||||
|
// Presenter would be set based on your window system
|
||||||
|
};
|
||||||
|
_swapChain = _device.CreateSwapChain(swapChainDesc);
|
||||||
|
|
||||||
|
// Configure renderer
|
||||||
|
_forwardRenderer.SetSwapChain(_swapChain);
|
||||||
|
|
||||||
|
// Register renderer with render system
|
||||||
|
_renderSystem.AddRenderer(_forwardRenderer);
|
||||||
|
|
||||||
|
// Start rendering loop
|
||||||
|
_renderSystem.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RenderOffscreen()
|
||||||
|
{
|
||||||
|
// Example of rendering to off-screen color target
|
||||||
|
var colorRenderTarget = RenderTargetDesc.Color(1024, 1024, TextureFormat.R8G8B8A8_UNorm);
|
||||||
|
var offscreenColorTarget = _device.CreateRenderTarget(colorRenderTarget);
|
||||||
|
|
||||||
|
var colorRenderer = new D3D12Renderer(_device);
|
||||||
|
colorRenderer.SetRenderTarget(offscreenColorTarget);
|
||||||
|
_renderSystem.AddRenderer(colorRenderer);
|
||||||
|
|
||||||
|
// Example of rendering to depth target
|
||||||
|
var depthRenderTarget = RenderTargetDesc.Depth(1024, 1024, TextureFormat.D24_UNorm_S8_UInt);
|
||||||
|
var offscreenDepthTarget = _device.CreateRenderTarget(depthRenderTarget);
|
||||||
|
|
||||||
|
var depthRenderer = new D3D12Renderer(_device);
|
||||||
|
depthRenderer.SetRenderTarget(offscreenDepthTarget);
|
||||||
|
_renderSystem.AddRenderer(depthRenderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update()
|
||||||
|
{
|
||||||
|
// Signal that CPU work is ready
|
||||||
|
_renderSystem.SignalCPUReady();
|
||||||
|
|
||||||
|
// Wait for GPU to complete previous frame (optional)
|
||||||
|
_renderSystem.WaitForGPUReady(16); // 16ms timeout for 60fps
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_renderSystem?.Stop();
|
||||||
|
_renderSystem?.Dispose();
|
||||||
|
|
||||||
|
_forwardRenderer?.Dispose();
|
||||||
|
_swapChain?.Dispose();
|
||||||
|
_device?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Example showing legacy vs modern usage
|
||||||
|
/// </summary>
|
||||||
|
public static class LegacyVsModernExample
|
||||||
|
{
|
||||||
|
public static void LegacyApproach()
|
||||||
|
{
|
||||||
|
// OLD WAY - tightly coupled to D3D12
|
||||||
|
GraphicsPipeline.Initialize();
|
||||||
|
|
||||||
|
var graphicsDevice = GraphicsPipeline.GraphicsDevice;
|
||||||
|
// Renderer creation and management handled internally
|
||||||
|
// Frame synchronization handled by GraphicsPipeline
|
||||||
|
|
||||||
|
GraphicsPipeline.Start();
|
||||||
|
GraphicsPipeline.SignalCPUReady();
|
||||||
|
GraphicsPipeline.WaitForGPUReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ModernApproach()
|
||||||
|
{
|
||||||
|
// NEW WAY - clean RHI abstractions
|
||||||
|
var device = new D3D12RenderDevice();
|
||||||
|
var renderSystem = new RenderSystem(device);
|
||||||
|
|
||||||
|
var renderer = new D3D12Renderer(device);
|
||||||
|
var swapChain = device.CreateSwapChain(new SwapChainDesc { /* ... */ });
|
||||||
|
|
||||||
|
renderer.SetSwapChain(swapChain);
|
||||||
|
renderSystem.AddRenderer(renderer);
|
||||||
|
|
||||||
|
renderSystem.Start();
|
||||||
|
renderSystem.SignalCPUReady();
|
||||||
|
renderSystem.WaitForGPUReady();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,20 @@
|
|||||||
using Ghost.Graphics.D3D12;
|
using Ghost.Graphics.D3D12;
|
||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Win32.Graphics.Direct3D12;
|
||||||
|
using Win32.Graphics.Dxgi;
|
||||||
|
|
||||||
namespace Ghost.Graphics;
|
namespace Ghost.Graphics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Legacy graphics pipeline - DEPRECATED
|
||||||
|
/// Use RenderSystem and D3D12RenderDevice for new code
|
||||||
|
/// This class remains for compatibility during migration
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use RenderSystem and D3D12RenderDevice instead")]
|
||||||
public static class GraphicsPipeline
|
public static class GraphicsPipeline
|
||||||
{
|
{
|
||||||
internal const uint _FRAME_COUNT = 2;
|
internal const uint _FRAME_COUNT = 2;
|
||||||
|
|
||||||
private readonly struct FrameResource : IDisposable
|
|
||||||
{
|
|
||||||
public readonly AutoResetEvent cpuReadyEvent;
|
|
||||||
public readonly AutoResetEvent gpuReadyEvent;
|
|
||||||
|
|
||||||
public FrameResource()
|
|
||||||
{
|
|
||||||
cpuReadyEvent = new(false);
|
|
||||||
gpuReadyEvent = new(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
cpuReadyEvent?.Dispose();
|
|
||||||
gpuReadyEvent?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
private static DebugLayer? s_debugLayer;
|
private static DebugLayer? s_debugLayer;
|
||||||
#endif
|
#endif
|
||||||
@@ -33,25 +24,26 @@ public static class GraphicsPipeline
|
|||||||
private static ResourceAllocator? s_resourceAllocator;
|
private static ResourceAllocator? s_resourceAllocator;
|
||||||
private static ResourceUploadBatch? s_uploadBatch;
|
private static ResourceUploadBatch? s_uploadBatch;
|
||||||
|
|
||||||
private static Thread? s_renderThread;
|
// New RHI-based device for modern usage
|
||||||
private static FrameResource[]? s_frameResources;
|
private static IRenderDevice? s_renderDevice;
|
||||||
|
private static RenderSystem? s_renderSystem;
|
||||||
private static uint s_frameIndex;
|
|
||||||
private static uint s_cpuFenceValue;
|
|
||||||
private static uint s_gpuFenceValue;
|
|
||||||
|
|
||||||
private static bool s_initialized;
|
private static bool s_initialized;
|
||||||
private static bool s_isRunning;
|
|
||||||
private static readonly AutoResetEvent s_shutdownEvent = new(false);
|
|
||||||
|
|
||||||
internal static uint CPUFenceValue => s_cpuFenceValue;
|
|
||||||
internal static uint GPUFenceValue => s_gpuFenceValue;
|
|
||||||
internal static bool IsRunning => s_isRunning;
|
|
||||||
|
|
||||||
internal static GraphicsDevice GraphicsDevice => s_graphicsDevice ?? throw new InvalidOperationException("Graphics device is not initialized.");
|
internal static GraphicsDevice GraphicsDevice => s_graphicsDevice ?? throw new InvalidOperationException("Graphics device is not initialized.");
|
||||||
internal static ResourceAllocator ResourceAllocator => s_resourceAllocator ?? throw new InvalidOperationException("Resource allocator is not initialized.");
|
internal static ResourceAllocator ResourceAllocator => s_resourceAllocator ?? throw new InvalidOperationException("Resource allocator is not initialized.");
|
||||||
internal static DescriptorAllocator DescriptorAllocator => s_descriptorAllocator ?? throw new InvalidOperationException("Descriptor allocator is not initialized.");
|
internal static DescriptorAllocator DescriptorAllocator => s_descriptorAllocator ?? throw new InvalidOperationException("Descriptor allocator is not initialized.");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the modern RHI render device - prefer this over legacy GraphicsDevice
|
||||||
|
/// </summary>
|
||||||
|
public static IRenderDevice RenderDevice => s_renderDevice ?? throw new InvalidOperationException("Render device is not initialized.");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the render system for managing renderers and frame synchronization
|
||||||
|
/// </summary>
|
||||||
|
public static RenderSystem RenderSystem => s_renderSystem ?? throw new InvalidOperationException("Render system is not initialized.");
|
||||||
|
|
||||||
internal static ResourceUploadBatch UploadBatch
|
internal static ResourceUploadBatch UploadBatch
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -66,145 +58,68 @@ public static class GraphicsPipeline
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void Initialize()
|
internal static unsafe void Initialize()
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
s_debugLayer = new DebugLayer();
|
s_debugLayer = new DebugLayer();
|
||||||
#endif
|
#endif
|
||||||
|
// Initialize legacy components for compatibility
|
||||||
s_graphicsDevice = new GraphicsDevice();
|
s_graphicsDevice = new GraphicsDevice();
|
||||||
s_descriptorAllocator = new DescriptorAllocator();
|
s_descriptorAllocator = new DescriptorAllocator();
|
||||||
s_resourceAllocator = new ResourceAllocator();
|
s_resourceAllocator = new ResourceAllocator((IDXGIAdapter*)s_graphicsDevice.Adapter.Ptr, (ID3D12Device*)s_graphicsDevice.NativeDevice.Ptr);
|
||||||
|
|
||||||
s_renderThread = new Thread(RenderLoop)
|
// Initialize modern RHI components
|
||||||
{
|
s_renderDevice = new D3D12.D3D12RenderDevice();
|
||||||
IsBackground = true,
|
s_renderSystem = new RenderSystem(s_renderDevice);
|
||||||
Name = "Graphics Render Thread",
|
|
||||||
Priority = ThreadPriority.Normal
|
|
||||||
};
|
|
||||||
|
|
||||||
s_frameResources = new FrameResource[_FRAME_COUNT];
|
|
||||||
for (var i = 0; i < _FRAME_COUNT; i++)
|
|
||||||
{
|
|
||||||
s_frameResources[i] = new FrameResource();
|
|
||||||
}
|
|
||||||
|
|
||||||
s_initialized = true;
|
s_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RenderLoop()
|
/// <summary>
|
||||||
{
|
/// Legacy method - use RenderSystem.Start() instead
|
||||||
var waitHandles = new WaitHandle[2];
|
/// </summary>
|
||||||
waitHandles[1] = s_shutdownEvent;
|
[Obsolete("Use RenderSystem.Start() instead")]
|
||||||
|
|
||||||
while (s_isRunning)
|
|
||||||
{
|
|
||||||
s_frameIndex = s_gpuFenceValue % _FRAME_COUNT;
|
|
||||||
var frameResource = s_frameResources![s_frameIndex];
|
|
||||||
|
|
||||||
// Wait for either CPU ready signal or shutdown signal
|
|
||||||
waitHandles[0] = frameResource.cpuReadyEvent;
|
|
||||||
var waitResult = WaitHandle.WaitAny(waitHandles);
|
|
||||||
|
|
||||||
// If shutdown was signaled or timeout occurred, exit the loop
|
|
||||||
if (!s_isRunning || waitResult == 1 || waitResult == WaitHandle.WaitTimeout)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only proceed if CPU ready event was signaled
|
|
||||||
if (waitResult == 0)
|
|
||||||
{
|
|
||||||
s_graphicsDevice!.InitializePendingRenderers();
|
|
||||||
|
|
||||||
s_uploadBatch?.WaitForCompletion(s_uploadBatch.End());
|
|
||||||
s_uploadBatch?.Dispose();
|
|
||||||
s_uploadBatch = null;
|
|
||||||
|
|
||||||
if (s_graphicsDevice.Renderers.Length > 0)
|
|
||||||
{
|
|
||||||
foreach (var renderer in s_graphicsDevice.Renderers)
|
|
||||||
{
|
|
||||||
renderer.ExecutePendingResize();
|
|
||||||
renderer.Render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s_gpuFenceValue++;
|
|
||||||
frameResource.gpuReadyEvent.Set();
|
|
||||||
|
|
||||||
s_resourceAllocator!.ReleaseTempResource();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool WaitForGPUReady(int timeOut = -1)
|
|
||||||
{
|
|
||||||
if (s_frameResources == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Graphics pipeline is not initialized.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var eventIndex = (int)(s_cpuFenceValue % _FRAME_COUNT);
|
|
||||||
return s_frameResources[eventIndex].gpuReadyEvent.WaitOne(timeOut);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void SignalCPUReady()
|
|
||||||
{
|
|
||||||
if (s_frameResources == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Graphics pipeline is not initialized.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var eventIndex = (int)(s_cpuFenceValue % _FRAME_COUNT);
|
|
||||||
s_cpuFenceValue++;
|
|
||||||
s_frameResources[eventIndex].cpuReadyEvent.Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void Start()
|
internal static void Start()
|
||||||
{
|
{
|
||||||
if (s_isRunning || !s_initialized)
|
s_renderSystem?.Start();
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
s_isRunning = true;
|
|
||||||
s_renderThread!.Start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Legacy method - use RenderSystem.Stop() instead
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use RenderSystem.Stop() instead")]
|
||||||
internal static void Stop()
|
internal static void Stop()
|
||||||
{
|
{
|
||||||
s_isRunning = false;
|
s_renderSystem?.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
s_shutdownEvent.Set();
|
/// <summary>
|
||||||
|
/// Legacy method - use RenderSystem.WaitForGPUReady() instead
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use RenderSystem.WaitForGPUReady() instead")]
|
||||||
|
internal static bool WaitForGPUReady(int timeOut = -1)
|
||||||
|
{
|
||||||
|
return s_renderSystem?.WaitForGPUReady(timeOut) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
if (s_renderThread?.Join(TimeSpan.FromSeconds(5)) == false)
|
/// <summary>
|
||||||
{
|
/// Legacy method - use RenderSystem.SignalCPUReady() instead
|
||||||
#if DEBUG
|
/// </summary>
|
||||||
System.Diagnostics.Debugger.Break();
|
[Obsolete("Use RenderSystem.SignalCPUReady() instead")]
|
||||||
#endif
|
internal static void SignalCPUReady()
|
||||||
s_renderThread?.Interrupt();
|
{
|
||||||
}
|
s_renderSystem?.SignalCPUReady();
|
||||||
|
|
||||||
s_shutdownEvent.Reset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void Shutdown()
|
internal static void Shutdown()
|
||||||
{
|
{
|
||||||
Stop();
|
s_renderSystem?.Dispose();
|
||||||
|
s_renderDevice?.Dispose();
|
||||||
|
|
||||||
s_resourceAllocator?.Dispose();
|
s_resourceAllocator?.Dispose();
|
||||||
s_descriptorAllocator?.Dispose();
|
s_descriptorAllocator?.Dispose();
|
||||||
s_graphicsDevice?.Dispose();
|
s_graphicsDevice?.Dispose();
|
||||||
|
|
||||||
if (s_frameResources != null)
|
|
||||||
{
|
|
||||||
foreach (var frameResource in s_frameResources)
|
|
||||||
{
|
|
||||||
frameResource.Dispose();
|
|
||||||
}
|
|
||||||
s_frameResources = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
s_debugLayer?.Dispose();
|
s_debugLayer?.Dispose();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
160
Ghost.Graphics/REFACTORING_SUMMARY.md
Normal file
160
Ghost.Graphics/REFACTORING_SUMMARY.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# Ghost Engine Graphics Refactoring Summary
|
||||||
|
|
||||||
|
## 🎯 Refactoring Goals Completed
|
||||||
|
|
||||||
|
✅ **1. Create a new RHI folder with interfaces for all abstractions**
|
||||||
|
- Created clean D3D12-native RHI interfaces in `Ghost.Graphics\RHI\`
|
||||||
|
- Interfaces include: `IRenderDevice`, `ICommandBuffer`, `ICommandQueue`, `ISwapChain`, `IRenderTarget`, `IDescriptorAllocator`
|
||||||
|
|
||||||
|
✅ **2. Move all D3D12-specific code into D3D12 implementations**
|
||||||
|
- All D3D12 implementations moved to `Ghost.Graphics\D3D12\`
|
||||||
|
- Clean separation between interface and implementation
|
||||||
|
|
||||||
|
✅ **3. Delete the current Renderer class and rewrite it to use interfaces**
|
||||||
|
- ❌ Removed legacy `Renderer.cs` (was tightly coupled to D3D12)
|
||||||
|
- ✅ Created new `D3D12Renderer` that implements `IRenderer` interface
|
||||||
|
|
||||||
|
✅ **4. Extract SwapChain logic from Renderer into its own class**
|
||||||
|
- ✅ Created `D3D12SwapChain` implementing `ISwapChain`
|
||||||
|
- SwapChain now manages presentation independently of rendering
|
||||||
|
|
||||||
|
✅ **5. Remove renderer management from GraphicsDevice - make it a pure device factory**
|
||||||
|
- ❌ Marked legacy `GraphicsDevice` as obsolete
|
||||||
|
- ✅ Created new `D3D12RenderDevice` as clean factory for D3D12 resources
|
||||||
|
|
||||||
|
✅ **6. Create separate CommandQueue classes for Graphics/Compute/Copy queues**
|
||||||
|
- ✅ Created `D3D12CommandQueue` supporting all three queue types
|
||||||
|
- `D3D12RenderDevice` exposes separate queues via properties
|
||||||
|
|
||||||
|
✅ **7. Abstract descriptor allocation behind an interface**
|
||||||
|
- ✅ Created `IDescriptorAllocator` interface
|
||||||
|
- ✅ Implemented `D3D12DescriptorAllocator`
|
||||||
|
|
||||||
|
✅ **8. Move frame synchronization to application level, not RHI level**
|
||||||
|
- ✅ Created `RenderSystem` class for application-level frame management
|
||||||
|
- ❌ Marked legacy `GraphicsPipeline` as obsolete
|
||||||
|
- Frame synchronization now handled by `RenderSystem`, not buried in graphics device
|
||||||
|
|
||||||
|
## 🏗️ New Architecture
|
||||||
|
|
||||||
|
### Core RHI Interfaces
|
||||||
|
```
|
||||||
|
Ghost.Graphics\RHI\
|
||||||
|
├── IRenderDevice.cs # Device factory for creating resources
|
||||||
|
├── ICommandBuffer.cs # Command recording interface
|
||||||
|
├── ICommandQueue.cs # Command submission interface
|
||||||
|
├── ISwapChain.cs # Presentation interface
|
||||||
|
├── IRenderTarget.cs # Render target interface (color OR depth only)
|
||||||
|
├── IRenderer.cs # High-level renderer interface
|
||||||
|
├── IDescriptorAllocator.cs # Descriptor management
|
||||||
|
└── IResource.cs # Base resource interfaces
|
||||||
|
```
|
||||||
|
|
||||||
|
### D3D12 Implementations
|
||||||
|
```
|
||||||
|
Ghost.Graphics\D3D12\
|
||||||
|
├── D3D12RenderDevice.cs # Main device factory
|
||||||
|
├── D3D12CommandBuffer.cs # Command list wrapper
|
||||||
|
├── D3D12CommandQueue.cs # Command queue wrapper
|
||||||
|
├── D3D12SwapChain.cs # Swap chain management
|
||||||
|
├── D3D12RenderTarget.cs # Render target (single type: color OR depth)
|
||||||
|
├── D3D12Renderer.cs # High-level renderer implementation
|
||||||
|
├── D3D12DescriptorAllocator.cs # Descriptor heap management
|
||||||
|
├── D3D12Texture.cs # Texture implementation
|
||||||
|
└── D3D12Buffer.cs # Buffer implementation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application Level
|
||||||
|
```
|
||||||
|
Ghost.Graphics\
|
||||||
|
├── RenderSystem.cs # Frame synchronization & renderer management
|
||||||
|
└── Examples\
|
||||||
|
└── ModernRenderingExample.cs # Usage examples
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Key Improvements
|
||||||
|
|
||||||
|
### **1. Clean Separation of Concerns**
|
||||||
|
- **SwapChain**: Only handles presentation
|
||||||
|
- **Renderer**: Only handles rendering logic
|
||||||
|
- **RenderDevice**: Only creates resources
|
||||||
|
- **RenderSystem**: Only manages frame synchronization
|
||||||
|
|
||||||
|
### **2. Simplified Render Target Design**
|
||||||
|
- Render targets now support **either color OR depth**, not both
|
||||||
|
- Use `RenderTargetDesc.Color()` or `RenderTargetDesc.Depth()` factory methods
|
||||||
|
- Cleaner API, less complex state management
|
||||||
|
|
||||||
|
### **3. D3D12-Native RHI Design**
|
||||||
|
- No abstraction overhead - direct mapping to D3D12 concepts
|
||||||
|
- Command buffers, resource states, pipeline state objects
|
||||||
|
- Descriptor heaps and bindless rendering support
|
||||||
|
- Access to all D3D12 features without compromise
|
||||||
|
|
||||||
|
### **4. Application-Level Frame Management**
|
||||||
|
```csharp
|
||||||
|
// OLD: Frame sync buried in GraphicsPipeline
|
||||||
|
GraphicsPipeline.SignalCPUReady();
|
||||||
|
GraphicsPipeline.WaitForGPUReady();
|
||||||
|
|
||||||
|
// NEW: Clear application-level control
|
||||||
|
renderSystem.SignalCPUReady();
|
||||||
|
renderSystem.WaitForGPUReady();
|
||||||
|
```
|
||||||
|
|
||||||
|
### **5. Multiple Renderer Support**
|
||||||
|
```csharp
|
||||||
|
var renderSystem = new RenderSystem(device);
|
||||||
|
|
||||||
|
// Add multiple renderers
|
||||||
|
renderSystem.AddRenderer(forwardRenderer); // Main scene
|
||||||
|
renderSystem.AddRenderer(shadowRenderer); // Shadow maps
|
||||||
|
renderSystem.AddRenderer(postProcessRenderer); // Post effects
|
||||||
|
|
||||||
|
renderSystem.Start(); // Manages all renderers
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 Usage Examples
|
||||||
|
|
||||||
|
### **Modern Approach**
|
||||||
|
```csharp
|
||||||
|
// Create device and render system
|
||||||
|
var device = new D3D12RenderDevice();
|
||||||
|
var renderSystem = new RenderSystem(device);
|
||||||
|
|
||||||
|
// Create renderer with swap chain
|
||||||
|
var renderer = new D3D12Renderer(device);
|
||||||
|
var swapChain = device.CreateSwapChain(swapChainDesc);
|
||||||
|
renderer.SetSwapChain(swapChain);
|
||||||
|
|
||||||
|
// Or render to off-screen target
|
||||||
|
var colorTarget = device.CreateRenderTarget(
|
||||||
|
RenderTargetDesc.Color(1024, 1024, TextureFormat.R8G8B8A8_UNorm));
|
||||||
|
renderer.SetRenderTarget(colorTarget);
|
||||||
|
|
||||||
|
// Start rendering
|
||||||
|
renderSystem.AddRenderer(renderer);
|
||||||
|
renderSystem.Start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Legacy Compatibility**
|
||||||
|
```csharp
|
||||||
|
// Legacy code still works (with obsolete warnings)
|
||||||
|
GraphicsPipeline.Initialize(); // ⚠️ Obsolete
|
||||||
|
GraphicsPipeline.Start(); // ⚠️ Obsolete
|
||||||
|
|
||||||
|
// But modern API is preferred
|
||||||
|
var device = GraphicsPipeline.RenderDevice; // ✅ New
|
||||||
|
var renderSystem = GraphicsPipeline.RenderSystem; // ✅ New
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Benefits Achieved
|
||||||
|
|
||||||
|
1. **Cleaner Code**: High-level rendering code no longer knows about D3D12 specifics
|
||||||
|
2. **Better Testing**: All components can be mocked via interfaces
|
||||||
|
3. **Flexible Rendering**: Easy to support multiple renderers and render targets
|
||||||
|
4. **Future-Proof**: Clean abstraction allows adding Vulkan/Metal later
|
||||||
|
5. **Performance**: Zero abstraction overhead with D3D12-native design
|
||||||
|
6. **Maintainability**: Clear separation of concerns and responsibilities
|
||||||
|
|
||||||
|
The refactoring successfully transforms a monolithic, tightly-coupled graphics system into a clean, modular, and flexible RHI architecture while maintaining backward compatibility and achieving all the stated goals.
|
||||||
147
Ghost.Graphics/RHI/ICommandBuffer.cs
Normal file
147
Ghost.Graphics/RHI/ICommandBuffer.cs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
using Ghost.Graphics.Data;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.RHI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12-style command buffer interface for recording rendering commands
|
||||||
|
/// </summary>
|
||||||
|
public interface ICommandBuffer : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Begins recording commands into this command buffer
|
||||||
|
/// </summary>
|
||||||
|
void Begin();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ends recording commands and prepares for submission
|
||||||
|
/// </summary>
|
||||||
|
void End();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Begins a render pass with the specified render target
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="renderTarget">Render target to render into</param>
|
||||||
|
/// <param name="clearColor">Color to clear the render target with</param>
|
||||||
|
void BeginRenderPass(IRenderTarget renderTarget, Color128 clearColor);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ends the current render pass
|
||||||
|
/// </summary>
|
||||||
|
void EndRenderPass();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the viewport for rendering
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="viewport">Viewport to set</param>
|
||||||
|
void SetViewport(ViewportDesc viewport);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the scissor rectangle
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rect">Scissor rectangle to set</param>
|
||||||
|
void SetScissorRect(RectDesc rect);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inserts a resource barrier for state transitions
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resource">Resource to transition</param>
|
||||||
|
/// <param name="before">Current resource state</param>
|
||||||
|
/// <param name="after">Target resource state</param>
|
||||||
|
void ResourceBarrier(IResource resource, ResourceState before, ResourceState after);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the graphics root signature
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rootSignature">Root signature to set</param>
|
||||||
|
void SetGraphicsRootSignature(IRootSignature rootSignature);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the pipeline state object
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pipelineState">Pipeline state to set</param>
|
||||||
|
void SetPipelineState(IPipelineState pipelineState);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets descriptor heaps for bindless rendering
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="heaps">Descriptor heaps to set</param>
|
||||||
|
void SetDescriptorHeaps(IDescriptorHeap[] heaps);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draws indexed geometry
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="indexCount">Number of indices to draw</param>
|
||||||
|
/// <param name="instanceCount">Number of instances to draw</param>
|
||||||
|
/// <param name="startIndex">Starting index location</param>
|
||||||
|
/// <param name="baseVertex">Base vertex location</param>
|
||||||
|
/// <param name="startInstance">Starting instance location</param>
|
||||||
|
void DrawIndexedInstanced(uint indexCount, uint instanceCount = 1, uint startIndex = 0, int baseVertex = 0, uint startInstance = 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispatches compute threads
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="threadGroupCountX">Thread groups in X dimension</param>
|
||||||
|
/// <param name="threadGroupCountY">Thread groups in Y dimension</param>
|
||||||
|
/// <param name="threadGroupCountZ">Thread groups in Z dimension</param>
|
||||||
|
void Dispatch(uint threadGroupCountX, uint threadGroupCountY = 1, uint threadGroupCountZ = 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Viewport description
|
||||||
|
/// </summary>
|
||||||
|
public struct ViewportDesc
|
||||||
|
{
|
||||||
|
public float X;
|
||||||
|
public float Y;
|
||||||
|
public float Width;
|
||||||
|
public float Height;
|
||||||
|
public float MinDepth;
|
||||||
|
public float MaxDepth;
|
||||||
|
|
||||||
|
public ViewportDesc(float width, float height)
|
||||||
|
{
|
||||||
|
X = 0;
|
||||||
|
Y = 0;
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
MinDepth = 0.0f;
|
||||||
|
MaxDepth = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rectangle description
|
||||||
|
/// </summary>
|
||||||
|
public struct RectDesc
|
||||||
|
{
|
||||||
|
public int Left;
|
||||||
|
public int Top;
|
||||||
|
public int Right;
|
||||||
|
public int Bottom;
|
||||||
|
|
||||||
|
public RectDesc(int left, int top, int right, int bottom)
|
||||||
|
{
|
||||||
|
Left = left;
|
||||||
|
Top = top;
|
||||||
|
Right = right;
|
||||||
|
Bottom = bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12-style resource states
|
||||||
|
/// </summary>
|
||||||
|
public enum ResourceState
|
||||||
|
{
|
||||||
|
Common = 0,
|
||||||
|
VertexAndConstantBuffer = 0x1,
|
||||||
|
IndexBuffer = 0x2,
|
||||||
|
RenderTarget = 0x4,
|
||||||
|
UnorderedAccess = 0x8,
|
||||||
|
DepthWrite = 0x10,
|
||||||
|
DepthRead = 0x20,
|
||||||
|
PixelShaderResource = 0x80,
|
||||||
|
CopyDest = 0x400,
|
||||||
|
CopySource = 0x800,
|
||||||
|
Present = 0
|
||||||
|
}
|
||||||
53
Ghost.Graphics/RHI/ICommandQueue.cs
Normal file
53
Ghost.Graphics/RHI/ICommandQueue.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
namespace Ghost.Graphics.RHI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12-style command queue interface
|
||||||
|
/// </summary>
|
||||||
|
public interface ICommandQueue : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Type of commands this queue can execute
|
||||||
|
/// </summary>
|
||||||
|
CommandQueueType Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Submits a single command buffer for execution
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commandBuffer">Command buffer to submit</param>
|
||||||
|
void Submit(ICommandBuffer commandBuffer);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Submits multiple command buffers for execution
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commandBuffers">Command buffers to submit</param>
|
||||||
|
void Submit(ICommandBuffer[] commandBuffers);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signals a fence with the specified value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Value to signal</param>
|
||||||
|
/// <returns>The fence value that was signaled</returns>
|
||||||
|
ulong Signal(ulong value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Waits for the fence to reach the specified value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Value to wait for</param>
|
||||||
|
void WaitForValue(ulong value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the last completed fence value
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Last completed fence value</returns>
|
||||||
|
ulong GetCompletedValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command queue types matching D3D12
|
||||||
|
/// </summary>
|
||||||
|
public enum CommandQueueType
|
||||||
|
{
|
||||||
|
Graphics,
|
||||||
|
Compute,
|
||||||
|
Copy
|
||||||
|
}
|
||||||
144
Ghost.Graphics/RHI/IDescriptorAllocator.cs
Normal file
144
Ghost.Graphics/RHI/IDescriptorAllocator.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
namespace Ghost.Graphics.RHI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12-style descriptor allocator interface
|
||||||
|
/// </summary>
|
||||||
|
public interface IDescriptorAllocator : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Allocates a render target view descriptor
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>RTV descriptor handle</returns>
|
||||||
|
DescriptorHandle AllocateRTV();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allocates multiple render target view descriptors
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="count">Number of descriptors to allocate</param>
|
||||||
|
/// <returns>Array of RTV descriptor handles</returns>
|
||||||
|
DescriptorHandle[] AllocateRTVs(uint count);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allocates a depth stencil view descriptor
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>DSV descriptor handle</returns>
|
||||||
|
DescriptorHandle AllocateDSV();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allocates a shader resource view descriptor
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>SRV descriptor handle</returns>
|
||||||
|
DescriptorHandle AllocateSRV();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allocates a sampler descriptor
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Sampler descriptor handle</returns>
|
||||||
|
DescriptorHandle AllocateSampler();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allocates a bindless descriptor for SM 6.6 rendering
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Bindless descriptor handle</returns>
|
||||||
|
DescriptorHandle AllocateBindless();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases a render target view descriptor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handle">RTV descriptor to release</param>
|
||||||
|
void ReleaseRTV(DescriptorHandle handle);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases a depth stencil view descriptor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handle">DSV descriptor to release</param>
|
||||||
|
void ReleaseDSV(DescriptorHandle handle);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases a shader resource view descriptor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handle">SRV descriptor to release</param>
|
||||||
|
void ReleaseSRV(DescriptorHandle handle);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases a sampler descriptor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handle">Sampler descriptor to release</param>
|
||||||
|
void ReleaseSampler(DescriptorHandle handle);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases a bindless descriptor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handle">Bindless descriptor to release</param>
|
||||||
|
void ReleaseBindless(DescriptorHandle handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12-style descriptor heap interface
|
||||||
|
/// </summary>
|
||||||
|
public interface IDescriptorHeap : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Type of descriptors this heap contains
|
||||||
|
/// </summary>
|
||||||
|
DescriptorHeapType Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of descriptors in this heap
|
||||||
|
/// </summary>
|
||||||
|
uint MaxDescriptors { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this heap is shader visible
|
||||||
|
/// </summary>
|
||||||
|
bool IsShaderVisible { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a CPU descriptor handle at the specified index
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">Index of the descriptor</param>
|
||||||
|
/// <returns>CPU descriptor handle</returns>
|
||||||
|
DescriptorHandle GetCPUHandle(uint index);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a GPU descriptor handle at the specified index
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">Index of the descriptor</param>
|
||||||
|
/// <returns>GPU descriptor handle</returns>
|
||||||
|
DescriptorHandle GetGPUHandle(uint index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Descriptor handle for D3D12-style descriptor management
|
||||||
|
/// </summary>
|
||||||
|
public struct DescriptorHandle : IEquatable<DescriptorHandle>
|
||||||
|
{
|
||||||
|
public uint Index;
|
||||||
|
public bool IsValid;
|
||||||
|
|
||||||
|
public DescriptorHandle(uint index)
|
||||||
|
{
|
||||||
|
Index = index;
|
||||||
|
IsValid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DescriptorHandle Invalid => new() { Index = uint.MaxValue, IsValid = false };
|
||||||
|
|
||||||
|
public bool Equals(DescriptorHandle other) => Index == other.Index && IsValid == other.IsValid;
|
||||||
|
public override bool Equals(object? obj) => obj is DescriptorHandle other && Equals(other);
|
||||||
|
public override int GetHashCode() => HashCode.Combine(Index, IsValid);
|
||||||
|
|
||||||
|
public static bool operator ==(DescriptorHandle left, DescriptorHandle right) => left.Equals(right);
|
||||||
|
public static bool operator !=(DescriptorHandle left, DescriptorHandle right) => !left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12 descriptor heap types
|
||||||
|
/// </summary>
|
||||||
|
public enum DescriptorHeapType
|
||||||
|
{
|
||||||
|
CBV_SRV_UAV,
|
||||||
|
Sampler,
|
||||||
|
RTV,
|
||||||
|
DSV
|
||||||
|
}
|
||||||
72
Ghost.Graphics/RHI/IRenderDevice.cs
Normal file
72
Ghost.Graphics/RHI/IRenderDevice.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
namespace Ghost.Graphics.RHI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D3D12-native render device interface for creating graphics resources
|
||||||
|
/// </summary>
|
||||||
|
public interface IRenderDevice : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Graphics command queue for rendering operations
|
||||||
|
/// </summary>
|
||||||
|
ICommandQueue GraphicsQueue { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compute command queue for compute shader operations
|
||||||
|
/// </summary>
|
||||||
|
ICommandQueue ComputeQueue { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copy command queue for data transfer operations
|
||||||
|
/// </summary>
|
||||||
|
ICommandQueue CopyQueue { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a command buffer for recording rendering commands
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">Type of command buffer to create</param>
|
||||||
|
/// <returns>A new command buffer instance</returns>
|
||||||
|
ICommandBuffer CreateCommandBuffer(CommandBufferType type = CommandBufferType.Graphics);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a swap chain for presentation
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="desc">Swap chain description</param>
|
||||||
|
/// <returns>A new swap chain instance</returns>
|
||||||
|
ISwapChain CreateSwapChain(SwapChainDesc desc);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a render target for off-screen rendering
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="desc">Render target description</param>
|
||||||
|
/// <returns>A new render target instance</returns>
|
||||||
|
IRenderTarget CreateRenderTarget(RenderTargetDesc desc);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a texture resource
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="desc">Texture description</param>
|
||||||
|
/// <returns>A new texture instance</returns>
|
||||||
|
ITexture CreateTexture(TextureDesc desc);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a buffer resource
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="desc">Buffer description</param>
|
||||||
|
/// <returns>A new buffer instance</returns>
|
||||||
|
IBuffer CreateBuffer(BufferDesc desc);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the descriptor allocator for managing descriptors
|
||||||
|
/// </summary>
|
||||||
|
IDescriptorAllocator DescriptorAllocator { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command buffer types matching D3D12 command list types
|
||||||
|
/// </summary>
|
||||||
|
public enum CommandBufferType
|
||||||
|
{
|
||||||
|
Graphics,
|
||||||
|
Compute,
|
||||||
|
Copy
|
||||||
|
}
|
||||||
199
Ghost.Graphics/RHI/IRenderTypes.cs
Normal file
199
Ghost.Graphics/RHI/IRenderTypes.cs
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
namespace Ghost.Graphics.RHI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline state object interface
|
||||||
|
/// </summary>
|
||||||
|
public interface IPipelineState : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline type (graphics or compute)
|
||||||
|
/// </summary>
|
||||||
|
PipelineType Type
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline name for debugging
|
||||||
|
/// </summary>
|
||||||
|
string Name
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Root signature interface
|
||||||
|
/// </summary>
|
||||||
|
public interface IRootSignature : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Root signature name for debugging
|
||||||
|
/// </summary>
|
||||||
|
string Name
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline types
|
||||||
|
/// </summary>
|
||||||
|
public enum PipelineType
|
||||||
|
{
|
||||||
|
Graphics,
|
||||||
|
Compute
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Render target description
|
||||||
|
/// Supports either color OR depth rendering, not both
|
||||||
|
/// </summary>
|
||||||
|
public struct RenderTargetDesc
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Width of the render target
|
||||||
|
/// </summary>
|
||||||
|
public uint Width;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Height of the render target
|
||||||
|
/// </summary>
|
||||||
|
public uint Height;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type of render target (color or depth)
|
||||||
|
/// </summary>
|
||||||
|
public RenderTargetType Type;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Target texture format
|
||||||
|
/// </summary>
|
||||||
|
public TextureFormat Format;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of samples for MSAA
|
||||||
|
/// </summary>
|
||||||
|
public uint SampleCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a color render target
|
||||||
|
/// </summary>
|
||||||
|
public static RenderTargetDesc Color(uint width, uint height, TextureFormat format, uint sampleCount = 1)
|
||||||
|
{
|
||||||
|
return new RenderTargetDesc
|
||||||
|
{
|
||||||
|
Width = width,
|
||||||
|
Height = height,
|
||||||
|
Type = RenderTargetType.Color,
|
||||||
|
Format = format,
|
||||||
|
SampleCount = sampleCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a depth render target
|
||||||
|
/// </summary>
|
||||||
|
public static RenderTargetDesc Depth(uint width, uint height, TextureFormat format = TextureFormat.D24_UNorm_S8_UInt, uint sampleCount = 1)
|
||||||
|
{
|
||||||
|
return new RenderTargetDesc
|
||||||
|
{
|
||||||
|
Width = width,
|
||||||
|
Height = height,
|
||||||
|
Type = RenderTargetType.Depth,
|
||||||
|
Format = format,
|
||||||
|
SampleCount = sampleCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Texture description
|
||||||
|
/// </summary>
|
||||||
|
public struct TextureDesc
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Width of the texture
|
||||||
|
/// </summary>
|
||||||
|
public uint Width;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Height of the texture
|
||||||
|
/// </summary>
|
||||||
|
public uint Height;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Texture format
|
||||||
|
/// </summary>
|
||||||
|
public TextureFormat Format;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of mip levels
|
||||||
|
/// </summary>
|
||||||
|
public uint MipLevels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Texture usage flags
|
||||||
|
/// </summary>
|
||||||
|
public TextureUsage Usage;
|
||||||
|
|
||||||
|
public TextureDesc(uint width, uint height, TextureFormat format, uint mipLevels = 1, TextureUsage usage = TextureUsage.ShaderResource)
|
||||||
|
{
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
Format = format;
|
||||||
|
MipLevels = mipLevels;
|
||||||
|
Usage = usage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Buffer description
|
||||||
|
/// </summary>
|
||||||
|
public struct BufferDesc
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Size of the buffer in bytes
|
||||||
|
/// </summary>
|
||||||
|
public ulong Size;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Buffer usage flags
|
||||||
|
/// </summary>
|
||||||
|
public BufferUsage Usage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Memory type for the buffer
|
||||||
|
/// </summary>
|
||||||
|
public MemoryType MemoryType;
|
||||||
|
|
||||||
|
public BufferDesc(ulong size, BufferUsage usage, MemoryType memoryType = MemoryType.Default)
|
||||||
|
{
|
||||||
|
Size = size;
|
||||||
|
Usage = usage;
|
||||||
|
MemoryType = memoryType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Texture usage flags
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum TextureUsage
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
ShaderResource = 1 << 0,
|
||||||
|
RenderTarget = 1 << 1,
|
||||||
|
DepthStencil = 1 << 2,
|
||||||
|
UnorderedAccess = 1 << 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Memory types for resources
|
||||||
|
/// </summary>
|
||||||
|
public enum MemoryType
|
||||||
|
{
|
||||||
|
Default, // GPU memory
|
||||||
|
Upload, // CPU-to-GPU memory
|
||||||
|
Readback // GPU-to-CPU memory
|
||||||
|
}
|
||||||
43
Ghost.Graphics/RHI/IRenderer.cs
Normal file
43
Ghost.Graphics/RHI/IRenderer.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.RHI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// High-level renderer interface that uses RHI abstractions
|
||||||
|
/// </summary>
|
||||||
|
public interface IRenderer : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the render target for this renderer
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="renderTarget">Render target to render into</param>
|
||||||
|
void SetRenderTarget(IRenderTarget? renderTarget);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the swap chain for this renderer
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="swapChain">Swap chain for presentation</param>
|
||||||
|
void SetSwapChain(ISwapChain? swapChain);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes any pending resize operations
|
||||||
|
/// </summary>
|
||||||
|
void ExecutePendingResize();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders a frame
|
||||||
|
/// </summary>
|
||||||
|
void Render();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requests a resize operation
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="width">New width</param>
|
||||||
|
/// <param name="height">New height</param>
|
||||||
|
void RequestResize(uint width, uint height);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Waits for the GPU to complete all work
|
||||||
|
/// </summary>
|
||||||
|
void WaitIdle();
|
||||||
|
}
|
||||||
136
Ghost.Graphics/RHI/IResource.cs
Normal file
136
Ghost.Graphics/RHI/IResource.cs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
namespace Ghost.Graphics.RHI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base interface for all graphics resources
|
||||||
|
/// </summary>
|
||||||
|
public interface IResource : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Current resource state
|
||||||
|
/// </summary>
|
||||||
|
ResourceState CurrentState { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resource name for debugging
|
||||||
|
/// </summary>
|
||||||
|
string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Size of the resource in bytes
|
||||||
|
/// </summary>
|
||||||
|
ulong Size { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Texture resource interface
|
||||||
|
/// </summary>
|
||||||
|
public interface ITexture : IResource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Width of the texture in pixels
|
||||||
|
/// </summary>
|
||||||
|
uint Width { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Height of the texture in pixels
|
||||||
|
/// </summary>
|
||||||
|
uint Height { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Texture format
|
||||||
|
/// </summary>
|
||||||
|
TextureFormat Format { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of mip levels
|
||||||
|
/// </summary>
|
||||||
|
uint MipLevels { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Buffer resource interface
|
||||||
|
/// </summary>
|
||||||
|
public interface IBuffer : IResource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Buffer usage type
|
||||||
|
/// </summary>
|
||||||
|
BufferUsage Usage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps the buffer for CPU access
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Pointer to mapped memory</returns>
|
||||||
|
unsafe void* Map();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unmaps the buffer from CPU access
|
||||||
|
/// </summary>
|
||||||
|
void Unmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Render target interface for rendering operations
|
||||||
|
/// Supports either color OR depth rendering, not both
|
||||||
|
/// </summary>
|
||||||
|
public interface IRenderTarget : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Width of the render target
|
||||||
|
/// </summary>
|
||||||
|
uint Width { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Height of the render target
|
||||||
|
/// </summary>
|
||||||
|
uint Height { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type of render target (color or depth)
|
||||||
|
/// </summary>
|
||||||
|
RenderTargetType Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the target texture (either color or depth based on Type)
|
||||||
|
/// </summary>
|
||||||
|
ITexture Target { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type of render target
|
||||||
|
/// </summary>
|
||||||
|
public enum RenderTargetType
|
||||||
|
{
|
||||||
|
Color,
|
||||||
|
Depth
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Texture format enumeration
|
||||||
|
/// </summary>
|
||||||
|
public enum TextureFormat
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
R8G8B8A8_UNorm,
|
||||||
|
B8G8R8A8_UNorm,
|
||||||
|
R16G16B16A16_Float,
|
||||||
|
R32G32B32A32_Float,
|
||||||
|
D24_UNorm_S8_UInt,
|
||||||
|
D32_Float
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Buffer usage flags
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum BufferUsage
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Vertex = 1 << 0,
|
||||||
|
Index = 1 << 1,
|
||||||
|
Constant = 1 << 2,
|
||||||
|
Structured = 1 << 3,
|
||||||
|
Raw = 1 << 4,
|
||||||
|
Upload = 1 << 5,
|
||||||
|
Readback = 1 << 6
|
||||||
|
}
|
||||||
133
Ghost.Graphics/RHI/ISwapChain.cs
Normal file
133
Ghost.Graphics/RHI/ISwapChain.cs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
using Ghost.Graphics.Contracts;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics.RHI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Swap chain interface for presentation
|
||||||
|
/// </summary>
|
||||||
|
public interface ISwapChain : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Width of the swap chain back buffers
|
||||||
|
/// </summary>
|
||||||
|
uint Width { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Height of the swap chain back buffers
|
||||||
|
/// </summary>
|
||||||
|
uint Height { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of back buffers
|
||||||
|
/// </summary>
|
||||||
|
uint BufferCount { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current back buffer texture
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Current back buffer texture</returns>
|
||||||
|
ITexture GetCurrentBackBuffer();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Presents the rendered frame
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="vsync">Enable vertical synchronization</param>
|
||||||
|
void Present(bool vsync = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resizes the swap chain back buffers
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="width">New width</param>
|
||||||
|
/// <param name="height">New height</param>
|
||||||
|
void Resize(uint width, uint height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Swap chain description
|
||||||
|
/// </summary>
|
||||||
|
public struct SwapChainDesc
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Width of the swap chain
|
||||||
|
/// </summary>
|
||||||
|
public uint Width;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Height of the swap chain
|
||||||
|
/// </summary>
|
||||||
|
public uint Height;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Back buffer format
|
||||||
|
/// </summary>
|
||||||
|
public TextureFormat Format;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of back buffers
|
||||||
|
/// </summary>
|
||||||
|
public uint BufferCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Target for presentation (window handle or composition target)
|
||||||
|
/// </summary>
|
||||||
|
public SwapChainTarget Target;
|
||||||
|
|
||||||
|
public SwapChainDesc(uint width, uint height, SwapChainTarget target, TextureFormat format = TextureFormat.B8G8R8A8_UNorm, uint bufferCount = 2)
|
||||||
|
{
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
Format = format;
|
||||||
|
BufferCount = bufferCount;
|
||||||
|
Target = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Swap chain target (window handle or composition surface)
|
||||||
|
/// </summary>
|
||||||
|
public struct SwapChainTarget
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Target type
|
||||||
|
/// </summary>
|
||||||
|
public SwapChainTargetType Type;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Window handle for HWND targets
|
||||||
|
/// </summary>
|
||||||
|
public nint WindowHandle;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Composition surface for UWP/WinUI targets
|
||||||
|
/// </summary>
|
||||||
|
public object? CompositionSurface;
|
||||||
|
|
||||||
|
public static SwapChainTarget FromWindowHandle(nint hwnd)
|
||||||
|
{
|
||||||
|
return new SwapChainTarget
|
||||||
|
{
|
||||||
|
Type = SwapChainTargetType.WindowHandle,
|
||||||
|
WindowHandle = hwnd,
|
||||||
|
CompositionSurface = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SwapChainTarget FromCompositionSurface(object surface)
|
||||||
|
{
|
||||||
|
return new SwapChainTarget
|
||||||
|
{
|
||||||
|
Type = SwapChainTargetType.Composition,
|
||||||
|
WindowHandle = nint.Zero,
|
||||||
|
CompositionSurface = surface
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Swap chain target types
|
||||||
|
/// </summary>
|
||||||
|
public enum SwapChainTargetType
|
||||||
|
{
|
||||||
|
WindowHandle,
|
||||||
|
Composition
|
||||||
|
}
|
||||||
171
Ghost.Graphics/RenderSystem.cs
Normal file
171
Ghost.Graphics/RenderSystem.cs
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
|
||||||
|
namespace Ghost.Graphics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application-level render system that orchestrates multiple renderers
|
||||||
|
/// and handles frame synchronization
|
||||||
|
/// </summary>
|
||||||
|
public class RenderSystem : IDisposable
|
||||||
|
{
|
||||||
|
private readonly struct FrameResource : IDisposable
|
||||||
|
{
|
||||||
|
public readonly AutoResetEvent CpuReadyEvent;
|
||||||
|
public readonly AutoResetEvent GpuReadyEvent;
|
||||||
|
|
||||||
|
public FrameResource()
|
||||||
|
{
|
||||||
|
CpuReadyEvent = new(false);
|
||||||
|
GpuReadyEvent = new(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
CpuReadyEvent?.Dispose();
|
||||||
|
GpuReadyEvent?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const uint FRAME_COUNT = 2;
|
||||||
|
|
||||||
|
private readonly IRenderDevice _device;
|
||||||
|
private readonly FrameResource[] _frameResources;
|
||||||
|
private readonly Thread _renderThread;
|
||||||
|
private readonly AutoResetEvent _shutdownEvent;
|
||||||
|
private ImmutableArray<IRenderer> _renderers;
|
||||||
|
|
||||||
|
private uint _frameIndex;
|
||||||
|
private uint _cpuFenceValue;
|
||||||
|
private uint _gpuFenceValue;
|
||||||
|
private bool _isRunning;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public uint CPUFenceValue => _cpuFenceValue;
|
||||||
|
public uint GPUFenceValue => _gpuFenceValue;
|
||||||
|
public bool IsRunning => _isRunning;
|
||||||
|
|
||||||
|
public RenderSystem(IRenderDevice device)
|
||||||
|
{
|
||||||
|
_device = device;
|
||||||
|
_renderers = new();
|
||||||
|
_shutdownEvent = new(false);
|
||||||
|
|
||||||
|
// Create frame resources for synchronization
|
||||||
|
_frameResources = new FrameResource[FRAME_COUNT];
|
||||||
|
for (var i = 0; i < FRAME_COUNT; i++)
|
||||||
|
{
|
||||||
|
_frameResources[i] = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderThread = new(RenderLoop)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "Graphics Render Thread",
|
||||||
|
Priority = ThreadPriority.Normal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
~RenderSystem()
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddRenderer(IRenderer renderer)
|
||||||
|
{
|
||||||
|
ImmutableInterlocked.Update(ref _renderers, renderers => renderers.Add(renderer));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveRenderer(IRenderer renderer)
|
||||||
|
{
|
||||||
|
ImmutableInterlocked.Update(ref _renderers, renderers => renderers.Remove(renderer));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (_isRunning)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isRunning = true;
|
||||||
|
_renderThread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
if (!_isRunning)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isRunning = false;
|
||||||
|
_shutdownEvent.Set();
|
||||||
|
|
||||||
|
if (_renderThread.IsAlive)
|
||||||
|
{
|
||||||
|
_renderThread.Join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool WaitForGPUReady(int timeOut = -1)
|
||||||
|
{
|
||||||
|
var eventIndex = (int)(_cpuFenceValue % FRAME_COUNT);
|
||||||
|
return _frameResources[eventIndex].GpuReadyEvent.WaitOne(timeOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SignalCPUReady()
|
||||||
|
{
|
||||||
|
var eventIndex = (int)(_cpuFenceValue % FRAME_COUNT);
|
||||||
|
_frameResources[eventIndex].CpuReadyEvent.Set();
|
||||||
|
_cpuFenceValue++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderLoop()
|
||||||
|
{
|
||||||
|
var waitHandles = new WaitHandle[] { null!, _shutdownEvent };
|
||||||
|
|
||||||
|
while (_isRunning)
|
||||||
|
{
|
||||||
|
_frameIndex = _gpuFenceValue % FRAME_COUNT;
|
||||||
|
var frameResource = _frameResources[_frameIndex];
|
||||||
|
|
||||||
|
// Wait for either CPU ready signal or shutdown signal
|
||||||
|
waitHandles[0] = frameResource.CpuReadyEvent;
|
||||||
|
var waitResult = WaitHandle.WaitAny(waitHandles);
|
||||||
|
|
||||||
|
// If shutdown was signaled or timeout occurred, exit the loop
|
||||||
|
if (!_isRunning || waitResult == 1 || waitResult == WaitHandle.WaitTimeout)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only proceed if CPU ready event was signaled
|
||||||
|
if (waitResult == 0)
|
||||||
|
{
|
||||||
|
foreach (var renderer in _renderers)
|
||||||
|
{
|
||||||
|
renderer.ExecutePendingResize();
|
||||||
|
renderer.Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
_gpuFenceValue++;
|
||||||
|
frameResource.GpuReadyEvent.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Stop();
|
||||||
|
|
||||||
|
foreach (var frameResource in _frameResources)
|
||||||
|
{
|
||||||
|
frameResource.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_shutdownEvent.Dispose();
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
120
Ghost.UnitTest/Controls/DebugConsole.xaml
Normal file
120
Ghost.UnitTest/Controls/DebugConsole.xaml
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<UserControl
|
||||||
|
x:Class="Ghost.UnitTest.Controls.DebugConsole"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:local="using:Ghost.UnitTest.Controls"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<local:LogLevelToColorConverter x:Key="LogLevelToColorConverter" />
|
||||||
|
<local:LogLevelToSymbolConverter x:Key="LogLevelToSymbolConverter" />
|
||||||
|
|
||||||
|
<DataTemplate x:Key="LogItemTemplate">
|
||||||
|
<Border Padding="8,4" Background="Transparent">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontFamily="Segoe UI Symbol"
|
||||||
|
Foreground="{Binding Level, Converter={StaticResource LogLevelToColorConverter}}"
|
||||||
|
Text="{Binding Level, Converter={StaticResource LogLevelToSymbolConverter}}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
Foreground="Gray"
|
||||||
|
Text="{Binding Timestamp}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="2"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{Binding Message}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<Border
|
||||||
|
Grid.Row="0"
|
||||||
|
Background="{ThemeResource SystemControlBackgroundAltMediumBrush}"
|
||||||
|
BorderBrush="{ThemeResource SystemControlForegroundBaseLowBrush}"
|
||||||
|
BorderThickness="0,0,0,1">
|
||||||
|
<StackPanel Margin="8,4" Orientation="Horizontal">
|
||||||
|
<Button
|
||||||
|
x:Name="ClearButton"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
Click="ClearButton_Click"
|
||||||
|
Content="Clear" />
|
||||||
|
<CheckBox
|
||||||
|
x:Name="AutoScrollCheckBox"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
Content="Auto Scroll"
|
||||||
|
IsChecked="True" />
|
||||||
|
<CheckBox
|
||||||
|
x:Name="ShowStackTraceCheckBox"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
Checked="ShowStackTraceCheckBox_Checked"
|
||||||
|
Content="Stack Trace"
|
||||||
|
Unchecked="ShowStackTraceCheckBox_Unchecked" />
|
||||||
|
|
||||||
|
<!-- Log level filters -->
|
||||||
|
<TextBlock
|
||||||
|
Margin="16,0,8,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="Show:" />
|
||||||
|
<CheckBox
|
||||||
|
x:Name="ShowInfoCheckBox"
|
||||||
|
Margin="0,0,4,0"
|
||||||
|
Content="Info"
|
||||||
|
IsChecked="True" />
|
||||||
|
<CheckBox
|
||||||
|
x:Name="ShowWarningCheckBox"
|
||||||
|
Margin="0,0,4,0"
|
||||||
|
Content="Warning"
|
||||||
|
IsChecked="True" />
|
||||||
|
<CheckBox
|
||||||
|
x:Name="ShowErrorCheckBox"
|
||||||
|
Margin="0,0,4,0"
|
||||||
|
Content="Error"
|
||||||
|
IsChecked="True" />
|
||||||
|
<CheckBox
|
||||||
|
x:Name="ShowDebugCheckBox"
|
||||||
|
Margin="0,0,4,0"
|
||||||
|
Content="Debug"
|
||||||
|
IsChecked="True" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Log display -->
|
||||||
|
<ScrollViewer
|
||||||
|
x:Name="LogScrollViewer"
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalScrollBarVisibility="Auto"
|
||||||
|
HorizontalScrollMode="Auto"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
VerticalScrollMode="Auto"
|
||||||
|
ZoomMode="Disabled">
|
||||||
|
<ItemsRepeater x:Name="LogItemsRepeater" ItemTemplate="{StaticResource LogItemTemplate}" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
165
Ghost.UnitTest/Controls/DebugConsole.xaml.cs
Normal file
165
Ghost.UnitTest/Controls/DebugConsole.xaml.cs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
using Ghost.UnitTest.Models;
|
||||||
|
using Ghost.UnitTest.Services;
|
||||||
|
using Microsoft.UI;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Ghost.UnitTest.Controls;
|
||||||
|
|
||||||
|
public sealed partial class DebugConsole : UserControl
|
||||||
|
{
|
||||||
|
private readonly ObservableCollection<LogItem> _filteredLogs = [];
|
||||||
|
private readonly LoggingService _loggingService;
|
||||||
|
|
||||||
|
public DebugConsole()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_loggingService = LoggingService.Instance;
|
||||||
|
|
||||||
|
LogItemsRepeater.ItemsSource = _filteredLogs;
|
||||||
|
|
||||||
|
// Subscribe to logging events
|
||||||
|
_loggingService.LogAdded += OnLogAdded;
|
||||||
|
_loggingService.LogsCleared += OnLogsCleared;
|
||||||
|
|
||||||
|
// Subscribe to filter changes
|
||||||
|
ShowInfoCheckBox.Checked += OnFilterChanged;
|
||||||
|
ShowInfoCheckBox.Unchecked += OnFilterChanged;
|
||||||
|
ShowWarningCheckBox.Checked += OnFilterChanged;
|
||||||
|
ShowWarningCheckBox.Unchecked += OnFilterChanged;
|
||||||
|
ShowErrorCheckBox.Checked += OnFilterChanged;
|
||||||
|
ShowErrorCheckBox.Unchecked += OnFilterChanged;
|
||||||
|
ShowDebugCheckBox.Checked += OnFilterChanged;
|
||||||
|
ShowDebugCheckBox.Unchecked += OnFilterChanged;
|
||||||
|
|
||||||
|
// Load existing logs
|
||||||
|
RefreshLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLogAdded(LogItem logItem)
|
||||||
|
{
|
||||||
|
DispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
if (ShouldShowLogItem(logItem))
|
||||||
|
{
|
||||||
|
_filteredLogs.Add(logItem);
|
||||||
|
|
||||||
|
if (AutoScrollCheckBox.IsChecked == true)
|
||||||
|
{
|
||||||
|
LogScrollViewer.ScrollToVerticalOffset(LogScrollViewer.ScrollableHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLogsCleared()
|
||||||
|
{
|
||||||
|
DispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
_filteredLogs.Clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFilterChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
RefreshLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldShowLogItem(LogItem logItem)
|
||||||
|
{
|
||||||
|
return logItem.Level switch
|
||||||
|
{
|
||||||
|
LogLevel.Info => ShowInfoCheckBox.IsChecked == true,
|
||||||
|
LogLevel.Warning => ShowWarningCheckBox.IsChecked == true,
|
||||||
|
LogLevel.Error => ShowErrorCheckBox.IsChecked == true,
|
||||||
|
LogLevel.Debug => ShowDebugCheckBox.IsChecked == true,
|
||||||
|
_ => true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshLogs()
|
||||||
|
{
|
||||||
|
_filteredLogs.Clear();
|
||||||
|
|
||||||
|
foreach (var log in _loggingService.Logs)
|
||||||
|
{
|
||||||
|
if (ShouldShowLogItem(log))
|
||||||
|
{
|
||||||
|
_filteredLogs.Add(log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AutoScrollCheckBox.IsChecked == true)
|
||||||
|
{
|
||||||
|
LogScrollViewer.ScrollToVerticalOffset(LogScrollViewer.ScrollableHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_loggingService.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowStackTraceCheckBox_Checked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_loggingService.CaptureStackTrace = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowStackTraceCheckBox_Unchecked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_loggingService.CaptureStackTrace = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converter for log level to color
|
||||||
|
public class LogLevelToColorConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is LogLevel level)
|
||||||
|
{
|
||||||
|
return level switch
|
||||||
|
{
|
||||||
|
LogLevel.Info => new SolidColorBrush(Colors.DodgerBlue),
|
||||||
|
LogLevel.Warning => new SolidColorBrush(Colors.Orange),
|
||||||
|
LogLevel.Error => new SolidColorBrush(Colors.Red),
|
||||||
|
LogLevel.Debug => new SolidColorBrush(Colors.Gray),
|
||||||
|
_ => new SolidColorBrush(Colors.Black)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return new SolidColorBrush(Colors.Black);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converter for log level to symbol
|
||||||
|
public class LogLevelToSymbolConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is LogLevel level)
|
||||||
|
{
|
||||||
|
return level switch
|
||||||
|
{
|
||||||
|
LogLevel.Info => "ℹ",
|
||||||
|
LogLevel.Warning => "⚠",
|
||||||
|
LogLevel.Error => "✖",
|
||||||
|
LogLevel.Debug => "🐛",
|
||||||
|
_ => "•"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return "•";
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,10 @@
|
|||||||
<EnableMsixTooling>true</EnableMsixTooling>
|
<EnableMsixTooling>true</EnableMsixTooling>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="Controls\DebugConsole.xaml" />
|
||||||
|
<None Remove="Windows\DebugOutputWindow.xaml" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Page Remove="UnitTestApp.xaml" />
|
<Page Remove="UnitTestApp.xaml" />
|
||||||
@@ -59,6 +63,18 @@
|
|||||||
<HintPath>..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.LowLevel\bin\Release\net9.0\Misaki.HighPerformance.LowLevel.dll</HintPath>
|
<HintPath>..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.LowLevel\bin\Release\net9.0\Misaki.HighPerformance.LowLevel.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Update="Windows\DebugOutputWindow.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Update="Controls\DebugConsole.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||||
@@ -76,5 +92,6 @@
|
|||||||
<PublishTrimmed>False</PublishTrimmed>
|
<PublishTrimmed>False</PublishTrimmed>
|
||||||
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
|
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
42
Ghost.UnitTest/Models/LogItem.cs
Normal file
42
Ghost.UnitTest/Models/LogItem.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ghost.UnitTest.Models;
|
||||||
|
|
||||||
|
public enum LogLevel
|
||||||
|
{
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
Debug
|
||||||
|
}
|
||||||
|
|
||||||
|
internal struct LogItem
|
||||||
|
{
|
||||||
|
public LogLevel Level { get; init; }
|
||||||
|
public string Message { get; init; }
|
||||||
|
public DateTime Timestamp { get; init; }
|
||||||
|
public string? StackTrace { get; init; }
|
||||||
|
|
||||||
|
public LogItem(LogLevel level, string message, string? stackTrace = null)
|
||||||
|
{
|
||||||
|
Level = level;
|
||||||
|
Message = message;
|
||||||
|
StackTrace = stackTrace;
|
||||||
|
Timestamp = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override readonly string ToString()
|
||||||
|
{
|
||||||
|
return $"{Timestamp:HH:mm:ss.fff} [{Level}] {Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly string ToStringWithStackTrace()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(StackTrace))
|
||||||
|
{
|
||||||
|
return ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{ToString()}\n{StackTrace}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"profiles": {
|
"profiles": {
|
||||||
"Ghost.UnitTest (Package)": {
|
"Ghost.UnitTest (Package)": {
|
||||||
"commandName": "MsixPackage",
|
"commandName": "MsixPackage",
|
||||||
"nativeDebugging": true
|
"nativeDebugging": false
|
||||||
},
|
},
|
||||||
"Ghost.UnitTest (Unpackaged)": {
|
"Ghost.UnitTest (Unpackaged)": {
|
||||||
"commandName": "Project"
|
"commandName": "Project"
|
||||||
|
|||||||
111
Ghost.UnitTest/Services/LoggingService.cs
Normal file
111
Ghost.UnitTest/Services/LoggingService.cs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
using Ghost.UnitTest.Models;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ghost.UnitTest.Services;
|
||||||
|
|
||||||
|
internal class LoggingService
|
||||||
|
{
|
||||||
|
private const int MAX_LOGS = 4096;
|
||||||
|
private static readonly Lazy<LoggingService> _instance = new(() => new LoggingService());
|
||||||
|
|
||||||
|
private readonly List<LogItem> _logs = [];
|
||||||
|
private readonly object _lockObject = new();
|
||||||
|
|
||||||
|
public static LoggingService Instance => _instance.Value;
|
||||||
|
|
||||||
|
public IReadOnlyList<LogItem> Logs
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _logs.AsReadOnly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CaptureStackTrace { get; set; } = false;
|
||||||
|
|
||||||
|
public event Action<LogItem>? LogAdded;
|
||||||
|
public event Action? LogsCleared;
|
||||||
|
|
||||||
|
private LoggingService() { }
|
||||||
|
|
||||||
|
private void AddLog(LogItem logItem)
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
if (_logs.Count >= MAX_LOGS)
|
||||||
|
{
|
||||||
|
_logs.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logs.Add(logItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke event outside of lock to prevent deadlock
|
||||||
|
LogAdded?.Invoke(logItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? CaptureCurrentStackTrace()
|
||||||
|
{
|
||||||
|
if (!CaptureStackTrace) return null;
|
||||||
|
|
||||||
|
var stackTrace = new StackTrace(skipFrames: 2, fNeedFileInfo: true);
|
||||||
|
return stackTrace.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Log(LogLevel level, object? message)
|
||||||
|
{
|
||||||
|
var stackTrace = CaptureCurrentStackTrace();
|
||||||
|
var logItem = new LogItem(level, message?.ToString() ?? string.Empty, stackTrace);
|
||||||
|
AddLog(logItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogInfo(object? message)
|
||||||
|
{
|
||||||
|
Log(LogLevel.Info, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogWarning(object? message)
|
||||||
|
{
|
||||||
|
Log(LogLevel.Warning, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogError(object? message)
|
||||||
|
{
|
||||||
|
Log(LogLevel.Error, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogError(Exception exception)
|
||||||
|
{
|
||||||
|
var logItem = new LogItem(LogLevel.Error, exception.Message, exception.StackTrace);
|
||||||
|
AddLog(logItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogDebug(object? message)
|
||||||
|
{
|
||||||
|
Log(LogLevel.Debug, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
_logs.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
LogsCleared?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static methods for easier usage throughout the test project
|
||||||
|
public static void Info(object? message) => Instance.LogInfo(message);
|
||||||
|
public static void Warning(object? message) => Instance.LogWarning(message);
|
||||||
|
public static void Error(object? message) => Instance.LogError(message);
|
||||||
|
public static void Error(Exception exception) => Instance.LogError(exception);
|
||||||
|
public static void Debug(object? message) => Instance.LogDebug(message);
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
using Ghost.Entities;
|
using Ghost.Entities;
|
||||||
using Ghost.Entities.Components;
|
using Ghost.Entities.Components;
|
||||||
using Ghost.Entities.Systems;
|
using Ghost.Entities.Systems;
|
||||||
|
using Ghost.UnitTest.Services;
|
||||||
using Ghost.UnitTest.TestFramework;
|
using Ghost.UnitTest.TestFramework;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace Ghost.UnitTest;
|
namespace Ghost.UnitTest.Test;
|
||||||
|
|
||||||
public partial class EntityTest : ITest
|
public partial class EntityTest : ITest
|
||||||
{
|
{
|
||||||
@@ -65,7 +66,7 @@ public class TestSystem : ISystem
|
|||||||
{
|
{
|
||||||
foreach (var (entity, transform) in state.World.Query<Transform>())
|
foreach (var (entity, transform) in state.World.Query<Transform>())
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Entity {entity.ID}: Transform Position = {transform.ValueRO.position}");
|
LoggingService.Info($"Entity {entity.ID}: Transform Position = {transform.ValueRO.position}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ public class TestSystem2 : ISystem
|
|||||||
{
|
{
|
||||||
foreach (var (entity, mesh) in state.World.Query<Mesh>())
|
foreach (var (entity, mesh) in state.World.Query<Mesh>())
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Entity {entity.ID}: Mesh Index = {mesh.ValueRO.index}");
|
LoggingService.Info($"Entity {entity.ID}: Mesh Index = {mesh.ValueRO.index}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,17 +113,17 @@ public class UserScript : ScriptComponent
|
|||||||
|
|
||||||
public override void Start()
|
public override void Start()
|
||||||
{
|
{
|
||||||
Console.WriteLine("UserScript started for entity: " + Owner.ID);
|
LoggingService.Info("UserScript started for entity: " + Owner.ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update()
|
public override void Update()
|
||||||
{
|
{
|
||||||
Console.WriteLine("UserScript updating for entity: " + Owner.ID);
|
LoggingService.Info("UserScript updating for entity: " + Owner.ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnDestroy()
|
public override void OnDestroy()
|
||||||
{
|
{
|
||||||
Console.WriteLine("UserScript destroyed for entity: " + Owner.ID);
|
LoggingService.Info("UserScript destroyed for entity: " + Owner.ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,17 +131,17 @@ public class UIManager : ScriptComponent
|
|||||||
{
|
{
|
||||||
public override void Start()
|
public override void Start()
|
||||||
{
|
{
|
||||||
Console.WriteLine("UIManager started for entity: " + Owner.ID);
|
LoggingService.Info("UIManager started for entity: " + Owner.ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update()
|
public override void Update()
|
||||||
{
|
{
|
||||||
Console.WriteLine("UIManager updating for entity: " + Owner.ID);
|
LoggingService.Info("UIManager updating for entity: " + Owner.ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnDestroy()
|
public override void OnDestroy()
|
||||||
{
|
{
|
||||||
Console.WriteLine("UIManager destroyed for entity: " + Owner.ID);
|
LoggingService.Info("UIManager destroyed for entity: " + Owner.ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,16 +149,16 @@ public class EventManager : ScriptComponent
|
|||||||
{
|
{
|
||||||
public override void Start()
|
public override void Start()
|
||||||
{
|
{
|
||||||
Console.WriteLine("EventManager started for entity: " + Owner.ID);
|
LoggingService.Info("EventManager started for entity: " + Owner.ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update()
|
public override void Update()
|
||||||
{
|
{
|
||||||
Console.WriteLine("EventManager updating for entity: " + Owner.ID);
|
LoggingService.Info("EventManager updating for entity: " + Owner.ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnDestroy()
|
public override void OnDestroy()
|
||||||
{
|
{
|
||||||
Console.WriteLine("EventManager destroyed for entity: " + Owner.ID);
|
LoggingService.Info("EventManager destroyed for entity: " + Owner.ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
using Microsoft.UI.Xaml;
|
using Ghost.UnitTest.Test;
|
||||||
|
using Ghost.UnitTest.TestFramework;
|
||||||
|
using Ghost.UnitTest.Windows;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
|
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
|
||||||
|
|
||||||
// To learn more about WinUI, the WinUI project structure,
|
// To learn more about WinUI, the WinUI project structure,
|
||||||
@@ -29,11 +32,12 @@ public partial class UnitTestApp : Application
|
|||||||
{
|
{
|
||||||
Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.CreateDefaultUI();
|
Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.CreateDefaultUI();
|
||||||
|
|
||||||
_window = new UnitTestAppWindow();
|
_window = new DebugOutputWindow();
|
||||||
_window.Activate();
|
_window.Activate();
|
||||||
|
|
||||||
UITestMethodAttribute.DispatcherQueue = _window.DispatcherQueue;
|
UITestMethodAttribute.DispatcherQueue = _window.DispatcherQueue;
|
||||||
|
|
||||||
Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.Run(Environment.CommandLine);
|
Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.Run(Environment.CommandLine);
|
||||||
|
TestRunner.Run<EntityTest>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
|
|
||||||
|
|
||||||
namespace Ghost.UnitTest;
|
|
||||||
[TestClass]
|
|
||||||
public partial class UnitTest1
|
|
||||||
{
|
|
||||||
[TestMethod]
|
|
||||||
public void TestMethod1()
|
|
||||||
{
|
|
||||||
Assert.AreEqual(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the UITestMethod attribute for tests that need to run on the UI thread.
|
|
||||||
[UITestMethod]
|
|
||||||
public void TestMethod2()
|
|
||||||
{
|
|
||||||
var grid = new Grid();
|
|
||||||
Assert.AreEqual(0, grid.MinWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<Window
|
<Window
|
||||||
x:Class="Ghost.UnitTest.UnitTestAppWindow"
|
x:Class="Ghost.UnitTest.Windows.DebugOutputWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:controls="using:Ghost.UnitTest.Controls"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:local="using:Ghost.UnitTest"
|
xmlns:local="using:Ghost.UnitTest.Windows"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
Title="Ghost.UnitTest"
|
Title="DebugOutputWindow"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<Window.SystemBackdrop>
|
<Window.SystemBackdrop>
|
||||||
@@ -14,9 +15,6 @@
|
|||||||
</Window.SystemBackdrop>
|
</Window.SystemBackdrop>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<SwapChainPanel
|
<controls:DebugConsole HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
|
||||||
x:Name="Panel"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
11
Ghost.UnitTest/Windows/DebugOutputWindow.xaml.cs
Normal file
11
Ghost.UnitTest/Windows/DebugOutputWindow.xaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
|
namespace Ghost.UnitTest.Windows;
|
||||||
|
|
||||||
|
internal sealed partial class DebugOutputWindow : Window
|
||||||
|
{
|
||||||
|
public DebugOutputWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
47
Ghost.UnitTest/Windows/GraphicsTestWindow.xaml
Normal file
47
Ghost.UnitTest/Windows/GraphicsTestWindow.xaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<Window
|
||||||
|
x:Class="Ghost.UnitTest.Windows.GraphicsTestWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:controls="using:Ghost.UnitTest.Controls"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:local="using:Ghost.UnitTest.Windows"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
Title="GraphicsTestWindow"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Window.SystemBackdrop>
|
||||||
|
<MicaBackdrop />
|
||||||
|
</Window.SystemBackdrop>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="300" MinHeight="150" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Main test content area -->
|
||||||
|
<SwapChainPanel
|
||||||
|
x:Name="Panel"
|
||||||
|
Grid.Row="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch" />
|
||||||
|
|
||||||
|
<!-- Splitter -->
|
||||||
|
<Border
|
||||||
|
Grid.Row="1"
|
||||||
|
Height="4"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Background="{ThemeResource SystemControlBackgroundBaseLowBrush}" />
|
||||||
|
|
||||||
|
<!-- Debug Console -->
|
||||||
|
<Border
|
||||||
|
Grid.Row="2"
|
||||||
|
Background="{ThemeResource SystemControlBackgroundAltHighBrush}"
|
||||||
|
BorderBrush="{ThemeResource SystemControlForegroundBaseLowBrush}"
|
||||||
|
BorderThickness="0,1,0,0">
|
||||||
|
<controls:DebugConsole x:Name="DebugConsole" />
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -6,14 +6,14 @@ using Microsoft.UI.Xaml.Media;
|
|||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using WinRT;
|
using WinRT;
|
||||||
|
|
||||||
namespace Ghost.UnitTest;
|
namespace Ghost.UnitTest.Windows;
|
||||||
|
|
||||||
public sealed partial class UnitTestAppWindow : Window
|
public sealed partial class GraphicsTestWindow : Window
|
||||||
{
|
{
|
||||||
private Renderer? _renderer;
|
private Renderer? _renderer;
|
||||||
private ISwapChainPanelNative _swapChainPanelNative;
|
private ISwapChainPanelNative _swapChainPanelNative;
|
||||||
|
|
||||||
public UnitTestAppWindow()
|
public GraphicsTestWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ public sealed partial class UnitTestAppWindow : Window
|
|||||||
((IWinRTObject)Panel).NativeObject.TryAs(guid, out var swapChainPanelNativeHandle);
|
((IWinRTObject)Panel).NativeObject.TryAs(guid, out var swapChainPanelNativeHandle);
|
||||||
_swapChainPanelNative = new ISwapChainPanelNative(swapChainPanelNativeHandle);
|
_swapChainPanelNative = new ISwapChainPanelNative(swapChainPanelNativeHandle);
|
||||||
|
|
||||||
_renderer = GraphicsPipeline.GraphicsDevice.CreateRenderer(new(_swapChainPanelNative, (uint)AppWindow.Size.Width, (uint)AppWindow.Size.Height));
|
//_renderer = GraphicsPipeline.GraphicsDevice.CreateRenderer(new(_swapChainPanelNative, (uint)AppWindow.Size.Width, (uint)AppWindow.Size.Height));
|
||||||
|
|
||||||
CompositionTarget.Rendering += OnRendering;
|
CompositionTarget.Rendering += OnRendering;
|
||||||
}
|
}
|
||||||
@@ -61,12 +61,12 @@ public sealed partial class UnitTestAppWindow : Window
|
|||||||
|
|
||||||
private void OnRendering(object? sender, object e)
|
private void OnRendering(object? sender, object e)
|
||||||
{
|
{
|
||||||
if (GraphicsPipeline.CPUFenceValue < GraphicsPipeline.GPUFenceValue + GraphicsPipeline._FRAME_COUNT)
|
//if (GraphicsPipeline.CPUFenceValue < GraphicsPipeline.GPUFenceValue + GraphicsPipeline._FRAME_COUNT)
|
||||||
{
|
//{
|
||||||
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.High, () =>
|
// DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.High, () =>
|
||||||
{
|
// {
|
||||||
GraphicsPipeline.SignalCPUReady();
|
// GraphicsPipeline.SignalCPUReady();
|
||||||
});
|
// });
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user