Skip to content
Waxed Display Server
← Back to Docs

Core API

Waxed Core API

⚠️ Unstable API

This API is under active development and is subject to change without notice. There are no stability guarantees until a V1.0.0 release. Plugin authors should expect interface changes and keep their code in sync with the latest Waxed source.

Overview

The Core API is the primary interface through which plugins interact with the Waxed compositor core. It provides controlled access to core functionality including plugin management, background rendering, Vulkan device access, input handling, display configuration, and cursor management.

The API follows a handle-based design: plugins receive a CoreAPI* handle during initialization that contains function pointers to all available core services. This design allows:

  • ABI stability: Functions can be added/removed without breaking existing plugins
  • Namespace isolation: Each plugin knows its own name via own_name
  • Controlled access: Plugins can only interact with core through defined interfaces

Core Architecture

Plugin

Waxed Core

CoreAPI* handle

Function calls

API calls

API calls

API calls

API calls

PluginManager

BackgroundSvc

ShaderService

InputManager

DisplayMgr

waxed_plugin_init

(receives CoreAPI)

core->funcs->load_plugin()

set_background()

get_shader_service()

get_input_state()

get_display_config()

CoreAPI Structure

The CoreAPI structure is the entry point for all plugin-to-core communication:

struct CoreAPI {
    void* internal_handle;        // PluginManager* (opaque to plugins)
    void* ipc_server_handle;      // IPCServer* (opaque to plugins)
    const CoreAPIFuncs* funcs;    // Function pointer table
    const char* own_name;         // This plugin's name
};

Usage notes:

  • internal_handle and ipc_server_handle are opaque pointers used internally by core functions
  • funcs points to the global function table containing all callable functions
  • own_name is the plugin’s own name for self-identification
  • Plugins should never access or modify internal_handle or ipc_server_handle directly

Function Reference

All Core API functions are accessed through the funcs member:

// Example pattern
core->funcs->load_plugin(core, "desktop");

Plugin Management Functions

load_plugin

auto (*load_plugin)(CoreAPI* core, const char* name) -> int;

Load another plugin by name.

Parameters:

  • core: Core API handle
  • name: Plugin name to load (e.g., “desktop”)

Return value:

  • 0 on success
  • Non-zero on error (plugin not found, load failed)

Usage example:

if (core->funcs->load_plugin(core, "transitioner") != 0) {
    // Handle error
}

Thread safety: Safe to call from any thread

Constraints:

  • Plugin path must be in the configured plugin directory
  • Plugin’s waxed_plugin_init will be called
  • Failed loads return error without modifying state

unload_plugin

auto (*unload_plugin)(CoreAPI* core, const char* name) -> int;

Unload a plugin by name.

Parameters:

  • core: Core API handle
  • name: Plugin name to unload

Return value:

  • 0 on success
  • Non-zero on error (plugin not found, cannot unload)

Usage example:

core->funcs->unload_plugin(core, "transitioner");

Thread safety: Safe to call from any thread

Constraints:

  • Plugin’s waxed_plugin_cleanup will be called
  • Vulkan provider cannot be unloaded if still in use
  • Self-unload should use request_self_unload instead

request_self_unload

auto (*request_self_unload)(CoreAPI* core) -> int;

Request that the current plugin be unloaded.

Parameters:

  • core: Core API handle

Return value:

  • 0 on success
  • Non-zero on error

Usage example:

// In a plugin's init function, if initialization fails:
if (init_failed) {
    core->funcs->request_self_unload(core);
    return nullptr;
}

Thread safety: Safe to call from any thread

Notes:

  • Safer than calling unload_plugin(core, core->own_name)
  • Uses the plugin’s own name from the CoreAPI handle
  • Cleanup will be called after the current function returns

is_plugin_loaded

auto (*is_plugin_loaded)(CoreAPI* core, const char* name) -> int;

Check if a plugin is currently loaded.

Parameters:

  • core: Core API handle
  • name: Plugin name to check

Return value:

  • 1 if plugin is loaded
  • 0 if plugin is not loaded
  • Negative value on error

Usage example:

if (core->funcs->is_plugin_loaded(core, "desktop") == 1) {
    // Desktop plugin is loaded
}

Thread safety: Safe to call from any thread


set_visible_plugin

auto (*set_visible_plugin)(CoreAPI* core, const char* name) -> int;

Set the currently visible plugin.

Parameters:

  • core: Core API handle
  • name: Plugin name to make visible

Return value:

  • 0 on success
  • Non-zero on error (plugin not found)

Usage example:

// Switch to desktop plugin
core->funcs->set_visible_plugin(core, "desktop");

Thread safety: Safe to call from any thread

Architectural notes:

  • Only one plugin is visible at a time
  • Visible plugin receives render callbacks
  • During transitions, both old and new plugins may be visible briefly

get_visible_plugin

auto (*get_visible_plugin)(CoreAPI* core, char* out_name, size_t max_len) -> int;

Get the name of the currently visible plugin.

Parameters:

  • core: Core API handle
  • out_name: Buffer to receive plugin name
  • max_len: Maximum buffer size (including null terminator)

Return value:

  • 0 on success
  • Non-zero on error (no visible plugin, buffer too small)

Usage example:

char visible_name[256];
if (core->funcs->get_visible_plugin(core, visible_name, sizeof(visible_name)) == 0) {
    printf("Visible plugin: %s\n", visible_name);
}

Thread safety: Safe to call from any thread


get_own_name

auto (*get_own_name)(CoreAPI* core, char* out_name, size_t max_len) -> int;

Get this plugin’s own name.

Parameters:

  • core: Core API handle
  • out_name: Buffer to receive plugin name
  • max_len: Maximum buffer size (including null terminator)

Return value:

  • 0 on success
  • Non-zero on error (buffer too small)

Usage example:

char my_name[256];
if (core->funcs->get_own_name(core, my_name, sizeof(my_name)) == 0) {
    printf("My name: %s\n", my_name);
}

Thread safety: Safe to call from any thread

Notes:

  • Same value as core->own_name, but copies to a buffer
  • Useful for logging or diagnostics

get_plugin_state

auto (*get_plugin_state)(CoreAPI* core, const char* name) -> PluginState*;

Get the opaque state pointer for a plugin.

Parameters:

  • core: Core API handle
  • name: Plugin name to query

Return value:

  • PluginState* pointer on success
  • nullptr on error (plugin not found)

Usage example:

PluginState* desktop_state = core->funcs->get_plugin_state(core, "desktop");
if (desktop_state) {
    // State pointer is valid - but don't cast or modify it!
    // Only pass it back to other Core API functions
}

Thread safety: Safe to call from any thread

CRITICAL CONSTRAINTS:

  • DO NOT cast this pointer to any other type
  • DO NOT modify the data pointed to
  • DO NOT dereference this pointer
  • Only valid to pass back to other Core API functions
  • Pointer becomes invalid if the target plugin is unloaded

Architectural note:

  • Returns pointer to plugin’s opaque state
  • The state is owned by the plugin itself
  • This is for inter-plugin communication only

Property Registration

register_property

using PropertySetterFn = auto (*)(const char* value, void* user_data) -> void;

auto (*register_property)(CoreAPI* core, const char* namespace_,
                         const char* key, const char* description,
                         PropertySetterFn setter, void* user_data) -> int;

Register a runtime-configurable property.

Parameters:

  • core: Core API handle
  • namespace: Property namespace (e.g., “yaml”, “config”)
  • key: Property key (e.g., “background”, “opacity”)
  • description: Human-readable description
  • setter: Callback function invoked when property is set
  • user_data: User data passed to the setter callback

Return value:

  • 0 on success
  • Non-zero on error

Usage example:

void background_setter(const char* value, void* user_data) {
    auto* ctx = static_cast<MyContext*>(user_data);
    // Parse and apply value
    ctx->background_path = std::string(value);
}

core->funcs->register_property(
    core,
    "yaml",           // namespace
    "background",     // key
    "Path to background image",
    background_setter,
    &my_context
);

Thread safety: Safe to call from initialization

Property key format:

  • Full key becomes: plugin_name:namespace:key
  • Example: desktop:yaml:background

waxedctl integration:

  • Properties can be set via waxedctl set <plugin>:<namespace>:<key> <value>
  • The setter callback is invoked when the value is set

IPC Commands

enqueue_command

auto (*enqueue_command)(CoreAPI* core, const char* command) -> int;

Enqueue an IPC command to be processed by the worker thread.

Parameters:

  • core: Core API handle
  • command: Command string to enqueue

Return value:

  • 0 on success (command enqueued)
  • Non-zero on error

Usage example:

// From a transitioner plugin after completing transition
core->funcs->enqueue_command(core, "plugin show desktop");
core->funcs->enqueue_command(core, "plugin unload transitioner");

Thread safety: Safe to call from any thread, including render callbacks

Architectural notes:

  • Enqueues request to IPC queue for async processing
  • Safe to call from render callbacks (avoids deadlock)
  • Worker thread processes request outside render lock
  • No response sent back (internal request)

Use cases:

  • Transitioner calling “plugin show desktop” after transition completes
  • Avoiding deadlock from set_visible_plugin during render
  • Chaining commands that need to execute outside render context

Background Management Functions

set_background

auto (*set_background)(CoreAPI* core, const char* image_path, const char* mode) -> int;

Set background image from file path.

Parameters:

  • core: Core API handle
  • image_path: Path to image file (jpg, png, etc.)
  • mode: Display mode (“contain”, “cover”, “tile”, “stretch”)

Return value:

  • 0 on success (load enqueued)
  • Non-zero on error

Usage example:

core->funcs->set_background(core, "/path/to/image.png", "cover");

Thread safety: Safe to call from any thread

Architectural notes:

  • Enqueues async load request via BackgroundService
  • Returns immediately; actual loading happens in worker thread
  • Case-insensitive mode parsing (defaults to “contain”)
  • Validates file existence and format before enqueueing

Display modes:

  • contain: Scale image to fit entirely within display (preserve aspect ratio)
  • cover: Scale image to cover entire display (preserve aspect ratio, may crop)
  • tile: Display image at native resolution, tiling if smaller
  • stretch: Stretch image to fill display (may distort)

get_background_descriptor

struct BackgroundDescriptor {
    VkDescriptorSet descriptor_set{};
    VkImageLayout image_layout{};
    BackgroundMode mode{BackgroundMode::Contain};
    BackgroundType type{BackgroundType::None};
    uint32_t width{0};
    uint32_t height{0};
    uint64_t timeline_value{0};
};

auto (*get_background_descriptor)(CoreAPI* core, BackgroundDescriptor* out_descriptor) -> int;

Get current background descriptor for rendering.

Parameters:

  • core: Core API handle
  • out_descriptor: Pointer to BackgroundDescriptor to populate

Return value:

  • 0 on success (background set, descriptor populated)
  • Non-zero on error or no background

Usage example:

BackgroundDescriptor bg_desc;
if (core->funcs->get_background_descriptor(core, &bg_desc) == 0) {
    if (bg_desc.descriptor_set != VK_NULL_HANDLE) {
        // Render background using descriptor_set
        vkCmdBindDescriptorSets(cmd, ..., bg_desc.descriptor_set, ...);
    }
}

Thread safety: Safe to call from any thread (concurrent calls supported)

Architectural notes:

  • Thread-safe: Multiple plugins can call this concurrently
  • Returns null descriptor (VK_NULL_HANDLE) when no background is set
  • Descriptor contains all info needed for rendering

Descriptor contents:

  • descriptor_set: Vulkan descriptor set for sampling
  • image_layout: Current image layout
  • mode: Display mode (contain/cover/tile/stretch)
  • type: Content type (None/StaticImage/Video)
  • width/height: Image dimensions
  • timeline_value: Timeline semaphore value for synchronization

is_background_ready

auto (*is_background_ready)(CoreAPI* core) -> int;

Check if background is ready to render.

Parameters:

  • core: Core API handle

Return value:

  • 1 if background descriptor is available
  • 0 if not ready or no background

Usage example:

// Before first render, check if background is ready
if (core->funcs->is_background_ready(core) == 0) {
    // Background still loading, render black
    return;
}

Thread safety: Safe to call from any thread

Architectural notes:

  • Thread-safe: Multiple plugins can call this concurrently
  • Returns 1 if a background has been set AND the image has been uploaded
  • Returns 0 if no background is set OR image is still loading
  • Plugins should call this before first render to avoid black frames

set_background_video

auto (*set_background_video)(CoreAPI* core, const char* video_path, const char* mode) -> int;

Set background video (placeholder for future implementation).

Parameters:

  • core: Core API handle
  • video_path: Path to video file
  • mode: Display mode (“contain”, “cover”, “tile”, “stretch”)

Return value:

  • Currently always returns -1 (not implemented)

Architectural notes:

  • Currently returns NotImplemented
  • Future: Will integrate with video player plugin
  • Will use ring buffer for frame streaming

set_background_dma_buf

struct BackgroundDmaBuf {
    core::utils::UniqueFd dma_buf_fd;  // Ownership transferred to caller
    uint32_t width;              // Image width in pixels
    uint32_t height;             // Image height in pixels
    uint32_t stride;             // Row stride in bytes
    uint32_t format;             // DRM fourcc format code
    uint64_t modifier;           // DRM format modifier
    VkDeviceSize allocation_size; // Total allocation size
    uint32_t memory_type_index;  // Vulkan memory type index

    // Pre-calculated push constants
    float scale_x;               // UV/Position scale X
    float scale_y;               // UV/Position scale Y
    float offset_x;              // UV/Position offset X
    float offset_y;              // UV/Position offset Y
    int32_t mode;                // 0=contain, 1=cover, 2=tile
};

using BackgroundDmaBufCallbackFn = auto (*)(const BackgroundDmaBuf* dma_buf, void* user_data) -> void;

auto (*set_background_dma_buf)(CoreAPI* core, const char* image_path, const char* mode,
                               uint32_t target_width, uint32_t target_height,
                               BackgroundDmaBufCallbackFn callback, void* user_data) -> int;

Set background image and export as DMA-BUF (async, callback-based).

Parameters:

  • core: Core API handle
  • image_path: Path to image file
  • mode: Display mode (“contain”, “cover”, “tile”, “stretch”)
  • target_width: Target display width for push constant calculation
  • target_height: Target display height for push constant calculation
  • callback: Function to call when DMA-BUF is ready (called from worker thread)
  • user_data: User data pointer passed to callback

Return value:

  • 0 on success (load enqueued)
  • Non-zero on error

Usage example:

void on_dma_buf_ready(const BackgroundDmaBuf* dma_buf, void* user_data) {
    auto* ctx = static_cast<MyContext*>(user_data);

    // Import DMA-BUF into Vulkan
    VkImportSemaphoreFdInfoKHR import_info = {
        .sType = VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR,
        .fd = dma_buf->dma_buf_fd.get(),
        // ...
    };

    // Use pre-calculated push constants
    vkCmdPushConstants(cmd, ..., dma_buf->scale_x, dma_buf->scale_y,
                       dma_buf->offset_x, dma_buf->offset_y, dma_buf->mode);
}

core->funcs->set_background_dma_buf(
    core,
    "/path/to/image.png",
    "cover",
    1920, 1080,
    on_dma_buf_ready,
    &my_context
);

Thread safety: Safe to call from any thread

Architectural notes:

  • Enqueues async background load - NON-BLOCKING, returns immediately
  • When texture is loaded and DMA-BUF exported, callback is invoked
  • Plugin should render black until callback is invoked
  • All operations are non-blocking and async
  • Callback is called from BackgroundService worker thread

Ownership model:

  • dma_buf_fd ownership is transferred to caller in callback
  • RAII-managed cleanup via UniqueFd
  • Callback must consume or take ownership of the FD

Push constants:

  • Pre-calculated by BackgroundService based on target resolution
  • Plugins should use these directly without additional calculation

Vulkan Device Access

register_vulkan_provider

auto (*register_vulkan_provider)(CoreAPI* core,
                                 vk::raii::Instance* instance,
                                 vk::raii::Device* device,
                                 vk::raii::PhysicalDevice* physical_device,
                                 vk::raii::Queue* graphics_queue,
                                 uint32_t graphics_queue_family,
                                 vk::raii::DescriptorPool* descriptor_pool,
                                 vk::raii::DescriptorSetLayout* descriptor_layout) -> int;

Register Vulkan context provider.

Parameters:

  • core: Core API handle
  • instance: Vulkan instance (RAII)
  • device: Vulkan device (RAII)
  • physical_device: Vulkan physical device (RAII)
  • graphics_queue: Vulkan graphics queue (RAII)
  • graphics_queue_family: Queue family index
  • descriptor_pool: Descriptor pool for allocating descriptor sets (RAII)
  • descriptor_layout: Descriptor set layout (RAII)

Return value:

  • 0 on success
  • Non-zero on error (already registered, invalid handles)

Usage example:

auto result = core->funcs->register_vulkan_provider(
    core,
    &my_instance,
    &my_device,
    &my_physical_device,
    &my_graphics_queue,
    my_queue_family,
    &my_descriptor_pool,
    &my_descriptor_layout
);

Thread safety: Call during initialization only

Architectural notes:

  • First plugin to register becomes the Vulkan provider
  • BackgroundService is initialized with provider’s handles
  • Subsequent registration attempts return error
  • Provider plugin cannot be unloaded (would invalidate handles)

MANDATORY: All Vulkan handles MUST be RAII types from vulkan_raii.hpp


get_vulkan_device

struct VulkanDeviceInfo {
    vk::raii::Instance* instance;
    vk::raii::Device* device;
    vk::raii::PhysicalDevice* physical_device;
    vk::raii::Queue* graphics_queue;
    uint32_t graphics_queue_family;
    VkPhysicalDeviceProperties device_properties;
    VkPhysicalDeviceMemoryProperties memory_properties;
    bool supports_dma_buf;
    bool supports_timeline_semaphore;
};

auto (*get_vulkan_device)(CoreAPI* core, VulkanDeviceInfo* out_info) -> int;

Get Vulkan device information.

Parameters:

  • core: Core API handle
  • out_info: Pointer to VulkanDeviceInfo to populate

Return value:

  • 0 on success
  • -1 on invalid parameters
  • -2 if device not initialized

Usage example:

VulkanDeviceInfo vk_info;
if (core->funcs->get_vulkan_device(core, &vk_info) == 0) {
    // Access device info
    uint32_t queue_family = vk_info.graphics_queue_family;
    bool dma_buf = vk_info.supports_dma_buf;

    // Use RAII pointers
    vk::raii::Device& device = *vk_info.device;
}

Thread safety: Thread-safe (reads from shared device)

Architectural notes:

  • Returns the registered Vulkan provider’s device information
  • Thread-safe: reads from shared device structure
  • Plugins can query device capabilities and extensions
  • Returns error if no Vulkan provider has been registered

Pointer lifetime:

  • Returned pointers point to shared Vulkan objects
  • Objects outlive the plugin (owned by core)
  • Plugin must not destroy these objects

Shader Service

get_shader_service

auto (*get_shader_service)(CoreAPI* core) -> ShaderService*;

Get ShaderService for runtime shader compilation.

Parameters:

  • core: Core API handle

Return value:

  • ShaderService* pointer on success
  • nullptr on error

Usage example:

ShaderService* shader_svc = core->funcs->get_shader_service(core);
if (shader_svc) {
    // Register shaders for hot-reload
    shader_svc->watch_shader("my_shader.vert", "main", vert_spirv);
}

Thread safety: Thread-safe (ShaderService handles its own synchronization)

Architectural notes:

  • Returns pointer to core’s ShaderService instance
  • Thread-safe: ShaderService handles its own synchronization
  • Plugins use this to register shaders for hot-reload

Display Configuration

get_display_config

struct DisplayConfig {
    uint32_t display_id;        // Unique identifier (0, 1, 2, ...)
    DisplayRole role;           // Primary or Secondary
    DisplayLayoutMode layout_mode; // Mirror or Extend
    uint32_t width;             // Display width in pixels
    uint32_t height;            // Display height in pixels
    uint32_t refresh_rate;      // Refresh rate in Hz
    int32_t position_x;         // X position in extended mode (0 for mirror)
    int32_t position_y;         // Y position in extended mode (0 for mirror)
    bool vrr_capable;           // Display/mode supports Variable Refresh Rate
    bool vrr_enabled;           // VRR is currently enabled on this display
};

enum class DisplayRole : uint32_t {
    Primary = 0,   // Main display (display_id = 0)
    Secondary = 1, // Other displays (display_id > 0)
};

enum class DisplayLayoutMode : uint32_t {
    Mirror = 0,   // All displays show identical content
    Extend = 1,   // Each display can show different content
};

auto (*get_display_config)(const CoreAPI* core, DisplayConfig* out_config) -> int;

Get the current display configuration.

Parameters:

  • core: Core API handle
  • out_config: Pointer to DisplayConfig to populate

Return value:

  • 0 on success
  • -EINVAL on error or called outside render context

Usage example:

void my_render_function(CoreAPI* core, RenderTarget* target, void* user_data) {
    DisplayConfig config;
    if (core->funcs->get_display_config(core, &config) == 0) {
        printf("Rendering for display %u: %ux%u\n",
               config.display_id, config.width, config.height);

        if (config.layout_mode == DisplayLayoutMode::Extend) {
            printf("Position: %d, %d\n", config.position_x, config.position_y);
        }
    }

    // Render...
}

Thread safety: Only valid during render callback

Architectural notes:

  • Core populates thread-local storage before each render call
  • display_id identifies which display is being rendered
  • layout_mode determines if rendering is mirrored or extended
  • In mirror mode, all displays render the same content
  • In extend mode, each display may render different content

get_display_count

auto (*get_display_count)(const CoreAPI* core, uint32_t* out_count) -> int;

Get number of active displays.

Parameters:

  • core: Core API handle
  • out_count: Pointer to receive number of displays

Return value:

  • 0 on success
  • Non-zero on error

Usage example:

uint32_t display_count = 0;
if (core->funcs->get_display_count(core, &display_count) == 0) {
    printf("Active displays: %u\n", display_count);
}

Thread safety: Safe to call from any thread

Notes:

  • Currently returns 1 (single display)
  • Phase 7: Will return actual count from DisplayManager

register_display_callback

using plugin_display_changed_fn = auto (*)(
    PluginState* state,
    const DisplayConfig* config
) noexcept -> void;

auto (*register_display_callback)(CoreAPI* core, plugin_display_changed_fn callback) -> int;

Register display change notification callback.

Parameters:

  • core: Core API handle
  • callback: Function to call when display config changes

Return value:

  • 0 on success
  • -EINVAL on error

Usage example:

void on_display_changed(PluginState* state, const DisplayConfig* config) {
    // Handle display configuration change
    printf("Display %u changed: %ux%u\n", config->display_id, config->width, config->height);
}

core->funcs->register_display_callback(core, on_display_changed);

Thread safety: Safe to call from initialization

Architectural notes:

  • Callback is invoked when display configuration changes
  • Called from core thread
  • Plugin should update its internal state accordingly

unregister_display_callback

auto (*unregister_display_callback)(CoreAPI* core) -> void;

Unregister display change callback.

Parameters:

  • core: Core API handle

Usage example:

core->funcs->unregister_display_callback(core);

Thread safety: Safe to call from any thread


API Registration

register_api_call

enum class APIArgType : uint8_t {
    Int,     // Integer (validated with stoi)
    Float,   // Float (validated with stof)
    String,  // String (no validation)
    Bool,    // true/false
    FourCC,  // DRM FourCC format code (validated as hex)
};

struct APIArgDef {
    const char* name;  // "x", "width", "format", etc.
    APIArgType type;
    bool required;     // Must be provided
};

using PluginAPICallback = void (*)(const char* api_name,
                                   int argc,
                                   const char** argv);

auto (*register_api_call)(CoreAPI* core,
                          const char* api_name,
                          const void* args,  // APIArgDef* array
                          size_t arg_count,
                          const char* description,
                          void* callback     // PluginAPICallback
                          ) -> int;

Register an API call for the plugin.

Parameters:

  • core: Core API handle
  • api_name: Name of the API (e.g., “surface”)
  • args: Array of argument definitions (can be null if arg_count is 0)
  • arg_count: Number of arguments in the array
  • description: Human-readable description of the API
  • callback: Function to call when API is invoked

Return value:

  • 0 on success
  • Non-zero on error

Usage example:

// Define arguments
static const APIArgDef surface_args[] = {
    { "x", APIArgType::Int, true },
    { "y", APIArgType::Int, true },
    { "width", APIArgType::Int, false },
    { "height", APIArgType::Int, false },
};

// Callback function
void surface_api_handler(const char* api_name, int argc, const char** argv) {
    // Parse arguments and handle the command
}

// Register
core->funcs->register_api_call(
    core,
    "surface",
    surface_args,
    sizeof(surface_args) / sizeof(surface_args[0]),
    "Create or manipulate a surface",
    (void*)surface_api_handler
);

Thread safety: Safe to call from initialization

waxedctl integration:

  • Registered APIs can be invoked via waxedctl <plugin> <api_name> <args>
  • Arguments are validated against the registered definition

unregister_api_call

auto (*unregister_api_call)(CoreAPI* core, const char* api_name) -> int;

Unregister a previously registered API call.

Parameters:

  • core: Core API handle
  • api_name: Name of the API to unregister

Return value:

  • 0 on success
  • Non-zero on error (API not found)

Usage example:

core->funcs->unregister_api_call(core, "surface");

Thread safety: Safe to call from any thread


Cursor Management

register_cursor_provider

struct CursorBuffer {
    const uint8_t* pixels;    // RGBA pixel data (premultiplied alpha)
    uint32_t width;           // Cursor width in pixels
    uint32_t height;          // Cursor height in pixels
    int32_t hotspot_x;        // Hotspot X offset from top-left
    int32_t hotspot_y;        // Hotspot Y offset from top-left
    uint32_t stride;          // Row stride in bytes (usually width * 4)
};

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

auto (*register_cursor_provider)(CoreAPI* core,
                                 CursorProviderCallback callback,
                                 void* user_data) -> int;

Register cursor provider callback.

Parameters:

  • core: Core API handle
  • callback: Function that returns current cursor buffer
  • user_data: Plugin’s user data passed to callback

Return value:

  • 0 on success
  • Non-zero on error

Usage example:

const CursorBuffer* get_cursor(void* user_data) {
    auto* ctx = static_cast<MyContext*>(user_data);
    return &ctx->current_cursor;  // Return pointer to cached cursor
}

core->funcs->register_cursor_provider(core, get_cursor, &my_context);

Thread safety: Safe to call from initialization

Architectural notes:

  • Core calls the callback when it needs to update the hardware cursor
  • Plugin returns pointer to its cached CursorBuffer
  • Cursor buffer is valid until next call or cleanup
  • Only one cursor provider at a time (last registration wins)

Cursor buffer requirements:

  • pixels: RGBA data with premultiplied alpha
  • width/height: Cursor size (typically 64x64 or smaller)
  • hotspot_x/y: Point within cursor that represents click position
  • stride: Bytes per row (usually width * 4)

notify_cursor_shape_changed

auto (*notify_cursor_shape_changed)(CoreAPI* core) -> int;

Notify core that cursor shape has changed.

Parameters:

  • core: Core API handle

Return value:

  • 0 on success
  • Non-zero on error

Usage example:

// When cursor shape changes (e.g., hover over link)
update_my_cursor_buffer("link");
core->funcs->notify_cursor_shape_changed(core);

Thread safety: Safe to call from any thread

Architectural notes:

  • Plugin calls this when cursor shape changes
  • Core will reload cursor buffer from provider on next render
  • Triggers immediate cursor buffer refresh

request_cursor_takeover

auto (*request_cursor_takeover)(CoreAPI* core, uint32_t display_id) -> int;

Request cursor takeover for a display.

Parameters:

  • core: Core API handle
  • display_id: Display to take over (from RenderTarget.display_id)

Return value:

  • 0 on success
  • Non-zero on error

Usage example:

void my_render_function(CoreAPI* core, RenderTarget* target, void* user_data) {
    // Take over cursor rendering for this display
    core->funcs->request_cursor_takeover(core, target->display_id);

    // Now render cursor in compositor...
}

Thread safety: Safe to call from any thread

Architectural notes:

  • Plugin calls this to take over cursor rendering for a specific display
  • Core hides hardware cursor; plugin draws cursor in compositor
  • Multiple displays can have independent takeover states

release_cursor_takeover

auto (*release_cursor_takeover)(CoreAPI* core, uint32_t display_id) -> int;

Release cursor takeover for a display.

Parameters:

  • core: Core API handle
  • display_id: Display to release

Return value:

  • 0 on success
  • Non-zero on error

Usage example:

core->funcs->release_cursor_takeover(core, 0);

Thread safety: Safe to call from any thread

Architectural notes:

  • Plugin calls this to return cursor control to core
  • Core resumes drawing on hardware cursor plane

is_cursor_takeover_active

auto (*is_cursor_takeover_active)(const CoreAPI* core, uint32_t display_id) -> bool;

Check if cursor takeover is active for a display.

Parameters:

  • core: Core API handle
  • display_id: Display to check

Return value:

  • true if takeover active
  • false otherwise

Usage example:

if (core->funcs->is_cursor_takeover_active(core, target->display_id)) {
    // Render software cursor
} else {
    // Core handles cursor, don't render
}

Thread safety: Safe to call from any thread


Input State

get_input_state

struct MouseEvent {
    enum Type : uint8_t {
        ButtonPress,      // Mouse button pressed
        ButtonRelease,    // Mouse button released
        WheelScroll,      // Mouse wheel scrolled
        DoubleClick       // Double-click detected
    };
    Type type;
    uint8_t button;       // Button code (BTN_LEFT, BTN_RIGHT, etc.)
    int16_t delta;        // Scroll delta (for WheelScroll)
    int32_t x, y;         // Position at event time
    uint64_t timestamp;   // Event timestamp (monotonic nanoseconds)
};

struct KeyboardEvent {
    enum Type : uint8_t { KeyPress, KeyRelease };
    Type type;
    uint32_t key_code;     // Linux KEY_* code
    uint32_t modifiers;    // Modifier bitmask
    uint64_t timestamp;    // Event timestamp (monotonic nanoseconds)
};

constexpr uint32_t KEYBOARD_MOD_CTRL  = 1 << 0;
constexpr uint32_t KEYBOARD_MOD_ALT   = 1 << 1;
constexpr uint32_t KEYBOARD_MOD_SHIFT = 1 << 2;

struct InputState {
    std::atomic<int32_t>* cursor_x;        // Global cursor X position
    std::atomic<int32_t>* cursor_y;        // Global cursor Y position
    std::atomic<uint32_t>* buttons_held;   // Bitmask of currently held buttons
    void* mouse_event_queue;               // Lock-free mouse event queue (opaque)
    void* keyboard_event_queue;            // Lock-free keyboard event queue (opaque)
    std::atomic<uint32_t>* modifiers_held; // Bitmask of currently held modifiers
};

auto (*get_input_state)(CoreAPI* core, InputState* out_state) -> int;

Get input state for zero-copy access.

Parameters:

  • core: Core API handle
  • out_state: Output: InputState with atomic pointers

Return value:

  • 0 on success
  • Non-zero on error

Usage example:

InputState input;
if (core->funcs->get_input_state(core, &input) == 0) {
    // Read cursor position atomically
    int x = input.cursor_x->load(std::memory_order_relaxed);
    int y = input.cursor_y->load(std::memory_order_relaxed);

    // Check button state
    uint32_t buttons = input.buttons_held->load(std::memory_order_relaxed);
    bool left_pressed = buttons & (1 << 0);

    // Pop events
    MouseEvent event;
    while (core->funcs->mouse_event_pop(input.mouse_event_queue, &event)) {
        handle_mouse_event(event);
    }
}

Thread safety: Thread-safe (atomic access)

Architectural notes:

  • Returns pointers to atomics for lock-free position/button tracking
  • Plugins can read input state without syscalls
  • Event queues use lock-free SPSC (single producer, single consumer)

Button codes:

  • Uses Linux input button codes (BTN_*)
  • buttons_held is a bitmask of currently pressed buttons

Modifier flags:

  • KEYBOARD_MOD_CTRL: Ctrl key held
  • KEYBOARD_MOD_ALT: Alt key held
  • KEYBOARD_MOD_SHIFT: Shift key held

mouse_event_pop

auto (*mouse_event_pop)(void* mouse_event_queue, MouseEvent* out_event) -> bool;

Pop a mouse event from the queue.

Parameters:

  • mouse_event_queue: Mouse event queue from InputState.mouse_event_queue
  • out_event: Output: popped event

Return value:

  • true if event popped
  • false if queue empty

Usage example:

MouseEvent event;
while (core->funcs->mouse_event_pop(input.mouse_event_queue, &event)) {
    if (event.type == MouseEvent::ButtonPress) {
        printf("Button %u pressed at (%d, %d)\n", event.button, event.x, event.y);
    }
}

Thread safety: Non-blocking, thread-safe

Notes:

  • Non-blocking: returns false if queue is empty
  • Events are consumed (removed from queue)

keyboard_event_pop

auto (*keyboard_event_pop)(void* keyboard_event_queue, KeyboardEvent* out_event) -> bool;

Pop a keyboard event from the queue.

Parameters:

  • keyboard_event_queue: Keyboard event queue from InputState.keyboard_event_queue
  • out_event: Output: popped event

Return value:

  • true if event popped
  • false if queue empty

Usage example:

KeyboardEvent event;
while (core->funcs->keyboard_event_pop(input.keyboard_event_queue, &event)) {
    if (event.type == KeyboardEvent::KeyPress) {
        printf("Key %u pressed, modifiers: 0x%x\n", event.key_code, event.modifiers);
    }
}

Thread safety: Non-blocking, thread-safe

Notes:

  • Non-blocking: returns false if queue is empty
  • Events are consumed (removed from queue)
  • Key codes are Linux KEY_* values

Thread Safety Summary

FunctionThread SafetyNotes
load_pluginSafeAny thread
unload_pluginSafeAny thread
request_self_unloadSafeAny thread
is_plugin_loadedSafeAny thread
set_visible_pluginSafeAny thread, but prefer enqueue_command during render
get_visible_pluginSafeAny thread
get_own_nameSafeAny thread
register_propertySafeAny thread (typically init)
get_plugin_stateSafeAny thread
enqueue_commandSafeAny thread, including render callbacks
set_backgroundSafeAny thread
get_background_descriptorSafeAny thread (concurrent)
is_background_readySafeAny thread (concurrent)
set_background_videoSafeAny thread (not implemented)
register_vulkan_providerInit onlyCall during initialization only
get_vulkan_deviceSafeAny thread (read-only)
set_background_dma_bufSafeAny thread
get_shader_serviceSafeAny thread (concurrent)
get_display_configRender onlyOnly valid during render callback
get_display_countSafeAny thread
register_display_callbackSafeAny thread (typically init)
unregister_display_callbackSafeAny thread
register_api_callSafeAny thread (typically init)
unregister_api_callSafeAny thread
register_cursor_providerSafeAny thread (typically init)
notify_cursor_shape_changedSafeAny thread
request_cursor_takeoverSafeAny thread
release_cursor_takeoverSafeAny thread
is_cursor_takeover_activeSafeAny thread (read-only)
get_input_stateSafeAny thread
mouse_event_popSafeAny thread (concurrent per queue)
keyboard_event_popSafeAny thread (concurrent per queue)

Return Value Conventions

Integer return values (most functions):

  • 0: Success
  • Negative values: Error
  • Positive values: Status (e.g., is_plugin_loaded returns 1 for true)

Pointer return values:

  • Non-null: Success, valid pointer/value
  • nullptr: Error or not found

Boolean return values:

  • true: Affirmative
  • false: Negative or empty

Common Patterns

Pattern 1: Safe Plugin Loading

auto load_plugin_safe(CoreAPI* core, const char* name) -> bool {
    if (core->funcs->is_plugin_loaded(core, name)) {
        return true;  // Already loaded
    }
    return core->funcs->load_plugin(core, name) == 0;
}

Pattern 2: Background Rendering

void render_background(CoreAPI* core, VkCommandBuffer cmd) {
    BackgroundDescriptor bg_desc;
    if (core->funcs->get_background_descriptor(core, &bg_desc) == 0) {
        if (bg_desc.descriptor_set != VK_NULL_HANDLE) {
            vkCmdBindDescriptorSets(cmd, ..., bg_desc.descriptor_set, ...);
            // Draw fullscreen quad
        }
    }
}

Pattern 3: Cursor Provider

class CursorManager {
public:
    const CursorBuffer* get_cursor(void* user_data) {
        auto* self = static_cast<CursorManager*>(user_data);
        return &self->current_cursor_;
    }

    void set_shape(const std::string& shape_name) {
        current_cursor_ = load_cursor(shape_name);
        // Notify core to reload
        core_->funcs->notify_cursor_shape_changed(core_);
    }

private:
    CoreAPI* core_;
    CursorBuffer current_cursor_;
};

Pattern 4: Input Handling

void process_input(CoreAPI* core) {
    InputState input;
    if (core->funcs->get_input_state(core, &input) != 0) {
        return;
    }

    // Read current cursor position
    int x = input.cursor_x->load(std::memory_order_relaxed);
    int y = input.cursor_y->load(std::memory_order_relaxed);

    // Process pending events
    MouseEvent mouse_event;
    while (core->funcs->mouse_event_pop(input.mouse_event_queue, &mouse_event)) {
        // Handle event
    }
}