Skip to content
Waxed Display Server
← Back to Docs

Display Manager

Display Manager

Overview

The DisplayManager is responsible for discovering, configuring, and managing all physical displays connected to the system via DRM/KMS. It provides a thread-safe API for multi-monitor setups, handles hotplug events, manages core-allocated swapchain buffers, and supports Variable Refresh Rate (VRR).

Key Responsibilities:

  • Discover connected displays via DRM/KMS
  • Allocate and manage CRTC resources
  • Provide atomic snapshot API for display configurations
  • Handle display hotplug (connect/disconnect)
  • Support VRR (Variable Refresh Rate / Adaptive Sync)
  • Allocate core-managed swapchain buffers (DMA-BUF export from Vulkan)
  • O(1) CRTC ID lookup for page flip handling
  • Thread-safe callback-based iteration API

DisplayState Structure

DisplayState represents a single physical monitor and contains all state needed for rendering and display management.

struct DisplayState {
    uint32_t display_id{UINT32_MAX};           // Unique identifier
    AtomicKMSOutput output;                    // DRM atomic commit interface
    DisplayMode mode;                          // Resolution, refresh rate, VRR
    DisplayBufferState buffer_state;           // Triple buffer management
    DisplayRuntimeState runtime;               // RenderLoop tracking
    std::atomic<bool> is_active{true};         // On/off state
    std::atomic<uint64_t> frame_counter{0};    // Frame counter
};

Key Properties:

  • Non-copyable, non-movable: DisplayBufferState contains atomics and mutex
  • RAII-managed: Vulkan resources automatically cleaned up on destruction
  • Thread-safe: Uses atomics for state tracking

DisplayState Hierarchy

DisplayManager

DisplayState[0]

DisplayState[1]

AtomicKMSOutput (DRM commits)

DisplayMode (resolution, refresh, VRR)

DisplayBufferState

DisplayRuntimeState

slots[3] (BufferSlot array)

swapchain_buffers[3] (CoreBuffer array with DMA-BUF)

write_index, current_slot, next_slot

commit_pending (EBUSY prevention)

first_frame_complete

bootstrap_failure_count

... similar structure

DisplayMode

Contains physical display properties from DRM/KMS:

struct DisplayMode {
    uint32_t width{0};           // Display width in pixels
    uint32_t height{0};          // Display height in pixels
    uint32_t refresh_rate{0};    // Refresh rate in Hz
    uint32_t connector_id{0};    // DRM connector ID
    uint32_t crtc_id{0};         // DRM CRTC ID
    bool vrr_capable{false};     // Hardware supports VRR
    bool vrr_enabled{false};     // VRR is currently active
};

DisplayConfig

Runtime configuration exposed to plugins via get_display_configs():

struct DisplayConfig {
    uint32_t display_id;
    DisplayRole role;            // Primary or Secondary
    DisplayLayoutMode layout_mode; // Mirror or Extend
    uint32_t width;
    uint32_t height;
    uint32_t refresh_rate;
    int32_t position_x;          // Virtual desktop position
    int32_t position_y;
    bool vrr_capable;
    bool vrr_enabled;
};

DisplayLayoutMode

Controls how multiple displays are arranged:

enum class DisplayLayoutMode : uint32_t {
    Mirror = 0,  // All displays show the same content
    Extend = 1,  // Displays extend the virtual desktop horizontally
};

Multi-Monitor Layout (Extend Mode)

Virtual Desktop: 3840x1080

Display 0 (Primary)

position_x=0

1920x1080

Display 1 (Secondary)

position_x=1920

1920x1080

In Mirror Mode:

  • All displays have position_x=0, position_y=0
  • All displays render identical content

DisplayRole

Identifies the primary vs secondary display:

enum class DisplayRole : uint32_t {
    Primary = 0,      // First display (display_id == 0)
    Secondary = 1,    // All subsequent displays
};

Usage:

  • The primary display receives special treatment for global UI elements
  • Display 0 is always considered primary
  • Used by plugins for window placement decisions

Hotplug Handling

The rescan_displays() method handles display connect/disconnect events:

auto rescan_displays() -> Result<size_t>;

Hotplug Process:

  1. Discovers all currently connected connectors
  2. Removes displays whose connector is no longer connected
  3. Adds newly connected displays (allocates CRTC, creates DisplayState)
  4. Updates CRTC ID lookup map
  5. Returns new display count

Thread Safety:

  • Acquires exclusive write lock during entire operation
  • Prevents race conditions with render loop

VRR (Variable Refresh Rate) Support

VRR (aka Adaptive Sync / FreeSync) allows the display to synchronize to the actual frame delivery time, reducing tearing and stuttering.

Detection

auto check_vrr_support(uint32_t connector_id) const -> bool;

Queries DRM properties for VRR capability:

  • "vrr_capable" on connector
  • "adaptive_sync" on connector
  • "VRR_CAPABLE" on connector

Enabling

auto try_enable_vrr(uint32_t connector_id, uint32_t crtc_id) const -> bool;

Attempts to enable VRR by checking for:

  • "VRR_ENABLED" on CRTC (AMDGPU)
  • "vrr_enabled" on CRTC or connector
  • "freesync" property

Mode Selection:

  • VRR-capable modes (60Hz+) are prioritized over EDID preferred modes
  • Scoring system: VRR bonus (1000000000000) + refresh_rate + preference_bonus

VRR Logging

Display 0 initialized: 1920x1080 @ 144Hz [VRR] (connector=42, crtc=50)

The [VRR] marker indicates VRR is enabled for that display.

Thread Safety

DisplayManager uses std::shared_mutex for read-write locking:

Read Lock (Shared Lock)

Multiple readers can access displays concurrently:

std::shared_lock lock(mutex_);

Used by:

  • get_display()
  • get_display_by_crtc_id()
  • get_display_count()
  • for_each_display()
  • get_display_configs()
  • refresh_cursor_buffers()

Write Lock (Exclusive Lock)

Only one writer, no concurrent readers:

std::unique_lock lock(mutex_);

Used by:

  • init()
  • rescan_displays()
  • Destructor (display cleanup)

Callback-Based Iteration

CRITICAL: for_each_display() is the ONLY safe way to iterate displays:

manager->for_each_display([](DisplayState& display) {
    // Display reference is valid for this callback only
    process_display(display);
});

Why callback-based?

  • Prevents iterator invalidation
  • No dangling references
  • Lock held for entire iteration duration

Anti-pattern (UNSAFE):

// DON'T DO THIS - Race condition!
auto display = manager->get_display(0);
// Lock released here, display may be invalidated
use_display(display);  // Potential use-after-free

CRTC ID Lookup

The page flip handler receives crtc_id directly from the kernel and needs O(1) lookup to find the corresponding DisplayState.

std::unordered_map<uint32_t, DisplayState*> crtc_id_to_display_;

Usage:

auto display = get_display_by_crtc_id(crtc_id);
if (display) {
    display->runtime.commit_pending.store(false);
}

Updated on:

  • Display initialization (init())
  • Hotplug (rescan_displays())

Swapchain Buffer Allocation

The core manages triple-buffered swapchains for each display using Vulkan with DMA-BUF export.

CoreBuffer

Represents a single swapchain buffer:

struct CoreBuffer {
    core::utils::UniqueFd fd;        // DMA-BUF file descriptor
    uint32_t width, height;          // Dimensions
    uint32_t stride;                 // Row pitch in bytes
    uint32_t format;                 // DRM_FORMAT_XBGR8888
    uint64_t modifier;               // Format modifier (0 = linear)
    uint32_t id;                     // Buffer index (0, 1, 2)

    // Vulkan handles (RAII-managed)
    std::unique_ptr<vk::raii::DeviceMemory> vk_memory;
    std::unique_ptr<vk::raii::Image> vk_image;
};

Allocation Process

auto allocate_swapchain_buffers(DisplayState& display) -> Result<void>;

Steps:

  1. Create Vulkan image with external memory (VK_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME)
  2. Allocate device memory with DMA-BUF export capability
  3. Bind image to memory
  4. Query actual row pitch from LINEAR tiling
  5. Export memory as DMA-BUF file descriptor
  6. Store in swapchain_buffers array

Key Details:

  • Uses linear tiling for DMA-BUF compatibility
  • Format: VK_FORMAT_R8G8B8A8_UNORM (matches DRM_FORMAT_XBGR8888)
  • Triple buffering: 3 buffers per display
  • RAII handles cleanup automatically

DisplayBufferState

Consolidates all buffer state for a display:

class DisplayBufferState {
    static constexpr size_t TRIPLE_BUFFER_COUNT = 3;

    std::array<BufferSlot, TRIPLE_BUFFER_COUNT> slots;
    std::array<CoreBuffer, TRIPLE_BUFFER_COUNT> swapchain_buffers;

    std::atomic<uint32_t> write_index{0};   // Next slot to acquire
    std::atomic<uint32_t> current_slot{0};  // Currently displayed
    std::atomic<uint32_t> next_slot{0};     // Queued for next frame

    std::mutex mutex;
};

Cursor Support

The DisplayManager integrates with CursorService for hardware cursor rendering:

auto set_cursor_provider(const CursorProvider* provider) -> void;
auto refresh_cursor_buffers() -> void;

Flow:

  1. Plugin registers cursor provider via core_register_cursor_provider()
  2. DisplayManager stores the provider
  3. On cursor shape change, refresh_cursor_buffers() is called
  4. Each display’s cursor buffer is updated via AtomicKMSOutput

Initialization Sequence

auto init(int drm_fd) -> Result<size_t>;

Steps:

  1. Initialize Vulkan context (for swapchain allocation)
  2. Initialize CursorService (Hyprcursor theme loading)
  3. Discover all connected connectors
  4. For each connector:
    • Allocate CRTC
    • Create DisplayState (select best mode, check VRR)
    • Initialize AtomicKMSOutput
    • Allocate swapchain buffers (DMA-BUF)
    • Add to displays vector and CRTC lookup map
  5. Return display count

Vulkan Context (DisplayManager-Managed)

The DisplayManager maintains its own Vulkan context, independent of plugin contexts:

std::unique_ptr<vk::raii::Context> vk_context_;
std::unique_ptr<vk::raii::Instance> vk_instance_;
std::unique_ptr<vk::raii::PhysicalDevice> vk_physical_device_;
std::unique_ptr<vk::raii::Device> vk_device_;
std::unique_ptr<vk::raii::Queue> vk_queue_;

Extensions Required:

  • VK_KHR_external_memory_capabilities
  • VK_KHR_external_semaphore_capabilities
  • VK_KHR_external_memory_fd
  • VK_KHR_external_semaphore_fd
  • VK_EXT_external_memory_dma_buf (preferred)

Destruction Order

CRITICAL: Displays must be destroyed BEFORE Vulkan context cleanup:

~DisplayManager() {
    {
        std::unique_lock lock(mutex_);
        displays_.clear();  // CoreBuffer RAII members destroyed first
    }
    // Then vk_device_, vk_instance_, vk_context_ destroyed
}

This ensures Vulkan device is still valid when swapchain_buffers are destroyed.

API Summary

MethodLock TypePurpose
init(drm_fd)WriteInitialize manager, discover displays
rescan_displays()WriteHandle hotplug, add/remove displays
for_each_display(cb)ReadIterate active displays
get_display_configs()ReadGet display configurations snapshot
get_display(id)ReadGet display by ID (immediate use only)
get_display_by_crtc_id(id)ReadGet display by CRTC (page flip handler)
get_display_count()ReadGet number of displays
set_cursor_provider(p)NoneSet cursor callback
refresh_cursor_buffers()ReadUpdate cursor on all displays
allocate_swapchain_buffers(d)NoneAllocate core-managed buffers