mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Read the phone's motion sensors only while Steam asks for them (#534)
perf: only read the phone's motion sensors when Steam wants them The gyro and accelerometer ran at 4 ms for the whole session, even in games without gyro, costing about 6% of a core. Steam already tells the Deck pad when it wants motion (setting 48), so libfakeinput now passes that on and PadMotion follows it, with a short delay to ride out Steam's brief toggles.
This commit is contained in:
1 parent
0579e688ba
commit
c115916962
3 files changed
+112
-12
No files matched your search
@@ -1246,7 +1246,7 @@ struct DeckHidraw {
|
||||
// Absent from a ring file written without one.
|
||||
struct DeckImu {
|
||||
uint32_t magic; // IMU1 once written
|
||||
uint32_t reserved;
|
||||
uint32_t imu_mode; // written by this library: the client's SETTING_IMU_MODE | 0x80000000
|
||||
uint64_t seq; // odd while being written
|
||||
int16_t accel[3];
|
||||
int16_t gyro[3];
|
||||
@@ -1906,6 +1906,41 @@ __attribute__((visibility("hidden"))) static void log_deck_feature(const char *w
|
||||
if ((n & (n - 1)) == 0) Logger::log("deck: feature 0x%02x %s (x%u)\n", feature, what, n);
|
||||
}
|
||||
|
||||
// A settings report (ID_SET_SETTINGS_VALUES) is a list of (setting id, 16-bit value) triples after
|
||||
// the length byte. Returns SETTING_IMU_MODE (48, a SETTING_GYRO_MODE_* bitmask from SDL's
|
||||
// controller_constants.h) when the report changes it, logged, else -1: the client repeats its
|
||||
// settings for as long as it has the pad.
|
||||
static constexpr uint8_t DECK_SET_SETTINGS_VALUES = 0x87;
|
||||
static constexpr uint8_t DECK_SETTING_IMU_MODE = 48;
|
||||
|
||||
__attribute__((visibility("hidden"))) static int deck_imu_mode_change(const uint8_t *buf, size_t size) {
|
||||
static std::atomic<int32_t> last{-1};
|
||||
size_t end = std::min(size, static_cast<size_t>(3) + buf[2]);
|
||||
for (size_t i = 3; i + 3 <= end; i += 3) {
|
||||
if (buf[i] != DECK_SETTING_IMU_MODE) continue;
|
||||
int32_t value = buf[i + 1] | buf[i + 2] << 8;
|
||||
if (last.exchange(value) == value) return -1;
|
||||
Logger::log("deck: imu mode 0x%04x\n", value);
|
||||
return value;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Tells the app whether the client wants motion (DeckImu::imu_mode, FakeInputWriter.readImuMode):
|
||||
// PadMotion reads the phone's sensors only while it does. The mapping is read-only, and the
|
||||
// client changes the mode a few times a minute at most, so the file is written directly.
|
||||
__attribute__((visibility("hidden"))) static void publish_imu_mode(const DeckHidraw &deck, int mode) {
|
||||
static auto my_open = reinterpret_cast<int (*)(const char *, int, ...)>(dlsym(RTLD_NEXT, "open"));
|
||||
if (deck.mapping_size < FAKE_INPUT_RING_SIZE + DECK_IMU_BLOCK_SIZE) return;
|
||||
std::string path = get_ring_path_for_slot(0);
|
||||
int fd = path.empty() ? -1 : my_open(path.c_str(), O_WRONLY | O_CLOEXEC);
|
||||
if (fd < 0) return;
|
||||
uint32_t value = 0x80000000u | static_cast<uint32_t>(mode);
|
||||
if (pwrite(fd, &value, sizeof(value), FAKE_INPUT_RING_SIZE + offsetof(DeckImu, imu_mode)) != sizeof(value))
|
||||
Logger::log("deck: imu mode not published (%s)\n", strerror(errno));
|
||||
syscall(SYS_close, fd);
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static int
|
||||
deck_feature(DeckHidraw &deck, ioctl_request_t op, uint8_t *buf, bool set) {
|
||||
size_t size = _IOC_SIZE(op);
|
||||
@@ -1919,6 +1954,10 @@ deck_feature(DeckHidraw &deck, ioctl_request_t op, uint8_t *buf, bool set) {
|
||||
int left = buf[6] | buf[7] << 8, right = buf[8] | buf[9] << 8;
|
||||
send_vibration(left, right, left || right ? 1000 : 0, 0);
|
||||
}
|
||||
if (buf[1] == DECK_SET_SETTINGS_VALUES && size >= 3) {
|
||||
int imu_mode = deck_imu_mode_change(buf, size);
|
||||
if (imu_mode >= 0) publish_imu_mode(deck, imu_mode);
|
||||
}
|
||||
log_deck_feature("set", buf[1]);
|
||||
return static_cast<int>(size);
|
||||
}
|
||||
|
||||
@@ -99,6 +99,8 @@ public class FakeInputWriter {
|
||||
// units, under a seqlock like the snapshot's. Only libfakeinput's Deck controller reads it.
|
||||
private static final int IMU_OFFSET = RING_SIZE;
|
||||
private static final int IMU_MAGIC = 0x31554D49; // IMU1
|
||||
// Written by libfakeinput, not here: the client's SETTING_IMU_MODE | 0x80000000 (readImuMode).
|
||||
private static final int IMU_MODE_OFFSET = IMU_OFFSET + 4;
|
||||
private static final int IMU_SEQ_OFFSET = IMU_OFFSET + 8;
|
||||
private static final int IMU_ACCEL_OFFSET = IMU_OFFSET + 16; // short[3]
|
||||
private static final int IMU_GYRO_OFFSET = IMU_OFFSET + 22; // short[3]
|
||||
@@ -215,6 +217,7 @@ public class FakeInputWriter {
|
||||
data.putShort(RING_SNAPSHOT_AXES_OFFSET + (i * 2), (short) 0);
|
||||
}
|
||||
data.putInt(IMU_OFFSET, 0);
|
||||
data.putInt(IMU_MODE_OFFSET, 0);
|
||||
data.putLong(IMU_SEQ_OFFSET, 0L);
|
||||
for (int i = 0; i < 12; i++) {
|
||||
data.putShort(IMU_ACCEL_OFFSET + (i * 2), (short) 0);
|
||||
@@ -476,6 +479,23 @@ public class FakeInputWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The motion the Steam client asked the Deck controller of slot {@code slot} for: its
|
||||
* SETTING_IMU_MODE (0 = off), or -1 until it has said. Safe from any thread.
|
||||
*/
|
||||
public static int readImuMode(int slot) {
|
||||
RingSlot ringSlot;
|
||||
synchronized (RING_LOCK) {
|
||||
ringSlot = slot >= 0 && slot < RING_SLOTS.length ? RING_SLOTS[slot] : null;
|
||||
}
|
||||
if (ringSlot == null) return -1;
|
||||
synchronized (ringSlot) {
|
||||
ByteBuffer ring = ringSlot.data;
|
||||
int value = ring == null ? 0 : ring.getInt(IMU_MODE_OFFSET);
|
||||
return value < 0 ? value & 0xffff : -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes the Deck's back grips and trackpads for slot {@code slot} (see DeckControls):
|
||||
* {@code controls} is the DeckControls bit set, {@code pads} left X, Y, right X, Y and
|
||||
|
||||
@@ -20,9 +20,10 @@ import kotlin.math.roundToInt
|
||||
* and units as SDL's Deck driver undoes them (SDL_hidapi_steamdeck.c): X right, Y away from the
|
||||
* player, Z up; 1 g = 16384, 2000 °/s = 32768. The Steam client calibrates the gyro itself.
|
||||
*
|
||||
* Sensors run only while the session is on screen with the pad a Deck controller, sampled as often
|
||||
* as the Deck reports (4 ms); each reading goes straight into the pad's ring
|
||||
* ([FakeInputWriter.writeMotion]), where libfakeinput's 4 ms report picks up the latest.
|
||||
* Sensors run only while the session is on screen with the pad a Deck controller, and the client
|
||||
* wants motion (see [follow]), sampled as often as the Deck reports (4 ms); each reading goes
|
||||
* straight into the pad's ring ([FakeInputWriter.writeMotion]), where libfakeinput's 4 ms report
|
||||
* picks up the latest.
|
||||
*/
|
||||
class PadMotion(private val context: Context, private val rotation: () -> Int) : SensorEventListener {
|
||||
private val sensors = context.getSystemService(SensorManager::class.java)
|
||||
@@ -32,6 +33,8 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) :
|
||||
// Touched only on the worker thread.
|
||||
private val accel = ShortArray(3)
|
||||
private val gyro = ShortArray(3)
|
||||
private var reading = false
|
||||
private var idlePolls = 0
|
||||
|
||||
fun start() {
|
||||
if (thread != null || sensors == null) return
|
||||
@@ -45,8 +48,13 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) :
|
||||
thread = worker
|
||||
val handler = Handler(worker.looper)
|
||||
active = true
|
||||
gyroscope?.let { sensors.registerListener(this, it, SAMPLING_US, handler) }
|
||||
accelerometer?.let { sensors.registerListener(this, it, SAMPLING_US, handler) }
|
||||
handler.post(object : Runnable {
|
||||
override fun run() {
|
||||
if (!active) return
|
||||
follow(handler, gyroscope, accelerometer)
|
||||
handler.postDelayed(this, POLL_MS)
|
||||
}
|
||||
})
|
||||
Log.i(TAG, "pad motion: gyro ${gyroscope?.name ?: "none"}, accelerometer ${accelerometer?.name ?: "none"}")
|
||||
}
|
||||
|
||||
@@ -54,17 +62,48 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) :
|
||||
val worker = thread ?: return
|
||||
thread = null
|
||||
active = false
|
||||
sensors?.unregisterListener(this)
|
||||
// A still controller, not one frozen mid-turn - published on the worker, after any reading
|
||||
// already queued there, so nothing can overwrite it.
|
||||
// On the worker, after any poll or reading already queued there, so nothing registers again
|
||||
// or overwrites the still controller.
|
||||
Handler(worker.looper).post {
|
||||
gyro.fill(0)
|
||||
FakeInputWriter.writeMotion(SLOT, accel, gyro)
|
||||
SteamTouchDevice.current?.setMotion(accel, gyro)
|
||||
sensors?.unregisterListener(this)
|
||||
reading = false
|
||||
idlePolls = 0
|
||||
still()
|
||||
}
|
||||
worker.quitSafely()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the sensors while the client wants motion: the Deck's IMU mode (unknown counts as
|
||||
* wanted), or Steam's touch controls asking for gyro. A game whose layout has no gyro turns the
|
||||
* mode off; Steam's own menus keep it on. The client also turns it off and on again for a few
|
||||
* ms whenever it re-applies a layout, so the sensors stop only after two polls without it.
|
||||
*/
|
||||
private fun follow(handler: Handler, gyroscope: Sensor?, accelerometer: Sensor?) {
|
||||
val touch = SteamTouchDevice.current
|
||||
val wanted = FakeInputWriter.readImuMode(SLOT) != 0 ||
|
||||
touch != null && touch.plugged && touch.gyroRequested && touch.motionEnabled
|
||||
idlePolls = if (wanted) 0 else idlePolls + 1
|
||||
if (wanted && !reading) {
|
||||
reading = true
|
||||
gyroscope?.let { sensors?.registerListener(this, it, SAMPLING_US, handler) }
|
||||
accelerometer?.let { sensors?.registerListener(this, it, SAMPLING_US, handler) }
|
||||
Log.i(TAG, "pad motion: reading")
|
||||
} else if (reading && idlePolls >= 2) {
|
||||
reading = false
|
||||
sensors?.unregisterListener(this)
|
||||
still()
|
||||
Log.i(TAG, "pad motion: paused, the client wants none")
|
||||
}
|
||||
}
|
||||
|
||||
/** A still controller, not one frozen mid-turn. */
|
||||
private fun still() {
|
||||
gyro.fill(0)
|
||||
FakeInputWriter.writeMotion(SLOT, accel, gyro)
|
||||
SteamTouchDevice.current?.setMotion(accel, gyro)
|
||||
}
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
if (!active) return
|
||||
val values = event.values
|
||||
@@ -99,6 +138,8 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) :
|
||||
private const val SLOT = 0
|
||||
/** The Deck controller's report interval (libfakeinput: DECK_REPORT_INTERVAL_US). */
|
||||
private const val SAMPLING_US = 4000
|
||||
/** How often [follow] checks whether the client wants motion. */
|
||||
private const val POLL_MS = 500L
|
||||
private const val ACCEL_COUNTS_PER_M_S2 = 16384f / SensorManager.GRAVITY_EARTH
|
||||
private const val GYRO_COUNTS_PER_RAD_S = (32768f / 2000f) * (180f / Math.PI.toFloat())
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user