17f66c4de564f24c820e8b90e356823af16a8598
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
17f66c4de5 |
Implement Scheduler: stage execution, conflict batching, debug enforcement
Schedule gains real execution on top of the registration bookkeeping
from the PluginHost pass: RunStage(stage, world) runs every system
registered for a stage, and Reads<>/Writes<>() declarations are
enforced live via SystemAccessScope — a system touching a component it
didn't declare throws immediately, with a message naming the
violation, from GameWorld.Query<T>() and GameObject.GetComponent<T>()/
AddComponent<T>()/RemoveComponent<T>(). Outside of a running system
(editor code, tests, scene construction) nothing is enforced.
Scope cut made deliberately, not by accident: systems are grouped into
conflict-free batches by declared access (ComputeBatches, tested
directly), but batches run sequentially rather than on real threads.
Actually parallelizing them needs GameWorld's structural changes
(Create/Destroy/AddComponent/RemoveComponent) deferred to a command
buffer first — without that, two systems with disjoint *declared*
types can still race on shared storage, since AddComponent<T>() on a
GameObject mutates that object's own component list regardless of T.
Building real concurrency on top of a known thread-safety hole would
be worse than not building it yet. Noted as a TODO on RunStage.
Two real bugs found and fixed while wiring this up, not designed in
from the start:
- ISchedule.Add took `Delegate`, and a lambda passed there doesn't
reliably compile down to `Action<IWorld>` at runtime — the
compiler's natural-type inference for lambdas (as opposed to method
groups, which do work this way) can synthesize a different, private
delegate type instead, so `is Action<IWorld>` silently failed for
every lambda-registered system. Changed Add's parameter type to
Action<IWorld> directly, which sidesteps the inference question
entirely — found by ScheduleTests actually using lambdas, which
EchoPlugin's method-group-based Tick had been masking.
- AlcUnloadTests started failing intermittently ("ALC survived unload
cycle 3") once PluginSystemTests existed alongside it — xUnit
parallelizes across test classes by default, and ALC-unload tests
are sensitive to any concurrent activity in the process. Added
[CollectionBehavior(DisableTestParallelization = true)] to the
harness assembly; stable across 5+ repeated runs since.
EchoPlugin.Tick is no longer a stub — it increments every Ping.Count
in World, which two new integration tests in
Engine.ConformanceHarness/PluginSystemTests.cs exercise end to end: a
plugin loaded from a real collectible ALC registers a system, Schedule
actually invokes that cross-ALC delegate, and it correctly mutates a
component owned by the Default-ALC World. This only works because
PluginLoadContext resolves Sandbox.Echo.Contracts to the copy this
test project references directly, rather than loading a second,
type-incompatible one — the harness's new normal ProjectReference to
Sandbox.Echo.Contracts.csproj makes that a live assertion, not just an
implementation detail no test would notice breaking.
27 tests total now (21 in Engine.Kernel.Tests, 6 in
Engine.ConformanceHarness), all green on a clean build, harness
verified stable across repeated runs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
|
||
|
|
978f34727f |
Implement PluginHost: two-ALC plugin loading, unloading verified live
The centerpiece of the whole "no domain reload" claim, now proven empirically rather than argued on paper: 200 load/unload cycles against a real plugin (sandbox.echo), each one checked with a WeakReference that the collectible ALC actually collected — docs/kernel-contract.md §4's leak test, previously Skip-marked since the very first scaffold, now runs and passes (stable across repeated runs). New pieces, minimal by design: - PluginLoadContext: the collectible ALC a plugin's implementation loads into. Load() defers to whatever's already in the Default ALC (Engine.Kernel, the plugin's own Contracts assembly) before consulting AssemblyDependencyResolver for genuinely private dependencies — the standard .NET plugin pattern, needed so component types stay identical across the plugin boundary instead of loading as two distinct, incompatible copies. - PluginHost: reads plugin.json, loads Contracts into the Default ALC (once — verified directly, not just inferred from the leak test), loads the implementation into a fresh PluginLoadContext, finds the IPlugin type via reflection, calls Configure(). Unload() calls Shutdown() first, then .Unload()s the ALC and hands back a WeakReference for the caller to check. - Schedule, ServiceRegistry, NullEventBus, ConsoleLogger: minimal real implementations of the remaining IPluginContext pieces — no stage execution or parallelism in Schedule yet, that's separate Scheduler work. Schedule.RemoveAllFrom is the one piece that has to be correct now, not later: it's what lets a plugin's Shutdown() actually drop the delegate reference into its own collectible ALC, which is exactly what the leak test is checking end to end. Explicitly out of scope for this pass: resolving a project's or plugin's dependsOn graph to order loading across multiple plugins. Nothing to test that against yet — sandbox.echo is deliberately the only, dependency-free fixture. Noted as a TODO on PluginHost rather than built speculatively. Sandbox.Echo.csproj gets <EnableDynamicLoading>true</EnableDynamicLoading> (future plugins with real dependencies will need the deps.json this generates). Engine.ConformanceHarness.csproj now copies plugin.json and both built DLLs into one flat directory under its own output, matching the layout PluginHost.Load(directory) expects — via $(Configuration)/ $(TargetFramework)-aware CopyToOutputDirectory items, correct in Release too, not just Debug. 17 tests total now (13 in Engine.Kernel.Tests, 4 in Engine.ConformanceHarness), all passing on a clean build. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N |
||
|
|
1459657408 |
Scaffold the .sln and M0 project structure
Buildable skeleton matching docs/kernel-contract.md: - src/Engine.Kernel — the frozen kernel. Interfaces and data types only (World, Component, GameObject, Transform, ISchedule, IServiceRegistry, IEventBus, ILogger, IPlugin/IPluginContext, plugin.json and project.json manifest models). No PluginHost/Scheduler/World implementation yet — that's M0's actual work, not scaffolding. - src/Engine.Host — the CLI runtime entry point, placeholder for now. - plugins/sandbox.echo — a minimal two-assembly plugin (Contracts in Default ALC, implementation in collectible ALC) that exists only to exercise the reload loop end to end once PluginHost exists. - tests/Engine.Kernel.Tests, tests/Engine.ConformanceHarness — wired up with one Skip-marked placeholder test each, naming what M0 needs to make them real (including the 200-cycle ALC leak test from §4). The harness references the sandbox plugin with ReferenceOutputAssembly="false" so it loads it dynamically by path instead of linking its types into its own Default ALC. - samples/EmptyProject — a starter project.json. - Directory.Build.props centralizes shared settings across every project, including AllowUnsafeBlocks=false to enforce the §7 rule at the build level rather than by convention. Solution builds clean and `dotnet test` runs both placeholders as Skipped (not failing) — confirms the wiring, not the engine. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N |