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:
- DRM dumb buffer creation - Allocates GPU-accessible buffers for cursor images
- Hardware cursor plane management - Creates DRM framebuffers compatible with cursor planes
- Plugin integration - Interfaces with
CursorProvidercallbacks from plugins
What It Does NOT Handle
- Cursor theme loading - Handled by plugins (e.g.,
desktopplugin viaCursorManager) - Cursor positioning - Managed by
DisplayManagerduring atomic commits - Cursor visibility toggling - Managed by
DisplayManager
Architecture
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:
- Allocate -
DRM_IOCTL_MODE_CREATE_DUMBcreates a buffer object - Map -
DRM_IOCTL_MODE_MAP_DUMBgets the mmap offset - Copy -
mmap()+memcpy()transfers pixel data - 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:
| Size | Usage |
|---|---|
| 64x64 | Default, works on all GPUs |
| 128x128 | High-DPI displays |
| 256x256 | Very high-DPI displays |
Note: Other GPU drivers may support arbitrary sizes, but CursorService uses the AMD-compatible set for maximum compatibility.
Size Selection Algorithm
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
Buffer Refresh Mechanism
When Buffers Are Refreshed
Cursor buffers are refreshed when:
- Initial registration - When plugin first provides a cursor provider
- Shape change - When
CursorManagerloads a new cursor shape (e.g., pointer → move) - 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:
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
| Property | Requirement |
|---|---|
| Format | ARGB8888 (DRM_FORMAT_ARGB8888) |
| Alpha | Non-premultiplied (straight alpha) |
| Alignment | 32-bit pixels |
| Stride | Width × 4 bytes |
Size Requirements
| Property | Requirement |
|---|---|
| Dimensions | Square (width == height) |
| Minimum | 1×1 pixel |
| Maximum | 256×256 (AMDGPU limit) |
| Recommended | 24, 32, 48, or 64 pixels |
Common Cursor Sizes
| Shape | Typical Size |
|---|---|
| Normal pointer | 24×24 or 32×32 |
| Resize arrows | 24×24 or 32×32 |
| Move cursor | 32×32 |
| I-beam (text) | 24×24 |
Error Handling
Error Codes
| Error | Condition |
|---|---|
ErrorCode::CursorNotInitialized | CursorService::init() not called |
ErrorCode::DRMInvalidFileDescriptor | Invalid DRM FD passed to init() |
ErrorCode::InvalidParameter | Null pixels or invalid dimensions |
ErrorCode::DRMBufferCreateFailed | DRM_IOCTL_MODE_CREATE_DUMB failed |
ErrorCode::DRMBufferMapFailed | mmap() of dumb buffer failed |
ErrorCode::DRMFramebufferFailed | drmModeAddFB2() failed |
ErrorCode::CursorNoImageData | Provider callback returned null |
Cleanup on Error
The CursorService performs proper cleanup at each stage:
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
DisplayManageroperations are single-threaded- DRM operations must be serialized anyway
Performance Considerations
Buffer Creation Cost
Creating a cursor buffer involves:
- DRM ioctl calls - 3 syscalls (create, map, addfb)
- Memory allocation - GPU dumb buffer (typically 64×64×4 = 16KB)
- 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.