From b51cf7db84df752f7541615c539674c517e31f61 Mon Sep 17 00:00:00 2001 From: Rafael Araujo Lehmkuhl Date: Fri, 6 Feb 2026 11:10:03 -0300 Subject: [PATCH 1/5] Allow initial axis calibration routine to be bypassed --- .../controller/controller-instance.js | 5 ++-- src/javascript/controller/index.js | 2 +- src/javascript/joystick/index.js | 2 +- src/javascript/joystick/joystick-instance.js | 5 ++-- src/native/controller.cpp | 12 +++++++- src/native/joystick.cpp | 29 +++++++++++++++---- src/native/joystick.h | 2 ++ src/types/index.d.ts | 22 ++++++++++++-- 8 files changed, 64 insertions(+), 15 deletions(-) diff --git a/src/javascript/controller/controller-instance.js b/src/javascript/controller/controller-instance.js index 5ddb5a1..0046b3c 100644 --- a/src/javascript/controller/controller-instance.js +++ b/src/javascript/controller/controller-instance.js @@ -13,12 +13,13 @@ const validEvents = [ ] class ControllerInstance extends EventsViaPoll { - constructor (device) { + constructor (device, options = {}) { super(validEvents) if (!Globals.controllerDevices.includes(device)) { throw Object.assign(new Error("invalid device"), { device }) } - const result = Bindings.controller_open(device._index) + const { rawAxisMode = false } = options + const result = Bindings.controller_open(device._index, rawAxisMode) this._firmwareVersion = result.firmwareVersion this._serialNumber = result.serialNumber diff --git a/src/javascript/controller/index.js b/src/javascript/controller/index.js index c84cf76..00b6176 100644 --- a/src/javascript/controller/index.js +++ b/src/javascript/controller/index.js @@ -13,7 +13,7 @@ const controller = new class extends EventsViaPoll { return Globals.controllerDevices } - openDevice (device) { return new ControllerInstance(device) } + openDevice (device, options = {}) { return new ControllerInstance(device, options) } addMappings (mappings) { if (!Array.isArray(mappings)) { throw Object.assign(new Error("mappings must be an array"), { mappings }) } diff --git a/src/javascript/joystick/index.js b/src/javascript/joystick/index.js index 84ab7bf..25cc548 100644 --- a/src/javascript/joystick/index.js +++ b/src/javascript/joystick/index.js @@ -28,7 +28,7 @@ const joystick = new class extends EventsViaPoll { return Globals.joystickDevices } - openDevice (device) { return new JoystickInstance(device) } + openDevice (device, options = {}) { return new JoystickInstance(device, options) } }() module.exports = { joystick } diff --git a/src/javascript/joystick/joystick-instance.js b/src/javascript/joystick/joystick-instance.js index 7b633f6..20fa548 100644 --- a/src/javascript/joystick/joystick-instance.js +++ b/src/javascript/joystick/joystick-instance.js @@ -13,12 +13,13 @@ const validEvents = [ ] class JoystickInstance extends EventsViaPoll { - constructor (device) { + constructor (device, options = {}) { super(validEvents) if (!Globals.joystickDevices.includes(device)) { throw Object.assign(new Error("invalid device"), { device }) } - const result = Bindings.joystick_open(device._index) + const { rawAxisMode = false } = options + const result = Bindings.joystick_open(device._index, rawAxisMode) this._firmwareVersion = result.firmwareVersion this._serialNumber = result.serialNumber diff --git a/src/native/controller.cpp b/src/native/controller.cpp index a00a22b..c6a64ea 100644 --- a/src/native/controller.cpp +++ b/src/native/controller.cpp @@ -124,6 +124,7 @@ controller::open (const Napi::CallbackInfo &info) Napi::Env env = info.Env(); int index = info[0].As().Int32Value(); + bool rawAxisMode = info.Length() > 1 && info[1].As().Value(); SDL_GameController *controller = SDL_GameControllerOpen(index); if (controller == nullptr) { @@ -133,6 +134,12 @@ controller::open (const Napi::CallbackInfo &info) throw Napi::Error::New(env, message.str()); } + SDL_Joystick *joystick = SDL_GameControllerGetJoystick(controller); + int joystick_id = SDL_JoystickInstanceID(joystick); + if (rawAxisMode) { + joystick::rawAxisModeDevices.insert(joystick_id); + } + // SDL_GameControllerOpen produces errors even though it succeeds const char *error = SDL_GetError(); if (error != global::no_error) { fprintf(stderr, "SDL silent error: %s\n", error); } @@ -154,7 +161,6 @@ controller::open (const Napi::CallbackInfo &info) Napi::Value steam_handle = getSteamHandle(env, controller); - SDL_Joystick *joystick = SDL_GameControllerGetJoystick(controller); Napi::Value power = joystick::getPowerLevel(env, joystick); Napi::Object result = Napi::Object::New(env); @@ -186,6 +192,10 @@ controller::close (const Napi::CallbackInfo &info) throw Napi::Error::New(env, message.str()); } + SDL_Joystick *joystick = SDL_GameControllerGetJoystick(controller); + int joystick_id = SDL_JoystickInstanceID(joystick); + joystick::rawAxisModeDevices.erase(joystick_id); + SDL_GameControllerClose(controller); return env.Undefined(); diff --git a/src/native/joystick.cpp b/src/native/joystick.cpp index 3b52cfc..e47c8cd 100644 --- a/src/native/joystick.cpp +++ b/src/native/joystick.cpp @@ -5,12 +5,14 @@ #include #include #include +#include std::map joystick::hat_positions; std::map joystick::types; std::map joystick::power_levels; SDL_JoystickGUID joystick::zero_guid; +std::set joystick::rawAxisModeDevices; double joystick::mapAxis (SDL_Joystick *joystick, int axis) { @@ -19,12 +21,20 @@ joystick::mapAxis (SDL_Joystick *joystick, int axis) { double joystick::mapAxisValue (SDL_Joystick *joystick, int axis, int value) { - Sint16 initial; - if (!SDL_JoystickGetAxisInitialState(joystick, axis, &initial)) { initial = 0; } - double range = value < initial - ? initial - SDL_JOYSTICK_AXIS_MIN - : SDL_JOYSTICK_AXIS_MAX - initial; - return (value - initial) / range; + int joystick_id = SDL_JoystickInstanceID(joystick); + bool rawMode = rawAxisModeDevices.count(joystick_id) > 0; + + if (!rawMode) { + Sint16 initial; + if (!SDL_JoystickGetAxisInitialState(joystick, axis, &initial)) { initial = 0; } + double range = value < initial + ? initial - SDL_JOYSTICK_AXIS_MIN + : SDL_JOYSTICK_AXIS_MAX - initial; + return (value - initial) / range; + } + + double range = value < 0 ? -SDL_JOYSTICK_AXIS_MIN : SDL_JOYSTICK_AXIS_MAX; + return value / range; } Napi::Array @@ -176,6 +186,7 @@ joystick::open (const Napi::CallbackInfo &info) Napi::Env env = info.Env(); int index = info[0].As().Int32Value(); + bool rawAxisMode = info.Length() > 1 && info[1].As().Value(); SDL_Joystick *joystick = SDL_JoystickOpen(index); if (joystick == nullptr) { @@ -185,6 +196,11 @@ joystick::open (const Napi::CallbackInfo &info) throw Napi::Error::New(env, message.str()); } + int joystick_id = SDL_JoystickInstanceID(joystick); + if (rawAxisMode) { + rawAxisModeDevices.insert(joystick_id); + } + // SDL_JoystickOpen produces errors even though it succeeds const char *error = SDL_GetError(); if (error != global::no_error) { fprintf(stderr, "SDL silent error: %s\n", error); } @@ -419,6 +435,7 @@ joystick::close (const Napi::CallbackInfo &info) throw Napi::Error::New(env, message.str()); } + rawAxisModeDevices.erase(joystick_id); SDL_JoystickClose(joystick); return env.Undefined(); diff --git a/src/native/joystick.h b/src/native/joystick.h index 89504c4..b89afc0 100644 --- a/src/native/joystick.h +++ b/src/native/joystick.h @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace joystick { @@ -12,6 +13,7 @@ namespace joystick { extern std::map hat_positions; extern std::map power_levels; extern SDL_JoystickGUID zero_guid; + extern std::set rawAxisModeDevices; double mapAxis (SDL_Joystick *joystick, int axis); double mapAxisValue (SDL_Joystick *joystick, int axis, int value); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 08c5889..e632e97 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -956,6 +956,15 @@ export namespace Sdl { export namespace Joystick { + export interface OpenOptions { + /** + * When true, axis values are normalized without SDL's initial state calibration. + * This avoids incorrect zero-point calibration when joysticks aren't centered at startup. + * @default false + */ + rawAxisMode?: boolean + } + export interface BallPosition { readonly x: number readonly y: number @@ -1050,13 +1059,22 @@ export namespace Sdl { readonly devices: Device[] - openDevice (device: Device): JoystickInstance + openDevice (device: Device, options?: OpenOptions): JoystickInstance } } export namespace Controller { + export interface OpenOptions { + /** + * When true, axis values are normalized without SDL's initial state calibration. + * This avoids incorrect zero-point calibration when controllers aren't centered at startup. + * @default false + */ + rawAxisMode?: boolean + } + export type ControllerType = null | 'xbox360' @@ -1190,7 +1208,7 @@ export namespace Sdl { readonly devices: Device[] - openDevice (device: Device): ControllerInstance + openDevice (device: Device, options?: OpenOptions): ControllerInstance } } From 7e49ae14be3377e854963f23364eb4bf4c2d1d3d Mon Sep 17 00:00:00 2001 From: Rafael Araujo Lehmkuhl Date: Thu, 12 Feb 2026 15:46:34 -0300 Subject: [PATCH 2/5] Prepare forked release assets for raw axis mode --- .github/workflows/release.yml | 65 ++++++++++++++++++++++++++++++++--- package.json | 4 +-- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 56eedcf..b74967a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,11 +1,68 @@ name: Build and upload -on: workflow_dispatch +on: + workflow_dispatch: + push: + tags: + - "v*" + +permissions: + contents: write jobs: - empty: + ensure-release: runs-on: ubuntu-latest steps: - - run: 'echo "empty"' + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve release tag + id: release-tag + shell: bash + run: | + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then + echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + else + echo "tag=v$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT" + fi + + - name: Ensure release exists + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + tag="${{ steps.release-tag.outputs.tag }}" + if gh release view "$tag" >/dev/null 2>&1; then + echo "release ${tag} already exists" + else + gh release create "$tag" --title "$tag" --prerelease + fi + + build-and-upload: + needs: ensure-release + strategy: + fail-fast: false + matrix: + include: + - runner: macos-15-intel + - runner: macos-15 + - runner: ubuntu-latest + - runner: ubuntu-24-arm + - runner: windows-latest + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install dependencies + run: npm ci -# This file is empty on the master branch. Releases are made from the 'workflow' branch. + - name: Build and upload release asset + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npm run release diff --git a/package.json b/package.json index b909a06..aae00dc 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.11.13", + "version": "0.11.13-rawaxis.0", "name": "@kmamal/sdl", "description": "SDL bindings for Node.js", "keywords": [ @@ -38,7 +38,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+ssh://git@github.com/kmamal/node-sdl.git" + "url": "git+ssh://git@github.com/rafaellehmkuhl/node-sdl.git" }, "main": "./src/javascript/index.js", "types": "./src/types/index.d.ts", From 35e7e89f5f8937a2c50c219642795447d62c4216 Mon Sep 17 00:00:00 2001 From: Rafael Araujo Lehmkuhl Date: Thu, 12 Feb 2026 15:59:06 -0300 Subject: [PATCH 3/5] Install Linux X11 headers in release workflow --- .github/workflows/release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b74967a..f1f8ff8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,12 @@ jobs: with: node-version: 22 + - name: Install Linux build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libx11-dev + - name: Install dependencies run: npm ci From d3357fd5634db57663c9b89c74f4509fae53d881 Mon Sep 17 00:00:00 2001 From: Rafael Araujo Lehmkuhl Date: Thu, 12 Feb 2026 16:47:09 -0300 Subject: [PATCH 4/5] Use correct GitHub ARM runner label --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1f8ff8..1b4c10f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: - runner: macos-15-intel - runner: macos-15 - runner: ubuntu-latest - - runner: ubuntu-24-arm + - runner: ubuntu-24.04-arm - runner: windows-latest runs-on: ${{ matrix.runner }} steps: From dd1b97aa75bb84a2c899be40b0c48fcb135a4f88 Mon Sep 17 00:00:00 2001 From: Rafael Araujo Lehmkuhl Date: Thu, 12 Feb 2026 17:20:54 -0300 Subject: [PATCH 5/5] Align release workflow with upstream and Node 20 --- .github/workflows/release.yml | 69 +++++++++-------------------------- package.json | 2 +- 2 files changed, 19 insertions(+), 52 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1b4c10f..95b0de8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,65 +10,32 @@ permissions: contents: write jobs: - ensure-release: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Resolve release tag - id: release-tag - shell: bash - run: | - if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then - echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" - else - echo "tag=v$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT" - fi - - - name: Ensure release exists - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash - run: | - tag="${{ steps.release-tag.outputs.tag }}" - if gh release view "$tag" >/dev/null 2>&1; then - echo "release ${tag} already exists" - else - gh release create "$tag" --title "$tag" --prerelease - fi - - build-and-upload: - needs: ensure-release + build: + name: ${{ matrix.platform.name }} strategy: fail-fast: false matrix: - include: - - runner: macos-15-intel - - runner: macos-15 - - runner: ubuntu-latest - - runner: ubuntu-24.04-arm - - runner: windows-latest - runs-on: ${{ matrix.runner }} + platform: + - { name: 'Linux (x64)', os: ubuntu-latest } + - { name: 'Linux (arm64)', os: ubuntu-24.04-arm } + - { name: 'Windows (x64)', os: windows-latest } + - { name: 'Mac (x64)', os: macos-15-intel } + - { name: 'Mac (arm64)', os: macos-15 } + runs-on: ${{ matrix.platform.os }} steps: - - name: Checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 20 - - name: Install Linux build dependencies - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y libx11-dev + - if: ${{ startsWith(matrix.platform.os, 'macos-') }} + run: brew update && ./scripts/install-deps-mac.sh - - name: Install dependencies - run: npm ci + - if: ${{ startsWith(matrix.platform.os, 'ubuntu-') }} + run: sudo apt-get update && sudo ./scripts/install-deps-ubuntu.sh - - name: Build and upload release asset - env: + - env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CROSS_COMPILE_ARCH: ${{ matrix.platform.arch }} run: npm run release diff --git a/package.json b/package.json index aae00dc..f5ac2d0 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.11.13-rawaxis.0", + "version": "0.11.13-rawaxis.1", "name": "@kmamal/sdl", "description": "SDL bindings for Node.js", "keywords": [