Home | History | Annotate | Download | only in graphics
      1 """
      2 A wrapper around the Direct Rendering Manager (DRM) library, which itself is a
      3 wrapper around the Direct Rendering Interface (DRI) between the kernel and
      4 userland.
      5 
      6 Since we are masochists, we use ctypes instead of cffi to load libdrm and
      7 access several symbols within it. We use Python's file descriptor and mmap
      8 wrappers.
      9 
     10 At some point in the future, cffi could be used, for approximately the same
     11 cost in lines of code.
     12 """
     13 
     14 from ctypes import *
     15 import mmap
     16 import os
     17 
     18 from PIL import Image
     19 
     20 
     21 class 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 
     55 class 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         ("count_connectors", c_int),
     66         ("connectors", POINTER(c_uint)),
     67         ("count_encoders", c_int),
     68         ("encoders", POINTER(c_uint)),
     69         ("min_width", c_int),
     70         ("max_width", c_int),
     71         ("min_height", c_int),
     72         ("max_height", c_int),
     73     ]
     74 
     75     _fd = None
     76     _l = None
     77 
     78     def __repr__(self):
     79         return "<DRM mode resources>"
     80 
     81     def __del__(self):
     82         if self._l:
     83             self._l.drmModeFreeResources(self)
     84 
     85     def getValidCrtc(self):
     86         for i in xrange(0, self.count_crtcs):
     87             crtc_id = self.crtcs[i]
     88             crtc = self._l.drmModeGetCrtc(self._fd, crtc_id).contents
     89             if crtc.mode_valid:
     90                 return crtc
     91         return None
     92 
     93     def getCrtc(self, crtc_id=None):
     94         """
     95         Obtain the CRTC at a given index.
     96 
     97         @param crtc_id: The CRTC to get.
     98         """
     99         crtc = None
    100         if crtc_id:
    101             crtc = self._l.drmModeGetCrtc(self._fd, crtc_id).contents
    102         else:
    103             crtc = self.getValidCrtc()
    104         crtc._fd = self._fd
    105         crtc._l = self._l
    106         return crtc
    107 
    108 
    109 class DrmModeCrtc(Structure):
    110     """
    111     A DRM modesetting CRTC.
    112     """
    113 
    114     _fields_ = [
    115         ("crtc_id", c_uint),
    116         ("buffer_id", c_uint),
    117         ("x", c_uint),
    118         ("y", c_uint),
    119         ("width", c_uint),
    120         ("height", c_uint),
    121         ("mode_valid", c_int),
    122         # XXX incomplete struct!
    123     ]
    124 
    125     _fd = None
    126     _l = None
    127 
    128     def __repr__(self):
    129         return "<CRTC (%d)>" % self.crtc_id
    130 
    131     def __del__(self):
    132         if self._l:
    133             self._l.drmModeFreeCrtc(self)
    134 
    135     def hasFb(self):
    136         """
    137         Whether this CRTC has an associated framebuffer.
    138         """
    139 
    140         return self.buffer_id != 0
    141 
    142     def fb(self):
    143         """
    144         Obtain the framebuffer, if one is associated.
    145         """
    146 
    147         if self.hasFb():
    148             fb = self._l.drmModeGetFB(self._fd, self.buffer_id).contents
    149             fb._fd = self._fd
    150             fb._l = self._l
    151             return fb
    152         else:
    153             raise RuntimeError("CRTC %d doesn't have a framebuffer!" %
    154                                self.crtc_id)
    155 
    156 
    157 class drm_mode_map_dumb(Structure):
    158     """
    159     Request a mapping of a modesetting buffer.
    160 
    161     The map will be "dumb;" it will be accessible via mmap() but very slow.
    162     """
    163 
    164     _fields_ = [
    165         ("handle", c_uint),
    166         ("pad", c_uint),
    167         ("offset", c_ulonglong),
    168     ]
    169 
    170 
    171 # This constant is not defined in any one header; it is the pieced-together
    172 # incantation for the ioctl that performs dumb mappings. I would love for this
    173 # to not have to be here, but it can't be imported from any header easily.
    174 DRM_IOCTL_MODE_MAP_DUMB = 0xc01064b3
    175 
    176 
    177 class DrmModeFB(Structure):
    178     """
    179     A DRM modesetting framebuffer.
    180     """
    181 
    182     _fields_ = [
    183         ("fb_id", c_uint),
    184         ("width", c_uint),
    185         ("height", c_uint),
    186         ("pitch", c_uint),
    187         ("bpp", c_uint),
    188         ("depth", c_uint),
    189         ("handle", c_uint),
    190     ]
    191 
    192     _l = None
    193     _map = None
    194 
    195     def __repr__(self):
    196         s = "<Framebuffer (%dx%d (pitch %d bytes), %d bits/pixel, depth %d)"
    197         vitals = s % (
    198             self.width,
    199             self.height,
    200             self.pitch,
    201             self.bpp,
    202             self.depth,
    203         )
    204         if self._map:
    205             tail = " (mapped)>"
    206         else:
    207             tail = ">"
    208         return vitals + tail
    209 
    210     def __del__(self):
    211         if self._l:
    212             self._l.drmModeFreeFB(self)
    213 
    214     def map(self):
    215         """
    216         Map the framebuffer.
    217         """
    218 
    219         if self._map:
    220             return
    221 
    222         mapDumb = drm_mode_map_dumb()
    223         mapDumb.handle = self.handle
    224 
    225         rv = self._l.drmIoctl(self._fd, DRM_IOCTL_MODE_MAP_DUMB,
    226                               pointer(mapDumb))
    227         if rv:
    228             raise IOError(rv, os.strerror(rv))
    229 
    230         size = self.pitch * self.height
    231 
    232         # mmap.mmap() has a totally different order of arguments in Python
    233         # compared to C; check the documentation before altering this
    234         # incantation.
    235         self._map = mmap.mmap(self._fd, size, flags=mmap.MAP_SHARED,
    236                               prot=mmap.PROT_READ, offset=mapDumb.offset)
    237 
    238     def unmap(self):
    239         """
    240         Unmap the framebuffer.
    241         """
    242 
    243         if self._map:
    244             self._map.close()
    245             self._map = None
    246 
    247 
    248 def loadDRM():
    249     """
    250     Load a handle to libdrm.
    251 
    252     In addition to loading, this function also configures the argument and
    253     return types of functions.
    254     """
    255 
    256     l = cdll.LoadLibrary("libdrm.so")
    257 
    258     l.drmGetVersion.argtypes = [c_int]
    259     l.drmGetVersion.restype = POINTER(DrmVersion)
    260 
    261     l.drmFreeVersion.argtypes = [POINTER(DrmVersion)]
    262     l.drmFreeVersion.restype = None
    263 
    264     l.drmModeGetResources.argtypes = [c_int]
    265     l.drmModeGetResources.restype = POINTER(DrmModeResources)
    266 
    267     l.drmModeFreeResources.argtypes = [POINTER(DrmModeResources)]
    268     l.drmModeFreeResources.restype = None
    269 
    270     l.drmModeGetCrtc.argtypes = [c_int, c_uint]
    271     l.drmModeGetCrtc.restype = POINTER(DrmModeCrtc)
    272 
    273     l.drmModeFreeCrtc.argtypes = [POINTER(DrmModeCrtc)]
    274     l.drmModeFreeCrtc.restype = None
    275 
    276     l.drmModeGetFB.argtypes = [c_int, c_uint]
    277     l.drmModeGetFB.restype = POINTER(DrmModeFB)
    278 
    279     l.drmModeFreeFB.argtypes = [POINTER(DrmModeFB)]
    280     l.drmModeFreeFB.restype = None
    281 
    282     l.drmIoctl.argtypes = [c_int, c_ulong, c_voidp]
    283     l.drmIoctl.restype = c_int
    284 
    285     return l
    286 
    287 
    288 class DRM(object):
    289     """
    290     A DRM node.
    291     """
    292 
    293     def __init__(self, library, fd):
    294         self._l = library
    295         self._fd = fd
    296 
    297     def __repr__(self):
    298         return "<DRM (FD %d)>" % self._fd
    299 
    300     @classmethod
    301     def fromHandle(cls, handle):
    302         """
    303         Create a node from a file handle.
    304 
    305         @param handle: A file-like object backed by a file descriptor.
    306         """
    307 
    308         self = cls(loadDRM(), handle.fileno())
    309         # We must keep the handle alive, and we cannot trust the caller to
    310         # keep it alive for us.
    311         self._handle = handle
    312         return self
    313 
    314     def version(self):
    315         """
    316         Obtain the version.
    317         """
    318 
    319         v = self._l.drmGetVersion(self._fd).contents
    320         v._l = self._l
    321         return v
    322 
    323     def resources(self):
    324         """
    325         Obtain the modesetting resources.
    326         """
    327 
    328         resources_ptr = self._l.drmModeGetResources(self._fd)
    329         if resources_ptr:
    330             r = resources_ptr.contents
    331             r._fd = self._fd
    332             r._l = self._l
    333             return r
    334 
    335         return None
    336 
    337 
    338 def drmFromPath(path):
    339     """
    340     Given a DRM node path, open the corresponding node.
    341 
    342     @param path: The path of the minor node to open.
    343     """
    344 
    345     handle = open(path)
    346     return DRM.fromHandle(handle)
    347 
    348 
    349 def _bgrx24(i):
    350     b = ord(next(i))
    351     g = ord(next(i))
    352     r = ord(next(i))
    353     next(i)
    354     return r, g, b
    355 
    356 
    357 def _screenshot(image, fb):
    358     fb.map()
    359     m = fb._map
    360     lineLength = fb.width * fb.bpp // 8
    361     pitch = fb.pitch
    362     pixels = []
    363 
    364     if fb.depth == 24:
    365         unformat = _bgrx24
    366     else:
    367         raise RuntimeError("Couldn't unformat FB: %r" % fb)
    368 
    369     for y in range(fb.height):
    370         offset = y * pitch
    371         m.seek(offset)
    372         channels = m.read(lineLength)
    373         ichannels = iter(channels)
    374         for x in range(fb.width):
    375             rgb = unformat(ichannels)
    376             image.putpixel((x, y), rgb)
    377 
    378     fb.unmap()
    379 
    380     return pixels
    381 
    382 
    383 _drm = None
    384 
    385 def crtcScreenshot(crtc_id=None):
    386     """
    387     Take a screenshot, returning an image object.
    388 
    389     @param crtc_id: The CRTC to screenshot.
    390     """
    391 
    392     global _drm
    393     if not _drm:
    394         paths = ["/dev/dri/" + n for n in os.listdir("/dev/dri")]
    395         for p in paths:
    396             d = drmFromPath(p)
    397             if d.resources():
    398                 _drm = d
    399                 break
    400 
    401     if _drm:
    402         fb = _drm.resources().getCrtc(crtc_id).fb()
    403         image = Image.new("RGB", (fb.width, fb.height))
    404         pixels = _screenshot(image, fb)
    405         return image
    406 
    407     raise RuntimeError("Couldn't screenshot with DRM devices")
    408