From 83fde0738cc819dc20196ba30fa7bc4313389b2b Mon Sep 17 00:00:00 2001 From: crueter Date: Sun, 8 Nov 2026 20:19:37 +0100 Subject: [PATCH] [common] nlohmann -> jacinth, httplib -> hydrocarbon, rework auto-updater UX (#4412) Replaces nlohmann_json with the much faster and much less bloated [jacinth](https://github.com/crueter/jacinth), a JSON library I'm working on that essentially aims to be a lightweight wrapper around `yyjson` with extra C++23 sauce. It's not as fast as yyjson or glaze, but has a very nice API that allows for compile-time struct serialization *and* traditional nlohmann-like OOPery (plus to_json and from_json). Jacinth also exclusively exposes itself as a C++20 module, so consider that migration started httplib has been replaced with the **much** smaller [Hydrocarbon](https://github.com/crueter/hydrocarbon) (which is <1500 LoC compared to 24k). It's a much cleaner API than httplib and doesn't depend on curl or anything else (besides openSSL). Insert joke about me NIHing everything here. Auto updater has been reworked. Windows setup executables will have a system.txt that indicates this is a system install; that will just execute the exe directly (same as .dmg on macOS) Zip files will instead: - Ask the user if they want to keep the old one around in a different directory. - Extracts the zip into a staging directory - Close while detach-launching a thin helper shim executable that: - Removes or renames the old application directory in accordance with the user's choice - Renames the staging directory to the current directory name - Launches and detaches the *new* eden.exe in the background. So to the user it looks like: download, extract, new Eden launches, yay. Alongside all of these changes, I had to make a bunch of CMake changes to bring this project up to be modules-compatible. Consider it a bonus. Assisted-by: Local Ornith-1.5-35B (fixing a few nonsensical build errors) Signed-off-by: crueter - [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions). - [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md) - [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability. ------------------- Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4412 Reviewed-by: lizzie --- .patch/httplib/0001-mingw.patch | 62 --- CMakeLists.txt | 25 +- CMakeModules/CPM.cmake | 4 +- CMakeModules/find/Findhttplib.cmake | 34 -- cpmfile.json | 53 +-- docs/Build.md | 37 +- docs/Caveats.md | 28 +- docs/Deps.md | 219 +++++---- docs/build/Android.md | 5 +- externals/CMakeLists.txt | 16 +- externals/cmake-modules/CompilerChecks.cmake | 57 +++ externals/cmake-modules/FasterLinker.cmake | 20 +- shell.nix | 6 +- src/CMakeLists.txt | 57 ++- src/android/app/build.gradle.kts | 20 +- .../yuzu_emu/utils/NativePostProcessing.kt | 10 +- src/android/app/src/main/jni/CMakeLists.txt | 2 +- src/android/app/src/main/jni/native.cpp | 11 +- .../src/main/jni/native_post_processing.cpp | 75 +-- src/android/build.gradle.kts | 5 +- src/audio_core/sink/sink_details.cpp | 1 + src/common/CMakeLists.txt | 18 +- src/common/android/android_common.h | 3 +- .../android/multiplayer/multiplayer.cpp | 4 +- src/common/announce_multiplayer_room.h | 10 +- src/common/fs/fs_android.cpp | 7 +- src/common/fs/path_util.cpp | 8 +- src/common/fs/path_util.h | 7 + src/common/httplib.h | 18 - src/common/net/net.cpp | 303 +++++-------- src/common/net/net.h | 47 +- src/core/CMakeLists.txt | 11 +- src/core/hle/kernel/kernel.cpp | 48 +- .../hle/service/bcat/news/builtin_news.cpp | 77 ++-- src/core/launch_timestamp_cache.cpp | 48 +- src/core/reporter.cpp | 402 +++++++++++------ src/frontend_common/CMakeLists.txt | 3 - src/frontend_common/update_checker.cpp | 7 +- .../abstracted_pad/abstract_pad_holder.h | 4 +- src/hid_core/resources/npad/npad_types.h | 1 + src/network/room.cpp | 2 +- src/qt_common/CMakeLists.txt | 2 +- src/qt_common/abstract/frontend.h | 14 +- src/qt_common/discord/discord_impl.cpp | 22 +- src/qt_common/qt_common.cpp | 8 + src/qt_common/qt_common.h | 3 + src/tests/CMakeLists.txt | 3 +- src/web_service/CMakeLists.txt | 3 +- src/web_service/announce_room_json.cpp | 108 ++--- src/web_service/verify_login.cpp | 24 +- src/web_service/web_backend.cpp | 74 ++- src/yuzu/CMakeLists.txt | 25 +- src/yuzu/applets/qt_amiibo_settings.cpp | 61 +-- src/yuzu/bootmanager.cpp | 4 + src/yuzu/main.cpp | 17 +- src/yuzu/multiplayer/lobby.cpp | 2 +- src/yuzu/updater/update_dialog.cpp | 426 ++++++++++++++---- src/yuzu/updater/update_dialog.h | 18 +- src/yuzu/updater/updater.cpp | 139 ++++++ src/yuzu_cmd/CMakeLists.txt | 1 + tools/cpm/cmake/ScriptUtils.cmake | 1 + tools/cpm/cmake/package/add.cmake | 4 +- 62 files changed, 1550 insertions(+), 1184 deletions(-) delete mode 100644 .patch/httplib/0001-mingw.patch delete mode 100644 CMakeModules/find/Findhttplib.cmake create mode 100644 externals/cmake-modules/CompilerChecks.cmake delete mode 100644 src/common/httplib.h create mode 100644 src/yuzu/updater/updater.cpp diff --git a/.patch/httplib/0001-mingw.patch b/.patch/httplib/0001-mingw.patch deleted file mode 100644 index da9a9e74bd..0000000000 --- a/.patch/httplib/0001-mingw.patch +++ /dev/null @@ -1,62 +0,0 @@ -From 436fc1978c78edd085d99b33275b24be0ac96aa0 Mon Sep 17 00:00:00 2001 -From: crueter -Date: Sun, 1 Feb 2026 16:21:10 -0500 -Subject: [PATCH] Fix build on MinGW - -MinGW doesn't define GetAddrInfoExCancel. - -Signed-off-by: crueter ---- - httplib.h | 18 ++++++++++++++++-- - 1 file changed, 16 insertions(+), 2 deletions(-) - -diff --git a/httplib.h b/httplib.h -index ec8d2a2..5f9a510 100644 ---- a/httplib.h -+++ b/httplib.h -@@ -203,14 +203,17 @@ - #error Sorry, Visual Studio versions prior to 2015 are not supported - #endif - --#pragma comment(lib, "ws2_32.lib") -- - #ifndef _SSIZE_T_DEFINED - using ssize_t = __int64; - #define _SSIZE_T_DEFINED - #endif - #endif // _MSC_VER - -+#if defined(_MSC_VER) || defined(__MINGW32__) -+#pragma comment(lib, "ws2_32.lib") -+#endif -+ -+ - #ifndef S_ISREG - #define S_ISREG(m) (((m) & S_IFREG) == S_IFREG) - #endif // S_ISREG -@@ -4528,7 +4531,17 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, - auto wait_result = - ::WaitForSingleObject(event, static_cast(timeout_sec * 1000)); - if (wait_result == WAIT_TIMEOUT) { -+#ifdef __MINGW32__ -+ typedef INT(WSAAPI * PFN_GETADDRINFOEXCANCEL)(HANDLE * CancelHandle); -+ auto wsdll = LoadLibraryW((wchar_t *)"ws2_32.lib"); -+ PFN_GETADDRINFOEXCANCEL GetAddrInfoExCancel = -+ (PFN_GETADDRINFOEXCANCEL)GetProcAddress(wsdll, "GetAddrInfoExCancel"); -+ -+ if (cancel_handle) { GetAddrInfoExCancel(&cancel_handle); } -+#else - if (cancel_handle) { ::GetAddrInfoExCancel(&cancel_handle); } -+#endif -+ - ::CloseHandle(event); - return EAI_AGAIN; - } -@@ -13952,3 +13965,4 @@ inline SSL_CTX *Client::ssl_context() const { - } // namespace httplib - - #endif // CPPHTTPLIB_HTTPLIB_H -+ --- -2.51.2 - diff --git a/CMakeLists.txt b/CMakeLists.txt index 61c52c2648..2ccff80c97 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,9 +10,17 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules/find") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/cmake-modules") +# run compiler checks before everything else +include(CompilerChecks) + set(CPM_SOURCE_CACHE ${CMAKE_SOURCE_DIR}/.cache/cpm) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -set(CMAKE_CXX_SCAN_FOR_MODULES 0) +set(CMAKE_CXX_SCAN_FOR_MODULES OFF) + +# force cmdlines to use rsp files to avoid 8191 char limit on winblows +if (CMAKE_GENERATOR MATCHES "Ninja" AND WIN32) + set(CMAKE_NINJA_FORCE_RESPONSE_FILE ON) +endif() include(DetectPlatform) include(DetectArchitecture) @@ -133,8 +141,6 @@ if (YUZU_STATIC_BUILD) set(YUZU_USE_BUNDLED_FFMPEG ON) set(YUZU_USE_BUNDLED_SDL3 ON) set(YUZU_USE_BUNDLED_OPENSSL ON) - - set(HTTPLIB_USE_BROTLI_IF_AVAILABLE OFF) elseif(APPLE) set(YUZU_USE_BUNDLED_FFMPEG ON) set(YUZU_USE_BUNDLED_SDL3 ON) @@ -371,7 +377,7 @@ endif() # Configure C++ standard # =========================== -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Output binaries to bin/ @@ -404,14 +410,13 @@ if (OpenSSL_ADDED) add_compile_definitions(YUZU_BUNDLED_OPENSSL) endif() -message(STATUS "Fetching needed dependencies with CPM") - set(BUILD_SHARED_LIBS OFF) set(BUILD_TESTING OFF) set(ENABLE_TESTING OFF) # boost -set(BOOST_INCLUDE_LIBRARIES algorithm icl pool container heap asio headers process filesystem crc variant) +set(BOOST_INCLUDE_LIBRARIES algorithm icl pool container heap asio headers + process filesystem crc variant pfr) AddJsonPackage(boost) @@ -453,9 +458,6 @@ if (zstd_ADDED) add_library(zstd::libzstd ALIAS libzstd_static) endif() -# nlohmann -AddJsonPackage(nlohmann) - if (NOT YUZU_STATIC_ROOM) # zlib AddJsonPackage(zlib) @@ -524,7 +526,8 @@ elseif (WIN32) set(PLATFORM_LIBRARIES ${PLATFORM_LIBRARIES} winmm iphlpapi ws2_32 wlanapi) if (MINGW) # PSAPI is the Process Status API - set(PLATFORM_LIBRARIES ${PLATFORM_LIBRARIES} psapi imm32 version crypt32 rpcrt4 gdi32 wldap32 mswsock) + set(PLATFORM_LIBRARIES ${PLATFORM_LIBRARIES} psapi imm32 version + crypt32 rpcrt4 gdi32 wldap32 mswsock bcrypt) endif() elseif (MANAGARM) set(PLATFORM_LIBRARIES iconv intl) diff --git a/CMakeModules/CPM.cmake b/CMakeModules/CPM.cmake index 431766b579..b9adc6b799 100644 --- a/CMakeModules/CPM.cmake +++ b/CMakeModules/CPM.cmake @@ -226,9 +226,7 @@ function(CPMAddPackage) list(APPEND fetchContentDeclareExtraArgs EXCLUDE_FROM_ALL) endif() - if(CPM_ARGS_SYSTEM) - list(APPEND fetchContentDeclareExtraArgs SYSTEM) - endif() + list(APPEND fetchContentDeclareExtraArgs SYSTEM) if(DEFINED CPM_ARGS_SOURCE_SUBDIR) list(APPEND fetchContentDeclareExtraArgs diff --git a/CMakeModules/find/Findhttplib.cmake b/CMakeModules/find/Findhttplib.cmake deleted file mode 100644 index e92a9be63f..0000000000 --- a/CMakeModules/find/Findhttplib.cmake +++ /dev/null @@ -1,34 +0,0 @@ -# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project -# SPDX-License-Identifier: GPL-3.0-or-later - -# SPDX-FileCopyrightText: 2022 Andrea Pappacoda -# -# SPDX-License-Identifier: GPL-2.0-or-later - -include(FindPackageHandleStandardArgs) - -find_package(httplib QUIET CONFIG) -if (httplib_CONSIDERED_CONFIGS) - find_package_handle_standard_args(httplib HANDLE_COMPONENTS CONFIG_MODE) -else() - find_package(PkgConfig QUIET) - pkg_search_module(HTTPLIB QUIET IMPORTED_TARGET cpp-httplib) - if ("-DCPPHTTPLIB_OPENSSL_SUPPORT" IN_LIST HTTPLIB_CFLAGS_OTHER) - set(httplib_OpenSSL_FOUND TRUE) - endif() - if ("-DCPPHTTPLIB_ZLIB_SUPPORT" IN_LIST HTTPLIB_CFLAGS_OTHER) - set(httplib_ZLIB_FOUND TRUE) - endif() - if ("-DCPPHTTPLIB_BROTLI_SUPPORT" IN_LIST HTTPLIB_CFLAGS_OTHER) - set(httplib_Brotli_FOUND TRUE) - endif() - find_package_handle_standard_args(httplib - REQUIRED_VARS HTTPLIB_INCLUDEDIR - VERSION_VAR HTTPLIB_VERSION - HANDLE_COMPONENTS - ) -endif() - -if (httplib_FOUND AND NOT TARGET httplib::httplib) - add_library(httplib::httplib ALIAS PkgConfig::HTTPLIB) -endif() diff --git a/cpmfile.json b/cpmfile.json index 4d7cb789f0..22871ccb0c 100644 --- a/cpmfile.json +++ b/cpmfile.json @@ -7,9 +7,9 @@ }, "boost": { "artifact": "boost-%VERSION%.tar.zst", - "find_args": "CONFIG OPTIONAL_COMPONENTS headers context system fiber filesystem", + "find_args": "CONFIG OPTIONAL_COMPONENTS headers context system fiber filesystem process", "hash": "681d97be4386767662e230b2e7c9754d45e709cc5db4e747c23c27d1f5b0e87770bdf19b0bc44dcb0e8349324887bbb8fffb432098f4d0ce74a5043021a90afc", - "min_version": "1.57", + "min_version": "1.87", "package": "Boost", "repo": "eden-emulator/ext-boost", "version": "1.92.0" @@ -34,9 +34,6 @@ "find_args": "CONFIG", "hash": "d11cbd5ddb3197b4c5ca15679bcd76a49963e7b530b7dd132db91e042925efa20dfb2c24ccfbe7ef82a7012af80deff0f72ee25851312ae80381a462df8534b8", "min_version": "1.4", - "options": [ - "CPP_JWT_USE_VENDORED_NLOHMANN_JSON OFF" - ], "patches": [ "0001-fix-missing-decl.patch" ], @@ -100,18 +97,16 @@ "repo": "FeralInteractive/gamemode", "version": "ce6fe122f3" }, - "httplib": { - "find_args": "MODULE GLOBAL", - "hash": "159ed94965018f2a371d45a3bfc1961e5fb1549e501ded70a6b4532d7fe99d0579c18b5195aff6e35f96f399b426cea2650ec9fb75ef80d4c9edeccb51f2e6c9", - "options": [ - "HTTPLIB_REQUIRE_OPENSSL ON", - "HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES ON" - ], - "patches": [ - "0001-mingw.patch" - ], - "repo": "yhirose/cpp-httplib", - "version": "v0.46.0" + "hydrocarbon": { + "hash": "e08520daa8acc6dcb3c181b1311f3386858907965eca1df5ff70b9f2b729bdacec3b54e5c852e89864d3e5dde6f83eb2b14e8f601b11d6b21963386e0a051d93", + "min_version": "0.1.1", + "repo": "crueter/hydrocarbon", + "version": "f4ec2a3754" + }, + "jacinth": { + "hash": "bba5f830724013b334043b0fc783ced7e478c8d2691d2ee6dafc92ce5df4866b2d656a1ee5ffe3e4b75d11e4ae3a4dc86b7578f9a73c438909fd54775540e6af", + "repo": "crueter/jacinth", + "version": "0.1.0" }, "lagoon": { "hash": "b9380f99c6effaeccc6d8f81d4942e852c11ad28613df637e155451556ae5826f93765bee57a5c87a9740d2bd1db463ad0f55a947772fe9d57eeabae3efa373e", @@ -119,12 +114,12 @@ "version": "1.0.0" }, "libadrenotools": { - "hash": "f6526620cb752876edc5ed4c0925d57b873a8218ee09ad10859ee476e9333259784f61c1dcc55a2bcba597352d18aff22cd2e4c1925ec2ae94074e09d7da2265", + "hash": "191b8dad7ec39c4431ee155ca74ffb51d9c9b085e6f44d631bc03a34b09894dff6b7e1dd153b6eaac95647662b851ccc5ebb7ef54e6f57a6ee114a9516cf7282", "patches": [ "0001-use-cpmfile-def-for-linkernsbypass.patch" ], "repo": "eden-emulator/libadrenotools", - "version": "8ba23b42d7" + "version": "6727fe4218" }, "libusb": { "find_args": "MODULE", @@ -149,11 +144,11 @@ "version": "20250828" }, "lz4": { - "hash": "35c21a5d9cfb5bbf314a5321d02b36819491d2ee3cf8007030ca09d13ca4dae672247b7aeab553e973093604fc48221cb03dc92197c6efe8fc3746891363fdab", + "hash": "5638a9ca76f333bea718b457588eadafd8dddbc4c8afcd8e21992805afff7af283283642558e52d1f1d4655dbe26f2aa85fa89dc365041ca621baf79c552f364", "name": "lz4", "repo": "lz4/lz4", "source_subdir": "build/cmake", - "version": "ebb370ca83" + "version": "v1.11.0" }, "moltenvk": { "artifact": "MoltenVK-macOS.tar", @@ -162,13 +157,6 @@ "repo": "V380-Ori/Ryujinx.MoltenVK", "version": "v1.4.1-ryujinx" }, - "nlohmann": { - "hash": "6cc1e86261f8fac21cc17a33da3b6b3c3cd5c116755651642af3c9e99bb3538fd42c1bd50397a77c8fb6821bc62d90e6b91bcdde77a78f58f2416c62fc53b97d", - "min_version": "3.8", - "package": "nlohmann_json", - "repo": "nlohmann/json", - "version": "v3.12.0" - }, "oaknut": { "hash": "9697e80a7d5d9bcb3ce51051a9a24962fb90ca79d215f1f03ae6b58da8ba13a63b5dda1b4dde3d26ac6445029696b8ef2883f4e5a777b342bba01283ed293856", "min_version": "2.0.1", @@ -187,10 +175,11 @@ }, "openssl-ci": { "ci": true, + "hash": "35001a02c98b32a53fafa22ab1fd94554701ad3de0a847a87a04c665f48908adc3ba5acced98e048c27ed8c5a4e2d804e759721de76be5316f3f3f3ea4cebc38", "name": "openssl", "package": "OpenSSL", "repo": "crueter-ci/OpenSSL", - "version": "4.0.1-1788234794-b64f68a94e" + "version": "4.0.1-1789774337-b64f68a94e" }, "openssl-cmake": { "bundled": true, @@ -344,7 +333,8 @@ "min_version": "1.2", "options": [ "ZLIB_BUILD_SHARED OFF", - "ZLIB_INSTALL OFF" + "ZLIB_INSTALL OFF", + "ZLIB_BUILD_TESTING OFF" ], "package": "ZLIB", "repo": "madler/zlib", @@ -355,7 +345,8 @@ "hash": "2b006c3c110c38bd2f5896b42d36a38931c8eae261f30552f9a9fd04458d218d8f1a1425f117fd12af545ed77042e4b2d1a1a9b6286c4d0b9d74ef79f20a8f08", "min_version": "1.5", "options": [ - "ZSTD_BUILD_SHARED OFF" + "ZSTD_BUILD_SHARED OFF", + "ZSTD_BUILD_PROGRAMS OFF" ], "repo": "facebook/zstd", "source_subdir": "build/cmake", diff --git a/docs/Build.md b/docs/Build.md index 84a3784fc0..32b4918b67 100644 --- a/docs/Build.md +++ b/docs/Build.md @@ -65,43 +65,37 @@ Hit "Configure Project", then wait for CMake to finish configuring (may take a w > [!WARNING] >For all systems: > ->- *CMake* **MUST** be in your PATH (and also *ninja*, if you are using it as ``) ->- You *MUST* be in the cloned *Eden* directory +>- `cmake` and `ninja` must be in your PATH +>- You must be in the cloned `eden` directory > >On Windows: > -> - It's recommended to install **[Ninja](https://ninja-build.org/)** -> - You must load **Visual C++ development environment**, this can be done by running our convenience script: -> - `tools/windows/load-msvc-env.ps1` (for PowerShell 5+) -> - `tools/windows/load-msvc-env.sh` (for MSYS2, Git Bash, etc) +> - You must install **[Ninja](https://ninja-build.org/)** +> - You must load the **Visual C++ development environment**. This can be done by searching "x64 dev tools" in the start menu. -Available ``: -- MSYS2: `MSYS Makefiles` -- MSVC: `Ninja` (preferred) or `Visual Studio 17 2022` -- macOS: `Ninja` (preferred) or `Xcode` -- Others: `Ninja` (preferred) or `UNIX Makefiles` - -Available ``: +Available build types: - `Release` (default) - `RelWithDebInfo` (debug symbols--compiled executable will be large) - `Debug` (if you are using a debugger and annoyed with stuff getting optimized out) -Caveat for Debug Builds: - -- If you're building with CCache, you will need to add the environment variable `CL` with the `/FS` flag ([Reference](https://learn.microsoft.com/pt-br/cpp/build/reference/fs-force-synchronous-pdb-writes?view=msvc-170)) - Also see the root CMakeLists.txt for more build options. Usually the default will provide the best experience, however. ```sh -cmake -S . -B build -G "" -DCMAKE_BUILD_TYPE= -DYUZU_TESTS=OFF +cmake -S . -B build -GNinja -DCMAKE_BUILD_TYPE= ``` If you are on Windows and prefer to use Clang: ```sh -cmake -S . -B build -G "" -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl +cmake -S . -B build -GNinja -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl +``` + +If you are on macOS, you **must** use Homebrew's Clang, as Apple Clang is unsupported: + +```sh +cmake -S . -B build -GNinja -DCMAKE_C_COMPILER="$(brew --prefix llvm)/bin/clang" -DCMAKE_CXX_COMPILER="$(brew --prefix llvm)/bin/clang++" ``` @@ -178,8 +172,6 @@ Simply hit Ctrl+B, or the "hammer" icon in the bottom left. To run, hit the "pla ### On Command Line -If you are using the `UNIX Makefiles` or `Visual Studio 17 2022` as ``, you should also add `--parallel` for faster build times. - ```sh cmake --build build ``` @@ -188,8 +180,9 @@ Your compiled executable will be in: - `build/bin/eden.exe` for Windows, - `build/bin/eden.app/Contents/MacOS/eden` for macOS, + - You can also use `open build/bin/eden.app`, or open `build/bin` in Finder - and `build/bin/eden` for others. ## Scripts -Take a look at our [CI scripts](https://github.com/Eden-CI/Workflow). You can use `.ci/common/configure.sh` on any POSIX-compliant shell, but you are heavily encouraged to instead write your own based. It's not really that hard, provided you can read CMake. +Take a look at our [CI scripts](https://github.com/Eden-CI/Workflow). You can use `.ci/common/configure.sh` on any POSIX-compliant shell, but you are heavily encouraged to instead write your own based on that template. It's not really that hard, provided you can read CMake. diff --git a/docs/Caveats.md b/docs/Caveats.md index b86edf4d75..e1e5b39b01 100644 --- a/docs/Caveats.md +++ b/docs/Caveats.md @@ -1,17 +1,19 @@ # Caveats -- [Arch Linux](#arch-linux) -- [Gentoo Linux](#gentoo-linux) -- [macOS](#macos) -- [OpenIndiana](#openindiana) -- [OmniOS](#omnios) -- [HaikuOS](#haikuos) -- [OpenBSD](#openbsd) -- [FreeBSD](#freebsd) -- [NetBSD](#netbsd) -- [MSYS2](#msys2) -- [RedoxOS](#redoxos) +- [Caveats](#caveats) + - [Arch Linux](#arch-linux) + - [Gentoo Linux](#gentoo-linux) + - [macOS](#macos) + - [OpenIndiana](#openindiana) + - [OmniOS](#omnios) + - [HaikuOS](#haikuos) + - [OpenBSD](#openbsd) + - [FreeBSD](#freebsd) + - [NetBSD](#netbsd) + - [DragonFlyBSD](#dragonflybsd) + - [MSYS2](#msys2) + - [RedoxOS](#redoxos) ## Arch Linux @@ -186,12 +188,12 @@ If build hangs, use `hammer2 bulkfree`. ## MSYS2 -Only the `MINGW64` environment is tested (or `CLANGARM64` on ARM); however, all of the others should work (in theory) sans `MINGW32`. +Only the `UCRT64` and `CLANGARM64` environments are tested. You are recommended to use `-DYUZU_USE_BUNDLED_QT=ON` and `-DYUZU_STATIC_BUILD=ON`. When packaging an MSYS2 build that is NOT fully static, you will need to copy all dependent DLLs recursively alongside the `windeployqt6`; for example: ```sh -# MSYS_TOOLCHAIN is typically just mingw64 +# MSYS_TOOLCHAIN is typically just ucrt64 # since Windows is case-insensitive, you can set this to $MSYSTEM # or, if cross-compiling from Linux, set it to usr/x86_64-w64-mingw32 export PATH="/${MSYS_TOOLCHAIN}/bin:$PATH" diff --git a/docs/Deps.md b/docs/Deps.md index 4f293b9642..15539187e4 100644 --- a/docs/Deps.md +++ b/docs/Deps.md @@ -1,44 +1,42 @@ # Dependencies -To build Eden, you MUST have a C++ compiler. +To build Eden, you MUST have a C++ compiler. Clang 19+ and GCC 16+ are supported. -* On Linux, this is usually [GCC](https://gcc.gnu.org/) 11+ or [Clang](https://clang.llvm.org/) v14+ - * GCC 12 also requires Clang 14+ -* On Windows, we support: - * **[MSVC](https://visualstudio.microsoft.com/downloads/)** (default) - * It's STRONGLY RECOMMENDED to use the **Community** option and **Visual Studio 2022** - * You need to install: **[Desktop development with C++](https://learn.microsoft.com/en-us/cpp/build/vscpp-step-0-installation?view=msvc-170)** - * **[clang-cl](https://learn.microsoft.com/en-us/cpp/build/clang-support-msbuild?view=msvc-180)** - * You need to install: **C++ Clang tools for Windows** - * **[MSYS2](https://www.msys2.org)** -* On macOS, this is Apple Clang - * This can be installed with `xcode-select --install` +- On Linux, this is usually GCC or Clang. Install it through your distribution's package manager. +- On Windows, you have two options: + - Install [Visual Studio](https://visualstudio.microsoft.com/downloads) and select `Desktop development with C++` and `C++ Clang tools for Windows`. You will need to set your C/C++ compiler to `clang-cl`. + - The default MSVC frontend might work for 2026 and newer, but it's completely untested and unsupported. + - You must also ensure that your CMake version is >= 4.4.0 in this scenario. + - Install [MSYS2](https://msys2.org) and use Clang or GCC. UCRT64 and CLANGARM64 are the only two supported environments for now. +- On macOS, you must use [Homebrew](https://brew.sh)'s Clang (`brew install llvm`). + - The default Apple Clang (`xcode-select --install`) is currently incompatible. + - This is because Apple Clang does not currently support C++20 modules (specifically, it lacks `clang-scan-deps`). It's currently unknown when/if it will ever support modules, so for now stick to Homebrew. The following additional tools are also required: -* **[CMake](https://www.cmake.org/)** 3.31+ - already included with the Android SDK -* **[Git](https://git-scm.com/)** for version control - * **[Windows installer](https://gitforwindows.org)** -* **[Python3](https://www.python.org/downloads/)** 3.10+ - necessary to download external repositories -* On Windows, you must install the **[Vulkan SDK](https://vulkan.lunarg.com/sdk/home#windows)** as well - * *A convenience script to install the latest SDK is provided in:* - * `tools/windows/install-vulkan-sdk.ps1` (for PowerShell 5+) - * `tools/windows/install-vulkan-sdk.sh` (for Git Bash, etc) +- **[CMake](https://www.cmake.org/)** 3.31+ +- **[Git](https://git-scm.com/)** for version control + - **[Windows installer](https://gitforwindows.org)** +- **[Python3](https://www.python.org/downloads/)** 3.10+ +- On Windows, you must install the **[Vulkan SDK](https://vulkan.lunarg.com/sdk/home#windows)** as well + - *A convenience script to install the latest SDK is provided in:* + - `tools/windows/install-vulkan-sdk.ps1` (for PowerShell 5+) + - `tools/windows/install-vulkan-sdk.sh` (for Git Bash, etc) -If you are on desktop and plan to use the Qt frontend, you *must* install Qt >=6.6, and optionally Qt Creator (the **RECOMMENDED** IDE for building) +If you are on desktop and plan to use the Qt frontend, you *must* install Qt >=6.6, and optionally Qt Creator (the recommended IDE for building) -* On Linux, *BSD and macOS, this can be done by the package manager - * If you wish to use Qt Creator, append `qtcreator` or `qt-creator` to the commands seen below. -* MSVC/clang-cl users on Windows must install through the official [Qt](https://www.qt.io/download-qt-installer-oss) installer -* Linux and macOS users may choose to use the installer as well. -* MSYS2 can also install Qt 6 via the package manager +- On Linux, *BSD and macOS, this can be done by the package manager + - If you wish to use Qt Creator, append `qtcreator` or `qt-creator` to the commands seen below. +- MSVC/clang-cl users on Windows must install through the official [Qt](https://www.qt.io/download-qt-installer-oss) installer +- Linux and macOS users may choose to use the installer as well. +- MSYS2 can also install Qt 6 via the package manager -* For help setting up Qt Creator, run `./install.sh -h qtcreator` +- For help setting up Qt Creator, run `./install.sh -h qtcreator` -* If you're using clang-cl and want to still use MSVC - * Check the option to add "C++ clang compiler for Windows" on Visual Studio installer and uncheck "x64/x86 build tool for MSVC" while selecting "C++ desktop developement tools" and change Visual Studio to 2026, from 2022. - * At qt creator section generator tab change Visual Studio 17 2022 to 2026. - * Finally, to use clang-cl: `cmake -S . -B build -G "Visual Studio 17 2026" -T ClangCL` +- If you're using clang-cl and want to still use MSVC + - Check the option to add "C++ clang compiler for Windows" on Visual Studio installer and uncheck "x64/x86 build tool for MSVC" while selecting "C++ desktop developement tools" and change Visual Studio to 2026, from 2022. + - For Qt Creator, ensure the generator is set to Ninja, and your selected kit uses clang-cl. + - Finally, to use clang-cl: `cmake -S . -B build -G Ninja -T ClangCL` If you are on **Windows** and building with **MSVC** or **clang-cl**, you may go [back home](Build.md) and continue. @@ -46,52 +44,51 @@ If you are on **Windows** and building with **MSVC** or **clang-cl**, you may go The following are handled by Eden's externals: -* [FFmpeg](https://ffmpeg.org/) (should use `-DYUZU_USE_EXTERNAL_FFMPEG=ON`) -* [SDL3](https://www.libsdl.org/download-2.0.php) 3.2.10+ (Use `-DYUZU_USE_BUNDLED_SDL2=ON` to reduce compile time) +- [FFmpeg](https://ffmpeg.org/) +- [SDL3](https://www.libsdl.org/download-2.0.php) 3.2.10+ -All other dependencies will be downloaded and built by [CPM](https://github.com/cpm-cmake/CPM.cmake/) if `YUZU_USE_CPM` is on, but will always use system dependencies if available (UNIX-like only): +All other dependencies will be downloaded and built by CPM, but will always use system dependencies if available (UNIX-like only): -* [Boost](https://www.boost.org/users/download/) 1.57.0+ -* [Catch2](https://github.com/catchorg/Catch2) 3.0.1 if `YUZU_TESTS` or `DYNARMIC_TESTS` are on -* [fmt](https://fmt.dev/) 8.0.1+ -* [lz4](http://www.lz4.org) -* [nlohmann\_json](https://github.com/nlohmann/json) 3.8+ -* [OpenSSL](https://www.openssl.org/source/) 3+ -* [ZLIB](https://www.zlib.net/) 1.2+ -* [zstd](https://facebook.github.io/zstd/) 1.5+ -* [enet](http://enet.bespin.org/) 1.3+ -* [Opus](https://opus-codec.org/) 1.3+ +- [Boost](https://www.boost.org/users/download/) 1.87.0+ +- [Catch2](https://github.com/catchorg/Catch2) 3.0.1 if `YUZU_TESTS` or `DYNARMIC_TESTS` are on +- [fmt](https://fmt.dev/) 8.0.1+ +- [lz4](http://www.lz4.org) +- [OpenSSL](https://www.openssl.org/source/) 3+ +- [ZLIB](https://www.zlib.net/) 1.2+ +- [zstd](https://facebook.github.io/zstd/) 1.5+ +- [enet](http://enet.bespin.org/) 1.3+ +- [Opus](https://opus-codec.org/) 1.3+ +- [Jacinth](https://github.com/crueter/jacinth) 0.1.0+ +- [Hydrocarbon](https://github.com/crueter/hydrocarbon) 0.1.1+ -Vulkan 1.3.274+ is also needed: +Vulkan 1.4.317+ is also needed: -* [VulkanUtilityLibraries](https://github.com/KhronosGroup/Vulkan-Utility-Libraries) -* [VulkanHeaders](https://github.com/KhronosGroup/Vulkan-Headers) -* [SPIRV-Tools](https://github.com/KhronosGroup/SPIRV-Tools) -* [SPIRV-Headers](https://github.com/KhronosGroup/SPIRV-Headers) +- [VulkanUtilityLibraries](https://github.com/KhronosGroup/Vulkan-Utility-Libraries) +- [VulkanHeaders](https://github.com/KhronosGroup/Vulkan-Headers) +- [SPIRV-Tools](https://github.com/KhronosGroup/SPIRV-Tools) +- [SPIRV-Headers](https://github.com/KhronosGroup/SPIRV-Headers) -Certain other dependencies will be fetched by CPM regardless. System packages *can* be used for these libraries, but many are either not packaged by most distributions OR have issues when used by the system: +System packages *can* be used for these libraries, but many are either not packaged by most distributions OR have issues when used by the system: -* [SimpleIni](https://github.com/brofield/simpleini) -* [DiscordRPC](https://github.com/eden-emulator/discord-rpc) -* [cubeb](https://github.com/mozilla/cubeb) -* [libusb](https://github.com/libusb/libusb) -* [VulkanMemoryAllocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator) -* [sirit](https://github.com/eden-emulator/sirit) -* [httplib](https://github.com/yhirose/cpp-httplib) - if `ENABLE_UPDATE_CHECKER` or `ENABLE_WEB_SERVICE` are on - * This package is known to be broken on the AUR. -* [cpp-jwt](https://github.com/arun11299/cpp-jwt) 1.4+ - if `ENABLE_WEB_SERVICE` is on +- [SimpleIni](https://github.com/brofield/simpleini) +- [DiscordRPC](https://github.com/eden-emulator/discord-rpc) +- [cubeb](https://github.com/mozilla/cubeb) +- [libusb](https://github.com/libusb/libusb) +- [VulkanMemoryAllocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator) +- [sirit](https://github.com/eden-emulator/sirit) +- [cpp-jwt](https://github.com/arun11299/cpp-jwt) 1.4+ - if `ENABLE_WEB_SERVICE` is on On amd64: -* [xbyak](https://github.com/herumi/xbyak) - 7.22 or earlier is recommended +- [xbyak](https://github.com/herumi/xbyak) - 7.22 or earlier is recommended On aarch64 OR if `DYNARMIC_TESTS` is on: -* [oaknut](https://github.com/merryhime/oaknut) 2.0.1+ +- [oaknut](https://github.com/merryhime/oaknut) 2.0.1+ On riscv64: -* [biscuit](https://github.com/lioncash/biscuit) 0.9.1+ +- [biscuit](https://github.com/lioncash/biscuit) 0.9.1+ ## Commands @@ -125,25 +122,23 @@ sudo emerge -a \ media-libs/VulkanMemoryAllocator media-libs/libsdl3 media-libs/cubeb \ net-libs/enet \ sys-libs/zlib \ - dev-cpp/nlohmann_json dev-cpp/simpleini dev-cpp/cpp-httplib dev-cpp/cpp-jwt \ + dev-cpp/simpleini dev-cpp/cpp-jwt dev-cpp/jacinth dev-cpp/hydrocarbon \ games-util/gamemode \ net-wireless/wireless-tools \ dev-qt/qtbase:6 dev-libs/quazip \ virtual/pkgconfig ``` -* On `amd64`, also add `dev-libs/xbyak` -* On `riscv64`, also add `dev-libs/biscuit` (currently unavailable) -* On `aarch64`, also add `dev-libs/oaknut` -* If tests are enabled, also add `dev-libs/oaknut` and `dev-cpp/catch` +- On `amd64`, also add `dev-libs/xbyak` +- On `riscv64`, also add `dev-libs/biscuit` (currently unavailable) +- On `aarch64`, also add `dev-libs/oaknut` +- If tests are enabled, also add `dev-libs/oaknut` and `dev-cpp/catch` Required USE flags: -* `dev-qt/qtbase network concurrent dbus gui widgets` -* `dev-libs/quazip qt6` -* `media-libs/libsdl3 haptic joystick sound video` - * Adding `X vulkan udev opengl` is recommended but not required -* `dev-cpp/cpp-httplib ssl` +- `dev-qt/qtbase network concurrent dbus gui widgets` +- `media-libs/libsdl3 haptic joystick sound video` + - Adding `X vulkan udev opengl` is recommended but not required [Caveats](./Caveats.md#gentoo-linux) @@ -153,12 +148,12 @@ Required USE flags: Arch Linux ```sh -sudo pacman -Syu --needed base-devel boost catch2 cmake enet ffmpeg fmt git glslang libzip lz4 ninja nlohmann-json openssl opus qt6-base qt6-multimedia qt6-charts sdl3 zlib zstd zip unzip vulkan-headers vulkan-utility-libraries libusb spirv-tools spirv-headers +sudo pacman -Syu --needed base-devel boost catch2 cmake enet ffmpeg fmt git glslang libzip lz4 ninja openssl opus qt6-base qt6-multimedia qt6-charts sdl3 zlib zstd zip unzip vulkan-headers vulkan-utility-libraries libusb spirv-tools spirv-headers ``` -* Building with QT Web Engine requires `qt6-webengine` as well. -* Proper Wayland support requires `qt6-wayland` -* GCC 11 or later is required. +- Building with QT Web Engine requires `qt6-webengine` as well. +- Proper Wayland support requires `qt6-wayland` +- GCC 16 or later is required. @@ -166,11 +161,11 @@ sudo pacman -Syu --needed base-devel boost catch2 cmake enet ffmpeg fmt git glsl Ubuntu, Debian, Mint Linux ```sh -sudo apt-get install autoconf cmake g++ gcc git glslang-tools libglu1-mesa-dev libhidapi-dev libpulse-dev libtool libudev-dev libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libxcb-xkb1 libxext-dev libxkbcommon-x11-0 mesa-common-dev nasm ninja-build qt6-base-private-dev catch2 libfmt-dev liblz4-dev nlohmann-json3-dev libzstd-dev libssl-dev libavfilter-dev libavcodec-dev libswscale-dev pkg-config zlib1g-dev libva-dev libvdpau-dev qt6-tools-dev qt6-charts-dev libvulkan-dev spirv-tools spirv-headers libusb-1.0-0-dev libxbyak-dev libboost-dev libboost-fiber-dev libboost-context-dev libsdl3-dev libopus-dev libasound2t64 vulkan-utility-libraries-dev +sudo apt-get install autoconf cmake g++ gcc git glslang-tools libglu1-mesa-dev libhidapi-dev libpulse-dev libtool libudev-dev libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libxcb-xkb1 libxext-dev libxkbcommon-x11-0 mesa-common-dev nasm ninja-build qt6-base-private-dev catch2 libfmt-dev liblz4-dev libzstd-dev libssl-dev libavfilter-dev libavcodec-dev libswscale-dev pkg-config zlib1g-dev libva-dev libvdpau-dev qt6-tools-dev qt6-charts-dev libvulkan-dev spirv-tools spirv-headers libusb-1.0-0-dev libxbyak-dev libboost-dev libboost-fiber-dev libboost-context-dev libsdl3-dev libopus-dev libasound2t64 vulkan-utility-libraries-dev ``` -* Ubuntu 26.04, Linux Mint 22.3, or Debian 13 or later is required. -* To enable QT Web Engine, add `-DYUZU_USE_QT_WEB_ENGINE=ON` when running CMake. +- Ubuntu 26.04, Linux Mint 22.3, or Debian 13 or later is required. +- To enable QT Web Engine, add `-DYUZU_USE_QT_WEB_ENGINE=ON` when running CMake. @@ -196,11 +191,10 @@ sudo dnf config-manager --enable crb sudo dnf install qt6-qtbase-private-devel ``` -For systems like OpenEuler or derivates, don't forget to also install: `SDL3-devel pkg-config fmt-dev nlohmann-json-dev`. +For systems like OpenEuler or derivates, don't forget to also install: `SDL3-devel pkg-config fmt-dev`. -* [RPM Fusion](https://rpmfusion.org/Configuration) is required for `ffmpeg-devel` -* Fedora 32 or later is required. -* Fedora 36+ users with GCC 12 need Clang and should configure CMake with: `cmake -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -B build` +- [RPM Fusion](https://rpmfusion.org/Configuration) is required for `ffmpeg-devel` +- Fedora 42 or later is required. @@ -213,7 +207,7 @@ First, enable the community repository; [see here](https://wiki.alpinelinux.org/ # Enable the community repository setup-apkrepos -c # Install -apk add g++ git cmake make mesa-dev qt6-qtbase-dev qt6-qtbase-private-dev libquazip1-qt6 ffmpeg-dev qt6-charts-dev libusb-dev libtool boost-dev sdl3-dev zstd-dev vulkan-utility-libraries spirv-tools-dev openssl-dev nlohmann-json lz4-dev opus-dev jq patch +apk add g++ git cmake make mesa-dev qt6-qtbase-dev qt6-qtbase-private-dev libquazip1-qt6 ffmpeg-dev qt6-charts-dev libusb-dev libtool boost-dev sdl3-dev zstd-dev vulkan-utility-libraries spirv-tools-dev openssl-dev lz4-dev opus-dev jq patch ``` @@ -221,11 +215,9 @@ apk add g++ git cmake make mesa-dev qt6-qtbase-dev qt6-qtbase-private-dev libqua Void Linux ```sh -xbps-install -Su git make cmake clang pkg-config patch SPIRV-Tools-devel SPIRV-Headers lz4 liblz4-devel boost-devel ffmpeg6-devel catch2 Vulkan-Utility-Libraries Vulkan-Headers glslang openssl-devel SDL3-devel quazip-qt6-devel qt6-base-devel qt6-qt5compat-devel qt6-charts-devel fmt-devel json-c++ libenet-devel libusb-devel +xbps-install -Su git make cmake clang pkg-config patch SPIRV-Tools-devel SPIRV-Headers lz4 liblz4-devel boost-devel ffmpeg6-devel catch2 Vulkan-Utility-Libraries Vulkan-Headers glslang openssl-devel SDL3-devel quazip-qt6-devel qt6-base-devel qt6-charts-devel fmt-devel libenet-devel libusb-devel ``` -Yes, `nlohmann-json` is just named `json-c++`. Why? -
@@ -242,7 +234,7 @@ If you're going for a pure build (i.e no downloaded deps), use `-DYUZU_USE_CPM=O Install dependencies from **[Homebrew](https://brew.sh/)** ```sh -brew install autoconf automake boost ffmpeg fmt glslang hidapi libtool libusb lz4 ninja nlohmann-json openssl pkg-config qt@6 sdl3 speexdsp zlib zstd cmake Catch2 molten-vk vulkan-loader spirv-tools +brew install autoconf automake boost ffmpeg fmt glslang hidapi libtool libusb lz4 ninja openssl pkg-config qt@6 sdl3 speexdsp zlib zstd cmake Catch2 molten-vk vulkan-loader spirv-tools ``` If you are compiling on Intel Mac, or are using a Rosetta Homebrew installation, you must replace all references of `/opt/homebrew` with `/usr/local`. @@ -260,11 +252,12 @@ brew install molten-vk FreeBSD As root run: + ```sh -pkg install devel/cmake devel/sdl3 devel/boost-libs devel/catch2 devel/libfmt devel/nlohmann-json devel/ninja devel/nasm devel/autoconf devel/pkgconf devel/qt6-base x11-toolkits/qt6-charts devel/simpleini net/enet multimedia/ffnvcodec-headers multimedia/ffmpeg audio/opus archivers/liblz4 lang/gcc12 graphics/glslang graphics/vulkan-utility-libraries graphics/spirv-tools www/cpp-httplib graphics/vulkan-utility-libraries graphics/vulkan-headers graphics/spirv-headers quazip-qt6 +pkg install devel/cmake devel/sdl3 devel/boost-libs devel/catch2 devel/libfmt devel/ninja devel/nasm devel/autoconf devel/pkgconf devel/qt6-base x11-toolkits/qt6-charts devel/simpleini net/enet multimedia/ffnvcodec-headers multimedia/ffmpeg audio/opus archivers/liblz4 lang/gcc16 graphics/glslang graphics/vulkan-utility-libraries graphics/spirv-tools graphics/vulkan-utility-libraries graphics/vulkan-headers graphics/spirv-headers quazip-qt6 ``` -If using FreeBSD 12 or prior, use `devel/pkg-config` instead. +FreeBSD 15 or later is required. [Caveats](./Caveats.md#freebsd). @@ -275,7 +268,7 @@ If using FreeBSD 12 or prior, use `devel/pkg-config` instead. For NetBSD +10.1: ```sh -pkgin install git cmake boost fmtlib SDL3 catch2 libjwt spirv-headers spirv-tools ffmpeg7 libva nlohmann-json jq libopus qt6-qtbase qt6-qtcharts qt6-qtmultimedia qt6-qttools cpp-httplib lz4 vulkan-headers nasm autoconf enet pkg-config libusb1 libcxx frozen +pkgin install git cmake boost fmtlib SDL3 catch2 libjwt spirv-headers spirv-tools ffmpeg7 libva jq libopus qt6-qtbase qt6-qtcharts qt6-qtmultimedia qt6-qttools lz4 vulkan-headers nasm autoconf enet pkg-config libusb1 libcxx frozen ``` [Caveats](./Caveats.md#netbsd). @@ -286,7 +279,7 @@ pkgin install git cmake boost fmtlib SDL3 catch2 libjwt spirv-headers spirv-tool ```sh pkg_add -u -pkg_add cmake nasm git boost unzip--iconv autoconf-2.72p0 bash ffmpeg glslang gmake qt6 jq fmt nlohmann-json enet boost vulkan-utility-libraries vulkan-headers spirv-headers spirv-tools catch2 sdl3 libusb1-1.0.29 quazip-qt6 +pkg_add cmake nasm git boost unzip--iconv autoconf-2.72p0 bash ffmpeg glslang gmake qt6 jq fmt enet boost vulkan-utility-libraries vulkan-headers spirv-headers spirv-tools catch2 sdl3 libusb1-1.0.29 quazip-qt6 ``` [Caveats](./Caveats.md#openbsd). @@ -296,7 +289,7 @@ pkg_add cmake nasm git boost unzip--iconv autoconf-2.72p0 bash ffmpeg glslang gm DragonFlyBSD ```sh -pkg install gcc14 git cmake unzip nasm autoconf bash pkgconf ffmpeg glslang gmake jq nlohmann-json enet spirv-tools sdl3 vulkan-utility-libraries vulkan-headers catch2 libfmt openssl liblz4 boost-libs cpp-httplib qt6-base qt6-charts quazip-qt6 libva-vdpau-driver libva-utils libva-intel-driver +pkg install gcc16 git cmake unzip nasm autoconf bash pkgconf ffmpeg glslang gmake jq enet spirv-tools sdl3 vulkan-utility-libraries vulkan-headers catch2 libfmt openssl liblz4 boost-libs qt6-base qt6-charts quazip-qt6 libva-vdpau-driver libva-utils libva-intel-driver ``` [Caveats](./Caveats.md#dragonflybsd). @@ -305,8 +298,10 @@ pkg install gcc14 git cmake unzip nasm autoconf bash pkgconf ffmpeg glslang gmak
OpenIndiana +OpenIndiana is currently unsupported due to its GCC version being too old. When GCC 16 gets added to the main repositories: + ```sh -sudo pkg install git cmake qt6 boost glslang libzip library/lz4 libusb-1 nlohmann-json openssl opus sdl3 zlib compress/zstd unzip pkg-config nasm autoconf mesa library/libdrm header-drm developer/fmt +sudo pkg install git cmake qt6 boost glslang libzip library/lz4 libusb-1 openssl opus sdl3 zlib compress/zstd unzip pkg-config nasm autoconf mesa library/libdrm header-drm developer/fmt gcc-16 ``` [Caveats](./Caveats.md#openindiana). @@ -315,8 +310,10 @@ sudo pkg install git cmake qt6 boost glslang libzip library/lz4 libusb-1 nlohman
OmniOS +OmniOS is currently unsupported due to its GCC version being too old. When GCC 16 gets added to the main repositories: + ```sh -sudo pkgin install git cmake autoconf build-essential libusb-1 nasm gcc13 +sudo pkgin install git cmake autoconf build-essential libusb-1 nasm gcc16 ``` [Caveats](./Caveats.md#omnios). @@ -325,29 +322,21 @@ sudo pkgin install git cmake autoconf build-essential libusb-1 nasm gcc13
MSYS2 -* Open the `MSYS2 MinGW 64-bit` shell (`mingw64.exe`) -* Download and install all dependencies: +- Open the `MSYS2 UCRT64` shell on AMD/Intel, or `MSYS2 CLANGARM64` on ARM +- Download and install all dependencies: ```sh -BASE="git make autoconf libtool automake-wrapper jq patch" -MINGW="qt6-base qt6-charts qt6-tools qt6-translations qt6-svg cmake toolchain clang python-pip openssl vulkan-memory-allocator vulkan-devel glslang boost fmt lz4 nlohmann-json zlib zstd enet opus libusb openssl SDL3" -# Either x86_64 or clang-aarch64 (Windows on ARM) -packages="$BASE" -for pkg in $MINGW; do - packages="$packages mingw-w64-x86_64-$pkg" - #packages="$packages mingw-w64-clang-aarch64-$pkg" -done -pacman -Syuu --needed --noconfirm $packages +pacman -Syu +pacman -S --needed git make autoconf libtool automake-wrapper jq patch pactoys +pacboy -S --needed qt6-base qt6-charts qt6-tools qt6-translations qt6-svg cmake toolchain clang python-pip openssl vulkan-memory-allocator vulkan-devel glslang boost fmt lz4 zlib zstd enet opus libusb openssl SDL3 ``` -* Notes: - * Using `qt6-static` is possible as well, provided you build with `-DYUZU_STATIC_BUILD=ON`. - * Other environments are entirely untested, but should theoretically work provided you install all the necessary packages. - * GCC is proven to work better with the MinGW environment. On ARM, only Clang is available through the CLANGARM64 environment, so use that until a GNU ARM environment is available. - * Add `qt-creator` to the `MINGW` variable to install Qt Creator. You can then create a Start Menu shortcut to the MinGW Qt Creator by running `powershell "\$s=(New-Object -COM WScript.Shell).CreateShortcut('C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Qt Creator.lnk');\$s.TargetPath='C:\\msys64\\mingw64\\bin\\qtcreator.exe';\$s.Save()"` in Git Bash or MSYS2. -* Add MinGW binaries to the PATH if they aren't already: - * `echo 'PATH=/mingw64/bin:$PATH' >> ~/.bashrc` - * or `echo 'PATH=/mingw64/bin:$PATH' >> ~/.zshrc` +- Notes: + - You can omit `qt6` entirely and choose to enable `-DYUZU_USE_BUNDLED_QT=ON`. + - Add `qt-creator` to the `MINGW` variable to install Qt Creator. You can then create a Start Menu shortcut to the MinGW Qt Creator by running `powershell "\$s=(New-Object -COM WScript.Shell).CreateShortcut('C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Qt Creator.lnk');\$s.TargetPath='C:\\msys64\\mingw64\\bin\\qtcreator.exe';\$s.Save()"` in Git Bash or MSYS2. +- Add MinGW binaries to the PATH if they aren't already: + - `echo 'PATH=/$MSYSTEM/bin:$PATH' >> ~/.bashrc` + - or `echo 'PATH=/$MSYSTEM/bin:$PATH' >> ~/.zshrc` [Caveats](./Caveats.md#msys2). @@ -356,7 +345,7 @@ pacman -Syuu --needed --noconfirm $packages HaikuOS ```sh -pkgman install git cmake patch libfmt_devel nlohmann_json lz4_devel opus_devel boost1.90_devel vulkan_devel qt6_base_devel qt6_declarative_devel libsdl3_devel ffmpeg7_devel libx11_devel enet_devel catch2_devel quazip1_qt5_devel qt6_5compat_devel glslang qt6_devel qt6_charts_devel cubeb_devel simpleini quazip_qt6_devel +pkgman install git cmake patch libfmt_devel lz4_devel opus_devel boost1.90_devel vulkan_devel qt6_base_devel qt6_declarative_devel libsdl3_devel ffmpeg7_devel libx11_devel enet_devel catch2_devel quazip1_qt5_devel qt6_5compat_devel glslang qt6_devel qt6_charts_devel cubeb_devel simpleini quazip_qt6_devel ``` [Caveats](./Caveats.md#haikuos). diff --git a/docs/build/Android.md b/docs/build/Android.md index add7fb90c7..3646700bb3 100644 --- a/docs/build/Android.md +++ b/docs/build/Android.md @@ -3,12 +3,13 @@ ## Dependencies * [Android Studio](https://developer.android.com/studio) -* [NDK 27+ and CMake 3.22.1](https://developer.android.com/studio/projects/install-ndk#default-version) +* [NDK 30+ and CMake 3.31.6](https://developer.android.com/studio/projects/install-ndk#default-version) + * `$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "cmake;3.31.6" "ndk;30.0.16138531"` * [Git](https://git-scm.com/download) ## WINDOWS ONLY - Additional Dependencies -* **[Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/)** - **Make sure to select "Desktop development with C++" support in the installer. Make sure to update to the latest version if already installed.** +* **[Visual Studio](https://visualstudio.microsoft.com/downloads/)** - **Make sure to select "Desktop development with C++" support in the installer. Make sure to update to the latest version if already installed.** * **[Vulkan SDK](https://vulkan.lunarg.com/sdk/home#windows)** - **Make sure to select Latest SDK.** * A convenience script to install the latest SDK is provided in `.ci\windows\install-vulkan-sdk.ps1`. diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index cb1deceb96..83e7d48553 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -15,6 +15,10 @@ option(DYNARMIC_TESTS "Build tests" ${BUILD_TESTING}) # some of its variables, which is only possible in 3.13+ set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) +# None of our externals have modules (yet) +# Jacinth does, but that is defined in src so it inherits compiler flags +set(CMAKE_CXX_SCAN_FOR_MODULES OFF) + # Disable tests/tools in all externals supporting the standard option name set(BUILD_TESTING OFF) @@ -49,9 +53,9 @@ if (NOT TARGET stb::headers) endif() AddJsonPackage(NAME zbic DOWNLOAD_ONLY) -set(ZBIC_INCLUDE_DIR - "${zbic_SOURCE_DIR}/src" - PARENT_SCOPE) +add_library(zbic INTERFACE) +target_include_directories(zbic INTERFACE ${zbic_SOURCE_DIR}/src) +add_library(zbic::zbic ALIAS zbic) # ItaniumDemangle (Windows only) if (WIN32 AND NOT TARGET LLVM::Demangle) @@ -63,12 +67,6 @@ if (WIN32 AND NOT TARGET LLVM::Demangle) add_library(LLVM::Demangle ALIAS demangle) endif() -# httplib -if (IOS) - set(HTTPLIB_USE_BROTLI_IF_AVAILABLE OFF) -endif() -AddJsonPackage(httplib) - if (YUZU_STATIC_ROOM) return() endif() diff --git a/externals/cmake-modules/CompilerChecks.cmake b/externals/cmake-modules/CompilerChecks.cmake new file mode 100644 index 0000000000..a1ceb37d04 --- /dev/null +++ b/externals/cmake-modules/CompilerChecks.cmake @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: Copyright 2026 crueter +# SPDX-License-Identifier: MIT + +# Vendored from Jacinth: +# https://github.com/crueter/jacinth/blob/master/cmake/CompilerChecks.cmake + +set(ver ${CMAKE_CXX_COMPILER_VERSION}) +set(id ${CMAKE_CXX_COMPILER_ID}) + +# must use a supported generator +if (NOT (CMAKE_GENERATOR MATCHES "^Visual Studio" OR + CMAKE_GENERATOR MATCHES "Ninja")) + message(FATAL_ERROR "You must use the Visual Studio or Ninja generator " + "for C++20 module support.") +endif() + +# AppleClang is entirely unsupported +if (id MATCHES "AppleClang") + message(FATAL_ERROR "AppleClang is unsupported. Use Homebrew's Clang " + "(brew install llvm) instead.") +endif() + +# GCC must be 16+ +if (id MATCHES "GNU" AND ver VERSION_LESS 16) + message(FATAL_ERROR "Unsupported GCC version ${ver}. Only 16 and up " + "are supported.") +endif() + +# Clang must be 19+ +if (id MATCHES "Clang") + if(ver VERSION_LESS 19) + message(FATAL_ERROR "Unsupported Clang version ${ver}. Only 19 and " + "up are supported.") + endif() + + # clang-cl + # https://cmake.org/cmake/help/v4.4/release/4.4.html + if (MSVC) + # doesn't support Visual Studio + if (CMAKE_GENERATOR MATCHES "^Visual Studio") + message(FATAL_ERROR "clang-cl does not support the Visual Studio " + "generator when C++20 modules are enabled. Use Ninja instead.") + endif() + + # requires CMake 4.4 to scan for modules + if (CMAKE_VERSION VERSION_LESS 4.4) + message(FATAL_ERROR "clang-cl requires CMake 4.4 or newer for " + "C++20 module support") + endif() + endif() +endif() + +# MSVC must be 2026+ (19.50+) +if (id MATCHES "MSVC" AND ver VERSION_LESS 19.50) + message(FATAL_ERROR "Unsupported MSVC version ${ver}. Only 2026 and " + "up (19.50+) are supported.") +endif() \ No newline at end of file diff --git a/externals/cmake-modules/FasterLinker.cmake b/externals/cmake-modules/FasterLinker.cmake index 37d8bca33b..af3587edc6 100644 --- a/externals/cmake-modules/FasterLinker.cmake +++ b/externals/cmake-modules/FasterLinker.cmake @@ -9,8 +9,7 @@ #[[ search order: - gold (GCC only) - the best, generally, but not packaged anymore - - mold (GCC only) - generally does well on GCC - - lld - preferred on clang + - lld - only on clang - bfd - the final fallback - If none are found just use the default linker ]] @@ -29,22 +28,11 @@ if (USE_FASTER_LINKER) endmacro() find_linker(bfd) - find_linker(lld) if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - find_program(LINKER_MOLD mold) - if (LINKER_MOLD AND - CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "12.1") - set(LINKER mold) - endif() - find_linker(gold) - - if (LINKER STREQUAL "lld") - message(WARNING - "[FasterLinker] Using lld on GCC may cause issues.\ - Install mold, gold, or disable USE_FASTER_LINKER.") - endif() + else() + find_linker(lld) endif() if (LINKER) @@ -53,4 +41,4 @@ if (USE_FASTER_LINKER) else() message(WARNING "[FasterLinker] No faster linker found--using default") endif() -endif() \ No newline at end of file +endif() diff --git a/shell.nix b/shell.nix index 786b94ccb2..dec27f3534 100755 --- a/shell.nix +++ b/shell.nix @@ -11,10 +11,10 @@ pkgs.mkShellNoCC { # essential programs git cmake clang gnumake patch jq pkg-config # libraries - openssl boost fmt nlohmann_json lz4 zlib zstd - enet libopus vulkan-headers vulkan-utility-libraries + openssl boost fmt lz4 zlib zstd + enet vulkan-headers vulkan-utility-libraries spirv-tools spirv-headers vulkan-loader unzip - glslang python3 httplib cpp-jwt ffmpeg-headless + glslang python3 cpp-jwt ffmpeg-headless libusb1 cubeb # eden qt6.qtbase qt6.qtmultimedia qt6.qtwayland qt6.qttools diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a2abc7d1b3..305c7449d7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,6 +7,9 @@ # Enable modules to include each other's files include_directories(.) +# Stuff in here may or may not use modules. +set(CMAKE_CXX_SCAN_FOR_MODULES ON) + # Dynarmic if ((ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64 OR ARCHITECTURE_riscv64 OR ARCHITECTURE_loongarch64) AND NOT YUZU_STATIC_ROOM) add_subdirectory(dynarmic) @@ -54,7 +57,7 @@ if (NOT BUILD_ID) endif() endif() -add_compile_definitions(BUILD_ID="${BUILD_ID}") +add_compile_definitions(EDEN_BUILD_ID="${BUILD_ID}") # Set compilation flags if (MSVC AND NOT CXX_CLANG) @@ -153,6 +156,8 @@ else() $<$:-pipe> # Disable RTTI (C++ only) $<$:-fno-rtti>) + else() + add_compile_options($<$:/GR->) endif() if (ENABLE_WERROR) @@ -169,9 +174,9 @@ else() $<$:-Wmissing-declarations> $<$:-Wshadow> $<$:-Wunused> - # Some compilers could particularly misbehave :) $<$:-Wno-error-all>) endif() + add_compile_options( $<$:-Wno-attributes> $<$:-Wno-invalid-offsetof> @@ -209,7 +214,10 @@ else() add_compile_options( $<$:-Wno-array-bounds> $<$:-Wno-stringop-overread> - $<$:-Wno-stringop-overflow>) + $<$:-Wno-stringop-overflow> + # Particularly bugged on GCC 16 + $<$:-Wno-maybe-uninitialized> + ) endif() # Set file offset size to 64 bits. @@ -248,6 +256,49 @@ else() endif() endif() +# These externals need their compiler flags to match the rest of the world +# C++20 modules are sad sometimes + +# Jacinth (json) +AddJsonPackage(jacinth) + +# Hydrocarbon (http) +AddJsonPackage(hydrocarbon) + +# Disable rtti, enable fwrapv, link POSIX threads +macro(fix_modules target type) + if (MSVC) + set(flags /GR-) + else() + set(flags -fno-rtti -fwrapv) + endif() + + target_link_libraries(${target} ${type} Threads::Threads) + + # apply to imported modules + if (type STREQUAL INTERFACE) + set_property(TARGET ${target} PROPERTY + IMPORTED_CXX_MODULES_COMPILE_OPTIONS ${flags}) + else() + # apply to compile target + target_compile_options(${target} ${type} ${flags}) + endif() +endmacro() + +# enable POSIX threads +if (jacinth_ADDED) + fix_modules(jacinth PRIVATE) +else() + fix_modules(jacinth::jacinth INTERFACE) +endif() + +# ditto, hydrocarbon +if (hydrocarbon_ADDED) + fix_modules(hydrocarbon PRIVATE) +else() + fix_modules(hydro::carbon INTERFACE) +endif() + add_subdirectory(common) add_subdirectory(network) diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 69dd6a8e4f..984414a004 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -5,7 +5,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later // import android.annotation.SuppressLint -import com.android.build.gradle.api.ApplicationVariant import kotlin.collections.setOf import org.jlleitschuh.gradle.ktlint.reporter.ReporterType import com.github.triplet.gradle.androidpublisher.ReleaseStatus @@ -36,7 +35,7 @@ android { namespace = "org.yuzu.yuzu_emu" compileSdkVersion = "android-36" - ndkVersion = "28.2.13676358" + ndkVersion = "30.0.16248370" val isNightly = providers.gradleProperty("nightly").orNull?.toBooleanStrictOrNull() ?: false @@ -81,7 +80,6 @@ android { "-DENABLE_QT=0", // Don't use QT "-DENABLE_WEB_SERVICE=1", // Enable web service "-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", @@ -153,6 +151,7 @@ android { // builds a release build that doesn't need signing // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build. register("relWithDebInfo") { + manifestPlaceholders += mapOf("appNameSuffix" to " Debug Release") isDefault = true signingConfig = signingConfigs.getByName("default") isDebuggable = true @@ -161,8 +160,6 @@ android { "proguard-rules.pro" ) - manifestPlaceholders += mapOf("appNameSuffix" to " Debug Release") - versionNameSuffix = "-relWithDebInfo" applicationIdSuffix = ".relWithDebInfo" isJniDebuggable = true @@ -171,13 +168,12 @@ android { // Signed by debug key disallowing distribution on Play Store. // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build. debug { + manifestPlaceholders += mapOf("appNameSuffix" to " Debug") signingConfig = signingConfigs.getByName("default") isDebuggable = true isJniDebuggable = true versionNameSuffix = "-debug" applicationIdSuffix = ".debug" - - manifestPlaceholders += mapOf("appNameSuffix" to " Debug") } } @@ -186,11 +182,11 @@ android { flavorDimensions.add("version") productFlavors { create("mainline") { + manifestPlaceholders += mapOf("appNameBase" to "Eden") dimension = "version" isDefault = true minSdk = 33 - manifestPlaceholders += mapOf("appNameBase" to "Eden") resValue("string", "app_name_suffixed", "Eden") ndk { @@ -199,9 +195,9 @@ android { } create("genshinSpoof") { + manifestPlaceholders += mapOf("appNameBase" to "Eden Optimized") dimension = "version" minSdk = 35 - manifestPlaceholders += mapOf("appNameBase" to "Eden Optimized") resValue("string", "app_name_suffixed", "Eden Optimized") applicationId = "com.miHoYo.Yuanshen" @@ -217,9 +213,9 @@ android { } create("legacy") { + manifestPlaceholders += mapOf("appNameBase" to "Eden Legacy") dimension = "version" minSdk = 29 - manifestPlaceholders += mapOf("appNameBase" to "Eden Legacy") resValue("string", "app_name_suffixed", "Eden Legacy") applicationId = "dev.legacy.eden_emulator" @@ -241,8 +237,8 @@ android { } create("chromeOS") { - dimension = "version" manifestPlaceholders += mapOf("appNameBase" to "Eden ChromeOS") + dimension = "version" resValue("string", "app_name_suffixed", "Eden ChromeOS") ndk { @@ -259,7 +255,7 @@ android { externalNativeBuild { cmake { - version = "3.31.5" + version = "3.31.6" path = file("${edenDir}/CMakeLists.txt") } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativePostProcessing.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativePostProcessing.kt index 2c04900064..eb19b60e4e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativePostProcessing.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativePostProcessing.kt @@ -227,13 +227,13 @@ object NativePostProcessing { tooltip = obj.optString("tooltip"), category = obj.optString("category"), kind = obj.optInt("kind", KIND_FLOAT), - uiType = obj.optInt("uiType", UI_HIDDEN), components = obj.optInt("components", 1), - min = obj.optDouble("min", 0.0).toFloat(), - max = obj.optDouble("max", 1.0).toFloat(), - step = obj.optDouble("step", 0.01).toFloat(), + uiType = obj.optInt("ui_type", UI_HIDDEN), + min = obj.optDouble("ui_min", 0.0).toFloat(), + max = obj.optDouble("ui_max", 1.0).toFloat(), + step = obj.optDouble("ui_step", 0.01).toFloat(), items = obj.optJSONArray("items").toStringList(), - defaults = obj.optJSONArray("defaults").toFloatList() + defaults = obj.optJSONArray("default_value").toFloatList() ) ) } diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index 337d9fea1b..539e697e49 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -28,7 +28,7 @@ if (ARCHITECTURE_arm64) target_link_libraries(yuzu-android PRIVATE adrenotools) endif() -target_link_libraries(yuzu-android PRIVATE ${FFmpeg_LIBRARIES} OpenSSL::SSL cpp-jwt::cpp-jwt) +target_link_libraries(yuzu-android PRIVATE ${FFmpeg_LIBRARIES} OpenSSL::SSL cpp-jwt::cpp-jwt jacinth::jacinth) if (ENABLE_UPDATE_CHECKER) target_compile_definitions(yuzu-android PUBLIC ENABLE_UPDATE_CHECKER) endif() diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 51d0085c6f..39a861c0e4 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -1904,14 +1904,14 @@ JNIEXPORT jobject JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_checkForUpdate( std::optional release = UpdateChecker::GetUpdate(); if (!release) return nullptr; - const std::string tag = release->tag; - const std::string title = release->title; + const std::string tag = release->tag_name; + const std::string title = release->name; const std::string body = release->body; const std::string url = release->html_url; // Android *should* only ever define a single asset. // If not, something has gone wrong, but the Kotlin side can handle it. - const auto assets = release->GetPlatformAssets(); + const auto assets = Common::Net::GetPlatformAssets(release.value()); jclass updateResultClass = env->FindClass("org/yuzu/yuzu_emu/NativeLibrary$UpdateResult"); if (!updateResultClass) { @@ -1951,9 +1951,10 @@ JNIEXPORT jobject JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_checkForUpdate( env->CallVoidMethod(updateResult, setUrl, jurl); // TODO(crueter): Handling for multiple assets? + // TODO(crueter): Digest verification // Maybe another data class x( - for (const Common::Net::Asset &a : assets) { - const auto jaurl = env->NewStringUTF(a.path.c_str()); + for (const Common::Net::NamedAsset &a : assets) { + const auto jaurl = env->NewStringUTF(a.asset.browser_download_url.c_str()); env->CallVoidMethod(updateResult, addAsset, jaurl); env->DeleteLocalRef(jaurl); } diff --git a/src/android/app/src/main/jni/native_post_processing.cpp b/src/android/app/src/main/jni/native_post_processing.cpp index 7fb04e7f9f..43e6246c9c 100644 --- a/src/android/app/src/main/jni/native_post_processing.cpp +++ b/src/android/app/src/main/jni/native_post_processing.cpp @@ -1,12 +1,13 @@ // SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +#define HAS_RESHADE 1 + #include #include #include #include -#include #include "common/android/android_common.h" #ifdef HAS_RESHADE @@ -16,6 +17,8 @@ #include "video_core/post_processing/fx_effect.h" #include "video_core/post_processing/fx_preset.h" +import jacinth; + extern std::unique_ptr per_game_config; #endif @@ -33,29 +36,6 @@ void BeginFxEdit() { VideoCore::UsePerGameFxSettings(); } -nlohmann::json SerializeUniform(const VideoCore::FxUniformDesc& uniform) { - nlohmann::json out; - out["name"] = uniform.name; - out["label"] = uniform.label; - out["tooltip"] = uniform.tooltip; - out["category"] = uniform.category; - out["kind"] = static_cast(uniform.kind); - out["uiType"] = static_cast(uniform.ui_type); - out["components"] = uniform.components; - out["min"] = uniform.ui_min; - out["max"] = uniform.ui_max; - out["step"] = uniform.ui_step; - out["items"] = uniform.items; - - nlohmann::json defaults = nlohmann::json::array(); - for (u32 i = 0; i < uniform.components; ++i) { - defaults.push_back(uniform.default_value[i]); - } - out["defaults"] = defaults; - - return out; -} - std::array DefaultValueOf(size_t index, const std::string& uniform) { const auto entries = VideoCore::FxChain::Instance().Entries(); if (index >= entries.size()) { @@ -79,50 +59,21 @@ extern "C" { jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getCatalogJson(JNIEnv* env, jobject obj) { - nlohmann::json out = nlohmann::json::array(); + jacinth::json out; #ifdef HAS_RESHADE VideoCore::FxChain::Instance().DropUnknownEntries(); - - for (const auto& effect : VideoCore::GetFxCatalog()) { - nlohmann::json entry; - entry["file"] = effect.file; - entry["name"] = effect.name; - entry["label"] = effect.label; - entry["description"] = effect.description; - entry["error"] = effect.error; - entry["techniques"] = effect.techniques; - - nlohmann::json uniforms = nlohmann::json::array(); - for (const auto& uniform : effect.uniforms) { - uniforms.push_back(SerializeUniform(uniform)); - } - entry["uniforms"] = uniforms; - - out.push_back(entry); - } + out = VideoCore::GetFxCatalog(); #endif return Common::Android::ToJString(env, out.dump()); } jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getChainJson(JNIEnv* env, jobject obj) { - nlohmann::json out = nlohmann::json::array(); + jacinth::json out; #ifdef HAS_RESHADE - for (const auto& entry : VideoCore::FxChain::Instance().Entries()) { - nlohmann::json item; - item["file"] = entry.file; - item["technique"] = entry.technique; - - nlohmann::json values = nlohmann::json::object(); - for (const auto& [name, value] : entry.values) { - values[name] = {value[0], value[1], value[2], value[3]}; - } - item["values"] = values; - - out.push_back(item); - } + out = VideoCore::FxChain::Instance().Entries(); #endif return Common::Android::ToJString(env, out.dump()); @@ -247,17 +198,11 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getShaderDirectory(JN jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getPresetsJson(JNIEnv* env, jobject obj) { - nlohmann::json out = nlohmann::json::array(); + jacinth::json out; #ifdef HAS_RESHADE VideoCore::ReloadFxPresetCatalog(); - for (const auto& preset : VideoCore::GetFxPresetCatalog()) { - nlohmann::json entry; - entry["name"] = preset.name; - entry["description"] = preset.description; - entry["bundled"] = preset.bundled; - out.push_back(entry); - } + out = VideoCore::GetFxPresetCatalog(); #endif return Common::Android::ToJString(env, out.dump()); } diff --git a/src/android/build.gradle.kts b/src/android/build.gradle.kts index 051695d2d1..23213eb590 100644 --- a/src/android/build.gradle.kts +++ b/src/android/build.gradle.kts @@ -1,9 +1,12 @@ +// SPDX-FileCopyrightText: Copyright 2026 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 // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.9.1" apply false + id("com.android.application") version "8.13.2" apply false id("com.android.library") version "8.1.4" apply false id("org.jetbrains.kotlin.android") version "1.9.20" apply false } diff --git a/src/audio_core/sink/sink_details.cpp b/src/audio_core/sink/sink_details.cpp index efd8fceb32..184d4e5ff6 100644 --- a/src/audio_core/sink/sink_details.cpp +++ b/src/audio_core/sink/sink_details.cpp @@ -9,6 +9,7 @@ #include #include +#include "audio_core/sink/sink.h" #include "audio_core/sink/sink_details.h" #ifdef HAVE_CUBEB #include "audio_core/sink/cubeb_sink.h" diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 5d1b30a549..487b0ce4cf 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -137,21 +137,21 @@ add_library( uuid.cpp uuid.h vector_math.h - zbic_compression.cpp - zbic_compression.h zstd_compression.cpp zstd_compression.h fs/ryujinx_compat.h fs/ryujinx_compat.cpp fs/symlink.h fs/symlink.cpp cpu_features.cpp cpu_features.h - httplib.h net/net.h net/net.cpp container/unordered_map.h container/unordered_set.h) -set_source_files_properties(zbic_compression.cpp PROPERTIES - INCLUDE_DIRECTORIES "${ZBIC_INCLUDE_DIR}" - COMPILE_OPTIONS "$<$:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>") +add_library(zbic_compression STATIC zbic_compression.cpp zbic_compression.h) +target_link_libraries(zbic_compression PRIVATE zbic::zbic) +target_compile_options(zbic_compression PRIVATE + "$<$:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>") + +target_link_libraries(common PUBLIC zbic_compression) if(WIN32) target_sources(common PRIVATE windows/timer_resolution.cpp @@ -234,12 +234,12 @@ if(CXX_CLANG) endif() if (BOOST_NO_HEADERS) - target_link_libraries(common PUBLIC Boost::algorithm Boost::heap Boost::icl Boost::pool) + target_link_libraries(common PUBLIC Boost::algorithm Boost::heap Boost::icl Boost::pool Boost::asio) else() target_link_libraries(common PUBLIC Boost::headers) endif() -target_link_libraries(common PRIVATE OpenSSL::SSL) -target_link_libraries(common PUBLIC Boost::filesystem Boost::context httplib::httplib nlohmann_json::nlohmann_json) +target_link_libraries(common PRIVATE OpenSSL::SSL hydro::carbon jacinth::jacinth) +target_link_libraries(common PUBLIC Boost::filesystem Boost::context) if (lz4_ADDED) target_include_directories(common PRIVATE ${lz4_SOURCE_DIR}/lib) diff --git a/src/common/android/android_common.h b/src/common/android/android_common.h index a16add3bdf..affe6b7b96 100644 --- a/src/common/android/android_common.h +++ b/src/common/android/android_common.h @@ -1,12 +1,13 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -// SPDX-FileCopyrightText: 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include +#include #include #include "common/common_types.h" diff --git a/src/common/android/multiplayer/multiplayer.cpp b/src/common/android/multiplayer/multiplayer.cpp index ff57eedec3..c8c06268ca 100644 --- a/src/common/android/multiplayer/multiplayer.cpp +++ b/src/common/android/multiplayer/multiplayer.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later #include "common/android/id_cache.h" @@ -387,7 +387,7 @@ std::vector AndroidMultiplayer::NetPlayGetPublicRooms() { auto rooms = session->GetRoomList(); for (const auto &room: rooms) { room_list.push_back(room.information.name + "|" + - (room.has_password ? "1" : "0") + "|" + + (room.hasPassword ? "1" : "0") + "|" + std::to_string(room.information.member_slots) + "|" + room.ip + "|" + std::to_string(room.information.port) + "|" + diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h index bca0ee8e2e..fcb137d639 100644 --- a/src/common/announce_multiplayer_room.h +++ b/src/common/announce_multiplayer_room.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project @@ -6,8 +6,6 @@ #pragma once -#include -#include #include #include #include "common/common_types.h" @@ -26,7 +24,7 @@ struct Member { std::string username; std::string nickname; std::string display_name; - std::string avatar_url; + std::string avatarUrl; Network::IPv4Address fake_ip; GameInfo game; }; @@ -46,8 +44,8 @@ struct Room { std::string id; std::string verify_uid; ///< UID used for verification std::string ip; - u32 net_version{}; - bool has_password = false; + u32 netVersion{}; + bool hasPassword = false; std::vector members; }; diff --git a/src/common/fs/fs_android.cpp b/src/common/fs/fs_android.cpp index 9a80532227..b96656da37 100644 --- a/src/common/fs/fs_android.cpp +++ b/src/common/fs/fs_android.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 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 @@ -95,7 +98,7 @@ bool Exists(const std::string& filepath) { std::string GetParentDirectory(const std::string& filepath) { if (s_get_parent_directory == nullptr) { - return 0; + return ""; } auto env = Common::Android::GetEnvForThread(); jstring j_return = static_cast(env->CallStaticObjectMethod( @@ -108,7 +111,7 @@ std::string GetParentDirectory(const std::string& filepath) { std::string GetFilename(const std::string& filepath) { if (s_get_filename == nullptr) { - return 0; + return ""; } auto env = Common::Android::GetEnvForThread(); jstring j_return = static_cast(env->CallStaticObjectMethod( diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index 82ea8f6cb2..f040e84871 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -315,7 +315,7 @@ void CreateEdenPaths() { #ifdef _WIN32 -fs::path GetExeDirectory() { +fs::path GetExePath() { wchar_t exe_path[MAX_PATH]; if (GetModuleFileNameW(nullptr, exe_path, MAX_PATH) == 0) { @@ -323,7 +323,11 @@ fs::path GetExeDirectory() { "Failed to get the path to the executable of the current process"); } - return fs::path{exe_path}.parent_path(); + return fs::path{exe_path}; +} + +fs::path GetExeDirectory() { + return GetExePath().parent_path(); } fs::path GetAppDataRoamingDirectory() { diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index e8fa5614ae..d09bc1ff6f 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -284,6 +284,13 @@ void SetEdenPath(EdenPath eden_path, const Path& new_path) { #ifdef _WIN32 +/** + * Gets the path to the executable of the current process. + * + * @returns The path to the executable of the current process. + */ +[[nodiscard]] std::filesystem::path GetExePath(); + /** * Gets the path of the directory containing the executable of the current process. * diff --git a/src/common/httplib.h b/src/common/httplib.h deleted file mode 100644 index 57bc4eeb93..0000000000 --- a/src/common/httplib.h +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#define CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES 1 -#define CPPHTTPLIB_OPENSSL_SUPPORT 1 - -#ifdef __GNUC__ -#pragma GCC diagnostic push -#ifndef __clang__ -#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" -#endif -#endif -#include -#ifdef __GNUC__ -#pragma GCC diagnostic pop -#endif diff --git a/src/common/net/net.cpp b/src/common/net/net.cpp index 5c45e13701..f3179bc4f3 100644 --- a/src/common/net/net.cpp +++ b/src/common/net/net.cpp @@ -2,27 +2,35 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include +#include + #include #include #include -#include #include "common/scm_rev.h" #include "net.h" #include "common/logging.h" -#include "common/httplib.h" - #ifdef YUZU_BUNDLED_OPENSSL #include #endif -#define QT_TR_NOOP(x) x +#ifdef _WIN32 +#include +#include "common/fs/path_util.h" +#endif + +import jacinth; +import hydrocarbon; namespace Common::Net { -std::vector Release::GetPlatformAssets() const { +// static constexpr auto DefaultTimeout = std::chrono::seconds{30}; +static constexpr auto DefaultConnectTimeout = std::chrono::milliseconds{10000}; + +std::vector GetPlatformAssets(const Release& r) { // TODO(crueter): Need better handling for this as a whole. #ifdef NIGHTLY_BUILD std::vector result; @@ -31,46 +39,48 @@ std::vector Release::GetPlatformAssets() const { return {}; const auto ref = result.at(1); #else - const auto ref = tag; + const auto ref = r.tag_name; #endif - std::vector found_assets; + std::vector found_assets; - // FIXME: This is mildly inefficient. - // Finds assets based on a hierarchy of regex search strings. - const auto find_asset = [&found_assets, ref, this](const std::string& name, - const std::vector& suffixes) { - for (const std::string& asset : assets) { - for (const auto& suffix : suffixes) { - if (asset.ends_with(suffix)) { - const std::string_view asset_sv = asset; - const size_t pos = asset_sv.find_last_of('/'); - const std::string_view filename = - (pos != std::string_view::npos) ? asset_sv.substr(pos + 1) : asset_sv; - - found_assets.emplace_back(Asset{ - .name = name, - .url = host, - .path = asset, - .filename = std::string{filename}, + [[maybe_unused]] const auto find_asset = + [&r, &found_assets](const std::string& name, const std::vector& suffixes) { + const auto& assets = r.assets; + for (const auto& s : suffixes) { + const auto it = std::ranges::find_if( + assets, [&s](const Asset& a) -> bool { + return a.name.ends_with(s); }); - return; + + if (it != assets.end()) { + auto asset = Asset(*it); + found_assets.emplace_back(NamedAsset{.name = name, .asset = asset}); } } - } - }; + }; #ifdef _WIN32 + // system.txt is (will be) installed in the same directory as the installed executable + const std::filesystem::path cwd = Common::FS::GetExeDirectory(); + const auto is_system = std::filesystem::exists(cwd / "system.txt"); + std::string ext; + if (is_system) { + ext = "exe"; + } else { + ext = "zip"; + } + #ifdef ARCHITECTURE_x86_64 #ifdef _MSC_VER - find_asset("Standard", {"amd64-msvc-standard.exe", "amd64-msvc-standard.zip"}); -#else // _MSC_VER - find_asset("Standard", {BUILD_ID "-gcc-standard.exe", BUILD_ID "-gcc-standard.zip"}); - find_asset("PGO", {BUILD_ID "-clang-pgo.exe", BUILD_ID "-clang-pgo.zip"}); + find_asset("Standard", {std::format("amd64-msvc-standard.{}", ext)}); +#else // _MSC_VER + find_asset("Standard", {std::format(EDEN_BUILD_ID "-gcc-standard.{}", ext)}); + find_asset("PGO", {std::format(EDEN_BUILD_ID "-clang-pgo.{}", ext)}); #endif // _MSC_VER #elif defined(ARCHITECTURE_arm64) - find_asset("Standard", {"arm64-clang-standard.exe", "arm64-clang-standard.zip"}); - find_asset("PGO", {"arm64-clang-pgo.exe", "arm64-clang-pgo.zip"}); + find_asset("Standard", {std::format("arm64-clang-standard.{}", ext)}); + find_asset("PGO", {std::format("arm64-clang-pgo.{}", ext)}); #endif // ARCHITECTURE_arm64 #elif defined(__APPLE__) #ifdef ARCHITECTURE_arm64 @@ -93,164 +103,29 @@ std::vector Release::GetPlatformAssets() const { return found_assets; } -static inline u64 ParseIsoTimestamp(const std::string& iso) { - if (iso.empty()) - return 0; - - std::string buf = iso; - if (buf.back() == 'Z') - buf.pop_back(); - - std::tm tm{}; - std::istringstream ss(buf); - ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); - if (ss.fail()) - return 0; - -#ifdef _WIN32 - return static_cast(_mkgmtime(&tm)); -#else - return static_cast(timegm(&tm)); -#endif -} - -std::optional Release::FromJson(const nlohmann::json& json, const std::string& host, - const std::string& repo) { - Release rel; - if (!json.is_object()) - return std::nullopt; - - rel.tag = json.value("tag_name", std::string{}); - if (rel.tag.empty()) - return std::nullopt; - - rel.title = json.value("name", rel.tag); - rel.id = json.value("id", std::hash{}(rel.title)); - - rel.published = ParseIsoTimestamp(json.value("published_at", std::string{})); - rel.prerelease = json.value("prerelease", false); - - auto body = json.value("body", rel.title); - boost::replace_all(body, "\\r", ""); - boost::replace_all(body, "\\n", "\n"); - rel.body = body; - - rel.host = host; - - const auto release_base = - fmt::format("{}/{}/releases", Common::g_build_auto_update_website, repo); - const auto fallback_html = fmt::format("{}/tag/{}", release_base, rel.tag); - rel.html_url = json.value("html_url", fallback_html); - - // This is our own "fake" API. - if (json.contains("base")) { - const auto base = json.value("base", fmt::format("https://{}", Common::g_build_auto_update_api)); - rel.base_download_url = fmt::format("{}/{}", base, rel.tag); - - // Assets are easy :) - rel.assets = json.value("assets", std::vector{}); - } else { - const auto base_download_url = fmt::format("/{}/releases/download/{}", repo, rel.tag); - - rel.base_download_url = base_download_url; - - // assets are a bit more complex here. :( - std::vector assets; - const nlohmann::json& arr = json["assets"]; - for (const auto &obj : arr) { - const auto url = obj.value("browser_download_url", std::string{}); - assets.emplace_back(url); - } - - rel.assets = assets; - } - - return rel; -} - -std::optional Release::FromJson(const std::string_view& json, const std::string& host, - const std::string& repo) { - try { - return FromJson(nlohmann::json::parse(json), host, repo); - } catch (std::exception& e) { - LOG_WARNING(Common, "Failed to parse JSON: {}", e.what()); - } - - return {}; -} - -std::vector Release::ListFromJson(const nlohmann::json& json, const std::string& host, - const std::string& repo) { - if (!json.is_array()) - return {}; - - std::vector releases; - for (const auto& obj : json) { - auto rel = Release::FromJson(obj, host, repo); - if (rel) - releases.emplace_back(rel.value()); - } - return releases; -} - -std::vector Release::ListFromJson(const std::string_view& json, const std::string& host, - const std::string& repo) { - try { - return ListFromJson(nlohmann::json::parse(json), host, repo); - } catch (std::exception& e) { - LOG_WARNING(Common, "Failed to parse JSON: {}", e.what()); - } - - return {}; -} - -std::optional MakeRequest(const std::string& url, const std::string& path) { - try { - constexpr std::size_t timeout_seconds = 15; - - std::unique_ptr client = std::make_unique(url); - client->set_connection_timeout(timeout_seconds); - client->set_read_timeout(timeout_seconds); - client->set_write_timeout(timeout_seconds); - +std::optional MakeRequest(const std::string& url) { + hydro::RequestOptions opts; #ifdef YUZU_BUNDLED_OPENSSL - client->load_ca_cert_store(kCert, sizeof(kCert)); + opts.connect.cert = std::string{kCert}; #endif - if (client == nullptr) { - LOG_ERROR(Common, "Invalid URL {}{}", url, path); - return {}; - } + opts.connect.connectionTimeout = DefaultConnectTimeout; - httplib::Request request{ - .method = "GET", - .path = path, - }; + auto res = hydro::get(url, opts); - client->set_follow_location(true); - httplib::Result result = client->send(request); - - if (!result) { - LOG_ERROR(Common, "GET to {}{} returned null", url, path); - return {}; - } - - const auto& response = result.value(); - if (response.status >= 400) { - LOG_ERROR(Common, "GET to {}{} returned error status code: {}", url, path, - response.status); - return {}; - } - if (!response.has_header("content-type")) { - LOG_ERROR(Common, "GET to {}{} returned no content", url, path); - return {}; - } - - return response.body; - } catch (std::exception& e) { - LOG_ERROR(Common, "GET to {}{} failed during update check: {}", url, path, e.what()); + if (!res) { + LOG_ERROR(Frontend, "Failed to download {}: {}", url, res.error()); + return std::nullopt; + } else if (res->status_code < hydro::Status::Ok || res->status_code >= hydro::Status::MultipleChoices) { + LOG_ERROR(Frontend, "Received status code {} for {}", std::to_underlying(res->status_code), url); return std::nullopt; } + + return res->body; +} + +std::optional MakeRequest(const std::string_view url) { + return MakeRequest(std::string(url)); } std::vector GetReleases() { @@ -261,32 +136,70 @@ std::vector GetReleases() { return {}; } - const std::string_view body_str = body.value(); - const auto url = fmt::format("https://{}", Common::g_build_auto_update_stable_api); - return Release::ListFromJson(body_str, url, Common::g_build_auto_update_stable_repo); + return GetReleasesFromJson(body.value()); } std::optional GetLatestRelease() { - const auto releases_path = Common::g_build_auto_update_api_path; - const auto url = fmt::format("https://{}", Common::g_build_auto_update_api); + const auto releases_path = Common::g_build_auto_update_api_path; + const auto url = std::format("https://{}{}", std::string{Common::g_build_auto_update_api}, releases_path); - const auto body = MakeRequest(url, releases_path); + const auto body = MakeRequest(url); if (!body) { LOG_WARNING(Common, "Failed to get latest release"); return std::nullopt; } const std::string_view body_str = body.value(); - return Release::FromJson(body_str, url, Common::g_build_auto_update_repo); + auto ec = jacinth::doc::try_read(body_str); + + if (!ec) { + LOG_CRITICAL(Common, "Failed to parse latest release: {}", ec.error().message()); + return std::nullopt; + } + + return ec.value(); } std::optional GetReleasesBody() { const auto releases_path = - fmt::format("/{}/{}/releases", Common::g_build_auto_update_stable_api_path, - Common::g_build_auto_update_stable_repo); - const auto url = fmt::format("https://{}", Common::g_build_auto_update_stable_api); + std::format("/{}/{}/releases", std::string{Common::g_build_auto_update_stable_api_path}, + std::string{Common::g_build_auto_update_stable_repo}); + const auto url = std::format("https://{}{}", std::string{Common::g_build_auto_update_stable_api}, releases_path); - return MakeRequest(url, releases_path); + return MakeRequest(url); +} + +std::vector GetReleasesFromJson(const std::string& body) { + auto ec = jacinth::doc::try_read>(body); + + if (!ec) { + LOG_CRITICAL(Common, "Release JSON parse error: {}", ec.error().message()); + return {}; + } + + return ec.value(); +} + +bool HeadRequest(const std::string& url) { + // TODO: add timeouts + hydro::RequestOptions opts; +#ifdef YUZU_BUNDLED_OPENSSL + opts.connect.cert = std::string{kCert}; +#endif + + opts.connect.connectionTimeout = DefaultConnectTimeout; + + auto res = hydro::head(url, opts); + + if (!res) { + LOG_ERROR(Frontend, "Failed to download {}: {}", url, res.error()); + return false; + } else if (res->status_code < hydro::Status::Ok || res->status_code >= hydro::Status::MultipleChoices) { + LOG_ERROR(Frontend, "Received status code {} for {}", std::to_underlying(res->status_code), url); + return false; + } + + return true; } } // namespace Common::Net diff --git a/src/common/net/net.h b/src/common/net/net.h index 77c80d9dc3..f105a9a094 100644 --- a/src/common/net/net.h +++ b/src/common/net/net.h @@ -6,43 +6,42 @@ #include #include #include -#include -#include "common/common_types.h" namespace Common::Net { struct Asset { std::string name; - std::string url; - std::string path; - std::string filename; + std::size_t size; + std::optional digest; + std::string created_at; + std::string browser_download_url; +}; + +struct NamedAsset { + std::string name; + Asset asset; }; struct Release { - std::string title; + std::string tag_name; + std::string name; std::string body; - std::string tag; - std::string base_download_url; std::string html_url; - std::string host; + std::optional published_at; - std::vector assets; - - u64 id; - u64 published; - bool prerelease; - - // Get the relevant list of assets for the current platform. - std::vector GetPlatformAssets() const; - - static std::optional FromJson(const nlohmann::json& json, const std::string &host, const std::string& repo); - static std::optional FromJson(const std::string_view& json, const std::string &host, const std::string& repo); - static std::vector ListFromJson(const nlohmann::json &json, const std::string &host, const std::string &repo); - static std::vector ListFromJson(const std::string_view &json, const std::string &host, const std::string &repo); + std::vector assets; }; -// Make a request via httplib, and return the response body if applicable. -std::optional MakeRequest(const std::string &url, const std::string &path); +std::vector GetReleasesFromJson(const std::string& body); + +// Get the relevant list of assets for the current platform. +std::vector GetPlatformAssets(const Release &r); + +// Make a request, and return the response body if applicable. +std::optional MakeRequest(const std::string &url); +std::optional MakeRequest(const std::string_view url); + +bool HeadRequest(const std::string &url); // Get all of the latest stable releases. std::vector GetReleases(); diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1b578f67ba..39fb32a1ce 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1219,12 +1219,13 @@ endif() target_link_libraries(core PRIVATE OpenSSL::SSL + OpenSSL::Crypto fmt::fmt - nlohmann_json::nlohmann_json RenderDoc::API + jacinth::jacinth ZLIB::ZLIB) -target_link_libraries(core PUBLIC httplib::httplib zstd::zstd) +target_link_libraries(core PUBLIC zstd::zstd) if (ENABLE_WEB_SERVICE) target_compile_definitions(core PUBLIC ENABLE_WEB_SERVICE) @@ -1270,11 +1271,9 @@ if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64 OR ARCHITECTURE_riscv64 OR ARCHITE target_link_libraries(core PRIVATE dynarmic::dynarmic) endif() -if (TARGET OpenSSL::SSL) +# reserved for future reimplementations +if (TRUE) target_sources(core PRIVATE hle/service/ssl/ssl_backend_openssl.cpp) - target_link_libraries(core PRIVATE OpenSSL::SSL OpenSSL::Crypto) -else() - target_sources(core PRIVATE hle/service/ssl/ssl_backend_none.cpp) endif() create_target_directory_groups(core) diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index 4f1d849209..f23705bb4a 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -51,6 +51,30 @@ namespace Kernel { +struct KernelCore::SlabHeapContainer { + KSlabHeap client_session; + KSlabHeap event; + KSlabHeap port; + KSlabHeap process; + KSlabHeap resource_limit; + KSlabHeap session; + KSlabHeap light_session; + KSlabHeap shared_memory; + KSlabHeap shared_memory_info; + KSlabHeap thread; + KSlabHeap transfer_memory; + KSlabHeap code_memory; + KSlabHeap device_address_space; + KSlabHeap page_buffer; + KSlabHeap thread_local_page; + KSlabHeap object_name; + KSlabHeap session_request; + KSlabHeap secure_system_resource; + KSlabHeap lock_info; + KSlabHeap event_info; + KSlabHeap debug; +}; + // Can only be used by a single implementation PER THREAD struct ThreadLocalData { std::optional raw_thread; @@ -1310,30 +1334,6 @@ const Core::System& KernelCore::System() const { return impl->system; } -struct KernelCore::SlabHeapContainer { - KSlabHeap client_session; - KSlabHeap event; - KSlabHeap port; - KSlabHeap process; - KSlabHeap resource_limit; - KSlabHeap session; - KSlabHeap light_session; - KSlabHeap shared_memory; - KSlabHeap shared_memory_info; - KSlabHeap thread; - KSlabHeap transfer_memory; - KSlabHeap code_memory; - KSlabHeap device_address_space; - KSlabHeap page_buffer; - KSlabHeap thread_local_page; - KSlabHeap object_name; - KSlabHeap session_request; - KSlabHeap secure_system_resource; - KSlabHeap lock_info; - KSlabHeap event_info; - KSlabHeap debug; -}; - template KSlabHeap& KernelCore::SlabHeap() { if constexpr (std::is_same_v) { diff --git a/src/core/hle/service/bcat/news/builtin_news.cpp b/src/core/hle/service/bcat/news/builtin_news.cpp index bf4e989fb8..79fde934a3 100644 --- a/src/core/hle/service/bcat/news/builtin_news.cpp +++ b/src/core/hle/service/bcat/news/builtin_news.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "common/net/net.h" -#include "common/scm_rev.h" #include "core/hle/service/bcat/news/builtin_news.h" #include "core/hle/service/bcat/news/msgpack.h" #include "core/hle/service/bcat/news/news_storage.h" @@ -14,12 +13,7 @@ #include #include #include -#include -#include -#include "common/httplib.h" - -#include #include #include #include @@ -28,10 +22,6 @@ #include #include -#ifdef YUZU_BUNDLED_OPENSSL -#include -#endif - namespace Service::News { namespace { @@ -55,7 +45,7 @@ std::filesystem::path GetDefaultLogoPath(bool large) { } std::filesystem::path GetNewsImagePath(std::string_view news_id, bool large) { - const std::string filename = fmt::format("{}_{}.jpg", news_id, large ? "large" : "small"); + const std::string filename = std::format("{}_{}.jpg", news_id, large ? "large" : "small"); return Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "news" / "images" / filename; } @@ -81,24 +71,16 @@ std::vector TryLoadFromDisk(const std::filesystem::path& path) { // TODO(crueter): Migrate to use Common::Net std::vector DownloadImage(const std::string& url_path, const std::filesystem::path& cache_path) { + const auto url = std::format("https://eden-emu.dev{}", url_path); LOG_DEBUG(Service_BCAT, "Downloading image: https://eden-emu.dev{}", url_path); try { - httplib::Client cli("https://eden-emu.dev"); - cli.set_follow_location(true); - cli.set_connection_timeout(std::chrono::seconds(2)); - cli.set_read_timeout(std::chrono::seconds(2)); - -#ifdef YUZU_BUNDLED_OPENSSL - cli.load_ca_cert_store(kCert, sizeof(kCert)); -#endif - - if (auto res = cli.Get(url_path); res && res->status == 200 && !res->body.empty()) { - std::vector data(res->body.begin(), res->body.end()); + if (auto body = Common::Net::MakeRequest(url)) { + std::vector data(body->begin(), body->end()); std::error_code ec; std::filesystem::create_directories(cache_path.parent_path(), ec); if (std::ofstream out(cache_path, std::ios::binary); out) { - out.write(res->body.data(), static_cast(res->body.size())); + out.write(body->data(), static_cast(body->size())); } return data; } @@ -141,7 +123,7 @@ std::vector GetNewsImage(std::string_view news_id, bool large) { auto data = TryLoadFromDisk(cache_path); if (data.empty()) { - const std::string url = fmt::format("/news/{}_{}.jpg", id_str, large ? "large" : "small"); + const std::string url = std::format("/news/{}_{}.jpg", id_str, large ? "large" : "small"); data = DownloadImage(url, cache_path); } @@ -163,7 +145,7 @@ void PreloadNewsImages(const std::vector& news_ids) { futures.reserve(news_ids.size() * 2); for (const u32 id : news_ids) { - const std::string id_str = fmt::format("{}", id); + const std::string id_str = std::format("{}", id); { std::lock_guard lock{images_mutex}; @@ -292,24 +274,47 @@ std::string FormatBody(std::string body, const std::string_view &title) { return body; } +static inline u64 ParseIsoTimestamp(const std::string& iso) { + if (iso.empty()) + return 0; + + std::string buf = iso; + if (buf.back() == 'Z') + buf.pop_back(); + + std::tm tm{}; + std::istringstream ss(buf); + ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); + if (ss.fail()) + return 0; + +#ifdef _WIN32 + return static_cast(_mkgmtime(&tm)); +#else + return static_cast(timegm(&tm)); +#endif +} + void ImportReleases(const std::vector &releases) { std::vector news_ids; for (const auto& rel : releases) { - const u32 news_id = u32(rel.id & 0x7FFFFFFF); + std::hash hash; + const u32 news_id = u32(hash(rel.name) & 0x7FFFFFFF); news_ids.push_back(news_id); } PreloadNewsImages(news_ids); for (const auto& rel : releases) { - const std::string title = rel.title; + const std::string title = rel.name; const std::string body = rel.body; const std::string html_url = rel.html_url; - const u32 news_id = u32(rel.id & 0x7FFFFFFF); - const u64 published = rel.published; + std::hash hash; + const u32 news_id = u32(hash(rel.name) & 0x7FFFFFFF); + const u64 published = rel.published_at ? ParseIsoTimestamp(rel.published_at.value()) : 0; const u64 pickup_limit = published + 600000000; - const u32 priority = rel.prerelease ? 1500 : 2500; + const u32 priority = 2500; std::string author = "Eden"; @@ -317,7 +322,7 @@ void ImportReleases(const std::vector &releases) { pickup_limit, priority, {"en"}, author, {}, html_url, news_id); - const std::string news_id_str = fmt::format("LA{:020}", rel.id); + const std::string news_id_str = std::format("LA{:020}", news_id); GithubNewsMeta meta{ .news_id = news_id_str, @@ -352,7 +357,7 @@ std::vector BuildMsgpack(std::string_view title, std::string_view body, MsgPack::Writer w; const u32 news_id = override_id.value_or(HashToNewsId(title.empty() ? "eden" : title)); - const std::string news_id_str = fmt::format("{}", news_id); + const std::string news_id_str = std::format("{}", news_id); const auto img_small = GetNewsImage(news_id_str, false); const auto img_large = GetNewsImage(news_id_str, true); @@ -462,8 +467,8 @@ void EnsureBuiltinNewsLoaded() { LoadDefaultLogos(); if (const auto cached = ReadCachedJson()) { - const std::string_view body = cached.value(); - const auto releases = Common::Net::Release::ListFromJson(body, Common::g_build_auto_update_stable_api, Common::g_build_auto_update_stable_repo); + const std::string body = cached.value(); + const auto releases = Common::Net::GetReleasesFromJson(body); ImportReleases(releases); LOG_INFO(Service_BCAT, "news: {} entries loaded from cache", NewsStorage::Instance().ListAll().size()); @@ -471,9 +476,9 @@ void EnsureBuiltinNewsLoaded() { std::thread([] { if (const auto fresh = Common::Net::GetReleasesBody()) { - const std::string_view body = fresh.value(); + const std::string body = fresh.value(); WriteCachedJson(body); - const auto releases = Common::Net::Release::ListFromJson(body, Common::g_build_auto_update_stable_api, Common::g_build_auto_update_stable_repo); + const auto releases = Common::Net::GetReleasesFromJson(body); ImportReleases(releases); LOG_INFO(Service_BCAT, "news: {} entries updated from Forgejo", NewsStorage::Instance().ListAll().size()); diff --git a/src/core/launch_timestamp_cache.cpp b/src/core/launch_timestamp_cache.cpp index 2b030f20b3..861162e2f8 100644 --- a/src/core/launch_timestamp_cache.cpp +++ b/src/core/launch_timestamp_cache.cpp @@ -10,17 +10,20 @@ #include #include "common/container/unordered_map.h" -#include -#include - #include "common/fs/fs.h" -#include "common/fs/file.h" #include "common/fs/path_util.h" #include "common/logging.h" +import jacinth; + namespace Core::LaunchTimestampCache { namespace { +struct CacheEntry { + s64 timestamp = 0; + u64 launch_count = 0; +}; + using CacheMap = ::Common::unordered_map; using CountMap = ::Common::unordered_map; @@ -75,29 +78,25 @@ void Load() { } try { - const auto json = nlohmann::json::parse(data->data(), data->data() + data->size()); - if (!json.is_object()) { + const auto json = jacinth::doc::read(data.value()); + if (!json.is_object()) return; - } - for (auto it = json.begin(); it != json.end(); ++it) { - const auto key_str = it.key(); - const auto value = it.value(); + + for (auto [key_str, value] : json.as_object()) { u64 key{}; try { - key = std::stoull(key_str, nullptr, 16); + key = std::stoull(std::string{key_str}, nullptr, 16); } catch (...) { continue; } if (value.is_object()) { - if (value.contains("timestamp") && value["timestamp"].is_number_integer()) { - g_cache[key] = value["timestamp"].get(); - } - if (value.contains("launch_count") && value["launch_count"].is_number_unsigned()) { - g_counts[key] = value["launch_count"].get(); - } - } else if (value.is_number_integer()) { + CacheEntry entry = value; + // these will just be 0 if not present + g_cache[key] = entry.timestamp; + g_counts[key] = entry.launch_count; + } else { // Legacy format: raw timestamp only - g_cache[key] = value.get(); + g_cache[key] = value; } } } catch (const std::exception& e) { @@ -106,17 +105,16 @@ void Load() { } void Save() { - nlohmann::json json = nlohmann::json::object(); + jacinth::json json; for (const auto& [key, value] : g_cache) { - nlohmann::json entry = nlohmann::json::object(); - entry["timestamp"] = value; const auto count_it = g_counts.find(key); - entry["launch_count"] = count_it != g_counts.end() ? count_it->second : 0; - json[fmt::format("{:016X}", key)] = entry; + const CacheEntry entry{value, count_it != g_counts.end() ? count_it->second : 0}; + json[std::format("{:016X}", key)] = entry; } const auto path = GetCachePath(); - if (!WriteStringToFile(path, json.dump(4))) { + + if (!WriteStringToFile(path, json.dump({.pretty = true}))) { LOG_WARNING(Core, "Failed to write launch timestamp cache: {}", Common::FS::PathToUTF8String(path)); } diff --git a/src/core/reporter.cpp b/src/core/reporter.cpp index d7edb3ffcb..867645dada 100644 --- a/src/core/reporter.cpp +++ b/src/core/reporter.cpp @@ -5,12 +5,12 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include - -#include -#include -#include +#include +#include +#include #include "common/fs/file.h" #include "common/fs/fs.h" @@ -18,21 +18,133 @@ #include "common/hex_util.h" #include "common/scm_rev.h" #include "common/settings.h" -#include "core/arm/arm_interface.h" #include "core/core.h" #include "core/hle/ipc.h" -#include "core/hle/kernel/k_page_table.h" -#include "core/hle/kernel/k_process.h" #include "core/hle/result.h" #include "core/hle/service/hle_ipc.h" #include "core/memory.h" #include "core/reporter.h" +import jacinth; + +struct YuzuVersionData { + std::string scm_rev; + std::string scm_branch; + std::string scm_desc; + std::string build_name; + std::string build_date; + std::string build_fullname; + std::string build_version; +}; + +struct ReportCommonData { + std::string title_id; + std::string result_raw; + std::string result_module; + std::string result_description; + std::string timestamp; + std::optional user_id; +}; + +struct ProcessorStateData { + std::string entry_point; + std::string sp; + std::string pc; + std::string pstate; + std::string architecture; + std::map registers; + std::optional> backtrace; +}; + +struct CrashReport { + YuzuVersionData yuzu_version; + ReportCommonData report_common; + ProcessorStateData processor_state; +}; + +struct SvcBreakData { + std::string type; + std::string signal_debugger; + std::string info1; + std::string info2; + std::optional debug_buffer; +}; + +struct SvcBreakReport { + YuzuVersionData yuzu_version; + ReportCommonData report_common; + SvcBreakData svc_break; +}; + +struct BufferDescriptorEntry { + std::string address; + std::string size; + std::optional data; +}; + +struct HLERequestContextData { + std::vector command_buffer; + std::vector buffer_descriptor_a; + std::vector buffer_descriptor_b; + std::vector buffer_descriptor_c; + std::vector buffer_descriptor_x; + std::optional command_id; + std::optional function_name; + std::optional service_name; +}; + +struct UnimplementedFunctionReport { + YuzuVersionData yuzu_version; + ReportCommonData report_common; + HLERequestContextData function; +}; + +struct AppletCommonArgs { + std::string applet_id; + std::string common_args_version; + std::string library_version; + std::string theme_color; + std::string startup_sound; + std::string system_tick; +}; + +struct UnimplementedAppletReport { + YuzuVersionData yuzu_version; + ReportCommonData report_common; + AppletCommonArgs applet_common_args; + std::vector applet_normal_data; + std::vector applet_interactive_data; +}; + +struct PlayReport { + YuzuVersionData yuzu_version; + ReportCommonData report_common; + std::optional play_report_process_id; + std::string play_report_type; + std::vector play_report_data; +}; + +struct ErrorCustomText { + std::string main; + std::string detail; +}; + +struct ErrorReport { + YuzuVersionData yuzu_version; + ReportCommonData report_common; + ErrorCustomText error_custom_text; +}; + +struct FullDataAuto { + YuzuVersionData yuzu_version; + ReportCommonData report_common; +}; + namespace { std::filesystem::path GetPath(std::string_view type, u64 title_id, std::string_view timestamp) { return Common::FS::GetEdenPath(Common::FS::EdenPath::LogDir) / type / - fmt::format("{:016X}_{}.json", title_id, timestamp); + std::format("{:016X}_{}.json", title_id, timestamp); } std::string GetTimestamp() { @@ -42,125 +154,123 @@ std::string GetTimestamp() { return oss.str(); } -using namespace nlohmann; - -void SaveToFile(const json& json, const std::filesystem::path& filename) { +template +void SaveToFile(const T& data, const std::filesystem::path& filename) { if (!Common::FS::CreateParentDirs(filename)) { LOG_ERROR(Core, "Failed to create path for '{}' to save report!", Common::FS::PathToUTF8String(filename)); return; } + const auto buffer = jacinth::json::dump(data, {.pretty = true}); + std::ofstream file; Common::FS::OpenFileStream(file, filename, std::ios_base::out | std::ios_base::trunc); - - file << std::setw(4) << json << std::endl; + file << buffer << std::endl; } -json GetYuzuVersionData() { +YuzuVersionData GetYuzuVersionData() { return { - {"scm_rev", std::string(Common::g_scm_rev)}, - {"scm_branch", std::string(Common::g_scm_branch)}, - {"scm_desc", std::string(Common::g_scm_desc)}, - {"build_name", std::string(Common::g_build_name)}, - {"build_date", std::string(Common::g_build_date)}, - {"build_fullname", std::string(Common::g_build_fullname)}, - {"build_version", std::string(Common::g_build_version)}, + .scm_rev = std::string(Common::g_scm_rev), + .scm_branch = std::string(Common::g_scm_branch), + .scm_desc = std::string(Common::g_scm_desc), + .build_name = std::string(Common::g_build_name), + .build_date = std::string(Common::g_build_date), + .build_fullname = std::string(Common::g_build_fullname), + .build_version = std::string(Common::g_build_version), }; } -json GetReportCommonData(u64 title_id, Result result, const std::string& timestamp, - std::optional user_id = {}) { - auto out = json{ - {"title_id", fmt::format("{:016X}", title_id)}, - {"result_raw", fmt::format("{:08X}", result.raw)}, - {"result_module", fmt::format("{:08X}", static_cast(result.GetModule()))}, - {"result_description", fmt::format("{:08X}", result.GetDescription())}, - {"timestamp", timestamp}, +ReportCommonData GetReportCommonData(u64 title_id, Result result, const std::string& timestamp, + std::optional user_id = {}) { + auto out = ReportCommonData{ + .title_id = std::format("{:016X}", title_id), + .result_raw = std::format("{:08X}", result.raw), + .result_module = std::format("{:08X}", static_cast(result.GetModule())), + .result_description = std::format("{:08X}", result.GetDescription()), + .timestamp = timestamp, }; if (user_id.has_value()) { - out["user_id"] = fmt::format("{:016X}{:016X}", (*user_id)[1], (*user_id)[0]); + out.user_id = std::format("{:016X}{:016X}", (*user_id)[1], (*user_id)[0]); } return out; } -json GetProcessorStateData(const std::string& architecture, u64 entry_point, u64 sp, u64 pc, - u64 pstate, const std::array& registers, - const std::optional>& backtrace = {}) { - auto out = json{ - {"entry_point", fmt::format("{:016X}", entry_point)}, - {"sp", fmt::format("{:016X}", sp)}, - {"pc", fmt::format("{:016X}", pc)}, - {"pstate", fmt::format("{:016X}", pstate)}, - {"architecture", architecture}, - }; - - auto registers_out = json::object(); +ProcessorStateData GetProcessorStateData(const std::string& architecture, u64 entry_point, u64 sp, + u64 pc, u64 pstate, + const std::array& registers, + const std::optional>& backtrace = {}) { + std::map registers_out; for (std::size_t i = 0; i < registers.size(); ++i) { - registers_out[fmt::format("X{:02d}", i)] = fmt::format("{:016X}", registers[i]); + registers_out[std::format("X{:02d}", i)] = std::format("{:016X}", registers[i]); } - out["registers"] = std::move(registers_out); - + std::optional> backtrace_out; if (backtrace.has_value()) { - auto backtrace_out = json::array(); - for (const auto& entry : *backtrace) { - backtrace_out.push_back(fmt::format("{:016X}", entry)); - } - out["backtrace"] = std::move(backtrace_out); + backtrace_out = (*backtrace) | std::ranges::views::transform([](u64 entry) { + return std::format("{:016X}", entry); + }) | + std::ranges::to>(); } - return out; + return { + .entry_point = std::format("{:016X}", entry_point), + .sp = std::format("{:016X}", sp), + .pc = std::format("{:016X}", pc), + .pstate = std::format("{:016X}", pstate), + .architecture = architecture, + .registers = std::move(registers_out), + .backtrace = std::move(backtrace_out), + }; } -json GetFullDataAuto(const std::string& timestamp, u64 title_id, Core::System& system) { - json out; - - out["yuzu_version"] = GetYuzuVersionData(); - out["report_common"] = GetReportCommonData(title_id, ResultSuccess, timestamp); - - return out; +FullDataAuto GetFullDataAuto(const std::string& timestamp, u64 title_id, Core::System& system) { + return { + .yuzu_version = GetYuzuVersionData(), + .report_common = GetReportCommonData(title_id, ResultSuccess, timestamp), + }; } template -json GetHLEBufferDescriptorData(const boost::container::static_vector& buffer, Core::Memory::Memory& memory) { - auto buffer_out = json::array(); +std::vector GetHLEBufferDescriptorData( + const boost::container::static_vector& buffer, + Core::Memory::Memory& memory) { + std::vector out; + out.reserve(buffer.size()); for (const auto& desc : buffer) { - auto entry = json{ - {"address", fmt::format("{:016X}", desc.Address())}, - {"size", fmt::format("{:016X}", desc.Size())}, + BufferDescriptorEntry entry{ + .address = std::format("{:016X}", desc.Address()), + .size = std::format("{:016X}", desc.Size()), }; if constexpr (read_value) { std::vector data(desc.Size()); memory.ReadBlock(desc.Address(), data.data(), desc.Size()); - entry["data"] = Common::HexToString(data); + entry.data = Common::HexToString(data); } - buffer_out.push_back(std::move(entry)); + out.push_back(std::move(entry)); } - - return buffer_out; + return out; } -json GetHLERequestContextData(Service::HLERequestContext& ctx, Core::Memory::Memory& memory) { - json out; - - auto cmd_buf = json::array(); +HLERequestContextData GetHLERequestContextData(Service::HLERequestContext& ctx, + Core::Memory::Memory& memory) { + std::vector cmd_buf; + cmd_buf.reserve(IPC::COMMAND_BUFFER_LENGTH); for (std::size_t i = 0; i < IPC::COMMAND_BUFFER_LENGTH; ++i) { - cmd_buf.push_back(fmt::format("{:08X}", ctx.CommandBuffer()[i])); + cmd_buf.push_back(std::format("{:08X}", ctx.CommandBuffer()[i])); } - out["command_buffer"] = std::move(cmd_buf); - - out["buffer_descriptor_a"] = GetHLEBufferDescriptorData(ctx.BufferDescriptorA(), memory); - out["buffer_descriptor_b"] = GetHLEBufferDescriptorData(ctx.BufferDescriptorB(), memory); - out["buffer_descriptor_c"] = GetHLEBufferDescriptorData(ctx.BufferDescriptorC(), memory); - out["buffer_descriptor_x"] = GetHLEBufferDescriptorData(ctx.BufferDescriptorX(), memory); - - return out; + return { + .command_buffer = std::move(cmd_buf), + .buffer_descriptor_a = GetHLEBufferDescriptorData(ctx.BufferDescriptorA(), memory), + .buffer_descriptor_b = GetHLEBufferDescriptorData(ctx.BufferDescriptorB(), memory), + .buffer_descriptor_c = GetHLEBufferDescriptorData(ctx.BufferDescriptorC(), memory), + .buffer_descriptor_x = GetHLEBufferDescriptorData(ctx.BufferDescriptorX(), memory), + }; } } // Anonymous namespace @@ -183,21 +293,21 @@ void Reporter::SaveCrashReport(u64 title_id, Result result, u64 set_flags, u64 e } const auto timestamp = GetTimestamp(); - json out; - out["yuzu_version"] = GetYuzuVersionData(); - out["report_common"] = GetReportCommonData(title_id, result, timestamp); + CrashReport out{ + .yuzu_version = GetYuzuVersionData(), + .report_common = GetReportCommonData(title_id, result, timestamp), + .processor_state = GetProcessorStateData(arch, entry_point, sp, pc, pstate, registers, + backtrace), + }; - auto proc_out = GetProcessorStateData(arch, entry_point, sp, pc, pstate, registers, backtrace); - proc_out["set_flags"] = fmt::format("{:016X}", set_flags); - proc_out["afsr0"] = fmt::format("{:016X}", afsr0); - proc_out["afsr1"] = fmt::format("{:016X}", afsr1); - proc_out["esr"] = fmt::format("{:016X}", esr); - proc_out["far"] = fmt::format("{:016X}", far); - proc_out["backtrace_size"] = fmt::format("{:08X}", backtrace_size); - proc_out["unknown_10"] = fmt::format("{:08X}", unk10); - - out["processor_state"] = std::move(proc_out); + out.processor_state.registers["set_flags"] = std::format("{:016X}", set_flags); + out.processor_state.registers["afsr0"] = std::format("{:016X}", afsr0); + out.processor_state.registers["afsr1"] = std::format("{:016X}", afsr1); + out.processor_state.registers["esr"] = std::format("{:016X}", esr); + out.processor_state.registers["far"] = std::format("{:016X}", far); + out.processor_state.registers["backtrace_size"] = std::format("{:08X}", backtrace_size); + out.processor_state.registers["unknown_10"] = std::format("{:08X}", unk10); SaveToFile(out, GetPath("crash_report", title_id, timestamp)); } @@ -210,21 +320,22 @@ void Reporter::SaveSvcBreakReport(u32 type, bool signal_debugger, u64 info1, u64 const auto timestamp = GetTimestamp(); const auto title_id = system.GetApplicationProcessProgramID(); - auto out = GetFullDataAuto(timestamp, title_id, system); - auto break_out = json{ - {"type", fmt::format("{:08X}", type)}, - {"signal_debugger", fmt::format("{}", signal_debugger)}, - {"info1", fmt::format("{:016X}", info1)}, - {"info2", fmt::format("{:016X}", info2)}, + SvcBreakReport out{ + .yuzu_version = GetYuzuVersionData(), + .report_common = GetReportCommonData(title_id, ResultSuccess, timestamp), + .svc_break = SvcBreakData{ + .type = std::format("{:08X}", type), + .signal_debugger = std::format("{}", signal_debugger), + .info1 = std::format("{:016X}", info1), + .info2 = std::format("{:016X}", info2), + }, }; if (resolved_buffer.has_value()) { - break_out["debug_buffer"] = Common::HexToString(*resolved_buffer); + out.svc_break.debug_buffer = Common::HexToString(*resolved_buffer); } - out["svc_break"] = std::move(break_out); - SaveToFile(out, GetPath("svc_break_report", title_id, timestamp)); } @@ -237,14 +348,16 @@ void Reporter::SaveUnimplementedFunctionReport(Service::HLERequestContext& ctx, const auto timestamp = GetTimestamp(); const auto title_id = system.GetApplicationProcessProgramID(); - auto out = GetFullDataAuto(timestamp, title_id, system); - auto function_out = GetHLERequestContextData(ctx, ctx.GetMemory()); - function_out["command_id"] = command_id; - function_out["function_name"] = name; - function_out["service_name"] = service_name; + UnimplementedFunctionReport out{ + .yuzu_version = GetYuzuVersionData(), + .report_common = GetReportCommonData(title_id, ResultSuccess, timestamp), + .function = GetHLERequestContextData(ctx, ctx.GetMemory()), + }; - out["function"] = std::move(function_out); + out.function.command_id = command_id; + out.function.function_name = name; + out.function.service_name = service_name; SaveToFile(out, GetPath("unimpl_func_report", title_id, timestamp)); } @@ -259,29 +372,30 @@ void Reporter::SaveUnimplementedAppletReport( const auto timestamp = GetTimestamp(); const auto title_id = system.GetApplicationProcessProgramID(); - auto out = GetFullDataAuto(timestamp, title_id, system); - out["applet_common_args"] = { - {"applet_id", fmt::format("{:02X}", applet_id)}, - {"common_args_version", fmt::format("{:08X}", common_args_version)}, - {"library_version", fmt::format("{:08X}", library_version)}, - {"theme_color", fmt::format("{:08X}", theme_color)}, - {"startup_sound", fmt::format("{}", startup_sound)}, - {"system_tick", fmt::format("{:016X}", system_tick)}, + UnimplementedAppletReport out{ + .yuzu_version = GetYuzuVersionData(), + .report_common = GetReportCommonData(title_id, ResultSuccess, timestamp), + .applet_common_args = AppletCommonArgs{ + .applet_id = std::format("{:02X}", applet_id), + .common_args_version = std::format("{:08X}", common_args_version), + .library_version = std::format("{:08X}", library_version), + .theme_color = std::format("{:08X}", theme_color), + .startup_sound = std::format("{}", startup_sound), + .system_tick = std::format("{:016X}", system_tick), + }, }; - auto normal_out = json::array(); - for (const auto& data : normal_channel) { - normal_out.push_back(Common::HexToString(data)); - } + out.applet_normal_data = normal_channel | std::ranges::views::transform([](const auto& data) { + return Common::HexToString(data); + }) | + std::ranges::to>(); - auto interactive_out = json::array(); - for (const auto& data : interactive_channel) { - interactive_out.push_back(Common::HexToString(data)); - } - - out["applet_normal_data"] = std::move(normal_out); - out["applet_interactive_data"] = std::move(interactive_out); + out.applet_interactive_data = + interactive_channel | std::ranges::views::transform([](const auto& data) { + return Common::HexToString(data); + }) | + std::ranges::to>(); SaveToFile(out, GetPath("unimpl_applet_report", title_id, timestamp)); } @@ -294,22 +408,22 @@ void Reporter::SavePlayReport(PlayReportType type, u64 title_id, } const auto timestamp = GetTimestamp(); - json out; - out["yuzu_version"] = GetYuzuVersionData(); - out["report_common"] = GetReportCommonData(title_id, ResultSuccess, timestamp, user_id); + PlayReport out{ + .yuzu_version = GetYuzuVersionData(), + .report_common = GetReportCommonData(title_id, ResultSuccess, timestamp, user_id), + }; - auto data_out = json::array(); - for (const auto& d : data) { - data_out.push_back(Common::HexToString(d)); - } + out.play_report_data = data | std::ranges::views::transform([](const auto& d) { + return Common::HexToString(d); + }) | + std::ranges::to>(); if (process_id.has_value()) { - out["play_report_process_id"] = fmt::format("{:016X}", *process_id); + out.play_report_process_id = std::format("{:016X}", *process_id); } - out["play_report_type"] = fmt::format("{:02}", static_cast(type)); - out["play_report_data"] = std::move(data_out); + out.play_report_type = std::format("{:02}", static_cast(type)); SaveToFile(out, GetPath("play_report", title_id, timestamp)); } @@ -322,14 +436,14 @@ void Reporter::SaveErrorReport(u64 title_id, Result result, } const auto timestamp = GetTimestamp(); - json out; - out["yuzu_version"] = GetYuzuVersionData(); - out["report_common"] = GetReportCommonData(title_id, result, timestamp); - - out["error_custom_text"] = { - {"main", custom_text_main.value_or("")}, - {"detail", custom_text_detail.value_or("")}, + ErrorReport out{ + .yuzu_version = GetYuzuVersionData(), + .report_common = GetReportCommonData(title_id, result, timestamp), + .error_custom_text = ErrorCustomText{ + .main = custom_text_main.value_or(""), + .detail = custom_text_detail.value_or(""), + }, }; SaveToFile(out, GetPath("error_report", title_id, timestamp)); diff --git a/src/frontend_common/CMakeLists.txt b/src/frontend_common/CMakeLists.txt index f9b3805939..8c417c3975 100644 --- a/src/frontend_common/CMakeLists.txt +++ b/src/frontend_common/CMakeLists.txt @@ -17,12 +17,9 @@ add_library(frontend_common STATIC mod_manager.h mod_manager.cpp) if (ENABLE_UPDATE_CHECKER) - target_link_libraries(frontend_common PRIVATE httplib::httplib) - target_link_libraries(frontend_common PRIVATE nlohmann_json::nlohmann_json) target_sources(frontend_common PRIVATE update_checker.cpp update_checker.h) - target_link_libraries(frontend_common PRIVATE OpenSSL::SSL OpenSSL::Crypto) endif() create_target_directory_groups(frontend_common) diff --git a/src/frontend_common/update_checker.cpp b/src/frontend_common/update_checker.cpp index cccced5f60..1470e663f0 100644 --- a/src/frontend_common/update_checker.cpp +++ b/src/frontend_common/update_checker.cpp @@ -10,7 +10,6 @@ #include #endif -#include #include "common/net/net.h" #include "common/scm_rev.h" #include "update_checker.h" @@ -21,12 +20,12 @@ std::optional UpdateChecker::GetUpdate() { const auto latest = Common::Net::GetLatestRelease(); if (!latest) return std::nullopt; - LOG_INFO(Frontend, "Received update {}", latest->title); + LOG_INFO(Frontend, "Received update {}", latest->name); #ifdef NIGHTLY_BUILD std::vector result; - boost::split(result, latest->tag, boost::is_any_of(".")); + boost::split(result, latest->tag_name, boost::is_any_of(".")); if (result.size() != 2) return std::nullopt; @@ -38,7 +37,7 @@ std::optional UpdateChecker::GetUpdate() { const std::string build = result[0]; #else - const std::string tag = latest->tag; + const std::string tag = latest->tag_name; const std::string build = Common::g_build_version; #endif diff --git a/src/hid_core/resources/abstracted_pad/abstract_pad_holder.h b/src/hid_core/resources/abstracted_pad/abstract_pad_holder.h index fb7f472e83..14afc328f4 100644 --- a/src/hid_core/resources/abstracted_pad/abstract_pad_holder.h +++ b/src/hid_core/resources/abstracted_pad/abstract_pad_holder.h @@ -1,10 +1,12 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include -#include #include #include "common/common_types.h" diff --git a/src/hid_core/resources/npad/npad_types.h b/src/hid_core/resources/npad/npad_types.h index 2f7ac2d4f7..4422b83f65 100644 --- a/src/hid_core/resources/npad/npad_types.h +++ b/src/hid_core/resources/npad/npad_types.h @@ -12,6 +12,7 @@ #include "common/common_funcs.h" #include "common/common_types.h" #include "hid_core/hid_types.h" +#include namespace Core::HID { class EmulatedController; diff --git a/src/network/room.cpp b/src/network/room.cpp index 2069673bae..66a9dba807 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -1108,7 +1108,7 @@ std::vector Room::GetRoomMemberList() const { member.nickname = member_impl.nickname; member.username = member_impl.user_data.username; member.display_name = member_impl.user_data.display_name; - member.avatar_url = member_impl.user_data.avatar_url; + member.avatarUrl = member_impl.user_data.avatar_url; member.fake_ip = member_impl.fake_ip; member.game = member_impl.game_info; member_list.push_back(member); diff --git a/src/qt_common/CMakeLists.txt b/src/qt_common/CMakeLists.txt index ea8b745567..70ccc0b545 100644 --- a/src/qt_common/CMakeLists.txt +++ b/src/qt_common/CMakeLists.txt @@ -49,7 +49,7 @@ if (USE_DISCORD_PRESENCE) discord/discord_impl.cpp discord/discord_impl.h ) - target_link_libraries(qt_common PUBLIC DiscordRPC::discord-rpc httplib::httplib) + target_link_libraries(qt_common PUBLIC DiscordRPC::discord-rpc) if (YUZU_USE_BUNDLED_OPENSSL) target_link_libraries(qt_common PUBLIC OpenSSL::SSL OpenSSL::Crypto) diff --git a/src/qt_common/abstract/frontend.h b/src/qt_common/abstract/frontend.h index 48c7c6edd0..e1c1d5dc63 100644 --- a/src/qt_common/abstract/frontend.h +++ b/src/qt_common/abstract/frontend.h @@ -71,13 +71,13 @@ StandardButton ShowMessage(Icon icon, const QString& title, const QString& text, StandardButtons buttons = StandardButton::NoButton, QObject* parent = nullptr); -#define UTIL_OVERRIDES(level) \ +#define UTIL_OVERRIDES(level, defaultButtons) \ inline StandardButton level(QObject* parent, const QString& title, const QString& text, \ StandardButtons buttons) { \ return ShowMessage(Icon::level, title, text, buttons, parent); \ } \ inline StandardButton level(QObject* parent, const QString& title, const QString& text, \ - int buttons = StandardButton::Ok) { \ + int buttons = defaultButtons) { \ return ShowMessage(Icon::level, title, text, StandardButtons(buttons), parent); \ } \ inline StandardButton level(const QString title, const QString& text, \ @@ -85,14 +85,14 @@ StandardButton ShowMessage(Icon icon, const QString& title, const QString& text, return ShowMessage(Icon::level, title, text, buttons, rootObject); \ } \ inline StandardButton level(const QString& title, const QString& text, \ - int buttons = StandardButton::Ok) { \ + int buttons = defaultButtons) { \ return ShowMessage(Icon::level, title, text, StandardButtons(buttons), rootObject); \ } -UTIL_OVERRIDES(Information) -UTIL_OVERRIDES(Warning) -UTIL_OVERRIDES(Critical) -UTIL_OVERRIDES(Question) +UTIL_OVERRIDES(Information, StandardButton::Ok) +UTIL_OVERRIDES(Warning, StandardButton::Ok) +UTIL_OVERRIDES(Critical, StandardButton::Ok) +UTIL_OVERRIDES(Question, StandardButton::Yes | StandardButton::No) const QString GetOpenFileName(const QString& title, const QString& dir, const QString& filter, QString* selectedFilter = nullptr); diff --git a/src/qt_common/discord/discord_impl.cpp b/src/qt_common/discord/discord_impl.cpp index 666b490347..291d4b4cb2 100644 --- a/src/qt_common/discord/discord_impl.cpp +++ b/src/qt_common/discord/discord_impl.cpp @@ -9,12 +9,11 @@ #include #include -#include "common/httplib.h" #include -#include #include "common/common_types.h" +#include "common/net/net.h" #include "common/string_util.h" #include "core/core.h" #include "core/loader/loader.h" @@ -98,26 +97,11 @@ void DiscordImpl::Update() { // Used to format Icon URL for yuzu website game compatibility page std::string icon_name = GetGameString(game_title); - auto const game_url = fmt::format( + auto const game_url = std::format( "https://raw.githubusercontent.com/eden-emulator/boxart/refs/heads/master/img/{}.png", icon_name); - httplib::SSLClient client(game_url); - client.set_connection_timeout(3); - client.set_read_timeout(3); - client.set_follow_location(true); - -#ifdef YUZU_BUNDLED_OPENSSL - client.load_ca_cert_store(kCert, sizeof(kCert)); -#endif - - httplib::Request request{ - .method = "HEAD", - .path = game_url, - }; - - auto res = client.send(request); - UpdateGameStatus(game_url, res && res->status == 200); + UpdateGameStatus(game_url, Common::Net::HeadRequest(game_url)); return; } diff --git a/src/qt_common/qt_common.cpp b/src/qt_common/qt_common.cpp index fa34a74cb9..eff63a37d5 100644 --- a/src/qt_common/qt_common.cpp +++ b/src/qt_common/qt_common.cpp @@ -285,6 +285,14 @@ void Init(QWidget* root) { "Platform backend is set to Wayland, which may cause performance/stability issues"); } +void Shutdown() { + emu_thread.reset(); + system.reset(); + provider.reset(); + vfs.reset(); + rootObject = nullptr; +} + std::filesystem::path GetEdenCommand() { std::filesystem::path command; diff --git a/src/qt_common/qt_common.h b/src/qt_common/qt_common.h index 0307dda1dc..96f6d95e0b 100644 --- a/src/qt_common/qt_common.h +++ b/src/qt_common/qt_common.h @@ -39,6 +39,9 @@ Core::Frontend::EmuWindow::WindowSystemInfo GetWindowSystemInfo(QWindow* window) void Init(QWidget* root); +// Destroys the global core state. Must be called before static destructors run. +void Shutdown(); + void SetupContentProviders(); void SetupHID(); diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 875c40604c..5653244426 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -25,7 +25,8 @@ add_executable(tests create_target_directory_groups(tests) target_link_libraries(tests PRIVATE common core input_common video_core) -target_link_libraries(tests PRIVATE ${PLATFORM_LIBRARIES} Catch2::Catch2WithMain Threads::Threads) +target_link_libraries(tests PRIVATE ${PLATFORM_LIBRARIES} Catch2::Catch2WithMain + Threads::Threads OpenSSL::SSL OpenSSL::Crypto) add_test(NAME tests COMMAND tests) diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index d4debb3c09..7f097b7af5 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -17,8 +17,7 @@ add_library(web_service STATIC ) create_target_directory_groups(web_service) -target_include_directories(web_service PUBLIC ${cpp-jwt_SOURCE_DIR}/include) -target_link_libraries(web_service PRIVATE common network nlohmann_json::nlohmann_json httplib::httplib cpp-jwt::cpp-jwt) +target_link_libraries(web_service PRIVATE common network hydro::carbon jacinth::jacinth cpp-jwt::cpp-jwt) find_package(OpenSSL REQUIRED) target_link_libraries(web_service PRIVATE OpenSSL::SSL OpenSSL::Crypto) diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp index c3a44257ef..219b93f74e 100644 --- a/src/web_service/announce_room_json.cpp +++ b/src/web_service/announce_room_json.cpp @@ -4,78 +4,55 @@ // SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include -#include #include "common/logging.h" #include "web_service/announce_room_json.h" #include "web_service/web_backend.h" +import jacinth; + namespace AnnounceMultiplayerRoom { -static void to_json(nlohmann::json& json, const Member& member) { - if (!member.username.empty()) { - json["username"] = member.username; - } - json["nickname"] = member.nickname; - if (!member.avatar_url.empty()) { - json["avatarUrl"] = member.avatar_url; - } - json["gameName"] = member.game.name; - json["gameId"] = member.game.id; -} - -static void from_json(const nlohmann::json& json, Member& member) { - member.nickname = json.at("nickname").get(); - member.game.name = json.at("gameName").get(); - member.game.id = json.at("gameId").get(); - try { - member.username = json.at("username").get(); - member.avatar_url = json.at("avatarUrl").get(); - } catch (const nlohmann::detail::out_of_range&) { - member.username = member.avatar_url = ""; - LOG_DEBUG(Network, "Member \'{}\' isn't authenticated", member.nickname); - } -} - -static void to_json(nlohmann::json& json, const Room& room) { +// TODO: rewrite this to use jacinth meta spec +static void to_json(jacinth::mutable_value json, const Room& room) +{ json["port"] = room.information.port; json["name"] = room.information.name; - if (!room.information.description.empty()) { + if (!room.information.description.empty()) json["description"] = room.information.description; - } json["preferredGameName"] = room.information.preferred_game.name; json["preferredGameId"] = room.information.preferred_game.id; json["maxPlayers"] = room.information.member_slots; - json["netVersion"] = room.net_version; - json["hasPassword"] = room.has_password; - if (room.members.size() > 0) { - nlohmann::json member_json = room.members; - json["players"] = member_json; - } + json["netVersion"] = room.netVersion; + json["hasPassword"] = room.hasPassword; + if (!room.members.empty()) + json["players"] = room.members; } -static void from_json(const nlohmann::json& json, Room& room) { - room.verify_uid = json.at("externalGuid").get(); - room.ip = json.at("address").get(); - room.information.name = json.at("name").get(); - try { - room.information.description = json.at("description").get(); - } catch (const nlohmann::detail::out_of_range&) { +static void from_json(const jacinth::value& json, Room& room) +{ + room.verify_uid = json.get("externalGuid"); + room.ip = json.get("address"); + room.information.name = json.get("name"); + + if (json["description"].is_null()) { room.information.description = ""; - LOG_DEBUG(Network, "Room \'{}\' doesn't contain a description", room.information.name); - } - room.information.host_username = json.at("owner").get(); - room.information.port = json.at("port").get(); - room.information.preferred_game.name = json.at("preferredGameName").get(); - room.information.preferred_game.id = json.at("preferredGameId").get(); - room.information.member_slots = json.at("maxPlayers").get(); - room.net_version = json.at("netVersion").get(); - room.has_password = json.at("hasPassword").get(); - try { - room.members = json.at("players").get>(); - } catch (const nlohmann::detail::out_of_range& e) { - LOG_DEBUG(Network, "Out of range {}", e.what()); + LOG_DEBUG(Network, "Room '{}' doesn't contain a description", room.information.name); + } else { + room.information.description = json.get("description"); } + + room.information.host_username = json.get("owner"); + room.information.port = json.get("port"); + room.information.preferred_game.name = json.get("preferredGameName"); + room.information.preferred_game.id = json.get("preferredGameId"); + room.information.member_slots = json.get("maxPlayers"); + room.netVersion = json.get("netVersion"); + room.hasPassword = json.get("hasPassword"); + + if (json["players"].is_null()) + LOG_DEBUG(Network, "No players key"); + else + room.members = json.get("players"); } } // namespace AnnounceMultiplayerRoom @@ -90,8 +67,8 @@ void RoomJson::SetRoomInformation(const std::string& name, const std::string& de room.information.description = description; room.information.port = port; room.information.member_slots = max_player; - room.net_version = net_version; - room.has_password = has_password; + room.netVersion = net_version; + room.hasPassword = has_password; room.information.preferred_game = preferred_game; } void RoomJson::AddPlayer(const AnnounceMultiplayerRoom::Member& member) { @@ -104,19 +81,22 @@ WebService::WebResult RoomJson::Update() { return WebService::WebResult{WebService::WebResult::Code::LibError, "Room is not registered", ""}; } - nlohmann::json json{{"players", room.members}}; + jacinth::json json; + json["players"] = room.members; return client.PostJson(fmt::format("/lobby/{}", room_id), json.dump(), false); } WebService::WebResult RoomJson::Register() { - nlohmann::json json = room; + jacinth::json json = room; auto result = client.PostJson("/lobby", json.dump(), false); if (result.result_code != WebService::WebResult::Code::Success) { return result; } - auto reply_json = nlohmann::json::parse(result.returned_data); - room = reply_json.get(); - room_id = reply_json.at("id").get(); + + auto reply_json = jacinth::json::read(result.returned_data); + room = reply_json; + room_id = reply_json.get("id"); + return WebService::WebResult{WebService::WebResult::Code::Success, "", room.verify_uid}; } @@ -129,7 +109,7 @@ AnnounceMultiplayerRoom::RoomList RoomJson::GetRoomList() { if (reply.empty()) { return {}; } - return nlohmann::json::parse(reply).at("rooms").get(); + return jacinth::json::read(reply)["rooms"]; } void RoomJson::Delete() { diff --git a/src/web_service/verify_login.cpp b/src/web_service/verify_login.cpp index d5b7161cbe..a7601c5a42 100644 --- a/src/web_service/verify_login.cpp +++ b/src/web_service/verify_login.cpp @@ -1,27 +1,39 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2017 Citra Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include +#include "common/logging.h" #include "web_service/verify_login.h" #include "web_service/web_backend.h" #include "web_service/web_result.h" +import jacinth; + namespace WebService { +struct Reply { + std::optional username; +}; + bool VerifyLogin(const std::string& host, const std::string& username, const std::string& token) { Client client(host, username, token); auto reply = client.GetJson("/profile", false).returned_data; if (reply.empty()) { return false; } - nlohmann::json json = nlohmann::json::parse(reply); - const auto iter = json.find("username"); - if (iter == json.end()) { - return username.empty(); + auto reply_s = jacinth::doc::try_read(reply); + + if (!reply_s) { + LOG_WARNING(WebService, "Failed to parse verification profile"); + return false; } - return *iter == username; + if (!reply_s->username) return username.empty(); + + return reply_s->username == username; } } // namespace WebService diff --git a/src/web_service/web_backend.cpp b/src/web_service/web_backend.cpp index 7fc2ae69b1..029319bc5f 100644 --- a/src/web_service/web_backend.cpp +++ b/src/web_service/web_backend.cpp @@ -5,11 +5,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include +#include #include #include - -#include -#include "common/httplib.h" +#include #ifdef YUZU_BUNDLED_OPENSSL #include @@ -19,12 +19,12 @@ #include "web_service/web_backend.h" #include "web_service/web_result.h" +import hydrocarbon; + namespace WebService { constexpr std::array API_VERSION{'1'}; -constexpr std::size_t TIMEOUT_SECONDS = 30; - struct Client::Impl { Impl(std::string host_, std::string username_, std::string token_) : host{std::move(host_)}, username{std::move(username_)}, token{std::move(token_)} { @@ -72,72 +72,53 @@ struct Client::Impl { const std::string& data, const std::string& accept, const std::string& jwt_ = "", const std::string& username_ = "", const std::string& token_ = "") { - if (cli == nullptr) { - cli = std::make_unique(host.c_str()); - cli->set_connection_timeout(TIMEOUT_SECONDS); - cli->set_read_timeout(TIMEOUT_SECONDS); - cli->set_write_timeout(TIMEOUT_SECONDS); + hydro::RequestOptions opts; #ifdef YUZU_BUNDLED_OPENSSL - cli->load_ca_cert_store(kCert, sizeof(kCert)); + opts.connect.cert = std::string{kCert}; #endif - } - if (!cli->is_valid()) { - LOG_ERROR(WebService, "Invalid URL {}", host + path); - return WebResult{WebResult::Code::InvalidURL, "Invalid URL", ""}; - } - httplib::Headers params; + opts.connect.connectionTimeout = std::chrono::milliseconds{10000}; + if (!jwt_.empty()) { - params = { - {std::string("Authorization"), fmt::format("Bearer {}", jwt_)}, - }; + opts.headers.add("Authorization", std::format("Bearer {}", jwt_)); } else if (!username_.empty()) { - params = { - {std::string("x-username"), username_}, - {std::string("x-token"), token_}, - }; + opts.headers.add("x-username", username_); + opts.headers.add("x-token", token_); } - params.emplace(std::string("api-version"), - std::string(API_VERSION.begin(), API_VERSION.end())); + opts.headers.add("api-version", std::string(API_VERSION.begin(), API_VERSION.end())); if (method != "GET") { - params.emplace(std::string("Content-Type"), std::string("application/json")); + opts.headers.add("Content-Type", "application/json"); } - httplib::Request request; - request.method = method; - request.path = path; - request.headers = params; - request.body = data; + opts.body = data; - httplib::Result result = cli->send(request); + auto res = hydro::post(std::format("{}{}", host, path), opts); - if (!result) { - LOG_ERROR(WebService, "{} to {} returned null", method, host + path); + if (!res) { + LOG_ERROR(WebService, "{} to {} returned error {}", method, host + path, res.error()); return WebResult{WebResult::Code::LibError, "Null response", ""}; } - httplib::Response response = result.value(); - - if (response.status >= 400) { + if (res->status_code >= hydro::Status::BadRequest) { LOG_ERROR(WebService, "{} to {} returned error status code: {}", method, host + path, - response.status); - return WebResult{WebResult::Code::HttpError, std::to_string(response.status), ""}; + std::to_underlying(res->status_code)); + return WebResult{WebResult::Code::HttpError, std::to_string(std::to_underlying(res->status_code)), ""}; } - auto content_type = response.headers.find("content-type"); + auto content_type = res->headers.get("content-type"); - if (content_type == response.headers.end()) { + if (!content_type) { LOG_ERROR(WebService, "{} to {} returned no content", method, host + path); return WebResult{WebResult::Code::WrongContent, "", ""}; } - if (content_type->second.find(accept) == std::string::npos) { + if (content_type->find(accept) == std::string::npos) { LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path, - content_type->second); + content_type.value()); return WebResult{WebResult::Code::WrongContent, "Wrong content", ""}; } - return WebResult{WebResult::Code::Success, "", response.body}; + return WebResult{WebResult::Code::Success, "", res->body}; } // Retrieve a new JWT from given username and token @@ -161,7 +142,6 @@ struct Client::Impl { std::string username; std::string token; std::string jwt; - std::unique_ptr cli; struct JWTCache { std::mutex mutex; @@ -199,7 +179,7 @@ WebResult Client::GetImage(const std::string& path, bool allow_anonymous) { } WebResult Client::GetExternalJWT(const std::string& audience) { - return impl->GenericRequest("POST", fmt::format("/jwt/external/{}", audience), "", false, + return impl->GenericRequest("POST", std::format("/jwt/external/{}", audience), "", false, "text/html"); } diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 6d7c25e15b..4f812da6fd 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -242,7 +242,6 @@ add_executable(yuzu configuration/addon/mod_select_dialog.h configuration/addon/mod_select_dialog.cpp configuration/addon/mod_select_dialog.ui render/performance_overlay.h render/performance_overlay.cpp render/performance_overlay.ui - updater/update_dialog.h updater/update_dialog.cpp updater/update_dialog.ui game/common.h game/carousel.h game/carousel.cpp ) @@ -274,6 +273,9 @@ file(GLOB_RECURSE THEMES ${PROJECT_SOURCE_DIR}/dist/qt_themes/*) if (ENABLE_UPDATE_CHECKER) target_compile_definitions(yuzu PUBLIC ENABLE_UPDATE_CHECKER) + target_sources(yuzu PRIVATE + updater/update_dialog.h updater/update_dialog.cpp updater/update_dialog.ui) + target_link_libraries(yuzu PRIVATE QuaZip::QuaZip) endif() if (ENABLE_QT_TRANSLATION) @@ -409,7 +411,7 @@ elseif(WIN32) endif() endif() -target_link_libraries(yuzu PRIVATE nlohmann_json::nlohmann_json) +target_link_libraries(yuzu PRIVATE hydro::carbon jacinth::jacinth) target_link_libraries(yuzu PRIVATE common core input_common frontend_common network video_core qt_common) target_link_libraries(yuzu PRIVATE Boost::headers Qt6::Widgets Qt6::Charts Qt6::Concurrent) target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) @@ -465,3 +467,22 @@ endif() target_link_libraries(yuzu PRIVATE OpenSSL::SSL OpenSSL::Crypto SDL3::SDL3) create_target_directory_groups(yuzu) + +# Small updater shim. +if (WIN32 AND ENABLE_UPDATE_CHECKER) + add_executable(updater updater/updater.cpp) + set_target_properties(updater PROPERTIES OUTPUT_NAME "eden-updater") + target_link_libraries(updater PRIVATE Boost::process Boost::asio ws2_32) + + # need to link to stdc++exp (on glibc++ only) for std::print on mingw + if (MINGW) + include(CheckCXXSymbolExists) + + check_cxx_symbol_exists(__GLIBCXX__ "version" HAS_LIBSTDCXX) + + if(HAS_LIBSTDCXX) + message(STATUS "MinGW libstdc++ needs stdc++exp") + target_link_libraries(updater PUBLIC stdc++exp) + endif() + endif() +endif() diff --git a/src/yuzu/applets/qt_amiibo_settings.cpp b/src/yuzu/applets/qt_amiibo_settings.cpp index 932f306bc3..7dadabe5d1 100644 --- a/src/yuzu/applets/qt_amiibo_settings.cpp +++ b/src/yuzu/applets/qt_amiibo_settings.cpp @@ -1,28 +1,37 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include -#include -#include -#include - -#include "common/assert.h" #include "common/string_util.h" #include "core/hle/service/nfc/common/device.h" -#include "core/hle/service/nfp/nfp_result.h" #include "input_common/drivers/virtual_amiibo.h" #include "input_common/main.h" #include "ui_qt_amiibo_settings.h" #include "web_service/web_result.h" + #ifdef ENABLE_WEB_SERVICE #include "web_service/web_backend.h" #endif + #include "yuzu/applets/qt_amiibo_settings.h" #include "yuzu/main_window.h" +import jacinth; + +struct Amiibo { + std::string amiiboSeries; + std::string name; + std::string image; + std::string type; +}; + +// TODO(crueter): get rid of this +struct AmiiboData { + Amiibo amiibo; +}; + QtAmiiboSettingsDialog::QtAmiiboSettingsDialog(QWidget* parent, Core::Frontend::CabinetParameters parameters_, InputCommon::InputSubsystem* input_subsystem_, @@ -84,9 +93,9 @@ void QtAmiiboSettingsDialog::LoadAmiiboInfo() { } const auto amiibo_id = - fmt::format("{:04x}{:02x}{:02x}{:04x}{:02x}02", Common::swap16(model_info.character_id), - model_info.character_variant, model_info.amiibo_type, model_info.model_number, - model_info.series); + std::format("{:04x}{:02x}{:02x}{:04x}{:02x}02", Common::swap16(model_info.character_id), + model_info.character_variant, u8(model_info.amiibo_type), model_info.model_number, + u8(model_info.series)); LOG_DEBUG(Frontend, "Loading amiibo id {}", amiibo_id); // Note: This function is not being used until we host the images on our server @@ -100,7 +109,7 @@ void QtAmiiboSettingsDialog::LoadAmiiboApiInfo(std::string_view amiibo_id) { // TODO: Host this data on our website WebService::Client client{"https://amiiboapi.com", {}, {}}; WebService::Client image_client{"https://raw.githubusercontent.com", {}, {}}; - const auto url_path = fmt::format("/api/amiibo/?id={}", amiibo_id); + const auto url_path = std::format("/api/amiibo/?id={}", amiibo_id); const auto amiibo_json = client.GetJson(url_path, true).returned_data; if (amiibo_json.empty()) { @@ -109,26 +118,24 @@ void QtAmiiboSettingsDialog::LoadAmiiboApiInfo(std::string_view amiibo_id) { return; } - std::string amiibo_series{}; - std::string amiibo_name{}; - std::string amiibo_image_url{}; - std::string amiibo_type{}; + auto amiiboData = jacinth::doc::try_read(amiibo_json); - const auto parsed_amiibo_json_json = nlohmann::json::parse(amiibo_json).at("amiibo"); - parsed_amiibo_json_json.at("amiiboSeries").get_to(amiibo_series); - parsed_amiibo_json_json.at("name").get_to(amiibo_name); - parsed_amiibo_json_json.at("image").get_to(amiibo_image_url); - parsed_amiibo_json_json.at("type").get_to(amiibo_type); + if (!amiiboData) { + LOG_WARNING(Frontend, "Failed to parse amiibo data"); + return; + } - ui->amiiboSeriesValue->setText(QString::fromStdString(amiibo_series)); - ui->amiiboNameValue->setText(QString::fromStdString(amiibo_name)); - ui->amiiboTypeValue->setText(QString::fromStdString(amiibo_type)); + auto amiibo = amiiboData->amiibo; - if (amiibo_image_url.size() < 34) { + ui->amiiboSeriesValue->setText(QString::fromStdString(amiibo.amiiboSeries)); + ui->amiiboNameValue->setText(QString::fromStdString(amiibo.name)); + ui->amiiboTypeValue->setText(QString::fromStdString(amiibo.type)); + + if (amiibo.image.size() < 34) { ui->amiiboImageLabel->setVisible(false); } - const auto image_url_path = amiibo_image_url.substr(34, amiibo_image_url.size() - 34); + const auto image_url_path = amiibo.image.substr(34, amiibo.image.size() - 34); const auto image_data = image_client.GetImage(image_url_path, true).returned_data; if (image_data.empty()) { @@ -225,7 +232,7 @@ void QtAmiiboSettingsDialog::SetGameDataName(u32 application_area_id) { } } - const auto application_area_string = fmt::format("{:016x}", application_area_id); + const auto application_area_string = std::format("{:016x}", application_area_id); ui->gameIdValue->setText(QString::fromStdString(application_area_string)); } diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp index ccebaf2513..5865fd4a38 100644 --- a/src/yuzu/bootmanager.cpp +++ b/src/yuzu/bootmanager.cpp @@ -6,6 +6,7 @@ #include #include #include +#include "hid_core/hid_core.h" #ifdef HAS_OPENGL #include @@ -155,6 +156,9 @@ void GRenderWindow::Exit() { } GRenderWindow::~GRenderWindow() { + if (QtCommon::system) + QtCommon::system->HIDCore().UnloadInputDevices(); + input_subsystem->Shutdown(); } diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 17aae46868..838a2f6c6f 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -146,10 +146,17 @@ int main(int argc, char* argv[]) { // generating shaders setlocale(LC_ALL, "C"); - MainWindow main_window{has_broken_vulkan}; - // After settings have been loaded by GMainWindow, apply the filter - main_window.show(); + int result = 0; + { + MainWindow main_window{has_broken_vulkan}; + // After settings have been loaded by MainWindow, apply the filter + main_window.show(); - app.connect(&app, &QGuiApplication::applicationStateChanged, &main_window, &MainWindow::OnAppFocusStateChanged); - return app.exec(); + app.connect(&app, &QGuiApplication::applicationStateChanged, &main_window, &MainWindow::OnAppFocusStateChanged); + result = app.exec(); + } + + // forcefully tear down core before static destructors run + QtCommon::Shutdown(); + return result; } diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp index d9e752896a..c3a719c40b 100644 --- a/src/yuzu/multiplayer/lobby.cpp +++ b/src/yuzu/multiplayer/lobby.cpp @@ -272,7 +272,7 @@ void Lobby::OnRefreshLobby() { QString::fromStdString(room.information.preferred_game.name), smdh_icon); auto row = QList({ first_item, - new LobbyItemName(room.has_password, QString::fromStdString(room.information.name)), + new LobbyItemName(room.hasPassword, QString::fromStdString(room.information.name)), new LobbyItemMemberList(members, room.information.member_slots), new LobbyItemHost(QString::fromStdString(room.information.host_username), QString::fromStdString(room.ip), room.information.port, diff --git a/src/yuzu/updater/update_dialog.cpp b/src/yuzu/updater/update_dialog.cpp index f40fced357..21de9af2e2 100644 --- a/src/yuzu/updater/update_dialog.cpp +++ b/src/yuzu/updater/update_dialog.cpp @@ -1,32 +1,64 @@ // SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +#include +#include +#include + #include #include #include -#include + +#include +#include +#include + +#include "common/fs/path_util.h" #include "common/logging.h" #include "qt_common/abstract/frontend.h" #include "qt_common/abstract/progress.h" +#include "qt_common/util/compress.h" #include "ui_update_dialog.h" #include "update_dialog.h" -#include "common/httplib.h" +#include "common/scm_rev.h" #ifdef YUZU_BUNDLED_OPENSSL #include #endif +#include + #include +#include #undef GetSaveFileName +import hydrocarbon; + +static std::string sha256_hash(EVP_MD_CTX* ctx, const std::string& input) { + char output[65]; + unsigned char hash[EVP_MAX_MD_SIZE]; + unsigned int hash_len; + + EVP_DigestInit_ex(ctx, EVP_sha256(), NULL); + EVP_DigestUpdate(ctx, input.c_str(), input.size()); + EVP_DigestFinal_ex(ctx, hash, &hash_len); + + for (unsigned int i = 0; i < hash_len; i++) { + sprintf(output + (i * 2), "%02x", hash[i]); + } + output[hash_len * 2] = '\0'; + + return std::string(output); +} + UpdateDialog::UpdateDialog(const Common::Net::Release& release, QWidget* parent) : QDialog(parent), ui(new Ui::UpdateDialog) { ui->setupUi(this); ui->version->setText( - tr("%1 is available for download.").arg(QString::fromStdString(release.title))); + tr("%1 is available for download.").arg(QString::fromStdString(release.name))); ui->url->setText( tr("View on Forgejo").arg(QString::fromStdString(release.html_url))); @@ -38,7 +70,7 @@ UpdateDialog::UpdateDialog(const Common::Net::Release& release, QWidget* parent) ui->body->setMarkdown(QString::fromStdString(text)); // TODO(crueter): Find a way to set default - const auto assets = release.GetPlatformAssets(); + const auto assets = Common::Net::GetPlatformAssets(release); if (assets.empty()) { ui->groupBox->setHidden(true); @@ -52,7 +84,7 @@ UpdateDialog::UpdateDialog(const Common::Net::Release& release, QWidget* parent) connect(this, &QDialog::accepted, this, &UpdateDialog::Download); } else { u32 i = 0; - for (const Common::Net::Asset& a : assets) { + for (const Common::Net::NamedAsset& a : assets) { QRadioButton* r = new QRadioButton(tr(a.name.c_str()), this); connect(r, &QRadioButton::toggled, this, [a, this](bool checked) { if (checked) @@ -74,107 +106,335 @@ UpdateDialog::~UpdateDialog() { delete ui; } -// TODO: migrate to a net.cpp wrapper +// TODO: migrate progress callback download to Common::Net? +// TODO: migrate to QtCommon void UpdateDialog::Download() { - const auto filename = QtCommon::Frontend::GetSaveFileName( - tr("New Version Location"), - qApp->applicationDirPath() % QStringLiteral("/") % QString::fromStdString(m_asset.filename), - tr("All Files (*.*)")); + namespace fs = std::filesystem; + const auto appDir = fs::path{QCoreApplication::applicationDirPath().toStdString()}.make_preferred(); - if (filename.isEmpty()) - return; + const auto parentDir = appDir.parent_path(); + const auto appDirName = appDir.filename().string(); + const auto oldDir = parentDir / std::format("{}_{}", appDirName, std::string{Common::g_build_version}); + const auto stagingDir = parentDir / std::format("{}_{}", appDirName, "staging"); - QSaveFile file(filename); - if (!file.open(QIODevice::Truncate | QIODevice::WriteOnly)) { - LOG_WARNING(Frontend, "Could not open file {}", filename.toStdString()); - QtCommon::Frontend::Critical(tr("Failed to save file"), - tr("Could not open file %1 for writing.").arg(filename)); - return; + std::error_code ec; + auto tmpDir = fs::temp_directory_path(ec); + + if (ec) { + // tmp dir creation failed, just use cache + LOG_WARNING(Frontend, "Failed to create temporary directory: {}", ec.message()); + tmpDir = Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "update" / "download"; + } else { + tmpDir = tmpDir / "eden" / "update" / "download"; } - // TODO(crueter): Move to net.cpp - constexpr std::size_t timeout_seconds = 15; + fs::remove_all(tmpDir); + fs::create_directories(tmpDir); - std::unique_ptr client = std::make_unique(m_asset.url); - client->set_connection_timeout(timeout_seconds); - client->set_read_timeout(timeout_seconds); - client->set_write_timeout(timeout_seconds); - -#ifdef YUZU_BUNDLED_OPENSSL - client->load_ca_cert_store(kCert, sizeof(kCert)); -#endif - - if (client == nullptr) { - LOG_ERROR(Frontend, "Invalid URL {}{}", m_asset.url, m_asset.path); - return; - } + const auto filename = m_asset.asset.name; + const auto absFilename = tmpDir / filename; + // Progress dialog. auto progress = QtCommon::Frontend::newProgressDialog(tr("Downloading..."), tr("Cancel"), 0, 100); progress->show(); QGuiApplication::processEvents(); - // Progress dialog. - auto progress_callback = [&](size_t processed_size, size_t total_size) { + // NOTE: This loads the entire file content into memory + // Since this is always <80MB this is actually fine for now, but is a target for future + // optimization (e.g. incr. read from disk) if assets are ever >200MB. + // Not that it matters because to be able to run games you need >1GB lol. + std::string tmp_data; + const auto download_res = + downloadFile(tmp_data, m_asset.asset.browser_download_url, absFilename, progress.get()); + + if (!download_res) { + progress->close(); + return; + } + + // TODO(crueter): Test + if (m_asset.asset.digest) { + progress->setLabelText(tr("Verifying...")); + progress->setValue(0); + progress->setMaximum(0); + + auto ctx = EVP_MD_CTX_new(); + auto actual = std::format("sha256:{}", sha256_hash(ctx, tmp_data)); + auto expected = m_asset.asset.digest.value(); + + EVP_MD_CTX_free(ctx); + + // TODO: auto-retry + if (actual != expected) { + LOG_ERROR(Frontend, "Hash mismatch, expected {}, got {}", expected, actual); + QtCommon::Frontend::Critical( + tr("Failed to download file"), + tr("File has invalid hash.\nGot:%1\nExpected: %2\nPlease re-launch Eden and try " + "again.") + .arg(QString::fromStdString(expected), QString::fromStdString(actual))); + + return; + } + } + + // Download is complete. + if (filename.ends_with(".dmg") || filename.ends_with(".exe") || filename.ends_with(".tar.gz")) { + progress->close(); + + // dmg, exe, tarball will just open it and let the OS handle the rest. + auto localUrl = QUrl::fromLocalFile(QString::fromStdString(absFilename.string())); + QDesktopServices::openUrl(localUrl); + } else if (filename.ends_with(".zip")) { + // This path should only ever trigger on Windows. + // If it did somehow anyways, open it and let the OS handle it. +#ifndef _WIN32 + progress->close(); + + auto localUrl = QUrl::fromLocalFile(QString::fromStdString(absFilename.string())); + QDesktopServices::openUrl(localUrl); + return; +#endif + + // request the user to keep the old version or not... + auto button = QtCommon::Frontend::Question( + tr("Download Complete"), + tr("Would you like to keep the old version of Eden in a separate folder (%1)?") + .arg(QString::fromStdString(oldDir.filename().string()))); + + const auto qstaging = QString::fromStdString(stagingDir.string()); + + // ...then extract to a staging dir... + std::error_code st_ec; + fs::create_directories(stagingDir, st_ec); + + if (st_ec) { + LOG_ERROR(Frontend, "Could not create staging directory {}: {}", stagingDir.string(), st_ec.message()); + + QtCommon::Frontend::Critical( + tr("Failed to create staging directory"), + tr("Could not create the staging directory %1.\n\nOS returned error: %2\n\nEnsure " + "you have write permissions in the current app directory, or try manually " + "updating.").arg(qstaging, QString::fromStdString(st_ec.message()))); + + return; + } + + auto progress_callback = [&](size_t total_size, size_t processed_size) { + QGuiApplication::processEvents(); + + if (total_size > 0) + progress->setValue(int((processed_size * 100) / total_size)); + return !progress->wasCanceled(); + }; + + progress->setLabelText(tr("Extracting...")); + progress->setValue(0); + progress->setMaximum(100); + progress->show(); + + // WARNING + // close() sets wasCanceled to true, this is not documented but it WILL mess with callback + // functions like this one! + const auto extracted = QtCommon::Compress::extractDir(QString::fromStdString(absFilename.string()), + qstaging, progress_callback); + + if (progress->wasCanceled()) { + progress->close(); + + QtCommon::Frontend::Information( + tr("Extraction cancelled"), + tr("Extraction was cancelled by the user.")); + + fs::remove_all(stagingDir, ec); + return; + } + + progress->close(); + + bool found = false; + for (const auto &file : std::as_const(extracted)) { + if (file.contains(QStringLiteral("eden.exe"))) { + found = true; + break; + } + } + + if (!found) { + LOG_ERROR(Frontend, "Extracted no valid files from {}, staging={}", absFilename.string(), stagingDir.string()); + + QtCommon::Frontend::Critical(tr("Failed to extract downloaded file"), + tr("No valid files were successfully extracted from %1.") + .arg(QString::fromStdString(absFilename.string()))); + + return; + } + + // ...then run a small updater shim + const auto updater = appDir / "eden-updater.exe"; + const auto cached_updater = + Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "eden-updater.exe"; + + const auto qcachedUpdater = QString::fromStdString(cached_updater.string()); + const auto qupdater = QString::fromStdString(updater.string()); + const auto qappDir = QString::fromStdString(appDir.string()); + const auto qoldDir = QString::fromStdString(oldDir.string()); + + std::error_code copy_ec; + // ignore remove failures + fs::remove_all(cached_updater, copy_ec); + fs::copy_file(updater.native(), cached_updater.native(), copy_ec); + + if (copy_ec) { + LOG_ERROR(Frontend, "Failed to copy updater executable: {}", copy_ec.message()); + + QtCommon::Frontend::Critical( + tr("Failed to run updater"), + tr("Could not copy the updater executable (%1) to the local cache directory " + "(%2).\nOS returned error: %3\n\nThe new release is extracted in %4.") + .arg(qupdater, + qcachedUpdater, + QString::fromStdString(copy_ec.message()), + qstaging)); + + return; + } + + const auto pid = QString::number(QCoreApplication::applicationPid()); + const auto keep_old = button == QtCommon::Frontend::Yes ? QStringLiteral("1") : QStringLiteral("0"); + const auto pwd = QString::fromStdString(cached_updater.parent_path().string()); + + // clang-format off + auto pres = QProcess::startDetached( + qcachedUpdater, + {pid, qappDir, qoldDir, qstaging, keep_old}, + pwd); + // clang-format on + + if (!pres) { + LOG_ERROR(Frontend, "Failed to start updater executable"); + + QtCommon::Frontend::Critical(tr("Failed to run updater"), + tr("Could not run the updater executable (%1)\n\nThe new " + "release is extracted in %4.") + .arg(qupdater, qstaging)); + + return; + } + + QCoreApplication::exit(0); + } +} + +bool UpdateDialog::downloadFile( + std::string& write_data, const std::string &url, std::optional path, + QtCommon::Frontend::QtProgressDialog *progress) { + std::function on_data; + + std::ofstream file; + std::string filename; + + if (path) { + LOG_INFO(Frontend, "Downloading {} to {}", url, path->string()); + + filename = path->filename().string(); + file = std::ofstream(path->string(), std::ios::out | std::ios::binary); + + // write file in chunks + on_data = [&write_data, filename, &file](std::string_view t_data) { + write_data += t_data; + + try { + file.write(t_data.data(), t_data.size()); + } catch (std::exception& e) { + LOG_ERROR(Frontend, "Could not write {} bytes to file {}, error{}=", t_data.size(), + filename, e.what()); + QtCommon::Frontend::Critical( + tr("Failed to save file"), + tr("Could not write to file %1.").arg(QString::fromStdString(filename))); + return false; + } + + return true; + }; + } else { + LOG_INFO(Frontend, "Downloading {}", url); + + on_data = [&write_data](std::string_view t_data) { + write_data += t_data; + return true; + }; + } + + auto callback = [&progress, &on_data](const hydro::DownloadState& state) { + if (!on_data(state.buffer)) + return false; + QGuiApplication::processEvents(); - progress->setValue(static_cast((processed_size * 100) / total_size)); + if (state.content_length > 0) + progress->setValue(int((state.bytes_received * 100) / state.content_length)); return !progress->wasCanceled(); }; - // Write file in chunks. - auto content_receiver = [&file, filename](const char* t_data, size_t data_length) -> bool { - if (file.write(t_data, data_length) == -1) { - LOG_WARNING(Frontend, "Could not write {} bytes to file {}", data_length, - filename.toStdString()); - QtCommon::Frontend::Critical(tr("Failed to save file"), - tr("Could not write to file %1.").arg(filename)); - return false; + hydro::RequestOptions opts; +#ifdef YUZU_BUNDLED_OPENSSL + opts.connect.cert = std::string{kCert}; +#endif + + opts.connect.connectionTimeout = std::chrono::milliseconds{10000}; + opts.callback = callback; + + auto res = hydro::get(url, opts); + + if (progress->wasCanceled()) { + LOG_ERROR(Frontend, "URL {} download cancelled by user", url); + + if (path) { + try { + file.flush(); + file.close(); + + std::error_code ec; + std::filesystem::remove_all(path.value()); + } catch (...) { + } } - return true; - }; + QtCommon::Frontend::Information( + tr("Download cancelled"), + tr("Download was cancelled by the user.")); + return false; + } else if (!res) { + LOG_ERROR(Frontend, "URL {} returned error {}", url, res.error()); - // Now send off request - auto result = client->Get(m_asset.path, content_receiver, progress_callback); - progress->close(); + QtCommon::Frontend::Critical( + tr("Failed to download file"), + tr("URL %1 returned error %2.") + .arg(QString::fromStdString(url), QString::fromStdString(std::format("{}", res.error())))); + return false; + } else if (res->status_code < hydro::Status::Ok || res->status_code >= hydro::Status::MultipleChoices) { + LOG_ERROR(Frontend, "URL {} returned status {}", url, std::to_underlying(res->status_code)); - // commit to file - if (!file.commit()) { - LOG_WARNING(Frontend, "Could not commit to file {}", filename.toStdString()); - QtCommon::Frontend::Critical(tr("Failed to save file"), - tr("Could not commit to file %1.").arg(filename)); + QtCommon::Frontend::Critical( + tr("Failed to download file"), + tr("URL %1 returned status code %2.") + .arg(QString::fromStdString(url), QString::number(std::to_underlying(res->status_code)))); + return false; } - if (!result) { - LOG_ERROR(Frontend, "GET to {}{} returned null", m_asset.url, m_asset.path); - return; + if (path) { + try { + file.flush(); + file.close(); + } catch (const std::exception& e) { + LOG_WARNING(Frontend, "Could not commit to file {}, OS returned error {}", filename, e.what()); + + QtCommon::Frontend::Critical( + tr("Failed to save file"), + tr("Could not commit to file %1.").arg(QString::fromStdString(filename))); + } } - const auto& response = result.value(); - if (response.status >= 400) { - LOG_ERROR(Frontend, "GET to {}{} returned error status code: {}", m_asset.url, m_asset.path, - response.status); - QtCommon::Frontend::Critical(tr("Failed to download file"), - tr("Could not download from %1%2\nError code: %3") - .arg(QString::fromStdString(m_asset.url), - QString::fromStdString(m_asset.path), - QString::number(response.status))); - return; - } - if (!response.has_header("content-type")) { - LOG_ERROR(Frontend, "GET to {}{} returned no content", m_asset.url, m_asset.path); - return; - } - - // Download is complete. User may choose to open in the file manager. - auto button = - QtCommon::Frontend::Question(tr("Download Complete"), - tr("Successfully downloaded %1. Would you like to open it?") - .arg(QString::fromStdString(m_asset.filename)), - QtCommon::Frontend::Yes | QtCommon::Frontend::No); - - if (button == QtCommon::Frontend::Yes) { - QDesktopServices::openUrl(QUrl::fromLocalFile(filename)); - } + return true; } diff --git a/src/yuzu/updater/update_dialog.h b/src/yuzu/updater/update_dialog.h index e821ef660d..354359c4dd 100644 --- a/src/yuzu/updater/update_dialog.h +++ b/src/yuzu/updater/update_dialog.h @@ -3,7 +3,11 @@ #pragma once +#include +#include + #include +#include "qt_common/abstract/progress.h" #include "common/net/net.h" class QRadioButton; @@ -24,5 +28,17 @@ private slots: private: Ui::UpdateDialog* ui; QList m_buttons; - Common::Net::Asset m_asset; + Common::Net::NamedAsset m_asset; + + /** + * @brief downloadFile Download a file, write to disk, and report progress + * @param data Write the data to this string reference + * @param url URL to download from + * @param path Path to write to (set empty to not write at all) + * @param dialog Progress dialog + * @return True on success + */ + bool downloadFile( + std::string& data, const std::string &url, std::optional path, + QtCommon::Frontend::QtProgressDialog *dialog); }; diff --git a/src/yuzu/updater/updater.cpp b/src/yuzu/updater/updater.cpp new file mode 100644 index 0000000000..dfdb878a4d --- /dev/null +++ b/src/yuzu/updater/updater.cpp @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// This process only runs on Windows. +// Technically you could port it to other platforms pretty easily. + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +static bool waitForPid(uint64_t pid) { + HANDLE h = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, DWORD(pid)); + if (!h) { + // the eden process already exited, or the pid is invalid in some way. + // Either way just go forth and pray it's actually been closed. + return GetLastError() == ERROR_INVALID_PARAMETER || GetLastError() == ERROR_FILE_NOT_FOUND; + } + + // wait 1m for the process to exit + DWORD result = WaitForSingleObject(h, 60'000); + CloseHandle(h); + return result == WAIT_OBJECT_0; +} + +// TODO: On error, launch main exe with an arg that prints a warning. +// TODO(crueter): unicode handling??? kys +int main(int argc, char** argv) { + namespace fs = std::filesystem; + std::vector args{argv + 1, argv + argc}; + + if (args.size() < 5) { + std::println(stderr, "Usage: {} [pid] [app dir] [old app dir] [staging app dir] [keep old]", argv[0]); + return 1; + } + + // pid + uint64_t pid = 0; + try { + pid = std::stoull(args[0]); + } catch (...) { + std::println(stderr, "Invalid PID string."); + return 2; + } + + // "Current" app dir + const auto app_dir = args[1]; + + // Old directory (move app dir to this if user asked) + const auto old_dir = args[2]; + + // Staging dir (contains new executable) + const auto staging_dir = args[3]; + + // Whether or not to keep the old dir (if arg is 1) + const auto keep_old_str = args[4]; + const auto keep_old = keep_old_str == "1"; + + // Wait for old PID. + if (!waitForPid(pid)) { + std::println(stderr, "Old process did not exit cleanly after 60 seconds."); + return 3; + } + + // Move an existing user directory to the staging dir + const auto old_user = fs::path(app_dir) / "user"; + if (fs::exists(old_user)) { + std::error_code ec; + + const auto new_user = fs::path(staging_dir) / "user"; + fs::rename(old_user, new_user, ec); + + if (ec) { + std::println(stderr, "Failed to copy old user directory: {}", ec.message()); + return 4; + } + } + + // Remove or rename app dir + if (keep_old) { + std::error_code ec; + + // remove existing old dir if present + fs::remove_all(old_dir, ec); + fs::rename(app_dir, old_dir, ec); + + if (ec) { + std::println(stderr, "Failed to create old directory: {}", ec.message()); + return 5; + } + } else { + std::error_code ec; + fs::remove_all(app_dir, ec); + + if (ec) { + std::println(stderr, "Failed to remove existing app directory: {}", ec.message()); + return 6; + } + } + + // Rename staging dir + { + std::error_code ec; + fs::rename(staging_dir, app_dir, ec); + + if (ec) { + std::println(stderr, "Failed to move staging directory: {}", ec.message()); + return 7; + } + } + + // spawn new proc and leave + // TODO(crueter): configurable exe name? + namespace bp = boost::process::v2; + const auto exe = fs::path{app_dir} / "eden.exe"; + + if (!fs::exists(exe)) { + std::println(stderr, "eden.exe does not exist in the new directory."); + return 8; + } + + try { + boost::asio::io_context ctx; + bp::process child(ctx, exe.string(), {}); + child.detach(); + } catch (std::exception &e) { + std::println(stderr, "Failed to spawn child process: {}", e.what()); + return 9; + } + + return 0; +} diff --git a/src/yuzu_cmd/CMakeLists.txt b/src/yuzu_cmd/CMakeLists.txt index 17a1545167..7b37b4ef82 100644 --- a/src/yuzu_cmd/CMakeLists.txt +++ b/src/yuzu_cmd/CMakeLists.txt @@ -48,6 +48,7 @@ create_resource("../../dist/eden.bmp" "yuzu_cmd/yuzu_icon.h" "yuzu_icon") target_include_directories(yuzu-cmd PRIVATE ${RESOURCES_DIR}) target_link_libraries(yuzu-cmd PRIVATE SDL3::SDL3) +target_link_libraries(yuzu-cmd PRIVATE OpenSSL::SSL OpenSSL::Crypto) if(UNIX AND NOT APPLE) install(TARGETS yuzu-cmd) diff --git a/tools/cpm/cmake/ScriptUtils.cmake b/tools/cpm/cmake/ScriptUtils.cmake index 183deae14d..1057caeabc 100755 --- a/tools/cpm/cmake/ScriptUtils.cmake +++ b/tools/cpm/cmake/ScriptUtils.cmake @@ -264,6 +264,7 @@ function(get_latest_tag out) # same as above string(REGEX REPLACE "[^0-9.-]" "" numeric_tag "${tag_name}") string(REGEX REPLACE "-$|^-" "" numeric_tag "${numeric_tag}") + string(REGEX REPLACE "_" "." numeric_tag "${numeric_tag}") if (numeric_tag VERSION_GREATER_EQUAL greatest_version_numeric) set(greatest_version_numeric ${numeric_tag}) diff --git a/tools/cpm/cmake/package/add.cmake b/tools/cpm/cmake/package/add.cmake index 02c4e1d900..1f0a97e3ea 100644 --- a/tools/cpm/cmake/package/add.cmake +++ b/tools/cpm/cmake/package/add.cmake @@ -71,12 +71,12 @@ if (CI) add(ci true) if (DEFINED DISABLED_PLATFORMS) - list_to_array(${DISABLED_PLATFORMS} json_disabled) + list_to_array("${DISABLED_PLATFORMS}" json_disabled) string(JSON object SET "${object}" disabled_platforms "${json_disabled}") endif() else() if (DEFINED OPTIONS) - list_to_array(${OPTIONS} json_options) + list_to_array("${OPTIONS}" json_options) string(JSON object SET "${object}" options "${json_options}") endif()