Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/streaming-host-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ intended shape:
the virtual controller before the first client input packet.
- Forward output callbacks back to the physical client controller or feedback
queue.
- Route `RuntimeOptions::log_callback` into the host logger so backend lifecycle,
failures, and debug-level input coordinates appear in the host log.
- When streaming one monitor from a multi-monitor desktop, provide both the full
virtual-desktop bounds and the selected monitor viewport in
`CreateMouseOptions` so absolute mouse input reaches the captured output.

This keeps one public code path for Linux, Windows, and future platforms while
still letting each backend report real capability limits.
Expand Down
42 changes: 42 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,48 @@ The API centers on portable device concepts:
- `BackendCapabilities`: reports runtime/backend limits such as virtual HID,
output report, keyboard, mouse, XTest fallback, and installed-driver support.

## Diagnostics

Hosts can route libvirtualhid diagnostics into their own logging system by
installing `RuntimeOptions::log_callback` before creating the runtime:

```cpp
lvh::RuntimeOptions runtime_options;
runtime_options.backend = lvh::BackendKind::platform_default;
runtime_options.log_callback = [](lvh::LogLevel level, const std::string &message) {
host_log(level, message);
};
auto runtime = lvh::Runtime::create(runtime_options);
```

The callback receives runtime and device lifecycle messages, operation failures,
and debug-level mouse coordinate diagnostics. It runs synchronously on the
calling thread. If a consumer callback throws, libvirtualhid disables it for
subsequent messages so it cannot interrupt input delivery.

## Absolute Mouse Viewports

Absolute mouse coordinates can target one monitor inside a larger virtual
desktop. Supply both the desktop bounds and the selected viewport in native
desktop pixels when creating the mouse:

```cpp
lvh::CreateMouseOptions mouse_options;
mouse_options.profile = lvh::profiles::mouse();
mouse_options.desktop = {.offset_x = -1920, .offset_y = 0, .width = 3840, .height = 1080};
mouse_options.viewport = {.offset_x = 0, .offset_y = 0, .width = 1920, .height = 1080};
auto created = runtime->create_mouse(mouse_options);
```

`Mouse::move_absolute()` coordinates are scaled from their supplied source
dimensions into the target viewport, then normalized against the virtual
desktop where the platform input API requires it. This contract covers
CoreGraphics on macOS, `SendInput` on Windows, and the XTest or `uinput` path on
Linux and FreeBSD, including virtual desktops whose origin is negative. Leave
both viewport dimensions at zero to retain the platform-default pointer area
(the main display on macOS and the virtual desktop on other current backends).
A configured target viewport must be fully contained by its desktop.

## Gamepad Example

```cpp
Expand Down
169 changes: 162 additions & 7 deletions src/core/runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@

// standard includes
#include <algorithm>
#include <atomic>
#include <cmath>
#include <format>
#include <memory>
#include <mutex>
#include <sstream>
#include <utility>

// local includes
Expand All @@ -19,6 +22,51 @@

namespace lvh::detail {

/**
* @brief Exception-safe dispatcher for a consumer diagnostic callback.
*/
class Logger {
public:
/**
* @brief Construct a dispatcher for an optional callback.
*
* @param callback Consumer diagnostic callback.
*/
explicit Logger(LogCallback callback):
callback_ {std::move(callback)} {}

/**
* @brief Report whether a callback is installed.
*
* @return `true` when messages have a destination.
*/
bool enabled() const noexcept {
return static_cast<bool>(callback_) && !callback_failed_.load(std::memory_order_relaxed);
}

/**
* @brief Emit one diagnostic message without allowing consumer exceptions to escape.
*
* @param level Message severity.
* @param message Diagnostic text.
*/
void emit(LogLevel level, const std::string &message) const noexcept {
if (!enabled()) {
return;
}

try {
callback_(level, message);
} catch (...) {
callback_failed_.store(true, std::memory_order_relaxed);
}
}

private:
LogCallback callback_;
mutable std::atomic_bool callback_failed_ = false; ///< Whether the consumer callback threw an exception.
};

class SynchronizedState {
public:
template<class Func>
Expand Down Expand Up @@ -71,14 +119,21 @@ namespace lvh::detail {
};

struct MouseDevice: SynchronizedState {
explicit MouseDevice(DeviceId device_id, CreateMouseOptions create_options, std::unique_ptr<BackendMouse> backend_mouse):
explicit MouseDevice(
DeviceId device_id,
CreateMouseOptions create_options,
std::unique_ptr<BackendMouse> backend_mouse,
std::shared_ptr<Logger> device_logger
):
id {device_id},
options {std::move(create_options)},
backend {std::move(backend_mouse)} {}
backend {std::move(backend_mouse)},
logger {std::move(device_logger)} {}

DeviceId id;
CreateMouseOptions options;
std::unique_ptr<BackendMouse> backend;
std::shared_ptr<Logger> logger;
bool open = true;
MouseEvent last_event;
std::size_t submitted_events = 0;
Expand Down Expand Up @@ -141,11 +196,15 @@ namespace lvh::detail {
class RuntimeState: public SynchronizedState {
public:
explicit RuntimeState(RuntimeOptions runtime_options):
options {runtime_options},
backend {create_backend(runtime_options.backend)},
caps {backend->capabilities()} {}
options {std::move(runtime_options)},
logger {std::make_shared<Logger>(options.log_callback)},
backend {create_backend(options.backend)},
caps {backend->capabilities()} {
logger->emit(LogLevel::info, "initialized " + caps.backend_name + " backend");
}

RuntimeOptions options;
std::shared_ptr<Logger> logger;
std::unique_ptr<Backend> backend;
BackendCapabilities caps;
DeviceId next_device_id = 1;
Expand Down Expand Up @@ -199,10 +258,91 @@ namespace lvh {
if (options.profile.name.empty()) {
return OperationStatus::failure(ErrorCode::invalid_argument, "device profile name must not be empty");
}
if (const auto valid_dimensions = [](const PointerViewport &viewport) {
return viewport.width >= 0 && viewport.height >= 0 &&
((viewport.width == 0 && viewport.height == 0) || (viewport.width > 0 && viewport.height > 0));
};
!valid_dimensions(options.desktop) || !valid_dimensions(options.viewport)) {
return OperationStatus::failure(
ErrorCode::invalid_argument,
"mouse viewport dimensions must both be positive or both be zero"
);
}
const auto has_desktop = options.desktop.width > 0;
if (const auto has_viewport = options.viewport.width > 0; has_desktop != has_viewport) {
return OperationStatus::failure(
ErrorCode::invalid_argument,
"mouse desktop and target viewport must be configured together"
);
}
if (has_desktop) {
const auto desktop_right = static_cast<std::int64_t>(options.desktop.offset_x) + options.desktop.width;
const auto desktop_bottom = static_cast<std::int64_t>(options.desktop.offset_y) + options.desktop.height;
const auto viewport_right = static_cast<std::int64_t>(options.viewport.offset_x) + options.viewport.width;
const auto viewport_bottom = static_cast<std::int64_t>(options.viewport.offset_y) + options.viewport.height;
if (options.viewport.offset_x < options.desktop.offset_x || options.viewport.offset_y < options.desktop.offset_y || viewport_right > desktop_right || viewport_bottom > desktop_bottom) {
return OperationStatus::failure(
ErrorCode::invalid_argument,
"mouse target viewport must be contained by the virtual desktop"
);
}
}

return OperationStatus::success();
}

/**
* @brief Format a mouse event for the consumer diagnostic callback.
*
* @param id Runtime mouse identifier.
* @param event Mouse event being submitted.
* @param desktop Configured virtual desktop bounds.
* @param viewport Configured target viewport.
* @return Human-readable diagnostic message.
*/
std::string mouse_event_description(
DeviceId id,
const MouseEvent &event,
const PointerViewport &desktop,
const PointerViewport &viewport
) {
std::ostringstream message;
message << "mouse " << id << ' ';
switch (event.kind) {
using enum MouseEventKind;

case relative_motion:
message << "relative motion x=" << event.x << " y=" << event.y;
break;
case absolute_motion:
message << "absolute motion x=";
if (event.has_fractional_absolute_coordinates) {
message << event.absolute_x << " y=" << event.absolute_y;
} else {
message << event.x << " y=" << event.y;
}
message << " source=" << event.width << 'x' << event.height;
if (viewport.width > 0 && viewport.height > 0) {
message << " viewport=" << viewport.offset_x << ',' << viewport.offset_y << ' '
<< viewport.width << 'x' << viewport.height << " desktop=" << desktop.offset_x << ','
<< desktop.offset_y << ' ' << desktop.width << 'x' << desktop.height;
} else {
message << " viewport=platform-default";
}
break;
case button:
message << "button " << static_cast<int>(std::to_underlying(event.button)) << (event.pressed ? " pressed" : " released");
break;
case vertical_scroll:
message << "vertical scroll distance=" << event.high_resolution_scroll;
break;
case horizontal_scroll:
message << "horizontal scroll distance=" << event.high_resolution_scroll;
break;
}
return message.str();
}

OperationStatus validate_touchscreen_options(const CreateTouchscreenOptions &options) {
if (options.profile.device_type != DeviceType::touchscreen) {
return OperationStatus::failure(ErrorCode::unsupported_profile, "device profile is not a touchscreen");
Expand Down Expand Up @@ -612,10 +752,18 @@ namespace lvh {

OperationStatus Mouse::submit(const MouseEvent &event) {
if (const auto validation = validate_mouse_event(event); !validation.ok()) {
device_->logger->emit(LogLevel::warning, "rejected mouse event: " + validation.message());
return validation;
}

return with_device(device_, [&event](auto &device) {
if (device_->logger->enabled()) {
device_->logger->emit(
LogLevel::debug,
mouse_event_description(device_->id, event, device_->options.desktop, device_->options.viewport)
);
}

const auto status = with_device(device_, [&event](auto &device) {
if (!device.open) {
return OperationStatus::failure(ErrorCode::device_closed, "mouse is closed");
}
Expand All @@ -630,6 +778,10 @@ namespace lvh {
++device.submitted_events;
return OperationStatus::success();
});
if (!status.ok()) {
device_->logger->emit(LogLevel::error, "mouse input failed: " + status.message());
}
return status;
}

OperationStatus Mouse::move_relative(std::int32_t delta_x, std::int32_t delta_y) {
Expand Down Expand Up @@ -1129,6 +1281,7 @@ namespace lvh {

MouseCreationResult Runtime::create_mouse(const CreateMouseOptions &options) {
if (const auto validation = validate_mouse_options(options); !validation.ok()) {
state_->logger->emit(LogLevel::warning, "rejected mouse creation: " + validation.message());
return {validation, nullptr};
}

Expand All @@ -1138,15 +1291,17 @@ namespace lvh {

auto backend_result = state_->backend->create_mouse(id, options);
if (!backend_result) {
state_->logger->emit(LogLevel::error, "mouse creation failed: " + backend_result.status.message());
return {std::move(backend_result.status), nullptr};
}

auto device = std::make_shared<detail::MouseDevice>(id, options, std::move(backend_result.mouse));
auto device = std::make_shared<detail::MouseDevice>(id, options, std::move(backend_result.mouse), state_->logger);
state_->with_lock([this, &device]() {
state_->mice.emplace_back(device);
});

auto mouse = std::make_unique<Mouse>(detail::RuntimeConstructionToken {}, std::move(device));
state_->logger->emit(LogLevel::info, std::format("created mouse {}", id));
return {OperationStatus::success(), std::move(mouse)};
}

Expand Down
Loading
Loading