gamescope/src/steamcompmgr.cpp
Simon Ser 146da86c89 steamcompmgr: don't erase multiple commits with future desired time
If the queue contains two commits with a desired time set in the
future, keep the second one in the queue instead of discarding it.
2023-11-24 11:49:23 -08:00

8203 lines
239 KiB
C++

/*
* Based on xcompmgr by Keith Packard et al.
* http://cgit.freedesktop.org/xorg/app/xcompmgr/
* Original xcompmgr legal notices follow:
*
* Copyright © 2003 Keith Packard
*
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice appear in all copies and that both that
* copyright notice and this permission notice appear in supporting
* documentation, and that the name of Keith Packard not be used in
* advertising or publicity pertaining to distribution of the software without
* specific, written prior permission. Keith Packard makes no
* representations about the suitability of this software for any purpose. It
* is provided "as is" without express or implied warranty.
*
* KEITH PACKARD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
* INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
* EVENT SHALL KEITH PACKARD BE LIABLE FOR ANY SPECIAL, INDIRECT OR
* CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
* DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/* Modified by Matthew Hawn. I don't know what to say here so follow what it
* says above. Not that I can really do anything about it
*/
#include "xwayland_ctx.hpp"
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/extensions/xfixeswire.h>
#include <cstdint>
#include <drm_mode.h>
#include <memory>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <atomic>
#include <vector>
#include <algorithm>
#include <array>
#include <iostream>
#include <fstream>
#include <string>
#include <queue>
#include <variant>
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/poll.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <sys/types.h>
#if defined(__linux__)
#include <sys/prctl.h>
#elif defined(__DragonFly__) || defined(__FreeBSD__)
#include <sys/procctl.h>
#endif
#include <sys/socket.h>
#include <sys/resource.h>
#include <time.h>
#include <unistd.h>
#include <getopt.h>
#include <spawn.h>
#include <signal.h>
#include <linux/input-event-codes.h>
#include <X11/Xmu/CurUtil.h>
#include "waitable.h"
#include "steamcompmgr_shared.hpp"
#include "main.hpp"
#include "wlserver.hpp"
#include "drm.hpp"
#include "rendervulkan.hpp"
#include "steamcompmgr.hpp"
#include "vblankmanager.hpp"
#include "sdlwindow.hpp"
#include "log.hpp"
#include "defer.hpp"
#if HAVE_PIPEWIRE
#include "pipewire.hpp"
#endif
#if HAVE_OPENVR
#include "vr_session.hpp"
#endif
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image.h>
#include <stb_image_write.h>
#define GPUVIS_TRACE_IMPLEMENTATION
#include "gpuvis_trace_utils.h"
static LogScope xwm_log("xwm");
LogScope g_WaitableLog("waitable");
bool g_bWasPartialComposite = false;
///
// Color Mgmt
//
gamescope_color_mgmt_tracker_t g_ColorMgmt{};
static gamescope_color_mgmt_luts g_ColorMgmtLutsOverride[ EOTF_Count ];
static lut3d_t g_ColorMgmtLooks[ EOTF_Count ];
gamescope_color_mgmt_luts g_ColorMgmtLuts[ EOTF_Count ];
gamescope_color_mgmt_luts g_ScreenshotColorMgmtLuts[ EOTF_Count ];
static lut1d_t g_tmpLut1d;
static lut3d_t g_tmpLut3d;
bool g_bForceHDRSupportDebug = false;
extern float g_flInternalDisplayBrightnessNits;
extern float g_flHDRItmSdrNits;
extern float g_flHDRItmTargetNits;
extern std::atomic<uint64_t> g_lastVblank;
static std::shared_ptr<wlserver_ctm> s_scRGB709To2020Matrix;
std::string clipboard;
std::string primarySelection;
std::string g_reshade_effect{};
uint32_t g_reshade_technique_idx = 0;
bool g_bSteamIsActiveWindow = false;
uint64_t timespec_to_nanos(struct timespec& spec)
{
return spec.tv_sec * 1'000'000'000ul + spec.tv_nsec;
}
static void
update_runtime_info();
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,
.displayColorimetry = displaycolorimetry_709,
.displayEOTF = EOTF_Gamma22,
.outputEncodingColorimetry = displaycolorimetry_709,
.outputEncodingEOTF = EOTF_Gamma22,
};
//#define COLOR_MGMT_MICROBENCH
// sudo cpupower frequency-set --governor performance
static void
create_color_mgmt_luts(const gamescope_color_mgmt_t& newColorMgmt, gamescope_color_mgmt_luts outColorMgmtLuts[ EOTF_Count ])
{
const displaycolorimetry_t& displayColorimetry = newColorMgmt.displayColorimetry;
const displaycolorimetry_t& outputEncodingColorimetry = newColorMgmt.outputEncodingColorimetry;
for ( uint32_t nInputEOTF = 0; nInputEOTF < EOTF_Count; nInputEOTF++ )
{
if (!outColorMgmtLuts[nInputEOTF].vk_lut1d)
outColorMgmtLuts[nInputEOTF].vk_lut1d = vulkan_create_1d_lut(s_nLutSize1d);
if (!outColorMgmtLuts[nInputEOTF].vk_lut3d)
outColorMgmtLuts[nInputEOTF].vk_lut3d = vulkan_create_3d_lut(s_nLutEdgeSize3d, s_nLutEdgeSize3d, s_nLutEdgeSize3d);
if ( g_ColorMgmtLutsOverride[nInputEOTF].HasLuts() )
{
memcpy(g_ColorMgmtLuts[nInputEOTF].lut1d, g_ColorMgmtLutsOverride[nInputEOTF].lut1d, sizeof(g_ColorMgmtLutsOverride[nInputEOTF].lut1d));
memcpy(g_ColorMgmtLuts[nInputEOTF].lut3d, g_ColorMgmtLutsOverride[nInputEOTF].lut3d, sizeof(g_ColorMgmtLutsOverride[nInputEOTF].lut3d));
}
else
{
displaycolorimetry_t inputColorimetry{};
colormapping_t colorMapping{};
tonemapping_t tonemapping{};
tonemapping.bUseShaper = true;
EOTF inputEOTF = static_cast<EOTF>( nInputEOTF );
float flGain = 1.f;
lut3d_t * pLook = g_ColorMgmtLooks[nInputEOTF].lutEdgeSize > 0 ? &g_ColorMgmtLooks[nInputEOTF] : nullptr;
if ( inputEOTF == EOTF_Gamma22 )
{
flGain = newColorMgmt.flSDRInputGain;
if ( newColorMgmt.outputEncodingEOTF == EOTF_Gamma22 )
{
// G22 -> G22. Does not matter what the g22 mult is
tonemapping.g22_luminance = 1.f;
// xwm_log.infof("G22 -> G22");
}
else if ( newColorMgmt.outputEncodingEOTF == EOTF_PQ )
{
// G22 -> PQ. SDR content going on an HDR output
tonemapping.g22_luminance = newColorMgmt.flSDROnHDRBrightness;
// xwm_log.infof("G22 -> PQ");
}
// The final display colorimetry is used to build the output mapping, as we want a gamut-aware handling
// for sdrGamutWideness indepdendent of the output encoding (for SDR data), and when mapping SDR -> PQ output
// we only want to utilize a portion of the gamut the actual display can reproduce
buildSDRColorimetry( &inputColorimetry, &colorMapping, newColorMgmt.sdrGamutWideness, displayColorimetry );
}
else if ( inputEOTF == EOTF_PQ )
{
flGain = newColorMgmt.flHDRInputGain;
if ( newColorMgmt.outputEncodingEOTF == EOTF_Gamma22 )
{
// PQ -> G22 Leverage the display's native brightness
tonemapping.g22_luminance = newColorMgmt.flInternalDisplayBrightness;
// Determine the tonemapping parameters
// Use the external atoms if provided
tonemap_info_t source = newColorMgmt.hdrTonemapSourceMetadata;
tonemap_info_t dest = newColorMgmt.hdrTonemapDisplayMetadata;
// Otherwise, rely on the Vulkan source info and the EDID
// TODO: If source is invalid, use the provided metadata.
// TODO: If hdrTonemapDisplayMetadata is invalid, use the one provided by the display
// Adjust the source brightness range by the requested HDR input gain
dest.flBlackPointNits /= flGain;
dest.flWhitePointNits /= flGain;
if ( source.BIsValid() && dest.BIsValid() )
{
tonemapping.eOperator = newColorMgmt.hdrTonemapOperator;
tonemapping.eetf2390.init( source, newColorMgmt.hdrTonemapDisplayMetadata );
}
else
{
tonemapping.eOperator = ETonemapOperator_None;
}
/*
xwm_log.infof("PQ -> 2.2 - g22_luminance %f gain %f", tonemapping.g22_luminance, flGain );
xwm_log.infof("source %f %f", source.flBlackPointNits, source.flWhitePointNits );
xwm_log.infof("dest %f %f", dest.flBlackPointNits, dest.flWhitePointNits );
xwm_log.infof("operator %d", (int) tonemapping.eOperator );*/
}
else if ( newColorMgmt.outputEncodingEOTF == EOTF_PQ )
{
// PQ -> PQ passthrough (though this does apply gain)
// TODO: should we manipulate the output static metadata to reflect the gain factor?
tonemapping.g22_luminance = 1.f;
// xwm_log.infof("PQ -> PQ");
}
buildPQColorimetry( &inputColorimetry, &colorMapping, displayColorimetry );
}
calcColorTransform( &g_tmpLut1d, s_nLutSize1d, &g_tmpLut3d, s_nLutEdgeSize3d, inputColorimetry, inputEOTF,
outputEncodingColorimetry, newColorMgmt.outputEncodingEOTF,
newColorMgmt.outputVirtualWhite, newColorMgmt.chromaticAdaptationMode,
colorMapping, newColorMgmt.nightmode, tonemapping, pLook, flGain );
// Create quantized output luts
for ( size_t i=0, end = g_tmpLut1d.dataR.size(); i<end; ++i )
{
outColorMgmtLuts[nInputEOTF].lut1d[4*i+0] = drm_quantize_lut_value( g_tmpLut1d.dataR[i] );
outColorMgmtLuts[nInputEOTF].lut1d[4*i+1] = drm_quantize_lut_value( g_tmpLut1d.dataG[i] );
outColorMgmtLuts[nInputEOTF].lut1d[4*i+2] = drm_quantize_lut_value( g_tmpLut1d.dataB[i] );
outColorMgmtLuts[nInputEOTF].lut1d[4*i+3] = 0;
}
for ( size_t i=0, end = g_tmpLut3d.data.size(); i<end; ++i )
{
outColorMgmtLuts[nInputEOTF].lut3d[4*i+0] = drm_quantize_lut_value( g_tmpLut3d.data[i].r );
outColorMgmtLuts[nInputEOTF].lut3d[4*i+1] = drm_quantize_lut_value( g_tmpLut3d.data[i].g );
outColorMgmtLuts[nInputEOTF].lut3d[4*i+2] = drm_quantize_lut_value( g_tmpLut3d.data[i].b );
outColorMgmtLuts[nInputEOTF].lut3d[4*i+3] = 0;
}
}
outColorMgmtLuts[nInputEOTF].bHasLut1D = true;
outColorMgmtLuts[nInputEOTF].bHasLut3D = true;
vulkan_update_luts(outColorMgmtLuts[nInputEOTF].vk_lut1d, outColorMgmtLuts[nInputEOTF].vk_lut3d, outColorMgmtLuts[nInputEOTF].lut1d, outColorMgmtLuts[nInputEOTF].lut3d);
}
}
int g_nAsyncFlipsEnabled = 0;
int g_nSteamMaxHeight = 0;
bool g_bVRRCapable_CachedValue = false;
bool g_bVRRInUse_CachedValue = false;
bool g_bSupportsST2084_CachedValue = false;
bool g_bForceHDR10OutputDebug = false;
bool g_bHDREnabled = false;
bool g_bHDRItmEnable = false;
int g_nCurrentRefreshRate_CachedValue = 0;
static void
update_color_mgmt()
{
// update pending native display colorimetry
if ( !BIsNested() )
{
drm_get_native_colorimetry( &g_DRM,
&g_ColorMgmt.pending.displayColorimetry, &g_ColorMgmt.pending.displayEOTF,
&g_ColorMgmt.pending.outputEncodingColorimetry, &g_ColorMgmt.pending.outputEncodingEOTF );
}
else if (g_bForceHDR10OutputDebug)
{
g_ColorMgmt.pending.displayColorimetry = displaycolorimetry_2020;
g_ColorMgmt.pending.displayEOTF = EOTF_PQ;
g_ColorMgmt.pending.outputEncodingColorimetry = displaycolorimetry_2020;
g_ColorMgmt.pending.outputEncodingEOTF = EOTF_PQ;
}
else
{
g_ColorMgmt.pending.displayColorimetry = displaycolorimetry_709;
g_ColorMgmt.pending.displayEOTF = EOTF_Gamma22;
g_ColorMgmt.pending.outputEncodingColorimetry = displaycolorimetry_709;
g_ColorMgmt.pending.outputEncodingEOTF = EOTF_Gamma22;
}
#ifdef COLOR_MGMT_MICROBENCH
struct timespec t0, t1;
#else
// check if any part of our color mgmt stack is dirty
if ( g_ColorMgmt.pending == g_ColorMgmt.current && g_ColorMgmt.serial != 0 )
return;
#endif
#ifdef COLOR_MGMT_MICROBENCH
clock_gettime(CLOCK_MONOTONIC_RAW, &t0);
#endif
if (g_ColorMgmt.pending.enabled)
{
create_color_mgmt_luts(g_ColorMgmt.pending, g_ColorMgmtLuts);
}
else
{
for ( uint32_t i = 0; i < EOTF_Count; i++ )
g_ColorMgmtLuts[i].reset();
}
#ifdef COLOR_MGMT_MICROBENCH
clock_gettime(CLOCK_MONOTONIC_RAW, &t1);
#endif
#ifdef COLOR_MGMT_MICROBENCH
double delta = (timespec_to_nanos(t1) - timespec_to_nanos(t0)) / 1000000.0;
static uint32_t iter = 0;
static const uint32_t iter_count = 120;
static double accum = 0;
accum += delta;
if (iter++ == iter_count)
{
printf("update_color_mgmt: %.3fms\n", accum / iter_count);
iter = 0;
accum = 0;
}
#endif
static uint32_t s_NextColorMgmtSerial = 0;
g_ColorMgmt.serial = ++s_NextColorMgmtSerial;
g_ColorMgmt.current = g_ColorMgmt.pending;
}
static void
update_screenshot_color_mgmt()
{
create_color_mgmt_luts(k_ScreenshotColorMgmt, g_ScreenshotColorMgmtLuts);
}
bool set_color_sdr_gamut_wideness( float flVal )
{
if ( g_ColorMgmt.pending.sdrGamutWideness == flVal )
return false;
g_ColorMgmt.pending.sdrGamutWideness = flVal;
return g_ColorMgmt.pending.enabled;
}
bool set_internal_display_brightness( float flVal )
{
if ( flVal < 1.f )
{
flVal = 500.f;
}
if ( g_ColorMgmt.pending.flInternalDisplayBrightness == flVal )
return false;
g_ColorMgmt.pending.flInternalDisplayBrightness = flVal;
g_flInternalDisplayBrightnessNits = flVal;
return g_ColorMgmt.pending.enabled;
}
bool set_sdr_on_hdr_brightness( float flVal )
{
if ( flVal < 1.f )
{
flVal = 203.f;
}
if ( g_ColorMgmt.pending.flSDROnHDRBrightness == flVal )
return false;
g_ColorMgmt.pending.flSDROnHDRBrightness = flVal;
return g_ColorMgmt.pending.enabled;
}
bool set_hdr_input_gain( float flVal )
{
if ( flVal < 0.f )
{
flVal = 1.f;
}
if ( g_ColorMgmt.pending.flHDRInputGain == flVal )
return false;
g_ColorMgmt.pending.flHDRInputGain = flVal;
return g_ColorMgmt.pending.enabled;
}
bool set_sdr_input_gain( float flVal )
{
if ( flVal < 0.f )
{
flVal = 1.f;
}
if ( g_ColorMgmt.pending.flSDRInputGain == flVal )
return false;
g_ColorMgmt.pending.flSDRInputGain = flVal;
return g_ColorMgmt.pending.enabled;
}
bool set_color_nightmode( const nightmode_t &nightmode )
{
if ( g_ColorMgmt.pending.nightmode == nightmode )
return false;
g_ColorMgmt.pending.nightmode = nightmode;
return g_ColorMgmt.pending.enabled;
}
bool set_color_mgmt_enabled( bool bEnabled )
{
if ( g_ColorMgmt.pending.enabled == bEnabled )
return false;
g_ColorMgmt.pending.enabled = bEnabled;
return true;
}
static std::shared_ptr<CVulkanTexture> s_MuraCorrectionImage[DRM_SCREEN_TYPE_COUNT];
static std::shared_ptr<wlserver_ctm> s_MuraCTMBlob[DRM_SCREEN_TYPE_COUNT];
static float g_flMuraScale = 1.0f;
static bool g_bMuraCompensationDisabled = false;
bool is_mura_correction_enabled()
{
return s_MuraCorrectionImage[drm_get_screen_type( &g_DRM )] != nullptr && !g_bMuraCompensationDisabled;
}
void update_mura_ctm()
{
s_MuraCTMBlob[DRM_SCREEN_TYPE_INTERNAL] = nullptr;
if (s_MuraCorrectionImage[DRM_SCREEN_TYPE_INTERNAL] == nullptr)
return;
static constexpr float kMuraMapScale = 0.0625f;
static constexpr float kMuraOffset = -127.0f / 255.0f;
// Mura's influence scales non-linearly with brightness, so we have an additional scale
// on top of the scale factor for the underlying mura map.
const float flScale = g_flMuraScale * kMuraMapScale;
glm::mat3x4 mura_scale_offset = glm::mat3x4
{
flScale, 0, 0, kMuraOffset * flScale,
0, flScale, 0, kMuraOffset * flScale,
0, 0, 0, 0, // No mura comp for blue channel.
};
s_MuraCTMBlob[DRM_SCREEN_TYPE_INTERNAL] = drm_create_ctm(&g_DRM, mura_scale_offset);
}
bool g_bMuraDebugFullColor = false;
bool set_mura_overlay( const char *path )
{
xwm_log.infof("[josh mura correction] Setting mura correction image to: %s", path);
s_MuraCorrectionImage[DRM_SCREEN_TYPE_INTERNAL] = nullptr;
update_mura_ctm();
std::string red_path = std::string(path) + "_red.png";
std::string green_path = std::string(path) + "_green.png";
int red_w, red_h, red_comp;
unsigned char *red_data = stbi_load(red_path.c_str(), &red_w, &red_h, &red_comp, 1);
int green_w, green_h, green_comp;
unsigned char *green_data = stbi_load(green_path.c_str(), &green_w, &green_h, &green_comp, 1);
if (!red_data || !green_data || red_w != green_w || red_h != green_h || red_comp != green_comp || red_comp != 1 || green_comp != 1)
{
xwm_log.infof("[josh mura correction] Couldn't load mura correction image, disabling mura correction.");
return true;
}
int w = red_w;
int h = red_h;
unsigned char *data = (unsigned char*)malloc(red_w * red_h * 4);
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
data[(y * w * 4) + (x * 4) + 0] = g_bMuraDebugFullColor ? 255 : red_data[y * w + x];
data[(y * w * 4) + (x * 4) + 1] = g_bMuraDebugFullColor ? 255 : green_data[y * w + x];
data[(y * w * 4) + (x * 4) + 2] = 127; // offset of 0.
data[(y * w * 4) + (x * 4) + 3] = 0; // Make alpha = 0 so we act as addtive.
}
}
free(red_data);
free(green_data);
CVulkanTexture::createFlags texCreateFlags;
texCreateFlags.bFlippable = !BIsNested();
texCreateFlags.bSampled = true;
s_MuraCorrectionImage[DRM_SCREEN_TYPE_INTERNAL] = vulkan_create_texture_from_bits(w, h, w, h, DRM_FORMAT_ABGR8888, texCreateFlags, (void*)data);
free(data);
xwm_log.infof("[josh mura correction] Loaded new mura correction image!");
update_mura_ctm();
return true;
}
bool set_mura_scale(float new_scale)
{
bool diff = g_flMuraScale != new_scale;
g_flMuraScale = new_scale;
update_mura_ctm();
return diff;
}
bool set_color_3dlut_override(const char *path)
{
int nLutIndex = EOTF_Gamma22;
g_ColorMgmt.pending.externalDirtyCtr++;
g_ColorMgmtLutsOverride[nLutIndex].bHasLut3D = false;
FILE *f = fopen(path, "rb");
if (!f) {
return true;
}
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
size_t elems = fsize / sizeof(uint16_t);
if (elems == 0) {
return true;
}
fread(g_ColorMgmtLutsOverride[nLutIndex].lut3d, elems, sizeof(uint16_t), f);
g_ColorMgmtLutsOverride[nLutIndex].bHasLut3D = true;
return true;
}
bool set_color_shaperlut_override(const char *path)
{
int nLutIndex = EOTF_Gamma22;
g_ColorMgmt.pending.externalDirtyCtr++;
g_ColorMgmtLutsOverride[nLutIndex].bHasLut1D = false;
FILE *f = fopen(path, "rb");
if (!f) {
return true;
}
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
size_t elems = fsize / sizeof(uint16_t);
if (elems == 0) {
return true;
}
fread(g_ColorMgmtLutsOverride[nLutIndex].lut1d, elems, sizeof(uint16_t), f);
g_ColorMgmtLutsOverride[nLutIndex].bHasLut1D = true;
return true;
}
bool set_color_look_pq(const char *path)
{
LoadCubeLut( &g_ColorMgmtLooks[EOTF_PQ], path );
g_ColorMgmt.pending.externalDirtyCtr++;
return true;
}
bool set_color_look_g22(const char *path)
{
LoadCubeLut( &g_ColorMgmtLooks[EOTF_Gamma22], path );
g_ColorMgmt.pending.externalDirtyCtr++;
return true;
}
bool g_bColorSliderInUse = false;
//
//
//
const uint32_t WS_OVERLAPPED = 0x00000000u;
const uint32_t WS_POPUP = 0x80000000u;
const uint32_t WS_CHILD = 0x40000000u;
const uint32_t WS_MINIMIZE = 0x20000000u;
const uint32_t WS_VISIBLE = 0x10000000u;
const uint32_t WS_DISABLED = 0x08000000u;
const uint32_t WS_CLIPSIBLINGS = 0x04000000u;
const uint32_t WS_CLIPCHILDREN = 0x02000000u;
const uint32_t WS_MAXIMIZE = 0x01000000u;
const uint32_t WS_BORDER = 0x00800000u;
const uint32_t WS_DLGFRAME = 0x00400000u;
const uint32_t WS_VSCROLL = 0x00200000u;
const uint32_t WS_HSCROLL = 0x00100000u;
const uint32_t WS_SYSMENU = 0x00080000u;
const uint32_t WS_THICKFRAME = 0x00040000u;
const uint32_t WS_GROUP = 0x00020000u;
const uint32_t WS_TABSTOP = 0x00010000u;
const uint32_t WS_MINIMIZEBOX = 0x00020000u;
const uint32_t WS_MAXIMIZEBOX = 0x00010000u;
const uint32_t WS_CAPTION = WS_BORDER | WS_DLGFRAME;
const uint32_t WS_TILED = WS_OVERLAPPED;
const uint32_t WS_ICONIC = WS_MINIMIZE;
const uint32_t WS_SIZEBOX = WS_THICKFRAME;
const uint32_t WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME| WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
const uint32_t WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU;
const uint32_t WS_CHILDWINDOW = WS_CHILD;
const uint32_t WS_TILEDWINDOW = WS_OVERLAPPEDWINDOW;
const uint32_t WS_EX_DLGMODALFRAME = 0x00000001u;
const uint32_t WS_EX_DRAGDETECT = 0x00000002u; // Undocumented
const uint32_t WS_EX_NOPARENTNOTIFY = 0x00000004u;
const uint32_t WS_EX_TOPMOST = 0x00000008u;
const uint32_t WS_EX_ACCEPTFILES = 0x00000010u;
const uint32_t WS_EX_TRANSPARENT = 0x00000020u;
const uint32_t WS_EX_MDICHILD = 0x00000040u;
const uint32_t WS_EX_TOOLWINDOW = 0x00000080u;
const uint32_t WS_EX_WINDOWEDGE = 0x00000100u;
const uint32_t WS_EX_CLIENTEDGE = 0x00000200u;
const uint32_t WS_EX_CONTEXTHELP = 0x00000400u;
const uint32_t WS_EX_RIGHT = 0x00001000u;
const uint32_t WS_EX_LEFT = 0x00000000u;
const uint32_t WS_EX_RTLREADING = 0x00002000u;
const uint32_t WS_EX_LTRREADING = 0x00000000u;
const uint32_t WS_EX_LEFTSCROLLBAR = 0x00004000u;
const uint32_t WS_EX_RIGHTSCROLLBAR = 0x00000000u;
const uint32_t WS_EX_CONTROLPARENT = 0x00010000u;
const uint32_t WS_EX_STATICEDGE = 0x00020000u;
const uint32_t WS_EX_APPWINDOW = 0x00040000u;
const uint32_t WS_EX_LAYERED = 0x00080000u;
const uint32_t WS_EX_NOINHERITLAYOUT = 0x00100000u;
const uint32_t WS_EX_NOREDIRECTIONBITMAP = 0x00200000u;
const uint32_t WS_EX_LAYOUTRTL = 0x00400000u;
const uint32_t WS_EX_COMPOSITED = 0x02000000u;
const uint32_t WS_EX_NOACTIVATE = 0x08000000u;
template< typename T >
constexpr const T& clamp( const T& x, const T& min, const T& max )
{
return x < min ? min : max < x ? max : x;
}
extern bool g_bForceRelativeMouse;
bool bSteamCompMgrGrab = false;
CommitDoneList_t g_steamcompmgr_xdg_done_commits;
struct ignore {
struct ignore *next;
unsigned long sequence;
};
gamescope::CAsyncWaiter g_ImageWaiter{ "gamescope_img" };
struct commit_t : public gamescope::IWaitable
{
commit_t()
{
static uint64_t maxCommmitID = 0;
commitID = ++maxCommmitID;
}
~commit_t()
{
if ( fb_id != 0 )
{
drm_unlock_fbid( &g_DRM, fb_id );
fb_id = 0;
}
wlserver_lock();
if (!presentation_feedbacks.empty())
{
wlserver_presentation_feedback_discard(surf, presentation_feedbacks);
// presentation_feedbacks cleared by wlserver_presentation_feedback_discard
}
wlr_buffer_unlock( buf );
wlserver_unlock();
}
GamescopeAppTextureColorspace colorspace() const
{
VkColorSpaceKHR colorspace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
if (feedback && vulkanTex)
colorspace = feedback->vk_colorspace;
if (!vulkanTex)
return GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR;
return VkColorSpaceToGamescopeAppTextureColorSpace(vulkanTex->format(), colorspace);
}
struct wlr_buffer *buf = nullptr;
uint32_t fb_id = 0;
std::shared_ptr<CVulkanTexture> vulkanTex;
uint64_t commitID = 0;
bool done = false;
bool async = false;
std::optional<wlserver_vk_swapchain_feedback> feedback = std::nullopt;
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;
// For waitable:
int GetFD() final
{
return m_nCommitFence;
}
void OnPollIn() final
{
gpuvis_trace_end_ctx_printf( commitID, "wait fence" );
g_ImageWaiter.RemoveWaitable( this );
close( m_nCommitFence );
m_nCommitFence = -1;
uint64_t frametime;
if ( m_bMangoNudge )
{
uint64_t now = get_time_in_nanos();
static uint64_t lastFrameTime = now;
frametime = now - lastFrameTime;
lastFrameTime = now;
}
// TODO: Move this so it's called in the main loop.
// Instead of looping over all the windows like before.
// When we get the new IWaitable stuff in there.
{
std::unique_lock< std::mutex > lock( pDoneCommits->listCommitsDoneLock );
pDoneCommits->listCommitsDone.push_back( CommitDoneEntry_t{ commitID, desired_present_time } );
}
if ( m_bMangoNudge )
mangoapp_update( frametime, uint64_t(~0ull), uint64_t(~0ull) );
nudge_steamcompmgr();
}
int m_nCommitFence = -1;
bool m_bMangoNudge = false;
CommitDoneList_t *pDoneCommits = nullptr; // I hate this
};
static std::vector<pollfd> pollfds;
#define MWM_HINTS_FUNCTIONS 1
#define MWM_HINTS_DECORATIONS 2
#define MWM_HINTS_INPUT_MODE 4
#define MWM_HINTS_STATUS 8
#define MWM_FUNC_ALL 0x01
#define MWM_FUNC_RESIZE 0x02
#define MWM_FUNC_MOVE 0x04
#define MWM_FUNC_MINIMIZE 0x08
#define MWM_FUNC_MAXIMIZE 0x10
#define MWM_FUNC_CLOSE 0x20
#define MWM_DECOR_ALL 0x01
#define MWM_DECOR_BORDER 0x02
#define MWM_DECOR_RESIZEH 0x04
#define MWM_DECOR_TITLE 0x08
#define MWM_DECOR_MENU 0x10
#define MWM_DECOR_MINIMIZE 0x20
#define MWM_DECOR_MAXIMIZE 0x40
#define MWM_INPUT_MODELESS 0
#define MWM_INPUT_PRIMARY_APPLICATION_MODAL 1
#define MWM_INPUT_SYSTEM_MODAL 2
#define MWM_INPUT_FULL_APPLICATION_MODAL 3
#define MWM_INPUT_APPLICATION_MODAL 1
#define MWM_TEAROFF_WINDOW 1
Window x11_win(steamcompmgr_win_t *w) {
if (w == nullptr)
return None;
if (w->type != steamcompmgr_win_type_t::XWAYLAND)
return None;
return w->xwayland().id;
}
struct global_focus_t : public focus_t
{
steamcompmgr_win_t *keyboardFocusWindow;
steamcompmgr_win_t *fadeWindow;
MouseCursor *cursor;
} global_focus;
uint32_t currentOutputWidth, currentOutputHeight;
bool currentHDROutput = false;
bool currentHDRForce = false;
std::vector< uint32_t > vecFocuscontrolAppIDs;
bool gameFocused;
unsigned int gamesRunningCount;
float overscanScaleRatio = 1.0;
float zoomScaleRatio = 1.0;
float globalScaleRatio = 1.0f;
float focusedWindowScaleX = 1.0f;
float focusedWindowScaleY = 1.0f;
float focusedWindowOffsetX = 0.0f;
float focusedWindowOffsetY = 0.0f;
uint32_t inputCounter;
uint32_t lastPublishedInputCounter;
bool focusDirty = false;
bool hasRepaint = false;
bool hasRepaintNonBasePlane = false;
unsigned long damageSequence = 0;
unsigned int cursorHideTime = 10'000;
bool gotXError = false;
unsigned int fadeOutStartTime = 0;
unsigned int g_FadeOutDuration = 0;
extern float g_flMaxWindowScale;
bool synchronize;
std::mutex g_SteamCompMgrXWaylandServerMutex;
VBlankTimeInfo_t g_SteamCompMgrVBlankTime = {};
static int g_nSteamCompMgrTargetFPS = 0;
static uint64_t g_uDynamicRefreshEqualityTime = 0;
static int g_nDynamicRefreshRate[DRM_SCREEN_TYPE_COUNT] = { 0, 0 };
// Delay to stop modes flickering back and forth.
static const uint64_t g_uDynamicRefreshDelay = 600'000'000; // 600ms
static int g_nCombinedAppRefreshCycleOverride[DRM_SCREEN_TYPE_COUNT] = { 0, 0 };
static void _update_app_target_refresh_cycle()
{
if ( BIsNested() )
{
g_nDynamicRefreshRate[ DRM_SCREEN_TYPE_INTERNAL ] = 0;
g_nSteamCompMgrTargetFPS = g_nCombinedAppRefreshCycleOverride[ DRM_SCREEN_TYPE_INTERNAL ];
return;
}
static drm_screen_type last_type;
static int last_target_fps;
static bool first = true;
drm_screen_type type = drm_get_screen_type( &g_DRM );
int target_fps = g_nCombinedAppRefreshCycleOverride[type];
if ( !first && type == last_type && last_target_fps == target_fps )
{
return;
}
last_type = type;
last_target_fps = target_fps;
first = false;
if ( !target_fps )
{
g_nDynamicRefreshRate[ type ] = 0;
g_nSteamCompMgrTargetFPS = 0;
return;
}
auto rates = drm_get_valid_refresh_rates( &g_DRM );
g_nDynamicRefreshRate[ type ] = 0;
g_nSteamCompMgrTargetFPS = target_fps;
// Find highest mode to do refresh doubling with.
for ( auto rate = rates.rbegin(); rate != rates.rend(); rate++ )
{
if (*rate % target_fps == 0)
{
g_nDynamicRefreshRate[ type ] = *rate;
g_nSteamCompMgrTargetFPS = target_fps;
return;
}
}
}
static void update_app_target_refresh_cycle()
{
int nPrevFPSLimit = g_nSteamCompMgrTargetFPS;
_update_app_target_refresh_cycle();
if ( !!g_nSteamCompMgrTargetFPS != !!nPrevFPSLimit )
update_runtime_info();
}
void steamcompmgr_set_app_refresh_cycle_override( drm_screen_type type, int override_fps )
{
g_nCombinedAppRefreshCycleOverride[ type ] = override_fps;
update_app_target_refresh_cycle();
}
static int g_nRuntimeInfoFd = -1;
bool g_bFSRActive = false;
BlurMode g_BlurMode = BLUR_MODE_OFF;
BlurMode g_BlurModeOld = BLUR_MODE_OFF;
unsigned int g_BlurFadeDuration = 0;
int g_BlurRadius = 5;
unsigned int g_BlurFadeStartTime = 0;
pid_t focusWindow_pid;
std::vector<std::shared_ptr<steamcompmgr_win_t>> g_steamcompmgr_xdg_wins;
static bool
window_is_steam( steamcompmgr_win_t *w )
{
return w && ( w->isSteamLegacyBigPicture || w->appID == 769 );
}
bool g_bChangeDynamicRefreshBasedOnGameOpenRatherThanActive = false;
static bool
steamcompmgr_user_has_any_game_open()
{
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
{
if (!server->ctx)
continue;
if (server->ctx->focus.focusWindow && !window_is_steam(server->ctx->focus.focusWindow))
return true;
}
return false;
}
bool steamcompmgr_window_should_limit_fps( steamcompmgr_win_t *w )
{
return w && !window_is_steam( w ) && !w->isOverlay && !w->isExternalOverlay;
}
bool steamcompmgr_window_should_refresh_switch( steamcompmgr_win_t *w )
{
if ( g_bChangeDynamicRefreshBasedOnGameOpenRatherThanActive )
return steamcompmgr_user_has_any_game_open();
return steamcompmgr_window_should_limit_fps( w );
}
enum HeldCommitTypes_t
{
HELD_COMMIT_BASE,
HELD_COMMIT_FADE,
HELD_COMMIT_COUNT,
};
std::array<std::shared_ptr<commit_t>, HELD_COMMIT_COUNT> g_HeldCommits;
bool g_bPendingFade = false;
/* opacity property name; sometime soon I'll write up an EWMH spec for it */
#define OPACITY_PROP "_NET_WM_WINDOW_OPACITY"
#define GAME_PROP "STEAM_GAME"
#define STEAM_PROP "STEAM_BIGPICTURE"
#define OVERLAY_PROP "STEAM_OVERLAY"
#define EXTERNAL_OVERLAY_PROP "GAMESCOPE_EXTERNAL_OVERLAY"
#define GAMES_RUNNING_PROP "STEAM_GAMES_RUNNING"
#define SCREEN_SCALE_PROP "STEAM_SCREEN_SCALE"
#define SCREEN_MAGNIFICATION_PROP "STEAM_SCREEN_MAGNIFICATION"
#define TRANSLUCENT 0x00000000
#define OPAQUE 0xffffffff
#define ICCCM_WITHDRAWN_STATE 0
#define ICCCM_NORMAL_STATE 1
#define ICCCM_ICONIC_STATE 3
#define NET_WM_STATE_REMOVE 0
#define NET_WM_STATE_ADD 1
#define NET_WM_STATE_TOGGLE 2
#define SYSTEM_TRAY_REQUEST_DOCK 0
#define SYSTEM_TRAY_BEGIN_MESSAGE 1
#define SYSTEM_TRAY_CANCEL_MESSAGE 2
#define FRAME_RATE_SAMPLING_PERIOD 160
unsigned int frameCounter;
unsigned int lastSampledFrameTime;
float currentFrameRate;
static bool debugFocus = false;
static bool drawDebugInfo = false;
static bool debugEvents = false;
bool steamMode = false;
static bool alwaysComposite = false;
static bool useXRes = true;
struct wlr_buffer_map_entry {
struct wl_listener listener;
struct wlr_buffer *buf;
std::shared_ptr<CVulkanTexture> vulkanTex;
uint32_t fb_id;
};
static std::mutex wlr_buffer_map_lock;
static std::unordered_map<struct wlr_buffer*, wlr_buffer_map_entry> wlr_buffer_map;
static std::atomic< int > g_nTakeScreenshot{ 0 };
static bool g_bPropertyRequestedScreenshot;
static std::atomic<bool> g_bForceRepaint{false};
static int g_nudgePipe[2] = {-1, -1};
static int g_nCursorScaleHeight = -1;
// poor man's semaphore
class sem
{
public:
void wait( void )
{
std::unique_lock<std::mutex> lock(mtx);
while(count == 0){
cv.wait(lock);
}
count--;
}
void signal( void )
{
std::unique_lock<std::mutex> lock(mtx);
count++;
cv.notify_one();
}
private:
std::mutex mtx;
std::condition_variable cv;
int count = 0;
};
static void
dispatch_nudge( int fd )
{
for (;;)
{
static char buf[1024];
if ( read( fd, buf, sizeof(buf) ) < 0 )
{
if ( errno != EAGAIN )
xwm_log.errorf_errno(" steamcompmgr: dispatch_nudge: read failed" );
break;
}
}
}
sem statsThreadSem;
std::mutex statsEventQueueLock;
std::vector< std::string > statsEventQueue;
std::string statsThreadPath;
int statsPipeFD = -1;
bool statsThreadRun;
void statsThreadMain( void )
{
pthread_setname_np( pthread_self(), "gamescope-stats" );
signal(SIGPIPE, SIG_IGN);
while ( statsPipeFD == -1 )
{
statsPipeFD = open( statsThreadPath.c_str(), O_WRONLY | O_CLOEXEC );
if ( statsPipeFD == -1 )
{
sleep( 10 );
}
}
wait:
statsThreadSem.wait();
if ( statsThreadRun == false )
{
return;
}
std::string event;
retry:
{
std::unique_lock< std::mutex > lock( statsEventQueueLock );
if( statsEventQueue.empty() )
{
goto wait;
}
event = statsEventQueue[ 0 ];
statsEventQueue.erase( statsEventQueue.begin() );
}
dprintf( statsPipeFD, "%s", event.c_str() );
goto retry;
}
static inline void stats_printf( const char* format, ...)
{
static char buffer[256];
static std::string eventstr;
va_list args;
va_start(args, format);
vsprintf(buffer,format, args);
va_end(args);
eventstr = buffer;
{
{
std::unique_lock< std::mutex > lock( statsEventQueueLock );
if( statsEventQueue.size() > 50 )
{
// overflow, drop event
return;
}
statsEventQueue.push_back( eventstr );
statsThreadSem.signal();
}
}
}
uint64_t get_time_in_nanos()
{
timespec ts;
// Kernel reports page flips with CLOCK_MONOTONIC.
clock_gettime(CLOCK_MONOTONIC, &ts);
return timespec_to_nanos(ts);
}
void sleep_for_nanos(uint64_t nanos)
{
timespec ts;
ts.tv_sec = time_t(nanos / 1'000'000'000ul);
ts.tv_nsec = long(nanos % 1'000'000'000ul);
nanosleep(&ts, nullptr);
}
void sleep_until_nanos(uint64_t nanos)
{
uint64_t now = get_time_in_nanos();
if (now >= nanos)
return;
sleep_for_nanos(nanos - now);
}
unsigned int
get_time_in_milliseconds(void)
{
return (unsigned int)(get_time_in_nanos() / 1'000'000ul);
}
static void
discard_ignore(xwayland_ctx_t *ctx, unsigned long sequence)
{
while (ctx->ignore_head)
{
if ((long) (sequence - ctx->ignore_head->sequence) > 0)
{
ignore *next = ctx->ignore_head->next;
free(ctx->ignore_head);
ctx->ignore_head = next;
if (!ctx->ignore_head)
ctx->ignore_tail = &ctx->ignore_head;
}
else
break;
}
}
static void
set_ignore(xwayland_ctx_t *ctx, unsigned long sequence)
{
ignore *i = (ignore *)malloc(sizeof(ignore));
if (!i)
return;
i->sequence = sequence;
i->next = NULL;
*ctx->ignore_tail = i;
ctx->ignore_tail = &i->next;
}
static int
should_ignore(xwayland_ctx_t *ctx, unsigned long sequence)
{
discard_ignore(ctx, sequence);
return ctx->ignore_head && ctx->ignore_head->sequence == sequence;
}
static bool
x_events_queued(xwayland_ctx_t* ctx)
{
// If mode is QueuedAlready, XEventsQueued() returns the number of
// events already in the event queue (and never performs a system call).
return XEventsQueued(ctx->dpy, QueuedAlready) != 0;
}
static steamcompmgr_win_t *
find_win(xwayland_ctx_t *ctx, Window id, bool find_children = true)
{
steamcompmgr_win_t *w;
if (id == None)
{
return NULL;
}
for (w = ctx->list; w; w = w->xwayland().next)
{
if (w->xwayland().id == id)
{
return w;
}
}
if ( !find_children )
return nullptr;
// Didn't find, must be a children somewhere; try again with parent.
Window root = None;
Window parent = None;
Window *children = NULL;
unsigned int childrenCount;
set_ignore(ctx, NextRequest(ctx->dpy));
XQueryTree(ctx->dpy, id, &root, &parent, &children, &childrenCount);
if (children)
XFree(children);
if (root == parent || parent == None)
{
return NULL;
}
return find_win(ctx, parent);
}
static steamcompmgr_win_t * find_win( xwayland_ctx_t *ctx, struct wlr_surface *surf )
{
steamcompmgr_win_t *w = nullptr;
for (w = ctx->list; w; w = w->xwayland().next)
{
if ( w->xwayland().surface.main_surface == surf )
return w;
if ( w->xwayland().surface.override_surface == surf )
return w;
}
return nullptr;
}
#ifdef COMMIT_REF_DEBUG
static int buffer_refs = 0;
#endif
static void
destroy_buffer( struct wl_listener *listener, void * )
{
std::lock_guard<std::mutex> lock( wlr_buffer_map_lock );
wlr_buffer_map_entry *entry = wl_container_of( listener, entry, listener );
if ( entry->fb_id != 0 )
{
drm_drop_fbid( &g_DRM, entry->fb_id );
}
wl_list_remove( &entry->listener.link );
#ifdef COMMIT_REF_DEBUG
fprintf(stderr, "destroy_buffer - refs: %d\n", --buffer_refs);
#endif
/* Has to be the last thing we do as this deletes *entry. */
wlr_buffer_map.erase( wlr_buffer_map.find( entry->buf ) );
}
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, 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 );
commit->surf = surf;
commit->buf = buf;
commit->async = async;
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() )
{
commit->vulkanTex = it->second.vulkanTex;
commit->fb_id = it->second.fb_id;
/* Unlock here to avoid deadlock [1],
* drm_lock_fbid calls wlserver_lock.
* Map is no longer used here and the element
* is no longer accessed. */
lock.unlock();
if (commit->fb_id)
{
drm_lock_fbid( &g_DRM, commit->fb_id );
}
return commit;
}
#ifdef COMMIT_REF_DEBUG
fprintf(stderr, "import_commit - refs %d\n", ++buffer_refs);
#endif
wlr_buffer_map_entry& entry = wlr_buffer_map[buf];
/* [1]
* Need to unlock the wlr_buffer_map_lock to avoid
* a deadlock on destroy_buffer if it owns the wlserver_lock.
* This is safe for a few reasons:
* - 1: All accesses to wlr_buffer_map are done before this lock.
* - 2: destroy_buffer cannot be called from this buffer before now
* as it only happens because of the signal added below.
* - 3: "References to elements in the unordered_map container remain
* valid in all cases, even after a rehash." */
lock.unlock();
commit->vulkanTex = vulkan_create_texture_from_wlr_buffer( buf );
assert( commit->vulkanTex );
struct wlr_dmabuf_attributes dmabuf = {0};
if ( BIsNested() == false && wlr_buffer_get_dmabuf( buf, &dmabuf ) )
{
commit->fb_id = drm_fbid_from_dmabuf( &g_DRM, buf, &dmabuf );
if ( commit->fb_id )
{
drm_lock_fbid( &g_DRM, commit->fb_id );
}
}
else
{
commit->fb_id = 0;
}
entry.listener.notify = destroy_buffer;
entry.buf = buf;
entry.vulkanTex = commit->vulkanTex;
entry.fb_id = commit->fb_id;
wlserver_lock();
wl_signal_add( &buf->events.destroy, &entry.listener );
wlserver_unlock();
return commit;
}
static int32_t
window_last_done_commit_id( steamcompmgr_win_t *w )
{
int32_t lastCommit = -1;
for ( uint32_t i = 0; i < w->commit_queue.size(); i++ )
{
if ( w->commit_queue[ i ]->done )
{
lastCommit = i;
}
}
return lastCommit;
}
static bool
window_has_commits( steamcompmgr_win_t *w )
{
return window_last_done_commit_id( w ) != -1;
}
static void
get_window_last_done_commit( steamcompmgr_win_t *w, std::shared_ptr<commit_t> &commit )
{
int32_t lastCommit = window_last_done_commit_id( w );
if ( lastCommit == -1 )
{
return;
}
if ( commit != w->commit_queue[ lastCommit ] )
commit = w->commit_queue[ lastCommit ];
}
static commit_t*
get_window_last_done_commit_peek( steamcompmgr_win_t *w )
{
int32_t lastCommit = window_last_done_commit_id( w );
if ( lastCommit == -1 )
{
return nullptr;
}
return w->commit_queue[ lastCommit ].get();
}
// For Steam, etc.
static bool
window_wants_no_focus_when_mouse_hidden( steamcompmgr_win_t *w )
{
return window_is_steam( w );
}
static bool
window_is_fullscreen( steamcompmgr_win_t *w )
{
return w && ( window_is_steam( w ) || w->isFullscreen );
}
void calc_scale_factor_scaler(float &out_scale_x, float &out_scale_y, float sourceWidth, float sourceHeight)
{
float XOutputRatio = currentOutputWidth / (float)g_nNestedWidth;
float YOutputRatio = currentOutputHeight / (float)g_nNestedHeight;
float outputScaleRatio = std::min(XOutputRatio, YOutputRatio);
float XRatio = (float)g_nNestedWidth / sourceWidth;
float YRatio = (float)g_nNestedHeight / sourceHeight;
if (g_upscaleScaler == GamescopeUpscaleScaler::STRETCH)
{
out_scale_x = XRatio * XOutputRatio;
out_scale_y = YRatio * YOutputRatio;
return;
}
if (g_upscaleScaler != GamescopeUpscaleScaler::FILL)
{
out_scale_x = std::min(XRatio, YRatio);
out_scale_y = std::min(XRatio, YRatio);
}
else
{
out_scale_x = std::max(XRatio, YRatio);
out_scale_y = std::max(XRatio, YRatio);
}
if (g_upscaleScaler == GamescopeUpscaleScaler::AUTO)
{
out_scale_x = std::min(g_flMaxWindowScale, out_scale_x);
out_scale_y = std::min(g_flMaxWindowScale, out_scale_y);
}
out_scale_x *= outputScaleRatio;
out_scale_y *= outputScaleRatio;
if (g_upscaleScaler == GamescopeUpscaleScaler::INTEGER)
{
if (out_scale_x > 1.0f)
{
// x == y here always.
out_scale_x = out_scale_y = floor(out_scale_x);
}
}
}
void calc_scale_factor(float &out_scale_x, float &out_scale_y, float sourceWidth, float sourceHeight)
{
calc_scale_factor_scaler(out_scale_x, out_scale_y, sourceWidth, sourceHeight);
out_scale_x *= globalScaleRatio;
out_scale_y *= globalScaleRatio;
}
/**
* Constructor for a cursor. It is hidden in the beginning (normally until moved by user).
*/
MouseCursor::MouseCursor(xwayland_ctx_t *ctx)
: m_texture(0)
, m_dirty(true)
, m_imageEmpty(false)
, m_hideForMovement(true)
, m_ctx(ctx)
{
m_lastX = g_nNestedWidth / 2;
m_lastY = g_nNestedHeight / 2;
updateCursorFeedback( true );
}
void MouseCursor::queryPositions(int &rootX, int &rootY, int &winX, int &winY)
{
Window window, child;
unsigned int mask;
XQueryPointer(m_ctx->dpy, DefaultRootWindow(m_ctx->dpy), &window, &child,
&rootX, &rootY, &winX, &winY, &mask);
}
void MouseCursor::queryGlobalPosition(int &x, int &y)
{
int winX, winY;
queryPositions(x, y, winX, winY);
}
void MouseCursor::queryButtonMask(unsigned int &mask)
{
Window window, child;
int rootX, rootY, winX, winY;
XQueryPointer(m_ctx->dpy, DefaultRootWindow(m_ctx->dpy), &window, &child,
&rootX, &rootY, &winX, &winY, &mask);
}
void MouseCursor::checkSuspension()
{
unsigned int buttonMask;
queryButtonMask(buttonMask);
bool bWasHidden = m_hideForMovement;
steamcompmgr_win_t *window = m_ctx->focus.inputFocusWindow;
if (window && window->ignoreNextClickForVisibility)
{
window->ignoreNextClickForVisibility--;
m_hideForMovement = true;
return;
}
else
{
if (buttonMask & ( Button1Mask | Button2Mask | Button3Mask | Button4Mask | Button5Mask )) {
m_hideForMovement = false;
m_lastMovedTime = get_time_in_milliseconds();
// Move the cursor back to where we left it if the window didn't want us to give
// it hover/focus where we left it and we moved it before.
if (window_wants_no_focus_when_mouse_hidden(window) && bWasHidden)
{
XWarpPointer(m_ctx->dpy, None, x11_win(m_ctx->focus.inputFocusWindow), 0, 0, 0, 0, m_lastX, m_lastY);
}
}
}
const bool suspended = get_time_in_milliseconds() - m_lastMovedTime > cursorHideTime;
if (!m_hideForMovement && suspended) {
m_hideForMovement = true;
// Rearm warp count
if (window) {
window->mouseMoved = 0;
// Move the cursor to the bottom right corner, just off screen if we can
// if the window (ie. Steam) doesn't want hover/focus events.
if ( window_wants_no_focus_when_mouse_hidden(window) )
{
m_lastX = m_x;
m_lastY = m_y;
XWarpPointer(m_ctx->dpy, None, x11_win(m_ctx->focus.inputFocusWindow), 0, 0, 0, 0, window->xwayland().a.width - 1, window->xwayland().a.height - 1);
}
}
// We're hiding the cursor, force redraw if we were showing it
if (window && !m_imageEmpty ) {
hasRepaintNonBasePlane = true;
nudge_steamcompmgr();
}
}
updateCursorFeedback();
}
void MouseCursor::warp(int x, int y)
{
XWarpPointer(m_ctx->dpy, None, x11_win(m_ctx->focus.inputFocusWindow), 0, 0, 0, 0, x, y);
}
void MouseCursor::resetPosition()
{
warp(m_x, m_y);
}
void MouseCursor::setDirty()
{
// We can't prove it's empty until checking again
m_imageEmpty = false;
m_dirty = true;
}
bool MouseCursor::setCursorImage(char *data, int w, int h, int hx, int hy)
{
XRenderPictFormat *pictformat;
Picture picture;
XImage* ximage;
Pixmap pixmap;
Cursor cursor;
GC gc;
if (!(ximage = XCreateImage(
m_ctx->dpy,
DefaultVisual(m_ctx->dpy, DefaultScreen(m_ctx->dpy)),
32, ZPixmap,
0,
data,
w, h,
32, 0)))
{
xwm_log.errorf("Failed to make ximage for cursor");
goto error_image;
}
if (!(pixmap = XCreatePixmap(m_ctx->dpy, DefaultRootWindow(m_ctx->dpy), w, h, 32)))
{
xwm_log.errorf("Failed to make pixmap for cursor");
goto error_pixmap;
}
if (!(gc = XCreateGC(m_ctx->dpy, pixmap, 0, NULL)))
{
xwm_log.errorf("Failed to make gc for cursor");
goto error_gc;
}
XPutImage(m_ctx->dpy, pixmap, gc, ximage, 0, 0, 0, 0, w, h);
if (!(pictformat = XRenderFindStandardFormat(m_ctx->dpy, PictStandardARGB32)))
{
xwm_log.errorf("Failed to create pictformat for cursor");
goto error_pictformat;
}
if (!(picture = XRenderCreatePicture(m_ctx->dpy, pixmap, pictformat, 0, NULL)))
{
xwm_log.errorf("Failed to create picture for cursor");
goto error_picture;
}
if (!(cursor = XRenderCreateCursor(m_ctx->dpy, picture, hx, hy)))
{
xwm_log.errorf("Failed to create cursor");
goto error_cursor;
}
XDefineCursor(m_ctx->dpy, DefaultRootWindow(m_ctx->dpy), cursor);
XFlush(m_ctx->dpy);
setDirty();
return true;
error_cursor:
XRenderFreePicture(m_ctx->dpy, picture);
error_picture:
error_pictformat:
XFreeGC(m_ctx->dpy, gc);
error_gc:
XFreePixmap(m_ctx->dpy, pixmap);
error_pixmap:
// XDestroyImage frees the data.
XDestroyImage(ximage);
error_image:
return false;
}
bool MouseCursor::setCursorImageByName(const char *name)
{
int screen = DefaultScreen(m_ctx->dpy);
XColor fg;
fg.pixel = WhitePixel(m_ctx->dpy, screen);
XQueryColor(m_ctx->dpy, DefaultColormap(m_ctx->dpy, screen), &fg);
XColor bg;
bg.pixel = BlackPixel(m_ctx->dpy, screen);
XQueryColor(m_ctx->dpy, DefaultColormap(m_ctx->dpy, screen), &bg);
int index = XmuCursorNameToIndex(name);
if (index < 0)
return false;
Font font = XLoadFont(m_ctx->dpy, "cursor");
if (!font)
return false;
defer( XUnloadFont(m_ctx->dpy, font) );
Cursor cursor = XCreateGlyphCursor(m_ctx->dpy, font, font, index, index + 1, &fg, &bg);
if ( !cursor )
return false;
defer( XFreeCursor(m_ctx->dpy, cursor) );
XDefineCursor(m_ctx->dpy, DefaultRootWindow(m_ctx->dpy), cursor);
XFlush(m_ctx->dpy);
setDirty();
return true;
}
void MouseCursor::constrainPosition()
{
int i;
steamcompmgr_win_t *window = m_ctx->focus.inputFocusWindow;
steamcompmgr_win_t *override = m_ctx->focus.overrideWindow;
if (window == override)
window = m_ctx->focus.focusWindow;
// If we had barriers before, get rid of them.
for (i = 0; i < 4; i++) {
if (m_scaledFocusBarriers[i] != None) {
XFixesDestroyPointerBarrier(m_ctx->dpy, m_scaledFocusBarriers[i]);
m_scaledFocusBarriers[i] = None;
}
}
auto barricade = [this](int x1, int y1, int x2, int y2) {
return XFixesCreatePointerBarrier(m_ctx->dpy, DefaultRootWindow(m_ctx->dpy),
x1, y1, x2, y2, 0, 0, NULL);
};
int x1 = window->xwayland().a.x;
int y1 = window->xwayland().a.y;
if (override)
{
x1 = std::min(x1, override->xwayland().a.x);
y1 = std::min(y1, override->xwayland().a.y);
}
int x2 = window->xwayland().a.x + window->xwayland().a.width;
int y2 = window->xwayland().a.y + window->xwayland().a.height;
if (override)
{
x2 = std::max(x2, override->xwayland().a.x + override->xwayland().a.width);
y2 = std::max(y2, override->xwayland().a.y + override->xwayland().a.height);
}
// Constrain it to the window; careful, the corners will leak due to a known X server bug.
m_scaledFocusBarriers[0] = barricade(0, y1, m_ctx->root_width, y1);
m_scaledFocusBarriers[1] = barricade(x2, 0, x2, m_ctx->root_height);
m_scaledFocusBarriers[2] = barricade(m_ctx->root_width, y2, 0, y2);
m_scaledFocusBarriers[3] = barricade(x1, m_ctx->root_height, x1, 0);
// Make sure the cursor is somewhere in our jail
int rootX, rootY;
queryGlobalPosition(rootX, rootY);
if ( rootX >= x2 || rootY >= y2 || rootX < x1 || rootY < y1 ) {
if ( window_wants_no_focus_when_mouse_hidden( window ) && m_hideForMovement )
warp(window->xwayland().a.width - 1, window->xwayland().a.height - 1);
else
warp(window->xwayland().a.width / 2, window->xwayland().a.height / 2);
m_lastX = window->xwayland().a.width / 2;
m_lastY = window->xwayland().a.height / 2;
}
}
void MouseCursor::move(int x, int y)
{
// Some stuff likes to warp in-place
if (m_x == x && m_y == y) {
return;
}
m_x = x;
m_y = y;
steamcompmgr_win_t *window = m_ctx->focus.inputFocusWindow;
if (window) {
// If mouse moved and we're on the hook for showing the cursor, repaint
if (!m_hideForMovement && !m_imageEmpty) {
hasRepaintNonBasePlane = true;
}
// If mouse moved and screen is magnified, repaint
if ( zoomScaleRatio != 1.0 )
{
hasRepaintNonBasePlane = true;
}
}
// Ignore the first events as it's likely to be non-user-initiated warps
if (!window )
return;
if ( ( window != global_focus.inputFocusWindow || !g_bPendingTouchMovement.exchange(false) ) && window->mouseMoved++ < 5 )
return;
m_lastMovedTime = get_time_in_milliseconds();
// Move the cursor back to centre if the window didn't want us to give
// it hover/focus where we left it.
if ( m_hideForMovement && window_wants_no_focus_when_mouse_hidden(window) )
{
XWarpPointer(m_ctx->dpy, None, x11_win(m_ctx->focus.inputFocusWindow), 0, 0, 0, 0, m_lastX, m_lastY);
}
m_hideForMovement = false;
updateCursorFeedback();
}
void MouseCursor::updatePosition()
{
int x,y;
queryGlobalPosition(x, y);
move(x, y);
checkSuspension();
}
int MouseCursor::x() const
{
return m_x;
}
int MouseCursor::y() const
{
return m_y;
}
bool MouseCursor::getTexture()
{
if (!m_dirty) {
return !m_imageEmpty;
}
auto *image = XFixesGetCursorImage(m_ctx->dpy);
if (!image) {
return false;
}
m_hotspotX = image->xhot;
m_hotspotY = image->yhot;
uint32_t surfaceWidth;
uint32_t surfaceHeight;
if ( BIsNested() == false && alwaysComposite == false )
{
surfaceWidth = g_DRM.cursor_width;
surfaceHeight = g_DRM.cursor_height;
}
else
{
surfaceWidth = image->width;
surfaceHeight = image->height;
}
m_texture = nullptr;
// Assume the cursor is fully translucent unless proven otherwise.
bool bNoCursor = true;
std::shared_ptr<std::vector<uint32_t>> cursorBuffer = nullptr;
if (image->width && image->height)
{
cursorBuffer = std::make_shared<std::vector<uint32_t>>(surfaceWidth * surfaceHeight);
for (int i = 0; i < image->height; i++) {
for (int j = 0; j < image->width; j++) {
(*cursorBuffer)[i * surfaceWidth + j] = image->pixels[i * image->width + j];
if ( (*cursorBuffer)[i * surfaceWidth + j] & 0xff000000 ) {
bNoCursor = false;
}
}
}
}
if (bNoCursor)
cursorBuffer = nullptr;
m_imageEmpty = bNoCursor;
if ( !g_bForceRelativeMouse )
{
sdlwindow_grab( m_imageEmpty );
bSteamCompMgrGrab = BIsNested() && m_imageEmpty;
}
m_dirty = false;
updateCursorFeedback();
if (m_imageEmpty) {
return false;
}
CVulkanTexture::createFlags texCreateFlags;
if ( BIsNested() == false )
{
texCreateFlags.bFlippable = true;
texCreateFlags.bLinear = true; // cursor buffer needs to be linear
// TODO: choose format & modifiers from cursor plane
}
m_texture = vulkan_create_texture_from_bits(surfaceWidth, surfaceHeight, image->width, image->height, DRM_FORMAT_ARGB8888, texCreateFlags, cursorBuffer->data());
sdlwindow_cursor(std::move(cursorBuffer), image->width, image->height, image->xhot, image->yhot);
assert(m_texture);
XFree(image);
return true;
}
void MouseCursor::paint(steamcompmgr_win_t *window, steamcompmgr_win_t *fit, struct FrameInfo_t *frameInfo)
{
if ( m_hideForMovement || m_imageEmpty ) {
return;
}
int rootX, rootY, winX, winY;
queryPositions(rootX, rootY, winX, winY);
move(rootX, rootY);
// Also need new texture
if (!getTexture()) {
return;
}
uint32_t sourceWidth = window->xwayland().a.width;
uint32_t sourceHeight = window->xwayland().a.height;
if ( fit )
{
// If we have an override window, try to fit it in as long as it won't make our scale go below 1.0.
sourceWidth = std::max<uint32_t>( sourceWidth, clamp<int>( fit->xwayland().a.x + fit->xwayland().a.width, 0, currentOutputWidth ) );
sourceHeight = std::max<uint32_t>( sourceHeight, clamp<int>( fit->xwayland().a.y + fit->xwayland().a.height, 0, currentOutputHeight ) );
}
float cursor_scale = 1.0f;
if ( g_nCursorScaleHeight > 0 )
{
cursor_scale = floor(currentOutputHeight / (float)g_nCursorScaleHeight);
}
cursor_scale = std::max(cursor_scale, 1.0f);
float scaledX, scaledY;
float currentScaleRatio_x = 1.0;
float currentScaleRatio_y = 1.0;
int cursorOffsetX, cursorOffsetY;
calc_scale_factor(currentScaleRatio_x, currentScaleRatio_y, sourceWidth, sourceHeight);
cursorOffsetX = (currentOutputWidth - sourceWidth * currentScaleRatio_x) / 2.0f;
cursorOffsetY = (currentOutputHeight - sourceHeight * currentScaleRatio_y) / 2.0f;
// Actual point on scaled screen where the cursor hotspot should be
scaledX = (winX - window->xwayland().a.x) * currentScaleRatio_x + cursorOffsetX;
scaledY = (winY - window->xwayland().a.y) * currentScaleRatio_y + cursorOffsetY;
if ( zoomScaleRatio != 1.0 )
{
scaledX += ((sourceWidth / 2) - winX) * currentScaleRatio_x;
scaledY += ((sourceHeight / 2) - winY) * currentScaleRatio_y;
}
// Apply the cursor offset inside the texture using the display scale
scaledX = scaledX - (m_hotspotX * cursor_scale);
scaledY = scaledY - (m_hotspotY * cursor_scale);
int curLayer = frameInfo->layerCount++;
FrameInfo_t::Layer_t *layer = &frameInfo->layers[ curLayer ];
layer->opacity = 1.0;
layer->scale.x = 1.0f / cursor_scale;
layer->scale.y = 1.0f / cursor_scale;
layer->offset.x = -scaledX;
layer->offset.y = -scaledY;
layer->zpos = g_zposCursor; // cursor, on top of both bottom layers
layer->applyColorMgmt = false;
layer->tex = m_texture;
layer->fbid = BIsNested() ? 0 : m_texture->fbid();
layer->filter = cursor_scale != 1.0f ? GamescopeUpscaleFilter::LINEAR : GamescopeUpscaleFilter::NEAREST;
layer->blackBorder = false;
layer->ctm = nullptr;
layer->colorspace = GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB;
}
void MouseCursor::updateCursorFeedback( bool bForce )
{
// Can't resolve this until cursor is un-dirtied.
if ( m_dirty && !bForce )
return;
bool bVisible = !isHidden();
if ( m_bCursorVisibleFeedback == bVisible && !bForce )
return;
uint32_t value = bVisible ? 1 : 0;
XChangeProperty(m_ctx->dpy, m_ctx->root, m_ctx->atoms.gamescopeCursorVisibleFeedback, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&value, 1 );
m_bCursorVisibleFeedback = bVisible;
m_needs_server_flush = true;
}
struct BaseLayerInfo_t
{
float scale[2];
float offset[2];
float opacity;
GamescopeUpscaleFilter filter;
};
std::array< BaseLayerInfo_t, HELD_COMMIT_COUNT > g_CachedPlanes = {};
static void
paint_cached_base_layer(const std::shared_ptr<commit_t>& commit, const BaseLayerInfo_t& base, struct FrameInfo_t *frameInfo, float flOpacityScale)
{
int curLayer = frameInfo->layerCount++;
FrameInfo_t::Layer_t *layer = &frameInfo->layers[ curLayer ];
layer->scale.x = base.scale[0];
layer->scale.y = base.scale[1];
layer->offset.x = base.offset[0];
layer->offset.y = base.offset[1];
layer->opacity = base.opacity * flOpacityScale;
layer->colorspace = commit->colorspace();
layer->ctm = nullptr;
if (layer->colorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB)
layer->ctm = s_scRGB709To2020Matrix;
layer->tex = commit->vulkanTex;
layer->fbid = commit->fb_id;
layer->filter = base.filter;
layer->blackBorder = true;
}
namespace PaintWindowFlag
{
static const uint32_t BasePlane = 1u << 0;
static const uint32_t FadeTarget = 1u << 1;
static const uint32_t NotificationMode = 1u << 2;
static const uint32_t DrawBorders = 1u << 3;
static const uint32_t NoScale = 1u << 4;
}
using PaintWindowFlags = uint32_t;
wlserver_vk_swapchain_feedback* steamcompmgr_get_base_layer_swapchain_feedback()
{
if ( !g_HeldCommits[ HELD_COMMIT_BASE ] )
return nullptr;
if ( !g_HeldCommits[ HELD_COMMIT_BASE ]->feedback )
return nullptr;
return &(*g_HeldCommits[ HELD_COMMIT_BASE ]->feedback);
}
static void
paint_window(steamcompmgr_win_t *w, steamcompmgr_win_t *scaleW, struct FrameInfo_t *frameInfo,
MouseCursor *cursor, PaintWindowFlags flags = 0, float flOpacityScale = 1.0f, steamcompmgr_win_t *fit = nullptr )
{
uint32_t sourceWidth, sourceHeight;
int drawXOffset = 0, drawYOffset = 0;
float currentScaleRatio_x = 1.0;
float currentScaleRatio_y = 1.0;
std::shared_ptr<commit_t> lastCommit;
if ( w )
get_window_last_done_commit( w, lastCommit );
if ( flags & PaintWindowFlag::BasePlane )
{
if ( !lastCommit )
{
// If we're the base plane and have no valid contents
// pick up that buffer we've been holding onto if we have one.
if ( g_HeldCommits[ HELD_COMMIT_BASE ] )
{
paint_cached_base_layer( g_HeldCommits[ HELD_COMMIT_BASE ], g_CachedPlanes[ HELD_COMMIT_BASE ], frameInfo, flOpacityScale );
return;
}
}
else
{
if ( g_bPendingFade )
{
fadeOutStartTime = get_time_in_milliseconds();
g_bPendingFade = false;
}
}
}
// Exit out if we have no window or
// no commit.
//
// We may have no commit if we're an overlay,
// in which case, we don't want to add it,
// or in the case of the base plane, this is our
// first ever frame so we have no cached base layer
// to hold on to, so we should not add a layer in that
// instance either.
if (!w || !lastCommit)
return;
// Base plane will stay as tex=0 if we don't have contents yet, which will
// make us fall back to compositing and use the Vulkan null texture
steamcompmgr_win_t *mainOverlayWindow = global_focus.overlayWindow;
const bool notificationMode = flags & PaintWindowFlag::NotificationMode;
if (notificationMode && !mainOverlayWindow)
return;
if (notificationMode)
{
sourceWidth = mainOverlayWindow->xwayland().a.width;
sourceHeight = mainOverlayWindow->xwayland().a.height;
}
else if ( flags & PaintWindowFlag::NoScale )
{
sourceWidth = currentOutputWidth;
sourceHeight = currentOutputHeight;
}
else
{
// If w == scaleW, then scale the window by the committed buffer size
// instead of the window size.
//
// Some games like Halo Infinite still make swapchains that are eg.
// 3840x2160 on a 720p window if you do borderless fullscreen.
//
// Typically XWayland would do a blit here to avoid that, but when we
// are using the bypass layer, we don't get that, so we need to handle
// this case explicitly.
if (w == scaleW) {
sourceWidth = lastCommit->vulkanTex->width();
sourceHeight = lastCommit->vulkanTex->height();
} else {
sourceWidth = scaleW->xwayland().a.width;
sourceHeight = scaleW->xwayland().a.height;
}
if ( fit )
{
// If we have an override window, try to fit it in as long as it won't make our scale go below 1.0.
sourceWidth = std::max<uint32_t>( sourceWidth, clamp<int>( fit->xwayland().a.x + fit->xwayland().a.width, 0, currentOutputWidth ) );
sourceHeight = std::max<uint32_t>( sourceHeight, clamp<int>( fit->xwayland().a.y + fit->xwayland().a.height, 0, currentOutputHeight ) );
}
}
bool offset = false;
if ( w->type == steamcompmgr_win_type_t::XWAYLAND )
offset = ( ( w->xwayland().a.x || w->xwayland().a.y ) && w != scaleW );
if (sourceWidth != currentOutputWidth || sourceHeight != currentOutputHeight || offset || globalScaleRatio != 1.0f)
{
calc_scale_factor(currentScaleRatio_x, currentScaleRatio_y, sourceWidth, sourceHeight);
drawXOffset = ((int)currentOutputWidth - (int)sourceWidth * currentScaleRatio_x) / 2.0f;
drawYOffset = ((int)currentOutputHeight - (int)sourceHeight * currentScaleRatio_y) / 2.0f;
if ( w->type == steamcompmgr_win_type_t::XWAYLAND && w != scaleW )
{
drawXOffset += w->xwayland().a.x * currentScaleRatio_x;
drawYOffset += w->xwayland().a.y * currentScaleRatio_y;
}
if ( zoomScaleRatio != 1.0 )
{
drawXOffset += (((int)sourceWidth / 2) - cursor->x()) * currentScaleRatio_x;
drawYOffset += (((int)sourceHeight / 2) - cursor->y()) * currentScaleRatio_y;
}
}
int curLayer = frameInfo->layerCount++;
FrameInfo_t::Layer_t *layer = &frameInfo->layers[ curLayer ];
layer->opacity = ( (w->isOverlay || w->isExternalOverlay) ? w->opacity / (float)OPAQUE : 1.0f ) * flOpacityScale;
layer->scale.x = 1.0 / currentScaleRatio_x;
layer->scale.y = 1.0 / currentScaleRatio_y;
if ( w != scaleW )
{
layer->offset.x = -drawXOffset;
layer->offset.y = -drawYOffset;
}
else if (notificationMode)
{
int xOffset = 0, yOffset = 0;
int width = w->xwayland().a.width * currentScaleRatio_x;
int height = w->xwayland().a.height * currentScaleRatio_y;
if (globalScaleRatio != 1.0f)
{
xOffset = (currentOutputWidth - currentOutputWidth * globalScaleRatio) / 2.0;
yOffset = (currentOutputHeight - currentOutputHeight * globalScaleRatio) / 2.0;
}
layer->offset.x = (currentOutputWidth - xOffset - width) * -1.0f;
layer->offset.y = (currentOutputHeight - yOffset - height) * -1.0f;
}
else
{
layer->offset.x = -drawXOffset;
layer->offset.y = -drawYOffset;
}
layer->blackBorder = flags & PaintWindowFlag::DrawBorders;
layer->applyColorMgmt = g_ColorMgmt.pending.enabled;
layer->zpos = g_zposBase;
if ( w != scaleW )
{
layer->zpos = g_zposOverride;
}
if ( w->isOverlay || w->isSteamStreamingClient )
{
layer->zpos = g_zposOverlay;
}
if ( w->isExternalOverlay )
{
layer->zpos = g_zposExternalOverlay;
}
layer->tex = lastCommit->vulkanTex;
layer->fbid = lastCommit->fb_id;
layer->filter = (w->isOverlay || w->isExternalOverlay) ? GamescopeUpscaleFilter::LINEAR : g_upscaleFilter;
layer->colorspace = lastCommit->colorspace();
layer->ctm = nullptr;
if (layer->colorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB)
layer->ctm = s_scRGB709To2020Matrix;
if (layer->filter == GamescopeUpscaleFilter::PIXEL)
{
// Don't bother doing more expensive filtering if we are sharp + integer.
if (float_is_integer(currentScaleRatio_x) && float_is_integer(currentScaleRatio_y))
layer->filter = GamescopeUpscaleFilter::NEAREST;
}
if ( flags & PaintWindowFlag::BasePlane )
{
BaseLayerInfo_t basePlane = {};
basePlane.scale[0] = layer->scale.x;
basePlane.scale[1] = layer->scale.y;
basePlane.offset[0] = layer->offset.x;
basePlane.offset[1] = layer->offset.y;
basePlane.opacity = layer->opacity;
basePlane.filter = layer->filter;
g_CachedPlanes[ HELD_COMMIT_BASE ] = basePlane;
if ( !(flags & PaintWindowFlag::FadeTarget) )
g_CachedPlanes[ HELD_COMMIT_FADE ] = basePlane;
}
}
bool g_bFirstFrame = true;
static bool is_fading_out()
{
return fadeOutStartTime || g_bPendingFade;
}
static void update_touch_scaling( const struct FrameInfo_t *frameInfo )
{
if ( !frameInfo->layerCount )
return;
focusedWindowScaleX = frameInfo->layers[ frameInfo->layerCount - 1 ].scale.x;
focusedWindowScaleY = frameInfo->layers[ frameInfo->layerCount - 1 ].scale.y;
focusedWindowOffsetX = frameInfo->layers[ frameInfo->layerCount - 1 ].offset.x;
focusedWindowOffsetY = frameInfo->layers[ frameInfo->layerCount - 1 ].offset.y;
}
static void
paint_all(bool async)
{
gamescope_xwayland_server_t *root_server = wlserver_get_xwayland_server(0);
xwayland_ctx_t *root_ctx = root_server->ctx.get();
static long long int paintID = 0;
update_color_mgmt();
paintID++;
gpuvis_trace_begin_ctx_printf( paintID, "paint_all" );
steamcompmgr_win_t *w;
steamcompmgr_win_t *overlay;
steamcompmgr_win_t *externalOverlay;
steamcompmgr_win_t *notification;
steamcompmgr_win_t *override;
steamcompmgr_win_t *input;
unsigned int currentTime = get_time_in_milliseconds();
bool fadingOut = ( currentTime - fadeOutStartTime < g_FadeOutDuration || g_bPendingFade ) && g_HeldCommits[HELD_COMMIT_FADE];
w = global_focus.focusWindow;
overlay = global_focus.overlayWindow;
externalOverlay = global_focus.externalOverlayWindow;
notification = global_focus.notificationWindow;
override = global_focus.overrideWindow;
input = global_focus.inputFocusWindow;
if (++frameCounter == 300)
{
currentFrameRate = 300 * 1000.0f / (currentTime - lastSampledFrameTime);
lastSampledFrameTime = currentTime;
frameCounter = 0;
stats_printf( "fps=%f\n", currentFrameRate );
if ( window_is_steam( w ) )
{
stats_printf( "focus=steam\n" );
}
else
{
stats_printf( "focus=%i\n", w ? w->appID : 0 );
}
}
struct FrameInfo_t frameInfo = {};
frameInfo.applyOutputColorMgmt = g_ColorMgmt.pending.enabled;
// If the window we'd paint as the base layer is the streaming client,
// find the video underlay and put it up first in the scenegraph
if ( w )
{
if ( w->isSteamStreamingClient == true )
{
steamcompmgr_win_t *videow = NULL;
bool bHasVideoUnderlay = false;
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
{
for ( videow = server->ctx->list; videow; videow = videow->xwayland().next )
{
if ( videow->isSteamStreamingClientVideo == true )
{
// TODO: also check matching AppID so we can have several pairs
paint_window(videow, videow, &frameInfo, global_focus.cursor, PaintWindowFlag::BasePlane | PaintWindowFlag::DrawBorders);
bHasVideoUnderlay = true;
break;
}
}
}
int nOldLayerCount = frameInfo.layerCount;
uint32_t flags = 0;
if ( !bHasVideoUnderlay )
flags |= PaintWindowFlag::BasePlane;
paint_window(w, w, &frameInfo, global_focus.cursor, flags);
update_touch_scaling( &frameInfo );
// paint UI unless it's fully hidden, which it communicates to us through opacity=0
// we paint it to extract scaling coefficients above, then remove the layer if one was added
if ( w->opacity == TRANSLUCENT && bHasVideoUnderlay && nOldLayerCount < frameInfo.layerCount )
frameInfo.layerCount--;
}
else
{
if ( fadingOut )
{
float opacityScale = g_bPendingFade
? 0.0f
: ((currentTime - fadeOutStartTime) / (float)g_FadeOutDuration);
paint_cached_base_layer(g_HeldCommits[HELD_COMMIT_FADE], g_CachedPlanes[HELD_COMMIT_FADE], &frameInfo, 1.0f - opacityScale);
paint_window(w, w, &frameInfo, global_focus.cursor, PaintWindowFlag::BasePlane | PaintWindowFlag::FadeTarget | PaintWindowFlag::DrawBorders, opacityScale, override);
}
else
{
{
if ( g_HeldCommits[HELD_COMMIT_FADE] )
{
g_HeldCommits[HELD_COMMIT_FADE] = nullptr;
g_bPendingFade = false;
fadeOutStartTime = 0;
global_focus.fadeWindow = None;
}
}
// Just draw focused window as normal, be it Steam or the game
paint_window(w, w, &frameInfo, global_focus.cursor, PaintWindowFlag::BasePlane | PaintWindowFlag::DrawBorders, 1.0f, override);
bool needsScaling = frameInfo.layers[0].scale.x < 0.999f && frameInfo.layers[0].scale.y < 0.999f;
frameInfo.useFSRLayer0 = g_upscaleFilter == GamescopeUpscaleFilter::FSR && needsScaling;
frameInfo.useNISLayer0 = g_upscaleFilter == GamescopeUpscaleFilter::NIS && needsScaling;
}
update_touch_scaling( &frameInfo );
}
}
else
{
if ( g_HeldCommits[HELD_COMMIT_BASE] )
paint_cached_base_layer(g_HeldCommits[HELD_COMMIT_BASE], g_CachedPlanes[HELD_COMMIT_BASE], &frameInfo, 1.0f);
}
// TODO: We want to paint this at the same scale as the normal window and probably
// with an offset.
// Josh: No override if we're streaming video
// as we will have too many layers. Better to be safe than sorry.
if ( override && w && !w->isSteamStreamingClient )
{
paint_window(override, w, &frameInfo, global_focus.cursor, 0, 1.0f, override);
// Don't update touch scaling for frameInfo. We don't ever make it our
// wlserver_mousefocus window.
//update_touch_scaling( &frameInfo );
}
// If we have any layers that aren't a cursor or overlay, then we have valid contents for presentation.
const bool bValidContents = frameInfo.layerCount > 0;
if (externalOverlay)
{
if (externalOverlay->opacity)
{
paint_window(externalOverlay, externalOverlay, &frameInfo, global_focus.cursor, PaintWindowFlag::NoScale);
if ( externalOverlay == global_focus.inputFocusWindow )
update_touch_scaling( &frameInfo );
}
}
if (overlay && overlay->opacity)
{
paint_window(overlay, overlay, &frameInfo, global_focus.cursor, PaintWindowFlag::DrawBorders);
if ( overlay == global_focus.inputFocusWindow )
update_touch_scaling( &frameInfo );
}
else
{
auto tex = vulkan_get_hacky_blank_texture();
if ( !BIsNested() && tex != nullptr )
{
// HACK! HACK HACK HACK
// To avoid stutter when toggling the overlay on
int curLayer = frameInfo.layerCount++;
FrameInfo_t::Layer_t *layer = &frameInfo.layers[ curLayer ];
layer->scale.x = g_nOutputWidth == tex->width() ? 1.0f : tex->width() / (float)g_nOutputWidth;
layer->scale.y = g_nOutputHeight == tex->height() ? 1.0f : tex->height() / (float)g_nOutputHeight;
layer->offset.x = 0.0f;
layer->offset.y = 0.0f;
layer->opacity = 1.0f; // BLAH
layer->zpos = g_zposOverlay;
layer->applyColorMgmt = g_ColorMgmt.pending.enabled;
layer->colorspace = GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR;
layer->ctm = nullptr;
layer->tex = tex;
layer->fbid = tex->fbid();
layer->filter = GamescopeUpscaleFilter::NEAREST;
layer->blackBorder = true;
}
}
if (notification)
{
if (notification->opacity)
{
paint_window(notification, notification, &frameInfo, global_focus.cursor, PaintWindowFlag::NotificationMode);
}
}
if (input)
{
// Make sure to un-dirty the texture before we do any painting logic.
// We determine whether we are grabbed etc this way.
global_focus.cursor->undirty();
}
bool bForceHideCursor = BIsSDLSession() && !bSteamCompMgrGrab;
bool bDrewCursor = false;
// Draw cursor if we need to
if (input && !bForceHideCursor) {
int nLayerCountBefore = frameInfo.layerCount;
global_focus.cursor->paint(
input, w == input ? override : nullptr,
&frameInfo);
int nLayerCountAfter = frameInfo.layerCount;
bDrewCursor = nLayerCountAfter > nLayerCountBefore;
}
if ( !bValidContents || ( BIsNested() == false && g_DRM.paused == true ) )
{
return;
}
unsigned int blurFadeTime = get_time_in_milliseconds() - g_BlurFadeStartTime;
bool blurFading = blurFadeTime < g_BlurFadeDuration;
BlurMode currentBlurMode = blurFading ? std::max(g_BlurMode, g_BlurModeOld) : g_BlurMode;
if (currentBlurMode && !(frameInfo.layerCount <= 1 && currentBlurMode == BLUR_MODE_COND))
{
frameInfo.blurLayer0 = currentBlurMode;
frameInfo.blurRadius = g_BlurRadius;
if (blurFading)
{
float ratio = blurFadeTime / (float) g_BlurFadeDuration;
bool fadingIn = g_BlurMode > g_BlurModeOld;
if (!fadingIn)
ratio = 1.0 - ratio;
frameInfo.blurRadius = ratio * g_BlurRadius;
}
frameInfo.useFSRLayer0 = false;
frameInfo.useNISLayer0 = false;
}
g_bFSRActive = frameInfo.useFSRLayer0;
bool bWasFirstFrame = g_bFirstFrame;
g_bFirstFrame = false;
bool bDoComposite = true;
// Handoff from whatever thread to this one since we check ours twice
int takeScreenshot = g_nTakeScreenshot.exchange( 0 );
bool propertyRequestedScreenshot = g_bPropertyRequestedScreenshot;
g_bPropertyRequestedScreenshot = false;
struct pipewire_buffer *pw_buffer = nullptr;
#if HAVE_PIPEWIRE
pw_buffer = dequeue_pipewire_buffer();
#endif
update_app_target_refresh_cycle();
int nDynamicRefresh = g_nDynamicRefreshRate[drm_get_screen_type( &g_DRM )];
int nTargetRefresh = nDynamicRefresh && steamcompmgr_window_should_refresh_switch( global_focus.focusWindow )// && !global_focus.overlayWindow
? nDynamicRefresh
: drm_get_default_refresh( &g_DRM );
uint64_t now = get_time_in_nanos();
if ( g_nOutputRefresh == nTargetRefresh )
g_uDynamicRefreshEqualityTime = now;
if ( !BIsNested() && g_nOutputRefresh != nTargetRefresh && g_uDynamicRefreshEqualityTime + g_uDynamicRefreshDelay < now )
drm_set_refresh( &g_DRM, nTargetRefresh );
bool bLayer0ScreenSize = close_enough(frameInfo.layers[0].scale.x, 1.0f) && close_enough(frameInfo.layers[0].scale.y, 1.0f);
bool bNeedsCompositeFromFilter = (g_upscaleFilter == GamescopeUpscaleFilter::NEAREST || g_upscaleFilter == GamescopeUpscaleFilter::PIXEL) && !bLayer0ScreenSize;
bool bDoMuraCompensation = is_mura_correction_enabled() && frameInfo.layerCount && !pw_buffer;
if ( bDoMuraCompensation )
{
auto& MuraCorrectionImage = s_MuraCorrectionImage[drm_get_screen_type( &g_DRM )];
int curLayer = frameInfo.layerCount++;
FrameInfo_t::Layer_t *layer = &frameInfo.layers[ curLayer ];
layer->applyColorMgmt = false;
layer->fbid = MuraCorrectionImage->fbid();
layer->offset = vec2_t{ 0, 0 };
layer->scale = vec2_t{ 1.0f, 1.0f };
layer->blackBorder = true;
layer->colorspace = GAMESCOPE_APP_TEXTURE_COLORSPACE_PASSTHRU;
layer->opacity = 1.0f;
layer->zpos = g_zposMuraCorrection;
layer->filter = GamescopeUpscaleFilter::NEAREST;
layer->tex = MuraCorrectionImage;
layer->ctm = s_MuraCTMBlob[drm_get_screen_type( &g_DRM )];
// Blending needs to be done in Gamma 2.2 space for mura correction to work.
frameInfo.applyOutputColorMgmt = false;
}
bool bWantsPartialComposite = frameInfo.layerCount >= 3 && !kDisablePartialComposition;
bool bNeedsFullComposite = BIsNested();
bNeedsFullComposite |= alwaysComposite;
bNeedsFullComposite |= pw_buffer != nullptr;
bNeedsFullComposite |= bWasFirstFrame;
bNeedsFullComposite |= frameInfo.useFSRLayer0;
bNeedsFullComposite |= frameInfo.useNISLayer0;
bNeedsFullComposite |= frameInfo.blurLayer0;
bNeedsFullComposite |= bNeedsCompositeFromFilter;
bNeedsFullComposite |= bDrewCursor;
bNeedsFullComposite |= g_bColorSliderInUse;
bNeedsFullComposite |= fadingOut;
bNeedsFullComposite |= !g_reshade_effect.empty();
constexpr bool bHackForceNV12DumpScreenshot = false;
for (uint32_t i = 0; i < EOTF_Count; i++)
{
if (g_ColorMgmtLuts[i].HasLuts())
{
frameInfo.shaperLut[i] = g_ColorMgmtLuts[i].vk_lut1d;
frameInfo.lut3D[i] = g_ColorMgmtLuts[i].vk_lut3d;
}
}
if ( !BIsNested() && g_bOutputHDREnabled )
{
bNeedsFullComposite |= g_bHDRItmEnable;
if ( !drm_supports_color_mgmt(&g_DRM) )
bNeedsFullComposite |= ( frameInfo.layerCount > 1 || frameInfo.layers[0].colorspace != GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ );
}
bNeedsFullComposite |= !!(g_uCompositeDebug & CompositeDebugFlag::Heatmap);
static int g_nLastSingleOverlayZPos = 0;
static bool g_bWasCompositing = false;
if ( !bNeedsFullComposite && !bWantsPartialComposite )
{
int ret = drm_prepare( &g_DRM, async, &frameInfo );
if ( ret == 0 )
{
bDoComposite = false;
g_bWasPartialComposite = false;
g_bWasCompositing = false;
if ( frameInfo.layerCount == 2 )
g_nLastSingleOverlayZPos = frameInfo.layers[1].zpos;
}
else if ( ret == -EACCES )
return;
}
// Update to let the vblank manager know we are currently compositing.
g_bCurrentlyCompositing = bDoComposite;
if ( bDoComposite == true )
{
if ( kDisablePartialComposition )
bNeedsFullComposite = true;
std::shared_ptr<CVulkanTexture> pPipewireTexture = nullptr;
#if HAVE_PIPEWIRE
if ( pw_buffer != nullptr )
{
pPipewireTexture = pw_buffer->texture;
}
#endif
struct FrameInfo_t compositeFrameInfo = frameInfo;
if ( compositeFrameInfo.layerCount == 1 )
{
// If we failed to flip a single plane then
// we definitely need to composite for some reason...
bNeedsFullComposite = true;
}
if ( !bNeedsFullComposite )
{
// If we want to partial composite, fallback to full
// composite if we have mismatching colorspaces in our overlays.
// This is 2, and we do i-1 so 1...layerCount. So AFTER we have removed baseplane.
// Overlays only.
//
// Josh:
// We could handle mismatching colorspaces for partial composition
// but I want to keep overlay -> partial composition promotion as simple
// as possible, using the same 3D + SHAPER LUTs + BLEND in DRM
// as changing them is incredibly expensive!! It takes forever.
// We can't just point it to random BDA or whatever, it has to be uploaded slowly
// thru registers which is SUPER SLOW.
// This avoids stutter.
for ( int i = 2; i < compositeFrameInfo.layerCount; i++ )
{
if ( frameInfo.layers[i - 1].colorspace != frameInfo.layers[i].colorspace )
{
bNeedsFullComposite = true;
break;
}
}
}
// If we ever promoted from partial -> full, for the first frame
// do NOT defer this partial composition.
// We were already stalling for the full composition before, so it's not an issue
// for latency, we just need to make sure we get 1 partial frame that isn't deferred
// in time so we don't lose layers.
bool bDefer = !bNeedsFullComposite && ( !g_bWasCompositing || g_bWasPartialComposite );
// If doing a partial composition then remove the baseplane
// from our frameinfo to composite.
if ( !bNeedsFullComposite )
{
for ( int i = 1; i < compositeFrameInfo.layerCount; i++ )
compositeFrameInfo.layers[i - 1] = compositeFrameInfo.layers[i];
compositeFrameInfo.layerCount -= 1;
// When doing partial composition, apply the shaper + 3D LUT stuff
// at scanout.
for ( uint32_t nEOTF = 0; nEOTF < EOTF_Count; nEOTF++ ) {
compositeFrameInfo.shaperLut[ nEOTF ] = nullptr;
compositeFrameInfo.lut3D[ nEOTF ] = nullptr;
}
}
// If using composite debug markers, make sure we mark them as partial
// so we know!
if ( bDefer && !!( g_uCompositeDebug & CompositeDebugFlag::Markers ) )
g_uCompositeDebug |= CompositeDebugFlag::Markers_Partial;
bool bResult;
// If using a pipewire stream, apply screenshot color management.
if ( pPipewireTexture )
{
for ( uint32_t nInputEOTF = 0; nInputEOTF < EOTF_Count; nInputEOTF++ )
{
compositeFrameInfo.lut3D[nInputEOTF] = g_ScreenshotColorMgmtLuts[nInputEOTF].vk_lut3d;
compositeFrameInfo.shaperLut[nInputEOTF] = g_ScreenshotColorMgmtLuts[nInputEOTF].vk_lut1d;
}
vulkan_composite( &compositeFrameInfo, pPipewireTexture, !bNeedsFullComposite, bDefer, nullptr, false );
for ( uint32_t nInputEOTF = 0; nInputEOTF < EOTF_Count; nInputEOTF++ )
{
if (g_ColorMgmtLuts[nInputEOTF].HasLuts())
{
compositeFrameInfo.shaperLut[nInputEOTF] = g_ColorMgmtLuts[nInputEOTF].vk_lut1d;
compositeFrameInfo.lut3D[nInputEOTF] = g_ColorMgmtLuts[nInputEOTF].vk_lut3d;
}
}
}
bResult = vulkan_composite( &compositeFrameInfo, nullptr, !bNeedsFullComposite, bDefer );
g_bWasCompositing = true;
g_uCompositeDebug &= ~CompositeDebugFlag::Markers_Partial;
if ( bResult != true )
{
xwm_log.errorf("vulkan_composite failed");
return;
}
if ( BIsNested() == true )
{
#if HAVE_OPENVR
if ( BIsVRSession() )
{
vulkan_present_to_openvr();
}
else if ( BIsSDLSession() )
#endif
{
vulkan_present_to_window();
}
// Update the time it took us to commit
g_uVblankDrawTimeNS = get_time_in_nanos() - g_SteamCompMgrVBlankTime.pipe_write_time;
}
else
{
struct FrameInfo_t presentCompFrameInfo = {};
if ( bNeedsFullComposite )
{
presentCompFrameInfo.applyOutputColorMgmt = false;
presentCompFrameInfo.layerCount = 1;
FrameInfo_t::Layer_t *baseLayer = &presentCompFrameInfo.layers[ 0 ];
baseLayer->scale.x = 1.0;
baseLayer->scale.y = 1.0;
baseLayer->opacity = 1.0;
baseLayer->zpos = g_zposBase;
baseLayer->tex = vulkan_get_last_output_image( false, false );
baseLayer->fbid = baseLayer->tex->fbid();
baseLayer->applyColorMgmt = false;
baseLayer->filter = GamescopeUpscaleFilter::NEAREST;
baseLayer->ctm = nullptr;
baseLayer->colorspace = g_bOutputHDREnabled ? GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ : GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB;
g_bWasPartialComposite = false;
}
else
{
if ( g_bWasPartialComposite || !bDefer )
{
presentCompFrameInfo.applyOutputColorMgmt = g_ColorMgmt.pending.enabled;
presentCompFrameInfo.layerCount = 2;
presentCompFrameInfo.layers[ 0 ] = frameInfo.layers[ 0 ];
presentCompFrameInfo.layers[ 0 ].zpos = g_zposBase;
FrameInfo_t::Layer_t *overlayLayer = &presentCompFrameInfo.layers[ 1 ];
overlayLayer->scale.x = 1.0;
overlayLayer->scale.y = 1.0;
overlayLayer->opacity = 1.0;
overlayLayer->zpos = g_zposOverlay;
overlayLayer->tex = vulkan_get_last_output_image( true, bDefer );
overlayLayer->fbid = overlayLayer->tex->fbid();
overlayLayer->applyColorMgmt = g_ColorMgmt.pending.enabled;
overlayLayer->filter = GamescopeUpscaleFilter::NEAREST;
// Partial composition stuff has the same colorspace.
// So read that from the composite frame info
overlayLayer->ctm = nullptr;
overlayLayer->colorspace = compositeFrameInfo.layers[0].colorspace;
}
else
{
// Use whatever overlay we had last while waiting for the
// partial composition to have anything queued.
presentCompFrameInfo.applyOutputColorMgmt = g_ColorMgmt.pending.enabled;
presentCompFrameInfo.layerCount = 1;
presentCompFrameInfo.layers[ 0 ] = frameInfo.layers[ 0 ];
presentCompFrameInfo.layers[ 0 ].zpos = g_zposBase;
FrameInfo_t::Layer_t *lastPresentedOverlayLayer = nullptr;
for (int i = 0; i < frameInfo.layerCount; i++)
{
if (frameInfo.layers[i].zpos == g_nLastSingleOverlayZPos)
{
lastPresentedOverlayLayer = &frameInfo.layers[i];
break;
}
}
if (lastPresentedOverlayLayer)
{
FrameInfo_t::Layer_t *overlayLayer = &presentCompFrameInfo.layers[ 1 ];
*overlayLayer = *lastPresentedOverlayLayer;
overlayLayer->zpos = g_zposOverlay;
presentCompFrameInfo.layerCount = 2;
}
}
g_bWasPartialComposite = true;
}
int ret = drm_prepare( &g_DRM, async, &presentCompFrameInfo );
// Happens when we're VT-switched away
if ( ret == -EACCES )
return;
if ( ret != 0 )
{
if ( g_DRM.current.mode_id == 0 )
{
xwm_log.errorf("We failed our modeset and have no mode to fall back to! (Initial modeset failed?): %s", strerror(-ret));
abort();
}
xwm_log.errorf("Failed to prepare 1-layer flip (%s), trying again with previous mode if modeset needed", strerror( -ret ));
drm_rollback( &g_DRM );
// Try once again to in case we need to fall back to another mode.
ret = drm_prepare( &g_DRM, async, &compositeFrameInfo );
// Happens when we're VT-switched away
if ( ret == -EACCES )
return;
if ( ret != 0 )
{
xwm_log.errorf("Failed to prepare 1-layer flip entirely: %s", strerror( -ret ));
// We should always handle a 1-layer flip, this used to abort,
// but lets be more friendly and just avoid a commit and try again later.
// Let's re-poll our state, and force grab the best connector again.
//
// Some intense connector hotplugging could be occuring and the
// connector could become destroyed before we had a chance to use it
// as we hadn't reffed it in a commit yet.
g_DRM.out_of_date = 2;
drm_poll_state( &g_DRM );
return;
}
}
drm_commit( &g_DRM, &compositeFrameInfo );
}
#if HAVE_PIPEWIRE
if ( pw_buffer != nullptr )
{
push_pipewire_buffer(pw_buffer);
// TODO: make sure the pw_buffer isn't lost in one of the failure
// code-paths above
}
#endif
}
else
{
assert( BIsNested() == false );
drm_commit( &g_DRM, &frameInfo );
}
if ( takeScreenshot )
{
uint32_t drmCaptureFormat = bHackForceNV12DumpScreenshot
? DRM_FORMAT_NV12
: DRM_FORMAT_XRGB8888;
std::shared_ptr<CVulkanTexture> pScreenshotTexture = vulkan_acquire_screenshot_texture(g_nOutputWidth, g_nOutputHeight, false, drmCaptureFormat);
if ( pScreenshotTexture )
{
if ( drmCaptureFormat == DRM_FORMAT_NV12 || takeScreenshot != TAKE_SCREENSHOT_SCREEN_BUFFER )
{
// Basically no color mgmt applied for screenshots. (aside from being able to handle HDR content with LUTs)
for ( uint32_t nInputEOTF = 0; nInputEOTF < EOTF_Count; nInputEOTF++ )
{
frameInfo.lut3D[nInputEOTF] = g_ScreenshotColorMgmtLuts[nInputEOTF].vk_lut3d;
frameInfo.shaperLut[nInputEOTF] = g_ScreenshotColorMgmtLuts[nInputEOTF].vk_lut1d;
}
if ( takeScreenshot == TAKE_SCREENSHOT_BASEPLANE_ONLY )
{
// Remove everything but base planes from the screenshot.
for (int i = 0; i < frameInfo.layerCount; i++)
{
if (frameInfo.layers[i].zpos >= (int)g_zposExternalOverlay)
{
frameInfo.layerCount = i;
break;
}
}
// Re-enable output color management (blending) if it was disabled by mura.
frameInfo.applyOutputColorMgmt = true;
}
else
{
if ( is_mura_correction_enabled() )
{
// Remove the last layer which is for mura...
for (int i = 0; i < frameInfo.layerCount; i++)
{
if (frameInfo.layers[i].zpos >= (int)g_zposMuraCorrection)
{
frameInfo.layerCount = i;
break;
}
}
// Re-enable output color management (blending) if it was disabled by mura.
frameInfo.applyOutputColorMgmt = true;
}
}
}
frameInfo.applyOutputColorMgmt = true;
bool bResult;
if ( drmCaptureFormat == DRM_FORMAT_NV12 )
bResult = vulkan_composite( &frameInfo, pScreenshotTexture, false, false, nullptr );
else if ( takeScreenshot == TAKE_SCREENSHOT_FULL_COMPOSITION || takeScreenshot == TAKE_SCREENSHOT_SCREEN_BUFFER )
bResult = vulkan_composite( &frameInfo, nullptr, false, false, pScreenshotTexture );
else
bResult = vulkan_screenshot( &frameInfo, pScreenshotTexture );
if ( bResult != true )
{
xwm_log.errorf("vulkan_screenshot failed");
return;
}
std::thread screenshotThread = std::thread([=] {
pthread_setname_np( pthread_self(), "gamescope-scrsh" );
const uint8_t *mappedData = pScreenshotTexture->mappedData();
if (pScreenshotTexture->format() == VK_FORMAT_B8G8R8A8_UNORM)
{
// Make our own copy of the image to remove the alpha channel.
auto imageData = std::vector<uint8_t>(currentOutputWidth * currentOutputHeight * 4);
const uint32_t comp = 4;
const uint32_t pitch = currentOutputWidth * comp;
for (uint32_t y = 0; y < currentOutputHeight; y++)
{
for (uint32_t x = 0; x < currentOutputWidth; x++)
{
// BGR...
imageData[y * pitch + x * comp + 0] = mappedData[y * pScreenshotTexture->rowPitch() + x * comp + 2];
imageData[y * pitch + x * comp + 1] = mappedData[y * pScreenshotTexture->rowPitch() + x * comp + 1];
imageData[y * pitch + x * comp + 2] = mappedData[y * pScreenshotTexture->rowPitch() + x * comp + 0];
imageData[y * pitch + x * comp + 3] = 255;
}
}
char pTimeBuffer[1024] = "/tmp/gamescope.png";
if ( !propertyRequestedScreenshot )
{
time_t currentTime = time(0);
struct tm *localTime = localtime( &currentTime );
strftime( pTimeBuffer, sizeof( pTimeBuffer ), "/tmp/gamescope_%Y-%m-%d_%H-%M-%S.png", localTime );
}
if ( stbi_write_png(pTimeBuffer, currentOutputWidth, currentOutputHeight, 4, imageData.data(), pitch) )
{
xwm_log.infof("Screenshot saved to %s", pTimeBuffer);
}
else
{
xwm_log.errorf( "Failed to save screenshot to %s", pTimeBuffer );
}
}
else if (pScreenshotTexture->format() == VK_FORMAT_G8_B8R8_2PLANE_420_UNORM)
{
char pTimeBuffer[1024] = "/tmp/gamescope.raw";
if ( !propertyRequestedScreenshot )
{
time_t currentTime = time(0);
struct tm *localTime = localtime( &currentTime );
strftime( pTimeBuffer, sizeof( pTimeBuffer ), "/tmp/gamescope_%Y-%m-%d_%H-%M-%S.raw", localTime );
}
FILE *file = fopen(pTimeBuffer, "wb");
if (file)
{
fwrite(mappedData, 1, pScreenshotTexture->totalSize(), file );
fclose(file);
char cmd[4096];
sprintf(cmd, "ffmpeg -f rawvideo -pixel_format nv12 -video_size %dx%d -i %s %s_encoded.png", pScreenshotTexture->width(), pScreenshotTexture->height(), pTimeBuffer, pTimeBuffer);
int ret = system(cmd);
/* Above call may fail, ffmpeg returns 0 on success */
if (ret) {
xwm_log.infof("Ffmpeg call return status %i", ret);
xwm_log.errorf( "Failed to save screenshot to %s", pTimeBuffer );
} else {
xwm_log.infof("Screenshot saved to %s", pTimeBuffer);
}
}
else
{
xwm_log.errorf( "Failed to save screenshot to %s", pTimeBuffer );
}
}
XDeleteProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeScreenShotAtom );
});
screenshotThread.detach();
takeScreenshot = 0;
}
else
{
xwm_log.errorf( "Oh no, we ran out of screenshot images. Not actually writing a screenshot." );
XDeleteProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeScreenShotAtom );
takeScreenshot = 0;
}
}
gpuvis_trace_end_ctx_printf( paintID, "paint_all" );
gpuvis_trace_printf( "paint_all %i layers, composite %i", (int)frameInfo.layerCount, bDoComposite );
}
/* Get prop from window
* not found: default
* otherwise the value
*/
__attribute__((__no_sanitize_address__)) // x11 broken, returns format 32 even when it only malloc'ed one byte. :(
static unsigned int
get_prop(xwayland_ctx_t *ctx, Window win, Atom prop, unsigned int def, bool *found = nullptr )
{
Atom actual;
int format;
unsigned long n, left;
unsigned char *data;
int result = XGetWindowProperty(ctx->dpy, win, prop, 0L, 1L, false,
XA_CARDINAL, &actual, &format,
&n, &left, &data);
if (result == Success && data != NULL)
{
unsigned int i;
memcpy(&i, data, sizeof(unsigned int));
XFree((void *) data);
if ( found != nullptr )
{
*found = true;
}
return i;
}
if ( found != nullptr )
{
*found = false;
}
return def;
}
// vectored version, return value is whether anything was found
__attribute__((__no_sanitize_address__)) // x11 broken :(
bool get_prop( xwayland_ctx_t *ctx, Window win, Atom prop, std::vector< uint32_t > &vecResult )
{
Atom actual;
int format;
unsigned long n, left;
vecResult.clear();
uint64_t *data;
int result = XGetWindowProperty(ctx->dpy, win, prop, 0L, ~0UL, false,
XA_CARDINAL, &actual, &format,
&n, &left, ( unsigned char** )&data);
if (result == Success && data != NULL)
{
for ( uint32_t i = 0; i < n; i++ )
{
vecResult.push_back( data[ i ] );
}
XFree((void *) data);
return true;
}
return false;
}
std::string get_string_prop( xwayland_ctx_t *ctx, Window win, Atom prop )
{
XTextProperty tp;
if ( !XGetTextProperty( ctx->dpy, win, &tp, prop ) )
return "";
std::string value = reinterpret_cast<const char *>( tp.value );
XFree( tp.value );
return value;
}
static bool
win_has_game_id( steamcompmgr_win_t *w )
{
return w->appID != 0;
}
static bool
win_is_useless( steamcompmgr_win_t *w )
{
if (w->type != steamcompmgr_win_type_t::XWAYLAND)
return false;
// Windows that are 1x1 are pretty useless for override redirects.
// Just ignore them.
// Fixes the Xbox Login in Age of Empires 2: DE.
return w->xwayland().a.width == 1 && w->xwayland().a.height == 1;
}
static bool
win_is_override_redirect( steamcompmgr_win_t *w )
{
if (w->type != steamcompmgr_win_type_t::XWAYLAND)
return false;
return w->xwayland().a.override_redirect && !w->ignoreOverrideRedirect && !win_is_useless( w );
}
static bool
win_skip_taskbar_and_pager( steamcompmgr_win_t *w )
{
return w->skipTaskbar && w->skipPager;
}
static bool
win_skip_and_not_fullscreen( steamcompmgr_win_t *w )
{
return win_skip_taskbar_and_pager( w ) && !w->isFullscreen;
}
static bool
win_maybe_a_dropdown( steamcompmgr_win_t *w )
{
if ( w->type != steamcompmgr_win_type_t::XWAYLAND )
return false;
// Josh:
// Right now we don't get enough info from Wine
// about the true nature of windows to distringuish
// something like the Fallout 4 Options menu from the
// Warframe language dropdown. Until we get more stuff
// exposed for that, there is this workaround to let that work.
if ( w->appID == 230410 && w->maybe_a_dropdown && w->xwayland().transientFor && ( w->skipPager || w->skipTaskbar ) )
return !win_is_useless( w );
// Work around Antichamber splash screen until we hook up
// the Proton window style deduction.
if ( w->appID == 219890 )
return false;
// The Launcher in Witcher 2 (20920) has a clear window with WS_EX_LAYERED on top of it.
//
// The Age of Empires 2 Launcher also has a WS_EX_LAYERED window to separate controls
// from its backing, which this seems to handle, although we seemingly don't handle
// it's transparency yet, which I do not understand.
//
// Layered windows are windows that are meant to be transparent
// with alpha blending + visual fx.
// https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features
//
// TODO: Come back to me for original Age of Empires HD launcher.
// Does that use it? It wants blending!
//
// Only do this if we have CONTROLPARENT right now. Some other apps, such as the
// Street Fighter V (310950) Splash Screen also use LAYERED and TOOLWINDOW, and we don't
// want that to be overlayed.
// Ignore LAYERED if it's marked as top-level with WS_EX_APPWINDOW.
// TODO: Find more apps using LAYERED.
const uint32_t validLayered = WS_EX_CONTROLPARENT | WS_EX_LAYERED;
const uint32_t invalidLayered = WS_EX_APPWINDOW;
if ( w->hasHwndStyleEx &&
( ( w->hwndStyleEx & validLayered ) == validLayered ) &&
( ( w->hwndStyleEx & invalidLayered ) == 0 ) )
return true;
// Josh:
// The logic here is as follows. The window will be treated as a dropdown if:
//
// If this window has a fixed position on the screen + static gravity:
// - If the window has either skipPage or skipTaskbar
// - If the window isn't a dialog, always treat it as a dropdown, as it's
// probably meant to be some form of popup.
// - If the window is a dialog
// - If the window has transient for, disregard it, as it is trying to redirecting us elsewhere
// ie. a settings menu dialog popup or something.
// - If the window has both skip taskbar and pager, treat it as a dialog.
bool valid_maybe_a_dropdown =
w->maybe_a_dropdown && ( ( !w->is_dialog || ( !w->xwayland().transientFor && win_skip_and_not_fullscreen( w ) ) ) && ( w->skipPager || w->skipTaskbar ) );
return ( valid_maybe_a_dropdown || win_is_override_redirect( w ) ) && !win_is_useless( w );
}
static bool
win_is_disabled( steamcompmgr_win_t *w )
{
if ( !w->hasHwndStyle )
return false;
return !!(w->hwndStyle & WS_DISABLED);
}
/* Returns true if a's focus priority > b's.
*
* This function establishes a list of criteria to decide which window should
* have focus. The first criteria has higher priority. If the first criteria
* is a tie, fallback to the second one, then the third, and so on.
*
* The general workflow is:
*
* if ( windows don't have the same criteria value )
* return true if a should be focused;
* // This is a tie, fallback to the next criteria
*/
static bool
is_focus_priority_greater( steamcompmgr_win_t *a, steamcompmgr_win_t *b )
{
if ( win_has_game_id( a ) != win_has_game_id( b ) )
return win_has_game_id( a );
// We allow using an override redirect window in some cases, but if we have
// a choice between two windows we always prefer the non-override redirect
// one.
if ( win_is_override_redirect( a ) != win_is_override_redirect( b ) )
return !win_is_override_redirect( a );
// If the window is 1x1 then prefer anything else we have.
if ( win_is_useless( a ) != win_is_useless( b ) )
return !win_is_useless( a );
if ( win_maybe_a_dropdown( a ) != win_maybe_a_dropdown( b ) )
return !win_maybe_a_dropdown( a );
if ( win_is_disabled( a ) != win_is_disabled( b ) )
return !win_is_disabled( a );
// Wine sets SKIP_TASKBAR and SKIP_PAGER hints for WS_EX_NOACTIVATE windows.
// See https://github.com/Plagman/gamescope/issues/87
if ( win_skip_and_not_fullscreen( a ) != win_skip_and_not_fullscreen( b ) )
return !win_skip_and_not_fullscreen( a );
// Prefer normal windows over dialogs
// if we are an override redirect/dropdown window.
if ( win_maybe_a_dropdown( a ) && win_maybe_a_dropdown( b ) &&
a->is_dialog != b->is_dialog )
return !a->is_dialog;
if (a->type != steamcompmgr_win_type_t::XWAYLAND)
{
return true;
}
// Attempt to tie-break dropdowns by transient-for.
if ( win_maybe_a_dropdown( a ) && win_maybe_a_dropdown( b ) &&
!a->xwayland().transientFor != !b->xwayland().transientFor )
return !a->xwayland().transientFor;
if ( win_has_game_id( a ) && a->xwayland().map_sequence != b->xwayland().map_sequence )
return a->xwayland().map_sequence > b->xwayland().map_sequence;
// The damage sequences are only relevant for game windows.
if ( win_has_game_id( a ) && a->xwayland().damage_sequence != b->xwayland().damage_sequence )
return a->xwayland().damage_sequence > b->xwayland().damage_sequence;
return false;
}
static bool is_good_override_candidate( steamcompmgr_win_t *override, steamcompmgr_win_t* focus )
{
// Some Chrome/Edge dropdowns (ie. FH5 xbox login) will automatically close themselves if you
// focus them while they are meant to be offscreen (-1,-1 and 1x1) so check that the
// override's position is on-screen.
if ( !focus )
return false;
return override != focus && override->xwayland().a.x >= 0 && override->xwayland().a.y >= 0;
}
static bool
pick_primary_focus_and_override(focus_t *out, Window focusControlWindow, const std::vector<steamcompmgr_win_t*>& vecPossibleFocusWindows, bool globalFocus, const std::vector<uint32_t>& ctxFocusControlAppIDs)
{
bool localGameFocused = false;
steamcompmgr_win_t *focus = NULL, *override_focus = NULL;
bool controlledFocus = focusControlWindow != None || !ctxFocusControlAppIDs.empty();
if ( controlledFocus )
{
if ( focusControlWindow != None )
{
for ( steamcompmgr_win_t *focusable_window : vecPossibleFocusWindows )
{
if ( focusable_window->type != steamcompmgr_win_type_t::XWAYLAND )
continue;
if ( focusable_window->xwayland().id == focusControlWindow )
{
focus = focusable_window;
localGameFocused = true;
goto found;
}
}
}
for ( auto focusable_appid : ctxFocusControlAppIDs )
{
for ( steamcompmgr_win_t *focusable_window : vecPossibleFocusWindows )
{
// HACK: Bring any Wayland windows to global focus for now
// until appID stuff is plumbed for them.
if ( focusable_window->type == steamcompmgr_win_type_t::XDG )
{
focus = focusable_window;
localGameFocused = true;
goto found;
}
if ( focusable_window->appID == focusable_appid )
{
focus = focusable_window;
localGameFocused = true;
goto found;
}
}
}
found:;
}
if ( !focus && ( !globalFocus || !controlledFocus ) )
{
if ( !vecPossibleFocusWindows.empty() )
{
focus = vecPossibleFocusWindows[ 0 ];
localGameFocused = focus->appID != 0;
}
}
auto resolveTransientOverrides = [&](bool maybe)
{
if ( !focus || focus->type != steamcompmgr_win_type_t::XWAYLAND )
return;
// Do some searches to find transient links to override redirects too.
while ( true )
{
bool bFoundTransient = false;
for ( steamcompmgr_win_t *candidate : vecPossibleFocusWindows )
{
if ( candidate->type != steamcompmgr_win_type_t::XWAYLAND )
continue;
bool is_dropdown = maybe ? win_maybe_a_dropdown( candidate ) : win_is_override_redirect( candidate );
if ( ( !override_focus || candidate != override_focus ) && candidate != focus &&
( ( !override_focus && candidate->xwayland().transientFor == focus->xwayland().id ) || ( override_focus && candidate->xwayland().transientFor == override_focus->xwayland().id ) ) &&
is_dropdown)
{
bFoundTransient = true;
override_focus = candidate;
break;
}
}
// Hopefully we can't have transient cycles or we'll have to maintain a list of visited windows here
if ( bFoundTransient == false )
break;
}
};
if ( focus && focus->type == steamcompmgr_win_type_t::XWAYLAND )
{
if ( !focusControlWindow )
{
// Do some searches through game windows to follow transient links if needed
while ( true )
{
bool bFoundTransient = false;
for ( steamcompmgr_win_t *candidate : vecPossibleFocusWindows )
{
if ( candidate->type != steamcompmgr_win_type_t::XWAYLAND )
continue;
if ( candidate != focus && candidate->xwayland().transientFor == focus->xwayland().id && !win_maybe_a_dropdown( candidate ) )
{
bFoundTransient = true;
focus = candidate;
break;
}
}
// Hopefully we can't have transient cycles or we'll have to maintain a list of visited windows here
if ( bFoundTransient == false )
break;
}
}
if ( !override_focus )
{
if ( !ctxFocusControlAppIDs.empty() )
{
for ( steamcompmgr_win_t *override : vecPossibleFocusWindows )
{
if ( win_is_override_redirect(override) && is_good_override_candidate(override, focus) && override->appID == focus->appID ) {
override_focus = override;
break;
}
}
}
else if ( !vecPossibleFocusWindows.empty() )
{
for ( steamcompmgr_win_t *override : vecPossibleFocusWindows )
{
if ( win_is_override_redirect(override) && is_good_override_candidate(override, focus) ) {
override_focus = override;
break;
}
}
}
resolveTransientOverrides( false );
}
}
if ( focus )
{
if ( window_has_commits( focus ) )
out->focusWindow = focus;
else
out->outdatedInteractiveFocus = true;
// Always update X's idea of focus, but still dirty
// the it being outdated so we can resolve that globally later.
//
// Only affecting X and not the WL idea of focus here,
// we always want to think the window is focused.
// but our real presenting focus and input focus can be elsewhere.
if ( !globalFocus )
out->focusWindow = focus;
}
if ( !override_focus && focus )
{
if ( controlledFocus )
{
for ( auto focusable_appid : ctxFocusControlAppIDs )
{
for ( steamcompmgr_win_t *fake_override : vecPossibleFocusWindows )
{
if ( fake_override->appID == focusable_appid )
{
if ( win_maybe_a_dropdown( fake_override ) && is_good_override_candidate( fake_override, focus ) && fake_override->appID == focus->appID )
{
override_focus = fake_override;
goto found2;
}
}
}
}
}
else
{
for ( steamcompmgr_win_t *fake_override : vecPossibleFocusWindows )
{
if ( win_maybe_a_dropdown( fake_override ) && is_good_override_candidate( fake_override, focus ) )
{
override_focus = fake_override;
goto found2;
}
}
}
found2:;
resolveTransientOverrides( true );
}
out->overrideWindow = override_focus;
return localGameFocused;
}
static void
determine_and_apply_focus(xwayland_ctx_t *ctx, std::vector<steamcompmgr_win_t*>& vecGlobalPossibleFocusWindows)
{
steamcompmgr_win_t *w;
steamcompmgr_win_t *inputFocus = NULL;
steamcompmgr_win_t *prevFocusWindow = ctx->focus.focusWindow;
ctx->focus.overlayWindow = nullptr;
ctx->focus.notificationWindow = nullptr;
ctx->focus.overrideWindow = nullptr;
ctx->focus.externalOverlayWindow = nullptr;
unsigned int maxOpacity = 0;
unsigned int maxOpacityExternal = 0;
std::vector< steamcompmgr_win_t* > vecPossibleFocusWindows;
for (w = ctx->list; w; w = w->xwayland().next)
{
// Always skip system tray icons
if ( w->isSysTrayIcon )
{
continue;
}
if ( w->xwayland().a.map_state == IsViewable && w->xwayland().a.c_class == InputOutput && w->isOverlay == false && w->isExternalOverlay == false &&
( win_has_game_id( w ) || window_is_steam( w ) || w->isSteamStreamingClient ) &&
(w->opacity > TRANSLUCENT || w->isSteamStreamingClient == true ) )
{
vecPossibleFocusWindows.push_back( w );
}
if (w->isOverlay)
{
if (w->xwayland().a.width > 1200 && w->opacity >= maxOpacity)
{
ctx->focus.overlayWindow = w;
maxOpacity = w->opacity;
}
else
{
ctx->focus.notificationWindow = w;
}
}
if (w->isExternalOverlay)
{
if (w->opacity > maxOpacityExternal)
{
ctx->focus.externalOverlayWindow = w;
maxOpacityExternal = w->opacity;
}
}
if ( w->isOverlay && w->inputFocusMode )
{
inputFocus = w;
}
}
std::stable_sort( vecPossibleFocusWindows.begin(), vecPossibleFocusWindows.end(),
is_focus_priority_greater );
vecGlobalPossibleFocusWindows.insert(vecGlobalPossibleFocusWindows.end(), vecPossibleFocusWindows.begin(), vecPossibleFocusWindows.end());
pick_primary_focus_and_override( &ctx->focus, ctx->focusControlWindow, vecPossibleFocusWindows, false, vecFocuscontrolAppIDs );
if ( inputFocus == NULL )
{
inputFocus = ctx->focus.focusWindow;
}
if ( !ctx->focus.focusWindow )
{
return;
}
if ( prevFocusWindow != ctx->focus.focusWindow )
{
/* Some games (e.g. DOOM Eternal) don't react well to being put back as
* iconic, so never do that. Only take them out of iconic. */
uint32_t wmState[] = { ICCCM_NORMAL_STATE, None };
XChangeProperty(ctx->dpy, ctx->focus.focusWindow->xwayland().id, ctx->atoms.WMStateAtom, ctx->atoms.WMStateAtom, 32,
PropModeReplace, (unsigned char *)wmState,
sizeof(wmState) / sizeof(wmState[0]));
gpuvis_trace_printf( "determine_and_apply_focus focus %lu", ctx->focus.focusWindow->xwayland().id );
if ( debugFocus == true )
{
xwm_log.debugf( "determine_and_apply_focus focus %lu", ctx->focus.focusWindow->xwayland().id );
char buf[512];
sprintf( buf, "xwininfo -id 0x%lx; xprop -id 0x%lx; xwininfo -root -tree", ctx->focus.focusWindow->xwayland().id, ctx->focus.focusWindow->xwayland().id );
system( buf );
}
}
steamcompmgr_win_t *keyboardFocusWin = inputFocus;
if ( inputFocus && inputFocus->inputFocusMode == 2 )
keyboardFocusWin = ctx->focus.focusWindow;
Window keyboardFocusWindow = keyboardFocusWin ? keyboardFocusWin->xwayland().id : None;
// If the top level parent of our current keyboard window is the same as our target (top level) input focus window
// then keep focus on that and don't yank it away to the top level input focus window.
// Fixes dropdowns in Steam CEF.
if ( keyboardFocusWindow && ctx->currentKeyboardFocusWindow && find_win( ctx, ctx->currentKeyboardFocusWindow ) == keyboardFocusWin )
keyboardFocusWindow = ctx->currentKeyboardFocusWindow;
bool bResetToCorner = false;
bool bResetToCenter = false;
if ( ctx->focus.inputFocusWindow != inputFocus ||
ctx->focus.inputFocusMode != inputFocus->inputFocusMode ||
ctx->currentKeyboardFocusWindow != keyboardFocusWindow )
{
if ( debugFocus == true )
{
xwm_log.debugf( "determine_and_apply_focus inputFocus %lu", inputFocus->xwayland().id );
}
if ( !ctx->focus.overrideWindow || ctx->focus.overrideWindow != keyboardFocusWin )
XSetInputFocus(ctx->dpy, keyboardFocusWin->xwayland().id, RevertToNone, CurrentTime);
if ( ctx->focus.inputFocusWindow != inputFocus ||
ctx->focus.inputFocusMode != inputFocus->inputFocusMode )
{
// If the window doesn't want focus when hidden, move it away
// as we are going to hide it straight after.
// otherwise, if we switch from wanting it to not
// (steam -> game)
// put us back in the centre of the screen.
if (window_wants_no_focus_when_mouse_hidden(inputFocus))
bResetToCorner = true;
else if ( window_wants_no_focus_when_mouse_hidden(inputFocus) != window_wants_no_focus_when_mouse_hidden(ctx->focus.inputFocusWindow) )
bResetToCenter = true;
// cursor is likely not interactable anymore in its original context, hide
// don't care if we change kb focus window due to that happening when
// going from override -> focus and we don't want to hide then as it's probably a dropdown.
ctx->cursor->hide();
}
ctx->focus.inputFocusWindow = inputFocus;
ctx->focus.inputFocusMode = inputFocus->inputFocusMode;
ctx->currentKeyboardFocusWindow = keyboardFocusWindow;
}
w = ctx->focus.focusWindow;
ctx->cursor->constrainPosition();
if ( inputFocus == ctx->focus.focusWindow && ctx->focus.overrideWindow )
{
if ( ctx->list[0].xwayland().id != ctx->focus.overrideWindow->xwayland().id )
{
XRaiseWindow(ctx->dpy, ctx->focus.overrideWindow->xwayland().id);
}
}
else
{
if ( ctx->list[0].xwayland().id != inputFocus->xwayland().id )
{
XRaiseWindow(ctx->dpy, inputFocus->xwayland().id);
}
}
if (!ctx->focus.focusWindow->nudged)
{
XMoveWindow(ctx->dpy, ctx->focus.focusWindow->xwayland().id, 1, 1);
ctx->focus.focusWindow->nudged = true;
}
if (w->xwayland().a.x != 0 || w->xwayland().a.y != 0)
XMoveWindow(ctx->dpy, ctx->focus.focusWindow->xwayland().id, 0, 0);
if ( window_is_fullscreen( ctx->focus.focusWindow ) || ctx->force_windows_fullscreen )
{
bool bIsSteam = window_is_steam( ctx->focus.focusWindow );
int fs_width = ctx->root_width;
int fs_height = ctx->root_height;
if ( bIsSteam && g_nSteamMaxHeight && ctx->root_height > g_nSteamMaxHeight )
{
float steam_height_scale = g_nSteamMaxHeight / (float)ctx->root_height;
fs_height = g_nSteamMaxHeight;
fs_width = ctx->root_width * steam_height_scale;
}
if ( w->xwayland().a.width != fs_width || w->xwayland().a.height != fs_height || globalScaleRatio != 1.0f )
XResizeWindow(ctx->dpy, ctx->focus.focusWindow->xwayland().id, fs_width, fs_height);
}
else
{
if (ctx->focus.focusWindow->sizeHintsSpecified &&
((unsigned)ctx->focus.focusWindow->xwayland().a.width != ctx->focus.focusWindow->requestedWidth ||
(unsigned)ctx->focus.focusWindow->xwayland().a.height != ctx->focus.focusWindow->requestedHeight))
{
XResizeWindow(ctx->dpy, ctx->focus.focusWindow->xwayland().id, ctx->focus.focusWindow->requestedWidth, ctx->focus.focusWindow->requestedHeight);
}
}
if ( inputFocus )
{
// Cannot simply XWarpPointer here as we immediately go on to
// do wlserver_mousefocus and need to update m_x and m_y of the cursor.
if ( bResetToCorner )
{
inputFocus->mouseMoved = 0;
ctx->cursor->forcePosition(inputFocus->xwayland().a.width - 1, inputFocus->xwayland().a.height - 1);
}
else if ( bResetToCenter )
{
inputFocus->mouseMoved = 0;
ctx->cursor->forcePosition(inputFocus->xwayland().a.width / 2, inputFocus->xwayland().a.height / 2);
}
}
Window root_return = None, parent_return = None;
Window *children = NULL;
unsigned int nchildren = 0;
unsigned int i = 0;
XQueryTree(ctx->dpy, w->xwayland().id, &root_return, &parent_return, &children, &nchildren);
while (i < nchildren)
{
XSelectInput( ctx->dpy, children[i], FocusChangeMask );
i++;
}
XFree(children);
}
wlr_surface *win_surface(steamcompmgr_win_t *window)
{
if (!window)
return nullptr;
return window->main_surface();
}
const char *get_win_display_name(steamcompmgr_win_t *window)
{
if ( window->type == steamcompmgr_win_type_t::XWAYLAND )
return window->xwayland().ctx->xwayland_server->get_nested_display_name();
else if ( window->type == steamcompmgr_win_type_t::XDG )
return wlserver_get_wl_display_name();
else
return "";
}
static void
determine_and_apply_focus()
{
gamescope_xwayland_server_t *root_server = wlserver_get_xwayland_server(0);
xwayland_ctx_t *root_ctx = root_server->ctx.get();
global_focus_t previous_focus = global_focus;
global_focus = global_focus_t{};
global_focus.focusWindow = previous_focus.focusWindow;
global_focus.cursor = root_ctx->cursor.get();
gameFocused = false;
std::vector< unsigned long > focusable_appids;
std::vector< unsigned long > focusable_windows;
// Determine local context focuses
std::vector< steamcompmgr_win_t* > vecPossibleFocusWindows;
{
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
{
determine_and_apply_focus(server->ctx.get(), vecPossibleFocusWindows);
}
}
for ( const auto& xdg_win : g_steamcompmgr_xdg_wins )
{
if ( xdg_win->xdg().surface.mapped )
vecPossibleFocusWindows.push_back( xdg_win.get() );
}
for ( steamcompmgr_win_t *focusable_window : vecPossibleFocusWindows )
{
if ( focusable_window->type != steamcompmgr_win_type_t::XWAYLAND )
continue;
// Exclude windows that are useless (1x1), skip taskbar + pager or override redirect windows
// from the reported focusable windows to Steam.
if ( win_is_useless( focusable_window ) ||
win_skip_and_not_fullscreen( focusable_window ) ||
focusable_window->xwayland().a.override_redirect )
continue;
unsigned int unAppID = focusable_window->appID;
if ( unAppID != 0 )
{
unsigned long j;
for( j = 0; j < focusable_appids.size(); j++ )
{
if ( focusable_appids[ j ] == unAppID )
{
break;
}
}
if ( j == focusable_appids.size() )
{
focusable_appids.push_back( unAppID );
}
}
// list of [window, appid, pid] triplets
focusable_windows.push_back( focusable_window->xwayland().id );
focusable_windows.push_back( focusable_window->appID );
focusable_windows.push_back( focusable_window->pid );
}
XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeFocusableAppsAtom, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)focusable_appids.data(), focusable_appids.size() );
XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeFocusableWindowsAtom, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)focusable_windows.data(), focusable_windows.size() );
// Determine global primary focus
std::stable_sort( vecPossibleFocusWindows.begin(), vecPossibleFocusWindows.end(),
is_focus_priority_greater );
gameFocused = pick_primary_focus_and_override(&global_focus, root_ctx->focusControlWindow, vecPossibleFocusWindows, true, vecFocuscontrolAppIDs);
// Pick overlay/notifications from root ctx
global_focus.overlayWindow = root_ctx->focus.overlayWindow;
global_focus.externalOverlayWindow = root_ctx->focus.externalOverlayWindow;
global_focus.notificationWindow = root_ctx->focus.notificationWindow;
// Pick inputFocusWindow
if (global_focus.overlayWindow && global_focus.overlayWindow->inputFocusMode)
{
global_focus.inputFocusWindow = global_focus.overlayWindow;
global_focus.keyboardFocusWindow = global_focus.overlayWindow;
}
else
{
global_focus.inputFocusWindow = global_focus.focusWindow;
global_focus.keyboardFocusWindow = global_focus.overrideWindow ? global_focus.overrideWindow : global_focus.focusWindow;
}
// Pick cursor from our input focus window
// Initially pick cursor from the ctx of our input focus.
if (global_focus.inputFocusWindow)
{
if (global_focus.inputFocusWindow->type == steamcompmgr_win_type_t::XWAYLAND)
global_focus.cursor = global_focus.inputFocusWindow->xwayland().ctx->cursor.get();
else
{
// TODO XDG:
// Implement cursor support.
// Probably want some form of abstraction here for
// wl cursor vs x11 cursor given we have virtual cursors.
// wlserver should update wl cursor pos xy directly.
static bool s_once = false;
if (!s_once)
{
xwm_log.errorf("NO CURSOR IMPL XDG");
s_once = true;
}
}
}
if (global_focus.inputFocusWindow)
global_focus.inputFocusMode = global_focus.inputFocusWindow->inputFocusMode;
if ( global_focus.inputFocusMode == 2 )
{
global_focus.keyboardFocusWindow = global_focus.overrideWindow
? global_focus.overrideWindow
: global_focus.focusWindow;
}
// Tell wlserver about our keyboard/mouse focus.
if ( global_focus.inputFocusWindow != previous_focus.inputFocusWindow ||
global_focus.keyboardFocusWindow != previous_focus.keyboardFocusWindow )
{
if ( win_surface(global_focus.inputFocusWindow) != nullptr ||
win_surface(global_focus.keyboardFocusWindow) != nullptr )
{
wlserver_lock();
if ( win_surface(global_focus.inputFocusWindow) != nullptr && global_focus.cursor )
wlserver_mousefocus( global_focus.inputFocusWindow->main_surface(), global_focus.cursor->x(), global_focus.cursor->y() );
if ( win_surface(global_focus.keyboardFocusWindow) != nullptr )
wlserver_keyboardfocus( global_focus.keyboardFocusWindow->main_surface() );
wlserver_unlock();
}
// Hide cursor on transitioning between xwaylands
// We already do this when transitioning input focus inside of an
// xwayland ctx.
// don't care if we change kb focus window due to that happening when
// going from override -> focus and we don't want to hide then as it's probably a dropdown.
if ( global_focus.cursor && global_focus.inputFocusWindow != previous_focus.inputFocusWindow )
global_focus.cursor->hide();
}
// Determine if we need to repaints
if (previous_focus.overlayWindow != global_focus.overlayWindow ||
previous_focus.externalOverlayWindow != global_focus.externalOverlayWindow ||
previous_focus.notificationWindow != global_focus.notificationWindow ||
previous_focus.overrideWindow != global_focus.overrideWindow)
{
hasRepaintNonBasePlane = true;
}
if (previous_focus.focusWindow != global_focus.focusWindow)
{
hasRepaint = true;
}
// Backchannel to Steam
unsigned long focusedWindow = 0;
unsigned long focusedAppId = 0;
unsigned long focusedBaseAppId = 0;
const char *focused_display = root_ctx->xwayland_server->get_nested_display_name();
const char *focused_keyboard_display = root_ctx->xwayland_server->get_nested_display_name();
const char *focused_mouse_display = root_ctx->xwayland_server->get_nested_display_name();
if ( global_focus.focusWindow )
{
focusedWindow = (unsigned long)global_focus.focusWindow->id();
focusedBaseAppId = global_focus.focusWindow->appID;
focusedAppId = global_focus.inputFocusWindow->appID;
focused_display = get_win_display_name(global_focus.focusWindow);
focusWindow_pid = global_focus.focusWindow->pid;
}
if ( global_focus.inputFocusWindow )
{
focused_mouse_display = get_win_display_name(global_focus.inputFocusWindow);
}
if ( global_focus.keyboardFocusWindow )
{
focused_keyboard_display = get_win_display_name(global_focus.keyboardFocusWindow);
}
if ( steamMode )
{
XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeFocusedAppAtom, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&focusedAppId, focusedAppId != 0 ? 1 : 0 );
XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeFocusedAppGfxAtom, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&focusedBaseAppId, focusedBaseAppId != 0 ? 1 : 0 );
}
XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeFocusedWindowAtom, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&focusedWindow, focusedWindow != 0 ? 1 : 0 );
XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeFocusDisplay, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)focused_display, strlen(focused_display) + 1 );
XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeMouseFocusDisplay, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)focused_mouse_display, strlen(focused_mouse_display) + 1 );
XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeKeyboardFocusDisplay, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)focused_keyboard_display, strlen(focused_keyboard_display) + 1 );
XFlush( root_ctx->dpy );
// Sort out fading.
if (previous_focus.focusWindow != global_focus.focusWindow)
{
if ( g_FadeOutDuration != 0 && !g_bFirstFrame )
{
if ( !g_HeldCommits[ HELD_COMMIT_FADE ] )
{
global_focus.fadeWindow = previous_focus.focusWindow;
g_HeldCommits[ HELD_COMMIT_FADE ] = g_HeldCommits[ HELD_COMMIT_BASE ];
g_bPendingFade = true;
}
else
{
// If we end up fading back to what we were going to fade to, cancel the fade.
if ( global_focus.fadeWindow != nullptr && global_focus.focusWindow == global_focus.fadeWindow )
{
g_HeldCommits[ HELD_COMMIT_FADE ] = nullptr;
g_bPendingFade = false;
fadeOutStartTime = 0;
global_focus.fadeWindow = nullptr;
}
}
}
}
// Update last focus commit
if ( global_focus.focusWindow &&
previous_focus.focusWindow != global_focus.focusWindow &&
!global_focus.focusWindow->isSteamStreamingClient )
{
get_window_last_done_commit( global_focus.focusWindow, g_HeldCommits[ HELD_COMMIT_BASE ] );
}
// Set SDL window title
if ( global_focus.focusWindow )
{
#if HAVE_OPENVR
if ( BIsVRSession() )
{
const char *title = global_focus.focusWindow->title
? global_focus.focusWindow->title->c_str()
: nullptr;
vrsession_title( title, global_focus.focusWindow->icon );
}
#endif
if ( BIsSDLSession() )
{
sdlwindow_title( global_focus.focusWindow->title, global_focus.focusWindow->icon );
}
}
#if HAVE_OPENVR
if ( BIsVRSession() )
{
vrsession_set_dashboard_visible( global_focus.focusWindow != nullptr );
}
else
#endif
{
if ( BIsSDLSession() )
{
sdlwindow_visible( global_focus.focusWindow != nullptr );
}
}
// Some games such as Disgaea PC (405900) don't take controller input until
// the window is first clicked on despite it having focus.
if ( global_focus.inputFocusWindow && global_focus.inputFocusWindow->appID == 405900 )
{
global_focus.inputFocusWindow->mouseMoved = 0;
global_focus.inputFocusWindow->ignoreNextClickForVisibility = 2;
auto now = get_time_in_milliseconds();
wlserver_lock();
wlserver_touchdown( 0.5, 0.5, 0, now );
wlserver_touchup( 0, now + 1 );
wlserver_unlock();
}
focusDirty = false;
}
static void
get_win_type(xwayland_ctx_t *ctx, steamcompmgr_win_t *w)
{
w->is_dialog = !!w->xwayland().transientFor;
std::vector<unsigned int> atoms;
if ( get_prop( ctx, w->xwayland().id, ctx->atoms.winTypeAtom, atoms ) )
{
for ( unsigned int atom : atoms )
{
if ( atom == ctx->atoms.winDialogAtom )
{
w->is_dialog = true;
}
if ( atom == ctx->atoms.winNormalAtom )
{
w->is_dialog = false;
}
}
}
}
static void
get_size_hints(xwayland_ctx_t *ctx, steamcompmgr_win_t *w)
{
XSizeHints hints;
long hintsSpecified = 0;
XGetWMNormalHints(ctx->dpy, w->xwayland().id, &hints, &hintsSpecified);
const bool bHasPositionAndGravityHints = ( hintsSpecified & ( PPosition | PWinGravity ) ) == ( PPosition | PWinGravity );
if ( bHasPositionAndGravityHints &&
hints.x && hints.y && hints.win_gravity == StaticGravity )
{
w->maybe_a_dropdown = true;
}
else
{
w->maybe_a_dropdown = false;
}
if (hintsSpecified & (PMaxSize | PMinSize) &&
hints.max_width && hints.max_height && hints.min_width && hints.min_height &&
hints.max_width == hints.min_width && hints.min_height == hints.max_height)
{
w->requestedWidth = hints.max_width;
w->requestedHeight = hints.max_height;
w->sizeHintsSpecified = true;
}
else
{
w->sizeHintsSpecified = false;
// Below block checks for a pattern that matches old SDL fullscreen applications;
// SDL creates a fullscreen overrride-redirect window and reparents the game
// window under it, centered. We get rid of the modeswitch and also want that
// black border gone.
if (w->xwayland().a.override_redirect)
{
Window root_return = None, parent_return = None;
Window *children = NULL;
unsigned int nchildren = 0;
XQueryTree(ctx->dpy, w->xwayland().id, &root_return, &parent_return, &children, &nchildren);
if (nchildren == 1)
{
XWindowAttributes attribs;
XGetWindowAttributes(ctx->dpy, children[0], &attribs);
// If we have a unique children that isn't override-reidrect that is
// contained inside this fullscreen window, it's probably it.
if (attribs.override_redirect == false &&
attribs.width <= w->xwayland().a.width &&
attribs.height <= w->xwayland().a.height)
{
w->sizeHintsSpecified = true;
w->requestedWidth = attribs.width;
w->requestedHeight = attribs.height;
XMoveWindow(ctx->dpy, children[0], 0, 0);
w->ignoreOverrideRedirect = true;
}
}
XFree(children);
}
}
}
static void
get_motif_hints( xwayland_ctx_t *ctx, steamcompmgr_win_t *w )
{
if ( w->motif_hints )
XFree( w->motif_hints );
Atom actual_type;
int actual_format;
unsigned long nitems, bytesafter;
XGetWindowProperty(ctx->dpy, w->xwayland().id, ctx->atoms.motifWMHints, 0L, 20L, False,
ctx->atoms.motifWMHints, &actual_type, &actual_format, &nitems,
&bytesafter, (unsigned char **)&w->motif_hints );
}
static void
get_win_title(xwayland_ctx_t *ctx, steamcompmgr_win_t *w, Atom atom)
{
assert(atom == XA_WM_NAME || atom == ctx->atoms.netWMNameAtom);
// Allocates a title we are meant to free,
// let's re-use this allocation for w->title :)
XTextProperty tp;
XGetTextProperty( ctx->dpy, w->xwayland().id, &tp, atom );
bool is_utf8;
if (tp.encoding == ctx->atoms.utf8StringAtom) {
is_utf8 = true;
} else if (tp.encoding == XA_STRING) {
is_utf8 = false;
} else {
return;
}
if (!is_utf8 && w->utf8_title) {
/* Clients usually set both the non-UTF8 title and the UTF8 title
* properties. If the client has set the UTF8 title prop, ignore the
* non-UTF8 one. */
return;
}
if (tp.nitems > 0) {
// Ride off the allocation from XGetTextProperty.
w->title = std::make_shared<std::string>((const char *)tp.value);
} else {
w->title = NULL;
}
w->utf8_title = is_utf8;
}
static void
get_net_wm_state(xwayland_ctx_t *ctx, steamcompmgr_win_t *w)
{
Atom type;
int format;
unsigned long nitems;
unsigned long bytesAfter;
unsigned char *data;
if (XGetWindowProperty(ctx->dpy, w->xwayland().id, ctx->atoms.netWMStateAtom, 0, 2048, false,
AnyPropertyType, &type, &format, &nitems, &bytesAfter, &data) != Success) {
return;
}
Atom *props = (Atom *)data;
for (size_t i = 0; i < nitems; i++) {
if (props[i] == ctx->atoms.netWMStateFullscreenAtom) {
w->isFullscreen = true;
} else if (props[i] == ctx->atoms.netWMStateSkipTaskbarAtom) {
w->skipTaskbar = true;
} else if (props[i] == ctx->atoms.netWMStateSkipPagerAtom) {
w->skipPager = true;
} else {
xwm_log.debugf("Unhandled initial NET_WM_STATE property: %s", XGetAtomName(ctx->dpy, props[i]));
}
}
XFree(data);
}
static void
get_win_icon(xwayland_ctx_t* ctx, steamcompmgr_win_t* w)
{
w->icon = std::make_shared<std::vector<uint32_t>>();
get_prop(ctx, w->xwayland().id, ctx->atoms.netWMIcon, *w->icon.get());
}
static void
map_win(xwayland_ctx_t* ctx, Window id, unsigned long sequence)
{
steamcompmgr_win_t *w = find_win(ctx, id);
if (!w)
return;
w->xwayland().a.map_state = IsViewable;
/* This needs to be here or else we lose transparency messages */
XSelectInput(ctx->dpy, id, PropertyChangeMask | SubstructureNotifyMask |
LeaveWindowMask | FocusChangeMask);
XFlush(ctx->dpy);
/* This needs to be here since we don't get PropertyNotify when unmapped */
w->opacity = get_prop(ctx, w->xwayland().id, ctx->atoms.opacityAtom, OPAQUE);
w->isSteamLegacyBigPicture = get_prop(ctx, w->xwayland().id, ctx->atoms.steamAtom, 0);
/* First try to read the UTF8 title prop, then fallback to the non-UTF8 one */
get_win_title( ctx, w, ctx->atoms.netWMNameAtom );
get_win_title( ctx, w, XA_WM_NAME );
get_win_icon( ctx, w );
w->inputFocusMode = get_prop(ctx, w->xwayland().id, ctx->atoms.steamInputFocusAtom, 0);
w->isSteamStreamingClient = get_prop(ctx, w->xwayland().id, ctx->atoms.steamStreamingClientAtom, 0);
w->isSteamStreamingClientVideo = get_prop(ctx, w->xwayland().id, ctx->atoms.steamStreamingClientVideoAtom, 0);
if ( steamMode == true )
{
uint32_t appID = get_prop(ctx, w->xwayland().id, ctx->atoms.gameAtom, 0);
if ( w->appID != 0 && appID != 0 && w->appID != appID )
{
xwm_log.errorf( "appid clash was %u now %u", w->appID, appID );
}
// Let the appID property be authoritative for now
if ( appID != 0 )
{
w->appID = appID;
}
}
else
{
w->appID = w->xwayland().id;
}
w->isOverlay = get_prop(ctx, w->xwayland().id, ctx->atoms.overlayAtom, 0);
w->isExternalOverlay = get_prop(ctx, w->xwayland().id, ctx->atoms.externalOverlayAtom, 0);
get_size_hints(ctx, w);
get_motif_hints(ctx, w);
get_net_wm_state(ctx, w);
XWMHints *wmHints = XGetWMHints( ctx->dpy, w->xwayland().id );
if ( wmHints != nullptr )
{
if ( wmHints->flags & (InputHint | StateHint ) && wmHints->input == true && wmHints->initial_state == NormalState )
{
XRaiseWindow( ctx->dpy, w->xwayland().id );
}
XFree( wmHints );
}
Window transientFor = None;
if ( XGetTransientForHint( ctx->dpy, w->xwayland().id, &transientFor ) )
{
w->xwayland().transientFor = transientFor;
}
else
{
w->xwayland().transientFor = None;
}
get_win_type( ctx, w );
w->xwayland().damage_sequence = 0;
w->xwayland().map_sequence = sequence;
if ( w == ctx->focus.inputFocusWindow || w->xwayland().id == ctx->currentKeyboardFocusWindow )
{
XSetInputFocus(ctx->dpy, w->xwayland().id, RevertToNone, CurrentTime);
}
focusDirty = true;
}
static void
finish_unmap_win(xwayland_ctx_t *ctx, steamcompmgr_win_t *w)
{
// TODO clear done commits here?
/* don't care about properties anymore */
set_ignore(ctx, NextRequest(ctx->dpy));
XSelectInput(ctx->dpy, w->xwayland().id, 0);
ctx->clipChanged = true;
}
static void
unmap_win(xwayland_ctx_t *ctx, Window id, bool fade)
{
steamcompmgr_win_t *w = find_win(ctx, id);
if (!w)
return;
w->xwayland().a.map_state = IsUnmapped;
focusDirty = true;
finish_unmap_win(ctx, w);
}
static uint32_t
get_appid_from_pid( pid_t pid )
{
uint32_t unFoundAppId = 0;
char filename[256];
pid_t next_pid = pid;
while ( 1 )
{
snprintf( filename, sizeof( filename ), "/proc/%i/stat", next_pid );
std::ifstream proc_stat_file( filename );
if (!proc_stat_file.is_open() || proc_stat_file.bad())
break;
std::string proc_stat;
std::getline( proc_stat_file, proc_stat );
char *procName = nullptr;
char *lastParens = nullptr;
for ( uint32_t i = 0; i < proc_stat.length(); i++ )
{
if ( procName == nullptr && proc_stat[ i ] == '(' )
{
procName = &proc_stat[ i + 1 ];
}
if ( proc_stat[ i ] == ')' )
{
lastParens = &proc_stat[ i ];
}
}
if (!lastParens)
break;
*lastParens = '\0';
char state;
int parent_pid = -1;
sscanf( lastParens + 1, " %c %d", &state, &parent_pid );
if ( strcmp( "reaper", procName ) == 0 )
{
snprintf( filename, sizeof( filename ), "/proc/%i/cmdline", next_pid );
std::ifstream proc_cmdline_file( filename );
std::string proc_cmdline;
bool bSteamLaunch = false;
uint32_t unAppId = 0;
std::getline( proc_cmdline_file, proc_cmdline );
for ( uint32_t j = 0; j < proc_cmdline.length(); j++ )
{
if ( proc_cmdline[ j ] == '\0' && j + 1 < proc_cmdline.length() )
{
if ( strcmp( "SteamLaunch", &proc_cmdline[ j + 1 ] ) == 0 )
{
bSteamLaunch = true;
}
else if ( sscanf( &proc_cmdline[ j + 1 ], "AppId=%u", &unAppId ) == 1 && unAppId != 0 )
{
if ( bSteamLaunch == true )
{
unFoundAppId = unAppId;
}
}
else if ( strcmp( "--", &proc_cmdline[ j + 1 ] ) == 0 )
{
break;
}
}
}
}
if ( parent_pid == -1 || parent_pid == 0 )
{
break;
}
else
{
next_pid = parent_pid;
}
}
return unFoundAppId;
}
static pid_t
get_win_pid(xwayland_ctx_t *ctx, Window id)
{
XResClientIdSpec client_spec = {
.client = id,
.mask = XRES_CLIENT_ID_PID_MASK,
};
long num_ids = 0;
XResClientIdValue *client_ids = NULL;
XResQueryClientIds(ctx->dpy, 1, &client_spec, &num_ids, &client_ids);
pid_t pid = -1;
for (long i = 0; i < num_ids; i++) {
pid = XResGetClientPid(&client_ids[i]);
if (pid > 0)
break;
}
XResClientIdsDestroy(num_ids, client_ids);
if (pid <= 0)
xwm_log.errorf("Failed to find PID for window 0x%lx", id);
return pid;
}
static void
add_win(xwayland_ctx_t *ctx, Window id, Window prev, unsigned long sequence)
{
steamcompmgr_win_t *new_win = new steamcompmgr_win_t{};
steamcompmgr_win_t **p;
if (!new_win)
return;
new_win->type = steamcompmgr_win_type_t::XWAYLAND;
new_win->_window_types.emplace<steamcompmgr_xwayland_win_t>();
if (prev)
{
for (p = &ctx->list; *p; p = &(*p)->xwayland().next)
if ((*p)->xwayland().id == prev)
break;
}
else
p = &ctx->list;
new_win->xwayland().id = id;
set_ignore(ctx, NextRequest(ctx->dpy));
if (!XGetWindowAttributes(ctx->dpy, id, &new_win->xwayland().a))
{
delete new_win;
return;
}
new_win->xwayland().ctx = ctx;
new_win->xwayland().damage_sequence = 0;
new_win->xwayland().map_sequence = 0;
if (new_win->xwayland().a.c_class == InputOnly)
new_win->xwayland().damage = None;
else
{
set_ignore(ctx, NextRequest(ctx->dpy));
new_win->xwayland().damage = XDamageCreate(ctx->dpy, id, XDamageReportRawRectangles);
}
new_win->opacity = OPAQUE;
if ( useXRes == true )
{
new_win->pid = get_win_pid(ctx, id);
}
else
{
new_win->pid = -1;
}
new_win->isOverlay = false;
new_win->isExternalOverlay = false;
new_win->isSteamLegacyBigPicture = false;
new_win->isSteamStreamingClient = false;
new_win->isSteamStreamingClientVideo = false;
new_win->inputFocusMode = 0;
new_win->is_dialog = false;
new_win->maybe_a_dropdown = false;
new_win->motif_hints = nullptr;
new_win->hasHwndStyle = false;
new_win->hwndStyle = 0;
new_win->hasHwndStyleEx = false;
new_win->hwndStyleEx = 0;
if ( steamMode == true )
{
if ( new_win->pid != -1 )
{
new_win->appID = get_appid_from_pid( new_win->pid );
}
else
{
new_win->appID = 0;
}
}
else
{
new_win->appID = id;
}
Window transientFor = None;
if ( XGetTransientForHint( ctx->dpy, id, &transientFor ) )
{
new_win->xwayland().transientFor = transientFor;
}
else
{
new_win->xwayland().transientFor = None;
}
get_win_type( ctx, new_win );
new_win->title = NULL;
new_win->utf8_title = false;
new_win->isFullscreen = false;
new_win->isSysTrayIcon = false;
new_win->sizeHintsSpecified = false;
new_win->skipTaskbar = false;
new_win->skipPager = false;
new_win->requestedWidth = 0;
new_win->requestedHeight = 0;
new_win->nudged = false;
new_win->ignoreOverrideRedirect = false;
new_win->mouseMoved = 0;
wlserver_x11_surface_info_init( &new_win->xwayland().surface, ctx->xwayland_server, id );
new_win->xwayland().next = *p;
*p = new_win;
if (new_win->xwayland().a.map_state == IsViewable)
map_win(ctx, id, sequence);
focusDirty = true;
}
static void
restack_win(xwayland_ctx_t *ctx, steamcompmgr_win_t *w, Window new_above)
{
Window old_above;
if (w->xwayland().next)
old_above = w->xwayland().next->xwayland().id;
else
old_above = None;
if (old_above != new_above)
{
steamcompmgr_win_t **prev;
/* unhook */
for (prev = &ctx->list; *prev; prev = &(*prev)->xwayland().next)
{
if ((*prev) == w)
break;
}
*prev = w->xwayland().next;
/* rehook */
for (prev = &ctx->list; *prev; prev = &(*prev)->xwayland().next)
{
if ((*prev)->xwayland().id == new_above)
break;
}
w->xwayland().next = *prev;
*prev = w;
focusDirty = true;
}
}
static void
configure_win(xwayland_ctx_t *ctx, XConfigureEvent *ce)
{
steamcompmgr_win_t *w = find_win(ctx, ce->window);
if (!w || w->xwayland().id != ce->window)
{
if (ce->window == ctx->root)
{
ctx->root_width = ce->width;
ctx->root_height = ce->height;
focusDirty = true;
gamescope_xwayland_server_t *root_server = wlserver_get_xwayland_server(0);
xwayland_ctx_t *root_ctx = root_server->ctx.get();
XDeleteProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeXWaylandModeControl );
XFlush( root_ctx->dpy );
}
return;
}
w->xwayland().a.x = ce->x;
w->xwayland().a.y = ce->y;
w->xwayland().a.width = ce->width;
w->xwayland().a.height = ce->height;
w->xwayland().a.border_width = ce->border_width;
w->xwayland().a.override_redirect = ce->override_redirect;
restack_win(ctx, w, ce->above);
focusDirty = true;
}
static void
circulate_win(xwayland_ctx_t *ctx, XCirculateEvent *ce)
{
steamcompmgr_win_t *w = find_win(ctx, ce->window);
Window new_above;
if (!w || w->xwayland().id != ce->window)
return;
if (ce->place == PlaceOnTop)
new_above = ctx->list->xwayland().id;
else
new_above = None;
restack_win(ctx, w, new_above);
ctx->clipChanged = true;
}
static void map_request(xwayland_ctx_t *ctx, XMapRequestEvent *mapRequest)
{
XMapWindow( ctx->dpy, mapRequest->window );
}
static void configure_request(xwayland_ctx_t *ctx, XConfigureRequestEvent *configureRequest)
{
XWindowChanges changes =
{
.x = configureRequest->x,
.y = configureRequest->y,
.width = configureRequest->width,
.height = configureRequest->height,
.border_width = configureRequest->border_width,
.sibling = configureRequest->above,
.stack_mode = configureRequest->detail
};
XConfigureWindow( ctx->dpy, configureRequest->window, configureRequest->value_mask, &changes );
}
static void circulate_request( xwayland_ctx_t *ctx, XCirculateRequestEvent *circulateRequest )
{
XCirculateSubwindows( ctx->dpy, circulateRequest->window, circulateRequest->place );
}
static void
finish_destroy_win(xwayland_ctx_t *ctx, Window id, bool gone)
{
steamcompmgr_win_t **prev, *w;
for (prev = &ctx->list; (w = *prev); prev = &w->xwayland().next)
if (w->xwayland().id == id)
{
if (gone)
finish_unmap_win (ctx, w);
*prev = w->xwayland().next;
if (w->xwayland().damage != None)
{
set_ignore(ctx, NextRequest(ctx->dpy));
XDamageDestroy(ctx->dpy, w->xwayland().damage);
w->xwayland().damage = None;
}
if (gone)
{
// release all commits now we are closed.
w->commit_queue.clear();
}
wlserver_lock();
wlserver_x11_surface_info_finish( &w->xwayland().surface );
wlserver_unlock();
delete w;
break;
}
}
static void
destroy_win(xwayland_ctx_t *ctx, Window id, bool gone, bool fade)
{
// Context
if (x11_win(ctx->focus.focusWindow) == id && gone)
ctx->focus.focusWindow = nullptr;
if (x11_win(ctx->focus.inputFocusWindow) == id && gone)
ctx->focus.inputFocusWindow = nullptr;
if (x11_win(ctx->focus.overlayWindow) == id && gone)
ctx->focus.overlayWindow = nullptr;
if (x11_win(ctx->focus.externalOverlayWindow) == id && gone)
ctx->focus.externalOverlayWindow = nullptr;
if (x11_win(ctx->focus.notificationWindow) == id && gone)
ctx->focus.notificationWindow = nullptr;
if (x11_win(ctx->focus.overrideWindow) == id && gone)
ctx->focus.overrideWindow = nullptr;
if (ctx->currentKeyboardFocusWindow == id && gone)
ctx->currentKeyboardFocusWindow = None;
// Global Focus
if (x11_win(global_focus.focusWindow) == id && gone)
global_focus.focusWindow = nullptr;
if (x11_win(global_focus.inputFocusWindow) == id && gone)
global_focus.inputFocusWindow = nullptr;
if (x11_win(global_focus.overlayWindow) == id && gone)
global_focus.overlayWindow = nullptr;
if (x11_win(global_focus.notificationWindow) == id && gone)
global_focus.notificationWindow = nullptr;
if (x11_win(global_focus.overrideWindow) == id && gone)
global_focus.overrideWindow = nullptr;
if (x11_win(global_focus.fadeWindow) == id && gone)
global_focus.fadeWindow = nullptr;
focusDirty = true;
finish_destroy_win(ctx, id, gone);
}
static void
damage_win(xwayland_ctx_t *ctx, XDamageNotifyEvent *de)
{
steamcompmgr_win_t *w = find_win(ctx, de->drawable);
steamcompmgr_win_t *focus = ctx->focus.focusWindow;
if (!w)
return;
if ((w->isOverlay || w->isExternalOverlay) && !w->opacity)
return;
// First damage event we get, compute focus; we only want to focus damaged
// windows to have meaningful frames.
if (w->appID && w->xwayland().damage_sequence == 0)
focusDirty = true;
w->xwayland().damage_sequence = damageSequence++;
// If we just passed the focused window, we might be eliglible to take over
if ( focus && focus != w && w->appID &&
w->xwayland().damage_sequence > focus->xwayland().damage_sequence)
focusDirty = true;
// Josh: This will sometimes cause a BadDamage error.
// I looked around at different compositors to see what
// they do here and they just seem to ignore it.
if (w->xwayland().damage)
{
set_ignore(ctx, NextRequest(ctx->dpy));
XDamageSubtract(ctx->dpy, w->xwayland().damage, None, None);
}
gpuvis_trace_printf( "damage_win win %lx appID %u", w->xwayland().id, w->appID );
}
static void
handle_wl_surface_id(xwayland_ctx_t *ctx, steamcompmgr_win_t *w, uint32_t surfaceID)
{
struct wlr_surface *current_surface = NULL;
struct wlr_surface *main_surface = NULL;
wlserver_lock();
ctx->xwayland_server->set_wl_id( &w->xwayland().surface, surfaceID );
current_surface = w->xwayland().surface.current_surface();
main_surface = w->xwayland().surface.main_surface;
if ( current_surface == NULL )
{
wlserver_unlock();
return;
}
// If we already focused on our side and are handling this late,
// let wayland know now.
if ( w == global_focus.inputFocusWindow )
wlserver_mousefocus( main_surface );
steamcompmgr_win_t *keyboardFocusWindow = global_focus.inputFocusWindow;
if ( keyboardFocusWindow && keyboardFocusWindow->inputFocusMode == 2 )
keyboardFocusWindow = global_focus.focusWindow;
if ( w == keyboardFocusWindow )
wlserver_keyboardfocus( main_surface );
// Pull the first buffer out of that window, if needed
xwayland_surface_commit( current_surface );
wlserver_unlock();
}
static void
update_net_wm_state(uint32_t action, bool *value)
{
switch (action) {
case NET_WM_STATE_REMOVE:
*value = false;
break;
case NET_WM_STATE_ADD:
*value = true;
break;
case NET_WM_STATE_TOGGLE:
*value = !*value;
break;
default:
xwm_log.debugf("Unknown NET_WM_STATE action: %" PRIu32, action);
}
}
static void
handle_net_wm_state(xwayland_ctx_t *ctx, steamcompmgr_win_t *w, XClientMessageEvent *ev)
{
uint32_t action = (uint32_t)ev->data.l[0];
Atom *props = (Atom *)&ev->data.l[1];
for (size_t i = 0; i < 2; i++) {
if (props[i] == ctx->atoms.netWMStateFullscreenAtom) {
update_net_wm_state(action, &w->isFullscreen);
focusDirty = true;
} else if (props[i] == ctx->atoms.netWMStateSkipTaskbarAtom) {
update_net_wm_state(action, &w->skipTaskbar);
focusDirty = true;
} else if (props[i] == ctx->atoms.netWMStateSkipPagerAtom) {
update_net_wm_state(action, &w->skipPager);
focusDirty = true;
} else if (props[i] != None) {
xwm_log.debugf("Unhandled NET_WM_STATE property change: %s", XGetAtomName(ctx->dpy, props[i]));
}
}
}
bool g_bLowLatency = false;
static void
handle_system_tray_opcode(xwayland_ctx_t *ctx, XClientMessageEvent *ev)
{
long opcode = ev->data.l[1];
switch (opcode) {
case SYSTEM_TRAY_REQUEST_DOCK: {
Window embed_id = ev->data.l[2];
/* At this point we're supposed to initiate the XEmbed lifecycle by
* sending XEMBED_EMBEDDED_NOTIFY. However we don't actually need to
* render the systray, we just want to recognize and blacklist these
* icons. So for now do nothing. */
steamcompmgr_win_t *w = find_win(ctx, embed_id);
if (w) {
w->isSysTrayIcon = true;
}
break;
}
default:
xwm_log.debugf("Unhandled _NET_SYSTEM_TRAY_OPCODE %ld", opcode);
}
}
/* See http://tronche.com/gui/x/icccm/sec-4.html#s-4.1.4 */
static void
handle_wm_change_state(xwayland_ctx_t *ctx, steamcompmgr_win_t *w, XClientMessageEvent *ev)
{
long state = ev->data.l[0];
if (state == ICCCM_ICONIC_STATE) {
/* Wine will request iconic state and cannot ensure that the WM has
* agreed on it; immediately revert to normal state to avoid being
* stuck in a paused state. */
xwm_log.debugf("Rejecting WM_CHANGE_STATE to ICONIC for window 0x%lx", w->xwayland().id);
uint32_t wmState[] = { ICCCM_NORMAL_STATE, None };
XChangeProperty(ctx->dpy, w->xwayland().id, ctx->atoms.WMStateAtom, ctx->atoms.WMStateAtom, 32,
PropModeReplace, (unsigned char *)wmState,
sizeof(wmState) / sizeof(wmState[0]));
} else {
xwm_log.debugf("Unhandled WM_CHANGE_STATE to %ld for window 0x%lx", state, w->xwayland().id);
}
}
static void
handle_client_message(xwayland_ctx_t *ctx, XClientMessageEvent *ev)
{
if (ev->window == ctx->ourWindow && ev->message_type == ctx->atoms.netSystemTrayOpcodeAtom)
{
handle_system_tray_opcode( ctx, ev );
return;
}
steamcompmgr_win_t *w = find_win(ctx, ev->window);
if (w)
{
if (ev->message_type == ctx->atoms.WLSurfaceIDAtom)
{
handle_wl_surface_id( ctx, w, uint32_t(ev->data.l[0]));
}
else if ( ev->message_type == ctx->atoms.activeWindowAtom )
{
XRaiseWindow( ctx->dpy, w->xwayland().id );
}
else if ( ev->message_type == ctx->atoms.netWMStateAtom )
{
handle_net_wm_state( ctx, w, ev );
}
else if ( ev->message_type == ctx->atoms.WMChangeStateAtom )
{
handle_wm_change_state( ctx, w, ev );
}
else if ( ev->message_type != 0 )
{
xwm_log.debugf( "Unhandled client message: %s", XGetAtomName( ctx->dpy, ev->message_type ) );
}
}
}
static void x11_set_selection_owner(xwayland_ctx_t *ctx, std::string contents, int selectionTarget)
{
Atom target;
if (selectionTarget == CLIPBOARD)
{
target = ctx->atoms.clipboard;
}
else if (selectionTarget == PRIMARYSELECTION)
{
target = ctx->atoms.primarySelection;
}
else
{
return;
}
XSetSelectionOwner(ctx->dpy, target, ctx->ourWindow, CurrentTime);
}
void gamescope_set_selection(std::string contents, int selection)
{
if (selection == CLIPBOARD)
{
clipboard = contents;
}
else if (selection == PRIMARYSELECTION)
{
primarySelection = contents;
}
gamescope_xwayland_server_t *server = NULL;
for (int i = 0; (server = wlserver_get_xwayland_server(i)); i++)
{
x11_set_selection_owner(server->ctx.get(), contents, selection);
}
}
static void
handle_selection_request(xwayland_ctx_t *ctx, XSelectionRequestEvent *ev)
{
std::string *selection = ev->selection == ctx->atoms.primarySelection ? &primarySelection : &clipboard;
const char *targetString = XGetAtomName(ctx->dpy, ev->target);
XEvent response;
response.xselection.type = SelectionNotify;
response.xselection.selection = ev->selection;
response.xselection.requestor = ev->requestor;
response.xselection.time = ev->time;
response.xselection.property = None;
response.xselection.target = None;
if (ev->requestor == ctx->ourWindow)
{
return;
}
if (ev->target == ctx->atoms.targets)
{
Atom targetList[] = {
ctx->atoms.targets,
XA_STRING,
};
XChangeProperty(ctx->dpy, ev->requestor, ev->property, XA_ATOM, 32, PropModeReplace,
(unsigned char *)&targetList, 2);
response.xselection.property = ev->property;
response.xselection.target = ev->target;
}
else if (!strcmp(targetString, "text/plain;charset=utf-8") ||
!strcmp(targetString, "text/plain") ||
!strcmp(targetString, "TEXT") ||
!strcmp(targetString, "UTF8_STRING") ||
!strcmp(targetString, "STRING"))
{
XChangeProperty(ctx->dpy, ev->requestor, ev->property, ev->target, 8, PropModeReplace,
(unsigned char *)selection->c_str(), selection->length());
response.xselection.property = ev->property;
response.xselection.target = ev->target;
}
else
{
xwm_log.debugf("Unsupported clipboard type: %s. Ignoring", targetString);
}
XSendEvent(ctx->dpy, ev->requestor, False, NoEventMask, &response);
XFlush(ctx->dpy);
}
static void
handle_selection_notify(xwayland_ctx_t *ctx, XSelectionEvent *ev)
{
int selection;
Atom actual_type;
int actual_format;
unsigned long nitems;
unsigned long bytes_after;
unsigned char *data = NULL;
XGetWindowProperty(ctx->dpy, ev->requestor, ev->property, 0, 0, False, AnyPropertyType,
&actual_type, &actual_format, &nitems, &bytes_after, &data);
if (data) {
XFree(data);
}
if (actual_type == ctx->atoms.utf8StringAtom && actual_format == 8) {
XGetWindowProperty(ctx->dpy, ev->requestor, ev->property, 0, bytes_after, False, AnyPropertyType,
&actual_type, &actual_format, &nitems, &bytes_after, &data);
if (data) {
const char *contents = (const char *) data;
if (ev->selection == ctx->atoms.clipboard)
{
selection = CLIPBOARD;
}
else if (ev->selection == ctx->atoms.primarySelection)
{
selection = PRIMARYSELECTION;
}
else
{
xwm_log.errorf( "Selection '%s' not supported. Ignoring", XGetAtomName(ctx->dpy, ev->selection) );
goto done;
}
if (BIsNested())
{
/*
* gamescope_set_selection() doesn't need to be called here.
* sdlwindow_set_selection triggers a clipboard update, which
* then indirectly ccalls gamescope_set_selection()
*/
sdlwindow_set_selection(contents, selection);
}
else
{
gamescope_set_selection(contents, selection);
}
done:
XFree(data);
}
}
}
template<typename T, typename J>
T bit_cast(const J& src) {
T dst;
memcpy(&dst, &src, sizeof(T));
return dst;
}
static void
update_runtime_info()
{
if ( g_nRuntimeInfoFd < 0 )
return;
uint32_t limiter_enabled = g_nSteamCompMgrTargetFPS != 0 ? 1 : 0;
pwrite( g_nRuntimeInfoFd, &limiter_enabled, sizeof( limiter_enabled ), 0 );
}
static void
init_runtime_info()
{
const char *path = getenv( "GAMESCOPE_LIMITER_FILE" );
if ( !path )
return;
g_nRuntimeInfoFd = open( path, O_CREAT | O_RDWR , 0644 );
update_runtime_info();
}
static void
steamcompmgr_flush_frame_done( steamcompmgr_win_t *w )
{
wlr_surface *current_surface = w->current_surface();
if ( current_surface && w->unlockedForFrameCallback && w->receivedDoneCommit )
{
// TODO: Look into making this _RAW
// wlroots, seems to just use normal MONOTONIC
// all over so this may be problematic to just change.
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
wlr_surface *main_surface = w->main_surface();
w->unlockedForFrameCallback = false;
w->receivedDoneCommit = false;
// Acknowledge commit once.
wlserver_lock();
if ( main_surface != nullptr )
{
wlserver_send_frame_done(main_surface, &now);
}
if ( current_surface != nullptr && main_surface != current_surface )
{
wlserver_send_frame_done(current_surface, &now);
}
wlserver_unlock();
}
}
static void
steamcompmgr_latch_frame_done( steamcompmgr_win_t *w, uint64_t vblank_idx )
{
bool bSendCallback = true;
int nRefresh = g_nNestedRefresh ? g_nNestedRefresh : g_nOutputRefresh;
int nTargetFPS = g_nSteamCompMgrTargetFPS;
if ( g_nSteamCompMgrTargetFPS && steamcompmgr_window_should_limit_fps( w ) && nRefresh > nTargetFPS )
{
int nVblankDivisor = nRefresh / nTargetFPS;
if ( vblank_idx % nVblankDivisor != 0 )
bSendCallback = false;
}
if ( bSendCallback )
{
w->unlockedForFrameCallback = true;
}
}
static inline float santitize_float( float f )
{
return ( std::isfinite( f ) ? f : 0.f );
}
static void
handle_property_notify(xwayland_ctx_t *ctx, XPropertyEvent *ev)
{
/* check if Trans property was changed */
if (ev->atom == ctx->atoms.opacityAtom)
{
/* reset mode and redraw window */
steamcompmgr_win_t * w = find_win(ctx, ev->window);
if ( w != nullptr )
{
unsigned int newOpacity = get_prop(ctx, w->xwayland().id, ctx->atoms.opacityAtom, OPAQUE);
if (newOpacity != w->opacity)
{
w->opacity = newOpacity;
if ( gameFocused && ( w == ctx->focus.overlayWindow || w == ctx->focus.notificationWindow ) )
{
hasRepaintNonBasePlane = true;
}
if ( w == ctx->focus.externalOverlayWindow )
{
hasRepaint = true;
}
}
unsigned int maxOpacity = 0;
unsigned int maxOpacityExternal = 0;
for (w = ctx->list; w; w = w->xwayland().next)
{
if (w->isOverlay)
{
if (w->xwayland().a.width > 1200 && w->opacity >= maxOpacity)
{
ctx->focus.overlayWindow = w;
maxOpacity = w->opacity;
}
}
if (w->isExternalOverlay)
{
if (w->opacity >= maxOpacityExternal)
{
ctx->focus.externalOverlayWindow = w;
maxOpacityExternal = w->opacity;
}
}
}
}
}
if (ev->atom == ctx->atoms.steamAtom)
{
steamcompmgr_win_t * w = find_win(ctx, ev->window);
if (w)
{
w->isSteamLegacyBigPicture = get_prop(ctx, w->xwayland().id, ctx->atoms.steamAtom, 0);
focusDirty = true;
}
}
if (ev->atom == ctx->atoms.steamInputFocusAtom )
{
steamcompmgr_win_t * w = find_win(ctx, ev->window);
if (w)
{
w->inputFocusMode = get_prop(ctx, w->xwayland().id, ctx->atoms.steamInputFocusAtom, 0);
focusDirty = true;
}
}
if (ev->atom == ctx->atoms.steamTouchClickModeAtom )
{
g_nTouchClickMode = (enum wlserver_touch_click_mode) get_prop(ctx, ctx->root, ctx->atoms.steamTouchClickModeAtom, g_nDefaultTouchClickMode );
#if HAVE_OPENVR
if (BIsVRSession())
vrsession_update_touch_mode();
#endif
}
if (ev->atom == ctx->atoms.steamStreamingClientAtom)
{
steamcompmgr_win_t * w = find_win(ctx, ev->window);
if (w)
{
w->isSteamStreamingClient = get_prop(ctx, w->xwayland().id, ctx->atoms.steamStreamingClientAtom, 0);
focusDirty = true;
}
}
if (ev->atom == ctx->atoms.steamStreamingClientVideoAtom)
{
steamcompmgr_win_t * w = find_win(ctx, ev->window);
if (w)
{
w->isSteamStreamingClientVideo = get_prop(ctx, w->xwayland().id, ctx->atoms.steamStreamingClientVideoAtom, 0);
focusDirty = true;
}
}
if (ev->atom == ctx->atoms.gamescopeCtrlAppIDAtom )
{
get_prop( ctx, ctx->root, ctx->atoms.gamescopeCtrlAppIDAtom, vecFocuscontrolAppIDs );
focusDirty = true;
}
if (ev->atom == ctx->atoms.gamescopeCtrlWindowAtom )
{
ctx->focusControlWindow = get_prop( ctx, ctx->root, ctx->atoms.gamescopeCtrlWindowAtom, None );
focusDirty = true;
}
if ( ev->atom == ctx->atoms.gamescopeScreenShotAtom )
{
if ( ev->state == PropertyNewValue )
{
g_nTakeScreenshot = (int)get_prop( ctx, ctx->root, ctx->atoms.gamescopeScreenShotAtom, None );
g_bPropertyRequestedScreenshot = true;
}
}
if ( ev->atom == ctx->atoms.gamescopeDebugScreenShotAtom )
{
if ( ev->state == PropertyNewValue )
{
g_nTakeScreenshot = (int)get_prop( ctx, ctx->root, ctx->atoms.gamescopeDebugScreenShotAtom, None );
}
}
if (ev->atom == ctx->atoms.gameAtom)
{
steamcompmgr_win_t * w = find_win(ctx, ev->window);
if (w)
{
uint32_t appID = get_prop(ctx, w->xwayland().id, ctx->atoms.gameAtom, 0);
if ( w->appID != 0 && appID != 0 && w->appID != appID )
{
xwm_log.errorf( "appid clash was %u now %u", w->appID, appID );
}
w->appID = appID;
focusDirty = true;
}
}
if (ev->atom == ctx->atoms.overlayAtom)
{
steamcompmgr_win_t * w = find_win(ctx, ev->window);
if (w)
{
w->isOverlay = get_prop(ctx, w->xwayland().id, ctx->atoms.overlayAtom, 0);
focusDirty = true;
}
}
if (ev->atom == ctx->atoms.externalOverlayAtom)
{
steamcompmgr_win_t * w = find_win(ctx, ev->window);
if (w)
{
w->isExternalOverlay = get_prop(ctx, w->xwayland().id, ctx->atoms.externalOverlayAtom, 0);
focusDirty = true;
}
}
if (ev->atom == ctx->atoms.winTypeAtom)
{
steamcompmgr_win_t * w = find_win(ctx, ev->window);
if (w)
{
get_win_type(ctx, w);
focusDirty = true;
}
}
if (ev->atom == ctx->atoms.sizeHintsAtom)
{
steamcompmgr_win_t * w = find_win(ctx, ev->window);
if (w)
{
get_size_hints(ctx, w);
focusDirty = true;
}
}
if (ev->atom == ctx->atoms.gamesRunningAtom)
{
gamesRunningCount = get_prop(ctx, ctx->root, ctx->atoms.gamesRunningAtom, 0);
focusDirty = true;
}
if (ev->atom == ctx->atoms.screenScaleAtom)
{
overscanScaleRatio = get_prop(ctx, ctx->root, ctx->atoms.screenScaleAtom, 0xFFFFFFFF) / (double)0xFFFFFFFF;
globalScaleRatio = overscanScaleRatio * zoomScaleRatio;
if (global_focus.focusWindow)
{
hasRepaint = true;
}
focusDirty = true;
}
if (ev->atom == ctx->atoms.screenZoomAtom)
{
zoomScaleRatio = get_prop(ctx, ctx->root, ctx->atoms.screenZoomAtom, 0xFFFF) / (double)0xFFFF;
globalScaleRatio = overscanScaleRatio * zoomScaleRatio;
if (global_focus.focusWindow)
{
hasRepaint = true;
}
focusDirty = true;
}
if (ev->atom == ctx->atoms.WMTransientForAtom)
{
steamcompmgr_win_t * w = find_win(ctx, ev->window);
if (w)
{
Window transientFor = None;
if ( XGetTransientForHint( ctx->dpy, ev->window, &transientFor ) )
{
w->xwayland().transientFor = transientFor;
}
else
{
w->xwayland().transientFor = None;
}
get_win_type( ctx, w );
focusDirty = true;
}
}
if (ev->atom == XA_WM_NAME || ev->atom == ctx->atoms.netWMNameAtom)
{
steamcompmgr_win_t *w = find_win(ctx, ev->window);
if (w)
{
get_win_title(ctx, w, ev->atom);
if (ev->window == x11_win(global_focus.focusWindow))
{
sdlwindow_title( w->title, w->icon );
}
}
}
if (ev->atom == ctx->atoms.netWMIcon)
{
steamcompmgr_win_t *w = find_win(ctx, ev->window);
if (w)
{
get_win_icon(ctx, w);
if (ev->window == x11_win(global_focus.focusWindow))
{
sdlwindow_title( w->title, w->icon );
}
}
}
if (ev->atom == ctx->atoms.motifWMHints)
{
steamcompmgr_win_t *w = find_win(ctx, ev->window);
if (w) {
get_motif_hints(ctx, w);
focusDirty = true;
}
}
if ( ev->atom == ctx->atoms.gamescopeTuneableVBlankRedZone )
{
g_uVblankDrawBufferRedZoneNS = (uint64_t)get_prop( ctx, ctx->root, ctx->atoms.gamescopeTuneableVBlankRedZone, g_uDefaultVBlankRedZone );
}
if ( ev->atom == ctx->atoms.gamescopeTuneableRateOfDecay )
{
g_uVBlankRateOfDecayPercentage = (uint64_t)get_prop( ctx, ctx->root, ctx->atoms.gamescopeTuneableRateOfDecay, g_uDefaultVBlankRateOfDecayPercentage );
}
if ( ev->atom == ctx->atoms.gamescopeScalingFilter )
{
int nScalingMode = get_prop( ctx, ctx->root, ctx->atoms.gamescopeScalingFilter, 0 );
switch ( nScalingMode )
{
default:
case 0:
g_wantedUpscaleScaler = GamescopeUpscaleScaler::AUTO;
g_wantedUpscaleFilter = GamescopeUpscaleFilter::LINEAR;
break;
case 1:
g_wantedUpscaleScaler = GamescopeUpscaleScaler::AUTO;
g_wantedUpscaleFilter = GamescopeUpscaleFilter::NEAREST;
break;
case 2:
g_wantedUpscaleScaler = GamescopeUpscaleScaler::INTEGER;
g_wantedUpscaleFilter = GamescopeUpscaleFilter::NEAREST;
break;
case 3:
g_wantedUpscaleScaler = GamescopeUpscaleScaler::AUTO;
g_wantedUpscaleFilter = GamescopeUpscaleFilter::FSR;
break;
case 4:
g_wantedUpscaleScaler = GamescopeUpscaleScaler::AUTO;
g_wantedUpscaleFilter = GamescopeUpscaleFilter::NIS;
break;
}
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeFSRSharpness || ev->atom == ctx->atoms.gamescopeSharpness )
{
g_upscaleFilterSharpness = (int)clamp( get_prop( ctx, ctx->root, ev->atom, 2 ), 0u, 20u );
if ( g_upscaleFilter == GamescopeUpscaleFilter::FSR || g_upscaleFilter == GamescopeUpscaleFilter::NIS )
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeXWaylandModeControl )
{
std::vector< uint32_t > xwayland_mode_ctl;
bool hasModeCtrl = get_prop( ctx, ctx->root, ctx->atoms.gamescopeXWaylandModeControl, xwayland_mode_ctl );
if ( hasModeCtrl && xwayland_mode_ctl.size() == 4 )
{
size_t server_idx = size_t{ xwayland_mode_ctl[ 0 ] };
int width = xwayland_mode_ctl[ 1 ];
int height = xwayland_mode_ctl[ 2 ];
bool allowSuperRes = !!xwayland_mode_ctl[ 3 ];
if ( !allowSuperRes )
{
width = std::min<int>(width, currentOutputWidth);
height = std::min<int>(height, currentOutputHeight);
}
gamescope_xwayland_server_t *server = wlserver_get_xwayland_server( server_idx );
if ( server )
{
bool root_size_identical = server->ctx->root_width == width && server->ctx->root_height == height;
wlserver_lock();
wlserver_set_xwayland_server_mode( server_idx, width, height, g_nOutputRefresh );
wlserver_unlock();
if ( root_size_identical )
{
gamescope_xwayland_server_t *root_server = wlserver_get_xwayland_server(0);
xwayland_ctx_t *root_ctx = root_server->ctx.get();
XDeleteProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeXWaylandModeControl );
XFlush( root_ctx->dpy );
}
}
}
}
if ( ev->atom == ctx->atoms.gamescopeFPSLimit )
{
g_nSteamCompMgrTargetFPS = get_prop( ctx, ctx->root, ctx->atoms.gamescopeFPSLimit, 0 );
update_runtime_info();
}
for (int i = 0; i < DRM_SCREEN_TYPE_COUNT; i++)
{
if ( ev->atom == ctx->atoms.gamescopeDynamicRefresh[i] )
{
g_nDynamicRefreshRate[i] = get_prop( ctx, ctx->root, ctx->atoms.gamescopeDynamicRefresh[i], 0 );
}
}
if ( ev->atom == ctx->atoms.gamescopeLowLatency )
{
g_bLowLatency = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeLowLatency, 0 );
}
if ( ev->atom == ctx->atoms.gamescopeBlurMode )
{
BlurMode newBlur = (BlurMode)get_prop( ctx, ctx->root, ctx->atoms.gamescopeBlurMode, 0 );
if (newBlur < BLUR_MODE_OFF || newBlur > BLUR_MODE_ALWAYS)
newBlur = BLUR_MODE_OFF;
if (newBlur != g_BlurMode) {
g_BlurFadeStartTime = get_time_in_milliseconds();
g_BlurModeOld = g_BlurMode;
g_BlurMode = newBlur;
hasRepaint = true;
}
}
if ( ev->atom == ctx->atoms.gamescopeBlurRadius )
{
unsigned int pixel = get_prop( ctx, ctx->root, ctx->atoms.gamescopeBlurRadius, 0 );
g_BlurRadius = (int)clamp((pixel / 2) + 1, 1u, kMaxBlurRadius - 1);
if ( g_BlurMode )
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeBlurFadeDuration )
{
g_BlurFadeDuration = get_prop( ctx, ctx->root, ctx->atoms.gamescopeBlurFadeDuration, 0 );
}
if ( ev->atom == ctx->atoms.gamescopeCompositeForce )
{
alwaysComposite = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeCompositeForce, 0 );
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeCompositeDebug )
{
g_uCompositeDebug = get_prop( ctx, ctx->root, ctx->atoms.gamescopeCompositeDebug, 0 );
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeAllowTearing )
{
g_nAsyncFlipsEnabled = get_prop( ctx, ctx->root, ctx->atoms.gamescopeAllowTearing, 0 );
}
if ( ev->atom == ctx->atoms.gamescopeSteamMaxHeight )
{
g_nSteamMaxHeight = get_prop( ctx, ctx->root, ctx->atoms.gamescopeSteamMaxHeight, 0 );
focusDirty = true;
}
if ( ev->atom == ctx->atoms.gamescopeVRREnabled )
{
bool enabled = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeVRREnabled, 0 );
drm_set_vrr_enabled( &g_DRM, enabled );
}
if ( ev->atom == ctx->atoms.gamescopeDisplayForceInternal )
{
if ( !BIsNested() )
{
g_DRM.force_internal = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeDisplayForceInternal, 0 );
g_DRM.out_of_date = 1;
}
}
if ( ev->atom == ctx->atoms.gamescopeDisplayModeNudge )
{
if ( !BIsNested() )
{
g_DRM.out_of_date = 2;
XDeleteProperty( ctx->dpy, ctx->root, ctx->atoms.gamescopeDisplayModeNudge );
}
}
if ( ev->atom == ctx->atoms.gamescopeNewScalingFilter )
{
GamescopeUpscaleFilter nScalingFilter = ( GamescopeUpscaleFilter ) get_prop( ctx, ctx->root, ctx->atoms.gamescopeNewScalingFilter, 0 );
if (g_wantedUpscaleFilter != nScalingFilter)
{
g_wantedUpscaleFilter = nScalingFilter;
hasRepaint = true;
}
}
if ( ev->atom == ctx->atoms.gamescopeNewScalingScaler )
{
GamescopeUpscaleScaler nScalingScaler = ( GamescopeUpscaleScaler ) get_prop( ctx, ctx->root, ctx->atoms.gamescopeNewScalingScaler, 0 );
if (g_wantedUpscaleScaler != nScalingScaler)
{
g_wantedUpscaleScaler = nScalingScaler;
hasRepaint = true;
}
}
if ( ev->atom == ctx->atoms.gamescopeDisplayHDREnabled )
{
g_bHDREnabled = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeDisplayHDREnabled, 0 );
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeDebugForceHDR10Output )
{
g_bForceHDR10OutputDebug = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeDebugForceHDR10Output, 0 );
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeDebugForceHDRSupport )
{
g_bForceHDRSupportDebug = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeDebugForceHDRSupport, 0 );
drm_update_patched_edid(&g_DRM);
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeDebugHDRHeatmap )
{
uint32_t heatmap = get_prop( ctx, ctx->root, ctx->atoms.gamescopeDebugHDRHeatmap, 0 );
g_uCompositeDebug &= ~CompositeDebugFlag::Heatmap;
g_uCompositeDebug &= ~CompositeDebugFlag::Heatmap_MSWCG;
g_uCompositeDebug &= ~CompositeDebugFlag::Heatmap_Hard;
if (heatmap != 0)
g_uCompositeDebug |= CompositeDebugFlag::Heatmap;
if (heatmap == 2)
g_uCompositeDebug |= CompositeDebugFlag::Heatmap_MSWCG;
if (heatmap == 3)
g_uCompositeDebug |= CompositeDebugFlag::Heatmap_Hard;
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeHDRTonemapOperator )
{
g_ColorMgmt.pending.hdrTonemapOperator = (ETonemapOperator) get_prop( ctx, ctx->root, ctx->atoms.gamescopeHDRTonemapOperator, 0 );
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeHDRTonemapDisplayMetadata )
{
std::vector< uint32_t > user_vec;
if ( get_prop( ctx, ctx->root, ctx->atoms.gamescopeHDRTonemapDisplayMetadata, user_vec ) && user_vec.size() >= 2 )
{
g_ColorMgmt.pending.hdrTonemapDisplayMetadata.flBlackPointNits = bit_cast<float>( user_vec[0] );
g_ColorMgmt.pending.hdrTonemapDisplayMetadata.flWhitePointNits = bit_cast<float>( user_vec[1] );
}
else
{
g_ColorMgmt.pending.hdrTonemapDisplayMetadata.reset();
}
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeHDRTonemapSourceMetadata )
{
std::vector< uint32_t > user_vec;
if ( get_prop( ctx, ctx->root, ctx->atoms.gamescopeHDRTonemapSourceMetadata, user_vec ) && user_vec.size() >= 2 )
{
g_ColorMgmt.pending.hdrTonemapSourceMetadata.flBlackPointNits = bit_cast<float>( user_vec[0] );
g_ColorMgmt.pending.hdrTonemapSourceMetadata.flWhitePointNits = bit_cast<float>( user_vec[1] );
}
else
{
g_ColorMgmt.pending.hdrTonemapSourceMetadata.reset();
}
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeSDROnHDRContentBrightness )
{
uint32_t val = get_prop( ctx, ctx->root, ctx->atoms.gamescopeSDROnHDRContentBrightness, 0 );
if ( set_sdr_on_hdr_brightness( bit_cast<float>(val) ) )
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeHDRItmEnable )
{
g_bHDRItmEnable = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeHDRItmEnable, 0 );
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeHDRItmSDRNits )
{
g_flHDRItmSdrNits = get_prop( ctx, ctx->root, ctx->atoms.gamescopeHDRItmSDRNits, 0 );
if ( g_flHDRItmSdrNits < 1.f )
g_flHDRItmSdrNits = 100.f;
else if ( g_flHDRItmSdrNits > 1000.f)
g_flHDRItmSdrNits = 1000.f;
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeHDRItmTargetNits )
{
g_flHDRItmTargetNits = get_prop( ctx, ctx->root, ctx->atoms.gamescopeHDRItmTargetNits, 0 );
if ( g_flHDRItmTargetNits < 1.f )
g_flHDRItmTargetNits = 1000.f;
else if ( g_flHDRItmTargetNits > 10000.f)
g_flHDRItmTargetNits = 10000.f;
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeColorLookPQ )
{
std::string path = get_string_prop( ctx, ctx->root, ctx->atoms.gamescopeColorLookPQ );
if ( set_color_look_pq( path.c_str() ) )
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeColorLookG22 )
{
std::string path = get_string_prop( ctx, ctx->root, ctx->atoms.gamescopeColorLookG22 );
if ( set_color_look_g22( path.c_str() ) )
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeColorOutputVirtualWhite )
{
std::vector< uint32_t > user_vec;
if ( get_prop( ctx, ctx->root, ctx->atoms.gamescopeColorOutputVirtualWhite, user_vec ) && user_vec.size() >= 2 )
{
g_ColorMgmt.pending.outputVirtualWhite.x = santitize_float( bit_cast<float>( user_vec[0] ) );
g_ColorMgmt.pending.outputVirtualWhite.y = santitize_float( bit_cast<float>( user_vec[1] ) );
}
else
{
g_ColorMgmt.pending.outputVirtualWhite.x = 0.f;
g_ColorMgmt.pending.outputVirtualWhite.y = 0.f;
}
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeInternalDisplayBrightness )
{
uint32_t val = get_prop( ctx, ctx->root, ctx->atoms.gamescopeInternalDisplayBrightness, 0 );
if ( set_internal_display_brightness( bit_cast<float>(val) ) )
{
drm_update_patched_edid(&g_DRM);
hasRepaint = true;
}
}
if ( ev->atom == ctx->atoms.gamescopeHDRInputGain )
{
uint32_t val = get_prop( ctx, ctx->root, ctx->atoms.gamescopeHDRInputGain, 0 );
if ( set_hdr_input_gain( bit_cast<float>(val) ) )
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeSDRInputGain )
{
uint32_t val = get_prop( ctx, ctx->root, ctx->atoms.gamescopeSDRInputGain, 0 );
if ( set_sdr_input_gain( bit_cast<float>(val) ) )
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeForceWindowsFullscreen )
{
ctx->force_windows_fullscreen = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeForceWindowsFullscreen, 0 );
focusDirty = true;
}
if ( ev->atom == ctx->atoms.gamescopeColorLut3DOverride )
{
std::string path = get_string_prop( ctx, ctx->root, ctx->atoms.gamescopeColorLut3DOverride );
if ( set_color_3dlut_override( path.c_str() ) )
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeColorShaperLutOverride )
{
std::string path = get_string_prop( ctx, ctx->root, ctx->atoms.gamescopeColorShaperLutOverride );
if ( set_color_shaperlut_override( path.c_str() ) )
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeColorSDRGamutWideness )
{
uint32_t val = get_prop(ctx, ctx->root, ctx->atoms.gamescopeColorSDRGamutWideness, 0);
if ( set_color_sdr_gamut_wideness( bit_cast<float>(val) ) )
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeColorNightMode )
{
std::vector< uint32_t > user_vec;
bool bHasVec = get_prop( ctx, ctx->root, ctx->atoms.gamescopeColorNightMode, user_vec );
// identity
float vec[3] = { 0.0f, 0.0f, 0.0f };
if ( bHasVec && user_vec.size() == 3 )
{
for (int i = 0; i < 3; i++)
vec[i] = bit_cast<float>( user_vec[i] );
}
nightmode_t nightmode;
nightmode.amount = vec[0];
nightmode.hue = vec[1];
nightmode.saturation = vec[2];
if ( set_color_nightmode( nightmode ) )
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeColorManagementDisable )
{
uint32_t val = get_prop(ctx, ctx->root, ctx->atoms.gamescopeColorManagementDisable, 0);
if ( set_color_mgmt_enabled( !val ) )
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeColorSliderInUse )
{
uint32_t val = get_prop(ctx, ctx->root, ctx->atoms.gamescopeColorSliderInUse, 0);
g_bColorSliderInUse = !!val;
}
if ( ev->atom == ctx->atoms.gamescopeColorChromaticAdaptationMode )
{
uint32_t val = get_prop(ctx, ctx->root, ctx->atoms.gamescopeColorChromaticAdaptationMode, 0);
g_ColorMgmt.pending.chromaticAdaptationMode = ( EChromaticAdaptationMethod ) val;
}
// TODO: Hook up gamescopeColorMuraCorrectionImage for external.
if ( ev->atom == ctx->atoms.gamescopeColorMuraCorrectionImage[DRM_SCREEN_TYPE_INTERNAL] )
{
std::string path = get_string_prop( ctx, ctx->root, ctx->atoms.gamescopeColorMuraCorrectionImage[DRM_SCREEN_TYPE_INTERNAL] );
if ( set_mura_overlay( path.c_str() ) )
hasRepaint = true;
}
// TODO: Hook up gamescopeColorMuraScale for external.
if ( ev->atom == ctx->atoms.gamescopeColorMuraScale[DRM_SCREEN_TYPE_INTERNAL] )
{
uint32_t val = get_prop(ctx, ctx->root, ctx->atoms.gamescopeColorMuraScale[DRM_SCREEN_TYPE_INTERNAL], 0);
float new_scale = bit_cast<float>(val);
if ( set_mura_scale( new_scale ) )
hasRepaint = true;
}
// TODO: Hook up gamescopeColorMuraCorrectionDisabled for external.
if ( ev->atom == ctx->atoms.gamescopeColorMuraCorrectionDisabled[DRM_SCREEN_TYPE_INTERNAL] )
{
bool disabled = !!get_prop(ctx, ctx->root, ctx->atoms.gamescopeColorMuraCorrectionDisabled[DRM_SCREEN_TYPE_INTERNAL], 0);
if ( g_bMuraCompensationDisabled != disabled ) {
g_bMuraCompensationDisabled = disabled;
hasRepaint = true;
}
}
if (ev->atom == ctx->atoms.gamescopeCreateXWaylandServer)
{
uint32_t identifier = get_prop(ctx, ctx->root, ctx->atoms.gamescopeCreateXWaylandServer, 0);
if (identifier)
{
wlserver_lock();
uint32_t server_id = (uint32_t)wlserver_make_new_xwayland_server();
assert(server_id != ~0u);
gamescope_xwayland_server_t *server = wlserver_get_xwayland_server(server_id);
init_xwayland_ctx(server_id, server);
char propertyString[256];
snprintf(propertyString, sizeof(propertyString), "%u %u %s", identifier, server_id, server->get_nested_display_name());
XTextProperty text_property =
{
.value = (unsigned char *)propertyString,
.encoding = ctx->atoms.utf8StringAtom,
.format = 8,
.nitems = strlen(propertyString),
};
pollfds.push_back(pollfd {
.fd = XConnectionNumber( server->ctx->dpy ),
.events = POLLIN,
});
XSetTextProperty( ctx->dpy, ctx->root, &text_property, ctx->atoms.gamescopeCreateXWaylandServerFeedback );
wlserver_unlock();
}
}
if (ev->atom == ctx->atoms.gamescopeDestroyXWaylandServer)
{
uint32_t server_id = get_prop(ctx, ctx->root, ctx->atoms.gamescopeDestroyXWaylandServer, 0);
gamescope_xwayland_server_t *server = wlserver_get_xwayland_server(server_id);
if (server)
{
if (global_focus.focusWindow &&
global_focus.focusWindow->type == steamcompmgr_win_type_t::XWAYLAND &&
global_focus.focusWindow->xwayland().ctx == server->ctx.get())
global_focus.focusWindow = nullptr;
if (global_focus.inputFocusWindow &&
global_focus.inputFocusWindow->type == steamcompmgr_win_type_t::XWAYLAND &&
global_focus.inputFocusWindow->xwayland().ctx == server->ctx.get())
global_focus.inputFocusWindow = nullptr;
if (global_focus.overlayWindow &&
global_focus.overlayWindow->type == steamcompmgr_win_type_t::XWAYLAND &&
global_focus.overlayWindow->xwayland().ctx == server->ctx.get())
global_focus.overlayWindow = nullptr;
if (global_focus.externalOverlayWindow &&
global_focus.externalOverlayWindow->type == steamcompmgr_win_type_t::XWAYLAND &&
global_focus.externalOverlayWindow->xwayland().ctx == server->ctx.get())
global_focus.externalOverlayWindow = nullptr;
if (global_focus.notificationWindow &&
global_focus.notificationWindow->type == steamcompmgr_win_type_t::XWAYLAND &&
global_focus.notificationWindow->xwayland().ctx == server->ctx.get())
global_focus.notificationWindow = nullptr;
if (global_focus.overrideWindow &&
global_focus.overrideWindow->type == steamcompmgr_win_type_t::XWAYLAND &&
global_focus.overrideWindow->xwayland().ctx == server->ctx.get())
global_focus.overrideWindow = nullptr;
if (global_focus.keyboardFocusWindow &&
global_focus.keyboardFocusWindow->type == steamcompmgr_win_type_t::XWAYLAND &&
global_focus.keyboardFocusWindow->xwayland().ctx == server->ctx.get())
global_focus.keyboardFocusWindow = nullptr;
if (global_focus.fadeWindow &&
global_focus.fadeWindow->type == steamcompmgr_win_type_t::XWAYLAND &&
global_focus.fadeWindow->xwayland().ctx == server->ctx.get())
global_focus.fadeWindow = nullptr;
if (global_focus.cursor &&
global_focus.cursor->getCtx() == server->ctx.get())
global_focus.cursor = nullptr;
wlserver_lock();
std::erase_if(pollfds, [=](const auto& other){
return other.fd == XConnectionNumber( server->ctx->dpy );
});
wlserver_destroy_xwayland_server(server);
wlserver_unlock();
focusDirty = true;
}
}
if (ev->atom == ctx->atoms.gamescopeReshadeTechniqueIdx)
{
uint32_t technique_idx = get_prop(ctx, ctx->root, ctx->atoms.gamescopeReshadeTechniqueIdx, 0);
g_reshade_technique_idx = technique_idx;
}
if (ev->atom == ctx->atoms.gamescopeReshadeEffect)
{
std::string path = get_string_prop( ctx, ctx->root, ctx->atoms.gamescopeReshadeEffect );
g_reshade_effect = path;
}
if (ev->atom == ctx->atoms.gamescopeDisplayDynamicRefreshBasedOnGamePresence)
{
g_bChangeDynamicRefreshBasedOnGameOpenRatherThanActive = !!get_prop(ctx, ctx->root, ctx->atoms.gamescopeDisplayDynamicRefreshBasedOnGamePresence, 0);
}
if (ev->atom == ctx->atoms.wineHwndStyle)
{
steamcompmgr_win_t * w = find_win(ctx, ev->window);
if (w)
{
w->hasHwndStyle = true;
w->hwndStyle = get_prop(ctx, w->xwayland().id, ctx->atoms.wineHwndStyle, 0);
focusDirty = true;
}
}
if (ev->atom == ctx->atoms.wineHwndStyleEx)
{
steamcompmgr_win_t * w = find_win(ctx, ev->window);
if (w)
{
w->hasHwndStyleEx = true;
w->hwndStyleEx = get_prop(ctx, w->xwayland().id, ctx->atoms.wineHwndStyleEx, 0);
focusDirty = true;
}
}
}
static int
error(Display *dpy, XErrorEvent *ev)
{
xwayland_ctx_t *ctx = NULL;
// Find ctx for dpy
{
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
{
if (server->ctx->dpy == dpy)
{
ctx = server->ctx.get();
break;
}
}
}
if ( !ctx )
{
// Not for us!
return 0;
}
int o;
const char *name = NULL;
static char buffer[256];
if (should_ignore(ctx, ev->serial))
return 0;
if (ev->request_code == ctx->composite_opcode &&
ev->minor_code == X_CompositeRedirectSubwindows)
{
xwm_log.errorf("Another composite manager is already running");
exit(1);
}
o = ev->error_code - ctx->xfixes_error;
switch (o) {
case BadRegion: name = "BadRegion"; break;
default: break;
}
o = ev->error_code - ctx->damage_error;
switch (o) {
case BadDamage: name = "BadDamage"; break;
default: break;
}
o = ev->error_code - ctx->render_error;
switch (o) {
case BadPictFormat: name ="BadPictFormat"; break;
case BadPicture: name ="BadPicture"; break;
case BadPictOp: name ="BadPictOp"; break;
case BadGlyphSet: name ="BadGlyphSet"; break;
case BadGlyph: name ="BadGlyph"; break;
default: break;
}
if (name == NULL)
{
buffer[0] = '\0';
XGetErrorText(ctx->dpy, ev->error_code, buffer, sizeof(buffer));
name = buffer;
}
xwm_log.errorf("error %d: %s request %d minor %d serial %lu",
ev->error_code, (strlen(name) > 0) ? name : "unknown",
ev->request_code, ev->minor_code, ev->serial);
gotXError = true;
/* abort(); this is just annoying to most people */
return 0;
}
[[noreturn]] static void
steamcompmgr_exit(void)
{
// Clean up any commits.
{
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
{
for ( steamcompmgr_win_t *w = server->ctx->list; w; w = w->xwayland().next )
w->commit_queue.clear();
}
}
g_steamcompmgr_xdg_wins.clear();
g_HeldCommits[ HELD_COMMIT_BASE ] = nullptr;
g_HeldCommits[ HELD_COMMIT_FADE ] = nullptr;
g_ImageWaiter.Shutdown();
if ( statsThreadRun == true )
{
statsThreadRun = false;
statsThreadSem.signal();
}
sdlwindow_shutdown();
wlserver_lock();
wlserver_force_shutdown();
wlserver_unlock(false);
finish_drm( &g_DRM );
pthread_exit(NULL);
}
static int
handle_io_error(Display *dpy)
{
xwm_log.errorf("X11 I/O error");
steamcompmgr_exit();
}
static bool
register_cm(xwayland_ctx_t *ctx)
{
Window w;
Atom a;
static char net_wm_cm[] = "_NET_WM_CM_Sxx";
snprintf(net_wm_cm, sizeof(net_wm_cm), "_NET_WM_CM_S%d", ctx->scr);
a = XInternAtom(ctx->dpy, net_wm_cm, false);
w = XGetSelectionOwner(ctx->dpy, a);
if (w != None)
{
XTextProperty tp;
char **strs;
int count;
Atom winNameAtom = XInternAtom(ctx->dpy, "_NET_WM_NAME", false);
if (!XGetTextProperty(ctx->dpy, w, &tp, winNameAtom) &&
!XGetTextProperty(ctx->dpy, w, &tp, XA_WM_NAME))
{
xwm_log.errorf("Another composite manager is already running (0x%lx)", (unsigned long) w);
return false;
}
if (XmbTextPropertyToTextList(ctx->dpy, &tp, &strs, &count) == Success)
{
xwm_log.errorf("Another composite manager is already running (%s)", strs[0]);
XFreeStringList(strs);
}
XFree(tp.value);
return false;
}
w = XCreateSimpleWindow(ctx->dpy, RootWindow(ctx->dpy, ctx->scr), 0, 0, 1, 1, 0, None,
None);
Xutf8SetWMProperties(ctx->dpy, w, "steamcompmgr", "steamcompmgr", NULL, 0, NULL, NULL,
NULL);
Atom atomWmCheck = XInternAtom(ctx->dpy, "_NET_SUPPORTING_WM_CHECK", false);
XChangeProperty(ctx->dpy, ctx->root, atomWmCheck,
XA_WINDOW, 32, PropModeReplace, (unsigned char *)&w, 1);
XChangeProperty(ctx->dpy, w, atomWmCheck,
XA_WINDOW, 32, PropModeReplace, (unsigned char *)&w, 1);
Atom supportedAtoms[] = {
XInternAtom(ctx->dpy, "_NET_WM_STATE", false),
XInternAtom(ctx->dpy, "_NET_WM_STATE_FULLSCREEN", false),
XInternAtom(ctx->dpy, "_NET_WM_STATE_SKIP_TASKBAR", false),
XInternAtom(ctx->dpy, "_NET_WM_STATE_SKIP_PAGER", false),
XInternAtom(ctx->dpy, "_NET_ACTIVE_WINDOW", false),
};
XChangeProperty(ctx->dpy, ctx->root, XInternAtom(ctx->dpy, "_NET_SUPPORTED", false),
XA_ATOM, 32, PropModeAppend, (unsigned char *)supportedAtoms,
sizeof(supportedAtoms) / sizeof(supportedAtoms[0]));
XSetSelectionOwner(ctx->dpy, a, w, 0);
ctx->ourWindow = w;
return true;
}
static void
register_systray(xwayland_ctx_t *ctx)
{
static char net_system_tray_name[] = "_NET_SYSTEM_TRAY_Sxx";
snprintf(net_system_tray_name, sizeof(net_system_tray_name),
"_NET_SYSTEM_TRAY_S%d", ctx->scr);
Atom net_system_tray = XInternAtom(ctx->dpy, net_system_tray_name, false);
XSetSelectionOwner(ctx->dpy, net_system_tray, ctx->ourWindow, 0);
}
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;
for ( j = 0; j < w->commit_queue.size(); j++ )
{
if ( w->commit_queue[ j ]->commitID == commitID )
{
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
// If this is an overlay that we're presenting, repaint
if ( gameFocused )
{
if ( w == global_focus.overlayWindow && w->opacity != TRANSLUCENT )
{
hasRepaintNonBasePlane = true;
}
if ( w == global_focus.notificationWindow && w->opacity != TRANSLUCENT )
{
hasRepaintNonBasePlane = true;
}
}
if ( ctx )
{
if ( ctx->focus.outdatedInteractiveFocus )
{
focusDirty = true;
ctx->focus.outdatedInteractiveFocus = false;
}
}
if ( global_focus.outdatedInteractiveFocus )
{
focusDirty = true;
global_focus.outdatedInteractiveFocus = false;
// If this is an external overlay, repaint
if ( w == global_focus.externalOverlayWindow && w->opacity != TRANSLUCENT )
{
hasRepaintNonBasePlane = true;
}
}
// If this is the main plane, repaint
if ( w == global_focus.focusWindow && !w->isSteamStreamingClient )
{
g_HeldCommits[ HELD_COMMIT_BASE ] = w->commit_queue[ j ];
hasRepaint = true;
}
if ( w == global_focus.overrideWindow )
{
hasRepaintNonBasePlane = true;
}
if ( w->isSteamStreamingClientVideo && global_focus.focusWindow && global_focus.focusWindow->isSteamStreamingClient )
{
g_HeldCommits[ HELD_COMMIT_BASE ] = w->commit_queue[ j ];
hasRepaint = true;
}
break;
}
}
if ( bFoundWindow == true )
{
if ( j > 0 )
{
// we can release all commits prior to done ones
w->commit_queue.erase( w->commit_queue.begin(), w->commit_queue.begin() + j );
}
w->receivedDoneCommit = true;
return true;
}
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 ( 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 );
continue;
}
for ( steamcompmgr_win_t *w = ctx->list; w; w = w->xwayland().next )
{
if (handle_done_commit(w, ctx, entry.commitID, entry.earliestPresentTime, entry.earliestLatchTime))
break;
}
}
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 ( 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, entry.commitID, entry.earliestPresentTime, entry.earliestLatchTime))
break;
}
}
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)
{
if (!lastCommit->presentation_feedbacks.empty() || lastCommit->present_id)
{
wlserver_lock();
if (!lastCommit->presentation_feedbacks.empty())
{
wlserver_presentation_feedback_presented(
lastCommit->surf,
lastCommit->presentation_feedbacks,
next_refresh_time,
refresh_cycle);
}
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();
}
}
if (struct wlr_surface *surface = w->current_surface())
{
auto info = get_wl_surface_info(surface);
if (info->gamescope_swapchain != nullptr && info->last_refresh_cycle != refresh_cycle)
{
wlserver_lock();
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)
{
info->last_refresh_cycle = refresh_cycle;
wlserver_refresh_cycle(surface, refresh_cycle);
}
}
wlserver_unlock();
}
}
}
void handle_presented_xwayland( xwayland_ctx_t *ctx )
{
for ( steamcompmgr_win_t *w = ctx->list; w; w = w->xwayland().next )
{
handle_presented_for_window(w);
}
}
void handle_presented_xdg()
{
for (const auto& xdg_win : g_steamcompmgr_xdg_wins)
{
handle_presented_for_window(xdg_win.get());
}
}
void nudge_steamcompmgr( void )
{
if ( write( g_nudgePipe[ 1 ], "\n", 1 ) < 0 )
xwm_log.errorf_errno( "nudge_steamcompmgr: write failed" );
}
void take_screenshot( int flags )
{
g_nTakeScreenshot = flags;
nudge_steamcompmgr();
}
void force_repaint( void )
{
g_bForceRepaint = true;
nudge_steamcompmgr();
}
void update_wayland_res(CommitDoneList_t *doneCommits, steamcompmgr_win_t *w, ResListEntry_t& reslistentry)
{
struct wlr_buffer *buf = reslistentry.buf;
if ( w == nullptr )
{
wlserver_lock();
wlr_buffer_unlock( buf );
wlserver_unlock();
xwm_log.errorf( "waylandres but no win" );
return;
}
// If we have an override surface, make sure this commit is for the current surface.
bool for_current_surface = !w->override_surface() || w->current_surface() == reslistentry.surf;
if (!for_current_surface)
{
wlserver_lock();
wlr_buffer_unlock( buf );
wlserver_unlock();
// Don't mark as recieve done commit, it was for the wrong surface.
return;
}
bool already_exists = false;
for ( const auto& existing_commit : w->commit_queue )
{
if (existing_commit->buf == buf)
already_exists = true;
}
if ( already_exists && !reslistentry.feedback && reslistentry.presentation_feedbacks.empty() )
{
wlserver_lock();
wlr_buffer_unlock( buf );
wlserver_unlock();
xwm_log.errorf( "got the same buffer committed twice, ignoring." );
// If we have a duplicated commit + frame callback, ensure that is signalled.
// This matches Mutter and Weston behavior, so it's plausible that some application relies on forward progress.
// We're essentially discarding the commit here, so consider it complete right away.
w->receivedDoneCommit = true;
return;
}
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 )
{
struct wlr_dmabuf_attributes dmabuf = {0};
if ( wlr_buffer_get_dmabuf( buf, &dmabuf ) )
{
fence = dup( dmabuf.fd[0] );
}
else
{
fence = newCommit->vulkanTex->memoryFence();
}
// Whether or not to nudge mango app when this commit is done.
const bool mango_nudge = ( w == global_focus.focusWindow && !w->isSteamStreamingClient ) ||
( global_focus.focusWindow && global_focus.focusWindow->isSteamStreamingClient && w->isSteamStreamingClientVideo );
gpuvis_trace_printf( "pushing wait for commit %lu win %lx", newCommit->commitID, w->type == steamcompmgr_win_type_t::XWAYLAND ? w->xwayland().id : 0 );
{
newCommit->m_nCommitFence = fence;
newCommit->m_bMangoNudge = mango_nudge;
newCommit->pDoneCommits = doneCommits;
g_ImageWaiter.AddWaitable( newCommit.get(), EPOLLIN );
}
w->commit_queue.push_back( std::move(newCommit) );
}
}
void check_new_xwayland_res(xwayland_ctx_t *ctx)
{
// When importing buffer, we'll potentially need to perform operations with
// a wlserver lock (e.g. wlr_buffer_lock). We can't do this with a
// wayland_commit_queue lock because that causes deadlocks.
std::vector<ResListEntry_t> tmp_queue = ctx->xwayland_server->retrieve_commits();
for ( uint32_t i = 0; i < tmp_queue.size(); i++ )
{
steamcompmgr_win_t *w = find_win( ctx, tmp_queue[ i ].surf );
update_wayland_res( &ctx->doneCommits, w, tmp_queue[ i ]);
}
}
void check_new_xdg_res()
{
std::vector<ResListEntry_t> tmp_queue = wlserver_xdg_commit_queue();
for ( uint32_t i = 0; i < tmp_queue.size(); i++ )
{
for ( const auto& xdg_win : g_steamcompmgr_xdg_wins )
{
if ( xdg_win->xdg().surface.main_surface == tmp_queue[ i ].surf )
{
update_wayland_res( &g_steamcompmgr_xdg_done_commits, xdg_win.get(), tmp_queue[ i ] );
break;
}
}
}
}
pid_t child_pid = 0;
static void
spawn_client( char **argv )
{
#if defined(__linux__)
// (Don't Lose) The Children
prctl( PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0 );
#elif defined(__DragonFly__) || defined(__FreeBSD__)
procctl(P_PID, getpid(), PROC_REAP_ACQUIRE, NULL);
#else
#warning "Changing reaper process for children is not supported on this platform"
#endif
std::string strNewPreload;
char *pchPreloadCopy = nullptr;
const char *pchCurrentPreload = getenv( "LD_PRELOAD" );
bool bFirst = true;
if ( pchCurrentPreload != nullptr )
{
pchPreloadCopy = strdup( pchCurrentPreload );
// First replace all the separators in our copy with terminators
for ( uint32_t i = 0; i < strlen( pchCurrentPreload ); i++ )
{
if ( pchPreloadCopy[ i ] == ' ' || pchPreloadCopy[ i ] == ':' )
{
pchPreloadCopy[ i ] = '\0';
}
}
// Then walk it again and find all the substrings
uint32_t i = 0;
while ( i < strlen( pchCurrentPreload ) )
{
// If there's a string and it's not gameoverlayrenderer, append it to our new LD_PRELOAD
if ( pchPreloadCopy[ i ] != '\0' )
{
if ( strstr( pchPreloadCopy + i, "gameoverlayrenderer.so" ) == nullptr )
{
if ( bFirst == false )
{
strNewPreload.append( ":" );
}
else
{
bFirst = false;
}
strNewPreload.append( pchPreloadCopy + i );
}
i += strlen( pchPreloadCopy + i );
}
else
{
i++;
}
}
free( pchPreloadCopy );
}
child_pid = fork();
if ( child_pid < 0 )
xwm_log.errorf_errno( "fork failed" );
// Are we in the child?
if ( child_pid == 0 )
{
// Try to snap back to old priority
if ( g_bNiceCap == true )
{
if ( g_bRt == true ){
sched_setscheduler(0, g_nOldPolicy, &g_schedOldParam);
}
nice( g_nOldNice - g_nNewNice );
}
// Restore prior rlimit in case child uses select()
restore_fd_limit();
// Set modified LD_PRELOAD if needed
if ( pchCurrentPreload != nullptr )
{
if ( strNewPreload.empty() == false )
{
setenv( "LD_PRELOAD", strNewPreload.c_str(), 1 );
}
else
{
unsetenv( "LD_PRELOAD" );
}
}
unsetenv( "ENABLE_VKBASALT" );
// Enable Gamescope WSI by default for nested.
setenv( "ENABLE_GAMESCOPE_WSI", "1", 0 );
// Unset this to avoid it leaking to Proton apps, etc.
unsetenv("SDL_VIDEODRIVER");
execvp( argv[ 0 ], argv );
xwm_log.errorf_errno( "execvp failed" );
_exit( 1 );
}
std::thread waitThread([]() {
pthread_setname_np( pthread_self(), "gamescope-wait" );
// Because we've set PR_SET_CHILD_SUBREAPER above, we'll get process
// status notifications for all of our child processes, even if our
// direct child exits. Wait until all have exited.
while ( true )
{
if ( wait( nullptr ) < 0 )
{
if ( errno == EINTR )
continue;
if ( errno != ECHILD )
xwm_log.errorf_errno( "steamcompmgr: wait failed" );
break;
}
}
fprintf(stderr, "gamescope: children shut down!\n");
child_pid = 0;
g_bRun = false;
nudge_steamcompmgr();
});
waitThread.detach();
}
static void
handle_xfixes_selection_notify( xwayland_ctx_t *ctx, XFixesSelectionNotifyEvent *event )
{
if (event->owner == ctx->ourWindow)
{
return;
}
XConvertSelection(ctx->dpy, event->selection, ctx->atoms.utf8StringAtom, event->selection, ctx->ourWindow, CurrentTime);
XFlush(ctx->dpy);
}
static void
dispatch_x11( xwayland_ctx_t *ctx )
{
MouseCursor *cursor = ctx->cursor.get();
bool bShouldResetCursor = false;
bool bSetFocus = false;
while (XPending(ctx->dpy))
{
XEvent ev;
int ret = XNextEvent(ctx->dpy, &ev);
if (ret != 0)
{
xwm_log.errorf("XNextEvent failed");
break;
}
if ((ev.type & 0x7f) != KeymapNotify)
discard_ignore(ctx, ev.xany.serial);
if (debugEvents)
{
gpuvis_trace_printf("event %d", ev.type);
printf("event %d\n", ev.type);
}
switch (ev.type) {
case CreateNotify:
if (ev.xcreatewindow.parent == ctx->root)
add_win(ctx, ev.xcreatewindow.window, 0, ev.xcreatewindow.serial);
break;
case ConfigureNotify:
configure_win(ctx, &ev.xconfigure);
break;
case DestroyNotify:
{
steamcompmgr_win_t * w = find_win(ctx, ev.xdestroywindow.window);
if (w && w->xwayland().id == ev.xdestroywindow.window)
destroy_win(ctx, ev.xdestroywindow.window, true, true);
break;
}
case MapNotify:
{
steamcompmgr_win_t * w = find_win(ctx, ev.xmap.window);
if (w && w->xwayland().id == ev.xmap.window)
map_win(ctx, ev.xmap.window, ev.xmap.serial);
break;
}
case UnmapNotify:
{
steamcompmgr_win_t * w = find_win(ctx, ev.xunmap.window);
if (w && w->xwayland().id == ev.xunmap.window)
unmap_win(ctx, ev.xunmap.window, true);
break;
}
case FocusOut:
{
steamcompmgr_win_t * w = find_win( ctx, ev.xfocus.window );
// If focus escaped the current desired keyboard focus window, check where it went
if ( w && w->xwayland().id == ctx->currentKeyboardFocusWindow )
{
Window newKeyboardFocus = None;
int nRevertMode = 0;
XGetInputFocus( ctx->dpy, &newKeyboardFocus, &nRevertMode );
// Find window or its toplevel parent
steamcompmgr_win_t *kbw = find_win( ctx, newKeyboardFocus );
if ( kbw )
{
if ( kbw->xwayland().id == ctx->currentKeyboardFocusWindow )
{
// focus went to a child, this is fine, make note of it in case we need to fix it
ctx->currentKeyboardFocusWindow = newKeyboardFocus;
}
else
{
// focus went elsewhere, correct it
bSetFocus = true;
}
}
}
break;
}
case ReparentNotify:
if (ev.xreparent.parent == ctx->root)
add_win(ctx, ev.xreparent.window, 0, ev.xreparent.serial);
else
{
steamcompmgr_win_t * w = find_win(ctx, ev.xreparent.window);
if (w && w->xwayland().id == ev.xreparent.window)
{
destroy_win(ctx, ev.xreparent.window, false, true);
}
else
{
// If something got reparented _to_ a toplevel window,
// go check for the fullscreen workaround again.
w = find_win(ctx, ev.xreparent.parent);
if (w)
{
get_size_hints(ctx, w);
focusDirty = true;
}
}
}
break;
case CirculateNotify:
circulate_win(ctx, &ev.xcirculate);
break;
case MapRequest:
map_request(ctx, &ev.xmaprequest);
break;
case ConfigureRequest:
configure_request(ctx, &ev.xconfigurerequest);
break;
case CirculateRequest:
circulate_request(ctx, &ev.xcirculaterequest);
break;
case Expose:
break;
case PropertyNotify:
handle_property_notify(ctx, &ev.xproperty);
break;
case ClientMessage:
handle_client_message(ctx, &ev.xclient);
break;
case LeaveNotify:
if (ev.xcrossing.window == x11_win(ctx->focus.inputFocusWindow) &&
!ctx->focus.overrideWindow)
{
// Josh: need to defer this as we could have a destroy later on
// and end up submitting commands with the currentInputFocusWIndow
bShouldResetCursor = true;
}
break;
case SelectionNotify:
handle_selection_notify(ctx, &ev.xselection);
break;
case SelectionRequest:
handle_selection_request(ctx, &ev.xselectionrequest);
break;
default:
if (ev.type == ctx->damage_event + XDamageNotify)
{
damage_win(ctx, (XDamageNotifyEvent *) &ev);
}
else if (ev.type == ctx->xfixes_event + XFixesCursorNotify)
{
cursor->setDirty();
}
else if (ev.type == ctx->xfixes_event + XFixesSelectionNotify)
{
handle_xfixes_selection_notify(ctx, (XFixesSelectionNotifyEvent *) &ev);
}
break;
}
XFlush(ctx->dpy);
}
if ( bShouldResetCursor )
{
// This shouldn't happen due to our pointer barriers,
// but there is a known X server bug; warp to last good
// position.
cursor->resetPosition();
}
if ( bSetFocus )
{
XSetInputFocus(ctx->dpy, ctx->currentKeyboardFocusWindow, RevertToNone, CurrentTime);
}
}
static bool
dispatch_vblank( int fd )
{
bool vblank = false;
for (;;)
{
VBlankTimeInfo_t vblanktime = {};
ssize_t ret = read( fd, &vblanktime, sizeof( vblanktime ) );
if ( ret < 0 )
{
if ( errno == EAGAIN )
break;
xwm_log.errorf_errno( "steamcompmgr: dispatch_vblank: read failed" );
break;
}
g_SteamCompMgrVBlankTime = vblanktime;
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" );
}
else
{
gpuvis_trace_printf( "got vblank" );
vblank = true;
}
}
return vblank;
}
struct rgba_t
{
uint8_t r,g,b,a;
};
static bool
load_mouse_cursor( MouseCursor *cursor, const char *path, int hx, int hy )
{
int w, h, channels;
rgba_t *data = (rgba_t *) stbi_load(path, &w, &h, &channels, STBI_rgb_alpha);
if (!data)
{
xwm_log.errorf("Failed to open/load cursor file");
return false;
}
std::transform(data, data + w * h, data, [](rgba_t x) {
if (x.a == 0)
return rgba_t{};
return rgba_t{
uint8_t((x.b * x.a) / 255),
uint8_t((x.g * x.a) / 255),
uint8_t((x.r * x.a) / 255),
x.a };
});
// Data is freed by XDestroyImage in setCursorImage.
return cursor->setCursorImage((char *)data, w, h, hx, hy);
}
static bool
load_host_cursor( MouseCursor *cursor )
{
extern const char *g_pOriginalDisplay;
if ( !g_pOriginalDisplay )
return false;
Display *display = XOpenDisplay( g_pOriginalDisplay );
if ( !display )
return false;
defer( XCloseDisplay( display ) );
int xfixes_event, xfixes_error;
if (!XFixesQueryExtension(display, &xfixes_event, &xfixes_error))
{
xwm_log.errorf("No XFixes extension on current compositor");
return false;
}
XFixesCursorImage *image = XFixesGetCursorImage( display );
if ( !image )
return false;
defer( XFree( image ) );
// image->pixels is `unsigned long*` :/
// Thanks X11.
std::vector<uint32_t> cursorData;
for (uint32_t y = 0; y < image->height; y++)
{
for (uint32_t x = 0; x < image->width; x++)
{
cursorData.push_back((uint32_t)image->pixels[image->height * y + x]);
}
}
cursor->setCursorImage((char *)cursorData.data(), image->width, image->height, image->xhot, image->yhot);
return true;
}
enum steamcompmgr_event_type {
EVENT_VBLANK,
EVENT_NUDGE,
EVENT_X11,
// Any past here are X11
};
const char* g_customCursorPath = nullptr;
int g_customCursorHotspotX = 0;
int g_customCursorHotspotY = 0;
xwayland_ctx_t g_ctx;
static bool setup_error_handlers = false;
void init_xwayland_ctx(uint32_t serverId, gamescope_xwayland_server_t *xwayland_server)
{
assert(!xwayland_server->ctx);
xwayland_server->ctx = std::make_unique<xwayland_ctx_t>();
xwayland_ctx_t *ctx = xwayland_server->ctx.get();
ctx->ignore_tail = &ctx->ignore_head;
int composite_major, composite_minor;
int xres_major, xres_minor;
ctx->xwayland_server = xwayland_server;
ctx->dpy = xwayland_server->get_xdisplay();
if (!ctx->dpy)
{
xwm_log.errorf("Can't open display");
exit(1);
}
if (!setup_error_handlers)
{
XSetErrorHandler(error);
XSetIOErrorHandler(handle_io_error);
setup_error_handlers = true;
}
if (synchronize)
XSynchronize(ctx->dpy, 1);
ctx->scr = DefaultScreen(ctx->dpy);
ctx->root = RootWindow(ctx->dpy, ctx->scr);
if (!XRenderQueryExtension(ctx->dpy, &ctx->render_event, &ctx->render_error))
{
xwm_log.errorf("No render extension");
exit(1);
}
if (!XQueryExtension(ctx->dpy, COMPOSITE_NAME, &ctx->composite_opcode,
&ctx->composite_event, &ctx->composite_error))
{
xwm_log.errorf("No composite extension");
exit(1);
}
XCompositeQueryVersion(ctx->dpy, &composite_major, &composite_minor);
if (!XDamageQueryExtension(ctx->dpy, &ctx->damage_event, &ctx->damage_error))
{
xwm_log.errorf("No damage extension");
exit(1);
}
if (!XFixesQueryExtension(ctx->dpy, &ctx->xfixes_event, &ctx->xfixes_error))
{
xwm_log.errorf("No XFixes extension");
exit(1);
}
if (!XShapeQueryExtension(ctx->dpy, &ctx->xshape_event, &ctx->xshape_error))
{
xwm_log.errorf("No XShape extension");
exit(1);
}
if (!XFixesQueryExtension(ctx->dpy, &ctx->xfixes_event, &ctx->xfixes_error))
{
xwm_log.errorf("No XFixes extension");
exit(1);
}
if (!XResQueryVersion(ctx->dpy, &xres_major, &xres_minor))
{
xwm_log.errorf("No XRes extension");
exit(1);
}
if (xres_major != 1 || xres_minor < 2)
{
xwm_log.errorf("Unsupported XRes version: have %d.%d, want 1.2", xres_major, xres_minor);
exit(1);
}
if (!register_cm(ctx))
{
exit(1);
}
register_systray(ctx);
/* get atoms */
ctx->atoms.steamAtom = XInternAtom(ctx->dpy, STEAM_PROP, false);
ctx->atoms.steamInputFocusAtom = XInternAtom(ctx->dpy, "STEAM_INPUT_FOCUS", false);
ctx->atoms.steamTouchClickModeAtom = XInternAtom(ctx->dpy, "STEAM_TOUCH_CLICK_MODE", false);
ctx->atoms.gameAtom = XInternAtom(ctx->dpy, GAME_PROP, false);
ctx->atoms.overlayAtom = XInternAtom(ctx->dpy, OVERLAY_PROP, false);
ctx->atoms.externalOverlayAtom = XInternAtom(ctx->dpy, EXTERNAL_OVERLAY_PROP, false);
ctx->atoms.opacityAtom = XInternAtom(ctx->dpy, OPACITY_PROP, false);
ctx->atoms.gamesRunningAtom = XInternAtom(ctx->dpy, GAMES_RUNNING_PROP, false);
ctx->atoms.screenScaleAtom = XInternAtom(ctx->dpy, SCREEN_SCALE_PROP, false);
ctx->atoms.screenZoomAtom = XInternAtom(ctx->dpy, SCREEN_MAGNIFICATION_PROP, false);
ctx->atoms.winTypeAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE", false);
ctx->atoms.winDesktopAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_DESKTOP", false);
ctx->atoms.winDockAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_DOCK", false);
ctx->atoms.winToolbarAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_TOOLBAR", false);
ctx->atoms.winMenuAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_MENU", false);
ctx->atoms.winUtilAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_UTILITY", false);
ctx->atoms.winSplashAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_SPLASH", false);
ctx->atoms.winDialogAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_DIALOG", false);
ctx->atoms.winNormalAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_NORMAL", false);
ctx->atoms.sizeHintsAtom = XInternAtom(ctx->dpy, "WM_NORMAL_HINTS", false);
ctx->atoms.netWMStateFullscreenAtom = XInternAtom(ctx->dpy, "_NET_WM_STATE_FULLSCREEN", false);
ctx->atoms.activeWindowAtom = XInternAtom(ctx->dpy, "_NET_ACTIVE_WINDOW", false);
ctx->atoms.netWMStateAtom = XInternAtom(ctx->dpy, "_NET_WM_STATE", false);
ctx->atoms.WMTransientForAtom = XInternAtom(ctx->dpy, "WM_TRANSIENT_FOR", false);
ctx->atoms.netWMStateHiddenAtom = XInternAtom(ctx->dpy, "_NET_WM_STATE_HIDDEN", false);
ctx->atoms.netWMStateFocusedAtom = XInternAtom(ctx->dpy, "_NET_WM_STATE_FOCUSED", false);
ctx->atoms.netWMStateSkipTaskbarAtom = XInternAtom(ctx->dpy, "_NET_WM_STATE_SKIP_TASKBAR", false);
ctx->atoms.netWMStateSkipPagerAtom = XInternAtom(ctx->dpy, "_NET_WM_STATE_SKIP_PAGER", false);
ctx->atoms.WLSurfaceIDAtom = XInternAtom(ctx->dpy, "WL_SURFACE_ID", false);
ctx->atoms.WMStateAtom = XInternAtom(ctx->dpy, "WM_STATE", false);
ctx->atoms.utf8StringAtom = XInternAtom(ctx->dpy, "UTF8_STRING", false);
ctx->atoms.netWMNameAtom = XInternAtom(ctx->dpy, "_NET_WM_NAME", false);
ctx->atoms.netWMIcon = XInternAtom(ctx->dpy, "_NET_WM_ICON", false);
ctx->atoms.motifWMHints = XInternAtom(ctx->dpy, "_MOTIF_WM_HINTS", false);
ctx->atoms.netSystemTrayOpcodeAtom = XInternAtom(ctx->dpy, "_NET_SYSTEM_TRAY_OPCODE", false);
ctx->atoms.steamStreamingClientAtom = XInternAtom(ctx->dpy, "STEAM_STREAMING_CLIENT", false);
ctx->atoms.steamStreamingClientVideoAtom = XInternAtom(ctx->dpy, "STEAM_STREAMING_CLIENT_VIDEO", false);
ctx->atoms.gamescopeFocusableAppsAtom = XInternAtom(ctx->dpy, "GAMESCOPE_FOCUSABLE_APPS", false);
ctx->atoms.gamescopeFocusableWindowsAtom = XInternAtom(ctx->dpy, "GAMESCOPE_FOCUSABLE_WINDOWS", false);
ctx->atoms.gamescopeFocusedAppAtom = XInternAtom( ctx->dpy, "GAMESCOPE_FOCUSED_APP", false );
ctx->atoms.gamescopeFocusedAppGfxAtom = XInternAtom( ctx->dpy, "GAMESCOPE_FOCUSED_APP_GFX", false );
ctx->atoms.gamescopeFocusedWindowAtom = XInternAtom( ctx->dpy, "GAMESCOPE_FOCUSED_WINDOW", false );
ctx->atoms.gamescopeCtrlAppIDAtom = XInternAtom(ctx->dpy, "GAMESCOPECTRL_BASELAYER_APPID", false);
ctx->atoms.gamescopeCtrlWindowAtom = XInternAtom(ctx->dpy, "GAMESCOPECTRL_BASELAYER_WINDOW", false);
ctx->atoms.WMChangeStateAtom = XInternAtom(ctx->dpy, "WM_CHANGE_STATE", false);
ctx->atoms.gamescopeInputCounterAtom = XInternAtom(ctx->dpy, "GAMESCOPE_INPUT_COUNTER", false);
ctx->atoms.gamescopeScreenShotAtom = XInternAtom( ctx->dpy, "GAMESCOPECTRL_REQUEST_SCREENSHOT", false );
ctx->atoms.gamescopeDebugScreenShotAtom = XInternAtom( ctx->dpy, "GAMESCOPECTRL_DEBUG_REQUEST_SCREENSHOT", false );
ctx->atoms.gamescopeFocusDisplay = XInternAtom(ctx->dpy, "GAMESCOPE_FOCUS_DISPLAY", false);
ctx->atoms.gamescopeMouseFocusDisplay = XInternAtom(ctx->dpy, "GAMESCOPE_MOUSE_FOCUS_DISPLAY", false);
ctx->atoms.gamescopeKeyboardFocusDisplay = XInternAtom( ctx->dpy, "GAMESCOPE_KEYBOARD_FOCUS_DISPLAY", false );
// In nanoseconds...
ctx->atoms.gamescopeTuneableVBlankRedZone = XInternAtom( ctx->dpy, "GAMESCOPE_TUNEABLE_VBLANK_REDZONE", false );
ctx->atoms.gamescopeTuneableRateOfDecay = XInternAtom( ctx->dpy, "GAMESCOPE_TUNEABLE_VBLANK_RATE_OF_DECAY_PERCENTAGE", false );
ctx->atoms.gamescopeScalingFilter = XInternAtom( ctx->dpy, "GAMESCOPE_SCALING_FILTER", false );
ctx->atoms.gamescopeFSRSharpness = XInternAtom( ctx->dpy, "GAMESCOPE_FSR_SHARPNESS", false );
ctx->atoms.gamescopeSharpness = XInternAtom( ctx->dpy, "GAMESCOPE_SHARPNESS", false );
ctx->atoms.gamescopeXWaylandModeControl = XInternAtom( ctx->dpy, "GAMESCOPE_XWAYLAND_MODE_CONTROL", false );
ctx->atoms.gamescopeFPSLimit = XInternAtom( ctx->dpy, "GAMESCOPE_FPS_LIMIT", false );
ctx->atoms.gamescopeDynamicRefresh[DRM_SCREEN_TYPE_INTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_DYNAMIC_REFRESH", false );
ctx->atoms.gamescopeDynamicRefresh[DRM_SCREEN_TYPE_EXTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_DYNAMIC_REFRESH_EXTERNAL", false );
ctx->atoms.gamescopeLowLatency = XInternAtom( ctx->dpy, "GAMESCOPE_LOW_LATENCY", false );
ctx->atoms.gamescopeFSRFeedback = XInternAtom( ctx->dpy, "GAMESCOPE_FSR_FEEDBACK", false );
ctx->atoms.gamescopeBlurMode = XInternAtom( ctx->dpy, "GAMESCOPE_BLUR_MODE", false );
ctx->atoms.gamescopeBlurRadius = XInternAtom( ctx->dpy, "GAMESCOPE_BLUR_RADIUS", false );
ctx->atoms.gamescopeBlurFadeDuration = XInternAtom( ctx->dpy, "GAMESCOPE_BLUR_FADE_DURATION", false );
ctx->atoms.gamescopeCompositeForce = XInternAtom( ctx->dpy, "GAMESCOPE_COMPOSITE_FORCE", false );
ctx->atoms.gamescopeCompositeDebug = XInternAtom( ctx->dpy, "GAMESCOPE_COMPOSITE_DEBUG", false );
ctx->atoms.gamescopeAllowTearing = XInternAtom( ctx->dpy, "GAMESCOPE_ALLOW_TEARING", false );
ctx->atoms.gamescopeDisplayForceInternal = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_FORCE_INTERNAL", false );
ctx->atoms.gamescopeDisplayModeNudge = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_MODE_NUDGE", false );
ctx->atoms.gamescopeDisplayIsExternal = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_IS_EXTERNAL", false );
ctx->atoms.gamescopeDisplayModeListExternal = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_MODE_LIST_EXTERNAL", false );
ctx->atoms.gamescopeCursorVisibleFeedback = XInternAtom( ctx->dpy, "GAMESCOPE_CURSOR_VISIBLE_FEEDBACK", false );
ctx->atoms.gamescopeSteamMaxHeight = XInternAtom( ctx->dpy, "GAMESCOPE_STEAM_MAX_HEIGHT", false );
ctx->atoms.gamescopeVRREnabled = XInternAtom( ctx->dpy, "GAMESCOPE_VRR_ENABLED", false );
ctx->atoms.gamescopeVRRCapable = XInternAtom( ctx->dpy, "GAMESCOPE_VRR_CAPABLE", false );
ctx->atoms.gamescopeVRRInUse = XInternAtom( ctx->dpy, "GAMESCOPE_VRR_FEEDBACK", false );
ctx->atoms.gamescopeNewScalingFilter = XInternAtom( ctx->dpy, "GAMESCOPE_NEW_SCALING_FILTER", false );
ctx->atoms.gamescopeNewScalingScaler = XInternAtom( ctx->dpy, "GAMESCOPE_NEW_SCALING_SCALER", false );
ctx->atoms.gamescopeDisplayEdidPath = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_EDID_PATH", false );
ctx->atoms.gamescopeXwaylandServerId = XInternAtom( ctx->dpy, "GAMESCOPE_XWAYLAND_SERVER_ID", false );
ctx->atoms.gamescopeDisplaySupportsHDR = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_SUPPORTS_HDR", false );
ctx->atoms.gamescopeDisplayHDREnabled = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_HDR_ENABLED", false );
ctx->atoms.gamescopeDebugForceHDR10Output = XInternAtom( ctx->dpy, "GAMESCOPE_DEBUG_FORCE_HDR10_PQ_OUTPUT", false );
ctx->atoms.gamescopeDebugForceHDRSupport = XInternAtom( ctx->dpy, "GAMESCOPE_DEBUG_FORCE_HDR_SUPPORT", false );
ctx->atoms.gamescopeDebugHDRHeatmap = XInternAtom( ctx->dpy, "GAMESCOPE_DEBUG_HDR_HEATMAP", false );
ctx->atoms.gamescopeHDROutputFeedback = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_OUTPUT_FEEDBACK", false );
ctx->atoms.gamescopeSDROnHDRContentBrightness = XInternAtom( ctx->dpy, "GAMESCOPE_SDR_ON_HDR_CONTENT_BRIGHTNESS", false );
ctx->atoms.gamescopeInternalDisplayBrightness = XInternAtom( ctx->dpy, "GAMESCOPE_INTERNAL_DISPLAY_BRIGHTNESS", false );
ctx->atoms.gamescopeHDRInputGain = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_INPUT_GAIN", false );
ctx->atoms.gamescopeSDRInputGain = XInternAtom( ctx->dpy, "GAMESCOPE_SDR_INPUT_GAIN", false );
ctx->atoms.gamescopeHDRItmEnable = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_ITM_ENABLE", false );
ctx->atoms.gamescopeHDRItmSDRNits = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_ITM_SDR_NITS", false );
ctx->atoms.gamescopeHDRItmTargetNits = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_ITM_TARGET_NITS", false );
ctx->atoms.gamescopeColorLookPQ = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_LOOK_PQ", false );
ctx->atoms.gamescopeColorLookG22 = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_LOOK_G22", false );
ctx->atoms.gamescopeColorOutputVirtualWhite = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_VIRTUAL_WHITE", false );
ctx->atoms.gamescopeHDRTonemapDisplayMetadata = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_TONEMAP_DISPLAY_METADATA", false );
ctx->atoms.gamescopeHDRTonemapSourceMetadata = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_TONEMAP_SOURCE_METADATA", false );
ctx->atoms.gamescopeHDRTonemapOperator = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_TONEMAP_OPERATOR", false );
ctx->atoms.gamescopeForceWindowsFullscreen = XInternAtom( ctx->dpy, "GAMESCOPE_FORCE_WINDOWS_FULLSCREEN", false );
ctx->atoms.gamescopeColorLut3DOverride = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_3DLUT_OVERRIDE", false );
ctx->atoms.gamescopeColorShaperLutOverride = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_SHAPERLUT_OVERRIDE", false );
ctx->atoms.gamescopeColorSDRGamutWideness = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_SDR_GAMUT_WIDENESS", false );
ctx->atoms.gamescopeColorNightMode = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_NIGHT_MODE", false );
ctx->atoms.gamescopeColorManagementDisable = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MANAGEMENT_DISABLE", false );
ctx->atoms.gamescopeColorAppWantsHDRFeedback = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_APP_WANTS_HDR_FEEDBACK", false );
ctx->atoms.gamescopeColorAppHDRMetadataFeedback = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_APP_HDR_METADATA_FEEDBACK", false );
ctx->atoms.gamescopeColorSliderInUse = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MANAGEMENT_CHANGING_HINT", false );
ctx->atoms.gamescopeColorChromaticAdaptationMode = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_CHROMATIC_ADAPTATION_MODE", false );
ctx->atoms.gamescopeColorMuraCorrectionImage[DRM_SCREEN_TYPE_INTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MURA_CORRECTION_IMAGE", false );
ctx->atoms.gamescopeColorMuraCorrectionImage[DRM_SCREEN_TYPE_EXTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MURA_CORRECTION_IMAGE_EXTERNAL", false );
ctx->atoms.gamescopeColorMuraScale[DRM_SCREEN_TYPE_INTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MURA_SCALE", false );
ctx->atoms.gamescopeColorMuraScale[DRM_SCREEN_TYPE_EXTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MURA_SCALE_EXTERNAL", false );
ctx->atoms.gamescopeColorMuraCorrectionDisabled[DRM_SCREEN_TYPE_INTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MURA_CORRECTION_DISABLED", false );
ctx->atoms.gamescopeColorMuraCorrectionDisabled[DRM_SCREEN_TYPE_EXTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MURA_CORRECTION_DISABLED_EXTERNAL", false );
ctx->atoms.gamescopeCreateXWaylandServer = XInternAtom( ctx->dpy, "GAMESCOPE_CREATE_XWAYLAND_SERVER", false );
ctx->atoms.gamescopeCreateXWaylandServerFeedback = XInternAtom( ctx->dpy, "GAMESCOPE_CREATE_XWAYLAND_SERVER_FEEDBACK", false );
ctx->atoms.gamescopeDestroyXWaylandServer = XInternAtom( ctx->dpy, "GAMESCOPE_DESTROY_XWAYLAND_SERVER", false );
ctx->atoms.gamescopeReshadeEffect = XInternAtom( ctx->dpy, "GAMESCOPE_RESHADE_EFFECT", false );
ctx->atoms.gamescopeReshadeTechniqueIdx = XInternAtom( ctx->dpy, "GAMESCOPE_RESHADE_TECHNIQUE_IDX", false );
ctx->atoms.gamescopeDisplayRefreshRateFeedback = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_REFRESH_RATE_FEEDBACK", false );
ctx->atoms.gamescopeDisplayDynamicRefreshBasedOnGamePresence = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_DYNAMIC_REFRESH_BASED_ON_GAME_PRESENCE", false );
ctx->atoms.wineHwndStyle = XInternAtom( ctx->dpy, "_WINE_HWND_STYLE", false );
ctx->atoms.wineHwndStyleEx = XInternAtom( ctx->dpy, "_WINE_HWND_EXSTYLE", false );
ctx->atoms.clipboard = XInternAtom(ctx->dpy, "CLIPBOARD", false);
ctx->atoms.primarySelection = XInternAtom(ctx->dpy, "PRIMARY", false);
ctx->atoms.targets = XInternAtom(ctx->dpy, "TARGETS", false);
ctx->root_width = DisplayWidth(ctx->dpy, ctx->scr);
ctx->root_height = DisplayHeight(ctx->dpy, ctx->scr);
ctx->allDamage = None;
ctx->clipChanged = true;
XChangeProperty(ctx->dpy, ctx->root, ctx->atoms.gamescopeXwaylandServerId, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&serverId, 1 );
XGrabServer(ctx->dpy);
XCompositeRedirectSubwindows(ctx->dpy, ctx->root, CompositeRedirectManual);
Window root_return, parent_return;
Window *children;
unsigned int nchildren;
XSelectInput(ctx->dpy, ctx->root,
SubstructureNotifyMask|
ExposureMask|
StructureNotifyMask|
SubstructureRedirectMask|
FocusChangeMask|
PointerMotionMask|
LeaveWindowMask|
PropertyChangeMask);
XShapeSelectInput(ctx->dpy, ctx->root, ShapeNotifyMask);
XFixesSelectCursorInput(ctx->dpy, ctx->root, XFixesDisplayCursorNotifyMask);
XFixesSelectSelectionInput(ctx->dpy, ctx->root, ctx->atoms.clipboard, XFixesSetSelectionOwnerNotifyMask);
XFixesSelectSelectionInput(ctx->dpy, ctx->root, ctx->atoms.primarySelection, XFixesSetSelectionOwnerNotifyMask);
XQueryTree(ctx->dpy, ctx->root, &root_return, &parent_return, &children, &nchildren);
for (uint32_t i = 0; i < nchildren; i++)
add_win(ctx, children[i], i ? children[i-1] : None, 0);
XFree(children);
XUngrabServer(ctx->dpy);
XF86VidModeLockModeSwitch(ctx->dpy, ctx->scr, true);
ctx->cursor = std::make_unique<MouseCursor>(ctx);
if (g_customCursorPath)
{
if (!load_mouse_cursor(ctx->cursor.get(), g_customCursorPath, g_customCursorHotspotX, g_customCursorHotspotY))
xwm_log.errorf("Failed to load mouse cursor: %s", g_customCursorPath);
}
else
{
if ( BIsNested() )
{
if ( !load_host_cursor( ctx->cursor.get() ) )
{
xwm_log.errorf("Failed to load host cursor. Falling back to left_ptr.");
if (!ctx->cursor->setCursorImageByName("left_ptr"))
xwm_log.errorf("Failed to load mouse cursor: left_ptr");
}
}
else
{
xwm_log.infof("Embedded, no cursor set. Using left_ptr by default.");
if (!ctx->cursor->setCursorImageByName("left_ptr"))
xwm_log.errorf("Failed to load mouse cursor: left_ptr");
}
}
XFlush(ctx->dpy);
}
void update_vrr_atoms(xwayland_ctx_t *root_ctx, bool force, bool* needs_flush = nullptr)
{
bool capable = drm_get_vrr_capable( &g_DRM );
if ( capable != g_bVRRCapable_CachedValue || force )
{
uint32_t capable_value = capable ? 1 : 0;
XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeVRRCapable, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&capable_value, 1 );
g_bVRRCapable_CachedValue = capable;
if (needs_flush)
*needs_flush = true;
}
bool st2084 = BIsNested() ? vulkan_supports_hdr10() : drm_supports_st2084( &g_DRM );
if ( st2084 != g_bSupportsST2084_CachedValue || force )
{
uint32_t hdr_value = st2084 ? 1 : 0;
XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeDisplaySupportsHDR, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&hdr_value, 1 );
g_bSupportsST2084_CachedValue = st2084;
if (needs_flush)
*needs_flush = true;
}
bool in_use = drm_get_vrr_in_use( &g_DRM );
if ( in_use != g_bVRRInUse_CachedValue || force )
{
uint32_t in_use_value = in_use ? 1 : 0;
XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeVRRInUse, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&in_use_value, 1 );
g_bVRRInUse_CachedValue = in_use;
if (needs_flush)
*needs_flush = true;
}
if ( g_nOutputRefresh != g_nCurrentRefreshRate_CachedValue || force )
{
XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeDisplayRefreshRateFeedback, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&g_nOutputRefresh, 1 );
g_nCurrentRefreshRate_CachedValue = g_nOutputRefresh;
if (needs_flush)
*needs_flush = true;
}
// Don't update this in-sync with DRM vrr usage.
// Keep this as a preference, starting with off.
if ( force )
{
bool wants_vrr = g_DRM.wants_vrr_enabled;
uint32_t enabled_value = wants_vrr ? 1 : 0;
XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeVRREnabled, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&enabled_value, 1 );
if (needs_flush)
*needs_flush = true;
}
}
void update_mode_atoms(xwayland_ctx_t *root_ctx, bool* needs_flush = nullptr)
{
if (needs_flush)
*needs_flush = true;
if ( drm_get_screen_type(&g_DRM) == DRM_SCREEN_TYPE_INTERNAL )
{
XDeleteProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeDisplayModeListExternal);
uint32_t zero = 0;
XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeDisplayIsExternal, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&zero, 1 );
return;
}
char modes[4096] = "";
int remaining_size = sizeof(modes) - 1;
int len = 0;
for (int i = 0; remaining_size > 0 && i < g_DRM.connector->connector->count_modes; i++)
{
const auto& mode = g_DRM.connector->connector->modes[i];
int mode_len = snprintf(&modes[len], remaining_size, "%s%dx%d@%d",
i == 0 ? "" : " ",
int(mode.hdisplay), int(mode.vdisplay), int(mode.vrefresh));
len += mode_len;
remaining_size -= mode_len;
}
XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeDisplayModeListExternal, XA_STRING, 8, PropModeReplace,
(unsigned char *)modes, strlen(modes) + 1 );
uint32_t one = 1;
XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeDisplayIsExternal, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&one, 1 );
}
extern int g_nPreferredOutputWidth;
extern int g_nPreferredOutputHeight;
static bool g_bWasFSRActive = false;
extern std::atomic<uint64_t> g_nCompletedPageFlipCount;
void steamcompmgr_check_xdg(bool vblank)
{
if (wlserver_xdg_dirty())
{
g_steamcompmgr_xdg_wins = wlserver_get_xdg_shell_windows();
focusDirty = true;
}
handle_done_commits_xdg();
// When we have observed both a complete commit and a VBlank, we should request a new frame.
if (vblank)
{
for ( const auto& xdg_win : g_steamcompmgr_xdg_wins )
{
steamcompmgr_flush_frame_done(xdg_win.get());
}
handle_presented_xdg();
}
check_new_xdg_res();
}
void update_edid_prop()
{
if ( !BIsNested() )
{
const char *filename = drm_get_patched_edid_path();
if (!filename)
return;
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
{
XTextProperty text_property =
{
.value = (unsigned char *)filename,
.encoding = server->ctx->atoms.utf8StringAtom,
.format = 8,
.nitems = strlen(filename),
};
XSetTextProperty( server->ctx->dpy, server->ctx->root, &text_property, server->ctx->atoms.gamescopeDisplayEdidPath );
}
}
}
void
steamcompmgr_main(int argc, char **argv)
{
int readyPipeFD = -1;
// Reset getopt() state
optind = 1;
bSteamCompMgrGrab = BIsNested() && g_bForceRelativeMouse;
int o;
int opt_index = -1;
bool bForceWindowsFullscreen = false;
while ((o = getopt_long(argc, argv, gamescope_optstring, gamescope_options, &opt_index)) != -1)
{
const char *opt_name;
switch (o) {
case 'R':
readyPipeFD = open( optarg, O_WRONLY | O_CLOEXEC );
break;
case 'T':
statsThreadPath = optarg;
{
statsThreadRun = true;
std::thread statsThreads( statsThreadMain );
statsThreads.detach();
}
break;
case 'C':
cursorHideTime = atoi( optarg );
break;
case 'v':
drawDebugInfo = true;
break;
case 'e':
steamMode = true;
break;
case 'c':
alwaysComposite = true;
break;
case 'x':
useXRes = false;
break;
case 0: // long options without a short option
opt_name = gamescope_options[opt_index].name;
if (strcmp(opt_name, "debug-focus") == 0) {
debugFocus = true;
} else if (strcmp(opt_name, "synchronous-x11") == 0) {
synchronize = true;
} else if (strcmp(opt_name, "debug-events") == 0) {
debugEvents = true;
} else if (strcmp(opt_name, "cursor") == 0) {
g_customCursorPath = optarg;
} else if (strcmp(opt_name, "cursor-hotspot") == 0) {
sscanf(optarg, "%d,%d", &g_customCursorHotspotX, &g_customCursorHotspotY);
} else if (strcmp(opt_name, "fade-out-duration") == 0) {
g_FadeOutDuration = atoi(optarg);
} else if (strcmp(opt_name, "force-windows-fullscreen") == 0) {
bForceWindowsFullscreen = true;
} else if (strcmp(opt_name, "cursor-scale-height") == 0) {
g_nCursorScaleHeight = atoi(optarg);
} else if (strcmp(opt_name, "hdr-enabled") == 0) {
g_bHDREnabled = true;
} else if (strcmp(opt_name, "hdr-debug-force-support") == 0) {
g_bForceHDRSupportDebug = true;
} else if (strcmp(opt_name, "hdr-debug-force-output") == 0) {
g_bForceHDR10OutputDebug = true;
} else if (strcmp(opt_name, "hdr-itm-enable") == 0) {
g_bHDRItmEnable = true;
} else if (strcmp(opt_name, "sdr-gamut-wideness") == 0) {
g_ColorMgmt.pending.sdrGamutWideness = atof(optarg);
} else if (strcmp(opt_name, "hdr-sdr-content-nits") == 0) {
g_ColorMgmt.pending.flSDROnHDRBrightness = atof(optarg);
} else if (strcmp(opt_name, "hdr-itm-sdr-nits") == 0) {
g_flHDRItmSdrNits = atof(optarg);
} else if (strcmp(opt_name, "hdr-itm-target-nits") == 0) {
g_flHDRItmTargetNits = atof(optarg);
} else if (strcmp(opt_name, "framerate-limit") == 0) {
g_nSteamCompMgrTargetFPS = atoi(optarg);
} else if (strcmp(opt_name, "reshade-effect") == 0) {
g_reshade_effect = optarg;
} else if (strcmp(opt_name, "reshade-technique-idx") == 0) {
g_reshade_technique_idx = atoi(optarg);
} else if (strcmp(opt_name, "mura-map") == 0) {
set_mura_overlay(optarg);
}
break;
case '?':
assert(false); // unreachable
}
}
int subCommandArg = -1;
if ( optind < argc )
{
subCommandArg = optind;
}
if ( pipe2( g_nudgePipe, O_CLOEXEC | O_NONBLOCK ) != 0 )
{
xwm_log.errorf_errno( "steamcompmgr: pipe2 failed" );
exit( 1 );
}
const char *pchEnableVkBasalt = getenv( "ENABLE_VKBASALT" );
if ( pchEnableVkBasalt != nullptr && pchEnableVkBasalt[0] == '1' )
{
alwaysComposite = true;
}
// Enable color mgmt by default.
g_ColorMgmt.pending.enabled = true;
currentOutputWidth = g_nPreferredOutputWidth;
currentOutputHeight = g_nPreferredOutputHeight;
init_runtime_info();
#if HAVE_OPENVR
if ( BIsVRSession() )
vrsession_steam_mode( steamMode );
#endif
int vblankFD = vblank_init();
assert( vblankFD >= 0 );
std::unique_lock<std::mutex> xwayland_server_guard(g_SteamCompMgrXWaylandServerMutex);
// Initialize any xwayland ctxs we have
{
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
init_xwayland_ctx(i, server);
}
gamescope_xwayland_server_t *root_server = wlserver_get_xwayland_server(0);
xwayland_ctx_t *root_ctx = root_server->ctx.get();
gamesRunningCount = get_prop(root_ctx, root_ctx->root, root_ctx->atoms.gamesRunningAtom, 0);
overscanScaleRatio = get_prop(root_ctx, root_ctx->root, root_ctx->atoms.screenScaleAtom, 0xFFFFFFFF) / (double)0xFFFFFFFF;
zoomScaleRatio = get_prop(root_ctx, root_ctx->root, root_ctx->atoms.screenZoomAtom, 0xFFFF) / (double)0xFFFF;
globalScaleRatio = overscanScaleRatio * zoomScaleRatio;
determine_and_apply_focus();
if ( readyPipeFD != -1 )
{
dprintf( readyPipeFD, "%s %s\n", root_ctx->xwayland_server->get_nested_display_name(), wlserver_get_wl_display_name() );
close( readyPipeFD );
readyPipeFD = -1;
}
if ( subCommandArg >= 0 )
{
spawn_client( &argv[ subCommandArg ] );
}
// EVENT_VBLANK
pollfds.push_back(pollfd {
.fd = vblankFD,
.events = POLLIN,
});
// EVENT_NUDGE
pollfds.push_back(pollfd {
.fd = g_nudgePipe[ 0 ],
.events = POLLIN,
});
// EVENT_X11
{
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
{
pollfds.push_back(pollfd {
.fd = XConnectionNumber( server->ctx->dpy ),
.events = POLLIN,
});
server->ctx->force_windows_fullscreen = bForceWindowsFullscreen;
}
}
update_vrr_atoms(root_ctx, true);
update_mode_atoms(root_ctx);
XFlush(root_ctx->dpy);
if ( !BIsNested() )
{
drm_update_patched_edid(&g_DRM);
update_edid_prop();
}
update_screenshot_color_mgmt();
// Transpose to get this 3x3 matrix into the right state for applying as a 3x4
// on DRM + the Vulkan side.
// ie. color.rgb = color.rgba * u_ctm[offsetLayerIdx];
s_scRGB709To2020Matrix = drm_create_ctm(&g_DRM, glm::mat3x4(glm::transpose(k_2020_from_709)));
for (;;)
{
bool vblank = false;
{
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
{
assert(server);
if (x_events_queued(server->ctx.get()))
dispatch_x11(server->ctx.get());
}
}
if ( poll( pollfds.data(), pollfds.size(), -1 ) < 0)
{
if ( errno == EAGAIN )
continue;
xwm_log.errorf_errno( "poll failed" );
break;
}
for (size_t i = EVENT_X11; i < pollfds.size(); i++)
{
if ( pollfds[ i ].revents & POLLHUP )
{
xwm_log.errorf( "Lost connection to the X11 server %zd", i - EVENT_X11 );
break;
}
}
assert( !( pollfds[ EVENT_VBLANK ].revents & POLLHUP ) );
assert( !( pollfds[ EVENT_NUDGE ].revents & POLLHUP ) );
for (size_t i = EVENT_X11; i < pollfds.size(); i++)
{
if ( pollfds[ i ].revents & POLLIN )
{
gamescope_xwayland_server_t *server = wlserver_get_xwayland_server(i - EVENT_X11);
assert(server);
dispatch_x11( server->ctx.get() );
}
}
if ( pollfds[ EVENT_VBLANK ].revents & POLLIN )
vblank = dispatch_vblank( vblankFD );
if ( pollfds[ EVENT_NUDGE ].revents & POLLIN )
dispatch_nudge( g_nudgePipe[ 0 ] );
if ( g_bRun == false )
{
break;
}
bool flush_root = false;
if ( inputCounter != lastPublishedInputCounter )
{
XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeInputCounterAtom, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&inputCounter, 1 );
lastPublishedInputCounter = inputCounter;
flush_root = true;
}
if ( g_bFSRActive != g_bWasFSRActive )
{
uint32_t active = g_bFSRActive ? 1 : 0;
XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeFSRFeedback, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&active, 1 );
g_bWasFSRActive = g_bFSRActive;
flush_root = true;
}
if (focusDirty)
determine_and_apply_focus();
// If our DRM state is out-of-date, refresh it. This might update
// the output size.
if ( BIsNested() == false )
{
if ( drm_poll_state( &g_DRM ) )
{
hasRepaint = true;
update_mode_atoms(root_ctx, &flush_root);
}
}
g_bOutputHDREnabled = (g_bSupportsST2084_CachedValue || g_bForceHDR10OutputDebug) && g_bHDREnabled;
// Pick our width/height for this potential frame, regardless of how it might change later
// At some point we might even add proper locking so we get real updates atomically instead
// of whatever jumble of races the below might cause over a couple of frames
if ( currentOutputWidth != g_nOutputWidth ||
currentOutputHeight != g_nOutputHeight ||
currentHDROutput != g_bOutputHDREnabled ||
currentHDRForce != g_bForceHDRSupportDebug )
{
if ( steamMode && g_nXWaylandCount > 1 )
{
g_nNestedHeight = ( g_nNestedWidth * g_nOutputHeight ) / g_nOutputWidth;
wlserver_lock();
// Update only Steam, the root ctx, with the new output size for now
wlserver_set_xwayland_server_mode( 0, g_nOutputWidth, g_nOutputHeight, g_nOutputRefresh );
wlserver_unlock();
}
if ( BIsSDLSession() )
{
vulkan_remake_swapchain();
while ( !acquire_next_image() )
vulkan_remake_swapchain();
}
else
{
if ( !BIsNested() )
{
if (g_bOutputHDREnabled != currentHDROutput)
{
drm_set_hdr_state(&g_DRM, g_bOutputHDREnabled);
}
}
vulkan_remake_output_images();
}
{
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
{
uint32_t hdr_value = ( g_bOutputHDREnabled || g_bForceHDRSupportDebug ) ? 1 : 0;
XChangeProperty(server->ctx->dpy, server->ctx->root, server->ctx->atoms.gamescopeHDROutputFeedback, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&hdr_value, 1 );
if (server->ctx.get() == root_ctx)
{
flush_root = true;
}
else
{
XFlush(server->ctx->dpy);
}
}
}
currentOutputWidth = g_nOutputWidth;
currentOutputHeight = g_nOutputHeight;
currentHDROutput = g_bOutputHDREnabled;
currentHDRForce = g_bForceHDRSupportDebug;
#if HAVE_PIPEWIRE
nudge_pipewire();
#endif
}
// Ask for a new surface every vblank
// When we observe a new commit being complete for a surface, we ask for a new frame.
// This ensures that FIFO works properly, since otherwise we might ask for a new frame
// application can commit a new frame that completes before we ever displayed
// the current pending commit.
if ( vblank == true )
{
static int vblank_idx = 0;
{
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
{
for (steamcompmgr_win_t *w = server->ctx->list; w; w = w->xwayland().next)
{
steamcompmgr_latch_frame_done( w, vblank_idx );
}
}
for ( const auto& xdg_win : g_steamcompmgr_xdg_wins )
{
steamcompmgr_latch_frame_done( xdg_win.get(), vblank_idx );
}
}
vblank_idx++;
}
{
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
{
handle_done_commits_xwayland(server->ctx.get());
// When we have observed both a complete commit and a VBlank, we should request a new frame.
if (vblank)
{
for (steamcompmgr_win_t *w = server->ctx->list; w; w = w->xwayland().next)
{
steamcompmgr_flush_frame_done(w);
}
}
}
}
if ( vblank )
{
int nRealRefresh = g_nNestedRefresh ? g_nNestedRefresh : g_nOutputRefresh;
int nTargetFPS = g_nSteamCompMgrTargetFPS ? g_nSteamCompMgrTargetFPS : nRealRefresh;
nTargetFPS = std::min<int>( nTargetFPS, 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:
//
// We send the presented event just after the latest latch time possible so PresentWait in Vulkan
// still returns pretty optimally. The extra 2ms or so can be "display latency"
// We still provide the predicted TTL refresh time in the presented event though.
//
// We ignore or lie most of the flags because they aren't particularly useful for a client
// to know anyway and it would delay us sending this at an optimal time.
// (particularly for DXGI frame latency handles under Proton.)
//
// The boat is still out as to whether we should do latest latch or pageflip/ttl for the event.
// For now, going to keep this, and if we change our minds later, it's no big deal.
//
// It's a little strange, but we return `presented` for any window not visible
// and `presented` for anything visible. It's a little disingenuous because we didn't
// actually show a window if it wasn't visible, but we could! And that is the first
// opportunity it had. It's confusing but we need this for forward progress.
if ( vblank )
{
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
handle_presented_xwayland( server->ctx.get() );
}
//
{
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
check_new_xwayland_res(server->ctx.get());
}
{
GamescopeAppTextureColorspace current_app_colorspace = GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB;
std::shared_ptr<wlserver_hdr_metadata> app_hdr_metadata = nullptr;
if ( g_HeldCommits[HELD_COMMIT_BASE] )
{
current_app_colorspace = g_HeldCommits[HELD_COMMIT_BASE]->colorspace();
if (g_HeldCommits[HELD_COMMIT_BASE]->feedback)
app_hdr_metadata = g_HeldCommits[HELD_COMMIT_BASE]->feedback->hdr_metadata_blob;
}
bool app_wants_hdr = ColorspaceIsHDR( current_app_colorspace );
static bool s_bAppWantsHDRCached = false;
if ( app_wants_hdr != s_bAppWantsHDRCached )
{
uint32_t app_wants_hdr_prop = app_wants_hdr ? 1 : 0;
XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeColorAppWantsHDRFeedback, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&app_wants_hdr_prop, 1 );
s_bAppWantsHDRCached = app_wants_hdr;
flush_root = true;
}
if ( app_hdr_metadata != g_ColorMgmt.pending.appHDRMetadata )
{
if ( app_hdr_metadata )
{
std::vector<uint32_t> app_hdr_metadata_blob;
app_hdr_metadata_blob.resize((sizeof(hdr_metadata_infoframe) + (sizeof(uint32_t) - 1)) / sizeof(uint32_t));
memset(app_hdr_metadata_blob.data(), 0, sizeof(uint32_t) * app_hdr_metadata_blob.size());
memcpy(app_hdr_metadata_blob.data(), &app_hdr_metadata->metadata, sizeof(hdr_metadata_infoframe));
XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeColorAppHDRMetadataFeedback, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)app_hdr_metadata_blob.data(), (int)app_hdr_metadata_blob.size() );
}
else
{
XDeleteProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeColorAppHDRMetadataFeedback);
}
g_ColorMgmt.pending.appHDRMetadata = app_hdr_metadata;
flush_root = true;
}
}
steamcompmgr_check_xdg(vblank);
// Handles if we got a commit for the window we want to focus
// to switch to it for painting (outdatedInteractiveFocus)
// Doesn't realllly matter but avoids an extra frame of being on the wrong window.
if (focusDirty)
determine_and_apply_focus();
if ( window_is_steam( global_focus.focusWindow ) )
{
g_bSteamIsActiveWindow = true;
g_upscaleScaler = GamescopeUpscaleScaler::FIT;
g_upscaleFilter = GamescopeUpscaleFilter::LINEAR;
}
else
{
g_bSteamIsActiveWindow = false;
g_upscaleScaler = g_wantedUpscaleScaler;
g_upscaleFilter = g_wantedUpscaleFilter;
}
static int nIgnoredOverlayRepaints = 0;
const bool bVRR = drm_get_vrr_in_use( &g_DRM );
// HACK: Disable tearing if we have an overlay to avoid stutters right now
// TODO: Fix properly.
static bool bHasOverlay = ( global_focus.overlayWindow && global_focus.overlayWindow->opacity ) ||
( global_focus.externalOverlayWindow && global_focus.externalOverlayWindow->opacity ) ||
( global_focus.overrideWindow && global_focus.focusWindow && !global_focus.focusWindow->isSteamStreamingClient && global_focus.overrideWindow->opacity );
const bool bSteamOverlayOpen = global_focus.overlayWindow && global_focus.overlayWindow->opacity;
// If we are running behind, allow tearing.
const bool bSurfaceWantsAsync = (g_HeldCommits[HELD_COMMIT_BASE] && g_HeldCommits[HELD_COMMIT_BASE]->async);
const bool bForceRepaint = g_bForceRepaint.exchange(false);
const bool bForceSyncFlip = bForceRepaint || is_fading_out();
// If we are compositing, always force sync flips because we currently wait
// for composition to finish before submitting.
// If we want to do async + composite, we should set up syncfile stuff and have DRM wait on it.
const bool bNeedsSyncFlip = bForceSyncFlip || g_bCurrentlyCompositing || nIgnoredOverlayRepaints;
const bool bDoAsyncFlip = ( ((g_nAsyncFlipsEnabled >= 1) && g_bSupportsAsyncFlips && bSurfaceWantsAsync && !bHasOverlay) || bVRR ) && !bSteamOverlayOpen && !bNeedsSyncFlip;
bool bShouldPaint = false;
if ( bDoAsyncFlip )
{
if ( hasRepaint && !g_bCurrentlyCompositing )
bShouldPaint = true;
}
else
{
bShouldPaint = vblank && ( hasRepaint || hasRepaintNonBasePlane || bForceSyncFlip );
}
// If we have a pending page flip and doing VRR, lets not do another...
if ( bVRR && g_nCompletedPageFlipCount != g_DRM.flipcount )
bShouldPaint = false;
if ( !bShouldPaint && hasRepaintNonBasePlane && vblank )
nIgnoredOverlayRepaints++;
#if HAVE_OPENVR
if ( BIsVRSession() && !vrsession_visible() )
bShouldPaint = false;
#endif
if ( bShouldPaint )
{
paint_all( !vblank && !bVRR );
hasRepaint = false;
hasRepaintNonBasePlane = false;
nIgnoredOverlayRepaints = 0;
// If we're in the middle of a fade, pump an event into the loop to
// make sure we keep pushing frames even if the app isn't updating.
if ( is_fading_out() )
{
nudge_steamcompmgr();
}
}
update_vrr_atoms(root_ctx, false, &flush_root);
if (global_focus.cursor)
{
global_focus.cursor->updatePosition();
if (global_focus.cursor->needs_server_flush())
{
flush_root = true;
global_focus.cursor->inform_flush();
}
}
if (flush_root)
{
XFlush(root_ctx->dpy);
}
vulkan_garbage_collect();
vblank = false;
}
steamcompmgr_exit();
}
void steamcompmgr_send_frame_done_to_focus_window()
{
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if ( global_focus.focusWindow && global_focus.focusWindow->xwayland().surface.main_surface )
{
wlserver_lock();
wlserver_send_frame_done( global_focus.focusWindow->xwayland().surface.main_surface , &now );
wlserver_unlock();
}
}
gamescope_xwayland_server_t *steamcompmgr_get_focused_server()
{
if (global_focus.inputFocusWindow != nullptr)
{
gamescope_xwayland_server_t *server = NULL;
for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++)
{
if (server->ctx->focus.inputFocusWindow == global_focus.inputFocusWindow)
return server;
}
}
return wlserver_get_xwayland_server(0);
}
struct wlr_surface *steamcompmgr_get_server_input_surface( size_t idx )
{
gamescope_xwayland_server_t *server = wlserver_get_xwayland_server( idx );
if ( server && server->ctx && server->ctx->focus.inputFocusWindow && server->ctx->focus.inputFocusWindow->xwayland().surface.main_surface )
return server->ctx->focus.inputFocusWindow->xwayland().surface.main_surface;
return NULL;
}
struct wlserver_x11_surface_info *lookup_x11_surface_info_from_xid( gamescope_xwayland_server_t *xwayland_server, uint32_t xid )
{
if ( !xwayland_server )
return nullptr;
if ( !xwayland_server->ctx )
return nullptr;
// Lookup children too so we can get the window
// and go back to it's top-level parent.
// The xwayland bypass layer does this as we can have child windows
// that cover the whole parent.
steamcompmgr_win_t *w = find_win( xwayland_server->ctx.get(), xid, true );
if ( !w )
return nullptr;
return &w->xwayland().surface;
}
MouseCursor *steamcompmgr_get_current_cursor()
{
return global_focus.cursor;
}
MouseCursor *steamcompmgr_get_server_cursor(uint32_t idx)
{
gamescope_xwayland_server_t *server = wlserver_get_xwayland_server( idx );
if ( server && server->ctx )
return server->ctx->cursor.get();
return nullptr;
}