Skip to content

Poll gamepads every frame without allocating - #9470

Merged
willeastcott merged 2 commits into
mainfrom
gamepad-update-no-alloc
Sep 20, 2026
Merged

willeastcott merged 2 commits into
mainfrom
gamepad-update-no-alloc

Conversation

@willeastcott

Copy link
Copy Markdown
Contributor

Fixes #842. Supersedes #8203.

GamePads#update() called poll() with no argument, so the default pads = [] allocated a new array every frame, which is what #842 reported (the original poll() allocation was fixed in #1154, but the per-frame default parameter was left behind).

Changes

  • GamePads#update() no longer allocates. The device walk lives in a private _poll(pads) that only collects results when the caller passes an array. poll(pads = []) keeps its public signature and return value.
  • GamePad#update() copies axes by index instead of previousAxes.push(...axes) / axes.push(...gamepad.axes), so no iterator or temporary array is created per frame. The previous axes are snapshotted at the old length before the new length is adopted (the case @mvaligursky flagged in Fixed unecessary axes array reallocation in GamePad update - Issue (#… #8203).
  • findByIndex() uses a loop instead of allocating a closure for Array#find on every polled device.
  • The Gamepad API is resolved per call rather than bound at module load. Cost is one property lookup per frame; the benefit is that the polling path can now be tested under Node by installing navigator.getGamepads before constructing GamePads.
  • Bug fix found by the new tests: GamePadButton guarded the previous state with if (previous), so a numeric previous value of 0 (a stick at rest) was treated as "no previous state". Synthesized axis-driven buttons (PAD_UP/PAD_DOWN/PAD_LEFT/PAD_RIGHT on the dual-stick map, or any updateMap with synthesizedButtons) therefore never reported wasPressed or wasTouched. The guard is now previous !== undefined.

Tests

New test/platform/input/game-pads.test.mjs (11 tests) covers poll() with and without a target array, instance reuse across polls, update() picking up new devices and tracking button edges over frames, synthesized-button edges through the previous-axes bookkeeping (including axis-count changes in both directions), and the GamePadButton previous-value cases.

Before the GamePadButton fix, the two synthesized-button wasPressed tests fail.

🤖 Generated with Claude Code

GamePads#update() called poll() with no argument, so the default `pads = []`
allocated a fresh array every frame. update() now walks the devices directly
through a shared private _poll() that only collects into an array when the
caller supplies one; poll() keeps its public signature and return value.

Also remove the remaining per-frame garbage on the polling path: GamePad#update
copies the axes by index instead of spreading into push(), and findByIndex()
uses a loop instead of allocating a closure for Array#find.

The Gamepad API is now resolved on each call rather than bound at module load,
so the polling path can be exercised under Node by installing
navigator.getGamepads before constructing GamePads.

Fix GamePadButton treating a numeric previous value of 0 as "no previous
state", which meant synthesized (axis-driven) buttons never reported wasPressed
or wasTouched from the resting position.

Fixes #842

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Build size report

This PR changes the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2472.1 KB (+120 B, +0.00%) 637.0 KB (+45 B, +0.01%) 494.8 KB (+317 B, +0.06%)
playcanvas.min.mjs 2469.4 KB (+120 B, +0.00%) 635.6 KB (+51 B, +0.01%) 494.1 KB (+123 B, +0.02%)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The test suite has a Node 18 setup issue, and the support flag can become inconsistent with the per-call API lookup.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Optimizes gamepad polling to avoid per-frame allocations and fixes synthesized-button edge tracking.

Changes:

  • Reuses polling state and preserves the public poll() API.
  • Copies axes without iterator or temporary-array allocations.
  • Fixes numeric previous-state handling and adds regression tests.
File Summary
test/​platform/​input/​game-pads.test.mjs Adds coverage for polling reuse, axis changes, and button edges; test setup must install the JSDOM navigator on globalThis.
src/​platform/​input/​game-pads.js Optimizes polling and state updates; gamepadsSupported may remain stale when the API becomes available after construction.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread test/platform/input/game-pads.test.mjs Outdated
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@willeastcott
willeastcott merged commit 19ca436 into main Sep 20, 2026
10 checks passed
@willeastcott
willeastcott deleted the gamepad-update-no-alloc branch September 20, 2026 19:46

This branch was successfully deployed

2 active deployments
Preview – engine — afc45752 Deployed Sep 20, 2026 by vercel[bot]
Preview – engine-api-docs — afc45752 Deployed Sep 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: input Input related issue bugfix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

game-pads.js allocates a new array every frame

2 participants