From 6794a3e76375a9a0c5a0b19fe1efad78c6943095 Mon Sep 17 00:00:00 2001 From: Kevin Martinez Date: Thu, 12 Nov 2026 11:55:27 -0500 Subject: [PATCH 1/5] docs(diagnostics): assess developer diagnostics gaps (DEBUG-1) --- docs/evaluations/debug-1.md | 116 ++++++++++++++++++++++++++++++++++++ odd/tasks/debug-1.md | 44 ++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 docs/evaluations/debug-1.md create mode 100644 odd/tasks/debug-1.md diff --git a/docs/evaluations/debug-1.md b/docs/evaluations/debug-1.md new file mode 100644 index 00000000..ad5390bb --- /dev/null +++ b/docs/evaluations/debug-1.md @@ -0,0 +1,116 @@ +# DEBUG-1: developer diagnostics and runtime inspection + +Issue NastMz/MicroEngine#119. Record: [`odd/tasks/debug-1.md`](../../odd/tasks/debug-1.md). Date: 2026-11-12. +The question: can a developer outside this repository find out why their game misbehaves (a hitch, a body that falls +through the floor, a jump that never fires, a crash) with the published SDK, without reading the engine's source? The +issue asks for a gap assessment, a small diagnostic workflow, and only the primitives that the gaps justify, optional +and cheap when disabled. + +## 1. What exists today + +| Area | What a game can read | Where | Default | +| --- | --- | --- | --- | +| Frame timing | `FrameProfiler`: per-frame events, update, render, present, steps, allocated bytes, GC counts; p50–p99.9, hitches; JSON/CSV | `src/MicroEngine/Core/FrameProfiler.cs`, `RunOptions.Profiler`, `--profile FILE` | off; the unprofiled loop takes no timestamps | +| Graphics | `GraphicsCounters`: passes, draw calls, pipeline changes, texture bindings, uploads, live resources; cumulative, subtractable | `GraphicsDeviceBase.Counters` (`GraphicsDeviceBase.cs:220`) | always on, one count per submitted list | +| Platform | `IPlatformDiagnostics`: renderer, driver, VSync, GPU facts | `src/MicroEngine/Core/Platform.cs:64` | SDL only | +| Assets | `AssetScope.Loaded` (path, kind, bytes, load ms, during play), `AssetCache.Statistics`, a warning for a slow load during play | `AssetScope.cs:225`, `AssetCache.cs:297` | always on | +| Audio | `ActiveVoices`, `MaxVoices`, `RefusedSounds` | `AudioEngine.cs:217` | always on | +| Scenes | `SceneManager.Current/Stack/Count`; `SceneWorld.Count`, `Children`, `All()`, `WorldMatrix` | `src/MicroEngine/Scenes/Scene.cs` | always on | +| Physics | `Bodies`, `KindOf`, `PositionOf`, `RayCast`, `Overlap`, `ContactEvents`, `SensorEvents` | `IPhysicsWorld2D.cs` | always on | +| Input | `Devices`, `Players`, `Layers`, the frame's raw `Events` | `InputSystem` | always on | +| Logging | `ILog` (`ConsoleLog`, stderr, Info and above), `Game.Log`, replaceable service | `src/MicroEngine.Kernel/Logging/Log.cs` | on | +| Errors | `RunResult.Error`; typed exceptions (`AssetLoadException`, `GraphicsCapabilityException`, `PhysicsCapabilityException`...) | `GameRunner.cs:82` | on | +| Determinism | `HashState`, `--state-trace` | `StateTrace.cs` | off | + +`src/` uses no `EventSource`, `System.Diagnostics.Metrics` or `ActivitySource`, and no samples or guide pages use any +of the rows above except `--profile` and asset statistics (`docs/manual/lifecycle.md` "Measuring a game", +`docs/manual/assets.md`). + +## 2. Scenarios an external developer meets + +Each scenario is a question someone asks while making a game; the verdict says whether the published SDK answers it +today. + +1. **"My game stutters every few seconds."** `--profile` records hitches, allocations and GC counts per frame, and + the summary reports them. The developer learns *that* a frame was slow and that it allocated, but not *which* of + their systems took the time: update and render are one number each, though `FrameSchedule` already has named + systems. They cannot watch it live either: the profiler writes a file at exit and the game cannot read it. + **Partial.** +2. **"Why is rendering slow on this phone?"** Draw calls and pipeline changes are counted, but on + `GraphicsDeviceBase`, while `Game.Graphics` is an `IGraphicsDevice`; the developer has to know to cast. The + profiler does not record them per frame. **Partial, hidden.** +3. **"My character falls through the floor / the crate does not touch the ramp."** No way to see shapes, contacts or + sensors: no debug draw exists, a body's shapes cannot be listed, and a shape's geometry cannot be read back from + `IPhysicsWorld2D`. Drawing them means keeping a copy of every `ShapeDescription` in game code. **Fails.** +4. **"My jump does not fire."** Raw events and device state are readable, but which input layer consumed the event, + or which actions fired this frame, is not recorded anywhere. **Fails.** +5. **"Something is wrong and the console says nothing."** `ILog` exists, but the engine itself writes platform, + audio and launcher messages straight to `Console.Error` (20 call sites), so a game that routes `ILog` to a file or + an overlay misses them. **Partial.** +6. **"The game crashed on a tester's machine."** The launcher prints `error: {message}` only: no stack trace, no file, + no record of the frames before it. A tester cannot send anything useful. **Fails.** +7. **"Show me an FPS counter / timings while I play."** No debug drawing that update code can call (draw a line now, + at this world position, for this frame), and no ready numbers to put on screen. The developer can build it on + `DrawAboveScenes`, `IRenderer` and `Game.Font`, given the numbers. **Fails for the numbers; the drawing is + possible but every game rebuilds it.** +8. **"Is this texture still loaded? Which sounds are playing?"** Assets: answered (`AssetScope.Loaded`). Audio: + counts only behind a cast, no list of playing sounds. **Mostly answered.** +9. **"Use the .NET tools I already know."** `dotnet-counters`, `dotnet-trace` and Visual Studio's diagnostics show + the runtime's GC and CPU, but nothing from the engine: no frame time, draw calls or voices appear there. + **Fails for engine data.** + +## 3. Options weighed + +- **Build a profiler UI or an inspector window.** Rejected: the issue asks for reusable infrastructure over prebuilt + UI, and perf-1 already ruled out embedding Tracy or Superluminal. A desktop inspector would not run on the browser + or mobile targets. +- **Publish engine numbers through `System.Diagnostics.Metrics`.** Chosen for the external-tool side: it is in the + base library (no package), `dotnet-counters`, `dotnet-monitor` and OpenTelemetry read it, and an instrument nobody + listens to costs one branch. It covers scenario 9 without the engine writing any tool. +- **One in-game diagnostics surface the game reads each frame.** Chosen for the in-game side (scenarios 1, 2, 7): + the same numbers the meter publishes, as a struct a game can put on screen, log or assert in a test. + +## 4. Proposed primitives + +Each is optional and costs nothing measurable when unused; each comes with a test, an example in `samples/Examples` +and a manual page section. Contract changes are marked. + +1. **`Game.Diagnostics` (frame statistics).** The last frame's update, render and present time, fixed steps, + allocated bytes, GC counts per generation, the graphics counters for that frame (`GraphicsCounters` delta) and + audio voices. Off by default; `Diagnostics.Enabled = true` (or `--diagnostics`) turns on the timestamps the + profiler already takes. Scenarios 1, 2, 7, 8. +2. **Per-system timing.** When enabled, `FrameSchedule` times each named system; `Diagnostics.Systems` lists name, + phase and time. Scenario 1. +3. **Engine meter.** A `Meter("MicroEngine")` with frame time, draw calls, allocations and voices as observable + instruments, read only when a listener is attached. Scenario 9. +4. **`DebugDraw`.** An immediate queue update code can call (`Line`, `Rect`, `Circle`, `Text`, in world or screen + space, with a duration), drawn after the scenes and cleared each frame; calls do nothing while it is disabled. + Scenarios 3, 4, 7. +5. **Physics shape inspection.** `IPhysicsWorld2D` gains `ShapesOf(body)` and `DescriptionOf(shape)` as default + interface members (a contract addition, not a break: an external backend that does not override them throws + `PhysicsCapabilityException`), and `PhysicsDebugDraw.Draw(world, DebugDraw)` draws shapes, sensors and the step's + contacts. Scenario 3. +6. **Input trace.** While diagnostics are enabled, `InputSystem` records, per frame, which layer handled each event + and which actions were pressed; `Diagnostics.Input` reads it. Scenario 4. +7. **Engine messages through `ILog`.** The 20 `Console.Error` call sites go through the game's `ILog` (the default + still writes stderr, so the console output keeps its text). Scenario 5. +8. **Crash report.** On an unhandled game error the launcher writes `crash-