Files
NobodyandClaude Fable 5 23b79e171b Progressive preview quality + scene caching: 21 fps camera drags
- While interacting the frontend requests quality=fast (256px, no AA,
  JPEG q80) and automatically follows with a quality=high refinement
  (384px, 2x AA) 250ms after the sliders settle. The queued render is
  unaffected and always uses the full width/height.
- The worker caches the built pyrender scene per mesh, so camera-only
  frames skip trimesh/GPU-mesh rebuilding and just move the camera and
  light nodes (background changes update the cached scene's bg_color;
  any mesh/texture change busts the key).
- The worker now encodes the preview JPEG itself, replacing the
  multi-megabyte float32 npz roundtrip with ~5-12KB per frame.
- Fix: np.savez appends .npz to extensionless paths, so the worker's
  two output modes use explicit out.npz / out.jpg names.

Measured end-to-end in a live ComfyUI instance: camera drag 47ms/frame
(21 fps), settle refinement 0.20s; was ~100ms/frame before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:17:36 +09:00

86 lines
2.6 KiB
Python

"""Render worker for ComfyUI-GNM.
Runs render_head in a fresh process so that GL/PyOpenGL state polluted by
other ComfyUI custom nodes (e.g. PYOPENGL_PLATFORM=egl on Windows) cannot
break rendering.
Modes:
python render_worker.py <in.npz> <params.json> <out.npz> one-shot
python render_worker.py --serve persistent
In --serve mode each stdin line is a request "<in>\t<params>\t<out>" and
each stdout line is a response "OK" or "ERR <message>". The process exits
when stdin closes (parent died). Keeping the process alive avoids paying
the ~1.5s interpreter/import startup on every live-preview frame.
"""
import importlib.util
import json
import os
import sys
import numpy as np
def _load_renderer():
here = os.path.dirname(os.path.abspath(__file__))
spec = importlib.util.spec_from_file_location(
"comfyui_gnm_renderer", os.path.join(here, "renderer.py")
)
renderer = importlib.util.module_from_spec(spec)
spec.loader.exec_module(renderer)
return renderer
def _render_one(renderer, in_path, params_path, out_path):
with np.load(in_path) as data:
vertices = data["vertices"]
faces = data["faces"]
uvs = data["uvs"] if "uvs" in data.files else None
with open(params_path, "r", encoding="utf-8") as f:
params = json.load(f)
texture_path = params.pop("texture_path", None)
encode = params.pop("_encode", None)
out = renderer.render_head(
vertices, faces, uvs=uvs, texture_path=texture_path, **params
)
if encode is not None:
jpeg = renderer._encode_jpeg(out["color"], encode.get("quality", 92))
with open(out_path, "wb") as f:
f.write(jpeg)
else:
np.savez(out_path, **out)
def serve():
renderer = _load_renderer()
# Single-threaded request loop -> safe to keep one GL context alive,
# saving ~100ms of context setup per render.
renderer.enable_renderer_reuse()
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
in_path, params_path, out_path = line.split("\t")
_render_one(renderer, in_path, params_path, out_path)
sys.stdout.write("OK\n")
except Exception as exc: # keep serving after a bad request
msg = " ".join(str(exc).split())[:500]
sys.stdout.write(f"ERR {msg}\n")
sys.stdout.flush()
def main():
if len(sys.argv) >= 2 and sys.argv[1] == "--serve":
serve()
return
in_path, params_path, out_path = sys.argv[1:4]
_render_one(_load_renderer(), in_path, params_path, out_path)
if __name__ == "__main__":
main()