1"""
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
17import subprocess
18
19# drmModeConnection enum
20DRM_MODE_CONNECTED         = 1
21DRM_MODE_DISCONNECTED      = 2
22DRM_MODE_UNKNOWNCONNECTION = 3
23
24DRM_MODE_CONNECTOR_Unknown     = 0
25DRM_MODE_CONNECTOR_VGA         = 1
26DRM_MODE_CONNECTOR_DVII        = 2
27DRM_MODE_CONNECTOR_DVID        = 3
28DRM_MODE_CONNECTOR_DVIA        = 4
29DRM_MODE_CONNECTOR_Composite   = 5
30DRM_MODE_CONNECTOR_SVIDEO      = 6
31DRM_MODE_CONNECTOR_LVDS        = 7
32DRM_MODE_CONNECTOR_Component   = 8
33DRM_MODE_CONNECTOR_9PinDIN     = 9
34DRM_MODE_CONNECTOR_DisplayPort = 10
35DRM_MODE_CONNECTOR_HDMIA       = 11
36DRM_MODE_CONNECTOR_HDMIB       = 12
37DRM_MODE_CONNECTOR_TV          = 13
38DRM_MODE_CONNECTOR_eDP         = 14
39DRM_MODE_CONNECTOR_VIRTUAL     = 15
40DRM_MODE_CONNECTOR_DSI         = 16
41
42# This constant is not defined in any one header; it is the pieced-together
43# incantation for the ioctl that performs dumb mappings. I would love for this
44# to not have to be here, but it can't be imported from any header easily.
45DRM_IOCTL_MODE_MAP_DUMB = 0xc01064b3
46
47# This define should be equal to O_CLOEXEC, which should be available in
48# python's os module, but isn't until version 3.3.  If we version up, we can
49# set this to os.O_CLOEXEC.
50DRM_CLOEXEC = 02000000
51
52
53class DrmVersion(Structure):
54    """
55    The version of a DRM node.
56    """
57
58    _fields_ = [
59        ("version_major", c_int),
60        ("version_minor", c_int),
61        ("version_patchlevel", c_int),
62        ("name_len", c_int),
63        ("name", c_char_p),
64        ("date_len", c_int),
65        ("date", c_char_p),
66        ("desc_len", c_int),
67        ("desc", c_char_p),
68    ]
69
70    _l = None
71
72    def __repr__(self):
73        return "%s %d.%d.%d (%s) (%s)" % (self.name,
74                                          self.version_major,
75                                          self.version_minor,
76                                          self.version_patchlevel,
77                                          self.desc,
78                                          self.date,)
79
80    def __del__(self):
81        if self._l:
82            self._l.drmFreeVersion(self)
83
84
85class DrmModeResources(Structure):
86    """
87    Resources associated with setting modes on a DRM node.
88    """
89
90    _fields_ = [
91        ("count_fbs", c_int),
92        ("fbs", POINTER(c_uint)),
93        ("count_crtcs", c_int),
94        ("crtcs", POINTER(c_uint)),
95        ("count_connectors", c_int),
96        ("connectors", POINTER(c_uint)),
97        ("count_encoders", c_int),
98        ("encoders", POINTER(c_uint)),
99        ("min_width", c_int),
100        ("max_width", c_int),
101        ("min_height", c_int),
102        ("max_height", c_int),
103    ]
104
105    _fd = None
106    _l = None
107
108    def __repr__(self):
109        return "<DRM mode resources>"
110
111    def __del__(self):
112        if self._l:
113            self._l.drmModeFreeResources(self)
114
115    def _wakeup_screen(self):
116        """
117        Send a synchronous dbus message to power on screen.
118        """
119        # Get and process reply to make this synchronous.
120        subprocess.check_output([
121            "dbus-send", "--type=method_call", "--system", "--print-reply",
122            "--dest=org.chromium.PowerManager", "/org/chromium/PowerManager",
123            "org.chromium.PowerManager.HandleUserActivity", "int32:0"
124        ])
125
126    def getValidCrtc(self):
127        for i in xrange(0, self.count_crtcs):
128            crtc_id = self.crtcs[i]
129            crtc = self._l.drmModeGetCrtc(self._fd, crtc_id).contents
130            if crtc.mode_valid:
131                return crtc
132        return None
133
134    def getCrtc(self, crtc_id):
135        """
136        Obtain the CRTC at a given index.
137
138        @param crtc_id: The CRTC to get.
139        """
140        if crtc_id:
141            return self._l.drmModeGetCrtc(self._fd, crtc_id).contents
142        return self.getValidCrtc()
143
144    def getCrtcRobust(self, crtc_id=None):
145        crtc = self.getCrtc(crtc_id)
146        if crtc is None:
147            self._wakeup_screen()
148            crtc = self.getCrtc(crtc_id)
149        if crtc is not None:
150            crtc._fd = self._fd
151            crtc._l = self._l
152        return crtc
153
154
155class DrmModeModeInfo(Structure):
156    """
157    A DRM modesetting mode info.
158    """
159
160    _fields_ = [
161        ("clock", c_uint),
162        ("hdisplay", c_ushort),
163        ("hsync_start", c_ushort),
164        ("hsync_end", c_ushort),
165        ("htotal", c_ushort),
166        ("hskew", c_ushort),
167        ("vdisplay", c_ushort),
168        ("vsync_start", c_ushort),
169        ("vsync_end", c_ushort),
170        ("vtotal", c_ushort),
171        ("vscan", c_ushort),
172        ("vrefresh", c_uint),
173        ("flags", c_uint),
174        ("type", c_uint),
175        ("name", c_char * 32),
176    ]
177
178
179class DrmModeCrtc(Structure):
180    """
181    A DRM modesetting CRTC.
182    """
183
184    _fields_ = [
185        ("crtc_id", c_uint),
186        ("buffer_id", c_uint),
187        ("x", c_uint),
188        ("y", c_uint),
189        ("width", c_uint),
190        ("height", c_uint),
191        ("mode_valid", c_int),
192        ("mode", DrmModeModeInfo),
193        ("gamma_size", c_int),
194    ]
195
196    _fd = None
197    _l = None
198
199    def __repr__(self):
200        return "<CRTC (%d)>" % self.crtc_id
201
202    def __del__(self):
203        if self._l:
204            self._l.drmModeFreeCrtc(self)
205
206    def hasFb(self):
207        """
208        Whether this CRTC has an associated framebuffer.
209        """
210
211        return self.buffer_id != 0
212
213    def fb(self):
214        """
215        Obtain the framebuffer, if one is associated.
216        """
217
218        if self.hasFb():
219            fb = self._l.drmModeGetFB(self._fd, self.buffer_id).contents
220            fb._fd = self._fd
221            fb._l = self._l
222            return fb
223        else:
224            raise RuntimeError("CRTC %d doesn't have a framebuffer!" %
225                               self.crtc_id)
226
227
228class DrmModeEncoder(Structure):
229    """
230    A DRM modesetting encoder.
231    """
232
233    _fields_ = [
234        ("encoder_id", c_uint),
235        ("encoder_type", c_uint),
236        ("crtc_id", c_uint),
237        ("possible_crtcs", c_uint),
238        ("possible_clones", c_uint),
239    ]
240
241    _fd = None
242    _l = None
243
244    def __repr__(self):
245        return "<Encoder (%d)>" % self.encoder_id
246
247    def __del__(self):
248        if self._l:
249            self._l.drmModeFreeEncoder(self)
250
251
252class DrmModeConnector(Structure):
253    """
254    A DRM modesetting connector.
255    """
256
257    _fields_ = [
258        ("connector_id", c_uint),
259        ("encoder_id", c_uint),
260        ("connector_type", c_uint),
261        ("connector_type_id", c_uint),
262        ("connection", c_uint), # drmModeConnection enum
263        ("mmWidth", c_uint),
264        ("mmHeight", c_uint),
265        ("subpixel", c_uint), # drmModeSubPixel enum
266        ("count_modes", c_int),
267        ("modes", POINTER(DrmModeModeInfo)),
268        ("count_propts", c_int),
269        ("props", POINTER(c_uint)),
270        ("prop_values", POINTER(c_ulonglong)),
271        ("count_encoders", c_int),
272        ("encoders", POINTER(c_uint)),
273    ]
274
275    _fd = None
276    _l = None
277
278    def __repr__(self):
279        return "<Connector (%d)>" % self.connector_id
280
281    def __del__(self):
282        if self._l:
283            self._l.drmModeFreeConnector(self)
284
285    def isInternal(self):
286        return (self.connector_type == DRM_MODE_CONNECTOR_LVDS or
287                self.connector_type == DRM_MODE_CONNECTOR_eDP or
288                self.connector_type == DRM_MODE_CONNECTOR_DSI)
289
290    def isConnected(self):
291        return self.connection == DRM_MODE_CONNECTED
292
293
294class drm_mode_map_dumb(Structure):
295    """
296    Request a mapping of a modesetting buffer.
297
298    The map will be "dumb;" it will be accessible via mmap() but very slow.
299    """
300
301    _fields_ = [
302        ("handle", c_uint),
303        ("pad", c_uint),
304        ("offset", c_ulonglong),
305    ]
306
307
308class DrmModeFB(Structure):
309    """
310    A DRM modesetting framebuffer.
311    """
312
313    _fields_ = [
314        ("fb_id", c_uint),
315        ("width", c_uint),
316        ("height", c_uint),
317        ("pitch", c_uint),
318        ("bpp", c_uint),
319        ("depth", c_uint),
320        ("handle", c_uint),
321    ]
322
323    _l = None
324    _map = None
325
326    def __repr__(self):
327        s = "<Framebuffer (%dx%d (pitch %d bytes), %d bits/pixel, depth %d)"
328        vitals = s % (self.width,
329                      self.height,
330                      self.pitch,
331                      self.bpp,
332                      self.depth,)
333        if self._map:
334            tail = " (mapped)>"
335        else:
336            tail = ">"
337        return vitals + tail
338
339    def __del__(self):
340        if self._l:
341            self._l.drmModeFreeFB(self)
342
343    def map(self, size):
344        """
345        Map the framebuffer.
346        """
347
348        if self._map:
349            return
350
351        mapDumb = drm_mode_map_dumb()
352        mapDumb.handle = self.handle
353
354        rv = self._l.drmIoctl(self._fd, DRM_IOCTL_MODE_MAP_DUMB,
355                              pointer(mapDumb))
356        if rv:
357            raise IOError(rv, os.strerror(rv))
358
359        # mmap.mmap() has a totally different order of arguments in Python
360        # compared to C; check the documentation before altering this
361        # incantation.
362        self._map = mmap.mmap(self._fd,
363                              size,
364                              flags=mmap.MAP_SHARED,
365                              prot=mmap.PROT_READ,
366                              offset=mapDumb.offset)
367
368    def unmap(self):
369        """
370        Unmap the framebuffer.
371        """
372
373        if self._map:
374            self._map.close()
375            self._map = None
376
377    def getFD(self):
378        """
379        Convert handle to a FD.
380        """
381        prime_fd = c_int(0)
382        rv = self._l.drmPrimeHandleToFD(self._fd, self.handle,
383                                        DRM_CLOEXEC, byref(prime_fd))
384        if rv:
385            raise RuntimeError("Failed to convert FB handle to FD. %d" % rv)
386        return prime_fd
387
388def loadDRM():
389    """
390    Load a handle to libdrm.
391
392    In addition to loading, this function also configures the argument and
393    return types of functions.
394    """
395
396    l = None
397
398    try:
399        l = cdll.LoadLibrary("libdrm.so")
400    except OSError:
401        l = cdll.LoadLibrary("libdrm.so.2") # ubuntu doesn't have libdrm.so
402
403    l.drmGetVersion.argtypes = [c_int]
404    l.drmGetVersion.restype = POINTER(DrmVersion)
405
406    l.drmFreeVersion.argtypes = [POINTER(DrmVersion)]
407    l.drmFreeVersion.restype = None
408
409    l.drmModeGetResources.argtypes = [c_int]
410    l.drmModeGetResources.restype = POINTER(DrmModeResources)
411
412    l.drmModeFreeResources.argtypes = [POINTER(DrmModeResources)]
413    l.drmModeFreeResources.restype = None
414
415    l.drmModeGetCrtc.argtypes = [c_int, c_uint]
416    l.drmModeGetCrtc.restype = POINTER(DrmModeCrtc)
417
418    l.drmModeFreeCrtc.argtypes = [POINTER(DrmModeCrtc)]
419    l.drmModeFreeCrtc.restype = None
420
421    l.drmModeGetEncoder.argtypes = [c_int, c_uint]
422    l.drmModeGetEncoder.restype = POINTER(DrmModeEncoder)
423
424    l.drmModeFreeEncoder.argtypes = [POINTER(DrmModeEncoder)]
425    l.drmModeFreeEncoder.restype = None
426
427    l.drmModeGetConnector.argtypes = [c_int, c_uint]
428    l.drmModeGetConnector.restype = POINTER(DrmModeConnector)
429
430    l.drmModeFreeConnector.argtypes = [POINTER(DrmModeConnector)]
431    l.drmModeFreeConnector.restype = None
432
433    l.drmModeGetFB.argtypes = [c_int, c_uint]
434    l.drmModeGetFB.restype = POINTER(DrmModeFB)
435
436    l.drmModeFreeFB.argtypes = [POINTER(DrmModeFB)]
437    l.drmModeFreeFB.restype = None
438
439    l.drmIoctl.argtypes = [c_int, c_ulong, c_voidp]
440    l.drmIoctl.restype = c_int
441
442    l.drmPrimeHandleToFD.argtypes = [c_int, c_uint, c_uint, POINTER(c_int)]
443    l.drmPrimeHandleToFD.restype = c_int
444
445    return l
446
447
448class DRM(object):
449    """
450    A DRM node.
451    """
452
453    def __init__(self, library, fd):
454        self._l = library
455        self._fd = fd
456
457    def __repr__(self):
458        return "<DRM (FD %d)>" % self._fd
459
460    @classmethod
461    def fromHandle(cls, handle):
462        """
463        Create a node from a file handle.
464
465        @param handle: A file-like object backed by a file descriptor.
466        """
467
468        self = cls(loadDRM(), handle.fileno())
469        # We must keep the handle alive, and we cannot trust the caller to
470        # keep it alive for us.
471        self._handle = handle
472        return self
473
474    def version(self):
475        """
476        Obtain the version.
477        """
478
479        v = self._l.drmGetVersion(self._fd).contents
480        v._l = self._l
481        return v
482
483    def resources(self):
484        """
485        Obtain the modesetting resources.
486        """
487
488        resources_ptr = self._l.drmModeGetResources(self._fd)
489        if resources_ptr:
490            r = resources_ptr.contents
491            r._fd = self._fd
492            r._l = self._l
493            return r
494
495        return None
496
497    def getCrtc(self, crtc_id):
498        c_ptr = self._l.drmModeGetCrtc(self._fd, crtc_id)
499        if c_ptr:
500            c = c_ptr.contents
501            c._fd = self._fd
502            c._l = self._l
503            return c
504
505        return None
506
507    def getEncoder(self, encoder_id):
508        e_ptr = self._l.drmModeGetEncoder(self._fd, encoder_id)
509        if e_ptr:
510            e = e_ptr.contents
511            e._fd = self._fd
512            e._l = self._l
513            return e
514
515        return None
516
517    def getConnector(self, connector_id):
518        c_ptr = self._l.drmModeGetConnector(self._fd, connector_id)
519        if c_ptr:
520            c = c_ptr.contents
521            c._fd = self._fd
522            c._l = self._l
523            return c
524
525        return None
526
527
528
529def drmFromPath(path):
530    """
531    Given a DRM node path, open the corresponding node.
532
533    @param path: The path of the minor node to open.
534    """
535    # Always open the device as RW (r+) so that mmap works later.
536    handle = open(path, "r+")
537    return DRM.fromHandle(handle)
538
539
540_drm = None
541
542
543def getCrtc(crtc_id=None):
544    """
545    @param crtc_id: None for first found CRTC with mode set
546                    or "internal" for crtc connected to internal LCD
547                    or "external" for crtc connected to external display
548                    or "usb" "evdi" or "udl" for crtc with valid mode on evdi or
549                    udl display
550                    or DRM integer crtc_id
551    """
552    global _drm
553
554    if not _drm:
555        paths = [
556            "/dev/dri/" + n
557            for n in filter(lambda x: x.startswith("card"),
558                            os.listdir("/dev/dri"))
559        ]
560
561        if crtc_id == "usb" or crtc_id == "evdi" or crtc_id == "udl":
562            for p in paths:
563                d = drmFromPath(p)
564                v = d.version()
565
566                if crtc_id == v.name:
567                    _drm = d
568                    break
569
570                if crtc_id == "usb" and (v.name == "evdi" or v.name == "udl"):
571                    _drm = d
572                    break
573
574        elif crtc_id == "internal" or crtc_id == "external":
575            internal = crtc_id == "internal"
576            for p in paths:
577                d = drmFromPath(p)
578                if d.resources() is None:
579                    continue
580                if d.resources() and d.resources().count_connectors > 0:
581                    for c in xrange(0, d.resources().count_connectors):
582                        connector = d.getConnector(d.resources().connectors[c])
583                        if (internal == connector.isInternal()
584                            and connector.isConnected()
585                            and connector.encoder_id != 0):
586                            e = d.getEncoder(connector.encoder_id)
587                            crtc = d.getCrtc(e.crtc_id)
588                            if crtc.mode_valid:
589                                crtc_id = crtc.crtc_id
590                                _drm = d
591                                break
592                if _drm:
593                    break
594
595        elif crtc_id is None or crtc_id == 0:
596            for p in paths:
597                d = drmFromPath(p)
598                if d.resources() is None:
599                    continue
600                for c in xrange(0, d.resources().count_crtcs):
601                    crtc = d.getCrtc(d.resources().crtcs[c])
602                    if crtc.mode_valid:
603                        crtc_id = d.resources().crtcs[c]
604                        _drm = d
605                        break
606                if _drm:
607                    break
608
609        else:
610            for p in paths:
611                d = drmFromPath(p)
612                if d.resources() is None:
613                    continue
614                for c in xrange(0, d.resources().count_crtcs):
615                    if crtc_id == d.resources().crtcs[c]:
616                        _drm = d
617                        break
618                if _drm:
619                    break
620
621    if _drm:
622        return _drm.resources().getCrtcRobust(crtc_id)
623
624    return None
625