sdl: ship SDL3 alongside SDL2 - #27
Conversation
Adds an SDL3 3.4.14 build to every platform that already builds SDL2, installed
to $INSTALL_PREFIX/SDL3 so it sits next to the existing $INSTALL_PREFIX/SDL2.
SDL3's ABI has been stable since 3.2.0 (Jan 2025) and the breaking API churn
predates it, so there is nothing left to wait for on the upstream side.
SDL2 stays because the change CANNOT be contained to this repo otherwise.
libossia is what actually consumes it, and it is SDL2-only in three places:
the joystick/game-controller protocols and the SDL audio engine use the SDL2
spellings throughout (SDL_JoystickOpen, SDL_NumJoysticks, SDL_GameController*,
SDL_OpenAudioDevice's callback form -- all renamed or replaced in SDL3),
cmake/deps/sdl.cmake does find_package(SDL2 CONFIG) and exports ossia::sdl2,
and the SDK prefix paths in OssiaOptions.cmake / score's CMakeLists.txt point
at $OSSIA_SDK/SDL2. Replacing SDL2 in place would silently flip
OSSIA_ENABLE_SDL to FALSE, which score's score_assert_feature(sdl) turns into a
hard failure on any deployment build. Side-by-side keeps every sdkNN tag
consumable by today's score while that port happens in libossia; removing SDL2
afterwards is then a mechanical delete of SDL_VERSION and the sdl.sh scripts.
The flag sets are the SDL2 ones translated to SDL3:
- SDL_EVENTS, SDL_FILE, SDL_ATOMIC, SDL_THREADS, SDL_TIMERS, SDL_LOADSO,
SDL_CPUINFO, SDL_FILESYSTEM, SDL_LIBSAMPLERATE, SDL_DLOPEN and SDL_SYSTEM
are no longer options -- SDL3 always builds those.
- SDL_STATIC_PIC is gone; CMAKE_POSITION_INDEPENDENT_CODE replaces it.
- SDL_UNIX_CONSOLE_BUILD is required on Linux: since we build with both
SDL_X11=0 and SDL_WAYLAND=0, SDL3 otherwise stops at configure time with
"SDL could not find X11 or Wayland development libraries on your system".
- SDL_HIDAPI/SDL_HIDAPI_JOYSTICK are explicit. They are what makes the extra
gamepad surface (PS4/PS5 rumble, gyro/accel sensors, touchpad fingers)
actually reachable, which is the point of the upgrade.
- SDL_TESTS defaults to ON for a top-level SDL3 project, so it and
SDL_TEST_LIBRARY/SDL_EXAMPLES/SDL_INSTALL_CPACK are turned off.
- The macOS build no longer symlinks a framework Resources dir into place:
SDL_FRAMEWORK defaults to OFF in SDL3 and the install lands a normal
lib/cmake/SDL3 tree. It is passed explicitly all the same.
- SDL_DYNAMIC_API=0 moves from -DCMAKE_C_FLAGS to $CFLAGS/$CXXFLAGS. Passing
CMAKE_C_FLAGS on the command line makes cmake ignore the environment, which
dropped every flag common.sh sets -- including macOS's -arch, so the
cross-built x86_64 leg was getting host-arch objects.
- The IBUS_FOUND -> 0 sed is not carried over: SDL3 guards that block behind
SDL_IBUS, which we already set to 0.
SDL3_VERSION is deliberately outside the core-hash allowlist in
.github/actions/core-hash, so this rotates the media stage only and every
published core keeps being reused.
Verified on linux-x86_64 end to end: clone-sdl3.sh fetches and patches the
tarball, the static lib builds and installs, find_package(SDL3 CONFIG) resolves
from the prefix, SDL3::SDL3-static links, and the result runs SDL_Init with
JOYSTICK|GAMEPAD|HAPTIC|SENSOR against a video- and audio-less build. Confirmed
the dynapi indirection is compiled out (no SDL_InitDynamicAPI), there are no
absolute 32-bit relocations, and SDL_RumbleGamepad /
SDL_SetGamepadSensorEnabled / SDL_GetGamepadTouchpadFinger are all present.
AArch64/ gets an sdl3.sh for consistency but is not in CI (its common.sh is a
dangling symlink into a developer's home). MSVC/sdl.ps1 is left alone: it pins
Visual Studio 2017 and SDL 2.0.9 and is not wired into any workflow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TTUi76cnUWh7dGZC64LqbZ
|
one thing to check with SDL: that it doesn't break the SDL API shipped by emscripten, I don't know if they updated to SDL3 too |
|
Checked. Short version: this PR can't touch the emscripten SDL, and emscripten did add SDL3 — but its port is marked experimental, which makes it a real question for the libossia half rather than this one. This PR doesn't go near WASMThe WASM SDK never built SDL at all — there is no
This PR only adds Emscripten has SDL3, flagged experimental
So the capability is there and joystick specifically is fine. The friction is the experimental warning, the 3.4.2-vs-3.4.14 skew, and the threads variant. Where it actually bites
I'd lean on (1) — browser gamepads come from the Gamepad API either way and the emscripten joystick driver is enabled in the SDL3 port's build config — but that's your call and it belongs in the libossia PR, not here. Nothing about it changes this diff. Worth noting the side-by-side layout helps here specifically: because the SDK keeps shipping SDL2 2.32.10 (matching emscripten's sdl2 port exactly), you can move native to SDL3 and leave the WASM leg on SDL2 for a while if the experimental port turns out to be a problem. |
|
Opened #28 to track removing the transitional SDL2 build once libossia is ported, so the side-by-side layout doesn't quietly become permanent. |
|
@jcelerier one decision needed from you before the libossia side starts — flagging it here rather than guessing, since it changes the shape of that PR. Edu and I went through the options internally first; this is the one we couldn't settle without you.
Our read is (1): browser gamepads come from the Gamepad API either way, the experimental warning looks like caution about API coverage we don't use (video/render/GPU) rather than about joystick, and (3)'s dual-API code is the thing we'd least like to live with. But you know what the wasm build actually needs from pthreads and how much you want to lean on an experimental emscripten port, so it's your call. Happy to go with (2) if you'd rather keep the WASM leg conservative — score's browser build losing gamepad input is a much smaller loss than the native releases staying on SDL2. Written by Claude (Anthropic), acting as Edu's assistant — Edu has reviewed the analysis above. |
|
note that for SDL2 it's not part of the WASM sdk we build because it's already a built-in part of the emscripten SDK |
|
in any case the best would be to do a complete WASM build with this sdk and enforce SDL3, and verify that it works |
Both macOS legs failed in CI, and both trace back to this branch passing $CFLAGS through to the SDL3 build instead of overriding CMAKE_C_FLAGS the way sdl.sh does. Overriding it made cmake ignore the environment, which is what had been hiding macOS/common.sh's flags from SDL all along. arm64: -ffast-math reaches the compiler, and SDL_audiotypecvt.c guards a `#pragma STDC FENV_ACCESS ON` behind __aarch64__ + clang >= 12, so SDL3-3.4.14/src/audio/SDL_audiotypecvt.c:541:14: error: '#pragma STDC FENV_ACCESS ON' is illegal when precise is disabled -fno-fast-math restores precise FP for this build only. Nothing in SDL wants fast-math semantics; the flag comes from common.sh for the DSP libraries. x86_64: CPUFLAGS carries `-arch x86_64 -arch x86_64h` for the fat build, and SDL3 (unlike SDL2) compiles through a precompiled header, so clang: error: cannot use 'precompiled-header' output with multiple -arch options CMAKE_DISABLE_PRECOMPILE_HEADERS turns the PCH off. This is unavoidable for a fat build regardless of how the arch reaches the compiler -- routing it through CMAKE_OSX_ARCHITECTURES instead still emits both -arch flags on one clang invocation. Keeping the passthrough rather than reverting to the CMAKE_C_FLAGS override is deliberate: the override silently dropped -arch too, so the x86_64 leg was building host-arch objects. Fat objects are the point of that leg. The linux legs and windows-arm64 are unaffected -- neither common.sh sets -ffast-math or a second -arch -- and both were already green. windows-x86_64's failure is unrelated to SDL3 and to this branch: curl could not reach www.libsdl.org for the SDL2 tarball ("Failed to connect to www.libsdl.org:443 after 21089 ms"), so the pre-existing common/clone-sdl.sh step died. windows-arm64 pulled the same tarball fine in the same run. clone-sdl3.sh fetches from github.com, which did not flake. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TTUi76cnUWh7dGZC64LqbZ
|
CI verdict from the first run, and a fix pushed for the two real failures.
|
|
The libossia + score halves are up as drafts, so the prerequisite for your plan on #28 is in place:
Two findings that matter for this PR: The SDK's avendish needs no PR. Its bare Still open on your side: the full WASM build with SDL3 enforced. That's the one thing I can't verify from here, and it's your explicit gate. Once it passes I'll flip this PR to modify Written by Claude (Anthropic), acting as Edu's assistant. |
Points the SDK prefix paths at $OSSIA_SDK/SDL3 (see ossia/sdk#27), renames the ossia::sdl2 link target to ossia::sdl3, moves the Emscripten link option to -sUSE_SDL=3, and bumps libossia to the branch carrying the API port. The libossia bump also picks up the 13 commits of master the submodule was behind; none of them touch SDL. avendish is deliberately untouched. avendish.ossia.cmake and avendish.standalone.cmake do reference a bare SDL2 link target, but every one of those references sits behind either the `if(1)` short-circuit at the top of avendish.ossia.cmake or an `if(TARGET ossia::ossia)` that score's build does not enter -- no link.txt in a configured score build mentions SDL2, and the only libSDL2.so left in build.ninja comes from system ffmpeg's avdevice pkg-config ldflags, which is unrelated. Verified on linux-x86_64 against SDL3 3.4.14: JoystickDevice.cpp, JoystickProtocolFactory.cpp, JoystickProtocolSettingsWidget.cpp, JoystickSpecificSettingsSerialization.cpp and score_plugin_audio.cpp (which is what pulls in Audio/SDLInterface.hpp) all compile clean under this build's own flags and warnings. joystick_info keeps its index-based API, so the enumeration loops in JoystickDevice and JoystickProtocolFactory and the (id, index) pair in JoystickSpecificSettings needed no change and existing save files keep loading. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TTUi76cnUWh7dGZC64LqbZ
The comment claimed score's deployment builds hard-assert the sdl feature.
They do not, in practice: score_assert_feature does list(APPEND
SCORE_MISSING_FEATURES ...) inside a function() with no PARENT_SCOPE, so the
variable its FATAL_ERROR tests is always empty, and it also passes a
dereferenced "${SCORE_FEATURES_LIST}" to IN_LIST, which expects the list's
name. Dropping SDL2 before libossia is ported therefore loses the joystick
protocol and the SDL audio engine from the releases silently rather than loudly,
which is a weaker argument for shipping both SDLs but still an argument.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TTUi76cnUWh7dGZC64LqbZ
|
Correction to something I asserted earlier in this PR and in #28, since it was load-bearing for the side-by-side argument. I said score's deployment builds hard-fail when the function(score_assert_feature name)
if(NOT "${name}" IN_LIST "${SCORE_FEATURES_LIST}")
list(APPEND SCORE_MISSING_FEATURES "${name}") # no PARENT_SCOPE
endif()
endfunction()Two problems: What this changes: dropping SDL2 before libossia is ported loses the joystick protocol and the SDL audio engine from the releases silently rather than failing the build. Still a reason not to swap it out from under un-ported consumers, but a weaker one than I gave. Pushed ee21443 to fix the comment in I have not touched One more thing the port surfaced, in ossia/score#2176: the prefix-path change only covers $OSSIA_SDK consumers. The flatpak had no SDL module at all and was taking SDL2 from the Written by Claude (Anthropic), acting as Edu's assistant. |
Closes #6.
Adds an SDL3 3.4.14 build to every platform that already builds SDL2, installed to
$INSTALL_PREFIX/SDL3next to the existing$INSTALL_PREFIX/SDL2. This unblocks the gamepad work — the releases are no longer stuck on SDL2.SDL3's ABI has been stable since 3.2.0 (Jan 2025); the breaking API churn all predates it and upstream is now on its 15th stable release. Nothing left to wait for.
Why SDL2 stays
I looked at whether this could be contained to this repo. It can't be, if SDL2 is replaced. libossia is the actual consumer and it is SDL2-only in three separate places:
src/ossia/protocols/joystick/{joystick,game_controller}_protocol.cpp,joystick_manager.hppSDL_JoystickOpen,SDL_NumJoysticks,SDL_JoystickGetDeviceInstanceID, the wholeSDL_GameController*family,SDL_CONTROLLER*event enums. All renamed or restructured in SDL3.src/ossia/audio/sdl_protocol.hppSDL_OpenAudioDevice's callback form +SDL_GetAudioDeviceStatus, replaced by audio streams in SDL3.cmake/deps/sdl.cmake,cmake/OssiaOptions.cmake(and score'sCMakeLists.txt)find_package(SDL2 CONFIG)→ossia::sdl2, and prefix paths pointing at$OSSIA_SDK/SDL2.Swapping SDL2 out from under that would silently flip
OSSIA_ENABLE_SDLtoFALSE, and score'sscore_assert_feature(sdl)turns that into a hard failure on any deployment build. Side-by-side keeps everysdkNNtag consumable by today's score while the port lands in libossia; dropping SDL2 afterwards is then a mechanical delete ofSDL_VERSIONand the foursdl.shscripts.Happy to switch this to a hard replace if you'd rather sequence it the other way — it's a one-line change to each
CMAKE_INSTALL_PREFIX.Flag translation
The SDL2 flag sets carried over to SDL3, with the differences that matter:
SDL_EVENTS,SDL_FILE,SDL_ATOMIC,SDL_THREADS,SDL_TIMERS,SDL_LOADSO,SDL_CPUINFO,SDL_FILESYSTEM,SDL_LIBSAMPLERATE,SDL_DLOPEN,SDL_SYSTEM.SDL_STATIC_PIC→CMAKE_POSITION_INDEPENDENT_CODE.SDL_UNIX_CONSOLE_BUILDis required on Linux. Since we build with bothSDL_X11=0andSDL_WAYLAND=0, SDL3 otherwise hard-stops at configure:SDL could not find X11 or Wayland development libraries on your system.SDL_HIDAPI/SDL_HIDAPI_JOYSTICKset explicitly — these are what make the extra gamepad surface (PS4/PS5 rumble, gyro/accel, touchpad fingers) reachable at all.SDL_TESTSdefaults ON for a top-level SDL3 project, so it plusSDL_TEST_LIBRARY/SDL_EXAMPLES/SDL_INSTALL_CPACKare turned off.Resources→cmakesymlink:SDL_FRAMEWORKdefaults OFF in SDL3 and the install lands a normallib/cmake/SDL3tree.IBUS_FOUND -> 0sed isn't carried over — SDL3 guards that block behindSDL_IBUS, already 0.SDL_DYNAMIC_API=0moved from-DCMAKE_C_FLAGSinto$CFLAGS/$CXXFLAGS. PassingCMAKE_C_FLAGSon the command line makes cmake ignore the environment, which dropped everythingcommon.shsets — including macOS's-arch, so the cross-built x86_64 leg was getting host-arch objects. Worth fixing inmacOS/sdl.shtoo, but I left SDL2 untouched here.Core hash
SDL3_VERSIONsits outside the allowlist in.github/actions/core-hash, so this rotates the media stage only — verified the hash is byte-identical to master:Testing
Ran
Linux/sdl3.shend to end on linux-x86_64 in a container-shaped harness (/image+ bind-mountedcommon/, so the./common/clone-sdl3.shand../common/versions.shsource paths are exercised as they are in CI):clone-sdl3.shfetches and patches the tarball; the#error Nopedeletion is load-bearing — without it the build fails onSDL_dynapi.cand the PCH.find_package(SDL3 REQUIRED CONFIG)resolves from the prefix,SDL3::SDL3-staticlinks, and the binary runsSDL_Init(JOYSTICK|GAMEPAD|HAPTIC|SENSOR)successfully against a video-less, audio-less build →SDL 3.4.14, joysticks: 0.hidapi linux virtual.SDL_InitDynamicAPI), zero absolute 32-bit relocations, andSDL_RumbleGamepad/SDL_SetGamepadSensorEnabled/SDL_GetGamepadTouchpadFingerall present in the archive.bash -nclean on all ten touched scripts.macOS and Windows are unverified locally — that's what this PR's CI run is for.
Not touched
AArch64/gets ansdl3.shfor consistency but isn't in CI (itscommon.shis a dangling symlink into/home/jcelerier/...).MSVC/sdl.ps1left alone — pins Visual Studio 2017 and SDL 2.0.9, not wired into any workflow.-sUSE_SDL=2port. Moving that to--use-port=sdl3is a libossia-side change.Follow-up
The libossia port is the other half of this. I can take it next: ~130 SDL references across 6 files, plus
deps/sdl.cmakeand the prefix paths inOssiaOptions.cmake/ score'sCMakeLists.txt. Mostly mechanical renames; the audio engine's callback → stream conversion is the only real rewrite.🤖 Generated with Claude Code
https://claude.ai/code/session_01TTUi76cnUWh7dGZC64LqbZ