diff --git a/web/js/input/input.js b/web/js/input/input.js index a636c6ab..2c3fffa4 100644 --- a/web/js/input/input.js +++ b/web/js/input/input.js @@ -5,7 +5,7 @@ import { sub } from 'event'; -export {KEY} from './keys.js?v=3'; +export {KEY, JOYPAD_KEYS} from './keys.js?v=3'; import {joystick} from './joystick.js?v=3'; import {keyboard} from './keyboard.js?v=3' @@ -44,7 +44,7 @@ export const input = { toggle(on = true) { if (on === input_state.retropad) return input_state.retropad = on - on ? retropad.enable() : retropad.disable() + retropad.toggle(on) } }, set kbm(v) { diff --git a/web/js/input/keys.js b/web/js/input/keys.js index 4406823e..60e45e3e 100644 --- a/web/js/input/keys.js +++ b/web/js/input/keys.js @@ -30,4 +30,12 @@ export const KEY = { R3: 'r3', REC: 'rec', RESET: 'reset', -} +}; + +// Keys match libretro RETRO_DEVICE_ID_JOYPAD_* +export const JOYPAD_KEYS = [ + KEY.B, KEY.Y, KEY.SELECT, KEY.START, + KEY.UP, KEY.DOWN, KEY.LEFT, KEY.RIGHT, + KEY.A, KEY.X, KEY.L, KEY.R, + KEY.L2, KEY.R2, KEY.L3, KEY.R3 +] diff --git a/web/js/input/retropad.js b/web/js/input/retropad.js index 2ecbd659..0e7026ee 100644 --- a/web/js/input/retropad.js +++ b/web/js/input/retropad.js @@ -1,101 +1,64 @@ -import { - pub, - CONTROLLER_UPDATED -} from 'event'; -import {KEY} from 'input' -import {log} from 'log'; +import {pub, CONTROLLER_UPDATED} from 'event'; +import {JOYPAD_KEYS} from 'input'; -const pollingIntervalMs = 5; -let controllerChangedIndex = -1; - -// Libretro config -let controllerState = { - [KEY.B]: false, - [KEY.Y]: false, - [KEY.SELECT]: false, - [KEY.START]: false, - [KEY.UP]: false, - [KEY.DOWN]: false, - [KEY.LEFT]: false, - [KEY.RIGHT]: false, - [KEY.A]: false, - [KEY.X]: false, - // extra - [KEY.L]: false, - [KEY.R]: false, - [KEY.L2]: false, - [KEY.R2]: false, - [KEY.L3]: false, - [KEY.R3]: false -}; - -const poll = (intervalMs, callback) => { - let _ticker = 0; - return { - enable: () => { - if (_ticker > 0) return; - log.debug(`[input] poll set to ${intervalMs}ms`); - _ticker = setInterval(callback, intervalMs) - }, - disable: () => { - if (_ticker < 1) return; - log.debug('[input] poll has been disabled'); - clearInterval(_ticker); - _ticker = 0; - } - } -}; - -const controllerEncoded = [0, 0, 0, 0, 0]; -const keys = Object.keys(controllerState); - -const sendControllerState = () => { - if (controllerChangedIndex >= 0) { - const state = _getState(); - pub(CONTROLLER_UPDATED, _encodeState(state)); - controllerChangedIndex = -1; - } -}; - -const setKeyState = (name, state) => { - if (controllerState[name] !== undefined) { - controllerState[name] = state; - controllerChangedIndex = Math.max(controllerChangedIndex, 0); - } -}; - -const setAxisChanged = (index, value) => { - if (controllerEncoded[index + 1] !== undefined) { - controllerEncoded[index + 1] = Math.floor(32767 * value); - controllerChangedIndex = Math.max(controllerChangedIndex, index + 1); - } -}; - -/** - * Converts key state into a bitmap and prepends it to the axes state. +/* + * [BUTTONS, LEFT_X, LEFT_Y, RIGHT_X, RIGHT_Y] * - * @returns {Uint16Array} The controller state. - * First uint16 is the controller state bitmap. - * The other uint16 are the axes values. - * Truncated to the last value changed. - * - * @private + * Buttons are packed into a 16-bit bitmask where each bit is one button. + * Axes are signed 16-bit values ranging from -32768 to 32767. + * The whole thing is 10 bytes when sent over the wire. */ -const _encodeState = (state) => new Uint16Array(state) +const state = new Int16Array(5); +let buttons = 0; +let dirty = false; +let rafId = 0; -const _getState = () => { - controllerEncoded[0] = 0; - for (let i = 0, len = keys.length; i < len; i++) { - controllerEncoded[0] += controllerState[keys[i]] ? 1 << i : 0; +/* + * Polls controller state using requestAnimationFrame which gives us + * ~60Hz update rate that syncs with the display. As a bonus, + * it automatically pauses when the tab goes to background. + * We only send data when something actually changed. + */ +const poll = () => { + if (dirty) { + state[0] = buttons; + pub(CONTROLLER_UPDATED, new Uint16Array(state.buffer)); + dirty = false; } - return controllerEncoded.slice(0, controllerChangedIndex + 1); -} + rafId = requestAnimationFrame(poll); +}; -const _poll = poll(pollingIntervalMs, sendControllerState) +/* + * Toggles a button on or off in the bitmask. The button's position + * in JOYPAD_KEYS determines which bit gets flipped. For example, + * if A is at index 8, pressing it sets bit 8. + */ +const setKeyState = (key, pressed) => { + const idx = JOYPAD_KEYS.indexOf(key); + if (idx < 0) return; -export const retropad = { - enable: () => _poll.enable(), - disable: () => _poll.disable(), - setKeyState, - setAxisChanged, -} + const prev = buttons; + buttons = pressed ? buttons | (1 << idx) : buttons & ~(1 << idx); + dirty ||= buttons !== prev; +}; + +/* + * Updates an analog stick axis. Axes 0-1 are the left stick (X and Y), + * axes 2-3 are the right stick. Input should be a float from -1 to 1 + * which gets converted to a signed 16-bit integer for transmission. + */ +const setAxisChanged = (axis, value) => { + if (axis < 0 || axis > 3) return; + + const v = Math.trunc(Math.max(-1, Math.min(1, value)) * 32767); + dirty ||= state[++axis] !== v; + state[axis] = v; +}; + +// Starts or stops the polling loop +const toggle = (on) => { + if (on === !!rafId) return; + rafId = on ? requestAnimationFrame(poll) : (cancelAnimationFrame(rafId), 0); +}; + +export const retropad = {toggle, setKeyState, setAxisChanged}; \ No newline at end of file