blob: fdadfb808b95dda2f1d4e73e239db072ba142fae [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
17
18from PIL import Image
19
20
21class DrmVersion(Structure):
22 """
23 The version of a DRM node.
24 """
25
26 _fields_ = [
27 ("version_major", c_int),
28 ("version_minor", c_int),
29 ("version_patchlevel", c_int),
30 ("name_len", c_int),
31 ("name", c_char_p),
32 ("date_len", c_int),
33 ("date", c_char_p),
34 ("desc_len", c_int),
35 ("desc", c_char_p),
36 ]
37
38 _l = None
39
40 def __repr__(self):
41 return "%s %d.%d.%d (%s) (%s)" % (
42 self.name,
43 self.version_major,
44 self.version_minor,
45 self.version_patchlevel,
46 self.desc,
47 self.date,
48 )
49
50 def __del__(self):
51 if self._l:
52 self._l.drmFreeVersion(self)
53
54
55class DrmModeResources(Structure):
56 """
57 Resources associated with setting modes on a DRM node.
58 """
59
60 _fields_ = [
61 ("count_fbs", c_int),
62 ("fbs", POINTER(c_uint)),
63 ("count_crtcs", c_int),
64 ("crtcs", POINTER(c_uint)),
65 # XXX incomplete struct!
66 ]
67
68 _fd = None
69 _l = None
70
71 def __repr__(self):
72 return "<DRM mode resources>"
73
74 def __del__(self):
75 if self._l:
76 self._l.drmModeFreeResources(self)
77
78 def getCrtc(self, index):
79 """
80 Obtain the CRTC at a given index.
81
82 @param index: The CRTC to get.
83 """
84
85 if not 0 <= index < self.count_crtcs:
86 raise IndexError("CRTC index out of range")
87
88 crtc = self._l.drmModeGetCrtc(self._fd, self.crtcs[index]).contents
89 crtc._fd = self._fd
90 crtc._l = self._l
91 return crtc
92
93
94class DrmModeCrtc(Structure):
95 """
96 A DRM modesetting CRTC.
97 """
98
99 _fields_ = [
100 ("crtc_id", c_uint),
101 ("buffer_id", c_uint),
102 # XXX incomplete struct!
103 ]
104
105 _fd = None
106 _l = None
107
108 def __repr__(self):
109 return "<CRTC (%d)>" % self.crtc_id
110
111 def __del__(self):
112 if self._l:
113 self._l.drmModeFreeCrtc(self)
114
115 def hasFb(self):
116 """
117 Whether this CRTC has an associated framebuffer.
118 """
119
120 return self.buffer_id != 0
121
122 def fb(self):
123 """
124 Obtain the framebuffer, if one is associated.
125 """
126
127 if self.hasFb():
128 fb = self._l.drmModeGetFB(self._fd, self.buffer_id).contents
129 fb._fd = self._fd
130 fb._l = self._l
131 return fb
Corbin Simpsonf2a4b9f2015-01-20 13:36:13 -0800132 else:
133 raise RuntimeError("CRTC %d doesn't have a framebuffer!" %
134 self.crtc_id)
Corbin Simpson5ff46242014-10-14 13:01:20 -0700135
136
137class drm_mode_map_dumb(Structure):
138 """
139 Request a mapping of a modesetting buffer.
140
141 The map will be "dumb;" it will be accessible via mmap() but very slow.
142 """
143
144 _fields_ = [
145 ("handle", c_uint),
146 ("pad", c_uint),
147 ("offset", c_ulonglong),
148 ]
149
150
151# This constant is not defined in any one header; it is the pieced-together
152# incantation for the ioctl that performs dumb mappings. I would love for this
153# to not have to be here, but it can't be imported from any header easily.
154DRM_IOCTL_MODE_MAP_DUMB = 0xc01064b3
155
156
157class DrmModeFB(Structure):
158 """
159 A DRM modesetting framebuffer.
160 """
161
162 _fields_ = [
163 ("fb_id", c_uint),
164 ("width", c_uint),
165 ("height", c_uint),
166 ("pitch", c_uint),
167 ("bpp", c_uint),
168 ("depth", c_uint),
169 ("handle", c_uint),
170 ]
171
172 _l = None
173 _map = None
174
175 def __repr__(self):
176 s = "<Framebuffer (%dx%d (pitch %d bytes), %d bits/pixel, depth %d)"
177 vitals = s % (
178 self.width,
179 self.height,
180 self.pitch,
181 self.bpp,
182 self.depth,
183 )
184 if self._map:
185 tail = " (mapped)>"
186 else:
187 tail = ">"
188 return vitals + tail
189
190 def __del__(self):
191 if self._l:
192 self._l.drmModeFreeFB(self)
193
194 def map(self):
195 """
196 Map the framebuffer.
197 """
198
199 if self._map:
200 return
201
202 mapDumb = drm_mode_map_dumb()
203 mapDumb.handle = self.handle
204
205 rv = self._l.drmIoctl(self._fd, DRM_IOCTL_MODE_MAP_DUMB,
206 pointer(mapDumb))
207 if rv:
208 raise IOError(rv, os.strerror(rv))
209
210 size = self.pitch * self.height
211
212 # mmap.mmap() has a totally different order of arguments in Python
213 # compared to C; check the documentation before altering this
214 # incantation.
215 self._map = mmap.mmap(self._fd, size, flags=mmap.MAP_SHARED,
216 prot=mmap.PROT_READ, offset=mapDumb.offset)
217
218 def unmap(self):
219 """
220 Unmap the framebuffer.
221 """
222
223 if self._map:
224 self._map.close()
225 self._map = None
226
227
228def loadDRM():
229 """
230 Load a handle to libdrm.
231
232 In addition to loading, this function also configures the argument and
233 return types of functions.
234 """
235
236 l = cdll.LoadLibrary("libdrm.so")
237
238 l.drmGetVersion.argtypes = [c_int]
239 l.drmGetVersion.restype = POINTER(DrmVersion)
240
241 l.drmFreeVersion.argtypes = [POINTER(DrmVersion)]
242 l.drmFreeVersion.restype = None
243
244 l.drmModeGetResources.argtypes = [c_int]
245 l.drmModeGetResources.restype = POINTER(DrmModeResources)
246
247 l.drmModeFreeResources.argtypes = [POINTER(DrmModeResources)]
248 l.drmModeFreeResources.restype = None
249
250 l.drmModeGetCrtc.argtypes = [c_int, c_uint]
251 l.drmModeGetCrtc.restype = POINTER(DrmModeCrtc)
252
253 l.drmModeFreeCrtc.argtypes = [POINTER(DrmModeCrtc)]
254 l.drmModeFreeCrtc.restype = None
255
256 l.drmModeGetFB.argtypes = [c_int, c_uint]
257 l.drmModeGetFB.restype = POINTER(DrmModeFB)
258
259 l.drmModeFreeFB.argtypes = [POINTER(DrmModeFB)]
260 l.drmModeFreeFB.restype = None
261
262 l.drmIoctl.argtypes = [c_int, c_ulong, c_voidp]
263 l.drmIoctl.restype = c_int
264
265 return l
266
267
268class DRM(object):
269 """
270 A DRM node.
271 """
272
273 def __init__(self, library, fd):
274 self._l = library
275 self._fd = fd
276
277 def __repr__(self):
278 return "<DRM (FD %d)>" % self._fd
279
280 @classmethod
281 def fromHandle(cls, handle):
282 """
283 Create a node from a file handle.
284
285 @param handle: A file-like object backed by a file descriptor.
286 """
287
288 self = cls(loadDRM(), handle.fileno())
289 # We must keep the handle alive, and we cannot trust the caller to
290 # keep it alive for us.
291 self._handle = handle
292 return self
293
294 def version(self):
295 """
296 Obtain the version.
297 """
298
299 v = self._l.drmGetVersion(self._fd).contents
300 v._l = self._l
301 return v
302
303 def resources(self):
304 """
305 Obtain the modesetting resources.
306 """
307
308 r = self._l.drmModeGetResources(self._fd).contents
309 r._fd = self._fd
310 r._l = self._l
311 return r
312
313
314def drmFromMinor(minor):
315 """
316 Given a DRM node number, open the corresponding node.
317
318 @param minor: The number of the minor node to open.
319 """
320
321 path = "/dev/dri/card%d" % minor
322 handle = open(path)
323 return DRM.fromHandle(handle)
324
325
326def _bgrx24(i):
327 b = ord(next(i))
328 g = ord(next(i))
329 r = ord(next(i))
330 next(i)
331 return r, g, b
332
333
334def _screenshot(image, fb):
335 fb.map()
336 m = fb._map
337 lineLength = fb.width * fb.bpp // 8
338 pitch = fb.pitch
339 pixels = []
340
341 if fb.depth == 24:
342 unformat = _bgrx24
343 else:
344 raise RuntimeError("Couldn't unformat FB: %r" % fb)
345
346 for y in range(fb.height):
347 offset = y * pitch
348 m.seek(offset)
349 channels = m.read(lineLength)
350 ichannels = iter(channels)
351 for x in range(fb.width):
352 rgb = unformat(ichannels)
353 image.putpixel((x, y), rgb)
354
355 fb.unmap()
356
357 return pixels
358
359
Corbin Simpsonf2a4b9f2015-01-20 13:36:13 -0800360def crtcScreenshot(crtc):
Corbin Simpson5ff46242014-10-14 13:01:20 -0700361 """
362 Take a screenshot, returning an image object.
Corbin Simpsonf2a4b9f2015-01-20 13:36:13 -0800363
364 @param crtc: The CRTC to screenshot.
Corbin Simpson5ff46242014-10-14 13:01:20 -0700365 """
366
367 d = drmFromMinor(0)
Corbin Simpsonf2a4b9f2015-01-20 13:36:13 -0800368 fb = d.resources().getCrtc(crtc).fb()
Corbin Simpson5ff46242014-10-14 13:01:20 -0700369 image = Image.new("RGB", (fb.width, fb.height))
370 pixels = _screenshot(image, fb)
371
372 return image