fix: ditched sdl and moved to xinput for windows for less ram usage

This commit is contained in:
Simeon Radivoev 2026-03-30 02:02:12 +03:00
parent 90d6711935
commit dc0f2d150a
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
18 changed files with 663 additions and 100 deletions

View file

@ -22,7 +22,7 @@ import UpdateStoreJob from "./jobs/update-store";
import { getStoreFolder } from "./store/services/gamesService";
import { PluginManager } from "./plugins/plugin-manager";
import registerPlugins from "./plugins/register-plugins";
import controls from '../controls';
import controls from './controls/controls';
export const config = new Conf<SettingsType>({
projectName: projectPackage.name,

View file

@ -0,0 +1,40 @@
import { LaunchGameJob } from '../jobs/launch-game-job';
import { events, taskQueue } from '../app';
import { GamepadManager } from './manager';
export default async function Initialize ()
{
let startSelectPressed = false;
const manager = new GamepadManager();
setInterval(() =>
{
for (const pad of manager.getGamepads())
{
const state = pad.update();
if (!state) continue;
if (state.buttons.START && state.buttons.SELECT)
{
if (!startSelectPressed)
{
startSelectPressed = true;
console.log("Focus");
const launchGameTask = taskQueue.findJob(LaunchGameJob.id, LaunchGameJob);
if (launchGameTask)
{
launchGameTask.abort('exit');
taskQueue.waitForJob(LaunchGameJob.id).then(() => setTimeout(() => events.emit('focus'), 300));
} else
{
events.emit('focus');
}
}
} else
{
startSelectPressed = false;
}
}
}, 100);
}

View file

@ -0,0 +1,32 @@
// ./gamepad/index.ts
import { platform } from "os";
import { GamepadWindows } from "./windows";
import { GamepadLinux } from "./linux";
import type { IGamepadBackend, GamepadState } from "./types";
export class Gamepad
{
private backend: IGamepadBackend;
constructor(index = 0)
{
if (platform() === "win32")
{
this.backend = new GamepadWindows(index);
} else
{
this.backend = new GamepadLinux(index);
}
}
update (): GamepadState | null
{
return this.backend.update();
}
close ()
{
this.backend.close?.();
}
}

View file

@ -0,0 +1,87 @@
import { IGamepadBackend, GamepadState, ButtonName } from "./types";
import { openSync, readSync, closeSync, readdirSync } from "fs";
export class GamepadLinux implements IGamepadBackend
{
private fd: number;
private buttons: boolean[];
private axes: number[];
private buttonsCount = 16;
private axesCount = 4;
constructor(index = 0)
{
const devices = readdirSync("/dev/input").filter(f => f.startsWith("js"));
if (!devices[index]) throw new Error("No gamepad found");
const path = `/dev/input/${devices[index]}`;
this.fd = openSync(path, "r");
this.buttons = Array(this.buttonsCount).fill(false);
this.axes = Array(this.axesCount).fill(0);
}
update (): GamepadState | null
{
const buf = Buffer.alloc(8);
let bytesRead;
try
{
bytesRead = readSync(this.fd, buf, 0, 8, null);
} catch
{
return null;
}
if (bytesRead !== 8) return null;
const [time, value, type, number] = [
buf.readUInt32LE(0),
buf.readInt16LE(4),
buf[6],
buf[7],
];
if (type === 1) this.buttons[number] = value !== 0;
else if (type === 2 && number < 4) this.axes[number] = value / 32767;
const btnMap: Record<ButtonName, boolean> = {
A: this.buttons[0] ?? false,
B: this.buttons[1] ?? false,
X: this.buttons[2] ?? false,
Y: this.buttons[3] ?? false,
UP: this.buttons[4] ?? false,
DOWN: this.buttons[5] ?? false,
LEFT: this.buttons[6] ?? false,
RIGHT: this.buttons[7] ?? false,
LB: this.buttons[8] ?? false,
RB: this.buttons[9] ?? false,
START: this.buttons[10] ?? false,
SELECT: this.buttons[11] ?? false,
L3: this.buttons[12] ?? false,
R3: this.buttons[13] ?? false,
};
return {
buttons: btnMap,
leftStick: { x: this.axes[0] ?? 0, y: this.axes[1] ?? 0 },
rightStick: { x: this.axes[2] ?? 0, y: this.axes[3] ?? 0 },
triggers: { left: 0, right: 0 },
};
}
isConnected ()
{
try
{
readSync(this.fd, Buffer.alloc(1), 0, 1, null);
return true;
} catch
{
return false; // file disappeared or read failed
}
}
close ()
{
closeSync(this.fd);
}
}

View file

@ -0,0 +1,55 @@
import { Gamepad } from "./gamepad";
import { platform } from "os";
export class GamepadManager
{
private gamepads: Gamepad[] = [];
private scanInterval: any;
constructor()
{
this.scanGamepads();
// scan every second for new/disconnected devices
this.scanInterval = setInterval(() => this.scanGamepads(), 1000);
}
private scanGamepads ()
{
const max = platform() === "win32" ? 4 : 8; // max controllers
for (let i = 0; i < max; i++)
{
if (!this.gamepads[i])
{
try
{
const pad = new Gamepad(i);
if (pad.update())
{
this.gamepads[i] = pad;
console.log(`Gamepad ${i} connected`);
}
} catch { }
} else
{
const connected = this.gamepads[i].update() !== null;
if (!connected)
{
console.log(`Gamepad ${i} disconnected`);
this.gamepads[i].close();
delete this.gamepads[i];
}
}
}
}
getGamepads ()
{
return this.gamepads.filter(Boolean);
}
stop ()
{
clearInterval(this.scanInterval);
for (const pad of this.gamepads) pad.close?.();
}
}

View file

@ -0,0 +1,38 @@
export type ButtonName =
| "A" | "B" | "X" | "Y"
| "UP" | "DOWN" | "LEFT" | "RIGHT"
| "LB" | "RB"
| "START" | "SELECT"
| "L3" | "R3";
export interface Stick
{
x: number; // -1 → 1
y: number; // -1 → 1
}
export interface Triggers
{
left: number; // 0 → 1
right: number; // 0 → 1
}
export interface GamepadState
{
buttons: Record<ButtonName, boolean>;
leftStick: Stick;
rightStick: Stick;
triggers: Triggers;
}
export interface IGamepadBackend
{
/** Polls the current state; returns null if disconnected */
update (): GamepadState | null;
/** Optional: release resources (like closing fd on Linux) */
close?(): void;
/** Optional: check if the gamepad is still connected */
isConnected?(): boolean;
}

View file

@ -0,0 +1,57 @@
import { IGamepadBackend, GamepadState, ButtonName } from "./types";
import { dlopen, FFIType } from "bun:ffi";
const xinput = dlopen("xinput1_4.dll", {
XInputGetState: { args: [FFIType.u32, FFIType.ptr], returns: FFIType.u32 },
});
const ERROR_SUCCESS = 0;
export class GamepadWindows implements IGamepadBackend
{
private index: number;
private buffer = new ArrayBuffer(16);
private view = new DataView(this.buffer);
private prevButtons = 0;
private currButtons = 0;
constructor(index = 0) { this.index = index; }
update (): GamepadState | null
{
const res = xinput.symbols.XInputGetState(this.index, this.buffer);
if (res !== ERROR_SUCCESS) return null;
this.prevButtons = this.currButtons;
this.currButtons = this.view.getUint16(4, true);
const btns: Record<ButtonName, boolean> = {
A: (this.currButtons & 0x1000) !== 0,
B: (this.currButtons & 0x2000) !== 0,
X: (this.currButtons & 0x4000) !== 0,
Y: (this.currButtons & 0x8000) !== 0,
UP: (this.currButtons & 0x0001) !== 0,
DOWN: (this.currButtons & 0x0002) !== 0,
LEFT: (this.currButtons & 0x0004) !== 0,
RIGHT: (this.currButtons & 0x0008) !== 0,
LB: (this.currButtons & 0x0100) !== 0,
RB: (this.currButtons & 0x0200) !== 0,
START: (this.currButtons & 0x0010) !== 0,
SELECT: (this.currButtons & 0x0020) !== 0,
L3: (this.currButtons & 0x0040) !== 0,
R3: (this.currButtons & 0x0080) !== 0,
};
return {
buttons: btns,
leftStick: { x: this.view.getInt16(6, true) / 32767, y: this.view.getInt16(8, true) / 32767 },
rightStick: { x: this.view.getInt16(10, true) / 32767, y: this.view.getInt16(12, true) / 32767 },
triggers: { left: this.view.getUint8(14) / 255, right: this.view.getUint8(15) / 255 },
};
}
isConnected ()
{
const res = xinput.symbols.XInputGetState(this.index, this.buffer);
return res === ERROR_SUCCESS;
}
}