From 1f1e2bf3427d11b6d3dcbf5364171dd96555178b Mon Sep 17 00:00:00 2001 From: Joshua Ashton Date: Sun, 24 Sep 2023 15:43:21 +0100 Subject: [PATCH 1/3] layer: Implement VK_GOOGLE_display_timing + present wait/id for nested Required a decent refactor of the protocol to be more swapchain based. Tested with Dota 2 + `-vulkan -vulkan_enable_google_display_timing` Should hopefully improve latency/pacing in nested a good amount too. --- layer/VkLayer_FROG_gamescope_wsi.cpp | 281 ++++++++++++++++------- layer/VkLayer_FROG_gamescope_wsi.json.in | 10 + protocol/gamescope-swapchain.xml | 185 +++++++++++++++ protocol/gamescope-xwayland.xml | 99 +------- protocol/meson.build | 1 + src/drm.cpp | 2 +- src/rendervulkan.cpp | 70 +++++- src/rendervulkan.hpp | 1 + src/steamcompmgr.cpp | 158 ++++++++++--- src/steamcompmgr.hpp | 3 +- src/vblankmanager.cpp | 62 +++-- src/vblankmanager.hpp | 10 + src/wlserver.cpp | 193 +++++++++++++--- src/wlserver.hpp | 16 +- src/xwayland_ctx.hpp | 10 +- 15 files changed, 843 insertions(+), 258 deletions(-) create mode 100644 protocol/gamescope-swapchain.xml diff --git a/layer/VkLayer_FROG_gamescope_wsi.cpp b/layer/VkLayer_FROG_gamescope_wsi.cpp index 8d179156e..b473a983a 100644 --- a/layer/VkLayer_FROG_gamescope_wsi.cpp +++ b/layer/VkLayer_FROG_gamescope_wsi.cpp @@ -3,7 +3,7 @@ #define VK_USE_PLATFORM_XLIB_KHR #include "vkroots.h" #include "xcb_helpers.hpp" -#include "gamescope-xwayland-client-protocol.h" +#include "gamescope-swapchain-client-protocol.h" #include "../src/color_helpers.h" #include "../src/layer_defines.h" @@ -14,6 +14,7 @@ #include // For limiter file. +#include #include #include @@ -21,6 +22,16 @@ using namespace std::literals; namespace GamescopeWSILayer { + static uint64_t timespecToNanos(struct timespec& spec) { + return spec.tv_sec * 1'000'000'000ul + spec.tv_nsec; + } + + [[maybe_unused]] static uint64_t getTimeMonotonic() { + timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return timespecToNanos(ts); + } + static bool contains(const std::vector vec, std::string_view lookupValue) { return std::find_if(vec.begin(), vec.end(), [=](const char* value) { return value == lookupValue; }) != vec.end(); @@ -58,12 +69,13 @@ namespace GamescopeWSILayer { struct GamescopeInstanceData { wl_display* display; wl_compositor* compositor; - gamescope_xwayland* gamescope; + gamescope_swapchain_factory* gamescopeSwapchainFactory; }; VKROOTS_DEFINE_SYNCHRONIZED_MAP_TYPE(GamescopeInstance, VkInstance); struct GamescopeSurfaceData { VkInstance instance; + wl_display *display; VkSurfaceKHR fallbackSurface; wl_surface* surface; @@ -116,12 +128,55 @@ namespace GamescopeWSILayer { VKROOTS_DEFINE_SYNCHRONIZED_MAP_TYPE(GamescopeSurface, VkSurfaceKHR); struct GamescopeSwapchainData { + gamescope_swapchain *object; + wl_display* display; VkSurfaceKHR surface; // Always the Gamescope Surface surface -- so the Wayland one. bool isBypassingXWayland; VkPresentModeKHR presentMode; VkPresentModeKHR originalPresentMode; + + std::unique_ptr presentTimingMutex = std::make_unique(); + std::vector pastPresentTimings; + uint64_t refreshCycle = 16'666'666; }; VKROOTS_DEFINE_SYNCHRONIZED_MAP_TYPE(GamescopeSwapchain, VkSwapchainKHR); + static constexpr gamescope_swapchain_listener s_swapchainListener = { + .past_present_timing = []( + void *data, + gamescope_swapchain *object, + uint32_t present_id, + uint32_t desired_present_time_hi, + uint32_t desired_present_time_lo, + uint32_t actual_present_time_hi, + uint32_t actual_present_time_lo, + uint32_t earliest_present_time_hi, + uint32_t earliest_present_time_lo, + uint32_t present_margin_hi, + uint32_t present_margin_lo) { + GamescopeSwapchainData *swapchain = reinterpret_cast(data); + std::unique_lock lock(*swapchain->presentTimingMutex); + swapchain->pastPresentTimings.emplace_back(VkPastPresentationTimingGOOGLE { + .presentID = present_id, + .desiredPresentTime = (uint64_t(desired_present_time_hi) << 32) | desired_present_time_lo, + .actualPresentTime = (uint64_t(actual_present_time_hi) << 32) | actual_present_time_lo, + .earliestPresentTime = (uint64_t(earliest_present_time_hi) << 32) | earliest_present_time_lo, + .presentMargin = (uint64_t(present_margin_hi) << 32) | present_margin_lo + }); + }, + + .refresh_cycle = []( + void *data, + gamescope_swapchain *object, + uint32_t refresh_cycle_hi, + uint32_t refresh_cycle_lo) { + GamescopeSwapchainData *swapchain = reinterpret_cast(data); + { + std::unique_lock lock(*swapchain->presentTimingMutex); + swapchain->refreshCycle = (uint64_t(refresh_cycle_hi) << 32) | refresh_cycle_lo; + } + fprintf(stderr, "[Gamescope WSI] Swapchain recieved new refresh cycle: %.2fms\n", swapchain->refreshCycle / 1'000'000.0); + } + }; class VkInstanceOverrides { public: @@ -168,8 +223,10 @@ namespace GamescopeWSILayer { }); wl_registry_add_listener(registry, &s_registryListener, reinterpret_cast(state.get())); } + // Dispatch then roundtrip to get registry info. wl_display_dispatch(display); wl_display_roundtrip(display); + wl_registry_destroy(registry); return result; @@ -413,13 +470,6 @@ namespace GamescopeWSILayer { if (auto prop = xcb::getPropertyValue(connection, "GAMESCOPE_HDR_OUTPUT_FEEDBACK"sv)) hdrOutput = !!*prop; - auto serverId = xcb::getPropertyValue(connection, "GAMESCOPE_XWAYLAND_SERVER_ID"sv); - if (!serverId) { - fprintf(stderr, "[Gamescope WSI] Failed to get Xwayland server id. Failing surface creation.\n"); - return VK_ERROR_SURFACE_LOST_KHR; - } - gamescope_xwayland_override_window_content2(gamescopeInstance->gamescope, waylandSurface, *serverId, window); - wl_display_flush(gamescopeInstance->display); VkWaylandSurfaceCreateInfoKHR waylandCreateInfo = { @@ -453,6 +503,7 @@ namespace GamescopeWSILayer { fprintf(stderr, "[Gamescope WSI] Made gamescope surface for xid: 0x%x\n", window); auto gamescopeSurface = GamescopeSurface::create(*pSurface, GamescopeSurfaceData { .instance = instance, + .display = gamescopeInstance->display, .fallbackSurface = fallbackSurface, .surface = waylandSurface, .connection = connection, @@ -536,9 +587,9 @@ namespace GamescopeWSILayer { if (interface == "wl_compositor"sv) { instance->compositor = reinterpret_cast( wl_registry_bind(registry, name, &wl_compositor_interface, version)); - } else if (interface == "gamescope_xwayland"sv) { - instance->gamescope = reinterpret_cast( - wl_registry_bind(registry, name, &gamescope_xwayland_interface, version)); + } else if (interface == "gamescope_swapchain_factory"sv) { + instance->gamescopeSwapchainFactory = reinterpret_cast( + wl_registry_bind(registry, name, &gamescope_swapchain_factory_interface, version)); } }, .global_remove = [](void* data, wl_registry* registry, uint32_t name) { @@ -554,6 +605,9 @@ namespace GamescopeWSILayer { VkDevice device, VkSwapchainKHR swapchain, const VkAllocationCallbacks* pAllocator) { + if (auto state = GamescopeSwapchain::get(swapchain)) { + gamescope_swapchain_destroy(state->object); + } GamescopeSwapchain::remove(swapchain); pDispatch->DestroySwapchainKHR(device, swapchain, pAllocator); } @@ -582,18 +636,15 @@ namespace GamescopeWSILayer { if (!canBypass) swapchainInfo.surface = gamescopeSurface->fallbackSurface; - if (gamescopeSurface) { - // If this is a gamescope surface - // Force the colorspace to sRGB before sending to the driver. - swapchainInfo.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; - - fprintf(stderr, "[Gamescope WSI] Creating swapchain for xid: 0x%0x - minImageCount: %u - format: %s - colorspace: %s - flip: %s\n", - gamescopeSurface->window, - pCreateInfo->minImageCount, - vkroots::helpers::enumString(pCreateInfo->imageFormat), - vkroots::helpers::enumString(pCreateInfo->imageColorSpace), - canBypass ? "true" : "false"); - } + // Force the colorspace to sRGB before sending to the driver. + swapchainInfo.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + + fprintf(stderr, "[Gamescope WSI] Creating swapchain for xid: 0x%0x - minImageCount: %u - format: %s - colorspace: %s - flip: %s\n", + gamescopeSurface->window, + pCreateInfo->minImageCount, + vkroots::helpers::enumString(pCreateInfo->imageFormat), + vkroots::helpers::enumString(pCreateInfo->imageColorSpace), + canBypass ? "true" : "false"); // Check for VkFormat support and return VK_ERROR_INITIALIZATION_FAILED // if that VkFormat is unsupported for the underlying surface. @@ -622,41 +673,61 @@ namespace GamescopeWSILayer { } } + auto serverId = xcb::getPropertyValue(gamescopeSurface->connection, "GAMESCOPE_XWAYLAND_SERVER_ID"sv); + if (!serverId) { + fprintf(stderr, "[Gamescope WSI] Failed to get Xwayland server id. Failing swapchain creation.\n"); + return VK_ERROR_SURFACE_LOST_KHR; + } + + auto gamescopeInstance = GamescopeInstance::get(gamescopeSurface->instance); + if (!gamescopeInstance) { + fprintf(stderr, "[Gamescope WSI] CreateSwapchainKHR: Instance for swapchain was already destroyed. (App use after free).\n"); + return VK_ERROR_SURFACE_LOST_KHR; + } + VkResult result = pDispatch->CreateSwapchainKHR(device, &swapchainInfo, pAllocator, pSwapchain); - if (gamescopeSurface) { - if (result == VK_SUCCESS) { - GamescopeSwapchain::create(*pSwapchain, GamescopeSwapchainData{ - .surface = pCreateInfo->surface, // Always the Wayland side surface. - .isBypassingXWayland = canBypass, - .presentMode = swapchainInfo.presentMode, // The new present mode. - .originalPresentMode = originalPresentMode, - }); - - auto gamescopeInstance = GamescopeInstance::get(gamescopeSurface->instance); - if (gamescopeInstance) { - uint32_t imageCount = 0; - pDispatch->GetSwapchainImagesKHR(device, *pSwapchain, &imageCount, nullptr); - - fprintf(stderr, "[Gamescope WSI] Created swapchain for xid: 0x%0x - imageCount: %u\n", - gamescopeSurface->window, - imageCount); - - gamescope_xwayland_swapchain_feedback( - gamescopeInstance->gamescope, - gamescopeSurface->surface, - imageCount, - uint32_t(pCreateInfo->imageFormat), - uint32_t(pCreateInfo->imageColorSpace), - uint32_t(pCreateInfo->compositeAlpha), - uint32_t(pCreateInfo->preTransform), - uint32_t(pCreateInfo->presentMode), - uint32_t(pCreateInfo->clipped)); - } - } else { - fprintf(stderr, "[Gamescope WSI] Failed to create swapchain - vr: %s xid: 0x%x\n", vkroots::helpers::enumString(result), gamescopeSurface->window); - } + if (result != VK_SUCCESS) { + fprintf(stderr, "[Gamescope WSI] Failed to create swapchain - vr: %s xid: 0x%x\n", vkroots::helpers::enumString(result), gamescopeSurface->window); + return result; } - return result; + + gamescope_swapchain *gamescopeSwapchainObject = gamescope_swapchain_factory_create_swapchain( + gamescopeInstance->gamescopeSwapchainFactory, + gamescopeSurface->surface); + + gamescope_swapchain_override_window_content(gamescopeSwapchainObject, *serverId, gamescopeSurface->window); + + { + auto gamescopeSwapchain = GamescopeSwapchain::create(*pSwapchain, GamescopeSwapchainData{ + .object = gamescopeSwapchainObject, + .display = gamescopeInstance->display, + .surface = pCreateInfo->surface, // Always the Wayland side surface. + .isBypassingXWayland = canBypass, + .presentMode = swapchainInfo.presentMode, // The new present mode. + .originalPresentMode = originalPresentMode, + }); + + gamescope_swapchain_add_listener(gamescopeSwapchainObject, &s_swapchainListener, reinterpret_cast(gamescopeSwapchain.get())); + } + + uint32_t imageCount = 0; + pDispatch->GetSwapchainImagesKHR(device, *pSwapchain, &imageCount, nullptr); + + fprintf(stderr, "[Gamescope WSI] Created swapchain for xid: 0x%0x - imageCount: %u\n", + gamescopeSurface->window, + imageCount); + + gamescope_swapchain_swapchain_feedback( + gamescopeSwapchainObject, + imageCount, + uint32_t(pCreateInfo->imageFormat), + uint32_t(pCreateInfo->imageColorSpace), + uint32_t(pCreateInfo->compositeAlpha), + uint32_t(pCreateInfo->preTransform), + uint32_t(pCreateInfo->presentMode), + uint32_t(pCreateInfo->clipped)); + + return VK_SUCCESS; } static VkResult QueuePresentKHR( @@ -665,6 +736,30 @@ namespace GamescopeWSILayer { const VkPresentInfoKHR* pPresentInfo) { const uint32_t limiterOverride = gamescopeFrameLimiterOverride(); + auto pPresentTimes = vkroots::FindInChain(pPresentInfo); + + for (uint32_t i = 0; i < pPresentInfo->swapchainCount; i++) { + if (auto gamescopeSwapchain = GamescopeSwapchain::get(pPresentInfo->pSwapchains[i])) { + if (pPresentTimes && pPresentTimes->pTimes) { + assert(pPresentTimes->swapchainCount == pPresentInfo->swapchainCount); + +#if GAMESCOPE_WSI_DISPLAY_TIMING_DEBUG + fprintf(stderr, "[Gamescope WSI] QueuePresentKHR: presentID: %u - desiredPresentTime: %lu - now: %lu\n", pPresentTimes->pTimes[i].presentID, pPresentTimes->pTimes[i].desiredPresentTime, getTimeMonotonic()); +#endif + gamescope_swapchain_set_present_time( + gamescopeSwapchain->object, + pPresentTimes->pTimes[i].presentID, + pPresentTimes->pTimes[i].desiredPresentTime >> 32, + pPresentTimes->pTimes[i].desiredPresentTime & 0xffffffff); + } + + // Dispatch then flush to ensure any of our callbacks that did Wayland-y stuff + // get their things flushed before QueuePresent's commit. + wl_display_dispatch(gamescopeSwapchain->display); + wl_display_flush(gamescopeSwapchain->display); + } + } + VkResult result = pDispatch->QueuePresentKHR(queue, pPresentInfo); for (uint32_t i = 0; i < pPresentInfo->swapchainCount; i++) { @@ -714,24 +809,9 @@ namespace GamescopeWSILayer { continue; } - auto gamescopeSurface = GamescopeSurface::get(gamescopeSwapchain->surface); - if (!gamescopeSurface) { - fprintf(stderr, "[Gamescope WSI] SetHdrMetadataEXT: Surface for swapchain %u was already destroyed. (App use after free).\n", i); - abort(); - continue; - } - - auto gamescopeInstance = GamescopeInstance::get(gamescopeSurface->instance); - if (!gamescopeInstance) { - fprintf(stderr, "[Gamescope WSI] SetHdrMetadataEXT: Instance for swapchain %u was already destroyed. (App use after free).\n", i); - abort(); - continue; - } - const VkHdrMetadataEXT& metadata = pMetadata[i]; - gamescope_xwayland_set_hdr_metadata( - gamescopeInstance->gamescope, - gamescopeSurface->surface, + gamescope_swapchain_set_hdr_metadata( + gamescopeSwapchain->object, color_xy_to_u16(metadata.displayPrimaryRed.x), color_xy_to_u16(metadata.displayPrimaryRed.y), color_xy_to_u16(metadata.displayPrimaryGreen.x), @@ -745,12 +825,59 @@ namespace GamescopeWSILayer { nits_to_u16(metadata.maxContentLightLevel), nits_to_u16(metadata.maxFrameAverageLightLevel)); - fprintf( stderr, "[Gamescope WSI] VkHdrMetadataEXT: mastering luminance min %f nits, max %f nits\n", metadata.minLuminance, metadata.maxLuminance ); - fprintf( stderr, "[Gamescope WSI] VkHdrMetadataEXT: maxContentLightLevel %f nits\n", metadata.maxContentLightLevel ); - fprintf( stderr, "[Gamescope WSI] VkHdrMetadataEXT: maxFrameAverageLightLevel %f nits\n", metadata.maxFrameAverageLightLevel ); + fprintf(stderr, "[Gamescope WSI] VkHdrMetadataEXT: mastering luminance min %f nits, max %f nits\n", metadata.minLuminance, metadata.maxLuminance); + fprintf(stderr, "[Gamescope WSI] VkHdrMetadataEXT: maxContentLightLevel %f nits\n", metadata.maxContentLightLevel); + fprintf(stderr, "[Gamescope WSI] VkHdrMetadataEXT: maxFrameAverageLightLevel %f nits\n", metadata.maxFrameAverageLightLevel); } } + static VkResult GetPastPresentationTimingGOOGLE( + const vkroots::VkDeviceDispatch* pDispatch, + VkDevice device, + VkSwapchainKHR swapchain, + uint32_t* pPresentationTimingCount, + VkPastPresentationTimingGOOGLE* pPresentationTimings) { + auto gamescopeSwapchain = GamescopeSwapchain::get(swapchain); + if (!gamescopeSwapchain) { + fprintf(stderr, "[Gamescope WSI] GetPastPresentationTimingGOOGLE: Not a gamescope swapchain.\n"); + return VK_ERROR_SURFACE_LOST_KHR; + } + + // Dispatch to get the latest timings. + wl_display_dispatch(gamescopeSwapchain->display); + + uint32_t originalCount = *pPresentationTimingCount; + + std::unique_lock(*gamescopeSwapchain->presentTimingMutex); + auto& timings = gamescopeSwapchain->pastPresentTimings; + + VkResult result = vkroots::helpers::array(timings, pPresentationTimingCount, pPresentationTimings); + // Erase those that we returned so we don't return them again. + timings.erase(timings.begin(), timings.begin() + originalCount); + + return result; + } + + static VkResult GetRefreshCycleDurationGOOGLE( + const vkroots::VkDeviceDispatch* pDispatch, + VkDevice device, + VkSwapchainKHR swapchain, + VkRefreshCycleDurationGOOGLE* pDisplayTimingProperties) { + auto gamescopeSwapchain = GamescopeSwapchain::get(swapchain); + if (!gamescopeSwapchain) { + fprintf(stderr, "[Gamescope WSI] GetRefreshCycleDurationGOOGLE: Not a gamescope swapchain.\n"); + return VK_ERROR_SURFACE_LOST_KHR; + } + + // Dispatch to get the latest cycle. + wl_display_dispatch(gamescopeSwapchain->display); + + std::unique_lock(*gamescopeSwapchain->presentTimingMutex); + pDisplayTimingProperties->refreshDuration = gamescopeSwapchain->refreshCycle; + + return VK_SUCCESS; + } + }; } diff --git a/layer/VkLayer_FROG_gamescope_wsi.json.in b/layer/VkLayer_FROG_gamescope_wsi.json.in index d995a7a16..8ee5892b7 100644 --- a/layer/VkLayer_FROG_gamescope_wsi.json.in +++ b/layer/VkLayer_FROG_gamescope_wsi.json.in @@ -10,6 +10,16 @@ "functions": { "vkNegotiateLoaderLayerInterfaceVersion": "vkNegotiateLoaderLayerInterfaceVersion" }, + "device_extensions": [ + { + "name" : "VK_GOOGLE_display_timing", + "spec_version" : "1", + "entrypoints": [ + "vkGetPastPresentationTimingGOOGLE", + "vkGetRefreshCycleDurationGOOGLE" + ] + } + ], "enable_environment": { "ENABLE_GAMESCOPE_WSI": "1" }, diff --git a/protocol/gamescope-swapchain.xml b/protocol/gamescope-swapchain.xml new file mode 100644 index 000000000..6568f0028 --- /dev/null +++ b/protocol/gamescope-swapchain.xml @@ -0,0 +1,185 @@ + + + + + Copyright © 2023 Joshua Ashton for Valve Software + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This is a private Gamescope protocol. Regular Wayland clients must not use + it. + + + + + + + + + + + + + + + + + + + + Xwayland creates a wl_surface for each X11 window. It sends a + WL_SURFACE_ID client message to indicate the mapping between the X11 + windows and the wl_surface objects. + + This request overrides this mapping for a given X11 window, allowing an + X11 client to submit buffers via the Wayland protocol. The override + only affects buffer submission. Everything else (e.g. input events) + still uses Xwayland's WL_SURFACE_ID. + + x11_server is gotten by the GAMESCOPE_XWAYLAND_SERVER_ID property on the + root window of the associated server. + + + + + + + + Provide swapchain feedback to the compositor. + + This is what the useless tearing protocol should have been. + Absolutely not enough information in the final protocol to do what we want for SteamOS -- + which is have the Allow Tearing toggle apply to *both* Mailbox + Immediate and NOT fifo, + essentially acting as an override for tearing on/off for games. + The upstream protocol is very useless for our usecase here. + + Provides image count ahead of time instead of needing to try and calculate it from + an initial stall if we are doing low latency. + + Provides colorspace info for us to do HDR for both HDR10 PQ and scRGB. + The upstream HDR efforts seem to have no interest in supporting scRGB but we *need* that so /shrug + We can do it here now! Yipee! + + Swapchain feedback solves so many problems! :D + + + + + + + + + + + + + Forward HDR metadata from Vulkan to the compositor. + + HDR Metadata Infoframe as per CTA 861.G spec. + This is expected to match exactly with the spec. + + display_primary_*: + Color Primaries of the Data. + Specifies X and Y coordinates. + These are coded as unsigned 16-bit values in units of + 0.00002, where 0x0000 represents zero and 0xC350 + represents 1.0000. + + white_point_*: + White Point of Colorspace Data. + Specifies X and Y coordinates. + These are coded as unsigned 16-bit values in units of + 0.00002, where 0x0000 represents zero and 0xC350 + represents 1.0000. + + max_display_mastering_luminance: + Max Mastering Display Luminance. + This value is coded as an unsigned 16-bit value in units of 1 cd/m2, + where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. + + max_display_mastering_luminance: + Min Mastering Display Luminance. + This value is coded as an unsigned 16-bit value in units of + 0.0001 cd/m2, where 0x0001 represents 0.0001 cd/m2 and 0xFFFF + represents 6.5535 cd/m2. + + max_cll: + Max Content Light Level. + This value is coded as an unsigned 16-bit value in units of 1 cd/m2, + where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. + + max_fall: + Max Frame Average Light Level. + This value is coded as an unsigned 16-bit value in units of 1 cd/m2, + where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. + + + + + + + + + + + + + + + + + + Sets the display timing of the next commit. + + This gets reset to 0s in the compositor's state after a commit. + + + + + + + + + Gives information on the past presentation timing + + + + + + + + + + + + + + + Gives information on the refresh cycle for this swapchain + + + + + + diff --git a/protocol/gamescope-xwayland.xml b/protocol/gamescope-xwayland.xml index 03e48d8ad..4caacc047 100644 --- a/protocol/gamescope-xwayland.xml +++ b/protocol/gamescope-xwayland.xml @@ -27,20 +27,14 @@ This is a private Gamescope protocol. Regular Wayland clients must not use it. + + This protocol has been superceded by the 'gamescope-swapchain' protocol. - + - - See override_window_content2. - - - - - - Xwayland creates a wl_surface for each X11 window. It sends a WL_SURFACE_ID client message to indicate the mapping between the X11 @@ -55,94 +49,7 @@ root window of the associated server. - - - - - Provide swapchain feedback to the compositor. - - This is what the useless tearing protocol should have been. - Absolutely not enough information in the final protocol to do what we want for SteamOS -- - which is have the Allow Tearing toggle apply to *both* Mailbox + Immediate and NOT fifo, - essentially acting as an override for tearing on/off for games. - The upstream protocol is very useless for our usecase here. - - Provides image count ahead of time instead of needing to try and calculate it from - an initial stall if we are doing low latency. - - Provides colorspace info for us to do HDR for both HDR10 PQ and scRGB. - The upstream HDR efforts seem to have no interest in supporting scRGB but we *need* that so /shrug - We can do it here now! Yipee! - - Swapchain feedback solves so many problems! :D - - - - - - - - - - - - - - Forward HDR metadata from Vulkan to the compositor. - - HDR Metadata Infoframe as per CTA 861.G spec. - This is expected to match exactly with the spec. - - display_primary_*: - Color Primaries of the Data. - Specifies X and Y coordinates. - These are coded as unsigned 16-bit values in units of - 0.00002, where 0x0000 represents zero and 0xC350 - represents 1.0000. - - white_point_*: - White Point of Colorspace Data. - Specifies X and Y coordinates. - These are coded as unsigned 16-bit values in units of - 0.00002, where 0x0000 represents zero and 0xC350 - represents 1.0000. - - max_display_mastering_luminance: - Max Mastering Display Luminance. - This value is coded as an unsigned 16-bit value in units of 1 cd/m2, - where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. - - max_display_mastering_luminance: - Min Mastering Display Luminance. - This value is coded as an unsigned 16-bit value in units of - 0.0001 cd/m2, where 0x0001 represents 0.0001 cd/m2 and 0xFFFF - represents 6.5535 cd/m2. - - max_cll: - Max Content Light Level. - This value is coded as an unsigned 16-bit value in units of 1 cd/m2, - where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. - - max_fall: - Max Frame Average Light Level. - This value is coded as an unsigned 16-bit value in units of 1 cd/m2, - where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. - - - - - - - - - - - - - - - diff --git a/protocol/meson.build b/protocol/meson.build index a0d4ea3bd..5583217f6 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -8,6 +8,7 @@ protocols = [ 'gamescope-input-method', 'gamescope-tearing-control-unstable-v1', 'gamescope-control', + 'gamescope-swapchain', 'xdg-shell', 'presentation-time', ] diff --git a/src/drm.cpp b/src/drm.cpp index ef7a7a661..5771f978d 100644 --- a/src/drm.cpp +++ b/src/drm.cpp @@ -1646,7 +1646,7 @@ int drm_commit(struct drm_t *drm, const struct FrameInfo_t *frameInfo ) // is queued and would end up being the new page flip, rather than here. // However, the page flip handler is called when the page flip occurs, // not when it is successfully queued. - g_uVblankDrawTimeNS = get_time_in_nanos() - g_SteamCompMgrVBlankTime; + g_uVblankDrawTimeNS = get_time_in_nanos() - g_SteamCompMgrVBlankTime.pipe_write_time; if ( isPageFlip ) { // Wait for flip handler to unlock diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp index b4efbbeea..e4f5aea9a 100644 --- a/src/rendervulkan.cpp +++ b/src/rendervulkan.cpp @@ -473,6 +473,9 @@ bool CVulkanDevice::createDevice() { enabledExtensions.push_back( VK_KHR_SWAPCHAIN_EXTENSION_NAME ); enabledExtensions.push_back( VK_KHR_SWAPCHAIN_MUTABLE_FORMAT_EXTENSION_NAME ); + + enabledExtensions.push_back( VK_KHR_PRESENT_ID_EXTENSION_NAME ); + enabledExtensions.push_back( VK_KHR_PRESENT_WAIT_EXTENSION_NAME ); } if ( m_bSupportsModifiers ) @@ -511,9 +514,21 @@ bool CVulkanDevice::createDevice() .dynamicRendering = VK_TRUE, }; + VkPhysicalDevicePresentWaitFeaturesKHR presentWaitFeatures = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_WAIT_FEATURES_KHR, + .pNext = &features13, + .presentWait = VK_TRUE, + }; + + VkPhysicalDevicePresentIdFeaturesKHR presentIdFeatures = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR, + .pNext = &presentWaitFeatures, + .presentId = VK_TRUE, + }; + VkPhysicalDeviceFeatures2 features2 = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, - .pNext = &features13, + .pNext = &presentIdFeatures, .features = { .shaderInt16 = m_bSupportsFp16, }, @@ -2517,18 +2532,58 @@ bool acquire_next_image( void ) return g_device.vk.ResetFences( g_device.device(), 1, &g_output.acquireFence ) == VK_SUCCESS; } + +static std::atomic g_currentPresentWaitId = {0u}; +static std::mutex present_wait_lock; + +static void present_wait_thread_func( void ) +{ + uint64_t present_wait_id = 0; + + while (true) + { + g_currentPresentWaitId.wait(present_wait_id); + + // Lock to make sure swapchain destruction is waited on and that + // it's for this swapchain. + { + std::unique_lock lock(present_wait_lock); + present_wait_id = g_currentPresentWaitId.load(); + + if (present_wait_id != 0) + { + g_device.vk.WaitForPresentKHR( g_device.device(), g_output.swapChain, present_wait_id, 1'000'000'000lu ); + vblank_mark_possible_vblank( get_time_in_nanos() ); + } + } + } +} + void vulkan_present_to_window( void ) { + static uint64_t s_lastPresentId = 0; + + uint64_t presentId = ++s_lastPresentId; + + VkPresentIdKHR presentIdInfo = { + .sType = VK_STRUCTURE_TYPE_PRESENT_ID_KHR, + .swapchainCount = 1, + .pPresentIds = &presentId, + }; + VkPresentInfoKHR presentInfo = { .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, + .pNext = &presentIdInfo, .swapchainCount = 1, .pSwapchains = &g_output.swapChain, .pImageIndices = &g_output.nOutImage, }; - if ( g_device.vk.QueuePresentKHR( g_device.queue(), &presentInfo ) != VK_SUCCESS ) + if ( g_device.vk.QueuePresentKHR( g_device.queue(), &presentInfo ) == VK_SUCCESS ) + g_currentPresentWaitId = presentId; + else vulkan_remake_swapchain(); - + while ( !acquire_next_image() ) vulkan_remake_swapchain(); } @@ -2698,6 +2753,9 @@ bool vulkan_make_swapchain( VulkanOutput_t *pOutput ) bool vulkan_remake_swapchain( void ) { + std::unique_lock lock(present_wait_lock); + g_currentPresentWaitId = 0; + VulkanOutput_t *pOutput = &g_output; g_device.waitIdle(); g_device.vk.QueueWaitIdle( g_device.queue() ); @@ -3002,6 +3060,12 @@ bool vulkan_init( VkInstance instance, VkSurfaceKHR surface ) if (!init_nis_data()) return false; + if (BIsNested() && !BIsVRSession()) + { + std::thread present_wait_thread( present_wait_thread_func ); + present_wait_thread.detach(); + } + return true; } diff --git a/src/rendervulkan.hpp b/src/rendervulkan.hpp index 89c66873c..0db56b4c7 100644 --- a/src/rendervulkan.hpp +++ b/src/rendervulkan.hpp @@ -654,6 +654,7 @@ static inline uint32_t div_roundup(uint32_t x, uint32_t y) VK_FUNC(UnmapMemory) \ VK_FUNC(UpdateDescriptorSets) \ VK_FUNC(WaitForFences) \ + VK_FUNC(WaitForPresentKHR) \ VK_FUNC(WaitSemaphores) class CVulkanDevice diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp index eb59347eb..126ca5a09 100644 --- a/src/steamcompmgr.cpp +++ b/src/steamcompmgr.cpp @@ -130,6 +130,7 @@ extern float g_flHDRItmTargetNits; extern std::atomic g_lastVblank; + std::string clipboard; std::string primarySelection; @@ -141,6 +142,9 @@ uint64_t timespec_to_nanos(struct timespec& spec) return spec.tv_sec * 1'000'000'000ul + spec.tv_nsec; } +static uint64_t g_SteamCompMgrLimitedAppRefreshCycle = 16'666'666; +static uint64_t g_SteamCompMgrAppRefreshCycle = 16'666'666; + static const gamescope_color_mgmt_t k_ScreenshotColorMgmt = { .enabled = true, @@ -636,6 +640,11 @@ struct commit_t struct wlr_surface *surf = nullptr; std::vector presentation_feedbacks; + + std::optional present_id = std::nullopt; + uint64_t desired_present_time = 0; + uint64_t earliest_present_time = 0; + uint64_t present_margin = 0; }; static std::vector pollfds; @@ -735,7 +744,7 @@ bool synchronize; std::mutex g_SteamCompMgrXWaylandServerMutex; -uint64_t g_SteamCompMgrVBlankTime = 0; +VBlankTimeInfo_t g_SteamCompMgrVBlankTime = {}; static int g_nSteamCompMgrTargetFPS = 0; static uint64_t g_uDynamicRefreshEqualityTime = 0; @@ -876,6 +885,7 @@ struct WaitListEntry_t // steamcompmgr thread in handle_done_commits, it is worth it. bool mangoapp_nudge; uint64_t commitID; + uint64_t desiredPresentTime; }; sem waitListSem; @@ -937,7 +947,7 @@ void imageWaitThreadMain( void ) { std::unique_lock< std::mutex > lock( entry.doneCommits->listCommitsDoneLock ); - entry.doneCommits->listCommitsDone.push_back( entry.commitID ); + entry.doneCommits->listCommitsDone.push_back( CommitDoneEntry_t{ entry.commitID, entry.desiredPresentTime } ); } nudge_steamcompmgr(); @@ -1185,7 +1195,7 @@ destroy_buffer( struct wl_listener *listener, void * ) } static std::shared_ptr -import_commit ( struct wlr_surface *surf, struct wlr_buffer *buf, bool async, std::shared_ptr swapchain_feedback, std::vector presentation_feedbacks ) +import_commit ( struct wlr_surface *surf, struct wlr_buffer *buf, bool async, std::shared_ptr swapchain_feedback, std::vector presentation_feedbacks, std::optional present_id, uint64_t desired_present_time ) { std::shared_ptr commit = std::make_shared(); std::unique_lock lock( wlr_buffer_map_lock ); @@ -1196,6 +1206,8 @@ import_commit ( struct wlr_surface *surf, struct wlr_buffer *buf, bool async, st commit->presentation_feedbacks = std::move(presentation_feedbacks); if (swapchain_feedback) commit->feedback = *swapchain_feedback; + commit->present_id = present_id; + commit->desired_present_time = desired_present_time; auto it = wlr_buffer_map.find( buf ); if ( it != wlr_buffer_map.end() ) @@ -2571,9 +2583,9 @@ paint_all(bool async) { vulkan_present_to_window(); } - // Update the time it took us to present. - // TODO: Use Vulkan present timing in future. - g_uVblankDrawTimeNS = get_time_in_nanos() - g_SteamCompMgrVBlankTime; + + // Update the time it took us to commit + g_uVblankDrawTimeNS = get_time_in_nanos() - g_SteamCompMgrVBlankTime.pipe_write_time; } else { @@ -5927,7 +5939,7 @@ register_systray(xwayland_ctx_t *ctx) XSetSelectionOwner(ctx->dpy, net_system_tray, ctx->ourWindow, 0); } -bool handle_done_commit( steamcompmgr_win_t *w, xwayland_ctx_t *ctx, uint64_t commitID ) +bool handle_done_commit( steamcompmgr_win_t *w, xwayland_ctx_t *ctx, uint64_t commitID, uint64_t earliestPresentTime, uint64_t earliestLatchTime ) { bool bFoundWindow = false; uint32_t j; @@ -5937,6 +5949,8 @@ bool handle_done_commit( steamcompmgr_win_t *w, xwayland_ctx_t *ctx, uint64_t co { gpuvis_trace_printf( "commit %lu done", w->commit_queue[ j ]->commitID ); w->commit_queue[ j ]->done = true; + w->commit_queue[ j ]->earliest_present_time = earliestPresentTime; + w->commit_queue[ j ]->present_margin = earliestPresentTime - earliestLatchTime; bFoundWindow = true; // Window just got a new available commit, determine if that's worth a repaint @@ -6009,56 +6023,135 @@ bool handle_done_commit( steamcompmgr_win_t *w, xwayland_ctx_t *ctx, uint64_t co return false; } +// TODO: Merge these two functions. void handle_done_commits_xwayland( xwayland_ctx_t *ctx ) { std::lock_guard lock( ctx->doneCommits.listCommitsDoneLock ); + uint64_t next_refresh_time = g_SteamCompMgrVBlankTime.target_vblank_time; + + // commits that were not ready to be presented based on their display timing. + std::vector< CommitDoneEntry_t > commits_before_their_time; + + uint64_t now = get_time_in_nanos(); + // very fast loop yes - for ( uint32_t i = 0; i < ctx->doneCommits.listCommitsDone.size(); i++ ) + for ( auto& entry : ctx->doneCommits.listCommitsDone ) { + if (!entry.earliestPresentTime) + { + entry.earliestPresentTime = next_refresh_time; + entry.earliestLatchTime = now; + } + + if ( entry.desiredPresentTime > next_refresh_time ) + { + commits_before_their_time.push_back( entry ); + break; + } + for ( steamcompmgr_win_t *w = ctx->list; w; w = w->xwayland().next ) { - if (handle_done_commit(w, ctx, ctx->doneCommits.listCommitsDone[i])) + if (handle_done_commit(w, ctx, entry.commitID, entry.earliestPresentTime, entry.earliestLatchTime)) break; } } - ctx->doneCommits.listCommitsDone.clear(); + ctx->doneCommits.listCommitsDone = std::move( commits_before_their_time ); } void handle_done_commits_xdg() { std::lock_guard lock( g_steamcompmgr_xdg_done_commits.listCommitsDoneLock ); + uint64_t next_refresh_time = g_SteamCompMgrVBlankTime.target_vblank_time; + + // commits that were not ready to be presented based on their display timing. + std::vector< CommitDoneEntry_t > commits_before_their_time; + + uint64_t now = get_time_in_nanos(); + // very fast loop yes - for ( uint32_t i = 0; i < g_steamcompmgr_xdg_done_commits.listCommitsDone.size(); i++ ) + for ( auto& entry : g_steamcompmgr_xdg_done_commits.listCommitsDone ) { + if (!entry.earliestPresentTime) + { + entry.earliestPresentTime = next_refresh_time; + entry.earliestLatchTime = now; + } + + if ( entry.desiredPresentTime > next_refresh_time ) + { + commits_before_their_time.push_back( entry ); + break; + } + for (const auto& xdg_win : g_steamcompmgr_xdg_wins) { - if (handle_done_commit(xdg_win.get(), nullptr, g_steamcompmgr_xdg_done_commits.listCommitsDone[i])) + if (handle_done_commit(xdg_win.get(), nullptr, entry.commitID, entry.earliestPresentTime, entry.earliestLatchTime)) break; } } - g_steamcompmgr_xdg_done_commits.listCommitsDone.clear(); + g_steamcompmgr_xdg_done_commits.listCommitsDone = std::move( commits_before_their_time ); } void handle_presented_for_window( steamcompmgr_win_t* w ) { + uint64_t next_refresh_time = g_SteamCompMgrVBlankTime.target_vblank_time; + + uint64_t refresh_cycle = g_nSteamCompMgrTargetFPS && steamcompmgr_window_should_limit_fps( w ) + ? g_SteamCompMgrLimitedAppRefreshCycle + : g_SteamCompMgrAppRefreshCycle; + commit_t *lastCommit = get_window_last_done_commit_peek(w); - if (lastCommit && !lastCommit->presentation_feedbacks.empty()) + if (lastCommit) { - wlserver_lock(); + if (!lastCommit->presentation_feedbacks.empty() || lastCommit->present_id) + { + wlserver_lock(); - int nRefresh = g_nNestedRefresh ? g_nNestedRefresh : g_nOutputRefresh; + if (!lastCommit->presentation_feedbacks.empty()) + { + wlserver_presentation_feedback_presented( + lastCommit->surf, + lastCommit->presentation_feedbacks, + next_refresh_time, + refresh_cycle); + } - wlserver_presentation_feedback_presented( - lastCommit->surf, - lastCommit->presentation_feedbacks, - g_lastVblank.load(), - nRefresh); + if (lastCommit->present_id) + { + wlserver_past_present_timing( + lastCommit->surf, + *lastCommit->present_id, + lastCommit->desired_present_time, + next_refresh_time, + lastCommit->earliest_present_time, + lastCommit->present_margin); + lastCommit->present_id = std::nullopt; + } - wlserver_unlock(); + wlserver_unlock(); + } + } + + if (struct wlr_surface *surface = w->current_surface()) + { + auto info = get_wl_surface_info(surface); + if (info->gamescope_swapchain != nullptr) + { + // Could have got the override set in this bubble.s + surface = w->current_surface(); + + if (info->last_refresh_cycle != refresh_cycle) + { + wlserver_lock(); + info->last_refresh_cycle = refresh_cycle; + wlserver_refresh_cycle(surface, refresh_cycle); + wlserver_unlock(); + } + } } } @@ -6142,7 +6235,7 @@ void update_wayland_res(CommitDoneList_t *doneCommits, steamcompmgr_win_t *w, Re return; } - std::shared_ptr newCommit = import_commit( reslistentry.surf, buf, reslistentry.async, std::move(reslistentry.feedback), std::move(reslistentry.presentation_feedbacks) ); + std::shared_ptr newCommit = import_commit( reslistentry.surf, buf, reslistentry.async, std::move(reslistentry.feedback), std::move(reslistentry.presentation_feedbacks), reslistentry.present_id, reslistentry.desired_present_time ); int fence = -1; if ( newCommit ) @@ -6170,6 +6263,7 @@ void update_wayland_res(CommitDoneList_t *doneCommits, steamcompmgr_win_t *w, Re .fence = fence, .mangoapp_nudge = mango_nudge, .commitID = newCommit->commitID, + .desiredPresentTime = newCommit->desired_present_time, }; waitList.push_back( entry ); } @@ -6534,7 +6628,7 @@ dispatch_vblank( int fd ) bool vblank = false; for (;;) { - uint64_t vblanktime = 0; + VBlankTimeInfo_t vblanktime = {}; ssize_t ret = read( fd, &vblanktime, sizeof( vblanktime ) ); if ( ret < 0 ) { @@ -6546,9 +6640,10 @@ dispatch_vblank( int fd ) } g_SteamCompMgrVBlankTime = vblanktime; - uint64_t diff = get_time_in_nanos() - vblanktime; - // give it 1 ms of slack.. maybe too long + uint64_t diff = get_time_in_nanos() - vblanktime.pipe_write_time; + + // give it 1 ms of slack from pipe to steamcompmgr... maybe too long if ( diff > 1'000'000ul ) { gpuvis_trace_printf( "ignored stale vblank" ); @@ -7488,6 +7583,17 @@ steamcompmgr_main(int argc, char **argv) } } + if ( vblank ) + { + int nRealRefresh = g_nNestedRefresh ? g_nNestedRefresh : g_nOutputRefresh; + int nTargetFPS = g_nSteamCompMgrTargetFPS ? g_nSteamCompMgrTargetFPS : nRealRefresh; + int nMultiplier = nRealRefresh / nTargetFPS; + + int nAppRefresh = nRealRefresh * nMultiplier; + g_SteamCompMgrAppRefreshCycle = 1'000'000'000ul / nRealRefresh; + g_SteamCompMgrLimitedAppRefreshCycle = 1'000'000'000ul / nAppRefresh; + } + // Handle presentation-time stuff // // Notes: diff --git a/src/steamcompmgr.hpp b/src/steamcompmgr.hpp index 676bb2938..89e621c19 100644 --- a/src/steamcompmgr.hpp +++ b/src/steamcompmgr.hpp @@ -17,6 +17,7 @@ void steamcompmgr_main(int argc, char **argv); #include "rendervulkan.hpp" #include "wlserver.hpp" +#include "vblankmanager.hpp" #include #include @@ -137,7 +138,7 @@ wlserver_vk_swapchain_feedback* steamcompmgr_get_base_layer_swapchain_feedback() struct wlserver_x11_surface_info *lookup_x11_surface_info_from_xid( gamescope_xwayland_server_t *xwayland_server, uint32_t xid ); -extern uint64_t g_SteamCompMgrVBlankTime; +extern VBlankTimeInfo_t g_SteamCompMgrVBlankTime; extern pid_t focusWindow_pid; void init_xwayland_ctx(uint32_t serverId, gamescope_xwayland_server_t *xwayland_server); diff --git a/src/vblankmanager.cpp b/src/vblankmanager.cpp index c4707b2b1..925fda3a9 100644 --- a/src/vblankmanager.cpp +++ b/src/vblankmanager.cpp @@ -64,6 +64,21 @@ const uint64_t g_uVBlankDrawTimeMinCompositing = 2'400'000; //#define VBLANK_DEBUG +uint64_t vblank_next_target( uint64_t offset ) +{ + const int refresh = g_nNestedRefresh ? g_nNestedRefresh : g_nOutputRefresh; + const uint64_t nsecInterval = 1'000'000'000ul / refresh; + + uint64_t lastVblank = g_lastVblank - offset; + + uint64_t now = get_time_in_nanos(); + uint64_t targetPoint = lastVblank + nsecInterval; + while ( targetPoint < now ) + targetPoint += nsecInterval; + + return targetPoint; +} + void vblankThreadRun( void ) { pthread_setname_np( pthread_self(), "gamescope-vblk" ); @@ -155,19 +170,17 @@ void vblankThreadRun( void ) lastOffset = offset; #endif - uint64_t lastVblank = g_lastVblank - offset; - - uint64_t now = get_time_in_nanos(); - uint64_t targetPoint = lastVblank + nsecInterval; - while ( targetPoint < now ) - targetPoint += nsecInterval; + uint64_t targetPoint = vblank_next_target( offset ); sleep_until_nanos( targetPoint ); - // give the time of vblank to steamcompmgr - uint64_t vblanktime = get_time_in_nanos(); + VBlankTimeInfo_t time_info = + { + .target_vblank_time = targetPoint + offset, + .pipe_write_time = get_time_in_nanos(), + }; - ssize_t ret = write( g_vblankPipe[ 1 ], &vblanktime, sizeof( vblanktime ) ); + ssize_t ret = write( g_vblankPipe[ 1 ], &time_info, sizeof( time_info ) ); if ( ret <= 0 ) { perror( "vblankmanager: write failed" ); @@ -182,22 +195,27 @@ void vblankThreadRun( void ) } } +#if HAVE_OPENVR void vblankThreadVR() { pthread_setname_np( pthread_self(), "gamescope-vblkvr" ); while ( true ) { -#if HAVE_OPENVR vrsession_wait_until_visible(); + // Includes redzone. vrsession_framesync( ~0u ); -#else - abort(); -#endif - uint64_t vblanktime = get_time_in_nanos(); - ssize_t ret = write( g_vblankPipe[ 1 ], &vblanktime, sizeof( vblanktime ) ); + uint64_t now = get_time_in_nanos(); + + VBlankTimeInfo_t time_info = + { + .target_vblank_time = now + 3'000'000, // not right. just a stop-gap for now. + .pipe_write_time = now, + }; + + ssize_t ret = write( g_vblankPipe[ 1 ], &time_info, sizeof( time_info ) ); if ( ret <= 0 ) { perror( "vblankmanager: write failed" ); @@ -208,7 +226,7 @@ void vblankThreadVR() } } } - +#endif int vblank_init( void ) { @@ -220,9 +238,17 @@ int vblank_init( void ) g_lastVblank = get_time_in_nanos(); - std::thread vblankThread( BIsVRSession() ? vblankThreadVR : vblankThreadRun ); - vblankThread.detach(); +#if HAVE_OPENVR + if ( BIsVRSession() ) + { + std::thread vblankThread( vblankThreadVR ); + vblankThread.detach(); + return g_vblankPipe[ 0 ]; + } +#endif + std::thread vblankThread( vblankThreadRun ); + vblankThread.detach(); return g_vblankPipe[ 0 ]; } diff --git a/src/vblankmanager.hpp b/src/vblankmanager.hpp index 1d65602ae..3c36ce07a 100644 --- a/src/vblankmanager.hpp +++ b/src/vblankmanager.hpp @@ -1,9 +1,19 @@ +#pragma once + // Try to figure out when vblank is and notify steamcompmgr to render some time before it +struct VBlankTimeInfo_t +{ + uint64_t target_vblank_time; + uint64_t pipe_write_time; +}; + int vblank_init( void ); void vblank_mark_possible_vblank( uint64_t nanos ); +uint64_t vblank_next_target( uint64_t offset = 0 ); + extern std::atomic g_uVblankDrawTimeNS; const unsigned int g_uDefaultVBlankRedZone = 1'650'000; diff --git a/src/wlserver.cpp b/src/wlserver.cpp index 667c749de..016a5b834 100644 --- a/src/wlserver.cpp +++ b/src/wlserver.cpp @@ -39,6 +39,7 @@ extern "C" { #include "gamescope-xwayland-protocol.h" #include "gamescope-pipewire-protocol.h" #include "gamescope-control-protocol.h" +#include "gamescope-swapchain-protocol.h" #include "gamescope-tearing-control-unstable-v1-protocol.h" #include "presentation-time-protocol.h" @@ -83,7 +84,7 @@ std::mutex g_wlserver_xdg_shell_windows_lock; static struct wl_list pending_surfaces = {0}; static void wlserver_x11_surface_info_set_wlr( struct wlserver_x11_surface_info *surf, struct wlr_surface *wlr_surf, bool override ); -static wlserver_wl_surface_info *get_wl_surface_info(struct wlr_surface *wlr_surf); +wlserver_wl_surface_info *get_wl_surface_info(struct wlr_surface *wlr_surf); std::vector gamescope_xwayland_server_t::retrieve_commits() { @@ -108,7 +109,11 @@ void gamescope_xwayland_server_t::wayland_commit(struct wlr_surface *surf, struc .async = wlserver_surface_is_async(surf), .feedback = wlserver_surface_swapchain_feedback(surf), .presentation_feedbacks = std::move(wl_surf->pending_presentation_feedbacks), + .present_id = wl_surf->present_id, + .desired_present_time = wl_surf->desired_present_time, }; + wl_surf->present_id = std::nullopt; + wl_surf->desired_present_time = 0; wl_surf->pending_presentation_feedbacks.clear(); wayland_commit_queue.push_back( newEntry ); } @@ -428,7 +433,7 @@ static void wlserver_new_input(struct wl_listener *listener, void *data) static struct wl_listener new_input_listener = { .notify = wlserver_new_input }; -static wlserver_wl_surface_info *get_wl_surface_info(struct wlr_surface *wlr_surf) +wlserver_wl_surface_info *get_wl_surface_info(struct wlr_surface *wlr_surf) { return reinterpret_cast(wlr_surf->data); } @@ -539,10 +544,8 @@ static void content_override_handle_surface_destroy( struct wl_listener *listene server->destroy_content_override( co ); } -void gamescope_xwayland_server_t::handle_override_window_content( struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface_resource, uint32_t x11_window ) +void gamescope_xwayland_server_t::handle_override_window_content( struct wl_client *client, struct wl_resource *resource, struct wlr_surface *surface, uint32_t x11_window ) { - struct wlr_surface *surface = wlr_surface_from_resource( surface_resource ); - if ( content_overrides.count( x11_window ) ) { destroy_content_override( content_overrides[ x11_window ] ); } @@ -573,6 +576,10 @@ struct wlr_output *gamescope_xwayland_server_t::get_output() return output; } + + + + static void gamescope_xwayland_handle_override_window_content( struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface_resource, uint32_t x11_window ) { // This should ideally use the surface's xwayland, but we don't know it. @@ -589,18 +596,68 @@ static void gamescope_xwayland_handle_override_window_content( struct wl_client // So... Just assume it comes from server 0 for now. gamescope_xwayland_server_t *server = wlserver_get_xwayland_server( 0 ); assert( server ); - server->handle_override_window_content(client, resource, surface_resource, x11_window); + struct wlr_surface *surface = wlr_surface_from_resource( surface_resource ); + server->handle_override_window_content(client, resource, surface, x11_window); } -static void gamescope_xwayland_handle_override_window_content2( struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface_resource, uint32_t server_id, uint32_t x11_window ) +static void gamescope_xwayland_handle_destroy( struct wl_client *client, struct wl_resource *resource ) { + wl_resource_destroy( resource ); +} + +static const struct gamescope_xwayland_interface gamescope_xwayland_impl = { + .destroy = gamescope_xwayland_handle_destroy, + .override_window_content = gamescope_xwayland_handle_override_window_content, +}; + +static void gamescope_xwayland_bind( struct wl_client *client, void *data, uint32_t version, uint32_t id ) +{ + struct wl_resource *resource = wl_resource_create( client, &gamescope_xwayland_interface, version, id ); + wl_resource_set_implementation( resource, &gamescope_xwayland_impl, NULL, NULL ); +} + +static void create_gamescope_xwayland( void ) +{ + uint32_t version = 1; + wl_global_create( wlserver.display, &gamescope_xwayland_interface, version, NULL, gamescope_xwayland_bind ); +} + + + + + + + + + + + + + +static void gamescope_swapchain_destroy( struct wl_client *client, struct wl_resource *resource ) +{ + wlserver_wl_surface_info *wl_surface_info = (wlserver_wl_surface_info *)wl_resource_get_user_data( resource ); + + wlserver_x11_surface_info *x11_surface = wl_surface_info->x11_surface; + if (x11_surface) + x11_surface->xwayland_server->destroy_content_override( x11_surface, wl_surface_info->wlr ); + + if (wl_surface_info->gamescope_swapchain == resource) + wl_surface_info->gamescope_swapchain = nullptr; + + wl_resource_destroy( resource ); +} + +static void gamescope_swapchain_override_window_content( struct wl_client *client, struct wl_resource *resource, uint32_t server_id, uint32_t x11_window ) +{ + wlserver_wl_surface_info *wl_surface_info = (wlserver_wl_surface_info *)wl_resource_get_user_data( resource ); + gamescope_xwayland_server_t *server = wlserver_get_xwayland_server( server_id ); assert( server ); - server->handle_override_window_content(client, resource, surface_resource, x11_window); + server->handle_override_window_content(client, resource, wl_surface_info->wlr, x11_window); } -static void gamescope_xwayland_handle_swapchain_feedback( struct wl_client *client, struct wl_resource *resource, - struct wl_resource *surface_resource, +static void gamescope_swapchain_swapchain_feedback( struct wl_client *client, struct wl_resource *resource, uint32_t image_count, uint32_t vk_format, uint32_t vk_colorspace, @@ -609,8 +666,7 @@ static void gamescope_xwayland_handle_swapchain_feedback( struct wl_client *clie uint32_t vk_present_mode, uint32_t vk_clipped) { - struct wlr_surface *surface = wlr_surface_from_resource( surface_resource ); - wlserver_wl_surface_info *wl_info = get_wl_surface_info( surface ); + wlserver_wl_surface_info *wl_info = (wlserver_wl_surface_info *)wl_resource_get_user_data( resource ); if ( wl_info ) { wl_info->swapchain_feedback = std::make_unique(wlserver_vk_swapchain_feedback{ @@ -626,8 +682,7 @@ static void gamescope_xwayland_handle_swapchain_feedback( struct wl_client *clie } } -static void gamescope_xwayland_handle_set_hdr_metadata( struct wl_client *client, struct wl_resource *resource, - struct wl_resource *surface_resource, +static void gamescope_swapchain_set_hdr_metadata( struct wl_client *client, struct wl_resource *resource, uint32_t display_primary_red_x, uint32_t display_primary_red_y, uint32_t display_primary_green_x, @@ -641,8 +696,7 @@ static void gamescope_xwayland_handle_set_hdr_metadata( struct wl_client *client uint32_t max_cll, uint32_t max_fall) { - struct wlr_surface *surface = wlr_surface_from_resource( surface_resource ); - wlserver_wl_surface_info *wl_info = get_wl_surface_info( surface ); + wlserver_wl_surface_info *wl_info = (wlserver_wl_surface_info *)wl_resource_get_user_data( resource ); if ( BIsNested() ) { wl_log.infof("Ignoring HDR metadata when nested."); @@ -685,31 +739,78 @@ static void gamescope_xwayland_handle_set_hdr_metadata( struct wl_client *client } } -static void gamescope_xwayland_handle_destroy( struct wl_client *client, struct wl_resource *resource ) +static void gamescope_swapchain_set_present_time( struct wl_client *client, struct wl_resource *resource, + uint32_t present_id, + uint32_t desired_present_time_hi, + uint32_t desired_present_time_lo) +{ + wlserver_wl_surface_info *wl_info = (wlserver_wl_surface_info *)wl_resource_get_user_data( resource ); + + if ( wl_info ) + { + wl_info->present_id = present_id; + wl_info->desired_present_time = (uint64_t(desired_present_time_hi) << 32) | desired_present_time_lo; + } +} + +static const struct gamescope_swapchain_interface gamescope_swapchain_impl = { + .destroy = gamescope_swapchain_destroy, + .override_window_content = gamescope_swapchain_override_window_content, + .swapchain_feedback = gamescope_swapchain_swapchain_feedback, + .set_hdr_metadata = gamescope_swapchain_set_hdr_metadata, + .set_present_time = gamescope_swapchain_set_present_time, +}; + +static void gamescope_swapchain_factory_destroy( struct wl_client *client, struct wl_resource *resource ) { wl_resource_destroy( resource ); } -static const struct gamescope_xwayland_interface gamescope_xwayland_impl = { - .destroy = gamescope_xwayland_handle_destroy, - .override_window_content = gamescope_xwayland_handle_override_window_content, - .override_window_content2 = gamescope_xwayland_handle_override_window_content2, - .swapchain_feedback = gamescope_xwayland_handle_swapchain_feedback, - .set_hdr_metadata = gamescope_xwayland_handle_set_hdr_metadata, +static void gamescope_swapchain_factory_create_swapchain( struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface_resource, uint32_t id ) +{ + struct wlr_surface *surface = wlr_surface_from_resource( surface_resource ); + + wlserver_wl_surface_info *wl_surface_info = get_wl_surface_info(surface); + + struct wl_resource *gamescope_swapchain_resource + = wl_resource_create( client, &gamescope_swapchain_interface, wl_resource_get_version( resource ), id ); + wl_resource_set_implementation( gamescope_swapchain_resource, &gamescope_swapchain_impl, wl_surface_info, NULL ); + + if (wl_surface_info->gamescope_swapchain != nullptr) + wl_log.errorf("create_swapchain: Surface already had a gamescope_swapchain! Overriding."); + + wl_surface_info->gamescope_swapchain = gamescope_swapchain_resource; +} + +static const struct gamescope_swapchain_factory_interface gamescope_swapchain_factory_impl = { + .destroy = gamescope_swapchain_factory_destroy, + .create_swapchain = gamescope_swapchain_factory_create_swapchain, }; -static void gamescope_xwayland_bind( struct wl_client *client, void *data, uint32_t version, uint32_t id ) +static void gamescope_swapchain_factory_bind( struct wl_client *client, void *data, uint32_t version, uint32_t id ) { - struct wl_resource *resource = wl_resource_create( client, &gamescope_xwayland_interface, version, id ); - wl_resource_set_implementation( resource, &gamescope_xwayland_impl, NULL, NULL ); + struct wl_resource *resource = wl_resource_create( client, &gamescope_swapchain_factory_interface, version, id ); + wl_resource_set_implementation( resource, &gamescope_swapchain_factory_impl, NULL, NULL ); } -static void create_gamescope_xwayland( void ) +static void create_gamescope_swapchain_factory( void ) { - uint32_t version = 2; - wl_global_create( wlserver.display, &gamescope_xwayland_interface, version, NULL, gamescope_xwayland_bind ); + uint32_t version = 1; + wl_global_create( wlserver.display, &gamescope_swapchain_factory_interface, version, NULL, gamescope_swapchain_factory_bind ); } + + + + + + + + + + + + #if HAVE_PIPEWIRE static void gamescope_pipewire_handle_destroy( struct wl_client *client, struct wl_resource *resource ) { @@ -854,7 +955,7 @@ static void create_presentation_time( void ) wl_global_create( wlserver.display, &wp_presentation_interface, version, NULL, presentation_time_bind ); } -void wlserver_presentation_feedback_presented( struct wlr_surface *surface, std::vector& presentation_feedbacks, uint64_t last_refresh_nsec, int refresh_rate ) +void wlserver_presentation_feedback_presented( struct wlr_surface *surface, std::vector& presentation_feedbacks, uint64_t last_refresh_nsec, uint64_t refresh_cycle ) { wlserver_wl_surface_info *wl_surface_info = get_wl_surface_info(surface); @@ -879,8 +980,6 @@ void wlserver_presentation_feedback_presented( struct wlr_surface *surface, std: for (auto& feedback : presentation_feedbacks) { - const uint64_t nsecInterval = 1'000'000'000ul / refresh_rate; - timespec last_refresh_ts; last_refresh_ts.tv_sec = time_t(last_refresh_nsec / 1'000'000'000ul); last_refresh_ts.tv_nsec = long(last_refresh_nsec % 1'000'000'000ul); @@ -890,7 +989,7 @@ void wlserver_presentation_feedback_presented( struct wlr_surface *surface, std: last_refresh_ts.tv_sec >> 32, last_refresh_ts.tv_sec & 0xffffffff, last_refresh_ts.tv_nsec, - uint32_t(nsecInterval), + uint32_t(refresh_cycle), wl_surface_info->sequence >> 32, wl_surface_info->sequence & 0xffffffff, flags); @@ -915,6 +1014,32 @@ void wlserver_presentation_feedback_discard( struct wlr_surface *surface, std::v /////////////////////// +void wlserver_past_present_timing( struct wlr_surface *surface, uint32_t present_id, uint64_t desired_present_time, uint64_t actual_present_time, uint64_t earliest_present_time, uint64_t present_margin ) +{ + wlserver_wl_surface_info *wl_info = get_wl_surface_info( surface ); + gamescope_swapchain_send_past_present_timing( + wl_info->gamescope_swapchain, + present_id, + desired_present_time >> 32, + desired_present_time & 0xffffffff, + actual_present_time >> 32, + actual_present_time & 0xffffffff, + earliest_present_time >> 32, + earliest_present_time & 0xffffffff, + present_margin >> 32, + present_margin & 0xffffffff); +} + +void wlserver_refresh_cycle( struct wlr_surface *surface, uint64_t refresh_cycle ) +{ + wlserver_wl_surface_info *wl_info = get_wl_surface_info( surface ); + gamescope_swapchain_send_refresh_cycle( + wl_info->gamescope_swapchain, + refresh_cycle >> 32, + refresh_cycle & 0xffffffff); +} + +/////////////////////// static void handle_session_active( struct wl_listener *listener, void *data ) { @@ -1267,6 +1392,8 @@ bool wlserver_init( void ) { create_gamescope_xwayland(); + create_gamescope_swapchain_factory(); + #if HAVE_PIPEWIRE create_gamescope_pipewire(); #endif diff --git a/src/wlserver.hpp b/src/wlserver.hpp index fc7446954..124440320 100644 --- a/src/wlserver.hpp +++ b/src/wlserver.hpp @@ -40,6 +40,8 @@ struct ResListEntry_t { bool async; std::shared_ptr feedback; std::vector presentation_feedbacks; + std::optional present_id; + uint64_t desired_present_time; }; struct wlserver_content_override; @@ -66,7 +68,7 @@ class gamescope_xwayland_server_t std::vector retrieve_commits(); - void handle_override_window_content( struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface_resource, uint32_t x11_window ); + void handle_override_window_content( struct wl_client *client, struct wl_resource *resource, struct wlr_surface *surface, uint32_t x11_window ); void destroy_content_override( struct wlserver_x11_surface_info *x11_surface, struct wlr_surface *surf); void destroy_content_override(struct wlserver_content_override *co); @@ -235,7 +237,14 @@ struct wlserver_wl_surface_info uint64_t sequence = 0; std::vector pending_presentation_feedbacks; + + std::atomic gamescope_swapchain = { nullptr }; + std::optional present_id = std::nullopt; + uint64_t desired_present_time = 0; + + uint64_t last_refresh_cycle = 0; }; +wlserver_wl_surface_info *get_wl_surface_info(struct wlr_surface *wlr_surf); void wlserver_x11_surface_info_init( struct wlserver_x11_surface_info *surf, gamescope_xwayland_server_t *server, uint32_t x11_id ); void wlserver_x11_surface_info_finish( struct wlserver_x11_surface_info *surf ); @@ -249,5 +258,8 @@ void wlserver_open_steam_menu( bool qam ); uint32_t wlserver_make_new_xwayland_server(); void wlserver_destroy_xwayland_server(gamescope_xwayland_server_t *server); -void wlserver_presentation_feedback_presented( struct wlr_surface *surface, std::vector& presentation_feedbacks, uint64_t last_refresh_nsec, int refresh_rate ); +void wlserver_presentation_feedback_presented( struct wlr_surface *surface, std::vector& presentation_feedbacks, uint64_t last_refresh_nsec, uint64_t refresh_cycle ); void wlserver_presentation_feedback_discard( struct wlr_surface *surface, std::vector& presentation_feedbacks ); + +void wlserver_past_present_timing( struct wlr_surface *surface, uint32_t present_id, uint64_t desired_present_time, uint64_t actual_present_time, uint64_t earliest_present_time, uint64_t present_margin ); +void wlserver_refresh_cycle( struct wlr_surface *surface, uint64_t refresh_cycle ); diff --git a/src/xwayland_ctx.hpp b/src/xwayland_ctx.hpp index 7ba8673a2..cd6c35d73 100644 --- a/src/xwayland_ctx.hpp +++ b/src/xwayland_ctx.hpp @@ -33,10 +33,18 @@ struct focus_t bool outdatedInteractiveFocus; }; +struct CommitDoneEntry_t +{ + uint64_t commitID; + uint64_t desiredPresentTime; + uint64_t earliestPresentTime; + uint64_t earliestLatchTime; +}; + struct CommitDoneList_t { std::mutex listCommitsDoneLock; - std::vector< uint64_t > listCommitsDone; + std::vector< CommitDoneEntry_t > listCommitsDone; }; struct xwayland_ctx_t From 539c6faf06aef6623f3d581c763fc0d45a5159c4 Mon Sep 17 00:00:00 2001 From: Joshua Ashton Date: Tue, 26 Sep 2023 06:15:26 +0100 Subject: [PATCH 2/3] main: Use SDL_VIDEODRIVER wayland by default, force on present wait --- src/main.cpp | 61 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 9c468ab71..82c2e852b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -34,11 +34,16 @@ #include "pipewire.hpp" #endif +#include + +using namespace std::literals; + EStreamColorspace g_ForcedNV12ColorSpace = k_EStreamColorspace_Unknown; static bool s_bInitialWantsVRREnabled = false; const char *gamescope_optstring = nullptr; const char *g_pOriginalDisplay = nullptr; +const char *g_pOriginalWaylandDisplay = nullptr; const struct option *gamescope_options = (struct option[]){ { "help", no_argument, nullptr, 0 }, @@ -470,6 +475,41 @@ static EStreamColorspace parse_colorspace_string( const char *pszStr ) return k_EStreamColorspace_Unknown; } + + + +static bool g_bSupportsWaylandPresentationTime = false; +static constexpr wl_registry_listener s_registryListener = { + .global = [](void* data, wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { + if (interface == "wp_presentation"sv) + g_bSupportsWaylandPresentationTime = true; + }, + + .global_remove = [](void* data, wl_registry* registry, uint32_t name) { + }, +}; + +static bool CheckWaylandPresentationTime() +{ + wl_display *display = wl_display_connect(g_pOriginalWaylandDisplay); + if (!display) { + fprintf(stderr, "Failed to connect to wayland socket: %s.\n", g_pOriginalWaylandDisplay); + exit(1); + return false; + } + wl_registry *registry = wl_display_get_registry(display); + + wl_registry_add_listener(registry, &s_registryListener, nullptr); + + wl_display_dispatch(display); + wl_display_roundtrip(display); + + wl_registry_destroy(registry); + + return g_bSupportsWaylandPresentationTime; +} + + int g_nPreferredOutputWidth = 0; int g_nPreferredOutputHeight = 0; @@ -647,10 +687,25 @@ int main(int argc, char **argv) XInitThreads(); g_mainThread = pthread_self(); - if ( getenv("DISPLAY") != NULL || getenv("WAYLAND_DISPLAY") != NULL ) + g_pOriginalDisplay = getenv("DISPLAY"); + g_pOriginalWaylandDisplay = getenv("WAYLAND_DISPLAY"); + g_bIsNested = g_pOriginalDisplay != NULL || g_pOriginalWaylandDisplay != NULL; + + if ( BIsSDLSession() && g_pOriginalWaylandDisplay != NULL ) { - g_bIsNested = true; - g_pOriginalDisplay = getenv("DISPLAY"); + // Default to SDL_VIDEODRIVER wayland under Wayland and force enable vk_khr_present_wait + // (not enabled by default in Mesa because instance does not know if Wayland + // compositor supports wp_presentation, but we can check that ourselves.) + setenv("vk_khr_present_wait", "true", 0); + setenv("SDL_VIDEODRIVER", "wayland", 0); + + if (!CheckWaylandPresentationTime()) + { + fprintf(stderr, + "Your Wayland compositor does NOT support wp_presentation/presentation-time which is required for VK_KHR_present_wait and VK_KHR_present_id which is needed for Gamescope to function.\n" + "Please update your compositor or complain to your compositor vendor for support.\n"); + return 1; + } } if ( !wlsession_init() ) From 56c24faa7e2a0447dde6b649f8660a2ac80f0593 Mon Sep 17 00:00:00 2001 From: Joshua Ashton Date: Tue, 26 Sep 2023 06:16:11 +0100 Subject: [PATCH 3/3] steamcompmgr: Enable Gamescope WSI layer by default in nested Hopefully this has had enough testing and can be used now. At the very least, this will help us smoke out some issues. --- src/steamcompmgr.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp index 126ca5a09..fa3586711 100644 --- a/src/steamcompmgr.cpp +++ b/src/steamcompmgr.cpp @@ -6402,6 +6402,8 @@ spawn_client( char **argv ) unsetenv( "ENABLE_VKBASALT" ); + // Enable Gamescope WSI by default for nested. + setenv( "ENABLE_GAMESCOPE_WSI", "1", 0 ); execvp( argv[ 0 ], argv ); xwm_log.errorf_errno( "execvp failed" );