diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 1142704d..35c8c1d7 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -30,10 +30,10 @@ jobs: run: | pip install meson --break-system-packages - - name: Setup ninja + - name: Setup dependencies run: | sudo apt-get update - sudo apt-get install ninja-build jq -y --no-install-recommends + sudo apt-get install ninja-build jq nasm -y --no-install-recommends - name: Setup JDK uses: actions/setup-java@v4 @@ -50,6 +50,7 @@ jobs: - name: Build native libraries run: | + export ANDROID_SDK_HOME="$HOME/.android/sdk" bash ./platforms/build-android.sh ${{ matrix.config.arch }} complete_rebuild release cp -r ./assets/ platforms/android/app/src/main diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ad4814af..2c669e97 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -32,9 +32,9 @@ jobs: - name: Prepare compile_commands.json run: | sudo apt-get update - sudo apt-get install ninja-build libsdl2-2.0-0 libsdl2-dev libsdl2-ttf* libsdl2-mixer* libsdl2-image* desktop-file-utils -y --no-install-recommends + sudo apt-get install ninja-build libsdl2-2.0-0 libsdl2-dev libsdl2-ttf* libsdl2-mixer* libsdl2-image* desktop-file-utils libavutil-dev libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev -y --no-install-recommends - meson setup build -Dbuildtype=release -Dclang_libcpp=disabled -Dtests=true + meson setup build -Dbuildtype=release -Dclang_libcpp=disabled -Dtests=true -Duse_embedded_ffmpeg=enabled meson compile -C build git_version.hpp - uses: cpp-linter/cpp-linter-action@v2 @@ -52,7 +52,7 @@ jobs: tidy-checks: '' step-summary: true file-annotations: true - ignore: subprojects|build|android|assets|recordings|docs|toolchains|platforms|wrapper|src/libs/core/hash-library|tests|src/helper/web_utils.*|src/lobby/web_client.*|src/lobby/curl_client.* + ignore: subprojects|build|android|assets|recordings|docs|toolchains|platforms|wrapper|src/libs/core/hash-library|tests|src/graphics/video_renderer_windows.*|src/helper/web_utils.*|src/lobby/web_client.*|src/lobby/curl_client.* - name: Fail CI run if linter checks failed if: steps.linter.outputs.checks-failed != 0 diff --git a/meson.options b/meson.options index 9902188b..2b4d536c 100644 --- a/meson.options +++ b/meson.options @@ -25,3 +25,10 @@ option( value: false, description: 'if you only want to build the libs, enable this', ) + +option( + 'use_embedded_ffmpeg', + type: 'feature', + value: 'auto', + description: 'embed ffmpeg to render recording videos', +) diff --git a/platforms/android/app/jni/Android.mk b/platforms/android/app/jni/Android.mk index 86754b76..f58c146c 100644 --- a/platforms/android/app/jni/Android.mk +++ b/platforms/android/app/jni/Android.mk @@ -68,6 +68,42 @@ LOCAL_SRC_FILES := $(shell find "${SUBPROJECTS_PATH}" -name libkeyutils.so) include $(PREBUILT_SHARED_LIBRARY) +include $(CLEAR_VARS) +LOCAL_MODULE := libavutil +LOCAL_SRC_FILES := $(shell meson introspect --dependencies "${BUILD_PATH}" | jq -r ".[] | select(.name==\"libavutil\") | .link_args | .[]") +include $(PREBUILT_SHARED_LIBRARY) + + +include $(CLEAR_VARS) +LOCAL_MODULE := libavcodec +LOCAL_SRC_FILES := $(shell meson introspect --dependencies "${BUILD_PATH}" | jq -r ".[] | select(.name==\"libavcodec\") | .link_args | .[]") +include $(PREBUILT_SHARED_LIBRARY) + + +include $(CLEAR_VARS) +LOCAL_MODULE := libavformat +LOCAL_SRC_FILES := $(shell meson introspect --dependencies "${BUILD_PATH}" | jq -r ".[] | select(.name==\"libavformat\") | .link_args | .[]") +include $(PREBUILT_SHARED_LIBRARY) + + +include $(CLEAR_VARS) +LOCAL_MODULE := libavfilter +LOCAL_SRC_FILES := $(shell meson introspect --dependencies "${BUILD_PATH}" | jq -r ".[] | select(.name==\"libavfilter\") | .link_args | .[]") +include $(PREBUILT_SHARED_LIBRARY) + + +include $(CLEAR_VARS) +LOCAL_MODULE := libswscale +LOCAL_SRC_FILES := $(shell meson introspect --dependencies "${BUILD_PATH}" | jq -r ".[] | select(.name==\"libswscale\") | .link_args | .[]") +include $(PREBUILT_SHARED_LIBRARY) + + +include $(CLEAR_VARS) +LOCAL_MODULE := libswresample +LOCAL_SRC_FILES := $(shell meson introspect --dependencies "${BUILD_PATH}" | jq -r ".[] | select(.name==\"libswresample\") | .link_args | .[]") +include $(PREBUILT_SHARED_LIBRARY) + + include $(CLEAR_VARS) LOCAL_MODULE := oopetris_core LIB_PATH := $(BUILD_PATH)/src/libs/core @@ -99,7 +135,7 @@ include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := main -LOCAL_SHARED_LIBRARIES := SDL2 sdl2_ttf freetype png16 sdl2_mixer vorbis vorbisfile ogg sdl2_image fmt keyutils oopetris_core oopetris_recordings oopetris_graphics oopetris +LOCAL_SHARED_LIBRARIES := SDL2 sdl2_ttf freetype png16 sdl2_mixer vorbis vorbisfile ogg sdl2_image fmt keyutils oopetris_core oopetris_recordings oopetris_graphics oopetris libavutil libavcodec libavformat libavfilter libswscale libswresample LOCAL_LDLIBS := -ldl -lGLESv1_CM -lGLESv2 -lOpenSLES -llog -landroid LOCAL_LDFLAGS := -Wl,--no-undefined include $(BUILD_SHARED_LIBRARY) diff --git a/platforms/build-android.sh b/platforms/build-android.sh index 7cd318b3..a1e92b5c 100755 --- a/platforms/build-android.sh +++ b/platforms/build-android.sh @@ -114,8 +114,9 @@ for INDEX in "${ARCH_KEYS_INDEX[@]}"; do export BIN_DIR="$HOST_ROOT/bin" export PATH="$BIN_DIR:$PATH" - LIB_PATH="${SYS_ROOT}/usr/lib/$ARM_TRIPLE:${SYS_ROOT}/usr/lib/$ARM_TRIPLE/${SDK_VERSION}" - INC_PATH="${SYS_ROOT}/usr/include" + export LIB_PATH="${SYS_ROOT}/usr/lib/$ARM_TRIPLE:${SYS_ROOT}/usr/lib/$ARM_TRIPLE/${SDK_VERSION}" + export INC_PATH="${SYS_ROOT}/usr/include" + export PKG_CONFIG_PATH="${SYS_ROOT}/usr/lib/pkgconfig/" export LIBRARY_PATH="$SYS_ROOT/usr/lib/$ARM_NAME_TRIPLE/$SDK_VERSION" @@ -146,6 +147,16 @@ for INDEX in "${ARCH_KEYS_INDEX[@]}"; do find "$HOST_ROOT/sysroot/usr/lib/$ARM_NAME_TRIPLE/$SDK_VERSION/" -maxdepth 1 -name "*.o" -exec ln -s "{}" "${SYS_ROOT:?}/usr/lib/" \; + # TODO: remove this temporary fix: + # see: https://github.com/android/ndk/issues/2107 + if [ "$ARCH_VERSION" = "armv7a" ]; then + sed -i -e 's/asm(/__asm__(/g' "$HOST_ROOT/sysroot/usr/include/arm-linux-androideabi/asm/swab.h" + elif [ "$ARCH_VERSION" = "i686" ]; then + sed -i -e 's/asm(/__asm__(/g' "$HOST_ROOT/sysroot/usr/include/i686-linux-android/asm/swab.h" + elif [ "$ARCH_VERSION" = "x86_64" ]; then + sed -i -e 's/asm(/__asm__(/g' "$HOST_ROOT/sysroot/usr/include/x86_64-linux-android/asm/swab.h" + fi + cd "$LAST_DIR" fi @@ -270,7 +281,6 @@ for INDEX in "${ARCH_KEYS_INDEX[@]}"; do -DBUILD_SHARED_LIBS=OFF \ -DINSTALL_PKGCONFIG_MODULES=ON - cmake --build . cmake --install . @@ -344,6 +354,52 @@ for INDEX in "${ARCH_KEYS_INDEX[@]}"; do cd "$LAST_DIR" + ## build ffmpeg for android (using https://github.com/Javernaut/ffmpeg-android-maker) + + LAST_DIR="$PWD" + + cd "$SYS_ROOT" + + BUILD_DIR_FFMPEG="build-ffmpeg" + + BUILD_FFMPEG_FILE="$SYS_ROOT/$BUILD_DIR_FFMPEG/build_successfull.meta" + + if [ "$COMPILE_TYPE" == "complete_rebuild" ] || ! [ -e "$BUILD_FFMPEG_FILE" ]; then + + mkdir -p "$BUILD_DIR_FFMPEG" + + cd "$BUILD_DIR_FFMPEG" + + FFMPEG_MAKER_DIR="maker" + + if ! [ -e "$FFMPEG_MAKER_DIR" ]; then + + git clone https://github.com/Javernaut/ffmpeg-android-maker.git "$FFMPEG_MAKER_DIR" + + cd "$FFMPEG_MAKER_DIR" + else + cd "$FFMPEG_MAKER_DIR" + + git pull + + fi + + ./ffmpeg-android-maker.sh "--target-abis=$ARCH" "--android-api-level=$SDK_VERSION" --enable-libx264 + + FFMPEG_MAKER_OUTPUT_DIR="output" + + find "$FFMPEG_MAKER_OUTPUT_DIR/include/" -maxdepth 3 -mindepth 2 -type d -exec cp -r {} "$SYS_ROOT/usr/include/" \; + + find "$FFMPEG_MAKER_OUTPUT_DIR/lib/" -type f -exec cp -r {} "$SYS_ROOT/usr/lib/" \; + + find "build/" -maxdepth 5 -mindepth 4 -type f -name "*.pc" -exec cp -r {} "$SYS_ROOT/usr/lib/pkgconfig/" \; + + touch "$BUILD_FFMPEG_FILE" + + fi + + cd "$LAST_DIR" + ## END of manual build of dependencies MESON_CPU_FAMILY=$ARCH @@ -393,7 +449,7 @@ prefix = '$SYS_ROOT' libdir = '$LIB_PATH' [properties] -pkg_config_libdir = '$SYS_ROOT/usr/lib/pkgconfig' +pkg_config_libdir = '$PKG_CONFIG_PATH' sys_root = '${SYS_ROOT}' EOF @@ -436,7 +492,8 @@ EOF --cross-file "./platforms/crossbuild-android-$ARM_TARGET_ARCH.ini" \ "-Dbuildtype=$BUILDTYPE" \ -Dsdl2:use_hidapi=enabled \ - -Dclang_libcpp=disabled + -Dclang_libcpp=disabled \ + -Duse_embedded_ffmpeg=enabled fi diff --git a/platforms/build-switch.sh b/platforms/build-switch.sh index 55810e6a..4196ac27 100755 --- a/platforms/build-switch.sh +++ b/platforms/build-switch.sh @@ -157,7 +157,8 @@ if [ "$COMPILE_TYPE" == "complete_rebuild" ] || [ ! -e "$BUILD_DIR" ]; then -Dcurl:unittests=disabled \ -Dcurl:bearer-auth=enabled \ -Dcurl:brotli=enabled \ - -Dcurl:libz=enabled + -Dcurl:libz=enabled \ + -Duse_embedded_ffmpeg=enabled fi diff --git a/platforms/build-web.sh b/platforms/build-web.sh index f2d099dc..b0c1b0a8 100755 --- a/platforms/build-web.sh +++ b/platforms/build-web.sh @@ -17,20 +17,21 @@ fi EMSCRIPTEN_UPSTREAM_ROOT="$EMSCRIPTEN_ROOT/upstream/emscripten" -EMSCRIPTEN_PACTH_FILE="$EMSCRIPTEN_UPSTREAM_ROOT/.patched_manually.meta" +EMSCRIPTEN_PATCH_FILE="$EMSCRIPTEN_UPSTREAM_ROOT/.patched_manually.meta" PATCH_DIR="platforms/emscripten" -if ! [ -e "$EMSCRIPTEN_PACTH_FILE" ]; then +if ! [ -e "$EMSCRIPTEN_PATCH_FILE" ]; then ##TODO: upstream those patches # see: https://github.com/emscripten-core/emscripten/pull/18379/commits # and: https://github.com/emscripten-core/emscripten/pull/18379 + # and: https://github.com/emscripten-core/emscripten/pull/22946 git apply --unsafe-paths -p1 --directory="$EMSCRIPTEN_UPSTREAM_ROOT" "$PATCH_DIR/sdl2_image_port.diff" git apply --unsafe-paths -p1 --directory="$EMSCRIPTEN_UPSTREAM_ROOT" "$PATCH_DIR/sdl2_mixer_port.diff" git apply --unsafe-paths -p1 --directory="$EMSCRIPTEN_UPSTREAM_ROOT" "$PATCH_DIR/default_settings.diff" - touch "$EMSCRIPTEN_PACTH_FILE" + touch "$EMSCRIPTEN_PATCH_FILE" fi # git apply path @@ -38,12 +39,17 @@ fi # shellcheck disable=SC1091 EMSDK_QUIET=1 source "$EMSCRIPTEN_ROOT/emsdk_env.sh" >/dev/null -## build theneeded dependencies +PTHREAD_POOL_SIZE="8" + +## build the needed dependencies embuilder build sdl2-mt harfbuzz-mt freetype zlib sdl2_ttf mpg123 "sdl2_mixer:formats=mp3" libpng-mt "sdl2_image:formats=png,svg:mt=1" icu-mt export EMSCRIPTEN_SYS_ROOT="$EMSCRIPTEN_UPSTREAM_ROOT/cache/sysroot" -export BUILD_DIR="build-web" +EMSCRIPTEN_SYS_LIB_DIR="$EMSCRIPTEN_SYS_ROOT/lib/wasm32-emscripten" +EMSCRIPTEN_SYS_PKGCONFIG_DIR="$EMSCRIPTEN_SYS_ROOT/lib/pkgconfig" + +export PKG_CONFIG_PATH="$EMSCRIPTEN_SYS_PKGCONFIG_DIR" export CC="emcc" export CXX="em++" @@ -52,18 +58,162 @@ export RANLIB="emranlib" export STRIP="emstrip" export NM="emnm" +EMSCRIPTEN_PORT_BUILD_DIR="$EMSCRIPTEN_UPSTREAM_ROOT/cache/ports" + +BUILD_DIR_FFMPEG="build-ffmpeg" + +BUILD_FFMPEG_FILE="$EMSCRIPTEN_PORT_BUILD_DIR/$BUILD_DIR_FFMPEG/build_successfull.meta" + +# build the ffmpeg dependencies +# taken from: https://dev.to/alfg/ffmpeg-webassembly-2cbl +# modifed to fit the style of this project + some manual modifications +if [ "$COMPILE_TYPE" == "complete_rebuild" ] || ! [ -e "$BUILD_FFMPEG_FILE" ]; then + + LAST_DIR="$PWD" + + cd "$EMSCRIPTEN_PORT_BUILD_DIR" + + mkdir -p "$BUILD_DIR_FFMPEG" + + cd "$BUILD_DIR_FFMPEG" + + LIBX264_DIR="x264-src" + + if ! [ -e "$LIBX264_DIR" ]; then + + LIBX264_DIR_VERSION="20191217-2245-stable" + + wget "https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${LIBX264_DIR_VERSION}.tar.bz2" + + tar xvfj "x264-snapshot-${LIBX264_DIR_VERSION}.tar.bz2" + + mv "x264-snapshot-${LIBX264_DIR_VERSION}" "$LIBX264_DIR" + fi + + cd "$LIBX264_DIR" + + BUILD_LIBX264_FILE="build_successfull.meta" + + if ! [ -e "$BUILD_LIBX264_FILE" ]; then + + emconfigure ./configure \ + --enable-static \ + --disable-cli \ + --disable-asm \ + --extra-cflags="-sUSE_PTHREADS=1" \ + --host=i686-gnu \ + --sysroot="$EMSCRIPTEN_SYS_ROOT" \ + --prefix="$EMSCRIPTEN_SYS_ROOT" \ + --libdir="$EMSCRIPTEN_SYS_LIB_DIR" + + emmake make -j + + emmake make install + + # move pkgconfig file into correct folder + + find "$EMSCRIPTEN_SYS_LIB_DIR/pkgconfig/" -name "*.pc" -exec mv {} "$EMSCRIPTEN_SYS_PKGCONFIG_DIR/" \; + + touch "$BUILD_LIBX264_FILE" + + fi + + cd .. + + FFMPEG_CLONE_DIR="ffmpeg-src" + + GIT_FFMPEG_TAG="n7.1" + + if ! [ -e "$FFMPEG_CLONE_DIR" ]; then + + git clone https://github.com/FFmpeg/FFmpeg "$FFMPEG_CLONE_DIR" + + cd "$FFMPEG_CLONE_DIR" + + git checkout "$GIT_FFMPEG_TAG" + + else + cd "$FFMPEG_CLONE_DIR" + + git checkout "$GIT_FFMPEG_TAG" + + fi + + FFMPEG_COMMON_FLAGS="-pthread -sUSE_PTHREADS=1" + + FFMPEG_LINK_FLAGS="$COMMON_FLAGS -sWASM=1 -sALLOW_MEMORY_GROWTH=1 -sASSERTIONS=1 -sERROR_ON_UNDEFINED_SYMBOLS=1 -sPTHREAD_POOL_SIZE=$PTHREAD_POOL_SIZE" + + ##TODO: add --disable-debug, in release mode + + # Configure and build FFmpeg with emscripten. + # Disable all programs and only enable features we will use. + # https://github.com/FFmpeg/FFmpeg/blob/master/configure + emconfigure ./configure \ + --disable-asm \ + --disable-x86asm \ + --disable-inline-asm \ + --disable-stripping \ + --target-os=none \ + --arch=x86_32 \ + --enable-cross-compile \ + --disable-doc \ + --disable-programs \ + --disable-sdl2 \ + --disable-all \ + --enable-avcodec \ + --enable-avformat \ + --enable-avfilter \ + --enable-avdevice \ + --enable-avutil \ + --enable-swresample \ + --enable-swscale \ + --enable-filters \ + --enable-protocol="file,pipe,tcp" \ + --enable-decoder="rawvideo" \ + --enable-encoder="libx264" \ + --enable-demuxer="rawvideo" \ + --enable-muxer="mp4" \ + --enable-gpl \ + --enable-libx264 \ + --extra-cflags="$FFMPEG_COMMON_FLAGS" \ + --extra-cxxflags="$FFMPEG_COMMON_FLAGS" \ + --extra-ldflags="$FFMPEG_LINK_FLAGS" \ + --sysroot="$EMSCRIPTEN_SYS_ROOT" \ + --prefix="$EMSCRIPTEN_SYS_ROOT" \ + --libdir="$EMSCRIPTEN_SYS_LIB_DIR" \ + --pkgconfigdir="$EMSCRIPTEN_SYS_PKGCONFIG_DIR" \ + --nm="$NM" \ + --ar="$AR" \ + --cc="$CC" \ + --cxx="$CXX" \ + --objcc="$CC" \ + --dep-cc="$CC" + + emmake make -j + + emmake make install + + touch "$BUILD_FFMPEG_FILE" + + cd "$LAST_DIR" + +fi + +export BUILD_DIR="build-web" + export ARCH="wasm32" export CPU_ARCH="wasm32" export ENDIANESS="little" export ROMFS="platforms/romfs" +#TODO: differentiate between release and debug mode, disable -sASSERTIONS and other debbug utilities export PACKAGE_FLAGS="'--use-port=sdl2', '--use-port=harfbuzz', '--use-port=freetype', '--use-port=zlib', '--use-port=sdl2_ttf', '--use-port=mpg123', '--use-port=sdl2_mixer', '-sSDL2_MIXER_FORMATS=[\"mp3\"]','--use-port=libpng', '--use-port=sdl2_image','-sSDL2_IMAGE_FORMATS=[\"png\",\"svg\"]', '--use-port=icu'" export COMMON_FLAGS="'-fexceptions', '-pthread', '-sUSE_PTHREADS=1', '-sEXCEPTION_CATCHING_ALLOWED=[..]', $PACKAGE_FLAGS" # TODO see if ALLOW_MEMORY_GROWTH is needed, but if we load ttf's and music it likely is and we don't have to debug OOm crashes, that aren't handled by some third party library, which is painful -export LINK_FLAGS="$COMMON_FLAGS, '-sEXPORT_ALL=1', '-sUSE_WEBGPU=1', '-sWASM=1', '-sALLOW_MEMORY_GROWTH=1', '-sASSERTIONS=1','-sERROR_ON_UNDEFINED_SYMBOLS=1', '-sFETCH=1', '-sEXIT_RUNTIME=1'" +export LINK_FLAGS="$COMMON_FLAGS, '-sEXPORT_ALL=1', '-sUSE_WEBGPU=1', '-sWASM=1', '-sALLOW_MEMORY_GROWTH=1', '-sASSERTIONS=1','-sERROR_ON_UNDEFINED_SYMBOLS=1', '-sFETCH=1', '-sEXIT_RUNTIME=1', '-sPTHREAD_POOL_SIZE=$PTHREAD_POOL_SIZE','-lidbfs.js'" export COMPILE_FLAGS="$COMMON_FLAGS ,'-DAUDIO_PREFER_MP3'" export CROSS_FILE="./platforms/crossbuild-web.ini" @@ -156,7 +306,8 @@ if [ "$COMPILE_TYPE" == "complete_rebuild" ] || [ ! -e "$BUILD_DIR" ]; then --cross-file "$CROSS_FILE" \ "-Dbuildtype=$BUILDTYPE" \ -Ddefault_library=static \ - -Dtests=false + -Dtests=false \ + -Duse_embedded_ffmpeg=enabled fi diff --git a/platforms/emscripten/sdl2_image_port.diff b/platforms/emscripten/sdl2_image_port.diff index 402868e7..c0c67612 100644 --- a/platforms/emscripten/sdl2_image_port.diff +++ b/platforms/emscripten/sdl2_image_port.diff @@ -1,8 +1,8 @@ diff --git a/tools/ports/sdl2_image.py b/tools/ports/sdl2_image.py -index c72ef576..0c12feba 100644 +index 70fa1499..36be807b 100644 --- a/tools/ports/sdl2_image.py +++ b/tools/ports/sdl2_image.py -@@ -16,15 +16,17 @@ variants = { +@@ -18,7 +18,8 @@ variants = { } OPTIONS = { @@ -12,40 +12,38 @@ index c72ef576..0c12feba 100644 } SUPPORTED_FORMATS = {'avif', 'bmp', 'gif', 'jpg', 'jxl', 'lbm', 'pcx', 'png', - 'pnm', 'qoi', 'svg', 'tga', 'tif', 'webp', 'xcf', 'xpm', 'xv'} +@@ -26,7 +27,8 @@ SUPPORTED_FORMATS = {'avif', 'bmp', 'gif', 'jpg', 'jxl', 'lbm', 'pcx', 'png', # user options (from --use-port) --opts: Dict[str, Set] = { + opts: Dict[str, Set] = { - 'formats': set() -+opts = { + 'formats': set(), + 'mt': 0 } -@@ -42,7 +44,7 @@ def get_lib_name(settings): +@@ -44,7 +46,7 @@ def get_lib_name(settings): libname = 'libSDL2_image' if formats != '': - libname += '_' + formats -- return libname + '.a' -+ return libname + ('-mt' if opts['mt'] else '') + '.a' - - - def get(ports, settings, shared): -@@ -70,6 +72,8 @@ def get(ports, settings, shared): + libname += '-' + formats +- if settings.PTHREADS: ++ if settings.PTHREADS or opts['mt']: + libname += '-mt' + return libname + '.a' +@@ -75,7 +77,7 @@ def get(ports, settings, shared): if 'jpg' in formats: - defs += ['-sUSE_LIBJPEG'] -+ if opts['mt']: -+ defs += ['-pthread'] + flags += ['-sUSE_LIBJPEG'] - ports.build_port(src_dir, final, 'sdl2_image', flags=defs, srcs=srcs) +- if settings.PTHREADS: ++ if settings.PTHREADS or opts['mt']: + flags += ['-pthread'] -@@ -99,7 +103,12 @@ def handle_options(options, error_handler): + ports.build_port(src_dir, final, 'sdl2_image', flags=flags, srcs=srcs) +@@ -106,6 +108,12 @@ def handle_options(options, error_handler): error_handler(f'{format} is not a supported format') else: opts['formats'].add(format) -- + + mt = options['mt'] + if mt not in ["1","0"]: @@ -53,5 +51,5 @@ index c72ef576..0c12feba 100644 + else: + opts['mt'] = int(mt) + def show(): - return 'sdl2_image (-sUSE_SDL_IMAGE=2 or --use-port=sdl2_image; zlib license)' diff --git a/src/discord/core.cpp b/src/discord/core.cpp index 6a5e32c6..b28ee212 100644 --- a/src/discord/core.cpp +++ b/src/discord/core.cpp @@ -5,9 +5,9 @@ #include "./core.hpp" #include +#include #include - [[nodiscard]] std::string constants::discord ::get_asset_key(constants::discord::ArtAsset asset) { switch (asset) { diff --git a/src/executables/game/application.cpp b/src/executables/game/application.cpp index 605b6a5e..e41e233f 100644 --- a/src/executables/game/application.cpp +++ b/src/executables/game/application.cpp @@ -456,12 +456,15 @@ void Application::loop_entry_emscripten() { void Application::initialize() { + utils::set_thread_name("oopetris"); auto loading_screen_arg = scenes::LoadingScreen{ this }; const auto start_time = SDL_GetTicks64(); std::future load_everything_thread = std::async(std::launch::async, [this] { + utils::set_thread_name("loading"); + this->m_settings_manager = std::make_unique(this); this->m_settings_manager->add_callback([this](const auto& settings) { this->reload_api(settings); }); diff --git a/src/executables/game/main.cpp b/src/executables/game/main.cpp index 4800a171..c6714689 100644 --- a/src/executables/game/main.cpp +++ b/src/executables/game/main.cpp @@ -48,7 +48,19 @@ namespace { #endif -#if !(defined(__EMSCRIPTEN__)) +#if defined(__EMSCRIPTEN__) + + // See: https://emscripten.org/docs/api_reference/Filesystem-API.html#filesystem-api-idbfs + EM_ASM(FS.mkdir('/persistent'); FS.mount(IDBFS, { autoPersist: true }, '/persistent'); FS.syncfs( + true, + function(err) { + if (err) { + console.error(err); + } + } + );); + +#endif const auto logs_path = utils::get_root_folder() / "logs"; @@ -64,7 +76,7 @@ namespace { fmt::format("{}/oopetris.log", logs_path.string()), 1024 * 1024 * 10, 5, true )); } -#endif + auto combined_logger = std::make_shared("combined_logger", begin(sinks), end(sinks)); spdlog::set_default_logger(combined_logger); diff --git a/src/game/game.cpp b/src/game/game.cpp index c44ee8bd..02104cc9 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -12,9 +12,20 @@ Game::Game( u32 simulation_frequency, const ui::Layout& layout, bool is_top_level +) + : Game{ service_provider, input, starting_parameters, std::make_shared(simulation_frequency), + layout, is_top_level } { } + +Game::Game( + ServiceProvider* const service_provider, + const std::shared_ptr& input, + const tetrion::StartingParameters& starting_parameters, + const std::shared_ptr& clock_source, + const ui::Layout& layout, + bool is_top_level ) : ui::Widget{ layout, ui::WidgetType::Component, is_top_level }, - m_clock_source{ std::make_unique(simulation_frequency) }, + m_clock_source{ clock_source }, m_input{ input } { @@ -64,7 +75,7 @@ void Game::render(const ServiceProvider& service_provider) const { m_tetrion->render(service_provider); } -[[nodiscard]] helper::BoolWrapper> +[[nodiscard]] ui::Widget::EventHandleResult Game::handle_event(const std::shared_ptr& /*input_manager*/, const SDL_Event& /*event*/) { return false; } diff --git a/src/game/game.hpp b/src/game/game.hpp index 0535edb8..a9762f56 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -12,7 +12,7 @@ struct Game : public ui::Widget { private: using TetrionHeaders = std::vector; - std::unique_ptr m_clock_source; + std::shared_ptr m_clock_source; SimulationStep m_simulation_step_index{ 0 }; std::unique_ptr m_tetrion; std::shared_ptr m_input; @@ -28,6 +28,15 @@ struct Game : public ui::Widget { bool is_top_level ); + OOPETRIS_GRAPHICS_EXPORTED explicit Game( + ServiceProvider* service_provider, + const std::shared_ptr& input, + const tetrion::StartingParameters& starting_parameters, + const std::shared_ptr& clock_source, + const ui::Layout& layout, + bool is_top_level + ); + OOPETRIS_GRAPHICS_EXPORTED void update() override; OOPETRIS_GRAPHICS_EXPORTED void render(const ServiceProvider& service_provider) const override; diff --git a/src/game/grid.cpp b/src/game/grid.cpp index d1365214..e30c4908 100644 --- a/src/game/grid.cpp +++ b/src/game/grid.cpp @@ -41,7 +41,7 @@ void Grid::render(const ServiceProvider& service_provider) const { draw_playing_field_background(service_provider); } -[[nodiscard]] helper::BoolWrapper> +[[nodiscard]] ui::Widget::EventHandleResult Grid::handle_event(const std::shared_ptr& /*input_manager*/, const SDL_Event& /*event*/) { return false; } diff --git a/src/game/grid.hpp b/src/game/grid.hpp index 447f61d1..a44dc69c 100644 --- a/src/game/grid.hpp +++ b/src/game/grid.hpp @@ -32,7 +32,7 @@ struct Grid final : public ui::Widget { OOPETRIS_GRAPHICS_EXPORTED void render(const ServiceProvider& service_provider) const override; - OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] helper::BoolWrapper> + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] Widget::EventHandleResult handle_event(const std::shared_ptr& input_manager, const SDL_Event& event) override; private: diff --git a/src/game/layout.cpp b/src/game/layout.cpp new file mode 100644 index 00000000..0402cfb8 --- /dev/null +++ b/src/game/layout.cpp @@ -0,0 +1,23 @@ + +#include "./layout.hpp" + +std::vector game::get_layouts_for(std::size_t players, const ui::Layout& layout) { + + std::vector layouts{}; + layouts.reserve(players); + + if (players == 0) { + throw std::runtime_error("An empty recording file isn't supported"); + } else if (players == 1) { // NOLINT(readability-else-after-return,llvm-else-after-return) + layouts.push_back(ui::RelativeLayout{ layout, 0.02, 0.01, 0.96, 0.98 }); + } else if (players == 2) { + layouts.push_back(ui::RelativeLayout{ layout, 0.02, 0.01, 0.46, 0.98 }); + layouts.push_back(ui::RelativeLayout{ layout, 0.52, 0.01, 0.46, 0.98 }); + } else { + + //TODO(Totto): support bigger layouts than just 2 + throw std::runtime_error("At the moment only replays from up to two players are supported"); + } + + return layouts; +} diff --git a/src/game/layout.hpp b/src/game/layout.hpp new file mode 100644 index 00000000..77b28a77 --- /dev/null +++ b/src/game/layout.hpp @@ -0,0 +1,15 @@ + + +#pragma once + +#include + +#include "helper/windows.hpp" +#include "ui/layout.hpp" +#include + +namespace game { + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::vector + get_layouts_for(std::size_t players, const ui::Layout& layout); + +} diff --git a/src/game/meson.build b/src/game/meson.build index 9b5f0f6c..32290bea 100644 --- a/src/game/meson.build +++ b/src/game/meson.build @@ -9,6 +9,9 @@ graphics_src_files += files( 'graphic_helpers.hpp', 'grid.cpp', 'grid.hpp', + 'layout.cpp', + 'layout.hpp', + 'rotation.cpp', 'rotation.hpp', 'simulated_tetrion.cpp', diff --git a/src/game/tetrion.cpp b/src/game/tetrion.cpp index 9fce86f4..514a297f 100644 --- a/src/game/tetrion.cpp +++ b/src/game/tetrion.cpp @@ -110,7 +110,7 @@ void Tetrion::render(const ServiceProvider& service_provider) const { } } -[[nodiscard]] helper::BoolWrapper> +[[nodiscard]] ui::Widget::EventHandleResult Tetrion::handle_event(const std::shared_ptr& /*input_manager*/, const SDL_Event& /*event*/) { return false; } diff --git a/src/graphics/meson.build b/src/graphics/meson.build index 098f6680..40a0608f 100644 --- a/src/graphics/meson.build +++ b/src/graphics/meson.build @@ -11,3 +11,35 @@ graphics_src_files += files( 'window.cpp', 'window.hpp', ) + +if replay_video_rendering_enabled + + graphics_src_files += files( + 'video_renderer.cpp', + 'video_renderer.hpp', + ) + + if use_embedded_ffmpeg + graphics_src_files += files( + 'video_renderer_embedded.cpp', + ) + else + cpp = meson.get_compiler('cpp') + if host_machine.system() == 'darwin' or host_machine.system() == 'linux' + graphics_src_files += files( + 'video_renderer_unix.cpp', + ) + elif host_machine.system() == 'windows' + graphics_src_files += files( + 'video_renderer_windows.cpp', + ) + else + error( + 'unhandled system for video rendering withour embedding: ' + + host_machine.system(), + ) + endif + + endif + +endif diff --git a/src/graphics/renderer.cpp b/src/graphics/renderer.cpp index 5eb09c8f..bdf333a5 100644 --- a/src/graphics/renderer.cpp +++ b/src/graphics/renderer.cpp @@ -5,8 +5,21 @@ //TODO(Totto): assert return values of all sdl functions + +Renderer::Renderer(SDL_Renderer* renderer) : m_renderer{ renderer } { + + if (m_renderer == nullptr) { + throw helper::InitializationError{ fmt::format("Failed creating a SDL Renderer: {}", SDL_GetError()) }; + } + + auto result = SDL_SetRenderDrawBlendMode(m_renderer, SDL_BLENDMODE_BLEND); + if (result < 0) { + throw helper::InitializationError{ fmt::format("Failed in setting BlendMode on Renderer: {}", SDL_GetError()) }; + } +} + Renderer::Renderer(const Window& window, const VSync v_sync) - : m_renderer{ SDL_CreateRenderer( + : Renderer{ SDL_CreateRenderer( window.get_sdl_window(), -1, (v_sync == VSync::Enabled ? SDL_RENDERER_PRESENTVSYNC : 0) | SDL_RENDERER_TARGETTEXTURE @@ -16,19 +29,20 @@ Renderer::Renderer(const Window& window, const VSync v_sync) | SDL_RENDERER_ACCELERATED #endif ) } { +} - if (m_renderer == nullptr) { - throw helper::InitializationError{ fmt::format("Failed creating a SDL Renderer: {}", SDL_GetError()) }; - } +Renderer Renderer::get_software_renderer(std::unique_ptr& surface) { + return Renderer{ SDL_CreateSoftwareRenderer(surface.get()) }; +} - auto result = SDL_SetRenderDrawBlendMode(m_renderer, SDL_BLENDMODE_BLEND); - if (result < 0) { - throw helper::InitializationError{ fmt::format("Failed in setting BlendMode on Renderer: {}", SDL_GetError()) }; - } +Renderer::Renderer(Renderer&& other) noexcept : m_renderer{ other.m_renderer } { + other.m_renderer = nullptr; } Renderer::~Renderer() { - SDL_DestroyRenderer(m_renderer); + if (m_renderer != nullptr) { + SDL_DestroyRenderer(m_renderer); + } } void Renderer::set_draw_color(const Color& color) const { diff --git a/src/graphics/renderer.hpp b/src/graphics/renderer.hpp index 13dd0d96..16ca3eaa 100644 --- a/src/graphics/renderer.hpp +++ b/src/graphics/renderer.hpp @@ -22,10 +22,22 @@ struct Renderer final { private: SDL_Renderer* m_renderer; + OOPETRIS_GRAPHICS_EXPORTED explicit Renderer(SDL_Renderer* renderer); + + public: OOPETRIS_GRAPHICS_EXPORTED explicit Renderer(const Window& window, VSync v_sync); + + OOPETRIS_GRAPHICS_EXPORTED static Renderer get_software_renderer(std::unique_ptr& surface); + OOPETRIS_GRAPHICS_EXPORTED Renderer(const Renderer&) = delete; OOPETRIS_GRAPHICS_EXPORTED Renderer& operator=(const Renderer&) = delete; + + + OOPETRIS_GRAPHICS_EXPORTED Renderer(Renderer&& other) noexcept; + + OOPETRIS_GRAPHICS_EXPORTED Renderer& operator=(Renderer&& other) = default; + OOPETRIS_GRAPHICS_EXPORTED ~Renderer(); OOPETRIS_GRAPHICS_EXPORTED void set_draw_color(const Color& color) const; diff --git a/src/graphics/sdl_context.cpp b/src/graphics/sdl_context.cpp index e0c426f1..b86f5e72 100644 --- a/src/graphics/sdl_context.cpp +++ b/src/graphics/sdl_context.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #if defined(__CONSOLE__) #include "helper/console_helpers.hpp" @@ -21,6 +22,18 @@ SdlContext::SdlContext() { throw helper::InitializationError{ fmt::format("Failed in initializing sdl: {}", SDL_GetError()) }; } + + // when using gdb / lldb to debug and you click something with the mouse, no other application can use the mouse, which is annoying, so not using this feature in debug mode +#if !defined(NDEBUG) + const auto hint_mouse_result = SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, "0"); + + if (hint_mouse_result != SDL_TRUE) { + // this is non fatal, so not returning + spdlog::error("Failed to set the SDL_HINT_MOUSE_AUTO_CAPTURE hint: {}", SDL_GetError()); + } +#endif + + if (TTF_Init() < 0) { throw helper::InitializationError{ fmt::format("Failed in initializing sdl ttf: {}", TTF_GetError()) }; } diff --git a/src/graphics/video_renderer.cpp b/src/graphics/video_renderer.cpp new file mode 100644 index 00000000..501f8b29 --- /dev/null +++ b/src/graphics/video_renderer.cpp @@ -0,0 +1,242 @@ + + +#include "video_renderer.hpp" +#include "game/layout.hpp" + +#include + +VideoRenderer::VideoRenderer( + ServiceProvider* service_provider, + const std::filesystem::path& recording_path, + shapes::UPoint size +) + : m_main_provider{ service_provider }, + m_size{ size } { + auto* surface = SDL_CreateRGBSurface(0, static_cast(size.x), static_cast(size.y), 32, 0, 0, 0, 0); + + if (surface == nullptr) { + throw std::runtime_error{ fmt::format("Failed creating a SDL RGB Surface: {}", SDL_GetError()) }; + } + + m_surface.reset(surface); + auto renderer = Renderer::get_software_renderer(m_surface); + + m_renderer = std::make_unique(std::move(renderer)); + + initialize_games(recording_path); +} + +void VideoRenderer::initialize_games(const std::filesystem::path& recording_path) { + + auto [parameters, information] = input::get_game_parameters_for_replay(this, recording_path); + + auto layout = ui::FullScreenLayout{ + shapes::URect{ { 0, 0 }, m_size } + }; + + std::vector layouts = game::get_layouts_for(parameters.size(), layout); + + m_clock = std::make_shared(); + + for (decltype(parameters.size()) i = 0; i < parameters.size(); ++i) { + auto [input, starting_parameters] = std::move(parameters.at(i)); + + m_games.emplace_back( + std::make_unique(this, std::move(input), starting_parameters, m_clock, layouts.at(i), false) + ); + } +} + + +VideoRenderer::~VideoRenderer() { + if (m_surface) { + SDL_FreeSurface(m_surface.get()); + } +} + + +std::optional VideoRenderer::render( + const std::string& destination_path, + u32 fps, + const std::function& progress_callback +) { + + auto backend = VideoRendererBackend{ destination_path }; + + if (auto result = backend.setup(fps, m_size); result.has_value()) { + return fmt::format("No video renderer backend available: {}", result.value()); + } + + auto all_games_finished = [this]() -> bool { + for (const auto& game : m_games) { + if (not game->is_game_finished()) { + return false; + } + } + + return true; + }; + + //TODO: this is just a dummy thing atm, change that + double progress = 0.0; + + while (not all_games_finished()) { + progress_callback(progress); + + m_renderer->clear(); + + for (const auto& game : m_games) { + if (not game->is_game_finished()) { + game->update(); + game->render(*this); + } + } + + m_renderer->present(); + + if (not backend.add_frame(m_surface.get())) { + break; + } + + m_clock->increment_simulation_step_index(); + + progress += 0.1; + + progress_callback(progress); + } + + if (not backend.finish(false)) { + return "Renderer failed"; + } + return std::nullopt; +} + +std::vector VideoRendererBackend::get_encoding_paramaters( + u32 fps, + shapes::UPoint size, + const std::filesystem::path& destination_path +) { + + const std::string resolution = fmt::format("{}x{}", size.x, size.y); + + const std::string framerate = fmt::format("{}", fps); + + return { + "-loglevel", +#if !defined(NDEBUG) + "verbose", +#else + "warning", +#endif + "-y", // always overwrite video + "-f", + "rawvideo", + "-pix_fmt", + "bgra", + "-s", + resolution, + "-r", + framerate, + "-i", + "-", + "-c:v", + "libx264", + "-crf", + "20", + "-pix_fmt", + "yuv420p", + destination_path.string(), + }; +} + + +// implementation of ServiceProvider + +[[nodiscard]] EventDispatcher& VideoRenderer::event_dispatcher() { + return m_main_provider->event_dispatcher(); +} + +[[nodiscard]] const EventDispatcher& VideoRenderer::event_dispatcher() const { + return m_main_provider->event_dispatcher(); +} + +FontManager& VideoRenderer::font_manager() { + return m_main_provider->font_manager(); +} + +[[nodiscard]] const FontManager& VideoRenderer::font_manager() const { + return m_main_provider->font_manager(); +} + +CommandLineArguments& VideoRenderer::command_line_arguments() { + return m_main_provider->command_line_arguments(); +} + +[[nodiscard]] const CommandLineArguments& VideoRenderer::command_line_arguments() const { + return m_main_provider->command_line_arguments(); +} + +SettingsManager& VideoRenderer::settings_manager() { + return m_main_provider->settings_manager(); +} + +[[nodiscard]] const SettingsManager& VideoRenderer::settings_manager() const { + return m_main_provider->settings_manager(); +} + +MusicManager& VideoRenderer::music_manager() { + return m_main_provider->music_manager(); +} + +[[nodiscard]] const MusicManager& VideoRenderer::music_manager() const { + return m_main_provider->music_manager(); +} + +[[nodiscard]] const Renderer& VideoRenderer::renderer() const { + return *m_renderer; +} + +[[nodiscard]] const Window& VideoRenderer::window() const { + return m_main_provider->window(); +} + +[[nodiscard]] Window& VideoRenderer::window() { + return m_main_provider->window(); +} + +[[nodiscard]] input::InputManager& VideoRenderer::input_manager() { + return m_main_provider->input_manager(); +} + +[[nodiscard]] const input::InputManager& VideoRenderer::input_manager() const { + return m_main_provider->input_manager(); +} + +[[nodiscard]] const std::unique_ptr& VideoRenderer::api() const { + return m_main_provider->api(); +} + +#if defined(_HAVE_DISCORD_SDK) + +[[nodiscard]] std::optional& VideoRenderer::discord_instance() { + return m_main_provider->discord_instance(); +} + +[[nodiscard]] const std::optional& VideoRenderer::discord_instance() const { + return m_main_provider->discord_instance(); +} + +#endif + + +#if defined(__EMSCRIPTEN__) + +[[nodiscard]] web::WebContext& VideoRenderer::web_context() { + return m_main_provider->web_context(); +} + +[[nodiscard]] const web::WebContext& VideoRenderer::web_context() const { + return m_main_provider->web_context(); +} + +#endif diff --git a/src/graphics/video_renderer.hpp b/src/graphics/video_renderer.hpp new file mode 100644 index 00000000..d3b2b09b --- /dev/null +++ b/src/graphics/video_renderer.hpp @@ -0,0 +1,114 @@ + + +#pragma once + +#include +#include + +#include "game/game.hpp" +#include "graphics/rect.hpp" +#include "helper/windows.hpp" +#include "manager/service_provider.hpp" +#include "renderer.hpp" + +struct VideoRenderer : ServiceProvider { +private: + std::unique_ptr m_surface; + std::unique_ptr m_renderer; + std::vector> m_games; + ServiceProvider* m_main_provider; + shapes::UPoint m_size; + std::shared_ptr m_clock; + + void initialize_games(const std::filesystem::path& recording_path); + +public: + OOPETRIS_GRAPHICS_EXPORTED explicit VideoRenderer( + ServiceProvider* service_provider, + const std::filesystem::path& recording_path, + shapes::UPoint size + ); + + std::optional + render(const std::string& destination_path, u32 fps, const std::function& progress_callback); + + ~VideoRenderer() override; + + + // implementation of ServiceProvider + + [[nodiscard]] EventDispatcher& event_dispatcher() override; + + [[nodiscard]] const EventDispatcher& event_dispatcher() const override; + + FontManager& font_manager() override; + + [[nodiscard]] const FontManager& font_manager() const override; + + CommandLineArguments& command_line_arguments() override; + + [[nodiscard]] const CommandLineArguments& command_line_arguments() const override; + + SettingsManager& settings_manager() override; + + [[nodiscard]] const SettingsManager& settings_manager() const override; + + MusicManager& music_manager() override; + + [[nodiscard]] const MusicManager& music_manager() const override; + + [[nodiscard]] const Renderer& renderer() const override; + + [[nodiscard]] const Window& window() const override; + + [[nodiscard]] Window& window() override; + + [[nodiscard]] input::InputManager& input_manager() override; + + [[nodiscard]] const input::InputManager& input_manager() const override; + + [[nodiscard]] const std::unique_ptr& api() const override; + +#if defined(_HAVE_DISCORD_SDK) + + [[nodiscard]] std::optional& discord_instance() override; + + [[nodiscard]] const std::optional& discord_instance() const override; + +#endif + +#if defined(__EMSCRIPTEN__) + + [[nodiscard]] web::WebContext& web_context() override; + [[nodiscard]] const web::WebContext& web_context() const override; + +#endif +}; + + +struct Decoder; + + +//TODO(Totto): also support library and not only subprocess call +// See e.g. https://github.com/Raveler/ffmpeg-cpp +struct VideoRendererBackend { +private: + std::filesystem::path m_destination_path; + std::unique_ptr m_decoder; + + [[nodiscard]] static std::vector + get_encoding_paramaters(u32 fps, shapes::UPoint size, const std::filesystem::path& destination_path); + +public: + OOPETRIS_GRAPHICS_EXPORTED explicit VideoRendererBackend(std::filesystem::path destination_path); + + OOPETRIS_GRAPHICS_EXPORTED ~VideoRendererBackend(); + + OOPETRIS_GRAPHICS_EXPORTED static void is_supported_async(const std::function& callback); + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::optional setup(u32 fps, shapes::UPoint size); + + [[nodiscard]] bool add_frame(SDL_Surface* surface); + + [[nodiscard]] bool finish(bool cancel); +}; diff --git a/src/graphics/video_renderer_embedded.cpp b/src/graphics/video_renderer_embedded.cpp new file mode 100644 index 00000000..617363c2 --- /dev/null +++ b/src/graphics/video_renderer_embedded.cpp @@ -0,0 +1,555 @@ + + +#include "helper/c_helpers.hpp" +#include "helper/constants.hpp" +#include "helper/git_helper.hpp" +#include "helper/graphic_utils.hpp" +#include "video_renderer.hpp" + +#if defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif + +extern "C" { + +#include +#include +#include +#include +#include +} + + +#include +#include +#include +#include + +#if defined(__NINTENDO_CONSOLE__) && defined(__SWITCH__) +#include +#include +#include +#include +#include +#include + +#ifndef INADDR_LOOPBACK +// 127.0.0.1 +#define INADDR_LOOPBACK (static_cast(0x7f000001)) +#endif + +#endif + +struct Decoder { + int input_fd; + std::future> encoding_thread; + std::atomic should_cancel; +}; + +// general information and usage from: https://ffmpeg.org//doxygen/trunk/index.html +// and https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/README +VideoRendererBackend::VideoRendererBackend(std::filesystem::path destination_path) + : m_destination_path{ std::move(destination_path) }, + m_decoder{ nullptr } { } + +VideoRendererBackend::~VideoRendererBackend() = default; + +namespace { + + constexpr const int read_end = 0; + constexpr const int write_end = 1; + + constexpr const size_t buf_len = 1024; + + std::string av_error_to_string(int errnum) { + auto* buf = new char[buf_len]; //NOLINT(cppcoreguidelines-owning-memory) + auto* buff_res = av_make_error_string(buf, buf_len, errnum); + if (buff_res == nullptr) { + return "Unknown error"; + } + + std::string result{ buff_res }; + + delete[] buf; //NOLINT(cppcoreguidelines-owning-memory) + + return result; + } + + std::optional start_encoding( //NOLINT(readability-function-cognitive-complexity) + u32 fps, + shapes::UPoint size, + const std::filesystem::path& destination_path, + const std::string& input_url, + const std::unique_ptr& decoder + ) { + + ScopeDeferMultiple scope_defer{}; + +#if !defined(NDEBUG) + // "-loglevel verbose" + av_log_set_level(AV_LOG_VERBOSE); +#else + // "-loglevel warning" + av_log_set_level(AV_LOG_WARNING); +#endif + // input setup + + AVFormatContext* input_format_ctx = avformat_alloc_context(); + if (input_format_ctx == nullptr) { + return fmt::format("Cannot allocate an input format context"); + } + + const std::string resolution = fmt::format("{}x{}", size.x, size.y); + + const std::string framerate = fmt::format("{}", fps); + + // "-f rawvideo" + const AVInputFormat* input_fmt = av_find_input_format("rawvideo"); + + if (input_fmt == nullptr) { + return "Couldn't find input format"; + } + + AVDictionary* input_options = nullptr; + // "-pix_fmt bgra" + av_dict_set(&input_options, "pixel_format", "bgra", 0); + // "-s {resolution}" + av_dict_set(&input_options, "video_size", resolution.c_str(), 0); + // "-r {framerate}" + av_dict_set(&input_options, "framerate", framerate.c_str(), 0); + + + // "-i {input_url}" + auto av_input_ret = avformat_open_input(&input_format_ctx, input_url.c_str(), input_fmt, &input_options); + if (av_input_ret != 0) { + return fmt::format("Could not open input file stdin: {}", av_error_to_string(av_input_ret)); + } + + scope_defer.add([&input_format_ctx](void*) { avformat_close_input(&input_format_ctx); }, nullptr); + + AVDictionaryEntry* unrecognized_key_inp = av_dict_get(input_options, "", nullptr, AV_DICT_IGNORE_SUFFIX); + if (unrecognized_key_inp != nullptr) { + return fmt::format("Option {} not recognized by the demuxer", unrecognized_key_inp->key); + } + + av_dict_free(&input_options); + + auto av_stream_info_ret = avformat_find_stream_info(input_format_ctx, nullptr); + if (av_stream_info_ret < 0) { + return fmt::format("Cannot find stream information: {}", av_error_to_string(av_stream_info_ret)); + } + + + // select the video stream + const AVCodec* input_decoder = nullptr; + auto video_stream_index = av_find_best_stream(input_format_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &input_decoder, 0); + if (video_stream_index < 0) { + return fmt::format( + "Cannot find a video stream in the input file: {}", av_error_to_string(video_stream_index) + ); + } + + AVStream* input_video_stream = + input_format_ctx->streams[video_stream_index]; //NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + + AVCodecContext* input_codec_context = avcodec_alloc_context3(input_decoder); + if (input_codec_context == nullptr) { + return fmt::format("Cannot allocate a input codec context"); + } + + scope_defer.add([&input_codec_context](void*) { avcodec_free_context(&input_codec_context); }, nullptr); + + auto codec_paramaters_ret = avcodec_parameters_to_context(input_codec_context, input_video_stream->codecpar); + if (codec_paramaters_ret < 0) { + return fmt::format( + "Cannot set the input codec context parameters: {}", av_error_to_string(codec_paramaters_ret) + ); + } + + /* Inform the decoder about the timebase for the packet timestamps. + * This is highly recommended, but not mandatory. */ + input_codec_context->pkt_timebase = input_video_stream->time_base; + + //NOTE: we also could set this to the provided u32, but this also uses that and converts it to the expected format (fractional) + input_codec_context->framerate = av_guess_frame_rate(input_format_ctx, input_video_stream, nullptr); + + auto codec_open_ret = avcodec_open2(input_codec_context, input_decoder, nullptr); + if (codec_open_ret != 0) { + return fmt::format("Cannot initializer the codec for the input: {}", av_error_to_string(codec_open_ret)); + } + + av_dump_format(input_format_ctx, 0, input_url.c_str(), 0); + + // output setup + + // "-c:v libx264" (h264) + const AVCodec* output_encoder = avcodec_find_encoder(AV_CODEC_ID_H264); + if (output_encoder == nullptr) { + return "Cannot find encoder h264"; + } + + + AVFormatContext* output_format_ctx = avformat_alloc_context(); + if (output_format_ctx == nullptr) { + return fmt::format("Cannot allocate an output format context"); + } + + scope_defer.add([output_format_ctx](void*) { avformat_free_context(output_format_ctx); }, nullptr); + + auto av_output_ret = + avformat_alloc_output_context2(&output_format_ctx, nullptr, "mp4", destination_path.c_str()); + + if (av_output_ret < 0) { + return fmt::format("Could not alloc output file {}: {}", destination_path.string(), av_output_ret); + } + + const std::string encoder_metadata_name = fmt::format( + "{} v{} ({}) {}", constants::program_name.string(), constants::version.string(), utils::git_commit(), + LIBAVFORMAT_IDENT + ); + + av_dict_set(&output_format_ctx->metadata, "encoder", encoder_metadata_name.c_str(), 0); + + AVStream* out_stream = avformat_new_stream(output_format_ctx, nullptr); + if (out_stream == nullptr) { + return fmt::format("Cannot allocate an output stream"); + } + + AVCodecContext* output_codec_context = avcodec_alloc_context3(output_encoder); + if (out_stream == nullptr) { + return fmt::format("Cannot allocate an output codec context"); + } + + scope_defer.add([&output_codec_context](void*) { avcodec_free_context(&output_codec_context); }, nullptr); + + output_codec_context->height = input_codec_context->height; + output_codec_context->width = input_codec_context->width; + output_codec_context->sample_aspect_ratio = input_codec_context->sample_aspect_ratio; + output_codec_context->framerate = input_codec_context->framerate; + + // video time_base can be set to whatever is handy and supported by encoder + output_codec_context->time_base = av_inv_q(input_codec_context->framerate); + + AVDictionary* output_options = nullptr; + // "-pix_fmt yuv420p" + av_dict_set(&output_options, "pixel_format", "yuv420p", 0); + // "-crf 20" + av_dict_set(&output_options, "crf", "20", 0); + + av_dict_set(&output_options, "video_size", resolution.c_str(), 0); + + + auto codec_open_out_ret = avcodec_open2(output_codec_context, output_encoder, &output_options); + if (codec_open_out_ret != 0) { + return fmt::format( + "Cannot initializer the codec for the output: {}", av_error_to_string(codec_open_out_ret) + ); + } + + AVDictionaryEntry* unrecognized_key_outp = av_dict_get(output_options, "", nullptr, AV_DICT_IGNORE_SUFFIX); + if (unrecognized_key_outp != nullptr) { + return fmt::format("Option {} not recognized by the muxer", unrecognized_key_outp->key); + } + + av_dict_free(&output_options); + + auto codec_params_ret = avcodec_parameters_from_context(out_stream->codecpar, output_codec_context); + if (codec_params_ret < 0) { + return fmt::format( + "Failed to copy encoder parameters to output stream: {}\n", av_error_to_string(codec_params_ret) + ); + } + + + out_stream->time_base = output_codec_context->time_base; + + const std::string stream_encoder_metadata_name = fmt::format( + "{} v{} ({}) {} {}", constants::program_name.string(), constants::version.string(), utils::git_commit(), + LIBAVCODEC_IDENT, output_encoder->name + ); + + av_dict_set(&out_stream->metadata, "encoder", stream_encoder_metadata_name.c_str(), 0); + + av_dump_format(output_format_ctx, 0, destination_path.c_str(), 1); + + // now do the actual work + + auto file_open_ret = avio_open(&output_format_ctx->pb, destination_path.c_str(), AVIO_FLAG_WRITE); + if (file_open_ret < 0) { + return fmt::format( + "Could not open output file '{}': {}", destination_path.string(), av_error_to_string(file_open_ret) + ); + } + + scope_defer.add([&output_format_ctx](void*) { avio_closep(&output_format_ctx->pb); }, nullptr); + + auto header_ret = avformat_write_header(output_format_ctx, nullptr); + if (header_ret < 0) { + return fmt::format( + "Error occurred when opening output file to write headers: {}", av_error_to_string(header_ret) + ); + } + + AVPacket* pkt = av_packet_alloc(); + if (pkt == nullptr) { + return "Could not allocate AVPacket"; + } + + scope_defer.add([&pkt](void*) { av_packet_free(&pkt); }, nullptr); + + AVFrame* decode_frame = av_frame_alloc(); + + if (decode_frame == nullptr) { + return "Could not allocate decode AVFrame"; + } + + scope_defer.add([&decode_frame](void*) { av_frame_free(&decode_frame); }, nullptr); + + + decode_frame->format = input_codec_context->pix_fmt; + decode_frame->width = input_codec_context->width; + decode_frame->height = input_codec_context->height; + + auto frame_buffer_ret = av_frame_get_buffer(decode_frame, 0); + if (frame_buffer_ret < 0) { + return fmt::format("Could not allocate decode frame buffer: {}", av_error_to_string(frame_buffer_ret)); + } + + AVFrame* encode_frame = av_frame_alloc(); + + if (encode_frame == nullptr) { + return "Could not allocate encode AVFrame"; + } + + scope_defer.add([&encode_frame](void*) { av_frame_free(&encode_frame); }, nullptr); + + + encode_frame->format = output_codec_context->pix_fmt; + encode_frame->width = output_codec_context->width; + encode_frame->height = output_codec_context->height; + + auto outp_frame_buffer_ret = av_frame_get_buffer(encode_frame, 0); + if (outp_frame_buffer_ret < 0) { + return fmt::format("Could not allocate encode frame buffer: {}", av_error_to_string(outp_frame_buffer_ret)); + } + + // allocate conversion context (for frame conversion) + SwsContext* sws_ctx = sws_getContext( + input_codec_context->width, input_codec_context->height, input_codec_context->pix_fmt, + output_codec_context->width, output_codec_context->height, output_codec_context->pix_fmt, SWS_BICUBIC, + nullptr, nullptr, nullptr + ); + if (sws_ctx == nullptr) { + return "Could not allocate conversion context"; + } + + while (true) { + // check atomic bool, if we are cancelled + // NOTE: the video is garbage after this, since we don't close it correctly (which isn't the intention of this) + if (decoder && decoder->should_cancel) { + return std::nullopt; + } + + // retrieve unencoded (raw) packet from input + auto read_frame_ret = av_read_frame(input_format_ctx, pkt); + if (read_frame_ret == AVERROR_EOF) { + break; + } + + if (read_frame_ret < 0) { + return fmt::format("Receiving a frame from the input failed: {}", av_error_to_string(read_frame_ret)); + } + + // send raw packet in packet to decoder + auto send_pkt_ret = avcodec_send_packet(input_codec_context, pkt); + if (send_pkt_ret != 0) { + if (send_pkt_ret == AVERROR(EAGAIN)) { + return "Decoding failed: Output was not read correctly"; + } + return fmt::format("Decoding failed: {}", av_error_to_string(send_pkt_ret)); + } + + int read_ret = 0; + + // encode and write as much frames as possible + while (read_ret >= 0) { + + // get decoded frame, if one is present + read_ret = avcodec_receive_frame(input_codec_context, decode_frame); + if (read_ret == AVERROR(EAGAIN) || read_ret == AVERROR_EOF) { + break; + } + + if (read_ret < 0) { + return fmt::format("Receiving a frame from the decoder failed: {}", av_error_to_string(read_ret)); + } + + // convert to correct output pixel format + read_ret = sws_scale_frame(sws_ctx, encode_frame, decode_frame); + if (read_ret < 0) { + return fmt::format("Frame conversion failed: {}", av_error_to_string(read_ret)); + } + + // copy the pts from the decoded frame + encode_frame->pts = decode_frame->pts; + + // encode decoded and converted frame with output encoder + read_ret = avcodec_send_frame(output_codec_context, encode_frame); + if (read_ret != 0) { + return fmt::format("Encoding failed: {}", av_error_to_string(read_ret)); + } + + int write_ret = 0; + + // write all encoded packets + while (write_ret >= 0) { + + // get encoded packet, if one is present + write_ret = avcodec_receive_packet(output_codec_context, pkt); + if (write_ret == AVERROR(EAGAIN) || write_ret == AVERROR_EOF) { + break; + } + + if (write_ret < 0) { + return fmt::format( + "Receiving a packet from the encoder failed: {}", av_error_to_string(write_ret) + ); + } + + // prepare packet for muxing + pkt->stream_index = out_stream->index; + + // rescale output packet timestamp values from codec to stream timebase + av_packet_rescale_ts(pkt, output_codec_context->time_base, out_stream->time_base); + + + // Write the compressed packet (frame inside that) to the media file. + write_ret = av_interleaved_write_frame(output_format_ctx, pkt); + /* pkt is now blank (av_interleaved_write_frame() takes ownership of + * its contents and resets pkt), so that no unreferencing is necessary. + * This would be different if one used av_write_frame(). */ + if (write_ret < 0) { + return fmt::format("Writing an output packet failed: {}", av_error_to_string(write_ret)); + } + } + } + } + + // flush encoder and decoder + // this is not necessary atm, but may be necessary in the future + //TODO(Totto): do it nevertheless + //NOTE: this is the case, since we send whole frames at once, trough the pipe, so if that changes, the video might get corrupted or miss a frame at the end + + // write the trailer, some video containers require this, like e.g. mp4 + auto trailer_ret = av_write_trailer(output_format_ctx); + if (trailer_ret != 0) { + return fmt::format("Writing the trailer failed: {}", av_error_to_string(trailer_ret)); + } + + return std::nullopt; + } + +} // namespace + + +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + +void VideoRendererBackend::is_supported_async(const std::function& callback) { + callback(true); +} + + +std::optional VideoRendererBackend::setup(u32 fps, shapes::UPoint size) { + +// see: https://ffmpeg.org/ffmpeg-protocols.html +#if defined(__NINTENDO_CONSOLE__) && defined(__SWITCH__) + int socket_fd = socket(AF_INET, SOCK_STREAM, 0); + if (socket_fd < 0) { + return fmt::format("Could not create a UNIX socket: {}", strerror(errno)); + } + + u16 port = 1045; + std::string input_url = fmt::format("tcp://localhost:{}?listen=1", port); + int close_fd = -1; + +#else + std::array pipefd = { 0, 0 }; + + if (pipe(pipefd.data()) < 0) { + return fmt::format("Could not create a pipe: {}", strerror(errno)); + } + const int close_fd = pipefd[read_end]; + const int input_fd = pipefd[write_end]; + const std::string input_url = fmt::format("pipe:{}", close_fd); +#endif + + std::future> encoding_thread = + std::async(std::launch::async, [close_fd, input_url, fps, size, this]() -> std::optional { + utils::set_thread_name("ffmpeg encoder"); + auto result = start_encoding(fps, size, this->m_destination_path, input_url, this->m_decoder); + + if (close_fd >= 0) { + if (close(close_fd) < 0) { + spdlog::warn("could not close read end of the pipe: {}", strerror(errno)); + } + } + + if (result.has_value()) { + return fmt::format("ffmpeg error: {}", result.value()); + } + + return std::nullopt; + }); + +#if defined(__NINTENDO_CONSOLE__) && defined(__SWITCH__) + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + + // localhost + addr.sin_addr.s_addr = INADDR_LOOPBACK; + + int input_fd = connect(socket_fd, reinterpret_cast(&addr), sizeof(addr)); + if (input_fd < 0) { + return fmt::format("Could not connect to a TCP socket: {}", strerror(errno)); + } +#endif + + m_decoder = std::make_unique(input_fd, std::move(encoding_thread), false); + + return std::nullopt; +} + +bool VideoRendererBackend::add_frame(SDL_Surface* surface) { + + if (write(m_decoder->input_fd, surface->pixels, static_cast(surface->h) * surface->pitch) < 0) { + spdlog::error("failed to write into ffmpeg pipe: {}", strerror(errno)); + return false; + } + + return true; +} + +bool VideoRendererBackend::finish(bool cancel) { + + if (cancel) { + m_decoder->should_cancel = true; + } + + if (close(m_decoder->input_fd) < 0) { + spdlog::warn("could not close write end of the pipe: {}", strerror(errno)); + } + + m_decoder->encoding_thread.wait(); + auto result = m_decoder->encoding_thread.get(); + if (result.has_value()) { + spdlog::error("FFMPEG error: {}", result.value()); + return false; + } + return true; +} diff --git a/src/graphics/video_renderer_unix.cpp b/src/graphics/video_renderer_unix.cpp new file mode 100644 index 00000000..c7fa0bb2 --- /dev/null +++ b/src/graphics/video_renderer_unix.cpp @@ -0,0 +1,133 @@ + +#include "video_renderer.hpp" + +#include +#include + +#include + +#include +#include +#include +#include + +struct Decoder { + int pipe; + pid_t pid; +}; + +// inspired by: https://github.com/tsoding/musializer/blob/762a729ff69ba1f984b0f2604e0eac08af46327c/src/ffmpeg_linux.c +VideoRendererBackend::VideoRendererBackend(std::filesystem::path destination_path) + : m_destination_path{ std::move(destination_path) }, + m_decoder{ nullptr } { } + +VideoRendererBackend::~VideoRendererBackend() = default; + +namespace { + + constexpr const int read_end = 0; + constexpr const int write_end = 1; + +} // namespace + + +void VideoRendererBackend::is_supported_async(const std::function& callback) { + //TODO: detect if we have the ffmpeg executable on the path + callback(false); +} + + +std::optional VideoRendererBackend::setup(u32 fps, shapes::UPoint size) { + + std::array pipefd = { 0, 0 }; + + if (pipe(pipefd.data()) < 0) { + return fmt::format("FFMPEG: Could not create a pipe: {}", strerror(errno)); + } + + const pid_t child = fork(); + if (child < 0) { + return fmt::format("FFMPEG: could not fork a child: {}", strerror(errno)); + } + + if (child == 0) { + if (dup2(pipefd.at(read_end), STDIN_FILENO) < 0) { + std::cerr << "FFMPEG CHILD: could not reopen read end of pipe as stdin: " << strerror(errno) << "\n"; + std::exit(1); + } + close(pipefd[write_end]); + + + auto paramaters = VideoRendererBackend::get_encoding_paramaters(fps, size, m_destination_path); + + std::vector args = { "ffmpeg" }; + for (const auto& parameter : paramaters) { + args.push_back(parameter.c_str()); + } + + args.push_back(nullptr); + //TODO(Totto): support audio, that loops the music as in the main game + const int ret = + execvp("ffmpeg", + const_cast(args.data())); // NOLINT(cppcoreguidelines-pro-type-const-cast) + if (ret < 0) { + std::cerr << "FFMPEG CHILD: could not run ffmpeg as a child process: " << strerror(errno) << "\n"; + std::exit(1); + } + UNREACHABLE(); + std::exit(1); + } + + if (close(pipefd[read_end]) < 0) { + spdlog::error("FFMPEG: could not close read end of the pipe on the parent's end: {}", strerror(errno)); + } + + m_decoder = std::make_unique(pipefd[write_end], child); + return std::nullopt; +} + +bool VideoRendererBackend::add_frame(SDL_Surface* surface) { + + if (write(m_decoder->pipe, surface->pixels, static_cast(surface->h) * surface->pitch) < 0) { + spdlog::error("FFMPEG: failed to write into ffmpeg pipe: {}", strerror(errno)); + return false; + } + return true; +} + +bool VideoRendererBackend::finish(bool cancel) { + + + if (close(m_decoder->pipe) < 0) { + spdlog::warn("FFMPEG: could not close write end of the pipe on the parent's end: {}", strerror(errno)); + } + + if (cancel) { + kill(m_decoder->pid, SIGKILL); + } + + while (true) { + int wstatus = 0; + if (waitpid(m_decoder->pid, &wstatus, 0) < 0) { + spdlog::error("FFMPEG: could not wait for ffmpeg child process to finish: {}", strerror(errno)); + return false; + } + + if (WIFEXITED(wstatus)) { + int exit_status = WEXITSTATUS(wstatus); + if (exit_status != 0) { + spdlog::error("FFMPEG: ffmpeg exited with code {}", exit_status); + return false; + } + + return true; + } + + if (WIFSIGNALED(wstatus)) { + spdlog::error("FFMPEG: ffmpeg got terminated by {}", strsignal(WTERMSIG(wstatus))); + return false; + } + } + + UNREACHABLE(); +} diff --git a/src/graphics/video_renderer_windows.cpp b/src/graphics/video_renderer_windows.cpp new file mode 100644 index 00000000..9eb24d20 --- /dev/null +++ b/src/graphics/video_renderer_windows.cpp @@ -0,0 +1,168 @@ + +#include "video_renderer.hpp" + +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include + +#include + +struct Decoder { + HANDLE hProcess; + HANDLE hPipeWrite; +}; + +// inspired by: https://github.com/tsoding/musializer/blob/762a729ff69ba1f984b0f2604e0eac08af46327c/src/ffmpeg_windows.c +VideoRendererBackend::VideoRendererBackend(std::filesystem::path destination_path) + : m_destination_path{ std::move(destination_path) }, + m_decoder{ nullptr } { } + +VideoRendererBackend::~VideoRendererBackend() = default; + +void VideoRendererBackend::is_supported_async(const std::function& callback) { + //TODO: detect if we have the ffmpeg executable on the path + callback(false); +} + + +std::optional VideoRendererBackend::setup(u32 fps, shapes::UPoint size) { + + HANDLE pipe_read; + HANDLE pipe_write; + + SECURITY_ATTRIBUTES saAttr = { .nLength = sizeof(SECURITY_ATTRIBUTES), + .lpSecurityDescriptor = nullptr, + .bInheritHandle = TRUE }; + + + if (!CreatePipe(&pipe_read, &pipe_write, &saAttr, 0)) { + return fmt::format("FFMPEG: Could not create pipe. System Error Code: {}", GetLastError()); + } + + if (!SetHandleInformation(pipe_write, HANDLE_FLAG_INHERIT, 0)) { + return fmt::format( + "FFMPEG: Could not mark write pipe as non-inheritable. System Error Code: {}", GetLastError() + ); + } + + // https://docs.microsoft.com/en-us/windows/win32/procthread/creating-a-child-process-with-redirected-input-and-output + + STARTUPINFO siStartInfo; + ZeroMemory(&siStartInfo, sizeof(siStartInfo)); + siStartInfo.cb = sizeof(STARTUPINFO); + // NOTE: theoretically setting NULL to std handles should not be a problem + // https://docs.microsoft.com/en-us/windows/console/getstdhandle?redirectedfrom=MSDN#attachdetach-behavior + siStartInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE); + if (siStartInfo.hStdError == INVALID_HANDLE_VALUE) { + return fmt::format( + "FFMPEG: Could get standard error handle for the child. System Error Code: {}", GetLastError() + ); + } + siStartInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + if (siStartInfo.hStdOutput == INVALID_HANDLE_VALUE) { + return fmt::format( + "FFMPEG: Could get standard output handle for the child. System Error Code: {}", GetLastError() + ); + } + siStartInfo.hStdInput = pipe_read; + siStartInfo.dwFlags |= STARTF_USESTDHANDLES; + + PROCESS_INFORMATION piProcInfo; + ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION)); + + + auto paramaters = VideoRendererBackend::get_encoding_paramaters(fps, size, m_destination_path); + + std::stringstream args{}; + args << "ffmpeg.exe"; + + for (const auto& parameter : paramaters) { + args << " "; + if (parameter.find(" ") != std::string::npos) { + args << "\""; + args << parameter; + args << "\""; + + } else { + args << parameter; + } + } + + std::string result = args.str(); + auto str_size = result.size(); + + using UniqueCharArray = std::unique_ptr>; + + UniqueCharArray raw_args{ new char[str_size + 1], [](const char* const char_value) { + if (char_value == nullptr) { + return; + } + + delete[] char_value; // NOLINT(cppcoreguidelines-owning-memory) + } }; + + std::memcpy(raw_args.get(), result.c_str(), str_size); + raw_args.get()[str_size] = '\0'; + + + if (!CreateProcess(NULL, raw_args.get(), NULL, NULL, TRUE, 0, NULL, NULL, &siStartInfo, &piProcInfo)) { + CloseHandle(pipe_write); + CloseHandle(pipe_read); + + return fmt::format("FFMPEG: Could not create child process. System Error Code: {}", GetLastError()); + } + + CloseHandle(pipe_read); + CloseHandle(piProcInfo.hThread); + + m_decoder = std::make_unique(piProcInfo.hProcess, pipe_write); + return std::nullopt; +} + +bool VideoRendererBackend::add_frame(SDL_Surface* surface) { + DWORD written{}; + + if (not WriteFile( + m_decoder->hPipeWrite, surface->pixels, static_cast(surface->h) * surface->pitch, &written, NULL + )) { + spdlog::error("FFMPEG: failed to write into ffmpeg pipe. System Error Code: {}", GetLastError()); + return false; + } + return true; +} + +bool VideoRendererBackend::finish(bool cancel) { + + FlushFileBuffers(m_decoder->hPipeWrite); + CloseHandle(m_decoder->hPipeWrite); + + if (cancel) { + TerminateProcess(m_decoder->hProcess, 1); + } + + if (WaitForSingleObject(m_decoder->hProcess, INFINITE) == WAIT_FAILED) { + spdlog::error("FFMPEG: could not wait on child process. System Error Code: {}", GetLastError()); + CloseHandle(m_decoder->hProcess); + return false; + } + + DWORD exit_status{}; + if (GetExitCodeProcess(m_decoder->hProcess, &exit_status) == 0) { + spdlog::error("FFMPEG: could not get process exit code. System Error Code: {}", GetLastError()); + CloseHandle(m_decoder->hProcess); + return false; + } + + if (exit_status != 0) { + spdlog::error("FFMPEG: command exited with exit code {}", exit_status); + CloseHandle(m_decoder->hProcess); + return false; + } + + CloseHandle(m_decoder->hProcess); + + return true; +} diff --git a/src/helper/c_helpers.hpp b/src/helper/c_helpers.hpp new file mode 100644 index 00000000..86e875c9 --- /dev/null +++ b/src/helper/c_helpers.hpp @@ -0,0 +1,50 @@ + + +#pragma once + +#include + +template +struct ScopeDefer { +private: + using CallbackType = std::function; + const CallbackType m_callback; + const Arg m_cleanup_value; + +public: + ScopeDefer(const ScopeDefer&) = delete; // no copy constructor + ScopeDefer& operator=(const ScopeDefer&) = delete; // no self-assignments (aka copy assignment) + + ScopeDefer(CallbackType&& callback, Arg cleanup_value) + : m_callback{ std::move(callback) }, + m_cleanup_value{ cleanup_value } { } + + ~ScopeDefer() { + this->m_callback(this->m_cleanup_value); + } +}; + + +template +struct ScopeDeferMultiple { +private: + using CallbackType = std::function; + std::vector> m_values{}; + +public: + ScopeDeferMultiple(const ScopeDeferMultiple&) = delete; // no copy constructor + ScopeDeferMultiple& operator=(const ScopeDeferMultiple&) = delete; // no self-assignments (aka copy assignment) + + ScopeDeferMultiple() = default; + + void add(CallbackType&& callback, Arg cleanup_value) { + m_values.emplace_back(std::move(callback), std::move(cleanup_value)); + } + + ~ScopeDeferMultiple() { + for (auto it = m_values.rbegin(); it != m_values.rend(); ++it) { + const auto& [callback, value] = *it; + callback(value); + } + } +}; diff --git a/src/helper/clock_source.cpp b/src/helper/clock_source.cpp index 385d8247..8d3fae60 100644 --- a/src/helper/clock_source.cpp +++ b/src/helper/clock_source.cpp @@ -1,7 +1,9 @@ #include "helper/clock_source.hpp" +#include #include #include +#include #include namespace { @@ -45,3 +47,31 @@ double LocalClock::resume() { spdlog::info("resuming clock (duration of pause: {} s)", duration); return duration; } + + +ManualClock::ManualClock() = default; + +[[nodiscard]] SimulationStep ManualClock::simulation_step_index() const { + return m_simulation_step_index; +} + +bool ManualClock::can_be_paused() { + return false; +} + +void ManualClock::pause() { + UNREACHABLE(); +} + +double ManualClock::resume() { + UNREACHABLE(); +} + + +void ManualClock::increment_simulation_step_index() { + ++m_simulation_step_index; +} + +void ManualClock::set_simulation_step_index(SimulationStep index) { + m_simulation_step_index = index; +} diff --git a/src/helper/clock_source.hpp b/src/helper/clock_source.hpp index bd3ad3c5..404ba549 100644 --- a/src/helper/clock_source.hpp +++ b/src/helper/clock_source.hpp @@ -40,3 +40,19 @@ struct LocalClock : public ClockSource { OOPETRIS_GRAPHICS_EXPORTED void pause() override; OOPETRIS_GRAPHICS_EXPORTED double resume() override; }; + + +struct ManualClock : public ClockSource { +private: + SimulationStep m_simulation_step_index{ 0 }; + +public: + OOPETRIS_GRAPHICS_EXPORTED explicit ManualClock(); + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] SimulationStep simulation_step_index() const override; + OOPETRIS_GRAPHICS_EXPORTED bool can_be_paused() override; + OOPETRIS_GRAPHICS_EXPORTED void pause() override; + OOPETRIS_GRAPHICS_EXPORTED double resume() override; + + OOPETRIS_GRAPHICS_EXPORTED void increment_simulation_step_index(); + OOPETRIS_GRAPHICS_EXPORTED void set_simulation_step_index(SimulationStep index); +}; diff --git a/src/helper/graphic_utils.cpp b/src/helper/graphic_utils.cpp index 2bafe4c3..5bb47e4b 100644 --- a/src/helper/graphic_utils.cpp +++ b/src/helper/graphic_utils.cpp @@ -25,6 +25,14 @@ std::vector utils::supported_features() { features.emplace_back("discord integration"); #endif +#if defined(_ENABLE_REPLAY_RENDERING) +#if defined(_FFMPEG_USE_EMBEDDED) + features.emplace_back("replay video rendering (embedded)"); +#else + features.emplace_back("replay video rendering (system)"); +#endif +#endif + return features; } @@ -40,7 +48,7 @@ std::vector utils::supported_features() { } return std::filesystem::path{ std::string{ pref_path } }; #elif defined(__EMSCRIPTEN__) - return std::filesystem::path{ "/" }; + return std::filesystem::path{ "/persistent/" }; #elif defined(__CONSOLE__) // this is in the sdcard of the switch / 3ds , since internal storage is read-only for applications! return std::filesystem::path{ "." }; @@ -195,3 +203,33 @@ void utils::exit(int status_code) { std::exit(status_code); #endif } + +// inspired by SDL_SYS_SetupThread also uses that code for most platforms +OOPETRIS_GRAPHICS_EXPORTED void utils::set_thread_name(const char* name) { + +#if defined(__APPLE__) || defined(__MACOSX__) + if (pthread_setname_np(name) == ERANGE) { + char namebuf[16] = {}; /* Limited to 16 chars (with 0 byte) */ + memcpy(namebuf, name, 15); + namebuf[15] = '\0'; + pthread_setname_np(namebuf); + } +#elif defined(__linux__) || defined(__ANDROID__) || defined(FLATPAK_BUILD) + if (pthread_setname_np(pthread_self(), name) == ERANGE) { + char namebuf[16] = {}; /* Limited to 16 chars (with 0 byte) */ + memcpy(namebuf, name, 15); + namebuf[15] = '\0'; + pthread_setname_np(pthread_self(), namebuf); + } +#elif defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) + std::wstring name_w{}; + for (std::size_t i = 0; i < strlen(name); ++i) { + name_w += name[i]; + } + + SetThreadDescription(GetCurrentThread(), name_w.c_str()); + +#else + UNUSED(name); +#endif +} diff --git a/src/helper/graphic_utils.hpp b/src/helper/graphic_utils.hpp index 0c525a98..a27b9adb 100644 --- a/src/helper/graphic_utils.hpp +++ b/src/helper/graphic_utils.hpp @@ -38,18 +38,9 @@ namespace utils { OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::optional create_directory(const std::filesystem::path& folder, bool recursive); -// this needs some special handling, so the macro is defined here -#if defined(_MSC_VER) -#if defined(OOPETRIS_LIBRARY_GRAPHICS_TYPE) && OOPETRIS_LIBRARY_GRAPHICS_TYPE == 0 - -#else - -#endif -#else - -#endif - + OOPETRIS_GRAPHICS_EXPORTED void set_thread_name(const char* name); +// this needs some special handling, so the macro is defined here #if defined(_MSC_VER) #if defined(OOPETRIS_LIBRARY_GRAPHICS_TYPE) && OOPETRIS_LIBRARY_GRAPHICS_TYPE == 0 #if defined(OOPETRIS_LIBRARY_GRAPHICS_EXPORT) diff --git a/src/helper/meson.build b/src/helper/meson.build index ca607430..f2fd9bd3 100644 --- a/src/helper/meson.build +++ b/src/helper/meson.build @@ -1,4 +1,5 @@ graphics_src_files += files( + 'c_helpers.hpp', 'clock_source.cpp', 'clock_source.hpp', 'console_helpers.cpp', diff --git a/src/input/controller_input.cpp b/src/input/controller_input.cpp index 5841a542..82b583a6 100644 --- a/src/input/controller_input.cpp +++ b/src/input/controller_input.cpp @@ -6,7 +6,7 @@ #include "input/joystick_input.hpp" #include "manager/sdl_controller_key.hpp" - +#include #include input::ControllerInput::ControllerInput( diff --git a/src/input/guid.cpp b/src/input/guid.cpp index 40ab78bf..41b0b277 100644 --- a/src/input/guid.cpp +++ b/src/input/guid.cpp @@ -6,6 +6,7 @@ #include #include +#include sdl::GUID::GUID(const SDL_GUID& data) : m_guid{} { std::ranges::copy(data.data, std::begin(m_guid)); diff --git a/src/input/keyboard_input.cpp b/src/input/keyboard_input.cpp index da37ce6e..199fbc16 100644 --- a/src/input/keyboard_input.cpp +++ b/src/input/keyboard_input.cpp @@ -1,4 +1,5 @@ #include +#include #include "input/game_input.hpp" #include "input/input.hpp" diff --git a/src/input/touch_input.cpp b/src/input/touch_input.cpp index b7654f01..115ef5b0 100644 --- a/src/input/touch_input.cpp +++ b/src/input/touch_input.cpp @@ -6,6 +6,7 @@ #include "touch_input.hpp" #include +#include #include #include #include diff --git a/src/libs/core/helper/bool_wrapper.hpp b/src/libs/core/helper/bool_wrapper.hpp index 32c41eb7..5233e815 100644 --- a/src/libs/core/helper/bool_wrapper.hpp +++ b/src/libs/core/helper/bool_wrapper.hpp @@ -20,6 +20,19 @@ namespace helper { BoolWrapper(bool value, const std::optional& additional) : m_value{ value }, m_additional{ additional } { } + BoolWrapper(const BoolWrapper& other) = delete; + + BoolWrapper& operator=(const BoolWrapper& other) = delete; + + BoolWrapper(BoolWrapper&& other) noexcept + : m_value{ other.m_value }, + m_additional{ std::move(other.m_additional) } { + other.m_value = false; + other.m_additional = std::nullopt; + } + + BoolWrapper& operator=(BoolWrapper&& other) noexcept = default; + const std::optional& get_additional() const { return m_additional; } diff --git a/src/libs/core/helper/color.hpp b/src/libs/core/helper/color.hpp index 12019903..f31c940d 100644 --- a/src/libs/core/helper/color.hpp +++ b/src/libs/core/helper/color.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include diff --git a/src/libs/core/helper/utils.hpp b/src/libs/core/helper/utils.hpp index a2289f5c..065ae126 100644 --- a/src/libs/core/helper/utils.hpp +++ b/src/libs/core/helper/utils.hpp @@ -7,8 +7,6 @@ #include #include #include -#include -#include #include #include #include diff --git a/src/lobby/api.cpp b/src/lobby/api.cpp index 72dcc7aa..e26092dd 100644 --- a/src/lobby/api.cpp +++ b/src/lobby/api.cpp @@ -119,7 +119,6 @@ void lobby::API::check_url( //TODO(Totto): is this done correctly std::ignore = std::async(std::launch::async, [url, callback = std::move(callback), service_provider] { auto result = lobby::API::get_api(service_provider, url); - callback(result.has_value()); }); } diff --git a/src/lobby/credentials/secret.cpp b/src/lobby/credentials/secret.cpp index 8529dd94..70f43595 100644 --- a/src/lobby/credentials/secret.cpp +++ b/src/lobby/credentials/secret.cpp @@ -5,6 +5,8 @@ #include #include +#include + namespace { namespace secrets::constants { diff --git a/src/manager/sdl_key.cpp b/src/manager/sdl_key.cpp index a5e6e20d..6b685cdc 100644 --- a/src/manager/sdl_key.cpp +++ b/src/manager/sdl_key.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -16,7 +17,6 @@ #include #include - sdl::Key::Key(SDL_KeyCode keycode, UnderlyingModifierType modifiers) : m_keycode{ keycode }, m_modifiers{ modifiers } { } diff --git a/src/scenes/loading_screen/loading_screen.cpp b/src/scenes/loading_screen/loading_screen.cpp index 47b6157c..b570d099 100644 --- a/src/scenes/loading_screen/loading_screen.cpp +++ b/src/scenes/loading_screen/loading_screen.cpp @@ -13,38 +13,15 @@ #include + scenes::LoadingScreen::LoadingScreen(ServiceProvider* service_provider) - : m_segments{ - { Mino{ grid::GridPoint{ 0, 0 }, helper::TetrominoType::J }, 1.0 }, - { Mino{ grid::GridPoint{ 1, 0 }, helper::TetrominoType::L }, 1.0 }, - { Mino{ grid::GridPoint{ 2, 0 }, helper::TetrominoType::I }, 1.0 }, - { Mino{ grid::GridPoint{ 2, 1 }, helper::TetrominoType::O }, 1.0 }, - { Mino{ grid::GridPoint{ 2, 2 }, helper::TetrominoType::S }, 1.0 }, - { Mino{ grid::GridPoint{ 1, 2 }, helper::TetrominoType::T }, 1.0 }, - { Mino{ grid::GridPoint{ 0, 2 }, helper::TetrominoType::I }, 1.0 }, - { Mino{ grid::GridPoint{ 0, 1 }, helper::TetrominoType::Z }, 1.0 }, -},m_logo{logo::get_logo(service_provider)} { - - const auto [total_x_tiles, total_y_tiles] = utils::get_orientation() == utils::Orientation::Landscape - ? std::pair{ 17, 9 } - : std::pair{ 9, 17 }; - - constexpr auto loading_segments_size = 3; + : m_logo{ logo::get_logo(service_provider) }, + m_spinner{ ui::FullScreenLayout{ service_provider->window() }, true } { const auto& window = service_provider->window(); const auto layout = window.size(); - const u32 tile_size_x = layout.x / total_x_tiles; - const u32 tile_size_y = layout.y / total_y_tiles; - - m_tile_size = std::min(tile_size_y, tile_size_x); - - const shapes::UPoint grid_start_offset = { (total_x_tiles - loading_segments_size) / 2, - (total_y_tiles - loading_segments_size) / 2 }; - - m_start_offset = grid_start_offset * m_tile_size; - constexpr const auto logo_width_percentage = 0.8; constexpr const auto start_x = (1.0 - logo_width_percentage) / 2.0; @@ -58,70 +35,14 @@ scenes::LoadingScreen::LoadingScreen(ServiceProvider* service_provider) m_logo_rect = ui::RelativeLayout(window, start_x, 0.05, logo_width_percentage, logo_height_percentage).get_rect(); } -namespace { - [[nodiscard]] double elapsed_time() { - return static_cast(SDL_GetTicks64()) / 1000.0; - } -} // namespace - void scenes::LoadingScreen::update() { - - constexpr const auto speed = std::numbers::pi_v * 1.0; - constexpr const auto amplitude = 1.1; - constexpr const auto scale_offset = 1.3; - - const auto length = m_segments.size(); - const auto length_d = static_cast(length); - - const auto time = elapsed_time(); - - for (size_t i = 0; i < length; ++i) { - - auto& segment = m_segments.at(i); - - auto& scale = std::get<1>(segment); - - const auto offset = std::numbers::pi_v * 2.0 * static_cast(length - i - 1) / length_d; - - scale = std::min((amplitude * std::sin((time * speed) + offset)) + scale_offset, 1.0); - } - // + m_spinner.update(); } void scenes::LoadingScreen::render(const ServiceProvider& service_provider) const { - - service_provider.renderer().draw_rect_filled(service_provider.window().screen_rect(), Color::black()); + // NOTE: this already fills the background + m_spinner.render(service_provider); service_provider.renderer().draw_texture(m_logo, m_logo_rect); - - constexpr const auto scale_threshold = 0.25; - - for (const auto& [mino, scale] : m_segments) { - if (scale >= scale_threshold) { - const auto original_scale = - static_cast(m_tile_size) / static_cast(grid::original_tile_size); - - - const auto tile_size = static_cast(static_cast(m_tile_size) * scale); - - helper::graphics::render_mino( - mino, service_provider, MinoTransparency::Solid, original_scale, - [this, tile_size](const grid::GridPoint& point) -> auto { - return this->to_screen_coords(point, tile_size); - }, - { tile_size, tile_size } - ); - } - - //TODO(Totto): render text here, but than we need to load the fonts before this, not in the loading thread (not that they take that long) - } -} - - -[[nodiscard]] shapes::UPoint scenes::LoadingScreen::to_screen_coords(const grid::GridPoint& point, u32 tile_size) - const { - const auto start_edge = m_start_offset + point.cast() * m_tile_size; - const auto inner_offset = m_tile_size - (tile_size / 2); - return start_edge + shapes::UPoint{ inner_offset, inner_offset }; } diff --git a/src/scenes/loading_screen/loading_screen.hpp b/src/scenes/loading_screen/loading_screen.hpp index 02dd81cd..2c2e2859 100644 --- a/src/scenes/loading_screen/loading_screen.hpp +++ b/src/scenes/loading_screen/loading_screen.hpp @@ -5,6 +5,7 @@ #include "../logo/logo.hpp" #include "graphics/rect.hpp" #include "manager/service_provider.hpp" +#include "ui/components/spinner.hpp" #include @@ -12,12 +13,10 @@ namespace scenes { struct LoadingScreen { private: - std::vector> m_segments; Texture m_logo; - shapes::URect m_logo_rect; - u32 m_tile_size; - shapes::UPoint m_start_offset; + shapes::URect m_logo_rect; + ui::IndeterminateSpinner m_spinner; public: OOPETRIS_GRAPHICS_EXPORTED explicit LoadingScreen(ServiceProvider* service_provider); @@ -25,9 +24,6 @@ namespace scenes { OOPETRIS_GRAPHICS_EXPORTED void update(); OOPETRIS_GRAPHICS_EXPORTED void render(const ServiceProvider& service_provider) const; - - private: - [[nodiscard]] shapes::UPoint to_screen_coords(const grid::GridPoint& point, u32 tile_size) const; }; } // namespace scenes diff --git a/src/scenes/online_lobby/online_lobby.cpp b/src/scenes/online_lobby/online_lobby.cpp index 072f3340..a5038b5f 100644 --- a/src/scenes/online_lobby/online_lobby.cpp +++ b/src/scenes/online_lobby/online_lobby.cpp @@ -121,10 +121,9 @@ namespace scenes { if (const auto additional = event_result.get_additional(); additional.has_value()) { const auto value = additional.value(); - if (value.first == ui::EventHandleType::RequestAction) { + if (value.handle_type == ui::EventHandleType::RequestAction) { - - if (auto text_input = utils::is_child_class(value.second); text_input.has_value()) { + if (auto text_input = utils::is_child_class(value.widget); text_input.has_value()) { spdlog::info("Pressed Enter on TextInput {}", text_input.value()->get_text()); if (text_input.value()->has_focus()) { @@ -138,7 +137,7 @@ namespace scenes { } throw helper::FatalError( - fmt::format("Unsupported Handle Type: {}", magic_enum::enum_name(additional->first)) + fmt::format("Unsupported Handle Type: {}", magic_enum::enum_name(value.handle_type)) ); } diff --git a/src/scenes/recording_selector/recording_chooser.cpp b/src/scenes/recording_selector/recording_chooser.cpp index 4ea7d2b0..2db9b8c7 100644 --- a/src/scenes/recording_selector/recording_chooser.cpp +++ b/src/scenes/recording_selector/recording_chooser.cpp @@ -95,13 +95,13 @@ void custom_ui::RecordingFileChooser::render(const ServiceProvider& service_prov m_main_grid.render(service_provider); } -helper::BoolWrapper> custom_ui::RecordingFileChooser::handle_event( +ui::Widget::EventHandleResult custom_ui::RecordingFileChooser::handle_event( const std::shared_ptr& input_manager, const SDL_Event& event ) { //TODO(Totto): this double nested component can't correctly detect focus changes (since the checking for a focus change only occurs at one level deep) //TODO(Totto): allow horizontal RIGHT <-> LEFT focus change on horizontal focus_layouts - if (const auto handled = m_main_grid.handle_event(input_manager, event); handled) { + if (auto handled = m_main_grid.handle_event(input_manager, event); handled) { return handled; } diff --git a/src/scenes/recording_selector/recording_component.cpp b/src/scenes/recording_selector/recording_component.cpp index ba8c87a3..34326068 100644 --- a/src/scenes/recording_selector/recording_component.cpp +++ b/src/scenes/recording_selector/recording_component.cpp @@ -5,10 +5,14 @@ #include "manager/font.hpp" #include "manager/resource_manager.hpp" #include "recording_component.hpp" +#include "ui/components/text_button.hpp" #include "ui/widget.hpp" #include +#if defined(_ENABLE_REPLAY_RENDERING) +#include "graphics/video_renderer.hpp" +#endif custom_ui::RecordingComponent::RecordingComponent( ServiceProvider* service_provider, @@ -20,23 +24,54 @@ custom_ui::RecordingComponent::RecordingComponent( ui::Focusable{focus_helper.focus_id()}, ui::Hoverable{layout.get_rect()}, m_main_layout{ utils::SizeIdentity<2>(), focus_helper.focus_id(), - ui::Direction::Vertical, - std::array{ 0.6 }, ui::RelativeMargin{layout.get_rect(), ui::Direction::Vertical,0.05}, std::pair{ 0.05, 0.03 }, + ui::Direction::Horizontal, + std::array{ 0.9 }, ui::RelativeMargin{layout.get_rect(), ui::Direction::Vertical,0.05}, std::pair{ 0.05, 0.03 }, layout,false - },m_metadata{std::move(metadata)}{ + },m_metadata{std::move(metadata)},m_current_focus_id{m_main_layout.focus_id()}{ - m_main_layout.add( + + auto text_layout_index = m_main_layout.add( + utils::SizeIdentity<2>(), focus_helper.focus_id(), ui::Direction::Vertical, std::array{ 0.6 }, + ui::RelativeMargin{ layout.get_rect(), ui::Direction::Vertical, 0.05 }, + std::pair{ 0.05, 0.03 } + ); + + auto* text_layout = m_main_layout.get(text_layout_index); + + + auto render_button_index = m_main_layout.add( + service_provider, "Render", service_provider->font_manager().get(FontId::Default), Color::white(), + focus_helper.focus_id(), [](const ui::TextButton&) -> bool { return false; }, + std::pair{ 0.95, 0.85 }, + ui::Alignment{ ui::AlignmentHorizontal::Middle, ui::AlignmentVertical::Center }, + std::pair{ 0.1, 0.1 } + ); + + auto* render_button = m_main_layout.get(render_button_index); + + render_button->disable(); + +#if defined(_ENABLE_REPLAY_RENDERING) + VideoRendererBackend::is_supported_async([render_button](bool is_supported) { + if (is_supported) { + render_button->enable(); + } + }); +#endif + + text_layout->add( service_provider, "name: ?", service_provider->font_manager().get(FontId::Default), Color::white(), std::pair{ 0.5, 0.5 }, ui::Alignment{ ui::AlignmentHorizontal::Middle, ui::AlignmentVertical::Center } ); - const auto information_layout_index = m_main_layout.add( + + const auto information_layout_index = text_layout->add( utils::SizeIdentity<3>(), focus_helper.focus_id(), ui::Direction::Horizontal, std::array{ 0.33, 0.66 }, ui::AbsolutMargin{ 10 }, std::pair{ 0.05, 0.03 } ); - auto* const information_layout = m_main_layout.get(information_layout_index); + auto* const information_layout = text_layout->get(information_layout_index); information_layout->add( @@ -71,24 +106,82 @@ void custom_ui::RecordingComponent::render(const ServiceProvider& service_provid m_main_layout.render(service_provider); } -helper::BoolWrapper> custom_ui::RecordingComponent::handle_event( +ui::Widget::EventHandleResult custom_ui::RecordingComponent::handle_event( const std::shared_ptr& input_manager, const SDL_Event& event ) { + auto* render_button = m_main_layout.get(1); + if (has_focus() and input_manager->get_navigation_event(event) == input::NavigationEvent::OK) { - return { - true, - { ui::EventHandleType::RequestAction, this } - }; + if (m_current_focus_id == m_main_layout.focus_id()) { + return { + true, + { .handle_type = ui::EventHandleType::RequestAction, .widget = this, .data = nullptr } + }; + } + + if (m_current_focus_id == render_button->focus_id()) { + return { + true, + { .handle_type = ui::EventHandleType::RequestAction, .widget = render_button, .data = nullptr } + }; + } + + spdlog::error("Recording selector has invalid focused element: {}", m_current_focus_id); + } + + if (has_focus() + and (input_manager->get_navigation_event(event) == input::NavigationEvent::LEFT + or input_manager->get_navigation_event(event) == input::NavigationEvent::RIGHT)) { + + if (m_current_focus_id == m_main_layout.focus_id()) { + m_current_focus_id = render_button->focus_id(); + return true; + } + + if (m_current_focus_id == render_button->focus_id()) { + m_current_focus_id = m_main_layout.focus_id(); + return true; + } + + spdlog::error("Recording selector has invalid focused element: {}", m_current_focus_id); } if (const auto hover_result = detect_hover(input_manager, event); hover_result) { + + + if (const auto render_button_hover_result = render_button->detect_hover(input_manager, event); + render_button_hover_result) { + + this->on_unhover(); + + if (render_button_hover_result.is(ui::ActionType::Clicked)) { + + if (not has_focus()) { + return { + true, + { .handle_type = ui::EventHandleType::RequestFocus, .widget = this, .data = nullptr } + }; + } + + return { + true, + { .handle_type = ui::EventHandleType::RequestAction, .widget = render_button, .data = this } + }; + } + + return true; + } + if (hover_result.is(ui::ActionType::Clicked)) { + return { true, - { has_focus() ? ui::EventHandleType::RequestAction : ui::EventHandleType::RequestFocus, this } + { .handle_type = has_focus() ? ui::EventHandleType::RequestAction : ui::EventHandleType::RequestFocus, + .widget = this, + .data = nullptr } }; } return true; @@ -104,9 +197,11 @@ helper::BoolWrapper> custom_ui::Reco [[nodiscard]] std::tuple custom_ui::RecordingComponent::get_texts() { - auto* name_text = m_main_layout.get(0); + auto* text_layout = m_main_layout.get(0); + + auto* name_text = text_layout->get(0); - auto* information_layout = m_main_layout.get(1); + auto* information_layout = text_layout->get(1); auto* source_text = information_layout->get(0); auto* date_text = information_layout->get(1); diff --git a/src/scenes/recording_selector/recording_component.hpp b/src/scenes/recording_selector/recording_component.hpp index 5e812c93..60277815 100644 --- a/src/scenes/recording_selector/recording_component.hpp +++ b/src/scenes/recording_selector/recording_component.hpp @@ -35,6 +35,7 @@ namespace custom_ui { private: ui::TileLayout m_main_layout; data::RecordingMetadata m_metadata; + u32 m_current_focus_id; public: OOPETRIS_GRAPHICS_EXPORTED explicit RecordingComponent( diff --git a/src/scenes/recording_selector/recording_render.hpp b/src/scenes/recording_selector/recording_render.hpp new file mode 100644 index 00000000..02dd81cd --- /dev/null +++ b/src/scenes/recording_selector/recording_render.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include "../logo/logo.hpp" +#include "graphics/rect.hpp" +#include "manager/service_provider.hpp" + +#include + +namespace scenes { + + struct LoadingScreen { + private: + std::vector> m_segments; + Texture m_logo; + shapes::URect m_logo_rect; + + u32 m_tile_size; + shapes::UPoint m_start_offset; + + public: + OOPETRIS_GRAPHICS_EXPORTED explicit LoadingScreen(ServiceProvider* service_provider); + + OOPETRIS_GRAPHICS_EXPORTED void update(); + + OOPETRIS_GRAPHICS_EXPORTED void render(const ServiceProvider& service_provider) const; + + private: + [[nodiscard]] shapes::UPoint to_screen_coords(const grid::GridPoint& point, u32 tile_size) const; + }; + +} // namespace scenes diff --git a/src/scenes/recording_selector/recording_selector.cpp b/src/scenes/recording_selector/recording_selector.cpp index 9f82116b..0dfb3f39 100644 --- a/src/scenes/recording_selector/recording_selector.cpp +++ b/src/scenes/recording_selector/recording_selector.cpp @@ -6,8 +6,6 @@ #include "recording_chooser.hpp" #endif -#include - #include "graphics/window.hpp" #include "helper/constants.hpp" #include "helper/graphic_utils.hpp" @@ -19,10 +17,16 @@ #include "ui/layout.hpp" #include "ui/layouts/scroll_layout.hpp" #include "ui/widget.hpp" +#include #include #include +#if defined(_ENABLE_REPLAY_RENDERING) +#include "graphics/video_renderer.hpp" +#endif + + namespace scenes { using namespace details::recording::selector; //NOLINT(google-build-using-namespace) @@ -99,6 +103,41 @@ namespace scenes { ) } }; } + + +#if defined(_ENABLE_REPLAY_RENDERING) + if (auto render_button = utils::is_child_class(action.widget); + render_button.has_value()) { + + + auto recording_component = utils::is_child_class( + reinterpret_cast< //NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + ui::Widget*>(action.data) + ); + if (not recording_component.has_value()) { + throw std::runtime_error( + "Requested action on render button has invalid data, this is a fatal " + "error" + ); + } + + const auto recording_path = recording_component.value()->metadata().path; + + auto ren = VideoRenderer{ + m_service_provider, recording_path, shapes::UPoint{ 1280, 720 } + }; + + //TODO: do this in a seperate thread + ren.render("test.mp4", 60, [](double progress) { + // spdlog::info("Progress: {}", progress); + UNUSED(progress); + }); + + return UpdateResult{ SceneUpdate::StopUpdating, std::nullopt }; + } +#endif + + #if defined(_HAVE_FILE_DIALOGS) if (auto recording_file_chooser = @@ -137,11 +176,12 @@ namespace scenes { bool RecordingSelector::handle_event(const std::shared_ptr& input_manager, const SDL_Event& event) { - if (const auto event_result = m_main_layout.handle_event(input_manager, event); event_result) { if (const auto additional = event_result.get_additional(); - additional.has_value() and additional.value().first == ui::EventHandleType::RequestAction) { - m_next_command = Command{ Action(additional.value().second) }; + additional.has_value() and additional.value().handle_type == ui::EventHandleType::RequestAction) { + m_next_command = Command{ + Action{ .widget = additional.value().widget, .data = additional.value().data } + }; } return true; @@ -218,6 +258,8 @@ namespace scenes { auto focus_helper = ui::FocusHelper{ 3 }; + //TODO(Totto): sort by date, get the date from additional information or the file creation date + for (const auto& metadata : metadata_vector) { scroll_layout->add( ui::RelativeItemSize{ scroll_layout->layout(), 0.2 }, m_service_provider, std::ref(focus_helper), diff --git a/src/scenes/recording_selector/recording_selector.hpp b/src/scenes/recording_selector/recording_selector.hpp index f7ce4648..968c62e7 100644 --- a/src/scenes/recording_selector/recording_selector.hpp +++ b/src/scenes/recording_selector/recording_selector.hpp @@ -13,6 +13,7 @@ namespace details::recording::selector { struct Action { ui::Widget* widget; + void* data; }; struct Command { diff --git a/src/scenes/replay_game/replay_game.cpp b/src/scenes/replay_game/replay_game.cpp index 75f0a2df..37239d4e 100644 --- a/src/scenes/replay_game/replay_game.cpp +++ b/src/scenes/replay_game/replay_game.cpp @@ -1,6 +1,7 @@ #include "replay_game.hpp" #include "../single_player_game/game_over.hpp" #include "../single_player_game/pause.hpp" +#include "game/layout.hpp" #include "helper/constants.hpp" #include "helper/graphic_utils.hpp" #include "helper/music_utils.hpp" @@ -21,21 +22,7 @@ namespace scenes { auto [parameters, information] = input::get_game_parameters_for_replay(service_provider, recording_path); - std::vector layouts{}; - layouts.reserve(parameters.size()); - - if (parameters.empty()) { - throw std::runtime_error("An empty recording file isn't supported"); - } else if (parameters.size() == 1) { // NOLINT(readability-else-after-return,llvm-else-after-return) - layouts.push_back(ui::RelativeLayout{ layout, 0.02, 0.01, 0.96, 0.98 }); - } else if (parameters.size() == 2) { - layouts.push_back(ui::RelativeLayout{ layout, 0.02, 0.01, 0.46, 0.98 }); - layouts.push_back(ui::RelativeLayout{ layout, 0.52, 0.01, 0.46, 0.98 }); - } else { - - //TODO(Totto): support bigger layouts than just 2 - throw std::runtime_error("At the moment only replays from up to two players are supported"); - } + std::vector layouts = game::get_layouts_for(parameters.size(), layout); u32 simulation_frequency = constants::simulation_frequency; if (const auto stored_simulation_frequency = information.get_if("simulation_frequency"); diff --git a/src/scenes/replay_game/replay_game.hpp b/src/scenes/replay_game/replay_game.hpp index c452edcd..589eaabf 100644 --- a/src/scenes/replay_game/replay_game.hpp +++ b/src/scenes/replay_game/replay_game.hpp @@ -9,7 +9,6 @@ namespace scenes { private: enum class NextScene : u8 { Pause, Settings }; - std::optional m_next_scene; std::vector> m_games; diff --git a/src/scenes/settings_menu/color_setting_row.cpp b/src/scenes/settings_menu/color_setting_row.cpp index cfe8d981..1920133e 100644 --- a/src/scenes/settings_menu/color_setting_row.cpp +++ b/src/scenes/settings_menu/color_setting_row.cpp @@ -54,7 +54,8 @@ void detail::ColorSettingRectangle::render(const ServiceProvider& service_provid //TODO(Totto): maybe use a dynamic color, to have some contrast? service_provider.renderer().draw_rect_outline(m_fill_rect, Color::white()); } -helper::BoolWrapper> detail::ColorSettingRectangle::handle_event( + +ui::Widget::EventHandleResult detail::ColorSettingRectangle::handle_event( const std::shared_ptr& input_manager, const SDL_Event& event ) { @@ -64,7 +65,7 @@ helper::BoolWrapper> detail::ColorSe if (has_focus() and navigation_event == input::NavigationEvent::OK) { return { true, - { ui::EventHandleType::RequestAction, this } + { .handle_type = ui::EventHandleType::RequestAction, .widget = this, .data = nullptr } }; } @@ -73,7 +74,7 @@ helper::BoolWrapper> detail::ColorSe if (hover_result.is(ui::ActionType::Clicked)) { return { true, - { ui::EventHandleType::RequestAction, this } + { .handle_type = ui::EventHandleType::RequestAction, .widget = this, .data = nullptr } }; } return true; @@ -174,20 +175,22 @@ void custom_ui::ColorSettingRow::render(const ServiceProvider& service_provider) m_main_layout.render(service_provider); } -helper::BoolWrapper> custom_ui::ColorSettingRow::handle_event( +ui::Widget::EventHandleResult custom_ui::ColorSettingRow::handle_event( const std::shared_ptr& input_manager, const SDL_Event& event ) { - const auto result = m_main_layout.handle_event(input_manager, event); + auto result = m_main_layout.handle_event(input_manager, event); if (const auto additional = result.get_additional(); additional.has_value()) { - if (additional->first == ui::EventHandleType::RequestAction) { + if (additional.value().handle_type == ui::EventHandleType::RequestAction) { return { result, - { ui::EventHandleType::RequestAction, this } + { .handle_type = ui::EventHandleType::RequestAction, .widget = this, .data = nullptr } }; } - throw helper::FatalError(fmt::format("Unsupported Handle Type: {}", magic_enum::enum_name(additional->first))); + throw helper::FatalError( + fmt::format("Unsupported Handle Type: {}", magic_enum::enum_name(additional.value().handle_type)) + ); } return result; diff --git a/src/scenes/settings_menu/settings_menu.cpp b/src/scenes/settings_menu/settings_menu.cpp index b5f1c7c1..8c04bb55 100644 --- a/src/scenes/settings_menu/settings_menu.cpp +++ b/src/scenes/settings_menu/settings_menu.cpp @@ -248,8 +248,10 @@ namespace scenes { bool SettingsMenu::handle_event(const std::shared_ptr& input_manager, const SDL_Event& event) { if (const auto event_result = m_main_layout.handle_event(input_manager, event); event_result) { if (const auto additional = event_result.get_additional(); - additional.has_value() and additional.value().first == ui::EventHandleType::RequestAction) { - m_next_command = Command{ Action{ additional.value().second } }; + additional.has_value() and additional.value().handle_type == ui::EventHandleType::RequestAction) { + m_next_command = Command{ + Action{ .widget = additional.value().widget, .data = additional->data } + }; } return true; diff --git a/src/scenes/settings_menu/settings_menu.hpp b/src/scenes/settings_menu/settings_menu.hpp index 831dbe34..5dd1cdfd 100644 --- a/src/scenes/settings_menu/settings_menu.hpp +++ b/src/scenes/settings_menu/settings_menu.hpp @@ -19,6 +19,7 @@ namespace details::settings::menu { struct Action { ui::Widget* widget; + void* data; }; struct Command { diff --git a/src/ui/components/abstract_slider.hpp b/src/ui/components/abstract_slider.hpp index 54934264..6b15c9bf 100644 --- a/src/ui/components/abstract_slider.hpp +++ b/src/ui/components/abstract_slider.hpp @@ -170,7 +170,7 @@ namespace ui { handled = { true, - { ui::EventHandleType::RequestFocus, this } + { ui::EventHandleType::RequestFocus, this, nullptr } }; } else if (pointer_event->is_in(m_slider_rect)) { @@ -180,7 +180,7 @@ namespace ui { handled = { true, - { ui::EventHandleType::RequestFocus, this } + { ui::EventHandleType::RequestFocus, this, nullptr } }; } diff --git a/src/ui/components/button.hpp b/src/ui/components/button.hpp index e89ae33f..81416333 100644 --- a/src/ui/components/button.hpp +++ b/src/ui/components/button.hpp @@ -102,7 +102,7 @@ namespace ui { if (on_clicked()) { return { true, - { ui::EventHandleType::RequestAction, this } + { ui::EventHandleType::RequestAction, this, nullptr } }; } return true; @@ -114,7 +114,7 @@ namespace ui { if (on_clicked()) { return { true, - { ui::EventHandleType::RequestAction, this } + { ui::EventHandleType::RequestAction, this, nullptr } }; } } diff --git a/src/ui/components/color_picker.cpp b/src/ui/components/color_picker.cpp index 7d52d150..5e8550b2 100644 --- a/src/ui/components/color_picker.cpp +++ b/src/ui/components/color_picker.cpp @@ -148,7 +148,7 @@ void detail::ColorCanvas::draw_pseudo_circle(const ServiceProvider& service_prov renderer.draw_self_computed_circle(center, diameter, circle_color); } -helper::BoolWrapper> +ui::Widget::EventHandleResult detail::ColorCanvas::handle_event(const std::shared_ptr& input_manager, const SDL_Event& event) { Widget::EventHandleResult handled = false; @@ -164,7 +164,7 @@ detail::ColorCanvas::handle_event(const std::shared_ptr& in SDL_CaptureMouse(SDL_TRUE); handled = { true, - { ui::EventHandleType::RequestFocus, this } + { .handle_type = ui::EventHandleType::RequestFocus, .widget = this, .data = nullptr } }; } } else if (pointer_event == input::PointerEvent::PointerUp) { @@ -457,7 +457,7 @@ void ui::ColorPicker::render(const ServiceProvider& service_provider) const { m_color_text->render(service_provider); } -helper::BoolWrapper> +ui::Widget::EventHandleResult ui::ColorPicker::handle_event(const std::shared_ptr& input_manager, const SDL_Event& event) { auto handled = m_color_slider->handle_event(input_manager, event); @@ -487,7 +487,7 @@ ui::ColorPicker::handle_event(const std::shared_ptr& input_ if (handled) { if (const auto additional = handled.get_additional(); additional.has_value()) { - switch (additional.value().first) { + switch (additional.value().handle_type) { case ui::EventHandleType::RequestFocus: if (not m_color_text->has_focus()) { m_color_text->focus(); diff --git a/src/ui/components/image_view.cpp b/src/ui/components/image_view.cpp index d6f44e15..e861f13e 100644 --- a/src/ui/components/image_view.cpp +++ b/src/ui/components/image_view.cpp @@ -29,7 +29,7 @@ void ui::ImageView::render(const ServiceProvider& service_provider) const { service_provider.renderer().draw_texture(m_image, m_fill_rect); } -helper::BoolWrapper> +ui::Widget::EventHandleResult ui::ImageView::handle_event(const std::shared_ptr& /*input_manager*/, const SDL_Event& /*event*/) { return false; } diff --git a/src/ui/components/label.cpp b/src/ui/components/label.cpp index 622318de..00271a4d 100644 --- a/src/ui/components/label.cpp +++ b/src/ui/components/label.cpp @@ -26,7 +26,7 @@ void ui::Label::render(const ServiceProvider& service_provider) const { m_text.render(service_provider); } -helper::BoolWrapper> +ui::Widget::EventHandleResult ui::Label::handle_event(const std::shared_ptr& /*input_manager*/, const SDL_Event& /*event*/) { return false; } diff --git a/src/ui/components/link_label.cpp b/src/ui/components/link_label.cpp index 0dff0ffd..c8cf1a7a 100644 --- a/src/ui/components/link_label.cpp +++ b/src/ui/components/link_label.cpp @@ -56,7 +56,7 @@ void ui::LinkLabel::render(const ServiceProvider& service_provider) const { } } -helper::BoolWrapper> +ui::Widget::EventHandleResult ui::LinkLabel::handle_event(const std::shared_ptr& input_manager, const SDL_Event& event) { if (const auto hover_result = detect_hover(input_manager, event); hover_result) { if (hover_result.is(ActionType::Clicked)) { diff --git a/src/ui/components/meson.build b/src/ui/components/meson.build index bdc68421..e6517163 100644 --- a/src/ui/components/meson.build +++ b/src/ui/components/meson.build @@ -13,8 +13,10 @@ graphics_src_files += files( 'link_label.hpp', 'slider.cpp', 'slider.hpp', + 'spinner.cpp', + 'spinner.hpp', 'text_button.cpp', 'text_button.hpp', 'textinput.cpp', - 'textinput.hpp', + 'textinput.cpp', ) diff --git a/src/ui/components/spinner.cpp b/src/ui/components/spinner.cpp new file mode 100644 index 00000000..5847873c --- /dev/null +++ b/src/ui/components/spinner.cpp @@ -0,0 +1,121 @@ +#include +#include + +#include "game/graphic_helpers.hpp" +#include "graphics/renderer.hpp" +#include "helper/platform.hpp" +#include "manager/service_provider.hpp" +#include "spinner.hpp" +#include "ui/layout.hpp" + +#include + +ui::IndeterminateSpinner::IndeterminateSpinner( + const Layout& layout, + bool is_top_level +):Widget{ + layout, WidgetType::Component, is_top_level +}, + m_segments{ + { Mino{ grid::GridPoint{ 0, 0 }, helper::TetrominoType::J }, 1.0 }, + { Mino{ grid::GridPoint{ 1, 0 }, helper::TetrominoType::L }, 1.0 }, + { Mino{ grid::GridPoint{ 2, 0 }, helper::TetrominoType::I }, 1.0 }, + { Mino{ grid::GridPoint{ 2, 1 }, helper::TetrominoType::O }, 1.0 }, + { Mino{ grid::GridPoint{ 2, 2 }, helper::TetrominoType::S }, 1.0 }, + { Mino{ grid::GridPoint{ 1, 2 }, helper::TetrominoType::T }, 1.0 }, + { Mino{ grid::GridPoint{ 0, 2 }, helper::TetrominoType::I }, 1.0 }, + { Mino{ grid::GridPoint{ 0, 1 }, helper::TetrominoType::Z }, 1.0 }, + }{ + + const auto [total_x_tiles, total_y_tiles] = utils::get_orientation() == utils::Orientation::Landscape + ? std::pair{ 17, 9 } + : std::pair{ 9, 17 }; + + constexpr auto loading_segments_size = 3; + + const auto layout_rect = layout.get_rect(); + const auto layout_size = shapes::UPoint{ layout_rect.width(), layout_rect.height() }; + + const u32 tile_size_x = layout_size.x / total_x_tiles; + const u32 tile_size_y = layout_size.y / total_y_tiles; + + m_tile_size = std::min(tile_size_y, tile_size_x); + + const shapes::UPoint grid_start_offset = { (total_x_tiles - loading_segments_size) / 2, + (total_y_tiles - loading_segments_size) / 2 }; + + m_start_offset = grid_start_offset * m_tile_size; +} + +namespace { + [[nodiscard]] double elapsed_time() { + return static_cast(SDL_GetTicks64()) / 1000.0; + } +} // namespace + + +void ui::IndeterminateSpinner::update() { + + constexpr const auto speed = std::numbers::pi_v * 1.0; + constexpr const auto amplitude = 1.1; + constexpr const auto scale_offset = 1.3; + + const auto length = m_segments.size(); + const auto length_d = static_cast(length); + + const auto time = elapsed_time(); + + for (size_t i = 0; i < length; ++i) { + + auto& segment = m_segments.at(i); + + auto& scale = std::get<1>(segment); + + const auto offset = std::numbers::pi_v * 2.0 * static_cast(length - i - 1) / length_d; + + scale = std::min((amplitude * std::sin((time * speed) + offset)) + scale_offset, 1.0); + } + // +} + +void ui::IndeterminateSpinner::render(const ServiceProvider& service_provider) const { + + service_provider.renderer().draw_rect_filled(layout().get_rect(), Color::black()); + + constexpr const auto scale_threshold = 0.25; + + for (const auto& [mino, scale] : m_segments) { + if (scale >= scale_threshold) { + const auto original_scale = + static_cast(m_tile_size) / static_cast(grid::original_tile_size); + + + const auto tile_size = static_cast(static_cast(m_tile_size) * scale); + + helper::graphics::render_mino( + mino, service_provider, MinoTransparency::Solid, original_scale, + [this, tile_size](const grid::GridPoint& point) -> auto { + return this->to_screen_coords(point, tile_size); + }, + { tile_size, tile_size } + ); + } + + //TODO(Totto): render text here, but than we need to load the fonts before this, not in the loading thread (not that they take that long) + } +} + + +[[nodiscard]] shapes::UPoint ui::IndeterminateSpinner::to_screen_coords(const grid::GridPoint& point, u32 tile_size) + const { + const auto start_edge = m_start_offset + point.cast() * m_tile_size; + const auto inner_offset = m_tile_size - (tile_size / 2); + return start_edge + shapes::UPoint{ inner_offset, inner_offset }; +} + +ui::Widget::EventHandleResult ui::IndeterminateSpinner::handle_event( + const std::shared_ptr& /* input_manager */, + const SDL_Event& /* event */ +) { + return false; +} diff --git a/src/ui/components/spinner.hpp b/src/ui/components/spinner.hpp new file mode 100644 index 00000000..11e71efc --- /dev/null +++ b/src/ui/components/spinner.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "graphics/rect.hpp" +#include "manager/service_provider.hpp" +#include "ui/widget.hpp" + +#include + +namespace ui { + + struct IndeterminateSpinner final : public Widget { + private: + std::vector> m_segments; + + u32 m_tile_size; + shapes::UPoint m_start_offset; + + public: + OOPETRIS_GRAPHICS_EXPORTED explicit IndeterminateSpinner(const Layout& layout, bool is_top_level); + + OOPETRIS_GRAPHICS_EXPORTED void update() override; + + OOPETRIS_GRAPHICS_EXPORTED void render(const ServiceProvider& service_provider) const override; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] EventHandleResult + handle_event(const std::shared_ptr& input_manager, const SDL_Event& event) override; + + private: + [[nodiscard]] shapes::UPoint to_screen_coords(const grid::GridPoint& point, u32 tile_size) const; + }; + +} // namespace ui diff --git a/src/ui/components/textinput.cpp b/src/ui/components/textinput.cpp index 2223980b..7d0dc799 100644 --- a/src/ui/components/textinput.cpp +++ b/src/ui/components/textinput.cpp @@ -105,8 +105,7 @@ void ui::TextInput::render(const ServiceProvider& service_provider) const { } } -helper::BoolWrapper> -ui::TextInput::handle_event( //NOLINT(readability-function-cognitive-complexity) +ui::Widget::EventHandleResult ui::TextInput::handle_event( //NOLINT(readability-function-cognitive-complexity) const std::shared_ptr& input_manager, const SDL_Event& event ) { @@ -116,7 +115,7 @@ ui::TextInput::handle_event( //NOLINT(readability-function-cognitive-complexity) if (hover_result.is(ActionType::Clicked)) { return { true, - { EventHandleType::RequestFocus, this } + { .handle_type = EventHandleType::RequestFocus, .widget = this, .data = nullptr } }; } @@ -130,7 +129,7 @@ ui::TextInput::handle_event( //NOLINT(readability-function-cognitive-complexity) on_unfocus(); return { true, - { EventHandleType::RequestAction, this } + { .handle_type = EventHandleType::RequestAction, .widget = this, .data = nullptr } }; } //TODO(Totto): in some cases this is caught before that, and never triggered diff --git a/src/ui/hoverable.cpp b/src/ui/hoverable.cpp new file mode 100644 index 00000000..cc6998d8 --- /dev/null +++ b/src/ui/hoverable.cpp @@ -0,0 +1,57 @@ + +#include "hoverable.hpp" + +#include + +ui::Hoverable::Hoverable(const shapes::URect& fill_rect) : m_fill_rect{ fill_rect } { }; + +ui::Hoverable::~Hoverable() = default; + +[[nodiscard]] bool ui::Hoverable::is_hovered() const { + return m_is_hovered; +} + +[[nodiscard]] const shapes::URect& ui::Hoverable::fill_rect() const { + return m_fill_rect; +} + +[[nodiscard]] helper::BoolWrapper +ui::Hoverable::detect_hover(const std::shared_ptr& input_manager, const SDL_Event& event) { + + + if (const auto result = input_manager->get_pointer_event(event); result.has_value()) { + if (result->is_in(m_fill_rect)) { + + on_hover(); + + switch (result->event()) { + case input::PointerEvent::PointerDown: + return { true, ActionType::Clicked }; + case input::PointerEvent::PointerUp: + return { true, ActionType::Released }; + case input::PointerEvent::Motion: + return { true, ActionType::Hover }; + case input::PointerEvent::Wheel: + return false; + + default: + UNREACHABLE(); + } + } + + on_unhover(); + return false; + } + + return false; +} + + +void ui::Hoverable::on_hover() { + m_is_hovered = true; +} + +//TODO(Totto): this has to be used correctly, a click or focus change isn't an event, where an unhover needs to happen! +void ui::Hoverable::on_unhover() { + m_is_hovered = false; +} diff --git a/src/ui/hoverable.hpp b/src/ui/hoverable.hpp index 02a6f7f3..fc6316a3 100644 --- a/src/ui/hoverable.hpp +++ b/src/ui/hoverable.hpp @@ -19,65 +19,24 @@ namespace ui { public: - explicit Hoverable(const shapes::URect& fill_rect) - : m_fill_rect{ fill_rect } { + explicit Hoverable(const shapes::URect& fill_rect); - }; Hoverable(const Hoverable&) = delete; Hoverable(Hoverable&&) = delete; Hoverable& operator=(const Hoverable&) = delete; Hoverable& operator=(Hoverable&&) = delete; - virtual ~Hoverable() = default; + virtual ~Hoverable(); - [[nodiscard]] auto is_hovered() const { - return m_is_hovered; - } - [[nodiscard]] const shapes::URect& fill_rect() const { - return m_fill_rect; - } + [[nodiscard]] bool is_hovered() const; + [[nodiscard]] const shapes::URect& fill_rect() const; [[nodiscard]] helper::BoolWrapper - detect_hover(const std::shared_ptr& input_manager, const SDL_Event& event) { + detect_hover(const std::shared_ptr& input_manager, const SDL_Event& event); + void on_hover(); - if (const auto result = input_manager->get_pointer_event(event); result.has_value()) { - if (result->is_in(m_fill_rect)) { - - on_hover(); - - switch (result->event()) { - case input::PointerEvent::PointerDown: - return { true, ActionType::Clicked }; - case input::PointerEvent::PointerUp: - return { true, ActionType::Released }; - case input::PointerEvent::Motion: - return { true, ActionType::Hover }; - case input::PointerEvent::Wheel: - return false; - - default: - UNREACHABLE(); - } - } - - on_unhover(); - return false; - } - - - return false; - } - - - void on_hover() { - m_is_hovered = true; - } - - //TODO(Totto): this has to be used correctly, a click or focus change isn't an event, where an unhover needs to happen! - void on_unhover() { - m_is_hovered = false; - } + void on_unhover(); }; diff --git a/src/ui/layout.cpp b/src/ui/layout.cpp index bb3e50fc..862f79b3 100644 --- a/src/ui/layout.cpp +++ b/src/ui/layout.cpp @@ -1,6 +1,7 @@ #include "ui/layout.hpp" +#include [[nodiscard]] u32 ui::get_horizontal_alignment_offset(const Layout& layout, AlignmentHorizontal alignment, u32 width) { switch (alignment) { diff --git a/src/ui/layouts/focus_layout.cpp b/src/ui/layouts/focus_layout.cpp index aa87dd5e..9234bc37 100644 --- a/src/ui/layouts/focus_layout.cpp +++ b/src/ui/layouts/focus_layout.cpp @@ -4,9 +4,9 @@ #include "input/input.hpp" #include "ui/widget.hpp" +#include #include - ui::FocusLayout::FocusLayout(const Layout& layout, u32 focus_id, FocusOptions options, bool is_top_level) : Widget{ layout, WidgetType::Container, is_top_level }, Focusable{ focus_id }, @@ -70,7 +70,7 @@ ui::Widget::EventHandleResult ui::FocusLayout::handle_focus_change_button_events const SDL_Event& event ) { - Widget::EventHandleResult handled = false; + ui::Widget::EventHandleResult handled = false; const auto navigation_action = input_manager->get_navigation_event(event); @@ -96,8 +96,7 @@ ui::Widget::EventHandleResult ui::FocusLayout::handle_focus_change_events( return false; } - Widget::EventHandleResult handled = false; - + ui::Widget::EventHandleResult handled{ false }; if (m_focus_id.has_value()) { const auto& widget = m_widgets.at(focusable_index_by_id(m_focus_id.value())); @@ -136,7 +135,7 @@ ui::FocusLayout::handle_event_result(const std::optional auto value = result.value(); - switch (value.first) { + switch (value.handle_type) { case ui::EventHandleType::RequestFocus: { const auto focusable = as_focusable(widget); if (not focusable.has_value()) { @@ -161,7 +160,9 @@ ui::FocusLayout::handle_event_result(const std::optional // if the layout itself has not focus, it needs focus itself too if (not has_focus()) { - return ui::Widget::InnerState{ ui::EventHandleType::RequestFocus, value.second }; + return ui::Widget::InnerState{ .handle_type = ui::EventHandleType::RequestFocus, + .widget = value.widget, + .data = value.data }; } @@ -192,12 +193,16 @@ ui::FocusLayout::handle_event_result(const std::optional const auto test_forward = try_set_next_focus(FocusChangeDirection::Forward); if (not test_forward) { if (m_options.wrap_around) { - return ui::Widget::InnerState{ ui::EventHandleType::RequestUnFocus, value.second }; + return ui::Widget::InnerState{ .handle_type = ui::EventHandleType::RequestUnFocus, + .widget = value.widget, + .data = value.data }; } const auto test_backwards = try_set_next_focus(FocusChangeDirection::Backward); if (not test_backwards) { - return ui::Widget::InnerState{ ui::EventHandleType::RequestUnFocus, value.second }; + return ui::Widget::InnerState{ .handle_type = ui::EventHandleType::RequestUnFocus, + .widget = value.widget, + .data = value.data }; } } @@ -205,7 +210,9 @@ ui::FocusLayout::handle_event_result(const std::optional } case ui::EventHandleType::RequestAction: { // just forward it - return ui::Widget::InnerState{ ui::EventHandleType::RequestAction, value.second }; + return ui::Widget::InnerState{ .handle_type = ui::EventHandleType::RequestAction, + .widget = value.widget, + .data = value.data }; } default: UNREACHABLE(); diff --git a/src/ui/layouts/scroll_layout.cpp b/src/ui/layouts/scroll_layout.cpp index 8e46ce69..58ba6db7 100644 --- a/src/ui/layouts/scroll_layout.cpp +++ b/src/ui/layouts/scroll_layout.cpp @@ -240,7 +240,7 @@ ui::Widget::EventHandleResult ui::ScrollLayout::handle_focus_change_events( return false; } - Widget::EventHandleResult handled = false; + Widget::EventHandleResult handled{ false }; if (m_focus_id.has_value()) { diff --git a/src/ui/meson.build b/src/ui/meson.build index 3c896191..69ac80b7 100644 --- a/src/ui/meson.build +++ b/src/ui/meson.build @@ -1,5 +1,7 @@ graphics_src_files += files( 'focusable.hpp', + 'hoverable.cpp', + 'hoverable.hpp', 'layout.cpp', 'layout.hpp', 'widget.cpp', diff --git a/src/ui/widget.hpp b/src/ui/widget.hpp index 28684771..2210adfc 100644 --- a/src/ui/widget.hpp +++ b/src/ui/widget.hpp @@ -8,7 +8,7 @@ #include "ui/layout.hpp" #include -#include +#include namespace ui { @@ -23,7 +23,12 @@ namespace ui { bool m_top_level; public: - using InnerState = std::pair; + struct InnerState { + ui::EventHandleType handle_type; + Widget* widget; + void* data; + }; + using EventHandleResult = helper::BoolWrapper; explicit Widget(const Layout& layout, WidgetType type, bool is_top_level) diff --git a/subprojects/packagefiles/discord_game_sdk/cpp/meson.build b/subprojects/packagefiles/discord_game_sdk/cpp/meson.build index f62b8545..0674ef97 100644 --- a/subprojects/packagefiles/discord_game_sdk/cpp/meson.build +++ b/subprojects/packagefiles/discord_game_sdk/cpp/meson.build @@ -49,7 +49,7 @@ if host_machine.system() == 'darwin' elif host_machine.cpu_family() == 'x86_64' lib_dir = 'lib' / 'x86_64' else - error('unsuported architectue for macos: ' + host_machine.cpu_family()) + error('unsupported architecture for macos: ' + host_machine.cpu_family()) endif elif host_machine.system() == 'linux' @@ -81,11 +81,11 @@ elif host_machine.system() == 'linux' endif if linux_distro == 'alpine' - error('unsuported libc for linux: musl') + error('unsupported libc for linux: musl') endif else - error('unsuported architectue for linux: ' + host_machine.cpu_family()) + error('unsupported architecture for linux: ' + host_machine.cpu_family()) endif elif host_machine.system() == 'windows' dynamic_lib = 'discord_game_sdk.dll' @@ -94,10 +94,10 @@ elif host_machine.system() == 'windows' elif host_machine.cpu_family() == 'x86_64' lib_dir = 'lib' / 'x86_64' else - error('unsuported architectue for windows: ' + host_machine.cpu_family()) + error('unsupported architecture for windows: ' + host_machine.cpu_family()) endif else - error('unsuported system: ' + host_machine.system()) + error('unsupported system: ' + host_machine.system()) endif c = meson.get_compiler('c') diff --git a/tools/dependencies/meson.build b/tools/dependencies/meson.build index f00e47fd..e6c1a02f 100644 --- a/tools/dependencies/meson.build +++ b/tools/dependencies/meson.build @@ -61,7 +61,7 @@ if meson.is_cross_build() ['SDL2_ttf'], ['mpg123'], ['SDL2_mixer_mp3', 'SDL2_mixer'], - ['SDL2_image_png-svg-mt', 'SDL2_image'], + ['SDL2_image-png-svg-mt', 'SDL2_image'], ['icu_common-mt', 'icu-uc'], ] foreach native_dependency_tuple : map_native_dependencies @@ -443,7 +443,7 @@ if build_application else # TODO: create a proper installer for macOS : https://mesonbuild.com/Creating-OSX-packages.html error( - 'unsuported system for building the installer: ' + 'unsupported system for building the installer: ' + host_machine.system(), ) @@ -456,6 +456,152 @@ if build_application endif + use_embedded_ffmpeg_option = get_option('use_embedded_ffmpeg') + use_embedded_ffmpeg = false + replay_video_rendering_enabled = true + + if host_machine.system() == 'darwin' or host_machine.system() == 'linux' or host_machine.system() == 'windows' + + use_embedded_ffmpeg = use_embedded_ffmpeg_option.enable_auto_if(is_flatpak_build).enabled() + + # on the desktop we only use the embedded ffmpeg, if we require it, we prefer the command line tool, that is installed on the system, except for flatpak builds + if use_embedded_ffmpeg + if host_machine.system() == 'windows' + error('Embedded ffmpeg is still WIP on windows') + endif + + ffmpeg_dep_names = [ + 'libavutil', + 'libavcodec', + 'libavformat', + 'libavfilter', + 'libswscale', + ] + ffmpeg_deps = [] + found_all_ffmpeg_deps = true + + foreach ffmpeg_dep_name : ffmpeg_dep_names + ffmpeg_dep = dependency(ffmpeg_dep_name, required: use_embedded_ffmpeg_option) + if not ffmpeg_dep.found() + found_all_ffmpeg_deps = false + break + endif + + ffmpeg_deps += ffmpeg_dep + + endforeach + + message( + 'ffmpeg deps: ' + + (found_all_ffmpeg_deps ? 'FOUND' : 'NOT ALL FOUND'), + ) + + replay_video_rendering_enabled = found_all_ffmpeg_deps + if replay_video_rendering_enabled + graphics_lib += { + 'deps': [graphics_lib.get('deps'), ffmpeg_deps], + } + endif + + endif + + else + if meson.is_cross_build() + + use_embedded_ffmpeg = use_embedded_ffmpeg_option.allowed() + + if not use_embedded_ffmpeg + error('only embedded ffmpeg is supported in cross builds') + endif + + ffmpeg_dep_names = [ + 'libavutil', + 'libavcodec', + 'libavformat', + 'libavfilter', + 'libswscale', + 'libswresample', + ] + + if host_machine.system() == 'android' + ffmpeg_can_be_supported = true + + elif host_machine.system() == 'switch' + ffmpeg_can_be_supported = true + ffmpeg_dep_names += [ + 'dav1d', + ] + elif host_machine.system() == '3ds' + ffmpeg_can_be_supported = false + elif host_machine.system() == 'emscripten' + ffmpeg_can_be_supported = true + ffmpeg_dep_names += [ + 'x264', + ] + else + + error( + 'unhandled cross built system for video rendering: ' + + host_machine.system(), + ) + endif + + if not ffmpeg_can_be_supported + replay_video_rendering_enabled = false + else + + ffmpeg_deps = [] + found_all_ffmpeg_deps = true + + c = meson.get_compiler('c') + + foreach ffmpeg_dep_name : ffmpeg_dep_names + ffmpeg_dep = dependency(ffmpeg_dep_name, required: use_embedded_ffmpeg_option) + if not ffmpeg_dep.found() + found_all_ffmpeg_deps = false + break + endif + + ffmpeg_deps += ffmpeg_dep + + endforeach + + message( + 'ffmpeg deps: ' + + (found_all_ffmpeg_deps ? 'FOUND' : 'NOT ALL FOUND'), + ) + + replay_video_rendering_enabled = found_all_ffmpeg_deps + if replay_video_rendering_enabled + graphics_lib += { + 'deps': [graphics_lib.get('deps'), ffmpeg_deps], + } + endif + + endif + + else + error( + 'unhandled system for video rendering: ' + + host_machine.system(), + ) + endif + endif + + if replay_video_rendering_enabled + _set_flags = ['-D_ENABLE_REPLAY_RENDERING'] + if use_embedded_ffmpeg + _set_flags += ['-D_FFMPEG_USE_EMBEDDED'] + endif + + graphics_lib += { + 'compile_args': [graphics_lib.get('compile_args'), _set_flags], + } + + _set_flags = 0 + + endif + if is_flatpak_build app_name = 'io.github.openbrickprotocolfoundation.oopetris' diff --git a/wrapper/c b/wrapper/c index e23c6346..7e2e962a 160000 --- a/wrapper/c +++ b/wrapper/c @@ -1 +1 @@ -Subproject commit e23c634661692ffb259e43c6a359e4c0d54bb247 +Subproject commit 7e2e962adf8109a772ba9498cd29772f63579e27 diff --git a/wrapper/haskell b/wrapper/haskell index ba08e719..98a66115 160000 --- a/wrapper/haskell +++ b/wrapper/haskell @@ -1 +1 @@ -Subproject commit ba08e719698217c3bac2d8c04fd48ba7c0477576 +Subproject commit 98a66115282942f9465b94e80f21f13be7d8e8c0 diff --git a/wrapper/javascript b/wrapper/javascript index 01b3759c..9708a7fd 160000 --- a/wrapper/javascript +++ b/wrapper/javascript @@ -1 +1 @@ -Subproject commit 01b3759cdf54492164b08694e0701c63a8aa97dd +Subproject commit 9708a7fd5e42d966007ce2c6f09a821751f68d39