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
78 changes: 76 additions & 2 deletions src/input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ extern "C" {
#include "input.h"
#include "text_context/bridge.h"
#include "input_activity.h"
#include "pen_barrel_roll.h"
#include "logging.h"
#include "platform/common.h"
#include "display_device/session.h"
Expand Down Expand Up @@ -217,6 +218,9 @@ namespace input {

std::vector<gamepad_t> gamepads;
activity::tracker_t activity_tracker;
pen_wire::roll_probe_t pen_roll_probe;
bool input_stopped = false;
std::chrono::steady_clock::time_point next_invalid_pen_warning {};
std::unique_ptr<platf::client_input_t> client_context;

safe::mail_raw_t::event_t<input::touch_port_t> touch_port_event;
Expand Down Expand Up @@ -615,6 +619,10 @@ namespace input {
case SS_PEN_MAGIC:
print((PSS_PEN_PACKET) payload);
break;
case pen_wire::magic:
print(&reinterpret_cast<pen_wire::packet_t *>(payload)->pen);
BOOST_LOG(debug) << "barrelRoll: " << pen_wire::decode_roll(reinterpret_cast<pen_wire::packet_t *>(payload)->barrelRoll);
break;
case SS_CONTROLLER_ARRIVAL_MAGIC:
print((PSS_CONTROLLER_ARRIVAL_PACKET) payload);
break;
Expand Down Expand Up @@ -1352,7 +1360,7 @@ namespace input {
* @param packet The pen packet.
*/
void
passthrough(std::shared_ptr<input_t> &input, PSS_PEN_PACKET packet) {
passthrough(std::shared_ptr<input_t> &input, PSS_PEN_PACKET packet, std::uint16_t barrel_roll = pen_wire::unknown) {
if (!config::input.mouse) {
return;
}
Expand Down Expand Up @@ -1400,6 +1408,7 @@ namespace input {
from_clamped_netfloat(packet->pressureOrDistance, 0.0f, 1.0f),
contact_area.first,
contact_area.second,
barrel_roll == pen_wire::unknown ? rotation : barrel_roll,
};

#ifdef SUNSHINE_TESTS
Expand Down Expand Up @@ -2006,6 +2015,16 @@ namespace input {
return batch((PSS_TOUCHPAD_FRAME_PACKET) dest, (PSS_TOUCHPAD_FRAME_PACKET) src);
case SS_PEN_MAGIC:
return batch((PSS_PEN_PACKET) dest, (PSS_PEN_PACKET) src);
case pen_wire::magic: {
auto *dest_pen = reinterpret_cast<pen_wire::packet_t *>(dest);
auto *src_pen = reinterpret_cast<pen_wire::packet_t *>(src);
auto result = batch(&dest_pen->pen, &src_pen->pen);
if (result == batch_result_e::batched) {
dest_pen->barrelRoll = src_pen->barrelRoll;
dest_pen->reserved = src_pen->reserved;
}
return result;
}
case SS_CONTROLLER_TOUCH_MAGIC:
return batch((PSS_CONTROLLER_TOUCH_PACKET) dest, (PSS_CONTROLLER_TOUCH_PACKET) src);
case SS_CONTROLLER_MOTION_MAGIC:
Expand Down Expand Up @@ -2120,6 +2139,11 @@ namespace input {
case SS_PEN_MAGIC:
passthrough(input, (PSS_PEN_PACKET) payload);
break;
case pen_wire::magic: {
auto *packet = reinterpret_cast<pen_wire::packet_t *>(payload);
passthrough(input, &packet->pen, pen_wire::decode_roll(packet->barrelRoll));
break;
}
case SS_CONTROLLER_ARRIVAL_MAGIC:
passthrough(input, (PSS_CONTROLLER_ARRIVAL_PACKET) payload);
break;
Expand Down Expand Up @@ -2148,6 +2172,41 @@ namespace input {
passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data) {
{
std::lock_guard<std::mutex> lg(input->input_queue_lock);
if (input->input_stopped) {
return;
}
if (input_data.size() >= sizeof(NV_INPUT_HEADER)) {
const auto *header = reinterpret_cast<const NV_INPUT_HEADER *>(input_data.data());
if (util::endian::little(header->magic) == pen_wire::magic &&
!pen_wire::valid_size(input_data.size(), header->size)) {
const auto now = std::chrono::steady_clock::now();
if (now >= input->next_invalid_pen_warning) {
input->next_invalid_pen_warning = now + std::chrono::seconds(5);
BOOST_LOG(warning) << "Dropping invalid barrel-roll pen packet [session_id=" << input->session_id
<< ", payload_bytes=" << input_data.size()
<< ", header.size=" << util::endian::big(header->size)
<< ", expected_payload_bytes=" << sizeof(pen_wire::packet_t)
<< ", expected_header.size=" << sizeof(pen_wire::packet_t) - sizeof(std::uint32_t)
<< "] (warnings limited to one per 5 seconds per session)";
}
return;
}
}
if (input_data.size() >= sizeof(SS_PEN_PACKET) &&
util::endian::little(reinterpret_cast<PNV_INPUT_HEADER>(input_data.data())->magic) == SS_PEN_MAGIC) {
input->pen_roll_probe.reset();
}
// Observe every received sample before hover batching can discard it.
// Keep the probe under the session queue lock and queue the resolved twist.
if (input_data.size() == sizeof(pen_wire::packet_t)) {
auto *packet = reinterpret_cast<pen_wire::packet_t *>(input_data.data());
if (util::endian::little(packet->pen.header.magic) == pen_wire::magic &&
pen_wire::valid_size(input_data.size(), packet->pen.header.size)) {
auto azimuth = pen_wire::decode_roll(packet->pen.rotation);
auto roll = input->pen_roll_probe.select(pen_wire::decode_roll(packet->barrelRoll), azimuth);
packet->barrelRoll = util::endian::little(roll);
}
}
input->input_queue.push_back(std::move(input_data));
}
task_pool.push(passthrough_next_message, input);
Expand Down Expand Up @@ -2179,11 +2238,26 @@ namespace input {

void
reset(std::shared_ptr<input_t> &input) {
{
std::lock_guard<std::mutex> lg(input->input_queue_lock);
input->input_stopped = true;
input->input_queue.clear();
input->pen_roll_probe.reset();
}
task_pool.cancel(key_press_repeat_id);
task_pool.cancel(input->mouse_left_button_timeout);

// Ensure input is synchronous, by using the task_pool
task_pool.push(reset_input_state);
task_pool.push([input]() {
#ifdef _WIN32
if (input->client_context) {
platf::pen_input_t cancel {};
cancel.eventType = LI_TOUCH_EVENT_CANCEL_ALL;
platf::pen_update(input->client_context.get(), {}, cancel);
}
#endif
reset_input_state();
});
}

class deinit_t: public platf::deinit_t {
Expand Down
6 changes: 6 additions & 0 deletions src/input_activity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "config.h"
#include "cursor_channel.h"
#include "input_activity.h"
#include "pen_barrel_roll.h"
#include "utility.h"

namespace input::activity {
Expand All @@ -21,6 +22,11 @@ namespace input::activity {
}

switch (util::endian::little(payload->magic)) {
case pen_wire::magic:
if (!pen_wire::valid_size(payload_size, payload->size)) {
return std::nullopt;
}
return false;
case MOUSE_MOVE_REL_MAGIC_GEN5: {
if (payload_size < sizeof(NV_REL_MOUSE_MOVE_PACKET)) {
return std::nullopt;
Expand Down
72 changes: 72 additions & 0 deletions src/pen_barrel_roll.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @file src/pen_barrel_roll.h
* @brief Wire layout for voidlink-c's independent pen barrel roll extension.
*/
#pragma once

#include <bit>
#include <cstddef>
#include <cstdint>
#include <moonlight-common-c/src/Input.h>

namespace input::pen_wire {
constexpr std::uint32_t magic = 0x5500000B;
static_assert(magic != SS_TOUCHPAD_MAGIC && magic != SS_TOUCHPAD_FRAME_MAGIC);
constexpr std::uint16_t unknown = 0xffff;

// Default to legacy azimuth until this session demonstrates real roll changes.
class roll_probe_t {
public:
void reset() {
first_ = unknown;
varied_ = false;
}

std::uint16_t select(std::uint16_t roll, std::uint16_t azimuth) {
if (roll == unknown) {
return azimuth;
}
if (first_ == unknown) {
first_ = roll;
}
if (roll != first_) {
varied_ = true;
}
return varied_ ? roll : azimuth;
}

private:
std::uint16_t first_ = unknown;
bool varied_ = false;
};

// Mirrors SS_PEN_BARREL_ROLL_PACKET in voidlink-c, retaining the legacy
// prefix and the two-byte trailing reserved field without changing it.
#pragma pack(push, 1)
struct packet_t {
SS_PEN_PACKET pen;
std::uint16_t barrelRoll;
std::uint16_t reserved;
};
#pragma pack(pop)

static_assert(sizeof(SS_PEN_PACKET) == 36);
static_assert(offsetof(packet_t, barrelRoll) == 36);
static_assert(sizeof(packet_t) == 40);

constexpr std::uint16_t
decode_roll(std::uint16_t value) {
if constexpr (std::endian::native == std::endian::big) {
value = std::byteswap(value);
}
return value == unknown ? unknown : value % 360;
}

constexpr bool
valid_size(std::size_t available, std::uint32_t network_size) {
if constexpr (std::endian::native == std::endian::little) {
network_size = std::byteswap(network_size);
}
return available == sizeof(packet_t) && network_size == sizeof(packet_t) - sizeof(std::uint32_t);
}
} // namespace input::pen_wire
1 change: 1 addition & 0 deletions src/platform/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ namespace platf {
float pressureOrDistance; // Distance for hover and pressure for contact
float contactAreaMajor;
float contactAreaMinor;
std::uint16_t barrelRoll = 0xffff; // Independent axial twist, or unknown
};

class deinit_t {
Expand Down
20 changes: 18 additions & 2 deletions src/platform/windows/input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1846,6 +1846,22 @@ namespace platf {

raw->penInfo.type = PT_PEN;

// A stopped session may retain its input context. Retire its virtual pen
// immediately so its last twist cannot keep being replayed after reconnect.
if (pen.eventType == LI_TOUCH_EVENT_CANCEL_ALL) {
auto &pointer = raw->penInfo.penInfo.pointerInfo;
if (pointer.pointerFlags != POINTER_FLAG_NONE) {
populate_common_pointer_info(pointer, {}, LI_TOUCH_EVENT_CANCEL_ALL, 0.0f, 0.0f);
if (!inject_synthetic_pointer_input(raw->global, raw->pen, &raw->penInfo, 1)) {
BOOST_LOG(warning) << "Failed to cancel virtual pen input: " << GetLastError();
}
}
raw->global->fnDestroySyntheticPointerDevice(raw->pen);
raw->pen = nullptr;
raw->penInfo = {};
return;
}

auto &penInfo = raw->penInfo.penInfo;
penInfo.pointerInfo.pointerType = PT_PEN;
penInfo.pointerInfo.pointerId = 0;
Expand Down Expand Up @@ -1888,9 +1904,9 @@ namespace platf {
penInfo.pressure = 0;
}

if (pen.rotation != LI_ROT_UNKNOWN) {
if (pen.barrelRoll != LI_ROT_UNKNOWN) {
penInfo.penMask |= PEN_MASK_ROTATION;
penInfo.rotation = pen.rotation;
penInfo.rotation = pen.barrelRoll % 360;
}
else {
penInfo.rotation = 0;
Expand Down
5 changes: 5 additions & 0 deletions src/rtsp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,11 @@ namespace rtsp_stream {
caps |= platf::platform_caps::remote_text_context;
}
ss << "a=x-ss-general.featureFlags:" << caps << std::endl;
#ifdef _WIN32
if (config::input.mouse && (caps & platf::platform_caps::pen_touch)) {
ss << "a=x-ss-general.penBarrelRoll:1" << std::endl;
}
#endif
}

// Always request new control stream encryption if the client supports it
Expand Down
6 changes: 6 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ set(INSTALL_GMOCK OFF)
add_subdirectory("${GTEST_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/googletest")
include_directories("${GTEST_SOURCE_DIR}/googletest/include" "${GTEST_SOURCE_DIR}")

add_executable(pen_barrel_roll_unit_tests ${CMAKE_SOURCE_DIR}/tests/unit/test_pen_barrel_roll.cpp)
target_include_directories(pen_barrel_roll_unit_tests PRIVATE "${CMAKE_SOURCE_DIR}/third-party")
target_link_libraries(pen_barrel_roll_unit_tests PRIVATE gtest_main)
set_target_properties(pen_barrel_roll_unit_tests PROPERTIES CXX_STANDARD 23)
add_test(NAME pen_barrel_roll_unit_tests COMMAND pen_barrel_roll_unit_tests)

# coverage
# https://gcovr.com/en/stable/guide/compiling.html#compiler-options
option(SUNSHINE_TESTS_ENABLE_COVERAGE "Enable gcov coverage instrumentation for tests" OFF)
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/test_input_activity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,22 @@
#include <src/config.h>
#include <src/cursor_channel.h>
#include <src/input_activity.h>
#include <src/pen_barrel_roll.h>
#include <src/utility.h>

TEST(PenBarrelRollActivity, ValidatesExtendedPacketsBeforeDispatch) {
input::activity::tracker_t tracker;
input::pen_wire::packet_t packet {};
packet.pen.header.magic = util::endian::little(input::pen_wire::magic);
packet.pen.header.size = util::endian::big(std::uint32_t {36});
auto result = tracker.evaluate(&packet.pen.header, sizeof(packet));
ASSERT_TRUE(result.has_value());
EXPECT_FALSE(*result);
EXPECT_FALSE(tracker.evaluate(&packet.pen.header, 36).has_value());
packet.pen.header.size = util::endian::big(std::uint32_t {32});
EXPECT_FALSE(tracker.evaluate(&packet.pen.header, sizeof(packet)).has_value());
}

#ifdef INPUT_ACTIVITY_STANDALONE_TEST
namespace config {
input_t input;
Expand Down
Loading