feat: low-latency controller and touch toggles in the session drawer

This commit is contained in:
xXJsonDeruloXx committed 2026-10-14 07:58:55 -04:00
1 parent 4b17bc9a23
commit ceb0bee79a
7 files changed
+121 -22

No files matched your search

+44 -20
View File
@@ -1140,8 +1140,12 @@ struct DeckImu {
uint16_t pressure[2];
uint32_t controls;
uint32_t pad_type;
uint32_t latency;
};
static_assert(offsetof(DeckImu, pad_type) == 44, "DeckImu::pad_type matches FakeInputWriter");
static_assert(offsetof(DeckImu, latency) == 48, "DeckImu::latency matches FakeInputWriter");
static constexpr uint32_t LATENCY_CONTROLLER = 1, LATENCY_TOUCH = 2;
static constexpr int LOW_LATENCY_REPORT_INTERVAL_US = 1000;
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,
DECK_EXTRA_RPAD_TOUCH = 32, DECK_EXTRA_LPAD_CLICK = 64,
@@ -1303,6 +1307,27 @@ __attribute__((visibility("hidden"))) static uint32_t live_pad_type() {
return value & PAD_TYPE_SET ? value & 0xff : session_pad_type();
}
__attribute__((visibility("hidden"))) static bool low_latency(uint32_t which) {
const uint8_t *blocks = ring_blocks();
const DeckImu *imu = blocks ? reinterpret_cast<const DeckImu *>(blocks + FAKE_INPUT_RING_SIZE) : nullptr;
return imu && (__atomic_load_n(&imu->latency, __ATOMIC_ACQUIRE) & which);
}
__attribute__((visibility("hidden"))) static int controller_report_interval_us(int normal_us) {
return low_latency(LATENCY_CONTROLLER) ? LOW_LATENCY_REPORT_INTERVAL_US : normal_us;
}
__attribute__((visibility("hidden"))) static void sleep_us(int us) {
struct timespec interval = {0, us * 1000L};
nanosleep(&interval, nullptr);
}
__attribute__((visibility("hidden"))) static uint64_t monotonic_us() {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return static_cast<uint64_t>(now.tv_sec) * 1000000ULL + static_cast<uint64_t>(now.tv_nsec) / 1000ULL;
}
static bool fake_triton_enabled() {
static int enabled = -1;
if (enabled < 0)
@@ -1453,8 +1478,7 @@ __attribute__((visibility("hidden"))) static void *deck_report_thread(void *arg)
if (send(self->peer, report, sizeof(report), MSG_DONTWAIT | MSG_NOSIGNAL) < 0 &&
errno != EAGAIN && errno != EWOULDBLOCK)
break;
struct timespec interval = {0, DECK_REPORT_INTERVAL_US * 1000L};
nanosleep(&interval, nullptr);
sleep_us(controller_report_interval_us(DECK_REPORT_INTERVAL_US));
}
syscall(SYS_close, self->peer);
munmap(self->ring, self->mapping_size);
@@ -1486,7 +1510,7 @@ __attribute__((visibility("hidden"))) static void put_sony_touch(uint8_t *at, So
}
__attribute__((visibility("hidden"))) static void
build_sony_report(uint8_t *report, uint8_t model, uint32_t packet, uint32_t buttons, const int32_t *axes,
build_sony_report(uint8_t *report, uint8_t model, uint32_t packet, uint64_t elapsed_us, uint32_t buttons, const int32_t *axes,
const DeckExtras &extras, SonyTouch &touch) {
memset(report, 0, SONY_REPORT_BYTES);
report[0] = 0x01;
@@ -1517,7 +1541,7 @@ build_sony_report(uint8_t *report, uint8_t model, uint32_t packet, uint32_t butt
p[6] = static_cast<uint8_t>(system | ((packet & 0x3f) << 2));
p[7] = left_trigger;
p[8] = right_trigger;
put16(p + 9, static_cast<int>(packet * 750));
put16(p + 9, static_cast<int>(elapsed_us * 3 / 16));
for (int i = 0; i < 3; i++) {
put16(p + 12 + i * 2, gyro[i]);
put16(p + 18 + i * 2, accel[i]);
@@ -1539,7 +1563,7 @@ build_sony_report(uint8_t *report, uint8_t model, uint32_t packet, uint32_t butt
put16(p + 15 + i * 2, gyro[i]);
put16(p + 21 + i * 2, accel[i]);
}
put32(p + 27, packet * 12000);
put32(p + 27, static_cast<uint32_t>(elapsed_us * 3));
put_sony_touch(p + 32, touch, 0, left, extras.pads[0], extras.pads[1], SONY_TOUCH_HEIGHT_DUALSENSE);
put_sony_touch(p + 36, touch, 1, right, extras.pads[2], extras.pads[3], SONY_TOUCH_HEIGHT_DUALSENSE);
p[52] = 0x2a;
@@ -1558,6 +1582,7 @@ __attribute__((visibility("hidden"))) static void *sony_report_thread(void *arg)
int32_t axes[8] = {};
DeckExtras extras;
SonyTouch touch;
const uint64_t start_us = monotonic_us();
while (live_pad_type() == type) {
if (read_snapshot(self->ring, snap) && snap.generation == ring_generation(self->ring)) {
buttons = snap.buttons;
@@ -1565,12 +1590,11 @@ __attribute__((visibility("hidden"))) static void *sony_report_thread(void *arg)
}
read_deck_extras(*self, extras);
uint8_t report[SONY_REPORT_BYTES];
build_sony_report(report, self->sony, ++packet, buttons, axes, extras, touch);
build_sony_report(report, self->sony, ++packet, monotonic_us() - start_us, buttons, axes, extras, touch);
if (send(self->peer, report, sizeof(report), MSG_DONTWAIT | MSG_NOSIGNAL) < 0 &&
errno != EAGAIN && errno != EWOULDBLOCK)
break;
struct timespec interval = {0, DECK_REPORT_INTERVAL_US * 1000L};
nanosleep(&interval, nullptr);
sleep_us(controller_report_interval_us(DECK_REPORT_INTERVAL_US));
}
syscall(SYS_close, self->peer);
munmap(self->ring, self->mapping_size);
@@ -1684,8 +1708,7 @@ __attribute__((visibility("hidden"))) static void *triton_report_thread(void *ar
errno != EAGAIN && errno != EWOULDBLOCK)
break;
}
struct timespec interval = {0, TRITON_REPORT_INTERVAL_US * 1000L};
nanosleep(&interval, nullptr);
sleep_us(controller_report_interval_us(TRITON_REPORT_INTERVAL_US));
}
syscall(SYS_close, self->peer);
munmap(self->ring, self->mapping_size);
@@ -1709,7 +1732,7 @@ static constexpr int16_t TRITON_WIRED_PRODUCT = 0x1302;
static constexpr int32_t TRITON_STICK_TOUCH = 2000;
__attribute__((visibility("hidden"))) static void
build_triton_report(uint8_t *report, uint32_t packet, uint32_t buttons, const int32_t *axes,
build_triton_report(uint8_t *report, uint32_t packet, uint64_t elapsed_us, uint32_t buttons, const int32_t *axes,
const DeckExtras &extras) {
memset(report, 0, TRITON_STATE_BYTES);
report[0] = static_cast<uint8_t>(packet);
@@ -1752,7 +1775,7 @@ build_triton_report(uint8_t *report, uint32_t packet, uint32_t buttons, const in
put16(report + 23, extras.pads[2]);
put16(report + 25, extras.pads[3]);
put16(report + 27, extras.pressure[1]);
put32(report + 29, packet * TRITON_REPORT_INTERVAL_US);
put32(report + 29, static_cast<uint32_t>(elapsed_us));
for (int i = 0; i < 3; i++) {
put16(report + 33 + i * 2, extras.accel[i]);
put16(report + 39 + i * 2, extras.gyro[i]);
@@ -1768,6 +1791,7 @@ __attribute__((visibility("hidden"))) static void *triton_pad_thread(void *arg)
uint32_t buttons = 0;
int32_t axes[8] = {};
DeckExtras extras;
const uint64_t start_us = monotonic_us();
while (live_pad_type() == PAD_TYPE_STEAM) {
if (read_snapshot(self->ring, snap) && snap.generation == ring_generation(self->ring)) {
buttons = snap.buttons;
@@ -1776,12 +1800,11 @@ __attribute__((visibility("hidden"))) static void *triton_pad_thread(void *arg)
read_deck_extras(*self, extras);
uint8_t report[TRITON_REPORT_ID_BYTES + TRITON_STATE_BYTES];
report[0] = TRITON_STATE_WIRED;
build_triton_report(report + 1, ++packet, buttons, axes, extras);
build_triton_report(report + 1, ++packet, monotonic_us() - start_us, buttons, axes, extras);
if (send(self->peer, report, sizeof(report), MSG_DONTWAIT | MSG_NOSIGNAL) < 0 &&
errno != EAGAIN && errno != EWOULDBLOCK)
break;
struct timespec interval = {0, TRITON_REPORT_INTERVAL_US * 1000L};
nanosleep(&interval, nullptr);
sleep_us(controller_report_interval_us(TRITON_REPORT_INTERVAL_US));
}
syscall(SYS_close, self->peer);
munmap(self->ring, self->mapping_size);
@@ -2019,8 +2042,7 @@ __attribute__((visibility("hidden"))) static void *touch_report_thread(void *arg
if (send(self->peer, report, sizeof(report), MSG_DONTWAIT | MSG_NOSIGNAL) < 0 &&
errno != EAGAIN && errno != EWOULDBLOCK)
break;
struct timespec interval = {0, TOUCH_REPORT_INTERVAL_US * 1000L};
nanosleep(&interval, nullptr);
sleep_us(low_latency(LATENCY_TOUCH) ? LOW_LATENCY_REPORT_INTERVAL_US : TOUCH_REPORT_INTERVAL_US);
}
syscall(SYS_close, self->peer);
touch_state([](TouchRingFile &r) { if (r.opened) r.opened--; });
@@ -3545,6 +3567,7 @@ EXPORT int close(int fd) {
}
static constexpr int kMaxRingWaitMs = 2;
static constexpr int kLowLatencyRingWaitUs = 250;
EXPORT ssize_t read(int fd, void *buf, size_t count) {
if (!watched(fd)) return syscall(SYS_read, fd, buf, count);
@@ -3633,7 +3656,8 @@ EXPORT ssize_t read(int fd, void *buf, size_t count) {
int result = nanosleep(&sleep_time, nullptr);
guard.lock();
if (result < 0) return -1;
if (backoff_ns < kMaxRingWaitMs * 1000 * 1000) backoff_ns *= 2;
if (low_latency(LATENCY_CONTROLLER)) backoff_ns = kLowLatencyRingWaitUs * 1000;
else if (backoff_ns < kMaxRingWaitMs * 1000 * 1000) backoff_ns *= 2;
}
}
@@ -3815,7 +3839,7 @@ static int poll_fake(struct pollfd *fds, nfds_t nfds, int timeout,
if (deadline_ms >= 0 && monotonic_ms() >= deadline_ms)
return 0;
if (backoff_ms < kMaxRingWaitMs)
if (backoff_ms < kMaxRingWaitMs && !low_latency(LATENCY_CONTROLLER))
backoff_ms *= 2;
}
}
@@ -3988,7 +4012,7 @@ EXPORT int select(int nfds, fd_set *readfds, fd_set *writefds,
if (deadline_ms >= 0 && monotonic_ms() >= deadline_ms)
return 0;
if (backoff_ms < kMaxRingWaitMs)
if (backoff_ms < kMaxRingWaitMs && !low_latency(LATENCY_CONTROLLER))
backoff_ms *= 2;
}
}
@@ -243,6 +243,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)
private var lowLatencyController by mutableStateOf(false)
private var lowLatencyTouch by mutableStateOf(false)
private var padType by mutableStateOf(SessionPrefs.CONTROLLER_DECK)
private var pcKeyboardOpen by mutableStateOf(false)
private var achievementsInFront by mutableStateOf<AchievementsInFront?>(null)
@@ -612,6 +614,18 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
SessionPrefs.setBackActionsInverted(this@SessionActivity, inverted)
backActionsInverted = inverted
},
lowLatencyController = lowLatencyController,
lowLatencyTouch = lowLatencyTouch,
onLowLatencyController = { on ->
SessionPrefs.setLowLatencyController(this@SessionActivity, on)
lowLatencyController = on
applyLowLatency()
},
onLowLatencyTouch = { on ->
SessionPrefs.setLowLatencyTouch(this@SessionActivity, on)
lowLatencyTouch = on
applyLowLatency()
},
onOsc = { v ->
SessionPrefs.setOscMode(this@SessionActivity, v)
readPrefs()
@@ -1081,6 +1095,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
steamDownloadsInBackground = SessionPrefs.steamDownloadsInBackground(this)
oscMode = SessionPrefs.oscMode(this)
backActionsInverted = SessionPrefs.backActionsInverted(this)
lowLatencyController = SessionPrefs.lowLatencyController(this)
lowLatencyTouch = SessionPrefs.lowLatencyTouch(this)
padType = SessionPrefs.steamController(this)
}
@@ -2389,8 +2405,16 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
super.onAttachedToWindow()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return
val decor = window.decorView
decor.requestUnbufferedDispatch(UNBUFFERED_SOURCES)
decor.viewTreeObserver.addOnGlobalFocusChangeListener { _, _ -> decor.post { decor.requestUnbufferedDispatch(UNBUFFERED_SOURCES) } }
decor.requestUnbufferedDispatch(unbufferedSources())
decor.viewTreeObserver.addOnGlobalFocusChangeListener { _, _ -> decor.post { decor.requestUnbufferedDispatch(unbufferedSources()) } }
}
private fun unbufferedSources() =
if (lowLatencyTouch) UNBUFFERED_SOURCES or InputDevice.SOURCE_CLASS_POINTER else UNBUFFERED_SOURCES
private fun applyLowLatency() {
com.droiddeck.launcher.input.FakeInputWriter.setLowLatency(lowLatencyController, lowLatencyTouch)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) window.decorView.requestUnbufferedDispatch(unbufferedSources())
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
@@ -79,6 +79,10 @@ public class FakeInputWriter {
private static final int DECK_CONTROLS_OFFSET = IMU_OFFSET + 40;
private static final int PAD_TYPE_OFFSET = IMU_OFFSET + 44;
private static final int PAD_TYPE_SET = 0x80000000;
private static final int LATENCY_OFFSET = IMU_OFFSET + 48;
public static final int LATENCY_CONTROLLER = 1;
public static final int LATENCY_TOUCH = 2;
private static volatile int latency = 0;
public static final int PAD_TYPE_DECK = 0;
public static final int PAD_TYPE_STEAM = 1;
public static final int PAD_TYPE_XBOX = 2;
@@ -214,6 +218,7 @@ public class FakeInputWriter {
data.putInt(DECK_CONTROLS_OFFSET, 0);
int type = padType;
data.putInt(PAD_TYPE_OFFSET, type >= 0 ? PAD_TYPE_SET | type : 0);
data.putInt(LATENCY_OFFSET, latency);
data.putInt(TRITON_OFFSET, TRITON_MAGIC);
data.putInt(TRITON_CONNECTED_OFFSET, tritonConnected ? 1 : 0);
data.putLong(TRITON_INPUT_SEQ_OFFSET, 0L);
@@ -514,6 +519,22 @@ public class FakeInputWriter {
}
}
public static void setLowLatency(boolean controller, boolean touch) {
int flags = (controller ? LATENCY_CONTROLLER : 0) | (touch ? LATENCY_TOUCH : 0);
latency = flags;
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(LATENCY_OFFSET, flags);
}
}
public static void setTritonConnected(int slot, boolean connected) {
tritonConnected = connected;
RingSlot ringSlot = tritonSlot(slot);
@@ -130,6 +130,18 @@ object SessionPrefs {
private fun mangoappSession(context: Context) =
SessionState.mode == SessionService.MODE_STEAM && mangoapp(context)
fun lowLatencyController(context: Context): Boolean = prefs(context).getBoolean("lowLatencyController", false)
fun setLowLatencyController(context: Context, on: Boolean) {
prefs(context).edit().putBoolean("lowLatencyController", on).apply()
}
fun lowLatencyTouch(context: Context): Boolean = prefs(context).getBoolean("lowLatencyTouch", false)
fun setLowLatencyTouch(context: Context, on: Boolean) {
prefs(context).edit().putBoolean("lowLatencyTouch", on).apply()
}
fun backActionsInverted(context: Context): Boolean = prefs(context).getBoolean("backActionsInverted", false)
fun setBackActionsInverted(context: Context, inverted: Boolean) {
@@ -610,6 +610,7 @@ class SessionService : Service() {
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()
com.droiddeck.launcher.input.FakeInputWriter.setLowLatency(SessionPrefs.lowLatencyController(this), SessionPrefs.lowLatencyTouch(this))
if (uinput) {
guest.add("FAKE_EVDEV_UINPUT=1")
if (SessionState.deckPad) {
@@ -233,6 +233,10 @@ class DrawerActions(
val onSuspendPolicy: (String) -> Unit,
val onSteamDownloadsInBackground: (Boolean) -> Unit = {},
val onBackActionsInverted: (Boolean) -> Unit,
val lowLatencyController: Boolean = false,
val lowLatencyTouch: Boolean = false,
val onLowLatencyController: (Boolean) -> Unit = {},
val onLowLatencyTouch: (Boolean) -> Unit = {},
val onTouch: (String) -> Unit,
val onFexPreset: (String) -> Unit,
val onUpscaler: (Int) -> Unit = {},
@@ -497,6 +501,14 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont
stringResource(R.string.unfolded_controls_hint), a.unfoldedControls,
chipModifier = focus.track(page, "unfolded-controls"), onChange = a.onUnfoldedControls)
}
SettingsGroup(stringResource(R.string.drawer_latency)) {
ToggleRow(host, "low-latency-controller", stringResource(R.string.drawer_low_latency_controller),
stringResource(R.string.drawer_low_latency_controller_hint), a.lowLatencyController,
chipModifier = focus.track(page, "low-latency-controller"), onChange = a.onLowLatencyController)
ToggleRow(host, "low-latency-touch", stringResource(R.string.drawer_low_latency_touch),
stringResource(R.string.drawer_low_latency_touch_hint), a.lowLatencyTouch,
chipModifier = focus.track(page, "low-latency-touch"), onChange = a.onLowLatencyTouch)
}
SettingsGroup(stringResource(R.string.drawer_keyboard)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth().padding(8.dp)) {
DrawerOutlineButton(stringResource(R.string.drawer_pc_keyboard), modifier = Modifier.weight(1f).height(48.dp).then(focus.track(page, "hardware"))) {
+5
View File
@@ -622,6 +622,11 @@
<string name="drawer_texture_sharpness_note">A negative mip bias: sharper textures, some shimmer. Auto matches how far the session is enlarged to the screen.</string>
<string name="drawer_touch_auto">Auto (%1$s)</string>
<string name="drawer_keyboard">Keyboard</string>
<string name="drawer_latency">Latency</string>
<string name="drawer_low_latency_controller">Low-latency controller</string>
<string name="drawer_low_latency_controller_hint">Pad reports at 1000 Hz instead of 250 Hz. Slightly more CPU.</string>
<string name="drawer_low_latency_touch">Low-latency touch</string>
<string name="drawer_low_latency_touch_hint">Touches skip frame batching; Steam touch controls at 1000 Hz. Slightly more CPU.</string>
<string name="drawer_pc_keyboard">PC keyboard</string>
<string name="drawer_android_keyboard">Android</string>
<string name="drawer_second_screen">Second screen</string>