From 355ff8cf8d5eb88946edd962fb5e3e64d767d6e1 Mon Sep 17 00:00:00 2001 From: The Stickmahn Date: Sun, 31 May 2026 23:35:35 +0200 Subject: [PATCH] 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. --- CMakeLists.txt | 82 +++++++++++-------- cmake/sdlchecks.cmake | 22 +++-- src/video/SDL_wgpu_defs.h | 10 ++- src/video/emscripten/SDL_emscriptenvideo.c | 3 + src/video/emscripten/SDL_emscriptenwgpu.h | 23 ++++++ .../emscripten/SDL_emscriptenwgpu_dawn.cpp | 32 ++++++++ 6 files changed, 130 insertions(+), 42 deletions(-) create mode 100644 src/video/emscripten/SDL_emscriptenwgpu.h create mode 100644 src/video/emscripten/SDL_emscriptenwgpu_dawn.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 65de90bff0..94f7ddc667 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,12 +23,12 @@ add_compile_options("$<$:/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 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) diff --git a/cmake/sdlchecks.cmake b/cmake/sdlchecks.cmake index 69be8ae055..9f9e926b4d 100644 --- a/cmake/sdlchecks.cmake +++ b/cmake/sdlchecks.cmake @@ -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 $) 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() diff --git a/src/video/SDL_wgpu_defs.h b/src/video/SDL_wgpu_defs.h index 7e9b7b8fb3..67f9b7cc7a 100644 --- a/src/video/SDL_wgpu_defs.h +++ b/src/video/SDL_wgpu_defs.h @@ -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; diff --git a/src/video/emscripten/SDL_emscriptenvideo.c b/src/video/emscripten/SDL_emscriptenvideo.c index 942809c4a2..68d6f5a8f0 100644 --- a/src/video/emscripten/SDL_emscriptenvideo.c +++ b/src/video/emscripten/SDL_emscriptenvideo.c @@ -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(); diff --git a/src/video/emscripten/SDL_emscriptenwgpu.h b/src/video/emscripten/SDL_emscriptenwgpu.h new file mode 100644 index 0000000000..320b215f95 --- /dev/null +++ b/src/video/emscripten/SDL_emscriptenwgpu.h @@ -0,0 +1,23 @@ +#ifndef SDL_emscriptenwgpu_h_ +#define SDL_emscriptenwgpu_h_ + +#include + +#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_ diff --git a/src/video/emscripten/SDL_emscriptenwgpu_dawn.cpp b/src/video/emscripten/SDL_emscriptenwgpu_dawn.cpp new file mode 100644 index 0000000000..7301b62553 --- /dev/null +++ b/src/video/emscripten/SDL_emscriptenwgpu_dawn.cpp @@ -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 + +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