9.1 KiB
The verification loop in detail
The core loop, the readiness gate, and the blind-judgment rule are in SKILL.md. This file has the
tool table (what exists where), the settle details, and the symptom to action map.
Two tooling tiers
Baseline (any project, no MCP)
This always works and needs nothing installed beyond the package setup.
- Launch:
flutter run --enable-flutter-gpu(native; add-d chromefor web). The flag is mandatory. - Console: read the run log. The readiness line,
debugPrintoutput, asserts, and the 0.22.0 blank-frame diagnostic all land here. - Frame: take a screenshot of the running app after it settles.
That is the whole loop when there is no editor. Run, settle, screenshot, read the log, correct.
Editor MCP (flutter_scene_mcp, when connected)
The editor exposes richer observation. Tool names below are exact. Do not assume they exist unless the MCP is actually connected for the current project.
| Tool | What it does | Reach for it when |
|---|---|---|
run_project |
Launch the editor-managed Play session (a managed flutter run). |
Starting a session under the editor. |
build_project |
Start the selected build config; output streams to the console. | You want a build without launching. |
stop_project |
Stop the running session. | Ending or restarting cleanly. |
hot_reload |
Hot reload the running debug session. | A Dart-only change, fastest turnaround. |
hot_restart |
Hot restart the session. | State or startup changed, or reload did not take. |
get_console |
The build/run console tail plus building/running flags. | EVERY iteration, paired with a screenshot. |
screenshot_viewport |
The viewport as a PNG, what the user sees. | EVERY iteration, paired with the console. |
describe_scene |
The scene-graph tree (ids, paths, names, component types). | Confirming a node/mesh is actually in the scene. |
scan_for_nans |
Capture a frame and scan every float render target for NaN/Inf in pass order. | A black or garbage frame with no error. Find where non-finite values start. |
capture_render_graph |
Capture the next frame's graph with thumbnails. | You need to see intermediate buffers. |
list_render_passes |
The executed passes in order with CPU timings and the buffer keys each read/wrote, plus target formats and sizes. No images. | Learning which pass owns which buffer, and the key names to read. |
get_pass_output |
Render one captured buffer (a key like scene_color, linear_depth) as a PNG. NaN paints magenta, Inf yellow, negative blue. |
Eyeballing an intermediate buffer to see which stage broke. |
read_pass_pixel |
One pixel's exact float RGBA from a captured buffer, with NaN/Inf flags. | Confirming an exact value (is this really 0, or NaN, or negative). |
list_viewport_debug_modes |
The available debug outputs (final, HDR color, linear depth, normals, AO, shadow atlas, ...) and which is active. | Seeing what debug views exist. |
set_viewport_debug_mode |
Render one debug output full-viewport. Set final to restore. |
Inspecting depth/normals/AO live, paired with screenshot_viewport. |
Render-graph capture (capture_render_graph, list_render_passes, get_pass_output,
read_pass_pixel, scan_for_nans) is gated on Scene.debugAllowRenderGraphCapture. It is a debug
opt-in, so a release build or a scene that never armed it returns nothing. The editor arms it for you;
outside the editor, set Scene.debugAllowRenderGraphCapture = true and call
Scene.captureRenderGraph(...) directly.
Settle, do not seed
Frames differ from run to run for benign reasons. That is normal, not a bug to eliminate.
- Auto-exposure (
Scene.autoExposure) ramps toward the target overspeedUp/speedDownseconds, so the first second is darker or brighter than the settled image. - Particles and trails carry a random phase, so a
ParticleSystemlooks different every launch. - Animations are mid-clip unless you seek them, so a screenshot lands on an arbitrary frame.
- Image-based lighting re-bakes after the first present on some paths, so reflections dim in for a frame before they are correct.
So let the scene settle before you trust a capture. Watch until the image stops changing, or advance a fixed few frames, then screenshot. Judge the settled frame, not the first one.
Seeding is a different job. Strict determinism (a fixed random seed, a pinned animation time, a frozen exposure) is what you set up for pixel-exact regression comparison, where two runs must be byte-identical. You do not need it for ordinary observation. For "does this change look right", settle and look. Reserve the seeding work for when you are building a golden or diffing two runs at the pixel level.
Symptom to action map
Localize before editing. Each row says what to capture first and the mistakes it usually points back
to. The mistakes are detailed in the flutter_scene-idioms skill's references/traps.md; this map
routes a symptom to the right one.
Entirely black frame
- Read the console FIRST. If
Flutter Scene is not ready to render. Skipping frame.is printing, it is the readiness gate, not your scene. Wait forScene.initializeStaticResources(). Stop here. - In 0.22.0 a frame that issues zero draws prints once in debug naming the likely cause (not ready, empty region, no views, no visible meshes, or a layer mask matching nothing). Read that line.
- If draws are happening but the image is black,
scan_for_nans. A NaN or Inf anywhere upstream collapses the final image to black, and the scan names the first offending pass. Thenget_pass_outputon that pass's buffer (NaN shows magenta) to confirm. - Common non-NaN causes: a degenerate camera (target equals position,
upparallel to the view direction on a top-down camera, FOV passed in degrees not radians),layerMask: 0, an oversized environment texture that failed to allocate on the device. See traps #23 and #16.
Washed-out, low-contrast, or too-bright color
screenshot_viewportafter settling, and check whether auto-exposure has finished ramping (a too-bright first second is just the ramp).- If it persists, suspect a shader-output contract break. A custom
ShaderMaterial/PostEffect/sky shader must output linear HDR premultiplied by alpha. Tone-mapping or gamma-encoding in the shader gets applied a second time by the resolve pass, giving exactly this washed-out look. See traps #37 and the rootMATERIALS.md. - Also check for a non-color texture bound as color (a normal or metallic-roughness map without the
right
TextureContent), which reads wrong and distance-dependent. Trap #2. - Hand-packed vertex data at the wrong stride also washes out color (the color attribute lands at the
wrong offset).
describe_sceneplus trap #17.
See-through or inside-out faces
set_viewport_debug_modeto normals (orget_pass_outputon the normals buffer) and look at the orientation. Inverted normals confirm a winding problem.- Cause is almost always clockwise hand-built triangles. flutter_scene front faces wind COUNTER-CLOCKWISE (CCW) in model space, matching glTF and standard conventions. Ensure triangle indices wind CCW around the outward face normal, or omit normals and let the constructor derive them. NEVER fix orientation with a per-triangle winding flip on an imported model; that leaves normals and IBL wrong. Traps #13 and #17.
- For an imported model rendered mirrored, check you did not overwrite the runtime importer's
scale(1, 1, -1)handedness root. Trap #5.
Missing or popping geometry
describe_sceneto confirm the node is actually in the graph. If it is absent, it is a scene-build bug, not a render bug.- If it is present but invisible, check the layer mask (
Node.layersis a bitmask, NOT inherited, and must match the view'slayerMask;layers = 2means1 << 1, not "layer 2"). Trap #10. - If it appears and disappears with camera angle, the bounds do not cover the geometry (a
caller-supplied
boundsorsetLocalBoundsthat is too small, or a swapped primitive geometry on an older version). Widen or omit the bounds. Traps #8 and #24. - A moved skinned mesh that will not move is the skinned-node transform being ignored by design; move the skeleton root instead. Trap #4.
A value looks numerically wrong (not visually)
Use read_pass_pixel on the relevant buffer to read the exact float RGBA at a coordinate, with NaN/Inf
flags. This settles "is this pixel actually 0.5, or is it NaN, or negative" without eyeballing a PNG
that the display remap has already clamped.
Judgment, restated
The blind-pairwise rule from SKILL.md is the part most likely to be skipped, so it bears repeating
here. When you have a before and an after, put them side by side and pick the better one as a binary
A-or-B choice against a reference or the previous frame. Do not narrate a score. A solo score climbs
on its own because you are grading your own progress; a blind pick between two concrete frames does
not. Every visual review runs through a pairwise pick, including the ones that feel too obvious to
bother with.