Added ScheduleEntityParallel and IJobEntityParallel for parallel querying

This commit is contained in:
2025-12-07 11:45:25 +09:00
parent 30c1d99959
commit 02084c1e47
10 changed files with 2388 additions and 72 deletions

View File

@@ -21,6 +21,7 @@
<ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.1" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="1.1.0" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.2.8" />
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.2.6" />
<PackageReference Include="System.IO.Hashing" Version="10.0.0" />

View File

@@ -1,15 +1,28 @@
using Ghost.Test.Core;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.Mathematics;
using System;
namespace Ghost.Entities.Test;
internal struct TestEntityQueryJob : IJobEntityParallel<Transform>
{
public readonly void Execute(Entity entity, ref Transform transform)
{
transform.position += new float3(10, 10, 10);
}
}
public partial class ArcEntityTest : ITest
{
private World _world = null!;
private JobScheduler _jobScheduler = null!;
public void Setup()
{
_world = World.Create();
_jobScheduler = new JobScheduler(4);
}
public void Run()
@@ -20,23 +33,27 @@ public partial class ArcEntityTest : ITest
var queryID = new QueryBuilder().WithAll<Transform>().Build(_world);
ref var query = ref _world.GetEntityQueryReference(queryID);
query.ForEach<Transform>((ref t) =>
var testJob = new TestEntityQueryJob();
var handle = query.ScheduleEntityParallel<TestEntityQueryJob, Transform>(_jobScheduler, testJob, Allocator.Temp, 64, JobHandle.Invalid);
_jobScheduler.WaitComplete(handle);
query.ForEach<Transform>((e, ref t) =>
{
t.position = new float3(1, 2, 3);
Console.WriteLine($"Entity {e} Has Position: {t.position}");
});
foreach (var chunk in query.GetChunkIterator())
{
var transforms = chunk.GetComponentData<Transform>();
var entities = chunk.GetEntities();
var bits = chunk.GetEnableBits<Transform>();
//foreach (var chunk in query.GetChunkIterator())
//{
// var transforms = chunk.GetComponentData<Transform>();
// var entities = chunk.GetEntities();
// var bits = chunk.GetEnableBits<Transform>();
var it = bits.GetIterator();
while (it.Next(out var index) && index < chunk.Count)
{
Console.WriteLine($"Entity {entities[index]} Updated Position: {transforms[index].position}");
}
}
// var it = bits.GetIterator();
// while (it.Next(out var index) && index < chunk.Count)
// {
// Console.WriteLine($"Entity {entities[index]} Updated Position: {transforms[index].position}");
// }
//}
}
public void Cleanup()

View File

@@ -52,6 +52,10 @@
<LastGenOutput>ForEach.gen.cs</LastGenOutput>
<Generator>TextTemplatingFileGenerator</Generator>
</None>
<None Update="Templates\EntityQuery.JobEntityParallel.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>EntityQuery.JobEntityParallel.gen.cs</LastGenOutput>
</None>
<None Update="Templates\QueryBuilder.With.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>QueryBuilder.With.gen.cs</LastGenOutput>
@@ -68,6 +72,11 @@
<AutoGen>True</AutoGen>
<DependentUpon>EntityQuery.ForEach.tt</DependentUpon>
</Compile>
<Compile Update="Templates\EntityQuery.JobEntityParallel.gen.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>EntityQuery.JobEntityParallel.tt</DependentUpon>
</Compile>
<Compile Update="Templates\ForEach.gen.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>

View File

@@ -231,37 +231,37 @@ public unsafe partial struct EntityQuery : IIdentifierType, IDisposable
}
}
private readonly Identifier<World> _worldID;
private UnsafeList<Identifier<Archetype>> _matchingArchetypes;
internal EntityQueryMask _mask;
internal EntityQuery(Identifier<World> worldID, EntityQueryMask mask)
private UnsafeList<Identifier<Archetype>> _matchingArchetypes;
private readonly Identifier<EntityQuery> _id;
private readonly Identifier<World> _worldID;
internal EntityQuery(Identifier<EntityQuery> id, Identifier<World> worldID, EntityQueryMask mask)
{
_id = id;
_worldID = worldID;
_mask = mask;
_matchingArchetypes = new UnsafeList<Identifier<Archetype>>(8, Allocator.Persistent);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsEntityValid(byte* chunkBase, int entityIndex, ref readonly Archetype archetype, ref readonly EntityQueryMask mask)
{
// 1. Check "Require Enabled" (WithAll)
// We iterate over the bits set in 'requireEnabled'
var it = mask.requireEnabled.GetIterator();
while (it.Next(out var id))
{
// Get the EnableBitmask for this component in this chunk
var layout = archetype.GetLayout(id).Value;
if (layout.enableBitsOffset == -1)
{
var layoutResult = archetype.GetLayout(id);
if (layoutResult.Status != ResultStatus.Success
// Not enableable, always true
|| layoutResult.Value.enableBitsOffset == -1)
{
continue;
}
// Check bit
if (!CheckBit(chunkBase + layout.enableBitsOffset, entityIndex))
if (!CheckBit(chunkBase + layoutResult.Value.enableBitsOffset, entityIndex))
{
return false;
}
@@ -271,17 +271,18 @@ public unsafe partial struct EntityQuery : IIdentifierType, IDisposable
it = mask.requireDisabled.GetIterator();
while (it.Next(out var id))
{
var layout = archetype.GetLayout(id).Value;
var layoutResult = archetype.GetLayout(id);
if (layoutResult.Status != ResultStatus.Success)
{
continue;
}
// If component is not enableable, it is technically "Always Enabled",
// so it cannot satisfy "WithDisabled".
if (layout.enableBitsOffset == -1)
{
return false;
}
// Check bit (Must be 0)
if (CheckBit(chunkBase + layout.enableBitsOffset, entityIndex))
if (layoutResult.Value.enableBitsOffset == -1
|| CheckBit(chunkBase + layoutResult.Value.enableBitsOffset, entityIndex))
{
return false;
}
@@ -294,19 +295,15 @@ public unsafe partial struct EntityQuery : IIdentifierType, IDisposable
var layoutResult = archetype.GetLayout(id);
if (layoutResult.Status != ResultStatus.Success)
{
// Component is absent, so it is not enabled.
continue;
}
// If component is not enableable, it is technically "Always Enabled",
// so it cannot satisfy "Reject if Enabled".
if (layoutResult.Value.enableBitsOffset == -1)
{
return false;
}
// Check bit (Must be 0)
if (CheckBit(chunkBase + layoutResult.Value.enableBitsOffset, entityIndex))
if (layoutResult.Value.enableBitsOffset == -1
|| CheckBit(chunkBase + layoutResult.Value.enableBitsOffset, entityIndex))
{
return false;
}
@@ -316,7 +313,7 @@ public unsafe partial struct EntityQuery : IIdentifierType, IDisposable
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CheckBit(byte* maskBase, int index)
internal static bool CheckBit(byte* maskBase, int index)
{
var byteIndex = index >> Chunk.BIT_SHIFT;
var bitIndex = index & Chunk.BIT_ALIGNMENT_MINUS_ONE;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,189 @@
<#@ template language="C#" #>
<#@ output extension="gen.cs" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ include file="Helpers.ttinclude" #>
using Ghost.Core;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Entities;
<# for (var i = 1; i <= Amount; i++)
{
var generics = AppendGenerics(i);
var restrictions = AppendGenericRestrictionsMultiline(i, "unmanaged, IComponent", 1);
#>
public interface IJobEntityParallel<<#= generics #>>
<#= restrictions #>
{
void Execute(Entity entity, <#= AppendParameters(i, "ref T{0} component{0}") #>);
}
internal unsafe struct JobEntityBatch<TJob, <#= generics #>> : IJobParallelFor
where TJob : unmanaged, IJobEntityParallel<<#= generics #>>
<#= restrictions #>
{
public TJob userJob;
public UnsafeList<IntPtr> chunks;
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 void Execute(int loopIndex, int threadIndex)
{
// 1. Get the specific pChunk for this thread
var pChunk = (byte*)chunks[loopIndex];
var count = chunkCount[loopIndex];
<# for (var j = 0; j < i; j++){ #>
var off<#= j #> = offsets<#= j #>[loopIndex];
var enableOff<#= j #> = bitsOffsets<#= j #>[loopIndex];
<# } #>
var pEntity = (Entity*)(pChunk + entityOffset[loopIndex]);
<# for (var j = 0; j < i; j++){ #>
var ptr<#= j #> = (<#= "T" + j #>*)(pChunk + off<#= j #>);
<# } #>
for (var i = 0; i < count; i++)
{
<# for (var j = 0; j < i; j++){ #>
if (enableOff<#= j #> != -1 && !EntityQuery.CheckBit(pChunk + enableOff<#= j #>, i))
{
continue;
}
<# } #>
userJob.Execute(pEntity[i], <#= AppendParameters(i, "ref ptr{0}[i]") #>);
}
}
}
<# } #>
public unsafe partial struct EntityQuery
{
<# for (var i = 1; i <= Amount; i++)
{
var generics = AppendGenerics(i);
var restrictions = AppendGenericRestrictionsMultiline(i, "unmanaged, IComponent", 2);
#>
private struct DisposeJobEntity<#= i #> : IJob
{
public UnsafeList<IntPtr> chunkList;
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 void Execute(int threadIndex)
{
chunkList.Dispose();
chunkEntityCounts.Dispose();
entityOffsets.Dispose();
<# for (var j = 0; j < i; j++){ #>
offsets<#= j #>.Dispose();
bitsOffsets<#= j #>.Dispose();
<# } #>
}
}
public JobHandle ScheduleEntityParallel<TJob, <#= generics #>>(JobScheduler scheduler, TJob jobData, Allocator allocator, int batchSize, JobHandle dependency)
where TJob : unmanaged, IJobEntityParallel<<#= generics #>>
<#= restrictions #>
{
// 1. Flatten the World
var chunkList = new UnsafeList<IntPtr>(128, allocator);
var chunkEntityCounts = new UnsafeList<int>(128, allocator);
var entityOffsets = new UnsafeList<int>(128, allocator);
<# for (var j = 0; j < i; j++){ #>
var offsets<#= j #> = new UnsafeList<int>(128, allocator);
var bitsOffsets<#= j #> = new UnsafeList<int>(128, allocator);
<# } #>
// Iterate the Query's matching archetypes
foreach (var archID in _matchingArchetypes)
{
ref var arch = ref World.GetWorld(_worldID)
.GetValueOrThrow(ResultStatus.Success)
.GetArchetypeReference(archID);
if (arch.ChunkCount == 0)
{
continue;
}
// Get offsets ONCE per archetype
<# for (var j = 0; j < i; j++){ #>
var layout<#= j #> = arch.GetLayout(ComponentTypeID<T<#= j #>>.value)
.GetValueOrThrow(ResultStatus.Success);
<# } #>
// Add all chunks from this archetype
for (var i = 0; i < arch.ChunkCount; i++)
{
ref var chunkRef = ref arch.GetChunkReference(i);
chunkList.Add((IntPtr)chunkRef.GetUnsafePtr());
chunkEntityCounts.Add(chunkRef.Count);
entityOffsets.Add(arch.EntityIDsOffset);
<# for (var j = 0; j < i; j++){ #>
offsets<#= j #>.Add(layout<#= j #>.offset);
bitsOffsets<#= j #>.Add(layout<#= j #>.enableBitsOffset);
<# } #>
}
}
// 2. Create the Runner
var runner = new JobEntityBatch<TJob, <#= generics #>>
{
userJob = jobData,
chunks = chunkList,
chunkCount = chunkEntityCounts,
entityOffset = entityOffsets,
<# for (var j = 0; j < i; j++){ #>
offsets<#= j #> = offsets<#= j #>,
bitsOffsets<#= j #> = bitsOffsets<#= j #>,
<# } #>
};
var jobHandle = scheduler.ScheduleParallel(ref runner, chunkList.Count, batchSize, dependency);
// 3. Dispose the temp lists
var disposeJob = new DisposeJobEntity<#= i #>
{
chunkList = chunkList,
chunkEntityCounts = chunkEntityCounts,
entityOffsets = entityOffsets,
<# for (var j = 0; j < i; j++){ #>
offsets<#= j #> = offsets<#= j #>,
bitsOffsets<#= j #> = bitsOffsets<#= j #>,
<# } #>
};
scheduler.Schedule(ref disposeJob, jobHandle);
return jobHandle;
}
<# } #>
}

View File

@@ -19,6 +19,7 @@
if (i > 0) sb.Append(", ");
sb.Append(string.Format(template, i));
}
return sb.ToString();
}
@@ -27,27 +28,18 @@
return AppendGenerics(amount, "T{0}");
}
public StringBuilder AppendGenericRefParameters(int amount)
public StringBuilder AppendParameters(int amount, string template)
{
var sb = new StringBuilder();
for (var localIndex = 0; localIndex < amount; localIndex++)
{
sb.Append($"ref T{localIndex} t{localIndex}Component,");
}
sb.Length--;
return sb;
}
public StringBuilder AppendRefParameters(int amount, string template)
sb.Append(string.Format(template, localIndex));
if (localIndex < amount - 1)
{
var sb = new StringBuilder();
for (var localIndex = 0; localIndex < amount; localIndex++)
{
sb.Append($"ref {string.Format(template, localIndex)},");
sb.Append(", ");
}
}
sb.Length--;
return sb;
}
@@ -62,6 +54,7 @@
sb.Append(' ');
}
}
return sb;
}
@@ -83,6 +76,7 @@
sb.AppendLine();
}
}
return sb;
}
@@ -102,6 +96,7 @@
sb.Append(" && ");
}
}
return sb;
}
@@ -116,6 +111,7 @@
sb.Append(" && ");
}
}
return sb;
}
@@ -130,6 +126,7 @@
sb.Append(", ");
}
}
return sb;
}
@@ -144,6 +141,7 @@
sb.Append(", ");
}
}
return sb;
}
#>

View File

@@ -148,7 +148,7 @@ public partial class World : IIdentifierType, IDisposable, IEquatable<World>
internal Identifier<EntityQuery> CreateEntityQuery(EntityQueryMask mask, int maskHash)
{
var queryID = new Identifier<EntityQuery>(_entityQueries.Count);
_entityQueries.Add(new EntityQuery(_id, mask));
_entityQueries.Add(new EntityQuery(queryID, _id, mask));
_querieLookup.Add(maskHash, queryID);
ref var query = ref _entityQueries[queryID.value];

View File

@@ -16,6 +16,12 @@
<IsTrimmable>True</IsTrimmable>
</PropertyGroup>
<ItemGroup>
<Compile Remove="RenderGraphModule\**" />
<EmbeddedResource Remove="RenderGraphModule\**" />
<None Remove="RenderGraphModule\**" />
</ItemGroup>
<ItemGroup>
<None Remove="runtime\win-x64\native\dxcompiler.dll" />
<None Remove="runtime\win-x64\native\dxil.dll" />
@@ -48,8 +54,4 @@
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
</ItemGroup>
<ItemGroup>
<Folder Include="RenderGraphModule\" />
</ItemGroup>
</Project>