XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET is a view configuration that renders four views per frame instead of the usual two: a wide, low-resolution outer view per eye, plus a narrow, high-resolution inset view per eye. The inset views cover a small portion of the field of view at higher pixels per degree, while the outer views cover the full field of view at lower pixels per degree. On devices with eye tracking, the runtime steers the inset to follow the user’s gaze each frame.
AndroidManifest.xml declaration described below. Without the opt-in, xrEnumerateViewConfigurations will not return it, even on devices that support it. xrEnumerateViewConfigurationViews returns four entries in the following fixed order:| Index | View |
|---|---|
0 | Left outer (wide field of view, lower pixels per degree) |
1 | Right outer (wide field of view, lower pixels per degree) |
2 | Left inset (narrow field of view, higher pixels per degree) |
3 | Right inset (narrow field of view, higher pixels per degree) |
pose returned by xrLocateViews — they are co-located cameras with different fields of view. The runtime guarantees that the inset’s projected rectangle is fully contained within the outer view’s projected rectangle, so the outer view must still render the full field of view including the region the inset covers. The compositor blends between the inset and outer images at the inset’s edges to hide the resolution boundary.fov returned from xrLocateViews shifts each frame to follow the user’s gaze (the runtime averages the gaze direction across both eyes so that the left and right insets remain aligned). When eye tracking is not active, the inset is fixed at the center of the field of view. See Eye tracking interaction below for the conditions under which eye tracking is active.<uses-feature> tag inside the <manifest> element of your AndroidManifest.xml:<uses-feature
android:name="com.oculus.feature.QUAD_VIEWS"
android:required="false" />
android:required="false" if your application falls back to XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO when the inset configuration is not available. Use android:required="true" only if your application cannot run without it — note that this restricts installation to Quest devices on Horizon OS v85 or higher.STEREO_WITH_FOVEATED_INSET from the list returned by xrEnumerateViewConfigurations, regardless of device capability.STEREO_WITH_FOVEATED_INSET is supported, the Meta runtime returns it first from xrEnumerateViewConfigurations, signaling that it is the runtime’s preferred configuration. Following the standard “select the first supported” pattern automatically opts your application in:uint32_t viewConfigCount = 0;
xrEnumerateViewConfigurations(instance, systemId, 0, &viewConfigCount, NULL);
XrViewConfigurationType viewConfigs[8];
xrEnumerateViewConfigurations(instance, systemId, viewConfigCount,
&viewConfigCount, viewConfigs);
XrViewConfigurationType selectedConfig = viewConfigs[0];
// selectedConfig will be XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET
// if the manifest tag is set and the device supports it; otherwise PRIMARY_STEREO.
xrEnumerateViewConfigurationViews with the selected configuration to retrieve the per-view recommended dimensions:uint32_t viewCount = 0;
xrEnumerateViewConfigurationViews(instance, systemId, selectedConfig,
0, &viewCount, NULL);
// viewCount == 4 for STEREO_WITH_FOVEATED_INSET
XrViewConfigurationView configViews[4] = {
{XR_TYPE_VIEW_CONFIGURATION_VIEW},
{XR_TYPE_VIEW_CONFIGURATION_VIEW},
{XR_TYPE_VIEW_CONFIGURATION_VIEW},
{XR_TYPE_VIEW_CONFIGURATION_VIEW},
};
xrEnumerateViewConfigurationViews(instance, systemId, selectedConfig,
viewCount, &viewCount, configViews);
// configViews[0..1] = outer views, configViews[2..3] = inset views.
// The recommended dimensions differ between outer and inset views.
arraySize = 2, sized to the outer recommended dimensions) and one for the two inset views (arraySize = 2, sized to the inset recommended dimensions). This pairs naturally with the recommended draw order in Step 5. The recommended dimensions for outer and inset views may differ (currently they are the same — the inset still achieves a higher pixels-per-degree because it covers a smaller field of view in the same pixel budget — but this can change in future runtime versions, so always size each pair from the values returned by xrEnumerateViewConfigurationViews).// Outer views (indices 0 and 1) share dimensions and use one multiview swapchain.
XrSwapchainCreateInfo outerCreateInfo = {
.type = XR_TYPE_SWAPCHAIN_CREATE_INFO,
.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT |
XR_SWAPCHAIN_USAGE_SAMPLED_BIT,
.format = vulkanColorFormat,
.sampleCount = 1,
.width = configViews[0].recommendedImageRectWidth,
.height = configViews[0].recommendedImageRectHeight,
.faceCount = 1,
.arraySize = 2, // multiview, two outer eyes
.mipCount = 1,
};
XrSwapchain outerSwapchain;
xrCreateSwapchain(session, &outerCreateInfo, &outerSwapchain);
// Inset views (indices 2 and 3) share dimensions and use one multiview swapchain.
XrSwapchainCreateInfo insetCreateInfo = outerCreateInfo;
insetCreateInfo.width = configViews[2].recommendedImageRectWidth;
insetCreateInfo.height = configViews[2].recommendedImageRectHeight;
XrSwapchain insetSwapchain;
xrCreateSwapchain(session, &insetCreateInfo, &insetSwapchain);
arraySize = 4 swapchain is only viable when all four recommended dimensions match. Even then, two multiview-2 swapchains are still preferred — see Step 5 for why batching the outer pair and inset pair as separate passes is the right rendering boundary.xrLocateViews with capacity 4. The returned views[2..3].fov will shift each frame on eye-tracked devices:XrView views[4] = {
{XR_TYPE_VIEW}, {XR_TYPE_VIEW}, {XR_TYPE_VIEW}, {XR_TYPE_VIEW},
};
XrViewState viewState = {XR_TYPE_VIEW_STATE};
XrViewLocateInfo locateInfo = {
.type = XR_TYPE_VIEW_LOCATE_INFO,
.viewConfigurationType =
XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET,
.displayTime = predictedDisplayTime,
.space = appSpace,
};
uint32_t viewCount = 4;
xrLocateViews(session, &locateInfo, &viewState, 4, &viewCount, views);
pose always equals the corresponding outer eye’s pose. Only the fov differs (and changes per frame when eye tracking is active).XrCompositionLayerProjection containing all four projection views with viewCount = 4. Views 0 and 1 reference the outer multiview swapchain (array indices 0 and 1); views 2 and 3 reference the inset multiview swapchain (array indices 0 and 1):XrCompositionLayerProjectionView projViews[4];
// Outer views: array indices 0 and 1 of outerSwapchain.
for (int i = 0; i < 2; i++) {
projViews[i] = (XrCompositionLayerProjectionView){
.type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW,
.pose = views[i].pose,
.fov = views[i].fov,
.subImage = {
.swapchain = outerSwapchain,
.imageArrayIndex = i,
.imageRect = {
.offset = {0, 0},
.extent = {
configViews[i].recommendedImageRectWidth,
configViews[i].recommendedImageRectHeight,
},
},
},
};
}
// Inset views: array indices 0 and 1 of insetSwapchain.
for (int i = 0; i < 2; i++) {
projViews[i + 2] = (XrCompositionLayerProjectionView){
.type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW,
.pose = views[i + 2].pose,
.fov = views[i + 2].fov,
.subImage = {
.swapchain = insetSwapchain,
.imageArrayIndex = i,
.imageRect = {
.offset = {0, 0},
.extent = {
configViews[i + 2].recommendedImageRectWidth,
configViews[i + 2].recommendedImageRectHeight,
},
},
},
};
}
XrCompositionLayerProjection projLayer = {
.type = XR_TYPE_COMPOSITION_LAYER_PROJECTION,
.space = appSpace,
.viewCount = 4,
.views = projViews,
};
XR_EXT_eye_gaze_interaction.xrLocateViews simply returns a stable views[2..3].fov each frame. No additional adaptation is required from the application.XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO for devices and runtime configurations where the inset configuration is not advertised. Use android:required="false" on the manifest tag so that your application can install on every Quest device.setprop) is present and the device supports it.STEREO_WITH_FOVEATED_INSET. The inset/outer split is itself a foveation technique — applying a fragment density map to the outer view on top of an already-reduced-density buffer compounds aliasing in the periphery without meaningful additional savings, and the inset is small enough that FFR provides little benefit there either. Use one or the other, not both.VK_QCOM_multiview_per_view_render_areas Vulkan extension to limit the rendered pixels to each eye’s actual visible region. With symmetric projection, the symmetric outer buffer is wider than each eye’s true field of view; per-view render areas let you skip shading the off-screen tiles directly. This is a better fit for quad views than relying on a fragment density map to discard those tiles, because it avoids the FFR/inset interaction described above.xrEnumerateViewConfigurationViews. The runtime sizes them so that the inset is at higher pixels per degree than the outer view; do not equalize them.