6 Commits
Author SHA1 Message Date
EmilandClaude Sonnet 5 faabfc2cb4 M4: Linux + Windows CI build pipeline (.github/workflows/build.yml)
The literal thing nothing on this dev machine can verify: whether the
build actually runs on Windows at all. Solved by having real CI do it —
ubuntu-latest and windows-latest both build the native Box3D/miniaudio
shims from source via CMake, build and test the full .NET solution, stage
a shippable configuration (engine.windowing/assets/render/input/physics/
audio + physics-demo-game — engine.editor deliberately excluded, that's
M4's actual "done when"), and then run it headless against samples/
PhysicsDemo for 200 frames, asserting DemoBox settled at y≈1.0 in the
resulting dump. Real physics, real scene load, real plugin loading,
proven on both platforms, not just built.

Required restructuring the native Content items into per-OS ItemGroups
(native/linux-x64/ vs native/win-x64/, selected via
$([MSBuild]::IsOSPlatform(...))): linux-x64's .so is committed (built and
verified here); win-x64's .dll is never committed — nothing here can
build or run one to verify — and only ever exists as something the
Windows job produces fresh, in-place, right before `dotnet build`.

Caught one real bug dry-running this exact staging locally before
trusting it to a workflow run: PluginHost.Load calls Assembly.
LoadFromAssemblyPath, which throws on a relative path — the verification
step's `--plugins ../../plugins` failed immediately with "is not an
absolute path." Every manual verification earlier this session happened
to always pass an absolute --plugins path, which is exactly why this
never surfaced before. Fixed by resolving to an absolute path before
invoking Engine.Host.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 17:52:28 +03:00
EmilandClaude Sonnet 5 2d0168104d M4: PhysicsDemo — the "one small game, end to end"
samples/PhysicsDemo ties physics, audio, input, and rendering together
through nothing but the kernel's own vocabulary — no plugin here
references another plugin's implementation, only Contracts. Press Space
to drop a box; engine.physics simulates it falling onto a static ground;
CubeRenderer (new — engine.render's QuadRenderer drew flat cards, no good
for a physics demo where a BoxCollider needs to actually look like a box)
draws it; physics-demo-game watches each spawned box's own Y velocity via
IPhysicsService and plays a bounce sound via IAudioService the moment a
real fall settles. A looping ambient track plays throughout via
AudioSource's own PlayOnAwake. project.json deliberately excludes
engine.editor — this is the shippable configuration M4's "done when"
actually asks for, not the dev one.

"Landed" isn't a Box3D contact event — the native shim never exposed one
(nothing but scalars crosses that boundary, see lingua_physics.c). Watching
velocity every frame is the honest, right-sized alternative for a demo
this size, not a shortcut around missing infrastructure.

Verified two ways. First, real Space-key input isn't simulable here (no
xdotool/ydotool under this Wayland session) — so PhysicsDemoGame.Tests
drives the actual PhysicsDemoGamePlugin.Configure/Tick through the real
Schedule with a controllable fake IEngineInput (and fake IPhysicsService/
IAudioService, since engine.physics/engine.audio's own correctness is
already covered elsewhere): spawn-on-press with edge detection, the
Rigidbody/BoxCollider/CubeRenderer combo the spawned box actually gets,
and — the case most likely to be subtly wrong — a box that never actually
falls doesn't false-positive as "landed," only one that fell past
FallingThreshold and then settled does, exactly once. All 5 passed
immediately. Second, a real windowed run: a box placed in scene.json above
the ground visibly falls and settles onto it on screen after a real
few-second wait — screenshotting by --screenshot-after-frames alone turned
out not to prove this (VSync is off, so frames race by far faster than
real physics time passes; enough elapsed frames isn't enough elapsed
seconds), an interactive run with a real sleep before the screenshot
command is what actually shows it.

Full suite: 92 tests. Remaining for M4: the Linux + Windows build pipeline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 17:47:15 +03:00
EmilandClaude Sonnet 5 c6d9ff1a26 Add scripts/stage-plugins.sh and run-sample.sh for running the engine locally
Contracts and Implementation build into two separate bin/ folders, so
there's nowhere valid to point --plugins at without a staging step first
— every manual verification this session has done that staging by hand.
stage-plugins.sh does it for every plugin in one pass; run-sample.sh
stages then runs samples/WindowDemo windowed, forwarding extra args to
`engine run` (e.g. --screenshot).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 17:16:24 +03:00
EmilandClaude Sonnet 5 792b626396 M4: engine.physics — Box3D, over a narrow, verified P/Invoke shim
native/physics-native/lingua_physics.c wraps Box3D at a specific pinned
commit (47d7f7c — Box3D has no 1.0 release yet, and its only tag, v0.1.0,
already diverges from this API, confirmed by diffing headers rather than
assuming). The wrapper is deliberately narrow: Box3D's own b3WorldDef/
b3BodyDef/b3ShapeDef are large structs with function pointers, and
b3BoxHull's own doc comment says it "has data hanging off the end and
cannot be directly copied" — none of that crosses the P/Invoke boundary.
Every exported Lingua_* function takes and returns only plain int32/float/
bool scalars, and handles are this shim's own array-index handles, not
Box3D's id structs. C# binds them with classic DllImport rather than the
newer LibraryImport specifically because LibraryImport's generated
marshalling needs AllowUnsafeBlocks even for an all-scalar signature like
every one of these — DllImport needs none, keeping this plugin inside the
kernel's "no unsafe in the v1 hot path" rule with no exception required.

engine.physics adds Rigidbody/BoxCollider/SphereCollider components and
PhysicsWorld, which diffs Query<Rigidbody>() against its own tracked set
every Stage.FixedUpdate (there's no destruction event to hook) to create
and destroy native bodies, steps Box3D once per invocation — Engine.Host's
accumulator decides how many times that runs per frame, not this plugin —
and writes each body's resulting transform back to GameObject.Transform.
IPhysicsService exposes ApplyLinearImpulse/Get/SetLinearVelocity for
gameplay code.

Verified twice: a standalone C smoke test against the native shim alone
(a box dropped from y=5 onto a static ground settles at y≈1.0, exactly
where the two half-heights sum to), and the full pipeline through Engine.
Host — a headless run with a real scene, --dump showing the same box
settling at y=0.9999 after physics, scene load, and Stage.FixedUpdate all
went through the real kernel. 5 new automated tests in Engine.Physics.
Tests cover the same settling behavior for both shapes, a missing-collider
warning that fires once and doesn't throw, cleanup after a GameObject is
destroyed mid-simulation, and that an applied impulse actually changes
velocity — all passed on the first run.

Physics-enabled GameObjects must be root-level for now: PhysicsWorld
writes Box3D's world-space transform straight into LocalPosition/
LocalRotation, correct only when local and world space are the same
thing. A parented rigidbody needs the same parent-WorldMatrix-inverse
handling GizmoMath.WorldToLocalPosition already does for the gizmo — real,
not-yet-done work, not silently wrong.

Full suite: 81 tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 17:05:39 +03:00
EmilandClaude Sonnet 5 d7fcc20f5e M3: Play/Stop — Engine.Host stops ticking Stage.Update outside Play mode
IPlayModeController (Engine.Editor.Contracts) wraps IWorld.Snapshot()/
Restore() as EnterPlay/ExitPlay — the entire mechanism is that one
snapshot, per Snapshot's own doc comment. Exposed as a service, not just
something engine.editor's UI calls directly, so a future non-UI driver
(a test harness, a headless "play for N frames" CLI command) can drive
Play mode without depending on ImGui.

Engine.Host now looks up IPlayModeController once and checks IsPlaying
every frame before running Stage.Update: Edit mode still renders the
scene (so the view isn't frozen and the editor UI stays responsive) but
never ticks it, same distinction Unity draws between its Scene and Game
views. A project with no engine.editor loaded sees no behavior change —
Update runs unconditionally, same as before this existed.

Added "play"/"stop" as stdin commands alongside the existing "r" and
"screenshot", both real controls (not just test scaffolding) for driving
Play mode without a mouse — which is also how this got verified: an
interactive run sent "play", screenshotted, sent "stop", and the log
shows "Entered Play mode in 13.2ms", the real editor path exercising the
same under-100ms budget WorldSnapshotTests already proves at the kernel
level. The Lingua Editor panel's button/label flip Play/(Edit mode) to
Stop/(Playing) correctly across both screenshots.

Also fixes a real layout bug hit while verifying this: SetNextWindowPos
with ImGuiCond.FirstUseEver only applies with no prior imgui.ini entry
for that window title, and this project had already accumulated one from
earlier runs (all three panels stacked exactly on top of each other) —
gave each panel its own default position and .gitignore'd imgui.ini,
which is per-machine session state, not source.

Full suite still green: 65 tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 16:26:23 +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