[common] nlohmann -> jacinth, httplib -> hydrocarbon, rework auto-updater UX (#4412)

Replaces nlohmann_json with the much faster and much less bloated [jacinth](https://github.com/crueter/jacinth), a JSON library I'm working on that essentially aims to be a lightweight wrapper around `yyjson` with extra C++23 sauce. It's not as fast as yyjson or glaze, but has a very nice API that allows for compile-time struct serialization *and* traditional nlohmann-like OOPery (plus to_json and from_json). Jacinth also exclusively exposes itself as a C++20 module, so consider that migration started

httplib has been replaced with the **much** smaller [Hydrocarbon](https://github.com/crueter/hydrocarbon) (which is <1500 LoC compared to 24k). It's a much cleaner API than httplib and doesn't depend on curl or anything else (besides openSSL). Insert joke about me NIHing everything here.

Auto updater has been reworked. Windows setup executables will have a system.txt that indicates this is a system install; that will just execute the exe directly (same as .dmg on macOS)

Zip files will instead:
- Ask the user if they want to keep the old one around in a different directory.
- Extracts the zip into a staging directory
- Close while detach-launching a thin helper shim executable that:
    - Removes or renames the old application directory in accordance with the user's choice
    - Renames the staging directory to the current directory name
    - Launches and detaches the *new* eden.exe in the background.

So to the user it looks like: download, extract, new Eden launches, yay.

Alongside all of these changes, I had to make a bunch of CMake changes to bring this project up to be modules-compatible. Consider it a bonus.

Assisted-by: Local Ornith-1.5-35B (fixing a few nonsensical build errors)

Signed-off-by: crueter <crueter@eden-emu.dev>

- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4412
Reviewed-by: lizzie <lizzie@eden-emu.dev>
This commit is contained in:
crueter committed 2026-11-08 20:19:37 +01:00
1 parent 8e81d84781
commit 83fde0738c
62 files changed
+1550 -1184

No files matched your search

-62
View File
@@ -1,62 +0,0 @@
From 436fc1978c78edd085d99b33275b24be0ac96aa0 Mon Sep 17 00:00:00 2001
From: crueter <crueter@eden-emu.dev>
Date: Sun, 1 Feb 2026 16:21:10 -0500
Subject: [PATCH] Fix build on MinGW
MinGW doesn't define GetAddrInfoExCancel.
Signed-off-by: crueter <crueter@eden-emu.dev>
---
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<DWORD>(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
+14 -11
View File
@@ -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)
+1 -3
View File
@@ -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
-34
View File
@@ -1,34 +0,0 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2022 Andrea Pappacoda <andrea@pappacoda.it>
#
# 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()
+22 -31
View File
@@ -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",
+15 -22
View File
@@ -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 `<GENERATOR>`)
>- 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 `<GENERATOR>`:
- MSYS2: `MSYS Makefiles`
- MSVC: `Ninja` (preferred) or `Visual Studio 17 2022`
- macOS: `Ninja` (preferred) or `Xcode`
- Others: `Ninja` (preferred) or `UNIX Makefiles`
Available `<BUILD_TYPE>`:
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 "<GENERATOR>" -DCMAKE_BUILD_TYPE=<BUILD_TYPE> -DYUZU_TESTS=OFF
cmake -S . -B build -GNinja -DCMAKE_BUILD_TYPE=<BUILD_TYPE>
```
If you are on Windows and prefer to use Clang:
```sh
cmake -S . -B build -G "<GENERATOR>" -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++"
```
</details>
@@ -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 `<GENERATOR>`, 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.
+15 -13
View File
@@ -1,17 +1,19 @@
# Caveats
<!-- TOC -->
- [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)
<!-- /TOC -->
## 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"
+104 -115
View File
@@ -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:
<summary>Arch Linux</summary>
```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.
</details>
@@ -166,11 +161,11 @@ sudo pacman -Syu --needed base-devel boost catch2 cmake enet ffmpeg fmt git glsl
<summary>Ubuntu, Debian, Mint Linux</summary>
```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.
</details>
@@ -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.
</details>
@@ -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
```
</details>
@@ -221,11 +215,9 @@ apk add g++ git cmake make mesa-dev qt6-qtbase-dev qt6-qtbase-private-dev libqua
<summary>Void Linux</summary>
```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?
</details>
<details>
@@ -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
<summary>FreeBSD</summary>
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
<summary>DragonFlyBSD</summary>
```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
<details>
<summary>OpenIndiana</summary>
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
<details>
<summary>OmniOS</summary>
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
<details>
<summary>MSYS2</summary>
* 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
<summary>HaikuOS</summary>
```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).
+3 -2
View File
@@ -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`.
+7 -9
View File
@@ -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()
+57
View File
@@ -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()
+4 -16
View File
@@ -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()
endif()
+3 -3
View File
@@ -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
+54 -3
View File
@@ -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()
$<$<COMPILE_LANGUAGE:C,CXX>:-pipe>
# Disable RTTI (C++ only)
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>)
else()
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:/GR->)
endif()
if (ENABLE_WERROR)
@@ -169,9 +174,9 @@ else()
$<$<COMPILE_LANGUAGE:C,CXX>:-Wmissing-declarations>
$<$<COMPILE_LANGUAGE:C,CXX>:-Wshadow>
$<$<COMPILE_LANGUAGE:C,CXX>:-Wunused>
# Some compilers could particularly misbehave :)
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-error-all>)
endif()
add_compile_options(
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-attributes>
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-invalid-offsetof>
@@ -209,7 +214,10 @@ else()
add_compile_options(
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-array-bounds>
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-stringop-overread>
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-stringop-overflow>)
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-stringop-overflow>
# Particularly bugged on GCC 16
$<$<COMPILE_LANGUAGE:C,CXX>:-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)
+8 -12
View File
@@ -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")
}
}
@@ -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()
)
)
}
+1 -1
View File
@@ -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()
+6 -5
View File
@@ -1904,14 +1904,14 @@ JNIEXPORT jobject JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_checkForUpdate(
std::optional<Common::Net::Release> 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);
}
@@ -1,12 +1,13 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#define HAS_RESHADE 1
#include <array>
#include <memory>
#include <string>
#include <jni.h>
#include <nlohmann/json.hpp>
#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<AndroidConfig> 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<int>(uniform.kind);
out["uiType"] = static_cast<int>(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<f32, 4> 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());
}
+4 -1
View File
@@ -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
}
+1
View File
@@ -9,6 +9,7 @@
#include <string>
#include <vector>
#include "audio_core/sink/sink.h"
#include "audio_core/sink/sink_details.h"
#ifdef HAVE_CUBEB
#include "audio_core/sink/cubeb_sink.h"
+9 -9
View File
@@ -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 "$<$<CXX_COMPILER_ID:Clang,GNU>:-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
"$<$<CXX_COMPILER_ID:Clang,GNU>:-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)
+2 -1
View File
@@ -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 <string>
#include <vector>
#include <jni.h>
#include "common/common_types.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
#include "common/android/id_cache.h"
@@ -387,7 +387,7 @@ std::vector<std::string> 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) + "|" +
+4 -6
View File
@@ -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 <array>
#include <functional>
#include <string>
#include <vector>
#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<Member> members;
};
+5 -2
View File
@@ -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<jstring>(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<jstring>(env->CallStaticObjectMethod(
+6 -2
View File
@@ -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() {
+7
View File
@@ -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.
*
-18
View File
@@ -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 <httplib.h>
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
+108 -195
View File
@@ -2,27 +2,35 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include <optional>
#include <utility>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <boost/algorithm/string/split.hpp>
#include <fmt/format.h>
#include "common/scm_rev.h"
#include "net.h"
#include "common/logging.h"
#include "common/httplib.h"
#ifdef YUZU_BUNDLED_OPENSSL
#include <openssl/cert.h>
#endif
#define QT_TR_NOOP(x) x
#ifdef _WIN32
#include <filesystem>
#include "common/fs/path_util.h"
#endif
import jacinth;
import hydrocarbon;
namespace Common::Net {
std::vector<Asset> Release::GetPlatformAssets() const {
// static constexpr auto DefaultTimeout = std::chrono::seconds{30};
static constexpr auto DefaultConnectTimeout = std::chrono::milliseconds{10000};
std::vector<NamedAsset> GetPlatformAssets(const Release& r) {
// TODO(crueter): Need better handling for this as a whole.
#ifdef NIGHTLY_BUILD
std::vector<std::string> result;
@@ -31,46 +39,48 @@ std::vector<Asset> Release::GetPlatformAssets() const {
return {};
const auto ref = result.at(1);
#else
const auto ref = tag;
const auto ref = r.tag_name;
#endif
std::vector<Asset> found_assets;
std::vector<NamedAsset> 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<std::string>& 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<std::string>& 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<Asset> 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<u64>(_mkgmtime(&tm));
#else
return static_cast<u64>(timegm(&tm));
#endif
}
std::optional<Release> 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<std::string>{}(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<std::string>{});
} 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<std::string> 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> 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> Release::ListFromJson(const nlohmann::json& json, const std::string& host,
const std::string& repo) {
if (!json.is_array())
return {};
std::vector<Release> 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> 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<std::string> MakeRequest(const std::string& url, const std::string& path) {
try {
constexpr std::size_t timeout_seconds = 15;
std::unique_ptr<httplib::Client> client = std::make_unique<httplib::Client>(url);
client->set_connection_timeout(timeout_seconds);
client->set_read_timeout(timeout_seconds);
client->set_write_timeout(timeout_seconds);
std::optional<std::string> 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<std::string> MakeRequest(const std::string_view url) {
return MakeRequest(std::string(url));
}
std::vector<Release> GetReleases() {
@@ -261,32 +136,70 @@ std::vector<Release> 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<Release> 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<Release>(body_str);
if (!ec) {
LOG_CRITICAL(Common, "Failed to parse latest release: {}", ec.error().message());
return std::nullopt;
}
return ec.value();
}
std::optional<std::string> 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<Release> GetReleasesFromJson(const std::string& body) {
auto ec = jacinth::doc::try_read<std::vector<Release>>(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
+23 -24
View File
@@ -6,43 +6,42 @@
#include <optional>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#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<std::string> 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<std::string> published_at;
std::vector<std::string> assets;
u64 id;
u64 published;
bool prerelease;
// Get the relevant list of assets for the current platform.
std::vector<Asset> GetPlatformAssets() const;
static std::optional<Release> FromJson(const nlohmann::json& json, const std::string &host, const std::string& repo);
static std::optional<Release> FromJson(const std::string_view& json, const std::string &host, const std::string& repo);
static std::vector<Release> ListFromJson(const nlohmann::json &json, const std::string &host, const std::string &repo);
static std::vector<Release> ListFromJson(const std::string_view &json, const std::string &host, const std::string &repo);
std::vector<Asset> assets;
};
// Make a request via httplib, and return the response body if applicable.
std::optional<std::string> MakeRequest(const std::string &url, const std::string &path);
std::vector<Release> GetReleasesFromJson(const std::string& body);
// Get the relevant list of assets for the current platform.
std::vector<NamedAsset> GetPlatformAssets(const Release &r);
// Make a request, and return the response body if applicable.
std::optional<std::string> MakeRequest(const std::string &url);
std::optional<std::string> MakeRequest(const std::string_view url);
bool HeadRequest(const std::string &url);
// Get all of the latest stable releases.
std::vector<Release> GetReleases();
+5 -6
View File
@@ -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)
+24 -24
View File
@@ -51,6 +51,30 @@
namespace Kernel {
struct KernelCore::SlabHeapContainer {
KSlabHeap<KClientSession> client_session;
KSlabHeap<KEvent> event;
KSlabHeap<KPort> port;
KSlabHeap<KProcess> process;
KSlabHeap<KResourceLimit> resource_limit;
KSlabHeap<KSession> session;
KSlabHeap<KLightSession> light_session;
KSlabHeap<KSharedMemory> shared_memory;
KSlabHeap<KSharedMemoryInfo> shared_memory_info;
KSlabHeap<KThread> thread;
KSlabHeap<KTransferMemory> transfer_memory;
KSlabHeap<KCodeMemory> code_memory;
KSlabHeap<KDeviceAddressSpace> device_address_space;
KSlabHeap<KPageBuffer> page_buffer;
KSlabHeap<KThreadLocalPage> thread_local_page;
KSlabHeap<KObjectName> object_name;
KSlabHeap<KSessionRequest> session_request;
KSlabHeap<KSecureSystemResource> secure_system_resource;
KSlabHeap<KThread::LockWithPriorityInheritanceInfo> lock_info;
KSlabHeap<KEventInfo> event_info;
KSlabHeap<KDebug> debug;
};
// Can only be used by a single implementation PER THREAD
struct ThreadLocalData {
std::optional<KThread> raw_thread;
@@ -1310,30 +1334,6 @@ const Core::System& KernelCore::System() const {
return impl->system;
}
struct KernelCore::SlabHeapContainer {
KSlabHeap<KClientSession> client_session;
KSlabHeap<KEvent> event;
KSlabHeap<KPort> port;
KSlabHeap<KProcess> process;
KSlabHeap<KResourceLimit> resource_limit;
KSlabHeap<KSession> session;
KSlabHeap<KLightSession> light_session;
KSlabHeap<KSharedMemory> shared_memory;
KSlabHeap<KSharedMemoryInfo> shared_memory_info;
KSlabHeap<KThread> thread;
KSlabHeap<KTransferMemory> transfer_memory;
KSlabHeap<KCodeMemory> code_memory;
KSlabHeap<KDeviceAddressSpace> device_address_space;
KSlabHeap<KPageBuffer> page_buffer;
KSlabHeap<KThreadLocalPage> thread_local_page;
KSlabHeap<KObjectName> object_name;
KSlabHeap<KSessionRequest> session_request;
KSlabHeap<KSecureSystemResource> secure_system_resource;
KSlabHeap<KThread::LockWithPriorityInheritanceInfo> lock_info;
KSlabHeap<KEventInfo> event_info;
KSlabHeap<KDebug> debug;
};
template <typename T>
KSlabHeap<T>& KernelCore::SlabHeap() {
if constexpr (std::is_same_v<T, KClientSession>) {
+41 -36
View File
@@ -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 <boost/algorithm/string/replace.hpp>
#include <boost/regex.hpp>
#include <boost/regex/v5/regex_replace.hpp>
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#include "common/httplib.h"
#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
@@ -28,10 +22,6 @@
#include <optional>
#include <thread>
#ifdef YUZU_BUNDLED_OPENSSL
#include <openssl/cert.h>
#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<u8> TryLoadFromDisk(const std::filesystem::path& path) {
// TODO(crueter): Migrate to use Common::Net
std::vector<u8> 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<u8> data(res->body.begin(), res->body.end());
if (auto body = Common::Net::MakeRequest(url)) {
std::vector<u8> 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<std::streamsize>(res->body.size()));
out.write(body->data(), static_cast<std::streamsize>(body->size()));
}
return data;
}
@@ -141,7 +123,7 @@ std::vector<u8> 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<u32>& 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<u64>(_mkgmtime(&tm));
#else
return static_cast<u64>(timegm(&tm));
#endif
}
void ImportReleases(const std::vector<Common::Net::Release> &releases) {
std::vector<u32> news_ids;
for (const auto& rel : releases) {
const u32 news_id = u32(rel.id & 0x7FFFFFFF);
std::hash<std::string> 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<std::string> 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<Common::Net::Release> &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<u8> 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());
+23 -25
View File
@@ -10,17 +10,20 @@
#include <string>
#include "common/container/unordered_map.h"
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#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<u64, s64>;
using CountMap = ::Common::unordered_map<u64, u64>;
@@ -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<s64>();
}
if (value.contains("launch_count") && value["launch_count"].is_number_unsigned()) {
g_counts[key] = value["launch_count"].get<u64>();
}
} 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<s64>();
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));
}
+258 -144
View File
@@ -5,12 +5,12 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <ctime>
#include <format>
#include <fstream>
#include <iomanip>
#include <fmt/chrono.h>
#include <fmt/ranges.h>
#include <nlohmann/json.hpp>
#include <map>
#include <ranges>
#include <vector>
#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<std::string> user_id;
};
struct ProcessorStateData {
std::string entry_point;
std::string sp;
std::string pc;
std::string pstate;
std::string architecture;
std::map<std::string, std::string> registers;
std::optional<std::vector<std::string>> 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<std::string> debug_buffer;
};
struct SvcBreakReport {
YuzuVersionData yuzu_version;
ReportCommonData report_common;
SvcBreakData svc_break;
};
struct BufferDescriptorEntry {
std::string address;
std::string size;
std::optional<std::string> data;
};
struct HLERequestContextData {
std::vector<std::string> command_buffer;
std::vector<BufferDescriptorEntry> buffer_descriptor_a;
std::vector<BufferDescriptorEntry> buffer_descriptor_b;
std::vector<BufferDescriptorEntry> buffer_descriptor_c;
std::vector<BufferDescriptorEntry> buffer_descriptor_x;
std::optional<u32> command_id;
std::optional<std::string> function_name;
std::optional<std::string> 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<std::string> applet_normal_data;
std::vector<std::string> applet_interactive_data;
};
struct PlayReport {
YuzuVersionData yuzu_version;
ReportCommonData report_common;
std::optional<std::string> play_report_process_id;
std::string play_report_type;
std::vector<std::string> 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 <typename T>
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<u128> 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<u32>(result.GetModule()))},
{"result_description", fmt::format("{:08X}", result.GetDescription())},
{"timestamp", timestamp},
ReportCommonData GetReportCommonData(u64 title_id, Result result, const std::string& timestamp,
std::optional<u128> 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<u32>(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<u64, 31>& registers,
const std::optional<std::array<u64, 32>>& 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<u64, 31>& registers,
const std::optional<std::array<u64, 32>>& backtrace = {}) {
std::map<std::string, std::string> 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<std::vector<std::string>> 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<std::vector<std::string>>();
}
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 <bool read_value, typename DescriptorType>
json GetHLEBufferDescriptorData(const boost::container::static_vector<DescriptorType, IPC::MAX_BUFFER_DESCRIPTORS>& buffer, Core::Memory::Memory& memory) {
auto buffer_out = json::array();
std::vector<BufferDescriptorEntry> GetHLEBufferDescriptorData(
const boost::container::static_vector<DescriptorType, IPC::MAX_BUFFER_DESCRIPTORS>& buffer,
Core::Memory::Memory& memory) {
std::vector<BufferDescriptorEntry> 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<u8> 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<std::string> 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<true>(ctx.BufferDescriptorA(), memory);
out["buffer_descriptor_b"] = GetHLEBufferDescriptorData<false>(ctx.BufferDescriptorB(), memory);
out["buffer_descriptor_c"] = GetHLEBufferDescriptorData<false>(ctx.BufferDescriptorC(), memory);
out["buffer_descriptor_x"] = GetHLEBufferDescriptorData<true>(ctx.BufferDescriptorX(), memory);
return out;
return {
.command_buffer = std::move(cmd_buf),
.buffer_descriptor_a = GetHLEBufferDescriptorData<true>(ctx.BufferDescriptorA(), memory),
.buffer_descriptor_b = GetHLEBufferDescriptorData<false>(ctx.BufferDescriptorB(), memory),
.buffer_descriptor_c = GetHLEBufferDescriptorData<false>(ctx.BufferDescriptorC(), memory),
.buffer_descriptor_x = GetHLEBufferDescriptorData<true>(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<std::vector<std::string>>();
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<std::vector<std::string>>();
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<std::vector<std::string>>();
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<u8>(type));
out["play_report_data"] = std::move(data_out);
out.play_report_type = std::format("{:02}", static_cast<u8>(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));
-3
View File
@@ -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)
+3 -4
View File
@@ -10,7 +10,6 @@
#include <boost/algorithm/string/split.hpp>
#endif
#include <fmt/format.h>
#include "common/net/net.h"
#include "common/scm_rev.h"
#include "update_checker.h"
@@ -21,12 +20,12 @@ std::optional<Common::Net::Release> 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<std::string> 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<Common::Net::Release> 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
@@ -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 <array>
#include <mutex>
#include <span>
#include "common/common_types.h"
+1
View File
@@ -12,6 +12,7 @@
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "hid_core/hid_types.h"
#include <vector>
namespace Core::HID {
class EmulatedController;
+1 -1
View File
@@ -1108,7 +1108,7 @@ std::vector<Member> 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);
+1 -1
View File
@@ -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)
+7 -7
View File
@@ -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);
+3 -19
View File
@@ -9,12 +9,11 @@
#include <QEventLoop>
#include <boost/algorithm/string/replace.hpp>
#include "common/httplib.h"
#include <discord_rpc.h>
#include <fmt/format.h>
#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;
}
+8
View File
@@ -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;
+3
View File
@@ -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();
+2 -1
View File
@@ -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)
+1 -2
View File
@@ -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)
+44 -64
View File
@@ -4,78 +4,55 @@
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <future>
#include <nlohmann/json.hpp>
#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<std::string>();
member.game.name = json.at("gameName").get<std::string>();
member.game.id = json.at("gameId").get<u64>();
try {
member.username = json.at("username").get<std::string>();
member.avatar_url = json.at("avatarUrl").get<std::string>();
} 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<std::string>();
room.ip = json.at("address").get<std::string>();
room.information.name = json.at("name").get<std::string>();
try {
room.information.description = json.at("description").get<std::string>();
} 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<std::string>();
room.information.port = json.at("port").get<u16>();
room.information.preferred_game.name = json.at("preferredGameName").get<std::string>();
room.information.preferred_game.id = json.at("preferredGameId").get<u64>();
room.information.member_slots = json.at("maxPlayers").get<u32>();
room.net_version = json.at("netVersion").get<u32>();
room.has_password = json.at("hasPassword").get<bool>();
try {
room.members = json.at("players").get<std::vector<Member>>();
} 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<AnnounceMultiplayerRoom::Room>();
room_id = reply_json.at("id").get<std::string>();
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<AnnounceMultiplayerRoom::RoomList>();
return jacinth::json::read(reply)["rooms"];
}
void RoomJson::Delete() {
+18 -6
View File
@@ -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 <nlohmann/json.hpp>
#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<std::string> 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>(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
+27 -47
View File
@@ -5,11 +5,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include <chrono>
#include <format>
#include <mutex>
#include <string>
#include <fmt/ranges.h>
#include "common/httplib.h"
#include <utility>
#ifdef YUZU_BUNDLED_OPENSSL
#include <openssl/cert.h>
@@ -19,12 +19,12 @@
#include "web_service/web_backend.h"
#include "web_service/web_result.h"
import hydrocarbon;
namespace WebService {
constexpr std::array<const char, 1> 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<httplib::Client>(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<httplib::Client> 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");
}
+23 -2
View File
@@ -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()
+34 -27
View File
@@ -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 <algorithm>
#include <thread>
#include <fmt/ranges.h>
#include <nlohmann/json.hpp>
#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<AmiiboData>(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));
}
+4
View File
@@ -6,6 +6,7 @@
#include <cmath>
#include <cstring>
#include <string>
#include "hid_core/hid_core.h"
#ifdef HAS_OPENGL
#include <glad/glad.h>
@@ -155,6 +156,9 @@ void GRenderWindow::Exit() {
}
GRenderWindow::~GRenderWindow() {
if (QtCommon::system)
QtCommon::system->HIDCore().UnloadInputDevices();
input_subsystem->Shutdown();
}
+12 -5
View File
@@ -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;
}
+1 -1
View File
@@ -272,7 +272,7 @@ void Lobby::OnRefreshLobby() {
QString::fromStdString(room.information.preferred_game.name), smdh_icon);
auto row = QList<QStandardItem*>({
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,
+343 -83
View File
@@ -1,32 +1,64 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <filesystem>
#include <fstream>
#include <utility>
#include <QRadioButton>
#include <QSaveFile>
#include <QStandardPaths>
#include <qdesktopservices.h>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/join.hpp>
#include <boost/algorithm/string/split.hpp>
#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 <openssl/cert.h>
#endif
#include <openssl/evp.h>
#include <QDesktopServices>
#include <QProcess>
#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("<a href=\"%1\">View on Forgejo</a>").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<httplib::Client> client = std::make_unique<httplib::Client>(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<std::filesystem::path> path,
QtCommon::Frontend::QtProgressDialog *progress) {
std::function<bool(std::string_view)> 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<int>((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;
}
+17 -1
View File
@@ -3,7 +3,11 @@
#pragma once
#include <filesystem>
#include <optional>
#include <QDialog>
#include "qt_common/abstract/progress.h"
#include "common/net/net.h"
class QRadioButton;
@@ -24,5 +28,17 @@ private slots:
private:
Ui::UpdateDialog* ui;
QList<QRadioButton*> 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<std::filesystem::path> path,
QtCommon::Frontend::QtProgressDialog *dialog);
};
+139
View File
@@ -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 <cstdint>
#include <string>
#include <vector>
#include <charconv>
#include <print>
#include <filesystem>
#include <boost/asio/io_context.hpp>
#include <boost/process/v2/process.hpp>
#include <windows.h>
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<std::string> 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;
}
+1
View File
@@ -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)
+1
View File
@@ -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})
+2 -2
View File
@@ -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()