Skip to content

[Televiz] ProjectionLayer + in-loop frame protocol refactor#544

Open
farbod-nv wants to merge 1 commit into
mainfrom
fm/projectionlayer
Open

[Televiz] ProjectionLayer + in-loop frame protocol refactor#544
farbod-nv wants to merge 1 commit into
mainfrom
fm/projectionlayer

Conversation

@farbod-nv

@farbod-nv farbod-nv commented May 18, 2026

Copy link
Copy Markdown
Contributor

Adds ProjectionLayer for full-view RGBD content (gsplat, nvblox, neural reconstruction) and refactors VizSession's frame loop so the poses fed to renderers match the poses submitted to OpenXR.

ProjectionLayer

In-loop contract:

info = session.begin_frame()                # xrWaitFrame + Begin + LocateViews
color, depth = renderer.render(info.views)  # render against THIS frame's views
layer.submit(color, depth)                  # publish for this frame
session.end_frame()                         # composite + xrEndFrame

The runtime / CloudXR paces the app via xrWaitFrame; if the renderer takes 30 ms, the app runs at ~30 fps and the runtime compositor reprojects the last submitted frame at display rate.

Storage: per-slot (color, depth) DeviceImages (7-slot mailbox per QuadLayer's pattern). Fragment shader samples color + depth and writes gl_FragDepth so QuadLayer / future OverlayLayer Z-composite correctly in the shared RT. Stereo: paired (left, right) slot storage; submit() ships both eyes on one CUDA stream.

In kXr, a visible ProjectionLayer that doesn't submit for the current frame is skipped at record() time so stale RGBD never gets composited under a new projection-layer pose. The freshness gate is off in kWindow / kOffscreen where no XR pose mismatch is possible.

Single XrCompositionLayerProjection covers QuadLayers + ProjectionLayer together — CloudXR fast-paths when only ProjectionLayer is present, and squashes per-layer-timewarped when mixed (Monado's compute-shader layer accumulator handles this automatically).

VizSession frame-loop refactor

VizSession::begin_frame previously returned placeholder identity views; the real per-eye XR poses were only acquired later inside VizCompositor::render via backend_->begin_frame. That meant renderers calling submit against the returned FrameInfo were rendering against the wrong pose.

Moves backend frame acquisition into VizSession::begin_frame: acquired Frame is stored in current_backend_frame_ and consumed by end_frame. FrameInfo.views now carries the real xrLocateViews output, so the in-loop pattern above is pose-correct by construction.

VizCompositor::render takes the acquired Frame as a parameter; the existing FrameGuard still owns end_frame/abort_frame protocol balance.

Adds Frame::predicted_display_time_ns so XR's
last_frame_state_.predictedDisplayTime flows through to FrameInfo.predicted_display_time.

LayerBase::on_frame_begin

New virtual called from VizSession::begin_frame on every registered layer (default no-op). ProjectionLayer uses it to clear its submitted-this-frame freshness flag.

Minor changes

  • DeviceImage allows PixelFormat::kD32F (was rejected as "reserved for ProjectionLayer"). kD32F + mip_levels > 1 rejected explicitly.
  • Python bindings: expose ViewInfo and FrameInfo.views. No ProjectionViewSnapshot / predicted_views / get_last_frame_info — the renderer reads info.views returned by begin_frame.

Tests

  • 13 C++ Catch2 tests (4 unit + 9 GPU): config validation, slot/view allocation, idempotent destroy, submit shape/format/dimension validation, mono+depth mailbox advance, stereo paired submit, no-depth path, on_frame_begin/submitted-this-frame toggling.
  • 8 Python pytest tests covering the same surface plus the in-loop submit pattern via begin_frame/end_frame.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added ProjectionLayer for projecting stereo/mono RGB-D (color + depth) content into the visualization pipeline, with support for flexible format configurations and frame-by-frame submissions.
    • Exposed ProjectionLayer to Python with full API support including stereo/depth configuration and buffer submission methods.
  • Enhancements

    • Extended frame information with predicted display timing from the backend system.
    • Improved frame lifecycle management with new frame-begin callbacks for layers.

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces ProjectionLayer, a new Vulkan compositing layer for stereo/mono RGBD (color + optional depth) rendering with CUDA interoperability. It refactors the session/compositor frame acquisition to pass frames explicitly rather than acquiring them internally, adds a new LayerBase::on_frame_begin() callback for per-frame CPU state management, and exposes the new functionality to Python with comprehensive tests covering configuration, submission validation, and render-loop semantics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The PR introduces significant new functionality (ProjectionLayer implementation, frame lifecycle refactor, shader programs, Python bindings) across heterogeneous files with dense logic in submission, rendering, and frame synchronization paths. The frame lifecycle refactor affects the compositor core and session state management, requiring careful review of exception handling and RAII patterns. Multiple independent subsystems (Vulkan resource management, CUDA interop, descriptor set binding, pipeline selection) must be verified separately for correctness.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: adding ProjectionLayer and refactoring the frame protocol, matching the changeset's scope and objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fm/projectionlayer

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Infer (1.2.0)
src/viz/core/cpp/device_image.cpp

src/viz/core/cpp/device_image.cpp:4:10: fatal error: 'viz/core/device_image.hpp' file not found
4 | #include <viz/core/device_image.hpp>
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.
Error: the following clang command did not run successfully:
/opt/infer-linux-x86_64-v1.2.0/lib/infer/facebook-clang-plugins/clang/install/bin/clang-18
@/tmp/coderabbit-infer/e1c88958fa7b642a350462cb60d64d45ea9d846c-a93d7c81402b8c3f/tmp/clang_command_.tmp.f71bb1.txt
++Contents of '/tmp/coderabbit-infer/e1c88958fa7b642a350462cb60d64d45ea9d846c-a93d7c81402b8c3f/tmp/clang_command_.tmp.f71bb1.txt':
"-cc1" "-load"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../../facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib"
"-add-plugin" "BiniouASTExporter" "-plugin-arg-BiniouASTExporter" "-"
"-plugin-arg-BiniouASTExporter" "PREPEND_CURRENT_DIR=1"
"-plugin-arg-BiniouASTExporter" "MAX_STRING_SIZE=65535" "-cc1" "-triple"
"x86_64-unknown-linux-gn

... [truncated 1119 characters] ...

l/lib/clang/18/include"
"-internal-isystem" "/usr/local/include" "-internal-isystem"
"/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/include"
"-internal-externc-isystem" "/usr/include/x86_64-linux-gnu"
"-internal-externc-isystem" "/include" "-internal-externc-isystem"
"/usr/include" "-Wno-ignored-optimization-argument" "-Wno-everything"
"-fdeprecated-macro" "-ferror-limit" "19" "-fgnuc-version=4.2.1"
"-fskip-odr-check-in-gmf" "-fcxx-exceptions" "-fexceptions"
"-D__GCC_HAVE_DWARF2_CFI_ASM=1" "-o"
"/tmp/coderabbit-infer/a93d7c81402b8c3f/file.o" "-x" "c++"
"src/viz/core/cpp/device_image.cpp" "-O0" "-fno-builtin" "-include"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../lib/clang_wrappers/global_defines.h"
"-Wno-everything"

src/viz/layers/cpp/projection_layer.cpp

src/viz/layers/cpp/projection_layer.cpp:4:10: fatal error: 'viz/core/render_target.hpp' file not found
4 | #include <viz/core/render_target.hpp>
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.
Error: the following clang command did not run successfully:
/opt/infer-linux-x86_64-v1.2.0/lib/infer/facebook-clang-plugins/clang/install/bin/clang-18
@/tmp/coderabbit-infer/e1c88958fa7b642a350462cb60d64d45ea9d846c-cef15e1b27734e0f/tmp/clang_command_.tmp.8dabd3.txt
++Contents of '/tmp/coderabbit-infer/e1c88958fa7b642a350462cb60d64d45ea9d846c-cef15e1b27734e0f/tmp/clang_command_.tmp.8dabd3.txt':
"-cc1" "-load"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../../facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib"
"-add-plugin" "BiniouASTExporter" "-plugin-arg-BiniouASTExporter" "-"
"-plugin-arg-BiniouASTExporter" "PREPEND_CURRENT_DIR=1"
"-plugin-arg-BiniouASTExporter" "MAX_STRING_SIZE=65535" "-cc1" "-triple"
"x86_64-unknown

... [truncated 1138 characters] ...

clang/18/include"
"-internal-isystem" "/usr/local/include" "-internal-isystem"
"/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/include"
"-internal-externc-isystem" "/usr/include/x86_64-linux-gnu"
"-internal-externc-isystem" "/include" "-internal-externc-isystem"
"/usr/include" "-Wno-ignored-optimization-argument" "-Wno-everything"
"-fdeprecated-macro" "-ferror-limit" "19" "-fgnuc-version=4.2.1"
"-fskip-odr-check-in-gmf" "-fcxx-exceptions" "-fexceptions"
"-D__GCC_HAVE_DWARF2_CFI_ASM=1" "-o"
"/tmp/coderabbit-infer/cef15e1b27734e0f/file.o" "-x" "c++"
"src/viz/layers/cpp/projection_layer.cpp" "-O0" "-fno-builtin" "-include"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../lib/clang_wrappers/global_defines.h"
"-Wno-everything"

src/viz/layers_tests/cpp/test_projection_layer.cpp

src/viz/layers_tests/cpp/test_projection_layer.cpp:9:10: fatal error: 'test_helpers.hpp' file not found
9 | #include "test_helpers.hpp"
| ^~~~~~~~~~~~~~~~~~
1 error generated.
Error: the following clang command did not run successfully:
/opt/infer-linux-x86_64-v1.2.0/lib/infer/facebook-clang-plugins/clang/install/bin/clang-18
@/tmp/coderabbit-infer/e1c88958fa7b642a350462cb60d64d45ea9d846c-ee53be5bc97bb915/tmp/clang_command_.tmp.d9b26b.txt
++Contents of '/tmp/coderabbit-infer/e1c88958fa7b642a350462cb60d64d45ea9d846c-ee53be5bc97bb915/tmp/clang_command_.tmp.d9b26b.txt':
"-cc1" "-load"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../../facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib"
"-add-plugin" "BiniouASTExporter" "-plugin-arg-BiniouASTExporter" "-"
"-plugin-arg-BiniouASTExporter" "PREPEND_CURRENT_DIR=1"
"-plugin-arg-BiniouASTExporter" "MAX_STRING_SIZE=65535" "-cc1" "-triple"
"x86_64-unknown-linux-gnu" "-emit-

... [truncated 1139 characters] ...

e"
"-internal-isystem" "/usr/local/include" "-internal-isystem"
"/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/include"
"-internal-externc-isystem" "/usr/include/x86_64-linux-gnu"
"-internal-externc-isystem" "/include" "-internal-externc-isystem"
"/usr/include" "-Wno-ignored-optimization-argument" "-Wno-everything"
"-fdeprecated-macro" "-ferror-limit" "19" "-fgnuc-version=4.2.1"
"-fskip-odr-check-in-gmf" "-fcxx-exceptions" "-fexceptions"
"-D__GCC_HAVE_DWARF2_CFI_ASM=1" "-o"
"/tmp/coderabbit-infer/ee53be5bc97bb915/file.o" "-x" "c++"
"src/viz/layers_tests/cpp/test_projection_layer.cpp" "-O0" "-fno-builtin"
"-include"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../lib/clang_wrappers/global_defines.h"
"-Wno-everything"

  • 6 others

Comment @coderabbitai help to get the list of available commands and usage tips.

Adds ProjectionLayer for full-view RGBD content (gsplat, nvblox,
neural reconstruction) and refactors VizSession's frame loop so the
poses fed to renderers match the poses submitted to OpenXR.

## ProjectionLayer

In-loop contract:

    info = session.begin_frame()                # xrWaitFrame + Begin + LocateViews
    color, depth = renderer.render(info.views)  # render against THIS frame's views
    layer.submit(color, depth)                  # publish for this frame
    session.end_frame()                         # composite + xrEndFrame

The runtime / CloudXR paces the app via xrWaitFrame; if the renderer
takes 30 ms, the app runs at ~30 fps and the runtime compositor
reprojects the last submitted frame at display rate.

Storage: per-slot (color, depth) DeviceImages (7-slot mailbox per
QuadLayer's pattern). Fragment shader samples color + depth and writes
gl_FragDepth so QuadLayer / future OverlayLayer Z-composite correctly
in the shared RT. Stereo: paired (left, right) slot storage; submit()
ships both eyes on one CUDA stream.

In kXr, a visible ProjectionLayer that doesn't submit for the current
frame is skipped at record() time so stale RGBD never gets composited
under a new projection-layer pose. The freshness gate is off in
kWindow / kOffscreen where no XR pose mismatch is possible.

Single XrCompositionLayerProjection covers QuadLayers + ProjectionLayer
together — CloudXR fast-paths when only ProjectionLayer is present,
and squashes per-layer-timewarped when mixed (Monado's compute-shader
layer accumulator handles this automatically).

## VizSession frame-loop refactor

``VizSession::begin_frame`` previously returned placeholder identity
views; the real per-eye XR poses were only acquired later inside
``VizCompositor::render`` via ``backend_->begin_frame``. That meant
renderers calling ``submit`` against the returned FrameInfo were
rendering against the wrong pose.

Moves backend frame acquisition into ``VizSession::begin_frame``:
acquired Frame is stored in ``current_backend_frame_`` and consumed
by ``end_frame``. ``FrameInfo.views`` now carries the real xrLocateViews
output, so the in-loop pattern above is pose-correct by construction.

VizCompositor::render takes the acquired Frame as a parameter; the
existing FrameGuard still owns end_frame/abort_frame protocol balance.

Adds Frame::predicted_display_time_ns so XR's
``last_frame_state_.predictedDisplayTime`` flows through to
``FrameInfo.predicted_display_time``.

## LayerBase::on_frame_begin

New virtual called from VizSession::begin_frame on every registered
layer (default no-op). ProjectionLayer uses it to clear its
submitted-this-frame freshness flag.

## Minor changes

- ``DeviceImage`` allows PixelFormat::kD32F (was rejected as
  "reserved for ProjectionLayer"). kD32F + mip_levels > 1 rejected
  explicitly.
- Python bindings: expose ``ViewInfo`` and ``FrameInfo.views``.
  No ``ProjectionViewSnapshot`` / ``predicted_views`` / ``get_last_frame_info``
  — the renderer reads ``info.views`` returned by ``begin_frame``.

## Tests

- 13 C++ Catch2 tests (4 unit + 9 GPU): config validation, slot/view
  allocation, idempotent destroy, submit shape/format/dimension
  validation, mono+depth mailbox advance, stereo paired submit,
  no-depth path, on_frame_begin/submitted-this-frame toggling.
- 8 Python pytest tests covering the same surface plus the in-loop
  submit pattern via begin_frame/end_frame.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@farbod-nv farbod-nv force-pushed the fm/projectionlayer branch from af669a5 to e1c8895 Compare May 18, 2026 22:14
@farbod-nv farbod-nv changed the title viz: ProjectionLayer + in-loop frame protocol refactor [Televiz] ProjectionLayer + in-loop frame protocol refactor Jun 8, 2026
@farbod-nv farbod-nv requested a review from junjeechao June 8, 2026 16:14
@farbod-nv farbod-nv marked this pull request as ready for review June 8, 2026 16:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/viz/layers_tests/cpp/test_projection_layer.cpp`:
- Around line 86-95: The GPU skip gate must also catch initialization failures:
wrap the Vulkan setup calls so that exceptions from VkContext::init({}) and
RenderTarget::create(...) are converted into SKIP(...) instead of letting them
throw; keep the initial is_gpu_available() check, then try/catch around ctx.init
and creation of target (referencing VkContext::init, RenderTarget::create,
RenderTarget::Config and Resolution) and call SKIP("...") with a brief message
when an exception is caught so the test suite skips cleanly on runners without
working Vulkan.

In `@src/viz/python/layers_bindings.cpp`:
- Around line 184-234: The binding for ProjectionLayer.submit in
layers_bindings.cpp currently throws std::runtime_error only for the left_color
nil case but lets std::invalid_argument from viz::ProjectionLayer::submit
propagate (which becomes Python ValueError), causing test mismatch; wrap the
call to self.submit(...) in a try/catch that catches std::invalid_argument
(thrown by the C++ submit/validation in src/viz/layers/cpp/projection_layer.cpp)
and rethrow as std::runtime_error so pybind11 converts it to Python
RuntimeError; update the catch to include the original exception message when
constructing the std::runtime_error to preserve error details.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 9d2a40b9-036d-453c-95d3-6a3b59e9b9ba

📥 Commits

Reviewing files that changed from the base of the PR and between 3bdb119 and e1c8895.

📒 Files selected for processing (22)
  • src/viz/core/cpp/device_image.cpp
  • src/viz/layers/cpp/CMakeLists.txt
  • src/viz/layers/cpp/inc/viz/layers/projection_layer.hpp
  • src/viz/layers/cpp/projection_layer.cpp
  • src/viz/layers_tests/cpp/CMakeLists.txt
  • src/viz/layers_tests/cpp/test_projection_layer.cpp
  • src/viz/python/core_bindings.cpp
  • src/viz/python/layers_bindings.cpp
  • src/viz/python/session_bindings.cpp
  • src/viz/python/viz_init.py
  • src/viz/python_tests/test_projection_layer.py
  • src/viz/session/cpp/inc/viz/session/display_backend.hpp
  • src/viz/session/cpp/inc/viz/session/layer_base.hpp
  • src/viz/session/cpp/inc/viz/session/viz_compositor.hpp
  • src/viz/session/cpp/inc/viz/session/viz_session.hpp
  • src/viz/session/cpp/viz_compositor.cpp
  • src/viz/session/cpp/viz_session.cpp
  • src/viz/session/cpp/xr_backend.cpp
  • src/viz/shaders/cpp/CMakeLists.txt
  • src/viz/shaders/cpp/projection_layer.frag
  • src/viz/shaders/cpp/projection_layer.vert
  • src/viz/shaders/cpp/projection_layer_no_depth.frag

Comment on lines +86 to +95
TEST_CASE("ProjectionLayer mono+depth creates valid handles for every slot+view", "[gpu][projection_layer]")
{
if (!is_gpu_available())
{
SKIP("No Vulkan-capable GPU available");
}
VkContext ctx;
ctx.init({});
auto target = RenderTarget::create(ctx, RenderTarget::Config{ Resolution{ 64, 64 } });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

GPU skip gate is incomplete; Vulkan init exceptions currently fail tests.

is_gpu_available() alone isn’t preventing hard failures when ctx.init({}) throws (seen in CI). Convert init/setup failures into SKIP(...) so GPU tests skip cleanly on unsupported runners instead of failing the suite.

Proposed fix (apply pattern to all [gpu] tests)
+static bool init_vk_or_skip(VkContext& ctx)
+{
+    try
+    {
+        ctx.init({});
+        return true;
+    }
+    catch (const std::exception&)
+    {
+        return false;
+    }
+}
+
 TEST_CASE("ProjectionLayer mono+depth creates valid handles for every slot+view", "[gpu][projection_layer]")
 {
     if (!is_gpu_available())
     {
         SKIP("No Vulkan-capable GPU available");
     }
     VkContext ctx;
-    ctx.init({});
+    if (!init_vk_or_skip(ctx))
+    {
+        SKIP("Vulkan initialization failed on this runner");
+    }
     auto target = RenderTarget::create(ctx, RenderTarget::Config{ Resolution{ 64, 64 } });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/viz/layers_tests/cpp/test_projection_layer.cpp` around lines 86 - 95, The
GPU skip gate must also catch initialization failures: wrap the Vulkan setup
calls so that exceptions from VkContext::init({}) and RenderTarget::create(...)
are converted into SKIP(...) instead of letting them throw; keep the initial
is_gpu_available() check, then try/catch around ctx.init and creation of target
(referencing VkContext::init, RenderTarget::create, RenderTarget::Config and
Resolution) and call SKIP("...") with a brief message when an exception is
caught so the test suite skips cleanly on runners without working Vulkan.

Source: Pipeline failures

Comment on lines +184 to +234
.def(
"submit",
[](viz::ProjectionLayer& self, py::object left_color, py::object left_depth, py::object right_color,
py::object right_depth, uintptr_t stream)
{
auto to_buf = [&self](py::object obj, viz::PixelFormat fmt, const char* label) -> viz::VizBuffer
{
if (py::isinstance<viz::VizBuffer>(obj))
{
return obj.cast<viz::VizBuffer>();
}
return cuda_array_to_viz_buffer(obj, fmt, self.view_resolution(), label);
};

// Materialize each buffer (or std::nullopt). View slots
// that aren't provided pass nullptr through to submit.
std::optional<viz::VizBuffer> lc;
std::optional<viz::VizBuffer> ld;
std::optional<viz::VizBuffer> rc;
std::optional<viz::VizBuffer> rd;
if (!left_color.is_none())
{
lc = to_buf(left_color, self.color_format(), "ProjectionLayer.submit(left_color)");
}
else
{
throw std::runtime_error("ProjectionLayer.submit: left_color is required");
}
if (!left_depth.is_none())
{
ld = to_buf(left_depth, viz::PixelFormat::kD32F, "ProjectionLayer.submit(left_depth)");
}
if (!right_color.is_none())
{
rc = to_buf(right_color, self.color_format(), "ProjectionLayer.submit(right_color)");
}
if (!right_depth.is_none())
{
rd = to_buf(right_depth, viz::PixelFormat::kD32F, "ProjectionLayer.submit(right_depth)");
}

py::gil_scoped_release release;
self.submit(*lc, ld.has_value() ? &*ld : nullptr, rc.has_value() ? &*rc : nullptr,
rd.has_value() ? &*rd : nullptr, reinterpret_cast<cudaStream_t>(stream));
},
"left_color"_a, "left_depth"_a = py::none(), "right_color"_a = py::none(), "right_depth"_a = py::none(),
"stream"_a = 0,
"Submit a frame. Each arg is a VizBuffer or any __cuda_array_interface__ object. "
"Mono: only ``left_color`` (+ ``left_depth`` if depth-enabled). "
"Stereo: pair with ``right_color`` (+ depths). Buffers must match view_resolution "
"and the layer's pixel formats. Releases the GIL across the copy + sync.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for custom pybind11 exception translators in the bindings
rg -n "register_exception" src/viz/python/ -A 5

# Look for exception_translator or translate_exception
rg -n "translate.*exception|exception.*translator" src/viz/python/ -A 5

# Check what validate_submit_buffer actually throws
ast-grep --pattern 'throw $_(validate_submit_buffer'

Repository: NVIDIA/IsaacTeleop

Length of output: 44


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect where submit/validate throws std::invalid_argument in the C++ layer
echo "== validate/submit exception sites =="
rg -n "validate_submit_buffer|invalid_argument" src/viz/layers -S
rg -n "validate_submit_buffer" src/viz -S

# Show the relevant region around the specific line numbers mentioned (496/513/530) if present
for f in $(rg -l "validate_submit_buffer|ProjectionLayer::submit" src/viz | head -n 20); do
  echo "---- $f ----"
  nl -ba "$f" | sed -n '450,580p' | sed -n '1,200p'
done

# 2) Search the entire python bindings directory for any custom exception translators
echo "== custom exception translator registrations in src/viz/python =="
rg -n "register_exception|translate_exception|exception_translator|PyExc|PyErr_SetString" src/viz/python -S

# 3) Search for pybind11 module initialization / translation elsewhere under src/viz
echo "== custom exception translator registrations in src/viz (broader) =="
rg -n "register_exception|translate_exception|exception_translator" src/viz -S

# 4) Inspect Python test expectations
echo "== test expectations =="
fd -a "test_projection_layer.py" -S . | head -n 20
python - <<'PY'
import pathlib, re
paths=[]
for p in pathlib.Path('.').rglob('test_projection_layer.py'):
    paths.append(p)
print("found:", [str(x) for x in paths])
for p in paths:
    txt=p.read_text(encoding='utf-8')
    # Print around the referenced lines 110-220 if possible
    lines=txt.splitlines()
    for i in [110,120,130,140,160,170,190,200,210,220]:
        if 1<=i<=len(lines):
            pass
    start=100
    end=min(240,len(lines))
    print("\n----",p,"lines",start,"-",end,"----")
    for idx in range(start-1,end):
        if idx<0: continue
        print(f"{idx+1:4d}: {lines[idx]}")
PY

Repository: NVIDIA/IsaacTeleop

Length of output: 6289


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== validate/submit exception sites (projection_layer) =="
rg -n "ProjectionLayer::validate_submit_buffer|throw std::invalid_argument|invalid_argument" src/viz/layers/cpp/projection_layer.cpp -n -S

echo
echo "== Python bindings: exception translator hooks in src/viz/python =="
rg -n "register_exception|translate_exception|exception_translator|PyErr_SetString" src/viz/python -S || true

echo
echo "== Python bindings: broader pybind11 exception translator hooks under src/viz =="
rg -n "register_exception|translate_exception|exception_translator|PyErr_SetString" src/viz -S || true

echo
echo "== Locate test_projection_layer.py and inspect expected exception types =="
fd -a "test_projection_layer.py" -S . || true
for p in $(fd -a "test_projection_layer.py" -S . 2>/dev/null); do
  echo "---- $p ----"
  # print a window around the previously referenced lines 100-230
  sed -n '90,240p' "$p" | cat
done

echo
echo "== Locate C++ tests (if any) for submit validation =="
fd -a "test_projection_layer.*" -S src/viz | head -n 50 || true
for p in $(fd -a "test_projection_layer.*" -S src/viz 2>/dev/null); do
  echo "---- $p ----"
  rg -n "invalid_argument|runtime_error|EXPECT_THROW|throw" "$p" -S || true
done

Repository: NVIDIA/IsaacTeleop

Length of output: 2245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate python tests referencing ProjectionLayer.submit =="
rg -n "test_projection_layer\.py|ProjectionLayer\.submit|submit\(left_color" -S . || true

echo
echo "== Search for RuntimeError/TypeError expectations around submit =="
rg -n "pytest\.raises\((RuntimeError|TypeError)|RuntimeError\)|TypeError\)" -S . || true

echo
echo "== Search for any pybind11 exception translator customizations =="
rg -n "register_exception|translate_exception|exception_translator|PyErr_SetString|py::register_exception|pybind11::register_exception|throw_exception|PYBIND11_MODULE" src/viz/python -S || true
rg -n "register_exception|translate_exception|exception_translator|PyErr_SetString|py::register_exception|PYBIND11_MODULE" src/viz -S || true

echo
echo "== Inspect python module init for exception translators (if any) =="
rg -n "PYBIND11_MODULE" -S src/viz/python src/viz || true

echo
echo "== Locate validate_submit_buffer call sites in bindings (to connect to exceptions) =="
rg -n "ProjectionLayer.*submit|def\(\s*\"submit\"|layers_bindings\.cpp" -S src/viz/python || true

Repository: NVIDIA/IsaacTeleop

Length of output: 10212


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== layers_bindings.cpp: submit binding (look for try/catch/translation) =="
p="src/viz/python/layers_bindings.cpp"
if [ -f "$p" ]; then
  sed -n '150,270p' "$p" | cat
else
  echo "missing $p"
fi

echo
echo "== Search src/viz/python for any invalid_argument handling =="
rg -n "invalid_argument" src/viz/python -S || true

echo
echo "== Search for pybind11 translator APIs specifically =="
rg -n "register_exception_translator|register_exception|translate_exception|exception_translator" src/viz/python -S || true
rg -n "py::.*register_exception|py::.*translate" src/viz/python -S || true

echo
echo "== Inspect top-level binding init for potential exception translation =="
for f in src/viz/python/viz_bindings.cpp src/viz/python/session_bindings.cpp src/viz/python/bindings_helpers.hpp; do
  echo "---- $f ----"
  if [ -f "$f" ]; then
    sed -n '1,120p' "$f" | cat
    sed -n '120,240p' "$f" | cat
  else
    echo "missing $f"
  fi
done

Repository: NVIDIA/IsaacTeleop

Length of output: 25270


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Show test_projection_layer.py around lines 110-220 =="
p="src/viz/python_tests/test_projection_layer.py"
if [ -f "$p" ]; then
  sed -n '100,240p' "$p" | cat
else
  echo "missing $p"
  ls -R src/viz/python_tests | head -n 200
fi

echo
echo "== Search for any py::register_exception_translator usage (anywhere) =="
rg -n "py::register_exception_translator|register_exception_translator" src/viz -S || true

echo
echo "== Search for custom exception translator lambdas by type (broader) =="
rg -n "translate_exception|exception_translator|register_exception" src/viz/python -S || true

echo
echo "== Search for std::invalid_argument handling in bindings =="
rg -n "catch \\(const std::invalid_argument|invalid_argument\\)" src/viz/python -S || true

Repository: NVIDIA/IsaacTeleop

Length of output: 4991


🌐 Web query:

pybind11 default exception translation std::invalid_argument maps to which Python exception type

💡 Result:

In pybind11, the C++ standard exception std::invalid_argument is automatically translated to the Python ValueError exception type [1][2]. This translation is handled by pybind11's default exception converter, which maps several common C++ standard exceptions to their corresponding built-in Python exceptions [1][3]. While these are default mappings, developers can override this behavior or extend translation for other types by using py::register_exception_translator [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

f="src/viz/layers/cpp/projection_layer.cpp"
echo "== projection_layer.cpp: locate submit + catches =="
rg -n "ProjectionLayer::submit|try \\{|catch \\(" "$f" -S

echo
echo "== projection_layer.cpp: show submit region =="
# show a window around the known validation call sites (~480-560) plus submit definition
sed -n '430,560p' "$f" | cat

echo
echo "== projection_layer.cpp: show any catch blocks near submit/validate =="
rg -n "catch \\(" "$f" -S

Repository: NVIDIA/IsaacTeleop

Length of output: 4898


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "py::register_exception_translator|register_exception_translator|exception_translator|translate_exception|PYBIND11_MODULE" . -S || true

Repository: NVIDIA/IsaacTeleop

Length of output: 1412


🌐 Web query:

pybind11 std::invalid_argument maps to RuntimeError vs ValueError

💡 Result:

pybind11 maps C++ std::invalid_argument to the Python ValueError by default (not RuntimeError). [1] Note: pybind11’s default exception translation table explicitly lists “std::invalid_argument -> ValueError”. [1][2] If you register a custom exception translator, you can override this mapping for exceptions you catch and rethrow, and which translator “wins” depends on module import/registration order. [1][2]

Citations:


Fix exception type mismatch for ProjectionLayer.submit validation errors

src/viz/layers/cpp/projection_layer.cpp throws std::invalid_argument for submit/validation failures (e.g., missing depth, resolution mismatch, missing right buffers). The Python binding in src/viz/python/layers_bindings.cpp does not catch these (it only throws std::runtime_error for the binding-side left_color is required case), so pybind11’s default translation maps std::invalid_argument → Python ValueError, not RuntimeError.

src/viz/python_tests/test_projection_layer.py expects RuntimeError for these failures (lines 126, 143, 173, 205), so the exception type expectations/API contract are inconsistent.

Fix by either (a) updating the tests to expect ValueError, or (b) catching std::invalid_argument in the submit binding wrapper and rethrowing std::runtime_error (or registering an explicit pybind11 translator).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/viz/python/layers_bindings.cpp` around lines 184 - 234, The binding for
ProjectionLayer.submit in layers_bindings.cpp currently throws
std::runtime_error only for the left_color nil case but lets
std::invalid_argument from viz::ProjectionLayer::submit propagate (which becomes
Python ValueError), causing test mismatch; wrap the call to self.submit(...) in
a try/catch that catches std::invalid_argument (thrown by the C++
submit/validation in src/viz/layers/cpp/projection_layer.cpp) and rethrow as
std::runtime_error so pybind11 converts it to Python RuntimeError; update the
catch to include the original exception message when constructing the
std::runtime_error to preserve error details.

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