Files
libvirtualhid/tests/fixtures/windows_backend_test_hooks.cpp
T

1251 lines
50 KiB
C++

/**
* @file tests/fixtures/windows_backend_test_hooks.cpp
* @brief Windows backend test hook definitions.
*/
// local includes
#include "fixtures/windows_backend_test_hooks.hpp"
// lib includes
#include <lizardbyte/common/env.h>
#define create_platform_backend create_platform_backend_for_windows_backend_test_hooks
#include "../../src/platform/windows/windows_backend.cpp"
#undef create_platform_backend
// standard includes
#include <bit>
namespace lvh::detail {
namespace {
class FakeWindowsControlChannelState {
public:
FakeWindowsControlChannelState() = default;
FakeWindowsControlChannelState(std::string path, std::string response_device_path):
path_ {std::move(path)},
response_device_path_ {std::move(response_device_path)} {}
const std::string &path() const {
return path_;
}
void set_create_transport_status(OperationStatus status) {
std::lock_guard lock {mutex_};
create_transport_status_ = std::move(status);
}
void set_create_protocol_status(std::uint32_t status) {
std::lock_guard lock {mutex_};
create_protocol_status_ = status;
}
OperationStatus create_device(
const LvhWindowsCreateDeviceRequest &request,
LvhWindowsCreateDeviceResponse &response
) {
std::lock_guard lock {mutex_};
create_requests_.push_back(request);
if (!create_transport_status_.ok()) {
return create_transport_status_;
}
response.version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION;
response.size = sizeof(response);
response.status = create_protocol_status_;
response.driver_device_id = next_driver_id_++;
response.session_token = session_token_;
windows::copy_string(response.device_path, response_device_path_);
if (response.status == LVH_WINDOWS_STATUS_SUCCESS && request.device_type == LVH_WINDOWS_DEVICE_GAMEPAD && request.gamepad_kind == LVH_WINDOWS_GAMEPAD_XBOX_360) {
response.transport_handle = reinterpret_cast<std::uintptr_t>(
::CreateEventW(nullptr, TRUE, FALSE, nullptr)
);
}
return protocol_status(response.status, "Windows driver rejected virtual HID device creation");
}
OperationStatus destroy_device(
std::uint64_t driver_device_id,
const LvhWindowsSessionToken &session_token
) {
std::lock_guard lock {mutex_};
if (!session_token_matches(session_token)) {
return OperationStatus::failure(ErrorCode::backend_failure, "unexpected destroy session token");
}
destroyed_ids_.push_back(driver_device_id);
return destroy_status_;
}
OperationStatus submit_input_report(
const LvhWindowsSessionToken &session_token,
const std::vector<std::uint8_t> &report
) {
using enum ErrorCode;
if (report.size() > LVH_WINDOWS_MAX_INPUT_REPORT_SIZE) {
return OperationStatus::failure(invalid_argument, "input report exceeds Windows control protocol limit");
}
std::lock_guard lock {mutex_};
if (!session_token_matches(session_token)) {
return OperationStatus::failure(backend_failure, "unexpected submit session token");
}
submit_reports_.push_back(report);
return submit_status_;
}
std::optional<LvhWindowsOutputReportEvent> pop_output_event() {
std::lock_guard lock {mutex_};
if (output_events_.empty()) {
return std::nullopt;
}
auto event = output_events_.front();
output_events_.erase(output_events_.begin());
return event;
}
bool output_events_empty() const {
std::lock_guard lock {mutex_};
return output_events_.empty();
}
std::uint64_t last_created_driver_id() const {
std::lock_guard lock {mutex_};
return next_driver_id_ - 1U;
}
void enqueue_output_event(const LvhWindowsOutputReportEvent &event) {
std::lock_guard lock {mutex_};
output_events_.push_back(event);
}
std::size_t create_request_count() const {
std::lock_guard lock {mutex_};
return create_requests_.size();
}
std::size_t submit_report_count() const {
std::lock_guard lock {mutex_};
return submit_reports_.size();
}
std::size_t destroy_request_count() const {
std::lock_guard lock {mutex_};
return destroyed_ids_.size();
}
std::vector<LvhWindowsCreateDeviceRequest> create_requests() const {
std::lock_guard lock {mutex_};
return create_requests_;
}
std::vector<std::vector<std::uint8_t>> submit_reports() const {
std::lock_guard lock {mutex_};
return submit_reports_;
}
private:
bool session_token_matches(const LvhWindowsSessionToken &session_token) const {
return std::ranges::equal(session_token.bytes, session_token_.bytes);
}
static LvhWindowsSessionToken make_session_token() {
LvhWindowsSessionToken session_token {};
for (std::uint8_t index = 0; index < LVH_WINDOWS_SESSION_TOKEN_SIZE; ++index) {
session_token.bytes[index] = static_cast<std::uint8_t>(0x10U + index);
}
return session_token;
}
mutable std::mutex mutex_;
std::string path_ = R"(\\.\LibVirtualHid)";
std::string response_device_path_ = R"(\\.\LibVirtualHid#100)";
std::uint64_t next_driver_id_ = 100;
LvhWindowsSessionToken session_token_ = make_session_token();
OperationStatus create_transport_status_ = OperationStatus::success();
std::uint32_t create_protocol_status_ = LVH_WINDOWS_STATUS_SUCCESS;
OperationStatus submit_status_ = OperationStatus::success();
OperationStatus destroy_status_ = OperationStatus::success();
std::vector<LvhWindowsCreateDeviceRequest> create_requests_;
std::vector<std::vector<std::uint8_t>> submit_reports_;
std::vector<std::uint64_t> destroyed_ids_;
std::vector<LvhWindowsOutputReportEvent> output_events_;
};
class FakeWindowsControlChannel final: public WindowsControlChannel {
public:
explicit FakeWindowsControlChannel(std::shared_ptr<FakeWindowsControlChannelState> state):
state_ {std::move(state)} {}
const std::string &path() const override {
return state_->path();
}
OperationStatus create_device(
const LvhWindowsCreateDeviceRequest &request,
LvhWindowsCreateDeviceResponse &response
) const override {
return state_->create_device(request, response);
}
OperationStatus destroy_device(
std::uint64_t driver_device_id,
const LvhWindowsSessionToken &session_token
) const override {
return state_->destroy_device(driver_device_id, session_token);
}
OperationStatus submit_input_report(
std::uint64_t /*driver_device_id*/,
const LvhWindowsSessionToken &session_token,
const std::vector<std::uint8_t> &report
) const override {
return state_->submit_input_report(session_token, report);
}
std::optional<LvhWindowsOutputReportEvent> read_output_report(HANDLE stop_event) const override {
if (auto event = state_->pop_output_event(); event.has_value()) {
return event;
}
static_cast<void>(::WaitForSingleObject(stop_event, 1U));
return std::nullopt;
}
private:
std::shared_ptr<FakeWindowsControlChannelState> state_;
};
std::unique_ptr<WindowsBackend> make_fake_windows_backend(
std::shared_ptr<FakeWindowsControlChannelState> command_state,
std::shared_ptr<FakeWindowsControlChannelState> event_state
) {
return std::make_unique<WindowsBackend>(
std::make_unique<FakeWindowsControlChannel>(std::move(command_state)),
std::make_unique<FakeWindowsControlChannel>(std::move(event_state))
);
}
template<typename Predicate>
bool wait_until(Predicate predicate) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds {250};
while (std::chrono::steady_clock::now() < deadline) {
if (predicate()) {
return true;
}
std::this_thread::sleep_for(std::chrono::milliseconds {1});
}
return predicate();
}
void wait_for_output_events_to_drain(const std::shared_ptr<FakeWindowsControlChannelState> &state) {
static_cast<void>(wait_until([&state] {
return state->output_events_empty();
}));
}
std::uint64_t last_created_driver_id(const std::shared_ptr<FakeWindowsControlChannelState> &state) {
return state->last_created_driver_id();
}
void capture_created_hid_device(
const std::shared_ptr<FakeWindowsControlChannelState> &state,
test::WindowsHidCreatedDevice &device
) {
const auto requests = state->create_requests();
if (requests.empty()) {
return;
}
const auto &request = requests.front();
device.device_type = request.device_type;
device.bus_type = request.bus_type;
device.flags = request.flags;
device.vendor_id = request.hardware_ids.vendor_id;
device.product_id = request.hardware_ids.product_id;
device.version = request.hardware_ids.device_version;
device.report_id = request.hardware_ids.report_id;
device.input_report_size = request.report_sizes.input_report_size;
device.output_report_size = request.report_sizes.output_report_size;
device.name = request.name.data();
device.manufacturer = request.manufacturer.data();
device.stable_id = request.stable_id.data();
}
void enqueue_output_report(
const std::shared_ptr<FakeWindowsControlChannelState> &state,
std::uint64_t driver_id,
const DeviceProfile &profile
) {
LvhWindowsOutputReportEvent event {};
event.version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION;
event.size = sizeof(event);
event.driver_device_id = driver_id;
event.report_size = static_cast<std::uint32_t>(profile.output_report_size);
if (profile.gamepad_kind == GamepadProfileKind::xbox_one || profile.gamepad_kind == GamepadProfileKind::xbox_series) {
event.report[0] = 0x03;
event.report[3] = 75;
event.report[4] = 100;
event.report[5] = 10;
} else {
event.report[0] = profile.report_id;
event.report[1] = 0x78;
event.report[2] = 0x56;
event.report[3] = 0x34;
event.report[4] = 0x12;
}
state->enqueue_output_event(event);
}
OperationStatus create_gamepad_with_protocol_status(std::uint32_t protocol_status_code) {
auto command_state = std::make_shared<FakeWindowsControlChannelState>();
command_state->set_create_protocol_status(protocol_status_code);
auto backend = make_fake_windows_backend(command_state, std::make_shared<FakeWindowsControlChannelState>());
CreateGamepadOptions options;
options.profile = profiles::xbox_series();
return backend->create_gamepad(1, options).status;
}
struct FakeSendInputState {
std::vector<test::WindowsSendInputRecord> sent_inputs;
std::optional<UINT> forced_sent_count;
};
test::WindowsSendInputRecord make_send_input_record(const INPUT &input) {
test::WindowsSendInputRecord record;
record.type = input.type;
if (input.type == INPUT_KEYBOARD) {
record.virtual_key = input.ki.wVk;
record.scan_code = input.ki.wScan;
record.key_flags = input.ki.dwFlags;
} else if (input.type == INPUT_MOUSE) {
record.mouse_x = input.mi.dx;
record.mouse_y = input.mi.dy;
record.mouse_data = input.mi.mouseData;
record.mouse_flags = input.mi.dwFlags;
}
return record;
}
UINT fake_send_input(FakeSendInputState &state, std::span<INPUT> inputs) {
for (const auto &input : inputs) {
state.sent_inputs.push_back(make_send_input_record(input));
}
if (state.forced_sent_count.has_value()) {
::SetLastError(ERROR_ACCESS_DENIED);
return *state.forced_sent_count;
}
return static_cast<UINT>(inputs.size());
}
class ScopedFakeSendInput {
public:
explicit ScopedFakeSendInput(FakeSendInputState &state) {
previous_function_ = send_input_function();
previous_sync_function_ = sync_thread_desktop_function();
send_input_function() = [&state](std::span<INPUT> inputs) {
return fake_send_input(state, inputs);
};
sync_thread_desktop_function() = [] {
return static_cast<HDESK>(nullptr);
};
}
ScopedFakeSendInput(const ScopedFakeSendInput &) = delete;
ScopedFakeSendInput &operator=(const ScopedFakeSendInput &) = delete;
ScopedFakeSendInput(ScopedFakeSendInput &&) noexcept = delete;
ScopedFakeSendInput &operator=(ScopedFakeSendInput &&) noexcept = delete;
~ScopedFakeSendInput() {
send_input_function() = std::move(previous_function_);
sync_thread_desktop_function() = std::move(previous_sync_function_);
}
private:
SendInputFunction previous_function_ = send_input_with_win32;
SyncThreadDesktopFunction previous_sync_function_ = sync_thread_desktop_with_win32;
};
struct FakeSyntheticPointerState {
std::vector<test::WindowsSyntheticPointerRecord> injected_pointers;
std::vector<POINTER_INPUT_TYPE> created_types;
std::size_t destroyed_devices = 0;
std::optional<BOOL> forced_inject_result;
bool force_create_failure = false;
std::uintptr_t next_device = 1;
};
test::WindowsSyntheticPointerRecord make_synthetic_pointer_record(const POINTER_TYPE_INFO &pointer, UINT32 count) {
test::WindowsSyntheticPointerRecord record;
record.type = pointer.type;
record.count = count;
if (pointer.type == PT_TOUCH) {
const auto &touch = pointer.touchInfo;
record.pointer_flags = touch.pointerInfo.pointerFlags;
record.pointer_id = touch.pointerInfo.pointerId;
record.x = touch.pointerInfo.ptPixelLocation.x;
record.y = touch.pointerInfo.ptPixelLocation.y;
record.touch_mask = touch.touchMask;
record.touch_pressure = touch.pressure;
record.touch_orientation = touch.orientation;
} else if (pointer.type == PT_PEN) {
const auto &pen = pointer.penInfo;
record.pointer_flags = pen.pointerInfo.pointerFlags;
record.pointer_id = pen.pointerInfo.pointerId;
record.x = pen.pointerInfo.ptPixelLocation.x;
record.y = pen.pointerInfo.ptPixelLocation.y;
record.pen_mask = pen.penMask;
record.pen_flags = pen.penFlags;
record.pen_tilt_x = pen.tiltX;
record.pen_tilt_y = pen.tiltY;
}
return record;
}
HSYNTHETICPOINTERDEVICE fake_create_synthetic_pointer_device(
FakeSyntheticPointerState &state,
POINTER_INPUT_TYPE type,
ULONG /*max_count*/,
POINTER_FEEDBACK_MODE /*mode*/
) {
if (state.force_create_failure) {
::SetLastError(ERROR_ACCESS_DENIED);
return nullptr;
}
static_assert(sizeof(HSYNTHETICPOINTERDEVICE) == sizeof(state.next_device));
state.created_types.push_back(type);
return std::bit_cast<HSYNTHETICPOINTERDEVICE>(state.next_device++);
}
BOOL fake_inject_synthetic_pointer_input(
FakeSyntheticPointerState &state,
HSYNTHETICPOINTERDEVICE /*device*/,
const POINTER_TYPE_INFO *pointer_info,
UINT32 count
) {
for (UINT32 index = 0; index < count; ++index) {
state.injected_pointers.push_back(make_synthetic_pointer_record(pointer_info[index], count));
}
if (state.forced_inject_result.has_value()) {
::SetLastError(ERROR_ACCESS_DENIED);
return *state.forced_inject_result;
}
return TRUE;
}
void fake_destroy_synthetic_pointer_device(FakeSyntheticPointerState &state, HSYNTHETICPOINTERDEVICE /*device*/) {
++state.destroyed_devices;
}
class ScopedFakeSyntheticPointerApi {
public:
explicit ScopedFakeSyntheticPointerApi(FakeSyntheticPointerState &state) {
previous_api_ = synthetic_pointer_api();
synthetic_pointer_api() = SyntheticPointerApi {
.create = [&state](POINTER_INPUT_TYPE type, ULONG max_count, POINTER_FEEDBACK_MODE mode) {
return fake_create_synthetic_pointer_device(state, type, max_count, mode);
},
.inject = [&state](HSYNTHETICPOINTERDEVICE device, const POINTER_TYPE_INFO *pointer_info, UINT32 count) {
return fake_inject_synthetic_pointer_input(state, device, pointer_info, count);
},
.destroy = [&state](HSYNTHETICPOINTERDEVICE device) {
fake_destroy_synthetic_pointer_device(state, device);
},
};
}
ScopedFakeSyntheticPointerApi(const ScopedFakeSyntheticPointerApi &) = delete;
ScopedFakeSyntheticPointerApi &operator=(const ScopedFakeSyntheticPointerApi &) = delete;
ScopedFakeSyntheticPointerApi(ScopedFakeSyntheticPointerApi &&) noexcept = delete;
ScopedFakeSyntheticPointerApi &operator=(ScopedFakeSyntheticPointerApi &&) noexcept = delete;
~ScopedFakeSyntheticPointerApi() {
synthetic_pointer_api() = std::move(previous_api_);
}
private:
SyntheticPointerApi previous_api_;
};
} // namespace
namespace test {
WindowsBackendLifecycleResult windows_backend_fake_channel_lifecycle() {
WindowsBackendLifecycleResult result;
auto command_state = std::make_shared<FakeWindowsControlChannelState>();
auto event_state = std::make_shared<FakeWindowsControlChannelState>();
auto backend = make_fake_windows_backend(command_state, event_state);
result.capabilities = backend->capabilities();
CreateGamepadOptions options;
options.profile = profiles::xbox_series();
auto created = backend->create_gamepad(7, options);
result.create_status = created.status;
if (created) {
result.device_nodes = created.gamepad->device_nodes();
std::vector<std::uint8_t> report(options.profile.input_report_size, 0x7A);
result.submit_status = created.gamepad->submit({}, report);
const auto driver_id = last_created_driver_id(command_state);
enqueue_output_report(event_state, driver_id, options.profile);
wait_for_output_events_to_drain(event_state);
std::atomic_bool output_seen {false};
created.gamepad->set_output_callback([&result, &output_seen](const GamepadOutput &output) {
if (output.kind == GamepadOutputKind::rumble) {
result.last_output = output;
result.saw_output = true;
output_seen.store(true);
}
});
enqueue_output_report(event_state, driver_id, options.profile);
static_cast<void>(wait_until([&output_seen] {
return output_seen.load();
}));
result.close_status = created.gamepad->close();
enqueue_output_report(event_state, driver_id, options.profile);
wait_for_output_events_to_drain(event_state);
result.second_close_status = created.gamepad->close();
result.submit_after_close_status = created.gamepad->submit({}, report);
result.create_requests = command_state->create_request_count();
result.submit_requests = command_state->submit_report_count();
result.destroy_requests = command_state->destroy_request_count();
}
return result;
}
WindowsSwitchReportStreamResult windows_backend_switch_report_stream() {
WindowsSwitchReportStreamResult result;
auto command_state = std::make_shared<FakeWindowsControlChannelState>();
auto backend = make_fake_windows_backend(command_state, std::make_shared<FakeWindowsControlChannelState>());
CreateGamepadOptions options;
options.profile = profiles::switch_pro();
auto created = backend->create_gamepad(8, options);
result.create_status = created.status;
if (created) {
result.expected_report.assign(options.profile.input_report_size, 0x5AU);
result.expected_report[0] = 0x30U;
result.submit_status = created.gamepad->submit({}, result.expected_report);
result.repeated_report = wait_until([&command_state] {
return command_state->submit_report_count() >= 2U;
});
result.close_status = created.gamepad->close();
result.submitted_reports = command_state->submit_reports();
}
return result;
}
WindowsXboxInputDeduplicationResult windows_backend_xbox_input_deduplication(GamepadProfileKind kind) {
WindowsXboxInputDeduplicationResult result;
auto command_state = std::make_shared<FakeWindowsControlChannelState>();
auto backend = make_fake_windows_backend(command_state, std::make_shared<FakeWindowsControlChannelState>());
CreateGamepadOptions options;
options.profile = kind == GamepadProfileKind::xbox_one ? profiles::xbox_one() : profiles::xbox_series();
auto created = backend->create_gamepad(9, options);
result.create_status = created.status;
if (created) {
auto report = std::vector<std::uint8_t>(options.profile.input_report_size, 0U);
report[12] = 0x10U;
result.left_shoulder_status = created.gamepad->submit({}, report);
result.repeated_left_shoulder_status = created.gamepad->submit({}, report);
report[12] = 0x20U;
result.right_shoulder_status = created.gamepad->submit({}, report);
result.repeated_right_shoulder_status = created.gamepad->submit({}, report);
report[12] = 0U;
result.release_status = created.gamepad->submit({}, report);
result.close_status = created.gamepad->close();
result.submitted_reports = command_state->submit_reports();
}
return result;
}
WindowsHidMouseResult windows_backend_hid_mouse() {
using enum MouseEventKind;
WindowsHidMouseResult result;
auto command_state = std::make_shared<FakeWindowsControlChannelState>();
auto backend = make_fake_windows_backend(command_state, std::make_shared<FakeWindowsControlChannelState>());
CreateMouseOptions options;
options.profile = profiles::mouse();
options.profile.bus_type = BusType::bluetooth;
options.profile.vendor_id = 0x1234;
options.profile.product_id = 0x5678;
options.profile.version = 0x4321;
options.profile.name = "Configured mouse";
options.profile.manufacturer = "Configured manufacturer";
options.stable_id = "configured-mouse";
auto created = backend->create_mouse(8U, options);
result.operations.create_status = created.status;
if (created) {
result.operations.motion_status = created.mouse->submit(MouseEvent {
.kind = relative_motion,
.x = 70000,
.y = -70000,
});
result.operations.large_scroll_status = created.mouse->submit(MouseEvent {
.kind = vertical_scroll,
.high_resolution_scroll = 120 * 132,
});
result.operations.partial_scroll_status = created.mouse->submit(MouseEvent {
.kind = horizontal_scroll,
.high_resolution_scroll = 60,
});
result.operations.completed_scroll_status = created.mouse->submit(MouseEvent {
.kind = horizontal_scroll,
.high_resolution_scroll = 60,
});
result.operations.close_status = created.mouse->close();
}
capture_created_hid_device(command_state, result.device);
result.observations.reports = command_state->submit_reports();
result.observations.destroy_requests = command_state->destroy_request_count();
{
auto failure_state = std::make_shared<FakeWindowsControlChannelState>();
failure_state->set_create_protocol_status(LVH_WINDOWS_STATUS_BACKEND_FAILURE);
auto failure_backend = make_fake_windows_backend(
failure_state,
std::make_shared<FakeWindowsControlChannelState>()
);
result.operations.protocol_failure_status = failure_backend->create_mouse(9U, options).status;
}
{
FakeSendInputState send_input_state;
ScopedFakeSendInput fake_send_input {send_input_state};
auto license_state = std::make_shared<FakeWindowsControlChannelState>();
license_state->set_create_transport_status(
OperationStatus::failure(ErrorCode::license_required, "license required")
);
auto license_backend = make_fake_windows_backend(
license_state,
std::make_shared<FakeWindowsControlChannelState>()
);
auto fallback = license_backend->create_mouse(10U, options);
result.operations.license_fallback_create_status = fallback.status;
if (fallback) {
result.operations.license_fallback_submit_status = fallback.mouse->submit(MouseEvent {
.kind = relative_motion,
.x = 1,
.y = 2,
});
}
result.observations.license_create_requests = license_state->create_request_count();
result.observations.license_fallback_send_inputs = send_input_state.sent_inputs.size();
}
return result;
}
WindowsHidKeyboardResult windows_backend_hid_keyboard() {
WindowsHidKeyboardResult result;
CreateKeyboardOptions options;
options.profile = profiles::keyboard();
options.profile.bus_type = BusType::usb;
options.profile.vendor_id = 0x1234;
options.profile.product_id = 0x5678;
options.profile.version = 0x4321;
options.profile.name = "Configured keyboard";
options.profile.manufacturer = "Configured manufacturer";
options.stable_id = "configured-keyboard";
{
FakeSendInputState send_input_state;
ScopedFakeSendInput fake_send_input {send_input_state};
auto command_state = std::make_shared<FakeWindowsControlChannelState>();
auto backend = make_fake_windows_backend(
command_state,
std::make_shared<FakeWindowsControlChannelState>()
);
auto created = backend->create_keyboard(18U, options);
result.operations.create_status = created.status;
if (created) {
result.operations.letter_press_status = created.keyboard->submit({
.key_code = 'A',
.pressed = true,
.uses_normalized_key_code = true,
.prefer_native_scan_code = true,
});
result.operations.modifier_press_status =
created.keyboard->submit({.key_code = VK_LSHIFT, .pressed = true});
result.operations.extended_press_status = created.keyboard->submit({
.key_code = VK_RIGHT,
.pressed = true,
.scan_code = 0x4DU,
});
result.operations.letter_release_status = created.keyboard->submit({.key_code = 'A', .pressed = false});
result.operations.unmapped_submit_status =
created.keyboard->submit({.key_code = VK_BROWSER_BACK, .pressed = true});
result.operations.text_status = created.keyboard->type_text({.text = "A"});
result.operations.close_status = created.keyboard->close();
}
capture_created_hid_device(command_state, result.device);
result.observations.reports = command_state->submit_reports();
result.observations.destroy_requests = command_state->destroy_request_count();
result.observations.fallback_send_inputs = send_input_state.sent_inputs.size();
}
{
auto failure_state = std::make_shared<FakeWindowsControlChannelState>();
failure_state->set_create_protocol_status(LVH_WINDOWS_STATUS_BACKEND_FAILURE);
auto failure_backend = make_fake_windows_backend(
failure_state,
std::make_shared<FakeWindowsControlChannelState>()
);
result.operations.protocol_failure_status = failure_backend->create_keyboard(19U, options).status;
}
{
FakeSendInputState send_input_state;
ScopedFakeSendInput fake_send_input {send_input_state};
auto license_state = std::make_shared<FakeWindowsControlChannelState>();
license_state->set_create_transport_status(
OperationStatus::failure(ErrorCode::license_required, "license required")
);
auto license_backend = make_fake_windows_backend(
license_state,
std::make_shared<FakeWindowsControlChannelState>()
);
auto fallback = license_backend->create_keyboard(20U, options);
result.operations.license_fallback_create_status = fallback.status;
if (fallback) {
result.operations.license_fallback_submit_status =
fallback.keyboard->submit({.key_code = 'B', .pressed = true});
}
result.observations.license_create_requests = license_state->create_request_count();
result.observations.license_fallback_send_inputs = send_input_state.sent_inputs.size();
}
return result;
}
WindowsPlayStationTransportResult windows_backend_playstation_transport() {
WindowsPlayStationTransportResult result;
auto command_state = std::make_shared<FakeWindowsControlChannelState>();
auto event_state = std::make_shared<FakeWindowsControlChannelState>();
auto backend = make_fake_windows_backend(command_state, event_state);
CreateGamepadOptions options;
options.profile = profiles::dualshock4();
auto dualshock4 = backend->create_gamepad(40, options);
result.dualshock4_status = dualshock4.status;
if (dualshock4.effective_profile.has_value()) {
result.dualshock4_effective_profile = std::move(*dualshock4.effective_profile);
}
if (dualshock4.gamepad) {
static_cast<void>(dualshock4.gamepad->close());
}
options.profile = profiles::dualsense();
auto dualsense = backend->create_gamepad(41, options);
result.dualsense_status = dualsense.status;
if (dualsense.effective_profile.has_value()) {
result.dualsense_effective_profile = std::move(*dualsense.effective_profile);
}
if (dualsense.gamepad) {
static_cast<void>(dualsense.gamepad->close());
}
return result;
}
WindowsGenericPidOrderingResult windows_backend_generic_pid_callback_ordering() {
WindowsGenericPidOrderingResult result;
auto command_state = std::make_shared<FakeWindowsControlChannelState>();
auto event_state = std::make_shared<FakeWindowsControlChannelState>();
auto backend = make_fake_windows_backend(command_state, event_state);
CreateGamepadOptions options;
options.profile = profiles::generic_gamepad();
auto created = backend->create_gamepad(30, options);
if (!created) {
return result;
}
std::mutex gate_mutex;
std::condition_variable gate_ready;
bool zero_waiting = false;
bool release_zero = false;
std::mutex strengths_mutex;
created.gamepad->set_output_callback([&gate_mutex,
&gate_ready,
&zero_waiting,
&release_zero,
&strengths_mutex,
&result](const GamepadOutput &output) {
if (output.kind != GamepadOutputKind::rumble) {
return;
}
if (output.low_frequency_rumble == 0U) {
std::unique_lock lock {gate_mutex};
zero_waiting = true;
gate_ready.notify_all();
gate_ready.wait(lock, [&release_zero] {
return release_zero;
});
}
std::lock_guard lock {strengths_mutex};
result.strengths.push_back(output.low_frequency_rumble);
});
const auto driver_id = last_created_driver_id(command_state);
const auto enqueue_report = [&](std::span<const std::uint8_t> report) {
LvhWindowsOutputReportEvent event {};
event.version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION;
event.size = sizeof(event);
event.driver_device_id = driver_id;
event.report_size = static_cast<std::uint32_t>(report.size());
std::ranges::copy(report, event.report.begin());
event_state->enqueue_output_event(event);
};
std::array<std::uint8_t, windows::generic_pid_output_report_size> set_effect {};
set_effect[0] = windows::generic_pid_set_effect_report_id;
set_effect[1] = 1;
set_effect[2] = 1;
set_effect[3] = 10; // Ten millisecond duration.
set_effect[11] = 255;
std::array<std::uint8_t, windows::generic_pid_output_report_size> set_magnitude {};
set_magnitude[0] = windows::generic_pid_set_constant_force_report_id;
set_magnitude[1] = 1;
set_magnitude[2] = 0x10;
set_magnitude[3] = 0x27;
std::array<std::uint8_t, windows::generic_pid_output_report_size> start_effect {};
start_effect[0] = windows::generic_pid_effect_operation_report_id;
start_effect[1] = 1;
start_effect[2] = 1;
start_effect[3] = 1;
enqueue_report(set_effect);
enqueue_report(set_magnitude);
enqueue_report(start_effect);
const auto saw_start = wait_until([&strengths_mutex, &result] {
std::lock_guard lock {strengths_mutex};
return !result.strengths.empty() && result.strengths.front() > 0U;
});
bool saw_expiry = false;
if (saw_start) {
std::unique_lock lock {gate_mutex};
saw_expiry = gate_ready.wait_for(lock, std::chrono::milliseconds {250}, [&zero_waiting] {
return zero_waiting;
});
}
if (saw_expiry) {
enqueue_report(start_effect);
wait_for_output_events_to_drain(event_state);
}
{
std::lock_guard lock {gate_mutex};
release_zero = true;
}
gate_ready.notify_all();
result.completed = saw_expiry && wait_until([&strengths_mutex, &result] {
std::lock_guard lock {strengths_mutex};
return result.strengths.size() >= 3U;
});
static_cast<void>(created.gamepad->close());
return result;
}
WindowsBackendFailureResult windows_backend_fake_channel_failures() {
WindowsBackendFailureResult result;
result.invalid_argument_status = create_gamepad_with_protocol_status(LVH_WINDOWS_STATUS_INVALID_ARGUMENT);
result.unsupported_profile_status = create_gamepad_with_protocol_status(LVH_WINDOWS_STATUS_UNSUPPORTED_PROFILE);
result.device_closed_status = create_gamepad_with_protocol_status(LVH_WINDOWS_STATUS_DEVICE_NOT_FOUND);
result.backend_failure_status = create_gamepad_with_protocol_status(LVH_WINDOWS_STATUS_BACKEND_FAILURE);
{
auto command_state = std::make_shared<FakeWindowsControlChannelState>();
command_state->set_create_transport_status(
OperationStatus::failure(ErrorCode::backend_failure, "transport failed")
);
auto backend = make_fake_windows_backend(command_state, std::make_shared<FakeWindowsControlChannelState>());
CreateGamepadOptions options;
options.profile = profiles::xbox_series();
result.transport_failure_status = backend->create_gamepad(2, options).status;
}
{
WindowsBackend backend {nullptr, nullptr};
CreateGamepadOptions options;
options.profile = profiles::xbox_series();
result.unavailable_status = backend.create_gamepad(3, options).status;
auto oversized_descriptor_options = options;
oversized_descriptor_options.profile.report_descriptor.assign(LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE + 1U, 0x01);
result.oversized_descriptor_status = backend.create_gamepad(17, oversized_descriptor_options).status;
auto oversized_input_options = options;
oversized_input_options.profile.input_report_size = LVH_WINDOWS_MAX_INPUT_REPORT_SIZE + 1U;
result.oversized_input_report_status = backend.create_gamepad(18, oversized_input_options).status;
auto oversized_output_options = options;
oversized_output_options.profile.output_report_size = LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE + 1U;
result.oversized_output_report_status = backend.create_gamepad(19, oversized_output_options).status;
}
{
auto backend = make_fake_windows_backend(
std::make_shared<FakeWindowsControlChannelState>(),
std::make_shared<FakeWindowsControlChannelState>()
);
CreateGamepadOptions options;
options.profile = profiles::xbox_360();
result.xbox_360_create_status = backend->create_gamepad(20, options).status;
}
{
auto command_state = std::make_shared<FakeWindowsControlChannelState>("", "");
auto backend = make_fake_windows_backend(command_state, std::make_shared<FakeWindowsControlChannelState>());
CreateGamepadOptions options;
options.profile = profiles::xbox_series();
auto created = backend->create_gamepad(4, options);
result.empty_nodes_create_status = created.status;
if (created) {
result.empty_device_nodes = created.gamepad->device_nodes();
result.oversized_submit_status = created.gamepad->submit(
{},
std::vector<std::uint8_t>(LVH_WINDOWS_MAX_INPUT_REPORT_SIZE + 1U, 0x7B)
);
}
}
return result;
}
WindowsBackendUtilityResult windows_backend_fake_channel_utilities() {
WindowsBackendUtilityResult result;
result.default_device_paths = resolve_control_device_paths();
constexpr auto environment_name = "LIBVIRTUALHID_WINDOWS_CONTROL_DEVICE";
constexpr auto custom_path = R"(\\.\LibVirtualHid-Test)";
std::string original_value;
const auto had_original_value = lizardbyte::common::get_env(environment_name, original_value);
static_cast<void>(lizardbyte::common::set_env(environment_name, custom_path));
result.custom_device_paths = resolve_control_device_paths();
if (had_original_value) {
static_cast<void>(lizardbyte::common::set_env(environment_name, original_value));
} else {
static_cast<void>(lizardbyte::common::unset_env(environment_name));
}
result.formatted_error_status =
windows_failure(ErrorCode::backend_failure, "format known Windows error", ERROR_FILE_NOT_FOUND);
result.fallback_error_status =
windows_failure(ErrorCode::backend_failure, "format unknown Windows error", 0xE0000001U);
{
WindowsBackend backend {nullptr, nullptr};
CreateKeyboardOptions keyboard_options;
keyboard_options.profile = profiles::keyboard();
auto keyboard = backend.create_keyboard(8, keyboard_options);
result.keyboard_create_status = keyboard.status;
if (keyboard) {
result.keyboard_close_status = keyboard.keyboard->close();
result.keyboard_submit_after_close_status =
keyboard.keyboard->submit({.key_code = 0x41, .pressed = true});
}
CreateMouseOptions mouse_options;
mouse_options.profile = profiles::mouse();
auto mouse = backend.create_mouse(9, mouse_options);
result.mouse_create_status = mouse.status;
if (mouse) {
result.mouse_close_status = mouse.mouse->close();
MouseEvent mouse_event;
mouse_event.kind = MouseEventKind::relative_motion;
mouse_event.x = 1;
result.mouse_submit_after_close_status = mouse.mouse->submit(mouse_event);
}
}
auto invalid_context = std::make_shared<WindowsBackendContext>(
std::unique_ptr<WindowsControlChannel> {},
std::unique_ptr<WindowsControlChannel> {}
);
invalid_context->start();
auto context = std::make_shared<WindowsBackendContext>(
std::make_unique<FakeWindowsControlChannel>(std::make_shared<FakeWindowsControlChannelState>()),
std::make_unique<FakeWindowsControlChannel>(std::make_shared<FakeWindowsControlChannelState>())
);
context->start();
context->start();
context->stop();
result.timeout_result = wait_until([] {
return false;
});
return result;
}
WindowsOverlappedIoResult windows_backend_overlapped_device_io() {
WindowsOverlappedIoResult result;
DWORD bytes_returned = 0;
result.immediate_status = run_overlapped_device_io(
"complete immediate test operation",
&bytes_returned,
[&result](const OVERLAPPED &overlapped, DWORD *result_size) {
result.immediate_saw_event = overlapped.hEvent != nullptr;
*result_size = 7U;
return TRUE;
},
[&result](OVERLAPPED &, DWORD *, BOOL) {
result.immediate_called_completion = true;
return TRUE;
}
);
result.immediate_bytes_returned = bytes_returned;
bytes_returned = 0;
result.pending_status = run_overlapped_device_io(
"complete pending test operation",
&bytes_returned,
[&result](const OVERLAPPED &overlapped, DWORD *) {
result.pending_saw_event = overlapped.hEvent != nullptr;
::SetLastError(ERROR_IO_PENDING);
return FALSE;
},
[&result](OVERLAPPED &, DWORD *result_size, BOOL wait) {
result.pending_waited = wait != FALSE;
*result_size = 11U;
return TRUE;
}
);
result.pending_bytes_returned = bytes_returned;
OVERLAPPED canceled {};
cancel_and_drain_overlapped_io(
canceled,
&bytes_returned,
[&result](OVERLAPPED &) {
result.cancellation_called = true;
return TRUE;
},
[&result](OVERLAPPED &, DWORD *, BOOL wait) {
result.cancellation_drained_after_cancel = result.cancellation_called;
result.cancellation_waited = wait != FALSE;
::SetLastError(ERROR_OPERATION_ABORTED);
return FALSE;
}
);
result.start_failure_status = run_overlapped_device_io(
"fail test operation start",
&bytes_returned,
[](OVERLAPPED &, DWORD *) {
::SetLastError(ERROR_ACCESS_DENIED);
return FALSE;
},
[](OVERLAPPED &, DWORD *, BOOL) {
return TRUE;
}
);
result.completion_failure_status = run_overlapped_device_io(
"fail pending test operation",
&bytes_returned,
[](OVERLAPPED &, DWORD *) {
::SetLastError(ERROR_IO_PENDING);
return FALSE;
},
[](OVERLAPPED &, DWORD *, BOOL) {
::SetLastError(ERROR_OPERATION_ABORTED);
return FALSE;
}
);
return result;
}
WindowsBackendSendInputResult windows_backend_send_input_devices() {
using enum MouseEventKind;
WindowsBackendSendInputResult result;
FakeSendInputState fake_send_input;
ScopedFakeSendInput scoped_send_input {fake_send_input};
FakeSyntheticPointerState fake_synthetic_pointer;
ScopedFakeSyntheticPointerApi scoped_synthetic_pointer {fake_synthetic_pointer};
WindowsBackend backend {nullptr, nullptr};
result.synthetic.capabilities = backend.capabilities();
CreateKeyboardOptions keyboard_options;
keyboard_options.profile = profiles::keyboard();
if (auto keyboard = backend.create_keyboard(10, keyboard_options); keyboard) {
result.keyboard.down_status = keyboard.keyboard->submit({.key_code = 0x41, .pressed = true});
result.keyboard.up_status = keyboard.keyboard->submit({.key_code = 0x41, .pressed = false});
result.keyboard.text_status = keyboard.keyboard->type_text({.text = "Az"});
result.keyboard.empty_text_status = keyboard.keyboard->type_text({.text = ""});
result.keyboard.invalid_text_status =
keyboard.keyboard->type_text({.text = std::string(1U, static_cast<char>(0xFF))});
fake_send_input.forced_sent_count = 0U;
result.keyboard.failure_status = keyboard.keyboard->submit({.key_code = 0x42, .pressed = true});
fake_send_input.forced_sent_count.reset();
const auto prior_input_count = fake_send_input.sent_inputs.size();
result.keyboard.normalized_status =
keyboard.keyboard->submit({.key_code = 0x41, .pressed = true, .uses_normalized_key_code = true});
if (fake_send_input.sent_inputs.size() > prior_input_count) {
result.keyboard.normalized_input = fake_send_input.sent_inputs.back();
fake_send_input.sent_inputs.resize(prior_input_count);
}
} else {
result.keyboard.down_status = keyboard.status;
}
CreateMouseOptions mouse_options;
mouse_options.profile = profiles::mouse();
if (auto mouse = backend.create_mouse(11, mouse_options); mouse) {
result.mouse.relative_status = mouse.mouse->submit({.kind = relative_motion, .x = 11, .y = -12});
result.mouse.absolute_status =
mouse.mouse->submit({.kind = absolute_motion, .x = 50, .y = 25, .width = 100, .height = 50});
result.mouse.degenerate_absolute_status =
mouse.mouse->submit({.kind = absolute_motion, .x = 99, .y = 99, .width = 1, .height = 0});
MouseEvent button_event;
button_event.kind = button;
button_event.button = MouseButton::left;
button_event.pressed = true;
result.mouse.left_button_status = mouse.mouse->submit(button_event);
button_event.button = MouseButton::middle;
button_event.pressed = false;
result.mouse.middle_button_status = mouse.mouse->submit(button_event);
button_event.button = MouseButton::right;
button_event.pressed = true;
result.mouse.right_button_status = mouse.mouse->submit(button_event);
button_event.button = MouseButton::side;
result.mouse.side_button_status = mouse.mouse->submit(button_event);
button_event.button = MouseButton::extra;
button_event.pressed = false;
result.mouse.extra_button_status = mouse.mouse->submit(button_event);
result.mouse.vertical_scroll_status = mouse.mouse->submit({.kind = vertical_scroll, .high_resolution_scroll = 120});
result.mouse.horizontal_scroll_status =
mouse.mouse->submit({.kind = horizontal_scroll, .high_resolution_scroll = -240});
fake_send_input.forced_sent_count = 0U;
result.mouse.failure_status = mouse.mouse->submit({.kind = relative_motion, .x = 1, .y = 1});
fake_send_input.forced_sent_count.reset();
} else {
result.mouse.relative_status = mouse.status;
}
CreateKeyboardOptions invalid_keyboard_options;
invalid_keyboard_options.profile = profiles::mouse();
result.keyboard.invalid_profile_status = backend.create_keyboard(12, invalid_keyboard_options).status;
CreateMouseOptions invalid_mouse_options;
invalid_mouse_options.profile = profiles::keyboard();
result.mouse.invalid_profile_status = backend.create_mouse(13, invalid_mouse_options).status;
CreateTouchscreenOptions touchscreen_options;
touchscreen_options.profile = profiles::touchscreen();
if (auto touchscreen = backend.create_touchscreen(14, touchscreen_options); touchscreen) {
result.synthetic.touchscreen_create_status = touchscreen.status;
result.synthetic.touchscreen_place_status = touchscreen.touchscreen->place_contact({
.id = 7,
.x = 0.25F,
.y = 0.5F,
.pressure = 0.75F,
.orientation = 30,
.viewport = {.offset_x = 100, .offset_y = 200, .width = 400, .height = 300},
.contact_major_axis = 20.0F,
.contact_minor_axis = 10.0F,
});
result.synthetic.touchscreen_release_status =
touchscreen.touchscreen->release_contact(7, PointerTransition::release);
fake_synthetic_pointer.forced_inject_result = FALSE;
result.synthetic.touchscreen_failure_status = touchscreen.touchscreen->place_contact({
.id = 8,
.x = 0.1F,
.y = 0.2F,
.viewport = {.offset_x = 0, .offset_y = 0, .width = 100, .height = 100},
});
fake_synthetic_pointer.forced_inject_result.reset();
} else {
result.synthetic.touchscreen_create_status = touchscreen.status;
}
CreateTrackpadOptions trackpad_options;
trackpad_options.profile = profiles::trackpad();
result.unsupported.trackpad_status = backend.create_trackpad(15, trackpad_options).status;
CreatePenTabletOptions pen_tablet_options;
pen_tablet_options.profile = profiles::pen_tablet();
if (auto pen_tablet = backend.create_pen_tablet(16, pen_tablet_options); pen_tablet) {
result.synthetic.pen_tablet_create_status = pen_tablet.status;
result.synthetic.pen_tablet_button_status = pen_tablet.pen_tablet->button(PenButton::secondary, true);
result.synthetic.pen_tablet_tool_status = pen_tablet.pen_tablet->place_tool({
.tool = PenToolType::eraser,
.x = 0.5F,
.y = 0.75F,
.pressure = 0.5F,
.distance = -1.0F,
.tilt_x = 10.0F,
.tilt_y = -20.0F,
.viewport = {.offset_x = 10, .offset_y = 20, .width = 200, .height = 100},
});
} else {
result.synthetic.pen_tablet_create_status = pen_tablet.status;
}
CreateTouchscreenOptions invalid_touchscreen_options;
invalid_touchscreen_options.profile = profiles::pen_tablet();
result.synthetic.touchscreen_invalid_profile_status = backend.create_touchscreen(17, invalid_touchscreen_options).status;
CreatePenTabletOptions invalid_pen_tablet_options;
invalid_pen_tablet_options.profile = profiles::touchscreen();
result.synthetic.pen_tablet_invalid_profile_status = backend.create_pen_tablet(18, invalid_pen_tablet_options).status;
result.sent_inputs = std::move(fake_send_input.sent_inputs);
result.synthetic.created_devices = fake_synthetic_pointer.created_types.size();
result.synthetic.destroyed_devices = fake_synthetic_pointer.destroyed_devices;
result.synthetic.injected_pointers = std::move(fake_synthetic_pointer.injected_pointers);
return result;
}
} // namespace test
} // namespace lvh::detail