- 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>
86 lines
2.6 KiB
Python
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()
|