From e457a124e1661f3d717fe7394a447d1f4faecca8 Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Sun, 13 Sep 2026 10:27:25 -0400 Subject: [PATCH 1/2] feat: add CLI falsifiers and exit tables to the remaining twelve examples Closes the last behavior gap in the retrofit: every example now has a flag that makes an existing assertion fail, without touching Grease Pencil version shims or changing any exit code value. Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor --- examples/compositor-glare/README.md | 24 +++++++++++-- examples/compositor-glare/compositor_glare.py | 14 ++++++-- examples/curve-bevel-arc/README.md | 28 +++++++++++++-- examples/curve-bevel-arc/curve_bevel_arc.py | 14 ++++++-- examples/gp-lineart-contour/README.md | 26 ++++++++++++-- .../gp-lineart-contour/gp_lineart_contour.py | 14 ++++++++ examples/grease-pencil-rosette/README.md | 27 ++++++++++++-- .../grease_pencil_rosette.py | 19 +++++++--- examples/light-link-studio/README.md | 27 ++++++++++++-- .../light-link-studio/light_link_studio.py | 13 +++++-- examples/png-exr-alpha/README.md | 31 +++++++++++++--- examples/png-exr-alpha/png_exr_alpha.py | 24 ++++++++----- examples/shader-node-group/README.md | 25 +++++++++++-- .../shader-node-group/shader_node_group.py | 15 ++++++-- examples/sky-texture-sun-elevation/README.md | 26 ++++++++++++-- .../sky_texture_sun_elevation.py | 19 ++++++++-- examples/text-version-stamp/README.md | 35 +++++++++++++++---- .../text-version-stamp/text_version_stamp.py | 18 +++++++--- examples/turntable/README.md | 26 ++++++++++++-- examples/turntable/turntable.py | 22 ++++++++---- examples/vse-cut-list/README.md | 33 ++++++++++++++--- examples/vse-cut-list/vse_cut_list.py | 18 ++++++++-- examples/vse-gamma-cross/README.md | 26 ++++++++++++-- examples/vse-gamma-cross/vse_gamma_cross.py | 17 ++++++--- 24 files changed, 454 insertions(+), 87 deletions(-) diff --git a/examples/compositor-glare/README.md b/examples/compositor-glare/README.md index 373b2c7..94db31c 100644 --- a/examples/compositor-glare/README.md +++ b/examples/compositor-glare/README.md @@ -29,11 +29,29 @@ absence of any EEVEE bloom toggle. # Cheap correctness check (two tiny renders) — the CI check: blender --background --python compositor_glare.py -- +# Falsifier: Threshold=10.0. Must exit non-zero (Threshold==1.0). +blender --background --python compositor_glare.py -- --threshold-high + # Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python compositor_glare.py -- --output rings.png blender --background --python compositor_glare.py -- --output rings.png --engine cycles ``` -It exits non-zero on failure (wrong tree plumbing, wrong Glare configuration, a -missing or non-falling halo, or halo pixels that appear without the compositor). -The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS. +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | Compositor tree / Glare / Threshold / link-chain contract (`--threshold-high` lands here) | +| 4 | Check render missing, tube dark, no halo, or halo does not fall off | +| 5 | Halo present with compositing off | +| 6 | `--output` produced no file | + +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 `--threshold-high`. diff --git a/examples/compositor-glare/compositor_glare.py b/examples/compositor-glare/compositor_glare.py index da94046..e78dc42 100644 --- a/examples/compositor-glare/compositor_glare.py +++ b/examples/compositor-glare/compositor_glare.py @@ -17,12 +17,17 @@ the ring silhouette, falls off strictly with distance, and vanishes entirely when `scene.render.use_compositing` is off. +``--threshold-high`` sets the shared ``Threshold`` input to 10.0 and still +asserts it equals 1.0. That is the falsifier (``--same-axis`` in +export-preset-axis). The 4.x/5.x Type-menu shim is untouched. + By default it runs only the correctness check (two 96x54 single-sample Cycles renders, compositor on vs off) — the CI smoke check. Pass --output to also render a still: - blender --background --python compositor_glare.py -- # check only - blender --background --python compositor_glare.py -- --output n.png # + render + blender --background --python compositor_glare.py -- # check only + blender --background --python compositor_glare.py -- --threshold-high # must fail + blender --background --python compositor_glare.py -- --output n.png # + render """ import bpy import sys @@ -337,9 +342,14 @@ def main(): help="render engine for --output (cycles for GPU-less hosts)") p.add_argument("--samples", type=int, default=32, help="--output sample count") p.add_argument("--width", type=int, default=1280, help="--output width; height is width*9/16") + p.add_argument("--threshold-high", action="store_true", + help="falsifier: Threshold=10.0, still assert Threshold==1.0") args = p.parse_args(argv) scene, tree, glare = build_scene() + if args.threshold_high: + glare.inputs['Threshold'].default_value = 10.0 + code = check_structure(scene, tree, glare) if code: return code diff --git a/examples/curve-bevel-arc/README.md b/examples/curve-bevel-arc/README.md index b9aff28..0958899 100644 --- a/examples/curve-bevel-arc/README.md +++ b/examples/curve-bevel-arc/README.md @@ -17,12 +17,34 @@ and that the depsgraph-evaluated mesh has the deterministic topology (1044 verts # Cheap correctness check (no render) — the CI check: blender --background --python curve_bevel_arc.py -- +# Falsifier: uncapped tube. Must exit non-zero (use_fill_caps). +blender --background --python curve_bevel_arc.py -- --no-caps + # Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python curve_bevel_arc.py -- --output arc.png blender --background --python curve_bevel_arc.py -- --output arc.png --engine cycles ``` -It exits non-zero on failure (wrong point count, bevel, caps, topology, or span). -The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS. +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is also the shared framing helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | Spline type is not BEZIER | +| 4 | Bezier point count ≠ 8 | +| 5 | `bevel_depth` ≠ 0.15 | +| 6 | `use_fill_caps` is False (`--no-caps` lands here) | +| 7 | Evaluated vert/face count off measured tessellation | +| 8 | Tube does not rest on the floor | +| 9 | Tube height ≠ 2 × bevel | +| 10 | X span off closed form; also gallery framing violation | +| 11 | `--output` produced no file | -The `--output` render path additionally measures framing against the Layer 1 band via `examples/gallery_framing.py` (exit 10 on violation) before writing the still. +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 `--no-caps`. diff --git a/examples/curve-bevel-arc/curve_bevel_arc.py b/examples/curve-bevel-arc/curve_bevel_arc.py index 2046059..9041007 100644 --- a/examples/curve-bevel-arc/curve_bevel_arc.py +++ b/examples/curve-bevel-arc/curve_bevel_arc.py @@ -9,10 +9,14 @@ has no simple closed form, so those two constants pin today's behavior (see EXPECT_VERTS below for how to re-measure if a future Blender retessellates). +``--no-caps`` leaves ``use_fill_caps`` False and still asserts the ends are +capped. That is the falsifier (``--same-axis`` in export-preset-axis). + By default it runs only the correctness check (no render) — the CI smoke check. Pass --output to also render a still: blender --background --python curve_bevel_arc.py -- # check only + blender --background --python curve_bevel_arc.py -- --no-caps # must fail blender --background --python curve_bevel_arc.py -- --output c.png # + render """ import bpy, bmesh, sys, os, math, argparse @@ -35,14 +39,15 @@ EXPECT_FACES = 1028 -def build(): +def build(no_caps=False): bpy.ops.wm.read_factory_settings(use_empty=True) curve = bpy.data.curves.new("Arc", 'CURVE') curve.dimensions = '3D' curve.bevel_depth = BEVEL curve.bevel_resolution = BEVEL_RES curve.resolution_u = RES_U - curve.use_fill_caps = True # solid ends — not a hollow pipe + curve.use_fill_caps = not no_caps # solid ends — not a hollow pipe + spline = curve.splines.new('BEZIER') spline.bezier_points.add(N_POINTS - 1) # one point exists already @@ -231,9 +236,12 @@ def main(): p.add_argument("--output", default=None, help="optional: render a still PNG here") p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"), help="render engine for --output (cycles for GPU-less hosts)") + p.add_argument("--no-caps", action="store_true", + help="falsifier: use_fill_caps=False, still assert caps") args = p.parse_args(argv) - obj = build() + obj = build(no_caps=args.no_caps) + code = check(obj) if code: return code diff --git a/examples/gp-lineart-contour/README.md b/examples/gp-lineart-contour/README.md index a063dfc..ada7872 100644 --- a/examples/gp-lineart-contour/README.md +++ b/examples/gp-lineart-contour/README.md @@ -36,11 +36,31 @@ not zeroing strokes (exit 6), flags-off or restore failure (exit 7). # Depsgraph contour check — the CI check: blender --background --python gp_lineart_contour.py -- +# Falsifier: use_contour off. Must exit non-zero. +blender --background --python gp_lineart_contour.py -- --no-contour + # Also render the gallery still: blender --background --python gp_lineart_contour.py -- --output lineart.png ``` -It exits non-zero on failure. The `blender-smoke` workflow runs the check on -Blender 5.2 LTS and 4.5 LTS. +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is the shared framing helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage; also GPv3 collection address contract | +| 3 | LINEART `thickness` / `radius` trap for this Blender | +| 4 | LINEART type, source, or `use_contour` (`--no-contour` lands here) | +| 5 | Evaluated contour too thin | +| 6 | Cleared `source_object` still produced strokes | +| 7 | Contour+crease off still produced strokes, or restore failed | +| 8 | `--output` produced no file | +| 10 | Gallery framing violation | -The `--output` render path additionally measures framing against the Layer 1 band via `examples/gallery_framing.py` (exit 10 on violation) before writing the still. +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 `--no-contour`. diff --git a/examples/gp-lineart-contour/gp_lineart_contour.py b/examples/gp-lineart-contour/gp_lineart_contour.py index f155cad..631ea30 100644 --- a/examples/gp-lineart-contour/gp_lineart_contour.py +++ b/examples/gp-lineart-contour/gp_lineart_contour.py @@ -17,9 +17,15 @@ The check evaluates the modifier through the depsgraph (no bake required) and asserts stroke/point lower bounds against the known failure modes above. +``--no-contour`` clears ``use_contour`` after the modifier is built and +still asserts it is True. That is the falsifier (``--same-axis`` in +export-preset-axis). The GPv3 address shim and the thickness/radius trap +are untouched. + By default it runs the correctness check only. Pass --output to render: blender --background --python gp_lineart_contour.py -- + blender --background --python gp_lineart_contour.py -- --no-contour blender --background --python gp_lineart_contour.py -- --output l.png """ import bpy, bmesh, sys, os, math, argparse @@ -415,10 +421,18 @@ def main(): choices=("eevee", "cycles"), help="render engine for --output", ) + p.add_argument( + "--no-contour", + action="store_true", + help="falsifier: use_contour=False, still assert True", + ) args = p.parse_args(argv) print(f"binary version: {bpy.app.version} ({bpy.app.version_string})") sc, crystal, la_ob, mod = build_scene() + if args.no_contour: + mod.use_contour = False + code = check(sc, crystal, la_ob, mod) if code: return code diff --git a/examples/grease-pencil-rosette/README.md b/examples/grease-pencil-rosette/README.md index 615fa0d..2cc8ed0 100644 --- a/examples/grease-pencil-rosette/README.md +++ b/examples/grease-pencil-rosette/README.md @@ -27,11 +27,32 @@ raw `position` attribute buffer via `foreach_get`. # Cheap correctness check (no render) — the CI check: blender --background --python grease_pencil_rosette.py -- +# Falsifier: open strokes. Must exit non-zero (cyclic). +blender --background --python grease_pencil_rosette.py -- --open-strokes + # Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python grease_pencil_rosette.py -- --output rosette.png blender --background --python grease_pencil_rosette.py -- --output rosette.png --engine cycles ``` -It exits non-zero on failure (wrong version gate, structural mismatch, missing attribute -layers, or attribute-buffer deviation from the closed form). The `blender-smoke` workflow -runs the check on Blender 5.2 LTS and 4.5 LTS. +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | GPv3 address / legacy trap for this Blender | +| 4 | Object type, layer count, or frame number | +| 5 | Stroke topology or cyclic (`--open-strokes` lands here) | +| 6 | Lazy attribute layers missing or wrong domain/type | +| 7 | Position buffer length or closed-form round-trip | +| 8 | Grease Pencil material missing gpencil settings | +| 9 | `--output` produced no file | + +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 `--open-strokes`. diff --git a/examples/grease-pencil-rosette/grease_pencil_rosette.py b/examples/grease-pencil-rosette/grease_pencil_rosette.py index 8198404..6114288 100644 --- a/examples/grease-pencil-rosette/grease_pencil_rosette.py +++ b/examples/grease-pencil-rosette/grease_pencil_rosette.py @@ -19,11 +19,16 @@ materialization, and a closed-form round-trip of every position through the raw POINT attribute buffer. +``--open-strokes`` leaves every stroke non-cyclic and still asserts they +are cyclic. That is the falsifier (``--same-axis`` in export-preset-axis). +The GPv3 address shim is untouched. + By default it runs only the correctness check (no render) — the CI smoke check. Pass --output to also render a still: - blender --background --python grease_pencil_rosette.py -- # check only - blender --background --python grease_pencil_rosette.py -- --output r.png # + render + blender --background --python grease_pencil_rosette.py -- # check only + blender --background --python grease_pencil_rosette.py -- --open-strokes # must fail + blender --background --python grease_pencil_rosette.py -- --output r.png # + render """ import bpy, sys, os, math, argparse, colorsys @@ -66,7 +71,7 @@ def gp_data_new(name): return bpy.data.grease_pencils_v3.new(name) # 4.5 LTS: GPv3 lives at _v3 -def build_rosette(): +def build_rosette(open_strokes=False): bpy.ops.wm.read_factory_settings(use_empty=True) gp = gp_data_new("Rosette") layer = gp.layers.new("Ink") @@ -75,8 +80,9 @@ def build_rosette(): drawing.add_strokes([POINTS] * RINGS) for ring, stroke in enumerate(drawing.strokes): - stroke.cyclic = True + stroke.cyclic = not open_strokes for i, pt in enumerate(stroke.points): + pt.position = rose_point(ring, i) pt.radius = point_radius(ring, i) pt.opacity = 1.0 @@ -267,9 +273,12 @@ def main(): p.add_argument("--output", default=None, help="optional: render a still PNG here") p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"), help="render engine for --output (cycles for GPU-less hosts)") + p.add_argument("--open-strokes", action="store_true", + help="falsifier: strokes not cyclic, still assert cyclic") args = p.parse_args(argv) - obj = build_rosette() + obj = build_rosette(open_strokes=args.open_strokes) + code = check(obj) if code: return code diff --git a/examples/light-link-studio/README.md b/examples/light-link-studio/README.md index 677a7b6..1dc06ff 100644 --- a/examples/light-link-studio/README.md +++ b/examples/light-link-studio/README.md @@ -53,10 +53,31 @@ pool raking the wall behind them — one key, one hero. # Two-render correctness check (tiny Cycles CPU renders) — the CI check: blender --background --python light_link_studio.py -- +# Falsifier: receiver_collection unset. Must exit non-zero (assignment). +blender --background --python light_link_studio.py -- --skip-link + # Also render the gallery still (Cycles, deterministic samples): blender --background --python light_link_studio.py -- --output linked.png ``` -It exits non-zero on failure (API moved, assignment lost, ratio below gate, -insufficient unlink rise, or hero drift). The `blender-smoke` workflow runs -the check on Blender 5.2 LTS and 4.5 LTS. +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | Light datablock gained `light_linking` | +| 4 | Light objects lost `light_linking` | +| 5 | `receiver_collection` assignment did not read back (`--skip-link` lands here) | +| 6 | Linked state: hero dark or hero/decoy ratio below gate | +| 7 | Decoy rise below gate when unlinked | +| 8 | Hero drifted across the unlink | +| 9 | `--output` produced no file | + +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 `--skip-link`. diff --git a/examples/light-link-studio/light_link_studio.py b/examples/light-link-studio/light_link_studio.py index 9e82368..3ef7614 100644 --- a/examples/light-link-studio/light_link_studio.py +++ b/examples/light-link-studio/light_link_studio.py @@ -20,10 +20,14 @@ 4.5 LTS and 5.1 (ObjectLightLinking with receiver_collection/blocker_collection on both). +``--skip-link`` leaves ``receiver_collection`` unset and still asserts the +hero collection. That is the falsifier (``--same-axis`` in export-preset-axis). + By default it runs the two-render correctness check (no gallery still) — the CI smoke check. Pass --output to also render a still: blender --background --python light_link_studio.py -- # check only + blender --background --python light_link_studio.py -- --skip-link # must fail blender --background --python light_link_studio.py -- --output l.png # + render """ import bpy, bmesh, sys, os, math, argparse, tempfile, shutil @@ -288,7 +292,7 @@ def render_lumas(sc, tmp, name, hero, decoy): return out[0], out[1] -def check(sc, key, hero_c, hero, decoy): +def check(sc, key, hero_c, hero, decoy, skip_link=False): # contract 0 (RNA guard): the API is on the light OBJECT ld = key.data if hasattr(ld, "light_linking"): @@ -300,7 +304,7 @@ def check(sc, key, hero_c, hero, decoy): return 4 # contract 1 (assignment round-trip through the API itself) - key.light_linking.receiver_collection = hero_c + key.light_linking.receiver_collection = None if skip_link else hero_c if key.light_linking.receiver_collection != hero_c: print("ERROR: receiver_collection assignment did not read back", file=sys.stderr) @@ -398,12 +402,15 @@ def main(): argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] p = argparse.ArgumentParser() p.add_argument("--output", default=None, help="optional: render a still PNG here") + p.add_argument("--skip-link", action="store_true", + help="falsifier: leave receiver_collection unset, still assert hero_c") args = p.parse_args(argv) bpy.ops.wm.read_factory_settings(use_empty=True) sc = bpy.context.scene hero, decoy, hero_c, key = build_studio(sc) - code = check(sc, key, hero_c, hero, decoy) + code = check(sc, key, hero_c, hero, decoy, skip_link=args.skip_link) + if code: return code diff --git a/examples/png-exr-alpha/README.md b/examples/png-exr-alpha/README.md index 4895b56..446b4a4 100644 --- a/examples/png-exr-alpha/README.md +++ b/examples/png-exr-alpha/README.md @@ -63,13 +63,36 @@ contract failed, both panels would read the same. # Cheap correctness check (no render) — the CI check: blender --background --python png_exr_alpha.py -- +# Falsifier: opaque authored alpha. Must exit non-zero (float→PNG error floor). +blender --background --python png_exr_alpha.py -- --opaque-alpha + # Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python png_exr_alpha.py -- --output alpha.png blender --background --python png_exr_alpha.py -- --output alpha.png --engine cycles ``` -It exits non-zero on failure and prints every measured error and tolerance on -success, so CI logs carry the numbers. The `blender-smoke` workflow runs the -check on Blender 5.2 LTS and 4.5 LTS. +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is also the shared framing helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage; also closed-form palette no longer stresses unpremul | +| 3 | `float_buffer=True` image reports `is_float=False` | +| 4 | Float→PNG IHDR is not RGBA16 | +| 5 | Float→PNG RGB error below floor (`--opaque-alpha` lands here) | +| 6 | Float→PNG disagrees with closed-form false-unpremul model | +| 7 | Float→EXR round-trip above tolerance | +| 8 | `float_buffer=False` image reports `is_float=True` | +| 9 | Byte→PNG IHDR is not RGBA8 | +| 10 | Byte→PNG disagrees with straight-alpha 8-bit model; also gallery framing violation | +| 11 | Byte→PNG stress cell looks false-unpremul-mangled | +| 12 | EXR `color_mode=RGB` alpha-drop contract | +| 13 | `--output` produced no file | -The `--output` render path additionally measures framing against the Layer 1 band via `examples/gallery_framing.py` (exit 10 on violation) before writing the still. +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 `--opaque-alpha`. diff --git a/examples/png-exr-alpha/png_exr_alpha.py b/examples/png-exr-alpha/png_exr_alpha.py index f9b961d..d041dcc 100644 --- a/examples/png-exr-alpha/png_exr_alpha.py +++ b/examples/png-exr-alpha/png_exr_alpha.py @@ -10,11 +10,14 @@ with straight alpha and only pays ordinary quantization. AI-generated Blender code commonly trusts `Image.save()` to PNG for float -RGBA scratch buffers (masks, ID mattes, AOVs). Pass --output to also render -a staged still of two framed verification displays: left is the PNG-mangled -reload, right is the EXR-clean reload. +RGBA scratch buffers (masks, ID mattes, AOVs). ``--opaque-alpha`` authors +every pixel at alpha 1.0 and still asserts the float→PNG error floor. +That is the falsifier (``--same-axis`` in export-preset-axis). Pass --output +to also render a staged still of two framed verification displays: left is +the PNG-mangled reload, right is the EXR-clean reload. blender --background --python png_exr_alpha.py -- + blender --background --python png_exr_alpha.py -- --opaque-alpha blender --background --python png_exr_alpha.py -- --output alpha.png """ import bpy @@ -86,11 +89,12 @@ def expected_byte_png_rgba(r, g, b, a): return (q8(r), q8(g), q8(b), q8(a)) -def fill_pattern(img): +def fill_pattern(img, opaque_alpha=False): buf = [0.0] * (W * H * 4) + alphas = [1.0] * W if opaque_alpha else ALPHAS for y, rgb in enumerate(COLORS): r, g, b = rgb - for x, a in enumerate(ALPHAS): + for x, a in enumerate(alphas): i = (y * W + x) * 4 buf[i : i + 4] = [r, g, b, a] img.pixels.foreach_set(buf) @@ -169,7 +173,7 @@ def png_bit_depth(path): return None, None -def check(): +def check(opaque_alpha=False): bpy.ops.wm.read_factory_settings(use_empty=True) # --- Closed-form worst case on this palette (independent of Blender) --- @@ -198,7 +202,7 @@ def check(): img_f = new_float_image("FloatSrc") if not img_f.is_float: return fail("float_buffer=True image reports is_float=False", 3) - orig = fill_pattern(img_f) + orig = fill_pattern(img_f, opaque_alpha=opaque_alpha) png_got, png_path = save_and_reload(img_f, "PNG", "png") bit, color = png_bit_depth(png_path) @@ -651,9 +655,13 @@ def main(): "--engine", default="eevee", choices=("eevee", "cycles"), help="render engine for --output (cycles for GPU-less hosts)", ) + p.add_argument( + "--opaque-alpha", action="store_true", + help="falsifier: author alpha=1.0, still assert the float→PNG error floor", + ) args = p.parse_args(argv) - code = check() + code = check(opaque_alpha=args.opaque_alpha) if code != 0: return code if args.output: diff --git a/examples/shader-node-group/README.md b/examples/shader-node-group/README.md index 32be36c..b26ec9a 100644 --- a/examples/shader-node-group/README.md +++ b/examples/shader-node-group/README.md @@ -18,12 +18,31 @@ colors. # Cheap correctness check (no render) — the CI check: blender --background --python shader_node_group.py -- +# Falsifier: identical instance Tints. Must exit non-zero. +blender --background --python shader_node_group.py -- --same-tint + # Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python shader_node_group.py -- --output spheres.png blender --background --python shader_node_group.py -- --output spheres.png --engine cycles ``` -It exits non-zero on failure (missing interface sockets, unshared group, or identical -instance parameters). The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS. +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is the shared framing helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | Interface sockets missing Tint / Roughness / Shader | +| 4 | Group datablock `users` ≠ 2 | +| 5 | Instance points at a different node tree | +| 6 | Instance Tint values identical (`--same-tint` lands here) | +| 7 | `--output` produced no file | +| 10 | Gallery framing violation | -The `--output` render path additionally measures framing against the Layer 1 band via `examples/gallery_framing.py` (exit 10 on violation) before writing the still. +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 `--same-tint`. diff --git a/examples/shader-node-group/shader_node_group.py b/examples/shader-node-group/shader_node_group.py index 8374780..d4e1edd 100644 --- a/examples/shader-node-group/shader_node_group.py +++ b/examples/shader-node-group/shader_node_group.py @@ -8,10 +8,15 @@ datablock (users == 2), and their instance-level Tint values differ — the whole point of grouping. +``--same-tint`` copies SphereA's Tint onto both instances and still asserts +the values differ. That is the falsifier (``--same-axis`` in +export-preset-axis). + By default it runs only the correctness check (no render) — the CI smoke check. Pass --output to also render a still: blender --background --python shader_node_group.py -- # check only + blender --background --python shader_node_group.py -- --same-tint # must fail blender --background --python shader_node_group.py -- --output s.png # + render """ import bpy, bmesh, sys, os, math, argparse @@ -59,7 +64,7 @@ def material_from_group(name, tree, tint): return mat -def build_scene(): +def build_scene(same_tint=False): bpy.ops.wm.read_factory_settings(use_empty=True) tree = build_group() objs = [] @@ -75,7 +80,8 @@ def build_scene(): obj.location = (-1.35 + i * 2.7, 0.0, 1.0) for poly in me.polygons: poly.use_smooth = True - me.materials.append(material_from_group(f"Mat.{name}", tree, tint)) + inst_tint = TINTS["SphereA"] if same_tint else tint + me.materials.append(material_from_group(f"Mat.{name}", tree, inst_tint)) bpy.context.collection.objects.link(obj) objs.append(obj) return tree, objs @@ -199,9 +205,12 @@ def main(): p.add_argument("--output", default=None, help="optional: render a still PNG here") p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"), help="render engine for --output (cycles for GPU-less hosts)") + p.add_argument("--same-tint", action="store_true", + help="falsifier: identical instance Tints, still assert they differ") args = p.parse_args(argv) - tree, objs = build_scene() + tree, objs = build_scene(same_tint=args.same_tint) + code = check(tree, objs) if code: return code diff --git a/examples/sky-texture-sun-elevation/README.md b/examples/sky-texture-sun-elevation/README.md index d67069e..4690962 100644 --- a/examples/sky-texture-sun-elevation/README.md +++ b/examples/sky-texture-sun-elevation/README.md @@ -54,9 +54,31 @@ props"` rather than enforcing. # Zenith-luminance correctness check (tiny Cycles CPU EXR probes) — the CI check: blender --background --python sky_texture_sun_elevation.py -- +# Falsifier: Sky→Background unlinked. Must exit non-zero (world links). +blender --background --python sky_texture_sun_elevation.py -- --unlink-sky + # Also render the gallery diptych (Cycles): blender --background --python sky_texture_sun_elevation.py -- --output sky.png ``` -It exits non-zero on failure. The `blender-smoke` workflow runs the check on -Blender 5.2 LTS and 4.5 LTS. +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | `sky_type` is not the identifier for this Blender | +| 4 | `sun_elevation` round-trip failed | +| 5 | `dust_density` / `aerosol_density` / `NISHITA` / `MULTIPLE_SCATTERING` trap | +| 6 | Sky → Background → World Output links broken (`--unlink-sky` lands here) | +| 7 | High-elevation zenith luma below floor | +| 8 | Zenith luma did not rise with elevation | +| 9 | `--output` produced no file | + +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 `--unlink-sky`. diff --git a/examples/sky-texture-sun-elevation/sky_texture_sun_elevation.py b/examples/sky-texture-sun-elevation/sky_texture_sun_elevation.py index c870ef2..b1d02cd 100644 --- a/examples/sky-texture-sun-elevation/sky_texture_sun_elevation.py +++ b/examples/sky-texture-sun-elevation/sky_texture_sun_elevation.py @@ -12,10 +12,15 @@ 4. ``sun_elevation`` is load-bearing: raising it brightens zenith luminance — proven with two tiny Cycles EXR probes (straight-up camera) in one check. +``--unlink-sky`` drops the Sky → Background Color link and still asserts +the chain. That is the falsifier (``--same-axis`` in export-preset-axis). +The sky_type / dust_density version traps are untouched. + By default it runs the correctness check (tiny Cycles CPU renders, no gallery still). Pass --output to also render a still: blender --background --python sky_texture_sun_elevation.py -- + blender --background --python sky_texture_sun_elevation.py -- --unlink-sky blender --background --python sky_texture_sun_elevation.py -- --output s.png """ import bpy, bmesh, sys, os, math, argparse, tempfile, shutil @@ -304,7 +309,12 @@ def check_links(world): return 0 -def check(sc, world, sky, bg): +def check(sc, world, sky, bg, unlink_sky=False): + if unlink_sky: + nt = world.node_tree + for link in list(nt.links): + if link.from_node == sky and link.to_node.name == "Background": + nt.links.remove(link) code = check_links(world) if code: return code @@ -502,11 +512,16 @@ def main(): choices=("eevee", "cycles"), help="render engine for --output (cycles default: sky is a Cycles strength)", ) + p.add_argument( + "--unlink-sky", + action="store_true", + help="falsifier: drop Sky→Background link, still assert the chain", + ) args = p.parse_args(argv) print(f"binary version: {bpy.app.version} ({bpy.app.version_string})") sc, world, sky, bg = build_scene() - code = check(sc, world, sky, bg) + code = check(sc, world, sky, bg, unlink_sky=args.unlink_sky) if code: return code diff --git a/examples/text-version-stamp/README.md b/examples/text-version-stamp/README.md index 199a40b..509e374 100644 --- a/examples/text-version-stamp/README.md +++ b/examples/text-version-stamp/README.md @@ -28,15 +28,36 @@ on every LTS build; branch on the `bpy.app.version` tuple instead. # Cheap correctness check (no render) — the CI check: blender --background --python text_version_stamp.py -- +# Falsifier: body is not version_string. Must exit non-zero. +blender --background --python text_version_stamp.py -- --wrong-body + # Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python text_version_stamp.py -- --output stamp.png blender --background --python text_version_stamp.py -- --output stamp.png --engine cycles ``` -It exits non-zero on failure (wrong subclass, missing font, body/version mismatch, -non-planar flat text, extrude/bevel closed form off, geometry not regenerating, or a -`to_mesh_clear()` reference surviving). The `blender-smoke` workflow runs the check on -Blender 5.2 LTS and 4.5 LTS. The render scales the stamp to a constant width, so the frame -holds for any version-string length. - -The `--output` render path additionally measures framing against the Layer 1 band via `examples/gallery_framing.py` (exit 10 on violation) before writing the still. +The render scales the stamp to a constant width, so the frame holds for any +version-string length. + +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is the shared framing helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | Not a FONT `TextCurve` with the built-in Bfont | +| 4 | `body` is not the live `version_string` (`--wrong-body` lands here) | +| 5 | Flat text is not a filled planar mesh | +| 6 | Extrude / bevel closed form failed | +| 7 | Appending characters did not widen the text | +| 8 | Mesh survived `to_mesh_clear()` | +| 9 | `--output` produced no file | +| 10 | Gallery framing violation | + +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 `--wrong-body`. diff --git a/examples/text-version-stamp/text_version_stamp.py b/examples/text-version-stamp/text_version_stamp.py index 28bf92c..25768e0 100644 --- a/examples/text-version-stamp/text_version_stamp.py +++ b/examples/text-version-stamp/text_version_stamp.py @@ -15,11 +15,16 @@ `to_mesh_clear()` the returned Mesh reference is dead and any access raises ReferenceError. +``--wrong-body`` assigns a string that is not ``version_string`` and still +asserts the body is the live version. That is the falsifier (``--same-axis`` +in export-preset-axis). + By default it runs only the correctness check (no render) — the CI smoke check. Pass --output to also render a still: - blender --background --python text_version_stamp.py -- # check only - blender --background --python text_version_stamp.py -- --output v.png # + render + blender --background --python text_version_stamp.py -- # check only + blender --background --python text_version_stamp.py -- --wrong-body # must fail + blender --background --python text_version_stamp.py -- --output v.png # + render """ import bpy, sys, os, math, argparse @@ -33,10 +38,10 @@ TOL = 1e-4 -def build_stamp(): +def build_stamp(wrong_body=False): bpy.ops.wm.read_factory_settings(use_empty=True) txt = bpy.data.curves.new("VersionStamp", type='FONT') - txt.body = bpy.app.version_string # the self-documenting payload + txt.body = "not-a-version" if wrong_body else bpy.app.version_string txt.align_x = 'CENTER' txt.align_y = 'CENTER' obj = bpy.data.objects.new("VersionStamp", txt) @@ -285,9 +290,12 @@ def main(): p.add_argument("--output", default=None, help="optional: render a still PNG here") p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"), help="render engine for --output (cycles for GPU-less hosts)") + p.add_argument("--wrong-body", action="store_true", + help="falsifier: body is not version_string, still assert it is") args = p.parse_args(argv) - obj = build_stamp() + obj = build_stamp(wrong_body=args.wrong_body) + code = check(obj) if code: return code diff --git a/examples/turntable/README.md b/examples/turntable/README.md index d202771..3987b1a 100644 --- a/examples/turntable/README.md +++ b/examples/turntable/README.md @@ -16,6 +16,9 @@ channelbag comes from `action_ensure_channelbag_for_slot`; on 4.4/4.5 from # Cheap correctness check only (no render) — the CI smoke check: blender --background --python turntable.py -- +# Falsifier: no rotation keys. Must exit non-zero (keys drive playback). +blender --background --python turntable.py -- --no-keys + # Also render one still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts): blender --background --python turntable.py -- --output turntable.png blender --background --python turntable.py -- --output turntable.png --engine cycles @@ -23,6 +26,23 @@ blender --background --python turntable.py -- --output turntable.png --engine cy By default it runs only the **frame-independent correctness check**: it inserts the rotation keys, samples the object's Z rotation at frame 1 vs a later frame, and asserts they **differ** -(the keys drive playback). It exits non-zero on failure — the same check the `blender-smoke` -workflow runs on Blender 5.2 LTS and 4.5 LTS. `--output` additionally renders a still; the full -animated loop is a showcase extra, not part of the CI check. +(the keys drive playback). `--output` additionally renders a still; the full animated loop is +a showcase extra, not part of the CI check. + +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | Rotation keys do not drive playback (`--no-keys` lands here) | +| 4 | `--output` produced no file | +| 5 | Wrong-era EEVEE engine id was accepted | + +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 `--no-keys`. diff --git a/examples/turntable/turntable.py b/examples/turntable/turntable.py index 65290ac..85924bb 100644 --- a/examples/turntable/turntable.py +++ b/examples/turntable/turntable.py @@ -10,7 +10,12 @@ they DIFFER -- proving the keys drive playback. Exits non-zero on failure. This is the check the CI smoke gate runs on both builds. +``--no-keys`` skips inserting the rotation keys and still asserts they drive playback. +That is the falsifier (``--same-axis`` in export-preset-axis). The EEVEE-id +era check is untouched. + blender --background --python turntable.py -- # correctness check only + blender --background --python turntable.py -- --no-keys # must fail blender --background --python turntable.py -- --output t.png # also render one still blender --background --python turntable.py -- --output t.png --engine cycles # GPU-less """ @@ -29,7 +34,7 @@ def get_channelbag_for_slot(action, slot): strip = layer.strips[0] if layer.strips else layer.strips.new(type='KEYFRAME') return strip.channelbag(slot, ensure=True) -def build(): +def build(no_keys=False): bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.mesh.primitive_monkey_add(location=(0, 0, 1.0)) obj = bpy.context.active_object @@ -48,11 +53,12 @@ def build(): slot = act.slots.new(id_type='OBJECT', name=obj.name); obj.animation_data.action_slot = slot cbag = get_channelbag_for_slot(act, slot) fc = cbag.fcurves.new("rotation_euler", index=2) - fc.keyframe_points.insert(1, 0.0) - fc.keyframe_points.insert(FRAMES, math.radians(360)) - for kp in fc.keyframe_points: - kp.interpolation = 'LINEAR' - fc.update() + if not no_keys: + fc.keyframe_points.insert(1, 0.0) + fc.keyframe_points.insert(FRAMES, math.radians(360)) + for kp in fc.keyframe_points: + kp.interpolation = 'LINEAR' + fc.update() return obj def correctness(obj): @@ -122,6 +128,8 @@ def main(): p = argparse.ArgumentParser() p.add_argument("--output", default=None, help="optional: render one still to this PNG") p.add_argument("--engine", choices=["auto", "cycles"], default="auto") + p.add_argument("--no-keys", action="store_true", + help="falsifier: skip rotation keys, still assert they drive playback") args = p.parse_args(argv) # the EEVEE-id mapping is asserted regardless of whether we render: the @@ -135,7 +143,7 @@ def main(): pass # correctly rejected bpy.context.scene.render.engine = eid # raises TypeError if the helper's id is invalid - obj = build() + obj = build(no_keys=args.no_keys) if not correctness(obj): print("ERROR: rotation keys do not drive playback", file=sys.stderr); return 3 diff --git a/examples/vse-cut-list/README.md b/examples/vse-cut-list/README.md index 813cf72..9da8c39 100644 --- a/examples/vse-cut-list/README.md +++ b/examples/vse-cut-list/README.md @@ -112,6 +112,10 @@ explicitly. # Cheap correctness check (no render) — the CI check: blender --background --python vse_cut_list.py -- +# Falsifier: GC T2 -> T1. Must exit non-zero (wiring). `--check-pixels` is a +# second witness, not this falsifier. +blender --background --python vse_cut_list.py -- --swap-inputs + # Compositing witness on a tiny render (Cycles CPU, CI-safe): blender --background --python vse_cut_list.py -- --check-pixels --engine cycles @@ -119,8 +123,27 @@ blender --background --python vse_cut_list.py -- --check-pixels --engine cycles blender --background --python vse_cut_list.py -- --output vse.png ``` -It exits non-zero on failure and prints every measured value and tolerance on -success, so CI logs carry the numbers. The `blender-smoke` workflow runs the -check and the pixel witness on Blender 5.2 LTS and 4.5 LTS. - -The `--output` render path additionally measures framing against the Layer 1 band via `examples/gallery_framing.py` (exit 10 on violation) before writing the still. +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is also the shared framing helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | `.sequences` accessor contract for this Blender | +| 4 | Wrong-era `new_effect` end kwarg or TRANSFORM type did not TypeError | +| 5 | Strip span or channel off closed form | +| 6 | `frame_final_*` deprecation bridge for this Blender | +| 7 | GC wiring or strip `.type` enum (`--swap-inputs` lands here) | +| 8 | Scene strip span or Stage source | +| 9 | Mosaic transform, compositing defaults, text body, or strip color | +| 10 | Save/reload round-trip re-assert failed; also gallery framing violation | +| 11 | `--output` produced no file | +| 12 | `--check-pixels` compositing contract | + +The `blender-smoke` workflow runs the check and the pixel witness 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 `--swap-inputs`. diff --git a/examples/vse-cut-list/vse_cut_list.py b/examples/vse-cut-list/vse_cut_list.py index a1bf991..5eeb8a7 100644 --- a/examples/vse-cut-list/vse_cut_list.py +++ b/examples/vse-cut-list/vse_cut_list.py @@ -31,6 +31,10 @@ The timeline below therefore feeds the cross a dedicated source pair (T1/T2) that owns no mosaic cell, on channels directly under GC. +``--swap-inputs`` wires GC as T2 -> T1 and still asserts T1 -> T2. That is +the falsifier (``--same-axis`` in export-preset-axis). ``--check-pixels`` +remains a second witness, not a falsifier. + The check builds a deterministic cut list, asserts every span against its closed form on each version's canonical accessors, then proves the spans, wiring, colors, and transforms survive a save/reload round-trip. Pass @@ -40,6 +44,7 @@ tiny render (cell colors and input consumption), the way CI does: blender --background --python vse_cut_list.py -- + blender --background --python vse_cut_list.py -- --swap-inputs blender --background --python vse_cut_list.py -- --check-pixels blender --background --python vse_cut_list.py -- --output vse.png """ @@ -314,10 +319,15 @@ def check_roundtrip(): return check(bpy.context.scene) -def run_checks(): +def run_checks(swap_inputs=False): bpy.ops.wm.read_factory_settings(use_empty=True) sc = bpy.context.scene build_cut_list(sc) + if swap_inputs: + coll = strips_coll(sc.sequence_editor) + gc = coll.get("GC") + gc.input_1 = coll.get("T2") + gc.input_2 = coll.get("T1") code = check(sc) if code != 0: return code @@ -690,9 +700,13 @@ def main(): "--check-pixels", action="store_true", help="also assert the compositing contract on a tiny render", ) + p.add_argument( + "--swap-inputs", action="store_true", + help="falsifier: GC T2 -> T1, still assert T1 -> T2", + ) args = p.parse_args(argv) - code = run_checks() + code = run_checks(swap_inputs=args.swap_inputs) if code != 0: return code if args.check_pixels: diff --git a/examples/vse-gamma-cross/README.md b/examples/vse-gamma-cross/README.md index de217b9..e75f659 100644 --- a/examples/vse-gamma-cross/README.md +++ b/examples/vse-gamma-cross/README.md @@ -52,11 +52,31 @@ identical, so the still visibly breaks with the contract. # Correctness check (tiny per-frame sample renders) — the CI check: blender --background --python vse_gamma_cross.py -- +# Falsifier: GC T2 -> T1. Must exit non-zero (inputs). +blender --background --python vse_gamma_cross.py -- --swap-inputs + # Also render the calibration lightbox still (EEVEE on a GPU host; cycles on GPU-less): blender --background --python vse_gamma_cross.py -- --output bench.png blender --background --python vse_gamma_cross.py -- --output bench.png --engine cycles ``` -It exits non-zero on failure (span drift, wrong inputs, t-convention drift, -a sample off the closed form, or a missing gamma dip). The `blender-smoke` -workflow runs the check on Blender 5.2 LTS and 4.5 LTS. +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | GC span off closed form | +| 4 | GC inputs are not T1 → T2 (`--swap-inputs` lands here) | +| 5 | Sample `t` convention drifted | +| 6 | Cross sample off the gamma-0.5 closed form | +| 7 | Mid-cross lerp deviation missing (naive mix) | +| 8 | `--output` produced no file | + +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 `--swap-inputs`. diff --git a/examples/vse-gamma-cross/vse_gamma_cross.py b/examples/vse-gamma-cross/vse_gamma_cross.py index 3330b8f..1615e49 100644 --- a/examples/vse-gamma-cross/vse_gamma_cross.py +++ b/examples/vse-gamma-cross/vse_gamma_cross.py @@ -21,11 +21,15 @@ The blend math is identical on Blender 4.5 LTS and 5.1 (every sample matches to the quantization step). +``--swap-inputs`` wires GC as T2 -> T1 and still asserts T1 -> T2. That is +the falsifier (``--same-axis`` in export-preset-axis). + By default it runs only the correctness check (no gallery render) — the CI smoke check. Pass --output to also render a still: - blender --background --python vse_gamma_cross.py -- # check only - blender --background --python vse_gamma_cross.py -- --output g.png # + render + blender --background --python vse_gamma_cross.py -- # check only + blender --background --python vse_gamma_cross.py -- --swap-inputs # must fail + blender --background --python vse_gamma_cross.py -- --output g.png # + render """ import bpy, sys, os, math, argparse, tempfile, shutil @@ -69,7 +73,7 @@ def strip_span(s): return s.frame_final_start, s.frame_final_end, s.frame_final_duration -def build_cross(sc): +def build_cross(sc, swap_inputs=False): """The two-strip cross: T1/T2 consumed by the GAMMA_CROSS above them (effect strips consume inputs only from below — vse-cut-list's wiring).""" sc.frame_start = SPAN[0] @@ -80,7 +84,8 @@ def build_cross(sc): t1.color = A_RGB t2 = new_effect(coll, "T2", "COLOR", 2, SPAN) t2.color = B_RGB - gc = new_effect(coll, "GC", "GAMMA_CROSS", 3, SPAN, input1=t1, input2=t2) + src1, src2 = (t2, t1) if swap_inputs else (t1, t2) + gc = new_effect(coll, "GC", "GAMMA_CROSS", 3, SPAN, input1=src1, input2=src2) return gc @@ -411,11 +416,13 @@ def main(): p.add_argument("--output", default=None, help="optional: render a still PNG here") p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"), help="render engine for --output (cycles for GPU-less hosts)") + p.add_argument("--swap-inputs", action="store_true", + help="falsifier: GC T2 -> T1, still assert T1 -> T2") args = p.parse_args(argv) bpy.ops.wm.read_factory_settings(use_empty=True) sc = bpy.context.scene - gc = build_cross(sc) + gc = build_cross(sc, swap_inputs=args.swap_inputs) code = check(sc, gc) if code: return code From 8d1b1c5eb6b4b34424f5bc59338d77befb7729a9 Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Sun, 13 Sep 2026 10:30:20 -0400 Subject: [PATCH 2/2] docs: backfill README exit tables on the remaining thirteen examples Inspection-only documentation so every shipped example lists its file-local nonzero codes. No flags, no behavior, no code-value changes. Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor --- examples/coincident-vert-weld/README.md | 17 +++++++++ .../cross-version-property-delete/README.md | 22 ++++++++++++ examples/exit-pre-sidecar/README.md | 28 ++++++++++----- examples/export-preset-axis/README.md | 27 +++++++++++--- examples/gn-bundle-roundtrip/README.md | 20 +++++++++++ examples/gn-zone-iterate/README.md | 22 ++++++++++++ examples/lightmap-uv-channel/README.md | 28 ++++++++++++--- examples/modular-kit-snap/README.md | 27 +++++++++++--- examples/ngon-triangulate/README.md | 17 +++++++++ examples/socket-attach-points/README.md | 30 ++++++++++++++-- examples/unapplied-scale-gltf/README.md | 17 +++++++++ examples/usd-export-evaluation-mode/README.md | 21 +++++++++++ examples/vertex-color-ao/README.md | 35 +++++++++++++++---- 13 files changed, 281 insertions(+), 30 deletions(-) diff --git a/examples/coincident-vert-weld/README.md b/examples/coincident-vert-weld/README.md index 7a90cd7..2b9bfff 100644 --- a/examples/coincident-vert-weld/README.md +++ b/examples/coincident-vert-weld/README.md @@ -33,3 +33,20 @@ blender --background --python coincident_vert_weld.py -- blender --background --python coincident_vert_weld.py -- --no-duplicate blender --background --python coincident_vert_weld.py -- --weld ``` + +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage; also exporter RNA missing expected glTF kwargs | +| 3 | Pathology missing: coincident shells (`--no-duplicate` lands here) | +| 4 | glTF export handling failed (`--weld` lands here) | + +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 `--no-duplicate` or `--weld`. diff --git a/examples/cross-version-property-delete/README.md b/examples/cross-version-property-delete/README.md index 93d0ac4..3dda544 100644 --- a/examples/cross-version-property-delete/README.md +++ b/examples/cross-version-property-delete/README.md @@ -36,3 +36,25 @@ the Clear plate tagged and exit 7. The `--output` render path measures framing against the Layer 1 band via `examples/gallery_framing.py` (exit 10 on violation) before writing the still. + +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is the shared framing helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage; also an active object existed after the data-API build | +| 3 | Custom ID property did not land | +| 4 | `property_unset` on a custom ID key did not TypeError, or it deleted the key | +| 5 | `del` did not report removal | +| 6 | Keep plate lost the ID property | +| 7 | Clear plate still has the ID property (`--skip-delete` / `--unset-instead` land here) | +| 10 | Gallery framing violation | +| 12 | `--output` produced no file | + +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`, `--skip-delete`, or `--unset-instead`. diff --git a/examples/exit-pre-sidecar/README.md b/examples/exit-pre-sidecar/README.md index 2286044..ea82507 100644 --- a/examples/exit-pre-sidecar/README.md +++ b/examples/exit-pre-sidecar/README.md @@ -12,16 +12,10 @@ accessing the handler list. No gallery still. There is no geometry. -**What failure each check would catch:** - -- exit 77 — Blender < 5.1 and not `--force-run` -- exit 2 — `--force-run` on 4.5 (`exit_pre` missing) -- harness FAIL missing sidecar — `--silent-handler` / `--no-handler` -- harness FAIL wrong contents — `--wrong-text` (`nope`), `--write-in-main` - (`from-main`), `--atexit-instead` (`atexit-ok`) - The harness checks **contents** (`sidecar_contains=exit_pre-ok`), not -existence only. A file written from `main` or `atexit` is red. +existence only. A file written from `main` or `atexit` is red. Several +falsifiers exit 0 from the script so the **harness** can fail after Blender +dies. ## Run @@ -33,3 +27,19 @@ python tests/smoke/run_example.py --name exit-pre-sidecar \ --series 5.2 --min-version 5.1 \ --expect-sidecar /tmp/exit-pre.sidecar --sidecar-contains exit_pre-ok ``` + +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `77` is the smoke skip protocol, not a product check. + +| Code | Meaning | +| --- | --- | +| 0 | Success (including `--silent-handler` / `--no-handler` / `--wrong-text` / `--write-in-main` / `--atexit-instead`, which the harness then fails) | +| 1 | Uncaught exception (FATAL wrapper); also `$BDT_SMOKE_SIDECAR` unset | +| 2 | argparse / usage; also `--force-run` on Blender < 5.1 (`exit_pre` missing or unexpectedly present) | +| 77 | `SMOKE_SKIP:` `exit_pre` requires Blender 5.1+ | + +The `blender-smoke` workflow runs the check on Blender 5.2 LTS (5.1 on the +weekly cron, the `needs-5.1` PR label, or manual dispatch) and skips on 4.5 +LTS. Smoke does not pass the falsifier flags. diff --git a/examples/export-preset-axis/README.md b/examples/export-preset-axis/README.md index 5ead816..dc84ad7 100644 --- a/examples/export-preset-axis/README.md +++ b/examples/export-preset-axis/README.md @@ -43,7 +43,26 @@ blender --background --python export_preset_axis.py -- --output beacon.png blender --background --python export_preset_axis.py -- --output beacon.png --engine cycles ``` -It exits non-zero on failure (RNA drift, source not Z-dominant, Unity disk -not converted, Godot disk not Z-up, Unity not standing, Godot not lying, -orientations equal). The `blender-smoke` workflow runs the check on Blender -5.2 LTS and 4.5 LTS (5.1 on the weekly cron). +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is the shared framing helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage; also exporter RNA missing expected glTF kwargs | +| 3 | Source mast is not Z-dominant, or tip drifted | +| 4 | glTF reimport produced no mesh | +| 5 | Unity disk POSITION is not `(x, z, -y)`, or Unity node has rotation | +| 6 | Godot disk POSITION is not raw Z-up; also `--output` produced no file | +| 7 | Unity reimport is not standing | +| 8 | Godot reimport is not lying along Y, or reimported tip mismatch | +| 9 | Reimported orientations did not differ (`--same-axis` lands here) | +| 10 | Gallery framing violation | +| 11 | `--same-axis` did not collapse the axis difference | + +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 `--same-axis`. diff --git a/examples/gn-bundle-roundtrip/README.md b/examples/gn-bundle-roundtrip/README.md index 6964e78..b01f3d0 100644 --- a/examples/gn-bundle-roundtrip/README.md +++ b/examples/gn-bundle-roundtrip/README.md @@ -43,3 +43,23 @@ not read at thumbnail. blender --background --python gn_bundle_roundtrip.py -- blender --background --python gn_bundle_roundtrip.py -- --force-run ``` + +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `77` is the smoke skip protocol, not a product check. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage; carrier mesh rewritten; `--legacy-rna` on 5.x | +| 3 | Evaluated vert/face count off closed form (`--mismatch`) | +| 4 | Unpacked Scale/Offset x-extent off (`--bypass`) | +| 5 | `bundle_mark` missing or off closed form | +| 77 | `SMOKE_SKIP:` Bundles require Blender 5.0+ | + +The `blender-smoke` workflow runs the check on Blender 5.2 LTS (5.1 on the +weekly cron, the `needs-5.1` PR label, or manual dispatch) and skips on 4.5 +LTS. Smoke does not pass `--bypass`, `--legacy-rna`, `--mismatch`, or +`--force-run`. diff --git a/examples/gn-zone-iterate/README.md b/examples/gn-zone-iterate/README.md index 14e329d..81744ff 100644 --- a/examples/gn-zone-iterate/README.md +++ b/examples/gn-zone-iterate/README.md @@ -40,3 +40,25 @@ blender --background --python gn_zone_iterate.py -- --output zones.png The `--output` render path measures framing via `examples/gallery_framing.py` (exit 10 on violation). + +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is the shared framing helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage; also carrier mesh was rewritten | +| 3 | Repeat evaluated vert/face count off closed form | +| 4 | Repeat X-centers off closed form | +| 5 | For Each evaluated vert/face count off closed form | +| 6 | For Each Z-centers off closed form | +| 10 | Gallery framing violation | +| 12 | `--output` produced no file | + +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`, `--no-offset`, `--unpair-foreach`, or +`--unpair-repeat`. diff --git a/examples/lightmap-uv-channel/README.md b/examples/lightmap-uv-channel/README.md index c543e3e..c68317e 100644 --- a/examples/lightmap-uv-channel/README.md +++ b/examples/lightmap-uv-channel/README.md @@ -87,7 +87,27 @@ blender --background --python lightmap_uv_channel.py -- --output atlas.png blender --background --python lightmap_uv_channel.py -- --falsify overlap.png ``` -Exits non-zero on failure. The `blender-smoke` workflow runs the check on -Blender 5.2 LTS and 4.5 LTS. The `--output` render path additionally measures -framing against the Layer 1 band via `examples/gallery_framing.py` (exit 10 -on violation) before writing the still. +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is the shared framing helper. `11` is the shared asset-quality +helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | UV layer names ≠ UV0 / UV1 | +| 4 | UVMap / UV1 render/clone flags wrong after re-assert | +| 5 | Channel 0 (UV0) touched by the UV1 unwrap | +| 6 | UV1 loops outside `[0,1]` | +| 7 | SAT overlap self-test failed, or UV1 triangle pairs overlap | +| 8 | Min island distance below margin | +| 9 | Part is not watertight | +| 10 | Gallery framing violation; also `--output` produced no file | +| 11 | Gallery asset-quality violation | + +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 `--falsify`. diff --git a/examples/modular-kit-snap/README.md b/examples/modular-kit-snap/README.md index 7e1cc77..985a3a5 100644 --- a/examples/modular-kit-snap/README.md +++ b/examples/modular-kit-snap/README.md @@ -83,7 +83,26 @@ blender --background --python modular_kit_snap.py -- --output corridor.png blender --background --python modular_kit_snap.py -- --falsify seams.png ``` -Exits non-zero on failure. The `blender-smoke` workflow runs the check on -Blender 5.2 LTS and 4.5 LTS. The `--output` render path additionally measures -framing against the Layer 1 band via `examples/gallery_framing.py` (reported -under the documented deviation above) before writing the still. +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is the shared framing helper. `11` is the shared asset-quality +helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | Boundary vert count or end-plane membership | +| 4 | End rings do not partition evenly, or opposing loops differ | +| 5 | Tiled instances do not share boundary positions | +| 6 | Shell or whole-asset bbox off the declared tile | +| 7 | Boundary edge count, non-manifold edges, or rim off the end planes | +| 8 | Detail part not watertight | +| 9 | Detail part reaches a tile boundary; also `--output` produced no file | +| 11 | Kit part count, unapplied scale, namespace, or origin; also gallery asset-quality violation | + +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 `--falsify`. diff --git a/examples/ngon-triangulate/README.md b/examples/ngon-triangulate/README.md index 50acdb2..320e8bf 100644 --- a/examples/ngon-triangulate/README.md +++ b/examples/ngon-triangulate/README.md @@ -29,3 +29,20 @@ blender --background --python ngon_triangulate.py -- blender --background --python ngon_triangulate.py -- --no-dissolve blender --background --python ngon_triangulate.py -- --skip-triangulate ``` + +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | Pathology missing: n-gon count, loops, or face count (`--no-dissolve` lands here) | +| 4 | `calc_tangents` / triangulate handling (`--skip-triangulate` lands here) | + +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 `--no-dissolve` or `--skip-triangulate`. diff --git a/examples/socket-attach-points/README.md b/examples/socket-attach-points/README.md index 2650862..64b2982 100644 --- a/examples/socket-attach-points/README.md +++ b/examples/socket-attach-points/README.md @@ -123,11 +123,35 @@ blender --background --python socket_attach_points.py -- --falsify adrift.png blender --background --python socket_attach_points.py -- --probe ``` -Exits non-zero on failure. The `blender-smoke` workflow runs the check on -Blender 5.2 LTS and 4.5 LTS. The `--output` render path additionally gates framing -via `examples/gallery_framing.py` (fill **0.881x**, margins +The `--output` render path additionally gates framing via +`examples/gallery_framing.py` (fill **0.881x**, margins **0.066/0.053/0.122/0.106**, no edge touched) and the asset floors via `examples/gallery_asset_quality.py` (32 materials, `edge90` **0.027**, no default names). The `--falsify` render is a diagnostic, not a gallery hero, so it takes a documented framing deviation — its whole point is that the modules leave the frame. + +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is the shared framing helper. `11` is the shared asset-quality +helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | Socket world matrix, orthonormal basis, or determinant | +| 4 | Socket pad normal, origin, or up-axis rule | +| 5 | Module seating / mount axis / leftover local transform | +| 6 | Population, namespace, default datablock name, SKT_ prefix, or skid plane | +| 7 | Re-pose: socket or module lost the root | +| 8 | Parent-inverse clear, child scale pushdown, or post-apply module drift | +| 9 | `--output` produced no file | +| 10 | Gallery framing violation | +| 11 | Gallery asset-quality violation | + +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`, `--falsify`, or `--probe`. diff --git a/examples/unapplied-scale-gltf/README.md b/examples/unapplied-scale-gltf/README.md index 2d5c988..879b724 100644 --- a/examples/unapplied-scale-gltf/README.md +++ b/examples/unapplied-scale-gltf/README.md @@ -31,3 +31,20 @@ blender --background --python unapplied_scale_gltf.py -- blender --background --python unapplied_scale_gltf.py -- --identity blender --background --python unapplied_scale_gltf.py -- --bake ``` + +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage; also exporter RNA missing expected glTF kwargs | +| 3 | Pathology missing: unapplied non-uniform scale (`--identity` lands here) | +| 4 | Export handling failed (`--bake` lands here) | + +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 `--identity` or `--bake`. diff --git a/examples/usd-export-evaluation-mode/README.md b/examples/usd-export-evaluation-mode/README.md index bb2ef91..8cab633 100644 --- a/examples/usd-export-evaluation-mode/README.md +++ b/examples/usd-export-evaluation-mode/README.md @@ -41,3 +41,24 @@ true in `--background`, `evaluation_mode` is `{RENDER, VIEWPORT}`. The blender --background --python usd_export_evaluation_mode.py -- blender --background --python usd_export_evaluation_mode.py -- --output u.png ``` + +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is the shared framing helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage; also base cube verts ≠ 8 | +| 3 | VIEWPORT TESSELLATE point/face count off closed form | +| 4 | RENDER TESSELLATE point/face count or scheme off closed form | +| 5 | BEST_MATCH cage point count or scheme off | +| 6 | RENDER and VIEWPORT USDA point counts are identical | +| 10 | Gallery framing violation | +| 12 | `--output` produced no file | + +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`, `--evaluation-mode`, or `--subdivision`. diff --git a/examples/vertex-color-ao/README.md b/examples/vertex-color-ao/README.md index 051c3c6..5e42d21 100644 --- a/examples/vertex-color-ao/README.md +++ b/examples/vertex-color-ao/README.md @@ -115,10 +115,33 @@ blender --background --python vertex_color_ao.py -- --output well.png blender --background --python vertex_color_ao.py -- --falsify inverted.png ``` -Exits non-zero on failure. The `blender-smoke` workflow runs the check on -Blender 5.2 LTS and 4.5 LTS (the calibration rig gets 4096 samples over six -points, the asset a cheap 64, so the whole check is ~1 s). The `--output` -render path additionally gates framing via `examples/gallery_framing.py` -(fill **0.839y**, margins **0.241/0.238/0.072/0.089**, no edge touched) and the -asset floors via `examples/gallery_asset_quality.py` (11 materials, `edge90` +The `--output` render path additionally gates framing via +`examples/gallery_framing.py` (fill **0.839y**, margins +**0.241/0.238/0.072/0.089**, no edge touched) and the asset floors via +`examples/gallery_asset_quality.py` (11 materials, `edge90` **0.152**, no default names). + +## Exit codes + +Per-script sequential checks. `9` is a valid check code; there is no rule +against it. `10` is the shared framing helper. `11` is the shared asset-quality +helper. + +| Code | Meaning | +| --- | --- | +| 0 | Success | +| 1 | Uncaught exception (FATAL wrapper) | +| 2 | argparse / usage | +| 3 | Calibration AO off closed form | +| 4 | Unoccluded plate is not 1.0, or AO is not strictly increasing with distance | +| 5 | Bake empty, out of range, or spread too small | +| 6 | FLOAT_COLOR / BYTE_COLOR round-trip contract | +| 7 | Colour attributes missing, retyped, or drifted under the depsgraph | +| 8 | Asset part count, unapplied scale, namespace, render attribute, or ground plane | +| 9 | `--output` produced no file | +| 10 | Gallery framing violation | +| 11 | Gallery asset-quality violation | + +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 `--falsify`.