Skip to content

sdl: ship SDL3 alongside SDL2 - #27

Draft
edumeneses wants to merge 3 commits into
ossia:masterfrom
edumeneses:sdl/add-sdl3
Draft

edumeneses wants to merge 3 commits into
ossia:masterfrom
edumeneses:sdl/add-sdl3

Conversation

@edumeneses

Copy link
Copy Markdown

Closes #6.

Adds an SDL3 3.4.14 build to every platform that already builds SDL2, installed to $INSTALL_PREFIX/SDL3 next 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:

Where What
src/ossia/protocols/joystick/{joystick,game_controller}_protocol.cpp, joystick_manager.hpp SDL2 spellings throughout — SDL_JoystickOpen, SDL_NumJoysticks, SDL_JoystickGetDeviceInstanceID, the whole SDL_GameController* family, SDL_CONTROLLER* event enums. All renamed or restructured in SDL3.
src/ossia/audio/sdl_protocol.hpp SDL_OpenAudioDevice's callback form + SDL_GetAudioDeviceStatus, replaced by audio streams in SDL3.
cmake/deps/sdl.cmake, cmake/OssiaOptions.cmake (and score's CMakeLists.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_SDL to FALSE, and score's score_assert_feature(sdl) turns that into a hard failure on any deployment build. Side-by-side keeps every sdkNN tag consumable by today's score while the port lands in libossia; dropping SDL2 afterwards is then a mechanical delete of SDL_VERSION and the four sdl.sh scripts.

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:

  • Gone as options (SDL3 always builds them): 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_BUILD is required on Linux. Since we build with both SDL_X11=0 and SDL_WAYLAND=0, SDL3 otherwise hard-stops at configure: SDL could not find X11 or Wayland development libraries on your system.
  • SDL_HIDAPI / SDL_HIDAPI_JOYSTICK set explicitly — these are what make the extra gamepad surface (PS4/PS5 rumble, gyro/accel, touchpad fingers) reachable at all.
  • SDL_TESTS defaults ON for a top-level SDL3 project, so it plus SDL_TEST_LIBRARY / SDL_EXAMPLES / SDL_INSTALL_CPACK are turned off.
  • macOS drops the framework Resources → cmake symlink: SDL_FRAMEWORK defaults OFF in SDL3 and the install lands a normal lib/cmake/SDL3 tree.
  • The IBUS_FOUND -> 0 sed isn't carried over — SDL3 guards that block behind SDL_IBUS, already 0.
  • SDL_DYNAMIC_API=0 moved from -DCMAKE_C_FLAGS into $CFLAGS/$CXXFLAGS. Passing CMAKE_C_FLAGS on the command line makes cmake ignore the environment, which dropped everything common.sh sets — including macOS's -arch, so the cross-built x86_64 leg was getting host-arch objects. Worth fixing in macOS/sdl.sh too, but I left SDL2 untouched here.

Core hash

SDL3_VERSION sits outside the allowlist in .github/actions/core-hash, so this rotates the media stage only — verified the hash is byte-identical to master:

branch: 3e7c834e268d4f74a66851465e71a42baccd5201d9587e0d33cfb9cb4dc0549b
master: 3e7c834e268d4f74a66851465e71a42baccd5201d9587e0d33cfb9cb4dc0549b

Testing

Ran Linux/sdl3.sh end to end on linux-x86_64 in a container-shaped harness (/image + bind-mounted common/, so the ./common/clone-sdl3.sh and ../common/versions.sh source paths are exercised as they are in CI):

  • clone-sdl3.sh fetches and patches the tarball; the #error Nope deletion is load-bearing — without it the build fails on SDL_dynapi.c and the PCH.
  • Static lib builds and installs; find_package(SDL3 REQUIRED CONFIG) resolves from the prefix, SDL3::SDL3-static links, and the binary runs SDL_Init(JOYSTICK|GAMEPAD|HAPTIC|SENSOR) successfully against a video-less, audio-less build → SDL 3.4.14, joysticks: 0.
  • Joystick drivers reported by configure: hidapi linux virtual.
  • dynapi indirection compiled out (no SDL_InitDynamicAPI), zero absolute 32-bit relocations, and SDL_RumbleGamepad / SDL_SetGamepadSensorEnabled / SDL_GetGamepadTouchpadFinger all present in the archive.
  • bash -n clean 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 an sdl3.sh for consistency but isn't in CI (its common.sh is a dangling symlink into /home/jcelerier/...).
  • MSVC/sdl.ps1 left alone — pins Visual Studio 2017 and SDL 2.0.9, not wired into any workflow.
  • WASM has no SDL at all; libossia uses emscripten's -sUSE_SDL=2 port. Moving that to --use-port=sdl3 is 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.cmake and the prefix paths in OssiaOptions.cmake / score's CMakeLists.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

@jcelerier

Copy link
Copy Markdown
Member

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

@edumeneses

Copy link
Copy Markdown
Author

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 WASM

The WASM SDK never built SDL at all — there is no WASM/sdl.sh, WASM/all.sh doesn't call one, and grep -riI sdl WASM/ is empty. WASM's SDL comes entirely from emscripten's own bundled port, requested in two places on the consumer side:

  • libossia/cmake/deps/sdl.cmake — the CMAKE_SYSTEM_NAME MATCHES Emscripten branch, -s USE_SDL=2 as compile + link options on ossia::sdl2
  • score/src/app/CMakeLists.txt:20 — add_link_options("SHELL:-sUSE_SDL=2")

This PR only adds sdl3.sh to Linux / macOS / MSYS / AArch64 plus common/clone-sdl3.sh, and doesn't touch WASM/ or wasm.yml. The WASM leg is already green on this branch.

Emscripten has SDL3, flagged experimental

tools/ports/sdl3.py exists and is present at tag 5.0.5, which is exactly our EMSDK_VERSION pin — so the toolchain we ship can already do it. Selector is -sUSE_SDL=3 (needed() returns settings.USE_SDL == 3). Caveats:

emscripten sdl2 port emscripten sdl3 port
SDL version 2.32.10 — same as our SDL_VERSION 3.4.2 — behind the 3.4.14 this PR builds natively
status no warning, not deprecated diagnostics.warning('experimental', 'sdl3 port is still experimental')
joystick emscripten driver SDL_JOYSTICK_EMSCRIPTEN 1 + SDL_JOYSTICK_VIRTUAL 1
haptic / sensor dummy dummy
threads — SDL_THREADS_DISABLED 1, with a separate sdl3-mt variant for PTHREADS=1

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

OSSIA_PROTOCOL_JOYSTICK defaults ON and is not gated off for emscripten, so joystick_protocol.cpp / game_controller_protocol.cpp are compiled on the WASM leg today against emscripten's SDL2. Since that's one codebase, porting it to SDL3 forces a choice on the WASM leg:

  1. move it to -sUSE_SDL=3 and accept the experimental port (+ pick sdl3-mt if the wasm build wants pthreads), or
  2. gate OSSIA_PROTOCOL_JOYSTICK off for emscripten and drop gamepad support in the browser.

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.

@edumeneses

Copy link
Copy Markdown
Author

Opened #28 to track removing the transitional SDL2 build once libossia is ported, so the side-by-side layout doesn't quietly become permanent.

@edumeneses

Copy link
Copy Markdown
Author

@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.

OSSIA_PROTOCOL_JOYSTICK is ON by default and isn't gated off for emscripten, so joystick_protocol.cpp / game_controller_protocol.cpp compile on the WASM leg today against emscripten's SDL2 port. It's one codebase, so porting it to SDL3 forces a choice:

  1. Move WASM to -sUSE_SDL=3. Works — the port exists at our EMSDK_VERSION=5.0.5 pin and has SDL_JOYSTICK_EMSCRIPTEN/SDL_JOYSTICK_VIRTUAL enabled. Costs: emscripten still prints sdl3 port is still experimental, the port ships SDL 3.4.2 against 3.4.14 natively, and its build config sets SDL_THREADS_DISABLED unless you take the sdl3-mt variant.
  2. Gate OSSIA_PROTOCOL_JOYSTICK off for emscripten. No experimental dependency, but no gamepads in the browser.
  3. Keep WASM on SDL2 for now. Possible only because sdl: ship SDL3 alongside SDL2 #27 keeps shipping SDL2 2.32.10, which matches emscripten's sdl2 port version exactly — native goes SDL3, WASM stays put. Costs an #if defined(__EMSCRIPTEN__) split through the joystick code, which is the ugliest of the three to maintain.

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.

@jcelerier

Copy link
Copy Markdown
Member

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

@jcelerier

Copy link
Copy Markdown
Member

in any case the best would be to do a complete WASM build with this sdk and enforce SDL3, and verify that it works

@edumeneses

Copy link
Copy Markdown
Author

CI verdict from the first run, and a fix pushed for the two real failures.

Build SDK WASM: green. Build SDK: 3 of 13 jobs failed — all six cores green, media-linux-x86_64 / media-linux-aarch64 / media-windows-arm64 green.

media-macos-arm64 and media-macos-x86_64 — my fault, fixed in 900b4c0

Both trace to one decision in the first commit: I passed $CFLAGS through to SDL3 instead of overriding CMAKE_C_FLAGS the way sdl.sh does. I'd treated the override as a latent bug (it silently drops -arch, so the x86_64 leg builds host-arch objects) — turns out it was also load-bearing, because it hid the rest of macOS/common.sh's flags from SDL.

  • arm64: -ffast-math reaches the compiler and SDL_audiotypecvt.c guards a #pragma STDC FENV_ACCESS ON behind __aarch64__ + clang ≥ 12 → error: '#pragma STDC FENV_ACCESS ON' is illegal when precise is disabled. Fixed with -fno-fast-math for this build only.
  • x86_64: CPUFLAGS carries -arch x86_64 -arch x86_64h, and SDL3 (unlike SDL2) compiles through a PCH → clang: error: cannot use 'precompiled-header' output with multiple -arch options. Fixed with CMAKE_DISABLE_PRECOMPILE_HEADERS. Unavoidable for a fat build however the arch reaches clang — routing it via CMAKE_OSX_ARCHITECTURES still emits both -arch on one invocation.

I kept the passthrough rather than reverting, since fat objects are the point of that leg. Neither linux common.sh nor MSYS sets -ffast-math or a second -arch, which is why those legs were already green.

media-windows-x86_64 — infra flake, not this branch

curl: (28) Failed to connect to www.libsdl.org:443 after 21089 ms: Could not connect to server
tar (child): SDL2-2.32.10.tar.gz: Cannot open: No such file or directory
CMake Error: The source directory "D:/a/sdk/sdk/MSYS/SDL2-2.32.10" does not exist.

That's the existing common/clone-sdl.sh failing to reach libsdl.org for the SDL2 tarball — windows-arm64 pulled the same file fine in the same run. clone-sdl3.sh fetches from github.com and didn't flake. Incidentally an argument for your plan below: moving sdl.sh to SDL3 also moves it off libsdl.org.

@edumeneses

Copy link
Copy Markdown
Author

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 SDL_AUDIO=0 doesn't break the audio engine. A -DSDL_AUDIO=0 SDL3 build still exports SDL_OpenAudioDeviceStream, SDL_PutAudioStreamData, SDL_ResumeAudioStreamDevice, SDL_DestroyAudioStream and SDL_AudioStreamDevicePaused. So sdl_protocol.hpp no longer needs the SDL_config.h / SDL_AUDIO_DISABLED guard at all — which is just as well, since SDL3 doesn't install SDL_build_config.h.

avendish needs no PR. Its bare SDL2 link references are all behind the if(1) short-circuit or an if(TARGET ossia::ossia) score never enters — no link.txt in a configured build mentions SDL2.

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 sdl.sh in place per #28 and drop the side-by-side SDL2.


Written by Claude (Anthropic), acting as Edu's assistant.

edumeneses added a commit to ossia/score that referenced this pull request Aug 6, 2026
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
@edumeneses

Copy link
Copy Markdown
Author

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 sdl feature goes missing. They don't. score_assert_feature is a no-op:

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: list(APPEND) inside a function() without PARENT_SCOPE never reaches the variable the later FATAL_ERROR tests, and IN_LIST is handed a dereferenced "${SCORE_FEATURES_LIST}" when it wants the list's name. Confirmed with a minimal CMake repro: asserting a definitely-missing feature leaves SCORE_MISSING_FEATURES empty. Same applies to score_assert_plugin. That also explains how the Debian legs pass today with their libsdl2-dev lines commented out.

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 versions.sh that repeated the wrong claim.

I have not touched score_assert_feature itself — that's an unrelated latent bug, and fixing it would turn several legs red at once. Worth its own issue in ossia/score if you want it.

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 org.kde.Platform runtime, so it needed SDL3 bundled like every other dep in that manifest. And of the distro legs, only Debian trixie/sid and Ubuntu plucky/questing actually ship libsdl3 — jammy, noble, lunar and oracular have none, so those lose the joystick protocol until someone decides whether to build SDL3 from source there.


Written by Claude (Anthropic), acting as Edu's assistant.

jcelerier pushed a commit to ossia/libossia that referenced this pull request Aug 26, 2026
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
jcelerier pushed a commit to ossia/score that referenced this pull request Aug 26, 2026
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
edumeneses added a commit to edumeneses/score that referenced this pull request Oct 10, 2026
HOLD_PRS lists "<pr>=<owner/repo>#<n>" pairs: the PR stays out of the
build until <n> is merged, then goes in by itself. The release body lists
the held PRs and what they wait for.

ossia#2176 (SDL3) is held on ossia/sdk#27: the SDK the AppImage builds against
has no SDL3 before it, so libossia would turn SDL off and the AppImage
would lose the joystick protocol and the SDL audio engine without failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lc7tYGHRn6TmaxMEfBTswW
edumeneses and others added 3 commits October 9, 2026 22:36
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
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
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
@edumeneses

Copy link
Copy Markdown
Author

This is Edu's Claude assistant, posting on Edu's behalf.

Rebased onto master (50 commits behind). Same three commits. The only conflict was common/versions.sh, where master added the ffmpeg 9 media-deps pins: SDL3_VERSION=3.4.14 now sits right after SDL_VERSION, and master's FFMPEG_VERSION=9.0 replaces the old 8.1 the branch still carried.

This is what ossia/score#2176 and ossia/libossia#922 wait on: without SDL3 in the SDK, libossia turns SDL off and the joystick protocol and the SDL audio engine silently drop out of the release builds.

edumeneses added a commit to ossia/libossia that referenced this pull request Oct 11, 2026
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 added a commit to ossia/score that referenced this pull request Oct 11, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sdl: update to sdl3

2 participants