From d558fe315b19e33ec984b2f1001e9067d1a24419 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Wed, 9 Sep 2026 13:17:46 -0400 Subject: [PATCH 1/5] fix(sdl): preserve received button edges and trigger transitions --- .github/workflows/sdl-regressions.yml | 45 ++++++++++ sdl/INPUT-DELIVERY.md | 27 ++++++ sdl/build-sdl.sh | 17 ++++ sdl/s2udp-input-edges.patch | 119 ++++++++++++++++++++++++++ tests/sdl-edges/check.c | 104 ++++++++++++++++++++++ 5 files changed, 312 insertions(+) create mode 100644 .github/workflows/sdl-regressions.yml create mode 100644 sdl/INPUT-DELIVERY.md create mode 100755 sdl/build-sdl.sh create mode 100644 sdl/s2udp-input-edges.patch create mode 100644 tests/sdl-edges/check.c diff --git a/.github/workflows/sdl-regressions.yml b/.github/workflows/sdl-regressions.yml new file mode 100644 index 0000000..6a062a1 --- /dev/null +++ b/.github/workflows/sdl-regressions.yml @@ -0,0 +1,45 @@ +name: SDL input regressions +on: + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + sdl: + runs-on: macos-26 + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + repository: libsdl-org/SDL + ref: 147a8ee32dbf9ac02f3794964490687b6bbda1bc + path: .ci-sdl + persist-credentials: false + - name: Build baseline and reproduce missing events + shell: bash + run: | + set -euo pipefail + git -C .ci-sdl apply "$GITHUB_WORKSPACE/sdl/sdl3-3.4.14-s2udp.patch" + cmake -S .ci-sdl -B "$RUNNER_TEMP/sdl-build" -DSDL_SHARED=ON -DSDL_STATIC=OFF -DSDL_TESTS=OFF + cmake --build "$RUNNER_TEMP/sdl-build" --parallel 3 + cc -I .ci-sdl/include tests/sdl-edges/check.c -L "$RUNNER_TEMP/sdl-build" -Wl,-rpath,"$RUNNER_TEMP/sdl-build" -lSDL3 -o "$RUNNER_TEMP/check-edges" + set +e + "$RUNNER_TEMP/check-edges" + result=$? + set -e + test "$result" -eq 42 + - name: Apply correction and pass the same regression + shell: bash + run: | + set -euo pipefail + git -C .ci-sdl apply "$GITHUB_WORKSPACE/sdl/s2udp-input-edges.patch" + cmake --build "$RUNNER_TEMP/sdl-build" --parallel 3 + "$RUNNER_TEMP/check-edges" + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: corrected-sdl-arm64 + path: ${{ runner.temp }}/sdl-build/libSDL3.0.dylib + retention-days: 7 diff --git a/sdl/INPUT-DELIVERY.md b/sdl/INPUT-DELIVERY.md new file mode 100644 index 0000000..3705c2a --- /dev/null +++ b/sdl/INPUT-DELIVERY.md @@ -0,0 +1,27 @@ +# Input delivery correction + +The tracked upstream libSDL3.0.dylib predates this fix. Editing a source patch +cannot change that binary. Build the corrected library on macOS: + +```sh +git clone https://github.com/libsdl-org/SDL /tmp/SDL +bash sdl/build-sdl.sh /tmp/SDL +``` + +The script exports exact commit 147a8ee32dbf9ac02f3794964490687b6bbda1bc, +applies the original patch and then s2udp-input-edges.patch, and writes +build/sdl/libSDL3.0.dylib without modifying the checkout or tracked binary. +Use that library for SDL3_DYNAMIC_API. The corrected-sdl-arm64 CI artifact +is also a development build, not a notarized application. + +Every received state now reaches an open SDL joystick before the next state +is read. The test sends complete button taps and analog-trigger excursions +between updates through actual localhost UDP sockets and SDL event APIs. +The CI negative control must fail with exit 42 before the correction; other +errors are not accepted as a reproduced defect. The same executable must +then pass against the rebuilt corrected library. + +Nintendo BLE commands, bonding, keep-alives and report decoding are untouched. +This does not recover datagrams lost before receipt, nor guarantee that a +state-polling game observes arbitrarily short transitions. Physical gameplay +and controller latency still need hardware acceptance. diff --git a/sdl/build-sdl.sh b/sdl/build-sdl.sh new file mode 100755 index 0000000..445f750 --- /dev/null +++ b/sdl/build-sdl.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Build reviewed SDL sources; never overwrite the tracked upstream dylib. +# Usage: bash sdl/build-sdl.sh /path/to/SDL-git-checkout +set -euo pipefail +root=$(cd "$(dirname "$0")/.." && pwd) +source_dir=${1:?Provide a git checkout of libsdl-org/SDL containing release-3.4.14} +revision=147a8ee32dbf9ac02f3794964490687b6bbda1bc +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +git -C "$source_dir" archive "$revision" | tar -x -C "$work" +git -C "$work" apply "$root/sdl/sdl3-3.4.14-s2udp.patch" +git -C "$work" apply "$root/sdl/s2udp-input-edges.patch" +cmake -S "$work" -B "$work/build" -DSDL_SHARED=ON -DSDL_STATIC=OFF -DSDL_TESTS=OFF +cmake --build "$work/build" --parallel 3 +mkdir -p "$root/build/sdl" +cp "$work/build/libSDL3.0.dylib" "$root/build/sdl/libSDL3.0.dylib" +shasum -a 256 "$root/build/sdl/libSDL3.0.dylib" diff --git a/sdl/s2udp-input-edges.patch b/sdl/s2udp-input-edges.patch new file mode 100644 index 0000000..59f8e3b --- /dev/null +++ b/sdl/s2udp-input-edges.patch @@ -0,0 +1,119 @@ +--- a/src/joystick/darwin/SDL_s2udpjoystick.c ++++ b/src/joystick/darwin/SDL_s2udpjoystick.c +@@ -83,6 +83,7 @@ + int sock; // -1 when unavailable + bool present; + SDL_JoystickID instance_id; ++ SDL_Joystick *joystick; // currently opened instance, never a replacement + Uint32 buttons; + float lx, ly, rx, ry; + Uint8 lt, rt; +@@ -124,8 +125,10 @@ + } + } + +-/* Drain one slot's queued datagrams, keep the newest state, and run its +- * presence state machine (hotplug on both edges). */ ++static void S2UDP_EmitState(SDL_Joystick *joystick, S2UDP_Slot *slot); ++ ++/* Every received state reaches an open joystick before the next is read. ++ * Keeping only the last packet would erase a complete press/release. */ + static void S2UDP_PumpSlot(S2UDP_Slot *slot) + { + Uint8 buf[128]; +@@ -148,6 +151,9 @@ + slot->lt = buf[28]; + slot->rt = buf[29]; + slot->last_state_ms = now; ++ if (slot->present && slot->joystick) { ++ S2UDP_EmitState(slot->joystick, slot); ++ } + } else if (n > 4 && n < 4 + (ssize_t)sizeof(slot->name) && + SDL_memcmp(buf, "S2N1", 4) == 0) { + // Bridge-provided display name (may be a user rename). A live +@@ -160,6 +166,8 @@ + if (slot->present) { + slot->present = false; + slot->last_state_ms = 0; // re-add on next state packet ++ if (slot->joystick) slot->joystick->hwdata = NULL; ++ slot->joystick = NULL; + SDL_PrivateJoystickRemoved(slot->instance_id); + } + } +@@ -177,6 +185,8 @@ + SDL_PrivateJoystickAdded(slot->instance_id); + } else if (slot->present && now - slot->last_state_ms >= PRESENCE_TIMEOUT_MS) { + slot->present = false; ++ if (slot->joystick) slot->joystick->hwdata = NULL; ++ slot->joystick = NULL; + SDL_PrivateJoystickRemoved(slot->instance_id); + } + } +@@ -352,13 +362,14 @@ + joystick->naxes = 6; + joystick->nhats = 0; + joystick->hwdata = (struct joystick_hwdata *)slot; ++ slot->joystick = joystick; + return true; + } + + static bool S2UDP_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble) + { + S2UDP_Slot *slot = (S2UDP_Slot *)joystick->hwdata; +- if (!slot) { ++ if (!slot || slot->joystick != joystick || !slot->present) { + return SDL_Unsupported(); + } + slot->rumble_strong = low_frequency_rumble; +@@ -400,7 +411,7 @@ + return (Sint16)(((int)value * 65535 / 255) - 32768); + } + +-static void S2UDP_JoystickUpdate(SDL_Joystick *joystick) ++static void S2UDP_EmitState(SDL_Joystick *joystick, S2UDP_Slot *slot) + { + static const struct { Uint8 index; Uint32 mask; } button_map[] = { + { 0, S2_BTN_B }, { 1, S2_BTN_A }, { 2, S2_BTN_Y }, { 3, S2_BTN_X }, +@@ -410,14 +421,8 @@ + { 11, S2_BTN_UP }, { 12, S2_BTN_DOWN }, { 13, S2_BTN_LEFT }, { 14, S2_BTN_RIGHT }, + { 15, S2_BTN_CAPTURE }, { 16, S2_BTN_GR }, { 17, S2_BTN_GL }, { 18, S2_BTN_C }, + }; +- S2UDP_Slot *slot = (S2UDP_Slot *)joystick->hwdata; + Uint64 timestamp = SDL_GetTicksNS(); + int i; +- +- S2UDP_PumpAll(); +- if (!slot || !slot->present) { +- return; +- } + + for (i = 0; i < (int)SDL_arraysize(button_map); ++i) { + SDL_SendJoystickButton(timestamp, joystick, button_map[i].index, +@@ -430,6 +435,18 @@ + SDL_SendJoystickAxis(timestamp, joystick, 4, S2UDP_AxisFromTrigger(slot->lt)); + SDL_SendJoystickAxis(timestamp, joystick, 5, S2UDP_AxisFromTrigger(slot->rt)); + ++} ++ ++static void S2UDP_JoystickUpdate(SDL_Joystick *joystick) ++{ ++ S2UDP_Slot *slot = (S2UDP_Slot *)joystick->hwdata; ++ S2UDP_PumpAll(); ++ if (!slot || !slot->present || slot->joystick != joystick) { ++ return; ++ } ++ // Also initialize a newly opened joystick when there is no new datagram. ++ S2UDP_EmitState(joystick, slot); ++ + // Sustain active rumble: the bridge expires intents after 500 ms so a + // crashed consumer can never leave the motor running. + if ((slot->rumble_strong || slot->rumble_weak) && +@@ -440,6 +457,8 @@ + + static void S2UDP_JoystickClose(SDL_Joystick *joystick) + { ++ S2UDP_Slot *slot = (S2UDP_Slot *)joystick->hwdata; ++ if (slot && slot->joystick == joystick) slot->joystick = NULL; + joystick->hwdata = NULL; + } + diff --git a/tests/sdl-edges/check.c b/tests/sdl-edges/check.c new file mode 100644 index 0000000..d898899 --- /dev/null +++ b/tests/sdl-edges/check.c @@ -0,0 +1,104 @@ +/* Synthetic loopback traffic through the real patched SDL, no radio or GUI. */ +#include +#include +#include +#include +#include +#include +#include + +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "line %d: %s (%s)\n", __LINE__, #x, SDL_GetError()); exit(2); } } while (0) + +static void packet(int fd, const struct sockaddr_in *to, unsigned buttons, + unsigned char trigger) +{ + unsigned char data[44] = {'S', '2', 'B', '1'}; + for (int i = 0; i < 4; ++i) data[8 + i] = (unsigned char)(buttons >> (8 * i)); + data[28] = trigger; + CHECK(sendto(fd, data, sizeof data, 0, (const struct sockaddr *)to, sizeof *to) == sizeof data); +} + +static void drain_events(void) +{ + SDL_Event event; + while (SDL_PollEvent(&event)) {} +} + +int main(void) +{ + int fd = socket(AF_INET, SOCK_DGRAM, 0); + CHECK(fd >= 0); + struct sockaddr_in server = {0}, peer = {0}; + server.sin_family = AF_INET; + server.sin_port = htons(24800); + server.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + CHECK(bind(fd, (struct sockaddr *)&server, sizeof server) == 0); + struct timeval timeout = {2, 0}; + CHECK(setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof timeout) == 0); + SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "0"); + CHECK(SDL_Init(SDL_INIT_JOYSTICK)); + unsigned char hello[64]; + socklen_t size = sizeof peer; + CHECK(recvfrom(fd, hello, sizeof hello, 0, (struct sockaddr *)&peer, &size) >= 0); + packet(fd, &peer, 0, 0); + SDL_UpdateJoysticks(); + int count = 0; + SDL_JoystickID *ids = SDL_GetJoysticks(&count); + CHECK(count == 1 && ids); + SDL_Joystick *joy = SDL_OpenJoystick(ids[0]); + SDL_JoystickID id = ids[0]; + SDL_free(ids); + CHECK(joy); + SDL_UpdateJoysticks(); + drain_events(); + + // A complete tap and a full trigger excursion queued before one update. + packet(fd, &peer, 8, 255); // Nintendo A -> existing joystick button 1 + packet(fd, &peer, 0, 0); + SDL_UpdateJoysticks(); + int downs = 0, ups = 0, trigger_down = 0, trigger_up = 0; + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_EVENT_JOYSTICK_BUTTON_DOWN && event.jbutton.which == id && event.jbutton.button == 1) { + CHECK(ups == 0); ++downs; + } + if (event.type == SDL_EVENT_JOYSTICK_BUTTON_UP && event.jbutton.which == id && event.jbutton.button == 1) { + CHECK(downs == 1); ++ups; + } + if (event.type == SDL_EVENT_JOYSTICK_AXIS_MOTION && event.jaxis.which == id && event.jaxis.axis == 4) { + if (event.jaxis.value == SDL_JOYSTICK_AXIS_MAX) ++trigger_down; + if (event.jaxis.value == SDL_JOYSTICK_AXIS_MIN) ++trigger_up; + } + } + if (downs != 1 || ups != 1 || trigger_down != 1 || trigger_up != 1) { + fprintf(stderr, "Lost queued transitions: A=%d/%d trigger=%d/%d\n", downs, ups, trigger_down, trigger_up); + SDL_CloseJoystick(joy); SDL_Quit(); close(fd); + return 42; // Expected only for the explicit baseline negative control. + } + CHECK(!SDL_GetJoystickButton(joy, 1)); + CHECK(SDL_GetJoystickAxis(joy, 4) == SDL_JOYSTICK_AXIS_MIN); + + // Two taps in one pump, including a simultaneous shoulder button. + packet(fd, &peer, 8 | 0x400000, 0); packet(fd, &peer, 0, 0); + packet(fd, &peer, 8, 0); packet(fd, &peer, 0, 0); + SDL_UpdateJoysticks(); + downs = ups = 0; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_EVENT_JOYSTICK_BUTTON_DOWN && event.jbutton.button == 1) ++downs; + if (event.type == SDL_EVENT_JOYSTICK_BUTTON_UP && event.jbutton.button == 1) ++ups; + } + CHECK(downs == 2 && ups == 2); + + // Close/reopen must not leave a dangling pointer used by packet delivery. + SDL_CloseJoystick(joy); + packet(fd, &peer, 8, 0); SDL_UpdateJoysticks(); + joy = SDL_OpenJoystick(id); CHECK(joy); + SDL_UpdateJoysticks(); + CHECK(SDL_GetJoystickButton(joy, 1)); + packet(fd, &peer, 0, 0); SDL_UpdateJoysticks(); + CHECK(!SDL_GetJoystickButton(joy, 1)); + SDL_CloseJoystick(joy); SDL_Quit(); close(fd); + puts("SDL queued-edge, analog-trigger and reopen regressions passed."); + return 0; +} From 7981c70a0336df5a886555b2b72acaf08109f4c6 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Wed, 9 Sep 2026 13:30:29 -0400 Subject: [PATCH 2/5] test(sdl): wait for throttled hotplug registration before checking edges --- tests/sdl-edges/check.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/sdl-edges/check.c b/tests/sdl-edges/check.c index d898899..754684d 100644 --- a/tests/sdl-edges/check.c +++ b/tests/sdl-edges/check.c @@ -41,10 +41,18 @@ int main(void) unsigned char hello[64]; socklen_t size = sizeof peer; CHECK(recvfrom(fd, hello, sizeof hello, 0, (struct sockaddr *)&peer, &size) >= 0); - packet(fd, &peer, 0, 0); - SDL_UpdateJoysticks(); int count = 0; - SDL_JoystickID *ids = SDL_GetJoysticks(&count); + SDL_JoystickID *ids = NULL; + // SDL throttles device discovery independently of packet delivery. + // Wait for registration, not for the input edges tested below. + Uint64 deadline = SDL_GetTicks() + 1500; + do { + packet(fd, &peer, 0, 0); + SDL_Delay(10); + SDL_UpdateJoysticks(); + SDL_free(ids); + ids = SDL_GetJoysticks(&count); + } while (count == 0 && SDL_GetTicks() < deadline); CHECK(count == 1 && ids); SDL_Joystick *joy = SDL_OpenJoystick(ids[0]); SDL_JoystickID id = ids[0]; From 1007f63dc848a92ff8faafe6a7d74debe39bb93f Mon Sep 17 00:00:00 2001 From: Johnny D Date: Wed, 9 Sep 2026 13:35:39 -0400 Subject: [PATCH 3/5] test(sdl): inspect queued SDL events after explicit update and axis warmup --- tests/sdl-edges/check.c | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/sdl-edges/check.c b/tests/sdl-edges/check.c index 754684d..d7abbed 100644 --- a/tests/sdl-edges/check.c +++ b/tests/sdl-edges/check.c @@ -21,7 +21,7 @@ static void packet(int fd, const struct sockaddr_in *to, unsigned buttons, static void drain_events(void) { SDL_Event event; - while (SDL_PollEvent(&event)) {} + while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_EVENT_FIRST, SDL_EVENT_LAST) > 0) {} } int main(void) @@ -38,6 +38,7 @@ int main(void) SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "0"); CHECK(SDL_Init(SDL_INIT_JOYSTICK)); + SDL_SetJoystickEventsEnabled(true); unsigned char hello[64]; socklen_t size = sizeof peer; CHECK(recvfrom(fd, hello, sizeof hello, 0, (struct sockaddr *)&peer, &size) >= 0); @@ -58,16 +59,23 @@ int main(void) SDL_JoystickID id = ids[0]; SDL_free(ids); CHECK(joy); - SDL_UpdateJoysticks(); + // Establish both resting axis value and real activity before the edge + // test; SDL suppresses initial analog jitter until an axis first moves. + packet(fd, &peer, 8, 128); + SDL_Delay(10); SDL_UpdateJoysticks(); + CHECK(SDL_GetJoystickButton(joy, 1)); + packet(fd, &peer, 0, 0); + SDL_Delay(10); SDL_UpdateJoysticks(); + CHECK(!SDL_GetJoystickButton(joy, 1)); drain_events(); // A complete tap and a full trigger excursion queued before one update. packet(fd, &peer, 8, 255); // Nintendo A -> existing joystick button 1 packet(fd, &peer, 0, 0); - SDL_UpdateJoysticks(); + SDL_Delay(10); SDL_UpdateJoysticks(); int downs = 0, ups = 0, trigger_down = 0, trigger_up = 0; SDL_Event event; - while (SDL_PollEvent(&event)) { + while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_EVENT_FIRST, SDL_EVENT_LAST) > 0) { if (event.type == SDL_EVENT_JOYSTICK_BUTTON_DOWN && event.jbutton.which == id && event.jbutton.button == 1) { CHECK(ups == 0); ++downs; } @@ -90,9 +98,9 @@ int main(void) // Two taps in one pump, including a simultaneous shoulder button. packet(fd, &peer, 8 | 0x400000, 0); packet(fd, &peer, 0, 0); packet(fd, &peer, 8, 0); packet(fd, &peer, 0, 0); - SDL_UpdateJoysticks(); + SDL_Delay(10); SDL_UpdateJoysticks(); downs = ups = 0; - while (SDL_PollEvent(&event)) { + while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_EVENT_FIRST, SDL_EVENT_LAST) > 0) { if (event.type == SDL_EVENT_JOYSTICK_BUTTON_DOWN && event.jbutton.button == 1) ++downs; if (event.type == SDL_EVENT_JOYSTICK_BUTTON_UP && event.jbutton.button == 1) ++ups; } @@ -100,11 +108,11 @@ int main(void) // Close/reopen must not leave a dangling pointer used by packet delivery. SDL_CloseJoystick(joy); - packet(fd, &peer, 8, 0); SDL_UpdateJoysticks(); + packet(fd, &peer, 8, 0); SDL_Delay(10); SDL_UpdateJoysticks(); joy = SDL_OpenJoystick(id); CHECK(joy); SDL_UpdateJoysticks(); CHECK(SDL_GetJoystickButton(joy, 1)); - packet(fd, &peer, 0, 0); SDL_UpdateJoysticks(); + packet(fd, &peer, 0, 0); SDL_Delay(10); SDL_UpdateJoysticks(); CHECK(!SDL_GetJoystickButton(joy, 1)); SDL_CloseJoystick(joy); SDL_Quit(); close(fd); puts("SDL queued-edge, analog-trigger and reopen regressions passed."); From ae5996d5254ca96971087ed33c1fe08b48862de6 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Wed, 9 Sep 2026 13:43:11 -0400 Subject: [PATCH 4/5] build(sdl): use and test the corrected library instead of the stale bundled binary --- .github/workflows/sdl-regressions.yml | 10 +++++----- sdl/INPUT-DELIVERY.md | 12 ++++++++---- sdl/make-gopher64-both.sh | 7 ++++++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/.github/workflows/sdl-regressions.yml b/.github/workflows/sdl-regressions.yml index 6a062a1..66d7d3e 100644 --- a/.github/workflows/sdl-regressions.yml +++ b/.github/workflows/sdl-regressions.yml @@ -22,6 +22,7 @@ jobs: shell: bash run: | set -euo pipefail + bash -n sdl/build-sdl.sh sdl/make-gopher64-both.sh git -C .ci-sdl apply "$GITHUB_WORKSPACE/sdl/sdl3-3.4.14-s2udp.patch" cmake -S .ci-sdl -B "$RUNNER_TEMP/sdl-build" -DSDL_SHARED=ON -DSDL_STATIC=OFF -DSDL_TESTS=OFF cmake --build "$RUNNER_TEMP/sdl-build" --parallel 3 @@ -31,15 +32,14 @@ jobs: result=$? set -e test "$result" -eq 42 - - name: Apply correction and pass the same regression + - name: Build corrected library with the documented script and verify override shell: bash run: | set -euo pipefail - git -C .ci-sdl apply "$GITHUB_WORKSPACE/sdl/s2udp-input-edges.patch" - cmake --build "$RUNNER_TEMP/sdl-build" --parallel 3 - "$RUNNER_TEMP/check-edges" + bash sdl/build-sdl.sh .ci-sdl + SDL3_DYNAMIC_API="$GITHUB_WORKSPACE/build/sdl/libSDL3.0.dylib" "$RUNNER_TEMP/check-edges" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: corrected-sdl-arm64 - path: ${{ runner.temp }}/sdl-build/libSDL3.0.dylib + path: build/sdl/libSDL3.0.dylib retention-days: 7 diff --git a/sdl/INPUT-DELIVERY.md b/sdl/INPUT-DELIVERY.md index 3705c2a..b2cbad4 100644 --- a/sdl/INPUT-DELIVERY.md +++ b/sdl/INPUT-DELIVERY.md @@ -6,20 +6,24 @@ cannot change that binary. Build the corrected library on macOS: ```sh git clone https://github.com/libsdl-org/SDL /tmp/SDL bash sdl/build-sdl.sh /tmp/SDL +bash sdl/make-gopher64-both.sh ``` -The script exports exact commit 147a8ee32dbf9ac02f3794964490687b6bbda1bc, +The build script exports exact commit 147a8ee32dbf9ac02f3794964490687b6bbda1bc, applies the original patch and then s2udp-input-edges.patch, and writes build/sdl/libSDL3.0.dylib without modifying the checkout or tracked binary. -Use that library for SDL3_DYNAMIC_API. The corrected-sdl-arm64 CI artifact -is also a development build, not a notarized application. +The Gopher64 wrapper defaults to that rebuilt library and refuses to proceed +when it is missing. SDL3_LIBRARY may explicitly select another compatible +library. The corrected-sdl-arm64 CI artifact is a development build, not a +notarized application. Existing installed Gopher64 copies are not updated +until the wrapper is rerun. Every received state now reaches an open SDL joystick before the next state is read. The test sends complete button taps and analog-trigger excursions between updates through actual localhost UDP sockets and SDL event APIs. The CI negative control must fail with exit 42 before the correction; other errors are not accepted as a reproduced defect. The same executable must -then pass against the rebuilt corrected library. +then pass via SDL3_DYNAMIC_API using the documented script's rebuilt library. Nintendo BLE commands, bonding, keep-alives and report decoding are untouched. This does not recover datagrams lost before receipt, nor guarantee that a diff --git a/sdl/make-gopher64-both.sh b/sdl/make-gopher64-both.sh index e330d4b..e907331 100755 --- a/sdl/make-gopher64-both.sh +++ b/sdl/make-gopher64-both.sh @@ -15,7 +15,12 @@ set -euo pipefail SRC="/Applications/Gopher64.app" -DYLIB="$(cd "$(dirname "$0")" && pwd)/libSDL3.0.dylib" +DYLIB="${SDL3_LIBRARY:-$(cd "$(dirname "$0")/.." && pwd)/build/sdl/libSDL3.0.dylib}" +if [ ! -f "$DYLIB" ]; then + echo "Build the corrected SDL first: bash sdl/build-sdl.sh /path/to/SDL" >&2 + echo "Or set SDL3_LIBRARY to an explicitly selected compatible dylib." >&2 + exit 1 +fi WORK="$(mktemp -d)/Gopher64-Both.app" DEST="$HOME/Applications/Gopher64-Both.app" # Portable: @executable_path resolves relative to the bundle no matter where From 231e49ab0ca83181b185531bef504d66854598e3 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Wed, 9 Sep 2026 20:01:13 -0400 Subject: [PATCH 5/5] Bind wired SDL vendor commands to the correct physical controller (#7) * fix(sdl): bind vendor commands to the HID device's exact USB ancestry Resolve the HID registry service to its USB ancestor and match bus plus address, not the first VID/PID entry. Unknown/ambiguous identity fails closed. The same path initializes wired devices, so hardware identity coverage is an explicit compatibility gate. BLE and command bytes remain unchanged. Test actual production function bodies with fake IOKit/libusb boundaries and compile the full wired SDL driver against the real Apple SDK. The baseline wrong-device reproducer exits 42; corrected selection/cleanup cases must pass. * test(sdl): explicitly enable libusb and inspect the generated configuration header The initial check looked in include/ rather than SDL's include-config-* path. Keep the assertion that HAVE_LIBUSB is enabled so the real wired driver cannot silently be preprocessed out of the Apple-SDK build. * ci(sdl): preserve compiler diagnostics and surface concise failure tails * fix(sdl): include the CoreFoundation number API used by registry validation * test: keep SDL source-dependent checks out of the Swift suite discovery --- .github/workflows/sdl-regressions.yml | 22 +++-- sdl/USB-IDENTITY.md | 29 +++++++ sdl/build-sdl.sh | 3 +- sdl/s2usb-device-identity.patch | 86 +++++++++++++++++++ tests/sdl-usb/check-sdl.sh | 38 +++++++++ tests/sdl-usb/harness.c | 116 ++++++++++++++++++++++++++ 6 files changed, 288 insertions(+), 6 deletions(-) create mode 100644 sdl/USB-IDENTITY.md create mode 100644 sdl/s2usb-device-identity.patch create mode 100644 tests/sdl-usb/check-sdl.sh create mode 100644 tests/sdl-usb/harness.c diff --git a/.github/workflows/sdl-regressions.yml b/.github/workflows/sdl-regressions.yml index 66d7d3e..c1b6ac6 100644 --- a/.github/workflows/sdl-regressions.yml +++ b/.github/workflows/sdl-regressions.yml @@ -18,26 +18,38 @@ jobs: ref: 147a8ee32dbf9ac02f3794964490687b6bbda1bc path: .ci-sdl persist-credentials: false - - name: Build baseline and reproduce missing events + - name: Ensure the real wired driver is compiled + run: brew list libusb >/dev/null 2>&1 || brew install libusb + - name: Build baseline and reproduce missing events and wrong USB selection shell: bash run: | set -euo pipefail bash -n sdl/build-sdl.sh sdl/make-gopher64-both.sh git -C .ci-sdl apply "$GITHUB_WORKSPACE/sdl/sdl3-3.4.14-s2udp.patch" - cmake -S .ci-sdl -B "$RUNNER_TEMP/sdl-build" -DSDL_SHARED=ON -DSDL_STATIC=OFF -DSDL_TESTS=OFF - cmake --build "$RUNNER_TEMP/sdl-build" --parallel 3 + bash tests/sdl-usb/check-sdl.sh .ci-sdl before + cmake -S .ci-sdl -B "$RUNNER_TEMP/sdl-build" -DSDL_SHARED=ON -DSDL_STATIC=OFF -DSDL_TESTS=OFF -DSDL_HIDAPI_LIBUSB=ON > "$RUNNER_TEMP/sdl-baseline.log" 2>&1 + cmake --build "$RUNNER_TEMP/sdl-build" --parallel 3 >> "$RUNNER_TEMP/sdl-baseline.log" 2>&1 || { tail -80 "$RUNNER_TEMP/sdl-baseline.log"; exit 1; } + grep '^#define HAVE_LIBUSB 1' "$RUNNER_TEMP/sdl-build"/include-config-*/build_config/SDL_build_config.h cc -I .ci-sdl/include tests/sdl-edges/check.c -L "$RUNNER_TEMP/sdl-build" -Wl,-rpath,"$RUNNER_TEMP/sdl-build" -lSDL3 -o "$RUNNER_TEMP/check-edges" set +e "$RUNNER_TEMP/check-edges" result=$? set -e test "$result" -eq 42 - - name: Build corrected library with the documented script and verify override + - name: Build corrected library and verify the same production functions shell: bash run: | set -euo pipefail - bash sdl/build-sdl.sh .ci-sdl + bash sdl/build-sdl.sh .ci-sdl > "$RUNNER_TEMP/sdl-corrected.log" 2>&1 || { tail -80 "$RUNNER_TEMP/sdl-corrected.log"; exit 1; } + git -C .ci-sdl apply "$GITHUB_WORKSPACE/sdl/s2usb-device-identity.patch" + bash tests/sdl-usb/check-sdl.sh .ci-sdl after SDL3_DYNAMIC_API="$GITHUB_WORKSPACE/build/sdl/libSDL3.0.dylib" "$RUNNER_TEMP/check-edges" + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + if: always() + with: + name: sdl-build-diagnostics + path: ${{ runner.temp }}/sdl-*.log + retention-days: 7 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: corrected-sdl-arm64 diff --git a/sdl/USB-IDENTITY.md b/sdl/USB-IDENTITY.md new file mode 100644 index 0000000..4fc5ec5 --- /dev/null +++ b/sdl/USB-IDENTITY.md @@ -0,0 +1,29 @@ +# Wired USB command ownership + +The upstream SDL addition selected the first openable device with the same +VID/PID. With identical controllers this can initialize or send feedback to +the wrong controller while HID input comes from another one. + +The added s2usb-device-identity.patch resolves the exact HID DevSrvsID path +through IOKit to its USB device ancestor. It reads locationID and USB Address +(or USBDeviceAddress), matches both libusb bus and device address plus VID/PID, +and opens only a unique match. Registry entries and properties are released. +Unknown paths, missing/invalid properties and ambiguous matches do not guess. + +**Compatibility tradeoff:** this acquisition path also performs wired +initialization. If identity cannot be established on a particular Mac/USB +backend, that wired SDL device may fail to initialize instead of falling back +to a potentially different controller. BLE/UDP output is unaffected. Capture +the actual HID/IOKit/libusb identities before qualifying supported hardware; +no physical-controller or driver-restoration test has been performed here. + +CI compiles the real wired driver with libusb and Apple IOKit headers. A small +harness extracts the actual acquisition/identity function bodies and supplies +fake platform boundaries: the original selects the wrong identical device +(exit 42), the correction passes reverse-order, missing-peer, duplicate-address, +failed-claim, alternate-property, malformed-path and cleanup tests. The existing +real SDL/UDP edge test also runs against the combined rebuilt library. + +Build with sdl/build-sdl.sh as documented in INPUT-DELIVERY.md. The tracked +upstream dylib is not updated; the builder applies all three patches and +produces build/sdl/libSDL3.0.dylib. No controller command bytes are changed. diff --git a/sdl/build-sdl.sh b/sdl/build-sdl.sh index 445f750..8ae5c2d 100755 --- a/sdl/build-sdl.sh +++ b/sdl/build-sdl.sh @@ -10,7 +10,8 @@ trap 'rm -rf "$work"' EXIT git -C "$source_dir" archive "$revision" | tar -x -C "$work" git -C "$work" apply "$root/sdl/sdl3-3.4.14-s2udp.patch" git -C "$work" apply "$root/sdl/s2udp-input-edges.patch" -cmake -S "$work" -B "$work/build" -DSDL_SHARED=ON -DSDL_STATIC=OFF -DSDL_TESTS=OFF +git -C "$work" apply "$root/sdl/s2usb-device-identity.patch" +cmake -S "$work" -B "$work/build" -DSDL_SHARED=ON -DSDL_STATIC=OFF -DSDL_TESTS=OFF -DSDL_HIDAPI_LIBUSB=ON cmake --build "$work/build" --parallel 3 mkdir -p "$root/build/sdl" cp "$work/build/libSDL3.0.dylib" "$root/build/sdl/libSDL3.0.dylib" diff --git a/sdl/s2usb-device-identity.patch b/sdl/s2usb-device-identity.patch new file mode 100644 index 0000000..169f85b --- /dev/null +++ b/sdl/s2usb-device-identity.patch @@ -0,0 +1,86 @@ +--- a/src/joystick/hidapi/SDL_hidapi_switch2.c ++++ b/src/joystick/hidapi/SDL_hidapi_switch2.c +@@ -417,10 +417,56 @@ ++#ifdef SDL_PLATFORM_MACOS ++#include ++#include ++ ++/* Match the opened HID service's USB ancestor, never just its model. */ ++static bool S2USB_GetIdentity(const char *path, Uint8 *bus, Uint8 *address) ++{ ++ unsigned long long identifier = 0; ++ char trailing; ++ if (!path || SDL_sscanf(path, "DevSrvsID:%llu%c", &identifier, &trailing) != 1) { ++ return false; ++ } ++ io_registry_entry_t entry = IOServiceGetMatchingService(kIOMainPortDefault, ++ IORegistryEntryIDMatching(identifier)); ++ while (entry && !IOObjectConformsTo(entry, "IOUSBHostDevice") && ++ !IOObjectConformsTo(entry, "IOUSBDevice")) { ++ io_registry_entry_t parent = IO_OBJECT_NULL; ++ kern_return_t result = IORegistryEntryGetParentEntry(entry, kIOServicePlane, &parent); ++ IOObjectRelease(entry); ++ if (result != KERN_SUCCESS) return false; ++ entry = parent; ++ } ++ if (!entry) return false; ++ CFTypeRef location = IORegistryEntryCreateCFProperty(entry, CFSTR("locationID"), kCFAllocatorDefault, 0); ++ CFTypeRef device_address = IORegistryEntryCreateCFProperty(entry, CFSTR("USB Address"), kCFAllocatorDefault, 0); ++ if (!device_address) { ++ device_address = IORegistryEntryCreateCFProperty(entry, CFSTR("USBDeviceAddress"), kCFAllocatorDefault, 0); ++ } ++ Sint64 loc = -1, addr = -1; ++ bool valid = location && device_address && ++ CFGetTypeID(location) == CFNumberGetTypeID() && ++ CFGetTypeID(device_address) == CFNumberGetTypeID() && ++ CFNumberGetValue((CFNumberRef)location, kCFNumberSInt64Type, &loc) && ++ CFNumberGetValue((CFNumberRef)device_address, kCFNumberSInt64Type, &addr) && ++ loc >= 0 && loc <= 0xffffffffLL && addr > 0 && addr <= 127; ++ if (location) CFRelease(location); ++ if (device_address) CFRelease(device_address); ++ IOObjectRelease(entry); ++ if (!valid) return false; ++ *bus = (Uint8)((Uint32)loc >> 24); ++ *address = (Uint8)addr; ++ return true; ++} ++#endif ++ + static bool AcquireVendorInterface(SDL_DriverSwitch2_Context *ctx) + { + #ifdef SDL_PLATFORM_MACOS + if (ctx->device_handle) { + return true; + } +- if (!ctx->libusb) { ++ Uint8 bus = 0, address = 0; ++ if (!ctx->libusb || !S2USB_GetIdentity(ctx->device->path, &bus, &address)) { + return false; + } + if (ctx->libusb->init(&ctx->usb_context) != 0) { +@@ -430,16 +476,20 @@ + { + libusb_device **usb_list = NULL; + ssize_t usb_count = ctx->libusb->get_device_list(ctx->usb_context, &usb_list); ++ libusb_device *matched = NULL; + for (ssize_t di = 0; di < usb_count; di++) { + struct libusb_device_descriptor desc; + if (ctx->libusb->get_device_descriptor(usb_list[di], &desc) == 0 && + desc.idVendor == ctx->device->vendor_id && +- desc.idProduct == ctx->device->product_id) { +- if (ctx->libusb->open(usb_list[di], &ctx->device_handle) == 0) { +- ctx->own_device_handle = true; +- break; +- } ++ desc.idProduct == ctx->device->product_id && ++ ctx->libusb->get_bus_number(usb_list[di]) == bus && ++ ctx->libusb->get_device_address(usb_list[di]) == address) { ++ if (matched) { matched = NULL; break; } // ambiguous: no command ++ matched = usb_list[di]; + } ++ } ++ if (matched && ctx->libusb->open(matched, &ctx->device_handle) == 0) { ++ ctx->own_device_handle = true; + } + if (usb_list) { + ctx->libusb->free_device_list(usb_list, 1); diff --git a/tests/sdl-usb/check-sdl.sh b/tests/sdl-usb/check-sdl.sh new file mode 100644 index 0000000..192e2e7 --- /dev/null +++ b/tests/sdl-usb/check-sdl.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Test functions from the actual patched SDL source; fake only IOKit/libusb. +set -euo pipefail +root=$(cd "$(dirname "$0")/../.." && pwd) +source_dir=${1:?Provide the SDL source directory with patches applied} +mode=${2:-after} +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/IOKit" +printf '/* Boundary declarations are in the harness. */\n' > "$work/IOKit/IOKitLib.h" +python3 - "$source_dir/src/joystick/hidapi/SDL_hidapi_switch2.c" "$work/production.c" "$mode" <<'PY' +from pathlib import Path +import sys +s=Path(sys.argv[1]).read_text() +def function(name): + a=s.index('static bool '+name+'(') + # Skip forward declarations, if present. + while s.index(';',a) < s.index('{',a): + a=s.index('static bool '+name+'(',a+1) + b=s.index('{',a);depth=1;c=b+1 + while depth: + depth += (s[c]=='{') - (s[c]=='}');c+=1 + return s[a:c]+'\n' +text='' if sys.argv[3]=='before' else function('S2USB_GetIdentity') +Path(sys.argv[2]).write_text(text+function('AcquireVendorInterface')) +PY +flags=() +[ "$mode" != before ] || flags+=(-DBASELINE) +cc "${flags[@]}" -I "$work" "$root/tests/sdl-usb/harness.c" -o "$work/test" +if [ "$mode" = before ]; then + set +e + "$work/test" + result=$? + set -e + test "$result" -eq 42 +else + "$work/test" +fi diff --git a/tests/sdl-usb/harness.c b/tests/sdl-usb/harness.c new file mode 100644 index 0000000..07b3753 --- /dev/null +++ b/tests/sdl-usb/harness.c @@ -0,0 +1,116 @@ +#include +#include +#include +#include +#include +#include +#define SDL_PLATFORM_MACOS 1 +#define SDL_sscanf sscanf +#define CHECK(x) do { if (!(x)) { fprintf(stderr,"line %d: %s\n",__LINE__,#x); exit(2); } } while(0) +typedef uint8_t Uint8; typedef uint32_t Uint32; typedef int64_t Sint64; +// Fake only the platform/USB boundary; included function bodies are production. +typedef int io_registry_entry_t; typedef int kern_return_t; +#define kIOMainPortDefault 0 +#define IO_OBJECT_NULL 0 +#define KERN_SUCCESS 0 +#define kIOServicePlane "IOService" +#define kCFAllocatorDefault 0 +#define kCFNumberSInt64Type 0 +#define CFSTR(x) (x) +typedef struct { int type; Sint64 value; } Number; +typedef Number *CFTypeRef; typedef Number *CFNumberRef; +static Number location = {1, 0x02000000}, address = {1, 5}; +static bool missing_address, alternate_address; +static int released_entries, released_properties; +static unsigned long long IORegistryEntryIDMatching(unsigned long long id) {return id;} +static int IOServiceGetMatchingService(int port, unsigned long long id) {return id == 42 ? 3 : 0;} +static bool IOObjectConformsTo(int entry, const char *name) {return entry == 1 && strcmp(name,"IOUSBHostDevice") == 0;} +static int IORegistryEntryGetParentEntry(int entry, const char *plane, int *parent) {*parent = entry - 1; return 0;} +static void IOObjectRelease(int entry) {CHECK(entry > 0); released_entries++;} +static CFTypeRef IORegistryEntryCreateCFProperty(int entry, const char *key, int alloc, int options) { + if (!strcmp(key,"locationID")) return &location; + if (missing_address) return NULL; + if (!strcmp(key,alternate_address ? "USBDeviceAddress" : "USB Address")) return &address; + return NULL; +} +static int CFGetTypeID(CFTypeRef v) {return v->type;} +static int CFNumberGetTypeID(void) {return 1;} +static bool CFNumberGetValue(CFNumberRef n, int type, Sint64 *out) {*out=n->value;return true;} +static void CFRelease(CFTypeRef p) {CHECK(p);released_properties++;} + +typedef struct {int index; Uint8 bus, address; unsigned vendor, product;} libusb_device; +typedef libusb_device libusb_device_handle; +typedef int libusb_context; +struct libusb_device_descriptor {unsigned idVendor,idProduct;}; +static libusb_device devices[]={{0,2,4,0x57e,0x2073},{1,2,5,0x57e,0x2073},{2,2,6,0x57e,0x2073}}; +static libusb_device *ordered[]={&devices[0],&devices[1],&devices[2]}; +static int count=2, opened, claimed, exited, freed, closed, claim_result; +static int usb_init(libusb_context **c) {static int context;*c=&context;return 0;} +static void usb_exit(libusb_context *c) {exited++;} +static ssize_t usb_list(libusb_context *c, libusb_device ***out) {*out=ordered;return count;} +static void usb_free(libusb_device **list,int unref) {freed++;} +static int usb_desc(libusb_device *d,struct libusb_device_descriptor *out) {out->idVendor=d->vendor;out->idProduct=d->product;return 0;} +static Uint8 usb_bus(libusb_device *d){return d->bus;} +static Uint8 usb_address(libusb_device *d){return d->address;} +static int usb_open(libusb_device *d,libusb_device_handle **out){opened++;*out=d;return 0;} +static int usb_claim(libusb_device_handle *d,Uint8 interface){claimed++;return claim_result;} +typedef struct { + int (*init)(libusb_context **);void (*exit)(libusb_context *); + ssize_t (*get_device_list)(libusb_context *,libusb_device ***); + void (*free_device_list)(libusb_device **,int); + int (*get_device_descriptor)(libusb_device *,struct libusb_device_descriptor *); + Uint8 (*get_bus_number)(libusb_device *);Uint8 (*get_device_address)(libusb_device *); + int (*open)(libusb_device *,libusb_device_handle **); + int (*claim_interface)(libusb_device_handle *,Uint8); +} API; +typedef struct {unsigned vendor_id,product_id; const char *path;} HIDDevice; +typedef struct { + API *libusb;HIDDevice *device;libusb_device_handle *device_handle; + libusb_context *usb_context;bool own_device_handle,interface_claimed; + Uint8 interface_number,out_endpoint,in_endpoint; +} SDL_DriverSwitch2_Context; +static bool FindBulkEndpoints(API *api,libusb_device_handle *d,Uint8 *i,Uint8 *out,Uint8 *in){*i=1;*out=2;*in=0x82;return true;} +static void ReleaseVendorInterface(SDL_DriverSwitch2_Context *ctx){ + if(ctx->device_handle)closed++; + if(ctx->usb_context)ctx->libusb->exit(ctx->usb_context); + ctx->device_handle=NULL;ctx->usb_context=NULL;ctx->own_device_handle=false;ctx->interface_claimed=false; +} +#include "production.c" + +int main(void) { + API api={usb_init,usb_exit,usb_list,usb_free,usb_desc,usb_bus,usb_address,usb_open,usb_claim}; + HIDDevice hid={0x57e,0x2073,"DevSrvsID:42"}; + SDL_DriverSwitch2_Context c={.libusb=&api,.device=&hid}; + CHECK(AcquireVendorInterface(&c)); + if(c.device_handle != &devices[1]) {fputs("Wrong identical USB controller selected\n",stderr);return 42;} + CHECK(opened==1 && claimed==1 && freed==1); + ReleaseVendorInterface(&c); + ordered[0]=&devices[1];ordered[1]=&devices[0]; + CHECK(AcquireVendorInterface(&c) && c.device_handle==&devices[1]); + ReleaseVendorInterface(&c); + // One visible wrong peer is not proof of association. + ordered[0]=&devices[0];count=1;int old=opened; + CHECK(!AcquireVendorInterface(&c) && opened==old && c.usb_context==NULL); + // Duplicate address records fail closed rather than choosing either. + ordered[0]=&devices[1];ordered[1]=&devices[2];devices[2].address=5;count=2; + CHECK(!AcquireVendorInterface(&c) && opened==old); + count=1;claim_result=-1; + CHECK(!AcquireVendorInterface(&c) && c.device_handle==NULL && c.usb_context==NULL); + CHECK(closed==3);claim_result=0; +#ifndef BASELINE + Uint8 bus=0,addr=0; + released_entries=released_properties=0; + CHECK(S2USB_GetIdentity("DevSrvsID:42",&bus,&addr) && bus==2 && addr==5); + CHECK(released_entries==3 && released_properties==2); + CHECK(!S2USB_GetIdentity(NULL,&bus,&addr)); + CHECK(!S2USB_GetIdentity("DevSrvsID:42junk",&bus,&addr)); + CHECK(!S2USB_GetIdentity("DevSrvsID:999",&bus,&addr)); + alternate_address=true;CHECK(S2USB_GetIdentity(hid.path,&bus,&addr)); + missing_address=true;CHECK(!S2USB_GetIdentity(hid.path,&bus,&addr));missing_address=false; + location.type=2;CHECK(!S2USB_GetIdentity(hid.path,&bus,&addr));location.type=1; + address.value=128;CHECK(!S2USB_GetIdentity(hid.path,&bus,&addr));address.value=5; + hid.path="DevSrvsID:999";old=opened;CHECK(!AcquireVendorInterface(&c) && opened==old); +#endif + puts("USB identity, reversed enumeration, ambiguity and cleanup regressions passed."); + return 0; +}