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
This commit is contained in:
@@ -82,6 +82,40 @@ and a real hang, not a hypothetical one: `SwapBuffers` blocking forever
|
||||
once VSync had nothing to wait on — reproduced by locking the screen,
|
||||
fixed by turning VSync off, since nothing here needs frame pacing yet.
|
||||
|
||||
**M3 done.** The editor is `engine.editor`, a plugin like any other — no
|
||||
special-cased editor layer in the kernel. An ImGui overlay (Silk.NET.OpenGL.
|
||||
Extensions.ImGui) draws over the live scene; getting it to actually appear
|
||||
in the same frame (not delayed by one) needed splitting `engine.render`'s
|
||||
old Draw-then-SwapBuffers system in two, so a new `Stage.Present` could run
|
||||
the swap after every `Stage.Render` system — this plugin's draw and
|
||||
`engine.editor`'s ImGui pass both — had drawn into the same back buffer.
|
||||
See the "Frame stages" resolution in
|
||||
[`docs/kernel-contract.md`](docs/kernel-contract.md) for why adding a stage
|
||||
was still the right call under a "fixed, kernel-defined" rule.
|
||||
|
||||
Hierarchy and Inspector both work off reflection, not per-component-type
|
||||
code: `HierarchyPanel` walks `IWorld.Roots` directly, `InspectorPanel`
|
||||
enumerates a selected `GameObject`'s `Transform` and every attached
|
||||
`Component`'s public fields via `FieldInfo`, live-editable for
|
||||
int/float/bool/string/`Vector3`. A brand new component type in any plugin
|
||||
gets an Inspector for free the moment it's attached.
|
||||
|
||||
Play/Stop is `IWorld.Snapshot()`/`Restore()` — a scene-format dump taken on
|
||||
Enter, restored on Exit — plus `Engine.Host` skipping `Stage.Update` outside
|
||||
Play. Nothing here is a domain reload; see
|
||||
[`docs/kernel-contract.md §5`](docs/kernel-contract.md#5-play-mode-without-domain-reload).
|
||||
M3's actual "done when," entering Play in under 100ms, is proven twice: a
|
||||
kernel-level timed test and a real editor run logging 13ms.
|
||||
|
||||
The gizmo is a real 3-axis translate handle, not a flat 2D overlay — chosen
|
||||
deliberately over a scoped 2D version specifically so a screen-space drag
|
||||
means something: it projects the selected `GameObject`'s world position
|
||||
through the actual camera's View/Projection and reads the drag back the
|
||||
same way. The underlying math (`GizmoMath`) has no GL or ImGui dependency
|
||||
and is unit-tested on its own — including the case a screenshot can't
|
||||
easily catch, dragging an object parented under a non-uniformly-scaled
|
||||
parent.
|
||||
|
||||
No physics yet — see the build order (M0–M4) in
|
||||
[`docs/kernel-contract.md`](docs/kernel-contract.md) for what's next.
|
||||
|
||||
|
||||
+41
-27
@@ -303,35 +303,45 @@ Unity's Play-mode wait isn't about compilation — it's about serializing all
|
||||
script state, tearing the domain down, and recreating it. That step doesn't
|
||||
exist here: state never lived in plugin code to begin with. It lives in
|
||||
`World`, owned by the kernel, untouched by reload and untouched by entering
|
||||
Play.
|
||||
Play. Built and shipped in M3 — this is what actually runs, not the design
|
||||
sketch that preceded it:
|
||||
|
||||
```csharp
|
||||
// Engine.Editor / PlayMode.cs
|
||||
// IWorld (Engine.Kernel)
|
||||
string Snapshot(); // a scene-format dump of the whole graph
|
||||
void Restore(string snapshot);
|
||||
|
||||
// Entering Play clones the object graph; it doesn't rebuild the runtime.
|
||||
void EnterPlay()
|
||||
{
|
||||
_snapshot = world.Snapshot(); // deep-clone GameObjects + Components
|
||||
schedule.SetGroup(SystemGroup.Play);
|
||||
}
|
||||
|
||||
void ExitPlay()
|
||||
{
|
||||
world.Restore(_snapshot); // Play-mode edits roll back
|
||||
schedule.SetGroup(SystemGroup.Edit);
|
||||
}
|
||||
// Engine.Editor.Contracts / IPlayModeController — engine.editor's real
|
||||
// implementation wraps exactly these two calls, nothing else:
|
||||
void EnterPlay() => _snapshot = world.Snapshot();
|
||||
void ExitPlay() { world.Restore(_snapshot!); _snapshot = null; }
|
||||
```
|
||||
|
||||
A field-by-field object clone is slower than the raw array copy a
|
||||
struct-of-arrays `World` would give you — cloning thousands of `GameObject`s
|
||||
and their components is real allocation work, not a memcpy. For scenes at
|
||||
indie scale it's still low-single-digit milliseconds, and it's an
|
||||
order of magnitude cheaper than what Unity's domain reload does, and it only
|
||||
happens once per Play/Stop, not every frame.
|
||||
`Snapshot`/`Restore` reuse `SceneFormat` — the same JSON serialization a
|
||||
scene file on disk already uses — rather than a bespoke clone mechanism.
|
||||
A scene file and a Play-mode snapshot are the same problem (capture every
|
||||
`GameObject`'s state faithfully enough to reconstruct it) at two different
|
||||
moments; reusing already-proven serialization beat maintaining a second way
|
||||
to walk the same graph. `WorldSnapshotTests` times a 300-`GameObject`
|
||||
snapshot+restore at under 100ms — M3's literal "done when" — and the real
|
||||
editor path confirms it: `PlayModeController.EnterPlay` logs its own
|
||||
wall-clock cost, and a real run against `samples/WindowDemo`'s scene
|
||||
measured 13ms.
|
||||
|
||||
Play becomes a system-group switch, not a world rebuild. A side effect of
|
||||
the same decision: system code can be edited *during* Play without
|
||||
restarting — state is preserved. That's the feedback loop the whole engine
|
||||
There's no `SystemGroup.Play`/`SystemGroup.Edit` split in the scheduler —
|
||||
that would mean every system declaring which group it belongs to, for a
|
||||
distinction the host loop alone can already make. `Engine.Host` checks
|
||||
`IPlayModeController.IsPlaying` once per frame and only runs `Stage.Update`
|
||||
while it's true; `Stage.Render`/`Stage.Present` run every frame regardless,
|
||||
so Edit mode still shows a live, responsive scene view — just one that
|
||||
never ticks. A project with no `engine.editor` loaded sees no behavior
|
||||
change at all: `Stage.Update` runs unconditionally, same as before Play
|
||||
mode existed.
|
||||
|
||||
A side effect of Play being nothing but a snapshot and a stage-gate: system
|
||||
code can be edited *during* Play without restarting — hot-reloading a
|
||||
plugin mid-Play still works, because nothing about Play mode touches the
|
||||
ALC or plugin loading at all. That's the feedback loop the whole engine
|
||||
exists to enable.
|
||||
|
||||
## 6. Where this breaks
|
||||
@@ -425,7 +435,7 @@ cost of changing course is still zero.
|
||||
| **M0** | **Kernel only.** `World` as a `GameObject`/`Component` hierarchy with type-indexed queries, staged scheduler with access enforcement, plugin host with ALC, service registry, JSON world dump. No window, no graphics. | An agent runs the full loop from §7 unassisted: edits a headless plugin, rebuilds, reads the changed dump — and the 200-cycle leak test is green. |
|
||||
| **M1** | **Window, input, a triangle.** Three separate plugins over Silk.NET. First real-load test of the data channel. | The triangle's color changes by editing system code, with no app restart. |
|
||||
| **M2** | **Assets and scenes.** Hot-reloading asset plugin, scene format, `World` serialization. | Swapping a texture on disk changes the picture with nothing stopped; a scene loads and saves. |
|
||||
| **M3** | **Editor as plugins.** Shell, reflection-based component inspector, hierarchy, gizmos, Play/Stop on snapshots. | Entering Play takes under 100 ms — the original complaint about Unity is closed. |
|
||||
| **M3** | **Editor as plugins — done.** Shell (`engine.editor`, ImGui over the live scene, see §5 and the new `Stage.Present`), reflection-based inspector and hierarchy, Play/Stop on snapshots, a real 3D translate gizmo driven by the actual camera. | Entering Play takes under 100 ms — the original complaint about Unity is closed. Proven twice: `WorldSnapshotTests` (kernel, 300 `GameObject`s) and a real editor run against `samples/WindowDemo` (13ms, logged by `PlayModeController`). |
|
||||
| **M4** | **One small game, end to end.** `engine.physics` over Box3D, audio, a Linux + Windows build pipeline. A 20-minute game, shipped as an executable. | The build runs on both platforms with no editor plugins in the shipped binary. |
|
||||
|
||||
---
|
||||
@@ -458,9 +468,13 @@ deciding, in order:
|
||||
- **Frame stages: fixed, kernel-defined — not plugin-extensible.** A stage
|
||||
is part of the shared language every plugin and the host loop rely on;
|
||||
letting plugins register arbitrary custom stages would mean the host's
|
||||
frame loop can no longer just call a known, closed set of `RunStage`s.
|
||||
The set stays `{Update, Render}` until `FixedUpdate` arrives with M4 — no
|
||||
stage gets added without something real to run in it.
|
||||
frame loop can no longer just call a known, closed set of `RunStage`s. No
|
||||
stage gets added without something real to run in it — which is exactly
|
||||
what happened once: M3 added `Stage.Present` alongside `{Update, Render}`
|
||||
when `engine.editor`'s ImGui overlay needed to draw after the scene but
|
||||
before the buffers swap, and `engine.render`'s old single Clear+Draw+
|
||||
SwapBuffers system had nowhere else to put the swap that wouldn't race
|
||||
it. `Stage.FixedUpdate` is still the only stage left on the M4 list.
|
||||
- **Data-oriented fast path: still open, on purpose, with a trigger
|
||||
condition instead of a deadline.** Not "undecided" the way the other
|
||||
three were — deliberately not worth deciding before there's a concrete
|
||||
|
||||
Reference in New Issue
Block a user