Atomic KMS Output
Atomic KMS Output System
1. Overview
The AtomicKMSOutput class handles display output using the Linux DRM (Direct Rendering Manager) atomic Mode-Setting (KMS) API. It provides zero-copy DMA-BUF import, explicit synchronization between GPU rendering and display scanout, hardware cursor support, and Variable Refresh Rate (VRR) capabilities.
Key Features
- Zero-copy DMA-BUF import: Vulkan renders directly to buffers that DRM displays without copying
- Explicit sync: GPU and display synchronize via kernel fences, eliminating CPU blocking
- Hardware cursor: Independent cursor plane updated at 1000Hz+ via async commits
- VRR support: AMDGPU Freesync/VRR for variable refresh rates
- Triple buffering: Proper framebuffer management for smooth frame pacing
Architecture
2. Plane Property Caching
Atomic commits require setting multiple properties on DRM objects (planes, CRTCs, connectors). Property names are string-based, and looking them up by name on every frame would be prohibitively expensive.
The PlaneProperties structure caches property IDs during initialization:
struct PlaneProperties {
uint32_t plane_id = 0; // Plane object ID
uint32_t type_prop = 0; // Plane type (primary/overlay/cursor)
uint32_t fb_prop = 0; // FB_ID property
uint32_t crtc_prop = 0; // CRTC_ID property
uint32_t src_x_prop = 0; // Source X position (16.16 fixed)
uint32_t src_y_prop = 0; // Source Y position (16.16 fixed)
uint32_t src_w_prop = 0; // Source width (16.16 fixed)
uint32_t src_h_prop = 0; // Source height (16.16 fixed)
uint32_t dst_x_prop = 0; // Destination X position
uint32_t dst_y_prop = 0; // Destination Y position
uint32_t dst_w_prop = 0; // Destination width
uint32_t dst_h_prop = 0; // Destination height
uint32_t in_fence_fd_prop = 0; // IN_FENCE_FD property (explicit sync)
};
Discovery Process
During init(), the system discovers planes compatible with the CRTC and caches their properties:
- Get CRTC index: The
possible_crtcsbitmask uses CRTC index bits, not CRTC IDs - Find planes: Iterate through all planes, checking
possible_crtcscompatibility - Check type: Use the
typeproperty to identifyDRM_PLANE_TYPE_PRIMARYorDRM_PLANE_TYPE_CURSOR - Cache properties: Look up each property by name and store the ID
Driver Compatibility
The cache_plane_properties() function supports fallback property names for driver compatibility:
| Preferred | Fallback | Purpose |
|---|---|---|
DST_X | CRTC_X | Destination X position |
DST_Y | CRTC_Y | Destination Y position |
DST_W | CRTC_W | Destination width |
DST_H | CRTC_H | Destination height |
3. CRTC Properties
The CRTCProperties structure caches CRTC-level properties:
struct CRTCProperties {
uint32_t crtc_id = 0; // CRTC object ID
uint32_t mode_id_prop = 0; // MODE_ID property (blob)
uint32_t active_prop = 0; // ACTIVE property
uint32_t out_fence_ptr_prop = 0; // OUT_FENCE_PTR property (explicit sync)
uint32_t vrr_enabled_prop = 0; // VRR_ENABLED property (AMDGPU Freesync/VRR)
};
Property Descriptions
- MODE_ID: Blob property containing display mode (resolution, refresh rate). Only set on first frame (modeset).
- ACTIVE: Boolean enabling/disabling the CRTC. Set to 1 during modeset.
- OUT_FENCE_PTR: Pointer to memory where DRM writes the release fence FD. Used for explicit sync.
- VRR_ENABLED: Enables Variable Refresh Rate on AMDGPU. Must be set on every commit.
4. Connector Properties
The ConnectorProperties structure links the connector to the CRTC:
struct ConnectorProperties {
uint32_t connector_id = 0; // Connector object ID
uint32_t crtc_id_prop = 0; // CRTC_ID property (links connector to CRTC)
};
CRITICAL: Connector-CRTC Linkage
The CRTC_ID property on the connector is mandatory for a functional display pipeline. Without setting this property in the atomic commit, the connector is not linked to the CRTC, and the screen remains black even though the commit succeeds.
5. Framebuffer Slot Management
The system does not use a framebuffer cache. Instead, each import_dma_buf() call creates a unique framebuffer ID. This is critical for proper triple buffering:
struct FramebufferSlot {
uint32_t fb_id; // DRM framebuffer ID
uint32_t gem_handle; // GEM handle (for cleanup)
int dma_buf_fd; // Original DMA-BUF FD (if we own it)
uint64_t sequence_number; // Frame sequence for debugging
};
Triple Buffering with Unique FB IDs
GEM Handle Reference Counting
The kernel’s GEM handle reference counting ensures the DMA-BUF memory stays alive until all framebuffers are destroyed:
6. DMA-BUF Import Process
The import_dma_buf() function imports a DMA-BUF file descriptor into DRM:
Format and Modifiers
- Format:
DRM_FORMAT_XBGR8888(matchesVK_FORMAT_R8G8B8A8_UNORM) - Modifier: Explicit modifier from
frame->modifier(e.g.,DRM_FORMAT_MOD_INVALIDfor linear, or tile-specific modifiers) - Flags:
DRM_MODE_FB_MODIFIERSto enable modifier support
Cleanup
After the atomic commit succeeds, the previous framebuffer is released via release_framebuffer():
drmModeRmFB(drm_fd_, old_fb_id); // Remove framebuffer
drmCloseBufferHandle(drm_fd_, gem_handle); // Close GEM handle
7. Explicit Synchronization
Explicit sync eliminates CPU blocking by having the GPU and display synchronize via kernel-managed fences.
Fences
- IN_FENCE_FD: Render completion fence from Vulkan. Tells DRM “don’t show this buffer until the GPU finishes rendering.”
- OUT_FENCE_PTR: Release fence from DRM. Tells the GPU “don’t render again until this buffer is no longer on screen.”
AMDGPU OUT_FENCE Encoding
AMDGPU encodes the OUT_FENCE FD in a specific format:
8. submit_frame() Detailed Flow
The submit_frame() method orchestrates the entire frame submission process:
EBUSY Retry Logic
The kernel returns EBUSY when a previous commit is still processing. The code retries up to 3 times with a 5ms delay:
constexpr int MAX_EBUSY_RETRIES = 3;
constexpr int EBUSY_RETRY_DELAY_MS = 5;
do {
ret = drmModeAtomicCommit(drm_fd_, req, commit_flags, user_data);
if (ret == -EBUSY && retry_count < MAX_EBUSY_RETRIES) {
usleep(EBUSY_RETRY_DELAY_MS * 1000);
continue;
}
break;
} while (true);
9. Hardware Cursor Integration
Hardware cursors use a dedicated cursor plane that overlays the primary plane:
Cursor Plane Properties
The cursor plane uses the same property types as the primary plane, but with different semantics:
| Property | Cursor Value | Description |
|---|---|---|
| FB_ID | cursor_fb_id_ | Hardware cursor framebuffer |
| CRTC_ID | crtc_id_ | Link to CRTC |
| SRC_X/Y | 0 << 16 | No source cropping (usually) |
| SRC_W/H | cursor_size_ << 16 | Full cursor size |
| DST_X/Y | cursor_x/y - hotspot | Screen position with hotspot offset |
| DST_W/H | cursor_size_ | Full cursor size (hardware clips) |
Hotspot Offset
The hotspot is the point within the cursor image that aligns with the mouse coordinates:
Hiding the Cursor
To hide the cursor, set both FB_ID=0 AND CRTC_ID=0:
drmModeAtomicAddProperty(req, cursor_plane_.plane_id, cursor_plane_.fb_prop, 0);
drmModeAtomicAddProperty(req, cursor_plane_.plane_id, cursor_plane_.crtc_prop, 0);
10. Async Cursor Position Updates
Cursor updates use DRM_MODE_PAGE_FLIP_ASYNC for immediate, non-blocking updates:
update_cursor_position_async()
This function bypasses the normal commit queuing logic:
- Validate: Check CRTC is enabled (first frame completed)
- Allocate request: Lightweight cursor-only atomic request
- Add properties: Only cursor plane DST_X/Y changed
- Commit with ASYNC flag:
DRM_MODE_PAGE_FLIP_ASYNC - No VBlank wait: Takes effect immediately on next scanline
Why Include Cursor in Primary Commits?
Even though cursor updates happen at 1000Hz via async commits, the primary 60Hz commit MUST include the latest cursor position. Otherwise, the cursor will “jump back” to the old position for one frame when the primary commit occurs.
11. VRR Support
Variable Refresh Rate (VRR) allows the display to refresh immediately when a frame is ready, reducing latency:
if (vrr_enabled_ && crtc_props_.vrr_enabled_prop != 0) {
drmModeAtomicAddProperty(req, crtc_id_, crtc_props_.vrr_enabled_prop, 1);
}
VRR Property Location
- AMDGPU: VRR property is on the CRTC (search for
VRR_ENABLED,vrr_enabled, orfreesync) - VRR must be set on every commit, not just the first frame
Enabling VRR
Call set_vrr_enabled(true) before the first frame submission:
output.set_vrr_enabled(true);
output.submit_frame(...); // First frame enables VRR
12. ASCII Diagrams
Atomic Commit Flow
Property Relationships
Triple Buffer Timeline
Key points:
- Each render creates a NEW FB ID (100, 101, 102, …)
- Each buffer cycles: Render → Display → Render
- GPU never overwrites buffer while DRM displays it
- OUT_FENCE from frame N signals when frame N leaves screen
Cursor Update Flow
13. RAII Wrappers (drm_raii.h)
The drm_raii.h header provides RAII wrappers for DRM resources to prevent leaks:
// RAII wrapper for drmModeConnector
using DrmModeConnectorPtr = std::unique_ptr<drmModeConnector, DrmModeConnectorDeleter>;
// RAII wrapper for property blobs with FD management
struct PropertyBlobPtr {
int fd{-1};
drmModePropertyBlobRes* ptr{nullptr};
~PropertyBlobPtr() {
if (ptr && fd >= 0) {
drmModeDestroyPropertyBlob(fd, ptr->id);
}
}
};
Usage
DrmUniquePtr<drmModeRes> resources(drmModeGetResources(drm_fd_));
// Automatically calls drmModeFreeResources() when going out of scope
14. Error Codes
The import_dma_buf() function returns Result<> types with specific error codes:
| Error Code | Description |
|---|---|
DRMInvalidFileDescriptor | Invalid DRM device FD |
DRMResourcesFailed | Failed to get DRM resources |
DRMGetCrtcFailed | Failed to get CRTC |
DRMPlaneNotFound | No compatible plane found |
DRMPropertyNotFound | Required property not found |
DRMDmaBufImportFailed | Failed to import DMA-BUF |
DRMFramebufferFailed | Failed to create framebuffer |
DRMAtomicAllocFailed | Failed to allocate atomic request |
DRMAtomicCommitFailed | Atomic commit failed |
DRMAsyncUpdateNotSupported | Driver doesn’t support async updates |