|
15 | 15 |
|
16 | 16 | from __future__ import annotations |
17 | 17 |
|
| 18 | +import ctypes |
18 | 19 | import time |
19 | 20 |
|
20 | 21 | import numpy as np |
21 | 22 | import warp as wp |
| 23 | +import warp.render.render_opengl |
22 | 24 |
|
23 | 25 | import newton as nt |
24 | 26 | from newton.selection import ArticulationView |
@@ -127,6 +129,10 @@ def __init__(self, width=1920, height=1080, vsync=False, headless=False): |
127 | 129 | # positions: "side", "stats", "free" |
128 | 130 | self._ui_callbacks = {"side": [], "stats": [], "free": []} |
129 | 131 |
|
| 132 | + # Initialize PBO (Pixel Buffer Object) resources used in the `get_frame` method. |
| 133 | + self._pbo = None |
| 134 | + self._wp_pbo = None |
| 135 | + |
130 | 136 | self.set_model(None) |
131 | 137 |
|
132 | 138 | def register_ui_callback(self, callback, position="side"): |
@@ -475,6 +481,83 @@ def _update(self): |
475 | 481 |
|
476 | 482 | self.renderer.present() |
477 | 483 |
|
| 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 | + |
478 | 561 | @override |
479 | 562 | def is_running(self) -> bool: |
480 | 563 | """ |
|
0 commit comments