DRM and Graphics

On this page

Frames may be rendered but not displayed on screen; successfully importing a dma-buf does not guarantee it can be directly scanned out. DRM must separately interpret rendering, buffer sharing, and display submission. Devices without monitors may still provide rendering capabilities.

Lookup GoalReading Location
User/kernel responsibilities and KMSSections 1–2
Submission completion and object handlesSections 3–4
Sharing synchronization and black screen troubleshootingSections 5–6

1. What User Space and Kernel Space Do

User-space drivers (UMD, such as OpenGL/Vulkan implementations in Mesa) handle graphics APIs, shader compilation, and command organization. Kernel drivers (KMD, such as amdgpu, i915, xe, nouveau) provide device access, buffer management, address spaces, scheduling, and synchronization mechanisms; drivers supporting display also implement KMS.

This is a separation of responsibilities, not "the kernel checks commands at all." Validation, isolation, and submission interfaces vary by hardware and driver; UMD must also match the UAPI and hardware capabilities supported by the kernel. Upgrading user space does not automatically add missing kernel features.

Device NodeCommon UsePermission Meaning
/dev/dri/cardNPrimary node, including display control interfacesModeset operations are subject to DRM master, lease, and other rules
/dev/dri/renderDNRendering and related resource operationsDoes not provide modesetting; still subject to file permissions and security policies

card0 is an enumeration name and does not guarantee being the "primary GPU" currently used by the desktop. Render nodes do not require obtaining DRM master, but this does not mean any user can naturally access them. Refer to the DRM User-space API for specific interfaces.

2. KMS: Describing Display Resources and Connections

Rendering completion and display completion are two different handoffs Application / UMD Submit GPU rendering Shared Buffer dma-buf and synchronization Compositor / KMS Composite / direct scanout Plane CRTC Encoder Connector Display path: Buffer as image source for plane Old buffers can only be reused after all users have finished accessing them Shared memory does not automatically resolve format compatibility, waiting for rendering, display release, and access permissions.
  • Plane: Provides an image layer, including framebuffer, source crop, and destination position. Types include primary, overlay, and cursor; cursors should not be uniformly classified as overlays.
  • CRTC: Manages scan timing and associated planes, outputting images according to modes. This historical name does not imply modern monitors use cathode ray tubes.
  • Encoder: Abstracts output encoding and routing resources.
  • Connector: Represents connections to display terminals, capable of reporting connection status, modes, EDID, and other information.

Connections are constrained by hardware resources: planes may not connect to arbitrary CRTCs, and display bandwidth and scaling capabilities are limited. A CRTC can be cloned to multiple outputs if hardware supports it; do not simply assume a one-to-one correspondence between CRTCs and monitors.

A framebuffer is an image layout object, not equivalent to memory allocation itself. It describes dimensions, pixel format, pitch, offset, modifier, and associations with underlying buffers. Even if two buffers have the same byte count, they may not be interchangeable due to different formats or modifiers. See KMS documentation for objects and attributes.

Calculate pitch and offset for a linear image

Consider a 3×2 linear XRGB8888 image, where each pixel occupies 4 bytes; assume a row pitch of 16 bytes, and the image starts at offset=8 of the underlying object. Each row's actual pixels occupy 12 bytes, but the next row advances by 16 bytes. The starting position of pixel (x, y) is offset + y × pitch + x × 4, so the last pixel (2, 1) is located at bytes 32–35, rather than packing the six pixels tightly from 8 to 31.

Interval (inclusive left, exclusive right)Content
[0, 8)Object space before this image
[8, 20)Three pixels of row 0
[20, 24)Inter-row padding
[24, 36)Three pixels of row 1
def pixel_span(width, height, pitch, offset, object_size, x, y):
    if min(width, height) <= 0 or offset < 0 or pitch < width * 4:
        raise ValueError("Invalid linear layout")
    end = offset + (height - 1) * pitch + width * 4
    if end > object_size or not (0 <= x < width and 0 <= y < height):
        raise ValueError("Pixel or image out of bounds")
    start = offset + y * pitch + x * 4
    return start, start + 4

assert pixel_span(3, 2, 16, 8, 36, 2, 1) == (32, 36)
assert pixel_span(3, 2, 16, 8, 36, 0, 1) == (24, 28)
for args in [(3, 2, 16, 8, 35, 2, 1), (3, 2, 8, 8, 36, 0, 0),
             (3, 2, 16, 8, 36, 3, 0), (3, 2, 16, 8, 36, 0, 2)]:
    try:
        pixel_span(*args)
    except ValueError:
        pass
    else:
        raise AssertionError("Invalid layout or coordinates should be rejected")

The 36 bytes here are just the mathematical lower bound covering the last valid pixel; real allocations are also affected by driver alignment, page size, and interface requirements. Use the pitch and size returned by the interface. Python integers do not overflow; when porting the formula to C, you must also check for multiplication-addition overflow.

This addressing scheme only applies to the assumed linear, single-format-plane layout. Storage with tile or compressed modifiers cannot be addressed this way; multi-plane pixel formats like NV12 require describing the pitch and offset of each plane separately. The "plane" in pixel formats is also not the same concept as the KMS display overlay Plane: a single KMS Plane can scan out an image composed of multiple format planes.

3. Atomic Modesetting: Validate a Set of States First

User space submits a collection of object attributes via atomic ioctls, such as placing a specific framebuffer on a plane and setting the CRTC mode and connector routing. The kernel constructs states for relevant objects, checking resources, formats, bandwidth, and driver constraints; drm_atomic_state is an internal kernel structure, not a generic C structure allocated directly by user space and passed in.

TEST_ONLY can validate without changing the display state, but passing a test does not reserve resources for future submissions. Actual submissions still require checking current conditions.

The value of atomic is to avoid user space having to modify objects one by one and manually handle unusable intermediate configurations. It does not promise that all hardware registers change in a single CPU instruction, nor does it guarantee that any mode change operation is completely black-screen-free. When involving multiple CRTCs, it cannot be deduced that all monitors physically refresh simultaneously.

Why must combinations be validated, not just each object individually?

Suppose two display pipelines share a teaching bandwidth budget of 10, with each currently consuming 4. When testing A increasing to 6 alone, the total is 6+4=10; testing B increasing to 6 alone also passes. However, the combination of both increasing to 6 requires 12 and must be rejected. Locally valid configurations that pass individually cannot be pieced together into a valid global configuration.

Similarly, after testing A's upgrade passes, if B submits its upgrade first, when A is actually submitted later, it must face the new state 4+6. The significance of TEST_ONLY is to check candidate states, not to reserve that remaining resource. Actual drivers also check conditions such as clocks, routing, scalers, and links; this single number only helps understand shared constraints and cannot be used to calculate specific GPU available display bandwidth.

Returning success vs. screen already displaying

Blocking and non-blocking commits have different wait behaviors. After accepting a non-blocking commit, hardware work can continue; completion must be determined via events or output fences. Input fences can indicate rendering dependencies for new buffers.

Standard page flips often pair with vblank to avoid tearing, but async updates, full modesets, and different driver paths have their own constraints and cannot be uniformly written as "verified, tear-free atomic application on vblank." Reuse of old framebuffers should follow corresponding completion and release protocols and cannot rely solely on the ioctl having returned.

New frame starting to display does not mean the new frame can immediately overwrite

Suppose CRTC A is currently scanning buffer X and preparing to page flip to Y. Y's input fence completion indicates the renderer has finished writing Y; A's page flip completion indicates this display switch has completed, and the old scan usage of X on A can end. At this point, Y is the current scan object, not the object just released. If CRTC B is still scanning X, X cannot be returned to the producer for overwriting just because A has finished flipping.

A has switched to Y, X may still be used by another pipeline Initial A → X; B → X X: Still being scanned A page flip complete A → Y; B → X X: Still not overwritable B page flip complete A → Y; B → Z X: No scan usage Y becomes the current frame on A, but still needs to wait for subsequent replacement to be reclaimed Only scan usage is drawn; other consumers must also complete before the buffer can be safely reused.

Below, we record "usage" as (CRTC, buffer) relationships, simulating only one scan layer per CRTC and at most one pending flip per CRTC at any time. Completion notifications are handled according to their respective CRTCs, without assuming two screens complete simultaneously.

class Scanout:
    def __init__(self):
        self.current = {"A": "X", "B": "X"}
        self.pending = {}
        self.ready = {"X"}

    def render_done(self, buffer):
        self.ready.add(buffer)

    def submit(self, crtc, new):
        assert crtc in self.current and crtc not in self.pending
        self.pending[crtc] = new           # Accepting request does not mean switch is complete

    def complete_flip(self, crtc):
        new = self.pending[crtc]
        if new not in self.ready:
            raise RuntimeError("Input rendering dependency not yet complete")
        self.current[crtc] = new
        del self.pending[crtc]

    def reusable(self, buffer):
        return buffer not in self.current.values() and buffer not in self.pending.values()

s = Scanout()
s.submit("A", "Y")
assert not s.reusable("X") and not s.reusable("Y")
try:
    s.complete_flip("A")
except RuntimeError:
    pass
else:
    raise AssertionError("Cannot bypass input dependencies")
s.render_done("Y")
s.complete_flip("A")
assert not s.reusable("X")                 # B is still using X
assert not s.reusable("Y")                 # A is scanning Y
s.render_done("Z")
s.submit("B", "Z")
assert not s.reusable("X")                 # ioctl acceptance is still insufficient to release
s.complete_flip("B")
assert s.reusable("X")
assert not s.reusable("Y") and not s.reusable("Z")

Reusability in the model only indicates no remaining scan or pending submission usage; real systems must also merge release conditions for other consumers such as GPUs and video encoders. OUT_FENCE_PTR belongs to the CRTC and cannot be interpreted as a global "unused" proof for any dma-buf; writeback output fences indicate write completion for the writeback target and should be interpreted according to the corresponding attribute protocol.

Render into A or B, queue a commit, and advance vblank. The screen keeps scanning the current frame until the pending frame meets its fence and presentation conditions.

Preparing the visual
Rendering, accepted commit, and presentation happen separately

Buffer existence does not mean rendering is complete, and successful atomic submission does not mean presentation. The old buffer can be overwritten only after its scanout reference is released.

4. GEM: Objects, Handles, and Actual Storage

GEM provides graphics buffer objects and related lifecycle frameworks; allocation methods are determined by the driver and specific interfaces, and there is no universally callable drm_gem_object_create(dev, size) to represent all GPU allocations.

Object storage can come from system memory or VRAM; mechanisms like TTM can manage placement and migration, but not all GEM drivers must use TTM. CPU mapping, GPU mapping, and display scan capabilities are established separately and are not naturally all available after creating an object.

Cross-process sharing: Pass references, do not copy integer handles

GEM handles usually belong to a specific DRM file context. Sending an integer handle to another process that independently opens a DRM device will not automatically point to the same object. Cross-process sharing is often done by exporting a dma-buf fd and passing it via SCM_RIGHTS of a Unix socket; the receiver obtains its own reference and imports it according to the interface. See DRM Memory Management.

5. dma-buf: Sharing Underlying Storage Also Requires Coordinating Access

Example: An application renders a frame, the compositor receives its dma-buf, and decides whether to composite it as a texture into an output buffer or, if conditions are met, directly hand it to the display plane for scanning.

Both paths can share the application's underlying storage, but the latter also requires all constraints on image format, modifier, scaling, position, output color processing, etc., to be met. Successful import does not guarantee direct scanout; cross-device may also require migration or copying, so dma-buf is not an unconditional zero-copy promise.

Handoff DirectionMust Wait For What
Producer hands new frame to consumerGPU rendering complete before consumer can read
Consumer returns old frame to producerDisplay or other consumption complete before producer can overwrite

References guarantee the object still exists, but do not prove there are no read/write races.

Implicit synchronization can track dependencies via reservation objects and fences; explicit synchronization passes sync objects like fences and syncobjs via API/protocol. Which mechanism to choose depends on the entire path. Cache synchronization when CPU accesses dma-buf cannot replace waiting for GPU work completion: cache visibility and task completion are different conditions. See dma-buf documentation.

6. fbdev and Troubleshooting Entry Points

fbdev provides traditional framebuffer interfaces with limited functionality and consistent resource modeling capabilities, but historically there were interfaces for acceleration and waiting for vertical sync, so it is absolutely incorrect to say "no GPU acceleration, no vsync." Modern DRM drivers can provide fbdev compatibility layers based on configuration, and not every system necessarily exposes /dev/fb0.

When troubleshooting, locate issues by boundaries: Did the application successfully submit rendering? Did the fence complete? Did the compositor receive the buffer? Did the KMS configuration pass? Is the connector connected? Do not attribute every black screen to "VRAM not allocated."

Read-only view of nodes and connection status:

ls -l /dev/dri/
from pathlib import Path

for status in sorted(Path('/sys/class/drm').glob('card*-*/status')):
    try:
        print(status.parent.name, status.read_text().strip())
    except OSError as exc:
        print(status.parent.name, type(exc).__name__)

connected only indicates connection detection status and does not prove that the output is scanning a valid image; the absence of these directories does not prove the system has no GPU at all, as the device may only provide render capabilities.