Implement scheduling system with parallel execution support and dummy job systems
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
namespace Nerfed.Runtime.Scheduling;
|
||||
|
||||
public interface IParallelSystemMetadata
|
||||
{
|
||||
string ScheduleName { get; }
|
||||
SystemAccessDeclaration AccessDeclaration { get; }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
namespace Nerfed.Runtime.Scheduling;
|
||||
|
||||
public enum SystemAccessMode
|
||||
{
|
||||
Read,
|
||||
WriteExisting,
|
||||
StructuralWrite,
|
||||
}
|
||||
|
||||
public readonly struct ComponentAccess
|
||||
{
|
||||
public ComponentAccess(Type componentType, SystemAccessMode mode)
|
||||
{
|
||||
ComponentType = componentType;
|
||||
Mode = mode;
|
||||
}
|
||||
|
||||
public Type ComponentType { get; }
|
||||
public SystemAccessMode Mode { get; }
|
||||
}
|
||||
|
||||
public readonly struct SystemAccessDeclaration
|
||||
{
|
||||
private readonly ComponentAccess[] accesses;
|
||||
|
||||
public static readonly SystemAccessDeclaration Empty = new(Array.Empty<ComponentAccess>());
|
||||
|
||||
public SystemAccessDeclaration(params ComponentAccess[] accesses)
|
||||
{
|
||||
this.accesses = accesses ?? Array.Empty<ComponentAccess>();
|
||||
}
|
||||
|
||||
public ReadOnlySpan<ComponentAccess> Accesses => accesses ?? Array.Empty<ComponentAccess>();
|
||||
|
||||
public bool ConflictsWith(in SystemAccessDeclaration other)
|
||||
{
|
||||
ReadOnlySpan<ComponentAccess> left = Accesses;
|
||||
ReadOnlySpan<ComponentAccess> right = other.Accesses;
|
||||
|
||||
for (int i = 0; i < left.Length; i++)
|
||||
{
|
||||
for (int j = 0; j < right.Length; j++)
|
||||
{
|
||||
if (left[i].Mode == SystemAccessMode.StructuralWrite || right[j].Mode == SystemAccessMode.StructuralWrite)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (left[i].ComponentType != right[j].ComponentType)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (left[i].Mode == SystemAccessMode.Read && right[j].Mode == SystemAccessMode.Read)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static ComponentAccess Read<T>() where T : unmanaged => new(typeof(T), SystemAccessMode.Read);
|
||||
public static ComponentAccess WriteExisting<T>() where T : unmanaged => new(typeof(T), SystemAccessMode.WriteExisting);
|
||||
public static ComponentAccess StructuralWrite<T>() where T : unmanaged => new(typeof(T), SystemAccessMode.StructuralWrite);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using MoonTools.ECS;
|
||||
|
||||
namespace Nerfed.Runtime.Scheduling;
|
||||
|
||||
public sealed class SystemScheduleEntry
|
||||
{
|
||||
public SystemScheduleEntry(
|
||||
MoonTools.ECS.System system,
|
||||
SystemSchedulePhase phase,
|
||||
string[] dependsOn,
|
||||
int registrationIndex)
|
||||
{
|
||||
System = system;
|
||||
Phase = phase;
|
||||
DependsOn = dependsOn ?? Array.Empty<string>();
|
||||
RegistrationIndex = registrationIndex;
|
||||
|
||||
if (system is IParallelSystemMetadata metadata)
|
||||
{
|
||||
Name = metadata.ScheduleName;
|
||||
Access = metadata.AccessDeclaration;
|
||||
}
|
||||
else
|
||||
{
|
||||
Name = system.GetType().Name;
|
||||
// Unknown systems are treated as structural to preserve safety until metadata is declared.
|
||||
Access = new SystemAccessDeclaration(new ComponentAccess(typeof(object), SystemAccessMode.StructuralWrite));
|
||||
}
|
||||
}
|
||||
|
||||
public MoonTools.ECS.System System { get; }
|
||||
public string Name { get; }
|
||||
public SystemSchedulePhase Phase { get; }
|
||||
public string[] DependsOn { get; }
|
||||
public int RegistrationIndex { get; }
|
||||
public SystemAccessDeclaration Access { get; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Nerfed.Runtime.Scheduling;
|
||||
|
||||
public enum SystemSchedulePhase
|
||||
{
|
||||
PreUpdate = 0,
|
||||
Simulation = 1,
|
||||
LateSimulation = 2,
|
||||
PreRender = 3,
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
using MoonTools.ECS;
|
||||
|
||||
namespace Nerfed.Runtime.Scheduling;
|
||||
|
||||
public sealed class SystemScheduler
|
||||
{
|
||||
private readonly List<SystemScheduleEntry> entries = new List<SystemScheduleEntry>();
|
||||
private readonly SystemSchedulerOptions options;
|
||||
|
||||
public SystemScheduler(SystemSchedulerOptions options)
|
||||
{
|
||||
this.options = options ?? new SystemSchedulerOptions();
|
||||
}
|
||||
|
||||
public IReadOnlyList<SystemScheduleEntry> Entries => entries;
|
||||
|
||||
public void Register(MoonTools.ECS.System system, SystemSchedulePhase phase, params string[] dependsOn)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(system);
|
||||
string[] dependencies = dependsOn?.Where(static value => !string.IsNullOrWhiteSpace(value)).ToArray() ?? Array.Empty<string>();
|
||||
entries.Add(new SystemScheduleEntry(system, phase, dependencies, entries.Count));
|
||||
}
|
||||
|
||||
public void Execute(TimeSpan delta)
|
||||
{
|
||||
if (entries.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, SystemScheduleEntry> byName = BuildNameIndex(entries);
|
||||
ValidateDependencies(byName, entries);
|
||||
|
||||
foreach (SystemSchedulePhase phase in Enum.GetValues<SystemSchedulePhase>())
|
||||
{
|
||||
ExecutePhase(phase, delta, byName);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, SystemScheduleEntry> BuildNameIndex(List<SystemScheduleEntry> allEntries)
|
||||
{
|
||||
var byName = new Dictionary<string, SystemScheduleEntry>(StringComparer.Ordinal);
|
||||
foreach (SystemScheduleEntry entry in allEntries)
|
||||
{
|
||||
if (byName.ContainsKey(entry.Name))
|
||||
{
|
||||
throw new InvalidOperationException($"Duplicate scheduled system name '{entry.Name}'.");
|
||||
}
|
||||
|
||||
byName.Add(entry.Name, entry);
|
||||
}
|
||||
|
||||
return byName;
|
||||
}
|
||||
|
||||
private void ValidateDependencies(Dictionary<string, SystemScheduleEntry> byName, List<SystemScheduleEntry> allEntries)
|
||||
{
|
||||
foreach (SystemScheduleEntry entry in allEntries)
|
||||
{
|
||||
foreach (string dependency in entry.DependsOn)
|
||||
{
|
||||
if (!byName.TryGetValue(dependency, out SystemScheduleEntry dependencyEntry))
|
||||
{
|
||||
if (options.StrictDependencyValidation)
|
||||
{
|
||||
throw new InvalidOperationException($"Scheduled system '{entry.Name}' depends on missing system '{dependency}'.");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dependencyEntry.Phase > entry.Phase)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Scheduled system '{entry.Name}' in phase {entry.Phase} cannot depend on later phase system '{dependency}' in phase {dependencyEntry.Phase}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecutePhase(SystemSchedulePhase phase, TimeSpan delta, Dictionary<string, SystemScheduleEntry> byName)
|
||||
{
|
||||
List<SystemScheduleEntry> phaseEntries = entries
|
||||
.Where(entry => entry.Phase == phase)
|
||||
.OrderBy(entry => entry.RegistrationIndex)
|
||||
.ToList();
|
||||
|
||||
if (phaseEntries.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var completed = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
while (completed.Count < phaseEntries.Count)
|
||||
{
|
||||
List<SystemScheduleEntry> ready = phaseEntries
|
||||
.Where(entry => !completed.Contains(entry.Name) && DependenciesSatisfied(entry, completed, byName))
|
||||
.OrderBy(entry => entry.RegistrationIndex)
|
||||
.ToList();
|
||||
|
||||
if (ready.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Cyclic or unsatisfied dependencies detected in phase {phase}.");
|
||||
}
|
||||
|
||||
List<SystemScheduleEntry> batch = BuildNonConflictingBatch(ready);
|
||||
ExecuteBatch(batch, delta, phase);
|
||||
|
||||
foreach (SystemScheduleEntry entry in batch)
|
||||
{
|
||||
completed.Add(entry.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool DependenciesSatisfied(SystemScheduleEntry entry, HashSet<string> completed, Dictionary<string, SystemScheduleEntry> byName)
|
||||
{
|
||||
foreach (string dependency in entry.DependsOn)
|
||||
{
|
||||
if (!byName.TryGetValue(dependency, out SystemScheduleEntry dependencyEntry))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dependencyEntry.Phase == entry.Phase && !completed.Contains(dependency))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<SystemScheduleEntry> BuildNonConflictingBatch(List<SystemScheduleEntry> ready)
|
||||
{
|
||||
var batch = new List<SystemScheduleEntry>(ready.Count);
|
||||
|
||||
for (int i = 0; i < ready.Count; i++)
|
||||
{
|
||||
SystemScheduleEntry candidate = ready[i];
|
||||
bool conflicts = false;
|
||||
|
||||
for (int j = 0; j < batch.Count; j++)
|
||||
{
|
||||
if (candidate.Access.ConflictsWith(batch[j].Access))
|
||||
{
|
||||
conflicts = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!conflicts)
|
||||
{
|
||||
batch.Add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
if (batch.Count == 0)
|
||||
{
|
||||
batch.Add(ready[0]);
|
||||
}
|
||||
|
||||
return batch;
|
||||
}
|
||||
|
||||
private void ExecuteBatch(List<SystemScheduleEntry> batch, TimeSpan delta, SystemSchedulePhase phase)
|
||||
{
|
||||
using ProfilerScope phaseScope = new($"Schedule.{phase}.Batch[{batch.Count}]");
|
||||
|
||||
if (options.EnableParallelSystemExecution && batch.Count > 1)
|
||||
{
|
||||
Parallel.ForEach(batch, entry => ExecuteSystem(entry, delta));
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < batch.Count; i++)
|
||||
{
|
||||
ExecuteSystem(batch[i], delta);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExecuteSystem(SystemScheduleEntry entry, TimeSpan delta)
|
||||
{
|
||||
using ProfilerScope scope = new(entry.Name);
|
||||
entry.System.Update(delta);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Nerfed.Runtime.Scheduling;
|
||||
|
||||
public sealed class SystemSchedulerOptions
|
||||
{
|
||||
public bool EnableParallelSystemExecution { get; set; }
|
||||
public bool StrictDependencyValidation { get; set; } = true;
|
||||
}
|
||||
Reference in New Issue
Block a user