Skip to content

Commit 63783a5

Browse files
Fix X11 multi-monitor capture on HiDPI (screen cut/shift, inflated canvas)
x11LegacyScreenshot() multiplied the united logical geometry by a single devicePixelRatio and drew each screen at its logical top-left. When the ratio is not 1 and the monitors have different sizes, the composite is wrong. On the xcb backend the virtual-desktop layout uses native (device) pixel offsets, while each screen reports a logical size (its device size divided by its ratio). Multiplying the whole union by one ratio double-counts the offsets: a 7680x3996 desktop is captured as 10560x5373, and the region the GUI overlay selects is cut or shifted onto the wrong monitor. grabWindow(0) already returns the device-pixel pixmap of each screen, and geometry().topLeft() is the native offset on xcb. So draw each pixmap at its native offset using its own physical size, and never scale. The canvas is the union of those device-pixel rectangles. Tested on X11/awesome, nvidia driver, Xft.dpi 168 (ratio 1.75), three monitors of two sizes (3264x1836 and two 3840x2160 in an L shape). Capture is now pixel-aligned to the framebuffer (per-monitor RMSE < 0.02), against 10560x5373 before.
1 parent b2b60c5 commit 63783a5

1 file changed

Lines changed: 27 additions & 14 deletions

File tree

src/utils/screengrabber.cpp

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -870,26 +870,39 @@ QPixmap ScreenGrabber::x11LegacyScreenshot()
870870
return p;
871871
}
872872

873-
// Composite all screens using logical geometry.
874-
// On i3 (tested) DPR is uniform so we don't need the per-screen
875-
// physical pixel math that the Windows backend does. Not sure if this is
876-
// true for other DE's like xmonad.
877-
QRect totalGeom;
873+
// Composite all screens in device (physical) pixels.
874+
//
875+
// The previous version multiplied the united logical geometry by a single
876+
// devicePixelRatio and placed each screen at its logical top-left. That
877+
// breaks when the ratio is not 1 and the monitors have different sizes: on
878+
// the xcb backend the virtual-desktop layout uses native (device) pixel
879+
// offsets while each screen reports a logical (divided by its ratio) size,
880+
// so multiplying the whole union by one ratio double counts the offsets and
881+
// inflates the canvas (a 7680x3996 desktop became 10560x5373 with
882+
// Xft.dpi 168). It also cuts or shifts the region the GUI overlay selects.
883+
//
884+
// grabWindow(0) already returns the true device-pixel pixmap of each
885+
// screen, and geometry().topLeft() is the native offset on xcb, so place
886+
// every pixmap at its native offset using its own physical size and never
887+
// scale. The canvas is the union of those device-pixel rectangles.
888+
QRect totalRect;
889+
QList<QPair<QRect, QPixmap>> grabs;
890+
grabs.reserve(screens.size());
878891
for (QScreen* s : screens) {
879-
totalGeom = totalGeom.united(s->geometry());
892+
QPixmap p = s->grabWindow(0);
893+
p.setDevicePixelRatio(1.0);
894+
QRect r(s->geometry().topLeft(), p.size());
895+
grabs.append({ r, p });
896+
totalRect = totalRect.united(r);
880897
}
881898

882-
qreal dpr = screens.first()->devicePixelRatio();
883-
QPixmap desktop(qRound(totalGeom.width() * dpr),
884-
qRound(totalGeom.height() * dpr));
885-
desktop.setDevicePixelRatio(dpr);
899+
QPixmap desktop(totalRect.size());
900+
desktop.setDevicePixelRatio(1.0);
886901
desktop.fill(Qt::black);
887902

888903
QPainter painter(&desktop);
889-
for (QScreen* s : screens) {
890-
QPixmap p = s->grabWindow(0);
891-
QPoint offset = s->geometry().topLeft() - totalGeom.topLeft();
892-
painter.drawPixmap(offset, p);
904+
for (const auto& g : grabs) {
905+
painter.drawPixmap(g.first.topLeft() - totalRect.topLeft(), g.second);
893906
}
894907
painter.end();
895908

0 commit comments

Comments
 (0)