"""GPU activation and engine configuration helpers for Blender headless renders. activate_gpu() must be called BEFORE open_mainfile / Cycles engine initialisation so that the CUDA/OptiX kernel is compiled with the correct compute_device_type. """ from __future__ import annotations import os import sys def activate_gpu(cycles_device: str = "auto") -> str | None: """Probe for GPU compute devices and activate them. Args: cycles_device: "auto" | "gpu" | "cpu" Returns: Device type string (e.g. "OPTIX", "CUDA") if GPU was activated, or None if CPU-only. """ if cycles_device == "cpu": return None import bpy # type: ignore[import] try: cprefs = bpy.context.preferences.addons['cycles'].preferences for dt in ('OPTIX', 'CUDA', 'HIP', 'ONEAPI'): try: cprefs.compute_device_type = dt cprefs.get_devices() gpu = [d for d in cprefs.devices if d.type != 'CPU'] if gpu: for d in cprefs.devices: d.use = (d.type != 'CPU') print(f"[blender_render] early GPU activation: {dt}, " f"devices={[(d.name, d.type) for d in gpu]}", flush=True) return dt except Exception as e: print(f"[blender_render] {dt} not available: {e}", flush=True) except Exception as e: print(f"[blender_render] early GPU probe failed: {e}", flush=True) return None def configure_engine( scene, engine: str, samples: int, cycles_device: str, early_gpu_type: str | None, noise_threshold_arg: str = "", denoiser_arg: str = "", denoising_input_passes_arg: str = "", denoising_prefilter_arg: str = "", denoising_quality_arg: str = "", denoising_use_gpu_arg: str = "", ) -> str: """Configure the Blender render engine (EEVEE or Cycles) on *scene*. Returns the effective engine name ("eevee" or "cycles"). Exits with code 2 if GPU required but unavailable (CYCLES_DEVICE=gpu env var). """ if engine == "eevee": set_ok = False for eevee_id in ('BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'): try: scene.render.engine = eevee_id set_ok = True print(f"[blender_render] EEVEE engine id: {eevee_id}") break except TypeError: continue if not set_ok: print("[blender_render] WARNING: could not set EEVEE engine – falling back to Cycles") engine = "cycles" if engine == "eevee": for attr in ('taa_render_samples', 'samples'): try: import bpy # type: ignore[import] setattr(scene.eevee, attr, samples) print(f"[blender_render] EEVEE samples: scene.eevee.{attr}={samples}") break except AttributeError: continue if engine != "eevee": gpu_type_found = activate_gpu(cycles_device) or early_gpu_type scene.render.engine = 'CYCLES' if gpu_type_found: scene.cycles.device = 'GPU' activate_gpu(cycles_device) print(f"[blender_render] Cycles GPU ({gpu_type_found}), samples={samples}", flush=True) print(f"RENDER_DEVICE_USED: engine=CYCLES device=GPU compute_type={gpu_type_found}", flush=True) else: scene.cycles.device = 'CPU' print(f"[blender_render] WARNING: GPU not found — falling back to CPU, samples={samples}", flush=True) print("RENDER_DEVICE_USED: engine=CYCLES device=CPU compute_type=NONE (fallback)", flush=True) if os.environ.get("CYCLES_DEVICE", "auto").lower() == "gpu": print("GPU_REQUIRED_BUT_CPU_USED: strict mode active (CYCLES_DEVICE=gpu)", flush=True) sys.exit(2) scene.cycles.samples = samples scene.cycles.use_denoising = True scene.cycles.denoiser = denoiser_arg if denoiser_arg else 'OPENIMAGEDENOISE' if denoising_input_passes_arg: try: scene.cycles.denoising_input_passes = denoising_input_passes_arg except Exception: pass if denoising_prefilter_arg: try: scene.cycles.denoising_prefilter = denoising_prefilter_arg except Exception: pass if denoising_quality_arg: try: scene.cycles.denoising_quality = denoising_quality_arg except Exception: pass if denoising_use_gpu_arg: try: scene.cycles.denoising_use_gpu = (denoising_use_gpu_arg == "1") except AttributeError: pass if noise_threshold_arg: scene.cycles.use_adaptive_sampling = True scene.cycles.adaptive_threshold = float(noise_threshold_arg) return engine