Emscripten support for SDL_WGPU.

Congratulations; you can now use WebGPU on the Web.

I'll make an example eventually, but it's midnight in Sweden and I gotta
go to bed.
This commit is contained in:
The Stickmahn 2026-05-31 23:35:35 +02:00
parent ea6a51fed7
commit 355ff8cf8d
6 changed files with 130 additions and 42 deletions

View file

@ -23,12 +23,12 @@ add_compile_options("$<$<CXX_COMPILER_ID:MSVC>:/utf-8>")
if(SDL3_MAINPROJECT)
get_property(is_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(is_multi_config)
# The first item in CMAKE_CONFIGURATION_TYPES is the default configuration
if(DEFINED CMAKE_CONFIGURATION_TYPES AND "RelWithDebInfo" IN_LIST CMAKE_CONFIGURATION_TYPES)
list(REMOVE_ITEM CMAKE_CONFIGURATION_TYPES "RelWithDebInfo")
list(INSERT CMAKE_CONFIGURATION_TYPES 0 "RelWithDebInfo")
set(CMAKE_CONFIGURATION_TYPES "${CMAKE_CONFIGURATION_TYPES}" CACHE STRING "CMake configuration types" FORCE)
endif()
# The first item in CMAKE_CONFIGURATION_TYPES is the default configuration
if(DEFINED CMAKE_CONFIGURATION_TYPES AND "RelWithDebInfo" IN_LIST CMAKE_CONFIGURATION_TYPES)
list(REMOVE_ITEM CMAKE_CONFIGURATION_TYPES "RelWithDebInfo")
list(INSERT CMAKE_CONFIGURATION_TYPES 0 "RelWithDebInfo")
set(CMAKE_CONFIGURATION_TYPES "${CMAKE_CONFIGURATION_TYPES}" CACHE STRING "CMake configuration types" FORCE)
endif()
else()
if(cmake_build_type_undefined)
set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "CMake build type" FORCE)
@ -411,13 +411,13 @@ endif()
# TODO: I'm firing blind here, I have no idea what the SDL convensions on static linking or finding libraries is.
if(APPLE OR LINUX OR FREEBSD OR OPENBSD)
# unix-style, default search path should be /usr/lib/
# unix-style, default search path should be /usr/lib/
set(DEFAULT_WGPU_LIB_SEARCH_PATH "/usr/lib/")
elseif(WINDOWS)
# Windows has no real analogue to Unix's /usr/lib, so we're just gonna search the source dir.
# Windows has no real analogue to Unix's /usr/lib, so we're just gonna search the source dir.
set(DEFAULT_WGPU_LIB_SEARCH_PATH ${CMAKE_SOURCE_DIR})
else()
# Same as Windows, we're searching the source path.
# Same as Windows, we're searching the source path.
set(DEFAULT_WGPU_LIB_SEARCH_PATH ${CMAKE_SOURCE_DIR})
endif()
@ -580,10 +580,10 @@ check_linker_supports_version_file(HAVE_WL_VERSION_SCRIPT)
if(HAVE_WL_VERSION_SCRIPT)
sdl_shared_link_options("-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/src/dynapi/SDL_dynapi.sym")
else()
# When building with tcc on Linux+glibc or Android, avoid emitting an error
# for lack of support of the version-script linker flag: the option will be
# silently ignored by the compiler and the build will still succeed.
if(((LINUX AND LIBC_IS_GLIBC) OR ANDROID) AND (NOT USE_TCC))
# When building with tcc on Linux+glibc or Android, avoid emitting an error
# for lack of support of the version-script linker flag: the option will be
# silently ignored by the compiler and the build will still succeed.
if(((LINUX AND LIBC_IS_GLIBC) OR ANDROID) AND (NOT USE_TCC))
message(FATAL_ERROR "Linker does not support '-Wl,--version-script=xxx.sym'. This is required on the current host platform (${SDL_CMAKE_PLATFORM}).")
endif()
endif()
@ -946,8 +946,8 @@ if(SDL_ASSEMBLY)
if(SDL_ARMSVE2)
cmake_push_check_state()
string(APPEND CMAKE_REQUIRED_FLAGS " -march=armv8-a+sve2")
check_arm_source_compiles([==[
string(APPEND CMAKE_REQUIRED_FLAGS " -march=armv8-a+sve2")
check_arm_source_compiles([==[
#include <arm_sve.h>
svuint32_t sve2_test(svuint32_t a, svuint32_t b) {
return svadd_u32_x(svptrue_b32(), a, b);
@ -956,13 +956,13 @@ if(SDL_ASSEMBLY)
sve2_test(svdup_u32(0), svdup_u32(0));
return 0;
}]==] COMPILER_SUPPORTS_ARMSVE2)
if(COMPILER_SUPPORTS_ARMSVE2)
# IMPORTANT: As not all AArch64 processors support SVE2, we only
# attach the following compilation option to SVE
# dedicated source files.
set(SVE2_MARCH_FLAG "-march=armv8-a+sve2")
set(HAVE_ARMSVE2 TRUE)
endif()
if(COMPILER_SUPPORTS_ARMSVE2)
# IMPORTANT: As not all AArch64 processors support SVE2, we only
# attach the following compilation option to SVE
# dedicated source files.
set(SVE2_MARCH_FLAG "-march=armv8-a+sve2")
set(HAVE_ARMSVE2 TRUE)
endif()
cmake_pop_check_state()
if(HAVE_ARMSVE2)
@ -1736,6 +1736,7 @@ elseif(EMSCRIPTEN)
# project. Uncomment at will for verbose cross-compiling -I/../ path info.
sdl_compile_options(PRIVATE "-Wno-warn-absolute-paths")
CheckWGPU()
if(NOT SDL_EMSCRIPTEN_PERSISTENT_PATH STREQUAL "")
set(SDL_EMSCRIPTEN_PERSISTENT_PATH_STRING "${SDL_EMSCRIPTEN_PERSISTENT_PATH}")
sdl_link_dependency(idbfs LIBS idbfs.js)
@ -1876,20 +1877,20 @@ elseif(UNIX AND NOT (APPLE OR RISCOS OR HAIKU OR CYGWIN))
if(SDL_AUDIO)
if(NETBSD)
set(SDL_AUDIO_DRIVER_NETBSD 1)
sdl_glob_sources(
set(SDL_AUDIO_DRIVER_NETBSD 1)
sdl_glob_sources(
"${SDL3_SOURCE_DIR}/src/audio/netbsd/*.c"
"${SDL3_SOURCE_DIR}/src/audio/netbsd/*.h"
)
set(HAVE_SDL_AUDIO TRUE)
set(HAVE_SDL_AUDIO TRUE)
elseif(QNX AND (CMAKE_SYSTEM_VERSION VERSION_LESS "8.0.0"))
set(SDL_AUDIO_DRIVER_QNX 1)
sdl_glob_sources(
set(SDL_AUDIO_DRIVER_QNX 1)
sdl_glob_sources(
"${SDL3_SOURCE_DIR}/src/audio/qnx/*.c"
"${SDL3_SOURCE_DIR}/src/audio/qnx/*.h"
)
sdl_link_dependency(asound LIBS asound)
set(HAVE_SDL_AUDIO TRUE)
sdl_link_dependency(asound LIBS asound)
set(HAVE_SDL_AUDIO TRUE)
endif()
CheckOSS()
CheckALSA()
@ -2276,7 +2277,7 @@ elseif(WINDOWS OR CYGWIN)
if(SDL_DIRECTX)
cmake_push_check_state()
if(DEFINED MSVC_VERSION AND NOT ${MSVC_VERSION} LESS 1700)
set(USE_WINSDK_DIRECTX TRUE)
set(USE_WINSDK_DIRECTX TRUE)
endif()
if(NOT (MINGW OR CYGWIN) AND NOT USE_WINSDK_DIRECTX)
if("$ENV{DXSDK_DIR}" STREQUAL "")
@ -3542,12 +3543,27 @@ endif()
# webgpu static linking shenanigans
if (SDL_WGPU_STATIC)
if (HAVE_DAWN)
if (EMSCRIPTEN)
# You see, I pulled what's called a "big stupid"
# I assumed that Dawn and Emdawnwebgpu were the same thing, so I added dawn support to SDL_WGPU.
# Turns out, no it isn't. (sorta)
# It's its own library and everything.
# I'm still gonna keep the native Dawn support since I strictly adhere to the sunk cost fallacy.
find_library(
WGPU_STATIC_LIB NAMES libemdawnwebgpu_c.a emdawnwebgpu_c
HINTS ${WGPU_STATIC_LIB_DIR} ${CMAKE_SOURCE_DIR}
REQUIRED
)
message("-- Using Emdawnwebgpu as WebGPU implementation")
else()
find_library(
WGPU_STATIC_LIB NAMES libwebgpu_dawn webgpu_dawn
HINTS ${WGPU_STATIC_LIB_DIR} ${CMAKE_SOURCE_DIR}
REQUIRED
)
message("-- Using Dawn as WebGPU implementation")
endif()
elseif(HAVE_WGPU_NATIVE)
find_library(
WGPU_STATIC_LIB NAMES libwgpu_native wgpu_native
@ -3563,9 +3579,9 @@ if (SDL_WGPU_STATIC)
target_link_libraries(SDL3-shared PRIVATE ${WGPU_STATIC_LIB})
endif()
elseif (HAVE_WGPU_NATIVE)
# We don't have to do anything here,
# but we'll still message that we're using wgpu-native (dyn) as our WebGPU lib.
message("-- Using wgpu-native (dynamically linked) as WebGPU implementation")
# We don't have to do anything here,
# but we'll still message that we're using wgpu-native (dyn) as our WebGPU lib.
message("-- Using wgpu-native (dynamically linked) as WebGPU implementation")
endif()
sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/SDL_dialog.c)

View file

@ -166,8 +166,8 @@ macro(CheckPipewire)
sdl_link_dependency(pipewire LIBS PkgConfig::PC_PIPEWIRE PKG_CONFIG_PREFIX PC_PIPEWIRE PKG_CONFIG_SPECS ${PipeWire_PKG_CONFIG_SPEC})
endif()
set(HAVE_SDL_AUDIO TRUE)
endif()
endif()
endif()
endmacro()
# Requires:
@ -620,24 +620,24 @@ macro(CheckLibThai)
endmacro()
macro(WaylandProtocolGen _SCANNER _CODE_MODE _XML _PROTL)
set(_WAYLAND_PROT_C_CODE "${CMAKE_CURRENT_BINARY_DIR}/wayland-generated-protocols/${_PROTL}-protocol.c")
set(_WAYLAND_PROT_H_CODE "${CMAKE_CURRENT_BINARY_DIR}/wayland-generated-protocols/${_PROTL}-client-protocol.h")
set(_WAYLAND_PROT_C_CODE "${CMAKE_CURRENT_BINARY_DIR}/wayland-generated-protocols/${_PROTL}-protocol.c")
set(_WAYLAND_PROT_H_CODE "${CMAKE_CURRENT_BINARY_DIR}/wayland-generated-protocols/${_PROTL}-client-protocol.h")
add_custom_command(
add_custom_command(
OUTPUT "${_WAYLAND_PROT_H_CODE}"
DEPENDS "${_XML}"
COMMAND "${_SCANNER}"
ARGS client-header "${_XML}" "${_WAYLAND_PROT_H_CODE}"
)
add_custom_command(
add_custom_command(
OUTPUT "${_WAYLAND_PROT_C_CODE}"
DEPENDS "${_WAYLAND_PROT_H_CODE}"
COMMAND "${_SCANNER}"
ARGS "${_CODE_MODE}" "${_XML}" "${_WAYLAND_PROT_C_CODE}"
)
sdl_sources("${_WAYLAND_PROT_C_CODE}")
sdl_sources("${_WAYLAND_PROT_C_CODE}")
endmacro()
# Requires:
@ -758,7 +758,7 @@ macro(CheckWayland)
sdl_link_dependency(libdecor INCLUDES $<TARGET_PROPERTY:PkgConfig::PC_LIBDECOR,INTERFACE_INCLUDE_DIRECTORIES>)
else()
sdl_link_dependency(libdecor LIBS PkgConfig::PC_LIBDECOR PKG_CONFIG_PREFIX PC_LIBDECOR PKG_CONFIG_SPECS ${LibDecor_PKG_CONFIG_SPEC})
endif()
endif()
endif()
endif()
@ -966,7 +966,15 @@ macro(CheckWGPU)
sdl_sources("${SDL3_SOURCE_DIR}/src/video/windows/SDL_windowswgpu_dawn.cpp")
endif()
if(EMSCRIPTEN)
sdl_sources("${SDL3_SOURCE_DIR}/src/video/emscripten/SDL_emscriptenwgpu_dawn.cpp")
endif()
elseif(SDL_WGPU_LIB STREQUAL "wgpu-native")
if(EMSCRIPTEN)
# wgpu-native doesn't support Emscripten. That's the entire reason we have Dawn.
message(FATAL_ERROR "wgpu-native doesn't support Emscripten! Use Dawn instead.")
endif()
set(WGPU_NATIVE 1)
set(HAVE_WGPU_NATIVE TRUE)
else()

View file

@ -53,8 +53,7 @@
#define WGPU_STRLEN (SIZE_MAX)
#endif
typedef enum WGPUSType
{
typedef enum WGPUSType {
WGPUSType_ShaderSourceSPIRV = 0x00000001,
WGPUSType_ShaderSourceWGSL = 0x00000002,
WGPUSType_RenderPassMaxDrawCount = 0x00000003,
@ -71,6 +70,8 @@ typedef enum WGPUSType
WGPUSType_ExternalTextureBindingEntry = 0x0000000E,
WGPUSType_CompatibilityModeLimits = 0x0000000F,
WGPUSType_TextureBindingViewDimension = 0x00000010,
WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector = 0x00040000,
WGPUSType_DawnCompilationMessageUtf16 = 0x0005003F,
WGPUSType_Force32 = 0x7FFFFFFF
} WGPUSType;
@ -111,6 +112,11 @@ typedef struct WGPUSurfaceSourceWindowsHWND {
void * hwnd;
} WGPUSurfaceSourceWindowsHWND;
typedef struct WGPUEmscriptenSurfaceSourceCanvasHTMLSelector {
WGPUChainedStruct chain;
WGPUStringView selector;
} WGPUEmscriptenSurfaceSourceCanvasHTMLSelector;
typedef WGPUSurface (*WGPUProcInstanceCreateSurface)(WGPUInstance instance, WGPUSurfaceDescriptor const *descriptor) WGPU_FUNCTION_ATTRIBUTE;
WGPU_EXTERN WGPUSurface wgpuInstanceCreateSurface(WGPUInstance instance, WGPUSurfaceDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE;

View file

@ -32,6 +32,7 @@
#include "SDL_emscriptenframebuffer.h"
#include "SDL_emscriptenevents.h"
#include "SDL_emscriptenmouse.h"
#include "SDL_emscriptenwgpu.h"
#define EMSCRIPTENVID_DRIVER_NAME "emscripten"
@ -187,6 +188,8 @@ static SDL_VideoDevice *Emscripten_CreateDevice(void)
device->GL_SwapWindow = Emscripten_GLES_SwapWindow;
device->GL_DestroyContext = Emscripten_GLES_DestroyContext;
device->WGPU_CreateSurface = Emscripten_WGPU_CreateSurface;
device->free = Emscripten_DeleteDevice;
Emscripten_ListenSystemTheme();

View file

@ -0,0 +1,23 @@
#ifndef SDL_emscriptenwgpu_h_
#define SDL_emscriptenwgpu_h_
#include <SDL3/SDL_wgpu.h>
#if defined(SDL_VIDEO_WGPU) && defined(SDL_VIDEO_DRIVER_EMSCRIPTEN)
// Dawn's a C++ lib so we'll have to do this to prevent any name mangling.
#ifdef __cplusplus
extern "C" {
#else
extern
#endif
WGPUSurface Emscripten_WGPU_CreateSurface(SDL_VideoDevice *_this, SDL_Window *window, WGPUInstance instance);
#ifdef __cplusplus
}
#endif
#endif
#endif // SDL_emscriptenwgpu_h_

View file

@ -0,0 +1,32 @@
#include "SDL_internal.h"
#if defined(SDL_VIDEO_WGPU) && defined(SDL_VIDEO_DRIVER_EMSCRIPTEN) && defined(WGPU_DAWN)
#include "../SDL_wgpu_defs.h"
#include "../SDL_sysvideo.h"
#include "SDL_emscriptenwgpu.h"
#include <SDL3/SDL_wgpu.h>
WGPUSurface
Emscripten_WGPU_CreateSurface(SDL_VideoDevice *_this, SDL_Window *window, WGPUInstance instance)
{
SDL_PropertiesID windowProperties = SDL_GetWindowProperties(window);
const char *canvas = SDL_GetStringProperty(windowProperties, SDL_PROP_WINDOW_EMSCRIPTEN_CANVAS_ID_STRING, 0);
WGPUSurfaceDescriptor desc;
WGPUEmscriptenSurfaceSourceCanvasHTMLSelector source;
source.selector.data = canvas;
source.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector;
source.chain.next = NULL;
desc.label = (WGPUStringView){ NULL, WGPU_STRLEN };
desc.nextInChain = &source.chain;
return wgpuInstanceCreateSurface(instance, &desc);
}
#endif