Preview Mesh Batch Render: revert GPU process-pool (kept serial + shading)

The per-process-EGL GPU pool fell back to vtkXOpenGLRenderWindow with no
DISPLAY ("bad X server connection") and ran ~12s/mesh -- worse than
serial. Gate the process pool back to the software path only; GPU renders
per-mesh on the serial path (fresh plotter -> shading). Software CPU pool
and the shading fix are kept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
PozzettiAndrea
2026-06-16 16:39:13 +02:00
co-authored by Claude Opus 4.8
parent c2d4ddd173
commit 1dc05beb5d
+19 -24
View File
@@ -181,17 +181,12 @@ def _render_mesh_array(mesh, title, o):
def _render_worker(payload):
"""ProcessPool worker. payload = (spec, title, opts) where spec is
("path", filepath) or ("mesh", trimesh). Each child makes its OWN GL context
(fork-safe: the parent holds no context). GPU children unset DISPLAY so VTK
picks EGL per-process; software children use the parent-started Xvfb.
"""ProcessPool worker (SOFTWARE path only). payload = (spec, title, opts) where
spec is ("path", filepath) or ("mesh", trimesh). The parent started Xvfb with no
GL context (fork-safe); each child makes its own software context.
Returns the array, or ("ERR", message, res) on failure."""
import os
try:
spec, title, o = payload
if o.get("gpu"):
# Force a per-process EGL (GPU) context; don't attach to any X display.
os.environ.pop("DISPLAY", None)
import pyvista as pv
pv.OFF_SCREEN = True
if spec[0] == "path":
@@ -284,7 +279,6 @@ class RenderMeshBatch(io.ComfyNode):
"revert_yz": bool(_s(revert_yz_flip, False)),
"three": three,
"titled": bool(_s(show_title, True)),
"gpu": gpu, # workers set up EGL (gpu) vs Xvfb/OSMesa (software)
}
try:
@@ -301,18 +295,18 @@ class RenderMeshBatch(io.ComfyNode):
imgs = [None] * len(meshes)
def _run_pool():
"""Render across CPU cores via a process pool (max workers). Each worker
renders in its OWN GL context and returns the small image (not the mesh),
so this parallelises the CPU-heavy prep for BOTH gpu (per-process EGL) and
software (shared Xvfb). Returns True on success, False -> serial fallback."""
# Software children share a parent-started Xvfb (started WITH NO GL context
# so the fork stays safe). GPU children unset DISPLAY -> their own EGL.
if not gpu:
try:
if os.name != "nt" and not os.environ.get("DISPLAY"):
pv.start_xvfb()
except Exception as e:
log.debug("[PreviewMeshBatch] start_xvfb: %s", e)
"""SOFTWARE-only: render across CPU cores via a process pool (max workers).
Each worker renders in its own software GL context and returns the small
image (not the mesh). Returns True on success, False -> serial fallback.
(GPU does NOT use this -- multiple EGL contexts gave 'bad X server' fallbacks
and were slower than serial; GPU goes through the per-mesh serial path.)"""
# Children share a parent-started Xvfb (started WITH NO GL context so the
# fork stays safe).
try:
if os.name != "nt" and not os.environ.get("DISPLAY"):
pv.start_xvfb()
except Exception as e:
log.debug("[PreviewMeshBatch] start_xvfb: %s", e)
payloads = []
for i, mesh in enumerate(meshes):
@@ -326,8 +320,8 @@ class RenderMeshBatch(io.ComfyNode):
from concurrent.futures import ProcessPoolExecutor
workers = min(len(meshes), (os.cpu_count() or 4)) # max available
log.info("[PreviewMeshBatch] %s render: %d mesh(es) @ %dpx across %d processes",
"gpu" if gpu else "software", len(meshes), res, workers)
log.info("[PreviewMeshBatch] software render: %d mesh(es) @ %dpx across %d processes",
len(meshes), res, workers)
try:
with ProcessPoolExecutor(max_workers=workers) as ex:
gen = ex.map(_render_worker, payloads)
@@ -346,7 +340,8 @@ class RenderMeshBatch(io.ComfyNode):
log.warning("[PreviewMeshBatch] parallel render failed (%s); serial fallback", e)
return False
done = _run_pool() if len(meshes) > 1 else False
# Process pool ONLY for the software path; GPU uses the serial per-mesh path.
done = _run_pool() if ((not gpu) and len(meshes) > 1) else False
if not done:
# ---- serial path (single mesh, or pool fallback) ----