10 Commits
Author SHA1 Message Date
EmilandClaude Sonnet 5 a77086b518 Docs: CI is green on both platforms — M4's literal "done when" is met
Build / build (windows-latest, win-x64) (push) Waiting to run
Build / build (ubuntu-latest, linux-x64) (push) Waiting to run
.github/workflows/build.yml just ran clean end to end on both
ubuntu-latest and windows-latest (build, full test suite, native shim
compilation, headless physics verification) after fixing the two real
cross-platform bugs its first run caught. Updating README/kernel-contract
to say so instead of "pending."

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 21:06:00 +03:00
EmilandClaude Sonnet 5 cd8f221ddd Fix real bugs from independent review (docs/review-handoff.md)
A second AI session read the whole codebase in parallel (read-only, no
code changes) and left a handoff doc. Addressed the correctness findings:

- PhysicsWorld.Sync's early return compared _bodies.Count to live.Count,
  not their contents — same size, different membership (destroy one
  tracked GameObject, gain one untracked-because-no-collider one; or any
  Restore where the scene has both a Rigidbody+collider object and a
  Rigidbody-without-one) skipped cleanup entirely, leaking the native
  Box3D body forever. Fixed by checking the actual stale set. Two new
  regression tests reproduce the review's own two scenarios via a new
  Lingua_GetBodyCount native export, asserting on the native table's own
  count rather than PhysicsWorld's C#-side bookkeeping.
- TryCreateBody stored handle -1 (native shim refused: invalid world, or
  its 8192-slot body table full) as if it were real — every later
  GetBodyTransform on it silently teleported the GameObject to the
  origin with a degenerate rotation, no error anywhere. Now checked and
  warned once, same as the missing-collider case.
- PluginHost.Load didn't roll back anything when Configure threw partway
  through: Schedule.Add/Events.Subscribe registrations it already made
  stayed forever, and its ALC was never unloaded — neither loaded (no
  _loaded entry) nor cleanly unloadable. Fixed with try/catch: best-effort
  Shutdown (the only thing that knows which services this plugin
  provided), RemoveAllFrom on both Schedule and EventBus, best-effort ALC
  unload, rethrow. New fixture plugin (sandbox.failing-configure, mirrors
  sandbox.echo's own real-load pattern) registers a system against a
  shared Ping component then throws, so FailedConfigureRollbackTests can
  assert the dangling system actually stops firing — an earlier version
  tried to prove this via AssemblyLoadContext.All instead, which passed
  even against the deliberately-reverted buggy code (the ALC turned out
  to get collected either way once its only references went out of
  scope); watching the dangling system is what actually distinguishes
  rolled-back from not, confirmed by deliberately reverting the fix and
  watching this specific test fail before restoring it.
- Engine.Host's "r <id>" left a plugin unloaded on a failed reload with
  no honest indication of that, and retrying threw "not loaded" instead
  of ever reaching Load again. Added PluginHost.IsLoaded so the handler
  only calls Unload when there's something to unload, and the failure
  message now says the plugin is unloaded, not just "failed."
- AssetService.ReloadWithRetry's `when (attempt < 4)` guard meant the
  5th and final IOException fell out of the loop and propagated from a
  discarded fire-and-forget Task — no log, no event, nothing. Now logged.
- EditorState.Selected kept pointing at a GameObject Restore had already
  destroyed after ExitPlay, so Inspector/gizmo would silently keep
  editing something no longer in the world. EditorPlugin.DrawUi now
  compares IsPlaying against its own previous frame (not just reacting to
  the Stop button) so this is caught whether Play was exited via the
  button or the "stop" stdin command — the same stdin-vs-real-control gap
  already hit once earlier this session — and re-resolves the selection
  by name.
- README.md and kernel-contract.md's Event Bus/Time rows had fallen a
  milestone behind (still said "no asset system yet" and "no fixed-step
  accumulator yet" after both shipped).

Full suite: 98 tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 20:57:08 +03:00
EmilandClaude Sonnet 5 5189bcd04f Close M3: update docs to match what actually shipped
docs/kernel-contract.md §5 described a design sketch — a fictional
SystemGroup.Play/Edit split, a "field-by-field clone" — written before
Play mode existed. Replaced it with what engine.editor actually does:
IWorld.Snapshot()/Restore() wrapped by IPlayModeController, and
Engine.Host gating Stage.Update on IsPlaying rather than the scheduler
knowing anything about system groups at all.

Also updates the "Frame stages" resolution to record Stage.Present (added
this milestone, not speculative) and the M3 row in the build-order table
to reflect everything actually built: the editor shell, reflection-based
Hierarchy/Inspector, Play/Stop, and the real camera-driven translate
gizmo. README.md's Status section gets the same M3 writeup M0-M2 already
had, in the same voice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 16:32:40 +03:00
EmilandClaude Sonnet 5 3c60e2cd45 Close the kernel's four open questions
Real decisions with real code behind them, not just answers written
into the doc:

- Time and Log: both stay in the kernel, both on IPluginContext.
  Log was already built this way by accident; Time (DeltaTime,
  ElapsedTime, FrameCount) ships now, split into ITime (plugin-facing,
  read-only) and Time (host-facing, an internal Tick(deltaTime) only
  Engine.Host calls) — the same split Schedule/ISchedule already
  established. The fixed-step accumulator from the original kernel
  scope is explicitly NOT included: nothing exists to test it against
  yet (no physics), so building it now would be exactly the kind of
  untested speculative machinery this project has avoided everywhere
  else. It arrives with M4, alongside the Stage.FixedUpdate it would
  drive — a "FixedUpdate" stage with no real fixed-timestep semantics
  behind it would be actively misleading, not just incomplete.

- Event Bus: real Publish/Subscribe/RemoveAllFrom, not events-as-
  World-entities (a worse fit for GameObject's persistent identity
  than for a disposable-entity pure ECS). Given a real consumer
  immediately rather than shipped as an unused API: PluginHost now
  publishes PluginLoaded/PluginUnloaded, and sandbox.echo subscribes
  to PluginLoaded for real, cleaning up in Shutdown() via
  Events.RemoveAllFrom — mirroring exactly how Schedule.RemoveAllFrom
  was proven. The 200-cycle leak test in AlcUnloadTests now exercises
  this cleanup path too, not just Schedule's; still green, stable
  across repeated runs. GameWorld does NOT auto-publish on every
  GameObject/Component change — a publish on every structural change
  would tax the hot path for listeners that usually don't exist.

- Frame stages: fixed, kernel-defined, not plugin-extensible — a
  stage is part of the shared vocabulary the host's loop and every
  plugin rely on. Set stays {Update, Render} until FixedUpdate earns
  its place alongside the accumulator in M4.

- Data-oriented fast path: left open on purpose, but with a trigger
  condition instead of a deadline — revisit when a concrete system
  (particles, the standing example) needs tens of thousands of
  GameObjects updated per frame AND profiling, not intuition, shows
  GameObject/Component overhead is the actual bottleneck.

PluginHost's constructor changed shape: takes EventBus (concrete, not
IEventBus — it needs RegisterPlugin, same reason it already took
Schedule instead of ISchedule) and ITime. NullEventBus is gone;
every call site now constructs a real EventBus. Engine.Host advances
Time each frame — real wall-clock delta in --windowed (via the
window's own Native.Time), a fixed nominal 1/60s in --headless, which
has no wall clock to measure and needs to stay deterministic anyway.

48 tests total now (40 in Engine.Kernel.Tests, 8 in
Engine.ConformanceHarness), all green on a clean build. Verified by
hand too: headless run against sandbox.echo now logs "[sandbox.echo]
observed load of 'sandbox.echo'" — the EventBus subscription actually
firing, not just compiling.

docs/kernel-contract.md's open-questions footer rewritten to record
each decision and why, plus the kernel scope table (§2) and the
IPluginContext listing (§3) updated to match what's actually built.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 04:57:38 +03:00
EmilandClaude Sonnet 5 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
2026-09-02 04:19:32 +03:00
EmilandClaude Sonnet 5 58e37a3482 Record Box3D as the physics engine decision
erincatto/box3d: pure C (no C++), MIT-licensed, ID-based handles —
verified via gh api rather than assumed, since this shapes the
engine.physics P/Invoke binding directly. Same "buy, don't build" call
already made for rendering, extended consistently in the scope risk
row and M4.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 01:01:31 +03:00
EmilandClaude Sonnet 5 f040f71045 Implement World: GameObject hierarchy, components, type-indexed queries
First real piece of M0 rather than scaffolding. GameWorld : IWorld owns
GameObject creation/destruction, a type-indexed Dictionary<Type,
HashSet<GameObject>> backing Query<T>(), and hierarchy bookkeeping
(roots list, parent/children, cycle rejection on SetParent).

Two corrections to the design doc found while implementing:

- WorldMatrix can't be cached on Transform as described — Transform is
  a plain struct with no reference to the hierarchy it would need to
  compose against. Moved to GameObject.WorldMatrix, computed from the
  parent chain on read; docs/kernel-contract.md and the §3 example
  updated (go.Transform.WorldMatrix -> go.WorldMatrix).
- The concrete World class collided with its own containing namespace
  (Engine.Kernel.World.World), which makes the bare name ambiguous for
  every consumer. Renamed to GameWorld; IWorld and the World folder/
  namespace are unaffected, and the doc never named the concrete class
  either way, so nothing there needed to change.

Also fixed a real correctness bug caught while writing World.Destroy:
a GameObject can carry more than one component of the same type, so
removing one from the type index has to check whether any others of
that type remain before dropping the GameObject from the index set —
tested directly (Query_Still_Finds_A_GameObject_With_A_Duplicate_
Component_After_One_Removal).

12 tests in Engine.Kernel.Tests cover creation, hierarchy (including
cycle rejection), component add/remove/query, destroy cascading
through a subtree, and WorldMatrix composition (verified against a
worked-through parent+child translation, not just asserted).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 00:33:18 +03:00
EmilandClaude Sonnet 5 ae6c4aba33 Final review pass: fix regressions, close a real spec gap
- Restore scheduler parallelism (dropped by accident in the ECS ->
  GameObject/Component rewrite; nothing about the object model
  actually prevents parallel execution of systems with disjoint
  declared access), plus the structural-change queuing that makes
  that safe.
- Rename leftover ECS-era `Entity` references to `GameObject`.
- Add per-project plugin configuration (project.json): the doc had no
  mechanism backing the "configurable per project" requirement, even
  though the README already claimed it.
- State the indie/small-team scope explicitly — it's the unstated
  premise behind several tradeoffs already in the doc (GC, GameObject
  over ECS, no custom RHI).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 00:19:19 +03:00
EmilandClaude Sonnet 5 03ca9c62b7 Switch World model from ECS to GameObject/Component
Match the more familiar Unity-style object model instead of a
struct-of-arrays ECS: GameObject hierarchy with type-indexed component
queries, Transform inlined as the one performance-motivated exception.
Data/behavior stay split the same way ECS required it, so hot-reload
safety is unaffected.

Also fixes an inconsistency in the previous reload sequence, which
described migrating live component data on a contract change even
though the Default ALC hosting contracts never unloads. Contract
changes now explicitly require an editor restart instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 00:12:36 +03:00
EmilandClaude Sonnet 5 043ff432e2 Initial commit: project scaffold and kernel contract v0
Lay out the design that everything else builds on: a small frozen
kernel plugins treat as a shared language, the plugin contract, the
two-assembly hot-reload model, and the build order through M4.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 00:04:01 +03:00