Skip to content
Waxed Display Server
← Back to Docs

Cursor Service

Cursor Service

Overview

The CursorService is a core system component that manages hardware cursor planes in Waxed. It bridges the gap between plugin-provided cursor images and DRM/KMS hardware cursor functionality.

Responsibilities

The CursorService has a focused, single-responsibility design:

  1. DRM dumb buffer creation - Allocates GPU-accessible buffers for cursor images
  2. Hardware cursor plane management - Creates DRM framebuffers compatible with cursor planes
  3. Plugin integration - Interfaces with CursorProvider callbacks from plugins

What It Does NOT Handle

  • Cursor theme loading - Handled by plugins (e.g., desktop plugin via CursorManager)
  • Cursor positioning - Managed by DisplayManager during atomic commits
  • Cursor visibility toggling - Managed by DisplayManager

Architecture

DRM / Hardware Layer

DisplayManager

CursorService

Plugin (e.g. desktop)

1. Load cursor theme

(hyprcursor)
2. Create DRM buffer

(dumb buffer)
3. Update cursor plane

CursorManager

(theme loading)

CursorProvider

(callback)

Cursor Plane Management

Pixels (ARGB)

DRM Dumb Buffer

(GEM handle)

Cursor Plane

(fb_id)


Hardware Cursor Buffer Creation

DRM Dumb Buffers

The CursorService creates DRM dumb buffers - simple, CPU-accessible GPU buffers suitable for small images like cursors. The process:

  1. Allocate - DRM_IOCTL_MODE_CREATE_DUMB creates a buffer object
  2. Map - DRM_IOCTL_MODE_MAP_DUMB gets the mmap offset
  3. Copy - mmap() + memcpy() transfers pixel data
  4. Framebuffer - drmModeAddFB2() creates a DRM framebuffer

Buffer Format

Cursor buffers use DRM_FORMAT_ARGB8888 (32 bits per pixel):

Byte Order: [A][R][G][B]  (little-endian: BGRA in memory)
           ^
           Most significant byte = Alpha

Important: The plugin provides pixels in ARGB format (matching DRM’s native format), not RGBA. This avoids conversion overhead.


Cursor Size Handling

AMDGPU Constraints

AMD GPUs have fixed cursor size support - only three sizes are valid:

SizeUsage
64x64Default, works on all GPUs
128x128High-DPI displays
256x256Very high-DPI displays

Note: Other GPU drivers may support arbitrary sizes, but CursorService uses the AMD-compatible set for maximum compatibility.

Size Selection Algorithm

Yes

No

Yes

No

Start

Query DRM_CAP_CURSOR_WIDTH

and DRM_CAP_CURSOR_HEIGHT

Size fits constraints?

Size >= visual_cursor_size

Size <= driver_max_width

Size <= driver_max_height

Use smallest AMDGPU size

(64, 128, or 256)

Visual size exceeds max?

Fallback to 256x256

Use driver's reported max

Done

Visual vs Hardware Size

  • Visual size: The actual cursor image size from the theme (e.g., 24px for a normal cursor)
  • Hardware size: The buffer size allocated (64, 128, or 256)

The hardware buffer is often larger than the visual cursor. This is handled at composition time.


Integration with CursorProvider

CursorProvider Structure

namespace waxed {

struct CursorBuffer {
    const uint8_t* pixels;    // ARGB pixel data
    uint32_t width;           // Cursor width
    uint32_t height;          // Cursor height
    int32_t hotspot_x;        // Hotspot X offset
    int32_t hotspot_y;        // Hotspot Y offset
    uint32_t stride;          // Bytes per row
};

using CursorProviderCallback = auto (*)(void* user_data) -> const CursorBuffer*;

struct CursorProvider {
    CursorProviderCallback callback;
    void* user_data;
};

} // namespace waxed

Provider Registration Flow

Plugin (desktop)

1. Register cursor provider

CoreAPI::

register_cursor_provider

(callback, user_data)

2. Store provider in PluginManager

cursor_provider_
3. Propagate to DisplayManager

set_cursor_provider()
4. Initial buffer refresh

refresh_cursor_buffers()
5. Create buffers for all displays

CursorService::

create_cursor_from_provider()

6. Call plugin callback

Plugin callback returns CursorBuffer*

7. Create DRM framebuffer

create_drm_buffer()

Buffer Refresh Mechanism

When Buffers Are Refreshed

Cursor buffers are refreshed when:

  1. Initial registration - When plugin first provides a cursor provider
  2. Shape change - When CursorManager loads a new cursor shape (e.g., pointer → move)
  3. Display hotplug - When a new display is connected

Refresh Process

auto DisplayManager::refresh_cursor_buffers() -> void {
    for (auto& display : displays_) {
        // Create new cursor buffer from plugin
        auto cursor_buffer = cursor_service_.create_cursor_from_provider(cursor_provider_);

        if (cursor_buffer) {
            // Update the display's cursor plane
            display->output.update_cursor_buffer(
                cursor_buffer->fb_id,
                cursor_buffer->gem_handle,
                cursor_buffer->hotspot_x,
                cursor_buffer->hotspot_y,
                cursor_buffer->size
            );
        }
    }
}

Per-Display Cursor Buffers

Each display (DisplayOutput) maintains its own cursor buffer:

Display 3 (DP-2)

output.update_cursor_buffer

(fb_id_3, ...)

Display 2 (DP-1)

output.update_cursor_buffer

(fb_id_2, ...)

Display 1 (HDMI-1)

output.update_cursor_buffer

(fb_id_1, ...)

DisplayManager

Why separate buffers?

Each DRM CRTC has its own cursor plane

with unique framebuffer requirements.

Same visual cursor replicated into

multiple DRM buffers.


Hotspot Handling

The hotspot is the cursor’s “active point” - the pixel that aligns with the mouse position.

Example: Pointer cursor (left_ptr)

    +-- hotspot (2, 2)
    v
    . . # # # # # . .
    . . # # # # # . .
    # # # # # # # . .
    # # # # # # # . .
    # # # # # # # . .
    # # . . # . . . .
    # . . . # . . . .
    # . . . . . . . .

Hotspot in CursorBuffer

// Plugin provides:
struct CursorBuffer {
    int32_t hotspot_x;  // Offset from left edge
    int32_t hotspot_y;  // Offset from top edge
    ...
};

// CursorService stores in core::CursorBuffer:
struct CursorBuffer {
    int hotspot_x{0};
    int hotspot_y{0};
    ...
};

The hotspot is passed through unchanged and used by DisplayManager when positioning the cursor plane.


Format Requirements

Pixel Format

PropertyRequirement
FormatARGB8888 (DRM_FORMAT_ARGB8888)
AlphaNon-premultiplied (straight alpha)
Alignment32-bit pixels
StrideWidth × 4 bytes

Size Requirements

PropertyRequirement
DimensionsSquare (width == height)
Minimum1×1 pixel
Maximum256×256 (AMDGPU limit)
Recommended24, 32, 48, or 64 pixels

Common Cursor Sizes

ShapeTypical Size
Normal pointer24×24 or 32×32
Resize arrows24×24 or 32×32
Move cursor32×32
I-beam (text)24×24

Error Handling

Error Codes

ErrorCondition
ErrorCode::CursorNotInitializedCursorService::init() not called
ErrorCode::DRMInvalidFileDescriptorInvalid DRM FD passed to init()
ErrorCode::InvalidParameterNull pixels or invalid dimensions
ErrorCode::DRMBufferCreateFailedDRM_IOCTL_MODE_CREATE_DUMB failed
ErrorCode::DRMBufferMapFailedmmap() of dumb buffer failed
ErrorCode::DRMFramebufferFaileddrmModeAddFB2() failed
ErrorCode::CursorNoImageDataProvider callback returned null

Cleanup on Error

The CursorService performs proper cleanup at each stage:

No

Yes

No

Yes

No

Yes

No

Yes

create_drm_buffer()

DRM_IOCTL_MODE_CREATE_DUMB

Success?

Return error

DRM_IOCTL_MODE_MAP_DUMB

Success?

DRM_IOCTL_MODE_DESTROY_DUMB

Return error

mmap()

Success?

memcpy()

munmap()

drmModeAddFB2()

Success?

Return (gem_handle, fb_id)


Public API

Initialization

CursorService service;
auto result = service.init(drm_fd);
if (!result) {
    // Handle error
}

Creating Cursor from Pixels

auto cursor = service.create_cursor_from_pixels(
    pixels,      // const uint8_t* - ARGB pixel data
    width,       // int - image width
    height,      // int - image height
    stride,      // int - bytes per row
    hotspot_x,   // int - hotspot offset
    hotspot_y    // int - hotspot offset
);

if (cursor) {
    uint32_t fb_id = cursor->fb_id;
    // Use fb_id for cursor plane
}

Creating Cursor from Provider

auto cursor = service.create_cursor_from_provider(cursor_provider);
if (cursor) {
    // Cursor buffer ready for hardware plane
}

Releasing Buffers

service.release_cursor_buffer(buffer);
// buffer.fb_id and buffer.gem_handle are now 0

Usage Example

Complete example from the DisplayManager:

void DisplayManager::refresh_cursor_buffers() {
    if (!cursor_service_.is_initialized()) {
        LOGC_DEBUG("Cursor service not initialized");
        return;
    }

    if (!cursor_provider_ || !cursor_provider_->callback) {
        LOGC_DEBUG("No cursor provider registered");
        return;
    }

    for (auto& display : displays_) {
        // Create cursor buffer from plugin
        auto cursor_buffer = cursor_service_.create_cursor_from_provider(
            cursor_provider_
        );

        if (!cursor_buffer) {
            LOGC_WARN("Failed to create cursor buffer: {}", cursor_buffer.error().message());
            continue;
        }

        // Update display's cursor plane
        display->output.update_cursor_buffer(
            cursor_buffer->fb_id,
            cursor_buffer->gem_handle,
            cursor_buffer->hotspot_x,
            cursor_buffer->hotspot_y,
            cursor_buffer->size
        );

        LOGC_DEBUG("Updated cursor for display {} (fb={})",
                   display->display_id, cursor_buffer->fb_id);
    }
}

Thread Safety

The CursorService is not thread-safe by design. All cursor operations must occur on the main compositor thread. This is enforced by the Waxed threading model:

  • Plugin callbacks run on the main thread
  • DisplayManager operations are single-threaded
  • DRM operations must be serialized anyway

Performance Considerations

Buffer Creation Cost

Creating a cursor buffer involves:

  1. DRM ioctl calls - 3 syscalls (create, map, addfb)
  2. Memory allocation - GPU dumb buffer (typically 64×64×4 = 16KB)
  3. Memory copy - CPU to GPU transfer

Total time: ~1-5ms per buffer

Optimization Strategy

  • Cache cursor buffers - Don’t recreate on every frame
  • Only refresh on shape change - Not on mouse movement
  • Per-display caching - Each display holds its buffer until shape changes

Memory Usage

For a 3-monitor setup with 64×64 cursors:

Per buffer: 64 × 64 × 4 bytes = 16,384 bytes
Total: 16,384 × 3 displays = 49,152 bytes (~48 KB)

Memory usage is negligible compared to framebuffers.