A runnable example that witnesses the UV-layer authoring hazard behind
bmesh.ops.create_grid(..., calc_uvs=True): the flag is a silent no-op
unless a UV layer already exists on the BMesh. AI-generated Blender code
commonly trusts calc_uvs=True, wires an Image Texture, and ships a mesh that
renders as one flat color — every fragment samples texel (0, 0).
Found while authoring image-pixels-testcard;
lifted into its own smoke-gated witness so the contract cannot quietly drift.
What it witnesses: three related contracts on the same grid topology
(SEG×SEG faces, (SEG+1)² verts, size 1.0 → coords in [-1, 1]):
- Silent no-op —
create_grid(..., calc_uvs=True)with no priorbm.loops.layers.uv.new(...)leaveslen(bm.loops.layers.uv) == 0andmesh.uv_layersempty afterto_mesh. - Pre-create repair — create the UV layer first, then
calc_uvs=Truefills every loop. UVs must match the closed formu = (x/size + 1)/2,v = (y/size + 1)/2within1e-6, both on the BMesh and after persisting tomesh.uv_layers.active.data. - Explicit assignment fallback —
calc_uvs=False, thenuv.new("UVMap")and a loop write of the same closed form. Does not depend oncalc_uvsat all; same tolerance.
What each check catches on failure:
- Silent no-op —
calc_uvs=Truestarts creating a layer on its own (falsified: expectingn_layers == 0fails if a layer appears; temporarily pre-creating a layer before the hazard probe exits 3). - Topology —
create_gridsegment/size contract drift (wrong face/vert counts). - Closed-form UVs —
calc_uvsfilling a different parameterization, or a one-texel shift (falsified: adding0.1to every U exited 6 with measured error0.1). - Mesh persistence — UV layer or loop data lost across
to_mesh. - Explicit path — loop assignment or the closed form itself regressing.
- Pixel witness (with
--output) — the saved PNG is read back and probed at each panel's projected center: the hazard panel must be one flat teal (per-channel spread ≤ 0.02, ordering b>g>r), the repaired panel a high-contrast checker (spread ≥ 0.25). Falsified twice: secretly giving the hazard panel UVs exited 11 with measured spread0.7333; repainting texel (0,0) magenta exited 12 with measured rgb(0.927, 0.118, 0.536).
Version divergence: none probed — the silent no-op and closed-form UV
fill assert identically on Blender 4.5 LTS and 5.1. The only gate in the file
is the EEVEE engine id for the optional render (BLENDER_EEVEE_NEXT on 4.x,
BLENDER_EEVEE on 5.x).
Render: two framed lightbox displays on floor trays (rear kick legs,
status LED) sharing one neon checker image, shot at a slight 3/4. Left is the
hazard (no UV layer) — flat teal of texel (0, 0). Right is the repair
(pre-create + calc_uvs) — full magenta/cyan checker. If the UV contract
failed, both panels would read the same — and the still is not just an
illustration: the script re-reads its own render and exits non-zero unless
the pixels prove the flat-vs-checker split (measured on 5.1.2: hazard spread
0.0078, repair spread 0.7412; identical on 4.5.11).
# Cheap correctness check (no render) — the CI check:
blender --background --python uv_layer_grid.py --
# Falsifier: pre-create a UV layer on the silent-no-op probe. Must exit non-zero.
blender --background --python uv_layer_grid.py -- --precreate-on-hazard
# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
blender --background --python uv_layer_grid.py -- --output uv.png
blender --background --python uv_layer_grid.py -- --output uv.png --engine cyclesPer-script sequential checks. 9 is a valid check code; there is no rule
against it. This example does not call the gallery framing helper; 10
is the missing-render-file check.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Silent-no-op hazard gone (--precreate-on-hazard lands here) |
| 4 | Grid topology drifted |
| 5 | Pre-create + calc_uvs did not persist one UV layer |
| 6 | calc_uvs closed-form error |
| 7 | Mesh UV round-trip error |
| 8 | calc_uvs=False unexpectedly created a UV layer |
| 9 | Explicit UV assignment error |
| 10 | --output produced no file |
| 11 | Broken panel is not flat |
| 12 | Broken panel is not texel-(0,0) teal |
| 13 | Repaired panel is not a checker |
| 14 | Broken and repaired panels render identically |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS
(5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch).
Smoke does not pass --output or --precreate-on-hazard.