diff --git a/docs/api/MicroEngine.Audio.AudioMixer.md b/docs/api/MicroEngine.Audio.AudioMixer.md
index 87ddd0e7..40a5ef37 100644
--- a/docs/api/MicroEngine.Audio.AudioMixer.md
+++ b/docs/api/MicroEngine.Audio.AudioMixer.md
@@ -133,6 +133,19 @@ public float SfxVolume { get; set; }
[float](https://learn.microsoft.com/dotnet/api/system.single)
+### Statistics
+
+The voices of the device's mixer: how many sounds play now, how many can play at once and how many the voice
+policy refused so far. Zeros for a device without the engine's mixer ([SilentAudioDevice](MicroEngine.Audio.SilentAudioDevice.md)).
+
+```csharp
+public AudioStatistics Statistics { get; }
+```
+
+#### Property Value
+
+ [AudioStatistics](MicroEngine.Audio.AudioStatistics.md)
+
## Methods
### AddMusic\(string, AudioClip, bool\)
diff --git a/docs/api/MicroEngine.Audio.AudioStatistics.md b/docs/api/MicroEngine.Audio.AudioStatistics.md
new file mode 100644
index 00000000..8fda3258
--- /dev/null
+++ b/docs/api/MicroEngine.Audio.AudioStatistics.md
@@ -0,0 +1,179 @@
+# Struct AudioStatistics
+
+
+
+Namespace: [MicroEngine.Audio](MicroEngine.Audio.md)
+Assembly: MicroEngine.Audio.dll
+Guide: [Audio](../manual/audio.md)
+
+What [Statistics](MicroEngine.Audio.AudioMixer.md#MicroEngine_Audio_AudioMixer_Statistics) reports about the mixer's voices.
+
+```csharp
+public readonly struct AudioStatistics : IEquatable
+```
+
+#### Implements
+
+[IEquatable](https://learn.microsoft.com/dotnet/api/system.iequatable\-1)
+
+## Constructors
+
+### AudioStatistics\(int, int, int\)
+
+What [Statistics](MicroEngine.Audio.AudioMixer.md#MicroEngine_Audio_AudioMixer_Statistics) reports about the mixer's voices.
+
+```csharp
+public AudioStatistics(int ActiveVoices, int MaxVoices, int RefusedSounds)
+```
+
+#### Parameters
+
+`ActiveVoices` [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+Sounds playing now, music included.
+
+`MaxVoices` [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+Sounds that can play at once.
+
+`RefusedSounds` [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+Sounds the voice policy refused since the start because every voice played something of higher priority.
+
+## Properties
+
+### ActiveVoices
+
+Sounds playing now, music included.
+
+```csharp
+public int ActiveVoices { get; init; }
+```
+
+#### Property Value
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+### MaxVoices
+
+Sounds that can play at once.
+
+```csharp
+public int MaxVoices { get; init; }
+```
+
+#### Property Value
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+### RefusedSounds
+
+Sounds the voice policy refused since the start because every voice played something of higher priority.
+
+```csharp
+public int RefusedSounds { get; init; }
+```
+
+#### Property Value
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+## Methods
+
+### Deconstruct\(out int, out int, out int\)
+
+```csharp
+public void Deconstruct(out int ActiveVoices, out int MaxVoices, out int RefusedSounds)
+```
+
+#### Parameters
+
+`ActiveVoices` [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+`MaxVoices` [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+`RefusedSounds` [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+### Equals\(object\)
+
+```csharp
+public override bool Equals(object obj)
+```
+
+#### Parameters
+
+`obj` [object](https://learn.microsoft.com/dotnet/api/system.object)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+### Equals\(AudioStatistics\)
+
+```csharp
+public bool Equals(AudioStatistics other)
+```
+
+#### Parameters
+
+`other` [AudioStatistics](MicroEngine.Audio.AudioStatistics.md)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+### GetHashCode\(\)
+
+```csharp
+public override int GetHashCode()
+```
+
+#### Returns
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+### ToString\(\)
+
+```csharp
+public override string ToString()
+```
+
+#### Returns
+
+ [string](https://learn.microsoft.com/dotnet/api/system.string)
+
+## Operators
+
+### operator ==\(AudioStatistics, AudioStatistics\)
+
+```csharp
+public static bool operator ==(AudioStatistics left, AudioStatistics right)
+```
+
+#### Parameters
+
+`left` [AudioStatistics](MicroEngine.Audio.AudioStatistics.md)
+
+`right` [AudioStatistics](MicroEngine.Audio.AudioStatistics.md)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+### operator \!=\(AudioStatistics, AudioStatistics\)
+
+```csharp
+public static bool operator !=(AudioStatistics left, AudioStatistics right)
+```
+
+#### Parameters
+
+`left` [AudioStatistics](MicroEngine.Audio.AudioStatistics.md)
+
+`right` [AudioStatistics](MicroEngine.Audio.AudioStatistics.md)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+
diff --git a/docs/api/MicroEngine.Audio.md b/docs/api/MicroEngine.Audio.md
index 57ae26af..849d3fc8 100644
--- a/docs/api/MicroEngine.Audio.md
+++ b/docs/api/MicroEngine.Audio.md
@@ -80,6 +80,10 @@ An opaque handle to a loaded sound effect.
### Structs
+ [AudioStatistics](MicroEngine.Audio.AudioStatistics.md)
+
+What [Statistics](MicroEngine.Audio.AudioMixer.md#MicroEngine_Audio_AudioMixer_Statistics) reports about the mixer's voices.
+
[PlayParameters](MicroEngine.Audio.PlayParameters.md)
How a sound plays.
diff --git a/docs/api/MicroEngine.Core.CrashReport.md b/docs/api/MicroEngine.Core.CrashReport.md
new file mode 100644
index 00000000..3c2da039
--- /dev/null
+++ b/docs/api/MicroEngine.Core.CrashReport.md
@@ -0,0 +1,83 @@
+# Class CrashReport
+
+
+
+Namespace: [MicroEngine.Core](MicroEngine.Core.md)
+Assembly: MicroEngine.dll
+Guide: [Game lifecycle and time](../manual/lifecycle.md)
+
+A text file that describes a run that stopped on an error, for a tester to send: the game and engine versions, the
+operating system and runtime, the platform's facts ([IPlatformDiagnostics](MicroEngine.Core.IPlatformDiagnostics.md)), the last measured frame and
+its slowest systems when [Diagnostics](MicroEngine.Core.Game.md#MicroEngine_Core_Game_Diagnostics) measured, and the error with its stack trace. The desktop
+launcher writes one when a game's run fails and names it on standard error.
+
+```csharp
+public static class CrashReport
+```
+
+#### Inheritance
+
+[object](https://learn.microsoft.com/dotnet/api/system.object) ←
+[CrashReport](MicroEngine.Core.CrashReport.md)
+
+## Methods
+
+### Describe\(Game, Exception\)
+
+The report's text for error, which stopped game.
+
+```csharp
+public static string Describe(Game game, Exception error)
+```
+
+#### Parameters
+
+`game` [Game](MicroEngine.Core.Game.md)
+
+`error` [Exception](https://learn.microsoft.com/dotnet/api/system.exception)
+
+#### Returns
+
+ [string](https://learn.microsoft.com/dotnet/api/system.string)
+
+#### Exceptions
+
+ [ArgumentNullException](https://learn.microsoft.com/dotnet/api/system.argumentnullexception)
+
+An argument is null.
+
+### Write\(Game, Exception, string\)
+
+Writes the report into directory (created if needed) as crash-<UTC time>.txt and returns its path.
+
+```csharp
+public static string Write(Game game, Exception error, string directory)
+```
+
+#### Parameters
+
+`game` [Game](MicroEngine.Core.Game.md)
+
+`error` [Exception](https://learn.microsoft.com/dotnet/api/system.exception)
+
+`directory` [string](https://learn.microsoft.com/dotnet/api/system.string)
+
+#### Returns
+
+ [string](https://learn.microsoft.com/dotnet/api/system.string)
+
+#### Exceptions
+
+ [ArgumentNullException](https://learn.microsoft.com/dotnet/api/system.argumentnullexception)
+
+An argument is null.
+
+ [IOException](https://learn.microsoft.com/dotnet/api/system.io.ioexception)
+
+The file cannot be written.
+
+ [UnauthorizedAccessException](https://learn.microsoft.com/dotnet/api/system.unauthorizedaccessexception)
+
+The folder is not writable.
+
+
diff --git a/docs/api/MicroEngine.Core.DebugDraw.PhysicsColors.md b/docs/api/MicroEngine.Core.DebugDraw.PhysicsColors.md
new file mode 100644
index 00000000..1cb365bd
--- /dev/null
+++ b/docs/api/MicroEngine.Core.DebugDraw.PhysicsColors.md
@@ -0,0 +1,94 @@
+# Class DebugDraw.PhysicsColors
+
+
+
+Namespace: [MicroEngine.Core](MicroEngine.Core.md)
+Assembly: MicroEngine.dll
+Guide: [Game lifecycle and time](../manual/lifecycle.md)
+
+The colours [Physics](MicroEngine.Core.DebugDraw.md#MicroEngine_Core_DebugDraw_Physics_MicroEngine_Physics_IPhysicsWorld2D_System_Nullable_MicroEngine_RectF__) uses.
+
+```csharp
+public static class DebugDraw.PhysicsColors
+```
+
+#### Inheritance
+
+[object](https://learn.microsoft.com/dotnet/api/system.object) ←
+[DebugDraw.PhysicsColors](MicroEngine.Core.DebugDraw.PhysicsColors.md)
+
+## Fields
+
+### Contact
+
+Contacts that began during the latest step: the normal from the first shape's body.
+
+```csharp
+public static readonly Color Contact
+```
+
+#### Field Value
+
+ [Color](MicroEngine.Color.md)
+
+### Dynamic
+
+Shapes of dynamic bodies.
+
+```csharp
+public static readonly Color Dynamic
+```
+
+#### Field Value
+
+ [Color](MicroEngine.Color.md)
+
+### Kinematic
+
+Shapes of kinematic bodies.
+
+```csharp
+public static readonly Color Kinematic
+```
+
+#### Field Value
+
+ [Color](MicroEngine.Color.md)
+
+### OneWay
+
+One-way shapes and one-way tiles.
+
+```csharp
+public static readonly Color OneWay
+```
+
+#### Field Value
+
+ [Color](MicroEngine.Color.md)
+
+### Sensor
+
+Sensors.
+
+```csharp
+public static readonly Color Sensor
+```
+
+#### Field Value
+
+ [Color](MicroEngine.Color.md)
+
+### Static
+
+Shapes of static bodies.
+
+```csharp
+public static readonly Color Static
+```
+
+#### Field Value
+
+ [Color](MicroEngine.Color.md)
+
+
diff --git a/docs/api/MicroEngine.Core.DebugDraw.md b/docs/api/MicroEngine.Core.DebugDraw.md
new file mode 100644
index 00000000..b1e53f72
--- /dev/null
+++ b/docs/api/MicroEngine.Core.DebugDraw.md
@@ -0,0 +1,187 @@
+# Class DebugDraw
+
+
+
+Namespace: [MicroEngine.Core](MicroEngine.Core.md)
+Assembly: MicroEngine.dll
+Guide: [Game lifecycle and time](../manual/lifecycle.md)
+
+Shapes and text any code can ask for while the game runs, drawn above the scenes at the end of the frame
+([DebugDraw](MicroEngine.Core.Game.md#MicroEngine_Core_Game_DebugDraw)): a ray an enemy casts, the box a move swept, a path, a value next to an entity. Calls
+do nothing while [Enabled](MicroEngine.Core.DebugDraw.md#MicroEngine_Core_DebugDraw_Enabled) is off, so they can stay in shipped code.
+
+```csharp
+public sealed class DebugDraw
+```
+
+#### Inheritance
+
+[object](https://learn.microsoft.com/dotnet/api/system.object) ←
+[DebugDraw](MicroEngine.Core.DebugDraw.md)
+
+## Examples
+
+```csharp
+// In Load:
+DebugDraw.Enabled = true;
+// Anywhere in an update:
+game.DebugDraw.Line(enemy.Position, player.Position, Color.Red);
+game.DebugDraw.Physics(world);
+```
+
+## Remarks
+
+An item drawn with seconds: 0 shows for one frame; ask for it every update to keep it. A longer one stays that
+many seconds of real time. World items follow the camera of the scene on top ([WorldCamera](MicroEngine.Scenes.Scene.md#MicroEngine_Scenes_Scene_WorldCamera));
+screen items are in the canvas's pixels. Lines are one world unit (or one canvas pixel) thick.
+
+## Properties
+
+### Count
+
+Items waiting to be drawn (the current frame's and the longer-lived ones).
+
+```csharp
+public int Count { get; }
+```
+
+#### Property Value
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+### Enabled
+
+Whether calls are recorded and drawn. Off by default; turning it off drops what is pending.
+
+```csharp
+public bool Enabled { get; set; }
+```
+
+#### Property Value
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+## Methods
+
+### Circle\(Vector2, float, Color, float, DebugSpace\)
+
+The outline of a circle.
+
+```csharp
+public void Circle(Vector2 center, float radius, Color color, float seconds = 0, DebugSpace space = DebugSpace.World)
+```
+
+#### Parameters
+
+`center` [Vector2](https://learn.microsoft.com/dotnet/api/system.numerics.vector2)
+
+`radius` [float](https://learn.microsoft.com/dotnet/api/system.single)
+
+`color` [Color](MicroEngine.Color.md)
+
+`seconds` [float](https://learn.microsoft.com/dotnet/api/system.single)
+
+`space` [DebugSpace](MicroEngine.Core.DebugSpace.md)
+
+### Line\(Vector2, Vector2, Color, float, DebugSpace\)
+
+A line from from to to.
+
+```csharp
+public void Line(Vector2 from, Vector2 to, Color color, float seconds = 0, DebugSpace space = DebugSpace.World)
+```
+
+#### Parameters
+
+`from` [Vector2](https://learn.microsoft.com/dotnet/api/system.numerics.vector2)
+
+`to` [Vector2](https://learn.microsoft.com/dotnet/api/system.numerics.vector2)
+
+`color` [Color](MicroEngine.Color.md)
+
+`seconds` [float](https://learn.microsoft.com/dotnet/api/system.single)
+
+`space` [DebugSpace](MicroEngine.Core.DebugSpace.md)
+
+### Physics\(IPhysicsWorld2D, RectF?\)
+
+Draws world as it is now: every shape of every body (coloured by body kind; sensors and
+one-way shapes in their own colour, [PhysicsColors](MicroEngine.Core.DebugDraw.PhysicsColors.md)), the solid and one-way tiles of tile grids inside
+tileArea (by default what the current scene's camera shows, or the canvas), and a normal for
+each contact that began during the latest step. Does nothing while [Enabled](MicroEngine.Core.DebugDraw.md#MicroEngine_Core_DebugDraw_Enabled) is off.
+
+```csharp
+public void Physics(IPhysicsWorld2D world, RectF? tileArea = null)
+```
+
+#### Parameters
+
+`world` [IPhysicsWorld2D](MicroEngine.Physics.IPhysicsWorld2D.md)
+
+`tileArea` [RectF](MicroEngine.RectF.md)?
+
+#### Exceptions
+
+ [PhysicsCapabilityException](MicroEngine.Physics.PhysicsCapabilityException.md)
+
+The backend does not declare [ShapeInspection](MicroEngine.Physics.PhysicsCapabilities.md).
+
+### Point\(Vector2, Color, float, float, DebugSpace\)
+
+A small cross at position (size wide).
+
+```csharp
+public void Point(Vector2 position, Color color, float size = 4, float seconds = 0, DebugSpace space = DebugSpace.World)
+```
+
+#### Parameters
+
+`position` [Vector2](https://learn.microsoft.com/dotnet/api/system.numerics.vector2)
+
+`color` [Color](MicroEngine.Color.md)
+
+`size` [float](https://learn.microsoft.com/dotnet/api/system.single)
+
+`seconds` [float](https://learn.microsoft.com/dotnet/api/system.single)
+
+`space` [DebugSpace](MicroEngine.Core.DebugSpace.md)
+
+### Rect\(RectF, Color, float, DebugSpace\)
+
+The outline of rect.
+
+```csharp
+public void Rect(RectF rect, Color color, float seconds = 0, DebugSpace space = DebugSpace.World)
+```
+
+#### Parameters
+
+`rect` [RectF](MicroEngine.RectF.md)
+
+`color` [Color](MicroEngine.Color.md)
+
+`seconds` [float](https://learn.microsoft.com/dotnet/api/system.single)
+
+`space` [DebugSpace](MicroEngine.Core.DebugSpace.md)
+
+### Text\(Vector2, string, Color, float, DebugSpace\)
+
+text in the game's font ([Font](MicroEngine.Core.Game.md#MicroEngine_Core_Game_Font)), its top-left corner at position.
+
+```csharp
+public void Text(Vector2 position, string text, Color color, float seconds = 0, DebugSpace space = DebugSpace.World)
+```
+
+#### Parameters
+
+`position` [Vector2](https://learn.microsoft.com/dotnet/api/system.numerics.vector2)
+
+`text` [string](https://learn.microsoft.com/dotnet/api/system.string)
+
+`color` [Color](MicroEngine.Color.md)
+
+`seconds` [float](https://learn.microsoft.com/dotnet/api/system.single)
+
+`space` [DebugSpace](MicroEngine.Core.DebugSpace.md)
+
+
+
+Namespace: [MicroEngine.Core](MicroEngine.Core.md)
+Assembly: MicroEngine.dll
+Guide: [Game lifecycle and time](../manual/lifecycle.md)
+
+What a frame cost, as [LastFrame](MicroEngine.Core.GameDiagnostics.md#MicroEngine_Core_GameDiagnostics_LastFrame) reports it. Times are in milliseconds of real time.
+
+```csharp
+public readonly struct FrameStatistics : IEquatable
+```
+
+#### Implements
+
+[IEquatable](https://learn.microsoft.com/dotnet/api/system.iequatable\-1)
+
+## Constructors
+
+### FrameStatistics\(long, double, double, double, double, double, int, long, int, int, int, GraphicsCounters, AudioStatistics\)
+
+What a frame cost, as [LastFrame](MicroEngine.Core.GameDiagnostics.md#MicroEngine_Core_GameDiagnostics_LastFrame) reports it. Times are in milliseconds of real time.
+
+```csharp
+public FrameStatistics(long Frame, double Interval, double Events, double Update, double Render, double Present, int Steps, long Allocated, int Gen0Collections, int Gen1Collections, int Gen2Collections, GraphicsCounters Graphics, AudioStatistics Audio)
+```
+
+#### Parameters
+
+`Frame` [long](https://learn.microsoft.com/dotnet/api/system.int64)
+
+The number of the frame, from 1; 0 when nothing was measured.
+
+`Interval` [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+Time since the previous measured frame started (0 for the first one).
+
+`Events` [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+The platform's event pump.
+
+`Update` [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+The fixed steps and the per-frame updates ([BeginFrame](MicroEngine.Kernel.FramePhase.md) to [LateUpdate](MicroEngine.Kernel.FramePhase.md)).
+
+`Render` [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+Extracting and drawing ([Extract](MicroEngine.Kernel.FramePhase.md) and [Render](MicroEngine.Kernel.FramePhase.md)).
+
+`Present` [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+Presenting ([Present](MicroEngine.Kernel.FramePhase.md) and [EndFrame](MicroEngine.Kernel.FramePhase.md)), which includes waiting for the display.
+
+`Steps` [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+Fixed steps the frame ran.
+
+`Allocated` [long](https://learn.microsoft.com/dotnet/api/system.int64)
+
+Bytes the game's thread allocated during the frame.
+
+`Gen0Collections` [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+Garbage collections of generation 0 during the frame.
+
+`Gen1Collections` [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+Garbage collections of generation 1 during the frame.
+
+`Gen2Collections` [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+Full garbage collections during the frame.
+
+`Graphics` [GraphicsCounters](MicroEngine.Graphics.Devices.GraphicsCounters.md)
+
+The work the frame submitted to the graphics device (draw calls, passes, uploads; [LiveResources](MicroEngine.Graphics.Devices.GraphicsCounters.md#MicroEngine_Graphics_Devices_GraphicsCounters_LiveResources) is the count at its end). Zeros on a platform whose device does not count.
+
+`Audio` [AudioStatistics](MicroEngine.Audio.AudioStatistics.md)
+
+The mixer's voices at the end of the frame.
+
+## Properties
+
+### Allocated
+
+Bytes the game's thread allocated during the frame.
+
+```csharp
+public long Allocated { get; init; }
+```
+
+#### Property Value
+
+ [long](https://learn.microsoft.com/dotnet/api/system.int64)
+
+### Audio
+
+The mixer's voices at the end of the frame.
+
+```csharp
+public AudioStatistics Audio { get; init; }
+```
+
+#### Property Value
+
+ [AudioStatistics](MicroEngine.Audio.AudioStatistics.md)
+
+### Collections
+
+Garbage collections of any generation during the frame.
+
+```csharp
+public int Collections { get; }
+```
+
+#### Property Value
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+### Events
+
+The platform's event pump.
+
+```csharp
+public double Events { get; init; }
+```
+
+#### Property Value
+
+ [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+### Frame
+
+The number of the frame, from 1; 0 when nothing was measured.
+
+```csharp
+public long Frame { get; init; }
+```
+
+#### Property Value
+
+ [long](https://learn.microsoft.com/dotnet/api/system.int64)
+
+### Gen0Collections
+
+Garbage collections of generation 0 during the frame.
+
+```csharp
+public int Gen0Collections { get; init; }
+```
+
+#### Property Value
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+### Gen1Collections
+
+Garbage collections of generation 1 during the frame.
+
+```csharp
+public int Gen1Collections { get; init; }
+```
+
+#### Property Value
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+### Gen2Collections
+
+Full garbage collections during the frame.
+
+```csharp
+public int Gen2Collections { get; init; }
+```
+
+#### Property Value
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+### Graphics
+
+The work the frame submitted to the graphics device (draw calls, passes, uploads; [LiveResources](MicroEngine.Graphics.Devices.GraphicsCounters.md#MicroEngine_Graphics_Devices_GraphicsCounters_LiveResources) is the count at its end). Zeros on a platform whose device does not count.
+
+```csharp
+public GraphicsCounters Graphics { get; init; }
+```
+
+#### Property Value
+
+ [GraphicsCounters](MicroEngine.Graphics.Devices.GraphicsCounters.md)
+
+### Interval
+
+Time since the previous measured frame started (0 for the first one).
+
+```csharp
+public double Interval { get; init; }
+```
+
+#### Property Value
+
+ [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+### Present
+
+Presenting ([Present](MicroEngine.Kernel.FramePhase.md) and [EndFrame](MicroEngine.Kernel.FramePhase.md)), which includes waiting for the display.
+
+```csharp
+public double Present { get; init; }
+```
+
+#### Property Value
+
+ [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+### Render
+
+Extracting and drawing ([Extract](MicroEngine.Kernel.FramePhase.md) and [Render](MicroEngine.Kernel.FramePhase.md)).
+
+```csharp
+public double Render { get; init; }
+```
+
+#### Property Value
+
+ [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+### Steps
+
+Fixed steps the frame ran.
+
+```csharp
+public int Steps { get; init; }
+```
+
+#### Property Value
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+### Total
+
+The frame's work: events, update, render and present.
+
+```csharp
+public double Total { get; }
+```
+
+#### Property Value
+
+ [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+### Update
+
+The fixed steps and the per-frame updates ([BeginFrame](MicroEngine.Kernel.FramePhase.md) to [LateUpdate](MicroEngine.Kernel.FramePhase.md)).
+
+```csharp
+public double Update { get; init; }
+```
+
+#### Property Value
+
+ [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+## Methods
+
+### Deconstruct\(out long, out double, out double, out double, out double, out double, out int, out long, out int, out int, out int, out GraphicsCounters, out AudioStatistics\)
+
+```csharp
+public void Deconstruct(out long Frame, out double Interval, out double Events, out double Update, out double Render, out double Present, out int Steps, out long Allocated, out int Gen0Collections, out int Gen1Collections, out int Gen2Collections, out GraphicsCounters Graphics, out AudioStatistics Audio)
+```
+
+#### Parameters
+
+`Frame` [long](https://learn.microsoft.com/dotnet/api/system.int64)
+
+`Interval` [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+`Events` [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+`Update` [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+`Render` [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+`Present` [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+`Steps` [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+`Allocated` [long](https://learn.microsoft.com/dotnet/api/system.int64)
+
+`Gen0Collections` [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+`Gen1Collections` [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+`Gen2Collections` [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+`Graphics` [GraphicsCounters](MicroEngine.Graphics.Devices.GraphicsCounters.md)
+
+`Audio` [AudioStatistics](MicroEngine.Audio.AudioStatistics.md)
+
+### Equals\(object\)
+
+```csharp
+public override bool Equals(object obj)
+```
+
+#### Parameters
+
+`obj` [object](https://learn.microsoft.com/dotnet/api/system.object)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+### Equals\(FrameStatistics\)
+
+```csharp
+public bool Equals(FrameStatistics other)
+```
+
+#### Parameters
+
+`other` [FrameStatistics](MicroEngine.Core.FrameStatistics.md)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+### GetHashCode\(\)
+
+```csharp
+public override int GetHashCode()
+```
+
+#### Returns
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+### ToString\(\)
+
+```csharp
+public override string ToString()
+```
+
+#### Returns
+
+ [string](https://learn.microsoft.com/dotnet/api/system.string)
+
+## Operators
+
+### operator ==\(FrameStatistics, FrameStatistics\)
+
+```csharp
+public static bool operator ==(FrameStatistics left, FrameStatistics right)
+```
+
+#### Parameters
+
+`left` [FrameStatistics](MicroEngine.Core.FrameStatistics.md)
+
+`right` [FrameStatistics](MicroEngine.Core.FrameStatistics.md)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+### operator \!=\(FrameStatistics, FrameStatistics\)
+
+```csharp
+public static bool operator !=(FrameStatistics left, FrameStatistics right)
+```
+
+#### Parameters
+
+`left` [FrameStatistics](MicroEngine.Core.FrameStatistics.md)
+
+`right` [FrameStatistics](MicroEngine.Core.FrameStatistics.md)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+
diff --git a/docs/api/MicroEngine.Core.Game.md b/docs/api/MicroEngine.Core.Game.md
index a81c78d3..6090d3c0 100644
--- a/docs/api/MicroEngine.Core.Game.md
+++ b/docs/api/MicroEngine.Core.Game.md
@@ -109,6 +109,32 @@ protected virtual string? ContentRoot { get; }
[string](https://learn.microsoft.com/dotnet/api/system.string)?
+### DebugDraw
+
+Lines, shapes, text and physics worlds any code can ask to see, drawn above the scenes at the end of the frame
+([DebugDraw](MicroEngine.Core.DebugDraw.md)). Calls do nothing until [Enabled](MicroEngine.Core.DebugDraw.md#MicroEngine_Core_DebugDraw_Enabled) is set.
+
+```csharp
+public DebugDraw DebugDraw { get; }
+```
+
+#### Property Value
+
+ [DebugDraw](MicroEngine.Core.DebugDraw.md)
+
+### Diagnostics
+
+The live measurements of the run: the last frame's times, allocations, graphics work and audio voices, and the time
+of each frame system. Off until [Enabled](MicroEngine.Core.GameDiagnostics.md#MicroEngine_Core_GameDiagnostics_Enabled) is set or a tool listens to the engine's meter.
+
+```csharp
+public GameDiagnostics Diagnostics { get; }
+```
+
+#### Property Value
+
+ [GameDiagnostics](MicroEngine.Core.GameDiagnostics.md)
+
### Font
The built-in pixel font, created before [Load](MicroEngine.Core.Game.md#MicroEngine_Core_Game_Load) and disposed by the engine when the game shuts down.
diff --git a/docs/api/MicroEngine.Core.GameDiagnostics.md b/docs/api/MicroEngine.Core.GameDiagnostics.md
new file mode 100644
index 00000000..f0995242
--- /dev/null
+++ b/docs/api/MicroEngine.Core.GameDiagnostics.md
@@ -0,0 +1,105 @@
+# Class GameDiagnostics
+
+
+
+Namespace: [MicroEngine.Core](MicroEngine.Core.md)
+Assembly: MicroEngine.dll
+Guide: [Game lifecycle and time](../manual/lifecycle.md)
+
+The game's live measurements ([Diagnostics](MicroEngine.Core.Game.md#MicroEngine_Core_Game_Diagnostics)): the last frame's times, allocations, collections,
+graphics work and audio voices, and the time of every system of the frame schedule. Off by default; a run measures
+while [Enabled](MicroEngine.Core.GameDiagnostics.md#MicroEngine_Core_GameDiagnostics_Enabled) is set or while a tool listens to the engine's meter ([MeterName](MicroEngine.Core.GameDiagnostics.md#MicroEngine_Core_GameDiagnostics_MeterName)), and costs
+nothing measurable otherwise.
+
+```csharp
+public sealed class GameDiagnostics
+```
+
+#### Inheritance
+
+[object](https://learn.microsoft.com/dotnet/api/system.object) ←
+[GameDiagnostics](MicroEngine.Core.GameDiagnostics.md)
+
+## Examples
+
+```csharp
+// In Load: measure, and show the numbers when F3 is held.
+Diagnostics.Enabled = true;
+// In DrawAboveScenes:
+var frame = Diagnostics.LastFrame;
+Font.Draw(renderer, $"{frame.Total:0.0} ms {frame.Graphics.DrawCalls} draws {frame.Allocated} B", new Vector2(4, 4), Color.White);
+```
+
+## Remarks
+
+The same numbers are published as `Metrics` instruments of the meter
+"MicroEngine", so dotnet-counters monitor --counters MicroEngine, dotnet-monitor or an
+OpenTelemetry exporter read them without code in the game. The runtime's own meter (System.Runtime) adds the
+heap size, GC pauses and thread-pool figures.
+
+## Fields
+
+### MeterName
+
+The name of the engine's meter (`Meter`): "MicroEngine".
+
+```csharp
+public const string MeterName = "MicroEngine"
+```
+
+#### Field Value
+
+ [string](https://learn.microsoft.com/dotnet/api/system.string)
+
+## Properties
+
+### Active
+
+Whether frames are measured now: [Enabled](MicroEngine.Core.GameDiagnostics.md#MicroEngine_Core_GameDiagnostics_Enabled), or a listener of the engine's meter is attached.
+
+```csharp
+public bool Active { get; }
+```
+
+#### Property Value
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+### Enabled
+
+Whether the game measures every frame. Off by default; a tool listening to the engine's meter measures too ([Active](MicroEngine.Core.GameDiagnostics.md#MicroEngine_Core_GameDiagnostics_Active)).
+
+```csharp
+public bool Enabled { get; set; }
+```
+
+#### Property Value
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+### LastFrame
+
+The last measured frame; default (frame 0) until a frame is measured. During an update it is the previous frame.
+
+```csharp
+public FrameStatistics LastFrame { get; }
+```
+
+#### Property Value
+
+ [FrameStatistics](MicroEngine.Core.FrameStatistics.md)
+
+### Systems
+
+Every system of the frame schedule with its time during the last measured frame, in the order phases run and, within
+a phase, in the order its systems run. Empty until a frame is measured.
+
+```csharp
+public IReadOnlyList Systems { get; }
+```
+
+#### Property Value
+
+ [IReadOnlyList](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist\-1)<[SystemTiming](MicroEngine.Core.SystemTiming.md)\>
+
+
diff --git a/docs/api/MicroEngine.Core.PlatformRequest.md b/docs/api/MicroEngine.Core.PlatformRequest.md
index 41b97f59..dfd4cf43 100644
--- a/docs/api/MicroEngine.Core.PlatformRequest.md
+++ b/docs/api/MicroEngine.Core.PlatformRequest.md
@@ -120,6 +120,20 @@ public AppIdentity? Identity { get; init; }
[AppIdentity](MicroEngine.Core.AppIdentity.md)?
+### Log
+
+Where the backend reports what it could not do and how it carried on (no audio device, no gamepads, a renderer
+that did not start). Standard error by default; [GameHost](MicroEngine.Core.GameHost.md) passes one that reaches the game's
+[Log](MicroEngine.Core.Game.md#MicroEngine_Core_Game_Log) once the game starts, so a game that sends its log elsewhere receives these too.
+
+```csharp
+public ILog Log { get; init; }
+```
+
+#### Property Value
+
+ [ILog](MicroEngine.Kernel.ILog.md)
+
### Mute
No sound: the backend opens no audio device.
diff --git a/docs/api/MicroEngine.Core.SystemTiming.md b/docs/api/MicroEngine.Core.SystemTiming.md
new file mode 100644
index 00000000..2576082e
--- /dev/null
+++ b/docs/api/MicroEngine.Core.SystemTiming.md
@@ -0,0 +1,179 @@
+# Struct SystemTiming
+
+
+
+Namespace: [MicroEngine.Core](MicroEngine.Core.md)
+Assembly: MicroEngine.dll
+Guide: [Game lifecycle and time](../manual/lifecycle.md)
+
+How long one system of the frame schedule took during the last frame ([Systems](MicroEngine.Core.GameDiagnostics.md#MicroEngine_Core_GameDiagnostics_Systems)).
+
+```csharp
+public readonly struct SystemTiming : IEquatable
+```
+
+#### Implements
+
+[IEquatable](https://learn.microsoft.com/dotnet/api/system.iequatable\-1)
+
+## Constructors
+
+### SystemTiming\(string, FramePhase, double\)
+
+How long one system of the frame schedule took during the last frame ([Systems](MicroEngine.Core.GameDiagnostics.md#MicroEngine_Core_GameDiagnostics_Systems)).
+
+```csharp
+public SystemTiming(string Name, FramePhase Phase, double Milliseconds)
+```
+
+#### Parameters
+
+`Name` [string](https://learn.microsoft.com/dotnet/api/system.string)
+
+The system's name in [FrameSchedule](MicroEngine.Kernel.FrameSchedule.md).
+
+`Phase` [FramePhase](MicroEngine.Kernel.FramePhase.md)
+
+Its phase.
+
+`Milliseconds` [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+Its time during the frame; a fixed phase adds up every step of the frame.
+
+## Properties
+
+### Milliseconds
+
+Its time during the frame; a fixed phase adds up every step of the frame.
+
+```csharp
+public double Milliseconds { get; init; }
+```
+
+#### Property Value
+
+ [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+### Name
+
+The system's name in [FrameSchedule](MicroEngine.Kernel.FrameSchedule.md).
+
+```csharp
+public string Name { get; init; }
+```
+
+#### Property Value
+
+ [string](https://learn.microsoft.com/dotnet/api/system.string)
+
+### Phase
+
+Its phase.
+
+```csharp
+public FramePhase Phase { get; init; }
+```
+
+#### Property Value
+
+ [FramePhase](MicroEngine.Kernel.FramePhase.md)
+
+## Methods
+
+### Deconstruct\(out string, out FramePhase, out double\)
+
+```csharp
+public void Deconstruct(out string Name, out FramePhase Phase, out double Milliseconds)
+```
+
+#### Parameters
+
+`Name` [string](https://learn.microsoft.com/dotnet/api/system.string)
+
+`Phase` [FramePhase](MicroEngine.Kernel.FramePhase.md)
+
+`Milliseconds` [double](https://learn.microsoft.com/dotnet/api/system.double)
+
+### Equals\(object\)
+
+```csharp
+public override bool Equals(object obj)
+```
+
+#### Parameters
+
+`obj` [object](https://learn.microsoft.com/dotnet/api/system.object)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+### Equals\(SystemTiming\)
+
+```csharp
+public bool Equals(SystemTiming other)
+```
+
+#### Parameters
+
+`other` [SystemTiming](MicroEngine.Core.SystemTiming.md)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+### GetHashCode\(\)
+
+```csharp
+public override int GetHashCode()
+```
+
+#### Returns
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+### ToString\(\)
+
+```csharp
+public override string ToString()
+```
+
+#### Returns
+
+ [string](https://learn.microsoft.com/dotnet/api/system.string)
+
+## Operators
+
+### operator ==\(SystemTiming, SystemTiming\)
+
+```csharp
+public static bool operator ==(SystemTiming left, SystemTiming right)
+```
+
+#### Parameters
+
+`left` [SystemTiming](MicroEngine.Core.SystemTiming.md)
+
+`right` [SystemTiming](MicroEngine.Core.SystemTiming.md)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+### operator \!=\(SystemTiming, SystemTiming\)
+
+```csharp
+public static bool operator !=(SystemTiming left, SystemTiming right)
+```
+
+#### Parameters
+
+`left` [SystemTiming](MicroEngine.Core.SystemTiming.md)
+
+`right` [SystemTiming](MicroEngine.Core.SystemTiming.md)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+
diff --git a/docs/api/MicroEngine.Core.md b/docs/api/MicroEngine.Core.md
index a3b1ce61..f280eb83 100644
--- a/docs/api/MicroEngine.Core.md
+++ b/docs/api/MicroEngine.Core.md
@@ -62,6 +62,23 @@ executable (games copy it there at build time). Missing files fail with the full
broken install or a typo is obvious. A content source ([IContentSource](MicroEngine.Platform.IContentSource.md)) over a
[FolderContentSource](MicroEngine.Platform.FolderContentSource.md).
+ [CrashReport](MicroEngine.Core.CrashReport.md)
+
+A text file that describes a run that stopped on an error, for a tester to send: the game and engine versions, the
+operating system and runtime, the platform's facts ([IPlatformDiagnostics](MicroEngine.Core.IPlatformDiagnostics.md)), the last measured frame and
+its slowest systems when [Diagnostics](MicroEngine.Core.Game.md#MicroEngine_Core_Game_Diagnostics) measured, and the error with its stack trace. The desktop
+launcher writes one when a game's run fails and names it on standard error.
+
+ [DebugDraw.PhysicsColors](MicroEngine.Core.DebugDraw.PhysicsColors.md)
+
+The colours [Physics](MicroEngine.Core.DebugDraw.md#MicroEngine_Core_DebugDraw_Physics_MicroEngine_Physics_IPhysicsWorld2D_System_Nullable_MicroEngine_RectF__) uses.
+
+ [DebugDraw](MicroEngine.Core.DebugDraw.md)
+
+Shapes and text any code can ask for while the game runs, drawn above the scenes at the end of the frame
+([DebugDraw](MicroEngine.Core.Game.md#MicroEngine_Core_Game_DebugDraw)): a ray an enemy casts, the box a move swept, a path, a value next to an entity. Calls
+do nothing while [Enabled](MicroEngine.Core.DebugDraw.md#MicroEngine_Core_DebugDraw_Enabled) is off, so they can stay in shipped code.
+
[DisplayInfo](MicroEngine.Core.DisplayInfo.md)
A display (monitor) as the system reports it.
@@ -83,6 +100,13 @@ What a profiled run looked like. [MotionError](MicroEngine.Core.FrameSummary.md#
the game time it advanced differs from the wall time that passed, so a frame that repeats the previous picture or
jumps two steps counts even when every frame took the same time.
+ [GameDiagnostics](MicroEngine.Core.GameDiagnostics.md)
+
+The game's live measurements ([Diagnostics](MicroEngine.Core.Game.md#MicroEngine_Core_Game_Diagnostics)): the last frame's times, allocations, collections,
+graphics work and audio voices, and the time of every system of the frame schedule. Off by default; a run measures
+while [Enabled](MicroEngine.Core.GameDiagnostics.md#MicroEngine_Core_GameDiagnostics_Enabled) is set or while a tool listens to the engine's meter ([MeterName](MicroEngine.Core.GameDiagnostics.md#MicroEngine_Core_GameDiagnostics_MeterName)), and costs
+nothing measurable otherwise.
+
[GameHost](MicroEngine.Core.GameHost.md)
The one launch path of every host: desktop ([GameLauncher](MicroEngine.Core.GameLauncher.md)), browser pages, Android and iOS apps and
@@ -350,6 +374,10 @@ Totals over a scope and its children ([Statistics](MicroEngine.Core.AssetCache.m
One frame as [FrameProfiler](MicroEngine.Core.FrameProfiler.md) measured it. Times are in milliseconds.
+ [FrameStatistics](MicroEngine.Core.FrameStatistics.md)
+
+What a frame cost, as [LastFrame](MicroEngine.Core.GameDiagnostics.md#MicroEngine_Core_GameDiagnostics_LastFrame) reports it. Times are in milliseconds of real time.
+
[Percentiles](MicroEngine.Core.Percentiles.md)
Percentiles of one frame measurement, in milliseconds.
@@ -358,6 +386,10 @@ Percentiles of one frame measurement, in milliseconds.
One recorded drawing call.
+ [SystemTiming](MicroEngine.Core.SystemTiming.md)
+
+How long one system of the frame schedule took during the last frame ([Systems](MicroEngine.Core.GameDiagnostics.md#MicroEngine_Core_GameDiagnostics_Systems)).
+
### Interfaces
[IDisplayControl](MicroEngine.Core.IDisplayControl.md)
@@ -411,6 +443,10 @@ Screen orientations a game allows on phones (MicroEngineOrientation
Where a run's sound may go ([AudioOutput](MicroEngine.Core.PlatformRequest.md#MicroEngine_Core_PlatformRequest_AudioOutput), the launcher's --audio).
+ [DebugSpace](MicroEngine.Core.DebugSpace.md)
+
+Where a [DebugDraw](MicroEngine.Core.DebugDraw.md) item is placed.
+
[GpuPreference](MicroEngine.Core.GpuPreference.md)
Which GPU a game prefers on machines with an integrated and a dedicated one. The system has the last word: on
diff --git a/docs/api/MicroEngine.Graphics.Devices.IGraphicsDevice.md b/docs/api/MicroEngine.Graphics.Devices.IGraphicsDevice.md
index 2f32dac6..df3dda46 100644
--- a/docs/api/MicroEngine.Graphics.Devices.IGraphicsDevice.md
+++ b/docs/api/MicroEngine.Graphics.Devices.IGraphicsDevice.md
@@ -35,6 +35,19 @@ GraphicsCapabilities Capabilities { get; }
[GraphicsCapabilities](MicroEngine.Graphics.Devices.GraphicsCapabilities.md)
+### Counters
+
+Deterministic counts of the work submitted to this device since it was created (draw calls, passes, uploads, live
+resources). Every device built on [GraphicsDeviceBase](MicroEngine.Graphics.Devices.GraphicsDeviceBase.md) counts; a device that does not reports zeros.
+
+```csharp
+GraphicsCounters Counters { get; }
+```
+
+#### Property Value
+
+ [GraphicsCounters](MicroEngine.Graphics.Devices.GraphicsCounters.md)
+
## Methods
### BeginCommands\(\)
diff --git a/docs/api/MicroEngine.Hosting.HostEnvironment.md b/docs/api/MicroEngine.Hosting.HostEnvironment.md
index 5f12198c..47a640ff 100644
--- a/docs/api/MicroEngine.Hosting.HostEnvironment.md
+++ b/docs/api/MicroEngine.Hosting.HostEnvironment.md
@@ -59,6 +59,19 @@ public Func ClockSeed { get; init; }
[Func](https://learn.microsoft.com/dotnet/api/system.func\-1)<[int](https://learn.microsoft.com/dotnet/api/system.int32)\>
+### CrashDirectory
+
+Where the desktop launcher writes the crash report of a run that fails (Core.CrashReport), or null for
+the game's save folder (SaveStore.DefaultDirectory).
+
+```csharp
+public string? CrashDirectory { get; init; }
+```
+
+#### Property Value
+
+ [string](https://learn.microsoft.com/dotnet/api/system.string)?
+
### Error
Where the host prints errors (the console's standard error by default, as it was when this was created).
diff --git a/docs/api/MicroEngine.Input.InputSystem.md b/docs/api/MicroEngine.Input.InputSystem.md
index b9a3d1dc..706c8103 100644
--- a/docs/api/MicroEngine.Input.InputSystem.md
+++ b/docs/api/MicroEngine.Input.InputSystem.md
@@ -118,6 +118,20 @@ public IReadOnlyList<(DeviceId Id, IInputSource Source)> PolledSources { get; }
[IReadOnlyList](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist\-1)<\([DeviceId](MicroEngine.Input.DeviceId.md) [Id](https://learn.microsoft.com/dotnet/api/system.valuetuple\-microengine.input.deviceid,microengine.input.iinputsource\-.id), [IInputSource](MicroEngine.Input.IInputSource.md) [Source](https://learn.microsoft.com/dotnet/api/system.valuetuple\-microengine.input.deviceid,microengine.input.iinputsource\-.source)\)\>
+### Routed
+
+While [Trace](MicroEngine.Input.InputSystem.md#MicroEngine_Input_InputSystem_Trace) is on, the events routed this frame (device changes excepted) in order, each with the
+layer that took it, or none when it reached the devices and gameplay: the answer to "why did my action not fire?"
+when a menu, the on-screen controls or another layer took the press.
+
+```csharp
+public IReadOnlyList Routed { get; }
+```
+
+#### Property Value
+
+ [IReadOnlyList](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist\-1)<[RoutedEvent](MicroEngine.Input.RoutedEvent.md)\>
+
### Text
The text channel.
@@ -130,6 +144,19 @@ public TextInput Text { get; }
[TextInput](MicroEngine.Input.TextInput.md)
+### Trace
+
+Whether [Routed](MicroEngine.Input.InputSystem.md#MicroEngine_Input_InputSystem_Routed) records where each event of the frame went. Off by default; the game's diagnostics
+turn it on while they measure.
+
+```csharp
+public bool Trace { get; set; }
+```
+
+#### Property Value
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
## Methods
### AddPolled\(IInputSource\)
diff --git a/docs/api/MicroEngine.Input.RoutedEvent.md b/docs/api/MicroEngine.Input.RoutedEvent.md
new file mode 100644
index 00000000..46ca114a
--- /dev/null
+++ b/docs/api/MicroEngine.Input.RoutedEvent.md
@@ -0,0 +1,161 @@
+# Struct RoutedEvent
+
+
+
+Namespace: [MicroEngine.Input](MicroEngine.Input.md)
+Assembly: MicroEngine.Input.dll
+Guide: [Input](../manual/input.md)
+
+One event of the frame and where it went ([Routed](MicroEngine.Input.InputSystem.md#MicroEngine_Input_InputSystem_Routed)).
+
+```csharp
+public readonly struct RoutedEvent : IEquatable
+```
+
+#### Implements
+
+[IEquatable](https://learn.microsoft.com/dotnet/api/system.iequatable\-1)
+
+## Constructors
+
+### RoutedEvent\(InputEvent, IInputLayer?\)
+
+One event of the frame and where it went ([Routed](MicroEngine.Input.InputSystem.md#MicroEngine_Input_InputSystem_Routed)).
+
+```csharp
+public RoutedEvent(InputEvent Event, IInputLayer? Layer)
+```
+
+#### Parameters
+
+`Event` [InputEvent](MicroEngine.Input.InputEvent.md)
+
+The event.
+
+`Layer` [IInputLayer](MicroEngine.Input.IInputLayer.md)?
+
+The input layer that took it (or that took the press it ends), or null when it reached the devices and gameplay.
+
+## Properties
+
+### Event
+
+The event.
+
+```csharp
+public InputEvent Event { get; init; }
+```
+
+#### Property Value
+
+ [InputEvent](MicroEngine.Input.InputEvent.md)
+
+### Layer
+
+The input layer that took it (or that took the press it ends), or null when it reached the devices and gameplay.
+
+```csharp
+public IInputLayer? Layer { get; init; }
+```
+
+#### Property Value
+
+ [IInputLayer](MicroEngine.Input.IInputLayer.md)?
+
+## Methods
+
+### Deconstruct\(out InputEvent, out IInputLayer?\)
+
+```csharp
+public void Deconstruct(out InputEvent Event, out IInputLayer? Layer)
+```
+
+#### Parameters
+
+`Event` [InputEvent](MicroEngine.Input.InputEvent.md)
+
+`Layer` [IInputLayer](MicroEngine.Input.IInputLayer.md)?
+
+### Equals\(object\)
+
+```csharp
+public override bool Equals(object obj)
+```
+
+#### Parameters
+
+`obj` [object](https://learn.microsoft.com/dotnet/api/system.object)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+### Equals\(RoutedEvent\)
+
+```csharp
+public bool Equals(RoutedEvent other)
+```
+
+#### Parameters
+
+`other` [RoutedEvent](MicroEngine.Input.RoutedEvent.md)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+### GetHashCode\(\)
+
+```csharp
+public override int GetHashCode()
+```
+
+#### Returns
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+### ToString\(\)
+
+```csharp
+public override string ToString()
+```
+
+#### Returns
+
+ [string](https://learn.microsoft.com/dotnet/api/system.string)
+
+## Operators
+
+### operator ==\(RoutedEvent, RoutedEvent\)
+
+```csharp
+public static bool operator ==(RoutedEvent left, RoutedEvent right)
+```
+
+#### Parameters
+
+`left` [RoutedEvent](MicroEngine.Input.RoutedEvent.md)
+
+`right` [RoutedEvent](MicroEngine.Input.RoutedEvent.md)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+### operator \!=\(RoutedEvent, RoutedEvent\)
+
+```csharp
+public static bool operator !=(RoutedEvent left, RoutedEvent right)
+```
+
+#### Parameters
+
+`left` [RoutedEvent](MicroEngine.Input.RoutedEvent.md)
+
+`right` [RoutedEvent](MicroEngine.Input.RoutedEvent.md)
+
+#### Returns
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+
diff --git a/docs/api/MicroEngine.Input.md b/docs/api/MicroEngine.Input.md
index 9611eaf6..6d4a3d3e 100644
--- a/docs/api/MicroEngine.Input.md
+++ b/docs/api/MicroEngine.Input.md
@@ -200,6 +200,10 @@ A pointer the game can read: the mouse while it is over the window, or a finger
The mouse (or another pointer) over the game's virtual canvas.
+ [RoutedEvent](MicroEngine.Input.RoutedEvent.md)
+
+One event of the frame and where it went ([Routed](MicroEngine.Input.InputSystem.md#MicroEngine_Input_InputSystem_Routed)).
+
[TouchControlView](MicroEngine.Input.TouchControlView.md)
Everything a skin needs to draw one control as it is this frame, in screen pixels.
diff --git a/docs/api/MicroEngine.Kernel.FrameSchedule.md b/docs/api/MicroEngine.Kernel.FrameSchedule.md
index fc990995..8d8bc0dc 100644
--- a/docs/api/MicroEngine.Kernel.FrameSchedule.md
+++ b/docs/api/MicroEngine.Kernel.FrameSchedule.md
@@ -59,6 +59,25 @@ public bool IsFrozen { get; }
[bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+### MeasureTime
+
+Whether [Run](MicroEngine.Kernel.FrameSchedule.md#MicroEngine_Kernel_FrameSchedule_Run_MicroEngine_Kernel_FramePhase_MicroEngine_Kernel_FrameContext__) measures how long each system takes ([Times](MicroEngine.Kernel.FrameSchedule.md#MicroEngine_Kernel_FrameSchedule_Times_MicroEngine_Kernel_FramePhase_)). Off by default: a schedule
+that does not measure takes no timestamps. It may change once the schedule is frozen, since it changes no system.
+
+```csharp
+public bool MeasureTime { get; set; }
+```
+
+#### Property Value
+
+ [bool](https://learn.microsoft.com/dotnet/api/system.boolean)
+
+#### Exceptions
+
+ [InvalidOperationException](https://learn.microsoft.com/dotnet/api/system.invalidoperationexception)
+
+Turning it on when the order cannot be resolved (see [Order](MicroEngine.Kernel.FrameSchedule.md#MicroEngine_Kernel_FrameSchedule_Order_MicroEngine_Kernel_FramePhase_)).
+
## Methods
### Add\(FramePhase, string, Action, string?, string?\)
@@ -258,6 +277,14 @@ No system is named name.
The schedule is frozen.
+### ResetTimes\(\)
+
+Sets every measured time ([Times](MicroEngine.Kernel.FrameSchedule.md#MicroEngine_Kernel_FrameSchedule_Times_MicroEngine_Kernel_FramePhase_)) back to zero; the loop does it at the start of each frame.
+
+```csharp
+public void ResetTimes()
+```
+
### Run\(FramePhase, in FrameContext\)
Runs the systems of phase, in order, with context.
@@ -282,4 +309,27 @@ An exception thrown by a system stops the phase and reaches the caller.
The order cannot be resolved (see [Order](MicroEngine.Kernel.FrameSchedule.md#MicroEngine_Kernel_FrameSchedule_Order_MicroEngine_Kernel_FramePhase_)).
+### Times\(FramePhase\)
+
+The milliseconds each system of phase took since the last [ResetTimes](MicroEngine.Kernel.FrameSchedule.md#MicroEngine_Kernel_FrameSchedule_ResetTimes), aligned with
+[Order](MicroEngine.Kernel.FrameSchedule.md#MicroEngine_Kernel_FrameSchedule_Order_MicroEngine_Kernel_FramePhase_): a fixed phase adds up every step it ran. All zero while [MeasureTime](MicroEngine.Kernel.FrameSchedule.md#MicroEngine_Kernel_FrameSchedule_MeasureTime) was off.
+
+```csharp
+public ReadOnlySpan Times(FramePhase phase)
+```
+
+#### Parameters
+
+`phase` [FramePhase](MicroEngine.Kernel.FramePhase.md)
+
+#### Returns
+
+ [ReadOnlySpan](https://learn.microsoft.com/dotnet/api/system.readonlyspan\-1)<[double](https://learn.microsoft.com/dotnet/api/system.double)\>
+
+#### Exceptions
+
+ [InvalidOperationException](https://learn.microsoft.com/dotnet/api/system.invalidoperationexception)
+
+The order cannot be resolved (see [Order](MicroEngine.Kernel.FrameSchedule.md#MicroEngine_Kernel_FrameSchedule_Order_MicroEngine_Kernel_FramePhase_)).
+
diff --git a/docs/api/MicroEngine.Physics.IPhysicsWorld2D.md b/docs/api/MicroEngine.Physics.IPhysicsWorld2D.md
index 07346d4c..68847fe8 100644
--- a/docs/api/MicroEngine.Physics.IPhysicsWorld2D.md
+++ b/docs/api/MicroEngine.Physics.IPhysicsWorld2D.md
@@ -227,6 +227,32 @@ The description needs an undeclared capability.
The description is invalid ([Validate](MicroEngine.Physics.BodyDescription.md#MicroEngine_Physics_BodyDescription_Validate)).
+### DescriptionOf\(ShapeId\)
+
+The description shape was added with (its geometry is relative to its body's origin).
+
+```csharp
+ShapeDescription DescriptionOf(ShapeId shape)
+```
+
+#### Parameters
+
+`shape` [ShapeId](MicroEngine.Physics.ShapeId.md)
+
+#### Returns
+
+ [ShapeDescription](MicroEngine.Physics.ShapeDescription.md)
+
+#### Exceptions
+
+ [InvalidOperationException](https://learn.microsoft.com/dotnet/api/system.invalidoperationexception)
+
+The id is stale.
+
+ [PhysicsCapabilityException](MicroEngine.Physics.PhysicsCapabilityException.md)
+
+The backend does not declare [ShapeInspection](MicroEngine.Physics.PhysicsCapabilities.md).
+
### DestroyBody\(BodyId\)
Destroys the body and its shapes; nothing for a stale or none id. Their sensor pairs end at the next step.
@@ -497,6 +523,35 @@ void SetVelocity(BodyId body, Vector2 velocity)
The backend does not declare [DynamicBodies](MicroEngine.Physics.PhysicsCapabilities.md).
+### ShapesOf\(BodyId, Span\)
+
+Writes the live shapes of body, in the order they were added, into results
+and returns how many it has (which may exceed the span's length: only the first ones are written).
+
+```csharp
+int ShapesOf(BodyId body, Span results)
+```
+
+#### Parameters
+
+`body` [BodyId](MicroEngine.Physics.BodyId.md)
+
+`results` [Span](https://learn.microsoft.com/dotnet/api/system.span\-1)<[ShapeId](MicroEngine.Physics.ShapeId.md)\>
+
+#### Returns
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+#### Exceptions
+
+ [InvalidOperationException](https://learn.microsoft.com/dotnet/api/system.invalidoperationexception)
+
+The body is stale.
+
+ [PhysicsCapabilityException](MicroEngine.Physics.PhysicsCapabilityException.md)
+
+The backend does not declare [ShapeInspection](MicroEngine.Physics.PhysicsCapabilities.md).
+
### Step\(float\)
Advances the world by dt seconds: dynamic bodies move, and the events of this step replace the
diff --git a/docs/api/MicroEngine.Physics.PhysicsCapabilities.md b/docs/api/MicroEngine.Physics.PhysicsCapabilities.md
index 22be4f22..b753e820 100644
--- a/docs/api/MicroEngine.Physics.PhysicsCapabilities.md
+++ b/docs/api/MicroEngine.Physics.PhysicsCapabilities.md
@@ -92,6 +92,13 @@ Rotated bodies ([Rotation](MicroEngine.Physics.BodyDescription.md#MicroEngine_Ph
+`ShapeInspection = 4096`
+
+Reading shapes back ([ShapesOf](MicroEngine.Physics.IPhysicsWorld2D.md#MicroEngine_Physics_IPhysicsWorld2D_ShapesOf_MicroEngine_Physics_BodyId_System_Span_MicroEngine_Physics_ShapeId__), [DescriptionOf](MicroEngine.Physics.IPhysicsWorld2D.md#MicroEngine_Physics_IPhysicsWorld2D_DescriptionOf_MicroEngine_Physics_ShapeId_)): what a
+debug view needs to draw the world. Both built-in backends declare it.
+
+
+
`TileGeometry = 1`
Grid-aligned static geometry fed directly from tile layers (the tile backend's own AddTiles).
diff --git a/docs/api/MicroEngine.Physics.Tiles.TilePhysicsWorld2D.md b/docs/api/MicroEngine.Physics.Tiles.TilePhysicsWorld2D.md
index 64773cdf..e2fb8fa0 100644
--- a/docs/api/MicroEngine.Physics.Tiles.TilePhysicsWorld2D.md
+++ b/docs/api/MicroEngine.Physics.Tiles.TilePhysicsWorld2D.md
@@ -73,7 +73,7 @@ public const float ContactDistance = 0.01
The capabilities this backend declares.
```csharp
-public const PhysicsCapabilities Declared = TileGeometry | OneWayPlatforms | MovingSolids
+public const PhysicsCapabilities Declared = TileGeometry | OneWayPlatforms | MovingSolids | ShapeInspection
```
#### Field Value
@@ -284,6 +284,32 @@ public BodyId CreateBody(EntityId owner, in BodyDescription body)
[BodyId](MicroEngine.Physics.BodyId.md)
+### DescriptionOf\(ShapeId\)
+
+The description shape was added with (its geometry is relative to its body's origin).
+
+```csharp
+public ShapeDescription DescriptionOf(ShapeId shape)
+```
+
+#### Parameters
+
+`shape` [ShapeId](MicroEngine.Physics.ShapeId.md)
+
+#### Returns
+
+ [ShapeDescription](MicroEngine.Physics.ShapeDescription.md)
+
+#### Exceptions
+
+ [InvalidOperationException](https://learn.microsoft.com/dotnet/api/system.invalidoperationexception)
+
+The id is stale.
+
+ [PhysicsCapabilityException](MicroEngine.Physics.PhysicsCapabilityException.md)
+
+The backend does not declare [ShapeInspection](MicroEngine.Physics.PhysicsCapabilities.md).
+
### DestroyBody\(BodyId\)
Destroys the body and its shapes; nothing for a stale or none id. Their sensor pairs end at the next step.
@@ -296,6 +322,28 @@ public void DestroyBody(BodyId body)
`body` [BodyId](MicroEngine.Physics.BodyId.md)
+### GridOf\(ShapeId\)
+
+The tiles shape stands for when it was added with [AddTiles](MicroEngine.Physics.Tiles.TilePhysicsWorld2D.md#MicroEngine_Physics_Tiles_TilePhysicsWorld2D_AddTiles_MicroEngine_Physics_BodyId_MicroEngine_World_ICollisionGrid_System_UInt64_System_UInt64_); null for any other shape.
+
+```csharp
+public ICollisionGrid? GridOf(ShapeId shape)
+```
+
+#### Parameters
+
+`shape` [ShapeId](MicroEngine.Physics.ShapeId.md)
+
+#### Returns
+
+ [ICollisionGrid](MicroEngine.World.ICollisionGrid.md)?
+
+#### Exceptions
+
+ [InvalidOperationException](https://learn.microsoft.com/dotnet/api/system.invalidoperationexception)
+
+The id is stale.
+
### HashState\(StateHasher\)
Adds the world's simulation state: the step count, every live body (id, kind, position, shapes) and the sensor pairs.
@@ -534,6 +582,35 @@ public void SetVelocity(BodyId body, Vector2 velocity)
Always.
+### ShapesOf\(BodyId, Span\)
+
+Writes the live shapes of body, in the order they were added, into results
+and returns how many it has (which may exceed the span's length: only the first ones are written).
+
+```csharp
+public int ShapesOf(BodyId body, Span results)
+```
+
+#### Parameters
+
+`body` [BodyId](MicroEngine.Physics.BodyId.md)
+
+`results` [Span](https://learn.microsoft.com/dotnet/api/system.span\-1)<[ShapeId](MicroEngine.Physics.ShapeId.md)\>
+
+#### Returns
+
+ [int](https://learn.microsoft.com/dotnet/api/system.int32)
+
+#### Exceptions
+
+ [InvalidOperationException](https://learn.microsoft.com/dotnet/api/system.invalidoperationexception)
+
+The body is stale.
+
+ [PhysicsCapabilityException](MicroEngine.Physics.PhysicsCapabilityException.md)
+
+The backend does not declare [ShapeInspection](MicroEngine.Physics.PhysicsCapabilities.md).
+
### Step\(float\)
Advances the world by dt seconds: dynamic bodies move, and the events of this step replace the
diff --git a/docs/api/MicroEngine.Sdl.SdlAudioDevice.md b/docs/api/MicroEngine.Sdl.SdlAudioDevice.md
index 7445c9b8..89a0a103 100644
--- a/docs/api/MicroEngine.Sdl.SdlAudioDevice.md
+++ b/docs/api/MicroEngine.Sdl.SdlAudioDevice.md
@@ -76,6 +76,22 @@ Opens the default playback device, or returns a silent device (and says why) whe
public static IAudioDevice TryCreate()
```
+#### Returns
+
+ [IAudioDevice](MicroEngine.Audio.IAudioDevice.md)
+
+### TryCreate\(ILog\)
+
+Opens the default playback device, or returns a silent device and writes why to log when there is none.
+
+```csharp
+public static IAudioDevice TryCreate(ILog log)
+```
+
+#### Parameters
+
+`log` [ILog](MicroEngine.Kernel.ILog.md)
+
#### Returns
[IAudioDevice](MicroEngine.Audio.IAudioDevice.md)
diff --git a/docs/evaluations/debug-1.md b/docs/evaluations/debug-1.md
new file mode 100644
index 00000000..0484ce2f
--- /dev/null
+++ b/docs/evaluations/debug-1.md
@@ -0,0 +1,118 @@
+# 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 the exception with its stack trace on standard
+ error and nothing else: no file, no versions or platform facts, no record of the frames before it. A tester who
+ double-clicked the game has no console and nothing to send. **Fails.** (Corrected after the map: the map read the
+ host-creation error line, which prints the message only; a game's run error already printed the stack.)
+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-