blender-agent / gpu_render.py
suvadityamuk's picture
suvadityamuk HF Staff
Upload gpu_render.py with huggingface_hub
b8ccc90 verified
Raw
History Blame Contribute Delete
5.68 kB
"""
Rendering entry points.
- ``render_blend_gpu``: Cycles CUDA/OptiX render inside a ZeroGPU worker. The Blender
subprocess must be spawned *inside* the ``@spaces.GPU`` function so it inherits the
worker's CUDA devices.
- ``render_blend_local``: CPU Cycles (reliable) or EEVEE via Mesa software GL
(experimental, slow) in the main container.
"""
from __future__ import annotations
import os
import subprocess
import spaces
from blender_session import BLENDER_BIN, USER_RESOURCES, wrap_with_memory_cap
RENDER_SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "render_script.py")
_MAX_CPU_SAMPLES = 256
DEFAULT_CAMERA = {"mode": "scene", "azimuth": 315.0, "elevation": 30.0, "zoom": 1.0, "focal": 50.0}
def _run_blender_render(
blend_path: str,
out_path: str,
engine: str,
samples: int,
res_x: int,
res_y: int,
camera: dict | None = None,
extra_env: dict[str, str] | None = None,
timeout: float = 600.0,
anim: dict | None = None,
) -> str:
env = {**os.environ, "BLENDER_USER_RESOURCES": USER_RESOURCES}
if extra_env:
env.update(extra_env)
cam = {**DEFAULT_CAMERA, **(camera or {})}
cmd = [
BLENDER_BIN, "-b", blend_path, "--python", RENDER_SCRIPT, "--",
out_path, engine, str(samples), str(res_x), str(res_y),
str(cam["mode"]), str(cam["azimuth"]), str(cam["elevation"]),
str(cam["zoom"]), str(cam["focal"]),
]
if anim is not None:
cmd += [
"anim", str(anim.get("fps", 0)),
str(anim.get("frame_start", 0)), str(anim.get("frame_end", 0)),
]
proc = subprocess.run(
wrap_with_memory_cap(cmd),
env=env, capture_output=True, text=True, timeout=timeout, check=False,
)
# Blender writes "file.png" possibly with frame suffix; render_script sets exact path.
if proc.returncode != 0 or not os.path.exists(out_path):
tail = "\n".join((proc.stdout + "\n" + proc.stderr).splitlines()[-30:])
raise RuntimeError(f"Render failed (exit {proc.returncode}):\n{tail}")
for line in proc.stdout.splitlines():
if line.startswith("RENDER_DONE"):
print(line)
return out_path
def estimate_render_duration(
blend_path: str, out_path: str, samples: int, res_x: int, res_y: int,
camera: dict | None = None,
) -> int:
"""ZeroGPU duration estimate: Blender startup + scene load + render time."""
base = 30.0
pixel_factor = (res_x * res_y) / (1920 * 1080)
render = 60.0 * (samples / 128.0) * max(pixel_factor, 0.1)
return int(min(max(base + render, 45.0), 300.0))
@spaces.GPU(duration=estimate_render_duration)
def render_blend_gpu(
blend_path: str, out_path: str, samples: int, res_x: int, res_y: int,
camera: dict | None = None,
) -> str:
"""Render a .blend with Cycles on the ZeroGPU worker's GPU (OptiX/CUDA)."""
return _run_blender_render(blend_path, out_path, "CYCLES_GPU", samples, res_x, res_y, camera)
def render_blend_local(
blend_path: str, out_path: str, engine: str, samples: int, res_x: int, res_y: int,
camera: dict | None = None,
) -> str:
"""CPU Cycles or experimental software-GL EEVEE render in the main container."""
if engine == "CYCLES_CPU":
samples = min(samples, _MAX_CPU_SAMPLES)
return _run_blender_render(blend_path, out_path, "CYCLES_CPU", samples, res_x, res_y, camera)
if engine == "EEVEE":
env = {"LIBGL_ALWAYS_SOFTWARE": "1", "GALLIUM_DRIVER": "llvmpipe"}
return _run_blender_render(
blend_path, out_path, "EEVEE", samples, res_x, res_y, camera, extra_env=env
)
raise ValueError(f"Unknown engine: {engine}")
# --- animation rendering -----------------------------------------------------
def estimate_animation_duration(
blend_path: str, out_path: str, samples: int, res_x: int, res_y: int,
fps: int = 0, frame_start: int = 0, frame_end: int = 0,
camera: dict | None = None,
) -> int:
"""
ZeroGPU duration estimate for animations: startup + per-frame render cost.
Kept as tight as realistic - over-declaring blocks low-quota visitors.
"""
frames = max((frame_end - frame_start + 1) if frame_end > frame_start else 72, 1)
pixel_factor = max((res_x * res_y) / (1280 * 720), 0.1)
per_frame = 1.0 * (samples / 64.0) * pixel_factor
return int(min(max(35.0 + frames * per_frame, 60.0), 300.0))
@spaces.GPU(duration=estimate_animation_duration)
def render_animation_gpu(
blend_path: str, out_path: str, samples: int, res_x: int, res_y: int,
fps: int = 0, frame_start: int = 0, frame_end: int = 0,
camera: dict | None = None,
) -> str:
"""Render the scene's animation to MP4 with Cycles on the ZeroGPU worker."""
return _run_blender_render(
blend_path, out_path, "CYCLES_GPU", samples, res_x, res_y, camera,
timeout=340.0,
anim={"fps": fps, "frame_start": frame_start, "frame_end": frame_end},
)
def render_animation_local(
blend_path: str, out_path: str, engine: str, samples: int, res_x: int, res_y: int,
fps: int = 0, frame_start: int = 0, frame_end: int = 0,
camera: dict | None = None,
) -> str:
"""Workbench/EEVEE preview animation via software GL in the main container."""
if engine not in ("WORKBENCH", "EEVEE"):
raise ValueError(f"Unknown animation preview engine: {engine}")
env = {"LIBGL_ALWAYS_SOFTWARE": "1", "GALLIUM_DRIVER": "llvmpipe"}
return _run_blender_render(
blend_path, out_path, engine, samples, res_x, res_y, camera,
extra_env=env, timeout=900.0,
anim={"fps": fps, "frame_start": frame_start, "frame_end": frame_end},
)