From d8deff3f2347825d5651b02c0c72bb088cf12939 Mon Sep 17 00:00:00 2001 From: Nimetu Date: Thu, 14 Oct 2021 15:09:56 +0300 Subject: [PATCH] Linux multi-monitor fullscreen support --- nel/src/3d/driver/opengl/driver_opengl.h | 3 + nel/src/3d/driver/opengl/driver_opengl_window.cpp | 354 ++++++++++++++++++++- nel/src/3d/driver_user.cpp | 1 + ryzom/client/src/client_cfg.cpp | 8 + ryzom/client/src/client_cfg.h | 2 + ryzom/client/src/init.cpp | 3 + .../src/interface_v3/action_handler_game.cpp | 8 + ryzom/client/src/login.cpp | 1 + ryzom/client/src/main_loop_utilities.cpp | 7 +- ryzom/client/src/misc.cpp | 4 + 10 files changed, 378 insertions(+), 13 deletions(-) diff --git a/nel/src/3d/driver/opengl/driver_opengl.h b/nel/src/3d/driver/opengl/driver_opengl.h index 4941a98d1..ec9b59345 100644 --- a/nel/src/3d/driver/opengl/driver_opengl.h +++ b/nel/src/3d/driver/opengl/driver_opengl.h @@ -999,6 +999,9 @@ private: bool createWindow(const GfxMode& mode); bool destroyWindow(); + // Return monitor info and positon in multi monitor setup or false if monitor not found. + bool getMonitorByName(const std::string &name, sint32 &x, sint32 &y, uint32 &w, uint32 &h) const; + enum EWindowStyle { EWSWindowed, EWSFullscreen }; void setWindowSize(uint32 width, uint32 height); diff --git a/nel/src/3d/driver/opengl/driver_opengl_window.cpp b/nel/src/3d/driver/opengl/driver_opengl_window.cpp index 06f95c38c..35dc433a8 100644 --- a/nel/src/3d/driver/opengl/driver_opengl_window.cpp +++ b/nel/src/3d/driver/opengl/driver_opengl_window.cpp @@ -650,8 +650,6 @@ bool CDriverGL::setDisplay(nlWindow wnd, const GfxMode &mode, bool show, bool re _win = EmptyWindow; - _CurrentMode = mode; - _WindowVisible = false; _Resizable = resizeable; _DestroyWindow = false; @@ -1194,6 +1192,8 @@ bool CDriverGL::saveScreenMode() #ifdef HAVE_XRANDR + // TODO: if using mode switching, save current xrandr mode to _OldSizeID + res = true; if (!res && _xrandr_version > 0) { XRRScreenConfiguration *screen_config = XRRGetScreenInfo(_dpy, RootWindow(_dpy, screen)); @@ -1254,6 +1254,8 @@ bool CDriverGL::restoreScreenMode() #ifdef HAVE_XRANDR + // TODO: if using mode switching, then restore mode from _OldSizeID + res = true; if (!res && _xrandr_version > 0) { Window root = RootWindow(_dpy, screen); @@ -1348,7 +1350,8 @@ bool CDriverGL::setScreenMode(const GfxMode &mode) && mode.Width == previousMode.Width && mode.Height == previousMode.Height && mode.Depth == previousMode.Depth - && mode.Frequency == previousMode.Frequency) + && mode.Frequency == previousMode.Frequency + && mode.DisplayDevice == previousMode.DisplayDevice) return true; #if defined(NL_OS_WINDOWS) @@ -1388,6 +1391,8 @@ bool CDriverGL::setScreenMode(const GfxMode &mode) bool found = false; #ifdef HAVE_XRANDR + // TODO: implement mode switching using xrandr crts + found = true; if (!found && _xrandr_version > 0) { @@ -1911,8 +1916,92 @@ bool CDriverGL::setWindowStyle(EWindowStyle windowStyle) return true; } +bool CDriverGL::getMonitorByName(const std::string &name, sint32 &x, sint32 &y, uint32 &w, uint32 &h) const +{ + bool found = false; + +#if HAVE_XRANDR + int screen = DefaultScreen(_dpy); + + // xrandr 1.5+ + if (_xrandr_version >= 105) + { + int nmonitors = 0; + XRRMonitorInfo *monitor = XRRGetMonitors(_dpy, RootWindow(_dpy, screen), 1, &nmonitors); + if (!monitor) + return false; + + for(sint i = 0; i< nmonitors; ++i) + { + char* pname = XGetAtomName(_dpy, monitor[i].name); + found = (nlstricmp(pname, name) == 0); + XFree(pname); + + if (found) + { + x = monitor[i].x; + y = monitor[i].y; + w = monitor[i].width; + h = monitor[i].height; + + break; + } + } + + XRRFreeMonitors(monitor); + } + else + { + XRRScreenResources *resources = XRRGetScreenResourcesCurrent(_dpy, RootWindow(_dpy, screen)); + if (!resources) + resources = XRRGetScreenResources(_dpy, RootWindow(_dpy, screen)); + + for(uint i = 0; i < resources->noutput; ++i) + { + XRROutputInfo *output = XRRGetOutputInfo(_dpy, resources, resources->outputs[i]); + if (!output) + continue; + + if (output->crtc && output->connection == RR_Connected && nlstricmp(name, output->name) == 0) + { + // physical monitor + XRRCrtcInfo *crtc = XRRGetCrtcInfo(_dpy, resources, output->crtc); + if (crtc) + { + found = true; + x = crtc->x; + y = crtc->y; + + // TODO: test rotation + if (crtc->rotation == RR_Rotate_0 || crtc->rotation == RR_Rotate_180) + { + w = crtc->width; + h = crtc->height; + } + else + { + w = crtc->height; + h = crtc->width; + } + + XRRFreeCrtcInfo(crtc); + } + } + + XRRFreeOutputInfo(output); + + if (found) + break; + } + + XRRFreeScreenResources(resources); + } +#endif + return found; +} + // -------------------------------------------------- -bool CDriverGL::setMode(const GfxMode& mode) +bool CDriverGL::setMode(const GfxMode& amode) { H_AUTO_OGL(CDriverGL_setMode); @@ -1920,6 +2009,10 @@ bool CDriverGL::setMode(const GfxMode& mode) if (!_DestroyWindow) return true; +#if !(HAVE_XRANDR) + const GfxMode &mode = amode; +#endif + #if defined(NL_OS_WINDOWS) // save relative cursor POINT cursorPos; @@ -1929,21 +2022,89 @@ bool CDriverGL::setMode(const GfxMode& mode) BOOL cursorPosOk = isSystemCursorInClientArea() && GetCursorPos(&cursorPos) && ScreenToClient(_win, &cursorPos); + + // FIXME: this probably needs to use _CurrentMode instead of mode sint curX = (sint)cursorPos.x * (sint)mode.Width; sint curY = (sint)cursorPos.y * (sint)mode.Height; #endif +#if HAVE_XRANDR + GfxMode mode = amode; + + if (!mode.Windowed) + { + GfxMode current; + if (!getCurrentScreenMode(current)) + nlinfo("3D: XrandR: Reading active monitor info failed"); + + sint newX = _WindowX; + sint newY = _WindowY; + + // make sure resolution matches requested or currently active monitor's resolution + mode.Width = current.Width; + mode.Height = current.Height; + if (!mode.DisplayDevice.empty()) + { + uint newW = current.Width; + uint newH = current.Height; + if (getMonitorByName(mode.DisplayDevice, newX, newY, newW, newH)) + { + mode.Width = newW; + mode.Height = newH; + } + else + { + nlinfo("3D: XrandR: Reading requested monitor '%s' info failed, using '%s'", mode.DisplayDevice.c_str(), current.DisplayDevice.c_str()); + mode.DisplayDevice = current.DisplayDevice; + } + } + + // switching monitors. + // first move mouse pointer to target monitor and then move window. + // if window is visible, then also restore mouse relative position. + if (!mode.DisplayDevice.empty() && mode.DisplayDevice != current.DisplayDevice) + { + int screen = DefaultScreen(_dpy); + Window root = RootWindow(_dpy, screen); + uint mouseX = mode.Width / 2; + uint mouseY = mode.Height / 2; + + XWindowAttributes xwa; + XGetWindowAttributes(_dpy, _win, &xwa); + if (xwa.map_state != IsUnmapped) + { + Window root_win; + Window child_win; + sint root_x, root_y, win_x, win_y; + uint mask; + + Bool res = XQueryPointer(_dpy, _win, &root_win, &child_win, &root_x, &root_y, &win_x, &win_y, &mask); + if (res) + { + mouseX = (uint)((float)win_x * mode.Width / current.Width); + mouseY = (uint)((float)win_y * mode.Height / current.Height); + } + } + + XWarpPointer(_dpy, None, root, None, None, None, None, newX + mouseX, newY + mouseY); + XMoveWindow(_dpy, _win, newX, newY); + _WindowX = newX; + _WindowY = newY; + } + } +#endif + if (!setScreenMode(mode)) return false; + _CurrentMode.Depth = mode.Depth; + _CurrentMode.Frequency = mode.Frequency; + _CurrentMode.DisplayDevice = mode.DisplayDevice; + // when changing window style, it's possible system change window size too setWindowStyle(mode.Windowed ? EWSWindowed : EWSFullscreen); - - if (!mode.Windowed) - _CurrentMode.Depth = mode.Depth; - - setWindowSize(mode.Width, mode.Height); setWindowPos(_WindowX, _WindowY); + setWindowSize(mode.Width, mode.Height); switch (_CurrentMode.Depth) { @@ -2120,6 +2281,66 @@ bool CDriverGL::getModes(std::vector &modes) int screen = DefaultScreen(_dpy); #if defined(HAVE_XRANDR) + if (_xrandr_version >= 105) + { + int nmonitors = 0; + // virtual monitors + XRRMonitorInfo *monitor = XRRGetMonitors(_dpy, RootWindow(_dpy, screen), 1, &nmonitors); + for(sint i = 0; i < nmonitors; ++i) + { + char * name = XGetAtomName(_dpy, monitor[i].name); + GfxMode mode; + mode.DisplayDevice = name; + mode.Width = monitor[i].width; + mode.Height = monitor[i].height; + mode.Frequency = 0; + modes.push_back(mode); + XFree(name); + } + XRRFreeMonitors(monitor); + } + else + { + XRRScreenResources *resources = XRRGetScreenResourcesCurrent(_dpy, RootWindow(_dpy, screen)); + if (!resources) + resources = XRRGetScreenResources(_dpy, RootWindow(_dpy, screen)); + + std::map resourceModeMap; + for(sint i = 0; i< resources->nmode; ++i) + resourceModeMap.insert(std::make_pair(resources->modes[i].id, i)); + + for(sint i = 0; i < resources->noutput; ++i) + { + XRROutputInfo *output = XRRGetOutputInfo(_dpy, resources, resources->outputs[i]); + if (!output) + continue; + + if (output->crtc && output->connection == RR_Connected) + { + // physical monitor + XRRCrtcInfo *crtc = XRRGetCrtcInfo(_dpy, resources, output->crtc); + if (crtc) + { + std::map::const_iterator it = resourceModeMap.find(crtc->mode); + if (it != resourceModeMap.end()) + { + GfxMode mode; + mode.DisplayDevice = output->name; + mode.Width = resources->modes[it->second].width; + mode.Height = resources->modes[it->second].height; + mode.Frequency = 0; + + modes.push_back(mode); + } + XRRFreeCrtcInfo(crtc); + } + } + XRRFreeOutputInfo(output); + } + + XRRFreeScreenResources(resources); + } + found = modes.size() > 0; if (!found && _xrandr_version >= 100) { XRRScreenConfiguration *screen_config = XRRGetScreenInfo(_dpy, RootWindow(_dpy, screen)); @@ -2255,6 +2476,120 @@ bool CDriverGL::getCurrentScreenMode(GfxMode &mode) int screen = DefaultScreen(_dpy); #ifdef HAVE_XRANDR + int x = 0; + int y = 0; + Window child; + + // get window position so we can compare monitors (or mouse position if window not visible yet) + XWindowAttributes xwa; + XGetWindowAttributes(_dpy, _win, &xwa); + if (xwa.map_state != IsUnmapped) + { + XTranslateCoordinates(_dpy, _win, xwa.root, xwa.x, xwa.y, &x, &y, &child); + } + else + { + sint rx, ry, wx, wy; + uint mask; + Bool res = XQueryPointer(_dpy, RootWindow(_dpy, screen), &child, &child, &rx, &ry, &wx, &wy, &mask); + if (res) + { + x = rx; + y = ry; + } + } + + if (_xrandr_version >= 105) + { + int nmonitors = 0; + XRRMonitorInfo *monitor = XRRGetMonitors(_dpy, RootWindow(_dpy, screen), 1, &nmonitors); + if (monitor) + { + sint bestMatch = -1; + for(sint i = 0; i< nmonitors; ++i) + { + if ((x >= monitor[i].x && x < (monitor[i].x + monitor[i].width) && + y >= monitor[i].y && y < (monitor[i].y + monitor[i].height)) || + (monitor[i].primary && bestMatch == -1)) + { + bestMatch = i; + } + } + + // best match or primary monitor + if (bestMatch != -1) + { + found = true; + char* pname = XGetAtomName(_dpy, monitor[bestMatch].name); + mode.DisplayDevice = pname; + mode.Width = monitor[bestMatch].width; + mode.Height = monitor[bestMatch].height; + mode.Windowed = _CurrentMode.Windowed; + mode.OffScreen = false; + mode.Depth = (uint) DefaultDepth(_dpy, screen); + mode.Frequency = 0; + XFree(pname); + } + + XRRFreeMonitors(monitor); + } + } + else + { + XRRScreenResources *resources = XRRGetScreenResourcesCurrent(_dpy, RootWindow(_dpy, screen)); + if (!resources) + resources = XRRGetScreenResources(_dpy, RootWindow(_dpy, screen)); + + for(uint i = 0; i < resources->noutput; ++i) + { + XRROutputInfo *output = XRRGetOutputInfo(_dpy, resources, resources->outputs[i]); + if (!output) + continue; + + if (output->crtc && output->connection == RR_Connected) + { + XRRCrtcInfo *crtc = XRRGetCrtcInfo(_dpy, resources, output->crtc); + if (crtc) + { + sint width, height; + bool match = false; + + // TODO: test rotation + if (crtc->rotation == RR_Rotate_0 || crtc->rotation == RR_Rotate_180) + { + width = crtc->width; + height = crtc->height; + } + else + { + width = crtc->height; + height = crtc->width; + } + + if (x >= crtc->x && y >= crtc->y && x < (crtc->x + width) && y < (crtc->y + height)) + { + found = true; + mode.DisplayDevice = output->name; + mode.Width = width; + mode.Height = height; + mode.Windowed = _CurrentMode.Windowed; + mode.OffScreen = false; + mode.Depth = (uint) DefaultDepth(_dpy, screen); + mode.Frequency = 0; + } + + XRRFreeCrtcInfo(crtc); + } + } + + XRRFreeOutputInfo(output); + + if (found) + break; + } + + XRRFreeScreenResources(resources); + } if (!found && _xrandr_version > 0) { @@ -2432,7 +2767,6 @@ void CDriverGL::setWindowPos(sint32 x, sint32 y) _DecorationWidth = -1; _DecorationHeight = -1; } - XMoveWindow(_dpy, _win, x, y); } diff --git a/nel/src/3d/driver_user.cpp b/nel/src/3d/driver_user.cpp index a7bd7a332..757d2183f 100644 --- a/nel/src/3d/driver_user.cpp +++ b/nel/src/3d/driver_user.cpp @@ -331,6 +331,7 @@ bool CDriverUser::getCurrentScreenMode(CMode &mode) GfxMode gfxMode; bool res= _Driver->getCurrentScreenMode(gfxMode); mode.Windowed= gfxMode.Windowed; + mode.DisplayDevice= gfxMode.DisplayDevice; mode.Width= gfxMode.Width; mode.Height= gfxMode.Height; mode.Depth= gfxMode.Depth; diff --git a/ryzom/client/src/client_cfg.cpp b/ryzom/client/src/client_cfg.cpp index f67c4e1db..a5034d7ac 100644 --- a/ryzom/client/src/client_cfg.cpp +++ b/ryzom/client/src/client_cfg.cpp @@ -302,6 +302,7 @@ CClientConfig::CClientConfig() SelectedSlot = 0; // Default is slot 0 Windowed = false; // Default is windowed mode. + MonitorName = ""; Width = 0; // Default Width for the window (0 = current screen resolution). Height = 0; // Default Height for the window (0 = current screen resolution). Depth = 32; // Default Bit per Pixel. @@ -847,6 +848,8 @@ void CClientConfig::setValues() } else cfgWarning("Default value used for 'Fullscreen'"); + + READ_STRING_FV(MonitorName); // Width READ_INT_FV(Width) // Height @@ -1941,6 +1944,11 @@ void CClientConfig::serial(NLMISC::IStream &f) f.serial(Light); f.xmlPop(); + f.xmlPushBegin("MonitorName"); + f.xmlPushEnd(); + f.serial(MonitorName); + f.xmlPop(); + f.xmlPushBegin("Windowed"); f.xmlPushEnd(); f.serial(Windowed); diff --git a/ryzom/client/src/client_cfg.h b/ryzom/client/src/client_cfg.h index 22fe46f61..b08843b3b 100644 --- a/ryzom/client/src/client_cfg.h +++ b/ryzom/client/src/client_cfg.h @@ -137,6 +137,8 @@ struct CClientConfig TDriver3D Driver3D; /// Application start in a window or in fullscreen. bool Windowed; + /// Monitor to use for fullscreen + std::string MonitorName; /// Width for the Application. uint16 Width; /// Height for the Application. diff --git a/ryzom/client/src/init.cpp b/ryzom/client/src/init.cpp index 87b16d1bf..a8c9c9837 100644 --- a/ryzom/client/src/init.cpp +++ b/ryzom/client/src/init.cpp @@ -1070,6 +1070,7 @@ void prelogInit() // fullscreen, using monitor resolution mode.Windowed = false; + ClientCfg.MonitorName = mode.DisplayDevice; ClientCfg.Windowed = mode.Windowed; ClientCfg.Width = mode.Width; ClientCfg.Height = mode.Height; @@ -1086,6 +1087,7 @@ void prelogInit() // update client.cfg with detected resolution ClientCfg.writeBool("FullScreen", !ClientCfg.Windowed, true); + ClientCfg.writeString("MonitorName", ClientCfg.MonitorName, true); ClientCfg.writeInt("Width", ClientCfg.Width, true); ClientCfg.writeInt("Height", ClientCfg.Height, true); ClientCfg.writeInt("Depth", ClientCfg.Depth, true); @@ -1095,6 +1097,7 @@ void prelogInit() } else { + mode.DisplayDevice = ClientCfg.MonitorName; mode.Windowed = ClientCfg.Windowed; mode.Width = ClientCfg.Width; mode.Height = ClientCfg.Height; diff --git a/ryzom/client/src/interface_v3/action_handler_game.cpp b/ryzom/client/src/interface_v3/action_handler_game.cpp index f3b5d42ab..0486c2efd 100644 --- a/ryzom/client/src/interface_v3/action_handler_game.cpp +++ b/ryzom/client/src/interface_v3/action_handler_game.cpp @@ -3458,6 +3458,7 @@ class CHandlerGameConfigApply : public IActionHandler { // Get W, H sint w = 1024, h = 768; + string name; { CDBGroupComboBox *pCB = dynamic_cast(CWidgetManager::getInstance()->getElementFromId( GAME_CONFIG_VIDEO_MODES_COMBO )); if( pCB != NULL ) @@ -3467,6 +3468,11 @@ class CHandlerGameConfigApply : public IActionHandler fromString(tmp, w); tmp = vidModeStr.substr(vidModeStr.find('x')+2,vidModeStr.size()); fromString(tmp, h); + + // extract monitor "1024x768 (VGA-1)" + string::size_type pos = vidModeStr.find('('); + if (pos != std::string::npos) + name = vidModeStr.substr(pos + 1, vidModeStr.find(")") - pos - 1); } } @@ -3509,6 +3515,7 @@ class CHandlerGameConfigApply : public IActionHandler ClientCfg.Width = w; ClientCfg.Height = h; + ClientCfg.MonitorName = name; // Write the modified client.cfg ClientCfg.writeBool("FullScreen", bFullscreen); @@ -3517,6 +3524,7 @@ class CHandlerGameConfigApply : public IActionHandler if (bFullscreen) { + ClientCfg.writeString("MonitorName", name, true); ClientCfg.writeInt("Depth", screenMode.Depth); ClientCfg.writeInt("Frequency", freq); } diff --git a/ryzom/client/src/login.cpp b/ryzom/client/src/login.cpp index 7b0377890..da038cdd2 100644 --- a/ryzom/client/src/login.cpp +++ b/ryzom/client/src/login.cpp @@ -2208,6 +2208,7 @@ class CAHLessLod : public IActionHandler REGISTER_ACTION_HANDLER (CAHLessLod, "less_lod"); // *************************************************************************** +// TODO: remove resolution change from login screen class CAHUninitResLod : public IActionHandler { virtual void execute (CCtrlBase * /* pCaller */, const string &/* sParams */) diff --git a/ryzom/client/src/main_loop_utilities.cpp b/ryzom/client/src/main_loop_utilities.cpp index d93e24481..95dcf82c8 100644 --- a/ryzom/client/src/main_loop_utilities.cpp +++ b/ryzom/client/src/main_loop_utilities.cpp @@ -76,7 +76,7 @@ void updateFromClientCfg() // set latest config display mode if not attached if (!StereoDisplayAttached) setVideoMode(UDriver::CMode(ClientCfg.Width, ClientCfg.Height, (uint8)ClientCfg.Depth, - ClientCfg.Windowed, ClientCfg.Frequency)); + ClientCfg.Windowed, ClientCfg.Frequency, -1, ClientCfg.MonitorName)); // force software cursor when attached InitMouseWithCursor(ClientCfg.HardwareCursor && !StereoDisplayAttached); } @@ -87,12 +87,13 @@ void updateFromClientCfg() (ClientCfg.Width != LastClientCfg.Width) || (ClientCfg.Height != LastClientCfg.Height) || (ClientCfg.Depth != LastClientCfg.Depth) || - (ClientCfg.Frequency != LastClientCfg.Frequency)) + (ClientCfg.Frequency != LastClientCfg.Frequency)|| + (ClientCfg.MonitorName != LastClientCfg.MonitorName)) { if (!StereoDisplayAttached) { setVideoMode(UDriver::CMode(ClientCfg.Width, ClientCfg.Height, (uint8)ClientCfg.Depth, - ClientCfg.Windowed, ClientCfg.Frequency)); + ClientCfg.Windowed, ClientCfg.Frequency, -1, ClientCfg.MonitorName)); } } diff --git a/ryzom/client/src/misc.cpp b/ryzom/client/src/misc.cpp index 2653c0e45..7cbeeebc2 100644 --- a/ryzom/client/src/misc.cpp +++ b/ryzom/client/src/misc.cpp @@ -1408,6 +1408,7 @@ bool getRyzomModes(std::vector &videoModes, std::vectorgetModes(videoModes); + // TODO: for resolutions below 1024x768, could use automatic UI scaling like in login/outgame // Remove modes under 1024x768 (outgame ui limitation) and get the unique strings sint i, j; for (i = 0; i < (sint)videoModes.size(); ++i) @@ -1424,6 +1425,9 @@ bool getRyzomModes(std::vector &videoModes, std::vector