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.
This commit is contained in:
Joshua Ashton 2023-09-24 15:43:21 +01:00 committed by Joshie
parent fc6e229506
commit 1bb7ef50ba
15 changed files with 843 additions and 258 deletions

View file

@ -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 <optional>
// For limiter file.
#include <time.h>
#include <fcntl.h>
#include <unistd.h>
@ -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<const char *> 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<std::mutex> presentTimingMutex = std::make_unique<std::mutex>();
std::vector<VkPastPresentationTimingGOOGLE> 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<GamescopeSwapchainData*>(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<GamescopeSwapchainData*>(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<void *>(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<uint32_t>(connection, "GAMESCOPE_HDR_OUTPUT_FEEDBACK"sv))
hdrOutput = !!*prop;
auto serverId = xcb::getPropertyValue<uint32_t>(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_compositor *>(
wl_registry_bind(registry, name, &wl_compositor_interface, version));
} else if (interface == "gamescope_xwayland"sv) {
instance->gamescope = reinterpret_cast<gamescope_xwayland *>(
wl_registry_bind(registry, name, &gamescope_xwayland_interface, version));
} else if (interface == "gamescope_swapchain_factory"sv) {
instance->gamescopeSwapchainFactory = reinterpret_cast<gamescope_swapchain_factory *>(
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;
// 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");
}
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 {
}
}
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);
}
auto serverId = xcb::getPropertyValue<uint32_t>(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;
}
return result;
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 (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;
}
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<void*>(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<VK_STRUCTURE_TYPE_PRESENT_TIMES_INFO_GOOGLE, const VkPresentTimesInfoGOOGLE>(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;
}
};
}

View file

@ -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"
},

View file

@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="gamescope_swapchain">
<copyright>
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.
</copyright>
<description summary="gamescope-specific swapchain protocol">
This is a private Gamescope protocol. Regular Wayland clients must not use
it.
</description>
<interface name="gamescope_swapchain_factory" version="1">
<request name="destroy" type="destructor"></request>
<request name="create_swapchain">
<description summary="create Gamescope swapchain interface">
</description>
<arg name="surface" type="object" interface="wl_surface"
summary="target surface"/>
<arg name="callback" type="new_id" interface="gamescope_swapchain"
summary="new swapchain object"/>
</request>
</interface>
<interface name="gamescope_swapchain" version="1">
<request name="destroy" type="destructor"></request>
<request name="override_window_content">
<description summary="override an X11's window wl_surface">
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.
</description>
<arg name="gamescope_xwayland_server_id" type="uint" summary="gamescope xwayland server ID"/>
<arg name="x11_window" type="uint" summary="X11 window ID"/>
</request>
<request name="swapchain_feedback">
<description summary="provide swapchain feedback">
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
</description>
<arg name="image_count" type="uint" summary="image count of swapchain"/>
<arg name="vk_format" type="uint" summary="VkFormat of swapchain"/>
<arg name="vk_colorspace" type="uint" summary="VkColorSpaceKHR of swapchain"/>
<arg name="vk_composite_alpha" type="uint" summary="VkCompositeAlphaFlagBitsKHR of swapchain"/>
<arg name="vk_pre_transform" type="uint" summary="VkSurfaceTransformFlagBitsKHR of swapchain"/>
<arg name="vk_present_mode" type="uint" summary="VkPresentModeKHR of swapchain"/>
<arg name="vk_clipped" type="uint" summary="clipped (VkBool32) of swapchain"/>
</request>
<request name="set_hdr_metadata">
<description summary="set HDR metadata for a surface">
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.
</description>
<arg name="display_primary_red_x" type="uint" summary="red primary x coordinate"/>
<arg name="display_primary_red_y" type="uint" summary="red primary y coordinate"/>
<arg name="display_primary_green_x" type="uint" summary="green primary x coordinate"/>
<arg name="display_primary_green_y" type="uint" summary="green primary y coordinate"/>
<arg name="display_primary_blue_x" type="uint" summary="blue primary x coordinate"/>
<arg name="display_primary_blue_y" type="uint" summary="blue primary y coordinate"/>
<arg name="white_point_x" type="uint" summary="white point x coordinate"/>
<arg name="white_point_y" type="uint" summary="white point y coordinate"/>
<arg name="max_display_mastering_luminance" type="uint" summary="max display mastering luminance"/>
<arg name="min_display_mastering_luminance" type="uint" summary="min display mastering luminance"/>
<arg name="max_cll" type="uint" summary="max content light level"/>
<arg name="max_fall" type="uint" summary="max frame average light level"/>
</request>
<request name="set_present_time">
<description summary="display timing of next commit">
Sets the display timing of the next commit.
This gets reset to 0s in the compositor's state after a commit.
</description>
<arg name="present_id" type="uint" summary="application provided presentation id"/>
<arg name="desired_present_time_hi" type="uint" summary="high part of the desired presentation time for this commit. Uses CLOCK_MONOTONIC. 0 = present as soon as possible."/>
<arg name="desired_present_time_lo" type="uint" summary="low part of the desired presentation time for this commit. Uses CLOCK_MONOTONIC. 0 = present as soon as possible."/>
</request>
<event name="past_present_timing">
<description summary="information about past presentation">
Gives information on the past presentation timing
</description>
<arg name="present_id" type="uint" summary="application provided presentation id"/>
<arg name="desired_present_time_hi" type="uint" summary="high part of the desired presentation time for the commit. (from the app)"/>
<arg name="desired_present_time_lo" type="uint" summary="low part of the desired presentation time for the commit. (from the app)"/>
<arg name="actual_present_time_hi" type="uint" summary="high part of the actual present time for this commit."/>
<arg name="actual_present_time_lo" type="uint" summary="low part of the actual present time for this commit."/>
<arg name="earliest_present_time_hi" type="uint" summary="high part of the refresh time that Gamescope thought this commit was done by."/>
<arg name="earliest_present_time_lo" type="uint" summary="low part of the refresh time that Gamescope thought this commit was done by."/>
<arg name="present_margin_hi" type="uint" summary="high part of the difference between earliest present time and the earliest latch time"/>
<arg name="present_margin_lo" type="uint" summary="low part of the difference between earliest present time and the earliest latch time"/>
</event>
<event name="refresh_cycle">
<description summary="information about refresh cycle for this swapchain">
Gives information on the refresh cycle for this swapchain
</description>
<arg name="refresh_cycle_hi" type="uint" summary="high part of the refresh cycle in nanos"/>
<arg name="refresh_cycle_lo" type="uint" summary="low part of the refresh cycle in nanos"/>
</event>
</interface>
</protocol>

View file

@ -27,20 +27,14 @@
<description summary="gamescope-specific xwayland protocol">
This is a private Gamescope protocol. Regular Wayland clients must not use
it.
This protocol has been superceded by the 'gamescope-swapchain' protocol.
</description>
<interface name="gamescope_xwayland" version="2">
<interface name="gamescope_xwayland" version="1">
<request name="destroy" type="destructor"></request>
<request name="override_window_content">
<description summary="override an X11's window wl_surface">
See override_window_content2.
</description>
<arg name="surface" type="object" interface="wl_surface" summary="Wayland surface"/>
<arg name="x11_window" type="uint" summary="X11 window ID"/>
</request>
<request name="override_window_content2" since="2">
<description summary="override an X11's window wl_surface">
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.
</description>
<arg name="surface" type="object" interface="wl_surface" summary="Wayland surface"/>
<arg name="gamescope_xwayland_server_id" type="uint" summary="gamescope xwayland server ID"/>
<arg name="x11_window" type="uint" summary="X11 window ID"/>
</request>
<request name="swapchain_feedback" since="2">
<description summary="provide swapchain feedback">
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
</description>
<arg name="surface" type="object" interface="wl_surface" summary="Wayland surface"/>
<arg name="image_count" type="uint" summary="image count of swapchain"/>
<arg name="vk_format" type="uint" summary="VkFormat of swapchain"/>
<arg name="vk_colorspace" type="uint" summary="VkColorSpaceKHR of swapchain"/>
<arg name="vk_composite_alpha" type="uint" summary="VkCompositeAlphaFlagBitsKHR of swapchain"/>
<arg name="vk_pre_transform" type="uint" summary="VkSurfaceTransformFlagBitsKHR of swapchain"/>
<arg name="vk_present_mode" type="uint" summary="VkPresentModeKHR of swapchain"/>
<arg name="vk_clipped" type="uint" summary="clipped (VkBool32) of swapchain"/>
</request>
<request name="set_hdr_metadata" since="2">
<description summary="set HDR metadata for a surface">
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.
</description>
<arg name="surface" type="object" interface="wl_surface" summary="Wayland surface"/>
<arg name="display_primary_red_x" type="uint" summary="red primary x coordinate"/>
<arg name="display_primary_red_y" type="uint" summary="red primary y coordinate"/>
<arg name="display_primary_green_x" type="uint" summary="green primary x coordinate"/>
<arg name="display_primary_green_y" type="uint" summary="green primary y coordinate"/>
<arg name="display_primary_blue_x" type="uint" summary="blue primary x coordinate"/>
<arg name="display_primary_blue_y" type="uint" summary="blue primary y coordinate"/>
<arg name="white_point_x" type="uint" summary="white point x coordinate"/>
<arg name="white_point_y" type="uint" summary="white point y coordinate"/>
<arg name="max_display_mastering_luminance" type="uint" summary="max display mastering luminance"/>
<arg name="min_display_mastering_luminance" type="uint" summary="min display mastering luminance"/>
<arg name="max_cll" type="uint" summary="max content light level"/>
<arg name="max_fall" type="uint" summary="max frame average light level"/>
</request>
</interface>
</protocol>

View file

@ -8,6 +8,7 @@ protocols = [
'gamescope-input-method',
'gamescope-tearing-control-unstable-v1',
'gamescope-control',
'gamescope-swapchain',
'xdg-shell',
'presentation-time',
]

View file

@ -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

View file

@ -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<uint64_t> 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;
}

View file

@ -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

View file

@ -130,6 +130,7 @@ extern float g_flHDRItmTargetNits;
extern std::atomic<uint64_t> 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<struct wl_resource*> presentation_feedbacks;
std::optional<uint32_t> present_id = std::nullopt;
uint64_t desired_present_time = 0;
uint64_t earliest_present_time = 0;
uint64_t present_margin = 0;
};
static std::vector<pollfd> 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 @@ retry:
{
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<commit_t>
import_commit ( struct wlr_surface *surf, struct wlr_buffer *buf, bool async, std::shared_ptr<wlserver_vk_swapchain_feedback> swapchain_feedback, std::vector<struct wl_resource*> presentation_feedbacks )
import_commit ( struct wlr_surface *surf, struct wlr_buffer *buf, bool async, std::shared_ptr<wlserver_vk_swapchain_feedback> swapchain_feedback, std::vector<struct wl_resource*> presentation_feedbacks, std::optional<uint32_t> present_id, uint64_t desired_present_time )
{
std::shared_ptr<commit_t> commit = std::make_shared<commit_t>();
std::unique_lock<std::mutex> 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<std::mutex> 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<std::mutex> 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<commit_t> newCommit = import_commit( reslistentry.surf, buf, reslistentry.async, std::move(reslistentry.feedback), std::move(reslistentry.presentation_feedbacks) );
std::shared_ptr<commit_t> 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:

View file

@ -17,6 +17,7 @@ void steamcompmgr_main(int argc, char **argv);
#include "rendervulkan.hpp"
#include "wlserver.hpp"
#include "vblankmanager.hpp"
#include <mutex>
#include <vector>
@ -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);

View file

@ -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 ];
}

View file

@ -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<uint64_t> g_uVblankDrawTimeNS;
const unsigned int g_uDefaultVBlankRedZone = 1'650'000;

View file

@ -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<ResListEntry_t> 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<wlserver_wl_surface_info *>(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>(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<struct wl_resource*>& presentation_feedbacks, uint64_t last_refresh_nsec, int refresh_rate )
void wlserver_presentation_feedback_presented( struct wlr_surface *surface, std::vector<struct wl_resource*>& 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

View file

@ -40,6 +40,8 @@ struct ResListEntry_t {
bool async;
std::shared_ptr<wlserver_vk_swapchain_feedback> feedback;
std::vector<struct wl_resource*> presentation_feedbacks;
std::optional<uint32_t> present_id;
uint64_t desired_present_time;
};
struct wlserver_content_override;
@ -66,7 +68,7 @@ public:
std::vector<ResListEntry_t> 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<struct wl_resource*> pending_presentation_feedbacks;
std::atomic<struct wl_resource *> gamescope_swapchain = { nullptr };
std::optional<uint32_t> 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<struct wl_resource*>& presentation_feedbacks, uint64_t last_refresh_nsec, int refresh_rate );
void wlserver_presentation_feedback_presented( struct wlr_surface *surface, std::vector<struct wl_resource*>& presentation_feedbacks, uint64_t last_refresh_nsec, uint64_t refresh_cycle );
void wlserver_presentation_feedback_discard( struct wlr_surface *surface, std::vector<struct wl_resource*>& 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 );

View file

@ -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