Render only the window in the Quest's immersive window

- The pacing thread aims each immersive window eye through the window itself (AimEyesThroughWindow):
  it keeps its position but looks square-on at the window's plane through an off-axis frustum just
  around it, so the eye image is the window, at the display's pixel density (about 680x380 per eye
  at render_scale 0.8 instead of 1344x1408), with a two-pixel border the mask leaves transparent.
  The frame's views carry that pose and field of view to the projection layer.
- vulkan_interop.cpp copies an eye smaller than its AHardwareBuffer into the buffer's corner, and
  the Quest layer's imageRect is the rendered part of the swapchain image.
- These eyes are not foveated: their field of view follows the head, which would rebuild the
  density map every frame.
- Quest only (kWindowShapedEyesSupported); the PC backends copy whole eyes and keep masking them.
  debug.wiicompiled.window_eyes 0 renders them whole and masked again for A/B timing.
- Quest 3, paused Retro Rewind race, render_scale 1.0: Immersive window runs the GPU at level 1
  (456 MHz, app GPU 11.0 ms, eyes 8.4 ms) where Immersive needs level 3 (599-640 MHz, 13.4 ms,
  10.3 ms), about 42% fewer GPU cycles. No edge artifacts, image as sharp. Docs: OPENXR.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
iChris4andClaude Opus 5.5 committed 2026-09-24 22:29:28 +02:00
1 parent 1cf9389d69
commit c990c595f0
6 files changed
+188 -23

No files matched your search

+3 -1
View File
@@ -841,7 +841,9 @@ gfx::StereoReplayFrame make_stereo_replay_frame(const AuroraStereoFrame& input,
.msaaSamples = webgpu::g_graphicsConfig.msaaSamples,
.depthFormat = owned.depth.format,
};
if (input.mode == AURORA_STEREO_FRAME_IMMERSIVE_REPLAY) {
// Not the immersive window's eyes: the host may aim them through the window, whose field of
// view then changes with every head movement and would rebuild the density map each frame.
if (input.mode == AURORA_STEREO_FRAME_IMMERSIVE_REPLAY && !input.window) {
view.target.foveatedColorView = foveated_eye_view(eye, input.eyes[eye]);
}
std::memcpy(&view.projection, input.eyes[eye].projection, sizeof(view.projection));
+6 -4
View File
@@ -255,11 +255,13 @@ public:
}
private:
// An eye may be smaller than its buffer (the immersive window's eyes are the window only): it is
// copied into the buffer's top-left corner, and the OpenXR side shows just that rectangle.
Import* EnsureImport(uint32_t eye, const stereo::EyeImage& source) noexcept {
const auto& target = m_targets[eye];
if (source.texture == nullptr || source.format != m_auroraFormat ||
source.size.width != target.width || source.size.height != target.height) {
Log.error("Stereo image {} does not match its OpenXR Vulkan target ({}x{} vs {}x{})", eye,
if (source.texture == nullptr || source.format != m_auroraFormat || source.size.width == 0 ||
source.size.height == 0 || source.size.width > target.width || source.size.height > target.height) {
Log.error("Stereo image {} does not fit its OpenXR Vulkan target ({}x{} in {}x{})", eye,
source.size.width, source.size.height, target.width, target.height);
return nullptr;
}
@@ -421,7 +423,7 @@ private:
.origin = {},
.aspect = wgpu::TextureAspect::All,
};
const wgpu::Extent3D extent{import.width, import.height, 1};
const wgpu::Extent3D extent{sources[eye].size.width, sources[eye].size.height, 1};
encoder.CopyTextureToTexture(&source, &destination, &extent);
m_encodedImports[eye] = imports[eye];
}