feat: added immersive window support for VR race views

- Introduced a new configuration option for immersive window mode in runtime_config.h.
- Updated the parsing and setting functions to handle the immersive window state.
- Modified the OpenXR backend to support rendering with the immersive window, blending the race view with the surrounding environment.
- Enhanced the settings overlay to allow users to select between immersive, immersive window, and flat screen race views.
- Implemented GPU rendering logic for the immersive window mask, ensuring correct visual output in various rendering paths.
- Added tests to validate the immersive window functionality and its interaction with existing race view settings.
This commit is contained in:
iChris4 committed 2026-09-24 21:28:17 +02:00
1 parent 1969dda0bf
commit 1cf9389d69
20 files changed
+917 -53

No files matched your search

+28 -7
View File
@@ -568,6 +568,10 @@ public:
passthrough_.store(enabled, std::memory_order_relaxed);
}
void SetImmersiveWindow(bool enabled) noexcept {
immersive_window_.store(enabled, std::memory_order_relaxed);
}
void SetLeanBackDegrees(float degrees) noexcept {
lean_back_degrees_.store(
std::clamp(degrees, -RuntimeConfigFile::kVrLeanBackDegreesLimit,
@@ -846,10 +850,15 @@ private:
aurora_get_stereo_screen_aspects(&picture_aspect, &snapshot_aspect)) {
presentation.quad_content_aspect = snapshot_aspect;
}
// The room around the menu screen and every other virtual screen, a
// Flat Screen race included; an immersive race is fully virtual, and the
// cameras are paused for it.
presentation.passthrough = !immersive && passthrough_.load(std::memory_order_relaxed);
// The immersive window: the race's stereo view seen through its 2D layer's screen.
// The flag travels with the packet, so the eyes Aurora masks and the layer the
// backend blends always belong to the same frame.
presentation.immersive_window = immersive && immersive_window_.load(std::memory_order_relaxed);
// The room around the menu screen and every other virtual screen, a Flat Screen
// race included, and around the immersive window; a fully immersive race is
// virtual all round, and the cameras are paused for it.
presentation.passthrough =
(!immersive || presentation.immersive_window) && passthrough_.load(std::memory_order_relaxed);
// The settings panel gets a compositor layer of its own while it is
// open, and Aurora leaves it out of the eyes. A backend that could
// not make that layer has the panel drawn into the eyes instead.
@@ -1247,6 +1256,7 @@ private:
destination.displayTimeNanos = DisplayTimeNanos(source.xr_frame.predicted_display_time);
destination.mode = immersive ? AURORA_STEREO_FRAME_IMMERSIVE_REPLAY
: AURORA_STEREO_FRAME_VIRTUAL_SCREEN;
destination.window = immersive && source.presentation.immersive_window;
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
destination.eyes[eye].width = source.render_width[eye];
destination.eyes[eye].height = source.render_height[eye];
@@ -1378,7 +1388,8 @@ private:
// that space (in metres) to base + lean * p in the application space, so the
// screen sits at base + lean * (0, 0, -distance), turned by the lean, its
// height following the picture aspect as stereo_hud_screen's does. With the
// 2D layer stretched across the eyes there is no screen to point at.
// 2D layer stretched across the eyes there is no screen to point at, except
// in the immersive window, which is that screen and always carries the layer.
OpenXRPointerScreen PointerScreen(const OpenXRBackendFrame& frame, const MkwVRPolicySnapshot& policy,
bool immersive) const noexcept {
OpenXRPointerScreen screen{};
@@ -1389,8 +1400,9 @@ private:
}
if (immersive) {
if (!aurora_get_stereo_hud_screen_enabled() || !(policy.config.hud_width_meters > 0.0f) ||
!RaceScreenPose(frame, policy, screen.pose)) {
// The immersive window always carries the 2D layer.
const bool on_screen = aurora_get_stereo_hud_screen_enabled() || frame.presentation.immersive_window;
if (!on_screen || !(policy.config.hud_width_meters > 0.0f) || !RaceScreenPose(frame, policy, screen.pose)) {
return screen;
}
screen.half_width_meters = 0.5f * policy.config.hud_width_meters;
@@ -1756,6 +1768,7 @@ private:
std::atomic_bool recenter_requested_{false};
std::atomic<float> lean_back_degrees_{RuntimeConfigFile::VrLeanBackDegrees()};
std::atomic_bool passthrough_{RuntimeConfigFile::VrPassthrough()};
std::atomic_bool immersive_window_{RuntimeConfigFile::VrImmersiveWindow()};
std::atomic_uint32_t frame_interpolation_fps_{RuntimeConfigFile::VrFrameInterpolationFps()};
std::atomic_bool interpolation_available_{false};
std::mutex interpolation_mutex_;
@@ -1877,6 +1890,14 @@ void OpenXRSetPassthrough(bool enabled) noexcept {
#endif
}
void OpenXRSetImmersiveWindow(bool enabled) noexcept {
#if MKW_OPENXR_GRAPHICS_BACKEND
OpenXRIntegration::Get().SetImmersiveWindow(enabled);
#else
(void)enabled;
#endif
}
void OpenXRSetFrameInterpolationFps(uint32_t target) noexcept {
#if MKW_OPENXR_GRAPHICS_BACKEND
OpenXRIntegration::Get().SetFrameInterpolationFps(target);
+10 -5
View File
@@ -942,7 +942,10 @@ public:
views[eye].subImage.imageArrayIndex = 0;
}
XrCompositionLayerProjection projection{XR_TYPE_COMPOSITION_LAYER_PROJECTION};
projection.layerFlags = 0;
// The immersive window's eyes are transparent outside the window (premultiplied alpha), so
// the room shows around it; otherwise the race covers the whole view and alpha is ignored.
projection.layerFlags =
frame.presentation.immersive_window ? XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT : 0;
projection.space = runtime_->AppSpace();
projection.viewCount = kOpenXREyeCount;
projection.views = views.data();
@@ -950,16 +953,18 @@ public:
}
// Ends the compositor frame with the scene's layer: over the room's camera
// view while that runs and the scene is the virtual screen (never under the
// race's projection, which covers the whole view), and, while the retained
// frame rendered it, under the settings panel's layer.
// view while that runs and the scene is the virtual screen or the immersive
// window (never under a fully immersive race's projection, which covers the
// whole view), and, while the retained frame rendered it, under the settings
// panel's layer.
bool EndFrameWithPanel(const OpenXRBackendFrame& frame, const XrCompositionLayerBaseHeader* scene) {
const auto& panel = frame.presentation.panel;
XrCompositionLayerQuad panel_quad{};
const XrCompositionLayerBaseHeader* layers[3] = {};
uint32_t count = 0;
if (const XrCompositionLayerBaseHeader* passthrough = passthrough_.Layer();
passthrough != nullptr && frame.presentation.mode == OpenXRFrameMode::VirtualScreen) {
passthrough != nullptr && (frame.presentation.mode == OpenXRFrameMode::VirtualScreen ||
frame.presentation.immersive_window)) {
layers[count++] = passthrough;
}
layers[count++] = scene;