Windows QPA: Force windows to the main screen in certain AMD GPU setups

A multi-screen setup with an AMD adapter set as the 'main display' leads
to using the AMD drivers for OpenGL. This then causes a crash when calling
SetPixelFormat and similar for windows located on another adapter's
screen. This workaround detects the conditions leading to the crash and moves
the window to the main display.

Task-number: QTBUG-50371
Change-Id: I4007c490bdcdc13d6e8bce82983b150aa4930338
Reviewed-by: Friedemann Kleint <Friedemann.Kleint@qt.io>
This commit is contained in:
Andre de la Rocha 2018-08-15 20:35:28 +02:00
parent c9c770d775
commit 38cb72b6fd
3 changed files with 136 additions and 11 deletions

View File

@ -60,6 +60,8 @@
QT_BEGIN_NAMESPACE
static const DWORD VENDOR_ID_AMD = 0x1002;
GpuDescription GpuDescription::detect()
{
typedef IDirect3D9 * (WINAPI *PtrDirect3DCreate9)(UINT);
@ -74,9 +76,16 @@ GpuDescription GpuDescription::detect()
IDirect3D9 *direct3D9 = direct3DCreate9(D3D_SDK_VERSION);
if (!direct3D9)
return result;
D3DADAPTER_IDENTIFIER9 adapterIdentifier;
const HRESULT hr = direct3D9->GetAdapterIdentifier(0, 0, &adapterIdentifier);
direct3D9->Release();
bool isAMD = false;
// Adapter "0" is D3DADAPTER_DEFAULT which returns the default adapter. In
// multi-GPU, multi-screen setups this is the GPU that is associated with
// the "main display" in the Display Settings, and this is the GPU OpenGL
// and D3D uses by default. Therefore querying any additional adapters is
// futile and not useful for our purposes in general, except for
// identifying a few special cases later on.
HRESULT hr = direct3D9->GetAdapterIdentifier(0, 0, &adapterIdentifier);
if (SUCCEEDED(hr)) {
result.vendorId = adapterIdentifier.VendorId;
result.deviceId = adapterIdentifier.DeviceId;
@ -90,7 +99,37 @@ GpuDescription GpuDescription::detect()
result.driverVersion = QVersionNumber(version);
result.driverName = adapterIdentifier.Driver;
result.description = adapterIdentifier.Description;
isAMD = result.vendorId == VENDOR_ID_AMD;
}
// Detect QTBUG-50371 (having AMD as the default adapter results in a crash
// when starting apps on a screen connected to the Intel card) by looking
// for a default AMD adapter and an additional non-AMD one.
if (isAMD) {
const UINT adapterCount = direct3D9->GetAdapterCount();
for (UINT adp = 1; adp < adapterCount; ++adp) {
hr = direct3D9->GetAdapterIdentifier(adp, 0, &adapterIdentifier);
if (SUCCEEDED(hr)) {
if (adapterIdentifier.VendorId != VENDOR_ID_AMD) {
// Bingo. Now figure out the display for the AMD card.
DISPLAY_DEVICE dd;
memset(&dd, 0, sizeof(dd));
dd.cb = sizeof(dd);
for (int dev = 0; EnumDisplayDevices(nullptr, dev, &dd, 0); ++dev) {
if (dd.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) {
// DeviceName is something like \\.\DISPLAY1 which can be used to
// match with the MONITORINFOEX::szDevice queried by QWindowsScreen.
result.gpuSuitableScreen = QString::fromWCharArray(dd.DeviceName);
break;
}
}
break;
}
}
}
}
direct3D9->Release();
return result;
}
@ -103,7 +142,8 @@ QDebug operator<<(QDebug d, const GpuDescription &gd)
<< ", deviceId=" << gd.deviceId << ", subSysId=" << gd.subSysId
<< dec << noshowbase << ", revision=" << gd.revision
<< ", driver: " << gd.driverName
<< ", version=" << gd.driverVersion << ", " << gd.description << ')';
<< ", version=" << gd.driverVersion << ", " << gd.description
<< gd.gpuSuitableScreen << ')';
return d;
}
#endif // !QT_NO_DEBUG_STREAM
@ -113,15 +153,17 @@ QString GpuDescription::toString() const
{
QString result;
QTextStream str(&result);
str << " Card name: " << description
<< "\n Driver Name: " << driverName
<< "\n Driver Version: " << driverVersion.toString()
<< "\n Vendor ID: 0x" << qSetPadChar(QLatin1Char('0'))
str << " Card name : " << description
<< "\n Driver Name : " << driverName
<< "\n Driver Version : " << driverVersion.toString()
<< "\n Vendor ID : 0x" << qSetPadChar(QLatin1Char('0'))
<< uppercasedigits << hex << qSetFieldWidth(4) << vendorId
<< "\n Device ID: 0x" << qSetFieldWidth(4) << deviceId
<< "\n SubSys ID: 0x" << qSetFieldWidth(8) << subSysId
<< "\n Revision ID: 0x" << qSetFieldWidth(4) << revision
<< "\n Device ID : 0x" << qSetFieldWidth(4) << deviceId
<< "\n SubSys ID : 0x" << qSetFieldWidth(8) << subSysId
<< "\n Revision ID : 0x" << qSetFieldWidth(4) << revision
<< dec;
if (!gpuSuitableScreen.isEmpty())
str << "\nGL windows forced to screen: " << gpuSuitableScreen;
return result;
}

View File

@ -62,6 +62,7 @@ struct GpuDescription
QVersionNumber driverVersion;
QByteArray driverName;
QByteArray description;
QString gpuSuitableScreen;
};
#ifndef QT_NO_DEBUG_STREAM

View File

@ -58,6 +58,7 @@
#else
# include "qwindowsopenglcontext.h"
#endif
#include "qwindowsopengltester.h"
#ifdef QT_NO_CURSOR
# include "qwindowscursor.h"
#endif
@ -541,6 +542,84 @@ static inline void fixTopLevelWindowFlags(Qt::WindowFlags &flags)
flags |= Qt::FramelessWindowHint;
}
static QScreen *screenForName(const QWindow *w, const QString &name)
{
QScreen *winScreen = w ? w->screen() : QGuiApplication::primaryScreen();
if (winScreen && winScreen->name() != name) {
const auto screens = winScreen->virtualSiblings();
for (QScreen *screen : screens) {
if (screen->name() == name)
return screen;
}
}
return winScreen;
}
static QScreen *forcedScreenForGLWindow(const QWindow *w)
{
const QString forceToScreen = GpuDescription::detect().gpuSuitableScreen;
return forceToScreen.isEmpty() ? nullptr : screenForName(w, forceToScreen);
}
static QPoint calcPosition(const QWindow *w, const QWindowCreationContextPtr &context, const QMargins &invMargins)
{
const QPoint orgPos(context->frameX - invMargins.left(), context->frameY - invMargins.top());
if (!w || (!w->isTopLevel() && w->surfaceType() != QWindow::OpenGLSurface))
return orgPos;
// Workaround for QTBUG-50371
const QScreen *screenForGL = forcedScreenForGLWindow(w);
if (!screenForGL)
return orgPos;
const QPoint posFrame(context->frameX, context->frameY);
const QMargins margins = context->margins;
const QRect scrGeo = screenForGL->handle()->availableGeometry();
// Point is already in the required screen.
if (scrGeo.contains(orgPos))
return orgPos;
// If the visible part of the window is already in the
// required screen, just ignore the invisible offset.
if (scrGeo.contains(posFrame))
return posFrame;
// Find the original screen containing the coordinates.
const QList<QScreen *> screens = screenForGL->virtualSiblings();
const QScreen *orgScreen = nullptr;
for (QScreen *screen : screens) {
if (screen->handle()->availableGeometry().contains(posFrame)) {
orgScreen = screen;
break;
}
}
const QPoint ctPos = QPoint(qMax(scrGeo.left(), scrGeo.center().x()
+ (margins.right() - margins.left() - context->frameWidth)/2),
qMax(scrGeo.top(), scrGeo.center().y()
+ (margins.bottom() - margins.top() - context->frameHeight)/2));
// If initial coordinates were outside all screens, center the window on the required screen.
if (!orgScreen)
return ctPos;
const QRect orgGeo = orgScreen->handle()->availableGeometry();
const QRect orgFrame(QPoint(context->frameX, context->frameY),
QSize(context->frameWidth, context->frameHeight));
// Window would be centered on orgScreen. Center it on the required screen.
if (orgGeo.center() == (orgFrame - margins).center())
return ctPos;
// Transform the coordinates to map them into the required screen.
const QPoint newPos(scrGeo.left() + ((posFrame.x() - orgGeo.left()) * scrGeo.width()) / orgGeo.width(),
scrGeo.top() + ((posFrame.y() - orgGeo.top()) * scrGeo.height()) / orgGeo.height());
const QPoint newPosNoMargin(newPos.x() - invMargins.left(), newPos.y() - invMargins.top());
return scrGeo.contains(newPosNoMargin) ? newPosNoMargin : newPos;
}
void WindowCreationData::fromWindow(const QWindow *w, const Qt::WindowFlags flagsIn,
unsigned creationFlags)
{
@ -688,9 +767,12 @@ QWindowsWindowData
<< " custom margins: " << context->customMargins
<< " invisible margins: " << invMargins;
QPoint pos = calcPosition(w, context, invMargins);
result.hwnd = CreateWindowEx(exStyle, classNameUtf16, titleUtf16,
style,
context->frameX - invMargins.left(), context->frameY - invMargins.top(),
pos.x(), pos.y(),
context->frameWidth, context->frameHeight,
parentHandle, NULL, appinst, NULL);
qCDebug(lcQpaWindows).nospace()