ORIGINAL POST:
I’m not necessarily saying this is an appropriate choice for implementing this specific task in a game, but would this work? It seems like this is adhering to all the constraints of the Burst compiler, right? Im working off of the manual iteration example in the 0.11 documentation
Points of note:
My job stuct execute function is indexing into 2 chunks in memory instead of one – will that screw anything up? Will this still be fast?
Will the sub batching logic (chunk.Count is a subbatch) screw up this plan? Is this actually guaranteed to check all pairs of entities?
Is there anything better I can be doing than those duplicated variables (translations, translations2 etc)? I’m scared of breaking Burst…
is 32 the right choice for the innerloopbatch size? I couldnt find any guidelines on how to set that number
here it is - code to check, for each entity with this component, whether or not there is at least one other entity within its threshold distance that has this component
using System.Linq;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
public struct MyComponent : IComponentData
{
public float threshold;
public bool closeEnough;
}
public class CheckAllPairsSystem : SystemBase
{
[BurstCompile]
struct CheckAllPairsJob : IJobParallelFor
{
[DeallocateOnJobCompletion] public NativeArray<ArchetypeChunk> Chunks;
[DeallocateOnJobCompletion] public NativeArray<int2> Combos;
public ArchetypeChunkComponentType<Translation> TranslationType;
[ReadOnly] public ArchetypeChunkComponentType<MyComponent> MyComponentType;
public void Execute(int comboIndex)
{
var combo = Combos[comboIndex];
var chunk = Chunks[combo.x];
var chunk2 = Chunks[combo.y];
var translations = chunk.GetNativeArray(TranslationType);
var translations2 = chunk2.GetNativeArray(TranslationType);
var mycomponents = chunk.GetNativeArray(MyComponentType);
var mycomponents2 = chunk2.GetNativeArray(MyComponentType);
var instanceCount = chunk.Count;
var instanceCount2 = chunk2.Count;
for (int i = 0; i < instanceCount; i++)
{
for (int j = 0; j < instanceCount2; j++)
{
// if checking against the same chunk, dont check against the same index,
// because that would be the same entity
if (combo.x == combo.y && i == j) { continue; }
var a = mycomponents[i];
var aTrans = translations[i];
var b = mycomponents2[j];
var bTrans = translations2[j];
var distance = math.distance(aTrans.Value, bTrans.Value);
a.closeEnough = a.closeEnough || distance < a.threshold;
b.closeEnough = b.closeEnough || distance < b.threshold;
}
}
}
}
EntityQuery query;
protected override void OnCreate()
{
query = GetEntityQuery(typeof(MyComponent), ComponentType.ReadOnly<Translation>());
}
protected override void OnUpdate()
{
var translationType = GetArchetypeChunkComponentType<Translation>(true);
var myComponentType = GetArchetypeChunkComponentType<MyComponent>();
var chunks = query.CreateArchetypeChunkArray(Allocator.TempJob);
var comboSeq = (
from i in Enumerable.Range(0, chunks.Length)
from j in Enumerable.Range(0, chunks.Length)
select new int2(i, j)).ToArray();
var combos = new NativeArray<int2>(comboSeq.Length, Allocator.TempJob);
combos.CopyFrom(comboSeq);
var checkAlignedJob = new CheckAllPairsJob()
{
Chunks = chunks,
Combos = combos,
MyComponentType = myComponentType,
TranslationType = translationType
};
checkAlignedJob.Schedule(combos.Length,32, this.Dependency);
}
}
I haven’t read the code thoroughly yet but here are some quick notes:
Use math.distancesq instead of distance if you just want to compare, it is a lot cheaper
Don’t use LINQ queries, especially in update. It is expensive and generates a lot of memory garbage that make GC spikes in your game.
I think you don’t need manual iteration for this and there’s a simpler way, I’ll check back later
For inner loop batch count, the general rule is lower number for heavy jobs, and higher number for lighter jobs (although usually I never get benefits past 64). But you’d need to test and profile yourself to get the best values for your specific job.
Cool. the LINQ in the update was specifically something I wanted feedback on
Oh, and when you say light vs heavy jobs, do you mean memory or compute heavy?
I’m sorry to say this, but your code isn’t even close to working the way you want it to.
Problem 1) Inside your inner loop, you are only ever calculating values that get assigned to stack variables in the inner loop scope. Since those variables get destroyed each loop iteration, you are effectively doing nothing. To fix that, you would need to write a and b back to the arrays, which you can’t because…
Problem 2) You marked MyComponentType as ReadOnly. You are going to have to get rid of that, which will cause more problems because…
Problem 3) You are allowing the same chunk to be accessed by two threads at once. That means you can’t write back to your components without causing race conditions. If you actually bothered to run your code, you would have received an error about accessing Chunks in parallel in a non-thread-safe manner.
My suggestion would be to forget about multithreading and just use IJob for now and see if you can get the algorithm to work with Burst. And don’t worry so much about creating local variables and whatnot. Burst is surprisingly good at figuring out your intent and optimizing out a lot of those variables in clever ways.
Once you get that working, come back and we can discuss options for multi-threading. This kind of problem is not the most trivial to multi-thread. Most likely you will need some intermediate buffers, or a NativeStream, or perhaps a clever acceleration structure that can spatially partition elements and perform spatially distinct queries in parallel.
Outstanding! Thank you! You’re right about my not having run it yet - guilty
But this is precisely the form of criticism I was hoping would be leveled. I really appreciate that you took the time to read it and tear it apart. I apologize for leaving in the low hanging fruit (problems 1 and 2)
Cool! Well, I modified it to be a bit less parallel (but still parallel) until I can come up with some clever strategies to make that combinatorial approach work.
More importantly I actually ran it to make sure it works
Thanks exceedingly, your criticism has been instructive and well reasoned.
using System.Linq;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
[GenerateAuthoringComponent]
public struct MyThreshold : IComponentData
{
public float threshold;
}
[GenerateAuthoringComponent]
public struct MyComponent : IComponentData
{
public bool closeEnough;
}
public class CheckAllPairsSystem : SystemBase
{
[BurstCompile]
struct CheckAllPairsJob : IJobParallelFor
{
[DeallocateOnJobCompletion] public NativeArray<ArchetypeChunk> Chunks;
[ReadOnly] public ArchetypeChunkComponentType<Translation> TranslationType;
[ReadOnly] public ArchetypeChunkComponentType<MyThreshold> MyThresholdType;
public ArchetypeChunkComponentType<MyComponent> MyComponentType;
public void Execute(int jobIndex)
{
var chunk = Chunks[jobIndex];
var translations = chunk.GetNativeArray(TranslationType);
var mycomponents = chunk.GetNativeArray(MyComponentType);
var mythresholds = chunk.GetNativeArray(MyThresholdType);
var instanceCount = chunk.Count;
ArchetypeChunk chunk2;
NativeArray<Translation> translations2;
int instanceCount2;
for (int i = 0; i < Chunks.Length; i++)
{
chunk2 = Chunks[i];
translations2 = chunk2.GetNativeArray(TranslationType);
instanceCount2 = chunk2.Count;
for (int j = 0; j < instanceCount; j++)
{
for (int k = 0; k < instanceCount2; k++)
{
// if checking against the same chunk, dont check against the same index,
// because that would be the same entity
if (i == jobIndex && j == k) { continue; }
var a = mycomponents[j];
var aTheshold = mythresholds[j];
var aTrans = translations[j];
var bTrans = translations2[k];
var distance = math.distance(aTrans.Value, bTrans.Value);
a.closeEnough = a.closeEnough || distance < aTheshold.threshold;
mycomponents[j] = a;
}
}
}
}
}
EntityQuery query;
protected override void OnCreate()
{
query = GetEntityQuery(typeof(MyComponent),
ComponentType.ReadOnly<MyThreshold>(),
ComponentType.ReadOnly<Translation>());
}
protected override void OnUpdate()
{
var translationType = GetArchetypeChunkComponentType<Translation>(true);
var myComponentType = GetArchetypeChunkComponentType<MyComponent>();
var myThesholdType = GetArchetypeChunkComponentType<MyThreshold>(true);
var chunks = query.CreateArchetypeChunkArray(Allocator.TempJob);
var checkAlignedJob = new CheckAllPairsJob()
{
Chunks = chunks,
MyComponentType = myComponentType,
MyThresholdType = myThesholdType,
TranslationType = translationType
};
this.Dependency = checkAlignedJob.Schedule(chunks.Length,32, this.Dependency);
}
}