Skip to content
113 changes: 102 additions & 11 deletions app/src/main/cpp/winlator/fakeinput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <dirent.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <limits.h>
#include <linux/input.h>
#include <linux/joystick.h>
#include <poll.h>
Expand All @@ -38,10 +39,11 @@

#define EXPORT __attribute__((visibility("default"))) extern "C"

static constexpr uint16_t GAMEPAD_VENDOR_ID_BASE = 0x1234;
static constexpr uint16_t GAMEPAD_PRODUCT_ID_BASE = 0x5678;
static constexpr uint16_t GAMEPAD_VENDOR_ID_DEFAULT = 0x045E; // Microsoft
static constexpr uint16_t GAMEPAD_PRODUCT_ID_DEFAULT = 0x028E; // Xbox 360 Controller
static constexpr uint16_t GAMEPAD_VERSION = 0x0110;
static constexpr const char *GAMEPAD_NAME_TEMPLATE = "Generic HID Gamepad %d";
static constexpr uint32_t GAMEPAD_IDENTITY_MAX_SLOTS = 4;
static constexpr const char *GAMEPAD_NAME_TEMPLATE = "Xbox 360 Controller (%d)";
static constexpr const char *GAMEPAD_PHYS_TEMPLATE = "usb-fakeinput/input%d";
static constexpr const char *GAMEPAD_UNIQ_TEMPLATE = "0000000000%02d";
static constexpr uint8_t GAMEPAD_AXIS_COUNT = 8;
Expand Down Expand Up @@ -80,6 +82,16 @@ static constexpr size_t FAKE_INPUT_RING_SIZE =
FAKE_INPUT_RING_HEADER_SIZE +
(FAKE_INPUT_RING_CAPACITY * FAKE_INPUT_EVENT_SIZE);

// Per-slot real-controller identity, published by the host when the user opts
// into "Force External Gamepad Identity" so games with dedicated non-Xbox
// controller support (e.g. PlayStation) can recognize the real pad instead
// of the spoofed Xbox 360 Controller identity.
struct GamepadIdentity {
uint16_t vendor;
uint16_t product;
std::string name;
};

struct FakeController {
char *event = nullptr;
int slot = -1;
Expand All @@ -93,6 +105,7 @@ struct FakeController {
size_t keyframe_remaining = 0;
int32_t keyframe_axes[8] = {0, 0, 0, 0, 0, 0, 0, 0};
uint32_t keyframe_buttons = 0;
GamepadIdentity identity;
};

struct NeutralEventSpec {
Expand Down Expand Up @@ -354,6 +367,68 @@ get_ring_path_for_slot(int slot) {
return it == ring_paths.end() ? nullptr : it->second.c_str();
}

// Reads the slot's identity straight out of its udev entry
// ("<udev_data_dir>/c13:<minor>"), which the host keeps up to date as pads are
// bound and released.
//
// Returns false when the entry is missing or incomplete, which leaves the caller
// on the compiled-in Xbox 360 default.
__attribute__((visibility("hidden"))) static bool
read_gamepad_identity(int slot, GamepadIdentity *out) {
if (!udev_data_dir || !*udev_data_dir || !my_open)
return false;
if (slot < 0 || slot >= static_cast<int>(GAMEPAD_IDENTITY_MAX_SLOTS))
return false;

char path[PATH_MAX];
snprintf(path, PATH_MAX, "%s/c13:%u", udev_data_dir, FAKE_INPUT_EVENT_MINOR_BASE + slot);

int fd = my_open(path, O_RDONLY);
if (fd < 0)
return false;

// A udev entry for one of our pads is a few hundred bytes; read it whole so a
// truncated tail can never hide the NAME line.
char buffer[2048];
ssize_t length = syscall(SYS_read, fd, buffer, sizeof(buffer) - 1);
syscall(SYS_close, fd);
if (length <= 0)
return false;
buffer[length] = '\0';

GamepadIdentity parsed;
parsed.vendor = 0;
parsed.product = 0;
bool have_vendor = false;
bool have_product = false;
char *saveptr = nullptr;
for (char *line = strtok_r(buffer, "\n", &saveptr); line;
line = strtok_r(nullptr, "\n", &saveptr)) {
if (!strncmp(line, "E:ID_VENDOR_ID=", 15)) {
parsed.vendor = static_cast<uint16_t>(strtoul(line + 15, nullptr, 16));
have_vendor = true;
} else if (!strncmp(line, "E:ID_MODEL_ID=", 14)) {
parsed.product = static_cast<uint16_t>(strtoul(line + 14, nullptr, 16));
have_product = true;
} else if (!strncmp(line, "E:NAME=", 7)) {
// udev quotes the value; the name itself never contains a quote because
// the host strips them before publishing.
const char *value = line + 7;
size_t value_length = strlen(value);
if (value_length >= 2 && value[0] == '"' && value[value_length - 1] == '"')
parsed.name.assign(value + 1, value_length - 2);
else
parsed.name.assign(value);
}
}

if (!have_vendor || !have_product || parsed.name.empty())
return false;

*out = parsed;
return true;
}

__attribute__((visibility("hidden"))) static uint64_t
ring_write_seq(const FakeInputRingHeader *ring) {
return __atomic_load_n(&ring->write_seq, __ATOMIC_ACQUIRE);
Expand Down Expand Up @@ -462,17 +537,15 @@ open_fake_input_ring(const char *event, int flags) {
if (fd < 0)
return -1;

void *mapping =
mmap(nullptr, FAKE_INPUT_RING_SIZE, PROT_READ, MAP_SHARED, fd, 0);
void *mapping = mmap(nullptr, FAKE_INPUT_RING_SIZE, PROT_READ, MAP_SHARED, fd, 0);
if (mapping == MAP_FAILED) {
int saved_errno = errno;
syscall(SYS_close, fd);
errno = saved_errno;
return -1;
}

FakeInputRingHeader *ring =
reinterpret_cast<FakeInputRingHeader *>(mapping);
FakeInputRingHeader *ring = reinterpret_cast<FakeInputRingHeader *>(mapping);
if (!ring_header_is_valid(ring)) {
munmap(mapping, FAKE_INPUT_RING_SIZE);
syscall(SYS_close, fd);
Expand All @@ -487,6 +560,15 @@ open_fake_input_ring(const char *event, int flags) {
controller.mapping_size = FAKE_INPUT_RING_SIZE;
controller.read_seq = ring_write_seq(ring);
controller.generation = ring_generation(ring);

if (!read_gamepad_identity(slot, &controller.identity)) {
char buf[32];
snprintf(buf, sizeof(buf), GAMEPAD_NAME_TEMPLATE, slot);
controller.identity.name = std::string(buf);
controller.identity.vendor = static_cast<uint16_t>(GAMEPAD_VENDOR_ID_DEFAULT);
controller.identity.product = static_cast<uint16_t>(GAMEPAD_PRODUCT_ID_DEFAULT);
}

// Emit the current absolute state as the first frame so a guest that opens
// mid-hold (or reopens after a slot hand-off) starts already in sync.
capture_keyframe(controller, "open", fd);
Expand All @@ -506,6 +588,15 @@ copy_slot_ioctl_string(int op, void *argp, const char *format, int event_number)
snprintf(static_cast<char *>(argp), size, format, event_number);
}

__attribute__((visibility("hidden"))) static void
copy_ioctl_string(int op, void *argp, const char *value) {
size_t size = _IOC_SIZE(op);
if (!argp || size == 0)
return;

snprintf(static_cast<char *>(argp), size, "%s", value);
}

__attribute__((visibility("hidden"))) static bool is_fake_input_fd(int fd) {
return controller_map.find(fd) != controller_map.end();
}
Expand Down Expand Up @@ -877,14 +968,14 @@ EXPORT int ioctl(int fd, int op, ...) {
struct input_id id;
memset(&id, 0, sizeof(id));
id.bustype = 0x03;
id.vendor = static_cast<uint16_t>(GAMEPAD_VENDOR_ID_BASE + event_number);
id.product = static_cast<uint16_t>(GAMEPAD_PRODUCT_ID_BASE + event_number);
id.vendor = controller->second.identity.vendor;
id.product = controller->second.identity.product;
id.version = GAMEPAD_VERSION;
memcpy(argp, (void *)&id, sizeof(id));
return 0;
} else if (type == 0x45 && number == 0x6) {
Logger::log("Hooking ioctl EVIOCGNAME for event %s\n", event);
copy_slot_ioctl_string(op, argp, GAMEPAD_NAME_TEMPLATE, event_number);
copy_ioctl_string(op, argp, controller->second.identity.name.c_str());
return 0;
} else if (type == 0x45 && number == 0x7) {
Logger::log("Hooking ioctl EVIOCGPHYS for event %s\n", event);
Expand Down Expand Up @@ -1008,7 +1099,7 @@ EXPORT int ioctl(int fd, int op, ...) {
return 0;
} else if (type == 0x6A && number == 0x13) {
Logger::log("Hooking ioctl JSIOCGNAME(len) for event %s\n", event);
copy_slot_ioctl_string(op, argp, GAMEPAD_NAME_TEMPLATE, event_number);
copy_ioctl_string(op, argp, controller->second.identity.name.c_str());
return 0;
} else {
Logger::log("Unhandled evdev ioctl, type %d number %d\n", type, number);
Expand Down
22 changes: 22 additions & 0 deletions app/src/main/feature/settings/input/InputControlsFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.winlator.cmod.runtime.input.controls.ControlElement
import com.winlator.cmod.runtime.input.controls.ControlsProfile
import com.winlator.cmod.runtime.input.controls.ExternalController
import com.winlator.cmod.runtime.input.controls.ExternalControllerBinding
import com.winlator.cmod.runtime.input.controls.GamepadIdentityStore
import com.winlator.cmod.runtime.input.controls.GestureProfileManager
import com.winlator.cmod.runtime.input.controls.InputControlsManager
import com.winlator.cmod.runtime.input.ui.InputControlsView
Expand Down Expand Up @@ -218,6 +219,7 @@ class InputControlsFragment : Fragment() {
onExportProfile = ::exportProfile,
onControllerExpandedToggle = ::toggleControllerExpanded,
onRemoveController = ::removeController,
onReportRealIdentityChanged = ::setReportRealIdentity,
onBindingTypeClick = ::showBindingTypePicker,
onBindingValueClick = ::showBindingValuePicker,
onRemoveBinding = ::removeBinding,
Expand Down Expand Up @@ -357,12 +359,29 @@ class InputControlsFragment : Fragment() {
} else {
emptyList()
},
reportRealIdentity = controller.isUseRealIdentity,
)
},
dialog = dialogState,
)
}

// Per-pad opt-in to reporting the real name/VID/PID instead of the Xbox 360 spoof.
private fun setReportRealIdentity(
controllerId: String,
useRealIdentity: Boolean,
) {
val controller = findVisibleController(controllerId) ?: return
val activity = activity ?: return
controller.setUseRealIdentity(useRealIdentity)
controller.savePreferences(activity)
lifecycleScope.launch(Dispatchers.IO) {
launch(Dispatchers.Main) {
publishUiState()
}
}
}

private fun buildBindingState(
controller: ExternalController,
bindingTypeEntries: Array<String>,
Expand Down Expand Up @@ -404,6 +423,7 @@ class InputControlsFragment : Fragment() {
for (i in 0 until pController.controllerBindingCount) {
liveMatch.addControllerBinding(pController.getControllerBindingAt(i))
}
liveMatch.setUseRealIdentity(pController.isUseRealIdentity());
visibleControllers.add(liveMatch)
} else {
visibleControllers.add(pController)
Expand All @@ -419,6 +439,8 @@ class InputControlsFragment : Fragment() {
visibleControllers.addAll(ExternalController.getControllers())
}

visibleControllers.forEach { controller -> controller.loadPreferences(activity) }

activeBindingController =
activeId?.let { id ->
visibleControllers.firstOrNull { it.id == id }
Expand Down
49 changes: 49 additions & 0 deletions app/src/main/feature/settings/input/InputControlsScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ data class InputControllerCardState(
val expanded: Boolean,
val showBindings: Boolean,
val bindings: List<InputControllerBindingState> = emptyList(),
val reportRealIdentity: Boolean = false,
)

data class InputControllerBindingState(
Expand Down Expand Up @@ -286,6 +287,7 @@ data class InputControlsScreenActions(
val onExportProfile: () -> Unit,
val onControllerExpandedToggle: (String) -> Unit,
val onRemoveController: (String) -> Unit,
val onReportRealIdentityChanged: (String, Boolean) -> Unit,
val onBindingTypeClick: (String, Int) -> Unit,
val onBindingValueClick: (String, Int) -> Unit,
val onRemoveBinding: (String, Int) -> Unit,
Expand Down Expand Up @@ -2688,6 +2690,53 @@ private fun ControllerCard(
}
}

// Only offer this for a pad that is actually attached: the identity comes from the
// live InputDevice, so there is nothing to report for a remembered-but-absent pad.
if (state.connected) {
Spacer(Modifier.height(InputCompactGap))
Row(
modifier =
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(InputFieldCorner))
.background(InputSubcard)
.border(1.dp, InputOutline, RoundedCornerShape(InputFieldCorner))
.paneNavItem(
cornerRadius = InputFieldCorner,
onActivate = {
actions.onReportRealIdentityChanged(
state.controllerId,
!state.reportRealIdentity,
)
},
highlightColor = InputNavHighlight,
).padding(horizontal = 8.dp, vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(R.string.input_controls_report_real_identity_title),
color = InputTextPrimary,
fontSize = InputPrimaryTextSize,
)
Spacer(Modifier.height(1.dp))
Text(
text = stringResource(R.string.input_controls_report_real_identity_summary),
color = InputTextSecondary,
fontSize = InputSecondaryTextSize,
lineHeight = 14.sp,
)
}
Spacer(Modifier.width(InputCompactGap))
AppSwitch(
checked = state.reportRealIdentity,
onCheckedChange = { enabled ->
actions.onReportRealIdentityChanged(state.controllerId, enabled)
},
)
}
}

if (state.showBindings) {
Spacer(Modifier.height(InputCompactGap))
Subcard(
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values-b+es+419/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,9 @@ Por ejemplo, <b>META</b> para la <i>tecla META</i>, \n
<string name="input_controls_auto_hide_on_controller_title">Ocultar automáticamente con el control</string>
<string name="input_controls_auto_hide_on_controller_summary">Oculta los controles táctiles cuando hay un control conectado</string>

<string name="input_controls_report_real_identity_title">Usar identidad real</string>
<string name="input_controls_report_real_identity_summary">Envía a los juegos el nombre y los ID propios de este dispositivo en lugar de simular un control Xbox 360.</string>

<!-- Session > Gyroscope -->
<string name="session_gyroscope_title">Giroscopio</string>
<string name="session_gyroscope_calibrate">Calibrar giroscopio</string>
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values-da/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,10 @@ F.eks. <b>META</b> for <i>META-tast</i>, \n
<string name="input_controls_auto_hide_section">Berøringskontroller</string>
<string name="input_controls_auto_hide_on_controller_title">Skjul automatisk ved controller</string>
<string name="input_controls_auto_hide_on_controller_summary">Skjul skærmkontroller, når en controller er tilsluttet</string>

<string name="input_controls_report_real_identity_title">Brug rigtig identitet</string>
<string name="input_controls_report_real_identity_summary">Send denne enheds eget navn og ID\'er til spil i stedet for at efterligne en Xbox 360-controller.</string>

<string name="session_gyroscope_title">Gyroskop</string>
<string name="session_gyroscope_calibrate">Kalibrer gyroskop</string>
<string name="session_gyroscope_enable_right_stick">Aktiver gyroskopisk bevægelse for højre stick</string>
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,10 @@ Z. B. <b>META</b> für <i>Meta-Taste</i>, \n
<string name="input_controls_auto_hide_section">Touchscreen-Steuerung</string>
<string name="input_controls_auto_hide_on_controller_title">Bei Controller automatisch ausblenden</string>
<string name="input_controls_auto_hide_on_controller_summary">Touchscreen-Steuerung ausblenden, wenn ein Controller verbunden ist</string>

<string name="input_controls_report_real_identity_title">Echte Identität verwenden</string>
<string name="input_controls_report_real_identity_summary">Den eigenen Namen und die IDs dieses Geräts an Spiele senden, anstatt einen Xbox 360-Controller zu simulieren.</string>

<string name="session_gyroscope_title">Gyroskop</string>
<string name="session_gyroscope_calibrate">Gyroskop kalibrieren</string>
<string name="session_gyroscope_enable_right_stick">Gyroskopische Bewegung des rechten Sticks aktivieren</string>
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,10 @@ Ej. <b>META</b> para la <i>tecla META</i>, \n
<string name="input_controls_auto_hide_section">Controles táctiles</string>
<string name="input_controls_auto_hide_on_controller_title">Ocultar automáticamente con mando</string>
<string name="input_controls_auto_hide_on_controller_summary">Oculta los controles táctiles cuando se conecta un mando</string>

<string name="input_controls_report_real_identity_title">Usar identidad real</string>
<string name="input_controls_report_real_identity_summary">Envía a los juegos el nombre y los ID propios de este dispositivo en lugar de simular un mando de Xbox 360.</string>

<string name="session_gyroscope_title">Giroscopio</string>
<string name="session_gyroscope_calibrate">Calibrar giroscopio</string>
<string name="session_gyroscope_enable_right_stick">Activar movimiento giroscópico del joystick derecho</string>
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values-fi/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,9 @@ E.g. <b>META</b> for <i>META-näppäin</i>, \n
<string name="input_controls_auto_hide_on_controller_title">Piilota automaattisesti ohjaimella</string>
<string name="input_controls_auto_hide_on_controller_summary">Piilota kosketusnäytön ohjaimet, kun ohjain on liitetty</string>

<string name="input_controls_report_real_identity_title">Käytä todellista tunnistetta</string>
<string name="input_controls_report_real_identity_summary">Lähetä peleille tämän laitteen oma nimi ja tunnisteet sen sijaan, että se esiintyisi Xbox 360 -ohjaimena.</string>

<!-- Session > Gyroscope -->
<string name="session_gyroscope_title">Gyroskooppi</string>
<string name="session_gyroscope_calibrate">Kalibroi gyroskooppi</string>
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,10 @@ Par ex. <b>META</b> pour la <i>touche META</i>, \n
<string name="input_controls_auto_hide_section">Commandes tactiles</string>
<string name="input_controls_auto_hide_on_controller_title">Masquer automatiquement avec une manette</string>
<string name="input_controls_auto_hide_on_controller_summary">Masquer les commandes tactiles lorsqu\'une manette est connectée</string>

<string name="input_controls_report_real_identity_title">Utiliser l\'identité réelle</string>
<string name="input_controls_report_real_identity_summary">Envoyer aux jeux le nom et les identifiants propres à cet appareil au lieu de simuler une manette Xbox 360.</string>

<string name="session_gyroscope_title">Gyroscope</string>
<string name="session_gyroscope_calibrate">Calibrer le gyroscope</string>
<string name="session_gyroscope_enable_right_stick">Activer le mouvement gyroscopique du stick droit</string>
Expand Down
Loading