master
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
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 |