gamescope/src/steamcompmgr.cpp
2021-09-01 18:33:57 +02:00

3854 lines
90 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 <thread>
#include <condition_variable>
#include <mutex>
#include <atomic>
#include <vector>
#include <algorithm>
#include <iostream>
#include <fstream>
#include <string>
#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>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/resource.h>
#include <time.h>
#include <unistd.h>
#include <getopt.h>
#include <spawn.h>
#include <signal.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/extensions/Xcomposite.h>
#include <X11/extensions/Xdamage.h>
#include <X11/extensions/Xrender.h>
#include <X11/extensions/XRes.h>
#include <X11/extensions/shape.h>
#include <X11/extensions/xf86vmode.h>
#include "main.hpp"
#include "wlserver.hpp"
#include "drm.hpp"
#include "rendervulkan.hpp"
#include "steamcompmgr.hpp"
#include "vblankmanager.hpp"
#include "sdlwindow.hpp"
#if HAVE_PIPEWIRE
#include "pipewire.hpp"
#endif
#define STB_IMAGE_IMPLEMENTATION
#include "../subprojects/stb/stb_image.h"
#define GPUVIS_TRACE_IMPLEMENTATION
#include "gpuvis_trace_utils.h"
extern char **environ;
typedef struct _ignore {
struct _ignore *next;
unsigned long sequence;
} ignore;
uint64_t maxCommmitID;
struct commit_t
{
struct wlr_buffer *buf;
uint32_t fb_id;
VulkanTexture_t vulkanTex;
uint64_t commitID;
bool done;
};
std::mutex listCommitsDoneLock;
std::vector< uint64_t > listCommitsDone;
typedef struct _win {
struct _win *next;
Window id;
XWindowAttributes a;
int mode;
Damage damage;
unsigned int opacity;
unsigned long map_sequence;
unsigned long damage_sequence;
char *title;
bool utf8_title;
pid_t pid;
Bool isSteam;
Bool isSteamStreamingClient;
Bool isSteamStreamingClientVideo;
uint32_t inputFocusMode;
uint32_t appID;
Bool isOverlay;
Bool isFullscreen;
Bool isSysTrayIcon;
Bool sizeHintsSpecified;
Bool skipTaskbar;
Bool skipPager;
unsigned int requestedWidth;
unsigned int requestedHeight;
Window transientFor;
Bool nudged;
Bool ignoreOverrideRedirect;
Bool mouseMoved;
struct wlserver_surface surface;
std::vector< commit_t > commit_queue;
} win;
static win *list;
static int scr;
static Window root;
static XserverRegion allDamage;
static Bool clipChanged;
static int root_height, root_width;
static ignore *ignore_head, **ignore_tail = &ignore_head;
static int xfixes_event, xfixes_error;
static int damage_event, damage_error;
static int composite_event, composite_error;
static int render_event, render_error;
static int xshape_event, xshape_error;
static Bool synchronize;
static int composite_opcode;
uint32_t currentOutputWidth, currentOutputHeight;
static Window currentFocusWindow;
static win* currentFocusWin;
static Window currentInputFocusWindow;
uint32_t currentInputFocusMode;
static Window currentKeyboardFocusWindow;
static Window currentOverlayWindow;
static Window currentNotificationWindow;
bool hasFocusWindow;
bool focusControlled;
std::vector< uint32_t > vecFocuscontrolAppIDs;
static Window ourWindow;
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;
Bool focusDirty = False;
bool hasRepaint = false;
unsigned long damageSequence = 0;
unsigned int cursorHideTime = 10'000;
Bool gotXError = False;
win fadeOutWindow;
Bool fadeOutWindowGone;
unsigned int fadeOutStartTime;
extern float g_flMaxWindowScale;
extern bool g_bIntegerScale;
#define FADE_OUT_DURATION 200
/* find these once and be done with it */
static Atom steamAtom;
static Atom gameAtom;
static Atom overlayAtom;
static Atom gamesRunningAtom;
static Atom screenZoomAtom;
static Atom screenScaleAtom;
static Atom opacityAtom;
static Atom winTypeAtom;
static Atom winDesktopAtom;
static Atom winDockAtom;
static Atom winToolbarAtom;
static Atom winMenuAtom;
static Atom winUtilAtom;
static Atom winSplashAtom;
static Atom winDialogAtom;
static Atom winNormalAtom;
static Atom sizeHintsAtom;
static Atom netWMStateFullscreenAtom;
static Atom activeWindowAtom;
static Atom netWMStateAtom;
static Atom WMTransientForAtom;
static Atom netWMStateHiddenAtom;
static Atom netWMStateFocusedAtom;
static Atom netWMStateSkipTaskbarAtom;
static Atom netWMStateSkipPagerAtom;
static Atom WLSurfaceIDAtom;
static Atom WMStateAtom;
static Atom steamInputFocusAtom;
static Atom WMChangeStateAtom;
static Atom steamTouchClickModeAtom;
static Atom utf8StringAtom;
static Atom netWMNameAtom;
static Atom netSystemTrayOpcodeAtom;
static Atom steamStreamingClientAtom;
static Atom steamStreamingClientVideoAtom;
static Atom gamescopeCtrlAppIDAtom;
/* 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 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 doRender = True;
static Bool debugFocus = False;
static Bool drawDebugInfo = False;
static Bool debugEvents = False;
static Bool steamMode = False;
static Bool alwaysComposite = False;
static Bool useXRes = True;
std::mutex wayland_commit_lock;
std::vector<ResListEntry_t> wayland_commit_queue;
static std::atomic< bool > g_bTakeScreenshot{false};
static int g_nudgePipe[2];
// 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;
};
sem waitListSem;
std::mutex waitListLock;
std::vector< std::pair< int, uint64_t > > waitList;
bool imageWaitThreadRun = true;
void imageWaitThreadMain( void )
{
wait:
waitListSem.wait();
if ( imageWaitThreadRun == false )
{
return;
}
bool bFound = false;
int fence;
uint64_t commitID;
retry:
{
std::unique_lock< std::mutex > lock( waitListLock );
if( waitList.size() == 0 )
{
goto wait;
}
fence = waitList[ 0 ].first;
commitID = waitList[ 0 ].second;
bFound = true;
waitList.erase( waitList.begin() );
}
assert( bFound == true );
gpuvis_trace_begin_ctx_printf( commitID, "wait fence" );
struct pollfd fd = { fence, POLLOUT, 0 };
int ret = poll( &fd, 1, 100 );
if ( ret < 0 )
{
perror( "failed to poll fence FD" );
}
gpuvis_trace_end_ctx_printf( commitID, "wait fence" );
close( fence );
{
std::unique_lock< std::mutex > lock( listCommitsDoneLock );
listCommitsDone.push_back( commitID );
}
nudge_steamcompmgr();
goto retry;
}
sem statsThreadSem;
std::mutex statsEventQueueLock;
std::vector< std::string > statsEventQueue;
std::string statsThreadPath;
int statsPipeFD = -1;
bool statsThreadRun;
void statsThreadMain( void )
{
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.size() == 0 )
{
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;
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
return ts.tv_sec * 1'000'000'000ul + ts.tv_nsec;
}
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 (Display *dpy, unsigned long sequence)
{
while (ignore_head)
{
if ((long) (sequence - ignore_head->sequence) > 0)
{
ignore *next = ignore_head->next;
free (ignore_head);
ignore_head = next;
if (!ignore_head)
ignore_tail = &ignore_head;
}
else
break;
}
}
static void
set_ignore (Display *dpy, unsigned long sequence)
{
ignore *i = (ignore *)malloc (sizeof (ignore));
if (!i)
return;
i->sequence = sequence;
i->next = NULL;
*ignore_tail = i;
ignore_tail = &i->next;
}
static int
should_ignore (Display *dpy, unsigned long sequence)
{
discard_ignore (dpy, sequence);
return ignore_head && ignore_head->sequence == sequence;
}
static win *
find_win (Display *dpy, Window id)
{
win *w;
if (id == None)
{
return NULL;
}
for (w = list; w; w = w->next)
{
if (w->id == id)
{
return w;
}
}
if ( dpy == nullptr )
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 (dpy, NextRequest (dpy));
XQueryTree(dpy, id, &root, &parent, &children, &childrenCount);
if (children)
XFree(children);
if (root == parent || parent == None)
{
return NULL;
}
return find_win(dpy, parent);
}
static win * find_win( struct wlr_surface *surf )
{
win *w = nullptr;
for (w = list; w; w = w->next)
{
if ( w->surface.wlr == surf )
return w;
}
return nullptr;
}
static void
release_commit ( commit_t &commit )
{
if ( commit.fb_id != 0 )
{
drm_drop_fbid( &g_DRM, commit.fb_id );
commit.fb_id = 0;
}
if ( commit.vulkanTex != 0 )
{
vulkan_free_texture( commit.vulkanTex );
commit.vulkanTex = 0;
}
wlserver_lock();
wlr_buffer_unlock( commit.buf );
wlserver_unlock();
}
static bool
import_commit ( struct wlr_buffer *buf, struct wlr_dmabuf_attributes *dmabuf, commit_t &commit )
{
commit.buf = buf;
if ( BIsNested() == False )
{
commit.fb_id = drm_fbid_from_dmabuf( &g_DRM, buf, dmabuf );
}
commit.vulkanTex = vulkan_create_texture_from_dmabuf( dmabuf );
assert( commit.vulkanTex != 0 );
return true;
}
static bool
get_window_last_done_commit( win *w, commit_t &commit )
{
int32_t lastCommit = -1;
for ( uint32_t i = 0; i < w->commit_queue.size(); i++ )
{
if ( w->commit_queue[ i ].done == true )
{
lastCommit = i;
}
}
if ( lastCommit == -1 )
{
return false;
}
commit = w->commit_queue[ lastCommit ];
return true;
}
/**
* Constructor for a cursor. It is hidden in the beginning (normally until moved by user).
*/
MouseCursor::MouseCursor(_XDisplay *display)
: m_texture(0)
, m_dirty(true)
, m_imageEmpty(false)
, m_hideForMovement(true)
, m_display(display)
{
}
void MouseCursor::queryPositions(int &rootX, int &rootY, int &winX, int &winY)
{
Window window, child;
unsigned int mask;
XQueryPointer(m_display, DefaultRootWindow(m_display), &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_display, DefaultRootWindow(m_display), &window, &child,
&rootX, &rootY, &winX, &winY, &mask);
}
void MouseCursor::checkSuspension()
{
unsigned int buttonMask;
queryButtonMask(buttonMask);
if (buttonMask & ( Button1Mask | Button2Mask | Button3Mask | Button4Mask | Button5Mask )) {
m_hideForMovement = false;
m_lastMovedTime = get_time_in_milliseconds();
}
const bool suspended = get_time_in_milliseconds() - m_lastMovedTime > cursorHideTime;
if (!m_hideForMovement && suspended) {
m_hideForMovement = true;
win *window = find_win(m_display, currentInputFocusWindow);
// Rearm warp count
if (window) {
window->mouseMoved = 0;
}
// We're hiding the cursor, force redraw if we were showing it
if (window && !m_imageEmpty ) {
hasRepaint = true;
nudge_steamcompmgr();
}
}
}
void MouseCursor::warp(int x, int y)
{
XWarpPointer(m_display, None, currentInputFocusWindow, 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)
{
XRenderPictFormat *pictformat;
Picture picture;
XImage* ximage;
Pixmap pixmap;
Cursor cursor;
GC gc;
if (!(ximage = XCreateImage(
m_display,
DefaultVisual(m_display, DefaultScreen(m_display)),
32, ZPixmap,
0,
data,
w, h,
32, 0)))
{
fprintf(stderr, "Failed to make ximage for cursor.\n");
goto error_image;
}
if (!(pixmap = XCreatePixmap(m_display, DefaultRootWindow(m_display), w, h, 32)))
{
fprintf(stderr, "Failed to make pixmap for cursor\n");
goto error_pixmap;
}
if (!(gc = XCreateGC(m_display, pixmap, 0, NULL)))
{
fprintf(stderr, "Failed to make gc for cursor\n");
goto error_gc;
}
XPutImage(m_display, pixmap, gc, ximage, 0, 0, 0, 0, w, h);
if (!(pictformat = XRenderFindStandardFormat(m_display, PictStandardARGB32)))
{
fprintf(stderr, "Failed to create pictformat for cursor\n");
goto error_pictformat;
}
if (!(picture = XRenderCreatePicture(m_display, pixmap, pictformat, 0, NULL)))
{
fprintf(stderr, "Failed to create picture for cursor\n");
goto error_picture;
}
if (!(cursor = XRenderCreateCursor(m_display, picture, 0, 0)))
{
fprintf(stderr, "Failed to create cursor\n");
goto error_cursor;
}
XDefineCursor(m_display, DefaultRootWindow(m_display), cursor);
XFlush(m_display);
setDirty();
return true;
error_cursor:
XRenderFreePicture(m_display, picture);
error_picture:
error_pictformat:
XFreeGC(m_display, gc);
error_gc:
XFreePixmap(m_display, pixmap);
error_pixmap:
// XDestroyImage frees the data.
XDestroyImage(ximage);
error_image:
return false;
}
void MouseCursor::constrainPosition()
{
int i;
win *window = find_win(m_display, currentInputFocusWindow);
// If we had barriers before, get rid of them.
for (i = 0; i < 4; i++) {
if (m_scaledFocusBarriers[i] != None) {
XFixesDestroyPointerBarrier(m_display, m_scaledFocusBarriers[i]);
m_scaledFocusBarriers[i] = None;
}
}
auto barricade = [this](int x1, int y1, int x2, int y2) {
return XFixesCreatePointerBarrier(m_display, DefaultRootWindow(m_display),
x1, y1, x2, y2, 0, 0, NULL);
};
// Constrain it to the window; careful, the corners will leak due to a known X server bug.
m_scaledFocusBarriers[0] = barricade(0, window->a.y, root_width, window->a.y);
m_scaledFocusBarriers[1] = barricade(window->a.x + window->a.width, 0,
window->a.x + window->a.width, root_height);
m_scaledFocusBarriers[2] = barricade(root_width, window->a.y + window->a.height,
0, window->a.y + window->a.height);
m_scaledFocusBarriers[3] = barricade(window->a.x, root_height, window->a.x, 0);
// Make sure the cursor is somewhere in our jail
int rootX, rootY;
queryGlobalPosition(rootX, rootY);
if (rootX >= window->a.width || rootY >= window->a.height) {
warp(window->a.width / 2, window->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;
win *window = find_win(m_display, currentInputFocusWindow);
if (window) {
// If mouse moved and we're on the hook for showing the cursor, repaint
if (!m_hideForMovement && !m_imageEmpty) {
hasRepaint = true;
}
// If mouse moved and screen is magnified, repaint
if ( zoomScaleRatio != 1.0 )
{
hasRepaint = true;
}
}
// Ignore the first events as it's likely to be non-user-initiated warps
// Account for one warp from us, one warp from the app and one warp from
// the toolkit.
if (!window || window->mouseMoved++ < 3 )
return;
m_lastMovedTime = get_time_in_milliseconds();
m_hideForMovement = false;
}
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_display);
if (!image) {
return false;
}
m_hotspotX = image->xhot;
m_hotspotY = image->yhot;
m_width = image->width;
m_height = image->height;
if ( BIsNested() == false && alwaysComposite == False )
{
m_width = g_DRM.cursor_width;
m_height = g_DRM.cursor_height;
}
if (m_texture) {
vulkan_free_texture(m_texture);
m_texture = 0;
}
// Assume the cursor is fully translucent unless proven otherwise.
bool bNoCursor = true;
auto cursorBuffer = std::vector<uint32_t>(m_width * m_height);
for (int i = 0; i < image->height; i++) {
for (int j = 0; j < image->width; j++) {
cursorBuffer[i * m_width + j] = image->pixels[i * image->width + j];
if ( cursorBuffer[i * m_width + j] & 0xff000000 ) {
bNoCursor = false;
}
}
}
if (bNoCursor != m_imageEmpty) {
m_imageEmpty = bNoCursor;
if (m_imageEmpty) {
// fprintf( stderr, "grab?\n" );
}
}
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(m_width, m_height, VK_FORMAT_B8G8R8A8_UNORM, texCreateFlags, cursorBuffer.data());
assert(m_texture);
XFree(image);
m_dirty = false;
return true;
}
void MouseCursor::paint(win *window, struct Composite_t *pComposite,
struct VulkanPipeline_t *pPipeline)
{
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;
}
float scaledX, scaledY;
float currentScaleRatio = 1.0;
float XRatio = (float)currentOutputWidth / window->a.width;
float YRatio = (float)currentOutputHeight / window->a.height;
int cursorOffsetX, cursorOffsetY;
currentScaleRatio = (XRatio < YRatio) ? XRatio : YRatio;
currentScaleRatio = std::min(g_flMaxWindowScale, currentScaleRatio);
if (g_bIntegerScale)
currentScaleRatio = floor(currentScaleRatio);
cursorOffsetX = (currentOutputWidth - window->a.width * currentScaleRatio * globalScaleRatio) / 2.0f;
cursorOffsetY = (currentOutputHeight - window->a.height * currentScaleRatio * globalScaleRatio) / 2.0f;
// Actual point on scaled screen where the cursor hotspot should be
scaledX = (winX - window->a.x) * currentScaleRatio * globalScaleRatio + cursorOffsetX;
scaledY = (winY - window->a.y) * currentScaleRatio * globalScaleRatio + cursorOffsetY;
if ( zoomScaleRatio != 1.0 )
{
scaledX += ((window->a.width / 2) - winX) * currentScaleRatio * globalScaleRatio;
scaledY += ((window->a.height / 2) - winY) * currentScaleRatio * globalScaleRatio;
}
// Apply the cursor offset inside the texture using the display scale
scaledX = scaledX - m_hotspotX;
scaledY = scaledY - m_hotspotY;
int curLayer = pComposite->nLayerCount;
pComposite->data.flOpacity[ curLayer ] = 1.0;
pComposite->data.vScale[ curLayer ].x = 1.0;
pComposite->data.vScale[ curLayer ].y = 1.0;
pComposite->data.vOffset[ curLayer ].x = -scaledX;
pComposite->data.vOffset[ curLayer ].y = -scaledY;
pPipeline->layerBindings[ curLayer ].surfaceWidth = m_width;
pPipeline->layerBindings[ curLayer ].surfaceHeight = m_height;
pPipeline->layerBindings[ curLayer ].zpos = 2; // cursor, on top of both bottom layers
pPipeline->layerBindings[ curLayer ].tex = m_texture;
pPipeline->layerBindings[ curLayer ].fbid = BIsNested() ? 0 :
vulkan_texture_get_fbid(m_texture);
pPipeline->layerBindings[ curLayer ].bFilter = false;
pPipeline->layerBindings[ curLayer ].bBlackBorder = false;
pComposite->nLayerCount += 1;
}
static void
paint_window (Display *dpy, win *w, struct Composite_t *pComposite,
struct VulkanPipeline_t *pPipeline, Bool notificationMode, MouseCursor *cursor)
{
uint32_t sourceWidth, sourceHeight;
int drawXOffset = 0, drawYOffset = 0;
float currentScaleRatio = 1.0;
commit_t lastCommit = {};
bool validContents = get_window_last_done_commit( w, lastCommit );
if (!w)
return;
// Don't add a layer at all if it's an overlay without contents
if (w->isOverlay && !validContents)
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
win *mainOverlayWindow = find_win(dpy, currentOverlayWindow);
if (notificationMode && !mainOverlayWindow)
return;
if (notificationMode)
{
sourceWidth = mainOverlayWindow->a.width;
sourceHeight = mainOverlayWindow->a.height;
}
else
{
sourceWidth = w->a.width;
sourceHeight = w->a.height;
}
if (sourceWidth != currentOutputWidth || sourceHeight != currentOutputHeight || globalScaleRatio != 1.0f)
{
float XRatio = (float)currentOutputWidth / sourceWidth;
float YRatio = (float)currentOutputHeight / sourceHeight;
currentScaleRatio = (XRatio < YRatio) ? XRatio : YRatio;
currentScaleRatio = std::min(g_flMaxWindowScale, currentScaleRatio);
if (g_bIntegerScale)
currentScaleRatio = floor(currentScaleRatio);
currentScaleRatio *= globalScaleRatio;
drawXOffset = ((int)currentOutputWidth - (int)sourceWidth * currentScaleRatio) / 2.0f;
drawYOffset = ((int)currentOutputHeight - (int)sourceHeight * currentScaleRatio) / 2.0f;
if ( zoomScaleRatio != 1.0 )
{
drawXOffset += (((int)sourceWidth / 2) - cursor->x()) * currentScaleRatio;
drawYOffset += (((int)sourceHeight / 2) - cursor->y()) * currentScaleRatio;
}
}
int curLayer = pComposite->nLayerCount;
pComposite->data.flOpacity[ curLayer ] = w->isOverlay ? w->opacity / (float)OPAQUE : 1.0f;
pComposite->data.vScale[ curLayer ].x = 1.0 / currentScaleRatio;
pComposite->data.vScale[ curLayer ].y = 1.0 / currentScaleRatio;
if (notificationMode)
{
int xOffset = 0, yOffset = 0;
int width = w->a.width * currentScaleRatio;
int height = w->a.height * currentScaleRatio;
if (globalScaleRatio != 1.0f)
{
xOffset = (currentOutputWidth - currentOutputWidth * globalScaleRatio) / 2.0;
yOffset = (currentOutputHeight - currentOutputHeight * globalScaleRatio) / 2.0;
}
pComposite->data.vOffset[ curLayer ].x = (currentOutputWidth - xOffset - width) * -1.0f;
pComposite->data.vOffset[ curLayer ].y = (currentOutputHeight - yOffset - height) * -1.0f;
}
else
{
pComposite->data.vOffset[ curLayer ].x = -drawXOffset;
pComposite->data.vOffset[ curLayer ].y = -drawYOffset;
}
pPipeline->layerBindings[ curLayer ].surfaceWidth = w->a.width;
pPipeline->layerBindings[ curLayer ].surfaceHeight = w->a.height;
pPipeline->layerBindings[ curLayer ].zpos = 0;
if ( w->isOverlay || w->isSteamStreamingClient )
{
pPipeline->layerBindings[ curLayer ].zpos = 1;
}
pPipeline->layerBindings[ curLayer ].tex = lastCommit.vulkanTex;
pPipeline->layerBindings[ curLayer ].fbid = lastCommit.fb_id;
pPipeline->layerBindings[ curLayer ].bFilter = w->isOverlay ? true : g_bFilterGameWindow;
pPipeline->layerBindings[ curLayer ].bBlackBorder = notificationMode ? false : true;
pComposite->nLayerCount += 1;
}
static void
paint_message (const char *message, int Y, float r, float g, float b)
{
}
static void
paint_debug_info (Display *dpy)
{
int Y = 100;
// glBindTexture(GL_TEXTURE_2D, 0);
char messageBuffer[256];
sprintf(messageBuffer, "Compositing at %.1f FPS", currentFrameRate);
float textYMax = 0.0f;
paint_message(messageBuffer, Y, 1.0f, 1.0f, 1.0f); Y += textYMax;
if (find_win(dpy, currentFocusWindow))
{
if (gameFocused)
{
sprintf(messageBuffer, "Presenting game window %x", (unsigned int)currentFocusWindow);
paint_message(messageBuffer, Y, 0.0f, 1.0f, 0.0f); Y += textYMax;
}
else
{
// must be Steam
paint_message("Presenting Steam", Y, 1.0f, 1.0f, 0.0f); Y += textYMax;
}
}
win *overlay = find_win(dpy, currentOverlayWindow);
win *notification = find_win(dpy, currentNotificationWindow);
if (overlay && gamesRunningCount && overlay->opacity)
{
sprintf(messageBuffer, "Compositing overlay at opacity %f", overlay->opacity / (float)OPAQUE);
paint_message(messageBuffer, Y, 1.0f, 0.0f, 1.0f); Y += textYMax;
}
if (notification && gamesRunningCount && notification->opacity)
{
sprintf(messageBuffer, "Compositing notification at opacity %f", notification->opacity / (float)OPAQUE);
paint_message(messageBuffer, Y, 1.0f, 0.0f, 1.0f); Y += textYMax;
}
if (gotXError) {
paint_message("Encountered X11 error", Y, 1.0f, 0.0f, 0.0f); Y += textYMax;
}
}
static void
paint_all(Display *dpy, MouseCursor *cursor)
{
static long long int paintID = 0;
paintID++;
gpuvis_trace_begin_ctx_printf( paintID, "paint_all" );
win *w;
win *overlay;
win *notification;
win *input;
unsigned int currentTime = get_time_in_milliseconds();
Bool fadingOut = ((currentTime - fadeOutStartTime) < FADE_OUT_DURATION && fadeOutWindow.id != None);
w = find_win(dpy, currentFocusWindow);
overlay = find_win(dpy, currentOverlayWindow);
notification = find_win(dpy, currentNotificationWindow);
input = find_win(dpy, currentInputFocusWindow);
if ( !w )
{
return;
}
Bool inGame = False;
if ( gamesRunningCount || w->appID != 0 )
{
inGame = True;
}
frameCounter++;
if (frameCounter == 300)
{
currentFrameRate = 300 * 1000.0f / (currentTime - lastSampledFrameTime);
lastSampledFrameTime = currentTime;
frameCounter = 0;
stats_printf( "fps=%f\n", currentFrameRate );
if ( w->isSteam )
{
stats_printf( "focus=steam\n" );
}
else
{
stats_printf( "focus=%i\n", w->appID );
}
}
struct Composite_t composite = {};
struct VulkanPipeline_t pipeline = {};
// Fading out from previous window?
if (fadingOut)
{
double newOpacity = ((currentTime - fadeOutStartTime) / (double)FADE_OUT_DURATION);
// Draw it in the background
fadeOutWindow.opacity = (1.0 - newOpacity) * OPAQUE;
paint_window(dpy, &fadeOutWindow, &composite, &pipeline, False, cursor);
// Blend new window on top with linear crossfade
w->opacity = newOpacity * OPAQUE;
paint_window(dpy, w, &composite, &pipeline, False, cursor);
}
else
{
// 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->isSteamStreamingClient == True )
{
win *videow = NULL;
for ( videow = list; videow; videow = videow->next )
{
if ( videow->isSteamStreamingClientVideo == True )
{
// TODO: also check matching AppID so we can have several pairs
paint_window(dpy, videow, &composite, &pipeline, False, cursor);
break;
}
}
// paint UI unless it's fully hidden, which it communicates to us through opacity=0
if ( w->opacity > TRANSLUCENT )
{
paint_window(dpy, w, &composite, &pipeline, False, cursor);
}
}
else
{
// Just draw focused window as normal, be it Steam or the game
paint_window(dpy, w, &composite, &pipeline, False, cursor);
}
if (fadeOutWindow.id) {
if (fadeOutWindowGone)
{
// This is the only reference to these resources now.
fadeOutWindowGone = False;
}
fadeOutWindow.id = None;
}
}
int touchInputFocusLayer = composite.nLayerCount - 1;
if (inGame && overlay)
{
if (overlay->opacity)
{
paint_window(dpy, overlay, &composite, &pipeline, False, cursor);
if ( overlay->id == currentInputFocusWindow )
touchInputFocusLayer = composite.nLayerCount - 1;
}
}
if ( touchInputFocusLayer >= 0 )
{
focusedWindowScaleX = composite.data.vScale[ touchInputFocusLayer ].x;
focusedWindowScaleY = composite.data.vScale[ touchInputFocusLayer ].y;
focusedWindowOffsetX = composite.data.vOffset[ touchInputFocusLayer ].x;
focusedWindowOffsetY = composite.data.vOffset[ touchInputFocusLayer ].y;
}
if (inGame && notification)
{
if (notification->opacity)
{
paint_window(dpy, notification, &composite, &pipeline, True, cursor);
}
}
// Draw cursor if we need to
if (input) {
cursor->paint(input, &composite, &pipeline );
}
if (drawDebugInfo)
paint_debug_info(dpy);
if ( BIsNested() == false && g_DRM.paused == true )
{
return;
}
bool bDoComposite = true;
// Handoff from whatever thread to this one since we check ours twice
bool takeScreenshot = g_bTakeScreenshot.exchange(false);
struct pipewire_buffer *pw_buffer = nullptr;
#if HAVE_PIPEWIRE
pw_buffer = dequeue_pipewire_buffer();
#endif
bool bCapture = takeScreenshot || pw_buffer != nullptr;
if ( BIsNested() == false && alwaysComposite == False && bCapture == false )
{
int ret = drm_prepare( &g_DRM, &composite, &pipeline );
if ( ret == 0 )
bDoComposite = false;
else if ( ret == -EACCES )
return;
}
if ( bDoComposite == true )
{
CVulkanTexture *pCaptureTexture = nullptr;
bool bResult = vulkan_composite( &composite, &pipeline, bCapture ? &pCaptureTexture : nullptr );
if ( bResult != true )
{
fprintf( stderr, "composite alarm!!!\n" );
return;
}
if ( BIsNested() == True )
{
vulkan_present_to_window();
}
else
{
memset( &composite, 0, sizeof( composite ) );
composite.nLayerCount = 1;
composite.data.vScale[ 0 ].x = 1.0;
composite.data.vScale[ 0 ].y = 1.0;
composite.data.flOpacity[ 0 ] = 1.0;
memset( &pipeline, 0, sizeof( pipeline ) );
pipeline.layerBindings[ 0 ].surfaceWidth = g_nOutputWidth;
pipeline.layerBindings[ 0 ].surfaceHeight = g_nOutputHeight;
pipeline.layerBindings[ 0 ].fbid = vulkan_get_last_composite_fbid();
pipeline.layerBindings[ 0 ].bFilter = false;
int ret = drm_prepare( &g_DRM, &composite, &pipeline );
// Happens when we're VT-switched away
if ( ret == -EACCES )
return;
if ( ret != 0 )
fprintf( stderr, "Failed to prepare 1-layer flip: %s\n", strerror(-ret) );
// We should always handle a 1-layer flip
assert( ret == 0 );
drm_commit( &g_DRM, &composite, &pipeline );
}
if ( takeScreenshot )
{
assert( pCaptureTexture != nullptr );
assert( pCaptureTexture->m_format == VK_FORMAT_B8G8R8A8_UNORM );
uint32_t redMask = 0x00ff0000;
uint32_t greenMask = 0x0000ff00;
uint32_t blueMask = 0x000000ff;
uint32_t alphaMask = 0;
SDL_Surface *pSDLSurface = SDL_CreateRGBSurfaceFrom( pCaptureTexture->m_pMappedData, currentOutputWidth, currentOutputHeight, 32, pCaptureTexture->m_unRowPitch, redMask, greenMask, blueMask, alphaMask );
static char pTimeBuffer[1024];
time_t currentTime = time(0);
struct tm *localTime = localtime( &currentTime );
strftime( pTimeBuffer, sizeof( pTimeBuffer ), "/tmp/gamescope_%Y-%m-%d_%H-%M-%S.bmp", localTime );
SDL_SaveBMP( pSDLSurface, pTimeBuffer );
SDL_FreeSurface( pSDLSurface );
fprintf(stderr, "Screenshot saved to %s\n", pTimeBuffer);
takeScreenshot = False;
}
#if HAVE_PIPEWIRE
if ( pw_buffer != nullptr )
{
assert( pCaptureTexture != nullptr );
assert( pCaptureTexture->m_format == VK_FORMAT_B8G8R8A8_UNORM );
if ( pw_buffer->video_info.size.width != currentOutputWidth || pw_buffer->video_info.size.height != currentOutputHeight )
{
// Push black frames until the PipeWire thread realizes the stream size has changed
memset( pw_buffer->data, 0, pw_buffer->stride * pw_buffer->video_info.size.height );
}
else
{
// TODO: avoid this memcpy by using multiple capture textures
int bpp = 4;
for ( unsigned int i = 0; i < currentOutputHeight; i++ )
{
memcpy( pw_buffer->data + i * pw_buffer->stride, (uint8_t *) pCaptureTexture->m_pMappedData + i * pCaptureTexture->m_unRowPitch, bpp * currentOutputWidth );
}
}
push_pipewire_buffer(pw_buffer);
// TODO: make sure the buffer isn't lost in one of the failure
// code-paths above
}
#endif
}
else
{
assert( BIsNested() == false );
drm_commit( &g_DRM, &composite, &pipeline );
}
gpuvis_trace_end_ctx_printf( paintID, "paint_all" );
gpuvis_trace_printf( "paint_all %i layers, composite %i", (int)composite.nLayerCount, bDoComposite );
}
/* Get prop from window
* not found: default
* otherwise the value
*/
static unsigned int
get_prop(Display *dpy, Window win, Atom prop, unsigned int def, bool *found = nullptr )
{
Atom actual;
int format;
unsigned long n, left;
unsigned char *data;
int result = XGetWindowProperty(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
bool get_prop( Display *dpy, Window win, Atom prop, std::vector< uint32_t > &vecResult )
{
Atom actual;
int format;
unsigned long n, left;
uint64_t *data;
// get up to 16 results in one go, we can add a real loop if we ever need anything beyong that
int result = XGetWindowProperty(dpy, win, prop, 0L, 16L, False,
XA_CARDINAL, &actual, &format,
&n, &left, ( unsigned char** )&data);
if (result == Success && data != NULL)
{
vecResult.clear();
for ( uint32_t i = 0; i < n; i++ )
{
vecResult.push_back( data[ i ] );
}
XFree( (void *) data);
return true;
}
return false;
}
static bool
win_has_game_id( win *w )
{
return w->appID != 0;
}
static bool
win_is_override_redirect( win *w )
{
return w->a.override_redirect && !w->ignoreOverrideRedirect;
}
static bool
win_skip_taskbar_and_pager( win *w )
{
return w->skipTaskbar && w->skipPager;
}
/* 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( win *a, win *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 );
// Wine sets SKIP_TASKBAR and SKIP_PAGER hints for WS_EX_NOACTIVATE windows.
// See https://github.com/Plagman/gamescope/issues/87
if ( win_skip_taskbar_and_pager( a ) != win_skip_taskbar_and_pager ( b ) )
return !win_skip_taskbar_and_pager( a );
// The damage sequences are only relevant for game windows.
if ( win_has_game_id( a ) && a->damage_sequence != b->damage_sequence )
return a->damage_sequence > b->damage_sequence;
return false;
}
static void
determine_and_apply_focus (Display *dpy, MouseCursor *cursor)
{
win *w, *focus = NULL;
win *inputFocus = NULL;
gameFocused = False;
Window prevFocusWindow = currentFocusWindow;
currentFocusWindow = None;
currentFocusWin = nullptr;
currentOverlayWindow = None;
currentNotificationWindow = None;
unsigned int maxOpacity = 0;
std::vector< win* > vecPossibleFocusWindows;
for (w = list; w; w = w->next)
{
// Always skip system tray icons
if ( w->isSysTrayIcon )
{
continue;
}
if ( w->a.map_state == IsViewable && w->a.c_class == InputOutput && w->isOverlay == False &&
(w->opacity > TRANSLUCENT || w->isSteamStreamingClient == True ) )
{
vecPossibleFocusWindows.push_back( w );
}
if (w->isOverlay)
{
if (w->a.width > 1200 && w->opacity >= maxOpacity)
{
currentOverlayWindow = w->id;
maxOpacity = w->opacity;
}
else
{
currentNotificationWindow = w->id;
}
}
if ( w->isOverlay && w->inputFocusMode )
{
inputFocus = w;
}
}
std::vector< unsigned long > focusable_appids;
for ( unsigned long i = 0; i < vecPossibleFocusWindows.size(); i++ )
{
unsigned int unAppID = vecPossibleFocusWindows[ i ]->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 );
}
}
}
XChangeProperty( dpy, root, XInternAtom( dpy, "GAMESCOPE_FOCUSABLE_APPS", False ),
XA_CARDINAL, 32, PropModeReplace, (unsigned char *)focusable_appids.data(),
focusable_appids.size() );
std::stable_sort( vecPossibleFocusWindows.begin(), vecPossibleFocusWindows.end(),
is_focus_priority_greater );
if ( focusControlled == true )
{
for ( unsigned long i = 0; i < vecFocuscontrolAppIDs.size(); i++ )
{
for ( unsigned long j = 0; j < vecPossibleFocusWindows.size(); j++ )
{
if ( vecPossibleFocusWindows[ j ]->appID == vecFocuscontrolAppIDs[ i ] )
{
focus = vecPossibleFocusWindows[ j ];
goto found;
}
}
}
found:
gameFocused = true;
}
else if ( vecPossibleFocusWindows.size() > 0 )
{
focus = vecPossibleFocusWindows[ 0 ];
gameFocused = focus->appID != 0;
}
unsigned long focusedWindow = 0;
unsigned long focusedAppId = 0;
if ( inputFocus == NULL )
{
inputFocus = focus;
}
if ( focus )
{
focusedWindow = focus->id;
focusedAppId = inputFocus->appID;
}
XChangeProperty( dpy, root, XInternAtom( dpy, "GAMESCOPE_FOCUSED_WINDOW", False ),
XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&focusedWindow,
focusedWindow != 0 ? 1 : 0 );
XChangeProperty( dpy, root, XInternAtom( dpy, "GAMESCOPE_FOCUSED_APP", False ),
XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&focusedAppId,
focusedAppId != 0 ? 1 : 0 );
if (!focus)
{
return;
}
if ( gameFocused )
{
// Do some searches through game windows to follow transient links if needed
while ( true )
{
bool bFoundTransient = false;
for ( uint32_t i = 0; i < vecPossibleFocusWindows.size(); i++ )
{
win *candidate = vecPossibleFocusWindows[ i ];
if ( candidate != focus && candidate->transientFor == focus->id )
{
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 (fadeOutWindow.id == None && currentFocusWindow != focus->id)
// {
// // Initiate fade out if switching focus
// w = find_win(dpy, currentFocusWindow);
//
// if (w)
// {
// ensure_win_resources(dpy, w);
// fadeOutWindow = *w;
// fadeOutStartTime = get_time_in_milliseconds();
// }
// }
// if (fadeOutWindow.id && currentFocusWindow != focus->id)
if ( prevFocusWindow != focus->id )
{
/* 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(dpy, focus->id, WMStateAtom, WMStateAtom, 32,
PropModeReplace, (unsigned char *)wmState,
sizeof(wmState) / sizeof(wmState[0]));
gpuvis_trace_printf( "determine_and_apply_focus focus %lu", focus->id );
if ( debugFocus == True )
{
fprintf( stderr, "determine_and_apply_focus focus %lu\n", focus->id );
char buf[512];
sprintf( buf, "xwininfo -id 0x%lx; xprop -id 0x%lx; xwininfo -root -tree", focus->id, focus->id );
system( buf );
}
}
currentFocusWindow = focus->id;
currentFocusWin = focus;
if ( currentInputFocusWindow != inputFocus->id ||
currentInputFocusMode != inputFocus->inputFocusMode )
{
win *keyboardFocusWin = inputFocus;
if ( inputFocus->inputFocusMode == 2 )
keyboardFocusWin = focus;
if ( inputFocus->surface.wlr != nullptr || keyboardFocusWin->surface.wlr != nullptr )
{
wlserver_lock();
if ( inputFocus->surface.wlr != nullptr )
wlserver_mousefocus( inputFocus->surface.wlr );
if ( keyboardFocusWin->surface.wlr != nullptr )
wlserver_keyboardfocus( keyboardFocusWin->surface.wlr );
wlserver_unlock();
}
XSetInputFocus(dpy, keyboardFocusWin->id, RevertToNone, CurrentTime);
currentInputFocusWindow = inputFocus->id;
currentInputFocusMode = inputFocus->inputFocusMode;
currentKeyboardFocusWindow = keyboardFocusWin->id;
}
w = focus;
cursor->constrainPosition();
if ( list[0].id != inputFocus->id )
{
XRaiseWindow(dpy, inputFocus->id);
}
if (!focus->nudged)
{
XMoveWindow(dpy, focus->id, 1, 1);
focus->nudged = True;
}
if (w->a.x != 0 || w->a.y != 0)
XMoveWindow(dpy, focus->id, 0, 0);
if ( focus->isFullscreen && ( w->a.width != root_width || w->a.height != root_height || globalScaleRatio != 1.0f ) )
{
XResizeWindow(dpy, focus->id, root_width, root_height);
}
else if (!focus->isFullscreen && focus->sizeHintsSpecified &&
((unsigned)focus->a.width != focus->requestedWidth ||
(unsigned)focus->a.height != focus->requestedHeight))
{
XResizeWindow(dpy, focus->id, focus->requestedWidth, focus->requestedHeight);
}
Window root_return = None, parent_return = None;
Window *children = NULL;
unsigned int nchildren = 0;
unsigned int i = 0;
XQueryTree (dpy, w->id, &root_return, &parent_return, &children, &nchildren);
while (i < nchildren)
{
XSelectInput( dpy, children[i], PointerMotionMask | FocusChangeMask );
i++;
}
XFree (children);
}
static void
get_size_hints(Display *dpy, win *w)
{
XSizeHints hints;
long hintsSpecified = 0;
XGetWMNormalHints(dpy, w->id, &hints, &hintsSpecified);
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->a.override_redirect)
{
Window root_return = None, parent_return = None;
Window *children = NULL;
unsigned int nchildren = 0;
XQueryTree (dpy, w->id, &root_return, &parent_return, &children, &nchildren);
if (nchildren == 1)
{
XWindowAttributes attribs;
XGetWindowAttributes (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->a.width &&
attribs.height <= w->a.height)
{
w->sizeHintsSpecified = True;
w->requestedWidth = attribs.width;
w->requestedHeight = attribs.height;
XMoveWindow(dpy, children[0], 0, 0);
w->ignoreOverrideRedirect = True;
}
}
XFree (children);
}
}
}
static void
get_win_title(Display *dpy, win *w, Atom atom)
{
assert(atom == XA_WM_NAME || atom == netWMNameAtom);
XTextProperty tp;
XGetTextProperty ( dpy, w->id, &tp, atom );
bool is_utf8;
if (tp.encoding == 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;
}
free(w->title);
if (tp.nitems > 0) {
w->title = strndup((char *)tp.value, tp.nitems);
} else {
w->title = NULL;
}
w->utf8_title = is_utf8;
}
static void
get_net_wm_state(Display *dpy, win *w)
{
Atom type;
int format;
unsigned long nitems;
unsigned long bytesAfter;
unsigned char *data;
if (XGetWindowProperty(dpy, w->id, 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] == netWMStateFullscreenAtom) {
w->isFullscreen = True;
} else if (props[i] == netWMStateSkipTaskbarAtom) {
w->skipTaskbar = True;
} else if (props[i] == netWMStateSkipPagerAtom) {
w->skipPager = True;
} else {
fprintf(stderr, "Unhandled initial NET_WM_STATE property: %s\n", XGetAtomName(dpy, props[i]));
}
}
XFree(data);
}
static void
map_win (Display *dpy, Window id, unsigned long sequence)
{
win *w = find_win (dpy, id);
if (!w)
return;
w->a.map_state = IsViewable;
/* This needs to be here or else we lose transparency messages */
XSelectInput (dpy, id, PropertyChangeMask | SubstructureNotifyMask |
PointerMotionMask | LeaveWindowMask | FocusChangeMask);
/* This needs to be here since we don't get PropertyNotify when unmapped */
w->opacity = get_prop (dpy, w->id, opacityAtom, OPAQUE);
w->isSteam = get_prop (dpy, w->id, steamAtom, 0);
/* First try to read the UTF8 title prop, then fallback to the non-UTF8 one */
get_win_title( dpy, w, netWMNameAtom );
get_win_title( dpy, w, XA_WM_NAME );
w->inputFocusMode = get_prop(dpy, w->id, steamInputFocusAtom, 0);
w->isSteamStreamingClient = get_prop(dpy, w->id, steamStreamingClientAtom, 0);
w->isSteamStreamingClientVideo = get_prop(dpy, w->id, steamStreamingClientVideoAtom, 0);
if ( steamMode == True )
{
uint32_t appID = get_prop (dpy, w->id, gameAtom, 0);
if ( w->appID != 0 && appID != 0 && w->appID != appID )
{
fprintf( stderr, "appid clash was %u now %u\n", w->appID, appID );
}
// Let the appID property be authoritative for now
if ( appID != 0 )
{
w->appID = appID;
}
}
else
{
w->appID = w->id;
}
w->isOverlay = get_prop (dpy, w->id, overlayAtom, 0);
get_size_hints(dpy, w);
get_net_wm_state(dpy, w);
XWMHints *wmHints = XGetWMHints( dpy, w->id );
if ( wmHints != nullptr )
{
if ( wmHints->flags & (InputHint | StateHint ) && wmHints->input == True && wmHints->initial_state == NormalState )
{
XRaiseWindow( dpy, w->id );
}
XFree( wmHints );
}
Window transientFor = None;
if ( XGetTransientForHint( dpy, w->id, &transientFor ) )
{
w->transientFor = transientFor;
}
else
{
w->transientFor = None;
}
w->damage_sequence = 0;
w->map_sequence = sequence;
focusDirty = True;
}
static void
finish_unmap_win (Display *dpy, win *w)
{
// TODO clear done commits here?
// if (fadeOutWindow.id != w->id)
// {
// teardown_win_resources( w );
// }
if (fadeOutWindow.id == w->id)
{
fadeOutWindowGone = True;
}
/* don't care about properties anymore */
set_ignore (dpy, NextRequest (dpy));
XSelectInput(dpy, w->id, 0);
clipChanged = True;
}
static void
unmap_win (Display *dpy, Window id, Bool fade)
{
win *w = find_win (dpy, id);
if (!w)
return;
w->a.map_state = IsUnmapped;
focusDirty = True;
finish_unmap_win (dpy, 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 );
std::string proc_stat;
std::getline( proc_stat_file, proc_stat );
char *procName = nullptr;
char *lastParens;
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 ];
}
}
*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 (Display *dpy, Window id)
{
XResClientIdSpec client_spec = {
.client = id,
.mask = XRES_CLIENT_ID_PID_MASK,
};
long num_ids = 0;
XResClientIdValue *client_ids = NULL;
XResQueryClientIds(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)
fprintf(stderr, "Failed to find PID for window 0x%lx\n", id);
return pid;
}
static void
add_win (Display *dpy, Window id, Window prev, unsigned long sequence)
{
win *new_win = new win;
win **p;
if (!new_win)
return;
if (prev)
{
for (p = &list; *p; p = &(*p)->next)
if ((*p)->id == prev)
break;
}
else
p = &list;
new_win->id = id;
set_ignore (dpy, NextRequest (dpy));
if (!XGetWindowAttributes (dpy, id, &new_win->a))
{
delete new_win;
return;
}
new_win->damage_sequence = 0;
new_win->map_sequence = 0;
if (new_win->a.c_class == InputOnly)
new_win->damage = None;
else
{
new_win->damage = XDamageCreate (dpy, id, XDamageReportRawRectangles);
}
new_win->opacity = OPAQUE;
if ( useXRes == True )
{
new_win->pid = get_win_pid (dpy, id);
}
else
{
new_win->pid = -1;
}
new_win->isOverlay = False;
new_win->isSteam = False;
new_win->isSteamStreamingClient = False;
new_win->isSteamStreamingClientVideo = False;
new_win->inputFocusMode = 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( dpy, id, &transientFor ) )
{
new_win->transientFor = transientFor;
}
else
{
new_win->transientFor = None;
}
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 = False;
wlserver_surface_init( &new_win->surface, id );
new_win->next = *p;
*p = new_win;
if (new_win->a.map_state == IsViewable)
map_win (dpy, id, sequence);
focusDirty = True;
}
static void
restack_win (Display *dpy, win *w, Window new_above)
{
Window old_above;
if (w->next)
old_above = w->next->id;
else
old_above = None;
if (old_above != new_above)
{
win **prev;
/* unhook */
for (prev = &list; *prev; prev = &(*prev)->next)
{
if ((*prev) == w)
break;
}
*prev = w->next;
/* rehook */
for (prev = &list; *prev; prev = &(*prev)->next)
{
if ((*prev)->id == new_above)
break;
}
w->next = *prev;
*prev = w;
focusDirty = True;
}
}
static void
configure_win (Display *dpy, XConfigureEvent *ce)
{
win *w = find_win (dpy, ce->window);
if (!w || w->id != ce->window)
{
if (ce->window == root)
{
root_width = ce->width;
root_height = ce->height;
}
return;
}
w->a.x = ce->x;
w->a.y = ce->y;
w->a.width = ce->width;
w->a.height = ce->height;
w->a.border_width = ce->border_width;
w->a.override_redirect = ce->override_redirect;
restack_win (dpy, w, ce->above);
focusDirty = True;
}
static void
circulate_win (Display *dpy, XCirculateEvent *ce)
{
win *w = find_win (dpy, ce->window);
Window new_above;
if (!w || w->id != ce->window)
return;
if (ce->place == PlaceOnTop)
new_above = list->id;
else
new_above = None;
restack_win (dpy, w, new_above);
clipChanged = True;
}
static void map_request (Display *dpy, XMapRequestEvent *mapRequest)
{
XMapWindow( dpy, mapRequest->window );
}
static void configure_request (Display *dpy, 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( dpy, configureRequest->window, configureRequest->value_mask, &changes );
}
static void circulate_request ( Display *dpy, XCirculateRequestEvent *circulateRequest )
{
XCirculateSubwindows( dpy, circulateRequest->window, circulateRequest->place );
}
static void
finish_destroy_win (Display *dpy, Window id, Bool gone)
{
win **prev, *w;
for (prev = &list; (w = *prev); prev = &w->next)
if (w->id == id)
{
if (gone)
finish_unmap_win (dpy, w);
*prev = w->next;
if (w->damage != None)
{
set_ignore (dpy, NextRequest (dpy));
XDamageDestroy (dpy, w->damage);
w->damage = None;
}
wlserver_lock();
wlserver_surface_finish( &w->surface );
wlserver_unlock();
free(w->title);
delete w;
break;
}
}
static void
destroy_win (Display *dpy, Window id, Bool gone, Bool fade)
{
if (currentFocusWindow == id && gone)
{
currentFocusWindow = None;
currentFocusWin = nullptr;
}
if (currentInputFocusWindow == id && gone)
currentInputFocusWindow = None;
if (currentOverlayWindow == id && gone)
currentOverlayWindow = None;
if (currentNotificationWindow == id && gone)
currentNotificationWindow = None;
if (currentKeyboardFocusWindow == id && gone)
currentKeyboardFocusWindow = None;
focusDirty = True;
finish_destroy_win (dpy, id, gone);
}
static void
damage_win (Display *dpy, XDamageNotifyEvent *de)
{
win *w = find_win (dpy, de->drawable);
win *focus = find_win(dpy, currentFocusWindow);
if (!w)
return;
if (w->isOverlay && !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->damage_sequence == 0)
focusDirty = True;
w->damage_sequence = damageSequence++;
// If we just passed the focused window, we might be eliglible to take over
if ( !focusControlled && focus && focus != w && w->appID &&
w->damage_sequence > focus->damage_sequence)
focusDirty = True;
if (w->damage)
XDamageSubtract(dpy, w->damage, None, None);
gpuvis_trace_printf( "damage_win win %lx appID %u", w->id, w->appID );
}
static void
handle_wl_surface_id(win *w, long surfaceID)
{
struct wlr_surface *surface = NULL;
wlserver_lock();
wlserver_surface_set_wl_id( &w->surface, surfaceID );
surface = w->surface.wlr;
if ( surface == NULL )
{
wlserver_unlock();
return;
}
// If we already focused on our side and are handling this late,
// let wayland know now.
if ( w->id == currentInputFocusWindow )
wlserver_mousefocus( surface );
win *inputFocusWin = find_win( nullptr, currentInputFocusWindow );
Window keyboardFocusWindow = currentInputFocusWindow;
if ( inputFocusWin && inputFocusWin->inputFocusMode == 2 )
keyboardFocusWindow = currentFocusWindow;
if ( w->id == keyboardFocusWindow )
wlserver_keyboardfocus( surface );
// Pull the first buffer out of that window, if needed
xwayland_surface_role_commit( 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:
fprintf(stderr, "Unknown NET_WM_STATE action: %" PRIu32 "\n", action);
}
}
static void
handle_net_wm_state(Display *dpy, win *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] == netWMStateFullscreenAtom) {
update_net_wm_state(action, &w->isFullscreen);
focusDirty = True;
} else if (props[i] == netWMStateSkipTaskbarAtom) {
update_net_wm_state(action, &w->skipTaskbar);
focusDirty = True;
} else if (props[i] == netWMStateSkipPagerAtom) {
update_net_wm_state(action, &w->skipPager);
focusDirty = True;
} else if (props[i] != None) {
fprintf(stderr, "Unhandled NET_WM_STATE property change: %s\n", XGetAtomName(dpy, props[i]));
}
}
}
static void
handle_system_tray_opcode(Display *dpy, 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. */
win *w = find_win(dpy, embed_id);
if (w) {
w->isSysTrayIcon = True;
}
break;
}
default:
fprintf(stderr, "Unhandled _NET_SYSTEM_TRAY_OPCODE %ld\n", opcode);
}
}
/* See http://tronche.com/gui/x/icccm/sec-4.html#s-4.1.4 */
static void
handle_wm_change_state(Display *dpy, win *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. */
fprintf(stderr, "Rejecting WM_CHANGE_STATE to ICONIC for window 0x%lx\n", w->id);
uint32_t wmState[] = { ICCCM_NORMAL_STATE, None };
XChangeProperty(dpy, w->id, WMStateAtom, WMStateAtom, 32,
PropModeReplace, (unsigned char *)wmState,
sizeof(wmState) / sizeof(wmState[0]));
} else {
fprintf(stderr, "Unhandled WM_CHANGE_STATE to %ld for window 0x%lx\n", state, w->id);
}
}
static void
handle_client_message(Display *dpy, XClientMessageEvent *ev)
{
if (ev->window == ourWindow && ev->message_type == netSystemTrayOpcodeAtom)
{
handle_system_tray_opcode( dpy, ev );
return;
}
win *w = find_win(dpy, ev->window);
if (w)
{
if (ev->message_type == WLSurfaceIDAtom)
{
handle_wl_surface_id( w, ev->data.l[0]);
}
else if ( ev->message_type == activeWindowAtom )
{
XRaiseWindow( dpy, w->id );
}
else if ( ev->message_type == netWMStateAtom )
{
handle_net_wm_state( dpy, w, ev );
}
else if ( ev->message_type == WMChangeStateAtom )
{
handle_wm_change_state( dpy, w, ev );
}
else if ( ev->message_type != 0 )
{
fprintf( stderr, "Unhandled client message: %s\n", XGetAtomName( dpy, ev->message_type ) );
}
}
}
static void
handle_property_notify(Display *dpy, XPropertyEvent *ev)
{
/* check if Trans property was changed */
if (ev->atom == opacityAtom)
{
/* reset mode and redraw window */
win * w = find_win(dpy, ev->window);
if ( w != nullptr )
{
unsigned int newOpacity = get_prop(dpy, w->id, opacityAtom, OPAQUE);
if (newOpacity != w->opacity)
{
w->opacity = newOpacity;
if ( gameFocused && ( w->id == currentOverlayWindow || w->id == currentNotificationWindow ) )
{
hasRepaint = true;
}
}
unsigned int maxOpacity = 0;
for (w = list; w; w = w->next)
{
if (w->isOverlay)
{
if (w->a.width > 1200 && w->opacity >= maxOpacity)
{
currentOverlayWindow = w->id;
maxOpacity = w->opacity;
}
}
}
}
}
if (ev->atom == steamAtom)
{
win * w = find_win(dpy, ev->window);
if (w)
{
w->isSteam = get_prop(dpy, w->id, steamAtom, 0);
focusDirty = True;
}
}
if (ev->atom == steamInputFocusAtom )
{
win * w = find_win(dpy, ev->window);
if (w)
{
w->inputFocusMode = get_prop(dpy, w->id, steamInputFocusAtom, 0);
focusDirty = True;
}
}
if (ev->atom == steamTouchClickModeAtom )
{
// Default to 1, left click
g_nTouchClickMode = (enum wlserver_touch_click_mode) get_prop(dpy, root, steamTouchClickModeAtom, 1 );
}
if (ev->atom == steamStreamingClientAtom)
{
win * w = find_win(dpy, ev->window);
if (w)
{
w->isSteamStreamingClient = get_prop(dpy, w->id, steamStreamingClientAtom, 0);
focusDirty = True;
}
}
if (ev->atom == steamStreamingClientVideoAtom)
{
win * w = find_win(dpy, ev->window);
if (w)
{
w->isSteamStreamingClientVideo = get_prop(dpy, w->id, steamStreamingClientVideoAtom, 0);
focusDirty = True;
}
}
if (ev->atom == gamescopeCtrlAppIDAtom )
{
focusControlled = get_prop( dpy, root, gamescopeCtrlAppIDAtom, vecFocuscontrolAppIDs );
focusDirty = True;
}
if (ev->atom == gameAtom)
{
win * w = find_win(dpy, ev->window);
if (w)
{
uint32_t appID = get_prop (dpy, w->id, gameAtom, 0);
if ( w->appID != 0 && appID != 0 && w->appID != appID )
{
fprintf( stderr, "appid clash was %u now %u\n", w->appID, appID );
}
w->appID = appID;
focusDirty = True;
}
}
if (ev->atom == overlayAtom)
{
win * w = find_win(dpy, ev->window);
if (w)
{
w->isOverlay = get_prop(dpy, w->id, overlayAtom, 0);
focusDirty = True;
}
}
if (ev->atom == sizeHintsAtom)
{
win * w = find_win(dpy, ev->window);
if (w)
{
get_size_hints(dpy, w);
focusDirty = True;
}
}
if (ev->atom == gamesRunningAtom)
{
gamesRunningCount = get_prop(dpy, root, gamesRunningAtom, 0);
focusDirty = True;
}
if (ev->atom == screenScaleAtom)
{
overscanScaleRatio = get_prop(dpy, root, screenScaleAtom, 0xFFFFFFFF) / (double)0xFFFFFFFF;
globalScaleRatio = overscanScaleRatio * zoomScaleRatio;
win *w;
if ((w = find_win(dpy, currentFocusWindow)))
{
hasRepaint = true;
}
focusDirty = True;
}
if (ev->atom == screenZoomAtom)
{
zoomScaleRatio = get_prop(dpy, root, screenZoomAtom, 0xFFFF) / (double)0xFFFF;
globalScaleRatio = overscanScaleRatio * zoomScaleRatio;
win *w;
if ((w = find_win(dpy, currentFocusWindow)))
{
hasRepaint = true;
}
focusDirty = True;
}
if (ev->atom == WMTransientForAtom)
{
win * w = find_win(dpy, ev->window);
if (w)
{
Window transientFor = None;
if ( XGetTransientForHint( dpy, ev->window, &transientFor ) )
{
w->transientFor = transientFor;
}
else
{
w->transientFor = None;
}
focusDirty = True;
}
}
if (ev->atom == XA_WM_NAME || ev->atom == netWMNameAtom)
{
win *w = find_win(dpy, ev->window);
if (w) {
get_win_title(dpy, w, ev->atom);
}
}
}
static int
error (Display *dpy, XErrorEvent *ev)
{
int o;
const char *name = NULL;
static char buffer[256];
if (should_ignore (dpy, ev->serial))
return 0;
if (ev->request_code == composite_opcode &&
ev->minor_code == X_CompositeRedirectSubwindows)
{
fprintf (stderr, "Another composite manager is already running\n");
exit (1);
}
o = ev->error_code - xfixes_error;
switch (o) {
case BadRegion: name = "BadRegion"; break;
default: break;
}
o = ev->error_code - damage_error;
switch (o) {
case BadDamage: name = "BadDamage"; break;
default: break;
}
o = ev->error_code - 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 (dpy, ev->error_code, buffer, sizeof (buffer));
name = buffer;
}
fprintf (stderr, "error %d: %s request %d minor %d serial %lu\n",
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;
}
static int
handle_io_error(Display *dpy)
{
fprintf(stderr, "X11 I/O error\n");
imageWaitThreadRun = false;
waitListSem.signal();
if ( statsThreadRun == true )
{
statsThreadRun = false;
statsThreadSem.signal();
}
pthread_exit(NULL);
}
static Bool
register_cm (Display *dpy)
{
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", scr);
a = XInternAtom (dpy, net_wm_cm, False);
w = XGetSelectionOwner (dpy, a);
if (w != None)
{
XTextProperty tp;
char **strs;
int count;
Atom winNameAtom = XInternAtom (dpy, "_NET_WM_NAME", False);
if (!XGetTextProperty (dpy, w, &tp, winNameAtom) &&
!XGetTextProperty (dpy, w, &tp, XA_WM_NAME))
{
fprintf (stderr,
"Another composite manager is already running (0x%lx)\n",
(unsigned long) w);
return False;
}
if (XmbTextPropertyToTextList (dpy, &tp, &strs, &count) == Success)
{
fprintf (stderr,
"Another composite manager is already running (%s)\n",
strs[0]);
XFreeStringList (strs);
}
XFree (tp.value);
return False;
}
w = XCreateSimpleWindow (dpy, RootWindow (dpy, scr), 0, 0, 1, 1, 0, None,
None);
Xutf8SetWMProperties (dpy, w, "steamcompmgr", "steamcompmgr", NULL, 0, NULL, NULL,
NULL);
Atom atomWmCheck = XInternAtom(dpy, "_NET_SUPPORTING_WM_CHECK", False);
XChangeProperty(dpy, root, atomWmCheck,
XA_WINDOW, 32, PropModeReplace, (unsigned char *)&w, 1);
XChangeProperty(dpy, w, atomWmCheck,
XA_WINDOW, 32, PropModeReplace, (unsigned char *)&w, 1);
Atom supportedAtoms[] = {
XInternAtom(dpy, "_NET_WM_STATE", False),
XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False),
XInternAtom(dpy, "_NET_WM_STATE_SKIP_TASKBAR", False),
XInternAtom(dpy, "_NET_WM_STATE_SKIP_PAGER", False),
XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False),
};
XChangeProperty(dpy, root, XInternAtom(dpy, "_NET_SUPPORTED", False),
XA_ATOM, 32, PropModeAppend, (unsigned char *)supportedAtoms,
sizeof(supportedAtoms) / sizeof(supportedAtoms[0]));
XSetSelectionOwner (dpy, a, w, 0);
ourWindow = w;
return True;
}
static void
register_systray(Display *dpy)
{
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", scr);
Atom net_system_tray = XInternAtom(dpy, net_system_tray_name, False);
XSetSelectionOwner(dpy, net_system_tray, ourWindow, 0);
}
void handle_done_commits( void )
{
std::lock_guard<std::mutex> lock( listCommitsDoneLock );
// very fast loop yes
for ( uint32_t i = 0; i < listCommitsDone.size(); i++ )
{
bool bFoundWindow = false;
for ( win *w = list; w; w = w->next )
{
uint32_t j;
for ( j = 0; j < w->commit_queue.size(); j++ )
{
if ( w->commit_queue[ j ].commitID == listCommitsDone[ i ] )
{
gpuvis_trace_printf( "commit %lu done", w->commit_queue[ j ].commitID );
w->commit_queue[ j ].done = true;
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->id == currentOverlayWindow && w->opacity != TRANSLUCENT )
{
hasRepaint = true;
}
if ( w->id == currentNotificationWindow && w->opacity != TRANSLUCENT )
{
hasRepaint = true;
}
}
// If this is the main plane, repaint
if ( w->id == currentFocusWindow )
{
hasRepaint = true;
}
if ( w->isSteamStreamingClientVideo && currentFocusWin && currentFocusWin->isSteamStreamingClient )
{
hasRepaint = true;
}
break;
}
}
if ( bFoundWindow == true )
{
if ( j > 0 )
{
// we can release all commits prior to done ones
for ( uint32_t k = 0; k < j; k++ )
{
release_commit( w->commit_queue[ k ] );
}
w->commit_queue.erase( w->commit_queue.begin(), w->commit_queue.begin() + j );
}
break;
}
}
}
listCommitsDone.clear();
}
void nudge_steamcompmgr( void )
{
if ( write( g_nudgePipe[ 1 ], "\n", 1 ) < 0 )
perror( "nudge_steamcompmgr: write failed" );
}
void take_screenshot( void )
{
g_bTakeScreenshot = true;
nudge_steamcompmgr();
}
void check_new_wayland_res( void )
{
// 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;
{
std::lock_guard<std::mutex> lock( wayland_commit_lock );
tmp_queue = wayland_commit_queue;
wayland_commit_queue.clear();
}
for ( uint32_t i = 0; i < tmp_queue.size(); i++ )
{
struct wlr_buffer *buf = tmp_queue[ i ].buf;
win *w = find_win( tmp_queue[ i ].surf );
if ( w == nullptr )
{
wlserver_lock();
wlr_buffer_unlock( buf );
wlserver_unlock();
fprintf (stderr, "waylandres but no win\n");
continue;
}
struct wlr_dmabuf_attributes dmabuf = {};
bool result = False;
if ( wlr_buffer_get_dmabuf( buf, &dmabuf ) ) {
result = true;
for ( int i = 0; i < dmabuf.n_planes; i++ ) {
dmabuf.fd[i] = dup( dmabuf.fd[i] );
assert( dmabuf.fd[i] >= 0 );
}
} else {
struct wlr_client_buffer *client_buf = (struct wlr_client_buffer *) buf;
result = wlr_texture_to_dmabuf( client_buf->texture, &dmabuf );
}
assert( result == true );
commit_t newCommit = {};
int fence = dup( dmabuf.fd[ 0 ] );
assert( fence >= 0 );
bool bSuccess = import_commit( buf, &dmabuf, newCommit );
wlr_dmabuf_attributes_finish( &dmabuf );
if ( bSuccess == true )
{
newCommit.commitID = ++maxCommmitID;
w->commit_queue.push_back( newCommit );
}
gpuvis_trace_printf( "pushing wait for commit %lu win %lx", newCommit.commitID, w->id );
{
std::unique_lock< std::mutex > lock( waitListLock );
waitList.push_back( std::make_pair( fence, newCommit.commitID ) );
}
// Wake up commit wait thread if chilling
waitListSem.signal();
}
}
static void
spawn_client( char **argv )
{
// (Don't Lose) The Children
prctl( PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0 );
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 );
}
pid_t pid = fork();
if ( pid < 0 )
perror( "fork failed" );
// Are we in the child?
if ( pid == 0 )
{
// Try to snap back to old priority
if ( g_bNiceCap == true )
{
nice( g_nOldNice - g_nNewNice );
}
// 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" );
execvp( argv[ 0 ], argv );
perror( "execvp failed" );
_exit( 1 );
}
std::thread waitThread([]() {
// 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 )
perror( "steamcompmgr: wait failed" );
break;
}
}
g_bRun = false;
nudge_steamcompmgr();
});
waitThread.detach();
}
static void
dispatch_x11( Display *dpy, MouseCursor *cursor )
{
do {
XEvent ev;
XNextEvent (dpy, &ev);
if ((ev.type & 0x7f) != KeymapNotify)
discard_ignore (dpy, 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 == root)
add_win (dpy, ev.xcreatewindow.window, 0, ev.xcreatewindow.serial);
break;
case ConfigureNotify:
configure_win (dpy, &ev.xconfigure);
break;
case DestroyNotify:
{
win * w = find_win(dpy, ev.xdestroywindow.window);
if (w && w->id == ev.xdestroywindow.window)
destroy_win (dpy, ev.xdestroywindow.window, True, True);
break;
}
case MapNotify:
{
win * w = find_win(dpy, ev.xmap.window);
if (w && w->id == ev.xmap.window)
map_win (dpy, ev.xmap.window, ev.xmap.serial);
break;
}
case UnmapNotify:
{
win * w = find_win(dpy, ev.xunmap.window);
if (w && w->id == ev.xunmap.window)
unmap_win (dpy, ev.xunmap.window, True);
break;
}
case FocusOut:
{
win * w = find_win( dpy, ev.xfocus.window );
// If focus escaped the current desired keyboard focus window, check where it went
if ( w && w->id == currentKeyboardFocusWindow )
{
Window newKeyboardFocus = None;
int nRevertMode = 0;
XGetInputFocus( dpy, &newKeyboardFocus, &nRevertMode );
// Find window or its toplevel parent
win *kbw = find_win( dpy, newKeyboardFocus );
if ( kbw )
{
if ( kbw->id == currentKeyboardFocusWindow )
{
// focus went to a child, this is fine, make note of it in case we need to fix it
currentKeyboardFocusWindow = newKeyboardFocus;
}
else
{
// focus went elsewhere, correct it
XSetInputFocus(dpy, currentKeyboardFocusWindow, RevertToNone, CurrentTime);
}
}
}
break;
}
case ReparentNotify:
if (ev.xreparent.parent == root)
add_win (dpy, ev.xreparent.window, 0, ev.xreparent.serial);
else
{
win * w = find_win(dpy, ev.xreparent.window);
if (w && w->id == ev.xreparent.window)
{
destroy_win (dpy, ev.xreparent.window, False, True);
}
else
{
// If something got reparented _to_ a toplevel window,
// go check for the fullscreen workaround again.
w = find_win(dpy, ev.xreparent.parent);
if (w)
{
get_size_hints(dpy, w);
focusDirty = True;
}
}
}
break;
case CirculateNotify:
circulate_win(dpy, &ev.xcirculate);
break;
case MapRequest:
map_request(dpy, &ev.xmaprequest);
break;
case ConfigureRequest:
configure_request(dpy, &ev.xconfigurerequest);
break;
case CirculateRequest:
circulate_request(dpy, &ev.xcirculaterequest);
break;
case Expose:
break;
case PropertyNotify:
handle_property_notify(dpy, &ev.xproperty);
break;
case ClientMessage:
handle_client_message(dpy, &ev.xclient);
break;
case LeaveNotify:
if (ev.xcrossing.window == currentInputFocusWindow)
{
// This shouldn't happen due to our pointer barriers,
// but there is a known X server bug; warp to last good
// position.
cursor->resetPosition();
}
break;
case MotionNotify:
{
win * w = find_win(dpy, ev.xmotion.window);
if (w && w->id == currentInputFocusWindow)
{
cursor->move(ev.xmotion.x, ev.xmotion.y);
}
break;
}
default:
if (ev.type == damage_event + XDamageNotify)
{
damage_win (dpy, (XDamageNotifyEvent *) &ev);
}
else if (ev.type == xfixes_event + XFixesCursorNotify)
{
cursor->setDirty();
}
break;
}
XFlush(dpy);
} while (XPending (dpy));
}
static bool
dispatch_vblank( int fd )
{
bool vblank = false;
for (;;)
{
uint64_t vblanktime = 0;
ssize_t ret = read( fd, &vblanktime, sizeof( vblanktime ) );
if ( ret < 0 )
{
if ( errno == EAGAIN )
break;
perror( "steamcompmgr: dispatch_vblank: read failed" );
break;
}
uint64_t diff = get_time_in_nanos() - vblanktime;
// give it 1 ms of slack.. maybe too long
if ( diff > 1'000'000ul )
{
gpuvis_trace_printf( "ignored stale vblank" );
}
else
{
gpuvis_trace_printf( "got vblank" );
vblank = true;
}
}
return vblank;
}
static void
dispatch_nudge( int fd )
{
for (;;)
{
static char buf[1024];
if ( read( fd, buf, sizeof(buf) ) < 0 )
{
if ( errno != EAGAIN )
perror(" steamcompmgr: dispatch_nudge: read failed" );
break;
}
}
}
struct rgba_t
{
uint8_t r,g,b,a;
};
static bool
load_mouse_cursor( MouseCursor *cursor, const char *path )
{
int w, h, channels;
rgba_t* data = (rgba_t*)stbi_load(path, &w, &h, &channels, STBI_rgb_alpha);
if (!data)
{
fprintf(stderr, "Failed to open/load cursor file\n");
return false;
}
std::transform(data, data + w * h, data, [](rgba_t x) {
if (x.a == 0)
return rgba_t{};
return rgba_t{ x.b, x.g, x.r, x.a };
});
// Data is freed by XDestroyImage in setCursorImage.
return cursor->setCursorImage((char*)data, w, h);
}
enum event_type {
EVENT_X11,
EVENT_VBLANK,
EVENT_NUDGE,
EVENT_COUNT // keep last
};
const char* g_customCursorPath = nullptr;
void
steamcompmgr_main (int argc, char **argv)
{
Display *dpy;
Window root_return, parent_return;
Window *children;
unsigned int nchildren;
int composite_major, composite_minor;
int xres_major, xres_minor;
int o;
int readyPipeFD = -1;
// Reset getopt() state
optind = 1;
int opt_index = -1;
while ((o = getopt_long(argc, argv, gamescope_optstring, gamescope_options, &opt_index)) != -1)
{
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 'N':
doRender = False;
break;
case 'F':
debugFocus = True;
break;
case 'S':
synchronize = True;
break;
case 'v':
drawDebugInfo = True;
break;
case 'V':
debugEvents = True;
break;
case 'e':
steamMode = True;
break;
case 'c':
alwaysComposite = True;
break;
case 'x':
useXRes = False;
break;
case 'a':
g_customCursorPath = optarg;
break;
default:
break;
}
}
int subCommandArg = -1;
if ( optind < argc )
{
subCommandArg = optind;
}
if ( pipe2( g_nudgePipe, O_CLOEXEC | O_NONBLOCK ) != 0 )
{
perror( "steamcompmgr: pipe failed" );
exit( 1 );
}
const char *pchEnableVkBasalt = getenv( "ENABLE_VKBASALT" );
if ( pchEnableVkBasalt != nullptr && pchEnableVkBasalt[0] == '1' )
{
alwaysComposite = True;
}
dpy = XOpenDisplay ( wlserver_get_nested_display_name() );
if (!dpy)
{
fprintf (stderr, "Can't open display\n");
exit (1);
}
XSetErrorHandler (error);
XSetIOErrorHandler (handle_io_error);
if (synchronize)
XSynchronize (dpy, 1);
scr = DefaultScreen (dpy);
root = RootWindow (dpy, scr);
if (!XRenderQueryExtension (dpy, &render_event, &render_error))
{
fprintf (stderr, "No render extension\n");
exit (1);
}
if (!XQueryExtension (dpy, COMPOSITE_NAME, &composite_opcode,
&composite_event, &composite_error))
{
fprintf (stderr, "No composite extension\n");
exit (1);
}
XCompositeQueryVersion (dpy, &composite_major, &composite_minor);
if (!XDamageQueryExtension (dpy, &damage_event, &damage_error))
{
fprintf (stderr, "No damage extension\n");
exit (1);
}
if (!XFixesQueryExtension (dpy, &xfixes_event, &xfixes_error))
{
fprintf (stderr, "No XFixes extension\n");
exit (1);
}
if (!XShapeQueryExtension (dpy, &xshape_event, &xshape_error))
{
fprintf (stderr, "No XShape extension\n");
exit (1);
}
if (!XFixesQueryExtension (dpy, &xfixes_event, &xfixes_error))
{
fprintf (stderr, "No XFixes extension\n");
exit (1);
}
if (!XResQueryVersion (dpy, &xres_major, &xres_minor))
{
fprintf (stderr, "No XRes extension\n");
exit (1);
}
if (xres_major != 1 || xres_minor < 2)
{
fprintf (stderr, "Unsupported XRes version: have %d.%d, want 1.2\n", xres_major, xres_minor);
exit (1);
}
if (!register_cm(dpy))
{
exit (1);
}
register_systray(dpy);
/* get atoms */
steamAtom = XInternAtom (dpy, STEAM_PROP, False);
steamInputFocusAtom = XInternAtom (dpy, "STEAM_INPUT_FOCUS", False);
steamTouchClickModeAtom = XInternAtom (dpy, "STEAM_TOUCH_CLICK_MODE", False);
gameAtom = XInternAtom (dpy, GAME_PROP, False);
overlayAtom = XInternAtom (dpy, OVERLAY_PROP, False);
opacityAtom = XInternAtom (dpy, OPACITY_PROP, False);
gamesRunningAtom = XInternAtom (dpy, GAMES_RUNNING_PROP, False);
screenScaleAtom = XInternAtom (dpy, SCREEN_SCALE_PROP, False);
screenZoomAtom = XInternAtom (dpy, SCREEN_MAGNIFICATION_PROP, False);
winTypeAtom = XInternAtom (dpy, "_NET_WM_WINDOW_TYPE", False);
winDesktopAtom = XInternAtom (dpy, "_NET_WM_WINDOW_TYPE_DESKTOP", False);
winDockAtom = XInternAtom (dpy, "_NET_WM_WINDOW_TYPE_DOCK", False);
winToolbarAtom = XInternAtom (dpy, "_NET_WM_WINDOW_TYPE_TOOLBAR", False);
winMenuAtom = XInternAtom (dpy, "_NET_WM_WINDOW_TYPE_MENU", False);
winUtilAtom = XInternAtom (dpy, "_NET_WM_WINDOW_TYPE_UTILITY", False);
winSplashAtom = XInternAtom (dpy, "_NET_WM_WINDOW_TYPE_SPLASH", False);
winDialogAtom = XInternAtom (dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False);
winNormalAtom = XInternAtom (dpy, "_NET_WM_WINDOW_TYPE_NORMAL", False);
sizeHintsAtom = XInternAtom (dpy, "WM_NORMAL_HINTS", False);
netWMStateFullscreenAtom = XInternAtom (dpy, "_NET_WM_STATE_FULLSCREEN", False);
activeWindowAtom = XInternAtom (dpy, "_NET_ACTIVE_WINDOW", False);
netWMStateAtom = XInternAtom (dpy, "_NET_WM_STATE", False);
WMTransientForAtom = XInternAtom (dpy, "WM_TRANSIENT_FOR", False);
netWMStateHiddenAtom = XInternAtom (dpy, "_NET_WM_STATE_HIDDEN", False);
netWMStateFocusedAtom = XInternAtom (dpy, "_NET_WM_STATE_FOCUSED", False);
netWMStateSkipTaskbarAtom = XInternAtom (dpy, "_NET_WM_STATE_SKIP_TASKBAR", False);
netWMStateSkipPagerAtom = XInternAtom (dpy, "_NET_WM_STATE_SKIP_PAGER", False);
WLSurfaceIDAtom = XInternAtom (dpy, "WL_SURFACE_ID", False);
WMStateAtom = XInternAtom (dpy, "WM_STATE", False);
utf8StringAtom = XInternAtom (dpy, "UTF8_STRING", False);
netWMNameAtom = XInternAtom (dpy, "_NET_WM_NAME", False);
netSystemTrayOpcodeAtom = XInternAtom (dpy, "_NET_SYSTEM_TRAY_OPCODE", False);
steamStreamingClientAtom = XInternAtom (dpy, "STEAM_STREAMING_CLIENT", False);
steamStreamingClientVideoAtom = XInternAtom (dpy, "STEAM_STREAMING_CLIENT_VIDEO", False);
gamescopeCtrlAppIDAtom = XInternAtom (dpy, "GAMESCOPECTRL_BASELAYER_APPID", False);
WMChangeStateAtom = XInternAtom (dpy, "WM_CHANGE_STATE", False);
root_width = DisplayWidth (dpy, scr);
root_height = DisplayHeight (dpy, scr);
allDamage = None;
clipChanged = True;
int vblankFD = vblank_init();
assert( vblankFD >= 0 );
currentOutputWidth = g_nOutputWidth;
currentOutputHeight = g_nOutputHeight;
XGrabServer (dpy);
if (doRender)
{
XCompositeRedirectSubwindows (dpy, root, CompositeRedirectManual);
}
XSelectInput (dpy, root,
SubstructureNotifyMask|
ExposureMask|
StructureNotifyMask|
SubstructureRedirectMask|
FocusChangeMask|
PointerMotionMask|
LeaveWindowMask|
PropertyChangeMask);
XShapeSelectInput (dpy, root, ShapeNotifyMask);
XFixesSelectCursorInput(dpy, root, XFixesDisplayCursorNotifyMask);
XQueryTree (dpy, root, &root_return, &parent_return, &children, &nchildren);
for (uint32_t i = 0; i < nchildren; i++)
add_win (dpy, children[i], i ? children[i-1] : None, 0);
XFree (children);
XUngrabServer (dpy);
XF86VidModeLockModeSwitch(dpy, scr, True);
std::unique_ptr<MouseCursor> cursor(new MouseCursor(dpy));
if (g_customCursorPath)
{
if (!load_mouse_cursor(cursor.get(), g_customCursorPath))
fprintf(stderr, "Failed to load mouse cursor: %s.\n", g_customCursorPath);
}
gamesRunningCount = get_prop(dpy, root, gamesRunningAtom, 0);
overscanScaleRatio = get_prop(dpy, root, screenScaleAtom, 0xFFFFFFFF) / (double)0xFFFFFFFF;
zoomScaleRatio = get_prop(dpy, root, screenZoomAtom, 0xFFFF) / (double)0xFFFF;
globalScaleRatio = overscanScaleRatio * zoomScaleRatio;
determine_and_apply_focus(dpy, cursor.get());
if ( readyPipeFD != -1 )
{
dprintf( readyPipeFD, "%s %s\n", wlserver_get_nested_display_name(), wlserver_get_wl_display_name() );
close( readyPipeFD );
readyPipeFD = -1;
}
if ( subCommandArg >= 0 )
{
spawn_client( &argv[ subCommandArg ] );
}
std::thread imageWaitThread( imageWaitThreadMain );
imageWaitThread.detach();
struct pollfd pollfds[] = {
[ EVENT_X11 ] = {
.fd = XConnectionNumber( dpy ),
.events = POLLIN,
},
[ EVENT_VBLANK ] = {
.fd = vblankFD,
.events = POLLIN,
},
[ EVENT_NUDGE ] = {
.fd = g_nudgePipe[ 0 ],
.events = POLLIN,
},
};
for (;;)
{
focusDirty = False;
bool vblank = false;
if ( poll( pollfds, EVENT_COUNT, -1 ) < 0)
{
if ( errno == EAGAIN )
continue;
perror( "poll failed" );
break;
}
if ( pollfds[ EVENT_X11 ].revents & POLLHUP )
{
fprintf( stderr, "Lost connection to the X11 server\n" );
break;
}
assert( !( pollfds[ EVENT_VBLANK ].revents & POLLHUP ) );
assert( !( pollfds[ EVENT_NUDGE ].revents & POLLHUP ) );
if ( pollfds[ EVENT_X11 ].revents & POLLIN )
dispatch_x11( dpy, cursor.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;
}
if (focusDirty == True)
{
determine_and_apply_focus(dpy, cursor.get());
hasFocusWindow = currentFocusWindow != None;
sdlwindow_pushupdate();
}
if (doRender)
{
// If our DRM state is out-of-date, refresh it. This might update
// the output size.
if ( BIsNested() == false )
drm_poll_state( &g_DRM );
// 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 )
{
if ( BIsNested() == true )
{
vulkan_remake_swapchain();
while ( !acquire_next_image() )
vulkan_remake_swapchain();
}
else
{
vulkan_remake_output_images();
}
currentOutputWidth = g_nOutputWidth;
currentOutputHeight = g_nOutputHeight;
#if HAVE_PIPEWIRE
nudge_pipewire();
#endif
}
handle_done_commits();
check_new_wayland_res();
if ( ( g_bTakeScreenshot == true || hasRepaint == true ) && vblank == true )
{
paint_all(dpy, cursor.get());
// Consumed the need to repaint here
hasRepaint = false;
}
// 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);
// 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 (fadeOutWindow.id)
{
nudge_steamcompmgr();
}
cursor->updatePosition();
// Ask for a new surface every vblank
if ( vblank == true )
{
for (win *w = list; w; w = w->next)
{
if ( w->surface.wlr != nullptr )
{
// Acknowledge commit once.
wlserver_lock();
if ( w->surface.wlr != nullptr )
{
wlserver_send_frame_done(w->surface.wlr, &now);
}
wlserver_unlock();
}
}
}
vulkan_garbage_collect();
vblank = false;
}
}
imageWaitThreadRun = false;
waitListSem.signal();
if ( statsThreadRun == true )
{
statsThreadRun = false;
statsThreadSem.signal();
}
}