Skip to content

Python: headless viewer, vsync control, pipelined frame capture#11

Open
haixuanTao wants to merge 8 commits into
dimforge:mainfrom
haixuanTao:feat/python-headless-capture
Open

Python: headless viewer, vsync control, pipelined frame capture#11
haixuanTao wants to merge 8 commits into
dimforge:mainfrom
haixuanTao:feat/python-headless-capture

Conversation

@haixuanTao

@haixuanTao haixuanTao commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on #8 (which stacks on #7) — merge order: #7#8 → this. After those merge, this PR reduces to its single commit (feat: headless viewer, vsync control, and pipelined frame capture (Python)); until then the diff below includes their changes.

New capture API in the nexus3d Python bindings:

  • NexusViewer(..., headless=True) — no OS window, no swapchain: frames render into an off-screen texture, never throttled by display vsync, and it works without a display server.
  • set_vsync(enabled) / vsync() — uncap a windowed viewer instead.
  • snap_rgb_async() / snap_rgb_flush() — pipelined capture: returns the previous frame while the current frame's GPU→CPU copy runs in the background (one frame of latency; flush collects the last frame).

Why: the windowed capture loop was vsync-locked at ~30 fps — the swapchain defaults to Fifo and the blocking per-frame snap_rgb() readback defeats frame pipelining, so each iteration ate ~2 vblanks regardless of GPU load. Headless + pipelined readback reaches 92 gen-fps at 640×480 (GPU physics; 37.5 with the Rapier CPU backend) on an RTX 5080, up from 33.

⚠️ Builds against the kiss3d headless-capture branch: dimforge/kiss3d#398.

🤖 Generated with Claude Code

haixuanTao and others added 3 commits July 7, 2026 16:51
The viewer could only present to its window; there was no way to get
rendered pixels back to Python for headless frame/video export (as
mujoco.Renderer.render() or a Genesis camera.render() would give).

kiss3d's Window already exposes snap_image(), so this just wires it
through:
- nexus_viewer3d::NexusViewer::snap_rgb() -> (w, h, RGB bytes)
- Python NexusViewer.render() -> (H, W, 3) uint8 NumPy array

Call render_frame() to draw, then render() to read the frame back.
Adds NexusViewer::new_with_size and exposes width/height (default
1200x900) on the Python constructor, so headless capture and path
tracing can run at arbitrary resolutions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
render_frame always drew the egui main panel, which then appeared in
snap_rgb captures. Expose a toggle (default on) so headless recording
gets clean frames.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@haixuanTao haixuanTao force-pushed the feat/python-headless-capture branch from e000dc5 to 5bd7f21 Compare July 8, 2026 19:56
@haixuanTao haixuanTao changed the title Python frame capture: render export, path tracing, headless viewer, pipelined readback Python: headless viewer, vsync control, pipelined frame capture Jul 8, 2026
haixuanTao and others added 5 commits July 9, 2026 09:04
Adds NexusViewer.raytrace_frame() (kiss3d 0.45 GPU path tracer with
cross-frame sample accumulation), set_raytracer_samples_per_frame/
max_bounces/denoise, and snap_rgb() returning the framebuffer as an
(H, W, 3) uint8 numpy array.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The zero-readback sync kernel writes body poses straight into kiss3d's
GPU instance buffers, but the ray tracer rebuilds its acceleration
structure from the CPU-side instance data — so path-traced frames froze
at the initial scene. Route sync through the readback path whenever a
RayTracer exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NexusViewer.raytracer_backend() returns "hardware" (RT-core ray
queries) or "software" (compute-shader BVH fallback) so users can
verify which backend kiss3d selected on their GPU.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…thon)

The windowed capture loop was vsync-locked: kiss3d's swapchain defaults to
AutoVsync and the blocking per-frame snap_rgb() readback defeats frame
pipelining, so each render_frame+render iteration ate ~2 vblanks (~30 fps
at 60 Hz) no matter how fast the GPU was. Fixes, exposed through nexus3d:

- NexusViewer(width, height, headless=True): no OS window, no swapchain —
  frames render into an off-screen texture (kiss3d headless Window), never
  throttled by the display and usable without a display server.
- NexusViewer.set_vsync(enabled)/vsync(): uncap a windowed viewer instead.
- NexusViewer.render_async()/render_flush(): pipelined capture — returns
  the previous frame while this frame's GPU->CPU copy runs in the
  background (one frame of latency; flush collects the last frame).

Cube-drop capture at 640x480: 33.1 -> 92.1 gen-fps (GPU physics),
26.5 -> 37.5 (Rapier CPU backend).

Requires kiss3d feat/headless-capture-pipeline (new_headless_with_setup +
snap_begin/snap_finish).
@haixuanTao haixuanTao force-pushed the feat/python-headless-capture branch from 5bd7f21 to 6a3c7e1 Compare July 9, 2026 07:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant