mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 11:31:08 +00:00
feat: the built-in pad's controller type switches from the session menu while Steam runs (#558)
This commit is contained in:
1 parent
542ad3e060
commit
6455b1dade
8 files changed
+304
-66
No files matched your search
@@ -805,17 +805,24 @@ __attribute__((visibility("hidden"))) static bool is_fake_input_fd(int fd) {
|
||||
return controller_map().find(fd) != controller_map().end();
|
||||
}
|
||||
|
||||
static bool fake_pad_withdrawn(const FakeController &fake);
|
||||
|
||||
// A node whose pad is gone: the app republished its ring, or the pad was switched to a type that
|
||||
// is not this node (it became the Deck or the Steam Controller again, fake_pad_withdrawn).
|
||||
static bool controller_gone(const FakeController &fake) {
|
||||
return ring_generation(fake.ring) != fake.generation || fake_pad_withdrawn(fake);
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static bool fake_fd_is_stale(int fd) {
|
||||
if (!watched(fd)) return false;
|
||||
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
|
||||
auto controller = controller_map().find(fd);
|
||||
return controller != controller_map().end() &&
|
||||
ring_generation(controller->second->ring) != controller->second->generation;
|
||||
return controller != controller_map().end() && controller_gone(*controller->second);
|
||||
}
|
||||
|
||||
// Caller holds controller_mutex().
|
||||
static bool fake_has_unread_data(const FakeController &fake) {
|
||||
if (ring_generation(fake.ring) != fake.generation)
|
||||
if (controller_gone(fake))
|
||||
return false;
|
||||
// Readiness never consumes resync requests or changes the read cursor.
|
||||
// Do not spin a nonblocking guest on a producer's unfinished publication.
|
||||
@@ -838,7 +845,7 @@ static bool fake_fd_has_unread_data(int fd) {
|
||||
static short fake_poll_revents(const std::shared_ptr<FakeController> &fake, short events) {
|
||||
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
|
||||
if (fake->closed) return POLLNVAL;
|
||||
if (ring_generation(fake->ring) != fake->generation) return POLLHUP;
|
||||
if (controller_gone(*fake)) return POLLHUP;
|
||||
short ready = events & (POLLOUT | POLLWRNORM);
|
||||
if ((events & (POLLIN | POLLRDNORM)) && fake_has_unread_data(*fake))
|
||||
ready |= events & (POLLIN | POLLRDNORM);
|
||||
@@ -1284,7 +1291,9 @@ struct DeckImu {
|
||||
int16_t pads[4]; // left X, Y, right X, Y: -32768..32767, Y up
|
||||
uint16_t pressure[2]; // left, right: 0..32767
|
||||
uint32_t controls; // DECK_EXTRA_* bits
|
||||
uint32_t pad_type; // PAD_TYPE_* | 0x80000000, written by the app; 0 until it does
|
||||
};
|
||||
static_assert(offsetof(DeckImu, pad_type) == 44, "DeckImu::pad_type matches FakeInputWriter");
|
||||
// DeckImu::controls (DeckControls.kt).
|
||||
static constexpr uint32_t DECK_EXTRA_L4 = 1, DECK_EXTRA_R4 = 2, DECK_EXTRA_L5 = 4,
|
||||
DECK_EXTRA_R5 = 8, DECK_EXTRA_LPAD_TOUCH = 16,
|
||||
@@ -1308,7 +1317,7 @@ static_assert(sizeof(DeckImu) <= DECK_IMU_BLOCK_SIZE, "the IMU block is 64 bytes
|
||||
// report characteristic and answers with get_reply set to the same number.
|
||||
struct TritonSharedData {
|
||||
uint32_t magic;
|
||||
uint32_t reserved;
|
||||
uint32_t connected; // written by the app: the paired controller is connected and streaming
|
||||
uint64_t input_seq;
|
||||
uint32_t report_id;
|
||||
uint32_t input_length;
|
||||
@@ -1326,6 +1335,7 @@ struct TritonSharedData {
|
||||
static constexpr uint32_t TRITON_SHARED_MAGIC = 0x4e525454;
|
||||
static constexpr size_t TRITON_SHARED_BLOCK_SIZE = 256;
|
||||
static constexpr size_t TRITON_SHARED_OFFSET = FAKE_INPUT_RING_SIZE + DECK_IMU_BLOCK_SIZE;
|
||||
static_assert(offsetof(TritonSharedData, connected) == 4, "Triton connected offset");
|
||||
static_assert(offsetof(TritonSharedData, input_seq) == 8, "Triton input sequence offset");
|
||||
static_assert(offsetof(TritonSharedData, report_id) == 16, "Triton report ID offset");
|
||||
static_assert(offsetof(TritonSharedData, input) == 24, "Triton input offset");
|
||||
@@ -1342,6 +1352,9 @@ static_assert(sizeof(TritonSharedData) <= TRITON_SHARED_BLOCK_SIZE, "Triton ring
|
||||
static constexpr const char *TRITON_HIDRAW_PATH = "/dev/hidraw18";
|
||||
static constexpr unsigned int TRITON_HIDRAW_MAJOR = 240;
|
||||
static constexpr unsigned int TRITON_HIDRAW_MINOR = 18;
|
||||
// The app's own pad as the wired Steam Controller: a node of its own, so a paired one keeps 18.
|
||||
static constexpr const char *TRITON_PAD_HIDRAW_PATH = "/dev/hidraw19";
|
||||
static constexpr unsigned int TRITON_PAD_HIDRAW_MINOR = 19;
|
||||
static constexpr int TRITON_REPORT_ID_BYTES = 1;
|
||||
static constexpr int TRITON_STATE_BYTES = 45;
|
||||
static constexpr int TRITON_REPORT_INTERVAL_US = 4000;
|
||||
@@ -1393,12 +1406,66 @@ static bool fake_deck_enabled() {
|
||||
return enabled == 1;
|
||||
}
|
||||
|
||||
// The app's own pad as the Steam Controller, in the Deck's place: hidraw18 then streams reports
|
||||
// built from ring slot 0 as the Deck's are, and answers its feature reports itself.
|
||||
static bool fake_triton_pad() {
|
||||
static int enabled = -1;
|
||||
if (enabled < 0) enabled = getenv("FAKE_EVDEV_TRITON_PAD") && atoi(getenv("FAKE_EVDEV_TRITON_PAD")) ? 1 : 0;
|
||||
return enabled == 1;
|
||||
// What the app's own pad is to the client, switchable while the session runs (the drawer's
|
||||
// Controller page): the Deck (/dev/hidraw16), the wired Steam Controller (/dev/hidraw19, built from
|
||||
// ring slot 0 as the Deck's reports are), or an Xbox 360 pad (its own evdev node, no HID device).
|
||||
// The app writes the choice into the slot's Deck block (DeckImu::pad_type); until it has,
|
||||
// FAKE_EVDEV_PAD_TYPE says what the session started with. Only the chosen device can be opened;
|
||||
// the others end their streams, and the client is told through udev (pad_type_thread).
|
||||
static constexpr uint32_t PAD_TYPE_DECK = 0, PAD_TYPE_STEAM = 1, PAD_TYPE_XBOX = 2;
|
||||
static constexpr uint32_t PAD_TYPE_SET = 0x80000000u;
|
||||
|
||||
__attribute__((visibility("hidden"))) static uint32_t session_pad_type() {
|
||||
static int type = -1;
|
||||
if (type < 0) {
|
||||
const char *env = getenv("FAKE_EVDEV_PAD_TYPE");
|
||||
type = env && !strcmp(env, "steamcontroller") ? PAD_TYPE_STEAM
|
||||
: env && !strcmp(env, "xbox360") ? PAD_TYPE_XBOX : PAD_TYPE_DECK;
|
||||
}
|
||||
return static_cast<uint32_t>(type);
|
||||
}
|
||||
|
||||
// Slot 0's Deck and Triton blocks, mapped once, read-only; null until the ring file has them.
|
||||
__attribute__((visibility("hidden"))) static const uint8_t *ring_blocks() {
|
||||
static std::atomic<const uint8_t *> block{nullptr};
|
||||
static std::atomic<int> tries{0};
|
||||
const uint8_t *mapped = block.load(std::memory_order_acquire);
|
||||
if (mapped || tries.load(std::memory_order_relaxed) > 200) return mapped;
|
||||
static std::mutex lock;
|
||||
std::lock_guard<std::mutex> guard(lock);
|
||||
if ((mapped = block.load(std::memory_order_acquire))) return mapped;
|
||||
tries.fetch_add(1, std::memory_order_relaxed);
|
||||
static auto my_open = reinterpret_cast<int (*)(const char *, int, ...)>(dlsym(RTLD_NEXT, "open"));
|
||||
std::string path = get_ring_path_for_slot(0);
|
||||
int fd = path.empty() ? -1 : my_open(path.c_str(), O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) return nullptr;
|
||||
struct stat st;
|
||||
void *mapping = MAP_FAILED;
|
||||
const size_t size = TRITON_SHARED_OFFSET + TRITON_SHARED_BLOCK_SIZE;
|
||||
if (fstat(fd, &st) == 0 && static_cast<size_t>(st.st_size) >= size)
|
||||
mapping = mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0);
|
||||
syscall(SYS_close, fd);
|
||||
if (mapping == MAP_FAILED) return nullptr;
|
||||
mapped = static_cast<const uint8_t *>(mapping);
|
||||
block.store(mapped, std::memory_order_release);
|
||||
return mapped;
|
||||
}
|
||||
|
||||
// The paired Bluetooth controller is offered only while the app has it connected: Steam asks a
|
||||
// device it is offered for its info, and one that never answers holds up every other controller.
|
||||
__attribute__((visibility("hidden"))) static bool triton_connected() {
|
||||
const uint8_t *blocks = ring_blocks();
|
||||
if (!blocks) return false;
|
||||
const auto *shared = reinterpret_cast<const TritonSharedData *>(blocks + TRITON_SHARED_OFFSET);
|
||||
return __atomic_load_n(&shared->magic, __ATOMIC_ACQUIRE) == TRITON_SHARED_MAGIC &&
|
||||
__atomic_load_n(&shared->connected, __ATOMIC_ACQUIRE) != 0;
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static uint32_t live_pad_type() {
|
||||
const uint8_t *blocks = ring_blocks();
|
||||
const DeckImu *imu = blocks ? reinterpret_cast<const DeckImu *>(blocks + FAKE_INPUT_RING_SIZE) : nullptr;
|
||||
uint32_t value = imu ? __atomic_load_n(&imu->pad_type, __ATOMIC_ACQUIRE) : 0;
|
||||
return value & PAD_TYPE_SET ? value & 0xff : session_pad_type();
|
||||
}
|
||||
|
||||
static bool fake_triton_enabled() {
|
||||
@@ -1435,17 +1502,24 @@ __attribute__((constructor)) static void log_client_start() {
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static bool is_deck_hidraw_path(const char *pathname) {
|
||||
return pathname && fake_deck_enabled() && !fake_triton_pad() && !strcmp(pathname, DECK_HIDRAW_PATH);
|
||||
return pathname && fake_deck_enabled() && !strcmp(pathname, DECK_HIDRAW_PATH) && live_pad_type() == PAD_TYPE_DECK;
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static bool is_triton_pad_path(const char *pathname) {
|
||||
return pathname && fake_deck_enabled() && !strcmp(pathname, TRITON_PAD_HIDRAW_PATH) &&
|
||||
live_pad_type() == PAD_TYPE_STEAM;
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static bool is_triton_hidraw_path(const char *pathname) {
|
||||
return pathname && fake_triton_enabled() && !strcmp(pathname, TRITON_HIDRAW_PATH);
|
||||
return pathname && fake_triton_enabled() && !strcmp(pathname, TRITON_HIDRAW_PATH) && triton_connected();
|
||||
}
|
||||
|
||||
// The app's first pad is the Deck now: its evdev node is withdrawn from everyone. Further players'
|
||||
// pads are left to the client alone, which hands them to games through Steam Input.
|
||||
__attribute__((visibility("hidden"))) static bool is_withdrawn_pad_path(const char *pathname) {
|
||||
if (!fake_deck_enabled() || !pathname || strncmp(pathname, "/dev/input/", 11)) return false;
|
||||
// As an Xbox 360 pad it is its own evdev node again, for the client and games alike.
|
||||
if (live_pad_type() == PAD_TYPE_XBOX) return false;
|
||||
const char *event = strrchr(pathname, '/') + 1;
|
||||
if (strncmp(event, "event", 5) && strncmp(event, "js", 2)) return false;
|
||||
int number = get_event_number(event);
|
||||
@@ -1453,6 +1527,13 @@ __attribute__((visibility("hidden"))) static bool is_withdrawn_pad_path(const ch
|
||||
return number == 0 || !process_is_steam_client();
|
||||
}
|
||||
|
||||
static bool fake_pad_withdrawn(const FakeController &fake) {
|
||||
if (fake.uinput || !fake.event) return false;
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "/dev/input/%s", fake.event);
|
||||
return is_withdrawn_pad_path(path);
|
||||
}
|
||||
|
||||
static inline void put16(uint8_t *at, int value) {
|
||||
at[0] = static_cast<uint8_t>(value & 0xff);
|
||||
at[1] = static_cast<uint8_t>((value >> 8) & 0xff);
|
||||
@@ -1524,7 +1605,8 @@ __attribute__((visibility("hidden"))) static void *deck_report_thread(void *arg)
|
||||
uint32_t buttons = 0;
|
||||
int32_t axes[8] = {};
|
||||
DeckExtras extras;
|
||||
for (;;) {
|
||||
// Switched to another type, the stream ends: the client's read fails and it lets the Deck go.
|
||||
while (live_pad_type() == PAD_TYPE_DECK) {
|
||||
if (read_snapshot(self->ring, snap) && snap.generation == ring_generation(self->ring)) {
|
||||
buttons = snap.buttons;
|
||||
memcpy(axes, snap.axes, sizeof(axes));
|
||||
@@ -1645,7 +1727,8 @@ __attribute__((visibility("hidden"))) static void *triton_report_thread(void *ar
|
||||
uint8_t report[TRITON_REPORT_ID_BYTES + 64] = {};
|
||||
uint8_t report_id = 0x45;
|
||||
size_t length = TRITON_STATE_BYTES;
|
||||
for (;;) {
|
||||
// A disconnect ends the stream, as unplugging the controller would.
|
||||
while (triton_connected()) {
|
||||
if (read_triton_state(*self, report_id, report + 1, length)) {
|
||||
report[0] = report_id;
|
||||
if (send(self->peer, report, length + 1, MSG_DONTWAIT | MSG_NOSIGNAL) < 0 &&
|
||||
@@ -1745,7 +1828,7 @@ __attribute__((visibility("hidden"))) static void *triton_pad_thread(void *arg)
|
||||
uint32_t buttons = 0;
|
||||
int32_t axes[8] = {};
|
||||
DeckExtras extras;
|
||||
for (;;) {
|
||||
while (live_pad_type() == PAD_TYPE_STEAM) {
|
||||
if (read_snapshot(self->ring, snap) && snap.generation == ring_generation(self->ring)) {
|
||||
buttons = snap.buttons;
|
||||
memcpy(axes, snap.axes, sizeof(axes));
|
||||
@@ -1765,7 +1848,7 @@ __attribute__((visibility("hidden"))) static void *triton_pad_thread(void *arg)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static int open_triton_hidraw(int flags) {
|
||||
__attribute__((visibility("hidden"))) static int open_triton_hidraw(int flags, bool emulated) {
|
||||
static auto my_open = reinterpret_cast<int (*)(const char *, int, ...)>(dlsym(RTLD_NEXT, "open"));
|
||||
if (!process_is_steam_client()) {
|
||||
errno = ENOENT;
|
||||
@@ -1808,7 +1891,7 @@ __attribute__((visibility("hidden"))) static int open_triton_hidraw(int flags) {
|
||||
triton->ring = static_cast<FakeInputRingHeader *>(mapping);
|
||||
triton->mapping_size = mapping_size;
|
||||
triton->triton = true;
|
||||
triton->emulated = fake_triton_pad();
|
||||
triton->emulated = emulated;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
|
||||
deck_map()[pair[0]] = triton;
|
||||
@@ -1828,8 +1911,8 @@ __attribute__((visibility("hidden"))) static int open_triton_hidraw(int flags) {
|
||||
return -1;
|
||||
}
|
||||
pthread_detach(thread);
|
||||
Logger::log("triton: %s opened as fd %d (%s)\n", TRITON_HIDRAW_PATH, pair[0],
|
||||
triton->emulated ? "28de:1302, the app's pad" : "28de:1303");
|
||||
Logger::log("triton: %s opened as fd %d (%s)\n", emulated ? TRITON_PAD_HIDRAW_PATH : TRITON_HIDRAW_PATH, pair[0],
|
||||
emulated ? "28de:1302, the app's pad" : "28de:1303");
|
||||
return pair[0];
|
||||
}
|
||||
|
||||
@@ -2122,13 +2205,86 @@ __attribute__((visibility("hidden"))) static int ioctl_touch(DeckHidraw &touch,
|
||||
|
||||
// Plugging in and out: the app flips `plugged`; this tells the client's HID discovery, which
|
||||
// otherwise never looks again (the sandbox gives it no kernel uevents).
|
||||
__attribute__((visibility("hidden"))) static void touch_uevent(const char *action) {
|
||||
__attribute__((visibility("hidden"))) static int inject_uevent(const char *action, const char *devpath,
|
||||
const char *subsystem, const char *devname,
|
||||
unsigned major, unsigned minor) {
|
||||
using inject_fn = int (*)(const char *, const char *, const char *, const char *, unsigned, unsigned);
|
||||
static auto inject = reinterpret_cast<inject_fn>(dlsym(RTLD_DEFAULT, "bl_udevmon_inject"));
|
||||
int sent = inject ? inject(action, TOUCH_DEVPATH, "hidraw", TOUCH_HIDRAW_PATH, DECK_HIDRAW_MAJOR, TOUCH_HIDRAW_MINOR) : -1;
|
||||
return inject ? inject(action, devpath, subsystem, devname, major, minor) : -1;
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static void touch_uevent(const char *action) {
|
||||
int sent = inject_uevent(action, TOUCH_DEVPATH, "hidraw", TOUCH_HIDRAW_PATH, DECK_HIDRAW_MAJOR, TOUCH_HIDRAW_MINOR);
|
||||
Logger::log("touch: %s (udev event to %d monitor%s)\n", action, sent, sent == 1 ? "" : "s");
|
||||
}
|
||||
|
||||
// The devices each pad type is, as SteamDeckPad.kt describes them and inputudev.c the evdev node.
|
||||
static constexpr const char *DECK_DEVPATH = "/devices/droiddeck/usb1/1-1:1.2/0003:28DE:1205.0001/hidraw/hidraw16";
|
||||
static constexpr const char *TRITON_PAD_DEVPATH = "/devices/droiddeck/usb3/3-1:1.0/0003:28DE:1302.0004/hidraw/hidraw19";
|
||||
static constexpr const char *PAD_EVDEV_DEVPATH = "/devices/virtual/input/droiddeck0/event0";
|
||||
static constexpr const char *TRITON_DEVPATH = "/devices/droiddeck/bluetooth/0005:28DE:1303.0003/hidraw/hidraw18";
|
||||
|
||||
__attribute__((visibility("hidden"))) static int pad_type_uevent(uint32_t type, const char *action) {
|
||||
if (type == PAD_TYPE_DECK)
|
||||
return inject_uevent(action, DECK_DEVPATH, "hidraw", DECK_HIDRAW_PATH, DECK_HIDRAW_MAJOR, DECK_HIDRAW_MINOR);
|
||||
if (type == PAD_TYPE_STEAM)
|
||||
return inject_uevent(action, TRITON_PAD_DEVPATH, "hidraw", TRITON_PAD_HIDRAW_PATH, TRITON_HIDRAW_MAJOR,
|
||||
TRITON_PAD_HIDRAW_MINOR);
|
||||
dev_t rdev = get_fake_input_rdev("event0");
|
||||
return inject_uevent(action, PAD_EVDEV_DEVPATH, "input", "/dev/input/event0", major(rdev), minor(rdev));
|
||||
}
|
||||
|
||||
// A switch, as the client sees one: the old device unplugged, then the new one plugged in.
|
||||
__attribute__((visibility("hidden"))) static void *pad_type_thread(void *) {
|
||||
static const char *kNames[] = {"Steam Deck controller", "Steam Controller", "Xbox 360 controller"};
|
||||
uint32_t was = live_pad_type();
|
||||
bool paired = fake_triton_enabled() && triton_connected();
|
||||
Logger::log("pad: a %s at start%s\n", kNames[was % 3], paired ? ", the paired Steam Controller connected" : "");
|
||||
for (;;) {
|
||||
struct timespec interval = {0, 50 * 1000 * 1000};
|
||||
nanosleep(&interval, nullptr);
|
||||
bool connected = fake_triton_enabled() && triton_connected();
|
||||
if (connected != paired) {
|
||||
paired = connected;
|
||||
int sent = inject_uevent(connected ? "add" : "remove", TRITON_DEVPATH, "hidraw", TRITON_HIDRAW_PATH,
|
||||
TRITON_HIDRAW_MAJOR, TRITON_HIDRAW_MINOR);
|
||||
Logger::log("pad: the paired Steam Controller %s (udev event to %d monitors)\n",
|
||||
connected ? "connected" : "disconnected", sent);
|
||||
}
|
||||
uint32_t now = live_pad_type();
|
||||
if (now == was || now > PAD_TYPE_XBOX) continue;
|
||||
int removed = pad_type_uevent(was, "remove");
|
||||
// Time for the client to let the old device go before the new one appears.
|
||||
struct timespec settle = {0, 300 * 1000 * 1000};
|
||||
nanosleep(&settle, nullptr);
|
||||
int added = pad_type_uevent(now, "add");
|
||||
// The pad's evdev node comes or goes with the Xbox 360 type. Its hotplug is watched with
|
||||
// inotify on /dev/input (SDL's, with udev off), so it is moved out and back in: everyone
|
||||
// watching sees it removed and added, and opens it again only if it is the pad now.
|
||||
if (was == PAD_TYPE_XBOX || now == PAD_TYPE_XBOX) {
|
||||
std::string node = std::string(fake_hook_dir()) + "/event0";
|
||||
std::string aside = std::string(fake_hook_dir()) + "/.event0-switching";
|
||||
if (rename(node.c_str(), aside.c_str()) == 0) {
|
||||
nanosleep(&settle, nullptr);
|
||||
if (rename(aside.c_str(), node.c_str()) != 0)
|
||||
Logger::log("pad: event0 not put back (%s)\n", strerror(errno));
|
||||
} else {
|
||||
Logger::log("pad: event0 not moved (%s)\n", strerror(errno));
|
||||
}
|
||||
}
|
||||
Logger::log("pad: now a %s (udev events to %d/%d monitors)\n", kNames[now], removed, added);
|
||||
was = now;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void start_pad_type_watch() {
|
||||
config();
|
||||
if (!process_is_steam_client() || !fake_deck_enabled()) return;
|
||||
pthread_t thread;
|
||||
if (pthread_create(&thread, nullptr, pad_type_thread, nullptr) == 0) pthread_detach(thread);
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static void *touch_plug_thread(void *) {
|
||||
bool was = touch_plugged();
|
||||
Logger::log("touch: %s at start\n", was ? "plugged in" : "unplugged");
|
||||
@@ -2727,7 +2883,8 @@ EXPORT int open(const char *pathname, int flags, ...) {
|
||||
|
||||
if (fake_uinput_enabled() && is_uinput_path(pathname)) return open_uinput(flags);
|
||||
if (is_deck_hidraw_path(pathname)) return open_deck_hidraw(flags);
|
||||
if (is_triton_hidraw_path(pathname)) return open_triton_hidraw(flags);
|
||||
if (is_triton_hidraw_path(pathname)) return open_triton_hidraw(flags, false);
|
||||
if (is_triton_pad_path(pathname)) return open_triton_hidraw(flags, true);
|
||||
if (is_touch_hidraw_path(pathname)) return open_touch_hidraw(flags);
|
||||
if (is_withdrawn_pad_path(pathname)) {
|
||||
errno = ENOENT;
|
||||
@@ -2803,7 +2960,8 @@ EXPORT int openat(int dirfd, const char *pathname, int flags, ...) {
|
||||
|
||||
if (fake_uinput_enabled() && is_uinput_path(pathname)) return open_uinput(flags);
|
||||
if (is_deck_hidraw_path(pathname)) return open_deck_hidraw(flags);
|
||||
if (is_triton_hidraw_path(pathname)) return open_triton_hidraw(flags);
|
||||
if (is_triton_hidraw_path(pathname)) return open_triton_hidraw(flags, false);
|
||||
if (is_triton_pad_path(pathname)) return open_triton_hidraw(flags, true);
|
||||
if (is_touch_hidraw_path(pathname)) return open_touch_hidraw(flags);
|
||||
if (is_withdrawn_pad_path(pathname)) {
|
||||
errno = ENOENT;
|
||||
@@ -2862,7 +3020,8 @@ EXPORT int openat(int dirfd, const char *pathname, int flags, ...) {
|
||||
template <typename S, typename Real>
|
||||
static int fake_stat_path(const char *pathname, S *statbuf, Real real) {
|
||||
if (is_withdrawn_pad_path(pathname) ||
|
||||
((is_deck_hidraw_path(pathname) || is_triton_hidraw_path(pathname)) && !process_is_steam_client())) {
|
||||
((is_deck_hidraw_path(pathname) || is_triton_hidraw_path(pathname) || is_triton_pad_path(pathname)) &&
|
||||
!process_is_steam_client())) {
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
}
|
||||
@@ -2878,10 +3037,10 @@ static int fake_stat_path(const char *pathname, S *statbuf, Real real) {
|
||||
statbuf->st_rdev = makedev(DECK_HIDRAW_MAJOR, DECK_HIDRAW_MINOR);
|
||||
return 0;
|
||||
}
|
||||
if (is_triton_hidraw_path(pathname)) {
|
||||
if (is_triton_hidraw_path(pathname) || is_triton_pad_path(pathname)) {
|
||||
memset(statbuf, 0, sizeof(*statbuf));
|
||||
statbuf->st_mode = S_IFCHR | 0666;
|
||||
statbuf->st_rdev = makedev(TRITON_HIDRAW_MAJOR, TRITON_HIDRAW_MINOR);
|
||||
statbuf->st_rdev = makedev(TRITON_HIDRAW_MAJOR, is_triton_pad_path(pathname) ? TRITON_PAD_HIDRAW_MINOR : TRITON_HIDRAW_MINOR);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2931,7 +3090,9 @@ static int fake_stat_fd(int fd, S *buf, Real real) {
|
||||
if (ret == 0 && deck_map().count(fd)) {
|
||||
buf->st_mode = (buf->st_mode & ~S_IFMT) | S_IFCHR;
|
||||
const auto &device = *deck_map()[fd];
|
||||
unsigned int minor = device.touch ? TOUCH_HIDRAW_MINOR : (device.triton ? TRITON_HIDRAW_MINOR : DECK_HIDRAW_MINOR);
|
||||
unsigned int minor = device.touch ? TOUCH_HIDRAW_MINOR
|
||||
: device.emulated ? TRITON_PAD_HIDRAW_MINOR
|
||||
: device.triton ? TRITON_HIDRAW_MINOR : DECK_HIDRAW_MINOR;
|
||||
buf->st_rdev = makedev(DECK_HIDRAW_MAJOR, minor);
|
||||
return ret;
|
||||
}
|
||||
@@ -2978,9 +3139,10 @@ EXPORT int access(const char *pathname, int mode) {
|
||||
static auto my_access = reinterpret_cast<decltype(&::access)>(dlsym(RTLD_NEXT, "access"));
|
||||
if (fake_uinput_enabled() && is_uinput_path(pathname)) return 0;
|
||||
if (is_deck_hidraw_path(pathname) && process_is_steam_client()) return 0;
|
||||
if (is_triton_hidraw_path(pathname) && process_is_steam_client()) return 0;
|
||||
if ((is_triton_hidraw_path(pathname) || is_triton_pad_path(pathname)) && process_is_steam_client()) return 0;
|
||||
if (is_touch_hidraw_path(pathname)) return 0;
|
||||
if (is_withdrawn_pad_path(pathname) || is_deck_hidraw_path(pathname) || is_triton_hidraw_path(pathname)) {
|
||||
if (is_withdrawn_pad_path(pathname) || is_deck_hidraw_path(pathname) || is_triton_hidraw_path(pathname) ||
|
||||
is_triton_pad_path(pathname)) {
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
}
|
||||
@@ -3016,9 +3178,10 @@ EXPORT int faccessat(int dirfd, const char *pathname, int mode, int flags) {
|
||||
static auto my_faccessat = reinterpret_cast<decltype(&::faccessat)>(dlsym(RTLD_NEXT, "faccessat"));
|
||||
if (fake_uinput_enabled() && is_uinput_path(pathname)) return 0;
|
||||
if (is_deck_hidraw_path(pathname) && process_is_steam_client()) return 0;
|
||||
if (is_triton_hidraw_path(pathname) && process_is_steam_client()) return 0;
|
||||
if ((is_triton_hidraw_path(pathname) || is_triton_pad_path(pathname)) && process_is_steam_client()) return 0;
|
||||
if (is_touch_hidraw_path(pathname)) return 0;
|
||||
if (is_withdrawn_pad_path(pathname) || is_deck_hidraw_path(pathname) || is_triton_hidraw_path(pathname)) {
|
||||
if (is_withdrawn_pad_path(pathname) || is_deck_hidraw_path(pathname) || is_triton_hidraw_path(pathname) ||
|
||||
is_triton_pad_path(pathname)) {
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
}
|
||||
@@ -3105,7 +3268,7 @@ static bool wait_snapshot(std::shared_ptr<FakeController> fake,
|
||||
errno = EBADF;
|
||||
return false;
|
||||
}
|
||||
if (ring_generation(fake->ring) != fake->generation) {
|
||||
if (controller_gone(*fake)) {
|
||||
errno = ENODEV;
|
||||
return false;
|
||||
}
|
||||
@@ -3399,7 +3562,7 @@ EXPORT ssize_t read(int fd, void *buf, size_t count) {
|
||||
errno = EBADF;
|
||||
return -1;
|
||||
}
|
||||
if (ring_generation(fake.ring) != fake.generation) {
|
||||
if (controller_gone(fake)) {
|
||||
errno = ENODEV;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -250,6 +250,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var drawerPage by mutableStateOf(SessionDrawerPage.CONTROLLER)
|
||||
private var drawerControllerActive by mutableStateOf(false)
|
||||
private var backActionsInverted by mutableStateOf(false)
|
||||
/** What the pad is to the Steam client (SessionPrefs.CONTROLLER_*), switched from the drawer. */
|
||||
private var padType by mutableStateOf(SessionPrefs.CONTROLLER_DECK)
|
||||
/** The on-screen PC keyboard (ui/PcKeyboard): real key presses, Esc and F1 included. */
|
||||
private var pcKeyboardOpen by mutableStateOf(false)
|
||||
private var hudOn by mutableStateOf(true)
|
||||
@@ -395,9 +397,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
SessionState.padBridge = bridge
|
||||
// A paired Steam Controller is read over Bluetooth LE, out of lizard mode, as Steam Link reads it.
|
||||
SteamControllerBle.listener = { uiHandler.post { updatePadMotion(); updateOnScreenControls() } }
|
||||
// Not when the pad itself is the Steam Controller: that device is then the pad's.
|
||||
if (SessionPrefs.steamController(this) != SessionPrefs.CONTROLLER_STEAM)
|
||||
steamController = SteamControllerBle(this).also { it.start() }
|
||||
steamController = SteamControllerBle(this).also { it.start() }
|
||||
padMotion = com.droiddeck.launcher.input.PadMotion(this) {
|
||||
@Suppress("DEPRECATION")
|
||||
(if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay)?.rotation ?: android.view.Surface.ROTATION_0
|
||||
@@ -589,6 +589,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
onKeyboardButton = { on -> updateControllerPrefs { ControllerPrefs.setKeyboardButton(this@SessionActivity, on) } },
|
||||
onSteamMenu = if (SessionState.mode == SessionService.MODE_STEAM) ({ sendSteamGuide() }) else null,
|
||||
onQam = if (SessionState.mode == SessionService.MODE_STEAM) ({ triggerSteamQam() }) else null,
|
||||
// Only where the session set the pad up to be switched (SessionService: deckPad).
|
||||
padType = if (SessionState.mode == SessionService.MODE_STEAM && SessionState.deckPad) padType else null,
|
||||
onPadType = { id ->
|
||||
SessionPrefs.setSteamController(this@SessionActivity, id)
|
||||
com.droiddeck.launcher.input.FakeInputWriter.setPadType(SessionPrefs.padType(id))
|
||||
padType = id
|
||||
},
|
||||
backActionsInverted = backActionsInverted,
|
||||
onBackActionsInverted = { inverted ->
|
||||
SessionPrefs.setBackActionsInverted(this@SessionActivity, inverted)
|
||||
@@ -849,6 +856,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
steamDownloadsInBackground = SessionPrefs.steamDownloadsInBackground(this)
|
||||
oscMode = SessionPrefs.oscMode(this)
|
||||
backActionsInverted = SessionPrefs.backActionsInverted(this)
|
||||
padType = SessionPrefs.steamController(this)
|
||||
}
|
||||
|
||||
// ── Compositor ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -107,8 +107,20 @@ public class FakeInputWriter {
|
||||
private static final int DECK_PADS_OFFSET = IMU_OFFSET + 28; // short[4]: left X, Y, right X, Y
|
||||
private static final int DECK_PRESSURE_OFFSET = IMU_OFFSET + 36; // short[2]: left, right
|
||||
private static final int DECK_CONTROLS_OFFSET = IMU_OFFSET + 40; // int: DeckControls bits
|
||||
// Slot 0 only: what the pad is to the Steam client (libfakeinput's PAD_TYPE_*), | PAD_TYPE_SET.
|
||||
private static final int PAD_TYPE_OFFSET = IMU_OFFSET + 44;
|
||||
private static final int PAD_TYPE_SET = 0x80000000;
|
||||
public static final int PAD_TYPE_DECK = 0;
|
||||
public static final int PAD_TYPE_STEAM = 1;
|
||||
public static final int PAD_TYPE_XBOX = 2;
|
||||
/** The type chosen this process, written into a ring made after the choice too; -1 for none. */
|
||||
private static volatile int padType = -1;
|
||||
/** The paired controller's state, likewise written into a ring made after it connected. */
|
||||
private static volatile boolean tritonConnected = false;
|
||||
private static final int TRITON_OFFSET = RING_SIZE + 64;
|
||||
private static final int TRITON_MAGIC = 0x4e525454; // TTRN
|
||||
// Whether the paired controller is connected: libfakeinput offers its device only then.
|
||||
private static final int TRITON_CONNECTED_OFFSET = TRITON_OFFSET + 4;
|
||||
private static final int TRITON_INPUT_SEQ_OFFSET = TRITON_OFFSET + 8;
|
||||
private static final int TRITON_REPORT_ID_OFFSET = TRITON_OFFSET + 16;
|
||||
private static final int TRITON_REPORT_LENGTH_OFFSET = TRITON_OFFSET + 20;
|
||||
@@ -241,7 +253,10 @@ public class FakeInputWriter {
|
||||
data.putShort(IMU_ACCEL_OFFSET + (i * 2), (short) 0);
|
||||
}
|
||||
data.putInt(DECK_CONTROLS_OFFSET, 0);
|
||||
int type = padType;
|
||||
data.putInt(PAD_TYPE_OFFSET, type >= 0 ? PAD_TYPE_SET | type : 0);
|
||||
data.putInt(TRITON_OFFSET, TRITON_MAGIC);
|
||||
data.putInt(TRITON_CONNECTED_OFFSET, tritonConnected ? 1 : 0);
|
||||
data.putLong(TRITON_INPUT_SEQ_OFFSET, 0L);
|
||||
data.putInt(TRITON_REPORT_ID_OFFSET, 0);
|
||||
data.putInt(TRITON_REPORT_LENGTH_OFFSET, 0);
|
||||
@@ -549,6 +564,38 @@ public class FakeInputWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches what the pad is to the Steam client while the session runs ([PAD_TYPE_DECK],
|
||||
* [PAD_TYPE_STEAM] or [PAD_TYPE_XBOX]); libfakeinput unplugs the old device and plugs in the new.
|
||||
*/
|
||||
public static void setPadType(int type) {
|
||||
padType = type;
|
||||
RingSlot ringSlot;
|
||||
synchronized (RING_LOCK) {
|
||||
ringSlot = RING_SLOTS[0];
|
||||
}
|
||||
if (ringSlot == null) return;
|
||||
synchronized (ringSlot) {
|
||||
ByteBuffer ring = ringSlot.data;
|
||||
if (ring == null) return;
|
||||
nativeStoreFence();
|
||||
ring.putInt(PAD_TYPE_OFFSET, PAD_TYPE_SET | type);
|
||||
}
|
||||
}
|
||||
|
||||
/** The paired Steam Controller came or went (SteamControllerBle): its device is plugged in or out. */
|
||||
public static void setTritonConnected(int slot, boolean connected) {
|
||||
tritonConnected = connected;
|
||||
RingSlot ringSlot = tritonSlot(slot);
|
||||
if (ringSlot == null) return;
|
||||
synchronized (ringSlot) {
|
||||
ByteBuffer ring = ringSlot.data;
|
||||
if (ring == null || ring.getInt(TRITON_OFFSET) != TRITON_MAGIC) return;
|
||||
nativeStoreFence();
|
||||
ring.putInt(TRITON_CONNECTED_OFFSET, connected ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
/** Publishes a native Triton input report (without the HID report ID byte). */
|
||||
public static void writeTritonState(int slot, int reportId, byte[] report) {
|
||||
if (report == null || report.length == 0 || report.length > TRITON_REPORT_MAX) return;
|
||||
|
||||
@@ -92,6 +92,7 @@ class SteamControllerBle(private val context: Context) {
|
||||
queuedWrites.clear()
|
||||
outputReports.clear()
|
||||
connected = false
|
||||
FakeInputWriter.setTritonConnected(SLOT, false)
|
||||
g.disconnect()
|
||||
g.close()
|
||||
release()
|
||||
@@ -129,6 +130,7 @@ class SteamControllerBle(private val context: Context) {
|
||||
if (g !== gatt || descriptor.characteristic.uuid != input) return
|
||||
Log.i(TAG, "subscribed to report 0x%02x (status %d)".format(reportId, status))
|
||||
connected = status == BluetoothGatt.GATT_SUCCESS
|
||||
FakeInputWriter.setTritonConnected(SLOT, connected)
|
||||
listener?.invoke()
|
||||
if (connected) {
|
||||
refreshLizardMode()
|
||||
|
||||
@@ -457,6 +457,12 @@ object SessionPrefs {
|
||||
fun steamController(context: Context): String =
|
||||
prefs(context).getString("steamController", CONTROLLER_DECK) ?: CONTROLLER_DECK
|
||||
fun setSteamController(context: Context, id: String) { prefs(context).edit().putString("steamController", id).apply() }
|
||||
/** [steamController]'s choice as libfakeinput's pad type (FakeInputWriter.PAD_TYPE_*). */
|
||||
fun padType(id: String): Int = when (id) {
|
||||
CONTROLLER_STEAM -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_STEAM
|
||||
CONTROLLER_XBOX360 -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_XBOX
|
||||
else -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DECK
|
||||
}
|
||||
|
||||
/**
|
||||
* Deck mode's performance overlay (mangoapp, beside gamescope): the QAM's Overlay Level draws
|
||||
|
||||
@@ -742,20 +742,17 @@ class SessionService : Service() {
|
||||
// hidraw (SteamDeckPad), which only works with Steam Input's virtual pad for games to read.
|
||||
// Decided only once its sysfs is in place: without it the client would find no Deck, and
|
||||
// with the Deck asked for the pad's own nodes are withdrawn - no controller at all.
|
||||
// Set up whatever the pad is to be, so the drawer can switch it between a Deck, a Steam
|
||||
// Controller and an Xbox 360 pad while the session runs (FakeInputWriter.setPadType).
|
||||
val wantsDeck = uinput && SessionState.mode == MODE_STEAM &&
|
||||
SessionState.steamUi != "desktop" &&
|
||||
SessionPrefs.steamController(this) != SessionPrefs.CONTROLLER_XBOX360 &&
|
||||
!File(Environment.getExternalStorageDirectory(), NO_DECK_PAD_SWITCH).exists()
|
||||
// The pad itself as a Steam Controller: the same machinery as the Deck (sysfs, withdrawn
|
||||
// evdev nodes, Steam Input's virtual pad for games), with the Triton node in the Deck's place.
|
||||
val padAsTriton = wantsDeck && SessionPrefs.steamController(this) == SessionPrefs.CONTROLLER_STEAM
|
||||
// Steam's touch controller rides on the Deck's sysfs and is offered only beside it.
|
||||
val wantsTouch = wantsDeck && !File(Environment.getExternalStorageDirectory(), NO_STEAM_TOUCH_SWITCH).exists()
|
||||
// A paired Triton is exposed as a second native Steam Controller HID, with its original
|
||||
// report IDs and output path. The handheld's own pad remains the Deck target.
|
||||
// With the pad as the Steam Controller, that node is the pad's and a paired one is not bridged.
|
||||
val wantsTriton = padAsTriton || (wantsDeck && SteamControllerBle.hasPairedController(this))
|
||||
deckBinds = if (wantsDeck) SteamDeckPad.prepare(this, fakeInputDir.parentFile!!.parentFile!!, wantsTouch, wantsTriton, deck = !padAsTriton) else emptyList()
|
||||
// report IDs and output path, beside whatever the handheld's own pad is.
|
||||
val wantsTriton = wantsDeck && SteamControllerBle.hasPairedController(this)
|
||||
deckBinds = if (wantsDeck) SteamDeckPad.prepare(this, fakeInputDir.parentFile!!.parentFile!!, wantsTouch, wantsTriton) else emptyList()
|
||||
SessionState.deckPad = deckBinds.isNotEmpty()
|
||||
if (wantsDeck && !SessionState.deckPad) Log.w(TAG, "deck pad: not available this session; the pad stays an Xbox 360 controller")
|
||||
logControllersAtStart()
|
||||
@@ -766,7 +763,10 @@ class SessionService : Service() {
|
||||
if (SessionState.deckPad) {
|
||||
guest.add("FAKE_EVDEV_DECK=1")
|
||||
if (wantsTriton) guest.add("FAKE_EVDEV_TRITON=1")
|
||||
if (padAsTriton) guest.add("FAKE_EVDEV_TRITON_PAD=1")
|
||||
// What the pad starts as; the drawer's changes reach libfakeinput through the ring.
|
||||
val padType = SessionPrefs.steamController(this)
|
||||
guest.add("FAKE_EVDEV_PAD_TYPE=$padType")
|
||||
com.droiddeck.launcher.input.FakeInputWriter.setPadType(SessionPrefs.padType(padType))
|
||||
// Steam's touch controller (SteamTouchDevice): the file the app and libfakeinput share.
|
||||
if (wantsTouch) com.droiddeck.launcher.input.SteamTouchDevice.prepare(fakeInputDir)?.let {
|
||||
guest.add("FAKE_TOUCHCTL_RING=" + it.path)
|
||||
@@ -1781,7 +1781,7 @@ class SessionService : Service() {
|
||||
pads.add(String.format(java.util.Locale.ROOT, "\"%s\" (%04x:%04x)", d.name, d.vendorId, d.productId))
|
||||
}
|
||||
Log.i(TAG, "controllers at start: " + (if (pads.isEmpty()) "none" else pads.joinToString(", ")) +
|
||||
"; presented to the guest as " + if (SessionState.deckPad) "a Steam Deck controller" else "an Xbox 360 controller")
|
||||
"; presented to the guest as " + if (SessionState.deckPad) "switchable, starting as " + SessionPrefs.steamController(this) else "an Xbox 360 controller")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -47,14 +47,15 @@ object SteamDeckPad {
|
||||
// The paired Bluetooth Steam Controller is a separate Triton HID device. It must not inherit
|
||||
// the Deck's identity or report format. Must match fakeinput_steam.cpp (TRITON_HIDRAW_*).
|
||||
private const val TRITON_MINOR = 18
|
||||
private const val TRITON_NODE = "hidraw$TRITON_MINOR"
|
||||
private const val TRITON_HID = "$GUEST_DEVICES/bluetooth/0005:28DE:1303.0003"
|
||||
private const val TRITON_SERIAL = "DROIDDECKSC01"
|
||||
// The handheld's own pad as the Steam Controller (SessionPrefs.CONTROLLER_STEAM): the same node,
|
||||
// as the wired controller (28de:1302) on a USB device of its own, since it is built in.
|
||||
// The handheld's own pad as the Steam Controller (SessionPrefs.CONTROLLER_STEAM): the wired
|
||||
// controller (28de:1302) on a USB device of its own, since it is built in.
|
||||
private const val TRITON_USB = "$GUEST_DEVICES/usb3"
|
||||
private const val TRITON_INTERFACE = "$TRITON_USB/3-1:1.0"
|
||||
private const val TRITON_WIRED_HID = "$TRITON_INTERFACE/0003:28DE:1302.0004"
|
||||
/** Must match libfakeinput (TRITON_PAD_HIDRAW_*): its own node, so a paired controller keeps 18. */
|
||||
private const val TRITON_PAD_MINOR = 19
|
||||
|
||||
// Steam's touch controller (SteamTouchDevice), a second device beside the Deck's: 0000:11fb,
|
||||
// "Mobile Touch Control", on a USB device of its own. Must match libfakeinput (TOUCH_*).
|
||||
@@ -104,7 +105,7 @@ object SteamDeckPad {
|
||||
fun listingDir(sessionRoot: File) = File(sessionRoot, "sys/deck/listing")
|
||||
|
||||
/** Writes the tree and returns the `host:guest` binds for it, or none if it could not be made. */
|
||||
fun prepare(context: Context, sessionRoot: File, touch: Boolean = false, triton: Boolean = false, deck: Boolean = true): List<String> {
|
||||
fun prepare(context: Context, sessionRoot: File, touch: Boolean = false, triton: Boolean = false): List<String> {
|
||||
val base = File(sessionRoot, "sys/deck")
|
||||
val devices = File(base, "devices")
|
||||
val hidrawClass = File(base, "class-hidraw")
|
||||
@@ -144,8 +145,7 @@ object SteamDeckPad {
|
||||
link(File(dir(HIDRAW), "device"), HID)
|
||||
|
||||
hidrawClass.mkdirs()
|
||||
// Without the Deck (the pad is the Steam Controller), its device is never listed.
|
||||
if (deck) link(File(hidrawClass, NODE), HIDRAW)
|
||||
link(File(hidrawClass, NODE), HIDRAW)
|
||||
|
||||
// Only the names matter: libudev reads a listing for the subsystems to descend into and
|
||||
// then opens /sys/class/hidraw itself, which is the bind above.
|
||||
@@ -155,9 +155,7 @@ object SteamDeckPad {
|
||||
link(File(context.cacheDir, "drm/sys/$MAJOR:$MINOR"), HIDRAW)
|
||||
|
||||
val udevData = File(LinuxRuntime.rootDir(context), "run/udev/data").apply { mkdirs() }
|
||||
File(udevData, "c$MAJOR:$MINOR").apply {
|
||||
if (deck) writeText("I:1\nE:ID_INPUT=1\nE:ID_INPUT_JOYSTICK=1\n") else delete()
|
||||
}
|
||||
File(udevData, "c$MAJOR:$MINOR").writeText("I:1\nE:ID_INPUT=1\nE:ID_INPUT_JOYSTICK=1\n")
|
||||
|
||||
if (touch) {
|
||||
write(TOUCH_USB, "uevent", "DEVTYPE=usb_device\nPRODUCT=0/11fb/0\nTYPE=0/0/0\nBUSNUM=002\nDEVNUM=002\n")
|
||||
@@ -187,12 +185,14 @@ object SteamDeckPad {
|
||||
Log.i(TAG, "deck pad: /dev/$TOUCH_NODE described as Steam's touch controller (0000:11fb)")
|
||||
}
|
||||
|
||||
if (triton) {
|
||||
// Without the Deck the node is the pad's own, so the wired controller; else the paired one.
|
||||
val wired = !deck
|
||||
// A Steam Controller's HID device: the handheld's own pad as the wired one, or the paired
|
||||
// one on Bluetooth. Both are always described; libfakeinput offers whichever is in use.
|
||||
fun describeTriton(wired: Boolean) {
|
||||
val hid = if (wired) TRITON_WIRED_HID else TRITON_HID
|
||||
val product = if (wired) "1302" else "1303"
|
||||
val bus = if (wired) "0003" else "0005"
|
||||
val minor = if (wired) TRITON_PAD_MINOR else TRITON_MINOR
|
||||
val node = "hidraw$minor"
|
||||
if (wired) {
|
||||
write(TRITON_USB, "uevent", "DEVTYPE=usb_device\nPRODUCT=28de/1302/100\nTYPE=0/0/0\nBUSNUM=003\nDEVNUM=002\n")
|
||||
write(TRITON_USB, "idVendor", "28de\n")
|
||||
@@ -213,16 +213,18 @@ object SteamDeckPad {
|
||||
"HID_UNIQ=$TRITON_SERIAL\nMODALIAS=hid:b${bus}g0001v000028DEp0000$product\n")
|
||||
File(dir(hid), "report_descriptor").writeBytes(if (wired) TRITON_WIRED_REPORT_DESCRIPTOR else TRITON_REPORT_DESCRIPTOR)
|
||||
link(File(dir(hid), "subsystem"), "/sys/bus/hid")
|
||||
val hidraw = "$hid/hidraw/$TRITON_NODE"
|
||||
write(hidraw, "uevent", "MAJOR=$MAJOR\nMINOR=$TRITON_MINOR\nDEVNAME=$TRITON_NODE\n")
|
||||
write(hidraw, "dev", "$MAJOR:$TRITON_MINOR\n")
|
||||
val hidraw = "$hid/hidraw/$node"
|
||||
write(hidraw, "uevent", "MAJOR=$MAJOR\nMINOR=$minor\nDEVNAME=$node\n")
|
||||
write(hidraw, "dev", "$MAJOR:$minor\n")
|
||||
link(File(dir(hidraw), "subsystem"), "/sys/class/hidraw")
|
||||
link(File(dir(hidraw), "device"), hid)
|
||||
link(File(hidrawClass, TRITON_NODE), hidraw)
|
||||
link(File(context.cacheDir, "drm/sys/$MAJOR:$TRITON_MINOR"), hidraw)
|
||||
File(udevData, "c$MAJOR:$TRITON_MINOR").writeText("I:1\nE:ID_INPUT=1\nE:ID_INPUT_JOYSTICK=1\n")
|
||||
Log.i(TAG, "steam controller: /dev/$TRITON_NODE described as Triton (28de:$product)")
|
||||
link(File(hidrawClass, node), hidraw)
|
||||
link(File(context.cacheDir, "drm/sys/$MAJOR:$minor"), hidraw)
|
||||
File(udevData, "c$MAJOR:$minor").writeText("I:1\nE:ID_INPUT=1\nE:ID_INPUT_JOYSTICK=1\n")
|
||||
Log.i(TAG, "steam controller: /dev/$node described as Triton (28de:$product)")
|
||||
}
|
||||
describeTriton(wired = true)
|
||||
if (triton) describeTriton(wired = false)
|
||||
|
||||
Log.i(TAG, "deck pad: /dev/$NODE described as a Steam Deck controller (28de:1205)")
|
||||
listOf(devices.path + ":" + GUEST_DEVICES, hidrawClass.path + ":/sys/class/hidraw")
|
||||
|
||||
@@ -194,6 +194,9 @@ class DrawerActions(
|
||||
val suspendPolicy: String,
|
||||
val steamDownloadsInBackground: Boolean = false,
|
||||
val backActionsInverted: Boolean,
|
||||
/** What the pad is to Steam (SessionPrefs.CONTROLLER_*), switched live; null hides the row. */
|
||||
val padType: String? = null,
|
||||
val onPadType: (String) -> Unit = {},
|
||||
val touchMode: String,
|
||||
val touchAuto: String,
|
||||
val fexPreset: String,
|
||||
@@ -419,6 +422,13 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont
|
||||
}
|
||||
SessionDrawerPage.CONTROLLER -> {
|
||||
SettingsGroup(stringResource(R.string.drawer_page_controller)) {
|
||||
a.padType?.let { type ->
|
||||
ChoiceRow(host, "pad-type", stringResource(R.string.mode_controller), null,
|
||||
listOf(SessionPrefs.CONTROLLER_DECK to stringResource(R.string.mode_controller_deck),
|
||||
SessionPrefs.CONTROLLER_STEAM to stringResource(R.string.mode_controller_steam),
|
||||
SessionPrefs.CONTROLLER_XBOX360 to stringResource(R.string.mode_controller_x360)),
|
||||
type, chipModifier = focus.track(page, "pad-type"), onPick = a.onPadType)
|
||||
}
|
||||
ChoiceRow(host, "touch", stringResource(R.string.mode_touch), null,
|
||||
listOf(SessionPrefs.TOUCH_AUTO to stringResource(R.string.drawer_touch_auto, a.touchAuto), SessionPrefs.TOUCH_PAD to stringResource(R.string.mode_touch_touchpad), SessionPrefs.TOUCH_DIRECT to stringResource(R.string.mode_touch_direct), SessionPrefs.TOUCH_OFF to stringResource(R.string.widgets_off)),
|
||||
a.touchMode, chipModifier = focus.track(page, "touch"), onPick = a.onTouch)
|
||||
|
||||
Reference in new issue
Block a user