diff --git a/docs/steam-compat-tool-interface.md b/docs/steam-compat-tool-interface.md
index ebeac3c6189b8faf64983efafc018ffee65acfb1..7fb910d69725725b9ca15fb6efe8a1e199dfad23 100644
--- a/docs/steam-compat-tool-interface.md
+++ b/docs/steam-compat-tool-interface.md
@@ -298,6 +298,24 @@ Some environment variables are set by Steam, including:
     game's metadata, affecting the functioning of various compatibility
     tools:
 
+    * `runtime-sdl2`: Use the [`$SDL_DYNAMIC_API` mechanism][SDL_DYNAMIC_API]
+        to load the version of SDL 2 included in
+        Steam Linux Runtime 2 (soldier) or later,
+        in preference to a bundled or statically linked SDL 2 that might be
+        included in the game itself.
+        For example, this can be used to ensure that older Source-engine
+        titles like [Fistful of Frags][]
+        runs with a version of SDL that supports game controller hotplug
+        inside a container, even though they include a bundled version
+        that does not.
+        Setting `STEAM_COMPAT_RUNTIME_SDL2=1` is equivalent to this,
+        and is more convenient to use in Launch Options, by setting the
+        Launch Options to `STEAM_COMPAT_RUNTIME_SDL2=1 %command%`.
+
+    * `runtime-sdl3`: Same as `runtime-sdl2`, but for SDL 3 (if available),
+        using [`$SDL3_DYNAMIC_API`][SDL3_DYNAMIC_API].
+        Setting `STEAM_COMPAT_RUNTIME_SDL3=1` is equivalent to this.
+
     * `search-cwd`: Instructs the legacy `LD_LIBRARY_PATH` runtime to
         append the `${STEAM_COMPAT_INSTALL_PATH}` to the `LD_LIBRARY_PATH`,
         for backward compatibility with older games that might rely on this
@@ -368,6 +386,20 @@ Some environment variables are set by Steam, including:
     Colon-delimited list of paths to additional directories that are to be
     made available read/write in a pressure-vessel container.
 
+* `STEAM_COMPAT_RUNTIME_SDL2`:
+
+    May be set to 1 for the same effect as adding `runtime-sdl2` to
+    `$STEAM_COMPAT_FLAGS`.
+    (For example, a game's Launch Options can be set to
+    `STEAM_COMPAT_RUNTIME_SDL2=1 %command%`)
+
+* `STEAM_COMPAT_RUNTIME_SDL3`:
+
+    May be set to 1 for the same effect as adding `runtime-sdl3` to
+    `$STEAM_COMPAT_FLAGS`.
+    (For example, a game's Launch Options can be set to
+    `STEAM_COMPAT_RUNTIME_SDL3=1 %command%`)
+
 * `STEAM_COMPAT_SESSION_ID`:
 
     In the historical session mode (see below), this is used to link together
@@ -611,6 +643,8 @@ unless this is set to the game's top-level directory.
 
 <!-- References: -->
 
+[SDL_DYNAMIC_API]: https://github.com/libsdl-org/SDL/blob/SDL2/docs/README-dynapi.md
+[SDL3_DYNAMIC_API]: https://github.com/libsdl-org/SDL/blob/main/docs/README-dynapi.md
 [Steamworks launch options]: https://partner.steamgames.com/doc/sdk/uploading
 [VDF]: https://developer.valvesoftware.com/wiki/KeyValues
 [app891390-info]: https://steamdb.info/app/891390/info/
diff --git a/pressure-vessel/adverb-sdl.c b/pressure-vessel/adverb-sdl.c
new file mode 100644
index 0000000000000000000000000000000000000000..72474c4b687f7f801f00a588ce952cc8fd72e38d
--- /dev/null
+++ b/pressure-vessel/adverb-sdl.c
@@ -0,0 +1,148 @@
+/*
+ * Copyright © 2024 Collabora Ltd.
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+#include "adverb-sdl.h"
+
+gboolean
+pv_adverb_set_up_dynamic_sdl (FlatpakBwrap *wrapped_command,
+                              PvPerArchDirs *lib_temp_dirs,
+                              const char *prefix,
+                              const char *overrides,
+                              const char *dynamic_var,
+                              const char *soname,
+                              GError **error)
+{
+  gboolean have_any = FALSE;
+  size_t abi_index;
+  const char *existing_value;
+
+  existing_value = g_environ_getenv (wrapped_command->envp, dynamic_var);
+
+  if (existing_value != NULL)
+    {
+      /* Treat SDL{,3}_DYNAMIC_API from e.g. launch options as being
+       * more important than STEAM_COMPAT_FLAGS=runtime-sdl{2,3} */
+      g_message ("Not using %s from runtime because %s is already set to \"%s\"",
+                 soname, dynamic_var, existing_value);
+      return TRUE;
+    }
+
+  if (lib_temp_dirs == NULL)
+    return glnx_throw (error,
+                       "Cannot set up dynamic %s without per-architecture directories",
+                       soname);
+
+  for (abi_index = 0; abi_index < PV_N_SUPPORTED_ARCHITECTURES; abi_index++)
+    {
+      const char *multiarch_tuple = pv_multiarch_tuples[abi_index];
+      g_autofree gchar *dest = NULL;
+      g_autofree gchar *from_runtime = NULL;
+      g_autofree gchar *override = NULL;
+      const char *target;
+
+      /* We assume a Debian multiarch layout here: in practice this
+       * is true for all Steam Runtime branches. */
+      from_runtime = g_build_filename (prefix, "lib", multiarch_tuple, soname, NULL);
+      override = g_build_filename (overrides, "lib", multiarch_tuple, soname, NULL);
+
+      if (g_file_test (override, G_FILE_TEST_EXISTS))
+        {
+          /* This is quite unexpected - we hope that none of the
+           * graphics drivers and Vulkan layers will have pulled in
+           * something as big as SDL, because if they do, that really
+           * undermines what we're trying to achieve. But if something
+           * in the graphics stack does depend on SDL, we really have
+           * no choice but to use that version. */
+          g_message ("Using %s %s from graphics stack provider instead of runtime",
+                     multiarch_tuple, soname);
+          target = override;
+        }
+      else if (g_file_test (from_runtime, G_FILE_TEST_EXISTS))
+        {
+          target = from_runtime;
+        }
+      else
+        {
+          g_info ("%s doesn't exist in container", from_runtime);
+          continue;
+        }
+
+      dest = g_build_filename (lib_temp_dirs->abi_paths[abi_index],
+                               soname, NULL);
+      g_info ("Creating symlink \"%s\" -> \"%s\" in container",
+              dest, target);
+
+      if (symlink (target, dest) != 0)
+        return glnx_throw_errno_prefix (error,
+                                        "While creating symlink \"%s\"",
+                                        dest);
+
+      have_any = TRUE;
+    }
+
+  if (have_any)
+    {
+      g_autofree gchar *value = NULL;
+
+      value = g_build_filename (lib_temp_dirs->libdl_token_path, soname, NULL);
+      g_info ("Setting %s=\"%s\" to use runtime's SDL", dynamic_var, soname);
+      flatpak_bwrap_set_env (wrapped_command, dynamic_var, value, TRUE);
+    }
+  else
+    {
+      return glnx_throw (error,
+                         "Unable to set %s: %s wasn't available for any architecture",
+                         dynamic_var, soname);
+    }
+
+  return TRUE;
+}
+
+void
+pv_adverb_set_up_dynamic_sdls (FlatpakBwrap *wrapped_command,
+                               PvPerArchDirs *lib_temp_dirs,
+                               const char *prefix,
+                               const char *overrides,
+                               SrtSteamCompatFlags compat_flags)
+{
+  static const struct
+    {
+      const char *dynamic_var;
+      const char *soname;
+      SrtSteamCompatFlags if_flag;
+    }
+  sdls[] =
+    {
+        {
+            "SDL_DYNAMIC_API",
+            "libSDL2-2.0.so.0",
+            SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2
+        },
+        {
+            "SDL3_DYNAMIC_API",
+            "libSDL3.so.0",
+            SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3
+        },
+    };
+  size_t i;
+
+  for (i = 0; i < G_N_ELEMENTS (sdls); i++)
+    {
+      if (compat_flags & sdls[i].if_flag)
+        {
+          g_autoptr(GError) local_error = NULL;
+
+          if (!pv_adverb_set_up_dynamic_sdl (wrapped_command,
+                                             lib_temp_dirs,
+                                             prefix,
+                                             overrides,
+                                             sdls[i].dynamic_var,
+                                             sdls[i].soname,
+                                             &local_error))
+            g_warning ("%s", local_error->message);
+        }
+    }
+}
diff --git a/pressure-vessel/adverb-sdl.h b/pressure-vessel/adverb-sdl.h
new file mode 100644
index 0000000000000000000000000000000000000000..a4fe9d4caa835b91c9d9e8dc0cd4aac93ff71df7
--- /dev/null
+++ b/pressure-vessel/adverb-sdl.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2024 Collabora Ltd.
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "libglnx.h"
+
+#include <glib.h>
+
+#include "steam-runtime-tools/steam-internal.h"
+
+#include "flatpak-bwrap-private.h"
+#include "per-arch-dirs.h"
+
+gboolean pv_adverb_set_up_dynamic_sdl (FlatpakBwrap *wrapped_command,
+                                       PvPerArchDirs *lib_temp_dirs,
+                                       const char *prefix,
+                                       const char *overrides,
+                                       const char *dynamic_var,
+                                       const char *soname,
+                                       GError **error);
+
+void pv_adverb_set_up_dynamic_sdls (FlatpakBwrap *wrapped_command,
+                                    PvPerArchDirs *lib_temp_dirs,
+                                    const char *prefix,
+                                    const char *overrides,
+                                    SrtSteamCompatFlags compat_flags);
diff --git a/pressure-vessel/adverb.c b/pressure-vessel/adverb.c
index d5a33d9341441d6d4cb05528464b23f30460b84a..bd9499d0c741cc6721e04d94e85f685164a4952b 100644
--- a/pressure-vessel/adverb.c
+++ b/pressure-vessel/adverb.c
@@ -46,6 +46,7 @@
 #include "libglnx.h"
 
 #include "adverb-preload.h"
+#include "adverb-sdl.h"
 #include "flatpak-utils-base-private.h"
 #include "per-arch-dirs.h"
 #include "supported-architectures.h"
@@ -849,6 +850,7 @@ main (int argc,
   glnx_autofd int original_stderr = -1;
   g_autoptr(FlatpakBwrap) wrapped_command = NULL;
   g_autoptr(PvPerArchDirs) lib_temp_dirs = NULL;
+  SrtSteamCompatFlags compat_flags;
 
   setlocale (LC_ALL, "");
 
@@ -1086,6 +1088,10 @@ main (int argc,
       g_clear_error (error);
     }
 
+  compat_flags = _srt_steam_get_compat_flags (_srt_const_strv (envp));
+  pv_adverb_set_up_dynamic_sdls (wrapped_command, lib_temp_dirs,
+                                 "/usr", opt_overrides, compat_flags);
+
   if (opt_overrides != NULL
       && !pv_adverb_set_up_overrides (wrapped_command,
                                       lib_temp_dirs,
diff --git a/pressure-vessel/meson.build b/pressure-vessel/meson.build
index 1394f188d2b38459df29ca3071808666878db631..245e313c7941edfda395a04a39c0e40fdb6dc808 100644
--- a/pressure-vessel/meson.build
+++ b/pressure-vessel/meson.build
@@ -108,6 +108,8 @@ pressure_vessel_adverb_lib = static_library(
   sources : [
     'adverb-preload.c',
     'adverb-preload.h',
+    'adverb-sdl.c',
+    'adverb-sdl.h',
     'per-arch-dirs.c',
     'per-arch-dirs.h',
     'wrap-interactive.c',
diff --git a/steam-runtime-tools/steam-internal.h b/steam-runtime-tools/steam-internal.h
index c0a5f9f4957f644f0db98a72910c3c97021e03cc..9cdf16149d4001ec065faa9d05e41ef13a9c1373 100644
--- a/steam-runtime-tools/steam-internal.h
+++ b/steam-runtime-tools/steam-internal.h
@@ -78,6 +78,8 @@ typedef enum
   SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD = (1 << 0),
   SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD_FIRST = (1 << 1),
   SRT_STEAM_COMPAT_FLAGS_SYSTEM_TRACING = (1 << 2),
+  SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2 = (1 << 3),
+  SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3 = (1 << 4),
   SRT_STEAM_COMPAT_FLAGS_NONE = 0
 } SrtSteamCompatFlags;
 
diff --git a/steam-runtime-tools/steam.c b/steam-runtime-tools/steam.c
index 86e0997469e462fd14205bff3f9643546a8debc6..ff9bd7242922124dadb844d3679410c84db266ba 100644
--- a/steam-runtime-tools/steam.c
+++ b/steam-runtime-tools/steam.c
@@ -751,21 +751,37 @@ _srt_steam_check (const char * const *envp,
 SrtSteamCompatFlags
 _srt_steam_get_compat_flags (const char * const *envp)
 {
+  static const struct
+    {
+      const char *name;
+      SrtSteamCompatFlags value;
+      gboolean def;
+    }
+  bool_vars[] =
+    {
+        { "STEAM_COMPAT_TRACING", SRT_STEAM_COMPAT_FLAGS_SYSTEM_TRACING, FALSE },
+        { "STEAM_COMPAT_RUNTIME_SDL2", SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2, FALSE },
+        { "STEAM_COMPAT_RUNTIME_SDL3", SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3, FALSE },
+    };
   SrtSteamCompatFlags ret = SRT_STEAM_COMPAT_FLAGS_NONE;
   const char *value;
-  gboolean tracing = FALSE;
+  size_t i;
 
-  _srt_environ_get_boolean (envp, "STEAM_COMPAT_TRACING", &tracing, NULL);
+  for (i = 0; i < G_N_ELEMENTS (bool_vars); i++)
+    {
+      gboolean bool_value = bool_vars[i].def;
 
-  if (tracing)
-    ret |= SRT_STEAM_COMPAT_FLAGS_SYSTEM_TRACING;
+      _srt_environ_get_boolean (envp, bool_vars[i].name, &bool_value, NULL);
+
+      if (bool_value)
+        ret |= bool_vars[i].value;
+    }
 
   value = _srt_environ_getenv (envp, "STEAM_COMPAT_FLAGS");
 
   if (value != NULL)
     {
       g_auto(GStrv) tokens = NULL;
-      size_t i;
 
       tokens = g_strsplit (value, ",", 0);
 
@@ -773,6 +789,14 @@ _srt_steam_get_compat_flags (const char * const *envp)
         {
           switch (tokens[i][0])
             {
+              case 'r':
+                if (g_str_equal (tokens[i], "runtime-sdl2"))
+                  ret |= SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2;
+                else if (g_str_equal (tokens[i], "runtime-sdl3"))
+                  ret |= SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3;
+
+                break;
+
               case 's':
                 if (g_str_equal (tokens[i], "search-cwd"))
                   ret |= SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD;
diff --git a/tests/pressure-vessel/adverb-sdl.c b/tests/pressure-vessel/adverb-sdl.c
new file mode 100644
index 0000000000000000000000000000000000000000..d6fde6ef8382bbc0591199b871ee950757854901
--- /dev/null
+++ b/tests/pressure-vessel/adverb-sdl.c
@@ -0,0 +1,451 @@
+/*
+ * Copyright © 2023-2024 Collabora Ltd.
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <glib.h>
+
+#include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/utils-internal.h"
+#include "libglnx.h"
+
+#include "adverb-sdl.h"
+#include "tests/test-utils.h"
+
+#define SDL_DYNAMIC_API "SDL_DYNAMIC_API"
+#define SDL2_SONAME "libSDL2-2.0.so.0"
+#define SDL3_DYNAMIC_API "SDL3_DYNAMIC_API"
+#define SDL3_SONAME "libSDL3.so.0"
+
+typedef struct
+{
+  TestsOpenFdSet old_fds;
+  FlatpakBwrap *bwrap;
+  PvPerArchDirs *lib_temp_dirs;
+  GError *lib_temp_dirs_error;
+  GLnxTmpDir mock_prefix;
+  GLnxTmpDir mock_overrides;
+} Fixture;
+
+typedef struct
+{
+  int unused;
+} Config;
+
+static void
+setup (Fixture *f,
+       gconstpointer context)
+{
+  G_GNUC_UNUSED const Config *config = context;
+  g_autoptr(GError) local_error = NULL;
+  gsize i;
+
+  f->old_fds = tests_check_fd_leaks_enter ();
+  f->bwrap = flatpak_bwrap_new (flatpak_bwrap_empty_env);
+  f->lib_temp_dirs = pv_per_arch_dirs_new (&f->lib_temp_dirs_error);
+
+  if (f->lib_temp_dirs != NULL)
+    {
+      g_test_message ("Cross-platform module prefix: %s",
+                      f->lib_temp_dirs->libdl_token_path);
+
+      for (i = 0; i < PV_N_SUPPORTED_ARCHITECTURES; i++)
+        g_test_message ("Concrete path for %s architecture: %s",
+                        pv_multiarch_tuples[i],
+                        f->lib_temp_dirs->abi_paths[i]);
+    }
+
+  glnx_mkdtemp ("usr-XXXXXX", 0700, &f->mock_prefix, &local_error);
+  g_assert_no_error (local_error);
+
+  glnx_mkdtemp ("overrides-XXXXXX", 0700, &f->mock_overrides, &local_error);
+  g_assert_no_error (local_error);
+}
+
+static void
+teardown (Fixture *f,
+          gconstpointer context)
+{
+  G_GNUC_UNUSED const Config *config = context;
+
+  g_clear_pointer (&f->lib_temp_dirs, pv_per_arch_dirs_free);
+  g_clear_error (&f->lib_temp_dirs_error);
+  g_clear_pointer (&f->bwrap, flatpak_bwrap_free);
+  glnx_tmpdir_cleanup (&f->mock_prefix);
+  glnx_tmpdir_cleanup (&f->mock_overrides);
+  tests_check_fd_leaks_leave (f->old_fds);
+}
+
+static void touch (gchar **, const char *, ...) G_GNUC_NULL_TERMINATED;
+static void
+touch (gchar **path_out,
+       const char *path,
+       ...)
+{
+  g_autoptr(GError) local_error = NULL;
+  g_autofree gchar *joined = NULL;
+  g_autofree gchar *parent = NULL;
+  va_list ap;
+
+  va_start (ap, path);
+  joined = g_build_filename_valist (path, &ap);
+  va_end (ap);
+  parent = g_path_get_dirname (joined);
+
+  glnx_shutil_mkdir_p_at (AT_FDCWD, parent, 0755, NULL, &local_error);
+  g_assert_no_error (local_error);
+  glnx_file_replace_contents_at (AT_FDCWD, joined, (const guint8 *) "", 0,
+                                 (GLNX_FILE_REPLACE_NODATASYNC
+                                  | GLNX_FILE_REPLACE_INCREASING_MTIME),
+                                 NULL, &local_error);
+  g_assert_no_error (local_error);
+
+  if (path_out != NULL)
+    *path_out = g_steal_pointer (&joined);
+}
+
+static void
+assert_symlink (const char *arch_dir,
+                const char *soname,
+                const char *expect_target,
+                GIOErrorEnum expect_error)
+{
+  g_autoptr(GError) local_error = NULL;
+  g_autofree gchar *joined = NULL;
+  g_autofree gchar *target = NULL;
+
+  joined = g_build_filename (arch_dir, soname, NULL);
+  target = glnx_readlinkat_malloc (AT_FDCWD, joined, NULL, &local_error);
+
+  if (target != NULL)
+    g_test_message ("%s -> %s", joined, target);
+  else
+    g_test_message ("%s doesn't exist", joined);
+
+  if (expect_target != NULL)
+    {
+      g_assert_no_error (local_error);
+      g_assert_cmpstr (target, ==, expect_target);
+    }
+  else
+    {
+      g_assert_error (local_error, G_IO_ERROR, expect_error);
+      g_assert_null (target);
+    }
+}
+
+static void
+assert_env (gchar **envp,
+            const char *var,
+            const char *expected_dir,
+            const char *expected_file)
+{
+  g_autofree gchar *expected = NULL;
+  const char *actual;
+
+  if (expected_dir != NULL)
+    expected = g_build_filename (expected_dir, expected_file, NULL);
+
+  actual = g_environ_getenv (envp, var);
+
+  if (actual != NULL)
+    g_test_message ("%s=%s", var, actual);
+  else
+    g_test_message ("$%s is unset", var);
+
+  g_assert_cmpstr (actual, ==, expected);
+}
+
+static void
+test_basic (Fixture *f,
+            gconstpointer context)
+{
+  g_autofree gchar *sdl2_target = NULL;
+  g_autofree gchar *sdl3_target = NULL;
+
+  if (f->lib_temp_dirs == NULL)
+    {
+      g_test_skip (f->lib_temp_dirs_error->message);
+      return;
+    }
+
+  g_test_summary ("Basic setup of " SDL_DYNAMIC_API);
+
+  touch (&sdl2_target,
+         f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE],
+         SDL2_SONAME, NULL);
+  touch (&sdl3_target,
+         f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE],
+         SDL3_SONAME, NULL);
+
+  g_test_message ("With no flags, not setup is done...");
+  pv_adverb_set_up_dynamic_sdls (f->bwrap,
+                                 f->lib_temp_dirs,
+                                 f->mock_prefix.path,
+                                 f->mock_overrides.path,
+                                 SRT_STEAM_COMPAT_FLAGS_NONE);
+  assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE],
+                  SDL2_SONAME, NULL, G_IO_ERROR_NOT_FOUND);
+  assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE],
+                  SDL3_SONAME, NULL, G_IO_ERROR_NOT_FOUND);
+  assert_env (f->bwrap->envp, SDL_DYNAMIC_API, NULL, NULL);
+  assert_env (f->bwrap->envp, SDL3_DYNAMIC_API, NULL, NULL);
+
+  g_test_message ("SDL2 flag sets up SDL2...");
+  pv_adverb_set_up_dynamic_sdls (f->bwrap,
+                                 f->lib_temp_dirs,
+                                 f->mock_prefix.path,
+                                 f->mock_overrides.path,
+                                 SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2);
+  assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE],
+                  SDL2_SONAME, sdl2_target, 0);
+  assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE],
+                  SDL3_SONAME, NULL, G_IO_ERROR_NOT_FOUND);
+  assert_env (f->bwrap->envp, SDL_DYNAMIC_API,
+              f->lib_temp_dirs->libdl_token_path, SDL2_SONAME);
+  assert_env (f->bwrap->envp, SDL3_DYNAMIC_API, NULL, NULL);
+
+  g_test_message ("SDL3 flag additionally sets up SDL3...");
+  pv_adverb_set_up_dynamic_sdls (f->bwrap,
+                                 f->lib_temp_dirs,
+                                 f->mock_prefix.path,
+                                 f->mock_overrides.path,
+                                 SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3);
+  assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE],
+                  SDL2_SONAME, sdl2_target, 0);
+  assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE],
+                  SDL3_SONAME, sdl3_target, 0);
+  assert_env (f->bwrap->envp, SDL_DYNAMIC_API,
+              f->lib_temp_dirs->libdl_token_path, SDL2_SONAME);
+  assert_env (f->bwrap->envp, SDL3_DYNAMIC_API,
+              f->lib_temp_dirs->libdl_token_path, SDL3_SONAME);
+}
+
+static void
+test_cannot_symlink (Fixture *f,
+                     gconstpointer context)
+{
+  g_autoptr(GError) local_error = NULL;
+  g_autofree gchar *sdl2_target = NULL;
+  gboolean res;
+
+  if (f->lib_temp_dirs == NULL)
+    {
+      g_test_skip (f->lib_temp_dirs_error->message);
+      return;
+    }
+
+  touch (&sdl2_target,
+         f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE],
+         SDL2_SONAME, NULL);
+  touch (NULL,
+         f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE],
+         SDL2_SONAME, NULL);
+
+  g_test_summary ("If we can't create the symlink, setup fails");
+  res = pv_adverb_set_up_dynamic_sdl (f->bwrap,
+                                      f->lib_temp_dirs,
+                                      f->mock_prefix.path,
+                                      f->mock_overrides.path,
+                                      SDL_DYNAMIC_API,
+                                      SDL2_SONAME,
+                                      &local_error);
+  g_assert_nonnull (local_error);
+  /* pv-adverb would log the GError as a warning */
+  g_test_message ("%s", local_error->message);
+  g_assert_false (res);
+  assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE],
+                  SDL2_SONAME, NULL, G_IO_ERROR_INVALID_ARGUMENT);
+  assert_env (f->bwrap->envp, SDL_DYNAMIC_API, NULL, NULL);
+}
+
+static void
+test_impossible (Fixture *f,
+                 gconstpointer context)
+{
+  g_autoptr(GError) local_error = NULL;
+  g_autofree gchar *sdl2_target = NULL;
+  gboolean res;
+
+  touch (&sdl2_target,
+         f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE],
+         SDL2_SONAME, NULL);
+
+  g_test_summary ("If we don't know the $LIB or $PLATFORM, nothing happens, "
+                  "with a warning");
+  res = pv_adverb_set_up_dynamic_sdl (f->bwrap,
+                                      NULL,
+                                      f->mock_prefix.path,
+                                      f->mock_overrides.path,
+                                      SDL_DYNAMIC_API,
+                                      SDL2_SONAME,
+                                      &local_error);
+  g_assert_nonnull (local_error);
+  /* pv-adverb would log the GError as a warning */
+  g_test_message ("%s", local_error->message);
+  g_assert_false (res);
+  assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE],
+                  SDL2_SONAME, NULL, G_IO_ERROR_NOT_FOUND);
+  assert_env (f->bwrap->envp, SDL_DYNAMIC_API, NULL, NULL);
+}
+
+static void
+test_in_gfx_stack (Fixture *f,
+                   gconstpointer context)
+{
+  g_autoptr(GError) local_error = NULL;
+  g_autofree gchar *sdl2_target = NULL;
+  g_autofree gchar *gfx_sdl2_target = NULL;
+  gboolean res;
+
+  if (f->lib_temp_dirs == NULL)
+    {
+      g_test_skip (f->lib_temp_dirs_error->message);
+      return;
+    }
+
+  touch (&sdl2_target,
+         f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE],
+         SDL2_SONAME, NULL);
+  touch (&gfx_sdl2_target,
+         f->mock_overrides.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE],
+         SDL2_SONAME, NULL);
+
+  g_test_summary ("We prefer SDL from the graphics provider if present");
+  res = pv_adverb_set_up_dynamic_sdl (f->bwrap,
+                                      f->lib_temp_dirs,
+                                      f->mock_prefix.path,
+                                      f->mock_overrides.path,
+                                      SDL_DYNAMIC_API,
+                                      SDL2_SONAME,
+                                      &local_error);
+  g_assert_no_error (local_error);
+  g_assert_true (res);
+  assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE],
+                  SDL2_SONAME, gfx_sdl2_target, 0);
+  assert_env (f->bwrap->envp, SDL_DYNAMIC_API,
+              f->lib_temp_dirs->libdl_token_path, SDL2_SONAME);
+}
+
+static void
+test_in_gfx_stack_only (Fixture *f,
+                        gconstpointer context)
+{
+  g_autoptr(GError) local_error = NULL;
+  g_autofree gchar *gfx_sdl2_target = NULL;
+  gboolean res;
+
+  if (f->lib_temp_dirs == NULL)
+    {
+      g_test_skip (f->lib_temp_dirs_error->message);
+      return;
+    }
+
+  touch (&gfx_sdl2_target,
+         f->mock_overrides.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE],
+         SDL2_SONAME, NULL);
+
+  g_test_summary ("We use SDL from the graphics provider if necessary");
+  res = pv_adverb_set_up_dynamic_sdl (f->bwrap,
+                                      f->lib_temp_dirs,
+                                      f->mock_prefix.path,
+                                      f->mock_overrides.path,
+                                      SDL_DYNAMIC_API,
+                                      SDL2_SONAME,
+                                      &local_error);
+  g_assert_no_error (local_error);
+  g_assert_true (res);
+  assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE],
+                  SDL2_SONAME, gfx_sdl2_target, 0);
+  assert_env (f->bwrap->envp, SDL_DYNAMIC_API,
+              f->lib_temp_dirs->libdl_token_path, SDL2_SONAME);
+}
+
+static void
+test_missing (Fixture *f,
+              gconstpointer context)
+{
+  g_autoptr(GError) local_error = NULL;
+  gboolean res;
+
+  if (f->lib_temp_dirs == NULL)
+    {
+      g_test_skip (f->lib_temp_dirs_error->message);
+      return;
+    }
+
+  g_test_summary ("If SDL is missing, nothing happens, with a warning");
+  res = pv_adverb_set_up_dynamic_sdl (f->bwrap,
+                                      f->lib_temp_dirs,
+                                      f->mock_prefix.path,
+                                      f->mock_overrides.path,
+                                      SDL_DYNAMIC_API,
+                                      SDL2_SONAME,
+                                      &local_error);
+  g_assert_nonnull (local_error);
+  /* pv-adverb would log the GError as a warning */
+  g_test_message ("%s", local_error->message);
+  g_assert_false (res);
+  assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE],
+                  SDL2_SONAME, NULL, G_IO_ERROR_NOT_FOUND);
+  assert_env (f->bwrap->envp, SDL_DYNAMIC_API, NULL, NULL);
+}
+
+static void
+test_overridden (Fixture *f,
+                 gconstpointer context)
+{
+  g_autoptr(GError) local_error = NULL;
+  gboolean res;
+
+  if (f->lib_temp_dirs == NULL)
+    {
+      g_test_skip (f->lib_temp_dirs_error->message);
+      return;
+    }
+
+  g_test_summary ("Setting " SDL_DYNAMIC_API " takes precedence");
+  flatpak_bwrap_set_env (f->bwrap, SDL_DYNAMIC_API, "/whatever", TRUE);
+  res = pv_adverb_set_up_dynamic_sdl (f->bwrap,
+                                      f->lib_temp_dirs,
+                                      f->mock_prefix.path,
+                                      f->mock_overrides.path,
+                                      SDL_DYNAMIC_API,
+                                      SDL2_SONAME,
+                                      &local_error);
+  g_assert_no_error (local_error);
+  g_assert_true (res);
+  assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE],
+                  SDL2_SONAME, NULL, G_IO_ERROR_NOT_FOUND);
+  assert_env (f->bwrap->envp, SDL_DYNAMIC_API, "/whatever", NULL);
+}
+
+int
+main (int argc,
+      char **argv)
+{
+  _srt_setenv_disable_gio_modules ();
+
+  /* In unit tests it isn't always straightforward to find the real
+   * ${PLATFORM}, so use a predictable mock implementation that always
+   * uses PvMultiarchDetails.platforms[0] */
+  g_setenv ("PRESSURE_VESSEL_TEST_STANDARDIZE_PLATFORM", "1", TRUE);
+
+  _srt_tests_init (&argc, &argv, NULL);
+  g_test_add ("/pv-adverb/sdl/basic", Fixture, NULL,
+              setup, test_basic, teardown);
+  g_test_add ("/pv-adverb/sdl/cannot-symlink", Fixture, NULL,
+              setup, test_cannot_symlink, teardown);
+  g_test_add ("/pv-adverb/sdl/impossible", Fixture, NULL,
+              setup, test_impossible, teardown);
+  g_test_add ("/pv-adverb/sdl/in_gfx_stack", Fixture, NULL,
+              setup, test_in_gfx_stack, teardown);
+  g_test_add ("/pv-adverb/sdl/in_gfx_stack_only", Fixture, NULL,
+              setup, test_in_gfx_stack_only, teardown);
+  g_test_add ("/pv-adverb/sdl/missing", Fixture, NULL,
+              setup, test_missing, teardown);
+  g_test_add ("/pv-adverb/sdl/overridden", Fixture, NULL,
+              setup, test_overridden, teardown);
+
+  return g_test_run ();
+}
diff --git a/tests/pressure-vessel/meson.build b/tests/pressure-vessel/meson.build
index c36e79474516fb78647a7b46bc9c3003a269010a..c7cd13675e7ac4f7d78b88c9a6b6e706b6bc2eca 100644
--- a/tests/pressure-vessel/meson.build
+++ b/tests/pressure-vessel/meson.build
@@ -68,6 +68,7 @@ tests = [
 
 compiled_tests = [
   {'name': 'adverb-preload', 'adverb': true},
+  {'name': 'adverb-sdl', 'adverb': true},
   {'name': 'bwrap', 'wrap': true},
   {'name': 'graphics-provider', 'wrap': true},
   {'name': 'wrap-setup', 'wrap': true},
diff --git a/tests/utils.c b/tests/utils.c
index c0128cd8cdb471d036a647dee79520144a6cae9d..a70beed3320615a71701e62b465e9d77c01ed5f4 100644
--- a/tests/utils.c
+++ b/tests/utils.c
@@ -129,18 +129,53 @@ test_compat_flags (Fixture *f,
 {
   static const struct
     {
-      const char *env;
+      /* Length is arbitrary, expand as needed but leave room for NULL
+       * termination */
+      const char * const envp[3];
       SrtSteamCompatFlags expected;
     }
   tests[] =
     {
-        { "search-cwd,search-cwd-first,reticulate-splines,fixme",
-          (SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD
-           | SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD_FIRST) },
-        { "reticulate-splines,search-cwd", SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD },
-        { ",,,,search-cwd-first,,,,", SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD_FIRST },
-        { "", SRT_STEAM_COMPAT_FLAGS_NONE },
-        { NULL, SRT_STEAM_COMPAT_FLAGS_NONE }
+        {
+            {
+                "STEAM_COMPAT_FLAGS=search-cwd,search-cwd-first,reticulate-splines,fixme",
+                NULL
+            },
+            (SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD
+             | SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD_FIRST),
+        },
+        {
+            { "STEAM_COMPAT_FLAGS=reticulate-splines,search-cwd", NULL },
+            SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD,
+        },
+        {
+            { "STEAM_COMPAT_FLAGS=,,,,search-cwd-first,,,,", NULL },
+            SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD_FIRST,
+        },
+        {
+            { "STEAM_COMPAT_FLAGS=runtime-sdl2", NULL },
+            SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2,
+        },
+        {
+            { "STEAM_COMPAT_FLAGS=runtime-sdl3", NULL },
+            SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3,
+        },
+        {
+            {
+                "STEAM_COMPAT_TRACING=1",
+                "STEAM_COMPAT_FLAGS=search-cwd",
+                NULL
+            },
+            (SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD
+             | SRT_STEAM_COMPAT_FLAGS_SYSTEM_TRACING),
+        },
+        { { "STEAM_COMPAT_FLAGS=", NULL }, SRT_STEAM_COMPAT_FLAGS_NONE },
+        { { "STEAM_COMPAT_TRACING=1", NULL }, SRT_STEAM_COMPAT_FLAGS_SYSTEM_TRACING },
+        { { "STEAM_COMPAT_TRACING=", NULL }, SRT_STEAM_COMPAT_FLAGS_NONE },
+        { { "STEAM_COMPAT_TRACING=0", NULL }, SRT_STEAM_COMPAT_FLAGS_NONE },
+        { { "STEAM_COMPAT_RUNTIME_SDL2=1", NULL }, SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2 },
+        { { "STEAM_COMPAT_RUNTIME_SDL3=1", NULL }, SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3 },
+        { { NULL }, SRT_STEAM_COMPAT_FLAGS_NONE }
     };
   size_t i;
 
@@ -149,14 +184,10 @@ test_compat_flags (Fixture *f,
 
   for (i = 0; i < G_N_ELEMENTS (tests); i++)
     {
-      gchar *envp[2] = { NULL, NULL };
-
-      if (tests[i].env != NULL)
-        envp[0] = g_strdup_printf ("STEAM_COMPAT_FLAGS=%s", tests[i].env);
-
-      g_assert_cmphex (_srt_steam_get_compat_flags (_srt_const_strv (envp)),
+      g_assert_cmpuint (g_strv_length ((gchar **) tests[i].envp),
+                        <, G_N_ELEMENTS (tests[i].envp));
+      g_assert_cmphex (_srt_steam_get_compat_flags (tests[i].envp),
                        ==, tests[i].expected);
-      g_free (envp[0]);
     }
 }