f6d9ea896f921de6244f214a2bd2341e2cd4c323
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f6d9ea896f |
Close M1: real triangle, engine.input, screenshot-to-file capture
All three plugins M1 called for now exist over Silk.NET, and the milestone's actual claim is proven against a live GL context, not just argued: edit a plugin's code, rebuild just it, reload it while a real window stays open, see the change with no app restart. Verified directly this time — two PNGs of the same running window, before and after a live reload (orange triangle -> green, same process, same window, same GL context throughout) — not by analogy to the earlier clear-color version. engine.render, upgraded from clear-color to real geometry: - Vertex/fragment shaders compiled and linked at Configure() time, with real error checking (GetShader/GetProgram *Status + InfoLog on failure) rather than trusting hand-typed GLSL to just work. - VAO/VBO for a hardcoded triangle; Shutdown() deletes all three GL objects rather than leaking them across reloads. - unsafe confined to Configure() (VertexAttribPointer takes a raw offset pointer) via a narrow, documented override of Directory.Build.props' default — native graphics interop, not the kernel data structures docs/kernel-contract.md §7 was written against. engine.input: publishes IEngineInput (keyboard state) via Silk.NET.Input. Reuses Silk.NET's own Key enum rather than inventing one, same call as IEngineWindow.Native. Loads and constructs cleanly against a real window; nothing reacts to it yet since there's no gameplay code to. IScreenCapture (new, engine.render): reads the frame back via ReadPixels and writes a PNG. No SixLabors.ImageSharp — checked its license first and it isn't MIT/Apache (revenue-gated), which would have been a real surprise for downstream users of an MIT engine. PngWriter is a from-scratch encoder instead: ZLibStream (BCL, .NET 6+) for the one genuinely hard part, a correctly zlib-wrapped DEFLATE stream; chunk framing and CRC32 are small enough to get right and to verify by actually decoding files this wrote (done repeatedly, by hand, across this session). Engine.Host: "screenshot <path>" joins "r <plugin-id>" as a live stdin command, plus a non-interactive --screenshot/--screenshot-after- frames pair that captures once and exits — for scripts and agents that can't easily hold a pipe open into a long-running process. Real bug found and fixed in PluginHost, not specific to any one environment: loading a Contracts assembly into the Default ALC never set up resolution for ITS OWN dependencies. engine.windowing's and engine.render's Contracts both need Silk.NET packages and loaded fine anyway, by accident — Engine.Host references those two Contracts projects directly (to drive the windowed loop and screenshot capture), so their dependencies were already sitting in Engine.Host's own output directory. engine.input's Contracts has no such lucky coincidence and failed with a real FileNotFoundException. Fixed by hooking AssemblyLoadContext.Default.Resolving with an AssemblyDependencyResolver per loaded Contracts path — mirroring what PluginLoadContext already does for collectible ALCs, applied to the one path that never had it. Hooked once per process via a static list/flag, not per PluginHost instance, specifically to avoid a PluginHost instance becoming unreclaimable through its own event subscription. Not covered by automated tests, deliberately, same reasoning as the windowing/render pass: opening a real window and reading back a real framebuffer both need a real display. Verified by hand instead, documented above and in commit history rather than asserted. README status: M1 done, not "in progress." Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N |
||
|
|
c5d3807a0f |
M1 (in progress): engine.windowing + engine.render, hot-reload proven live
The first plugins with a real external dependency (Silk.NET) and the first that need a display. Both built, both verified by hand against a real window and a live GL context — not just "compiles." engine.windowing: - IEngineWindow, not IWindow — Silk.NET's own windowing type already owns that name; same lesson as the kernel's World -> GameWorld rename, applied proactively this time instead of hitting the build error first. docs/kernel-contract.md's §3 illustrative example updated to match. - Exposes the real Silk.NET IWindow directly (IEngineWindow.Native) rather than re-wrapping it — GL context creation and event pumping both need it, and hiding it buys nothing yet. engine.render: - Minimal: glClear + SwapBuffers against a hardcoded color, no mesh. Enough to prove M1's actual claim, which has nothing to do with triangles specifically: edit a plugin, rebuild just it, reload it while a real window stays open, see the change with no app restart. - No Contracts assembly — PluginManifest.Contracts is now nullable rather than forcing an empty assembly into existence just to satisfy the schema; PluginHost.Load skips the Default-ALC step when absent. Engine.Host: - --windowed alongside --headless: pumps window events plus Stage.Update/Stage.Render each frame instead of a bounded --frames loop. References Engine.Windowing.Contracts directly (never the implementation) to know how to drive that loop — same "Contracts are safe to share" pattern already proven for Sandbox.Echo.Contracts. - New: typing "r <plugin-id>" + Enter reloads that plugin live. Not a file watcher (still not built), but real Unload+Load through the same PluginHost path, against a running window — this is what actually exercised the hot-reload claim below. - IServiceRegistry gained TryRequire<T> so Engine.Host can ask "is a window available" without treating its absence as an error; a plugin's own Configure()/Shutdown() should keep using Require(). Verified end to end by hand: opened the window, watched it render its hardcoded color, edited RenderPlugin.cs's ClearColor, rebuilt only that project, typed "r engine.render" into the running process, and watched the color change with the same window and GL context still alive. Also found and documented a real platform gotcha along the way: on Wayland (unlike X11), a window with no committed buffer isn't shown at all, not even as a black rectangle — engine.windowing alone produces an invisible window; engine.render's first Clear+SwapBuffers is what actually makes it appear. Noted directly on RenderPlugin. Not covered by automated tests, deliberately: opening a real window needs a real display, which isn't safe to assume of every environment this runs in. ServiceRegistry.TryRequire<T> and the null-Contracts path in PluginHost are unit-tested; the window/render/reload flow is described here and was checked by hand instead. README status updated: M1 in progress, not done — engine.input and an actual drawn triangle (vs. a clear color) are still open. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N |
||
|
|
246b969744 |
Close M0: headless CLI, JSON world dump, project-level plugin loading
The last piece of the agent loop from docs/kernel-contract.md §7:
- WorldDumper serializes a World to JSON — every GameObject, its
transform, its components (arbitrary plugin-defined classes, so this
leans on System.Text.Json's own reflection with IncludeFields=true,
since components are public fields, not properties). Components are
a list of {type, data}, not a dictionary keyed by type name:
AddComponent<T>() doesn't enforce uniqueness, so a GameObject can
carry two components of the same type, and a dictionary would throw
on exactly that case — tested directly.
- PluginHost.LoadProject reads a project.json and resolves each
referenced plugin id against engine + project-local search paths,
finally putting ProjectManifest/PluginReference to use — they'd sat
unused since the very first scaffold commit.
- Engine.Host is a real CLI now: `engine run --headless --plugins <dir>
--project <project.json> --frames <n> [--dump <path>]`. --scene and
--assert are explicitly rejected with a message pointing at why
(no scene format yet — that's M2; no query DSL was ever actually
specified for --assert, and jq over a plain JSON dump already covers
that need), rather than silently ignored or generically rejected.
Same for `engine diag why-pinned`: nothing has ever failed to unload
in testing, so there's nothing to build that against yet.
- EchoPlugin now seeds one Ping-bearing GameObject in Configure() —
sandbox.echo is documented as a test fixture, not a real subsystem,
and there's no scene format yet to seed content any other way.
Verified by hand, not just by unit tests, since Program.cs itself
isn't covered by any: assembled a real flat plugin directory
(plugin.json + both built DLLs) and actually ran the CLI against
sandbox.echo end to end — 3 frames in, Ping.Count came back 3 in the
dump. Also checked every "not implemented" path (--scene, `diag`,
missing required args) prints its intended message rather than a
generic error.
32 tests total now (26 in Engine.Kernel.Tests, 6 in
Engine.ConformanceHarness), all green on a clean build.
README's Status section updated — M0 was the whole reason this
project exists (fast iteration without Unity's domain reload), and
that claim is no longer just architecture on paper.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
|
||
|
|
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 |