diff --git a/include/mocktail/vr/openxr_backend.h b/include/mocktail/vr/openxr_backend.h index ba4302f..88e6cd7 100644 --- a/include/mocktail/vr/openxr_backend.h +++ b/include/mocktail/vr/openxr_backend.h @@ -161,6 +161,7 @@ class OpenXrBackend final { // hands[0]=left, hands[1]=right carry this frame's tracked controller grip // poses (metres, LOCAL space) when a controller layer is active. ScriptedPoseSample PublishedHeadPose() const; + bool RecommendedEyeExtent(std::uint32_t* width, std::uint32_t* height) const; void NotePoseApplied(void* owner, std::uint64_t frame); // Controller input delivery, consumed by the window-thread input runtime @@ -289,6 +290,8 @@ class OpenXrBackend final { void* xr_session_ = nullptr; void* xr_local_space_ = nullptr; void* xr_view_space_ = nullptr; + std::uint32_t xr_eye_width_[2] = {}; + std::uint32_t xr_eye_height_[2] = {}; EyeSwapchain eye_swapchains_[2]; int xr_blend_mode_ = 0; // Runtime name captured at xrCreateInstance so the bind-time diagnostics line diff --git a/src/vr/gles_transport.cc b/src/vr/gles_transport.cc index f083c51..4ed92e4 100644 --- a/src/vr/gles_transport.cc +++ b/src/vr/gles_transport.cc @@ -309,9 +309,8 @@ void GlesTransport::Bind(unsigned target, unsigned framebuffer, if (it == storage.end()) return; const auto &size = it->second; - // DebugDeviceVR currently creates single-sample 500x500 RGBA eyes. // Reject unsupported formats/extents instead of copying unrelated pixels. - if (size.width != 500 || size.height != 500 || size.samples != 0 || + if (size.samples != 0 || (size.format != GL_RGBA8 && size.format != GL_RGBA && size.format != GL_SRGB8_ALPHA8)) return; diff --git a/src/vr/openxr_backend.cc b/src/vr/openxr_backend.cc index 0b21d72..66a67b5 100644 --- a/src/vr/openxr_backend.cc +++ b/src/vr/openxr_backend.cc @@ -878,6 +878,42 @@ bool OpenXrBackend::InitializeSessionResourcesLocked(std::string *error) { xr_blend_mode_ = static_cast(blends[index]); } + std::uint32_t view_count = 0; + const XrInstance instance = static_cast(xr_instance_); + CheckXr(instance, + xrEnumerateViewConfigurationViews( + instance, static_cast(xr_system_), + XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 0, &view_count, + nullptr), + "xrEnumerateViewConfigurationViews count"); + Require(view_count >= 2, + "OpenXR primary stereo configuration exposes fewer than two views"); + + std::vector config_views(view_count); + for (auto& view : config_views) { + view = XrInfo(XR_TYPE_VIEW_CONFIGURATION_VIEW); + } + + CheckXr(instance, + xrEnumerateViewConfigurationViews( + instance, static_cast(xr_system_), + XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, view_count, + &view_count, config_views.data()), + "xrEnumerateViewConfigurationViews"); + + xr_eye_width_[0] = config_views[0].recommendedImageRectWidth; + xr_eye_height_[0] = config_views[0].recommendedImageRectHeight; + xr_eye_width_[1] = config_views[1].recommendedImageRectWidth; + xr_eye_height_[1] = config_views[1].recommendedImageRectHeight; + + Require(xr_eye_width_[0] != 0 && xr_eye_height_[0] != 0 && + xr_eye_width_[1] != 0 && xr_eye_height_[1] != 0, + "OpenXR returned zero recommended stereo eye extent"); + + Log(" [vr-backend] OpenXR recommended eye extents L=%ux%u R=%ux%u\n", + xr_eye_width_[0], xr_eye_height_[0], xr_eye_width_[1], + xr_eye_height_[1]); + if (!CreateSwapchains(error)) { TeardownSessionLocked("swapchain failure", session_recovery_pending_); return false; @@ -936,11 +972,10 @@ bool OpenXrBackend::CreateSwapchains(std::string* error) { Require(chosen != VK_FORMAT_UNDEFINED, "the runtime exposes no 8-bit RGBA swapchain format"); - // Default eye extent until the guest eye images are bound; the simulator - // runtime advertises 500x500-capable limits, and DebugDeviceVR renders - // 500x500. Re-created lazily if the recorded eye extent differs. - const std::uint32_t width = 500; - const std::uint32_t height = 500; + const std::uint32_t width = + std::max(xr_eye_width_[0], xr_eye_width_[1]); + const std::uint32_t height = + std::max(xr_eye_height_[0], xr_eye_height_[1]); for (int eye = 0; eye < 2; ++eye) { auto create_info = XrInfo(XR_TYPE_SWAPCHAIN_CREATE_INFO); @@ -1354,6 +1389,22 @@ ScriptedPoseSample OpenXrBackend::PublishedHeadPose() const { return published_pose_; } +bool OpenXrBackend::RecommendedEyeExtent(std::uint32_t* width, + std::uint32_t* height) const { + if (width == nullptr || height == nullptr) { + return false; + } + std::lock_guard lock(mutex_); + if (xr_instance_ == nullptr || xr_system_ == 0 || + xr_eye_width_[0] == 0 || xr_eye_height_[0] == 0 || + xr_eye_width_[1] == 0 || xr_eye_height_[1] == 0) { + return false; + } + *width = std::max(xr_eye_width_[0], xr_eye_width_[1]); + *height = std::max(xr_eye_height_[0], xr_eye_height_[1]); + return true; +} + bool OpenXrBackend::TakeControllerDelivery(ControllerDelivery* out) { if (out == nullptr) { return false; diff --git a/src/vr/roblox_vr_device_bridge.cc b/src/vr/roblox_vr_device_bridge.cc index 5a58cde..06e4471 100644 --- a/src/vr/roblox_vr_device_bridge.cc +++ b/src/vr/roblox_vr_device_bridge.cc @@ -1067,6 +1067,18 @@ void RobloxVrDeviceBridge::TryInitializeEyes(void *self) { const auto initializer = reinterpret_cast( image_base_.load(std::memory_order_acquire) + profile_.eye_initializer_rva); + + if (auto* backend = ActiveVrBackend()) { + std::uint32_t width = 0; + std::uint32_t height = 0; + if (backend->RecommendedEyeExtent(&width, &height)) { + *reinterpret_cast(bytes + kWidthOffset) = width; + *reinterpret_cast(bytes + kHeightOffset) = height; + Log(" [vr-device][evidence] using OpenXR recommended eye extent %ux%u\n", + width, height); + } + } + Log(" [vr-device][evidence] eye-init-begin self=%p device=%p tid=%" PRIu64 " presents=%" PRIu64 "\n", self, device, CurrentThreadId(),