feat: GPU rendering + material matching + perf improvements
- GPU: fix Cycles device activation order — set compute_device_type BEFORE engine init, re-set AFTER open_mainfile wipes preferences - GPU: remove _mark_sharp_and_seams edit-mode loop (redundant with Blender 5.0 shade_smooth_by_angle), saves ~200s/render on 175 parts - Material: fix _AFN suffix mismatch — build AF-stripped mat_map keys and add prefix fallback in _apply_material_library (blender_render.py) - Material: production GLB now uses get_material_library_path() which checks active AssetLibrary instead of empty legacy system setting - Admin: RenderTemplateTable multi-select output types (M2M frontend) - Admin: MaterialLibraryPanel replaced with link to Asset Libraries - UX: move Toaster to top-left to avoid dispatch button overlap - SQLAlchemy: add .unique() to all RenderTemplate M2M collection queries - Logging: flush=True on all Blender progress prints, stdout reconfigure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,7 @@ def _glb_from_step(step_path: Path, glb_path: Path, quality: str = "low") -> Non
|
||||
import sys as _sys
|
||||
|
||||
linear_deflection = 0.3 if quality == "low" else 0.05
|
||||
angular_deflection = 0.3 if quality == "low" else 0.1
|
||||
angular_deflection = 0.5 if quality == "low" else 0.2
|
||||
|
||||
scripts_dir = Path(os.environ.get("RENDER_SCRIPTS_DIR", "/render-scripts"))
|
||||
script_path = scripts_dir / "export_step_to_gltf.py"
|
||||
@@ -95,6 +95,7 @@ def render_still(
|
||||
denoising_quality: str = "",
|
||||
denoising_use_gpu: str = "",
|
||||
mesh_attributes: dict | None = None,
|
||||
log_callback: "Callable[[str], None] | None" = None,
|
||||
) -> dict:
|
||||
"""Convert STEP → GLB (OCC) → PNG (Blender subprocess).
|
||||
|
||||
@@ -170,49 +171,75 @@ def render_still(
|
||||
cmd += ["--mesh-attributes", json.dumps(mesh_attributes)]
|
||||
return cmd
|
||||
|
||||
def _run(eng: str) -> subprocess.CompletedProcess:
|
||||
def _run(eng: str) -> tuple[int, list[str], list[str]]:
|
||||
"""Run Blender subprocess, streaming stdout line-by-line.
|
||||
|
||||
Returns (returncode, stdout_lines, stderr_lines).
|
||||
"""
|
||||
import selectors
|
||||
proc = subprocess.Popen(
|
||||
_build_cmd(eng),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, env=env, start_new_session=True,
|
||||
)
|
||||
stdout_lines: list[str] = []
|
||||
stderr_lines: list[str] = []
|
||||
deadline = time.monotonic() + 600
|
||||
|
||||
sel = selectors.DefaultSelector()
|
||||
sel.register(proc.stdout, selectors.EVENT_READ, "stdout")
|
||||
sel.register(proc.stderr, selectors.EVENT_READ, "stderr")
|
||||
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=600)
|
||||
except subprocess.TimeoutExpired:
|
||||
try:
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
||||
except (ProcessLookupError, OSError):
|
||||
pass
|
||||
stdout, stderr = proc.communicate()
|
||||
return subprocess.CompletedProcess(_build_cmd(eng), proc.returncode, stdout, stderr)
|
||||
while sel.get_map():
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
try:
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
||||
except (ProcessLookupError, OSError):
|
||||
pass
|
||||
break
|
||||
events = sel.select(timeout=min(remaining, 2.0))
|
||||
for key, _ in events:
|
||||
line = key.fileobj.readline()
|
||||
if not line:
|
||||
sel.unregister(key.fileobj)
|
||||
continue
|
||||
line = line.rstrip("\n")
|
||||
if key.data == "stdout":
|
||||
stdout_lines.append(line)
|
||||
logger.info("[blender] %s", line)
|
||||
if log_callback and "[blender_render]" in line:
|
||||
log_callback(line)
|
||||
else:
|
||||
stderr_lines.append(line)
|
||||
logger.warning("[blender stderr] %s", line)
|
||||
finally:
|
||||
sel.close()
|
||||
|
||||
proc.wait(timeout=10)
|
||||
return proc.returncode, stdout_lines, stderr_lines
|
||||
|
||||
t_render = time.monotonic()
|
||||
result = _run(engine)
|
||||
returncode, stdout_lines, stderr_lines = _run(engine)
|
||||
engine_used = engine
|
||||
|
||||
log_lines = []
|
||||
for line in (result.stdout or "").splitlines():
|
||||
logger.info("[blender] %s", line)
|
||||
if "[blender_render]" in line:
|
||||
log_lines.append(line)
|
||||
for line in (result.stderr or "").splitlines():
|
||||
logger.warning("[blender stderr] %s", line)
|
||||
log_lines = [l for l in stdout_lines if "[blender_render]" in l]
|
||||
|
||||
# EEVEE fallback to Cycles on non-signal error
|
||||
if result.returncode > 0 and engine == "eevee":
|
||||
logger.warning("EEVEE failed (exit %d) — retrying with Cycles", result.returncode)
|
||||
result = _run("cycles")
|
||||
if returncode > 0 and engine == "eevee":
|
||||
logger.warning("EEVEE failed (exit %d) — retrying with Cycles", returncode)
|
||||
returncode, stdout_lines2, stderr_lines2 = _run("cycles")
|
||||
engine_used = "cycles (eevee fallback)"
|
||||
for line in (result.stdout or "").splitlines():
|
||||
logger.info("[blender-fallback] %s", line)
|
||||
if "[blender_render]" in line:
|
||||
log_lines.append(line)
|
||||
log_lines.extend(l for l in stdout_lines2 if "[blender_render]" in l)
|
||||
|
||||
if result.returncode != 0:
|
||||
if returncode != 0:
|
||||
stdout_tail = "\n".join(stdout_lines[-50:]) if stdout_lines else ""
|
||||
stderr_tail = "\n".join(stderr_lines[-20:]) if stderr_lines else ""
|
||||
raise RuntimeError(
|
||||
f"Blender exited with code {result.returncode}.\n"
|
||||
f"stdout: {(result.stdout or '')[-2000:]}\n"
|
||||
f"stderr: {(result.stderr or '')[-500:]}"
|
||||
f"Blender exited with code {returncode}.\n"
|
||||
f"stdout: {stdout_tail[-2000:]}\n"
|
||||
f"stderr: {stderr_tail[-500:]}"
|
||||
)
|
||||
|
||||
render_duration_s = round(time.monotonic() - t_render, 2)
|
||||
|
||||
Reference in New Issue
Block a user