diff --git a/.ci/android/build.sh b/.ci/android/build.sh index edd65d9ae4..836faa38d5 100755 --- a/.ci/android/build.sh +++ b/.ci/android/build.sh @@ -1,19 +1,21 @@ #!/bin/bash -e -# SPDX-FileCopyrightText: 2025 eden Emulator Project +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project # SPDX-License-Identifier: GPL-3.0-or-later export NDK_CCACHE=$(which ccache) -# keystore & pass are stored locally -export ANDROID_KEYSTORE_FILE=~/android.keystore -export ANDROID_KEYSTORE_PASS=`cat ~/android.pass` -export ANDROID_KEY_ALIAS=`cat ~/android.alias` +if [ ! -z "${ANDROID_KEYSTORE_B64}" ]; then + export ANDROID_KEYSTORE_FILE="${GITHUB_WORKSPACE}/ks.jks" + base64 --decode <<< "${ANDROID_KEYSTORE_B64}" > "${ANDROID_KEYSTORE_FILE}" +fi cd src/android chmod +x ./gradlew -./gradlew assembleRelease -./gradlew bundleRelease +./gradlew assembleMainlineRelease +./gradlew bundleMainlineRelease -ccache -s -v +if [ ! -z "${ANDROID_KEYSTORE_B64}" ]; then + rm "${ANDROID_KEYSTORE_FILE}" +fi diff --git a/.ci/android/package.sh b/.ci/android/package.sh index c2eb975a02..50b7bbc332 100755 --- a/.ci/android/package.sh +++ b/.ci/android/package.sh @@ -1,6 +1,6 @@ #!/bin/sh -# SPDX-FileCopyrightText: 2025 eden Emulator Project +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project # SPDX-License-Identifier: GPL-3.0-or-later GITDATE="$(git show -s --date=short --format='%ad' | sed 's/-//g')" diff --git a/.ci/license-header.rb b/.ci/license-header.rb index 5049acb01b..dda5522026 100644 --- a/.ci/license-header.rb +++ b/.ci/license-header.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true license_header = <<~EOF - // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project + // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later EOF diff --git a/.ci/license-header.sh b/.ci/license-header.sh new file mode 100755 index 0000000000..3d4929d1c1 --- /dev/null +++ b/.ci/license-header.sh @@ -0,0 +1,146 @@ +#!/bin/sh -e + +HEADER="$(cat "$PWD/.ci/license/header.txt")" +HEADER_HASH="$(cat "$PWD/.ci/license/header-hash.txt")" + +echo "Getting branch changes" + +# BRANCH=`git rev-parse --abbrev-ref HEAD` +# COMMITS=`git log ${BRANCH} --not master --pretty=format:"%h"` +# RANGE="${COMMITS[${#COMMITS[@]}-1]}^..${COMMITS[0]}" +# FILES=`git diff-tree --no-commit-id --name-only ${RANGE} -r` + +BASE=`git merge-base master HEAD` +FILES=`git diff --name-only $BASE` + +#FILES=$(git diff --name-only master) + +echo "Done" + +check_header() { + CONTENT="`head -n3 < $1`" + case "$CONTENT" in + "$HEADER"*) ;; + *) BAD_FILES="$BAD_FILES $1" ;; + esac +} + +check_cmake_header() { + CONTENT="`head -n3 < $1`" + + case "$CONTENT" in + "$HEADER_HASH"*) ;; + *) + BAD_CMAKE="$BAD_CMAKE $1" ;; + esac +} +for file in $FILES; do + [ -f "$file" ] || continue + + if [ `basename -- "$file"` = "CMakeLists.txt" ]; then + check_cmake_header "$file" + continue + fi + + EXTENSION="${file##*.}" + case "$EXTENSION" in + kts|kt|cpp|h) + check_header "$file" + ;; + cmake) + check_cmake_header "$file" + ;; + esac +done + +if [ "$BAD_FILES" = "" ] && [ "$BAD_CMAKE" = "" ]; then + echo + echo "All good." + + exit +fi + +if [ "$BAD_FILES" != "" ]; then + echo "The following source files have incorrect license headers:" + echo + + for file in $BAD_FILES; do echo $file; done + + cat << EOF + +The following license header should be added to the start of all offending SOURCE files: + +=== BEGIN === +$HEADER +=== END === + +EOF + +fi + +if [ "$BAD_CMAKE" != "" ]; then + echo "The following CMake files have incorrect license headers:" + echo + + for file in $BAD_CMAKE; do echo $file; done + + cat << EOF + +The following license header should be added to the start of all offending CMake files: + +=== BEGIN === +$HEADER_HASH +=== END === + +EOF + +fi + +cat << EOF +If some of the code in this PR is not being contributed by the original author, +the files which have been exclusively changed by that code can be ignored. +If this happens, this PR requirement can be bypassed once all other files are addressed. +EOF + +if [ "$FIX" = "true" ]; then + echo + echo "FIX set to true. Fixing headers." + echo + + for file in $BAD_FILES; do + cat $file > $file.bak + + cat .ci/license/header.txt > $file + echo >> $file + cat $file.bak >> $file + + rm $file.bak + + git add $file + done + + for file in $BAD_CMAKE; do + cat $file > $file.bak + + cat .ci/license/header-hash.txt > $file + echo >> $file + cat $file.bak >> $file + + rm $file.bak + + git add $file + done + echo "License headers fixed." + + if [ "$COMMIT" = "true" ]; then + echo + echo "COMMIT set to true. Committing changes." + echo + + git commit -m "Fix license headers" + + echo "Changes committed. You may now push." + fi +else + exit 1 +fi diff --git a/.ci/license/header-hash.txt b/.ci/license/header-hash.txt new file mode 100644 index 0000000000..91bc195e23 --- /dev/null +++ b/.ci/license/header-hash.txt @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/.ci/license/header.txt b/.ci/license/header.txt new file mode 100644 index 0000000000..53a4f4396e --- /dev/null +++ b/.ci/license/header.txt @@ -0,0 +1,2 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later diff --git a/.ci/linux/build.sh b/.ci/linux/build.sh index c020cc7edd..41e0ca308b 100755 --- a/.ci/linux/build.sh +++ b/.ci/linux/build.sh @@ -1,83 +1,116 @@ -#!/bin/bash -ex +#!/bin/bash -e -# SPDX-FileCopyrightText: 2025 eden Emulator Project +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project # SPDX-License-Identifier: GPL-3.0-or-later -export ARCH="$(uname -m)" +case "$1" in +amd64 | "") + echo "Making amd64-v3 optimized build of Eden" + ARCH="amd64_v3" + ARCH_FLAGS="-march=x86-64-v3" + export EXTRA_CMAKE_FLAGS=(-DYUZU_BUILD_PRESET=v3) + ;; +steamdeck | zen2) + echo "Making Steam Deck (Zen 2) optimized build of Eden" + ARCH="steamdeck" + ARCH_FLAGS="-march=znver2 -mtune=znver2" + export EXTRA_CMAKE_FLAGS=(-DYUZU_BUILD_PRESET=zen2 -DYUZU_SYSTEM_PROFILE=steamdeck) + ;; +rog-ally | allyx | zen4) + echo "Making ROG Ally X (Zen 4) optimized build of Eden" + ARCH="rog-ally-x" + ARCH_FLAGS="-march=znver4 -mtune=znver4" + export EXTRA_CMAKE_FLAGS=(-DYUZU_BUILD_PRESET=zen2 -DYUZU_SYSTEM_PROFILE=steamdeck) + ;; +legacy) + echo "Making amd64 generic build of Eden" + ARCH=amd64 + ARCH_FLAGS="-march=x86-64 -mtune=generic" + export EXTRA_CMAKE_FLAGS=(-DYUZU_BUILD_PRESET=generic) + ;; +aarch64) + echo "Making armv8-a build of Eden" + ARCH=aarch64 + ARCH_FLAGS="-march=armv8-a -mtune=generic -w" + export EXTRA_CMAKE_FLAGS=(-DYUZU_BUILD_PRESET=generic) + ;; +armv9) + echo "Making armv9-a build of Eden" + ARCH=armv9 + ARCH_FLAGS="-march=armv9-a -mtune=generic -w" + export EXTRA_CMAKE_FLAGS=(-DYUZU_BUILD_PRESET=armv9) + ;; +native) + echo "Making native build of Eden" + ARCH="$(uname -m)" + ARCH_FLAGS="-march=native -mtune=native" + export EXTRA_CMAKE_FLAGS=(-DYUZU_BUILD_PRESET=native) + ;; +*) + echo "Invalid target $1 specified, must be one of native, amd64, steamdeck, zen2, allyx, rog-ally, zen4, legacy, aarch64, armv9" + exit 1 + ;; +esac -if [ "$ARCH" = 'x86_64' ]; then - if [ "$1" = 'v3' ]; then - echo "Making x86-64-v3 optimized build of eden" - ARCH="${ARCH}_v3" - ARCH_FLAGS="-march=x86-64-v3 -O3" - else - echo "Making x86-64 generic build of eden" - ARCH_FLAGS="-march=x86-64 -mtune=generic -O3" - fi -else - echo "Making aarch64 build of eden" - ARCH_FLAGS="-march=armv8-a -mtune=generic -O3" -fi +export ARCH_FLAGS="$ARCH_FLAGS -O3" -NPROC="$2" if [ -z "$NPROC" ]; then NPROC="$(nproc)" fi +if [ "$1" != "" ]; then shift; fi + if [ "$TARGET" = "appimage" ]; then - # Compile the AppImage we distribute with Clang. - export EXTRA_CMAKE_FLAGS=(-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DCMAKE_LINKER=/etc/bin/ld.lld) - # Bundle required QT wayland libraries - export EXTRA_QT_PLUGINS="waylandcompositor" - export EXTRA_PLATFORM_PLUGINS="libqwayland-egl.so;libqwayland-generic.so" + export EXTRA_CMAKE_FLAGS=("${EXTRA_CMAKE_FLAGS[@]}" -DCMAKE_INSTALL_PREFIX=/usr -DYUZU_ROOM=ON -DYUZU_ROOM_STANDALONE=OFF -DYUZU_CMD=OFF) else # For the linux-fresh verification target, verify compilation without PCH as well. - export EXTRA_CMAKE_FLAGS=(-DCITRA_USE_PRECOMPILED_HEADERS=OFF) + export EXTRA_CMAKE_FLAGS=("${EXTRA_CMAKE_FLAGS[@]}" -DYUZU_USE_PRECOMPILED_HEADERS=OFF) fi -if [ "$GITHUB_REF_TYPE" == "tag" ]; then - export EXTRA_CMAKE_FLAGS=($EXTRA_CMAKE_FLAGS -DENABLE_QT_UPDATE_CHECKER=ON) +if [ "$DEVEL" != "true" ]; then + export EXTRA_CMAKE_FLAGS=("${EXTRA_CMAKE_FLAGS[@]}" -DENABLE_QT_UPDATE_CHECKER=ON) fi +if [ "$USE_WEBENGINE" = "true" ]; then + WEBENGINE=ON +else + WEBENGINE=OFF +fi + +if [ "$USE_MULTIMEDIA" = "false" ]; then + MULTIMEDIA=OFF +else + MULTIMEDIA=ON +fi + +if [ -z "$BUILD_TYPE" ]; then + export BUILD_TYPE="Release" +fi + +export EXTRA_CMAKE_FLAGS=("${EXTRA_CMAKE_FLAGS[@]}" $@) + mkdir -p build && cd build cmake .. -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_C_COMPILER_LAUNCHER=ccache \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DCMAKE_BUILD_TYPE="$BUILD_TYPE" \ -DENABLE_QT_TRANSLATION=ON \ -DUSE_DISCORD_PRESENCE=ON \ - -DUSE_CCACHE=ON \ -DCMAKE_CXX_FLAGS="$ARCH_FLAGS" \ -DCMAKE_C_FLAGS="$ARCH_FLAGS" \ - -DYUZU_USE_BUNDLED_VCPKG=OFF \ -DYUZU_USE_BUNDLED_QT=OFF \ - -DUSE_SYSTEM_QT=ON \ - -DYUZU_USE_BUNDLED_FFMPEG=OFF \ -DYUZU_USE_BUNDLED_SDL2=OFF \ -DYUZU_USE_EXTERNAL_SDL2=ON \ -DYUZU_TESTS=OFF \ - -DYUZU_USE_LLVM_DEMANGLE=OFF \ - -DYUZU_USE_QT_MULTIMEDIA=OFF \ - -DYUZU_USE_QT_WEB_ENGINE=OFF \ - -DENABLE_QT_TRANSLATION=ON \ - -DUSE_DISCORD_PRESENCE=OFF \ - -DYUZU_USE_FASTER_LD=OFF \ + -DYUZU_USE_QT_MULTIMEDIA=$MULTIMEDIA \ + -DYUZU_USE_QT_WEB_ENGINE=$WEBENGINE \ + -DYUZU_USE_FASTER_LD=ON \ -DYUZU_ENABLE_LTO=ON \ - -DCMAKE_LINKER=/usr/bin/mold \ - "${EXTRA_CMAKE_FLAGS[@]}" + -DDYNARMIC_ENABLE_LTO=ON \ + "${EXTRA_CMAKE_FLAGS[@]}" ninja -j${NPROC} if [ -d "bin/Release" ]; then - strip -s bin/Release/* + strip -s bin/Release/* else - strip -s bin/* + strip -s bin/* fi - -if [ "$TARGET" = "appimage" ]; then - ccache -s -else - ccache -s -v -fi - -#ctest -VV -C Release diff --git a/.ci/linux/eden.dwfsprof b/.ci/linux/eden.dwfsprof index 377d5fec89..9a3bee6f14 100644 --- a/.ci/linux/eden.dwfsprof +++ b/.ci/linux/eden.dwfsprof @@ -1,219 +1,150 @@ AppRun -org.eden_emu.eden.desktop -bin/eden +eden.desktop +dev.eden_emu.eden.desktop +shared/bin/eden shared/lib/lib.path shared/lib/ld-linux-x86-64.so.2 -shared/lib/libQt6Widgets.so.6.9.0 -shared/lib/libQt6Network.so.6.9.0 -shared/lib/libusb-1.0.so.0.5.0 -shared/lib/libenet.so.7.0.6 -shared/lib/libbrotlicommon.so.1.1.0 -shared/lib/libbrotlienc.so.1.1.0 -shared/lib/libbrotlidec.so.1.1.0 -shared/lib/libz.so.1.3.1 +shared/lib/libQt6Widgets.so.6.4.2 +shared/lib/libQt6DBus.so.6.4.2 +shared/lib/libudev.so.1.7.5 +shared/lib/libbrotlienc.so.1.0.9 +shared/lib/libbrotlidec.so.1.0.9 shared/lib/libssl.so.3 shared/lib/libcrypto.so.3 -shared/lib/libavcodec.so.61.19.100 -shared/lib/libavdevice.so.61.3.100 -shared/lib/libavfilter.so.10.4.100 -shared/lib/libavformat.so.61.7.100 -shared/lib/libavutil.so.59.39.100 -shared/lib/libswresample.so.5.3.100 -shared/lib/libswscale.so.8.3.100 -shared/lib/libva.so.2.2200.0 -shared/lib/libboost_context.so.1.87.0 -shared/lib/liblz4.so.1.10.0 -shared/lib/libzstd.so.1.5.7 -shared/lib/libSDL2-2.0.so.0.3200.54 -shared/lib/libfmt.so.11.1.4 -shared/lib/libopus.so.0.10.1 -shared/lib/libQt6Gui.so.6.9.0 -shared/lib/libQt6DBus.so.6.9.0 -shared/lib/libGLX.so.0.0.0 -shared/lib/libOpenGL.so.0.0.0 -shared/lib/libQt6Core.so.6.9.0 -shared/lib/libstdc++.so.6.0.33 +shared/lib/libavcodec.so.59.37.100 +shared/lib/libavutil.so.57.28.100 +shared/lib/libQt6Gui.so.6.4.2 +shared/lib/libQt6Core.so.6.4.2 +shared/lib/libstdc++.so.6.0.30 shared/lib/libm.so.6 shared/lib/libgcc_s.so.1 shared/lib/libc.so.6 -shared/lib/libgssapi_krb5.so.2.2 -shared/lib/libproxy.so.0.5.9 -shared/lib/libudev.so.1.7.10 -shared/lib/libvpx.so.9.1.0 -shared/lib/libwebpmux.so.3.1.1 -shared/lib/liblzma.so.5.8.1 -shared/lib/libdav1d.so.7.0.0 -shared/lib/libopencore-amrwb.so.0.0.5 -shared/lib/librsvg-2.so.2.60.0 -shared/lib/libcairo.so.2.11804.4 -shared/lib/libgobject-2.0.so.0.8400.1 -shared/lib/libglib-2.0.so.0.8400.1 -shared/lib/libsnappy.so.1.2.2 -shared/lib/libaom.so.3.12.0 -shared/lib/libgsm.so.1.0.22 -shared/lib/libjxl.so.0.11.1 -shared/lib/libjxl_threads.so.0.11.1 +shared/lib/libdbus-1.so.3.32.4 +shared/lib/libbrotlicommon.so.1.0.9 +shared/lib/libswresample.so.4.7.100 +shared/lib/libvpx.so.7.1.0 +shared/lib/libwebpmux.so.3.0.10 +shared/lib/libwebp.so.7.1.5 +shared/lib/liblzma.so.5.4.1 +shared/lib/libdav1d.so.6.6.0 +shared/lib/librsvg-2.so.2.48.0 +shared/lib/libgobject-2.0.so.0.7400.6 +shared/lib/libglib-2.0.so.0.7400.6 +shared/lib/libcairo.so.2.11600.0 +shared/lib/libzvbi.so.0.13.2 +shared/lib/libz.so.1.2.13 +shared/lib/libsnappy.so.1.1.9 +shared/lib/libaom.so.3.6.0 +shared/lib/libcodec2.so.1.0 +shared/lib/libgsm.so.1.0.19 +shared/lib/libjxl.so.0.7.0 +shared/lib/libjxl_threads.so.0.7.0 shared/lib/libmp3lame.so.0.0.0 -shared/lib/libopencore-amrnb.so.0.0.5 -shared/lib/libopenjp2.so.2.5.3 +shared/lib/libopenjp2.so.2.5.0 +shared/lib/libopus.so.0.8.0 +shared/lib/librav1e.so.0.5.1 +shared/lib/libshine.so.3.0.1 shared/lib/libspeex.so.1.5.2 +shared/lib/libSvtAv1Enc.so.1.4.1 shared/lib/libtheoraenc.so.1.1.2 shared/lib/libtheoradec.so.1.1.4 +shared/lib/libtwolame.so.0.0.0 shared/lib/libvorbis.so.0.4.9 shared/lib/libvorbisenc.so.2.0.12 -shared/lib/libwebp.so.7.1.10 shared/lib/libx264.so.164 +shared/lib/libx265.so.199 shared/lib/libxvidcore.so.4.3 -shared/lib/libvpl.so.2.14 -shared/lib/libraw1394.so.11.1.0 -shared/lib/libavc1394.so.0.3.0 -shared/lib/librom1394.so.0.3.0 -shared/lib/libiec61883.so.0.1.1 -shared/lib/libjack.so.0.1.0 -shared/lib/libdrm.so.2.124.0 -shared/lib/libxcb.so.1.1.0 -shared/lib/libxcb-shm.so.0.0.0 -shared/lib/libxcb-shape.so.0.0.0 -shared/lib/libxcb-xfixes.so.0.0.0 -shared/lib/libasound.so.2.0.0 -shared/lib/libGL.so.1.7.0 -shared/lib/libpulse.so.0.24.3 -shared/lib/libv4l2.so.0.0.0 -shared/lib/libXv.so.1.0.0 -shared/lib/libX11.so.6.4.0 -shared/lib/libXext.so.6.4.0 -shared/lib/libpostproc.so.58.3.100 -shared/lib/libbs2b.so.0.0.0 -shared/lib/librubberband.so.3.0.0 -shared/lib/libharfbuzz.so.0.61101.0 -shared/lib/libfribidi.so.0.4.0 -shared/lib/libplacebo.so.349 -shared/lib/libvmaf.so.3.0.0 -shared/lib/libass.so.9.3.1 -shared/lib/libvidstab.so.1.2 -shared/lib/libzmq.so.5.2.5 -shared/lib/libzimg.so.2.0.0 -shared/lib/libglslang.so.15.2.0 -shared/lib/libOpenCL.so.1.0.0 -shared/lib/libfontconfig.so.1.15.0 -shared/lib/libfreetype.so.6.20.2 -shared/lib/libdvdnav.so.4.3.0 -shared/lib/libdvdread.so.8.0.0 -shared/lib/libxml2.so.2.13.5 -shared/lib/libbz2.so.1.0.8 -shared/lib/libmodplug.so.1.0.0 -shared/lib/libopenmpt.so.0.4.4 -shared/lib/libbluray.so.2.4.3 -shared/lib/libgmp.so.10.5.0 -shared/lib/libgnutls.so.30.40.3 -shared/lib/libsrt.so.1.5.4 -shared/lib/libssh.so.4.10.1 -shared/lib/libva-drm.so.2.2200.0 -shared/lib/libva-x11.so.2.2200.0 +shared/lib/libva.so.2.1700.0 +shared/lib/libmfx.so.1.35 +shared/lib/libva-drm.so.2.1700.0 +shared/lib/libva-x11.so.2.1700.0 shared/lib/libvdpau.so.1.0.0 -shared/lib/libsoxr.so.0.1.2 +shared/lib/libX11.so.6.4.0 +shared/lib/libdrm.so.2.4.0 +shared/lib/libOpenCL.so.1.0.0 shared/lib/libEGL.so.1.1.0 -shared/lib/libxkbcommon.so.0.8.1 -shared/lib/libpng16.so.16.47.0 -shared/lib/libmd4c.so.0.5.2 -shared/lib/libdbus-1.so.3.38.3 -shared/lib/libGLdispatch.so.0.0.0 -shared/lib/libdouble-conversion.so.3.3.0 +shared/lib/libfontconfig.so.1.12.0 +shared/lib/libxkbcommon.so.0.0.0 +shared/lib/libGLX.so.0.0.0 +shared/lib/libOpenGL.so.0.0.0 +shared/lib/libpng16.so.16.39.0 +shared/lib/libharfbuzz.so.0.60000.0 +shared/lib/libmd4c.so.0.4.8 +shared/lib/libfreetype.so.6.18.3 +shared/lib/libicui18n.so.72.1 +shared/lib/libicuuc.so.72.1 +shared/lib/libdouble-conversion.so.3.1 shared/lib/libb2.so.1.0.4 -shared/lib/libpcre2-16.so.0.14.0 -shared/lib/libkrb5.so.3.3 -shared/lib/libk5crypto.so.3.1 -shared/lib/libcom_err.so.2.1 -shared/lib/libkrb5support.so.0.1 -shared/lib/libkeyutils.so.1.10 -shared/lib/libresolv.so.2 -shared/lib/libproxy/libpxbackend-1.0.so -shared/lib/libcap.so.2.75 -shared/lib/libgio-2.0.so.0.8400.1 -shared/lib/libgdk_pixbuf-2.0.so.0.4200.12 -shared/lib/libpangocairo-1.0.so.0.5600.3 -shared/lib/libpango-1.0.so.0.5600.3 -shared/lib/libXrender.so.1.3.0 +shared/lib/libpcre2-16.so.0.11.2 +shared/lib/libzstd.so.1.5.4 +shared/lib/libsystemd.so.0.35.0 +shared/lib/libsoxr.so.0.1.2 +shared/lib/libcairo-gobject.so.2.11600.0 +shared/lib/libgdk_pixbuf-2.0.so.0.4200.10 +shared/lib/libgio-2.0.so.0.7400.6 +shared/lib/libxml2.so.2.9.14 +shared/lib/libpangocairo-1.0.so.0.5000.12 +shared/lib/libpango-1.0.so.0.5000.12 +shared/lib/libffi.so.8.1.2 +shared/lib/libpcre2-8.so.0.11.2 +shared/lib/libpixman-1.so.0.42.2 +shared/lib/libxcb-shm.so.0.0.0 +shared/lib/libxcb.so.1.1.0 shared/lib/libxcb-render.so.0.0.0 -shared/lib/libpixman-1.so.0.44.2 -shared/lib/libffi.so.8.1.4 -shared/lib/libpcre2-8.so.0.14.0 -shared/lib/libjxl_cms.so.0.11.1 -shared/lib/libhwy.so.1.2.0 +shared/lib/libXrender.so.1.3.0 +shared/lib/libXext.so.6.4.0 +shared/lib/libhwy.so.1.0.3 +shared/lib/liblcms2.so.2.0.14 shared/lib/libogg.so.0.8.5 -shared/lib/libsharpyuv.so.0.1.1 -shared/lib/libdb-5.3.so -shared/lib/libXau.so.6.0.0 -shared/lib/libXdmcp.so.6.0.0 -shared/lib/pulseaudio/libpulsecommon-17.0.so -shared/lib/libv4lconvert.so.0.0.0 -shared/lib/libfftw3.so.3.6.10 -shared/lib/libsamplerate.so.0.2.2 -shared/lib/libgraphite2.so.3.2.1 -shared/lib/libunwind.so.8.1.0 -shared/lib/libshaderc_shared.so.1 -shared/lib/libglslang-default-resource-limits.so.15.2.0 -shared/lib/libvulkan.so.1.4.309 -shared/lib/liblcms2.so.2.0.17 -shared/lib/libdovi.so.3.3.1 -shared/lib/libunibreak.so.6.0.1 -shared/lib/libgomp.so.1.0.0 -shared/lib/libsodium.so.26.2.0 -shared/lib/libpgm-5.3.so.0.0.128 -shared/lib/libSPIRV-Tools-opt.so -shared/lib/libSPIRV-Tools.so -shared/lib/libexpat.so.1.10.2 -shared/lib/libmpg123.so.0.48.3 -shared/lib/libvorbisfile.so.3.3.8 -shared/lib/libleancrypto.so.1 -shared/lib/libp11-kit.so.0.4.1 -shared/lib/libidn2.so.0.4.0 -shared/lib/libunistring.so.5.2.0 -shared/lib/libtasn1.so.6.6.4 -shared/lib/libhogweed.so.6.10 -shared/lib/libnettle.so.8.10 +shared/lib/libnuma.so.1.0.0 +shared/lib/libpthread.so.0 shared/lib/libXfixes.so.3.1.0 shared/lib/libX11-xcb.so.1.0.0 shared/lib/libxcb-dri3.so.0.1.0 -shared/lib/libsystemd.so.0.40.0 -shared/lib/libcurl.so.4.8.0 -shared/lib/libduktape.so.207.20700 -shared/lib/libgmodule-2.0.so.0.8400.1 +shared/lib/libGLdispatch.so.0.0.0 +shared/lib/libexpat.so.1.8.10 +shared/lib/libgraphite2.so.3.2.1 +shared/lib/libicudata.so.72.1 +shared/lib/libgomp.so.1.0.0 +shared/lib/libcap.so.2.66 +shared/lib/libgcrypt.so.20.4.1 +shared/lib/liblz4.so.1.9.4 +shared/lib/libgmodule-2.0.so.0.7400.6 +shared/lib/libjpeg.so.62.3.0 shared/lib/libmount.so.1.1.0 -shared/lib/libjpeg.so.8.3.2 -shared/lib/libtiff.so.6.1.0 -shared/lib/libpangoft2-1.0.so.0.5600.3 +shared/lib/libselinux.so.1 +shared/lib/libpangoft2-1.0.so.0.5000.12 +shared/lib/libfribidi.so.0.4.0 shared/lib/libthai.so.0.3.1 -shared/lib/libsndfile.so.1.0.37 -shared/lib/libasyncns.so.0.3.1 -shared/lib/libnghttp3.so.9.2.6 -shared/lib/libnghttp2.so.14.28.4 -shared/lib/libssh2.so.1.0.1 -shared/lib/libpsl.so.5.3.5 +shared/lib/libXau.so.6.0.0 +shared/lib/libXdmcp.so.6.0.0 +shared/lib/libgpg-error.so.0.33.1 shared/lib/libblkid.so.1.1.0 -shared/lib/libjbig.so.2.1 shared/lib/libdatrie.so.1.4.0 -shared/lib/libFLAC.so.14.0.0 -shared/lib/libSDL3.so.0.2.10 +shared/lib/libbsd.so.0.11.7 +shared/lib/libmd.so.0.0.5 +shared/lib/libvulkan.so.1.3.239 share/vulkan/icd.d/intel_hasvk_icd.x86_64.json shared/lib/libvulkan_intel_hasvk.so +shared/lib/libwayland-client.so.0.21.0 shared/lib/libxcb-present.so.0.0.0 +shared/lib/libxcb-xfixes.so.0.0.0 shared/lib/libxcb-sync.so.1.0.0 shared/lib/libxcb-randr.so.0.1.0 shared/lib/libxshmfence.so.1.0.0 -shared/lib/libxcb-keysyms.so.1.0.0 -shared/lib/libwayland-client.so.0.23.1 share/vulkan/icd.d/intel_icd.x86_64.json shared/lib/libvulkan_intel.so -share/vulkan/icd.d/nouveau_icd.x86_64.json -shared/lib/libvulkan_nouveau.so +share/vulkan/icd.d/lvp_icd.x86_64.json +shared/lib/libvulkan_lvp.so +shared/lib/libLLVM-15.so.1 +shared/lib/libedit.so.2.0.70 +shared/lib/libz3.so.4 +shared/lib/libtinfo.so.6.4 share/vulkan/icd.d/radeon_icd.x86_64.json shared/lib/libvulkan_radeon.so -shared/lib/libLLVM.so.19.1 -shared/lib/libelf-0.192.so -shared/lib/libdrm_amdgpu.so.1.124.0 -shared/lib/libedit.so.0.0.75 -shared/lib/libncursesw.so.6.5 +shared/lib/libdrm_amdgpu.so.1.0.0 +shared/lib/libelf-0.188.so +shared/lib/libVkLayer_MESA_device_select.so bin/qt.conf shared/lib/qt6/plugins/platforms/libqeglfs.so shared/lib/qt6/plugins/platforms/libqlinuxfb.so @@ -225,29 +156,57 @@ shared/lib/qt6/plugins/platforms/libqvnc.so shared/lib/qt6/plugins/platforms/libqwayland-egl.so shared/lib/qt6/plugins/platforms/libqwayland-generic.so shared/lib/qt6/plugins/platforms/libqxcb.so -shared/lib/libQt6XcbQpa.so.6.9.0 -shared/lib/libxcb-cursor.so.0.0.0 -shared/lib/libxcb-icccm.so.4.0.0 -shared/lib/libxcb-image.so.0.0.0 -shared/lib/libxcb-render-util.so.0.0.0 -shared/lib/libxcb-xkb.so.1.0.0 -shared/lib/libSM.so.6.0.1 -shared/lib/libICE.so.6.3.0 -shared/lib/libxcb-xinput.so.0.1.0 -shared/lib/libxkbcommon-x11.so.0.8.1 -shared/lib/libxcb-util.so.1.0.0 -shared/lib/libuuid.so.1.3.0 +shared/lib/libQt6WaylandClient.so.6.4.2 +shared/lib/libwayland-cursor.so.0.21.0 shared/lib/qt6/plugins/platformthemes/libqgtk3.so -shared/lib/qt6/plugins/platformthemes/libqt6ct.so -shared/lib/qt6/plugins/platformthemes/libqxdgdesktopportal.so -shared/lib/libqt6ct-common.so.0.10 -etc/fonts/fonts.conf +shared/lib/libgtk-3.so.0.2406.32 +shared/lib/libgdk-3.so.0.2406.32 +shared/lib/libatk-1.0.so.0.24609.1 +shared/lib/libepoxy.so.0.0.0 shared/lib/libXi.so.6.1.0 -shared/lib/libwayland-cursor.so.0.23.1 -shared/lib/libwayland-egl.so.1.23.1 +shared/lib/libatk-bridge-2.0.so.0.0.0 +shared/lib/libwayland-egl.so.1.21.0 shared/lib/libXcursor.so.1.0.2 +shared/lib/libXdamage.so.1.1.0 +shared/lib/libXcomposite.so.1.0.0 shared/lib/libXrandr.so.2.2.0 -shared/lib/qt6/plugins/styles/libqt6ct-style.so +shared/lib/libXinerama.so.1.0.0 +shared/lib/libdl.so.2 +shared/lib/libatspi.so.0.0.1 +share/glib-2.0/schemas/gschemas.compiled +shared/lib/gio/modules/giomodule.cache +shared/lib/gio/modules/libdconfsettings.so +shared/lib/gio/modules/libgvfsdbus.so +shared/lib/gvfs/libgvfscommon.so +share/X11/xkb/rules/evdev +share/X11/xkb/keycodes/evdev +share/X11/xkb/keycodes/aliases +share/X11/xkb/types/complete +share/X11/xkb/types/basic +share/X11/xkb/types/mousekeys +share/X11/xkb/types/pc +share/X11/xkb/types/iso9995 +share/X11/xkb/types/level5 +share/X11/xkb/types/extra +share/X11/xkb/types/numpad +share/X11/xkb/compat/complete +share/X11/xkb/compat/basic +share/X11/xkb/compat/ledcaps +share/X11/xkb/compat/lednum +share/X11/xkb/compat/iso9995 +share/X11/xkb/compat/mousekeys +share/X11/xkb/compat/accessx +share/X11/xkb/compat/misc +share/X11/xkb/compat/ledscroll +share/X11/xkb/compat/xfree86 +share/X11/xkb/compat/level5 +share/X11/xkb/compat/caps +share/X11/xkb/symbols/pc +share/X11/xkb/symbols/srvr_ctrl +share/X11/xkb/symbols/keypad +share/X11/xkb/symbols/altwin +share/X11/xkb/symbols/us +share/X11/xkb/symbols/inet shared/lib/qt6/plugins/platforminputcontexts/libcomposeplatforminputcontextplugin.so shared/lib/qt6/plugins/platforminputcontexts/libibusplatforminputcontextplugin.so shared/lib/qt6/plugins/iconengines/libqsvgicon.so @@ -255,5 +214,37 @@ shared/lib/qt6/plugins/imageformats/libqgif.so shared/lib/qt6/plugins/imageformats/libqico.so shared/lib/qt6/plugins/imageformats/libqjpeg.so shared/lib/qt6/plugins/imageformats/libqsvg.so -shared/lib/libQt6Svg.so.6.9.0 +shared/lib/libQt6Svg.so.6.4.2 +etc/fonts/fonts.conf +shared/lib/qt6/plugins/wayland-shell-integration/libfullscreen-shell-v1.so +shared/lib/qt6/plugins/wayland-shell-integration/libivi-shell.so +shared/lib/qt6/plugins/wayland-shell-integration/libqt-shell.so +shared/lib/qt6/plugins/wayland-shell-integration/libwl-shell-plugin.so +shared/lib/qt6/plugins/wayland-shell-integration/libxdg-shell.so +shared/lib/qt6/plugins/wayland-graphics-integration-client/libdmabuf-server.so +shared/lib/qt6/plugins/wayland-graphics-integration-client/libdrm-egl-server.so +shared/lib/qt6/plugins/wayland-graphics-integration-client/libqt-plugin-wayland-egl.so +shared/lib/qt6/plugins/wayland-graphics-integration-client/libshm-emulation-server.so +shared/lib/qt6/plugins/wayland-graphics-integration-client/libvulkan-server.so +shared/lib/libQt6WaylandEglClientHwIntegration.so.6.4.2 +shared/lib/libQt6OpenGL.so.6.4.2 +share/glvnd/egl_vendor.d/50_mesa.json +shared/lib/libEGL_mesa.so.0.0.0 +shared/lib/libgbm.so.1.0.0 +shared/lib/libglapi.so.0.0.0 +shared/lib/libxcb-dri2.so.0.0.0 +shared/lib/libwayland-server.so.0.21.0 +shared/lib/dri/swrast_dri.so +shared/lib/libsensors.so.5.0.0 +shared/lib/libdrm_radeon.so.1.0.1 +shared/lib/libdrm_nouveau.so.2.0.0 +shared/lib/libdrm_intel.so.1.0.0 +shared/lib/libpciaccess.so.0.11.1 +shared/lib/qt6/plugins/wayland-decoration-client/libbradient.so +shared/lib/gtk-3.0/modules/libcanberra-gtk3-module.so +shared/lib/libcanberra-gtk3.so.0.1.9 +shared/lib/libcanberra.so.0.2.5 +shared/lib/libvorbisfile.so.3.3.8 +shared/lib/libtdb.so.1.4.8 +shared/lib/libltdl.so.7.3.2 shared/lib/libXss.so.1.0.0 diff --git a/.ci/linux/package.sh b/.ci/linux/package.sh index 4d58b8f328..838476097a 100755 --- a/.ci/linux/package.sh +++ b/.ci/linux/package.sh @@ -1,67 +1,97 @@ -#!/bin/sh +#!/bin/sh -e -# SPDX-FileCopyrightText: 2025 eden Emulator Project +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project # SPDX-License-Identifier: GPL-3.0-or-later # This script assumes you're in the source directory -set -ex export APPIMAGE_EXTRACT_AND_RUN=1 export BASE_ARCH="$(uname -m)" -export ARCH="$BASE_ARCH" -LIB4BN="https://raw.githubusercontent.com/VHSgunzo/sharun/refs/heads/main/lib4bin" -URUNTIME="https://github.com/VHSgunzo/uruntime/releases/latest/download/uruntime-appimage-dwarfs-$ARCH" +SHARUN="https://github.com/VHSgunzo/sharun/releases/latest/download/sharun-${BASE_ARCH}-aio" +URUNTIME="https://github.com/VHSgunzo/uruntime/releases/latest/download/uruntime-appimage-dwarfs-${BASE_ARCH}" -if [ "$ARCH" = 'x86_64' ]; then - if [ "$1" = 'v3' ]; then - ARCH="${ARCH}_v3" - fi +case "$1" in + amd64|"") + echo "Packaging amd64-v3 optimized build of Eden" + ARCH="amd64_v3" + ;; + steamdeck|zen2) + echo "Packaging Steam Deck (Zen 2) optimized build of Eden" + ARCH="steamdeck" + ;; + rog-ally|allyx|zen4) + echo "Packaging ROG Ally X (Zen 4) optimized build of Eden" + ARCH="rog-ally-x" + ;; + legacy) + echo "Packaging amd64 generic build of Eden" + ARCH=amd64 + ;; + aarch64) + echo "Packaging armv8-a build of Eden" + ARCH=aarch64 + ;; + armv9) + echo "Packaging armv9-a build of Eden" + ARCH=armv9 + ;; + native) + echo "Packaging native build of Eden" + ARCH="$BASE_ARCH" + ;; + +esac + +export BUILDDIR="$2" + +if [ "$BUILDDIR" = '' ] +then + BUILDDIR=build fi EDEN_TAG=$(git describe --tags --abbrev=0) -echo "Making stable \"$EDEN_TAG\" build" -git checkout "$EDEN_TAG" +echo "Making \"$EDEN_TAG\" build" +# git checkout "$EDEN_TAG" VERSION="$(echo "$EDEN_TAG")" # NOW MAKE APPIMAGE mkdir -p ./AppDir cd ./AppDir -cat > eden.desktop << EOL -[Desktop Entry] -Type=Application -Name=Eden -Icon=eden -StartupWMClass=eden -Exec=eden -Categories=Game;Emulator; -EOL +cp ../dist/dev.eden_emu.eden.desktop . +cp ../dist/dev.eden_emu.eden.svg . -cp ../dist/eden.svg ./eden.svg +ln -sf ./dev.eden_emu.eden.svg ./.DirIcon -ln -sf ./eden.svg ./.DirIcon +UPINFO='gh-releases-zsync|eden-emulator|Releases|latest|*.AppImage.zsync' if [ "$DEVEL" = 'true' ]; then - sed -i 's|Name=Eden|Name=Eden Nightly|' ./eden.desktop - UPINFO="$(echo "$UPINFO" | sed 's|latest|nightly|')" + sed -i 's|Name=Eden|Name=Eden Nightly|' ./dev.eden_emu.eden.desktop + UPINFO="$(echo "$UPINFO" | sed 's|Releases|nightly|')" fi LIBDIR="/usr/lib" -# some distros are weird and use a subdir -if [ ! -f "/usr/lib/libGL.so" ] +# Workaround for Gentoo +if [ ! -d "$LIBDIR/qt6" ] +then + LIBDIR="/usr/lib64" +fi + +# Workaround for Debian +if [ ! -d "$LIBDIR/qt6" ] then LIBDIR="/usr/lib/${BASE_ARCH}-linux-gnu" fi # Bundle all libs -wget --retry-connrefused --tries=30 "$LIB4BN" -O ./lib4bin -chmod +x ./lib4bin -xvfb-run -a -- ./lib4bin -p -v -e -s -k \ - ../build/bin/eden* \ + +wget --retry-connrefused --tries=30 "$SHARUN" -O ./sharun-aio +chmod +x ./sharun-aio +xvfb-run -a ./sharun-aio l -p -v -e -s -k \ + ../$BUILDDIR/bin/eden* \ $LIBDIR/lib*GL*.so* \ - $LIBDIR/libSDL2*.so* \ $LIBDIR/dri/* \ $LIBDIR/vdpau/* \ $LIBDIR/libvulkan* \ @@ -83,14 +113,18 @@ xvfb-run -a -- ./lib4bin -p -v -e -s -k \ $LIBDIR/spa-0.2/*/* \ $LIBDIR/alsa-lib/* +rm -f ./sharun-aio + # Prepare sharun if [ "$ARCH" = 'aarch64' ]; then - # allow the host vulkan to be used for aarch64 given the sed situation + # allow the host vulkan to be used for aarch64 given the sad situation echo 'SHARUN_ALLOW_SYS_VKICD=1' > ./.env fi -wget https://github.com/VHSgunzo/sharun/releases/download/v0.6.3/sharun-x86_64 -O sharun -chmod a+x sharun +# Workaround for Gentoo +if [ -d "shared/libproxy" ]; then + cp shared/libproxy/* lib/ +fi ln -f ./sharun ./AppRun ./sharun -g @@ -116,9 +150,4 @@ echo "Generating AppImage..." echo "Generating zsync file..." zsyncmake *.AppImage -u *.AppImage -echo "All Done!" - -# Cleanup - -rm -rf AppDir -rm uruntime +echo "All Done!" \ No newline at end of file diff --git a/.ci/translate.sh b/.ci/translate.sh new file mode 100755 index 0000000000..c0b7dba9f6 --- /dev/null +++ b/.ci/translate.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +for i in dist/languages/*.ts; do + SRC=en_US + TARGET=`head -n1 $i | awk -F 'language="' '{split($2, a, "\""); print a[1]}'` + + # requires fd + SOURCES=`fd . src/yuzu src/qt_common -tf -e ui -e cpp -e h -e plist` + + lupdate -source-language $SRC -target-language $TARGET $SOURCES -ts /data/code/eden/$i +done diff --git a/.ci/windows/build-bqt.bat b/.ci/windows/build-bqt.bat deleted file mode 100755 index 925d420690..0000000000 --- a/.ci/windows/build-bqt.bat +++ /dev/null @@ -1,27 +0,0 @@ -echo off - -set chain=%1 - -if not defined DevEnvDir ( - "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" %chain% -) - -mkdir build - -cmake -S . -B build\%chain% ^ --DCMAKE_BUILD_TYPE=Release ^ --DYUZU_USE_BUNDLED_QT=ON ^ --DENABLE_QT_TRANSLATION=ON ^ --DUSE_DISCORD_PRESENCE=ON ^ --DYUZU_USE_BUNDLED_VCPKG=ON ^ --DYUZU_USE_BUNDLED_SDL2=ON ^ --G "Ninja" ^ --DYUZU_TESTS=OFF ^ --DCMAKE_C_COMPILER_LAUNCHER=ccache ^ --DCMAKE_CXX_COMPILER_LAUNCHER=ccache ^ --DCMAKE_TOOLCHAIN_FILE="%CD%\CMakeModules\MSVCCache.cmake" ^ --DUSE_CCACHE=ON - -cmake --build build\%chain% - -ccache -s -v \ No newline at end of file diff --git a/.ci/windows/build.bat b/.ci/windows/build.bat deleted file mode 100755 index 4dbe862a94..0000000000 --- a/.ci/windows/build.bat +++ /dev/null @@ -1,31 +0,0 @@ -echo off - -set chain=%1 -set qt_ver=%2 - -if not defined DevEnvDir ( - CALL "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" %chain% -) - -CALL mkdir build - -CALL cmake -S . -B build\%chain% ^ --DCMAKE_BUILD_TYPE=Release ^ --DYUZU_USE_BUNDLED_QT=ON ^ --DENABLE_QT_TRANSLATION=ON ^ --DUSE_DISCORD_PRESENCE=ON ^ --DYUZU_USE_BUNDLED_VCPKG=ON ^ --DYUZU_USE_BUNDLED_SDL2=ON ^ --G "Ninja" ^ --DYUZU_TESTS=OFF ^ --DUSE_BUNDLED_QT=OFF ^ --DUSE_SYSTEM_QT=ON ^ --DCMAKE_PREFIX_PATH=C:\Qt\%qt_ver%\msvc2022_64 ^ --DCMAKE_C_COMPILER_LAUNCHER=ccache ^ --DCMAKE_CXX_COMPILER_LAUNCHER=ccache ^ --DCMAKE_TOOLCHAIN_FILE="%CD%\CMakeModules\MSVCCache.cmake" ^ --DUSE_CCACHE=ON - -CALL cmake --build build\%chain% - -CALL ccache -s -v \ No newline at end of file diff --git a/.ci/windows/build.sh b/.ci/windows/build.sh new file mode 100644 index 0000000000..d554e00e1b --- /dev/null +++ b/.ci/windows/build.sh @@ -0,0 +1,52 @@ +#!/bin/bash -ex + +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later + +if [ "$COMPILER" == "clang" ] +then + EXTRA_CMAKE_FLAGS+=( + -DCMAKE_CXX_COMPILER=clang-cl + -DCMAKE_C_COMPILER=clang-cl + -DCMAKE_CXX_FLAGS="-O3" + -DCMAKE_C_FLAGS="-O3" + ) + + BUILD_TYPE="RelWithDebInfo" +fi + +[ -z "$WINDEPLOYQT" ] && { echo "WINDEPLOYQT environment variable required."; exit 1; } + +echo $EXTRA_CMAKE_FLAGS + +mkdir -p build && cd build +cmake .. -G Ninja \ + -DCMAKE_BUILD_TYPE="${BUILD_TYPE:-Release}" \ + -DENABLE_QT_TRANSLATION=ON \ + -DUSE_DISCORD_PRESENCE=ON \ + -DYUZU_USE_BUNDLED_SDL2=ON \ + -DBUILD_TESTING=OFF \ + -DYUZU_TESTS=OFF \ + -DDYNARMIC_TESTS=OFF \ + -DYUZU_CMD=OFF \ + -DYUZU_ROOM_STANDALONE=OFF \ + -DYUZU_USE_QT_MULTIMEDIA=${USE_MULTIMEDIA:-false} \ + -DYUZU_USE_QT_WEB_ENGINE=${USE_WEBENGINE:-false} \ + -DYUZU_ENABLE_LTO=ON \ + -DCMAKE_EXE_LINKER_FLAGS=" /LTCG" \ + -DDYNARMIC_ENABLE_LTO=ON \ + -DYUZU_USE_BUNDLED_QT=${BUNDLE_QT:-false} \ + -DUSE_CCACHE=${CCACHE:-false} \ + -DENABLE_QT_UPDATE_CHECKER=${DEVEL:-true} \ + "${EXTRA_CMAKE_FLAGS[@]}" \ + "$@" + +ninja + +set +e +rm -f bin/*.pdb +set -e + +$WINDEPLOYQT --release --no-compiler-runtime --no-opengl-sw --no-system-dxc-compiler --no-system-d3d-compiler --dir pkg bin/eden.exe + +cp bin/* pkg diff --git a/.ci/windows/cygwin.bat b/.ci/windows/cygwin.bat deleted file mode 100755 index e772780fac..0000000000 --- a/.ci/windows/cygwin.bat +++ /dev/null @@ -1,19 +0,0 @@ -echo off - -call C:\tools\cygwin\cygwinsetup.exe -q -P autoconf,automake,libtool,make,pkg-config - -REM Create wrapper batch files for Cygwin tools in a directory that will be in PATH -REM uncomment this for first-run only -REM call mkdir C:\cygwin-wrappers - -REM Create autoconf.bat wrapper -call echo @echo off > C:\cygwin-wrappers\autoconf.bat -call echo C:\tools\cygwin\bin\bash.exe -l -c "autoconf %%*" >> C:\cygwin-wrappers\autoconf.bat - -REM Add other wrappers if needed for other Cygwin tools -call echo @echo off > C:\cygwin-wrappers\automake.bat -call echo C:\tools\cygwin\bin\bash.exe -l -c "automake %%*" >> C:\cygwin-wrappers\automake.bat - -REM Add the wrappers directory to PATH -call echo C:\cygwin-wrappers>>"%GITHUB_PATH%" -call echo C:\tools\cygwin\bin>>"%GITHUB_PATH%" diff --git a/.ci/windows/install-msvc.ps1 b/.ci/windows/install-msvc.ps1 new file mode 100755 index 0000000000..788b2848ad --- /dev/null +++ b/.ci/windows/install-msvc.ps1 @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: 2025 Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later + +$ErrorActionPreference = "Stop" + +# Check if running as administrator +if (-not ([bool](net session 2>$null))) { + Write-Host "This script must be run with administrator privileges!" + Exit 1 +} + +$VSVer = "17" +$ExeFile = "vs_community.exe" +$Uri = "https://aka.ms/vs/$VSVer/release/$ExeFile" +$Destination = "./$ExeFile" + +Write-Host "Downloading Visual Studio Build Tools from $Uri" +$WebClient = New-Object System.Net.WebClient +$WebClient.DownloadFile($Uri, $Destination) +Write-Host "Finished downloading $ExeFile" + +$Arguments = @( + "--quiet", # Suppress installer UI + "--wait", # Wait for installation to complete + "--norestart", # Prevent automatic restart + "--force", # Force installation even if components are already installed + "--add Microsoft.VisualStudio.Workload.NativeDesktop", # Desktop development with C++ + "--add Microsoft.VisualStudio.Component.VC.Tools.x86.x64", # Core C++ compiler/tools for x86/x64 + "--add Microsoft.VisualStudio.Component.Windows11SDK.26100",# Windows 11 SDK (26100) + "--add Microsoft.VisualStudio.Component.Windows10SDK.19041",# Windows 10 SDK (19041) + "--add Microsoft.VisualStudio.Component.VC.Llvm.Clang", # LLVM Clang compiler + "--add Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset", # LLVM Clang integration toolset + "--add Microsoft.VisualStudio.Component.Windows11SDK.22621",# Windows 11 SDK (22621) + "--add Microsoft.VisualStudio.Component.VC.CMake.Project", # CMake project support + "--add Microsoft.VisualStudio.ComponentGroup.VC.Tools.142.x86.x64", # VC++ 14.2 toolset + "--add Microsoft.VisualStudio.ComponentGroup.NativeDesktop.Llvm.Clang" # LLVM Clang for native desktop +) + +Write-Host "Installing Visual Studio Build Tools" +$InstallProcess = Start-Process -FilePath $Destination -NoNewWindow -PassThru -ArgumentList $Arguments + +# Spinner while installing +$Spinner = "|/-\" +$i = 0 +while (-not $InstallProcess.HasExited) { + Write-Host -NoNewline ("`rInstalling... " + $Spinner[$i % $Spinner.Length]) + Start-Sleep -Milliseconds 250 + $i++ +} + +# Clear spinner line +Write-Host "`rSetup completed! " + +$ExitCode = $InstallProcess.ExitCode +if ($ExitCode -ne 0) { + Write-Host "Error installing Visual Studio Build Tools (Error: $ExitCode)" + Exit $ExitCode +} + +Write-Host "Finished installing Visual Studio Build Tools" diff --git a/.ci/windows/install-vulkan-sdk.ps1 b/.ci/windows/install-vulkan-sdk.ps1 index de218d90ad..4c5274d1b7 100755 --- a/.ci/windows/install-vulkan-sdk.ps1 +++ b/.ci/windows/install-vulkan-sdk.ps1 @@ -3,8 +3,14 @@ $ErrorActionPreference = "Stop" -$VulkanSDKVer = "1.3.250.1" -$ExeFile = "VulkanSDK-$VulkanSDKVer-Installer.exe" +# Check if running as administrator +if (-not ([bool](net session 2>$null))) { + Write-Host "This script must be run with administrator privileges!" + Exit 1 +} + +$VulkanSDKVer = "1.4.321.1" +$ExeFile = "vulkansdk-windows-X64-$VulkanSDKVer.exe" $Uri = "https://sdk.lunarg.com/sdk/download/$VulkanSDKVer/windows/$ExeFile" $Destination = "./$ExeFile" @@ -30,4 +36,4 @@ echo "Finished installing Vulkan SDK $VulkanSDKVer" if ("$env:GITHUB_ACTIONS" -eq "true") { echo "VULKAN_SDK=$VULKAN_SDK" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append echo "$VULKAN_SDK/Bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append -} +} \ No newline at end of file diff --git a/.ci/windows/package.ps1 b/.ci/windows/package.ps1 deleted file mode 100755 index f7c5f09042..0000000000 --- a/.ci/windows/package.ps1 +++ /dev/null @@ -1,42 +0,0 @@ -# SPDX-FileCopyrightText: 2025 eden Emulator Project -# SPDX-License-Identifier: GPL-3.0-or-later - -$target=$args[0] -$debug=$args[1] - -$GITDATE = $(git show -s --date=short --format='%ad') -replace "-", "" -$GITREV = $(git show -s --format='%h') -$RELEASE_DIST = "eden-windows-msvc" -$ARTIFACTS_DIR = "artifacts" - -New-Item -ItemType Directory -Path $ARTIFACTS_DIR -Force -New-Item -ItemType Directory -Path $RELEASE_DIST -Force - -if ($debug -eq "yes") { - mkdir -p pdb - $BUILD_PDB = "eden-windows-msvc-$GITDATE-$GITREV-debugsymbols.zip" - Get-ChildItem "build/$target/bin/" -Recurse -Filter "*.pdb" | Copy-Item -destination .\pdb -ErrorAction SilentlyContinue - - if (Test-Path -Path ".\pdb\*.pdb") { - 7z a -tzip $BUILD_PDB .\pdb\*.pdb - Move-Item $BUILD_PDB $ARTIFACTS_DIR/ -ErrorAction SilentlyContinue - } -} else { - Remove-Item -Force "$RELEASE_DIST\*.pdb" -} - - -Copy-Item "build/$target/bin/Release/*" -Destination "$RELEASE_DIST" -Recurse -ErrorAction SilentlyContinue -if (-not $?) { - # Try without Release subfolder if that doesn't exist - Copy-Item "build/$target/bin/*" -Destination "$RELEASE_DIST" -Recurse -ErrorAction SilentlyContinue -} - - -$BUILD_ZIP = "eden-windows-msvc-$GITDATE-$GITREV.zip" - -7z a -tzip $BUILD_ZIP $RELEASE_DIST\* - -Move-Item $BUILD_ZIP $ARTIFACTS_DIR/ -Force #-ErrorAction SilentlyContinue -Copy-Item "LICENSE*" -Destination "$RELEASE_DIST" -ErrorAction SilentlyContinue -Copy-Item "README*" -Destination "$RELEASE_DIST" -ErrorAction SilentlyContinue diff --git a/.ci/windows/package.sh b/.ci/windows/package.sh new file mode 100644 index 0000000000..2d126dc5be --- /dev/null +++ b/.ci/windows/package.sh @@ -0,0 +1,18 @@ +GITDATE=$(git show -s --date=short --format='%ad' | tr -d "-") +GITREV=$(git show -s --format='%h') + +ZIP_NAME="Eden-Windows-${ARCH}-${GITDATE}-${GITREV}.zip" + +ARTIFACTS_DIR="artifacts" +PKG_DIR="build/pkg" + +mkdir -p "$ARTIFACTS_DIR" + +TMP_DIR=$(mktemp -d) + +cp -r "$PKG_DIR"/* "$TMP_DIR"/ +cp LICENSE* README* "$TMP_DIR"/ + +7z a -tzip "$ARTIFACTS_DIR/$ZIP_NAME" "$TMP_DIR"/* + +rm -rf "$TMP_DIR" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d8d636c117..ac67f73d00 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,26 +2,27 @@ # some stuff needs cleaned up etc name: eden-build -on: - push: - branches: [ "master" ] - pull_request: - branches: [ master ] +#on: +# push: +# branches: [ "master" ] # TODO: combine build.yml into trigger_release.yml jobs: source: if: ${{ !github.head_ref }} - runs-on: linux + runs-on: source steps: - uses: actions/checkout@v4 with: submodules: recursive + - name: Pack run: ./.ci/source.sh + - name: Upload uses: forgejo/upload-artifact@v4 with: + retention-days: 2 name: source.zip path: artifacts/ @@ -66,17 +67,21 @@ jobs: run: ./.ci/windows/cygwin.bat - name: Configure & Build - id: cmake - shell: cmd - run: ./.ci/windows/build-bqt.bat amd64 yes + shell: bash + run: | + ./.ci/windows/qt-envvars.sh + DEVEL=true WINDEPLOYQT="/c/Qt/6.9.0/msvc2022_64/bin/windeployqt6.exe" .ci/windows/build.sh -DCMAKE_PREFIX_PATH=C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6 - name: Package artifacts - shell: powershell - run: ./.ci/windows/package.ps1 amd64 yes + shell: bash + run: | + ./.ci/windows/qt-envvars.sh + ./.ci/windows/package.sh - name: Upload Windows artifacts uses: forgejo/upload-artifact@v4 with: + retention-days: 2 name: ${{ matrix.target }}.zip path: artifacts/* @@ -96,14 +101,15 @@ jobs: fetch-tags: true - name: Build - run: ./.ci/linux/build.sh v3 8 + run: TARGET=appimage DEVEL=true ./.ci/linux/build.sh - name: Package AppImage - run: ./.ci/linux/package.sh v3 &> /dev/null + run: DEVEL=true ./.ci/linux/package.sh &> /dev/null - name: Upload Linux artifacts uses: forgejo/upload-artifact@v4 with: + retention-days: 3 name: linux.zip path: ./*.AppImage @@ -111,9 +117,6 @@ jobs: runs-on: android env: - CCACHE_DIR: /home/runner/.cache/ccache - CCACHE_COMPILERCHECK: content - CCACHE_SLOPPINESS: time_macros OS: android TARGET: universal @@ -132,7 +135,11 @@ jobs: echo $GIT_TAG_NAME - name: Build - run: ANDROID_HOME=/opt/android-sdk ./.ci/android/build.sh + run: ANDROID_HOME=/home/runner/sdk ./.ci/android/build.sh + env: + ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASS }} - name: Package Android artifacts run: ./.ci/android/package.sh @@ -140,5 +147,6 @@ jobs: - name: Upload Android artifacts uses: forgejo/upload-artifact@v4 with: + retention-days: 2 name: android.zip path: artifacts/* diff --git a/.github/workflows/license-header.yml b/.github/workflows/license-header.yml index e07be810dd..d6935dcac9 100644 --- a/.github/workflows/license-header.yml +++ b/.github/workflows/license-header.yml @@ -1,22 +1,22 @@ name: eden-license on: - pull_request_target: + pull_request: branches: [ master ] jobs: license-header: - runs-on: linux + runs-on: source steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Fetch master branch + - name: Fetch run: git fetch origin master:master - name: Make script executable - run: chmod +x ./.ci/license-header.rb + run: chmod +x ./.ci/license-header.sh - name: Check license headers - run: ./.ci/license-header.rb + run: ./.ci/license-header.sh diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index 5553884862..645b21e25a 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -1,15 +1,15 @@ name: Build Application and Make Release -on: - push: - tags: [ "*" ] +#on: +# push: +# tags: [ "*" ] permissions: contents: write jobs: source: - runs-on: linux + runs-on: source steps: - uses: actions/checkout@v4 with: @@ -19,6 +19,7 @@ jobs: - name: Upload uses: forgejo/upload-artifact@v4 with: + retention-days: 2 name: source.zip path: artifacts/ @@ -63,17 +64,19 @@ jobs: run: ./.ci/windows/cygwin.bat - name: Configure & Build - id: cmake - shell: cmd - run: ./.ci/windows/build-bqt.bat amd64 no + shell: bash + run: DEVEL=false ./.ci/windows/build.sh - name: Package artifacts - shell: powershell - run: ./.ci/windows/package.ps1 amd64 no + shell: bash + run: | + export PATH="${PATH}:/c/Qt/6.9.0/msvc2022_64/bin" + ./.ci/windows/package.sh - name: Upload Windows artifacts uses: forgejo/upload-artifact@v4 with: + retention-days: 2 name: ${{ matrix.target }}.zip path: artifacts/* @@ -93,7 +96,7 @@ jobs: fetch-tags: true - name: Build - run: ./.ci/linux/build.sh v3 8 + run: TARGET=appimage RELEASE=1 ./.ci/linux/build.sh v3 8 - name: Package AppImage run: ./.ci/linux/package.sh v3 &> /dev/null @@ -101,6 +104,7 @@ jobs: - name: Upload Linux artifacts uses: forgejo/upload-artifact@v4 with: + retention-days: 2 name: linux.zip path: ./*.AppImage* @@ -129,7 +133,11 @@ jobs: echo $GIT_TAG_NAME - name: Build - run: ANDROID_HOME=/opt/android-sdk ./.ci/android/build.sh + run: ANDROID_HOME=/home/runner/sdk ./.ci/android/build.sh + env: + ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASS }} - name: Package Android artifacts run: ./.ci/android/package.sh @@ -137,6 +145,7 @@ jobs: - name: Upload Android artifacts uses: forgejo/upload-artifact@v4 with: + retention-days: 2 name: android.zip path: artifacts/* diff --git a/.gitignore b/.gitignore index fbadb208be..2b342e5145 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,14 @@ # SPDX-FileCopyrightText: 2013 Citra Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later + # Build directory -[Bb]uild*/ +/[Bb]uild*/ doc-build/ +AppDir/ +uruntime # Generated source files src/common/scm_rev.cpp @@ -14,14 +19,21 @@ dist/english_plurals/generated_en.ts .idea/ .vs/ .vscode/ +.cache/ +profile.json.gz CMakeLists.txt.user* +# kdevelop +.kdev4/ +*.kdev4 + # *nix related # Common convention for backup or temporary files *~ # Visual Studio CMake settings CMakeSettings.json +.cache/ # OSX global filetypes # Created by Finder or Spotlight in directories for various OS functionality (indexing, etc) @@ -36,3 +48,8 @@ CMakeSettings.json # Windows global filetypes Thumbs.db +# Artifacts +eden-windows-msvc +artifacts +*.AppImage* +/install* diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ce5adfe266..0000000000 --- a/.gitmodules +++ /dev/null @@ -1,75 +0,0 @@ -# SPDX-FileCopyrightText: 2014 Citra Emulator Project -# SPDX-License-Identifier: GPL-2.0-or-later - -[submodule "enet"] - path = externals/enet - url = https://git.eden-emu.dev/eden-emu/enet.git -[submodule "cubeb"] - path = externals/cubeb - url = https://git.eden-emu.dev/eden-emu/cubeb.git -[submodule "dynarmic"] - path = externals/dynarmic - url = https://git.eden-emu.dev/eden-emu/dynarmic.git -[submodule "libusb"] - path = externals/libusb/libusb - url = https://git.eden-emu.dev/eden-emu/libusb.git -[submodule "discord-rpc"] - path = externals/discord-rpc - url = https://git.eden-emu.dev/eden-emu/discord-rpc.git -[submodule "Vulkan-Headers"] - path = externals/Vulkan-Headers - url = https://git.eden-emu.dev/eden-emu/Vulkan-Headers.git -[submodule "sirit"] - path = externals/sirit - url = https://git.eden-emu.dev/eden-emu/sirit.git -[submodule "mbedtls"] - path = externals/mbedtls - url = https://git.eden-emu.dev/eden-emu/mbedtls.git -[submodule "xbyak"] - path = externals/xbyak - url = https://git.eden-emu.dev/eden-emu/xbyak.git -[submodule "opus"] - path = externals/opus - url = https://git.eden-emu.dev/eden-emu/opus.git -[submodule "SDL"] - path = externals/SDL - url = https://git.eden-emu.dev/eden-emu/SDL.git -[submodule "cpp-httplib"] - path = externals/cpp-httplib - url = https://git.eden-emu.dev/eden-emu/cpp-httplib.git -[submodule "ffmpeg"] - path = externals/ffmpeg/ffmpeg - url = https://git.eden-emu.dev/eden-emu/FFmpeg.git -[submodule "vcpkg"] - path = externals/vcpkg - url = https://git.eden-emu.dev/eden-emu/vcpkg.git -[submodule "cpp-jwt"] - path = externals/cpp-jwt - url = https://git.eden-emu.dev/eden-emu/cpp-jwt.git -[submodule "libadrenotools"] - path = externals/libadrenotools - url = https://git.eden-emu.dev/eden-emu/libadrenotools.git -[submodule "tzdb_to_nx"] - path = externals/nx_tzdb/tzdb_to_nx - url = https://git.eden-emu.dev/eden-emu/tzdb_to_nx.git -[submodule "VulkanMemoryAllocator"] - path = externals/VulkanMemoryAllocator - url = https://git.eden-emu.dev/eden-emu/VulkanMemoryAllocator.git -[submodule "breakpad"] - path = externals/breakpad - url = https://git.eden-emu.dev/eden-emu/breakpad.git -[submodule "simpleini"] - path = externals/simpleini - url = https://git.eden-emu.dev/eden-emu/simpleini.git -[submodule "oaknut"] - path = externals/oaknut - url = https://git.eden-emu.dev/eden-emu/oaknut.git -[submodule "Vulkan-Utility-Libraries"] - path = externals/Vulkan-Utility-Libraries - url = https://git.eden-emu.dev/eden-emu/Vulkan-Utility-Libraries.git -[submodule "oboe"] - path = externals/oboe - url = https://git.eden-emu.dev/eden-emu/oboe.git -[submodule "externals/boost-headers"] - path = externals/boost-headers - url = https://git.eden-emu.dev/eden-emu/headers.git diff --git a/.patch/boost/0001-clang-cl.patch b/.patch/boost/0001-clang-cl.patch new file mode 100644 index 0000000000..cdabc712cb --- /dev/null +++ b/.patch/boost/0001-clang-cl.patch @@ -0,0 +1,13 @@ +diff --git a/libs/cobalt/include/boost/cobalt/concepts.hpp b/libs/cobalt/include/boost/cobalt/concepts.hpp +index d49f2ec..a9bdb80 100644 +--- a/libs/cobalt/include/boost/cobalt/concepts.hpp ++++ b/libs/cobalt/include/boost/cobalt/concepts.hpp +@@ -62,7 +62,7 @@ struct enable_awaitables + template + concept with_get_executor = requires (T& t) + { +- {t.get_executor()} -> asio::execution::executor; ++ t.get_executor(); + }; + + diff --git a/.patch/boost/0002-use-marmasm.patch b/.patch/boost/0002-use-marmasm.patch new file mode 100644 index 0000000000..10f490b878 --- /dev/null +++ b/.patch/boost/0002-use-marmasm.patch @@ -0,0 +1,11 @@ +--- a/libs/context/CMakeLists.txt 2025-09-08 00:42:31.303651800 -0400 ++++ b/libs/context/CMakeLists.txt 2025-09-08 00:42:40.592184300 -0400 +@@ -146,7 +146,7 @@ + set(ASM_LANGUAGE ASM) + endif() + elseif(BOOST_CONTEXT_ASSEMBLER STREQUAL armasm) +- set(ASM_LANGUAGE ASM_ARMASM) ++ set(ASM_LANGUAGE ASM_MARMASM) + else() + set(ASM_LANGUAGE ASM_MASM) + endif() diff --git a/.patch/boost/0003-armasm-options.patch b/.patch/boost/0003-armasm-options.patch new file mode 100644 index 0000000000..3869f95f6f --- /dev/null +++ b/.patch/boost/0003-armasm-options.patch @@ -0,0 +1,14 @@ +diff --git a/libs/context/CMakeLists.txt b/libs/context/CMakeLists.txt +index 8210f65..0e59dd7 100644 +--- a/libs/context/CMakeLists.txt ++++ b/libs/context/CMakeLists.txt +@@ -186,7 +186,8 @@ if(BOOST_CONTEXT_IMPLEMENTATION STREQUAL "fcontext") + set_property(SOURCE ${ASM_SOURCES} APPEND PROPERTY COMPILE_OPTIONS "/safeseh") + endif() + +- else() # masm ++ # armasm doesn't support most of these options ++ elseif(NOT BOOST_CONTEXT_ASSEMBLER STREQUAL armasm) # masm + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set_property(SOURCE ${ASM_SOURCES} APPEND PROPERTY COMPILE_OPTIONS "-x" "assembler-with-cpp") + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") diff --git a/.patch/libadrenotools/0001-linkerns-cpm.patch b/.patch/libadrenotools/0001-linkerns-cpm.patch new file mode 100644 index 0000000000..8c5abe28e5 --- /dev/null +++ b/.patch/libadrenotools/0001-linkerns-cpm.patch @@ -0,0 +1,20 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 16c6092..9e75548 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -8,7 +8,14 @@ project(adrenotools LANGUAGES CXX C) + + set(GEN_INSTALL_TARGET OFF CACHE BOOL "") + +-add_subdirectory(lib/linkernsbypass) ++include(CPM) ++set(CPM_USE_LOCAL_PACKAGES OFF) ++ ++CPMAddPackage( ++ NAME linkernsbypass ++ URL "https://github.com/bylaws/liblinkernsbypass/archive/aa3975893d.zip" ++ URL_HASH SHA512=43d3d146facb7ec99d066a9b8990369ab7b9eec0d5f9a67131b0a0744fde0af27d884ca1f2a272cd113718a23356530ed97703c8c0659c4c25948d50c106119e ++) + + set(LIB_SOURCES src/bcenabler.cpp + src/driver.cpp diff --git a/.patch/mbedtls/0001-cmake-version.patch b/.patch/mbedtls/0001-cmake-version.patch new file mode 100644 index 0000000000..2b78804884 --- /dev/null +++ b/.patch/mbedtls/0001-cmake-version.patch @@ -0,0 +1,10 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 1811c42..bac9098 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -1,4 +1,4 @@ +-cmake_minimum_required(VERSION 2.6) ++cmake_minimum_required(VERSION 3.5) + if(TEST_CPP) + project("mbed TLS" C CXX) + else() diff --git a/.reuse/dep5 b/.reuse/dep5 index 79ea95781d..9bcddb1afd 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -128,10 +128,6 @@ Copyright: 2020-2021 Its-Rei 2020-2021 yuzu Emulator Project License: GPL-2.0-or-later -Files: vcpkg.json -Copyright: 2022 yuzu Emulator Project -License: GPL-3.0-or-later - Files: .github/ISSUE_TEMPLATE/* Copyright: 2022 yuzu Emulator Project License: GPL-2.0-or-later diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000000..96e22629de --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1 @@ +shell=sh diff --git a/.tx/config b/.tx/config new file mode 100755 index 0000000000..fdca1f30dc --- /dev/null +++ b/.tx/config @@ -0,0 +1,21 @@ +[main] +host = https://app.transifex.com + +[o:edenemu:p:eden-emulator:r:android-translations] +file_filter = src/android/app/src/main/res/values-/strings.xml +source_file = src/android/app/src/main/res/values/strings.xml +type = ANDROID +minimum_perc = 0 +resource_name = Android Translations +replace_edited_strings = false +keep_translations = false +lang_map = zh_CN: zh-rCN, zh_TW: zh-rTW, pt_BR: pt-rBR, pt_PT: pt-rPT, vi_VN: vi, ku: ckb, ja_JP: ja, ko_KR: ko, ru_RU: ru + +[o:edenemu:p:eden-emulator:r:qt-translations] +file_filter = dist/languages/.ts +source_file = dist/languages/en.ts +type = QT +minimum_perc = 0 +resource_name = Qt Translations +replace_edited_strings = false +keep_translations = false diff --git a/CMakeLists.txt b/CMakeLists.txt index 72b03ec2e8..1b69782a23 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,238 +1,64 @@ -# SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project # SPDX-License-Identifier: GPL-3.0-or-later cmake_minimum_required(VERSION 3.22) project(yuzu) +if (${CMAKE_SYSTEM_NAME} STREQUAL "SunOS") + set(PLATFORM_SUN ON) +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + set(PLATFORM_FREEBSD ON) +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + set(PLATFORM_OPENBSD ON) +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + set(PLATFORM_LINUX ON) +endif() + +if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + set(CXX_CLANG ON) + if (MSVC) + set(CXX_CLANG_CL ON) + endif() +elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(CXX_GCC ON) +elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + set(CXX_CL ON) +elseif (CMAKE_CXX_COMPILER_ID STREQUAL "IntelLLVM") + set(CXX_ICC ON) +elseif (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + set(CXX_APPLE ON) +endif() + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/cmake-modules") -include(DownloadExternals) -include(CMakeDependentOption) -include(CTest) +# NB: this does not account for SPARC +# If you get Eden working on SPARC, please shoot crueter@crueter.xyz multiple emails +# and you will be hailed for eternity +if (PLATFORM_SUN) + # Terrific Solaris pkg shenanigans + list(APPEND CMAKE_PREFIX_PATH "/usr/lib/qt/6.6/lib/amd64/cmake") + list(APPEND CMAKE_MODULE_PATH "/usr/lib/qt/6.6/lib/amd64/cmake") -# Disable Warnings as Errors for MSVC -if (MSVC) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3 /WX-") -endif() + # amazing + # absolutely incredible + list(APPEND CMAKE_PREFIX_PATH "/usr/lib/amd64/cmake") + list(APPEND CMAKE_MODULE_PATH "/usr/lib/amd64/cmake") -# Check if SDL2::SDL2 target exists; if not, create an alias -if (TARGET SDL2::SDL2-static) - add_library(SDL2::SDL2 ALIAS SDL2::SDL2-static) -elseif (TARGET SDL2::SDL2-shared) - add_library(SDL2::SDL2 ALIAS SDL2::SDL2-shared) -endif() - -# Set bundled sdl2/qt as dependent options. -option(ENABLE_SDL2 "Enable the SDL2 frontend" ON) -CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_SDL2 "Download bundled SDL2 binaries" ON "ENABLE_SDL2;MSVC" OFF) -# On Linux system SDL2 is likely to be lacking HIDAPI support which have drawbacks but is needed for SDL motion -CMAKE_DEPENDENT_OPTION(YUZU_USE_EXTERNAL_SDL2 "Compile external SDL2" ON "ENABLE_SDL2;NOT MSVC" OFF) - -cmake_dependent_option(ENABLE_LIBUSB "Enable the use of LibUSB" ON "NOT ANDROID" OFF) - -option(ENABLE_OPENGL "Enable OpenGL" ON) -mark_as_advanced(FORCE ENABLE_OPENGL) -option(ENABLE_QT "Enable the Qt frontend" ON) - -option(ENABLE_QT_TRANSLATION "Enable translations for the Qt frontend" OFF) -CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" "${MSVC}" "ENABLE_QT" OFF) - -option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON) - -option(YUZU_USE_BUNDLED_FFMPEG "Download/Build bundled FFmpeg" "${WIN32}") - -option(YUZU_USE_EXTERNAL_VULKAN_HEADERS "Use Vulkan-Headers from externals" ON) - -option(YUZU_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES "Use Vulkan-Utility-Libraries from externals" ON) - -option(YUZU_USE_QT_MULTIMEDIA "Use QtMultimedia for Camera" OFF) - -option(YUZU_USE_QT_WEB_ENGINE "Use QtWebEngine for web applet implementation" OFF) - -option(ENABLE_CUBEB "Enables the cubeb audio backend" ON) - -option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF) - -option(YUZU_TESTS "Compile tests" "${BUILD_TESTING}") - -option(YUZU_USE_PRECOMPILED_HEADERS "Use precompiled headers" ON) - -option(YUZU_DOWNLOAD_ANDROID_VVL "Download validation layer binary for android" ON) - -CMAKE_DEPENDENT_OPTION(YUZU_ROOM "Compile LDN room server" ON "NOT ANDROID" OFF) - -CMAKE_DEPENDENT_OPTION(YUZU_CRASH_DUMPS "Compile crash dump (Minidump) support" OFF "WIN32 OR LINUX" OFF) - -option(YUZU_USE_BUNDLED_VCPKG "Use vcpkg for yuzu dependencies" "${MSVC}") - -option(YUZU_CHECK_SUBMODULES "Check if submodules are present" ON) - -option(YUZU_ENABLE_LTO "Enable link-time optimization" OFF) - -option(YUZU_DOWNLOAD_TIME_ZONE_DATA "Always download time zone binaries" OFF) - -option(YUZU_ENABLE_PORTABLE "Allow yuzu to enable portable mode if a user folder is found in the CWD" ON) - -CMAKE_DEPENDENT_OPTION(YUZU_USE_FASTER_LD "Check if a faster linker is available" ON "NOT WIN32" OFF) - -CMAKE_DEPENDENT_OPTION(USE_SYSTEM_MOLTENVK "Use the system MoltenVK lib (instead of the bundled one)" OFF "APPLE" OFF) - -set(DEFAULT_ENABLE_OPENSSL ON) -if (ANDROID OR WIN32 OR APPLE) - set(DEFAULT_ENABLE_OPENSSL OFF) -endif() -option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL}) - -if (ANDROID OR WIN32 OR APPLE) - # - Windows defaults to the Schannel backend. - # - macOS defaults to the SecureTransport backend. - # - Android currently has no SSL backend as the NDK doesn't include any SSL - # library; a proper 'native' backend would have to go through Java. - # But you can force builds for those platforms to use OpenSSL if you have - # your own copy of it. - set(DEFAULT_ENABLE_OPENSSL OFF) -endif() -option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL}) - -if (ANDROID AND YUZU_DOWNLOAD_ANDROID_VVL) - set(vvl_version "sdk-1.3.261.1") - set(vvl_zip_file "${CMAKE_BINARY_DIR}/externals/vvl-android.zip") - if (NOT EXISTS "${vvl_zip_file}") - # Download and extract validation layer release to externals directory - set(vvl_base_url "https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download") - file(DOWNLOAD "${vvl_base_url}/${vvl_version}/android-binaries-${vvl_version}-android.zip" - "${vvl_zip_file}" SHOW_PROGRESS) - execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${vvl_zip_file}" - WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals") - endif() - - # Copy the arm64 binary to src/android/app/main/jniLibs - set(vvl_lib_path "${CMAKE_CURRENT_SOURCE_DIR}/src/android/app/src/main/jniLibs/arm64-v8a/") - file(COPY "${CMAKE_BINARY_DIR}/externals/android-binaries-${vvl_version}/arm64-v8a/libVkLayer_khronos_validation.so" - DESTINATION "${vvl_lib_path}") -endif() - -if (ANDROID) - set(CMAKE_SKIP_INSTALL_RULES ON) - set(CMAKE_POLICY_VERSION_MINIMUM 3.5) # Workaround for Oboe -endif() - -if (YUZU_USE_BUNDLED_VCPKG) - if (ANDROID) - set(ENV{ANDROID_NDK_HOME} "${ANDROID_NDK}") - list(APPEND VCPKG_MANIFEST_FEATURES "android") - - if (CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a") - set(VCPKG_TARGET_TRIPLET "arm64-android") - # this is to avoid CMake using the host pkg-config to find the host - # libraries when building for Android targets - set(PKG_CONFIG_EXECUTABLE "aarch64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE) - elseif (CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64") - set(VCPKG_TARGET_TRIPLET "x64-android") - set(PKG_CONFIG_EXECUTABLE "x86_64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE) - else() - message(FATAL_ERROR "Unsupported Android architecture ${CMAKE_ANDROID_ARCH_ABI}") - endif() - endif() - - if (MSVC) - set(VCPKG_DOWNLOADS_PATH ${PROJECT_SOURCE_DIR}/externals/vcpkg/downloads) - set(NASM_VERSION "2.16.01") - set(NASM_DESTINATION_PATH ${VCPKG_DOWNLOADS_PATH}/nasm-${NASM_VERSION}-win64.zip) - set(NASM_DOWNLOAD_URL "https://github.com/eden-emulator/ext-windows-bin/raw/master/nasm/nasm-${NASM_VERSION}-win64.zip") - - if (NOT EXISTS ${NASM_DESTINATION_PATH}) - file(DOWNLOAD ${NASM_DOWNLOAD_URL} ${NASM_DESTINATION_PATH} SHOW_PROGRESS STATUS NASM_STATUS) - - if (NOT NASM_STATUS EQUAL 0) - # Warn and not fail since vcpkg is supposed to download this package for us in the first place - message(WARNING "External nasm vcpkg package download from ${NASM_DOWNLOAD_URL} failed with status ${NASM_STATUS}") - endif() - endif() - endif() - - if (YUZU_TESTS) - list(APPEND VCPKG_MANIFEST_FEATURES "yuzu-tests") - endif() - if (ENABLE_WEB_SERVICE) - list(APPEND VCPKG_MANIFEST_FEATURES "web-service") - endif() - if (ANDROID) - list(APPEND VCPKG_MANIFEST_FEATURES "android") - endif() - - include(${CMAKE_SOURCE_DIR}/externals/vcpkg/scripts/buildsystems/vcpkg.cmake) -elseif(NOT "$ENV{VCPKG_TOOLCHAIN_FILE}" STREQUAL "") - # Disable manifest mode (use vcpkg classic mode) when using a custom vcpkg installation - option(VCPKG_MANIFEST_MODE "") - include("$ENV{VCPKG_TOOLCHAIN_FILE}") -endif() - -if (YUZU_USE_PRECOMPILED_HEADERS) - if (MSVC AND CCACHE) - # buildcache does not properly cache PCH files, leading to compilation errors. - # See https://github.com/mbitsnbites/buildcache/discussions/230 - message(WARNING "buildcache does not properly support Precompiled Headers. Disabling PCH") - set(DYNARMIC_USE_PRECOMPILED_HEADERS OFF CACHE BOOL "" FORCE) - set(YUZU_USE_PRECOMPILED_HEADERS OFF CACHE BOOL "" FORCE) - endif() -endif() -if (YUZU_USE_PRECOMPILED_HEADERS) - message(STATUS "Using Precompiled Headers.") - set(CMAKE_PCH_INSTANTIATE_TEMPLATES ON) -endif() - - -# Default to a Release build -get_property(IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) -if (NOT IS_MULTI_CONFIG AND NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel." FORCE) - message(STATUS "Defaulting to a Release build") -endif() - -if(EXISTS ${PROJECT_SOURCE_DIR}/hooks/pre-commit AND NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/hooks/pre-commit) - if (EXISTS ${PROJECT_SOURCE_DIR}/.git/) - message(STATUS "Copying pre-commit hook") - file(COPY hooks/pre-commit DESTINATION ${PROJECT_SOURCE_DIR}/.git/hooks) + # For some mighty reason, doing a normal release build sometimes may not trigger + # the proper -O3 switch to materialize + if (CMAKE_BUILD_TYPE MATCHES "Release") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3") endif() endif() -# Sanity check : Check that all submodules are present -# ======================================================================= - -function(check_submodules_present) - file(READ "${PROJECT_SOURCE_DIR}/.gitmodules" gitmodules) - string(REGEX MATCHALL "path *= *[^ \t\r\n]*" gitmodules ${gitmodules}) - foreach(module ${gitmodules}) - string(REGEX REPLACE "path *= *" "" module ${module}) - if (NOT EXISTS "${PROJECT_SOURCE_DIR}/${module}/.git") - message(FATAL_ERROR "Git submodule ${module} not found. " - "Please run: \ngit submodule update --init --recursive") - endif() - endforeach() -endfunction() - -if(EXISTS ${PROJECT_SOURCE_DIR}/.gitmodules AND YUZU_CHECK_SUBMODULES) - check_submodules_present() -endif() -configure_file(${PROJECT_SOURCE_DIR}/dist/compatibility_list/compatibility_list.qrc - ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc - COPYONLY) -if (EXISTS ${PROJECT_SOURCE_DIR}/dist/compatibility_list/compatibility_list.json) - configure_file("${PROJECT_SOURCE_DIR}/dist/compatibility_list/compatibility_list.json" - "${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json" - COPYONLY) -endif() -if (ENABLE_COMPATIBILITY_LIST_DOWNLOAD AND NOT EXISTS ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json) - message(STATUS "Downloading compatibility list for yuzu...") - file(DOWNLOAD - https://api.yuzu-emu.org/gamedb/ - "${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json" SHOW_PROGRESS) -endif() -if (NOT EXISTS ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json) - file(WRITE ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json "") +# Needed for FFmpeg w/ VAAPI and DRM +if (PLATFORM_OPENBSD) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I/usr/X11R6/include") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I/usr/X11R6/include") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L/usr/X11R6/lib") endif() # Detect current compilation architecture and create standard definitions @@ -274,17 +100,300 @@ if (NOT DEFINED ARCHITECTURE) set(ARCHITECTURE_GENERIC 1) add_definitions(-DARCHITECTURE_GENERIC=1) endif() + message(STATUS "Target architecture: ${ARCHITECTURE}") -if (UNIX) - add_definitions(-DYUZU_UNIX=1) +if (MSVC AND ARCHITECTURE_x86) + message(FATAL_ERROR "Attempting to build with the x86 environment is not supported. \ + This can typically happen if you used the Developer Command Prompt from the start menu; \ + instead, run vcvars64.bat directly, located at C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Auxiliary/Build/vcvars64.bat") endif() -if (ARCHITECTURE_arm64 AND (ANDROID OR ${CMAKE_SYSTEM_NAME} STREQUAL "Linux")) - set(HAS_NCE 1) - add_definitions(-DHAS_NCE=1) +if (CXX_CLANG_CL) + add_compile_options( + # clang-cl prints literally 10000+ warnings without this + $<$:-Wno-unused-command-line-argument> + $<$:-Wno-unsafe-buffer-usage> + $<$:-Wno-unused-value> + $<$:-Wno-extra-semi-stmt> + $<$:-Wno-sign-conversion> + $<$:-Wno-reserved-identifier> + $<$:-Wno-deprecated-declarations> + $<$:-Wno-cast-function-type-mismatch> + $<$:/EHsc> # thanks microsoft + ) + + if (ARCHITECTURE_x86_64) + add_compile_options( + # Required CPU features for amd64 + $<$:-msse4.1> + $<$:-mcx16> + ) + endif() endif() +set(CPM_SOURCE_CACHE ${CMAKE_SOURCE_DIR}/.cache/cpm) + +include(DownloadExternals) +include(CMakeDependentOption) +include(CTest) + +# Disable Warnings as Errors for MSVC +if (MSVC AND NOT CXX_CLANG) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3 /WX-") +endif() + +if (PLATFORM_FREEBSD) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L/usr/local/lib") +endif() + +# Set bundled sdl2/qt as dependent options. +# On Linux system SDL2 is likely to be lacking HIDAPI support which have drawbacks but is needed for SDL motion +cmake_dependent_option(ENABLE_SDL2 "Enable the SDL2 frontend" ON "NOT ANDROID" OFF) + +if (ENABLE_SDL2) + # TODO(crueter): Cleanup, each dep that has a bundled option should allow to choose between bundled, external, system + cmake_dependent_option(YUZU_USE_EXTERNAL_SDL2 "Compile external SDL2" OFF "NOT MSVC" OFF) + option(YUZU_USE_BUNDLED_SDL2 "Download bundled SDL2 build" "${MSVC}") +endif() + +# qt stuff +option(ENABLE_QT "Enable the Qt frontend" ON) +option(ENABLE_QT_TRANSLATION "Enable translations for the Qt frontend" OFF) +option(ENABLE_QT_UPDATE_CHECKER "Enable update checker for the Qt frontend" OFF) +cmake_dependent_option(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" "${MSVC}" "ENABLE_QT" OFF) +option(YUZU_USE_QT_MULTIMEDIA "Use QtMultimedia for Camera" OFF) +option(YUZU_USE_QT_WEB_ENGINE "Use QtWebEngine for web applet implementation" OFF) +set(YUZU_QT_MIRROR "" CACHE STRING "What mirror to use for downloading the bundled Qt libraries") + +option(ENABLE_CUBEB "Enables the cubeb audio backend" ON) + +set(EXT_DEFAULT OFF) +if (MSVC OR ANDROID) + set(EXT_DEFAULT ON) +endif() +option(YUZU_USE_CPM "Use CPM to fetch system dependencies (fmt, boost, etc) if needed. Externals will still be fetched." ${EXT_DEFAULT}) + +# ffmpeg +option(YUZU_USE_BUNDLED_FFMPEG "Download bundled FFmpeg" ${EXT_DEFAULT}) +cmake_dependent_option(YUZU_USE_EXTERNAL_FFMPEG "Build FFmpeg from source" "${PLATFORM_SUN}" "NOT WIN32 AND NOT ANDROID" OFF) + +# sirit +option(YUZU_USE_BUNDLED_SIRIT "Download bundled sirit" ${EXT_DEFAULT}) + +cmake_dependent_option(ENABLE_LIBUSB "Enable the use of LibUSB" ON "NOT ANDROID" OFF) + +cmake_dependent_option(ENABLE_OPENGL "Enable OpenGL" ON "NOT WIN32 OR NOT ARCHITECTURE_arm64" OFF) +mark_as_advanced(FORCE ENABLE_OPENGL) + +option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON) +option(ENABLE_WIFI_SCAN "Enable WiFi scanning" OFF) + +cmake_dependent_option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF "ENABLE_QT" OFF) + +option(YUZU_TESTS "Compile tests" "${BUILD_TESTING}") + +option(YUZU_USE_PRECOMPILED_HEADERS "Use precompiled headers" OFF) +if (YUZU_USE_PRECOMPILED_HEADERS) + message(STATUS "Using Precompiled Headers.") + set(CMAKE_PCH_INSTANTIATE_TEMPLATES ON) +endif() +option(YUZU_ENABLE_LTO "Enable link-time optimization" OFF) +if(YUZU_ENABLE_LTO) + include(CheckIPOSupported) + check_ipo_supported(RESULT COMPILER_SUPPORTS_LTO) + if(NOT COMPILER_SUPPORTS_LTO) + message(FATAL_ERROR "Your compiler does not support interprocedural optimization (IPO). Re-run CMake with -DYUZU_ENABLE_LTO=OFF.") + endif() + set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ${COMPILER_SUPPORTS_LTO}) +endif() +option(USE_CCACHE "Use ccache for compilation" OFF) +set(CCACHE_PATH "ccache" CACHE STRING "Path to ccache binary") +if(USE_CCACHE) + find_program(CCACHE_BINARY ${CCACHE_PATH}) + if(CCACHE_BINARY) + message(STATUS "Found ccache at: ${CCACHE_BINARY}") + set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_BINARY}) + set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_BINARY}) + if (YUZU_USE_PRECOMPILED_HEADERS) + message(FATAL_ERROR "Precompiled headers are incompatible with ccache. Re-run CMake with -DYUZU_USE_PRECOMPILED_HEADERS=OFF.") + endif() + else() + message(WARNING "USE_CCACHE enabled, but no executable found at: ${CCACHE_PATH}") + endif() +endif() + +# TODO(crueter): CI this? +option(YUZU_DOWNLOAD_ANDROID_VVL "Download validation layer binary for android" ON) + +option(YUZU_LEGACY "Apply patches that improve compatibility with older GPUs (e.g. Snapdragon 865) at the cost of performance" OFF) + +cmake_dependent_option(YUZU_ROOM "Enable dedicated room functionality" ON "NOT ANDROID" OFF) +cmake_dependent_option(YUZU_ROOM_STANDALONE "Enable standalone room executable" ON "YUZU_ROOM" OFF) + +cmake_dependent_option(YUZU_CMD "Compile the eden-cli executable" ON "ENABLE_SDL2;NOT ANDROID" OFF) + +cmake_dependent_option(YUZU_CRASH_DUMPS "Compile crash dump (Minidump) support" OFF "WIN32 OR LINUX" OFF) + +option(YUZU_DOWNLOAD_TIME_ZONE_DATA "Always download time zone binaries" ON) +set(YUZU_TZDB_PATH "" CACHE STRING "Path to a pre-downloaded timezone database") + +cmake_dependent_option(YUZU_USE_FASTER_LD "Check if a faster linker is available" ON "LINUX" OFF) + +cmake_dependent_option(YUZU_USE_BUNDLED_MOLTENVK "Download bundled MoltenVK lib" ON "APPLE" OFF) + +option(YUZU_DISABLE_LLVM "Disable LLVM (useful for CI)" OFF) + +set(DEFAULT_ENABLE_OPENSSL ON) +if (ANDROID OR WIN32 OR APPLE OR PLATFORM_SUN) + # - Windows defaults to the Schannel backend. + # - macOS defaults to the SecureTransport backend. + # - Android currently has no SSL backend as the NDK doesn't include any SSL + # library; a proper 'native' backend would have to go through Java. + # But you can force builds for those platforms to use OpenSSL if you have + # your own copy of it. + set(DEFAULT_ENABLE_OPENSSL OFF) +endif() +if (ENABLE_WEB_SERVICE) + set(DEFAULT_ENABLE_OPENSSL ON) +endif() +option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL}) +if (ENABLE_OPENSSL) + cmake_dependent_option(YUZU_USE_BUNDLED_OPENSSL "Download bundled OpenSSL build" "${MSVC}" "NOT ANDROID" ON) +endif() + +if (ANDROID AND YUZU_DOWNLOAD_ANDROID_VVL) + # TODO(crueter): CPM this + set(vvl_version "1.4.321.0") + set(vvl_zip_file "${CMAKE_BINARY_DIR}/externals/vvl-android.zip") + if (NOT EXISTS "${vvl_zip_file}") + # Download and extract validation layer release to externals directory + set(vvl_base_url "https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download") + file(DOWNLOAD "${vvl_base_url}/vulkan-sdk-${vvl_version}/android-binaries-${vvl_version}.zip" + "${vvl_zip_file}" SHOW_PROGRESS) + execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${vvl_zip_file}" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals") + endif() + + # Copy the arm64 binary to src/android/app/main/jniLibs + set(vvl_lib_path "${CMAKE_CURRENT_SOURCE_DIR}/src/android/app/src/main/jniLibs/arm64-v8a/") + file(COPY "${CMAKE_BINARY_DIR}/externals/android-binaries-${vvl_version}/arm64-v8a/libVkLayer_khronos_validation.so" + DESTINATION "${vvl_lib_path}") +endif() + +if (ANDROID) + set(CMAKE_SKIP_INSTALL_RULES ON) + set(CMAKE_POLICY_VERSION_MINIMUM 3.5) # Workaround for Oboe +endif() + +# We need to downgrade debug info (/Zi -> /Z7) to use an older but more cacheable format +# See https://github.com/nanoant/CMakePCHCompiler/issues/21 +if(WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) + string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") + string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") +endif() + +# Default to a Release build +get_property(IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if (NOT IS_MULTI_CONFIG AND NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel." FORCE) + message(STATUS "Defaulting to a Release build") +endif() + +if(EXISTS ${PROJECT_SOURCE_DIR}/hooks/pre-commit AND NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/hooks/pre-commit) + if (EXISTS ${PROJECT_SOURCE_DIR}/.git/) + message(STATUS "Copying pre-commit hook") + file(COPY hooks/pre-commit DESTINATION ${PROJECT_SOURCE_DIR}/.git/hooks) + endif() +endif() + +configure_file(${PROJECT_SOURCE_DIR}/dist/compatibility_list/compatibility_list.qrc + ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc + COPYONLY) + +if (EXISTS ${PROJECT_SOURCE_DIR}/dist/compatibility_list/compatibility_list.json) + configure_file("${PROJECT_SOURCE_DIR}/dist/compatibility_list/compatibility_list.json" + "${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json" + COPYONLY) +endif() + +if (ENABLE_COMPATIBILITY_LIST_DOWNLOAD AND NOT EXISTS ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json) + message(STATUS "Downloading compatibility list for yuzu...") + file(DOWNLOAD + https://api.yuzu-emu.org/gamedb/ + "${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json" SHOW_PROGRESS) +endif() + +if (NOT EXISTS ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json) + file(WRITE ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json "") +endif() + +if (UNIX) + add_compile_definitions(YUZU_UNIX=1) +endif() + +if (YUZU_LEGACY) + message(WARNING "Making legacy build. Performance may suffer.") + add_compile_definitions(YUZU_LEGACY) +endif() + +if (ARCHITECTURE_arm64 AND (ANDROID OR PLATFORM_LINUX)) + set(HAS_NCE 1) + add_compile_definitions(HAS_NCE=1) +endif() + +if (YUZU_ROOM) + add_compile_definitions(YUZU_ROOM) +endif() + +if (ANDROID OR PLATFORM_FREEBSD OR PLATFORM_OPENBSD OR PLATFORM_SUN OR APPLE) + if(CXX_APPLE OR CXX_CLANG) + # libc++ has stop_token and jthread as experimental + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexperimental-library") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fexperimental-library") + else() + # Uses glibc, mostly? + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_LIBCPP_ENABLE_EXPERIMENTAL=1") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_LIBCPP_ENABLE_EXPERIMENTAL=1") + endif() +endif() + +# Build/optimization presets +if (PLATFORM_LINUX OR CXX_CLANG) + if (ARCHITECTURE_x86_64) + set(YUZU_BUILD_PRESET "custom" CACHE STRING "Build preset to use. One of: custom, generic, v3, zen2, zen4, native") + if (${YUZU_BUILD_PRESET} STREQUAL "generic") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=x86-64 -mtune=generic") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=x86-64 -mtune=generic") + elseif (${YUZU_BUILD_PRESET} STREQUAL "v3") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=x86-64-v3 -mtune=generic") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=x86-64-v3 -mtune=generic") + elseif (${YUZU_BUILD_PRESET} STREQUAL "zen2") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=znver2 -mtune=znver2") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=znver2 -mtune=znver2") + elseif (${YUZU_BUILD_PRESET} STREQUAL "zen4") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=znver4 -mtune=znver4") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=znver4 -mtune=znver4") + elseif (${YUZU_BUILD_PRESET} STREQUAL "native") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native -mtune=native") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=native -mtune=native") + endif() + elseif(ARCHITECTURE_arm64) + set(YUZU_BUILD_PRESET "custom" CACHE STRING "Build preset to use. One of: custom, generic, armv9") + if (${YUZU_BUILD_PRESET} STREQUAL "generic") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=armv8-a -mtune=generic") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=armv8-a -mtune=generic") + elseif (${YUZU_BUILD_PRESET} STREQUAL "armv9") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=armv9-a -mtune=generic") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=armv9-a -mtune=generic") + endif() + endif() +endif() + +# Other presets, e.g. steamdeck +set(YUZU_SYSTEM_PROFILE "generic" CACHE STRING "CMake and Externals profile to use. One of: generic, steamdeck") + # Configure C++ standard # =========================== @@ -297,115 +406,224 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin) # System imported libraries # ======================================================================= -# Enforce the search mode of non-required packages for better and shorter failure messages -find_package(enet 1.3 MODULE) -find_package(fmt 10 REQUIRED) -find_package(LLVM 17.0.2 MODULE COMPONENTS Demangle) -find_package(lz4 REQUIRED) -find_package(nlohmann_json 3.8 REQUIRED) -find_package(Opus 1.3 MODULE) -find_package(RenderDoc MODULE) -find_package(SimpleIni MODULE) -find_package(stb MODULE) -find_package(VulkanMemoryAllocator CONFIG) -find_package(ZLIB 1.2 REQUIRED) -find_package(zstd 1.5 REQUIRED) +include(CPMUtil) -if (NOT YUZU_USE_EXTERNAL_VULKAN_HEADERS) - find_package(VulkanHeaders 1.3.274 REQUIRED) -endif() +# openssl funniness +if (ENABLE_OPENSSL) + if (YUZU_USE_BUNDLED_OPENSSL) + AddJsonPackage(openssl) + endif() -if (NOT YUZU_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES) - find_package(VulkanUtilityLibraries REQUIRED) -endif() - -if (ENABLE_LIBUSB) - find_package(libusb 1.0.24 MODULE) -endif() - -if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64) - find_package(xbyak 7 CONFIG) -endif() - -if (ARCHITECTURE_arm64) - find_package(oaknut 2.0.1 CONFIG) -endif() - -if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64) - find_package(dynarmic 6.4.0 CONFIG) -endif() - -if (ENABLE_CUBEB) - find_package(cubeb CONFIG) -endif() - -if (USE_DISCORD_PRESENCE) - find_package(DiscordRPC MODULE) -endif() - -if (ENABLE_WEB_SERVICE) - find_package(cpp-jwt 1.4 CONFIG) - find_package(httplib 0.12 MODULE COMPONENTS OpenSSL) -endif() - -if (YUZU_TESTS) - find_package(Catch2 3.0.1 REQUIRED) -endif() - -if(ENABLE_OPENSSL) find_package(OpenSSL 1.1.1 REQUIRED) endif() -if (UNIX AND NOT APPLE) - find_package(gamemode 1.7 MODULE) -endif() +if (YUZU_USE_CPM) + message(STATUS "Fetching needed dependencies with CPM") -# Please consider this as a stub -if(ENABLE_QT6 AND Qt6_LOCATION) - list(APPEND CMAKE_PREFIX_PATH "${Qt6_LOCATION}") -endif() + set(BUILD_SHARED_LIBS OFF) + set(BUILD_TESTING OFF) + set(ENABLE_TESTING OFF) -# find SDL2 exports a bunch of variables that are needed, so its easier to do this outside of the YUZU_find_package -if (ENABLE_SDL2) - if (YUZU_USE_BUNDLED_SDL2) - # Detect toolchain and platform - if ((MSVC_VERSION GREATER_EQUAL 1920) AND ARCHITECTURE_x86_64) - set(SDL2_VER "SDL2-2.28.2") - else() - message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable YUZU_USE_BUNDLED_SDL2 and provide your own.") + # TODO(crueter): renderdoc? + + # boost + set(BOOST_INCLUDE_LIBRARIES algorithm icl pool container heap asio headers process filesystem crc variant) + + AddJsonPackage(boost) + + # really annoying thing where boost::headers doesn't work with cpm + # TODO(crueter) investigate + set(BOOST_NO_HEADERS ${Boost_ADDED}) + + if (Boost_ADDED) + if (MSVC OR ANDROID) + add_compile_definitions(YUZU_BOOST_v1) endif() - if (DEFINED SDL2_VER) - download_bundled_external("sdl2/" ${SDL2_VER} SDL2_PREFIX) + if (NOT MSVC OR CXX_CLANG) + # boost sucks + if (PLATFORM_SUN) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthreads") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthreads") + endif() + + target_compile_options(boost_heap INTERFACE -Wno-shadow) + target_compile_options(boost_icl INTERFACE -Wno-shadow) + target_compile_options(boost_asio INTERFACE -Wno-conversion -Wno-implicit-fallthrough) endif() + endif() - set(SDL2_FOUND YES) - set(SDL2_INCLUDE_DIR "${SDL2_PREFIX}/include" CACHE PATH "Path to SDL2 headers") - set(SDL2_LIBRARY "${SDL2_PREFIX}/lib/x64/SDL2.lib" CACHE PATH "Path to SDL2 library") - set(SDL2_DLL_DIR "${SDL2_PREFIX}/lib/x64/" CACHE PATH "Path to SDL2.dll") + # fmt + AddJsonPackage(fmt) - add_library(SDL2::SDL2 INTERFACE IMPORTED) - target_link_libraries(SDL2::SDL2 INTERFACE "${SDL2_LIBRARY}") - target_include_directories(SDL2::SDL2 INTERFACE "${SDL2_INCLUDE_DIR}") - elseif (YUZU_USE_EXTERNAL_SDL2) - message(STATUS "Using SDL2 from externals.") + # lz4 + AddJsonPackage(lz4) + + if (lz4_ADDED) + add_library(lz4::lz4 ALIAS lz4_static) + endif() + + # nlohmann + AddJsonPackage(nlohmann) + + # zlib + AddJsonPackage(zlib) + + if (ZLIB_ADDED) + add_library(ZLIB::ZLIB ALIAS zlibstatic) + endif() + + # zstd + AddJsonPackage(zstd) + + if (zstd_ADDED) + add_library(zstd::zstd ALIAS libzstd_static) + add_library(zstd::libzstd ALIAS libzstd_static) + endif() + + # Opus + AddJsonPackage(opus) + + if (Opus_ADDED) + if (MSVC AND CXX_CLANG) + target_compile_options(opus PRIVATE + -Wno-implicit-function-declaration + ) + endif() + endif() + + if (NOT TARGET Opus::opus) + add_library(Opus::opus ALIAS opus) + endif() +else() + # Enforce the search mode of non-required packages for better and shorter failure messages + find_package(fmt 8 REQUIRED) + + if (NOT YUZU_DISABLE_LLVM) + find_package(LLVM MODULE COMPONENTS Demangle) + endif() + + find_package(nlohmann_json 3.8 REQUIRED) + find_package(lz4 REQUIRED) + find_package(RenderDoc MODULE) + find_package(stb MODULE) + + find_package(Opus 1.3 MODULE REQUIRED) + find_package(ZLIB 1.2 REQUIRED) + find_package(zstd 1.5 REQUIRED MODULE) + + # wow + if (PLATFORM_LINUX) + find_package(Boost 1.57.0 CONFIG REQUIRED headers context system fiber) else() - find_package(SDL2 2.26.4 REQUIRED) + find_package(Boost 1.57.0 CONFIG REQUIRED) + endif() + + if (CMAKE_SYSTEM_NAME STREQUAL "Linux" OR ANDROID) + find_package(gamemode 1.7 MODULE) + endif() + + if (ENABLE_OPENSSL) + find_package(OpenSSL 1.1.1 REQUIRED) endif() endif() +if(NOT TARGET Boost::headers) + AddJsonPackage(boost_headers) +endif() + +# List of all FFmpeg components required +set(FFmpeg_COMPONENTS + avcodec + avfilter + avutil + swscale) + +# This function should be passed a list of all files in a target. It will automatically generate +# file groups following the directory hierarchy, so that the layout of the files in IDEs matches the +# one in the filesystem. +function(create_target_directory_groups target_name) + # Place any files that aren't in the source list in a separate group so that they don't get in + # the way. + source_group("Other Files" REGULAR_EXPRESSION ".") + + get_target_property(target_sources "${target_name}" SOURCES) + + foreach(file_name IN LISTS target_sources) + get_filename_component(dir_name "${file_name}" PATH) + # Group names use '\' as a separator even though the entire rest of CMake uses '/'... + string(REPLACE "/" "\\" group_name "${dir_name}") + source_group("${group_name}" FILES "${file_name}") + endforeach() +endfunction() + add_subdirectory(externals) +# pass targets from externals +find_package(libusb) +find_package(VulkanMemoryAllocator) +find_package(enet) +find_package(MbedTLS) +find_package(VulkanUtilityLibraries) +find_package(SimpleIni) +find_package(SPIRV-Tools) +find_package(sirit) + +if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64) + find_package(xbyak) +endif() + +if (ENABLE_WEB_SERVICE) + find_package(httplib) +endif() + +if (ENABLE_WEB_SERVICE OR ENABLE_QT_UPDATE_CHECKER) + find_package(cpp-jwt) +endif() + +if (ARCHITECTURE_arm64 OR DYNARMIC_TESTS) + find_package(oaknut) +endif() + +if (ENABLE_SDL2) + find_package(SDL2) +endif() + +if (USE_DISCORD_PRESENCE) + find_package(DiscordRPC) +endif() + +if (ENABLE_CUBEB) + find_package(cubeb) +endif() + +if (YUZU_TESTS OR DYNARMIC_TESTS) + find_package(Catch2) +endif() if (ENABLE_QT) - if (NOT USE_SYSTEM_QT) - download_qt(6.7.3) + if (YUZU_USE_BUNDLED_QT) + download_qt(6.8.3) + else() + message(STATUS "Using system Qt") + if (NOT Qt6_DIR) + set(Qt6_DIR "" CACHE PATH "Additional path to search for Qt6 libraries like C:/Qt/6.8.3/msvc2022_64/lib/cmake/Qt6") + endif() + list(APPEND CMAKE_PREFIX_PATH "${Qt6_DIR}") endif() - find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia Concurrent) + find_package(Qt6 REQUIRED COMPONENTS Widgets Concurrent) - if (UNIX AND NOT APPLE) - find_package(Qt6 REQUIRED COMPONENTS DBus) + if (YUZU_USE_QT_MULTIMEDIA) + find_package(Qt6 REQUIRED COMPONENTS Multimedia) + endif() + + if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + # yes Qt, we get it + set(QT_NO_PRIVATE_MODULE_WARNING ON) + find_package(Qt6 REQUIRED COMPONENTS DBus OPTIONAL_COMPONENTS GuiPrivate) + elseif (UNIX AND NOT APPLE) + find_package(Qt6 REQUIRED COMPONENTS DBus Gui) endif() if (ENABLE_QT_TRANSLATION) @@ -426,6 +644,7 @@ if (ENABLE_QT) if (NOT DEFINED QT_HOST_PATH) set(QT_HOST_PATH "${QT_TARGET_PATH}") endif() + message(STATUS "Using target Qt at ${QT_TARGET_PATH}") message(STATUS "Using host Qt at ${QT_HOST_PATH}") endif() @@ -433,7 +652,7 @@ endif() function(set_yuzu_qt_components) # Best practice is to ask for all components at once, so they are from the same version set(YUZU_QT_COMPONENTS2 Core Widgets Concurrent) - if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + if (PLATFORM_LINUX) list(APPEND YUZU_QT_COMPONENTS2 DBus) endif() if (YUZU_USE_QT_MULTIMEDIA) @@ -451,20 +670,19 @@ function(set_yuzu_qt_components) set(YUZU_QT_COMPONENTS ${YUZU_QT_COMPONENTS2} PARENT_SCOPE) endfunction(set_yuzu_qt_components) -# List of all FFmpeg components required -set(FFmpeg_COMPONENTS - avcodec - avfilter - avutil - swscale) - if (UNIX AND NOT APPLE AND NOT ANDROID) find_package(PkgConfig REQUIRED) pkg_check_modules(LIBVA libva) endif() -if (NOT YUZU_USE_BUNDLED_FFMPEG) + +if (NOT (YUZU_USE_BUNDLED_FFMPEG OR YUZU_USE_EXTERNAL_FFMPEG)) # Use system installed FFmpeg - find_package(FFmpeg 4.3 REQUIRED QUIET COMPONENTS ${FFmpeg_COMPONENTS}) + find_package(FFmpeg REQUIRED QUIET COMPONENTS ${FFmpeg_COMPONENTS}) + + # TODO(crueter): Version + set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_NAMES FFmpeg) + set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_SHAS "unknown (system)") + set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_URLS "https://github.com/FFmpeg/FFmpeg") endif() if(ENABLE_QT) @@ -477,7 +695,7 @@ endif() if (WIN32 AND YUZU_CRASH_DUMPS) set(BREAKPAD_VER "breakpad-c89f9dd") - download_bundled_external("breakpad/" ${BREAKPAD_VER} BREAKPAD_PREFIX) + download_bundled_external("breakpad/" ${BREAKPAD_VER} "breakpad-win" BREAKPAD_PREFIX "c89f9dd") set(BREAKPAD_CLIENT_INCLUDE_DIR "${BREAKPAD_PREFIX}/include") set(BREAKPAD_CLIENT_LIBRARY "${BREAKPAD_PREFIX}/lib/libbreakpad_client.lib") @@ -498,9 +716,11 @@ if (APPLE) # Umbrella framework for everything GUI-related find_library(COCOA_LIBRARY Cocoa) set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${IOKIT_LIBRARY} ${COREVIDEO_LIBRARY}) + find_library(ICONV_LIBRARY iconv REQUIRED) + list(APPEND PLATFORM_LIBRARIES ${ICONV_LIBRARY}) elseif (WIN32) # Target Windows 10 - add_definitions(-D_WIN32_WINNT=0x0A00 -DWINVER=0x0A00) + add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00) set(PLATFORM_LIBRARIES winmm ws2_32 iphlpapi) if (MINGW) # PSAPI is the Process Status API @@ -561,24 +781,6 @@ endif() # Include source code # =================== -# This function should be passed a list of all files in a target. It will automatically generate -# file groups following the directory hierarchy, so that the layout of the files in IDEs matches the -# one in the filesystem. -function(create_target_directory_groups target_name) - # Place any files that aren't in the source list in a separate group so that they don't get in - # the way. - source_group("Other Files" REGULAR_EXPRESSION ".") - - get_target_property(target_sources "${target_name}" SOURCES) - - foreach(file_name IN LISTS target_sources) - get_filename_component(dir_name "${file_name}" PATH) - # Group names use '\' as a separator even though the entire rest of CMake uses '/'... - string(REPLACE "/" "\\" group_name "${dir_name}") - source_group("${group_name}" FILES "${file_name}") - endforeach() -endfunction() - # Adjustments for MSVC + Ninja if (MSVC AND CMAKE_GENERATOR STREQUAL "Ninja") add_compile_options( @@ -588,18 +790,67 @@ if (MSVC AND CMAKE_GENERATOR STREQUAL "Ninja") ) endif() -if (YUZU_USE_FASTER_LD AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - # We will assume that if the compiler is GCC, it will attempt to use ld.bfd by default. - # Try to pick a faster linker. - find_program(LLD lld) - find_program(MOLD mold) +# Adjustments for clang-cl +if (MSVC AND CXX_CLANG) + if (ARCHITECTURE_x86_64) + set(FILE_ARCH x86_64) + elseif (ARCHITECTURE_arm64) + set(FILE_ARCH aarch64) + else() + message(FATAL_ERROR "clang-cl: Unsupported architecture ${ARCHITECTURE}") + endif() - if (MOLD AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "12.1") - message(NOTICE "Selecting mold as linker") - add_link_options("-fuse-ld=mold") - elseif (LLD) - message(NOTICE "Selecting lld as linker") - add_link_options("-fuse-ld=lld") + AddJsonPackage(llvm-mingw) + set(LIB_PATH "${llvm-mingw_SOURCE_DIR}/libclang_rt.builtins-${FILE_ARCH}.a") + + add_library(llvm-mingw-runtime STATIC IMPORTED) + set_target_properties(llvm-mingw-runtime PROPERTIES + IMPORTED_LOCATION "${LIB_PATH}" + ) + + link_libraries(llvm-mingw-runtime) +endif() + +#[[ + search order: + - gold (GCC only) - the best, generally, but unfortunately not packaged anymore + - mold (GCC only) - generally does well on GCC + - ldd - preferred on clang + - bfd - the final fallback + - If none are found (macOS uses ld.prime, etc) just use the default linker +]] +if (YUZU_USE_FASTER_LD) + find_program(LINKER_BFD bfd) + if (LINKER_BFD) + set(LINKER bfd) + endif() + + find_program(LINKER_LLD lld) + if (LINKER_LLD) + set(LINKER lld) + endif() + + if (CXX_GCC) + find_program(LINKER_MOLD mold) + if (LINKER_MOLD AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "12.1") + set(LINKER mold) + endif() + + find_program(LINKER_GOLD gold) + if (LINKER_GOLD) + set(LINKER gold) + endif() + endif() + + if (LINKER) + message(NOTICE "Selecting ${LINKER} as linker") + add_link_options("-fuse-ld=${LINKER}") + else() + message(WARNING "No faster linker found--using default") + endif() + + if (LINKER STREQUAL "lld" AND CXX_GCC) + message(WARNING "Using lld on GCC may cause issues with certain LTO settings. If the program fails to compile, disable YUZU_USE_FASTER_LD, or install mold or GNU gold.") endif() endif() @@ -631,7 +882,6 @@ else() set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT yuzu-cmd) endif() - # Installation instructions # ========================= @@ -641,13 +891,14 @@ endif() # https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html # https://www.freedesktop.org/software/appstream/docs/ if(ENABLE_QT AND UNIX AND NOT APPLE) - install(FILES "dist/org.yuzu_emu.yuzu.desktop" + install(FILES "dist/dev.eden_emu.eden.desktop" DESTINATION "share/applications") - install(FILES "dist/eden.svg" - DESTINATION "share/icons/hicolor/scalable/apps" - RENAME "org.yuzu_emu.eden.svg") - install(FILES "dist/org.yuzu_emu.yuzu.xml" + install(FILES "dist/dev.eden_emu.eden.svg" + DESTINATION "share/icons/hicolor/scalable/apps") + + # TODO: these files need to be updated. + install(FILES "dist/dev.eden_emu.eden.xml" DESTINATION "share/mime/packages") - install(FILES "dist/org.yuzu_emu.yuzu.metainfo.xml" + install(FILES "dist/dev.eden_emu.eden.metainfo.xml" DESTINATION "share/metainfo") endif() diff --git a/CMakeModules/CPM.cmake b/CMakeModules/CPM.cmake new file mode 100644 index 0000000000..5544d8eefe --- /dev/null +++ b/CMakeModules/CPM.cmake @@ -0,0 +1,1365 @@ +# CPM.cmake - CMake's missing package manager +# =========================================== +# See https://github.com/cpm-cmake/CPM.cmake for usage and update instructions. +# +# MIT License +# ----------- +#[[ + Copyright (c) 2019-2023 Lars Melchior and contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +]] + +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +# Initialize logging prefix +if(NOT CPM_INDENT) + set(CPM_INDENT + "CPM:" + CACHE INTERNAL "" + ) +endif() + +if(NOT COMMAND cpm_message) + function(cpm_message) + message(${ARGV}) + endfunction() +endif() + +if(DEFINED EXTRACTED_CPM_VERSION) + set(CURRENT_CPM_VERSION "${EXTRACTED_CPM_VERSION}${CPM_DEVELOPMENT}") +else() + set(CURRENT_CPM_VERSION 0.42.0) +endif() + +get_filename_component(CPM_CURRENT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" REALPATH) +if(CPM_DIRECTORY) + if(NOT CPM_DIRECTORY STREQUAL CPM_CURRENT_DIRECTORY) + if(CPM_VERSION VERSION_LESS CURRENT_CPM_VERSION) + message( + AUTHOR_WARNING + "${CPM_INDENT} \ +A dependency is using a more recent CPM version (${CURRENT_CPM_VERSION}) than the current project (${CPM_VERSION}). \ +It is recommended to upgrade CPM to the most recent version. \ +See https://github.com/cpm-cmake/CPM.cmake for more information." + ) + endif() + if(${CMAKE_VERSION} VERSION_LESS "3.17.0") + include(FetchContent) + endif() + return() + endif() + + get_property( + CPM_INITIALIZED GLOBAL "" + PROPERTY CPM_INITIALIZED + SET + ) + if(CPM_INITIALIZED) + return() + endif() +endif() + +if(CURRENT_CPM_VERSION MATCHES "development-version") + message( + WARNING "${CPM_INDENT} Your project is using an unstable development version of CPM.cmake. \ +Please update to a recent release if possible. \ +See https://github.com/cpm-cmake/CPM.cmake for details." + ) +endif() + +set_property(GLOBAL PROPERTY CPM_INITIALIZED true) + +macro(cpm_set_policies) + # the policy allows us to change options without caching + cmake_policy(SET CMP0077 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + + # the policy allows us to change set(CACHE) without caching + if(POLICY CMP0126) + cmake_policy(SET CMP0126 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0126 NEW) + endif() + + # The policy uses the download time for timestamp, instead of the timestamp in the archive. This + # allows for proper rebuilds when a projects url changes + if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0135 NEW) + endif() + + # treat relative git repository paths as being relative to the parent project's remote + if(POLICY CMP0150) + cmake_policy(SET CMP0150 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0150 NEW) + endif() +endmacro() +cpm_set_policies() + +option(CPM_USE_LOCAL_PACKAGES "Always try to use `find_package` to get dependencies" + $ENV{CPM_USE_LOCAL_PACKAGES} +) +option(CPM_LOCAL_PACKAGES_ONLY "Only use `find_package` to get dependencies" + $ENV{CPM_LOCAL_PACKAGES_ONLY} +) +option(CPM_DOWNLOAD_ALL "Always download dependencies from source" $ENV{CPM_DOWNLOAD_ALL}) +option(CPM_DONT_UPDATE_MODULE_PATH "Don't update the module path to allow using find_package" + $ENV{CPM_DONT_UPDATE_MODULE_PATH} +) +option(CPM_DONT_CREATE_PACKAGE_LOCK "Don't create a package lock file in the binary path" + $ENV{CPM_DONT_CREATE_PACKAGE_LOCK} +) +option(CPM_INCLUDE_ALL_IN_PACKAGE_LOCK + "Add all packages added through CPM.cmake to the package lock" + $ENV{CPM_INCLUDE_ALL_IN_PACKAGE_LOCK} +) +option(CPM_USE_NAMED_CACHE_DIRECTORIES + "Use additional directory of package name in cache on the most nested level." + $ENV{CPM_USE_NAMED_CACHE_DIRECTORIES} +) + +set(CPM_VERSION + ${CURRENT_CPM_VERSION} + CACHE INTERNAL "" +) +set(CPM_DIRECTORY + ${CPM_CURRENT_DIRECTORY} + CACHE INTERNAL "" +) +set(CPM_FILE + ${CMAKE_CURRENT_LIST_FILE} + CACHE INTERNAL "" +) +set(CPM_PACKAGES + "" + CACHE INTERNAL "" +) +set(CPM_DRY_RUN + OFF + CACHE INTERNAL "Don't download or configure dependencies (for testing)" +) + +if(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_SOURCE_CACHE_DEFAULT $ENV{CPM_SOURCE_CACHE}) +else() + set(CPM_SOURCE_CACHE_DEFAULT OFF) +endif() + +set(CPM_SOURCE_CACHE + ${CPM_SOURCE_CACHE_DEFAULT} + CACHE PATH "Directory to download CPM dependencies" +) + +if(NOT CPM_DONT_UPDATE_MODULE_PATH AND NOT DEFINED CMAKE_FIND_PACKAGE_REDIRECTS_DIR) + set(CPM_MODULE_PATH + "${CMAKE_BINARY_DIR}/CPM_modules" + CACHE INTERNAL "" + ) + # remove old modules + file(REMOVE_RECURSE ${CPM_MODULE_PATH}) + file(MAKE_DIRECTORY ${CPM_MODULE_PATH}) + # locally added CPM modules should override global packages + set(CMAKE_MODULE_PATH "${CPM_MODULE_PATH};${CMAKE_MODULE_PATH}") +endif() + +if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + set(CPM_PACKAGE_LOCK_FILE + "${CMAKE_BINARY_DIR}/cpm-package-lock.cmake" + CACHE INTERNAL "" + ) + file(WRITE ${CPM_PACKAGE_LOCK_FILE} + "# CPM Package Lock\n# This file should be committed to version control\n\n" + ) +endif() + +include(FetchContent) + +# Try to infer package name from git repository uri (path or url) +function(cpm_package_name_from_git_uri URI RESULT) + if("${URI}" MATCHES "([^/:]+)/?.git/?$") + set(${RESULT} + ${CMAKE_MATCH_1} + PARENT_SCOPE + ) + else() + unset(${RESULT} PARENT_SCOPE) + endif() +endfunction() + +# Find the shortest hash that can be used eg, if origin_hash is +# cccb77ae9609d2768ed80dd42cec54f77b1f1455 the following files will be checked, until one is found +# that is either empty (allowing us to assign origin_hash), or whose contents matches ${origin_hash} +# +# * .../cccb.hash +# * .../cccb77ae.hash +# * .../cccb77ae9609.hash +# * .../cccb77ae9609d276.hash +# * etc +# +# We will be able to use a shorter path with very high probability, but in the (rare) event that the +# first couple characters collide, we will check longer and longer substrings. +function(cpm_get_shortest_hash source_cache_dir origin_hash short_hash_output_var) + # for compatibility with caches populated by a previous version of CPM, check if a directory using + # the full hash already exists + if(EXISTS "${source_cache_dir}/${origin_hash}") + set(${short_hash_output_var} + "${origin_hash}" + PARENT_SCOPE + ) + return() + endif() + + foreach(len RANGE 4 40 4) + string(SUBSTRING "${origin_hash}" 0 ${len} short_hash) + set(hash_lock ${source_cache_dir}/${short_hash}.lock) + set(hash_fp ${source_cache_dir}/${short_hash}.hash) + # Take a lock, so we don't have a race condition with another instance of cmake. We will release + # this lock when we can, however, if there is an error, we want to ensure it gets released on + # it's own on exit from the function. + file(LOCK ${hash_lock} GUARD FUNCTION) + + # Load the contents of .../${short_hash}.hash + file(TOUCH ${hash_fp}) + file(READ ${hash_fp} hash_fp_contents) + + if(hash_fp_contents STREQUAL "") + # Write the origin hash + file(WRITE ${hash_fp} ${origin_hash}) + file(LOCK ${hash_lock} RELEASE) + break() + elseif(hash_fp_contents STREQUAL origin_hash) + file(LOCK ${hash_lock} RELEASE) + break() + else() + file(LOCK ${hash_lock} RELEASE) + endif() + endforeach() + set(${short_hash_output_var} + "${short_hash}" + PARENT_SCOPE + ) +endfunction() + +# Try to infer package name and version from a url +function(cpm_package_name_and_ver_from_url url outName outVer) + if(url MATCHES "[/\\?]([a-zA-Z0-9_\\.-]+)\\.(tar|tar\\.gz|tar\\.bz2|zip|ZIP)(\\?|/|$)") + # We matched an archive + set(filename "${CMAKE_MATCH_1}") + + if(filename MATCHES "([a-zA-Z0-9_\\.-]+)[_-]v?(([0-9]+\\.)*[0-9]+[a-zA-Z0-9]*)") + # We matched - (ie foo-1.2.3) + set(${outName} + "${CMAKE_MATCH_1}" + PARENT_SCOPE + ) + set(${outVer} + "${CMAKE_MATCH_2}" + PARENT_SCOPE + ) + elseif(filename MATCHES "(([0-9]+\\.)+[0-9]+[a-zA-Z0-9]*)") + # We couldn't find a name, but we found a version + # + # In many cases (which we don't handle here) the url would look something like + # `irrelevant/ACTUAL_PACKAGE_NAME/irrelevant/1.2.3.zip`. In such a case we can't possibly + # distinguish the package name from the irrelevant bits. Moreover if we try to match the + # package name from the filename, we'd get bogus at best. + unset(${outName} PARENT_SCOPE) + set(${outVer} + "${CMAKE_MATCH_1}" + PARENT_SCOPE + ) + else() + # Boldly assume that the file name is the package name. + # + # Yes, something like `irrelevant/ACTUAL_NAME/irrelevant/download.zip` will ruin our day, but + # such cases should be quite rare. No popular service does this... we think. + set(${outName} + "${filename}" + PARENT_SCOPE + ) + unset(${outVer} PARENT_SCOPE) + endif() + else() + # No ideas yet what to do with non-archives + unset(${outName} PARENT_SCOPE) + unset(${outVer} PARENT_SCOPE) + endif() +endfunction() + +function(cpm_find_package NAME VERSION) + string(REPLACE " " ";" EXTRA_ARGS "${ARGN}") + find_package(${NAME} ${VERSION} ${EXTRA_ARGS} QUIET) + if(${CPM_ARGS_NAME}_FOUND) + if(DEFINED ${CPM_ARGS_NAME}_VERSION) + set(VERSION ${${CPM_ARGS_NAME}_VERSION}) + endif() + cpm_message(STATUS "${CPM_INDENT} Using local package ${CPM_ARGS_NAME}@${VERSION}") + CPMRegisterPackage(${CPM_ARGS_NAME} "${VERSION}") + set(CPM_PACKAGE_FOUND + YES + PARENT_SCOPE + ) + else() + set(CPM_PACKAGE_FOUND + NO + PARENT_SCOPE + ) + endif() +endfunction() + +# Create a custom FindXXX.cmake module for a CPM package This prevents `find_package(NAME)` from +# finding the system library +function(cpm_create_module_file Name) + if(NOT CPM_DONT_UPDATE_MODULE_PATH) + if(DEFINED CMAKE_FIND_PACKAGE_REDIRECTS_DIR) + # Redirect find_package calls to the CPM package. This is what FetchContent does when you set + # OVERRIDE_FIND_PACKAGE. The CMAKE_FIND_PACKAGE_REDIRECTS_DIR works for find_package in CONFIG + # mode, unlike the Find${Name}.cmake fallback. CMAKE_FIND_PACKAGE_REDIRECTS_DIR is not defined + # in script mode, or in CMake < 3.24. + # https://cmake.org/cmake/help/latest/module/FetchContent.html#fetchcontent-find-package-integration-examples + string(TOLOWER ${Name} NameLower) + file(WRITE ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/${NameLower}-config.cmake + "include(\"\${CMAKE_CURRENT_LIST_DIR}/${NameLower}-extra.cmake\" OPTIONAL)\n" + "include(\"\${CMAKE_CURRENT_LIST_DIR}/${Name}Extra.cmake\" OPTIONAL)\n" + ) + file(WRITE ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/${NameLower}-config-version.cmake + "set(PACKAGE_VERSION_COMPATIBLE TRUE)\n" "set(PACKAGE_VERSION_EXACT TRUE)\n" + ) + else() + file(WRITE ${CPM_MODULE_PATH}/Find${Name}.cmake + "include(\"${CPM_FILE}\")\n${ARGN}\nset(${Name}_FOUND TRUE)" + ) + endif() + endif() +endfunction() + +# Find a package locally or fallback to CPMAddPackage +function(CPMFindPackage) + set(oneValueArgs NAME VERSION GIT_TAG FIND_PACKAGE_ARGUMENTS) + + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "" ${ARGN}) + + if(NOT DEFINED CPM_ARGS_VERSION) + if(DEFINED CPM_ARGS_GIT_TAG) + cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) + endif() + endif() + + set(downloadPackage ${CPM_DOWNLOAD_ALL}) + if(DEFINED CPM_DOWNLOAD_${CPM_ARGS_NAME}) + set(downloadPackage ${CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + elseif(DEFINED ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + set(downloadPackage $ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + endif() + if(downloadPackage) + CPMAddPackage(${ARGN}) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) + + if(NOT CPM_PACKAGE_FOUND) + CPMAddPackage(${ARGN}) + cpm_export_variables(${CPM_ARGS_NAME}) + endif() + +endfunction() + +# checks if a package has been added before +function(cpm_check_if_package_already_added CPM_ARGS_NAME CPM_ARGS_VERSION) + if("${CPM_ARGS_NAME}" IN_LIST CPM_PACKAGES) + CPMGetPackageVersion(${CPM_ARGS_NAME} CPM_PACKAGE_VERSION) + if("${CPM_PACKAGE_VERSION}" VERSION_LESS "${CPM_ARGS_VERSION}") + message( + WARNING + "${CPM_INDENT} Requires a newer version of ${CPM_ARGS_NAME} (${CPM_ARGS_VERSION}) than currently included (${CPM_PACKAGE_VERSION})." + ) + endif() + cpm_get_fetch_properties(${CPM_ARGS_NAME}) + set(${CPM_ARGS_NAME}_ADDED NO) + set(CPM_PACKAGE_ALREADY_ADDED + YES + PARENT_SCOPE + ) + cpm_export_variables(${CPM_ARGS_NAME}) + else() + set(CPM_PACKAGE_ALREADY_ADDED + NO + PARENT_SCOPE + ) + endif() +endfunction() + +# Parse the argument of CPMAddPackage in case a single one was provided and convert it to a list of +# arguments which can then be parsed idiomatically. For example gh:foo/bar@1.2.3 will be converted +# to: GITHUB_REPOSITORY;foo/bar;VERSION;1.2.3 +function(cpm_parse_add_package_single_arg arg outArgs) + # Look for a scheme + if("${arg}" MATCHES "^([a-zA-Z]+):(.+)$") + string(TOLOWER "${CMAKE_MATCH_1}" scheme) + set(uri "${CMAKE_MATCH_2}") + + # Check for CPM-specific schemes + if(scheme STREQUAL "gh") + set(out "GITHUB_REPOSITORY;${uri}") + set(packageType "git") + elseif(scheme STREQUAL "gl") + set(out "GITLAB_REPOSITORY;${uri}") + set(packageType "git") + elseif(scheme STREQUAL "bb") + set(out "BITBUCKET_REPOSITORY;${uri}") + set(packageType "git") + # A CPM-specific scheme was not found. Looks like this is a generic URL so try to determine + # type + elseif(arg MATCHES ".git/?(@|#|$)") + set(out "GIT_REPOSITORY;${arg}") + set(packageType "git") + else() + # Fall back to a URL + set(out "URL;${arg}") + set(packageType "archive") + + # We could also check for SVN since FetchContent supports it, but SVN is so rare these days. + # We just won't bother with the additional complexity it will induce in this function. SVN is + # done by multi-arg + endif() + else() + if(arg MATCHES ".git/?(@|#|$)") + set(out "GIT_REPOSITORY;${arg}") + set(packageType "git") + else() + # Give up + message(FATAL_ERROR "${CPM_INDENT} Can't determine package type of '${arg}'") + endif() + endif() + + # For all packages we interpret @... as version. Only replace the last occurrence. Thus URIs + # containing '@' can be used + string(REGEX REPLACE "@([^@]+)$" ";VERSION;\\1" out "${out}") + + # Parse the rest according to package type + if(packageType STREQUAL "git") + # For git repos we interpret #... as a tag or branch or commit hash + string(REGEX REPLACE "#([^#]+)$" ";GIT_TAG;\\1" out "${out}") + elseif(packageType STREQUAL "archive") + # For archives we interpret #... as a URL hash. + string(REGEX REPLACE "#([^#]+)$" ";URL_HASH;\\1" out "${out}") + # We don't try to parse the version if it's not provided explicitly. cpm_get_version_from_url + # should do this at a later point + else() + # We should never get here. This is an assertion and hitting it means there's a problem with the + # code above. A packageType was set, but not handled by this if-else. + message(FATAL_ERROR "${CPM_INDENT} Unsupported package type '${packageType}' of '${arg}'") + endif() + + set(${outArgs} + ${out} + PARENT_SCOPE + ) +endfunction() + +# Check that the working directory for a git repo is clean +function(cpm_check_git_working_dir_is_clean repoPath gitTag isClean) + + find_package(Git REQUIRED) + + if(NOT GIT_EXECUTABLE) + # No git executable, assume directory is clean + set(${isClean} + TRUE + PARENT_SCOPE + ) + return() + endif() + + # check for uncommitted changes + execute_process( + COMMAND ${GIT_EXECUTABLE} status --porcelain + RESULT_VARIABLE resultGitStatus + OUTPUT_VARIABLE repoStatus + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET + WORKING_DIRECTORY ${repoPath} + ) + if(resultGitStatus) + # not supposed to happen, assume clean anyway + message(WARNING "${CPM_INDENT} Calling git status on folder ${repoPath} failed") + set(${isClean} + TRUE + PARENT_SCOPE + ) + return() + endif() + + if(NOT "${repoStatus}" STREQUAL "") + set(${isClean} + FALSE + PARENT_SCOPE + ) + return() + endif() + + # check for committed changes + execute_process( + COMMAND ${GIT_EXECUTABLE} diff -s --exit-code ${gitTag} + RESULT_VARIABLE resultGitDiff + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_QUIET + WORKING_DIRECTORY ${repoPath} + ) + + if(${resultGitDiff} EQUAL 0) + set(${isClean} + TRUE + PARENT_SCOPE + ) + else() + set(${isClean} + FALSE + PARENT_SCOPE + ) + endif() + +endfunction() + +# Add PATCH_COMMAND to CPM_ARGS_UNPARSED_ARGUMENTS. This method consumes a list of files in ARGN +# then generates a `PATCH_COMMAND` appropriate for `ExternalProject_Add()`. This command is appended +# to the parent scope's `CPM_ARGS_UNPARSED_ARGUMENTS`. +function(cpm_add_patches) + # Return if no patch files are supplied. + if(NOT ARGN) + return() + endif() + + # Find the patch program. + find_program(PATCH_EXECUTABLE patch) + if(CMAKE_HOST_WIN32 AND NOT PATCH_EXECUTABLE) + # The Windows git executable is distributed with patch.exe. Find the path to the executable, if + # it exists, then search `../usr/bin` and `../../usr/bin` for patch.exe. + find_package(Git QUIET) + if(GIT_EXECUTABLE) + get_filename_component(extra_search_path ${GIT_EXECUTABLE} DIRECTORY) + get_filename_component(extra_search_path_1up ${extra_search_path} DIRECTORY) + get_filename_component(extra_search_path_2up ${extra_search_path_1up} DIRECTORY) + find_program( + PATCH_EXECUTABLE patch HINTS "${extra_search_path_1up}/usr/bin" + "${extra_search_path_2up}/usr/bin" + ) + endif() + endif() + if(NOT PATCH_EXECUTABLE) + message(FATAL_ERROR "Couldn't find `patch` executable to use with PATCHES keyword.") + endif() + + # Create a temporary + set(temp_list ${CPM_ARGS_UNPARSED_ARGUMENTS}) + + # Ensure each file exists (or error out) and add it to the list. + set(first_item True) + foreach(PATCH_FILE ${ARGN}) + # Make sure the patch file exists, if we can't find it, try again in the current directory. + if(NOT EXISTS "${PATCH_FILE}") + if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") + message(FATAL_ERROR "Couldn't find patch file: '${PATCH_FILE}'") + endif() + set(PATCH_FILE "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") + endif() + + # Convert to absolute path for use with patch file command. + get_filename_component(PATCH_FILE "${PATCH_FILE}" ABSOLUTE) + + # The first patch entry must be preceded by "PATCH_COMMAND" while the following items are + # preceded by "&&". + if(first_item) + set(first_item False) + list(APPEND temp_list "PATCH_COMMAND") + else() + list(APPEND temp_list "&&") + endif() + # Add the patch command to the list + list(APPEND temp_list "${PATCH_EXECUTABLE}" "-p1" "<" "${PATCH_FILE}") + endforeach() + + # Move temp out into parent scope. + set(CPM_ARGS_UNPARSED_ARGUMENTS + ${temp_list} + PARENT_SCOPE + ) + +endfunction() + +# method to overwrite internal FetchContent properties, to allow using CPM.cmake to overload +# FetchContent calls. As these are internal cmake properties, this method should be used carefully +# and may need modification in future CMake versions. Source: +# https://github.com/Kitware/CMake/blob/dc3d0b5a0a7d26d43d6cfeb511e224533b5d188f/Modules/FetchContent.cmake#L1152 +function(cpm_override_fetchcontent contentName) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "SOURCE_DIR;BINARY_DIR" "") + if(NOT "${arg_UNPARSED_ARGUMENTS}" STREQUAL "") + message(FATAL_ERROR "${CPM_INDENT} Unsupported arguments: ${arg_UNPARSED_ARGUMENTS}") + endif() + + string(TOLOWER ${contentName} contentNameLower) + set(prefix "_FetchContent_${contentNameLower}") + + set(propertyName "${prefix}_sourceDir") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} "${arg_SOURCE_DIR}") + + set(propertyName "${prefix}_binaryDir") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} "${arg_BINARY_DIR}") + + set(propertyName "${prefix}_populated") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} TRUE) +endfunction() + +# Download and add a package from source +function(CPMAddPackage) + cpm_set_policies() + + set(oneValueArgs + NAME + FORCE + VERSION + GIT_TAG + DOWNLOAD_ONLY + GITHUB_REPOSITORY + GITLAB_REPOSITORY + BITBUCKET_REPOSITORY + GIT_REPOSITORY + SOURCE_DIR + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + EXCLUDE_FROM_ALL + SOURCE_SUBDIR + CUSTOM_CACHE_KEY + ) + + set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND PATCHES) + + list(LENGTH ARGN argnLength) + + # Parse single shorthand argument + if(argnLength EQUAL 1) + cpm_parse_add_package_single_arg("${ARGN}" ARGN) + + # The shorthand syntax implies EXCLUDE_FROM_ALL and SYSTEM + set(ARGN "${ARGN};EXCLUDE_FROM_ALL;YES;SYSTEM;YES;") + + # Parse URI shorthand argument + elseif(argnLength GREATER 1 AND "${ARGV0}" STREQUAL "URI") + list(REMOVE_AT ARGN 0 1) # remove "URI gh:<...>@version#tag" + cpm_parse_add_package_single_arg("${ARGV1}" ARGV0) + + set(ARGN "${ARGV0};EXCLUDE_FROM_ALL;YES;SYSTEM;YES;${ARGN}") + endif() + + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") + + # Set default values for arguments + if(NOT DEFINED CPM_ARGS_VERSION) + if(DEFINED CPM_ARGS_GIT_TAG) + cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) + endif() + endif() + + if(CPM_ARGS_DOWNLOAD_ONLY) + set(DOWNLOAD_ONLY ${CPM_ARGS_DOWNLOAD_ONLY}) + else() + set(DOWNLOAD_ONLY NO) + endif() + + if(DEFINED CPM_ARGS_GITHUB_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://github.com/${CPM_ARGS_GITHUB_REPOSITORY}.git") + elseif(DEFINED CPM_ARGS_GITLAB_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://gitlab.com/${CPM_ARGS_GITLAB_REPOSITORY}.git") + elseif(DEFINED CPM_ARGS_BITBUCKET_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://bitbucket.org/${CPM_ARGS_BITBUCKET_REPOSITORY}.git") + endif() + + if(DEFINED CPM_ARGS_GIT_REPOSITORY) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_REPOSITORY ${CPM_ARGS_GIT_REPOSITORY}) + if(NOT DEFINED CPM_ARGS_GIT_TAG) + set(CPM_ARGS_GIT_TAG v${CPM_ARGS_VERSION}) + endif() + + # If a name wasn't provided, try to infer it from the git repo + if(NOT DEFINED CPM_ARGS_NAME) + cpm_package_name_from_git_uri(${CPM_ARGS_GIT_REPOSITORY} CPM_ARGS_NAME) + endif() + endif() + + set(CPM_SKIP_FETCH FALSE) + + if(DEFINED CPM_ARGS_GIT_TAG) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_TAG ${CPM_ARGS_GIT_TAG}) + # If GIT_SHALLOW is explicitly specified, honor the value. + if(DEFINED CPM_ARGS_GIT_SHALLOW) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW ${CPM_ARGS_GIT_SHALLOW}) + endif() + endif() + + if(DEFINED CPM_ARGS_URL) + # If a name or version aren't provided, try to infer them from the URL + list(GET CPM_ARGS_URL 0 firstUrl) + cpm_package_name_and_ver_from_url(${firstUrl} nameFromUrl verFromUrl) + # If we fail to obtain name and version from the first URL, we could try other URLs if any. + # However multiple URLs are expected to be quite rare, so for now we won't bother. + + # If the caller provided their own name and version, they trump the inferred ones. + if(NOT DEFINED CPM_ARGS_NAME) + set(CPM_ARGS_NAME ${nameFromUrl}) + endif() + + # this is dumb and should not be done + # if(NOT DEFINED CPM_ARGS_VERSION) + # set(CPM_ARGS_VERSION ${verFromUrl}) + # endif() + + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS URL "${CPM_ARGS_URL}") + endif() + + # Check for required arguments + + if(NOT DEFINED CPM_ARGS_NAME) + message( + FATAL_ERROR + "${CPM_INDENT} 'NAME' was not provided and couldn't be automatically inferred for package added with arguments: '${ARGN}'" + ) + endif() + + # Check if package has been added before + cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + if(CPM_PACKAGE_ALREADY_ADDED) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + # Check for manual overrides + if(NOT CPM_ARGS_FORCE AND NOT "${CPM_${CPM_ARGS_NAME}_SOURCE}" STREQUAL "") + set(PACKAGE_SOURCE ${CPM_${CPM_ARGS_NAME}_SOURCE}) + set(CPM_${CPM_ARGS_NAME}_SOURCE "") + CPMAddPackage( + NAME "${CPM_ARGS_NAME}" + SOURCE_DIR "${PACKAGE_SOURCE}" + EXCLUDE_FROM_ALL "${CPM_ARGS_EXCLUDE_FROM_ALL}" + SYSTEM "${CPM_ARGS_SYSTEM}" + PATCHES "${CPM_ARGS_PATCHES}" + OPTIONS "${CPM_ARGS_OPTIONS}" + SOURCE_SUBDIR "${CPM_ARGS_SOURCE_SUBDIR}" + DOWNLOAD_ONLY "${DOWNLOAD_ONLY}" + FORCE True + ) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + # Check for available declaration + if(NOT CPM_ARGS_FORCE AND NOT "${CPM_DECLARATION_${CPM_ARGS_NAME}}" STREQUAL "") + set(declaration ${CPM_DECLARATION_${CPM_ARGS_NAME}}) + set(CPM_DECLARATION_${CPM_ARGS_NAME} "") + CPMAddPackage(${declaration}) + cpm_export_variables(${CPM_ARGS_NAME}) + # checking again to ensure version and option compatibility + cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + return() + endif() + + if(NOT CPM_ARGS_FORCE) + if(CPM_USE_LOCAL_PACKAGES OR CPM_LOCAL_PACKAGES_ONLY) + cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) + + if(CPM_PACKAGE_FOUND) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + if(CPM_LOCAL_PACKAGES_ONLY) + message( + SEND_ERROR + "${CPM_INDENT} ${CPM_ARGS_NAME} not found via find_package(${CPM_ARGS_NAME} ${CPM_ARGS_VERSION})" + ) + endif() + endif() + endif() + + CPMRegisterPackage("${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}") + + if(DEFINED CPM_ARGS_GIT_TAG) + set(PACKAGE_INFO "${CPM_ARGS_GIT_TAG}") + elseif(DEFINED CPM_ARGS_SOURCE_DIR) + set(PACKAGE_INFO "${CPM_ARGS_SOURCE_DIR}") + else() + set(PACKAGE_INFO "${CPM_ARGS_VERSION}") + endif() + + if(DEFINED FETCHCONTENT_BASE_DIR) + # respect user's FETCHCONTENT_BASE_DIR if set + set(CPM_FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR}) + else() + set(CPM_FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_deps) + endif() + + cpm_add_patches(${CPM_ARGS_PATCHES}) + + if(DEFINED CPM_ARGS_DOWNLOAD_COMMAND) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS DOWNLOAD_COMMAND ${CPM_ARGS_DOWNLOAD_COMMAND}) + elseif(DEFINED CPM_ARGS_SOURCE_DIR) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${CPM_ARGS_SOURCE_DIR}) + if(NOT IS_ABSOLUTE ${CPM_ARGS_SOURCE_DIR}) + # Expand `CPM_ARGS_SOURCE_DIR` relative path. This is important because EXISTS doesn't work + # for relative paths. + get_filename_component( + source_directory ${CPM_ARGS_SOURCE_DIR} REALPATH BASE_DIR ${CMAKE_CURRENT_BINARY_DIR} + ) + else() + set(source_directory ${CPM_ARGS_SOURCE_DIR}) + endif() + if(NOT EXISTS ${source_directory}) + string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) + # remove timestamps so CMake will re-download the dependency + file(REMOVE_RECURSE "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild") + endif() + elseif(CPM_SOURCE_CACHE AND NOT CPM_ARGS_NO_CACHE) + string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) + set(origin_parameters ${CPM_ARGS_UNPARSED_ARGUMENTS}) + list(SORT origin_parameters) + if(CPM_ARGS_CUSTOM_CACHE_KEY) + # Application set a custom unique directory name + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${CPM_ARGS_CUSTOM_CACHE_KEY}) + elseif(CPM_USE_NAMED_CACHE_DIRECTORIES) + string(SHA1 origin_hash "${origin_parameters};NEW_CACHE_STRUCTURE_TAG") + cpm_get_shortest_hash( + "${CPM_SOURCE_CACHE}/${lower_case_name}" # source cache directory + "${origin_hash}" # Input hash + origin_hash # Computed hash + ) + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}/${CPM_ARGS_NAME}) + else() + string(SHA1 origin_hash "${origin_parameters}") + cpm_get_shortest_hash( + "${CPM_SOURCE_CACHE}/${lower_case_name}" # source cache directory + "${origin_hash}" # Input hash + origin_hash # Computed hash + ) + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}) + endif() + # Expand `download_directory` relative path. This is important because EXISTS doesn't work for + # relative paths. + get_filename_component(download_directory ${download_directory} ABSOLUTE) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${download_directory}) + + if(CPM_SOURCE_CACHE) + file(LOCK ${download_directory}/../cmake.lock) + endif() + + if(EXISTS ${download_directory}) + if(CPM_SOURCE_CACHE) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + + cpm_store_fetch_properties( + ${CPM_ARGS_NAME} "${download_directory}" + "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" + ) + cpm_get_fetch_properties("${CPM_ARGS_NAME}") + + if(DEFINED CPM_ARGS_GIT_TAG AND NOT (PATCH_COMMAND IN_LIST CPM_ARGS_UNPARSED_ARGUMENTS)) + # warn if cache has been changed since checkout + cpm_check_git_working_dir_is_clean(${download_directory} ${CPM_ARGS_GIT_TAG} IS_CLEAN) + if(NOT ${IS_CLEAN}) + message( + WARNING "${CPM_INDENT} Cache for ${CPM_ARGS_NAME} (${download_directory}) is dirty" + ) + endif() + endif() + + cpm_add_subdirectory( + "${CPM_ARGS_NAME}" + "${DOWNLOAD_ONLY}" + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + "${${CPM_ARGS_NAME}_BINARY_DIR}" + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + "${CPM_ARGS_SYSTEM}" + "${CPM_ARGS_OPTIONS}" + ) + set(PACKAGE_INFO "${PACKAGE_INFO} at ${download_directory}") + + # As the source dir is already cached/populated, we override the call to FetchContent. + set(CPM_SKIP_FETCH TRUE) + cpm_override_fetchcontent( + "${lower_case_name}" SOURCE_DIR "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + BINARY_DIR "${${CPM_ARGS_NAME}_BINARY_DIR}" + ) + + else() + # Enable shallow clone when GIT_TAG is not a commit hash. Our guess may not be accurate, but + # it should guarantee no commit hash get mis-detected. + if(NOT DEFINED CPM_ARGS_GIT_SHALLOW) + cpm_is_git_tag_commit_hash("${CPM_ARGS_GIT_TAG}" IS_HASH) + if(NOT ${IS_HASH}) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW TRUE) + endif() + endif() + + # remove timestamps so CMake will re-download the dependency + file(REMOVE_RECURSE ${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild) + set(PACKAGE_INFO "${PACKAGE_INFO} to ${download_directory}") + endif() + endif() + + if(NOT "${DOWNLOAD_ONLY}") + cpm_create_module_file(${CPM_ARGS_NAME} "CPMAddPackage(\"${ARGN}\")") + endif() + + if(CPM_PACKAGE_LOCK_ENABLED) + if((CPM_ARGS_VERSION AND NOT CPM_ARGS_SOURCE_DIR) OR CPM_INCLUDE_ALL_IN_PACKAGE_LOCK) + cpm_add_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") + elseif(CPM_ARGS_SOURCE_DIR) + cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "local directory") + else() + cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") + endif() + endif() + + cpm_message( + STATUS "${CPM_INDENT} Adding package ${CPM_ARGS_NAME}@${CPM_ARGS_VERSION} (${PACKAGE_INFO})" + ) + + if(NOT CPM_SKIP_FETCH) + # CMake 3.28 added EXCLUDE, SYSTEM (3.25), and SOURCE_SUBDIR (3.18) to FetchContent_Declare. + # Calling FetchContent_MakeAvailable will then internally forward these options to + # add_subdirectory. Up until these changes, we had to call FetchContent_Populate and + # add_subdirectory separately, which is no longer necessary and has been deprecated as of 3.30. + # A Bug in CMake prevents us to use the non-deprecated functions until 3.30.3. + set(fetchContentDeclareExtraArgs "") + if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.30.3") + if(${CPM_ARGS_EXCLUDE_FROM_ALL}) + list(APPEND fetchContentDeclareExtraArgs EXCLUDE_FROM_ALL) + endif() + if(${CPM_ARGS_SYSTEM}) + list(APPEND fetchContentDeclareExtraArgs SYSTEM) + endif() + if(DEFINED CPM_ARGS_SOURCE_SUBDIR) + list(APPEND fetchContentDeclareExtraArgs SOURCE_SUBDIR ${CPM_ARGS_SOURCE_SUBDIR}) + endif() + # For CMake version <3.28 OPTIONS are parsed in cpm_add_subdirectory + if(CPM_ARGS_OPTIONS AND NOT DOWNLOAD_ONLY) + foreach(OPTION ${CPM_ARGS_OPTIONS}) + cpm_parse_option("${OPTION}") + set(${OPTION_KEY} "${OPTION_VALUE}") + endforeach() + endif() + endif() + cpm_declare_fetch( + "${CPM_ARGS_NAME}" ${fetchContentDeclareExtraArgs} "${CPM_ARGS_UNPARSED_ARGUMENTS}" + ) + + cpm_fetch_package("${CPM_ARGS_NAME}" ${DOWNLOAD_ONLY} populated ${CPM_ARGS_UNPARSED_ARGUMENTS}) + if(CPM_SOURCE_CACHE AND download_directory) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + if(${populated} AND ${CMAKE_VERSION} VERSION_LESS "3.30.3") + cpm_add_subdirectory( + "${CPM_ARGS_NAME}" + "${DOWNLOAD_ONLY}" + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + "${${CPM_ARGS_NAME}_BINARY_DIR}" + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + "${CPM_ARGS_SYSTEM}" + "${CPM_ARGS_OPTIONS}" + ) + endif() + cpm_get_fetch_properties("${CPM_ARGS_NAME}") + endif() + + set(${CPM_ARGS_NAME}_ADDED YES) + cpm_export_variables("${CPM_ARGS_NAME}") +endfunction() + +# Fetch a previously declared package +macro(CPMGetPackage Name) + if(DEFINED "CPM_DECLARATION_${Name}") + CPMAddPackage(NAME ${Name}) + else() + message(SEND_ERROR "${CPM_INDENT} Cannot retrieve package ${Name}: no declaration available") + endif() +endmacro() + +# export variables available to the caller to the parent scope expects ${CPM_ARGS_NAME} to be set +macro(cpm_export_variables name) + set(${name}_SOURCE_DIR + "${${name}_SOURCE_DIR}" + PARENT_SCOPE + ) + set(${name}_BINARY_DIR + "${${name}_BINARY_DIR}" + PARENT_SCOPE + ) + set(${name}_ADDED + "${${name}_ADDED}" + PARENT_SCOPE + ) + set(CPM_LAST_PACKAGE_NAME + "${name}" + PARENT_SCOPE + ) +endmacro() + +# declares a package, so that any call to CPMAddPackage for the package name will use these +# arguments instead. Previous declarations will not be overridden. +macro(CPMDeclarePackage Name) + if(NOT DEFINED "CPM_DECLARATION_${Name}") + set("CPM_DECLARATION_${Name}" "${ARGN}") + endif() +endmacro() + +function(cpm_add_to_package_lock Name) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + cpm_prettify_package_arguments(PRETTY_ARGN false ${ARGN}) + file(APPEND ${CPM_PACKAGE_LOCK_FILE} "# ${Name}\nCPMDeclarePackage(${Name}\n${PRETTY_ARGN})\n") + endif() +endfunction() + +function(cpm_add_comment_to_package_lock Name) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + cpm_prettify_package_arguments(PRETTY_ARGN true ${ARGN}) + file(APPEND ${CPM_PACKAGE_LOCK_FILE} + "# ${Name} (unversioned)\n# CPMDeclarePackage(${Name}\n${PRETTY_ARGN}#)\n" + ) + endif() +endfunction() + +# includes the package lock file if it exists and creates a target `cpm-update-package-lock` to +# update it +macro(CPMUsePackageLock file) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + get_filename_component(CPM_ABSOLUTE_PACKAGE_LOCK_PATH ${file} ABSOLUTE) + if(EXISTS ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + include(${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + endif() + if(NOT TARGET cpm-update-package-lock) + add_custom_target( + cpm-update-package-lock COMMAND ${CMAKE_COMMAND} -E copy ${CPM_PACKAGE_LOCK_FILE} + ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH} + ) + endif() + set(CPM_PACKAGE_LOCK_ENABLED true) + endif() +endmacro() + +# registers a package that has been added to CPM +function(CPMRegisterPackage PACKAGE VERSION) + list(APPEND CPM_PACKAGES ${PACKAGE}) + set(CPM_PACKAGES + ${CPM_PACKAGES} + CACHE INTERNAL "" + ) + set("CPM_PACKAGE_${PACKAGE}_VERSION" + ${VERSION} + CACHE INTERNAL "" + ) +endfunction() + +# retrieve the current version of the package to ${OUTPUT} +function(CPMGetPackageVersion PACKAGE OUTPUT) + set(${OUTPUT} + "${CPM_PACKAGE_${PACKAGE}_VERSION}" + PARENT_SCOPE + ) +endfunction() + +# declares a package in FetchContent_Declare +function(cpm_declare_fetch PACKAGE) + if(${CPM_DRY_RUN}) + cpm_message(STATUS "${CPM_INDENT} Package not declared (dry run)") + return() + endif() + + FetchContent_Declare(${PACKAGE} ${ARGN}) +endfunction() + +# returns properties for a package previously defined by cpm_declare_fetch +function(cpm_get_fetch_properties PACKAGE) + if(${CPM_DRY_RUN}) + return() + endif() + + set(${PACKAGE}_SOURCE_DIR + "${CPM_PACKAGE_${PACKAGE}_SOURCE_DIR}" + PARENT_SCOPE + ) + set(${PACKAGE}_BINARY_DIR + "${CPM_PACKAGE_${PACKAGE}_BINARY_DIR}" + PARENT_SCOPE + ) +endfunction() + +function(cpm_store_fetch_properties PACKAGE source_dir binary_dir) + if(${CPM_DRY_RUN}) + return() + endif() + + set(CPM_PACKAGE_${PACKAGE}_SOURCE_DIR + "${source_dir}" + CACHE INTERNAL "" + ) + set(CPM_PACKAGE_${PACKAGE}_BINARY_DIR + "${binary_dir}" + CACHE INTERNAL "" + ) +endfunction() + +# adds a package as a subdirectory if viable, according to provided options +function( + cpm_add_subdirectory + PACKAGE + DOWNLOAD_ONLY + SOURCE_DIR + BINARY_DIR + EXCLUDE + SYSTEM + OPTIONS +) + + if(NOT DOWNLOAD_ONLY AND EXISTS ${SOURCE_DIR}/CMakeLists.txt) + set(addSubdirectoryExtraArgs "") + if(EXCLUDE) + list(APPEND addSubdirectoryExtraArgs EXCLUDE_FROM_ALL) + endif() + if("${SYSTEM}" AND "${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.25") + # https://cmake.org/cmake/help/latest/prop_dir/SYSTEM.html#prop_dir:SYSTEM + list(APPEND addSubdirectoryExtraArgs SYSTEM) + endif() + if(OPTIONS) + foreach(OPTION ${OPTIONS}) + cpm_parse_option("${OPTION}") + set(${OPTION_KEY} "${OPTION_VALUE}") + endforeach() + endif() + set(CPM_OLD_INDENT "${CPM_INDENT}") + set(CPM_INDENT "${CPM_INDENT} ${PACKAGE}:") + add_subdirectory(${SOURCE_DIR} ${BINARY_DIR} ${addSubdirectoryExtraArgs}) + set(CPM_INDENT "${CPM_OLD_INDENT}") + endif() +endfunction() + +# downloads a previously declared package via FetchContent and exports the variables +# `${PACKAGE}_SOURCE_DIR` and `${PACKAGE}_BINARY_DIR` to the parent scope +function(cpm_fetch_package PACKAGE DOWNLOAD_ONLY populated) + set(${populated} + FALSE + PARENT_SCOPE + ) + if(${CPM_DRY_RUN}) + cpm_message(STATUS "${CPM_INDENT} Package ${PACKAGE} not fetched (dry run)") + return() + endif() + + FetchContent_GetProperties(${PACKAGE}) + + string(TOLOWER "${PACKAGE}" lower_case_name) + + if(NOT ${lower_case_name}_POPULATED) + if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.30.3") + if(DOWNLOAD_ONLY) + # MakeAvailable will call add_subdirectory internally which is not what we want when + # DOWNLOAD_ONLY is set. Populate will only download the dependency without adding it to the + # build + FetchContent_Populate( + ${PACKAGE} + SOURCE_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-src" + BINARY_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" + SUBBUILD_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild" + ${ARGN} + ) + else() + FetchContent_MakeAvailable(${PACKAGE}) + endif() + else() + FetchContent_Populate(${PACKAGE}) + endif() + set(${populated} + TRUE + PARENT_SCOPE + ) + endif() + + cpm_store_fetch_properties( + ${CPM_ARGS_NAME} ${${lower_case_name}_SOURCE_DIR} ${${lower_case_name}_BINARY_DIR} + ) + + set(${PACKAGE}_SOURCE_DIR + ${${lower_case_name}_SOURCE_DIR} + PARENT_SCOPE + ) + set(${PACKAGE}_BINARY_DIR + ${${lower_case_name}_BINARY_DIR} + PARENT_SCOPE + ) +endfunction() + +# splits a package option +function(cpm_parse_option OPTION) + string(REGEX MATCH "^[^ ]+" OPTION_KEY "${OPTION}") + string(LENGTH "${OPTION}" OPTION_LENGTH) + string(LENGTH "${OPTION_KEY}" OPTION_KEY_LENGTH) + if(OPTION_KEY_LENGTH STREQUAL OPTION_LENGTH) + # no value for key provided, assume user wants to set option to "ON" + set(OPTION_VALUE "ON") + else() + math(EXPR OPTION_KEY_LENGTH "${OPTION_KEY_LENGTH}+1") + string(SUBSTRING "${OPTION}" "${OPTION_KEY_LENGTH}" "-1" OPTION_VALUE) + endif() + set(OPTION_KEY + "${OPTION_KEY}" + PARENT_SCOPE + ) + set(OPTION_VALUE + "${OPTION_VALUE}" + PARENT_SCOPE + ) +endfunction() + +# guesses the package version from a git tag +function(cpm_get_version_from_git_tag GIT_TAG RESULT) + string(LENGTH ${GIT_TAG} length) + if(length EQUAL 40) + # GIT_TAG is probably a git hash + set(${RESULT} + 0 + PARENT_SCOPE + ) + else() + string(REGEX MATCH "v?([0123456789.]*).*" _ ${GIT_TAG}) + set(${RESULT} + ${CMAKE_MATCH_1} + PARENT_SCOPE + ) + endif() +endfunction() + +# guesses if the git tag is a commit hash or an actual tag or a branch name. +function(cpm_is_git_tag_commit_hash GIT_TAG RESULT) + string(LENGTH "${GIT_TAG}" length) + # full hash has 40 characters, and short hash has at least 7 characters. + if(length LESS 7 OR length GREATER 40) + set(${RESULT} + 0 + PARENT_SCOPE + ) + else() + if(${GIT_TAG} MATCHES "^[a-fA-F0-9]+$") + set(${RESULT} + 1 + PARENT_SCOPE + ) + else() + set(${RESULT} + 0 + PARENT_SCOPE + ) + endif() + endif() +endfunction() + +function(cpm_prettify_package_arguments OUT_VAR IS_IN_COMMENT) + set(oneValueArgs + NAME + FORCE + VERSION + GIT_TAG + DOWNLOAD_ONLY + GITHUB_REPOSITORY + GITLAB_REPOSITORY + BITBUCKET_REPOSITORY + GIT_REPOSITORY + SOURCE_DIR + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + EXCLUDE_FROM_ALL + SOURCE_SUBDIR + ) + set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND) + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + foreach(oneArgName ${oneValueArgs}) + if(DEFINED CPM_ARGS_${oneArgName}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + if(${oneArgName} STREQUAL "SOURCE_DIR") + string(REPLACE ${CMAKE_SOURCE_DIR} "\${CMAKE_SOURCE_DIR}" CPM_ARGS_${oneArgName} + ${CPM_ARGS_${oneArgName}} + ) + endif() + string(APPEND PRETTY_OUT_VAR " ${oneArgName} ${CPM_ARGS_${oneArgName}}\n") + endif() + endforeach() + foreach(multiArgName ${multiValueArgs}) + if(DEFINED CPM_ARGS_${multiArgName}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " ${multiArgName}\n") + foreach(singleOption ${CPM_ARGS_${multiArgName}}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " \"${singleOption}\"\n") + endforeach() + endif() + endforeach() + + if(NOT "${CPM_ARGS_UNPARSED_ARGUMENTS}" STREQUAL "") + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " ") + foreach(CPM_ARGS_UNPARSED_ARGUMENT ${CPM_ARGS_UNPARSED_ARGUMENTS}) + string(APPEND PRETTY_OUT_VAR " ${CPM_ARGS_UNPARSED_ARGUMENT}") + endforeach() + string(APPEND PRETTY_OUT_VAR "\n") + endif() + + set(${OUT_VAR} + ${PRETTY_OUT_VAR} + PARENT_SCOPE + ) + +endfunction() diff --git a/CMakeModules/CPMUtil.cmake b/CMakeModules/CPMUtil.cmake new file mode 100644 index 0000000000..3d7b84c029 --- /dev/null +++ b/CMakeModules/CPMUtil.cmake @@ -0,0 +1,628 @@ +# SPDX-FileCopyrightText: Copyright 2025 crueter +# SPDX-License-Identifier: GPL-3.0-or-later + +if (MSVC OR ANDROID) + set(BUNDLED_DEFAULT ON) +else() + set(BUNDLED_DEFAULT OFF) +endif() + +option(CPMUTIL_FORCE_BUNDLED + "Force bundled packages for all CPM depdendencies" ${BUNDLED_DEFAULT}) + +option(CPMUTIL_FORCE_SYSTEM + "Force system packages for all CPM dependencies (NOT RECOMMENDED)" OFF) + +cmake_minimum_required(VERSION 3.22) +include(CPM) + +# cpmfile parsing +set(CPMUTIL_JSON_FILE "${CMAKE_CURRENT_SOURCE_DIR}/cpmfile.json") + +if (EXISTS ${CPMUTIL_JSON_FILE}) + file(READ ${CPMUTIL_JSON_FILE} CPMFILE_CONTENT) +else() + message(WARNING "[CPMUtil] cpmfile ${CPMUTIL_JSON_FILE} does not exist, AddJsonPackage will be a no-op") +endif() + +# Utility stuff +function(cpm_utils_message level name message) + message(${level} "[CPMUtil] ${name}: ${message}") +endfunction() + +function(array_to_list array length out) + math(EXPR range "${length} - 1") + + foreach(IDX RANGE ${range}) + string(JSON _element GET "${array}" "${IDX}") + + list(APPEND NEW_LIST ${_element}) + endforeach() + + set("${out}" "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +function(get_json_element object out member default) + string(JSON out_type ERROR_VARIABLE err TYPE "${object}" ${member}) + + if (err) + set("${out}" "${default}" PARENT_SCOPE) + return() + endif() + + string(JSON outvar GET "${object}" ${member}) + + if (out_type STREQUAL "ARRAY") + string(JSON _len LENGTH "${object}" ${member}) + # array_to_list("${outvar}" ${_len} outvar) + set("${out}_LENGTH" "${_len}" PARENT_SCOPE) + endif() + + set("${out}" "${outvar}" PARENT_SCOPE) +endfunction() + +# The preferred usage +function(AddJsonPackage) + set(oneValueArgs + NAME + + # these are overrides that can be generated at runtime, so can be defined separately from the json + DOWNLOAD_ONLY + BUNDLED_PACKAGE + ) + + set(multiValueArgs OPTIONS) + + cmake_parse_arguments(JSON "" "${oneValueArgs}" "${multiValueArgs}" + "${ARGN}") + + list(LENGTH ARGN argnLength) + + # single name argument + if(argnLength EQUAL 1) + set(JSON_NAME "${ARGV0}") + endif() + + if (NOT DEFINED CPMFILE_CONTENT) + cpm_utils_message(WARNING ${name} "No cpmfile, AddJsonPackage is a no-op") + return() + endif() + + if (NOT DEFINED JSON_NAME) + cpm_utils_message(FATAL_ERROR "json package" "No name specified") + endif() + + string(JSON object ERROR_VARIABLE err GET "${CPMFILE_CONTENT}" "${JSON_NAME}") + + if (err) + cpm_utils_message(FATAL_ERROR ${JSON_NAME} "Not found in cpmfile") + endif() + + get_json_element("${object}" package package ${JSON_NAME}) + get_json_element("${object}" repo repo "") + get_json_element("${object}" ci ci OFF) + get_json_element("${object}" version version "") + + if (ci) + get_json_element("${object}" name name "${JSON_NAME}") + get_json_element("${object}" extension extension "tar.zst") + get_json_element("${object}" min_version min_version "") + get_json_element("${object}" raw_disabled disabled_platforms "") + + if (raw_disabled) + array_to_list("${raw_disabled}" ${raw_disabled_LENGTH} disabled_platforms) + else() + set(disabled_platforms "") + endif() + + AddCIPackage( + VERSION ${version} + NAME ${name} + REPO ${repo} + PACKAGE ${package} + EXTENSION ${extension} + MIN_VERSION ${min_version} + DISABLED_PLATFORMS ${disabled_platforms} + ) + + # pass stuff to parent scope + set(${package}_ADDED "${${package}_ADDED}" + PARENT_SCOPE) + set(${package}_SOURCE_DIR "${${package}_SOURCE_DIR}" + PARENT_SCOPE) + set(${package}_BINARY_DIR "${${package}_BINARY_DIR}" + PARENT_SCOPE) + + return() + endif() + + get_json_element("${object}" hash hash "") + get_json_element("${object}" hash_suffix hash_suffix "") + get_json_element("${object}" sha sha "") + get_json_element("${object}" url url "") + get_json_element("${object}" key key "") + get_json_element("${object}" tag tag "") + get_json_element("${object}" artifact artifact "") + get_json_element("${object}" git_version git_version "") + get_json_element("${object}" git_host git_host "") + get_json_element("${object}" source_subdir source_subdir "") + get_json_element("${object}" bundled bundled "unset") + get_json_element("${object}" find_args find_args "") + get_json_element("${object}" raw_patches patches "") + + # okay here comes the fun part: REPLACEMENTS! + # first: tag gets %VERSION% replaced if applicable, with either git_version (preferred) or version + # second: artifact gets %VERSION% and %TAG% replaced accordingly (same rules for VERSION) + + if (git_version) + set(version_replace ${git_version}) + else() + set(version_replace ${version}) + endif() + + # TODO(crueter): fmt module for cmake + if (tag) + string(REPLACE "%VERSION%" "${version_replace}" tag ${tag}) + endif() + + if (artifact) + string(REPLACE "%VERSION%" "${version_replace}" artifact ${artifact}) + string(REPLACE "%TAG%" "${tag}" artifact ${artifact}) + endif() + + # format patchdir + if (raw_patches) + math(EXPR range "${raw_patches_LENGTH} - 1") + + foreach(IDX RANGE ${range}) + string(JSON _patch GET "${raw_patches}" "${IDX}") + + set(full_patch "${CMAKE_SOURCE_DIR}/.patch/${JSON_NAME}/${_patch}") + if (NOT EXISTS ${full_patch}) + cpm_utils_message(FATAL_ERROR ${JSON_NAME} "specifies patch ${full_patch} which does not exist") + endif() + + list(APPEND patches "${full_patch}") + endforeach() + endif() + # end format patchdir + + # options + get_json_element("${object}" raw_options options "") + + if (raw_options) + array_to_list("${raw_options}" ${raw_options_LENGTH} options) + endif() + + set(options ${options} ${JSON_OPTIONS}) + # end options + + # system/bundled + if (bundled STREQUAL "unset" AND DEFINED JSON_BUNDLED_PACKAGE) + set(bundled ${JSON_BUNDLED_PACKAGE}) + endif() + + AddPackage( + NAME "${package}" + VERSION "${version}" + URL "${url}" + HASH "${hash}" + HASH_SUFFIX "${hash_suffix}" + SHA "${sha}" + REPO "${repo}" + KEY "${key}" + PATCHES "${patches}" + OPTIONS "${options}" + FIND_PACKAGE_ARGUMENTS "${find_args}" + BUNDLED_PACKAGE "${bundled}" + SOURCE_SUBDIR "${source_subdir}" + + GIT_VERSION ${git_version} + GIT_HOST ${git_host} + + ARTIFACT ${artifact} + TAG ${tag} + ) + + # pass stuff to parent scope + set(${package}_ADDED "${${package}_ADDED}" + PARENT_SCOPE) + set(${package}_SOURCE_DIR "${${package}_SOURCE_DIR}" + PARENT_SCOPE) + set(${package}_BINARY_DIR "${${package}_BINARY_DIR}" + PARENT_SCOPE) + +endfunction() + +function(AddPackage) + cpm_set_policies() + + # TODO(crueter): git clone? + + #[[ + URL configurations, descending order of precedence: + - URL [+ GIT_URL] -> bare URL fetch + - REPO + TAG + ARTIFACT -> github release artifact + - REPO + TAG -> github release archive + - REPO + SHA -> github commit archive + - REPO + BRANCH -> github branch + + Hash configurations, descending order of precedence: + - HASH -> bare sha512sum + - HASH_SUFFIX -> hash grabbed from the URL + this suffix + - HASH_URL -> hash grabbed from a URL + * technically this is unsafe since a hacker can attack that url + + NOTE: hash algo defaults to sha512 + #]] + set(oneValueArgs + NAME + VERSION + GIT_VERSION + GIT_HOST + + REPO + TAG + ARTIFACT + SHA + BRANCH + + HASH + HASH_SUFFIX + HASH_URL + HASH_ALGO + + URL + GIT_URL + + KEY + BUNDLED_PACKAGE + FORCE_BUNDLED_PACKAGE + FIND_PACKAGE_ARGUMENTS + ) + + set(multiValueArgs OPTIONS PATCHES) + + cmake_parse_arguments(PKG_ARGS "" "${oneValueArgs}" "${multiValueArgs}" + "${ARGN}") + + if (NOT DEFINED PKG_ARGS_NAME) + cpm_utils_message(FATAL_ERROR "package" "No package name defined") + endif() + + option(${PKG_ARGS_NAME}_FORCE_SYSTEM "Force the system package for ${PKG_ARGS_NAME}") + option(${PKG_ARGS_NAME}_FORCE_BUNDLED "Force the bundled package for ${PKG_ARGS_NAME}") + + if (NOT DEFINED PKG_ARGS_GIT_HOST) + set(git_host github.com) + else() + set(git_host ${PKG_ARGS_GIT_HOST}) + endif() + + if (DEFINED PKG_ARGS_URL) + set(pkg_url ${PKG_ARGS_URL}) + + if (DEFINED PKG_ARGS_REPO) + set(pkg_git_url https://${git_host}/${PKG_ARGS_REPO}) + else() + if (DEFINED PKG_ARGS_GIT_URL) + set(pkg_git_url ${PKG_ARGS_GIT_URL}) + else() + set(pkg_git_url ${pkg_url}) + endif() + endif() + elseif (DEFINED PKG_ARGS_REPO) + set(pkg_git_url https://${git_host}/${PKG_ARGS_REPO}) + + if (DEFINED PKG_ARGS_TAG) + set(pkg_key ${PKG_ARGS_TAG}) + + if(DEFINED PKG_ARGS_ARTIFACT) + set(pkg_url + ${pkg_git_url}/releases/download/${PKG_ARGS_TAG}/${PKG_ARGS_ARTIFACT}) + else() + set(pkg_url + ${pkg_git_url}/archive/refs/tags/${PKG_ARGS_TAG}.tar.gz) + endif() + elseif (DEFINED PKG_ARGS_SHA) + set(pkg_url "${pkg_git_url}/archive/${PKG_ARGS_SHA}.zip") + else() + if (DEFINED PKG_ARGS_BRANCH) + set(PKG_BRANCH ${PKG_ARGS_BRANCH}) + else() + cpm_utils_message(WARNING ${PKG_ARGS_NAME} + "REPO defined but no TAG, SHA, BRANCH, or URL specified, defaulting to master") + set(PKG_BRANCH master) + endif() + + set(pkg_url ${pkg_git_url}/archive/refs/heads/${PKG_BRANCH}.zip) + endif() + else() + cpm_utils_message(FATAL_ERROR ${PKG_ARGS_NAME} "No URL or repository defined") + endif() + + cpm_utils_message(STATUS ${PKG_ARGS_NAME} "Download URL is ${pkg_url}") + + if (NOT DEFINED PKG_ARGS_KEY) + if (DEFINED PKG_ARGS_SHA) + string(SUBSTRING ${PKG_ARGS_SHA} 0 4 pkg_key) + cpm_utils_message(DEBUG ${PKG_ARGS_NAME} + "No custom key defined, using ${pkg_key} from sha") + elseif(DEFINED PKG_ARGS_GIT_VERSION) + set(pkg_key ${PKG_ARGS_GIT_VERSION}) + cpm_utils_message(DEBUG ${PKG_ARGS_NAME} + "No custom key defined, using ${pkg_key}") + elseif (DEFINED PKG_ARGS_TAG) + set(pkg_key ${PKG_ARGS_TAG}) + cpm_utils_message(DEBUG ${PKG_ARGS_NAME} + "No custom key defined, using ${pkg_key}") + elseif (DEFINED PKG_ARGS_VERSION) + set(pkg_key ${PKG_ARGS_VERSION}) + cpm_utils_message(DEBUG ${PKG_ARGS_NAME} + "No custom key defined, using ${pkg_key}") + else() + cpm_utils_message(WARNING ${PKG_ARGS_NAME} + "Could not determine cache key, using CPM defaults") + endif() + else() + set(pkg_key ${PKG_ARGS_KEY}) + endif() + + if (DEFINED PKG_ARGS_HASH_ALGO) + set(hash_algo ${PKG_ARGS_HASH_ALGO}) + else() + set(hash_algo SHA512) + endif() + + if (DEFINED PKG_ARGS_HASH) + set(pkg_hash "${hash_algo}=${PKG_ARGS_HASH}") + elseif (DEFINED PKG_ARGS_HASH_SUFFIX) + # funny sanity check + string(TOLOWER ${hash_algo} hash_algo_lower) + string(TOLOWER ${PKG_ARGS_HASH_SUFFIX} suffix_lower) + if (NOT ${suffix_lower} MATCHES ${hash_algo_lower}) + cpm_utils_message(WARNING + "Hash algorithm and hash suffix do not match, errors may occur") + endif() + + set(hash_url ${pkg_url}.${PKG_ARGS_HASH_SUFFIX}) + elseif (DEFINED PKG_ARGS_HASH_URL) + set(hash_url ${PKG_ARGS_HASH_URL}) + else() + cpm_utils_message(WARNING ${PKG_ARGS_NAME} + "No hash or hash URL found") + endif() + + if (DEFINED hash_url) + set(outfile ${CMAKE_CURRENT_BINARY_DIR}/${PKG_ARGS_NAME}.hash) + + # TODO(crueter): This is kind of a bad solution + # because "technically" the hash is invalidated each week + # but it works for now kjsdnfkjdnfjksdn + string(TOLOWER ${PKG_ARGS_NAME} lowername) + if (NOT EXISTS ${outfile} AND NOT EXISTS ${CPM_SOURCE_CACHE}/${lowername}/${pkg_key}) + file(DOWNLOAD ${hash_url} ${outfile}) + endif() + + if (EXISTS ${outfile}) + file(READ ${outfile} pkg_hash_tmp) + endif() + + if (DEFINED ${pkg_hash_tmp}) + set(pkg_hash "${hash_algo}=${pkg_hash_tmp}") + endif() + endif() + + macro(set_precedence local force) + set(CPM_USE_LOCAL_PACKAGES ${local}) + set(CPM_LOCAL_PACKAGES_ONLY ${force}) + endmacro() + + #[[ + Precedence: + - package_FORCE_SYSTEM + - package_FORCE_BUNDLED + - CPMUTIL_FORCE_SYSTEM + - CPMUTIL_FORCE_BUNDLED + - BUNDLED_PACKAGE + - default to allow local + ]]# + if (PKG_ARGS_FORCE_BUNDLED_PACKAGE) + set_precedence(OFF OFF) + elseif (${PKG_ARGS_NAME}_FORCE_SYSTEM) + set_precedence(ON ON) + elseif (${PKG_ARGS_NAME}_FORCE_BUNDLED) + set_precedence(OFF OFF) + elseif (CPMUTIL_FORCE_SYSTEM) + set_precedence(ON ON) + elseif(CPMUTIL_FORCE_BUNDLED) + set_precedence(OFF OFF) + elseif (DEFINED PKG_ARGS_BUNDLED_PACKAGE AND NOT PKG_ARGS_BUNDLED_PACKAGE STREQUAL "unset") + if (PKG_ARGS_BUNDLED_PACKAGE) + set(local OFF) + else() + set(local ON) + endif() + + set_precedence(${local} OFF) + else() + set_precedence(ON OFF) + endif() + + if (DEFINED PKG_ARGS_VERSION) + list(APPEND EXTRA_ARGS + VERSION ${PKG_ARGS_VERSION} + ) + endif() + + CPMAddPackage( + NAME ${PKG_ARGS_NAME} + URL ${pkg_url} + URL_HASH ${pkg_hash} + CUSTOM_CACHE_KEY ${pkg_key} + DOWNLOAD_ONLY ${PKG_ARGS_DOWNLOAD_ONLY} + FIND_PACKAGE_ARGUMENTS ${PKG_ARGS_FIND_PACKAGE_ARGUMENTS} + + OPTIONS ${PKG_ARGS_OPTIONS} + PATCHES ${PKG_ARGS_PATCHES} + EXCLUDE_FROM_ALL ON + + ${EXTRA_ARGS} + + ${PKG_ARGS_UNPARSED_ARGUMENTS} + ) + + set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_NAMES ${PKG_ARGS_NAME}) + set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_URLS ${pkg_git_url}) + + if (${PKG_ARGS_NAME}_ADDED) + if (DEFINED PKG_ARGS_SHA) + set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_SHAS + ${PKG_ARGS_SHA}) + elseif (DEFINED PKG_ARGS_GIT_VERSION) + set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_SHAS + ${PKG_ARGS_GIT_VERSION}) + elseif (DEFINED PKG_ARGS_TAG) + set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_SHAS + ${PKG_ARGS_TAG}) + elseif(DEFINED PKG_ARGS_VERSION) + set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_SHAS + ${PKG_ARGS_VERSION}) + else() + cpm_utils_message(WARNING ${PKG_ARGS_NAME} + "Package has no specified sha, tag, or version") + set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_SHAS "unknown") + endif() + else() + if (DEFINED CPM_PACKAGE_${PKG_ARGS_NAME}_VERSION AND NOT + "${CPM_PACKAGE_${PKG_ARGS_NAME}_VERSION}" STREQUAL "") + set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_SHAS + "${CPM_PACKAGE_${PKG_ARGS_NAME}_VERSION} (system)") + else() + set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_SHAS + "unknown (system)") + endif() + endif() + + # pass stuff to parent scope + set(${PKG_ARGS_NAME}_ADDED "${${PKG_ARGS_NAME}_ADDED}" + PARENT_SCOPE) + set(${PKG_ARGS_NAME}_SOURCE_DIR "${${PKG_ARGS_NAME}_SOURCE_DIR}" + PARENT_SCOPE) + set(${PKG_ARGS_NAME}_BINARY_DIR "${${PKG_ARGS_NAME}_BINARY_DIR}" + PARENT_SCOPE) + +endfunction() + +function(add_ci_package key) + set(ARTIFACT ${ARTIFACT_NAME}-${key}-${ARTIFACT_VERSION}.${ARTIFACT_EXT}) + + AddPackage( + NAME ${ARTIFACT_PACKAGE} + REPO ${ARTIFACT_REPO} + TAG v${ARTIFACT_VERSION} + GIT_VERSION ${ARTIFACT_VERSION} + ARTIFACT ${ARTIFACT} + + KEY ${key}-${ARTIFACT_VERSION} + HASH_SUFFIX sha512sum + FORCE_BUNDLED_PACKAGE ON + ) + + set(ARTIFACT_DIR ${${ARTIFACT_PACKAGE}_SOURCE_DIR} PARENT_SCOPE) +endfunction() + +# TODO(crueter): we could do an AddMultiArchPackage, multiplatformpackage? +# name is the artifact name, package is for find_package override +function(AddCIPackage) + set(oneValueArgs + VERSION + NAME + REPO + PACKAGE + EXTENSION + MIN_VERSION + DISABLED_PLATFORMS + ) + + cmake_parse_arguments(PKG_ARGS "" "${oneValueArgs}" "" ${ARGN}) + + if(NOT DEFINED PKG_ARGS_VERSION) + message(FATAL_ERROR "[CPMUtil] VERSION is required") + endif() + if(NOT DEFINED PKG_ARGS_NAME) + message(FATAL_ERROR "[CPMUtil] NAME is required") + endif() + if(NOT DEFINED PKG_ARGS_REPO) + message(FATAL_ERROR "[CPMUtil] REPO is required") + endif() + if(NOT DEFINED PKG_ARGS_PACKAGE) + message(FATAL_ERROR "[CPMUtil] PACKAGE is required") + endif() + + if (NOT DEFINED PKG_ARGS_CMAKE_FILENAME) + set(ARTIFACT_CMAKE ${PKG_ARGS_NAME}) + else() + set(ARTIFACT_CMAKE ${PKG_ARGS_CMAKE_FILENAME}) + endif() + + if(NOT DEFINED PKG_ARGS_EXTENSION) + set(ARTIFACT_EXT "tar.zst") + else() + set(ARTIFACT_EXT ${PKG_ARGS_EXTENSION}) + endif() + + if (DEFINED PKG_ARGS_MIN_VERSION) + set(ARTIFACT_MIN_VERSION ${PKG_ARGS_MIN_VERSION}) + endif() + + if (DEFINED PKG_ARGS_DISABLED_PLATFORMS) + set(DISABLED_PLATFORMS ${PKG_ARGS_DISABLED_PLATFORMS}) + endif() + + # this is mildly annoying + set(ARTIFACT_VERSION ${PKG_ARGS_VERSION}) + set(ARTIFACT_NAME ${PKG_ARGS_NAME}) + set(ARTIFACT_REPO ${PKG_ARGS_REPO}) + set(ARTIFACT_PACKAGE ${PKG_ARGS_PACKAGE}) + + if ((MSVC AND ARCHITECTURE_x86_64) AND NOT "windows-amd64" IN_LIST DISABLED_PLATFORMS) + add_ci_package(windows-amd64) + endif() + + if ((MSVC AND ARCHITECTURE_arm64) AND NOT "windows-arm64" IN_LIST DISABLED_PLATFORMS) + add_ci_package(windows-arm64) + endif() + + if (ANDROID AND NOT "android" IN_LIST DISABLED_PLATFORMS) + add_ci_package(android) + endif() + + if(PLATFORM_SUN AND NOT "solaris-amd64" IN_LIST DISABLED_PLATFORMS) + add_ci_package(solaris-amd64) + endif() + + if(PLATFORM_FREEBSD AND NOT "freebsd-amd64" IN_LIST DISABLED_PLATFORMS) + add_ci_package(freebsd-amd64) + endif() + + if((PLATFORM_LINUX AND ARCHITECTURE_x86_64) AND NOT "linux-amd64" IN_LIST DISABLED_PLATFORMS) + add_ci_package(linux-amd64) + endif() + + if((PLATFORM_LINUX AND ARCHITECTURE_arm64) AND NOT "linux-aarch64" IN_LIST DISABLED_PLATFORMS) + add_ci_package(linux-aarch64) + endif() + + # TODO(crueter): macOS amd64/aarch64 split mayhaps + if (APPLE AND NOT "macos-universal" IN_LIST DISABLED_PLATFORMS) + add_ci_package(macos-universal) + endif() + + if (DEFINED ARTIFACT_DIR) + set(${ARTIFACT_PACKAGE}_ADDED TRUE PARENT_SCOPE) + set(${ARTIFACT_PACKAGE}_SOURCE_DIR "${ARTIFACT_DIR}" PARENT_SCOPE) + else() + find_package(${ARTIFACT_PACKAGE} ${ARTIFACT_MIN_VERSION} REQUIRED) + endif() +endfunction() diff --git a/CMakeModules/CopyYuzuQt6Deps.cmake b/CMakeModules/CopyYuzuQt6Deps.cmake index c6a5fa2db2..5ea8f74233 100644 --- a/CMakeModules/CopyYuzuQt6Deps.cmake +++ b/CMakeModules/CopyYuzuQt6Deps.cmake @@ -16,7 +16,7 @@ function(copy_yuzu_Qt6_deps target_dir) set(PLATFORMS ${DLL_DEST}plugins/platforms/) set(STYLES ${DLL_DEST}plugins/styles/) set(IMAGEFORMATS ${DLL_DEST}plugins/imageformats/) - + set(RESOURCES ${DLL_DEST}resources/) if (MSVC) windows_copy_files(${target_dir} ${Qt6_DLL_DIR} ${DLL_DEST} Qt6Core$<$:d>.* @@ -31,20 +31,31 @@ function(copy_yuzu_Qt6_deps target_dir) endif() if (YUZU_USE_QT_WEB_ENGINE) windows_copy_files(${target_dir} ${Qt6_DLL_DIR} ${DLL_DEST} + Qt6OpenGL$<$:d>.* + Qt6Positioning$<$:d>.* + Qt6PrintSupport$<$:d>.* + Qt6Qml$<$:d>.* + Qt6QmlMeta$<$:d>.* + Qt6QmlModels$<$:d>.* + Qt6QmlWorkerScript$<$:d>.* + Qt6Quick$<$:d>.* + Qt6QuickWidgets$<$:d>.* + Qt6WebChannel$<$:d>.* Qt6WebEngineCore$<$:d>.* Qt6WebEngineWidgets$<$:d>.* - QtWebEngineProcess$<$:d>.* + QtWebEngineProcess$<$:d>.* ) - windows_copy_files(${target_dir} ${Qt6_RESOURCES_DIR} ${DLL_DEST} + windows_copy_files(${target_dir} ${Qt6_RESOURCES_DIR} ${RESOURCES} icudtl.dat qtwebengine_devtools_resources.pak qtwebengine_resources.pak qtwebengine_resources_100p.pak qtwebengine_resources_200p.pak + v8_context_snapshot.bin ) endif() windows_copy_files(yuzu ${Qt6_PLATFORMS_DIR} ${PLATFORMS} qwindows$<$:d>.*) - windows_copy_files(yuzu ${Qt6_STYLES_DIR} ${STYLES} qwindowsvistastyle$<$:d>.*) + windows_copy_files(yuzu ${Qt6_STYLES_DIR} ${STYLES} qmodernwindowsstyle$<$:d>.*) windows_copy_files(yuzu ${Qt6_IMAGEFORMATS_DIR} ${IMAGEFORMATS} qjpeg$<$:d>.* qgif$<$:d>.* @@ -52,9 +63,4 @@ function(copy_yuzu_Qt6_deps target_dir) else() # Update for non-MSVC platforms if needed endif() - - # Create an empty qt.conf file - add_custom_command(TARGET yuzu POST_BUILD - COMMAND ${CMAKE_COMMAND} -E touch ${DLL_DEST}qt.conf - ) endfunction(copy_yuzu_Qt6_deps) diff --git a/CMakeModules/DownloadExternals.cmake b/CMakeModules/DownloadExternals.cmake index 3fe15a16c4..f6e3aaa4ad 100644 --- a/CMakeModules/DownloadExternals.cmake +++ b/CMakeModules/DownloadExternals.cmake @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project # SPDX-License-Identifier: GPL-3.0-or-later # This function downloads a binary library package from our external repo. @@ -6,54 +6,55 @@ # remote_path: path to the file to download, relative to the remote repository root # prefix_var: name of a variable which will be set with the path to the extracted contents set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR}) -function(download_bundled_external remote_path lib_name prefix_var) +function(download_bundled_external remote_path lib_name cpm_key prefix_var version) + set(package_base_url "https://github.com/eden-emulator/") + set(package_repo "no_platform") + set(package_extension "no_platform") + set(CACHE_KEY "") -set(package_base_url "https://github.com/eden-emulator/") -set(package_repo "no_platform") -set(package_extension "no_platform") -if (WIN32) - set(package_repo "ext-windows-bin/raw/master/") - set(package_extension ".7z") -elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") - set(package_repo "ext-linux-bin/raw/main/") - set(package_extension ".tar.xz") -elseif (ANDROID) - set(package_repo "ext-android-bin/raw/main/") - set(package_extension ".tar.xz") -else() - message(FATAL_ERROR "No package available for this platform") -endif() -set(package_url "${package_base_url}${package_repo}") - -set(prefix "${CMAKE_BINARY_DIR}/externals/${lib_name}") -if (NOT EXISTS "${prefix}") - message(STATUS "Downloading binaries for ${lib_name}...") - file(DOWNLOAD - ${package_url}${remote_path}${lib_name}${package_extension} - "${CMAKE_BINARY_DIR}/externals/${lib_name}${package_extension}" SHOW_PROGRESS) - execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${CMAKE_BINARY_DIR}/externals/${lib_name}${package_extension}" - WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals") -endif() -message(STATUS "Using bundled binaries at ${prefix}") -set(${prefix_var} "${prefix}" PARENT_SCOPE) -endfunction() - -function(download_moltenvk_external platform version) - set(MOLTENVK_DIR "${CMAKE_BINARY_DIR}/externals/MoltenVK") - set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar") - if (NOT EXISTS ${MOLTENVK_DIR}) - if (NOT EXISTS ${MOLTENVK_TAR}) - file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/download/${version}/MoltenVK-${platform}.tar - ${MOLTENVK_TAR} SHOW_PROGRESS) + # TODO(crueter): Need to convert ffmpeg to a CI. + if (WIN32 OR FORCE_WIN_ARCHIVES) + if (ARCHITECTURE_arm64) + set(CACHE_KEY "windows") + set(package_repo "ext-windows-arm64-bin/raw/master/") + set(package_extension ".zip") + elseif(ARCHITECTURE_x86_64) + set(CACHE_KEY "windows") + set(package_repo "ext-windows-bin/raw/master/") + set(package_extension ".7z") endif() - - execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${MOLTENVK_TAR}" - WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals") + elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + set(CACHE_KEY "linux") + set(package_repo "ext-linux-bin/raw/master/") + set(package_extension ".tar.xz") + elseif (ANDROID) + set(CACHE_KEY "android") + set(package_repo "ext-android-bin/raw/master/") + set(package_extension ".tar.xz") + else() + message(FATAL_ERROR "No package available for this platform") endif() + string(CONCAT package_url "${package_base_url}" "${package_repo}") + string(CONCAT full_url "${package_url}" "${remote_path}" "${lib_name}" "${package_extension}") + message(STATUS "Resolved bundled URL: ${full_url}") - # Add the MoltenVK library path to the prefix so find_library can locate it. - list(APPEND CMAKE_PREFIX_PATH "${MOLTENVK_DIR}/MoltenVK/dylib/${platform}") - set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE) + # TODO(crueter): DELETE THIS ENTIRELY, GLORY BE TO THE CI! + AddPackage( + NAME ${cpm_key} + VERSION ${version} + URL ${full_url} + DOWNLOAD_ONLY YES + KEY ${CACHE_KEY} + BUNDLED_PACKAGE ON + # TODO(crueter): hash + ) + + if (DEFINED ${cpm_key}_SOURCE_DIR) + set(${prefix_var} "${${cpm_key}_SOURCE_DIR}" PARENT_SCOPE) + message(STATUS "Using bundled binaries at ${${cpm_key}_SOURCE_DIR}") + else() + message(FATAL_ERROR "AddPackage did not set ${cpm_key}_SOURCE_DIR") + endif() endfunction() # Determine installation parameters for OS, architecture, and compiler @@ -68,22 +69,22 @@ function(determine_qt_parameters target host_out type_out arch_out arch_path_out set(arch_path "mingw_64") elseif (MSVC) if ("arm64" IN_LIST ARCHITECTURE) - set(arch_path "msvc2019_arm64") + set(arch_path "msvc2022_arm64") elseif ("x86_64" IN_LIST ARCHITECTURE) - set(arch_path "msvc2019_64") + set(arch_path "msvc2022_64") else() - message(FATAL_ERROR "Unsupported bundled Qt architecture. Enable USE_SYSTEM_QT and provide your own.") + message(FATAL_ERROR "Unsupported bundled Qt architecture. Disable YUZU_USE_BUNDLED_QT and provide your own.") endif() set(arch "win64_${arch_path}") if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "AMD64") - set(host_arch_path "msvc2019_64") + set(host_arch_path "msvc2022_64") elseif (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "ARM64") - set(host_arch_path "msvc2019_64") + set(host_arch_path "msvc2022_arm64") endif() set(host_arch "win64_${host_arch_path}") else() - message(FATAL_ERROR "Unsupported bundled Qt toolchain. Enable USE_SYSTEM_QT and provide your own.") + message(FATAL_ERROR "Unsupported bundled Qt toolchain. Disable YUZU_USE_BUNDLED_QT and provide your own.") endif() endif() elseif (APPLE) @@ -94,8 +95,8 @@ function(determine_qt_parameters target host_out type_out arch_out arch_path_out else() set(host "linux") set(type "desktop") - set(arch "gcc_64") - set(arch_path "linux") + set(arch "linux_gcc_64") + set(arch_path "gcc_64") endif() set(${host_out} "${host}" PARENT_SCOPE) @@ -130,43 +131,79 @@ function(download_qt_configuration prefix_out target host type arch arch_path ba set(install_args -c "${CURRENT_MODULE_DIR}/aqt_config.ini") if (tool) set(prefix "${base_path}/Tools") - set(install_args ${install_args} install-tool --outputdir ${base_path} ${host} desktop ${target}) + list(APPEND install_args install-tool --outputdir "${base_path}" "${host}" desktop "${target}") else() set(prefix "${base_path}/${target}/${arch_path}") - set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} - -m qtmultimedia --archives qttranslations qttools qtsvg qtbase) + list(APPEND install_args install-qt --outputdir "${base_path}" "${host}" "${type}" "${target}" "${arch}" -m qt_base) + + if (YUZU_USE_QT_MULTIMEDIA) + list(APPEND install_args qtmultimedia) + endif() + + if (YUZU_USE_QT_WEB_ENGINE) + list(APPEND install_args qtpositioning qtwebchannel qtwebengine) + endif() + + if (NOT "${YUZU_QT_MIRROR}" STREQUAL "") + message(STATUS "Using Qt mirror ${YUZU_QT_MIRROR}") + list(APPEND install_args -b "${YUZU_QT_MIRROR}") + endif() endif() + message(STATUS "Install Args: ${install_args}") + if (NOT EXISTS "${prefix}") message(STATUS "Downloading Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path}") - set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.1.18") + set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.3.0") if (WIN32) set(aqt_path "${base_path}/aqt.exe") if (NOT EXISTS "${aqt_path}") - file(DOWNLOAD - ${AQT_PREBUILD_BASE_URL}/aqt.exe - ${aqt_path} SHOW_PROGRESS) + file(DOWNLOAD "${AQT_PREBUILD_BASE_URL}/aqt.exe" "${aqt_path}" SHOW_PROGRESS) + endif() + execute_process(COMMAND "${aqt_path}" ${install_args} + WORKING_DIRECTORY "${base_path}" + RESULT_VARIABLE aqt_res + OUTPUT_VARIABLE aqt_out + ERROR_VARIABLE aqt_err) + if (NOT aqt_res EQUAL 0) + message(FATAL_ERROR "aqt.exe failed: ${aqt_err}") endif() - execute_process(COMMAND ${aqt_path} ${install_args} - WORKING_DIRECTORY ${base_path}) elseif (APPLE) set(aqt_path "${base_path}/aqt-macos") if (NOT EXISTS "${aqt_path}") - file(DOWNLOAD - ${AQT_PREBUILD_BASE_URL}/aqt-macos - ${aqt_path} SHOW_PROGRESS) + file(DOWNLOAD "${AQT_PREBUILD_BASE_URL}/aqt-macos" "${aqt_path}" SHOW_PROGRESS) + endif() + execute_process(COMMAND chmod +x "${aqt_path}") + execute_process(COMMAND "${aqt_path}" ${install_args} + WORKING_DIRECTORY "${base_path}" + RESULT_VARIABLE aqt_res + ERROR_VARIABLE aqt_err) + if (NOT aqt_res EQUAL 0) + message(FATAL_ERROR "aqt-macos failed: ${aqt_err}") endif() - execute_process(COMMAND chmod +x ${aqt_path}) - execute_process(COMMAND ${aqt_path} ${install_args} - WORKING_DIRECTORY ${base_path}) else() + find_program(PYTHON3_EXECUTABLE python3) + if (NOT PYTHON3_EXECUTABLE) + message(FATAL_ERROR "python3 is required to install Qt using aqt (pip mode).") + endif() set(aqt_install_path "${base_path}/aqt") file(MAKE_DIRECTORY "${aqt_install_path}") - execute_process(COMMAND python3 -m pip install --target=${aqt_install_path} aqtinstall - WORKING_DIRECTORY ${base_path}) - execute_process(COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${aqt_install_path} python3 -m aqt ${install_args} - WORKING_DIRECTORY ${base_path}) + execute_process(COMMAND "${PYTHON3_EXECUTABLE}" -m pip install --target="${aqt_install_path}" aqtinstall + WORKING_DIRECTORY "${base_path}" + RESULT_VARIABLE pip_res + ERROR_VARIABLE pip_err) + if (NOT pip_res EQUAL 0) + message(FATAL_ERROR "pip install aqtinstall failed: ${pip_err}") + endif() + + execute_process(COMMAND "${CMAKE_COMMAND}" -E env PYTHONPATH="${aqt_install_path}" "${PYTHON3_EXECUTABLE}" -m aqt ${install_args} + WORKING_DIRECTORY "${base_path}" + RESULT_VARIABLE aqt_res + ERROR_VARIABLE aqt_err) + if (NOT aqt_res EQUAL 0) + message(FATAL_ERROR "aqt (python) failed: ${aqt_err}") + endif() endif() message(STATUS "Downloaded Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path} to ${prefix}") @@ -184,7 +221,7 @@ endfunction() function(download_qt target) determine_qt_parameters("${target}" host type arch arch_path host_type host_arch host_arch_path) - get_external_prefix(qt base_path) + set(base_path "${CMAKE_BINARY_DIR}/externals/qt") file(MAKE_DIRECTORY "${base_path}") download_qt_configuration(prefix "${target}" "${host}" "${type}" "${arch}" "${arch_path}" "${base_path}") @@ -201,26 +238,34 @@ function(download_qt target) set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE) endfunction() -function(download_moltenvk) -set(MOLTENVK_PLATFORM "macOS") +function(download_moltenvk version platform) + if(NOT version) + message(FATAL_ERROR "download_moltenvk: version argument is required") + endif() + if(NOT platform) + message(FATAL_ERROR "download_moltenvk: platform argument is required") + endif() -set(MOLTENVK_DIR "${CMAKE_BINARY_DIR}/externals/MoltenVK") -set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar") -if (NOT EXISTS ${MOLTENVK_DIR}) -if (NOT EXISTS ${MOLTENVK_TAR}) - file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/download/v1.2.10-rc2/MoltenVK-all.tar - ${MOLTENVK_TAR} SHOW_PROGRESS) -endif() + set(MOLTENVK_DIR "${CMAKE_BINARY_DIR}/externals/MoltenVK") + set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar") -execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${MOLTENVK_TAR}" - WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals") -endif() + if(NOT EXISTS "${MOLTENVK_DIR}") + if(NOT EXISTS "${MOLTENVK_TAR}") + file(DOWNLOAD "https://github.com/KhronosGroup/MoltenVK/releases/download/${version}/MoltenVK-${platform}.tar" + "${MOLTENVK_TAR}" SHOW_PROGRESS) + endif() -# Add the MoltenVK library path to the prefix so find_library can locate it. -list(APPEND CMAKE_PREFIX_PATH "${MOLTENVK_DIR}/MoltenVK/dylib/${MOLTENVK_PLATFORM}") -set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE) + execute_process( + COMMAND ${CMAKE_COMMAND} -E tar xf "${MOLTENVK_TAR}" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals" + RESULT_VARIABLE tar_res + ERROR_VARIABLE tar_err + ) + if(NOT tar_res EQUAL 0) + message(FATAL_ERROR "Extracting MoltenVK failed: ${tar_err}") + endif() + endif() + list(APPEND CMAKE_PREFIX_PATH "${MOLTENVK_DIR}/MoltenVK/dylib/${platform}") + set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE) endfunction() -function(get_external_prefix lib_name prefix_var) - set(${prefix_var} "${CMAKE_BINARY_DIR}/externals/${lib_name}" PARENT_SCOPE) -endfunction() diff --git a/CMakeModules/FindLLVM.cmake b/CMakeModules/FindLLVM.cmake index efbd0ca460..8dc064d5d1 100644 --- a/CMakeModules/FindLLVM.cmake +++ b/CMakeModules/FindLLVM.cmake @@ -19,7 +19,7 @@ if (LLVM_FOUND AND LLVM_Demangle_FOUND AND NOT TARGET LLVM::Demangle) target_include_directories(LLVM::Demangle INTERFACE ${LLVM_INCLUDE_DIRS}) # prefer shared LLVM: https://github.com/llvm/llvm-project/issues/34593 # but use ugly hack because llvm_config doesn't support interface library - add_library(_dummy_lib SHARED EXCLUDE_FROM_ALL src/yuzu/main.cpp) + add_library(_dummy_lib SHARED EXCLUDE_FROM_ALL ${CMAKE_SOURCE_DIR}/src/yuzu/main.cpp) llvm_config(_dummy_lib USE_SHARED demangle) get_target_property(LLVM_LIBRARIES _dummy_lib LINK_LIBRARIES) target_link_libraries(LLVM::Demangle INTERFACE ${LLVM_LIBRARIES}) diff --git a/CMakeModules/FindSPIRV-Tools.cmake b/CMakeModules/FindSPIRV-Tools.cmake new file mode 100644 index 0000000000..aef74df5d9 --- /dev/null +++ b/CMakeModules/FindSPIRV-Tools.cmake @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2022 yuzu Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + +include(FindPackageHandleStandardArgs) + +find_package(PkgConfig QUIET) +pkg_search_module(SPIRV-Tools QUIET IMPORTED_TARGET SPIRV-Tools) +find_package_handle_standard_args(SPIRV-Tools + REQUIRED_VARS SPIRV-Tools_LINK_LIBRARIES + VERSION_VAR SPIRV-Tools_VERSION +) + +if (SPIRV-Tools_FOUND AND NOT TARGET SPIRV-Tools::SPIRV-Tools) + if (TARGET SPIRV-Tools) + add_library(SPIRV-Tools::SPIRV-Tools ALIAS SPIRV-Tools) + else() + add_library(SPIRV-Tools::SPIRV-Tools ALIAS PkgConfig::SPIRV-Tools) + endif() +endif() diff --git a/CMakeModules/Findlibiw.cmake b/CMakeModules/Findlibiw.cmake new file mode 100644 index 0000000000..1d13d7705a --- /dev/null +++ b/CMakeModules/Findlibiw.cmake @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later + +find_package(PkgConfig QUIET) +pkg_search_module(IW QUIET IMPORTED_TARGET iw) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Libiw + REQUIRED_VARS IW_LIBRARIES IW_INCLUDE_DIRS + VERSION_VAR IW_VERSION + FAIL_MESSAGE "libiw (Wireless Tools library) not found. Please install libiw-dev (Debian/Ubuntu), wireless-tools-devel (Fedora), wireless_tools (Arch), or net-wireless/iw (Gentoo)." +) + +if (Libiw_FOUND AND TARGET PkgConfig::IW AND NOT TARGET iw::iw) + add_library(iw::iw ALIAS PkgConfig::IW) +endif() + +if(Libiw_FOUND) + mark_as_advanced( + IW_INCLUDE_DIRS + IW_LIBRARIES + IW_LIBRARY_DIRS + IW_LINK_LIBRARIES # Often set by pkg-config + IW_LDFLAGS + IW_CFLAGS + IW_CFLAGS_OTHER + IW_VERSION + ) +endif() diff --git a/CMakeModules/Findzstd.cmake b/CMakeModules/Findzstd.cmake index ae3ea08653..17efec2192 100644 --- a/CMakeModules/Findzstd.cmake +++ b/CMakeModules/Findzstd.cmake @@ -3,24 +3,22 @@ include(FindPackageHandleStandardArgs) -find_package(zstd QUIET CONFIG) -if (zstd_CONSIDERED_CONFIGS) - find_package_handle_standard_args(zstd CONFIG_MODE) -else() - find_package(PkgConfig QUIET) - pkg_search_module(ZSTD QUIET IMPORTED_TARGET libzstd) - find_package_handle_standard_args(zstd - REQUIRED_VARS ZSTD_LINK_LIBRARIES - VERSION_VAR ZSTD_VERSION - ) -endif() +find_package(PkgConfig QUIET) +pkg_search_module(ZSTD QUIET IMPORTED_TARGET libzstd) +find_package_handle_standard_args(zstd + REQUIRED_VARS ZSTD_LINK_LIBRARIES + VERSION_VAR ZSTD_VERSION +) if (zstd_FOUND AND NOT TARGET zstd::zstd) if (TARGET zstd::libzstd_shared) add_library(zstd::zstd ALIAS zstd::libzstd_shared) + add_library(zstd::libzstd ALIAS zstd::libzstd_shared) elseif (TARGET zstd::libzstd_static) add_library(zstd::zstd ALIAS zstd::libzstd_static) + add_library(zstd::libzstd ALIAS zstd::libzstd_static) else() add_library(zstd::zstd ALIAS PkgConfig::ZSTD) + add_library(zstd::libzstd ALIAS PkgConfig::ZSTD) endif() endif() diff --git a/CMakeModules/GenerateDepHashes.cmake b/CMakeModules/GenerateDepHashes.cmake new file mode 100644 index 0000000000..d0d59bd22f --- /dev/null +++ b/CMakeModules/GenerateDepHashes.cmake @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later + +get_property(NAMES GLOBAL PROPERTY CPM_PACKAGE_NAMES) +get_property(SHAS GLOBAL PROPERTY CPM_PACKAGE_SHAS) +get_property(URLS GLOBAL PROPERTY CPM_PACKAGE_URLS) + +list(LENGTH NAMES DEPS_LENGTH) + +list(JOIN NAMES "\",\n\t\"" DEP_NAME_DIRTY) +set(DEP_NAMES "\t\"${DEP_NAME_DIRTY}\"") + +list(JOIN SHAS "\",\n\t\"" DEP_SHAS_DIRTY) +set(DEP_SHAS "\t\"${DEP_SHAS_DIRTY}\"") + +list(JOIN URLS "\",\n\t\"" DEP_URLS_DIRTY) +set(DEP_URLS "\t\"${DEP_URLS_DIRTY}\"") + +configure_file(dep_hashes.h.in dep_hashes.h @ONLY) +target_sources(common PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/dep_hashes.h) +target_include_directories(common PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/CMakeModules/GenerateSCMRev.cmake b/CMakeModules/GenerateSCMRev.cmake index 1d4aa979d3..1ae608c085 100644 --- a/CMakeModules/GenerateSCMRev.cmake +++ b/CMakeModules/GenerateSCMRev.cmake @@ -1,56 +1,41 @@ +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later + # SPDX-FileCopyrightText: 2019 yuzu Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later -# Gets a UTC timestamp and sets the provided variable to it +# generate git/build information +include(GetSCMRev) + function(get_timestamp _var) string(TIMESTAMP timestamp UTC) set(${_var} "${timestamp}" PARENT_SCOPE) endfunction() -# generate git/build information -include(GetGitRevisionDescription) -if(NOT GIT_REF_SPEC) - get_git_head_revision(GIT_REF_SPEC GIT_REV) -endif() -if(NOT GIT_DESC) - git_describe(GIT_DESC --always --long --dirty) -endif() -if (NOT GIT_BRANCH) - git_branch_name(GIT_BRANCH) -endif() get_timestamp(BUILD_DATE) +if (DEFINED GIT_RELEASE) + set(BUILD_VERSION "${GIT_TAG}") + set(GIT_REFSPEC "${GIT_RELEASE}") + set(IS_DEV_BUILD false) +else() + string(SUBSTRING ${GIT_COMMIT} 0 10 BUILD_VERSION) + set(BUILD_VERSION "${BUILD_VERSION}-${GIT_REFSPEC}") + set(IS_DEV_BUILD true) +endif() + +set(GIT_DESC ${BUILD_VERSION}) + # Generate cpp with Git revision from template # Also if this is a CI build, add the build name (ie: Nightly, Canary) to the scm_rev file as well -set(REPO_NAME "") -set(BUILD_VERSION "0") -set(BUILD_ID ${DISPLAY_VERSION}) -if (BUILD_REPOSITORY) - # regex capture the string nightly or canary into CMAKE_MATCH_1 - string(REGEX MATCH "yuzu-emu/yuzu-?(.*)" OUTVAR ${BUILD_REPOSITORY}) - if ("${CMAKE_MATCH_COUNT}" GREATER 0) - # capitalize the first letter of each word in the repo name. - string(REPLACE "-" ";" REPO_NAME_LIST ${CMAKE_MATCH_1}) - foreach(WORD ${REPO_NAME_LIST}) - string(SUBSTRING ${WORD} 0 1 FIRST_LETTER) - string(SUBSTRING ${WORD} 1 -1 REMAINDER) - string(TOUPPER ${FIRST_LETTER} FIRST_LETTER) - set(REPO_NAME "${REPO_NAME}${FIRST_LETTER}${REMAINDER}") - endforeach() - if (BUILD_TAG) - string(REGEX MATCH "${CMAKE_MATCH_1}-([0-9]+)" OUTVAR ${BUILD_TAG}) - if (${CMAKE_MATCH_COUNT} GREATER 0) - set(BUILD_VERSION ${CMAKE_MATCH_1}) - endif() - if (BUILD_VERSION) - # This leaves a trailing space on the last word, but we actually want that - # because of how it's styled in the title bar. - set(BUILD_FULLNAME "${REPO_NAME} ${BUILD_VERSION} ") - else() - set(BUILD_FULLNAME "") - endif() - endif() - endif() -endif() +set(REPO_NAME "Eden") +set(BUILD_ID ${GIT_REFSPEC}) +set(BUILD_FULLNAME "${REPO_NAME} ${BUILD_VERSION} ") +set(CXX_COMPILER "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}") + +# Auto-updater metadata! Must somewhat mirror GitHub API endpoint +set(BUILD_AUTO_UPDATE_WEBSITE "https://github.com") +set(BUILD_AUTO_UPDATE_API "http://api.github.com") +set(BUILD_AUTO_UPDATE_REPO "eden-emulator/Releases") configure_file(scm_rev.cpp.in scm_rev.cpp @ONLY) diff --git a/CMakeModules/GetSCMRev.cmake b/CMakeModules/GetSCMRev.cmake new file mode 100644 index 0000000000..ee5ce6a91c --- /dev/null +++ b/CMakeModules/GetSCMRev.cmake @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: 2025 crueter +# SPDX-License-Identifier: GPL-3.0-or-later + +include(GetGitRevisionDescription) + +function(trim var) + string(REGEX REPLACE "\n" "" new "${${var}}") + set(${var} ${new} PARENT_SCOPE) +endfunction() + +set(TAG_FILE ${CMAKE_SOURCE_DIR}/GIT-TAG) +set(REF_FILE ${CMAKE_SOURCE_DIR}/GIT-REFSPEC) +set(COMMIT_FILE ${CMAKE_SOURCE_DIR}/GIT-COMMIT) +set(RELEASE_FILE ${CMAKE_SOURCE_DIR}/GIT-RELEASE) + +if (EXISTS ${REF_FILE} AND EXISTS ${COMMIT_FILE}) + file(READ ${REF_FILE} GIT_REFSPEC) + file(READ ${COMMIT_FILE} GIT_COMMIT) +else() + get_git_head_revision(GIT_REFSPEC GIT_COMMIT) + git_branch_name(GIT_REFSPEC) + if (GIT_REFSPEC MATCHES "NOTFOUND") + set(GIT_REFSPEC 1.0.0) + set(GIT_COMMIT stable) + endif() +endif() + +if (EXISTS ${TAG_FILE}) + file(READ ${TAG_FILE} GIT_TAG) +else() + git_describe(GIT_TAG --tags --abbrev=0) + if (GIT_TAG MATCHES "NOTFOUND") + set(GIT_TAG "${GIT_REFSPEC}") + endif() +endif() + +if (EXISTS ${RELEASE_FILE}) + file(READ ${RELEASE_FILE} GIT_RELEASE) + trim(GIT_RELEASE) + message(STATUS "Git release: ${GIT_RELEASE}") +endif() + +trim(GIT_REFSPEC) +trim(GIT_COMMIT) +trim(GIT_TAG) + +message(STATUS "Git commit: ${GIT_COMMIT}") +message(STATUS "Git tag: ${GIT_TAG}") +message(STATUS "Git refspec: ${GIT_REFSPEC}") diff --git a/CMakeModules/WindowsCopyFiles.cmake b/CMakeModules/WindowsCopyFiles.cmake index 08b598365d..a4afeb77bf 100644 --- a/CMakeModules/WindowsCopyFiles.cmake +++ b/CMakeModules/WindowsCopyFiles.cmake @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later + # SPDX-FileCopyrightText: 2018 yuzu Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later @@ -12,16 +15,25 @@ set(__windows_copy_files YES) # Any number of files to copy from SOURCE_DIR to DEST_DIR can be specified after DEST_DIR. # This copying happens post-build. -function(windows_copy_files TARGET SOURCE_DIR DEST_DIR) - # windows commandline expects the / to be \ so switch them - string(REPLACE "/" "\\\\" SOURCE_DIR ${SOURCE_DIR}) - string(REPLACE "/" "\\\\" DEST_DIR ${DEST_DIR}) +if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") + function(windows_copy_files TARGET SOURCE_DIR DEST_DIR) + # windows commandline expects the / to be \ so switch them + string(REPLACE "/" "\\\\" SOURCE_DIR ${SOURCE_DIR}) + string(REPLACE "/" "\\\\" DEST_DIR ${DEST_DIR}) - # /NJH /NJS /NDL /NFL /NC /NS /NP - Silence any output - # cmake adds an extra check for command success which doesn't work too well with robocopy - # so trick it into thinking the command was successful with the || cmd /c "exit /b 0" - add_custom_command(TARGET ${TARGET} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory ${DEST_DIR} - COMMAND robocopy ${SOURCE_DIR} ${DEST_DIR} ${ARGN} /NJH /NJS /NDL /NFL /NC /NS /NP || cmd /c "exit /b 0" - ) -endfunction() + # /NJH /NJS /NDL /NFL /NC /NS /NP - Silence any output + # cmake adds an extra check for command success which doesn't work too well with robocopy + # so trick it into thinking the command was successful with the || cmd /c "exit /b 0" + add_custom_command(TARGET ${TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory ${DEST_DIR} + COMMAND robocopy ${SOURCE_DIR} ${DEST_DIR} ${ARGN} /NJH /NJS /NDL /NFL /NC /NS /NP || cmd /c "exit /b 0" + ) + endfunction() +else() + function(windows_copy_files TARGET SOURCE_DIR DEST_DIR) + add_custom_command(TARGET ${TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory ${DEST_DIR} + COMMAND cp -ra ${SOURCE_DIR}/. ${DEST_DIR} + ) + endfunction() +endif() diff --git a/README.md b/README.md index 8d766a9c98..c5aa17ad1e 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,34 @@


- Eden + Eden
Eden

-

Eden is the world's most popular open-source Nintendo Switch emulator, forked from the Yuzu emulator — started by former Citron developer Camille LaVey and the Eden team. -
+

Eden is an open-source Nintendo Switch emulator, forked from the Yuzu emulator — started by former Citron developer Camille LaVey and the Eden team. It is written in C++ with portability in mind, and we actively maintain builds for Windows, Linux and Android.

- Discord + + Revolt +

@@ -42,29 +46,34 @@ The emulator is capable of running most commercial games at full speed, provided A list of supported games will be available in future. Please be patient. -Check out our [website](https://eden-emulator.github.io) for the latest news on exciting features, monthly progress reports, and more! +Check out our [website](https://eden-emu.dev) for the latest news on exciting features, monthly progress reports, and more! + +[![Packaging status](https://repology.org/badge/vertical-allrepos/eden-emulator.svg)](https://repology.org/project/eden-emulator/versions) ## Development -Most of the development happens on our Git server. It is also where [our central repository](https://git.eden-emu.dev/eden-emu/eden) is hosted. For development discussions, please join us on [Discord](https://discord.gg/ynGGJAN4Rx). +Most of the development happens on our Git server. It is also where [our central repository](https://git.eden-emu.dev/eden-emu/eden) is hosted. For development discussions, please join us on [Discord](https://discord.gg/kXAmGCXBGD) or [Revolt](https://rvlt.gg/qKgFEAbH). +You can also follow us on [X (Twitter)](https://x.com/edenemuofficial) for updates and announcements. -If you would like to contribute, we are open to new developers and pull requests. Please ensure that your work is of a high standard and properly documented. -You can also contact any of the developers on Discord to learn more about the current state of the emulator. +If you would like to contribute, we are open to new developers and pull requests. Please ensure that your work is of a high standard and properly documented. You can also contact any of the developers on Discord or Revolt to learn more about the current state of the emulator. + +See the [sign-up instructions](docs/SIGNUP.md) for information on registration. + +Alternatively, if you wish to add translations, go to the [Eden project on Transifex](https://app.transifex.com/edenemu/eden-emulator) and review [the translations README](./dist/languages). ## Building -* **Windows**: [Windows Building Guide](https://git.eden-emu.dev/eden-emu/eden/wiki/Building-for-Windows.-) -* **Linux**: [Linux Building Guide](https://git.eden-emu.dev/eden-emu/eden/wiki/Building-for-Linux.-) -* **Android**: [Android Building Guide](https://git.eden-emu.dev/eden-emu/eden/wiki/Building-for-Android.-) +See the [General Build Guide](docs/Build.md) + +For information on provided development tooling, see the [Tools directory](./tools) ## Download -You will be able to download the latest releases from [here](https://git.eden-emu.dev/eden-emu/eden/releases), or with MEGA and Archive links provided on Discord. +You can download the latest releases from [here](https://github.com/eden-emulator/Releases/releases). ## Support -If you enjoy the project and would like to support us financially, please check out our developers' donation pages! -- [crueter/Camille](https://liberapay.com/crueter) +If you enjoy the project and would like to support us financially, please check out our developers' [donation pages](https://eden-emu.dev/donations)! Any donations received will go towards things such as: * Switch consoles to explore and reverse-engineer the hardware @@ -73,7 +82,7 @@ Any donations received will go towards things such as: * Additional hardware (e.g. GPUs as needed to improve rendering support, other peripherals to add support for, etc.) * CI Infrastructure -If you would prefer to support us in a different way, please join our [Discord](https://discord.gg/ynGGJAN4Rx), once public, and talk to Camille or any of our other developers. +If you would prefer to support us in a different way, please join our [Discord](https://discord.gg/edenemu) and talk to Camille or any of our other developers. ## License diff --git a/cpmfile.json b/cpmfile.json new file mode 100644 index 0000000000..e9e53ed326 --- /dev/null +++ b/cpmfile.json @@ -0,0 +1,98 @@ +{ + "openssl": { + "ci": true, + "package": "OpenSSL", + "name": "openssl", + "repo": "crueter-ci/OpenSSL", + "version": "3.6.0", + "min_version": "1.1.1", + "disabled_platforms": [ + "macos-universal" + ] + }, + "boost": { + "package": "Boost", + "repo": "boostorg/boost", + "tag": "boost-%VERSION%", + "artifact": "%TAG%-cmake.tar.xz", + "hash": "4fb7f6fde92762305aad8754d7643cd918dd1f3f67e104e9ab385b18c73178d72a17321354eb203b790b6702f2cf6d725a5d6e2dfbc63b1e35f9eb59fb42ece9", + "git_version": "1.89.0", + "version": "1.57", + "find_args": "CONFIG", + "patches": [ + "0001-clang-cl.patch", + "0002-use-marmasm.patch", + "0003-armasm-options.patch" + ] + }, + "fmt": { + "repo": "fmtlib/fmt", + "tag": "%VERSION%", + "hash": "c4ab814c20fbad7e3f0ae169125a4988a2795631194703251481dc36b18da65c886c4faa9acd046b0a295005217b3689eb0126108a9ba5aac2ca909aae263c2f", + "version": "8", + "git_version": "12.0.0" + }, + "lz4": { + "name": "lz4", + "repo": "lz4/lz4", + "sha": "ebb370ca83", + "hash": "43600e87b35256005c0f2498fa56a77de6783937ba4cfce38c099f27c03188d097863e8a50c5779ca0a7c63c29c4f7ed0ae526ec798c1fd2e3736861b62e0a37", + "source_subdir": "build/cmake" + }, + "nlohmann": { + "package": "nlohmann_json", + "repo": "nlohmann/json", + "tag": "v%VERSION%", + "hash": "6cc1e86261f8fac21cc17a33da3b6b3c3cd5c116755651642af3c9e99bb3538fd42c1bd50397a77c8fb6821bc62d90e6b91bcdde77a78f58f2416c62fc53b97d", + "version": "3.8", + "git_version": "3.12.0" + }, + "zlib": { + "package": "ZLIB", + "repo": "madler/zlib", + "tag": "v%VERSION%", + "hash": "8c9642495bafd6fad4ab9fb67f09b268c69ff9af0f4f20cf15dfc18852ff1f312bd8ca41de761b3f8d8e90e77d79f2ccacd3d4c5b19e475ecf09d021fdfe9088", + "version": "1.2", + "git_version": "1.3.1", + "options": [ + "ZLIB_BUILD_SHARED OFF", + "ZLIB_INSTALL OFF" + ] + }, + "zstd": { + "repo": "facebook/zstd", + "sha": "b8d6101fba", + "hash": "a6c8e5272214fd3e65e03ae4fc375f452bd2f646623886664ee23e239e35751cfc842db4d34a84a8039d89fc8f76556121f2a4ae350d017bdff5e22150f9c3de", + "version": "1.5", + "source_subdir": "build/cmake", + "find_args": "MODULE", + "options": [ + "ZSTD_BUILD_SHARED OFF" + ] + }, + "opus": { + "package": "Opus", + "repo": "crueter/opus", + "sha": "ab19c44fad", + "hash": "79d0d015b19e74ce6076197fc32b86fe91d724a0b5a79e86adfc4bdcb946ece384e252adbbf742b74d03040913b70bb0e9556eafa59ef20e42d2f3f4d6f2859a", + "version": "1.3", + "find_args": "MODULE", + "options": [ + "OPUS_PRESUME_NEON ON" + ] + }, + "boost_headers": { + "repo": "boostorg/headers", + "sha": "95930ca8f5", + "hash": "d1dece16f3b209109de02123c537bfe1adf07a62b16c166367e7e5d62e0f7c323bf804c89b3192dd6871bc58a9d879d25a1cc3f7b9da0e497cf266f165816e2a", + "bundled": true + }, + "llvm-mingw": { + "repo": "misc/llvm-mingw", + "git_host": "git.crueter.xyz", + "tag": "%VERSION%", + "version": "20250828", + "artifact": "clang-rt-builtins.tar.zst", + "hash": "d902392caf94e84f223766e2cc51ca5fab6cae36ab8dc6ef9ef6a683ab1c483bfcfe291ef0bd38ab16a4ecc4078344fa8af72da2f225ab4c378dee23f6186181" + } +} diff --git a/dist/org.yuzu_emu.yuzu.desktop b/dist/dev.eden_emu.eden.desktop similarity index 76% rename from dist/org.yuzu_emu.yuzu.desktop rename to dist/dev.eden_emu.eden.desktop index 4a102995ec..5d2d7cd8c5 100644 --- a/dist/org.yuzu_emu.yuzu.desktop +++ b/dist/dev.eden_emu.eden.desktop @@ -1,13 +1,16 @@ +# SPDX-FileCopyrightText: 2025 Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later + # SPDX-FileCopyrightText: 2018 yuzu Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later [Desktop Entry] Version=1.0 Type=Application -Name=eden +Name=Eden GenericName=Switch Emulator Comment=Nintendo Switch video game console emulator -Icon=org.yuzu_emu.eden +Icon=dev.eden_emu.eden TryExec=eden Exec=eden %f Categories=Game;Emulator;Qt; diff --git a/dist/org.yuzu_emu.yuzu.metainfo.xml b/dist/dev.eden_emu.eden.metainfo.xml similarity index 100% rename from dist/org.yuzu_emu.yuzu.metainfo.xml rename to dist/dev.eden_emu.eden.metainfo.xml diff --git a/dist/dev.eden_emu.eden.svg b/dist/dev.eden_emu.eden.svg new file mode 100644 index 0000000000..eff6ccbb01 --- /dev/null +++ b/dist/dev.eden_emu.eden.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dist/org.yuzu_emu.yuzu.xml b/dist/dev.eden_emu.eden.xml similarity index 81% rename from dist/org.yuzu_emu.yuzu.xml rename to dist/dev.eden_emu.eden.xml index b774eb0c4e..922ebf7ff3 100644 --- a/dist/org.yuzu_emu.yuzu.xml +++ b/dist/dev.eden_emu.eden.xml @@ -1,5 +1,10 @@ + + \n"); - - S.nActiveGroup = nActiveGroup; - S.nRunning = nRunning; - -#if MICROPROFILE_DEBUG - int64_t nTicksEnd = MP_TICK(); - float fMs = fToMsCpu * (nTicksEnd - S.nPauseTicks); - printf("html dump took %6.2fms\n", fMs); -#endif - - -} - -void MicroProfileWriteFile(void* Handle, size_t nSize, const char* pData) -{ - fwrite(pData, nSize, 1, (FILE*)Handle); -} - -void MicroProfileDumpToFile() -{ - std::lock_guard Lock(MicroProfileMutex()); - if(S.nDumpFileNextFrame&1) - { - FILE* F = fopen(S.HtmlDumpPath, "w"); - if(F) - { - MicroProfileDumpHtml(MicroProfileWriteFile, F, MICROPROFILE_WEBSERVER_MAXFRAMES, S.HtmlDumpPath); - fclose(F); - } - } - if(S.nDumpFileNextFrame&2) - { - FILE* F = fopen(S.CsvDumpPath, "w"); - if(F) - { - MicroProfileDumpCsv(MicroProfileWriteFile, F, MICROPROFILE_WEBSERVER_MAXFRAMES); - fclose(F); - } - } -} - -void MicroProfileFlushSocket(MpSocket Socket) -{ - send(Socket, &S.WebServerBuffer[0], S.WebServerPut, 0); - S.WebServerPut = 0; - -} - -void MicroProfileWriteSocket(void* Handle, size_t nSize, const char* pData) -{ - S.nWebServerDataSent += nSize; - MpSocket Socket = *(MpSocket*)Handle; - if(nSize > MICROPROFILE_WEBSERVER_SOCKET_BUFFER_SIZE / 2) - { - MicroProfileFlushSocket(Socket); - send(Socket, pData, nSize, 0); - - } - else - { - memcpy(&S.WebServerBuffer[S.WebServerPut], pData, nSize); - S.WebServerPut += nSize; - if(S.WebServerPut > MICROPROFILE_WEBSERVER_SOCKET_BUFFER_SIZE/2) - { - MicroProfileFlushSocket(Socket); - } - } -} - -#if MICROPROFILE_MINIZ -#ifndef MICROPROFILE_COMPRESS_BUFFER_SIZE -#define MICROPROFILE_COMPRESS_BUFFER_SIZE (256<<10) -#endif - -#define MICROPROFILE_COMPRESS_CHUNK (MICROPROFILE_COMPRESS_BUFFER_SIZE/2) -struct MicroProfileCompressedSocketState -{ - unsigned char DeflateOut[MICROPROFILE_COMPRESS_CHUNK]; - unsigned char DeflateIn[MICROPROFILE_COMPRESS_CHUNK]; - mz_stream Stream; - MpSocket Socket; - uint32_t nSize; - uint32_t nCompressedSize; - uint32_t nFlushes; - uint32_t nMemmoveBytes; -}; - -void MicroProfileCompressedSocketFlush(MicroProfileCompressedSocketState* pState) -{ - mz_stream& Stream = pState->Stream; - unsigned char* pSendStart = &pState->DeflateOut[0]; - unsigned char* pSendEnd = &pState->DeflateOut[MICROPROFILE_COMPRESS_CHUNK - Stream.avail_out]; - if(pSendStart != pSendEnd) - { - send(pState->Socket, (const char*)pSendStart, pSendEnd - pSendStart, 0); - pState->nCompressedSize += pSendEnd - pSendStart; - } - Stream.next_out = &pState->DeflateOut[0]; - Stream.avail_out = MICROPROFILE_COMPRESS_CHUNK; - -} -void MicroProfileCompressedSocketStart(MicroProfileCompressedSocketState* pState, MpSocket Socket) -{ - mz_stream& Stream = pState->Stream; - memset(&Stream, 0, sizeof(Stream)); - Stream.next_out = &pState->DeflateOut[0]; - Stream.avail_out = MICROPROFILE_COMPRESS_CHUNK; - Stream.next_in = &pState->DeflateIn[0]; - Stream.avail_in = 0; - mz_deflateInit(&Stream, Z_DEFAULT_COMPRESSION); - pState->Socket = Socket; - pState->nSize = 0; - pState->nCompressedSize = 0; - pState->nFlushes = 0; - pState->nMemmoveBytes = 0; - -} -void MicroProfileCompressedSocketFinish(MicroProfileCompressedSocketState* pState) -{ - mz_stream& Stream = pState->Stream; - MicroProfileCompressedSocketFlush(pState); - int r = mz_deflate(&Stream, MZ_FINISH); - MP_ASSERT(r == MZ_STREAM_END); - MicroProfileCompressedSocketFlush(pState); - r = mz_deflateEnd(&Stream); - MP_ASSERT(r == MZ_OK); -} - -void MicroProfileCompressedWriteSocket(void* Handle, size_t nSize, const char* pData) -{ - MicroProfileCompressedSocketState* pState = (MicroProfileCompressedSocketState*)Handle; - mz_stream& Stream = pState->Stream; - const unsigned char* pDeflateInEnd = Stream.next_in + Stream.avail_in; - const unsigned char* pDeflateInStart = &pState->DeflateIn[0]; - const unsigned char* pDeflateInRealEnd = &pState->DeflateIn[MICROPROFILE_COMPRESS_CHUNK]; - pState->nSize += nSize; - if(nSize <= pDeflateInRealEnd - pDeflateInEnd) - { - memcpy((void*)pDeflateInEnd, pData, nSize); - Stream.avail_in += nSize; - MP_ASSERT(Stream.next_in + Stream.avail_in <= pDeflateInRealEnd); - return; - } - int Flush = 0; - while(nSize) - { - pDeflateInEnd = Stream.next_in + Stream.avail_in; - if(Flush) - { - pState->nFlushes++; - MicroProfileCompressedSocketFlush(pState); - pDeflateInRealEnd = &pState->DeflateIn[MICROPROFILE_COMPRESS_CHUNK]; - if(pDeflateInEnd == pDeflateInRealEnd) - { - if(Stream.avail_in) - { - MP_ASSERT(pDeflateInStart != Stream.next_in); - memmove((void*)pDeflateInStart, Stream.next_in, Stream.avail_in); - pState->nMemmoveBytes += Stream.avail_in; - } - Stream.next_in = pDeflateInStart; - pDeflateInEnd = Stream.next_in + Stream.avail_in; - } - } - size_t nSpace = pDeflateInRealEnd - pDeflateInEnd; - size_t nBytes = MicroProfileMin(nSpace, nSize); - MP_ASSERT(nBytes + pDeflateInEnd <= pDeflateInRealEnd); - memcpy((void*)pDeflateInEnd, pData, nBytes); - Stream.avail_in += nBytes; - nSize -= nBytes; - pData += nBytes; - int r = mz_deflate(&Stream, MZ_NO_FLUSH); - Flush = r == MZ_BUF_ERROR || nBytes == 0 || Stream.avail_out == 0 ? 1 : 0; - MP_ASSERT(r == MZ_BUF_ERROR || r == MZ_OK); - if(r == MZ_BUF_ERROR) - { - r = mz_deflate(&Stream, MZ_SYNC_FLUSH); - } - } -} -#endif - - -#ifndef MicroProfileSetNonBlocking //fcntl doesnt work on a some unix like platforms.. -void MicroProfileSetNonBlocking(MpSocket Socket, int NonBlocking) -{ -#ifdef _WIN32 - u_long nonBlocking = NonBlocking ? 1 : 0; - ioctlsocket(Socket, FIONBIO, &nonBlocking); -#else - int Options = fcntl(Socket, F_GETFL); - if(NonBlocking) - { - fcntl(Socket, F_SETFL, Options|O_NONBLOCK); - } - else - { - fcntl(Socket, F_SETFL, Options&(~O_NONBLOCK)); - } -#endif -} -#endif - -void MicroProfileWebServerStart() -{ -#ifdef _WIN32 - WSADATA wsa; - if(WSAStartup(MAKEWORD(2, 2), &wsa)) - { - S.ListenerSocket = -1; - return; - } -#endif - - S.ListenerSocket = socket(PF_INET, SOCK_STREAM, 6); - MP_ASSERT(!MP_INVALID_SOCKET(S.ListenerSocket)); - MicroProfileSetNonBlocking(S.ListenerSocket, 1); - - S.nWebServerPort = (uint32_t)-1; - struct sockaddr_in Addr; - Addr.sin_family = AF_INET; - Addr.sin_addr.s_addr = INADDR_ANY; - for(int i = 0; i < 20; ++i) - { - Addr.sin_port = htons(MICROPROFILE_WEBSERVER_PORT+i); - if(0 == bind(S.ListenerSocket, (sockaddr*)&Addr, sizeof(Addr))) - { - S.nWebServerPort = MICROPROFILE_WEBSERVER_PORT+i; - break; - } - } - listen(S.ListenerSocket, 8); -} - -void MicroProfileWebServerStop() -{ -#ifdef _WIN32 - closesocket(S.ListenerSocket); - WSACleanup(); -#else - close(S.ListenerSocket); -#endif -} - -int MicroProfileParseGet(const char* pGet) -{ - const char* pStart = pGet; - while(*pGet != '\0') - { - if(*pGet < '0' || *pGet > '9') - return 0; - pGet++; - } - int nFrames = atoi(pStart); - if(nFrames) - { - return nFrames; - } - else - { - return MICROPROFILE_WEBSERVER_MAXFRAMES; - } -} -bool MicroProfileWebServerUpdate() -{ - MICROPROFILE_SCOPEI("MicroProfile", "Webserver-update", -1); - MpSocket Connection = accept(S.ListenerSocket, 0, 0); - bool bServed = false; - if(!MP_INVALID_SOCKET(Connection)) - { - std::lock_guard Lock(MicroProfileMutex()); - char Req[8192]; - MicroProfileSetNonBlocking(Connection, 0); - int nReceived = recv(Connection, Req, sizeof(Req)-1, 0); - if(nReceived > 0) - { - Req[nReceived] = '\0'; -#if MICROPROFILE_MINIZ -#define MICROPROFILE_HTML_HEADER "HTTP/1.0 200 OK\r\nContent-Type: text/html\r\nContent-Encoding: deflate\r\nExpires: Tue, 01 Jan 2199 16:00:00 GMT\r\n\r\n" -#else -#define MICROPROFILE_HTML_HEADER "HTTP/1.0 200 OK\r\nContent-Type: text/html\r\nExpires: Tue, 01 Jan 2199 16:00:00 GMT\r\n\r\n" -#endif - char* pHttp = strstr(Req, "HTTP/"); - char* pGet = strstr(Req, "GET /"); - char* pHost = strstr(Req, "Host: "); - auto Terminate = [](char* pString) - { - char* pEnd = pString; - while(*pEnd != '\0') - { - if(*pEnd == '\r' || *pEnd == '\n' || *pEnd == ' ') - { - *pEnd = '\0'; - return; - } - pEnd++; - } - }; - if(pHost) - { - pHost += sizeof("Host: ")-1; - Terminate(pHost); - } - - if(pHttp && pGet) - { - *pHttp = '\0'; - pGet += sizeof("GET /")-1; - Terminate(pGet); - int nFrames = MicroProfileParseGet(pGet); - if(nFrames) - { - uint64_t nTickStart = MP_TICK(); - send(Connection, MICROPROFILE_HTML_HEADER, sizeof(MICROPROFILE_HTML_HEADER)-1, 0); - uint64_t nDataStart = S.nWebServerDataSent; - S.WebServerPut = 0; - #if 0 == MICROPROFILE_MINIZ - MicroProfileDumpHtml(MicroProfileWriteSocket, &Connection, nFrames, pHost); - uint64_t nDataEnd = S.nWebServerDataSent; - uint64_t nTickEnd = MP_TICK(); - uint64_t nDiff = (nTickEnd - nTickStart); - float fMs = MicroProfileTickToMsMultiplier(MicroProfileTicksPerSecondCpu()) * nDiff; - int nKb = ((nDataEnd-nDataStart)>>10) + 1; - int nCompressedKb = nKb; - MicroProfilePrintf(MicroProfileWriteSocket, &Connection, "\n\n\n",nKb, fMs); - MicroProfileFlushSocket(Connection); - #else - MicroProfileCompressedSocketState CompressState; - MicroProfileCompressedSocketStart(&CompressState, Connection); - MicroProfileDumpHtml(MicroProfileCompressedWriteSocket, &CompressState, nFrames, pHost); - S.nWebServerDataSent += CompressState.nSize; - uint64_t nDataEnd = S.nWebServerDataSent; - uint64_t nTickEnd = MP_TICK(); - uint64_t nDiff = (nTickEnd - nTickStart); - float fMs = MicroProfileTickToMsMultiplier(MicroProfileTicksPerSecondCpu()) * nDiff; - int nKb = ((nDataEnd-nDataStart)>>10) + 1; - int nCompressedKb = ((CompressState.nCompressedSize)>>10) + 1; - MicroProfilePrintf(MicroProfileCompressedWriteSocket, &CompressState, "\n\n\n", nKb, nCompressedKb, fMs); - MicroProfileCompressedSocketFinish(&CompressState); - MicroProfileFlushSocket(Connection); - #endif - - #if MICROPROFILE_DEBUG - printf("\n\n\n", nKb, nCompressedKb, fMs); - #endif - } - } - } -#ifdef _WIN32 - closesocket(Connection); -#else - close(Connection); -#endif - } - return bServed; -} -#endif - - - - -#if MICROPROFILE_CONTEXT_SWITCH_TRACE -//functions that need to be implemented per platform. -void* MicroProfileTraceThread(void* unused); -bool MicroProfileIsLocalThread(uint32_t nThreadId); - - -void MicroProfileStartContextSwitchTrace() -{ - if(!S.bContextSwitchRunning) - { - S.bContextSwitchRunning = true; - S.bContextSwitchStop = false; - MicroProfileThreadStart(&S.ContextSwitchThread, MicroProfileTraceThread); - } -} - -void MicroProfileStopContextSwitchTrace() -{ - if(S.bContextSwitchRunning) - { - S.bContextSwitchStop = true; - MicroProfileThreadJoin(&S.ContextSwitchThread); - } -} - - -#ifdef _WIN32 -#define INITGUID -#include -#include -#include - - -static GUID g_MicroProfileThreadClassGuid = { 0x3d6fa8d1, 0xfe05, 0x11d0, 0x9d, 0xda, 0x00, 0xc0, 0x4f, 0xd7, 0xba, 0x7c }; - -struct MicroProfileSCSwitch -{ - uint32_t NewThreadId; - uint32_t OldThreadId; - int8_t NewThreadPriority; - int8_t OldThreadPriority; - uint8_t PreviousCState; - int8_t SpareByte; - int8_t OldThreadWaitReason; - int8_t OldThreadWaitMode; - int8_t OldThreadState; - int8_t OldThreadWaitIdealProcessor; - uint32_t NewThreadWaitTime; - uint32_t Reserved; -}; - - -VOID WINAPI MicroProfileContextSwitchCallback(PEVENT_TRACE pEvent) -{ - if (pEvent->Header.Guid == g_MicroProfileThreadClassGuid) - { - if (pEvent->Header.Class.Type == 36) - { - MicroProfileSCSwitch* pCSwitch = (MicroProfileSCSwitch*) pEvent->MofData; - if ((pCSwitch->NewThreadId != 0) || (pCSwitch->OldThreadId != 0)) - { - MicroProfileContextSwitch Switch; - Switch.nThreadOut = pCSwitch->OldThreadId; - Switch.nThreadIn = pCSwitch->NewThreadId; - Switch.nCpu = pEvent->BufferContext.ProcessorNumber; - Switch.nTicks = pEvent->Header.TimeStamp.QuadPart; - MicroProfileContextSwitchPut(&Switch); - } - } - } -} - -ULONG WINAPI MicroProfileBufferCallback(PEVENT_TRACE_LOGFILE Buffer) -{ - return (S.bContextSwitchStop || !S.bContextSwitchRunning) ? FALSE : TRUE; -} - - -struct MicroProfileKernelTraceProperties : public EVENT_TRACE_PROPERTIES -{ - char dummy[sizeof(KERNEL_LOGGER_NAME)]; -}; - -void MicroProfileContextSwitchShutdownTrace() -{ - TRACEHANDLE SessionHandle = 0; - MicroProfileKernelTraceProperties sessionProperties; - - ZeroMemory(&sessionProperties, sizeof(sessionProperties)); - sessionProperties.Wnode.BufferSize = sizeof(sessionProperties); - sessionProperties.Wnode.Flags = WNODE_FLAG_TRACED_GUID; - sessionProperties.Wnode.ClientContext = 1; //QPC clock resolution - sessionProperties.Wnode.Guid = SystemTraceControlGuid; - sessionProperties.BufferSize = 1; - sessionProperties.NumberOfBuffers = 128; - sessionProperties.EnableFlags = EVENT_TRACE_FLAG_CSWITCH; - sessionProperties.LogFileMode = EVENT_TRACE_REAL_TIME_MODE; - sessionProperties.MaximumFileSize = 0; - sessionProperties.LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES); - sessionProperties.LogFileNameOffset = 0; - - EVENT_TRACE_LOGFILE log; - ZeroMemory(&log, sizeof(log)); - log.LoggerName = KERNEL_LOGGER_NAME; - log.ProcessTraceMode = 0; - TRACEHANDLE hLog = OpenTrace(&log); - if (hLog) - { - ControlTrace(SessionHandle, KERNEL_LOGGER_NAME, &sessionProperties, EVENT_TRACE_CONTROL_STOP); - } - CloseTrace(hLog); - - -} - -void* MicroProfileTraceThread(void* unused) -{ - - MicroProfileContextSwitchShutdownTrace(); - ULONG status = ERROR_SUCCESS; - TRACEHANDLE SessionHandle = 0; - MicroProfileKernelTraceProperties sessionProperties; - - ZeroMemory(&sessionProperties, sizeof(sessionProperties)); - sessionProperties.Wnode.BufferSize = sizeof(sessionProperties); - sessionProperties.Wnode.Flags = WNODE_FLAG_TRACED_GUID; - sessionProperties.Wnode.ClientContext = 1; //QPC clock resolution - sessionProperties.Wnode.Guid = SystemTraceControlGuid; - sessionProperties.BufferSize = 1; - sessionProperties.NumberOfBuffers = 128; - sessionProperties.EnableFlags = EVENT_TRACE_FLAG_CSWITCH|EVENT_TRACE_FLAG_PROCESS; - sessionProperties.LogFileMode = EVENT_TRACE_REAL_TIME_MODE; - sessionProperties.MaximumFileSize = 0; - sessionProperties.LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES); - sessionProperties.LogFileNameOffset = 0; - - - status = StartTrace((PTRACEHANDLE) &SessionHandle, KERNEL_LOGGER_NAME, &sessionProperties); - - if (ERROR_SUCCESS != status) - { - S.bContextSwitchRunning = false; - return 0; - } - - EVENT_TRACE_LOGFILE log; - ZeroMemory(&log, sizeof(log)); - - log.LoggerName = KERNEL_LOGGER_NAME; - log.ProcessTraceMode = PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_RAW_TIMESTAMP; - log.EventCallback = MicroProfileContextSwitchCallback; - log.BufferCallback = MicroProfileBufferCallback; - - TRACEHANDLE hLog = OpenTrace(&log); - ProcessTrace(&hLog, 1, 0, 0); - CloseTrace(hLog); - MicroProfileContextSwitchShutdownTrace(); - - S.bContextSwitchRunning = false; - return 0; -} - -bool MicroProfileIsLocalThread(uint32_t nThreadId) -{ - HANDLE h = OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, nThreadId); - if(h == NULL) - return false; - DWORD hProcess = GetProcessIdOfThread(h); - CloseHandle(h); - return GetCurrentProcessId() == hProcess; -} - -#elif defined(__APPLE__) -#include -void* MicroProfileTraceThread(void* unused) -{ - FILE* pFile = fopen("mypipe", "r"); - if(!pFile) - { - printf("CONTEXT SWITCH FAILED TO OPEN FILE: make sure to run dtrace script\n"); - S.bContextSwitchRunning = false; - return 0; - } - printf("STARTING TRACE THREAD\n"); - char* pLine = 0; - size_t cap = 0; - size_t len = 0; - struct timeval tv; - - gettimeofday(&tv, NULL); - - uint64_t nsSinceEpoch = ((uint64_t)(tv.tv_sec) * 1000000 + (uint64_t)(tv.tv_usec)) * 1000; - uint64_t nTickEpoch = MP_TICK(); - uint32_t nLastThread[MICROPROFILE_MAX_CONTEXT_SWITCH_THREADS] = {0}; - mach_timebase_info_data_t sTimebaseInfo; - mach_timebase_info(&sTimebaseInfo); - S.bContextSwitchRunning = true; - - uint64_t nProcessed = 0; - uint64_t nProcessedLast = 0; - while((len = getline(&pLine, &cap, pFile))>0 && !S.bContextSwitchStop) - { - nProcessed += len; - if(nProcessed - nProcessedLast > 10<<10) - { - nProcessedLast = nProcessed; - printf("processed %llukb %llukb\n", (nProcessed-nProcessedLast)>>10,nProcessed >>10); - } - - char* pX = strchr(pLine, 'X'); - if(pX) - { - int cpu = atoi(pX+1); - char* pX2 = strchr(pX + 1, 'X'); - char* pX3 = strchr(pX2 + 1, 'X'); - int thread = atoi(pX2+1); - char* lala; - int64_t timestamp = strtoll(pX3 + 1, &lala, 10); - MicroProfileContextSwitch Switch; - - //convert to ticks. - uint64_t nDeltaNsSinceEpoch = timestamp - nsSinceEpoch; - uint64_t nDeltaTickSinceEpoch = sTimebaseInfo.numer * nDeltaNsSinceEpoch / sTimebaseInfo.denom; - uint64_t nTicks = nDeltaTickSinceEpoch + nTickEpoch; - if(cpu < MICROPROFILE_MAX_CONTEXT_SWITCH_THREADS) - { - Switch.nThreadOut = nLastThread[cpu]; - Switch.nThreadIn = thread; - nLastThread[cpu] = thread; - Switch.nCpu = cpu; - Switch.nTicks = nTicks; - MicroProfileContextSwitchPut(&Switch); - } - } - } - printf("EXITING TRACE THREAD\n"); - S.bContextSwitchRunning = false; - return 0; -} - -bool MicroProfileIsLocalThread(uint32_t nThreadId) -{ - return false; -} - -#endif -#else - -bool MicroProfileIsLocalThread([[maybe_unused]] uint32_t nThreadId) { return false; } -void MicroProfileStopContextSwitchTrace(){} -void MicroProfileStartContextSwitchTrace(){} - -#endif - - - - -#if MICROPROFILE_GPU_TIMERS_D3D11 -uint32_t MicroProfileGpuInsertTimeStamp() -{ - MicroProfileD3D11Frame& Frame = S.GPU.m_QueryFrames[S.GPU.m_nQueryFrame]; - if(Frame.m_nRateQueryStarted) - { - uint32_t nCurrent = (Frame.m_nQueryStart + Frame.m_nQueryCount) % MICROPROFILE_D3D_MAX_QUERIES; - uint32_t nNext = (nCurrent + 1) % MICROPROFILE_D3D_MAX_QUERIES; - if(nNext != S.GPU.m_nQueryGet) - { - Frame.m_nQueryCount++; - ID3D11Query* pQuery = (ID3D11Query*)S.GPU.m_pQueries[nCurrent]; - ID3D11DeviceContext* pContext = (ID3D11DeviceContext*)S.GPU.m_pDeviceContext; - pContext->End(pQuery); - S.GPU.m_nQueryPut = nNext; - return nCurrent; - } - } - return (uint32_t)-1; -} - -uint64_t MicroProfileGpuGetTimeStamp(uint32_t nIndex) -{ - if(nIndex == (uint32_t)-1) - { - return (uint64_t)-1; - } - int64_t nResult = S.GPU.m_nQueryResults[nIndex]; - MP_ASSERT(nResult != -1); - return nResult; -} - -bool MicroProfileGpuGetData(void* pQuery, void* pData, uint32_t nDataSize) -{ - HRESULT hr; - do - { - hr = ((ID3D11DeviceContext*)S.GPU.m_pDeviceContext)->GetData((ID3D11Query*)pQuery, pData, nDataSize, 0); - }while(hr == S_FALSE); - switch(hr) - { - case DXGI_ERROR_DEVICE_REMOVED: - case DXGI_ERROR_INVALID_CALL: - case E_INVALIDARG: - MP_BREAK(); - return false; - - } - return true; -} - -uint64_t MicroProfileTicksPerSecondGpu() -{ - return S.GPU.m_nQueryFrequency; -} - -void MicroProfileGpuFlip() -{ - MicroProfileD3D11Frame& CurrentFrame = S.GPU.m_QueryFrames[S.GPU.m_nQueryFrame]; - ID3D11DeviceContext* pContext = (ID3D11DeviceContext*)S.GPU.m_pDeviceContext; - if(CurrentFrame.m_nRateQueryStarted) - { - pContext->End((ID3D11Query*)CurrentFrame.m_pRateQuery); - } - uint32_t nNextFrame = (S.GPU.m_nQueryFrame + 1) % MICROPROFILE_GPU_FRAME_DELAY; - MicroProfileD3D11Frame& OldFrame = S.GPU.m_QueryFrames[nNextFrame]; - if(OldFrame.m_nRateQueryStarted) - { - struct RateQueryResult - { - uint64_t nFrequency; - BOOL bDisjoint; - }; - RateQueryResult Result; - if(MicroProfileGpuGetData(OldFrame.m_pRateQuery, &Result, sizeof(Result))) - { - if(S.GPU.m_nQueryFrequency != (int64_t)Result.nFrequency) - { - if(S.GPU.m_nQueryFrequency) - { - OutputDebugString("Query freq changing"); - } - S.GPU.m_nQueryFrequency = Result.nFrequency; - } - uint32_t nStart = OldFrame.m_nQueryStart; - uint32_t nCount = OldFrame.m_nQueryCount; - for(uint32_t i = 0; i < nCount; ++i) - { - uint32_t nIndex = (i + nStart) % MICROPROFILE_D3D_MAX_QUERIES; - - - - if(!MicroProfileGpuGetData(S.GPU.m_pQueries[nIndex], &S.GPU.m_nQueryResults[nIndex], sizeof(uint64_t))) - { - S.GPU.m_nQueryResults[nIndex] = -1; - } - } - } - else - { - uint32_t nStart = OldFrame.m_nQueryStart; - uint32_t nCount = OldFrame.m_nQueryCount; - for(uint32_t i = 0; i < nCount; ++i) - { - uint32_t nIndex = (i + nStart) % MICROPROFILE_D3D_MAX_QUERIES; - S.GPU.m_nQueryResults[nIndex] = -1; - } - } - S.GPU.m_nQueryGet = (OldFrame.m_nQueryStart + OldFrame.m_nQueryCount) % MICROPROFILE_D3D_MAX_QUERIES; - } - - S.GPU.m_nQueryFrame = nNextFrame; - MicroProfileD3D11Frame& NextFrame = S.GPU.m_QueryFrames[nNextFrame]; - pContext->Begin((ID3D11Query*)NextFrame.m_pRateQuery); - NextFrame.m_nQueryStart = S.GPU.m_nQueryPut; - NextFrame.m_nQueryCount = 0; - - NextFrame.m_nRateQueryStarted = 1; -} - -void MicroProfileGpuInitD3D11(void* pDevice_, void* pDeviceContext_) -{ - ID3D11Device* pDevice = (ID3D11Device*)pDevice_; - ID3D11DeviceContext* pDeviceContext = (ID3D11DeviceContext*)pDeviceContext_; - S.GPU.m_pDeviceContext = pDeviceContext_; - - D3D11_QUERY_DESC Desc; - Desc.MiscFlags = 0; - Desc.Query = D3D11_QUERY_TIMESTAMP; - for(uint32_t i = 0; i < MICROPROFILE_D3D_MAX_QUERIES; ++i) - { - HRESULT hr = pDevice->CreateQuery(&Desc, (ID3D11Query**)&S.GPU.m_pQueries[i]); - MP_ASSERT(hr == S_OK); - S.GPU.m_nQueryResults[i] = -1; - } - S.GPU.m_nQueryPut = 0; - S.GPU.m_nQueryGet = 0; - S.GPU.m_nQueryFrame = 0; - S.GPU.m_nQueryFrequency = 0; - Desc.Query = D3D11_QUERY_TIMESTAMP_DISJOINT; - for(uint32_t i = 0; i < MICROPROFILE_GPU_FRAME_DELAY; ++i) - { - S.GPU.m_QueryFrames[i].m_nQueryStart = 0; - S.GPU.m_QueryFrames[i].m_nQueryCount = 0; - S.GPU.m_QueryFrames[i].m_nRateQueryStarted = 0; - HRESULT hr = pDevice->CreateQuery(&Desc, (ID3D11Query**)&S.GPU.m_QueryFrames[i].m_pRateQuery); - MP_ASSERT(hr == S_OK); - } -} - - -void MicroProfileGpuShutdown() -{ - for(uint32_t i = 0; i < MICROPROFILE_D3D_MAX_QUERIES; ++i) - { - ((ID3D11Query*)&S.GPU.m_pQueries[i])->Release(); - S.GPU.m_pQueries[i] = 0; - } - for(uint32_t i = 0; i < MICROPROFILE_GPU_FRAME_DELAY; ++i) - { - ((ID3D11Query*)S.GPU.m_QueryFrames[i].m_pRateQuery)->Release(); - S.GPU.m_QueryFrames[i].m_pRateQuery = 0; - } -} - -int MicroProfileGetGpuTickReference(int64_t* pOutCPU, int64_t* pOutGpu) -{ - return 0; -} - - -#elif MICROPROFILE_GPU_TIMERS_GL -void MicroProfileGpuInitGL() -{ - S.GPU.GLTimerPos = 0; - glGenQueries(MICROPROFILE_GL_MAX_QUERIES, &S.GPU.GLTimers[0]); -} - -uint32_t MicroProfileGpuInsertTimeStamp() -{ - uint32_t nIndex = (S.GPU.GLTimerPos+1)%MICROPROFILE_GL_MAX_QUERIES; - glQueryCounter(S.GPU.GLTimers[nIndex], GL_TIMESTAMP); - S.GPU.GLTimerPos = nIndex; - return nIndex; -} -uint64_t MicroProfileGpuGetTimeStamp(uint32_t nKey) -{ - uint64_t result; - glGetQueryObjectui64v(S.GPU.GLTimers[nKey], GL_QUERY_RESULT, &result); - return result; -} - -uint64_t MicroProfileTicksPerSecondGpu() -{ - return 1000000000ll; -} - -int MicroProfileGetGpuTickReference(int64_t* pOutCpu, int64_t* pOutGpu) -{ - int64_t nGpuTimeStamp; - glGetInteger64v(GL_TIMESTAMP, &nGpuTimeStamp); - if(nGpuTimeStamp) - { - *pOutCpu = MP_TICK(); - *pOutGpu = nGpuTimeStamp; - #if 0 //debug test if timestamp diverges - static int64_t nTicksPerSecondCpu = MicroProfileTicksPerSecondCpu(); - static int64_t nTicksPerSecondGpu = MicroProfileTicksPerSecondGpu(); - static int64_t nGpuStart = 0; - static int64_t nCpuStart = 0; - if(!nCpuStart) - { - nCpuStart = *pOutCpu; - nGpuStart = *pOutGpu; - } - static int nCountDown = 100; - if(0 == nCountDown--) - { - int64_t nCurCpu = *pOutCpu; - int64_t nCurGpu = *pOutGpu; - double fDistanceCpu = (nCurCpu - nCpuStart) / (double)nTicksPerSecondCpu; - double fDistanceGpu = (nCurGpu - nGpuStart) / (double)nTicksPerSecondGpu; - - char buf[254]; - snprintf(buf, sizeof(buf)-1,"Distance %f %f diff %f\n", fDistanceCpu, fDistanceGpu, fDistanceCpu-fDistanceGpu); - OutputDebugString(buf); - nCountDown = 100; - } - #endif - return 1; - } - return 0; -} - - -#endif - -#undef S - -#ifdef _MSC_VER -#pragma warning(pop) -#endif - - - - - -#endif -#endif -#ifdef MICROPROFILE_EMBED_HTML -#include "microprofile_html.h" -#endif diff --git a/externals/microprofile/microprofile_html.h b/externals/microprofile/microprofile_html.h deleted file mode 100644 index 01b624b60a..0000000000 --- a/externals/microprofile/microprofile_html.h +++ /dev/null @@ -1,3868 +0,0 @@ -///start file generated from microprofile.html -#ifdef MICROPROFILE_EMBED_HTML -const char g_MicroProfileHtml_begin_0[] = -"\n" -"\n" -"\n" -"MicroProfile Capture\n" -"\n" -"\n" -"\n" -"\n" -"

\n" -"\n" -"
\n" -"\n" -"\n" -" "; - -const size_t g_MicroProfileHtml_end_2_size = sizeof(g_MicroProfileHtml_end_2); -const char* g_MicroProfileHtml_end[] = { -&g_MicroProfileHtml_end_0[0], -&g_MicroProfileHtml_end_1[0], -&g_MicroProfileHtml_end_2[0], -}; -size_t g_MicroProfileHtml_end_sizes[] = { -sizeof(g_MicroProfileHtml_end_0), -sizeof(g_MicroProfileHtml_end_1), -sizeof(g_MicroProfileHtml_end_2), -}; -size_t g_MicroProfileHtml_end_count = 3; -#endif //MICROPROFILE_EMBED_HTML - -///end file generated from microprofile.html diff --git a/externals/microprofile/microprofileui.h b/externals/microprofile/microprofileui.h deleted file mode 100644 index ca9fe70631..0000000000 --- a/externals/microprofile/microprofileui.h +++ /dev/null @@ -1,3349 +0,0 @@ -#pragma once -// This is free and unencumbered software released into the public domain. -// Anyone is free to copy, modify, publish, use, compile, sell, or -// distribute this software, either in source code form or as a compiled -// binary, for any purpose, commercial or non-commercial, and by any -// means. -// In jurisdictions that recognize copyright laws, the author or authors -// of this software dedicate any and all copyright interest in the -// software to the public domain. We make this dedication for the benefit -// of the public at large and to the detriment of our heirs and -// successors. We intend this dedication to be an overt act of -// relinquishment in perpetuity of all present and future rights to this -// software under copyright law. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// For more information, please refer to -// -// *********************************************************************** -// -// -// - - -#ifndef MICROPROFILE_ENABLED -#error "microprofile.h must be included before including microprofileui.h" -#endif - -#ifndef MICROPROFILEUI_ENABLED -#define MICROPROFILEUI_ENABLED MICROPROFILE_ENABLED -#endif - -#ifndef MICROPROFILEUI_API -#define MICROPROFILEUI_API -#endif - - -#if 0 == MICROPROFILEUI_ENABLED -#define MicroProfileMouseButton(foo, bar) do{}while(0) -#define MicroProfileMousePosition(foo, bar, z) do{}while(0) -#define MicroProfileModKey(key) do{}while(0) -#define MicroProfileDraw(foo, bar) do{}while(0) -#define MicroProfileIsDrawing() 0 -#define MicroProfileToggleDisplayMode() do{}while(0) -#define MicroProfileSetDisplayMode(f) do{}while(0) -#else - -#ifndef MICROPROFILE_DRAWCURSOR -#define MICROPROFILE_DRAWCURSOR 0 -#endif - -#ifndef MICROPROFILE_DETAILED_BAR_NAMES -#define MICROPROFILE_DETAILED_BAR_NAMES 1 -#endif - -#ifndef MICROPROFILE_TEXT_WIDTH -#define MICROPROFILE_TEXT_WIDTH 5 -#endif - -#ifndef MICROPROFILE_TEXT_HEIGHT -#define MICROPROFILE_TEXT_HEIGHT 8 -#endif - -#ifndef MICROPROFILE_DETAILED_BAR_HEIGHT -#define MICROPROFILE_DETAILED_BAR_HEIGHT 12 -#endif - -#ifndef MICROPROFILE_DETAILED_CONTEXT_SWITCH_HEIGHT -#define MICROPROFILE_DETAILED_CONTEXT_SWITCH_HEIGHT 7 -#endif - -#ifndef MICROPROFILE_GRAPH_WIDTH -#define MICROPROFILE_GRAPH_WIDTH 256 -#endif - -#ifndef MICROPROFILE_GRAPH_HEIGHT -#define MICROPROFILE_GRAPH_HEIGHT 256 -#endif - -#ifndef MICROPROFILE_BORDER_SIZE -#define MICROPROFILE_BORDER_SIZE 1 -#endif - -#ifndef MICROPROFILE_HELP_LEFT -#define MICROPROFILE_HELP_LEFT "Left-Click" -#endif - -#ifndef MICROPROFILE_HELP_ALT -#define MICROPROFILE_HELP_ALT "Alt-Click" -#endif - -#ifndef MICROPROFILE_HELP_MOD -#define MICROPROFILE_HELP_MOD "Mod" -#endif - -#ifndef MICROPROFILE_BAR_WIDTH -#define MICROPROFILE_BAR_WIDTH 100 -#endif - -#ifndef MICROPROFILE_CUSTOM_MAX -#define MICROPROFILE_CUSTOM_MAX 8 -#endif - -#ifndef MICROPROFILE_CUSTOM_MAX_TIMERS -#define MICROPROFILE_CUSTOM_MAX_TIMERS 64 -#endif - -#ifndef MICROPROFILE_CUSTOM_PADDING -#define MICROPROFILE_CUSTOM_PADDING 12 -#endif - - -#define MICROPROFILE_FRAME_HISTORY_HEIGHT 50 -#define MICROPROFILE_FRAME_HISTORY_WIDTH 7 -#define MICROPROFILE_FRAME_HISTORY_COLOR_CPU 0xffff7f27 //255 127 39 -#define MICROPROFILE_FRAME_HISTORY_COLOR_GPU 0xff37a0ee //55 160 238 -#define MICROPROFILE_FRAME_HISTORY_COLOR_HIGHTLIGHT 0x7733bb44 -#define MICROPROFILE_FRAME_COLOR_HIGHTLIGHT 0x20009900 -#define MICROPROFILE_FRAME_COLOR_HIGHTLIGHT_GPU 0x20996600 -#define MICROPROFILE_NUM_FRAMES (MICROPROFILE_MAX_FRAME_HISTORY - (MICROPROFILE_GPU_FRAME_DELAY+1)) - -#define MICROPROFILE_TOOLTIP_MAX_STRINGS (32 + MICROPROFILE_MAX_GROUPS*2) -#define MICROPROFILE_TOOLTIP_STRING_BUFFER_SIZE (4*1024) -#define MICROPROFILE_TOOLTIP_MAX_LOCKED 3 - - -enum -{ - MICROPROFILE_CUSTOM_BARS = 0x1, - MICROPROFILE_CUSTOM_BAR_SOURCE_MAX = 0x2, - MICROPROFILE_CUSTOM_BAR_SOURCE_AVG = 0, - MICROPROFILE_CUSTOM_STACK = 0x4, - MICROPROFILE_CUSTOM_STACK_SOURCE_MAX = 0x8, - MICROPROFILE_CUSTOM_STACK_SOURCE_AVG = 0, -}; - - -MICROPROFILEUI_API void MicroProfileDraw(uint32_t nWidth, uint32_t nHeight); //! call if drawing microprofilers -MICROPROFILEUI_API bool MicroProfileIsDrawing(); -MICROPROFILEUI_API void MicroProfileToggleGraph(MicroProfileToken nToken); -MICROPROFILEUI_API bool MicroProfileDrawGraph(uint32_t nScreenWidth, uint32_t nScreenHeight); -MICROPROFILEUI_API void MicroProfileToggleDisplayMode(); //switch between off, bars, detailed -MICROPROFILEUI_API void MicroProfileSetDisplayMode(int); //switch between off, bars, detailed -MICROPROFILEUI_API void MicroProfileClearGraph(); -MICROPROFILEUI_API void MicroProfileMousePosition(uint32_t nX, uint32_t nY, int nWheelDelta); -MICROPROFILEUI_API void MicroProfileModKey(uint32_t nKeyState); -MICROPROFILEUI_API void MicroProfileMouseButton(uint32_t nLeft, uint32_t nRight); -MICROPROFILEUI_API void MicroProfileDrawLineVertical(int nX, int nTop, int nBottom, uint32_t nColor); -MICROPROFILEUI_API void MicroProfileDrawLineHorizontal(int nLeft, int nRight, int nY, uint32_t nColor); -MICROPROFILEUI_API void MicroProfileLoadPreset(const char* pSuffix); -MICROPROFILEUI_API void MicroProfileSavePreset(const char* pSuffix); - -MICROPROFILEUI_API void MicroProfileDrawText(int nX, int nY, uint32_t nColor, const char* pText, uint32_t nNumCharacters); -MICROPROFILEUI_API void MicroProfileDrawBox(int nX, int nY, int nX1, int nY1, uint32_t nColor, MicroProfileBoxType = MicroProfileBoxTypeFlat); -MICROPROFILEUI_API void MicroProfileDrawLine2D(uint32_t nVertices, float* pVertices, uint32_t nColor); -MICROPROFILEUI_API void MicroProfileDumpTimers(); - -MICROPROFILEUI_API void MicroProfileInitUI(); - -MICROPROFILEUI_API void MicroProfileCustomGroupToggle(const char* pCustomName); -MICROPROFILEUI_API void MicroProfileCustomGroupEnable(const char* pCustomName); -MICROPROFILEUI_API void MicroProfileCustomGroupEnable(uint32_t nIndex); -MICROPROFILEUI_API void MicroProfileCustomGroupDisable(); -MICROPROFILEUI_API void MicroProfileCustomGroup(const char* pCustomName, uint32_t nMaxTimers, uint32_t nAggregateFlip, float fReferenceTime, uint32_t nFlags); -MICROPROFILEUI_API void MicroProfileCustomGroupAddTimer(const char* pCustomName, const char* pGroup, const char* pTimer); - -#ifdef MICROPROFILEUI_IMPL -#include -#include -#include -#include -#include -#include -#include - -MICROPROFILE_DEFINE(g_MicroProfileDetailed, "MicroProfile", "Detailed View", 0x8888000); -MICROPROFILE_DEFINE(g_MicroProfileDrawGraph, "MicroProfile", "Draw Graph", 0xff44ee00); -MICROPROFILE_DEFINE(g_MicroProfileDrawBarView, "MicroProfile", "DrawBarView", 0x00dd77); -MICROPROFILE_DEFINE(g_MicroProfileDraw,"MicroProfile", "Draw", 0x737373); - - -struct MicroProfileStringArray -{ - const char* ppStrings[MICROPROFILE_TOOLTIP_MAX_STRINGS]; - char Buffer[MICROPROFILE_TOOLTIP_STRING_BUFFER_SIZE]; - char* pBufferPos; - uint32_t nNumStrings; -}; - -struct MicroProfileGroupMenuItem -{ - uint32_t nIsCategory; - uint32_t nCategoryIndex; - uint32_t nIndex; - const char* pName; -}; - -struct MicroProfileCustom -{ - char pName[MICROPROFILE_NAME_MAX_LEN]; - uint32_t nFlags; - uint32_t nAggregateFlip; - uint32_t nNumTimers; - uint32_t nMaxTimers; - uint64_t nGroupMask; - float fReference; - uint64_t* pTimers; -}; - -struct SOptionDesc -{ - SOptionDesc()=default; - SOptionDesc(uint8_t nSubType_, uint8_t nIndex_, const char* fmt, ...):nSubType(nSubType_), nIndex(nIndex_) - { - va_list args; - va_start (args, fmt); - vsprintf(Text, fmt, args); - va_end(args); - } - char Text[32]; - uint8_t nSubType; - uint8_t nIndex; - bool bSelected; -}; -static const std::array g_MicroProfileAggregatePresets{0, 10, 20, 30, 60, 120}; -static const std::array g_MicroProfileReferenceTimePresets{5.f, 10.f, 15.f,20.f, 33.33f, 66.66f, 100.f, 250.f, 500.f, 1000.f}; -static const std::array g_MicroProfileOpacityPresets{0x40, 0x80, 0xc0, 0xff}; -static const std::array g_MicroProfilePresetNames -{ - MICROPROFILE_DEFAULT_PRESET, - "Render", - "GPU", - "Lighting", - "AI", - "Visibility", - "Sound", -}; - -enum -{ - MICROPROFILE_NUM_REFERENCE_PRESETS = g_MicroProfileReferenceTimePresets.size(), - MICROPROFILE_NUM_OPACITY_PRESETS = g_MicroProfileOpacityPresets.size(), -#if MICROPROFILE_CONTEXT_SWITCH_TRACE - MICROPROFILE_OPTION_SIZE = MICROPROFILE_NUM_REFERENCE_PRESETS + MICROPROFILE_NUM_OPACITY_PRESETS * 2 + 2 + 7, -#else - MICROPROFILE_OPTION_SIZE = MICROPROFILE_NUM_REFERENCE_PRESETS + MICROPROFILE_NUM_OPACITY_PRESETS * 2 + 2 + 3, -#endif -}; - -struct MicroProfileUI -{ - //menu/mouse over stuff - uint64_t nHoverToken; - int64_t nHoverTime; - int nHoverFrame; -#if MICROPROFILE_DEBUG - uint64_t nHoverAddressEnter; - uint64_t nHoverAddressLeave; -#endif - - uint32_t nWidth; - uint32_t nHeight; - - - int nOffsetX; - int nOffsetY; - float fDetailedOffset; //display offset relative to start of latest displayable frame. - float fDetailedRange; //no. of ms to display - float fDetailedOffsetTarget; - float fDetailedRangeTarget; - uint32_t nOpacityBackground; - uint32_t nOpacityForeground; - bool bShowSpikes; - - - - uint32_t nMouseX; - uint32_t nMouseY; - uint32_t nMouseDownX; - uint32_t nMouseDownY; - int nMouseWheelDelta; - uint32_t nMouseDownLeft; - uint32_t nMouseDownRight; - uint32_t nMouseLeft; - uint32_t nMouseRight; - uint32_t nMouseLeftMod; - uint32_t nMouseRightMod; - uint32_t nModDown; - uint32_t nActiveMenu; - - MicroProfileLogEntry* pDisplayMouseOver; - - int64_t nRangeBegin; - int64_t nRangeEnd; - int64_t nRangeBeginGpu; - int64_t nRangeEndGpu; - uint32_t nRangeBeginIndex; - uint32_t nRangeEndIndex; - MicroProfileThreadLog* pRangeLog; - uint32_t nHoverColor; - uint32_t nHoverColorShared; - - MicroProfileStringArray LockedToolTips[MICROPROFILE_TOOLTIP_MAX_LOCKED]; - uint32_t nLockedToolTipColor[MICROPROFILE_TOOLTIP_MAX_LOCKED]; - int LockedToolTipFront; - - MicroProfileGroupMenuItem GroupMenu[MICROPROFILE_MAX_GROUPS + MICROPROFILE_MAX_CATEGORIES]; - uint32_t GroupMenuCount; - - - uint32_t nCustomActive; - uint32_t nCustomTimerCount; - uint32_t nCustomCount; - MicroProfileCustom Custom[MICROPROFILE_CUSTOM_MAX]; - uint64_t CustomTimer[MICROPROFILE_CUSTOM_MAX_TIMERS]; - - SOptionDesc Options[MICROPROFILE_OPTION_SIZE]; - - -}; - -MicroProfileUI g_MicroProfileUI; -#define UI g_MicroProfileUI -static const std::array g_nMicroProfileBackColors{ 0x474747, 0x313131 }; -#define MICROPROFILE_NUM_CONTEXT_SWITCH_COLORS 16 -static const std::array g_nMicroProfileContextSwitchThreadColors //palette generated by http://tools.medialab.sciences-po.fr/iwanthue/index.php -{ - 0x63607B, - 0x755E2B, - 0x326A55, - 0x523135, - 0x904F42, - 0x87536B, - 0x346875, - 0x5E6046, - 0x35404C, - 0x224038, - 0x413D1E, - 0x5E3A26, - 0x5D6161, - 0x4C6234, - 0x7D564F, - 0x5C4352, -}; - - -void MicroProfileInitUI() -{ - static bool bInitialized = false; - if(!bInitialized) - { - bInitialized = true; - memset(&g_MicroProfileUI, 0, sizeof(g_MicroProfileUI)); - UI.nActiveMenu = UINT32_MAX; - UI.fDetailedOffsetTarget = UI.fDetailedOffset = 0.f; - UI.fDetailedRangeTarget = UI.fDetailedRange = 50.f; - - UI.nOpacityBackground = 0xff<<24; - UI.nOpacityForeground = 0xff<<24; - - UI.bShowSpikes = false; - - UI.nWidth = 100; - UI.nHeight = 100; - - UI.nCustomActive = UINT32_MAX; - UI.nCustomTimerCount = 0; - UI.nCustomCount = 0; - - int nIndex = 0; - UI.Options[nIndex++] = SOptionDesc(0xff, 0, "%s", "Reference"); - for(int i = 0; i < MICROPROFILE_NUM_REFERENCE_PRESETS; ++i) - { - UI.Options[nIndex++] = SOptionDesc(0, i, " %6.2fms", g_MicroProfileReferenceTimePresets[i]); - } - UI.Options[nIndex++] = SOptionDesc(0xff, 0, "%s", "BG Opacity"); - for(int i = 0; i < MICROPROFILE_NUM_OPACITY_PRESETS; ++i) - { - UI.Options[nIndex++] = SOptionDesc(1, i, " %7d%%", (i+1)*25); - } - UI.Options[nIndex++] = SOptionDesc(0xff, 0, "%s", "FG Opacity"); - for(int i = 0; i < MICROPROFILE_NUM_OPACITY_PRESETS; ++i) - { - UI.Options[nIndex++] = SOptionDesc(2, i, " %7d%%", (i+1)*25); - } - UI.Options[nIndex++] = SOptionDesc(0xff, 0, "%s", "Spike Display"); - UI.Options[nIndex++] = SOptionDesc(3, 0, "%s", " Enable"); - -#if MICROPROFILE_CONTEXT_SWITCH_TRACE - UI.Options[nIndex++] = SOptionDesc(0xff, 0, "%s", "CSwitch Trace"); - UI.Options[nIndex++] = SOptionDesc(4, 0, "%s", " Enable"); - UI.Options[nIndex++] = SOptionDesc(4, 1, "%s", " All Threads"); - UI.Options[nIndex++] = SOptionDesc(4, 2, "%s", " No Bars"); -#endif - MP_ASSERT(nIndex == MICROPROFILE_OPTION_SIZE); - } -} - -void MicroProfileSetDisplayMode(int nValue) -{ - MicroProfile& S = *MicroProfileGet(); - nValue = nValue >= 0 && nValue < 4 ? nValue : S.nDisplay; - S.nDisplay = nValue; - UI.nOffsetY = 0; -} - -void MicroProfileToggleDisplayMode() -{ - MicroProfile& S = *MicroProfileGet(); - S.nDisplay = (S.nDisplay + 1) % 4; - UI.nOffsetY = 0; -} - - -inline void MicroProfileStringArrayClear(MicroProfileStringArray* pArray) -{ - pArray->nNumStrings = 0; - pArray->pBufferPos = &pArray->Buffer[0]; -} - -inline void MicroProfileStringArrayAddLiteral(MicroProfileStringArray* pArray, const char* pLiteral) -{ - MP_ASSERT(pArray->nNumStrings < MICROPROFILE_TOOLTIP_MAX_STRINGS); - pArray->ppStrings[pArray->nNumStrings++] = pLiteral; -} - -inline void MicroProfileStringArrayFormat(MicroProfileStringArray* pArray, const char* fmt, ...) -{ - MP_ASSERT(pArray->nNumStrings < MICROPROFILE_TOOLTIP_MAX_STRINGS); - pArray->ppStrings[pArray->nNumStrings++] = pArray->pBufferPos; - va_list args; - va_start (args, fmt); - pArray->pBufferPos += 1 + vsprintf(pArray->pBufferPos, fmt, args); - va_end(args); - MP_ASSERT(pArray->pBufferPos < pArray->Buffer + MICROPROFILE_TOOLTIP_STRING_BUFFER_SIZE); -} -inline void MicroProfileStringArrayCopy(MicroProfileStringArray* pDest, MicroProfileStringArray* pSrc) -{ - memcpy(&pDest->ppStrings[0], &pSrc->ppStrings[0], sizeof(pDest->ppStrings)); - memcpy(&pDest->Buffer[0], &pSrc->Buffer[0], sizeof(pDest->Buffer)); - for(uint32_t i = 0; i < MICROPROFILE_TOOLTIP_MAX_STRINGS; ++i) - { - if(i < pSrc->nNumStrings) - { - if(pSrc->ppStrings[i] >= &pSrc->Buffer[0] && pSrc->ppStrings[i] < &pSrc->Buffer[0] + MICROPROFILE_TOOLTIP_STRING_BUFFER_SIZE) - { - pDest->ppStrings[i] += &pDest->Buffer[0] - &pSrc->Buffer[0]; - } - } - } - pDest->nNumStrings = pSrc->nNumStrings; -} - -inline void MicroProfileFloatWindowSize(const char** ppStrings, uint32_t nNumStrings, uint32_t* pColors, uint32_t& nWidth, uint32_t& nHeight, uint32_t* pStringLengths = 0) -{ - uint32_t* nStringLengths = pStringLengths ? pStringLengths : (uint32_t*)alloca(nNumStrings * sizeof(uint32_t)); - uint32_t nTextCount = nNumStrings/2; - for(uint32_t i = 0; i < nTextCount; ++i) - { - uint32_t i0 = i * 2; - uint32_t s0, s1; - nStringLengths[i0] = s0 = (uint32_t)strlen(ppStrings[i0]); - nStringLengths[i0+1] = s1 = (uint32_t)strlen(ppStrings[i0+1]); - nWidth = MicroProfileMax(s0+s1, nWidth); - } - nWidth = (MICROPROFILE_TEXT_WIDTH+1) * (2+nWidth) + 2 * MICROPROFILE_BORDER_SIZE; - if(pColors) - nWidth += MICROPROFILE_TEXT_WIDTH + 1; - nHeight = (MICROPROFILE_TEXT_HEIGHT+1) * nTextCount + 2 * MICROPROFILE_BORDER_SIZE; -} - -inline void MicroProfileDrawFloatWindow(uint32_t nX, uint32_t nY, const char** ppStrings, uint32_t nNumStrings, uint32_t nColor, uint32_t* pColors = 0) -{ - uint32_t nWidth = 0, nHeight = 0; - uint32_t* nStringLengths = (uint32_t*)alloca(nNumStrings * sizeof(uint32_t)); - MicroProfileFloatWindowSize(ppStrings, nNumStrings, pColors, nWidth, nHeight, nStringLengths); - uint32_t nTextCount = nNumStrings/2; - if(nX + nWidth > UI.nWidth) - nX = UI.nWidth - nWidth; - if(nY + nHeight > UI.nHeight) - nY = UI.nHeight - nHeight; - MicroProfileDrawBox(nX-1, nY-1, nX + nWidth+1, nY + nHeight+1, 0xff000000|nColor); - MicroProfileDrawBox(nX, nY, nX + nWidth, nY + nHeight, 0xff000000); - if(pColors) - { - nX += MICROPROFILE_TEXT_WIDTH+1; - nWidth -= MICROPROFILE_TEXT_WIDTH+1; - } - for(uint32_t i = 0; i < nTextCount; ++i) - { - int i0 = i * 2; - if(pColors) - { - MicroProfileDrawBox(nX-MICROPROFILE_TEXT_WIDTH, nY, nX, nY + MICROPROFILE_TEXT_WIDTH, pColors[i]|0xff000000); - } - MicroProfileDrawText(nX + 1, nY + 1, UINT32_MAX, ppStrings[i0], (uint32_t)strlen(ppStrings[i0])); - MicroProfileDrawText(nX + nWidth - nStringLengths[i0+1] * (MICROPROFILE_TEXT_WIDTH+1), nY + 1, UINT32_MAX, ppStrings[i0+1], (uint32_t)strlen(ppStrings[i0+1])); - nY += (MICROPROFILE_TEXT_HEIGHT+1); - } -} -inline void MicroProfileDrawTextBox(uint32_t nX, uint32_t nY, const char** ppStrings, uint32_t nNumStrings, uint32_t nColor, uint32_t* pColors = 0) -{ - uint32_t nWidth = 0, nHeight = 0; - uint32_t* nStringLengths = (uint32_t*)alloca(nNumStrings * sizeof(uint32_t)); - for(uint32_t i = 0; i < nNumStrings; ++i) - { - nStringLengths[i] = (uint32_t)strlen(ppStrings[i]); - nWidth = MicroProfileMax(nWidth, nStringLengths[i]); - nHeight++; - } - nWidth = (MICROPROFILE_TEXT_WIDTH+1) * (2+nWidth) + 2 * MICROPROFILE_BORDER_SIZE; - nHeight = (MICROPROFILE_TEXT_HEIGHT+1) * nHeight + 2 * MICROPROFILE_BORDER_SIZE; - if(nX + nWidth > UI.nWidth) - nX = UI.nWidth - nWidth; - if(nY + nHeight > UI.nHeight) - nY = UI.nHeight - nHeight; - MicroProfileDrawBox(nX, nY, nX + nWidth, nY + nHeight, 0xff000000); - for(uint32_t i = 0; i < nNumStrings; ++i) - { - MicroProfileDrawText(nX + 1, nY + 1, UINT32_MAX, ppStrings[i], (uint32_t)strlen(ppStrings[i])); - nY += (MICROPROFILE_TEXT_HEIGHT+1); - } -} - - - -inline void MicroProfileToolTipMeta(MicroProfileStringArray* pToolTip) -{ - MicroProfile& S = *MicroProfileGet(); - if(UI.nRangeBeginIndex != UI.nRangeEndIndex && UI.pRangeLog) - { - uint64_t nMetaSum[MICROPROFILE_META_MAX] = {0}; - uint64_t nMetaSumInclusive[MICROPROFILE_META_MAX] = {0}; - int nStackDepth = 0; - uint32_t nRange[2][2]; - MicroProfileThreadLog* pLog = UI.pRangeLog; - - - MicroProfileGetRange(UI.nRangeEndIndex, UI.nRangeBeginIndex, nRange); - for(uint32_t i = 0; i < 2; ++i) - { - uint32_t nStart = nRange[i][0]; - uint32_t nEnd = nRange[i][1]; - for(uint32_t j = nStart; j < nEnd; ++j) - { - MicroProfileLogEntry LE = pLog->Log[j]; - int nType = MicroProfileLogType(LE); - switch(nType) - { - case MP_LOG_META: - { - int64_t nMetaIndex = MicroProfileLogTimerIndex(LE); - int64_t nMetaCount = MicroProfileLogGetTick(LE); - MP_ASSERT(nMetaIndex < MICROPROFILE_META_MAX); - if(nStackDepth>1) - { - nMetaSumInclusive[nMetaIndex] += nMetaCount; - } - else - { - nMetaSum[nMetaIndex] += nMetaCount; - } - } - break; - case MP_LOG_LEAVE: - if(nStackDepth) - { - nStackDepth--; - } - else - { - for(int k = 0; k < MICROPROFILE_META_MAX; ++k) - { - nMetaSumInclusive[k] += nMetaSum[k]; - nMetaSum[k] = 0; - } - } - break; - case MP_LOG_ENTER: - nStackDepth++; - break; - } - - } - } - bool bSpaced = false; - for(int i = 0; i < MICROPROFILE_META_MAX; ++i) - { - if(S.MetaCounters[i].pName && (nMetaSum[i]||nMetaSumInclusive[i])) - { - if(!bSpaced) - { - bSpaced = true; - MicroProfileStringArrayAddLiteral(pToolTip, ""); - MicroProfileStringArrayAddLiteral(pToolTip, ""); - } - MicroProfileStringArrayFormat(pToolTip, "%s excl", S.MetaCounters[i].pName); - MicroProfileStringArrayFormat(pToolTip, "%5d", nMetaSum[i]); - MicroProfileStringArrayFormat(pToolTip, "%s incl", S.MetaCounters[i].pName); - MicroProfileStringArrayFormat(pToolTip, "%5d", nMetaSum[i] + nMetaSumInclusive[i]); - } - } - } -} - -inline void MicroProfileDrawFloatTooltip(uint32_t nX, uint32_t nY, uint32_t nToken, uint64_t nTime) -{ - MicroProfile& S = *MicroProfileGet(); - - uint32_t nIndex = MicroProfileGetTimerIndex(nToken); - uint32_t nAggregateFrames = S.nAggregateFrames ? S.nAggregateFrames : 1; - uint32_t nAggregateCount = S.Aggregate[nIndex].nCount ? S.Aggregate[nIndex].nCount : 1; - - uint32_t nGroupId = MicroProfileGetGroupIndex(nToken); - uint32_t nTimerId = MicroProfileGetTimerIndex(nToken); - bool bGpu = S.GroupInfo[nGroupId].Type == MicroProfileTokenTypeGpu; - - float fToMs = MicroProfileTickToMsMultiplier(bGpu ? MicroProfileTicksPerSecondGpu() : MicroProfileTicksPerSecondCpu()); - - float fMs = fToMs * (nTime); - float fFrameMs = fToMs * (S.Frame[nIndex].nTicks); - float fAverage = fToMs * (S.Aggregate[nIndex].nTicks/nAggregateFrames); - float fCallAverage = fToMs * (S.Aggregate[nIndex].nTicks / nAggregateCount); - float fMax = fToMs * (S.AggregateMax[nIndex]); - - float fFrameMsExclusive = fToMs * (S.FrameExclusive[nIndex]); - float fAverageExclusive = fToMs * (S.AggregateExclusive[nIndex]/nAggregateFrames); - float fMaxExclusive = fToMs * (S.AggregateMaxExclusive[nIndex]); - - float fGroupAverage = fToMs * (S.AggregateGroup[nGroupId] / nAggregateFrames); - float fGroupMax = fToMs * (S.AggregateGroupMax[nGroupId]); - float fGroup = fToMs * (S.FrameGroup[nGroupId]); - - - MicroProfileStringArray ToolTip; - MicroProfileStringArrayClear(&ToolTip); - const char* pGroupName = S.GroupInfo[nGroupId].pName; - const char* pTimerName = S.TimerInfo[nTimerId].pName; - MicroProfileStringArrayAddLiteral(&ToolTip, "Timer:"); - MicroProfileStringArrayFormat(&ToolTip, "%s", pTimerName); - -#if MICROPROFILE_DEBUG - MicroProfileStringArrayFormat(&ToolTip,"0x%p", UI.nHoverAddressEnter); - MicroProfileStringArrayFormat(&ToolTip,"0x%p", UI.nHoverAddressLeave); -#endif - - if(nTime != (uint64_t)0) - { - MicroProfileStringArrayAddLiteral(&ToolTip, "Time:"); - MicroProfileStringArrayFormat(&ToolTip,"%6.3fms", fMs); - MicroProfileStringArrayAddLiteral(&ToolTip, ""); - MicroProfileStringArrayAddLiteral(&ToolTip, ""); - } - - MicroProfileStringArrayAddLiteral(&ToolTip, "Frame Time:"); - MicroProfileStringArrayFormat(&ToolTip,"%6.3fms", fFrameMs); - - MicroProfileStringArrayAddLiteral(&ToolTip, "Average:"); - MicroProfileStringArrayFormat(&ToolTip,"%6.3fms", fAverage); - - MicroProfileStringArrayAddLiteral(&ToolTip, "Max:"); - MicroProfileStringArrayFormat(&ToolTip,"%6.3fms", fMax); - - MicroProfileStringArrayAddLiteral(&ToolTip, ""); - MicroProfileStringArrayAddLiteral(&ToolTip, ""); - - MicroProfileStringArrayAddLiteral(&ToolTip, "Frame Call Average:"); - MicroProfileStringArrayFormat(&ToolTip,"%6.3fms", fCallAverage); - - MicroProfileStringArrayAddLiteral(&ToolTip, "Frame Call Count:"); - MicroProfileStringArrayFormat(&ToolTip, "%6d", nAggregateCount / nAggregateFrames); - - MicroProfileStringArrayAddLiteral(&ToolTip, ""); - MicroProfileStringArrayAddLiteral(&ToolTip, ""); - - MicroProfileStringArrayAddLiteral(&ToolTip, "Exclusive Frame Time:"); - MicroProfileStringArrayFormat(&ToolTip, "%6.3fms", fFrameMsExclusive); - - MicroProfileStringArrayAddLiteral(&ToolTip, "Exclusive Average:"); - MicroProfileStringArrayFormat(&ToolTip, "%6.3fms", fAverageExclusive); - - MicroProfileStringArrayAddLiteral(&ToolTip, "Exclusive Max:"); - MicroProfileStringArrayFormat(&ToolTip, "%6.3fms", fMaxExclusive); - - MicroProfileStringArrayAddLiteral(&ToolTip, ""); - MicroProfileStringArrayAddLiteral(&ToolTip, ""); - - MicroProfileStringArrayAddLiteral(&ToolTip, "Group:"); - MicroProfileStringArrayFormat(&ToolTip, "%s", pGroupName); - MicroProfileStringArrayAddLiteral(&ToolTip, "Frame Time:"); - MicroProfileStringArrayFormat(&ToolTip, "%6.3f", fGroup); - MicroProfileStringArrayAddLiteral(&ToolTip, "Frame Average:"); - MicroProfileStringArrayFormat(&ToolTip, "%6.3f", fGroupAverage); - MicroProfileStringArrayAddLiteral(&ToolTip, "Frame Max:"); - MicroProfileStringArrayFormat(&ToolTip, "%6.3f", fGroupMax); - - - - - MicroProfileToolTipMeta(&ToolTip); - - - MicroProfileDrawFloatWindow(nX, nY+20, &ToolTip.ppStrings[0], ToolTip.nNumStrings, S.TimerInfo[nTimerId].nColor); - - if(UI.nMouseLeftMod) - { - int nToolTipIndex = (g_MicroProfileUI.LockedToolTipFront + MICROPROFILE_TOOLTIP_MAX_LOCKED - 1) % MICROPROFILE_TOOLTIP_MAX_LOCKED; - g_MicroProfileUI.nLockedToolTipColor[nToolTipIndex] = S.TimerInfo[nTimerId].nColor; - MicroProfileStringArrayCopy(&g_MicroProfileUI.LockedToolTips[nToolTipIndex], &ToolTip); - g_MicroProfileUI.LockedToolTipFront = nToolTipIndex; - - } -} - - -inline void MicroProfileZoomTo(int64_t nTickStart, int64_t nTickEnd) -{ - MicroProfile& S = *MicroProfileGet(); - - int64_t nStart = S.Frames[S.nFrameCurrent].nFrameStartCpu; - float fToMs = MicroProfileTickToMsMultiplier(MicroProfileTicksPerSecondCpu()); - UI.fDetailedOffsetTarget = MicroProfileLogTickDifference(nStart, nTickStart) * fToMs; - UI.fDetailedRangeTarget = MicroProfileLogTickDifference(nTickStart, nTickEnd) * fToMs; -} - -inline void MicroProfileCenter(int64_t nTickCenter) -{ - MicroProfile& S = *MicroProfileGet(); - int64_t nStart = S.Frames[S.nFrameCurrent].nFrameStartCpu; - float fToMs = MicroProfileTickToMsMultiplier(MicroProfileTicksPerSecondCpu()); - float fCenter = MicroProfileLogTickDifference(nStart, nTickCenter) * fToMs; - UI.fDetailedOffsetTarget = UI.fDetailedOffset = fCenter - 0.5f * UI.fDetailedRange; -} -#ifdef MICROPROFILE_DEBUG -uint64_t* g_pMicroProfileDumpStart = 0; -uint64_t* g_pMicroProfileDumpEnd = 0; -inline void MicroProfileDebugDumpRange() -{ - MicroProfile& S = *MicroProfileGet(); - if(g_pMicroProfileDumpStart != g_pMicroProfileDumpEnd) - { - uint64_t* pStart = g_pMicroProfileDumpStart; - uint64_t* pEnd = g_pMicroProfileDumpEnd; - while(pStart != pEnd) - { - uint64_t nTick = MicroProfileLogGetTick(*pStart); - uint64_t nToken = MicroProfileLogTimerIndex(*pStart); - uint32_t nTimerId = MicroProfileGetTimerIndex(nToken); - - const char* pTimerName = S.TimerInfo[nTimerId].pName; - char buffer[256]; - int type = MicroProfileLogType(*pStart); - - const char* pBegin = type == MP_LOG_LEAVE ? "END" : - (type == MP_LOG_ENTER ? "BEGIN" : "META"); - snprintf(buffer, 255, "DUMP 0x%p: %s :: %llx: %s\n", pStart, pBegin, nTick, pTimerName); -#ifdef _WIN32 - OutputDebugString(buffer); -#else - printf("%s", buffer); -#endif - pStart++; - } - - g_pMicroProfileDumpStart = g_pMicroProfileDumpEnd; - } -} -#define MP_DEBUG_DUMP_RANGE() MicroProfileDebugDumpRange(); -#else -#define MP_DEBUG_DUMP_RANGE() do{} while(0) -#endif - -#define MICROPROFILE_HOVER_DIST 0.5f - -inline void MicroProfileDrawDetailedContextSwitchBars(uint32_t nY, uint32_t nThreadId, uint32_t nContextSwitchStart, uint32_t nContextSwitchEnd, int64_t nBaseTicks, uint32_t nBaseY) -{ - MicroProfile& S = *MicroProfileGet(); - int64_t nTickIn = -1; - uint32_t nThreadBefore = UINT32_MAX; - float fToMs = MicroProfileTickToMsMultiplier(MicroProfileTicksPerSecondCpu()); - float fMsToScreen = UI.nWidth / UI.fDetailedRange; - float fMouseX = (float)UI.nMouseX; - float fMouseY = (float)UI.nMouseY; - - - for(uint32_t j = nContextSwitchStart; j != nContextSwitchEnd; j = (j+1) % MICROPROFILE_CONTEXT_SWITCH_BUFFER_SIZE) - { - MP_ASSERT(j < MICROPROFILE_CONTEXT_SWITCH_BUFFER_SIZE); - MicroProfileContextSwitch CS = S.ContextSwitch[j]; - - if(nTickIn == -1) - { - if(CS.nThreadIn == nThreadId) - { - nTickIn = CS.nTicks; - nThreadBefore = CS.nThreadOut; - } - } - else - { - if(CS.nThreadOut == nThreadId) - { - int64_t nTickOut = CS.nTicks; - float fMsStart = fToMs * MicroProfileLogTickDifference(nBaseTicks, nTickIn); - float fMsEnd = fToMs * MicroProfileLogTickDifference(nBaseTicks, nTickOut); - if(fMsStart <= fMsEnd) - { - float fXStart = fMsStart * fMsToScreen; - float fXEnd = fMsEnd * fMsToScreen; - float fYStart = (float)nY; - float fYEnd = fYStart + (MICROPROFILE_DETAILED_CONTEXT_SWITCH_HEIGHT); - uint32_t nColor = g_nMicroProfileContextSwitchThreadColors[CS.nCpu%MICROPROFILE_NUM_CONTEXT_SWITCH_COLORS]; - float fXDist = MicroProfileMax(fXStart - fMouseX, fMouseX - fXEnd); - bool bHover = fXDist < MICROPROFILE_HOVER_DIST && fYStart <= fMouseY && fMouseY <= fYEnd && nBaseY < fMouseY; - if(bHover) - { - UI.nRangeBegin = nTickIn; - UI.nRangeEnd = nTickOut; - S.nContextSwitchHoverTickIn = nTickIn; - S.nContextSwitchHoverTickOut = nTickOut; - S.nContextSwitchHoverThread = CS.nThreadOut; - S.nContextSwitchHoverThreadBefore = nThreadBefore; - S.nContextSwitchHoverThreadAfter = CS.nThreadIn; - S.nContextSwitchHoverCpuNext = CS.nCpu; - nColor = UI.nHoverColor; - } - if(CS.nCpu == S.nContextSwitchHoverCpu) - { - nColor = UI.nHoverColorShared; - } - MicroProfileDrawBox(fXStart, fYStart, fXEnd, fYEnd, nColor|UI.nOpacityForeground, MicroProfileBoxTypeFlat); - } - nTickIn = -1; - } - } - } -} - -inline void MicroProfileDrawDetailedBars(uint32_t nWidth, uint32_t nHeight, int nBaseY, int nSelectedFrame) -{ - MicroProfile& S = *MicroProfileGet(); - MP_DEBUG_DUMP_RANGE(); - int nY = nBaseY - UI.nOffsetY; - - uint32_t nFrameNext = (S.nFrameCurrent+1) % MICROPROFILE_MAX_FRAME_HISTORY; - MicroProfileFrameState* pFrameCurrent = &S.Frames[S.nFrameCurrent]; - MicroProfileFrameState* pFrameNext = &S.Frames[nFrameNext]; - - UI.nRangeBegin = 0; - UI.nRangeEnd = 0; - UI.nRangeBeginGpu = 0; - UI.nRangeEndGpu = 0; - UI.nRangeBeginIndex = UI.nRangeEndIndex = 0; - UI.pRangeLog = 0; - int64_t nFrameStartCpu = pFrameCurrent->nFrameStartCpu; - int64_t nFrameStartGpu = pFrameCurrent->nFrameStartGpu; - int64_t nTicksPerSecondCpu = MicroProfileTicksPerSecondCpu(); - int64_t nTicksPerSecondGpu = MicroProfileTicksPerSecondGpu(); - float fToMsCpu = MicroProfileTickToMsMultiplier(nTicksPerSecondCpu); - float fToMsGpu = MicroProfileTickToMsMultiplier(nTicksPerSecondGpu); - - float fDetailedOffset = UI.fDetailedOffset; - float fDetailedRange = UI.fDetailedRange; - - - int64_t nDetailedOffsetTicksCpu = MicroProfileMsToTick(fDetailedOffset, MicroProfileTicksPerSecondCpu()); - int64_t nDetailedOffsetTicksGpu = MicroProfileMsToTick(fDetailedOffset, MicroProfileTicksPerSecondGpu()); - int64_t nBaseTicksCpu = nDetailedOffsetTicksCpu + nFrameStartCpu; - int64_t nBaseTicksGpu = nDetailedOffsetTicksGpu + nFrameStartGpu; - int64_t nBaseTicksEndCpu = nBaseTicksCpu + MicroProfileMsToTick(fDetailedRange, MicroProfileTicksPerSecondCpu()); - - int64_t nTickReferenceCpu = 0, nTickReferenceGpu = 0; - static int64_t nRefCpu = 0, nRefGpu = 0; - if(MicroProfileGetGpuTickReference(&nTickReferenceCpu, &nTickReferenceGpu)) - { - if(0 == nRefCpu || std::abs(nRefCpu-nBaseTicksCpu) > std::abs(nTickReferenceCpu-nBaseTicksCpu)) - { - nRefCpu = nTickReferenceCpu; - nRefGpu = nTickReferenceGpu; - } - else - { - nTickReferenceCpu = nRefCpu; - nTickReferenceGpu = nRefGpu; - } - nBaseTicksGpu = (nBaseTicksCpu - nTickReferenceCpu) * nTicksPerSecondGpu / nTicksPerSecondCpu + nTickReferenceGpu; - } - int64_t nBaseTicksEndGpu = nBaseTicksCpu + MicroProfileMsToTick(fDetailedRange, MicroProfileTicksPerSecondCpu()); - - MicroProfileFrameState* pFrameFirst = pFrameCurrent; - int64_t nGapTime = MicroProfileTicksPerSecondCpu() * MICROPROFILE_GAP_TIME / 1000; - for(uint32_t i = 0; i < MICROPROFILE_MAX_FRAME_HISTORY - MICROPROFILE_GPU_FRAME_DELAY; ++i) - { - uint32_t nNextIndex = (S.nFrameCurrent + MICROPROFILE_MAX_FRAME_HISTORY - i) % MICROPROFILE_MAX_FRAME_HISTORY; - pFrameFirst = &S.Frames[nNextIndex]; - if(pFrameFirst->nFrameStartCpu <= nBaseTicksCpu-nGapTime) - break; - } - - float fMsBase = fToMsCpu * nDetailedOffsetTicksCpu; - float fMs = fDetailedRange; - float fMsEnd = fMs + fMsBase; - float fWidth = (float)nWidth; - float fMsToScreen = fWidth / fMs; - - { - float fRate = floor(2*(log10(fMs)-1))/2; - float fStep = powf(10.f, fRate); - float fRcpStep = 1.f / fStep; - int nColorIndex = (int)(floor(fMsBase*fRcpStep)); - float fStart = floor(fMsBase*fRcpStep) * fStep; - for(float f = fStart; f < fMsEnd; ) - { - float fNext = f + fStep; - MicroProfileDrawBox(((f-fMsBase) * fMsToScreen), nBaseY, (fNext-fMsBase) * fMsToScreen+1, nBaseY + nHeight, UI.nOpacityBackground | g_nMicroProfileBackColors[nColorIndex++ & 1]); - f = fNext; - } - } - - nY += MICROPROFILE_TEXT_HEIGHT+1; - MicroProfileLogEntry* pMouseOver = UI.pDisplayMouseOver; - MicroProfileLogEntry* pMouseOverNext = 0; - uint64_t nMouseOverToken = pMouseOver ? MicroProfileLogTimerIndex(*pMouseOver) : MICROPROFILE_INVALID_TOKEN; - float fMouseX = (float)UI.nMouseX; - float fMouseY = (float)UI.nMouseY; - uint64_t nHoverToken = MICROPROFILE_INVALID_TOKEN; - int64_t nHoverTime = 0; - - static int nHoverCounter = 155; - static int nHoverCounterDelta = 10; - nHoverCounter += nHoverCounterDelta; - if(nHoverCounter >= 245) - nHoverCounterDelta = -10; - else if(nHoverCounter < 100) - nHoverCounterDelta = 10; - UI.nHoverColor = (nHoverCounter<<24)|(nHoverCounter<<16)|(nHoverCounter<<8)|nHoverCounter; - uint32_t nHoverCounterShared = nHoverCounter>>2; - UI.nHoverColorShared = (nHoverCounterShared<<24)|(nHoverCounterShared<<16)|(nHoverCounterShared<<8)|nHoverCounterShared; - - uint32_t nLinesDrawn[MICROPROFILE_STACK_MAX]={0}; - - uint32_t nContextSwitchHoverThreadAfter = S.nContextSwitchHoverThreadAfter; - uint32_t nContextSwitchHoverThreadBefore = S.nContextSwitchHoverThreadBefore; - S.nContextSwitchHoverThread = S.nContextSwitchHoverThreadAfter = S.nContextSwitchHoverThreadBefore = UINT32_MAX; - - uint32_t nContextSwitchStart = UINT32_MAX; - uint32_t nContextSwitchEnd = UINT32_MAX; - S.nContextSwitchHoverCpuNext = 0xff; - S.nContextSwitchHoverTickIn = -1; - S.nContextSwitchHoverTickOut = -1; - if(S.bContextSwitchRunning) - { - MicroProfileContextSwitchSearch(&nContextSwitchStart, &nContextSwitchEnd, nBaseTicksCpu, nBaseTicksEndCpu); - } - - bool bSkipBarView = S.bContextSwitchRunning && S.bContextSwitchNoBars; - - if(!bSkipBarView) - { - for(uint32_t i = 0; i < MICROPROFILE_MAX_THREADS; ++i) - { - MicroProfileThreadLog* pLog = S.Pool[i]; - if(!pLog) - continue; - - uint32_t nPut = pFrameNext->nLogStart[i]; - ///note: this may display new samples as old data, but this will only happen when - // unpaused, where the detailed view is hardly perceptible - uint32_t nFront = S.Pool[i]->nPut.load(std::memory_order_relaxed); - MicroProfileFrameState* pFrameLogFirst = pFrameCurrent; - MicroProfileFrameState* pFrameLogLast = pFrameNext; - uint32_t nGet = pFrameLogFirst->nLogStart[i]; - do - { - MP_ASSERT(pFrameLogFirst >= &S.Frames[0] && pFrameLogFirst < &S.Frames[MICROPROFILE_MAX_FRAME_HISTORY]); - uint32_t nNewGet = pFrameLogFirst->nLogStart[i]; - bool bIsValid = false; - if(nPut < nFront) - { - bIsValid = nNewGet <= nPut || nNewGet >= nFront; - } - else - { - bIsValid = nNewGet <= nPut && nNewGet >= nFront; - } - if(bIsValid) - { - nGet = nNewGet; - pFrameLogFirst--; - if(pFrameLogFirst < &S.Frames[0]) - pFrameLogFirst = &S.Frames[MICROPROFILE_MAX_FRAME_HISTORY-1]; - } - else - { - break; - } - }while(pFrameLogFirst != pFrameFirst); - - - if (nGet == UINT32_MAX) { - continue; - } - MP_ASSERT(nGet != UINT32_MAX); - - nPut = pFrameLogLast->nLogStart[i]; - - uint32_t nRange[2][2] = { {0, 0}, {0, 0}, }; - - MicroProfileGetRange(nPut, nGet, nRange); - if(nPut == nGet) - continue; - uint32_t nMaxStackDepth = 0; - - bool bGpu = pLog->nGpu != 0; - float fToMs = bGpu ? fToMsGpu : fToMsCpu; - int64_t nBaseTicks = bGpu ? nBaseTicksGpu : nBaseTicksCpu; - char ThreadName[MicroProfileThreadLog::THREAD_MAX_LEN + 16]; - uint64_t nThreadId = pLog->nThreadId; - snprintf(ThreadName, sizeof(ThreadName)-1, "%04" PRIx64 ": %s", nThreadId, &pLog->ThreadName[0] ); - nY += 3; - uint32_t nThreadColor = UINT32_MAX; - if(pLog->nThreadId == nContextSwitchHoverThreadAfter || pLog->nThreadId == nContextSwitchHoverThreadBefore) - nThreadColor = UI.nHoverColorShared|0x906060; - MicroProfileDrawText(0, nY, nThreadColor, &ThreadName[0], (uint32_t)strlen(&ThreadName[0])); - nY += 3; - nY += MICROPROFILE_TEXT_HEIGHT + 1; - - if(S.bContextSwitchRunning) - { - MicroProfileDrawDetailedContextSwitchBars(nY, pLog->nThreadId, nContextSwitchStart, nContextSwitchEnd, nBaseTicks, nBaseY); - nY -= MICROPROFILE_DETAILED_BAR_HEIGHT; - nY += MICROPROFILE_DETAILED_CONTEXT_SWITCH_HEIGHT+1; - } - - uint32_t nYDelta = MICROPROFILE_DETAILED_BAR_HEIGHT; - uint32_t nStack[MICROPROFILE_STACK_MAX]; - uint32_t nStackPos = 0; - for(uint32_t j = 0; j < 2; ++j) - { - uint32_t nStart = nRange[j][0]; - uint32_t nEnd = nRange[j][1]; - for(uint32_t k = nStart; k < nEnd; ++k) - { - MicroProfileLogEntry* pEntry = &pLog->Log[k]; - int nType = MicroProfileLogType(*pEntry); - if(MP_LOG_ENTER == nType) - { - MP_ASSERT(nStackPos < MICROPROFILE_STACK_MAX); - nStack[nStackPos++] = k; - } - else if(MP_LOG_META == nType) - { - - } - else if(MP_LOG_LEAVE == nType) - { - if(0 == nStackPos) - { - continue; - } - - MicroProfileLogEntry* pEntryEnter = &pLog->Log[nStack[nStackPos-1]]; - if(MicroProfileLogTimerIndex(*pEntryEnter) != MicroProfileLogTimerIndex(*pEntry)) - { - //uprintf("mismatch %llx %llx\n", pEntryEnter->nToken, pEntry->nToken); - continue; - } - int64_t nTickStart = MicroProfileLogGetTick(*pEntryEnter); - int64_t nTickEnd = MicroProfileLogGetTick(*pEntry); - uint64_t nTimerIndex = MicroProfileLogTimerIndex(*pEntry); - uint32_t nColor = S.TimerInfo[nTimerIndex].nColor; - if(nMouseOverToken == nTimerIndex) - { - if(pEntry == pMouseOver) - { - nColor = UI.nHoverColor; - if(bGpu) - { - UI.nRangeBeginGpu = *pEntryEnter; - UI.nRangeEndGpu = *pEntry; - uint32_t nCpuBegin = (nStack[nStackPos-1] + 1) % MICROPROFILE_BUFFER_SIZE; - uint32_t nCpuEnd = (k + 1) % MICROPROFILE_BUFFER_SIZE; - MicroProfileLogEntry LogCpuBegin = pLog->Log[nCpuBegin]; - MicroProfileLogEntry LogCpuEnd = pLog->Log[nCpuEnd]; - if(MicroProfileLogType(LogCpuBegin)==3 && MicroProfileLogType(LogCpuEnd) == 3) - { - UI.nRangeBegin = LogCpuBegin; - UI.nRangeEnd = LogCpuEnd; - } - UI.nRangeBeginIndex = nStack[nStackPos-1]; - UI.nRangeEndIndex = k; - UI.pRangeLog = pLog; - } - else - { - UI.nRangeBegin = *pEntryEnter; - UI.nRangeEnd = *pEntry; - UI.nRangeBeginIndex = nStack[nStackPos-1]; - UI.nRangeEndIndex = k; - UI.pRangeLog = pLog; - - } - } - else - { - nColor = UI.nHoverColorShared; - } - } - - nMaxStackDepth = MicroProfileMax(nMaxStackDepth, nStackPos); - float fMsStart = fToMs * MicroProfileLogTickDifference(nBaseTicks, nTickStart); - float fMsEnd2 = fToMs * MicroProfileLogTickDifference(nBaseTicks, nTickEnd); - float fXStart = fMsStart * fMsToScreen; - float fXEnd = fMsEnd2 * fMsToScreen; - float fYStart = (float)(nY + nStackPos * nYDelta); - float fYEnd = fYStart + (MICROPROFILE_DETAILED_BAR_HEIGHT); - float fXDist = MicroProfileMax(fXStart - fMouseX, fMouseX - fXEnd); - bool bHover = fXDist < MICROPROFILE_HOVER_DIST && fYStart <= fMouseY && fMouseY <= fYEnd && nBaseY < fMouseY; - uint32_t nIntegerWidth = (uint32_t)(fXEnd - fXStart); - if(nIntegerWidth) - { - if(bHover && UI.nActiveMenu == UINT32_MAX) - { - nHoverToken = MicroProfileLogTimerIndex(*pEntry); - #if MICROPROFILE_DEBUG - UI.nHoverAddressEnter = (uint64_t)pEntryEnter; - UI.nHoverAddressLeave = (uint64_t)pEntry; - #endif - nHoverTime = MicroProfileLogTickDifference(nTickStart, nTickEnd); - pMouseOverNext = pEntry; - } - - MicroProfileDrawBox(fXStart, fYStart, fXEnd, fYEnd, nColor|UI.nOpacityForeground, MicroProfileBoxTypeBar); -#if MICROPROFILE_DETAILED_BAR_NAMES - if(nIntegerWidth>3*MICROPROFILE_TEXT_WIDTH) - { - float fXStartText = MicroProfileMax(fXStart, 0.f); - int nTextWidth = (int)(fXEnd - fXStartText); - int nCharacters = (nTextWidth - 2*MICROPROFILE_TEXT_WIDTH) / MICROPROFILE_TEXT_WIDTH; - if(nCharacters>0) - { - MicroProfileDrawText(fXStartText + 1, fYStart + 1, UINT32_MAX, S.TimerInfo[nTimerIndex].pName, MicroProfileMin(S.TimerInfo[nTimerIndex].nNameLen, nCharacters)); - } - } -#endif - } - else - { - float fXAvg = 0.5f * (fXStart + fXEnd); - int nLineX = (int)floor(fXAvg+0.5f); - if(nLineX != (int)nLinesDrawn[nStackPos]) - { - if(bHover && UI.nActiveMenu == UINT32_MAX) - { - nHoverToken = (uint32_t)MicroProfileLogTimerIndex(*pEntry); - nHoverTime = MicroProfileLogTickDifference(nTickStart, nTickEnd); - pMouseOverNext = pEntry; - } - nLinesDrawn[nStackPos] = nLineX; - MicroProfileDrawLineVertical(nLineX, fYStart + 0.5f, fYEnd + 0.5f, nColor|UI.nOpacityForeground); - } - } - nStackPos--; - if(0 == nStackPos) - { - if(bGpu ? (nTickStart > nBaseTicksEndGpu) : (nTickStart > nBaseTicksEndCpu)) - { - break; - } - } - } - } - } - nY += nMaxStackDepth * nYDelta + MICROPROFILE_DETAILED_BAR_HEIGHT+1; - } - } - if(S.bContextSwitchRunning && (S.bContextSwitchAllThreads||S.bContextSwitchNoBars)) - { - uint32_t nNumThreads = 0; - uint32_t nThreads[MICROPROFILE_MAX_CONTEXT_SWITCH_THREADS]; - for(uint32_t i = 0; i < MICROPROFILE_MAX_THREADS && S.Pool[i]; ++i) - nThreads[nNumThreads++] = S.Pool[i]->nThreadId; - uint32_t nNumThreadsBase = nNumThreads; - if(S.bContextSwitchAllThreads) - { - for(uint32_t i = nContextSwitchStart; i != nContextSwitchEnd; i = (i+1) % MICROPROFILE_CONTEXT_SWITCH_BUFFER_SIZE) - { - MicroProfileContextSwitch CS = S.ContextSwitch[i]; - ThreadIdType nThreadId = CS.nThreadIn; - if(nThreadId) - { - bool bSeen = false; - for(uint32_t j = 0; j < nNumThreads; ++j) - { - if(nThreads[j] == nThreadId) - { - bSeen = true; - break; - } - } - if(!bSeen) - { - nThreads[nNumThreads++] = nThreadId; - } - } - if(nNumThreads == MICROPROFILE_MAX_CONTEXT_SWITCH_THREADS) - { - S.nOverflow = 10; - break; - } - } - std::sort(&nThreads[nNumThreadsBase], &nThreads[nNumThreads]); - } - uint32_t nStart = nNumThreadsBase; - if(S.bContextSwitchNoBars) - nStart = 0; - for(uint32_t i = nStart; i < nNumThreads; ++i) - { - ThreadIdType nThreadId = nThreads[i]; - if(nThreadId) - { - char ThreadName[MicroProfileThreadLog::THREAD_MAX_LEN + 16]; - const char* cLocal = MicroProfileIsLocalThread(nThreadId) ? "*": " "; - -#if defined(_WIN32) - // nThreadId is 32-bit on Windows - int nStrLen = snprintf(ThreadName, sizeof(ThreadName)-1, "%04x: %s%s", nThreadId, cLocal, i < nNumThreadsBase ? &S.Pool[i]->ThreadName[0] : MICROPROFILE_THREAD_NAME_FROM_ID(nThreadId) ); -#else - int nStrLen = snprintf(ThreadName, sizeof(ThreadName)-1, "%04" PRIx64 ": %s%s", nThreadId, cLocal, i < nNumThreadsBase ? &S.Pool[i]->ThreadName[0] : MICROPROFILE_THREAD_NAME_FROM_ID(nThreadId) ); -#endif - uint32_t nThreadColor = UINT32_MAX; - if(nThreadId == nContextSwitchHoverThreadAfter || nThreadId == nContextSwitchHoverThreadBefore) - nThreadColor = UI.nHoverColorShared|0x906060; - MicroProfileDrawDetailedContextSwitchBars(nY+2, nThreadId, nContextSwitchStart, nContextSwitchEnd, nBaseTicksCpu, nBaseY); - MicroProfileDrawText(0, nY, nThreadColor, &ThreadName[0], nStrLen); - nY += MICROPROFILE_TEXT_HEIGHT+1; - } - } - } - - S.nContextSwitchHoverCpu = S.nContextSwitchHoverCpuNext; - - UI.pDisplayMouseOver = pMouseOverNext; - - if(!S.nRunning) - { - if(nHoverToken != MICROPROFILE_INVALID_TOKEN && nHoverTime) - { - UI.nHoverToken = nHoverToken; - UI.nHoverTime = nHoverTime; - } - - if(nSelectedFrame != -1) - { - UI.nRangeBegin = S.Frames[nSelectedFrame].nFrameStartCpu; - UI.nRangeEnd = S.Frames[(nSelectedFrame+1)%MICROPROFILE_MAX_FRAME_HISTORY].nFrameStartCpu; - UI.nRangeBeginGpu = S.Frames[nSelectedFrame].nFrameStartGpu; - UI.nRangeEndGpu = S.Frames[(nSelectedFrame+1)%MICROPROFILE_MAX_FRAME_HISTORY].nFrameStartGpu; - } - if(UI.nRangeBegin != UI.nRangeEnd) - { - float fMsStart = fToMsCpu * MicroProfileLogTickDifference(nBaseTicksCpu, UI.nRangeBegin); - float fMsEnd3 = fToMsCpu * MicroProfileLogTickDifference(nBaseTicksCpu, UI.nRangeEnd); - float fXStart = fMsStart * fMsToScreen; - float fXEnd = fMsEnd3 * fMsToScreen; - MicroProfileDrawBox(fXStart, nBaseY, fXEnd, nHeight, MICROPROFILE_FRAME_COLOR_HIGHTLIGHT, MicroProfileBoxTypeFlat); - MicroProfileDrawLineVertical(fXStart, nBaseY, nHeight, MICROPROFILE_FRAME_COLOR_HIGHTLIGHT | 0x44000000); - MicroProfileDrawLineVertical(fXEnd, nBaseY, nHeight, MICROPROFILE_FRAME_COLOR_HIGHTLIGHT | 0x44000000); - - fMsStart += fDetailedOffset; - fMsEnd3 += fDetailedOffset; - char sBuffer[32]; - uint32_t nLenStart = snprintf(sBuffer, sizeof(sBuffer)-1, "%.2fms", fMsStart); - float fStartTextWidth = (float)((1+MICROPROFILE_TEXT_WIDTH) * nLenStart); - float fStartTextX = fXStart - fStartTextWidth - 2; - MicroProfileDrawBox(fStartTextX, nBaseY, fStartTextX + fStartTextWidth + 2, MICROPROFILE_TEXT_HEIGHT + 2 + nBaseY, 0x33000000, MicroProfileBoxTypeFlat); - MicroProfileDrawText(fStartTextX+1, nBaseY, UINT32_MAX, sBuffer, nLenStart); - uint32_t nLenEnd = snprintf(sBuffer, sizeof(sBuffer)-1, "%.2fms", fMsEnd3); - MicroProfileDrawBox(fXEnd+1, nBaseY, fXEnd+1+(1+MICROPROFILE_TEXT_WIDTH) * nLenEnd + 3, MICROPROFILE_TEXT_HEIGHT + 2 + nBaseY, 0x33000000, MicroProfileBoxTypeFlat); - MicroProfileDrawText(fXEnd+2, nBaseY+1, UINT32_MAX, sBuffer, nLenEnd); - - if(UI.nMouseRight) - { - MicroProfileZoomTo(UI.nRangeBegin, UI.nRangeEnd); - } - } - - if(UI.nRangeBeginGpu != UI.nRangeEndGpu) - { - float fMsStart = fToMsGpu * MicroProfileLogTickDifference(nBaseTicksGpu, UI.nRangeBeginGpu); - float fMsEnd4 = fToMsGpu * MicroProfileLogTickDifference(nBaseTicksGpu, UI.nRangeEndGpu); - float fXStart = fMsStart * fMsToScreen; - float fXEnd = fMsEnd4 * fMsToScreen; - MicroProfileDrawBox(fXStart, nBaseY, fXEnd, nHeight, MICROPROFILE_FRAME_COLOR_HIGHTLIGHT_GPU, MicroProfileBoxTypeFlat); - MicroProfileDrawLineVertical(fXStart, nBaseY, nHeight, MICROPROFILE_FRAME_COLOR_HIGHTLIGHT_GPU | 0x44000000); - MicroProfileDrawLineVertical(fXEnd, nBaseY, nHeight, MICROPROFILE_FRAME_COLOR_HIGHTLIGHT_GPU | 0x44000000); - - nBaseY += MICROPROFILE_TEXT_HEIGHT+1; - - fMsStart += fDetailedOffset; - fMsEnd4 += fDetailedOffset; - char sBuffer[32]; - uint32_t nLenStart = snprintf(sBuffer, sizeof(sBuffer)-1, "%.2fms", fMsStart); - float fStartTextWidth = (float)((1+MICROPROFILE_TEXT_WIDTH) * nLenStart); - float fStartTextX = fXStart - fStartTextWidth - 2; - MicroProfileDrawBox(fStartTextX, nBaseY, fStartTextX + fStartTextWidth + 2, MICROPROFILE_TEXT_HEIGHT + 2 + nBaseY, 0x33000000, MicroProfileBoxTypeFlat); - MicroProfileDrawText(fStartTextX+1, nBaseY, UINT32_MAX, sBuffer, nLenStart); - uint32_t nLenEnd = snprintf(sBuffer, sizeof(sBuffer)-1, "%.2fms", fMsEnd4); - MicroProfileDrawBox(fXEnd+1, nBaseY, fXEnd+1+(1+MICROPROFILE_TEXT_WIDTH) * nLenEnd + 3, MICROPROFILE_TEXT_HEIGHT + 2 + nBaseY, 0x33000000, MicroProfileBoxTypeFlat); - MicroProfileDrawText(fXEnd+2, nBaseY+1, UINT32_MAX, sBuffer, nLenEnd); - } - } -} - - -inline void MicroProfileDrawDetailedFrameHistory(uint32_t nWidth, uint32_t nHeight, uint32_t nBaseY, uint32_t nSelectedFrame) -{ - MicroProfile& S = *MicroProfileGet(); - - const uint32_t nBarHeight = MICROPROFILE_FRAME_HISTORY_HEIGHT; - float fBaseX = (float)nWidth; - float fDx = fBaseX / MICROPROFILE_NUM_FRAMES; - - uint32_t nLastIndex = (S.nFrameCurrent+1) % MICROPROFILE_MAX_FRAME_HISTORY; - MicroProfileDrawBox(0, nBaseY, nWidth, nBaseY+MICROPROFILE_FRAME_HISTORY_HEIGHT, 0xff000000 | g_nMicroProfileBackColors[0], MicroProfileBoxTypeFlat); - float fToMs = MicroProfileTickToMsMultiplier(MicroProfileTicksPerSecondCpu()) * S.fRcpReferenceTime; - float fToMsGpu = MicroProfileTickToMsMultiplier(MicroProfileTicksPerSecondGpu()) * S.fRcpReferenceTime; - - - MicroProfileFrameState* pFrameCurrent = &S.Frames[S.nFrameCurrent]; - uint64_t nFrameStartCpu = pFrameCurrent->nFrameStartCpu; - int64_t nDetailedOffsetTicksCpu = MicroProfileMsToTick(UI.fDetailedOffset, MicroProfileTicksPerSecondCpu()); - int64_t nCpuStart = nDetailedOffsetTicksCpu + nFrameStartCpu; - int64_t nCpuEnd = nCpuStart + MicroProfileMsToTick(UI.fDetailedRange, MicroProfileTicksPerSecondCpu());; - - - float fSelectionStart = (float)nWidth; - float fSelectionEnd = 0.f; - for(uint32_t i = 0; i < MICROPROFILE_NUM_FRAMES; ++i) - { - uint32_t nIndex = (S.nFrameCurrent + MICROPROFILE_MAX_FRAME_HISTORY - i) % MICROPROFILE_MAX_FRAME_HISTORY; - MicroProfileFrameState* pCurrent = &S.Frames[nIndex]; - MicroProfileFrameState* pNext = &S.Frames[nLastIndex]; - - int64_t nTicks = pNext->nFrameStartCpu - pCurrent->nFrameStartCpu; - int64_t nTicksGpu = pNext->nFrameStartGpu - pCurrent->nFrameStartGpu; - float fScale = fToMs * nTicks; - float fScaleGpu = fToMsGpu * nTicksGpu; - fScale = fScale > 1.f ? 0.f : 1.f - fScale; - fScaleGpu = fScaleGpu > 1.f ? 0.f : 1.f - fScaleGpu; - float fXEnd = fBaseX; - float fXStart = fBaseX - fDx; - fBaseX = fXStart; - uint32_t nColor = MICROPROFILE_FRAME_HISTORY_COLOR_CPU; - if(nIndex == nSelectedFrame) - nColor = UINT32_MAX; - MicroProfileDrawBox(fXStart, nBaseY + fScale * nBarHeight, fXEnd, nBaseY+MICROPROFILE_FRAME_HISTORY_HEIGHT, nColor, MicroProfileBoxTypeBar); - if(pNext->nFrameStartCpu > nCpuStart) - { - fSelectionStart = fXStart; - } - if(pCurrent->nFrameStartCpu < nCpuEnd && fSelectionEnd == 0.f) - { - fSelectionEnd = fXEnd; - } - nLastIndex = nIndex; - } - MicroProfileDrawBox(fSelectionStart, nBaseY, fSelectionEnd, nBaseY+MICROPROFILE_FRAME_HISTORY_HEIGHT, MICROPROFILE_FRAME_HISTORY_COLOR_HIGHTLIGHT, MicroProfileBoxTypeFlat); -} -inline void MicroProfileDrawDetailedView(uint32_t nWidth, uint32_t nHeight) -{ - MicroProfile& S = *MicroProfileGet(); - - MICROPROFILE_SCOPE(g_MicroProfileDetailed); - uint32_t nBaseY = MICROPROFILE_TEXT_HEIGHT + 1; - - int nSelectedFrame = -1; - if(UI.nMouseY > nBaseY && UI.nMouseY <= nBaseY + MICROPROFILE_FRAME_HISTORY_HEIGHT && UI.nActiveMenu == UINT32_MAX) - { - - nSelectedFrame = ((MICROPROFILE_NUM_FRAMES) * (UI.nWidth-UI.nMouseX) / UI.nWidth); - nSelectedFrame = (S.nFrameCurrent + MICROPROFILE_MAX_FRAME_HISTORY - nSelectedFrame) % MICROPROFILE_MAX_FRAME_HISTORY; - UI.nHoverFrame = nSelectedFrame; - if(UI.nMouseRight) - { - int64_t nRangeBegin = S.Frames[nSelectedFrame].nFrameStartCpu; - int64_t nRangeEnd = S.Frames[(nSelectedFrame+1)%MICROPROFILE_MAX_FRAME_HISTORY].nFrameStartCpu; - MicroProfileZoomTo(nRangeBegin, nRangeEnd); - } - if(UI.nMouseDownLeft) - { - uint64_t nFrac = (1024 * (MICROPROFILE_NUM_FRAMES) * (UI.nMouseX) / UI.nWidth) % 1024; - int64_t nRangeBegin = S.Frames[nSelectedFrame].nFrameStartCpu; - int64_t nRangeEnd = S.Frames[(nSelectedFrame+1)%MICROPROFILE_MAX_FRAME_HISTORY].nFrameStartCpu; - MicroProfileCenter(nRangeBegin + (nRangeEnd-nRangeBegin) * nFrac / 1024); - } - } - else - { - UI.nHoverFrame = -1; - } - - MicroProfileDrawDetailedBars(nWidth, nHeight, nBaseY + MICROPROFILE_FRAME_HISTORY_HEIGHT, nSelectedFrame); - MicroProfileDrawDetailedFrameHistory(nWidth, nHeight, nBaseY, nSelectedFrame); -} - -inline void MicroProfileDrawTextRight(uint32_t nX, uint32_t nY, uint32_t nColor, const char* pStr, uint32_t nStrLen) -{ - MicroProfileDrawText(nX - nStrLen * (MICROPROFILE_TEXT_WIDTH+1), nY, nColor, pStr, nStrLen); -} -inline void MicroProfileDrawHeader(int32_t nX, uint32_t nWidth, const char* pName) -{ - if(pName) - { - MicroProfileDrawBox(nX-8, MICROPROFILE_TEXT_HEIGHT + 2, nX + nWidth+5, MICROPROFILE_TEXT_HEIGHT + 2 + (MICROPROFILE_TEXT_HEIGHT+1), 0xff000000|g_nMicroProfileBackColors[1]); - MicroProfileDrawText(nX, MICROPROFILE_TEXT_HEIGHT + 2, UINT32_MAX, pName, (uint32_t)strlen(pName)); - } -} - - -typedef void (*MicroProfileLoopGroupCallback)(uint32_t nTimer, uint32_t nIdx, uint64_t nGroupMask, uint32_t nX, uint32_t nY, void* pData); - -inline void MicroProfileLoopActiveGroupsDraw(int32_t nX, int32_t nY, const char* pName, MicroProfileLoopGroupCallback CB, void* pData) -{ - MicroProfile& S = *MicroProfileGet(); - nY += MICROPROFILE_TEXT_HEIGHT + 2; - uint64_t nGroup = S.nAllGroupsWanted ? S.nGroupMask : S.nActiveGroupWanted; - uint32_t nCount = 0; - for(uint32_t j = 0; j < MICROPROFILE_MAX_GROUPS; ++j) - { - uint64_t nMask = 1ULL << j; - if(nMask & nGroup) - { - nY += MICROPROFILE_TEXT_HEIGHT + 1; - for(uint32_t i = 0; i < S.nTotalTimers;++i) - { - uint64_t nTokenMask = MicroProfileGetGroupMask(S.TimerInfo[i].nToken); - if(nTokenMask & nMask) - { - if(nY >= 0) - CB(i, nCount, nMask, nX, nY, pData); - - nCount += 2; - nY += MICROPROFILE_TEXT_HEIGHT + 1; - - if(nY > (int)UI.nHeight) - return; - } - } - - } - } -} - - -inline void MicroProfileCalcTimers(float* pTimers, float* pAverage, float* pMax, float* pCallAverage, float* pExclusive, float* pAverageExclusive, float* pMaxExclusive, uint64_t nGroup, uint32_t nSize) -{ - MicroProfile& S = *MicroProfileGet(); - - uint32_t nCount = 0; - uint64_t nMask = 1; - - for(uint32_t j = 0; j < MICROPROFILE_MAX_GROUPS; ++j) - { - if(nMask & nGroup) - { - const float fToMs = MicroProfileTickToMsMultiplier(S.GroupInfo[j].Type == MicroProfileTokenTypeGpu ? MicroProfileTicksPerSecondGpu() : MicroProfileTicksPerSecondCpu()); - for(uint32_t i = 0; i < S.nTotalTimers;++i) - { - uint64_t nTokenMask = MicroProfileGetGroupMask(S.TimerInfo[i].nToken); - if(nTokenMask & nMask) - { - { - uint32_t nTimer = i; - uint32_t nIdx = nCount; - uint32_t nAggregateFrames = S.nAggregateFrames ? S.nAggregateFrames : 1; - uint32_t nAggregateCount = S.Aggregate[nTimer].nCount ? S.Aggregate[nTimer].nCount : 1; - float fToPrc = S.fRcpReferenceTime; - float fMs = fToMs * (S.Frame[nTimer].nTicks); - float fPrc = MicroProfileMin(fMs * fToPrc, 1.f); - float fAverageMs = fToMs * (S.Aggregate[nTimer].nTicks / nAggregateFrames); - float fAveragePrc = MicroProfileMin(fAverageMs * fToPrc, 1.f); - float fMaxMs = fToMs * (S.AggregateMax[nTimer]); - float fMaxPrc = MicroProfileMin(fMaxMs * fToPrc, 1.f); - float fCallAverageMs = fToMs * (S.Aggregate[nTimer].nTicks / nAggregateCount); - float fCallAveragePrc = MicroProfileMin(fCallAverageMs * fToPrc, 1.f); - float fMsExclusive = fToMs * (S.FrameExclusive[nTimer]); - float fPrcExclusive = MicroProfileMin(fMsExclusive * fToPrc, 1.f); - float fAverageMsExclusive = fToMs * (S.AggregateExclusive[nTimer] / nAggregateFrames); - float fAveragePrcExclusive = MicroProfileMin(fAverageMsExclusive * fToPrc, 1.f); - float fMaxMsExclusive = fToMs * (S.AggregateMaxExclusive[nTimer]); - float fMaxPrcExclusive = MicroProfileMin(fMaxMsExclusive * fToPrc, 1.f); - pTimers[nIdx] = fMs; - pTimers[nIdx+1] = fPrc; - pAverage[nIdx] = fAverageMs; - pAverage[nIdx+1] = fAveragePrc; - pMax[nIdx] = fMaxMs; - pMax[nIdx+1] = fMaxPrc; - pCallAverage[nIdx] = fCallAverageMs; - pCallAverage[nIdx+1] = fCallAveragePrc; - pExclusive[nIdx] = fMsExclusive; - pExclusive[nIdx+1] = fPrcExclusive; - pAverageExclusive[nIdx] = fAverageMsExclusive; - pAverageExclusive[nIdx+1] = fAveragePrcExclusive; - pMaxExclusive[nIdx] = fMaxMsExclusive; - pMaxExclusive[nIdx+1] = fMaxPrcExclusive; - } - nCount += 2; - } - } - } - nMask <<= 1; - } -} - -#define SBUF_MAX 32 - -inline void MicroProfileDrawBarArrayCallback(uint32_t nTimer, uint32_t nIdx, uint64_t nGroupMask, uint32_t nX, uint32_t nY, void* pExtra) -{ - const uint32_t nHeight = MICROPROFILE_TEXT_HEIGHT; - const uint32_t nTextWidth = 6 * (1+MICROPROFILE_TEXT_WIDTH); - const float fWidth = (float)MICROPROFILE_BAR_WIDTH; - - float* pTimers = ((float**)pExtra)[0]; - float* pTimers2 = ((float**)pExtra)[1]; - MicroProfile& S = *MicroProfileGet(); - char sBuffer[SBUF_MAX]; - if (pTimers2 && pTimers2[nIdx] > 0.1f) - snprintf(sBuffer, SBUF_MAX-1, "%5.2f %3.1fx", pTimers[nIdx], pTimers[nIdx] / pTimers2[nIdx]); - else - snprintf(sBuffer, SBUF_MAX-1, "%5.2f", pTimers[nIdx]); - if (!pTimers2) - MicroProfileDrawBox(nX + nTextWidth, nY, nX + nTextWidth + fWidth * pTimers[nIdx+1], nY + nHeight, UI.nOpacityForeground|S.TimerInfo[nTimer].nColor, MicroProfileBoxTypeBar); - MicroProfileDrawText(nX, nY, UINT32_MAX, sBuffer, (uint32_t)strlen(sBuffer)); -} - - -inline uint32_t MicroProfileDrawBarArray(int32_t nX, int32_t nY, float* pTimers, const char* pName, uint32_t nTotalHeight, float* pTimers2 = NULL) -{ - const uint32_t nTextWidth = 6 * (1+MICROPROFILE_TEXT_WIDTH); - const uint32_t nWidth = MICROPROFILE_BAR_WIDTH; - - MicroProfileDrawLineVertical(nX-5, 0, nTotalHeight+nY, UI.nOpacityBackground|g_nMicroProfileBackColors[0]|g_nMicroProfileBackColors[1]); - float* pTimersArray[2] = {pTimers, pTimers2}; - MicroProfileLoopActiveGroupsDraw(nX, nY, pName, MicroProfileDrawBarArrayCallback, pTimersArray); - MicroProfileDrawHeader(nX, nTextWidth + nWidth, pName); - return nWidth + 5 + nTextWidth; - -} -inline void MicroProfileDrawBarCallCountCallback(uint32_t nTimer, uint32_t nIdx, uint64_t nGroupMask, uint32_t nX, uint32_t nY, void* pExtra) -{ - MicroProfile& S = *MicroProfileGet(); - char sBuffer[SBUF_MAX]; - int nLen = snprintf(sBuffer, SBUF_MAX-1, "%5d", S.Frame[nTimer].nCount);//fix - MicroProfileDrawText(nX, nY, UINT32_MAX, sBuffer, nLen); -} - -inline uint32_t MicroProfileDrawBarCallCount(int32_t nX, int32_t nY, const char* pName) -{ - MicroProfileLoopActiveGroupsDraw(nX, nY, pName, MicroProfileDrawBarCallCountCallback, 0); - const uint32_t nTextWidth = 6 * MICROPROFILE_TEXT_WIDTH; - MicroProfileDrawHeader(nX, 5 + nTextWidth, pName); - return 5 + nTextWidth; -} - -struct MicroProfileMetaAverageArgs -{ - uint64_t* pCounters; - float fRcpFrames; -}; - -inline void MicroProfileDrawBarMetaAverageCallback(uint32_t nTimer, uint32_t nIdx, uint64_t nGroupMask, uint32_t nX, uint32_t nY, void* pExtra) -{ - MicroProfileMetaAverageArgs* pArgs = (MicroProfileMetaAverageArgs*)pExtra; - uint64_t* pCounters = pArgs->pCounters; - float fRcpFrames = pArgs->fRcpFrames; - char sBuffer[SBUF_MAX]; - int nLen = snprintf(sBuffer, SBUF_MAX-1, "%5.2f", pCounters[nTimer] * fRcpFrames); - MicroProfileDrawText(nX - nLen * (MICROPROFILE_TEXT_WIDTH+1), nY, UINT32_MAX, sBuffer, nLen); -} - -inline uint32_t MicroProfileDrawBarMetaAverage(int32_t nX, int32_t nY, uint64_t* pCounters, const char* pName, uint32_t nTotalHeight) -{ - if(!pName) - return 0; - MicroProfileDrawLineVertical(nX-5, 0, nTotalHeight+nY, UI.nOpacityBackground|g_nMicroProfileBackColors[0]|g_nMicroProfileBackColors[1]); - uint32_t nTextWidth = (1+MICROPROFILE_TEXT_WIDTH) * MicroProfileMax(6, (uint32_t)strlen(pName)); - float fRcpFrames = 1.f / (MicroProfileGet()->nAggregateFrames ? MicroProfileGet()->nAggregateFrames : 1); - MicroProfileMetaAverageArgs Args = {pCounters, fRcpFrames}; - MicroProfileLoopActiveGroupsDraw(nX + nTextWidth, nY, pName, MicroProfileDrawBarMetaAverageCallback, &Args); - MicroProfileDrawHeader(nX, 5 + nTextWidth, pName); - return 5 + nTextWidth; -} - - -inline void MicroProfileDrawBarMetaCountCallback(uint32_t nTimer, uint32_t nIdx, uint64_t nGroupMask, uint32_t nX, uint32_t nY, void* pExtra) -{ - uint64_t* pCounters = (uint64_t*)pExtra; - char sBuffer[SBUF_MAX]; - int nLen = snprintf(sBuffer, SBUF_MAX-1, "%5" PRIu64, pCounters[nTimer]); - MicroProfileDrawText(nX - nLen * (MICROPROFILE_TEXT_WIDTH+1), nY, UINT32_MAX, sBuffer, nLen); -} - -inline uint32_t MicroProfileDrawBarMetaCount(int32_t nX, int32_t nY, uint64_t* pCounters, const char* pName, uint32_t nTotalHeight) -{ - if(!pName) - return 0; - - MicroProfileDrawLineVertical(nX-5, 0, nTotalHeight+nY, UI.nOpacityBackground|g_nMicroProfileBackColors[0]|g_nMicroProfileBackColors[1]); - uint32_t nTextWidth = (1+MICROPROFILE_TEXT_WIDTH) * MicroProfileMax(6, (uint32_t)strlen(pName)); - MicroProfileLoopActiveGroupsDraw(nX + nTextWidth, nY, pName, MicroProfileDrawBarMetaCountCallback, pCounters); - MicroProfileDrawHeader(nX, 5 + nTextWidth, pName); - return 5 + nTextWidth; -} - -inline void MicroProfileDrawBarLegendCallback(uint32_t nTimer, uint32_t nIdx, uint64_t nGroupMask, uint32_t nX, uint32_t nY, void* pExtra) -{ - MicroProfile& S = *MicroProfileGet(); - if (S.TimerInfo[nTimer].bGraph) - { - MicroProfileDrawText(nX, nY, S.TimerInfo[nTimer].nColor, ">", 1); - } - MicroProfileDrawTextRight(nX, nY, S.TimerInfo[nTimer].nColor, S.TimerInfo[nTimer].pName, (uint32_t)strlen(S.TimerInfo[nTimer].pName)); - if(UI.nMouseY >= nY && UI.nMouseY < nY + MICROPROFILE_TEXT_HEIGHT+1) - { - UI.nHoverToken = nTimer; - UI.nHoverTime = 0; - } -} - -inline uint32_t MicroProfileDrawBarLegend(int32_t nX, int32_t nY, uint32_t nTotalHeight, uint32_t nMaxWidth) -{ - MicroProfileDrawLineVertical(nX-5, nY, nTotalHeight, UI.nOpacityBackground | g_nMicroProfileBackColors[0]|g_nMicroProfileBackColors[1]); - MicroProfileLoopActiveGroupsDraw(nMaxWidth, nY, 0, MicroProfileDrawBarLegendCallback, 0); - return nX; -} - -bool MicroProfileDrawGraph(uint32_t nScreenWidth, uint32_t nScreenHeight) -{ - MicroProfile& S = *MicroProfileGet(); - - MICROPROFILE_SCOPE(g_MicroProfileDrawGraph); - bool bEnabled = false; - for(uint32_t i = 0; i < MICROPROFILE_MAX_GRAPHS; ++i) - if(S.Graph[i].nToken != MICROPROFILE_INVALID_TOKEN) - bEnabled = true; - if(!bEnabled) - return false; - - uint32_t nX = nScreenWidth - MICROPROFILE_GRAPH_WIDTH; - uint32_t nY = nScreenHeight - MICROPROFILE_GRAPH_HEIGHT; - MicroProfileDrawBox(nX, nY, nX + MICROPROFILE_GRAPH_WIDTH, nY + MICROPROFILE_GRAPH_HEIGHT, 0x88000000 | g_nMicroProfileBackColors[0]); - bool bMouseOver = UI.nMouseX >= nX && UI.nMouseY >= nY; - float fMouseXPrc =(float(UI.nMouseX - nX)) / MICROPROFILE_GRAPH_WIDTH; - if(bMouseOver) - { - float fXAvg = fMouseXPrc * MICROPROFILE_GRAPH_WIDTH + nX; - MicroProfileDrawLineVertical(fXAvg, nY, nY + MICROPROFILE_GRAPH_HEIGHT, UINT32_MAX); - } - - - float fY = (float)nScreenHeight; - float fDX = MICROPROFILE_GRAPH_WIDTH * 1.f / MICROPROFILE_GRAPH_HISTORY; - float fDY = MICROPROFILE_GRAPH_HEIGHT; - uint32_t nPut = S.nGraphPut; - float* pGraphData = (float*)alloca(sizeof(float)* MICROPROFILE_GRAPH_HISTORY*2); - for(uint32_t i = 0; i < MICROPROFILE_MAX_GRAPHS; ++i) - { - if(S.Graph[i].nToken != MICROPROFILE_INVALID_TOKEN) - { - uint32_t nGroupId = MicroProfileGetGroupIndex(S.Graph[i].nToken); - bool bGpu = S.GroupInfo[nGroupId].Type == MicroProfileTokenTypeGpu; - float fToMs = MicroProfileTickToMsMultiplier(bGpu ? MicroProfileTicksPerSecondGpu() : MicroProfileTicksPerSecondCpu()); - float fToPrc = fToMs * S.fRcpReferenceTime * 3 / 4; - - float fX = (float)nX; - for(uint32_t j = 0; j < MICROPROFILE_GRAPH_HISTORY; ++j) - { - float fWeigth = MicroProfileMin(fToPrc * (S.Graph[i].nHistory[(j+nPut)%MICROPROFILE_GRAPH_HISTORY]), 1.f); - pGraphData[(j*2)] = fX; - pGraphData[(j*2)+1] = fY - fDY * fWeigth; - fX += fDX; - } - MicroProfileDrawLine2D(MICROPROFILE_GRAPH_HISTORY, pGraphData, S.TimerInfo[MicroProfileGetTimerIndex(S.Graph[i].nToken)].nColor); - } - } - { - float fY1 = 0.25f * MICROPROFILE_GRAPH_HEIGHT + nY; - float fY2 = 0.50f * MICROPROFILE_GRAPH_HEIGHT + nY; - float fY3 = 0.75f * MICROPROFILE_GRAPH_HEIGHT + nY; - MicroProfileDrawLineHorizontal(nX, nX + MICROPROFILE_GRAPH_WIDTH, fY1, 0xffdd4444); - MicroProfileDrawLineHorizontal(nX, nX + MICROPROFILE_GRAPH_WIDTH, fY2, 0xff000000| g_nMicroProfileBackColors[0]); - MicroProfileDrawLineHorizontal(nX, nX + MICROPROFILE_GRAPH_WIDTH, fY3, 0xff000000|g_nMicroProfileBackColors[0]); - - char buf[32]; - int nLen = snprintf(buf, sizeof(buf)-1, "%5.2fms", S.fReferenceTime); - MicroProfileDrawText(nX+1, fY1 - (2+MICROPROFILE_TEXT_HEIGHT), UINT32_MAX, buf, nLen); - } - - - - if(bMouseOver) - { - uint32_t pColors[MICROPROFILE_MAX_GRAPHS]; - MicroProfileStringArray Strings; - MicroProfileStringArrayClear(&Strings); - uint32_t nTextCount = 0; - uint32_t nGraphIndex = (S.nGraphPut + MICROPROFILE_GRAPH_HISTORY - int(MICROPROFILE_GRAPH_HISTORY*(1.f - fMouseXPrc))) % MICROPROFILE_GRAPH_HISTORY; - - uint32_t nMouseX = UI.nMouseX; - uint32_t nMouseY = UI.nMouseY + 20; - - for(uint32_t i = 0; i < MICROPROFILE_MAX_GRAPHS; ++i) - { - if(S.Graph[i].nToken != MICROPROFILE_INVALID_TOKEN) - { - uint32_t nGroupId = MicroProfileGetGroupIndex(S.Graph[i].nToken); - bool bGpu = S.GroupInfo[nGroupId].Type == MicroProfileTokenTypeGpu; - float fToMs = MicroProfileTickToMsMultiplier(bGpu ? MicroProfileTicksPerSecondGpu() : MicroProfileTicksPerSecondCpu()); - uint32_t nIndex = MicroProfileGetTimerIndex(S.Graph[i].nToken); - uint32_t nColor = S.TimerInfo[nIndex].nColor; - const char* pName = S.TimerInfo[nIndex].pName; - pColors[nTextCount++] = nColor; - MicroProfileStringArrayAddLiteral(&Strings, pName); - MicroProfileStringArrayFormat(&Strings, "%5.2fms", fToMs * (S.Graph[i].nHistory[nGraphIndex])); - } - } - if(nTextCount) - { - MicroProfileDrawFloatWindow(nMouseX, nMouseY, Strings.ppStrings, Strings.nNumStrings, 0, pColors); - } - - if(UI.nMouseRight) - { - for(uint32_t i = 0; i < MICROPROFILE_MAX_GRAPHS; ++i) - { - S.Graph[i].nToken = MICROPROFILE_INVALID_TOKEN; - } - } - } - - return bMouseOver; -} - -void MicroProfileDumpTimers() -{ - MicroProfile& S = *MicroProfileGet(); - - uint64_t nActiveGroup = S.nGroupMask; - - uint32_t nNumTimers = S.nTotalTimers; - uint32_t nBlockSize = 2 * nNumTimers; - float* pTimers = (float*)alloca(nBlockSize * 7 * sizeof(float)); - float* pAverage = pTimers + nBlockSize; - float* pMax = pTimers + 2 * nBlockSize; - float* pCallAverage = pTimers + 3 * nBlockSize; - float* pTimersExclusive = pTimers + 4 * nBlockSize; - float* pAverageExclusive = pTimers + 5 * nBlockSize; - float* pMaxExclusive = pTimers + 6 * nBlockSize; - MicroProfileCalcTimers(pTimers, pAverage, pMax, pCallAverage, pTimersExclusive, pAverageExclusive, pMaxExclusive, nActiveGroup, nNumTimers); - - MICROPROFILE_PRINTF("%11s, ", "Time"); - MICROPROFILE_PRINTF("%11s, ", "Average"); - MICROPROFILE_PRINTF("%11s, ", "Max"); - MICROPROFILE_PRINTF("%11s, ", "Call Avg"); - MICROPROFILE_PRINTF("%9s, ", "Count"); - MICROPROFILE_PRINTF("%11s, ", "Excl"); - MICROPROFILE_PRINTF("%11s, ", "Avg Excl"); - MICROPROFILE_PRINTF("%11s, \n", "Max Excl"); - - for(uint32_t j = 0; j < MICROPROFILE_MAX_GROUPS; ++j) - { - uint64_t nMask = 1ULL << j; - if(nMask & nActiveGroup) - { - MICROPROFILE_PRINTF("%s\n", S.GroupInfo[j].pName); - for(uint32_t i = 0; i < S.nTotalTimers;++i) - { - uint64_t nTokenMask = MicroProfileGetGroupMask(S.TimerInfo[i].nToken); - if(nTokenMask & nMask) - { - uint32_t nIdx = i * 2; - MICROPROFILE_PRINTF("%9.2fms, ", pTimers[nIdx]); - MICROPROFILE_PRINTF("%9.2fms, ", pAverage[nIdx]); - MICROPROFILE_PRINTF("%9.2fms, ", pMax[nIdx]); - MICROPROFILE_PRINTF("%9.2fms, ", pCallAverage[nIdx]); - MICROPROFILE_PRINTF("%9d, ", S.Frame[i].nCount); - MICROPROFILE_PRINTF("%9.2fms, ", pTimersExclusive[nIdx]); - MICROPROFILE_PRINTF("%9.2fms, ", pAverageExclusive[nIdx]); - MICROPROFILE_PRINTF("%9.2fms, ", pMaxExclusive[nIdx]); - MICROPROFILE_PRINTF("%s\n", S.TimerInfo[i].pName); - } - } - } - } -} - -inline void MicroProfileDrawBarView(uint32_t nScreenWidth, uint32_t nScreenHeight) -{ - MicroProfile& S = *MicroProfileGet(); - - uint64_t nActiveGroup = S.nAllGroupsWanted ? S.nGroupMask : S.nActiveGroupWanted; - if(!nActiveGroup) - return; - MICROPROFILE_SCOPE(g_MicroProfileDrawBarView); - - const uint32_t nHeight = MICROPROFILE_TEXT_HEIGHT; - int nColorIndex = 0; - uint32_t nMaxTimerNameLen = 1; - uint32_t nNumTimers = 0; - uint32_t nNumGroups = 0; - for(uint32_t j = 0; j < MICROPROFILE_MAX_GROUPS; ++j) - { - if(nActiveGroup & (1ULL << j)) - { - nNumTimers += S.GroupInfo[j].nNumTimers; - nNumGroups += 1; - nMaxTimerNameLen = MicroProfileMax(nMaxTimerNameLen, S.GroupInfo[j].nMaxTimerNameLen); - } - } - uint32_t nTimerWidth = 2+(4+nMaxTimerNameLen) * (MICROPROFILE_TEXT_WIDTH+1); - uint32_t nX = nTimerWidth + UI.nOffsetX; - uint32_t nY = nHeight + 3 - UI.nOffsetY; - uint32_t nBlockSize = 2 * nNumTimers; - float* pTimers = (float*)alloca(nBlockSize * 7 * sizeof(float)); - float* pAverage = pTimers + nBlockSize; - float* pMax = pTimers + 2 * nBlockSize; - float* pCallAverage = pTimers + 3 * nBlockSize; - float* pTimersExclusive = pTimers + 4 * nBlockSize; - float* pAverageExclusive = pTimers + 5 * nBlockSize; - float* pMaxExclusive = pTimers + 6 * nBlockSize; - MicroProfileCalcTimers(pTimers, pAverage, pMax, pCallAverage, pTimersExclusive, pAverageExclusive, pMaxExclusive, nActiveGroup, nNumTimers); - uint32_t nWidth = 0; - { - uint32_t nMetaIndex = 0; - for(uint32_t i = 1; i ; i <<= 1) - { - if(S.nBars & i) - { - if(i >= MP_DRAW_META_FIRST) - { - if(nMetaIndex < MICROPROFILE_META_MAX && S.MetaCounters[nMetaIndex].pName) - { - uint32_t nStrWidth = static_cast(strlen(S.MetaCounters[nMetaIndex].pName)); - if(S.nBars & MP_DRAW_TIMERS) - nWidth += 6 + (1+MICROPROFILE_TEXT_WIDTH) * (nStrWidth); - if(S.nBars & MP_DRAW_AVERAGE) - nWidth += 6 + (1+MICROPROFILE_TEXT_WIDTH) * (nStrWidth + 4); - if(S.nBars & MP_DRAW_MAX) - nWidth += 6 + (1+MICROPROFILE_TEXT_WIDTH) * (nStrWidth + 4); - } - } - else - { - nWidth += MICROPROFILE_BAR_WIDTH + 6 + 6 * (1+MICROPROFILE_TEXT_WIDTH); - if(i & MP_DRAW_CALL_COUNT) - nWidth += 6 + 6 * MICROPROFILE_TEXT_WIDTH; - } - } - if(i >= MP_DRAW_META_FIRST) - { - ++nMetaIndex; - } - } - nWidth += (1+nMaxTimerNameLen) * (MICROPROFILE_TEXT_WIDTH+1); - for(uint32_t i = 0; i < nNumTimers+nNumGroups+1; ++i) - { - uint32_t nY0 = nY + i * (nHeight + 1); - bool bInside = (UI.nActiveMenu == UINT32_MAX) && ((UI.nMouseY >= nY0) && (UI.nMouseY < (nY0 + nHeight + 1))); - MicroProfileDrawBox(nX, nY0, nWidth+nX, nY0 + (nHeight+1)+1, UI.nOpacityBackground | (g_nMicroProfileBackColors[nColorIndex++ & 1] + ((bInside) ? 0x002c2c2c : 0))); - } - nX += 10; - } - int nTotalHeight = (nNumTimers+nNumGroups+1) * (nHeight+1); - uint32_t nLegendOffset = 1; - if(S.nBars & MP_DRAW_TIMERS) - nX += MicroProfileDrawBarArray(nX, nY, pTimers, "Time", nTotalHeight) + 1; - if(S.nBars & MP_DRAW_AVERAGE) - nX += MicroProfileDrawBarArray(nX, nY, pAverage, "Average", nTotalHeight) + 1; - if(S.nBars & MP_DRAW_MAX) - nX += MicroProfileDrawBarArray(nX, nY, pMax, (!UI.bShowSpikes) ? "Max Time" : "Max Time, Spike", nTotalHeight, UI.bShowSpikes ? pAverage : NULL) + 1; - if(S.nBars & MP_DRAW_CALL_COUNT) - { - nX += MicroProfileDrawBarArray(nX, nY, pCallAverage, "Call Average", nTotalHeight) + 1; - nX += MicroProfileDrawBarCallCount(nX, nY, "Count") + 1; - } - if(S.nBars & MP_DRAW_TIMERS_EXCLUSIVE) - nX += MicroProfileDrawBarArray(nX, nY, pTimersExclusive, "Exclusive Time", nTotalHeight) + 1; - if(S.nBars & MP_DRAW_AVERAGE_EXCLUSIVE) - nX += MicroProfileDrawBarArray(nX, nY, pAverageExclusive, "Exclusive Average", nTotalHeight) + 1; - if(S.nBars & MP_DRAW_MAX_EXCLUSIVE) - nX += MicroProfileDrawBarArray(nX, nY, pMaxExclusive, (!UI.bShowSpikes) ? "Exclusive Max Time" :"Excl Max Time, Spike", nTotalHeight, UI.bShowSpikes ? pAverageExclusive : NULL) + 1; - - for(int i = 0; i < MICROPROFILE_META_MAX; ++i) - { - if(0 != (S.nBars & (MP_DRAW_META_FIRST<(strlen(S.MetaCounters[i].pName) + 32); - char* buffer = (char*)alloca(nBufferSize); - if(S.nBars & MP_DRAW_TIMERS) - nX += MicroProfileDrawBarMetaCount(nX, nY, &S.MetaCounters[i].nCounters[0], S.MetaCounters[i].pName, nTotalHeight) + 1; - if(S.nBars & MP_DRAW_AVERAGE) - { - snprintf(buffer, nBufferSize-1, "%s Avg", S.MetaCounters[i].pName); - nX += MicroProfileDrawBarMetaAverage(nX, nY, &S.MetaCounters[i].nAggregate[0], buffer, nTotalHeight) + 1; - } - if(S.nBars & MP_DRAW_MAX) - { - snprintf(buffer, nBufferSize-1, "%s Max", S.MetaCounters[i].pName); - nX += MicroProfileDrawBarMetaCount(nX, nY, &S.MetaCounters[i].nAggregateMax[0], buffer, nTotalHeight) + 1; - } - } - } - nX = 0; - nY = nHeight + 3 - UI.nOffsetY; - for(uint32_t i = 0; i < nNumTimers+nNumGroups+1; ++i) - { - const uint32_t nY0 = nY + i * (nHeight + 1); - const bool bInside = (UI.nActiveMenu == UINT32_MAX) && ((UI.nMouseY >= nY0) && (UI.nMouseY < (nY0 + nHeight + 1))); - MicroProfileDrawBox(nX, nY0, nTimerWidth, nY0 + (nHeight+1)+1, 0xff0000000 | (g_nMicroProfileBackColors[nColorIndex++ & 1] + ((bInside) ? 0x002c2c2c : 0))); - } - nX += MicroProfileDrawBarLegend(nX, nY, nTotalHeight, nTimerWidth-5) + 1; - - for(uint32_t j = 0; j < MICROPROFILE_MAX_GROUPS; ++j) - { - if(nActiveGroup & (1ULL << j)) - { - MicroProfileDrawText(nX, nY + (1+nHeight) * nLegendOffset, UINT32_MAX, S.GroupInfo[j].pName, S.GroupInfo[j].nNameLen); - nLegendOffset += S.GroupInfo[j].nNumTimers+1; - } - } - MicroProfileDrawHeader(nX, nTimerWidth-5, "Group"); - MicroProfileDrawTextRight(nTimerWidth-3, MICROPROFILE_TEXT_HEIGHT + 2, UINT32_MAX, "Timer", 5); - MicroProfileDrawLineVertical(nTimerWidth, 0, nTotalHeight+nY, UI.nOpacityBackground|g_nMicroProfileBackColors[0]|g_nMicroProfileBackColors[1]); - MicroProfileDrawLineHorizontal(0, nWidth, 2*MICROPROFILE_TEXT_HEIGHT + 3, UI.nOpacityBackground|g_nMicroProfileBackColors[0]|g_nMicroProfileBackColors[1]); -} - -typedef const char* (*MicroProfileSubmenuCallback)(int, bool* bSelected); -typedef void (*MicroProfileClickCallback)(int); - - -inline const char* MicroProfileUIMenuMode(int nIndex, bool* bSelected) -{ - MicroProfile& S = *MicroProfileGet(); - switch(nIndex) - { - case 0: - *bSelected = S.nDisplay == MP_DRAW_DETAILED; - return "Detailed"; - case 1: - *bSelected = S.nDisplay == MP_DRAW_BARS; - return "Timers"; - case 2: - *bSelected = S.nDisplay == MP_DRAW_HIDDEN; - return "Hidden"; - case 3: - *bSelected = true; - return "Off"; - case 4: - *bSelected = true; - return "------"; - case 5: - *bSelected = S.nForceEnable != 0; - return "Force Enable"; - - default: return 0; - } -} - -inline const char* MicroProfileUIMenuGroups(int nIndex, bool* bSelected) -{ - MicroProfile& S = *MicroProfileGet(); - *bSelected = false; - if(nIndex == 0) - { - *bSelected = S.nAllGroupsWanted != 0; - return "[ALL]"; - } - else - { - nIndex = nIndex-1; - if(static_cast(nIndex) < UI.GroupMenuCount) - { - MicroProfileGroupMenuItem& Item = UI.GroupMenu[nIndex]; - static char buffer[MICROPROFILE_NAME_MAX_LEN+32]; - if(Item.nIsCategory) - { - uint64_t nGroupMask = S.CategoryInfo[Item.nIndex].nGroupMask; - *bSelected = nGroupMask == (nGroupMask & S.nActiveGroupWanted); - snprintf(buffer, sizeof(buffer)-1, "[%s]", Item.pName); - } - else - { - *bSelected = 0 != (S.nActiveGroupWanted & (1ULL << Item.nIndex)); - snprintf(buffer, sizeof(buffer)-1, " %s", Item.pName); - } - return buffer; - } - return 0; - } -} - -inline const char* MicroProfileUIMenuAggregate(int nIndex, bool* bSelected) -{ - MicroProfile& S = *MicroProfileGet(); - if(static_cast(nIndex) < g_MicroProfileAggregatePresets.size()) - { - uint32_t val = g_MicroProfileAggregatePresets[nIndex]; - *bSelected = S.nAggregateFlip == val; - if (0 == val) - { - return "Infinite"; - } - else - { - static char buf[128]; - snprintf(buf, sizeof(buf)-1, "%7u", val); - return buf; - } - } - return 0; - -} - -inline const char* MicroProfileUIMenuTimers(int nIndex, bool* bSelected) -{ - MicroProfile& S = *MicroProfileGet(); - *bSelected = 0 != (S.nBars & (1 << nIndex)); - switch(nIndex) - { - case 0: return "Time"; - case 1: return "Average"; - case 2: return "Max"; - case 3: return "Call Count"; - case 4: return "Exclusive Timers"; - case 5: return "Exclusive Average"; - case 6: return "Exclusive Max"; - } - int nMetaIndex = nIndex - 7; - if(nMetaIndex < MICROPROFILE_META_MAX) - { - return S.MetaCounters[nMetaIndex].pName; - } - return 0; -} - -inline const char* MicroProfileUIMenuOptions(int nIndex, bool* bSelected) -{ - MicroProfile& S = *MicroProfileGet(); - if(nIndex >= MICROPROFILE_OPTION_SIZE) return 0; - switch(UI.Options[nIndex].nSubType) - { - case 0: - *bSelected = S.fReferenceTime == g_MicroProfileReferenceTimePresets[UI.Options[nIndex].nIndex]; - break; - case 1: - *bSelected = UI.nOpacityBackground>>24 == g_MicroProfileOpacityPresets[UI.Options[nIndex].nIndex]; - break; - case 2: - *bSelected = UI.nOpacityForeground>>24 == g_MicroProfileOpacityPresets[UI.Options[nIndex].nIndex]; - break; - case 3: - *bSelected = UI.bShowSpikes; - break; -#if MICROPROFILE_CONTEXT_SWITCH_TRACE - case 4: - { - switch(UI.Options[nIndex].nIndex) - { - case 0: - *bSelected = S.bContextSwitchRunning; - break; - case 1: - *bSelected = S.bContextSwitchAllThreads; - break; - case 2: - *bSelected = S.bContextSwitchNoBars; - break; - } - } - break; -#endif - } - return UI.Options[nIndex].Text; -} - -inline const char* MicroProfileUIMenuPreset(int nIndex, bool* bSelected) -{ - static char buf[128]; - *bSelected = false; - int nNumPresets = static_cast(g_MicroProfilePresetNames.size()); - int nIndexSave = nIndex - nNumPresets - 1; - if (nIndex == nNumPresets) - { - return "--"; - } - else if(nIndexSave >=0 && nIndexSave < nNumPresets) - { - snprintf(buf, sizeof(buf)-1, "Save '%s'", g_MicroProfilePresetNames[nIndexSave]); - return buf; - } - else if(nIndex < nNumPresets) - { - snprintf(buf, sizeof(buf)-1, "Load '%s'", g_MicroProfilePresetNames[nIndex]); - return buf; - } - else - { - return 0; - } -} - -inline const char* MicroProfileUIMenuCustom(int nIndex, bool* bSelected) -{ - if(UINT32_MAX == UI.nCustomActive) - { - *bSelected = nIndex == 0; - } - else - { - *bSelected = nIndex-2 == static_cast(UI.nCustomActive); - } - switch(nIndex) - { - case 0: return "Disable"; - case 1: return "--"; - default: - nIndex -= 2; - if(static_cast(nIndex) < UI.nCustomCount) - { - return UI.Custom[nIndex].pName; - } - else - { - return 0; - } - } -} - -inline const char* MicroProfileUIMenuEmpty(int nIndex, bool* bSelected) -{ - return 0; -} - - -inline void MicroProfileUIClickMode(int nIndex) -{ - MicroProfile& S = *MicroProfileGet(); - switch(nIndex) - { - case 0: - S.nDisplay = MP_DRAW_DETAILED; - break; - case 1: - S.nDisplay = MP_DRAW_BARS; - break; - case 2: - S.nDisplay = MP_DRAW_HIDDEN; - break; - case 3: - S.nDisplay = 0; - break; - case 4: - break; - case 5: - S.nForceEnable = !S.nForceEnable; - break; - } -} - -inline void MicroProfileUIClickGroups(int nIndex) -{ - MicroProfile& S = *MicroProfileGet(); - if(nIndex == 0) - S.nAllGroupsWanted = 1-S.nAllGroupsWanted; - else - { - nIndex -= 1; - if(static_cast(nIndex) < UI.GroupMenuCount) - { - MicroProfileGroupMenuItem& Item = UI.GroupMenu[nIndex]; - if(Item.nIsCategory) - { - uint64_t nGroupMask = S.CategoryInfo[Item.nIndex].nGroupMask; - if(nGroupMask != (nGroupMask & S.nActiveGroupWanted)) - { - S.nActiveGroupWanted |= nGroupMask; - } - else - { - S.nActiveGroupWanted &= ~nGroupMask; - } - } - else - { - MP_ASSERT(Item.nIndex < S.nGroupCount); - S.nActiveGroupWanted ^= (1ULL << Item.nIndex); - } - } - } -} - -inline void MicroProfileUIClickAggregate(int nIndex) -{ - MicroProfile& S = *MicroProfileGet(); - S.nAggregateFlip = g_MicroProfileAggregatePresets[nIndex]; - if(0 == S.nAggregateFlip) - { - S.nAggregateClear = 1; - } -} - -inline void MicroProfileUIClickTimers(int nIndex) -{ - MicroProfile& S = *MicroProfileGet(); - S.nBars ^= (1 << nIndex); -} - -inline void MicroProfileUIClickOptions(int nIndex) -{ - MicroProfile& S = *MicroProfileGet(); - switch(UI.Options[nIndex].nSubType) - { - case 0: - S.fReferenceTime = g_MicroProfileReferenceTimePresets[UI.Options[nIndex].nIndex]; - S.fRcpReferenceTime = 1.f / S.fReferenceTime; - break; - case 1: - UI.nOpacityBackground = g_MicroProfileOpacityPresets[UI.Options[nIndex].nIndex]<<24; - break; - case 2: - UI.nOpacityForeground = g_MicroProfileOpacityPresets[UI.Options[nIndex].nIndex]<<24; - break; - case 3: - UI.bShowSpikes = !UI.bShowSpikes; - break; -#if MICROPROFILE_CONTEXT_SWITCH_TRACE - case 4: - { - switch(UI.Options[nIndex].nIndex) - { - case 0: - if(S.bContextSwitchRunning) - { - MicroProfileStopContextSwitchTrace(); - } - else - { - MicroProfileStartContextSwitchTrace(); - } - break; - case 1: - S.bContextSwitchAllThreads = !S.bContextSwitchAllThreads; - break; - case 2: - S.bContextSwitchNoBars= !S.bContextSwitchNoBars; - break; - - } - } - break; -#endif - } -} - -inline void MicroProfileUIClickPreset(int nIndex) -{ - int nNumPresets = static_cast(g_MicroProfilePresetNames.size()); - int nIndexSave = nIndex - nNumPresets - 1; - if(nIndexSave >= 0 && nIndexSave < nNumPresets) - { - MicroProfileSavePreset(g_MicroProfilePresetNames[nIndexSave]); - } - else if(nIndex >= 0 && nIndex < nNumPresets) - { - MicroProfileLoadPreset(g_MicroProfilePresetNames[nIndex]); - } -} - -inline void MicroProfileUIClickCustom(int nIndex) -{ - if(nIndex == 0) - { - MicroProfileCustomGroupDisable(); - } - else - { - MicroProfileCustomGroupEnable(nIndex-2); - } - -} - -inline void MicroProfileUIClickEmpty(int nIndex) -{ - -} - - -inline void MicroProfileDrawMenu(uint32_t nWidth, uint32_t nHeight) -{ - MicroProfile& S = *MicroProfileGet(); - - uint32_t nX = 0; - uint32_t nY = 0; - -#define SBUF_SIZE 256 - char buffer[256]; - MicroProfileDrawBox(nX, nY, nX + nWidth, nY + (MICROPROFILE_TEXT_HEIGHT+1)+1, 0xff000000|g_nMicroProfileBackColors[1]); - -#define MICROPROFILE_MENU_MAX 16 - const char* pMenuText[MICROPROFILE_MENU_MAX] = {0}; - uint32_t nMenuX[MICROPROFILE_MENU_MAX] = {0}; - uint32_t nNumMenuItems = 0; - - int nMPTextLen = snprintf(buffer, 127, "MicroProfile"); - MicroProfileDrawText(nX, nY, UINT32_MAX, buffer, nMPTextLen); - nX += (sizeof("MicroProfile")+2) * (MICROPROFILE_TEXT_WIDTH+1); - pMenuText[nNumMenuItems++] = "Mode"; - pMenuText[nNumMenuItems++] = "Groups"; - char AggregateText[64]; - snprintf(AggregateText, sizeof(AggregateText)-1, "Aggregate[%d]", S.nAggregateFlip ? S.nAggregateFlip : S.nAggregateFlipCount); - pMenuText[nNumMenuItems++] = &AggregateText[0]; - pMenuText[nNumMenuItems++] = "Timers"; - pMenuText[nNumMenuItems++] = "Options"; - pMenuText[nNumMenuItems++] = "Preset"; - pMenuText[nNumMenuItems++] = "Custom"; - const int nPauseIndex = nNumMenuItems; - pMenuText[nNumMenuItems++] = S.nRunning ? "Pause" : "Unpause"; - pMenuText[nNumMenuItems++] = "Help"; - - if(S.nOverflow) - { - pMenuText[nNumMenuItems++] = "!BUFFERSFULL!"; - } - - - if(UI.GroupMenuCount != S.nGroupCount + S.nCategoryCount) - { - UI.GroupMenuCount = S.nGroupCount + S.nCategoryCount; - for(uint32_t i = 0; i < S.nCategoryCount; ++i) - { - UI.GroupMenu[i].nIsCategory = 1; - UI.GroupMenu[i].nCategoryIndex = i; - UI.GroupMenu[i].nIndex = i; - UI.GroupMenu[i].pName = S.CategoryInfo[i].pName; - } - for(uint32_t i = 0; i < S.nGroupCount; ++i) - { - uint32_t idx = i + S.nCategoryCount; - UI.GroupMenu[idx].nIsCategory = 0; - UI.GroupMenu[idx].nCategoryIndex = S.GroupInfo[i].nCategory; - UI.GroupMenu[idx].nIndex = i; - UI.GroupMenu[idx].pName = S.GroupInfo[i].pName; - } - std::sort(&UI.GroupMenu[0], &UI.GroupMenu[UI.GroupMenuCount], - [] (const MicroProfileGroupMenuItem& l, const MicroProfileGroupMenuItem& r) -> bool - { - if(l.nCategoryIndex < r.nCategoryIndex) - { - return true; - } - else if(r.nCategoryIndex < l.nCategoryIndex) - { - return false; - } - if(r.nIsCategory || l.nIsCategory) - { - return l.nIsCategory > r.nIsCategory; - } - return MP_STRCASECMP(l.pName, r.pName)<0; - } - ); - } - - MicroProfileSubmenuCallback GroupCallback[MICROPROFILE_MENU_MAX] = - { - MicroProfileUIMenuMode, - MicroProfileUIMenuGroups, - MicroProfileUIMenuAggregate, - MicroProfileUIMenuTimers, - MicroProfileUIMenuOptions, - MicroProfileUIMenuPreset, - MicroProfileUIMenuCustom, - MicroProfileUIMenuEmpty, - MicroProfileUIMenuEmpty, - MicroProfileUIMenuEmpty, - }; - - MicroProfileClickCallback CBClick[MICROPROFILE_MENU_MAX] = - { - MicroProfileUIClickMode, - MicroProfileUIClickGroups, - MicroProfileUIClickAggregate, - MicroProfileUIClickTimers, - MicroProfileUIClickOptions, - MicroProfileUIClickPreset, - MicroProfileUIClickCustom, - MicroProfileUIClickEmpty, - MicroProfileUIClickEmpty, - MicroProfileUIClickEmpty, - }; - - - uint32_t nSelectMenu = UINT32_MAX; - for(uint32_t i = 0; i < nNumMenuItems; ++i) - { - nMenuX[i] = nX; - uint32_t nLen = (uint32_t)strlen(pMenuText[i]); - uint32_t nEnd = nX + nLen * (MICROPROFILE_TEXT_WIDTH+1); - if(UI.nMouseY <= MICROPROFILE_TEXT_HEIGHT && UI.nMouseX <= nEnd && UI.nMouseX >= nX) - { - MicroProfileDrawBox(nX-1, nY, nX + nLen * (MICROPROFILE_TEXT_WIDTH+1), nY +(MICROPROFILE_TEXT_HEIGHT+1)+1, 0xff888888); - nSelectMenu = i; - if((UI.nMouseLeft || UI.nMouseRight) && i == (uint32_t)nPauseIndex) - { - S.nToggleRunning = 1; - } - } - MicroProfileDrawText(nX, nY, UINT32_MAX, pMenuText[i], (uint32_t)strlen(pMenuText[i])); - nX += (nLen+1) * (MICROPROFILE_TEXT_WIDTH+1); - } - uint32_t nMenu = nSelectMenu != UINT32_MAX ? nSelectMenu : UI.nActiveMenu; - UI.nActiveMenu = nMenu; - if(UINT32_MAX != nMenu) - { - nX = nMenuX[nMenu]; - nY += MICROPROFILE_TEXT_HEIGHT+1; - MicroProfileSubmenuCallback CB = GroupCallback[nMenu]; - int nNumLines = 0; - bool bSelected = false; - const char* pString = CB(nNumLines, &bSelected); - uint32_t nTextWidth = 0, nTextHeight = 0; - while(pString) - { - nTextWidth = MicroProfileMax(nTextWidth, (int)strlen(pString)); - nNumLines++; - pString = CB(nNumLines, &bSelected); - } - nTextWidth = (2+nTextWidth) * (MICROPROFILE_TEXT_WIDTH+1); - nTextHeight = nNumLines * (MICROPROFILE_TEXT_HEIGHT+1); - if(UI.nMouseY <= nY + nTextHeight+0 && UI.nMouseY >= nY-0 && UI.nMouseX <= nX + nTextWidth + 0 && UI.nMouseX >= nX - 0) - { - UI.nActiveMenu = nMenu; - } - else if(nSelectMenu == UINT32_MAX) - { - UI.nActiveMenu = UINT32_MAX; - } - MicroProfileDrawBox(nX, nY, nX + nTextWidth, nY + nTextHeight, 0xff000000|g_nMicroProfileBackColors[1]); - for(int i = 0; i < nNumLines; ++i) - { - bool bSelected2 = false; - const char* pString2 = CB(i, &bSelected2); - if(UI.nMouseY >= nY && UI.nMouseY < nY + MICROPROFILE_TEXT_HEIGHT + 1) - { - if(UI.nMouseLeft || UI.nMouseRight) - { - CBClick[nMenu](i); - } - MicroProfileDrawBox(nX, nY, nX + nTextWidth, nY + MICROPROFILE_TEXT_HEIGHT + 1, 0xff888888); - } - int nTextLen = snprintf(buffer, SBUF_SIZE-1, "%c %s", bSelected2 ? '*' : ' ' ,pString2); - MicroProfileDrawText(nX, nY, UINT32_MAX, buffer, nTextLen); - nY += MICROPROFILE_TEXT_HEIGHT+1; - } - } - - - { - static char FrameTimeMessage[64]; - float fToMs = MicroProfileTickToMsMultiplier(MicroProfileTicksPerSecondCpu()); - uint32_t nAggregateFrames = S.nAggregateFrames ? S.nAggregateFrames : 1; - float fMs = fToMs * (S.nFlipTicks); - float fAverageMs = fToMs * (S.nFlipAggregateDisplay / nAggregateFrames); - float fMaxMs = fToMs * S.nFlipMaxDisplay; - int nLen = snprintf(FrameTimeMessage, sizeof(FrameTimeMessage)-1, "Time[%6.2f] Avg[%6.2f] Max[%6.2f]", fMs, fAverageMs, fMaxMs); - pMenuText[nNumMenuItems++] = &FrameTimeMessage[0]; - MicroProfileDrawText(nWidth - nLen * (MICROPROFILE_TEXT_WIDTH+1), 0, UINT32_MAX, FrameTimeMessage, nLen); - } -} - - -inline void MicroProfileMoveGraph() -{ - - int nZoom = UI.nMouseWheelDelta; - int nPanX = 0; - int nPanY = 0; - static int X = 0, Y = 0; - if(UI.nMouseDownLeft && !UI.nModDown) - { - nPanX = UI.nMouseX - X; - nPanY = UI.nMouseY - Y; - } - X = UI.nMouseX; - Y = UI.nMouseY; - - if(nZoom) - { - float fOldRange = UI.fDetailedRange; - if(nZoom>0) - { - UI.fDetailedRangeTarget = UI.fDetailedRange *= UI.nModDown ? 1.40f : 1.05f; - } - else - { - float fNewDetailedRange = UI.fDetailedRange / (UI.nModDown ? 1.40f : 1.05f); - if(fNewDetailedRange < 1e-4f) //100ns - fNewDetailedRange = 1e-4f; - UI.fDetailedRangeTarget = UI.fDetailedRange = fNewDetailedRange; - } - - float fDiff = fOldRange - UI.fDetailedRange; - float fMousePrc = MicroProfileMax((float)UI.nMouseX / UI.nWidth ,0.f); - UI.fDetailedOffsetTarget = UI.fDetailedOffset += fDiff * fMousePrc; - - } - if(nPanX) - { - UI.fDetailedOffsetTarget = UI.fDetailedOffset += -nPanX * UI.fDetailedRange / UI.nWidth; - } - UI.nOffsetY -= nPanY; - UI.nOffsetX += nPanX; - if(UI.nOffsetX > 0) - UI.nOffsetX = 0; - if(UI.nOffsetY<0) - UI.nOffsetY = 0; -} - -inline void MicroProfileDrawCustom(uint32_t nWidth, uint32_t nHeight) -{ - if(UINT32_MAX != UI.nCustomActive) - { - MicroProfile& S = *MicroProfileGet(); - MP_ASSERT(UI.nCustomActive < MICROPROFILE_CUSTOM_MAX); - MicroProfileCustom* pCustom = &UI.Custom[UI.nCustomActive]; - uint32_t nCount = pCustom->nNumTimers; - uint32_t nAggregateFrames = S.nAggregateFrames ? S.nAggregateFrames : 1; - uint32_t nExtraOffset = 1 + ((pCustom->nFlags & MICROPROFILE_CUSTOM_STACK) != 0 ? 3 : 0); - uint32_t nOffsetYBase = nHeight - (nExtraOffset+nCount)* (1+MICROPROFILE_TEXT_HEIGHT) - MICROPROFILE_CUSTOM_PADDING; - uint32_t nOffsetY = nOffsetYBase; - float fReference = pCustom->fReference; - float fRcpReference = 1.f / fReference; - uint32_t nReducedWidth = UI.nWidth - 2*MICROPROFILE_CUSTOM_PADDING - MICROPROFILE_GRAPH_WIDTH; - - char Buffer[MICROPROFILE_NAME_MAX_LEN*2+1]; - float* pTime = (float*)alloca(sizeof(float)*nCount); - float* pTimeAvg = (float*)alloca(sizeof(float)*nCount); - float* pTimeMax = (float*)alloca(sizeof(float)*nCount); - uint32_t* pColors = (uint32_t*)alloca(sizeof(uint32_t)*nCount); - uint32_t nMaxOffsetX = 0; - MicroProfileDrawBox(MICROPROFILE_CUSTOM_PADDING-1, nOffsetY-1, MICROPROFILE_CUSTOM_PADDING+nReducedWidth+1, UI.nHeight - MICROPROFILE_CUSTOM_PADDING+1, 0x88000000|g_nMicroProfileBackColors[0]); - - for(uint32_t i = 0; i < nCount; ++i) - { - uint16_t nTimerIndex = MicroProfileGetTimerIndex(pCustom->pTimers[i]); - uint16_t nGroupIndex = MicroProfileGetGroupIndex(pCustom->pTimers[i]); - float fToMs = MicroProfileTickToMsMultiplier(S.GroupInfo[nGroupIndex].Type == MicroProfileTokenTypeGpu ? MicroProfileTicksPerSecondGpu() : MicroProfileTicksPerSecondCpu()); - pTime[i] = S.Frame[nTimerIndex].nTicks * fToMs; - pTimeAvg[i] = fToMs * (S.Aggregate[nTimerIndex].nTicks / nAggregateFrames); - pTimeMax[i] = fToMs * (S.AggregateMax[nTimerIndex]); - pColors[i] = S.TimerInfo[nTimerIndex].nColor; - } - - MicroProfileDrawText(MICROPROFILE_CUSTOM_PADDING + 3*MICROPROFILE_TEXT_WIDTH, nOffsetY, UINT32_MAX, "Avg", sizeof("Avg")-1); - MicroProfileDrawText(MICROPROFILE_CUSTOM_PADDING + 13*MICROPROFILE_TEXT_WIDTH, nOffsetY, UINT32_MAX, "Max", sizeof("Max")-1); - for(uint32_t i = 0; i < nCount; ++i) - { - nOffsetY += (1+MICROPROFILE_TEXT_HEIGHT); - uint16_t nTimerIndex = MicroProfileGetTimerIndex(pCustom->pTimers[i]); - uint16_t nGroupIndex = MicroProfileGetGroupIndex(pCustom->pTimers[i]); - MicroProfileTimerInfo* pTimerInfo = &S.TimerInfo[nTimerIndex]; - int nSize; - uint32_t nOffsetX = MICROPROFILE_CUSTOM_PADDING; - nSize = snprintf(Buffer, sizeof(Buffer)-1, "%6.2f", pTimeAvg[i]); - MicroProfileDrawText(nOffsetX, nOffsetY, UINT32_MAX, Buffer, nSize); - nOffsetX += (nSize+2) * (MICROPROFILE_TEXT_WIDTH+1); - nSize = snprintf(Buffer, sizeof(Buffer)-1, "%6.2f", pTimeMax[i]); - MicroProfileDrawText(nOffsetX, nOffsetY, UINT32_MAX, Buffer, nSize); - nOffsetX += (nSize+2) * (MICROPROFILE_TEXT_WIDTH+1); - nSize = snprintf(Buffer, sizeof(Buffer)-1, "%s:%s", S.GroupInfo[nGroupIndex].pName, pTimerInfo->pName); - MicroProfileDrawText(nOffsetX, nOffsetY, pTimerInfo->nColor, Buffer, nSize); - nOffsetX += (nSize+2) * (MICROPROFILE_TEXT_WIDTH+1); - nMaxOffsetX = MicroProfileMax(nMaxOffsetX, nOffsetX); - } - uint32_t nMaxWidth = nReducedWidth- nMaxOffsetX; - - if(pCustom->nFlags & MICROPROFILE_CUSTOM_BARS) - { - nOffsetY = nOffsetYBase; - float* pMs = pCustom->nFlags & MICROPROFILE_CUSTOM_BAR_SOURCE_MAX ? pTimeMax : pTimeAvg; - const char* pString = pCustom->nFlags & MICROPROFILE_CUSTOM_BAR_SOURCE_MAX ? "Max" : "Avg"; - MicroProfileDrawText(nMaxOffsetX, nOffsetY, UINT32_MAX, pString, static_cast(strlen(pString))); - int nSize = snprintf(Buffer, sizeof(Buffer)-1, "%6.2fms", fReference); - MicroProfileDrawText(nReducedWidth - (1+nSize) * (MICROPROFILE_TEXT_WIDTH+1), nOffsetY, UINT32_MAX, Buffer, nSize); - for(uint32_t i = 0; i < nCount; ++i) - { - nOffsetY += (1+MICROPROFILE_TEXT_HEIGHT); - nWidth = MicroProfileMin(nMaxWidth, (uint32_t)(nMaxWidth * pMs[i] * fRcpReference)); - MicroProfileDrawBox(nMaxOffsetX, nOffsetY, nMaxOffsetX+nWidth, nOffsetY+MICROPROFILE_TEXT_HEIGHT, pColors[i]|0xff000000); - } - } - if(pCustom->nFlags & MICROPROFILE_CUSTOM_STACK) - { - nOffsetY += 2*(1+MICROPROFILE_TEXT_HEIGHT); - const char* pString = pCustom->nFlags & MICROPROFILE_CUSTOM_STACK_SOURCE_MAX ? "Max" : "Avg"; - MicroProfileDrawText(MICROPROFILE_CUSTOM_PADDING, nOffsetY, UINT32_MAX, pString, static_cast(strlen(pString))); - int nSize = snprintf(Buffer, sizeof(Buffer)-1, "%6.2fms", fReference); - MicroProfileDrawText(nReducedWidth - (1+nSize) * (MICROPROFILE_TEXT_WIDTH+1), nOffsetY, UINT32_MAX, Buffer, nSize); - nOffsetY += (1+MICROPROFILE_TEXT_HEIGHT); - float fPosX = MICROPROFILE_CUSTOM_PADDING; - float* pMs = pCustom->nFlags & MICROPROFILE_CUSTOM_STACK_SOURCE_MAX ? pTimeMax : pTimeAvg; - for(uint32_t i = 0; i < nCount; ++i) - { - float fWidth = pMs[i] * fRcpReference * nReducedWidth; - uint32_t nX = fPosX; - fPosX += fWidth; - uint32_t nXEnd = fPosX; - if(nX < nXEnd) - { - MicroProfileDrawBox(nX, nOffsetY, nXEnd, nOffsetY+MICROPROFILE_TEXT_HEIGHT, pColors[i]|0xff000000); - } - } - } - } -} -inline void MicroProfileDraw(uint32_t nWidth, uint32_t nHeight) -{ - MICROPROFILE_SCOPE(g_MicroProfileDraw); - MicroProfile& S = *MicroProfileGet(); - - { - static int once = 0; - if(0 == once) - { - std::recursive_mutex& m = MicroProfileGetMutex(); - m.lock(); - MicroProfileInitUI(); - - - - uint32_t nDisplay = S.nDisplay; - MicroProfileLoadPreset(MICROPROFILE_DEFAULT_PRESET); - once++; - S.nDisplay = nDisplay;// dont load display, just state - m.unlock(); - - } - } - - - if(S.nDisplay) - { - std::recursive_mutex& m = MicroProfileGetMutex(); - m.lock(); - UI.nWidth = nWidth; - UI.nHeight = nHeight; - UI.nHoverToken = MICROPROFILE_INVALID_TOKEN; - UI.nHoverTime = 0; - UI.nHoverFrame = -1; - if(S.nDisplay != MP_DRAW_DETAILED) - S.nContextSwitchHoverThread = S.nContextSwitchHoverThreadAfter = S.nContextSwitchHoverThreadBefore = UINT32_MAX; - MicroProfileMoveGraph(); - - - if(S.nDisplay == MP_DRAW_DETAILED) - { - MicroProfileDrawDetailedView(nWidth, nHeight); - } - else if(S.nDisplay == MP_DRAW_BARS && S.nBars) - { - MicroProfileDrawBarView(nWidth, nHeight); - } - - MicroProfileDrawMenu(nWidth, nHeight); - bool bMouseOverGraph = MicroProfileDrawGraph(nWidth, nHeight); - MicroProfileDrawCustom(nWidth, nHeight); - bool bHidden = S.nDisplay == MP_DRAW_HIDDEN; - if(!bHidden) - { - uint32_t nLockedToolTipX = 3; - bool bDeleted = false; - for(int i = 0; i < MICROPROFILE_TOOLTIP_MAX_LOCKED; ++i) - { - int nIndex = (g_MicroProfileUI.LockedToolTipFront + i) % MICROPROFILE_TOOLTIP_MAX_LOCKED; - if(g_MicroProfileUI.LockedToolTips[nIndex].ppStrings[0]) - { - uint32_t nToolTipWidth = 0, nToolTipHeight = 0; - MicroProfileFloatWindowSize(g_MicroProfileUI.LockedToolTips[nIndex].ppStrings, g_MicroProfileUI.LockedToolTips[nIndex].nNumStrings, 0, nToolTipWidth, nToolTipHeight, 0); - uint32_t nStartY = nHeight - nToolTipHeight - 2; - if(!bDeleted && UI.nMouseY > nStartY && UI.nMouseX > nLockedToolTipX && UI.nMouseX <= nLockedToolTipX + nToolTipWidth && (UI.nMouseLeft || UI.nMouseRight) ) - { - bDeleted = true; - int j = i; - for(; j < MICROPROFILE_TOOLTIP_MAX_LOCKED-1; ++j) - { - int nIndex0 = (g_MicroProfileUI.LockedToolTipFront + j) % MICROPROFILE_TOOLTIP_MAX_LOCKED; - int nIndex1 = (g_MicroProfileUI.LockedToolTipFront + j+1) % MICROPROFILE_TOOLTIP_MAX_LOCKED; - MicroProfileStringArrayCopy(&g_MicroProfileUI.LockedToolTips[nIndex0], &g_MicroProfileUI.LockedToolTips[nIndex1]); - } - MicroProfileStringArrayClear(&g_MicroProfileUI.LockedToolTips[(g_MicroProfileUI.LockedToolTipFront + j) % MICROPROFILE_TOOLTIP_MAX_LOCKED]); - } - else - { - MicroProfileDrawFloatWindow(nLockedToolTipX, nHeight-nToolTipHeight-2, &g_MicroProfileUI.LockedToolTips[nIndex].ppStrings[0], g_MicroProfileUI.LockedToolTips[nIndex].nNumStrings, g_MicroProfileUI.nLockedToolTipColor[nIndex]); - nLockedToolTipX += nToolTipWidth + 4; - } - } - } - - if(UI.nActiveMenu == 8) - { - if(S.nDisplay & MP_DRAW_DETAILED) - { - MicroProfileStringArray DetailedHelp; - MicroProfileStringArrayClear(&DetailedHelp); - MicroProfileStringArrayFormat(&DetailedHelp, "%s", MICROPROFILE_HELP_LEFT); - MicroProfileStringArrayAddLiteral(&DetailedHelp, "Toggle Graph"); - MicroProfileStringArrayFormat(&DetailedHelp, "%s", MICROPROFILE_HELP_ALT); - MicroProfileStringArrayAddLiteral(&DetailedHelp, "Zoom"); - MicroProfileStringArrayFormat(&DetailedHelp, "%s + %s", MICROPROFILE_HELP_MOD, MICROPROFILE_HELP_LEFT); - MicroProfileStringArrayAddLiteral(&DetailedHelp, "Lock Tooltip"); - MicroProfileStringArrayAddLiteral(&DetailedHelp, "Drag"); - MicroProfileStringArrayAddLiteral(&DetailedHelp, "Pan View"); - MicroProfileStringArrayAddLiteral(&DetailedHelp, "Mouse Wheel"); - MicroProfileStringArrayAddLiteral(&DetailedHelp, "Zoom"); - MicroProfileDrawFloatWindow(nWidth, MICROPROFILE_FRAME_HISTORY_HEIGHT+20, DetailedHelp.ppStrings, DetailedHelp.nNumStrings, 0xff777777); - - MicroProfileStringArray DetailedHistoryHelp; - MicroProfileStringArrayClear(&DetailedHistoryHelp); - MicroProfileStringArrayFormat(&DetailedHistoryHelp, "%s", MICROPROFILE_HELP_LEFT); - MicroProfileStringArrayAddLiteral(&DetailedHistoryHelp, "Center View"); - MicroProfileStringArrayFormat(&DetailedHistoryHelp, "%s", MICROPROFILE_HELP_ALT); - MicroProfileStringArrayAddLiteral(&DetailedHistoryHelp, "Zoom to frame"); - MicroProfileDrawFloatWindow(nWidth, 20, DetailedHistoryHelp.ppStrings, DetailedHistoryHelp.nNumStrings, 0xff777777); - - - - } - else if(0 != (S.nDisplay & MP_DRAW_BARS) && S.nBars) - { - MicroProfileStringArray BarHelp; - MicroProfileStringArrayClear(&BarHelp); - MicroProfileStringArrayFormat(&BarHelp, "%s", MICROPROFILE_HELP_LEFT); - MicroProfileStringArrayAddLiteral(&BarHelp, "Toggle Graph"); - MicroProfileStringArrayFormat(&BarHelp, "%s + %s", MICROPROFILE_HELP_MOD, MICROPROFILE_HELP_LEFT); - MicroProfileStringArrayAddLiteral(&BarHelp, "Lock Tooltip"); - MicroProfileStringArrayAddLiteral(&BarHelp, "Drag"); - MicroProfileStringArrayAddLiteral(&BarHelp, "Pan View"); - MicroProfileDrawFloatWindow(nWidth, MICROPROFILE_FRAME_HISTORY_HEIGHT+20, BarHelp.ppStrings, BarHelp.nNumStrings, 0xff777777); - - } - MicroProfileStringArray Debug; - MicroProfileStringArrayClear(&Debug); - MicroProfileStringArrayAddLiteral(&Debug, "Memory Usage"); - MicroProfileStringArrayFormat(&Debug, "%4.2fmb", S.nMemUsage / (1024.f * 1024.f)); - MicroProfileStringArrayAddLiteral(&Debug, "Web Server Port"); - MicroProfileStringArrayFormat(&Debug, "%d", MicroProfileWebServerPort()); - uint32_t nFrameNext = (S.nFrameCurrent+1) % MICROPROFILE_MAX_FRAME_HISTORY; - MicroProfileFrameState* pFrameCurrent = &S.Frames[S.nFrameCurrent]; - MicroProfileFrameState* pFrameNext = &S.Frames[nFrameNext]; - - - MicroProfileStringArrayAddLiteral(&Debug, ""); - MicroProfileStringArrayAddLiteral(&Debug, ""); - MicroProfileStringArrayAddLiteral(&Debug, "Usage"); - MicroProfileStringArrayAddLiteral(&Debug, "markers [frames] "); - -#if MICROPROFILE_CONTEXT_SWITCH_TRACE - MicroProfileStringArrayAddLiteral(&Debug, "Context Switch"); - MicroProfileStringArrayFormat(&Debug, "%9d [%7d]", S.nContextSwitchUsage, MICROPROFILE_CONTEXT_SWITCH_BUFFER_SIZE / S.nContextSwitchUsage ); -#endif - - for(int i = 0; i < MICROPROFILE_MAX_THREADS; ++i) - { - if(pFrameCurrent->nLogStart[i] && S.Pool[i]) - { - uint32_t nEnd = pFrameNext->nLogStart[i]; - uint32_t nStart = pFrameCurrent->nLogStart[i]; - uint32_t nUsage = nStart < nEnd ? (nEnd - nStart) : (nEnd + MICROPROFILE_BUFFER_SIZE - nStart); - uint32_t nFrameSupport = MICROPROFILE_BUFFER_SIZE / nUsage; - MicroProfileStringArrayFormat(&Debug, "%s", &S.Pool[i]->ThreadName[0]); - MicroProfileStringArrayFormat(&Debug, "%9d [%7d]", nUsage, nFrameSupport); - } - } - - MicroProfileDrawFloatWindow(0, nHeight-10, Debug.ppStrings, Debug.nNumStrings, 0xff777777); - } - - - - if(UI.nActiveMenu == UINT32_MAX && !bMouseOverGraph) - { - if(UI.nHoverToken != MICROPROFILE_INVALID_TOKEN) - { - MicroProfileDrawFloatTooltip(UI.nMouseX, UI.nMouseY, UI.nHoverToken, UI.nHoverTime); - } - else if(S.nContextSwitchHoverThreadAfter != UINT32_MAX && S.nContextSwitchHoverThreadBefore != UINT32_MAX) - { - float fToMs = MicroProfileTickToMsMultiplier(MicroProfileTicksPerSecondCpu()); - MicroProfileStringArray ToolTip; - MicroProfileStringArrayClear(&ToolTip); - MicroProfileStringArrayAddLiteral(&ToolTip, "Context Switch"); - MicroProfileStringArrayFormat(&ToolTip, "%04x", S.nContextSwitchHoverThread); - MicroProfileStringArrayAddLiteral(&ToolTip, "Before"); - MicroProfileStringArrayFormat(&ToolTip, "%04x", S.nContextSwitchHoverThreadBefore); - MicroProfileStringArrayAddLiteral(&ToolTip, "After"); - MicroProfileStringArrayFormat(&ToolTip, "%04x", S.nContextSwitchHoverThreadAfter); - MicroProfileStringArrayAddLiteral(&ToolTip, "Duration"); - int64_t nDifference = MicroProfileLogTickDifference(S.nContextSwitchHoverTickIn, S.nContextSwitchHoverTickOut); - MicroProfileStringArrayFormat(&ToolTip, "%6.2fms", fToMs * nDifference ); - MicroProfileStringArrayAddLiteral(&ToolTip, "CPU"); - MicroProfileStringArrayFormat(&ToolTip, "%d", S.nContextSwitchHoverCpu); - MicroProfileDrawFloatWindow(UI.nMouseX, UI.nMouseY+20, &ToolTip.ppStrings[0], ToolTip.nNumStrings, UINT32_MAX); - - - } - else if(UI.nHoverFrame != -1) - { - uint32_t nNextFrame = (UI.nHoverFrame+1)%MICROPROFILE_MAX_FRAME_HISTORY; - int64_t nTick = S.Frames[UI.nHoverFrame].nFrameStartCpu; - int64_t nTickNext = S.Frames[nNextFrame].nFrameStartCpu; - int64_t nTickGpu = S.Frames[UI.nHoverFrame].nFrameStartGpu; - int64_t nTickNextGpu = S.Frames[nNextFrame].nFrameStartGpu; - - float fToMs = MicroProfileTickToMsMultiplier(MicroProfileTicksPerSecondCpu()); - float fToMsGpu = MicroProfileTickToMsMultiplier(MicroProfileTicksPerSecondGpu()); - float fMs = fToMs * (nTickNext - nTick); - float fMsGpu = fToMsGpu * (nTickNextGpu - nTickGpu); - MicroProfileStringArray ToolTip; - MicroProfileStringArrayClear(&ToolTip); - MicroProfileStringArrayFormat(&ToolTip, "Frame %d", UI.nHoverFrame); - #if MICROPROFILE_DEBUG - MicroProfileStringArrayFormat(&ToolTip, "%p", &S.Frames[UI.nHoverFrame]); - #else - MicroProfileStringArrayAddLiteral(&ToolTip, ""); - #endif - MicroProfileStringArrayAddLiteral(&ToolTip, "CPU Time"); - MicroProfileStringArrayFormat(&ToolTip, "%6.2fms", fMs); - MicroProfileStringArrayAddLiteral(&ToolTip, "GPU Time"); - MicroProfileStringArrayFormat(&ToolTip, "%6.2fms", fMsGpu); - #if MICROPROFILE_DEBUG - for(int i = 0; i < MICROPROFILE_MAX_THREADS; ++i) - { - if(S.Frames[UI.nHoverFrame].nLogStart[i]) - { - MicroProfileStringArrayFormat(&ToolTip, "%d", i); - MicroProfileStringArrayFormat(&ToolTip, "%d", S.Frames[UI.nHoverFrame].nLogStart[i]); - } - } - #endif - MicroProfileDrawFloatWindow(UI.nMouseX, UI.nMouseY+20, &ToolTip.ppStrings[0], ToolTip.nNumStrings, UINT32_MAX); - } - if(UI.nMouseLeft) - { - if(UI.nHoverToken != MICROPROFILE_INVALID_TOKEN) - MicroProfileToggleGraph(UI.nHoverToken); - } - } - } - -#if MICROPROFILE_DRAWCURSOR - { - float fCursor[8] = - { - MicroProfileMax(0, (int)UI.nMouseX-3), UI.nMouseY, - MicroProfileMin(nWidth, UI.nMouseX+3), UI.nMouseY, - UI.nMouseX, MicroProfileMax((int)UI.nMouseY-3, 0), - UI.nMouseX, MicroProfileMin(nHeight, UI.nMouseY+3), - }; - MicroProfileDrawLine2D(2, &fCursor[0], 0xff00ff00); - MicroProfileDrawLine2D(2, &fCursor[4], 0xff00ff00); - } -#endif - m.unlock(); - } - else if(UI.nCustomActive != UINT32_MAX) - { - std::recursive_mutex& m = MicroProfileGetMutex(); - m.lock(); - MicroProfileDrawGraph(nWidth, nHeight); - MicroProfileDrawCustom(nWidth, nHeight); - m.unlock(); - - } - UI.nMouseLeft = UI.nMouseRight = 0; - UI.nMouseLeftMod = UI.nMouseRightMod = 0; - UI.nMouseWheelDelta = 0; - if(S.nOverflow) - S.nOverflow--; - - UI.fDetailedOffset = UI.fDetailedOffset + (UI.fDetailedOffsetTarget - UI.fDetailedOffset) * MICROPROFILE_ANIM_DELAY_PRC; - UI.fDetailedRange = UI.fDetailedRange + (UI.fDetailedRangeTarget - UI.fDetailedRange) * MICROPROFILE_ANIM_DELAY_PRC; - - -} - -bool MicroProfileIsDrawing() -{ - MicroProfile& S = *MicroProfileGet(); - return S.nDisplay != 0; -} - -void MicroProfileToggleGraph(MicroProfileToken nToken) -{ - MicroProfile& S = *MicroProfileGet(); - uint32_t nTimerId = MicroProfileGetTimerIndex(nToken); - nToken &= 0xffff; - int32_t nMinSort = 0x7fffffff; - int32_t nFreeIndex = -1; - int32_t nMinIndex = 0; - int32_t nMaxSort = 0x80000000; - for(uint32_t i = 0; i < MICROPROFILE_MAX_GRAPHS; ++i) - { - if(S.Graph[i].nToken == MICROPROFILE_INVALID_TOKEN) - nFreeIndex = i; - if(S.Graph[i].nToken == nToken) - { - S.Graph[i].nToken = MICROPROFILE_INVALID_TOKEN; - S.TimerInfo[nTimerId].bGraph = false; - return; - } - if(S.Graph[i].nKey < nMinSort) - { - nMinSort = S.Graph[i].nKey; - nMinIndex = i; - } - if(S.Graph[i].nKey > nMaxSort) - { - nMaxSort = S.Graph[i].nKey; - } - } - int nIndex = nFreeIndex > -1 ? nFreeIndex : nMinIndex; - if (nFreeIndex == -1) - { - uint32_t idx = MicroProfileGetTimerIndex(S.Graph[nIndex].nToken); - S.TimerInfo[idx].bGraph = false; - } - S.Graph[nIndex].nToken = nToken; - S.Graph[nIndex].nKey = nMaxSort+1; - memset(&S.Graph[nIndex].nHistory[0], 0, sizeof(S.Graph[nIndex].nHistory)); - S.TimerInfo[nTimerId].bGraph = true; -} - - -void MicroProfileMousePosition(uint32_t nX, uint32_t nY, int nWheelDelta) -{ - UI.nMouseX = nX; - UI.nMouseY = nY; - UI.nMouseWheelDelta = nWheelDelta; -} - -void MicroProfileModKey(uint32_t nKeyState) -{ - UI.nModDown = nKeyState ? 1 : 0; -} - -void MicroProfileClearGraph() -{ - MicroProfile& S = *MicroProfileGet(); - for(uint32_t i = 0; i < MICROPROFILE_MAX_GRAPHS; ++i) - { - if(S.Graph[i].nToken != 0) - { - S.Graph[i].nToken = MICROPROFILE_INVALID_TOKEN; - } - } -} - -void MicroProfileMouseButton(uint32_t nLeft, uint32_t nRight) -{ - bool bCanRelease = abs((int)(UI.nMouseDownX - UI.nMouseX)) + abs((int)(UI.nMouseDownY - UI.nMouseY)) < 3; - - if(0 == nLeft && UI.nMouseDownLeft && bCanRelease) - { - if(UI.nModDown) - UI.nMouseLeftMod = 1; - else - UI.nMouseLeft = 1; - } - - if(0 == nRight && UI.nMouseDownRight && bCanRelease) - { - if(UI.nModDown) - UI.nMouseRightMod = 1; - else - UI.nMouseRight = 1; - } - if((nLeft || nRight) && !(UI.nMouseDownLeft || UI.nMouseDownRight)) - { - UI.nMouseDownX = UI.nMouseX; - UI.nMouseDownY = UI.nMouseY; - } - - UI.nMouseDownLeft = nLeft; - UI.nMouseDownRight = nRight; - -} - -void MicroProfileDrawLineVertical(int nX, int nTop, int nBottom, uint32_t nColor) -{ - MicroProfileDrawBox(nX, nTop, nX + 1, nBottom, nColor); -} - -void MicroProfileDrawLineHorizontal(int nLeft, int nRight, int nY, uint32_t nColor) -{ - MicroProfileDrawBox(nLeft, nY, nRight, nY + 1, nColor); -} - - - -#include - -#define MICROPROFILE_PRESET_HEADER_MAGIC 0x28586813 -#define MICROPROFILE_PRESET_HEADER_VERSION 0x00000102 -struct MicroProfilePresetHeader -{ - uint32_t nMagic; - uint32_t nVersion; - //groups, threads, aggregate, reference frame, graphs timers - uint32_t nGroups[MICROPROFILE_MAX_GROUPS]; - uint32_t nThreads[MICROPROFILE_MAX_THREADS]; - uint32_t nGraphName[MICROPROFILE_MAX_GRAPHS]; - uint32_t nGraphGroupName[MICROPROFILE_MAX_GRAPHS]; - uint32_t nAllGroupsWanted; - uint32_t nAllThreadsWanted; - uint32_t nAggregateFlip; - float fReferenceTime; - uint32_t nBars; - uint32_t nDisplay; - uint32_t nOpacityBackground; - uint32_t nOpacityForeground; - uint32_t nShowSpikes; -}; - -#ifndef MICROPROFILE_PRESET_FILENAME_FUNC -#define MICROPROFILE_PRESET_FILENAME_FUNC MicroProfilePresetFilename -static const char* MicroProfilePresetFilename(const char* pSuffix) -{ - static char filename[512]; - snprintf(filename, sizeof(filename)-1, ".microprofilepreset.%s", pSuffix); - return filename; -} -#endif - -void MicroProfileSavePreset(const char* pPresetName) -{ - std::lock_guard Lock(MicroProfileGetMutex()); - FILE* F = fopen(MICROPROFILE_PRESET_FILENAME_FUNC(pPresetName), "wb"); - if(!F) return; - - MicroProfile& S = *MicroProfileGet(); - - MicroProfilePresetHeader Header; - memset(&Header, 0, sizeof(Header)); - Header.nAggregateFlip = S.nAggregateFlip; - Header.nBars = S.nBars; - Header.fReferenceTime = S.fReferenceTime; - Header.nAllGroupsWanted = S.nAllGroupsWanted; - Header.nAllThreadsWanted = S.nAllThreadsWanted; - Header.nMagic = MICROPROFILE_PRESET_HEADER_MAGIC; - Header.nVersion = MICROPROFILE_PRESET_HEADER_VERSION; - Header.nDisplay = S.nDisplay; - Header.nOpacityBackground = UI.nOpacityBackground; - Header.nOpacityForeground = UI.nOpacityForeground; - Header.nShowSpikes = UI.bShowSpikes ? 1 : 0; - fwrite(&Header, sizeof(Header), 1, F); - uint64_t nMask = 1; - for(uint32_t i = 0; i < MICROPROFILE_MAX_GROUPS; ++i) - { - if(S.nActiveGroupWanted & nMask) - { - uint32_t offset = ftell(F); - const char* pName = S.GroupInfo[i].pName; - int nLen = (int)strlen(pName)+1; - fwrite(pName, nLen, 1, F); - Header.nGroups[i] = offset; - } - nMask <<= 1; - } - for(uint32_t i = 0; i < MICROPROFILE_MAX_THREADS; ++i) - { - MicroProfileThreadLog* pLog = S.Pool[i]; - if(pLog && S.nThreadActive[i]) - { - uint32_t nOffset = ftell(F); - const char* pName = &pLog->ThreadName[0]; - int nLen = (int)strlen(pName)+1; - fwrite(pName, nLen, 1, F); - Header.nThreads[i] = nOffset; - } - } - for(uint32_t i = 0; i < MICROPROFILE_MAX_GRAPHS; ++i) - { - MicroProfileToken nToken = S.Graph[i].nToken; - if(nToken != MICROPROFILE_INVALID_TOKEN) - { - uint32_t nGroupIndex = MicroProfileGetGroupIndex(nToken); - uint32_t nTimerIndex = MicroProfileGetTimerIndex(nToken); - const char* pGroupName = S.GroupInfo[nGroupIndex].pName; - const char* pTimerName = S.TimerInfo[nTimerIndex].pName; - MP_ASSERT(pGroupName); - MP_ASSERT(pTimerName); - int nGroupLen = (int)strlen(pGroupName)+1; - int nTimerLen = (int)strlen(pTimerName)+1; - - uint32_t nOffsetGroup = ftell(F); - fwrite(pGroupName, nGroupLen, 1, F); - uint32_t nOffsetTimer = ftell(F); - fwrite(pTimerName, nTimerLen, 1, F); - Header.nGraphName[i] = nOffsetTimer; - Header.nGraphGroupName[i] = nOffsetGroup; - } - } - fseek(F, 0, SEEK_SET); - fwrite(&Header, sizeof(Header), 1, F); - - fclose(F); - -} - - - -void MicroProfileLoadPreset(const char* pSuffix) -{ - std::lock_guard Lock(MicroProfileGetMutex()); - FILE* F = fopen(MICROPROFILE_PRESET_FILENAME_FUNC(pSuffix), "rb"); - if(!F) - { - return; - } - fseek(F, 0, SEEK_END); - int nSize = ftell(F); - char* const pBuffer = (char*)alloca(nSize); - fseek(F, 0, SEEK_SET); - int nRead = (int)fread(pBuffer, nSize, 1, F); - fclose(F); - if(1 != nRead) - return; - - MicroProfile& S = *MicroProfileGet(); - - MicroProfilePresetHeader& Header = *(MicroProfilePresetHeader*)pBuffer; - - if(Header.nMagic != MICROPROFILE_PRESET_HEADER_MAGIC || Header.nVersion != MICROPROFILE_PRESET_HEADER_VERSION) - { - return; - } - - S.nAggregateFlip = Header.nAggregateFlip; - S.nBars = Header.nBars; - S.fReferenceTime = Header.fReferenceTime; - S.fRcpReferenceTime = 1.f / Header.fReferenceTime; - S.nAllGroupsWanted = Header.nAllGroupsWanted; - S.nAllThreadsWanted = Header.nAllThreadsWanted; - S.nDisplay = Header.nDisplay; - S.nActiveGroupWanted = 0; - UI.nOpacityBackground = Header.nOpacityBackground; - UI.nOpacityForeground = Header.nOpacityForeground; - UI.bShowSpikes = Header.nShowSpikes == 1; - - memset(&S.nThreadActive[0], 0, sizeof(S.nThreadActive)); - - for(uint32_t i = 0; i < MICROPROFILE_MAX_GROUPS; ++i) - { - if(Header.nGroups[i]) - { - const char* pGroupName = pBuffer + Header.nGroups[i]; - for(uint32_t j = 0; j < MICROPROFILE_MAX_GROUPS; ++j) - { - if(0 == MP_STRCASECMP(pGroupName, S.GroupInfo[j].pName)) - { - S.nActiveGroupWanted |= (1ULL << j); - } - } - } - } - for(uint32_t i = 0; i < MICROPROFILE_MAX_THREADS; ++i) - { - if(Header.nThreads[i]) - { - const char* pThreadName = pBuffer + Header.nThreads[i]; - for(uint32_t j = 0; j < MICROPROFILE_MAX_THREADS; ++j) - { - MicroProfileThreadLog* pLog = S.Pool[j]; - if(pLog && 0 == MP_STRCASECMP(pThreadName, &pLog->ThreadName[0])) - { - S.nThreadActive[j] = 1; - } - } - } - } - for(uint32_t i = 0; i < MICROPROFILE_MAX_GRAPHS; ++i) - { - MicroProfileToken nPrevToken = S.Graph[i].nToken; - S.Graph[i].nToken = MICROPROFILE_INVALID_TOKEN; - if(Header.nGraphName[i] && Header.nGraphGroupName[i]) - { - const char* pGraphName = pBuffer + Header.nGraphName[i]; - const char* pGraphGroupName = pBuffer + Header.nGraphGroupName[i]; - for(uint32_t j = 0; j < S.nTotalTimers; ++j) - { - uint64_t nGroupIndex = S.TimerInfo[j].nGroupIndex; - if(0 == MP_STRCASECMP(pGraphName, S.TimerInfo[j].pName) && 0 == MP_STRCASECMP(pGraphGroupName, S.GroupInfo[nGroupIndex].pName)) - { - MicroProfileToken nToken = MicroProfileMakeToken(1ULL << nGroupIndex, (uint16_t)j); - S.Graph[i].nToken = nToken; // note: group index is stored here but is checked without in MicroProfileToggleGraph()! - S.TimerInfo[j].bGraph = true; - if(nToken != nPrevToken) - { - memset(&S.Graph[i].nHistory, 0, sizeof(S.Graph[i].nHistory)); - } - break; - } - } - } - } -} - -inline uint32_t MicroProfileCustomGroupFind(const char* pCustomName) -{ - for(uint32_t i = 0; i < UI.nCustomCount; ++i) - { - if(!MP_STRCASECMP(pCustomName, UI.Custom[i].pName)) - { - return i; - } - } - return UINT32_MAX; -} - -inline uint32_t MicroProfileCustomGroup(const char* pCustomName) -{ - for(uint32_t i = 0; i < UI.nCustomCount; ++i) - { - if(!MP_STRCASECMP(pCustomName, UI.Custom[i].pName)) - { - return i; - } - } - MP_ASSERT(UI.nCustomCount < MICROPROFILE_CUSTOM_MAX); - uint32_t nIndex = UI.nCustomCount; - UI.nCustomCount++; - memset(&UI.Custom[nIndex], 0, sizeof(UI.Custom[nIndex])); - size_t nLen = strlen(pCustomName); - if(nLen > MICROPROFILE_NAME_MAX_LEN-1) - nLen = MICROPROFILE_NAME_MAX_LEN-1; - memcpy(&UI.Custom[nIndex].pName[0], pCustomName, nLen); - UI.Custom[nIndex].pName[nLen] = '\0'; - return nIndex; -} -void MicroProfileCustomGroup(const char* pCustomName, uint32_t nMaxTimers, uint32_t nAggregateFlip, float fReferenceTime, uint32_t nFlags) -{ - uint32_t nIndex = MicroProfileCustomGroup(pCustomName); - MP_ASSERT(UI.Custom[nIndex].pTimers == 0);//only call once! - UI.Custom[nIndex].pTimers = &UI.CustomTimer[UI.nCustomTimerCount]; - UI.Custom[nIndex].nMaxTimers = nMaxTimers; - UI.Custom[nIndex].fReference = fReferenceTime; - UI.nCustomTimerCount += nMaxTimers; - MP_ASSERT(UI.nCustomTimerCount <= MICROPROFILE_CUSTOM_MAX_TIMERS); //bump MICROPROFILE_CUSTOM_MAX_TIMERS - UI.Custom[nIndex].nFlags = nFlags; - UI.Custom[nIndex].nAggregateFlip = nAggregateFlip; -} - -inline void MicroProfileCustomGroupEnable(uint32_t nIndex) -{ - if(nIndex < UI.nCustomCount) - { - MicroProfile& S = *MicroProfileGet(); - S.nForceGroupUI = UI.Custom[nIndex].nGroupMask; - MicroProfileSetAggregateFrames(UI.Custom[nIndex].nAggregateFlip); - S.fReferenceTime = UI.Custom[nIndex].fReference; - S.fRcpReferenceTime = 1.f / UI.Custom[nIndex].fReference; - UI.nCustomActive = nIndex; - - for(uint32_t i = 0; i < MICROPROFILE_MAX_GRAPHS; ++i) - { - if(S.Graph[i].nToken != MICROPROFILE_INVALID_TOKEN) - { - uint32_t nTimerId = MicroProfileGetTimerIndex(S.Graph[i].nToken); - S.TimerInfo[nTimerId].bGraph = false; - S.Graph[i].nToken = MICROPROFILE_INVALID_TOKEN; - } - } - - for(uint32_t i = 0; i < UI.Custom[nIndex].nNumTimers; ++i) - { - if(i == MICROPROFILE_MAX_GRAPHS) - { - break; - } - S.Graph[i].nToken = UI.Custom[nIndex].pTimers[i]; - S.Graph[i].nKey = i; - uint32_t nTimerId = MicroProfileGetTimerIndex(S.Graph[i].nToken); - S.TimerInfo[nTimerId].bGraph = true; - } - } -} - -void MicroProfileCustomGroupToggle(const char* pCustomName) -{ - uint32_t nIndex = MicroProfileCustomGroupFind(pCustomName); - if(nIndex == UINT32_MAX || nIndex == UI.nCustomActive) - { - MicroProfileCustomGroupDisable(); - } - else - { - MicroProfileCustomGroupEnable(nIndex); - } -} - -void MicroProfileCustomGroupEnable(const char* pCustomName) -{ - uint32_t nIndex = MicroProfileCustomGroupFind(pCustomName); - MicroProfileCustomGroupEnable(nIndex); -} -void MicroProfileCustomGroupDisable() -{ - MicroProfile& S = *MicroProfileGet(); - S.nForceGroupUI = 0; - UI.nCustomActive = UINT32_MAX; -} - -void MicroProfileCustomGroupAddTimer(const char* pCustomName, const char* pGroup, const char* pTimer) -{ - uint32_t nIndex = MicroProfileCustomGroupFind(pCustomName); - if(UINT32_MAX == nIndex) - { - return; - } - uint32_t nTimerIndex = UI.Custom[nIndex].nNumTimers; - MP_ASSERT(nTimerIndex < UI.Custom[nIndex].nMaxTimers); - uint64_t nToken = MicroProfileFindToken(pGroup, pTimer); - MP_ASSERT(nToken != MICROPROFILE_INVALID_TOKEN); //Timer must be registered first. - UI.Custom[nIndex].pTimers[nTimerIndex] = nToken; - uint16_t nGroup = MicroProfileGetGroupIndex(nToken); - UI.Custom[nIndex].nGroupMask |= (1ULL << nGroup); - UI.Custom[nIndex].nNumTimers++; -} - -#undef UI - -#endif -#endif diff --git a/externals/nx_tzdb/CMakeLists.txt b/externals/nx_tzdb/CMakeLists.txt index 13723f1750..a51939f7c8 100644 --- a/externals/nx_tzdb/CMakeLists.txt +++ b/externals/nx_tzdb/CMakeLists.txt @@ -1,6 +1,11 @@ +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later + # SPDX-FileCopyrightText: 2023 yuzu Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later +include(CPMUtil) + set(NX_TZDB_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/include") add_library(nx_tzdb INTERFACE) @@ -11,33 +16,41 @@ find_program(DATE_PROG date) set(CAN_BUILD_NX_TZDB true) -if (NOT GIT) - set(CAN_BUILD_NX_TZDB false) -endif() -if (NOT GNU_MAKE) - set(CAN_BUILD_NX_TZDB false) -endif() -if (NOT DATE_PROG) - set(CAN_BUILD_NX_TZDB false) -endif() -if (CMAKE_SYSTEM_NAME STREQUAL "Windows" OR ANDROID) +if (NOT (GIT AND GNU_MAKE AND DATE_PROG) OR CMAKE_SYSTEM_NAME STREQUAL "Windows" OR ANDROID) # tzdb_to_nx currently requires a posix-compliant host # MinGW and Android are handled here due to the executable format being different from the host system # TODO (lat9nq): cross-compiling support + set(CAN_BUILD_NX_TZDB false) endif() -set(NX_TZDB_VERSION "221202") -set(NX_TZDB_ARCHIVE "${CMAKE_CURRENT_BINARY_DIR}/${NX_TZDB_VERSION}.zip") +if (CAN_BUILD_NX_TZDB AND NOT YUZU_DOWNLOAD_TIME_ZONE_DATA) + message(FATAL_ERROR "Building tzdb is currently unsupported. Check back later.") + add_subdirectory(tzdb_to_nx) + add_dependencies(nx_tzdb x80e) -set(NX_TZDB_ROMFS_DIR "${CMAKE_CURRENT_BINARY_DIR}/nx_tzdb") + set(NX_TZDB_BASE_DIR "${NX_TZDB_DIR}") + set(NX_TZDB_TZ_DIR "${NX_TZDB_BASE_DIR}/zoneinfo") +endif() -if ((NOT CAN_BUILD_NX_TZDB OR YUZU_DOWNLOAD_TIME_ZONE_DATA) AND NOT EXISTS ${NX_TZDB_ROMFS_DIR}) - set(NX_TZDB_DOWNLOAD_URL "https://github.com/lat9nq/tzdb_to_nx/releases/download/${NX_TZDB_VERSION}/${NX_TZDB_VERSION}.zip") +if(NOT YUZU_TZDB_PATH STREQUAL "") + set(NX_TZDB_BASE_DIR "${YUZU_TZDB_PATH}") + set(NX_TZDB_TZ_DIR "${NX_TZDB_BASE_DIR}/zoneinfo") +elseif (MSVC) + # TODO(crueter): This is a terrible solution, but MSVC fails to link without it + # Need to investigate further but I still can't reproduce... + set(NX_TZDB_VERSION "250725") + set(NX_TZDB_ARCHIVE "${CPM_SOURCE_CACHE}/nx_tzdb/${NX_TZDB_VERSION}.zip") + + set(NX_TZDB_BASE_DIR "${CPM_SOURCE_CACHE}/nx_tzdb/tz") + set(NX_TZDB_TZ_DIR "${NX_TZDB_BASE_DIR}/zoneinfo") + + set(NX_TZDB_DOWNLOAD_URL "https://github.com/crueter/tzdb_to_nx/releases/download/${NX_TZDB_VERSION}/${NX_TZDB_VERSION}.zip") message(STATUS "Downloading time zone data from ${NX_TZDB_DOWNLOAD_URL}...") file(DOWNLOAD ${NX_TZDB_DOWNLOAD_URL} ${NX_TZDB_ARCHIVE} STATUS NX_TZDB_DOWNLOAD_STATUS) + list(GET NX_TZDB_DOWNLOAD_STATUS 0 NX_TZDB_DOWNLOAD_STATUS_CODE) if (NOT NX_TZDB_DOWNLOAD_STATUS_CODE EQUAL 0) message(FATAL_ERROR "Time zone data download failed (status code ${NX_TZDB_DOWNLOAD_STATUS_CODE})") @@ -47,12 +60,17 @@ if ((NOT CAN_BUILD_NX_TZDB OR YUZU_DOWNLOAD_TIME_ZONE_DATA) AND NOT EXISTS ${NX_ INPUT ${NX_TZDB_ARCHIVE} DESTINATION - ${NX_TZDB_ROMFS_DIR}) -elseif (CAN_BUILD_NX_TZDB AND NOT YUZU_DOWNLOAD_TIME_ZONE_DATA) - add_subdirectory(tzdb_to_nx) - add_dependencies(nx_tzdb x80e) + ${NX_TZDB_BASE_DIR}) +else() + message(STATUS "Downloading time zone data...") + AddJsonPackage(tzdb) - set(NX_TZDB_ROMFS_DIR "${NX_TZDB_DIR}") + target_include_directories(nx_tzdb + INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include + INTERFACE ${NX_TZDB_INCLUDE_DIR}) + + set(NX_TZDB_BASE_DIR "${CPM_SOURCE_CACHE}/nx_tzdb") + set(NX_TZDB_TZ_DIR "${nx_tzdb_SOURCE_DIR}") endif() target_include_directories(nx_tzdb @@ -77,25 +95,25 @@ function(CreateHeader ZONE_PATH HEADER_NAME) target_sources(nx_tzdb PRIVATE ${HEADER_PATH}) endfunction() -CreateHeader(${NX_TZDB_ROMFS_DIR} base) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo zoneinfo) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/Africa africa) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/America america) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/America/Argentina america_argentina) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/America/Indiana america_indiana) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/America/Kentucky america_kentucky) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/America/North_Dakota america_north_dakota) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/Antarctica antarctica) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/Arctic arctic) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/Asia asia) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/Atlantic atlantic) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/Australia australia) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/Brazil brazil) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/Canada canada) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/Chile chile) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/Etc etc) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/Europe europe) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/Indian indian) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/Mexico mexico) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/Pacific pacific) -CreateHeader(${NX_TZDB_ROMFS_DIR}/zoneinfo/US us) +CreateHeader(${NX_TZDB_BASE_DIR} base) +CreateHeader(${NX_TZDB_TZ_DIR} zoneinfo) +CreateHeader(${NX_TZDB_TZ_DIR}/Africa africa) +CreateHeader(${NX_TZDB_TZ_DIR}/America america) +CreateHeader(${NX_TZDB_TZ_DIR}/America/Argentina america_argentina) +CreateHeader(${NX_TZDB_TZ_DIR}/America/Indiana america_indiana) +CreateHeader(${NX_TZDB_TZ_DIR}/America/Kentucky america_kentucky) +CreateHeader(${NX_TZDB_TZ_DIR}/America/North_Dakota america_north_dakota) +CreateHeader(${NX_TZDB_TZ_DIR}/Antarctica antarctica) +CreateHeader(${NX_TZDB_TZ_DIR}/Arctic arctic) +CreateHeader(${NX_TZDB_TZ_DIR}/Asia asia) +CreateHeader(${NX_TZDB_TZ_DIR}/Atlantic atlantic) +CreateHeader(${NX_TZDB_TZ_DIR}/Australia australia) +CreateHeader(${NX_TZDB_TZ_DIR}/Brazil brazil) +CreateHeader(${NX_TZDB_TZ_DIR}/Canada canada) +CreateHeader(${NX_TZDB_TZ_DIR}/Chile chile) +CreateHeader(${NX_TZDB_TZ_DIR}/Etc etc) +CreateHeader(${NX_TZDB_TZ_DIR}/Europe europe) +CreateHeader(${NX_TZDB_TZ_DIR}/Indian indian) +CreateHeader(${NX_TZDB_TZ_DIR}/Mexico mexico) +CreateHeader(${NX_TZDB_TZ_DIR}/Pacific pacific) +CreateHeader(${NX_TZDB_TZ_DIR}/US us) diff --git a/externals/nx_tzdb/cpmfile.json b/externals/nx_tzdb/cpmfile.json new file mode 100644 index 0000000000..feb9daf7da --- /dev/null +++ b/externals/nx_tzdb/cpmfile.json @@ -0,0 +1,11 @@ +{ + "tzdb": { + "package": "nx_tzdb", + "repo": "misc/tzdb_to_nx", + "git_host": "git.crueter.xyz", + "artifact": "%VERSION%.zip", + "tag": "%VERSION%", + "hash": "8f60b4b29f285e39c0443f3d5572a73780f3dbfcfd5b35004451fadad77f3a215b2e2aa8d0fffe7e348e2a7b0660882b35228b6178dda8804a14ce44509fd2ca", + "version": "250725" + } +} diff --git a/externals/nx_tzdb/tzdb_to_nx b/externals/nx_tzdb/tzdb_to_nx deleted file mode 160000 index 9792969023..0000000000 --- a/externals/nx_tzdb/tzdb_to_nx +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 97929690234f2b4add36b33657fe3fe09bd57dfd diff --git a/externals/oaknut b/externals/oaknut deleted file mode 160000 index 9d091109de..0000000000 --- a/externals/oaknut +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9d091109deb445bc6e9289c6195a282b7c993d49 diff --git a/externals/oboe b/externals/oboe deleted file mode 160000 index 17ab1e4f41..0000000000 --- a/externals/oboe +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 17ab1e4f41e5028b344a79984ee3eb7b071f4167 diff --git a/externals/opus b/externals/opus deleted file mode 160000 index df02d25f0c..0000000000 --- a/externals/opus +++ /dev/null @@ -1 +0,0 @@ -Subproject commit df02d25f0c6334a60f8c01c5ecf63081845d6c9d diff --git a/externals/renderdoc/renderdoc_app.h b/externals/renderdoc/renderdoc_app.h index 0f4a1f98b3..3fdc233165 100644 --- a/externals/renderdoc/renderdoc_app.h +++ b/externals/renderdoc/renderdoc_app.h @@ -1,10 +1,13 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Baldur Karlsson // SPDX-License-Identifier: MIT /****************************************************************************** * The MIT License (MIT) * - * Copyright (c) 2019-2023 Baldur Karlsson + * Copyright (c) 2019-2025 Baldur Karlsson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -38,7 +41,7 @@ #if defined(WIN32) || defined(__WIN32__) || defined(_WIN32) || defined(_MSC_VER) #define RENDERDOC_CC __cdecl -#elif defined(__linux__) +#elif defined(__linux__) || defined(__FreeBSD__) || defined(__sun__) #define RENDERDOC_CC #elif defined(__APPLE__) #define RENDERDOC_CC @@ -358,14 +361,14 @@ typedef enum RENDERDOC_OverlayBits eRENDERDOC_Overlay_FrameNumber | eRENDERDOC_Overlay_CaptureList), // Enable all bits - eRENDERDOC_Overlay_All = ~0U, + eRENDERDOC_Overlay_All = 0x7ffffff, // Disable all bits eRENDERDOC_Overlay_None = 0, } RENDERDOC_OverlayBits; // returns the overlay bits that have been set -typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetOverlayBits)(); +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetOverlayBits)(void); // sets the overlay bits with an and & or mask typedef void(RENDERDOC_CC *pRENDERDOC_MaskOverlayBits)(uint32_t And, uint32_t Or); @@ -376,7 +379,7 @@ typedef void(RENDERDOC_CC *pRENDERDOC_MaskOverlayBits)(uint32_t And, uint32_t Or // injected hooks and shut down. Behaviour is undefined if this is called // after any API functions have been called, and there is still no guarantee of // success. -typedef void(RENDERDOC_CC *pRENDERDOC_RemoveHooks)(); +typedef void(RENDERDOC_CC *pRENDERDOC_RemoveHooks)(void); // DEPRECATED: compatibility for code compiled against pre-1.4.1 headers. typedef pRENDERDOC_RemoveHooks pRENDERDOC_Shutdown; @@ -386,7 +389,7 @@ typedef pRENDERDOC_RemoveHooks pRENDERDOC_Shutdown; // If you use your own crash handler and don't want RenderDoc's handler to // intercede, you can call this function to unload it and any unhandled // exceptions will pass to the next handler. -typedef void(RENDERDOC_CC *pRENDERDOC_UnloadCrashHandler)(); +typedef void(RENDERDOC_CC *pRENDERDOC_UnloadCrashHandler)(void); // Sets the capture file path template // @@ -408,14 +411,14 @@ typedef void(RENDERDOC_CC *pRENDERDOC_UnloadCrashHandler)(); typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureFilePathTemplate)(const char *pathtemplate); // returns the current capture path template, see SetCaptureFileTemplate above, as a UTF-8 string -typedef const char *(RENDERDOC_CC *pRENDERDOC_GetCaptureFilePathTemplate)(); +typedef const char *(RENDERDOC_CC *pRENDERDOC_GetCaptureFilePathTemplate)(void); // DEPRECATED: compatibility for code compiled against pre-1.1.2 headers. typedef pRENDERDOC_SetCaptureFilePathTemplate pRENDERDOC_SetLogFilePathTemplate; typedef pRENDERDOC_GetCaptureFilePathTemplate pRENDERDOC_GetLogFilePathTemplate; // returns the number of captures that have been made -typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetNumCaptures)(); +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetNumCaptures)(void); // This function returns the details of a capture, by index. New captures are added // to the end of the list. @@ -446,7 +449,7 @@ typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureFileComments)(const char *filePa const char *comments); // returns 1 if the RenderDoc UI is connected to this application, 0 otherwise -typedef uint32_t(RENDERDOC_CC *pRENDERDOC_IsTargetControlConnected)(); +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_IsTargetControlConnected)(void); // DEPRECATED: compatibility for code compiled against pre-1.1.1 headers. // This was renamed to IsTargetControlConnected in API 1.1.1, the old typedef is kept here for @@ -478,7 +481,7 @@ typedef void(RENDERDOC_CC *pRENDERDOC_GetAPIVersion)(int *major, int *minor, int // This will return 1 if the request was successfully passed on, though it's not guaranteed that // the UI will be on top in all cases depending on OS rules. It will return 0 if there is no current // target control connection to make such a request, or if there was another error -typedef uint32_t(RENDERDOC_CC *pRENDERDOC_ShowReplayUI)(); +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_ShowReplayUI)(void); ////////////////////////////////////////////////////////////////////////// // Capturing functions @@ -509,7 +512,7 @@ typedef void(RENDERDOC_CC *pRENDERDOC_SetActiveWindow)(RENDERDOC_DevicePointer d RENDERDOC_WindowHandle wndHandle); // capture the next frame on whichever window and API is currently considered active -typedef void(RENDERDOC_CC *pRENDERDOC_TriggerCapture)(); +typedef void(RENDERDOC_CC *pRENDERDOC_TriggerCapture)(void); // capture the next N frames on whichever window and API is currently considered active typedef void(RENDERDOC_CC *pRENDERDOC_TriggerMultiFrameCapture)(uint32_t numFrames); @@ -538,7 +541,7 @@ typedef void(RENDERDOC_CC *pRENDERDOC_StartFrameCapture)(RENDERDOC_DevicePointer // Returns whether or not a frame capture is currently ongoing anywhere. // // This will return 1 if a capture is ongoing, and 0 if there is no capture running -typedef uint32_t(RENDERDOC_CC *pRENDERDOC_IsFrameCapturing)(); +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_IsFrameCapturing)(void); // Ends capturing immediately. // diff --git a/externals/simpleini b/externals/simpleini deleted file mode 160000 index 382ddbb4b9..0000000000 --- a/externals/simpleini +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 382ddbb4b92c0b26aa1b32cefba2002119a5b1f2 diff --git a/externals/sirit b/externals/sirit deleted file mode 160000 index 6e6d79272c..0000000000 --- a/externals/sirit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6e6d79272cd4933b6647dafb6a662e5269241464 diff --git a/externals/sse2neon/sse2neon.h b/externals/sse2neon/sse2neon.h index 66b93c1c74..67ad0ae6f8 100755 --- a/externals/sse2neon/sse2neon.h +++ b/externals/sse2neon/sse2neon.h @@ -183,7 +183,7 @@ } /* Compiler barrier */ -#if defined(_MSC_VER) +#if defined(_MSC_VER) && !defined(__clang__) #define SSE2NEON_BARRIER() _ReadWriteBarrier() #else #define SSE2NEON_BARRIER() \ @@ -859,7 +859,7 @@ FORCE_INLINE uint64x2_t _sse2neon_vmull_p64(uint64x1_t _a, uint64x1_t _b) { poly64_t a = vget_lane_p64(vreinterpret_p64_u64(_a), 0); poly64_t b = vget_lane_p64(vreinterpret_p64_u64(_b), 0); -#if defined(_MSC_VER) +#if defined(_MSC_VER) && !defined(__clang__) __n64 a1 = {a}, b1 = {b}; return vreinterpretq_u64_p128(vmull_p64(a1, b1)); #else @@ -1770,7 +1770,7 @@ FORCE_INLINE void _mm_free(void *addr) FORCE_INLINE uint64_t _sse2neon_get_fpcr(void) { uint64_t value; -#if defined(_MSC_VER) +#if defined(_MSC_VER) && !defined(__clang__) value = _ReadStatusReg(ARM64_FPCR); #else __asm__ __volatile__("mrs %0, FPCR" : "=r"(value)); /* read */ @@ -1780,7 +1780,7 @@ FORCE_INLINE uint64_t _sse2neon_get_fpcr(void) FORCE_INLINE void _sse2neon_set_fpcr(uint64_t value) { -#if defined(_MSC_VER) +#if defined(_MSC_VER) && !defined(__clang__) _WriteStatusReg(ARM64_FPCR, value); #else __asm__ __volatile__("msr FPCR, %0" ::"r"(value)); /* write */ @@ -2249,7 +2249,7 @@ FORCE_INLINE __m128 _mm_or_ps(__m128 a, __m128 b) FORCE_INLINE void _mm_prefetch(char const *p, int i) { (void) i; -#if defined(_MSC_VER) +#if defined(_MSC_VER) && !defined(__clang__) switch (i) { case _MM_HINT_NTA: __prefetch2(p, 1); @@ -4820,7 +4820,7 @@ FORCE_INLINE __m128i _mm_packus_epi16(const __m128i a, const __m128i b) // https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#text=_mm_pause FORCE_INLINE void _mm_pause(void) { -#if defined(_MSC_VER) +#if defined(_MSC_VER) && !defined(__clang__) __isb(_ARM64_BARRIER_SY); #else __asm__ __volatile__("isb\n"); @@ -5716,7 +5716,7 @@ FORCE_INLINE __m128d _mm_undefined_pd(void) #pragma GCC diagnostic ignored "-Wuninitialized" #endif __m128d a; -#if defined(_MSC_VER) +#if defined(_MSC_VER) && !defined(__clang__) a = _mm_setzero_pd(); #endif return a; @@ -8130,7 +8130,7 @@ FORCE_INLINE int _sse2neon_sido_negative(int res, int lb, int imm8, int bound) FORCE_INLINE int _sse2neon_clz(unsigned int x) { -#ifdef _MSC_VER +#if defined(_MSC_VER) && !defined(__clang__) unsigned long cnt = 0; if (_BitScanReverse(&cnt, x)) return 31 - cnt; @@ -8142,7 +8142,7 @@ FORCE_INLINE int _sse2neon_clz(unsigned int x) FORCE_INLINE int _sse2neon_ctz(unsigned int x) { -#ifdef _MSC_VER +#if defined(_MSC_VER) && !defined(__clang__) unsigned long cnt = 0; if (_BitScanForward(&cnt, x)) return cnt; @@ -9058,7 +9058,7 @@ FORCE_INLINE __m128i _mm_aeskeygenassist_si128(__m128i a, const int rcon) // AESE does ShiftRows and SubBytes on A uint8x16_t u8 = vaeseq_u8(vreinterpretq_u8_m128i(a), vdupq_n_u8(0)); -#ifndef _MSC_VER +#if !defined(_MSC_VER) || defined(__clang__) uint8x16_t dest = { // Undo ShiftRows step from AESE and extract X1 and X3 u8[0x4], u8[0x1], u8[0xE], u8[0xB], // SubBytes(X1) @@ -9245,7 +9245,7 @@ FORCE_INLINE uint64_t _rdtsc(void) * bits wide and it is attributed with the flag 'cap_user_time_short' * is true. */ -#if defined(_MSC_VER) +#if defined(_MSC_VER) && !defined(__clang__) val = _ReadStatusReg(ARM64_SYSREG(3, 3, 14, 0, 2)); #else __asm__ __volatile__("mrs %0, cntvct_el0" : "=r"(val)); diff --git a/externals/vcpkg b/externals/vcpkg deleted file mode 160000 index ea2a964f93..0000000000 --- a/externals/vcpkg +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ea2a964f9303270322cf3f2d51c265ba146c422d diff --git a/externals/xbyak b/externals/xbyak deleted file mode 160000 index 0d67fd1530..0000000000 --- a/externals/xbyak +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0d67fd1530016b7c56f3cd74b3fca920f4c3e2b4 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 117a0e1265..0f3c5cfd4b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,28 +1,37 @@ +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-FileCopyrightText: 2018 yuzu Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later # Enable modules to include each other's files include_directories(.) +# Dynarmic +if ((ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64)) + set(DYNARMIC_IGNORE_ASSERTS ON) + add_subdirectory(dynarmic) + add_library(dynarmic::dynarmic ALIAS dynarmic) +endif() + # CMake seems to only define _DEBUG on Windows set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS $<$:_DEBUG> $<$>:NDEBUG>) # Set compilation flags -if (MSVC) +if (MSVC AND NOT CXX_CLANG) set(CMAKE_CONFIGURATION_TYPES Debug Release CACHE STRING "" FORCE) # Silence "deprecation" warnings - add_definitions(-D_CRT_SECURE_NO_WARNINGS -D_CRT_NONSTDC_NO_DEPRECATE -D_SCL_SECURE_NO_WARNINGS) + add_compile_definitions(_CRT_SECURE_NO_WARNINGS _CRT_NONSTDC_NO_DEPRECATE _SCL_SECURE_NO_WARNINGS) # Avoid windows.h junk - add_definitions(-DNOMINMAX) + add_compile_definitions(NOMINMAX) # Avoid windows.h from including some usually unused libs like winsocks.h, since this might cause some redefinition errors. - add_definitions(-DWIN32_LEAN_AND_MEAN) + add_compile_definitions(WIN32_LEAN_AND_MEAN) # Ensure that projects are built with Unicode support. - add_definitions(-DUNICODE -D_UNICODE) + add_compile_definitions(UNICODE _UNICODE) # /W4 - Level 4 warnings # /MP - Multi-threaded compilation @@ -60,10 +69,6 @@ if (MSVC) /external:anglebrackets # Treats all headers included by #include
, where the header file is enclosed in angle brackets (< >), as external headers /external:W0 # Sets the default warning level to 0 for external headers, effectively disabling warnings for them. - # Warnings - /W4 - /WX- - /we4062 # Enumerator 'identifier' in a switch of enum 'enumeration' is not handled /we4189 # 'identifier': local variable is initialized but not referenced /we4265 # 'class': class has virtual functions, but destructor is not virtual @@ -88,15 +93,17 @@ if (MSVC) /wd4702 # unreachable code (when used with LTO) ) - if (USE_CCACHE OR YUZU_USE_PRECOMPILED_HEADERS) - # when caching, we need to use /Z7 to downgrade debug info to use an older but more cacheable format - # Precompiled headers are deleted if not using /Z7. See https://github.com/nanoant/CMakePCHCompiler/issues/21 - add_compile_options(/Z7) - # Avoid D9025 warning - string(REPLACE "/Zi" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") - string(REPLACE "/Zi" "" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") - else() - add_compile_options(/Zi) + if (NOT CXX_CLANG) + add_compile_options( + # Warnings + /W4 + /WX- + ) + endif() + + if (WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) + string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") + string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") endif() if (ARCHITECTURE_x86_64) @@ -109,9 +116,13 @@ if (MSVC) set(CMAKE_EXE_LINKER_FLAGS_DEBUG "/DEBUG /MANIFEST:NO" CACHE STRING "" FORCE) set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/DEBUG /MANIFEST:NO /INCREMENTAL:NO /OPT:REF,ICF" CACHE STRING "" FORCE) else() - add_compile_options( - -fwrapv + if (NOT MSVC) + add_compile_options( + -fwrapv + ) + endif() + add_compile_options( -Werror=all -Werror=extra -Werror=missing-declarations @@ -124,14 +135,19 @@ else() -Wno-missing-field-initializers ) - if (CMAKE_CXX_COMPILER_ID MATCHES Clang) # Clang or AppleClang + if (CXX_CLANG OR CXX_ICC OR CXX_APPLE) # Clang, AppleClang, or Intel C++ + if (NOT MSVC) + add_compile_options( + -Werror=shadow-uncaptured-local + -Werror=implicit-fallthrough + -Werror=type-limits + ) + endif() + add_compile_options( -Wno-braced-scalar-init -Wno-unused-private-field -Wno-nullability-completeness - -Werror=shadow-uncaptured-local - -Werror=implicit-fallthrough - -Werror=type-limits ) endif() @@ -139,12 +155,12 @@ else() add_compile_options("-mcx16") endif() - if (APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL Clang) + if (APPLE AND CXX_CLANG) add_compile_options("-stdlib=libc++") endif() # GCC bugs - if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "11" AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "11" AND CXX_GCC) # These diagnostics would be great if they worked, but are just completely broken # and produce bogus errors on external libraries like fmt. add_compile_options( @@ -160,15 +176,15 @@ else() # glibc, which may default to 32 bits. glibc allows this to be configured # by setting _FILE_OFFSET_BITS. if(CMAKE_SYSTEM_NAME STREQUAL "Linux" OR MINGW) - add_definitions(-D_FILE_OFFSET_BITS=64) + add_compile_definitions(_FILE_OFFSET_BITS=64) endif() if (MINGW) - add_definitions(-DMINGW_HAS_SECURE_API) + add_compile_definitions(MINGW_HAS_SECURE_API) add_compile_options("-msse4.1") if (MINGW_STATIC_BUILD) - add_definitions(-DQT_STATICPLUGIN) + add_compile_definitions(QT_STATICPLUGIN) add_compile_options("-static") endif() endif() @@ -194,19 +210,26 @@ add_subdirectory(frontend_common) add_subdirectory(shader_recompiler) if (YUZU_ROOM) - add_subdirectory(dedicated_room) + add_subdirectory(dedicated_room) endif() if (YUZU_TESTS) add_subdirectory(tests) endif() -if (ENABLE_SDL2) +if (ENABLE_SDL2 AND YUZU_CMD) add_subdirectory(yuzu_cmd) set_target_properties(yuzu-cmd PROPERTIES OUTPUT_NAME "eden-cli") endif() +if (YUZU_ROOM_STANDALONE) + add_subdirectory(yuzu_room_standalone) + set_target_properties(yuzu-room PROPERTIES OUTPUT_NAME "eden-room") +endif() + if (ENABLE_QT) + add_definitions(-DYUZU_QT_WIDGETS) + add_subdirectory(qt_common) add_subdirectory(yuzu) endif() @@ -218,3 +241,5 @@ if (ANDROID) add_subdirectory(android/app/src/main/jni) target_include_directories(yuzu-android PRIVATE android/app/src/main) endif() + +include(GenerateDepHashes) diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 95a9cd84f5..31db36199a 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -1,6 +1,9 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + import android.annotation.SuppressLint import kotlin.collections.setOf import org.jlleitschuh.gradle.ktlint.reporter.ReporterType @@ -27,20 +30,21 @@ val autoVersion = (((System.currentTimeMillis() / 1000) - 1451606400) / 10).toIn android { namespace = "org.yuzu.yuzu_emu" - compileSdkVersion = "android-35" - ndkVersion = "26.1.10909125" + compileSdkVersion = "android-36" + ndkVersion = "28.2.13676358" buildFeatures { viewBinding = true + buildConfig = true } compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "21" + jvmTarget = "17" } packaging { @@ -53,17 +57,13 @@ android { } defaultConfig { - // TODO If this is ever modified, change application_id in strings.xml applicationId = "dev.eden.eden_emulator" - minSdk = 30 - targetSdk = 35 + + minSdk = 28 + targetSdk = 36 versionName = getGitVersion() - versionCode = if (System.getenv("AUTO_VERSIONED") == "true") { - autoVersion - } else { - 1 - } + versionCode = autoVersion ndk { @SuppressLint("ChromeOsAbiSupport") @@ -72,8 +72,33 @@ android { buildConfigField("String", "GIT_HASH", "\"${getGitHash()}\"") buildConfigField("String", "BRANCH", "\"${getBranch()}\"") + + externalNativeBuild { + cmake { + val extraCMakeArgs = (project.findProperty("YUZU_ANDROID_ARGS") as String?)?.split("\\s+".toRegex()) ?: emptyList() + + arguments.addAll(listOf( + "-DENABLE_QT=0", // Don't use QT + "-DENABLE_SDL2=0", // Don't use SDL + "-DENABLE_WEB_SERVICE=1", // Enable web service + "-DENABLE_OPENSSL=ON", + "-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work + "-DYUZU_USE_CPM=ON", + "-DCPMUTIL_FORCE_BUNDLED=ON", + "-DYUZU_USE_BUNDLED_FFMPEG=ON", + "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", + "-DBUILD_TESTING=OFF", + "-DYUZU_TESTS=OFF", + "-DDYNARMIC_TESTS=OFF", + *extraCMakeArgs.toTypedArray() + )) + + abiFilters("arm64-v8a") + } + } } + val keystoreFile = System.getenv("ANDROID_KEYSTORE_FILE") signingConfigs { if (keystoreFile != null) { @@ -94,7 +119,6 @@ android { // Define build types, which are orthogonal to product flavors. buildTypes { - // Signed by release key, allowing for upload to Play Store. release { signingConfig = if (keystoreFile != null) { @@ -103,7 +127,6 @@ android { signingConfigs.getByName("default") } - resValue("string", "app_name_suffixed", "eden") isMinifyEnabled = true isDebuggable = false proguardFiles( @@ -116,9 +139,7 @@ android { // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build. register("relWithDebInfo") { isDefault = true - resValue("string", "app_name_suffixed", "eden Debug Release") signingConfig = signingConfigs.getByName("default") - isMinifyEnabled = true isDebuggable = true proguardFiles( getDefaultProguardFile("proguard-android.txt"), @@ -133,7 +154,6 @@ android { // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build. debug { signingConfig = signingConfigs.getByName("default") - resValue("string", "app_name_suffixed", "eden Debug") isDebuggable = true isJniDebuggable = true versionNameSuffix = "-debug" @@ -141,16 +161,62 @@ android { } } - flavorDimensions.add("version") - productFlavors { - create("mainline") { - isDefault = true - dimension = "version" + // this is really annoying but idk any other ways to fix this behavior + applicationVariants.all { + val variant = this + when { + variant.flavorName == "legacy" && variant.buildType.name == "debug" -> { + variant.resValue("string", "app_name_suffixed", "Eden Legacy Debug") + } + variant.flavorName == "mainline" && variant.buildType.name == "debug" -> { + variant.resValue("string", "app_name_suffixed", "Eden Debug") + } + variant.flavorName == "genshinSpoof" && variant.buildType.name == "debug" -> { + variant.resValue("string", "app_name_suffixed", "Eden Optimized Debug") + } + variant.flavorName == "legacy" && variant.buildType.name == "relWithDebInfo" -> { + variant.resValue("string", "app_name_suffixed", "Eden Legacy Debug Release") + } + variant.flavorName == "mainline" && variant.buildType.name == "relWithDebInfo" -> { + variant.resValue("string", "app_name_suffixed", "Eden Debug Release") + } + variant.flavorName == "genshinSpoof" && variant.buildType.name == "relWithDebInfo" -> { + variant.resValue("string", "app_name_suffixed", "Eden Optimized Debug Release") + } } + } - create("ea") { - dimension = "version" - applicationIdSuffix = ".ea" + android { + flavorDimensions.add("version") + productFlavors { + create("mainline") { + dimension = "version" + resValue("string", "app_name_suffixed", "Eden") + } + + create("genshinSpoof") { + dimension = "version" + resValue("string", "app_name_suffixed", "Eden Optimized") + applicationId = "com.miHoYo.Yuanshen" + } + + create("legacy") { + dimension = "version" + resValue("string", "app_name_suffixed", "Eden Legacy") + applicationId = "dev.legacy.eden_emulator" + + externalNativeBuild { + cmake { + arguments.add("-DYUZU_LEGACY=ON") + } + } + + sourceSets { + getByName("legacy") { + res.srcDirs("src/main/legacy") + } + } + } } } @@ -160,30 +226,11 @@ android { path = file("../../../CMakeLists.txt") } } - - defaultConfig { - externalNativeBuild { - cmake { - arguments( - "-DENABLE_QT=0", // Don't use QT - "-DENABLE_SDL2=0", // Don't use SDL - "-DENABLE_WEB_SERVICE=0", // Don't use telemetry - "-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work - "-DYUZU_USE_BUNDLED_VCPKG=ON", - "-DYUZU_USE_BUNDLED_FFMPEG=ON", - "-DYUZU_ENABLE_LTO=ON", - "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON" - ) - - abiFilters("arm64-v8a", "x86_64") - } - } - } } -tasks.create("ktlintReset") { - delete(File(buildDir.path + File.separator + "intermediates/ktLint")) -} +tasks.register("ktlintReset", fun Delete.() { + delete(File(layout.buildDirectory.toString() + File.separator + "intermediates/ktLint")) +}) val showFormatHelp = { logger.lifecycle( @@ -230,17 +277,21 @@ dependencies { implementation("com.google.android.material:material:1.12.0") implementation("androidx.preference:preference-ktx:1.2.1") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7") + implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("io.coil-kt:coil:2.2.2") implementation("androidx.core:core-splashscreen:1.0.1") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2") implementation("androidx.window:window:1.3.0") implementation("androidx.constraintlayout:constraintlayout:2.2.1") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") + implementation("org.commonmark:commonmark:0.22.0") implementation("androidx.navigation:navigation-fragment-ktx:2.8.9") implementation("androidx.navigation:navigation-ui-ktx:2.8.9") implementation("info.debatty:java-string-similarity:2.0.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") implementation("androidx.compose.ui:ui-graphics-android:1.7.8") implementation("androidx.compose.ui:ui-text-android:1.7.8") + implementation("net.swiftzer.semver:semver:2.0.0") } fun runGitCommand(command: List): String { diff --git a/src/android/app/src/ea/res/drawable/ic_yuzu.xml b/src/android/app/src/ea/res/drawable/ic_yuzu.xml deleted file mode 100644 index deb8ba53fd..0000000000 --- a/src/android/app/src/ea/res/drawable/ic_yuzu.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - diff --git a/src/android/app/src/ea/res/drawable/ic_yuzu_full.xml b/src/android/app/src/ea/res/drawable/ic_yuzu_full.xml deleted file mode 100644 index 4ef4728769..0000000000 --- a/src/android/app/src/ea/res/drawable/ic_yuzu_full.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/src/android/app/src/ea/res/drawable/ic_yuzu_title.xml b/src/android/app/src/ea/res/drawable/ic_yuzu_title.xml deleted file mode 100644 index 29d0cfced8..0000000000 --- a/src/android/app/src/ea/res/drawable/ic_yuzu_title.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 6ad1d1c1bf..45c5dfef8c 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -3,19 +3,34 @@ + - + + + + + + + + + - + + + + + - - + - @@ -61,26 +78,27 @@ SPDX-License-Identifier: GPL-3.0-or-later android:supportsPictureInPicture="true" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode" android:exported="true"> - - - + - - + + + + + + + + + - diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index 1848ca9ef9..ba50bcad34 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -1,9 +1,8 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later package org.yuzu.yuzu_emu @@ -15,6 +14,7 @@ import android.view.Surface import android.view.View import android.widget.TextView import androidx.annotation.Keep +import androidx.core.net.toUri import com.google.android.material.dialog.MaterialAlertDialogBuilder import java.lang.ref.WeakReference import org.yuzu.yuzu_emu.activities.EmulationActivity @@ -101,6 +101,21 @@ object NativeLibrary { FileUtil.getFilename(Uri.parse(path)) } + @Keep + @JvmStatic + fun copyFileToStorage(source: String, destdir: String): Boolean { + return FileUtil.copyUriToInternalStorage( + source.toUri(), + destdir + ) != null + } + + @Keep + @JvmStatic + fun getFileExtension(source: String): String { + return FileUtil.getExtension(source.toUri()) + } + external fun setAppDirectory(directory: String) /** @@ -166,6 +181,11 @@ object NativeLibrary { */ external fun getPerfStats(): DoubleArray + /** + * Returns the number of shaders being built + */ + external fun getShadersBuilding(): Int + /** * Returns the current CPU backend. */ @@ -253,8 +273,7 @@ object NativeLibrary { val emulationActivity = sEmulationActivity.get() if (emulationActivity != null) { emulationActivity.addNetPlayMessages(type, message) - } - else { + } else { NetPlayManager.addNetPlayMessage(type, message) } } @@ -265,8 +284,7 @@ object NativeLibrary { NetPlayManager.clearChat() } - - external fun netPlayInit() + external fun initMultiplayer() @Keep @JvmStatic @@ -403,6 +421,37 @@ object NativeLibrary { */ external fun isFirmwareAvailable(): Boolean + /** + * Gets the firmware version. + * + * @return Reported firmware version + */ + external fun firmwareVersion(): String + + /** + * Verifies installed firmware. + * + * @return The result code. + */ + external fun verifyFirmware(): Int + + /** + * Check if a game requires firmware to be playable. + * + * @param programId The game's Program ID. + * @return Whether or not the game requires firmware to be playable. + */ + external fun gameRequiresFirmware(programId: String): Boolean + + /** + * Installs keys from the specified path. + * + * @param path The path to install keys from. + * @param ext What extension the keys should have. + * @return The result code. + */ + external fun installKeys(path: String, ext: String): Int + /** * Checks the PatchManager for any addons that are available * @@ -485,4 +534,9 @@ object NativeLibrary { * Checks if all necessary keys are present for decryption */ external fun areKeysPresent(): Boolean + + /** + * Updates the device power state to global variables + */ + external fun updatePowerState(percentage: Int, isCharging: Boolean, hasBattery: Boolean) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt index 72943f33e0..e9a0d5a75c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -13,11 +16,23 @@ import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.DocumentsTree import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.Log +import org.yuzu.yuzu_emu.utils.PowerStateUpdater fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir class YuzuApplication : Application() { private fun createNotificationChannels() { + val name: CharSequence = getString(R.string.app_notification_channel_name) + val description = getString(R.string.app_notification_channel_description) + val foregroundService = NotificationChannel( + getString(R.string.app_notification_channel_id), + name, + NotificationManager.IMPORTANCE_DEFAULT + ) + foregroundService.description = description + foregroundService.setSound(null, null) + foregroundService.vibrationPattern = null + val noticeChannel = NotificationChannel( getString(R.string.notice_notification_channel_id), getString(R.string.notice_notification_channel_name), @@ -30,6 +45,7 @@ class YuzuApplication : Application() { // or other notification behaviors after this val notificationManager = getSystemService(NotificationManager::class.java) notificationManager.createNotificationChannel(noticeChannel) + notificationManager.createNotificationChannel(foregroundService) } override fun onCreate() { @@ -40,6 +56,7 @@ class YuzuApplication : Application() { GpuDriverHelper.initializeDriverParameters() NativeInput.reloadInputDevices() NativeLibrary.logDeviceInfo() + PowerStateUpdater.start() Log.logDeviceInfo() createNotificationChannels() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index 496fc99e4c..da40453497 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -1,13 +1,13 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later package org.yuzu.yuzu_emu.activities import android.annotation.SuppressLint +import android.app.Activity import android.app.PendingIntent import android.app.PictureInPictureParams import android.app.RemoteAction @@ -60,6 +60,7 @@ import org.yuzu.yuzu_emu.utils.ParamPackage import org.yuzu.yuzu_emu.utils.ThemeHelper import java.text.NumberFormat import kotlin.math.roundToInt +import org.yuzu.yuzu_emu.utils.ForegroundService class EmulationActivity : AppCompatActivity(), SensorEventListener { private lateinit var binding: ActivityEmulationBinding @@ -67,6 +68,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { var isActivityRecreated = false private lateinit var nfcReader: NfcReader + private var touchDownTime: Long = 0 + private val maxTapDuration = 500L + private val gyro = FloatArray(3) private val accel = FloatArray(3) private var motionTimestamp: Long = 0 @@ -79,6 +83,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { private val emulationViewModel: EmulationViewModel by viewModels() + private var foregroundService: Intent? = null + override fun onCreate(savedInstanceState: Bundle?) { Log.gameLaunched = true ThemeHelper.setTheme(this) @@ -129,6 +135,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { nfcReader = NfcReader(this) nfcReader.initialize() + foregroundService = Intent(this, ForegroundService::class.java) + startForegroundService(foregroundService) + val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) { if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) { @@ -190,6 +199,12 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { stopMotionSensorListener() } + override fun onDestroy() { + super.onDestroy() + stopForegroundService(this) + + } + override fun onUserLeaveHint() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { if (BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && !isInPictureInPictureMode) { @@ -203,6 +218,12 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) + + // Reset navigation graph with new intent data to recreate EmulationFragment + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment + navHostFragment.navController.setGraph(R.navigation.emulation_navigation, intent.extras) + nfcReader.onNewIntent(intent) InputHandler.updateControllerData() } @@ -420,7 +441,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { NetPlayManager.addNetPlayMessage(type, msg) } - private var pictureInPictureReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent) { if (intent.action == actionPlay) { @@ -472,6 +492,38 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { } } + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment_container) as? NavHostFragment + val emulationFragment = navHostFragment?.childFragmentManager?.fragments?.firstOrNull() as? org.yuzu.yuzu_emu.fragments.EmulationFragment + + emulationFragment?.let { fragment -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + touchDownTime = System.currentTimeMillis() + // show overlay immediately on touch and cancel timer + if (!emulationViewModel.drawerOpen.value) { + fragment.handler.removeCallbacksAndMessages(null) + fragment.showOverlay() + } + } + MotionEvent.ACTION_UP -> { + if (!emulationViewModel.drawerOpen.value) { + val touchDuration = System.currentTimeMillis() - touchDownTime + + if (touchDuration <= maxTapDuration) { + fragment.handleScreenTap(false) + } else { + // just start the auto-hide timer without toggling visibility + fragment.handleScreenTap(true) + } + } + } + } + } + + return super.dispatchTouchEvent(event) + } + fun onEmulationStarted() { emulationViewModel.setEmulationStarted(true) } @@ -507,6 +559,12 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { companion object { const val EXTRA_SELECTED_GAME = "SelectedGame" + fun stopForegroundService(activity: Activity) { + val startIntent = Intent(activity, ForegroundService::class.java) + startIntent.action = ForegroundService.ACTION_STOP + activity.startForegroundService(startIntent) + } + fun launch(activity: AppCompatActivity, game: Game) { val launcher = Intent(activity, EmulationActivity::class.java) launcher.putExtra(EXTRA_SELECTED_GAME, game) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt index 671e897448..9ea2a9ee17 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt @@ -1,9 +1,10 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.adapters -import android.net.Uri +import android.content.DialogInterface +import android.text.Html import android.view.LayoutInflater import android.view.ViewGroup import android.widget.ImageView @@ -25,18 +26,28 @@ import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.databinding.CardGameListBinding import org.yuzu.yuzu_emu.databinding.CardGameGridBinding +import org.yuzu.yuzu_emu.databinding.CardGameCarouselBinding import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.utils.GameIconUtils import org.yuzu.yuzu_emu.utils.ViewUtils.marquee import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder +import androidx.core.net.toUri +import androidx.core.content.edit +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.databinding.CardGameGridCompactBinding +import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting +import org.yuzu.yuzu_emu.features.settings.model.Settings class GameAdapter(private val activity: AppCompatActivity) : AbstractDiffAdapter(exact = false) { companion object { const val VIEW_TYPE_GRID = 0 - const val VIEW_TYPE_LIST = 1 + const val VIEW_TYPE_GRID_COMPACT = 1 + const val VIEW_TYPE_LIST = 2 + const val VIEW_TYPE_CAROUSEL = 3 } private var viewType = 0 @@ -46,29 +57,102 @@ class GameAdapter(private val activity: AppCompatActivity) : notifyDataSetChanged() } + public var cardSize: Int = 0 + private set + + fun setCardSize(size: Int) { + if (cardSize != size && size > 0) { + cardSize = size + notifyDataSetChanged() + } + } + override fun getItemViewType(position: Int): Int = viewType + override fun onBindViewHolder(holder: GameViewHolder, position: Int) { + super.onBindViewHolder(holder, position) + when (getItemViewType(position)) { + VIEW_TYPE_LIST -> { + val listBinding = holder.binding as CardGameListBinding + listBinding.cardGameList.scaleX = 1f + listBinding.cardGameList.scaleY = 1f + listBinding.cardGameList.alpha = 1f + // Reset layout params to XML defaults + listBinding.root.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + listBinding.root.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + } + VIEW_TYPE_GRID -> { + val gridBinding = holder.binding as CardGameGridBinding + gridBinding.cardGameGrid.scaleX = 1f + gridBinding.cardGameGrid.scaleY = 1f + gridBinding.cardGameGrid.alpha = 1f + // Reset layout params to XML defaults + gridBinding.root.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + gridBinding.root.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + } + + VIEW_TYPE_GRID_COMPACT -> { + val gridCompactBinding = holder.binding as CardGameGridCompactBinding + gridCompactBinding.cardGameGridCompact.scaleX = 1f + gridCompactBinding.cardGameGridCompact.scaleY = 1f + gridCompactBinding.cardGameGridCompact.alpha = 1f + // Reset layout params to XML defaults (same as normal grid) + gridCompactBinding.root.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + gridCompactBinding.root.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + } + + VIEW_TYPE_CAROUSEL -> { + val carouselBinding = holder.binding as CardGameCarouselBinding + // soothens transient flickering + carouselBinding.cardGameCarousel.scaleY = 0f + carouselBinding.cardGameCarousel.alpha = 0f + } + } + } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { val binding = when (viewType) { - VIEW_TYPE_LIST -> CardGameListBinding.inflate(LayoutInflater.from(parent.context), parent, false) - VIEW_TYPE_GRID -> CardGameGridBinding.inflate(LayoutInflater.from(parent.context), parent, false) + VIEW_TYPE_LIST -> CardGameListBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + + VIEW_TYPE_GRID -> CardGameGridBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + + VIEW_TYPE_GRID_COMPACT -> CardGameGridCompactBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + + VIEW_TYPE_CAROUSEL -> CardGameCarouselBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + else -> throw IllegalArgumentException("Invalid view type") } return GameViewHolder(binding, viewType) } inner class GameViewHolder( - private val binding: ViewBinding, + internal val binding: ViewBinding, private val viewType: Int ) : AbstractViewHolder(binding) { - override fun bind(model: Game) { when (viewType) { VIEW_TYPE_LIST -> bindListView(model) VIEW_TYPE_GRID -> bindGridView(model) + VIEW_TYPE_CAROUSEL -> bindCarouselView(model) + VIEW_TYPE_GRID_COMPACT -> bindGridCompactView(model) } } @@ -84,6 +168,10 @@ class GameAdapter(private val activity: AppCompatActivity) : listBinding.textGameTitle.marquee() listBinding.cardGameList.setOnClickListener { onClick(model) } listBinding.cardGameList.setOnLongClickListener { onLongClick(model) } + + // Reset layout params to XML defaults + listBinding.root.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + listBinding.root.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT } private fun bindGridView(model: Game) { @@ -97,13 +185,53 @@ class GameAdapter(private val activity: AppCompatActivity) : gridBinding.textGameTitle.marquee() gridBinding.cardGameGrid.setOnClickListener { onClick(model) } gridBinding.cardGameGrid.setOnLongClickListener { onLongClick(model) } + + // Reset layout params to XML defaults + gridBinding.root.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + gridBinding.root.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + } + + private fun bindGridCompactView(model: Game) { + val gridCompactBinding = binding as CardGameGridCompactBinding + + gridCompactBinding.imageGameScreenCompact.scaleType = ImageView.ScaleType.CENTER_CROP + GameIconUtils.loadGameIcon(model, gridCompactBinding.imageGameScreenCompact) + + gridCompactBinding.textGameTitleCompact.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ") + + gridCompactBinding.textGameTitleCompact.marquee() + gridCompactBinding.cardGameGridCompact.setOnClickListener { onClick(model) } + gridCompactBinding.cardGameGridCompact.setOnLongClickListener { onLongClick(model) } + + // Reset layout params to XML defaults (same as normal grid) + gridCompactBinding.root.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + gridCompactBinding.root.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + } + + private fun bindCarouselView(model: Game) { + val carouselBinding = binding as CardGameCarouselBinding + + carouselBinding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP + GameIconUtils.loadGameIcon(model, carouselBinding.imageGameScreen) + + carouselBinding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ") + carouselBinding.textGameTitle.marquee() + carouselBinding.cardGameCarousel.setOnClickListener { onClick(model) } + carouselBinding.cardGameCarousel.setOnLongClickListener { onLongClick(model) } + + carouselBinding.imageGameScreen.contentDescription = + binding.root.context.getString(R.string.game_image_desc, model.title) + + // Ensure zero-heighted-full-width cards for carousel + carouselBinding.root.layoutParams.width = cardSize } fun onClick(game: Game) { val gameExists = DocumentFile.fromSingleUri( YuzuApplication.appContext, - Uri.parse(game.path) + game.path.toUri() )?.exists() == true + if (!gameExists) { Toast.makeText( YuzuApplication.appContext, @@ -115,29 +243,52 @@ class GameAdapter(private val activity: AppCompatActivity) : return } - val preferences = - PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) - preferences.edit() - .putLong( - game.keyLastPlayedTime, - System.currentTimeMillis() - ) - .apply() - - activity.lifecycleScope.launch { - withContext(Dispatchers.IO) { - val shortcut = - ShortcutInfoCompat.Builder(YuzuApplication.appContext, game.path) - .setShortLabel(game.title) - .setIcon(GameIconUtils.getShortcutIcon(activity, game)) - .setIntent(game.launchIntent) - .build() - ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut) + val launch: () -> Unit = { + val preferences = + PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + preferences.edit { + putLong( + game.keyLastPlayedTime, + System.currentTimeMillis() + ) } + + activity.lifecycleScope.launch { + withContext(Dispatchers.IO) { + val shortcut = + ShortcutInfoCompat.Builder(YuzuApplication.appContext, game.path) + .setShortLabel(game.title) + .setIcon(GameIconUtils.getShortcutIcon(activity, game)) + .setIntent(game.launchIntent) + .build() + ShortcutManagerCompat.pushDynamicShortcut( + YuzuApplication.appContext, + shortcut + ) + } + } + + val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true) + binding.root.findNavController().navigate(action) } - val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true) - binding.root.findNavController().navigate(action) + if (NativeLibrary.gameRequiresFirmware(game.programId) && !NativeLibrary.isFirmwareAvailable()) { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.loader_requires_firmware) + .setMessage( + Html.fromHtml( + activity.getString(R.string.loader_requires_firmware_description), + Html.FROM_HTML_MODE_LEGACY + ) + ) + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + launch() + } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() + } else { + launch() + } } fun onLongClick(game: Game): Boolean { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt index 5d72efa350..9e1dc7709e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -6,10 +9,8 @@ package org.yuzu.yuzu_emu.adapters import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.lifecycle.LifecycleOwner -import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.model.HomeSetting diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/ChatDialog.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/ChatDialog.kt index 9f7e80e7d0..5d6679bd28 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/ChatDialog.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/ChatDialog.kt @@ -1,9 +1,9 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later - package org.yuzu.yuzu_emu.dialogs +import android.annotation.SuppressLint import android.content.Context import android.content.res.Configuration import android.os.Bundle @@ -18,6 +18,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.DialogChatBinding import org.yuzu.yuzu_emu.databinding.ItemChatMessageBinding +import org.yuzu.yuzu_emu.features.settings.model.StringSetting import org.yuzu.yuzu_emu.network.NetPlayManager import java.text.SimpleDateFormat import java.util.* @@ -27,14 +28,17 @@ class ChatMessage( val username: String, // Username is the community/forum username val message: String, val timestamp: String = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date()) -) { -} +) class ChatDialog(context: Context) : BottomSheetDialog(context) { private lateinit var binding: DialogChatBinding private lateinit var chatAdapter: ChatAdapter private val handler = Handler(Looper.getMainLooper()) + // TODO(alekpop, crueter): Top drawer for message notifications, perhaps use system notifs? + // TODO(alekpop, crueter): Context menu actions for chat users + // TODO(alekpop, crueter): Block users (depends on the above) + @SuppressLint("NotifyDataSetChanged") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DialogChatBinding.inflate(LayoutInflater.from(context)) @@ -45,8 +49,8 @@ class ChatDialog(context: Context) : BottomSheetDialog(context) { behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BottomSheetBehavior.STATE_EXPANDED - behavior.skipCollapsed = context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - + behavior.skipCollapsed = + context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE handler.post { chatAdapter.notifyDataSetChanged() @@ -76,8 +80,9 @@ class ChatDialog(context: Context) : BottomSheetDialog(context) { super.dismiss() } + @SuppressLint("NotifyDataSetChanged") private fun sendMessage(message: String) { - val username = NetPlayManager.getUsername(context) + val username = StringSetting.WEB_USERNAME.getString() NetPlayManager.netPlaySendMessage(message) val chatMessage = ChatMessage( @@ -128,10 +133,12 @@ class ChatAdapter(private val messages: List) : fun bind(message: ChatMessage) { binding.usernameText.text = message.nickname binding.messageText.text = message.message - binding.userIcon.setImageResource(when (message.nickname) { - "System" -> R.drawable.ic_system - else -> R.drawable.ic_user - }) + binding.userIcon.setImageResource( + when (message.nickname) { + "System" -> R.drawable.ic_system + else -> R.drawable.ic_user + } + ) } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/LobbyBrowser.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/LobbyBrowser.kt new file mode 100644 index 0000000000..57fd551e02 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/LobbyBrowser.kt @@ -0,0 +1,252 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.dialogs + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Configuration +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.core.content.getSystemService +import androidx.core.widget.doOnTextChanged +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputEditText +import info.debatty.java.stringsimilarity.Jaccard +import info.debatty.java.stringsimilarity.JaroWinkler +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.DialogLobbyBrowserBinding +import org.yuzu.yuzu_emu.databinding.ItemLobbyRoomBinding +import org.yuzu.yuzu_emu.features.settings.model.StringSetting +import org.yuzu.yuzu_emu.network.NetPlayManager +import java.util.Locale + +class LobbyBrowser(context: Context) : BottomSheetDialog(context) { + private lateinit var binding: DialogLobbyBrowserBinding + private lateinit var adapter: LobbyRoomAdapter + private val handler = Handler(Looper.getMainLooper()) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + behavior.state = BottomSheetBehavior.STATE_EXPANDED + behavior.skipCollapsed = + context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + + binding = DialogLobbyBrowserBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.emptyRefreshButton.setOnClickListener { + binding.progressBar.visibility = View.VISIBLE + refreshRoomList() + } + + setupRecyclerView() + setupRefreshButton() + refreshRoomList() + setupSearchBar() + } + + private fun setupRecyclerView() { + adapter = LobbyRoomAdapter { room -> handleRoomSelection(room) } + + binding.roomList.apply { + layoutManager = LinearLayoutManager(context) + adapter = this@LobbyBrowser.adapter + addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + } + } + + private fun setupRefreshButton() { + binding.refreshButton.setOnClickListener { + binding.refreshButton.isEnabled = false + binding.progressBar.visibility = View.VISIBLE + refreshRoomList() + } + } + + private fun setupSearchBar() { + binding.chipHideFull.setOnCheckedChangeListener { _, _ -> adapter.filterAndSearch() } + binding.chipHideEmpty.setOnCheckedChangeListener { _, _ -> adapter.filterAndSearch() } + + binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int -> + if (text.toString().isNotEmpty()) { + binding.clearButton.visibility = View.VISIBLE + } else { + binding.clearButton.visibility = View.INVISIBLE + } + } + + binding.searchText.setOnEditorActionListener { v, action, _ -> + if (action == EditorInfo.IME_ACTION_DONE) { + v.clearFocus() + + val imm = context.getSystemService() + imm?.hideSoftInputFromWindow(v.windowToken, 0) + + adapter.filterAndSearch() + true + } else { + false + } + } + + binding.btnSubmit.setOnClickListener { adapter.filterAndSearch() } + + binding.clearButton.setOnClickListener { + binding.searchText.setText("") + adapter.updateRooms(NetPlayManager.getPublicRooms()) + } + } + + private fun refreshRoomList() { + NetPlayManager.refreshRoomListAsync { rooms -> + binding.emptyView.visibility = if (rooms.isEmpty()) View.VISIBLE else View.GONE + binding.roomList.visibility = if (rooms.isEmpty()) View.GONE else View.VISIBLE + binding.appbar.visibility = if (rooms.isEmpty()) View.GONE else View.VISIBLE + adapter.updateRooms(rooms) + adapter.filterAndSearch() + binding.refreshButton.isEnabled = true + binding.progressBar.visibility = View.GONE + } + } + + private fun handleRoomSelection(room: NetPlayManager.RoomInfo) { + if (room.hasPassword) { + showPasswordDialog(room) + } else { + joinRoom(room, "") + } + } + + private fun showPasswordDialog(room: NetPlayManager.RoomInfo) { + val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_password_input, null) + val passwordInput = dialogView.findViewById(R.id.password_input) + + MaterialAlertDialogBuilder(context) + .setTitle(context.getString(R.string.multiplayer_password_required)) + .setView(dialogView) + .setPositiveButton(R.string.multiplayer_join_room) { _, _ -> + joinRoom(room, passwordInput.text.toString()) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun joinRoom(room: NetPlayManager.RoomInfo, password: String) { + val username = StringSetting.WEB_USERNAME.getString() + + Thread { + val result = NetPlayManager.netPlayJoinRoom(room.ip, room.port, username, password) + + handler.post { + if (result == 0) { + dismiss() + NetPlayDialog(context).show() + } + } + }.start() + } + + inner class LobbyRoomAdapter(private val onRoomSelected: (NetPlayManager.RoomInfo) -> Unit) : + RecyclerView.Adapter() { + + private val rooms = mutableListOf() + + inner class RoomViewHolder(private val binding: ItemLobbyRoomBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(room: NetPlayManager.RoomInfo) { + binding.roomName.text = room.name + binding.roomOwner.text = room.owner + binding.playerCount.text = context.getString( + R.string.multiplayer_player_count, + room.members.size, + room.maxPlayers + ) + + binding.lockIcon.visibility = if (room.hasPassword) View.VISIBLE else View.GONE + + if (room.preferredGameName.isNotEmpty() && room.preferredGameId != 0L) { + binding.gameName.text = room.preferredGameName + } else { + binding.gameName.text = context.getString(R.string.multiplayer_no_game_info) + } + + itemView.setOnClickListener { onRoomSelected(room) } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoomViewHolder { + val binding = ItemLobbyRoomBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return RoomViewHolder(binding) + } + + override fun onBindViewHolder(holder: RoomViewHolder, position: Int) { + holder.bind(rooms[position]) + } + + override fun getItemCount() = rooms.size + + @SuppressLint("NotifyDataSetChanged") + fun updateRooms(newRooms: List) { + rooms.clear() + rooms.addAll(newRooms) + notifyDataSetChanged() + } + + fun filterAndSearch() { + if (binding.searchText.text.toString().isEmpty() && + !binding.chipHideFull.isChecked && !binding.chipHideEmpty.isChecked + ) { + adapter.updateRooms(NetPlayManager.getPublicRooms()) + return + } + + val baseList = NetPlayManager.getPublicRooms() + val filteredList = baseList.filter { room -> + (!binding.chipHideFull.isChecked || room.members.size < room.maxPlayers) && + (!binding.chipHideEmpty.isChecked || room.members.isNotEmpty()) + } + + if (binding.searchText.text.toString().isEmpty() && + (binding.chipHideFull.isChecked || binding.chipHideEmpty.isChecked) + ) { + adapter.updateRooms(filteredList) + return + } + + val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault()) + val searchAlgorithm = if (searchTerm.length > 1) Jaccard(2) else JaroWinkler() + val sortedList: List = filteredList.mapNotNull { room -> + val roomName = room.name.lowercase(Locale.getDefault()) + + val score = searchAlgorithm.similarity(roomName, searchTerm) + if (score > 0.03) { + ScoreItem(score, room) + } else { + null + } + }.sortedByDescending { it -> + it.score + }.map { it.item } + adapter.updateRooms(sortedList) + } + } + + private inner class ScoreItem(val score: Double, val item: NetPlayManager.RoomInfo) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/NetPlayDialog.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/NetPlayDialog.kt index 494b8524ff..73452b4b69 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/NetPlayDialog.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/NetPlayDialog.kt @@ -1,21 +1,21 @@ -// Copyright 2024 Mandarine Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.dialogs +import android.annotation.SuppressLint import android.content.Context -import org.yuzu.yuzu_emu.R import android.content.res.Configuration import android.os.Bundle import android.os.Handler import android.os.Looper +import android.text.Editable +import android.text.TextWatcher import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.Button import android.widget.PopupMenu import android.widget.Toast import androidx.recyclerview.widget.LinearLayoutManager @@ -23,6 +23,8 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputLayout +import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.databinding.DialogMultiplayerConnectBinding import org.yuzu.yuzu_emu.databinding.DialogMultiplayerLobbyBinding @@ -30,22 +32,30 @@ import org.yuzu.yuzu_emu.databinding.DialogMultiplayerRoomBinding import org.yuzu.yuzu_emu.databinding.ItemBanListBinding import org.yuzu.yuzu_emu.databinding.ItemButtonNetplayBinding import org.yuzu.yuzu_emu.databinding.ItemTextNetplayBinding -import org.yuzu.yuzu_emu.utils.CompatUtils +import org.yuzu.yuzu_emu.features.settings.model.StringSetting +import org.yuzu.yuzu_emu.network.NetDataValidators import org.yuzu.yuzu_emu.network.NetPlayManager +import org.yuzu.yuzu_emu.utils.CompatUtils +import org.yuzu.yuzu_emu.utils.GameHelper class NetPlayDialog(context: Context) : BottomSheetDialog(context) { private lateinit var adapter: NetPlayAdapter + private val gameNameList: MutableList> = mutableListOf() + private val gameIdList: MutableList> = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BottomSheetBehavior.STATE_EXPANDED - behavior.skipCollapsed = context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + behavior.skipCollapsed = + context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE when { - NetPlayManager.netPlayIsJoined() -> DialogMultiplayerLobbyBinding.inflate(layoutInflater) + NetPlayManager.netPlayIsJoined() -> DialogMultiplayerLobbyBinding.inflate( + layoutInflater + ) .apply { setContentView(root) adapter = NetPlayAdapter() @@ -62,15 +72,27 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { refreshAdapterItems() - btnModeration.visibility = if (NetPlayManager.netPlayIsModerator()) View.VISIBLE else View.GONE + btnModeration.visibility = + if (NetPlayManager.netPlayIsModerator()) View.VISIBLE else View.GONE btnModeration.setOnClickListener { showModerationDialog() } - } + else -> { DialogMultiplayerConnectBinding.inflate(layoutInflater).apply { setContentView(root) + for (game in GameHelper.cachedGameList) { + val gameName = game.title + if (gameNameList.none { it[0] == gameName }) { + gameNameList.add(arrayOf(gameName)) + } + + val gameId = game.programId.toLong() + if (gameIdList.none { it[0] == gameId }) { + gameIdList.add(arrayOf(gameId)) + } + } btnCreate.setOnClickListener { showNetPlayInputDialog(true) dismiss() @@ -79,6 +101,18 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { showNetPlayInputDialog(false) dismiss() } + btnLobbyBrowser.setOnClickListener { + if (!NetDataValidators.username()) { + Toast.makeText( + context, + R.string.multiplayer_nickname_invalid, + Toast.LENGTH_LONG + ).show() + } else { + LobbyBrowser(context).show() + dismiss() + } + } } } } @@ -101,17 +135,22 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { } } + // TODO(alekpop, crueter): Disable context menu for self and if not moderator inner class NetPlayAdapter : RecyclerView.Adapter() { val netPlayItems = mutableListOf() - abstract inner class NetPlayViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { + abstract inner class NetPlayViewHolder(itemView: View) : + RecyclerView.ViewHolder(itemView), + View.OnClickListener { init { itemView.setOnClickListener(this) } + abstract fun bind(item: NetPlayItems) } - inner class TextViewHolder(private val binding: ItemTextNetplayBinding) : NetPlayViewHolder(binding.root) { + inner class TextViewHolder(private val binding: ItemTextNetplayBinding) : + NetPlayViewHolder(binding.root) { private lateinit var netPlayItem: NetPlayItems override fun onClick(clicked: View) {} @@ -128,12 +167,15 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { visibility = if (iconRes != 0) { setImageResource(iconRes) View.VISIBLE - } else View.GONE + } else { + View.GONE + } } } } - inner class ButtonViewHolder(private val binding: ItemButtonNetplayBinding) : NetPlayViewHolder(binding.root) { + inner class ButtonViewHolder(private val binding: ItemButtonNetplayBinding) : + NetPlayViewHolder(binding.root) { private lateinit var netPlayItems: NetPlayItems private val isModerator = NetPlayManager.netPlayIsModerator() @@ -146,14 +188,13 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { override fun onClick(clicked: View) {} - private fun showPopupMenu(view: View) { PopupMenu(view.context, view).apply { menuInflater.inflate(R.menu.menu_netplay_member, menu) menu.findItem(R.id.action_kick).isEnabled = isModerator && - netPlayItems.name != NetPlayManager.getUsername(context) + netPlayItems.name != StringSetting.WEB_USERNAME.getString() menu.findItem(R.id.action_ban).isEnabled = isModerator && - netPlayItems.name != NetPlayManager.getUsername(context) + netPlayItems.name != StringSetting.WEB_USERNAME.getString() setOnMenuItemClickListener { item -> if (item.itemId == R.id.action_kick) { NetPlayManager.netPlayKickUser(netPlayItems.name) @@ -161,7 +202,9 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { } else if (item.itemId == R.id.action_ban) { NetPlayManager.netPlayBanUser(netPlayItems.name) true - } else false + } else { + false + } } show() } @@ -177,11 +220,35 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { val infos = NetPlayManager.netPlayRoomInfo() if (infos.isNotEmpty()) { val roomInfo = infos[0].split("|") - netPlayItems.add(NetPlayItems(NetPlayItems.MULTIPLAYER_ROOM_TEXT, roomInfo[0], NetPlayItems.TYPE_TEXT)) - netPlayItems.add(NetPlayItems(NetPlayItems.MULTIPLAYER_ROOM_COUNT, "${infos.size - 1}/${roomInfo[1]}", NetPlayItems.TYPE_TEXT)) - netPlayItems.add(NetPlayItems(NetPlayItems.MULTIPLAYER_SEPARATOR, "", NetPlayItems.TYPE_SEPARATOR)) + netPlayItems.add( + NetPlayItems( + NetPlayItems.MULTIPLAYER_ROOM_TEXT, + roomInfo[0], + NetPlayItems.TYPE_TEXT + ) + ) + netPlayItems.add( + NetPlayItems( + NetPlayItems.MULTIPLAYER_ROOM_COUNT, + "${infos.size - 1}/${roomInfo[1]}", + NetPlayItems.TYPE_TEXT + ) + ) + netPlayItems.add( + NetPlayItems( + NetPlayItems.MULTIPLAYER_SEPARATOR, + "", + NetPlayItems.TYPE_SEPARATOR + ) + ) for (i in 1 until infos.size) { - netPlayItems.add(NetPlayItems(NetPlayItems.MULTIPLAYER_ROOM_MEMBER, infos[i], NetPlayItems.TYPE_BUTTON)) + netPlayItems.add( + NetPlayItems( + NetPlayItems.MULTIPLAYER_ROOM_MEMBER, + infos[i], + NetPlayItems.TYPE_BUTTON + ) + ) } } } @@ -191,12 +258,33 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NetPlayViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { - NetPlayItems.TYPE_TEXT -> TextViewHolder(ItemTextNetplayBinding.inflate(inflater, parent, false)) - NetPlayItems.TYPE_BUTTON -> ButtonViewHolder(ItemButtonNetplayBinding.inflate(inflater, parent, false)) - NetPlayItems.TYPE_SEPARATOR -> object : NetPlayViewHolder(inflater.inflate(R.layout.item_separator_netplay, parent, false)) { + NetPlayItems.TYPE_TEXT -> TextViewHolder( + ItemTextNetplayBinding.inflate( + inflater, + parent, + false + ) + ) + + NetPlayItems.TYPE_BUTTON -> ButtonViewHolder( + ItemButtonNetplayBinding.inflate( + inflater, + parent, + false + ) + ) + + NetPlayItems.TYPE_SEPARATOR -> object : NetPlayViewHolder( + inflater.inflate( + R.layout.item_separator_netplay, + parent, + false + ) + ) { override fun bind(item: NetPlayItems) {} override fun onClick(clicked: View) {} } + else -> throw IllegalStateException("Unsupported view type") } } @@ -208,10 +296,11 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { override fun getItemCount() = netPlayItems.size } + @SuppressLint("NotifyDataSetChanged") fun refreshAdapterItems() { val handler = Handler(Looper.getMainLooper()) - NetPlayManager.setOnAdapterRefreshListener() { type, msg -> + NetPlayManager.setOnAdapterRefreshListener() { _, _ -> handler.post { adapter.netPlayItems.clear() adapter.loadMultiplayerMenu() @@ -220,90 +309,272 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { } } + abstract class TextValidatorWatcher( + private val btnConfirm: Button, + private val layout: TextInputLayout, + private val errorMessage: String + ) : TextWatcher { + companion object { + val validStates: HashMap = hashMapOf() + } + + abstract fun validate(s: String): Boolean + + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + s: CharSequence?, + start: Int, + before: Int, + count: Int + ) { + } + + override fun afterTextChanged(s: Editable?) { + val input = s.toString() + val isValid = validate(input) + layout.isErrorEnabled = !isValid + layout.error = if (isValid) null else errorMessage + + validStates[layout] = isValid + btnConfirm.isEnabled = !validStates.containsValue(false) + } + } + + // TODO(alekpop, crueter): Properly handle getting banned (both during and in future connects) private fun showNetPlayInputDialog(isCreateRoom: Boolean) { + TextValidatorWatcher.validStates.clear() val activity = CompatUtils.findActivity(context) val dialog = BottomSheetDialog(activity) dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED - dialog.behavior.skipCollapsed = context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - + dialog.behavior.skipCollapsed = + context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE val binding = DialogMultiplayerRoomBinding.inflate(LayoutInflater.from(activity)) dialog.setContentView(binding.root) + val visibilityList: List = listOf( + context.getString(R.string.multiplayer_public_visibility), + context.getString(R.string.multiplayer_unlisted_visibility) + ) + binding.textTitle.text = activity.getString( - if (isCreateRoom) R.string.multiplayer_create_room - else R.string.multiplayer_join_room + if (isCreateRoom) { + R.string.multiplayer_create_room + } else { + R.string.multiplayer_join_room + } ) - binding.ipAddress.setText( - if (isCreateRoom) NetPlayManager.getIpAddressByWifi(activity) - else NetPlayManager.getRoomAddress(activity) - ) - binding.ipPort.setText(NetPlayManager.getRoomPort(activity)) - binding.username.setText(NetPlayManager.getUsername(activity)) - - binding.roomName.visibility = if (isCreateRoom) View.VISIBLE else View.GONE - binding.maxPlayersContainer.visibility = if (isCreateRoom) View.VISIBLE else View.GONE - binding.maxPlayersLabel.text = context.getString(R.string.multiplayer_max_players_value, binding.maxPlayers.value.toInt()) - - binding.maxPlayers.addOnChangeListener { _, value, _ -> - binding.maxPlayersLabel.text = context.getString(R.string.multiplayer_max_players_value, value.toInt()) + // setup listeners etc + val roomNameWatcher = object : TextValidatorWatcher( + binding.btnConfirm, + binding.layoutRoomName, + context.getString( + R.string.multiplayer_room_name_error + ) + ) { + override fun validate(s: String): Boolean { + return NetDataValidators.roomName(s) + } } + val preferredWatcher = object : TextValidatorWatcher( + binding.btnConfirm, + binding.preferredGameName, + context.getString(R.string.multiplayer_required) + ) { + override fun validate(s: String): Boolean { + return NetDataValidators.notEmpty(s) + } + } + + val visibilityWatcher = object : TextValidatorWatcher( + binding.btnConfirm, + binding.lobbyVisibility, + context.getString(R.string.multiplayer_token_required) + ) { + override fun validate(s: String): Boolean { + return NetDataValidators.roomVisibility(s, context) + } + } + + val ipWatcher = object : TextValidatorWatcher( + binding.btnConfirm, + binding.layoutIpAddress, + context.getString(R.string.multiplayer_ip_error) + ) { + override fun validate(s: String): Boolean { + return NetDataValidators.ipAddress(s) + } + } + + val usernameWatcher = object : TextValidatorWatcher( + binding.btnConfirm, + binding.layoutUsername, + context.getString(R.string.multiplayer_username_error) + ) { + override fun validate(s: String): Boolean { + return NetDataValidators.username(s) + } + } + + val portWatcher = object : TextValidatorWatcher( + binding.btnConfirm, + binding.layoutIpPort, + context.getString(R.string.multiplayer_port_error) + ) { + override fun validate(s: String): Boolean { + return NetDataValidators.port(s) + } + } + + if (isCreateRoom) { + binding.roomName.addTextChangedListener(roomNameWatcher) + binding.dropdownPreferredGameName.addTextChangedListener(preferredWatcher) + binding.dropdownLobbyVisibility.addTextChangedListener(visibilityWatcher) + + binding.dropdownPreferredGameName.apply { + setAdapter( + ArrayAdapter( + activity, + R.layout.dropdown_item, + gameNameList.map { it[0] } + ) + ) + } + + binding.dropdownLobbyVisibility.setText( + context.getString(R.string.multiplayer_unlisted_visibility) + ) + + binding.dropdownLobbyVisibility.apply { + setAdapter( + ArrayAdapter( + activity, + R.layout.dropdown_item, + visibilityList + ) + ) + } + } + + binding.ipAddress.addTextChangedListener(ipWatcher) + binding.ipPort.addTextChangedListener(portWatcher) + binding.username.addTextChangedListener(usernameWatcher) + + binding.ipAddress.setText(NetPlayManager.getRoomAddress(activity)) + binding.ipPort.setText(NetPlayManager.getRoomPort(activity)) + binding.username.setText(StringSetting.WEB_USERNAME.getString()) + + // manually trigger text listeners + if (isCreateRoom) { + roomNameWatcher.afterTextChanged(binding.roomName.text) + preferredWatcher.afterTextChanged(binding.dropdownPreferredGameName.text) + + // It's not needed here, the watcher is called by the initial set method + // visibilityWatcher.afterTextChanged(binding.dropdownLobbyVisibility.text) + } + + ipWatcher.afterTextChanged(binding.ipAddress.text) + portWatcher.afterTextChanged(binding.ipPort.text) + usernameWatcher.afterTextChanged(binding.username.text) + + binding.preferredGameName.visibility = if (isCreateRoom) View.VISIBLE else View.GONE + binding.lobbyVisibility.visibility = if (isCreateRoom) View.VISIBLE else View.GONE + binding.roomName.visibility = if (isCreateRoom) View.VISIBLE else View.GONE + binding.maxPlayersContainer.visibility = if (isCreateRoom) View.VISIBLE else View.GONE + + binding.maxPlayersLabel.text = context.getString( + R.string.multiplayer_max_players_value, + binding.maxPlayers.value.toInt() + ) + + binding.maxPlayers.addOnChangeListener { _, value, _ -> + binding.maxPlayersLabel.text = + context.getString(R.string.multiplayer_max_players_value, value.toInt()) + } + + // TODO(alekpop, crueter): Room descriptions + // TODO(alekpop, crueter): Preview preferred games binding.btnConfirm.setOnClickListener { binding.btnConfirm.isEnabled = false - binding.btnConfirm.text = activity.getString(R.string.disabled_button_text) + binding.btnConfirm.text = + activity.getString( + if (isCreateRoom) { + R.string.multiplayer_creating + } else { + R.string.multiplayer_joining + } + ) + // We don't need to worry about validation because it's already been done. val ipAddress = binding.ipAddress.text.toString() val username = binding.username.text.toString() val portStr = binding.ipPort.text.toString() val password = binding.password.text.toString() - val port = portStr.toIntOrNull() ?: run { - Toast.makeText(activity, R.string.multiplayer_port_invalid, Toast.LENGTH_LONG).show() - binding.btnConfirm.isEnabled = true - binding.btnConfirm.text = activity.getString(R.string.original_button_text) - return@setOnClickListener - } + val port = portStr.toInt() val roomName = binding.roomName.text.toString() val maxPlayers = binding.maxPlayers.value.toInt() - if (isCreateRoom && (roomName.length !in 3..20)) { - Toast.makeText(activity, R.string.multiplayer_room_name_invalid, Toast.LENGTH_LONG).show() - binding.btnConfirm.isEnabled = true - binding.btnConfirm.text = activity.getString(R.string.original_button_text) - return@setOnClickListener - } + val preferredGameName = binding.dropdownPreferredGameName.text.toString() + val preferredIdx = gameNameList.indexOfFirst { it[0] == preferredGameName } + val preferredGameId = if (preferredIdx == -1) 0 else gameIdList[preferredIdx][0] - if (ipAddress.length < 7 || username.length < 5) { - Toast.makeText(activity, R.string.multiplayer_input_invalid, Toast.LENGTH_LONG).show() - binding.btnConfirm.isEnabled = true - binding.btnConfirm.text = activity.getString(R.string.original_button_text) - } else { - Handler(Looper.getMainLooper()).post { - val result = if (isCreateRoom) { - NetPlayManager.netPlayCreateRoom(ipAddress, port, username, password, roomName, maxPlayers) - } else { - NetPlayManager.netPlayJoinRoom(ipAddress, port, username, password) - } + val visibility = binding.dropdownLobbyVisibility.text.toString() + val isPublic = visibility == context.getString(R.string.multiplayer_public_visibility) - if (result == 0) { - NetPlayManager.setUsername(activity, username) - NetPlayManager.setRoomPort(activity, portStr) - if (!isCreateRoom) NetPlayManager.setRoomAddress(activity, ipAddress) - Toast.makeText( - YuzuApplication.appContext, - if (isCreateRoom) R.string.multiplayer_create_room_success - else R.string.multiplayer_join_room_success, - Toast.LENGTH_LONG - ).show() - dialog.dismiss() - } else { - Toast.makeText(activity, R.string.multiplayer_could_not_connect, Toast.LENGTH_LONG).show() - binding.btnConfirm.isEnabled = true - binding.btnConfirm.text = activity.getString(R.string.original_button_text) - } + Handler(Looper.getMainLooper()).post { + val result = if (isCreateRoom) { + NetPlayManager.netPlayCreateRoom( + ipAddress, + port, + username, + preferredGameName, + preferredGameId, + password, + roomName, + maxPlayers, + isPublic + ) + } else { + NetPlayManager.netPlayJoinRoom(ipAddress, port, username, password) + } + + if (result == 0) { + StringSetting.WEB_USERNAME.setString(username) + NetPlayManager.setRoomPort(activity, portStr) + + if (!isCreateRoom) NetPlayManager.setRoomAddress(activity, ipAddress) + + Toast.makeText( + YuzuApplication.appContext, + if (isCreateRoom) { + R.string.multiplayer_create_room_success + } else { + R.string.multiplayer_join_room_success + }, + Toast.LENGTH_LONG + ).show() + + dialog.dismiss() + } else { + Toast.makeText( + activity, + R.string.multiplayer_could_not_connect, + Toast.LENGTH_LONG + ).show() + + binding.btnConfirm.isEnabled = true + binding.btnConfirm.text = activity.getString(R.string.ok) } } } @@ -362,13 +633,17 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val binding = ItemBanListBinding.inflate( - LayoutInflater.from(parent.context), parent, false) + LayoutInflater.from(parent.context), + parent, + false + ) return ViewHolder(binding) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val isUsername = position < usernameBans.size - val item = if (isUsername) usernameBans[position] else ipBans[position - usernameBans.size] + val item = + if (isUsername) usernameBans[position] else ipBans[position - usernameBans.size] holder.binding.apply { banText.text = item @@ -395,6 +670,5 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { notifyItemRemoved(position) } } - } -} \ No newline at end of file +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt new file mode 100644 index 0000000000..1607371cf5 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.features.fetcher + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.databinding.ItemDriverGroupBinding +import org.yuzu.yuzu_emu.fragments.DriverFetcherFragment.DriverGroup +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentActivity +import androidx.transition.ChangeBounds +import androidx.transition.Fade +import androidx.transition.TransitionManager +import androidx.transition.TransitionSet +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.yuzu.yuzu_emu.model.DriverViewModel + +class DriverGroupAdapter( + private val activity: FragmentActivity, + private val driverViewModel: DriverViewModel +) : RecyclerView.Adapter() { + private var driverGroups: List = emptyList() + private val adapterJobs = mutableMapOf() + + inner class DriverGroupViewHolder( + private val binding: ItemDriverGroupBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(group: DriverGroup) { + binding.textGroupName.text = group.name + + if (binding.recyclerReleases.layoutManager == null) { + binding.recyclerReleases.layoutManager = LinearLayoutManager(activity) + binding.recyclerReleases.addItemDecoration( + SpacingItemDecoration( + (activity.resources.displayMetrics.density * 8).toInt() + ) + ) + } + + val onClick = { + adapterJobs[bindingAdapterPosition]?.cancel() + + TransitionManager.beginDelayedTransition( + binding.root, + TransitionSet().addTransition(Fade()).addTransition(ChangeBounds()) + .setDuration(200) + ) + + val isVisible = binding.recyclerReleases.isVisible + + if (!isVisible && binding.recyclerReleases.adapter == null) { + val job = CoroutineScope(Dispatchers.Main).launch { + // It prevents blocking the ui thread. + var adapter: ReleaseAdapter? + + withContext(Dispatchers.IO) { + adapter = ReleaseAdapter(group.releases, activity, driverViewModel) + } + + binding.recyclerReleases.adapter = adapter + } + + adapterJobs[bindingAdapterPosition] = job + } + + binding.recyclerReleases.visibility = if (isVisible) View.GONE else View.VISIBLE + binding.imageDropdownArrow.rotation = if (isVisible) 0f else 180f + } + + binding.textGroupName.setOnClickListener { onClick() } + binding.imageDropdownArrow.setOnClickListener { onClick() } + } + + fun clear() { + adapterJobs[bindingAdapterPosition]?.cancel() + adapterJobs.remove(bindingAdapterPosition) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverGroupViewHolder { + val binding = ItemDriverGroupBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return DriverGroupViewHolder(binding) + } + + override fun onBindViewHolder(holder: DriverGroupViewHolder, position: Int) { + holder.bind(driverGroups[position]) + } + + override fun getItemCount(): Int = driverGroups.size + + @SuppressLint("NotifyDataSetChanged") + fun updateDriverGroups(newDriverGroups: List) { + driverGroups = newDriverGroups + notifyDataSetChanged() + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt new file mode 100644 index 0000000000..e35ef741fd --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt @@ -0,0 +1,285 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.features.fetcher + +import android.animation.LayoutTransition +import android.content.res.ColorStateList +import android.text.Html +import android.text.Html.FROM_HTML_MODE_COMPACT +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentActivity +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.button.MaterialButton +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.ItemReleaseBinding +import org.yuzu.yuzu_emu.fragments.DriverFetcherFragment.Release +import androidx.core.net.toUri +import androidx.transition.ChangeBounds +import androidx.transition.Fade +import androidx.transition.TransitionManager +import androidx.transition.TransitionSet +import com.google.android.material.color.MaterialColors +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.yuzu.yuzu_emu.databinding.DialogProgressBinding +import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.utils.FileUtil +import org.yuzu.yuzu_emu.utils.GpuDriverHelper +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + +class ReleaseAdapter( + private val releases: List, + private val activity: FragmentActivity, + private val driverViewModel: DriverViewModel +) : RecyclerView.Adapter() { + + inner class ReleaseViewHolder( + private val binding: ItemReleaseBinding + ) : RecyclerView.ViewHolder(binding.root) { + private var isPreview: Boolean = true + private val client = OkHttpClient() + private val markdownParser = Parser.builder().build() + private val htmlRenderer = HtmlRenderer.builder().build() + + init { + binding.root.let { root -> + val layoutTransition = root.layoutTransition ?: LayoutTransition().apply { + enableTransitionType(LayoutTransition.CHANGING) + setDuration(125) + } + root.layoutTransition = layoutTransition + } + + (binding.textBody.parent as ViewGroup).isTransitionGroup = false + binding.containerDownloads.isTransitionGroup = false + } + + fun bind(release: Release) { + binding.textReleaseName.text = release.title + binding.badgeLatest.isVisible = release.latest + + // truncates to 150 chars so it does not take up too much space. + var bodyPreview = release.body.take(150) + bodyPreview = bodyPreview.replace("#", "").removeSurrounding(" ") + + val body = + bodyPreview.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\n", "
") + + binding.textBody.text = Html.fromHtml(body, FROM_HTML_MODE_COMPACT) + + binding.textBody.setOnClickListener { + TransitionManager.beginDelayedTransition( + binding.root, + TransitionSet().addTransition(Fade()).addTransition(ChangeBounds()) + .setDuration(100) + ) + + isPreview = !isPreview + if (isPreview) { + val body = bodyPreview.replace("\\r\\n", "\n").replace("\\n", "\n") + .replace("\n", "
") + + binding.textBody.text = Html.fromHtml(body, FROM_HTML_MODE_COMPACT) + binding.textBody.maxLines = 3 + binding.textBody.ellipsize = TextUtils.TruncateAt.END + } else { + val body = release.body.replace("\\r\\n", "\n\n").replace("\\n", "\n\n") + + try { + val doc = markdownParser.parse(body) + val html = htmlRenderer.render(doc) + binding.textBody.text = Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT) + } catch (e: Exception) { + e.printStackTrace() + binding.textBody.text = body + } + + binding.textBody.maxLines = Integer.MAX_VALUE + binding.textBody.ellipsize = null + } + } + + val onDownloadsClick = { + val isVisible = binding.containerDownloads.isVisible + TransitionManager.beginDelayedTransition( + binding.root, + TransitionSet().addTransition(Fade()).addTransition(ChangeBounds()) + .setDuration(100) + ) + + binding.containerDownloads.isVisible = !isVisible + + binding.imageDownloadsArrow.rotation = if (isVisible) 0f else 180f + binding.buttonToggleDownloads.text = + if (isVisible) { + activity.getString(R.string.show_downloads) + } else { + activity.getString(R.string.hide_downloads) + } + } + + binding.buttonToggleDownloads.setOnClickListener { + onDownloadsClick() + } + + binding.imageDownloadsArrow.setOnClickListener { + onDownloadsClick() + } + + binding.containerDownloads.removeAllViews() + + release.artifacts.forEach { artifact -> + val button = MaterialButton(binding.root.context).apply { + text = artifact.name + setTextAppearance( + com.google.android.material.R.style.TextAppearance_Material3_LabelLarge + ) + textAlignment = MaterialButton.TEXT_ALIGNMENT_VIEW_START + setBackgroundColor( + context.getColor( + com.google.android.material.R.color.m3_button_background_color_selector + ) + ) + setIconResource(R.drawable.ic_import) + iconTint = ColorStateList.valueOf( + MaterialColors.getColor( + this, + com.google.android.material.R.attr.colorPrimary + ) + ) + + elevation = 6f + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + setOnClickListener { + val dialogBinding = + DialogProgressBinding.inflate(LayoutInflater.from(context)) + dialogBinding.progressBar.isIndeterminate = true + dialogBinding.title.text = context.getString(R.string.installing_driver) + dialogBinding.status.text = context.getString(R.string.downloading) + + val progressDialog = MaterialAlertDialogBuilder(context) + .setView(dialogBinding.root) + .setCancelable(false) + .create() + + progressDialog.show() + + CoroutineScope(Dispatchers.Main).launch { + try { + val request = Request.Builder() + .url(artifact.url) + .header("Accept", "application/octet-stream") + .build() + + val cacheDir = context.externalCacheDir ?: throw IOException( + context.getString(R.string.failed_cache_dir) + ) + + cacheDir.mkdirs() + + val file = File(cacheDir, artifact.name) + + withContext(Dispatchers.IO) { + client.newBuilder() + .followRedirects(true) + .followSslRedirects(true) + .build() + .newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw IOException("${response.code}") + } + + response.body?.byteStream()?.use { input -> + FileOutputStream(file).use { output -> + input.copyTo(output) + } + } + ?: throw IOException( + context.getString(R.string.empty_response_body) + ) + } + } + + if (file.length() == 0L) { + throw IOException(context.getString(R.string.driver_empty)) + } + + dialogBinding.status.text = context.getString(R.string.installing) + + val driverData = GpuDriverHelper.getMetadataFromZip(file) + val driverPath = + "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename( + file.toUri() + )}" + + if (GpuDriverHelper.copyDriverToInternalStorage(file.toUri())) { + driverViewModel.onDriverAdded(Pair(driverPath, driverData)) + + progressDialog.dismiss() + Toast.makeText( + context, + context.getString( + R.string.successfully_installed, + driverData.name + ), + Toast.LENGTH_SHORT + ).show() + } else { + throw IOException( + context.getString( + R.string.failed_install_driver, + artifact.name + ) + ) + } + } catch (e: Exception) { + progressDialog.dismiss() + + MaterialAlertDialogBuilder(context) + .setTitle(context.getString(R.string.driver_failed_title)) + .setMessage(e.message) + .setPositiveButton(R.string.ok) { dialog, _ -> + dialog.cancel() + } + .show() + } + } + } + } + binding.containerDownloads.addView(button) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReleaseViewHolder { + val binding = ItemReleaseBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return ReleaseViewHolder(binding) + } + + override fun onBindViewHolder(holder: ReleaseViewHolder, position: Int) { + holder.bind(releases[position]) + } + + override fun getItemCount(): Int = releases.size +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/SpacingItemDecoration.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/SpacingItemDecoration.kt new file mode 100644 index 0000000000..f3d000a739 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/SpacingItemDecoration.kt @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.features.fetcher + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +class SpacingItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + outRect.bottom = spacing + if (parent.getChildAdapterPosition(view) == 0) { + outRect.top = spacing + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt index 2601387b76..b26fb1dec5 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -12,6 +15,10 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting { FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives"), CORE_SYNC_CORE_SPEED("sync_core_speed"), RENDERER_USE_SPEED_LIMIT("use_speed_limit"), + USE_FAST_CPU_TIME("use_fast_cpu_time"), + USE_CUSTOM_CPU_TICKS("use_custom_cpu_ticks"), + SKIP_CPU_INNER_INVALIDATION("skip_cpu_inner_invalidation"), + CPUOPT_UNSAFE_HOST_MMU("cpuopt_unsafe_host_mmu"), USE_DOCKED_MODE("use_docked_mode"), USE_AUTO_STUB("use_auto_stub"), RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache"), @@ -19,33 +26,53 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting { RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"), RENDERER_FAST_GPU("use_fast_gpu_time"), RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"), + RENDERER_EARLY_RELEASE_FENCES("early_release_fences"), + SYNC_MEMORY_OPERATIONS("sync_memory_operations"), + BUFFER_REORDER_DISABLE("disable_buffer_reorder"), RENDERER_DEBUG("debug"), - RENDERER_DYNA_STATE3("dyna_state3"), RENDERER_PROVOKING_VERTEX("provoking_vertex"), RENDERER_DESCRIPTOR_INDEXING("descriptor_indexing"), + RENDERER_SAMPLE_SHADING("sample_shading"), PICTURE_IN_PICTURE("picture_in_picture"), USE_CUSTOM_RTC("custom_rtc_enabled"), BLACK_BACKGROUNDS("black_backgrounds"), JOYSTICK_REL_CENTER("joystick_rel_center"), DPAD_SLIDE("dpad_slide"), HAPTIC_FEEDBACK("haptic_feedback"), - SHOW_PERFORMANCE_OVERLAY("show_performance_overlay"), SHOW_INPUT_OVERLAY("show_input_overlay"), TOUCHSCREEN("touchscreen"), - SHOW_THERMAL_OVERLAY("show_thermal_overlay"), + AIRPLANE_MODE("airplane_mode"), + + SHOW_SOC_OVERLAY("show_soc_overlay"), + SHOW_DEVICE_MODEL("show_device_model"), + SHOW_GPU_MODEL("show_gpu_model"), + SHOW_SOC_MODEL("show_soc_model"), + SHOW_FW_VERSION("show_firmware_version"), + + SOC_OVERLAY_BACKGROUND("soc_overlay_background"), + FRAME_INTERPOLATION("frame_interpolation"), - FRAME_SKIPPING("frame_skipping"), +// FRAME_SKIPPING("frame_skipping"), + + ENABLE_INPUT_OVERLAY_AUTO_HIDE("enable_input_overlay_auto_hide"), + + PERF_OVERLAY_BACKGROUND("perf_overlay_background"), + SHOW_PERFORMANCE_OVERLAY("show_performance_overlay"), + SHOW_FPS("show_fps"), SHOW_FRAMETIME("show_frame_time"), - SHOW_SPEED("show_speed"), SHOW_APP_RAM_USAGE("show_app_ram_usage"), SHOW_SYSTEM_RAM_USAGE("show_system_ram_usage"), SHOW_BAT_TEMPERATURE("show_bat_temperature"), - OVERLAY_BACKGROUND("overlay_background"), - USE_LRU_CACHE("use_lru_cache"),; - external fun isFrameSkippingEnabled(): Boolean - external fun isFrameInterpolationEnabled(): Boolean + SHOW_POWER_INFO("show_power_info"), + SHOW_SHADERS_BUILDING("show_shaders_building"), + DEBUG_FLUSH_BY_LINE("flush_line"), + USE_LRU_CACHE("use_lru_cache"); + + +// external fun isFrameSkippingEnabled(): Boolean + external fun isFrameInterpolationEnabled(): Boolean override fun getBoolean(needsGlobal: Boolean): Boolean = NativeConfig.getBoolean(key, needsGlobal) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt index 035a33a762..d5556a337b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -24,6 +27,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting { RENDERER_SCREEN_LAYOUT("screen_layout"), RENDERER_ASPECT_RATIO("aspect_ratio"), RENDERER_OPTIMIZE_SPIRV_OUTPUT("optimize_spirv_output"), + DMA_ACCURACY("dma_accuracy"), AUDIO_OUTPUT_ENGINE("output_engine"), MAX_ANISOTROPY("max_anisotropy"), THEME("theme"), @@ -33,7 +37,31 @@ enum class IntSetting(override val key: String) : AbstractIntSetting { LOCK_DRAWER("lock_drawer"), VERTICAL_ALIGNMENT("vertical_alignment"), PERF_OVERLAY_POSITION("perf_overlay_position"), - FSR_SHARPENING_SLIDER("fsr_sharpening_slider"); + SOC_OVERLAY_POSITION("soc_overlay_position"), + MEMORY_LAYOUT("memory_layout_mode"), + FSR_SHARPENING_SLIDER("fsr_sharpening_slider"), + RENDERER_SAMPLE_SHADING_FRACTION("sample_shading_fraction"), + FAST_CPU_TIME("fast_cpu_time"), + CPU_TICKS("cpu_ticks"), + FAST_GPU_TIME("fast_gpu_time"), + BAT_TEMPERATURE_UNIT("bat_temperature_unit"), + CABINET_APPLET("cabinet_applet_mode"), + CONTROLLER_APPLET("controller_applet_mode"), + DATA_ERASE_APPLET("data_erase_applet_mode"), + ERROR_APPLET("error_applet_mode"), + NET_CONNECT_APPLET("net_connect_applet_mode"), + PLAYER_SELECT_APPLET("player_select_applet"), + SWKBD_APPLET("swkbd_applet_mode"), + MII_EDIT_APPLET("mii_edit_applet_mode"), + WEB_APPLET("web_applet_mode"), + SHOP_APPLET("shop_applet_mode"), + PHOTO_VIEWER_APPLET("photo_viewer_applet_mode"), + OFFLINE_WEB_APPLET("offline_web_applet_mode"), + LOGIN_SHARE_APPLET("login_share_applet_mode"), + WIFI_WEB_AUTH_APPLET("wifi_web_auth_applet_mode"), + MY_PAGE_APPLET("my_page_applet_mode"), + INPUT_OVERLAY_AUTO_HIDE("input_overlay_auto_hide") + ; override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt index 299a192a13..454d0e4807 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt @@ -1,7 +1,6 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later - package org.yuzu.yuzu_emu.features.settings.model import org.yuzu.yuzu_emu.R @@ -12,7 +11,9 @@ object Settings { SECTION_ROOT(R.string.advanced_settings), SECTION_SYSTEM(R.string.preferences_system), SECTION_RENDERER(R.string.preferences_graphics), - SECTION_PERFORMANCE_STATS(R.string.show_stats_overlay), + SECTION_PERFORMANCE_STATS(R.string.stats_overlay_options), + SECTION_INPUT_OVERLAY(R.string.input_overlay_options), + SECTION_SOC_OVERLAY(R.string.soc_overlay_options), SECTION_AUDIO(R.string.preferences_audio), SECTION_INPUT(R.string.preferences_controls), SECTION_INPUT_PLAYER_ONE, @@ -25,14 +26,17 @@ object Settings { SECTION_INPUT_PLAYER_EIGHT, SECTION_THEME(R.string.preferences_theme), SECTION_DEBUG(R.string.preferences_debug), - SECTION_EDEN_VEIL(R.string.eden_veil); + SECTION_EDEN_VEIL(R.string.eden_veil), + SECTION_APPLETS(R.string.applets_menu); } fun getPlayerString(player: Int): String = YuzuApplication.appContext.getString(R.string.preferences_player, player) const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" + const val PREF_SHOULD_SHOW_DRIVER_WARNING = "ShouldShowDriverWarning" const val PREF_SHOULD_SHOW_PRE_ALPHA_WARNING = "ShouldShowPreAlphaWarning" + const val PREF_SHOULD_SHOW_EDENS_VEIL_DIALOG = "ShouldShowEdensVeilDialog" const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown" const val SECTION_STATS_OVERLAY = "Stats Overlay" @@ -124,14 +128,14 @@ object Settings { } } - enum class OptimizeSpirvOutput(val int: Int) { - Never(0), - OnLoad(1), - Always(2); + enum class OptimizeSpirvOutput(val int: Int) { + Never(0), + OnLoad(1), + Always(2); - companion object { - fun from(int: Int): OptimizeSpirvOutput = - entries.firstOrNull { it.int == int } ?: OnLoad - } - } + companion object { + fun from(int: Int): OptimizeSpirvOutput = + entries.firstOrNull { it.int == int } ?: OnLoad + } + } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt index 6f16cf5b19..55ddd5950c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -7,7 +10,11 @@ import org.yuzu.yuzu_emu.utils.NativeConfig enum class StringSetting(override val key: String) : AbstractStringSetting { DRIVER_PATH("driver_path"), - DEVICE_NAME("device_name"); + DEVICE_NAME("device_name"), + + WEB_TOKEN("eden_token"), + WEB_USERNAME("eden_username") + ; override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index a373bd816c..ebc726225a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -17,7 +20,7 @@ import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.LongSetting import org.yuzu.yuzu_emu.features.settings.model.ShortSetting import org.yuzu.yuzu_emu.features.settings.model.StringSetting -import org.yuzu.yuzu_emu.utils.GpuDriverHelper +import org.yuzu.yuzu_emu.network.NetDataValidators import org.yuzu.yuzu_emu.utils.NativeConfig /** @@ -52,10 +55,6 @@ abstract class SettingsItem( val isEditable: Boolean get() { - if (setting.key == BooleanSetting.FRAME_SKIPPING.key) { - // disabled for now - return false - } // Can't change docked mode toggle when using handheld mode if (setting.key == BooleanSetting.USE_DOCKED_MODE.key) { return NativeInput.getStyleIndex(0) != NpadStyleIndex.Handheld @@ -97,6 +96,7 @@ abstract class SettingsItem( const val TYPE_INT_SINGLE_CHOICE = 9 const val TYPE_INPUT_PROFILE = 10 const val TYPE_STRING_INPUT = 11 + const val TYPE_SPINBOX = 12 const val FASTMEM_COMBINED = "fastmem_combined" @@ -131,19 +131,12 @@ abstract class SettingsItem( ) ) put( - SliderSetting( + SingleChoiceSetting( ByteSetting.RENDERER_DYNA_STATE, titleId = R.string.dyna_state, descriptionId = R.string.dyna_state_description, - min = 0, - max = 2, - ) - ) - put( - SwitchSetting( - BooleanSetting.RENDERER_DYNA_STATE3, - titleId = R.string.dyna_state3, - descriptionId = R.string.dyna_state3_description + choicesId = R.array.dynaStateEntries, + valuesId = R.array.dynaStateValues ) ) put( @@ -160,6 +153,21 @@ abstract class SettingsItem( descriptionId = R.string.descriptor_indexing_description ) ) + put( + SwitchSetting( + BooleanSetting.RENDERER_SAMPLE_SHADING, + titleId = R.string.sample_shading, + descriptionId = R.string.sample_shading_description + ) + ) + put( + SliderSetting( + IntSetting.RENDERER_SAMPLE_SHADING_FRACTION, + titleId = R.string.sample_shading_fraction, + descriptionId = R.string.sample_shading_fraction_description, + units = "%" + ) + ) put( SliderSetting( ShortSetting.RENDERER_SPEED_LIMIT, @@ -193,6 +201,13 @@ abstract class SettingsItem( descriptionId = R.string.picture_in_picture_description ) ) + put( + SwitchSetting( + BooleanSetting.DEBUG_FLUSH_BY_LINE, + titleId = R.string.flush_by_line, + descriptionId = R.string.flush_by_line_description + ) + ) val dockedModeSetting = object : AbstractBooleanSetting { override val key = BooleanSetting.USE_DOCKED_MODE.key @@ -214,7 +229,6 @@ abstract class SettingsItem( override fun reset() = BooleanSetting.USE_DOCKED_MODE.reset() } - put( SwitchSetting( BooleanSetting.FRAME_INTERPOLATION, @@ -223,13 +237,13 @@ abstract class SettingsItem( ) ) - put( - SwitchSetting( - BooleanSetting.FRAME_SKIPPING, - titleId = R.string.frame_skipping, - descriptionId = R.string.frame_skipping_description - ) - ) +// put( +// SwitchSetting( +// BooleanSetting.FRAME_SKIPPING, +// titleId = R.string.frame_skipping, +// descriptionId = R.string.frame_skipping_description +// ) +// ) put( SwitchSetting( @@ -239,13 +253,21 @@ abstract class SettingsItem( ) ) put( - SwitchSetting( - BooleanSetting.CORE_SYNC_CORE_SPEED, - titleId = R.string.use_sync_core, - descriptionId = R.string.use_sync_core_description - ) - ) - + SingleChoiceSetting( + IntSetting.MEMORY_LAYOUT, + titleId = R.string.memory_layout, + descriptionId = R.string.memory_layout_description, + choicesId = R.array.memoryNames, + valuesId = R.array.memoryValues + ) + ) + put( + SwitchSetting( + BooleanSetting.CORE_SYNC_CORE_SPEED, + titleId = R.string.use_sync_core, + descriptionId = R.string.use_sync_core_description + ) + ) put( SingleChoiceSetting( IntSetting.REGION_INDEX, @@ -269,6 +291,29 @@ abstract class SettingsItem( descriptionId = R.string.use_custom_rtc_description ) ) + put( + StringInputSetting( + StringSetting.WEB_TOKEN, + titleId = R.string.web_token, + descriptionId = R.string.web_token_description, + onGenerate = { + val chars = "abcdefghijklmnopqrstuvwxyz" + (1..48).map { chars.random() }.joinToString("") + }, + validator = NetDataValidators::token, + errorId = R.string.multiplayer_token_error + ) + ) + + put( + StringInputSetting( + StringSetting.WEB_USERNAME, + titleId = R.string.web_username, + descriptionId = R.string.web_username_description, + validator = NetDataValidators::username, + errorId = R.string.multiplayer_username_error + ) + ) put(DateTimeSetting(LongSetting.CUSTOM_RTC, titleId = R.string.set_custom_rtc)) put( SingleChoiceSetting( @@ -329,9 +374,28 @@ abstract class SettingsItem( IntSetting.RENDERER_RESOLUTION, titleId = R.string.renderer_resolution, choicesId = R.array.rendererResolutionNames, - valuesId = R.array.rendererResolutionValues + valuesId = R.array.rendererResolutionValues, + warnChoices = (5..7).toList(), + warningMessage = R.string.warning_resolution ) ) + put( + SwitchSetting( + BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE, + titleId = R.string.enable_input_overlay_auto_hide, + ) + ) + put( + SpinBoxSetting( + IntSetting.INPUT_OVERLAY_AUTO_HIDE, + titleId = R.string.overlay_auto_hide, + descriptionId = R.string.overlay_auto_hide_description, + min = 1, + max = 999, + valueHint = R.string.seconds + ) + ) + put( SwitchSetting( BooleanSetting.SHOW_PERFORMANCE_OVERLAY, @@ -341,9 +405,9 @@ abstract class SettingsItem( ) put( SwitchSetting( - BooleanSetting.OVERLAY_BACKGROUND, - R.string.overlay_background, - descriptionId = R.string.overlay_background_description + BooleanSetting.PERF_OVERLAY_BACKGROUND, + R.string.perf_overlay_background, + descriptionId = R.string.perf_overlay_background_description ) ) put( @@ -355,6 +419,7 @@ abstract class SettingsItem( valuesId = R.array.staticThemeValues ) ) + put( SwitchSetting( BooleanSetting.SHOW_FPS, @@ -369,13 +434,6 @@ abstract class SettingsItem( descriptionId = R.string.show_frametime_description ) ) - put( - SwitchSetting( - BooleanSetting.SHOW_SPEED, - R.string.show_speed, - descriptionId = R.string.show_speed_description - ) - ) put( SwitchSetting( BooleanSetting.SHOW_APP_RAM_USAGE, @@ -397,6 +455,82 @@ abstract class SettingsItem( descriptionId = R.string.show_bat_temperature_description ) ) + put( + SingleChoiceSetting( + IntSetting.BAT_TEMPERATURE_UNIT, + R.string.bat_temperature_unit, + choicesId = R.array.temperatureUnitEntries, + valuesId = R.array.temperatureUnitValues + ) + ) + put( + SwitchSetting( + BooleanSetting.SHOW_POWER_INFO, + R.string.show_power_info, + descriptionId = R.string.show_power_info_description + ) + ) + put( + SwitchSetting( + BooleanSetting.SHOW_SHADERS_BUILDING, + R.string.show_shaders_building, + descriptionId = R.string.show_shaders_building_description + ) + ) + + put( + SwitchSetting( + BooleanSetting.SHOW_SOC_OVERLAY, + R.string.enable_soc_overlay, + descriptionId = R.string.soc_overlay_options_description + ) + ) + put( + SwitchSetting( + BooleanSetting.SOC_OVERLAY_BACKGROUND, + R.string.perf_overlay_background, + descriptionId = R.string.perf_overlay_background_description + ) + ) + put( + SingleChoiceSetting( + IntSetting.SOC_OVERLAY_POSITION, + titleId = R.string.overlay_position, + descriptionId = R.string.overlay_position_description, + choicesId = R.array.statsPosition, + valuesId = R.array.staticThemeValues + ) + ) + + put( + SwitchSetting( + BooleanSetting.SHOW_DEVICE_MODEL, + titleId = R.string.show_device_model, + descriptionId = R.string.show_device_model_description + ) + ) + put( + SwitchSetting( + BooleanSetting.SHOW_GPU_MODEL, + titleId = R.string.show_gpu_model, + descriptionId = R.string.show_gpu_model_description + ) + ) + put( + SwitchSetting( + BooleanSetting.SHOW_SOC_MODEL, + titleId = R.string.show_soc_model, + descriptionId = R.string.show_soc_model_description + ) + ) + put( + SwitchSetting( + BooleanSetting.SHOW_FW_VERSION, + titleId = R.string.show_fw_version, + descriptionId = R.string.show_fw_version_description + ) + ) + put( SingleChoiceSetting( IntSetting.RENDERER_VSYNC, @@ -469,14 +603,23 @@ abstract class SettingsItem( ) ) put( - SingleChoiceSetting( - IntSetting.RENDERER_OPTIMIZE_SPIRV_OUTPUT, - titleId = R.string.renderer_optimize_spirv_output, - descriptionId = R.string.renderer_optimize_spirv_output_description, - choicesId = R.array.optimizeSpirvOutputEntries, - valuesId = R.array.optimizeSpirvOutputValues - ) - ) + SingleChoiceSetting( + IntSetting.RENDERER_OPTIMIZE_SPIRV_OUTPUT, + titleId = R.string.renderer_optimize_spirv_output, + descriptionId = R.string.renderer_optimize_spirv_output_description, + choicesId = R.array.optimizeSpirvOutputEntries, + valuesId = R.array.optimizeSpirvOutputValues + ) + ) + put( + SingleChoiceSetting( + IntSetting.DMA_ACCURACY, + titleId = R.string.dma_accuracy, + descriptionId = R.string.dma_accuracy_description, + choicesId = R.array.dmaAccuracyNames, + valuesId = R.array.dmaAccuracyValues + ) + ) put( SwitchSetting( BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS, @@ -491,6 +634,61 @@ abstract class SettingsItem( descriptionId = R.string.use_fast_gpu_time_description ) ) + put( + SingleChoiceSetting( + IntSetting.FAST_GPU_TIME, + titleId = R.string.fast_gpu_time, + descriptionId = R.string.fast_gpu_time_description, + choicesId = R.array.gpuEntries, + valuesId = R.array.gpuValues + ) + ) + put( + SwitchSetting( + BooleanSetting.USE_FAST_CPU_TIME, + titleId = R.string.use_fast_cpu_time, + descriptionId = R.string.use_fast_cpu_time_description + ) + ) + put( + SingleChoiceSetting( + IntSetting.FAST_CPU_TIME, + titleId = R.string.fast_cpu_time, + descriptionId = R.string.fast_cpu_time_description, + choicesId = R.array.clockNames, + valuesId = R.array.clockValues + ) + ) + put( + SwitchSetting( + BooleanSetting.USE_CUSTOM_CPU_TICKS, + titleId = R.string.custom_cpu_ticks, + descriptionId = R.string.custom_cpu_ticks_description + ) + ) + put( + SliderSetting( + IntSetting.CPU_TICKS, + titleId = R.string.cpu_ticks, + descriptionId = 0, + min = 77, + max = 65535 + ) + ) + put( + SwitchSetting( + BooleanSetting.SKIP_CPU_INNER_INVALIDATION, + titleId = R.string.skip_cpu_inner_invalidation, + descriptionId = R.string.skip_cpu_inner_invalidation_description + ) + ) + put( + SwitchSetting( + BooleanSetting.CPUOPT_UNSAFE_HOST_MMU, + titleId = R.string.cpuopt_unsafe_host_mmu, + descriptionId = R.string.cpuopt_unsafe_host_mmu_description + ) + ) put( SwitchSetting( BooleanSetting.RENDERER_REACTIVE_FLUSHING, @@ -498,6 +696,27 @@ abstract class SettingsItem( descriptionId = R.string.renderer_reactive_flushing_description ) ) + put( + SwitchSetting( + BooleanSetting.RENDERER_EARLY_RELEASE_FENCES, + titleId = R.string.renderer_early_release_fences, + descriptionId = R.string.renderer_early_release_fences_description + ) + ) + put( + SwitchSetting( + BooleanSetting.SYNC_MEMORY_OPERATIONS, + titleId = R.string.sync_memory_operations, + descriptionId = R.string.sync_memory_operations_description + ) + ) + put( + SwitchSetting( + BooleanSetting.BUFFER_REORDER_DISABLE, + titleId = R.string.buffer_reorder_disable, + descriptionId = R.string.buffer_reorder_disable_description + ) + ) put( SingleChoiceSetting( IntSetting.MAX_ANISOTROPY, @@ -539,12 +758,12 @@ abstract class SettingsItem( ) ) put( - SwitchSetting( - BooleanSetting.USE_AUTO_STUB, - titleId = R.string.use_auto_stub, - descriptionId = R.string.use_auto_stub_description - ) - ) + SwitchSetting( + BooleanSetting.USE_AUTO_STUB, + titleId = R.string.use_auto_stub, + descriptionId = R.string.use_auto_stub_description + ) + ) put( SwitchSetting( BooleanSetting.CPU_DEBUG_MODE, @@ -586,6 +805,25 @@ abstract class SettingsItem( override fun reset() = setBoolean(defaultValue) } put(SwitchSetting(fastmem, R.string.fastmem)) + + // Applet Settings + put( + SingleChoiceSetting( + IntSetting.SWKBD_APPLET, + titleId = R.string.swkbd_applet, + choicesId = R.array.appletEntries, + valuesId = R.array.appletValues + ) + ) + + put( + SwitchSetting( + BooleanSetting.AIRPLANE_MODE, + titleId = R.string.airplane_mode, + descriptionId = R.string.airplane_mode_description + ) + ) } } } + diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt index ea5e099ede..31d06c1891 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -5,6 +8,7 @@ package org.yuzu.yuzu_emu.features.settings.model.view import androidx.annotation.ArrayRes import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting @@ -15,15 +19,23 @@ class SingleChoiceSetting( @StringRes descriptionId: Int = 0, descriptionString: String = "", @ArrayRes val choicesId: Int, - @ArrayRes val valuesId: Int + @ArrayRes val valuesId: Int, + val warnChoices: List = ArrayList(), + @StringRes val warningMessage: Int = 0 ) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) { override val type = TYPE_SINGLE_CHOICE fun getSelectedValue(needsGlobal: Boolean = false) = when (setting) { is AbstractIntSetting -> setting.getInt(needsGlobal) + is AbstractByteSetting -> setting.getByte(needsGlobal).toInt() else -> -1 } - fun setSelectedValue(value: Int) = (setting as AbstractIntSetting).setInt(value) + fun setSelectedValue(value: Int) = + when (setting) { + is AbstractIntSetting -> setting.setInt(value) + is AbstractByteSetting -> setting.setByte(value.toByte()) + else -> -1 + } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SpinBoxSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SpinBoxSetting.kt new file mode 100644 index 0000000000..0b0d01dfe0 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SpinBoxSetting.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting +import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting +import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting +import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting +import org.yuzu.yuzu_emu.features.settings.model.AbstractShortSetting + +class SpinBoxSetting( + setting: AbstractSetting, + @StringRes titleId: Int = 0, + titleString: String = "", + @StringRes descriptionId: Int = 0, + descriptionString: String = "", + val valueHint: Int, + val min: Int, + val max: Int +) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) { + override val type = TYPE_SPINBOX + + fun getSelectedValue(needsGlobal: Boolean = false) = + when (setting) { + is AbstractByteSetting -> setting.getByte(needsGlobal).toInt() + is AbstractShortSetting -> setting.getShort(needsGlobal).toInt() + is AbstractIntSetting -> setting.getInt(needsGlobal) + is AbstractFloatSetting -> setting.getFloat(needsGlobal).toInt() + else -> 0 + } + + fun setSelectedValue(value: Int) = + when (setting) { + is AbstractByteSetting -> setting.setByte(value.toByte()) + is AbstractShortSetting -> setting.setShort(value.toShort()) + is AbstractFloatSetting -> setting.setFloat(value.toFloat()) + else -> (setting as AbstractIntSetting).setInt(value) + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringInputSetting.kt index 1eb999416a..1d6de233b8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringInputSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringInputSetting.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -11,7 +14,10 @@ class StringInputSetting( @StringRes titleId: Int = 0, titleString: String = "", @StringRes descriptionId: Int = 0, - descriptionString: String = "" + descriptionString: String = "", + val onGenerate: (() -> String)? = null, + val validator: ((s: String?) -> Boolean)? = null, + @StringRes val errorId: Int = 0 ) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) { override val type = TYPE_STRING_INPUT diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt index 500ac6e66e..bdc51b7070 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.features.settings.ui @@ -61,6 +61,10 @@ class SettingsAdapter( SliderViewHolder(ListItemSettingBinding.inflate(inflater), this) } + SettingsItem.TYPE_SPINBOX -> { + SpinBoxViewHolder(ListItemSettingBinding.inflate(inflater), this) + } + SettingsItem.TYPE_SUBMENU -> { SubmenuViewHolder(ListItemSettingBinding.inflate(inflater), this) } @@ -191,6 +195,14 @@ class SettingsAdapter( position ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) } + fun onSpinBoxClick(item: SpinBoxSetting, position: Int) { + SettingsDialogFragment.newInstance( + settingsViewModel, + item, + SettingsItem.TYPE_SPINBOX, + position + ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) + } fun onSubmenuClick(item: SubmenuSetting) { val action = SettingsNavigationDirections.actionGlobalSettingsFragment(item.menuKey, null) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt index 7f562a1f41..2a97f15892 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -6,9 +9,13 @@ package org.yuzu.yuzu_emu.features.settings.ui import android.app.Dialog import android.content.DialogInterface import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -16,6 +23,7 @@ import com.google.android.material.slider.Slider import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding import org.yuzu.yuzu_emu.databinding.DialogSliderBinding +import org.yuzu.yuzu_emu.databinding.DialogSpinboxBinding import org.yuzu.yuzu_emu.features.input.NativeInput import org.yuzu.yuzu_emu.features.input.model.AnalogDirection import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting @@ -24,6 +32,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting +import org.yuzu.yuzu_emu.features.settings.model.view.SpinBoxSetting import org.yuzu.yuzu_emu.features.settings.model.view.StringInputSetting import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting import org.yuzu.yuzu_emu.utils.ParamPackage @@ -40,6 +49,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener private lateinit var sliderBinding: DialogSliderBinding private lateinit var stringInputBinding: DialogEditTextBinding + private lateinit var spinboxBinding: DialogSpinboxBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -106,6 +116,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener SettingsItem.TYPE_SINGLE_CHOICE -> { val item = settingsViewModel.clickedItem as SingleChoiceSetting val value = getSelectionForSingleChoiceValue(item) + MaterialAlertDialogBuilder(requireContext()) .setTitle(item.title) .setSingleChoiceItems(item.choicesId, value, this) @@ -118,6 +129,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener settingsViewModel.setSliderTextValue(item.getSelectedValue().toFloat(), item.units) sliderBinding.slider.apply { + stepSize = 1.0f valueFrom = item.min.toFloat() valueTo = item.max.toFloat() value = settingsViewModel.sliderProgress.value.toFloat() @@ -134,10 +146,127 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener .create() } + SettingsItem.TYPE_SPINBOX -> { + spinboxBinding = DialogSpinboxBinding.inflate(layoutInflater) + val item = settingsViewModel.clickedItem as SpinBoxSetting + + val currentValue = item.getSelectedValue() + spinboxBinding.editValue.setText(currentValue.toString()) + spinboxBinding.textInputLayout.hint = getString(item.valueHint) + + val dialog = MaterialAlertDialogBuilder(requireContext()) + .setTitle(item.title) + .setView(spinboxBinding.root) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, defaultCancelListener) + .create() + + val updateButtonState = { enabled: Boolean -> + dialog.setOnShowListener { dialogInterface -> + (dialogInterface as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled = enabled + } + if (dialog.isShowing) { + dialog.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled = enabled + } + } + + val updateValidity = { value: Int -> + val isValid = value in item.min..item.max + if (isValid) { + spinboxBinding.textInputLayout.error = null + } else { + spinboxBinding.textInputLayout.error = getString( + if (value < item.min) R.string.value_too_low else R.string.value_too_high, + if (value < item.min) item.min else item.max + ) + } + updateButtonState(isValid) + } + + spinboxBinding.buttonDecrement.setOnClickListener { + val current = spinboxBinding.editValue.text.toString().toIntOrNull() ?: currentValue + val newValue = current - 1 + spinboxBinding.editValue.setText(newValue.toString()) + updateValidity(newValue) + } + + spinboxBinding.buttonIncrement.setOnClickListener { + val current = spinboxBinding.editValue.text.toString().toIntOrNull() ?: currentValue + val newValue = current + 1 + spinboxBinding.editValue.setText(newValue.toString()) + updateValidity(newValue) + } + + spinboxBinding.editValue.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + val value = s.toString().toIntOrNull() + if (value != null) { + updateValidity(value) + } else { + spinboxBinding.textInputLayout.error = getString(R.string.invalid_value) + updateButtonState(false) + } + } + }) + + updateValidity(currentValue) + + dialog + } + SettingsItem.TYPE_STRING_INPUT -> { stringInputBinding = DialogEditTextBinding.inflate(layoutInflater) val item = settingsViewModel.clickedItem as StringInputSetting stringInputBinding.editText.setText(item.getSelectedValue()) + + val onGenerate = item.onGenerate + stringInputBinding.generate.isVisible = onGenerate != null + + if (onGenerate != null) { + stringInputBinding.generate.setOnClickListener { + stringInputBinding.editText.setText(onGenerate()) + } + } + + val validator = item.validator + + if (validator != null) { + val watcher = object : TextWatcher { + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + s: CharSequence?, + start: Int, + before: Int, + count: Int + ) { + } + + override fun afterTextChanged(s: Editable?) { + val isValid = validator(s.toString()) + stringInputBinding.editTextLayout.isErrorEnabled = !isValid + stringInputBinding.editTextLayout.error = if (isValid) { + null + } else { + requireContext().getString( + item.errorId + ) + } + } + } + + stringInputBinding.editText.addTextChangedListener(watcher) + watcher.afterTextChanged(stringInputBinding.editText.text) + } + MaterialAlertDialogBuilder(requireContext()) .setTitle(item.title) .setView(stringInputBinding.root) @@ -197,6 +326,15 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener is SingleChoiceSetting -> { val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting val value = getValueForSingleChoiceSelection(scSetting, which) + + if (value in scSetting.warnChoices) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.warning) + .setMessage(scSetting.warningMessage) + .setPositiveButton(R.string.ok, null) + .create() + .show() + } scSetting.setSelectedValue(value) } @@ -217,6 +355,14 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value) } + is SpinBoxSetting -> { + val spinBoxSetting = settingsViewModel.clickedItem as SpinBoxSetting + val value = spinboxBinding.editValue.text.toString().toIntOrNull() + if (value != null && value in spinBoxSetting.min..spinBoxSetting.max) { + spinBoxSetting.setSelectedValue(value) + } + } + is StringInputSetting -> { val stringInputSetting = settingsViewModel.clickedItem as StringInputSetting stringInputSetting.setSelectedValue( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt index ec16f16c46..b2fde638db 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -23,7 +26,7 @@ import org.yuzu.yuzu_emu.features.input.NativeInput import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins -import org.yuzu.yuzu_emu.utils.collect +import org.yuzu.yuzu_emu.utils.* class SettingsFragment : Fragment() { private lateinit var presenter: SettingsFragmentPresenter @@ -66,7 +69,8 @@ class SettingsFragment : Fragment() { presenter = SettingsFragmentPresenter( settingsViewModel, settingsAdapter!!, - args.menuTag + args.menuTag, + activity ) binding.toolbarSettingsLayout.title = if (args.menuTag == Settings.MenuTag.SECTION_ROOT && @@ -86,6 +90,7 @@ class SettingsFragment : Fragment() { else -> getString(args.menuTag.titleId) } } + binding.listSettings.apply { adapter = settingsAdapter layoutManager = LinearLayoutManager(requireContext()) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index aa7cae083b..0d882a7f01 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.features.settings.ui @@ -29,11 +29,14 @@ import org.yuzu.yuzu_emu.features.settings.model.view.* import org.yuzu.yuzu_emu.utils.InputHandler import org.yuzu.yuzu_emu.utils.NativeConfig import androidx.core.content.edit +import androidx.fragment.app.FragmentActivity +import org.yuzu.yuzu_emu.fragments.MessageDialogFragment class SettingsFragmentPresenter( private val settingsViewModel: SettingsViewModel, private val adapter: SettingsAdapter, - private var menuTag: MenuTag + private var menuTag: MenuTag, + private var activity: FragmentActivity? ) { private var settingsList = ArrayList() @@ -51,6 +54,7 @@ class SettingsFragmentPresenter( } val pairedSettingKey = item.setting.pairedSettingKey + if (pairedSettingKey.isNotEmpty()) { val pairedSettingValue = NativeConfig.getBoolean( pairedSettingKey, @@ -66,7 +70,7 @@ class SettingsFragmentPresenter( } // Allows you to show/hide abstract settings based on the paired setting key - fun ArrayList.addAbstract(item: SettingsItem) { + private fun ArrayList.addAbstract(item: SettingsItem) { val pairedSettingKey = item.setting.pairedSettingKey if (pairedSettingKey.isNotEmpty()) { val pairedSettingsItem = @@ -78,6 +82,9 @@ class SettingsFragmentPresenter( } fun onViewCreated() { + if (menuTag == MenuTag.SECTION_EDEN_VEIL) { + showEdenVeilWarningDialog() + } loadSettingsList() } @@ -88,7 +95,9 @@ class SettingsFragmentPresenter( MenuTag.SECTION_ROOT -> addConfigSettings(sl) MenuTag.SECTION_SYSTEM -> addSystemSettings(sl) MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl) - MenuTag.SECTION_PERFORMANCE_STATS -> addPerfomanceOverlaySettings(sl) + MenuTag.SECTION_PERFORMANCE_STATS -> addPerformanceOverlaySettings(sl) + MenuTag.SECTION_SOC_OVERLAY -> addSocOverlaySettings(sl) + MenuTag.SECTION_INPUT_OVERLAY -> addInputOverlaySettings(sl) MenuTag.SECTION_AUDIO -> addAudioSettings(sl) MenuTag.SECTION_INPUT -> addInputSettings(sl) MenuTag.SECTION_INPUT_PLAYER_ONE -> addInputPlayer(sl, 0) @@ -102,6 +111,7 @@ class SettingsFragmentPresenter( MenuTag.SECTION_THEME -> addThemeSettings(sl) MenuTag.SECTION_DEBUG -> addDebugSettings(sl) MenuTag.SECTION_EDEN_VEIL -> addEdenVeilSettings(sl) + MenuTag.SECTION_APPLETS -> addAppletSettings(sl) } settingsList = sl adapter.submitList(settingsList) { @@ -129,7 +139,7 @@ class SettingsFragmentPresenter( menuKey = MenuTag.SECTION_RENDERER ) ) - if (!NativeConfig.isPerGameConfigLoaded()) + if (!NativeConfig.isPerGameConfigLoaded()) { add( SubmenuSetting( titleId = R.string.stats_overlay_options, @@ -138,6 +148,24 @@ class SettingsFragmentPresenter( menuKey = MenuTag.SECTION_PERFORMANCE_STATS ) ) + + add( + SubmenuSetting( + titleId = R.string.soc_overlay_options, + descriptionId = R.string.soc_overlay_options_description, + iconId = R.drawable.ic_system, + menuKey = MenuTag.SECTION_SOC_OVERLAY + ) + ) + add( + SubmenuSetting( + titleId = R.string.input_overlay_options, + iconId = R.drawable.ic_controller, + descriptionId = R.string.input_overlay_options_description, + menuKey = MenuTag.SECTION_INPUT_OVERLAY + ) + ) + } add( SubmenuSetting( titleId = R.string.preferences_audio, @@ -162,6 +190,14 @@ class SettingsFragmentPresenter( menuKey = MenuTag.SECTION_EDEN_VEIL ) ) + add( + SubmenuSetting( + titleId = R.string.applets_menu, + descriptionId = R.string.applets_menu_description, + iconId = R.drawable.ic_applet, + menuKey = MenuTag.SECTION_APPLETS + ) + ) add( RunnableSetting( titleId = R.string.reset_to_default, @@ -173,88 +209,6 @@ class SettingsFragmentPresenter( } } - private val InterpolationSetting = object : AbstractBooleanSetting { - override val key = BooleanSetting.FRAME_INTERPOLATION.key - - override fun getBoolean(needsGlobal: Boolean): Boolean { - return BooleanSetting.FRAME_INTERPOLATION.getBoolean(needsGlobal) - } - - override fun setBoolean(value: Boolean) { - BooleanSetting.FRAME_INTERPOLATION.setBoolean(value) - } - - override val defaultValue = BooleanSetting.FRAME_INTERPOLATION.defaultValue - - override fun getValueAsString(needsGlobal: Boolean): String = - BooleanSetting.FRAME_INTERPOLATION.getValueAsString(needsGlobal) - - override fun reset() = BooleanSetting.FRAME_INTERPOLATION.reset() - } - - private val syncCoreSpeedSetting = object : AbstractBooleanSetting { - override val key = BooleanSetting.CORE_SYNC_CORE_SPEED.key - - override fun getBoolean(needsGlobal: Boolean): Boolean { - return BooleanSetting.CORE_SYNC_CORE_SPEED.getBoolean(needsGlobal) - } - - override fun setBoolean(value: Boolean) { - BooleanSetting.CORE_SYNC_CORE_SPEED.setBoolean(value) - } - - override val defaultValue = BooleanSetting.CORE_SYNC_CORE_SPEED.defaultValue - - override fun getValueAsString(needsGlobal: Boolean): String = - BooleanSetting.CORE_SYNC_CORE_SPEED.getValueAsString(needsGlobal) - - override fun reset() = BooleanSetting.CORE_SYNC_CORE_SPEED.reset() - } - - private val frameSkippingSetting = object : AbstractBooleanSetting { - override val key = BooleanSetting.FRAME_SKIPPING.key - - override fun getBoolean(needsGlobal: Boolean): Boolean { - return BooleanSetting.FRAME_SKIPPING.getBoolean(needsGlobal) - } - - override fun setBoolean(value: Boolean) { - BooleanSetting.FRAME_SKIPPING.setBoolean(value) - } - - override val defaultValue = BooleanSetting.FRAME_SKIPPING.defaultValue - - override fun getValueAsString(needsGlobal: Boolean): String = - BooleanSetting.FRAME_SKIPPING.getValueAsString(needsGlobal) - - override fun reset() = BooleanSetting.FRAME_SKIPPING.reset() - } - - private fun addEdenVeilSubmenu(sl: ArrayList) { - sl.apply { - add( - SubmenuSetting( - titleId = R.string.eden_veil, - descriptionId = R.string.eden_veil_description, - iconId = R.drawable.ic_code, - menuKey = MenuTag.SECTION_EDEN_VEIL - ) - ) - addEdenVeilSettings(sl) - - add(BooleanSetting.FRAME_INTERPOLATION.key) - add(BooleanSetting.FRAME_SKIPPING.key) - add(BooleanSetting.CORE_SYNC_CORE_SPEED.key) - add(IntSetting.RENDERER_SHADER_BACKEND.key) - add(IntSetting.RENDERER_OPTIMIZE_SPIRV_OUTPUT.key) - add(IntSetting.RENDERER_NVDEC_EMULATION.key) - add(IntSetting.RENDERER_ASTC_DECODE_METHOD.key) - add(IntSetting.RENDERER_ASTC_RECOMPRESSION.key) - add(IntSetting.RENDERER_VRAM_USAGE_MODE.key) - add(BooleanSetting.USE_LRU_CACHE.key) - } - } - private fun addSystemSettings(sl: ArrayList) { sl.apply { add(StringSetting.DEVICE_NAME.key) @@ -265,44 +219,85 @@ class SettingsFragmentPresenter( add(IntSetting.LANGUAGE_INDEX.key) add(BooleanSetting.USE_CUSTOM_RTC.key) add(LongSetting.CUSTOM_RTC.key) + + add(HeaderSetting(R.string.network)) + add(StringSetting.WEB_TOKEN.key) + add(StringSetting.WEB_USERNAME.key) } } private fun addGraphicsSettings(sl: ArrayList) { sl.apply { + add(HeaderSetting(R.string.backend)) + add(IntSetting.RENDERER_ACCURACY.key) add(IntSetting.RENDERER_RESOLUTION.key) - add(IntSetting.RENDERER_VSYNC.key) - add(IntSetting.RENDERER_SCALING_FILTER.key) - add(IntSetting.FSR_SHARPENING_SLIDER.key) - add(IntSetting.RENDERER_ANTI_ALIASING.key) - add(IntSetting.MAX_ANISOTROPY.key) - add(IntSetting.RENDERER_SCREEN_LAYOUT.key) - add(IntSetting.RENDERER_ASPECT_RATIO.key) - add(IntSetting.VERTICAL_ALIGNMENT.key) - add(BooleanSetting.PICTURE_IN_PICTURE.key) add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key) add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key) add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key) add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key) + + add(HeaderSetting(R.string.processing)) + + add(IntSetting.RENDERER_VSYNC.key) + add(IntSetting.RENDERER_ANTI_ALIASING.key) + add(IntSetting.MAX_ANISOTROPY.key) + add(IntSetting.RENDERER_SCALING_FILTER.key) + add(IntSetting.FSR_SHARPENING_SLIDER.key) + + add(HeaderSetting(R.string.display)) + + add(IntSetting.RENDERER_SCREEN_LAYOUT.key) + add(IntSetting.RENDERER_ASPECT_RATIO.key) + add(IntSetting.VERTICAL_ALIGNMENT.key) + add(BooleanSetting.PICTURE_IN_PICTURE.key) } } - private fun addPerfomanceOverlaySettings(sl: ArrayList) { + private fun addPerformanceOverlaySettings(sl: ArrayList) { sl.apply { add(HeaderSetting(R.string.stats_overlay_customization)) add(BooleanSetting.SHOW_PERFORMANCE_OVERLAY.key) - add(BooleanSetting.OVERLAY_BACKGROUND.key) + add(BooleanSetting.PERF_OVERLAY_BACKGROUND.key) add(IntSetting.PERF_OVERLAY_POSITION.key) + add(HeaderSetting(R.string.stats_overlay_items)) add(BooleanSetting.SHOW_FPS.key) add(BooleanSetting.SHOW_FRAMETIME.key) - add(BooleanSetting.SHOW_SPEED.key) add(BooleanSetting.SHOW_APP_RAM_USAGE.key) add(BooleanSetting.SHOW_SYSTEM_RAM_USAGE.key) add(BooleanSetting.SHOW_BAT_TEMPERATURE.key) + add(IntSetting.BAT_TEMPERATURE_UNIT.key) + add(BooleanSetting.SHOW_POWER_INFO.key) + add(BooleanSetting.SHOW_SHADERS_BUILDING.key) } + } + private fun addInputOverlaySettings(sl: ArrayList) { + sl.apply { + add(BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE.key) + add(IntSetting.INPUT_OVERLAY_AUTO_HIDE.key) + } + } + + private fun addSocOverlaySettings(sl: ArrayList) { + sl.apply { + add(HeaderSetting(R.string.stats_overlay_customization)) + add(BooleanSetting.SHOW_SOC_OVERLAY.key) + add(BooleanSetting.SOC_OVERLAY_BACKGROUND.key) + add(IntSetting.SOC_OVERLAY_POSITION.key) + + add(HeaderSetting(R.string.stats_overlay_items)) + add(BooleanSetting.SHOW_DEVICE_MODEL.key) + add(BooleanSetting.SHOW_GPU_MODEL.key) + + // the Build.SOC_MODEL API is 31+ only + if (Build.VERSION.SDK_INT >= 31) { + add(BooleanSetting.SHOW_SOC_MODEL.key) + } + + add(BooleanSetting.SHOW_FW_VERSION.key) + } } private fun addAudioSettings(sl: ArrayList) { @@ -459,25 +454,45 @@ class SettingsFragmentPresenter( private fun addEdenVeilSettings(sl: ArrayList) { sl.apply { - add(BooleanSetting.FRAME_INTERPOLATION.key) - add(BooleanSetting.FRAME_SKIPPING.key) - add(BooleanSetting.USE_LRU_CACHE.key) - add(BooleanSetting.RENDERER_FAST_GPU.key) - + add(HeaderSetting(R.string.veil_extensions)) add(ByteSetting.RENDERER_DYNA_STATE.key) - - add(BooleanSetting.RENDERER_DYNA_STATE3.key) add(BooleanSetting.RENDERER_PROVOKING_VERTEX.key) add(BooleanSetting.RENDERER_DESCRIPTOR_INDEXING.key) + add(BooleanSetting.RENDERER_SAMPLE_SHADING.key) + add(IntSetting.RENDERER_SAMPLE_SHADING_FRACTION.key) - add(BooleanSetting.CORE_SYNC_CORE_SPEED.key) - + add(HeaderSetting(R.string.veil_renderer)) + add(BooleanSetting.RENDERER_EARLY_RELEASE_FENCES.key) + add(IntSetting.DMA_ACCURACY.key) + add(BooleanSetting.BUFFER_REORDER_DISABLE.key) + add(BooleanSetting.FRAME_INTERPOLATION.key) + add(BooleanSetting.RENDERER_FAST_GPU.key) + add(IntSetting.FAST_GPU_TIME.key) add(IntSetting.RENDERER_SHADER_BACKEND.key) add(IntSetting.RENDERER_NVDEC_EMULATION.key) add(IntSetting.RENDERER_ASTC_DECODE_METHOD.key) add(IntSetting.RENDERER_ASTC_RECOMPRESSION.key) add(IntSetting.RENDERER_VRAM_USAGE_MODE.key) add(IntSetting.RENDERER_OPTIMIZE_SPIRV_OUTPUT.key) + + add(HeaderSetting(R.string.veil_misc)) + add(BooleanSetting.USE_FAST_CPU_TIME.key) + add(IntSetting.FAST_CPU_TIME.key) + add(BooleanSetting.USE_CUSTOM_CPU_TICKS.key) + add(IntSetting.CPU_TICKS.key) + add(BooleanSetting.SKIP_CPU_INNER_INVALIDATION.key) + add(BooleanSetting.CPUOPT_UNSAFE_HOST_MMU.key) + add(BooleanSetting.USE_LRU_CACHE.key) + add(BooleanSetting.CORE_SYNC_CORE_SPEED.key) + add(BooleanSetting.SYNC_MEMORY_OPERATIONS.key) + add(IntSetting.MEMORY_LAYOUT.key) + } + } + + private fun addAppletSettings(sl: ArrayList) { + sl.apply { + add(IntSetting.SWKBD_APPLET.key) + add(BooleanSetting.AIRPLANE_MODE.key) } } private fun addInputPlayer(sl: ArrayList, playerIndex: Int) { @@ -1060,7 +1075,9 @@ class SettingsFragmentPresenter( } val staticThemeColor: AbstractIntSetting = object : AbstractIntSetting { - val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + val preferences = PreferenceManager.getDefaultSharedPreferences( + YuzuApplication.appContext + ) override fun getInt(needsGlobal: Boolean): Int = preferences.getInt(Settings.PREF_STATIC_THEME_COLOR, 0) override fun setInt(value: Int) { @@ -1143,6 +1160,34 @@ class SettingsFragmentPresenter( add(BooleanSetting.USE_AUTO_STUB.key) add(BooleanSetting.CPU_DEBUG_MODE.key) add(SettingsItem.FASTMEM_COMBINED) + + add(HeaderSetting(R.string.log)) + add(BooleanSetting.DEBUG_FLUSH_BY_LINE.key) + } + } + + fun showEdenVeilWarningDialog() { + val shouldDisplayVeilWarning = + PreferenceManager.getDefaultSharedPreferences(activity!!.applicationContext) + .getBoolean(Settings.PREF_SHOULD_SHOW_EDENS_VEIL_DIALOG, true) + + if (shouldDisplayVeilWarning) { + activity?.let { + MessageDialogFragment.newInstance( + it, + titleId = R.string.eden_veil_warning_title, + descriptionId = R.string.eden_veil_warning_description, + positiveButtonTitleId = R.string.dont_show_again, + negativeButtonTitleId = R.string.close, + showNegativeButton = true, + positiveAction = { + PreferenceManager.getDefaultSharedPreferences(activity!!.applicationContext) + .edit() { + putBoolean(Settings.PREF_SHOULD_SHOW_EDENS_VEIL_DIALOG, false) + } + } + ).show(it.supportFragmentManager, MessageDialogFragment.TAG) + } } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SpinBoxViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SpinBoxViewHolder.kt new file mode 100644 index 0000000000..1f9a16b798 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SpinBoxViewHolder.kt @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui.viewholder + +import android.view.View +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem +import org.yuzu.yuzu_emu.features.settings.model.view.SpinBoxSetting +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible + +class SpinBoxViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + private lateinit var setting: SpinBoxSetting + + override fun bind(item: SettingsItem) { + setting = item as SpinBoxSetting + binding.textSettingName.text = setting.title + binding.textSettingDescription.setVisible(item.description.isNotEmpty()) + binding.textSettingDescription.text = setting.description + binding.textSettingValue.setVisible(true) + binding.textSettingValue.text = setting.getSelectedValue().toString() + + binding.buttonClear.setVisible(setting.clearable) + binding.buttonClear.setOnClickListener { + adapter.onClearClick(setting, bindingAdapterPosition) + } + + setStyle(setting.isEditable, binding) + } + override fun onClick(clicked: View) { + if (setting.isEditable) { + adapter.onSpinBoxClick(setting, bindingAdapterPosition) + } + } + override fun onLongClick(clicked: View): Boolean { + if (setting.isEditable) { + return adapter.onLongClick(setting, bindingAdapterPosition) + } + return false + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index e5763264a4..e4a2a82c9a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt @@ -22,6 +22,7 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter binding.textSettingDescription.setVisible(setting.description.isNotEmpty()) binding.textSettingDescription.text = setting.description + // TODO(alekpop): A race condition occurs here if the button is clicked too fast binding.switchWidget.setOnCheckedChangeListener(null) binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal) binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt index fc627473a2..88ec13dd4c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt @@ -1,6 +1,8 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.fragments @@ -92,7 +94,9 @@ class AboutFragment : Fragment() { } } - binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) } + binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.discord_link)) } + binding.buttonRevolt.setOnClickListener { openLink(getString(R.string.revolt_link)) } + binding.buttonX.setOnClickListener { openLink(getString(R.string.x_link)) } binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) } binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt index b027547cef..e2cb5f600d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.fragments @@ -33,7 +33,10 @@ class AddGameFolderDialogFragment : DialogFragment() { .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked) homeViewModel.setGamesDirSelected(true) - val calledFromGameFragment = requireArguments().getBoolean("calledFromGameFragment", false) + val calledFromGameFragment = requireArguments().getBoolean( + "calledFromGameFragment", + false + ) gamesViewModel.addFolder(newGameDir, calledFromGameFragment) } .setNegativeButton(android.R.string.cancel, null) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt index 5229e25475..573549d84b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.fragments diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt index f39bb1affd..3ab171a8d4 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.fragments diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt new file mode 100644 index 0000000000..dea762dc17 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt @@ -0,0 +1,350 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isVisible +import androidx.core.view.updatePadding +import androidx.fragment.app.activityViewModels +import androidx.navigation.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.transition.MaterialSharedAxis +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.FragmentDriverFetcherBinding +import org.yuzu.yuzu_emu.features.fetcher.DriverGroupAdapter +import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.utils.GpuDriverHelper +import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import java.io.IOException +import java.net.URL +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import kotlin.getValue + +class DriverFetcherFragment : Fragment() { + private var _binding: FragmentDriverFetcherBinding? = null + private val binding get() = _binding!! + + private val client = OkHttpClient() + + private val gpuModel: String? + get() = GpuDriverHelper.getGpuModel() + + private val adrenoModel: Int + get() = parseAdrenoModel() + + private val recommendedDriver: String + get() = driverMap.firstOrNull { adrenoModel in it.first }?.second ?: "Unsupported" + + enum class SortMode { + Default, PublishTime, + } + + private data class DriverRepo( + val name: String = "", + val path: String = "", + val sort: Int = 0, + val useTagName: Boolean = false, + val sortMode: SortMode = SortMode.Default + ) + + private val repoList: List = listOf( + DriverRepo("Mr. Purple Turnip", "MrPurple666/purple-turnip", 0), + DriverRepo("GameHub Adreno 8xx", "crueter/GameHub-8Elite-Drivers", 1), + DriverRepo("KIMCHI Turnip", "K11MCH1/AdrenoToolsDrivers", 2, true, SortMode.PublishTime), + DriverRepo("Weab-Chan Freedreno", "Weab-chan/freedreno_turnip-CI", 3) + ) + + private val driverMap = listOf( + IntRange(Integer.MIN_VALUE, 9) to "Unsupported", + IntRange(10, 99) to "KIMCHI Latest", // Special case for Adreno Axx + IntRange(100, 599) to "Unsupported", + IntRange(600, 639) to "Mr. Purple EOL-24.3.4", + IntRange(640, 699) to "Mr. Purple T19", + IntRange(700, 710) to "KIMCHI 25.2.0_r5", + IntRange(711, 799) to "Mr. Purple T22", + IntRange(800, 899) to "GameHub Adreno 8xx", + IntRange(900, Int.MAX_VALUE) to "Unsupported" + ) + + private lateinit var driverGroupAdapter: DriverGroupAdapter + private val driverViewModel: DriverViewModel by activityViewModels() + + private fun parseAdrenoModel(): Int { + if (gpuModel == null) { + return 0 + } + + val modelList = gpuModel!!.split(" ") + + // format: Adreno (TM) + if (modelList.size < 3 || modelList[0] != "Adreno") { + return 0 + } + + val model = modelList[2] + + try { + // special case for Axx GPUs (e.g. AYANEO Pocket S2) + // driverMap has specific ranges for this + if (model.startsWith("A")) { + return model.substring(1).toInt() + } + + return model.toInt() + } catch (e: Exception) { + // Model parse error, just say unsupported + e.printStackTrace() + return 0 + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentDriverFetcherBinding.inflate(inflater) + binding.badgeRecommendedDriver.text = recommendedDriver + binding.badgeGpuModel.text = gpuModel + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.toolbarDrivers.setNavigationOnClickListener { + binding.root.findNavController().popBackStack() + } + + binding.listDrivers.layoutManager = LinearLayoutManager(context) + driverGroupAdapter = DriverGroupAdapter(requireActivity(), driverViewModel) + binding.listDrivers.adapter = driverGroupAdapter + + setInsets() + + fetchDrivers() + } + + private fun fetchDrivers() { + binding.loadingIndicator.isVisible = true + + val driverGroups = arrayListOf() + + repoList.forEach { driver -> + val name = driver.name + val path = driver.path + val useTagName = driver.useTagName + val sortMode = driver.sortMode + val sort = driver.sort + + CoroutineScope(Dispatchers.Main).launch { + val request = + Request.Builder().url("https://api.github.com/repos/$path/releases").build() + + withContext(Dispatchers.IO) { + var releases: ArrayList + try { + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw IOException(response.body.toString()) + } + + val body = response.body?.string() ?: return@withContext + releases = Release.fromJsonArray(body, useTagName, sortMode) + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + MaterialAlertDialogBuilder(requireActivity()).setTitle( + getString(R.string.error_during_fetch) + ) + .setMessage( + "${getString(R.string.failed_to_fetch)} $name:\n${e.message}" + ) + .setPositiveButton(getString(R.string.ok)) { dialog, _ -> dialog.cancel() } + .show() + + releases = ArrayList() + } + } + + val group = DriverGroup( + name, + releases, + sort + ) + + synchronized(driverGroups) { + driverGroups.add(group) + driverGroups.sortBy { + it.sort + } + } + + withContext(Dispatchers.Main) { + driverGroupAdapter.updateDriverGroups(driverGroups) + + if (driverGroups.size >= repoList.size) { + binding.loadingIndicator.isVisible = false + } + } + } + } + } + } + + private fun setInsets() = ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> + val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + + val leftInsets = barInsets.left + cutoutInsets.left + val rightInsets = barInsets.right + cutoutInsets.right + + binding.toolbarDrivers.updateMargins(left = leftInsets, right = rightInsets) + binding.listDrivers.updateMargins(left = leftInsets, right = rightInsets) + + binding.listDrivers.updatePadding( + bottom = barInsets.bottom + resources.getDimensionPixelSize( + R.dimen.spacing_bottom_list_fab + ) + ) + + windowInsets + } + + data class Artifact(val url: URL, val name: String) + + data class Release( + var tagName: String = "", + var titleName: String = "", + var title: String = "", + var body: String = "", + var artifacts: List = ArrayList(), + var prerelease: Boolean = false, + var latest: Boolean = false, + var publishTime: LocalDateTime = LocalDateTime.now() + ) { + companion object { + fun fromJsonArray( + jsonString: String, + useTagName: Boolean, + sortMode: SortMode + ): ArrayList { + val mapper = jacksonObjectMapper() + + try { + val rootNode = mapper.readTree(jsonString) + + val releases = ArrayList() + + var latestRelease: Release? = null + + if (rootNode.isArray) { + rootNode.forEach { node -> + val release = fromJson(node, useTagName) + + if (latestRelease == null && !release.prerelease) { + latestRelease = release + release.latest = true + } + + releases.add(release) + } + } + + when (sortMode) { + SortMode.PublishTime -> releases.sortByDescending { + it.publishTime + } + + else -> {} + } + + return releases + } catch (e: Exception) { + e.printStackTrace() + return ArrayList() + } + } + + private fun fromJson(node: JsonNode, useTagName: Boolean): Release { + try { + val tagName = node.get("tag_name").toString().removeSurrounding("\"") + val body = node.get("body").toString().removeSurrounding("\"") + val prerelease = node.get("prerelease").toString().toBoolean() + val titleName = node.get("name").toString().removeSurrounding("\"") + + val published = node.get("published_at").toString().removeSurrounding("\"") + val instantTime: Instant? = Instant.parse(published) + val localTime = instantTime?.atZone(ZoneId.systemDefault())?.toLocalDateTime() ?: LocalDateTime.now() + + val title = if (useTagName) tagName else titleName + + val assets = node.get("assets") + val artifacts = ArrayList() + if (assets?.isArray == true) { + assets.forEach { subNode -> + val urlStr = subNode.get("browser_download_url").toString() + .removeSurrounding("\"") + + val url = URL(urlStr) + val name = subNode.get("name").toString().removeSurrounding("\"") + + val artifact = Artifact(url, name) + artifacts.add(artifact) + } + } + + return Release( + tagName, + titleName, + title, + body, + artifacts, + prerelease, + false, + localTime + ) + } catch (e: Exception) { + // TODO: handle malformed input. + e.printStackTrace() + } + + return Release() + } + } + } + + data class DriverGroup( + val name: String, + val releases: ArrayList, + val sort: Int + ) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt index c58c02506c..f521343272 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.fragments @@ -8,6 +8,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.edit import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding @@ -15,6 +16,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs +import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.transition.MaterialSharedAxis import kotlinx.coroutines.Dispatchers @@ -23,6 +25,7 @@ import kotlinx.coroutines.withContext import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.adapters.DriverAdapter import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding +import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.StringSetting import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver import org.yuzu.yuzu_emu.model.DriverViewModel @@ -103,6 +106,12 @@ class DriverManagerFragment : Fragment() { getDriver.launch(arrayOf("application/zip")) } + binding.buttonFetch.setOnClickListener { + binding.root.findNavController().navigate( + R.id.action_driverManagerFragment_to_driverFetcherFragment + ) + } + binding.listDrivers.apply { layoutManager = GridLayoutManager( requireContext(), @@ -112,6 +121,10 @@ class DriverManagerFragment : Fragment() { } setInsets() + + if (!GpuDriverHelper.supportsCustomDriverLoading()) { + showDriverWarningDialog() + } } override fun onDestroy() { @@ -139,6 +152,12 @@ class DriverManagerFragment : Fragment() { bottom = barInsets.bottom + fabSpacing ) + binding.buttonFetch.updateMargins( + left = leftInsets + fabSpacing, + right = rightInsets + fabSpacing, + bottom = barInsets.bottom + fabSpacing + ) + binding.listDrivers.updatePadding( bottom = barInsets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) @@ -195,4 +214,26 @@ class DriverManagerFragment : Fragment() { return@newInstance Any() }.show(childFragmentManager, ProgressDialogFragment.TAG) } + + fun showDriverWarningDialog() { + val shouldDisplayGpuWarning = + PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getBoolean(Settings.PREF_SHOULD_SHOW_DRIVER_WARNING, true) + if (shouldDisplayGpuWarning) { + MessageDialogFragment.newInstance( + activity, + titleId = R.string.unsupported_gpu, + descriptionId = R.string.unsupported_gpu_warning, + positiveButtonTitleId = R.string.dont_show_again, + negativeButtonTitleId = R.string.close, + showNegativeButton = true, + positiveAction = { + PreferenceManager.getDefaultSharedPreferences(requireContext()) + .edit() { + putBoolean(Settings.PREF_SHOULD_SHOW_DRIVER_WARNING, false) + } + } + ).show(requireActivity().supportFragmentManager, MessageDialogFragment.TAG) + } + } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index ddf3cb1fe1..b2d6135372 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -1,9 +1,8 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later package org.yuzu.yuzu_emu.fragments @@ -16,9 +15,10 @@ import android.content.Intent import android.content.IntentFilter import android.content.pm.ActivityInfo import android.content.res.Configuration -import android.graphics.Color import android.net.Uri import android.os.BatteryManager +import android.os.BatteryManager.* +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper @@ -45,6 +45,7 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout.DrawerListener import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs import androidx.window.layout.FoldingFeature @@ -52,7 +53,7 @@ import androidx.window.layout.WindowInfoTracker import androidx.window.layout.WindowLayoutInfo import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.slider.Slider +import com.google.android.material.textview.MaterialTextView import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R @@ -74,32 +75,52 @@ import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.FileUtil import org.yuzu.yuzu_emu.utils.GameHelper import org.yuzu.yuzu_emu.utils.GameIconUtils +import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.ViewUtils import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.utils.collect +import org.yuzu.yuzu_emu.utils.CustomSettingsHandler +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.Dispatchers +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine import java.io.File class EmulationFragment : Fragment(), SurfaceHolder.Callback { private lateinit var emulationState: EmulationState private var emulationActivity: EmulationActivity? = null - private var perfStatsUpdater: (() -> Unit)? = null - private lateinit var cpuBackend: String - private lateinit var gpuDriver: String + private var perfStatsUpdater: (() -> Unit)? = null + private var socUpdater: (() -> Unit)? = null + + val handler = Handler(Looper.getMainLooper()) + private var isOverlayVisible = true private var _binding: FragmentEmulationBinding? = null + private val binding get() = _binding!! private val args by navArgs() - private lateinit var game: Game + private var game: Game? = null private val emulationViewModel: EmulationViewModel by activityViewModels() private val driverViewModel: DriverViewModel by activityViewModels() private var isInFoldableLayout = false + private var emulationStarted = false + + private lateinit var gpuModel: String + private lateinit var fwVersion: String + + private var intentGame: Game? = null + private var isCustomSettingsIntent = false + + private var perfStatsRunnable: Runnable? = null + private var socRunnable: Runnable? = null override fun onAttach(context: Context) { super.onAttach(context) @@ -118,9 +139,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { super.onCreate(savedInstanceState) updateOrientation() - val intentUri: Uri? = requireActivity().intent.data - var intentGame: Game? = null - if (intentUri != null) { + val intent = requireActivity().intent + val intentUri: Uri? = intent.data + intentGame = null + isCustomSettingsIntent = false + + if (intent.action == CustomSettingsHandler.CUSTOM_CONFIG_ACTION) { + handleEmuReadyIntent(intent) + return + } else if (intentUri != null) { intentGame = if (Game.extensions.contains(FileUtil.getExtension(intentUri))) { GameHelper.getGame(requireActivity().intent.data!!, false) } else { @@ -128,40 +155,329 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } + finishGameSetup() + } + + /** + * Complete the game setup process (extracted for async custom settings handling) + */ + private fun finishGameSetup() { try { - game = if (args.game != null) { - args.game!! - } else { - intentGame!! + val gameToUse = args.game ?: intentGame + + if (gameToUse == null) { + Log.error("[EmulationFragment] No game found in arguments or intent") + Toast.makeText( + requireContext(), + R.string.no_game_present, + Toast.LENGTH_SHORT + ).show() + requireActivity().finish() + return } - } catch (e: NullPointerException) { + + game = gameToUse + } catch (e: Exception) { + Log.error("[EmulationFragment] Error during game setup: ${e.message}") Toast.makeText( requireContext(), - R.string.no_game_present, + "Setup error: ${e.message?.take(30) ?: "Unknown"}", Toast.LENGTH_SHORT ).show() requireActivity().finish() return } - // Always load custom settings when launching a game from an intent - if (args.custom || intentGame != null) { - SettingsFile.loadCustomConfig(game) - NativeConfig.unloadPerGameConfig() - } else { - NativeConfig.reloadGlobalConfig() + try { + when { + // Game launched via intent (check for existing custom config) + intentGame != null -> { + game?.let { gameInstance -> + val customConfigFile = SettingsFile.getCustomSettingsFile(gameInstance) + if (customConfigFile.exists()) { + Log.info( + "[EmulationFragment] Found existing custom settings for ${gameInstance.title}, loading them" + ) + SettingsFile.loadCustomConfig(gameInstance) + } else { + Log.info( + "[EmulationFragment] No custom settings found for ${gameInstance.title}, using global settings" + ) + NativeConfig.reloadGlobalConfig() + } + } ?: run { + Log.info("[EmulationFragment] No game available, using global settings") + NativeConfig.reloadGlobalConfig() + } + } + + // Normal game launch from arguments + else -> { + val shouldUseCustom = game?.let { it == args.game && args.custom } ?: false + + if (shouldUseCustom) { + SettingsFile.loadCustomConfig(game!!) + NativeConfig.unloadPerGameConfig() + Log.info("[EmulationFragment] Loading custom settings for ${game!!.title}") + } else { + Log.info("[EmulationFragment] Using global settings") + NativeConfig.reloadGlobalConfig() + } + } + } + } catch (e: Exception) { + Log.error("[EmulationFragment] Error loading configuration: ${e.message}") + Log.info("[EmulationFragment] Falling back to global settings") + try { + NativeConfig.reloadGlobalConfig() + } catch (fallbackException: Exception) { + Log.error( + "[EmulationFragment] Critical error: could not load global config: ${fallbackException.message}" + ) + throw fallbackException + } } - // Install the selected driver asynchronously as the game starts - driverViewModel.onLaunchGame() - - // So this fragment doesn't restart on configuration changes; i.e. rotation. - retainInstance = true - emulationState = EmulationState(game.path) { + emulationState = EmulationState(game!!.path) { return@EmulationState driverViewModel.isInteractionAllowed.value } } + /** + * Handle EmuReady intent for launching games with or without custom settings + */ + private fun handleEmuReadyIntent(intent: Intent) { + val titleId = intent.getStringExtra(CustomSettingsHandler.EXTRA_TITLE_ID) + val customSettings = intent.getStringExtra(CustomSettingsHandler.EXTRA_CUSTOM_SETTINGS) + + if (titleId != null) { + Log.info("[EmulationFragment] Received EmuReady intent for title: $titleId") + + lifecycleScope.launch { + try { + Toast.makeText( + requireContext(), + getString(R.string.searching_for_game), + Toast.LENGTH_SHORT + ).show() + val foundGame = CustomSettingsHandler.findGameByTitleId( + titleId, + requireContext() + ) + if (foundGame == null) { + Log.error("[EmulationFragment] Game not found for title ID: $titleId") + Toast.makeText( + requireContext(), + getString(R.string.game_not_found_for_title_id, titleId), + Toast.LENGTH_LONG + ).show() + requireActivity().finish() + return@launch + } + + val shouldLaunch = showLaunchConfirmationDialog( + foundGame.title, + customSettings != null + ) + if (!shouldLaunch) { + Log.info("[EmulationFragment] User cancelled EmuReady launch") + requireActivity().finish() + return@launch + } + + if (customSettings != null) { + intentGame = CustomSettingsHandler.applyCustomSettingsWithDriverCheck( + titleId, + customSettings, + requireContext(), + requireActivity(), + driverViewModel + ) + + if (intentGame == null) { + Log.error( + "[EmulationFragment] Custom settings processing failed for title ID: $titleId" + ) + Toast.makeText( + requireContext(), + getString(R.string.custom_settings_failed_title), + Toast.LENGTH_SHORT + ).show() + + val launchWithDefault = askUserToLaunchWithDefaultSettings( + foundGame.title, + getString(R.string.custom_settings_failure_reasons) + ) + + if (launchWithDefault) { + Log.info( + "[EmulationFragment] User chose to launch with default settings" + ) + Toast.makeText( + requireContext(), + getString(R.string.launch_with_default_settings), + Toast.LENGTH_SHORT + ).show() + intentGame = foundGame + isCustomSettingsIntent = false + } else { + Log.info( + "[EmulationFragment] User cancelled launch after custom settings failure" + ) + Toast.makeText( + requireContext(), + getString(R.string.launch_cancelled), + Toast.LENGTH_SHORT + ).show() + requireActivity().finish() + return@launch + } + } else { + isCustomSettingsIntent = true + } + } else { + Log.info("[EmulationFragment] Launching game with default settings") + + val customConfigFile = SettingsFile.getCustomSettingsFile(foundGame) + if (customConfigFile.exists()) { + Log.info( + "[EmulationFragment] Found existing custom settings for ${foundGame.title}, loading them" + ) + SettingsFile.loadCustomConfig(foundGame) + } else { + Log.info( + "[EmulationFragment] No custom settings found for ${foundGame.title}, using global settings" + ) + } + + Toast.makeText( + requireContext(), + getString(R.string.launching_game, foundGame.title), + Toast.LENGTH_SHORT + ).show() + intentGame = foundGame + isCustomSettingsIntent = false + } + + if (intentGame != null) { + withContext(Dispatchers.Main) { + try { + finishGameSetup() + Log.info( + "[EmulationFragment] Game setup complete for intent launch" + ) + + if (_binding != null) { + // Hide loading indicator immediately for intent launches + binding.loadingIndicator.visibility = View.GONE + binding.surfaceEmulation.visibility = View.VISIBLE + + completeViewSetup() + + // For intent launches, check if surface is ready and start emulation + binding.root.post { + if (binding.surfaceEmulation.holder.surface?.isValid == true && !emulationStarted) { + emulationStarted = true + emulationState.newSurface( + binding.surfaceEmulation.holder.surface + ) + } + } + } + } catch (e: Exception) { + Log.error( + "[EmulationFragment] Error in finishGameSetup: ${e.message}" + ) + requireActivity().finish() + return@withContext + } + } + } else { + Log.error("[EmulationFragment] No valid game found after processing intent") + Toast.makeText( + requireContext(), + getString(R.string.failed_to_initialize_game), + Toast.LENGTH_SHORT + ).show() + requireActivity().finish() + } + } catch (e: Exception) { + Log.error("[EmulationFragment] Error processing EmuReady intent: ${e.message}") + Toast.makeText( + requireContext(), + "Error: ${e.message?.take(50) ?: "Unknown error"}", + Toast.LENGTH_LONG + ).show() + requireActivity().finish() + } + } + } else { + Log.error("[EmulationFragment] EmuReady intent missing title_id") + Toast.makeText( + requireContext(), + "Invalid request: missing title ID", + Toast.LENGTH_SHORT + ).show() + requireActivity().finish() + } + } + + /** + * Show confirmation dialog for EmuReady game launches + */ + private suspend fun showLaunchConfirmationDialog(gameTitle: String, hasCustomSettings: Boolean): Boolean { + return suspendCoroutine { continuation -> + requireActivity().runOnUiThread { + val message = if (hasCustomSettings) { + getString( + R.string.custom_intent_launch_message_with_settings, + gameTitle + ) + } else { + getString(R.string.custom_intent_launch_message, gameTitle) + } + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.custom_intent_launch_title)) + .setMessage(message) + .setPositiveButton(getString(R.string.launch)) { _, _ -> + continuation.resume(true) + } + .setNegativeButton(getString(R.string.cancel)) { _, _ -> + continuation.resume(false) + } + .setCancelable(false) + .show() + } + } + } + + /** + * Ask user if they want to launch with default settings when custom settings fail + */ + private suspend fun askUserToLaunchWithDefaultSettings( + gameTitle: String, + errorMessage: String + ): Boolean { + return suspendCoroutine { continuation -> + requireActivity().runOnUiThread { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.custom_settings_failed_title)) + .setMessage( + getString(R.string.custom_settings_failed_message, gameTitle, errorMessage) + ) + .setPositiveButton(getString(R.string.launch_with_default_settings)) { _, _ -> + continuation.resume(true) + } + .setNegativeButton(getString(R.string.cancel)) { _, _ -> + continuation.resume(false) + } + .setCancelable(false) + .show() + } + } + } + /** * Initialize the UI and start emulation in here. */ @@ -180,6 +496,27 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { return } + if (game == null) { + Log.warning( + "[EmulationFragment] Game not yet initialized in onViewCreated - will be set up by async intent handler" + ) + return + } + + completeViewSetup() + } + + private fun completeViewSetup() { + if (_binding == null || game == null) { + return + } + Log.info("[EmulationFragment] Starting view setup for game: ${game?.title}") + + gpuModel = GpuDriverHelper.getGpuModel().toString() + fwVersion = NativeLibrary.firmwareVersion() + + updateQuickOverlayMenuEntry(BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()) + binding.surfaceEmulation.holder.addCallback(this) binding.doneControlConfig.setOnClickListener { stopConfiguringControls() } @@ -201,6 +538,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) binding.inGameMenu.requestFocus() emulationViewModel.setDrawerOpen(true) + updateQuickOverlayMenuEntry(BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()) } override fun onDrawerClosed(drawerView: View) { @@ -213,10 +551,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } }) binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - binding.inGameMenu.getHeaderView(0).apply { - val titleView = findViewById(R.id.text_game_title) - titleView.text = game.title - } + + updateGameTitle() binding.inGameMenu.menu.findItem(R.id.menu_lock_drawer).apply { val lockMode = IntSetting.LOCK_DRAWER.getInt() @@ -244,25 +580,24 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { R.id.menu_pause_emulation -> { if (emulationState.isPaused) { emulationState.run(false) - it.title = resources.getString(R.string.emulation_pause) - it.icon = ResourcesCompat.getDrawable( - resources, - R.drawable.ic_pause, - requireContext().theme - ) + updatePauseMenuEntry(false) } else { emulationState.pause() - it.title = resources.getString(R.string.emulation_unpause) - it.icon = ResourcesCompat.getDrawable( - resources, - R.drawable.ic_play, - requireContext().theme - ) + updatePauseMenuEntry(true) } binding.inGameMenu.requestFocus() true } + R.id.menu_quick_overlay -> { + val newState = !BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean() + BooleanSetting.SHOW_INPUT_OVERLAY.setBoolean(newState) + updateQuickOverlayMenuEntry(newState) + binding.surfaceInputOverlay.refreshControls() + NativeConfig.saveGlobalConfig() + true + } + R.id.menu_settings -> { val action = HomeNavigationDirections.actionGlobalSettingsActivity( null, @@ -283,13 +618,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { true } - R.id.menu_multiplayer -> { emulationActivity?.displayMultiplayerDialog() true } - R.id.menu_controls -> { val action = HomeNavigationDirections.actionGlobalSettingsActivity( null, @@ -358,8 +691,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } ) - GameIconUtils.loadGameIcon(game, binding.loadingImage) - binding.loadingTitle.text = game.title + GameIconUtils.loadGameIcon(game!!, binding.loadingImage) + binding.loadingTitle.text = game!!.title binding.loadingTitle.isSelected = true binding.loadingText.isSelected = true @@ -398,24 +731,27 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { emulationState.updateSurface() - // Setup overlays - updateshowStatsOvelray() + updateShowStatsOverlay() + updateSocOverlay() + + initializeOverlayAutoHide() // Re update binding when the specs values get initialized properly binding.inGameMenu.getHeaderView(0).apply { val titleView = findViewById(R.id.text_game_title) val cpuBackendLabel = findViewById(R.id.cpu_backend) - val gpuvendorLabel = findViewById(R.id.gpu_vendor) + val vendorLabel = findViewById(R.id.gpu_vendor) - titleView.text = game.title + titleView.text = game?.title ?: "" cpuBackendLabel.text = NativeLibrary.getCpuBackend() - gpuvendorLabel.text = NativeLibrary.getGpuDriver() + vendorLabel.text = NativeLibrary.getGpuDriver() } - val position = IntSetting.PERF_OVERLAY_POSITION.getInt() updateStatsPosition(position) + val socPosition = IntSetting.SOC_OVERLAY_POSITION.getInt() + updateSocPosition(socPosition) } } emulationViewModel.isEmulationStopping.collect(viewLifecycleOwner) { @@ -444,11 +780,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { ViewUtils.showView(binding.loadingIndicator) } } - emulationViewModel.emulationStopped.collect(viewLifecycleOwner) { - if (it && emulationViewModel.programChanged.value != -1) { - if (perfStatsUpdater != null) { - perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) + + emulationViewModel.emulationStopped.collect(viewLifecycleOwner) { stopped -> + if (stopped && emulationViewModel.programChanged.value != -1) { + perfStatsRunnable?.let { runnable -> + perfStatsUpdateHandler.removeCallbacks( + runnable + ) } + socRunnable?.let { runnable -> socUpdateHandler.removeCallbacks(runnable) } emulationState.changeProgram(emulationViewModel.programChanged.value) emulationViewModel.setProgramChanged(-1) emulationViewModel.setEmulationStopped(false) @@ -456,8 +796,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } driverViewModel.isInteractionAllowed.collect(viewLifecycleOwner) { - if (it) startEmulation() + if (it && !NativeLibrary.isRunning() && !NativeLibrary.isPaused()) { + startEmulation() + } } + + driverViewModel.onLaunchGame() } private fun startEmulation(programIndex: Int = 0) { @@ -474,36 +818,87 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - if (_binding == null) { - return - } + val b = _binding ?: return updateScreenLayout() val showInputOverlay = BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean() if (emulationActivity?.isInPictureInPictureMode == true) { - if (binding.drawerLayout.isOpen) { - binding.drawerLayout.close() + if (b.drawerLayout.isOpen) { + b.drawerLayout.close() } if (showInputOverlay) { - binding.surfaceInputOverlay.setVisible(visible = false, gone = false) + b.surfaceInputOverlay.setVisible(visible = false, gone = false) } } else { - binding.surfaceInputOverlay.setVisible( + b.surfaceInputOverlay.setVisible( showInputOverlay && emulationViewModel.emulationStarted.value ) if (!isInFoldableLayout) { if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { - binding.surfaceInputOverlay.layout = OverlayLayout.Portrait + b.surfaceInputOverlay.layout = OverlayLayout.Portrait } else { - binding.surfaceInputOverlay.layout = OverlayLayout.Landscape + b.surfaceInputOverlay.layout = OverlayLayout.Landscape } } } } + private fun updateGameTitle() { + game?.let { + binding.inGameMenu.getHeaderView(0).apply { + val titleView = findViewById(R.id.text_game_title) + titleView.text = it.title + } + } + } + + private fun updateQuickOverlayMenuEntry(isVisible: Boolean) { + val b = _binding ?: return + val item = b.inGameMenu.menu.findItem(R.id.menu_quick_overlay) ?: return + + if (isVisible) { + item.title = getString(R.string.emulation_hide_overlay) + item.icon = ResourcesCompat.getDrawable( + resources, + R.drawable.ic_controller_disconnected, + requireContext().theme + ) + } else { + item.title = getString(R.string.emulation_show_overlay) + item.icon = ResourcesCompat.getDrawable( + resources, + R.drawable.ic_controller, + requireContext().theme + ) + } + } + + private fun updatePauseMenuEntry(isPaused: Boolean) { + val b = _binding ?: return + val pauseItem = b.inGameMenu.menu.findItem(R.id.menu_pause_emulation) ?: return + if (isPaused) { + pauseItem.title = getString(R.string.emulation_unpause) + pauseItem.icon = ResourcesCompat.getDrawable( + resources, + R.drawable.ic_play, + requireContext().theme + ) + } else { + pauseItem.title = getString(R.string.emulation_pause) + pauseItem.icon = ResourcesCompat.getDrawable( + resources, + R.drawable.ic_pause, + requireContext().theme + ) + } + } + override fun onPause() { - if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) { - emulationState.pause() + if (this::emulationState.isInitialized) { + if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) { + emulationState.pause() + updatePauseMenuEntry(true) + } } super.onPause() } @@ -517,11 +912,24 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { NativeLibrary.clearEmulationActivity() super.onDetach() } + override fun onResume() { super.onResume() - // If the overlay is enabled, we need to update the position if changed - val position = IntSetting.PERF_OVERLAY_POSITION.getInt() - updateStatsPosition(position) + val b = _binding ?: return + updateStatsPosition(IntSetting.PERF_OVERLAY_POSITION.getInt()) + updateSocPosition(IntSetting.SOC_OVERLAY_POSITION.getInt()) + + if (this::emulationState.isInitialized) { + b.inGameMenu.post { + if (!this::emulationState.isInitialized || _binding == null) return@post + updatePauseMenuEntry(emulationState.isPaused) + } + } + + // if the overlay auto-hide setting is changed while paused, + // we need to reinitialize the auto-hide timer + initializeOverlayAutoHide() + } private fun resetInputOverlay() { @@ -529,17 +937,19 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { IntSetting.OVERLAY_OPACITY.reset() binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.resetLayoutVisibilityAndPlacement() + binding.surfaceInputOverlay.resetIndividualControlScale() } } + @SuppressLint("DefaultLocale") - private fun updateshowStatsOvelray() { + private fun updateShowStatsOverlay() { val showOverlay = BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean() binding.showStatsOverlayText.apply { setTextColor( MaterialColors.getColor( this, com.google.android.material.R.attr.colorPrimary - ) + ) ) } binding.showStatsOverlayText.setVisible(showOverlay) @@ -553,14 +963,16 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { if (emulationViewModel.emulationStarted.value && !emulationViewModel.isEmulationStopping.value ) { + val needsGlobal = NativeConfig.isPerGameConfigLoaded() sb.setLength(0) val perfStats = NativeLibrary.getPerfStats() val actualFps = perfStats[FPS] - if (BooleanSetting.SHOW_FPS.getBoolean(NativeConfig.isPerGameConfigLoaded())) { - val enableFrameInterpolation = BooleanSetting.FRAME_INTERPOLATION.getBoolean() - val enableFrameSkipping = BooleanSetting.FRAME_SKIPPING.getBoolean() + if (BooleanSetting.SHOW_FPS.getBoolean(needsGlobal)) { + val enableFrameInterpolation = + BooleanSetting.FRAME_INTERPOLATION.getBoolean() +// val enableFrameSkipping = BooleanSetting.FRAME_SKIPPING.getBoolean() var fpsText = String.format("FPS: %.1f", actualFps) @@ -568,14 +980,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { fpsText += " " + getString(R.string.enhanced_fps_suffix) } - if (enableFrameSkipping) { - fpsText += " " + getString(R.string.skipping_fps_suffix) - } +// if (enableFrameSkipping) { +// fpsText += " " + getString(R.string.skipping_fps_suffix) +// } sb.append(fpsText) } - if (BooleanSetting.SHOW_FRAMETIME.getBoolean(NativeConfig.isPerGameConfigLoaded())) { + if (BooleanSetting.SHOW_FRAMETIME.getBoolean(needsGlobal)) { if (sb.isNotEmpty()) sb.append(" | ") sb.append( String.format( @@ -585,26 +997,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { ) } - if (BooleanSetting.SHOW_SPEED.getBoolean(NativeConfig.isPerGameConfigLoaded())) { + if (BooleanSetting.SHOW_APP_RAM_USAGE.getBoolean(needsGlobal)) { if (sb.isNotEmpty()) sb.append(" | ") - sb.append( - String.format( - "Speed: %d%%", - (perfStats[SPEED] * 100.0 + 0.5).toInt() - ) - ) + val appRamUsage = + File("/proc/self/statm").readLines()[0].split(' ')[1].toLong() * 4096 / 1000000 + sb.append(getString(R.string.process_ram, appRamUsage)) } - if (BooleanSetting.SHOW_APP_RAM_USAGE.getBoolean(NativeConfig.isPerGameConfigLoaded())) { - if (sb.isNotEmpty()) sb.append(" | ") - val appRamUsage = File("/proc/self/statm").readLines()[0].split(' ')[1].toLong() * 4096 / 1000000 - sb.append("Process RAM: $appRamUsage MB") - } - - if (BooleanSetting.SHOW_SYSTEM_RAM_USAGE.getBoolean(NativeConfig.isPerGameConfigLoaded())) { + if (BooleanSetting.SHOW_SYSTEM_RAM_USAGE.getBoolean(needsGlobal)) { if (sb.isNotEmpty()) sb.append(" | ") context?.let { ctx -> - val activityManager = ctx.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val activityManager = + ctx.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager val memInfo = ActivityManager.MemoryInfo() activityManager.getMemoryInfo(memInfo) val usedRamMB = (memInfo.totalMem - memInfo.availMem) / 1048576L @@ -612,33 +1016,84 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } - if (BooleanSetting.SHOW_BAT_TEMPERATURE.getBoolean(NativeConfig.isPerGameConfigLoaded())) { + if (BooleanSetting.SHOW_BAT_TEMPERATURE.getBoolean(needsGlobal)) { if (sb.isNotEmpty()) sb.append(" | ") + val batteryTemp = getBatteryTemperature() - val tempF = celsiusToFahrenheit(batteryTemp) - sb.append(String.format("%.1f°C/%.1f°F", batteryTemp, tempF)) + when (IntSetting.BAT_TEMPERATURE_UNIT.getInt(needsGlobal)) { + 0 -> sb.append(String.format("%.1f°C", batteryTemp)) + 1 -> sb.append( + String.format( + "%.1f°F", + celsiusToFahrenheit(batteryTemp) + ) + ) + } } - if (BooleanSetting.OVERLAY_BACKGROUND.getBoolean(NativeConfig.isPerGameConfigLoaded())) { - binding.showStatsOverlayText.setBackgroundResource(R.color.yuzu_transparent_black) + if (BooleanSetting.SHOW_POWER_INFO.getBoolean(needsGlobal)) { + if (sb.isNotEmpty()) sb.append(" | ") + + val battery: BatteryManager = + requireContext().getSystemService(Context.BATTERY_SERVICE) as BatteryManager + val batteryIntent = requireContext().registerReceiver( + null, + IntentFilter(Intent.ACTION_BATTERY_CHANGED) + ) + + val capacity = battery.getIntProperty(BATTERY_PROPERTY_CAPACITY) + val nowUAmps = battery.getIntProperty(BATTERY_PROPERTY_CURRENT_NOW) + + sb.append(String.format("%.1fA (%d%%)", nowUAmps / 1000000.0, capacity)) + + val status = batteryIntent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || + status == BatteryManager.BATTERY_STATUS_FULL + + if (isCharging) { + sb.append(" ${getString(R.string.charging)}") + } + } + + val shadersBuilding = NativeLibrary.getShadersBuilding() + + if (BooleanSetting.SHOW_SHADERS_BUILDING.getBoolean(needsGlobal) && shadersBuilding != 0) { + if (sb.isNotEmpty()) sb.append(" | ") + + val prefix = getString(R.string.shaders_prefix) + val suffix = getString(R.string.shaders_suffix) + sb.append(String.format("$prefix %d $suffix", shadersBuilding)) + } + + if (BooleanSetting.PERF_OVERLAY_BACKGROUND.getBoolean(needsGlobal)) { + binding.showStatsOverlayText.setBackgroundResource( + R.color.yuzu_transparent_black + ) } else { binding.showStatsOverlayText.setBackgroundResource(0) } binding.showStatsOverlayText.text = sb.toString() } - perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 800) + perfStatsUpdateHandler.postDelayed(perfStatsRunnable!!, 800) } - perfStatsUpdateHandler.post(perfStatsUpdater!!) + perfStatsRunnable = Runnable { perfStatsUpdater?.invoke() } + perfStatsUpdateHandler.post(perfStatsRunnable!!) } else { - if (perfStatsUpdater != null) { - perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) - } + perfStatsRunnable?.let { perfStatsUpdateHandler.removeCallbacks(it) } } } private fun updateStatsPosition(position: Int) { - val params = binding.showStatsOverlayText.layoutParams as FrameLayout.LayoutParams + updateOverlayPosition(binding.showStatsOverlayText, position) + } + + private fun updateSocPosition(position: Int) { + updateOverlayPosition(binding.showSocOverlayText, position) + } + + private fun updateOverlayPosition(overlay: MaterialTextView, position: Int) { + val params = overlay.layoutParams as FrameLayout.LayoutParams when (position) { 0 -> { params.gravity = (Gravity.TOP or Gravity.START) @@ -672,7 +1127,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private fun getBatteryTemperature(): Float { try { - val batteryIntent = requireContext().registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) + val batteryIntent = + requireContext().registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) // Temperature in tenths of a degree Celsius val temperature = batteryIntent?.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) ?: 0 // Convert to degrees Celsius @@ -681,32 +1137,88 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { return 0.0f } } + private fun celsiusToFahrenheit(celsius: Float): Float { return (celsius * 9 / 5) + 32 } - private fun updateThermalOverlay(temperature: Float) { - if (BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean() && - emulationViewModel.emulationStarted.value && - !emulationViewModel.isEmulationStopping.value - ) { - // Convert to Fahrenheit - val fahrenheit = (temperature * 9f / 5f) + 32f + private fun updateSocOverlay() { + val showOverlay = BooleanSetting.SHOW_SOC_OVERLAY.getBoolean() + binding.showSocOverlayText.apply { + setTextColor( + MaterialColors.getColor( + this, + com.google.android.material.R.attr.colorPrimary + ) + ) + } + binding.showSocOverlayText.setVisible(showOverlay) - // Determine color based on temperature ranges - val color = when { - temperature < 35 -> Color.parseColor("#00C8FF") - temperature < 40 -> Color.parseColor("#A146FF") - temperature < 45 -> Color.parseColor("#FFA500") - else -> Color.RED + if (showOverlay) { + val sb = StringBuilder() + + socUpdater = { + if (emulationViewModel.emulationStarted.value && + !emulationViewModel.isEmulationStopping.value + ) { + sb.setLength(0) + + if (BooleanSetting.SHOW_DEVICE_MODEL.getBoolean( + NativeConfig.isPerGameConfigLoaded() + ) + ) { + sb.append(Build.MODEL) + } + + if (BooleanSetting.SHOW_GPU_MODEL.getBoolean( + NativeConfig.isPerGameConfigLoaded() + ) + ) { + if (sb.isNotEmpty()) sb.append(" | ") + sb.append(gpuModel) + } + + if (Build.VERSION.SDK_INT >= 31) { + if (BooleanSetting.SHOW_SOC_MODEL.getBoolean( + NativeConfig.isPerGameConfigLoaded() + ) + ) { + if (sb.isNotEmpty()) sb.append(" | ") + sb.append(Build.SOC_MODEL) + } + } + + if (BooleanSetting.SHOW_FW_VERSION.getBoolean( + NativeConfig.isPerGameConfigLoaded() + ) + ) { + if (sb.isNotEmpty()) sb.append(" | ") + sb.append(fwVersion) + } + + binding.showSocOverlayText.text = sb.toString() + + if (BooleanSetting.SOC_OVERLAY_BACKGROUND.getBoolean( + NativeConfig.isPerGameConfigLoaded() + ) + ) { + binding.showSocOverlayText.setBackgroundResource( + R.color.yuzu_transparent_black + ) + } else { + binding.showSocOverlayText.setBackgroundResource(0) + } + } + + socUpdateHandler.postDelayed(socRunnable!!, 1000) } - - binding.showThermalsText.setTextColor(color) - binding.showThermalsText.text = String.format("%.1f°C • %.1f°F", temperature, fahrenheit) + socRunnable = Runnable { socUpdater?.invoke() } + socUpdateHandler.post(socRunnable!!) + } else { + socRunnable?.let { socUpdateHandler.removeCallbacks(it) } } } - @SuppressLint("SourceLockedOrientationActivity") private fun updateOrientation() { emulationActivity?.let { @@ -732,6 +1244,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } private fun updateScreenLayout() { + val b = _binding ?: return val verticalAlignment = EmulationVerticalAlignment.from(IntSetting.VERTICAL_ALIGNMENT.getInt()) val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.getInt()) { @@ -743,35 +1256,37 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } when (verticalAlignment) { EmulationVerticalAlignment.Top -> { - binding.surfaceEmulation.setAspectRatio(aspectRatio) + b.surfaceEmulation.setAspectRatio(aspectRatio) val params = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) params.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL - binding.surfaceEmulation.layoutParams = params + b.surfaceEmulation.layoutParams = params } EmulationVerticalAlignment.Center -> { - binding.surfaceEmulation.setAspectRatio(null) - binding.surfaceEmulation.updateLayoutParams { + b.surfaceEmulation.setAspectRatio(null) + b.surfaceEmulation.updateLayoutParams { width = ViewGroup.LayoutParams.MATCH_PARENT height = ViewGroup.LayoutParams.MATCH_PARENT } } EmulationVerticalAlignment.Bottom -> { - binding.surfaceEmulation.setAspectRatio(aspectRatio) + b.surfaceEmulation.setAspectRatio(aspectRatio) val params = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) params.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL - binding.surfaceEmulation.layoutParams = params + b.surfaceEmulation.layoutParams = params } } - emulationState.updateSurface() + if (this::emulationState.isInitialized) { + emulationState.updateSurface() + } emulationActivity?.buildPictureInPictureParams() updateOrientation() } @@ -818,11 +1333,34 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height) - emulationState.newSurface(holder.surface) + if (!emulationStarted) { + emulationStarted = true + + // For intent launches, wait for driver initialization to complete + if (isCustomSettingsIntent || intentGame != null) { + if (!driverViewModel.isInteractionAllowed.value) { + Log.info("[EmulationFragment] Intent launch: waiting for driver initialization") + // Driver is still initializing, wait for it + lifecycleScope.launch { + driverViewModel.isInteractionAllowed.collect { allowed -> + if (allowed && holder.surface.isValid) { + emulationState.newSurface(holder.surface) + } + } + } + return + } + } + + emulationState.newSurface(holder.surface) + } else { + emulationState.newSurface(holder.surface) + } } override fun surfaceDestroyed(holder: SurfaceHolder) { emulationState.clearSurface() + emulationStarted = false } private fun showOverlayOptions() { @@ -834,6 +1372,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { popup.menu.apply { findItem(R.id.menu_show_stats_overlay).isChecked = BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean() + findItem(R.id.menu_show_soc_overlay).isChecked = + BooleanSetting.SHOW_SOC_OVERLAY.getBoolean() findItem(R.id.menu_rel_stick_center).isChecked = BooleanSetting.JOYSTICK_REL_CENTER.getBoolean() findItem(R.id.menu_dpad_slide).isChecked = BooleanSetting.DPAD_SLIDE.getBoolean() @@ -849,9 +1389,17 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { R.id.menu_show_stats_overlay -> { it.isChecked = !it.isChecked BooleanSetting.SHOW_PERFORMANCE_OVERLAY.setBoolean(it.isChecked) - updateshowStatsOvelray() + updateShowStatsOverlay() true } + + R.id.menu_show_soc_overlay -> { + it.isChecked = !it.isChecked + BooleanSetting.SHOW_SOC_OVERLAY.setBoolean(it.isChecked) + updateSocOverlay() + true + } + R.id.menu_edit_overlay -> { binding.drawerLayout.close() binding.surfaceInputOverlay.requestFocus() @@ -908,6 +1456,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { R.id.menu_show_overlay -> { it.isChecked = !it.isChecked BooleanSetting.SHOW_INPUT_OVERLAY.setBoolean(it.isChecked) + updateQuickOverlayMenuEntry(it.isChecked) binding.surfaceInputOverlay.refreshControls() true } @@ -985,22 +1534,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { inputScaleSlider.apply { valueTo = 150F value = IntSetting.OVERLAY_SCALE.getInt().toFloat() - addOnChangeListener( - Slider.OnChangeListener { _, value, _ -> - inputScaleValue.text = "${value.toInt()}%" - setControlScale(value.toInt()) - } - ) + addOnChangeListener { _, value, _ -> + inputScaleValue.text = "${value.toInt()}%" + setControlScale(value.toInt()) + } } inputOpacitySlider.apply { valueTo = 100F value = IntSetting.OVERLAY_OPACITY.getInt().toFloat() - addOnChangeListener( - Slider.OnChangeListener { _, value, _ -> - inputOpacityValue.text = "${value.toInt()}%" - setControlOpacity(value.toInt()) - } - ) + addOnChangeListener { _, value, _ -> + inputOpacityValue.text = "${value.toInt()}%" + setControlOpacity(value.toInt()) + } } inputScaleValue.text = "${inputScaleSlider.value.toInt()}%" inputOpacityValue.text = "${inputOpacitySlider.value.toInt()}%" @@ -1015,6 +1560,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { .setNeutralButton(R.string.slider_default) { _: DialogInterface?, _: Int -> setControlScale(50) setControlOpacity(100) + binding.surfaceInputOverlay.resetIndividualControlScale() } .show() } @@ -1036,7 +1582,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { val cutInsets: Insets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) var left = 0 var right = 0 - if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) { + if (v.layoutDirection == View.LAYOUT_DIRECTION_LTR) { left = cutInsets.left } else { right = cutInsets.right @@ -1057,7 +1603,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { lateinit var emulationThread: Thread init { - // Starting state is stopped. state = State.STOPPED } @@ -1065,7 +1610,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { val isStopped: Boolean get() = state == State.STOPPED - // Getters for the current state @get:Synchronized val isPaused: Boolean get() = state == State.PAUSED @@ -1085,7 +1629,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } - // State changing methods @Synchronized fun pause() { if (state != State.PAUSED) { @@ -1196,6 +1739,63 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { companion object { private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!) - private val thermalStatsUpdateHandler = Handler(Looper.myLooper()!!) + private val socUpdateHandler = Handler(Looper.myLooper()!!) + } + + private fun startOverlayAutoHideTimer(seconds: Int) { + handler.removeCallbacksAndMessages(null) + + handler.postDelayed({ + if (isOverlayVisible) { + hideOverlay() + } + }, seconds * 1000L) + } + + fun handleScreenTap(isLongTap: Boolean) { + val autoHideSeconds = IntSetting.INPUT_OVERLAY_AUTO_HIDE.getInt() + val shouldProceed = BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean() && BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE.getBoolean() + + if (!shouldProceed) { + return + } + + // failsafe + if (autoHideSeconds == 0) { + showOverlay() + return + } + + if (!isOverlayVisible && !isLongTap) { + showOverlay() + } + + startOverlayAutoHideTimer(autoHideSeconds) + } + + private fun initializeOverlayAutoHide() { + val autoHideSeconds = IntSetting.INPUT_OVERLAY_AUTO_HIDE.getInt() + val autoHideEnabled = BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE.getBoolean() + val showOverlay = BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean() + + if (autoHideEnabled && showOverlay) { + showOverlay() + startOverlayAutoHideTimer(autoHideSeconds) + } + } + + + fun showOverlay() { + if (!isOverlayVisible) { + isOverlayVisible = true + ViewUtils.showView(binding.surfaceInputOverlay, 500) + } + } + + private fun hideOverlay() { + if (isOverlayVisible) { + isOverlayVisible = false + ViewUtils.hideView(binding.surfaceInputOverlay, 500) + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt index 83ae5622a2..87b1533408 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.fragments diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt index 3cdee36d9c..7863e40ff5 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.fragments diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt index 8dbd69121b..f55edb418e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.fragments diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 2a0ed2f639..5763f3120f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -1,10 +1,6 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project -// SPDX-License-Identifier: GPL-3.0-or-later - - package org.yuzu.yuzu_emu.fragments import android.Manifest @@ -31,7 +27,6 @@ import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.transition.MaterialSharedAxis -import org.yuzu.yuzu_emu.BuildConfig import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R @@ -39,15 +34,14 @@ import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding import org.yuzu.yuzu_emu.features.DocumentProvider +import org.yuzu.yuzu_emu.features.fetcher.SpacingItemDecoration import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.HomeSetting import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.utils.FileUtil -import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.Log -import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins class HomeSettingsFragment : Fragment() { private var _binding: FragmentHomeSettingsBinding? = null @@ -116,7 +110,7 @@ class HomeSettingsFragment : Fragment() { .actionHomeSettingsFragmentToDriverManagerFragment(null) binding.root.findNavController().navigate(action) }, - { GpuDriverHelper.supportsCustomDriverLoading() }, + { true }, R.string.custom_driver_not_supported, R.string.custom_driver_not_supported_description, driverViewModel.selectedDriverTitle @@ -128,8 +122,8 @@ class HomeSettingsFragment : Fragment() { R.string.multiplayer_description, R.drawable.ic_two_users, { - val action = mainActivity.displayMultiplayerDialog() - }, + mainActivity.displayMultiplayerDialog() + } ) ) add( @@ -258,6 +252,8 @@ class HomeSettingsFragment : Fragment() { viewLifecycleOwner, optionsList ) + val spacing = resources.getDimensionPixelSize(R.dimen.spacing_small) + addItemDecoration(SpacingItemDecoration(spacing)) } setInsets() @@ -407,7 +403,7 @@ class HomeSettingsFragment : Fragment() { val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) binding.scrollViewSettings.updatePadding( - top = barInsets.top, + top = barInsets.top ) binding.homeSettingsList.updatePadding( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt index 9c39f7b166..c02411d1bb 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.fragments @@ -134,10 +134,10 @@ class InstallableFragment : Fragment() { install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) } ), Installable( - R.string.uninstall_firmware, - R.string.uninstall_firmware_description, - install = { mainActivity.uninstallFirmware() } - ), + R.string.uninstall_firmware, + R.string.uninstall_firmware_description, + install = { mainActivity.uninstallFirmware() } + ), Installable( R.string.install_prod_keys, R.string.install_prod_keys_description, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt index 5fdcea29fa..aa18aa2482 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt @@ -1,7 +1,6 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later - package org.yuzu.yuzu_emu.fragments import android.os.Bundle diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt index ee3bb0386a..26fcf7c0db 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt @@ -1,9 +1,13 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later package org.yuzu.yuzu_emu.fragments import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -28,6 +32,8 @@ class ProgressDialogFragment : DialogFragment() { private val PROGRESS_BAR_RESOLUTION = 1000 + var onDialogComplete: (() -> Unit)? = null + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val titleId = requireArguments().getInt(TITLE) val cancellable = requireArguments().getBoolean(CANCELLABLE) @@ -121,6 +127,11 @@ class ProgressDialogFragment : DialogFragment() { } } + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + onDialogComplete?.invoke() + } + companion object { const val TAG = "IndeterminateProgressDialogFragment" diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt index bf622b9299..b7c75c127f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.fragments @@ -78,7 +78,6 @@ class SetupFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { mainActivity = requireActivity() as MainActivity - requireActivity().onBackPressedDispatcher.addCallback( viewLifecycleOwner, object : OnBackPressedCallback(true) { @@ -166,6 +165,32 @@ class SetupFragment : Fragment() { } ) ) + add( + SetupPage( + R.drawable.ic_firmware, + R.string.firmware, + R.string.firmware_description, + R.drawable.ic_add, + true, + R.string.select_firmware, + { + firmwareCallback = it + getFirmware.launch(arrayOf("application/zip")) + }, + true, + R.string.install_firmware_warning, + R.string.install_firmware_warning_description, + R.string.install_firmware_warning_help, + { + if (NativeLibrary.isFirmwareAvailable()) { + StepState.COMPLETE + } else { + StepState.INCOMPLETE + } + } + ) + ) + add( SetupPage( R.drawable.ic_controller, @@ -321,17 +346,29 @@ class SetupFragment : Fragment() { } private lateinit var keyCallback: SetupCallback + private lateinit var firmwareCallback: SetupCallback val getProdKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> if (result != null) { - mainActivity.processKey(result) + mainActivity.processKey(result, "keys") if (NativeLibrary.areKeysPresent()) { keyCallback.onStepCompleted() } } } + val getFirmware = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result != null) { + mainActivity.processFirmware(result) { + if (NativeLibrary.isFirmwareAvailable()) { + firmwareCallback.onStepCompleted() + } + } + } + } + private lateinit var gamesDirCallback: SetupCallback val getGamesDirectory = diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/MidScreenSwipeRefreshLayout.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/MidScreenSwipeRefreshLayout.kt new file mode 100644 index 0000000000..35d027567e --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/MidScreenSwipeRefreshLayout.kt @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.ui + +import org.yuzu.yuzu_emu.R +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout + +class MidScreenSwipeRefreshLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : SwipeRefreshLayout(context, attrs) { + + private var startX = 0f + private var allowRefresh = false + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + when (ev.actionMasked) { + MotionEvent.ACTION_DOWN -> { + startX = ev.x + val width = width + val center_fraction = resources.getFraction( + R.fraction.carousel_midscreenswipe_width_fraction, + 1, + 1 + ).coerceIn(0f, 1f) + val leftBound = ((1 - center_fraction) / 2) * width + val rightBound = leftBound + (width * center_fraction) + allowRefresh = startX >= leftBound && startX <= rightBound + } + } + return if (allowRefresh) super.onInterceptTouchEvent(ev) else false + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt index 72a15ccef3..72ce006a7e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.model @@ -42,11 +42,16 @@ class GamesViewModel : ViewModel() { val searchFocused: StateFlow get() = _searchFocused private val _searchFocused = MutableStateFlow(false) + val shouldScrollAfterReload: StateFlow get() = _shouldScrollAfterReload + private val _shouldScrollAfterReload = MutableStateFlow(false) + private val _folders = MutableStateFlow(mutableListOf()) val folders = _folders.asStateFlow() private val _filteredGames = MutableStateFlow>(emptyList()) + var lastScrollPosition: Int = 0 + init { // Ensure keys are loaded so that ROM metadata can be decrypted. NativeLibrary.reloadKeys() @@ -74,6 +79,10 @@ class GamesViewModel : ViewModel() { _shouldScrollToTop.value = shouldScroll } + fun setShouldScrollAfterReload(shouldScroll: Boolean) { + _shouldScrollAfterReload.value = shouldScroll + } + fun setSearchFocused(searchFocused: Boolean) { _searchFocused.value = searchFocused } @@ -123,6 +132,7 @@ class GamesViewModel : ViewModel() { setGames(GameHelper.getGames()) reloading.set(false) _isReloading.value = false + _shouldScrollAfterReload.value = true if (directoriesChanged) { setShouldSwapData(true) @@ -131,7 +141,7 @@ class GamesViewModel : ViewModel() { } } - fun addFolder(gameDir: GameDir, savedFromGameFragment: Boolean) = + fun addFolder(gameDir: GameDir, savedFromGameFragment: Boolean) = viewModelScope.launch { withContext(Dispatchers.IO) { NativeConfig.addGameDir(gameDir) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt index eb1a329e8d..a06abb394f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.model diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetDataValidators.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetDataValidators.kt new file mode 100644 index 0000000000..c2ad475c95 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetDataValidators.kt @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.network + +import android.content.Context +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.features.settings.model.StringSetting +import java.net.InetAddress + +object NetDataValidators { + fun roomName(s: String): Boolean { + return s.length in 3..20 + } + + fun notEmpty(s: String): Boolean { + return s.isNotEmpty() + } + + fun token(s: String?): Boolean { + return s?.matches(Regex("[a-z]{48}")) == true + } + + fun token(): Boolean { + return token(StringSetting.WEB_TOKEN.getString()) + } + + fun roomVisibility(s: String, context: Context): Boolean { + if (s != context.getString(R.string.multiplayer_public_visibility)) { + return true + } + + return token() + } + + fun ipAddress(s: String): Boolean { + return try { + InetAddress.getByName(s) + s.length >= 7 + } catch (_: Exception) { + false + } + } + + fun username(s: String?): Boolean { + return s?.matches(Regex("^[ a-zA-Z0-9._-]{4,20}$")) == true + } + + fun username(): Boolean { + return username(StringSetting.WEB_USERNAME.getString()) + } + + fun port(s: String): Boolean { + return s.toIntOrNull() in 1..65535 + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetPlayManager.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetPlayManager.kt index 1570f04468..1e8e9f97d0 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetPlayManager.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetPlayManager.kt @@ -1,28 +1,43 @@ -// Copyright 2024 Mandarine Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later - package org.yuzu.yuzu_emu.network import android.app.Activity import android.content.Context -import android.net.wifi.WifiManager +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build import android.os.Handler import android.os.Looper -import android.text.format.Formatter import android.widget.Toast import androidx.preference.PreferenceManager -import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.dialogs.ChatMessage +import java.net.Inet4Address +import androidx.core.content.edit object NetPlayManager { - external fun netPlayCreateRoom(ipAddress: String, port: Int, username: String, password: String, roomName: String, maxPlayers: Int): Int - external fun netPlayJoinRoom(ipAddress: String, port: Int, username: String, password: String): Int + external fun netPlayCreateRoom( + ipAddress: String, + port: Int, + username: String, + preferredGameName: String, + preferredGameId: Long, + password: String, + roomName: String, + maxPlayers: Int, + isPublic: Boolean + ): Int + + external fun netPlayJoinRoom( + ipAddress: String, + port: Int, + username: String, + password: String + ): Int + external fun netPlayRoomInfo(): Array external fun netPlayIsJoined(): Boolean external fun netPlayIsHostedRoom(): Boolean @@ -33,6 +48,27 @@ object NetPlayManager { external fun netPlayGetBanList(): Array external fun netPlayBanUser(username: String) external fun netPlayUnbanUser(username: String) + external fun netPlayGetPublicRooms(): Array + + data class RoomInfo( + val name: String, + val hasPassword: Boolean, + val maxPlayers: Int, + val ip: String, + val port: Int, + val description: String, + val owner: String, + val preferredGameId: Long, + val preferredGameName: String, + val members: MutableList = mutableListOf() + ) + + data class RoomMember( + val username: String, + val nickname: String, + val gameId: Long, + val gameName: String + ) private var messageListener: ((Int, String) -> Unit)? = null private var adapterRefreshListener: ((Int, String) -> Unit)? = null @@ -41,20 +77,55 @@ object NetPlayManager { messageListener = listener } + fun getPublicRooms(): List { + val roomData = netPlayGetPublicRooms() + val rooms = mutableMapOf() + + for (data in roomData) { + val parts = data.split("|") + + if (parts[0] == "MEMBER" && parts.size >= 6) { + val roomName = parts[1] + val member = RoomMember( + username = parts[2], + nickname = parts[3], + gameId = parts[4].toLongOrNull() ?: 0L, + gameName = parts[5] + ) + rooms[roomName]?.members?.add(member) + } else if (parts.size >= 9) { + val roomInfo = RoomInfo( + name = parts[0], + hasPassword = parts[1] == "1", + maxPlayers = parts[2].toIntOrNull() ?: 0, + ip = parts[3], + port = parts[4].toIntOrNull() ?: 0, + description = parts[5], + owner = parts[6], + preferredGameId = parts[7].toLongOrNull() ?: 0L, + preferredGameName = parts[8] + ) + rooms[roomInfo.name] = roomInfo + } + } + + return rooms.values.toList() + } + + fun refreshRoomListAsync(callback: (List) -> Unit) { + Thread { + val rooms = getPublicRooms() + + Handler(Looper.getMainLooper()).post { + callback(rooms) + } + }.start() + } + fun setOnAdapterRefreshListener(listener: (Int, String) -> Unit) { adapterRefreshListener = listener } - fun getUsername(activity: Context): String { val prefs = PreferenceManager.getDefaultSharedPreferences(activity) - val name = "Eden${(Math.random() * 100).toInt()}" - return prefs.getString("NetPlayUsername", name) ?: name - } - - fun setUsername(activity: Activity, name: String) { - val prefs = PreferenceManager.getDefaultSharedPreferences(activity) - prefs.edit().putString("NetPlayUsername", name).apply() - } - fun getRoomAddress(activity: Activity): String { val prefs = PreferenceManager.getDefaultSharedPreferences(activity) val address = getIpAddressByWifi(activity) @@ -63,7 +134,7 @@ object NetPlayManager { fun setRoomAddress(activity: Activity, address: String) { val prefs = PreferenceManager.getDefaultSharedPreferences(activity) - prefs.edit().putString("NetPlayRoomAddress", address).apply() + prefs.edit { putString("NetPlayRoomAddress", address) } } fun getRoomPort(activity: Activity): String { @@ -73,7 +144,7 @@ object NetPlayManager { fun setRoomPort(activity: Activity, port: String) { val prefs = PreferenceManager.getDefaultSharedPreferences(activity) - prefs.edit().putString("NetPlayRoomPort", port).apply() + prefs.edit { putString("NetPlayRoomPort", port) } } private val chatMessages = mutableListOf() @@ -103,32 +174,36 @@ object NetPlayManager { if (parts.size == 2) { val nickname = parts[0].trim() val chatMessage = parts[1].trim() - addChatMessage(ChatMessage( - nickname = nickname, - username = "", - message = chatMessage - )) + addChatMessage( + ChatMessage( + nickname = nickname, + username = "", + message = chatMessage + ) + ) } } + NetPlayStatus.MEMBER_JOIN, NetPlayStatus.MEMBER_LEAVE, NetPlayStatus.MEMBER_KICKED, NetPlayStatus.MEMBER_BANNED -> { - addChatMessage(ChatMessage( - nickname = "System", - username = "", - message = message - )) + addChatMessage( + ChatMessage( + nickname = "System", + username = "", + message = message + ) + ) } } - - Handler(Looper.getMainLooper()).post { - if (!isChatOpen) { - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() - } + Handler(Looper.getMainLooper()).post { + if (!isChatOpen) { + // TODO(alekpop, crueter): Improve this, potentially a drawer at the top? + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } - + } messageListener?.invoke(type, msg) adapterRefreshListener?.invoke(type, msg) @@ -140,53 +215,90 @@ object NetPlayManager { NetPlayStatus.LOST_CONNECTION -> context.getString(R.string.multiplayer_lost_connection) NetPlayStatus.NAME_COLLISION -> context.getString(R.string.multiplayer_name_collision) NetPlayStatus.MAC_COLLISION -> context.getString(R.string.multiplayer_mac_collision) - NetPlayStatus.CONSOLE_ID_COLLISION -> context.getString(R.string.multiplayer_console_id_collision) + NetPlayStatus.CONSOLE_ID_COLLISION -> context.getString( + R.string.multiplayer_console_id_collision + ) NetPlayStatus.WRONG_VERSION -> context.getString(R.string.multiplayer_wrong_version) NetPlayStatus.WRONG_PASSWORD -> context.getString(R.string.multiplayer_wrong_password) - NetPlayStatus.COULD_NOT_CONNECT -> context.getString(R.string.multiplayer_could_not_connect) + NetPlayStatus.COULD_NOT_CONNECT -> context.getString( + R.string.multiplayer_could_not_connect + ) NetPlayStatus.ROOM_IS_FULL -> context.getString(R.string.multiplayer_room_is_full) NetPlayStatus.HOST_BANNED -> context.getString(R.string.multiplayer_host_banned) - NetPlayStatus.PERMISSION_DENIED -> context.getString(R.string.multiplayer_permission_denied) + NetPlayStatus.PERMISSION_DENIED -> context.getString( + R.string.multiplayer_permission_denied + ) NetPlayStatus.NO_SUCH_USER -> context.getString(R.string.multiplayer_no_such_user) NetPlayStatus.ALREADY_IN_ROOM -> context.getString(R.string.multiplayer_already_in_room) - NetPlayStatus.CREATE_ROOM_ERROR -> context.getString(R.string.multiplayer_create_room_error) + NetPlayStatus.CREATE_ROOM_ERROR -> context.getString( + R.string.multiplayer_create_room_error + ) NetPlayStatus.HOST_KICKED -> context.getString(R.string.multiplayer_host_kicked) NetPlayStatus.UNKNOWN_ERROR -> context.getString(R.string.multiplayer_unknown_error) - NetPlayStatus.ROOM_UNINITIALIZED -> context.getString(R.string.multiplayer_room_uninitialized) + NetPlayStatus.ROOM_UNINITIALIZED -> context.getString( + R.string.multiplayer_room_uninitialized + ) NetPlayStatus.ROOM_IDLE -> context.getString(R.string.multiplayer_room_idle) NetPlayStatus.ROOM_JOINING -> context.getString(R.string.multiplayer_room_joining) NetPlayStatus.ROOM_JOINED -> context.getString(R.string.multiplayer_room_joined) NetPlayStatus.ROOM_MODERATOR -> context.getString(R.string.multiplayer_room_moderator) NetPlayStatus.MEMBER_JOIN -> context.getString(R.string.multiplayer_member_join, msg) NetPlayStatus.MEMBER_LEAVE -> context.getString(R.string.multiplayer_member_leave, msg) - NetPlayStatus.MEMBER_KICKED -> context.getString(R.string.multiplayer_member_kicked, msg) - NetPlayStatus.MEMBER_BANNED -> context.getString(R.string.multiplayer_member_banned, msg) - NetPlayStatus.ADDRESS_UNBANNED -> context.getString(R.string.multiplayer_address_unbanned) + NetPlayStatus.MEMBER_KICKED -> context.getString( + R.string.multiplayer_member_kicked, + msg + ) + + NetPlayStatus.MEMBER_BANNED -> context.getString( + R.string.multiplayer_member_banned, + msg + ) + + NetPlayStatus.ADDRESS_UNBANNED -> context.getString( + R.string.multiplayer_address_unbanned + ) NetPlayStatus.CHAT_MESSAGE -> msg else -> "" } } - fun getIpAddressByWifi(activity: Activity): String { - var ipAddress = 0 - val wifiManager = activity.getSystemService(WifiManager::class.java) - val wifiInfo = wifiManager.connectionInfo - if (wifiInfo != null) { - ipAddress = wifiInfo.ipAddress - } + fun isConnectedToWifi(activity: Activity): Boolean { + val connectivityManager = activity.getSystemService(ConnectivityManager::class.java) + val network = connectivityManager.activeNetwork + val capabilities = connectivityManager.getNetworkCapabilities(network) + return capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true + } - if (ipAddress == 0) { - val dhcpInfo = wifiManager.dhcpInfo - if (dhcpInfo != null) { - ipAddress = dhcpInfo.ipAddress + fun getIpAddressByWifi(activity: Activity): String { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // For Android 12 (API 31) and above + val connectivityManager = activity.getSystemService(ConnectivityManager::class.java) + val network = connectivityManager.activeNetwork + val capabilities = connectivityManager.getNetworkCapabilities(network) + + if (capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) { + val linkProperties = connectivityManager.getLinkProperties(network) + linkProperties?.linkAddresses?.firstOrNull { it.address is Inet4Address }?.let { + return it.address.hostAddress ?: "192.168.0.1" + } } } - return if (ipAddress == 0) { - "192.168.0.1" - } else { - Formatter.formatIpAddress(ipAddress) + // For Android 11 (API 30) and below + try { + val connectivityManager = activity.getSystemService(ConnectivityManager::class.java) + val network = connectivityManager.activeNetwork + if (network != null) { + val linkProperties = connectivityManager.getLinkProperties(network) + linkProperties?.linkAddresses?.firstOrNull { it.address is Inet4Address }?.let { + return it.address.hostAddress ?: "192.168.0.1" + } + } + } catch (e: Exception) { + e.printStackTrace() } + + return "192.168.0.1" } fun getBanList(): List { @@ -223,4 +335,4 @@ object NetPlayManager { const val ADDRESS_UNBANNED = 26 const val CHAT_MESSAGE = 27 } -} \ No newline at end of file +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt index 737e035840..9f050a5053 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.overlay @@ -13,6 +13,8 @@ import android.graphics.Rect import android.graphics.drawable.Drawable import android.graphics.drawable.VectorDrawable import android.os.Build +import android.os.Handler +import android.os.Looper import android.util.AttributeSet import android.view.HapticFeedbackConstants import android.view.MotionEvent @@ -52,6 +54,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : private var dpadBeingConfigured: InputOverlayDrawableDpad? = null private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null + private var scaleDialog: OverlayScaleDialog? = null + private var touchStartX = 0f + private var touchStartY = 0f + private var hasMoved = false + private val moveThreshold = 20f + private lateinit var windowInsets: WindowInsets var layout = OverlayLayout.Landscape @@ -254,23 +262,44 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : ) { buttonBeingConfigured = button buttonBeingConfigured!!.onConfigureTouch(event) + touchStartX = event.getX(pointerIndex) + touchStartY = event.getY(pointerIndex) + hasMoved = false } MotionEvent.ACTION_MOVE -> if (buttonBeingConfigured != null) { - buttonBeingConfigured!!.onConfigureTouch(event) - invalidate() - return true + val moveDistance = kotlin.math.sqrt( + (event.getX(pointerIndex) - touchStartX).let { it * it } + + (event.getY(pointerIndex) - touchStartY).let { it * it } + ) + + if (moveDistance > moveThreshold) { + hasMoved = true + buttonBeingConfigured!!.onConfigureTouch(event) + invalidate() + return true + } } MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> if (buttonBeingConfigured === button) { - // Persist button position by saving new place. - saveControlPosition( - buttonBeingConfigured!!.overlayControlData.id, - buttonBeingConfigured!!.bounds.centerX(), - buttonBeingConfigured!!.bounds.centerY(), - layout - ) + if (!hasMoved) { + showScaleDialog( + buttonBeingConfigured, + null, + null, + fingerPositionX, + fingerPositionY + ) + } else { + saveControlPosition( + buttonBeingConfigured!!.overlayControlData.id, + buttonBeingConfigured!!.bounds.centerX(), + buttonBeingConfigured!!.bounds.centerY(), + individuaScale = buttonBeingConfigured!!.overlayControlData.individualScale, + layout + ) + } buttonBeingConfigured = null } } @@ -287,23 +316,46 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : ) { dpadBeingConfigured = dpad dpadBeingConfigured!!.onConfigureTouch(event) + touchStartX = event.getX(pointerIndex) + touchStartY = event.getY(pointerIndex) + hasMoved = false } MotionEvent.ACTION_MOVE -> if (dpadBeingConfigured != null) { - dpadBeingConfigured!!.onConfigureTouch(event) - invalidate() - return true + val moveDistance = kotlin.math.sqrt( + (event.getX(pointerIndex) - touchStartX).let { it * it } + + (event.getY(pointerIndex) - touchStartY).let { it * it } + ) + + if (moveDistance > moveThreshold) { + hasMoved = true + dpadBeingConfigured!!.onConfigureTouch(event) + invalidate() + return true + } } MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> if (dpadBeingConfigured === dpad) { - // Persist button position by saving new place. - saveControlPosition( - OverlayControl.COMBINED_DPAD.id, - dpadBeingConfigured!!.bounds.centerX(), - dpadBeingConfigured!!.bounds.centerY(), - layout - ) + if (!hasMoved) { + // This was a click, show scale dialog for dpad + showScaleDialog( + null, + dpadBeingConfigured, + null, + fingerPositionX, + fingerPositionY + ) + } else { + // This was a move, save position + saveControlPosition( + OverlayControl.COMBINED_DPAD.id, + dpadBeingConfigured!!.bounds.centerX(), + dpadBeingConfigured!!.bounds.centerY(), + individuaScale = dpadBeingConfigured!!.individualScale, + layout + ) + } dpadBeingConfigured = null } } @@ -317,21 +369,43 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : ) { joystickBeingConfigured = joystick joystickBeingConfigured!!.onConfigureTouch(event) + touchStartX = event.getX(pointerIndex) + touchStartY = event.getY(pointerIndex) + hasMoved = false } MotionEvent.ACTION_MOVE -> if (joystickBeingConfigured != null) { - joystickBeingConfigured!!.onConfigureTouch(event) - invalidate() + val moveDistance = kotlin.math.sqrt( + (event.getX(pointerIndex) - touchStartX).let { it * it } + + (event.getY(pointerIndex) - touchStartY).let { it * it } + ) + + if (moveDistance > moveThreshold) { + hasMoved = true + joystickBeingConfigured!!.onConfigureTouch(event) + invalidate() + } } MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> if (joystickBeingConfigured != null) { - saveControlPosition( - joystickBeingConfigured!!.prefId, - joystickBeingConfigured!!.bounds.centerX(), - joystickBeingConfigured!!.bounds.centerY(), - layout - ) + if (!hasMoved) { + showScaleDialog( + null, + null, + joystickBeingConfigured, + fingerPositionX, + fingerPositionY + ) + } else { + saveControlPosition( + joystickBeingConfigured!!.prefId, + joystickBeingConfigured!!.bounds.centerX(), + joystickBeingConfigured!!.bounds.centerY(), + individuaScale = joystickBeingConfigured!!.individualScale, + layout + ) + } joystickBeingConfigured = null } } @@ -607,25 +681,117 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : invalidate() } - private fun saveControlPosition(id: String, x: Int, y: Int, layout: OverlayLayout) { + private fun saveControlPosition( + id: String, + x: Int, + y: Int, + individuaScale: Float, + layout: OverlayLayout + ) { val windowSize = getSafeScreenSize(context, Pair(measuredWidth, measuredHeight)) val min = windowSize.first val max = windowSize.second val overlayControlData = NativeConfig.getOverlayControlData() val data = overlayControlData.firstOrNull { it.id == id } val newPosition = Pair((x - min.x).toDouble() / max.x, (y - min.y).toDouble() / max.y) + when (layout) { OverlayLayout.Landscape -> data?.landscapePosition = newPosition OverlayLayout.Portrait -> data?.portraitPosition = newPosition OverlayLayout.Foldable -> data?.foldablePosition = newPosition + } + + data?.individualScale = individuaScale + NativeConfig.setOverlayControlData(overlayControlData) } fun setIsInEditMode(editMode: Boolean) { inEditMode = editMode + if (!editMode) { + scaleDialog?.dismiss() + scaleDialog = null + } } + private fun showScaleDialog( + button: InputOverlayDrawableButton?, + dpad: InputOverlayDrawableDpad?, + joystick: InputOverlayDrawableJoystick?, + x: Int, y: Int + ) { + val overlayControlData = NativeConfig.getOverlayControlData() + // prevent dialog from being spam opened + scaleDialog?.dismiss() + + + when { + button != null -> { + val buttonData = + overlayControlData.firstOrNull { it.id == button.overlayControlData.id } + if (buttonData != null) { + scaleDialog = + OverlayScaleDialog(context, button.overlayControlData) { newScale -> + saveControlPosition( + button.overlayControlData.id, + button.bounds.centerX(), + button.bounds.centerY(), + individuaScale = newScale, + layout + ) + refreshControls() + } + + scaleDialog?.showDialog(x,y, button.bounds.width(), button.bounds.height()) + + } + } + + dpad != null -> { + val dpadData = + overlayControlData.firstOrNull { it.id == OverlayControl.COMBINED_DPAD.id } + if (dpadData != null) { + scaleDialog = OverlayScaleDialog(context, dpadData) { newScale -> + saveControlPosition( + OverlayControl.COMBINED_DPAD.id, + dpad.bounds.centerX(), + dpad.bounds.centerY(), + newScale, + layout + ) + + refreshControls() + } + + scaleDialog?.showDialog(x,y, dpad.bounds.width(), dpad.bounds.height()) + + } + } + + joystick != null -> { + val joystickData = overlayControlData.firstOrNull { it.id == joystick.prefId } + if (joystickData != null) { + scaleDialog = OverlayScaleDialog(context, joystickData) { newScale -> + saveControlPosition( + joystick.prefId, + joystick.bounds.centerX(), + joystick.bounds.centerY(), + individuaScale = newScale, + layout + ) + + refreshControls() + } + + scaleDialog?.showDialog(x,y, joystick.bounds.width(), joystick.bounds.height()) + + } + } + } + } + + /** * Applies and saves all default values for the overlay */ @@ -664,12 +830,24 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : val overlayControlData = NativeConfig.getOverlayControlData() overlayControlData.forEach { it.enabled = OverlayControl.from(it.id)?.defaultVisibility == true + it.individualScale = OverlayControl.from(it.id)?.defaultIndividualScaleResource!! } NativeConfig.setOverlayControlData(overlayControlData) refreshControls() } + fun resetIndividualControlScale() { + val overlayControlData = NativeConfig.getOverlayControlData() + overlayControlData.forEach { data -> + val defaultControlData = OverlayControl.from(data.id) ?: return@forEach + data.individualScale = defaultControlData.defaultIndividualScaleResource + } + NativeConfig.setOverlayControlData(overlayControlData) + NativeConfig.saveGlobalConfig() + refreshControls() + } + private fun defaultOverlayPositionByLayout(layout: OverlayLayout) { val overlayControlData = NativeConfig.getOverlayControlData() for (data in overlayControlData) { @@ -860,6 +1038,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat() scale /= 100f + // Apply individual scale + scale *= overlayControlData.individualScale + // Initialize the InputOverlayDrawableButton. val defaultStateBitmap = getBitmap(context, defaultResId, scale) val pressedStateBitmap = getBitmap(context, pressedResId, scale) @@ -922,11 +1103,20 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : // Resources handle for fetching the initial Drawable resource. val res = context.resources + // Get the dpad control data for individual scale + val overlayControlData = NativeConfig.getOverlayControlData() + val dpadData = overlayControlData.firstOrNull { it.id == OverlayControl.COMBINED_DPAD.id } + // Decide scale based on button ID and user preference var scale = 0.25f scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat() scale /= 100f + // Apply individual scale + if (dpadData != null) { + scale *= dpadData.individualScale + } + // Initialize the InputOverlayDrawableDpad. val defaultStateBitmap = getBitmap(context, defaultResId, scale) @@ -1000,6 +1190,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat() scale /= 100f + // Apply individual scale + scale *= overlayControlData.individualScale + // Initialize the InputOverlayDrawableJoystick. val bitmapOuter = getBitmap(context, resOuter, scale) val bitmapInnerDefault = getBitmap(context, defaultResInner, 1.0f) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt index 0cb6ff2440..01f07e4f36 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.overlay @@ -42,6 +42,8 @@ class InputOverlayDrawableDpad( val width: Int val height: Int + var individualScale: Float = 1.0f + private val defaultStateBitmap: BitmapDrawable private val pressedOneDirectionStateBitmap: BitmapDrawable private val pressedTwoDirectionsStateBitmap: BitmapDrawable diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt index 4b07107fca..bc3ff15b21 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.overlay @@ -51,6 +51,8 @@ class InputOverlayDrawableJoystick( val width: Int val height: Int + var individualScale: Float = 1.0f + private var opacity: Int = 0 private var virtBounds: Rect diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/OverlayScaleDialog.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/OverlayScaleDialog.kt new file mode 100644 index 0000000000..f489ef3b7c --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/OverlayScaleDialog.kt @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.overlay + +import android.app.Dialog +import android.content.Context +import android.view.Gravity +import android.view.LayoutInflater +import android.view.WindowManager +import android.widget.TextView +import com.google.android.material.button.MaterialButton +import com.google.android.material.slider.Slider +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.overlay.model.OverlayControlData + +class OverlayScaleDialog( + context: Context, + private val overlayControlData: OverlayControlData, + private val onScaleChanged: (Float) -> Unit +) : Dialog(context) { + + private var currentScale = overlayControlData.individualScale + private val originalScale = overlayControlData.individualScale + private lateinit var scaleValueText: TextView + private lateinit var scaleSlider: Slider + + init { + setupDialog() + } + + private fun setupDialog() { + val view = LayoutInflater.from(context).inflate(R.layout.dialog_overlay_scale, null) + setContentView(view) + + window?.setBackgroundDrawable(null) + + window?.apply { + attributes = attributes.apply { + flags = flags and WindowManager.LayoutParams.FLAG_DIM_BEHIND.inv() + flags = flags or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + } + } + + scaleValueText = view.findViewById(R.id.scaleValueText) + scaleSlider = view.findViewById(R.id.scaleSlider) + val resetButton = view.findViewById(R.id.resetButton) + val confirmButton = view.findViewById(R.id.confirmButton) + val cancelButton = view.findViewById(R.id.cancelButton) + + scaleValueText.text = String.format("%.1fx", currentScale) + scaleSlider.value = currentScale + + scaleSlider.addOnChangeListener { _, value, input -> + if (input) { + currentScale = value + scaleValueText.text = String.format("%.1fx", currentScale) + } + } + + scaleSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener { + override fun onStartTrackingTouch(slider: Slider) { + // pass + } + + override fun onStopTrackingTouch(slider: Slider) { + onScaleChanged(currentScale) + } + }) + + resetButton.setOnClickListener { + currentScale = 1.0f + scaleSlider.value = 1.0f + scaleValueText.text = String.format("%.1fx", currentScale) + onScaleChanged(currentScale) + } + + confirmButton.setOnClickListener { + overlayControlData.individualScale = currentScale + //slider value is already saved on touch dispatch but just to be sure + onScaleChanged(currentScale) + dismiss() + } + + // both cancel button and back gesture should revert the scale change + cancelButton.setOnClickListener { + onScaleChanged(originalScale) + dismiss() + } + + setOnCancelListener { + onScaleChanged(originalScale) + dismiss() + } + } + + fun showDialog(anchorX: Int, anchorY: Int, anchorHeight: Int, anchorWidth: Int) { + show() + + show() + + // TODO: this calculation is a bit rough, improve it later on + window?.let { window -> + val layoutParams = window.attributes + layoutParams.gravity = Gravity.TOP or Gravity.START + + val density = context.resources.displayMetrics.density + val dialogWidthPx = (320 * density).toInt() + val dialogHeightPx = (400 * density).toInt() // set your estimated dialog height + + val screenHeight = context.resources.displayMetrics.heightPixels + + + layoutParams.x = anchorX + anchorWidth / 2 - dialogWidthPx / 2 + layoutParams.y = anchorY + anchorHeight / 2 - dialogHeightPx / 2 + layoutParams.width = dialogWidthPx + + + window.attributes = layoutParams + } + + } + +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControl.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControl.kt index a0eeadf4bc..10cc547d0b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControl.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControl.kt @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.overlay.model @@ -12,126 +12,144 @@ enum class OverlayControl( val defaultVisibility: Boolean, @IntegerRes val defaultLandscapePositionResources: Pair, @IntegerRes val defaultPortraitPositionResources: Pair, - @IntegerRes val defaultFoldablePositionResources: Pair + @IntegerRes val defaultFoldablePositionResources: Pair, + val defaultIndividualScaleResource: Float, ) { BUTTON_A( "button_a", true, Pair(R.integer.BUTTON_A_X, R.integer.BUTTON_A_Y), Pair(R.integer.BUTTON_A_X_PORTRAIT, R.integer.BUTTON_A_Y_PORTRAIT), - Pair(R.integer.BUTTON_A_X_FOLDABLE, R.integer.BUTTON_A_Y_FOLDABLE) + Pair(R.integer.BUTTON_A_X_FOLDABLE, R.integer.BUTTON_A_Y_FOLDABLE), + 1.0f ), BUTTON_B( "button_b", true, Pair(R.integer.BUTTON_B_X, R.integer.BUTTON_B_Y), Pair(R.integer.BUTTON_B_X_PORTRAIT, R.integer.BUTTON_B_Y_PORTRAIT), - Pair(R.integer.BUTTON_B_X_FOLDABLE, R.integer.BUTTON_B_Y_FOLDABLE) + Pair(R.integer.BUTTON_B_X_FOLDABLE, R.integer.BUTTON_B_Y_FOLDABLE), + 1.0f ), BUTTON_X( "button_x", true, Pair(R.integer.BUTTON_X_X, R.integer.BUTTON_X_Y), Pair(R.integer.BUTTON_X_X_PORTRAIT, R.integer.BUTTON_X_Y_PORTRAIT), - Pair(R.integer.BUTTON_X_X_FOLDABLE, R.integer.BUTTON_X_Y_FOLDABLE) + Pair(R.integer.BUTTON_X_X_FOLDABLE, R.integer.BUTTON_X_Y_FOLDABLE), + 1.0f ), BUTTON_Y( "button_y", true, Pair(R.integer.BUTTON_Y_X, R.integer.BUTTON_Y_Y), Pair(R.integer.BUTTON_Y_X_PORTRAIT, R.integer.BUTTON_Y_Y_PORTRAIT), - Pair(R.integer.BUTTON_Y_X_FOLDABLE, R.integer.BUTTON_Y_Y_FOLDABLE) + Pair(R.integer.BUTTON_Y_X_FOLDABLE, R.integer.BUTTON_Y_Y_FOLDABLE), + 1.0f ), BUTTON_PLUS( "button_plus", true, Pair(R.integer.BUTTON_PLUS_X, R.integer.BUTTON_PLUS_Y), Pair(R.integer.BUTTON_PLUS_X_PORTRAIT, R.integer.BUTTON_PLUS_Y_PORTRAIT), - Pair(R.integer.BUTTON_PLUS_X_FOLDABLE, R.integer.BUTTON_PLUS_Y_FOLDABLE) + Pair(R.integer.BUTTON_PLUS_X_FOLDABLE, R.integer.BUTTON_PLUS_Y_FOLDABLE), + 1.0f ), BUTTON_MINUS( "button_minus", true, Pair(R.integer.BUTTON_MINUS_X, R.integer.BUTTON_MINUS_Y), Pair(R.integer.BUTTON_MINUS_X_PORTRAIT, R.integer.BUTTON_MINUS_Y_PORTRAIT), - Pair(R.integer.BUTTON_MINUS_X_FOLDABLE, R.integer.BUTTON_MINUS_Y_FOLDABLE) + Pair(R.integer.BUTTON_MINUS_X_FOLDABLE, R.integer.BUTTON_MINUS_Y_FOLDABLE), + 1.0f ), BUTTON_HOME( "button_home", false, Pair(R.integer.BUTTON_HOME_X, R.integer.BUTTON_HOME_Y), Pair(R.integer.BUTTON_HOME_X_PORTRAIT, R.integer.BUTTON_HOME_Y_PORTRAIT), - Pair(R.integer.BUTTON_HOME_X_FOLDABLE, R.integer.BUTTON_HOME_Y_FOLDABLE) + Pair(R.integer.BUTTON_HOME_X_FOLDABLE, R.integer.BUTTON_HOME_Y_FOLDABLE), + 1.0f ), BUTTON_CAPTURE( "button_capture", false, Pair(R.integer.BUTTON_CAPTURE_X, R.integer.BUTTON_CAPTURE_Y), Pair(R.integer.BUTTON_CAPTURE_X_PORTRAIT, R.integer.BUTTON_CAPTURE_Y_PORTRAIT), - Pair(R.integer.BUTTON_CAPTURE_X_FOLDABLE, R.integer.BUTTON_CAPTURE_Y_FOLDABLE) + Pair(R.integer.BUTTON_CAPTURE_X_FOLDABLE, R.integer.BUTTON_CAPTURE_Y_FOLDABLE), + 1.0f ), BUTTON_L( "button_l", true, Pair(R.integer.BUTTON_L_X, R.integer.BUTTON_L_Y), Pair(R.integer.BUTTON_L_X_PORTRAIT, R.integer.BUTTON_L_Y_PORTRAIT), - Pair(R.integer.BUTTON_L_X_FOLDABLE, R.integer.BUTTON_L_Y_FOLDABLE) + Pair(R.integer.BUTTON_L_X_FOLDABLE, R.integer.BUTTON_L_Y_FOLDABLE), + 1.0f ), BUTTON_R( "button_r", true, Pair(R.integer.BUTTON_R_X, R.integer.BUTTON_R_Y), Pair(R.integer.BUTTON_R_X_PORTRAIT, R.integer.BUTTON_R_Y_PORTRAIT), - Pair(R.integer.BUTTON_R_X_FOLDABLE, R.integer.BUTTON_R_Y_FOLDABLE) + Pair(R.integer.BUTTON_R_X_FOLDABLE, R.integer.BUTTON_R_Y_FOLDABLE), + 1.0f ), BUTTON_ZL( "button_zl", true, Pair(R.integer.BUTTON_ZL_X, R.integer.BUTTON_ZL_Y), Pair(R.integer.BUTTON_ZL_X_PORTRAIT, R.integer.BUTTON_ZL_Y_PORTRAIT), - Pair(R.integer.BUTTON_ZL_X_FOLDABLE, R.integer.BUTTON_ZL_Y_FOLDABLE) + Pair(R.integer.BUTTON_ZL_X_FOLDABLE, R.integer.BUTTON_ZL_Y_FOLDABLE), + 1.0f ), BUTTON_ZR( "button_zr", true, Pair(R.integer.BUTTON_ZR_X, R.integer.BUTTON_ZR_Y), Pair(R.integer.BUTTON_ZR_X_PORTRAIT, R.integer.BUTTON_ZR_Y_PORTRAIT), - Pair(R.integer.BUTTON_ZR_X_FOLDABLE, R.integer.BUTTON_ZR_Y_FOLDABLE) + Pair(R.integer.BUTTON_ZR_X_FOLDABLE, R.integer.BUTTON_ZR_Y_FOLDABLE), + 1.0f ), BUTTON_STICK_L( "button_stick_l", true, Pair(R.integer.BUTTON_STICK_L_X, R.integer.BUTTON_STICK_L_Y), Pair(R.integer.BUTTON_STICK_L_X_PORTRAIT, R.integer.BUTTON_STICK_L_Y_PORTRAIT), - Pair(R.integer.BUTTON_STICK_L_X_FOLDABLE, R.integer.BUTTON_STICK_L_Y_FOLDABLE) + Pair(R.integer.BUTTON_STICK_L_X_FOLDABLE, R.integer.BUTTON_STICK_L_Y_FOLDABLE), + 1.0f ), BUTTON_STICK_R( "button_stick_r", true, Pair(R.integer.BUTTON_STICK_R_X, R.integer.BUTTON_STICK_R_Y), Pair(R.integer.BUTTON_STICK_R_X_PORTRAIT, R.integer.BUTTON_STICK_R_Y_PORTRAIT), - Pair(R.integer.BUTTON_STICK_R_X_FOLDABLE, R.integer.BUTTON_STICK_R_Y_FOLDABLE) + Pair(R.integer.BUTTON_STICK_R_X_FOLDABLE, R.integer.BUTTON_STICK_R_Y_FOLDABLE), + 1.0f ), STICK_L( "stick_l", true, Pair(R.integer.STICK_L_X, R.integer.STICK_L_Y), Pair(R.integer.STICK_L_X_PORTRAIT, R.integer.STICK_L_Y_PORTRAIT), - Pair(R.integer.STICK_L_X_FOLDABLE, R.integer.STICK_L_Y_FOLDABLE) + Pair(R.integer.STICK_L_X_FOLDABLE, R.integer.STICK_L_Y_FOLDABLE), + 1.0f ), STICK_R( "stick_r", true, Pair(R.integer.STICK_R_X, R.integer.STICK_R_Y), Pair(R.integer.STICK_R_X_PORTRAIT, R.integer.STICK_R_Y_PORTRAIT), - Pair(R.integer.STICK_R_X_FOLDABLE, R.integer.STICK_R_Y_FOLDABLE) + Pair(R.integer.STICK_R_X_FOLDABLE, R.integer.STICK_R_Y_FOLDABLE), + 1.0f ), COMBINED_DPAD( "combined_dpad", true, Pair(R.integer.COMBINED_DPAD_X, R.integer.COMBINED_DPAD_Y), Pair(R.integer.COMBINED_DPAD_X_PORTRAIT, R.integer.COMBINED_DPAD_Y_PORTRAIT), - Pair(R.integer.COMBINED_DPAD_X_FOLDABLE, R.integer.COMBINED_DPAD_Y_FOLDABLE) + Pair(R.integer.COMBINED_DPAD_X_FOLDABLE, R.integer.COMBINED_DPAD_Y_FOLDABLE), + 1.0f ); fun getDefaultPositionForLayout(layout: OverlayLayout): Pair { @@ -173,7 +191,8 @@ enum class OverlayControl( defaultVisibility, getDefaultPositionForLayout(OverlayLayout.Landscape), getDefaultPositionForLayout(OverlayLayout.Portrait), - getDefaultPositionForLayout(OverlayLayout.Foldable) + getDefaultPositionForLayout(OverlayLayout.Foldable), + defaultIndividualScaleResource ) companion object { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControlData.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControlData.kt index 26cfeb1db5..6cc5a59c98 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControlData.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControlData.kt @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.overlay.model @@ -8,7 +8,8 @@ data class OverlayControlData( var enabled: Boolean, var landscapePosition: Pair, var portraitPosition: Pair, - var foldablePosition: Pair + var foldablePosition: Pair, + var individualScale: Float ) { fun positionFromLayout(layout: OverlayLayout): Pair = when (layout) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt index dd5f8518c3..80055628e1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt @@ -1,22 +1,20 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.ui +import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.content.res.Configuration import android.os.Bundle -import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.PopupMenu -import android.widget.TextView import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity -import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding @@ -25,11 +23,10 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.color.MaterialColors -import info.debatty.java.stringsimilarity.Jaccard -import info.debatty.java.stringsimilarity.JaroWinkler +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.adapters.GameAdapter @@ -40,17 +37,25 @@ import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.utils.collect +import info.debatty.java.stringsimilarity.Jaccard +import info.debatty.java.stringsimilarity.JaroWinkler import java.util.Locale import androidx.core.content.edit -import androidx.core.view.updateLayoutParams +import androidx.core.view.doOnNextLayout class GamesFragment : Fragment() { private var _binding: FragmentGamesBinding? = null private val binding get() = _binding!! + private var originalHeaderTopMargin: Int? = null + private var originalHeaderBottomMargin: Int? = null + private var originalHeaderRightMargin: Int? = null + private var originalHeaderLeftMargin: Int? = null + + private var lastViewType: Int = GameAdapter.VIEW_TYPE_GRID + companion object { private const val SEARCH_TEXT = "SearchText" - private const val PREF_VIEW_TYPE = "GamesViewType" private const val PREF_SORT_TYPE = "GamesSortType" } @@ -58,7 +63,8 @@ class GamesFragment : Fragment() { private val homeViewModel: HomeViewModel by activityViewModels() private lateinit var gameAdapter: GameAdapter - private val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + private val preferences = + PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) private lateinit var mainActivity: MainActivity private val getGamesDirectory = @@ -68,7 +74,18 @@ class GamesFragment : Fragment() { } } + private fun getCurrentViewType(): Int { + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + val key = if (isLandscape) CarouselRecyclerView.CAROUSEL_VIEW_TYPE_LANDSCAPE else CarouselRecyclerView.CAROUSEL_VIEW_TYPE_PORTRAIT + val fallback = if (isLandscape) GameAdapter.VIEW_TYPE_CAROUSEL else GameAdapter.VIEW_TYPE_GRID + return preferences.getInt(key, fallback) + } + private fun setCurrentViewType(type: Int) { + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + val key = if (isLandscape) CarouselRecyclerView.CAROUSEL_VIEW_TYPE_LANDSCAPE else CarouselRecyclerView.CAROUSEL_VIEW_TYPE_PORTRAIT + preferences.edit { putInt(key, type) } + } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -78,6 +95,7 @@ class GamesFragment : Fragment() { return binding.root } + @SuppressLint("NotifyDataSetChanged") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) homeViewModel.setStatusBarShadeVisibility(true) @@ -88,49 +106,46 @@ class GamesFragment : Fragment() { } gameAdapter = GameAdapter( - requireActivity() as AppCompatActivity, + requireActivity() as AppCompatActivity ) applyGridGamesBinding() binding.swipeRefresh.apply { - // Add swipe down to refresh gesture - setOnRefreshListener { + (binding.swipeRefresh as? SwipeRefreshLayout)?.setOnRefreshListener { gamesViewModel.reloadGames(false) } - - // Set theme color to the refresh animation's background - setProgressBackgroundColorSchemeColor( - MaterialColors.getColor( + (binding.swipeRefresh as? SwipeRefreshLayout)?.setProgressBackgroundColorSchemeColor( + com.google.android.material.color.MaterialColors.getColor( binding.swipeRefresh, com.google.android.material.R.attr.colorPrimary ) ) - setColorSchemeColors( - MaterialColors.getColor( + (binding.swipeRefresh as? SwipeRefreshLayout)?.setColorSchemeColors( + com.google.android.material.color.MaterialColors.getColor( binding.swipeRefresh, com.google.android.material.R.attr.colorOnPrimary ) ) - - // Make sure the loading indicator appears even if the layout is told to refresh before being fully drawn post { if (_binding == null) { return@post } - binding.swipeRefresh.isRefreshing = gamesViewModel.isReloading.value + (binding.swipeRefresh as? SwipeRefreshLayout)?.isRefreshing = gamesViewModel.isReloading.value } } gamesViewModel.isReloading.collect(viewLifecycleOwner) { - binding.swipeRefresh.isRefreshing = it + (binding.swipeRefresh as? SwipeRefreshLayout)?.isRefreshing = it binding.noticeText.setVisible( visible = gamesViewModel.games.value.isEmpty() && !it, gone = false ) } gamesViewModel.games.collect(viewLifecycleOwner) { - setAdapter(it) + if (it.isNotEmpty()) { + setAdapter(it) + } } gamesViewModel.shouldSwapData.collect( viewLifecycleOwner, @@ -145,31 +160,63 @@ class GamesFragment : Fragment() { resetState = { gamesViewModel.setShouldScrollToTop(false) } ) { if (it) scrollToTop() } + gamesViewModel.shouldScrollAfterReload.collect(viewLifecycleOwner) { shouldScroll -> + if (shouldScroll) { + binding.gridGames.post { + (binding.gridGames as? CarouselRecyclerView)?.pendingScrollAfterReload = true + gameAdapter.notifyDataSetChanged() + } + gamesViewModel.setShouldScrollAfterReload(false) + } + } + setupTopView() binding.addDirectory.setOnClickListener { getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) } - setInsets() - addPreAlphaBanner() + setInsets() } val applyGridGamesBinding = { - binding.gridGames.apply { - val savedViewType = preferences.getInt(PREF_VIEW_TYPE, GameAdapter.VIEW_TYPE_GRID) + (binding.gridGames as? RecyclerView)?.apply { + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + val currentViewType = getCurrentViewType() + val savedViewType = if (isLandscape || currentViewType != GameAdapter.VIEW_TYPE_CAROUSEL) currentViewType else GameAdapter.VIEW_TYPE_GRID + gameAdapter.setViewType(savedViewType) currentFilter = preferences.getInt(PREF_SORT_TYPE, View.NO_ID) - adapter = gameAdapter - val gameGrid = when (savedViewType) { - GameAdapter.VIEW_TYPE_LIST -> R.integer.game_columns_list - GameAdapter.VIEW_TYPE_GRID -> R.integer.game_columns_grid - else -> 0 + // Set the correct layout manager + layoutManager = when (savedViewType) { + GameAdapter.VIEW_TYPE_GRID -> { + val columns = resources.getInteger(R.integer.game_columns_grid) + GridLayoutManager(context, columns) + } + GameAdapter.VIEW_TYPE_GRID_COMPACT -> { + val columns = resources.getInteger(R.integer.game_columns_grid) + GridLayoutManager(context, columns) + } + GameAdapter.VIEW_TYPE_LIST -> { + val columns = resources.getInteger(R.integer.game_columns_list) + GridLayoutManager(context, columns) + } + GameAdapter.VIEW_TYPE_CAROUSEL -> { + LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) + } + else -> throw IllegalArgumentException("Invalid view type: $savedViewType") } - - layoutManager = GridLayoutManager(requireContext(), resources.getInteger(gameGrid)) - + if (savedViewType == GameAdapter.VIEW_TYPE_CAROUSEL) { + doOnNextLayout { + (this as? CarouselRecyclerView)?.setCarouselMode(true, gameAdapter) + adapter = gameAdapter + } + } else { + (this as? CarouselRecyclerView)?.setCarouselMode(false) + } + adapter = gameAdapter + lastViewType = savedViewType } } @@ -180,15 +227,38 @@ class GamesFragment : Fragment() { } } + override fun onPause() { + super.onPause() + if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) { + gamesViewModel.lastScrollPosition = (binding.gridGames as? CarouselRecyclerView)?.getClosestChildPosition() ?: 0 + } + } + + override fun onResume() { + super.onResume() + if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) { + (binding.gridGames as? CarouselRecyclerView)?.restoreScrollState( + gamesViewModel.lastScrollPosition + ) + } + } + + private var lastSearchText: String = "" + private var lastFilter: Int = preferences.getInt(PREF_SORT_TYPE, View.NO_ID) + private fun setAdapter(games: List) { val currentSearchText = binding.searchText.text.toString() val currentFilter = binding.filterButton.id + val searchChanged = currentSearchText != lastSearchText + val filterChanged = currentFilter != lastFilter - if (currentSearchText.isNotEmpty() || currentFilter != View.NO_ID) { + if (searchChanged || filterChanged) { filterAndSearch(games) + lastSearchText = currentSearchText + lastFilter = currentFilter } else { - (binding.gridGames.adapter as GameAdapter).submitList(games) + ((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(games) gamesViewModel.setFilteredGames(games) } } @@ -223,73 +293,58 @@ class GamesFragment : Fragment() { navController.navigate(R.id.action_gamesFragment_to_homeSettingsFragment) } - private fun addPreAlphaBanner() { - val preAlphaBanner = TextView(requireContext()).apply { - id = "pre_alpha_banner".hashCode() - layoutParams = ConstraintLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ).apply { - marginStart = resources.getDimensionPixelSize(R.dimen.spacing_med) - marginEnd = resources.getDimensionPixelSize(R.dimen.spacing_med) - topMargin = resources.getDimensionPixelSize(R.dimen.spacing_large) - topToBottom = R.id.frame_search - } - setPadding( - resources.getDimensionPixelSize(R.dimen.spacing_med), - resources.getDimensionPixelSize(R.dimen.spacing_large), - resources.getDimensionPixelSize(R.dimen.spacing_med), - resources.getDimensionPixelSize(R.dimen.spacing_med) - ) - - setBackgroundColor( - MaterialColors.getColor( - this, - com.google.android.material.R.attr.colorPrimary - ) - ) - text = getString(R.string.pre_alpha_warning) - setTextAppearance( - com.google.android.material.R.style.TextAppearance_Material3_HeadlineSmall - ) - setTextColor( - MaterialColors.getColor( - this, - com.google.android.material.R.attr.colorOnError - ) - ) - gravity = Gravity.CENTER - } - binding.root.addView(preAlphaBanner) - binding.swipeRefresh.updateLayoutParams { - topToBottom = preAlphaBanner.id - } - } - private fun showViewMenu(anchor: View) { val popup = PopupMenu(requireContext(), anchor) popup.menuInflater.inflate(R.menu.menu_game_views, popup.menu) + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + if (!isLandscape) { + popup.menu.findItem(R.id.view_carousel)?.isVisible = false + } - val currentViewType = (preferences.getInt(PREF_VIEW_TYPE, GameAdapter.VIEW_TYPE_GRID)) + val currentViewType = getCurrentViewType() when (currentViewType) { GameAdapter.VIEW_TYPE_LIST -> popup.menu.findItem(R.id.view_list).isChecked = true + GameAdapter.VIEW_TYPE_GRID_COMPACT -> popup.menu.findItem(R.id.view_grid_compact).isChecked = true GameAdapter.VIEW_TYPE_GRID -> popup.menu.findItem(R.id.view_grid).isChecked = true + GameAdapter.VIEW_TYPE_CAROUSEL -> popup.menu.findItem(R.id.view_carousel).isChecked = true } popup.setOnMenuItemClickListener { item -> when (item.itemId) { R.id.view_grid -> { - preferences.edit() { putInt(PREF_VIEW_TYPE, GameAdapter.VIEW_TYPE_GRID) } + if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause() + setCurrentViewType(GameAdapter.VIEW_TYPE_GRID) applyGridGamesBinding() item.isChecked = true true } + + R.id.view_grid_compact -> { + if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause() + setCurrentViewType(GameAdapter.VIEW_TYPE_GRID_COMPACT) + applyGridGamesBinding() + item.isChecked = true + true + } + R.id.view_list -> { - preferences.edit() { putInt(PREF_VIEW_TYPE, GameAdapter.VIEW_TYPE_LIST) } + if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause() + setCurrentViewType(GameAdapter.VIEW_TYPE_LIST) applyGridGamesBinding() item.isChecked = true true } + + R.id.view_carousel -> { + if (!item.isChecked || getCurrentViewType() != GameAdapter.VIEW_TYPE_CAROUSEL) { + setCurrentViewType(GameAdapter.VIEW_TYPE_CAROUSEL) + applyGridGamesBinding() + item.isChecked = true + onResume() + } + true + } + else -> false } } @@ -313,7 +368,7 @@ class GamesFragment : Fragment() { popup.setOnMenuItemClickListener { item -> currentFilter = item.itemId - preferences.edit().putInt(PREF_SORT_TYPE, currentFilter).apply() + preferences.edit { putInt(PREF_SORT_TYPE, currentFilter) } filterAndSearch() true } @@ -333,20 +388,20 @@ class GamesFragment : Fragment() { lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000) }.sortedByDescending { preferences.getLong(it.keyLastPlayedTime, 0L) } } - R.id.filter_recently_added -> { baseList.filter { val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L) addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000) }.sortedByDescending { preferences.getLong(it.keyAddedToLibraryTime, 0L) } } - else -> baseList } val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault()) if (searchTerm.isEmpty()) { - (binding.gridGames.adapter as GameAdapter).submitList(filteredList) + ((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList( + filteredList + ) gamesViewModel.setFilteredGames(filteredList) return } @@ -362,7 +417,7 @@ class GamesFragment : Fragment() { } }.sortedByDescending { it.score }.map { it.item } - (binding.gridGames.adapter as GameAdapter).submitList(sortedList) + ((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(sortedList) gamesViewModel.setFilteredGames(sortedList) } @@ -375,7 +430,6 @@ class GamesFragment : Fragment() { imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT) } - override fun onDestroyView() { super.onDestroyView() _binding = null @@ -383,49 +437,61 @@ class GamesFragment : Fragment() { private fun scrollToTop() { if (_binding != null) { - binding.gridGames.smoothScrollToPosition(0) + (binding.gridGames as? CarouselRecyclerView)?.smoothScrollToPosition(0) } } private fun setInsets() = ViewCompat.setOnApplyWindowInsetsListener( binding.root - ) { view: View, windowInsets: WindowInsetsCompat -> + ) { _: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation) - resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail) - val isLandscape = - resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail) - binding.swipeRefresh.setProgressViewEndTarget( + (binding.swipeRefresh as? SwipeRefreshLayout)?.setProgressViewEndTarget( false, barInsets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end) ) - val leftInsets = barInsets.left + cutoutInsets.left - val rightInsets = barInsets.right + cutoutInsets.right + val leftInset = barInsets.left + cutoutInsets.left + val rightInset = barInsets.right + cutoutInsets.right + val topInset = maxOf(barInsets.top, cutoutInsets.top) + val mlpSwipe = binding.swipeRefresh.layoutParams as ViewGroup.MarginLayoutParams - if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) { - mlpSwipe.leftMargin = leftInsets - mlpSwipe.rightMargin = rightInsets - } else { - mlpSwipe.leftMargin = leftInsets - mlpSwipe.rightMargin = rightInsets - } + mlpSwipe.leftMargin = leftInset + mlpSwipe.rightMargin = rightInset binding.swipeRefresh.layoutParams = mlpSwipe + val mlpHeader = binding.header.layoutParams as ViewGroup.MarginLayoutParams + + // Store original margins only once + if (originalHeaderTopMargin == null) { + originalHeaderTopMargin = mlpHeader.topMargin + originalHeaderRightMargin = mlpHeader.rightMargin + originalHeaderLeftMargin = mlpHeader.leftMargin + } + + // Always set margin as original + insets + mlpHeader.leftMargin = (originalHeaderLeftMargin ?: 0) + leftInset + mlpHeader.rightMargin = (originalHeaderRightMargin ?: 0) + rightInset + mlpHeader.topMargin = (originalHeaderTopMargin ?: 0) + topInset + resources.getDimensionPixelSize( + R.dimen.spacing_med + ) + binding.header.layoutParams = mlpHeader + binding.noticeText.updatePadding(bottom = spacingNavigation) - binding.header.updatePadding(top = cutoutInsets.top + resources.getDimensionPixelSize(R.dimen.spacing_large) + if (isLandscape) barInsets.top else 0) + binding.gridGames.updatePadding( - top = resources.getDimensionPixelSize(R.dimen.spacing_med) + top = resources.getDimensionPixelSize(R.dimen.spacing_med) ) val mlpFab = binding.addDirectory.layoutParams as ViewGroup.MarginLayoutParams val fabPadding = resources.getDimensionPixelSize(R.dimen.spacing_large) - mlpFab.leftMargin = leftInsets + fabPadding + mlpFab.leftMargin = leftInset + fabPadding mlpFab.bottomMargin = barInsets.bottom + fabPadding - mlpFab.rightMargin = rightInsets + fabPadding + mlpFab.rightMargin = rightInset + fabPadding binding.addDirectory.layoutParams = mlpFab windowInsets diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 468c758c2f..956d93a4eb 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -1,10 +1,6 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project -// SPDX-License-Identifier: GPL-3.0-or-later - - package org.yuzu.yuzu_emu.ui.main import android.content.Intent @@ -25,13 +21,10 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.ui.setupWithNavController import androidx.preference.PreferenceManager import com.google.android.material.color.MaterialColors -import com.google.android.material.navigation.NavigationBarView import java.io.File import java.io.FilenameFilter -import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.ActivityMainBinding @@ -45,6 +38,7 @@ import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.InstallResult +import android.os.Build import org.yuzu.yuzu_emu.model.TaskState import org.yuzu.yuzu_emu.model.TaskViewModel import org.yuzu.yuzu_emu.utils.* @@ -54,6 +48,8 @@ import java.io.BufferedOutputStream import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import androidx.core.content.edit +import org.yuzu.yuzu_emu.activities.EmulationActivity +import kotlin.text.compareTo class MainActivity : AppCompatActivity(), ThemeProvider { private lateinit var binding: ActivityMainBinding @@ -69,28 +65,80 @@ class MainActivity : AppCompatActivity(), ThemeProvider { private val CHECKED_DECRYPTION = "CheckedDecryption" private var checkedDecryption = false + private val CHECKED_FIRMWARE = "CheckedFirmware" + private var checkedFirmware = false + + private val requestBluetoothPermissionsLauncher = + registerForActivityResult( + androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + val granted = permissions.entries.all { it.value } + if (granted) { + // Permissions were granted. + Toast.makeText(this, R.string.bluetooth_permissions_granted, Toast.LENGTH_SHORT) + .show() + } else { + // Permissions were denied. + Toast.makeText(this, R.string.bluetooth_permissions_denied, Toast.LENGTH_LONG) + .show() + } + } + + private fun checkAndRequestBluetoothPermissions() { + // This check is only necessary for Android 12 (API level 31) and above. + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + val permissionsToRequest = arrayOf( + android.Manifest.permission.BLUETOOTH_SCAN, + android.Manifest.permission.BLUETOOTH_CONNECT + ) + + val permissionsNotGranted = permissionsToRequest.filter { + checkSelfPermission(it) != android.content.pm.PackageManager.PERMISSION_GRANTED + } + + if (permissionsNotGranted.isNotEmpty()) { + requestBluetoothPermissionsLauncher.launch(permissionsNotGranted.toTypedArray()) + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } ThemeHelper.ThemeChangeListener(this) ThemeHelper.setTheme(this) - NativeLibrary.netPlayInit() - super.onCreate(savedInstanceState) + NativeLibrary.initMultiplayer() binding = ActivityMainBinding.inflate(layoutInflater) + + // Since Android 15, google automatically forces "games" to be 60 hrz + // This ensures the display's max refresh rate is actually used + display?.let { + val supportedModes = it.supportedModes + val maxRefreshRate = supportedModes.maxByOrNull { mode -> mode.refreshRate } + + if (maxRefreshRate != null) { + val layoutParams = window.attributes + layoutParams.preferredDisplayModeId = maxRefreshRate.modeId + window.attributes = layoutParams + } + } + setContentView(binding.root) + checkAndRequestBluetoothPermissions() + if (savedInstanceState != null) { checkedDecryption = savedInstanceState.getBoolean(CHECKED_DECRYPTION) + checkedFirmware = savedInstanceState.getBoolean(CHECKED_FIRMWARE) } if (!checkedDecryption) { val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext) .getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true) if (!firstTimeSetup) { checkKeys() - showPreAlphaWarningDialog() } checkedDecryption = true } @@ -112,9 +160,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { ThemeHelper.SYSTEM_BAR_ALPHA ) ) - if (InsetsHelper.getSystemGestureType(applicationContext) != - InsetsHelper.GESTURE_NAVIGATION - ) { + if (InsetsHelper.getSystemGestureType(applicationContext) != InsetsHelper.GESTURE_NAVIGATION) { binding.navigationBarShade.setBackgroundColor( ThemeHelper.getColorWithOpacity( MaterialColors.getColor( @@ -143,6 +189,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { if (it) checkKeys() } + // Dismiss previous notifications (should not happen unless a crash occurred) + EmulationActivity.stopForegroundService(this) + setInsets() } @@ -159,10 +208,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { negativeButtonTitleId = R.string.close, showNegativeButton = true, positiveAction = { - PreferenceManager.getDefaultSharedPreferences(applicationContext) - .edit() { - putBoolean(Settings.PREF_SHOULD_SHOW_PRE_ALPHA_WARNING, false) - } + PreferenceManager.getDefaultSharedPreferences(applicationContext).edit() { + putBoolean(Settings.PREF_SHOULD_SHOW_PRE_ALPHA_WARNING, false) + } } ).show(supportFragmentManager, MessageDialogFragment.TAG) } @@ -182,10 +230,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider { ).show(supportFragmentManager, MessageDialogFragment.TAG) } } - override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putBoolean(CHECKED_DECRYPTION, checkedDecryption) + outState.putBoolean(CHECKED_FIRMWARE, checkedFirmware) } fun finishSetup(navController: NavController) { @@ -227,29 +275,33 @@ class MainActivity : AppCompatActivity(), ThemeProvider { super.onResume() } - private fun setInsets() = - ViewCompat.setOnApplyWindowInsetsListener( - binding.root - ) { _: View, windowInsets: WindowInsetsCompat -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - val mlpStatusShade = binding.statusBarShade.layoutParams as MarginLayoutParams - mlpStatusShade.height = insets.top - binding.statusBarShade.layoutParams = mlpStatusShade + private fun setInsets() = ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val mlpStatusShade = binding.statusBarShade.layoutParams as MarginLayoutParams + mlpStatusShade.height = insets.top + binding.statusBarShade.layoutParams = mlpStatusShade - // The only situation where we care to have a nav bar shade is when it's at the bottom - // of the screen where scrolling list elements can go behind it. - val mlpNavShade = binding.navigationBarShade.layoutParams as MarginLayoutParams - mlpNavShade.height = insets.bottom - binding.navigationBarShade.layoutParams = mlpNavShade + // The only situation where we care to have a nav bar shade is when it's at the bottom + // of the screen where scrolling list elements can go behind it. + val mlpNavShade = binding.navigationBarShade.layoutParams as MarginLayoutParams + mlpNavShade.height = insets.bottom + binding.navigationBarShade.layoutParams = mlpNavShade - windowInsets - } + windowInsets + } override fun setTheme(resId: Int) { super.setTheme(resId) themeId = resId } + override fun onDestroy() { + EmulationActivity.stopForegroundService(this) + super.onDestroy() + } + val getGamesDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> if (result != null) { @@ -278,106 +330,104 @@ class MainActivity : AppCompatActivity(), ThemeProvider { .show(supportFragmentManager, AddGameFolderDialogFragment.TAG) } - val getProdKey = - registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result != null) { - processKey(result) - } + val getProdKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result != null) { + processKey(result, "keys") } + } - fun processKey(result: Uri): Boolean { - if (FileUtil.getExtension(result) != "keys") { - MessageDialogFragment.newInstance( - this, - titleId = R.string.reading_keys_failure, - descriptionId = R.string.install_prod_keys_failure_extension_description - ).show(supportFragmentManager, MessageDialogFragment.TAG) - return false + val getAmiiboKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result != null) { + processKey(result, "bin") } + } + fun processKey(result: Uri, extension: String = "keys") { contentResolver.takePersistableUriPermission( result, Intent.FLAG_GRANT_READ_URI_PERMISSION ) - val dstPath = DirectoryInitialization.userDirectory + "/keys/" - if (FileUtil.copyUriToInternalStorage( - result, - dstPath, - "prod.keys" - ) != null - ) { - if (NativeLibrary.reloadKeys()) { - Toast.makeText( - applicationContext, - R.string.install_keys_success, - Toast.LENGTH_SHORT - ).show() - homeViewModel.setCheckKeys(true) - gamesViewModel.reloadGames(true) - return true - } else { - MessageDialogFragment.newInstance( - this, - titleId = R.string.invalid_keys_error, - descriptionId = R.string.install_keys_failure_description, - helpLinkId = R.string.dumping_keys_quickstart_link - ).show(supportFragmentManager, MessageDialogFragment.TAG) - return false - } + val resultCode: Int = NativeLibrary.installKeys(result.toString(), extension) + + if (resultCode == 0) { + // TODO(crueter): It may be worth it to switch some of these Toasts to snackbars, + // since most of it is foreground-only anyways. + Toast.makeText( + applicationContext, + R.string.keys_install_success, + Toast.LENGTH_SHORT + ).show() + + gamesViewModel.reloadGames(true) + + return } - return false + + val resultString: String = + resources.getStringArray(R.array.installKeysResults)[resultCode] + + MessageDialogFragment.newInstance( + titleId = R.string.keys_failed, + descriptionString = resultString, + helpLinkId = R.string.keys_missing_help + ).show(supportFragmentManager, MessageDialogFragment.TAG) } - val getFirmware = - registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) { - return@registerForActivityResult - } - - val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") } - - val firmwarePath = - File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/") - val cacheFirmwareDir = File("${cacheDir.path}/registered/") - - ProgressDialogFragment.newInstance( - this, - R.string.firmware_installing - ) { progressCallback, _ -> - var messageToShow: Any - try { - FileUtil.unzipToInternalStorage( - result.toString(), - cacheFirmwareDir, - progressCallback - ) - val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 - val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 - messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { - MessageDialogFragment.newInstance( - this, - titleId = R.string.firmware_installed_failure, - descriptionId = R.string.firmware_installed_failure_description - ) - } else { - firmwarePath.deleteRecursively() - cacheFirmwareDir.copyRecursively(firmwarePath, true) - NativeLibrary.initializeSystem(true) - homeViewModel.setCheckKeys(true) - getString(R.string.save_file_imported_success) - } - } catch (e: Exception) { - Log.error("[MainActivity] Firmware install failed - ${e.message}") - messageToShow = getString(R.string.fatal_error) - } finally { - cacheFirmwareDir.deleteRecursively() - } - messageToShow - }.show(supportFragmentManager, ProgressDialogFragment.TAG) + val getFirmware = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result != null) { + processFirmware(result) } + } + + fun processFirmware(result: Uri, onComplete: (() -> Unit)? = null) { + val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") } + + val firmwarePath = + File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/") + val cacheFirmwareDir = File("${cacheDir.path}/registered/") + + ProgressDialogFragment.newInstance( + this, + R.string.firmware_installing + ) { progressCallback, _ -> + var messageToShow: Any + try { + FileUtil.unzipToInternalStorage( + result.toString(), + cacheFirmwareDir, + progressCallback + ) + val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 + val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 + messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { + MessageDialogFragment.newInstance( + this, + titleId = R.string.firmware_installed_failure, + descriptionId = R.string.firmware_installed_failure_description + ) + } else { + firmwarePath.deleteRecursively() + cacheFirmwareDir.copyRecursively(firmwarePath, true) + NativeLibrary.initializeSystem(true) + homeViewModel.setCheckKeys(true) + getString(R.string.save_file_imported_success) + } + } catch (e: Exception) { + Log.error("[MainActivity] Firmware install failed - ${e.message}") + messageToShow = getString(R.string.fatal_error) + } finally { + cacheFirmwareDir.deleteRecursively() + } + messageToShow + }.apply { + onDialogComplete = onComplete + }.show(supportFragmentManager, ProgressDialogFragment.TAG) + } + fun uninstallFirmware() { - val firmwarePath = File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/") + val firmwarePath = + File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/") ProgressDialogFragment.newInstance( this, R.string.firmware_uninstalling @@ -401,49 +451,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider { messageToShow }.show(supportFragmentManager, ProgressDialogFragment.TAG) } - val getAmiiboKey = - registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) { - return@registerForActivityResult - } - - if (FileUtil.getExtension(result) != "bin") { - MessageDialogFragment.newInstance( - this, - titleId = R.string.reading_keys_failure, - descriptionId = R.string.install_amiibo_keys_failure_extension_description - ).show(supportFragmentManager, MessageDialogFragment.TAG) - return@registerForActivityResult - } - - contentResolver.takePersistableUriPermission( - result, - Intent.FLAG_GRANT_READ_URI_PERMISSION - ) - - val dstPath = DirectoryInitialization.userDirectory + "/keys/" - if (FileUtil.copyUriToInternalStorage( - result, - dstPath, - "key_retail.bin" - ) != null - ) { - if (NativeLibrary.reloadKeys()) { - Toast.makeText( - applicationContext, - R.string.install_keys_success, - Toast.LENGTH_SHORT - ).show() - } else { - MessageDialogFragment.newInstance( - this, - titleId = R.string.invalid_keys_error, - descriptionId = R.string.install_keys_failure_description, - helpLinkId = R.string.dumping_keys_quickstart_link - ).show(supportFragmentManager, MessageDialogFragment.TAG) - } - } - } val installGameUpdate = registerForActivityResult( ActivityResultContracts.OpenMultipleDocuments() @@ -527,7 +534,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { addonViewModel.refreshAddons() - val separator = System.getProperty("line.separator") ?: "\n" + val separator = System.lineSeparator() ?: "\n" val installResult = StringBuilder() if (installSuccess > 0) { installResult.append( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/CompatUtils.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/CompatUtils.kt index 6467f7c931..640aa8f80c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/CompatUtils.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/CompatUtils.kt @@ -1,8 +1,4 @@ -// Copyright 2024 Mandarine Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.utils diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/CustomSettingsHandler.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/CustomSettingsHandler.kt new file mode 100644 index 0000000000..377313d0aa --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/CustomSettingsHandler.kt @@ -0,0 +1,474 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.utils + +import android.content.Context +import android.widget.Toast +import androidx.fragment.app.FragmentActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.model.Game +import java.io.File +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +import android.net.Uri +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.yuzu.yuzu_emu.databinding.DialogProgressBinding +import android.view.LayoutInflater +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.coroutines.CoroutineScope + +object CustomSettingsHandler { + const val CUSTOM_CONFIG_ACTION = "dev.eden.eden_emulator.LAUNCH_WITH_CUSTOM_CONFIG" + const val EXTRA_TITLE_ID = "title_id" + const val EXTRA_CUSTOM_SETTINGS = "custom_settings" + + /** + * Apply custom settings from a string instead of loading from file + * @param titleId The game title ID (16-digit hex string) + * @param customSettings The complete INI file content as string + * @param context Application context + * @return Game object created from title ID, or null if not found + */ + fun applyCustomSettings(titleId: String, customSettings: String, context: Context): Game? { + // For synchronous calls without driver checking + Log.info("[CustomSettingsHandler] Applying custom settings for title ID: $titleId") + // Find the game by title ID + val game = findGameByTitleId(titleId, context) + if (game == null) { + Log.error("[CustomSettingsHandler] Game not found for title ID: $titleId") + return null + } + + // Check if config already exists - this should be handled by the caller + val configFile = getConfigFile(game) + if (configFile.exists()) { + Log.warning( + "[CustomSettingsHandler] Config file already exists for game: ${game.title}" + ) + } + + // Write the config file + if (!writeConfigFile(game, customSettings)) { + Log.error("[CustomSettingsHandler] Failed to write config file") + return null + } + + // Initialize per-game config + try { + val fileName = FileUtil.getFilename(Uri.parse(game.path)) + NativeConfig.initializePerGameConfig(game.programId, fileName) + Log.info("[CustomSettingsHandler] Successfully applied custom settings") + return game + } catch (e: Exception) { + Log.error("[CustomSettingsHandler] Failed to apply custom settings: ${e.message}") + return null + } + } + + /** + * Apply custom settings with automatic driver checking and installation + * @param titleId The game title ID (16-digit hex string) + * @param customSettings The complete INI file content as string + * @param context Application context + * @param activity Fragment activity for driver installation dialogs (optional) + * @param driverViewModel DriverViewModel for driver management (optional) + * @return Game object created from title ID, or null if not found + */ + suspend fun applyCustomSettingsWithDriverCheck( + titleId: String, + customSettings: String, + context: Context, + activity: FragmentActivity?, + driverViewModel: DriverViewModel? + ): Game? { + Log.info("[CustomSettingsHandler] Applying custom settings for title ID: $titleId") + // Find the game by title ID + val game = findGameByTitleId(titleId, context) + if (game == null) { + Log.error("[CustomSettingsHandler] Game not found for title ID: $titleId") + // This will be handled by the caller to show appropriate error message + return null + } + + // Check if config already exists + val configFile = getConfigFile(game) + if (configFile.exists() && activity != null) { + Log.info( + "[CustomSettingsHandler] Config file already exists, asking user for confirmation" + ) + Toast.makeText( + activity, + activity.getString(R.string.config_exists_prompt), + Toast.LENGTH_SHORT + ).show() + val shouldOverwrite = askUserToOverwriteConfig(activity, game.title) + if (!shouldOverwrite) { + Log.info("[CustomSettingsHandler] User chose not to overwrite existing config") + Toast.makeText( + activity, + activity.getString(R.string.overwrite_cancelled), + Toast.LENGTH_SHORT + ).show() + return null + } + } + + // Check for driver requirements if activity and driverViewModel are provided + if (activity != null && driverViewModel != null) { + val rawDriverPath = extractDriverPath(customSettings) + if (rawDriverPath != null) { + // Normalize to local storage path (we only store drivers under driverStoragePath) + val driverFilename = rawDriverPath.substringAfterLast('/') + .substringAfterLast('\\') + val localDriverPath = "${GpuDriverHelper.driverStoragePath}$driverFilename" + Log.info("[CustomSettingsHandler] Custom settings specify driver: $rawDriverPath (normalized: $localDriverPath)") + + // Check if driver exists in the driver storage + val driverFile = File(localDriverPath) + if (!driverFile.exists()) { + Log.info("[CustomSettingsHandler] Driver not found locally: ${driverFile.name}") + + // Ask user if they want to download the missing driver + val shouldDownload = askUserToDownloadDriver(activity, driverFile.name) + if (!shouldDownload) { + Log.info("[CustomSettingsHandler] User declined to download driver") + Toast.makeText( + activity, + activity.getString(R.string.driver_download_cancelled), + Toast.LENGTH_SHORT + ).show() + return null + } + + // Check network connectivity after user consent + if (!DriverResolver.isNetworkAvailable(activity)) { + Log.error("[CustomSettingsHandler] No network connection available") + Toast.makeText( + activity, + activity.getString(R.string.network_unavailable), + Toast.LENGTH_LONG + ).show() + return null + } + + Log.info("[CustomSettingsHandler] User approved, downloading driver") + + // Show progress dialog for driver download + val dialogBinding = DialogProgressBinding.inflate(LayoutInflater.from(activity)) + dialogBinding.progressBar.isIndeterminate = false + dialogBinding.title.text = activity.getString(R.string.installing_driver) + dialogBinding.status.text = activity.getString(R.string.downloading) + + val progressDialog = MaterialAlertDialogBuilder(activity) + .setView(dialogBinding.root) + .setCancelable(false) + .create() + + withContext(Dispatchers.Main) { + progressDialog.show() + } + + try { + // Set up progress channel for thread-safe UI updates + val progressChannel = Channel(Channel.CONFLATED) + val progressJob = CoroutineScope(Dispatchers.Main).launch { + for (progress in progressChannel) { + dialogBinding.progressBar.progress = progress + } + } + + // Attempt to download and install the driver + val driverUri = DriverResolver.ensureDriverAvailable(driverFilename, activity) { progress -> + progressChannel.trySend(progress.toInt()) + } + + progressChannel.close() + progressJob.cancel() + + withContext(Dispatchers.Main) { + progressDialog.dismiss() + } + + if (driverUri == null) { + Log.error( + "[CustomSettingsHandler] Failed to download driver: ${driverFile.name}" + ) + Toast.makeText( + activity, + activity.getString( + R.string.custom_settings_failed_message, + game.title, + activity.getString(R.string.driver_not_found, driverFile.name) + ), + Toast.LENGTH_LONG + ).show() + return null + } + + // Verify the downloaded driver (from normalized local path) + val installedFile = File(localDriverPath) + val metadata = GpuDriverHelper.getMetadataFromZip(installedFile) + if (metadata.name == null) { + Log.error( + "[CustomSettingsHandler] Downloaded driver is invalid: $localDriverPath" + ) + Toast.makeText( + activity, + activity.getString( + R.string.custom_settings_failed_message, + game.title, + activity.getString( + R.string.invalid_driver_file, + driverFile.name + ) + ), + Toast.LENGTH_LONG + ).show() + return null + } + + // Add to driver list + driverViewModel.onDriverAdded(Pair(localDriverPath, metadata)) + Log.info( + "[CustomSettingsHandler] Successfully downloaded and installed driver: ${metadata.name}" + ) + + Toast.makeText( + activity, + activity.getString( + R.string.successfully_installed, + metadata.name ?: driverFile.name + ), + Toast.LENGTH_SHORT + ).show() + } catch (e: Exception) { + withContext(Dispatchers.Main) { + progressDialog.dismiss() + } + Log.error("[CustomSettingsHandler] Error downloading driver: ${e.message}") + Toast.makeText( + activity, + activity.getString( + R.string.custom_settings_failed_message, + game.title, + e.message ?: activity.getString( + R.string.driver_not_found, + driverFile.name + ) + ), + Toast.LENGTH_LONG + ).show() + return null + } + } else { + // Driver exists, verify it's valid + val metadata = GpuDriverHelper.getMetadataFromZip(driverFile) + if (metadata.name == null) { + Log.error("[CustomSettingsHandler] Invalid driver file: $localDriverPath") + Toast.makeText( + activity, + activity.getString( + R.string.custom_settings_failed_message, + game.title, + activity.getString(R.string.invalid_driver_file, driverFile.name) + ), + Toast.LENGTH_LONG + ).show() + return null + } + Log.info("[CustomSettingsHandler] Driver verified: ${metadata.name}") + } + } + } + + // Only write the config file after all checks pass + if (!writeConfigFile(game, customSettings)) { + Log.error("[CustomSettingsHandler] Failed to write config file") + activity?.let { + Toast.makeText( + it, + it.getString(R.string.config_write_failed), + Toast.LENGTH_SHORT + ).show() + } + return null + } + + // Initialize per-game config + try { + SettingsFile.loadCustomConfig(game) + Log.info("[CustomSettingsHandler] Successfully applied custom settings") + activity?.let { + Toast.makeText( + it, + it.getString(R.string.custom_settings_applied), + Toast.LENGTH_SHORT + ).show() + } + return game + } catch (e: Exception) { + Log.error("[CustomSettingsHandler] Failed to apply custom settings: ${e.message}") + activity?.let { + Toast.makeText( + it, + it.getString(R.string.config_apply_failed), + Toast.LENGTH_SHORT + ).show() + } + return null + } + } + + /** + * Find a game by its title ID in the user's game library + */ + fun findGameByTitleId(titleId: String, context: Context): Game? { + Log.info("[CustomSettingsHandler] Searching for game with title ID: $titleId") + // Convert hex title ID to decimal for comparison with programId + val programIdDecimal = try { + titleId.toLong(16).toString() + } catch (e: NumberFormatException) { + Log.error("[CustomSettingsHandler] Invalid title ID format: $titleId") + return null + } + + // Expected hex format with "0" prefix + val expectedHex = "0${titleId.uppercase()}" + // First check cached games for fast lookup + GameHelper.cachedGameList.find { game -> + game.programId == programIdDecimal || + game.programIdHex.equals(expectedHex, ignoreCase = true) + }?.let { foundGame -> + Log.info("[CustomSettingsHandler] Found game in cache: ${foundGame.title}") + return foundGame + } + // If not in cache, perform full game library scan + Log.info("[CustomSettingsHandler] Game not in cache, scanning full library...") + val allGames = GameHelper.getGames() + val foundGame = allGames.find { game -> + game.programId == programIdDecimal || + game.programIdHex.equals(expectedHex, ignoreCase = true) + } + if (foundGame != null) { + Log.info("[CustomSettingsHandler] Found game: ${foundGame.title} at ${foundGame.path}") + } else { + Log.warning("[CustomSettingsHandler] No game found for title ID: $titleId") + } + return foundGame + } + + /** + * Get the config file path for a game + */ + private fun getConfigFile(game: Game): File { + return SettingsFile.getCustomSettingsFile(game) + } + + /** + * Get the title ID config file path + */ + private fun getTitleIdConfigFile(titleId: String): File { + val configDir = File(DirectoryInitialization.userDirectory, "config/custom") + return File(configDir, "$titleId.ini") + } + + /** + * Write the config file with the custom settings + */ + private fun writeConfigFile(game: Game, customSettings: String): Boolean { + return try { + val configFile = getConfigFile(game) + val configDir = configFile.parentFile + if (configDir != null && !configDir.exists()) { + configDir.mkdirs() + } + + configFile.writeText(customSettings) + + Log.info("[CustomSettingsHandler] Wrote config file: ${configFile.absolutePath}") + true + } catch (e: Exception) { + Log.error("[CustomSettingsHandler] Failed to write config file: ${e.message}") + false + } + } + + /** + * Ask user if they want to overwrite existing configuration + */ + private suspend fun askUserToOverwriteConfig(activity: FragmentActivity, gameTitle: String): Boolean { + return suspendCoroutine { continuation -> + activity.runOnUiThread { + MaterialAlertDialogBuilder(activity) + .setTitle(activity.getString(R.string.config_already_exists_title)) + .setMessage( + activity.getString(R.string.config_already_exists_message, gameTitle) + ) + .setPositiveButton(activity.getString(R.string.overwrite)) { _, _ -> + continuation.resume(true) + } + .setNegativeButton(activity.getString(R.string.cancel)) { _, _ -> + continuation.resume(false) + } + .setCancelable(false) + .show() + } + } + } + + /** + * Ask user if they want to download a missing driver + */ + private suspend fun askUserToDownloadDriver(activity: FragmentActivity, driverName: String): Boolean { + return suspendCoroutine { continuation -> + activity.runOnUiThread { + MaterialAlertDialogBuilder(activity) + .setTitle(activity.getString(R.string.driver_missing_title)) + .setMessage( + activity.getString(R.string.driver_missing_message, driverName) + ) + .setPositiveButton(activity.getString(R.string.download)) { _, _ -> + continuation.resume(true) + } + .setNegativeButton(activity.getString(R.string.cancel)) { _, _ -> + continuation.resume(false) + } + .setCancelable(false) + .show() + } + } + } + + /** + * Extract driver path from custom settings INI content + */ + private fun extractDriverPath(customSettings: String): String? { + val lines = customSettings.lines() + var inGpuDriverSection = false + + for (line in lines) { + val trimmed = line.trim() + if (trimmed.startsWith("[") && trimmed.endsWith("]")) { + inGpuDriverSection = trimmed == "[GpuDriver]" + continue + } + + if (inGpuDriverSection && trimmed.startsWith("driver_path=")) { + return trimmed.substringAfter("driver_path=") + .trim() + .removeSurrounding("\"", "\"") + } + } + + return null + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt index de0794a17f..5325f688b6 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.utils @@ -170,7 +170,8 @@ object DirectoryInitialization { buttonEnabled, Pair(landscapeXPosition, landscapeYPosition), Pair(portraitXPosition, portraitYPosition), - Pair(foldableXPosition, foldableYPosition) + Pair(foldableXPosition, foldableYPosition), + OverlayControl.map[buttonId]?.defaultIndividualScaleResource ?: 1.0f ) overlayControlDataMap[buttonId] = controlData setOverlayData = true diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DriverResolver.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DriverResolver.kt new file mode 100644 index 0000000000..2072344bdf --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DriverResolver.kt @@ -0,0 +1,447 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.utils + +import android.content.Context +import android.net.Uri +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import org.yuzu.yuzu_emu.fragments.DriverFetcherFragment +import java.io.File +import java.io.IOException +import java.util.concurrent.TimeUnit +import java.util.concurrent.ConcurrentHashMap +import okhttp3.ConnectionPool +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import kotlinx.coroutines.delay +import kotlin.math.pow + +/** + * Resolves driver download URLs from filenames by searching GitHub repositories + */ +object DriverResolver { + private const val CONNECTION_TIMEOUT_SECONDS = 30L + private const val CACHE_DURATION_MS = 3600000L // 1 hour + private const val BUFFER_SIZE = 8192 + private const val MIN_API_CALL_INTERVAL = 2000L // 2 seconds between API calls + private const val MAX_RETRY_COUNT = 3 + + @Volatile + private var client: OkHttpClient? = null + + private fun getClient(): OkHttpClient { + return client ?: synchronized(this) { + client ?: OkHttpClient.Builder() + .connectTimeout(CONNECTION_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .readTimeout(CONNECTION_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .writeTimeout(CONNECTION_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .followRedirects(true) + .followSslRedirects(true) + .connectionPool(ConnectionPool(5, 1, TimeUnit.MINUTES)) + .build().also { client = it } + } + } + + // Driver repository paths - (from DriverFetcherFragment) might extract these to a config file later + private val repositories = listOf( + "MrPurple666/purple-turnip", + "crueter/GameHub-8Elite-Drivers", + "K11MCH1/AdrenoToolsDrivers", + "Weab-chan/freedreno_turnip-CI" + ) + + private val urlCache = ConcurrentHashMap() + private val releaseCache = ConcurrentHashMap>() + private var lastCacheTime = 0L + private var lastApiCallTime = 0L + + data class ResolvedDriver( + val downloadUrl: String, + val repoPath: String, + val releaseTag: String, + val filename: String + ) + + // Matching helpers + private val KNOWN_SUFFIXES = listOf( + ".adpkg.zip", + ".zip", + ".7z", + ".tar.gz", + ".tar.xz", + ".rar" + ) + + private fun stripKnownSuffixes(name: String): String { + var result = name + var changed: Boolean + do { + changed = false + for (s in KNOWN_SUFFIXES) { + if (result.endsWith(s, ignoreCase = true)) { + result = result.dropLast(s.length) + changed = true + } + } + } while (changed) + return result + } + + private fun normalizeName(name: String): String { + val base = stripKnownSuffixes(name.lowercase()) + // Remove non-alphanumerics to make substring checks resilient + return base.replace(Regex("[^a-z0-9]+"), " ").trim() + } + + private fun tokenize(name: String): Set = + normalizeName(name).split(Regex("\\s+")).filter { it.isNotBlank() }.toSet() + + // Jaccard similarity between two sets + private fun jaccard(a: Set, b: Set): Double { + if (a.isEmpty() || b.isEmpty()) return 0.0 + val inter = a.intersect(b).size.toDouble() + val uni = a.union(b).size.toDouble() + return if (uni == 0.0) 0.0 else inter / uni + } + + /** + * Resolve a driver download URL from its filename + * @param filename The driver filename (e.g., "turnip_mrpurple-T19-toasted.adpkg.zip") + * @return ResolvedDriver with download URL and metadata, or null if not found + */ + suspend fun resolveDriverUrl(filename: String): ResolvedDriver? { + // Validate input + require(filename.isNotBlank()) { "Filename cannot be blank" } + require(!filename.contains("..")) { "Invalid filename: path traversal detected" } + + // Check cache first + urlCache[filename]?.let { + Log.info("[DriverResolver] Found cached URL for $filename") + return it + } + + Log.info("[DriverResolver] Resolving download URL for: $filename") + + // Clear cache if expired + if (System.currentTimeMillis() - lastCacheTime > CACHE_DURATION_MS) { + releaseCache.clear() + lastCacheTime = System.currentTimeMillis() + } + + return coroutineScope { + // Search all repositories in parallel + repositories.map { repoPath -> + async { + searchRepository(repoPath, filename) + } + }.firstNotNullOfOrNull { it.await() }.also { resolved -> + // Cache the result if found + resolved?.let { + urlCache[filename] = it + Log.info("[DriverResolver] Cached resolution for $filename from ${it.repoPath}") + } + } + } + } + + /** + * Search a specific repository for a driver file + */ + private suspend fun searchRepository(repoPath: String, filename: String): ResolvedDriver? { + return withContext(Dispatchers.IO) { + try { + // Get releases from cache or fetch + val releases = releaseCache[repoPath] ?: fetchReleases(repoPath).also { + releaseCache[repoPath] = it + } + + // First pass: exact name (case-insensitive) against asset filenames + val target = filename.lowercase() + for (release in releases) { + for (artifact in release.artifacts) { + if (artifact.name.equals(filename, ignoreCase = true) || artifact.name.lowercase() == target) { + Log.info("[DriverResolver] Found $filename in $repoPath/${release.tagName}") + return@withContext ResolvedDriver( + downloadUrl = artifact.url.toString(), + repoPath = repoPath, + releaseTag = release.tagName, + filename = artifact.name + ) + } + } + } + + // Second pass: fuzzy match by asset filenames only + val reqNorm = normalizeName(filename) + val reqTokens = tokenize(filename) + var best: ResolvedDriver? = null + var bestScore = 0.0 + + for (release in releases) { + for (artifact in release.artifacts) { + val artNorm = normalizeName(artifact.name) + val artTokens = tokenize(artifact.name) + + var score = jaccard(reqTokens, artTokens) + // Boost if one normalized name contains the other + if (artNorm.contains(reqNorm) || reqNorm.contains(artNorm)) { + score = maxOf(score, 0.92) + } + + if (score > bestScore) { + bestScore = score + best = ResolvedDriver( + downloadUrl = artifact.url.toString(), + repoPath = repoPath, + releaseTag = release.tagName, + filename = artifact.name + ) + } + } + } + + // Threshold to avoid bad guesses, this worked fine in testing but might need tuning + if (best != null && bestScore >= 0.6) { + Log.info("[DriverResolver] Fuzzy matched $filename -> ${best.filename} in ${best.repoPath} (score=%.2f)".format(bestScore)) + return@withContext best + } + null + } catch (e: Exception) { + Log.error("[DriverResolver] Failed to search $repoPath: ${e.message}") + null + } + } + } + + /** + * Fetch releases from a GitHub repository + */ + private suspend fun fetchReleases(repoPath: String): List = withContext( + Dispatchers.IO + ) { + // Rate limiting + val timeSinceLastCall = System.currentTimeMillis() - lastApiCallTime + if (timeSinceLastCall < MIN_API_CALL_INTERVAL) { + delay(MIN_API_CALL_INTERVAL - timeSinceLastCall) + } + lastApiCallTime = System.currentTimeMillis() + + // Retry logic with exponential backoff + var retryCount = 0 + var lastException: Exception? = null + + while (retryCount < MAX_RETRY_COUNT) { + try { + val request = Request.Builder() + .url("https://api.github.com/repos/$repoPath/releases") + .header("Accept", "application/vnd.github.v3+json") + .build() + + return@withContext getClient().newCall(request).execute().use { response -> + when { + response.code == 404 -> throw IOException("Repository not found: $repoPath") + response.code == 403 -> { + val resetTime = response.header("X-RateLimit-Reset")?.toLongOrNull() ?: 0 + throw IOException( + "API rate limit exceeded. Resets at ${java.util.Date( + resetTime * 1000 + )}" + ) + } + !response.isSuccessful -> throw IOException( + "HTTP ${response.code}: ${response.message}" + ) + } + + val body = response.body?.string() + ?: throw IOException("Empty response from $repoPath") + + // Determine if this repo uses tag names (from DriverFetcherFragment logic) + val useTagName = repoPath.contains("K11MCH1") + val sortMode = if (useTagName) { + DriverFetcherFragment.SortMode.PublishTime + } else { + DriverFetcherFragment.SortMode.Default + } + + DriverFetcherFragment.Release.fromJsonArray(body, useTagName, sortMode) + } + } catch (e: IOException) { + lastException = e + if (retryCount == MAX_RETRY_COUNT - 1) throw e + delay((2.0.pow(retryCount) * 1000).toLong()) + retryCount++ + } + } + throw lastException ?: IOException( + "Failed to fetch releases after $MAX_RETRY_COUNT attempts" + ) + } + + /** + * Download a driver file to the cache directory + * @param resolvedDriver The resolved driver information + * @param context Android context for cache directory + * @return The downloaded file, or null if download failed + */ + suspend fun downloadDriver( + resolvedDriver: ResolvedDriver, + context: Context, + onProgress: ((Float) -> Unit)? = null + ): File? { + return withContext(Dispatchers.IO) { + try { + Log.info( + "[DriverResolver] Downloading ${resolvedDriver.filename} from ${resolvedDriver.repoPath}" + ) + + val cacheDir = context.externalCacheDir ?: throw IOException("Failed to access cache directory") + cacheDir.mkdirs() + + val file = File(cacheDir, resolvedDriver.filename) + + // If file already exists in cache and has content, return it + if (file.exists() && file.length() > 0) { + Log.info("[DriverResolver] Using cached file: ${file.absolutePath}") + return@withContext file + } + + val request = Request.Builder() + .url(resolvedDriver.downloadUrl) + .header("Accept", "application/octet-stream") + .build() + + getClient().newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw IOException("Download failed: ${response.code}") + } + + response.body?.use { body -> + val contentLength = body.contentLength() + body.byteStream().use { input -> + file.outputStream().use { output -> + val buffer = ByteArray(BUFFER_SIZE) + var totalBytesRead = 0L + var bytesRead: Int + + while (input.read(buffer).also { bytesRead = it } != -1) { + output.write(buffer, 0, bytesRead) + totalBytesRead += bytesRead + + if (contentLength > 0) { + val progress = (totalBytesRead.toFloat() / contentLength) * 100f + onProgress?.invoke(progress) + } + } + } + } + } ?: throw IOException("Empty response body") + } + + if (file.length() == 0L) { + file.delete() + throw IOException("Downloaded file is empty") + } + + Log.info( + "[DriverResolver] Successfully downloaded ${file.length()} bytes to ${file.absolutePath}" + ) + file + } catch (e: Exception) { + Log.error("[DriverResolver] Download failed: ${e.message}") + null + } + } + } + + /** + * Download and install a driver if not already present + * @param driverPath The driver filename or full path + * @param context Android context + * @param onProgress Optional progress callback (0-100) + * @return Uri of the installed driver, or null if failed + */ + suspend fun ensureDriverAvailable( + driverPath: String, + context: Context, + onProgress: ((Float) -> Unit)? = null + ): Uri? { + // Extract filename from path (support both separators) + val filename = driverPath.substringAfterLast('/').substringAfterLast('\\') + + // Check if driver already exists locally + val localPath = "${GpuDriverHelper.driverStoragePath}$filename" + val localFile = File(localPath) + + if (localFile.exists() && localFile.length() > 0) { + Log.info("[DriverResolver] Driver already exists locally: $localPath") + return Uri.fromFile(localFile) + } + + Log.info("[DriverResolver] Driver not found locally, attempting to download: $filename") + + // Resolve download URL + val resolvedDriver = resolveDriverUrl(filename) + if (resolvedDriver == null) { + Log.error("[DriverResolver] Failed to resolve download URL for $filename") + return null + } + + // Download the driver with progress callback + val downloadedFile = downloadDriver(resolvedDriver, context, onProgress) + if (downloadedFile == null) { + Log.error("[DriverResolver] Failed to download driver $filename") + return null + } + + // Install the driver to internal storage + val downloadedUri = Uri.fromFile(downloadedFile) + if (GpuDriverHelper.copyDriverToInternalStorage(downloadedUri)) { + Log.info("[DriverResolver] Successfully installed driver to internal storage") + // Clean up cache file + downloadedFile.delete() + return Uri.fromFile(File(localPath)) + } else { + Log.error("[DriverResolver] Failed to copy driver to internal storage") + downloadedFile.delete() + return null + } + } + + /** + * Check network connectivity + */ + fun isNetworkAvailable(context: Context): Boolean { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager + ?: return false + val network = connectivityManager.activeNetwork ?: return false + val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } + + /** + * Clear all caches + */ + fun clearCache() { + urlCache.clear() + releaseCache.clear() + lastCacheTime = 0L + lastApiCallTime = 0L + } + + /** + * Clean up resources + */ + fun cleanup() { + client?.dispatcher?.executorService?.shutdown() + client?.connectionPool?.evictAll() + client = null + clearCache() + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt index fc2339f5a2..52ee7b01ea 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -24,6 +27,7 @@ import java.nio.charset.StandardCharsets import java.util.zip.Deflater import java.util.zip.ZipOutputStream import kotlin.IllegalStateException +import androidx.core.net.toUri object FileUtil { const val PATH_TREE = "tree" @@ -195,6 +199,12 @@ object FileUtil { * @return String display name */ fun getFilename(uri: Uri): String { + if (uri.scheme == "file") { + return uri.lastPathSegment?.takeIf { it.isNotEmpty() } ?: throw IOException( + "Invalid file URI: $uri" + ) + } + val resolver = YuzuApplication.appContext.contentResolver val columns = arrayOf( DocumentsContract.Document.COLUMN_DISPLAY_NAME @@ -236,7 +246,7 @@ object FileUtil { var size: Long = 0 var c: Cursor? = null try { - val mUri = Uri.parse(path) + val mUri = path.toUri() c = resolver.query(mUri, columns, null, null, null) c!!.moveToNext() size = c.getLong(0) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.kt new file mode 100644 index 0000000000..c181656d99 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.kt @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.yuzu.yuzu_emu.utils + +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.os.IBinder +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.activities.EmulationActivity + +/** + * A service that shows a permanent notification in the background to avoid the app getting + * cleared from memory by the system. + */ +class ForegroundService : Service() { + companion object { + const val EMULATION_RUNNING_NOTIFICATION = 0x1000 + + const val ACTION_STOP = "stop" + } + + private fun showRunningNotification() { + // Intent is used to resume emulation if the notification is clicked + val contentIntent = PendingIntent.getActivity( + this, + 0, + Intent(this, EmulationActivity::class.java), + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + val builder = + NotificationCompat.Builder(this, getString(R.string.app_notification_channel_id)) + .setSmallIcon(R.drawable.ic_stat_notification_logo) + .setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.app_notification_running)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setOngoing(true) + .setVibrate(null) + .setSound(null) + .setContentIntent(contentIntent) + startForeground(EMULATION_RUNNING_NOTIFICATION, builder.build()) + } + + override fun onBind(intent: Intent): IBinder? { + return null + } + + override fun onCreate() { + showRunningNotification() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent?.action == ACTION_STOP) { + try { + NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION) + stopForeground(STOP_FOREGROUND_REMOVE) + } catch (e: Exception) { + Log.error("Failed to stop foreground service") + } + stopSelfResult(startId) + return START_NOT_STICKY + } + + if (intent != null) { + showRunningNotification() + } + return START_STICKY + } + + override fun onDestroy() = + NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt index 579b600f1a..e27bc94696 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + package org.yuzu.yuzu_emu.utils import android.content.SharedPreferences @@ -13,11 +16,15 @@ import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.GameDir import org.yuzu.yuzu_emu.model.MinimalDocumentFile +import androidx.core.content.edit +import androidx.core.net.toUri object GameHelper { private const val KEY_OLD_GAME_PATH = "game_path" const val KEY_GAMES = "Games" + var cachedGameList = mutableListOf() + private lateinit var preferences: SharedPreferences fun getGames(): List { @@ -29,7 +36,7 @@ object GameHelper { val oldGamesDir = preferences.getString(KEY_OLD_GAME_PATH, "") ?: "" if (oldGamesDir.isNotEmpty()) { gameDirs.add(GameDir(oldGamesDir, true)) - preferences.edit().remove(KEY_OLD_GAME_PATH).apply() + preferences.edit() { remove(KEY_OLD_GAME_PATH) } } gameDirs.addAll(NativeConfig.getGameDirs()) @@ -44,7 +51,7 @@ object GameHelper { val badDirs = mutableListOf() gameDirs.forEachIndexed { index: Int, gameDir: GameDir -> - val gameDirUri = Uri.parse(gameDir.uriString) + val gameDirUri = gameDir.uriString.toUri() val isValid = FileUtil.isTreeUriValid(gameDirUri) if (isValid) { addGamesRecursive( @@ -72,11 +79,12 @@ object GameHelper { games.forEach { serializedGames.add(Json.encodeToString(it)) } - preferences.edit() - .remove(KEY_GAMES) - .putStringSet(KEY_GAMES, serializedGames) - .apply() + preferences.edit() { + remove(KEY_GAMES) + .putStringSet(KEY_GAMES, serializedGames) + } + cachedGameList = games.toMutableList() return games.toList() } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt index a72dea8f10..99f7fd81fe 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -177,6 +180,10 @@ object GpuDriverHelper { * @return A non-null [GpuDriverMetadata] instance that may have null members */ fun getMetadataFromZip(driver: File): GpuDriverMetadata { + if (!driver.exists()) { + return GpuDriverMetadata() + } + try { ZipFile(driver).use { zf -> val entries = zf.entries() @@ -202,6 +209,11 @@ object GpuDriverHelper { hookLibPath: String = GpuDriverHelper.hookLibPath!! ): Array? + external fun getGpuModel( + surface: Surface = Surface(SurfaceTexture(true)), + hookLibPath: String = GpuDriverHelper.hookLibPath!! + ): String? + // Parse the custom driver metadata to retrieve the name. val installedCustomDriverData: GpuDriverMetadata get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME)) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUpdater.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUpdater.kt new file mode 100644 index 0000000000..ec492569ce --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUpdater.kt @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.utils + +import android.os.Handler +import android.os.Looper +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.YuzuApplication + +object PowerStateUpdater { + + private lateinit var handler: Handler + private lateinit var runnable: Runnable + private const val UPDATE_INTERVAL_MS = 1000L + private var isStarted = false + + fun start() { + if (isStarted) { + return + } + + val context = YuzuApplication.appContext + + handler = Handler(Looper.getMainLooper()) + runnable = Runnable { + val info = PowerStateUtils.getBatteryInfo(context) + NativeLibrary.updatePowerState(info[0], info[1] == 1, info[2] == 1) + handler.postDelayed(runnable, UPDATE_INTERVAL_MS) + } + handler.post(runnable) + isStarted = true + } + + fun stop() { + if (!isStarted) { + return + } + if (::handler.isInitialized) { + handler.removeCallbacks(runnable) + } + isStarted = false + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUtils.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUtils.kt new file mode 100644 index 0000000000..48382fad5b --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUtils.kt @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.utils + +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import android.os.Build + +object PowerStateUtils { + + @JvmStatic + fun getBatteryInfo(context: Context?): IntArray { + if (context == null) { + return intArrayOf(0, 0, 0) // Percentage, IsCharging, HasBattery + } + + val results = intArrayOf(100, 0, 1) + val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + val batteryStatusIntent: Intent? = context.registerReceiver(null, iFilter) + + if (batteryStatusIntent != null) { + val present = batteryStatusIntent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true) + if (!present) { + results[2] = 0; results[0] = 0; results[1] = 0; return results + } + results[2] = 1 + val level = batteryStatusIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + val scale = batteryStatusIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + if (level != -1 && scale != -1 && scale != 0) { + results[0] = (level.toFloat() / scale.toFloat() * 100.0f).toInt() + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager? + results[0] = bm?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) ?: 100 + } + val status = batteryStatusIntent.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + results[1] = if (status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL) 1 else 0 + } + + return results + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt index 89ec3672ca..3e138c0244 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project +// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.utils @@ -23,10 +23,12 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings object ThemeHelper { const val SYSTEM_BAR_ALPHA = 0.9f + // Listener that detects if the theme keys are being changed from the setting menu and recreates the activity private var listener: SharedPreferences.OnSharedPreferenceChangeListener? = null - private val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) - + private val preferences = PreferenceManager.getDefaultSharedPreferences( + YuzuApplication.appContext + ) fun setTheme(activity: AppCompatActivity) { setThemeMode(activity) @@ -52,6 +54,7 @@ object ThemeHelper { private fun getSelectedStaticThemeColor(): Int { val themeIndex = preferences.getInt(Settings.PREF_STATIC_THEME_COLOR, 0) val themes = arrayOf( + R.style.Theme_Eden_Main, R.style.Theme_Yuzu_Main_Violet, R.style.Theme_Yuzu_Main_Blue, R.style.Theme_Yuzu_Main_Cyan, @@ -120,7 +123,11 @@ object ThemeHelper { fun ThemeChangeListener(activity: AppCompatActivity) { listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - val relevantKeys = listOf(Settings.PREF_STATIC_THEME_COLOR, Settings.PREF_THEME_MODE, Settings.PREF_BLACK_BACKGROUNDS) + val relevantKeys = listOf( + Settings.PREF_STATIC_THEME_COLOR, + Settings.PREF_THEME_MODE, + Settings.PREF_BLACK_BACKGROUNDS + ) if (key in relevantKeys) { activity.recreate() } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/CarouselRecyclerView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/CarouselRecyclerView.kt new file mode 100644 index 0000000000..2c35e7349a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/CarouselRecyclerView.kt @@ -0,0 +1,469 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.ui + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.PagerSnapHelper +import androidx.recyclerview.widget.RecyclerView +import kotlin.math.abs +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.adapters.GameAdapter +import androidx.core.view.doOnNextLayout +import org.yuzu.yuzu_emu.YuzuApplication +import androidx.preference.PreferenceManager +import androidx.core.view.WindowInsetsCompat + +/** + * CarouselRecyclerView encapsulates all carousel logic for the games UI. + * It manages overlapping cards, center snapping, custom drawing order, + * joypad & fling navigation and mid-screen swipe-to-refresh. + */ +class CarouselRecyclerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : RecyclerView(context, attrs, defStyle) { + + private var overlapFactor: Float = 0f + private var overlapPx: Int = 0 + private var overlapDecoration: OverlappingDecoration? = null + private var pagerSnapHelper: PagerSnapHelper? = null + private var scalingScrollListener: OnScrollListener? = null + + companion object { + private const val CAROUSEL_CARD_SIZE_FACTOR = "CarouselCardSizeMultiplier" + private const val CAROUSEL_BORDERCARDS_SCALE = "CarouselBorderCardsScale" + private const val CAROUSEL_BORDERCARDS_ALPHA = "CarouselBorderCardsAlpha" + private const val CAROUSEL_OVERLAP_FACTOR = "CarouselOverlapFactor" + private const val CAROUSEL_MAX_FLING_COUNT = "CarouselMaxFlingCount" + private const val CAROUSEL_FLING_MULTIPLIER = "CarouselFlingMultiplier" + private const val CAROUSEL_CARDS_SCALING_SHAPE = "CarouselCardsScalingShape" + private const val CAROUSEL_CARDS_ALPHA_SHAPE = "CarouselCardsAlphaShape" + const val CAROUSEL_LAST_SCROLL_POSITION = "CarouselLastScrollPosition" + const val CAROUSEL_VIEW_TYPE_PORTRAIT = "GamesViewTypePortrait" + const val CAROUSEL_VIEW_TYPE_LANDSCAPE = "GamesViewTypeLandscape" + } + + private val preferences = + PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + + var flingMultiplier: Float = 1f + + var pendingScrollAfterReload: Boolean = false + + var useCustomDrawingOrder: Boolean = false + set(value) { + field = value + setChildrenDrawingOrderEnabled(value) + invalidate() + } + + init { + setChildrenDrawingOrderEnabled(true) + } + + private fun calculateCenter(width: Int, paddingStart: Int, paddingEnd: Int): Int { + return paddingStart + (width - paddingStart - paddingEnd) / 2 + } + + private fun getRecyclerViewCenter(): Float { + return calculateCenter(width, paddingLeft, paddingRight).toFloat() + } + + private fun getLayoutManagerCenter(layoutManager: RecyclerView.LayoutManager): Int { + return if (layoutManager is LinearLayoutManager) { + calculateCenter( + layoutManager.width, + layoutManager.paddingStart, + layoutManager.paddingEnd + ) + } else { + width / 2 + } + } + + private fun getChildDistanceToCenter(view: View): Float { + return 0.5f * (view.left + view.right) - getRecyclerViewCenter() + } + + fun restoreScrollState(position: Int = 0, attempts: Int = 0) { + val lm = layoutManager as? LinearLayoutManager ?: return + if (lm.findLastVisibleItemPosition() == RecyclerView.NO_POSITION && attempts < 10) { + post { restoreScrollState(position, attempts + 1) } + return + } + scrollToPosition(position) + } + + fun getClosestChildPosition(fullRange: Boolean = false): Int { + val lm = layoutManager as? LinearLayoutManager ?: return RecyclerView.NO_POSITION + var minDistance = Int.MAX_VALUE + var closestPosition = RecyclerView.NO_POSITION + val start = if (fullRange) 0 else lm.findFirstVisibleItemPosition() + val end = if (fullRange) lm.childCount - 1 else lm.findLastVisibleItemPosition() + for (i in start..end) { + val child = lm.findViewByPosition(i) ?: continue + val distance = kotlin.math.abs(getChildDistanceToCenter(child).toInt()) + if (distance < minDistance) { + minDistance = distance + closestPosition = i + } + } + return closestPosition + } + + fun updateChildScalesAndAlpha() { + for (i in 0 until childCount) { + val child = getChildAt(i) ?: continue + updateChildScaleAndAlphaForPosition(child) + } + } + + fun shapingFunction(x: Float, option: Int = 0): Float { + return when (option) { + 0 -> 1f // Off + 1 -> 1f - x // linear descending + 2 -> (1f - x) * (1f - x) // Ease out + 3 -> if (x < 0.05f) 1f else (1f - x) * 0.8f + 4 -> kotlin.math.cos(x * Math.PI).toFloat() // Cosine + 5 -> kotlin.math.cos((1.5f * x).coerceIn(0f, 1f) * Math.PI).toFloat() // Cosine 1.5x trimmed + else -> 1f // Default to Off + } + } + + fun updateChildScaleAndAlphaForPosition(child: View) { + val cardSize = (adapter as? GameAdapter ?: return).cardSize + val position = getChildViewHolder(child).bindingAdapterPosition + if (position == RecyclerView.NO_POSITION || cardSize <= 0) { + return // No valid position or card size + } + child.layoutParams.width = cardSize + child.layoutParams.height = cardSize + + val center = getRecyclerViewCenter() + val distance = abs(getChildDistanceToCenter(child)) + val internalBorderScale = resources.getFraction(R.fraction.carousel_bordercards_scale, 1, 1) + val borderScale = preferences.getFloat(CAROUSEL_BORDERCARDS_SCALE, internalBorderScale).coerceIn( + 0f, + 1f + ) + + val shapeInput = (distance / center).coerceIn(0f, 1f) + val internalShapeSetting = resources.getInteger(R.integer.carousel_cards_scaling_shape) + val scalingShapeSetting = preferences.getInt( + CAROUSEL_CARDS_SCALING_SHAPE, + internalShapeSetting + ) + val shapedScaling = shapingFunction(shapeInput, scalingShapeSetting) + val scale = (borderScale + (1f - borderScale) * shapedScaling).coerceIn(0f, 1f) + + val maxDistance = width / 2f + val alphaInput = (distance / maxDistance).coerceIn(0f, 1f) + val internalBordersAlpha = resources.getFraction( + R.fraction.carousel_bordercards_alpha, + 1, + 1 + ) + val borderAlpha = preferences.getFloat(CAROUSEL_BORDERCARDS_ALPHA, internalBordersAlpha).coerceIn( + 0f, + 1f + ) + val internalAlphaShapeSetting = resources.getInteger(R.integer.carousel_cards_alpha_shape) + val alphaShapeSetting = preferences.getInt( + CAROUSEL_CARDS_ALPHA_SHAPE, + internalAlphaShapeSetting + ) + val shapedAlpha = shapingFunction(alphaInput, alphaShapeSetting) + val alpha = (borderAlpha + (1f - borderAlpha) * shapedAlpha).coerceIn(0f, 1f) + + child.animate().cancel() + child.alpha = alpha + child.scaleX = scale + child.scaleY = scale + } + + fun focusCenteredCard() { + val centeredPos = getClosestChildPosition() + if (centeredPos != RecyclerView.NO_POSITION) { + val vh = findViewHolderForAdapterPosition(centeredPos) + vh?.itemView?.let { child -> + child.isFocusable = true + child.isFocusableInTouchMode = true + child.requestFocus() + } + } + } + + fun setCarouselMode(enabled: Boolean, gameAdapter: GameAdapter? = null) { + if (enabled) { + useCustomDrawingOrder = true + + val insets = rootWindowInsets?.let { WindowInsetsCompat.toWindowInsetsCompat(it, this) } + val bottomInset = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom ?: 0 + val internalFactor = resources.getFraction(R.fraction.carousel_card_size_factor, 1, 1) + val userFactor = preferences.getFloat(CAROUSEL_CARD_SIZE_FACTOR, internalFactor).coerceIn( + 0f, + 1f + ) + val cardSize = (userFactor * (height - bottomInset)).toInt() + gameAdapter?.setCardSize(cardSize) + + val internalOverlapFactor = resources.getFraction( + R.fraction.carousel_overlap_factor, + 1, + 1 + ) + overlapFactor = preferences.getFloat(CAROUSEL_OVERLAP_FACTOR, internalOverlapFactor).coerceIn( + 0f, + 1f + ) + overlapPx = (cardSize * overlapFactor).toInt() + + val internalFlingMultiplier = resources.getFraction( + R.fraction.carousel_fling_multiplier, + 1, + 1 + ) + flingMultiplier = preferences.getFloat( + CAROUSEL_FLING_MULTIPLIER, + internalFlingMultiplier + ).coerceIn(1f, 5f) + + gameAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onChanged() { + if (pendingScrollAfterReload) { + post { + jigglyScroll() + pendingScrollAfterReload = false + } + } + } + }) + + // Detach SnapHelper during setup + pagerSnapHelper?.attachToRecyclerView(null) + + // Add overlap decoration if not present + if (overlapDecoration == null) { + overlapDecoration = OverlappingDecoration(overlapPx) + addItemDecoration(overlapDecoration!!) + } + + // Gradual scalingAdd commentMore actions + if (scalingScrollListener == null) { + scalingScrollListener = object : OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + updateChildScalesAndAlpha() + } + } + addOnScrollListener(scalingScrollListener!!) + } + + if (cardSize > 0) { + val topPadding = ((height - bottomInset - cardSize) / 2).coerceAtLeast(0) // Center vertically + val sidePadding = (width - cardSize) / 2 // Center first/last card + setPadding(sidePadding, topPadding, sidePadding, 0) + clipToPadding = false + } + + if (pagerSnapHelper == null) { + pagerSnapHelper = CenterPagerSnapHelper() + pagerSnapHelper!!.attachToRecyclerView(this) + } + } else { + // Remove overlap decoration + overlapDecoration?.let { removeItemDecoration(it) } + overlapDecoration = null + // Remove scaling scroll listener + scalingScrollListener?.let { removeOnScrollListener(it) } + scalingScrollListener = null + // Detach PagerSnapHelper + pagerSnapHelper?.attachToRecyclerView(null) + pagerSnapHelper = null + useCustomDrawingOrder = false + // Reset padding and fling + setPadding(0, 0, 0, 0) + clipToPadding = true + flingMultiplier = 1f + // Reset scaling + for (i in 0 until childCount) { + val child = getChildAt(i) + child?.scaleX = 1f + child?.scaleY = 1f + child?.alpha = 1f + } + } + } + + override fun onScrollStateChanged(state: Int) { + super.onScrollStateChanged(state) + if (state == RecyclerView.SCROLL_STATE_IDLE) { + focusCenteredCard() + } + } + + override fun scrollToPosition(position: Int) { + super.scrollToPosition(position) + (layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(position, overlapPx) + doOnNextLayout { + updateChildScalesAndAlpha() + focusCenteredCard() + } + } + + private var lastFocusSearchTime: Long = 0 + override fun focusSearch(focused: View, direction: Int): View? { + if (layoutManager !is LinearLayoutManager) return super.focusSearch(focused, direction) + val vh = findContainingViewHolder(focused) ?: return super.focusSearch(focused, direction) + val itemCount = adapter?.itemCount ?: return super.focusSearch(focused, direction) + val position = vh.bindingAdapterPosition + + return when (direction) { + View.FOCUS_LEFT -> { + if (position > 0) { + val now = System.currentTimeMillis() + val repeatDetected = (now - lastFocusSearchTime) < resources.getInteger( + R.integer.carousel_focus_search_repeat_threshold_ms + ) + lastFocusSearchTime = now + if (!repeatDetected) { // ensures the first run + val offset = focused.width - overlapPx + smoothScrollBy(-offset, 0) + } + findViewHolderForAdapterPosition(position - 1)?.itemView ?: super.focusSearch( + focused, + direction + ) + } else { + focused + } + } + View.FOCUS_RIGHT -> { + if (position < itemCount - 1) { + findViewHolderForAdapterPosition(position + 1)?.itemView ?: super.focusSearch( + focused, + direction + ) + } else { + focused + } + } + else -> super.focusSearch(focused, direction) + } + } + + // Custom fling multiplier for carousel + override fun fling(velocityX: Int, velocityY: Int): Boolean { + val newVelocityX = (velocityX * flingMultiplier).toInt() + val newVelocityY = (velocityY * flingMultiplier).toInt() + return super.fling(newVelocityX, newVelocityY) + } + + // Custom drawing order for carousel (for alpha fade) + override fun getChildDrawingOrder(childCount: Int, i: Int): Int { + if (!useCustomDrawingOrder || childCount == 0) return i + val children = (0 until childCount).map { idx -> + val distance = abs(getChildDistanceToCenter(getChildAt(idx))) + Pair(idx, distance) + } + val sorted = children.sortedWith( + compareByDescending> { it.second } + .thenBy { it.first } + ) + return sorted[i].first + } + + fun jigglyScroll() { + scrollBy(-1, 0) + scrollBy(1, 0) + focusCenteredCard() + } + + inner class OverlappingDecoration(private val overlap: Int) : ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: State + ) { + val position = parent.getChildAdapterPosition(view) + if (position > 0) { + outRect.left = -overlap + } + } + } + + inner class VerticalCenterDecoration : ItemDecoration() { + override fun getItemOffsets( + outRect: android.graphics.Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + val parentHeight = parent.height + val childHeight = view.layoutParams.height.takeIf { it > 0 } + ?: view.measuredHeight.takeIf { it > 0 } + ?: view.height + + if (parentHeight > 0 && childHeight > 0) { + val verticalPadding = ((parentHeight - childHeight) / 2).coerceAtLeast(0) + outRect.top = verticalPadding + outRect.bottom = verticalPadding + } + } + } + + inner class CenterPagerSnapHelper : PagerSnapHelper() { + + // NEEDED: fixes center snapping, but introduces ghost movement + override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? { + if (layoutManager !is LinearLayoutManager) return null + return layoutManager.findViewByPosition(getClosestChildPosition()) + } + + // NEEDED: fixes ghost movement when snapping, but breaks inertial scrolling + override fun calculateDistanceToFinalSnap( + layoutManager: RecyclerView.LayoutManager, + targetView: View + ): IntArray? { + if (layoutManager !is LinearLayoutManager) { + return super.calculateDistanceToFinalSnap( + layoutManager, + targetView + ) + } + val out = IntArray(2) + out[0] = getChildDistanceToCenter(targetView).toInt() + out[1] = 0 + return out + } + + // NEEDED: fixes inertial scrolling (broken by calculateDistanceToFinalSnap) + override fun findTargetSnapPosition( + layoutManager: RecyclerView.LayoutManager, + velocityX: Int, + velocityY: Int + ): Int { + if (layoutManager !is LinearLayoutManager) return RecyclerView.NO_POSITION + val closestPosition = this@CarouselRecyclerView.getClosestChildPosition() + val internalMaxFling = resources.getInteger(R.integer.carousel_max_fling_count) + val maxFling = preferences.getInt(CAROUSEL_MAX_FLING_COUNT, internalMaxFling).coerceIn( + 1, + 10 + ) + val rawFlingCount = if (velocityX == 0) 0 else velocityX / 2000 + val flingCount = rawFlingCount.coerceIn(-maxFling, maxFling) + val targetPos = (closestPosition + flingCount).coerceIn(0, layoutManager.itemCount - 1) + return targetPos + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/GradientBorderCardView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/GradientBorderCardView.kt new file mode 100644 index 0000000000..8f730fc490 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/GradientBorderCardView.kt @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.views + +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import com.google.android.material.card.MaterialCardView +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.features.settings.model.Settings +import androidx.preference.PreferenceManager + +class GradientBorderCardView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MaterialCardView(context, attrs, defStyleAttr) { + + private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + strokeWidth = 6f + } + + private val borderPath = Path() + private val borderRect = RectF() + private var showGradientBorder = false + private var isEdenTheme = false + + init { + setWillNotDraw(false) + updateThemeState() + } + + private fun updateThemeState() { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + val themeIndex = try { + prefs.getInt(Settings.PREF_STATIC_THEME_COLOR, 0) + } catch (e: Exception) { + 0 // Default to Eden theme if error + } + isEdenTheme = themeIndex == 0 + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + + // Update border style based on theme + if (isEdenTheme) { + // Gradient for Eden theme + borderPaint.shader = LinearGradient( + 0f, + 0f, + w.toFloat(), + h.toFloat(), + context.getColor(R.color.eden_border_gradient_start), + context.getColor(R.color.eden_border_gradient_end), + Shader.TileMode.CLAMP + ) + } else { + // Solid color for other themes + borderPaint.shader = null + val typedValue = android.util.TypedValue() + context.theme.resolveAttribute( + com.google.android.material.R.attr.colorPrimary, + typedValue, + true + ) + borderPaint.color = typedValue.data + } + + // Update border rect with padding for stroke + val halfStroke = borderPaint.strokeWidth / 2 + borderRect.set( + halfStroke, + halfStroke, + w - halfStroke, + h - halfStroke + ) + + // Update path with rounded corners + borderPath.reset() + borderPath.addRoundRect( + borderRect, + radius, + radius, + Path.Direction.CW + ) + } + + override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) + showGradientBorder = gainFocus + invalidate() + } + + override fun setSelected(selected: Boolean) { + super.setSelected(selected) + showGradientBorder = selected + invalidate() + } + + override fun setPressed(pressed: Boolean) { + super.setPressed(pressed) + if (pressed) { + showGradientBorder = true + invalidate() + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + if (showGradientBorder && !isPressed) { + canvas.drawPath(borderPath, borderPaint) + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + updateThemeState() + requestLayout() + } +} diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index ec8ae5c57d..0557236394 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later + # SPDX-FileCopyrightText: 2023 yuzu Emulator Project # SPDX-License-Identifier: GPL-3.0-or-later @@ -17,10 +20,14 @@ add_library(yuzu-android SHARED set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR}) -target_link_libraries(yuzu-android PRIVATE audio_core common core input_common frontend_common Vulkan::Headers) +target_link_libraries(yuzu-android PRIVATE audio_core common core input_common frontend_common video_core) target_link_libraries(yuzu-android PRIVATE android camera2ndk EGL glad jnigraphics log) if (ARCHITECTURE_arm64) target_link_libraries(yuzu-android PRIVATE adrenotools) endif() +if (ENABLE_OPENSSL OR ENABLE_WEB_SERVICE) + target_link_libraries(yuzu-android PRIVATE OpenSSL::SSL cpp-jwt::cpp-jwt) +endif() + set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} yuzu-android) diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp index a79a64afbb..41ac680d6b 100644 --- a/src/android/app/src/main/jni/android_config.cpp +++ b/src/android/app/src/main/jni/android_config.cpp @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later #include #include @@ -103,6 +103,7 @@ void AndroidConfig::ReadOverlayValues() { ReadDoubleSetting(std::string("foldable\\x_position")); control_data.foldable_position.second = ReadDoubleSetting(std::string("foldable\\y_position")); + control_data.individual_scale = static_cast(ReadDoubleSetting(std::string("individual_scale"))); AndroidSettings::values.overlay_control_data.push_back(control_data); } EndArray(); @@ -255,6 +256,7 @@ void AndroidConfig::SaveOverlayValues() { control_data.foldable_position.first); WriteDoubleSetting(std::string("foldable\\y_position"), control_data.foldable_position.second); + WriteDoubleSetting(std::string("individual_scale"), static_cast(control_data.individual_scale)); } EndArray(); diff --git a/src/android/app/src/main/jni/android_settings.h b/src/android/app/src/main/jni/android_settings.h index ff569d1981..c9e59ce105 100644 --- a/src/android/app/src/main/jni/android_settings.h +++ b/src/android/app/src/main/jni/android_settings.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -6,93 +9,181 @@ #include #include "common/common_types.h" #include "common/settings_setting.h" +#include "common/settings_enums.h" namespace AndroidSettings { -struct GameDir { - std::string path; - bool deep_scan = false; -}; + struct GameDir { + std::string path; + bool deep_scan = false; + }; -struct OverlayControlData { - std::string id; - bool enabled; - std::pair landscape_position; - std::pair portrait_position; - std::pair foldable_position; -}; + struct OverlayControlData { + std::string id; + bool enabled; + std::pair landscape_position; + std::pair portrait_position; + std::pair foldable_position; + float individual_scale; + }; -struct Values { - Settings::Linkage linkage; + struct Values { + Settings::Linkage linkage; - // Path settings - std::vector game_dirs; + // Path settings + std::vector game_dirs; - // Android - Settings::Setting picture_in_picture{linkage, false, "picture_in_picture", - Settings::Category::Android}; - Settings::Setting screen_layout{linkage, - 5, - "screen_layout", - Settings::Category::Android, - Settings::Specialization::Default, - true, - true}; - Settings::Setting vertical_alignment{linkage, - 0, - "vertical_alignment", - Settings::Category::Android, - Settings::Specialization::Default, - true, - true}; + // Android + Settings::Setting picture_in_picture{linkage, false, "picture_in_picture", + Settings::Category::Android}; + Settings::Setting screen_layout{linkage, + 5, + "screen_layout", + Settings::Category::Android, + Settings::Specialization::Default, + true, + true}; + Settings::Setting vertical_alignment{linkage, + 0, + "vertical_alignment", + Settings::Category::Android, + Settings::Specialization::Default, + true, + true}; - Settings::SwitchableSetting driver_path{linkage, "", "driver_path", - Settings::Category::GpuDriver}; + Settings::SwitchableSetting driver_path{linkage, "", "driver_path", + Settings::Category::GpuDriver}; - // LRU Cache - Settings::SwitchableSetting use_lru_cache{linkage, true, "use_lru_cache", - Settings::Category::System}; + // LRU Cache + Settings::SwitchableSetting use_lru_cache{linkage, true, "use_lru_cache", + Settings::Category::System}; - Settings::Setting theme{linkage, 0, "theme", Settings::Category::Android}; - Settings::Setting theme_mode{linkage, -1, "theme_mode", Settings::Category::Android}; - Settings::Setting black_backgrounds{linkage, false, "black_backgrounds", - Settings::Category::Android}; + Settings::Setting theme{linkage, 0, "theme", Settings::Category::Android}; + Settings::Setting theme_mode{linkage, -1, "theme_mode", Settings::Category::Android}; + Settings::Setting black_backgrounds{linkage, false, "black_backgrounds", + Settings::Category::Android}; - // Input/performance overlay settings - std::vector overlay_control_data; - Settings::Setting overlay_scale{linkage, 50, "control_scale", Settings::Category::Overlay}; - Settings::Setting overlay_opacity{linkage, 100, "control_opacity", + // Input/performance overlay settings + std::vector overlay_control_data; + Settings::Setting overlay_scale{linkage, 50, "control_scale", + Settings::Category::Overlay}; + Settings::Setting overlay_opacity{linkage, 100, "control_opacity", + Settings::Category::Overlay}; + + Settings::Setting joystick_rel_center{linkage, true, "joystick_rel_center", + Settings::Category::Overlay}; + Settings::Setting dpad_slide{linkage, true, "dpad_slide", + Settings::Category::Overlay}; + Settings::Setting haptic_feedback{linkage, true, "haptic_feedback", + Settings::Category::Overlay}; + Settings::Setting show_performance_overlay{linkage, true, "show_performance_overlay", + Settings::Category::Overlay, + Settings::Specialization::Paired, true, + true}; + Settings::Setting enable_input_overlay_auto_hide{linkage, false, + "enable_input_overlay_auto_hide", + Settings::Category::Overlay, + Settings::Specialization::Default, true, + true,}; + + Settings::Setting input_overlay_auto_hide{linkage, 5, "input_overlay_auto_hide", + Settings::Category::Overlay, + Settings::Specialization::Default, true, true, &enable_input_overlay_auto_hide}; + Settings::Setting perf_overlay_background{linkage, false, "perf_overlay_background", + Settings::Category::Overlay, + Settings::Specialization::Default, true, + true, + &show_performance_overlay}; + Settings::Setting perf_overlay_position{linkage, 0, "perf_overlay_position", + Settings::Category::Overlay, + Settings::Specialization::Default, true, true, + &show_performance_overlay}; + + Settings::Setting show_fps{linkage, true, "show_fps", + Settings::Category::Overlay, + Settings::Specialization::Default, true, true, + &show_performance_overlay}; + Settings::Setting show_frame_time{linkage, false, "show_frame_time", + Settings::Category::Overlay, + Settings::Specialization::Default, true, true, + &show_performance_overlay}; + Settings::Setting show_app_ram_usage{linkage, false, "show_app_ram_usage", + Settings::Category::Overlay, + Settings::Specialization::Default, true, true, + &show_performance_overlay}; + Settings::Setting show_system_ram_usage{linkage, false, "show_system_ram_usage", + Settings::Category::Overlay, + Settings::Specialization::Default, true, true, + &show_performance_overlay}; + Settings::Setting show_bat_temperature{linkage, false, "show_bat_temperature", + Settings::Category::Overlay, + Settings::Specialization::Default, true, true, + &show_performance_overlay}; + Settings::Setting bat_temperature_unit{linkage, + Settings::TemperatureUnits::Celsius, + "bat_temperature_unit", + Settings::Category::Overlay, + Settings::Specialization::Default, + true, true, + &show_bat_temperature}; + Settings::Setting show_power_info{linkage, false, "show_power_info", + Settings::Category::Overlay, + Settings::Specialization::Default, true, true, + &show_performance_overlay}; + Settings::Setting show_shaders_building{linkage, true, "show_shaders_building", + Settings::Category::Overlay, + Settings::Specialization::Default, true, true, + &show_performance_overlay}; + + + Settings::Setting show_input_overlay{linkage, true, "show_input_overlay", + Settings::Category::Overlay}; + Settings::Setting touchscreen{linkage, true, "touchscreen", + Settings::Category::Overlay}; + Settings::Setting lock_drawer{linkage, false, "lock_drawer", Settings::Category::Overlay}; - Settings::Setting joystick_rel_center{linkage, true, "joystick_rel_center", - Settings::Category::Overlay}; - Settings::Setting dpad_slide{linkage, true, "dpad_slide", Settings::Category::Overlay}; - Settings::Setting haptic_feedback{linkage, true, "haptic_feedback", - Settings::Category::Overlay}; - Settings::Setting show_performance_overlay{linkage, true, "show_performance_overlay", - Settings::Category::Overlay, Settings::Specialization::Paired, true , true}; - Settings::Setting overlay_background{linkage, false, "overlay_background", - Settings::Category::Overlay, Settings::Specialization::Default, true , true, &show_performance_overlay}; - Settings::Setting perf_overlay_position{linkage, 0, "perf_overlay_position", - Settings::Category::Overlay, Settings::Specialization::Default, true , true, &show_performance_overlay}; - Settings::Setting show_fps{linkage, true, "show_fps", - Settings::Category::Overlay, Settings::Specialization::Default, true , true, &show_performance_overlay}; - Settings::Setting show_frame_time{linkage, false, "show_frame_time", - Settings::Category::Overlay, Settings::Specialization::Default, true , true, &show_performance_overlay}; - Settings::Setting show_speed{linkage, true, "show_speed", - Settings::Category::Overlay, Settings::Specialization::Default, true , true, &show_performance_overlay}; - Settings::Setting show_app_ram_usage{linkage, false, "show_app_ram_usage", - Settings::Category::Overlay, Settings::Specialization::Default, true , true, &show_performance_overlay}; - Settings::Setting show_system_ram_usage{linkage, false, "show_system_ram_usage", - Settings::Category::Overlay, Settings::Specialization::Default, true , true, &show_performance_overlay}; - Settings::Setting show_bat_temperature{linkage, false, "show_bat_temperature", - Settings::Category::Overlay, Settings::Specialization::Default, true , true, &show_performance_overlay}; - Settings::Setting show_input_overlay{linkage, true, "show_input_overlay", - Settings::Category::Overlay}; - Settings::Setting touchscreen{linkage, true, "touchscreen", Settings::Category::Overlay}; - Settings::Setting lock_drawer{linkage, false, "lock_drawer", Settings::Category::Overlay}; -}; + /// DEVICE/SOC OVERLAY -extern Values values; + Settings::Setting show_soc_overlay{linkage, true, "show_soc_overlay", + Settings::Category::Overlay, + Settings::Specialization::Paired, true, true}; + + Settings::Setting show_device_model{linkage, true, "show_device_model", + Settings::Category::Overlay, + Settings::Specialization::Default, true, true, + &show_performance_overlay}; + + Settings::Setting show_gpu_model{linkage, true, "show_gpu_model", + Settings::Category::Overlay, + Settings::Specialization::Default, true, true, + &show_performance_overlay}; + + Settings::Setting show_soc_model{linkage, true, "show_soc_model", + Settings::Category::Overlay, + Settings::Specialization::Default, true, true, + &show_soc_overlay}; + + Settings::Setting show_fw_version{linkage, true, "show_firmware_version", + Settings::Category::Overlay, + Settings::Specialization::Default, true, true, + &show_performance_overlay}; + + Settings::Setting soc_overlay_background{linkage, false, "soc_overlay_background", + Settings::Category::Overlay, + Settings::Specialization::Default, true, + true, + &show_soc_overlay}; + Settings::Setting soc_overlay_position{linkage, 2, "soc_overlay_position", + Settings::Category::Overlay, + Settings::Specialization::Default, true, true, + &show_soc_overlay}; + + Settings::Setting dont_show_eden_veil_warning{linkage, false, + "dont_show_eden_veil_warning", + Settings::Category::Miscellaneous}; + }; + + extern Values values; } // namespace AndroidSettings diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 6dac1ef84e..306b7e2a4c 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -1,9 +1,9 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project -// SPDX-License-Identifier: GPL-3.0-or-later - #include #include @@ -32,7 +32,6 @@ #include "common/fs/path_util.h" #include "common/logging/backend.h" #include "common/logging/log.h" -#include "common/microprofile.h" #include "common/scm_rev.h" #include "common/scope_exit.h" #include "common/settings.h" @@ -57,8 +56,10 @@ #include "core/hle/service/am/applet_manager.h" #include "core/hle/service/am/frontend/applets.h" #include "core/hle/service/filesystem/filesystem.h" +#include "core/hle/service/set/system_settings_server.h" #include "core/loader/loader.h" #include "frontend_common/config.h" +#include "frontend_common/firmware_manager.h" #include "hid_core/frontend/emulated_controller.h" #include "hid_core/hid_core.h" #include "hid_core/hid_types.h" @@ -67,12 +68,23 @@ #include "video_core/renderer_vulkan/renderer_vulkan.h" #include "video_core/vulkan_common/vulkan_instance.h" #include "video_core/vulkan_common/vulkan_surface.h" +#include "video_core/shader_notify.h" +#include "network/announce_multiplayer_session.h" #define jconst [[maybe_unused]] const auto #define jauto [[maybe_unused]] auto static EmulationSession s_instance; +//Abdroid Multiplayer which can be initialized with parameters +std::unique_ptr multiplayer{nullptr}; +std::shared_ptr announce_multiplayer_session; + +//Power Status default values +std::atomic g_battery_percentage = {100}; +std::atomic g_is_charging = {false}; +std::atomic g_has_battery = {true}; + EmulationSession::EmulationSession() { m_vfs = std::make_shared(); } @@ -158,6 +170,12 @@ const Core::PerfStatsResults& EmulationSession::PerfStats() { return m_perf_stats; } +int EmulationSession::ShadersBuilding() { + auto& shader_notify = m_system.GPU().ShaderNotify(); + m_shaders_building = shader_notify.ShadersBuilding(); + return m_shaders_building; +} + void EmulationSession::SurfaceChanged() { if (!IsRunning()) { return; @@ -252,6 +270,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string nullptr, // Profile Selector std::move(android_keyboard), // Software Keyboard nullptr, // Web Browser + nullptr, // Net Connect }); // Initialize filesystem. @@ -264,6 +283,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string : Service::AM::LaunchType::ApplicationInitiated, .program_index = static_cast(program_index), }; + m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath, params); if (m_load_result != Core::SystemResultStatus::Success) { return m_load_result; @@ -411,11 +431,6 @@ u64 EmulationSession::GetProgramId(JNIEnv* env, jstring jprogramId) { static Core::SystemResultStatus RunEmulation(const std::string& filepath, const size_t program_index, const bool frontend_initiated) { - MicroProfileOnThreadCreate("EmuThread"); - SCOPE_EXIT { - MicroProfileShutdown(); - }; - LOG_INFO(Frontend, "starting"); if (filepath.empty()) { @@ -560,6 +575,32 @@ jobjectArray Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_getSystemDriverInfo( return j_driver_info; } +jstring Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_getGpuModel(JNIEnv *env, jobject j_obj, jobject j_surf, jstring j_hook_lib_dir) { + const char* file_redirect_dir_{}; + int featureFlags{}; + std::string hook_lib_dir = Common::Android::GetJString(env, j_hook_lib_dir); + auto handle = adrenotools_open_libvulkan(RTLD_NOW, featureFlags, nullptr, hook_lib_dir.c_str(), + nullptr, nullptr, file_redirect_dir_, nullptr); + auto driver_library = std::make_shared(handle); + InputCommon::InputSubsystem input_subsystem; + auto window = + std::make_unique(ANativeWindow_fromSurface(env, j_surf), driver_library); + + Vulkan::vk::InstanceDispatch dld; + Vulkan::vk::Instance vk_instance = Vulkan::CreateInstance( + *driver_library, dld, VK_API_VERSION_1_1, Core::Frontend::WindowSystemType::Android); + + auto surface = Vulkan::CreateSurface(vk_instance, window->GetWindowInfo()); + + auto device = Vulkan::CreateDevice(vk_instance, dld, *surface); + + const std::string model_name{device.GetModelName()}; + + window.release(); + + return Common::Android::ToJString(env, model_name); +} + jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadKeys(JNIEnv* env, jclass clazz) { Core::Crypto::KeyManager::Instance().ReloadKeys(); return static_cast(Core::Crypto::KeyManager::Instance().AreKeysLoaded()); @@ -610,6 +651,16 @@ jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jcl return j_stats; } +jint Java_org_yuzu_yuzu_1emu_NativeLibrary_getShadersBuilding(JNIEnv* env, jclass clazz) { + jint j_shaders = 0; + + if (EmulationSession::GetInstance().IsRunning()) { + j_shaders = EmulationSession::GetInstance().ShadersBuilding(); + } + + return j_shaders; +} + jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass clazz) { if (Settings::IsNceEnabled()) { return Common::Android::ToJString(env, "NCE"); @@ -710,20 +761,46 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_setCabinetMode(JNIEnv* env, jclass cl static_cast(jcabinetMode)); } +bool isFirmwarePresent() { + return FirmwareManager::CheckFirmwarePresence(EmulationSession::GetInstance().System()); +} + jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env, jclass clazz) { - auto bis_system = - EmulationSession::GetInstance().System().GetFileSystemController().GetSystemNANDContents(); - if (!bis_system) { - return false; + return isFirmwarePresent(); +} + +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_firmwareVersion(JNIEnv* env, jclass clazz) { + const auto pair = FirmwareManager::GetFirmwareVersion(EmulationSession::GetInstance().System()); + const auto firmware_data = pair.first; + const auto result = pair.second; + + if (result.IsError() || !isFirmwarePresent()) { + return Common::Android::ToJString(env, "N/A"); } - // Query an applet to see if it's available - auto applet_nca = - bis_system->GetEntry(0x010000000000100Dull, FileSys::ContentRecordType::Program); - if (!applet_nca) { - return false; - } - return true; + const std::string display_version(firmware_data.display_version.data()); + const std::string display_title(firmware_data.display_title.data()); + + LOG_INFO(Frontend, "Installed firmware: {}", display_title); + + return Common::Android::ToJString(env, display_version); +} + +jint Java_org_yuzu_yuzu_1emu_NativeLibrary_verifyFirmware(JNIEnv* env, jclass clazz) { + return static_cast(FirmwareManager::VerifyFirmware(EmulationSession::GetInstance().System())); +} + +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_gameRequiresFirmware(JNIEnv* env, jclass clazz, jstring jprogramId) { + auto program_id = EmulationSession::GetProgramId(env, jprogramId); + + return FirmwareManager::GameRequiresFirmware(program_id); +} + +jint Java_org_yuzu_yuzu_1emu_NativeLibrary_installKeys(JNIEnv* env, jclass clazz, jstring jpath, jstring jext) { + const auto path = Common::Android::GetJString(env, jpath); + const auto ext = Common::Android::GetJString(env, jext); + + return static_cast(FirmwareManager::InstallKeys(path, ext)); } jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj, @@ -875,83 +952,109 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_areKeysPresent(JNIEnv* env, jobje return ContentManager::AreKeysPresent(); } +JNIEXPORT void JNICALL +Java_org_yuzu_yuzu_1emu_NativeLibrary_initMultiplayer( + JNIEnv* env, [[maybe_unused]] jobject obj) { + if (multiplayer) { + return; + } + + announce_multiplayer_session = std::make_shared(); + + multiplayer = std::make_unique(s_instance.System(), announce_multiplayer_session); + multiplayer->NetworkInit(); +} + +JNIEXPORT jobjectArray JNICALL +Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayGetPublicRooms( + JNIEnv *env, [[maybe_unused]] jobject obj) { + return Common::Android::ToJStringArray(env, multiplayer->NetPlayGetPublicRooms()); +} + JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayCreateRoom( JNIEnv* env, [[maybe_unused]] jobject obj, jstring ipaddress, jint port, - jstring username, jstring password, jstring room_name, jint max_players) { + jstring username, jstring preferredGameName, jlong preferredGameId, jstring password, + jstring room_name, jint max_players, jboolean isPublic) { return static_cast( - NetPlayCreateRoom(Common::Android::GetJString(env, ipaddress), port, - Common::Android::GetJString(env, username), Common::Android::GetJString(env, password), - Common::Android::GetJString(env, room_name), max_players)); + multiplayer->NetPlayCreateRoom(Common::Android::GetJString(env, ipaddress), port, + Common::Android::GetJString(env, username), Common::Android::GetJString(env, preferredGameName), + preferredGameId,Common::Android::GetJString(env, password), + Common::Android::GetJString(env, room_name), max_players, isPublic)); } JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayJoinRoom( JNIEnv* env, [[maybe_unused]] jobject obj, jstring ipaddress, jint port, jstring username, jstring password) { return static_cast( - NetPlayJoinRoom(Common::Android::GetJString(env, ipaddress), port, + multiplayer->NetPlayJoinRoom(Common::Android::GetJString(env, ipaddress), port, Common::Android::GetJString(env, username), Common::Android::GetJString(env, password))); } JNIEXPORT jobjectArray JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayRoomInfo( JNIEnv* env, [[maybe_unused]] jobject obj) { - return Common::Android::ToJStringArray(env, NetPlayRoomInfo()); + return Common::Android::ToJStringArray(env, multiplayer->NetPlayRoomInfo()); } JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayIsJoined( [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { - return NetPlayIsJoined(); + return multiplayer->NetPlayIsJoined(); } JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayIsHostedRoom( [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { - return NetPlayIsHostedRoom(); + return multiplayer->NetPlayIsHostedRoom(); } JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlaySendMessage( JNIEnv* env, [[maybe_unused]] jobject obj, jstring msg) { - NetPlaySendMessage(Common::Android::GetJString(env, msg)); + multiplayer->NetPlaySendMessage(Common::Android::GetJString(env, msg)); } JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayKickUser( JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) { - NetPlayKickUser(Common::Android::GetJString(env, username)); + multiplayer->NetPlayKickUser(Common::Android::GetJString(env, username)); } JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayLeaveRoom( [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { - NetPlayLeaveRoom(); + multiplayer->NetPlayLeaveRoom(); } JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayIsModerator( [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { - return NetPlayIsModerator(); + return multiplayer->NetPlayIsModerator(); } JNIEXPORT jobjectArray JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayGetBanList( JNIEnv* env, [[maybe_unused]] jobject obj) { - return Common::Android::ToJStringArray(env, NetPlayGetBanList()); + return Common::Android::ToJStringArray(env, multiplayer->NetPlayGetBanList()); } JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayBanUser( JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) { - NetPlayBanUser(Common::Android::GetJString(env, username)); + multiplayer->NetPlayBanUser(Common::Android::GetJString(env, username)); } JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayUnbanUser( JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) { - NetPlayUnbanUser(Common::Android::GetJString(env, username)); + multiplayer->NetPlayUnbanUser(Common::Android::GetJString(env, username)); } -JNIEXPORT void JNICALL -Java_org_yuzu_yuzu_1emu_NativeLibrary_netPlayInit( - JNIEnv* env, [[maybe_unused]] jobject obj) { - NetworkInit(&EmulationSession::GetInstance().System().GetRoomNetwork()); -} +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_updatePowerState( + JNIEnv* env, + jobject, + jint percentage, + jboolean isCharging, + jboolean hasBattery) { + g_battery_percentage.store(percentage, std::memory_order_relaxed); + g_is_charging.store(isCharging, std::memory_order_relaxed); + g_has_battery.store(hasBattery, std::memory_order_relaxed); +} } // extern "C" diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index 6a4551ada2..dfbc8b2943 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h @@ -44,6 +44,7 @@ public: void ShutdownEmulation(); const Core::PerfStatsResults& PerfStats(); + int ShadersBuilding(); void ConfigureFilesystemProvider(const std::string& filepath); void InitializeSystem(bool reload); void SetAppletId(int applet_id); @@ -72,6 +73,7 @@ private: InputCommon::InputSubsystem m_input_subsystem; Common::DetachedTasks m_detached_tasks; Core::PerfStatsResults m_perf_stats{}; + int m_shaders_building{0}; std::shared_ptr m_vfs; Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; std::atomic m_is_running = false; diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp index 0b26280c6c..e6021ed217 100644 --- a/src/android/app/src/main/jni/native_config.cpp +++ b/src/android/app/src/main/jni/native_config.cpp @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later #include @@ -369,7 +369,9 @@ jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getOverlayControlData(JN env->NewObject(Common::Android::GetOverlayControlDataClass(), Common::Android::GetOverlayControlDataConstructor(), Common::Android::ToJString(env, control_data.id), control_data.enabled, - jlandscapePosition, jportraitPosition, jfoldablePosition); + jlandscapePosition, jportraitPosition, jfoldablePosition, + control_data.individual_scale); + env->SetObjectArrayElement(joverlayControlDataArray, i, jcontrolData); } return joverlayControlDataArray; @@ -418,9 +420,12 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setOverlayControlData( env, env->GetObjectField(jfoldablePosition, Common::Android::GetPairSecondField()))); + float individual_scale = static_cast(env->GetFloatField( + joverlayControlData, Common::Android::GetOverlayControlDataIndividualScaleField())); + AndroidSettings::values.overlay_control_data.push_back(AndroidSettings::OverlayControlData{ Common::Android::GetJString(env, jidString), enabled, landscape_position, - portrait_position, foldable_position}); + portrait_position, foldable_position, individual_scale}); } } diff --git a/src/android/app/src/main/legacy/drawable/ic_icon_bg.png b/src/android/app/src/main/legacy/drawable/ic_icon_bg.png new file mode 100644 index 0000000000..3327014f8f Binary files /dev/null and b/src/android/app/src/main/legacy/drawable/ic_icon_bg.png differ diff --git a/src/android/app/src/main/legacy/drawable/ic_icon_bg_orig.png b/src/android/app/src/main/legacy/drawable/ic_icon_bg_orig.png new file mode 100644 index 0000000000..a9fc55a4f5 Binary files /dev/null and b/src/android/app/src/main/legacy/drawable/ic_icon_bg_orig.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.png b/src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.png index 5f82432397..7ea5c71380 100644 Binary files a/src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.png and b/src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.png b/src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.png index 9c80904a86..d1b8f5d358 100644 Binary files a/src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.png and b/src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/tv_banner.png b/src/android/app/src/main/res/drawable-xhdpi/tv_banner.png index 20c770591d..1b6ee05b59 100644 Binary files a/src/android/app/src/main/res/drawable-xhdpi/tv_banner.png and b/src/android/app/src/main/res/drawable-xhdpi/tv_banner.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.png b/src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.png index 63ef14ce0f..1d01126b30 100644 Binary files a/src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.png and b/src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.png differ diff --git a/src/android/app/src/main/res/drawable/eden_background_gradient.xml b/src/android/app/src/main/res/drawable/eden_background_gradient.xml new file mode 100644 index 0000000000..476e88bb32 --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_background_gradient.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/eden_button_primary_background.xml b/src/android/app/src/main/res/drawable/eden_button_primary_background.xml new file mode 100644 index 0000000000..ec22911d66 --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_button_primary_background.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/eden_card_background.xml b/src/android/app/src/main/res/drawable/eden_card_background.xml new file mode 100644 index 0000000000..a944c19fed --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_card_background.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/src/android/app/src/main/res/drawable/eden_card_elevated_background.xml b/src/android/app/src/main/res/drawable/eden_card_elevated_background.xml new file mode 100644 index 0000000000..04ccf3172c --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_card_elevated_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/eden_card_elevated_selector.xml b/src/android/app/src/main/res/drawable/eden_card_elevated_selector.xml new file mode 100644 index 0000000000..9c7144c100 --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_card_elevated_selector.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/eden_dialog_background.xml b/src/android/app/src/main/res/drawable/eden_dialog_background.xml new file mode 100644 index 0000000000..d06b85d406 --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_dialog_background.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/eden_gradient_border.xml b/src/android/app/src/main/res/drawable/eden_gradient_border.xml new file mode 100644 index 0000000000..d17b57d7ae --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_gradient_border.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/eden_list_item_selector.xml b/src/android/app/src/main/res/drawable/eden_list_item_selector.xml new file mode 100644 index 0000000000..2689c6d41e --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_list_item_selector.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/gradient_overlay_bottom.xml b/src/android/app/src/main/res/drawable/gradient_overlay_bottom.xml new file mode 100644 index 0000000000..f74cfa0d05 --- /dev/null +++ b/src/android/app/src/main/res/drawable/gradient_overlay_bottom.xml @@ -0,0 +1,9 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/ic_dropdown_arrow.xml b/src/android/app/src/main/res/drawable/ic_dropdown_arrow.xml new file mode 100644 index 0000000000..26424d9d4c --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_dropdown_arrow.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_icon_bg.png b/src/android/app/src/main/res/drawable/ic_icon_bg.png new file mode 100644 index 0000000000..7da2c6a1a3 Binary files /dev/null and b/src/android/app/src/main/res/drawable/ic_icon_bg.png differ diff --git a/src/android/app/src/main/res/drawable/ic_icon_bg.xml b/src/android/app/src/main/res/drawable/ic_icon_bg.xml deleted file mode 100644 index df62dde92e..0000000000 --- a/src/android/app/src/main/res/drawable/ic_icon_bg.xml +++ /dev/null @@ -1,751 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/android/app/src/main/res/drawable/ic_icon_bg_orig.png b/src/android/app/src/main/res/drawable/ic_icon_bg_orig.png new file mode 100644 index 0000000000..18325c031a Binary files /dev/null and b/src/android/app/src/main/res/drawable/ic_icon_bg_orig.png differ diff --git a/src/android/app/src/main/res/drawable/ic_ip.xml b/src/android/app/src/main/res/drawable/ic_ip.xml new file mode 100644 index 0000000000..ee02b72e29 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_ip.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_joined.xml b/src/android/app/src/main/res/drawable/ic_joined.xml new file mode 100644 index 0000000000..c8f3dd5225 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_joined.xml @@ -0,0 +1,10 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/ic_multiplayer.xml b/src/android/app/src/main/res/drawable/ic_multiplayer.xml new file mode 100644 index 0000000000..8248dc2c24 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_multiplayer.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_revolt.xml b/src/android/app/src/main/res/drawable/ic_revolt.xml new file mode 100644 index 0000000000..cd0295b3d9 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_revolt.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/ic_x.xml b/src/android/app/src/main/res/drawable/ic_x.xml new file mode 100644 index 0000000000..d93c8d6b63 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_x.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/android/app/src/main/res/drawable/item_release_box.xml b/src/android/app/src/main/res/drawable/item_release_box.xml new file mode 100644 index 0000000000..2f2ada1961 --- /dev/null +++ b/src/android/app/src/main/res/drawable/item_release_box.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/item_release_latest_badge_background.xml b/src/android/app/src/main/res/drawable/item_release_latest_badge_background.xml new file mode 100644 index 0000000000..3fff3b7333 --- /dev/null +++ b/src/android/app/src/main/res/drawable/item_release_latest_badge_background.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/popup_menu_background.xml b/src/android/app/src/main/res/drawable/popup_menu_background.xml new file mode 100644 index 0000000000..f792c97a23 --- /dev/null +++ b/src/android/app/src/main/res/drawable/popup_menu_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/theme_card_background.xml b/src/android/app/src/main/res/drawable/theme_card_background.xml new file mode 100644 index 0000000000..1d369c6519 --- /dev/null +++ b/src/android/app/src/main/res/drawable/theme_card_background.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/theme_dialog_background.xml b/src/android/app/src/main/res/drawable/theme_dialog_background.xml new file mode 100644 index 0000000000..9d456afb60 --- /dev/null +++ b/src/android/app/src/main/res/drawable/theme_dialog_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/theme_list_item_selector.xml b/src/android/app/src/main/res/drawable/theme_list_item_selector.xml new file mode 100644 index 0000000000..ab6fb2f213 --- /dev/null +++ b/src/android/app/src/main/res/drawable/theme_list_item_selector.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/layout-land/card_game_carousel.xml b/src/android/app/src/main/res/layout-land/card_game_carousel.xml new file mode 100644 index 0000000000..4d8e5fd148 --- /dev/null +++ b/src/android/app/src/main/res/layout-land/card_game_carousel.xml @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/src/android/app/src/main/res/layout-land/fragment_games.xml b/src/android/app/src/main/res/layout-land/fragment_games.xml new file mode 100644 index 0000000000..d264f58baf --- /dev/null +++ b/src/android/app/src/main/res/layout-land/fragment_games.xml @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout-w600dp/activity_main.xml b/src/android/app/src/main/res/layout-w600dp/activity_main.xml index d4188e7c9d..b0960223a7 100644 --- a/src/android/app/src/main/res/layout-w600dp/activity_main.xml +++ b/src/android/app/src/main/res/layout-w600dp/activity_main.xml @@ -6,7 +6,8 @@ android:id="@+id/coordinator_main" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/colorSurface"> + android:background="@drawable/eden_background_gradient" + > + > + android:touchscreenBlocksFocus="false" + android:background="@android:color/transparent" + app:elevation="0dp"> + android:orientation="horizontal" + android:padding="24dp"> - - - + + android:orientation="vertical" + android:paddingHorizontal="24dp" + android:paddingVertical="20dp"> - + - + - + + - - - + + android:orientation="vertical" + android:paddingHorizontal="24dp" + android:paddingVertical="20dp"> - + - + - + + - - - + + android:orientation="vertical" + android:paddingHorizontal="24dp" + android:paddingVertical="20dp"> - + - + - + + - - - + + android:orientation="vertical" + android:paddingHorizontal="24dp" + android:paddingVertical="20dp"> - + - + - + + -