Files

189 lines
5.9 KiB
C#

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);
}
}