3 Commits

Author SHA1 Message Date
b84ee586bf fix(ecs): optimize hidden enableable mask resolution in EntityQuery and fix T4 compilation errors 2026-05-29 14:34:36 +09:00
52dfa12e84 Remove test projects and sample app code, cleanup core
Removed all test and sample application files, including Ghost.Entities.Test, Ghost.Graphics.Test, related XAML, assets, and project files. Updated solution to exclude deleted projects. Adjusted tests to comment out allocation manager usage. Updated ComponentDescriptorTests for camelCase property names and display name checks. Minor cleanup in core engine files for resource management and destructors. This streamlines the repository to focus on core engine/runtime code.
2026-05-28 18:32:12 +09:00
326aee2b1c Make FMOD structs/methods readonly, update tests/components
Refactored FMOD and FMOD Studio C# bindings to mark struct methods and property getters as readonly, improving immutability and enabling compiler optimizations. Updated usage of handle for readonly struct members. Simplified using statements with C# 8+ syntax. Updated test/component code: added EditorWorldService.RootNodes, disabled SharedComponentStore, revised EntityQueryTest/SerializationTest for new job scheduler, switched Mesh/Position to IComponentData, and adjusted SystemTest initialization. Minor code style cleanups throughout.
2026-05-28 14:37:48 +09:00
65 changed files with 7859 additions and 6076 deletions

View File

@@ -209,8 +209,6 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}; };
private readonly JobScheduler _jobScheduler = EditorApplication.GetService<EngineCore>().JobScheduler;
public IAssetSettings? CreateDefaultSettings(string ext) public IAssetSettings? CreateDefaultSettings(string ext)
{ {
if (string.Equals(ext, ".obj", StringComparison.OrdinalIgnoreCase)) if (string.Equals(ext, ".obj", StringComparison.OrdinalIgnoreCase))
@@ -264,10 +262,12 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
var meshSettings = ResolveSettings(sourcePath, settings); var meshSettings = ResolveSettings(sourcePath, settings);
using var root = new MeshNode(); using var root = new MeshNode();
var result = await MeshProcessor.ParseMeshAsync(root, sourcePath, AllocationHandle.TLSF, meshSettings, token).ConfigureAwait(false);
var parseJob = new MeshParsingJob(root, sourcePath, AllocationHandle.Persistent, meshSettings); if (result.IsFailure)
var handle = _jobScheduler.Schedule(in parseJob); {
await _jobScheduler.WaitAsync(handle, token); return Result.Failure(result.Message);
}
var manifest = new ModelManifest var manifest = new ModelManifest
{ {
@@ -376,8 +376,8 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
private async ValueTask<(int materialSlotCount, int lodLevelCount)> WriteMeshContentAsync(string targetPath, GeometryMeshNode geometry, CancellationToken token) private async ValueTask<(int materialSlotCount, int lodLevelCount)> WriteMeshContentAsync(string targetPath, GeometryMeshNode geometry, CancellationToken token)
{ {
using var meshletData = await MeshProcessor.BuildMeshletsAsync(_jobScheduler, geometry.Vertices, geometry.Indices, geometry.MaterialParts, token).ConfigureAwait(false); using var meshletData = await MeshProcessor.BuildMeshletsAsync(geometry.Vertices, geometry.Indices, geometry.MaterialParts, token).ConfigureAwait(false);
await MeshProcessor.BuildClusterLodHierarchyAsync(_jobScheduler, meshletData.Share(), token).ConfigureAwait(false); await MeshProcessor.BuildClusterLodHierarchyAsync(meshletData.Share(), token).ConfigureAwait(false);
var bounds = ComputeBounds(geometry.Vertices); var bounds = ComputeBounds(geometry.Vertices);
var header = new MeshContentHeader var header = new MeshContentHeader

View File

@@ -4,7 +4,6 @@ using Ghost.Graphics.RHI;
using Ghost.Graphics.Utilities; using Ghost.Graphics.Utilities;
using Ghost.MeshOptimizer; using Ghost.MeshOptimizer;
using Ghost.Ufbx; using Ghost.Ufbx;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel; using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
@@ -15,7 +14,7 @@ using System.Text;
namespace Ghost.Editor.Core.Assets; namespace Ghost.Editor.Core.Assets;
internal readonly unsafe struct MeshParsingJob : IJob internal unsafe class MeshParsingJob
{ {
private struct GeometryPart : IDisposable private struct GeometryPart : IDisposable
{ {
@@ -38,12 +37,18 @@ internal readonly unsafe struct MeshParsingJob : IJob
private readonly AllocationHandle _allocationHandle; private readonly AllocationHandle _allocationHandle;
private readonly MeshAssetSettings _settings; private readonly MeshAssetSettings _settings;
private readonly TaskCompletionSource<Result> _taskCompletionSource;
public Task<Result> Task => _taskCompletionSource.Task;
public MeshParsingJob(MeshNode rootNode, string filePath, AllocationHandle allocationHandle, MeshAssetSettings settings) public MeshParsingJob(MeshNode rootNode, string filePath, AllocationHandle allocationHandle, MeshAssetSettings settings)
{ {
_rootNode = rootNode; _rootNode = rootNode;
_filePath = filePath; _filePath = filePath;
_allocationHandle = allocationHandle; _allocationHandle = allocationHandle;
_settings = settings; _settings = settings;
_taskCompletionSource = new TaskCompletionSource<Result>(TaskCreationOptions.RunContinuationsAsynchronously);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -319,7 +324,7 @@ internal readonly unsafe struct MeshParsingJob : IJob
}; };
} }
public void Execute(ref readonly JobExecutionContext context) public Result Execute()
{ {
var error = new ufbx_error(); var error = new ufbx_error();
var load_Opts = new ufbx_load_opts var load_Opts = new ufbx_load_opts
@@ -352,15 +357,20 @@ internal readonly unsafe struct MeshParsingJob : IJob
using var scene = new DisposablePtr<ufbx_scene>(ufbx_scene.LoadFile((sbyte*)str.GetUnsafePtr(), &load_Opts, &error)); using var scene = new DisposablePtr<ufbx_scene>(ufbx_scene.LoadFile((sbyte*)str.GetUnsafePtr(), &load_Opts, &error));
if (scene.Get() == null) if (scene.Get() == null)
{ {
Logger.Error(error.description.ToString()); return Result.Failure(error.description.ToString());
return;
} }
ParseHierarchy(scene.Get()->root_node, _rootNode, AllocationHandle.TLSF); ParseHierarchy(scene.Get()->root_node, _rootNode, AllocationHandle.TLSF);
return Result.Success();
} }
} }
internal partial class MeshProcessor internal static partial class MeshProcessor
{ {
public static Task<Result> ParseMeshAsync(MeshNode root, string sourcePath, AllocationHandle allocationHandle, MeshAssetSettings meshSettings, CancellationToken token = default)
{
var parseJob = new MeshParsingJob(root, sourcePath, allocationHandle, meshSettings);
return Task.Run(parseJob.Execute, token);
}
} }

View File

@@ -14,6 +14,7 @@ using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Mathematics.Geometry; using Misaki.HighPerformance.Mathematics.Geometry;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using TerraFX.Interop.Windows;
namespace Ghost.Editor.Core.Assets; namespace Ghost.Editor.Core.Assets;
@@ -785,15 +786,14 @@ internal static unsafe partial class MeshProcessor
internal static partial class MeshProcessor internal static partial class MeshProcessor
{ {
private class MeshletBuildJob
private struct MeshletBuildJob : IJob
{ {
public ClodConfig clodConfig; public ClodConfig clodConfig;
public ClodMesh clodMesh; public ClodMesh clodMesh;
public MeshletContext context; public MeshletContext context;
public readonly void Execute(ref readonly JobExecutionContext ctx) public void Execute()
{ {
Build(in clodConfig, in clodMesh, context, MeshletOutputCallback); Build(in clodConfig, in clodMesh, context, MeshletOutputCallback);
} }
@@ -804,9 +804,7 @@ internal static partial class MeshProcessor
/// Each <see cref="MaterialPartInfo"/> describes a material partition's index range within the unified buffer. /// Each <see cref="MaterialPartInfo"/> describes a material partition's index range within the unified buffer.
/// Meshlets are built per-part and tagged with the corresponding <c>localMaterialIndex</c>. /// Meshlets are built per-part and tagged with the corresponding <c>localMaterialIndex</c>.
/// </summary> /// </summary>
public static async Task<DisposablePtr<MeshletMeshData>> BuildMeshletsAsync(JobScheduler jobScheduler, public static async Task<DisposablePtr<MeshletMeshData>> BuildMeshletsAsync(ReadOnlyView<Vertex> vertices, ReadOnlyView<uint> indices, ReadOnlyView<MaterialPartInfo> parts, CancellationToken token)
ReadOnlyView<Vertex> vertices, ReadOnlyView<uint> indices, ReadOnlyView<MaterialPartInfo> parts,
CancellationToken token)
{ {
Logger.DebugAssert(vertices.Count > 0, "Mesh must have vertices to build meshlets."); Logger.DebugAssert(vertices.Count > 0, "Mesh must have vertices to build meshlets.");
Logger.DebugAssert(indices.Count > 0, "Mesh must have indices to build meshlets."); Logger.DebugAssert(indices.Count > 0, "Mesh must have indices to build meshlets.");
@@ -836,8 +834,6 @@ internal static partial class MeshProcessor
simplifyFallbackSloppy = true, simplifyFallbackSloppy = true,
}; };
var jobs = new MeshletBuildJob[parts.Length];
IntPtr meshletData; IntPtr meshletData;
unsafe unsafe
{ {
@@ -851,6 +847,7 @@ internal static partial class MeshProcessor
for (var i = 0; i < parts.Length; i++) for (var i = 0; i < parts.Length; i++)
{ {
ref readonly var part = ref parts[i]; ref readonly var part = ref parts[i];
MeshletBuildJob job;
unsafe unsafe
{ {
@@ -874,21 +871,15 @@ internal static partial class MeshProcessor
materialIndex = part.materialIndex materialIndex = part.materialIndex
}; };
var job = new MeshletBuildJob job = new MeshletBuildJob
{ {
clodConfig = config, clodConfig = config,
clodMesh = clodMesh, clodMesh = clodMesh,
context = context context = context
}; };
jobs[i] = job;
}
} }
foreach (var job in jobs) await Task.Run(job.Execute, token);
{
var handle = jobScheduler.Schedule(in job);
await jobScheduler.WaitAsync(handle, token);
} }
unsafe unsafe
@@ -1176,20 +1167,19 @@ internal static partial class MeshProcessor
} }
} }
private unsafe struct BuildClusterLodHierarchyJob : IJob private unsafe class BuildClusterLodHierarchyJob
{ {
public MeshletMeshData* meshletData; public MeshletMeshData* meshletData;
public readonly void Execute(ref readonly JobExecutionContext ctx) public void Execute()
{ {
using var scope = AllocationManager.CreateStackScope(); using var meshletIndices = new UnsafeArray<int>(meshletData->meshletCount, AllocationHandle.TLSF);
using var meshletIndices = new UnsafeArray<int>(meshletData->meshletCount, scope.AllocationHandle);
for (var i = 0; i < meshletData->meshletCount; i++) for (var i = 0; i < meshletData->meshletCount; i++)
{ {
meshletIndices[i] = i; meshletIndices[i] = i;
} }
var binaryNodes = new UnsafeList<TempBinaryNode>(meshletData->meshletCount * 2, scope.AllocationHandle); var binaryNodes = new UnsafeList<TempBinaryNode>(meshletData->meshletCount * 2, AllocationHandle.TLSF);
try try
{ {
@@ -1240,14 +1230,13 @@ internal static partial class MeshProcessor
/// Builds a cluster LOD hierarchy from the input meshlet data. /// Builds a cluster LOD hierarchy from the input meshlet data.
/// </summary> /// </summary>
/// <param name="meshletData">The meshlet data.</param> /// <param name="meshletData">The meshlet data.</param>
public static async Task BuildClusterLodHierarchyAsync(JobScheduler jobScheduler, SharedPtr<MeshletMeshData> meshletData, CancellationToken token) public static Task BuildClusterLodHierarchyAsync(SharedPtr<MeshletMeshData> meshletData, CancellationToken token)
{ {
if (meshletData.GetRef().meshletCount == 0) if (meshletData.GetRef().meshletCount == 0)
{ {
return; return Task.CompletedTask;
} }
JobHandle handle;
unsafe unsafe
{ {
var job = new BuildClusterLodHierarchyJob var job = new BuildClusterLodHierarchyJob
@@ -1255,9 +1244,7 @@ internal static partial class MeshProcessor
meshletData = meshletData.Get() meshletData = meshletData.Get()
}; };
handle = jobScheduler.Schedule(in job); return Task.Run(job.Execute, token);
} }
await jobScheduler.WaitAsync(handle, token);
} }
} }

View File

@@ -10,4 +10,7 @@ public interface IInspectable
UIElement? CreateHeader(); UIElement? CreateHeader();
IInspectorModel CreateInspectorModel(); IInspectorModel CreateInspectorModel();
// void OnSelected();
// void OnDeselected();
} }

View File

@@ -1,6 +1,7 @@
using Ghost.Editor.Core.Utilities; using Ghost.Editor.Core.Utilities;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using System.Diagnostics.CodeAnalysis;
namespace Ghost.Editor.Core; namespace Ghost.Editor.Core;
@@ -89,12 +90,25 @@ public static class EditorApplication
public static T GetService<T>() public static T GetService<T>()
where T : class where T : class
{ {
if (s_serviceProvider?.GetService(typeof(T)) is not T service) if (TryGetService<T>(out var service))
{ {
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices."); return service;
} }
return service; throw new ArgumentException("Requested service of type " + typeof(T).FullName + " is not registered.");
}
public static bool TryGetService<T>([NotNullWhen(true)] out T? service)
where T : class
{
if (s_serviceProvider?.GetService(typeof(T)) is T resolvedService)
{
service = resolvedService;
return true;
}
service = null;
return false;
} }
internal static void Shutdown() internal static void Shutdown()

View File

@@ -3,14 +3,13 @@ using Ghost.Editor.Core.SceneGraph;
using Ghost.Engine; using Ghost.Engine;
using Ghost.Engine.Core; using Ghost.Engine.Core;
using Ghost.Entities; using Ghost.Entities;
using Misaki.HighPerformance.Jobs;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
namespace Ghost.Editor.Core.Services; namespace Ghost.Editor.Core.Services;
public class EditorWorldService : IDisposable public class EditorWorldService : IDisposable
{ {
private const int DEFAULT_ENTITY_CAPACITY = 1024;
public World EditorWorld public World EditorWorld
{ {
get; get;
@@ -27,9 +26,9 @@ public class EditorWorldService : IDisposable
public event Action<Entity, string>? EntityNameChanged; public event Action<Entity, string>? EntityNameChanged;
public event Action? SceneGraphRebuilt; public event Action? SceneGraphRebuilt;
public EditorWorldService() public EditorWorldService(JobScheduler? jobScheduler = null)
{ {
EditorWorld = World.Create(entityCapacity: DEFAULT_ENTITY_CAPACITY); EditorWorld = World.Create(jobScheduler, 1024);
} }
public Entity CreateEntity(string name, ushort sceneID, Entity parent = default) public Entity CreateEntity(string name, ushort sceneID, Entity parent = default)

View File

@@ -55,9 +55,16 @@ internal sealed partial class ImportCoordinator : IDisposable
} }
public ValueTask EnqueueAsync(ImportJob job, CancellationToken token = default) public ValueTask EnqueueAsync(ImportJob job, CancellationToken token = default)
{
try
{ {
return _importChannel.Writer.WriteAsync(job, token); return _importChannel.Writer.WriteAsync(job, token);
} }
catch (ChannelClosedException)
{
return ValueTask.CompletedTask;
}
}
private async Task WorkerLoop(CancellationToken token) private async Task WorkerLoop(CancellationToken token)
{ {
@@ -209,6 +216,15 @@ internal sealed partial class ImportCoordinator : IDisposable
{ {
_importChannel.Writer.TryComplete(); _importChannel.Writer.TryComplete();
_cts.Cancel(); _cts.Cancel();
try
{
Task.WaitAll(_workers);
}
catch (AggregateException)
{
}
_cts.Dispose(); _cts.Dispose();
} }
} }

View File

@@ -46,13 +46,6 @@
<Project Path="Runtime/Ghost.Graphics/Ghost.Graphics.csproj" /> <Project Path="Runtime/Ghost.Graphics/Ghost.Graphics.csproj" />
</Folder> </Folder>
<Folder Name="/Test/"> <Folder Name="/Test/">
<Project Path="Test/Ghost.Entities.Test/Ghost.Entities.Test.csproj" />
<Project Path="Test/Ghost.Graphics.Test/Ghost.Graphics.Test.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
<Platform Solution="*|x86" Project="x86" />
<Deploy />
</Project>
<Project Path="Test/Ghost.MicroTest/Ghost.MicroTest.csproj" Id="8c8ffa4b-e1e4-46a1-9221-7b508a109edd" /> <Project Path="Test/Ghost.MicroTest/Ghost.MicroTest.csproj" Id="8c8ffa4b-e1e4-46a1-9221-7b508a109edd" />
<Project Path="Test/Ghost.Shader.Test/Ghost.Shader.Test.csproj" /> <Project Path="Test/Ghost.Shader.Test/Ghost.Shader.Test.csproj" />
<Project Path="Test/Ghost.TestCore/Ghost.TestCore.csproj" /> <Project Path="Test/Ghost.TestCore/Ghost.TestCore.csproj" />

View File

@@ -23,9 +23,9 @@ public sealed partial class EngineCore : IDisposable
private readonly RenderSystem _renderSystem; private readonly RenderSystem _renderSystem;
private readonly AssetManager _assetManager; private readonly AssetManager _assetManager;
internal JobScheduler JobScheduler => _jobScheduler; public JobScheduler JobScheduler => _jobScheduler;
internal RenderSystem RenderSystem => _renderSystem; public RenderSystem RenderSystem => _renderSystem;
internal AssetManager AssetManager => _assetManager; public AssetManager AssetManager => _assetManager;
public EngineCore(IContentProvider contentProvider, IShaderCompilationBridge? shaderCompilationBridge = null) public EngineCore(IContentProvider contentProvider, IShaderCompilationBridge? shaderCompilationBridge = null)
{ {
@@ -55,6 +55,11 @@ public sealed partial class EngineCore : IDisposable
_assetManager = new AssetManager(_renderSystem.GraphicsEngine.ResourceDatabase, _renderSystem.ResourceManager, _contentProvider, _streamingProcessor, _jobScheduler); _assetManager = new AssetManager(_renderSystem.GraphicsEngine.ResourceDatabase, _renderSystem.ResourceManager, _contentProvider, _streamingProcessor, _jobScheduler);
} }
internal void Start()
{
_renderSystem.Start();
}
public void Dispose() public void Dispose()
{ {
_assetManager.Dispose(); _assetManager.Dispose();
@@ -62,11 +67,3 @@ public sealed partial class EngineCore : IDisposable
_jobScheduler.Dispose(); _jobScheduler.Dispose();
} }
} }
[GenerateShaderProperty("TestShader")]
public partial struct TestShaderProperty
{
public Texture2DHandle texture;
public uint someValue;
public float3 otherValue;
}

View File

@@ -293,6 +293,8 @@ internal unsafe struct Archetype : IDisposable
if (sharedInfos.Count > 0) if (sharedInfos.Count > 0)
{ {
sharedInfos.AsSpan().Sort(static (a, b) => a.id.Value.CompareTo(b.id.Value));
var offset = 0; var offset = 0;
_sharedLayouts = new UnsafeArray<SharedComponentLayout>(sharedInfos.Count, AllocationHandle.Persistent); _sharedLayouts = new UnsafeArray<SharedComponentLayout>(sharedInfos.Count, AllocationHandle.Persistent);
@@ -431,7 +433,7 @@ internal unsafe struct Archetype : IDisposable
for (var i = 0; i < _chunkGroups.Count; i++) for (var i = 0; i < _chunkGroups.Count; i++)
{ {
var group = _chunkGroups[i]; var group = _chunkGroups[i];
if (group.sharedDataHash == sharedDataHash) if (group.sharedDataHash == sharedDataHash && group.sharedData.AsSpan().SequenceEqual(sharedData))
{ {
groupIndex = i; groupIndex = i;
group.refCount++; group.refCount++;
@@ -499,7 +501,7 @@ internal unsafe struct Archetype : IDisposable
for (var i = 0; i < _chunkGroups.Count; i++) for (var i = 0; i < _chunkGroups.Count; i++)
{ {
var group = _chunkGroups[i]; var group = _chunkGroups[i];
if (group.sharedDataHash == sharedDataHash) if (group.sharedDataHash == sharedDataHash && group.sharedData.AsSpan().SequenceEqual(sharedData))
{ {
groupIndex = i; groupIndex = i;
group.refCount++; group.refCount++;

View File

@@ -38,7 +38,15 @@ internal struct ComponentInfo
public static class ComponentTypeID<T> public static class ComponentTypeID<T>
where T : unmanaged, IComponent where T : unmanaged, IComponent
{ {
public static readonly Identifier<IComponent> Value = ComponentRegistry.GetOrRegisterComponentID<T>(); public static readonly Identifier<IComponent> Value;
public static readonly bool IsShared = typeof(ISharedComponent).IsAssignableFrom(typeof(T));
public static readonly bool IsEnableable = typeof(IEnableableComponent).IsAssignableFrom(typeof(T));
public static readonly bool IsCleanup = typeof(ICleanupComponent).IsAssignableFrom(typeof(T));
static ComponentTypeID()
{
Value = ComponentRegistry.GetOrRegisterComponentID<T>();
}
} }
internal static class ComponentRegistry internal static class ComponentRegistry
@@ -341,32 +349,97 @@ public partial class ComponentManager : IDisposable
public struct SharedComponentSet : IDisposable public struct SharedComponentSet : IDisposable
{ {
private struct Element : IComparable<Element>
{
public int componentID;
public int offset;
public int size;
public readonly int CompareTo(Element other) => componentID.CompareTo(other.componentID);
}
private BufferWriter _writer; private BufferWriter _writer;
private UnsafeList<Element> _elements;
private bool _isSorted;
public SharedComponentSet(int capacity, AllocationHandle allocationHandle) public SharedComponentSet(int capacity, AllocationHandle allocationHandle)
{ {
_writer = new BufferWriter(capacity, allocationHandle); _writer = new BufferWriter(capacity, allocationHandle);
_elements = new UnsafeList<Element>(4, allocationHandle);
_isSorted = true;
} }
public void With<T>(scoped in T data) public void With<T>(scoped in T data)
where T : unmanaged, ISharedComponent where T : unmanaged, ISharedComponent
{ {
var id = ComponentTypeID<T>.Value.Value;
var offset = _writer.Position;
int size;
unsafe
{
size = sizeof(T);
}
if (_elements.Count > 0 && _elements[_elements.Count - 1].componentID > id)
{
_isSorted = false;
}
_elements.Add(new Element
{
componentID = id,
offset = offset,
size = size
});
_writer.Write(in data); _writer.Write(in data);
} }
public readonly ReadOnlySpan<byte> AsSpan() public ReadOnlySpan<byte> AsSpan()
{ {
if (!_isSorted && _elements.Count > 1)
{
SortData();
}
return _writer.AsSpan(); return _writer.AsSpan();
} }
private void SortData()
{
_elements.AsSpan().Sort();
using var tempBuffer = new UnsafeArray<byte>(_writer.Position, AllocationHandle.Temp);
var srcSpan = _writer.AsSpan();
var dstSpan = tempBuffer.AsSpan();
var currentOffset = 0;
for (var i = 0; i < _elements.Count; i++)
{
var el = _elements[i];
srcSpan.Slice(el.offset, el.size).CopyTo(dstSpan.Slice(currentOffset, el.size));
el.offset = currentOffset;
_elements[i] = el;
currentOffset += el.size;
}
dstSpan.CopyTo(srcSpan);
_isSorted = true;
}
public void Reset() public void Reset()
{ {
_writer.Reset(); _writer.Reset();
_elements.Clear();
_isSorted = true;
} }
public void Dispose() public void Dispose()
{ {
_writer.Dispose(); _writer.Dispose();
_elements.Dispose();
} }
} }
@@ -415,26 +488,24 @@ public struct ComponentSet : IDisposable, IEquatable<ComponentSet>
{ {
_components = new UnsafeArray<Identifier<IComponent>>(components.Length, allocationHandle); _components = new UnsafeArray<Identifier<IComponent>>(components.Length, allocationHandle);
components.CopyTo(_components.AsSpan()); components.CopyTo(_components.AsSpan());
_components.AsSpan().Sort(static (a, b) => a.Value.CompareTo(b.Value));
_hashCode = -1;
_sharedHashCode = -1;
}
public ComponentSet(AllocationHandle allocationHandle, ReadOnlySpan<Identifier<IComponent>> components, ReadOnlySpan<byte> sharedData)
{
_components = new UnsafeArray<Identifier<IComponent>>(components.Length, allocationHandle);
components.CopyTo(_components.AsSpan());
_sharedData = new UnsafeArray<byte>(sharedData.Length, allocationHandle);
sharedData.CopyTo(_sharedData);
_hashCode = -1; _hashCode = -1;
_sharedHashCode = -1; _sharedHashCode = -1;
} }
public ComponentSet(AllocationHandle allocationHandle, ReadOnlySpan<Identifier<IComponent>> components, SharedComponentSet sharedComponentSet) public ComponentSet(AllocationHandle allocationHandle, ReadOnlySpan<Identifier<IComponent>> components, SharedComponentSet sharedComponentSet)
: this(allocationHandle, components, sharedComponentSet.AsSpan())
{ {
_components = new UnsafeArray<Identifier<IComponent>>(components.Length, allocationHandle);
components.CopyTo(_components.AsSpan());
_components.AsSpan().Sort(static (a, b) => a.Value.CompareTo(b.Value));
var sharedData = sharedComponentSet.AsSpan();
_sharedData = new UnsafeArray<byte>(sharedData.Length, allocationHandle);
sharedData.CopyTo(_sharedData);
_hashCode = -1;
_sharedHashCode = -1;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -445,7 +516,7 @@ public struct ComponentSet : IDisposable, IEquatable<ComponentSet>
public readonly bool Equals(ComponentSet other) public readonly bool Equals(ComponentSet other)
{ {
return _hashCode == other._hashCode && _sharedHashCode == other._sharedHashCode; return _hashCode == other._hashCode && _sharedHashCode == other._sharedHashCode && _components.AsSpan().SequenceEqual(other._components.AsSpan()) && _sharedData.AsSpan().SequenceEqual(other._sharedData.AsSpan());
} }
public override int GetHashCode() public override int GetHashCode()
@@ -520,6 +591,10 @@ public ref struct ComponentSetView : IEquatable<ComponentSetView>
} }
} }
/// <summary>
/// Creates a new instance of <see cref="ComponentSetView"/> with the specified component identifiers and no shared component data.
/// </summary>
/// <param name="components">The collection of component identifiers.</param>
public ComponentSetView(ReadOnlySpan<Identifier<IComponent>> components) public ComponentSetView(ReadOnlySpan<Identifier<IComponent>> components)
{ {
_components = components; _components = components;
@@ -527,6 +602,14 @@ public ref struct ComponentSetView : IEquatable<ComponentSetView>
_sharedHashCode = -1; _sharedHashCode = -1;
} }
/// <summary>
/// Create a new instance of <see cref="ComponentSetView"/> with the specified component identifiers and shared component data.
/// </summary>
/// <remarks>
/// This API does not sort the shared component data internally. Which means [A, B] is different from [B, A] during chunk grouping.
/// </remarks>
/// <param name="components">The collection of component identifiers.</param>
/// <param name="sharedData">The shared component data.</param>
public ComponentSetView(ReadOnlySpan<Identifier<IComponent>> components, ReadOnlySpan<byte> sharedData) public ComponentSetView(ReadOnlySpan<Identifier<IComponent>> components, ReadOnlySpan<byte> sharedData)
{ {
_components = components; _components = components;
@@ -535,6 +618,11 @@ public ref struct ComponentSetView : IEquatable<ComponentSetView>
_sharedHashCode = -1; _sharedHashCode = -1;
} }
/// <summary>
/// Creates a new instance of <see cref="ComponentSetView"/> with the specified component identifiers and shared component set.
/// </summary>
/// <param name="components">The collection of component identifiers.</param>
/// <param name="sharedComponentSet">The shared component set.</param>
public ComponentSetView(ReadOnlySpan<Identifier<IComponent>> components, SharedComponentSet sharedComponentSet) public ComponentSetView(ReadOnlySpan<Identifier<IComponent>> components, SharedComponentSet sharedComponentSet)
: this(components, sharedComponentSet.AsSpan()) : this(components, sharedComponentSet.AsSpan())
{ {
@@ -542,7 +630,7 @@ public ref struct ComponentSetView : IEquatable<ComponentSetView>
public readonly bool Equals(ComponentSetView other) public readonly bool Equals(ComponentSetView other)
{ {
return _hashCode == other._hashCode && _sharedHashCode == other._sharedHashCode; return _hashCode == other._hashCode && _sharedHashCode == other._sharedHashCode && _components.SequenceEqual(other._components) && _sharedData.SequenceEqual(other._sharedData);
} }
public override int GetHashCode() public override int GetHashCode()

View File

@@ -1,3 +1,5 @@
#if false
using Ghost.Core; using Ghost.Core;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
@@ -235,3 +237,5 @@ internal sealed unsafe class SharedComponentStore : IDisposable
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
} }
#endif

View File

@@ -59,6 +59,14 @@ public unsafe partial struct EntityQuery
private readonly VirtualStack.Scope _scope; private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs; private UnsafeList<int> _changedComponentIDs;
private int _reqCount;
private fixed int _reqOffsets[16];
private int _reqDisCount;
private fixed int _reqDisOffsets[16];
private int _rejCount;
private fixed int _rejOffsets[16];
private bool _requiresFiltering;
private ref Archetype _currentArchetype; private ref Archetype _currentArchetype;
private ref Chunk _currentChunk; private ref Chunk _currentChunk;
private byte* _chunkBasePtr; private byte* _chunkBasePtr;
@@ -102,13 +110,50 @@ public unsafe partial struct EntityQuery
<# if (i > 1) { #> <# if (i > 1) { #>
public QueryItem Current => new( public QueryItem Current => new(
<# for (var j = 0; j < i; j++) { #> <# for (var j = 0; j < i; j++) { #>
ref *(T<#= j #>*)(_compBasePtrs[<#= j #>] + _currentEntityIndex * sizeof(T<#= j #>))<#= j < i - 1 ? "," : "" #> ref (ComponentTypeID<T<#= j #>>.IsShared ? ref ((T<#= j #>*)_compBasePtrs[<#= j #>])[0] : ref ((T<#= j #>*)_compBasePtrs[<#= j #>])[_currentEntityIndex])<#= j < i - 1 ? "," : "" #>
<# } #> <# } #>
); );
<# } else { #> <# } else { #>
public ref T0 Current => ref *(T0*)(_compBasePtrs[0] + _currentEntityIndex * sizeof(T0)); public ref T0 Current => ref (ComponentTypeID<T0>.IsShared ? ref ((T0*)_compBasePtrs[0])[0] : ref ((T0*)_compBasePtrs[0])[_currentEntityIndex]);
<# } #> <# } #>
private void SetArchetype(int index)
{
_currentArchetypeIndex = index;
_currentArchetype = ref _world.ComponentManager.GetArchetypeReference(_matchingArchetypes[index]);
_requiresFiltering = RequiresEnableableFiltering(in _currentArchetype, in _mask);
if (_requiresFiltering)
{
_reqCount = 0;
var itE = _mask.requireEnabled.GetIterator();
while (itE.Next(out var id) && _reqCount < 16)
{
var layoutResult = _currentArchetype.GetLayout(id);
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
_reqOffsets[_reqCount++] = layoutResult.Value.enableBitsOffset;
}
_reqDisCount = 0;
itE = _mask.requireDisabled.GetIterator();
while (itE.Next(out var id) && _reqDisCount < 16)
{
var layoutResult = _currentArchetype.GetLayout(id);
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
_reqDisOffsets[_reqDisCount++] = layoutResult.Value.enableBitsOffset;
}
_rejCount = 0;
itE = _mask.rejectIfEnabled.GetIterator();
while (itE.Next(out var id) && _rejCount < 16)
{
var layoutResult = _currentArchetype.GetLayout(id);
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
_rejOffsets[_rejCount++] = layoutResult.Value.enableBitsOffset;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetChunk(int chunkIndex) private void SetChunk(int chunkIndex)
{ {
@@ -116,13 +161,21 @@ public unsafe partial struct EntityQuery
_chunkBasePtr = _currentChunk.GetUnsafePtr(); _chunkBasePtr = _currentChunk.GetUnsafePtr();
_currentChunkEntityCount = _currentChunk._count; _currentChunkEntityCount = _currentChunk._count;
for (var index = 0; index < <#= i #>; index++) <# for (var index = 0; index < i; index++) { #>
if (ComponentTypeID<T<#= index #>>.IsShared)
{ {
var layout = _currentArchetype.GetLayout(_compTypeIDs[index]) var layout = _currentArchetype.GetSharedLayout(_compTypeIDs[<#= index #>]).GetValueOrThrow();
.GetValueOrThrow(); _offsets[<#= index #>] = layout.offset;
_offsets[index] = layout.offset; var sharedSpan = _currentArchetype._chunkGroups[_currentChunk._groupIndex].sharedData.AsSpan();
_compBasePtrs[index] = (long)(_chunkBasePtr + _offsets[index]); _compBasePtrs[<#= index #>] = (long)Unsafe.AsPointer(ref System.Runtime.InteropServices.MemoryMarshal.GetReference(sharedSpan)) + _offsets[<#= index #>];
} }
else
{
var layout = _currentArchetype.GetLayout(_compTypeIDs[<#= index #>]).GetValueOrThrow();
_offsets[<#= index #>] = layout.offset;
_compBasePtrs[<#= index #>] = (long)(_chunkBasePtr + _offsets[<#= index #>]);
}
<# } #>
for (var i = 0; i < _changedComponentIDs.Count; i++) for (var i = 0; i < _changedComponentIDs.Count; i++)
{ {
@@ -138,12 +191,29 @@ public unsafe partial struct EntityQuery
if (_currentEntityIndex < _currentChunk._count) if (_currentEntityIndex < _currentChunk._count)
{ {
var pChunkData = _currentChunk.GetUnsafePtr(); var pChunkData = _currentChunk.GetUnsafePtr();
if (IsEntityValid(pChunkData, _currentEntityIndex, in _currentArchetype, in _mask)) if (!_requiresFiltering)
{ {
return true; return true;
} }
continue; var valid = true;
for (var h = 0; h < _reqCount; h++)
{
if (!CheckBit(pChunkData + _reqOffsets[h], _currentEntityIndex)) { valid = false; break; }
}
if (!valid) continue;
for (var h = 0; h < _reqDisCount; h++)
{
if (CheckBit(pChunkData + _reqDisOffsets[h], _currentEntityIndex)) { valid = false; break; }
}
if (!valid) continue;
for (var h = 0; h < _rejCount; h++)
{
if (CheckBit(pChunkData + _rejOffsets[h], _currentEntityIndex)) { valid = false; break; }
}
if (valid) return true;
} }
_currentChunkIndex++; _currentChunkIndex++;
@@ -158,7 +228,7 @@ public unsafe partial struct EntityQuery
_currentArchetypeIndex++; _currentArchetypeIndex++;
if (_currentArchetypeIndex < _matchingArchetypes.Count) if (_currentArchetypeIndex < _matchingArchetypes.Count)
{ {
_currentArchetype = ref _world.ComponentManager.GetArchetypeReference(_matchingArchetypes[_currentArchetypeIndex]); SetArchetype(_currentArchetypeIndex);
_currentChunkIndex = 0; _currentChunkIndex = 0;
if (_currentArchetype.ChunkCount > 0) if (_currentArchetype.ChunkCount > 0)
@@ -187,7 +257,7 @@ public unsafe partial struct EntityQuery
if (_matchingArchetypes.Count > 0) if (_matchingArchetypes.Count > 0)
{ {
_currentArchetype = ref _world.ComponentManager.GetArchetypeReference(_matchingArchetypes[0]); SetArchetype(0);
if (_currentArchetype.ChunkCount > 0) if (_currentArchetype.ChunkCount > 0)
{ {
SetChunk(0); SetChunk(0);

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,9 @@
<#@ import namespace="System.Linq" #> <#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #> <#@ import namespace="System.Text" #>
<#@ include file="Helpers.ttinclude" #> <#@ include file="Helpers.ttinclude" #>
using System.Runtime.CompilerServices;
using Ghost.Core;
namespace Ghost.Entities; namespace Ghost.Entities;
public unsafe partial struct EntityQuery public unsafe partial struct EntityQuery
@@ -42,12 +45,12 @@ public unsafe partial struct EntityQuery
var changedCompCount = 0; var changedCompCount = 0;
var it = _mask.writeAccess.GetIterator(); var writeIt = _mask.writeAccess.GetIterator();
while (it.Next(out var id)) while (writeIt.Next(out var id))
{ {
for (var i =0; i < <#= i #>; i++) for (var idx = 0; idx < <#= i #>; idx++)
{ {
if (id == compTypeIDs[i]) if (id == compTypeIDs[idx])
{ {
changedCompIDs[changedCompCount] = id; changedCompIDs[changedCompCount] = id;
changedCompCount++; changedCompCount++;
@@ -56,30 +59,72 @@ public unsafe partial struct EntityQuery
} }
} }
for (var i = 0; i < _matchingArchetypes.Count; i++) var reqOffsets = stackalloc int[16];
var reqDisOffsets = stackalloc int[16];
var rejOffsets = stackalloc int[16];
for (var archIndex = 0; archIndex < _matchingArchetypes.Count; archIndex++)
{ {
ref var archetype = ref world.ComponentManager.GetArchetypeReference(_matchingArchetypes[i]); ref var archetype = ref world.ComponentManager.GetArchetypeReference(_matchingArchetypes[archIndex]);
var hasAllComponents = true; var hasAllComponents = true;
for (var index = 0; index < <#= i #>; index++) <# for (var index = 0; index < i; index++) { #>
if (ComponentTypeID<T<#= index #>>.IsShared)
{ {
var layoutResult = archetype.GetLayout(compTypeIDs[index]); var layoutResult = archetype.GetSharedLayout(compTypeIDs[<#= index #>]);
if (!layoutResult) if (!layoutResult) { hasAllComponents = false; goto skipArchetype; }
offsets[<#= index #>] = layoutResult.Value.offset;
}
else
{ {
hasAllComponents = false; var layoutResult = archetype.GetLayout(compTypeIDs[<#= index #>]);
break; if (!layoutResult) { hasAllComponents = false; goto skipArchetype; }
offsets[<#= index #>] = layoutResult.Value.offset;
} }
<# } #>
offsets[index] = layoutResult.Value.offset; skipArchetype:
}
if (!hasAllComponents) if (!hasAllComponents)
{ {
continue; continue;
} }
var requiresFiltering = RequiresEnableableFiltering(in archetype, in _mask);
var reqCount = 0;
var reqDisCount = 0;
var rejCount = 0;
if (requiresFiltering)
{
var itE = _mask.requireEnabled.GetIterator();
while (itE.Next(out var id) && reqCount < 16)
{
var layoutResult = archetype.GetLayout(id);
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
reqOffsets[reqCount++] = layoutResult.Value.enableBitsOffset;
}
itE = _mask.requireDisabled.GetIterator();
while (itE.Next(out var id) && reqDisCount < 16)
{
var layoutResult = archetype.GetLayout(id);
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
reqDisOffsets[reqDisCount++] = layoutResult.Value.enableBitsOffset;
}
itE = _mask.rejectIfEnabled.GetIterator();
while (itE.Next(out var id) && rejCount < 16)
{
var layoutResult = archetype.GetLayout(id);
if (layoutResult.Error == Error.None && layoutResult.Value.enableBitsOffset != -1)
rejOffsets[rejCount++] = layoutResult.Value.enableBitsOffset;
}
}
for (var chunkIndex = 0; chunkIndex < archetype.ChunkCount; chunkIndex++) for (var chunkIndex = 0; chunkIndex < archetype.ChunkCount; chunkIndex++)
{ {
ref var chunk = ref archetype.GetChunkReference(chunkIndex); ref var chunk = ref archetype.GetChunkReference(chunkIndex);
if (chunk._count == 0) continue;
var pChunkData = chunk.GetUnsafePtr(); var pChunkData = chunk.GetUnsafePtr();
for (var j = 0; j < changedCompCount; j++) for (var j = 0; j < changedCompCount; j++)
@@ -87,28 +132,70 @@ public unsafe partial struct EntityQuery
archetype.MarkChanged(chunkIndex, changedCompIDs[j], globalVersion); archetype.MarkChanged(chunkIndex, changedCompIDs[j], globalVersion);
} }
for (var index = 0; index < <#= i #>; index++) <# for (var index = 0; index < i; index++) { #>
if (ComponentTypeID<T<#= index #>>.IsShared)
{ {
basePtrs[index] = pChunkData + offsets[index]; var sharedSpan = archetype._chunkGroups[chunk._groupIndex].sharedData.AsSpan();
basePtrs[<#= index #>] = (byte*)Unsafe.AsPointer(ref System.Runtime.InteropServices.MemoryMarshal.GetReference(sharedSpan)) + offsets[<#= index #>];
} }
else
{
basePtrs[<#= index #>] = pChunkData + offsets[<#= index #>];
}
<# } #>
if (!requiresFiltering)
{
for (var entityIndex = 0; entityIndex < chunk._count; entityIndex++) for (var entityIndex = 0; entityIndex < chunk._count; entityIndex++)
{ {
if (!IsEntityValid(pChunkData, entityIndex, in archetype, in _mask)) <# if (isForEachWithEntity) { #>
var pEntity = (Entity*)(pChunkData + archetype.EntityIDsOffset + (sizeof(Entity) * entityIndex));
<# } #>
action(<# if (isForEachWithEntity) { #>*pEntity, <# } #>
<# for (var localIndex = 0; localIndex < i; localIndex++) { #>
ref (ComponentTypeID<T<#= localIndex #>>.IsShared ? ref ((T<#= localIndex #>*)basePtrs[<#= localIndex #>])[0] : ref ((T<#= localIndex #>*)basePtrs[<#= localIndex #>])[entityIndex])<#= localIndex < i - 1 ? "," : "" #>
<# } #>);
}
}
else
{ {
continue; var ulongCount = (chunk._count + 63) / 64;
for (var block = 0; block < ulongCount; block++)
{
var validMask = ulong.MaxValue;
var remaining = chunk._count - (block * 64);
if (remaining < 64) validMask = (1UL << remaining) - 1UL;
for (var h = 0; h < reqCount; h++)
{
validMask &= ((ulong*)(pChunkData + reqOffsets[h]))[block];
} }
<# for (var localIndex = 0; localIndex < i; localIndex++) { #> for (var h = 0; h < reqDisCount; h++)
var pComp<#= localIndex #> = (T<#= localIndex #>*)(basePtrs[<#= localIndex #>] + (sizeof(T<#= localIndex #>) * entityIndex)); {
<# } #> validMask &= ~((ulong*)(pChunkData + reqDisOffsets[h]))[block];
}
for (var h = 0; h < rejCount; h++)
{
validMask &= ~((ulong*)(pChunkData + rejOffsets[h]))[block];
}
while (validMask != 0)
{
var bit = System.Numerics.BitOperations.TrailingZeroCount(validMask);
var entityIndex = (block * 64) + bit;
<# if (isForEachWithEntity) { #> <# if (isForEachWithEntity) { #>
var pEntity = (Entity*)(pChunkData + archetype.EntityIDsOffset + (sizeof(Entity) * entityIndex)); var pEntity = (Entity*)(pChunkData + archetype.EntityIDsOffset + (sizeof(Entity) * entityIndex));
action(*pEntity, <#= AppendParameters(i, "ref *pComp{0}") #>);
<# } else { #>
action(<#= AppendParameters(i, "ref *pComp{0}") #>);
<# } #> <# } #>
action(<# if (isForEachWithEntity) { #>*pEntity, <# } #>
<# for (var localIndex = 0; localIndex < i; localIndex++) { #>
ref (ComponentTypeID<T<#= localIndex #>>.IsShared ? ref ((T<#= localIndex #>*)basePtrs[<#= localIndex #>])[0] : ref ((T<#= localIndex #>*)basePtrs[<#= localIndex #>])[entityIndex])<#= localIndex < i - 1 ? "," : "" #>
<# } #>);
validMask ^= (1UL << bit);
}
}
} }
} }
} }

View File

@@ -7,6 +7,8 @@
using Ghost.Core; using Ghost.Core;
using Misaki.HighPerformance.Jobs; using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
namespace Ghost.Entities; namespace Ghost.Entities;
@@ -21,6 +23,24 @@ public interface IJobEntity<<#= generics #>>
void Execute(Entity entity, <#= AppendParameters(i, "ref T{0} component{0}") #>, ref readonly JobExecutionContext ctx); void Execute(Entity entity, <#= AppendParameters(i, "ref T{0} component{0}") #>, ref readonly JobExecutionContext ctx);
} }
internal unsafe struct JobBatchContext<#= i #>
{
public byte* chunk;
public uint* chunkVersions;
public byte* sharedDataBlob;
public int chunkCount;
public int entityOffset;
<# for (var j = 0; j < i; j++){ #>
public int offset<#= j #>;
public int enableOff<#= j #>;
public int versionIndex<#= j #>;
<# } #>
public int hiddenEnableCount;
public fixed int hiddenEnableOffsets[16];
}
internal unsafe struct JobEntityBatch<TJob, <#= generics #>> : IJobParallelFor internal unsafe struct JobEntityBatch<TJob, <#= generics #>> : IJobParallelFor
where TJob : unmanaged, IJobEntity<<#= generics #>> where TJob : unmanaged, IJobEntity<<#= generics #>>
<#= restrictions #> <#= restrictions #>
@@ -29,39 +49,31 @@ internal unsafe struct JobEntityBatch<TJob, <#= generics #>> : IJobParallelFor
public fixed bool componentRW[<#= i #>]; public fixed bool componentRW[<#= i #>];
public TJob userJob; public TJob userJob;
public UnsafeList<JobBatchContext<#= i #>> batches;
public UnsafeList<IntPtr> chunks; public EntityQueryMask mask;
public UnsafeList<IntPtr> chunkVersions;
public UnsafeList<int> chunkCount;
public UnsafeList<int> entityOffset;
<# for (var j = 0; j < i; j++){ #>
public UnsafeList<int> offsets<#= j #>;
public UnsafeList<int> bitsOffsets<#= j #>;
public UnsafeList<int> versionIndices<#= j #>;
<# } #>
public uint version; public uint version;
public void Execute(int loopIndex, ref readonly JobExecutionContext ctx) public void Execute(int loopIndex, ref readonly JobExecutionContext ctx)
{ {
// 1. Get the specific pChunk for this thread ref var batch = ref ((JobBatchContext<#= i #>*)batches.GetUnsafePtr())[loopIndex];
var pChunk = (byte*)chunks[loopIndex];
var pVersions = (uint*)chunkVersions[loopIndex]; var pChunk = batch.chunk;
var count = chunkCount[loopIndex]; var pVersions = batch.chunkVersions;
var pSharedBlob = batch.sharedDataBlob;
var count = batch.chunkCount;
<# for (var j = 0; j < i; j++){ #> <# for (var j = 0; j < i; j++){ #>
var off<#= j #> = offsets<#= j #>[loopIndex]; var off<#= j #> = batch.offset<#= j #>;
var enableOff<#= j #> = bitsOffsets<#= j #>[loopIndex]; var enableOff<#= j #> = batch.enableOff<#= j #>;
var versionIndex<#= j #> = versionIndices<#= j #>[loopIndex]; var versionIndex<#= j #> = batch.versionIndex<#= j #>;
<# } #> <# } #>
var pEntity = (Entity*)(pChunk + entityOffset[loopIndex]);
var pEntity = (Entity*)(pChunk + batch.entityOffset);
<# for (var j = 0; j < i; j++){ #> <# for (var j = 0; j < i; j++){ #>
var ptr<#= j #> = (<#= "T" + j #>*)(pChunk + off<#= j #>); var ptr<#= j #> = (T<#= j #>*)(ComponentTypeID<T<#= j #>>.IsShared ? (pSharedBlob + off<#= j #>) : (pChunk + off<#= j #>));
<# } #> <# } #>
// 2. Update versions for RW components
<# for (var j = 0; j < i; j++){ #> <# for (var j = 0; j < i; j++){ #>
if (componentRW[<#= j #>]) if (componentRW[<#= j #>])
{ {
@@ -69,17 +81,43 @@ internal unsafe struct JobEntityBatch<TJob, <#= generics #>> : IJobParallelFor
} }
<# } #> <# } #>
// 3. Iterate all entities in this chunk
for (var i = 0; i < count; i++) // Execute batch
var ulongCount = (count + 63) / 64;
for (var block = 0; block < ulongCount; block++)
{ {
var validMask = ulong.MaxValue;
var remaining = count - (block * 64);
if (remaining < 64) validMask = (1UL << remaining) - 1UL;
// Enforce enableable bits checking based on components required by Job Signature
<# for (var j = 0; j < i; j++){ #> <# for (var j = 0; j < i; j++){ #>
if (enableOff<#= j #> != -1 && !EntityQuery.CheckBit(pChunk + enableOff<#= j #>, i)) if (enableOff<#= j #> != -1)
{ {
continue; var pMask = (ulong*)(pChunk + enableOff<#= j #>);
validMask &= pMask[block];
}
<# } #>
// Enforce EntityQuery Mask hidden enableable constraints
for (var h = 0; h < batch.hiddenEnableCount; h++)
{
var pMask = (ulong*)(pChunk + batch.hiddenEnableOffsets[h]);
validMask &= pMask[block];
} }
<# } #> while (validMask != 0)
userJob.Execute(pEntity[i], <#= AppendParameters(i, "ref ptr{0}[i]") #>, in ctx); {
var bit = System.Numerics.BitOperations.TrailingZeroCount(validMask);
var i_ent = (block * 64) + bit;
userJob.Execute(pEntity[i_ent],
<# for (var j = 0; j < i; j++){ #>
ref (ComponentTypeID<T<#= j #>>.IsShared ? ref ptr<#= j #>[0] : ref ptr<#= j #>[i_ent])<#= j < i - 1 ? "," : "" #>
<# } #>, in ctx);
validMask ^= (1UL << bit);
}
} }
} }
} }
@@ -94,30 +132,11 @@ public unsafe partial struct EntityQuery
#> #>
private struct DisposeJobEntity<#= i #> : IJob private struct DisposeJobEntity<#= i #> : IJob
{ {
public UnsafeList<IntPtr> chunks; public UnsafeList<JobBatchContext<#= i #>> batches;
public UnsafeList<IntPtr> chunkVersions;
public UnsafeList<int> chunkEntityCounts;
public UnsafeList<int> entityOffsets;
<# for (var j = 0; j < i; j++){ #>
public UnsafeList<int> offsets<#= j #>;
public UnsafeList<int> bitsOffsets<#= j #>;
public UnsafeList<int> versionIndices<#= j #>;
<# } #>
public void Execute(ref readonly JobExecutionContext ctx) public void Execute(ref readonly JobExecutionContext ctx)
{ {
chunks.Dispose(); batches.Dispose();
chunkVersions.Dispose();
chunkEntityCounts.Dispose();
entityOffsets.Dispose();
<# for (var j = 0; j < i; j++){ #>
offsets<#= j #>.Dispose();
bitsOffsets<#= j #>.Dispose();
versionIndices<#= j #>.Dispose();
<# } #>
} }
} }
@@ -136,19 +155,9 @@ public unsafe partial struct EntityQuery
throw new InvalidOperationException("The World has no JobScheduler assigned."); throw new InvalidOperationException("The World has no JobScheduler assigned.");
} }
// 1. Flatten the World var batches = new UnsafeList<JobBatchContext<#= i #>>(128, TempJobAllocator.AllocationHandle);
var chunks = new UnsafeList<IntPtr>(128, TempJobAllocator.AllocationHandle); var hiddenOffsets = stackalloc int[16];
var chunkVersions = new UnsafeList<IntPtr>(128, TempJobAllocator.AllocationHandle);
var chunkEntityCounts = new UnsafeList<int>(128, TempJobAllocator.AllocationHandle);
var entityOffsets = new UnsafeList<int>(128, TempJobAllocator.AllocationHandle);
<# for (var j = 0; j < i; j++){ #>
var offsets<#= j #> = new UnsafeList<int>(128, TempJobAllocator.AllocationHandle);
var bitsOffsets<#= j #> = new UnsafeList<int>(128, TempJobAllocator.AllocationHandle);
var versionIndices<#= j #> = new UnsafeList<int>(128, TempJobAllocator.AllocationHandle);
<# } #>
// Iterate the Query's matching archetypes
foreach (var archID in _matchingArchetypes) foreach (var archID in _matchingArchetypes)
{ {
ref var arch = ref world.ComponentManager.GetArchetypeReference(archID); ref var arch = ref world.ComponentManager.GetArchetypeReference(archID);
@@ -158,79 +167,112 @@ public unsafe partial struct EntityQuery
continue; continue;
} }
// Get offsets ONCE per archetype
<# for (var j = 0; j < i; j++){ #> <# for (var j = 0; j < i; j++){ #>
var layout<#= j #> = arch.GetLayout(ComponentTypeID<T<#= j #>>.Value).GetValueOrThrow(); int off<#= j #>;
<# } #> int enableOff<#= j #>;
int versionIdx<#= j #>;
// Add all chunks from this archetype if (ComponentTypeID<T<#= j #>>.IsShared)
for (var i = 0; i < arch.ChunkCount; i++)
{ {
ref var chunkRef = ref arch.GetChunkReference(i); var layout = arch.GetSharedLayout(ComponentTypeID<T<#= j #>>.Value).GetValueOrThrow();
off<#= j #> = layout.offset;
enableOff<#= j #> = -1;
versionIdx<#= j #> = -1;
}
else
{
var layout = arch.GetLayout(ComponentTypeID<T<#= j #>>.Value).GetValueOrThrow();
off<#= j #> = layout.offset;
enableOff<#= j #> = layout.enableBitsOffset;
versionIdx<#= j #> = layout.versionIndex;
}
<# } #>
chunks.Add((IntPtr)chunkRef.GetUnsafePtr()); var hiddenCount = 0;
chunkVersions.Add((IntPtr)chunkRef.GetVersionUnsafePtr()); var itE = _mask.requireEnabled.GetIterator();
chunkEntityCounts.Add(chunkRef._count); while (itE.Next(out var id) && hiddenCount < 16)
entityOffsets.Add(arch.EntityIDsOffset); {
var found = false;
<# for (var j = 0; j < i; j++){ #>
if (id == ComponentTypeID<T<#= j #>>.Value) found = true;
<# } #>
if (!found)
{
var layout = arch.GetLayout(id);
if (layout.Error == Error.None && layout.Value.enableBitsOffset != -1)
{
hiddenOffsets[hiddenCount++] = layout.Value.enableBitsOffset;
}
}
}
for (var chunkIdx = 0; chunkIdx < arch.ChunkCount; chunkIdx++)
{
ref var chunkRef = ref arch.GetChunkReference(chunkIdx);
byte* pSharedBlob = null;
if (arch._chunkGroups.Count > 0 && chunkRef._groupIndex >= 0 && chunkRef._groupIndex < arch._chunkGroups.Count)
{
var sharedSpan = arch._chunkGroups[chunkRef._groupIndex].sharedData;
if (sharedSpan.IsCreated)
{
pSharedBlob = (byte*)sharedSpan.GetUnsafePtr();
}
}
var ctx = new JobBatchContext<#= i #>
{
chunk = chunkRef.GetUnsafePtr(),
chunkVersions = chunkRef.GetVersionUnsafePtr(),
chunkCount = chunkRef._count,
entityOffset = arch.EntityIDsOffset,
sharedDataBlob = pSharedBlob,
<# for (var j = 0; j < i; j++){ #> <# for (var j = 0; j < i; j++){ #>
offsets<#= j #>.Add(layout<#= j #>.offset); offset<#= j #> = off<#= j #>,
bitsOffsets<#= j #>.Add(layout<#= j #>.enableBitsOffset); enableOff<#= j #> = enableOff<#= j #>,
versionIndices<#= j #>.Add(layout<#= j #>.versionIndex); versionIndex<#= j #> = versionIdx<#= j #>,
<# } #> <# } #>
hiddenEnableCount = hiddenCount,
};
for (var h = 0; h < hiddenCount; h++)
{
ctx.hiddenEnableOffsets[h] = hiddenOffsets[h];
}
batches.Add(ctx);
} }
} }
// 2. Create the Runner
var runner = new JobEntityBatch<TJob, <#= generics #>> var runner = new JobEntityBatch<TJob, <#= generics #>>
{ {
userJob = jobData, userJob = jobData,
chunks = chunks, batches = batches,
chunkVersions = chunkVersions, mask = _mask,
chunkCount = chunkEntityCounts,
entityOffset = entityOffsets,
<# for (var j = 0; j < i; j++){ #>
offsets<#= j #> = offsets<#= j #>,
bitsOffsets<#= j #> = bitsOffsets<#= j #>,
versionIndices<#= j #> = versionIndices<#= j #>,
<# } #>
version = world.Version, version = world.Version,
}; };
runner.componentIDs[0] = ComponentTypeID<T0>.Value; <# for (var j = 0; j < i; j++){ #>
runner.componentIDs[<#= j #>] = ComponentTypeID<T<#= j #>>.Value;
<# } #>
var it = _mask.writeAccess.GetIterator(); var it = _mask.writeAccess.GetIterator();
while (it.Next(out var id)) while (it.Next(out var id))
{ {
for (var i =0; i < 1; i++) for (var idx = 0; idx < <#= i #>; idx++)
{ {
if (id == runner.componentIDs[i]) if (id == runner.componentIDs[idx])
{ {
runner.componentRW[i] = true; runner.componentRW[idx] = true;
break; break;
} }
} }
} }
var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, chunks.Count, batchSize, dependency); var jobHandle = world.JobScheduler.ScheduleParallelFor(ref runner, batches.Count, batchSize, dependency);
// 3. Dispose the temp lists
var disposeJob = new DisposeJobEntity<#= i #> var disposeJob = new DisposeJobEntity<#= i #>
{ {
chunks = chunks, batches = batches,
chunkVersions = chunkVersions,
chunkEntityCounts = chunkEntityCounts,
entityOffsets = entityOffsets,
<# for (var j = 0; j < i; j++){ #>
offsets<#= j #> = offsets<#= j #>,
bitsOffsets<#= j #> = bitsOffsets<#= j #>,
versionIndices<#= j #> = versionIndices<#= j #>,
<# } #>
}; };
world.JobScheduler.Schedule(ref disposeJob, jobHandle); world.JobScheduler.Schedule(ref disposeJob, jobHandle);

View File

@@ -153,10 +153,6 @@ public partial class World : IDisposable, IEquatable<World>
} }
} }
~World()
{
Dispose();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void PlaybackEntityCommandBuffers() internal void PlaybackEntityCommandBuffers()
@@ -291,10 +287,13 @@ public partial class World : IDisposable, IEquatable<World>
_entityManager.Dispose(); _entityManager.Dispose();
_entityCommandBuffer.Dispose(); _entityCommandBuffer.Dispose();
if (_threadLocalECBs.IsCreated)
{
for (var i = 0; i < _threadLocalECBs.Length; i++) for (var i = 0; i < _threadLocalECBs.Length; i++)
{ {
_threadLocalECBs[i].Dispose(); _threadLocalECBs[i].Dispose();
} }
}
_componentManager.Dispose(); _componentManager.Dispose();
_systemManager.Dispose(); _systemManager.Dispose();

View File

@@ -408,23 +408,6 @@ public class RenderSystem : IDisposable
_resizeRequest.AddOrUpdate(swapChain, newSize, (_, _) => newSize); _resizeRequest.AddOrUpdate(swapChain, newSize, (_, _) => newSize);
} }
internal bool TryAcquireCPUFrame()
{
Logger.DebugAssert(!_disposed, "Cannot acquire CPU frame on a disposed RenderSystem.");
var requiredGpuFence = _cpuFenceValue < (ulong)_frameResources.Length ? 0 : _cpuFenceValue - (ulong)_frameResources.Length + 1;
if (requiredGpuFence > 0 && _fence.CompletedValue < requiredGpuFence)
{
return false;
}
var eventIndex = (int)(_cpuFenceValue % (ulong)_frameResources.Length);
ref var frameResource = ref _frameResources[eventIndex];
return true;
}
public bool WaitForGPUReady(int timeOut = -1) public bool WaitForGPUReady(int timeOut = -1)
{ {
Logger.DebugAssert(!_disposed, "Cannot wait for GPU ready on a disposed RenderSystem."); Logger.DebugAssert(!_disposed, "Cannot wait for GPU ready on a disposed RenderSystem.");

View File

@@ -1,115 +0,0 @@
using Ghost.TestCore;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.Mathematics;
namespace Ghost.Entities.Test;
internal struct TestEntityQueryJob : IJobEntity<Transform>
{
public readonly void Execute(Entity entity, ref Transform transform, ref readonly JobExecutionContext ctx)
{
transform.position += new float3(5, 5, 5);
}
}
internal struct TestChunkQueryJob : IJobChunk
{
public readonly void Execute(ChunkView view, ref readonly JobExecutionContext ctx)
{
var random = new random((uint)ctx.ThreadIndex + 1u);
var transforms = view.GetComponentDataRW<Transform>();
for (var i = 0; i < view.EntityCount; i++)
{
transforms[i].position += random.NextFloat3();
}
}
}
public partial class EntityQueryTest : ITest
{
private JobScheduler _jobScheduler = null!;
private World _world = null!;
public void Setup()
{
_jobScheduler = new JobScheduler(4);
_world = World.Create(_jobScheduler);
}
public void Run()
{
var entities = (Span<Entity>)stackalloc Entity[1000];
using var scope = AllocationManager.CreateStackScope();
using var set = new ComponentSet(scope.AllocationHandle, ComponentTypeID<Transform>.Value);
_world.EntityManager.CreateEntities(entities, set);
var queryID = new QueryBuilder().WithAllRW<Transform>().Build(_world);
ref var query = ref _world.ComponentManager.GetEntityQueryReference(queryID);
_world.AdvanceVersion();
var testJob = new TestChunkQueryJob();
var handle = query.ScheduleChunkParallel(testJob, 1, JobHandle.Invalid);
_jobScheduler.Wait(handle);
query.ForEach<Transform>((e, ref t) =>
{
Console.WriteLine($"Entity {e} Has Position: {t.position}");
});
foreach (var (entity, transform) in query.GetEntityComponentIterator<Transform>())
{
Console.WriteLine($"Entity {entity} Updated Position: {transform.Get().position}");
}
foreach (var chunk in query.GetChunkIterator())
{
var transforms = chunk.GetComponentData<Transform>();
var chunkEntities = chunk.GetEntities();
// if (chunk.HasChanged<Transform>(0))
{
// var bits = chunk.GetEnableBits<Transform>();
// var it = bits.GetIterator();
// while (it.Next(out var index) && index < chunk.EntityCount)
for (var index = 0; index < chunk.EntityCount; index++)
{
Console.WriteLine($"Entity {chunkEntities[index]} Updated Position: {transforms[index].position}");
}
}
}
_world.EntityManager.DestroyEntities(entities);
}
public void Cleanup()
{
_world.Dispose();
_jobScheduler.Dispose();
}
}
public struct Transform : IEnableableComponent
{
public float3 position;
public override string ToString()
{
return $"Position: {position}";
}
}
public struct Mesh : IComponent
{
public int index;
public override string ToString()
{
return $"Index: {index}";
}
}

View File

@@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.15.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Entities\Ghost.Entities.csproj" />
<ProjectReference Include="..\..\Test\Ghost.TestCore\Ghost.TestCore.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,12 +0,0 @@
using BenchmarkDotNet.Running;
using Ghost.Entities.Test;
//AllocationManager.EnableDebugLayer();
//TestRunner.Run<SerializationTest>();
//AllocationManager.Dispose();
BenchmarkRunner.Run<QueryBenchmark>();
//var test = new QueryBenchmark();
//test.Setup();
//test.QueryEntities();
//test.Cleanup();

View File

@@ -1,81 +0,0 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using Ghost.Core;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Entities.Test;
internal class GameObject
{
public Vector4 Position { get; set; }
}
internal struct Position : IComponent
{
public Vector4 value;
}
[HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.LlcReference, HardwareCounter.InstructionRetired)]
public class QueryBenchmark
{
private World _world = null!;
private Identifier<EntityQuery> _queryIdentifier;
private GameObject[] _gameObjects = null!;
private float _dt = Random.Shared.NextSingle();
[GlobalSetup]
public void Setup()
{
_world = World.Create(entityCapacity: 1_000_000);
_gameObjects = new GameObject[1_000_000];
using var scope = AllocationManager.CreateStackScope();
var componentSet = new ComponentSet(scope.AllocationHandle, ComponentTypeID<Position>.Value);
_world.EntityManager.CreateEntities(1_000_000, componentSet);
_queryIdentifier = new QueryBuilder().WithAllRW<Position>().Build(_world);
for (var i = 0; i < 1_000_000; i++)
{
_gameObjects[i] = new GameObject { Position = new Vector4(i, i, i, 0) };
}
}
[GlobalCleanup]
public void Cleanup()
{
_world.Dispose();
}
[Benchmark]
public void QueryGameObjects()
{
for (var i = 0; i < _gameObjects.Length; i++)
{
_gameObjects[i].Position += new Vector4(_dt, _dt, _dt, 0);
}
}
// Results: 620 us, 98.5% cache hits (14731 misses), 13260812 instructions retired, 4x faster than QueryGameObjects
[Benchmark(Baseline = true)]
public void QueryEntities()
{
ref var query = ref _world.ComponentManager.GetEntityQueryReference(_queryIdentifier);
foreach (var chunkView in query.GetChunkIterator())
{
var positions = chunkView.GetComponentDataRW<Position>();
ref var address = ref MemoryMarshal.GetReference(positions);
for (var i = 0; i < positions.Length; i++)
{
Unsafe.Add(ref address, i).value += new Vector4(_dt, _dt, _dt, 0);
}
}
}
}

View File

@@ -1,140 +0,0 @@
using Ghost.TestCore;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.Mathematics;
using System.Runtime.InteropServices;
using System.Text.Json;
namespace Ghost.Entities.Test;
public class SerializationTest : ITest
{
private World _world = null!;
public void Setup()
{
_world = World.Create();
}
public unsafe void Run()
{
using var scope = AllocationManager.CreateStackScope();
var set1 = new ComponentSet(scope.AllocationHandle, ComponentTypeID<Transform>.Value);
var set2 = new ComponentSet(scope.AllocationHandle, ComponentTypeID<Transform>.Value, ComponentTypeID<Mesh>.Value);
var e1 = _world.EntityManager.CreateEntity(set1);
var e2 = _world.EntityManager.CreateEntity(set2);
_world.EntityManager.SetComponent(e1, new Transform { position = new float3(1, 2, 3) });
_world.EntityManager.SetComponent(e2, new Transform { position = new float3(4, 5, 6) });
_world.EntityManager.SetComponent(e2, new Mesh { index = 42 });
using var stream = new MemoryStream();
var serializeOptions = new JsonSerializerOptions
{
IncludeFields = true
};
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });
writer.WriteStartObject();
writer.WriteString("Name", "world 1");
writer.WriteStartArray("Entities");
for (var i = 0; i < _world.ComponentManager.ArchetypeCount; i++)
{
ref var archetype = ref _world.ComponentManager.GetArchetypeReference(i);
for (var j = 0; j < archetype.ChunkCount; j++)
{
ref var chunk = ref archetype.GetChunkReference(j);
for (var k = 0; k < chunk._count; k++)
{
writer.WriteStartObject();
var entity = *(Entity*)(chunk.GetUnsafePtr() + archetype.EntityIDsOffset + k * sizeof(Entity));
writer.WriteNumber("ID", entity.ID);
writer.WriteStartArray("Components");
foreach (var layout in archetype._layouts)
{
var type = ComponentRegistry.s_runtimeIDToType[layout.componentID];
var size = ComponentRegistry.GetComponentInfo(layout.componentID).size;
if (type.AssemblyQualifiedName == null)
{
continue;
}
writer.WriteStartObject();
writer.WriteString("Type", type.AssemblyQualifiedName);
writer.WritePropertyName("Data");
var pComponentData = chunk.GetUnsafePtr() + layout.offset + (k * size);
var instace = Marshal.PtrToStructure((nint)pComponentData, type);
JsonSerializer.Serialize(writer, instace, type, serializeOptions);
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.WriteEndObject();
}
}
}
writer.WriteEndArray();
writer.WriteEndObject();
writer.Flush();
var data = stream.ToArray();
var json = System.Text.Encoding.UTF8.GetString(data);
Console.WriteLine(json);
var reader = new Utf8JsonReader(data);
var root = JsonDocument.ParseValue(ref reader).RootElement;
var name = root.GetProperty("Name").GetString();
Console.WriteLine($"Deserialized World Name: {name}");
var entityData = new List<(int EntityID, Type ComponentType, object Instance)>();
foreach (var entityElement in root.GetProperty("Entities").EnumerateArray())
{
var id = entityElement.GetProperty("ID").GetInt32();
// Access the new "Components" array
var componentsElement = entityElement.GetProperty("Components");
foreach (var componentElement in componentsElement.EnumerateArray())
{
var typeName = componentElement.GetProperty("Type").GetString();
var dataElement = componentElement.GetProperty("Data");
var type = Type.GetType(typeName!);
if (type == null)
{
continue;
}
var instance = dataElement.Deserialize(type, serializeOptions);
if (instance != null)
{
entityData.Add((id, type, instance));
}
}
}
foreach (var (id, type, instance) in entityData)
{
Console.WriteLine($"Entity ID: {id}, Component: {type.Name}, Data: {instance}");
}
}
public void Cleanup()
{
_world.Dispose();
}
}

View File

@@ -1,46 +0,0 @@
using Ghost.TestCore;
namespace Ghost.Entities.Test;
internal class SystemTest : ITest
{
private World _world = null!;
public void Setup()
{
_world = World.Create();
}
public void Run()
{
var group = _world.SystemManager.GetSystem<DefaultSystemGroup>();
group.AddSystem<TestSystemB>();
group.AddSystem<TestSystemA>();
group.SortSystems();
_world.SystemManager.InitializeAll(new TimeData());
}
public void Cleanup()
{
_world.Dispose();
}
}
internal class TestSystemA : SystemBase
{
protected override void OnInitialize(ref readonly SystemAPI systemAPI)
{
Console.WriteLine("TestSystemA Initialized");
}
}
[UpdateAfter<TestSystemA>]
internal class TestSystemB : SystemBase
{
protected override void OnInitialize(ref readonly SystemAPI systemAPI)
{
Console.WriteLine("TestSystemB Initialized");
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Ghost.Graphics.Test.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.Graphics.Test.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>

View File

@@ -1,157 +0,0 @@
using Ghost.Core;
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.Graphics.Test.Controls;
public sealed partial class DebugConsole : UserControl
{
private readonly ObservableCollection<LogMessage> _filteredLogs = [];
public DebugConsole()
{
InitializeComponent();
LogItemsRepeater.ItemsSource = _filteredLogs;
Logger.Impl.OnLogAdded += OnLogAdded;
Logger.Impl.OnLogsCleared += OnLogCleared;
// 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(LogMessage message)
{
if (ShouldShowLogItem(message))
{
DispatcherQueue.TryEnqueue(() =>
{
_filteredLogs.Add(message);
if (AutoScrollCheckBox.IsChecked == true)
{
LogScrollViewer.ScrollToVerticalOffset(LogScrollViewer.ScrollableHeight);
}
});
}
}
private void OnLogCleared()
{
DispatcherQueue.TryEnqueue(_filteredLogs.Clear);
}
private void OnFilterChanged(object sender, RoutedEventArgs e)
{
RefreshLogs();
}
private bool ShouldShowLogItem(LogMessage message)
{
return message.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 Logger.Logs)
{
if (ShouldShowLogItem(log))
{
_filteredLogs.Add(log);
}
}
if (AutoScrollCheckBox.IsChecked == true)
{
LogScrollViewer.ScrollToVerticalOffset(LogScrollViewer.ScrollableHeight);
}
}
private void ClearButton_Click(object sender, RoutedEventArgs e)
{
Logger.Impl.Clear();
}
private void ShowStackTraceCheckBox_Checked(object sender, RoutedEventArgs e)
{
Logger.Impl.CaptureStackTrace = true;
}
private void ShowStackTraceCheckBox_Unchecked(object sender, RoutedEventArgs e)
{
Logger.Impl.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();
}
}

View File

@@ -1,96 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Ghost.Graphics.Test</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>
<Configurations>Debug;Release;Debug_Editor;Release_Editor</Configurations>
</PropertyGroup>
<ItemGroup>
<Page Remove="UnitTestApp.xaml" />
<ApplicationDefinition Include="UnitTestApp.xaml" />
<ProjectCapability Include="TestContainer" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.TestPlatform.TestHost" Version="18.5.1" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.1.3" />
<PackageReference Include="MSTest.TestAdapter" Version="4.2.3" />
<PackageReference Include="MSTest.TestFramework" Version="4.2.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" />
<ProjectReference Include="..\..\Test\Ghost.TestCore\Ghost.TestCore.csproj" />
<ProjectReference Include="..\..\Runtime\Ghost.Graphics\Ghost.Graphics.csproj" />
<ProjectReference Include="..\..\Editor\Ghost.DSL\Ghost.DSL.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.Ufbx\Ghost.Ufbx.csproj" />
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
Explorer "Package and Publish" context menu entry to be enabled for this project even if
the Windows App SDK Nuget package has not yet been restored.
-->
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)'=='Debug_Editor'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|x86'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|x64'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug_Editor|ARM64'">
<DebugType>embedded</DebugType>
</PropertyGroup>
</Project>

View File

@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="7329af59-6d61-48e9-9041-8f2d3d23696b"
Publisher="CN=Misaki"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="7329af59-6d61-48e9-9041-8f2d3d23696b" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>Ghost.UnitTest</DisplayName>
<PublisherDisplayName>Misaki</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="Ghost.Graphics.Test"
Description="Ghost.UnitTest"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -1,11 +0,0 @@
{
"profiles": {
"Ghost.Graphics.Test (Package)": {
"commandName": "MsixPackage",
"nativeDebugging": true
},
"Ghost.Graphics.Test (Unpackaged)": {
"commandName": "Project"
}
}
}

View File

@@ -1,99 +0,0 @@
#include "F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/Shaders/Includes/Properties.hlsl"
#include "F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/Shaders/Includes/Common.hlsl"
struct PixelInput
{
float4 position : SV_POSITION;
float4 color : COLOR;
float4 uv : TEXCOORD0;
};
struct Meshlet
{
float4 boundingSphere;
float3 boundingBoxMin;
float3 boundingBoxMax;
uint vertexOffset;
uint triangleOffset;
uint groupIndex;
float parentError;
uint packedCounts; // byte vertexCount, byte triangleCount, byte localMaterialIndex, byte lodLevel
};
[numthreads(64, 1, 1)] // 64 threads for max 64 vertices and up to 124 triangles
[outputtopology("triangle")]
void MSMain(
uint3 groupThreadID : SV_GroupThreadID,
uint groupID : SV_GroupID,
out vertices PixelInput outVerts[64],
out indices uint3 outTris[124])
{
PerObjectData perObjectData = LoadData<PerObjectData>(g_PushConstantData.perObjectBuffer, 0);
ByteAddressBuffer meshletBuffer = GET_BUFFER(perObjectData.meshletBuffer);
Meshlet m = meshletBuffer.Load<Meshlet>(groupID.x * sizeof(Meshlet));
uint vertexCount = m.packedCounts & 0xFF;
uint triangleCount = (m.packedCounts >> 8) & 0xFF;
SetMeshOutputCounts(vertexCount, triangleCount);
ByteAddressBuffer meshletVerticesBuffer = GET_BUFFER(perObjectData.meshletVerticesBuffer);
ByteAddressBuffer meshletTrianglesBuffer = GET_BUFFER(perObjectData.meshletTrianglesBuffer);
// Write vertex output
if (groupThreadID.x < vertexCount)
{
uint vertexIndex = meshletVerticesBuffer.Load((m.vertexOffset + groupThreadID.x) * 4);
ByteAddressBuffer vertices = GET_BUFFER(perObjectData.vertexBuffer);
Vertex v = vertices.Load<Vertex>(vertexIndex * sizeof(Vertex));
// Basic MVP transform not needed if already in world space, but usually we need localToWorld and ViewProj
PerViewData perViewData = LoadData<PerViewData>(g_PushConstantData.perViewBuffer, 0);
float4 worldPos = mul(perObjectData.localToWorld, float4(v.position.xyz, 1.0f));
outVerts[groupThreadID.x].position = mul(perViewData.viewMatrix, worldPos);
outVerts[groupThreadID.x].position = mul(perViewData.projectionMatrix, outVerts[groupThreadID.x].position);
outVerts[groupThreadID.x].color = v.color;
outVerts[groupThreadID.x].uv = v.uv;
}
// Write triangle output (1 thread processes 1 triangle)
// We could pack 3 indices in a uint or just use byte offset
// In our CPU code, we packed it as individual bytes, so 3 bytes per triangle.
// For 124 triangles, we have 372 bytes.
if (groupThreadID.x < triangleCount)
{
uint triangleIndex = groupThreadID.x;
uint baseOffset = m.triangleOffset + triangleIndex * 3;
// Load 4 bytes to get the 3 index bytes
// Needs byte-aligned loading
uint wordOffset = baseOffset & ~3;
uint shift = (baseOffset & 3) * 8;
uint packedIndices1 = meshletTrianglesBuffer.Load(wordOffset);
uint packedIndices2 = meshletTrianglesBuffer.Load(wordOffset + 4);
uint64_t combined = ((uint64_t)packedIndices2 << 32) | packedIndices1;
uint packedIndices = (uint)(combined >> shift);
uint i0 = packedIndices & 0xFF;
uint i1 = (packedIndices >> 8) & 0xFF;
uint i2 = (packedIndices >> 16) & 0xFF;
outTris[triangleIndex] = uint3(i0, i1, i2);
}
}
float4 PSMain(PixelInput input) : SV_TARGET
{
PerMaterialData perMaterialData = LoadData<PerMaterialData>(g_PushConstantData.perMaterialBuffer, 0);
float4 color1 = SAMPLE_TEXTURE2D(perMaterialData.texture1, perMaterialData.tex_sampler, input.uv.xy);
float4 color2 = SAMPLE_TEXTURE2D(perMaterialData.texture2, perMaterialData.tex_sampler, input.uv.xy);
float4 color3 = SAMPLE_TEXTURE2D(perMaterialData.texture3, perMaterialData.tex_sampler, input.uv.xy);
float4 color4 = SAMPLE_TEXTURE2D(perMaterialData.texture4, perMaterialData.tex_sampler, input.uv.xy);
float4 blendedColor = (color1 + color2 + color3 + color4) * 0.25f;
return perMaterialData.color * blendedColor + input.color;
}

View File

@@ -1,373 +0,0 @@
#if false
using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.DSL.ShaderCompiler;
using Ghost.Graphics.Core;
using Ghost.Graphics.RenderGraphModule;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Mathematics.Geometry;
using Misaki.HighPerformance.Utilities;
namespace Ghost.Graphics.Test.RenderPipeline;
public unsafe partial class TestRenderPipeline : IRenderPipeline
{
private class MeshletDebugPassData
{
public RenderList renderList;
public Handle<Material> material;
public uint globalIndex;
public uint viewIndex;
public uint instanceIndex;
}
private readonly RenderSystem _renderSystem;
private readonly RenderGraph _renderGraph;
private Handle<Shader> _meshletShader;
private Handle<Material> _meshletMaterial;
private bool _disposed;
~TestRenderPipeline()
{
Dispose();
}
internal TestRenderPipeline(RenderSystem renderSystem)
{
_renderSystem = renderSystem;
_renderGraph = new RenderGraph(renderSystem.ResourceManager,
renderSystem.GraphicsEngine.ResourceAllocator,
renderSystem.GraphicsEngine.ResourceDatabase,
renderSystem.GraphicsEngine.PipelineLibrary,
renderSystem.GraphicsEngine.ShaderCompiler);
var shaderDescriptor = DSLShaderCompiler.CompileGraphicsShader("F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/test.gshdr").GetValueOrThrow();
var config = new ShaderCompilationConfig
{
optimizeLevel = CompilerOptimizeLevel.O3,
options = CompilerOption.KeepReflections,
model = shaderDescriptor.shaderModel
};
ref readonly var pass = ref shaderDescriptor.passes[0];
var emptyKeywords = new LocalKeywordSet();
var compiled = renderSystem.GraphicsEngine.ShaderCompiler.CompilePass(in pass, in config, in emptyKeywords).GetValueOrThrow();
_meshletShader = renderSystem.ResourceManager.CreateGraphicsShader(shaderDescriptor, [compiled]);
_meshletMaterial = renderSystem.ResourceManager.CreateMaterial(_meshletShader);
}
private static float3 IntersectFrustumPlanes(float4 p0, float4 p1, float4 p2)
{
var n0 = p0.xyz;
var n1 = p1.xyz;
var n2 = p2.xyz;
float det = math.dot(math.cross(n0, n1), n2);
return (math.cross(n2, n1) * p0.w + math.cross(n0, n2) * p1.w - math.cross(n0, n1) * p2.w) * (1.0f / det);
}
private static Frustum CreateFrustum(float nearClip, float farClip, float4x4 vp, float3 viewDir, float3 viewPos)
{
var frustum = new Frustum();
Frustum.CalculateFrustumPlanes(vp, ref frustum.planes);
// We need to recalculate the near and far planes otherwise it does not work for oblique projection matrices used for reflection.
var nearPlane = Plane.CreateFromUnitNormalAndPointInPlane(viewDir, viewPos);
nearPlane.Distance -= nearClip;
var farPlane = Plane.CreateFromUnitNormalAndPointInPlane(-viewDir, viewPos);
farPlane.Distance += farClip;
frustum.planes[4] = nearPlane;
frustum.planes[5] = farPlane;
frustum.corners[0] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[3], frustum.planes[4]);
frustum.corners[1] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[3], frustum.planes[4]);
frustum.corners[2] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[2], frustum.planes[4]);
frustum.corners[3] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[2], frustum.planes[4]);
frustum.corners[4] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[3], frustum.planes[5]);
frustum.corners[5] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[3], frustum.planes[5]);
frustum.corners[6] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[2], frustum.planes[5]);
frustum.corners[7] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[2], frustum.planes[5]);
return frustum;
}
public void Render(RenderContext ctx, int frameIndex, IRenderPayload payload)
{
var testPayload = (TestRenderPayload)payload;
var resourceManager = _renderSystem.ResourceManager;
var resourceDatabase = _renderSystem.GraphicsEngine.ResourceDatabase;
var requests = testPayload.renderRequests;
for (var i = 0; i < requests.Count; i++)
{
ref readonly var request = ref requests[i];
// 1. Allocate and populate Instance Data buffer
var instanceCount = request.opaqueRenderList.TotalRecordCount;
if (instanceCount == 0)
{
continue; // Nothing to render
}
Handle<GPUTexture> rt;
if (request.swapChainIndex < 0)
{
rt = request.colorTarget;
}
else if (_renderSystem.SwapChainManager.TryGetSwapChain(request.swapChainIndex, out var swapChain))
{
rt = swapChain.GetCurrentBackBuffer();
}
else
{
continue;
}
try
{
var rtResult = _renderSystem.GraphicsEngine.ResourceDatabase.GetResourceDescription(rt.AsResource());
if (rtResult.IsFailure)
{
continue;
}
var rtSize = new uint2(rtResult.Value.TextureDescriptor.Width, rtResult.Value.TextureDescriptor.Height);
var aspectScreen = (float)rtSize.x / rtSize.y;
// NOTE: We assume camera's scale is always (1, 1, 1). Otherwise fastinverse will fail and we need to use regular inverse which is more expensive.
var viewMatrix = math.fastinverse(request.view.localToWorld);
var vfov = 2.0f * math.atan(request.view.sensorSize.y / (2.0f * request.view.focalLength));
var hfov = 2.0f * math.atan(request.view.sensorSize.x / (2.0f * request.view.focalLength));
var aspectSensor = request.view.sensorSize.x / request.view.sensorSize.y;
float vfovF;
switch (request.view.gateFit)
{
case GateFit.Vertical:
vfovF = vfov;
break;
case GateFit.Horizontal:
// Adjust VFOV so that the sensor width fits the screen width
var horizontalAspectBuffer = math.tan(hfov * 0.5f);
vfovF = 2.0f * math.atan(horizontalAspectBuffer / aspectScreen);
break;
case GateFit.Fill:
if (aspectSensor > aspectScreen)
{
goto case GateFit.Vertical;
}
else
{
goto case GateFit.Horizontal;
}
case GateFit.Overscan:
if (aspectSensor > aspectScreen)
{
goto case GateFit.Horizontal;
}
else
{
goto case GateFit.Vertical;
}
default:
vfovF = vfov;
break;
}
var m_11 = 1.0f / math.tan(vfovF * 0.5f);
var m_00 = m_11 / aspectScreen;
var m_22 = request.view.farClipPlane / (request.view.farClipPlane - request.view.nearClipPlane);
var m_23 = -(request.view.farClipPlane * request.view.nearClipPlane) / (request.view.farClipPlane - request.view.nearClipPlane);
var projectionMatrix = new float4x4
(
m_00, 0, 0, 0,
0, m_11, 0, 0,
0, 0, m_22, m_23,
0, 0, 1, 0
);
//var vp = math.mul(projectionMatrix, viewMatrix);
//var viewDir = math.normalize(request.view.localToWorld.c2.xyz);
//var viewPos = request.view.localToWorld.c3.xyz;
//var frustum = CreateFrustum(request.view.nearClipPlane, request.view.farClipPlane, vp, viewDir, viewPos);
var instanceDataSize = (uint)(instanceCount * sizeof(InstanceData));
var instanceBufferDesc = new BufferDesc
{
Size = instanceDataSize,
Stride = (uint)sizeof(InstanceData),
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
HeapType = HeapType.Upload, // Upload directly for simplicity in testing
};
var instanceBufferHandle = resourceManager.CreateTransientBuffer(in instanceBufferDesc, "Instance Buffer");
var instanceBufferResource = instanceBufferHandle.AsResource();
var instanceDataArray = new InstanceData[instanceCount];
var instanceIdx = 0;
foreach (var record in request.opaqueRenderList)
{
var (mesh, error) = resourceManager.GetMeshReference(record.mesh);
if (error.IsFailure)
{
continue;
}
(var mat, error) = resourceManager.GetMaterialReference(_meshletMaterial);
if (error.IsFailure)
{
continue;
}
instanceDataArray[instanceIdx++] = new InstanceData
{
localToWorld = record.localToWorld,
meshBuffer = resourceDatabase.GetBindlessIndex(mesh.Get().MeshDataBuffer.AsResource()),
materialBuffer = resourceDatabase.GetBindlessIndex(mat.Get()._cBufferCache.GpuResource.AsResource())
};
}
ctx.CommandBuffer.Barrier(BarrierDesc.Buffer(instanceBufferResource, BarrierSync.Copy, BarrierAccess.CopyDest));
ctx.UploadBuffer(instanceBufferHandle, instanceDataArray.AsSpan());
ctx.CommandBuffer.Barrier(BarrierDesc.Buffer(instanceBufferResource, BarrierSync.AllShading, BarrierAccess.ShaderResource));
// 2. Allocate and populate View Data buffer
var viewBufferDesc = new BufferDesc
{
Size = (uint)sizeof(ViewData),
Stride = (uint)sizeof(ViewData),
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
HeapType = HeapType.Upload,
};
var viewBufferHandle = resourceManager.CreateTransientBuffer(in viewBufferDesc, "View Buffer");
var viewBufferResource = viewBufferHandle.AsResource();
var viewData = new ViewData
{
viewMatrix = viewMatrix,
projectionMatrix = projectionMatrix,
cameraPosition = request.view.localToWorld.c3.xyz,
nearClip = request.view.nearClipPlane,
cameraDirection = viewMatrix.c2.xyz, // check if that's correct orientation
farClip = request.view.farClipPlane,
screenSize = new float4(rtSize.x, rtSize.y, 1.0f / rtSize.x, 1.0f / rtSize.y)
};
ctx.CommandBuffer.Barrier(BarrierDesc.Buffer(viewBufferResource, BarrierSync.Copy, BarrierAccess.CopyDest));
ctx.UploadBuffer(viewBufferHandle, new ReadOnlySpan<ViewData>(in viewData));
ctx.CommandBuffer.Barrier(BarrierDesc.Buffer(viewBufferResource, BarrierSync.AllShading, BarrierAccess.ShaderResource));
// 3. Allocate and populate Global Frame Data buffer
var frameDataSize = (uint)sizeof(FrameData);
var frameBufferDesc = new BufferDesc
{
Size = frameDataSize,
Stride = frameDataSize,
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
HeapType = HeapType.Upload,
};
var frameBufferHandle = resourceManager.CreateTransientBuffer(in frameBufferDesc, "Frame Buffer");
var frameBufferResource = frameBufferHandle.AsResource();
var frameData = new FrameData
{
instanceBuffer = resourceDatabase.GetBindlessIndex(instanceBufferResource)
};
ctx.CommandBuffer.Barrier(BarrierDesc.Buffer(frameBufferResource, BarrierSync.Copy, BarrierAccess.CopyDest));
ctx.UploadBuffer(frameBufferHandle, new ReadOnlySpan<FrameData>(in frameData));
ctx.CommandBuffer.Barrier(BarrierDesc.Buffer(frameBufferResource, BarrierSync.AllShading, BarrierAccess.ShaderResource));
_renderGraph.Reset();
var backBuffer = _renderGraph.ImportTexture(rt, "BackBuffer");
MeshletDebugPass(backBuffer, request.opaqueRenderList,
uint.MaxValue,
resourceDatabase.GetBindlessIndex(viewBufferResource));
var viewState = new ViewState(rtSize.x, rtSize.y, rtSize.x, rtSize.y);
_renderGraph.Compile(viewState);
_renderGraph.Execute(ctx.CommandBuffer);
}
finally
{
if (request.swapChainIndex >= 0)
{
_renderSystem.SwapChainManager.ReleaseSwapChain(request.swapChainIndex);
}
}
}
}
private void MeshletDebugPass(Identifier<RGTexture> backbuffer, RenderList renderList, uint globalIndex, uint viewIndex)
{
using (var builder = _renderGraph.AddRasterRenderPass<MeshletDebugPassData>("Meshlet Debug Pass", out var passData))
{
var depth = builder.CreateTexture(RGTextureDesc.RelativeDepth(1.0f), "Depth Texture");
passData.renderList = renderList;
passData.globalIndex = globalIndex;
passData.viewIndex = viewIndex;
passData.material = _meshletMaterial;
builder.SetColorAttachment(backbuffer, 0);
builder.SetDepthAttachment(depth);
builder.SetRenderFunc<MeshletDebugPassData>(static (data, ctx) =>
{
ctx.SetGlobalData(data.globalIndex, data.viewIndex);
ctx.SetActiveMaterial(data.material);
var instanceIndex = 0u;
foreach (var record in data.renderList)
{
ctx.SetInstanceIndex(instanceIndex);
var meshRefResult = ctx.ResourceManager.GetMeshReference(record.mesh);
if (meshRefResult.IsSuccess)
{
var meshletCount = (uint)meshRefResult.Value.MeshletData.meshletCount;
ctx.DispatchMesh(new uint3(meshletCount, 1, 1));
}
instanceIndex++;
}
});
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
_renderSystem.ResourceManager.ReleaseMaterial(_meshletMaterial);
_renderSystem.ResourceManager.ReleaseShader(_meshletShader);
_renderGraph.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
}
}
#endif

View File

@@ -1,47 +0,0 @@
#if false
using Ghost.Graphics.Core;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.Test.RenderPipeline;
internal sealed class TestRenderPayload : IRenderPayload
{
public UnsafeList<RenderRequest> renderRequests;
public TestRenderPayload()
{
renderRequests = new UnsafeList<RenderRequest>(2, Allocator.Persistent);
}
public void Reset()
{
for (int i = 0; i < renderRequests.Count; i++)
{
renderRequests[i].Dispose();
}
renderRequests.Clear();
}
public void Dispose()
{
renderRequests.Dispose();
}
}
internal sealed class TestRenderPipelineSettings : IRenderPipelineSettings
{
public IRenderPipeline CreatePipeline(RenderSystem renderSystem)
{
return new TestRenderPipeline(renderSystem);
}
public IRenderPayload CreatePayload(RenderSystem renderSystem)
{
return new TestRenderPayload();
}
}
#endif

View File

@@ -1,50 +0,0 @@
using Ghost.Core;
using Ghost.Engine.Components;
using Ghost.Engine.Utilities;
using Ghost.Entities;
using Misaki.HighPerformance.Mathematics;
namespace Ghost.Graphics.Test.Systems;
internal class CameraMovingSystem : ISystem
{
private Identifier<EntityQuery> _cameraQueryID;
private random _random;
private float3 _target;
public void Initialize(ref readonly SystemAPI systemAPI)
{
_cameraQueryID = QueryBuilder.New()
.WithAll<Camera, LocalToWorld>()
.Build(systemAPI.World, true);
_random = new random(123456);
_target = _random.NextFloat3(-20, 20);
}
public void Update(ref readonly SystemAPI systemAPI)
{
ref var cameraQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_cameraQueryID);
foreach (ref var ltw in cameraQuery.GetComponentIterator<LocalToWorld>())
{
var position = ltw.matrix.c3.xyz;
if (math.distance(position, _target) < 0.1f)
{
_target = _random.NextFloat3(-20, 20);
}
var newPosition = math.lerp(position, _target, 0.025f);
var forward = math.normalize(new float3(0f, 0.5f, 0f) - newPosition);
var rotation = quaternion.LookRotation(forward, math.up());
var matrix = float4x4.TRS(newPosition, rotation, float3.one);
ltw.matrix = matrix;
}
}
public void Cleanup(ref readonly SystemAPI systemAPI)
{
}
}

View File

@@ -1,149 +0,0 @@
#if false
using Ghost.Core;
using Ghost.Engine;
using Ghost.Engine.Components;
using Ghost.Engine.Systems;
using Ghost.Entities;
using Ghost.Graphics.Core;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.Mathematics;
namespace Ghost.Graphics.Test.Systems;
//[RenderPipelineSystem<TestRenderPipelineSettings>]
[UpdateAfter<CameraMovingSystem>]
public class RenderExtractionSystem : ISystem
{
private RenderSystem _renderSystem = null!;
private Identifier<EntityQuery> _cameraQueryID;
private Identifier<EntityQuery> _meshQueryID;
public void Initialize(ref readonly SystemAPI systemAPI)
{
_renderSystem = systemAPI.World.GetService<RenderSystem>();
var builder = new QueryBuilder();
_cameraQueryID = builder
.WithAll<Camera, LocalToWorld>()
.Build(systemAPI.World, false);
builder.Clear();
_meshQueryID = builder
.WithAll<MeshInstance, LocalToWorld>()
.Build(systemAPI.World, true);
}
public void Update(ref readonly SystemAPI systemAPI)
{
if (_meshQueryID.IsInvalid)
{
return;
}
ref var cameraQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_cameraQueryID);
ref var meshQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_meshQueryID);
foreach (var (cam, camLtw) in cameraQuery.GetComponentIterator<Camera, LocalToWorld>())
{
ref readonly var camRef = ref cam.Get();
ref readonly var camLtwRef = ref camLtw.Get();
// TODO: Classify transparent objects into a separate render list and render via oit.
var renderList = new RenderList(1, 64, AllocationHandle.FreeList);
var transparentRenderList = new RenderList(1, 64, AllocationHandle.FreeList);
var shadowCasterRenderList = new RenderList(1, 64, AllocationHandle.FreeList);
// TODO: This chould be done in earallel jobs.
foreach (var chunk in meshQuery.GetChunkIterator())
{
var meshInstances = chunk.GetComponentData<MeshInstance>();
var localToWorlds = chunk.GetComponentData<LocalToWorld>();
for (var i = 0; i < chunk.EntityCount; i++)
{
ref readonly var meshInstance = ref meshInstances[i];
if ((meshInstance.renderingLayerMask & camRef.renderingLayerMask) == 0u)
{
// Not in the same rendering layer, skip.
continue;
}
ref readonly var meshLtw = ref localToWorlds[i];
var meshPosition = meshLtw.matrix.c3.xyz;
var camPosition = camLtwRef.matrix.c3.xyz;
var distance = math.distance(meshPosition, camPosition);
// TODO: Use bounding sphere or AABB for better culling. Currently it just uses the pivot point which can cause popping when the pivot is far from the actual geometry.
if (distance < camRef.nearClipPlane || distance > camRef.farClipPlane)
{
continue;
}
if (meshInstance.shadowCastingMode != ShadowCastingMode.ShadowsOnly)
{
renderList.Add(new RenderRecord
{
localToWorld = meshLtw.matrix,
mesh = meshInstance.mesh,
materialPalette = meshInstance.materialPalette,
renderingLayerMask = meshInstance.renderingLayerMask,
}, 0);
}
if (meshInstance.shadowCastingMode != ShadowCastingMode.Off)
{
shadowCasterRenderList.Add(new RenderRecord
{
localToWorld = meshLtw.matrix,
mesh = meshInstance.mesh,
materialPalette = meshInstance.materialPalette,
renderingLayerMask = meshInstance.renderingLayerMask,
}, 0);
}
}
}
var request = new RenderRequest
{
swapChainIndex = camRef.swapChainIndex,
colorTarget = camRef.colorTarget,
depthTarget = camRef.depthTarget,
opaqueRenderList = renderList,
shadowCasterRenderList = shadowCasterRenderList,
transparentRenderList = transparentRenderList,
view = new RenderView
{
localToWorld = camLtwRef.matrix,
//viewMatrix = viewMatrix,
//projectionMatrix = projectionMatrix,
//position = camLtwRef.matrix.c3.xyz,
//frustum = frustum,
nearClipPlane = camRef.nearClipPlane,
farClipPlane = camRef.farClipPlane,
sensorSize = camRef.sensorSize,
gateFit = camRef.gateFit,
iso = camRef.iso,
shutterSpeed = camRef.shutterSpeed,
aperture = camRef.aperture,
focalLength = camRef.focalLength,
focusDistance = camRef.focusDistance,
renderingLayerMask = camRef.renderingLayerMask,
},
};
((TestRenderPayload)_renderSystem.GetCurrentFramePayload()).renderRequests.Add(request);
}
}
public void Cleanup(ref readonly SystemAPI systemAPI)
{
}
}
#endif

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Application
x:Class="Ghost.Graphics.Test.UnitTestApp"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Graphics.Test">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="ms-appx:///Microsoft.UI.Xaml/DensityStyles/Compact.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -1,44 +0,0 @@
using Ghost.Core;
using Ghost.Graphics.Test.Windows;
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Ghost.Graphics.Test;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class UnitTestApp : Application
{
private Window? _window;
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public UnitTestApp()
{
InitializeComponent();
}
/// <summary>
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
_window = new GraphicsTestWindow();
_window.Activate();
UnhandledException += (sender, e) =>
{
Logger.Error(e.Exception);
#if DEBUG
System.Diagnostics.Debugger.Break();
#endif
Environment.FailFast("Unhandled exception", e.Exception);
};
}
}

View File

@@ -1,182 +0,0 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Utilities;
using Ghost.MeshOptimizer;
using Ghost.Ufbx;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.Mathematics;
using System.Runtime.CompilerServices;
using System.Text;
namespace Ghost.Graphics.Test.Utilities;
internal static class MeshUtility
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float4 ComputeTangent(float3 t, float3 n, float3 b)
{
var proj = n * math.dot(n, t);
t = math.normalize(t - proj);
var w = math.dot(math.cross(n.xyz, t.xyz), b.xyz) < 0.0f ? -1.0f : 1.0f;
return new float4(t.xyz, w);
}
public static unsafe Result LoadMesh(string filePath, AllocationHandle allocationHandle, out UnsafeList<Vertex> vertices, out UnsafeList<uint> indices)
{
vertices = default;
indices = default;
if (!File.Exists(filePath))
{
return Result.Failure("Invalid file path.");
}
if (!Path.GetExtension(filePath).Equals(".obj", StringComparison.OrdinalIgnoreCase)
&& !Path.GetExtension(filePath).Equals(".fbx", StringComparison.OrdinalIgnoreCase))
{
return Result.Failure("Unsupported file format. Only .obj and .fbx are supported.");
}
var error = new ufbx_error();
var load_Opts = new ufbx_load_opts
{
target_axes = ufbx_coordinate_axes.left_handed_y_up,
obj_axes = ufbx_coordinate_axes.right_handed_y_up,
// Force X-axis mirroring to correctly convert handedness to Left-Handed,
// while preserving correct left/right orientation when viewed from the front.
handedness_conversion_axis = ufbx_mirror_axis.UFBX_MIRROR_AXIS_X,
space_conversion = ufbx_space_conversion.UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY,
};
using var str = new UnsafeArray<byte>(Encoding.UTF8.GetByteCount(filePath) + 1, AllocationHandle.FreeList);
var count = Encoding.UTF8.GetBytes(filePath, str.AsSpan());
str[count] = 0;
using var scene = new DisposablePtr<ufbx_scene>(ufbx_scene.LoadFile((sbyte*)str.GetUnsafePtr(), &load_Opts, &error));
if (scene.Get() == null)
{
return Result.Failure(error.description.ToString());
}
using var flatVertices = new UnsafeList<Vertex>(1024, AllocationHandle.FreeList);
var needComputeNormals = false;
for (var i = 0u; i < scene.Get()->nodes.count; i++)
{
var data = scene.Get()->nodes.data;
var node = scene.Get()->nodes.data[i];
if (node->is_root)
{
continue;
}
if (node->mesh != null)
{
var pMesh = node->mesh;
if (pMesh->num_faces == 0)
{
continue;
}
var maxScratchIndices = (int)(pMesh->max_face_triangles * 3u);
using var triIndicesArray = new UnsafeArray<uint>(maxScratchIndices, AllocationHandle.FreeList);
for (var j = 0u; j < pMesh->num_faces; j++)
{
var face = pMesh->faces.data[j];
var numTris = UfbxApi.TriangulateFace(triIndicesArray.AsSpan(0, maxScratchIndices), pMesh, face);
var totalIndices = numTris * 3;
for (var k = 0; k < totalIndices; k++)
{
var ufbxTopologyIndex = triIndicesArray[k];
var posIdx = pMesh->vertex_position.indices.data[ufbxTopologyIndex];
var normIdx = pMesh->vertex_normal.exists ? pMesh->vertex_normal.indices.data[ufbxTopologyIndex] : uint.MaxValue;
var tanIdx = pMesh->vertex_tangent.exists ? pMesh->vertex_tangent.indices.data[ufbxTopologyIndex] : uint.MaxValue;
var uvIdx = pMesh->vertex_uv.exists ? pMesh->vertex_uv.indices.data[ufbxTopologyIndex] : uint.MaxValue;
var colIdx = pMesh->vertex_color.exists ? pMesh->vertex_color.indices.data[ufbxTopologyIndex] : uint.MaxValue;
var btanIdx = pMesh->vertex_bitangent.exists ? pMesh->vertex_bitangent.indices.data[ufbxTopologyIndex] : uint.MaxValue;
var position = pMesh->vertex_position.values.data[posIdx];
var normal = normIdx != uint.MaxValue ? pMesh->vertex_normal.values.data[normIdx] : default;
var uv = uvIdx != uint.MaxValue ? pMesh->vertex_uv.values.data[uvIdx] : default;
var color = colIdx != uint.MaxValue ? pMesh->vertex_color.values.data[colIdx] : default;
var vertex = new Vertex
{
position = new float3(position.x, position.y, position.z),
normal = new float3(normal.x, normal.y, normal.z),
uv = new float2(uv.x, uv.y),
color = new Color128(color.x, color.y, color.z, color.w)
};
if (tanIdx != uint.MaxValue)
{
var mt = pMesh->vertex_tangent.values.data[tanIdx];
var mb = btanIdx != uint.MaxValue ? pMesh->vertex_bitangent.values.data[btanIdx] : default;
var t = new float3(mt.x, mt.y, mt.z);
var n = vertex.normal;
var b = btanIdx != uint.MaxValue ? new float3(mb.x, mb.y, mb.z) : math.cross(n, t);
vertex.tangent = ComputeTangent(t, n, b);
}
var newIndex = (uint)flatVertices.Count;
flatVertices.Add(vertex);
if (!needComputeNormals)
{
needComputeNormals = normIdx == uint.MaxValue || tanIdx == uint.MaxValue;
}
}
}
}
}
var numIndices = (uint)flatVertices.Count;
using var weldedIndices = new UnsafeArray<uint>((int)numIndices, AllocationHandle.FreeList);
using var cachedIndices = new UnsafeArray<uint>((int)numIndices, AllocationHandle.FreeList);
var stream = new ufbx_vertex_stream
{
data = flatVertices.GetUnsafePtr(),
vertex_count = numIndices,
vertex_size = (nuint)sizeof(Vertex)
};
var numUniqueVertices = UfbxApi.GenerateIndices([stream], weldedIndices, null, &error);
if (numUniqueVertices == 0 && error.type != ufbx_error_type.UFBX_ERROR_NONE)
{
return Result.Failure($"Welding failed: {error.description}");
}
MeshOptApi.OptimizeVertexCache((uint*)cachedIndices.GetUnsafePtr(), (uint*)weldedIndices.GetUnsafePtr(), numIndices, numUniqueVertices);
vertices = new UnsafeList<Vertex>((int)numUniqueVertices, allocationHandle);
indices = new UnsafeList<uint>((int)numIndices, allocationHandle);
var finalVertexCount = MeshOptApi.OptimizeVertexFetch(vertices.GetUnsafePtr(), (uint*)cachedIndices.GetUnsafePtr(), numIndices, flatVertices.GetUnsafePtr(), numIndices, (nuint)sizeof(Vertex));
vertices.UnsafeSetCount((int)finalVertexCount);
MemoryUtility.MemCpy(indices.GetUnsafePtr(), cachedIndices.GetUnsafePtr(), numIndices * sizeof(uint));
indices.UnsafeSetCount((int)numIndices);
if (needComputeNormals)
{
MeshBuilder.ComputeNormal(vertices, indices);
MeshBuilder.ComputeTangents(vertices, indices);
}
return Result.Success();
}
}

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="Ghost.Graphics.Test.Windows.DebugOutputWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Ghost.Graphics.Test.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.Graphics.Test.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="DebugOutputWindow"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid>
<controls:DebugConsole HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
</Grid>
</Window>

View File

@@ -1,11 +0,0 @@
using Microsoft.UI.Xaml;
namespace Ghost.Graphics.Test.Windows;
internal sealed partial class DebugOutputWindow : Window
{
public DebugOutputWindow()
{
InitializeComponent();
}
}

View File

@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="Ghost.Graphics.Test.Windows.GraphicsTestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Ghost.Graphics.Test.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.Graphics.Test.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}" />
</Grid>
</Window>

View File

@@ -1,208 +0,0 @@
using Ghost.Core;
using Ghost.Entities;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.Mathematics;
namespace Ghost.Graphics.Test.Windows;
public sealed partial class GraphicsTestWindow : Window
{
private RenderSystem? _renderSystem;
private ISwapChain? _swapChain;
//private JobScheduler _jobScheduler;
private World? _world;
private Handle<Mesh> _meshHandle;
private bool _isFirstActivationHandled;
public GraphicsTestWindow()
{
InitializeComponent();
Activated += GraphicsTestWindow_Activated;
Closed += GraphicsTestWindow_Closed;
Panel.SizeChanged += SwapChainPanel_SizeChanged;
Panel.CompositionScaleChanged += SwapChainPanel_CompositionScaleChanged;
AllocationManager.Initialize(AllocationManagerDesc.Default);
//_jobScheduler = new JobScheduler(Environment.ProcessorCount - 1);
}
private void GraphicsTestWindow_Activated(object sender, WindowActivatedEventArgs e)
{
#if false
if (_isFirstActivationHandled)
{
return;
}
e.Handled = true;
_isFirstActivationHandled = true;
_renderSystem = new RenderSystem(new RenderSystemDesc()
{
FrameBufferCount = 2,
GraphicsAPI = GraphicsAPI.Direct3D12,
InitialRenderPipelineSettings = new RenderPipeline.TestRenderPipelineSettings()
});
_swapChain = _renderSystem.SwapChainManager.EnsureSwapChain(0, new SwapChainDesc
{
Width = (uint)AppWindow.Size.Width,
Height = (uint)AppWindow.Size.Height,
ScaleX = Panel.CompositionScaleX,
ScaleY = Panel.CompositionScaleY,
Format = TextureFormat.B8G8R8A8_UNorm,
Target = SwapChainTarget.FromCompositionSurface(Panel)
});
_renderSystem.Start();
// ECS Setup
_world = World.Create();
_world.AddService(_renderSystem);
// Add Systems
var group = _world.SystemManager.GetSystem<DefaultSystemGroup>();
group.AddSystem<RenderExtractionSystem>();
group.AddSystem<CameraMovingSystem>();
group.SortSystems();
_world.SystemManager.InitializeAll();
// Create Camera Entity
using var scope = AllocationManager.CreateStackScope();
using var camSet = new ComponentSet(scope.AllocationHandle, ComponentTypeID<Camera>.Value, ComponentTypeID<LocalToWorld>.Value);
var cameraEntity = _world.EntityManager.CreateEntity(camSet);
_world.EntityManager.SetComponent(cameraEntity, new Camera
{
swapChainIndex = 0,
depthTarget = Handle<GPUTexture>.Invalid,
nearClipPlane = 0.1f,
farClipPlane = 1000.0f,
focalLength = 50.0f,
sensorSize = new float2(36.0f, 24.0f),
gateFit = GateFit.Vertical,
renderingLayerMask = RenderingLayerMask.All,
});
_world.EntityManager.SetComponent(cameraEntity, new LocalToWorld
{
matrix = float4x4.TRS(new float3(0.0f, 1.0f, 5.0f), quaternion.EulerXYZ(new float3(0, math.radians(180.0f), 0)), float3.one)
});
// Create Mesh Entity
//MeshBuilder.CreateCube(0.75f, default, Allocator.Persistent, out var vertices, out var indices);
Utilities.MeshUtility.LoadMesh("F:/c/SimpleRayTracer/native/assets/bunny.obj", AllocationHandle.Persistent, out var vertices, out var indices).ThrowIfFailed();
// TODO: Put this to the beginning of the frame without creating another command buffer?
using var directCmd = _renderSystem.GraphicsEngine.CreateCommandBuffer(CommandBufferType.Graphics);
var ctx = new RenderContext(_renderSystem.ResourceManager, _renderSystem.GraphicsEngine, directCmd);
using var cmdAllocator = _renderSystem.GraphicsEngine.CreateCommandAllocator(CommandBufferType.Graphics);
directCmd.Begin(cmdAllocator);
_meshHandle = ctx.CreateMesh(vertices, indices, true);
ctx.UpdateObjectData(_meshHandle);
directCmd.End().ThrowIfFailed();
// Maybe async upload support in the future?
_renderSystem.GraphicsEngine.Device.GraphicsQueue.Submit(directCmd);
using var meshSet = new ComponentSet(scope.AllocationHandle, ComponentTypeID<MeshInstance>.Value, ComponentTypeID<LocalToWorld>.Value);
var meshEntity = _world.EntityManager.CreateEntity(meshSet);
_world.EntityManager.SetComponent(meshEntity, new MeshInstance
{
mesh = _meshHandle,
renderingLayerMask = RenderingLayerMask.All,
shadowCastingMode = Engine.ShadowCastingMode.On
});
_world.EntityManager.SetComponent(meshEntity, new LocalToWorld
{
matrix = float4x4.TRS(float3.zero, quaternion.EulerXYZ(new float3(0, 0, 0)), float3.one)
});
CompositionTarget.Rendering += OnRendering;
#endif
}
private void GraphicsTestWindow_Closed(object sender, WindowEventArgs e)
{
try
{
CompositionTarget.Rendering -= OnRendering;
_renderSystem?.Stop();
if (_world != null)
{
World.Destroy(_world.ID);
}
_renderSystem?.ResourceManager.ReleaseMesh(_meshHandle);
//_jobScheduler.Dispose();
_renderSystem?.Dispose();
AllocationManager.Dispose();
}
catch (Exception ex)
{
#if DEBUG
System.Diagnostics.Debugger.Break();
#endif
Environment.FailFast("Failed to close the window properly.", ex);
}
finally
{
}
}
private void SwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (_renderSystem == null || _swapChain == null)
{
return;
}
var newWidth = (uint)(Panel.ActualWidth * Panel.CompositionScaleX);
var newHeight = (uint)(Panel.ActualHeight * Panel.CompositionScaleY);
if (newWidth < 8 || newHeight < 8)
{
return;
}
_renderSystem.RequestSwapChainResize(_swapChain, new uint2(newWidth, newHeight));
}
private void SwapChainPanel_CompositionScaleChanged(SwapChainPanel sender, object args)
{
_swapChain?.SetScale(sender.CompositionScaleX, sender.CompositionScaleY);
}
private void OnRendering(object? sender, object e)
{
if (_renderSystem == null || _world == null || _swapChain == null)
{
return;
}
if (_renderSystem.TryAcquireCPUFrame())
{
_world.SystemManager.UpdateAll(default);
_renderSystem.SignalCPUReady();
}
}
}

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Ghost.Graphics.Test.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -20,22 +20,15 @@ public class AssetCatalogTests
[TestCleanup] [TestCleanup]
public void Cleanup() public void Cleanup()
{ {
SqliteConnection.ClearAllPools(); var connectionString = new SqliteConnectionStringBuilder
var dir = Path.GetDirectoryName(_dbPath);
if (dir != null && Directory.Exists(dir))
{ {
try DataSource = _dbPath,
{ ForeignKeys = true,
Directory.Delete(dir, true); Pooling = true
} }.ToString();
catch (IOException)
{ using var connection = new SqliteConnection(connectionString);
// Sometimes SQLite holds a lock for a bit longer SqliteConnection.ClearPool(connection);
Thread.Sleep(100);
if (Directory.Exists(dir))
Directory.Delete(dir, true);
}
}
} }
[TestMethod] [TestMethod]

View File

@@ -32,8 +32,6 @@ public class AssetManagerTest
[TestInitialize] [TestInitialize]
public void Setup() public void Setup()
{ {
AllocationManager.Initialize(AllocationManagerDesc.Default);
_graphicsEngine = new MockingGraphicsEngine(); _graphicsEngine = new MockingGraphicsEngine();
_commandBuffer = (MockingCommandBuffer)_graphicsEngine.CreateCommandBuffer(); _commandBuffer = (MockingCommandBuffer)_graphicsEngine.CreateCommandBuffer();
_provider = new MockingContentProvider(); _provider = new MockingContentProvider();
@@ -62,8 +60,6 @@ public class AssetManagerTest
_copyPipeline.Dispose(); _copyPipeline.Dispose();
_commandBuffer.Dispose(); _commandBuffer.Dispose();
_graphicsEngine.Dispose(); _graphicsEngine.Dispose();
AllocationManager.Dispose();
} }
[TestMethod] [TestMethod]

View File

@@ -26,21 +26,15 @@ public class ImportCoordinatorTests
[TestCleanup] [TestCleanup]
public void Cleanup() public void Cleanup()
{ {
SqliteConnection.ClearAllPools(); var connectionString = new SqliteConnectionStringBuilder
var dir = Path.GetDirectoryName(_libraryRoot);
if (dir != null && Directory.Exists(dir))
{ {
try DataSource = _dbPath,
{ ForeignKeys = true,
Directory.Delete(dir, true); Pooling = true
} }.ToString();
catch (IOException)
{ using var connection = new SqliteConnection(connectionString);
Thread.Sleep(100); SqliteConnection.ClearPool(connection);
if (Directory.Exists(dir))
Directory.Delete(dir, true);
}
}
} }
[TestMethod] [TestMethod]

View File

@@ -2,7 +2,6 @@ using Ghost.Editor.Core;
using Ghost.Editor.Core.Assets; using Ghost.Editor.Core.Assets;
using Ghost.Editor.Core.Services; using Ghost.Editor.Core.Services;
using Ghost.Engine.Streaming; using Ghost.Engine.Streaming;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@@ -20,47 +19,20 @@ public class MeshAssetHandlerTests
} }
private string _projectRoot = null!; private string _projectRoot = null!;
private string _previousCurrentDirectory = null!;
[TestInitialize] [TestInitialize]
public void Setup() public void Setup()
{ {
AllocationManager.Initialize(AllocationManagerDesc.Default);
_previousCurrentDirectory = Environment.CurrentDirectory;
_projectRoot = Path.Combine(Path.GetTempPath(), "GhostEngineTests", Guid.NewGuid().ToString()); _projectRoot = Path.Combine(Path.GetTempPath(), "GhostEngineTests", Guid.NewGuid().ToString());
Directory.CreateDirectory(_projectRoot); Directory.CreateDirectory(_projectRoot);
EditorApplication.Initialize(new EmptyServiceProvider(), _projectRoot, "MeshImportTest"); EditorApplication.Initialize(new EmptyServiceProvider(), _projectRoot, "MeshImportTest");
} }
[TestCleanup]
public void Cleanup()
{
AllocationManager.Dispose();
Environment.CurrentDirectory = _previousCurrentDirectory;
if (Directory.Exists(_projectRoot))
{
try
{
Directory.Delete(_projectRoot, true);
}
catch (IOException)
{
Thread.Sleep(100);
if (Directory.Exists(_projectRoot))
{
Directory.Delete(_projectRoot, true);
}
}
}
}
[TestMethod] [TestMethod]
public async Task FBXAssetHandler_ImportsObjAsManifestAndMeshSubAssets() public async Task FBXAssetHandler_ImportsObjAsManifestAndMeshSubAssets()
{ {
var sourcePath = Path.Combine(EditorApplication.AssetsFolderPath, "kit.obj"); var sourcePath = Path.Combine(EditorApplication.AssetsFolderPath, "kit.obj");
await File.WriteAllTextAsync(sourcePath, CreateTwoObjectObj()); await File.WriteAllTextAsync(sourcePath, CreateTwoObjectObj(), TestContext.CancellationToken);
var parentGuid = Guid.NewGuid(); var parentGuid = Guid.NewGuid();
var targetPath = ImportCoordinator.GetImportedAssetPath(parentGuid); var targetPath = ImportCoordinator.GetImportedAssetPath(parentGuid);

View File

@@ -1,5 +1,6 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Entities; using Ghost.Entities;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ghost.UnitTest.ECS; namespace Ghost.UnitTest.ECS;
@@ -53,11 +54,11 @@ public class SharedComponentTests
var sharedValue = new SharedGroup { groupID = groupID }; var sharedValue = new SharedGroup { groupID = groupID };
// Build shared data blob: SharedGroup is the only shared component. // Build shared data blob: SharedGroup is the only shared component.
Span<byte> sharedData = stackalloc byte[sizeof(SharedGroup)]; using var sharedSet = new SharedComponentSet(sizeof(SharedGroup), AllocationHandle.Persistent);
MemoryMarshal.Write(sharedData, sharedValue); sharedSet.With(sharedValue);
Identifier<IComponent>[] ids = [ComponentTypeID<Tag>.Value, ComponentTypeID<SharedGroup>.Value]; Identifier<IComponent>[] ids = [ComponentTypeID<Tag>.Value, ComponentTypeID<SharedGroup>.Value];
var set = new ComponentSetView(ids, sharedData); var set = new ComponentSetView(ids, sharedSet);
Span<Entity> result = stackalloc Entity[1]; Span<Entity> result = stackalloc Entity[1];
_world.EntityManager.CreateEntities(result, set); _world.EntityManager.CreateEntities(result, set);
@@ -464,4 +465,37 @@ public class SharedComponentTests
Assert.IsTrue(groups.ContainsKey(2) && groups[2] == 1, "Group 2 entity should be untouched."); Assert.IsTrue(groups.ContainsKey(2) && groups[2] == 1, "Group 2 entity should be untouched.");
} }
// ══════════════════════════════════════════════════════════════════════════
// 9. Canonical Ordering
// ══════════════════════════════════════════════════════════════════════════
[TestMethod]
public void CanonicalOrdering_SharedComponentSet_IndependentOfInsertionOrder()
{
using var sharedSet1 = new SharedComponentSet(64, AllocationHandle.Temp);
sharedSet1.With(new SharedGroup { groupID = 42 });
sharedSet1.With(new SharedGroup2 { subID = 99 });
using var sharedSet2 = new SharedComponentSet(64, AllocationHandle.Temp);
sharedSet2.With(new SharedGroup2 { subID = 99 });
sharedSet2.With(new SharedGroup { groupID = 42 });
Identifier<IComponent>[] ids1 = [ComponentTypeID<Tag>.Value, ComponentTypeID<SharedGroup>.Value, ComponentTypeID<SharedGroup2>.Value];
var set1 = new ComponentSetView(ids1, sharedSet1);
Identifier<IComponent>[] ids2 = [ComponentTypeID<Tag>.Value, ComponentTypeID<SharedGroup2>.Value, ComponentTypeID<SharedGroup>.Value];
var set2 = new ComponentSetView(ids2, sharedSet2);
Span<Entity> result1 = stackalloc Entity[1];
_world.EntityManager.CreateEntities(result1, set1);
Span<Entity> result2 = stackalloc Entity[1];
_world.EntityManager.CreateEntities(result2, set2);
var groups = CollectGroupCounts();
Assert.AreEqual(1, groups.Count, "Expected exactly 1 chunk group due to canonical sorting of SharedComponentSet.");
Assert.AreEqual(2, groups[42], "Both entities should be grouped under groupID 42.");
}
} }

View File

@@ -11,11 +11,13 @@ public class WorldTests
private struct CompB : IComponentData { public int value; } private struct CompB : IComponentData { public int value; }
private World _world = null!; private World _world = null!;
private EntityManager _entityManager;
[TestInitialize] [TestInitialize]
public void Setup() public void Setup()
{ {
_world = World.Create(null, 64); _world = World.Create(entityCapacity: 128);
_entityManager = _world.EntityManager;
} }
[TestCleanup] [TestCleanup]

View File

@@ -1,5 +1,7 @@
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
[assembly: DoNotParallelize]
namespace Ghost.UnitTest; namespace Ghost.UnitTest;
[TestClass] [TestClass]

View File

@@ -54,18 +54,6 @@ public class ShaderLibraryTest
} }
} }
[TestInitialize]
public void Setup()
{
AllocationManager.Initialize(AllocationManagerDesc.Default);
}
[TestCleanup]
public void Cleanup()
{
AllocationManager.Dispose();
}
[TestMethod] [TestMethod]
public unsafe void TestInvalidateShaderCache_EvictsPipelinesAndClearsCache() public unsafe void TestInvalidateShaderCache_EvictsPipelinesAndClearsCache()
{ {

View File

@@ -36,22 +36,26 @@ public unsafe class ComponentDescriptorTests
Assert.AreEqual(4, descriptor.Properties.Length, "Should have exactly 4 visible properties (HiddenFloat is ignored)."); Assert.AreEqual(4, descriptor.Properties.Length, "Should have exactly 4 visible properties (HiddenFloat is ignored).");
var p0 = descriptor.Properties[0]; var p0 = descriptor.Properties[0];
Assert.AreEqual("IntValue", p0.Name); Assert.AreEqual("intValue", p0.Name);
Assert.AreEqual("IntValue", p0.DisplayName);
Assert.AreEqual(typeof(int), p0.FieldType); Assert.AreEqual(typeof(int), p0.FieldType);
Assert.AreEqual(0, p0.OffsetInComponent); Assert.AreEqual(0, p0.OffsetInComponent);
var p1 = descriptor.Properties[1]; var p1 = descriptor.Properties[1];
Assert.AreEqual("doubleValue", p1.Name);
Assert.AreEqual("Custom Name", p1.DisplayName); Assert.AreEqual("Custom Name", p1.DisplayName);
Assert.AreEqual(typeof(double), p1.FieldType); Assert.AreEqual(typeof(double), p1.FieldType);
// Offset of double after int+float is 8 (with alignment) // Offset of double after int+float is 8 (with alignment)
Assert.AreEqual((int)Marshal.OffsetOf<TestComponent>("DoubleValue"), p1.OffsetInComponent); Assert.AreEqual((int)Marshal.OffsetOf<TestComponent>("doubleValue"), p1.OffsetInComponent);
var p2 = descriptor.Properties[2]; var p2 = descriptor.Properties[2];
Assert.AreEqual("IsReadOnly", p2.Name); Assert.AreEqual("isReadOnly", p2.Name);
Assert.AreEqual("IsReadOnly", p2.DisplayName);
Assert.IsTrue(p2.IsReadOnly); Assert.IsTrue(p2.IsReadOnly);
var p3 = descriptor.Properties[3]; var p3 = descriptor.Properties[3];
Assert.AreEqual("Position", p3.Name); Assert.AreEqual("position", p3.Name);
Assert.AreEqual("Position", p3.DisplayName);
Assert.AreEqual(typeof(float3), p3.FieldType); Assert.AreEqual(typeof(float3), p3.FieldType);
Assert.IsNull(p3.Children); // float3 is a primitive so it has no children Assert.IsNull(p3.Children); // float3 is a primitive so it has no children
} }

View File

@@ -34,8 +34,6 @@ public class SceneSerializationTests
[TestInitialize] [TestInitialize]
public void Setup() public void Setup()
{ {
AllocationManager.Initialize(AllocationManagerDesc.Default);
_previousCurrentDirectory = Environment.CurrentDirectory; _previousCurrentDirectory = Environment.CurrentDirectory;
_projectRoot = Path.Combine(Path.GetTempPath(), "GhostEngineTests", Guid.NewGuid().ToString()); _projectRoot = Path.Combine(Path.GetTempPath(), "GhostEngineTests", Guid.NewGuid().ToString());
Directory.CreateDirectory(_projectRoot); Directory.CreateDirectory(_projectRoot);
@@ -51,7 +49,6 @@ public class SceneSerializationTests
{ {
_worldService.Dispose(); _worldService.Dispose();
AllocationManager.Dispose();
Environment.CurrentDirectory = _previousCurrentDirectory; Environment.CurrentDirectory = _previousCurrentDirectory;
if (Directory.Exists(_projectRoot)) if (Directory.Exists(_projectRoot))

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@ namespace Ghost.FMOD.Core
These properties take advantage of the fact that numbuffers is always zero or one These properties take advantage of the fact that numbuffers is always zero or one
*/ */
public int numchannels public readonly int numchannels
{ {
get get
{ {
@@ -43,7 +43,7 @@ namespace Ghost.FMOD.Core
} }
} }
public IntPtr buffer public readonly IntPtr buffer
{ {
get get
{ {
@@ -307,7 +307,7 @@ namespace Ghost.FMOD.Core
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
private IntPtr[] spectrum_internal; private IntPtr[] spectrum_internal;
public float[][] spectrum public readonly float[][] spectrum
{ {
get get
{ {
@@ -332,7 +332,7 @@ namespace Ghost.FMOD.Core
} }
} }
public void getSpectrum(int channel, ref float[] buffer) public readonly void getSpectrum(int channel, ref float[] buffer)
{ {
var bufferLength = Math.Min(buffer.Length, length); var bufferLength = Math.Min(buffer.Length, length);
Marshal.Copy(spectrum_internal[channel], buffer, 0, bufferLength); Marshal.Copy(spectrum_internal[channel], buffer, 0, bufferLength);
@@ -442,14 +442,14 @@ namespace Ghost.FMOD.Core
public DSP_GETLISTENERATTRIBUTES_FUNC getlistenerattributes; public DSP_GETLISTENERATTRIBUTES_FUNC getlistenerattributes;
public DSP_LOG_FUNC log; public DSP_LOG_FUNC log;
public DSP_GETUSERDATA_FUNC getuserdata; public DSP_GETUSERDATA_FUNC getuserdata;
public DSP_STATE_DFT_FUNCTIONS dft public readonly DSP_STATE_DFT_FUNCTIONS dft
{ {
get get
{ {
return Marshal.PtrToStructure<DSP_STATE_DFT_FUNCTIONS>(dft_internal); return Marshal.PtrToStructure<DSP_STATE_DFT_FUNCTIONS>(dft_internal);
} }
} }
public DSP_STATE_PAN_FUNCTIONS pan public readonly DSP_STATE_PAN_FUNCTIONS pan
{ {
get get
{ {
@@ -470,7 +470,7 @@ namespace Ghost.FMOD.Core
private IntPtr functions_internal; private IntPtr functions_internal;
public int systemobject; public int systemobject;
public DSP_STATE_FUNCTIONS functions public readonly DSP_STATE_FUNCTIONS functions
{ {
get get
{ {

File diff suppressed because it is too large Load Diff