diff --git a/.ci/emscripten/toolchain/Toolchain-Emscripten.cmake b/.ci/emscripten/toolchain/Toolchain-Emscripten.cmake new file mode 100644 index 00000000000..752998f7f57 --- /dev/null +++ b/.ci/emscripten/toolchain/Toolchain-Emscripten.cmake @@ -0,0 +1,33 @@ +# Toolchain for compiling for Emscripten + +# Note: +# This is a bit of a hack. +# We actually want to use the Emscripten.cmake toolchain provided by Emscripten... +# However we also want to override and enable pthreads for everything + +if(NOT DEFINED ENV{EMSCRIPTEN_ROOT}) + find_path(EMSCRIPTEN_ROOT "emcc") +else() + set(EMSCRIPTEN_ROOT "$ENV{EMSCRIPTEN_ROOT}") +endif() + +if(NOT EMSCRIPTEN_ROOT) + if(NOT DEFINED ENV{EMSDK}) + message(FATAL_ERROR "The emcc compiler not found in PATH") + endif() + set(EMSCRIPTEN_ROOT "$ENV{EMSDK}/upstream/emscripten") +endif() + +if(NOT EXISTS "${EMSCRIPTEN_ROOT}/cmake/Modules/Platform/Emscripten.cmake") + message(FATAL_ERROR "Emscripten.cmake toolchain file not found") +endif() + +include("${EMSCRIPTEN_ROOT}/cmake/Modules/Platform/Emscripten.cmake") + +# Always enable PThreads +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -s USE_PTHREADS=1 -s USE_SDL=0") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -s USE_PTHREADS=1 -s USE_SDL=0") + +# Enable optimizations for release builds +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2") +set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O2") diff --git a/.ci/vcpkg/overlay-ports/sdl2/alsa-dep-fix.patch b/.ci/vcpkg/overlay-ports/sdl2/alsa-dep-fix.patch new file mode 100644 index 00000000000..5b2c77b937d --- /dev/null +++ b/.ci/vcpkg/overlay-ports/sdl2/alsa-dep-fix.patch @@ -0,0 +1,13 @@ +diff --git a/SDL2Config.cmake.in b/SDL2Config.cmake.in +index cc8bcf26d..ead829767 100644 +--- a/SDL2Config.cmake.in ++++ b/SDL2Config.cmake.in +@@ -35,7 +35,7 @@ include("${CMAKE_CURRENT_LIST_DIR}/sdlfind.cmake") + + set(SDL_ALSA @SDL_ALSA@) + set(SDL_ALSA_SHARED @SDL_ALSA_SHARED@) +-if(SDL_ALSA AND NOT SDL_ALSA_SHARED AND TARGET SDL2::SDL2-static) ++if(SDL_ALSA) + sdlFindALSA() + endif() + unset(SDL_ALSA) diff --git a/.ci/vcpkg/overlay-ports/sdl2/deps.patch b/.ci/vcpkg/overlay-ports/sdl2/deps.patch new file mode 100644 index 00000000000..a8637d8c801 --- /dev/null +++ b/.ci/vcpkg/overlay-ports/sdl2/deps.patch @@ -0,0 +1,13 @@ +diff --git a/cmake/sdlchecks.cmake b/cmake/sdlchecks.cmake +index 65a98efbe..2f99f28f1 100644 +--- a/cmake/sdlchecks.cmake ++++ b/cmake/sdlchecks.cmake +@@ -352,7 +352,7 @@ endmacro() + # - HAVE_SDL_LOADSO opt + macro(CheckLibSampleRate) + if(SDL_LIBSAMPLERATE) +- find_package(SampleRate QUIET) ++ find_package(SampleRate CONFIG REQUIRED) + if(SampleRate_FOUND AND TARGET SampleRate::samplerate) + set(HAVE_LIBSAMPLERATE TRUE) + set(HAVE_LIBSAMPLERATE_H TRUE) diff --git a/.ci/vcpkg/overlay-ports/sdl2/emscripten-webgl.patch b/.ci/vcpkg/overlay-ports/sdl2/emscripten-webgl.patch new file mode 100644 index 00000000000..a14af9f05bb --- /dev/null +++ b/.ci/vcpkg/overlay-ports/sdl2/emscripten-webgl.patch @@ -0,0 +1,320 @@ +diff --git a/src/video/emscripten/SDL_emscriptenopengles.c b/src/video/emscripten/SDL_emscriptenopengles.c +--- a/src/video/emscripten/SDL_emscriptenopengles.c ++++ b/src/video/emscripten/SDL_emscriptenopengles.c +@@ -20,81 +20,139 @@ + */ + #include "../../SDL_internal.h" + +-#if SDL_VIDEO_DRIVER_EMSCRIPTEN && SDL_VIDEO_OPENGL_EGL ++#if SDL_VIDEO_DRIVER_EMSCRIPTEN + + #include ++#include + #include + + #include "SDL_emscriptenvideo.h" + #include "SDL_emscriptenopengles.h" + #include "SDL_hints.h" + +-#define LOAD_FUNC(NAME) _this->egl_data->NAME = NAME; ++int Emscripten_GLES_LoadLibrary(_THIS, const char *path) ++{ ++ return 0; ++} ++ ++void Emscripten_GLES_UnloadLibrary(_THIS) ++{ ++} + +-/* EGL implementation of SDL OpenGL support */ ++void * Emscripten_GLES_GetProcAddress(_THIS, const char *proc) ++{ ++ return emscripten_webgl_get_proc_address(proc); ++} + +-int Emscripten_GLES_LoadLibrary(_THIS, const char *path) ++int Emscripten_GLES_SetSwapInterval(_THIS, int interval) + { +- /*we can't load EGL dynamically*/ +- _this->egl_data = (struct SDL_EGL_VideoData *) SDL_calloc(1, sizeof(SDL_EGL_VideoData)); +- if (!_this->egl_data) { +- return SDL_OutOfMemory(); ++ if (interval < 0) { ++ return SDL_SetError("Late swap tearing currently unsupported"); ++ } else if(interval == 0) { ++ emscripten_set_main_loop_timing(EM_TIMING_SETTIMEOUT, 0); ++ } else { ++ emscripten_set_main_loop_timing(EM_TIMING_RAF, interval); + } ++ return 0; ++} ++ ++int Emscripten_GLES_GetSwapInterval(_THIS) ++{ ++ int mode, value; ++ ++ emscripten_get_main_loop_timing(&mode, &value); ++ ++ if(mode == EM_TIMING_RAF) ++ return value; ++ ++ return 0; ++} ++ ++SDL_GLContext Emscripten_GLES_CreateContext(_THIS, SDL_Window * window) ++{ ++ SDL_WindowData *window_data; ++ ++ EmscriptenWebGLContextAttributes attribs; ++ EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context; + +- /* Emscripten forces you to manually cast eglGetProcAddress to the real +- function type; grep for "__eglMustCastToProperFunctionPointerType" in +- Emscripten's egl.h for details. */ +- _this->egl_data->eglGetProcAddress = (void *(EGLAPIENTRY *)(const char *)) eglGetProcAddress; +- +- LOAD_FUNC(eglGetDisplay); +- LOAD_FUNC(eglInitialize); +- LOAD_FUNC(eglTerminate); +- LOAD_FUNC(eglChooseConfig); +- LOAD_FUNC(eglGetConfigAttrib); +- LOAD_FUNC(eglCreateContext); +- LOAD_FUNC(eglDestroyContext); +- LOAD_FUNC(eglCreateWindowSurface); +- LOAD_FUNC(eglDestroySurface); +- LOAD_FUNC(eglMakeCurrent); +- LOAD_FUNC(eglSwapBuffers); +- LOAD_FUNC(eglSwapInterval); +- LOAD_FUNC(eglWaitNative); +- LOAD_FUNC(eglWaitGL); +- LOAD_FUNC(eglBindAPI); +- LOAD_FUNC(eglQueryString); +- LOAD_FUNC(eglGetError); +- +- _this->egl_data->egl_display = _this->egl_data->eglGetDisplay(EGL_DEFAULT_DISPLAY); +- if (!_this->egl_data->egl_display) { +- return SDL_SetError("Could not get EGL display"); ++ emscripten_webgl_init_context_attributes(&attribs); ++ ++ attribs.alpha = _this->gl_config.alpha_size > 0; ++ attribs.depth = _this->gl_config.depth_size > 0; ++ attribs.stencil = _this->gl_config.stencil_size > 0; ++ attribs.antialias = _this->gl_config.multisamplebuffers == 1; ++ ++ if(_this->gl_config.major_version == 3) ++ attribs.majorVersion = 2; /* WebGL 2.0 ~= GLES 3.0 */ ++ ++ window_data = (SDL_WindowData *) window->driverdata; ++ ++ if (window_data->gl_context) { ++ SDL_SetError("Cannot create multiple webgl contexts per window"); ++ return NULL; + } + +- if (_this->egl_data->eglInitialize(_this->egl_data->egl_display, NULL, NULL) != EGL_TRUE) { +- return SDL_SetError("Could not initialize EGL"); ++ context = emscripten_webgl_create_context(window_data->canvas_id, &attribs); ++ ++ if (context < 0) { ++ SDL_SetError("Could not create webgl context"); ++ return NULL; + } + +- if (path) { +- SDL_strlcpy(_this->gl_config.driver_path, path, sizeof(_this->gl_config.driver_path) - 1); +- } else { +- *_this->gl_config.driver_path = '\0'; ++ if (emscripten_webgl_make_context_current(context) != EMSCRIPTEN_RESULT_SUCCESS) { ++ emscripten_webgl_destroy_context(context); ++ return NULL; + } + +- return 0; ++ window_data->gl_context = (SDL_GLContext)context; ++ ++ return (SDL_GLContext)context; + } + +-SDL_EGL_CreateContext_impl(Emscripten) +-SDL_EGL_MakeCurrent_impl(Emscripten) ++void Emscripten_GLES_DeleteContext(_THIS, SDL_GLContext context) ++{ ++ SDL_Window *window; ++ ++ /* remove the context from its window */ ++ for (window = _this->windows; window != NULL; window = window->next) { ++ SDL_WindowData *window_data; ++ window_data = (SDL_WindowData *) window->driverdata; ++ ++ if (window_data->gl_context == context) { ++ window_data->gl_context = NULL; ++ } ++ } ++ ++ emscripten_webgl_destroy_context((EMSCRIPTEN_WEBGL_CONTEXT_HANDLE)context); ++} + + int Emscripten_GLES_SwapWindow(_THIS, SDL_Window *window) + { +- EGLBoolean ret = SDL_EGL_SwapBuffers(_this, ((SDL_WindowData *) window->driverdata)->egl_surface); + if (emscripten_has_asyncify() && SDL_GetHintBoolean(SDL_HINT_EMSCRIPTEN_ASYNCIFY, SDL_TRUE)) { + /* give back control to browser for screen refresh */ + emscripten_sleep(0); + } +- return ret; ++ return 0; ++} ++ ++int Emscripten_GLES_MakeCurrent(_THIS, SDL_Window * window, SDL_GLContext context) ++{ ++ /* it isn't possible to reuse contexts across canvases */ ++ if (window && context) { ++ SDL_WindowData *window_data; ++ window_data = (SDL_WindowData *) window->driverdata; ++ ++ if (context != window_data->gl_context) { ++ return SDL_SetError("Cannot make context current to another window"); ++ } ++ } ++ ++ if (emscripten_webgl_make_context_current((EMSCRIPTEN_WEBGL_CONTEXT_HANDLE)context) != EMSCRIPTEN_RESULT_SUCCESS) { ++ return SDL_SetError("Unable to make context current"); ++ } ++ return 0; + } + +-#endif /* SDL_VIDEO_DRIVER_EMSCRIPTEN && SDL_VIDEO_OPENGL_EGL */ ++#endif /* SDL_VIDEO_DRIVER_EMSCRIPTEN */ + + /* vi: set ts=4 sw=4 expandtab: */ +diff --git a/src/video/emscripten/SDL_emscriptenopengles.h b/src/video/emscripten/SDL_emscriptenopengles.h +--- a/src/video/emscripten/SDL_emscriptenopengles.h ++++ b/src/video/emscripten/SDL_emscriptenopengles.h +@@ -23,25 +23,24 @@ + #ifndef SDL_emscriptenopengles_h_ + #define SDL_emscriptenopengles_h_ + +-#if SDL_VIDEO_DRIVER_EMSCRIPTEN && SDL_VIDEO_OPENGL_EGL ++#if SDL_VIDEO_DRIVER_EMSCRIPTEN + + #include "../SDL_sysvideo.h" +-#include "../SDL_egl_c.h" + + /* OpenGLES functions */ +-#define Emscripten_GLES_GetAttribute SDL_EGL_GetAttribute +-#define Emscripten_GLES_GetProcAddress SDL_EGL_GetProcAddress +-#define Emscripten_GLES_UnloadLibrary SDL_EGL_UnloadLibrary +-#define Emscripten_GLES_SetSwapInterval SDL_EGL_SetSwapInterval +-#define Emscripten_GLES_GetSwapInterval SDL_EGL_GetSwapInterval +-#define Emscripten_GLES_DeleteContext SDL_EGL_DeleteContext + + extern int Emscripten_GLES_LoadLibrary(_THIS, const char *path); ++extern void Emscripten_GLES_UnloadLibrary(_THIS); ++extern void * Emscripten_GLES_GetProcAddress(_THIS, const char *proc); ++extern int Emscripten_GLES_SetSwapInterval(_THIS, int interval); ++extern int Emscripten_GLES_GetSwapInterval(_THIS); ++ + extern SDL_GLContext Emscripten_GLES_CreateContext(_THIS, SDL_Window * window); ++extern void Emscripten_GLES_DeleteContext(_THIS, SDL_GLContext context); + extern int Emscripten_GLES_SwapWindow(_THIS, SDL_Window * window); + extern int Emscripten_GLES_MakeCurrent(_THIS, SDL_Window * window, SDL_GLContext context); + +-#endif /* SDL_VIDEO_DRIVER_EMSCRIPTEN && SDL_VIDEO_OPENGL_EGL */ ++#endif /* SDL_VIDEO_DRIVER_EMSCRIPTEN */ + + #endif /* SDL_emscriptenopengles_h_ */ + +diff --git a/src/video/emscripten/SDL_emscriptenvideo.c b/src/video/emscripten/SDL_emscriptenvideo.c +--- a/src/video/emscripten/SDL_emscriptenvideo.c ++++ b/src/video/emscripten/SDL_emscriptenvideo.c +@@ -27,7 +27,6 @@ + #include "SDL_hints.h" + #include "../SDL_sysvideo.h" + #include "../SDL_pixels_c.h" +-#include "../SDL_egl_c.h" + #include "../../events/SDL_events_c.h" + + #include "SDL_emscriptenvideo.h" +@@ -106,7 +105,6 @@ static SDL_VideoDevice *Emscripten_CreateDevice(void) + device->UpdateWindowFramebuffer = Emscripten_UpdateWindowFramebuffer; + device->DestroyWindowFramebuffer = Emscripten_DestroyWindowFramebuffer; + +-#if SDL_VIDEO_OPENGL_EGL + device->GL_LoadLibrary = Emscripten_GLES_LoadLibrary; + device->GL_GetProcAddress = Emscripten_GLES_GetProcAddress; + device->GL_UnloadLibrary = Emscripten_GLES_UnloadLibrary; +@@ -116,7 +114,6 @@ static SDL_VideoDevice *Emscripten_CreateDevice(void) + device->GL_GetSwapInterval = Emscripten_GLES_GetSwapInterval; + device->GL_SwapWindow = Emscripten_GLES_SwapWindow; + device->GL_DeleteContext = Emscripten_GLES_DeleteContext; +-#endif + + device->free = Emscripten_DeleteDevice; + +@@ -247,21 +244,6 @@ static int Emscripten_CreateWindow(_THIS, SDL_Window *window) + } + } + +-#if SDL_VIDEO_OPENGL_EGL +- if (window->flags & SDL_WINDOW_OPENGL) { +- if (!_this->egl_data) { +- if (SDL_GL_LoadLibrary(NULL) < 0) { +- return -1; +- } +- } +- wdata->egl_surface = SDL_EGL_CreateSurface(_this, 0); +- +- if (wdata->egl_surface == EGL_NO_SURFACE) { +- return SDL_SetError("Could not create GLES window surface"); +- } +- } +-#endif +- + wdata->window = window; + + /* Setup driver data for this window */ +@@ -314,12 +296,6 @@ static void Emscripten_DestroyWindow(_THIS, SDL_Window *window) + data = (SDL_WindowData *)window->driverdata; + + Emscripten_UnregisterEventHandlers(data); +-#if SDL_VIDEO_OPENGL_EGL +- if (data->egl_surface != EGL_NO_SURFACE) { +- SDL_EGL_DestroySurface(_this, data->egl_surface); +- data->egl_surface = EGL_NO_SURFACE; +- } +-#endif + + /* We can't destroy the canvas, so resize it to zero instead */ + emscripten_set_canvas_element_size(data->canvas_id, 0, 0); +@@ -341,6 +317,7 @@ static void Emscripten_SetWindowFullscreen(_THIS, SDL_Window *window, SDL_VideoD + SDL_bool is_desktop_fullscreen = (window->flags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN_DESKTOP; + int res; + ++ SDL_zero(strategy); + strategy.scaleMode = is_desktop_fullscreen ? EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH : EMSCRIPTEN_FULLSCREEN_SCALE_ASPECT; + + if (!is_desktop_fullscreen) { +diff --git a/src/video/emscripten/SDL_emscriptenvideo.h b/src/video/emscripten/SDL_emscriptenvideo.h +--- a/src/video/emscripten/SDL_emscriptenvideo.h ++++ b/src/video/emscripten/SDL_emscriptenvideo.h +@@ -28,18 +28,13 @@ + #include + #include + +-#if SDL_VIDEO_OPENGL_EGL +-#include +-#endif +- + typedef struct SDL_WindowData + { +-#if SDL_VIDEO_OPENGL_EGL +- EGLSurface egl_surface; +-#endif + SDL_Window *window; + SDL_Surface *surface; + ++ SDL_GLContext gl_context; ++ + char *canvas_id; + + float pixel_ratio; diff --git a/.ci/vcpkg/overlay-ports/sdl2/portfile.cmake b/.ci/vcpkg/overlay-ports/sdl2/portfile.cmake new file mode 100644 index 00000000000..8a6534f0ea5 --- /dev/null +++ b/.ci/vcpkg/overlay-ports/sdl2/portfile.cmake @@ -0,0 +1,142 @@ +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO libsdl-org/SDL + REF "release-${VERSION}" + SHA512 d3cf7d356b79184dd211c9fbbfcb2a83d1acb68ee549ab82be109cd899039f18f0dbf3aedbf0800793c3a68580688014863b5d9bf79bcd366ff0e88252955e3c + HEAD_REF main + PATCHES + deps.patch + alsa-dep-fix.patch + emscripten-webgl.patch +) + +string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "static" SDL_STATIC) +string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "dynamic" SDL_SHARED) +string(COMPARE EQUAL "${VCPKG_CRT_LINKAGE}" "static" FORCE_STATIC_VCRT) + +vcpkg_check_features(OUT_FEATURE_OPTIONS FEATURE_OPTIONS + FEATURES + alsa SDL_ALSA + alsa CMAKE_REQUIRE_FIND_PACKAGE_ALSA + ibus SDL_IBUS + samplerate SDL_LIBSAMPLERATE + vulkan SDL_VULKAN + wayland SDL_WAYLAND + x11 SDL_X11 + INVERTED_FEATURES + alsa CMAKE_DISABLE_FIND_PACKAGE_ALSA +) + +if ("x11" IN_LIST FEATURES) + message(WARNING "You will need to install Xorg dependencies to use feature x11:\nsudo apt install libx11-dev libxft-dev libxext-dev\n") +endif() +if ("wayland" IN_LIST FEATURES) + message(WARNING "You will need to install Wayland dependencies to use feature wayland:\nsudo apt install libwayland-dev libxkbcommon-dev libegl1-mesa-dev\n") +endif() +if ("ibus" IN_LIST FEATURES) + message(WARNING "You will need to install ibus dependencies to use feature ibus:\nsudo apt install libibus-1.0-dev\n") +endif() + +if(VCPKG_TARGET_IS_UWP) + set(configure_opts WINDOWS_USE_MSBUILD) +endif() + +if(VCPKG_TARGET_IS_EMSCRIPTEN) + list(APPEND FEATURE_OPTIONS "-DSDL_PTHREADS=ON") +endif() + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + ${configure_opts} + OPTIONS ${FEATURE_OPTIONS} + -DSDL_STATIC=${SDL_STATIC} + -DSDL_SHARED=${SDL_SHARED} + -DSDL_FORCE_STATIC_VCRT=${FORCE_STATIC_VCRT} + -DSDL_LIBC=ON + -DSDL_TEST=OFF + -DSDL_INSTALL_CMAKEDIR="cmake" + -DCMAKE_DISABLE_FIND_PACKAGE_Git=ON + -DPKG_CONFIG_USE_CMAKE_PREFIX_PATH=ON + -DSDL_LIBSAMPLERATE_SHARED=OFF + MAYBE_UNUSED_VARIABLES + SDL_FORCE_STATIC_VCRT + PKG_CONFIG_USE_CMAKE_PREFIX_PATH +) + +vcpkg_cmake_install() +vcpkg_cmake_config_fixup(CONFIG_PATH cmake) + +file(REMOVE_RECURSE + "${CURRENT_PACKAGES_DIR}/debug/include" + "${CURRENT_PACKAGES_DIR}/debug/share" + "${CURRENT_PACKAGES_DIR}/bin/sdl2-config" + "${CURRENT_PACKAGES_DIR}/debug/bin/sdl2-config" + "${CURRENT_PACKAGES_DIR}/SDL2.framework" + "${CURRENT_PACKAGES_DIR}/debug/SDL2.framework" + "${CURRENT_PACKAGES_DIR}/share/licenses" + "${CURRENT_PACKAGES_DIR}/share/aclocal" +) + +file(GLOB BINS "${CURRENT_PACKAGES_DIR}/debug/bin/*" "${CURRENT_PACKAGES_DIR}/bin/*") +if(NOT BINS) + file(REMOVE_RECURSE + "${CURRENT_PACKAGES_DIR}/bin" + "${CURRENT_PACKAGES_DIR}/debug/bin" + ) +endif() + +if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_UWP AND NOT VCPKG_TARGET_IS_MINGW) + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") + file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/lib/manual-link") + file(RENAME "${CURRENT_PACKAGES_DIR}/lib/SDL2main.lib" "${CURRENT_PACKAGES_DIR}/lib/manual-link/SDL2main.lib") + endif() + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") + file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/debug/lib/manual-link") + file(RENAME "${CURRENT_PACKAGES_DIR}/debug/lib/SDL2maind.lib" "${CURRENT_PACKAGES_DIR}/debug/lib/manual-link/SDL2maind.lib") + endif() + + file(GLOB SHARE_FILES "${CURRENT_PACKAGES_DIR}/share/sdl2/*.cmake") + foreach(SHARE_FILE ${SHARE_FILES}) + vcpkg_replace_string("${SHARE_FILE}" "lib/SDL2main" "lib/manual-link/SDL2main") + endforeach() +endif() + +vcpkg_copy_pdbs() + +set(DYLIB_COMPATIBILITY_VERSION_REGEX "set\\(DYLIB_COMPATIBILITY_VERSION (.+)\\)") +set(DYLIB_CURRENT_VERSION_REGEX "set\\(DYLIB_CURRENT_VERSION (.+)\\)") +file(STRINGS "${SOURCE_PATH}/CMakeLists.txt" DYLIB_COMPATIBILITY_VERSION REGEX ${DYLIB_COMPATIBILITY_VERSION_REGEX}) +file(STRINGS "${SOURCE_PATH}/CMakeLists.txt" DYLIB_CURRENT_VERSION REGEX ${DYLIB_CURRENT_VERSION_REGEX}) +string(REGEX REPLACE ${DYLIB_COMPATIBILITY_VERSION_REGEX} "\\1" DYLIB_COMPATIBILITY_VERSION "${DYLIB_COMPATIBILITY_VERSION}") +string(REGEX REPLACE ${DYLIB_CURRENT_VERSION_REGEX} "\\1" DYLIB_CURRENT_VERSION "${DYLIB_CURRENT_VERSION}") + +if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2main" "-lSDL2maind") + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2 " "-lSDL2d ") + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2-static " "-lSDL2-staticd ") +endif() + +if(VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic" AND VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/sdl2.pc" "-lSDL2-static " " ") + endif() + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-lSDL2-staticd " " ") + endif() +endif() + +if(VCPKG_TARGET_IS_UWP) + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/sdl2.pc" "$<$:d>.lib" "") + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/sdl2.pc" "-l-nodefaultlib:" "-nodefaultlib:") + endif() + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "$<$:d>.lib" "d") + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/sdl2.pc" "-l-nodefaultlib:" "-nodefaultlib:") + endif() +endif() + +vcpkg_fixup_pkgconfig() + +file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.txt") diff --git a/.ci/vcpkg/overlay-ports/sdl2/usage b/.ci/vcpkg/overlay-ports/sdl2/usage new file mode 100644 index 00000000000..1cddcd46ffc --- /dev/null +++ b/.ci/vcpkg/overlay-ports/sdl2/usage @@ -0,0 +1,8 @@ +sdl2 provides CMake targets: + + find_package(SDL2 CONFIG REQUIRED) + target_link_libraries(main + PRIVATE + $ + $,SDL2::SDL2,SDL2::SDL2-static> + ) diff --git a/.ci/vcpkg/overlay-ports/sdl2/vcpkg.json b/.ci/vcpkg/overlay-ports/sdl2/vcpkg.json new file mode 100644 index 00000000000..d0932679abf --- /dev/null +++ b/.ci/vcpkg/overlay-ports/sdl2/vcpkg.json @@ -0,0 +1,69 @@ +{ + "name": "sdl2", + "version": "2.28.5", + "port-version": 3, + "description": "Simple DirectMedia Layer is a cross-platform development library designed to provide low level access to audio, keyboard, mouse, joystick, and graphics hardware via OpenGL and Direct3D.", + "homepage": "https://www.libsdl.org/download-2.0.php", + "license": "Zlib", + "dependencies": [ + { + "name": "dbus", + "default-features": false, + "platform": "linux" + }, + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ], + "default-features": [ + { + "name": "ibus", + "platform": "linux" + }, + { + "name": "wayland", + "platform": "linux" + }, + { + "name": "x11", + "platform": "linux" + } + ], + "features": { + "alsa": { + "description": "Support for alsa audio", + "dependencies": [ + { + "name": "alsa", + "platform": "linux" + } + ] + }, + "ibus": { + "description": "Build with ibus IME support", + "supports": "linux" + }, + "samplerate": { + "description": "Use libsamplerate for audio rate conversion", + "dependencies": [ + "libsamplerate" + ] + }, + "vulkan": { + "description": "Vulkan functionality for SDL" + }, + "wayland": { + "description": "Build with Wayland support", + "supports": "linux" + }, + "x11": { + "description": "Build with X11 support", + "supports": "!windows" + } + } +} diff --git a/.ci/vcpkg/overlay-triplets/wasm32-emscripten.cmake b/.ci/vcpkg/overlay-triplets/wasm32-emscripten.cmake new file mode 100644 index 00000000000..7f9ecc1b227 --- /dev/null +++ b/.ci/vcpkg/overlay-triplets/wasm32-emscripten.cmake @@ -0,0 +1,32 @@ +set(VCPKG_ENV_PASSTHROUGH_UNTRACKED EMSCRIPTEN_ROOT EMSDK PATH) + +if(NOT DEFINED ENV{EMSCRIPTEN_ROOT}) + find_path(EMSCRIPTEN_ROOT "emcc") +else() + set(EMSCRIPTEN_ROOT "$ENV{EMSCRIPTEN_ROOT}") +endif() + +if(NOT EMSCRIPTEN_ROOT) + if(NOT DEFINED ENV{EMSDK}) + message(FATAL_ERROR "The emcc compiler not found in PATH") + endif() + set(EMSCRIPTEN_ROOT "$ENV{EMSDK}/upstream/emscripten") +endif() + +if(NOT EXISTS "${EMSCRIPTEN_ROOT}/cmake/Modules/Platform/Emscripten.cmake") + message(FATAL_ERROR "Emscripten.cmake toolchain file not found") +endif() + +# Get the path to *this* triplet file, and then back up to get the known path to the meta-toolchain in WZ's repo +get_filename_component(WZ_WASM_META_TOOLCHAIN "${CMAKE_CURRENT_LIST_DIR}/../../emscripten/toolchain/Toolchain-Emscripten.cmake" ABSOLUTE) +if(NOT EXISTS "${WZ_WASM_META_TOOLCHAIN}") + message(FATAL_ERROR "Failed to find WZ's Toolchain-Emscripten.cmake") +endif() + +set(VCPKG_TARGET_ARCHITECTURE wasm32) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) +set(VCPKG_CMAKE_SYSTEM_NAME Emscripten) +set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE "${WZ_WASM_META_TOOLCHAIN}") + +set(VCPKG_BUILD_TYPE "release") diff --git a/.github/workflows/CI_emscripten.yml b/.github/workflows/CI_emscripten.yml new file mode 100644 index 00000000000..944e02a8d25 --- /dev/null +++ b/.github/workflows/CI_emscripten.yml @@ -0,0 +1,227 @@ +name: Emscripten + +on: + push: + branches-ignore: + - 'l10n_**' # Push events to translation service branches (that begin with "l10n_") + pull_request: + # Match all pull requests... + paths-ignore: + # Except some text-only files / documentation + - 'ChangeLog' + # Except those that only include changes to stats + - 'data/base/stats/**' + - 'data/mp/stats/**' + - 'data/mp/multiplay/script/functions/camTechEnabler.js' + # Support running after "Draft Tag Release" workflow completes, as part of automated release process + workflow_run: + workflows: ["Draft Tag Release"] + push: + tags: + - '*' + types: + - completed + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + EMSDK_VERSION: 3.1.53 + CMAKE_BUILD_PARALLEL_LEVEL: 4 # See: https://github.blog/2024-01-17-github-hosted-runners-double-the-power-for-open-source/ + +jobs: + web-build: + strategy: + matrix: + include: + - arch: "wasm32" + upload_artifact: true + fail-fast: false + name: '${{ matrix.arch }}' + permissions: + contents: read + runs-on: ubuntu-latest + env: + WZ_WASM_ARCH: '${{ matrix.arch }}' + if: "!contains(github.event.head_commit.message, '[ci skip]')" + outputs: + # Needed by the release + other later jobs - despite this being a matrix job, this should be the same for all, so we can allow whatever is last to persist it + WZ_GITHUB_REF: ${{ steps.checkout-config.outputs.WZ_GITHUB_REF }} + WZ_GITHUB_SHA: ${{ steps.checkout-config.outputs.WZ_GITHUB_SHA }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + path: 'src' + - name: Configure Repo Checkout + id: checkout-config + working-directory: ./src + env: + WORKFLOW_RUN_CONCLUSION: ${{ github.event.workflow_run.conclusion }} + WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + . .ci/githubactions/checkout_config.sh + - name: Prepare Git Repo for autorevision + working-directory: '${{ github.workspace }}/src' + run: cmake -P .ci/githubactions/prepare_git_repo.cmake + - name: Init Git Submodules + working-directory: '${{ github.workspace }}/src' + run: git submodule update --init --recursive + - name: Prep Directories + run: | + mkdir -p "${{ github.workspace }}/build" + mkdir -p "${{ github.workspace }}/installed" + mkdir -p "${{ github.workspace }}/output" + - name: Compute build variables + run: | + WZ_DISTRIBUTOR="UNKNOWN" + if [ "${GITHUB_REPOSITORY}" == "Warzone2100/warzone2100" ]; then + WZ_DISTRIBUTOR="wz2100.net" + fi + echo "WZ_DISTRIBUTOR=${WZ_DISTRIBUTOR}" + echo "WZ_DISTRIBUTOR=${WZ_DISTRIBUTOR}" >> $GITHUB_ENV + + WZ_BUILD_DESC="web_${WZ_WASM_ARCH}" + echo "WZ_BUILD_DESC=${WZ_BUILD_DESC}" + echo "WZ_BUILD_DESC=${WZ_BUILD_DESC}" >> $GITHUB_ENV + + - name: Prep Build Environment + run: | + # Install additional host tools + DEBIAN_FRONTEND=noninteractive sudo apt-get -y install cmake git zip unzip gettext asciidoctor + + - uses: actions/setup-node@v4 + + - name: Install workbox-cli + run: npm install workbox-cli --global + + - name: Install EMSDK + id: emsdk + run: | + git clone https://github.com/emscripten-core/emsdk.git + cd emsdk + # Download and install the latest SDK tools + ./emsdk install ${EMSDK_VERSION} + # Make the "latest" SDK "active" for the current user. (writes .emscripten file) + ./emsdk activate ${EMSDK_VERSION} + # Output full path to activation script + echo "CI_EMSDK_ENV_SCRIPT_PATH=$(pwd)/emsdk_env.sh" >> $GITHUB_OUTPUT + echo "CI_EMSDK_ENV_SCRIPT_PATH=$(pwd)/emsdk_env.sh" >> $GITHUB_ENV + + - name: CMake Configure + working-directory: '${{ github.workspace }}/build' + env: + WZ_INSTALL_DIR: '${{ github.workspace }}/installed' + run: | + # Setup vcpkg in build dir + git clone https://github.com/microsoft/vcpkg.git vcpkg + # CMake Configure + source "${CI_EMSDK_ENV_SCRIPT_PATH}" + echo "::add-matcher::${GITHUB_WORKSPACE}/src/.ci/githubactions/pattern_matchers/cmake.json" + cmake -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DWZ_ENABLE_WARNINGS:BOOL=ON -DWZ_DISTRIBUTOR:STRING="${WZ_DISTRIBUTOR}" "-DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake" "-DVCPKG_TARGET_TRIPLET=${WZ_WASM_ARCH}-emscripten" "-DCMAKE_INSTALL_PREFIX:PATH=${WZ_INSTALL_DIR}" "${{ github.workspace }}/src" + echo "::remove-matcher owner=cmake::" + + - name: CMake Build + working-directory: '${{ github.workspace }}/build' + run: | + source "${CI_EMSDK_ENV_SCRIPT_PATH}" + echo "::add-matcher::${GITHUB_WORKSPACE}/src/.ci/githubactions/pattern_matchers/clang.json" + cmake --build . --config ${BUILD_TYPE} --target install + echo "::remove-matcher owner=clang::" + + - name: Debug Output + working-directory: ${{github.workspace}}/installed + run: ls -al + + - name: Package Archive + working-directory: '${{ github.workspace }}/build' + env: + OUTPUT_DIR: "${{ github.workspace }}/output" + run: | + cpack --config "./CPackConfig.cmake" -G ZIP -D CPACK_PACKAGE_FILE_NAME="warzone2100_archive" -D CPACK_INCLUDE_TOPLEVEL_DIRECTORY=OFF -D CPACK_ARCHIVE_COMPONENT_INSTALL=ON -D CPACK_COMPONENTS_GROUPING=ALL_COMPONENTS_IN_ONE + OUTPUT_FILE_NAME="warzone2100_${WZ_BUILD_DESC}_archive.zip" + mv "./warzone2100_archive.zip" "${OUTPUT_DIR}/${OUTPUT_FILE_NAME}" + echo "Generated .zip: \"${OUTPUT_FILE_NAME}\"" + echo " -> SHA512: $(sha512sum "${OUTPUT_DIR}/${OUTPUT_FILE_NAME}")" + echo " -> Size (bytes): $(stat -c %s "${OUTPUT_DIR}/${OUTPUT_FILE_NAME}")" + echo "WZ_FULL_OUTPUT_ZIP_PATH=${OUTPUT_DIR}/${OUTPUT_FILE_NAME}" >> $GITHUB_ENV + + - name: 'Upload Artifact - (Archive)' + uses: actions/upload-artifact@v3 + if: success() && (matrix.upload_artifact == true) && (github.repository == 'Warzone2100/warzone2100') + with: + name: warzone2100_${{ env.WZ_BUILD_DESC }}_archive + path: '${{ env.WZ_FULL_OUTPUT_ZIP_PATH }}' + if-no-files-found: 'error' + + upload_to_release: + strategy: + matrix: + architecture: ["wasm32"] + fail-fast: false + name: 'Upload to Release' + permissions: + contents: write # Needed to upload to releases + runs-on: ubuntu-latest + if: (github.repository == 'Warzone2100/warzone2100') && (github.event_name == 'workflow_run' && github.event.workflow_run.name == 'Draft Tag Release') + needs: web-build + env: + WZ_WASM_ARCH: '${{ matrix.arch }}' + WZ_BUILD_DESC: 'web_${{ matrix.arch }}' + steps: + - name: Prep Environment + run: mkdir dl-archive + - name: Download Archive Artifact + uses: actions/download-artifact@v3 + with: + name: 'warzone2100_${{ env.WZ_BUILD_DESC }}_archive' + path: ./dl-archive + - name: Upload Release Asset + run: | + SOURCE_TAG="${WZ_GITHUB_REF#refs/tags/}" + gh release upload "${SOURCE_TAG}" "${{ env.WZ_FULL_OUTPUT_ZIP_PATH }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + WZ_GITHUB_REF: ${{ needs.web-build.outputs.WZ_GITHUB_REF }} + WZ_FULL_OUTPUT_ZIP_PATH: ./dl-archive/warzone2100_${{ env.WZ_BUILD_DESC }}_archive.zip + + publish_dev_build: + strategy: + matrix: + architecture: ["wasm32"] + fail-fast: false + name: 'Publish Dev Build (${{ matrix.architecture }})' + # Run on push to master branch (development build) + if: (github.repository == 'Warzone2100/warzone2100') && (github.event_name == 'push' && github.ref == 'refs/heads/master') + needs: web-build + uses: ./.github/workflows/publish_web_build.yml + with: + env: webedition_deploy_dev + architecture: ${{ matrix.architecture }} + artifact: 'warzone2100_web_${{ matrix.architecture }}_archive' + built-ref: ${{ needs.web-build.outputs.WZ_GITHUB_REF }} + built-commit-sha: ${{ needs.web-build.outputs.WZ_GITHUB_SHA }} + release: false + ssh-dest-host: ${{ vars.WZ_WEB_BUILD_UPLOAD_SSH_HOST }} + secrets: inherit + + publish_release_build: + strategy: + matrix: + architecture: ["wasm32"] + fail-fast: false + name: 'Publish Release Build (${{ matrix.architecture }})' + # Run on tag release automation build + # Note: the build will be uploaded to a /release// folder, but will not be set as the "latest" release until the GitHub Release is published + if: (github.repository == 'Warzone2100/warzone2100') && (github.event_name == 'workflow_run' && github.event.workflow_run.name == 'Draft Tag Release') + needs: web-build + uses: ./.github/workflows/publish_web_build.yml + with: + env: webedition_deploy_release + architecture: ${{ matrix.architecture }} + artifact: 'warzone2100_web_${{ matrix.architecture }}_archive' + built-ref: ${{ needs.web-build.outputs.WZ_GITHUB_REF }} + built-commit-sha: ${{ needs.web-build.outputs.WZ_GITHUB_SHA }} + release: true + ssh-dest-host: ${{ vars.WZ_WEB_BUILD_UPLOAD_SSH_HOST }} + secrets: inherit diff --git a/.github/workflows/CI_linter.yml b/.github/workflows/CI_linter.yml index ea6e014d08b..fe74c5c17be 100644 --- a/.github/workflows/CI_linter.yml +++ b/.github/workflows/CI_linter.yml @@ -13,6 +13,9 @@ jobs: name: 'Lint Code Base' permissions: contents: read + packages: read + # To report GitHub Actions status checks + statuses: write runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[ci skip]')" steps: @@ -21,14 +24,16 @@ jobs: # Full git history is needed to get a proper list of changed files within `super-linter` fetch-depth: 0 - name: Lint Code Base - uses: github/super-linter@v4 + uses: super-linter/super-linter@v6.0.0 # x-release-please-version env: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: master # .eslintrc.json allows non-standard comments - FILTER_REGEX_EXCLUDE: .*(\.eslintrc\.json|3rdparty/).* + FILTER_REGEX_EXCLUDE: .*(\.eslintrc\.json|3rdparty/|platforms/emscripten/).* LINTER_RULES_PATH: / VALIDATE_YAML: true VALIDATE_JSON: true VALIDATE_JAVASCRIPT_ES: true JAVASCRIPT_ES_CONFIG_FILE: '.eslintrc.json' + # To report GitHub Actions status checks + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish_web_build.yml b/.github/workflows/publish_web_build.yml new file mode 100644 index 00000000000..27e9fcdedb8 --- /dev/null +++ b/.github/workflows/publish_web_build.yml @@ -0,0 +1,162 @@ +name: Publish Web Build + +on: + workflow_call: + inputs: + env: + description: 'The environment used for deployment (must contain the required secrets)' + required: true + type: string + architecture: + required: true + type: string + artifact: + description: 'The name of the artifact to download' + required: true + type: string + built-ref: + description: 'The built ref (i.e. WZ_GITHUB_REF from the calling workflow)' + required: true + type: string + built-commit-sha: + description: 'The built commit sha (i.e. WZ_GITHUB_SHA from the calling workflow)' + required: true + type: string + release: + description: 'Whether this is a release build' + required: true + type: boolean + ssh-dest-host: + description: 'The destination HOST for the rsync' + required: true + type: string + secrets: + WZ_WEB_BUILD_UPLOAD_SSH_KEY: + required: true + WZ_WEB_BUILD_UPLOAD_SSH_USERNAME: + required: true + +jobs: + publish_web_build: + name: 'Publish Build (${{ inputs.architecture }})' + permissions: + contents: read + runs-on: ubuntu-latest + # For this job to work, the following secrets must be set in the specified environment: + # WZ_WEB_BUILD_UPLOAD_SSH_KEY, WZ_WEB_BUILD_UPLOAD_SSH_USERNAME + environment: ${{ inputs.env }} + steps: + - name: Prep Environment + id: settings + env: + WASM_ARCHITECTURE: ${{ inputs.architecture }} + WZ_GITHUB_REF: ${{ inputs.built-ref }} + WZ_IS_RELEASE: ${{ inputs.release }} + run: | + mkdir dl-archive + CHANGELOG_DIR="${GITHUB_WORKSPACE}/temp/changes" + mkdir -p "${CHANGELOG_DIR}" + echo "CHANGELOG_DIR=${CHANGELOG_DIR}" >> $GITHUB_OUTPUT + # BUILD_ASSETS_UPLOAD_DIR + BUILD_ASSETS_UPLOAD_DIR="build_wz_${{ inputs.architecture }}}" + echo "BUILD_ASSETS_UPLOAD_DIR=${BUILD_ASSETS_UPLOAD_DIR}" >> $GITHUB_ENV + echo "BUILD_ASSETS_UPLOAD_DIR=${BUILD_ASSETS_UPLOAD_DIR}" >> $GITHUB_OUTPUT + # Determine additional output subdir based on architecture + # wasm32 gets the root, wasm64 (or other) gets a subdirectory + WZ_UPLOAD_SUBDIR="arch/${WASM_ARCHITECTURE}" + if [ "${WASM_ARCHITECTURE}" == "wasm32" ]; then + WZ_UPLOAD_SUBDIR="" + fi + # Determine upload path + if [ "${WZ_IS_RELEASE}" == "true" ]; then + SOURCE_TAG="${WZ_GITHUB_REF#refs/tags/}" + WZ_UPLOAD_PATH="/${SOURCE_TAG}/${WZ_UPLOAD_SUBDIR}" + else + WZ_UPLOAD_PATH="/${WZ_UPLOAD_SUBDIR}" + fi + echo "WZ_UPLOAD_PATH=${WZ_UPLOAD_PATH}" >> $GITHUB_ENV + echo "WZ_UPLOAD_PATH=${WZ_UPLOAD_PATH}" >> $GITHUB_OUTPUT + - name: Download Archive Artifact + uses: actions/download-artifact@v3 + with: + name: '${{ inputs.artifact }}' + path: ./dl-archive + - name: Extract archive + run: | + ZIPFILE="./dl-archive/warzone2100_web_${{ inputs.architecture }}_archive.zip" + unzip -o "${ZIPFILE}" -d "${BUILD_ASSETS_UPLOAD_DIR}" && rm "${ZIPFILE}" + - name: Get list of files + id: fileslist + working-directory: '${{ steps.settings.outputs.BUILD_ASSETS_UPLOAD_DIR }}' + run: | + # Get the list of files / paths changed in the latest commit + CHANGED_FILES_LIST="${{ steps.settings.outputs.CHANGELOG_DIR }}/changedpaths.txt" + find . -type f -print | cut -d/ -f2- > "${CHANGED_FILES_LIST}" + cat "${CHANGED_FILES_LIST}" + echo "CHANGED_FILES_LIST=${CHANGED_FILES_LIST}" >> $GITHUB_OUTPUT + - name: Set up SSH Agent + env: + UPLOAD_SSH_KEY: ${{ secrets.WZ_WEB_BUILD_UPLOAD_SSH_KEY }} + run: | + eval "$(ssh-agent -s)" + set +x + + # Add the private key to SSH agent + mkdir -p ~/.ssh/ + echo "${UPLOAD_SSH_KEY}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-add ~/.ssh/id_ed25519 + + # Create a public key file + ssh-keygen -y -f ~/.ssh/id_ed25519 > ~/.ssh/id_ed25519.pub + - name: Upload build to server + env: + BUILD_ASSETS_UPLOAD_DIR: '${{ steps.settings.outputs.BUILD_ASSETS_UPLOAD_DIR }}' + WZ_WEB_BUILD_UPLOAD_USERNAME: ${{ secrets.WZ_WEB_BUILD_UPLOAD_SSH_USERNAME }} + WZ_WEB_BUILD_UPLOAD_SSH_HOST: ${{ inputs.ssh-dest-host }} + run: | + # Two-stage rsync: + # First, sync everything except the server-worker.js + # Then, sync the server-worker.js + # (If replacing in-place, the service-worker should be updated last) + echo "::group::rsync" + rsync -chvzP -rlpt --exclude=/service-worker.js --stats --delete "${BUILD_ASSETS_UPLOAD_DIR}/" "${WZ_WEB_BUILD_UPLOAD_USERNAME}@${WZ_WEB_BUILD_UPLOAD_SSH_HOST}.wz2100.net:${WZ_UPLOAD_PATH}" + echo "::endgroup::" + echo "::group::rsync (service-worker.js)" + rsync -chvzP -lpt --stats "${BUILD_ASSETS_UPLOAD_DIR}/service-worker.js" "${WZ_WEB_BUILD_UPLOAD_USERNAME}@${WZ_WEB_BUILD_UPLOAD_SSH_HOST}:${WZ_UPLOAD_PATH}/" + echo "::endgroup::" + rm ~/.ssh/id_ed25519 + - name: 'Generate Cloudflare Cache Purge URLs List' + id: purgeurls + env: + CHANGED_FILES_LIST: '${{ steps.fileslist.outputs.CHANGED_FILES_LIST }}' + GEN_PURGE_URLS_SCRIPT_DL: https://raw.githubusercontent.com/Warzone2100/update-data/master/ci/gen_purge_url_batches.py + GEN_PURGE_URLS_SCRIPT_SHA512: 65d21f9b204d8febc700d613070b50e1ef6f13a46eb406206c047c7085b7d94124aaee082d1ef8c2d656983f9270d151794909ba859ad7438666ed821a0b9ea3 + run: | + PURGE_URLS_DATA_FILES_DIR="purged-files-dir" + mkdir "${PURGE_URLS_DATA_FILES_DIR}" + # Get the script + curl -L --retry 3 -o "gen_purge_url_batches.py" "${GEN_PURGE_URLS_SCRIPT_DL}" + DOWNLOADED_SHA512="$(sha512sum "${GEN_PURGE_URLS_SCRIPT_DL}")" + if [ "${GEN_PURGE_URLS_SCRIPT_SHA512}" != "${DOWNLOADED_SHA512}" ]; then + echo "::error ::Downloaded script hash ${DOWNLOADED_SHA512} does not match expected ${GEN_PURGE_URLS_SCRIPT_SHA512}" + exit 1 + fi + # Run the gen_purge_url_batches script + python3 "./gen_purge_url_batches.py" "play.wz2100.net" "${CHANGED_FILES_LIST}" "${PURGE_URLS_DATA_FILES_DIR}" + echo "PURGE_URLS_DATA_FILES_DIR=${PURGE_URLS_DATA_FILES_DIR}" >> $GITHUB_OUTPUT + - name: 'Purge Cloudflare Cache' + env: + CLOUDFLARE_ZONE: ${{ secrets.CLOUDFLARE_WZ2100_ZONE }} + CLOUDFLARE_CACHEPURGE_TOKEN: ${{ secrets.CLOUDFLARE_WZ2100_CACHEPURGE_TOKEN }} + run: | + # Needs to handle multiple data files, since each purge command can only send a max of 30 URLs + for file in ${{ steps.purgeurls.outputs.PURGE_URLS_DATA_FILES_DIR }}/* + do + echo "File: $file" + curl -X POST "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE}/purge_cache" \ + -H "Authorization: Bearer ${CLOUDFLARE_CACHEPURGE_TOKEN}" \ + -H "Content-Type: application/json" \ + --data-binary "@$file" + done; # file + echo "Done." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6bdb6353afd..9cf904f8b79 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,6 +47,42 @@ jobs: -H 'Accept: application/vnd.github.everest-preview+json' \ -u ${{ secrets.DEPLOY_DISPATCH_ACCESS_TOKEN }} \ --data '{"event_type": "github_release_update", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}' + webedition-release-commit: + name: 'Commit Web Edition Release' + permissions: + contents: read + runs-on: ubuntu-latest + if: ((github.event.action == 'published') || (github.event.action == 'released')) && (github.repository == 'Warzone2100/warzone2100') + environment: webedition_release_management + # The following must be set in the 'webedition_release_management' environment: + # Secret: WZ_WEB_BUILD_RELEASE_MANAGEMENT_TOKEN + # Variable: WZ_WEB_BUILD_RELEASE_MANAGEMENT_API_URL + steps: + - name: 'Get actual latest release from GH' + run: | + # Get the actual latest release from GitHub directly + # (which could differ from the release associated with this event, if someone publishes some older versions as GitHub Releases at some point) + # (this also ensure that prereleases - which won't get set as the "latest" release - aren't set as /latest/) + WZ_LATEST_RELEASE_TAG="$(gh --repo $GITHUB_REPOSITORY release view --json 'tagName' --jq '.tagName')" + echo "WZ_LATEST_RELEASE_TAG=${WZ_LATEST_RELEASE_TAG}" + echo "WZ_LATEST_RELEASE_TAG=${WZ_LATEST_RELEASE_TAG}" >> $GITHUB_ENV + - name: 'Set play.wz2100.net/latest/ to the latest release' + env: + WZ_WEB_BUILD_RELEASE_MANAGEMENT_TOKEN: ${{ secrets.WZ_WEB_BUILD_RELEASE_MANAGEMENT_TOKEN }} + WZ_WEB_BUILD_RELEASE_MANAGEMENT_API_URL: ${{ vars.WZ_WEB_BUILD_RELEASE_MANAGEMENT_API_URL }} + run: | + curl -i -X POST "${WZ_WEB_BUILD_RELEASE_MANAGEMENT_API_URL}" \ + --data "token=${WZ_WEB_BUILD_RELEASE_MANAGEMENT_TOKEN}&setlatest=${WZ_LATEST_RELEASE_TAG}" + - name: 'Purge Cloudflare Cache' + if: success() && (steps.publishpages.outputs.PROCESS_DEPLOYMENT == 'true') + env: + CLOUDFLARE_ZONE: ${{ secrets.CLOUDFLARE_WZ2100_ZONE }} + CLOUDFLARE_CACHEPURGE_TOKEN: ${{ secrets.CLOUDFLARE_WZ2100_CACHEPURGE_TOKEN }} + run: | + curl -X POST "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE}/purge_cache" \ + -H "Authorization: Bearer ${CLOUDFLARE_CACHEPURGE_TOKEN}" \ + -H "Content-Type: application/json" \ + --data '{"purge_everything":true}' sentry-release-commit-info: name: 'Upload Release Commit Info' permissions: diff --git a/.gitignore b/.gitignore index f10bf931526..6bb34e0176a 100644 --- a/.gitignore +++ b/.gitignore @@ -271,7 +271,6 @@ devpkg/* *.tlog *.lastbuildstate *.cache* -*manifest.* *.exp warzone*.*build *.lnk diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index 0f2c6498c1c..65187258c73 100644 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -1,13 +1,13 @@ cmake_minimum_required (VERSION 3.5...3.24) SET(UTF8PROC_INSTALL OFF CACHE BOOL "Enable installation of utf8proc" FORCE) -add_subdirectory(utf8proc) +add_subdirectory(utf8proc EXCLUDE_FROM_ALL) set_property(TARGET utf8proc PROPERTY FOLDER "3rdparty") -add_subdirectory(launchinfo) +add_subdirectory(launchinfo EXCLUDE_FROM_ALL) set_property(TARGET launchinfo PROPERTY FOLDER "3rdparty") -add_subdirectory(fmt) +add_subdirectory(fmt EXCLUDE_FROM_ALL) set_property(TARGET fmt PROPERTY FOLDER "3rdparty") # inih library @@ -34,7 +34,7 @@ set_property(TARGET re2 PROPERTY FOLDER "3rdparty") set_property(TARGET re2 PROPERTY XCODE_ATTRIBUTE_GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS NO) # -Wmissing-field-initializers set_property(TARGET re2 PROPERTY XCODE_ATTRIBUTE_WARNING_CFLAGS "-Wno-missing-field-initializers") -add_subdirectory(EmbeddedJSONSignature) +add_subdirectory(EmbeddedJSONSignature EXCLUDE_FROM_ALL) set_property(TARGET EmbeddedJSONSignature PROPERTY FOLDER "3rdparty") if(ENABLE_DISCORD) @@ -54,6 +54,9 @@ set(SQLITECPP_RUN_CPPLINT OFF CACHE BOOL "Run cpplint.py tool for Google C++ Sty set(SQLITECPP_RUN_CPPCHECK OFF CACHE BOOL "Run cppcheck C++ static analysis tool." FORCE) set(SQLITECPP_BUILD_EXAMPLES OFF CACHE BOOL "Build examples." FORCE) set(SQLITECPP_BUILD_TESTS OFF CACHE BOOL "Build and run tests." FORCE) +if(CMAKE_SYSTEM_NAME MATCHES "Emscripten") + set(SQLITECPP_USE_STACK_PROTECTION OFF CACHE BOOL "USE Stack Protection hardening." FORCE) +endif() add_subdirectory(SQLiteCpp EXCLUDE_FROM_ALL) set_property(TARGET SQLiteCpp PROPERTY FOLDER "3rdparty") diff --git a/3rdparty/basis_universal_host_build/CMakeLists.txt b/3rdparty/basis_universal_host_build/CMakeLists.txt index 1dfd9a01b2b..0afa5119264 100644 --- a/3rdparty/basis_universal_host_build/CMakeLists.txt +++ b/3rdparty/basis_universal_host_build/CMakeLists.txt @@ -27,4 +27,8 @@ ExternalProject_Add(basisuExecutable INSTALL_COMMAND "" #EXCLUDE_FROM_ALL TRUE BUILD_ALWAYS TRUE + LOG_CONFIGURE TRUE + LOG_BUILD TRUE + LOG_INSTALL TRUE + LOG_OUTPUT_ON_FAILURE TRUE ) diff --git a/CMakeLists.txt b/CMakeLists.txt index a57272e9d32..3ff20101610 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,14 +14,21 @@ endif() include(CMakeDependentOption) -OPTION(ENABLE_DOCS "Enable documentation generation" ON) -OPTION(ENABLE_NLS "Native Language Support" ON) OPTION(WZ_ENABLE_WARNINGS "Enable (additional) warnings" OFF) OPTION(WZ_ENABLE_WARNINGS_AS_ERRORS "Enable compiler flags that treat (most) warnings as errors" ON) -OPTION(WZ_ENABLE_BACKEND_VULKAN "Enable Vulkan backend" ON) OPTION(WZ_ENABLE_BASIS_UNIVERSAL "Enable Basis Universal texture support" ON) OPTION(WZ_DEBUG_GFX_API_LEAKS "Enable debugging for graphics API leaks" ON) OPTION(WZ_FORCE_MINIMAL_OPUSFILE "Force a minimal build of Opusfile, since WZ does not need (or want) HTTP stream support" ON) +OPTION(ENABLE_NLS "Native Language Support" ON) +if(NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten") + OPTION(ENABLE_DOCS "Enable documentation generation" ON) + OPTION(WZ_ENABLE_BACKEND_VULKAN "Enable Vulkan backend" ON) + set(WZ_USE_STACK_PROTECTION ON CACHE BOOL "Use Stack Protection hardening." FORCE) +else() + set(WZ_SKIP_ADDITIONAL_FONTS ON CACHE BOOL "Skip additional fonts (used to display CJK glyphs)" FORCE) + set(WZ_USE_STACK_PROTECTION OFF CACHE BOOL "Use Stack Protection hardening." FORCE) + OPTION(WZ_EMSCRIPTEN_COMPRESS_OUTPUT "Compress Emscripten output (generate .gz files)" ON) +endif() # Dev options OPTION(WZ_PROFILING_NVTX "Add NVTX-based profiling instrumentation to the code" OFF) @@ -93,6 +100,12 @@ if(NOT DEFINED WZ_DATADIR) set(WZ_DATADIR "${CMAKE_INSTALL_DATAROOTDIR}/warzone2100${WZ_OUTPUT_NAME_SUFFIX}") endif() endif() +if(NOT DEFINED WZ_LOCALEDIR) + set(WZ_LOCALEDIR "${CMAKE_INSTALL_LOCALEDIR}") + if(CMAKE_SYSTEM_NAME MATCHES "Emscripten") + set(WZ_LOCALEDIR "/share/locale") + endif() +endif() if(CMAKE_SYSTEM_NAME MATCHES "Windows") if(NOT CMAKE_INSTALL_BINDIR STREQUAL "bin") # Windows builds expect a non-empty BINDIR @@ -133,6 +146,63 @@ if(CMAKE_HOST_SYSTEM_NAME MATCHES "Darwin") list(APPEND CMAKE_PREFIX_PATH "/usr/local/opt/gettext") endif() +if(CMAKE_SYSTEM_NAME MATCHES "Emscripten") + # Check minimum Emscripten version + if (NOT "${EMSCRIPTEN_VERSION}" VERSION_GREATER 3.1.0) + message(FATAL_ERROR "Emscripten version must be at least 3.1.0") + endif() + + # For Emscripten, we must currently get the following packages from Emscripten ports: + # - SDL2 # NOT FOR NOW - must use vcpkg port as it has custom patches + # - Freetype + # - Harfbuzz + # We must also specify: + # - Pthread support + # - Exception support + # - WebGL 2.0 support [LINK - see src/CMakeLists.txt] + # - Fetch API [LINK - see src/CMakeLists.txt] + + set(COMP_AND_LINK_FLAGS "-fwasm-exceptions") + if (WZ_EMSCRIPTEN_ENABLE_ASAN) + set(COMP_AND_LINK_FLAGS "${COMP_AND_LINK_FLAGS} -fsanitize=address -fsanitize-recover=address") + endif() + #set(USE_FLAGS "-s USE_PTHREADS=1 -s USE_SDL=2 -s USE_FREETYPE=1 -s USE_HARFBUZZ=1") + set(USE_FLAGS "-s USE_PTHREADS=1 -s USE_SDL=0 -s USE_FREETYPE=1 -s USE_HARFBUZZ=1") + + # Set various flags and executable settings + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COMP_AND_LINK_FLAGS} ${USE_FLAGS}") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COMP_AND_LINK_FLAGS} ${USE_FLAGS}") + set(CMAKE_EXECUTABLE_SUFFIX .html) + + # -fwasm-exceptions must be passed to linker as well + add_link_options( + "$<$:-fwasm-exceptions>" + ) + if (WZ_EMSCRIPTEN_ENABLE_ASAN) + add_link_options( + "$<$:-fsanitize=address>" + "$<$:-fsanitize-recover=address>" + ) + endif() + + # enable separate-dwarf debug info for Debug/RelWithDebInfo builds + set(debug_builds_only "$,$>") + add_compile_options( + "$<$,${debug_builds_only}>:-gseparate-dwarf>" + ) + add_link_options( + "$<$,${debug_builds_only}>:-gseparate-dwarf>" + ) + if (WZ_EMSCRIPTEN_ENABLE_ASAN) + add_compile_options( + #"$<$,${debug_builds_only}>:-gsource-map>" + ) + add_link_options( + #"$<$,${debug_builds_only}>:-gsource-map>" + ) + endif() +endif() + INCLUDE(AddTargetLinkFlagsIfSupported) # Use "-fPIC" / "-fPIE" for all targets by default, including static libs @@ -163,8 +233,8 @@ if("${CMAKE_CXX_COMPILER_ID}" MATCHES "MSVC") # Default stack size is 1MB - increase to better match other platforms set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /STACK:8000000") -elseif("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|Clang" AND NOT APPLE) - # Ensure all builds always have debug info built (Xcode is handled separately below) +elseif("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|Clang" AND NOT APPLE AND NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten") + # Ensure all builds always have debug info built (Xcode is handled separately below, Emscripten handled above) set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -g") # Ensure symbols can be demangled on Linux Debug builds set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -rdynamic") @@ -199,34 +269,36 @@ endif() include(CheckCCompilerFlag) include(CheckCXXCompilerFlag) -# Enable stack protection, if supported by the compiler -# Prefer -fstack-protector-strong if supported, fall-back to -fstack-protector -check_c_compiler_flag(-fstack-protector-strong HAS_CFLAG_FSTACK_PROTECTOR_STRONG) -if (HAS_CFLAG_FSTACK_PROTECTOR_STRONG) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector-strong") -else() - check_c_compiler_flag(-fstack-protector HAS_CFLAG_FSTACK_PROTECTOR) - if (HAS_CFLAG_FSTACK_PROTECTOR) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector") +if (WZ_USE_STACK_PROTECTION) + # Enable stack protection, if supported by the compiler + # Prefer -fstack-protector-strong if supported, fall-back to -fstack-protector + check_c_compiler_flag(-fstack-protector-strong HAS_CFLAG_FSTACK_PROTECTOR_STRONG) + if (HAS_CFLAG_FSTACK_PROTECTOR_STRONG) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector-strong") + else() + check_c_compiler_flag(-fstack-protector HAS_CFLAG_FSTACK_PROTECTOR) + if (HAS_CFLAG_FSTACK_PROTECTOR) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector") + endif() endif() -endif() -check_cxx_compiler_flag(-fstack-protector-strong HAS_CXXFLAG_FSTACK_PROTECTOR_STRONG) -if (HAS_CXXFLAG_FSTACK_PROTECTOR_STRONG) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstack-protector-strong") -else() - check_cxx_compiler_flag(-fstack-protector HAS_CXXFLAG_FSTACK_PROTECTOR) - if (HAS_CXXFLAG_FSTACK_PROTECTOR) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstack-protector") + check_cxx_compiler_flag(-fstack-protector-strong HAS_CXXFLAG_FSTACK_PROTECTOR_STRONG) + if (HAS_CXXFLAG_FSTACK_PROTECTOR_STRONG) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstack-protector-strong") + else() + check_cxx_compiler_flag(-fstack-protector HAS_CXXFLAG_FSTACK_PROTECTOR) + if (HAS_CXXFLAG_FSTACK_PROTECTOR) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstack-protector") + endif() + endif() + # Enable -fstack-clash-protection if available + check_c_compiler_flag(-fstack-clash-protection HAS_CFLAG_FSTACK_CLASH_PROTECTION) + if (HAS_CFLAG_FSTACK_CLASH_PROTECTION AND NOT (MINGW OR APPLE)) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-clash-protection") + endif() + check_cxx_compiler_flag(-fstack-clash-protection HAS_CXXFLAG_FSTACK_CLASH_PROTECTION) + if (HAS_CXXFLAG_FSTACK_CLASH_PROTECTION AND NOT (MINGW OR APPLE)) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstack-clash-protection") endif() -endif() -# Enable -fstack-clash-protection if available -check_c_compiler_flag(-fstack-clash-protection HAS_CFLAG_FSTACK_CLASH_PROTECTION) -if (HAS_CFLAG_FSTACK_CLASH_PROTECTION AND NOT (MINGW OR APPLE)) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-clash-protection") -endif() -check_cxx_compiler_flag(-fstack-clash-protection HAS_CXXFLAG_FSTACK_CLASH_PROTECTION) -if (HAS_CXXFLAG_FSTACK_CLASH_PROTECTION AND NOT (MINGW OR APPLE)) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstack-clash-protection") endif() include(CheckCompilerFlagsOutput) @@ -455,6 +527,11 @@ macro(CONFIGURE_WZ_COMPILER_WARNINGS) set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK YES) # -Warc-repeated-use-of-weak set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN__ARC_BRIDGE_CAST_NONARC YES) # -Warc-bridge-casts-disallowed-in-nonarc + elseif(CMAKE_SYSTEM_NAME MATCHES "Emscripten") + + list(APPEND WZ_TARGET_ADDITIONAL_C_BUILD_FLAGS -fno-common -fno-math-errno -fno-rounding-math -ffp-model=precise) + list(APPEND WZ_TARGET_ADDITIONAL_CXX_BUILD_FLAGS -fno-common -fno-math-errno -fno-rounding-math -ffp-model=precise) + else() # GCC, Clang, etc # Comments are provided next to each warning option detailing expected compiler support (from GCC 3.4+, Clang 3.2+ - earlier versions may / may not support these options) @@ -735,8 +812,14 @@ CHECK_FUNCTION_EXISTS(gettext HAVE_GETTEXT) CHECK_FUNCTION_EXISTS(iconv HAVE_ICONV) CHECK_CXX_SYMBOL_EXISTS(strlcat "string.h" HAVE_SYSTEM_STRLCAT) CHECK_CXX_SYMBOL_EXISTS(strlcpy "string.h" HAVE_SYSTEM_STRLCPY) -CHECK_CXX_SYMBOL_EXISTS(strlcat "string.h" HAVE_VALID_STRLCAT) -CHECK_CXX_SYMBOL_EXISTS(strlcpy "string.h" HAVE_VALID_STRLCPY) +if (NOT EMSCRIPTEN) + CHECK_CXX_SYMBOL_EXISTS(strlcpy "string.h" HAVE_VALID_STRLCPY) + CHECK_CXX_SYMBOL_EXISTS(strlcat "string.h" HAVE_VALID_STRLCAT) +else() + # Emscripten's implementation currently leads to ASAN errors, so disable the built-in and use WZ's custom backup implementation + set(HAVE_VALID_STRLCPY OFF CACHE BOOL "Have valid strlcpy" FORCE) + set(HAVE_VALID_STRLCAT OFF CACHE BOOL "Have valid strlcat" FORCE) +endif() CHECK_CXX_SYMBOL_EXISTS(putenv "stdlib.h" HAVE_PUTENV) CHECK_CXX_SYMBOL_EXISTS(setenv "stdlib.h" HAVE_SETENV) CHECK_CXX_SYMBOL_EXISTS(posix_spawn "spawn.h" HAVE_POSIX_SPAWN) @@ -760,7 +843,6 @@ else() endif() set(WZ_BINDIR "${CMAKE_INSTALL_BINDIR}") -set(WZ_LOCALEDIR "${CMAKE_INSTALL_LOCALEDIR}") message(STATUS "WZ_BINDIR=\"${WZ_BINDIR}\"") message(STATUS "WZ_LOCALEDIR=\"${WZ_LOCALEDIR}\"") function(CHECK_IS_ABSOLUTE_PATH _var _output) diff --git a/cmake/EmscriptenCompressZip.cmake b/cmake/EmscriptenCompressZip.cmake new file mode 100644 index 00000000000..f4bd03b14c9 --- /dev/null +++ b/cmake/EmscriptenCompressZip.cmake @@ -0,0 +1,155 @@ +# +# Provides a function COMPRESS_ZIP that is compatible with the function in FindZIP.cmake, but just copies the files to a folder +# +# +# Copyright © 2018-2024 pastdue ( https://github.com/past-due/ ) and contributors +# License: MIT License ( https://opensource.org/licenses/MIT ) +# +# Script Version: 2024-01-26a +# + +cmake_minimum_required(VERSION 3.5...3.24) + +set(_THIS_MODULE_BASE_DIR "${CMAKE_CURRENT_LIST_DIR}") + +# COMPRESS_ZIP(OUTPUT outputFile +# [COMPRESSION_LEVEL <0 | 1 | 3 | 5 | 7 | 9>] +# PATHS [files...] [WORKING_DIRECTORY dir] +# [PATHS [files...] [WORKING_DIRECTORY dir]] +# [DEPENDS [depends...]] +# [BUILD_ALWAYS_TARGET [target name]] +# [IGNORE_GIT] +# [QUIET]) +# +# Compress a list of files / folders into a ZIP file, named . +# Any directories specified will cause the directory's contents to be recursively included. +# +# If COMPRESSION_LEVEL is specified, the ZIP compression level setting will be passed +# through to the ZIP_EXECUTABLE. A compression level of "0" means no compression. +# +# If WORKING_DIRECTORY is specified, the WORKING_DIRECTORY will be set for the execution of +# the ZIP_EXECUTABLE. +# +# Each set of PATHS may also optionally specify an associated WORKING_DIRECTORY. +# +# DEPENDS may be used to specify additional dependencies, which are appended to the +# auto-generated list of dependencies used for the internal call to `add_custom_command`. +# +# If BUILD_ALWAYS_TARGET is specified, uses add_custom_target to create a target that is always built. +# +# QUIET attempts to suppress (most) output from the ZIP_EXECUTABLE that is used. +# (This option may have no effect, if unsupported by the ZIP_EXECUTABLE.) +# +function(COMPRESS_ZIP) + + set(_options ALL IGNORE_GIT QUIET) + set(_oneValueArgs OUTPUT COMPRESSION_LEVEL BUILD_ALWAYS_TARGET) #WORKING_DIRECTORY) + set(_multiValueArgs DEPENDS) + + CMAKE_PARSE_ARGUMENTS(_parsedArguments "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN}) + + # Check that OUTPUT was provided + if(NOT _parsedArguments_OUTPUT) + message( FATAL_ERROR "Missing required OUTPUT parameter" ) + endif() + + # Check arguments "unparsed" by CMAKE_PARSE_ARGUMENTS for PATHS sets + set(_COMMAND_LIST) + set(_depends_PATHS) + set(_inPATHSet FALSE) + set(_expecting_WORKINGDIR FALSE) + unset(_currentPATHSet_PATHS) + unset(_currentPATHSet_WORKINGDIR) + foreach(currentArg ${_parsedArguments_UNPARSED_ARGUMENTS}) + if("${currentArg}" STREQUAL "PATHS") + if(_expecting_WORKINGDIR) + # Provided "WORKING_DIRECTORY" keyword, but no variable after it + message( FATAL_ERROR "WORKING_DIRECTORY keyword provided, but missing variable afterwards" ) + endif() + if(_inPATHSet AND DEFINED _currentPATHSet_PATHS) + # Ending one non-empty PATH set, beginning another + if(NOT DEFINED _currentPATHSet_WORKINGDIR) + set(_currentPATHSet_WORKINGDIR "${CMAKE_CURRENT_SOURCE_DIR}") + endif() + foreach (_path ${_currentPATHSet_PATHS}) + list(APPEND _COMMAND_LIST + COMMAND + ${CMAKE_COMMAND} -E chdir ${_currentPATHSet_WORKINGDIR} + ${CMAKE_COMMAND} -DSOURCE=${_path} -DDEST_DIR=${_parsedArguments_OUTPUT} -P ${_THIS_MODULE_BASE_DIR}/EmscriptenCompressZipCopy.cmake + ) + set(_dependPath "${_currentPATHSet_WORKINGDIR}/${_path}") +# list(APPEND _COMMAND_LIST +# COMMAND +# ${CMAKE_COMMAND} -DSOURCE=${_dependPath} -DDEST_DIR=${_parsedArguments_OUTPUT} -P ${_THIS_MODULE_BASE_DIR}/EmscriptenCompressZipCopy.cmake +# ) + list(APPEND _depends_PATHS "${_dependPath}") + endforeach() + endif() + set(_inPATHSet TRUE) + unset(_currentPATHSet_PATHS) + unset(_currentPATHSet_WORKINGDIR) + elseif("${currentArg}" STREQUAL "WORKING_DIRECTORY") + if(NOT _inPATHSet) + message( FATAL_ERROR "WORKING_DIRECTORY must be specified at end of PATHS set" ) + endif() + if(_expecting_WORKINGDIR) + message( FATAL_ERROR "Duplicate WORKING_DIRECTORY keyword" ) + endif() + if(DEFINED _currentPATHSet_WORKINGDIR) + message( FATAL_ERROR "PATHS set has more than one WORKING_DIRECTORY keyword" ) + endif() + set(_expecting_WORKINGDIR TRUE) + elseif(_expecting_WORKINGDIR) + set(_currentPATHSet_WORKINGDIR "${currentArg}") + set(_expecting_WORKINGDIR FALSE) + elseif(_inPATHSet) + # Treat argument as a PATH + list(APPEND _currentPATHSet_PATHS "${currentArg}") + else() + # Unexpected argument + message( FATAL_ERROR "Unexpected argument: ${currentArg}" ) + endif() + endforeach() + if(_expecting_WORKINGDIR) + # Provided "WORKING_DIRECTORY" keyword, but no variable after it + message( FATAL_ERROR "WORKING_DIRECTORY keyword provided, but missing variable afterwards" ) + endif() + if(_inPATHSet AND DEFINED _currentPATHSet_PATHS) + # Ending one non-empty PATH set + if(NOT DEFINED _currentPATHSet_WORKINGDIR) + set(_currentPATHSet_WORKINGDIR "${CMAKE_CURRENT_SOURCE_DIR}") + endif() + foreach (_path ${_currentPATHSet_PATHS}) + set(_dependPath "${_currentPATHSet_WORKINGDIR}/${_path}") + list(APPEND _COMMAND_LIST + COMMAND + ${CMAKE_COMMAND} -E chdir ${_currentPATHSet_WORKINGDIR} + ${CMAKE_COMMAND} -DSOURCE=${_path} -DDEST_DIR=${_parsedArguments_OUTPUT} -P ${_THIS_MODULE_BASE_DIR}/EmscriptenCompressZipCopy.cmake + ) + list(APPEND _depends_PATHS "${_dependPath}") + endforeach() + endif() + + if(_parsedArguments_DEPENDS) + list(APPEND _depends_PATHS ${_parsedArguments_DEPENDS}) + endif() + + if(NOT _parsedArguments_BUILD_ALWAYS_TARGET) + add_custom_command( + OUTPUT "${_parsedArguments_OUTPUT}" + ${_COMMAND_LIST} + DEPENDS ${_depends_PATHS} + WORKING_DIRECTORY "${_workingDirectory}" + VERBATIM + ) + else() + add_custom_target( + ${_parsedArguments_BUILD_ALWAYS_TARGET} ALL + ${_COMMAND_LIST} + DEPENDS ${_depends_PATHS} + WORKING_DIRECTORY "${_workingDirectory}" + VERBATIM + ) + endif() + +endfunction() diff --git a/cmake/EmscriptenCompressZipCopy.cmake b/cmake/EmscriptenCompressZipCopy.cmake new file mode 100644 index 00000000000..72556ec46d0 --- /dev/null +++ b/cmake/EmscriptenCompressZipCopy.cmake @@ -0,0 +1,11 @@ +# SOURCE denotes the file or directory to copy. +# DEST_DIR denotes the directory for copy to. +if(IS_DIRECTORY "${SOURCE}") + set(SOURCE "${SOURCE}/") + set(_dest_subdir "${SOURCE}") +else() + get_filename_component(_dest_subdir "${SOURCE}" DIRECTORY) +endif() +file(COPY "${SOURCE}" DESTINATION "${DEST_DIR}/${_dest_subdir}" + PATTERN ".git*" EXCLUDE +) diff --git a/cmake/FindNPM.cmake b/cmake/FindNPM.cmake new file mode 100644 index 00000000000..7aadab61b70 --- /dev/null +++ b/cmake/FindNPM.cmake @@ -0,0 +1,63 @@ +# FindNPM +# -------- +# +# This module finds an installed npm. +# It sets the following variables: +# +# NPM_FOUND - True when NPM is found +# NPM_GLOBAL_PREFIX_DIR - The global prefix directory +# NPM_EXECUTABLE - The path to the npm executable +# NPM_VERSION - The version number of the npm executable + +find_program(NPM_EXECUTABLE NAMES npm HINTS /usr) + +if (NPM_EXECUTABLE) + + # Get the global npm prefix + execute_process(COMMAND ${NPM_EXECUTABLE} prefix -g + OUTPUT_VARIABLE NPM_GLOBAL_PREFIX_DIR + ERROR_VARIABLE NPM_prefix_g_error + RESULT_VARIABLE NPM_prefix_g_result_code + ) + # Remove spaces and newlines + string (STRIP ${NPM_GLOBAL_PREFIX_DIR} NPM_GLOBAL_PREFIX_DIR) + if (NPM_prefix_g_result_code) + if(NPM_FIND_REQUIRED) + message(SEND_ERROR "Command \"${NPM_EXECUTABLE} prefix -g\" failed with output:\n${NPM_prefix_g_error}") + else() + message(STATUS "Command \"${NPM_EXECUTABLE} prefix -g\" failed with output:\n${NPM_prefix_g_error}") + endif() + endif() + unset(NPM_prefix_g_error) + unset(NPM_prefix_g_result_code) + + # Get the VERSION + execute_process(COMMAND ${NPM_EXECUTABLE} -v + OUTPUT_VARIABLE NPM_VERSION + ERROR_VARIABLE NPM_version_error + RESULT_VARIABLE NPM_version_result_code + ) + if(NPM_version_result_code) + if(NPM_FIND_REQUIRED) + message(SEND_ERROR "Command \"${NPM_EXECUTABLE} -v\" failed with output:\n${NPM_version_error}") + else() + message(STATUS "Command \"${NPM_EXECUTABLE} -v\" failed with output:\n${NPM_version_error}") + endif() + endif() + unset(NPM_version_error) + unset(NPM_version_result_code) + + # Remove spaces and newlines + string (STRIP ${NPM_VERSION} NPM_VERSION) +else() + if (NPM_FIND_REQUIRED) + message(SEND_ERROR "Failed to find npm executable") + endif() +endif() + +find_package_handle_standard_args(NPM + REQUIRED_VARS NPM_EXECUTABLE NPM_GLOBAL_PREFIX_DIR + VERSION_VAR NPM_VERSION +) + +mark_as_advanced(NPM_GLOBAL_PREFIX_DIR NPM_EXECUTABLE NPM_VERSION) diff --git a/cmake/FindWorkboxCLI.cmake b/cmake/FindWorkboxCLI.cmake new file mode 100644 index 00000000000..1ec407f1f21 --- /dev/null +++ b/cmake/FindWorkboxCLI.cmake @@ -0,0 +1,38 @@ +# FindWorkboxCLI +# -------------- +# +# This module finds a globally-installed workbox-cli. +# It sets the following variables: +# +# WorkboxCLI_FOUND - True when workbox-cli is found +# WorkboxCLI_COMMAND - The command used to execute workbox-cli + +find_package(NPM REQUIRED) + +# Check for workbox-cli (global install) +execute_process(COMMAND + ${CMAKE_COMMAND} -E env NPM_CONFIG_PREFIX=${NPM_GLOBAL_PREFIX_DIR} ${NPM_EXECUTABLE} list -g workbox-cli + OUTPUT_VARIABLE NPM_LIST_GLOBAL_WORKBOX_CLI_output + ERROR_VARIABLE NPM_LIST_GLOBAL_WORKBOX_CLI_error + RESULT_VARIABLE NPM_LIST_GLOBAL_WORKBOX_CLI_result_code +) + +if (NPM_LIST_GLOBAL_WORKBOX_CLI_result_code EQUAL 0) + set(WorkboxCLI_COMMAND "${CMAKE_COMMAND}" -E env "NPM_CONFIG_PREFIX=${NPM_GLOBAL_PREFIX_DIR}" "${NPM_EXECUTABLE}" exec -no -- workbox-cli) +else() + if(WorkboxCLI_FIND_REQUIRED) + message(SEND_ERROR "Command \"${NPM_EXECUTABLE} list -g workbox-cli\" failed with output:\n${NPM_LIST_GLOBAL_WORKBOX_CLI_error}") + else() + message(STATUS "Command \"${NPM_EXECUTABLE} list -g workbox-cli\" failed with output:\n${NPM_LIST_GLOBAL_WORKBOX_CLI_error}") + endif() +endif() + +unset(NPM_LIST_GLOBAL_WORKBOX_CLI_output) +unset(NPM_LIST_GLOBAL_WORKBOX_CLI_error) +unset(NPM_LIST_GLOBAL_WORKBOX_CLI_result_code) + +find_package_handle_standard_args(WorkboxCLI + REQUIRED_VARS WorkboxCLI_COMMAND +) + +mark_as_advanced(NPM_GLOBAL_PREFIX_DIR NPM_EXECUTABLE NPM_VERSION) diff --git a/cmake/WZVcpkgInit.cmake b/cmake/WZVcpkgInit.cmake index 1be0867bcd6..efdb6ae5420 100644 --- a/cmake/WZVcpkgInit.cmake +++ b/cmake/WZVcpkgInit.cmake @@ -20,6 +20,11 @@ if(NOT DEFINED VCPKG_OVERLAY_TRIPLETS) list(APPEND VCPKG_OVERLAY_TRIPLETS "${_build_dir_overlay_triplets}") endif() unset(_build_dir_overlay_triplets) + set(_ci_dir_overlay_triplets "${CMAKE_CURRENT_SOURCE_DIR}/.ci/vcpkg/overlay-triplets") + if(EXISTS "${_ci_dir_overlay_triplets}" AND IS_DIRECTORY "${_ci_dir_overlay_triplets}") + list(APPEND VCPKG_OVERLAY_TRIPLETS "${_ci_dir_overlay_triplets}") + endif() + unset(_ci_dir_overlay_triplets) if(DEFINED VCPKG_OVERLAY_TRIPLETS) set(VCPKG_OVERLAY_TRIPLETS "${VCPKG_OVERLAY_TRIPLETS}" CACHE STRING "") endif() @@ -37,3 +42,9 @@ if(NOT DEFINED VCPKG_OVERLAY_PORTS OR VCPKG_OVERLAY_PORTS STREQUAL "") set(VCPKG_OVERLAY_PORTS "${VCPKG_OVERLAY_PORTS}" CACHE STRING "") endif() endif() + +if(VCPKG_TARGET_TRIPLET MATCHES "wasm32-emscripten") + if(NOT DEFINED VCPKG_CHAINLOAD_TOOLCHAIN_FILE) + set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/.ci/emscripten/toolchain/Toolchain-Emscripten.cmake") + endif() +endif() diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index 4d8fb935522..411925483fe 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -8,7 +8,12 @@ endif() OPTION(WZ_INCLUDE_TERRAIN_HIGH "Include high terrain texture pack" ON) OPTION(WZ_DOWNLOAD_PREBUILT_PACKAGES "Download prebuilt texture packages (if OFF, will generate from scratch - this can take a while to encode textures)" ON) -find_package(ZIP REQUIRED) +if(CMAKE_SYSTEM_NAME MATCHES "Emscripten") + # "Fake" COMPRESS_ZIP that just stages the needed files in folders + include(EmscriptenCompressZip) +else() + find_package(ZIP REQUIRED) +endif() ########################### # Prebuilt package DL info @@ -221,7 +226,45 @@ if(WZ_ENABLE_BASIS_UNIVERSAL AND NOT WZ_CI_DISABLE_BASIS_COMPRESS_TEXTURES) endif() WZ_BASIS_ENCODE_TEXTURES(OUTPUT_DIR "${_output_dir}" TYPE "TEXTURE" RESIZE "${_terrain_max_size}" RDO ENCODING_TARGET texture_encoding TARGET_FOLDER data ALL FILES ${TEXPAGES_TERRAIN}) - set(PROCESSED_TEXTURE_FILES ${TEXPAGES_TERRAIN}) + set(PROCESSED_TEXTURE_FILES "${TEXPAGES_TERRAIN}") + + if(CMAKE_SYSTEM_NAME MATCHES "Emscripten") + # Additional Normal Texpages (that are 1024x1024) + file(GLOB TEXPAGES_BASE_1024 + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-8-player-buildings-bases-rockies.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-8-player-buildings-bases-urban.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-8-player-buildings-bases.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-9-player-buildings-bases-rockies.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-9-player-buildings-bases-urban.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-9-player-buildings-bases.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-11-player-buildings.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-12-player-buildings.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-13-player-buildings.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-29-features-arizona.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-30-features-rockies.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-31-features-urban.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-34-buildings.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-111-player-buildings-nexus-bases.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-112-player-buildings-nexus.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-114-player-buildings_Collective.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-116-player-buildings_nex.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-117-player-buildings-bases_nex.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-120-player-buildings-bases_Collective.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-121-player-buildings_Collective.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-122-player-buildings_Collective.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/page-123-laboratories_Collective.png" + ) + + set(_output_dir "${CMAKE_CURRENT_BINARY_DIR}/base/texpages") + file(MAKE_DIRECTORY "${_output_dir}") + set(_texpage_max_size 1024) + if(CMAKE_SYSTEM_NAME MATCHES "Emscripten") + set(_texpage_max_size 512) + endif() + + WZ_BASIS_ENCODE_TEXTURES(OUTPUT_DIR "${_output_dir}" TYPE "TEXTURE" RESIZE "${_texpage_max_size}" RDO ENCODING_TARGET texture_encoding TARGET_FOLDER data ALL FILES ${TEXPAGES_BASE_1024}) + list(APPEND PROCESSED_TEXTURE_FILES "${TEXPAGES_BASE_1024}") + endif() # Backdrops file(GLOB TEXPAGES_BACKDROPS_ALL @@ -231,6 +274,7 @@ if(WZ_ENABLE_BASIS_UNIVERSAL AND NOT WZ_CI_DISABLE_BASIS_COMPRESS_TEXTURES) # limit backdrops included on Emscripten (to save space) file(GLOB TEXPAGES_BACKDROPS "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/bdrops/backdrop0.png" + "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/bdrops/missionend.png" ) else() set(TEXPAGES_BACKDROPS ${TEXPAGES_BACKDROPS_ALL}) @@ -245,6 +289,17 @@ if(WZ_ENABLE_BASIS_UNIVERSAL AND NOT WZ_CI_DISABLE_BASIS_COMPRESS_TEXTURES) WZ_BASIS_ENCODE_TEXTURES(OUTPUT_DIR "${_output_dir}" TYPE "UITEXTURE" RESIZE "${_bdrop_max_size}" RDO ENCODING_TARGET texture_encoding TARGET_FOLDER data ALL FILES ${TEXPAGES_BACKDROPS}) + # Excluding pre-generated decal mips + file(GLOB DECALS_128 LIST_DIRECTORIES false CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/tertilesc*hw-128/*.png") + if (DECALS_128) + file(GLOB DECALS_ALL LIST_DIRECTORIES false CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/tertilesc*hw-*/*.png") + set(DECALS_skipped "${DECALS_ALL}") + unset(DECALS_ALL) + foreach(high_qual_decal IN LISTS DECALS_128) + list(REMOVE_ITEM DECALS_skipped "${high_qual_decal}") + endforeach() + endif() + # The rest of the texture files set(_output_dir "${CMAKE_CURRENT_BINARY_DIR}/base/texpages") file(GLOB_RECURSE ALL_TEXPAGES LIST_DIRECTORIES false CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/*.png" "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/compression_overrides.txt" "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages/*.radar") @@ -252,6 +307,7 @@ if(WZ_ENABLE_BASIS_UNIVERSAL AND NOT WZ_CI_DISABLE_BASIS_COMPRESS_TEXTURES) list(APPEND ALL_TEXPAGES_unprocessed ${ALL_TEXPAGES}) list(REMOVE_ITEM ALL_TEXPAGES_unprocessed ${PROCESSED_TEXTURE_FILES}) list(REMOVE_ITEM ALL_TEXPAGES_unprocessed ${TEXPAGES_BACKDROPS_ALL}) + list(REMOVE_ITEM ALL_TEXPAGES_unprocessed ${DECALS_skipped}) foreach(TEXPAGE_FILE ${ALL_TEXPAGES_unprocessed}) file(RELATIVE_PATH _output_name "${CMAKE_CURRENT_SOURCE_DIR}/base/texpages" "${TEXPAGE_FILE}") message(STATUS "Copy unprocessed image file: ${_output_name}") @@ -562,10 +618,6 @@ set(DATA_FILES if(WZ_INCLUDE_VIDEOS) list(APPEND DATA_FILES "${CMAKE_CURRENT_BINARY_DIR}/sequences.wz") endif() -install(FILES ${DATA_FILES} - DESTINATION "${WZ_DATADIR}" - COMPONENT Data -) set(DATA_TERRAIN_OVERRIDES_FILES "${CMAKE_CURRENT_BINARY_DIR}/terrain_overrides/classic.wz" @@ -573,25 +625,78 @@ set(DATA_TERRAIN_OVERRIDES_FILES if(TARGET data_terrain_overrides_high OR EXISTS "${CMAKE_CURRENT_BINARY_DIR}/terrain_overrides/high.wz") list(APPEND DATA_TERRAIN_OVERRIDES_FILES "${CMAKE_CURRENT_BINARY_DIR}/terrain_overrides/high.wz") endif() -install(FILES ${DATA_TERRAIN_OVERRIDES_FILES} - DESTINATION "${WZ_DATADIR}/terrain_overrides" - COMPONENT Data -) -install(FILES - ${wz2100_fonts_FILES} - COMPONENT Data DESTINATION "${WZ_DATADIR}/fonts" -) +if(NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten") -file(GLOB DATA_MUSIC_FILES "${CMAKE_CURRENT_SOURCE_DIR}/music/*.opus" "${CMAKE_CURRENT_SOURCE_DIR}/music/albums/*/*.*") -foreach(_music_file ${DATA_MUSIC_FILES}) - file(RELATIVE_PATH _music_file_relative_path "${CMAKE_CURRENT_SOURCE_DIR}/music" "${_music_file}") - get_filename_component(_music_file_subdir_path "${_music_file_relative_path}" DIRECTORY) - install(FILES ${_music_file} - DESTINATION "${WZ_DATADIR}/music/${_music_file_subdir_path}" + install(FILES ${DATA_FILES} + DESTINATION "${WZ_DATADIR}" COMPONENT Data ) -endforeach() + install(FILES ${DATA_TERRAIN_OVERRIDES_FILES} + DESTINATION "${WZ_DATADIR}/terrain_overrides" + COMPONENT Data + ) + install(FILES + ${wz2100_fonts_FILES} + COMPONENT Data DESTINATION "${WZ_DATADIR}/fonts" + ) + + file(GLOB DATA_MUSIC_FILES "${CMAKE_CURRENT_SOURCE_DIR}/music/*.opus" "${CMAKE_CURRENT_SOURCE_DIR}/music/albums/*/*.*") + foreach(_music_file ${DATA_MUSIC_FILES}) + file(RELATIVE_PATH _music_file_relative_path "${CMAKE_CURRENT_SOURCE_DIR}/music" "${_music_file}") + get_filename_component(_music_file_subdir_path "${_music_file_relative_path}" DIRECTORY) + install(FILES ${_music_file} + DESTINATION "${WZ_DATADIR}/music/${_music_file_subdir_path}" + COMPONENT Data + ) + endforeach() + +else() + # Emscripten: + + if (NOT DEFINED EMSCRIPTEN_ROOT_PATH OR NOT EMSCRIPTEN_ROOT_PATH) + message(WARNING "Invalid EMSCRIPTEN_ROOT_PATH? (=${EMSCRIPTEN_ROOT_PATH})") + endif() + find_file(EMSCRIPTEN_FILE_PACKAGER_PY NAMES "file_packager.py" PATHS "${EMSCRIPTEN_ROOT_PATH}/tools" NO_CMAKE_FIND_ROOT_PATH) + if (NOT EMSCRIPTEN_FILE_PACKAGER_PY) + message(FATAL_ERROR "Unable to find Emscripten file_packager.py") + endif() + find_package (Python3 COMPONENTS Interpreter REQUIRED) + + # Bundle the classic terrain into a separate package + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/empackaged/terrain_overrides") + add_custom_target( + data_terrain_overrides_classic_packaging ALL + ${Python3_EXECUTABLE} "${EMSCRIPTEN_FILE_PACKAGER_PY}" warzone2100-terrain-classic.data --preload "${CMAKE_CURRENT_BINARY_DIR}/terrain_overrides/classic.wz@/data/terrain_overrides/classic/" --js-output=warzone2100-terrain-classic.js --use-preload-cache --indexedDB-name=EM_PRELOAD_TERRAIN_CLASSIC_CACHE --no-node + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/empackaged/terrain_overrides" + DEPENDS data_terrain_overrides_classic + VERBATIM + ) + list(APPEND DATA_ADDITIONAL_EMPACKAGE_DIRS "${CMAKE_CURRENT_BINARY_DIR}/empackaged/terrain_overrides") + + # Bundle the original music into a separate package + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/music_staging") + file(GLOB DATA_ORIG_MUSIC_FILES "${CMAKE_CURRENT_SOURCE_DIR}/music/*.opus" "${CMAKE_CURRENT_SOURCE_DIR}/music/albums/original_soundtrack/*.*") + foreach(_music_file ${DATA_ORIG_MUSIC_FILES}) + file(RELATIVE_PATH _music_file_relative_path "${CMAKE_CURRENT_SOURCE_DIR}/music" "${_music_file}") + get_filename_component(_music_file_subdir_path "${_music_file_relative_path}" DIRECTORY) + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/music_staging/${_music_file_subdir_path}") + configure_file("${_music_file}" "${CMAKE_CURRENT_BINARY_DIR}/music_staging/${_music_file_subdir_path}" COPYONLY) + endforeach() + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/empackaged/music") + add_custom_target( + data_music_empackage ALL + ${Python3_EXECUTABLE} "${EMSCRIPTEN_FILE_PACKAGER_PY}" warzone2100-music.data --preload "${CMAKE_CURRENT_BINARY_DIR}/music_staging@/data/music/" --js-output=warzone2100-music.js --use-preload-cache --indexedDB-name=EM_PRELOAD_MUSIC_CACHE --no-node + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/empackaged/music" + VERBATIM + ) + list(APPEND DATA_ADDITIONAL_EMPACKAGE_DIRS "${CMAKE_CURRENT_BINARY_DIR}/empackaged/music") + + set(DATA_ADDITIONAL_EMPACKAGE_DIRS ${DATA_ADDITIONAL_EMPACKAGE_DIRS} PARENT_SCOPE) + set(DATA_ADDITIONAL_EMPACKAGE_BASEDIR "${CMAKE_CURRENT_BINARY_DIR}/empackaged" PARENT_SCOPE) + + +endif() set(DATA_FILES ${DATA_FILES} PARENT_SCOPE) set(DATA_TERRAIN_OVERRIDES_FILES ${DATA_TERRAIN_OVERRIDES_FILES} PARENT_SCOPE) diff --git a/data/base/shaders/gfx_text.vert b/data/base/shaders/gfx_text.vert index 0cfca770767..4a556ec7aa1 100644 --- a/data/base/shaders/gfx_text.vert +++ b/data/base/shaders/gfx_text.vert @@ -6,11 +6,9 @@ uniform mat4 posMatrix; #if (!defined(GL_ES) && (__VERSION__ >= 130)) || (defined(GL_ES) && (__VERSION__ >= 300)) in vec4 vertex; in vec2 vertexTexCoord; -in vec4 vertexColor; #else attribute vec4 vertex; attribute vec2 vertexTexCoord; -attribute vec4 vertexColor; #endif #if (!defined(GL_ES) && (__VERSION__ >= 130)) || (defined(GL_ES) && (__VERSION__ >= 300)) diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index d5a381a33ce..caf742ba5ee 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -250,7 +250,7 @@ install(FILES ${wz2100_doc_FILES} DESTINATION "${CMAKE_INSTALL_DOCDIR}${WZ_OUTPU install(FILES ${host_doc_FILES} DESTINATION "${CMAKE_INSTALL_DOCDIR}${WZ_OUTPUT_NAME_SUFFIX}/hosting" COMPONENT Docs) install(FILES ${host_doc_linux_scripts_FILES} DESTINATION "${CMAKE_INSTALL_DOCDIR}${WZ_OUTPUT_NAME_SUFFIX}/hosting/linux_scripts" COMPONENT Docs) -if(UNIX AND NOT SKIPPED_DOC_GENERATION) +if(UNIX AND NOT EMSCRIPTEN AND NOT SKIPPED_DOC_GENERATION) # Man-page gzip and installation find_program(GZIP_BIN NAMES gzip PATHS /bin /usr/bin /usr/local/bin) diff --git a/icons/CMakeLists.txt b/icons/CMakeLists.txt index 401df561b41..e46694a4d32 100644 --- a/icons/CMakeLists.txt +++ b/icons/CMakeLists.txt @@ -1,4 +1,4 @@ -if(UNIX AND NOT APPLE AND NOT WIN32) +if(UNIX AND NOT APPLE AND NOT WIN32 AND NOT EMSCRIPTEN) include(GNUInstallDirs) if (NOT DEFINED WZ_APPSTREAM_ID) diff --git a/lib/exceptionhandler/exceptionhandler.cpp b/lib/exceptionhandler/exceptionhandler.cpp index 1b3e794f528..b681d9d52c8 100644 --- a/lib/exceptionhandler/exceptionhandler.cpp +++ b/lib/exceptionhandler/exceptionhandler.cpp @@ -784,7 +784,7 @@ void setupExceptionHandler(int argc, const char * const *argv, const char *packa #if defined(WZ_OS_WIN) ExchndlSetup(packageVersion, writeDir, portable_mode); -#elif defined(WZ_OS_UNIX) && !defined(WZ_OS_MAC) +#elif defined(WZ_OS_UNIX) && !defined(WZ_OS_MAC) && !defined(__EMSCRIPTEN__) programCommand = argv[0]; // Get full path to this program. Needed for gdb to find the binary. diff --git a/lib/framework/debug.cpp b/lib/framework/debug.cpp index 5db22d17d58..ddbfe9a0677 100644 --- a/lib/framework/debug.cpp +++ b/lib/framework/debug.cpp @@ -350,6 +350,14 @@ void debug_init() enabled_debug[LOG_INFO] = true; enabled_debug[LOG_FATAL] = true; enabled_debug[LOG_POPUP] = true; +#if defined(__EMSCRIPTEN__) + // start with certain options off so that we can control them predictably from the command-line options via the web interface + enabled_debug[LOG_INFO] = false; + enabled_debug[LOG_WARNING] = false; + enabled_debug[LOG_3D] = false; + // must be false or sound breaks (some openal edge case) + enabled_debug[LOG_SOUND] = false; +#endif #ifdef DEBUG enabled_debug[LOG_WARNING] = true; #endif @@ -803,3 +811,38 @@ void _debug_multiline(int line, code_part part, const char *function, const std: } } +#if defined(__EMSCRIPTEN__) + +#include + +/** + * Callback for outputting to a emscripten log / console + * + * \param data Ignored. Use NULL. + * \param outputBuffer Buffer containing the preprocessed text to output. + */ +void debug_callback_emscripten_log(WZ_DECL_UNUSED void **data, const char *outputBuffer, code_part part) +{ + int flags = EM_LOG_NO_PATHS | EM_LOG_CONSOLE; + switch (part) + { + case LOG_ERROR: + flags |= EM_LOG_ERROR; + break; + case LOG_WARNING: + flags |= EM_LOG_WARN; + break; + default: + break; + } + if (outputBuffer[strlen(outputBuffer) - 1] != '\n') + { + emscripten_log(flags, "%s\n", outputBuffer); + } + else + { + emscripten_log(flags, "%s", outputBuffer); + } +} + +#endif diff --git a/lib/framework/debug.h b/lib/framework/debug.h index 7cfbe73012a..5a75c4873e5 100644 --- a/lib/framework/debug.h +++ b/lib/framework/debug.h @@ -265,6 +265,10 @@ void debug_callback_file_exit(void **data); void debug_callback_stderr(void **data, const char *outputBuffer, code_part part); +#if defined(__EMSCRIPTEN__) +void debug_callback_emscripten_log(void **data, const char *outputBuffer, code_part part); +#endif + #if defined(_WIN32) && defined(DEBUG) void debug_callback_win32debug(void **data, const char *outputBuffer, code_part part); #endif diff --git a/lib/framework/i18n.cpp b/lib/framework/i18n.cpp index f4a105457ad..79be814d2d8 100644 --- a/lib/framework/i18n.cpp +++ b/lib/framework/i18n.cpp @@ -17,6 +17,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "frame.h" +#include "string_ext.h" #include #include @@ -331,6 +332,16 @@ static bool setLocaleWindows(USHORT usPrimaryLanguage, USHORT usSubLanguage) return success; } +# elif defined(__EMSCRIPTEN__) +static bool setLocaleEmscripten(const char *localeFilename) +{ + setlocale(LC_ALL, localeFilename); + + const char * numericLocale = setlocale(LC_NUMERIC, "C"); // set radix character to the period (".") + debug(LOG_WZ, "LC_NUMERIC: \"%s\"", (numericLocale != nullptr) ? numericLocale : ""); + + return true; +} # else /*! * Set the prefered locale @@ -426,6 +437,8 @@ bool setLanguage(const char *language) # if defined(WZ_OS_WIN) return setLocaleWindows(map[i].usPrimaryLanguage, map[i].usSubLanguage); +# elif defined(__EMSCRIPTEN__) + return setLocaleEmscripten(map[i].localeFilename); # else return setLocaleUnix(map[i].locale) || setLocaleUnix(map[i].localeFallback) || setLocaleUnix_LANGUAGEFallback(map[i].localeFilename); # endif @@ -560,9 +573,53 @@ static bool checkSupportsLANGUAGEenvVarOverride() #endif // defined(_LIBINTL_H) && defined(LIBINTL_VERSION) } +#if defined(__EMSCRIPTEN__) +static std::string getEmscriptenDefaultLanguage() +{ + auto pEnvLang = getenv("LANG"); + if (pEnvLang) + { + std::string defaultLanguage = pEnvLang; + // Remove any .UTF-8 suffix + if (strEndsWith(defaultLanguage, ".UTF-8")) + { + defaultLanguage = defaultLanguage.substr(0, defaultLanguage.size() - 6); + } + // Find matching closest language (if present) + for (unsigned i = 0; i < ARRAY_SIZE(map); i++) + { + if (strcmp(defaultLanguage.c_str(), map[i].language) == 0) + { + return map[i].language; + } + } + for (unsigned i = 0; i < ARRAY_SIZE(map); i++) + { + if (strcmp(defaultLanguage.c_str(), map[i].localeFilename) == 0) + { + return map[i].language; + } + } + for (unsigned i = 0; i < ARRAY_SIZE(map); i++) + { + if (strcmp(defaultLanguage.c_str(), map[i].localeFallback) == 0) + { + return map[i].language; + } + } + } + return ""; +} +#endif + void initI18n() { std::string textdomainDirectory; + std::string defaultLanguage = ""; // "" is system default (in most cases) + +#if defined(__EMSCRIPTEN__) + defaultLanguage = getEmscriptenDefaultLanguage(); +#endif #if defined(_LIBINTL_H) && defined(LIBINTL_VERSION) int wz_libintl_maj = LIBINTL_VERSION >> 16; @@ -623,7 +680,7 @@ void initI18n() // Should come *after* bindTextDomain canUseLANGUAGEEnvVar = checkSupportsLANGUAGEenvVarOverride(); - if (!setLanguage("")) // set to system default + if (!setLanguage(defaultLanguage.c_str())) // set to system default { // no system default? debug(LOG_ERROR, "initI18n: No system language found"); diff --git a/lib/framework/wzglobal.h b/lib/framework/wzglobal.h index 9c7bfd463d2..ad320136c43 100644 --- a/lib/framework/wzglobal.h +++ b/lib/framework/wzglobal.h @@ -155,6 +155,7 @@ #elif defined(__INTEGRITY) # define WZ_OS_INTEGRITY #elif defined(__MAKEDEPEND__) +#elif defined(__EMSCRIPTEN__) #else # error "Warzone has not been tested on this OS. Please contact warzone2100-project@lists.sourceforge.net" #endif /* WZ_OS_x */ diff --git a/lib/ivis_opengl/CMakeLists.txt b/lib/ivis_opengl/CMakeLists.txt index 1b4fb550450..02ce6baea78 100644 --- a/lib/ivis_opengl/CMakeLists.txt +++ b/lib/ivis_opengl/CMakeLists.txt @@ -73,8 +73,12 @@ if(WZ_USE_SPNG) else() find_package(PNG 1.2 REQUIRED) endif() -find_package(Freetype REQUIRED) -find_package(Harfbuzz 1.0 REQUIRED) +if(CMAKE_SYSTEM_NAME MATCHES "Emscripten") + # We should be using the Emscripten port linker flags for FreeType & Harfbuzz +else() + find_package(Freetype REQUIRED) + find_package(Harfbuzz 1.0 REQUIRED) +endif() find_package(Fribidi) # recommended, but optional include(CheckCXXCompilerFlag) @@ -84,8 +88,13 @@ set_property(TARGET ivis-opengl PROPERTY FOLDER "lib") include(WZTargetConfiguration) WZ_TARGET_CONFIGURATION(ivis-opengl) -target_include_directories(ivis-opengl PRIVATE ${HARFBUZZ_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIR_ft2build}) -target_link_libraries(ivis-opengl PRIVATE framework launchinfo ${HARFBUZZ_LIBRARIES} ${FREETYPE_LIBRARIES}) +target_link_libraries(ivis-opengl PRIVATE framework launchinfo) +if(CMAKE_SYSTEM_NAME MATCHES "Emscripten") + # We should be using the Emscripten port linker flags for FreeType & Harfbuzz +else() + target_include_directories(ivis-opengl PRIVATE ${HARFBUZZ_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIR_ft2build}) + target_link_libraries(ivis-opengl PRIVATE ${HARFBUZZ_LIBRARIES} ${FREETYPE_LIBRARIES}) +endif() if(WZ_USE_SPNG) target_link_libraries(ivis-opengl PRIVATE $,spng::spng,spng::spng_static>) else() diff --git a/lib/ivis_opengl/gfx_api_gl.cpp b/lib/ivis_opengl/gfx_api_gl.cpp index 120fd5133e9..f75b971e907 100644 --- a/lib/ivis_opengl/gfx_api_gl.cpp +++ b/lib/ivis_opengl/gfx_api_gl.cpp @@ -50,6 +50,42 @@ # define WZ_GL_TIMER_QUERY_SUPPORTED #endif +#if defined(__EMSCRIPTEN__) +#include +# if defined(WZ_STATIC_GL_BINDINGS) +# include +# endif + +// forward-declarations +static std::unordered_set enabledWebGLExtensions; +static bool initWebGLExtensions(); + +static int GLAD_GL_ES_VERSION_3_0 = 0; +static int GLAD_GL_EXT_texture_filter_anisotropic = 0; + +#ifndef GL_COMPRESSED_RGB8_ETC2 +# define GL_COMPRESSED_RGB8_ETC2 0x9274 +#endif +#ifndef GL_COMPRESSED_RGBA8_ETC2_EAC +# define GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278 +#endif +#ifndef GL_COMPRESSED_R11_EAC +# define GL_COMPRESSED_R11_EAC 0x9270 +#endif +#ifndef GL_COMPRESSED_RG11_EAC +# define GL_COMPRESSED_RG11_EAC 0x9272 +#endif + +#ifndef GL_COMPRESSED_RGBA_ASTC_4x4_KHR +# define GL_COMPRESSED_RGBA_ASTC_4x4_KHR 0x93B0 +#endif + +#ifndef GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS +# define GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS 0x8DA8 +#endif + +#endif + struct OPENGL_DATA { char vendor[256] = {}; @@ -68,9 +104,15 @@ static bool perfStarted = false; static std::unordered_set debugLiveTextures; #endif +#if !defined(WZ_STATIC_GL_BINDINGS) PFNGLDRAWARRAYSINSTANCEDPROC wz_dyn_glDrawArraysInstanced = nullptr; PFNGLDRAWELEMENTSINSTANCEDPROC wz_dyn_glDrawElementsInstanced = nullptr; PFNGLVERTEXATTRIBDIVISORPROC wz_dyn_glVertexAttribDivisor = nullptr; +#else +#define wz_dyn_glDrawArraysInstanced glDrawArraysInstanced +#define wz_dyn_glDrawElementsInstanced glDrawElementsInstanced +#define wz_dyn_glVertexAttribDivisor glVertexAttribDivisor +#endif static const GLubyte* wzSafeGlGetString(GLenum name); @@ -86,17 +128,20 @@ static GLenum to_gl_internalformat(const gfx_api::pixel_format& format, bool gle case gfx_api::pixel_format::FORMAT_RGB8_UNORM_PACK8: return GL_RGB8; case gfx_api::pixel_format::FORMAT_RG8_UNORM: +#if !defined(__EMSCRIPTEN__) if (gles && GLAD_GL_EXT_texture_rg) { // the internal format is GL_RG_EXT return GL_RG_EXT; } else +#endif { - // for Desktop OpenGL, use GL_RG8 for the internal format + // for Desktop OpenGL (or WebGL 2.0), use GL_RG8 for the internal format return GL_RG8; } case gfx_api::pixel_format::FORMAT_R8_UNORM: +#if !defined(__EMSCRIPTEN__) if ((!gles && GLAD_GL_VERSION_3_0) || (gles && GLAD_GL_ES_VERSION_3_0)) { // OpenGL 3.0+ or OpenGL ES 3.0+ @@ -110,6 +155,10 @@ static GLenum to_gl_internalformat(const gfx_api::pixel_format& format, bool gle // (b) it ensures the single channel value ends up in "red" so the shaders don't have to care return GL_LUMINANCE; } +#else + // WebGL 2.0 + return GL_R8; +#endif // COMPRESSED FORMAT case gfx_api::pixel_format::FORMAT_RGB_BC1_UNORM: return GL_COMPRESSED_RGB_S3TC_DXT1_EXT; @@ -118,11 +167,19 @@ static GLenum to_gl_internalformat(const gfx_api::pixel_format& format, bool gle case gfx_api::pixel_format::FORMAT_RGBA_BC3_UNORM: return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; case gfx_api::pixel_format::FORMAT_R_BC4_UNORM: +#if defined(__EMSCRIPTEN__) && defined(GL_EXT_texture_compression_rgtc) + return GL_COMPRESSED_RED_RGTC1_EXT; +#else return GL_COMPRESSED_RED_RGTC1; +#endif case gfx_api::pixel_format::FORMAT_RG_BC5_UNORM: +#if defined(__EMSCRIPTEN__) && defined(GL_EXT_texture_compression_rgtc) + return GL_COMPRESSED_RED_GREEN_RGTC2_EXT; +#else return GL_COMPRESSED_RG_RGTC2; +#endif case gfx_api::pixel_format::FORMAT_RGBA_BPTC_UNORM: - return GL_COMPRESSED_RGBA_BPTC_UNORM_ARB; // same value as GL_COMPRESSED_RGBA_BPTC_UNORM_EXT + return GL_COMPRESSED_RGBA_BPTC_UNORM_EXT; // same value as GL_COMPRESSED_RGBA_BPTC_UNORM_ARB case gfx_api::pixel_format::FORMAT_RGB8_ETC1: return GL_ETC1_RGB8_OES; case gfx_api::pixel_format::FORMAT_RGB8_ETC2: @@ -149,21 +206,28 @@ static GLenum to_gl_format(const gfx_api::pixel_format& format, bool gles) case gfx_api::pixel_format::FORMAT_RGBA8_UNORM_PACK8: return GL_RGBA; case gfx_api::pixel_format::FORMAT_BGRA8_UNORM_PACK8: +#if defined(__EMSCRIPTEN__) + return GL_INVALID_ENUM; +#else return GL_BGRA; +#endif case gfx_api::pixel_format::FORMAT_RGB8_UNORM_PACK8: return GL_RGB; case gfx_api::pixel_format::FORMAT_RG8_UNORM: +#if !defined(__EMSCRIPTEN__) if (gles && GLAD_GL_EXT_texture_rg) { // the internal format is GL_RG_EXT return GL_RG_EXT; } else +#endif { - // for Desktop OpenGL, use GL_RG for the format + // for Desktop OpenGL or WebGL 2.0, use GL_RG for the format return GL_RG; } case gfx_api::pixel_format::FORMAT_R8_UNORM: +#if !defined(__EMSCRIPTEN__) if ((!gles && GLAD_GL_VERSION_3_0) || (gles && GLAD_GL_ES_VERSION_3_0)) { // OpenGL 3.0+ or OpenGL ES 3.0+ @@ -177,6 +241,10 @@ static GLenum to_gl_format(const gfx_api::pixel_format& format, bool gles) // (b) it ensures the single channel value ends up in "red" so the shaders don't have to care return GL_LUMINANCE; } +#else + // WebGL 2.0 + return GL_RED; +#endif // COMPRESSED FORMAT default: return to_gl_internalformat(format, gles); @@ -273,11 +341,13 @@ static GLenum to_gl(const gfx_api::context::context_value property) // NOTE: Not to be used in critical code paths, but more for initialization static bool _wzGLCheckErrors(int line, const char *function) { +#if !defined(WZ_STATIC_GL_BINDINGS) if (glGetError == nullptr) { // function not available? can't check... return true; } +#endif GLenum err = glGetError(); if (err == GL_NO_ERROR) { @@ -313,14 +383,18 @@ static bool _wzGLCheckErrors(int line, const char *function) // Once GL_OUT_OF_MEMORY is set, the state of the OpenGL context is *undefined* encounteredCriticalError = true; break; +#ifdef GL_STACK_UNDERFLOW case GL_STACK_UNDERFLOW: errAsStr = "GL_STACK_UNDERFLOW"; encounteredCriticalError = true; break; +#endif +#ifdef GL_STACK_OVERFLOW case GL_STACK_OVERFLOW: errAsStr = "GL_STACK_OVERFLOW"; encounteredCriticalError = true; break; +#endif } if (enabled_debug[part]) @@ -335,7 +409,9 @@ static bool _wzGLCheckErrors(int line, const char *function) static void _wzGLClearErrors() { // clear OpenGL error queue +#if !defined(WZ_STATIC_GL_BINDINGS) if (glGetError != nullptr) +#endif { while(glGetError() != GL_NO_ERROR) { } // clear the OpenGL error queue } @@ -909,14 +985,18 @@ const char * shaderVersionString(SHADER_VERSION_ES version) GLint wz_GetGLIntegerv(GLenum pname, GLint defaultValue = 0) { GLint retVal = defaultValue; +#if !defined(WZ_STATIC_GL_BINDINGS) ASSERT_OR_RETURN(retVal, glGetIntegerv != nullptr, "glGetIntegerv is null"); if (glGetError != nullptr) +#endif { while(glGetError() != GL_NO_ERROR) { } // clear the OpenGL error queue } glGetIntegerv(pname, &retVal); GLenum err = GL_NO_ERROR; +#if !defined(WZ_STATIC_GL_BINDINGS) if (glGetError != nullptr) +#endif { err = glGetError(); } @@ -1077,6 +1157,7 @@ desc(createInfo.state_desc), vertex_buffer_desc(createInfo.attribute_description const char *shaderVersionStr = shaderVersionString(getMaximumShaderVersionForCurrentGLESContext(VERSION_ES_100, VERSION_ES_300)); vertexShaderHeader = shaderVersionStr; + fragmentShaderHeader = shaderVersionStr; // OpenGL ES Shading Language - 4. Variables and Types - pp. 35-36 // https://www.khronos.org/registry/gles/specs/2.0/GLSL_ES_Specification_1.0.17.pdf?#page=41 // @@ -1084,7 +1165,12 @@ desc(createInfo.state_desc), vertex_buffer_desc(createInfo.attribute_description // > Hence for float, floating point vector and matrix variable declarations, either the // > declaration must include a precision qualifier or the default float precision must // > have been previously declared. - fragmentShaderHeader = std::string(shaderVersionStr) + "#if GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\nprecision highp int;\n#else\nprecision mediump float;\n#endif\n"; +#if defined(__EMSCRIPTEN__) + vertexShaderHeader += "precision highp float;\n"; + fragmentShaderHeader += "precision highp float;precision highp int;\n"; +#else + fragmentShaderHeader = "#if GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\nprecision highp int;\n#else\nprecision mediump float;\n#endif\n"; +#endif fragmentShaderHeader += "#if __VERSION__ >= 300 || defined(GL_EXT_texture_array)\nprecision lowp sampler2DArray;\n#endif\n"; fragmentShaderHeader += "#if __VERSION__ >= 300\nprecision lowp sampler2DShadow;\nprecision lowp sampler2DArrayShadow;\n#endif\n"; } @@ -2557,8 +2643,10 @@ void gl_context::bind_vertex_buffers(const std::size_t& first, const std::vector if (get_type(attribute.type) == GL_INT || attribute.type == gfx_api::vertex_attribute_type::u8x4_uint) { - // glVertexAttribIPointer only supported in: OpenGL 3.0+, OpenGL ES 3.0+ - ASSERT(glVertexAttribIPointer != nullptr, "Missing glVertexAttribIPointer?"); + #if !defined(WZ_STATIC_GL_BINDINGS) + // glVertexAttribIPointer only supported in: OpenGL 3.0+, OpenGL ES 3.0+ + ASSERT(glVertexAttribIPointer != nullptr, "Missing glVertexAttribIPointer?"); + #endif glVertexAttribIPointer(static_cast(attribute.id), get_size(attribute.type), get_type(attribute.type), static_cast(buffer_desc.stride), reinterpret_cast(attribute.offset + std::get<1>(vertex_buffers_offset[i]))); } else @@ -2714,9 +2802,13 @@ void gl_context::bind_textures(const std::vector& textur case gfx_api::sampler_type::nearest_border: glTexParameteri(type, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(type, GL_TEXTURE_MAG_FILTER, GL_NEAREST); +#if !defined(__EMSCRIPTEN__) glTexParameteri(type, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(type, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, to_gl(desc.border)); +#else + // POSSIBLE FIXME: Emulate GL_CLAMP_TO_BORDER for WebGL? +#endif break; case gfx_api::sampler_type::bilinear: glTexParameteri(type, GL_TEXTURE_MIN_FILTER, GL_LINEAR); @@ -2733,9 +2825,13 @@ void gl_context::bind_textures(const std::vector& textur case gfx_api::sampler_type::bilinear_border: glTexParameteri(type, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(type, GL_TEXTURE_MAG_FILTER, GL_LINEAR); +#if !defined(__EMSCRIPTEN__) glTexParameteri(type, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(type, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, to_gl(desc.border)); +#else + // POSSIBLE FIXME: Emulate GL_CLAMP_TO_BORDER for WebGL? +#endif break; case gfx_api::sampler_type::anisotropic_repeat: glTexParameteri(type, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); @@ -2811,11 +2907,13 @@ void gl_context::set_polygon_offset(const float& offset, const float& slope) void gl_context::set_depth_range(const float& min, const float& max) { +#if !defined(__EMSCRIPTEN__) if (!gles) { glDepthRange(min, max); } else +#endif { glDepthRangef(min, max); } @@ -2830,6 +2928,7 @@ int32_t gl_context::get_context_value(const context_value property) return maxMultiSampleBufferFormatSamples; case gfx_api::context::context_value::MAX_VERTEX_OUTPUT_COMPONENTS: // special-handling for MAX_VERTEX_OUTPUT_COMPONENTS +#if !defined(__EMSCRIPTEN__) if (!gles) { if (GLAD_GL_VERSION_3_2) @@ -2855,6 +2954,10 @@ int32_t gl_context::get_context_value(const context_value property) value *= 4; } } +#else + // For WebGL 2 + glGetIntegerv(GL_MAX_VERTEX_OUTPUT_COMPONENTS, &value); +#endif break; default: glGetIntegerv(to_gl(property), &value); @@ -2864,6 +2967,8 @@ int32_t gl_context::get_context_value(const context_value property) return value; } +#if !defined(__EMSCRIPTEN__) + uint64_t gl_context::get_estimated_vram_mb(bool dedicatedOnly) { if (GLAD_GL_NVX_gpu_memory_info) @@ -2907,6 +3012,15 @@ uint64_t gl_context::get_estimated_vram_mb(bool dedicatedOnly) return 0; } +#else + +uint64_t gl_context::get_estimated_vram_mb(bool dedicatedOnly) +{ + return 0; +} + +#endif + // MARK: gl_context - debug void gl_context::debugStringMarker(const char *str) @@ -3023,37 +3137,60 @@ uint64_t gl_context::debugGetPerfValue(PERF_POINT pp) #endif } -// Returns a space-separated list of OpenGL extensions -static std::string getGLExtensions() +static std::vector splitStringIntoVector(const char* pStr, char delimeter) { - std::string extensions; - if (GLAD_GL_VERSION_3_0) + const char *pStrBegin = pStr; + const char *pStrEnd = pStrBegin + strlen(pStr); + std::vector result; + for (const char *i = pStrBegin; i < pStrEnd;) { - // OpenGL 3.0+ - if (!glGetIntegerv || !glGetStringi) - { - return extensions; - } + const char *j = std::find(i, pStrEnd, delimeter); + result.push_back(std::string(i, j)); + i = j + 1; + } + return result; +} - GLint ext_count = 0; - glGetIntegerv(GL_NUM_EXTENSIONS, &ext_count); - if (ext_count < 0) - { - ext_count = 0; - } - for (GLint i = 0; i < ext_count; i++) +static std::vector wzGLGetStringi_GL_EXTENSIONS_Impl() +{ + std::vector extensions; + +#if !defined(WZ_STATIC_GL_BINDINGS) + if (!glGetIntegerv || !glGetStringi) + { + return extensions; + } +#endif + + GLint ext_count = 0; + glGetIntegerv(GL_NUM_EXTENSIONS, &ext_count); + if (ext_count < 0) + { + ext_count = 0; + } + for (GLint i = 0; i < ext_count; i++) + { + const char *pGLStr = (const char*) glGetStringi(GL_EXTENSIONS, i); + if (pGLStr != nullptr) { - const char *pGLStr = (const char*) glGetStringi(GL_EXTENSIONS, i); - if (pGLStr != nullptr) - { - if (!extensions.empty()) - { - extensions += " "; - } - extensions += pGLStr; - } + extensions.push_back(std::string(pGLStr)); } } + + return extensions; +} + +#if !defined(__EMSCRIPTEN__) + +// Returns a space-separated list of OpenGL extensions +static std::vector getGLExtensions() +{ + std::vector extensions; + if (GLAD_GL_VERSION_3_0) + { + // OpenGL 3.0+ + return wzGLGetStringi_GL_EXTENSIONS_Impl(); + } else { // OpenGL < 3.0 @@ -3061,12 +3198,23 @@ static std::string getGLExtensions() const char *pExtensionsStr = (const char *) glGetString(GL_EXTENSIONS); if (pExtensionsStr != nullptr) { - extensions = std::string(pExtensionsStr); + extensions = splitStringIntoVector(pExtensionsStr, ' '); } } return extensions; } +#else + +// Return a list of *enabled* WebGL extensions +static std::vector getGLExtensions() +{ + // Note: Only works after initWebGLExtensions() has been called + return std::vector(enabledWebGLExtensions.begin(), enabledWebGLExtensions.end()); +} + +#endif // !defined(__EMSCRIPTEN__) + std::map gl_context::getBackendGameInfo() { std::map backendGameInfo; @@ -3074,7 +3222,11 @@ std::map gl_context::getBackendGameInfo() backendGameInfo["openGL_renderer"] = opengl.renderer; backendGameInfo["openGL_version"] = opengl.version; backendGameInfo["openGL_GLSL_version"] = opengl.GLSLversion; - backendGameInfo["GL_EXTENSIONS"] = getGLExtensions(); +#if !defined(__EMSCRIPTEN__) + backendGameInfo["GL_EXTENSIONS"] = fmt::format("{}",fmt::join(getGLExtensions()," ")); +#else + backendGameInfo["GL_EXTENSIONS"] = fmt::format("{}",fmt::join(enabledWebGLExtensions," ")); +#endif return backendGameInfo; } @@ -3244,6 +3396,7 @@ static void GLAPIENTRY khr_callback(GLenum source, GLenum type, GLuint id, GLenu uint32_t gl_context::getSuggestedDefaultDepthBufferResolution() const { +#if defined(GL_NVX_gpu_memory_info) // Use a (very simple) heuristic, that may or may not be useful - but basically try to find graphics cards that have lots of memory... if (GLAD_GL_NVX_gpu_memory_info) { @@ -3261,6 +3414,7 @@ uint32_t gl_context::getSuggestedDefaultDepthBufferResolution() const return 2048; } } +#endif // else if (GLAD_GL_ATI_meminfo) // { // // For GL_ATI_meminfo, we could get the current free texture memory (w/ GL_TEXTURE_FREE_MEMORY_ATI, checking stats_kb[0]) @@ -3424,7 +3578,9 @@ bool gl_context::createDefaultTextures() static const GLubyte* wzSafeGlGetString(GLenum name) { static const GLubyte emptyString[1] = {0}; +#if !defined(WZ_STATIC_GL_BINDINGS) ASSERT_OR_RETURN(emptyString, glGetString != nullptr, "glGetString is null"); +#endif auto result = glGetString(name); if (result == nullptr) { @@ -3487,14 +3643,15 @@ bool gl_context::isBlocklistedGraphicsDriver() const bool gl_context::initGLContext() { frameNum = 1; + gles = backend_impl->isOpenGLES(); +#if !defined(WZ_STATIC_GL_BINDINGS) GLADloadproc func_GLGetProcAddress = backend_impl->getGLGetProcAddress(); if (!func_GLGetProcAddress) { debug(LOG_FATAL, "backend_impl->getGLGetProcAddress() returned NULL"); return false; } - gles = backend_impl->isOpenGLES(); if (!gles) { if (!gladLoadGLLoader(func_GLGetProcAddress)) @@ -3511,6 +3668,7 @@ bool gl_context::initGLContext() return false; } } +#endif /* Dump general information about OpenGL implementation to the console and the dump file */ ssprintf(opengl.vendor, "OpenGL Vendor: %s", wzSafeGlGetString(GL_VENDOR)); @@ -3538,19 +3696,11 @@ bool gl_context::initGLContext() khr_debug = false; #endif - std::string extensionsStr = getGLExtensions(); - const char *extensionsBegin = extensionsStr.data(); - const char *extensionsEnd = extensionsBegin + strlen(extensionsBegin); - std::vector glExtensions; - for (const char *i = extensionsBegin; i < extensionsEnd;) - { - const char *j = std::find(i, extensionsEnd, ' '); - glExtensions.push_back(std::string(i, j)); - i = j + 1; - } +#if !defined(__EMSCRIPTEN__) /* Dump extended information about OpenGL implementation to the console */ + std::vector glExtensions = getGLExtensions(); std::string line; for (unsigned n = 0; n < glExtensions.size(); ++n) { @@ -3617,6 +3767,43 @@ bool gl_context::initGLContext() return false; } +#else + + // Emscripten-specific + + const char* version = (const char*)wzSafeGlGetString(GL_VERSION); + bool WZ_WEB_GL_VERSION_2_0 = false; + if (strncmp(version, "OpenGL ES 2.0", 13) == 0) + { + // WebGL 1 - not supported + debug(LOG_POPUP, "WebGL 2.0 not supported. Please upgrade your browser / drivers."); + return false; + } + else if (strncmp(version, "OpenGL ES 3.0", 13) == 0) + { + // WebGL 2 + WZ_WEB_GL_VERSION_2_0 = true; + GLAD_GL_ES_VERSION_3_0 = 1; + } + else + { + debug(LOG_POPUP, "Unsupported WebGL version string: %s", version); + return false; + } + + debug(LOG_3D, " * WebGL 2.0 %s supported!", WZ_WEB_GL_VERSION_2_0 ? "is" : "is NOT"); + + if (!initWebGLExtensions()) + { + debug(LOG_ERROR, "Failed to get WebGL extensions"); + } + GLAD_GL_EXT_texture_filter_anisotropic = enabledWebGLExtensions.count("EXT_texture_filter_anisotropic") > 0; + + debug(LOG_3D, " * Anisotropic filtering %s supported.", GLAD_GL_EXT_texture_filter_anisotropic ? "is" : "is NOT"); + // FUTURE TODO: Check and output other extensions + +#endif + fragmentHighpFloatAvailable = true; fragmentHighpIntAvailable = true; if (gles) @@ -3708,6 +3895,7 @@ bool gl_context::initGLContext() } #endif +#if !defined(__EMSCRIPTEN__) if (GLAD_GL_VERSION_3_0) // if context is OpenGL 3.0+ { // Very simple VAO code - just bind a single global VAO (this gets things working, but is not optimal) @@ -3722,6 +3910,7 @@ bool gl_context::initGLContext() glBindVertexArray(vaoId); wzGLCheckErrors(); } +#endif #if defined(WZ_GL_TIMER_QUERY_SUPPORTED) if (GLAD_GL_ARB_timer_query) @@ -3735,6 +3924,7 @@ bool gl_context::initGLContext() { maxTextureAnisotropy = 0.f; glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxTextureAnisotropy); + debug(LOG_3D, " * (current) maxTextureAnisotropy: %f", maxTextureAnisotropy); wzGLCheckErrors(); } @@ -3832,6 +4022,196 @@ bool gl_context::supportsInstancedRendering() return hasInstancedRenderingSupport; } +bool gl_context::textureFormatIsSupported(gfx_api::pixel_format_target target, gfx_api::pixel_format format, gfx_api::pixel_format_usage::flags usage) +{ + size_t formatIdx = static_cast(format); + ASSERT_OR_RETURN(false, formatIdx < textureFormatsSupport[static_cast(target)].size(), "Invalid format index: %zu", formatIdx); + return (textureFormatsSupport[static_cast(target)][formatIdx] & usage) == usage; +} + +#if defined(__EMSCRIPTEN__) + +static gfx_api::pixel_format_usage::flags getPixelFormatUsageSupport_gl(GLenum target, gfx_api::pixel_format format, bool gles) +{ + gfx_api::pixel_format_usage::flags retVal = gfx_api::pixel_format_usage::none; + + switch (format) + { + // UNCOMPRESSED FORMATS + case gfx_api::pixel_format::FORMAT_RGBA8_UNORM_PACK8: + retVal |= gfx_api::pixel_format_usage::sampled_image; + break; + case gfx_api::pixel_format::FORMAT_BGRA8_UNORM_PACK8: + retVal |= gfx_api::pixel_format_usage::sampled_image; + break; + case gfx_api::pixel_format::FORMAT_RGB8_UNORM_PACK8: + retVal |= gfx_api::pixel_format_usage::sampled_image; + break; + case gfx_api::pixel_format::FORMAT_RG8_UNORM: + // supported in WebGL2 + retVal |= gfx_api::pixel_format_usage::sampled_image; + break; + case gfx_api::pixel_format::FORMAT_R8_UNORM: + retVal |= gfx_api::pixel_format_usage::sampled_image; + break; + // COMPRESSED FORMAT + case gfx_api::pixel_format::FORMAT_RGB_BC1_UNORM: + case gfx_api::pixel_format::FORMAT_RGBA_BC2_UNORM: + case gfx_api::pixel_format::FORMAT_RGBA_BC3_UNORM: + if (enabledWebGLExtensions.count("WEBGL_compressed_texture_s3tc") > 0) + { + retVal |= gfx_api::pixel_format_usage::sampled_image; + } + break; + case gfx_api::pixel_format::FORMAT_R_BC4_UNORM: + case gfx_api::pixel_format::FORMAT_RG_BC5_UNORM: + if (enabledWebGLExtensions.count("EXT_texture_compression_rgtc") > 0) + { + retVal |= gfx_api::pixel_format_usage::sampled_image; + } + break; + case gfx_api::pixel_format::FORMAT_RGBA_BPTC_UNORM: + if (enabledWebGLExtensions.count("EXT_texture_compression_bptc") > 0) + { + retVal |= gfx_api::pixel_format_usage::sampled_image; + } + break; + case gfx_api::pixel_format::FORMAT_RGB8_ETC1: + // not supported + break; + case gfx_api::pixel_format::FORMAT_RGB8_ETC2: + case gfx_api::pixel_format::FORMAT_RGBA8_ETC2_EAC: + case gfx_api::pixel_format::FORMAT_R11_EAC: + case gfx_api::pixel_format::FORMAT_RG11_EAC: + if (enabledWebGLExtensions.count("WEBGL_compressed_texture_etc") > 0) + { + retVal |= gfx_api::pixel_format_usage::sampled_image; + } + break; + case gfx_api::pixel_format::FORMAT_ASTC_4x4_UNORM: + if (enabledWebGLExtensions.count("WEBGL_compressed_texture_astc") > 0) + { + retVal |= gfx_api::pixel_format_usage::sampled_image; + } + break; + default: + debug(LOG_INFO, "Unrecognised pixel format"); + } + + return retVal; +} + +void gl_context::initPixelFormatsSupport() +{ + // set any existing entries to false + for (size_t target = 0; target < gfx_api::PIXEL_FORMAT_TARGET_COUNT; target++) + { + for (size_t i = 0; i < textureFormatsSupport[target].size(); i++) + { + textureFormatsSupport[target][i] = gfx_api::pixel_format_usage::none; + } + textureFormatsSupport[target].resize(static_cast(gfx_api::MAX_PIXEL_FORMAT) + 1, gfx_api::pixel_format_usage::none); + } + + // check if 2D texture array support is available + has2DTextureArraySupport = true; // Texture arrays are supported in OpenGL ES 3.0+ / WebGL 2.0 + + #define PIXEL_2D_FORMAT_SUPPORT_SET(x) \ + textureFormatsSupport[static_cast(gfx_api::pixel_format_target::texture_2d)][static_cast(x)] = getPixelFormatUsageSupport_gl(GL_TEXTURE_2D, x, gles); + + #define PIXEL_2D_TEXTURE_ARRAY_FORMAT_SUPPORT_SET(x) \ + if (has2DTextureArraySupport) { textureFormatsSupport[static_cast(gfx_api::pixel_format_target::texture_2d_array)][static_cast(x)] = getPixelFormatUsageSupport_gl(GL_TEXTURE_2D_ARRAY, x, gles); } + + // The following are always guaranteed to be supported + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGBA8_UNORM_PACK8) + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGB8_UNORM_PACK8) + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_R8_UNORM) + + PIXEL_2D_TEXTURE_ARRAY_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGBA8_UNORM_PACK8) + PIXEL_2D_TEXTURE_ARRAY_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGB8_UNORM_PACK8) + PIXEL_2D_TEXTURE_ARRAY_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_R8_UNORM) + + // RG8 + // WebGL: WebGL 2.0 + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RG8_UNORM) + PIXEL_2D_TEXTURE_ARRAY_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RG8_UNORM) + + // S3TC + // WebGL: WEBGL_compressed_texture_s3tc + if (enabledWebGLExtensions.count("WEBGL_compressed_texture_s3tc") > 0) + { + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGB_BC1_UNORM) // DXT1 + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGBA_BC2_UNORM) // DXT3 + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGBA_BC3_UNORM) // DXT5 + + PIXEL_2D_TEXTURE_ARRAY_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGB_BC1_UNORM) // DXT1 + PIXEL_2D_TEXTURE_ARRAY_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGBA_BC2_UNORM) // DXT3 + PIXEL_2D_TEXTURE_ARRAY_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGBA_BC3_UNORM) // DXT5 + } + + // RGTC + // WebGL: EXT_texture_compression_rgtc + if (enabledWebGLExtensions.count("EXT_texture_compression_rgtc") > 0) + { + // Note: EXT_texture_compression_rgtc does *NOT* support glCompressedTex*Image3D (even for 2d texture arrays)? + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_R_BC4_UNORM) + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RG_BC5_UNORM) + } + + // BPTC + // WebGL: EXT_texture_compression_bptc + if (enabledWebGLExtensions.count("EXT_texture_compression_bptc") > 0) + { + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGBA_BPTC_UNORM) + PIXEL_2D_TEXTURE_ARRAY_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGBA_BPTC_UNORM) + } + + // ETC1 + + // ETC2 + // WebGL: WEBGL_compressed_texture_etc + if (enabledWebGLExtensions.count("WEBGL_compressed_texture_etc") > 0) + { + // NOTES: + // WebGL 2.0 claims it is supported for 2d texture arrays + bool canSupport2DTextureArrays = true; + + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGB8_ETC2) + if (canSupport2DTextureArrays) + { + PIXEL_2D_TEXTURE_ARRAY_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGB8_ETC2) + } + + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGBA8_ETC2_EAC) + if (canSupport2DTextureArrays) + { + PIXEL_2D_TEXTURE_ARRAY_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RGBA8_ETC2_EAC) + } + + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_R11_EAC) + if (canSupport2DTextureArrays) + { + PIXEL_2D_TEXTURE_ARRAY_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_R11_EAC) + } + + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RG11_EAC) + if (canSupport2DTextureArrays) + { + PIXEL_2D_TEXTURE_ARRAY_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_RG11_EAC) + } + } + + // ASTC (LDR) + // WebGL: WEBGL_compressed_texture_astc + if (enabledWebGLExtensions.count("WEBGL_compressed_texture_astc") > 0) + { + PIXEL_2D_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_ASTC_4x4_UNORM) + PIXEL_2D_TEXTURE_ARRAY_FORMAT_SUPPORT_SET(gfx_api::pixel_format::FORMAT_ASTC_4x4_UNORM) + } +} + +#else // !defined(__EMSCRIPTEN__) // Regular OpenGL / OpenGL ES implementation + static gfx_api::pixel_format_usage::flags getPixelFormatUsageSupport_gl(GLenum target, gfx_api::pixel_format format, bool gles) { gfx_api::pixel_format_usage::flags retVal = gfx_api::pixel_format_usage::none; @@ -3904,13 +4284,6 @@ static gfx_api::pixel_format_usage::flags getPixelFormatUsageSupport_gl(GLenum t return retVal; } -bool gl_context::textureFormatIsSupported(gfx_api::pixel_format_target target, gfx_api::pixel_format format, gfx_api::pixel_format_usage::flags usage) -{ - size_t formatIdx = static_cast(format); - ASSERT_OR_RETURN(false, formatIdx < textureFormatsSupport[static_cast(target)].size(), "Invalid format index: %zu", formatIdx); - return (textureFormatsSupport[static_cast(target)][formatIdx] & usage) == usage; -} - void gl_context::initPixelFormatsSupport() { // set any existing entries to false @@ -4122,8 +4495,11 @@ void gl_context::initPixelFormatsSupport() maxMultiSampleBufferFormatSamples = std::max(maxMultiSampleBufferFormatSamples, 0); } +#endif + bool gl_context::initInstancedFunctions() { +#if !defined(WZ_STATIC_GL_BINDINGS) wz_dyn_glDrawArraysInstanced = nullptr; wz_dyn_glDrawElementsInstanced = nullptr; wz_dyn_glVertexAttribDivisor = nullptr; @@ -4179,7 +4555,7 @@ bool gl_context::initInstancedFunctions() wz_dyn_glVertexAttribDivisor = nullptr; return false; } - +#endif return true; } @@ -4233,14 +4609,24 @@ bool gl_context::shouldDraw() void gl_context::shutdown() { - if(glClear) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); +#if !defined(WZ_STATIC_GL_BINDINGS) + if (glClear) +#endif + { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + } deleteSceneRenderpass(); - if (glDeleteFramebuffers && depthFBO.size() > 0) +#if !defined(WZ_STATIC_GL_BINDINGS) + if (glDeleteFramebuffers) +#endif { - glDeleteFramebuffers(static_cast(depthFBO.size()), depthFBO.data()); - depthFBO.clear(); + if (depthFBO.size() > 0) + { + glDeleteFramebuffers(static_cast(depthFBO.size()), depthFBO.data()); + depthFBO.clear(); + } } if (depthTexture) @@ -4267,7 +4653,9 @@ void gl_context::shutdown() pDefaultDepthTexture = nullptr; } +#if !defined(WZ_STATIC_GL_BINDINGS) if (glDeleteBuffers) // glDeleteBuffers might be NULL (if initializing the OpenGL loader library fails) +#endif { glDeleteBuffers(1, &scratchbuffer); scratchbuffer = 0; @@ -4280,6 +4668,7 @@ void gl_context::shutdown() } #endif +#if !defined(__EMSCRIPTEN__) if (GLAD_GL_VERSION_3_0) // if context is OpenGL 3.0+ { // Cleanup from very simple VAO code (just bind a single global VAO) @@ -4292,6 +4681,7 @@ void gl_context::shutdown() vaoId = 0; } } +#endif for (auto& pipelineInfo : createdPipelines) { @@ -4337,6 +4727,7 @@ gfx_api::context::swap_interval_mode gl_context::getSwapInterval() const bool gl_context::supportsMipLodBias() const { +#if !defined(__EMSCRIPTEN__) if (!gles) { if (GLAD_GL_VERSION_2_1) @@ -4357,6 +4748,11 @@ bool gl_context::supportsMipLodBias() const } return false; } +#else + // Can support on OpenGL ES 2.0+ + // By providing bias to texture() (OpenGL ES 3.0+) or texture2d() (OpenGL ES 2.0) sampling call in shader + return true; +#endif } bool gl_context::supports2DTextureArrays() const @@ -4366,10 +4762,15 @@ bool gl_context::supports2DTextureArrays() const bool gl_context::supportsIntVertexAttributes() const { +#if !defined(__EMSCRIPTEN__) // glVertexAttribIPointer requires: OpenGL 3.0+ or OpenGL ES 3.0+ bool hasRequiredVersion = (!gles && GLAD_GL_VERSION_3_0) || (gles && GLAD_GL_ES_VERSION_3_0); bool hasRequiredFunction = glVertexAttribIPointer != nullptr; return hasRequiredVersion && hasRequiredFunction; +#else + // Always available in WebGL 2.0 + return true; +#endif } size_t gl_context::maxFramesInFlight() const @@ -4428,8 +4829,12 @@ static const char *cbframebuffererror(GLenum err) case GL_FRAMEBUFFER_UNDEFINED: return "GL_FRAMEBUFFER_UNDEFINED"; case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: return "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT"; case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: return "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"; +#ifdef GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: return "GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER"; +#endif +#ifdef GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: return "GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER"; +#endif case GL_FRAMEBUFFER_UNSUPPORTED: return "GL_FRAMEBUFFER_UNSUPPORTED"; case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: return "GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE"; case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: return "GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS"; @@ -4441,6 +4846,7 @@ size_t gl_context::initDepthPasses(size_t resolution) { depthPassCount = std::min(depthPassCount, WZ_MAX_SHADOW_CASCADES); +#if !defined(__EMSCRIPTEN__) if (depthPassCount > 1) { if ((!gles && !GLAD_GL_VERSION_3_0) || (gles && !GLAD_GL_ES_VERSION_3_0)) @@ -4449,12 +4855,18 @@ size_t gl_context::initDepthPasses(size_t resolution) debug(LOG_ERROR, "Cannot create depth texture array - requires OpenGL 3.0+ / OpenGL ES 3.0+ - this will fail"); } } +#endif // delete prior depth texture & FBOs (if present) - if (glDeleteFramebuffers && depthFBO.size() > 0) +#if !defined(WZ_STATIC_GL_BINDINGS) + if (glDeleteFramebuffers) +#endif { - glDeleteFramebuffers(static_cast(depthFBO.size()), depthFBO.data()); - depthFBO.clear(); + if (depthFBO.size() > 0) + { + glDeleteFramebuffers(static_cast(depthFBO.size()), depthFBO.data()); + depthFBO.clear(); + } } if (depthTexture) { @@ -4490,7 +4902,9 @@ size_t gl_context::initDepthPasses(size_t resolution) glBindFramebuffer(GL_FRAMEBUFFER, newFBO); if (depthTexture->isArray()) { +#if !defined(WZ_STATIC_GL_BINDINGS) ASSERT(glFramebufferTextureLayer != nullptr, "glFramebufferTextureLayer is not available?"); +#endif glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthTexture->id(), 0, static_cast(i)); // OpenGL 3.0+ / ES 3.0+ only } else @@ -4516,15 +4930,20 @@ size_t gl_context::initDepthPasses(size_t resolution) void gl_context::deleteSceneRenderpass() { // delete prior scene texture & FBOs (if present) - if (glDeleteFramebuffers && sceneFBO.size() > 0) - { - glDeleteFramebuffers(static_cast(sceneFBO.size()), sceneFBO.data()); - sceneFBO.clear(); - } - if (glDeleteFramebuffers && sceneResolveFBO.size() > 0) +#if !defined(WZ_STATIC_GL_BINDINGS) + if (glDeleteFramebuffers) +#endif { - glDeleteFramebuffers(static_cast(sceneResolveFBO.size()), sceneResolveFBO.data()); - sceneResolveFBO.clear(); + if (sceneFBO.size() > 0) + { + glDeleteFramebuffers(static_cast(sceneFBO.size()), sceneFBO.data()); + sceneFBO.clear(); + } + if (sceneResolveFBO.size() > 0) + { + glDeleteFramebuffers(static_cast(sceneResolveFBO.size()), sceneResolveFBO.data()); + sceneResolveFBO.clear(); + } } if (sceneMsaaRBO) { @@ -4547,12 +4966,14 @@ bool gl_context::createSceneRenderpass() { deleteSceneRenderpass(); +#if !defined(__EMSCRIPTEN__) if ( ! ((!gles && GLAD_GL_VERSION_3_0) || (gles && GLAD_GL_ES_VERSION_3_0)) ) { // The following requires OpenGL 3.0+ or OpenGL ES 3.0+ debug(LOG_ERROR, "Unsupported version of OpenGL / OpenGL ES."); return false; } +#endif wzGLClearErrors(); // clear OpenGL error states @@ -4709,19 +5130,31 @@ void gl_context::endSceneRenderPass() invalid_ap[0] = GL_DEPTH_STENCIL_ATTACHMENT; glInvalidateFramebuffer(GL_FRAMEBUFFER, 1, invalid_ap); } +#if !defined(__EMSCRIPTEN__) else { invalid_ap[0] = GL_DEPTH_ATTACHMENT; invalid_ap[1] = GL_STENCIL_ATTACHMENT; - if (!gles && GLAD_GL_ARB_invalidate_subdata && glInvalidateFramebuffer) + if (!gles && GLAD_GL_ARB_invalidate_subdata) { - glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, invalid_ap); + #if !defined(WZ_STATIC_GL_BINDINGS) + if (glInvalidateFramebuffer) + #endif + { + glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, invalid_ap); + } } - else if (gles && GLAD_GL_EXT_discard_framebuffer && glDiscardFramebufferEXT) + else if (gles && GLAD_GL_EXT_discard_framebuffer) { - glDiscardFramebufferEXT(GL_FRAMEBUFFER, 2, invalid_ap); + #if !defined(WZ_STATIC_GL_BINDINGS) + if (glDiscardFramebufferEXT) + #endif + { + glDiscardFramebufferEXT(GL_FRAMEBUFFER, 2, invalid_ap); + } } } +#endif // If MSAA is enabled, use glBiltFramebuffer from the intermediate MSAA-enabled renderbuffer storage to a standard texture (resolving MSAA) bool usingMSAAIntermediate = (sceneMsaaRBO != 0); @@ -4748,10 +5181,12 @@ void gl_context::endSceneRenderPass() } else { +#if defined(GL_ARB_invalidate_subdata) if (!gles && GLAD_GL_ARB_invalidate_subdata && glInvalidateFramebuffer) { glInvalidateFramebuffer(GL_READ_FRAMEBUFFER, 1, invalid_msaarbo_ap); } +#endif // else if (gles && GLAD_GL_EXT_discard_framebuffer && glDiscardFramebufferEXT) // { // // NOTE: glDiscardFramebufferEXT only supports GL_FRAMEBUFFER... but that doesn't work here @@ -4779,3 +5214,99 @@ gfx_api::abstract_texture* gl_context::getSceneTexture() { return sceneTexture; } + +#if defined(__EMSCRIPTEN__) + +static std::vector getEmscriptenSupportedGLExtensions() +{ + // Use the GL_NUM_EXTENSIONS / GL_EXTENSIONS implementation to get the list of extensions that *Emscripten* natively supports + // This may be a subset of all the extensions that the browser supports + // (WebGL extensions must be enabled to be available) + auto extensions = wzGLGetStringi_GL_EXTENSIONS_Impl(); + + // Remove the "GL_" prefix that Emscripten may add + const std::string GL_prefix = "GL_"; + for (auto& extensionStr : extensions) + { + if (extensionStr.rfind(GL_prefix, 0) == 0) + { + extensionStr = extensionStr.substr(GL_prefix.length()); + } + } + + return extensions; +} + +static bool initWebGLExtensions() +{ + // Get list of _supported_ WebGL extensions (which may be a superset of the Emscripten-default-enabled ones) + std::unordered_set supportedWebGLExtensions; + char* spaceSeparatedExtensions = emscripten_webgl_get_supported_extensions(); + if (!spaceSeparatedExtensions) + { + return false; + } + + debug(LOG_INFO, "Supported WebGL extensions: %s", spaceSeparatedExtensions); + + std::vector strings; + std::istringstream str_stream(spaceSeparatedExtensions); + std::string s; + while (getline(str_stream, s, ' ')) { + supportedWebGLExtensions.insert(s); + } + + free(spaceSeparatedExtensions); + + // Get the list of Emscripten-enabled / default-supported WebGL extensions + auto glExtensionsResult = getEmscriptenSupportedGLExtensions(); + std::unordered_set emscriptenSupportedExtensionsList(glExtensionsResult.begin(), glExtensionsResult.end()); + + // Enable WebGL extensions + // NOTE: It is possible to compile for Emscripten without auto-enabling of the default set of extensions + // So we *MUST* always call emscripten_webgl_enable_extension() for each desired extension + enabledWebGLExtensions.clear(); + auto ctx = emscripten_webgl_get_current_context(); + auto tryEnableWebGLExtension = [&](const char* extensionName, bool bypassEmscriptenSupportedCheck = false) { + if (supportedWebGLExtensions.count(extensionName) == 0) + { + debug(LOG_3D, "Extension not available: %s", extensionName); + return; + } + if (!bypassEmscriptenSupportedCheck && emscriptenSupportedExtensionsList.count(extensionName) == 0) + { + debug(LOG_3D, "Skipping due to lack of Emscripten-advertised support: %s", extensionName); + return; + } + if (!emscripten_webgl_enable_extension(ctx, extensionName)) + { + debug(LOG_3D, "Failed to enable extension: %s", extensionName); + return; + } + + debug(LOG_3D, "Enabled extension: %s", extensionName); + enabledWebGLExtensions.insert(extensionName); + }; + + // NOTE: WebGL 2 includes some features by default (that used to be in extensions) + // See: https://webgl2fundamentals.org/webgl/lessons/webgl1-to-webgl2.html#features-you-can-take-for-granted + + // general + tryEnableWebGLExtension("EXT_color_buffer_half_float"); + tryEnableWebGLExtension("EXT_color_buffer_float"); + tryEnableWebGLExtension("EXT_float_blend"); + tryEnableWebGLExtension("EXT_texture_filter_anisotropic", true); + tryEnableWebGLExtension("OES_texture_float_linear"); + tryEnableWebGLExtension("WEBGL_blend_func_extended"); + + // compressed texture format extensions + tryEnableWebGLExtension("WEBGL_compressed_texture_s3tc", true); + tryEnableWebGLExtension("WEBGL_compressed_texture_s3tc_srgb", true); + tryEnableWebGLExtension("EXT_texture_compression_rgtc", true); + tryEnableWebGLExtension("EXT_texture_compression_bptc", true); + tryEnableWebGLExtension("WEBGL_compressed_texture_etc", true); + tryEnableWebGLExtension("WEBGL_compressed_texture_astc", true); + + return true; +} +#endif diff --git a/lib/ivis_opengl/gfx_api_gl.h b/lib/ivis_opengl/gfx_api_gl.h index 0b33b408fb5..4c0a7bf9fd0 100644 --- a/lib/ivis_opengl/gfx_api_gl.h +++ b/lib/ivis_opengl/gfx_api_gl.h @@ -21,7 +21,17 @@ #include "gfx_api.h" +#if defined(__EMSCRIPTEN__) +# define WZ_STATIC_GL_BINDINGS +#endif + +#if !defined(__EMSCRIPTEN__) || !defined(WZ_STATIC_GL_BINDINGS) #include +#else +// Emscripten uses static linking for performance +#include +typedef void* (* GLADloadproc)(const char *name); +#endif #include #include #include diff --git a/lib/ivis_opengl/textdraw.cpp b/lib/ivis_opengl/textdraw.cpp index 64cbfb118a0..c9812cbf823 100644 --- a/lib/ivis_opengl/textdraw.cpp +++ b/lib/ivis_opengl/textdraw.cpp @@ -1076,7 +1076,9 @@ static bool inline initializeCJKFontsIfNeeded() cjkFonts->smallBold = std::make_unique(getGlobalFTlib().lib, CJK_FONT_PATH, 9 * 64, horizDPI, vertDPI, 700); } catch (const std::exception &e) { +#if !defined(__EMSCRIPTEN__) debug(LOG_ERROR, "Failed to load font:\n%s", e.what()); +#endif delete cjkFonts; cjkFonts = nullptr; failedToLoadCJKFonts = true; @@ -1203,7 +1205,9 @@ void iV_TextInit(unsigned int horizScalePercentage, unsigned int vertScalePercen // (since it's only loaded on-demand, and thus might fail with a fatal error later if missing) if (PHYSFS_exists(CJK_FONT_PATH) == 0) { +#if !defined(__EMSCRIPTEN__) debug(LOG_FATAL, "Missing data file: %s", CJK_FONT_PATH); +#endif } m_unicode_funcs_hb = hb_unicode_funcs_get_default(); diff --git a/lib/sdl/main_sdl.cpp b/lib/sdl/main_sdl.cpp index 49f7d29d99d..96128bd8109 100644 --- a/lib/sdl/main_sdl.cpp +++ b/lib/sdl/main_sdl.cpp @@ -70,6 +70,10 @@ #include "cocoa_wz_menus.h" #endif +#if defined(__EMSCRIPTEN__) +#include +#endif + #include using nonstd::optional; using nonstd::nullopt; @@ -93,8 +97,9 @@ int main(int argc, char *argv[]) static SDL_Window *WZwindow = nullptr; static optional WZbackend = video_backend::opengl; -#if defined(WZ_OS_MAC) || defined(WZ_OS_WIN) +#if defined(WZ_OS_MAC) || defined(WZ_OS_WIN) || defined(__EMSCRIPTEN__) // on macOS, SDL_WINDOW_FULLSCREEN_DESKTOP *must* be used (or high-DPI fullscreen toggling breaks) +// on Emscripten (browser), SDL_WINDOW_FULLSCREEN_DESKTOP should be used (to avoid various toggling breaks) const WINDOW_MODE WZ_SDL_DEFAULT_FULLSCREEN_MODE = WINDOW_MODE::desktop_fullscreen; #else const WINDOW_MODE WZ_SDL_DEFAULT_FULLSCREEN_MODE = WINDOW_MODE::fullscreen; @@ -214,6 +219,17 @@ static optional wzQuitExitCode; bool wzReduceDisplayScalingIfNeeded(int currWidth, int currHeight); +#if defined(__EMSCRIPTEN__) + +#include +#include + +void wzemscripten_startup_ensure_canvas_displayed(); +bool wz_emscripten_enable_soft_fullscreen(); +EM_BOOL wz_emscripten_fullscreenchange_callback(int eventType, const EmscriptenFullscreenChangeEvent *fullscreenChangeEvent, void *userData); + +#endif + /**************************/ /*** Misc support ***/ /**************************/ @@ -394,7 +410,9 @@ static std::vector& sortGfxBackendsForCurrentSystem(std::vector wzAvailableGfxBackends() { std::vector availableBackends; +#if !defined(__EMSCRIPTEN__) availableBackends.push_back(video_backend::opengl); +#endif #if !defined(WZ_OS_MAC) // OpenGL ES is not supported on macOS, and WZ doesn't currently ship with an OpenGL ES library on macOS availableBackends.push_back(video_backend::opengles); #endif @@ -412,7 +430,10 @@ video_backend wzGetDefaultGfxBackendForCurrentSystem() { // SDL backend supports: OpenGL, OpenGLES, Vulkan (if compiled with support), DirectX (on Windows, via LibANGLE) -#if defined(_WIN32) && defined(WZ_BACKEND_DIRECTX) && (defined(_M_ARM64) || defined(_M_ARM)) +#if defined(__EMSCRIPTEN__) + // For Emscripten, OpenGLES (WebGL) should be the default + return video_backend::opengles; +#elif defined(_WIN32) && defined(WZ_BACKEND_DIRECTX) && (defined(_M_ARM64) || defined(_M_ARM)) // On ARM-based Windows, DirectX should be the default (for compatibility) return video_backend::directx; #else @@ -666,7 +687,7 @@ WINDOW_MODE wzGetCurrentWindowMode() std::vector wzSupportedWindowModes() { -#if defined(WZ_OS_MAC) +#if defined(WZ_OS_MAC) || defined(__EMSCRIPTEN__) // on macOS, SDL_WINDOW_FULLSCREEN_DESKTOP *must* be used (or high-DPI fullscreen toggling breaks) // thus "classic" fullscreen is not supported return {WINDOW_MODE::desktop_fullscreen, WINDOW_MODE::windowed}; @@ -751,8 +772,8 @@ WINDOW_MODE wzGetToggleFullscreenMode() bool wzChangeWindowMode(WINDOW_MODE mode, bool silent) { - auto currMode = wzGetCurrentWindowMode(); - if (currMode == mode) + auto previousMode = wzGetCurrentWindowMode(); + if (previousMode == mode) { // already in this mode return true; @@ -766,15 +787,27 @@ bool wzChangeWindowMode(WINDOW_MODE mode, bool silent) if (!silent) { - debug(LOG_INFO, "Changing window mode: %s -> %s", to_display_string(currMode).c_str(), to_display_string(mode).c_str()); + debug(LOG_INFO, "Changing window mode: %s -> %s", to_display_string(previousMode).c_str(), to_display_string(mode).c_str()); } int sdl_result = -1; switch (mode) { case WINDOW_MODE::desktop_fullscreen: - sdl_result = SDL_SetWindowFullscreen(WZwindow, SDL_WINDOW_FULLSCREEN_DESKTOP); - if (sdl_result != 0) { return false; } +#if defined(__EMSCRIPTEN__) + emscripten_exit_soft_fullscreen(); +#endif + sdl_result = SDL_SetWindowFullscreen(WZwindow, SDL_WINDOW_FULLSCREEN_DESKTOP); // TODO: Currently crashes in Emscripten builds? + if (sdl_result != 0) + { +#if defined(__EMSCRIPTEN__) + if (previousMode == WINDOW_MODE::windowed) + { + wz_emscripten_enable_soft_fullscreen(); + } +#endif + return false; + } wzSetWindowIsResizable(false); break; case WINDOW_MODE::windowed: @@ -814,8 +847,20 @@ bool wzChangeWindowMode(WINDOW_MODE mode, bool silent) } case WINDOW_MODE::fullscreen: { +#if defined(__EMSCRIPTEN__) + emscripten_exit_soft_fullscreen(); +#endif sdl_result = SDL_SetWindowFullscreen(WZwindow, SDL_WINDOW_FULLSCREEN); - if (sdl_result != 0) { return false; } + if (sdl_result != 0) + { +#if defined(__EMSCRIPTEN__) + if (previousMode == WINDOW_MODE::windowed) + { + wz_emscripten_enable_soft_fullscreen(); + } +#endif + return false; + } wzSetWindowIsResizable(false); int currWidth = 0, currHeight = 0; SDL_GetWindowSize(WZwindow, &currWidth, &currHeight); @@ -3183,6 +3228,10 @@ bool wzMainScreenSetup(optional backend, int antialiasing, WINDOW } #endif +#if defined(__EMSCRIPTEN__) + wzemscripten_startup_ensure_canvas_displayed(); +#endif + SDL_gfx_api_Impl_Factory::Configuration sdl_impl_config; if (backend.has_value()) @@ -3263,6 +3312,20 @@ bool wzMainScreenSetup(optional backend, int antialiasing, WINDOW if (backend.has_value()) { wzMainScreenSetup_VerifyWindow(); + +#if defined(__EMSCRIPTEN__) + // Catch the full screen change events + // - If user-initiated (i.e. by pressing ESC or similar to exit fullscreen), SDL does not currently expose this event + // - SDL itself sets a fullscreenchange callback on the document, which must remain for SDL functionality - fortunately, emscripten lets us set one on the canvas element itself + emscripten_set_fullscreenchange_callback("#canvas", nullptr, 0, wz_emscripten_fullscreenchange_callback); + + auto mode = wzGetCurrentWindowMode(); + if (mode == WINDOW_MODE::windowed) + { + // Enable "soft fullscreen" - where the canvas automatically fills the window + wz_emscripten_enable_soft_fullscreen(); + } +#endif } #if defined(WZ_OS_WIN) @@ -3555,9 +3618,20 @@ static void handleActiveEvent(SDL_Event *event) } static SDL_Event event; +#if defined(__EMSCRIPTEN__) +std::function saved_onShutdown; +unsigned lastLoopReturn = 0; +#endif void wzEventLoopOneFrame(void* arg) { +#if defined(__EMSCRIPTEN__) + if (lastLoopReturn > 0) + { + wz_emscripten_did_finish_render(lastLoopReturn - wzGetTicks()); + } +#endif + /* Deal with any windows messages */ while (SDL_PollEvent(&event)) { @@ -3584,12 +3658,27 @@ void wzEventLoopOneFrame(void* arg) inputhandleText(&event.text); break; case SDL_QUIT: +#if defined(__EMSCRIPTEN__) + // Exit "soft fullscreen" - (as long as we aren't in "real" fullscreen mode) + emscripten_exit_soft_fullscreen(); + + // Actually trigger cleanup code + if (saved_onShutdown) + { + saved_onShutdown(); + } + wzShutdown(); + + // Stop Emscripten from calling the main loop + emscripten_cancel_main_loop(); +#else ASSERT(arg != nullptr, "No valid bContinue"); if (arg) { bool *bContinue = static_cast(arg); *bContinue = false; } +#endif return; default: break; @@ -3617,6 +3706,9 @@ void wzEventLoopOneFrame(void* arg) processScreenSizeChangeNotificationIfNeeded(); mainLoop(); // WZ does its thing inputNewFrame(); // reset input states +#if defined(__EMSCRIPTEN__) + lastLoopReturn = wzGetTicks(); +#endif } // Actual mainloop @@ -3624,6 +3716,11 @@ void wzMainEventLoop(std::function onShutdown) { event.type = 0; +#if defined(__EMSCRIPTEN__) + saved_onShutdown = onShutdown; + // Receives a function to call and some user data to provide it. + emscripten_set_main_loop_arg(wzEventLoopOneFrame, nullptr, -1, true); +#else bool bContinue = true; while (bContinue) { @@ -3634,6 +3731,7 @@ void wzMainEventLoop(std::function onShutdown) { onShutdown(); } +#endif } void wzPumpEventsWhileLoading() @@ -3686,3 +3784,76 @@ uint64_t wzGetCurrentSystemRAM() int value = SDL_GetSystemRAM(); return (value > 0) ? static_cast(value) : 0; } + +// MARK: - Emscripten-specific functions + +#if defined(__EMSCRIPTEN__) + +void wzemscripten_startup_ensure_canvas_displayed() +{ + MAIN_THREAD_EM_ASM({ + if (typeof wz_js_display_canvas === "function") { + wz_js_display_canvas(); + } + else { + console.log('Cannot find wz_js_display_canvas function'); + } + }); +} + +EM_BOOL wz_emscripten_window_resized_callback(int eventType, const void *reserved, void *userData) +{ + double width, height; + emscripten_get_element_css_size("#canvas", &width, &height); + + int newWindowWidth = (int)width, newWindowHeight = (int)height; + + wzAsyncExecOnMainThread([newWindowWidth, newWindowHeight]{ + // resize SDL window + SDL_SetWindowSize(WZwindow, newWindowWidth, newWindowHeight); + + unsigned int oldWindowWidth = windowWidth; + unsigned int oldWindowHeight = windowHeight; + handleWindowSizeChange(oldWindowWidth, oldWindowHeight, newWindowWidth, newWindowHeight); + // Store the new values (in case the user manually resized the window bounds) + war_SetWidth(newWindowWidth); + war_SetHeight(newWindowHeight); + }); + return EMSCRIPTEN_RESULT_SUCCESS; +} + +bool wz_emscripten_enable_soft_fullscreen() +{ + // Enable "soft fullscreen" - where the canvas automatically fills the window + debug(LOG_INFO, "Would enter soft fullscreen"); + EmscriptenFullscreenStrategy strategy; + strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; + strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; + strategy.canvasResizedCallback = wz_emscripten_window_resized_callback; + strategy.canvasResizedCallbackUserData = nullptr; // pointer to user data + strategy.canvasResizedCallbackTargetThread = pthread_self(); // not used + EMSCRIPTEN_RESULT result = emscripten_enter_soft_fullscreen("#canvas", &strategy); + return result == EMSCRIPTEN_RESULT_SUCCESS || result == EMSCRIPTEN_RESULT_DEFERRED; +} + +EM_BOOL wz_emscripten_fullscreenchange_callback(int eventType, const EmscriptenFullscreenChangeEvent *fullscreenChangeEvent, void *userData) +{ + if (!fullscreenChangeEvent->isFullscreen) + { + // browser left fullscreen, so reset soft fullscreen mode + wzAsyncExecOnMainThread([]{ + + war_setWindowMode(WINDOW_MODE::windowed); // persist the change + + wz_emscripten_enable_soft_fullscreen(); + + // manually trigger resize callback + wz_emscripten_window_resized_callback(EMSCRIPTEN_EVENT_FULLSCREENCHANGE, nullptr, nullptr); + + }); + } + + return EM_FALSE; // return false to ensure this event "bubbles up" to the SDL event handler +} + +#endif diff --git a/pkg/CMakeLists.txt b/pkg/CMakeLists.txt index 75e72fbf860..9e884a8fd69 100644 --- a/pkg/CMakeLists.txt +++ b/pkg/CMakeLists.txt @@ -300,6 +300,18 @@ elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin") set(CPACK_DMG_DS_STORE_SETUP_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/dmg/WZDMGSetup.scpt") set(CPACK_DMG_VOLUME_NAME "Warzone 2100") +elseif(CMAKE_SYSTEM_NAME MATCHES "Emscripten") + # Emscripten builds + + # Only install the "Core" component, as it installs all of the data & supporting files & languages + set(CPACK_COMPONENTS_ALL Core) + unset(CPACK_COMPONENT_CORE_DEPENDS) + set(CPACK_ARCHIVE_COMPONENT_INSTALL ON) + + # Default package generator for Emscripten (archive) + set(CPACK_GENERATOR "ZIP") + set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY FALSE) + endif() ##################### diff --git a/platforms/emscripten/README-build.md b/platforms/emscripten/README-build.md new file mode 100644 index 00000000000..6410b6e523b --- /dev/null +++ b/platforms/emscripten/README-build.md @@ -0,0 +1,49 @@ +# Building Warzone 2100 for the Web + +## Prerequisites: + +- **Git** +- [**Emscripten 3.1.53+**](https://emscripten.org/docs/getting_started/downloads.html) +- [**CMake 2.27+**](https://cmake.org/download/#latest) +- [**workbox-cli**](https://developer.chrome.com/docs/workbox/modules/workbox-cli) (to generate a service worker) +- For language support: [_Gettext_](https://www.gnu.org/software/gettext/) +- To generate documentation: [_Asciidoctor_](https://asciidoctor.org/) + +## Building: + +1. [Install the Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html) +2. Install [workbox-cli](https://developer.chrome.com/docs/workbox/modules/workbox-cli) + ``` + npm install workbox-cli --global + ``` +3. Follow the instructions for [Warzone 2100: Getting the Source](https://github.com/Warzone2100/warzone2100#getting-the-source) +4. `mkdir` a new build folder (as a sibling directory to the warzone2100 repo) +5. `cd` into the build folder +6. Clone vcpkg into the build folder + ``` + git clone https://github.com/microsoft/vcpkg.git vcpkg + ``` +7. Run CMake configure: + ```shell + # Specify your own install dir + export WZ_INSTALL_DIR="~/wz_web/installed" + cmake -S ../warzone2100/ -B . -DCMAKE_BUILD_TYPE=Release "-DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake" "-DVCPKG_TARGET_TRIPLET=wasm32-emscripten" "-DCMAKE_INSTALL_PREFIX:PATH=${WZ_INSTALL_DIR}" + ``` +8. Run CMake build & install: + ``` + cmake --build . --target install + ``` + +## Testing: + +1. Run a local web server and open the browser to the compiled WebAssembly version of WZ. + From the build directory: + ``` + emrun --browser chrome src/index.html + ``` + From the install directory: + ```shell + cd "${WZ_INSTALL_DIR}" + emrun --browser chrome ./index.html + ``` + Reference: [`emrun` documentation](https://emscripten.org/docs/compiling/Running-html-files-with-emrun.html) \ No newline at end of file diff --git a/platforms/emscripten/README.md b/platforms/emscripten/README.md new file mode 100644 index 00000000000..42b1a152fd3 --- /dev/null +++ b/platforms/emscripten/README.md @@ -0,0 +1,85 @@ +# Warzone 2100 - Web Edition + +The Warzone 2100 Web Edition uses [Emscripten](https://emscripten.org) to compile Warzone 2100 to run in modern web browsers via [WebGL2](https://get.webgl.org/webgl2/) and [WebAssembly](https://webassembly.org). + + + * [Browser Compatibility](#browser-compatibility) + * [Features & Differences](#features--differences) + * [Persisting Data](#persisting-data) + * [Campaign Videos](#campaign-videos) + * [↗ Building](README-build.md) + + +## Browser Compatibility: + +To run Warzone 2100 in your web browser, we recommend: +- Recent versions of Chrome, Edge, Firefox, or Safari + - With JavaScript and modern WebAssembly support enabled +- WebGL 2.0 support +- 4-8+ GiB of RAM +- Minimum 1024 x 768 resolution display / browser window +- Keyboard & mouse are also strongly recommended + +The page will automatically perform a basic series of checks for compatibility and inform you if any issues were detected. + +## Features & Differences: + +This port is able to support most core Warzone 2100 features, including: campaign, challenges, and skirmish. + +> [!TIP] +> Some functionality may be limited or not available, due to size constraints or restrictions of the browser environment. + +| Feature | Web | Native | +| :----------------------------- | :---: | :---: | +| Campaign | ✅ | ✅ | +| Campaign Videos | ✅1 | ✅ | +| Challenges | ✅ | ✅ | +| Skirmish | ✅ | ✅ | +| Savegames | ✅ | ✅ | +| HQ graphics | ❌ | ✅ | +| HQ music | ❌ | ✅ | +| Additional music | ❌ | ✅ | +| Multiplayer (online) | ❌ | ✅ | +| Mods | ❌2 | ✅ | +| Multi-language support | ✅3 | ✅ | +| Performance | 🆗 | ✅🚀 | + +> [!NOTE] +> 1 The Web port supports low-quality video sequences, which it streams on-demand. _An active Internet connection is required._ +> 2 The Web port does not currently provide an interface for uploading mods into the configuration directory, but support _could_ be added in the future. +> 3 The Web port currently supports _most_ of the same languages, but certain languages that require additional large fonts (ex. Chinese, Korean, Japanese) are unsupported. + +The Web Edition also ships with textures that have been optimized for size, at the expense of quality. + +> If you want the highest quality textures, and the complete set of features, you should consider downloading the latest [native build for your system](https://github.com/Warzone2100/warzone2100/releases/latest). + +## Persisting Data: + +The Web Edition can persist Warzone 2100 settings, configuration, savegames and more in your browser storage using technologies such as IndexedDB. + +When you explicitly save your game, Warzone 2100 will ask the browser to opt-in to [persistent storage](https://web.dev/articles/persistent-storage). + +Depending on your browser, you may receive a prompt (ex. on Firefox), or this may automatically succeed (or be denied by the browser) without any prompt or notice. + +> [!TIP] +> By default, browsers store data in a "best-effort" manner. +> This means it may be cleared by the browser: +> - When storage is low +> - If a site hasn't been visited in a while +> - Or for other reasons +> +> **Persistent storage can help prevent the browser from automatically evicting your saved games and data.** +> See: [Storage for the Web: Eviction](https://web.dev/articles/storage-for-the-web#eviction) + +> [!IMPORTANT] +> If you manually clear your browser's cache / history for all sites, this will still clear your savegames. + +## Campaign Videos: + +The Web Edition can automatically stream campaign video sequences on-demand (albeit at low-quality). + +> [!IMPORTANT] +> If you've never played Warzone 2100 before, the campaign videos provide critical context for the plot of the campaign. +> **It is strongly recommended you play with an active Internet connection** so these videos can be streamed during gameplay. + +If you have a spotty Internet connection, you should strongly consider the native builds instead, which include the campaign videos for offline viewing. diff --git a/platforms/emscripten/assets/android-chrome-192x192.png b/platforms/emscripten/assets/android-chrome-192x192.png new file mode 100644 index 00000000000..71647e3ac48 Binary files /dev/null and b/platforms/emscripten/assets/android-chrome-192x192.png differ diff --git a/platforms/emscripten/assets/android-chrome-512x512.png b/platforms/emscripten/assets/android-chrome-512x512.png new file mode 100644 index 00000000000..3a50a386fb6 Binary files /dev/null and b/platforms/emscripten/assets/android-chrome-512x512.png differ diff --git a/platforms/emscripten/assets/apple-touch-icon.png b/platforms/emscripten/assets/apple-touch-icon.png new file mode 100644 index 00000000000..e3ead298815 Binary files /dev/null and b/platforms/emscripten/assets/apple-touch-icon.png differ diff --git a/platforms/emscripten/assets/favicon-16x16.png b/platforms/emscripten/assets/favicon-16x16.png new file mode 100644 index 00000000000..b82579c6951 Binary files /dev/null and b/platforms/emscripten/assets/favicon-16x16.png differ diff --git a/platforms/emscripten/assets/favicon-32x32.png b/platforms/emscripten/assets/favicon-32x32.png new file mode 100644 index 00000000000..454c8bff052 Binary files /dev/null and b/platforms/emscripten/assets/favicon-32x32.png differ diff --git a/platforms/emscripten/assets/favicon.ico b/platforms/emscripten/assets/favicon.ico new file mode 100644 index 00000000000..f5ad349852e Binary files /dev/null and b/platforms/emscripten/assets/favicon.ico differ diff --git a/platforms/emscripten/cmake/WorkboxRemoveOldFiles.cmake b/platforms/emscripten/cmake/WorkboxRemoveOldFiles.cmake new file mode 100644 index 00000000000..a5ee2ca7ce6 --- /dev/null +++ b/platforms/emscripten/cmake/WorkboxRemoveOldFiles.cmake @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.5...3.24) +if(${CMAKE_VERSION} VERSION_LESS 3.12) + cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) +endif() + +# Remove the files that workbox-cli generates as output from the current/working directory +# - service-worker.js +# - service-worker.js.map +# - workbox-*.js +# - workbox-*.js.map + +file(GLOB WORKBOX_GENERATED_FILES LIST_DIRECTORIES false + "${CMAKE_CURRENT_SOURCE_DIR}/service-worker.js" + "${CMAKE_CURRENT_SOURCE_DIR}/service-worker.js.map" + "${CMAKE_CURRENT_SOURCE_DIR}/workbox-*.js" + "${CMAKE_CURRENT_SOURCE_DIR}/workbox-*.js.map" +) + +foreach(_file IN LISTS WORKBOX_GENERATED_FILES) + execute_process(COMMAND ${CMAKE_COMMAND} -E echo "Removing old generated file: ${_file}") + file(REMOVE "${_file}") +endforeach() diff --git a/platforms/emscripten/manifest.json b/platforms/emscripten/manifest.json new file mode 100644 index 00000000000..6e1099cd2b7 --- /dev/null +++ b/platforms/emscripten/manifest.json @@ -0,0 +1,30 @@ +{ + "name": "Warzone 2100", + "short_name": "Warzone 2100", + "description": "The Web Edition of Warzone 2100. Command the forces of The Project in a battle to rebuild the world.", + "icons": [ + { + "src": "./assets/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "./assets/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#000000", + "background_color": "#000000", + "display": "standalone", + "orientation": "landscape", + "related_applications": [ + { + "platform": "windows", + "url": "https://apps.microsoft.com/detail/9MW0Z4MPCS8C" + } + ], + "prefer_related_applications": true, + "start_url": "./", + "scope": "." +} \ No newline at end of file diff --git a/platforms/emscripten/no-op-service-worker.js b/platforms/emscripten/no-op-service-worker.js new file mode 100644 index 00000000000..f19c0117628 --- /dev/null +++ b/platforms/emscripten/no-op-service-worker.js @@ -0,0 +1,8 @@ +// no-op service worker + +self.addEventListener('install', () => { + // Skip over the "waiting" lifecycle state, to ensure that our + // new service worker is activated immediately, even if there's + // another tab open controlled by our older service worker code. + self.skipWaiting(); +}); diff --git a/platforms/emscripten/postjs.js b/platforms/emscripten/postjs.js new file mode 100644 index 00000000000..1c42f04bc0b --- /dev/null +++ b/platforms/emscripten/postjs.js @@ -0,0 +1,64 @@ + +/* Setup persistent config dir */ +function wzSetupPersistentConfigDir() { + + Module['WZVAL_configDirPath'] = ''; + if (typeof wz_js_get_config_dir_path === "function") { + Module['WZVAL_configDirPath'] = wz_js_get_config_dir_path(); + } + else { + console.log('Unable to get config dir suffix'); + Module['WZVAL_configDirPath'] = '/warzone2100'; + } + + let configDirPath = Module['WZVAL_configDirPath']; + + // Create a directory in IDBFS to store the config dir + FS.mkdir(configDirPath); + + // Mount IDBFS as the file system + FS.mount(FS.filesystems.IDBFS, {}, configDirPath); + + // Synchronize IDBFS -> Emscripten virtual filesystem + Module["addRunDependency"]("persistent_warzone2100_config_dir"); + FS.syncfs(true, (err) => { + console.log('loaded from idbfs', FS.readdir(configDirPath)); + Module["removeRunDependency"]("persistent_warzone2100_config_dir"); + }) +} +function wzSaveConfigDirToPersistentStore(callback) { + let configDirPath = Module['WZVAL_configDirPath']; + FS.syncfs(false, (err) => { + if (!err) { + console.log('saved to idbfs', FS.readdir(configDirPath)); + } else { + console.warn('Failed to save to idbfs store - data may not be persisted'); + } + if (callback) callback(err); + }) +} +Module.wzSaveConfigDirToPersistentStore = wzSaveConfigDirToPersistentStore; + +if (!Module['preRun']) +{ + Module['preRun'] = []; +} +Module['preRun'].push(wzSetupPersistentConfigDir); + +Module["onExit"] = function() { + // Sync IDBFS + wzSaveConfigDirToPersistentStore(() => { + if (typeof wz_js_display_loading_indicator === "function") { + wz_js_display_loading_indicator(false); + } + else { + alert('It is now safe to close your browser window.'); + } + }); + + if (typeof wz_js_handle_app_exit === "function") { + wz_js_handle_app_exit(); + } +} + +Module['ASAN_OPTIONS'] = 'halt_on_error=0' diff --git a/platforms/emscripten/prejs.js b/platforms/emscripten/prejs.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/platforms/emscripten/shell.html b/platforms/emscripten/shell.html new file mode 100644 index 00000000000..2679989016f --- /dev/null +++ b/platforms/emscripten/shell.html @@ -0,0 +1,1674 @@ + + + + + + + + Warzone 2100 - Web Edition + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+ Loading ... +
+
+
+ + + + +
+
+ +
+
+
+
+ Warzone 2100 Web Edition + +
+
+ + + +
+

Unsupported Browser

+

Unfortunately, your browser does not support the Web Edition of Warzone 2100

+

:-(

+

Please try updating to the latest version of a supported browser
(ex. Chrome, Edge, Firefox, or Safari).

+

Missing or disabled features: WebAssembly, WebGL 2

+
+ +
+

Launch the Web Edition

+

Classic look, Medium-quality textures, Campaign & skirmish

+

+ +

+

If this is your first time, this will download approximately 60MiB of data.

+

Plays Best With: Mouse & Keyboard|View Options

+
+ +
+

Loading ...

+
+
+ +
+

 

+
+ +
+
+
+
+
+
+
+
+
+
+ +
+

+ +

+

An error occurred trying to load Warzone 2100.

+

Please check your Internet connection, and reload to try again.

+

Error Message:

+

+ +

+
+ +
+

Thanks for playing!

+

If you'd like to play again, simply reload.

+

+ +

+

Or help support by donating:

+ Donate +
+ + +
+
+ + +
+
+

Too Small

+

Window / display is too small for Warzone 2100

+

Required Minimum: 640x480

+
+
+

Or get the Full Desktop version, with HQ remastered graphics, Online multiplayer, Better performance, and more.

+

+ Download Warzone 2100 +

+
+
+ + + + + + + +
+ +
+ + + + {{{ SCRIPT }}} + + + diff --git a/platforms/emscripten/wz-workbox-config.js b/platforms/emscripten/wz-workbox-config.js new file mode 100644 index 00000000000..b98ec28bd64 --- /dev/null +++ b/platforms/emscripten/wz-workbox-config.js @@ -0,0 +1,95 @@ +// NOTE: This config should be run on the *installed* files +module.exports = { + // ----------------------------- + // Pre-cache configuration + globDirectory: './', + globPatterns: [ + 'index.html', + 'manifest.json', + // warzone2100.js, warzone2100.worker.js + '*.js', + // Core WASM file + 'warzone2100.wasm', + // Specific favicon assets + 'assets/favicon-16x16.png', + 'assets/favicon-32x32.png', + 'assets/android-chrome-192x192.png', + // Any remaining css, js, or json files from the assets directory + 'assets/*.{css,js,json}' + ], + globIgnores: [ + '**\/node_modules\/**\/*', + '**/*.data', // do not precache .data files, which are often huge + '**/*.debug.wasm', // do not precache wasm debug symbols + '**\/music\/**\/*', // do not precache music (which is optional) + '**\/terrain_overrides\/**\/*' // do not precache terrain_overrides (which are optional) + ], + // NOTE: These should match the versions used in shell.html! + additionalManifestEntries: [ + { url: 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css', revision: null, integrity: 'sha512-b2QcS5SsA8tZodcDtGRELiGv5SaKSk1vDHDaQRda0htPYWZ6046lr3kJ5bAAQdpV2mmA/4v0wQF9MyU6/pDIAg==' }, + { url: 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js', revision: null, integrity: 'sha512-X/YkDZyjTf4wyc2Vy16YGCPHwAY8rZJY+POgokZjQB2mhIRFJCckEGc6YyX9eNsPfn0PzThEuNs+uaomE5CO6A==' } + ], + maximumFileSizeToCacheInBytes: 104857600, // Must be greater than the file size of any precached files + // ----------------------------- + // Runtime caching configuration + runtimeCaching: [ + // NOTE: In regards to warzone2100.data: + // - We do not want to precache it, because it's huge (and precaching doesn't let us show a nice progress bar currently) + // - It is version/build-specific, and should always be in sync with the other data / files + // - It is already cached by the Emscripten preload cache, which handles checking the expected cached file checksum + // So we do not want to runtime cache it via the service worker + // + // NOTE: These named runtime caches are shared across all different build variants of WZ hosted on a domain + // So set maxEntries to constrain cache size and evict old entries + // + // Cache music & terrain_overrides data loader JS for offline use + { + urlPattern: new RegExp('/(music|terrain_overrides)/.*\.js$'), + handler: 'NetworkFirst', + options: { + cacheName: 'optional-data-js-loaders', + expiration: { + maxEntries: 12, + purgeOnQuotaError: true + } + } + }, + // Music & terrain_overrides data + // Note: Each build will generate music and terrain_override packages, but these are not expected to change frequently. + // Since the .data files should be the same (unless Emscripten's file_packager.py changes its packing format), + // ideally do not cache them here, and instead rely on Emscripten's preload-cache (which will handle differences if they occur) + // (Otherwise we'd end up with likely duplicate data in the cache, if the user loads multiple build variants of WZ hosted on a domain) + // { + // urlPattern: new RegExp('/(music|terrain_overrides)/.*\.data$'), + // handler: 'NetworkFirst', + // options: { + // cacheName: 'optional-data-packages', + // expiration: { + // maxEntries: 4, + // purgeOnQuotaError: true + // } + // } + // }, + // + // Backup on-demand caching of any additional utilized CSS and JS files for offline use + // (useful in case someone forgot to update the additionalManifestEntries above) + { + urlPattern: ({url}) => (url.pathname.endsWith('.js') || url.pathname.endsWith('.css')) && url.origin !== 'https://static.cloudflareinsights.com', + handler: 'NetworkFirst', + options: { + cacheName: 'additional-dependencies', + expiration: { + maxEntries: 20, + purgeOnQuotaError: true + } + } + }, + ], + // ----------------------------- + swDest: './service-worker.js', + offlineGoogleAnalytics: false, + ignoreURLParametersMatching: [ + /^utm_/, + /^fbclid$/ + ] +}; diff --git a/po/CMakeLists.txt b/po/CMakeLists.txt index 21b396d5bd1..6fab830a222 100644 --- a/po/CMakeLists.txt +++ b/po/CMakeLists.txt @@ -174,12 +174,18 @@ set(wz2100_translations_LOCALE_FOLDER "${CMAKE_CURRENT_BINARY_DIR}/locale") # On CMake configure, clear the build dir "locale" folder (to ensure re-generation) file(REMOVE_RECURSE "${wz2100_translations_LOCALE_FOLDER}/") +set(_po_install_config) +if(NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten") + set(_po_install_config + INSTALL_DESTINATION "${WZ_LOCALEDIR}" + INSTALL_COMPONENT Languages) +endif() + # Build the *.gmo files from the *.po files (using the .pot file) include(GNUInstallDirs) file (GLOB POFILES *.po) GETTEXT_CREATE_TRANSLATIONS_WZ ("${_potFile}" ALL - INSTALL_DESTINATION "${CMAKE_INSTALL_LOCALEDIR}" - INSTALL_COMPONENT Languages + ${_po_install_config} MSGMERGE_OPTIONS --quiet --no-wrap --width=1 TARGET_FOLDER "po" POFILES ${POFILES}) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4c65f854e83..baddf629cf2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -116,8 +116,12 @@ target_link_libraries(warzone2100 optional-lite) target_link_libraries(warzone2100 quickjs) target_link_libraries(warzone2100 inih) -include(IncludeFindCurl) -target_link_libraries(warzone2100 CURL::libcurl) +if(CMAKE_SYSTEM_NAME MATCHES "Emscripten") + # Exclude CURL implementation, Use Fetch API for Emscripten +else() + include(IncludeFindCurl) + target_link_libraries(warzone2100 CURL::libcurl) +endif() target_link_libraries(warzone2100 re2::re2) find_package(SQLite3 3.14 REQUIRED) @@ -182,7 +186,7 @@ if (DEFINED CURL_OPENSSL_REQUIRES_CALLBACKS) message(STATUS "Ignoring cURL OpenSSL backend, as other thread-safe backend(s) exist") endif() endif() -if (NOT DEFINED CURL_SUPPORTED_SSL_BACKENDS) +if (NOT DEFINED CURL_SUPPORTED_SSL_BACKENDS AND NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten") if (NOT VCPKG_TOOLCHAIN) # ignore warning when using vcpkg message(WARNING "Could not determine cURL's SSL/TLS backends; if cURL is built with OpenSSL < 1.1.0 or GnuTLS < 2.11.0, this may result in thread-safety issues") endif() @@ -230,6 +234,140 @@ endif() # (WZ_APP_INSTALL_DEST is for the platform / generator-specific overrides *in* this file) set(WZ_APP_INSTALL_DEST "${CMAKE_INSTALL_BINDIR}") +####################### +# Emscripten Build Config + +if(CMAKE_SYSTEM_NAME MATCHES "Emscripten") + + # WZ requires WebGL 2 + target_link_options(warzone2100 PRIVATE "SHELL:-s MIN_WEBGL_VERSION=2") + target_link_options(warzone2100 PRIVATE "SHELL:-s MAX_WEBGL_VERSION=2") + + target_link_options(warzone2100 PRIVATE "SHELL:-s FETCH=1") + target_link_options(warzone2100 PRIVATE "SHELL:-s WASM_BIGINT") + + target_link_options(warzone2100 PRIVATE "SHELL:-s ENVIRONMENT=web,worker") # Worker must also be enabled because multithreading is enabled + + target_link_options(warzone2100 PRIVATE "SHELL:-s MINIFY_HTML=0") # Disable MINIFY_HTML + + # Stack and memory usage + target_link_options(warzone2100 PRIVATE "SHELL:-s TOTAL_STACK=10485760") + target_link_options(warzone2100 PRIVATE "SHELL:-s INITIAL_MEMORY=268435456") # 256 MB + target_link_options(warzone2100 PRIVATE "SHELL:-s ALLOW_MEMORY_GROWTH=1") + # NOTE: WZ's shell.html handles intelligently detecting and picking a maximum for the WebAssembly.Memory request + + target_link_options(warzone2100 PRIVATE "SHELL:-s DEMANGLE_SUPPORT=1") +# target_link_options(warzone2100 PRIVATE "SHELL:-s GL_ASSERTIONS=1") # Useful for debugging, but impacts performance +# target_link_options(warzone2100 PRIVATE "SHELL:-s LZ4=1") # LZ4 can't be used while also supporting gettext translations (no support for mmap in LZ4 filesystem backend) + #target_link_options(warzone2100 PRIVATE "SHELL:-s FULL_ES3=1") + target_link_options(warzone2100 PRIVATE "SHELL:-s PTHREAD_POOL_SIZE=8") + + # Needed for SDL2 + if ("${EMSCRIPTEN_VERSION}" VERSION_GREATER 3.1.50) + target_link_options(warzone2100 PRIVATE "SHELL:-s GL_ENABLE_GET_PROC_ADDRESS=1") + endif() + + # Display more information on linking issues + target_link_options(warzone2100 PRIVATE "SHELL:-s LLD_REPORT_UNDEFINED") + + # Offscreen canvas and proxy to pthread support + # - not currently supported - seems to lead to eventual freezes in Firefox and flickering in Safari + # target_link_options(warzone2100 PRIVATE "SHELL:-s PROXY_TO_PTHREAD=1") + # target_link_options(warzone2100 PRIVATE "SHELL:-s OFFSCREENCANVAS_SUPPORT=1") + + target_link_options(warzone2100 PRIVATE "SHELL:-lidbfs.js") # Explicitly include IDBFS, see: https://github.com/emscripten-core/emscripten/issues/9406 + + target_link_options(warzone2100 PRIVATE "SHELL:--shell-file ${PROJECT_SOURCE_DIR}/platforms/emscripten/shell.html") + target_link_options(warzone2100 PRIVATE "SHELL:--pre-js ${PROJECT_SOURCE_DIR}/platforms/emscripten/prejs.js") + target_link_options(warzone2100 PRIVATE "SHELL:--post-js ${PROJECT_SOURCE_DIR}/platforms/emscripten/postjs.js") + + target_link_options(warzone2100 PRIVATE "SHELL:-s MODULARIZE") + target_link_options(warzone2100 PRIVATE "SHELL:-s EXPORT_NAME=createWZModule") + target_link_options(warzone2100 PRIVATE "SHELL:-s EXIT_RUNTIME") + + # Bundle core data files + foreach(_data_file ${DATA_FILES}) + get_filename_component(_foldername "${_data_file}" NAME_WE) + message(STATUS "COPYING DATA: ${_foldername}") + target_link_options(warzone2100 PRIVATE "SHELL:--preload-file ${_data_file}@/data/${_foldername}/") + endforeach() + + # Bundle needed base fonts + foreach(_font_file ${DATA_FONTS}) + get_filename_component(_fontname "${_font_file}" NAME) + message(STATUS "COPYING FONT FILE: ${_fontname} (from: ${_font_file})") + target_link_options(warzone2100 PRIVATE "SHELL:--preload-file ${_font_file}@/data/fonts/") + endforeach() + + # Bundle the translations + if(ENABLE_NLS AND TARGET translations AND DEFINED wz2100_translations_LOCALE_FOLDER) + if (NOT WZ_LOCALEDIR_ISABSOLUTE) + message(FATAL_ERROR "Expected absolute path for WZ_LOCALEDIR: ${WZ_LOCALEDIR}") + endif() + target_link_options(warzone2100 PRIVATE "SHELL:--preload-file ${wz2100_translations_LOCALE_FOLDER}@${WZ_LOCALEDIR}/") + endif() + + # Cache preload data in browser (if possible) + target_link_options(warzone2100 PRIVATE "SHELL:--use-preload-cache") + + # Add dependencies on optional packages (if available) + if (TARGET data_music_empackage) + add_dependencies(warzone2100 data_music_empackage) + else() + message(WARNING "Missing data_music package - Emscripten build will not ship with music") + endif() + if (TARGET data_terrain_overrides_classic_packaging) + add_dependencies(warzone2100 data_terrain_overrides_classic_packaging) + else() + message(WARNING "Missing data_terrain_overrides_classic package - Emscripten build will not ship with classic terrain overrides") + endif() + + # Install the app files directly in the destination root + set(WZ_APP_INSTALL_DEST ".") + + # Rename output warzone2100.html -> index.html + add_custom_command(TARGET warzone2100 POST_BUILD + COMMAND ${CMAKE_COMMAND} -E rename "$" "$/index.html" + VERBATIM) + + # Copy additional (optional) file packages to build dir to allow direct-running + foreach(_empackage_dir IN LISTS DATA_ADDITIONAL_EMPACKAGE_DIRS) + file(RELATIVE_PATH _empackage_dir_subdir_path "${DATA_ADDITIONAL_EMPACKAGE_BASEDIR}" "${_empackage_dir}") + add_custom_command(TARGET warzone2100 POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory_if_different "${_empackage_dir}" "$/pkg/${_empackage_dir_subdir_path}" + VERBATIM) + endforeach() + + # If workbox-cli is available, generate a proper service worker + set(GENERATED_SERVICE_WORKER FALSE) + find_package(WorkboxCLI) + if (WorkboxCLI_FOUND) + add_custom_target(generate_service_worker ALL + COMMAND ${CMAKE_COMMAND} -P "${CMAKE_SOURCE_DIR}/platforms/emscripten/cmake/WorkboxRemoveOldFiles.cmake" + COMMAND ${WorkboxCLI_COMMAND} generateSW "${CMAKE_SOURCE_DIR}/platforms/emscripten/wz-workbox-config.js" + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + BYPRODUCTS "${CMAKE_CURRENT_BINARY_DIR}/service-worker.js" + VERBATIM + ) + add_dependencies(generate_service_worker warzone2100) + set(GENERATED_SERVICE_WORKER TRUE) + endif() + # Otherwise, copy the no-op-service-worker.js -> service-worker.js + if (NOT GENERATED_SERVICE_WORKER) + message(WARNING "workbox-cli is not available - will use a no-op service worker") + # configure_file("${CMAKE_SOURCE_DIR}/platforms/emscripten/no-op-service-worker.js" "${CMAKE_CURRENT_BINARY_DIR}/service-worker.js" COPYONLY) + add_custom_target(generate_service_worker ALL + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/platforms/emscripten/no-op-service-worker.js" "${CMAKE_CURRENT_BINARY_DIR}/service-worker.js" + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + DEPENDS "${CMAKE_SOURCE_DIR}/platforms/emscripten/no-op-service-worker.js" + BYPRODUCTS "${CMAKE_CURRENT_BINARY_DIR}/service-worker.js" + VERBATIM + ) + add_dependencies(generate_service_worker warzone2100) + endif() + +endif() + ####################### # macOS Build Config @@ -583,7 +721,7 @@ if(CMAKE_SYSTEM_NAME MATCHES "Windows") endif() endif() -if(NOT CMAKE_SYSTEM_NAME MATCHES "Darwin" AND NOT MSVC) +if(NOT CMAKE_SYSTEM_NAME MATCHES "(Darwin|Emscripten)" AND NOT MSVC) # Ensure noexecstack ADD_TARGET_LINK_FLAGS_IF_SUPPORTED(TARGET warzone2100 LINK_FLAGS "-Wl,-z,noexecstack" CACHED_RESULT_NAME LINK_FLAG_WL_Z_NOEXECSTACK_SUPPORTED) # Enable RELRO (if supported) @@ -645,10 +783,103 @@ endif() ####################### # Install -install(TARGETS warzone2100 COMPONENT Core DESTINATION "${WZ_APP_INSTALL_DEST}") -# For Portable packages only, copy the ".portable" file that triggers portable mode (Windows-only) -install(FILES "${CMAKE_SOURCE_DIR}/pkg/portable.in" COMPONENT PortableConfig DESTINATION "${WZ_APP_INSTALL_DEST}" RENAME ".portable" EXCLUDE_FROM_ALL) +if(NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten") + + # Executable + install(TARGETS warzone2100 COMPONENT Core DESTINATION "${WZ_APP_INSTALL_DEST}") + + # For Portable packages only, copy the ".portable" file that triggers portable mode (Windows-only) + install(FILES "${CMAKE_SOURCE_DIR}/pkg/portable.in" COMPONENT PortableConfig DESTINATION "${WZ_APP_INSTALL_DEST}" RENAME ".portable" EXCLUDE_FROM_ALL) + +else() + + # For Emscripten: install the generated files + install(FILES + "$/index.html" + COMPONENT Core + DESTINATION "${WZ_APP_INSTALL_DEST}" + RENAME "index.html") + install(FILES + "$/warzone2100.js" + "$/warzone2100.worker.js" + "$/warzone2100.wasm" + "$/warzone2100.data" + COMPONENT Core + DESTINATION "${WZ_APP_INSTALL_DEST}") + + # Install manifest file + configure_file("${CMAKE_SOURCE_DIR}/platforms/emscripten/manifest.json" "${CMAKE_CURRENT_BINARY_DIR}/manifest.json" COPYONLY) + install(FILES + "${CMAKE_SOURCE_DIR}/platforms/emscripten/manifest.json" + COMPONENT Core + DESTINATION "${WZ_APP_INSTALL_DEST}") + + # Install static assets + file(GLOB static_assets "${CMAKE_SOURCE_DIR}/platforms/emscripten/assets/*.*") + if (static_assets) + install(DIRECTORY DESTINATION "${WZ_APP_INSTALL_DEST}/assets" COMPONENT Core) + install(FILES + ${static_assets} + COMPONENT Core + DESTINATION "${WZ_APP_INSTALL_DEST}/assets") + # Also place assets in build dir so it can be run directly + foreach(asset IN LISTS static_assets) + configure_file("${asset}" "${CMAKE_CURRENT_BINARY_DIR}/" COPYONLY) + endforeach() + endif() + + # Install additional (optional) file packages + foreach(_empackage_dir IN LISTS DATA_ADDITIONAL_EMPACKAGE_DIRS) + install(DIRECTORY + ${_empackage_dir} + COMPONENT Core + DESTINATION "${WZ_APP_INSTALL_DEST}/pkg") + endforeach() + + # Install service worker (and associated files) + install(CODE " + file(GLOB _service_worker_files LIST_DIRECTORIES false \"${CMAKE_CURRENT_BINARY_DIR}/service-worker.js\" \"${CMAKE_CURRENT_BINARY_DIR}/workbox-*.js\") + list(LENGTH _service_worker_files _num_service_worker_files) + if (_num_service_worker_files GREATER 0) + foreach(_input_file IN LISTS _service_worker_files) + file(INSTALL \"\${_input_file}\" DESTINATION \"\${CMAKE_INSTALL_PREFIX}/${WZ_APP_INSTALL_DEST}\") + endforeach() + else() + message(WARNING \"Did not find any service worker files in: ${CMAKE_CURRENT_BINARY_DIR}/\") + endif() + " COMPONENT Core) + + if (WZ_EMSCRIPTEN_COMPRESS_OUTPUT AND NOT CMAKE_VERSION VERSION_LESS "3.18.0") + install(CODE " + execute_process(COMMAND \${CMAKE_COMMAND} -E echo \"++install CODE: CMAKE_PREFIX_PATH: ${CMAKE_PREFIX_PATH}\") + + file(GLOB _files_to_compress LIST_DIRECTORIES false + \"\${CMAKE_INSTALL_PREFIX}/${WZ_APP_INSTALL_DEST}/*.js\" + \"\${CMAKE_INSTALL_PREFIX}/${WZ_APP_INSTALL_DEST}/*.wasm\" + \"\${CMAKE_INSTALL_PREFIX}/${WZ_APP_INSTALL_DEST}/*.data\" + \"\${CMAKE_INSTALL_PREFIX}/${WZ_APP_INSTALL_DEST}/*.html\" + \"\${CMAKE_INSTALL_PREFIX}/${WZ_APP_INSTALL_DEST}/pkg/music/*.js\" + \"\${CMAKE_INSTALL_PREFIX}/${WZ_APP_INSTALL_DEST}/pkg/terrain_overrides/*.js\" + \"\${CMAKE_INSTALL_PREFIX}/${WZ_APP_INSTALL_DEST}/pkg/terrain_overrides/*.data\" + ) + + list(LENGTH _files_to_compress _num_files_to_compress) + if (_num_files_to_compress GREATER 0) + foreach(_input_file IN LISTS _files_to_compress) + file(RELATIVE_PATH _input_file_relative_path \"\${CMAKE_INSTALL_PREFIX}/${WZ_APP_INSTALL_DEST}\" \"\${_input_file}\") + get_filename_component(_input_file_subdir_path \"\${_input_file_relative_path}\" DIRECTORY) + get_filename_component(_input_file_filename \"\${_input_file}\" NAME) + execute_process(COMMAND \${CMAKE_COMMAND} -E echo \"++install CODE: Compressing file: \${_input_file_subdir_path}/\${_input_file_filename} -> \${_input_file_subdir_path}/\${_input_file_filename}.gz\") + file(ARCHIVE_CREATE OUTPUT \"\${CMAKE_INSTALL_PREFIX}/${WZ_APP_INSTALL_DEST}/\${_input_file_subdir_path}/\${_input_file_filename}.gz\" PATHS \"\${_input_file}\" FORMAT raw COMPRESSION GZip COMPRESSION_LEVEL 7) + endforeach() + else() + message(WARNING \"Did not find any files to compress in: \${CMAKE_INSTALL_PREFIX}/${WZ_APP_INSTALL_DEST}/\") + endif() + " COMPONENT Core) + endif() + +endif() ##################### # Installing Required Runtime Dependencies diff --git a/src/configuration.cpp b/src/configuration.cpp index 8955a289004..cbd7e3d9dc0 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -425,7 +425,7 @@ bool loadConfig() NETsetDefaultMPHostFreeChatPreference(iniGetBool("hostingChatDefault", NETgetDefaultMPHostFreeChatPreference()).value()); setPublicIPv4LookupService(iniGetString("publicIPv4LookupService_Url", WZ_DEFAULT_PUBLIC_IPv4_LOOKUP_SERVICE_URL).value(), iniGetString("publicIPv4LookupService_JSONKey", WZ_DEFAULT_PUBLIC_IPv4_LOOKUP_SERVICE_JSONKEY).value()); setPublicIPv6LookupService(iniGetString("publicIPv6LookupService_Url", WZ_DEFAULT_PUBLIC_IPv6_LOOKUP_SERVICE_URL).value(), iniGetString("publicIPv6LookupService_JSONKey", WZ_DEFAULT_PUBLIC_IPv6_LOOKUP_SERVICE_JSONKEY).value()); - war_SetFMVmode((FMV_MODE)iniGetInteger("FMVmode", FMV_FULLSCREEN).value()); + war_SetFMVmode((FMV_MODE)iniGetInteger("FMVmode", war_GetFMVmode()).value()); war_setScanlineMode((SCANLINE_MODE)iniGetInteger("scanlines", SCANLINES_OFF).value()); seq_SetSubtitles(iniGetBool("subtitles", true).value()); setDifficultyLevel((DIFFICULTY_LEVEL)iniGetInteger("difficulty", DL_NORMAL).value()); diff --git a/src/display3d.cpp b/src/display3d.cpp index e1e52ccad76..685d27bff27 100644 --- a/src/display3d.cpp +++ b/src/display3d.cpp @@ -1190,6 +1190,9 @@ void draw3DScene() # pragma GCC diagnostic pop #endif } +#if defined(__EMSCRIPTEN__) + abort(); +#endif exit(-1); // should never reach this, but just in case... } //visualize radius if needed diff --git a/src/emscripten_helpers.cpp b/src/emscripten_helpers.cpp new file mode 100644 index 00000000000..0f51840b569 --- /dev/null +++ b/src/emscripten_helpers.cpp @@ -0,0 +1,126 @@ +/* + This file is part of Warzone 2100. + Copyright (C) 2022-2024 Warzone 2100 Project + + Warzone 2100 is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + Warzone 2100 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Warzone 2100; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#if defined(__EMSCRIPTEN__) + +#include "emscripten_helpers.h" +#include +#include +#include +#include +#include "lib/ivis_opengl/gfx_api.h" + +/* Older Emscriptens don't have this, but it's needed for wasm64 compatibility. */ +#ifndef MAIN_THREAD_EM_ASM_PTR + #ifdef __wasm64__ + #error You need to upgrade your Emscripten compiler to support wasm64 + #else + #define MAIN_THREAD_EM_ASM_PTR MAIN_THREAD_EM_ASM_INT + #endif +#endif + +EM_JS_DEPS(wz2100emhelpers, "$stringToUTF8,$UTF8ToString"); + +static std::string windowLocationURL; +static WzString emBottomRendererSystemInfoText; + +std::string WZ_GetEmscriptenWindowLocationURL() +{ + return windowLocationURL; +} + +void initWZEmscriptenHelpers() +{ + // Get window location URL + char *str = (char*)MAIN_THREAD_EM_ASM_PTR({ + let jsString = window.location.href; + let lengthBytes = lengthBytesUTF8(jsString) + 1; + let stringOnWasmHeap = _malloc(lengthBytes); + stringToUTF8(jsString, stringOnWasmHeap, lengthBytes); + return stringOnWasmHeap; + }); + windowLocationURL = (str) ? str : ""; + free(str); // Each call to _malloc() must be paired with free(), or heap memory will leak! +} + +void initWZEmscriptenHelpers_PostInit() +{ + // Generate bottom renderer + system info text + emBottomRendererSystemInfoText.clear(); + if (gfx_api::context::isInitialized()) + { + emBottomRendererSystemInfoText = WzString::fromUtf8(gfx_api::context::get().getFormattedRendererInfoString()); + } + else + { + ASSERT(gfx_api::context::isInitialized(), "Function called before gfx context initialized"); + emBottomRendererSystemInfoText = "WebGL"; + } + emBottomRendererSystemInfoText.append(" | Available Memory: "); + emBottomRendererSystemInfoText.append(WzString::number(WZ_EmscriptenGetMaximumMemoryMiB())); + emBottomRendererSystemInfoText.append(" MiB"); +} + +unsigned int WZ_EmscriptenGetMaximumMemoryMiB() +{ + int result = MAIN_THREAD_EM_ASM_INT({ + if (typeof wz_js_get_maximum_memory_mib !== "function") { + return 0; + } + return wz_js_get_maximum_memory_mib(); + }); + return static_cast(std::max(result, 0)); +} + +const WzString& WZ_EmscriptenGetBottomRendererSysInfoString() +{ + return emBottomRendererSystemInfoText; +} + +void WZ_EmscriptenSyncPersistFSChanges(bool isUserInitiatedSave) +{ + emscripten_runtime_keepalive_push(); // Must be used so that onExit handlers aren't called + emscripten_pause_main_loop(); + + // Note: Module.resumeMainLoop() is equivalent to emscripten_resume_main_loop() + MAIN_THREAD_EM_ASM({ + if (typeof wz_js_display_saving_indicator === "function") { + wz_js_display_saving_indicator(true); + } + let handleFinished = function() { + if (typeof wz_js_display_saving_indicator === "function") { + wz_js_display_saving_indicator(false); + } + Module.resumeMainLoop(); + runtimeKeepalivePop(); + }; + try { + wz_js_save_config_dir_to_persistent_storage($0, () => { + handleFinished(); + }); + } + catch (error) { + console.error(error); + // Always resume the main loop on error + handleFinished(); + } + }, isUserInitiatedSave); +} + +#endif diff --git a/src/emscripten_helpers.h b/src/emscripten_helpers.h new file mode 100644 index 00000000000..faa60809104 --- /dev/null +++ b/src/emscripten_helpers.h @@ -0,0 +1,37 @@ +/* + This file is part of Warzone 2100. + Copyright (C) 2022-2024 Warzone 2100 Project + + Warzone 2100 is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + Warzone 2100 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Warzone 2100; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#if defined(__EMSCRIPTEN__) + +#include +#include "lib/framework/wzstring.h" + +std::string WZ_GetEmscriptenWindowLocationURL(); +const WzString& WZ_EmscriptenGetBottomRendererSysInfoString(); + +unsigned int WZ_EmscriptenGetMaximumMemoryMiB(); +void WZ_EmscriptenSyncPersistFSChanges(bool isUserInitiatedSave); + +// must be called on the main thread +void initWZEmscriptenHelpers(); +void initWZEmscriptenHelpers_PostInit(); + +#endif diff --git a/src/frontend.cpp b/src/frontend.cpp index 20265e0ac25..4229f5bcb68 100644 --- a/src/frontend.cpp +++ b/src/frontend.cpp @@ -86,6 +86,10 @@ #include +#if defined(__EMSCRIPTEN__) +# include "emscripten_helpers.h" +#endif + // //////////////////////////////////////////////////////////////////////////// // Global variables @@ -271,6 +275,9 @@ bool runTitleMenu() break; case FRONTEND_MULTIPLAYER: changeTitleMode(MULTI); +#if defined(__EMSCRIPTEN__) + wzDisplayDialog(Dialog_Information, "Multiplayer requires the native version.", "The web version of Warzone 2100 does not support online multiplayer. Please visit https://wz2100.net to download the native version for your platform."); +#endif break; case FRONTEND_SINGLEPLAYER: changeTitleMode(SINGLE); @@ -621,8 +628,10 @@ void startMultiPlayerMenu() addSideText(FRONTEND_SIDETEXT , FRONTEND_SIDEX, FRONTEND_SIDEY, _("MULTI PLAYER")); +#if !defined(__EMSCRIPTEN__) addTextButton(FRONTEND_HOST, FRONTEND_POS2X, FRONTEND_POS2Y, _("Host Game"), WBUT_TXTCENTRE); addTextButton(FRONTEND_JOIN, FRONTEND_POS3X, FRONTEND_POS3Y, _("Join Game"), WBUT_TXTCENTRE); +#endif addTextButton(FRONTEND_REPLAY, FRONTEND_POS7X, FRONTEND_POS7Y, _("View Replay"), WBUT_TXTCENTRE); addMultiBut(psWScreen, FRONTEND_BOTFORM, FRONTEND_QUIT, 10, 10, 30, 29, P_("menu", "Return"), IMAGE_RETURN, IMAGE_RETURN_HI, IMAGE_RETURN_HI); @@ -3521,7 +3530,11 @@ static void displayTitleBitmap(WZ_DECL_UNUSED WIDGET *psWidget, WZ_DECL_UNUSED U cache.formattedVersionString.setText(version_getFormattedVersionString(), font_regular); cache.modListText.setText(modListText, font_regular); +#if defined(__EMSCRIPTEN__) + cache.gfxBackend.setText(WZ_EmscriptenGetBottomRendererSysInfoString(), font_small); +#else cache.gfxBackend.setText(WzString::fromUtf8(gfx_api::context::get().getFormattedRendererInfoString()), font_small); +#endif cache.formattedVersionString.render(pie_GetVideoBufferWidth() - 9, pie_GetVideoBufferHeight() - 14, WZCOL_GREY, 270.f); diff --git a/src/game.cpp b/src/game.cpp index f6e97f97c34..24596f2ef48 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -94,7 +94,6 @@ #include "multigifts.h" #include "wzscriptdebug.h" #include "gamehistorylogger.h" -#include "build_tools/autorevision.h" #include #if defined(__clang__) @@ -115,6 +114,10 @@ # pragma GCC diagnostic ignored "-Wunused-function" #endif +#if defined(__EMSCRIPTEN__) +# include "emscripten_helpers.h" +#endif + bool saveJSONToFile(const nlohmann::json& obj, const char* pFileName) { std::ostringstream stream; @@ -3359,7 +3362,7 @@ bool loadGame(const char *pGameToLoad, bool keepObjects, bool freeMem, bool User } // ----------------------------------------------------------------------------------------- -bool saveGame(const char *aFileName, GAME_TYPE saveType) +bool saveGame(const char *aFileName, GAME_TYPE saveType, bool isAutoSave) { size_t fileExtension; char CurrentFileName[PATH_MAX] = {'\0'}; @@ -3645,6 +3648,10 @@ bool saveGame(const char *aFileName, GAME_TYPE saveType) // strip the last filename CurrentFileName[fileExtension - 1] = '\0'; +#if defined(__EMSCRIPTEN__) + WZ_EmscriptenSyncPersistFSChanges(!isAutoSave); // NOTE: Will block main loop iterations until it finishes (asynchronously) +#endif + /* Start the game clock */ triggerEvent(TRIGGER_GAME_SAVED); gameTimeStart(); diff --git a/src/game.h b/src/game.h index 3cb8052e56a..f7d445d6eb9 100644 --- a/src/game.h +++ b/src/game.h @@ -55,7 +55,7 @@ bool loadScriptState(char *pFileName); /// Load the terrain types bool loadTerrainTypeMap(const char *pFilePath); -bool saveGame(const char *aFileName, GAME_TYPE saveType); +bool saveGame(const char *aFileName, GAME_TYPE saveType, bool isAutoSave = false); // Get the campaign number for loadGameInit game UDWORD getCampaign(const char *fileName); @@ -70,4 +70,9 @@ void gameScreenSizeDidChange(unsigned int oldWidth, unsigned int oldHeight, unsi void gameDisplayScaleFactorDidChange(float newDisplayScaleFactor); nonstd::optional parseJsonFile(const char *filename); bool saveJSONToFile(const nlohmann::json& obj, const char* pFileName); + +#if defined(__EMSCRIPTEN__) +void wz_emscripten_did_finish_render(unsigned int browserRenderDelta); +#endif + #endif // __INCLUDED_SRC_GAME_H__ diff --git a/src/loadsave.cpp b/src/loadsave.cpp index ffb0980d6a7..2f72a548179 100644 --- a/src/loadsave.cpp +++ b/src/loadsave.cpp @@ -1161,7 +1161,7 @@ bool autoSave() std::string suggestedName = suggestSaveName(dir).toStdString(); char savefile[PATH_MAX]; snprintf(savefile, sizeof(savefile), "%s/%s_%s.gam", dir, suggestedName.c_str(), savedate); - if (saveGame(savefile, GTYPE_SAVE_MIDMISSION)) + if (saveGame(savefile, GTYPE_SAVE_MIDMISSION, true)) { console(_("AutoSave %s"), savegameWithoutExtension(savefile)); return true; diff --git a/src/loop.cpp b/src/loop.cpp index 8b50b544dc7..9d64d8759b8 100644 --- a/src/loop.cpp +++ b/src/loop.cpp @@ -603,6 +603,24 @@ void setMaxFastForwardTicks(optional value, bool fixedToNormalTickRate) fastForwardTicksFixedToNormalTickRate = fixedToNormalTickRate; } +static int renderBudget = 0; // Scaled time spent rendering minus scaled time spent updating. +const Rational renderFraction(2, 5); // Minimum fraction of time spent rendering. +const Rational updateFraction = Rational(1) - renderFraction; + +#if defined(__EMSCRIPTEN__) +unsigned lastRenderDelta = 0; +void wz_emscripten_did_finish_render(unsigned int browserRenderDelta) +{ + if (GetGameMode() != GS_NORMAL) + { + return; + } + renderBudget += (lastRenderDelta + browserRenderDelta) * updateFraction.n; + renderBudget = std::min(renderBudget, (renderFraction * 500).floor()); + lastRenderDelta = 0; +} +#endif + /* The main game loop */ GAMECODE gameLoop() { @@ -610,10 +628,7 @@ GAMECODE gameLoop() static uint32_t lastFlushTime = 0; static size_t numForcedUpdatesLastCall = 0; - static int renderBudget = 0; // Scaled time spent rendering minus scaled time spent updating. static bool previousUpdateWasRender = false; - const Rational renderFraction(2, 5); // Minimum fraction of time spent rendering. - const Rational updateFraction = Rational(1) - renderFraction; // Shouldn't this be when initialising the game, rather than randomly called between ticks? countUpdate(false); // kick off with correct counts @@ -683,8 +698,12 @@ GAMECODE gameLoop() pie_ScreenFrameRenderEnd(); // must happen here for proper renderBudget calculation unsigned after = wzGetTicks(); +#if defined(__EMSCRIPTEN__) + lastRenderDelta = (after - before); +#else renderBudget += (after - before) * updateFraction.n; renderBudget = std::min(renderBudget, (renderFraction * 500).floor()); +#endif previousUpdateWasRender = true; if (headlessGameMode() && autogame_enabled()) diff --git a/src/main.cpp b/src/main.cpp index 1d78634f109..295eac1640e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1619,6 +1619,10 @@ void osSpecificPostInit_Win() } #endif /* defined(WZ_OS_WIN) */ +#if defined(__EMSCRIPTEN__) +# include "emscripten_helpers.h" +#endif + void osSpecificFirstChanceProcessSetup() { #if defined(WZ_OS_WIN) @@ -1637,12 +1641,18 @@ void osSpecificFirstChanceProcessSetup() #else // currently, no-op #endif + +#if defined(__EMSCRIPTEN__) // must be separate, because WZ_OS_UNIX is also defined for emscripten builds + initWZEmscriptenHelpers(); +#endif } void osSpecificPostInit() { #if defined(WZ_OS_WIN) osSpecificPostInit_Win(); +#elif defined(__EMSCRIPTEN__) + initWZEmscriptenHelpers_PostInit(); #else // currently, no-op #endif @@ -1838,7 +1848,11 @@ int realmain(int argc, char *argv[]) osSpecificFirstChanceProcessSetup(); debug_init(); +#if defined(__EMSCRIPTEN__) + debug_register_callback(debug_callback_emscripten_log, nullptr, nullptr, nullptr); +#else debug_register_callback(debug_callback_stderr, nullptr, nullptr, nullptr); +#endif #if defined(_WIN32) && defined(DEBUG_INSANE) debug_register_callback(debug_callback_win32debug, NULL, NULL, NULL); #endif // WZ_OS_WIN && DEBUG_INSANE diff --git a/src/seqdisp.cpp b/src/seqdisp.cpp index ea153bcb78a..ab618060940 100644 --- a/src/seqdisp.cpp +++ b/src/seqdisp.cpp @@ -148,6 +148,9 @@ class OnDemandVideoDownloader // Returns whether an error occurred trying to get the video data bool getVideoDataError(const WzString& videoName); + // Cancel video download + bool cancelDownloadRequest(const WzString& videoName); + // Clear all cached requests void clear(); @@ -180,6 +183,7 @@ class OnDemandVideoDownloader private: AsyncURLRequestHandle requestHandle; friend bool OnDemandVideoDownloader::requestVideoData(const WzString& videoName); + friend bool OnDemandVideoDownloader::cancelDownloadRequest(const WzString& videoName); }; std::unordered_map> priorRequests; optional baseURLPath; @@ -327,6 +331,22 @@ bool OnDemandVideoDownloader::getVideoDataError(const WzString& videoName) return false; } +// Cancel video download request +bool OnDemandVideoDownloader::cancelDownloadRequest(const WzString& videoName) +{ + auto it = priorRequests.find(videoName); + if (it == priorRequests.end()) + { + return false; + } + if (it->second->requestHandle) + { + urlRequestSetCancelFlag(it->second->requestHandle); + } + priorRequests.erase(it); + return true; +} + void OnDemandVideoDownloader::clear() { priorRequests.clear(); @@ -709,6 +729,8 @@ bool seq_StopFullScreenVideo() loop_ClearVideoPlaybackMode(); } + onDemandVideoProvider.cancelDownloadRequest(currVideoName); + seq_Shutdown(); wzCachedSeqText.clear(); diff --git a/src/stdinreader.cpp b/src/stdinreader.cpp index a9484976b02..cdbe6d5cff1 100644 --- a/src/stdinreader.cpp +++ b/src/stdinreader.cpp @@ -109,7 +109,7 @@ int quitSignalEventFd = -1; int quitSignalPipeFds[2] = {-1, -1}; #endif -#if !defined(_WIN32) && (defined(HAVE_SYS_EVENTFD_H) || defined(HAVE_UNISTD_H)) +#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && (defined(HAVE_SYS_EVENTFD_H) || defined(HAVE_UNISTD_H)) # define WZ_STDIN_READER_SUPPORTED #endif diff --git a/src/updatemanager.cpp b/src/updatemanager.cpp index 8dbe7657715..c29a8eca050 100644 --- a/src/updatemanager.cpp +++ b/src/updatemanager.cpp @@ -26,6 +26,7 @@ using json = nlohmann::json; #include #include #include +#include #include "lib/framework/wzglobal.h" // required for config.h #include "lib/framework/frame.h" @@ -529,6 +530,11 @@ ProcessResult WzUpdateManager::processUpdateJSONFile(const json& updateData, boo void WzUpdateManager::initUpdateCheck() { std::vector updateDataUrls = {"https://data.wz2100.net/wz2100.json", "https://warzone2100.github.io/update-data/wz2100.json"}; +#if defined(__EMSCRIPTEN__) + // Bypass browser cache (if needed) by appending a query string parameter + std::string queryStringParam = std::to_string(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()); + updateDataUrls.insert(updateDataUrls.begin() + 1, "https://data.wz2100.net/wz2100.json?v=" + queryStringParam); +#endif initProcessData(updateDataUrls, WzUpdateManager::processUpdateJSONFile, updatesCachePaths, nullptr); } @@ -669,6 +675,11 @@ ProcessResult WzCompatCheckManager::processCompatCheckJSONFile(const json& updat void WzCompatCheckManager::initCompatCheck() { std::vector updateDataUrls = {"https://data.wz2100.net/wz2100_compat.json", "https://warzone2100.github.io/update-data/wz2100_compat.json"}; +#if defined(__EMSCRIPTEN__) + // Bypass browser cache (if needed) by appending a query string parameter + std::string queryStringParam = std::to_string(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()); + updateDataUrls.insert(updateDataUrls.begin() + 1, "https://data.wz2100.net/wz2100_compat.json?v=" + queryStringParam); +#endif initProcessData(updateDataUrls, WzCompatCheckManager::processCompatCheckJSONFile, compatCachePaths, []() { // set an unsuccessful result (if no prior result set) setCompatCheckResults(CompatCheckResults(false), true); diff --git a/src/urlhelpers.cpp b/src/urlhelpers.cpp index 3ef201c84c3..c1c6823fca9 100644 --- a/src/urlhelpers.cpp +++ b/src/urlhelpers.cpp @@ -206,6 +206,16 @@ bool openURLInBrowser(char const *url) return succeededOpeningUrl; } +#if defined(__EMSCRIPTEN__) + +std::string urlEncode(const char* urlFragment) +{ + // TODO: Implement? + return urlFragment; +} + +#else // !defined(__EMSCRIPTEN__) + #include std::string urlEncode(const char* urlFragment) @@ -239,6 +249,8 @@ std::string urlEncode(const char* urlFragment) return result; } +#endif // defined(__EMSCRIPTEN__) + bool openFolderInDefaultFileManager(const char* path) { #if defined(WZ_OS_WIN) diff --git a/src/urlrequest_emscripten.cpp b/src/urlrequest_emscripten.cpp new file mode 100644 index 00000000000..47aea771c01 --- /dev/null +++ b/src/urlrequest_emscripten.cpp @@ -0,0 +1,572 @@ +/* + This file is part of Warzone 2100. + Copyright (C) 2022 Warzone 2100 Project + + Warzone 2100 is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + Warzone 2100 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Warzone 2100; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#if defined(__EMSCRIPTEN__) + +#include "urlrequest.h" +#include "urlrequest_private.h" +#include "lib/framework/frame.h" +#include "lib/framework/wzapp.h" +#include +#include + +#include + +#define MAXIMUM_DOWNLOAD_SIZE 2147483647L +#define MAXIMUM_IN_MEMORY_DOWNLOAD_SIZE 1 << 29 // 512 MB, default max download limit + +class URLTransferRequest; + +static WZ_MUTEX *urlRequestMutex = nullptr; +static std::list> activeURLRequests; + +struct AsyncRequestImpl : public AsyncRequest +{ +public: + AsyncRequestImpl(const std::shared_ptr& request) + : weakRequest(request) + { } +public: + std::weak_ptr weakRequest; +}; + +class EmFetchHTTPResponseDetails : public HTTPResponseDetails { +public: + EmFetchHTTPResponseDetails(bool fetchResult, long httpStatusCode, std::shared_ptr responseHeaders) + : HTTPResponseDetails(httpStatusCode, responseHeaders) + , _fetchResult(fetchResult) + { } + virtual ~EmFetchHTTPResponseDetails() + { } + + std::string getInternalResultDescription() const override + { + return (_fetchResult) ? "Success" : "Returned Error"; + } + +private: + bool _fetchResult; +}; + +void wz_fetch_success(emscripten_fetch_t *fetch); +void wz_fetch_failure(emscripten_fetch_t *fetch); +void wz_fetch_onProgress(emscripten_fetch_t *fetch); +void wz_fetch_onReadyStateChange(emscripten_fetch_t *fetch); + +class URLTransferRequest : public std::enable_shared_from_this +{ +public: + emscripten_fetch_t *handle = nullptr; + + struct FetchRequestHeaders + { + private: + std::vector fetchHeadersLayout; + public: + FetchRequestHeaders(const std::unordered_map& requestHeaders) + { + for (auto it : requestHeaders) + { + const auto& header_key = it.first; + const auto& header_value = it.second; + fetchHeadersLayout.push_back(strdup(it.first.c_str())); + fetchHeadersLayout.push_back(strdup(it.second.c_str())); + } + fetchHeadersLayout.push_back(0); + } + ~FetchRequestHeaders() + { + for (auto& value : fetchHeadersLayout) + { + if (value) + { + free(value); + } + } + fetchHeadersLayout.clear(); + } + + // FetchRequestHeaders is non-copyable + FetchRequestHeaders(const FetchRequestHeaders&) = delete; + FetchRequestHeaders& operator=(const FetchRequestHeaders&) = delete; + + // FetchRequestHeaders is movable + FetchRequestHeaders(FetchRequestHeaders&& other) = default; + FetchRequestHeaders& operator=(FetchRequestHeaders&& other) = default; + public: + const char* const * getPointer() + { + return fetchHeadersLayout.data(); + } + }; + +public: + URLTransferRequest() + { } + + virtual ~URLTransferRequest() + { } + + void cancel() + { + deliberatelyCancelled = true; + emscripten_fetch_close(handle); + handle = nullptr; + } + + virtual const std::string& url() const = 0; + virtual const char* requestMethod() const { return "GET"; } + virtual InternetProtocol protocol() const = 0; + virtual bool noProxy() const = 0; + virtual const std::unordered_map& requestHeaders() const = 0; + virtual uint64_t maxDownloadSize() const { return MAXIMUM_DOWNLOAD_SIZE; } + + virtual emscripten_fetch_t* initiateFetch() + { + // Create emscripten fetch attr struct + emscripten_fetch_attr_t attr; + emscripten_fetch_attr_init(&attr); + + // Set request method + strcpy(attr.requestMethod, requestMethod()); + + // Always load to memory + attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY; + + if (waitOnShutdown()) + { + attr.attributes |= EMSCRIPTEN_FETCH_WAITABLE; + } + + if (noProxy()) + { + wzAsyncExecOnMainThread([]{ + debug(LOG_NET, "Fetch: NOPROXY is not supported"); + }); + } + + formattedRequestHeaders = std::make_shared(requestHeaders()); + attr.requestHeaders = formattedRequestHeaders->getPointer(); + + // Set callbacks + attr.onsuccess = wz_fetch_success; + attr.onerror = wz_fetch_failure; + attr.onprogress = wz_fetch_onProgress; + attr.onreadystatechange = wz_fetch_onReadyStateChange; + + // Set userdata to point to this + attr.userData = (void*)this; + + // Create handle + handle = emscripten_fetch(&attr, url().c_str()); + + return handle; + } + + bool wasCancelled() const { return deliberatelyCancelled; } + + virtual bool hasWriteMemoryCallback() { return false; } + virtual bool writeMemoryCallback(const void *contents, uint64_t numBytes, uint64_t dataOffset) { return false; } + + virtual bool onProgressUpdate(uint64_t dltotal, uint64_t dlnow) { return false; } + + virtual bool waitOnShutdown() const { return false; } + + virtual void handleRequestSuccess(unsigned short status) { } + virtual void handleRequestError(unsigned short status) { } + virtual void requestFailedToFinish(URLRequestFailureType type) { } + +protected: + friend void wz_fetch_onReadyStateChange(emscripten_fetch_t *fetch); + std::shared_ptr responseHeaders = std::make_shared(); +private: + bool deliberatelyCancelled = false; + std::shared_ptr formattedRequestHeaders; +}; + +void wz_fetch_success(emscripten_fetch_t *fetch) +{ + std::shared_ptr pSharedRequest; + if (fetch->userData) + { + URLTransferRequest* pRequest = static_cast(fetch->userData); + + if (pRequest->hasWriteMemoryCallback()) + { + // Need to pass the downloaded block to the callback + pRequest->writeMemoryCallback(fetch->data, fetch->numBytes, fetch->dataOffset); + } + + pRequest->handleRequestSuccess(fetch->status); + + // now remove from the list of activeURLRequests + pSharedRequest = pRequest->shared_from_this(); + wzMutexLock(urlRequestMutex); + activeURLRequests.remove(pSharedRequest); + wzMutexUnlock(urlRequestMutex); + } + + emscripten_fetch_close(fetch); +} + +void wz_fetch_failure(emscripten_fetch_t *fetch) +{ + if (!fetch) return; + bool wasCancelled = false; + std::shared_ptr pSharedRequest; + if (fetch->userData) + { + URLTransferRequest* pRequest = static_cast(fetch->userData); + wasCancelled = pRequest->wasCancelled(); + if (!wasCancelled) + { + // handle the error + pRequest->handleRequestError(fetch->status); + } + + // now remove from the list of activeURLRequests + pSharedRequest = pRequest->shared_from_this(); + wzMutexLock(urlRequestMutex); + activeURLRequests.remove(pSharedRequest); + wzMutexUnlock(urlRequestMutex); + } + + if (!wasCancelled) // until emscripten bug is fixed, can't call emscripten_fetch_close from within error handler if already triggered by cancellation + { + emscripten_fetch_close(fetch); + } +} + +void wz_fetch_onProgress(emscripten_fetch_t *fetch) +{ + if (!fetch) return; + if (fetch->status != 200) return; + if (fetch->userData == nullptr) return; + + URLTransferRequest* pRequest = static_cast(fetch->userData); + pRequest->onProgressUpdate(fetch->totalBytes /* NOTE: May be 0 */, fetch->dataOffset + fetch->numBytes); +} + +void wz_fetch_onReadyStateChange(emscripten_fetch_t *fetch) +{ + if (fetch->readyState != 2) return; // 2 = HEADERS_RECEIVED + + if (fetch->userData == nullptr) return; + URLTransferRequest* pRequest = static_cast(fetch->userData); + + size_t headersLengthBytes = emscripten_fetch_get_response_headers_length(fetch) + 1; + char *headerString = new char[headersLengthBytes]; + emscripten_fetch_get_response_headers(fetch, headerString, headersLengthBytes); + char **responseHeaders = emscripten_fetch_unpack_response_headers(headerString); + delete[] headerString; + + int numHeaders = 0; + for(; responseHeaders[numHeaders * 2]; ++numHeaders) + { + if (responseHeaders[(numHeaders * 2) + 1] == nullptr) + { + break; + } + + std::string header_key = (responseHeaders[numHeaders * 2]) ? responseHeaders[numHeaders * 2] : ""; + std::string header_value = (responseHeaders[(numHeaders * 2) + 1]) ? responseHeaders[(numHeaders * 2) + 1] : ""; + trim_str(header_key); + trim_str(header_value); + pRequest->responseHeaders->responseHeaders[header_key] = header_value; + } + + emscripten_fetch_free_unpacked_response_headers(responseHeaders); +} + +class RunningURLTransferRequestBase : public URLTransferRequest +{ +public: + virtual const URLRequestBase& getBaseRequest() const = 0; +public: + RunningURLTransferRequestBase() + : URLTransferRequest() + { } + + virtual const std::string& url() const override + { + return getBaseRequest().url; + } + + virtual InternetProtocol protocol() const override + { + return getBaseRequest().protocol; + } + + virtual bool noProxy() const override + { + return getBaseRequest().noProxy; + } + + virtual const std::unordered_map& requestHeaders() const override + { + return getBaseRequest().getRequestHeaders(); + } + + virtual bool onProgressUpdate(uint64_t dltotal, uint64_t dlnow) override + { + auto request = getBaseRequest(); + if (request.progressCallback) + { + request.progressCallback(request.url, static_cast(dltotal), static_cast(dlnow)); + } + return false; + } + + virtual void handleRequestSuccess(unsigned short status) override + { + onSuccess(EmFetchHTTPResponseDetails(true, status, responseHeaders)); + } + + virtual void handleRequestError(unsigned short status) override + { + onFailure(URLRequestFailureType::TRANSFER_FAILED, std::make_shared(false, status, responseHeaders)); + } + + virtual void requestFailedToFinish(URLRequestFailureType type) override + { + ASSERT_OR_RETURN(, type != URLRequestFailureType::TRANSFER_FAILED, "TRANSFER_FAILED should be handled by handleRequestDone"); + onFailure(type, nullptr); + } + +private: + virtual void onSuccess(const HTTPResponseDetails& responseDetails) = 0; + + void onFailure(URLRequestFailureType type, const std::shared_ptr& transferDetails) + { + auto request = getBaseRequest(); + + const std::string& url = request.url; + switch (type) + { + case URLRequestFailureType::INITIALIZE_REQUEST_ERROR: + wzAsyncExecOnMainThread([url]{ + debug(LOG_NET, "Fetch: Failed to initialize request for (%s)", url.c_str()); + }); + break; + case URLRequestFailureType::TRANSFER_FAILED: + if (!transferDetails) + { + wzAsyncExecOnMainThread([url]{ + debug(LOG_ERROR, "Fetch: Request for (%s) failed - but no transfer failure details provided!", url.c_str()); + }); + } + else + { + long httpStatusCode = transferDetails->httpStatusCode(); + wzAsyncExecOnMainThread([url, httpStatusCode]{ + debug(LOG_NET, "Fetch: Request for (%s) failed with HTTP response code: %ld", url.c_str(), httpStatusCode); + }); + } + break; + case URLRequestFailureType::CANCELLED: + wzAsyncExecOnMainThread([url]{ + debug(LOG_NET, "Fetch: Request for (%s) was cancelled", url.c_str()); + }); + break; + case URLRequestFailureType::CANCELLED_BY_SHUTDOWN: + wzAsyncExecOnMainThread([url]{ + debug(LOG_NET, "Fetch: Request for (%s) was cancelled by application shutdown", url.c_str()); + }); + break; + } + + if (request.onFailure) + { + request.onFailure(request.url, type, transferDetails); + } + } +}; + +class RunningURLDataRequest : public RunningURLTransferRequestBase +{ +public: + URLDataRequest request; + std::shared_ptr chunk; +private: + uint64_t _maxDownloadSize = MAXIMUM_IN_MEMORY_DOWNLOAD_SIZE; // 512 MB, default max download limit + +public: + RunningURLDataRequest(const URLDataRequest& request) + : RunningURLTransferRequestBase() + , request(request) + { + chunk = std::make_shared(); + if (request.maxDownloadSizeLimit > 0) + { + _maxDownloadSize = std::min(request.maxDownloadSizeLimit, static_cast(MAXIMUM_IN_MEMORY_DOWNLOAD_SIZE)); + } + } + + virtual const URLRequestBase& getBaseRequest() const override + { + return request; + } + + virtual uint64_t maxDownloadSize() const override + { + // For downloading to memory, set a lower default max download limit + return _maxDownloadSize; + } + + virtual bool hasWriteMemoryCallback() override { return true; } + virtual bool writeMemoryCallback(const void *contents, uint64_t numBytes, uint64_t dataOffset) override + { + size_t realsize = numBytes; + MemoryStruct *mem = chunk.get(); + +#if UINT64_MAX > SIZE_MAX + if (numBytes > static_cast(std::numeric_limits::max())) + { + return false; + } + if (dataOffset > static_cast(std::numeric_limits::max())) + { + return false; + } + if ((dataOffset + numBytes) > static_cast(std::numeric_limits::max())) + { + return false; + } +#endif + + if (static_cast(dataOffset + numBytes) > mem->size) + { + char *ptr = (char*) realloc(mem->memory, static_cast(dataOffset + numBytes) + 1); + if(ptr == NULL) { + /* out of memory! */ + return false; + } + mem->memory = ptr; + mem->size = static_cast(dataOffset + numBytes); + } + + memcpy(&(mem->memory[static_cast(dataOffset)]), contents, static_cast(numBytes)); + mem->memory[mem->size] = 0; + + return true; + } + +private: + void onSuccess(const HTTPResponseDetails& responseDetails) override + { + if (request.onSuccess) + { + request.onSuccess(request.url, responseDetails, chunk); + } + } +}; + + +// Request data from a URL (stores the response in memory) +// Generally, you should define both onSuccess and onFailure callbacks +// If you want to actually process the response, you *must* define an onSuccess callback +// +// IMPORTANT: Callbacks may be called on a background thread +AsyncURLRequestHandle urlRequestData(const URLDataRequest& request) +{ + ASSERT_OR_RETURN(nullptr, !request.url.empty(), "A valid request must specify a URL"); + ASSERT_OR_RETURN(nullptr, request.maxDownloadSizeLimit <= (curl_off_t)MAXIMUM_IN_MEMORY_DOWNLOAD_SIZE, "Requested maxDownloadSizeLimit exceeds maximum in-memory download size limit %zu", (size_t)MAXIMUM_IN_MEMORY_DOWNLOAD_SIZE); + + std::shared_ptr pNewRequest = std::make_shared(request); + std::shared_ptr requestHandle = std::make_shared(pNewRequest); + + wzMutexLock(urlRequestMutex); + activeURLRequests.push_back(pNewRequest); + wzMutexUnlock(urlRequestMutex); + + if (!pNewRequest->initiateFetch()) + { + pNewRequest->requestFailedToFinish(URLRequestFailureType::INITIALIZE_REQUEST_ERROR); + wzMutexLock(urlRequestMutex); + activeURLRequests.remove(pNewRequest); + wzMutexUnlock(urlRequestMutex); + return nullptr; + } + + return requestHandle; +} + +// Download a file (stores the response in the outFilePath) +// Generally, you should define both onSuccess and onFailure callbacks +// +// IMPORTANT: Callbacks may be called on a background thread +AsyncURLRequestHandle urlDownloadFile(const URLFileDownloadRequest& request) +{ + // TODO: Implement + return nullptr; +} + +// Sets a flag that will cancel an asynchronous url request +// NOTE: It is possible that the request will finish successfully before it is cancelled. +void urlRequestSetCancelFlag(AsyncURLRequestHandle requestHandle) +{ + std::shared_ptr pRequestImpl = std::dynamic_pointer_cast(requestHandle); + if (pRequestImpl) + { + if (auto request = pRequestImpl->weakRequest.lock()) + { + request->cancel(); + } + } +} + +void urlRequestInit() +{ + // Currently, nothing to do +} +void urlRequestOutputDebugInfo() +{ + // Currently a no-op for Emscripten Fetch backend +} +void urlRequestShutdown() +{ + // For now, just cancel all outstanding non-"waitOnShutdown" requests + // FUTURE TODO: Examine how to best handle "waitOnShutdown" requests? + + // build a copy of the active list - the actual active list is managed by the callbacks for url requests + wzMutexLock(urlRequestMutex); + auto activeURLRequestsCopy = activeURLRequests; + wzMutexUnlock(urlRequestMutex); + + debug(LOG_NET, "urlRequestShutdown: Remaining requests: %zu", activeURLRequestsCopy.size()); + + auto it = activeURLRequestsCopy.begin(); + while (it != activeURLRequestsCopy.end()) + { + auto runningTransfer = *it; + if (!runningTransfer->waitOnShutdown()) + { + // just cancel it + runningTransfer->cancel(); // this will ultimately handle removing from the main global list + runningTransfer->requestFailedToFinish(URLRequestFailureType::CANCELLED_BY_SHUTDOWN); + it = activeURLRequestsCopy.erase(it); + } + else + { + ++it; + } + } +} + +#endif diff --git a/src/warzoneconfig.cpp b/src/warzoneconfig.cpp index b1fe8074c1a..779cae71373 100644 --- a/src/warzoneconfig.cpp +++ b/src/warzoneconfig.cpp @@ -40,9 +40,15 @@ constexpr int MAX_OLD_LOGS = 50; /***************************************************************************/ +#if !defined(__EMSCRIPTEN__) +#define WZ_DEFAULT_FMV_MODE FMV_FULLSCREEN +#else +#define WZ_DEFAULT_FMV_MODE FMV_2X +#endif + struct WARZONE_GLOBALS { - FMV_MODE FMVmode = FMV_FULLSCREEN; + FMV_MODE FMVmode = WZ_DEFAULT_FMV_MODE; UDWORD width = 1024; UDWORD height = 768; UDWORD videoBufferDepth = 32; diff --git a/src/wrappers.cpp b/src/wrappers.cpp index 237ce72b8a5..b4b00180145 100644 --- a/src/wrappers.cpp +++ b/src/wrappers.cpp @@ -45,6 +45,10 @@ #include "wrappers.h" #include "titleui/titleui.h" +#if defined(__EMSCRIPTEN__) +#include +#endif + struct STAR { int xPos; @@ -278,6 +282,20 @@ void loadingScreenCallback() wzPumpEventsWhileLoading(); } +#if defined(__EMSCRIPTEN__) +void wzemscripten_display_web_loading_indicator(int x) +{ + MAIN_THREAD_EM_ASM({ + if (typeof wz_js_display_loading_indicator === "function") { + wz_js_display_loading_indicator($0); + } + else { + console.log('Cannot find wz_js_display_loading_indicator function'); + } + }, x); +} +#endif + // fill buffers with the static screen void initLoadingScreen(bool drawbdrop) { @@ -286,8 +304,12 @@ void initLoadingScreen(bool drawbdrop) wzShowMouse(false); pie_SetFogStatus(false); +#if !defined(__EMSCRIPTEN__) // setup the callback.... resSetLoadCallback(loadingScreenCallback); +#else + wzemscripten_display_web_loading_indicator(1); +#endif if (drawbdrop) { @@ -311,7 +333,11 @@ void closeLoadingScreen() free(stars); stars = nullptr; } +#if !defined(__EMSCRIPTEN__) resSetLoadCallback(nullptr); +#else + wzemscripten_display_web_loading_indicator(0); +#endif } diff --git a/src/wzpropertyproviders.cpp b/src/wzpropertyproviders.cpp index cd5a75036db..a4f362cdee6 100644 --- a/src/wzpropertyproviders.cpp +++ b/src/wzpropertyproviders.cpp @@ -59,6 +59,11 @@ #include #endif +// Includes for Emscripten +#if defined(__EMSCRIPTEN__) +# include "emscripten_helpers.h" +#endif + // MARK: - BuildPropertyProvider enum class BuildProperty { @@ -313,9 +318,13 @@ static const std::unordered_map