Skip to content

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

Merged
sebcrozet merged 1 commit into
dimforge:masterfrom
haixuanTao:feat/headless-capture-pipeline
Jul 16, 2026
Merged

Headless Window + pipelined (non-blocking) pixel readback#398
sebcrozet merged 1 commit into
dimforge:masterfrom
haixuanTao:feat/headless-capture-pipeline

Conversation

@haixuanTao

@haixuanTao haixuanTao commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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 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
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).
@sebcrozet
sebcrozet force-pushed the feat/headless-capture-pipeline branch from 7003b7d to 1e23ce7 Compare July 16, 2026 11:27
Comment thread src/window/screenshot.rs
Comment on lines +96 to +98
let img = ImageBuffer::from_vec(width, height, buf)
.expect("readback buffer was not big enough for image");
Some(imageops::flip_vertical(&img))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m curious if we could avoid flipping the image and if we could cache img or take a &mut ImageBuffer argument to avoid CPU-side allocation of the image buffer.

Not blocking for this PR though.

@sebcrozet
sebcrozet merged commit 0143371 into dimforge:master Jul 16, 2026
6 checks passed
@sebcrozet

Copy link
Copy Markdown
Member

Thanks!

@sebcrozet sebcrozet mentioned this pull request Jul 16, 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.

2 participants