Skip to content

[TeleViz] Add XR session and xr compositing utils#471

Open
farbod-nv wants to merge 26 commits intomainfrom
fm/televiz_m5
Open

[TeleViz] Add XR session and xr compositing utils#471
farbod-nv wants to merge 26 commits intomainfrom
fm/televiz_m5

Conversation

@farbod-nv
Copy link
Copy Markdown
Contributor

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

Adds XR session, depth submission, XR compositing
And utils for head_pose, xr timing and etc.
Also adds an example for viz_xr smoke test

Summary by CodeRabbit

Release Notes

  • New Features
    • Added OpenXR support for developing and running VR/XR applications
    • Integrated GPU timing metrics for frame rendering performance profiling
    • New XR demonstration example showcasing stereo 3D quad rendering
    • Enhanced layer placement control for positioned virtual objects in XR space

farbod-nv and others added 5 commits May 6, 2026 10:06
Scaffolding for the M5 OpenXR backend. No graphics-bound XR session
yet — that lands in phase 2 with Vulkan device negotiation via
XR_KHR_VULKAN_ENABLE2 and the XrBackend (DisplayBackend) impl.

Adds:
- BUILD_VIZ_XR root CMake option (gated on BUILD_VIZ).
- src/viz/xr/ module with viz_xr static lib. Reuses the project-level
  OpenXR FetchContent (deps/third_party). Exposes viz::xr alias.
- xr_runtime.hpp: enumerate_openxr_instance_extensions() +
  openxr_loader_available() — queryable without an XrInstance, no
  runtime contact required.
- src/viz/xr_tests/ with unit-tagged Catch2 tests verifying the
  loader links and advertises XR_KHR_vulkan_enable2 (the foundation
  for phase 2 — if it's not there the milestone is dead on arrival).

Both tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OpenXR is foundational to IsaacTeleop (loader fetched at the project
root in deps/third_party for everyone), so gating viz_xr behind a
separate flag bought nothing — there's no scenario where you'd want
viz_session but opt out of viz_xr. Build-time linkage is fine without
a runtime; the graphics-bound XR session only contacts the runtime
when actually used.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wraps xrCreateInstance + xrGetSystem(HMD). Always enables
XR_KHR_VULKAN_ENABLE2; callers add their own extensions via the
constructor's extra_extensions list. Required input for the
XR-bound Vulkan device negotiation and for XrSession creation.

[xr]-tagged test exercises construction; SKIP if no runtime / HMD.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
VkContext::Config gains xr_instance + xr_system_id. When set, init()
uses xrGetVulkanGraphicsRequirements2KHR, xrCreateVulkanInstanceKHR,
xrGetVulkanGraphicsDevice2KHR, and xrCreateVulkanDeviceKHR instead of
direct Vulkan creation; physical_device_index is ignored — OpenXR
picks the device. Standalone path is untouched.

viz_core PUBLIC-links openxr_loader (project-wide foundational); the
header includes <openxr/openxr.h> so callers can use XR_NULL_HANDLE
without ceremony.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 74752066-c4ef-4083-aa9b-a3895a1fe0b8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fm/televiz_m5

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

farbod-nv and others added 12 commits May 6, 2026 14:36
Wraps xrCreateSession with XrGraphicsBindingVulkan2KHR, drives the
session state machine via poll_events (auto xrBeginSession on READY,
xrEndSession on STOPPING), and exposes wait_frame/begin_frame/
locate_views/end_frame for the renderer.

Construction enumerates view configuration (PRIMARY_STEREO; rejects
runtimes that report zero views) and creates a LOCAL reference space
by default (seated teleop dashboards; STAGE/room-scale opt-in via
Config). Default Config is exposed via a no-arg overload rather than
`= Config{}` default arg — the latter requires Config's nested member
initializers to be visible at the constructor declaration site, which
they aren't yet inside the enclosing class.

[xr]-tagged Catch2 test asserts construction against a real runtime
(2 views, non-zero recommended dims). SKIP cleanly without runtime.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ereo)

XrBackend final : DisplayBackend. Owns OpenXrInstance + OpenXrSession +
N XrSwapchains (one per view, typically 2 for stereo) + one intermediate
RenderTarget. viz_session links viz::xr to use OpenXrInstance / OpenXrSession.

Two-phase init: ctor creates the OpenXrInstance up front so VkContext can
take its XR-bound init path (xrCreateVulkanInstanceKHR/DeviceKHR);
init() creates the session, swapchains, and intermediate RT after
VkContext is ready. VizSession orchestrates the order — the new public
xr_instance_handle() / xr_system_id() expose what VkContext::Config needs.

Phase 1 = monoscopic-into-stereo: compositor renders ONE image at view 0's
recommended resolution; record_post_render_pass blits it into BOTH eyes'
swapchain images; end_frame builds two ProjectionViews (per-eye pose +
fov from xrLocateViews, each subImage referencing its own swapchain) and
submits via xrEndFrame. True per-eye rendering lands in Phase 2 with
ProjectionLayer; the wide-intermediate vs per-view-RT decision is
deferred to that point.

Per-view swapchain state lives as a private nested struct (handle +
runtime-owned VkImages + current acquired index). No separate Swapchain
class — see M5 design discussion: this state is tightly coupled to
XrBackend's frame loop, no independent lifetime, no CUDA interop on
the swapchain (that's upstream in DeviceImage / QuadLayer).

abort_frame still calls xrEndFrame with empty layers to keep the
xrBegin/xrEnd protocol balanced if compositor's submit unwinds.

oxr_handles() exposes XrInstance / XrSession / XrSpace / loader-level
xrGetInstanceProcAddr for app-side TeleopSession sharing.

VizSession kXR wiring + smoke example land in follow-up commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
VizSession::create now accepts DisplayMode::kXr and constructs an
XrBackend with config_.app_name + config_.required_extensions
(maps to XrBackend's extra_xr_extensions). The two-phase init pulls
the raw XrInstance / XrSystemId from the backend immediately after
construction and threads them into VkContext::Config so VkContext
takes its xrCreateVulkanInstanceKHR / xrCreateVulkanDeviceKHR path.

New get_oxr_handles() returns std::optional<core::OpenXRSessionHandles>
populated from XrBackend's oxr_handles() — XrInstance, XrSession,
reference space, loader-level xrGetInstanceProcAddr. Returns nullopt
outside kXr or before the session is established. App-side
TeleopSession can now share the same OpenXR session via the existing
OpenXRSessionHandles plumbing in src/core/oxr_utils.

viz_session links oxr::oxr_utils for the handle struct and viz::xr
for the XrBackend impl.

xr_smoke example follows in the next commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Minimal example exercising VizSession in DisplayMode::kXr. Pushes a
single 1024x1024 QuadLayer fed by a CUDA producer that animates a
gradient with a moving stripe; renders mono content that the runtime
duplicates across both eyes via the two ProjectionViews built by
XrBackend::end_frame.

Bails out cleanly with EXIT_SUCCESS + a friendly message when no
OpenXR runtime / HMD is reachable, so this is safe to run on any
dev machine. Verified locally without a runtime — exits 0 after the
loader's "no runtime" error from xrCreateInstance.

Pattern follows examples/televiz/window_smoke (CudaDeviceBuffer RAII,
host->device cudaMemcpy, mailbox submit), with extras for kXr:
  - SIGINT/SIGTERM handlers so Ctrl-C exits the render loop
  - frame counter for animation phase
  - host pattern refilled every 4 frames (mailbox holds latest publish)

On a machine with a runtime + HMD attached, it should render the
animated quad in both eyes and report fps every 60 frames.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…CTOR_UNAVAILABLE

CloudXR / streaming runtimes start up before any headset client has
connected, so xrCreateInstance succeeds but xrGetSystem returns
XR_ERROR_FORM_FACTOR_UNAVAILABLE (-35) until the client joins. The
OpenXR-blessed pattern is to poll xrGetSystem; this commit exposes
that as a system_wait_seconds parameter.

OpenXrInstance gains a third (default 0) constructor arg that loops
xrGetSystem with 200ms sleeps until success, the deadline expires, or
a non-FORM_FACTOR_UNAVAILABLE error fires. Logs "waiting for HMD..."
to stderr every 3s during the wait so users know the app isn't hung.

XrBackend::Config::system_wait_seconds and VizSession::Config::
xr_system_wait_seconds thread the value through. Default stays 0
(fail-fast) so existing tests / no-runtime paths behave unchanged.

xr_smoke sets the wait to 60s — start the binary first, then the
CloudXR client (`python -m isaacteleop.cloudxr` or similar) within
60s, and the smoke proceeds to render.

Repro of the original error:
  ./build/examples/televiz/xr_smoke/viz_xr_smoke
  → "OpenXrInstance: xrGetSystem failed: XrResult=-35"
After this commit:
  → "OpenXrInstance: waiting for HMD to connect (60s remaining)..."
  → ... (client connects) ...
  → "OpenXrInstance: HMD connected." → renders

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Negative system_wait_seconds now means "wait forever for an HMD";
useful when the runtime is up indefinitely and the user wants to
launch the binary first, then connect the CloudXR client at any
later time. Ctrl-C breaks the loop (the constructor's xrDestroyInstance
runs on the way out).

Convention:
  0  → fail fast (default; tests / CI)
  >0 → bounded wait, throws on deadline
  <0 → wait forever

Smoke example switches from 60s to -1 (wait forever).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Removes the per-frame CPU pixel fill + host->device copy from the
render thread. The mailbox holds the pattern across every render,
so fps numbers reflect runtime / GPU / xrWaitFrame pacing without
our CPU work as a confounder.

Confirmed by testing against CloudXR: same 40↔60 fps oscillation
as the animated version, ruling out our CPU contribution as the
source of jitter (it's CloudXR's streaming pipeline).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Moves backend.begin_frame from inside compositor.render() up to
viz_session.begin_frame(). For kXr that means xrWaitFrame (the
runtime's pacing primitive) now blocks at the API's stated start of
frame, matching holohub's XrBeginFrameOp / XrEndFrameOp split. The
fps behavior is unchanged — CloudXR still controls pacing — but the
restructure unlocks two things that matter going forward:

  1. FrameInfo.predicted_display_time becomes meaningful in kXr (was
     always 0). Apps can locate spaces / drive animations to display
     time between begin_frame and end_frame, hiding head-motion and
     input latency. Required for ProjectionLayer with depth, and for
     TeleopSession's tracker queries to align with the rendered frame.

  2. Apps can do their own per-frame work between begin and end with
     real timing info, not just the previous-frame's wall clock.

Mechanics:
  - DisplayBackend::Frame gains predicted_display_time (XrTime; 0 for
    non-XR backends).
  - VizCompositor::render(layers) becomes render(Frame, layers) — no
    longer fetches the frame internally. Its FrameGuard still calls
    backend.abort_frame on throw between record and end_frame.
  - VizSession caches the Frame between begin_frame and end_frame in
    a new cached_frame_ field. begin_frame populates FrameInfo.views
    and FrameInfo.predicted_display_time from the cached Frame.
  - VizSession::destroy() aborts any in-progress frame so the XR
    xrBegin/xrEnd protocol stays balanced if the app throws between
    begin and end (or destroys the session mid-frame).
  - render() simple API (begin + end) keeps working transparently.

Backend interfaces are otherwise unchanged. Window/Offscreen pacers
(GLFW frame_period sleep, none) move to the new begin_frame point
naturally — they were already inside backend.begin_frame.

52 unit tests stay green; xr_smoke and window_smoke unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
QuadLayer can now render as a textured rectangle in space at a
configurable pose + size, with per-eye MVP from xrLocateViews.
Foundation for camera_viz / examples/televiz/camera_streamer-replacement
where multiple camera planes need real 3D placement (parallax + depth
sort) rather than mono-into-stereo blits.

User-facing API:
  QuadLayer::Config::placement_pose       — Pose3D (default identity)
  QuadLayer::Config::placement_size_meters — vec2 (default {0,0} = legacy
                                              fullscreen-blit behavior)
  QuadLayer::set_pose / get_pose           — runtime updates
                                              (mutex'd, render-thread safe)

Behavior matrix:
  placement_size == (0,0)        → fullscreen pass (current behavior)
  placement_size \!= (0,0) + kXr  → 3D placed quad with per-eye MVP
  placement_size \!= (0,0) + win  → fullscreen anyway (ViewInfo::is_xr=false)

Implementation:
  ViewInfo gains is_xr — XrBackend sets true and populates view_matrix /
    projection_matrix from xrLocateViews; window/offscreen leave
    identity matrices and is_xr=false. QuadLayer's MVP path is gated
    on is_xr so non-XR backends ignore placement entirely.
  textured_quad.vert grows a push constant block (mat4 mvp + int mode)
    selecting between the legacy 3-vert oversized triangle (mode=0)
    and a 4-vert triangle strip in local [-0.5,0.5] (mode=1). One
    pipeline; topology = TRIANGLE_STRIP works for both vert counts.
  QuadLayer pipeline_layout adds a 68-byte push constant range.
  QuadLayer::record snapshots placement_pose under pose_mutex_ (small
    crit section), builds model = T * R * S(W,-H,1) (negative Y for
    Vulkan clip-space), MVP = proj * view * model, pushes + draws.
  XrBackend goes wide-intermediate (sum of view widths × max height)
    so each eye renders into its own region. begin_frame populates
    Frame.views with per-eye viewport offsets + view/projection
    matrices computed from xr_view.pose / xr_view.fov via helpers
    (pose_to_world_matrix, fov_to_projection_matrix). Conventions
    follow xr_plane_renderer (frustumRH_ZO with up/down passed as
    bottom/top to flip Y for Vulkan; near/far = 0.05/100 m).
  XrBackend::record_post_render_pass split-blits each per-view region
    of the wide intermediate to its eye's swapchain image (instead
    of duplicating the whole intermediate to both).

xr_smoke: configures placement_pose at 1.5 m forward, 1 m square. The
gradient now appears as a real plane in space — user can lean around
it, walk closer, etc. Same gradient as before; same fullscreen behavior
in window/offscreen modes (xr_smoke is XR-only so this only matters
for the principle).

window/offscreen behavior unchanged. 52 unit tests pass; format clean.

Placement strategies (head-locked / world-locked / lazy-locked) still
ship in examples/televiz/placements/ per the design memo — that's a
follow-up. This commit gives them something to drive (set_pose on a
QuadLayer with placement_size > 0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The compositor's tile_layout + per-layer scissor + view[0].viewport
override are correct for window/offscreen multi-layer letterboxing
but actively wrong for XR's per-eye layout:

  - tile_layout for a single 1:1 QuadLayer in a 2:1 wide intermediate
    produces a CENTERED square tile spanning the middle of both eyes'
    regions. Setting per-layer scissor to that tile clips both eye
    renders into that center band.
  - view[0].viewport gets overwritten with tile.content, destroying
    the left-eye region viewport that XrBackend::begin_frame populated.

Net effect (the bug the user hit on a real HMD): two ghost gradients
visible only in the right eye — view 0 rendering left-eye-perspective
content into the centered tile (clobbered viewport), view 1 rendering
right-eye content into right-eye region but scissor-clipped to the
center. Split blit then pulls overlapping garbage into each swapchain.

Fix: detect XR mode (frame->views[0].is_xr) and bypass both pieces of
logic. In XR mode set scissor = full RT and leave per-eye viewports
alone; per-eye viewports + clip-space culling do the spatial isolation.
Window/offscreen path unchanged.

52 unit tests pass; clang-format clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tests

CloudXR reprojection (XR_KHR_composition_layer_depth):
  - OpenXrInstance opt-in enables the extension when the runtime
    advertises it; xr_backend allocates per-eye depth swapchains, copies
    depth from the intermediate's per-eye region each frame, and chains
    XrCompositionLayerDepthInfoKHR into each ProjectionView. RenderTarget's
    depth attachment now stores into TRANSFER_SRC_OPTIMAL so the copy
    sees valid contents. QuadLayer's pipeline enables depth test/write
    (LESS_OR_EQUAL) so the placed quad actually writes depth.
  - Projection layer carries XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT
    when the env blend mode is non-OPAQUE; new VizSession::Config::
    xr_environment_blend_mode (wrapper enum, values 1:1 with
    XrEnvironmentBlendMode) plumbs through to OpenXrSession. xr_smoke
    enables ALPHA_BLEND + alpha=0 clear so passthrough shows around
    the placed quad.
  - xr_smoke gradient stripe is now animated (120-frame sweep) so
    headset-side smoothness is eyeballable.

VIEW reference space + head pose:
  - OpenXrSession creates a VIEW XrSpace alongside the LOCAL reference
    space. New view_space() accessor + locate_view_space(time, &out)
    method. XrBackend::OxrHandles carries view_space so apps can locate
    the head at arbitrary XrTimes.
  - DisplayBackend::Frame now carries Pose3D head_pose +
    head_pose_valid. XrBackend populates them in begin_frame via
    locate_view_space at predicted_display_time (non-fatal on tracking
    loss; head_pose_valid stays false in that case). Layers consume
    head_pose directly without doing their own xrLocateSpace.

Configurable near/far Z:
  - OpenXrSession::Config::near_z/far_z (defaults 0.05 / 100.0) feed
    BOTH the per-eye projection matrix and XrCompositionLayerDepthInfoKHR.
    Removes the file-scope kNearZ/kFarZ constants — they could no
    longer drift apart. New VizSession::Config::xr_near_z/xr_far_z
    plumbs through.

XR_KHR_convert_timespec_time:
  - OpenXrInstance opt-in enables when advertised, resolves
    xrConvertTimespecTimeToTimeKHR / xrConvertTimeToTimespecTimeKHR
    via xrGetInstanceProcAddr. Stored as type-erased PFN_xrVoidFunction
    so consumers don't need XR_USE_TIMESPEC. Methods xr_time_to_steady_clock
    / steady_clock_to_xr_time on the instance + forwarders on
    VizSession (kXr-only). Lets apps correlate XrTime values with
    sensor-side capture timestamps captured in steady_clock.

GPU timestamp infrastructure (VizCompositor::Config::gpu_timing):
  - Opt-in VkQueryPool with 4 timestamps per frame: cmd-buffer-begin,
    after-render-pass, after-record_post_render_pass, cmd-buffer-end.
    Read after the trailing fence wait, exposed via
    VizCompositor::last_gpu_timing() / VizSession::get_gpu_timing()
    as { total_ms, render_pass_ms, post_pass_ms }. Off by default;
    production builds don't pay for timestamp writes.

Tests:
  - test_perf_audit.cpp (new): two GPU/perf cases prove the timestamp
    infra populates sane values (sum-of-parts <= total, sanity
    ceilings) and a third uses global new/delete overrides to assert
    the steady-state render() hot path stays under a per-frame
    allocation ceiling — catches "someone added a vector to the hot
    path" regressions.
  - test_viz_session.cpp: defaults check now covers the new XR
    config fields; XrBlendMode wrapper-enum invariants against
    XrEnvironmentBlendMode (1, 2, 3) so the static_cast in
    make_backend can't silently desync from the OpenXR header.
  - test_openxr_instance.cpp: time-conversion round-trip (drift
    <= 1us, monotonic forward progress) when extension available,
    plus negative test that converters throw when extension is
    absent.
  - test_openxr_session.cpp: VIEW space exists from construction,
    near/far Z config round-trips, locate_view_space doesn't throw
    at any session state.

54 unit tests pass (was 52). 7 [xr] tests + 3 [gpu][perf] tests
discovered and skip cleanly without runtime/GPU. clang-format clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@farbod-nv farbod-nv changed the title Fm/televiz m5 [TeleViz] Add XR session and xr compositing utils May 7, 2026
@farbod-nv farbod-nv marked this pull request as ready for review May 7, 2026 18:16
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/viz/session_tests/cpp/test_viz_session.cpp (1)

83-90: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

This test asserts the pre-XR contract.

This PR also adds examples/televiz/xr_smoke/main.cpp, which creates a VizSession in DisplayMode::kXr. Keeping a unit test that expects VizSession::create(cfg_xr) to throw "not yet implemented" will either fail on XR-capable hosts or force the implementation back to the old behavior.

🤖 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/session_tests/cpp/test_viz_session.cpp` around lines 83 - 90, The
unit test TEST_CASE "VizSession::create rejects kXr (not yet implemented)" is
now outdated because examples/televiz/xr_smoke/main.cpp creates a VizSession
with DisplayMode::kXr; remove or update this test so it no longer asserts
VizSession::create(cfg_xr) throws. Locate the test in
src/viz/session_tests/cpp/test_viz_session.cpp (the TEST_CASE referencing
VizSession::create and DisplayMode::kXr) and either delete it or change it to
assert the new behavior (e.g., ensure mode validation happens without requiring
GPU or that create succeeds/returns the expected state) rather than expecting a
std::runtime_error.
🤖 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 `@examples/televiz/xr_smoke/main.cpp`:
- Around line 141-154: The current catch-all around viz::VizSession::create()
masks real failures; change the error handling to only treat known "no runtime /
no HMD" conditions as success and treat any other exception as a real failure:
replace the broad catch(const std::exception& e) with either (1) catching the
specific exception type(s) thrown for runtime-unavailable (if the XR wrapper
exposes one) and return EXIT_SUCCESS there, and add a second catch(const
std::exception& e) that logs the error from viz::VizSession::create and returns
EXIT_FAILURE, or (2) if only std::exception is available, inspect e.what() for
the explicit runtime/HMD-not-available message(s) and return EXIT_SUCCESS only
in those cases, otherwise log and return EXIT_FAILURE; reference
viz::VizSession::create and the catch block to locate where to implement this
branching.

In `@src/viz/core/cpp/inc/viz/core/vk_context.hpp`:
- Line 6: The public header currently exposes OpenXR via the include and by
using XrInstance/XrSystemId in viz::VkContext::Config; remove the direct
dependency by replacing those members with opaque types (e.g., uint64_t or void*
or a small XRHandle typedef) or by moving XR-specific fields into a non-public
viz::detail/XR wrapper; update VkContext::Config to use the opaque handle type
and remove `#include` <openxr/openxr.h> from the header, and perform
OpenXR-to-opaque translations inside the viz::xr boundary (e.g., where
XrInstance/XrSystemId are known) so the public API no longer exposes OpenXR
symbols.

In `@src/viz/core/cpp/render_target.cpp`:
- Around line 298-309: The depth attachment (attachments[1] using depth_format_)
is transitioned to TRANSFER_SRC for post-pass copy but deps[1] only syncs color;
update deps[1] to include fragment test stages and depth write visibility by
adding VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT to deps[1].srcStageMask and
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT to deps[1].srcAccessMask so depth
writes are properly visible before the transfer.

In `@src/viz/core/cpp/vk_context.cpp`:
- Around line 194-205: In init(), validate the XR configuration as an
all-or-nothing pair before branching: check config.xr_instance and
config.xr_system_id together (e.g., if one is set and the other is not), and
reject the partial XR config by returning an error/logging and aborting the XR
path instead of calling create_instance_xr, select_physical_device_xr, or
create_logical_device_xr; ensure the check is performed in init() prior to the
existing if (config.xr_instance != nullptr) branch so only fully-populated XR
configs proceed to the XR-specific functions.
- Around line 591-600: The code sets app.apiVersion = kApiVersion without
validating against the OpenXR-provided limits in reqs obtained via
xrGetVulkanGraphicsRequirements2KHR; update the logic after obtaining
XrGraphicsRequirementsVulkan2KHR reqs to verify that kApiVersion is within
[reqs.minApiVersionSupported, reqs.maxApiVersionSupported] and if not either
clamp app.apiVersion to that range or log/return an error before calling
xrCreateVulkanInstanceKHR; reference the symbols
XrGraphicsRequirementsVulkan2KHR reqs, app.apiVersion, kApiVersion,
xrGetVulkanGraphicsRequirements2KHR and xrCreateVulkanInstanceKHR when locating
where to add the check and handling.

In `@src/viz/session_tests/cpp/test_viz_session.cpp`:
- Around line 53-68: The test currently only calls SUCCEED() so it never
exercises the XR-only guard; update it to assert that the XR-only APIs throw in
non-XR mode by keeping cfg.mode = DisplayMode::kOffscreen, then call
has_xr_time_conversion on the (uninitialized) VizSession pointer to confirm it
returns false and invoke at least one of the conversion methods that are
documented to throw on non-kXr (the conversion methods declared to throw
std::logic_error) and assert it throws (e.g. REQUIRE_THROWS_AS or
CHECK_THROWS_AS expecting std::logic_error); reference VizSession,
has_xr_time_conversion and the conversion methods in your change so the test
fails if those methods stop rejecting non-kXr modes.

In `@src/viz/session/cpp/viz_session.cpp`:
- Around line 41-42: The cast from cfg.xr_environment_blend_mode to
XrEnvironmentBlendMode via static_cast is fragile; add a compile-time parity
check and replace the blind cast with an explicit mapping: introduce a helper
like mapEnvironmentBlendMode(cfg.xr_environment_blend_mode) or
ConvertToXrEnvironmentBlendMode that uses a switch over your wrapper enum and
returns the corresponding XrEnvironmentBlendMode (with a default case that
logs/handles unexpected values), and also add static_asserts that verify the
integral values of each wrapper enumerator equal the corresponding
XrEnvironmentBlendMode enumerator to catch divergence at compile time; update
the assignment to use the mapping helper instead of static_cast (target symbols:
xc.session_config.environment_blend_mode, cfg.xr_environment_blend_mode,
XrEnvironmentBlendMode).

In `@src/viz/session/cpp/xr_backend.cpp`:
- Around line 402-418: The loop acquiring swapchain images (lambda acquire_pair
used with view_swapchains_ and depth_swapchains_) is not exception-safe: if an
acquire or wait fails mid-loop, previously acquired images remain unreleased;
modify the logic to track which swapchains were successfully acquired (e.g., a
local vector of ViewSwapchain* or handles and indices) and on any check_xr
failure immediately iterate that list and call xrReleaseSwapchainImage for each
to undo the partial acquisitions, then propagate the error or return early;
alternatively set frame_renderable_ only after all acquires succeed as now but
ensure the cleanup path calls the same release logic as abort_frame(); update
acquire_pair (or its caller) to push successful acquisitions into the tracking
list and perform the cleanup on error so no acquired image is left unreleased.

In `@src/viz/xr_tests/cpp/test_openxr_instance.cpp`:
- Around line 15-24: The catch-all of std::runtime_error around constructing
viz::OpenXrInstance masks real failures; change the handler so it only SKIPs for
the known "no runtime / no HMD" conditions and re-throws or fails for any other
runtime_error. Concretely, in the try/catch blocks around viz::OpenXrInstance
(the constructors used in the blocks at the shown locations and the other
occurrences around lines 35-42 and 73-80), inspect e.what() for the expected
unavailability substrings (e.g. "no OpenXR runtime", "no HMD" or your library's
exact message) and call SKIP(...) only when matched; otherwise rethrow the
exception or let the test framework treat it as a failure. Ensure the behavior
is applied consistently to all try/catch sites that construct OpenXrInstance so
only genuine runtime-unavailable cases are skipped.

In `@src/viz/xr_tests/cpp/test_xr_runtime.cpp`:
- Around line 9-20: The two tests "OpenXR loader is linked and queryable" and
"OpenXR loader advertises XR_KHR_vulkan_enable2" should be re-tagged from [unit]
to [xr] and must early-skip when no loader is present; update the TEST_CASE tags
to "[xr][viz_xr]" and add a guard at the start of each test using
viz::openxr_loader_available() (call viz::openxr_loader_available() and
return/skip if false) so tests only run when the OpenXR loader/runtime is
available; reference the TEST_CASEs in src/viz/xr_tests/cpp/test_xr_runtime.cpp
and use the existing viz::enumerate_openxr_instance_extensions() in the second
test without changing its logic.

---

Outside diff comments:
In `@src/viz/session_tests/cpp/test_viz_session.cpp`:
- Around line 83-90: The unit test TEST_CASE "VizSession::create rejects kXr
(not yet implemented)" is now outdated because
examples/televiz/xr_smoke/main.cpp creates a VizSession with DisplayMode::kXr;
remove or update this test so it no longer asserts VizSession::create(cfg_xr)
throws. Locate the test in src/viz/session_tests/cpp/test_viz_session.cpp (the
TEST_CASE referencing VizSession::create and DisplayMode::kXr) and either delete
it or change it to assert the new behavior (e.g., ensure mode validation happens
without requiring GPU or that create succeeds/returns the expected state) rather
than expecting a std::runtime_error.
🪄 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: f5a4585e-eb29-4059-9e9b-f604e99bf346

📥 Commits

Reviewing files that changed from the base of the PR and between c330462 and 61bd909.

📒 Files selected for processing (36)
  • examples/televiz/CMakeLists.txt
  • examples/televiz/xr_smoke/CMakeLists.txt
  • examples/televiz/xr_smoke/main.cpp
  • src/viz/CMakeLists.txt
  • src/viz/core/cpp/CMakeLists.txt
  • src/viz/core/cpp/inc/viz/core/viz_types.hpp
  • src/viz/core/cpp/inc/viz/core/vk_context.hpp
  • src/viz/core/cpp/render_target.cpp
  • src/viz/core/cpp/vk_context.cpp
  • src/viz/layers/cpp/inc/viz/layers/quad_layer.hpp
  • src/viz/layers/cpp/quad_layer.cpp
  • src/viz/session/cpp/CMakeLists.txt
  • src/viz/session/cpp/inc/viz/session/display_backend.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/inc/viz/session/xr_backend.hpp
  • src/viz/session/cpp/viz_compositor.cpp
  • src/viz/session/cpp/viz_session.cpp
  • src/viz/session/cpp/xr_backend.cpp
  • src/viz/session_tests/cpp/CMakeLists.txt
  • src/viz/session_tests/cpp/test_perf_audit.cpp
  • src/viz/session_tests/cpp/test_viz_session.cpp
  • src/viz/shaders/cpp/textured_quad.vert
  • src/viz/xr/CMakeLists.txt
  • src/viz/xr/cpp/CMakeLists.txt
  • src/viz/xr/cpp/inc/viz/xr/openxr_instance.hpp
  • src/viz/xr/cpp/inc/viz/xr/openxr_session.hpp
  • src/viz/xr/cpp/inc/viz/xr/xr_runtime.hpp
  • src/viz/xr/cpp/openxr_instance.cpp
  • src/viz/xr/cpp/openxr_session.cpp
  • src/viz/xr/cpp/xr_runtime.cpp
  • src/viz/xr_tests/CMakeLists.txt
  • src/viz/xr_tests/cpp/CMakeLists.txt
  • src/viz/xr_tests/cpp/test_openxr_instance.cpp
  • src/viz/xr_tests/cpp/test_openxr_session.cpp
  • src/viz/xr_tests/cpp/test_xr_runtime.cpp

Comment thread examples/televiz/xr_smoke/main.cpp
Comment thread src/viz/core/cpp/inc/viz/core/vk_context.hpp
Comment thread src/viz/core/cpp/render_target.cpp
Comment thread src/viz/core/cpp/vk_context.cpp Outdated
Comment thread src/viz/core/cpp/vk_context.cpp
Comment thread src/viz/session_tests/cpp/test_viz_session.cpp Outdated
Comment thread src/viz/session/cpp/viz_session.cpp Outdated
Comment thread src/viz/session/cpp/xr_backend.cpp
Comment thread src/viz/xr_tests/cpp/test_openxr_instance.cpp Outdated
Comment thread src/viz/xr_tests/cpp/test_xr_runtime.cpp Outdated
Addresses review of the M5 closure commit:

High: xrBeginFrame is now protected by a local scope guard installed
immediately after begin_frame returns. Previously, any throw between
xrBeginFrame and the compositor's outer FrameGuard install (which
only happens once Frame is returned) leaked the OpenXR frame —
relevant throw points were locate_view_space and the color/depth
xrAcquireSwapchainImage / WaitSwapchainImage cascade. The guard
calls a shared abort_in_flight_frame() helper that releases acquired
swapchains and submits an empty xrEndFrame, then is dismissed at the
final return. Idempotent: clears frame_began_ BEFORE the runtime call
so a throw inside xrEndFrame can't recursively re-enter via the
destructor or another abort path.

High: partial swapchain acquisition is tracked per-swapchain. Added
`bool acquired` to ViewSwapchain. Set true after each successful
xrWaitSwapchainImage; cleared on release. abort_frame /
abort_in_flight_frame iterate both color and depth swapchains and
release ONLY those flagged — partial states (e.g. color eye 0/1
acquired, depth eye 0 throws) no longer leak. The previous
frame_renderable_ gate skipped release entirely in that case.

High/Medium: fullscreen QuadLayer mode now writes z = 1.0 (Vulkan far
plane in [0,1]) instead of z = 0. Telling CloudXR every pixel is at
the near plane was strictly worse than no depth — server-side
reprojection would have squashed the image to inches from the user's
face. Pinning depth at far makes reprojection a no-op for head-locked
content, which is the correct semantic. Pipeline depth compare is
LESS_OR_EQUAL so 1.0 ≤ 1.0 still passes against the cleared depth.

Medium: OpenXrSession::locate_view_space no longer throws on
XR_FAILED. The method's documented contract is "head pose is optional
/ non-fatal" and XrBackend calls it between xrBeginFrame and
xrEndFrame — a throw would unbalance the protocol unless a guard is
installed (which the high-1 fix now does anyway). Failure now
returns false consistently with locate_views.

Medium: test_xr_runtime cases retagged from [unit] to [xr] and SKIP
when the loader isn't reachable. Previously these failed as unit
tests on hosts without a configured OpenXR runtime, causing CI noise
on machines that legitimately lack one.

Build clean. 52 unit tests pass (was 54; 2 moved to [xr]). 7 [xr]
tests + 3 [gpu][perf] tests skip cleanly when their prerequisites
aren't available.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
farbod-nv and others added 5 commits May 7, 2026 15:23
Pure rename with no content change so the move shows as a 100%
similarity rename in every diff viewer. Content edits (back-pointer,
friend hook, accessor) follow in the next commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ptive blend mode

Architectural cleanup: LayerBase belongs in viz_session, not viz_layers.
LayerBase is the session's contract for what a layer is; concrete layer
impls (QuadLayer, ClearRectLayer, ThrowingLayer) are session consumers.
The previous grouping (LayerBase + impls in viz_layers, session in
viz_session, session depending on layers) made it impossible for layers
to hold a back-pointer to the session without a header cycle. Inverting
the dep direction (concrete layers depend on viz::session for the base)
removes that constraint with no cycle.

  - LayerBase now stores VizSession* (set by add_layer via friend hook),
    exposes session() accessor. Layers gating XR-specific behavior read
    session()->is_xr_mode() instead of a per-view ViewInfo flag.
  - VizSession::is_xr_mode() exposes the display mode.
  - DisplayBackend::is_xr() virtual lets the compositor read the mode
    without a session pointer; XrBackend overrides true.
  - Subdirectory order in src/viz/CMakeLists.txt re-sorted to follow the
    dep DAG (core → xr → shaders → session → layers).
  - viz_layers PUBLIC-links viz::session (was the other way round).
  - ViewInfo::is_xr removed (was the same value across all views in a
    frame — properly a session property, not a view property).

Runtime-adaptive env blend mode: OpenXrSession now picks the runtime's
first-advertised mode (xrEnumerateEnvironmentBlendModes preference
order), so the same binary works on passthrough Quest (ALPHA_BLEND),
pure-VR HMDs (OPAQUE), and optical see-through (ADDITIVE).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ssion

QuadLayer placement reshape: Config::placement_pose + placement_size_meters
collapse into std::optional<Config::Placement> { Pose3D pose; vec2 size_meters; }.
The optional encodes the fullscreen-vs-3D-placed distinction directly
instead of via a (0,0) sentinel; pose and size travel together because
they're meaningless apart. record() throws std::logic_error if XR mode
sees a nullopt placement (head-locked stereo quads are not a real use
case for camera teleop). Constructor validates size_meters > 0 in both
components when placement is set. set_pose / get_pose / placement_size_meters
accessors collapse into set_placement / placement(). Field name picked
to match XrCompositionLayerQuad::size convention (the unit-suffix carries
the world-space disambiguation; "extent" was over-correction).

End-frame state ordering: clear frame_began_ / frame_renderable_ BEFORE
the session_->end_frame call. If xrEndFrame throws (typically session
loss), the compositor's outer FrameGuard would otherwise fire abort_frame
on a session that just rejected the first attempt — doubles the noise
without helping. With flags cleared first, abort_in_flight_frame's
early-return on \!frame_began_ makes the unwind a no-op and the original
throw propagates cleanly.

xrBeginSession failure no longer silent: handle_session_state_change
now logs the XrResult and sets exit_requested_ on failure. Previously
session_running_ stayed false, wait_frame() returned false, the app
spun on nullopt frames forever with no diagnostic. Throwing from
poll_events would unbalance begin_frame's protocol guard, so we use
the existing exit-request channel — should_close() picks it up and
the app loop terminates cleanly.

50 unit tests pass. clang-format clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two MSVC failures on the Windows CI:

(1) openxr_platform.h's XR_USE_PLATFORM_WIN32 sections reference
LARGE_INTEGER (winnt.h) and IUnknown (unknwn.h). Those types aren't
in scope unless <Windows.h> + <Unknwn.h> are included first, and
unknwn.h is NOT pulled in by Windows.h when WIN32_LEAN_AND_MEAN is
set — which oxr_utils does, transitively, via its INTERFACE compile
definitions. viz_session links oxr::oxr_utils (for oxr_session_handles)
so xr_backend.cpp inherits XR_USE_PLATFORM_WIN32 + WIN32_LEAN_AND_MEAN
without the matching include guards.

Mirror the include order oxr_time.hpp uses: include <Unknwn.h> +
<Windows.h> before <openxr/openxr_platform.h>, gated on
XR_USE_PLATFORM_WIN32 so non-Windows builds are untouched. Comment
documents the chain so future contributors know why.

(2) Dead `(void)check_vk;` line tripped MSVC's C4551 ("function call
missing argument list"). check_vk was an unused helper — removed
both the function and the dead reference. -8 lines, no behavior
change.

50 unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
H1: order DiscardSwapchainImage cleanup so the per-swapchain `acquired`
flag governs the release on every error path through xrAcquireSwapchainImage
+ xrWaitSwapchainImage.

H2: route every <openxr/openxr_platform.h> include through a new
viz/core/openxr_platform_compat.hpp wrapper. The wrapper pulls in
<Unknwn.h> + <Windows.h> on Win32 so LARGE_INTEGER / IUnknown resolve
under WIN32_LEAN_AND_MEAN, and <ctime> when XR_USE_TIMESPEC is set.

H3: drop the dead local check_vk that was tripping MSVC C4551.

M2: DisplayBackend::Frame::head_pose is now std::optional<Pose3D>;
removes the parallel head_pose_valid bool.

M4: VizSession::head_pose_now() exposes a now-time view-space query,
and viz_xr_smoke gains --head-locked to re-anchor the QuadLayer to the
head each frame.

Comment audit across the M5 surface (xr_backend, openxr_session,
openxr_instance, viz_session, viz_compositor, vk_context, quad_layer,
display_backend, layer_base, xr_smoke, perf/session/xr tests, testing
helpers): keep the WHY, drop restated WHATs, no holoscan/holohub
references.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/viz/session_tests/cpp/test_viz_session.cpp (1)

72-78: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Test will fail when OpenXR runtime is available—add runtime detection or retag.

This [unit] test assumes the host has no OpenXR runtime and will break if one is installed (e.g., Monado for other tests). The codebase already has viz::openxr_loader_available() for detection (used throughout xr_tests/cpp/); use it here to skip gracefully when a runtime is present, or retag to [xr] and invert the test logic to REQUIRE_NOTHROW.

🤖 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/session_tests/cpp/test_viz_session.cpp` around lines 72 - 78, Test
assumes no OpenXR runtime; update the test around VizSession::create to detect
runtime availability using viz::openxr_loader_available() and either (a)
early-skip/return the test when a runtime is present so the existing
CHECK_THROWS_AS remains valid, or (b) retag the case from [unit] to [xr] and
invert the assertion to REQUIRE_NOTHROW when viz::openxr_loader_available() is
true; locate the test by the TEST_CASE name and the VizSession::create call to
implement the conditional behavior.
🤖 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/session/cpp/xr_backend.cpp`:
- Around line 120-129: Before tearing down, ensure any in-flight frame and
acquired swapchain images are cleanly ended/released: in XrBackend::destroy(),
if frame_began_ is true call xrEndFrame (with an empty XrFrameEndInfo / no
layers) using the current session handle and clear frame_began_ only after a
successful call; then iterate swapchains (the same structures
destroy_swapchains() would touch) and for any swapchain with sw.acquired set
call xrReleaseSwapchainImage on that swapchain handle and clear sw.acquired,
checking xrResult and logging errors but proceeding; only after balancing
xrEndFrame and releasing acquired images proceed to call destroy_swapchains(),
session_.reset(), and xr_instance_.reset() and set ctx_ = nullptr. Ensure you
reference and use the existing symbols frame_began_, destroy_swapchains(),
sw.acquired, xrEndFrame, and xrReleaseSwapchainImage when making the changes.

---

Outside diff comments:
In `@src/viz/session_tests/cpp/test_viz_session.cpp`:
- Around line 72-78: Test assumes no OpenXR runtime; update the test around
VizSession::create to detect runtime availability using
viz::openxr_loader_available() and either (a) early-skip/return the test when a
runtime is present so the existing CHECK_THROWS_AS remains valid, or (b) retag
the case from [unit] to [xr] and invert the assertion to REQUIRE_NOTHROW when
viz::openxr_loader_available() is true; locate the test by the TEST_CASE name
and the VizSession::create call to implement the conditional behavior.
🪄 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: 7d32c8ef-7b6e-4dea-a008-81ad35be46bf

📥 Commits

Reviewing files that changed from the base of the PR and between 61bd909 and e4ad4e6.

📒 Files selected for processing (32)
  • examples/televiz/xr_smoke/main.cpp
  • src/viz/CMakeLists.txt
  • src/viz/core/cpp/CMakeLists.txt
  • src/viz/core/cpp/inc/viz/core/openxr_platform_compat.hpp
  • src/viz/core/cpp/inc/viz/core/vk_context.hpp
  • src/viz/core/cpp/render_target.cpp
  • src/viz/core/cpp/vk_context.cpp
  • src/viz/layers/cpp/CMakeLists.txt
  • src/viz/layers/cpp/inc/viz/layers/layer_base.hpp
  • src/viz/layers/cpp/inc/viz/layers/quad_layer.hpp
  • src/viz/layers/cpp/quad_layer.cpp
  • src/viz/layers_tests/cpp/inc/viz/layers/testing/clear_rect_layer.hpp
  • src/viz/layers_tests/cpp/inc/viz/layers/testing/throwing_layer.hpp
  • src/viz/session/cpp/CMakeLists.txt
  • 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/inc/viz/session/xr_backend.hpp
  • src/viz/session/cpp/viz_compositor.cpp
  • src/viz/session/cpp/viz_session.cpp
  • src/viz/session/cpp/xr_backend.cpp
  • src/viz/session_tests/cpp/test_perf_audit.cpp
  • src/viz/session_tests/cpp/test_viz_session.cpp
  • src/viz/shaders/cpp/textured_quad.vert
  • src/viz/xr/cpp/inc/viz/xr/openxr_instance.hpp
  • src/viz/xr/cpp/inc/viz/xr/openxr_session.hpp
  • src/viz/xr/cpp/openxr_instance.cpp
  • src/viz/xr/cpp/openxr_session.cpp
  • src/viz/xr_tests/cpp/test_openxr_instance.cpp
  • src/viz/xr_tests/cpp/test_openxr_session.cpp
  • src/viz/xr_tests/cpp/test_xr_runtime.cpp
💤 Files with no reviewable changes (1)
  • src/viz/layers/cpp/inc/viz/layers/layer_base.hpp
✅ Files skipped from review due to trivial changes (3)
  • src/viz/layers_tests/cpp/inc/viz/layers/testing/throwing_layer.hpp
  • src/viz/core/cpp/inc/viz/core/openxr_platform_compat.hpp
  • src/viz/session_tests/cpp/test_perf_audit.cpp
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/viz/xr_tests/cpp/test_xr_runtime.cpp
  • src/viz/core/cpp/render_target.cpp
  • src/viz/session/cpp/CMakeLists.txt
  • src/viz/session/cpp/inc/viz/session/display_backend.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/xr/cpp/inc/viz/xr/openxr_instance.hpp
  • src/viz/xr/cpp/openxr_session.cpp

Comment thread src/viz/session/cpp/xr_backend.cpp
Comment thread src/viz/xr_tests/cpp/test_openxr_session.cpp
CI was failing on clang-format violations:
  - openxr_platform_compat.hpp: IndentPPDirectives: AfterHash wants
    `#    include` inside #if/#endif blocks.
  - vk_context.cpp / xr_backend.cpp / openxr_session.cpp: include
    block regrouping put openxr_platform_compat ahead of
    vulkan/vulkan.h in some TUs, which broke compile because
    XR_USE_GRAPHICS_API_VULKAN sections in openxr_platform.h need
    Vulkan types in scope. Made the wrapper self-contained by
    pulling vulkan.h in itself.
  - quad_layer.cpp: long throw line wrapped per ColumnLimit=120.

Also refactor QuadLayer::record():
  - Hoist the push-constant struct to anonymous namespace as
    QuadShaderData (matching the renamed shader block).
  - Extract MVP build into placement_mvp(Placement, ViewInfo) helper.
  - Collapse the per-view loop so vkCmdPushConstants + vkCmdDraw run
    once, with vertex_count (3 NDC-cover or 4 strip) as the only
    branched value.

Tighten XrBackend::OxrHandles doc-comment: it's the backend-internal
bundle, not what TeleopSession sees — VizSession::get_oxr_handles()
converts to core::OpenXRSessionHandles (drops view_space, renames
reference_space → space).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two cleanup wins lifted from src/core/oxr's OpenXRSession pattern:

1. Use std::unique_ptr<remove_pointer_t<XrFoo>, PFN_xrDestroyFoo> for
   every owned OpenXR handle (instance, session, reference_space,
   view_space). Destruction order falls out of member declaration order;
   no explicit destroy method needed. The ctor's partial-init rollback
   collapses to RAII unwinding — ~80 lines deleted across both classes.

2. Merge OpenXrInstance into OpenXrSession with two-stage construction:
     ctor              — xrCreateInstance + xrGetSystem + extension
                         probing (no graphics).
     attach_graphics() — xrCreateSession + reference / VIEW spaces +
                         view config + env blend mode.
   Two stages are required because session creation needs a VkContext
   that was XR-bound using instance() + system_id() — the same shape
   XrBackend was already orchestrating across two classes.

   The viz_xr unit tests still cover stage-1-only paths
   (extension probing, time conversion) without a Vulkan context.

XrBackend now owns a single OpenXrSession; viz_session.cpp's
has_xr_time_conversion / xr_time_to_steady_clock / steady_clock_to_xr_time
go through xr_session() instead of a separate xr_instance().

test_openxr_instance.cpp folded into test_openxr_session.cpp under
"stage 1" / "stage 2" sections.

Net: -91 lines, three files deleted (openxr_instance.{hpp,cpp},
test_openxr_instance.cpp).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If destroy() runs while a frame is in flight (mid-loop teardown,
exception during render), unbalanced xrBeginFrame and dangling acquired
swapchain images leak into xrDestroySession. abort_in_flight_frame() is
already noexcept and does the right thing — release acquired images,
clear frame_began_, post an empty xrEndFrame — so call it first.

No-op on the common path (nothing in flight at shutdown).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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