mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
feat: low-latency controller and touch toggles in the session drawer
This commit is contained in:
1 parent
4b17bc9a23
commit
ceb0bee79a
7 files changed
+121
-22
No files matched your search
@@ -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"))) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user