Skip to main content

Python bindings for Linux DRM/KMS display output via /dev/dri/cardN

Project description

drm-display

Python display library for Linux — write NumPy image arrays directly to a screen without a compositor, X server, or Wayland session.

Initially designed for Raspberry PI as DRM setup for kiosk mode.

Designed for embedded systems, CVM/KVM virtual machines, single-board computers, and headless servers with an attached display where the traditional /dev/fb0 framebuffer is not exposed (or should not be used) because the driver stack has moved to the modern DRM/KMS subsystem.

from drm_display import Screen
import numpy as np

screen = Screen()               # auto-detects the best available backend
w, h   = screen.get_screen_size()

canvas = np.zeros((h, w, 4), dtype=np.uint8)
canvas[:, :, 2] = 255          # BGRA — red fill
screen.show(canvas)

Features

  • Three backends, one interfaceScreen probes devices in priority order and selects the best one automatically; fall back is always graceful
  • DRM/KMS backend — direct access to /dev/dri/cardN via libdrm, works with modern virtio-gpu, vmwgfx, vc4, i915, amdgpu, and any other KMS-capable driver; no compositor required
  • Smart mode selection — reads the connector's advertised mode list and picks the preferred mode automatically; accepts explicit width/height for custom LCD panels that don't enumerate EDID modes
  • Legacy framebuffer backend — pure-Python /dev/fb0 access via numpy.memmap; zero C dependencies, kept for compatibility
  • Headless / in-memory backend — numpy buffer that always succeeds; useful for unit testing and CI pipelines
  • OpenCV integrationScreen.show_image() scales and centres any BGR/BGRA OpenCV image to fit the display, with optional side-by-side layout
  • Partial updatessend_partial_image(patch, x, y) blits a sub-region without touching the rest of the framebuffer
  • drm-list-modes CLI — built-in diagnostic tool: checks kernel modules, master-lock status, connector modes, and framebuffer devices in one pass

Why DRM/KMS instead of /dev/fb0?

Modern Linux GPU drivers (including virtio-gpu used by QEMU/KVM, and vc4 used by Raspberry Pi OS) register as DRM/KMS devices and expose a display through /dev/dri/cardN. They may also expose a compatibility /dev/fb0 node, but it is often read-only, absent, or explicitly disabled by the distribution.

If you are seeing errors like Permission denied on /dev/fb0 or /dev/fb0: No such file or directory on a machine that clearly has a working display, the driver has moved to DRM. This package handles that transparently.


Backend comparison

Backend Class Device C build Dependency Use when
DRM/KMS DRMDisplay /dev/dri/cardN required libdrm Modern drivers, CVM/KVM, SBC
Framebuffer FBDisplay /dev/fb0 none numpy only Legacy kernels, compatibility
Headless DBDisplay (in-memory) none numpy only Testing, CI, no display

Installation

Option 1 — PyPI (recommended)

pip install drm-display

pip install automatically compiles the small C helper (drm_display.c) for the DRM backend using your system's libdrm.

System prerequisites for the DRM backend:

Distribution Command
Debian / Ubuntu apt install gcc libdrm-dev
Fedora / RHEL dnf install gcc libdrm-devel
Alpine apk add gcc musl-dev libdrm-dev
Arch pacman -S gcc libdrm

pkg-config libdrm is used when available; otherwise the common include paths /usr/include/libdrm and /usr/include/drm are tried in order.

If libdrm or gcc is absent the package still installs — FBDisplay and DBDisplay work without the C build step.

Option 2 — Editable install from source

git clone https://github.com/carstenbund/drm_display.git
cd drm_display
pip install -e .        # compiles libdrm_display.so in-place

Local changes to Python files take effect immediately. Re-run pip install -e . (or make) after changing drm_display.c.

Option 3 — Compile the C helper manually

make                                          # auto via pkg-config
make CFLAGS_EXTRA="-I/opt/custom/include"     # custom include path
make CC=aarch64-linux-gnu-gcc                 # cross-compile
make info                                     # show resolved flags

Quick start

Screen — automatic backend selection (recommended)

from drm_display import Screen
import numpy as np

# Auto-detect: tries card0 → card1 → /dev/fb0 → headless
screen = Screen()

# Force a specific device
screen = Screen(device="/dev/dri/card0")

# Custom LCD with no EDID — pass explicit size
screen = Screen(device="/dev/dri/card0", width=800, height=480)

w, h = screen.get_screen_size()     # actual size after init

# Send a frame (BGRA uint8, shape (h, w, 4))
canvas = np.zeros((h, w, 4), dtype=np.uint8)
canvas[:, :, 1] = 128              # mid-green
screen.show(canvas)

# Get the last shown frame
last = screen.copy()

screen.clear()
screen.close()

Displaying an image array

show_image() accepts any (H, W, 3|4) uint8 NumPy array — BGR, RGB, BGRA, or RGBA. Downscaling is done with a vectorised numpy area-average; no OpenCV or Pillow needed.

import numpy as np
from drm_display import Screen

screen = Screen()

# Any uint8 array works — from OpenCV, Pillow, imageio, …
img = np.zeros((480, 640, 3), dtype=np.uint8)   # plain numpy
img[:, :, 0] = 200                               # blue-ish

screen.show_image(img)              # scales + centres automatically

# Side-by-side comparison
screen.show_image(img, img2)

Low-level backends

Use these directly when you need explicit control over the device or mode.

DRMDisplay — DRM/KMS

from drm_display import DRMDisplay
import numpy as np

# Auto mode: driver picks the preferred resolution
drm = DRMDisplay(device="/dev/dri/card0")

# Explicit size: finds matching mode in connector list;
# if none found, uses connector's preferred mode for set_crtc
# and creates the framebuffer at the requested size
# (panel does internal scaling — common on custom DSI/LVDS screens)
drm = DRMDisplay(device="/dev/dri/card0", width=800, height=480)

w = drm.screen_width
h = drm.screen_height

canvas = np.zeros((h, w, 4), dtype=np.uint8)
drm.send_full_image(canvas)             # full-screen blit

patch  = np.zeros((100, 200, 4), dtype=np.uint8)
patch[:, :, 2] = 255
drm.send_partial_image(patch, x=50, y=50)  # blit a region

drm.cleanup()   # or just let __del__ handle it

On init, DRMDisplay prints every mode the connector advertises — useful when diagnosing custom panel issues:

Connector reports 2 mode(s):
  1920x1080@60 [preferred] (1920x1080)
   1280x720@60             (1280x720)
Auto-selected mode 1920x1080@60
Framebuffer: 1920x1080

FBDisplay — legacy /dev/fb0

from drm_display import FBDisplay

fb = FBDisplay("/dev/fb0")          # resolution auto-detected from sysfs
print(fb.screen_width, fb.screen_height)

fb.send_full_image(canvas)
fb.clear()
fb.close()

No C build, no libdrm — just numpy.

DBDisplay — headless buffer

from drm_display import DBDisplay

db = DBDisplay(width=1280, height=720)
db.send_full_image(canvas)
db.send_partial_image(patch, x=10, y=10)

# Access the backing buffer directly
last_frame = db.fb.copy()          # shape (720, 1280, 4)

Always succeeds regardless of what hardware is present. Ideal for unit tests:

def test_rendering():
    display = DBDisplay(width=320, height=240)
    render_something(display)
    assert display.fb[120, 160, 2] == 255   # check a pixel

drm-list-modes — display diagnostic tool

After pip install, a drm-list-modes command is available system-wide. It reports everything relevant to display availability in one pass — no more assembling answers from lsmod, ls /dev/dri, and ls /dev/fb* separately.

── Kernel modules ──────────────────────────────────────────────
  DRM core:
    drm                  loaded    (DRM subsystem)
    drm_kms_helper       loaded    (KMS helpers)
  Virtual / CVM / KVM:
    virtio_gpu           loaded    (virtio-GPU (QEMU/KVM))

── DRM devices ─────────────────────────────────────────────────
  /dev/dri/card0  [driver: virtio_gpu]
    Master: ⚠ locked by pid 1234 (Xorg)
    Connector 1: Virtual-1       [connected]  527 x 296 mm
      * 1920x1080 @  60 Hz   (1920x1080)
        1280x720  @  60 Hz   (1280x720)

── Framebuffer devices (/dev/fb*) ──────────────────────────────
  /dev/fb0  [driver: virtio_gpu  DRM-backed]  1920x1080
    in use by pid 1234 (Xorg)

── Summary ─────────────────────────────────────────────────────
  Suggested usage:
  DRMDisplay('/dev/dri/card0') — has modes but master is locked;
    stop the compositor first, or use FBDisplay if a DRM-backed fb is free

What it checks

Section What is reported
Kernel modules Which DRM core, GPU driver, and framebuffer modules are loaded — grouped by role (CVM/KVM, desktop GPU, embedded SBC, legacy fb)
DRM devices Driver name, DRM master lock (names the locking process by scanning /proc/*/fdinfo), all connectors with connection status and physical size, all modes with preferred marker
Framebuffer devices Always shown even when no DRM devices exist — driver name, DRM-backed vs legacy, physical size, in-use status
Summary Plain-English recommended call: DRMDisplay(...), FBDisplay(...), or explicit width/height for panels with no EDID

Usage

drm-list-modes                        # scan everything
drm-list-modes /dev/dri/card0         # specific device only
python -m drm_display.list_modes      # run without installing

Exit codes: 0 = at least one usable device found, 1 = nothing usable, 2 = fatal error (libdrm missing).


Custom LCD panels

Custom DSI and LVDS panels attached to embedded SBCs often do not expose EDID data, so the DRM connector reports zero modes. DRMDisplay handles this:

# Pass explicit size — DRMDisplay will use whatever mode the CRTC has
# and create the framebuffer at the requested size.
# The panel does internal scaling (common on DSI/LVDS screens).
drm = DRMDisplay(device="/dev/dri/card0", width=480, height=800)

If the connector does enumerate modes and the requested size matches one of them exactly, that mode is used for drmModeSetCrtc. If there is no exact match, the preferred mode (or first listed mode) is used for the CRTC call while the framebuffer is still created at the requested size.

Run drm-list-modes first to see what your connector actually reports — it saves a lot of guesswork.


How it works

Screen()
  │
  ├─ try /dev/dri/card0  ──►  DRMDisplay
  │     open device
  │     drmModeGetResources
  │     find connected connector
  │     select best mode (preferred flag → first → explicit size)
  │     create dumb framebuffer
  │     drmModeSetCrtc with selected mode
  │     ▼
  │   send_full_image(canvas)
  │     mmap framebuffer
  │     memcpy row-by-row (supports partial updates)
  │
  ├─ try /dev/fb0  ──►  FBDisplay
  │     numpy.memmap(device, shape=(h, w, 4))
  │     canvas slice assignment
  │
  └─ dummy  ──►  DBDisplay
        numpy.zeros(shape=(h, w, 4))
        always succeeds

The DRM backend compiles a small C helper (drm_display.c) that wraps libdrm ioctl calls. The Python layer uses ctypes to call it — no Python C extension build system required, no ABI compatibility issues.


Requirements

  • Python 3.8+
  • numpy (installed automatically)
  • gcc + libdrm-dev — required only for DRMDisplay; the package installs and FBDisplay/DBDisplay work without it
  • Linux with a KMS-capable GPU for DRMDisplay
  • User must be in the video group (or run as root) to open /dev/dri/cardN

Troubleshooting

Failed to open device / Permission denied

sudo usermod -aG video $USER   # then log out and back in
# or run once as root to verify the device works

No connected connector found The display cable is not plugged in, or the driver does not see the panel. Run drm-list-modes to see the raw connector state.

Failed to set CRTC on a custom panel The connector likely reports zero modes (no EDID). Pass width= and height= explicitly:

DRMDisplay("/dev/dri/card0", width=800, height=480)

libdrm_display.so not found The C helper was not compiled. Run pip install -e . (editable) or make from the repo root. Check that gcc and libdrm-dev are installed first.

DRM device exists but all operations fail A compositor (X11, Wayland) may hold the DRM master lock.

drm-list-modes    # shows which process holds master

Changes:

0.1.5 DirtyFB vmwgfx (VMware/QEMU virtual GPU) only pushes pixels to the host display when explicitly notified via drmModeDirtyFB. Without it the buffer is written in memory but never rendered. 0.1.4 Added Screen.close() 0.1.3 Cleanup README.md 0.1.2 added Screen handler class, removed CV2 and Pillow dependencies. 0.1.0 initial


License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

drm_display-0.1.5.tar.gz (30.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

drm_display-0.1.5-py3-none-any.whl (26.1 kB view details)

Uploaded Python 3

File details

Details for the file drm_display-0.1.5.tar.gz.

File metadata

  • Download URL: drm_display-0.1.5.tar.gz
  • Upload date:
  • Size: 30.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for drm_display-0.1.5.tar.gz
Algorithm Hash digest
SHA256 6ee78e80e58985bb470e7025095c9a8614c386dc504d1a8f7e139047a4e468bd
MD5 7d427c0ef620c9da2ace36e4fcdfd01f
BLAKE2b-256 4456bd9828b9b46d1b70fdd3b05757b3ec6ce8300576d0ba369bd8a2701d70b8

See more details on using hashes here.

File details

Details for the file drm_display-0.1.5-py3-none-any.whl.

File metadata

  • Download URL: drm_display-0.1.5-py3-none-any.whl
  • Upload date:
  • Size: 26.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for drm_display-0.1.5-py3-none-any.whl
Algorithm Hash digest
SHA256 5de5e53035f782a6199196db9dcd4d5a6d670d9ea1ef4d3b90b83a17010a77ce
MD5 f73b14b2d8156c2adc2bf1b92b2143c6
BLAKE2b-256 a1a9afb6b9203784cfe51e3b55cb73cf69b184e0630be042e43d1b9cf4918d57

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page