blob: 637c22843871ba545f4fc7c9709768f804aa2d4a [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
227 def map(self):
228 """
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
243 size = self.pitch * self.height
244
245 # mmap.mmap() has a totally different order of arguments in Python
246 # compared to C; check the documentation before altering this
247 # incantation.
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -0700248 self._map = mmap.mmap(self._fd,
249 size,
250 flags=mmap.MAP_SHARED,
251 prot=mmap.PROT_READ,
252 offset=mapDumb.offset)
Corbin Simpson5ff46242014-10-14 13:01:20 -0700253
254 def unmap(self):
255 """
256 Unmap the framebuffer.
257 """
258
259 if self._map:
260 self._map.close()
261 self._map = None
262
263
264def loadDRM():
265 """
266 Load a handle to libdrm.
267
268 In addition to loading, this function also configures the argument and
269 return types of functions.
270 """
271
272 l = cdll.LoadLibrary("libdrm.so")
273
274 l.drmGetVersion.argtypes = [c_int]
275 l.drmGetVersion.restype = POINTER(DrmVersion)
276
277 l.drmFreeVersion.argtypes = [POINTER(DrmVersion)]
278 l.drmFreeVersion.restype = None
279
280 l.drmModeGetResources.argtypes = [c_int]
281 l.drmModeGetResources.restype = POINTER(DrmModeResources)
282
283 l.drmModeFreeResources.argtypes = [POINTER(DrmModeResources)]
284 l.drmModeFreeResources.restype = None
285
286 l.drmModeGetCrtc.argtypes = [c_int, c_uint]
287 l.drmModeGetCrtc.restype = POINTER(DrmModeCrtc)
288
289 l.drmModeFreeCrtc.argtypes = [POINTER(DrmModeCrtc)]
290 l.drmModeFreeCrtc.restype = None
291
292 l.drmModeGetFB.argtypes = [c_int, c_uint]
293 l.drmModeGetFB.restype = POINTER(DrmModeFB)
294
295 l.drmModeFreeFB.argtypes = [POINTER(DrmModeFB)]
296 l.drmModeFreeFB.restype = None
297
298 l.drmIoctl.argtypes = [c_int, c_ulong, c_voidp]
299 l.drmIoctl.restype = c_int
300
301 return l
302
303
304class DRM(object):
305 """
306 A DRM node.
307 """
308
309 def __init__(self, library, fd):
310 self._l = library
311 self._fd = fd
312
313 def __repr__(self):
314 return "<DRM (FD %d)>" % self._fd
315
316 @classmethod
317 def fromHandle(cls, handle):
318 """
319 Create a node from a file handle.
320
321 @param handle: A file-like object backed by a file descriptor.
322 """
323
324 self = cls(loadDRM(), handle.fileno())
325 # We must keep the handle alive, and we cannot trust the caller to
326 # keep it alive for us.
327 self._handle = handle
328 return self
329
330 def version(self):
331 """
332 Obtain the version.
333 """
334
335 v = self._l.drmGetVersion(self._fd).contents
336 v._l = self._l
337 return v
338
339 def resources(self):
340 """
341 Obtain the modesetting resources.
342 """
343
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800344 resources_ptr = self._l.drmModeGetResources(self._fd)
345 if resources_ptr:
346 r = resources_ptr.contents
347 r._fd = self._fd
348 r._l = self._l
349 return r
350
351 return None
Corbin Simpson5ff46242014-10-14 13:01:20 -0700352
353
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800354def drmFromPath(path):
Corbin Simpson5ff46242014-10-14 13:01:20 -0700355 """
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800356 Given a DRM node path, open the corresponding node.
Corbin Simpson5ff46242014-10-14 13:01:20 -0700357
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800358 @param path: The path of the minor node to open.
Corbin Simpson5ff46242014-10-14 13:01:20 -0700359 """
360
Corbin Simpson5ff46242014-10-14 13:01:20 -0700361 handle = open(path)
362 return DRM.fromHandle(handle)
363
364
365def _bgrx24(i):
366 b = ord(next(i))
367 g = ord(next(i))
368 r = ord(next(i))
369 next(i)
370 return r, g, b
371
372
373def _screenshot(image, fb):
374 fb.map()
375 m = fb._map
376 lineLength = fb.width * fb.bpp // 8
377 pitch = fb.pitch
378 pixels = []
379
380 if fb.depth == 24:
381 unformat = _bgrx24
382 else:
383 raise RuntimeError("Couldn't unformat FB: %r" % fb)
384
385 for y in range(fb.height):
386 offset = y * pitch
387 m.seek(offset)
388 channels = m.read(lineLength)
389 ichannels = iter(channels)
390 for x in range(fb.width):
391 rgb = unformat(ichannels)
392 image.putpixel((x, y), rgb)
393
394 fb.unmap()
395
396 return pixels
397
398
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800399_drm = None
400
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -0700401
Ilja H. Friedeld94feb62015-02-17 12:54:52 -0800402def crtcScreenshot(crtc_id=None):
Corbin Simpson5ff46242014-10-14 13:01:20 -0700403 """
404 Take a screenshot, returning an image object.
Corbin Simpsonf2a4b9f2015-01-20 13:36:13 -0800405
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800406 @param crtc_id: The CRTC to screenshot.
Corbin Simpson5ff46242014-10-14 13:01:20 -0700407 """
Corbin Simpson5ff46242014-10-14 13:01:20 -0700408
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800409 global _drm
410 if not _drm:
411 paths = ["/dev/dri/" + n for n in os.listdir("/dev/dri")]
412 for p in paths:
413 d = drmFromPath(p)
Nicolas Boichatf1c0cab2016-03-29 18:24:50 +0800414 if d.resources() and d.resources().count_crtcs > 0:
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800415 _drm = d
416 break
417
418 if _drm:
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -0700419 crtc = _drm.resources().getCrtcRobust(crtc_id)
420 if crtc is not None:
421 framebuffer = crtc.fb()
422 image = Image.new("RGB", (framebuffer.width, framebuffer.height))
423 pixels = _screenshot(image, framebuffer)
424 return image
Yuli Huangc5c4cfb2015-06-04 16:22:31 +0800425
Ilja H. Friedel5f4922c2016-04-27 21:35:09 -0700426 raise RuntimeError(
427 "Unable to take screenshot. There may not be anything on the screen.")