/** * @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 #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 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( ::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 &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 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 create_requests() const { std::lock_guard lock {mutex_}; return create_requests_; } std::vector> 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(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 create_requests_; std::vector> submit_reports_; std::vector destroyed_ids_; std::vector output_events_; }; class FakeWindowsControlChannel final: public WindowsControlChannel { public: explicit FakeWindowsControlChannel(std::shared_ptr 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 &report ) const override { return state_->submit_input_report(session_token, report); } std::optional read_output_report(HANDLE stop_event) const override { if (auto event = state_->pop_output_event(); event.has_value()) { return event; } static_cast(::WaitForSingleObject(stop_event, 1U)); return std::nullopt; } private: std::shared_ptr state_; }; std::unique_ptr make_fake_windows_backend( std::shared_ptr command_state, std::shared_ptr event_state ) { return std::make_unique( std::make_unique(std::move(command_state)), std::make_unique(std::move(event_state)) ); } template 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 &state) { static_cast(wait_until([&state] { return state->output_events_empty(); })); } std::uint64_t last_created_driver_id(const std::shared_ptr &state) { return state->last_created_driver_id(); } void capture_created_hid_device( const std::shared_ptr &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 &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(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(); command_state->set_create_protocol_status(protocol_status_code); auto backend = make_fake_windows_backend(command_state, std::make_shared()); CreateGamepadOptions options; options.profile = profiles::xbox_series(); return backend->create_gamepad(1, options).status; } struct FakeSendInputState { std::vector sent_inputs; std::optional 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 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(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 inputs) { return fake_send_input(state, inputs); }; sync_thread_desktop_function() = [] { return static_cast(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 injected_pointers; std::vector created_types; std::size_t destroyed_devices = 0; std::optional 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(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 { std::int32_t windows_backend_scale_absolute_axis_to_viewport( float value, std::int32_t source_dimension, std::int32_t viewport_offset, std::int32_t viewport_dimension, std::int32_t desktop_offset, std::int32_t desktop_dimension ) { return scale_absolute_axis_to_viewport( value, source_dimension, viewport_offset, viewport_dimension, desktop_offset, desktop_dimension ); } WindowsBackendLifecycleResult windows_backend_fake_channel_lifecycle() { WindowsBackendLifecycleResult result; auto command_state = std::make_shared(); auto event_state = std::make_shared(); 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 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(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(); auto backend = make_fake_windows_backend(command_state, std::make_shared()); 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(); auto backend = make_fake_windows_backend(command_state, std::make_shared()); 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(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(); auto backend = make_fake_windows_backend(command_state, std::make_shared()); 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(); failure_state->set_create_protocol_status(LVH_WINDOWS_STATUS_BACKEND_FAILURE); auto failure_backend = make_fake_windows_backend( failure_state, std::make_shared() ); 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(); 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() ); 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(); auto backend = make_fake_windows_backend( command_state, std::make_shared() ); 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.enter_press_status = created.keyboard->submit({ .key_code = VK_RETURN, .pressed = true, .uses_normalized_key_code = true, }); result.operations.enter_release_status = created.keyboard->submit({ .key_code = VK_RETURN, .pressed = false, .uses_normalized_key_code = true, }); result.operations.keypad_enter_press_status = created.keyboard->submit({ .key_code = VK_RETURN, .pressed = true, .extended = true, }); result.operations.keypad_enter_release_status = created.keyboard->submit({ .key_code = VK_RETURN, .pressed = false, .extended = true, }); 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(); failure_state->set_create_protocol_status(LVH_WINDOWS_STATUS_BACKEND_FAILURE); auto failure_backend = make_fake_windows_backend( failure_state, std::make_shared() ); 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(); 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() ); 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(); auto event_state = std::make_shared(); 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(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(dualsense.gamepad->close()); } return result; } WindowsGenericPidOrderingResult windows_backend_generic_pid_callback_ordering() { WindowsGenericPidOrderingResult result; auto command_state = std::make_shared(); auto event_state = std::make_shared(); 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 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(report.size()); std::ranges::copy(report, event.report.begin()); event_state->enqueue_output_event(event); }; std::array 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 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 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(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(); command_state->set_create_transport_status( OperationStatus::failure(ErrorCode::backend_failure, "transport failed") ); auto backend = make_fake_windows_backend(command_state, std::make_shared()); 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(), std::make_shared() ); CreateGamepadOptions options; options.profile = profiles::xbox_360(); result.xbox_360_create_status = backend->create_gamepad(20, options).status; } { auto command_state = std::make_shared("", ""); auto backend = make_fake_windows_backend(command_state, std::make_shared()); 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(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(lizardbyte::common::set_env(environment_name, custom_path)); result.custom_device_paths = resolve_control_device_paths(); if (had_original_value) { static_cast(lizardbyte::common::set_env(environment_name, original_value)); } else { static_cast(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( std::unique_ptr {}, std::unique_ptr {} ); invalid_context->start(); auto context = std::make_shared( std::make_unique(std::make_shared()), std::make_unique(std::make_shared()) ); 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(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); } result.keyboard.enter_status = keyboard.keyboard->submit({ .key_code = VK_RETURN, .pressed = true, .uses_normalized_key_code = true, }); if (fake_send_input.sent_inputs.size() > prior_input_count) { result.keyboard.enter_input = fake_send_input.sent_inputs.back(); fake_send_input.sent_inputs.resize(prior_input_count); } result.keyboard.keypad_enter_status = keyboard.keyboard->submit({ .key_code = VK_RETURN, .pressed = true, .uses_normalized_key_code = true, .extended = true, }); if (fake_send_input.sent_inputs.size() > prior_input_count) { result.keyboard.keypad_enter_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