Refactor job API: add JobExecutionContext, update tests

Major breaking change: job interfaces now use JobExecutionContext
instead of threadIndex, enabling thread-aware and dynamic job
dispatching. Updated all job system, SPMD, and test code to match.
Collections improved with new methods and clearer enumerators.
Renamed IJobScheduler.WaitComplete to Wait. Incremented project
versions. Includes bug fixes, documentation, and style updates.
This commit is contained in:
2026-03-04 11:43:39 +09:00
parent b9ca71834f
commit 37d548085e
31 changed files with 652 additions and 207 deletions

View File

@@ -1,7 +1,8 @@
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Mathematics.SPMD;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.Test.UnitTest.Jobs;
@@ -11,7 +12,7 @@ internal unsafe struct DotProductJob : IJobSPMD<float>
public float3* arrayB; // source array 2
public float* results; // output array (dot products)
public readonly void Execute<TLane>(int baseIndex, int threadIndex)
public readonly void Execute<TLane>(int baseIndex, ref readonly JobExecutionContext ctx)
where TLane : ISPMD<TLane, float>
{
var vecA = MathV.LoadVector3<TLane, float>((float*)(arrayA + baseIndex));
@@ -28,7 +29,7 @@ internal unsafe struct Vector2LerpJob : IJobSPMD<float>
public float2[] arrayB;
public float[] results;
public readonly void Execute<TLane>(int baseIndex, int threadIndex)
public readonly void Execute<TLane>(int baseIndex, ref readonly JobExecutionContext ctx)
where TLane : ISPMD<TLane, float>
{
var a = MathV.LoadVector2<TLane, float>(ref arrayA[baseIndex].x);
@@ -47,7 +48,7 @@ internal unsafe struct Vector4NormalizeJob : IJobSPMD<float>
public float4[] input;
public float4[] output;
public readonly void Execute<TLane>(int baseIndex, int threadIndex)
public readonly void Execute<TLane>(int baseIndex, ref readonly JobExecutionContext ctx)
where TLane : ISPMD<TLane, float>
{
var vec = MathV.LoadVector4<TLane, float>(ref input[baseIndex].x);
@@ -62,7 +63,7 @@ internal unsafe struct Vector3CrossJob : IJobSPMD<float>
public float3[] arrayB;
public float3[] results;
public readonly void Execute<TLane>(int baseIndex, int threadIndex)
public readonly void Execute<TLane>(int baseIndex, ref readonly JobExecutionContext ctx)
where TLane : ISPMD<TLane, float>
{
var a = MathV.LoadVector3<TLane, float>(ref arrayA[baseIndex].x);
@@ -80,7 +81,7 @@ internal unsafe struct MinMaxClampJob : IJobSPMD<float>
public float3[] maxs;
public float3[] results;
public readonly void Execute<TLane>(int baseIndex, int threadIndex)
public readonly void Execute<TLane>(int baseIndex, ref readonly JobExecutionContext ctx)
where TLane : ISPMD<TLane, float>
{
var val = MathV.LoadVector3<TLane, float>(ref values[baseIndex].x);
@@ -98,7 +99,7 @@ internal unsafe struct DistanceJob : IJobSPMD<float>
public float3[] arrayB;
public float[] results;
public readonly void Execute<TLane>(int baseIndex, int threadIndex)
public readonly void Execute<TLane>(int baseIndex, ref readonly JobExecutionContext ctx)
where TLane : ISPMD<TLane, float>
{
var a = MathV.LoadVector3<TLane, float>(ref arrayA[baseIndex].x);
@@ -134,7 +135,8 @@ public class SPMDTest
results = results
};
job.Run<DotProductJob, float>(count, -1);
job.Run<DotProductJob, float>(count, default);
// Verify first result: dot([0,1,2], [1,2,3]) = 0*1 + 1*2 + 2*3 = 8
Assert.AreEqual(8.0f, results[0], 0.001f);
@@ -168,7 +170,7 @@ public class SPMDTest
results = results
};
job.Run<Vector2LerpJob, float>(count, -1);
job.Run<Vector2LerpJob, float>(count, default);
// Verify first result: lerp([0,1], [10,11], 0.5) = [5,6], length = sqrt(25+36) = sqrt(61)
var expectedFirst = math.sqrt(5 * 5 + 6 * 6);
@@ -198,7 +200,7 @@ public class SPMDTest
output = output
};
job.Run<Vector4NormalizeJob, float>(count, -1);
job.Run<Vector4NormalizeJob, float>(count, default);
// Verify first result: normalize([1,2,3,4])
var len0 = math.sqrt(1 * 1 + 2 * 2 + 3 * 3 + 4 * 4);
@@ -239,7 +241,7 @@ public class SPMDTest
results = results
};
job.Run<Vector3CrossJob, float>(count, -1);
job.Run<Vector3CrossJob, float>(count, default);
// cross([1,0,0], [0,1,0]) = [0,0,1]
for (var i = 0; i < count; i++)
@@ -275,7 +277,7 @@ public class SPMDTest
results = results
};
job.Run<MinMaxClampJob, float>(count, -1);
job.Run<MinMaxClampJob, float>(count, default);
// Verify clamping works correctly
for (var i = 0; i < count; i++)
@@ -313,7 +315,7 @@ public class SPMDTest
results = results
};
job.Run<DistanceJob, float>(count, -1);
job.Run<DistanceJob, float>(count, default);
// distance([0,0,0], [3,4,0]) = 5
for (var i = 0; i < count; i++)

View File

@@ -3,6 +3,7 @@ using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.Mathematics.SPMD;
using Misaki.HighPerformance.Test.Jobs;
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.Test.UnitTest.Jobs;
@@ -11,7 +12,7 @@ namespace Misaki.HighPerformance.Test.UnitTest.Jobs;
[DoNotParallelize]
public unsafe class TestJobSystem
{
private JobScheduler _jobScheduler = null!;
private static JobScheduler s_jobScheduler = null!;
public TestContext TestContext
{
@@ -19,16 +20,17 @@ public unsafe class TestJobSystem
set;
}
[TestInitialize]
public void Initialize()
[ClassInitialize]
public static void Initialize(TestContext testContext)
{
_jobScheduler = new JobScheduler(3);
s_jobScheduler = new JobScheduler(3);
}
[TestCleanup]
public void Cleanup()
[ClassCleanup(ClassCleanupBehavior.EndOfClass)]
public static void Cleanup()
{
_jobScheduler.Dispose();
s_jobScheduler.Dispose();
AllocationManager.Dispose();
}
[TestMethod]
@@ -42,8 +44,8 @@ public unsafe class TestJobSystem
result = result
};
var handle = _jobScheduler.Schedule(ref job, -1);
_jobScheduler.WaitComplete(handle);
var handle = s_jobScheduler.Schedule(ref job, -1);
s_jobScheduler.Wait(handle);
Assert.AreEqual(4.0f, *result);
}
@@ -59,7 +61,7 @@ public unsafe class TestJobSystem
result = result
};
var handle1 = _jobScheduler.Schedule(ref job1, -1);
var handle1 = s_jobScheduler.Schedule(ref job1, -1);
var job2 = new AddJob
{
@@ -67,8 +69,8 @@ public unsafe class TestJobSystem
result = result
};
var handle2 = _jobScheduler.Schedule(ref job2, -1, handle1);
_jobScheduler.WaitComplete(handle2);
var handle2 = s_jobScheduler.Schedule(ref job2, -1, handle1);
s_jobScheduler.Wait(handle2);
Assert.AreEqual(8.0f, *result);
}
@@ -84,8 +86,8 @@ public unsafe class TestJobSystem
result = result
};
var handle1 = _jobScheduler.Schedule(ref job1);
_jobScheduler.WaitComplete(handle1);
var handle1 = s_jobScheduler.Schedule(ref job1);
s_jobScheduler.Wait(handle1);
var job2 = new AddJob
{
@@ -93,8 +95,8 @@ public unsafe class TestJobSystem
result = result
};
var handle2 = _jobScheduler.Schedule(ref job2, handle1);
_jobScheduler.WaitComplete(handle2);
var handle2 = s_jobScheduler.Schedule(ref job2, handle1);
s_jobScheduler.Wait(handle2);
Assert.AreEqual(8.0f, *result);
}
@@ -110,7 +112,7 @@ public unsafe class TestJobSystem
result = result
};
var handle1 = _jobScheduler.Schedule(ref job1);
var handle1 = s_jobScheduler.Schedule(ref job1);
var job2 = new AddJob
{
@@ -118,7 +120,7 @@ public unsafe class TestJobSystem
result = result
};
var handle2 = _jobScheduler.Schedule(ref job2, handle1);
var handle2 = s_jobScheduler.Schedule(ref job2, handle1);
var job3 = new AddJob
{
@@ -126,10 +128,10 @@ public unsafe class TestJobSystem
result = result
};
var combinedHandle = _jobScheduler.CombineDependencies(handle1, handle2);
var handle3 = _jobScheduler.Schedule(ref job3, combinedHandle);
var combinedHandle = s_jobScheduler.CombineDependencies(handle1, handle2);
var handle3 = s_jobScheduler.Schedule(ref job3, combinedHandle);
_jobScheduler.WaitComplete(handle3);
s_jobScheduler.Wait(handle3);
Assert.AreEqual(19.0f, *result);
}
@@ -146,8 +148,8 @@ public unsafe class TestJobSystem
inout = result
};
var handle = _jobScheduler.ScheduleParallel(ref job, size, 64);
_jobScheduler.WaitComplete(handle);
var handle = s_jobScheduler.ScheduleParallel(ref job, size, 64);
s_jobScheduler.Wait(handle);
Assert.AreEqual(1.0f, result[500]);
}
@@ -198,11 +200,11 @@ public unsafe class TestJobSystem
output = result
};
var handle1 = _jobScheduler.ScheduleParallel(ref addJob, arraySize, 64);
var handle2 = _jobScheduler.ScheduleParallel(ref multiplyJob, arraySize, 64);
var handle3 = _jobScheduler.Schedule(ref sumJob, handle2);
var handle1 = s_jobScheduler.ScheduleParallel(ref addJob, arraySize, 64);
var handle2 = s_jobScheduler.ScheduleParallel(ref multiplyJob, arraySize, 64);
var handle3 = s_jobScheduler.Schedule(ref sumJob, handle2);
_jobScheduler.WaitComplete(handle3);
s_jobScheduler.Wait(handle3);
var expected = ComputeExpectedSum(arraySize);
Assert.AreEqual(expected, *result, 0.01f);
@@ -226,13 +228,13 @@ public unsafe class TestJobSystem
result = result2
};
var handle1 = _jobScheduler.Schedule(ref job1);
var handle2 = _jobScheduler.Schedule(ref job2);
var handle1 = s_jobScheduler.Schedule(ref job1);
var handle2 = s_jobScheduler.Schedule(ref job2);
_jobScheduler.WaitAll(handle1, handle2);
s_jobScheduler.WaitAll(handle1, handle2);
Assert.AreEqual(JobState.Completed, _jobScheduler.GetJobStatus(handle1));
Assert.AreEqual(JobState.Completed, _jobScheduler.GetJobStatus(handle2));
Assert.AreEqual(JobState.Completed, s_jobScheduler.GetJobStatus(handle1));
Assert.AreEqual(JobState.Completed, s_jobScheduler.GetJobStatus(handle2));
}
[TestMethod]
@@ -253,12 +255,12 @@ public unsafe class TestJobSystem
result = result2
};
var handle1 = _jobScheduler.Schedule(ref job1);
var handle2 = _jobScheduler.Schedule(ref job2);
var handle1 = s_jobScheduler.Schedule(ref job1);
var handle2 = s_jobScheduler.Schedule(ref job2);
var completedHandle = _jobScheduler.WaitAny(handle1, handle2);
var completedHandle = s_jobScheduler.WaitAny(handle1, handle2);
Assert.AreEqual(JobState.Completed, _jobScheduler.GetJobStatus(completedHandle));
Assert.AreEqual(JobState.Completed, s_jobScheduler.GetJobStatus(completedHandle));
}
[TestMethod]
@@ -274,7 +276,7 @@ public unsafe class TestJobSystem
// 1. Create a "Gatekeeper" vectorJob that spins/blocks a worker thread until signaled.
// This allows us to control exactly when the dependency completes.
var rootJob = new WaitJob { pSignal = &startSignal };
var rootHandle = _jobScheduler.Schedule(ref rootJob);
var rootHandle = s_jobScheduler.Schedule(ref rootJob);
// 2. Start a background task to flood the scheduler with dependencies on the Gatekeeper.
using var barrier = new Barrier(2);
@@ -288,7 +290,7 @@ public unsafe class TestJobSystem
// CONTENTION POINT:
// Trying to add a dependency to 'rootHandle'.
// Eventually, this will happen exactly while 'rootHandle' is transitioning to Completed.
_jobScheduler.Schedule(ref depJob, rootHandle);
s_jobScheduler.Schedule(ref depJob, rootHandle);
}
}, TestContext.CancellationTokenSource.Token);
@@ -321,7 +323,7 @@ public unsafe class TestJobSystem
}
// Ensure the root vectorJob is officially cleaned up
_jobScheduler.WaitComplete(rootHandle);
s_jobScheduler.Wait(rootHandle);
Assert.AreEqual(jobCount, *pExecutedCount, "Race condition detected: Some dependent jobs failed to execute (Wait timeout).");
@@ -335,27 +337,71 @@ public unsafe class TestJobSystem
var vectorBuf = stackalloc float[size * size];
var vs = new Span<float>(vectorBuf, size * size);
var vectorJob = new Misaki.HighPerformance.Test.Jobs.NoiseJobVector
var vectorJob = new NoiseJobVector
{
buffers = vectorBuf,
width = size,
height = size,
};
vectorJob.Run(size * size, -1);
var ctx = new JobExecutionContext(-1, s_jobScheduler);
vectorJob.Run(size * size, in ctx);
var spmdBuf = stackalloc float[size * size];
var ss = new Span<float>(spmdBuf, size * size);
var spmdJob = new Misaki.HighPerformance.Test.Jobs.NoiseJobMath
var spmdJob = new NoiseJobMath
{
buffers = spmdBuf,
width = size,
height = size,
};
//spmdJob.Run(size * size, -1);
spmdJob.Run(size * size, default);
var eq = vs.SequenceCompareTo(ss);
Assert.AreEqual(0, eq);
}
[TestMethod]
public void DynamicDispatch()
{
using var arr = new UnsafeArray<UnsafeArray<int>>(256, Allocator.Persistent);
for (var i = 0; i < arr.Length; i++)
{
arr[i] = new UnsafeArray<int>(256, Allocator.Persistent);
for (var j = 0; j < arr[i].Length; j++)
{
arr[i][j] = j;
}
}
using var handles = new UnsafeList<JobHandle>(arr.Length, Allocator.Persistent);
var job = new JobDispatchingJob
{
data = arr,
handles = handles.AsParallelWriter()
};
var handle = s_jobScheduler.ScheduleParallelFor(ref job, arr.Length, 64);
s_jobScheduler.Wait(handle);
s_jobScheduler.WaitAll(handles.AsSpan());
for (var i = 0; i < arr.Length; i++)
{
if (i % 2 == 0)
{
for (var j = 0; j < arr[i].Length; j++)
{
Assert.AreEqual(j * 2, arr[i][j]);
}
}
}
for (var i = 0; i < arr.Length; i++)
{
arr[i].Dispose();
}
}
}

View File

@@ -9,7 +9,7 @@ internal unsafe struct TwoSumJob : IJob
public float* result;
public void Execute(int threadIndex)
public void Execute(ref readonly JobExecutionContext ctx)
{
*result = value1 + value2;
}
@@ -21,7 +21,7 @@ internal unsafe struct AddJob : IJob
public float* result;
public void Execute(int threadIndex)
public void Execute(ref readonly JobExecutionContext ctx)
{
*result += value;
}
@@ -33,7 +33,7 @@ internal unsafe struct KahanSumJob : IJob
public int length;
public float* output;
public void Execute(int threadIndex)
public void Execute(ref readonly JobExecutionContext ctx)
{
var sum = 0f;
var c = 0f; // Compensation for lost low-order bits
@@ -55,7 +55,7 @@ internal unsafe struct ParallelAddJob : IJobParallel
public float value;
public float* inout;
public void Execute(int startIndex, int endIndex, int threadIndex)
public void Execute(int startIndex, int endIndex, ref readonly JobExecutionContext ctx)
{
for (var i = startIndex; i < endIndex; i++)
{
@@ -69,7 +69,7 @@ internal unsafe struct ParallelMultiplyJob : IJobParallel
public float multiplier;
public float* inout;
public void Execute(int startIndex, int endIndex, int threadIndex)
public void Execute(int startIndex, int endIndex, ref readonly JobExecutionContext ctx)
{
for (var i = startIndex; i < endIndex; i++)
{
@@ -82,7 +82,7 @@ public unsafe struct WaitJob : IJob
{
public bool* pSignal;
public void Execute(int loopIndex)
public void Execute(ref readonly JobExecutionContext ctx)
{
var spin = new SpinWait();
while (!Volatile.Read(ref *pSignal))
@@ -96,7 +96,7 @@ public unsafe struct IncrementJob : IJob
{
public int* pCounter;
public void Execute(int loopIndex)
public void Execute(ref readonly JobExecutionContext ctx)
{
Interlocked.Increment(ref *pCounter);
}