Skip to content

Commit a4bef90

Browse files
committed
ci: make smoke runner headless-safe and add explicit EEVEE-id assertion
Runner fix: GPU-less GitHub runners abort EEVEE GPU rendering with a C-level core dump (libEGL.so.1 missing, exit 134) that Python try/except cannot catch. Install Blender's GL runtime libs + xvfb and run Blender under xvfb-run; render the non-black frame with Cycles (CPU) instead of EEVEE. Gate fix: keep a dedicated, frame-independent EEVEE engine-id assertion (eevee-engine-id-assigns) that compares the helper's id against the version-expected id (BLENDER_EEVEE on 5.x, BLENDER_EEVEE_NEXT on 4.2-4.5) so the polarity bug (CRITICAL #1) stays guarded even though rendering is Cycles. Verified: green on 5.1.1 and 4.5.10; re-inverting get_eevee_engine_id() makes eevee-engine-id-assigns FAIL (exit 1) on both builds (enum not found). Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
1 parent ee7be71 commit a4bef90

2 files changed

Lines changed: 33 additions & 23 deletions

File tree

.github/workflows/blender-smoke.yml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ jobs:
3434
steps:
3535
- uses: actions/checkout@v6
3636

37+
- name: Install Blender runtime libraries
38+
run: |
39+
set -euo pipefail
40+
sudo apt-get update
41+
# Blender needs these shared libs even in --background (GPU/GL module init);
42+
# xvfb provides a virtual display so GL/EGL init does not abort the process.
43+
sudo apt-get install -y --no-install-recommends \
44+
xvfb libgl1 libegl1 libxrender1 libxxf86vm1 libxfixes3 libxi6 \
45+
libxkbcommon0 libsm6 libice6
46+
3747
- name: Resolve and download Blender ${{ matrix.series }}
3848
run: |
3949
set -euo pipefail
@@ -72,17 +82,17 @@ jobs:
7282
run: |
7383
set -euo pipefail
7484
mkdir -p "$RUNNER_TEMP/out"
75-
"$BLENDER" --background --python tests/smoke/run_smoke.py -- "$RUNNER_TEMP/out"
85+
xvfb-run -a "$BLENDER" --background --python tests/smoke/run_smoke.py -- "$RUNNER_TEMP/out"
7686
7787
- name: Build template input scene
7888
run: |
7989
set -euo pipefail
80-
"$BLENDER" --background --python tests/smoke/make_input.py -- "$RUNNER_TEMP/out/input.blend"
90+
xvfb-run -a "$BLENDER" --background --python tests/smoke/make_input.py -- "$RUNNER_TEMP/out/input.blend"
8191
8292
- name: Headless glTF template runs (exit 0, .glb produced)
8393
run: |
8494
set -euo pipefail
85-
"$BLENDER" --background "$RUNNER_TEMP/out/input.blend" \
95+
xvfb-run -a "$BLENDER" --background "$RUNNER_TEMP/out/input.blend" \
8696
--python tests/smoke/tmpl_gltf.py -- \
8797
--output "$RUNNER_TEMP/out/out.glb" --apply-modifier SUBSURF
8898
test -s "$RUNNER_TEMP/out/out.glb" || { echo "::error::glTF output missing/empty"; exit 1; }
@@ -91,7 +101,7 @@ jobs:
91101
- name: Headless template no-mesh path returns exit 2
92102
run: |
93103
set +e
94-
"$BLENDER" --background "$RUNNER_TEMP/out/empty.blend" \
104+
xvfb-run -a "$BLENDER" --background "$RUNNER_TEMP/out/empty.blend" \
95105
--python tests/smoke/tmpl_gltf.py -- --output "$RUNNER_TEMP/out/none.glb"
96106
code=$?
97107
set -e
@@ -103,7 +113,7 @@ jobs:
103113
set -euo pipefail
104114
# Cycles (CPU) so this is reliable on GPU-less runners; the EEVEE-id regression
105115
# itself is gated in run_smoke.py via engine assignment.
106-
"$BLENDER" --background "$RUNNER_TEMP/out/input.blend" \
116+
xvfb-run -a "$BLENDER" --background "$RUNNER_TEMP/out/input.blend" \
107117
--python tests/smoke/tmpl_render.py -- \
108118
--output "$RUNNER_TEMP/out/render.png" --engine CYCLES
109119
test -s "$RUNNER_TEMP/out/render.png" || { echo "::error::render PNG missing/empty"; exit 1; }

tests/smoke/run_smoke.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -108,31 +108,31 @@ def smoke_eevee():
108108
cam = bpy.data.objects.new("cam", bpy.data.cameras.new("cam")); bpy.context.collection.objects.link(cam)
109109
cam.location = (0,0,10); bpy.context.scene.camera = cam
110110
sc = bpy.context.scene
111-
# the EEVEE-id regression guard: assigning the version-correct id must succeed
111+
# THE EEVEE-id polarity guard (CRITICAL #1). Independent of any rendered frame, so it
112+
# cannot flake on the GL/EGL context. `eid` is the repo helper's output (under test);
113+
# `expected` is computed here from the version, so an inverted helper is caught.
112114
eid = get_eevee_engine_id()
115+
expected = 'BLENDER_EEVEE' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE_NEXT'
116+
assigned = True; err = ""
113117
try:
114118
sc.render.engine = eid
115119
except Exception as e:
116-
require("eevee-id-assign", False, f"assigning '{eid}' raised {type(e).__name__}: {e}")
117-
require("eevee-id-assign", sc.render.engine == eid, f"engine id '{eid}' valid on this build")
120+
assigned = False; err = f"{type(e).__name__}: {e}"
121+
require("eevee-engine-id-assigns",
122+
assigned and sc.render.engine == expected,
123+
f"helper returned '{eid}'; engine='{sc.render.engine if assigned else 'UNASSIGNED('+err+')'}' expected='{expected}'")
124+
# Render the non-black frame with Cycles (CPU): reliable on GPU-less headless runners,
125+
# where an EEVEE GPU render aborts the process (no EGL). The EEVEE-id regression itself
126+
# is already gated by the assignment above; EEVEE *rendering* is not exercised here.
127+
sc.render.engine = 'CYCLES'
128+
try: sc.cycles.samples = 4
129+
except Exception: pass
118130
sc.render.resolution_x = 16; sc.render.resolution_y = 16; sc.render.image_settings.file_format = 'PNG'
119131
png = os.path.join(OUT, f"smoke_render_{V[0]}{V[1]}.png"); sc.render.filepath = png
120-
# try EEVEE render; if the headless runner has no GPU and it raises, fall back to Cycles (CPU)
121-
used = sc.render.engine
122-
try:
123-
try: sc.eevee.taa_render_samples = 4
124-
except Exception: pass
125-
bpy.ops.render.render(write_still=True)
126-
if not os.path.exists(png): raise RuntimeError("no PNG written")
127-
except Exception as e:
128-
print(f" (EEVEE render unavailable headless: {type(e).__name__}; falling back to CYCLES)")
129-
sc.render.engine = 'CYCLES'; used = 'CYCLES'
130-
try: sc.cycles.samples = 4
131-
except Exception: pass
132-
bpy.ops.render.render(write_still=True)
133-
require("eevee-render-file", os.path.exists(png) and os.path.getsize(png) > 0, f"render PNG written by {used}")
132+
bpy.ops.render.render(write_still=True)
133+
require("render-file", os.path.exists(png) and os.path.getsize(png) > 0, "1-frame render PNG written (Cycles CPU)")
134134
img = bpy.data.images.load(png); px = list(img.pixels); mx = max(px) if px else 0.0
135-
require("eevee-render-nonblack", mx > 0.05, f"max pixel {round(mx,3)} > 0.05 (engine used: {used})")
135+
require("render-nonblack", mx > 0.05, f"max pixel {round(mx,3)} > 0.05 (emissive material renders bright)")
136136

137137
# ---------- slotted actions: correct branch + legacy behaviour ----------
138138
def smoke_slotted():

0 commit comments

Comments
 (0)