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()