fix: ditched sdl and moved to xinput for windows for less ram usage
This commit is contained in:
parent
90d6711935
commit
dc0f2d150a
18 changed files with 663 additions and 100 deletions
|
|
@ -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,
|
||||
|
|
|
|||
40
src/bun/api/controls/controls.ts
Normal file
40
src/bun/api/controls/controls.ts
Normal 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);
|
||||
}
|
||||
32
src/bun/api/controls/gamepad.ts
Normal file
32
src/bun/api/controls/gamepad.ts
Normal 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?.();
|
||||
}
|
||||
}
|
||||
87
src/bun/api/controls/linux.ts
Normal file
87
src/bun/api/controls/linux.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
55
src/bun/api/controls/manager.ts
Normal file
55
src/bun/api/controls/manager.ts
Normal 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?.();
|
||||
}
|
||||
}
|
||||
38
src/bun/api/controls/types.ts
Normal file
38
src/bun/api/controls/types.ts
Normal 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;
|
||||
}
|
||||
57
src/bun/api/controls/windows.ts
Normal file
57
src/bun/api/controls/windows.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { killBrowser, spawnBrowser } from './utils/browser-spawner';
|
|||
import { BrowserParams, BuildParams } from './utils/browser-params';
|
||||
import os from 'node:os';
|
||||
import { EventEmitter } from 'node:stream';
|
||||
import { dlopen, FFIType } from "bun:ffi";
|
||||
import { dlopen, FFIType, Pointer } from "bun:ffi";
|
||||
|
||||
export default async function init (events: EventEmitter, forceBrowser: boolean, params: BrowserParams)
|
||||
{
|
||||
|
|
@ -21,6 +21,29 @@ export default async function init (events: EventEmitter, forceBrowser: boolean,
|
|||
}
|
||||
}
|
||||
|
||||
function focusWindow (id: Pointer)
|
||||
{
|
||||
if (process.platform === 'win32')
|
||||
{
|
||||
const user32 = dlopen("user32.dll", {
|
||||
SetForegroundWindow: { args: [FFIType.ptr], returns: FFIType.bool },
|
||||
ShowWindow: { args: [FFIType.ptr, FFIType.i32], returns: FFIType.bool },
|
||||
BringWindowToTop: { args: [FFIType.ptr], returns: FFIType.bool },
|
||||
keybd_event: { args: [FFIType.u8, FFIType.u8, FFIType.u32, FFIType.ptr], returns: FFIType.void },
|
||||
});
|
||||
|
||||
const SW_RESTORE = 9;
|
||||
|
||||
if (id)
|
||||
{
|
||||
user32.symbols.ShowWindow(id, SW_RESTORE);
|
||||
user32.symbols.keybd_event(0, 0, 0, null); // fake input event
|
||||
user32.symbols.BringWindowToTop(id);
|
||||
user32.symbols.SetForegroundWindow(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runWebview (events: EventEmitter, params: BrowserParams)
|
||||
{
|
||||
const webviewPath = process.env.IS_BINARY ? `./webview/${os.platform()}` : new URL(`./webview/${os.platform()}`, import.meta.url).href;
|
||||
|
|
@ -73,25 +96,7 @@ async function runWebview (events: EventEmitter, params: BrowserParams)
|
|||
events.on('exitapp', handleExit);
|
||||
events.on('focus', () =>
|
||||
{
|
||||
if (process.platform === 'win32')
|
||||
{
|
||||
const user32 = dlopen("user32.dll", {
|
||||
SetForegroundWindow: { args: [FFIType.ptr], returns: FFIType.bool },
|
||||
ShowWindow: { args: [FFIType.ptr, FFIType.i32], returns: FFIType.bool },
|
||||
BringWindowToTop: { args: [FFIType.ptr], returns: FFIType.bool },
|
||||
keybd_event: { args: [FFIType.u8, FFIType.u8, FFIType.u32, FFIType.ptr], returns: FFIType.void },
|
||||
});
|
||||
|
||||
const SW_RESTORE = 9;
|
||||
|
||||
if (pointer)
|
||||
{
|
||||
user32.symbols.ShowWindow(pointer, SW_RESTORE);
|
||||
user32.symbols.keybd_event(0, 0, 0, null); // fake input event
|
||||
user32.symbols.BringWindowToTop(pointer);
|
||||
user32.symbols.SetForegroundWindow(pointer);
|
||||
}
|
||||
}
|
||||
focusWindow(pointer);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
|
||||
|
||||
import { LaunchGameJob } from './api/jobs/launch-game-job';
|
||||
import { events, taskQueue } from './api/app';
|
||||
|
||||
process.env.SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS = "1";
|
||||
process.env.SDL_JOYSTICK_THREAD = "1";
|
||||
|
||||
export default async function Initialize ()
|
||||
{
|
||||
const { default: sdl } = await import('@kmamal/sdl');
|
||||
const launcherWin = sdl.video.createWindow({ title: "Launcher", visible: false });
|
||||
|
||||
sdl.controller.devices.forEach(d => connectToController(d));
|
||||
sdl.controller.on('deviceAdd', e =>
|
||||
{
|
||||
connectToController(e.device);
|
||||
});
|
||||
|
||||
function connectToController (device: any)
|
||||
{
|
||||
let selectHeld = false;
|
||||
const ctrl = sdl.controller.openDevice(device);
|
||||
console.log("Connected to", device.name);
|
||||
|
||||
ctrl.on("buttonDown", ({ button }) =>
|
||||
{
|
||||
if (button === "back") selectHeld = true;
|
||||
if (button === "start" && selectHeld)
|
||||
{
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
if (button === 'guide')
|
||||
{
|
||||
events.emit('focus');
|
||||
}
|
||||
});
|
||||
|
||||
ctrl.on("buttonUp", ({ button }) =>
|
||||
{
|
||||
if (button === "back") selectHeld = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ if (process.env.HEADLESS)
|
|||
});
|
||||
} else
|
||||
{
|
||||
await init(app.events, Bun.env.FORCE_BROWSER === "true", {
|
||||
await init(app.events, process.env.FORCE_BROWSER === "true", {
|
||||
configPath: dirname(app.config.path),
|
||||
windowPosition: app.config.get('windowPosition'),
|
||||
windowSize: app.config.get('windowSize'),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { ChildProcessWithoutNullStreams } from "node:child_process";
|
|||
import os from 'node:os';
|
||||
|
||||
export type RunBrowserType = "chrome" | "chromium" | "firefox" | "edge";
|
||||
export type RunBrowserSource = "running" | "system" | "flatpak";
|
||||
export type RunBrowserSource = "running" | "system" | "flatpak" | "bundled";
|
||||
|
||||
/**
|
||||
* Options for spawning a browser process.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { spawnSync } from "bun";
|
||||
import { platform } from "node:os";
|
||||
import { RunBrowserType } from "./browser-spawner";
|
||||
import path from 'node:path';
|
||||
|
||||
export type GetBrowserType = "chrome" | "chromium" | "firefox";
|
||||
export type GetBrowserSource = "running" | "system" | "flatpak";
|
||||
export type GetBrowserSource = "running" | "system" | "flatpak" | "bundled";
|
||||
|
||||
/**
|
||||
* Browser discovery priority configuration
|
||||
|
|
@ -12,6 +13,7 @@ interface BrowserPriorityConfig
|
|||
{
|
||||
/** Include currently running browser processes in search */
|
||||
includeRunning?: boolean;
|
||||
includeBundled?: boolean;
|
||||
/** Browser types to search for, in priority order */
|
||||
browserOrder?: GetBrowserType[];
|
||||
/** Include system default browser on Windows */
|
||||
|
|
@ -33,6 +35,27 @@ interface BrowserResult
|
|||
source: GetBrowserSource;
|
||||
}
|
||||
|
||||
const PLATFORM_MAP: Record<string, string> = {
|
||||
linux: "linux",
|
||||
win32: "windows",
|
||||
darwin: 'macos'
|
||||
};
|
||||
|
||||
const ARCH_MAP: Record<string, Record<string, string>> = {
|
||||
linux: { x64: "x86_64", arm64: "arm64" },
|
||||
darwin: { x64: "x86_64", arm64: "arm64" },
|
||||
win32: { x64: "x64", arm64: "arm64" },
|
||||
};
|
||||
|
||||
/** The expected binary path per platform after extraction */
|
||||
function getBundledBinaryPath (outDir: string, version: string, platform: string, arch: string): string
|
||||
{
|
||||
const subFolder = `ungoogled-chromium_${version}_${PLATFORM_MAP[platform]}_${ARCH_MAP[platform][arch]}`;
|
||||
if (platform === "linux") return path.join(outDir, subFolder, "chrome");
|
||||
if (platform === "darwin") return path.join(outDir, "Chromium.app");
|
||||
return path.join(outDir, subFolder, "chrome.exe");
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to find a valid browser executable.
|
||||
*
|
||||
|
|
@ -63,6 +86,7 @@ export async function getBrowserPath (config?: BrowserPriorityConfig): Promise<B
|
|||
// Default configuration
|
||||
const {
|
||||
includeRunning = true,
|
||||
includeBundled = true,
|
||||
browserOrder = ["firefox", "chrome", "chromium"],
|
||||
includeSystemDefault = true,
|
||||
includeFlatpak = true
|
||||
|
|
@ -70,6 +94,17 @@ export async function getBrowserPath (config?: BrowserPriorityConfig): Promise<B
|
|||
|
||||
const currentPlatform = platform();
|
||||
|
||||
// Check bundled
|
||||
if (includeBundled)
|
||||
{
|
||||
const getVerstion = await Bun.file('./bin/chromium/.chromium-version').text();
|
||||
const binPath = getBundledBinaryPath("./bin/chromium", getVerstion, process.platform, process.arch);
|
||||
if (await Bun.file(binPath).exists())
|
||||
{
|
||||
return { path: binPath, type: "chromium", source: "bundled" };
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Check for currently running browser process
|
||||
if (includeRunning)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue