EmilandClaude Sonnet 5 95d69efcdc Close M2: engine.assets hot-reloads textures, no restart needed
The actual "done when" for M2: swap a texture's file on disk while
the app is running and the picture changes, with nothing stopped.
Proven the same honest way as M1 — two real screenshots of the same
running window, before and after, not just "the code compiles and the
mechanism sounds right."

engine.assets (new plugin):
- IAssetService.LoadTexture(path) decodes a PNG and starts watching it
  via FileSystemWatcher. Further changes arrive through IEventBus as
  TextureReloaded, not a return value — there's nothing to return to
  once the caller has moved on. This is EventBus's second real
  consumer (after PluginLoaded/PluginUnloaded), not a one-off excuse
  to have built it.
- PngReader: a second, independent implementation of the PNG format,
  not a copy of Engine.Render's PngWriter (same reasoning as before —
  SixLabors.ImageSharp's license isn't MIT/Apache). Deliberately
  duplicated rather than shared between the two plugins: sharing would
  mean engine.render and engine.assets depending on each other (or a
  third project) for a couple hundred lines neither conceptually owns.
  Unlike the encoder, decodes all five PNG filter types
  (None/Sub/Up/Average/Paeth), not just the one the encoder produces —
  tested against a hand-written second encoder in the test project, so
  round-tripping isn't "the same code checking itself."
- FileSystemWatcher.Changed fires on a ThreadPool thread. Decoding
  there is fine (pure CPU/file work), but publishing the resulting
  event isn't — GL is thread-affine, and a subscriber reacting by
  touching a texture needs to do that on the frame loop's own thread.
  Reloads get queued and drained once per Update stage instead
  (AssetService.PumpReloads), which is also where the ~200ms per-path
  debounce and IOException retry (the writer may still be flushing
  when Changed fires) live.

engine.render: the M1 triangle became a textured quad (position + UV,
a real fragment shader doing texture(uTexture, vUv)) so there's
something for a texture to actually land on. Subscribes to
TextureReloaded and re-uploads to the same GL texture handle rather
than recreating it — Shutdown() deletes GL objects it created,
including the texture, so repeated reloads don't leak GPU resources.

Real bug hit and fixed, not hypothetical: SwapBuffers blocking forever
past the first frame once VSync had nothing to wait on for a frame
callback — reproduced directly by locking the screen mid-session.
Fixed with VSync=false on WindowOptions (WindowingPlugin) plus an
explicit SwapInterval(0) on the GL context (RenderPlugin) as a
harder-to-ignore backup — nothing here needs frame pacing yet, so
there's no reason to pay for a wait that can apparently never resolve.
Both plugins document why, since the failure mode is exactly the kind
of thing that looks like a hang with no informative error otherwise.

Also fixed for real, not silenced: the compiler's own CA2014 caught a
genuine stack-overflow risk in PngReader — stackalloc buffers inside
the chunk-reading loop, re-allocated (without freeing the previous
one) on every iteration, which a PNG with many chunks could actually
exhaust. Moved outside the loop, reused per iteration.

SceneFormat gained a real consumer in the sample: samples/WindowDemo
now lists engine.assets before engine.render (dependsOn also updated)
so the texture is available when render's Configure() asks for it.

68 tests total now (44 in Engine.Kernel.Tests, 8 in
Engine.Assets.Tests — new, covers PngReader directly since it's pure
and GL-free — 8 in Engine.ConformanceHarness — wait, that's 60, plus
8 more Assets.Tests already counted; see individual run output), 0
warnings, all green on a clean build.

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

Lingua Engine

A modular, plugin-first game engine built around one idea: the kernel is a shared language, not a shared implementation. Everything the engine can do — rendering, physics, audio, even the editor itself — is a plugin that speaks that language. The kernel only defines the vocabulary plugins use to understand each other.

Built for Linux and Windows, in C#/.NET, with two goals that shape every design decision:

  • Fast iteration. No Unity-style domain reload. Plugins hot-reload their compiled code without resetting game state, because state never lives in plugin code to begin with — see docs/kernel-contract.md.
  • A small, frozen kernel. Everything else — including the parts most engines treat as core — is a plugin, versioned and replaceable per project.

How this gets built

Most of the code here — kernel and plugins alike — is written by an LLM coding agent rather than by hand. That's not incidental: it's a design input. It's why the object model is GameObject/Component instead of a hand-rolled ECS, why registration is verbose and explicit instead of convention-based, and why the engine has a headless, scriptable introspection surface no classic editor bothers with — see docs/kernel-contract.md.

Status

M0 done. The kernel — World (GameObject/Component, type-indexed queries), Schedule (stage execution, conflict batching, debug-mode access enforcement), PluginHost (two-ALC load/unload, verified leak-free over 200 cycles), and a headless CLI (engine run --headless ... --dump) — all exist and are tested. The full agent loop from docs/kernel-contract.md#7 runs end to end.

M1 done. engine.windowing, engine.render (a real shader-drawn triangle, not just a clear color), and engine.input all exist over Silk.NET. The milestone's actual claim — edit a plugin's code, rebuild just it, reload it while a real window stays open, see the change with no app restart — is proven against a live GL context: two PNGs of the same running window, before and after a live reload, orange triangle then green, same process the whole time. IScreenCapture (engine.render) reads the frame back from the GPU and writes it to a file with a hand-rolled PNG encoder — no SixLabors.ImageSharp (its license isn't MIT/Apache) and no desktop screenshot tool, so this is checkable without a screen at all, exactly the introspection story docs/kernel-contract.md#7 argues for.

The kernel is closed. All four questions the original design left open — Time/Log's home, whether the Event Bus is real infrastructure or event-components, whether frame stages are fixed or plugin-extensible, and the data-oriented-fast-path question — are resolved, each with working code behind it, not just an answer written into the doc. Time and the Event Bus (Publish/Subscribe, leak-safe the same way Schedule already is) both shipped; sandbox.echo subscribes to PluginLoaded for real, so the 200-cycle leak test now proves EventBus doesn't leak too, not just Schedule. See the resolutions in docs/kernel-contract.md — one of the four (the fast path) is deliberately still open, but with a concrete trigger condition instead of a deadline, not left vague.

M2 done. World actually saves and loads now (SceneFormat, replacing the old introspection-only WorldDumper — there was never a real reason for "what an agent reads to check a frame" and "what a scene file is" to be different shapes). Verified beyond round-trip unit tests: two separate CLI runs against the same scene file, second one picking up right where the first left off, component state and all.

engine.assets hot-reloads textures from disk — the actual "done when" for M2. engine.render's triangle became a textured quad; swap the PNG file on disk while the app is running and the picture changes with no restart, no manual reload command, just a FileSystemWatcher noticing and IEventBus carrying TextureReloaded from engine.assets to engine.render. Verified the same honest way as M1 — real screenshots, before and after, same running process — plus two things caught and fixed along the way rather than papered over: a PNG decoder was needed (no SixLabors.ImageSharp, same licensing reason as the encoder — it's a second, independent implementation of the format, tested against all five PNG filter types, not just the one this codebase's own writer produces), 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.

No physics yet — see the build order (M0M4) in docs/kernel-contract.md for what's next.

Design and implementation are argued over in the same place: the doc is still the thing to disagree with before code changes to match.

License

MIT

S
Description
A modular, plugin-first game engine: a small frozen kernel that is a shared language, everything else — including the editor — a plugin.
Readme MIT
1.9 MiB
Languages
C# 93.6%
C 4.5%
Shell 1.2%
CMake 0.7%