sdl: port the joystick, gamepad and audio backends to SDL3 - #922
Draft
edumeneses wants to merge 1 commit into
Draft
sdl: port the joystick, gamepad and audio backends to SDL3#922edumeneses wants to merge 1 commit into
edumeneses wants to merge 1 commit into
Conversation
Moves every SDL2 call site to SDL3 and switches find_package(SDL2) to SDL3. ossia::sdl2 becomes ossia::sdl3, and the SDK prefix path moves from $OSSIA_SDK/SDL2 to $OSSIA_SDK/SDL3 to match ossia/sdk#27. Most of it is upstream's rename table (SDL_oldnames.h, which is also what build-scripts/rename_symbols.py applies): SDL_JoystickOpen -> SDL_OpenJoystick, SDL_GameControllerHasAxis -> SDL_GamepadHasAxis, SDL_CONTROLLER* -> SDL_GAMEPAD*, the SDL_Controller*Event structs -> SDL_Gamepad*Event and their union members caxis/cbutton/ctouchpad/csensor -> gaxis/gbutton/gtouchpad/gsensor, and the event enums to SDL_EVENT_*. Four things needed more than a rename: Device indices are gone. SDL2 enumerated joysticks as 0..SDL_NumJoysticks()-1 and mapped an index to an identity with SDL_JoystickGetDeviceInstanceID; SDL3 has only SDL_GetJoysticks(), returning an owned array of stable SDL_JoystickID. joystick_info keeps its index-based signatures on purpose: an index into that array plays exactly the role SDL2's device index did, and is just as transient (valid until the device list changes), so score's enumeration loop and the (id, index) pair it serialises into save files keep working untouched. The new sdl_joystick_ids RAII helper owns the array; index 0 maps to SDL3's invalid id of 0, which joystick_info translates back to the -1 sentinel its callers expect. game_controller_protocol no longer needs to remember the index at all, since SDL_GetGamepadTypeForID takes the id, so m_joystick_index is gone. Booleans changed shape. SDL_Init and the Has*/SetSensorEnabled family return bool rather than an int status, so `< 0`, `>= 0` and `== SDL_TRUE` tests are dropped; button events carry `down` instead of `state == SDL_PRESSED`. The rumble capability queries were replaced by properties: SDL_GameControllerHasRumble/HasRumbleTriggers become SDL_GetBooleanProperty on SDL_GetGamepadProperties with SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN and SDL_PROP_GAMEPAD_CAP_TRIGGER_RUMBLE_BOOLEAN. The audio engine is a real rewrite. SDL3 has no pull callback filling a fixed-size buffer: SDL_OpenAudioDeviceStream hands us a stream and asks for `additional_amount` bytes, which varies. The callback now services that request in whole blocks of effective_buffer_size frames and pushes each through SDL_PutAudioStreamData, with SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES set to the requested block size so the device buffer matches. Overshooting the request by at most one block is fine - the stream buffers it. SDL_AudioSpec lost `samples`, `callback` and `userdata`, so effective_buffer_size is now what we asked for rather than what the device reported. The interleave scratch buffers are preallocated members instead of per-callback alloca, which the fixed block size now makes possible. Status comes from SDL_AudioStreamDevicePaused instead of SDL_GetAudioDeviceStatus. Two guards were dropped. The SDL_VERSION_ATLEAST(2, 26, 0) fallback for SDL_SENSOR_GYRO_R is dead now that SDL3 is the floor. And sdl_protocol.hpp no longer includes SDL_config.h to test SDL_AUDIO_DISABLED: SDL3 does not install SDL_build_config.h, and it does not need to - verified that a -DSDL_AUDIO=0 SDL3 build still exports SDL_OpenAudioDeviceStream, SDL_PutAudioStreamData, SDL_ResumeAudioStreamDevice, SDL_DestroyAudioStream and SDL_AudioStreamDevicePaused, so the engine compiles and links either way and an audio-less build simply fails SDL_Init(SDL_INIT_AUDIO) at runtime. WASM moves to `-s USE_SDL=3`. Emscripten's sdl3 port exists at the EMSDK_VERSION the SDK pins and has SDL_JOYSTICK_EMSCRIPTEN and SDL_AUDIO_DRIVER_EMSCRIPTEN enabled, so both backends stay live in the browser; it does still warn that the port is experimental. Verified on linux-x86_64: game_controller_protocol.cpp, joystick_protocol.cpp and audio_engine.cpp compile clean against SDL3 3.4.14 under score's own flags and warnings, and audio_engine.cpp really does compile the SDL engine (the preprocessed TU contains class sdl_protocol and the stream API, with no SDL2 call left). A standalone harness mirroring the two rewritten paths confirms the behaviour: SDL_Init(JOYSTICK|GAMEPAD) succeeds, enumeration and GUID lookup work, an out-of-range index yields id 0, and the audio callback produced exactly 77 blocks of 256 frames with the expected paused -> resumed transition. No gamepad was attached to the test machine, so rumble, sensors and touchpad input are compile- and API-verified but not exercised against real hardware. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TTUi76cnUWh7dGZC64LqbZ
edumeneses
had a problem deploying
to
Apple Certificate
August 6, 2026 13:44 — with
GitHub Actions
Failure
edumeneses
temporarily deployed
to
Apple Certificate
August 6, 2026 13:44 — with
GitHub Actions
Inactive
edumeneses
had a problem deploying
to
Apple Certificate
August 6, 2026 13:44 — with
GitHub Actions
Failure
edumeneses
temporarily deployed
to
Apple Certificate
August 6, 2026 13:44 — with
GitHub Actions
Inactive
edumeneses
temporarily deployed
to
Apple Certificate
August 6, 2026 13:44 — with
GitHub Actions
Inactive
edumeneses
temporarily deployed
to
Apple Certificate
August 6, 2026 13:44 — with
GitHub Actions
Inactive
edumeneses
temporarily deployed
to
Apple Certificate
August 6, 2026 13:44 — with
GitHub Actions
Inactive
edumeneses
temporarily deployed
to
Apple Certificate
August 6, 2026 13:44 — with
GitHub Actions
Inactive
edumeneses
had a problem deploying
to
Apple Certificate
August 6, 2026 13:44 — with
GitHub Actions
Error
edumeneses
had a problem deploying
to
Apple Certificate
August 6, 2026 13:44 — with
GitHub Actions
Error
edumeneses
temporarily deployed
to
Apple Certificate
August 6, 2026 13:44 — with
GitHub Actions
Inactive
This was referenced Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The libossia half of the SDL3 move. Pairs with ossia/sdk#27 (SDK) and ossia/score#2176 (consumer).
Per @jcelerier's plan on ossia/sdk#28 — prove SDL3 works, then flip
sdl.shin place rather than shipping both SDLs — this is the prerequisite: an SDL3-only SDK can't be validated against libossia until libossia speaks SDL3.Most of it is upstream's rename table
SDL_oldnames.h(whatbuild-scripts/rename_symbols.pyapplies) covers the bulk:SDL_JoystickOpen→SDL_OpenJoystick,SDL_GameControllerHasAxis→SDL_GamepadHasAxis,SDL_CONTROLLER*→SDL_GAMEPAD*, theSDL_Controller*Eventstructs →SDL_Gamepad*Eventwith union memberscaxis/cbutton/ctouchpad/csensor→gaxis/gbutton/gtouchpad/gsensor, event enums →SDL_EVENT_*.Four things needed more than a rename
Device indices are gone. SDL2 enumerated
0..SDL_NumJoysticks()-1and mapped index → identity viaSDL_JoystickGetDeviceInstanceID. SDL3 has onlySDL_GetJoysticks(), returning an owned array of stableSDL_JoystickID.joystick_infokeeps its index-based signatures on purpose. An index into that array plays exactly the role SDL2's device index did, and is just as transient — valid until the device list changes. So score's enumeration loops and the(id, index)pair it serialises into save files keep working untouched. A newsdl_joystick_idsRAII helper owns the array; an out-of-range index yields SDL3's invalid id of0, whichjoystick_infotranslates back to the-1sentinel its callers expect.game_controller_protocolno longer needs the index at all (SDL_GetGamepadTypeForIDtakes the id), som_joystick_indexis gone.One tradeoff: each
joystick_infoaccessor now materialises the id array, so score'sfor i in 0..countloop is O(n²) allocations instead of O(n). With a handful of joysticks that's noise, and it keeps the helper trivially correct. Happy to hoist the list into the callers if you'd rather.Booleans changed shape.
SDL_Initand theHas*/SetSensorEnabledfamily returnbool, not an int status — so< 0,>= 0and== SDL_TRUEtests go away. Button events carrydowninstead ofstate == SDL_PRESSED.Rumble capabilities became properties.
SDL_GameControllerHasRumble/HasRumbleTriggers→SDL_GetBooleanProperty(SDL_GetGamepadProperties(...), SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN / ..._TRIGGER_RUMBLE_BOOLEAN, false).Also worth flagging: SDL3 merged
SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLEand_PS5_RUMBLEinto a singleSDL_HINT_JOYSTICK_ENHANCED_REPORTS(perdocs/README-migration.md), so those two switch cases collapse into one.The audio engine is a real rewrite
SDL3 has no pull callback filling a fixed-size buffer.
SDL_OpenAudioDeviceStreamhands us a stream and asks foradditional_amountbytes, which varies per call. The callback now services that request in whole blocks ofeffective_buffer_sizeframes, pushing each throughSDL_PutAudioStreamData, withSDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMESset to the requested block size so the device buffer matches. Overshooting by at most one block is fine — the stream buffers it.SDL_AudioSpeclostsamples,callbackanduserdata, soeffective_buffer_sizeis now what we asked for rather than what the device reported. The interleave scratch buffers are preallocated members instead of per-callbackalloca, which the fixed block size makes possible — no allocation in the audio callback. Status comes fromSDL_AudioStreamDevicePaused.Two guards dropped
The
SDL_VERSION_ATLEAST(2, 26, 0)fallback forSDL_SENSOR_GYRO_Ris dead with SDL3 as the floor.More interesting:
sdl_protocol.hppno longer includesSDL_config.hto testSDL_AUDIO_DISABLED. SDL3 doesn't installSDL_build_config.h— and doesn't need to. I checked a-DSDL_AUDIO=0SDL3 build and it still exportsSDL_OpenAudioDeviceStream,SDL_PutAudioStreamData,SDL_ResumeAudioStreamDevice,SDL_DestroyAudioStreamandSDL_AudioStreamDevicePaused. So the engine compiles and links either way, and an audio-less build just failsSDL_Init(SDL_INIT_AUDIO)at runtime. That matters because the SDK builds SDL withSDL_AUDIO=0on every platform.WASM
-s USE_SDL=2→-s USE_SDL=3, i.e. option (1) from the ossia/sdk#27 discussion. Emscripten'ssdl3port exists at theEMSDK_VERSIONthe SDK pins and has bothSDL_JOYSTICK_EMSCRIPTENandSDL_AUDIO_DRIVER_EMSCRIPTENenabled, so both backends stay live in the browser. It does still printsdl3 port is still experimental, and it ships SDL 3.4.2 against the 3.4.14 built natively.This is the part that most needs your full-WASM verification — it's the one behaviour I can't check from here.
Testing (linux-x86_64, SDL3 3.4.14)
Compiled under score's own flags and warnings, not a synthetic setup:
game_controller_protocol.cpp,joystick_protocol.cpp,audio_engine.cpp— clean.audio_engine.cppreally compiles the SDL engine rather than skipping it via__has_include: the preprocessed TU containsclass sdl_protocoland 8 references to the stream API, and the onlySDL_OpenAudioDeviceleft is SDL3's own (different signature) declaration in its header.SDL_Init(JOYSTICK|GAMEPAD)succeeds, enumeration + GUID lookup +SDL_IsGamepadwork, out-of-range index → id 0, and the audio callback produced exactly 77 blocks × 256 frames = 19712 with the expectedpaused=1→paused=0transition.Not covered: real hardware. No gamepad was attached to the test machine, so rumble, sensors and touchpad input are compile- and API-verified only. Someone with a DualSense or an Xbox pad should exercise those before this goes in.
🤖 Generated with Claude Code
https://claude.ai/code/session_01TTUi76cnUWh7dGZC64LqbZ