mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-08-13 00:19:17 +02:00
Reduce Virtual Controller's input latency
This commit is contained in:
@@ -19,6 +19,7 @@ import type { MkbConvertedPresetData } from "@/types/presets";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { BxEventBus } from "@/utils/bx-event-bus";
|
||||
import { generateVirtualControllerMapping, toXcloudGamepadKey } from "@/utils/gamepad";
|
||||
|
||||
const PointerToMouseButton = {
|
||||
1: 0,
|
||||
@@ -152,6 +153,8 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
};
|
||||
private nativeGetGamepads: Navigator['getGamepads'];
|
||||
|
||||
private xCloudGamepad: XcloudGamepad = generateVirtualControllerMapping(0);
|
||||
|
||||
private initialized = false;
|
||||
private enabled = false;
|
||||
private mouseDataProvider: MouseDataProvider | undefined;
|
||||
@@ -171,16 +174,16 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
|
||||
private popup: MkbPopup;
|
||||
|
||||
private STICK_MAP: { [key in GamepadKey]?: [GamepadKey[], number, number] } = {
|
||||
[GamepadKey.LS_LEFT]: [this.LEFT_STICK_X, 0, -1],
|
||||
[GamepadKey.LS_RIGHT]: [this.LEFT_STICK_X, 0, 1],
|
||||
[GamepadKey.LS_UP]: [this.LEFT_STICK_Y, 1, -1],
|
||||
[GamepadKey.LS_DOWN]: [this.LEFT_STICK_Y, 1, 1],
|
||||
private STICK_MAP: { [key in GamepadKey]?: [GamepadKey[], number] } = {
|
||||
[GamepadKey.LS_LEFT]: [this.LEFT_STICK_X, -1],
|
||||
[GamepadKey.LS_RIGHT]: [this.LEFT_STICK_X, 1],
|
||||
[GamepadKey.LS_UP]: [this.LEFT_STICK_Y, 1],
|
||||
[GamepadKey.LS_DOWN]: [this.LEFT_STICK_Y, -1],
|
||||
|
||||
[GamepadKey.RS_LEFT]: [this.RIGHT_STICK_X, 2, -1],
|
||||
[GamepadKey.RS_RIGHT]: [this.RIGHT_STICK_X, 2, 1],
|
||||
[GamepadKey.RS_UP]: [this.RIGHT_STICK_Y, 3, -1],
|
||||
[GamepadKey.RS_DOWN]: [this.RIGHT_STICK_Y, 3, 1],
|
||||
[GamepadKey.RS_LEFT]: [this.RIGHT_STICK_X, -1],
|
||||
[GamepadKey.RS_RIGHT]: [this.RIGHT_STICK_X, 1],
|
||||
[GamepadKey.RS_UP]: [this.RIGHT_STICK_Y, 1],
|
||||
[GamepadKey.RS_DOWN]: [this.RIGHT_STICK_Y, -1],
|
||||
};
|
||||
|
||||
private constructor() {
|
||||
@@ -205,11 +208,16 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
private getVirtualGamepad = () => this.VIRTUAL_GAMEPAD;
|
||||
|
||||
private updateStick(stick: GamepadStick, x: number, y: number) {
|
||||
const virtualGamepad = this.getVirtualGamepad();
|
||||
virtualGamepad.axes[stick * 2] = x;
|
||||
virtualGamepad.axes[stick * 2 + 1] = y;
|
||||
const gamepad = this.xCloudGamepad;
|
||||
if (stick === GamepadStick.LEFT) {
|
||||
gamepad.LeftThumbXAxis = x;
|
||||
gamepad.LeftThumbYAxis = -y;
|
||||
} else {
|
||||
gamepad.RightThumbXAxis = x;
|
||||
gamepad.RightThumbYAxis = -y;
|
||||
}
|
||||
|
||||
virtualGamepad.timestamp = performance.now();
|
||||
window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -224,29 +232,20 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
|
||||
private vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2);
|
||||
|
||||
private resetGamepad() {
|
||||
const gamepad = this.getVirtualGamepad();
|
||||
resetXcloudGamepads() {
|
||||
const index = getPref(PrefKey.MKB_P1_SLOT) - 1;
|
||||
|
||||
// Reset axes
|
||||
gamepad.axes = [0, 0, 0, 0];
|
||||
|
||||
// Reset buttons
|
||||
for (const button of gamepad.buttons) {
|
||||
button.pressed = false;
|
||||
button.value = 0;
|
||||
}
|
||||
|
||||
gamepad.timestamp = performance.now();
|
||||
this.xCloudGamepad = generateVirtualControllerMapping(0, {
|
||||
GamepadIndex: getPref(PrefKey.LOCAL_CO_OP_ENABLED) ? index : 0,
|
||||
Dirty: true,
|
||||
});
|
||||
this.VIRTUAL_GAMEPAD.index = index;
|
||||
}
|
||||
|
||||
private pressButton(buttonIndex: GamepadKey, pressed: boolean) {
|
||||
const virtualGamepad = this.getVirtualGamepad();
|
||||
|
||||
const xCloudKey = toXcloudGamepadKey(buttonIndex)!;
|
||||
if (buttonIndex >= 100) {
|
||||
let [valueArr, axisIndex] = this.STICK_MAP[buttonIndex]!;
|
||||
valueArr = valueArr as number[];
|
||||
axisIndex = axisIndex as number;
|
||||
|
||||
let [valueArr]: [GamepadKey[], number] = this.STICK_MAP[buttonIndex]!;
|
||||
// Remove old index of the array
|
||||
for (let i = valueArr.length - 1; i >= 0; i--) {
|
||||
if (valueArr[i] === buttonIndex) {
|
||||
@@ -259,18 +258,19 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
let value;
|
||||
if (valueArr.length) {
|
||||
// Get value of the last key of the axis
|
||||
value = this.STICK_MAP[valueArr[valueArr.length - 1]]![2] as number;
|
||||
value = this.STICK_MAP[valueArr[valueArr.length - 1]]![1] as number;
|
||||
} else {
|
||||
value = 0;
|
||||
}
|
||||
|
||||
virtualGamepad.axes[axisIndex] = value;
|
||||
// @ts-ignore
|
||||
this.xCloudGamepad[xCloudKey] = value;
|
||||
} else {
|
||||
virtualGamepad.buttons[buttonIndex].pressed = pressed;
|
||||
virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0;
|
||||
// @ts-ignore
|
||||
this.xCloudGamepad[xCloudKey] = pressed ? 1 : 0;
|
||||
}
|
||||
|
||||
virtualGamepad.timestamp = performance.now();
|
||||
window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);
|
||||
}
|
||||
|
||||
private onKeyboardEvent = (e: KeyboardEvent) => {
|
||||
@@ -453,7 +453,7 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
|
||||
refreshPresetData() {
|
||||
this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset;
|
||||
this.resetGamepad();
|
||||
this.resetXcloudGamepads();
|
||||
}
|
||||
|
||||
waitForMouseData(showPopup: boolean) {
|
||||
@@ -581,11 +581,6 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
|
||||
}
|
||||
|
||||
updateGamepadSlots() {
|
||||
// Set gamepad slot
|
||||
this.VIRTUAL_GAMEPAD.index = getPref(PrefKey.MKB_P1_SLOT) - 1;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.enabled) {
|
||||
this.enabled = true;
|
||||
@@ -595,8 +590,8 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
this.isPolling = true;
|
||||
this.escKeyDownTime = -1;
|
||||
|
||||
this.resetGamepad();
|
||||
this.updateGamepadSlots();
|
||||
window.BX_EXPOSED.toggleLocalCoOp(getPref(PrefKey.LOCAL_CO_OP_ENABLED));
|
||||
this.resetXcloudGamepads();
|
||||
window.navigator.getGamepads = this.patchedGetGamepads;
|
||||
|
||||
this.waitForMouseData(false);
|
||||
@@ -625,7 +620,7 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
const virtualGamepad = this.getVirtualGamepad();
|
||||
if (virtualGamepad.connected) {
|
||||
// Dispatch "gamepaddisconnected" event
|
||||
this.resetGamepad();
|
||||
this.resetXcloudGamepads();
|
||||
|
||||
virtualGamepad.connected = false;
|
||||
virtualGamepad.timestamp = performance.now();
|
||||
|
@@ -13,19 +13,7 @@ import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { NativeMkbMode } from "@/enums/pref-values";
|
||||
import { BxEventBus } from "@/utils/bx-event-bus";
|
||||
|
||||
type NativeMouseData = {
|
||||
X: number,
|
||||
Y: number,
|
||||
Buttons: number,
|
||||
WheelX: number,
|
||||
WheelY: number,
|
||||
Type?: 0, // 0: Relative, 1: Absolute
|
||||
}
|
||||
|
||||
type XcloudInputSink = {
|
||||
onMouseInput: (data: NativeMouseData) => void;
|
||||
}
|
||||
import type { NativeMouseData, XcloudInputChannel } from "@/utils/gamepad";
|
||||
|
||||
export class NativeMkbHandler extends MkbHandler {
|
||||
private static instance: NativeMkbHandler | null | undefined;
|
||||
@@ -54,7 +42,7 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
private mouseVerticalMultiply = 0;
|
||||
private mouseHorizontalMultiply = 0;
|
||||
|
||||
private inputSink: XcloudInputSink | undefined;
|
||||
private inputChannel: XcloudInputChannel | undefined;
|
||||
|
||||
private popup!: MkbPopup;
|
||||
|
||||
@@ -114,7 +102,7 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
|
||||
init() {
|
||||
this.pointerClient = PointerClient.getInstance();
|
||||
this.inputSink = window.BX_EXPOSED.inputSink;
|
||||
this.inputChannel = window.BX_EXPOSED.inputChannel;
|
||||
|
||||
// Stop keyboard input at startup
|
||||
this.updateInputConfigurationAsync(false);
|
||||
@@ -274,7 +262,7 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
|
||||
private sendMouseInput(data: NativeMouseData) {
|
||||
data.Type = 0; // Relative
|
||||
this.inputSink?.onMouseInput(data);
|
||||
this.inputChannel?.queueMouseInput(data);
|
||||
}
|
||||
|
||||
private resetMouseInput() {
|
||||
|
@@ -643,15 +643,14 @@ true` + text;
|
||||
return str;
|
||||
},
|
||||
|
||||
exposeInputSink(str: string) {
|
||||
let text = 'this.controlChannel=null,this.inputChannel=null';
|
||||
if (!str.includes(text)) {
|
||||
exposeInputChannel(str: string) {
|
||||
let index = str.indexOf('this.flushData=');
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newCode = 'window.BX_EXPOSED.inputSink = this;';
|
||||
|
||||
str = str.replace(text, newCode + text);
|
||||
const newCode = 'window.BX_EXPOSED.inputChannel = this,';
|
||||
str = PatcherUtils.insertAt(str, index, newCode);
|
||||
return str;
|
||||
},
|
||||
|
||||
@@ -1120,7 +1119,6 @@ ${subsVar} = subs;
|
||||
let PATCH_ORDERS = PatcherUtils.filterPatches([
|
||||
...(AppInterface && getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
|
||||
'enableNativeMkb',
|
||||
'exposeInputSink',
|
||||
'disableAbsoluteMouse',
|
||||
] : []),
|
||||
|
||||
@@ -1208,6 +1206,8 @@ let HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
|
||||
// TODO: check this
|
||||
// @ts-ignore
|
||||
let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
|
||||
'exposeInputChannel',
|
||||
|
||||
'patchXcloudTitleInfo',
|
||||
'disableGamepadDisconnectedScreen',
|
||||
'patchStreamHud',
|
||||
@@ -1377,6 +1377,7 @@ export class Patcher {
|
||||
|
||||
// Apply patched functions
|
||||
if (modified) {
|
||||
BX_FLAGS.Debug && console.time(LOG_TAG);
|
||||
try {
|
||||
chunkData[chunkId] = eval(patchedFuncStr);
|
||||
} catch (e: unknown) {
|
||||
@@ -1384,6 +1385,7 @@ export class Patcher {
|
||||
BxLogger.error(LOG_TAG, 'Error', appliedPatches, e.message, patchedFuncStr);
|
||||
}
|
||||
}
|
||||
BX_FLAGS.Debug && console.timeEnd(LOG_TAG);
|
||||
}
|
||||
|
||||
// Save to cache
|
||||
|
@@ -7,8 +7,8 @@ export class VirtualControllerShortcut {
|
||||
return;
|
||||
}
|
||||
|
||||
const released = generateVirtualControllerMapping();
|
||||
const pressed = generateVirtualControllerMapping({
|
||||
const released = generateVirtualControllerMapping(0);
|
||||
const pressed = generateVirtualControllerMapping(0, {
|
||||
Nexus: 1,
|
||||
VirtualPhysicality: 1024, // Home
|
||||
});
|
||||
|
@@ -69,7 +69,7 @@ export class MkbExtraSettings extends HTMLElement {
|
||||
createSettingRow(
|
||||
t('virtual-controller-slot'),
|
||||
SettingElement.fromPref(PrefKey.MKB_P1_SLOT, STORAGE.Global, () => {
|
||||
EmulatedMkbHandler.getInstance()?.updateGamepadSlots();
|
||||
EmulatedMkbHandler.getInstance()?.resetXcloudGamepads();
|
||||
}),
|
||||
),
|
||||
] : []),
|
||||
|
3
src/types/global.d.ts
vendored
3
src/types/global.d.ts
vendored
@@ -5,6 +5,7 @@ import type { StreamSettings, type StreamSettingsData } from "@/utils/stream-set
|
||||
import type { BxEvent } from "@/utils/bx-event";
|
||||
import type { BxEventBus } from "@/utils/bx-event-bus";
|
||||
import type { BxLogger } from "@/utils/bx-logger";
|
||||
import type { XcloudInputChannel } from "@/utils/gamepad";
|
||||
|
||||
export {};
|
||||
|
||||
@@ -20,7 +21,7 @@ declare global {
|
||||
closeAll: () => void;
|
||||
};
|
||||
showStreamMenu: () => void;
|
||||
inputSink: any;
|
||||
inputChannel: XcloudInputChannel | undefined;
|
||||
streamSession: any;
|
||||
touchLayoutManager: any;
|
||||
}>;
|
||||
|
@@ -4,7 +4,21 @@ import { Toast } from "@utils/toast";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
import { GamepadKeyName, type GamepadKey } from "@/enums/gamepad";
|
||||
import { GamepadKey, GamepadKeyName } from "@/enums/gamepad";
|
||||
|
||||
export type NativeMouseData = {
|
||||
X: number,
|
||||
Y: number,
|
||||
Buttons: number,
|
||||
WheelX: number,
|
||||
WheelY: number,
|
||||
Type?: 0, // 0: Relative, 1: Absolute
|
||||
}
|
||||
|
||||
export type XcloudInputChannel = {
|
||||
sendGamepadInput: (timestamp: number, gamepads: XcloudGamepad[]) => void;
|
||||
queueMouseInput: (data: NativeMouseData) => void;
|
||||
}
|
||||
|
||||
// Show a toast when connecting/disconecting controller
|
||||
export function showGamepadToast(gamepad: Gamepad) {
|
||||
@@ -59,9 +73,9 @@ export function hasGamepad() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function generateVirtualControllerMapping(override: {}={}) {
|
||||
export function generateVirtualControllerMapping(index: number, override: Partial<XcloudGamepad>={}) {
|
||||
const mapping = {
|
||||
GamepadIndex: 0,
|
||||
GamepadIndex: index,
|
||||
A: 0,
|
||||
B: 0,
|
||||
X: 0,
|
||||
@@ -95,3 +109,44 @@ export function generateVirtualControllerMapping(override: {}={}) {
|
||||
export function getGamepadPrompt(gamepadKey: GamepadKey): string {
|
||||
return GamepadKeyName[gamepadKey][1];
|
||||
}
|
||||
|
||||
const XCLOUD_GAMEPAD_KEY_MAPPING: { [key in GamepadKey]?: keyof XcloudGamepad } = {
|
||||
[GamepadKey.A]: 'A',
|
||||
[GamepadKey.B]: 'B',
|
||||
[GamepadKey.X]: 'X',
|
||||
[GamepadKey.Y]: 'Y',
|
||||
|
||||
[GamepadKey.UP]: 'DPadUp',
|
||||
[GamepadKey.RIGHT]: 'DPadRight',
|
||||
[GamepadKey.DOWN]: 'DPadDown',
|
||||
[GamepadKey.LEFT]: 'DPadLeft',
|
||||
|
||||
[GamepadKey.LB]: 'LeftShoulder',
|
||||
[GamepadKey.RB]: 'RightShoulder',
|
||||
[GamepadKey.LT]: 'LeftTrigger',
|
||||
[GamepadKey.RT]: 'RightTrigger',
|
||||
|
||||
[GamepadKey.L3]: 'LeftThumb',
|
||||
[GamepadKey.R3]: 'RightThumb',
|
||||
[GamepadKey.LS]: 'LeftStickAxes',
|
||||
[GamepadKey.RS]: 'RightStickAxes',
|
||||
|
||||
[GamepadKey.SELECT]: 'View',
|
||||
[GamepadKey.START]: 'Menu',
|
||||
[GamepadKey.HOME]: 'Nexus',
|
||||
[GamepadKey.SHARE]: 'Share',
|
||||
|
||||
[GamepadKey.LS_LEFT]: 'LeftThumbXAxis',
|
||||
[GamepadKey.LS_RIGHT]: 'LeftThumbXAxis',
|
||||
[GamepadKey.LS_UP]: 'LeftThumbYAxis',
|
||||
[GamepadKey.LS_DOWN]: 'LeftThumbYAxis',
|
||||
|
||||
[GamepadKey.RS_LEFT]: 'RightThumbXAxis',
|
||||
[GamepadKey.RS_RIGHT]: 'RightThumbXAxis',
|
||||
[GamepadKey.RS_UP]: 'RightThumbYAxis',
|
||||
[GamepadKey.RS_DOWN]: 'RightThumbYAxis',
|
||||
};
|
||||
|
||||
export function toXcloudGamepadKey(gamepadKey: GamepadKey) {
|
||||
return XCLOUD_GAMEPAD_KEY_MAPPING[gamepadKey];
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import type { ControllerCustomizationConvertedPresetData, ControllerCustomizatio
|
||||
import { STATES } from "./global";
|
||||
import { DeviceVibrationMode } from "@/enums/pref-values";
|
||||
import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler";
|
||||
import { hasGamepad } from "./gamepad";
|
||||
import { hasGamepad, toXcloudGamepadKey } from "./gamepad";
|
||||
import { MkbMappingPresetsTable } from "./local-db/mkb-mapping-presets-table";
|
||||
import { GamepadKey } from "@/enums/gamepad";
|
||||
import { MkbPresetKey, MouseConstant } from "@/enums/mkb";
|
||||
@@ -51,32 +51,6 @@ export class StreamSettings {
|
||||
keyboardShortcuts: {},
|
||||
};
|
||||
|
||||
private static CONTROLLER_CUSTOMIZATION_MAPPING: { [key in GamepadKey]?: keyof XcloudGamepad } = {
|
||||
[GamepadKey.A]: 'A',
|
||||
[GamepadKey.B]: 'B',
|
||||
[GamepadKey.X]: 'X',
|
||||
[GamepadKey.Y]: 'Y',
|
||||
|
||||
[GamepadKey.UP]: 'DPadUp',
|
||||
[GamepadKey.RIGHT]: 'DPadRight',
|
||||
[GamepadKey.DOWN]: 'DPadDown',
|
||||
[GamepadKey.LEFT]: 'DPadLeft',
|
||||
|
||||
[GamepadKey.LB]: 'LeftShoulder',
|
||||
[GamepadKey.RB]: 'RightShoulder',
|
||||
[GamepadKey.LT]: 'LeftTrigger',
|
||||
[GamepadKey.RT]: 'RightTrigger',
|
||||
|
||||
[GamepadKey.L3]: 'LeftThumb',
|
||||
[GamepadKey.R3]: 'RightThumb',
|
||||
[GamepadKey.LS]: 'LeftStickAxes',
|
||||
[GamepadKey.RS]: 'RightStickAxes',
|
||||
|
||||
[GamepadKey.SELECT]: 'View',
|
||||
[GamepadKey.START]: 'Menu',
|
||||
[GamepadKey.SHARE]: 'Share',
|
||||
};
|
||||
|
||||
static getPref<T extends keyof PrefTypeMap>(key: T) {
|
||||
return getPref<T>(key);
|
||||
}
|
||||
@@ -146,14 +120,14 @@ export class StreamSettings {
|
||||
// Swap GamepadKey.A with "A"
|
||||
let gamepadKey: unknown;
|
||||
for (gamepadKey in customization.mapping) {
|
||||
const gamepadStr = StreamSettings.CONTROLLER_CUSTOMIZATION_MAPPING[gamepadKey as GamepadKey];
|
||||
const gamepadStr = toXcloudGamepadKey(gamepadKey as GamepadKey);
|
||||
if (!gamepadStr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mappedKey = customization.mapping[gamepadKey as GamepadKey];
|
||||
if (typeof mappedKey === 'number') {
|
||||
converted.mapping[gamepadStr] = StreamSettings.CONTROLLER_CUSTOMIZATION_MAPPING[mappedKey as GamepadKey];
|
||||
converted.mapping[gamepadStr] = toXcloudGamepadKey(mappedKey as GamepadKey);
|
||||
} else {
|
||||
converted.mapping[gamepadStr] = false;
|
||||
}
|
||||
|
Reference in New Issue
Block a user