blob: 8a5faf46a2c0ede7fcc273d94917e4be5bdfc41e [file] [log] [blame]
Corbin Simpson5ff46242014-10-14 13:01:20 -07001"""
2A wrapper around the Direct Rendering Manager (DRM) library, which itself is a
3wrapper around the Direct Rendering Interface (DRI) between the kernel and
4userland.
5
6Since we are masochists, we use ctypes instead of cffi to load libdrm and
7access several symbols within it. We use Python's file descriptor and mmap
8wrappers.
9
10At some point in the future, cffi could be used, for approximately the same
11cost in lines of code.
12"""
13
14from ctypes import *
15import mmap
16import os
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -070017import subprocess
Corbin Simpson5ff46242014-10-14 13:01:20 -070018
19from PIL import Image
20
21
22class DrmVersion(Structure):
23 """
24 The version of a DRM node.
25 """
26
27 _fields_ = [
28 ("version_major", c_int),
29 ("version_minor", c_int),
30 ("version_patchlevel", c_int),
31 ("name_len", c_int),
32 ("name", c_char_p),
33 ("date_len", c_int),
34 ("date", c_char_p),
35 ("desc_len", c_int),
36 ("desc", c_char_p),
37 ]
38
39 _l = None
40
41 def __repr__(self):
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -070042 return "%s %d.%d.%d (%s) (%s)" % (self.name,
43 self.version_major,
44 self.version_minor,
45 self.version_patchlevel,
46 self.desc,
47 self.date,)
Corbin Simpson5ff46242014-10-14 13:01:20 -070048
49 def __del__(self):
50 if self._l:
51 self._l.drmFreeVersion(self)
52
53
54class DrmModeResources(Structure):
55 """
56 Resources associated with setting modes on a DRM node.
57 """
58
59 _fields_ = [
60 ("count_fbs", c_int),
61 ("fbs", POINTER(c_uint)),
62 ("count_crtcs", c_int),
63 ("crtcs", POINTER(c_uint)),
Ilja H. Friedeld2c9f442015-05-20 23:26:00 -070064 ("count_connectors", c_int),
65 ("connectors", POINTER(c_uint)),
66 ("count_encoders", c_int),
67 ("encoders", POINTER(c_uint)),
68 ("min_width", c_int),
69 ("max_width", c_int),
70 ("min_height", c_int),
71 ("max_height", c_int),
Corbin Simpson5ff46242014-10-14 13:01:20 -070072 ]
73
74 _fd = None
75 _l = None
76
77 def __repr__(self):
78 return "<DRM mode resources>"
79
80 def __del__(self):
81 if self._l:
82 self._l.drmModeFreeResources(self)
83
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -070084 def _wakeup_screen(self):
85 """
86 Send a synchronous dbus message to power on screen.
87 """
88 # Get and process reply to make this synchronous.
89 subprocess.check_output([
90 "dbus-send", "--type=method_call", "--system", "--print-reply",
91 "--dest=org.chromium.PowerManager", "/org/chromium/PowerManager",
92 "org.chromium.PowerManager.HandleUserActivity", "int32:0"
93 ])
94
Ilja H. Friedeld2c9f442015-05-20 23:26:00 -070095 def getValidCrtc(self):
96 for i in xrange(0, self.count_crtcs):
97 crtc_id = self.crtcs[i]
98 crtc = self._l.drmModeGetCrtc(self._fd, crtc_id).contents
99 if crtc.mode_valid:
100 return crtc
101 return None
102
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -0700103 def getCrtc(self, crtc_id):
Corbin Simpson5ff46242014-10-14 13:01:20 -0700104 """
105 Obtain the CRTC at a given index.
106
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800107 @param crtc_id: The CRTC to get.
Corbin Simpson5ff46242014-10-14 13:01:20 -0700108 """
Ilja H. Friedeld2c9f442015-05-20 23:26:00 -0700109 if crtc_id:
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -0700110 return self._l.drmModeGetCrtc(self._fd, crtc_id).contents
111 return self.getValidCrtc()
112
113 def getCrtcRobust(self, crtc_id=None):
114 crtc = self.getCrtc(crtc_id)
115 if crtc is None:
116 self._wakeup_screen()
117 crtc = self.getCrtc(crtc_id)
118 if crtc is not None:
119 crtc._fd = self._fd
120 crtc._l = self._l
Corbin Simpson5ff46242014-10-14 13:01:20 -0700121 return crtc
122
123
124class DrmModeCrtc(Structure):
125 """
126 A DRM modesetting CRTC.
127 """
128
129 _fields_ = [
130 ("crtc_id", c_uint),
131 ("buffer_id", c_uint),
Ilja H. Friedeld2c9f442015-05-20 23:26:00 -0700132 ("x", c_uint),
133 ("y", c_uint),
134 ("width", c_uint),
135 ("height", c_uint),
136 ("mode_valid", c_int),
Corbin Simpson5ff46242014-10-14 13:01:20 -0700137 # XXX incomplete struct!
138 ]
139
140 _fd = None
141 _l = None
142
143 def __repr__(self):
144 return "<CRTC (%d)>" % self.crtc_id
145
146 def __del__(self):
147 if self._l:
148 self._l.drmModeFreeCrtc(self)
149
150 def hasFb(self):
151 """
152 Whether this CRTC has an associated framebuffer.
153 """
154
155 return self.buffer_id != 0
156
157 def fb(self):
158 """
159 Obtain the framebuffer, if one is associated.
160 """
161
162 if self.hasFb():
163 fb = self._l.drmModeGetFB(self._fd, self.buffer_id).contents
164 fb._fd = self._fd
165 fb._l = self._l
166 return fb
Corbin Simpsonf2a4b9f2015-01-20 13:36:13 -0800167 else:
168 raise RuntimeError("CRTC %d doesn't have a framebuffer!" %
Ilja H. Friedeld94feb62015-02-17 12:54:52 -0800169 self.crtc_id)
Corbin Simpson5ff46242014-10-14 13:01:20 -0700170
171
172class drm_mode_map_dumb(Structure):
173 """
174 Request a mapping of a modesetting buffer.
175
176 The map will be "dumb;" it will be accessible via mmap() but very slow.
177 """
178
179 _fields_ = [
180 ("handle", c_uint),
181 ("pad", c_uint),
182 ("offset", c_ulonglong),
183 ]
184
185
186# This constant is not defined in any one header; it is the pieced-together
187# incantation for the ioctl that performs dumb mappings. I would love for this
188# to not have to be here, but it can't be imported from any header easily.
189DRM_IOCTL_MODE_MAP_DUMB = 0xc01064b3
190
191
192class DrmModeFB(Structure):
193 """
194 A DRM modesetting framebuffer.
195 """
196
197 _fields_ = [
198 ("fb_id", c_uint),
199 ("width", c_uint),
200 ("height", c_uint),
201 ("pitch", c_uint),
202 ("bpp", c_uint),
203 ("depth", c_uint),
204 ("handle", c_uint),
205 ]
206
207 _l = None
208 _map = None
209
210 def __repr__(self):
211 s = "<Framebuffer (%dx%d (pitch %d bytes), %d bits/pixel, depth %d)"
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -0700212 vitals = s % (self.width,
213 self.height,
214 self.pitch,
215 self.bpp,
216 self.depth,)
Corbin Simpson5ff46242014-10-14 13:01:20 -0700217 if self._map:
218 tail = " (mapped)>"
219 else:
220 tail = ">"
221 return vitals + tail
222
223 def __del__(self):
224 if self._l:
225 self._l.drmModeFreeFB(self)
226
Haixia Shid0762ed2016-10-11 18:10:00 -0700227 def map(self, size):
Corbin Simpson5ff46242014-10-14 13:01:20 -0700228 """
229 Map the framebuffer.
230 """
231
232 if self._map:
233 return
234
235 mapDumb = drm_mode_map_dumb()
236 mapDumb.handle = self.handle
237
238 rv = self._l.drmIoctl(self._fd, DRM_IOCTL_MODE_MAP_DUMB,
Ilja H. Friedeld94feb62015-02-17 12:54:52 -0800239 pointer(mapDumb))
Corbin Simpson5ff46242014-10-14 13:01:20 -0700240 if rv:
241 raise IOError(rv, os.strerror(rv))
242
Corbin Simpson5ff46242014-10-14 13:01:20 -0700243 # mmap.mmap() has a totally different order of arguments in Python
244 # compared to C; check the documentation before altering this
245 # incantation.
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -0700246 self._map = mmap.mmap(self._fd,
247 size,
248 flags=mmap.MAP_SHARED,
249 prot=mmap.PROT_READ,
250 offset=mapDumb.offset)
Corbin Simpson5ff46242014-10-14 13:01:20 -0700251
252 def unmap(self):
253 """
254 Unmap the framebuffer.
255 """
256
257 if self._map:
258 self._map.close()
259 self._map = None
260
261
262def loadDRM():
263 """
264 Load a handle to libdrm.
265
266 In addition to loading, this function also configures the argument and
267 return types of functions.
268 """
269
270 l = cdll.LoadLibrary("libdrm.so")
271
272 l.drmGetVersion.argtypes = [c_int]
273 l.drmGetVersion.restype = POINTER(DrmVersion)
274
275 l.drmFreeVersion.argtypes = [POINTER(DrmVersion)]
276 l.drmFreeVersion.restype = None
277
278 l.drmModeGetResources.argtypes = [c_int]
279 l.drmModeGetResources.restype = POINTER(DrmModeResources)
280
281 l.drmModeFreeResources.argtypes = [POINTER(DrmModeResources)]
282 l.drmModeFreeResources.restype = None
283
284 l.drmModeGetCrtc.argtypes = [c_int, c_uint]
285 l.drmModeGetCrtc.restype = POINTER(DrmModeCrtc)
286
287 l.drmModeFreeCrtc.argtypes = [POINTER(DrmModeCrtc)]
288 l.drmModeFreeCrtc.restype = None
289
290 l.drmModeGetFB.argtypes = [c_int, c_uint]
291 l.drmModeGetFB.restype = POINTER(DrmModeFB)
292
293 l.drmModeFreeFB.argtypes = [POINTER(DrmModeFB)]
294 l.drmModeFreeFB.restype = None
295
296 l.drmIoctl.argtypes = [c_int, c_ulong, c_voidp]
297 l.drmIoctl.restype = c_int
298
299 return l
300
301
302class DRM(object):
303 """
304 A DRM node.
305 """
306
307 def __init__(self, library, fd):
308 self._l = library
309 self._fd = fd
310
311 def __repr__(self):
312 return "<DRM (FD %d)>" % self._fd
313
314 @classmethod
315 def fromHandle(cls, handle):
316 """
317 Create a node from a file handle.
318
319 @param handle: A file-like object backed by a file descriptor.
320 """
321
322 self = cls(loadDRM(), handle.fileno())
323 # We must keep the handle alive, and we cannot trust the caller to
324 # keep it alive for us.
325 self._handle = handle
326 return self
327
328 def version(self):
329 """
330 Obtain the version.
331 """
332
333 v = self._l.drmGetVersion(self._fd).contents
334 v._l = self._l
335 return v
336
337 def resources(self):
338 """
339 Obtain the modesetting resources.
340 """
341
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800342 resources_ptr = self._l.drmModeGetResources(self._fd)
343 if resources_ptr:
344 r = resources_ptr.contents
345 r._fd = self._fd
346 r._l = self._l
347 return r
348
349 return None
Corbin Simpson5ff46242014-10-14 13:01:20 -0700350
351
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800352def drmFromPath(path):
Corbin Simpson5ff46242014-10-14 13:01:20 -0700353 """
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800354 Given a DRM node path, open the corresponding node.
Corbin Simpson5ff46242014-10-14 13:01:20 -0700355
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800356 @param path: The path of the minor node to open.
Corbin Simpson5ff46242014-10-14 13:01:20 -0700357 """
358
Corbin Simpson5ff46242014-10-14 13:01:20 -0700359 handle = open(path)
360 return DRM.fromHandle(handle)
361
362
363def _bgrx24(i):
364 b = ord(next(i))
365 g = ord(next(i))
366 r = ord(next(i))
367 next(i)
368 return r, g, b
369
370
Haixia Shid0762ed2016-10-11 18:10:00 -0700371def _copyImageBlocklinear(image, fb, unformat):
Haixia Shi90ec4ae2016-10-03 18:30:22 -0700372 gobPitch = 64
373 gobHeight = 128
374 while gobHeight > 8 and gobHeight >= 2 * fb.height:
375 gobHeight //= 2
376 gobSize = gobPitch * gobHeight
377 gobWidth = gobPitch // (fb.bpp // 8)
Haixia Shid0762ed2016-10-11 18:10:00 -0700378
379 gobCountX = (fb.pitch + gobPitch - 1) // gobPitch
380 gobCountY = (fb.height + gobHeight - 1) // gobHeight
381 fb.map(gobCountX * gobCountY * gobSize)
382 m = fb._map
383
Haixia Shi90ec4ae2016-10-03 18:30:22 -0700384 offset = 0
Haixia Shid0762ed2016-10-11 18:10:00 -0700385 for gobY in range(gobCountY):
Haixia Shi90ec4ae2016-10-03 18:30:22 -0700386 gobTop = gobY * gobHeight
Haixia Shid0762ed2016-10-11 18:10:00 -0700387 for gobX in range(gobCountX):
Haixia Shi90ec4ae2016-10-03 18:30:22 -0700388 m.seek(offset)
389 gob = m.read(gobSize)
390 iterGob = iter(gob)
391 gobLeft = gobX * gobWidth
392 for y in range(gobHeight):
393 if gobTop + y >= fb.height:
394 break
395 for x in range(gobWidth):
396 rgb = unformat(iterGob)
397 if gobLeft + x < fb.width:
398 image.putpixel((gobLeft + x, gobTop + y), rgb)
399 offset += gobSize
Haixia Shid0762ed2016-10-11 18:10:00 -0700400 fb.unmap()
Haixia Shi90ec4ae2016-10-03 18:30:22 -0700401
402
Haixia Shid0762ed2016-10-11 18:10:00 -0700403def _copyImageLinear(image, fb, unformat):
404 fb.map(fb.pitch * fb.height)
405 m = fb._map
Corbin Simpson5ff46242014-10-14 13:01:20 -0700406 pitch = fb.pitch
Haixia Shi90ec4ae2016-10-03 18:30:22 -0700407 lineLength = fb.width * fb.bpp // 8
Corbin Simpson5ff46242014-10-14 13:01:20 -0700408 for y in range(fb.height):
409 offset = y * pitch
410 m.seek(offset)
411 channels = m.read(lineLength)
412 ichannels = iter(channels)
413 for x in range(fb.width):
414 rgb = unformat(ichannels)
415 image.putpixel((x, y), rgb)
Haixia Shid0762ed2016-10-11 18:10:00 -0700416 fb.unmap()
Corbin Simpson5ff46242014-10-14 13:01:20 -0700417
Haixia Shi90ec4ae2016-10-03 18:30:22 -0700418
419def _screenshot(drm, image, fb):
Haixia Shi90ec4ae2016-10-03 18:30:22 -0700420 if fb.depth == 24:
421 unformat = _bgrx24
422 else:
423 raise RuntimeError("Couldn't unformat FB: %r" % fb)
424
425 if drm.version().name == "tegra":
Haixia Shid0762ed2016-10-11 18:10:00 -0700426 _copyImageBlocklinear(image, fb, unformat)
Haixia Shi90ec4ae2016-10-03 18:30:22 -0700427 else:
Haixia Shid0762ed2016-10-11 18:10:00 -0700428 _copyImageLinear(image, fb, unformat)
Corbin Simpson5ff46242014-10-14 13:01:20 -0700429
430
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800431_drm = None
432
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -0700433
Ilja H. Friedeld94feb62015-02-17 12:54:52 -0800434def crtcScreenshot(crtc_id=None):
Corbin Simpson5ff46242014-10-14 13:01:20 -0700435 """
436 Take a screenshot, returning an image object.
Corbin Simpsonf2a4b9f2015-01-20 13:36:13 -0800437
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800438 @param crtc_id: The CRTC to screenshot.
Corbin Simpson5ff46242014-10-14 13:01:20 -0700439 """
Corbin Simpson5ff46242014-10-14 13:01:20 -0700440
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800441 global _drm
442 if not _drm:
443 paths = ["/dev/dri/" + n for n in os.listdir("/dev/dri")]
444 for p in paths:
445 d = drmFromPath(p)
Nicolas Boichatf1c0cab2016-03-29 18:24:50 +0800446 if d.resources() and d.resources().count_crtcs > 0:
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800447 _drm = d
448 break
449
450 if _drm:
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -0700451 crtc = _drm.resources().getCrtcRobust(crtc_id)
452 if crtc is not None:
453 framebuffer = crtc.fb()
454 image = Image.new("RGB", (framebuffer.width, framebuffer.height))
Haixia Shid0762ed2016-10-11 18:10:00 -0700455 _screenshot(_drm, image, framebuffer)
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -0700456 return image
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800457
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -0700458 raise RuntimeError(
459 "Unable to take screenshot. There may not be anything on the screen.")