diff --git a/src/api/api.h b/src/api/api.h index 069089047..b9c850c3c 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -40,6 +40,7 @@ extern StringEntry lovrEffect[]; extern StringEntry lovrEventType[]; extern StringEntry lovrFileAction[]; extern StringEntry lovrFilterMode[]; +extern StringEntry lovrFoveationLevel[]; extern StringEntry lovrHeadsetDriver[]; extern StringEntry lovrHorizontalAlign[]; extern StringEntry lovrJointType[]; diff --git a/src/api/l_graphics_pass.c b/src/api/l_graphics_pass.c index a41a3bb87..6ec29f4d6 100644 --- a/src/api/l_graphics_pass.c +++ b/src/api/l_graphics_pass.c @@ -40,8 +40,9 @@ static int l_lovrPassGetCanvas(lua_State* L) { CanvasTexture color[4]; CanvasTexture depth; uint32_t depthFormat; + Texture* foveation; uint32_t samples; - lovrPassGetCanvas(pass, color, &depth, &depthFormat, &samples); + lovrPassGetCanvas(pass, color, &depth, &depthFormat, &foveation, &samples); if (!color[0].texture && !depth.texture) { lua_pushnil(L); @@ -141,7 +142,7 @@ int l_lovrPassSetCanvas(lua_State* L) { } else if (!lua_isnoneornil(L, 2)) { luaL_error(L, "Expected Texture, table, or nil for canvas"); } - luax_assert(L, lovrPassSetCanvas(pass, color, &depth, depthFormat, samples)); + luax_assert(L, lovrPassSetCanvas(pass, color, &depth, depthFormat, NULL, samples)); return 0; } diff --git a/src/api/l_headset.c b/src/api/l_headset.c index 777df8a08..50803c343 100644 --- a/src/api/l_headset.c +++ b/src/api/l_headset.c @@ -21,6 +21,14 @@ StringEntry lovrControllerSkeletonMode[] = { { 0 } }; +StringEntry lovrFoveationLevel[] = { + [FOVEATION_NONE] = ENTRY("none"), + [FOVEATION_LOW] = ENTRY("low"), + [FOVEATION_MEDIUM] = ENTRY("medium"), + [FOVEATION_HIGH] = ENTRY("high"), + { 0 } +}; + StringEntry lovrPassthroughMode[] = { [PASSTHROUGH_OPAQUE] = ENTRY("opaque"), [PASSTHROUGH_BLEND] = ENTRY("blend"), @@ -218,6 +226,29 @@ static int l_lovrHeadsetGetRefreshRates(lua_State* L) { return 1; } +static int l_lovrHeadsetGetFoveation(lua_State* L) { + FoveationLevel level; + bool dynamic; + lovrHeadsetInterface->getFoveation(&level, &dynamic); + luax_pushenum(L, FoveationLevel, level); + lua_pushboolean(L, dynamic); + return 2; +} + +static int l_lovrHeadsetSetFoveation(lua_State* L) { + if (lua_isnoneornil(L, 1)) { + bool success = lovrHeadsetInterface->setFoveation(FOVEATION_NONE, false); + lua_pushboolean(L, success); + return 1; + } else { + FoveationLevel level = luax_checkenum(L, 1, FoveationLevel, NULL); + bool dynamic = lua_isnoneornil(L, -1) ? true : lua_toboolean(L, 2); + bool success = lovrHeadsetInterface->setFoveation(level, dynamic); + lua_pushboolean(L, success); + return 1; + } +} + static int l_lovrHeadsetGetPassthrough(lua_State* L) { PassthroughMode mode = lovrHeadsetInterface->getPassthrough(); luax_pushenum(L, PassthroughMode, mode); @@ -895,6 +926,8 @@ static const luaL_Reg lovrHeadset[] = { { "getRefreshRate", l_lovrHeadsetGetRefreshRate }, { "setRefreshRate", l_lovrHeadsetSetRefreshRate }, { "getRefreshRates", l_lovrHeadsetGetRefreshRates }, + { "getFoveation", l_lovrHeadsetGetFoveation }, + { "setFoveation", l_lovrHeadsetSetFoveation }, { "getPassthrough", l_lovrHeadsetGetPassthrough }, { "setPassthrough", l_lovrHeadsetSetPassthrough }, { "getPassthroughModes", l_lovrHeadsetGetPassthroughModes }, diff --git a/src/core/gpu.h b/src/core/gpu.h index c096ac3c9..8ad99713e 100644 --- a/src/core/gpu.h +++ b/src/core/gpu.h @@ -54,7 +54,8 @@ enum { GPU_TEXTURE_RENDER = (1 << 1), GPU_TEXTURE_STORAGE = (1 << 2), GPU_TEXTURE_COPY_SRC = (1 << 3), - GPU_TEXTURE_COPY_DST = (1 << 4) + GPU_TEXTURE_COPY_DST = (1 << 4), + GPU_TEXTURE_FOVEATION = (1 << 5) }; typedef enum { @@ -348,6 +349,7 @@ typedef struct { uint32_t colorCount; uint32_t samples; uint32_t views; + bool foveated; bool surface; } gpu_pass_info; @@ -579,6 +581,7 @@ typedef struct { typedef struct { gpu_color_attachment color[4]; gpu_depth_attachment depth; + gpu_texture* foveation; gpu_pass* pass; uint32_t width; uint32_t height; @@ -690,6 +693,7 @@ typedef struct { bool wireframe; bool depthClamp; bool depthResolve; + bool foveation; bool indirectDrawFirstInstance; bool packedBuffers; bool shaderDebug; diff --git a/src/core/gpu_vk.c b/src/core/gpu_vk.c index 6ad51315d..8aa10d841 100644 --- a/src/core/gpu_vk.c +++ b/src/core/gpu_vk.c @@ -177,6 +177,8 @@ typedef struct { bool renderPass2; bool synchronization2; bool scalarBlockLayout; + bool fdExternalMemory; + bool foveation; } gpu_extensions; // State @@ -486,6 +488,7 @@ bool gpu_texture_init(gpu_texture* texture, gpu_texture_info* info) { ((info->usage & GPU_TEXTURE_STORAGE) ? VK_IMAGE_USAGE_STORAGE_BIT : 0) | ((info->usage & GPU_TEXTURE_COPY_SRC) ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0) | ((info->usage & GPU_TEXTURE_COPY_DST) ? VK_IMAGE_USAGE_TRANSFER_DST_BIT : 0) | + ((info->usage & GPU_TEXTURE_FOVEATION) ? VK_IMAGE_USAGE_FRAGMENT_DENSITY_MAP_BIT_EXT : 0) | ((info->usage == GPU_TEXTURE_RENDER) ? VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT : 0) | (info->upload.levelCount > 0 ? VK_IMAGE_USAGE_TRANSFER_DST_BIT : 0) | (info->upload.generateMipmaps ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0) @@ -697,7 +700,8 @@ bool gpu_texture_init_view(gpu_texture* texture, gpu_texture_view_info* info) { ((info->usage & GPU_TEXTURE_SAMPLE) ? VK_IMAGE_USAGE_SAMPLED_BIT : 0) | (((info->usage & GPU_TEXTURE_RENDER) && texture->aspect == VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) | (((info->usage & GPU_TEXTURE_RENDER) && texture->aspect != VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : 0) | - ((info->usage & GPU_TEXTURE_STORAGE) && !texture->srgb ? VK_IMAGE_USAGE_STORAGE_BIT : 0) + ((info->usage & GPU_TEXTURE_STORAGE) && !texture->srgb ? VK_IMAGE_USAGE_STORAGE_BIT : 0) | + ((info->usage & GPU_TEXTURE_FOVEATION) ? VK_IMAGE_USAGE_FRAGMENT_DENSITY_MAP_BIT_EXT : 0) }; if (viewUsage.usage == 0) { @@ -1413,6 +1417,18 @@ bool gpu_pass_init(gpu_pass* pass, gpu_pass_info* info) { } } + if (info->foveated) { + attachments[attachmentCount++] = (VkAttachmentDescription2) { + .sType = VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION_2, + .format = VK_FORMAT_R8G8_UNORM, + .samples = VK_SAMPLE_COUNT_1_BIT, + .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD, + .storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, + .initialLayout = VK_IMAGE_LAYOUT_FRAGMENT_DENSITY_MAP_OPTIMAL_EXT, + .finalLayout = VK_IMAGE_LAYOUT_FRAGMENT_DENSITY_MAP_OPTIMAL_EXT + }; + } + uint32_t referenceCount = (info->colorCount << hasColorResolve) + (depth << info->depth.resolve); VkSubpassDescription2 subpass = { @@ -1432,6 +1448,13 @@ bool gpu_pass_init(gpu_pass* pass, gpu_pass_info* info) { VkRenderPassCreateInfo2 createInfo = { .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO_2, + .pNext = info->foveated ? &(VkRenderPassFragmentDensityMapCreateInfoEXT) { + .sType = VK_STRUCTURE_TYPE_RENDER_PASS_FRAGMENT_DENSITY_MAP_CREATE_INFO_EXT, + .fragmentDensityMapAttachment = { + .attachment = attachmentCount - 1, + .layout = VK_IMAGE_LAYOUT_FRAGMENT_DENSITY_MAP_OPTIMAL_EXT + } + } : NULL, .attachmentCount = attachmentCount, .pAttachments = attachments, .subpassCount = 1, @@ -1860,8 +1883,8 @@ void gpu_render_begin(gpu_stream* stream, gpu_canvas* canvas) { // Framebuffer - VkImageView images[10]; - VkClearValue clears[10]; + VkImageView images[11]; + VkClearValue clears[11]; uint32_t attachmentCount = 0; for (uint32_t i = 0; i < pass->colorCount; i++) { @@ -1885,6 +1908,11 @@ void gpu_render_begin(gpu_stream* stream, gpu_canvas* canvas) { } } + if (canvas->foveation) { + uint32_t index = attachmentCount++; + images[index] = canvas->foveation->view; + } + VkFramebufferCreateInfo info = { .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, .renderPass = pass->handle, @@ -2386,7 +2414,9 @@ bool gpu_init(gpu_config* config) { { "VK_KHR_shader_non_semantic_info", config->debug, &state.extensions.shaderDebug }, { "VK_KHR_image_format_list", true, &state.extensions.formatList }, { "VK_KHR_synchronization2", true, &state.extensions.synchronization2 }, - { "VK_EXT_scalar_block_layout", true, &state.extensions.scalarBlockLayout } + { "VK_KHR_external_memory_fd", true, &state.extensions.fdExternalMemory }, // Not used by us, Meta forgot to enable this and it spews errors + { "VK_EXT_scalar_block_layout", true, &state.extensions.scalarBlockLayout }, + { "VK_EXT_fragment_density_map", true, &state.extensions.foveation } }; uint32_t extensionCount = 0; @@ -2471,6 +2501,10 @@ bool gpu_init(gpu_config* config) { // Features + VkPhysicalDeviceFragmentDensityMapFeaturesEXT fragmentDensityMapFeatures = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_FEATURES_EXT + }; + VkPhysicalDeviceScalarBlockLayoutFeaturesEXT scalarBlockLayoutFeatures = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SCALAR_BLOCK_LAYOUT_FEATURES_EXT }; @@ -2498,6 +2532,12 @@ bool gpu_init(gpu_config* config) { VkPhysicalDeviceFeatures2 features2 = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2 }; VkPhysicalDeviceFeatures* enable = &enabledFeatures.features; VkPhysicalDeviceFeatures* supports = &features2.features; + + if (state.extensions.foveation) { + fragmentDensityMapFeatures.pNext = features2.pNext; + features2.pNext = &fragmentDensityMapFeatures; + } + vkGetPhysicalDeviceFeatures2(state.adapter, &features2); // Required features @@ -2537,6 +2577,14 @@ bool gpu_init(gpu_config* config) { enabledFeatures.pNext = &scalarBlockLayoutFeatures; } + if (state.extensions.foveation && fragmentDensityMapFeatures.fragmentDensityMap) { + fragmentDensityMapFeatures.fragmentDensityMapDynamic = false; + fragmentDensityMapFeatures.fragmentDensityMapNonSubsampledImages = true; + fragmentDensityMapFeatures.pNext = enabledFeatures.pNext; + enabledFeatures.pNext = &fragmentDensityMapFeatures; + config->features->foveation = true; + } + // Formats for (uint32_t i = 0; i < GPU_FORMAT_COUNT; i++) { for (int j = 0; j < 2; j++) { diff --git a/src/modules/graphics/graphics.c b/src/modules/graphics/graphics.c index be192c87c..75a23a2b4 100644 --- a/src/modules/graphics/graphics.c +++ b/src/modules/graphics/graphics.c @@ -429,6 +429,7 @@ typedef struct { typedef struct { Attachment color[4]; Attachment depth; + Texture* foveation; uint32_t count; uint32_t width; uint32_t height; @@ -1192,6 +1193,7 @@ static bool recordRenderPass(Pass* pass, gpu_stream* stream) { target.pass = pass->gpu; target.width = canvas->width; target.height = canvas->height; + target.foveation = canvas->foveation ? canvas->foveation->gpu : NULL; // Cameras @@ -2406,7 +2408,8 @@ Texture* lovrTextureCreate(const TextureInfo* info) { ((info->usage & TEXTURE_SAMPLE) ? GPU_TEXTURE_SAMPLE : 0) | ((info->usage & TEXTURE_RENDER) ? GPU_TEXTURE_RENDER : 0) | ((info->usage & TEXTURE_STORAGE) ? GPU_TEXTURE_STORAGE : 0) | - (transfer ? GPU_TEXTURE_COPY_SRC | GPU_TEXTURE_COPY_DST : 0), + (transfer ? GPU_TEXTURE_COPY_SRC | GPU_TEXTURE_COPY_DST : 0) | + ((info->usage & TEXTURE_FOVEATION) ? GPU_TEXTURE_FOVEATION : 0), .srgb = srgb, .handle = info->handle, .label = info->label, @@ -5790,7 +5793,7 @@ bool lovrGraphicsGetWindowPass(Pass** pass) { lovrPassReset(state.windowPass); memcpy(state.windowPass->canvas.color[0].clear, state.background, 4 * sizeof(float)); CanvasTexture color[4] = { [0].texture = window }; - lovrPassSetCanvas(state.windowPass, color, NULL, state.depthFormat, state.config.antialias ? 4 : 1); + lovrPassSetCanvas(state.windowPass, color, NULL, state.depthFormat, NULL, state.config.antialias ? 4 : 1); *pass = state.windowPass; return true; } @@ -5916,7 +5919,7 @@ const char* lovrPassGetLabel(Pass* pass) { return pass->label; } -void lovrPassGetCanvas(Pass* pass, CanvasTexture color[4], CanvasTexture* depth, uint32_t* depthFormat, uint32_t* samples) { +void lovrPassGetCanvas(Pass* pass, CanvasTexture color[4], CanvasTexture* depth, uint32_t* depthFormat, Texture** foveation, uint32_t* samples) { for (uint32_t i = 0; i < COUNTOF(pass->canvas.color); i++) { color[i].texture = pass->canvas.color[i].texture; color[i].resolve = pass->canvas.color[i].resolve; @@ -5924,10 +5927,11 @@ void lovrPassGetCanvas(Pass* pass, CanvasTexture color[4], CanvasTexture* depth, depth->texture = pass->canvas.depth.texture; depth->resolve = pass->canvas.depth.resolve; *depthFormat = pass->canvas.depth.format; + *foveation = pass->canvas.foveation; *samples = pass->canvas.samples; } -bool lovrPassSetCanvas(Pass* pass, CanvasTexture color[4], CanvasTexture* depth, uint32_t depthFormat, uint32_t samples) { +bool lovrPassSetCanvas(Pass* pass, CanvasTexture color[4], CanvasTexture* depth, uint32_t depthFormat, Texture* foveation, uint32_t samples) { Canvas* canvas = &pass->canvas; for (uint32_t i = 0; i < canvas->count; i++) { @@ -5943,6 +5947,9 @@ bool lovrPassSetCanvas(Pass* pass, CanvasTexture color[4], CanvasTexture* depth, canvas->depth.resolve = NULL; canvas->depth.format = 0; + lovrRelease(canvas->foveation, lovrTextureDestroy); + canvas->foveation = NULL; + canvas->count = 0; canvas->width = 0; canvas->height = 0; @@ -6043,10 +6050,13 @@ bool lovrPassSetCanvas(Pass* pass, CanvasTexture color[4], CanvasTexture* depth, canvas->depth.automsaa = true; } + lovrRetain(foveation); + canvas->foveation = foveation; + pass->gpu = getPass(canvas); if (!pass->gpu) { - return lovrPassSetCanvas(pass, NULL, NULL, 0, 0); + return lovrPassSetCanvas(pass, NULL, NULL, 0, NULL, 0); } lovrPassReset(pass); @@ -8348,6 +8358,7 @@ static gpu_pass* getPass(Canvas* canvas) { info.colorCount = canvas->count; info.samples = canvas->samples; info.views = canvas->views; + info.foveated = !!canvas->foveation; info.surface = canvas->count > 0 && canvas->color[0].texture == state.window; uint64_t hash = hash64(&info, sizeof(info)); diff --git a/src/modules/graphics/graphics.h b/src/modules/graphics/graphics.h index 9025cbbab..2bf578ec4 100644 --- a/src/modules/graphics/graphics.h +++ b/src/modules/graphics/graphics.h @@ -195,10 +195,11 @@ typedef enum { } TextureType; enum { - TEXTURE_SAMPLE = (1 << 0), - TEXTURE_RENDER = (1 << 1), - TEXTURE_STORAGE = (1 << 2), - TEXTURE_TRANSFER = (1 << 3) + TEXTURE_SAMPLE = (1 << 0), + TEXTURE_RENDER = (1 << 1), + TEXTURE_STORAGE = (1 << 2), + TEXTURE_TRANSFER = (1 << 3), + TEXTURE_FOVEATION = (1 << 4) }; typedef struct { @@ -580,8 +581,8 @@ void lovrPassReset(Pass* pass); const PassStats* lovrPassGetStats(Pass* pass); const char* lovrPassGetLabel(Pass* pass); -void lovrPassGetCanvas(Pass* pass, CanvasTexture color[4], CanvasTexture* depth, uint32_t* depthFormat, uint32_t* samples); -bool lovrPassSetCanvas(Pass* pass, CanvasTexture color[4], CanvasTexture* depth, uint32_t depthFormat, uint32_t samples); +void lovrPassGetCanvas(Pass* pass, CanvasTexture color[4], CanvasTexture* depth, uint32_t* depthFormat, Texture** foveation, uint32_t* samples); +bool lovrPassSetCanvas(Pass* pass, CanvasTexture color[4], CanvasTexture* depth, uint32_t depthFormat, Texture* foveation, uint32_t samples); void lovrPassGetClear(Pass* pass, LoadAction loads[4], float clears[4][4], LoadAction* depthLoad, float* depthClear); bool lovrPassSetClear(Pass* pass, LoadAction loads[4], float clears[4][4], LoadAction depthLoad, float depthClear); uint32_t lovrPassGetAttachmentCount(Pass* pass, bool* depth); diff --git a/src/modules/headset/headset.h b/src/modules/headset/headset.h index 5197b3486..0d196c778 100644 --- a/src/modules/headset/headset.h +++ b/src/modules/headset/headset.h @@ -62,6 +62,13 @@ typedef struct { bool layerFilter; } HeadsetFeatures; +typedef enum { + FOVEATION_NONE, + FOVEATION_LOW, + FOVEATION_MEDIUM, + FOVEATION_HIGH +} FoveationLevel; + typedef enum { PASSTHROUGH_OPAQUE, PASSTHROUGH_BLEND, @@ -205,6 +212,8 @@ typedef struct HeadsetInterface { float (*getRefreshRate)(void); bool (*setRefreshRate)(float refreshRate); const float* (*getRefreshRates)(uint32_t* count); + void (*getFoveation)(FoveationLevel* level, bool* dynamic); + bool (*setFoveation)(FoveationLevel level, bool dynamic); PassthroughMode (*getPassthrough)(void); bool (*setPassthrough)(PassthroughMode mode); bool (*isPassthroughSupported)(PassthroughMode mode); diff --git a/src/modules/headset/headset_openxr.c b/src/modules/headset/headset_openxr.c index ff90b4364..7a6886b8b 100644 --- a/src/modules/headset/headset_openxr.c +++ b/src/modules/headset/headset_openxr.c @@ -120,13 +120,16 @@ uintptr_t gpu_vk_get_queue(uint32_t* queueFamilyIndex, uint32_t* queueIndex); X(xrRequestDisplayRefreshRateFB)\ X(xrQuerySystemTrackedKeyboardFB)\ X(xrCreateKeyboardSpaceFB)\ + X(xrCreateFoveationProfileFB)\ + X(xrDestroyFoveationProfileFB)\ X(xrCreatePassthroughFB)\ X(xrDestroyPassthroughFB)\ X(xrPassthroughStartFB)\ X(xrPassthroughPauseFB)\ X(xrCreatePassthroughLayerFB)\ X(xrDestroyPassthroughLayerFB)\ - X(xrGetPassthroughPreferencesMETA) + X(xrGetPassthroughPreferencesMETA)\ + X(xrUpdateSwapchainFB) #define XR_DECLARE(fn) static PFN_##fn fn; #define XR_LOAD(fn) xrGetInstanceProcAddr(state.instance, #fn, (PFN_xrVoidFunction*) &fn); @@ -181,6 +184,7 @@ typedef struct { uint32_t textureIndex; uint32_t textureCount; Texture* textures[MAX_IMAGES]; + Texture* foveationTextures[MAX_IMAGES]; bool immutable; bool acquired; } Swapchain; @@ -204,7 +208,18 @@ struct Layer { XrCompositionLayerSettingsFB settings; }; -enum { COLOR, DEPTH }; +enum { + COLOR, + DEPTH +}; + +enum { + FLAG_STEREO = (1 << 0), + FLAG_DEPTH = (1 << 1), + FLAG_CUBE = (1 << 2), + FLAG_STATIC = (1 << 3), + FLAG_FOVEATED = (1 << 4) +}; static struct { HeadsetConfig config; @@ -244,6 +259,9 @@ static struct { XrPath actionFilters[MAX_DEVICES]; XrHandTrackerEXT handTrackers[2]; XrControllerModelKeyMSFT controllerModelKeys[2]; + FoveationLevel foveationLevel; + bool foveationDynamic; + XrFoveationProfileFB foveationProfile; XrPassthroughFB passthrough; XrPassthroughLayerFB passthroughLayerHandle; bool passthroughActive; @@ -253,6 +271,9 @@ static struct { bool controllerModel; bool debug; bool depth; + bool foveation; + bool foveationConfig; + bool foveationVulkan; bool gaze; bool handInteraction; bool handTracking; @@ -279,6 +300,7 @@ static struct { bool picoController; bool presence; bool questPassthrough; + bool swapchainUpdate; bool refreshRate; bool threadHint; bool visibilityMask; @@ -585,7 +607,13 @@ static bool loadVisibilityMask(void) { return true; } -static bool swapchain_init(Swapchain* swapchain, uint32_t width, uint32_t height, bool stereo, bool depth, bool cube, bool immutable) { +static bool swapchain_init(Swapchain* swapchain, uint32_t width, uint32_t height, uint32_t flags) { + bool stereo = flags & FLAG_STEREO; + bool depth = flags & FLAG_DEPTH; + bool cube = flags & FLAG_CUBE; + bool immutable = flags & FLAG_STATIC; + bool foveated = state.extensions.foveation && (flags & FLAG_FOVEATED); + XrSwapchainCreateInfo info = { .type = XR_TYPE_SWAPCHAIN_CREATE_INFO, .createFlags = immutable ? XR_SWAPCHAIN_CREATE_STATIC_IMAGE_BIT : 0, @@ -597,6 +625,17 @@ static bool swapchain_init(Swapchain* swapchain, uint32_t width, uint32_t height .mipCount = 1 }; + XrSwapchainCreateInfoFoveationFB foveation = { + .type = XR_TYPE_SWAPCHAIN_CREATE_INFO_FOVEATION_FB, +#ifdef LOVR_VK + .flags = XR_SWAPCHAIN_CREATE_FOVEATION_FRAGMENT_DENSITY_MAP_BIT_FB +#endif + }; + + if (foveated) { + info.next = &foveation; + } + if (depth) { info.usageFlags = XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; switch (state.depthFormat) { @@ -616,10 +655,15 @@ static bool swapchain_init(Swapchain* swapchain, uint32_t width, uint32_t height XR(xrCreateSwapchain(state.session, &info, &swapchain->handle), "xrCreateSwapchain"); #ifdef LOVR_VK + XrSwapchainImageFoveationVulkanFB foveationImages[MAX_IMAGES]; XrSwapchainImageVulkanKHR images[MAX_IMAGES]; for (uint32_t i = 0; i < MAX_IMAGES; i++) { - images[i].type = XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR; - images[i].next = NULL; + images[i] = (XrSwapchainImageVulkanKHR) { .type = XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR }; + + if (foveated) { + foveationImages[i] = (XrSwapchainImageFoveationVulkanFB) { .type = XR_TYPE_SWAPCHAIN_IMAGE_FOVEATION_VULKAN_FB }; + images[i].next = &foveationImages[i]; + } } #else #error "Unsupported graphics backend" @@ -648,6 +692,24 @@ static bool swapchain_init(Swapchain* swapchain, uint32_t width, uint32_t height swapchain_destroy(swapchain); return false; } + +#ifdef LOVR_VK + if (foveated) { + swapchain->foveationTextures[i] = lovrTextureCreate(&(TextureInfo) { + .type = stereo ? TEXTURE_ARRAY : TEXTURE_2D, + .format = FORMAT_RG8, + .width = foveationImages[i].width, + .height = foveationImages[i].height, + .layers = 1 << stereo, + .mipmaps = 1, + .samples = 1, + .usage = TEXTURE_FOVEATION, + .handle = (uintptr_t) foveationImages[i].image, + .label = "OpenXR Foveation Texture", + .xr = false // Does not require layout transitions (although spec doesn't say anything!) + }); + } +#endif } swapchain->immutable = immutable; @@ -840,10 +902,14 @@ static bool openxr_init(HeadsetConfig* config) { { "XR_FB_composition_layer_depth_test", &state.extensions.layerDepthTest, true }, { "XR_FB_composition_layer_settings", &state.extensions.layerSettings, true }, { "XR_FB_display_refresh_rate", &state.extensions.refreshRate, true }, + { "XR_FB_foveation", &state.extensions.foveation, true }, + { "XR_FB_foveation_configuration", &state.extensions.foveationConfig, true }, + { "XR_FB_foveation_vulkan", &state.extensions.foveationVulkan, true }, { "XR_FB_hand_tracking_aim", &state.extensions.handTrackingAim, true }, { "XR_FB_hand_tracking_mesh", &state.extensions.handTrackingMesh, true }, { "XR_FB_keyboard_tracking", &state.extensions.keyboardTracking, true }, { "XR_FB_passthrough", &state.extensions.questPassthrough, true }, + { "XR_FB_swapchain_update_state", &state.extensions.swapchainUpdate, true }, { "XR_LOGITECH_mx_ink_stylus_interaction", &state.extensions.mxInk, true }, { "XR_META_automatic_layer_filter", &state.extensions.layerAutoFilter, true }, { "XR_META_passthrough_preferences", &state.extensions.passthroughPreferences, true }, @@ -1683,15 +1749,17 @@ static bool openxr_start(void) { } } + uint32_t flags = FLAG_STEREO | (state.extensions.foveation ? FLAG_FOVEATED : 0); + lovrAssertGoto(stop, supportsColor, "This VR runtime does not support sRGB rgba8 textures"); - if (!swapchain_init(&state.swapchains[COLOR], state.width, state.height, true, false, false, false)) { + if (!swapchain_init(&state.swapchains[COLOR], state.width, state.height, flags)) { goto stop; } GraphicsFeatures features; lovrGraphicsGetFeatures(&features); if (state.extensions.depth && supportsDepth && features.depthResolve) { - if (!swapchain_init(&state.swapchains[DEPTH], state.width, state.height, true, true, false, false)) { + if (!swapchain_init(&state.swapchains[DEPTH], state.width, state.height, FLAG_STEREO | FLAG_DEPTH)) { goto stop; } } else { @@ -1904,6 +1972,65 @@ static const float* openxr_getRefreshRates(uint32_t* count) { return state.refreshRates; } +static void openxr_getFoveation(FoveationLevel* level, bool* dynamic) { + *level = state.foveationLevel; + *dynamic = state.foveationDynamic; +} + +static bool openxr_setFoveation(FoveationLevel level, bool dynamic) { + if (!state.session || !state.extensions.foveation) { + return level == FOVEATION_NONE; + } + + if (state.foveationLevel == level && state.foveationDynamic == dynamic) { + return true; + } + + XrFoveationLevelProfileCreateInfoFB profileInfo = { + .type = XR_TYPE_FOVEATION_LEVEL_PROFILE_CREATE_INFO_FB + }; + + switch (level) { + case FOVEATION_NONE: profileInfo.level = XR_FOVEATION_LEVEL_NONE_FB; break; + case FOVEATION_LOW: profileInfo.level = XR_FOVEATION_LEVEL_LOW_FB; break; + case FOVEATION_MEDIUM: profileInfo.level = XR_FOVEATION_LEVEL_MEDIUM_FB; break; + case FOVEATION_HIGH: profileInfo.level = XR_FOVEATION_LEVEL_HIGH_FB; break; + default: lovrUnreachable(); + } + + if (dynamic) { + profileInfo.dynamic = XR_FOVEATION_DYNAMIC_LEVEL_ENABLED_FB; + } else { + profileInfo.dynamic = XR_FOVEATION_DYNAMIC_DISABLED_FB; + } + + XrFoveationProfileCreateInfoFB info = { + .type = XR_TYPE_FOVEATION_PROFILE_CREATE_INFO_FB, + .next = &profileInfo + }; + + XrFoveationProfileFB profile; + if (XR_FAILED(xrCreateFoveationProfileFB(state.session, &info, &profile))) { + return false; + } + + XrSwapchainStateFoveationFB foveationState = { + .type = XR_TYPE_SWAPCHAIN_STATE_FOVEATION_FB, + .profile = profile + }; + + if (XR_FAILED(xrUpdateSwapchainFB(state.swapchains[COLOR].handle, (XrSwapchainStateBaseHeaderFB*) &foveationState))) { + return false; + } + + xrDestroyFoveationProfileFB(profile); + + state.foveationLevel = level; + state.foveationDynamic = dynamic; + + return true; +} + static XrEnvironmentBlendMode convertPassthroughMode(PassthroughMode mode) { switch (mode) { case PASSTHROUGH_OPAQUE: return XR_ENVIRONMENT_BLEND_MODE_OPAQUE; @@ -2932,12 +3059,17 @@ static Layer* openxr_newLayer(const LayerInfo* info) { layer->ref = 1; layer->info = *info; - if (!swapchain_init(&layer->swapchain, info->width, info->height, info->stereo, false, info->type == LAYER_CUBE, info->immutable)) { + uint32_t flags = + (info->stereo ? FLAG_STEREO : 0) | + (info->type == LAYER_CUBE ? FLAG_CUBE : 0) | + (info->immutable ? FLAG_STATIC : 0); + + if (!swapchain_init(&layer->swapchain, info->width, info->height, flags)) { lovrLayerDestroy(layer); return NULL; } - XrCompositionLayerFlags flags = info->transparent ? XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT : 0; + XrCompositionLayerFlags layerFlags = info->transparent ? XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT : 0; XrEyeVisibility eyes = info->stereo ? XR_EYE_VISIBILITY_LEFT : XR_EYE_VISIBILITY_BOTH; XrSwapchainSubImage subimage = { layer->swapchain.handle, { 0, 0, info->width, info->height }, 0 }; @@ -2945,7 +3077,7 @@ static Layer* openxr_newLayer(const LayerInfo* info) { case LAYER_QUAD: layer->quad = (XrCompositionLayerQuad) { .type = XR_TYPE_COMPOSITION_LAYER_QUAD, - .layerFlags = flags, + .layerFlags = layerFlags, .eyeVisibility = eyes, .subImage = subimage, .pose.orientation.w = 1.f, @@ -2955,7 +3087,7 @@ static Layer* openxr_newLayer(const LayerInfo* info) { case LAYER_CUBE: layer->cube = (XrCompositionLayerCubeKHR) { .type = XR_TYPE_COMPOSITION_LAYER_CUBE_KHR, - .layerFlags = flags, + .layerFlags = layerFlags, .eyeVisibility = eyes, .swapchain = layer->swapchain.handle, .orientation.w = 1.f @@ -2965,7 +3097,7 @@ static Layer* openxr_newLayer(const LayerInfo* info) { if (state.extensions.layerEquirect2) { layer->equirect2 = (XrCompositionLayerEquirect2KHR) { .type = XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR, - .layerFlags = flags, + .layerFlags = layerFlags, .eyeVisibility = eyes, .subImage = subimage, .pose.orientation.w = 1.f, @@ -2976,7 +3108,7 @@ static Layer* openxr_newLayer(const LayerInfo* info) { } else { layer->equirect = (XrCompositionLayerEquirectKHR) { .type = XR_TYPE_COMPOSITION_LAYER_EQUIRECT_KHR, - .layerFlags = flags, + .layerFlags = layerFlags, .eyeVisibility = eyes, .subImage = subimage, .pose.orientation.w = 1.f, @@ -3271,7 +3403,7 @@ static Pass* openxr_getLayerPass(Layer* layer) { if (!texture) return NULL; CanvasTexture color[4] = { [0].texture = texture }; - if (!lovrPassSetCanvas(layer->pass, color, NULL, state.depthFormat, state.config.antialias ? 4 : 1)) { + if (!lovrPassSetCanvas(layer->pass, color, NULL, state.depthFormat, NULL, state.config.antialias ? 4 : 1)) { return NULL; } @@ -3366,7 +3498,9 @@ static bool openxr_getPass(Pass** pass) { return true; } - if (!lovrPassSetCanvas(state.pass, color, &depth, state.depthFormat, state.config.antialias ? 4 : 1)) { + Texture* foveation = state.swapchains[COLOR].foveationTextures[state.swapchains[COLOR].textureIndex]; + + if (!lovrPassSetCanvas(state.pass, color, &depth, state.depthFormat, foveation, state.config.antialias ? 4 : 1)) { return false; } @@ -3715,6 +3849,8 @@ HeadsetInterface lovrHeadsetOpenXRDriver = { .getRefreshRate = openxr_getRefreshRate, .setRefreshRate = openxr_setRefreshRate, .getRefreshRates = openxr_getRefreshRates, + .getFoveation = openxr_getFoveation, + .setFoveation = openxr_setFoveation, .getPassthrough = openxr_getPassthrough, .setPassthrough = openxr_setPassthrough, .isPassthroughSupported = openxr_isPassthroughSupported, diff --git a/src/modules/headset/headset_simulator.c b/src/modules/headset/headset_simulator.c index c5d7ee472..d7d6abf6d 100644 --- a/src/modules/headset/headset_simulator.c +++ b/src/modules/headset/headset_simulator.c @@ -191,6 +191,15 @@ static const float* simulator_getRefreshRates(uint32_t* count) { return *count = 0, NULL; } +static void simulator_getFoveation(FoveationLevel* level, bool* dynamic) { + *level = FOVEATION_NONE; + *dynamic = false; +} + +static bool simulator_setFoveation(FoveationLevel level, bool dynamic) { + return level == FOVEATION_NONE; +} + static PassthroughMode simulator_getPassthrough(void) { return PASSTHROUGH_OPAQUE; } @@ -451,7 +460,7 @@ static bool simulator_getPass(Pass** pass) { } CanvasTexture color[4] = { [0].texture = state.texture }; - if (!lovrPassSetCanvas(state.pass, color, NULL, state.depthFormat, state.config.antialias ? 4 : 1)) { + if (!lovrPassSetCanvas(state.pass, color, NULL, state.depthFormat, NULL, state.config.antialias ? 4 : 1)) { return false; } } @@ -606,6 +615,8 @@ HeadsetInterface lovrHeadsetSimulatorDriver = { .getRefreshRate = simulator_getRefreshRate, .setRefreshRate = simulator_setRefreshRate, .getRefreshRates = simulator_getRefreshRates, + .getFoveation = simulator_getFoveation, + .setFoveation = simulator_setFoveation, .getPassthrough = simulator_getPassthrough, .setPassthrough = simulator_setPassthrough, .isPassthroughSupported = simulator_isPassthroughSupported,