blob: d53e5d152314740f8c8c4a80d72a04e6789b96e6 [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
132
133
134class drm_mode_map_dumb(Structure):
135 """
136 Request a mapping of a modesetting buffer.
137
138 The map will be "dumb;" it will be accessible via mmap() but very slow.
139 """
140
141 _fields_ = [
142 ("handle", c_uint),
143 ("pad", c_uint),
144 ("offset", c_ulonglong),
145 ]
146
147
148# This constant is not defined in any one header; it is the pieced-together
149# incantation for the ioctl that performs dumb mappings. I would love for this
150# to not have to be here, but it can't be imported from any header easily.
151DRM_IOCTL_MODE_MAP_DUMB = 0xc01064b3
152
153
154class DrmModeFB(Structure):
155 """
156 A DRM modesetting framebuffer.
157 """
158
159 _fields_ = [
160 ("fb_id", c_uint),
161 ("width", c_uint),
162 ("height", c_uint),
163 ("pitch", c_uint),
164 ("bpp", c_uint),
165 ("depth", c_uint),
166 ("handle", c_uint),
167 ]
168
169 _l = None
170 _map = None
171
172 def __repr__(self):
173 s = "<Framebuffer (%dx%d (pitch %d bytes), %d bits/pixel, depth %d)"
174 vitals = s % (
175 self.width,
176 self.height,
177 self.pitch,
178 self.bpp,
179 self.depth,
180 )
181 if self._map:
182 tail = " (mapped)>"
183 else:
184 tail = ">"
185 return vitals + tail
186
187 def __del__(self):
188 if self._l:
189 self._l.drmModeFreeFB(self)
190
191 def map(self):
192 """
193 Map the framebuffer.
194 """
195
196 if self._map:
197 return
198
199 mapDumb = drm_mode_map_dumb()
200 mapDumb.handle = self.handle
201
202 rv = self._l.drmIoctl(self._fd, DRM_IOCTL_MODE_MAP_DUMB,
203 pointer(mapDumb))
204 if rv:
205 raise IOError(rv, os.strerror(rv))
206
207 size = self.pitch * self.height
208
209 # mmap.mmap() has a totally different order of arguments in Python
210 # compared to C; check the documentation before altering this
211 # incantation.
212 self._map = mmap.mmap(self._fd, size, flags=mmap.MAP_SHARED,
213 prot=mmap.PROT_READ, offset=mapDumb.offset)
214
215 def unmap(self):
216 """
217 Unmap the framebuffer.
218 """
219
220 if self._map:
221 self._map.close()
222 self._map = None
223
224
225def loadDRM():
226 """
227 Load a handle to libdrm.
228
229 In addition to loading, this function also configures the argument and
230 return types of functions.
231 """
232
233 l = cdll.LoadLibrary("libdrm.so")
234
235 l.drmGetVersion.argtypes = [c_int]
236 l.drmGetVersion.restype = POINTER(DrmVersion)
237
238 l.drmFreeVersion.argtypes = [POINTER(DrmVersion)]
239 l.drmFreeVersion.restype = None
240
241 l.drmModeGetResources.argtypes = [c_int]
242 l.drmModeGetResources.restype = POINTER(DrmModeResources)
243
244 l.drmModeFreeResources.argtypes = [POINTER(DrmModeResources)]
245 l.drmModeFreeResources.restype = None
246
247 l.drmModeGetCrtc.argtypes = [c_int, c_uint]
248 l.drmModeGetCrtc.restype = POINTER(DrmModeCrtc)
249
250 l.drmModeFreeCrtc.argtypes = [POINTER(DrmModeCrtc)]
251 l.drmModeFreeCrtc.restype = None
252
253 l.drmModeGetFB.argtypes = [c_int, c_uint]
254 l.drmModeGetFB.restype = POINTER(DrmModeFB)
255
256 l.drmModeFreeFB.argtypes = [POINTER(DrmModeFB)]
257 l.drmModeFreeFB.restype = None
258
259 l.drmIoctl.argtypes = [c_int, c_ulong, c_voidp]
260 l.drmIoctl.restype = c_int
261
262 return l
263
264
265class DRM(object):
266 """
267 A DRM node.
268 """
269
270 def __init__(self, library, fd):
271 self._l = library
272 self._fd = fd
273
274 def __repr__(self):
275 return "<DRM (FD %d)>" % self._fd
276
277 @classmethod
278 def fromHandle(cls, handle):
279 """
280 Create a node from a file handle.
281
282 @param handle: A file-like object backed by a file descriptor.
283 """
284
285 self = cls(loadDRM(), handle.fileno())
286 # We must keep the handle alive, and we cannot trust the caller to
287 # keep it alive for us.
288 self._handle = handle
289 return self
290
291 def version(self):
292 """
293 Obtain the version.
294 """
295
296 v = self._l.drmGetVersion(self._fd).contents
297 v._l = self._l
298 return v
299
300 def resources(self):
301 """
302 Obtain the modesetting resources.
303 """
304
305 r = self._l.drmModeGetResources(self._fd).contents
306 r._fd = self._fd
307 r._l = self._l
308 return r
309
310
311def drmFromMinor(minor):
312 """
313 Given a DRM node number, open the corresponding node.
314
315 @param minor: The number of the minor node to open.
316 """
317
318 path = "/dev/dri/card%d" % minor
319 handle = open(path)
320 return DRM.fromHandle(handle)
321
322
323def _bgrx24(i):
324 b = ord(next(i))
325 g = ord(next(i))
326 r = ord(next(i))
327 next(i)
328 return r, g, b
329
330
331def _screenshot(image, fb):
332 fb.map()
333 m = fb._map
334 lineLength = fb.width * fb.bpp // 8
335 pitch = fb.pitch
336 pixels = []
337
338 if fb.depth == 24:
339 unformat = _bgrx24
340 else:
341 raise RuntimeError("Couldn't unformat FB: %r" % fb)
342
343 for y in range(fb.height):
344 offset = y * pitch
345 m.seek(offset)
346 channels = m.read(lineLength)
347 ichannels = iter(channels)
348 for x in range(fb.width):
349 rgb = unformat(ichannels)
350 image.putpixel((x, y), rgb)
351
352 fb.unmap()
353
354 return pixels
355
356
357def screenshot():
358 """
359 Take a screenshot, returning an image object.
360 """
361
362 d = drmFromMinor(0)
363 fb = d.resources().getCrtc(0).fb()
364 image = Image.new("RGB", (fb.width, fb.height))
365 pixels = _screenshot(image, fb)
366
367 return image