perf: render pipeline optimizations — sample scaling, USD logging, persistent BVH

Task 1: Resolution-aware sample count
- Auto-scale samples for resolutions <= 1024: max(32, samples * max_dim / 2048)
- 512x512 thumbnails: 256 → 64 samples (75% GPU savings)
- Thumbnail tasks capped at 64 samples via context manager
- 2048x2048 HQ renders unchanged

Task 2: USD path preference audit + logging
- Verified USD master path is correctly preferred over GLB tessellation
- Added clear emit() messages: "Using USD master" vs "No USD master — GLB path"
- Dynamic render log label: "USD → Blender" vs "STEP → GLB → Blender"

Task 3: Persistent BVH for turntable animations
- Added scene.render.use_persistent_data = True before frame loop
- BVH acceleration structure cached between frames (not rebuilt per frame)
- Applies to both camera orbit and object rotation modes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 12:03:31 +01:00
parent ce15526a15
commit ffe3eebfca
4 changed files with 125 additions and 103 deletions
@@ -14,6 +14,40 @@ from app.core.pipeline_logger import PipelineLogger
logger = logging.getLogger(__name__)
# Maximum samples for thumbnail renders (512x512).
# Full-resolution renders use 256+ samples; thumbnails don't need more than 64.
_THUMBNAIL_SAMPLE_CAP = 64
@contextmanager
def _capped_thumbnail_samples():
"""Temporarily cap render samples for thumbnail renders.
Thumbnails are 512x512 — using 256 Cycles samples is wasteful.
This patches _get_all_settings in step_processor to cap samples
at _THUMBNAIL_SAMPLE_CAP for the duration of the thumbnail render.
"""
import app.services.step_processor as _sp
_original = _sp._get_all_settings
def _patched() -> dict[str, str]:
settings = _original()
for key in ("blender_cycles_samples", "blender_eevee_samples"):
try:
val = int(settings.get(key, "256"))
if val > _THUMBNAIL_SAMPLE_CAP:
logger.info("Capping thumbnail %s: %d -> %d", key, val, _THUMBNAIL_SAMPLE_CAP)
settings[key] = str(_THUMBNAIL_SAMPLE_CAP)
except (ValueError, TypeError):
pass
return settings
_sp._get_all_settings = _patched
try:
yield
finally:
_sp._get_all_settings = _original
@contextmanager
def _pipeline_session(tenant_id: str | None = None):
@@ -67,11 +101,12 @@ def render_step_thumbnail(self, cad_file_id: str):
except Exception:
logger.warning(f"step_file_hash computation failed for {cad_file_id} (non-fatal)")
# ── Render thumbnail ──────────────────────────────────────────────────
# ── Render thumbnail (with capped samples for 512x512) ──────────────
try:
from app.services.step_processor import regenerate_cad_thumbnail
pl.info("render_step_thumbnail", "Calling regenerate_cad_thumbnail")
success = regenerate_cad_thumbnail(cad_file_id, part_colors={})
with _capped_thumbnail_samples():
success = regenerate_cad_thumbnail(cad_file_id, part_colors={})
if not success:
raise RuntimeError("regenerate_cad_thumbnail returned False")
except Exception as exc:
@@ -166,7 +201,8 @@ def regenerate_thumbnail(self, cad_file_id: str, part_colors: dict):
try:
from app.services.step_processor import regenerate_cad_thumbnail
success = regenerate_cad_thumbnail(cad_file_id, part_colors)
with _capped_thumbnail_samples():
success = regenerate_cad_thumbnail(cad_file_id, part_colors)
if not success:
raise RuntimeError("regenerate_cad_thumbnail returned False")
except Exception as exc: