1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qlinuxfbscreen.h" |
5 | #include <QtFbSupport/private/qfbcursor_p.h> |
6 | #include <QtFbSupport/private/qfbwindow_p.h> |
7 | #include <QtCore/QFile> |
8 | #include <QtCore/QRegularExpression> |
9 | #include <QtGui/QPainter> |
10 | |
11 | #include <private/qcore_unix_p.h> // overrides QT_OPEN |
12 | #include <qimage.h> |
13 | #include <qdebug.h> |
14 | |
15 | #include <unistd.h> |
16 | #include <stdlib.h> |
17 | #include <sys/ioctl.h> |
18 | #include <sys/types.h> |
19 | #include <sys/stat.h> |
20 | #include <sys/mman.h> |
21 | #include <linux/kd.h> |
22 | #include <fcntl.h> |
23 | #include <errno.h> |
24 | #include <stdio.h> |
25 | #include <limits.h> |
26 | #include <signal.h> |
27 | |
28 | #include <linux/fb.h> |
29 | |
30 | QT_BEGIN_NAMESPACE |
31 | |
32 | using namespace Qt::StringLiterals; |
33 | |
34 | static int openFramebufferDevice(const QString &dev) |
35 | { |
36 | int fd = -1; |
37 | |
38 | if (access(name: dev.toLatin1().constData(), R_OK|W_OK) == 0) |
39 | fd = QT_OPEN(pathname: dev.toLatin1().constData(), O_RDWR); |
40 | |
41 | if (fd == -1) { |
42 | if (access(name: dev.toLatin1().constData(), R_OK) == 0) |
43 | fd = QT_OPEN(pathname: dev.toLatin1().constData(), O_RDONLY); |
44 | } |
45 | |
46 | return fd; |
47 | } |
48 | |
49 | static int determineDepth(const fb_var_screeninfo &vinfo) |
50 | { |
51 | int depth = vinfo.bits_per_pixel; |
52 | if (depth== 24) { |
53 | depth = vinfo.red.length + vinfo.green.length + vinfo.blue.length; |
54 | if (depth <= 0) |
55 | depth = 24; // reset if color component lengths are not reported |
56 | } else if (depth == 16) { |
57 | depth = vinfo.red.length + vinfo.green.length + vinfo.blue.length; |
58 | if (depth <= 0) |
59 | depth = 16; |
60 | } |
61 | return depth; |
62 | } |
63 | |
64 | static QRect determineGeometry(const fb_var_screeninfo &vinfo, const QRect &userGeometry) |
65 | { |
66 | int xoff = vinfo.xoffset; |
67 | int yoff = vinfo.yoffset; |
68 | int w, h; |
69 | if (userGeometry.isValid()) { |
70 | w = userGeometry.width(); |
71 | h = userGeometry.height(); |
72 | if ((uint)w > vinfo.xres) |
73 | w = vinfo.xres; |
74 | if ((uint)h > vinfo.yres) |
75 | h = vinfo.yres; |
76 | |
77 | int xxoff = userGeometry.x(), yyoff = userGeometry.y(); |
78 | if (xxoff != 0 || yyoff != 0) { |
79 | if (xxoff < 0 || xxoff + w > (int)(vinfo.xres)) |
80 | xxoff = vinfo.xres - w; |
81 | if (yyoff < 0 || yyoff + h > (int)(vinfo.yres)) |
82 | yyoff = vinfo.yres - h; |
83 | xoff += xxoff; |
84 | yoff += yyoff; |
85 | } else { |
86 | xoff += (vinfo.xres - w)/2; |
87 | yoff += (vinfo.yres - h)/2; |
88 | } |
89 | } else { |
90 | w = vinfo.xres; |
91 | h = vinfo.yres; |
92 | } |
93 | |
94 | if (w == 0 || h == 0) { |
95 | qWarning(msg: "Unable to find screen geometry, using 320x240" ); |
96 | w = 320; |
97 | h = 240; |
98 | } |
99 | |
100 | return QRect(xoff, yoff, w, h); |
101 | } |
102 | |
103 | static QSizeF determinePhysicalSize(const fb_var_screeninfo &vinfo, const QSize &mmSize, const QSize &res) |
104 | { |
105 | int mmWidth = mmSize.width(), mmHeight = mmSize.height(); |
106 | |
107 | if (mmWidth <= 0 && mmHeight <= 0) { |
108 | if (vinfo.width != 0 && vinfo.height != 0 |
109 | && vinfo.width != UINT_MAX && vinfo.height != UINT_MAX) { |
110 | mmWidth = vinfo.width; |
111 | mmHeight = vinfo.height; |
112 | } else { |
113 | const int dpi = 100; |
114 | mmWidth = qRound(d: res.width() * 25.4 / dpi); |
115 | mmHeight = qRound(d: res.height() * 25.4 / dpi); |
116 | } |
117 | } else if (mmWidth > 0 && mmHeight <= 0) { |
118 | mmHeight = res.height() * mmWidth/res.width(); |
119 | } else if (mmHeight > 0 && mmWidth <= 0) { |
120 | mmWidth = res.width() * mmHeight/res.height(); |
121 | } |
122 | |
123 | return QSize(mmWidth, mmHeight); |
124 | } |
125 | |
126 | static QImage::Format determineFormat(const fb_var_screeninfo &info, int depth) |
127 | { |
128 | const fb_bitfield rgba[4] = { info.red, info.green, |
129 | info.blue, info.transp }; |
130 | |
131 | QImage::Format format = QImage::Format_Invalid; |
132 | |
133 | switch (depth) { |
134 | case 32: { |
135 | const fb_bitfield argb8888[4] = {{.offset: 16, .length: 8, .msb_right: 0}, {.offset: 8, .length: 8, .msb_right: 0}, |
136 | {.offset: 0, .length: 8, .msb_right: 0}, {.offset: 24, .length: 8, .msb_right: 0}}; |
137 | const fb_bitfield abgr8888[4] = {{.offset: 0, .length: 8, .msb_right: 0}, {.offset: 8, .length: 8, .msb_right: 0}, |
138 | {.offset: 16, .length: 8, .msb_right: 0}, {.offset: 24, .length: 8, .msb_right: 0}}; |
139 | if (memcmp(s1: rgba, s2: argb8888, n: 4 * sizeof(fb_bitfield)) == 0) { |
140 | format = QImage::Format_ARGB32; |
141 | } else if (memcmp(s1: rgba, s2: argb8888, n: 3 * sizeof(fb_bitfield)) == 0) { |
142 | format = QImage::Format_RGB32; |
143 | } else if (memcmp(s1: rgba, s2: abgr8888, n: 3 * sizeof(fb_bitfield)) == 0) { |
144 | format = QImage::Format_RGB32; |
145 | // pixeltype = BGRPixel; |
146 | } |
147 | break; |
148 | } |
149 | case 24: { |
150 | const fb_bitfield rgb888[4] = {{.offset: 16, .length: 8, .msb_right: 0}, {.offset: 8, .length: 8, .msb_right: 0}, |
151 | {.offset: 0, .length: 8, .msb_right: 0}, {.offset: 0, .length: 0, .msb_right: 0}}; |
152 | const fb_bitfield bgr888[4] = {{.offset: 0, .length: 8, .msb_right: 0}, {.offset: 8, .length: 8, .msb_right: 0}, |
153 | {.offset: 16, .length: 8, .msb_right: 0}, {.offset: 0, .length: 0, .msb_right: 0}}; |
154 | if (memcmp(s1: rgba, s2: rgb888, n: 3 * sizeof(fb_bitfield)) == 0) { |
155 | format = QImage::Format_RGB888; |
156 | } else if (memcmp(s1: rgba, s2: bgr888, n: 3 * sizeof(fb_bitfield)) == 0) { |
157 | format = QImage::Format_BGR888; |
158 | // pixeltype = BGRPixel; |
159 | } |
160 | break; |
161 | } |
162 | case 18: { |
163 | const fb_bitfield rgb666[4] = {{.offset: 12, .length: 6, .msb_right: 0}, {.offset: 6, .length: 6, .msb_right: 0}, |
164 | {.offset: 0, .length: 6, .msb_right: 0}, {.offset: 0, .length: 0, .msb_right: 0}}; |
165 | if (memcmp(s1: rgba, s2: rgb666, n: 3 * sizeof(fb_bitfield)) == 0) |
166 | format = QImage::Format_RGB666; |
167 | break; |
168 | } |
169 | case 16: { |
170 | const fb_bitfield rgb565[4] = {{.offset: 11, .length: 5, .msb_right: 0}, {.offset: 5, .length: 6, .msb_right: 0}, |
171 | {.offset: 0, .length: 5, .msb_right: 0}, {.offset: 0, .length: 0, .msb_right: 0}}; |
172 | const fb_bitfield bgr565[4] = {{.offset: 0, .length: 5, .msb_right: 0}, {.offset: 5, .length: 6, .msb_right: 0}, |
173 | {.offset: 11, .length: 5, .msb_right: 0}, {.offset: 0, .length: 0, .msb_right: 0}}; |
174 | if (memcmp(s1: rgba, s2: rgb565, n: 3 * sizeof(fb_bitfield)) == 0) { |
175 | format = QImage::Format_RGB16; |
176 | } else if (memcmp(s1: rgba, s2: bgr565, n: 3 * sizeof(fb_bitfield)) == 0) { |
177 | format = QImage::Format_RGB16; |
178 | // pixeltype = BGRPixel; |
179 | } |
180 | break; |
181 | } |
182 | case 15: { |
183 | const fb_bitfield rgb1555[4] = {{.offset: 10, .length: 5, .msb_right: 0}, {.offset: 5, .length: 5, .msb_right: 0}, |
184 | {.offset: 0, .length: 5, .msb_right: 0}, {.offset: 15, .length: 1, .msb_right: 0}}; |
185 | const fb_bitfield bgr1555[4] = {{.offset: 0, .length: 5, .msb_right: 0}, {.offset: 5, .length: 5, .msb_right: 0}, |
186 | {.offset: 10, .length: 5, .msb_right: 0}, {.offset: 15, .length: 1, .msb_right: 0}}; |
187 | if (memcmp(s1: rgba, s2: rgb1555, n: 3 * sizeof(fb_bitfield)) == 0) { |
188 | format = QImage::Format_RGB555; |
189 | } else if (memcmp(s1: rgba, s2: bgr1555, n: 3 * sizeof(fb_bitfield)) == 0) { |
190 | format = QImage::Format_RGB555; |
191 | // pixeltype = BGRPixel; |
192 | } |
193 | break; |
194 | } |
195 | case 12: { |
196 | const fb_bitfield rgb444[4] = {{.offset: 8, .length: 4, .msb_right: 0}, {.offset: 4, .length: 4, .msb_right: 0}, |
197 | {.offset: 0, .length: 4, .msb_right: 0}, {.offset: 0, .length: 0, .msb_right: 0}}; |
198 | if (memcmp(s1: rgba, s2: rgb444, n: 3 * sizeof(fb_bitfield)) == 0) |
199 | format = QImage::Format_RGB444; |
200 | break; |
201 | } |
202 | case 8: |
203 | break; |
204 | case 1: |
205 | format = QImage::Format_Mono; //###: LSB??? |
206 | break; |
207 | default: |
208 | break; |
209 | } |
210 | |
211 | return format; |
212 | } |
213 | |
214 | static int openTtyDevice(const QString &device) |
215 | { |
216 | const char *const devs[] = { "/dev/tty0" , "/dev/tty" , "/dev/console" , nullptr }; |
217 | |
218 | int fd = -1; |
219 | if (device.isEmpty()) { |
220 | for (const char * const *dev = devs; *dev; ++dev) { |
221 | fd = QT_OPEN(pathname: *dev, O_RDWR); |
222 | if (fd != -1) |
223 | break; |
224 | } |
225 | } else { |
226 | fd = QT_OPEN(pathname: QFile::encodeName(fileName: device).constData(), O_RDWR); |
227 | } |
228 | |
229 | return fd; |
230 | } |
231 | |
232 | static void switchToGraphicsMode(int ttyfd, bool doSwitch, int *oldMode) |
233 | { |
234 | // Do not warn if the switch fails: the ioctl fails when launching from a |
235 | // remote console and there is nothing we can do about it. The matching |
236 | // call in resetTty should at least fail then, too, so we do no harm. |
237 | if (ioctl(fd: ttyfd, KDGETMODE, oldMode) == 0) { |
238 | if (doSwitch && *oldMode != KD_GRAPHICS) |
239 | ioctl(fd: ttyfd, KDSETMODE, KD_GRAPHICS); |
240 | } |
241 | } |
242 | |
243 | static void resetTty(int ttyfd, int oldMode) |
244 | { |
245 | ioctl(fd: ttyfd, KDSETMODE, oldMode); |
246 | |
247 | QT_CLOSE(fd: ttyfd); |
248 | } |
249 | |
250 | static void blankScreen(int fd, bool on) |
251 | { |
252 | ioctl(fd: fd, FBIOBLANK, on ? VESA_POWERDOWN : VESA_NO_BLANKING); |
253 | } |
254 | |
255 | QLinuxFbScreen::QLinuxFbScreen(const QStringList &args) |
256 | : mArgs(args), mFbFd(-1), mTtyFd(-1), mBlitter(nullptr) |
257 | { |
258 | mMmap.data = nullptr; |
259 | } |
260 | |
261 | QLinuxFbScreen::~QLinuxFbScreen() |
262 | { |
263 | if (mFbFd != -1) { |
264 | if (mMmap.data) |
265 | munmap(addr: mMmap.data - mMmap.offset, len: mMmap.size); |
266 | close(fd: mFbFd); |
267 | } |
268 | |
269 | if (mTtyFd != -1) |
270 | resetTty(ttyfd: mTtyFd, oldMode: mOldTtyMode); |
271 | |
272 | delete mBlitter; |
273 | } |
274 | |
275 | bool QLinuxFbScreen::initialize() |
276 | { |
277 | QRegularExpression ttyRx("tty=(.*)"_L1 ); |
278 | QRegularExpression fbRx("fb=(.*)"_L1 ); |
279 | QRegularExpression mmSizeRx("mmsize=(\\d+)x(\\d+)"_L1 ); |
280 | QRegularExpression sizeRx("size=(\\d+)x(\\d+)"_L1 ); |
281 | QRegularExpression offsetRx("offset=(\\d+)x(\\d+)"_L1 ); |
282 | |
283 | QString fbDevice, ttyDevice; |
284 | QSize userMmSize; |
285 | QRect userGeometry; |
286 | bool doSwitchToGraphicsMode = true; |
287 | |
288 | // Parse arguments |
289 | for (const QString &arg : std::as_const(t&: mArgs)) { |
290 | QRegularExpressionMatch match; |
291 | if (arg == "nographicsmodeswitch"_L1 ) |
292 | doSwitchToGraphicsMode = false; |
293 | else if (arg.contains(re: mmSizeRx, rmatch: &match)) |
294 | userMmSize = QSize(match.captured(nth: 1).toInt(), match.captured(nth: 2).toInt()); |
295 | else if (arg.contains(re: sizeRx, rmatch: &match)) |
296 | userGeometry.setSize(QSize(match.captured(nth: 1).toInt(), match.captured(nth: 2).toInt())); |
297 | else if (arg.contains(re: offsetRx, rmatch: &match)) |
298 | userGeometry.setTopLeft(QPoint(match.captured(nth: 1).toInt(), match.captured(nth: 2).toInt())); |
299 | else if (arg.contains(re: ttyRx, rmatch: &match)) |
300 | ttyDevice = match.captured(nth: 1); |
301 | else if (arg.contains(re: fbRx, rmatch: &match)) |
302 | fbDevice = match.captured(nth: 1); |
303 | } |
304 | |
305 | if (fbDevice.isEmpty()) { |
306 | fbDevice = "/dev/fb0"_L1 ; |
307 | if (!QFile::exists(fileName: fbDevice)) |
308 | fbDevice = "/dev/graphics/fb0"_L1 ; |
309 | if (!QFile::exists(fileName: fbDevice)) { |
310 | qWarning(msg: "Unable to figure out framebuffer device. Specify it manually." ); |
311 | return false; |
312 | } |
313 | } |
314 | |
315 | // Open the device |
316 | mFbFd = openFramebufferDevice(dev: fbDevice); |
317 | if (mFbFd == -1) { |
318 | qErrnoWarning(errno, msg: "Failed to open framebuffer %s" , qPrintable(fbDevice)); |
319 | return false; |
320 | } |
321 | |
322 | // Read the fixed and variable screen information |
323 | fb_fix_screeninfo finfo; |
324 | fb_var_screeninfo vinfo; |
325 | memset(s: &vinfo, c: 0, n: sizeof(vinfo)); |
326 | memset(s: &finfo, c: 0, n: sizeof(finfo)); |
327 | |
328 | if (ioctl(fd: mFbFd, FBIOGET_FSCREENINFO, &finfo) != 0) { |
329 | qErrnoWarning(errno, msg: "Error reading fixed information" ); |
330 | return false; |
331 | } |
332 | |
333 | if (ioctl(fd: mFbFd, FBIOGET_VSCREENINFO, &vinfo)) { |
334 | qErrnoWarning(errno, msg: "Error reading variable information" ); |
335 | return false; |
336 | } |
337 | |
338 | mDepth = determineDepth(vinfo); |
339 | mBytesPerLine = finfo.line_length; |
340 | QRect geometry = determineGeometry(vinfo, userGeometry); |
341 | mGeometry = QRect(QPoint(0, 0), geometry.size()); |
342 | mFormat = determineFormat(info: vinfo, depth: mDepth); |
343 | mPhysicalSize = determinePhysicalSize(vinfo, mmSize: userMmSize, res: geometry.size()); |
344 | |
345 | // mmap the framebuffer |
346 | mMmap.size = finfo.smem_len; |
347 | uchar *data = (unsigned char *)mmap(addr: nullptr, len: mMmap.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd: mFbFd, offset: 0); |
348 | if ((long)data == -1) { |
349 | qErrnoWarning(errno, msg: "Failed to mmap framebuffer" ); |
350 | return false; |
351 | } |
352 | |
353 | mMmap.offset = geometry.y() * mBytesPerLine + geometry.x() * mDepth / 8; |
354 | mMmap.data = data + mMmap.offset; |
355 | |
356 | QFbScreen::initializeCompositor(); |
357 | mFbScreenImage = QImage(mMmap.data, geometry.width(), geometry.height(), mBytesPerLine, mFormat); |
358 | |
359 | mCursor = new QFbCursor(this); |
360 | |
361 | mTtyFd = openTtyDevice(device: ttyDevice); |
362 | if (mTtyFd == -1) |
363 | qErrnoWarning(errno, msg: "Failed to open tty" ); |
364 | |
365 | switchToGraphicsMode(ttyfd: mTtyFd, doSwitch: doSwitchToGraphicsMode, oldMode: &mOldTtyMode); |
366 | blankScreen(fd: mFbFd, on: false); |
367 | |
368 | return true; |
369 | } |
370 | |
371 | QRegion QLinuxFbScreen::doRedraw() |
372 | { |
373 | QRegion touched = QFbScreen::doRedraw(); |
374 | |
375 | if (touched.isEmpty()) |
376 | return touched; |
377 | |
378 | if (!mBlitter) |
379 | mBlitter = new QPainter(&mFbScreenImage); |
380 | |
381 | mBlitter->setCompositionMode(QPainter::CompositionMode_Source); |
382 | for (const QRect &rect : touched) |
383 | mBlitter->drawImage(targetRect: rect, image: mScreenImage, sourceRect: rect); |
384 | |
385 | return touched; |
386 | } |
387 | |
388 | // grabWindow() grabs "from the screen" not from the backingstores. |
389 | // In linuxfb's case it will also include the mouse cursor. |
390 | QPixmap QLinuxFbScreen::grabWindow(WId wid, int x, int y, int width, int height) const |
391 | { |
392 | if (!wid) { |
393 | if (width < 0) |
394 | width = mFbScreenImage.width() - x; |
395 | if (height < 0) |
396 | height = mFbScreenImage.height() - y; |
397 | return QPixmap::fromImage(image: mFbScreenImage).copy(ax: x, ay: y, awidth: width, aheight: height); |
398 | } |
399 | |
400 | QFbWindow *window = windowForId(wid); |
401 | if (window) { |
402 | const QRect geom = window->geometry(); |
403 | if (width < 0) |
404 | width = geom.width() - x; |
405 | if (height < 0) |
406 | height = geom.height() - y; |
407 | QRect rect(geom.topLeft() + QPoint(x, y), QSize(width, height)); |
408 | rect &= window->geometry(); |
409 | return QPixmap::fromImage(image: mFbScreenImage).copy(rect); |
410 | } |
411 | |
412 | return QPixmap(); |
413 | } |
414 | |
415 | QT_END_NAMESPACE |
416 | |
417 | #include "moc_qlinuxfbscreen.cpp" |
418 | |
419 | |