# Debug inspector of your game (debug.inspector) One debug panel for every game: frame time, GPU, rendering counters, wireframe, collision boxes, the scene tree, and the debug modes your kits add. The same data is available as JSON for a page and for agents. Readable offline: `gripforge docs debug-inspector`, or the MCP tool `gripforge_docs` (topic `debug-inspector`). ## What it is for - **Frame**: frames per second, mean frame time, p50 / p95 / p99, the worst frame, frames over 33 ms (a window of the last 240 frames), and where the time of a frame goes. - **GPU**: the backend (`webgpu`, `webgl2`, `vulkan`, `dx12`, `dx11`, `metal`, `opengl`, `gles`), the adapter, its vendor and type, and whether it is a software rasteriser (the measures then say nothing about a real GPU). - **Rendering**: draw calls, drawn meshes, triangles, materials, lights, shadow casters, textures and their bytes, entities, memory. - **Modes**: wireframe, collision boxes (the `size` of every entity), the bounds of every drawn mesh. - **Scene tree**: every scene and its nodes and meshes, within a depth and node budget. A node can be hidden, outlined or selected (components, transform, mesh, the kit entity it draws). - **Kit modes**: the modes other kits register (paths of the AI, display optimisation, and so on). ## Turn it on `debug.inspector` is installed in every new game and open to everyone by default; existing games receive it at their next opening. Press **F3** (on a Mac: `fn + F3`, or the fallback **Ctrl + Alt + D**). Nothing is loaded, measured, allocated or drawn until the key is pressed. ```text gripforge_gamekit_install { project_id: "gkp_…", id: "debug.inspector" } gripforge_gamekit_configure { project_id: "gkp_…", id: "debug.inspector", config: { "access": { "players": false }, "toggleKey": "F4" } } gripforge_gamekit_configure { project_id: "gkp_…", id: "debug.inspector", config: { "enabled": false } } # off for the game ``` Removing the kit has the same effect as `enabled: false`: no key, no panel, no page or agent API. It requires `input.actions` and `game.engine`. The full reference is `gripforge kit api debug.inspector`. ## Settings and defaults | Key | Default | Meaning | | --- | --- | --- | | `enabled` | `true` | the developer's switch; `false` turns the whole inspector off | | `toggleKey` | `F3` | the key that opens and closes it; `null` = no key (then only `?debug=1` or the API). Free in every control scheme | | `access.team` | `true` | the owner, the members of the workspace and a local build (engine editor, desktop export run by its developer) | | `access.players` | `true` | public players (`false`: the team only) | | `access.query` | `debug` | `?debug=1` opens it at start for those allowed | | `refreshFrames` | 10 | frames between two snapshots while open | | `treeDepth`, `treeMaxNodes` | 6, 2000 | budget of the scene tree | | `startModes` | all `false` | `wireframe`, `colliders`, `bounds` on at start | | `panel` | `{ side: "right", width: 380, layer: "top" }` | where the panel sits | | `modeFrames` | 15 | frames between two calls of an open kit mode | The key goes through `input.actions` like every key. It is registered only when the inspector is enabled and the viewer is allowed, and it is never shown in the panel nor listed in a Controls screen. ## Cost Closed and unread, the inspector costs one key test per frame. On the web nothing of the panel is loaded (code, markup, styles, passport requests) until the first press; the first press brings it in with one dynamic import. Open, the engine measures every frame within fixed bounds (a ring of 240 frame times, counts every 30 frames), rebuilds the boxes every 6 frames and the snapshot every `refreshFrames`. The scene tree is built only when asked, the wireframe within 600 000 edges. ## The API (`gripforge.debug/1`) One contract on every engine. A **snapshot** is `{ schema, engine, open, sampling, frame, timing, gpu, render, modes, selection, hidden, outlined, tree?, details?, tick?, extensions, notes }`. **Ops** are sent alone, in a list, or as `{ ops: [...] }`: | Op | Fields | Does | | --- | --- | --- | | `snapshot` | | answers the snapshot (keeps the measures for 10 s when the panel is closed) | | `open`, `close` | | opens or closes the panel | | `mode` | `wireframe`, `colliders`, `bounds` (booleans, any subset) | turns modes on or off | | `tree` | `root?`, `depth?`, `maxNodes?` | builds the scene tree once | | `select` | `ids` | selects nodes (outlined, details of the first) | | `hide` | `id`, `hidden` | hides or shows a node | | `outline` | `id`, `on` | outlines a node | | `reset` | | shows every hidden node and removes every outline | | `extension` | `id`, `on` | turns a kit mode on or off | **TICK** (section of the F3 panel, field `tick` of the snapshot): the game logic tick, separate from the render frame and the physics step. It gives the requested and played rate (`hz`), the tick counter, ticks per frame, logic milliseconds per tick, the time dropped by catch-up, the interpolation alpha and the configured periods (`tick.systems`). The 15/20/30/60 buttons change the rate live (next tick boundary, nothing reset; in multiplayer only the authority). Ways to reach it: - **Agents**: MCP `gripforge_debug_snapshot` and `gripforge_debug_set`; CLI `gripforge debug snapshot ` and `gripforge debug set ''`. They act on a game open in a browser by a member of the team, with the same rights as the panel. - **The page**: `gripforgePresenter.debug(json)` in the frame of the native player (synchronous: the last snapshot; the ops are applied at the next frame), and the runtime protocol operations `debug_snapshot` / `debug_set`. - **A kit**: the service `debug.inspector` (`allowed()`, `isOpen()`, `open(by)`, `close()`, `toggle(by)`, `set(ops)`, `view()`, `modes()`, `addMode(mode)`, `setMode(id, on)`, `scenes()`). ## Hooks, events, registry | What | Name | Detail | | --- | --- | --- | | Hook | `debug.open` | payload `{ by, viewer }` (`by`: key, query, api; `viewer`: owner, member, local, player); cancellable. A game or a mod refuses it (a ranked match); the inspector stays closed and `debug.refused` follows | | Events | `debug.opened`, `debug.closed`, `debug.mode`, `debug.refused` | | | Registry | `debug_modes` | the kit modes; a game or a mod adds one (moddable, `extends` allowed) | | Moddable config | `enabled`, `toggleKey`, `access`, `refreshFrames`, `treeDepth`, `treeMaxNodes`, `startModes`, `panel`, `modeFrames` | a mod changes them like the game does | ### Add a mode A debug mode is data: `{ id: ".", kit, label, kind, call, args?, on? }`. `kind` is `overlay` (the call answers what to draw, `{ lines | paths | marks | boxes, color }` in world units), `info` (rows for the panel) or `scene` (nodes listed beside the scene tree, for a kit with its own notion of scene). While a mode is on and the panel open, the inspector calls it every `modeFrames` frames and hands the answer to the engine. A kit adds modes with `addMode(mode)` when it mounts: ```json { "id": "ai.navigation.paths", "kit": "ai.navigation", "label": "AI paths", "kind": "overlay", "call": "ai.navigation.debugPaths" } ``` ## Engines The measures and the drawing come from one Rust core, adapted by each engine with its own tools. The web and native players implement the full contract. Godot, Unity and Unreal follow the same contract. ## When it does not do what you expect | Symptom | Cause and fix | | --- | --- | | F3 does nothing on a Mac | the F-keys are media keys: hold **fn** (`fn + F3`), or use the fallback **Ctrl + Alt + D** (same rule), or `?debug=1` | | F3 does nothing | the kit is removed or `enabled: false`; the viewer is refused by `access` (players: false) or by a `debug.open` tap; `toggleKey` was changed or is `null` | | The GPU line says software | the measures come from a software rasteriser; measure on a machine with a real GPU | | The scene tree is cut | raise `treeMaxNodes` / `treeDepth`, or send a `tree` op with a narrower `root` | | A kit mode is missing from the list | the kit did not register it with `addMode`, or it is not in `debug_modes` | ## Turning a Worker off (comparison) `?workers=0` turns every Worker off; `?worker.render=0`, `?worker.physics=0` or `?worker.=0` (e.g. `worker.physics.rapier=0`) one only; `=1` asks it (it still falls back with a reason if impossible). `?playerWorker=0` still works. The same keys in localStorage: `gripforge.workers`, `gripforge.worker.`; the address wins. F3 and the snapshot (`renderThread`, `physicsThread`) then read `main (disabled by option (...))`.