Skip to content

Headless Window + pipelined (non-blocking) pixel readback#398

Open
haixuanTao wants to merge 2 commits into
dimforge:masterfrom
haixuanTao:feat/headless-capture-pipeline
Open

Headless Window + pipelined (non-blocking) pixel readback#398
haixuanTao wants to merge 2 commits into
dimforge:masterfrom
haixuanTao:feat/headless-capture-pipeline

Conversation

@haixuanTao

@haixuanTao haixuanTao commented Jul 8, 2026

Copy link
Copy Markdown

Stacked on #397 — merge #397 first; this PR then reduces to its single commit (feat: public headless Window + pipelined (non-blocking) pixel readback). Until then the diff below includes #397's read_pixels changes.

Two additions for off-screen video capture:

  • Window::new_headless_with_setup(width, height, setup): a full-featured Window backed by no OS window and no swapchain — the same headless canvas as OffscreenSurface, but exposing the whole Window API (custom renderers, ray tracer, snap* readbacks). Never throttled by display vsync; works without a display server.

  • snap_begin() / snap_finish(): pipelined readback that enqueues the texture→buffer copy and buffer map without waiting, so frame N's pixels are collected after frame N+1 renders instead of stalling the GPU pipeline every frame. read_pixels() is refactored onto the same begin/finish path with unchanged behavior.

Motivation: a windowed capture loop (render_frame + blocking snap) is vsync-locked — the swapchain defaults to AutoVsync (Fifo) and the blocking readback defeats frame pipelining, so every iteration eats ~2 vblanks (~30 fps at 60 Hz) regardless of GPU load. Headless + pipelined readback took a downstream nexus3d capture loop from 33 to 92 gen-fps at 640×480.

🤖 Generated with Claude Code

haixuanTao and others added 2 commits July 8, 2026 12:19
…ing buffer

read_pixels indexed the mapped staging buffer byte-by-byte during
BGRA->RGB conversion. Mapped readback memory is uncached
(write-combined), so scalar reads run at ~10 MB/s — 99 ms for a 640x480
frame on an RTX 5080, dwarfing the 4 ms render. memcpy each row into a
cached local buffer first and convert from there: 5.4 ms (18x).

Also: reuse the staging buffer across calls (grown on demand) instead of
allocating per call, and wait on the copy's submission index instead of
polling the device indefinitely.

Per-frame capture loop (sim + render + snap, 640x480): 113 ms -> 12 ms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two additions for off-screen video capture:

- Window::new_headless_with_setup(width, height, setup): a full-featured
  Window backed by no OS window and no swapchain (same headless canvas as
  OffscreenSurface, but exposing the whole Window API — custom renderers,
  ray tracer, snap* readbacks). Rendering is never throttled by the
  display's vsync and works without a display server.

- snap_begin()/snap_finish() (begin_read_pixels/finish_read_pixels on the
  canvas): pipelined readback that enqueues the texture->buffer copy and
  buffer map without waiting, so the copy of frame N can be collected after
  frame N+1 renders instead of stalling the GPU pipeline every frame the
  way the blocking snap()/read_pixels() does. read_pixels() is refactored
  to begin+finish with unchanged behavior.

Motivation: a windowed capture loop (render_frame + blocking snap) is
vsync-locked — the blocking readback defeats swapchain frame pipelining, so
every iteration eats ~2 vblanks (~30 fps at 60 Hz) regardless of GPU load.
Headless + pipelined readback took a downstream nexus3d capture loop from
33 to 92 fps at 640x480.

Builds on feat/persistent-readback-staging (dimforge#397).
haixuanTao added a commit to haixuanTao/nexus that referenced this pull request Jul 8, 2026
…ewer, pipelined readback

Makes the nexus3d Python bindings usable for off-screen video capture:

- NexusViewer(width, height): configurable resolution; set_draw_ui(False)
  keeps the egui panel out of captured frames.
- NexusViewer.render() -> (H, W, 3) uint8 numpy frame export, like
  mujoco.Renderer.render().
- raytrace_frame() + set_raytracer_samples_per_frame/max_bounces/denoise
  + raytracer_backend(): kiss3d's GPU path tracer from Python, with
  progressive sample accumulation. sync takes the CPU-readback path while
  the tracer is active (the zero-readback kernel only updates the
  rasterizer's instance buffers, which the tracer's BVH never reads).
- NexusViewer(..., headless=True): no OS window, no swapchain — frames
  render into an off-screen texture, never throttled by display vsync,
  works without a display server.
- set_vsync(enabled)/vsync(): uncap a windowed viewer instead.
- render_async()/render_flush(): pipelined capture — returns the previous
  frame while the current frame's GPU->CPU copy runs in the background.

A windowed capture loop was vsync-locked at ~30 fps (blocking readback +
Fifo swapchain = ~2 vblanks per iteration); headless + pipelined readback
reaches 92 gen-fps at 640x480 (GPU physics) on an RTX 5080.

Requires the kiss3d headless-capture branch (dimforge/kiss3d#398).
Supersedes dimforge#7 and dimforge#8 (included here).
@haixuanTao haixuanTao force-pushed the feat/headless-capture-pipeline branch from 4440e6b to 7003b7d Compare July 8, 2026 19:56
@haixuanTao haixuanTao changed the title Headless capture: public headless Window, pipelined pixel readback, fast read_pixels Headless Window + pipelined (non-blocking) pixel readback Jul 8, 2026
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