using System.Reflection;
using Engine.Kernel.World;
namespace Engine.Kernel.Scheduling;
///
/// Registration plus execution. See docs/kernel-contract.md §2 and §7.
///
/// Ownership is tracked by the registering delegate's declaring assembly,
/// not by a string tag on each entry — a plugin can only accidentally
/// mislabel a system if it hands another plugin's delegate to Add(), which
/// isn't a realistic failure mode. See .
///
public sealed class Schedule : ISchedule
{
private readonly List _systems = [];
private readonly Dictionary _pluginAssemblies = [];
/// Called by PluginHost right after loading a plugin's
/// implementation assembly, before Configure() runs.
internal void RegisterPlugin(string pluginId, Assembly implementationAssembly)
=> _pluginAssemblies[pluginId] = implementationAssembly;
public ISystemBuilder Add(Stage stage, Action system)
{
var entry = new SystemEntry(stage, system);
_systems.Add(entry);
return new SystemBuilder(entry);
}
public void RemoveAllFrom(string pluginId)
{
// Removing the assembly mapping here too, not just the systems —
// leaving it behind would itself be a stray reference into the
// ALC the caller is about to unload.
if (_pluginAssemblies.Remove(pluginId, out var assembly))
_systems.RemoveAll(e => e.System.Method.DeclaringType?.Assembly == assembly);
}
///
/// Runs every system registered for , grouped
/// into conflict-free batches by declared Reads/Writes (see
/// ) and, in DEBUG builds, enforced against
/// what each one actually touches (see SystemAccessScope).
///
/// TODO: batches are computed but run sequentially, not on separate
/// threads — real parallel dispatch needs structural changes
/// (Create/Destroy/AddComponent/RemoveComponent) deferred to a command
/// buffer flushed after the batch first. Without that, two systems with
/// disjoint *declared* types can still race on shared, non-thread-safe
/// storage: AddComponent<T>() on a GameObject mutates that
/// GameObject's own component list regardless of T, and GameWorld's
/// index/roots collections aren't safe for concurrent mutation either.
/// See the Scheduler row in docs/kernel-contract.md §2.
///
public void RunStage(Stage stage, IWorld world)
{
var stageSystems = _systems.Where(e => e.Stage == stage).ToList();
foreach (var batch in ComputeBatches(stageSystems))
{
foreach (var entry in batch)
Invoke(entry, world);
}
}
private static void Invoke(SystemEntry entry, IWorld world)
{
using var _ = SystemAccessScope.Enter(entry.Reads, entry.Writes);
entry.System(world);
}
///
/// Groups systems into the fewest sequential batches such that no two
/// systems in the same batch conflict — greedy first-fit, preserving
/// registration order. Not yet consumed for real parallelism (see the
/// TODO on RunStage), but exercised directly by ScheduleTests so the
/// conflict logic itself is validated independent of that.
///
internal static List> ComputeBatches(IReadOnlyList systems)
{
var batches = new List>();
foreach (var entry in systems)
{
var batch = batches.FirstOrDefault(b => b.TrueForAll(other => !Conflicts(entry, other)));
if (batch is not null)
batch.Add(entry);
else
batches.Add([entry]);
}
return batches;
}
private static bool Conflicts(SystemEntry a, SystemEntry b) =>
a.Writes.Overlaps(b.Reads) || a.Writes.Overlaps(b.Writes) || b.Writes.Overlaps(a.Reads);
}