From 37594e4f4954639e639e47311d5473b143394ad4 Mon Sep 17 00:00:00 2001 From: bjorn Date: Tue, 14 Oct 2025 19:00:37 -0700 Subject: [PATCH 1/4] Upgrade to OpenXR 1.0.53; --- deps/openxr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/openxr b/deps/openxr index 977f6675b..75c53b6e8 160000 --- a/deps/openxr +++ b/deps/openxr @@ -1 +1 @@ -Subproject commit 977f6675bc0057d5a54ed290cb5c71c699b1c0ab +Subproject commit 75c53b6e853dc12c7b3c771edc9c9c841b15faaa From afcbee7fd11f973341db1d2a7c269782ce97a610 Mon Sep 17 00:00:00 2001 From: bjorn Date: Tue, 14 Oct 2025 21:51:56 -0700 Subject: [PATCH 2/4] Support foveated inset and mono view configurations; --- src/modules/headset/headset.c | 144 ++++++++++++++++++++-------------- 1 file changed, 86 insertions(+), 58 deletions(-) diff --git a/src/modules/headset/headset.c b/src/modules/headset/headset.c index 49445e18d..7448e1c58 100644 --- a/src/modules/headset/headset.c +++ b/src/modules/headset/headset.c @@ -154,11 +154,12 @@ enum { }; enum { - STEREO = (1 << 0), - DEPTH = (1 << 1), - CUBE = (1 << 2), - STATIC = (1 << 3), - FOVEATED = (1 << 4) + VIEW = (1 << 0), + STEREO = (1 << 1), + DEPTH = (1 << 2), + CUBE = (1 << 3), + STATIC = (1 << 4), + FOVEATED = (1 << 5) }; typedef struct { @@ -249,6 +250,8 @@ static struct { Simulator simulator; XrInstance instance; XrSystemId system; + XrViewConfigurationType viewConfiguration; + uint32_t viewCount; XrSession session; XrSessionState sessionState; XrSpace referenceSpace; @@ -262,8 +265,8 @@ static struct { Pass* pass; Swapchain swapchains[3]; XrCompositionLayerProjection layer; - XrCompositionLayerProjectionView layerViews[2]; - XrCompositionLayerDepthInfoKHR depthInfo[2]; + XrCompositionLayerProjectionView layerViews[4]; + XrCompositionLayerDepthInfoKHR depthInfo[4]; XrCompositionLayerPassthroughFB passthroughLayer; union { XrCompositionLayerBaseHeader header; @@ -302,6 +305,7 @@ static struct { bool controllerModel; bool debug; bool depth; + bool foveatedInset; bool foveation; bool foveationConfig; bool foveationVulkan; @@ -490,6 +494,7 @@ bool lovrHeadsetConnect(void) { { "XR_ML_ml2_controller_interaction", &state.extensions.ml2Controller, true }, { "XR_MND_headless", &state.extensions.headless, true }, { "XR_ULTRALEAP_hand_tracking_forearm", &state.extensions.handTrackingElbow, true }, + { "XR_VARJO_quad_views", &state.extensions.foveatedInset, true }, { "XR_EXTX_overlay", &state.extensions.overlay, config->overlay }, { "XR_HTCX_vive_tracker_interaction", &state.extensions.viveTrackers, true } }; @@ -604,32 +609,57 @@ bool lovrHeadsetConnect(void) { // View Configuration + XrViewConfigurationType supportedViewConfigurations[] = { + XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET, + XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, + XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MONO + }; + uint32_t viewConfigurationCount; - XrViewConfigurationType viewConfigurations[2]; - XRG(xrEnumerateViewConfigurations(state.instance, state.system, 2, &viewConfigurationCount, viewConfigurations), "xrEnumerateViewConfigurations", fail); + XrViewConfigurationType viewConfigurations[4]; + XRG(xrEnumerateViewConfigurations(state.instance, state.system, 4, &viewConfigurationCount, viewConfigurations), "xrEnumerateViewConfigurations", fail); - uint32_t viewCount; - XrViewConfigurationView views[2] = { [0].type = XR_TYPE_VIEW_CONFIGURATION_VIEW, [1].type = XR_TYPE_VIEW_CONFIGURATION_VIEW }; - XRG(xrEnumerateViewConfigurationViews(state.instance, state.system, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 0, &viewCount, NULL), "xrEnumerateViewConfigurationViews", fail); - XRG(xrEnumerateViewConfigurationViews(state.instance, state.system, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 2, &viewCount, views), "xrEnumerateViewConfigurationViews", fail); + for (uint32_t i = 0; i < COUNTOF(supportedViewConfigurations); i++) { + for (uint32_t j = 0; j < viewConfigurationCount; j++) { + if (viewConfigurations[j] == supportedViewConfigurations[i]) { + state.viewConfiguration = supportedViewConfigurations[i]; + break; + } + } - if (viewCount != 2) { - return lovrSetError("Headset view count must be 2"); + lovrAssertGoto(fail, i + 1 < COUNTOF(supportedViewConfigurations), "No supported view configuration available"); } - uint32_t maxWidth = MIN(views[0].maxImageRectWidth, views[1].maxImageRectWidth); - uint32_t maxHeight = MIN(views[0].maxImageRectHeight, views[1].maxImageRectHeight); - uint32_t recommendedWidth = MIN(views[0].recommendedImageRectWidth, views[1].recommendedImageRectWidth); - uint32_t recommendedHeight = MIN(views[0].recommendedImageRectHeight, views[1].recommendedImageRectHeight); + XrViewConfigurationView views[4] = { + [0].type = XR_TYPE_VIEW_CONFIGURATION_VIEW, + [1].type = XR_TYPE_VIEW_CONFIGURATION_VIEW, + [2].type = XR_TYPE_VIEW_CONFIGURATION_VIEW, + [3].type = XR_TYPE_VIEW_CONFIGURATION_VIEW + }; + + XRG(xrEnumerateViewConfigurationViews(state.instance, state.system, state.viewConfiguration, 0, &state.viewCount, NULL), "xrEnumerateViewConfigurationViews", fail); + XRG(xrEnumerateViewConfigurationViews(state.instance, state.system, state.viewConfiguration, COUNTOF(views), &state.viewCount, views), "xrEnumerateViewConfigurationViews", fail); + + uint32_t maxWidth = ~0u; + uint32_t maxHeight = ~0u; + uint32_t recommendedWidth = 0; + uint32_t recommendedHeight = 0; + + for (uint32_t i = 0; i < state.viewCount; i++) { + maxWidth = MIN(maxWidth, views[i].maxImageRectWidth); + maxHeight = MIN(maxHeight, views[i].maxImageRectHeight); + recommendedWidth = MAX(recommendedWidth, views[i].recommendedImageRectWidth); + recommendedHeight = MAX(recommendedHeight, views[i].recommendedImageRectHeight); + } state.width = MIN(recommendedWidth * config->supersample, maxWidth); state.height = MIN(recommendedHeight * config->supersample, maxHeight); // Blend Modes - XRG(xrEnumerateEnvironmentBlendModes(state.instance, state.system, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 0, &state.blendModeCount, NULL), "xrEnumerateEnvironmentBlendModes", fail); + XRG(xrEnumerateEnvironmentBlendModes(state.instance, state.system, state.viewConfiguration, 0, &state.blendModeCount, NULL), "xrEnumerateEnvironmentBlendModes", fail); state.blendModes = lovrMalloc(state.blendModeCount * sizeof(XrEnvironmentBlendMode)); - XRG(xrEnumerateEnvironmentBlendModes(state.instance, state.system, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, state.blendModeCount, &state.blendModeCount, state.blendModes), "xrEnumerateEnvironmentBlendModes", fail); + XRG(xrEnumerateEnvironmentBlendModes(state.instance, state.system, state.viewConfiguration, state.blendModeCount, &state.blendModeCount, state.blendModes), "xrEnumerateEnvironmentBlendModes", fail); state.blendMode = state.blendModes[0]; // Actions @@ -1511,17 +1541,15 @@ bool lovrHeadsetStart(void) { } } - uint32_t flags = STEREO | (state.extensions.foveation ? FOVEATED : 0); - lovrAssertGoto(stop, supportsColor, "This VR runtime does not support sRGB rgba8 textures"); - if (!lovrSwapchainInit(&state.swapchains[SWAPCHAIN_COLOR], state.width, state.height, flags)) { + if (!lovrSwapchainInit(&state.swapchains[SWAPCHAIN_COLOR], state.width, state.height, VIEW | FOVEATED)) { goto stop; } GraphicsFeatures features; lovrGraphicsGetFeatures(&features); if (state.extensions.depth && supportsDepth && features.depthResolve) { - if (!lovrSwapchainInit(&state.swapchains[SWAPCHAIN_DEPTH], state.width, state.height, STEREO | DEPTH)) { + if (!lovrSwapchainInit(&state.swapchains[SWAPCHAIN_DEPTH], state.width, state.height, VIEW | DEPTH)) { goto stop; } } else { @@ -1531,23 +1559,20 @@ bool lovrHeadsetStart(void) { // Pre-init composition layer state.layer = (XrCompositionLayerProjection) { .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION, - .viewCount = 2, + .viewCount = state.viewCount, .views = state.layerViews }; // Pre-init composition layer views - state.layerViews[0] = (XrCompositionLayerProjectionView) { - .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW, - .subImage = { state.swapchains[SWAPCHAIN_COLOR].handle, { { 0, 0 }, { state.width, state.height } }, 0 } - }; - - state.layerViews[1] = (XrCompositionLayerProjectionView) { - .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW, - .subImage = { state.swapchains[SWAPCHAIN_COLOR].handle, { { 0, 0 }, { state.width, state.height } }, 1 } - }; + for (uint32_t i = 0; i < state.viewCount; i++) { + state.layerViews[i] = (XrCompositionLayerProjectionView) { + .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW, + .subImage = { state.swapchains[SWAPCHAIN_COLOR].handle, { { 0, 0 }, { state.width, state.height } }, i } + }; + } if (state.extensions.depth) { - for (uint32_t i = 0; i < 2; i++) { + for (uint32_t i = 0; i < state.viewCount; i++) { state.layerViews[i].next = &state.depthInfo[i]; state.depthInfo[i] = (XrCompositionLayerDepthInfoKHR) { .type = XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR, @@ -1713,7 +1738,7 @@ bool lovrHeadsetPollEvents(void) { case XR_SESSION_STATE_READY: XR(xrBeginSession(state.session, &(XrSessionBeginInfo) { .type = XR_TYPE_SESSION_BEGIN_INFO, - .primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO + .primaryViewConfigurationType = state.viewConfiguration }), "xrBeginSession"); break; @@ -2071,25 +2096,25 @@ bool lovrHeadsetIsPassthroughSupported(PassthroughMode mode) { return false; } -static XrViewStateFlags getViews(XrView views[2], uint32_t* count) { +static XrViewStateFlags getViews(XrView views[4], uint32_t* count) { if (state.frameState.predictedDisplayTime <= 0) { return 0; } XrViewLocateInfo viewLocateInfo = { .type = XR_TYPE_VIEW_LOCATE_INFO, - .viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, + .viewConfigurationType = state.viewConfiguration, .displayTime = state.frameState.predictedDisplayTime, .space = state.referenceSpace }; - for (uint32_t i = 0; i < 2; i++) { + for (uint32_t i = 0; i < 4; i++) { views[i].type = XR_TYPE_VIEW; views[i].next = NULL; } XrViewState viewState = { .type = XR_TYPE_VIEW_STATE }; - if (XR_FAILED(xrLocateViews(state.session, &viewLocateInfo, &viewState, 2, count, views))) { + if (XR_FAILED(xrLocateViews(state.session, &viewLocateInfo, &viewState, state.viewCount, count, views))) { return 0; } @@ -2097,7 +2122,7 @@ static XrViewStateFlags getViews(XrView views[2], uint32_t* count) { } uint32_t lovrHeadsetGetViewCount(void) { - return state.session ? 2 : 1; + return state.session ? state.viewCount : 1; } bool lovrHeadsetGetViewPose(uint32_t view, float* position, float* orientation) { @@ -2108,7 +2133,7 @@ bool lovrHeadsetGetViewPose(uint32_t view, float* position, float* orientation) } uint32_t count; - XrView views[2]; + XrView views[4]; XrViewStateFlags flags = getViews(views, &count); if (view >= count || !flags) { @@ -2144,7 +2169,7 @@ bool lovrHeadsetGetViewAngles(uint32_t view, float* left, float* right, float* u } uint32_t count; - XrView views[2]; + XrView views[4]; XrViewStateFlags flags = getViews(views, &count); if (view >= count || !flags) { @@ -3179,7 +3204,7 @@ bool lovrHeadsetGetPass(Pass** pass) { lovrPassSetClear(state.pass, loads, background, LOAD_CLEAR, 0.f); uint32_t count; - XrView views[2]; + XrView views[4]; XrViewStateFlags flags = getViews(views, &count); for (uint32_t i = 0; i < count; i++) { @@ -3284,12 +3309,14 @@ bool lovrHeadsetSubmit(void) { } if (state.extensions.depth) { - if (state.clipFar == 0.f) { - state.depthInfo[0].nearZ = state.depthInfo[1].nearZ = +INFINITY; - state.depthInfo[0].farZ = state.depthInfo[1].farZ = state.clipNear; - } else { - state.depthInfo[0].nearZ = state.depthInfo[1].nearZ = state.clipNear; - state.depthInfo[0].farZ = state.depthInfo[1].farZ = state.clipFar; + for (uint32_t i = 0; i < state.viewCount; i++) { + if (state.clipFar == 0.f) { + state.depthInfo[i].nearZ = +INFINITY; + state.depthInfo[i].farZ = state.clipNear; + } else { + state.depthInfo[i].nearZ = state.clipNear; + state.depthInfo[i].farZ = state.clipFar; + } } } @@ -3640,11 +3667,12 @@ uintptr_t lovrHeadsetGetSessionHandle(void) { // Helpers static bool lovrSwapchainInit(Swapchain* swapchain, uint32_t width, uint32_t height, uint32_t flags) { + bool view = flags & VIEW; bool stereo = flags & STEREO; bool depth = flags & DEPTH; bool cube = flags & CUBE; bool immutable = flags & STATIC; - bool foveated = flags & FOVEATED; + bool foveated = flags & FOVEATED && state.extensions.foveation; XrSwapchainCreateInfo info = { .type = XR_TYPE_SWAPCHAIN_CREATE_INFO, @@ -3653,7 +3681,7 @@ static bool lovrSwapchainInit(Swapchain* swapchain, uint32_t width, uint32_t hei .height = height, .sampleCount = 1, .faceCount = cube ? 6 : 1, - .arraySize = 1 << stereo, + .arraySize = view ? state.viewCount : 1 << stereo, .mipCount = 1 }; @@ -3711,7 +3739,7 @@ static bool lovrSwapchainInit(Swapchain* swapchain, uint32_t width, uint32_t hei .srgb = !depth, .width = width, .height = height, - .layers = (cube ? 6 : 1) << stereo, + .layers = view ? state.viewCount : ((cube ? 6 : 1) << stereo), .usage = TEXTURE_RENDER | TEXTURE_TRANSFER | (depth ? 0 : TEXTURE_SAMPLE), .handle = (uintptr_t) images[i].image, .label = "OpenXR Swapchain", @@ -3730,7 +3758,7 @@ static bool lovrSwapchainInit(Swapchain* swapchain, uint32_t width, uint32_t hei .format = FORMAT_RG8, .width = foveationImages[i].width, .height = foveationImages[i].height, - .layers = 1 << stereo, + .layers = state.viewCount, .usage = TEXTURE_FOVEATION, .handle = (uintptr_t) foveationImages[i].image, .label = "OpenXR Foveation Texture" @@ -4056,14 +4084,14 @@ static bool loadControllerModels(void) { } static bool loadVisibilityMask(void) { - XrViewConfigurationType viewConfig = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; + XrViewConfigurationType viewConfig = state.viewConfiguration; XrVisibilityMaskTypeKHR type = XR_VISIBILITY_MASK_TYPE_HIDDEN_TRIANGLE_MESH_KHR; XrVisibilityMaskKHR info = { .type = XR_TYPE_VISIBILITY_MASK_KHR }; uint32_t vertexCount = 0; uint32_t indexCount = 0; - for (uint32_t i = 0; i < 2; i++) { + for (uint32_t i = 0; i < state.viewCount; i++) { XR(xrGetVisibilityMaskKHR(state.session, viewConfig, i, type, &info), "xrGetVisibilityMask"); lovrCheck(UINT32_MAX - vertexCount >= info.vertexCountOutput, "Too many mask vertices"); lovrCheck(UINT32_MAX - indexCount >= info.indexCountOutput, "Too many mask indices"); @@ -4099,7 +4127,7 @@ static bool loadVisibilityMask(void) { } uint32_t baseVertex = 0; - for (uint32_t i = 0; i < 2; i++) { + for (uint32_t i = 0; i < state.viewCount; i++) { if (XR_FAILED(xrGetVisibilityMaskKHR(state.session, viewConfig, i, type, &info))) { lovrRelease(state.mask, lovrMeshDestroy); state.mask = NULL; From e0a06386b84845074d6911b3e4f6884d94eecb6b Mon Sep 17 00:00:00 2001 From: bjorn Date: Tue, 14 Oct 2025 22:03:56 -0700 Subject: [PATCH 3/4] For loops are hard; --- src/modules/headset/headset.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/headset/headset.c b/src/modules/headset/headset.c index 7448e1c58..38df4d7bf 100644 --- a/src/modules/headset/headset.c +++ b/src/modules/headset/headset.c @@ -626,10 +626,10 @@ bool lovrHeadsetConnect(void) { break; } } - - lovrAssertGoto(fail, i + 1 < COUNTOF(supportedViewConfigurations), "No supported view configuration available"); } + lovrAssertGoto(fail, state.viewConfiguration, "No supported view configuration available"); + XrViewConfigurationView views[4] = { [0].type = XR_TYPE_VIEW_CONFIGURATION_VIEW, [1].type = XR_TYPE_VIEW_CONFIGURATION_VIEW, From f59a5e9f80b4d3d75d443f8908a47f68900c3c9f Mon Sep 17 00:00:00 2001 From: bjorn Date: Tue, 14 Oct 2025 22:14:20 -0700 Subject: [PATCH 4/4] Fix swapchain initialization; --- src/modules/headset/headset.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/headset/headset.c b/src/modules/headset/headset.c index 38df4d7bf..f7913005e 100644 --- a/src/modules/headset/headset.c +++ b/src/modules/headset/headset.c @@ -3734,7 +3734,7 @@ static bool lovrSwapchainInit(Swapchain* swapchain, uint32_t width, uint32_t hei for (uint32_t i = 0; i < textureCount; i++, swapchain->textureCount++) { swapchain->textures[i] = lovrTextureCreate(&(TextureInfo) { - .type = cube ? TEXTURE_CUBE : (stereo ? TEXTURE_ARRAY : TEXTURE_2D), + .type = cube ? TEXTURE_CUBE : (stereo || view ? TEXTURE_ARRAY : TEXTURE_2D), .format = depth ? state.depthFormat : FORMAT_RGBA8, .srgb = !depth, .width = width, @@ -3754,7 +3754,7 @@ static bool lovrSwapchainInit(Swapchain* swapchain, uint32_t width, uint32_t hei #ifdef LOVR_VK if (foveated) { swapchain->foveationTextures[i] = lovrTextureCreate(&(TextureInfo) { - .type = stereo ? TEXTURE_ARRAY : TEXTURE_2D, + .type = stereo || view ? TEXTURE_ARRAY : TEXTURE_2D, .format = FORMAT_RG8, .width = foveationImages[i].width, .height = foveationImages[i].height,