Skip to content

Commit ae810e5

Browse files
authored
Merge branch 'main' into diegoferigo/fix_mjcf_with_texture
2 parents d5ed629 + f8df642 commit ae810e5

File tree

1 file changed

+83
-0
lines changed

1 file changed

+83
-0
lines changed

newton/_src/viewer/viewer_gl.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515

1616
from __future__ import annotations
1717

18+
import ctypes
1819
import time
1920

2021
import numpy as np
2122
import warp as wp
23+
import warp.render.render_opengl
2224

2325
import newton as nt
2426
from newton.selection import ArticulationView
@@ -127,6 +129,10 @@ def __init__(self, width=1920, height=1080, vsync=False, headless=False):
127129
# positions: "side", "stats", "free"
128130
self._ui_callbacks = {"side": [], "stats": [], "free": []}
129131

132+
# Initialize PBO (Pixel Buffer Object) resources used in the `get_frame` method.
133+
self._pbo = None
134+
self._wp_pbo = None
135+
130136
self.set_model(None)
131137

132138
def register_ui_callback(self, callback, position="side"):
@@ -475,6 +481,83 @@ def _update(self):
475481

476482
self.renderer.present()
477483

484+
def get_frame(self, target_image: wp.array | None = None) -> wp.array:
485+
"""
486+
Retrieve the last rendered frame.
487+
488+
This method uses OpenGL Pixel Buffer Objects (PBO) and CUDA interoperability
489+
to transfer pixel data entirely on the GPU, avoiding expensive CPU-GPU transfers.
490+
491+
Args:
492+
target_image (wp.array, optional):
493+
Optional pre-allocated Warp array with shape `(height, width, 3)`
494+
and dtype `wp.uint8`. If `None`, a new array will be created.
495+
496+
Returns:
497+
wp.array: GPU array containing RGB image data with shape `(height, width, 3)`
498+
and dtype `wp.uint8`. Origin is top-left (OpenGL's bottom-left is flipped).
499+
"""
500+
501+
gl = RendererGL.gl
502+
w, h = self.renderer._screen_width, self.renderer._screen_height
503+
504+
# Lazy initialization of PBO (Pixel Buffer Object).
505+
if self._pbo is None:
506+
pbo_id = (gl.GLuint * 1)()
507+
gl.glGenBuffers(1, pbo_id)
508+
self._pbo = pbo_id[0]
509+
510+
# Allocate PBO storage.
511+
gl.glBindBuffer(gl.GL_PIXEL_PACK_BUFFER, self._pbo)
512+
gl.glBufferData(gl.GL_PIXEL_PACK_BUFFER, gl.GLsizeiptr(w * h * 3), None, gl.GL_STREAM_READ)
513+
gl.glBindBuffer(gl.GL_PIXEL_PACK_BUFFER, 0)
514+
515+
# Register with CUDA.
516+
self._wp_pbo = wp.RegisteredGLBuffer(
517+
gl_buffer_id=int(self._pbo),
518+
device=self.device,
519+
flags=wp.RegisteredGLBuffer.READ_ONLY,
520+
)
521+
522+
# Set alignment once.
523+
gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
524+
525+
# GPU-to-GPU readback into PBO.
526+
assert self.renderer._frame_fbo is not None
527+
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.renderer._frame_fbo)
528+
gl.glBindBuffer(gl.GL_PIXEL_PACK_BUFFER, self._pbo)
529+
gl.glReadPixels(0, 0, w, h, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, ctypes.c_void_p(0))
530+
gl.glBindBuffer(gl.GL_PIXEL_PACK_BUFFER, 0)
531+
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
532+
533+
# Map PBO buffer and copy using RGB kernel.
534+
assert self._wp_pbo is not None
535+
buf = self._wp_pbo.map(dtype=wp.uint8, shape=(w * h * 3,))
536+
537+
if target_image is None:
538+
target_image = wp.empty(
539+
shape=(h, w, 3),
540+
dtype=wp.uint8, # pyright: ignore[reportArgumentType]
541+
device=self.device,
542+
)
543+
544+
if target_image.shape != (h, w, 3):
545+
raise ValueError(f"Shape of `target_image` must be ({h}, {w}, 3), got {target_image.shape}")
546+
547+
# Launch the RGB kernel.
548+
wp.launch(
549+
warp.render.render_opengl.copy_rgb_frame_uint8,
550+
dim=(w, h),
551+
inputs=[buf, w, h],
552+
outputs=[target_image],
553+
device=self.device,
554+
)
555+
556+
# Unmap the PBO buffer.
557+
self._wp_pbo.unmap()
558+
559+
return target_image
560+
478561
@override
479562
def is_running(self) -> bool:
480563
"""

0 commit comments

Comments
 (0)