""" A wrapper around the Direct Rendering Manager (DRM) library, which itself is a wrapper around the Direct Rendering Interface (DRI) between the kernel and userland. Since we are masochists, we use ctypes instead of cffi to load libdrm and access several symbols within it. We use Python's file descriptor and mmap wrappers. At some point in the future, cffi could be used, for approximately the same cost in lines of code. """ from ctypes import * import mmap import os from PIL import Image class DrmVersion(Structure): """ The version of a DRM node. """ _fields_ = [ ("version_major", c_int), ("version_minor", c_int), ("version_patchlevel", c_int), ("name_len", c_int), ("name", c_char_p), ("date_len", c_int), ("date", c_char_p), ("desc_len", c_int), ("desc", c_char_p), ] _l = None def __repr__(self): return "%s %d.%d.%d (%s) (%s)" % ( self.name, self.version_major, self.version_minor, self.version_patchlevel, self.desc, self.date, ) def __del__(self): if self._l: self._l.drmFreeVersion(self) class DrmModeResources(Structure): """ Resources associated with setting modes on a DRM node. """ _fields_ = [ ("count_fbs", c_int), ("fbs", POINTER(c_uint)), ("count_crtcs", c_int), ("crtcs", POINTER(c_uint)), ("count_connectors", c_int), ("connectors", POINTER(c_uint)), ("count_encoders", c_int), ("encoders", POINTER(c_uint)), ("min_width", c_int), ("max_width", c_int), ("min_height", c_int), ("max_height", c_int), ] _fd = None _l = None def __repr__(self): return "<DRM mode resources>" def __del__(self): if self._l: self._l.drmModeFreeResources(self) def getValidCrtc(self): for i in xrange(0, self.count_crtcs): crtc_id = self.crtcs[i] crtc = self._l.drmModeGetCrtc(self._fd, crtc_id).contents if crtc.mode_valid: return crtc return None def getCrtc(self, crtc_id=None): """ Obtain the CRTC at a given index. @param crtc_id: The CRTC to get. """ crtc = None if crtc_id: crtc = self._l.drmModeGetCrtc(self._fd, crtc_id).contents else: crtc = self.getValidCrtc() crtc._fd = self._fd crtc._l = self._l return crtc class DrmModeCrtc(Structure): """ A DRM modesetting CRTC. """ _fields_ = [ ("crtc_id", c_uint), ("buffer_id", c_uint), ("x", c_uint), ("y", c_uint), ("width", c_uint), ("height", c_uint), ("mode_valid", c_int), # XXX incomplete struct! ] _fd = None _l = None def __repr__(self): return "<CRTC (%d)>" % self.crtc_id def __del__(self): if self._l: self._l.drmModeFreeCrtc(self) def hasFb(self): """ Whether this CRTC has an associated framebuffer. """ return self.buffer_id != 0 def fb(self): """ Obtain the framebuffer, if one is associated. """ if self.hasFb(): fb = self._l.drmModeGetFB(self._fd, self.buffer_id).contents fb._fd = self._fd fb._l = self._l return fb else: raise RuntimeError("CRTC %d doesn't have a framebuffer!" % self.crtc_id) class drm_mode_map_dumb(Structure): """ Request a mapping of a modesetting buffer. The map will be "dumb;" it will be accessible via mmap() but very slow. """ _fields_ = [ ("handle", c_uint), ("pad", c_uint), ("offset", c_ulonglong), ] # This constant is not defined in any one header; it is the pieced-together # incantation for the ioctl that performs dumb mappings. I would love for this # to not have to be here, but it can't be imported from any header easily. DRM_IOCTL_MODE_MAP_DUMB = 0xc01064b3 class DrmModeFB(Structure): """ A DRM modesetting framebuffer. """ _fields_ = [ ("fb_id", c_uint), ("width", c_uint), ("height", c_uint), ("pitch", c_uint), ("bpp", c_uint), ("depth", c_uint), ("handle", c_uint), ] _l = None _map = None def __repr__(self): s = "<Framebuffer (%dx%d (pitch %d bytes), %d bits/pixel, depth %d)" vitals = s % ( self.width, self.height, self.pitch, self.bpp, self.depth, ) if self._map: tail = " (mapped)>" else: tail = ">" return vitals + tail def __del__(self): if self._l: self._l.drmModeFreeFB(self) def map(self): """ Map the framebuffer. """ if self._map: return mapDumb = drm_mode_map_dumb() mapDumb.handle = self.handle rv = self._l.drmIoctl(self._fd, DRM_IOCTL_MODE_MAP_DUMB, pointer(mapDumb)) if rv: raise IOError(rv, os.strerror(rv)) size = self.pitch * self.height # mmap.mmap() has a totally different order of arguments in Python # compared to C; check the documentation before altering this # incantation. self._map = mmap.mmap(self._fd, size, flags=mmap.MAP_SHARED, prot=mmap.PROT_READ, offset=mapDumb.offset) def unmap(self): """ Unmap the framebuffer. """ if self._map: self._map.close() self._map = None def loadDRM(): """ Load a handle to libdrm. In addition to loading, this function also configures the argument and return types of functions. """ l = cdll.LoadLibrary("libdrm.so") l.drmGetVersion.argtypes = [c_int] l.drmGetVersion.restype = POINTER(DrmVersion) l.drmFreeVersion.argtypes = [POINTER(DrmVersion)] l.drmFreeVersion.restype = None l.drmModeGetResources.argtypes = [c_int] l.drmModeGetResources.restype = POINTER(DrmModeResources) l.drmModeFreeResources.argtypes = [POINTER(DrmModeResources)] l.drmModeFreeResources.restype = None l.drmModeGetCrtc.argtypes = [c_int, c_uint] l.drmModeGetCrtc.restype = POINTER(DrmModeCrtc) l.drmModeFreeCrtc.argtypes = [POINTER(DrmModeCrtc)] l.drmModeFreeCrtc.restype = None l.drmModeGetFB.argtypes = [c_int, c_uint] l.drmModeGetFB.restype = POINTER(DrmModeFB) l.drmModeFreeFB.argtypes = [POINTER(DrmModeFB)] l.drmModeFreeFB.restype = None l.drmIoctl.argtypes = [c_int, c_ulong, c_voidp] l.drmIoctl.restype = c_int return l class DRM(object): """ A DRM node. """ def __init__(self, library, fd): self._l = library self._fd = fd def __repr__(self): return "<DRM (FD %d)>" % self._fd @classmethod def fromHandle(cls, handle): """ Create a node from a file handle. @param handle: A file-like object backed by a file descriptor. """ self = cls(loadDRM(), handle.fileno()) # We must keep the handle alive, and we cannot trust the caller to # keep it alive for us. self._handle = handle return self def version(self): """ Obtain the version. """ v = self._l.drmGetVersion(self._fd).contents v._l = self._l return v def resources(self): """ Obtain the modesetting resources. """ resources_ptr = self._l.drmModeGetResources(self._fd) if resources_ptr: r = resources_ptr.contents r._fd = self._fd r._l = self._l return r return None def drmFromPath(path): """ Given a DRM node path, open the corresponding node. @param path: The path of the minor node to open. """ handle = open(path) return DRM.fromHandle(handle) def _bgrx24(i): b = ord(next(i)) g = ord(next(i)) r = ord(next(i)) next(i) return r, g, b def _screenshot(image, fb): fb.map() m = fb._map lineLength = fb.width * fb.bpp // 8 pitch = fb.pitch pixels = [] if fb.depth == 24: unformat = _bgrx24 else: raise RuntimeError("Couldn't unformat FB: %r" % fb) for y in range(fb.height): offset = y * pitch m.seek(offset) channels = m.read(lineLength) ichannels = iter(channels) for x in range(fb.width): rgb = unformat(ichannels) image.putpixel((x, y), rgb) fb.unmap() return pixels _drm = None def crtcScreenshot(crtc_id=None): """ Take a screenshot, returning an image object. @param crtc_id: The CRTC to screenshot. """ global _drm if not _drm: paths = ["/dev/dri/" + n for n in os.listdir("/dev/dri")] for p in paths: d = drmFromPath(p) if d.resources(): _drm = d break if _drm: fb = _drm.resources().getCrtc(crtc_id).fb() image = Image.new("RGB", (fb.width, fb.height)) pixels = _screenshot(image, fb) return image raise RuntimeError("Couldn't screenshot with DRM devices")