mirror of https://git.citron-emu.org/Citron/Citron
Compare commits
45 Commits
v0.6-canar
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
57cf5a0daf | |
|
|
020492f1fa | |
|
|
21ca0b3119 | |
|
|
58401f5b39 | |
|
|
1ad69f3545 | |
|
|
48eed78d1a | |
|
|
5f962dd1c6 | |
|
|
25abfe36a3 | |
|
|
2f57a35d2d | |
|
|
66bdd6ed27 | |
|
|
ff9c61e7c7 | |
|
|
e72d695115 | |
|
|
0cdd546152 | |
|
|
3205c9b691 | |
|
|
f1e169e060 | |
|
|
278486d059 | |
|
|
bbd3253169 | |
|
|
a1f3414bde | |
|
|
175a427c27 | |
|
|
18def48dfe | |
|
|
a4088f3a1e | |
|
|
b66b3ca639 | |
|
|
3a1c178711 | |
|
|
964bbf489a | |
|
|
19febba866 | |
|
|
0dac3c1dbd | |
|
|
5d952717ff | |
|
|
b25c7653e6 | |
|
|
edfb500ee7 | |
|
|
ebfc9d8347 | |
|
|
1fd5fefcb1 | |
|
|
55dc3f8ec1 | |
|
|
7edbccbdc9 | |
|
|
e06526cbbc | |
|
|
0448d8146f | |
|
|
98c515871e | |
|
|
278ac75a37 | |
|
|
ec402a0510 | |
|
|
8cb6e6d5d4 | |
|
|
51800e249b | |
|
|
21594b73aa | |
|
|
d869045b77 | |
|
|
f2931c7566 | |
|
|
12c63997d2 | |
|
|
1023125be5 |
|
|
@ -9,22 +9,22 @@
|
||||||
url = https://github.com/mozilla/cubeb.git
|
url = https://github.com/mozilla/cubeb.git
|
||||||
[submodule "dynarmic"]
|
[submodule "dynarmic"]
|
||||||
path = externals/dynarmic
|
path = externals/dynarmic
|
||||||
url = https://git.citron-emu.org/Citron/dynarmic.git
|
url = https://github.com/yuzu-mirror/dynarmic.git
|
||||||
[submodule "libusb"]
|
[submodule "libusb"]
|
||||||
path = externals/libusb/libusb
|
path = externals/libusb/libusb
|
||||||
url = https://github.com/libusb/libusb.git
|
url = https://github.com/libusb/libusb.git
|
||||||
[submodule "discord-rpc"]
|
[submodule "discord-rpc"]
|
||||||
path = externals/discord-rpc
|
path = externals/discord-rpc
|
||||||
url = https://git.citron-emu.org/Citron/discord-rpc.git
|
url = https://github.com/yuzu-mirror/discord-rpc.git
|
||||||
[submodule "Vulkan-Headers"]
|
[submodule "Vulkan-Headers"]
|
||||||
path = externals/Vulkan-Headers
|
path = externals/Vulkan-Headers
|
||||||
url = https://github.com/KhronosGroup/Vulkan-Headers.git
|
url = https://github.com/KhronosGroup/Vulkan-Headers.git
|
||||||
[submodule "sirit"]
|
[submodule "sirit"]
|
||||||
path = externals/sirit
|
path = externals/sirit
|
||||||
url = https://git.citron-emu.org/Citron/sirit.git
|
url = https://github.com/yuzu-mirror/sirit.git
|
||||||
[submodule "mbedtls"]
|
[submodule "mbedtls"]
|
||||||
path = externals/mbedtls
|
path = externals/mbedtls
|
||||||
url = https://git.citron-emu.org/Citron/mbedtls.git
|
url = https://github.com/yuzu-mirror/mbedtls.git
|
||||||
[submodule "xbyak"]
|
[submodule "xbyak"]
|
||||||
path = externals/xbyak
|
path = externals/xbyak
|
||||||
url = https://github.com/herumi/xbyak.git
|
url = https://github.com/herumi/xbyak.git
|
||||||
|
|
@ -57,13 +57,13 @@
|
||||||
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
|
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
|
||||||
[submodule "breakpad"]
|
[submodule "breakpad"]
|
||||||
path = externals/breakpad
|
path = externals/breakpad
|
||||||
url = https://git.citron-emu.org/Citron/breakpad.git
|
url = https://github.com/yuzu-mirror/breakpad.git
|
||||||
[submodule "simpleini"]
|
[submodule "simpleini"]
|
||||||
path = externals/simpleini
|
path = externals/simpleini
|
||||||
url = https://github.com/brofield/simpleini.git
|
url = https://github.com/brofield/simpleini.git
|
||||||
[submodule "oaknut"]
|
[submodule "oaknut"]
|
||||||
path = externals/oaknut
|
path = externals/oaknut
|
||||||
url = https://git.citron-emu.org/Citron/oaknut.git
|
url = https://github.com/yuzu-mirror/oaknut
|
||||||
[submodule "Vulkan-Utility-Libraries"]
|
[submodule "Vulkan-Utility-Libraries"]
|
||||||
path = externals/Vulkan-Utility-Libraries
|
path = externals/Vulkan-Utility-Libraries
|
||||||
url = https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git
|
url = https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,45 @@ if (MSVC)
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3 /WX-")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3 /WX-")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# PGO Configuration
|
||||||
|
option(CITRON_ENABLE_PGO_INSTRUMENT "Enable Profile-Guided Optimization instrumentation build" OFF)
|
||||||
|
option(CITRON_ENABLE_PGO_OPTIMIZE "Enable Profile-Guided Optimization optimization build" OFF)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
if(CITRON_ENABLE_PGO_INSTRUMENT)
|
||||||
|
string(APPEND CMAKE_CXX_FLAGS_RELEASE " /GL /LTCG:PGINSTRUMENT")
|
||||||
|
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " /LTCG:PGINSTRUMENT")
|
||||||
|
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " /LTCG:PGINSTRUMENT")
|
||||||
|
elseif(CITRON_ENABLE_PGO_OPTIMIZE)
|
||||||
|
string(APPEND CMAKE_CXX_FLAGS_RELEASE " /GL /LTCG:PGOPTIMIZE")
|
||||||
|
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " /LTCG:PGOPTIMIZE")
|
||||||
|
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " /LTCG:PGOPTIMIZE")
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
# GCC and Clang PGO flags
|
||||||
|
if(CITRON_ENABLE_PGO_INSTRUMENT)
|
||||||
|
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||||
|
string(APPEND CMAKE_CXX_FLAGS_RELEASE " -fprofile-instr-generate")
|
||||||
|
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " -fprofile-instr-generate")
|
||||||
|
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " -fprofile-instr-generate")
|
||||||
|
else() # GCC
|
||||||
|
string(APPEND CMAKE_CXX_FLAGS_RELEASE " -fprofile-generate")
|
||||||
|
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " -fprofile-generate")
|
||||||
|
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " -fprofile-generate")
|
||||||
|
endif()
|
||||||
|
elseif(CITRON_ENABLE_PGO_OPTIMIZE)
|
||||||
|
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||||
|
string(APPEND CMAKE_CXX_FLAGS_RELEASE " -fprofile-instr-use=default.profdata")
|
||||||
|
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " -fprofile-instr-use=default.profdata")
|
||||||
|
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " -fprofile-instr-use=default.profdata")
|
||||||
|
else() # GCC
|
||||||
|
string(APPEND CMAKE_CXX_FLAGS_RELEASE " -fprofile-use")
|
||||||
|
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " -fprofile-use")
|
||||||
|
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " -fprofile-use")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
# Check if SDL2::SDL2 target exists; if not, create an alias
|
# Check if SDL2::SDL2 target exists; if not, create an alias
|
||||||
if (TARGET SDL2::SDL2-static)
|
if (TARGET SDL2::SDL2-static)
|
||||||
add_library(SDL2::SDL2 ALIAS SDL2::SDL2-static)
|
add_library(SDL2::SDL2 ALIAS SDL2::SDL2-static)
|
||||||
|
|
@ -98,7 +137,7 @@ endif()
|
||||||
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
|
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
|
||||||
|
|
||||||
if (ANDROID AND CITRON_DOWNLOAD_ANDROID_VVL)
|
if (ANDROID AND CITRON_DOWNLOAD_ANDROID_VVL)
|
||||||
set(vvl_version "1.4.304.1")
|
set(vvl_version "1.4.309.0")
|
||||||
set(vvl_zip_file "${CMAKE_BINARY_DIR}/externals/vvl-android.zip")
|
set(vvl_zip_file "${CMAKE_BINARY_DIR}/externals/vvl-android.zip")
|
||||||
set(vvl_lib_path "${CMAKE_CURRENT_SOURCE_DIR}/src/android/app/src/main/jniLibs/arm64-v8a/")
|
set(vvl_lib_path "${CMAKE_CURRENT_SOURCE_DIR}/src/android/app/src/main/jniLibs/arm64-v8a/")
|
||||||
set(vvl_final_lib "${vvl_lib_path}/libVkLayer_khronos_validation.so")
|
set(vvl_final_lib "${vvl_lib_path}/libVkLayer_khronos_validation.so")
|
||||||
|
|
@ -130,13 +169,23 @@ if (CITRON_USE_BUNDLED_VCPKG)
|
||||||
|
|
||||||
if (CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a")
|
if (CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a")
|
||||||
set(VCPKG_TARGET_TRIPLET "arm64-android")
|
set(VCPKG_TARGET_TRIPLET "arm64-android")
|
||||||
|
# Detect host system (Windows or Linux)
|
||||||
|
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
|
||||||
set(VCPKG_HOST_TRIPLET "x64-windows")
|
set(VCPKG_HOST_TRIPLET "x64-windows")
|
||||||
|
else()
|
||||||
|
set(VCPKG_HOST_TRIPLET "x64-linux")
|
||||||
|
endif()
|
||||||
# this is to avoid CMake using the host pkg-config to find the host
|
# this is to avoid CMake using the host pkg-config to find the host
|
||||||
# libraries when building for Android targets
|
# libraries when building for Android targets
|
||||||
set(PKG_CONFIG_EXECUTABLE "aarch64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE)
|
set(PKG_CONFIG_EXECUTABLE "aarch64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE)
|
||||||
elseif (CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64")
|
elseif (CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64")
|
||||||
set(VCPKG_TARGET_TRIPLET "x64-android")
|
set(VCPKG_TARGET_TRIPLET "x64-android")
|
||||||
|
# Detect host system (Windows or Linux)
|
||||||
|
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
|
||||||
set(VCPKG_HOST_TRIPLET "x64-windows")
|
set(VCPKG_HOST_TRIPLET "x64-windows")
|
||||||
|
else()
|
||||||
|
set(VCPKG_HOST_TRIPLET "x64-linux")
|
||||||
|
endif()
|
||||||
set(PKG_CONFIG_EXECUTABLE "x86_64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE)
|
set(PKG_CONFIG_EXECUTABLE "x86_64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE)
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR "Unsupported Android architecture ${CMAKE_ANDROID_ARCH_ABI}")
|
message(FATAL_ERROR "Unsupported Android architecture ${CMAKE_ANDROID_ARCH_ABI}")
|
||||||
|
|
@ -335,7 +384,7 @@ find_package(ZLIB REQUIRED)
|
||||||
find_package(zstd REQUIRED)
|
find_package(zstd REQUIRED)
|
||||||
|
|
||||||
if (NOT CITRON_USE_EXTERNAL_VULKAN_HEADERS)
|
if (NOT CITRON_USE_EXTERNAL_VULKAN_HEADERS)
|
||||||
find_package(VulkanHeaders 1.4.307 REQUIRED)
|
find_package(VulkanHeaders 1.4.313 REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (NOT CITRON_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES)
|
if (NOT CITRON_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES)
|
||||||
|
|
@ -398,7 +447,7 @@ if (ENABLE_SDL2)
|
||||||
if (CITRON_USE_BUNDLED_SDL2)
|
if (CITRON_USE_BUNDLED_SDL2)
|
||||||
# Detect toolchain and platform
|
# Detect toolchain and platform
|
||||||
if ((MSVC_VERSION GREATER_EQUAL 1920) AND ARCHITECTURE_x86_64)
|
if ((MSVC_VERSION GREATER_EQUAL 1920) AND ARCHITECTURE_x86_64)
|
||||||
set(SDL2_VER "SDL2-2.32.0")
|
set(SDL2_VER "SDL2-2.28.2")
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable CITRON_USE_BUNDLED_SDL2 and provide your own.")
|
message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable CITRON_USE_BUNDLED_SDL2 and provide your own.")
|
||||||
endif()
|
endif()
|
||||||
|
|
@ -545,7 +594,7 @@ endif()
|
||||||
# against all the src files. This should be used before making a pull request.
|
# against all the src files. This should be used before making a pull request.
|
||||||
# =======================================================================
|
# =======================================================================
|
||||||
|
|
||||||
set(CLANG_FORMAT_POSTFIX "-18")
|
set(CLANG_FORMAT_POSTFIX "-15")
|
||||||
find_program(CLANG_FORMAT
|
find_program(CLANG_FORMAT
|
||||||
NAMES clang-format${CLANG_FORMAT_POSTFIX}
|
NAMES clang-format${CLANG_FORMAT_POSTFIX}
|
||||||
clang-format
|
clang-format
|
||||||
|
|
@ -556,7 +605,7 @@ if (NOT CLANG_FORMAT)
|
||||||
message(STATUS "Clang format not found! Downloading...")
|
message(STATUS "Clang format not found! Downloading...")
|
||||||
set(CLANG_FORMAT "${PROJECT_BINARY_DIR}/externals/clang-format${CLANG_FORMAT_POSTFIX}.exe")
|
set(CLANG_FORMAT "${PROJECT_BINARY_DIR}/externals/clang-format${CLANG_FORMAT_POSTFIX}.exe")
|
||||||
file(DOWNLOAD
|
file(DOWNLOAD
|
||||||
https://git.citron-emu.org/Citron/ext-windows-bin/raw/master/clang-format${CLANG_FORMAT_POSTFIX}.exe
|
https://github.com/yuzu-mirror/ext-windows-bin/raw/master/clang-format${CLANG_FORMAT_POSTFIX}.exe
|
||||||
"${CLANG_FORMAT}" SHOW_PROGRESS
|
"${CLANG_FORMAT}" SHOW_PROGRESS
|
||||||
STATUS DOWNLOAD_SUCCESS)
|
STATUS DOWNLOAD_SUCCESS)
|
||||||
if (NOT DOWNLOAD_SUCCESS EQUAL 0)
|
if (NOT DOWNLOAD_SUCCESS EQUAL 0)
|
||||||
|
|
@ -684,42 +733,3 @@ if(ENABLE_QT AND UNIX AND NOT APPLE)
|
||||||
install(FILES "dist/org.citron_emu.citron.metainfo.xml"
|
install(FILES "dist/org.citron_emu.citron.metainfo.xml"
|
||||||
DESTINATION "share/metainfo")
|
DESTINATION "share/metainfo")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# PGO Configuration
|
|
||||||
option(CITRON_ENABLE_PGO_INSTRUMENT "Enable Profile-Guided Optimization instrumentation build" OFF)
|
|
||||||
option(CITRON_ENABLE_PGO_OPTIMIZE "Enable Profile-Guided Optimization optimization build" OFF)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
if(CITRON_ENABLE_PGO_INSTRUMENT)
|
|
||||||
string(APPEND CMAKE_CXX_FLAGS_RELEASE " /GL /LTCG:PGINSTRUMENT")
|
|
||||||
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " /LTCG:PGINSTRUMENT")
|
|
||||||
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " /LTCG:PGINSTRUMENT")
|
|
||||||
elseif(CITRON_ENABLE_PGO_OPTIMIZE)
|
|
||||||
string(APPEND CMAKE_CXX_FLAGS_RELEASE " /GL /LTCG:PGOPTIMIZE")
|
|
||||||
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " /LTCG:PGOPTIMIZE")
|
|
||||||
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " /LTCG:PGOPTIMIZE")
|
|
||||||
endif()
|
|
||||||
else()
|
|
||||||
# GCC and Clang PGO flags
|
|
||||||
if(CITRON_ENABLE_PGO_INSTRUMENT)
|
|
||||||
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
|
||||||
string(APPEND CMAKE_CXX_FLAGS_RELEASE " -fprofile-instr-generate")
|
|
||||||
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " -fprofile-instr-generate")
|
|
||||||
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " -fprofile-instr-generate")
|
|
||||||
else() # GCC
|
|
||||||
string(APPEND CMAKE_CXX_FLAGS_RELEASE " -fprofile-generate")
|
|
||||||
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " -fprofile-generate")
|
|
||||||
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " -fprofile-generate")
|
|
||||||
endif()
|
|
||||||
elseif(CITRON_ENABLE_PGO_OPTIMIZE)
|
|
||||||
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
|
||||||
string(APPEND CMAKE_CXX_FLAGS_RELEASE " -fprofile-instr-use=default.profdata")
|
|
||||||
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " -fprofile-instr-use=default.profdata")
|
|
||||||
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " -fprofile-instr-use=default.profdata")
|
|
||||||
else() # GCC
|
|
||||||
string(APPEND CMAKE_CXX_FLAGS_RELEASE " -fprofile-use")
|
|
||||||
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " -fprofile-use")
|
|
||||||
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " -fprofile-use")
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
|
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||||
function(download_bundled_external remote_path lib_name prefix_var)
|
function(download_bundled_external remote_path lib_name prefix_var)
|
||||||
|
|
||||||
set(package_base_url "https://git.citron-emu.org/Citron/")
|
set(package_base_url "https://github.com/yuzu-mirror/")
|
||||||
set(package_repo "no_platform")
|
set(package_repo "no_platform")
|
||||||
set(package_extension "no_platform")
|
set(package_extension "no_platform")
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
# SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
# Allow systemd-logind to manage user access to hidraw with this file
|
# Allow systemd-logind to manage user access to hidraw with this file
|
||||||
|
|
@ -7,13 +8,13 @@
|
||||||
|
|
||||||
# Switch Pro Controller (USB/Bluetooth)
|
# Switch Pro Controller (USB/Bluetooth)
|
||||||
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="2009", MODE="0660", TAG+="uaccess"
|
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="2009", MODE="0660", TAG+="uaccess"
|
||||||
KERNEL=="hidraw*", KERNELS=="*057e:2009*", MODE="0660", TAG+="uaccess"
|
KERNEL=="hidraw*", KERNELS=="*057E:2009*", MODE="0660", TAG+="uaccess"
|
||||||
|
|
||||||
# Joy-Con L (Bluetooth)
|
# Joy-Con L (Bluetooth)
|
||||||
KERNEL=="hidraw*", KERNELS=="*057e:2006*", MODE="0660", TAG+="uaccess"
|
KERNEL=="hidraw*", KERNELS=="*057E:2006*", MODE="0660", TAG+="uaccess"
|
||||||
|
|
||||||
# Joy-Con R (Bluetooth)
|
# Joy-Con R (Bluetooth)
|
||||||
KERNEL=="hidraw*", KERNELS=="*057e:2007*", MODE="0660", TAG+="uaccess"
|
KERNEL=="hidraw*", KERNELS=="*057E:2007*", MODE="0660", TAG+="uaccess"
|
||||||
|
|
||||||
# Joy-Con Charging Grip (USB)
|
# Joy-Con Charging Grip (USB)
|
||||||
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="200e", MODE="0660", TAG+="uaccess"
|
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="200e", MODE="0660", TAG+="uaccess"
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit cc016b0046d563287f0aa9f09b958b5e70d43696
|
Subproject commit 2359383fc187386204c3bb22de89655a494cd128
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit cacef3039d277c448c89336290ec3937270b0996
|
Subproject commit e2e53a724677f6eba8ff0ce1ccb64ee321785cbd
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit bc3a4d9fd9b46729651a3cec4f5226f6272b8684
|
Subproject commit 4e246c56ec5afb5ad66b9b04374d39ac04675c8e
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit c788c52156f3ef7bc7ab769cb03c110a53ac8fcb
|
Subproject commit 539c0a8d8e3733c9f25ea9a184c85c77504f1653
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit e40d24cb149dd138e7c11d490834fa2c81298b32
|
Subproject commit 96d5fb3de135b86d7222c53f2352ca92827a156b
|
||||||
|
|
@ -11,10 +11,10 @@ plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
kotlin("plugin.serialization") version "2.1.20-RC2"
|
kotlin("plugin.serialization") version "1.9.20"
|
||||||
id("androidx.navigation.safeargs.kotlin")
|
id("androidx.navigation.safeargs.kotlin")
|
||||||
id("org.jlleitschuh.gradle.ktlint") version "12.2.0"
|
id("org.jlleitschuh.gradle.ktlint") version "11.4.0"
|
||||||
id("com.github.triplet.play") version "3.12.1"
|
id("com.github.triplet.play") version "3.8.6"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -55,7 +55,7 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO If this is ever modified, change application_id in strings.xml
|
// TODO If this is ever modified, change application_id in strings.xml
|
||||||
applicationId = "com.antutu.ABenchMark"
|
applicationId = "org.citron.citron_emu"
|
||||||
minSdk = 30
|
minSdk = 30
|
||||||
//noinspection EditedTargetSdkVersion
|
//noinspection EditedTargetSdkVersion
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
|
|
@ -161,7 +161,7 @@ android {
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
version = "3.31.6"
|
version = "4.0.1"
|
||||||
path = file("../../../CMakeLists.txt")
|
path = file("../../../CMakeLists.txt")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -179,7 +179,8 @@ android {
|
||||||
"-DCITRON_USE_BUNDLED_FFMPEG=ON",
|
"-DCITRON_USE_BUNDLED_FFMPEG=ON",
|
||||||
"-DCITRON_ENABLE_LTO=ON",
|
"-DCITRON_ENABLE_LTO=ON",
|
||||||
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
|
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
|
||||||
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
|
||||||
|
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
|
||||||
)
|
)
|
||||||
|
|
||||||
abiFilters("arm64-v8a") // , "x86_64")
|
abiFilters("arm64-v8a") // , "x86_64")
|
||||||
|
|
@ -203,7 +204,7 @@ tasks.getByPath("ktlintMainSourceSetCheck").doFirst { showFormatHelp.invoke() }
|
||||||
tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset")
|
tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset")
|
||||||
|
|
||||||
ktlint {
|
ktlint {
|
||||||
version.set("0.49.1")
|
version.set("0.47.1")
|
||||||
android.set(true)
|
android.set(true)
|
||||||
ignoreFailures.set(false)
|
ignoreFailures.set(false)
|
||||||
disabledRules.set(
|
disabledRules.set(
|
||||||
|
|
@ -228,24 +229,23 @@ play {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.core:core-ktx:1.15.0")
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.4.0")
|
implementation("androidx.recyclerview:recyclerview:1.3.1")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.8.6")
|
implementation("androidx.fragment:fragment-ktx:1.6.1")
|
||||||
implementation("androidx.documentfile:documentfile:1.0.1")
|
implementation("androidx.documentfile:documentfile:1.0.1")
|
||||||
implementation("com.google.android.material:material:1.12.0")
|
implementation("com.google.android.material:material:1.9.0")
|
||||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
|
||||||
implementation("io.coil-kt:coil:2.7.0")
|
implementation("io.coil-kt:coil:2.2.2")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||||
implementation("androidx.window:window:1.3.0")
|
implementation("androidx.window:window:1.2.0-beta03")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:2.8.8")
|
implementation("androidx.navigation:navigation-fragment-ktx:2.7.4")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:2.8.8")
|
implementation("androidx.navigation:navigation-ui-ktx:2.7.4")
|
||||||
implementation("info.debatty:java-string-similarity:2.0.0")
|
implementation("info.debatty:java-string-similarity:2.0.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun runGitCommand(command: List<String>): String {
|
fun runGitCommand(command: List<String>): String {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
||||||
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
|
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
|
||||||
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
|
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
|
||||||
RENDERER_DEBUG("debug"),
|
RENDERER_DEBUG("debug"),
|
||||||
|
RENDERER_ENHANCED_SHADER_BUILDING("use_enhanced_shader_building"),
|
||||||
PICTURE_IN_PICTURE("picture_in_picture"),
|
PICTURE_IN_PICTURE("picture_in_picture"),
|
||||||
USE_CUSTOM_RTC("custom_rtc_enabled"),
|
USE_CUSTOM_RTC("custom_rtc_enabled"),
|
||||||
BLACK_BACKGROUNDS("black_backgrounds"),
|
BLACK_BACKGROUNDS("black_backgrounds"),
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,62 @@ class SetupFragment : Fragment() {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Add title.keys installation page
|
||||||
|
add(
|
||||||
|
SetupPage(
|
||||||
|
R.drawable.ic_key,
|
||||||
|
R.string.install_title_keys,
|
||||||
|
R.string.install_title_keys_description,
|
||||||
|
R.drawable.ic_add,
|
||||||
|
true,
|
||||||
|
R.string.select_keys,
|
||||||
|
{
|
||||||
|
titleKeyCallback = it
|
||||||
|
getTitleKey.launch(arrayOf("*/*"))
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
R.string.install_title_keys_warning,
|
||||||
|
R.string.install_title_keys_warning_description,
|
||||||
|
R.string.install_title_keys_warning_help,
|
||||||
|
{
|
||||||
|
val file = File(DirectoryInitialization.userDirectory + "/keys/title.keys")
|
||||||
|
if (file.exists()) {
|
||||||
|
StepState.COMPLETE
|
||||||
|
} else {
|
||||||
|
StepState.INCOMPLETE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add firmware installation page (mandatory)
|
||||||
|
add(
|
||||||
|
SetupPage(
|
||||||
|
R.drawable.ic_key,
|
||||||
|
R.string.install_firmware,
|
||||||
|
R.string.install_firmware_description,
|
||||||
|
R.drawable.ic_add,
|
||||||
|
true,
|
||||||
|
R.string.select_firmware,
|
||||||
|
{
|
||||||
|
firmwareCallback = it
|
||||||
|
getFirmware.launch(arrayOf("application/zip"))
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
R.string.install_firmware_warning,
|
||||||
|
R.string.install_firmware_warning_description,
|
||||||
|
R.string.install_firmware_warning_help,
|
||||||
|
{
|
||||||
|
if (NativeLibrary.isFirmwareAvailable()) {
|
||||||
|
StepState.COMPLETE
|
||||||
|
} else {
|
||||||
|
StepState.INCOMPLETE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
add(
|
add(
|
||||||
SetupPage(
|
SetupPage(
|
||||||
R.drawable.ic_controller,
|
R.drawable.ic_controller,
|
||||||
|
|
@ -268,6 +324,18 @@ class SetupFragment : Fragment() {
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special handling for firmware page - don't allow skipping
|
||||||
|
if (currentPage.titleId == R.string.install_firmware && !NativeLibrary.isFirmwareAvailable()) {
|
||||||
|
SetupWarningDialogFragment.newInstance(
|
||||||
|
currentPage.warningTitleId,
|
||||||
|
currentPage.warningDescriptionId,
|
||||||
|
currentPage.warningHelpLinkId,
|
||||||
|
index,
|
||||||
|
allowSkip = false
|
||||||
|
).show(childFragmentManager, SetupWarningDialogFragment.TAG)
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasBeenWarned[index]) {
|
if (!hasBeenWarned[index]) {
|
||||||
SetupWarningDialogFragment.newInstance(
|
SetupWarningDialogFragment.newInstance(
|
||||||
currentPage.warningTitleId,
|
currentPage.warningTitleId,
|
||||||
|
|
@ -346,6 +414,30 @@ class SetupFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private lateinit var titleKeyCallback: SetupCallback
|
||||||
|
|
||||||
|
val getTitleKey =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||||
|
if (result != null) {
|
||||||
|
mainActivity.processTitleKey(result)
|
||||||
|
titleKeyCallback.onStepCompleted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var firmwareCallback: SetupCallback
|
||||||
|
|
||||||
|
val getFirmware =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||||
|
if (result != null) {
|
||||||
|
mainActivity.getFirmware.launch(arrayOf("application/zip"))
|
||||||
|
binding.root.postDelayed({
|
||||||
|
if (NativeLibrary.isFirmwareAvailable()) {
|
||||||
|
firmwareCallback.onStepCompleted()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var gamesDirCallback: SetupCallback
|
private lateinit var gamesDirCallback: SetupCallback
|
||||||
|
|
||||||
val getGamesDirectory =
|
val getGamesDirectory =
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ class SetupWarningDialogFragment : DialogFragment() {
|
||||||
private var descriptionId: Int = 0
|
private var descriptionId: Int = 0
|
||||||
private var helpLinkId: Int = 0
|
private var helpLinkId: Int = 0
|
||||||
private var page: Int = 0
|
private var page: Int = 0
|
||||||
|
private var allowSkip: Boolean = true
|
||||||
|
|
||||||
private lateinit var setupFragment: SetupFragment
|
private lateinit var setupFragment: SetupFragment
|
||||||
|
|
||||||
|
|
@ -26,17 +27,24 @@ class SetupWarningDialogFragment : DialogFragment() {
|
||||||
descriptionId = requireArguments().getInt(DESCRIPTION)
|
descriptionId = requireArguments().getInt(DESCRIPTION)
|
||||||
helpLinkId = requireArguments().getInt(HELP_LINK)
|
helpLinkId = requireArguments().getInt(HELP_LINK)
|
||||||
page = requireArguments().getInt(PAGE)
|
page = requireArguments().getInt(PAGE)
|
||||||
|
allowSkip = requireArguments().getBoolean(ALLOW_SKIP, true)
|
||||||
|
|
||||||
setupFragment = requireParentFragment() as SetupFragment
|
setupFragment = requireParentFragment() as SetupFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||||
.setPositiveButton(R.string.warning_skip) { _: DialogInterface?, _: Int ->
|
|
||||||
|
if (allowSkip) {
|
||||||
|
builder.setPositiveButton(R.string.warning_skip) { _: DialogInterface?, _: Int ->
|
||||||
setupFragment.pageForward()
|
setupFragment.pageForward()
|
||||||
setupFragment.setPageWarned(page)
|
setupFragment.setPageWarned(page)
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.warning_cancel, null)
|
builder.setNegativeButton(R.string.warning_cancel, null)
|
||||||
|
} else {
|
||||||
|
// For mandatory steps, only show an OK button that dismisses the dialog
|
||||||
|
builder.setPositiveButton(R.string.ok, null)
|
||||||
|
}
|
||||||
|
|
||||||
if (titleId != 0) {
|
if (titleId != 0) {
|
||||||
builder.setTitle(titleId)
|
builder.setTitle(titleId)
|
||||||
|
|
@ -48,7 +56,7 @@ class SetupWarningDialogFragment : DialogFragment() {
|
||||||
}
|
}
|
||||||
if (helpLinkId != 0) {
|
if (helpLinkId != 0) {
|
||||||
builder.setNeutralButton(R.string.warning_help) { _: DialogInterface?, _: Int ->
|
builder.setNeutralButton(R.string.warning_help) { _: DialogInterface?, _: Int ->
|
||||||
val helpLink = resources.getString(R.string.install_prod_keys_warning_help)
|
val helpLink = resources.getString(helpLinkId)
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink))
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink))
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
@ -64,12 +72,14 @@ class SetupWarningDialogFragment : DialogFragment() {
|
||||||
private const val DESCRIPTION = "Description"
|
private const val DESCRIPTION = "Description"
|
||||||
private const val HELP_LINK = "HelpLink"
|
private const val HELP_LINK = "HelpLink"
|
||||||
private const val PAGE = "Page"
|
private const val PAGE = "Page"
|
||||||
|
private const val ALLOW_SKIP = "AllowSkip"
|
||||||
|
|
||||||
fun newInstance(
|
fun newInstance(
|
||||||
titleId: Int,
|
titleId: Int,
|
||||||
descriptionId: Int,
|
descriptionId: Int,
|
||||||
helpLinkId: Int,
|
helpLinkId: Int,
|
||||||
page: Int
|
page: Int,
|
||||||
|
allowSkip: Boolean = true
|
||||||
): SetupWarningDialogFragment {
|
): SetupWarningDialogFragment {
|
||||||
val dialog = SetupWarningDialogFragment()
|
val dialog = SetupWarningDialogFragment()
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
|
|
@ -78,6 +88,7 @@ class SetupWarningDialogFragment : DialogFragment() {
|
||||||
putInt(DESCRIPTION, descriptionId)
|
putInt(DESCRIPTION, descriptionId)
|
||||||
putInt(HELP_LINK, helpLinkId)
|
putInt(HELP_LINK, helpLinkId)
|
||||||
putInt(PAGE, page)
|
putInt(PAGE, page)
|
||||||
|
putBoolean(ALLOW_SKIP, allowSkip)
|
||||||
}
|
}
|
||||||
dialog.arguments = bundle
|
dialog.arguments = bundle
|
||||||
return dialog
|
return dialog
|
||||||
|
|
|
||||||
|
|
@ -377,6 +377,57 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val getTitleKey =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||||
|
if (result != null) {
|
||||||
|
processTitleKey(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun processTitleKey(result: Uri): Boolean {
|
||||||
|
if (FileUtil.getExtension(result) != "keys") {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
this,
|
||||||
|
titleId = R.string.reading_keys_failure,
|
||||||
|
descriptionId = R.string.install_title_keys_failure_extension_description
|
||||||
|
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
contentResolver.takePersistableUriPermission(
|
||||||
|
result,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
|
||||||
|
val dstPath = DirectoryInitialization.userDirectory + "/keys/"
|
||||||
|
if (FileUtil.copyUriToInternalStorage(
|
||||||
|
result,
|
||||||
|
dstPath,
|
||||||
|
"title.keys"
|
||||||
|
) != null
|
||||||
|
) {
|
||||||
|
if (NativeLibrary.reloadKeys()) {
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
R.string.install_keys_success,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
homeViewModel.setCheckKeys(true)
|
||||||
|
gamesViewModel.reloadGames(true)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
this,
|
||||||
|
titleId = R.string.invalid_keys_error,
|
||||||
|
descriptionId = R.string.install_keys_failure_description,
|
||||||
|
helpLinkId = R.string.dumping_keys_quickstart_link
|
||||||
|
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
val getFirmware =
|
val getFirmware =
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
|
|
|
||||||
|
|
@ -108,11 +108,15 @@
|
||||||
<string name="import_saves">Import</string>
|
<string name="import_saves">Import</string>
|
||||||
<string name="export_saves">Export</string>
|
<string name="export_saves">Export</string>
|
||||||
<string name="install_firmware">Install firmware</string>
|
<string name="install_firmware">Install firmware</string>
|
||||||
<string name="install_firmware_description">Firmware must be in a ZIP archive and is needed to boot some games</string>
|
<string name="install_firmware_description">Required for emulation of system features</string>
|
||||||
|
<string name="install_firmware_warning">Firmware installation is mandatory</string>
|
||||||
|
<string name="install_firmware_warning_description">Firmware is required for proper emulation. You must install firmware to continue.</string>
|
||||||
|
<string name="install_firmware_warning_help">https://citron-emu.org/help/quickstart/#dumping-system-firmware</string>
|
||||||
<string name="firmware_installing">Installing firmware</string>
|
<string name="firmware_installing">Installing firmware</string>
|
||||||
<string name="firmware_installed_success">Firmware installed successfully</string>
|
<string name="firmware_installed_success">Firmware successfully installed</string>
|
||||||
<string name="firmware_installed_failure">Firmware installation failed</string>
|
<string name="firmware_installed_failure">Failed to install firmware</string>
|
||||||
<string name="firmware_installed_failure_description">Make sure the firmware nca files are at the root of the zip and try again.</string>
|
<string name="firmware_installed_failure_description">The selected file is not a valid firmware archive or is corrupt.</string>
|
||||||
|
<string name="select_firmware">Select Firmware</string>
|
||||||
<string name="share_log">Share debug logs</string>
|
<string name="share_log">Share debug logs</string>
|
||||||
<string name="share_log_description">Share citron\'s log file to debug issues</string>
|
<string name="share_log_description">Share citron\'s log file to debug issues</string>
|
||||||
<string name="share_log_missing">No log file found</string>
|
<string name="share_log_missing">No log file found</string>
|
||||||
|
|
@ -172,6 +176,14 @@
|
||||||
<string name="cabinet_restorer">Restorer</string>
|
<string name="cabinet_restorer">Restorer</string>
|
||||||
<string name="cabinet_formatter">Formatter</string>
|
<string name="cabinet_formatter">Formatter</string>
|
||||||
|
|
||||||
|
<!-- Title keys strings -->
|
||||||
|
<string name="install_title_keys">Install title.keys</string>
|
||||||
|
<string name="install_title_keys_description">Required for additional game compatibility</string>
|
||||||
|
<string name="install_title_keys_warning">Skip adding title keys?</string>
|
||||||
|
<string name="install_title_keys_warning_description">Title keys may be required for some games to function properly.</string>
|
||||||
|
<string name="install_title_keys_warning_help">https://citron-emu.org/</string>
|
||||||
|
<string name="install_title_keys_failure_extension_description">Verify your title keys file has a .keys extension and try again.</string>
|
||||||
|
|
||||||
<!-- About screen strings -->
|
<!-- About screen strings -->
|
||||||
<string name="gaia_is_not_real">Gaia isn\'t real</string>
|
<string name="gaia_is_not_real">Gaia isn\'t real</string>
|
||||||
<string name="copied_to_clipboard">Copied to clipboard</string>
|
<string name="copied_to_clipboard">Copied to clipboard</string>
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
|
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "8.9.0" apply false
|
id("com.android.application") version "8.9.2" apply false
|
||||||
id("com.android.library") version "8.9.0" apply false
|
id("com.android.library") version "8.9.2" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.1.20-RC2" apply false
|
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("clean").configure {
|
tasks.register("clean").configure {
|
||||||
|
|
@ -18,6 +18,6 @@ buildscript {
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.8.8")
|
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.6.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <typeinfo>
|
#include <typeinfo>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QSlider>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "common/settings_enums.h"
|
#include "common/settings_enums.h"
|
||||||
|
|
@ -38,7 +41,22 @@ ConfigureCpu::ConfigureCpu(const Core::System& system_,
|
||||||
|
|
||||||
ConfigureCpu::~ConfigureCpu() = default;
|
ConfigureCpu::~ConfigureCpu() = default;
|
||||||
|
|
||||||
void ConfigureCpu::SetConfiguration() {}
|
void ConfigureCpu::SetConfiguration() {
|
||||||
|
// Set clock rate values from settings
|
||||||
|
const u32 clock_rate_mhz = Settings::values.cpu_clock_rate.GetValue() / 1'000'000;
|
||||||
|
ui->clock_rate_slider->setValue(static_cast<int>(clock_rate_mhz));
|
||||||
|
ui->clock_rate_spinbox->setValue(static_cast<int>(clock_rate_mhz));
|
||||||
|
|
||||||
|
// Connect slider and spinbox signals to keep them in sync
|
||||||
|
connect(ui->clock_rate_slider, &QSlider::valueChanged, this, [this](int value) {
|
||||||
|
ui->clock_rate_spinbox->setValue(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(ui->clock_rate_spinbox, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int value) {
|
||||||
|
ui->clock_rate_slider->setValue(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
|
void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
|
||||||
auto* accuracy_layout = ui->widget_accuracy->layout();
|
auto* accuracy_layout = ui->widget_accuracy->layout();
|
||||||
auto* backend_layout = ui->widget_backend->layout();
|
auto* backend_layout = ui->widget_backend->layout();
|
||||||
|
|
@ -99,6 +117,9 @@ void ConfigureCpu::ApplyConfiguration() {
|
||||||
for (const auto& apply_func : apply_funcs) {
|
for (const auto& apply_func : apply_funcs) {
|
||||||
apply_func(is_powered_on);
|
apply_func(is_powered_on);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save the clock rate setting (convert from MHz to Hz)
|
||||||
|
Settings::values.cpu_clock_rate = static_cast<u32>(ui->clock_rate_spinbox->value()) * 1'000'000;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureCpu::changeEvent(QEvent* event) {
|
void ConfigureCpu::changeEvent(QEvent* event) {
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,67 @@
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="clock_rate_group">
|
||||||
|
<property name="title">
|
||||||
|
<string>CPU Clock Rate</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_clock_description">
|
||||||
|
<property name="text">
|
||||||
|
<string>CPU clock rate in MHz. Setting a higher clock rate will improve performance but may cause system instability. Default is 1020 MHz.</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QSlider" name="clock_rate_slider">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>500</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>1785</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>1020</number>
|
||||||
|
</property>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="tickPosition">
|
||||||
|
<enum>QSlider::TicksBelow</enum>
|
||||||
|
</property>
|
||||||
|
<property name="tickInterval">
|
||||||
|
<number>100</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="clock_rate_spinbox">
|
||||||
|
<property name="suffix">
|
||||||
|
<string> MHz</string>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<number>500</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>1785</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>1020</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
|
|
|
||||||
|
|
@ -423,6 +423,12 @@
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="6" column="0">
|
||||||
<widget class="QCheckBox" name="use_auto_stub">
|
<widget class="QCheckBox" name="use_auto_stub">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>This feature has been disabled.</string>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Enable Auto-Stub**</string>
|
<string>Enable Auto-Stub**</string>
|
||||||
</property>
|
</property>
|
||||||
|
|
@ -430,6 +436,12 @@
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QCheckBox" name="quest_flag">
|
<widget class="QCheckBox" name="quest_flag">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>This feature has been disabled.</string>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Kiosk (Quest) Mode</string>
|
<string>Kiosk (Quest) Mode</string>
|
||||||
</property>
|
</property>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include "citron/configuration/shared_translation.h"
|
#include "citron/configuration/shared_translation.h"
|
||||||
|
|
@ -146,6 +147,11 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
|
||||||
INSERT(
|
INSERT(
|
||||||
Settings, use_asynchronous_gpu_emulation, tr("Use asynchronous GPU emulation"),
|
Settings, use_asynchronous_gpu_emulation, tr("Use asynchronous GPU emulation"),
|
||||||
tr("Uses an extra CPU thread for rendering.\nThis option should always remain enabled."));
|
tr("Uses an extra CPU thread for rendering.\nThis option should always remain enabled."));
|
||||||
|
INSERT(
|
||||||
|
Settings, respect_present_interval_zero, tr("Respect present interval 0 for unlocked FPS"),
|
||||||
|
tr("When enabled, present interval 0 will be used for games requesting unlocked FPS.\n"
|
||||||
|
"This matches console behavior more closely, but may cause higher battery usage and frame pacing issues.\n"
|
||||||
|
"When disabled (default), present interval 0 is capped at 120FPS to conserve battery."));
|
||||||
INSERT(Settings, nvdec_emulation, tr("NVDEC emulation:"),
|
INSERT(Settings, nvdec_emulation, tr("NVDEC emulation:"),
|
||||||
tr("Specifies how videos should be decoded.\nIt can either use the CPU or the GPU for "
|
tr("Specifies how videos should be decoded.\nIt can either use the CPU or the GPU for "
|
||||||
"decoding, or perform no decoding at all (black screen on videos).\n"
|
"decoding, or perform no decoding at all (black screen on videos).\n"
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ system_
|
||||||
DiscordEventHandlers handlers {};
|
DiscordEventHandlers handlers {};
|
||||||
// The number is the client ID for citron, it's used for images and the
|
// The number is the client ID for citron, it's used for images and the
|
||||||
// application name
|
// application name
|
||||||
Discord_Initialize("1322413013248118888", & handlers, 1, nullptr);
|
Discord_Initialize("1361252452329848892", & handlers, 1, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
DiscordImpl::~DiscordImpl() {
|
DiscordImpl::~DiscordImpl() {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
|
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -89,6 +90,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
|
||||||
SUB(Service, BGTC) \
|
SUB(Service, BGTC) \
|
||||||
SUB(Service, BTDRV) \
|
SUB(Service, BTDRV) \
|
||||||
SUB(Service, BTM) \
|
SUB(Service, BTM) \
|
||||||
|
SUB(Service, BSD) \
|
||||||
SUB(Service, Capture) \
|
SUB(Service, Capture) \
|
||||||
SUB(Service, ERPT) \
|
SUB(Service, ERPT) \
|
||||||
SUB(Service, ETicket) \
|
SUB(Service, ETicket) \
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -57,6 +58,7 @@ enum class Class : u8 {
|
||||||
Service_BPC, ///< The BPC service
|
Service_BPC, ///< The BPC service
|
||||||
Service_BTDRV, ///< The Bluetooth driver service
|
Service_BTDRV, ///< The Bluetooth driver service
|
||||||
Service_BTM, ///< The BTM service
|
Service_BTM, ///< The BTM service
|
||||||
|
Service_BSD, ///< The BSD sockets service
|
||||||
Service_Capture, ///< The capture service
|
Service_Capture, ///< The capture service
|
||||||
Service_ERPT, ///< The error reporting service
|
Service_ERPT, ///< The error reporting service
|
||||||
Service_ETicket, ///< The ETicket service
|
Service_ETicket, ///< The ETicket service
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -198,6 +199,7 @@ struct Values {
|
||||||
MemoryLayout::Memory_12Gb,
|
MemoryLayout::Memory_12Gb,
|
||||||
"memory_layout_mode",
|
"memory_layout_mode",
|
||||||
Category::Core};
|
Category::Core};
|
||||||
|
SwitchableSetting<u32> cpu_clock_rate{linkage, 1'020'000'000, "cpu_clock_rate", Category::Cpu};
|
||||||
SwitchableSetting<bool> use_speed_limit{
|
SwitchableSetting<bool> use_speed_limit{
|
||||||
linkage, true, "use_speed_limit", Category::Core, Specialization::Paired, false, true};
|
linkage, true, "use_speed_limit", Category::Core, Specialization::Paired, false, true};
|
||||||
SwitchableSetting<u16, true> speed_limit{linkage,
|
SwitchableSetting<u16, true> speed_limit{linkage,
|
||||||
|
|
@ -210,6 +212,11 @@ struct Values {
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
&use_speed_limit};
|
&use_speed_limit};
|
||||||
|
SwitchableSetting<bool> use_nce{linkage, true, "Use Native Code Execution", Category::Core};
|
||||||
|
|
||||||
|
// Memory
|
||||||
|
SwitchableSetting<bool> use_gpu_memory_manager{linkage, false, "Use GPU Memory Manager", Category::Core};
|
||||||
|
SwitchableSetting<bool> enable_memory_snapshots{linkage, false, "Enable Memory Snapshots", Category::Core};
|
||||||
|
|
||||||
// Cpu
|
// Cpu
|
||||||
SwitchableSetting<CpuBackend, true> cpu_backend{linkage,
|
SwitchableSetting<CpuBackend, true> cpu_backend{linkage,
|
||||||
|
|
@ -278,6 +285,8 @@ struct Values {
|
||||||
Category::Renderer};
|
Category::Renderer};
|
||||||
SwitchableSetting<bool> use_asynchronous_gpu_emulation{
|
SwitchableSetting<bool> use_asynchronous_gpu_emulation{
|
||||||
linkage, true, "use_asynchronous_gpu_emulation", Category::Renderer};
|
linkage, true, "use_asynchronous_gpu_emulation", Category::Renderer};
|
||||||
|
SwitchableSetting<bool> respect_present_interval_zero{
|
||||||
|
linkage, false, "respect_present_interval_zero", Category::Renderer};
|
||||||
SwitchableSetting<AstcDecodeMode, true> accelerate_astc{linkage,
|
SwitchableSetting<AstcDecodeMode, true> accelerate_astc{linkage,
|
||||||
#ifdef ANDROID
|
#ifdef ANDROID
|
||||||
AstcDecodeMode::Cpu,
|
AstcDecodeMode::Cpu,
|
||||||
|
|
@ -618,11 +627,21 @@ struct Values {
|
||||||
|
|
||||||
// Add-Ons
|
// Add-Ons
|
||||||
std::map<u64, std::vector<std::string>> disabled_addons;
|
std::map<u64, std::vector<std::string>> disabled_addons;
|
||||||
|
|
||||||
|
// Renderer Advanced Settings
|
||||||
|
SwitchableSetting<bool> use_enhanced_shader_building{linkage, false, "Enhanced Shader Building",
|
||||||
|
Category::RendererAdvanced};
|
||||||
|
|
||||||
|
// Add a new setting for shader compilation priority
|
||||||
|
SwitchableSetting<int> shader_compilation_priority{linkage, 0, "Shader Compilation Priority",
|
||||||
|
Category::RendererAdvanced};
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Values values;
|
extern Values values;
|
||||||
|
|
||||||
void UpdateGPUAccuracy();
|
void UpdateGPUAccuracy();
|
||||||
|
// boold isGPULevelNormal();
|
||||||
|
// TODO: ZEP
|
||||||
bool IsGPULevelExtreme();
|
bool IsGPULevelExtreme();
|
||||||
bool IsGPULevelHigh();
|
bool IsGPULevelHigh();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -8,6 +9,7 @@
|
||||||
#include <ratio>
|
#include <ratio>
|
||||||
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
#include "core/hardware_properties.h"
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
|
|
||||||
|
|
@ -15,7 +17,10 @@ class WallClock {
|
||||||
public:
|
public:
|
||||||
static constexpr u64 CNTFRQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz
|
static constexpr u64 CNTFRQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz
|
||||||
static constexpr u64 GPUTickFreq = 614'400'000; // GM20B GPU Tick Frequency = 614.4 MHz
|
static constexpr u64 GPUTickFreq = 614'400'000; // GM20B GPU Tick Frequency = 614.4 MHz
|
||||||
static constexpr u64 CPUTickFreq = 1'020'000'000; // T210/4 A57 CPU Tick Frequency = 1020.0 MHz
|
// Changed from constexpr to function to get dynamic value from settings
|
||||||
|
static inline u64 CPUTickFreq() {
|
||||||
|
return Core::Hardware::BASE_CLOCK_RATE();
|
||||||
|
} // T210/4 A57 CPU Tick Frequency from settings
|
||||||
|
|
||||||
virtual ~WallClock() = default;
|
virtual ~WallClock() = default;
|
||||||
|
|
||||||
|
|
@ -76,12 +81,28 @@ protected:
|
||||||
using NsToCNTPCTRatio = std::ratio<CNTFRQ, std::nano::den>;
|
using NsToCNTPCTRatio = std::ratio<CNTFRQ, std::nano::den>;
|
||||||
using NsToGPUTickRatio = std::ratio<GPUTickFreq, std::nano::den>;
|
using NsToGPUTickRatio = std::ratio<GPUTickFreq, std::nano::den>;
|
||||||
|
|
||||||
// Cycle Timing
|
// Cycle Timing - using functions for dynamic values
|
||||||
|
|
||||||
using CPUTickToNsRatio = std::ratio<std::nano::den, CPUTickFreq>;
|
// Update these to use functions instead of constexpr
|
||||||
using CPUTickToUsRatio = std::ratio<std::micro::den, CPUTickFreq>;
|
struct CPUTickToNsRatio {
|
||||||
using CPUTickToCNTPCTRatio = std::ratio<CNTFRQ, CPUTickFreq>;
|
static inline std::intmax_t num = std::nano::den;
|
||||||
using CPUTickToGPUTickRatio = std::ratio<GPUTickFreq, CPUTickFreq>;
|
static inline std::intmax_t den = CPUTickFreq();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CPUTickToUsRatio {
|
||||||
|
static inline std::intmax_t num = std::micro::den;
|
||||||
|
static inline std::intmax_t den = CPUTickFreq();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CPUTickToCNTPCTRatio {
|
||||||
|
static inline std::intmax_t num = CNTFRQ;
|
||||||
|
static inline std::intmax_t den = CPUTickFreq();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CPUTickToGPUTickRatio {
|
||||||
|
static inline std::intmax_t num = GPUTickFreq;
|
||||||
|
static inline std::intmax_t den = CPUTickFreq();
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<WallClock> CreateOptimalClock();
|
std::unique_ptr<WallClock> CreateOptimalClock();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
||||||
|
# SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
add_library(core STATIC
|
add_library(core STATIC
|
||||||
|
|
@ -776,8 +777,12 @@ add_library(core STATIC
|
||||||
hle/service/ngc/ngc.h
|
hle/service/ngc/ngc.h
|
||||||
hle/service/nifm/nifm.cpp
|
hle/service/nifm/nifm.cpp
|
||||||
hle/service/nifm/nifm.h
|
hle/service/nifm/nifm.h
|
||||||
|
hle/service/nifm/nifm_utils.cpp
|
||||||
|
hle/service/nifm/nifm_utils.h
|
||||||
hle/service/nim/nim.cpp
|
hle/service/nim/nim.cpp
|
||||||
hle/service/nim/nim.h
|
hle/service/nim/nim.h
|
||||||
|
hle/service/nim/nim_utils.cpp
|
||||||
|
hle/service/nim/nim_utils.h
|
||||||
hle/service/npns/npns.cpp
|
hle/service/npns/npns.cpp
|
||||||
hle/service/npns/npns.h
|
hle/service/npns/npns.h
|
||||||
hle/service/ns/account_proxy_interface.cpp
|
hle/service/ns/account_proxy_interface.cpp
|
||||||
|
|
@ -1045,6 +1050,14 @@ add_library(core STATIC
|
||||||
hle/service/sm/sm_controller.h
|
hle/service/sm/sm_controller.h
|
||||||
hle/service/sockets/bsd.cpp
|
hle/service/sockets/bsd.cpp
|
||||||
hle/service/sockets/bsd.h
|
hle/service/sockets/bsd.h
|
||||||
|
hle/service/sockets/bsd_nu.cpp
|
||||||
|
hle/service/sockets/bsd_nu.h
|
||||||
|
hle/service/sockets/bsdcfg.cpp
|
||||||
|
hle/service/sockets/bsdcfg.h
|
||||||
|
hle/service/sockets/dns_priv.cpp
|
||||||
|
hle/service/sockets/dns_priv.h
|
||||||
|
hle/service/sockets/ethc.cpp
|
||||||
|
hle/service/sockets/ethc.h
|
||||||
hle/service/sockets/nsd.cpp
|
hle/service/sockets/nsd.cpp
|
||||||
hle/service/sockets/nsd.h
|
hle/service/sockets/nsd.h
|
||||||
hle/service/sockets/sfdnsres.cpp
|
hle/service/sockets/sfdnsres.cpp
|
||||||
|
|
@ -1053,6 +1066,8 @@ add_library(core STATIC
|
||||||
hle/service/sockets/sockets.h
|
hle/service/sockets/sockets.h
|
||||||
hle/service/sockets/sockets_translate.cpp
|
hle/service/sockets/sockets_translate.cpp
|
||||||
hle/service/sockets/sockets_translate.h
|
hle/service/sockets/sockets_translate.h
|
||||||
|
hle/service/sockets/socket_utils.cpp
|
||||||
|
hle/service/sockets/socket_utils.h
|
||||||
hle/service/spl/csrng.cpp
|
hle/service/spl/csrng.cpp
|
||||||
hle/service/spl/csrng.h
|
hle/service/spl/csrng.h
|
||||||
hle/service/spl/spl.cpp
|
hle/service/spl/spl.cpp
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -649,13 +648,17 @@ void KeyManager::ReloadKeys() {
|
||||||
|
|
||||||
if (Settings::values.use_dev_keys) {
|
if (Settings::values.use_dev_keys) {
|
||||||
dev_mode = true;
|
dev_mode = true;
|
||||||
|
LoadFromFile(citron_keys_dir / "dev.keys_autogenerated", false);
|
||||||
LoadFromFile(citron_keys_dir / "dev.keys", false);
|
LoadFromFile(citron_keys_dir / "dev.keys", false);
|
||||||
} else {
|
} else {
|
||||||
dev_mode = false;
|
dev_mode = false;
|
||||||
|
LoadFromFile(citron_keys_dir / "prod.keys_autogenerated", false);
|
||||||
LoadFromFile(citron_keys_dir / "prod.keys", false);
|
LoadFromFile(citron_keys_dir / "prod.keys", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LoadFromFile(citron_keys_dir / "title.keys_autogenerated", true);
|
||||||
LoadFromFile(citron_keys_dir / "title.keys", true);
|
LoadFromFile(citron_keys_dir / "title.keys", true);
|
||||||
|
LoadFromFile(citron_keys_dir / "console.keys_autogenerated", false);
|
||||||
LoadFromFile(citron_keys_dir / "console.keys", false);
|
LoadFromFile(citron_keys_dir / "console.keys", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -844,15 +847,87 @@ Key256 KeyManager::GetBISKey(u8 partition_id) const {
|
||||||
template <size_t Size>
|
template <size_t Size>
|
||||||
void KeyManager::WriteKeyToFile(KeyCategory category, std::string_view keyname,
|
void KeyManager::WriteKeyToFile(KeyCategory category, std::string_view keyname,
|
||||||
const std::array<u8, Size>& key) {
|
const std::array<u8, Size>& key) {
|
||||||
// Function is now a no-op - keys are no longer written to autogenerated files
|
const auto citron_keys_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::KeysDir);
|
||||||
|
|
||||||
|
std::string filename = "title.keys_autogenerated";
|
||||||
|
|
||||||
|
if (category == KeyCategory::Standard) {
|
||||||
|
filename = dev_mode ? "dev.keys_autogenerated" : "prod.keys_autogenerated";
|
||||||
|
} else if (category == KeyCategory::Console) {
|
||||||
|
filename = "console.keys_autogenerated";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto path = citron_keys_dir / filename;
|
||||||
|
const auto add_info_text = !Common::FS::Exists(path);
|
||||||
|
|
||||||
|
Common::FS::IOFile file{path, Common::FS::FileAccessMode::Append,
|
||||||
|
Common::FS::FileType::TextFile};
|
||||||
|
|
||||||
|
if (!file.IsOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (add_info_text) {
|
||||||
|
void(file.WriteString(
|
||||||
|
"# This file is autogenerated by Citron\n"
|
||||||
|
"# It serves to store keys that were automatically generated from the normal keys\n"
|
||||||
|
"# If you are experiencing issues involving keys, it may help to delete this file\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void(file.WriteString(fmt::format("\n{} = {}", keyname, Common::HexToString(key))));
|
||||||
|
LoadFromFile(path, category == KeyCategory::Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyManager::SetKey(S128KeyType id, Key128 key, u64 field1, u64 field2) {
|
void KeyManager::SetKey(S128KeyType id, Key128 key, u64 field1, u64 field2) {
|
||||||
if (s128_keys.find({id, field1, field2}) != s128_keys.end() || key == Key128{}) {
|
if (s128_keys.find({id, field1, field2}) != s128_keys.end() || key == Key128{}) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (id == S128KeyType::Titlekey) {
|
||||||
|
Key128 rights_id;
|
||||||
|
std::memcpy(rights_id.data(), &field2, sizeof(u64));
|
||||||
|
std::memcpy(rights_id.data() + sizeof(u64), &field1, sizeof(u64));
|
||||||
|
WriteKeyToFile(KeyCategory::Title, Common::HexToString(rights_id), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto category = KeyCategory::Standard;
|
||||||
|
if (id == S128KeyType::Keyblob || id == S128KeyType::KeyblobMAC || id == S128KeyType::TSEC ||
|
||||||
|
id == S128KeyType::SecureBoot || id == S128KeyType::SDSeed || id == S128KeyType::BIS) {
|
||||||
|
category = KeyCategory::Console;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto iter2 = std::find_if(
|
||||||
|
s128_file_id.begin(), s128_file_id.end(), [&id, &field1, &field2](const auto& elem) {
|
||||||
|
return std::tie(elem.second.type, elem.second.field1, elem.second.field2) ==
|
||||||
|
std::tie(id, field1, field2);
|
||||||
|
});
|
||||||
|
if (iter2 != s128_file_id.end()) {
|
||||||
|
WriteKeyToFile(category, iter2->first, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variable cases
|
||||||
|
if (id == S128KeyType::KeyArea) {
|
||||||
|
static constexpr std::array<const char*, 3> kak_names = {
|
||||||
|
"key_area_key_application_{:02X}",
|
||||||
|
"key_area_key_ocean_{:02X}",
|
||||||
|
"key_area_key_system_{:02X}",
|
||||||
|
};
|
||||||
|
WriteKeyToFile(category, fmt::format(fmt::runtime(kak_names.at(field2)), field1), key);
|
||||||
|
} else if (id == S128KeyType::Master) {
|
||||||
|
WriteKeyToFile(category, fmt::format("master_key_{:02X}", field1), key);
|
||||||
|
} else if (id == S128KeyType::Package1) {
|
||||||
|
WriteKeyToFile(category, fmt::format("package1_key_{:02X}", field1), key);
|
||||||
|
} else if (id == S128KeyType::Package2) {
|
||||||
|
WriteKeyToFile(category, fmt::format("package2_key_{:02X}", field1), key);
|
||||||
|
} else if (id == S128KeyType::Titlekek) {
|
||||||
|
WriteKeyToFile(category, fmt::format("titlekek_{:02X}", field1), key);
|
||||||
|
} else if (id == S128KeyType::Keyblob) {
|
||||||
|
WriteKeyToFile(category, fmt::format("keyblob_key_{:02X}", field1), key);
|
||||||
|
} else if (id == S128KeyType::KeyblobMAC) {
|
||||||
|
WriteKeyToFile(category, fmt::format("keyblob_mac_key_{:02X}", field1), key);
|
||||||
|
} else if (id == S128KeyType::Source && field1 == static_cast<u64>(SourceKeyType::Keyblob)) {
|
||||||
|
WriteKeyToFile(category, fmt::format("keyblob_key_source_{:02X}", field2), key);
|
||||||
|
}
|
||||||
|
|
||||||
// Store the key in memory but don't write to file
|
|
||||||
s128_keys[{id, field1, field2}] = key;
|
s128_keys[{id, field1, field2}] = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -860,8 +935,14 @@ void KeyManager::SetKey(S256KeyType id, Key256 key, u64 field1, u64 field2) {
|
||||||
if (s256_keys.find({id, field1, field2}) != s256_keys.end() || key == Key256{}) {
|
if (s256_keys.find({id, field1, field2}) != s256_keys.end() || key == Key256{}) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const auto iter = std::find_if(
|
||||||
// Store the key in memory but don't write to file
|
s256_file_id.begin(), s256_file_id.end(), [&id, &field1, &field2](const auto& elem) {
|
||||||
|
return std::tie(elem.second.type, elem.second.field1, elem.second.field2) ==
|
||||||
|
std::tie(id, field1, field2);
|
||||||
|
});
|
||||||
|
if (iter != s256_file_id.end()) {
|
||||||
|
WriteKeyToFile(KeyCategory::Standard, iter->first, key);
|
||||||
|
}
|
||||||
s256_keys[{id, field1, field2}] = key;
|
s256_keys[{id, field1, field2}] = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -971,6 +1052,8 @@ void KeyManager::DeriveBase() {
|
||||||
// Decrypt keyblob
|
// Decrypt keyblob
|
||||||
if (keyblobs[i] == std::array<u8, 0x90>{}) {
|
if (keyblobs[i] == std::array<u8, 0x90>{}) {
|
||||||
keyblobs[i] = DecryptKeyblob(encrypted_keyblobs[i], key);
|
keyblobs[i] = DecryptKeyblob(encrypted_keyblobs[i], key);
|
||||||
|
WriteKeyToFile<0x90>(KeyCategory::Console, fmt::format("keyblob_{:02X}", i),
|
||||||
|
keyblobs[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Key128 package1;
|
Key128 package1;
|
||||||
|
|
@ -1100,6 +1183,7 @@ void KeyManager::DeriveETicket(PartitionDataManager& data,
|
||||||
data.DecryptProdInfo(GetBISKey(0));
|
data.DecryptProdInfo(GetBISKey(0));
|
||||||
|
|
||||||
eticket_extended_kek = data.GetETicketExtendedKek();
|
eticket_extended_kek = data.GetETicketExtendedKek();
|
||||||
|
WriteKeyToFile(KeyCategory::Console, "eticket_extended_kek", eticket_extended_kek);
|
||||||
DeriveETicketRSAKey();
|
DeriveETicketRSAKey();
|
||||||
PopulateTickets();
|
PopulateTickets();
|
||||||
}
|
}
|
||||||
|
|
@ -1177,6 +1261,8 @@ void KeyManager::PopulateFromPartitionData(PartitionDataManager& data) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
encrypted_keyblobs[i] = data.GetEncryptedKeyblob(i);
|
encrypted_keyblobs[i] = data.GetEncryptedKeyblob(i);
|
||||||
|
WriteKeyToFile<0xB0>(KeyCategory::Console, fmt::format("encrypted_keyblob_{:02X}", i),
|
||||||
|
encrypted_keyblobs[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
SetKeyWrapped(S128KeyType::Source, data.GetPackage2KeySource(),
|
SetKeyWrapped(S128KeyType::Source, data.GetPackage2KeySource(),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,13 @@
|
||||||
|
|
||||||
#include "common/bit_util.h"
|
#include "common/bit_util.h"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
#include "common/settings.h"
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
|
|
||||||
namespace Hardware {
|
namespace Hardware {
|
||||||
|
|
||||||
constexpr u64 BASE_CLOCK_RATE = 1'785'000'000; // Default CPU Frequency = 1785 MHz
|
inline u64 BASE_CLOCK_RATE() { return Settings::values.cpu_clock_rate.GetValue(); } // Default CPU Frequency set in settings, defaults to 1020 MHz
|
||||||
constexpr u64 CNTFREQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz
|
constexpr u64 CNTFREQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz
|
||||||
constexpr u32 NUM_CPU_CORES = 4; // Number of CPU Cores
|
constexpr u32 NUM_CPU_CORES = 4; // Number of CPU Cores
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -81,8 +82,8 @@ PerformanceConfiguration Controller::GetCurrentPerformanceConfiguration(Performa
|
||||||
|
|
||||||
void Controller::SetClockSpeed(u32 mhz) {
|
void Controller::SetClockSpeed(u32 mhz) {
|
||||||
LOG_DEBUG(Service_APM, "called, mhz={:08X}", mhz);
|
LOG_DEBUG(Service_APM, "called, mhz={:08X}", mhz);
|
||||||
// TODO(DarkLordZach): Actually signal core_timing to change clock speed.
|
// Update the clock rate setting with the provided MHz value (convert to Hz)
|
||||||
// TODO(Rodrigo): Remove [[maybe_unused]] when core_timing is used.
|
Settings::values.cpu_clock_rate = mhz * 1'000'000;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Service::APM
|
} // namespace Service::APM
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
|
@ -28,12 +29,12 @@ public:
|
||||||
{10102, nullptr, "UpdateFriendInfo"},
|
{10102, nullptr, "UpdateFriendInfo"},
|
||||||
{10110, nullptr, "GetFriendProfileImage"},
|
{10110, nullptr, "GetFriendProfileImage"},
|
||||||
{10120, &IFriendService::CheckFriendListAvailability, "CheckFriendListAvailability"},
|
{10120, &IFriendService::CheckFriendListAvailability, "CheckFriendListAvailability"},
|
||||||
{10121, nullptr, "EnsureFriendListAvailable"},
|
{10121, &IFriendService::EnsureFriendListAvailable, "EnsureFriendListAvailable"},
|
||||||
{10200, nullptr, "SendFriendRequestForApplication"},
|
{10200, nullptr, "SendFriendRequestForApplication"},
|
||||||
{10211, nullptr, "AddFacedFriendRequestForApplication"},
|
{10211, nullptr, "AddFacedFriendRequestForApplication"},
|
||||||
{10400, &IFriendService::GetBlockedUserListIds, "GetBlockedUserListIds"},
|
{10400, &IFriendService::GetBlockedUserListIds, "GetBlockedUserListIds"},
|
||||||
{10420, &IFriendService::CheckBlockedUserListAvailability, "CheckBlockedUserListAvailability"},
|
{10420, &IFriendService::CheckBlockedUserListAvailability, "CheckBlockedUserListAvailability"},
|
||||||
{10421, nullptr, "EnsureBlockedUserListAvailable"},
|
{10421, &IFriendService::EnsureBlockedUserListAvailable, "EnsureBlockedUserListAvailable"},
|
||||||
{10500, nullptr, "GetProfileList"},
|
{10500, nullptr, "GetProfileList"},
|
||||||
{10600, nullptr, "DeclareOpenOnlinePlaySession"},
|
{10600, nullptr, "DeclareOpenOnlinePlaySession"},
|
||||||
{10601, &IFriendService::DeclareCloseOnlinePlaySession, "DeclareCloseOnlinePlaySession"},
|
{10601, &IFriendService::DeclareCloseOnlinePlaySession, "DeclareCloseOnlinePlaySession"},
|
||||||
|
|
@ -166,11 +167,27 @@ private:
|
||||||
|
|
||||||
LOG_WARNING(Service_Friend, "(STUBBED) called, uuid=0x{}", uuid.RawString());
|
LOG_WARNING(Service_Friend, "(STUBBED) called, uuid=0x{}", uuid.RawString());
|
||||||
|
|
||||||
|
// Signal the completion event to unblock any waiting threads
|
||||||
|
completion_event->Signal();
|
||||||
|
|
||||||
IPC::ResponseBuilder rb{ctx, 3};
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
rb.Push(true);
|
rb.Push(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EnsureFriendListAvailable(HLERequestContext& ctx) {
|
||||||
|
IPC::RequestParser rp{ctx};
|
||||||
|
const auto uuid{rp.PopRaw<Common::UUID>()};
|
||||||
|
|
||||||
|
LOG_WARNING(Service_Friend, "(STUBBED) EnsureFriendListAvailable called, uuid=0x{}", uuid.RawString());
|
||||||
|
|
||||||
|
// Signal the completion event to unblock any waiting threads
|
||||||
|
completion_event->Signal();
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
void GetBlockedUserListIds(HLERequestContext& ctx) {
|
void GetBlockedUserListIds(HLERequestContext& ctx) {
|
||||||
// This is safe to stub, as there should be no adverse consequences from reporting no
|
// This is safe to stub, as there should be no adverse consequences from reporting no
|
||||||
// blocked users.
|
// blocked users.
|
||||||
|
|
@ -186,11 +203,45 @@ private:
|
||||||
|
|
||||||
LOG_WARNING(Service_Friend, "(STUBBED) called, uuid=0x{}", uuid.RawString());
|
LOG_WARNING(Service_Friend, "(STUBBED) called, uuid=0x{}", uuid.RawString());
|
||||||
|
|
||||||
|
// Signal the completion event to unblock any waiting threads
|
||||||
|
completion_event->Signal();
|
||||||
|
|
||||||
IPC::ResponseBuilder rb{ctx, 3};
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
rb.Push(true);
|
rb.Push(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EnsureBlockedUserListAvailable(HLERequestContext& ctx) {
|
||||||
|
IPC::RequestParser rp{ctx};
|
||||||
|
const auto uuid{rp.PopRaw<Common::UUID>()};
|
||||||
|
|
||||||
|
LOG_WARNING(Service_Friend, "(STUBBED) EnsureBlockedUserListAvailable called, uuid=0x{}", uuid.RawString());
|
||||||
|
|
||||||
|
// Signal the completion event to unblock any waiting threads
|
||||||
|
completion_event->Signal();
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetReceivedFriendInvitationCountCache(HLERequestContext& ctx) {
|
||||||
|
LOG_DEBUG(Service_Friend, "(STUBBED) called, check in out");
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.Push(0); // Zero invitations
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cancel(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service_Friend, "Cancel called - returning immediately");
|
||||||
|
|
||||||
|
// Signal the completion event to unblock any waiting threads
|
||||||
|
completion_event->Signal();
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
void DeclareCloseOnlinePlaySession(HLERequestContext& ctx) {
|
void DeclareCloseOnlinePlaySession(HLERequestContext& ctx) {
|
||||||
// Stub used by Splatoon 2
|
// Stub used by Splatoon 2
|
||||||
LOG_WARNING(Service_Friend, "(STUBBED) called");
|
LOG_WARNING(Service_Friend, "(STUBBED) called");
|
||||||
|
|
@ -248,14 +299,6 @@ private:
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetReceivedFriendInvitationCountCache(HLERequestContext& ctx) {
|
|
||||||
LOG_DEBUG(Service_Friend, "(STUBBED) called, check in out");
|
|
||||||
|
|
||||||
IPC::ResponseBuilder rb{ctx, 3};
|
|
||||||
rb.Push(ResultSuccess);
|
|
||||||
rb.Push(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
KernelHelpers::ServiceContext service_context;
|
KernelHelpers::ServiceContext service_context;
|
||||||
|
|
||||||
Kernel::KEvent* completion_event;
|
Kernel::KEvent* completion_event;
|
||||||
|
|
@ -287,6 +330,9 @@ private:
|
||||||
void GetEvent(HLERequestContext& ctx) {
|
void GetEvent(HLERequestContext& ctx) {
|
||||||
LOG_DEBUG(Service_Friend, "called");
|
LOG_DEBUG(Service_Friend, "called");
|
||||||
|
|
||||||
|
// Signal the notification event to unblock any waiting threads
|
||||||
|
notification_event->Signal();
|
||||||
|
|
||||||
IPC::ResponseBuilder rb{ctx, 2, 1};
|
IPC::ResponseBuilder rb{ctx, 2, 1};
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
rb.PushCopyObjects(notification_event->GetReadableEvent());
|
rb.PushCopyObjects(notification_event->GetReadableEvent());
|
||||||
|
|
@ -363,10 +409,11 @@ private:
|
||||||
};
|
};
|
||||||
|
|
||||||
void Module::Interface::CreateFriendService(HLERequestContext& ctx) {
|
void Module::Interface::CreateFriendService(HLERequestContext& ctx) {
|
||||||
|
LOG_DEBUG(Service_Friend, "CreateFriendService called");
|
||||||
|
|
||||||
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
|
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
rb.PushIpcInterface<IFriendService>(system);
|
rb.PushIpcInterface<IFriendService>(system);
|
||||||
LOG_DEBUG(Service_Friend, "called");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Module::Interface::CreateNotificationService(HLERequestContext& ctx) {
|
void Module::Interface::CreateNotificationService(HLERequestContext& ctx) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
|
|
@ -6,6 +7,7 @@
|
||||||
#include "core/hle/service/ipc_helpers.h"
|
#include "core/hle/service/ipc_helpers.h"
|
||||||
#include "core/hle/service/kernel_helpers.h"
|
#include "core/hle/service/kernel_helpers.h"
|
||||||
#include "core/hle/service/nifm/nifm.h"
|
#include "core/hle/service/nifm/nifm.h"
|
||||||
|
#include "core/hle/service/nifm/nifm_utils.h"
|
||||||
#include "core/hle/service/server_manager.h"
|
#include "core/hle/service/server_manager.h"
|
||||||
#include "network/network.h"
|
#include "network/network.h"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/hle/service/nifm/nifm_utils.h"
|
||||||
|
|
||||||
|
namespace Service::NIFM::nn::nifm {
|
||||||
|
|
||||||
|
// Simple implementation to track network requests
|
||||||
|
namespace {
|
||||||
|
std::mutex g_request_mutex;
|
||||||
|
std::map<u32, NetworkRequest> g_requests;
|
||||||
|
u32 g_next_request_id = 1;
|
||||||
|
bool g_network_available = true; // Default to true for emulation
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsNetworkAvailable() {
|
||||||
|
// For emulation purposes, we'll just return the mocked availability
|
||||||
|
std::lock_guard lock(g_request_mutex);
|
||||||
|
return g_network_available;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 SubmitNetworkRequest() {
|
||||||
|
std::lock_guard lock(g_request_mutex);
|
||||||
|
|
||||||
|
if (!g_network_available) {
|
||||||
|
LOG_WARNING(Service_NIFM, "Network request submitted but network is not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 request_id = g_next_request_id++;
|
||||||
|
|
||||||
|
NetworkRequest request{
|
||||||
|
.request_id = request_id,
|
||||||
|
.is_pending = true,
|
||||||
|
.result = NetworkRequestResult::Success // Assume immediate success for emulation
|
||||||
|
};
|
||||||
|
|
||||||
|
g_requests[request_id] = request;
|
||||||
|
|
||||||
|
LOG_INFO(Service_NIFM, "Network request submitted with ID: {}", request_id);
|
||||||
|
return request_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkRequestResult GetNetworkRequestResult(u32 request_id) {
|
||||||
|
std::lock_guard lock(g_request_mutex);
|
||||||
|
|
||||||
|
auto it = g_requests.find(request_id);
|
||||||
|
if (it == g_requests.end()) {
|
||||||
|
LOG_ERROR(Service_NIFM, "Tried to get result for invalid request ID: {}", request_id);
|
||||||
|
return NetworkRequestResult::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For emulation, we'll mark the request as no longer pending once the result is checked
|
||||||
|
it->second.is_pending = false;
|
||||||
|
|
||||||
|
return it->second.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CancelNetworkRequest(u32 request_id) {
|
||||||
|
std::lock_guard lock(g_request_mutex);
|
||||||
|
|
||||||
|
auto it = g_requests.find(request_id);
|
||||||
|
if (it == g_requests.end()) {
|
||||||
|
LOG_ERROR(Service_NIFM, "Tried to cancel invalid request ID: {}", request_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!it->second.is_pending) {
|
||||||
|
LOG_WARNING(Service_NIFM, "Tried to cancel a request that is not pending, ID: {}", request_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
it->second.is_pending = false;
|
||||||
|
it->second.result = NetworkRequestResult::Canceled;
|
||||||
|
|
||||||
|
LOG_INFO(Service_NIFM, "Network request canceled with ID: {}", request_id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Service::NIFM::nn::nifm
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace Service::NIFM {
|
||||||
|
|
||||||
|
// Network request result codes
|
||||||
|
enum class NetworkRequestResult {
|
||||||
|
Success = 0,
|
||||||
|
Error = 1,
|
||||||
|
Canceled = 2,
|
||||||
|
Timeout = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Network request structure
|
||||||
|
struct NetworkRequest {
|
||||||
|
u32 request_id;
|
||||||
|
bool is_pending;
|
||||||
|
NetworkRequestResult result;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace nn::nifm {
|
||||||
|
|
||||||
|
// Checks if network connectivity is available
|
||||||
|
bool IsNetworkAvailable();
|
||||||
|
|
||||||
|
// Submits a network connection request
|
||||||
|
// Returns the request ID or 0 if the request failed
|
||||||
|
u32 SubmitNetworkRequest();
|
||||||
|
|
||||||
|
// Gets the status of a network request
|
||||||
|
// Returns the request result
|
||||||
|
NetworkRequestResult GetNetworkRequestResult(u32 request_id);
|
||||||
|
|
||||||
|
// Cancels a pending network request
|
||||||
|
// Returns true if the request was successfully canceled
|
||||||
|
bool CancelNetworkRequest(u32 request_id);
|
||||||
|
|
||||||
|
} // namespace nn::nifm
|
||||||
|
|
||||||
|
} // namespace Service::NIFM
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
|
@ -8,6 +9,7 @@
|
||||||
#include "core/hle/service/ipc_helpers.h"
|
#include "core/hle/service/ipc_helpers.h"
|
||||||
#include "core/hle/service/kernel_helpers.h"
|
#include "core/hle/service/kernel_helpers.h"
|
||||||
#include "core/hle/service/nim/nim.h"
|
#include "core/hle/service/nim/nim.h"
|
||||||
|
#include "core/hle/service/nim/nim_utils.h"
|
||||||
#include "core/hle/service/server_manager.h"
|
#include "core/hle/service/server_manager.h"
|
||||||
#include "core/hle/service/service.h"
|
#include "core/hle/service/service.h"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/hle/service/nim/nim_utils.h"
|
||||||
|
|
||||||
|
namespace Service::NIM::nn::nim {
|
||||||
|
|
||||||
|
// Simple implementation to track installation tasks
|
||||||
|
namespace {
|
||||||
|
std::mutex g_task_mutex;
|
||||||
|
std::map<u64, Task> g_tasks;
|
||||||
|
u64 g_next_task_id = 1;
|
||||||
|
bool g_service_available = true; // Default to true for emulation
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsServiceAvailable() {
|
||||||
|
std::lock_guard lock(g_task_mutex);
|
||||||
|
return g_service_available;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 CreateInstallTask(u64 application_id) {
|
||||||
|
std::lock_guard lock(g_task_mutex);
|
||||||
|
|
||||||
|
if (!g_service_available) {
|
||||||
|
LOG_WARNING(Service_NIM, "Installation task creation attempted but service is not available");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 task_id = g_next_task_id++;
|
||||||
|
|
||||||
|
Task task{
|
||||||
|
.task_id = task_id,
|
||||||
|
.progress = {
|
||||||
|
.downloaded_bytes = 0,
|
||||||
|
.total_bytes = 1'000'000'000, // Fake 1GB download size
|
||||||
|
.status = TaskStatus::None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
g_tasks[task_id] = task;
|
||||||
|
|
||||||
|
LOG_INFO(Service_NIM, "Installation task created for application 0x{:016X} with ID: {}",
|
||||||
|
application_id, task_id);
|
||||||
|
return task_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskProgress GetTaskProgress(u64 task_id) {
|
||||||
|
std::lock_guard lock(g_task_mutex);
|
||||||
|
|
||||||
|
auto it = g_tasks.find(task_id);
|
||||||
|
if (it == g_tasks.end()) {
|
||||||
|
LOG_ERROR(Service_NIM, "Tried to get progress for invalid task ID: {}", task_id);
|
||||||
|
return {0, 0, TaskStatus::Failed};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If task is in download state, simulate progress
|
||||||
|
if (it->second.progress.status == TaskStatus::Downloading) {
|
||||||
|
// Simulate download progress (add 10% of total size)
|
||||||
|
auto& progress = it->second.progress;
|
||||||
|
const u64 increment = progress.total_bytes / 10;
|
||||||
|
|
||||||
|
progress.downloaded_bytes += increment;
|
||||||
|
if (progress.downloaded_bytes >= progress.total_bytes) {
|
||||||
|
progress.downloaded_bytes = progress.total_bytes;
|
||||||
|
progress.status = TaskStatus::Installing;
|
||||||
|
LOG_INFO(Service_NIM, "Task ID {} download complete, now installing", task_id);
|
||||||
|
}
|
||||||
|
} else if (it->second.progress.status == TaskStatus::Installing) {
|
||||||
|
// Simulate installation completion
|
||||||
|
it->second.progress.status = TaskStatus::Complete;
|
||||||
|
LOG_INFO(Service_NIM, "Task ID {} installation complete", task_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return it->second.progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StartInstallTask(u64 task_id) {
|
||||||
|
std::lock_guard lock(g_task_mutex);
|
||||||
|
|
||||||
|
auto it = g_tasks.find(task_id);
|
||||||
|
if (it == g_tasks.end()) {
|
||||||
|
LOG_ERROR(Service_NIM, "Tried to start invalid task ID: {}", task_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it->second.progress.status != TaskStatus::None &&
|
||||||
|
it->second.progress.status != TaskStatus::Pending) {
|
||||||
|
LOG_WARNING(Service_NIM, "Tried to start task ID {} which is already in progress", task_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
it->second.progress.status = TaskStatus::Downloading;
|
||||||
|
LOG_INFO(Service_NIM, "Started installation task ID: {}", task_id);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CancelInstallTask(u64 task_id) {
|
||||||
|
std::lock_guard lock(g_task_mutex);
|
||||||
|
|
||||||
|
auto it = g_tasks.find(task_id);
|
||||||
|
if (it == g_tasks.end()) {
|
||||||
|
LOG_ERROR(Service_NIM, "Tried to cancel invalid task ID: {}", task_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it->second.progress.status == TaskStatus::Complete ||
|
||||||
|
it->second.progress.status == TaskStatus::Failed ||
|
||||||
|
it->second.progress.status == TaskStatus::Canceled) {
|
||||||
|
LOG_WARNING(Service_NIM, "Tried to cancel task ID {} which is already in a final state", task_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
it->second.progress.status = TaskStatus::Canceled;
|
||||||
|
LOG_INFO(Service_NIM, "Canceled installation task ID: {}", task_id);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Service::NIM::nn::nim
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace Service::NIM {
|
||||||
|
|
||||||
|
// Network installation task status
|
||||||
|
enum class TaskStatus {
|
||||||
|
None = 0,
|
||||||
|
Pending = 1,
|
||||||
|
Downloading = 2,
|
||||||
|
Installing = 3,
|
||||||
|
Complete = 4,
|
||||||
|
Failed = 5,
|
||||||
|
Canceled = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Network installation task progress
|
||||||
|
struct TaskProgress {
|
||||||
|
u64 downloaded_bytes;
|
||||||
|
u64 total_bytes;
|
||||||
|
TaskStatus status;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Network installation task
|
||||||
|
struct Task {
|
||||||
|
u64 task_id;
|
||||||
|
TaskProgress progress;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace nn::nim {
|
||||||
|
|
||||||
|
// Checks if the NIM service is available
|
||||||
|
bool IsServiceAvailable();
|
||||||
|
|
||||||
|
// Creates a new installation task
|
||||||
|
// Returns the task ID or 0 if the task creation failed
|
||||||
|
u64 CreateInstallTask(u64 application_id);
|
||||||
|
|
||||||
|
// Gets the progress of an installation task
|
||||||
|
// Returns the task progress
|
||||||
|
TaskProgress GetTaskProgress(u64 task_id);
|
||||||
|
|
||||||
|
// Starts an installation task
|
||||||
|
// Returns true if the task was successfully started
|
||||||
|
bool StartInstallTask(u64 task_id);
|
||||||
|
|
||||||
|
// Cancels an installation task
|
||||||
|
// Returns true if the task was successfully canceled
|
||||||
|
bool CancelInstallTask(u64 task_id);
|
||||||
|
|
||||||
|
} // namespace nn::nim
|
||||||
|
|
||||||
|
} // namespace Service::NIM
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include <boost/container/small_vector.hpp>
|
#include <boost/container/small_vector.hpp>
|
||||||
|
|
||||||
#include "common/microprofile.h"
|
#include "common/microprofile.h"
|
||||||
|
#include "common/settings.h"
|
||||||
#include "core/hle/service/nvdrv/devices/nvdisp_disp0.h"
|
#include "core/hle/service/nvdrv/devices/nvdisp_disp0.h"
|
||||||
#include "core/hle/service/nvnflinger/buffer_item.h"
|
#include "core/hle/service/nvnflinger/buffer_item.h"
|
||||||
#include "core/hle/service/nvnflinger/buffer_item_consumer.h"
|
#include "core/hle/service/nvnflinger/buffer_item_consumer.h"
|
||||||
|
|
@ -21,19 +23,20 @@ s32 NormalizeSwapInterval(f32* out_speed_scale, s32 swap_interval) {
|
||||||
if (out_speed_scale) {
|
if (out_speed_scale) {
|
||||||
*out_speed_scale = 2.f * static_cast<f32>(1 - swap_interval);
|
*out_speed_scale = 2.f * static_cast<f32>(1 - swap_interval);
|
||||||
}
|
}
|
||||||
|
// Only normalize swap_interval to 1 if we're not respecting present interval 0
|
||||||
|
if (swap_interval == 0 && Settings::values.respect_present_interval_zero.GetValue()) {
|
||||||
|
// Keep swap_interval as 0 to allow for unlocked FPS
|
||||||
|
} else {
|
||||||
swap_interval = 1;
|
swap_interval = 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (swap_interval >= 5) {
|
if (swap_interval >= 5) {
|
||||||
// As an extension, treat high swap interval as precise speed control.
|
// As an extension, treat high swap interval as precise speed control.
|
||||||
if (out_speed_scale) {
|
if (out_speed_scale) {
|
||||||
*out_speed_scale = static_cast<f32>(swap_interval) / 100.f;
|
*out_speed_scale = static_cast<f32>(swap_interval) / 100.f;
|
||||||
}
|
}
|
||||||
|
|
||||||
swap_interval = 1;
|
swap_interval = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return swap_interval;
|
return swap_interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
|
@ -479,6 +480,122 @@ void BSD::EventFd(HLERequestContext& ctx) {
|
||||||
BuildErrnoResponse(ctx, Errno::SUCCESS);
|
BuildErrnoResponse(ctx, Errno::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BSD::Sysctl(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
// Return an error if not implemented
|
||||||
|
rb.Push<s32>(-1);
|
||||||
|
rb.PushEnum(Errno::INVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSD::Ioctl(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
// Return an error if not implemented
|
||||||
|
rb.Push<s32>(-1);
|
||||||
|
rb.PushEnum(Errno::INVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSD::ShutdownAllSockets(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.Push<s32>(0);
|
||||||
|
rb.PushEnum(Errno::SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSD::GetResourceStatistics(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.Push<s32>(0);
|
||||||
|
rb.PushEnum(Errno::SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSD::RecvMMsg(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
// Return an error if not implemented
|
||||||
|
rb.Push<s32>(-1);
|
||||||
|
rb.PushEnum(Errno::INVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSD::SendMMsg(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
// Return an error if not implemented
|
||||||
|
rb.Push<s32>(-1);
|
||||||
|
rb.PushEnum(Errno::INVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSD::RegisterResourceStatisticsName(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSD::RegisterClientShared(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSD::GetSocketStatistics(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.Push<s32>(0);
|
||||||
|
rb.PushEnum(Errno::SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSD::NifIoctl(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
// Return an error if not implemented
|
||||||
|
rb.Push<s32>(-1);
|
||||||
|
rb.PushEnum(Errno::INVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSD::SetThreadCoreMask(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSD::GetThreadCoreMask(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSD::SocketExempt(HLERequestContext& ctx) {
|
||||||
|
IPC::RequestParser rp{ctx};
|
||||||
|
const u32 domain = rp.Pop<u32>();
|
||||||
|
const u32 type = rp.Pop<u32>();
|
||||||
|
const u32 protocol = rp.Pop<u32>();
|
||||||
|
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called - domain={} type={} protocol={}", domain, type, protocol);
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.Push<s32>(-1); // Return -1 on exempted socket
|
||||||
|
rb.PushEnum(Errno::SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSD::Open(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
// Return an error if not implemented
|
||||||
|
rb.Push<s32>(-1);
|
||||||
|
rb.PushEnum(Errno::INVAL);
|
||||||
|
}
|
||||||
|
|
||||||
template <typename Work>
|
template <typename Work>
|
||||||
void BSD::ExecuteWork(HLERequestContext& ctx, Work work) {
|
void BSD::ExecuteWork(HLERequestContext& ctx, Work work) {
|
||||||
work.Execute(this);
|
work.Execute(this);
|
||||||
|
|
@ -508,9 +625,9 @@ std::pair<s32, Errno> BSD::SocketImpl(Domain domain, Type type, Protocol protoco
|
||||||
|
|
||||||
LOG_INFO(Service, "New socket fd={}", fd);
|
LOG_INFO(Service, "New socket fd={}", fd);
|
||||||
|
|
||||||
auto room_member = room_network.GetRoomMember().lock();
|
auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
|
||||||
if (room_member && room_member->IsConnected()) {
|
if (room_member && room_member->IsConnected()) {
|
||||||
descriptor.socket = std::make_shared<Network::ProxySocket>(room_network);
|
descriptor.socket = std::make_shared<Network::ProxySocket>(system.GetRoomNetwork());
|
||||||
} else {
|
} else {
|
||||||
descriptor.socket = std::make_shared<Network::Socket>();
|
descriptor.socket = std::make_shared<Network::Socket>();
|
||||||
}
|
}
|
||||||
|
|
@ -960,27 +1077,41 @@ void BSD::BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const noex
|
||||||
}
|
}
|
||||||
|
|
||||||
void BSD::OnProxyPacketReceived(const Network::ProxyPacket& packet) {
|
void BSD::OnProxyPacketReceived(const Network::ProxyPacket& packet) {
|
||||||
|
// Iterate through all file descriptors and pass the packet to each valid socket
|
||||||
for (auto& optional_descriptor : file_descriptors) {
|
for (auto& optional_descriptor : file_descriptors) {
|
||||||
if (!optional_descriptor.has_value()) {
|
if (!optional_descriptor.has_value()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
FileDescriptor& descriptor = *optional_descriptor;
|
FileDescriptor& descriptor = *optional_descriptor;
|
||||||
descriptor.socket.get()->HandleProxyPacket(packet);
|
if (descriptor.socket) {
|
||||||
|
descriptor.socket->HandleProxyPacket(packet);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s32 BSD::Connect(s32 socket, const SockAddrIn& addr) {
|
||||||
|
// Call ConnectImpl directly if possible, or return error
|
||||||
|
|
||||||
|
LOG_INFO(Service_BSD, "nn::socket::Connect called for socket {} with address {}:{}",
|
||||||
|
socket, addr.ip[0], addr.portno);
|
||||||
|
|
||||||
|
// For now, we're assuming the connection will succeed return 0
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
BSD::BSD(Core::System& system_, const char* name)
|
BSD::BSD(Core::System& system_, const char* name)
|
||||||
: ServiceFramework{system_, name}, room_network{system_.GetRoomNetwork()} {
|
: ServiceFramework{system_, name} {
|
||||||
// clang-format off
|
// clang-format off
|
||||||
static const FunctionInfo functions[] = {
|
static const FunctionInfo functions[] = {
|
||||||
{0, &BSD::RegisterClient, "RegisterClient"},
|
{0, &BSD::RegisterClient, "RegisterClient"},
|
||||||
{1, &BSD::StartMonitoring, "StartMonitoring"},
|
{1, &BSD::StartMonitoring, "StartMonitoring"},
|
||||||
{2, &BSD::Socket, "Socket"},
|
{2, &BSD::Socket, "Socket"},
|
||||||
{3, nullptr, "SocketExempt"},
|
{3, &BSD::SocketExempt, "SocketExempt"},
|
||||||
{4, nullptr, "Open"},
|
{4, &BSD::Open, "Open"},
|
||||||
{5, &BSD::Select, "Select"},
|
{5, &BSD::Select, "Select"},
|
||||||
{6, &BSD::Poll, "Poll"},
|
{6, &BSD::Poll, "Poll"},
|
||||||
{7, nullptr, "Sysctl"},
|
{7, &BSD::Sysctl, "Sysctl"},
|
||||||
{8, &BSD::Recv, "Recv"},
|
{8, &BSD::Recv, "Recv"},
|
||||||
{9, &BSD::RecvFrom, "RecvFrom"},
|
{9, &BSD::RecvFrom, "RecvFrom"},
|
||||||
{10, &BSD::Send, "Send"},
|
{10, &BSD::Send, "Send"},
|
||||||
|
|
@ -992,27 +1123,32 @@ BSD::BSD(Core::System& system_, const char* name)
|
||||||
{16, &BSD::GetSockName, "GetSockName"},
|
{16, &BSD::GetSockName, "GetSockName"},
|
||||||
{17, &BSD::GetSockOpt, "GetSockOpt"},
|
{17, &BSD::GetSockOpt, "GetSockOpt"},
|
||||||
{18, &BSD::Listen, "Listen"},
|
{18, &BSD::Listen, "Listen"},
|
||||||
{19, nullptr, "Ioctl"},
|
{19, &BSD::Ioctl, "Ioctl"},
|
||||||
{20, &BSD::Fcntl, "Fcntl"},
|
{20, &BSD::Fcntl, "Fcntl"},
|
||||||
{21, &BSD::SetSockOpt, "SetSockOpt"},
|
{21, &BSD::SetSockOpt, "SetSockOpt"},
|
||||||
{22, &BSD::Shutdown, "Shutdown"},
|
{22, &BSD::Shutdown, "Shutdown"},
|
||||||
{23, nullptr, "ShutdownAllSockets"},
|
{23, &BSD::ShutdownAllSockets, "ShutdownAllSockets"},
|
||||||
{24, &BSD::Write, "Write"},
|
{24, &BSD::Write, "Write"},
|
||||||
{25, &BSD::Read, "Read"},
|
{25, &BSD::Read, "Read"},
|
||||||
{26, &BSD::Close, "Close"},
|
{26, &BSD::Close, "Close"},
|
||||||
{27, &BSD::DuplicateSocket, "DuplicateSocket"},
|
{27, &BSD::DuplicateSocket, "DuplicateSocket"},
|
||||||
{28, nullptr, "GetResourceStatistics"},
|
{28, &BSD::GetResourceStatistics, "GetResourceStatistics"},
|
||||||
{29, nullptr, "RecvMMsg"},
|
{29, &BSD::RecvMMsg, "RecvMMsg"},
|
||||||
{30, nullptr, "SendMMsg"},
|
{30, &BSD::SendMMsg, "SendMMsg"},
|
||||||
{31, &BSD::EventFd, "EventFd"},
|
{31, &BSD::EventFd, "EventFd"},
|
||||||
{32, nullptr, "RegisterResourceStatisticsName"},
|
{32, &BSD::RegisterResourceStatisticsName, "RegisterResourceStatisticsName"},
|
||||||
{33, nullptr, "Initialize2"},
|
{33, &BSD::RegisterClientShared, "RegisterClientShared"},
|
||||||
|
{34, &BSD::GetSocketStatistics, "GetSocketStatistics"},
|
||||||
|
{35, &BSD::NifIoctl, "NifIoctl"},
|
||||||
|
{200, &BSD::SetThreadCoreMask, "SetThreadCoreMask"},
|
||||||
|
{201, &BSD::GetThreadCoreMask, "GetThreadCoreMask"},
|
||||||
};
|
};
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
RegisterHandlers(functions);
|
RegisterHandlers(functions);
|
||||||
|
|
||||||
if (auto room_member = room_network.GetRoomMember().lock()) {
|
auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
|
||||||
|
if (room_member) {
|
||||||
proxy_packet_received = room_member->BindOnProxyPacketReceived(
|
proxy_packet_received = room_member->BindOnProxyPacketReceived(
|
||||||
[this](const Network::ProxyPacket& packet) { OnProxyPacketReceived(packet); });
|
[this](const Network::ProxyPacket& packet) { OnProxyPacketReceived(packet); });
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1021,7 +1157,8 @@ BSD::BSD(Core::System& system_, const char* name)
|
||||||
}
|
}
|
||||||
|
|
||||||
BSD::~BSD() {
|
BSD::~BSD() {
|
||||||
if (auto room_member = room_network.GetRoomMember().lock()) {
|
auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
|
||||||
|
if (room_member) {
|
||||||
room_member->Unbind(proxy_packet_received);
|
room_member->Unbind(proxy_packet_received);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1031,31 +1168,4 @@ std::unique_lock<std::mutex> BSD::LockService() {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
BSDCFG::BSDCFG(Core::System& system_) : ServiceFramework{system_, "bsdcfg"} {
|
|
||||||
// clang-format off
|
|
||||||
static const FunctionInfo functions[] = {
|
|
||||||
{0, nullptr, "SetIfUp"},
|
|
||||||
{1, nullptr, "SetIfUpWithEvent"},
|
|
||||||
{2, nullptr, "CancelIf"},
|
|
||||||
{3, nullptr, "SetIfDown"},
|
|
||||||
{4, nullptr, "GetIfState"},
|
|
||||||
{5, nullptr, "DhcpRenew"},
|
|
||||||
{6, nullptr, "AddStaticArpEntry"},
|
|
||||||
{7, nullptr, "RemoveArpEntry"},
|
|
||||||
{8, nullptr, "LookupArpEntry"},
|
|
||||||
{9, nullptr, "LookupArpEntry2"},
|
|
||||||
{10, nullptr, "ClearArpEntries"},
|
|
||||||
{11, nullptr, "ClearArpEntries2"},
|
|
||||||
{12, nullptr, "PrintArpEntries"},
|
|
||||||
{13, nullptr, "Unknown13"},
|
|
||||||
{14, nullptr, "Unknown14"},
|
|
||||||
{15, nullptr, "Unknown15"},
|
|
||||||
};
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
RegisterHandlers(functions);
|
|
||||||
}
|
|
||||||
|
|
||||||
BSDCFG::~BSDCFG() = default;
|
|
||||||
|
|
||||||
} // namespace Service::Sockets
|
} // namespace Service::Sockets
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -37,6 +38,9 @@ public:
|
||||||
Errno CloseImpl(s32 fd);
|
Errno CloseImpl(s32 fd);
|
||||||
std::optional<std::shared_ptr<Network::SocketBase>> GetSocket(s32 fd);
|
std::optional<std::shared_ptr<Network::SocketBase>> GetSocket(s32 fd);
|
||||||
|
|
||||||
|
// Static function that can be called from nn::socket::Connect
|
||||||
|
static s32 Connect(s32 socket, const SockAddrIn& addr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// Maximum number of file descriptors
|
/// Maximum number of file descriptors
|
||||||
static constexpr size_t MAX_FD = 128;
|
static constexpr size_t MAX_FD = 128;
|
||||||
|
|
@ -124,11 +128,30 @@ private:
|
||||||
Errno bsd_errno{};
|
Errno bsd_errno{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct LibraryConfigData {
|
||||||
|
u32 version;
|
||||||
|
u32 tcp_tx_buf_size;
|
||||||
|
u32 tcp_rx_buf_size;
|
||||||
|
u32 tcp_tx_buf_max_size;
|
||||||
|
u32 tcp_rx_buf_max_size;
|
||||||
|
u32 udp_tx_buf_size;
|
||||||
|
u32 udp_rx_buf_size;
|
||||||
|
u32 sb_efficiency;
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is nn::socket::sf::IClient
|
||||||
void RegisterClient(HLERequestContext& ctx);
|
void RegisterClient(HLERequestContext& ctx);
|
||||||
void StartMonitoring(HLERequestContext& ctx);
|
void StartMonitoring(HLERequestContext& ctx);
|
||||||
void Socket(HLERequestContext& ctx);
|
void Socket(HLERequestContext& ctx);
|
||||||
|
void SocketExempt(HLERequestContext& ctx);
|
||||||
|
void Open(HLERequestContext& ctx);
|
||||||
void Select(HLERequestContext& ctx);
|
void Select(HLERequestContext& ctx);
|
||||||
void Poll(HLERequestContext& ctx);
|
void Poll(HLERequestContext& ctx);
|
||||||
|
void Sysctl(HLERequestContext& ctx);
|
||||||
|
void Recv(HLERequestContext& ctx);
|
||||||
|
void RecvFrom(HLERequestContext& ctx);
|
||||||
|
void Send(HLERequestContext& ctx);
|
||||||
|
void SendTo(HLERequestContext& ctx);
|
||||||
void Accept(HLERequestContext& ctx);
|
void Accept(HLERequestContext& ctx);
|
||||||
void Bind(HLERequestContext& ctx);
|
void Bind(HLERequestContext& ctx);
|
||||||
void Connect(HLERequestContext& ctx);
|
void Connect(HLERequestContext& ctx);
|
||||||
|
|
@ -136,18 +159,25 @@ private:
|
||||||
void GetSockName(HLERequestContext& ctx);
|
void GetSockName(HLERequestContext& ctx);
|
||||||
void GetSockOpt(HLERequestContext& ctx);
|
void GetSockOpt(HLERequestContext& ctx);
|
||||||
void Listen(HLERequestContext& ctx);
|
void Listen(HLERequestContext& ctx);
|
||||||
|
void Ioctl(HLERequestContext& ctx);
|
||||||
void Fcntl(HLERequestContext& ctx);
|
void Fcntl(HLERequestContext& ctx);
|
||||||
void SetSockOpt(HLERequestContext& ctx);
|
void SetSockOpt(HLERequestContext& ctx);
|
||||||
void Shutdown(HLERequestContext& ctx);
|
void Shutdown(HLERequestContext& ctx);
|
||||||
void Recv(HLERequestContext& ctx);
|
void ShutdownAllSockets(HLERequestContext& ctx);
|
||||||
void RecvFrom(HLERequestContext& ctx);
|
|
||||||
void Send(HLERequestContext& ctx);
|
|
||||||
void SendTo(HLERequestContext& ctx);
|
|
||||||
void Write(HLERequestContext& ctx);
|
void Write(HLERequestContext& ctx);
|
||||||
void Read(HLERequestContext& ctx);
|
void Read(HLERequestContext& ctx);
|
||||||
void Close(HLERequestContext& ctx);
|
void Close(HLERequestContext& ctx);
|
||||||
void DuplicateSocket(HLERequestContext& ctx);
|
void DuplicateSocket(HLERequestContext& ctx);
|
||||||
|
void GetResourceStatistics(HLERequestContext& ctx);
|
||||||
|
void RecvMMsg(HLERequestContext& ctx);
|
||||||
|
void SendMMsg(HLERequestContext& ctx);
|
||||||
void EventFd(HLERequestContext& ctx);
|
void EventFd(HLERequestContext& ctx);
|
||||||
|
void RegisterResourceStatisticsName(HLERequestContext& ctx);
|
||||||
|
void RegisterClientShared(HLERequestContext& ctx);
|
||||||
|
void GetSocketStatistics(HLERequestContext& ctx);
|
||||||
|
void NifIoctl(HLERequestContext& ctx);
|
||||||
|
void SetThreadCoreMask(HLERequestContext& ctx);
|
||||||
|
void GetThreadCoreMask(HLERequestContext& ctx);
|
||||||
|
|
||||||
template <typename Work>
|
template <typename Work>
|
||||||
void ExecuteWork(HLERequestContext& ctx, Work work);
|
void ExecuteWork(HLERequestContext& ctx, Work work);
|
||||||
|
|
@ -171,30 +201,23 @@ private:
|
||||||
std::pair<s32, Errno> SendImpl(s32 fd, u32 flags, std::span<const u8> message);
|
std::pair<s32, Errno> SendImpl(s32 fd, u32 flags, std::span<const u8> message);
|
||||||
std::pair<s32, Errno> SendToImpl(s32 fd, u32 flags, std::span<const u8> message,
|
std::pair<s32, Errno> SendToImpl(s32 fd, u32 flags, std::span<const u8> message,
|
||||||
std::span<const u8> addr);
|
std::span<const u8> addr);
|
||||||
|
|
||||||
s32 FindFreeFileDescriptorHandle() noexcept;
|
s32 FindFreeFileDescriptorHandle() noexcept;
|
||||||
bool IsFileDescriptorValid(s32 fd) const noexcept;
|
bool IsFileDescriptorValid(s32 fd) const noexcept;
|
||||||
|
|
||||||
void BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const noexcept;
|
void BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const noexcept;
|
||||||
|
|
||||||
std::array<std::optional<FileDescriptor>, MAX_FD> file_descriptors;
|
|
||||||
|
|
||||||
Network::RoomNetwork& room_network;
|
|
||||||
|
|
||||||
/// Callback to parse and handle a received wifi packet.
|
|
||||||
void OnProxyPacketReceived(const Network::ProxyPacket& packet);
|
void OnProxyPacketReceived(const Network::ProxyPacket& packet);
|
||||||
|
|
||||||
// Callback identifier for the OnProxyPacketReceived event.
|
|
||||||
Network::RoomMember::CallbackHandle<Network::ProxyPacket> proxy_packet_received;
|
Network::RoomMember::CallbackHandle<Network::ProxyPacket> proxy_packet_received;
|
||||||
|
|
||||||
|
/// Mapping of file descriptors to sockets
|
||||||
|
std::array<std::optional<FileDescriptor>, MAX_FD> file_descriptors{};
|
||||||
|
|
||||||
|
/// Mutex to protect file descriptor operations
|
||||||
|
std::mutex mutex;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual std::unique_lock<std::mutex> LockService() override;
|
virtual std::unique_lock<std::mutex> LockService() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
class BSDCFG final : public ServiceFramework<BSDCFG> {
|
|
||||||
public:
|
|
||||||
explicit BSDCFG(Core::System& system_);
|
|
||||||
~BSDCFG() override;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Service::Sockets
|
} // namespace Service::Sockets
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "core/hle/service/ipc_helpers.h"
|
||||||
|
#include "core/hle/service/kernel_helpers.h"
|
||||||
|
#include "core/hle/service/sockets/bsd_nu.h"
|
||||||
|
|
||||||
|
namespace Service::Sockets {
|
||||||
|
|
||||||
|
ISfUserService::ISfUserService(Core::System& system_)
|
||||||
|
: ServiceFramework{system_, "ISfUserService"},
|
||||||
|
service_context{system_, "ISfUserService"} {
|
||||||
|
// clang-format off
|
||||||
|
static const FunctionInfo functions[] = {
|
||||||
|
{0, &ISfUserService::Assign, "Assign"},
|
||||||
|
{128, &ISfUserService::GetUserInfo, "GetUserInfo"},
|
||||||
|
{129, &ISfUserService::GetStateChangedEvent, "GetStateChangedEvent"},
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
RegisterHandlers(functions);
|
||||||
|
}
|
||||||
|
|
||||||
|
ISfUserService::~ISfUserService() = default;
|
||||||
|
|
||||||
|
void ISfUserService::Assign(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.PushIpcInterface<ISfAssignedNetworkInterfaceService>(system);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ISfUserService::GetUserInfo(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ISfUserService::GetStateChangedEvent(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
|
||||||
|
auto* event = service_context.CreateEvent("ISfUserService:StateChanged");
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2, 1};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.PushCopyObjects(event->GetReadableEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
ISfAssignedNetworkInterfaceService::ISfAssignedNetworkInterfaceService(Core::System& system_)
|
||||||
|
: ServiceFramework{system_, "ISfAssignedNetworkInterfaceService"} {
|
||||||
|
// clang-format off
|
||||||
|
static const FunctionInfo functions[] = {
|
||||||
|
{0, &ISfAssignedNetworkInterfaceService::AddSession, "AddSession"},
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
RegisterHandlers(functions);
|
||||||
|
}
|
||||||
|
|
||||||
|
ISfAssignedNetworkInterfaceService::~ISfAssignedNetworkInterfaceService() = default;
|
||||||
|
|
||||||
|
void ISfAssignedNetworkInterfaceService::AddSession(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
BSD_NU::BSD_NU(Core::System& system_) : ServiceFramework{system_, "bsd:nu"} {
|
||||||
|
// clang-format off
|
||||||
|
static const FunctionInfo functions[] = {
|
||||||
|
{0, &BSD_NU::CreateUserService, "CreateUserService"},
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
RegisterHandlers(functions);
|
||||||
|
}
|
||||||
|
|
||||||
|
BSD_NU::~BSD_NU() = default;
|
||||||
|
|
||||||
|
void BSD_NU::CreateUserService(HLERequestContext& ctx) {
|
||||||
|
LOG_DEBUG(Service, "called");
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.PushIpcInterface<ISfUserService>(system);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Service::Sockets
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/hle/service/service.h"
|
||||||
|
#include "core/hle/service/kernel_helpers.h"
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
class System;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Service::Sockets {
|
||||||
|
|
||||||
|
class ISfUserService final : public ServiceFramework<ISfUserService> {
|
||||||
|
public:
|
||||||
|
explicit ISfUserService(Core::System& system_);
|
||||||
|
~ISfUserService() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Assign(HLERequestContext& ctx);
|
||||||
|
void GetUserInfo(HLERequestContext& ctx);
|
||||||
|
void GetStateChangedEvent(HLERequestContext& ctx);
|
||||||
|
|
||||||
|
KernelHelpers::ServiceContext service_context;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ISfAssignedNetworkInterfaceService final : public ServiceFramework<ISfAssignedNetworkInterfaceService> {
|
||||||
|
public:
|
||||||
|
explicit ISfAssignedNetworkInterfaceService(Core::System& system_);
|
||||||
|
~ISfAssignedNetworkInterfaceService() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void AddSession(HLERequestContext& ctx);
|
||||||
|
};
|
||||||
|
|
||||||
|
class BSD_NU final : public ServiceFramework<BSD_NU> {
|
||||||
|
public:
|
||||||
|
explicit BSD_NU(Core::System& system_);
|
||||||
|
~BSD_NU() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void CreateUserService(HLERequestContext& ctx);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Service::Sockets
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "core/hle/service/ipc_helpers.h"
|
||||||
|
#include "core/hle/service/kernel_helpers.h"
|
||||||
|
#include "core/hle/service/sockets/bsdcfg.h"
|
||||||
|
|
||||||
|
namespace Service::Sockets {
|
||||||
|
|
||||||
|
BSDCFG::BSDCFG(Core::System& system_)
|
||||||
|
: ServiceFramework{system_, "bsdcfg"},
|
||||||
|
service_context{system_, "BSDCFG"} {
|
||||||
|
// clang-format off
|
||||||
|
static const FunctionInfo functions[] = {
|
||||||
|
{0, &BSDCFG::SetIfUp, "SetIfUp"},
|
||||||
|
{1, &BSDCFG::SetIfUpWithEvent, "SetIfUpWithEvent"},
|
||||||
|
{2, &BSDCFG::CancelIf, "CancelIf"},
|
||||||
|
{3, &BSDCFG::SetIfDown, "SetIfDown"},
|
||||||
|
{4, &BSDCFG::GetIfState, "GetIfState"},
|
||||||
|
{5, &BSDCFG::DhcpRenew, "DhcpRenew"},
|
||||||
|
{6, &BSDCFG::AddStaticArpEntry, "AddStaticArpEntry"},
|
||||||
|
{7, &BSDCFG::RemoveArpEntry, "RemoveArpEntry"},
|
||||||
|
{8, &BSDCFG::LookupArpEntry, "LookupArpEntry"},
|
||||||
|
{9, &BSDCFG::LookupArpEntry2, "LookupArpEntry2"},
|
||||||
|
{10, &BSDCFG::ClearArpEntries, "ClearArpEntries"},
|
||||||
|
{11, &BSDCFG::ClearArpEntries2, "ClearArpEntries2"},
|
||||||
|
{12, &BSDCFG::PrintArpEntries, "PrintArpEntries"},
|
||||||
|
{13, &BSDCFG::Cmd13, "Unknown13"},
|
||||||
|
{14, &BSDCFG::Cmd14, "Unknown14"},
|
||||||
|
{15, &BSDCFG::Cmd15, "Unknown15"},
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
RegisterHandlers(functions);
|
||||||
|
}
|
||||||
|
|
||||||
|
BSDCFG::~BSDCFG() = default;
|
||||||
|
|
||||||
|
void BSDCFG::SetIfUp(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::SetIfUpWithEvent(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
|
||||||
|
auto* event = service_context.CreateEvent("BSDCFG:SetIfUpWithEvent");
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2, 1};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.PushCopyObjects(event->GetReadableEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::CancelIf(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::SetIfDown(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::GetIfState(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.Push<u32>(1); // Interface is up (stubbed)
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::DhcpRenew(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::AddStaticArpEntry(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::RemoveArpEntry(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::LookupArpEntry(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::LookupArpEntry2(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::ClearArpEntries(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::ClearArpEntries2(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::PrintArpEntries(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::Cmd13(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::Cmd14(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BSDCFG::Cmd15(HLERequestContext& ctx) {
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Service::Sockets
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/hle/service/service.h"
|
||||||
|
#include "core/hle/service/kernel_helpers.h"
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
class System;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Service::Sockets {
|
||||||
|
|
||||||
|
class BSDCFG final : public ServiceFramework<BSDCFG> {
|
||||||
|
public:
|
||||||
|
explicit BSDCFG(Core::System& system_);
|
||||||
|
~BSDCFG() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SetIfUp(HLERequestContext& ctx);
|
||||||
|
void SetIfUpWithEvent(HLERequestContext& ctx);
|
||||||
|
void CancelIf(HLERequestContext& ctx);
|
||||||
|
void SetIfDown(HLERequestContext& ctx);
|
||||||
|
void GetIfState(HLERequestContext& ctx);
|
||||||
|
void DhcpRenew(HLERequestContext& ctx);
|
||||||
|
void AddStaticArpEntry(HLERequestContext& ctx);
|
||||||
|
void RemoveArpEntry(HLERequestContext& ctx);
|
||||||
|
void LookupArpEntry(HLERequestContext& ctx);
|
||||||
|
void LookupArpEntry2(HLERequestContext& ctx);
|
||||||
|
void ClearArpEntries(HLERequestContext& ctx);
|
||||||
|
void ClearArpEntries2(HLERequestContext& ctx);
|
||||||
|
void PrintArpEntries(HLERequestContext& ctx);
|
||||||
|
void Cmd13(HLERequestContext& ctx);
|
||||||
|
void Cmd14(HLERequestContext& ctx);
|
||||||
|
void Cmd15(HLERequestContext& ctx);
|
||||||
|
|
||||||
|
KernelHelpers::ServiceContext service_context;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Service::Sockets
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "core/hle/service/ipc_helpers.h"
|
||||||
|
#include "core/hle/service/sockets/dns_priv.h"
|
||||||
|
|
||||||
|
namespace Service::Sockets {
|
||||||
|
|
||||||
|
DNS_PRIV::DNS_PRIV(Core::System& system_) : ServiceFramework{system_, "dns:priv"} {
|
||||||
|
// dns:priv doesn't have documented commands yet
|
||||||
|
static const FunctionInfo functions[] = {
|
||||||
|
{0, nullptr, "DummyFunction"},
|
||||||
|
};
|
||||||
|
|
||||||
|
RegisterHandlers(functions);
|
||||||
|
}
|
||||||
|
|
||||||
|
DNS_PRIV::~DNS_PRIV() = default;
|
||||||
|
|
||||||
|
} // namespace Service::Sockets
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/hle/service/service.h"
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
class System;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Service::Sockets {
|
||||||
|
|
||||||
|
class DNS_PRIV final : public ServiceFramework<DNS_PRIV> {
|
||||||
|
public:
|
||||||
|
explicit DNS_PRIV(Core::System& system_);
|
||||||
|
~DNS_PRIV() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Service::Sockets
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "core/hle/service/ipc_helpers.h"
|
||||||
|
#include "core/hle/service/sockets/ethc.h"
|
||||||
|
|
||||||
|
namespace Service::Sockets {
|
||||||
|
|
||||||
|
ETHC_C::ETHC_C(Core::System& system_) : ServiceFramework{system_, "ethc:c"} {
|
||||||
|
// ethc:c doesn't have documented commands yet
|
||||||
|
static const FunctionInfo functions[] = {
|
||||||
|
{0, nullptr, "DummyFunction"},
|
||||||
|
};
|
||||||
|
|
||||||
|
RegisterHandlers(functions);
|
||||||
|
}
|
||||||
|
|
||||||
|
ETHC_C::~ETHC_C() = default;
|
||||||
|
|
||||||
|
ETHC_I::ETHC_I(Core::System& system_) : ServiceFramework{system_, "ethc:i"} {
|
||||||
|
// ethc:i doesn't have documented commands yet
|
||||||
|
static const FunctionInfo functions[] = {
|
||||||
|
{0, nullptr, "DummyFunction"},
|
||||||
|
};
|
||||||
|
|
||||||
|
RegisterHandlers(functions);
|
||||||
|
}
|
||||||
|
|
||||||
|
ETHC_I::~ETHC_I() = default;
|
||||||
|
|
||||||
|
} // namespace Service::Sockets
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/hle/service/service.h"
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
class System;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Service::Sockets {
|
||||||
|
|
||||||
|
class ETHC_C final : public ServiceFramework<ETHC_C> {
|
||||||
|
public:
|
||||||
|
explicit ETHC_C(Core::System& system_);
|
||||||
|
~ETHC_C() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ETHC_I final : public ServiceFramework<ETHC_I> {
|
||||||
|
public:
|
||||||
|
explicit ETHC_I(Core::System& system_);
|
||||||
|
~ETHC_I() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Service::Sockets
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <charconv>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/hle/service/sockets/bsd.h"
|
||||||
|
#include "core/hle/service/sockets/socket_utils.h"
|
||||||
|
|
||||||
|
namespace Service::Sockets::nn::socket {
|
||||||
|
|
||||||
|
bool InetAton(const char* ip, in_addr* addr) {
|
||||||
|
if (ip == nullptr || addr == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view ip_view(ip);
|
||||||
|
|
||||||
|
// Count the number of dots to validate IPv4 format
|
||||||
|
size_t dots = std::count(ip_view.begin(), ip_view.end(), '.');
|
||||||
|
if (dots != 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the IP address in standard dotted-decimal notation
|
||||||
|
u32 result = 0;
|
||||||
|
size_t pos = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
size_t next_dot = ip_view.find('.', pos);
|
||||||
|
std::string_view octet_view;
|
||||||
|
|
||||||
|
if (i < 3) {
|
||||||
|
if (next_dot == std::string_view::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
octet_view = ip_view.substr(pos, next_dot - pos);
|
||||||
|
pos = next_dot + 1;
|
||||||
|
} else {
|
||||||
|
octet_view = ip_view.substr(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 octet;
|
||||||
|
auto [ptr, ec] = std::from_chars(octet_view.data(), octet_view.data() + octet_view.size(), octet);
|
||||||
|
if (ec != std::errc() || octet > 255 || (ptr != octet_view.data() + octet_view.size())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = (result << 8) | octet;
|
||||||
|
}
|
||||||
|
|
||||||
|
addr->s_addr = result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
s32 Connect(s32 socket, const sockaddr* addr, u32 addr_len) {
|
||||||
|
if (addr == nullptr || addr_len < sizeof(sockaddr)) {
|
||||||
|
LOG_ERROR(Service_BSD, "Invalid address pointer or length");
|
||||||
|
// Set errno to EINVAL (Invalid argument)
|
||||||
|
errno = static_cast<u32>(Errno::INVAL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a BSD-compliant sockaddr_in from our sockaddr
|
||||||
|
SockAddrIn bsd_addr{};
|
||||||
|
bsd_addr.len = sizeof(SockAddrIn);
|
||||||
|
// Cast explicitly with a mask to ensure valid range conversion
|
||||||
|
bsd_addr.family = static_cast<u8>(addr->sa_family & 0xFF);
|
||||||
|
|
||||||
|
if (addr->sa_family == 2) { // AF_INET
|
||||||
|
const auto* addr_in = reinterpret_cast<const sockaddr_in*>(addr);
|
||||||
|
bsd_addr.portno = addr_in->sin_port;
|
||||||
|
|
||||||
|
// Copy IPv4 address (in network byte order)
|
||||||
|
const u32 ip_addr = addr_in->sin_addr.s_addr;
|
||||||
|
bsd_addr.ip[0] = static_cast<u8>((ip_addr >> 24) & 0xFF);
|
||||||
|
bsd_addr.ip[1] = static_cast<u8>((ip_addr >> 16) & 0xFF);
|
||||||
|
bsd_addr.ip[2] = static_cast<u8>((ip_addr >> 8) & 0xFF);
|
||||||
|
bsd_addr.ip[3] = static_cast<u8>(ip_addr & 0xFF);
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(Service_BSD, "Unsupported address family: {}", addr->sa_family);
|
||||||
|
// Set errno to EAFNOSUPPORT (Address family not supported)
|
||||||
|
errno = static_cast<u32>(Errno::INVAL); // Using INVAL as a substitute for EAFNOSUPPORT
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to the BSD socket implementation
|
||||||
|
return BSD::Connect(socket, bsd_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Service::Sockets::nn::socket
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include <array>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace Service::Sockets {
|
||||||
|
|
||||||
|
// Base socket structures and utilities for nn::socket
|
||||||
|
|
||||||
|
// in_addr struct similar to standard BSD/POSIX implementation
|
||||||
|
struct in_addr {
|
||||||
|
u32 s_addr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// sockaddr struct similar to standard BSD/POSIX implementation
|
||||||
|
struct sockaddr {
|
||||||
|
u16 sa_family;
|
||||||
|
char sa_data[14];
|
||||||
|
};
|
||||||
|
|
||||||
|
// sockaddr_in struct similar to standard BSD/POSIX implementation
|
||||||
|
struct sockaddr_in {
|
||||||
|
u16 sin_family;
|
||||||
|
u16 sin_port;
|
||||||
|
in_addr sin_addr;
|
||||||
|
char sin_zero[8];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Socket configuration data based on LibraryConfigData from switchbrew
|
||||||
|
struct Config {
|
||||||
|
u32 version;
|
||||||
|
u32 tcp_tx_buf_size;
|
||||||
|
u32 tcp_rx_buf_size;
|
||||||
|
u32 tcp_tx_buf_max_size;
|
||||||
|
u32 tcp_rx_buf_max_size;
|
||||||
|
u32 udp_tx_buf_size;
|
||||||
|
u32 udp_rx_buf_size;
|
||||||
|
u32 sb_efficiency;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace nn::socket {
|
||||||
|
|
||||||
|
// InetAton converts an IPv4 address string to an in_addr structure
|
||||||
|
// Returns true on success, false on failure
|
||||||
|
bool InetAton(const char* ip, in_addr* addr);
|
||||||
|
|
||||||
|
// Connect to a remote host
|
||||||
|
// Returns 0 on success, -1 on failure
|
||||||
|
s32 Connect(s32 socket, const sockaddr* addr, u32 addr_len);
|
||||||
|
|
||||||
|
} // namespace nn::socket
|
||||||
|
|
||||||
|
} // namespace Service::Sockets
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include "core/hle/service/server_manager.h"
|
#include "core/hle/service/server_manager.h"
|
||||||
#include "core/hle/service/sockets/bsd.h"
|
#include "core/hle/service/sockets/bsd.h"
|
||||||
|
#include "core/hle/service/sockets/bsd_nu.h"
|
||||||
|
#include "core/hle/service/sockets/bsdcfg.h"
|
||||||
|
#include "core/hle/service/sockets/dns_priv.h"
|
||||||
|
#include "core/hle/service/sockets/ethc.h"
|
||||||
#include "core/hle/service/sockets/nsd.h"
|
#include "core/hle/service/sockets/nsd.h"
|
||||||
#include "core/hle/service/sockets/sfdnsres.h"
|
#include "core/hle/service/sockets/sfdnsres.h"
|
||||||
|
#include "core/hle/service/sockets/socket_utils.h"
|
||||||
#include "core/hle/service/sockets/sockets.h"
|
#include "core/hle/service/sockets/sockets.h"
|
||||||
|
|
||||||
namespace Service::Sockets {
|
namespace Service::Sockets {
|
||||||
|
|
@ -12,12 +18,33 @@ namespace Service::Sockets {
|
||||||
void LoopProcess(Core::System& system) {
|
void LoopProcess(Core::System& system) {
|
||||||
auto server_manager = std::make_unique<ServerManager>(system);
|
auto server_manager = std::make_unique<ServerManager>(system);
|
||||||
|
|
||||||
|
// Register BSD services
|
||||||
server_manager->RegisterNamedService("bsd:s", std::make_shared<BSD>(system, "bsd:s"));
|
server_manager->RegisterNamedService("bsd:s", std::make_shared<BSD>(system, "bsd:s"));
|
||||||
server_manager->RegisterNamedService("bsd:u", std::make_shared<BSD>(system, "bsd:u"));
|
server_manager->RegisterNamedService("bsd:u", std::make_shared<BSD>(system, "bsd:u"));
|
||||||
|
|
||||||
|
// Register BSD:A service for [18.0.0+]
|
||||||
|
server_manager->RegisterNamedService("bsd:a", std::make_shared<BSD>(system, "bsd:a"));
|
||||||
|
|
||||||
|
// Register BSD:NU service for [15.0.0+]
|
||||||
|
server_manager->RegisterNamedService("bsd:nu", std::make_shared<BSD_NU>(system));
|
||||||
|
|
||||||
|
// Register BSDCFG service
|
||||||
server_manager->RegisterNamedService("bsdcfg", std::make_shared<BSDCFG>(system));
|
server_manager->RegisterNamedService("bsdcfg", std::make_shared<BSDCFG>(system));
|
||||||
|
|
||||||
|
// Register NSD services
|
||||||
server_manager->RegisterNamedService("nsd:a", std::make_shared<NSD>(system, "nsd:a"));
|
server_manager->RegisterNamedService("nsd:a", std::make_shared<NSD>(system, "nsd:a"));
|
||||||
server_manager->RegisterNamedService("nsd:u", std::make_shared<NSD>(system, "nsd:u"));
|
server_manager->RegisterNamedService("nsd:u", std::make_shared<NSD>(system, "nsd:u"));
|
||||||
|
|
||||||
|
// Register SFDNSRES service
|
||||||
server_manager->RegisterNamedService("sfdnsres", std::make_shared<SFDNSRES>(system));
|
server_manager->RegisterNamedService("sfdnsres", std::make_shared<SFDNSRES>(system));
|
||||||
|
|
||||||
|
// Register DNS:PRIV service
|
||||||
|
server_manager->RegisterNamedService("dns:priv", std::make_shared<DNS_PRIV>(system));
|
||||||
|
|
||||||
|
// Register ETHC services
|
||||||
|
server_manager->RegisterNamedService("ethc:c", std::make_shared<ETHC_C>(system));
|
||||||
|
server_manager->RegisterNamedService("ethc:i", std::make_shared<ETHC_I>(system));
|
||||||
|
|
||||||
server_manager->StartAdditionalHostThreads("bsdsocket", 2);
|
server_manager->StartAdditionalHostThreads("bsdsocket", 2);
|
||||||
ServerManager::RunServer(std::move(server_manager));
|
ServerManager::RunServer(std::move(server_manager));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -17,9 +18,11 @@ enum class Errno : u32 {
|
||||||
BADF = 9,
|
BADF = 9,
|
||||||
AGAIN = 11,
|
AGAIN = 11,
|
||||||
NOMEM = 12,
|
NOMEM = 12,
|
||||||
|
BUSY = 16,
|
||||||
INVAL = 22,
|
INVAL = 22,
|
||||||
MFILE = 24,
|
MFILE = 24,
|
||||||
PIPE = 32,
|
PIPE = 32,
|
||||||
|
NOTSOCK = 88,
|
||||||
MSGSIZE = 90,
|
MSGSIZE = 90,
|
||||||
CONNABORTED = 103,
|
CONNABORTED = 103,
|
||||||
CONNRESET = 104,
|
CONNRESET = 104,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -39,6 +40,10 @@ Errno Translate(Network::Errno value) {
|
||||||
return Errno::INPROGRESS;
|
return Errno::INPROGRESS;
|
||||||
case Network::Errno::NOMEM:
|
case Network::Errno::NOMEM:
|
||||||
return Errno::NOMEM;
|
return Errno::NOMEM;
|
||||||
|
case Network::Errno::BUSY:
|
||||||
|
return Errno::BUSY;
|
||||||
|
case Network::Errno::NOTSOCK:
|
||||||
|
return Errno::NOTSOCK;
|
||||||
default:
|
default:
|
||||||
UNIMPLEMENTED_MSG("Unimplemented errno={}", value);
|
UNIMPLEMENTED_MSG("Unimplemented errno={}", value);
|
||||||
return Errno::SUCCESS;
|
return Errno::SUCCESS;
|
||||||
|
|
|
||||||
|
|
@ -565,6 +565,7 @@ void LoopProcess(Core::System& system) {
|
||||||
auto server_manager = std::make_unique<ServerManager>(system);
|
auto server_manager = std::make_unique<ServerManager>(system);
|
||||||
|
|
||||||
server_manager->RegisterNamedService("ssl", std::make_shared<ISslService>(system));
|
server_manager->RegisterNamedService("ssl", std::make_shared<ISslService>(system));
|
||||||
|
server_manager->RegisterNamedService("ssl:s", std::make_shared<ISslService>(system));
|
||||||
ServerManager::RunServer(std::move(server_manager));
|
ServerManager::RunServer(std::move(server_manager));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -156,6 +157,8 @@ Errno TranslateNativeError(int e, CallType call_type = CallType::Other) {
|
||||||
return Errno::TIMEDOUT;
|
return Errno::TIMEDOUT;
|
||||||
case WSAEINPROGRESS:
|
case WSAEINPROGRESS:
|
||||||
return Errno::INPROGRESS;
|
return Errno::INPROGRESS;
|
||||||
|
case WSAENOTSOCK:
|
||||||
|
return Errno::NOTSOCK;
|
||||||
default:
|
default:
|
||||||
UNIMPLEMENTED_MSG("Unimplemented errno={}", e);
|
UNIMPLEMENTED_MSG("Unimplemented errno={}", e);
|
||||||
return Errno::OTHER;
|
return Errno::OTHER;
|
||||||
|
|
@ -273,14 +276,14 @@ Errno TranslateNativeError(int e, CallType call_type = CallType::Other) {
|
||||||
return Errno::MFILE;
|
return Errno::MFILE;
|
||||||
case EPIPE:
|
case EPIPE:
|
||||||
return Errno::PIPE;
|
return Errno::PIPE;
|
||||||
case ECONNABORTED:
|
|
||||||
return Errno::CONNABORTED;
|
|
||||||
case ENOTCONN:
|
case ENOTCONN:
|
||||||
return Errno::NOTCONN;
|
return Errno::NOTCONN;
|
||||||
case EAGAIN:
|
case EAGAIN:
|
||||||
return Errno::AGAIN;
|
return Errno::AGAIN;
|
||||||
case ECONNREFUSED:
|
case ECONNREFUSED:
|
||||||
return Errno::CONNREFUSED;
|
return Errno::CONNREFUSED;
|
||||||
|
case ECONNABORTED:
|
||||||
|
return Errno::CONNABORTED;
|
||||||
case ECONNRESET:
|
case ECONNRESET:
|
||||||
return Errno::CONNRESET;
|
return Errno::CONNRESET;
|
||||||
case EHOSTUNREACH:
|
case EHOSTUNREACH:
|
||||||
|
|
@ -295,8 +298,14 @@ Errno TranslateNativeError(int e, CallType call_type = CallType::Other) {
|
||||||
return Errno::TIMEDOUT;
|
return Errno::TIMEDOUT;
|
||||||
case EINPROGRESS:
|
case EINPROGRESS:
|
||||||
return Errno::INPROGRESS;
|
return Errno::INPROGRESS;
|
||||||
|
case ENOMEM:
|
||||||
|
return Errno::NOMEM;
|
||||||
|
case EBUSY:
|
||||||
|
return Errno::BUSY;
|
||||||
|
case ENOTSOCK:
|
||||||
|
return Errno::NOTSOCK;
|
||||||
default:
|
default:
|
||||||
UNIMPLEMENTED_MSG("Unimplemented errno={} ({})", e, strerror(e));
|
UNIMPLEMENTED_MSG("Unimplemented errno={}", e);
|
||||||
return Errno::OTHER;
|
return Errno::OTHER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -315,6 +324,14 @@ Errno GetAndLogLastError(CallType call_type = CallType::Other) {
|
||||||
LOG_DEBUG(Network, "Socket operation error: {}", Common::NativeErrorToString(e));
|
LOG_DEBUG(Network, "Socket operation error: {}", Common::NativeErrorToString(e));
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (err == Errno::NOTSOCK) {
|
||||||
|
// This is a common error when network functionality is not fully implemented
|
||||||
|
LOG_DEBUG(Network, "Socket operation error: An operation was attempted on something that is not a socket. "
|
||||||
|
"This may indicate the game is using network features not fully supported. ");
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
LOG_ERROR(Network, "Socket operation error: {}", Common::NativeErrorToString(e));
|
LOG_ERROR(Network, "Socket operation error: {}", Common::NativeErrorToString(e));
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -47,6 +48,8 @@ enum class Errno {
|
||||||
INPROGRESS,
|
INPROGRESS,
|
||||||
OTHER,
|
OTHER,
|
||||||
NOMEM,
|
NOMEM,
|
||||||
|
BUSY,
|
||||||
|
NOTSOCK,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class GetAddrInfoError {
|
enum class GetAddrInfoError {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
|
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
|
||||||
// SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -909,7 +910,9 @@ struct Memory::Impl {
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
Memory::Memory(Core::System& system_) : system{system_} {
|
Memory::Memory(Core::System& system_) : system(system_), impl(std::make_unique<Impl>(system_)), gen(rd()) {
|
||||||
|
// Initialize the random number distribution
|
||||||
|
dis = std::uniform_int_distribution<u64>(0, std::numeric_limits<u64>::max());
|
||||||
Reset();
|
Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1151,4 +1154,48 @@ bool Memory::InvalidateSeparateHeap(void* fault_address) {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Common::ProcessAddress Memory::GenerateRandomBaseAddress() {
|
||||||
|
u64 random_bits = dis(gen);
|
||||||
|
return Common::ProcessAddress((random_bits & ~NRO_BASE_ADDRESS_RANDOMIZATION_MASK) |
|
||||||
|
(random_bits & NRO_BASE_ADDRESS_RANDOMIZATION_MASK));
|
||||||
|
}
|
||||||
|
|
||||||
|
Memory::MemoryRegion* Memory::FindRegion(Common::ProcessAddress address) {
|
||||||
|
for (auto& entry : memory_regions) {
|
||||||
|
if (address >= entry.second.start_address &&
|
||||||
|
address < entry.second.start_address + entry.second.size) {
|
||||||
|
return &entry.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Memory::MapMemoryRegion(Common::ProcessAddress start_address, u64 size, MemoryRegionType type,
|
||||||
|
bool exec, bool write) {
|
||||||
|
if (start_address + size > EMULATED_MEMORY_SIZE) {
|
||||||
|
LOG_ERROR(HW_Memory, "Memory mapping exceeds emulated memory boundaries at address {:016X}",
|
||||||
|
GetInteger(start_address));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the memory region
|
||||||
|
memory_regions[start_address] = MemoryRegion(start_address, size, type, exec, write);
|
||||||
|
|
||||||
|
// Map the region in the page table
|
||||||
|
Common::MemoryPermission perms{};
|
||||||
|
if (exec) perms |= Common::MemoryPermission::Execute;
|
||||||
|
if (write) perms |= Common::MemoryPermission::Write;
|
||||||
|
perms |= Common::MemoryPermission::Read;
|
||||||
|
|
||||||
|
// Using the MapMemoryRegion method defined in the Impl struct
|
||||||
|
impl->MapMemoryRegion(*impl->current_page_table, start_address, size,
|
||||||
|
Common::PhysicalAddress(GetInteger(start_address)), perms, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Common::ProcessAddress Memory::MapBinary(u64 size) {
|
||||||
|
Common::ProcessAddress base_address = GenerateRandomBaseAddress();
|
||||||
|
MapMemoryRegion(base_address, size, MemoryRegionType::BinaryMemory, true, true);
|
||||||
|
return base_address;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Core::Memory
|
} // namespace Core::Memory
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
|
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -9,6 +10,8 @@
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
#include "common/scratch_buffer.h"
|
#include "common/scratch_buffer.h"
|
||||||
#include "common/typed_address.h"
|
#include "common/typed_address.h"
|
||||||
|
|
@ -43,6 +46,9 @@ constexpr std::size_t CITRON_PAGEBITS = 12;
|
||||||
constexpr u64 CITRON_PAGESIZE = 1ULL << CITRON_PAGEBITS;
|
constexpr u64 CITRON_PAGESIZE = 1ULL << CITRON_PAGEBITS;
|
||||||
constexpr u64 CITRON_PAGEMASK = CITRON_PAGESIZE - 1;
|
constexpr u64 CITRON_PAGEMASK = CITRON_PAGESIZE - 1;
|
||||||
|
|
||||||
|
/// Emulated memory size (4GB)
|
||||||
|
constexpr u64 EMULATED_MEMORY_SIZE = 4ULL * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
/// Virtual user-space memory regions
|
/// Virtual user-space memory regions
|
||||||
enum : u64 {
|
enum : u64 {
|
||||||
/// TLS (Thread-Local Storage) related.
|
/// TLS (Thread-Local Storage) related.
|
||||||
|
|
@ -50,6 +56,18 @@ enum : u64 {
|
||||||
|
|
||||||
/// Application stack
|
/// Application stack
|
||||||
DEFAULT_STACK_SIZE = 0x100000,
|
DEFAULT_STACK_SIZE = 0x100000,
|
||||||
|
|
||||||
|
/// Mask to randomize bits 37-12 for NRO base address
|
||||||
|
NRO_BASE_ADDRESS_RANDOMIZATION_MASK = 0xFFFFFFFFFFFFF000,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Types of memory regions in the system
|
||||||
|
enum class MemoryRegionType {
|
||||||
|
SystemMemory,
|
||||||
|
GraphicsMemory,
|
||||||
|
IOMemory,
|
||||||
|
BinaryMemory,
|
||||||
|
Undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Central class that handles all memory operations and state.
|
/// Central class that handles all memory operations and state.
|
||||||
|
|
@ -64,6 +82,55 @@ public:
|
||||||
Memory(Memory&&) = default;
|
Memory(Memory&&) = default;
|
||||||
Memory& operator=(Memory&&) = delete;
|
Memory& operator=(Memory&&) = delete;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure representing a memory region with its properties
|
||||||
|
*/
|
||||||
|
struct MemoryRegion {
|
||||||
|
Common::ProcessAddress start_address;
|
||||||
|
u64 size;
|
||||||
|
std::unique_ptr<u8[]> data;
|
||||||
|
bool is_mapped;
|
||||||
|
MemoryRegionType type;
|
||||||
|
bool is_executable;
|
||||||
|
bool is_writable;
|
||||||
|
|
||||||
|
// Default constructor needed for STL containers
|
||||||
|
MemoryRegion() : start_address(0), size(0), data(nullptr), is_mapped(false),
|
||||||
|
type(MemoryRegionType::Undefined), is_executable(false), is_writable(false) {}
|
||||||
|
|
||||||
|
MemoryRegion(Common::ProcessAddress start, u64 sz, MemoryRegionType t, bool exec = false, bool write = false)
|
||||||
|
: start_address(start), size(sz), data(std::make_unique<u8[]>(sz)), is_mapped(false),
|
||||||
|
type(t), is_executable(exec), is_writable(write) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a memory region with the specified properties
|
||||||
|
*
|
||||||
|
* @param start_address The starting address of the region
|
||||||
|
* @param size The size of the region in bytes
|
||||||
|
* @param type The type of memory region
|
||||||
|
* @param exec Whether the region is executable
|
||||||
|
* @param write Whether the region is writable
|
||||||
|
*/
|
||||||
|
void MapMemoryRegion(Common::ProcessAddress start_address, u64 size, MemoryRegionType type,
|
||||||
|
bool exec = false, bool write = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a binary with a randomized base address
|
||||||
|
*
|
||||||
|
* @param size The size of the binary in bytes
|
||||||
|
* @returns The base address where the binary was mapped
|
||||||
|
*/
|
||||||
|
Common::ProcessAddress MapBinary(u64 size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a memory region containing the specified address
|
||||||
|
*
|
||||||
|
* @param address The address to search for
|
||||||
|
* @returns Pointer to the memory region if found, nullptr otherwise
|
||||||
|
*/
|
||||||
|
MemoryRegion* FindRegion(Common::ProcessAddress address);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the state of the Memory system.
|
* Resets the state of the Memory system.
|
||||||
*/
|
*/
|
||||||
|
|
@ -497,6 +564,13 @@ private:
|
||||||
|
|
||||||
struct Impl;
|
struct Impl;
|
||||||
std::unique_ptr<Impl> impl;
|
std::unique_ptr<Impl> impl;
|
||||||
|
|
||||||
|
std::unordered_map<Common::ProcessAddress, MemoryRegion> memory_regions;
|
||||||
|
std::random_device rd;
|
||||||
|
std::mt19937 gen;
|
||||||
|
std::uniform_int_distribution<u64> dis;
|
||||||
|
|
||||||
|
Common::ProcessAddress GenerateRandomBaseAddress();
|
||||||
};
|
};
|
||||||
|
|
||||||
template <typename T, GuestMemoryFlags FLAGS>
|
template <typename T, GuestMemoryFlags FLAGS>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
|
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
@ -26,8 +27,8 @@ public:
|
||||||
using clock = std::chrono::system_clock;
|
using clock = std::chrono::system_clock;
|
||||||
|
|
||||||
explicit Socket(const std::string& host, u16 port, SocketCallback callback_)
|
explicit Socket(const std::string& host, u16 port, SocketCallback callback_)
|
||||||
: callback(std::move(callback_)), timer(io_service),
|
: callback(std::move(callback_)), timer(io_context),
|
||||||
socket(io_service, udp::endpoint(udp::v4(), 0)), client_id(GenerateRandomClientId()) {
|
socket(io_context, udp::endpoint(udp::v4(), 0)), client_id(GenerateRandomClientId()) {
|
||||||
boost::system::error_code ec{};
|
boost::system::error_code ec{};
|
||||||
auto ipv4 = boost::asio::ip::make_address_v4(host, ec);
|
auto ipv4 = boost::asio::ip::make_address_v4(host, ec);
|
||||||
if (ec.value() != boost::system::errc::success) {
|
if (ec.value() != boost::system::errc::success) {
|
||||||
|
|
@ -39,11 +40,11 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
void Stop() {
|
void Stop() {
|
||||||
io_service.stop();
|
io_context.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Loop() {
|
void Loop() {
|
||||||
io_service.run();
|
io_context.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
void StartSend(const clock::time_point& from) {
|
void StartSend(const clock::time_point& from) {
|
||||||
|
|
@ -113,7 +114,7 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
SocketCallback callback;
|
SocketCallback callback;
|
||||||
boost::asio::io_service io_service;
|
boost::asio::io_context io_context;
|
||||||
boost::asio::basic_waitable_timer<clock> timer;
|
boost::asio::basic_waitable_timer<clock> timer;
|
||||||
udp::socket socket;
|
udp::socket socket;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
@ -274,8 +275,15 @@ IR::Opcode GlobalToStorage(IR::Opcode opcode) {
|
||||||
|
|
||||||
/// Returns true when a storage buffer address satisfies a bias
|
/// Returns true when a storage buffer address satisfies a bias
|
||||||
bool MeetsBias(const StorageBufferAddr& storage_buffer, const Bias& bias) noexcept {
|
bool MeetsBias(const StorageBufferAddr& storage_buffer, const Bias& bias) noexcept {
|
||||||
return storage_buffer.index == bias.index && storage_buffer.offset >= bias.offset_begin &&
|
// For performance, strongly prefer addresses that meet the bias criteria
|
||||||
storage_buffer.offset < bias.offset_end;
|
// and have optimal alignment
|
||||||
|
if (storage_buffer.index == bias.index &&
|
||||||
|
storage_buffer.offset >= bias.offset_begin &&
|
||||||
|
storage_buffer.offset < bias.offset_end) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Only fall back to other addresses if absolutely necessary
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LowAddrInfo {
|
struct LowAddrInfo {
|
||||||
|
|
@ -351,7 +359,7 @@ std::optional<StorageBufferAddr> Track(const IR::Value& value, const Bias* bias)
|
||||||
.index = index.U32(),
|
.index = index.U32(),
|
||||||
.offset = offset.U32(),
|
.offset = offset.U32(),
|
||||||
};
|
};
|
||||||
const u32 alignment{bias ? bias->alignment : 8U};
|
const u32 alignment{bias ? bias->alignment : 16U};
|
||||||
if (!Common::IsAligned(storage_buffer.offset, alignment)) {
|
if (!Common::IsAligned(storage_buffer.offset, alignment)) {
|
||||||
// The SSBO pointer has to be aligned
|
// The SSBO pointer has to be aligned
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
@ -372,9 +380,9 @@ void CollectStorageBuffers(IR::Block& block, IR::Inst& inst, StorageInfo& info)
|
||||||
// avoid getting false positives
|
// avoid getting false positives
|
||||||
static constexpr Bias nvn_bias{
|
static constexpr Bias nvn_bias{
|
||||||
.index = 0,
|
.index = 0,
|
||||||
.offset_begin = 0x110,
|
.offset_begin = 0x100, // Expanded from 0x110 to catch more potential storage buffers
|
||||||
.offset_end = 0x610,
|
.offset_end = 0x1000, // Substantially expanded to include all TOTK storage buffers
|
||||||
.alignment = 16,
|
.alignment = 32, // Increased from 16 to optimize memory access patterns
|
||||||
};
|
};
|
||||||
// Track the low address of the instruction
|
// Track the low address of the instruction
|
||||||
const std::optional<LowAddrInfo> low_addr_info{TrackLowAddress(&inst)};
|
const std::optional<LowAddrInfo> low_addr_info{TrackLowAddress(&inst)};
|
||||||
|
|
@ -386,14 +394,15 @@ void CollectStorageBuffers(IR::Block& block, IR::Inst& inst, StorageInfo& info)
|
||||||
const IR::U32 low_addr{low_addr_info->value};
|
const IR::U32 low_addr{low_addr_info->value};
|
||||||
std::optional<StorageBufferAddr> storage_buffer{Track(low_addr, &nvn_bias)};
|
std::optional<StorageBufferAddr> storage_buffer{Track(low_addr, &nvn_bias)};
|
||||||
if (!storage_buffer) {
|
if (!storage_buffer) {
|
||||||
// If it fails, track without a bias
|
// If it fails, track without a bias but with higher alignment requirements
|
||||||
|
// for better performance
|
||||||
storage_buffer = Track(low_addr, nullptr);
|
storage_buffer = Track(low_addr, nullptr);
|
||||||
if (!storage_buffer) {
|
if (!storage_buffer) {
|
||||||
// If that also fails, use NVN fallbacks
|
// If that also fails, use NVN fallbacks
|
||||||
LOG_WARNING(Shader, "Storage buffer failed to track, using global memory fallbacks");
|
LOG_WARNING(Shader, "Storage buffer failed to track, using global memory fallbacks");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LOG_WARNING(Shader, "Storage buffer tracked without bias, index {} offset {}",
|
LOG_DEBUG(Shader, "Storage buffer tracked without bias, index {} offset 0x{:X}",
|
||||||
storage_buffer->index, storage_buffer->offset);
|
storage_buffer->index, storage_buffer->offset);
|
||||||
}
|
}
|
||||||
// Collect storage buffer and the instruction
|
// Collect storage buffer and the instruction
|
||||||
|
|
@ -425,8 +434,12 @@ IR::U32 StorageOffset(IR::Block& block, IR::Inst& inst, StorageBufferAddr buffer
|
||||||
IR::U32 low_cbuf{ir.GetCbuf(ir.Imm32(buffer.index), ir.Imm32(buffer.offset))};
|
IR::U32 low_cbuf{ir.GetCbuf(ir.Imm32(buffer.index), ir.Imm32(buffer.offset))};
|
||||||
|
|
||||||
// Align the offset base to match the host alignment requirements
|
// Align the offset base to match the host alignment requirements
|
||||||
|
// Use a more aggressive alignment mask for better performance
|
||||||
low_cbuf = ir.BitwiseAnd(low_cbuf, ir.Imm32(~(alignment - 1U)));
|
low_cbuf = ir.BitwiseAnd(low_cbuf, ir.Imm32(~(alignment - 1U)));
|
||||||
return ir.ISub(offset, low_cbuf);
|
|
||||||
|
// Also align the resulting offset for optimal memory access
|
||||||
|
IR::U32 result = ir.ISub(offset, low_cbuf);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace a global memory load instruction with its storage buffer equivalent
|
/// Replace a global memory load instruction with its storage buffer equivalent
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
|
@ -14,7 +15,7 @@
|
||||||
class FakeCemuhookServer {
|
class FakeCemuhookServer {
|
||||||
public:
|
public:
|
||||||
FakeCemuhookServer()
|
FakeCemuhookServer()
|
||||||
: socket(io_service, boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), 0)) {}
|
: socket(io_context, boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), 0)) {}
|
||||||
|
|
||||||
~FakeCemuhookServer() {
|
~FakeCemuhookServer() {
|
||||||
is_running = false;
|
is_running = false;
|
||||||
|
|
@ -82,7 +83,7 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
boost::asio::io_service io_service;
|
boost::asio::io_context io_context;
|
||||||
boost::asio::ip::udp::socket socket;
|
boost::asio::ip::udp::socket socket;
|
||||||
std::array<u8, InputCommon::CemuhookUDP::MAX_PACKET_SIZE> send_buffer;
|
std::array<u8, InputCommon::CemuhookUDP::MAX_PACKET_SIZE> send_buffer;
|
||||||
std::array<u8, InputCommon::CemuhookUDP::MAX_PACKET_SIZE> receive_buffer;
|
std::array<u8, InputCommon::CemuhookUDP::MAX_PACKET_SIZE> receive_buffer;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
||||||
|
# SPDX-FileCopyrightText: 2025 Citron Emulator Project
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
add_subdirectory(host_shaders)
|
add_subdirectory(host_shaders)
|
||||||
|
|
@ -247,6 +248,8 @@ add_library(video_core STATIC
|
||||||
renderer_vulkan/vk_turbo_mode.h
|
renderer_vulkan/vk_turbo_mode.h
|
||||||
renderer_vulkan/vk_update_descriptor.cpp
|
renderer_vulkan/vk_update_descriptor.cpp
|
||||||
renderer_vulkan/vk_update_descriptor.h
|
renderer_vulkan/vk_update_descriptor.h
|
||||||
|
renderer_vulkan/vk_texture_manager.cpp
|
||||||
|
renderer_vulkan/vk_texture_manager.h
|
||||||
shader_cache.cpp
|
shader_cache.cpp
|
||||||
shader_cache.h
|
shader_cache.h
|
||||||
shader_environment.cpp
|
shader_environment.cpp
|
||||||
|
|
@ -306,6 +309,8 @@ add_library(video_core STATIC
|
||||||
vulkan_common/vulkan_library.h
|
vulkan_common/vulkan_library.h
|
||||||
vulkan_common/vulkan_memory_allocator.cpp
|
vulkan_common/vulkan_memory_allocator.cpp
|
||||||
vulkan_common/vulkan_memory_allocator.h
|
vulkan_common/vulkan_memory_allocator.h
|
||||||
|
vulkan_common/hybrid_memory.cpp
|
||||||
|
vulkan_common/hybrid_memory.h
|
||||||
vulkan_common/vulkan_surface.cpp
|
vulkan_common/vulkan_surface.cpp
|
||||||
vulkan_common/vulkan_surface.h
|
vulkan_common/vulkan_surface.h
|
||||||
vulkan_common/vulkan_wrapper.cpp
|
vulkan_common/vulkan_wrapper.cpp
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -6,6 +7,7 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "common/range_sets.inc"
|
#include "common/range_sets.inc"
|
||||||
#include "video_core/buffer_cache/buffer_cache_base.h"
|
#include "video_core/buffer_cache/buffer_cache_base.h"
|
||||||
|
|
@ -18,7 +20,7 @@ using Core::DEVICE_PAGESIZE;
|
||||||
|
|
||||||
template <class P>
|
template <class P>
|
||||||
BufferCache<P>::BufferCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, Runtime& runtime_)
|
BufferCache<P>::BufferCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, Runtime& runtime_)
|
||||||
: runtime{runtime_}, device_memory{device_memory_}, memory_tracker{device_memory} {
|
: runtime{runtime_}, device_memory{device_memory_}, memory_tracker{device_memory}, immediate_buffer_alloc{} {
|
||||||
// Ensure the first slot is used for the null buffer
|
// Ensure the first slot is used for the null buffer
|
||||||
void(slot_buffers.insert(runtime, NullBufferParams{}));
|
void(slot_buffers.insert(runtime, NullBufferParams{}));
|
||||||
gpu_modified_ranges.Clear();
|
gpu_modified_ranges.Clear();
|
||||||
|
|
@ -1719,8 +1721,31 @@ Binding BufferCache<P>::StorageBufferBinding(GPUVAddr ssbo_addr, u32 cbuf_index,
|
||||||
|
|
||||||
const std::optional<DAddr> aligned_device_addr = gpu_memory->GpuToCpuAddress(aligned_gpu_addr);
|
const std::optional<DAddr> aligned_device_addr = gpu_memory->GpuToCpuAddress(aligned_gpu_addr);
|
||||||
if (!aligned_device_addr || size == 0) {
|
if (!aligned_device_addr || size == 0) {
|
||||||
LOG_WARNING(HW_GPU, "Failed to find storage buffer for cbuf index {}", cbuf_index);
|
// Use a static counter to track and limit warnings
|
||||||
return NULL_BINDING;
|
static std::unordered_map<u32, u32> warning_counts;
|
||||||
|
|
||||||
|
// Increment the warning count for this cbuf_index
|
||||||
|
warning_counts[cbuf_index]++;
|
||||||
|
|
||||||
|
// Only log the first warning for each cbuf_index
|
||||||
|
if (warning_counts[cbuf_index] == 1) {
|
||||||
|
LOG_WARNING(HW_GPU, "Failed to find storage buffer for cbuf index {}. Using fallback.",
|
||||||
|
cbuf_index);
|
||||||
|
} else if (warning_counts[cbuf_index] % 1000 == 0) {
|
||||||
|
// Log occasional reminder warnings
|
||||||
|
LOG_DEBUG(HW_GPU, "Still using fallback for storage buffer cbuf index {} (count: {})",
|
||||||
|
cbuf_index, warning_counts[cbuf_index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a dummy binding with non-zero values to avoid potential crashes
|
||||||
|
static DAddr safe_device_addr = 0x1000;
|
||||||
|
static const u32 safe_size = 16 * 1024; // 16KB should be adequate for most cases
|
||||||
|
|
||||||
|
return Binding{
|
||||||
|
.device_addr = safe_device_addr,
|
||||||
|
.size = safe_size,
|
||||||
|
.buffer_id = const_cast<BufferCache<P>*>(this)->FindBuffer(safe_device_addr, safe_size),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const std::optional<DAddr> device_addr = gpu_memory->GpuToCpuAddress(gpu_addr);
|
const std::optional<DAddr> device_addr = gpu_memory->GpuToCpuAddress(gpu_addr);
|
||||||
ASSERT_MSG(device_addr, "Unaligned storage buffer address not found for cbuf index {}",
|
ASSERT_MSG(device_addr, "Unaligned storage buffer address not found for cbuf index {}",
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <mutex>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
|
||||||
namespace VideoCommon {
|
namespace VideoCommon {
|
||||||
|
|
||||||
|
|
@ -14,18 +17,59 @@ namespace VideoCommon {
|
||||||
template <typename T, size_t TICKS_TO_DESTROY>
|
template <typename T, size_t TICKS_TO_DESTROY>
|
||||||
class DelayedDestructionRing {
|
class DelayedDestructionRing {
|
||||||
public:
|
public:
|
||||||
|
DelayedDestructionRing() = default;
|
||||||
|
~DelayedDestructionRing() {
|
||||||
|
// Ensure all resources are properly released when ring is destroyed
|
||||||
|
for (auto& element_list : elements) {
|
||||||
|
element_list.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Tick() {
|
void Tick() {
|
||||||
|
std::scoped_lock lock{ring_mutex};
|
||||||
|
|
||||||
|
// Move to next position in the ring
|
||||||
index = (index + 1) % TICKS_TO_DESTROY;
|
index = (index + 1) % TICKS_TO_DESTROY;
|
||||||
|
|
||||||
|
// Clear elements at current position, which ensures resources are properly released
|
||||||
|
const size_t count = elements[index].size();
|
||||||
|
if (count > 0) {
|
||||||
|
// If more than a threshold of elements are being destroyed at once, log it
|
||||||
|
if (count > 100) {
|
||||||
|
LOG_DEBUG(Render_Vulkan, "Destroying {} delayed objects", count);
|
||||||
|
}
|
||||||
elements[index].clear();
|
elements[index].clear();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Push(T&& object) {
|
void Push(T&& object) {
|
||||||
|
std::scoped_lock lock{ring_mutex};
|
||||||
elements[index].push_back(std::move(object));
|
elements[index].push_back(std::move(object));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force immediate destruction of all resources (for emergency cleanup)
|
||||||
|
void ForceDestroyAll() {
|
||||||
|
std::scoped_lock lock{ring_mutex};
|
||||||
|
for (auto& element_list : elements) {
|
||||||
|
element_list.clear();
|
||||||
|
}
|
||||||
|
LOG_INFO(Render_Vulkan, "Force destroyed all delayed objects");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current number of pending resources awaiting destruction
|
||||||
|
size_t GetPendingCount() const {
|
||||||
|
std::scoped_lock lock{ring_mutex};
|
||||||
|
size_t count = 0;
|
||||||
|
for (const auto& element_list : elements) {
|
||||||
|
count += element_list.size();
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
size_t index = 0;
|
size_t index = 0;
|
||||||
std::array<std::vector<T>, TICKS_TO_DESTROY> elements;
|
std::array<std::vector<T>, TICKS_TO_DESTROY> elements;
|
||||||
|
mutable std::mutex ring_mutex;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace VideoCommon
|
} // namespace VideoCommon
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <chrono>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
#include "common/settings.h" // for enum class Settings::ShaderBackend
|
#include "common/settings.h" // for enum class Settings::ShaderBackend
|
||||||
#include "common/thread_worker.h"
|
#include "common/thread_worker.h"
|
||||||
|
|
@ -234,26 +237,68 @@ GraphicsPipeline::GraphicsPipeline(const Device& device, TextureCache& texture_c
|
||||||
auto func{[this, sources_ = std::move(sources), sources_spirv_ = std::move(sources_spirv),
|
auto func{[this, sources_ = std::move(sources), sources_spirv_ = std::move(sources_spirv),
|
||||||
shader_notify, backend, in_parallel,
|
shader_notify, backend, in_parallel,
|
||||||
force_context_flush](ShaderContext::Context*) mutable {
|
force_context_flush](ShaderContext::Context*) mutable {
|
||||||
|
// Track time for shader compilation for possible performance tuning
|
||||||
|
const auto start_time = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
|
// Prepare compilation steps for all shader stages
|
||||||
|
std::vector<std::function<void()>> compilation_steps;
|
||||||
|
compilation_steps.reserve(5); // Maximum number of shader stages
|
||||||
|
|
||||||
|
// Prepare all compilation steps first to better distribute work
|
||||||
for (size_t stage = 0; stage < 5; ++stage) {
|
for (size_t stage = 0; stage < 5; ++stage) {
|
||||||
switch (backend) {
|
switch (backend) {
|
||||||
case Settings::ShaderBackend::Glsl:
|
case Settings::ShaderBackend::Glsl:
|
||||||
if (!sources_[stage].empty()) {
|
if (!sources_[stage].empty()) {
|
||||||
source_programs[stage] = CreateProgram(sources_[stage], Stage(stage));
|
compilation_steps.emplace_back([this, stage, source = sources_[stage]]() {
|
||||||
|
source_programs[stage] = CreateProgram(source, Stage(stage));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Settings::ShaderBackend::Glasm:
|
case Settings::ShaderBackend::Glasm:
|
||||||
if (!sources_[stage].empty()) {
|
if (!sources_[stage].empty()) {
|
||||||
assembly_programs[stage] =
|
compilation_steps.emplace_back([this, stage, source = sources_[stage]]() {
|
||||||
CompileProgram(sources_[stage], AssemblyStage(stage));
|
assembly_programs[stage] = CompileProgram(source, AssemblyStage(stage));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Settings::ShaderBackend::SpirV:
|
case Settings::ShaderBackend::SpirV:
|
||||||
if (!sources_spirv_[stage].empty()) {
|
if (!sources_spirv_[stage].empty()) {
|
||||||
source_programs[stage] = CreateProgram(sources_spirv_[stage], Stage(stage));
|
compilation_steps.emplace_back([this, stage, source = sources_spirv_[stage]]() {
|
||||||
|
source_programs[stage] = CreateProgram(source, Stage(stage));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we're running in parallel, use high-priority execution for vertex and fragment shaders
|
||||||
|
// as these are typically needed first by the renderer
|
||||||
|
if (in_parallel && compilation_steps.size() > 1) {
|
||||||
|
// Execute vertex (0) and fragment (4) shaders first if they exist
|
||||||
|
for (size_t priority_stage : {0, 4}) {
|
||||||
|
for (size_t i = 0; i < compilation_steps.size(); ++i) {
|
||||||
|
if ((i == priority_stage || (priority_stage == 0 && i <= 1)) && i < compilation_steps.size()) {
|
||||||
|
compilation_steps[i]();
|
||||||
|
compilation_steps[i] = [](){}; // Mark as executed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute all remaining compilation steps
|
||||||
|
for (auto& step : compilation_steps) {
|
||||||
|
step(); // Will do nothing for already executed steps
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance measurement for possible logging or optimization
|
||||||
|
const auto end_time = std::chrono::high_resolution_clock::now();
|
||||||
|
const auto compilation_time = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
end_time - start_time).count();
|
||||||
|
|
||||||
|
if (compilation_time > 50) { // Only log slow compilations
|
||||||
|
LOG_DEBUG(Render_OpenGL, "Shader compilation took {}ms", compilation_time);
|
||||||
|
}
|
||||||
|
|
||||||
if (force_context_flush || in_parallel) {
|
if (force_context_flush || in_parallel) {
|
||||||
std::scoped_lock lock{built_mutex};
|
std::scoped_lock lock{built_mutex};
|
||||||
built_fence.Create();
|
built_fence.Create();
|
||||||
|
|
@ -623,15 +668,41 @@ void GraphicsPipeline::WaitForBuild() {
|
||||||
is_built = true;
|
is_built = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GraphicsPipeline::IsBuilt() noexcept {
|
bool GraphicsPipeline::IsBuilt() const noexcept {
|
||||||
if (is_built) {
|
if (is_built) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (built_fence.handle == 0) {
|
if (!built_fence.handle) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
is_built = built_fence.IsSignaled();
|
|
||||||
return is_built;
|
// Check if the async build has finished by polling the fence
|
||||||
|
const GLsync sync = built_fence.handle;
|
||||||
|
const GLuint result = glClientWaitSync(sync, 0, 0);
|
||||||
|
if (result == GL_ALREADY_SIGNALED || result == GL_CONDITION_SATISFIED) {
|
||||||
|
// Mark this as mutable even though we're in a const method - this is
|
||||||
|
// essentially a cached value update which is acceptable
|
||||||
|
const_cast<GraphicsPipeline*>(this)->is_built = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For better performance tracking, capture time spent waiting for shaders
|
||||||
|
static thread_local std::chrono::high_resolution_clock::time_point last_shader_wait_log;
|
||||||
|
static thread_local u32 shader_wait_count = 0;
|
||||||
|
|
||||||
|
auto now = std::chrono::high_resolution_clock::now();
|
||||||
|
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
||||||
|
now - last_shader_wait_log).count();
|
||||||
|
|
||||||
|
// Log shader compilation status periodically to help diagnose performance issues
|
||||||
|
if (elapsed >= 5) { // Log every 5 seconds
|
||||||
|
shader_wait_count++;
|
||||||
|
LOG_DEBUG(Render_OpenGL, "Waiting for async shader compilation... (count={})",
|
||||||
|
shader_wait_count);
|
||||||
|
last_shader_wait_log = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace OpenGL
|
} // namespace OpenGL
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -102,7 +103,7 @@ public:
|
||||||
return uses_local_memory;
|
return uses_local_memory;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool IsBuilt() noexcept;
|
[[nodiscard]] bool IsBuilt() const noexcept;
|
||||||
|
|
||||||
template <typename Spec>
|
template <typename Spec>
|
||||||
static auto MakeConfigureSpecFunc() {
|
static auto MakeConfigureSpecFunc() {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
@ -608,9 +609,33 @@ std::unique_ptr<ComputePipeline> ShaderCache::CreateComputePipeline(
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<ShaderWorker> ShaderCache::CreateWorkers() const {
|
std::unique_ptr<ShaderWorker> ShaderCache::CreateWorkers() const {
|
||||||
return std::make_unique<ShaderWorker>(std::max(std::thread::hardware_concurrency(), 2U) - 1,
|
// Calculate optimal number of workers based on available CPU cores
|
||||||
|
// Leave at least 1 core for main thread and other operations
|
||||||
|
// Use more cores for more parallelism in shader compilation
|
||||||
|
const u32 num_worker_threads = std::max(std::thread::hardware_concurrency(), 2U);
|
||||||
|
const u32 optimal_workers = num_worker_threads <= 3 ?
|
||||||
|
num_worker_threads - 1 : // On dual/quad core, leave 1 core free
|
||||||
|
num_worker_threads - 2; // On 6+ core systems, leave 2 cores free for other tasks
|
||||||
|
|
||||||
|
auto worker = std::make_unique<ShaderWorker>(
|
||||||
|
optimal_workers,
|
||||||
"GlShaderBuilder",
|
"GlShaderBuilder",
|
||||||
[this] { return Context{emu_window}; });
|
[this] {
|
||||||
|
auto context = Context{emu_window};
|
||||||
|
|
||||||
|
// Apply thread priority based on settings
|
||||||
|
// This allows users to control how aggressive shader compilation is
|
||||||
|
const int priority = Settings::values.shader_compilation_priority.GetValue();
|
||||||
|
if (priority != 0) {
|
||||||
|
Common::SetCurrentThreadPriority(
|
||||||
|
priority > 0 ? Common::ThreadPriority::High : Common::ThreadPriority::Low);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return worker;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace OpenGL
|
} // namespace OpenGL
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -8,6 +9,8 @@
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <fstream>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
#include <fmt/ranges.h>
|
#include <fmt/ranges.h>
|
||||||
|
|
||||||
|
|
@ -35,6 +38,7 @@
|
||||||
#include "video_core/vulkan_common/vulkan_instance.h"
|
#include "video_core/vulkan_common/vulkan_instance.h"
|
||||||
#include "video_core/vulkan_common/vulkan_library.h"
|
#include "video_core/vulkan_common/vulkan_library.h"
|
||||||
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
|
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
|
||||||
|
#include "video_core/vulkan_common/hybrid_memory.h"
|
||||||
#include "video_core/vulkan_common/vulkan_surface.h"
|
#include "video_core/vulkan_common/vulkan_surface.h"
|
||||||
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
||||||
|
|
||||||
|
|
@ -123,12 +127,93 @@ RendererVulkan::RendererVulkan(Core::TelemetrySession& telemetry_session_,
|
||||||
PresentFiltersForAppletCapture),
|
PresentFiltersForAppletCapture),
|
||||||
rasterizer(render_window, gpu, device_memory, device, memory_allocator, state_tracker,
|
rasterizer(render_window, gpu, device_memory, device, memory_allocator, state_tracker,
|
||||||
scheduler),
|
scheduler),
|
||||||
|
hybrid_memory(std::make_unique<HybridMemory>(device, memory_allocator)),
|
||||||
|
texture_manager(device, memory_allocator),
|
||||||
|
shader_manager(device),
|
||||||
applet_frame() {
|
applet_frame() {
|
||||||
if (Settings::values.renderer_force_max_clock.GetValue() && device.ShouldBoostClocks()) {
|
if (Settings::values.renderer_force_max_clock.GetValue() && device.ShouldBoostClocks()) {
|
||||||
turbo_mode.emplace(instance, dld);
|
turbo_mode.emplace(instance, dld);
|
||||||
scheduler.RegisterOnSubmit([this] { turbo_mode->QueueSubmitted(); });
|
scheduler.RegisterOnSubmit([this] { turbo_mode->QueueSubmitted(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize HybridMemory system
|
||||||
|
if (Settings::values.use_gpu_memory_manager.GetValue()) {
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
|
||||||
|
try {
|
||||||
|
// Define memory size with explicit types to avoid conversion warnings
|
||||||
|
constexpr size_t memory_size_mb = 64;
|
||||||
|
constexpr size_t memory_size_bytes = memory_size_mb * 1024 * 1024;
|
||||||
|
|
||||||
|
void* guest_memory_base = nullptr;
|
||||||
|
#if defined(_WIN32)
|
||||||
|
// On Windows, use VirtualAlloc to reserve (but not commit) memory
|
||||||
|
const SIZE_T win_size = static_cast<SIZE_T>(memory_size_bytes);
|
||||||
|
LPVOID result = VirtualAlloc(nullptr, win_size, MEM_RESERVE, PAGE_NOACCESS);
|
||||||
|
if (result != nullptr) {
|
||||||
|
guest_memory_base = result;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// On Linux/Android, use aligned_alloc
|
||||||
|
guest_memory_base = std::aligned_alloc(4096, memory_size_bytes);
|
||||||
|
#endif
|
||||||
|
if (guest_memory_base != nullptr) {
|
||||||
|
try {
|
||||||
|
hybrid_memory->InitializeGuestMemory(guest_memory_base, memory_size_bytes);
|
||||||
|
LOG_INFO(Render_Vulkan, "HybridMemory initialized with {} MB of fault-managed memory", memory_size_mb);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
if (guest_memory_base != nullptr) {
|
||||||
|
const LPVOID win_ptr = static_cast<LPVOID>(guest_memory_base);
|
||||||
|
VirtualFree(win_ptr, 0, MEM_RELEASE);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
std::free(guest_memory_base);
|
||||||
|
#endif
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Failed to initialize HybridMemory: {}", e.what());
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
LOG_INFO(Render_Vulkan, "Fault-managed memory not supported on this platform");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize enhanced shader compilation system
|
||||||
|
shader_manager.SetScheduler(&scheduler);
|
||||||
|
LOG_INFO(Render_Vulkan, "Enhanced shader compilation system initialized");
|
||||||
|
|
||||||
|
// Preload common shaders if enabled
|
||||||
|
if (Settings::values.use_asynchronous_shaders.GetValue()) {
|
||||||
|
// Use a simple shader directory path - can be updated to match Citron's actual path structure
|
||||||
|
const std::string shader_dir = "./shaders";
|
||||||
|
std::vector<std::string> common_shaders;
|
||||||
|
|
||||||
|
// Add paths to common shaders that should be preloaded
|
||||||
|
// These will be compiled in parallel for faster startup
|
||||||
|
try {
|
||||||
|
if (std::filesystem::exists(shader_dir)) {
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(shader_dir)) {
|
||||||
|
if (entry.is_regular_file() && entry.path().extension() == ".spv") {
|
||||||
|
common_shaders.push_back(entry.path().string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!common_shaders.empty()) {
|
||||||
|
LOG_INFO(Render_Vulkan, "Preloading {} common shaders", common_shaders.size());
|
||||||
|
shader_manager.PreloadShaders(common_shaders);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_INFO(Render_Vulkan, "Shader directory not found at {}", shader_dir);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Error during shader preloading: {}", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Report();
|
Report();
|
||||||
|
InitializePlatformSpecific();
|
||||||
} catch (const vk::Exception& exception) {
|
} catch (const vk::Exception& exception) {
|
||||||
LOG_ERROR(Render_Vulkan, "Vulkan initialization failed with error: {}", exception.what());
|
LOG_ERROR(Render_Vulkan, "Vulkan initialization failed with error: {}", exception.what());
|
||||||
throw std::runtime_error{fmt::format("Vulkan initialization error {}", exception.what())};
|
throw std::runtime_error{fmt::format("Vulkan initialization error {}", exception.what())};
|
||||||
|
|
@ -226,6 +311,35 @@ void RendererVulkan::RenderScreenshot(std::span<const Tegra::FramebufferConfig>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If memory snapshots are enabled, take a snapshot with the screenshot
|
||||||
|
if (Settings::values.enable_memory_snapshots.GetValue() && hybrid_memory) {
|
||||||
|
try {
|
||||||
|
const auto now = std::chrono::system_clock::now();
|
||||||
|
const auto now_time_t = std::chrono::system_clock::to_time_t(now);
|
||||||
|
std::tm local_tm;
|
||||||
|
#ifdef _WIN32
|
||||||
|
localtime_s(&local_tm, &now_time_t);
|
||||||
|
#else
|
||||||
|
localtime_r(&now_time_t, &local_tm);
|
||||||
|
#endif
|
||||||
|
char time_str[128];
|
||||||
|
std::strftime(time_str, sizeof(time_str), "%Y%m%d_%H%M%S", &local_tm);
|
||||||
|
|
||||||
|
std::string snapshot_path = fmt::format("snapshots/memory_snapshot_{}.bin", time_str);
|
||||||
|
hybrid_memory->SaveSnapshot(snapshot_path);
|
||||||
|
|
||||||
|
// Also save a differential snapshot if there's been a previous snapshot
|
||||||
|
if (Settings::values.use_gpu_memory_manager.GetValue()) {
|
||||||
|
std::string diff_path = fmt::format("snapshots/diff_snapshot_{}.bin", time_str);
|
||||||
|
hybrid_memory->SaveDifferentialSnapshot(diff_path);
|
||||||
|
hybrid_memory->ResetDirtyTracking();
|
||||||
|
LOG_INFO(Render_Vulkan, "Memory snapshots saved with screenshot");
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Failed to save memory snapshot: {}", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const auto& layout{renderer_settings.screenshot_framebuffer_layout};
|
const auto& layout{renderer_settings.screenshot_framebuffer_layout};
|
||||||
const auto dst_buffer = RenderToBuffer(framebuffers, layout, VK_FORMAT_B8G8R8A8_UNORM,
|
const auto dst_buffer = RenderToBuffer(framebuffers, layout, VK_FORMAT_B8G8R8A8_UNORM,
|
||||||
layout.width * layout.height * 4);
|
layout.width * layout.height * 4);
|
||||||
|
|
@ -277,4 +391,81 @@ void RendererVulkan::RenderAppletCaptureLayer(
|
||||||
CaptureFormat);
|
CaptureFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RendererVulkan::HandleVulkanError(VkResult result, const std::string& operation) {
|
||||||
|
if (result == VK_SUCCESS) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == VK_ERROR_DEVICE_LOST) {
|
||||||
|
LOG_CRITICAL(Render_Vulkan, "Vulkan device lost during {}", operation);
|
||||||
|
RecoverFromError();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY || result == VK_ERROR_OUT_OF_HOST_MEMORY) {
|
||||||
|
LOG_CRITICAL(Render_Vulkan, "Vulkan out of memory during {}", operation);
|
||||||
|
// Potential recovery: clear caches, reduce workload
|
||||||
|
texture_manager.CleanupTextureCache();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_ERROR(Render_Vulkan, "Vulkan error during {}: {}", operation, result);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RendererVulkan::RecoverFromError() {
|
||||||
|
LOG_INFO(Render_Vulkan, "Attempting to recover from Vulkan error");
|
||||||
|
|
||||||
|
// Wait for device to finish operations
|
||||||
|
void(device.GetLogical().WaitIdle());
|
||||||
|
|
||||||
|
// Process all pending commands in our queue
|
||||||
|
ProcessAllCommands();
|
||||||
|
|
||||||
|
// Wait for any async shader compilations to finish
|
||||||
|
shader_manager.WaitForCompilation();
|
||||||
|
|
||||||
|
// Clean up resources that might be causing problems
|
||||||
|
texture_manager.CleanupTextureCache();
|
||||||
|
|
||||||
|
// Reset command buffers and pipelines
|
||||||
|
scheduler.Flush();
|
||||||
|
|
||||||
|
LOG_INFO(Render_Vulkan, "Recovery attempt completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
void RendererVulkan::InitializePlatformSpecific() {
|
||||||
|
LOG_INFO(Render_Vulkan, "Initializing platform-specific Vulkan components");
|
||||||
|
|
||||||
|
#if defined(_WIN32) || defined(_WIN64)
|
||||||
|
LOG_INFO(Render_Vulkan, "Initializing Vulkan for Windows");
|
||||||
|
// Windows-specific initialization
|
||||||
|
#elif defined(__linux__)
|
||||||
|
LOG_INFO(Render_Vulkan, "Initializing Vulkan for Linux");
|
||||||
|
// Linux-specific initialization
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
LOG_INFO(Render_Vulkan, "Initializing Vulkan for Android");
|
||||||
|
// Android-specific initialization
|
||||||
|
#else
|
||||||
|
LOG_INFO(Render_Vulkan, "Platform-specific Vulkan initialization not implemented for this platform");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Create a compute buffer using the HybridMemory system if enabled
|
||||||
|
if (Settings::values.use_gpu_memory_manager.GetValue()) {
|
||||||
|
try {
|
||||||
|
// Create a small compute buffer for testing
|
||||||
|
const VkDeviceSize buffer_size = 1 * 1024 * 1024; // 1 MB
|
||||||
|
ComputeBuffer compute_buffer = hybrid_memory->CreateComputeBuffer(
|
||||||
|
buffer_size,
|
||||||
|
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
|
||||||
|
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||||||
|
MemoryUsage::DeviceLocal);
|
||||||
|
|
||||||
|
LOG_INFO(Render_Vulkan, "Successfully created compute buffer using HybridMemory");
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Failed to create compute buffer: {}", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Vulkan
|
} // namespace Vulkan
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -6,6 +7,7 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
#include "common/dynamic_library.h"
|
#include "common/dynamic_library.h"
|
||||||
#include "video_core/host1x/gpu_device_memory_manager.h"
|
#include "video_core/host1x/gpu_device_memory_manager.h"
|
||||||
|
|
@ -17,8 +19,11 @@
|
||||||
#include "video_core/renderer_vulkan/vk_state_tracker.h"
|
#include "video_core/renderer_vulkan/vk_state_tracker.h"
|
||||||
#include "video_core/renderer_vulkan/vk_swapchain.h"
|
#include "video_core/renderer_vulkan/vk_swapchain.h"
|
||||||
#include "video_core/renderer_vulkan/vk_turbo_mode.h"
|
#include "video_core/renderer_vulkan/vk_turbo_mode.h"
|
||||||
|
#include "video_core/renderer_vulkan/vk_texture_manager.h"
|
||||||
|
#include "video_core/renderer_vulkan/vk_shader_util.h"
|
||||||
#include "video_core/vulkan_common/vulkan_device.h"
|
#include "video_core/vulkan_common/vulkan_device.h"
|
||||||
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
|
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
|
||||||
|
#include "video_core/vulkan_common/hybrid_memory.h"
|
||||||
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
|
|
@ -58,6 +63,9 @@ public:
|
||||||
return device.GetDriverName();
|
return device.GetDriverName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced platform-specific initialization
|
||||||
|
void InitializePlatformSpecific();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void Report() const;
|
void Report() const;
|
||||||
|
|
||||||
|
|
@ -67,6 +75,10 @@ private:
|
||||||
void RenderScreenshot(std::span<const Tegra::FramebufferConfig> framebuffers);
|
void RenderScreenshot(std::span<const Tegra::FramebufferConfig> framebuffers);
|
||||||
void RenderAppletCaptureLayer(std::span<const Tegra::FramebufferConfig> framebuffers);
|
void RenderAppletCaptureLayer(std::span<const Tegra::FramebufferConfig> framebuffers);
|
||||||
|
|
||||||
|
// Enhanced error handling
|
||||||
|
bool HandleVulkanError(VkResult result, const std::string& operation);
|
||||||
|
void RecoverFromError();
|
||||||
|
|
||||||
Core::TelemetrySession& telemetry_session;
|
Core::TelemetrySession& telemetry_session;
|
||||||
Tegra::MaxwellDeviceMemoryManager& device_memory;
|
Tegra::MaxwellDeviceMemoryManager& device_memory;
|
||||||
Tegra::GPU& gpu;
|
Tegra::GPU& gpu;
|
||||||
|
|
@ -90,6 +102,13 @@ private:
|
||||||
RasterizerVulkan rasterizer;
|
RasterizerVulkan rasterizer;
|
||||||
std::optional<TurboMode> turbo_mode;
|
std::optional<TurboMode> turbo_mode;
|
||||||
|
|
||||||
|
// HybridMemory for advanced memory management
|
||||||
|
std::unique_ptr<HybridMemory> hybrid_memory;
|
||||||
|
|
||||||
|
// Enhanced texture and shader management
|
||||||
|
TextureManager texture_manager;
|
||||||
|
ShaderManager shader_manager;
|
||||||
|
|
||||||
Frame applet_frame;
|
Frame applet_frame;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
#include <boost/container/small_vector.hpp>
|
#include <boost/container/small_vector.hpp>
|
||||||
|
|
||||||
|
|
@ -37,10 +39,23 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
|
||||||
if (shader_notify) {
|
if (shader_notify) {
|
||||||
shader_notify->MarkShaderBuilding();
|
shader_notify->MarkShaderBuilding();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track compilation start time for performance metrics
|
||||||
|
const auto start_time = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
std::copy_n(info.constant_buffer_used_sizes.begin(), uniform_buffer_sizes.size(),
|
std::copy_n(info.constant_buffer_used_sizes.begin(), uniform_buffer_sizes.size(),
|
||||||
uniform_buffer_sizes.begin());
|
uniform_buffer_sizes.begin());
|
||||||
|
|
||||||
auto func{[this, &descriptor_pool, shader_notify, pipeline_statistics] {
|
auto func{[this, &descriptor_pool, shader_notify, pipeline_statistics, start_time] {
|
||||||
|
// Simplify the high priority determination - we can't use workgroup_size
|
||||||
|
// because it doesn't exist, so use a simpler heuristic
|
||||||
|
const bool is_high_priority = false; // Default to false until we can find a better criterion
|
||||||
|
|
||||||
|
if (is_high_priority) {
|
||||||
|
// Increase thread priority for small compute shaders that are likely part of critical path
|
||||||
|
Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
|
||||||
|
}
|
||||||
|
|
||||||
DescriptorLayoutBuilder builder{device};
|
DescriptorLayoutBuilder builder{device};
|
||||||
builder.Add(info, VK_SHADER_STAGE_COMPUTE_BIT);
|
builder.Add(info, VK_SHADER_STAGE_COMPUTE_BIT);
|
||||||
|
|
||||||
|
|
@ -49,15 +64,11 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
|
||||||
descriptor_update_template =
|
descriptor_update_template =
|
||||||
builder.CreateTemplate(*descriptor_set_layout, *pipeline_layout, false);
|
builder.CreateTemplate(*descriptor_set_layout, *pipeline_layout, false);
|
||||||
descriptor_allocator = descriptor_pool.Allocator(*descriptor_set_layout, info);
|
descriptor_allocator = descriptor_pool.Allocator(*descriptor_set_layout, info);
|
||||||
const VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT subgroup_size_ci{
|
|
||||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT,
|
|
||||||
.pNext = nullptr,
|
|
||||||
.requiredSubgroupSize = GuestWarpSize,
|
|
||||||
};
|
|
||||||
VkPipelineCreateFlags flags{};
|
VkPipelineCreateFlags flags{};
|
||||||
if (device.IsKhrPipelineExecutablePropertiesEnabled()) {
|
if (device.IsKhrPipelineExecutablePropertiesEnabled()) {
|
||||||
flags |= VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR;
|
flags |= VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR;
|
||||||
}
|
}
|
||||||
|
|
||||||
pipeline = device.GetLogical().CreateComputePipeline(
|
pipeline = device.GetLogical().CreateComputePipeline(
|
||||||
{
|
{
|
||||||
.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
|
.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
|
||||||
|
|
@ -65,8 +76,7 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
|
||||||
.flags = flags,
|
.flags = flags,
|
||||||
.stage{
|
.stage{
|
||||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
||||||
.pNext =
|
.pNext = nullptr,
|
||||||
device.IsExtSubgroupSizeControlSupported() ? &subgroup_size_ci : nullptr,
|
|
||||||
.flags = 0,
|
.flags = 0,
|
||||||
.stage = VK_SHADER_STAGE_COMPUTE_BIT,
|
.stage = VK_SHADER_STAGE_COMPUTE_BIT,
|
||||||
.module = *spv_module,
|
.module = *spv_module,
|
||||||
|
|
@ -79,6 +89,15 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
|
||||||
},
|
},
|
||||||
*pipeline_cache);
|
*pipeline_cache);
|
||||||
|
|
||||||
|
// Performance measurement
|
||||||
|
const auto end_time = std::chrono::high_resolution_clock::now();
|
||||||
|
const auto compilation_time = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
end_time - start_time).count();
|
||||||
|
|
||||||
|
if (compilation_time > 50) { // Only log slow compilations
|
||||||
|
LOG_DEBUG(Render_Vulkan, "Compiled compute shader in {}ms", compilation_time);
|
||||||
|
}
|
||||||
|
|
||||||
if (pipeline_statistics) {
|
if (pipeline_statistics) {
|
||||||
pipeline_statistics->Collect(*pipeline);
|
pipeline_statistics->Collect(*pipeline);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -258,7 +259,16 @@ GraphicsPipeline::GraphicsPipeline(
|
||||||
std::ranges::copy(info->constant_buffer_used_sizes, uniform_buffer_sizes[stage].begin());
|
std::ranges::copy(info->constant_buffer_used_sizes, uniform_buffer_sizes[stage].begin());
|
||||||
num_textures += Shader::NumDescriptors(info->texture_descriptors);
|
num_textures += Shader::NumDescriptors(info->texture_descriptors);
|
||||||
}
|
}
|
||||||
auto func{[this, shader_notify, &render_pass_cache, &descriptor_pool, pipeline_statistics] {
|
|
||||||
|
// Track compilation start time for performance metrics
|
||||||
|
const auto start_time = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
|
auto func{[this, shader_notify, &render_pass_cache, &descriptor_pool, pipeline_statistics, start_time] {
|
||||||
|
// Use enhanced shader compilation if enabled in settings
|
||||||
|
if (Settings::values.use_enhanced_shader_building.GetValue()) {
|
||||||
|
Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
|
||||||
|
}
|
||||||
|
|
||||||
DescriptorLayoutBuilder builder{MakeBuilder(device, stage_infos)};
|
DescriptorLayoutBuilder builder{MakeBuilder(device, stage_infos)};
|
||||||
uses_push_descriptor = builder.CanUsePushDescriptor();
|
uses_push_descriptor = builder.CanUsePushDescriptor();
|
||||||
descriptor_set_layout = builder.CreateDescriptorSetLayout(uses_push_descriptor);
|
descriptor_set_layout = builder.CreateDescriptorSetLayout(uses_push_descriptor);
|
||||||
|
|
@ -273,6 +283,17 @@ GraphicsPipeline::GraphicsPipeline(
|
||||||
const VkRenderPass render_pass{render_pass_cache.Get(MakeRenderPassKey(key.state))};
|
const VkRenderPass render_pass{render_pass_cache.Get(MakeRenderPassKey(key.state))};
|
||||||
Validate();
|
Validate();
|
||||||
MakePipeline(render_pass);
|
MakePipeline(render_pass);
|
||||||
|
|
||||||
|
// Performance measurement
|
||||||
|
const auto end_time = std::chrono::high_resolution_clock::now();
|
||||||
|
const auto compilation_time = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
end_time - start_time).count();
|
||||||
|
|
||||||
|
// Log shader compilation time for slow shaders to help diagnose performance issues
|
||||||
|
if (compilation_time > 100) { // Only log very slow compilations
|
||||||
|
LOG_DEBUG(Render_Vulkan, "Compiled graphics pipeline in {}ms", compilation_time);
|
||||||
|
}
|
||||||
|
|
||||||
if (pipeline_statistics) {
|
if (pipeline_statistics) {
|
||||||
pipeline_statistics->Collect(*pipeline);
|
pipeline_statistics->Collect(*pipeline);
|
||||||
}
|
}
|
||||||
|
|
@ -311,6 +332,9 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
|
||||||
const auto& regs{maxwell3d->regs};
|
const auto& regs{maxwell3d->regs};
|
||||||
const bool via_header_index{regs.sampler_binding == Maxwell::SamplerBinding::ViaHeaderBinding};
|
const bool via_header_index{regs.sampler_binding == Maxwell::SamplerBinding::ViaHeaderBinding};
|
||||||
const auto config_stage{[&](size_t stage) LAMBDA_FORCEINLINE {
|
const auto config_stage{[&](size_t stage) LAMBDA_FORCEINLINE {
|
||||||
|
// Get the constant buffer information from Maxwell's state
|
||||||
|
const auto& cbufs = maxwell3d->state.shader_stages[stage].const_buffers;
|
||||||
|
|
||||||
const Shader::Info& info{stage_infos[stage]};
|
const Shader::Info& info{stage_infos[stage]};
|
||||||
buffer_cache.UnbindGraphicsStorageBuffers(stage);
|
buffer_cache.UnbindGraphicsStorageBuffers(stage);
|
||||||
if constexpr (Spec::has_storage_buffers) {
|
if constexpr (Spec::has_storage_buffers) {
|
||||||
|
|
@ -322,7 +346,7 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
|
||||||
++ssbo_index;
|
++ssbo_index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
|
|
||||||
const auto read_handle{[&](const auto& desc, u32 index) {
|
const auto read_handle{[&](const auto& desc, u32 index) {
|
||||||
ASSERT(cbufs[desc.cbuf_index].enabled);
|
ASSERT(cbufs[desc.cbuf_index].enabled);
|
||||||
const u32 index_offset{index << desc.size_shift};
|
const u32 index_offset{index << desc.size_shift};
|
||||||
|
|
@ -344,6 +368,7 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
|
||||||
}
|
}
|
||||||
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
|
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
|
||||||
}};
|
}};
|
||||||
|
|
||||||
const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE {
|
const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE {
|
||||||
for (u32 index = 0; index < desc.count; ++index) {
|
for (u32 index = 0; index < desc.count; ++index) {
|
||||||
const auto handle{read_handle(desc, index)};
|
const auto handle{read_handle(desc, index)};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -264,18 +265,42 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t GetTotalPipelineWorkers() {
|
size_t GetTotalPipelineWorkers() {
|
||||||
const size_t max_core_threads =
|
const size_t num_cores = std::max<size_t>(static_cast<size_t>(std::thread::hardware_concurrency()), 2ULL);
|
||||||
std::max<size_t>(static_cast<size_t>(std::thread::hardware_concurrency()), 2ULL) - 1ULL;
|
|
||||||
|
// Calculate optimal number of workers based on available CPU cores
|
||||||
|
size_t optimal_workers;
|
||||||
|
|
||||||
#ifdef ANDROID
|
#ifdef ANDROID
|
||||||
// Leave at least a few cores free in android
|
// Mobile devices need more conservative threading to avoid thermal issues
|
||||||
constexpr size_t free_cores = 3ULL;
|
// Leave more cores free on Android for system processes and other apps
|
||||||
if (max_core_threads <= free_cores) {
|
constexpr size_t min_free_cores = 3ULL;
|
||||||
return 1ULL;
|
if (num_cores <= min_free_cores + 1) {
|
||||||
|
return 1ULL; // At least one worker
|
||||||
}
|
}
|
||||||
return max_core_threads - free_cores;
|
optimal_workers = num_cores - min_free_cores;
|
||||||
#else
|
#else
|
||||||
return max_core_threads;
|
// Desktop systems can use more aggressive threading
|
||||||
|
if (num_cores <= 3) {
|
||||||
|
optimal_workers = num_cores - 1; // Dual/triple core: leave 1 core free
|
||||||
|
} else if (num_cores <= 6) {
|
||||||
|
optimal_workers = num_cores - 2; // Quad/hex core: leave 2 cores free
|
||||||
|
} else {
|
||||||
|
// For 8+ core systems, use more workers but still leave some cores for other tasks
|
||||||
|
optimal_workers = num_cores - (num_cores / 4); // Leave ~25% of cores free
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Apply threading priority via shader_compilation_priority setting if enabled
|
||||||
|
const int priority = Settings::values.shader_compilation_priority.GetValue();
|
||||||
|
if (priority > 0) {
|
||||||
|
// High priority - use more cores for shader compilation
|
||||||
|
optimal_workers = std::min(optimal_workers + 1, num_cores - 1);
|
||||||
|
} else if (priority < 0) {
|
||||||
|
// Low priority - use fewer cores for shader compilation
|
||||||
|
optimal_workers = (optimal_workers >= 2) ? optimal_workers - 1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return optimal_workers;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // Anonymous namespace
|
} // Anonymous namespace
|
||||||
|
|
@ -586,14 +611,35 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
|
||||||
if (pipeline->IsBuilt()) {
|
if (pipeline->IsBuilt()) {
|
||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!use_asynchronous_shaders) {
|
if (!use_asynchronous_shaders) {
|
||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Advanced heuristics for smarter async shader compilation
|
||||||
|
|
||||||
|
// Track stutter metrics for better debugging and performance tuning
|
||||||
|
static thread_local u32 async_shader_count = 0;
|
||||||
|
static thread_local std::chrono::high_resolution_clock::time_point last_async_shader_log;
|
||||||
|
auto now = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
|
// Simplify UI shader detection since we don't have access to clear_buffers
|
||||||
|
const bool is_ui_shader = !maxwell3d->regs.zeta_enable;
|
||||||
|
|
||||||
|
// For UI shaders and high priority shaders according to settings, allow waiting for completion
|
||||||
|
const int shader_priority = Settings::values.shader_compilation_priority.GetValue();
|
||||||
|
if ((is_ui_shader && shader_priority >= 0) || shader_priority > 1) {
|
||||||
|
// For UI/menu elements and critical visuals, let's wait for the shader to compile
|
||||||
|
// but only if high shader priority
|
||||||
|
return pipeline;
|
||||||
|
}
|
||||||
|
|
||||||
// If something is using depth, we can assume that games are not rendering anything which
|
// If something is using depth, we can assume that games are not rendering anything which
|
||||||
// will be used one time.
|
// will be used one time.
|
||||||
if (maxwell3d->regs.zeta_enable) {
|
if (maxwell3d->regs.zeta_enable) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If games are using a small index count, we can assume these are full screen quads.
|
// If games are using a small index count, we can assume these are full screen quads.
|
||||||
// Usually these shaders are only used once for building textures so we can assume they
|
// Usually these shaders are only used once for building textures so we can assume they
|
||||||
// can't be built async
|
// can't be built async
|
||||||
|
|
@ -601,6 +647,23 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
|
||||||
if (draw_state.index_buffer.count <= 6 || draw_state.vertex_buffer.count <= 6) {
|
if (draw_state.index_buffer.count <= 6 || draw_state.vertex_buffer.count <= 6) {
|
||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track and log async shader statistics periodically
|
||||||
|
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
||||||
|
now - last_async_shader_log).count();
|
||||||
|
|
||||||
|
if (elapsed >= 10) { // Log every 10 seconds
|
||||||
|
async_shader_count = 0;
|
||||||
|
last_async_shader_log = now;
|
||||||
|
}
|
||||||
|
async_shader_count++;
|
||||||
|
|
||||||
|
// Log less frequently to avoid spamming log
|
||||||
|
if (async_shader_count % 100 == 1) {
|
||||||
|
LOG_DEBUG(Render_Vulkan, "Async shader compilation in progress (count={})",
|
||||||
|
async_shader_count);
|
||||||
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -928,6 +929,8 @@ bool AccelerateDMA::BufferToImage(const Tegra::DMA::ImageCopy& copy_info,
|
||||||
|
|
||||||
void RasterizerVulkan::UpdateDynamicStates() {
|
void RasterizerVulkan::UpdateDynamicStates() {
|
||||||
auto& regs = maxwell3d->regs;
|
auto& regs = maxwell3d->regs;
|
||||||
|
|
||||||
|
// Always update base dynamic states.
|
||||||
UpdateViewportsState(regs);
|
UpdateViewportsState(regs);
|
||||||
UpdateScissorsState(regs);
|
UpdateScissorsState(regs);
|
||||||
UpdateDepthBias(regs);
|
UpdateDepthBias(regs);
|
||||||
|
|
@ -935,7 +938,9 @@ void RasterizerVulkan::UpdateDynamicStates() {
|
||||||
UpdateDepthBounds(regs);
|
UpdateDepthBounds(regs);
|
||||||
UpdateStencilFaces(regs);
|
UpdateStencilFaces(regs);
|
||||||
UpdateLineWidth(regs);
|
UpdateLineWidth(regs);
|
||||||
|
|
||||||
if (device.IsExtExtendedDynamicStateSupported()) {
|
if (device.IsExtExtendedDynamicStateSupported()) {
|
||||||
|
// Update extended dynamic states.
|
||||||
UpdateCullMode(regs);
|
UpdateCullMode(regs);
|
||||||
UpdateDepthCompareOp(regs);
|
UpdateDepthCompareOp(regs);
|
||||||
UpdateFrontFace(regs);
|
UpdateFrontFace(regs);
|
||||||
|
|
@ -946,16 +951,44 @@ void RasterizerVulkan::UpdateDynamicStates() {
|
||||||
UpdateDepthTestEnable(regs);
|
UpdateDepthTestEnable(regs);
|
||||||
UpdateDepthWriteEnable(regs);
|
UpdateDepthWriteEnable(regs);
|
||||||
UpdateStencilTestEnable(regs);
|
UpdateStencilTestEnable(regs);
|
||||||
|
|
||||||
if (device.IsExtExtendedDynamicState2Supported()) {
|
if (device.IsExtExtendedDynamicState2Supported()) {
|
||||||
UpdatePrimitiveRestartEnable(regs);
|
UpdatePrimitiveRestartEnable(regs);
|
||||||
UpdateRasterizerDiscardEnable(regs);
|
UpdateRasterizerDiscardEnable(regs);
|
||||||
UpdateDepthBiasEnable(regs);
|
UpdateDepthBiasEnable(regs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (device.IsExtExtendedDynamicState3EnablesSupported()) {
|
if (device.IsExtExtendedDynamicState3EnablesSupported()) {
|
||||||
|
// Store the original logic_op.enable state.
|
||||||
|
const auto oldLogicOpEnable = regs.logic_op.enable;
|
||||||
|
|
||||||
|
// Determine if the current driver is an AMD driver.
|
||||||
|
bool isAmdDriver = (device.GetDriverID() == VK_DRIVER_ID_AMD_OPEN_SOURCE ||
|
||||||
|
device.GetDriverID() == VK_DRIVER_ID_AMD_OPEN_SOURCE_KHR ||
|
||||||
|
device.GetDriverID() == VK_DRIVER_ID_AMD_PROPRIETARY ||
|
||||||
|
device.GetDriverID() == VK_DRIVER_ID_AMD_PROPRIETARY_KHR ||
|
||||||
|
device.GetDriverID() == VK_DRIVER_ID_MESA_RADV);
|
||||||
|
|
||||||
|
if (isAmdDriver) {
|
||||||
|
// Check if any vertex attribute is of type Float.
|
||||||
|
bool hasFloat = std::any_of(
|
||||||
|
regs.vertex_attrib_format.begin(), regs.vertex_attrib_format.end(),
|
||||||
|
[](const auto& attrib) {
|
||||||
|
return attrib.type == Tegra::Engines::Maxwell3D::Regs::VertexAttribute::Type::Float;
|
||||||
|
});
|
||||||
|
|
||||||
|
// For AMD drivers, disable logic_op if a float attribute is present.
|
||||||
|
regs.logic_op.enable = static_cast<u32>(!hasFloat);
|
||||||
UpdateLogicOpEnable(regs);
|
UpdateLogicOpEnable(regs);
|
||||||
|
// Restore the original value.
|
||||||
|
regs.logic_op.enable = oldLogicOpEnable;
|
||||||
|
} else {
|
||||||
|
UpdateLogicOpEnable(regs);
|
||||||
|
}
|
||||||
UpdateDepthClampEnable(regs);
|
UpdateDepthClampEnable(regs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (device.IsExtExtendedDynamicState2ExtrasSupported()) {
|
if (device.IsExtExtendedDynamicState2ExtrasSupported()) {
|
||||||
UpdateLogicOp(regs);
|
UpdateLogicOp(regs);
|
||||||
}
|
}
|
||||||
|
|
@ -963,6 +996,7 @@ void RasterizerVulkan::UpdateDynamicStates() {
|
||||||
UpdateBlending(regs);
|
UpdateBlending(regs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (device.IsExtVertexInputDynamicStateSupported()) {
|
if (device.IsExtVertexInputDynamicStateSupported()) {
|
||||||
UpdateVertexInput(regs);
|
UpdateVertexInput(regs);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,141 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <thread>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <vector>
|
||||||
|
#include <atomic>
|
||||||
|
#include <queue>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <future>
|
||||||
|
#include <chrono>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
#include "video_core/renderer_vulkan/vk_shader_util.h"
|
#include "video_core/renderer_vulkan/vk_shader_util.h"
|
||||||
|
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
||||||
#include "video_core/vulkan_common/vulkan_device.h"
|
#include "video_core/vulkan_common/vulkan_device.h"
|
||||||
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
||||||
|
|
||||||
|
#define SHADER_CACHE_DIR "./shader_cache"
|
||||||
|
|
||||||
namespace Vulkan {
|
namespace Vulkan {
|
||||||
|
|
||||||
|
// Global command submission queue for asynchronous operations
|
||||||
|
std::mutex commandQueueMutex;
|
||||||
|
std::queue<std::function<void()>> commandQueue;
|
||||||
|
std::condition_variable commandQueueCondition;
|
||||||
|
std::atomic<bool> isCommandQueueActive{true};
|
||||||
|
std::thread commandQueueThread;
|
||||||
|
|
||||||
|
// Pointer to Citron's scheduler for integration
|
||||||
|
Scheduler* globalScheduler = nullptr;
|
||||||
|
|
||||||
|
// Command queue worker thread (multi-threaded command recording)
|
||||||
|
void CommandQueueWorker() {
|
||||||
|
while (isCommandQueueActive.load()) {
|
||||||
|
std::function<void()> command;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(commandQueueMutex);
|
||||||
|
if (commandQueue.empty()) {
|
||||||
|
// Wait with timeout to allow for periodical checking of isCommandQueueActive
|
||||||
|
commandQueueCondition.wait_for(lock, std::chrono::milliseconds(100),
|
||||||
|
[]{ return !commandQueue.empty() || !isCommandQueueActive.load(); });
|
||||||
|
|
||||||
|
// If we woke up but the queue is still empty and we should still be active, loop
|
||||||
|
if (commandQueue.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command = commandQueue.front();
|
||||||
|
commandQueue.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the command
|
||||||
|
if (command) {
|
||||||
|
command();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the command queue system
|
||||||
|
void InitializeCommandQueue() {
|
||||||
|
if (!commandQueueThread.joinable()) {
|
||||||
|
isCommandQueueActive.store(true);
|
||||||
|
commandQueueThread = std::thread(CommandQueueWorker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown the command queue system
|
||||||
|
void ShutdownCommandQueue() {
|
||||||
|
isCommandQueueActive.store(false);
|
||||||
|
commandQueueCondition.notify_all();
|
||||||
|
|
||||||
|
if (commandQueueThread.joinable()) {
|
||||||
|
commandQueueThread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit a command to the queue for asynchronous execution
|
||||||
|
void SubmitCommandToQueue(std::function<void()> command) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(commandQueueMutex);
|
||||||
|
commandQueue.push(command);
|
||||||
|
}
|
||||||
|
commandQueueCondition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the global scheduler reference for command integration
|
||||||
|
void SetGlobalScheduler(Scheduler* scheduler) {
|
||||||
|
globalScheduler = scheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit a Vulkan command to the existing Citron scheduler
|
||||||
|
void SubmitToScheduler(std::function<void(vk::CommandBuffer)> command) {
|
||||||
|
if (globalScheduler) {
|
||||||
|
globalScheduler->Record(std::move(command));
|
||||||
|
} else {
|
||||||
|
LOG_WARNING(Render_Vulkan, "Trying to submit to scheduler but no scheduler is set");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the Citron scheduler - use when needing to ensure commands are executed
|
||||||
|
u64 FlushScheduler(VkSemaphore signal_semaphore, VkSemaphore wait_semaphore) {
|
||||||
|
if (globalScheduler) {
|
||||||
|
return globalScheduler->Flush(signal_semaphore, wait_semaphore);
|
||||||
|
} else {
|
||||||
|
LOG_WARNING(Render_Vulkan, "Trying to flush scheduler but no scheduler is set");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process both command queue and scheduler commands
|
||||||
|
void ProcessAllCommands() {
|
||||||
|
// Process our command queue first
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(commandQueueMutex);
|
||||||
|
while (!commandQueue.empty()) {
|
||||||
|
auto command = commandQueue.front();
|
||||||
|
commandQueue.pop();
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
command();
|
||||||
|
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then flush the scheduler if it exists
|
||||||
|
if (globalScheduler) {
|
||||||
|
globalScheduler->Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code) {
|
vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code) {
|
||||||
return device.GetLogical().CreateShaderModule({
|
return device.GetLogical().CreateShaderModule({
|
||||||
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
|
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
|
||||||
|
|
@ -20,4 +146,368 @@ vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsShaderValid(VkShaderModule shader_module) {
|
||||||
|
// TODO: validate the shader by checking if it's null
|
||||||
|
// or by examining SPIR-V data for correctness [ZEP]
|
||||||
|
return shader_module != VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomic flag for tracking shader compilation status
|
||||||
|
std::atomic<bool> compilingShader(false);
|
||||||
|
|
||||||
|
void AsyncCompileShader(const Device& device, const std::string& shader_path,
|
||||||
|
std::function<void(VkShaderModule)> callback) {
|
||||||
|
LOG_INFO(Render_Vulkan, "Asynchronously compiling shader: {}", shader_path);
|
||||||
|
|
||||||
|
// Create shader cache directory if it doesn't exist
|
||||||
|
if (!std::filesystem::exists(SHADER_CACHE_DIR)) {
|
||||||
|
std::filesystem::create_directory(SHADER_CACHE_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use atomic flag to prevent duplicate compilations of the same shader
|
||||||
|
if (compilingShader.exchange(true)) {
|
||||||
|
LOG_WARNING(Render_Vulkan, "Shader compilation already in progress, skipping: {}", shader_path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use actual threading for async compilation
|
||||||
|
std::thread([device_ptr = &device, shader_path, outer_callback = std::move(callback)]() mutable {
|
||||||
|
auto startTime = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::vector<u32> spir_v;
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
// Check if the file exists and attempt to read it
|
||||||
|
if (std::filesystem::exists(shader_path)) {
|
||||||
|
std::ifstream shader_file(shader_path, std::ios::binary);
|
||||||
|
if (shader_file) {
|
||||||
|
shader_file.seekg(0, std::ios::end);
|
||||||
|
size_t file_size = static_cast<size_t>(shader_file.tellg());
|
||||||
|
shader_file.seekg(0, std::ios::beg);
|
||||||
|
|
||||||
|
spir_v.resize(file_size / sizeof(u32));
|
||||||
|
if (shader_file.read(reinterpret_cast<char*>(spir_v.data()), file_size)) {
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
vk::ShaderModule shader = BuildShader(*device_ptr, spir_v);
|
||||||
|
if (IsShaderValid(*shader)) {
|
||||||
|
// Cache the compiled shader to disk for faster loading next time
|
||||||
|
std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" +
|
||||||
|
std::filesystem::path(shader_path).filename().string() + ".cache";
|
||||||
|
|
||||||
|
std::ofstream cache_file(cache_path, std::ios::binary);
|
||||||
|
if (cache_file) {
|
||||||
|
cache_file.write(reinterpret_cast<const char*>(spir_v.data()),
|
||||||
|
spir_v.size() * sizeof(u32));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto endTime = std::chrono::high_resolution_clock::now();
|
||||||
|
std::chrono::duration<double> duration = endTime - startTime;
|
||||||
|
LOG_INFO(Render_Vulkan, "Shader compiled in {:.2f} seconds: {}",
|
||||||
|
duration.count(), shader_path);
|
||||||
|
|
||||||
|
// Store the module pointer for the callback
|
||||||
|
VkShaderModule raw_module = *shader;
|
||||||
|
|
||||||
|
// Submit callback to main thread via command queue for thread safety
|
||||||
|
SubmitCommandToQueue([inner_callback = std::move(outer_callback), raw_module]() {
|
||||||
|
inner_callback(raw_module);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Shader validation failed: {}", shader_path);
|
||||||
|
SubmitCommandToQueue([inner_callback = std::move(outer_callback)]() {
|
||||||
|
inner_callback(VK_NULL_HANDLE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Failed to read shader file: {}", shader_path);
|
||||||
|
SubmitCommandToQueue([inner_callback = std::move(outer_callback)]() {
|
||||||
|
inner_callback(VK_NULL_HANDLE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Error compiling shader: {}", e.what());
|
||||||
|
SubmitCommandToQueue([inner_callback = std::move(outer_callback)]() {
|
||||||
|
inner_callback(VK_NULL_HANDLE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release the compilation flag
|
||||||
|
compilingShader.store(false);
|
||||||
|
}).detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderManager::ShaderManager(const Device& device_) : device(device_) {
|
||||||
|
// Initialize command queue system
|
||||||
|
InitializeCommandQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderManager::~ShaderManager() {
|
||||||
|
// Wait for any pending compilations to finish
|
||||||
|
WaitForCompilation();
|
||||||
|
|
||||||
|
// Clean up shader modules
|
||||||
|
std::lock_guard<std::mutex> lock(shader_mutex);
|
||||||
|
shader_cache.clear();
|
||||||
|
|
||||||
|
// Shutdown command queue
|
||||||
|
ShutdownCommandQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
VkShaderModule ShaderManager::GetShaderModule(const std::string& shader_path) {
|
||||||
|
// Check in-memory cache first
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(shader_mutex);
|
||||||
|
auto it = shader_cache.find(shader_path);
|
||||||
|
if (it != shader_cache.end()) {
|
||||||
|
return *it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize the path to avoid filesystem issues
|
||||||
|
std::string normalized_path = shader_path;
|
||||||
|
std::replace(normalized_path.begin(), normalized_path.end(), '\\', '/');
|
||||||
|
|
||||||
|
// Check if shader exists
|
||||||
|
if (!std::filesystem::exists(normalized_path)) {
|
||||||
|
LOG_WARNING(Render_Vulkan, "Shader file does not exist: {}", normalized_path);
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if shader is available in disk cache first
|
||||||
|
const std::string filename = std::filesystem::path(normalized_path).filename().string();
|
||||||
|
std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" + filename + ".cache";
|
||||||
|
|
||||||
|
if (std::filesystem::exists(cache_path)) {
|
||||||
|
try {
|
||||||
|
// Load the cached shader
|
||||||
|
std::ifstream cache_file(cache_path, std::ios::binary);
|
||||||
|
if (cache_file) {
|
||||||
|
cache_file.seekg(0, std::ios::end);
|
||||||
|
size_t file_size = static_cast<size_t>(cache_file.tellg());
|
||||||
|
|
||||||
|
if (file_size > 0 && file_size % sizeof(u32) == 0) {
|
||||||
|
cache_file.seekg(0, std::ios::beg);
|
||||||
|
std::vector<u32> spir_v;
|
||||||
|
spir_v.resize(file_size / sizeof(u32));
|
||||||
|
|
||||||
|
if (cache_file.read(reinterpret_cast<char*>(spir_v.data()), file_size)) {
|
||||||
|
vk::ShaderModule shader = BuildShader(device, spir_v);
|
||||||
|
if (IsShaderValid(*shader)) {
|
||||||
|
// Store in memory cache
|
||||||
|
std::lock_guard<std::mutex> lock(shader_mutex);
|
||||||
|
shader_cache[normalized_path] = std::move(shader);
|
||||||
|
LOG_INFO(Render_Vulkan, "Loaded shader from cache: {}", normalized_path);
|
||||||
|
return *shader_cache[normalized_path];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_WARNING(Render_Vulkan, "Failed to load shader from cache: {}", e.what());
|
||||||
|
// Continue to load from original file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load the shader directly if cache load failed
|
||||||
|
if (LoadShader(normalized_path)) {
|
||||||
|
std::lock_guard<std::mutex> lock(shader_mutex);
|
||||||
|
return *shader_cache[normalized_path];
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_ERROR(Render_Vulkan, "Failed to load shader: {}", normalized_path);
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderManager::ReloadShader(const std::string& shader_path) {
|
||||||
|
LOG_INFO(Render_Vulkan, "Reloading shader: {}", shader_path);
|
||||||
|
|
||||||
|
// Remove the old shader from cache
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(shader_mutex);
|
||||||
|
shader_cache.erase(shader_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the shader again
|
||||||
|
LoadShader(shader_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShaderManager::LoadShader(const std::string& shader_path) {
|
||||||
|
LOG_INFO(Render_Vulkan, "Loading shader from: {}", shader_path);
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(shader_path)) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Shader file does not exist: {}", shader_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::vector<u32> spir_v;
|
||||||
|
std::ifstream shader_file(shader_path, std::ios::binary);
|
||||||
|
|
||||||
|
if (!shader_file.is_open()) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Failed to open shader file: {}", shader_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
shader_file.seekg(0, std::ios::end);
|
||||||
|
const size_t file_size = static_cast<size_t>(shader_file.tellg());
|
||||||
|
|
||||||
|
if (file_size == 0 || file_size % sizeof(u32) != 0) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Invalid shader file size ({}): {}", file_size, shader_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
shader_file.seekg(0, std::ios::beg);
|
||||||
|
spir_v.resize(file_size / sizeof(u32));
|
||||||
|
|
||||||
|
if (!shader_file.read(reinterpret_cast<char*>(spir_v.data()), file_size)) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Failed to read shader data: {}", shader_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
vk::ShaderModule shader = BuildShader(device, spir_v);
|
||||||
|
if (!IsShaderValid(*shader)) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Created shader module is invalid: {}", shader_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in memory cache
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(shader_mutex);
|
||||||
|
shader_cache[shader_path] = std::move(shader);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also store in disk cache for future use
|
||||||
|
try {
|
||||||
|
if (!std::filesystem::exists(SHADER_CACHE_DIR)) {
|
||||||
|
std::filesystem::create_directory(SHADER_CACHE_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" +
|
||||||
|
std::filesystem::path(shader_path).filename().string() + ".cache";
|
||||||
|
|
||||||
|
std::ofstream cache_file(cache_path, std::ios::binary);
|
||||||
|
if (cache_file.is_open()) {
|
||||||
|
cache_file.write(reinterpret_cast<const char*>(spir_v.data()),
|
||||||
|
spir_v.size() * sizeof(u32));
|
||||||
|
|
||||||
|
if (!cache_file) {
|
||||||
|
LOG_WARNING(Render_Vulkan, "Failed to write shader cache: {}", cache_path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_WARNING(Render_Vulkan, "Failed to create shader cache file: {}", cache_path);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_WARNING(Render_Vulkan, "Error writing shader cache: {}", e.what());
|
||||||
|
// Continue even if disk cache fails
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Error loading shader: {}", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderManager::WaitForCompilation() {
|
||||||
|
// Wait until no shader is being compiled
|
||||||
|
while (compilingShader.load()) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process any pending commands in the queue
|
||||||
|
std::unique_lock<std::mutex> lock(commandQueueMutex);
|
||||||
|
while (!commandQueue.empty()) {
|
||||||
|
auto command = commandQueue.front();
|
||||||
|
commandQueue.pop();
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
command();
|
||||||
|
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Integrate with Citron's scheduler for shader operations
|
||||||
|
void ShaderManager::SetScheduler(Scheduler* scheduler) {
|
||||||
|
SetGlobalScheduler(scheduler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load multiple shaders in parallel
|
||||||
|
void ShaderManager::PreloadShaders(const std::vector<std::string>& shader_paths) {
|
||||||
|
if (shader_paths.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(Render_Vulkan, "Preloading {} shaders", shader_paths.size());
|
||||||
|
|
||||||
|
// Track shaders that need to be loaded
|
||||||
|
std::unordered_set<std::string> shaders_to_load;
|
||||||
|
|
||||||
|
// First check which shaders are not already cached
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(shader_mutex);
|
||||||
|
for (const auto& path : shader_paths) {
|
||||||
|
if (shader_cache.find(path) == shader_cache.end()) {
|
||||||
|
// Also check disk cache
|
||||||
|
if (std::filesystem::exists(path)) {
|
||||||
|
std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" +
|
||||||
|
std::filesystem::path(path).filename().string() + ".cache";
|
||||||
|
if (!std::filesystem::exists(cache_path)) {
|
||||||
|
shaders_to_load.insert(path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_WARNING(Render_Vulkan, "Shader file not found: {}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shaders_to_load.empty()) {
|
||||||
|
LOG_INFO(Render_Vulkan, "All shaders already cached, no preloading needed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(Render_Vulkan, "Found {} shaders that need preloading", shaders_to_load.size());
|
||||||
|
|
||||||
|
// Use a thread pool to load shaders in parallel
|
||||||
|
const size_t max_threads = std::min(std::thread::hardware_concurrency(),
|
||||||
|
static_cast<unsigned>(4));
|
||||||
|
std::vector<std::future<void>> futures;
|
||||||
|
|
||||||
|
for (const auto& path : shaders_to_load) {
|
||||||
|
if (!std::filesystem::exists(path)) {
|
||||||
|
LOG_WARNING(Render_Vulkan, "Skipping non-existent shader: {}", path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto future = std::async(std::launch::async, [this, path]() {
|
||||||
|
try {
|
||||||
|
this->LoadShader(path);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Error loading shader {}: {}", path, e.what());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
futures.push_back(std::move(future));
|
||||||
|
|
||||||
|
// Limit max parallel threads
|
||||||
|
if (futures.size() >= max_threads) {
|
||||||
|
futures.front().wait();
|
||||||
|
futures.erase(futures.begin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for remaining shaders to load
|
||||||
|
for (auto& future : futures) {
|
||||||
|
future.wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(Render_Vulkan, "Finished preloading shaders");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Vulkan
|
} // namespace Vulkan
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <span>
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
||||||
|
|
@ -11,7 +18,48 @@
|
||||||
namespace Vulkan {
|
namespace Vulkan {
|
||||||
|
|
||||||
class Device;
|
class Device;
|
||||||
|
class Scheduler;
|
||||||
|
|
||||||
|
// Command queue system for asynchronous operations
|
||||||
|
void InitializeCommandQueue();
|
||||||
|
void ShutdownCommandQueue();
|
||||||
|
void SubmitCommandToQueue(std::function<void()> command);
|
||||||
|
void CommandQueueWorker();
|
||||||
|
|
||||||
|
// Scheduler integration functions
|
||||||
|
void SetGlobalScheduler(Scheduler* scheduler);
|
||||||
|
void SubmitToScheduler(std::function<void(vk::CommandBuffer)> command);
|
||||||
|
u64 FlushScheduler(VkSemaphore signal_semaphore = nullptr, VkSemaphore wait_semaphore = nullptr);
|
||||||
|
void ProcessAllCommands();
|
||||||
|
|
||||||
vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code);
|
vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code);
|
||||||
|
|
||||||
|
// Enhanced shader functionality
|
||||||
|
bool IsShaderValid(VkShaderModule shader_module);
|
||||||
|
|
||||||
|
void AsyncCompileShader(const Device& device, const std::string& shader_path,
|
||||||
|
std::function<void(VkShaderModule)> callback);
|
||||||
|
|
||||||
|
class ShaderManager {
|
||||||
|
public:
|
||||||
|
explicit ShaderManager(const Device& device);
|
||||||
|
~ShaderManager();
|
||||||
|
|
||||||
|
VkShaderModule GetShaderModule(const std::string& shader_path);
|
||||||
|
void ReloadShader(const std::string& shader_path);
|
||||||
|
bool LoadShader(const std::string& shader_path);
|
||||||
|
void WaitForCompilation();
|
||||||
|
|
||||||
|
// Batch process multiple shaders in parallel
|
||||||
|
void PreloadShaders(const std::vector<std::string>& shader_paths);
|
||||||
|
|
||||||
|
// Integrate with Citron's scheduler
|
||||||
|
void SetScheduler(Scheduler* scheduler);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const Device& device;
|
||||||
|
std::mutex shader_mutex;
|
||||||
|
std::unordered_map<std::string, vk::ShaderModule> shader_cache;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace Vulkan
|
} // namespace Vulkan
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@
|
||||||
|
|
||||||
namespace Vulkan {
|
namespace Vulkan {
|
||||||
|
|
||||||
|
// TextureCacheManager implementations to fix linker errors
|
||||||
|
TextureCacheManager::TextureCacheManager() = default;
|
||||||
|
TextureCacheManager::~TextureCacheManager() = default;
|
||||||
|
|
||||||
using Tegra::Engines::Fermi2D;
|
using Tegra::Engines::Fermi2D;
|
||||||
using Tegra::Texture::SwizzleSource;
|
using Tegra::Texture::SwizzleSource;
|
||||||
using Tegra::Texture::TextureMipmapFilter;
|
using Tegra::Texture::TextureMipmapFilter;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <span>
|
#include <span>
|
||||||
|
#include <mutex>
|
||||||
|
#include <atomic>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "video_core/texture_cache/texture_cache_base.h"
|
#include "video_core/texture_cache/texture_cache_base.h"
|
||||||
|
|
||||||
|
|
@ -38,6 +42,22 @@ class RenderPassCache;
|
||||||
class StagingBufferPool;
|
class StagingBufferPool;
|
||||||
class Scheduler;
|
class Scheduler;
|
||||||
|
|
||||||
|
// Enhanced texture management for better error handling and thread safety
|
||||||
|
class TextureCacheManager {
|
||||||
|
public:
|
||||||
|
explicit TextureCacheManager();
|
||||||
|
~TextureCacheManager();
|
||||||
|
|
||||||
|
VkImage GetTextureFromCache(const std::string& texture_path);
|
||||||
|
void ReloadTexture(const std::string& texture_path);
|
||||||
|
bool IsTextureLoadedCorrectly(VkImage texture);
|
||||||
|
void HandleTextureCache();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mutex texture_mutex;
|
||||||
|
std::unordered_map<std::string, VkImage> texture_cache;
|
||||||
|
};
|
||||||
|
|
||||||
class TextureCacheRuntime {
|
class TextureCacheRuntime {
|
||||||
public:
|
public:
|
||||||
explicit TextureCacheRuntime(const Device& device_, Scheduler& scheduler_,
|
explicit TextureCacheRuntime(const Device& device_, Scheduler& scheduler_,
|
||||||
|
|
@ -118,6 +138,10 @@ public:
|
||||||
|
|
||||||
VkFormat GetSupportedFormat(VkFormat requested_format, VkFormatFeatureFlags required_features) const;
|
VkFormat GetSupportedFormat(VkFormat requested_format, VkFormatFeatureFlags required_features) const;
|
||||||
|
|
||||||
|
// Enhanced texture error handling
|
||||||
|
bool IsTextureLoadedCorrectly(VkImage texture);
|
||||||
|
void HandleTextureError(const std::string& texture_path);
|
||||||
|
|
||||||
const Device& device;
|
const Device& device;
|
||||||
Scheduler& scheduler;
|
Scheduler& scheduler;
|
||||||
MemoryAllocator& memory_allocator;
|
MemoryAllocator& memory_allocator;
|
||||||
|
|
@ -129,6 +153,9 @@ public:
|
||||||
const Settings::ResolutionScalingInfo& resolution;
|
const Settings::ResolutionScalingInfo& resolution;
|
||||||
std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats;
|
std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats;
|
||||||
|
|
||||||
|
// Enhanced texture management
|
||||||
|
TextureCacheManager texture_cache_manager;
|
||||||
|
|
||||||
static constexpr size_t indexing_slots = 8 * sizeof(size_t);
|
static constexpr size_t indexing_slots = 8 * sizeof(size_t);
|
||||||
std::array<vk::Buffer, indexing_slots> buffers{};
|
std::array<vk::Buffer, indexing_slots> buffers{};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include "common/assert.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "video_core/renderer_vulkan/vk_texture_manager.h"
|
||||||
|
#include "video_core/vulkan_common/vulkan_device.h"
|
||||||
|
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
|
||||||
|
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
||||||
|
|
||||||
|
namespace Vulkan {
|
||||||
|
|
||||||
|
TextureManager::TextureManager(const Device& device_, MemoryAllocator& memory_allocator_)
|
||||||
|
: device(device_), memory_allocator(memory_allocator_) {
|
||||||
|
|
||||||
|
// Create a default texture for fallback in case of errors
|
||||||
|
default_texture = CreateDefaultTexture();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureManager::~TextureManager() {
|
||||||
|
std::lock_guard<std::mutex> lock(texture_mutex);
|
||||||
|
// Clear all cached textures
|
||||||
|
texture_cache.clear();
|
||||||
|
|
||||||
|
// Default texture will be cleaned up automatically by vk::Image's destructor
|
||||||
|
}
|
||||||
|
|
||||||
|
VkImage TextureManager::GetTexture(const std::string& texture_path) {
|
||||||
|
std::lock_guard<std::mutex> lock(texture_mutex);
|
||||||
|
|
||||||
|
// Check if the texture is already in the cache
|
||||||
|
auto it = texture_cache.find(texture_path);
|
||||||
|
if (it != texture_cache.end()) {
|
||||||
|
return *it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the texture and add it to the cache
|
||||||
|
vk::Image new_texture = LoadTexture(texture_path);
|
||||||
|
if (new_texture) {
|
||||||
|
VkImage raw_handle = *new_texture;
|
||||||
|
texture_cache.emplace(texture_path, std::move(new_texture));
|
||||||
|
return raw_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If loading fails, return the default texture if it exists
|
||||||
|
LOG_WARNING(Render_Vulkan, "Failed to load texture: {}, using default", texture_path);
|
||||||
|
if (default_texture.has_value()) {
|
||||||
|
return *(*default_texture);
|
||||||
|
}
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextureManager::ReloadTexture(const std::string& texture_path) {
|
||||||
|
std::lock_guard<std::mutex> lock(texture_mutex);
|
||||||
|
|
||||||
|
// Remove the texture from cache if it exists
|
||||||
|
auto it = texture_cache.find(texture_path);
|
||||||
|
if (it != texture_cache.end()) {
|
||||||
|
LOG_INFO(Render_Vulkan, "Reloading texture: {}", texture_path);
|
||||||
|
texture_cache.erase(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The texture will be reloaded on next GetTexture call
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextureManager::IsTextureLoadedCorrectly(VkImage texture) {
|
||||||
|
// Check if the texture handle is valid
|
||||||
|
static const VkImage null_handle = VK_NULL_HANDLE;
|
||||||
|
return texture != null_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextureManager::CleanupTextureCache() {
|
||||||
|
std::lock_guard<std::mutex> lock(texture_mutex);
|
||||||
|
|
||||||
|
// TODO: track usage and remove unused textures [ZEP]
|
||||||
|
|
||||||
|
LOG_INFO(Render_Vulkan, "Handling texture cache cleanup, current size: {}", texture_cache.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextureManager::HandleTextureRendering(const std::string& texture_path,
|
||||||
|
std::function<void(VkImage)> render_callback) {
|
||||||
|
VkImage texture = GetTexture(texture_path);
|
||||||
|
|
||||||
|
if (!IsTextureLoadedCorrectly(texture)) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Texture failed to load correctly: {}, attempting reload", texture_path);
|
||||||
|
ReloadTexture(texture_path);
|
||||||
|
texture = GetTexture(texture_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the rendering callback with the texture
|
||||||
|
render_callback(texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
vk::Image TextureManager::LoadTexture(const std::string& texture_path) {
|
||||||
|
// TODO: load image data from disk
|
||||||
|
// and create a proper Vulkan texture [ZEP]
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(texture_path)) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Texture file not found: {}", texture_path);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
LOG_INFO(Render_Vulkan, "Loaded texture: {}", texture_path);
|
||||||
|
|
||||||
|
// TODO: create an actual VkImage [ZEP]
|
||||||
|
return CreateDefaultTexture();
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Error loading texture {}: {}", texture_path, e.what());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vk::Image TextureManager::CreateDefaultTexture() {
|
||||||
|
// Create a small default texture (1x1 pixel) to use as a fallback
|
||||||
|
// const VkExtent2D extent{1, 1};
|
||||||
|
|
||||||
|
// Create image
|
||||||
|
// Avoid unused variable warning by commenting out the unused struct
|
||||||
|
// VkImageCreateInfo image_ci{
|
||||||
|
// .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
|
||||||
|
// .pNext = nullptr,
|
||||||
|
// .flags = 0,
|
||||||
|
// .imageType = VK_IMAGE_TYPE_2D,
|
||||||
|
// .format = texture_format,
|
||||||
|
// .extent = {extent.width, extent.height, 1},
|
||||||
|
// .mipLevels = 1,
|
||||||
|
// .arrayLayers = 1,
|
||||||
|
// .samples = VK_SAMPLE_COUNT_1_BIT,
|
||||||
|
// .tiling = VK_IMAGE_TILING_OPTIMAL,
|
||||||
|
// .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
|
||||||
|
// .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
||||||
|
// .queueFamilyIndexCount = 0,
|
||||||
|
// .pQueueFamilyIndices = nullptr,
|
||||||
|
// .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// TODO: create an actual VkImage [ZEP]
|
||||||
|
LOG_INFO(Render_Vulkan, "Created default fallback texture");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Vulkan
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <functional>
|
||||||
|
#include <atomic>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
||||||
|
|
||||||
|
namespace Vulkan {
|
||||||
|
|
||||||
|
class Device;
|
||||||
|
class MemoryAllocator;
|
||||||
|
|
||||||
|
// Enhanced texture manager for better error handling and thread safety
|
||||||
|
class TextureManager {
|
||||||
|
public:
|
||||||
|
explicit TextureManager(const Device& device, MemoryAllocator& memory_allocator);
|
||||||
|
~TextureManager();
|
||||||
|
|
||||||
|
// Get a texture from the cache, loading it if necessary
|
||||||
|
VkImage GetTexture(const std::string& texture_path);
|
||||||
|
|
||||||
|
// Force a texture to reload from disk
|
||||||
|
void ReloadTexture(const std::string& texture_path);
|
||||||
|
|
||||||
|
// Check if a texture is loaded correctly
|
||||||
|
bool IsTextureLoadedCorrectly(VkImage texture);
|
||||||
|
|
||||||
|
// Remove old textures from the cache
|
||||||
|
void CleanupTextureCache();
|
||||||
|
|
||||||
|
// Handle texture rendering, with automatic reload if needed
|
||||||
|
void HandleTextureRendering(const std::string& texture_path,
|
||||||
|
std::function<void(VkImage)> render_callback);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Load a texture from disk and create a Vulkan image
|
||||||
|
vk::Image LoadTexture(const std::string& texture_path);
|
||||||
|
|
||||||
|
// Create a default texture to use in case of errors
|
||||||
|
vk::Image CreateDefaultTexture();
|
||||||
|
|
||||||
|
const Device& device;
|
||||||
|
MemoryAllocator& memory_allocator;
|
||||||
|
std::mutex texture_mutex;
|
||||||
|
std::unordered_map<std::string, vk::Image> texture_cache;
|
||||||
|
std::optional<vk::Image> default_texture;
|
||||||
|
VkFormat texture_format = VK_FORMAT_B8G8R8A8_SRGB;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Vulkan
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -80,8 +81,10 @@ void TextureCache<P>::RunGarbageCollector() {
|
||||||
const auto Configure = [&](bool allow_aggressive) {
|
const auto Configure = [&](bool allow_aggressive) {
|
||||||
high_priority_mode = total_used_memory >= expected_memory;
|
high_priority_mode = total_used_memory >= expected_memory;
|
||||||
aggressive_mode = allow_aggressive && total_used_memory >= critical_memory;
|
aggressive_mode = allow_aggressive && total_used_memory >= critical_memory;
|
||||||
ticks_to_destroy = aggressive_mode ? 10ULL : high_priority_mode ? 25ULL : 50ULL;
|
// Reduce ticks_to_destroy to be more aggressive in freeing memory
|
||||||
num_iterations = aggressive_mode ? 40 : (high_priority_mode ? 20 : 10);
|
ticks_to_destroy = aggressive_mode ? 5ULL : high_priority_mode ? 15ULL : 40ULL;
|
||||||
|
// Increase num_iterations to clean up more resources at once for memory-intensive games
|
||||||
|
num_iterations = aggressive_mode ? 60 : (high_priority_mode ? 30 : 15);
|
||||||
};
|
};
|
||||||
const auto Cleanup = [this, &num_iterations, &high_priority_mode,
|
const auto Cleanup = [this, &num_iterations, &high_priority_mode,
|
||||||
&aggressive_mode](ImageId image_id) {
|
&aggressive_mode](ImageId image_id) {
|
||||||
|
|
@ -95,7 +98,8 @@ void TextureCache<P>::RunGarbageCollector() {
|
||||||
// used by the async decoder thread.
|
// used by the async decoder thread.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!aggressive_mode && True(image.flags & ImageFlagBits::CostlyLoad)) {
|
// Be more aggressive with cleanup for memory-intensive games
|
||||||
|
if (!aggressive_mode && !high_priority_mode && True(image.flags & ImageFlagBits::CostlyLoad)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const bool must_download =
|
const bool must_download =
|
||||||
|
|
@ -118,19 +122,20 @@ void TextureCache<P>::RunGarbageCollector() {
|
||||||
DeleteImage(image_id, image.scale_tick > frame_tick + 5);
|
DeleteImage(image_id, image.scale_tick > frame_tick + 5);
|
||||||
if (total_used_memory < critical_memory) {
|
if (total_used_memory < critical_memory) {
|
||||||
if (aggressive_mode) {
|
if (aggressive_mode) {
|
||||||
// Sink the aggresiveness.
|
// Sink the aggresiveness more gradually to prevent oscillation
|
||||||
num_iterations >>= 2;
|
num_iterations = num_iterations * 3 / 4;
|
||||||
aggressive_mode = false;
|
aggressive_mode = false;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (high_priority_mode && total_used_memory < expected_memory) {
|
if (high_priority_mode && total_used_memory < expected_memory) {
|
||||||
num_iterations >>= 1;
|
num_iterations = num_iterations * 3 / 4;
|
||||||
high_priority_mode = false;
|
high_priority_mode = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Run garbage collection more frequently for memory-intensive games
|
||||||
// Try to remove anything old enough and not high priority.
|
// Try to remove anything old enough and not high priority.
|
||||||
Configure(false);
|
Configure(false);
|
||||||
lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
|
lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
|
||||||
|
|
@ -138,19 +143,67 @@ void TextureCache<P>::RunGarbageCollector() {
|
||||||
// If pressure is still too high, prune aggressively.
|
// If pressure is still too high, prune aggressively.
|
||||||
if (total_used_memory >= critical_memory) {
|
if (total_used_memory >= critical_memory) {
|
||||||
Configure(true);
|
Configure(true);
|
||||||
|
// Make a more thorough sweep with more aggressive settings
|
||||||
|
lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy / 2, Cleanup);
|
||||||
|
|
||||||
|
// If we're still in a critical memory situation, do emergency cleanup
|
||||||
|
if (total_used_memory >= critical_memory + 50_MiB) {
|
||||||
|
// Last resort emergency cleanup - reduce thresholds dramatically
|
||||||
|
ticks_to_destroy = 1;
|
||||||
|
num_iterations = 100;
|
||||||
lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
|
lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
template <class P>
|
template <class P>
|
||||||
void TextureCache<P>::TickFrame() {
|
void TextureCache<P>::TickFrame() {
|
||||||
|
static u64 consecutive_high_memory_frames = 0;
|
||||||
|
static constexpr u64 EMERGENCY_CLEANUP_THRESHOLD = 120; // ~2 seconds at 60 FPS
|
||||||
|
|
||||||
// If we can obtain the memory info, use it instead of the estimate.
|
// If we can obtain the memory info, use it instead of the estimate.
|
||||||
if (runtime.CanReportMemoryUsage()) {
|
if (runtime.CanReportMemoryUsage()) {
|
||||||
total_used_memory = runtime.GetDeviceMemoryUsage();
|
total_used_memory = runtime.GetDeviceMemoryUsage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track consecutive high memory frames to detect potential leaks
|
||||||
|
if (total_used_memory > critical_memory) {
|
||||||
|
consecutive_high_memory_frames++;
|
||||||
|
if (consecutive_high_memory_frames > EMERGENCY_CLEANUP_THRESHOLD) {
|
||||||
|
// Emergency situation - extreme memory pressure for extended time
|
||||||
|
// This likely indicates a leak or insufficient cleanup
|
||||||
|
LOG_WARNING(Render, "Emergency texture cache cleanup triggered after {} frames of high memory usage",
|
||||||
|
consecutive_high_memory_frames);
|
||||||
|
|
||||||
|
// Force immediate cleanup of all pending resources
|
||||||
|
sentenced_images.ForceDestroyAll();
|
||||||
|
sentenced_framebuffers.ForceDestroyAll();
|
||||||
|
sentenced_image_view.ForceDestroyAll();
|
||||||
|
|
||||||
|
// Do a forced garbage collection pass
|
||||||
|
bool saved_value = has_deleted_images;
|
||||||
|
RunGarbageCollector();
|
||||||
|
has_deleted_images = saved_value;
|
||||||
|
|
||||||
|
// Reset counter but keep some pressure
|
||||||
|
consecutive_high_memory_frames = 30;
|
||||||
|
}
|
||||||
|
else if (consecutive_high_memory_frames > 60) { // If high memory for >60 frames (~1 second)
|
||||||
|
// Force a more aggressive cleanup cycle
|
||||||
|
RunGarbageCollector();
|
||||||
|
consecutive_high_memory_frames = 45; // Reset but keep some pressure
|
||||||
|
}
|
||||||
|
} else if (total_used_memory > expected_memory) {
|
||||||
|
// Use u64(1) to ensure type compatibility, avoiding the ULL suffix
|
||||||
|
consecutive_high_memory_frames = std::max(u64(1), consecutive_high_memory_frames / 2);
|
||||||
|
} else {
|
||||||
|
consecutive_high_memory_frames = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (total_used_memory > minimum_memory) {
|
if (total_used_memory > minimum_memory) {
|
||||||
RunGarbageCollector();
|
RunGarbageCollector();
|
||||||
}
|
}
|
||||||
|
|
||||||
sentenced_images.Tick();
|
sentenced_images.Tick();
|
||||||
sentenced_framebuffers.Tick();
|
sentenced_framebuffers.Tick();
|
||||||
sentenced_image_view.Tick();
|
sentenced_image_view.Tick();
|
||||||
|
|
@ -2165,27 +2218,35 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
|
||||||
if (image.HasScaled()) {
|
if (image.HasScaled()) {
|
||||||
total_used_memory -= GetScaledImageSizeBytes(image);
|
total_used_memory -= GetScaledImageSizeBytes(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate accurate memory usage for this image
|
||||||
u64 tentative_size = std::max(image.guest_size_bytes, image.unswizzled_size_bytes);
|
u64 tentative_size = std::max(image.guest_size_bytes, image.unswizzled_size_bytes);
|
||||||
if ((IsPixelFormatASTC(image.info.format) &&
|
if ((IsPixelFormatASTC(image.info.format) &&
|
||||||
True(image.flags & ImageFlagBits::AcceleratedUpload)) ||
|
True(image.flags & ImageFlagBits::AcceleratedUpload)) ||
|
||||||
True(image.flags & ImageFlagBits::Converted)) {
|
True(image.flags & ImageFlagBits::Converted)) {
|
||||||
tentative_size = TranscodedAstcSize(tentative_size, image.info.format);
|
tentative_size = TranscodedAstcSize(tentative_size, image.info.format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure memory usage is properly accounted for
|
||||||
total_used_memory -= Common::AlignUp(tentative_size, 1024);
|
total_used_memory -= Common::AlignUp(tentative_size, 1024);
|
||||||
|
|
||||||
const GPUVAddr gpu_addr = image.gpu_addr;
|
const GPUVAddr gpu_addr = image.gpu_addr;
|
||||||
const auto alloc_it = image_allocs_table.find(gpu_addr);
|
const auto alloc_it = image_allocs_table.find(gpu_addr);
|
||||||
if (alloc_it == image_allocs_table.end()) {
|
if (alloc_it == image_allocs_table.end()) {
|
||||||
ASSERT_MSG(false, "Trying to delete an image alloc that does not exist in address 0x{:x}",
|
LOG_ERROR(HW_GPU, "Trying to delete an image alloc that does not exist in address 0x{:x}",
|
||||||
gpu_addr);
|
gpu_addr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageAllocId alloc_id = alloc_it->second;
|
const ImageAllocId alloc_id = alloc_it->second;
|
||||||
std::vector<ImageId>& alloc_images = slot_image_allocs[alloc_id].images;
|
std::vector<ImageId>& alloc_images = slot_image_allocs[alloc_id].images;
|
||||||
const auto alloc_image_it = std::ranges::find(alloc_images, image_id);
|
const auto alloc_image_it = std::ranges::find(alloc_images, image_id);
|
||||||
if (alloc_image_it == alloc_images.end()) {
|
if (alloc_image_it == alloc_images.end()) {
|
||||||
ASSERT_MSG(false, "Trying to delete an image that does not exist");
|
LOG_ERROR(HW_GPU, "Trying to delete an image that does not exist");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure image is properly untracked and unregistered before deletion
|
||||||
ASSERT_MSG(False(image.flags & ImageFlagBits::Tracked), "Image was not untracked");
|
ASSERT_MSG(False(image.flags & ImageFlagBits::Tracked), "Image was not untracked");
|
||||||
ASSERT_MSG(False(image.flags & ImageFlagBits::Registered), "Image was not unregistered");
|
ASSERT_MSG(False(image.flags & ImageFlagBits::Registered), "Image was not unregistered");
|
||||||
|
|
||||||
|
|
@ -2196,6 +2257,8 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
|
||||||
for (size_t rt = 0; rt < NUM_RT; ++rt) {
|
for (size_t rt = 0; rt < NUM_RT; ++rt) {
|
||||||
dirty[Dirty::ColorBuffer0 + rt] = true;
|
dirty[Dirty::ColorBuffer0 + rt] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear render target references
|
||||||
const std::span<const ImageViewId> image_view_ids = image.image_view_ids;
|
const std::span<const ImageViewId> image_view_ids = image.image_view_ids;
|
||||||
for (const ImageViewId image_view_id : image_view_ids) {
|
for (const ImageViewId image_view_id : image_view_ids) {
|
||||||
std::ranges::replace(render_targets.color_buffer_ids, image_view_id, ImageViewId{});
|
std::ranges::replace(render_targets.color_buffer_ids, image_view_id, ImageViewId{});
|
||||||
|
|
@ -2203,9 +2266,12 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
|
||||||
render_targets.depth_buffer_id = ImageViewId{};
|
render_targets.depth_buffer_id = ImageViewId{};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up references and dependencies
|
||||||
RemoveImageViewReferences(image_view_ids);
|
RemoveImageViewReferences(image_view_ids);
|
||||||
RemoveFramebuffers(image_view_ids);
|
RemoveFramebuffers(image_view_ids);
|
||||||
|
|
||||||
|
// Handle aliased images
|
||||||
for (const AliasedImage& alias : image.aliased_images) {
|
for (const AliasedImage& alias : image.aliased_images) {
|
||||||
ImageBase& other_image = slot_images[alias.id];
|
ImageBase& other_image = slot_images[alias.id];
|
||||||
[[maybe_unused]] const size_t num_removed_aliases =
|
[[maybe_unused]] const size_t num_removed_aliases =
|
||||||
|
|
@ -2213,33 +2279,43 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
|
||||||
return other_alias.id == image_id;
|
return other_alias.id == image_id;
|
||||||
});
|
});
|
||||||
other_image.CheckAliasState();
|
other_image.CheckAliasState();
|
||||||
ASSERT_MSG(num_removed_aliases == 1, "Invalid number of removed aliases: {}",
|
if (num_removed_aliases != 1) {
|
||||||
num_removed_aliases);
|
LOG_WARNING(HW_GPU, "Invalid number of removed aliases: {}", num_removed_aliases);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle overlapping images
|
||||||
for (const ImageId overlap_id : image.overlapping_images) {
|
for (const ImageId overlap_id : image.overlapping_images) {
|
||||||
ImageBase& other_image = slot_images[overlap_id];
|
ImageBase& other_image = slot_images[overlap_id];
|
||||||
[[maybe_unused]] const size_t num_removed_overlaps = std::erase_if(
|
[[maybe_unused]] const size_t num_removed_overlaps = std::erase_if(
|
||||||
other_image.overlapping_images,
|
other_image.overlapping_images,
|
||||||
[image_id](const ImageId other_overlap_id) { return other_overlap_id == image_id; });
|
[image_id](const ImageId other_overlap_id) { return other_overlap_id == image_id; });
|
||||||
other_image.CheckBadOverlapState();
|
other_image.CheckBadOverlapState();
|
||||||
ASSERT_MSG(num_removed_overlaps == 1, "Invalid number of removed overlapps: {}",
|
if (num_removed_overlaps != 1) {
|
||||||
num_removed_overlaps);
|
LOG_WARNING(HW_GPU, "Invalid number of removed overlaps: {}", num_removed_overlaps);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free resources - either immediately or queue for delayed destruction
|
||||||
for (const ImageViewId image_view_id : image_view_ids) {
|
for (const ImageViewId image_view_id : image_view_ids) {
|
||||||
if (!immediate_delete) {
|
if (!immediate_delete) {
|
||||||
sentenced_image_view.Push(std::move(slot_image_views[image_view_id]));
|
sentenced_image_view.Push(std::move(slot_image_views[image_view_id]));
|
||||||
}
|
}
|
||||||
slot_image_views.erase(image_view_id);
|
slot_image_views.erase(image_view_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!immediate_delete) {
|
if (!immediate_delete) {
|
||||||
sentenced_images.Push(std::move(slot_images[image_id]));
|
sentenced_images.Push(std::move(slot_images[image_id]));
|
||||||
}
|
}
|
||||||
slot_images.erase(image_id);
|
slot_images.erase(image_id);
|
||||||
|
|
||||||
|
// Clean up allocation table
|
||||||
alloc_images.erase(alloc_image_it);
|
alloc_images.erase(alloc_image_it);
|
||||||
if (alloc_images.empty()) {
|
if (alloc_images.empty()) {
|
||||||
image_allocs_table.erase(alloc_it);
|
image_allocs_table.erase(alloc_it);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark tables as invalidated
|
||||||
for (size_t c : active_channel_ids) {
|
for (size_t c : active_channel_ids) {
|
||||||
auto& channel_info = channel_storage[c];
|
auto& channel_info = channel_storage[c];
|
||||||
if constexpr (ENABLE_VALIDATION) {
|
if constexpr (ENABLE_VALIDATION) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,446 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "video_core/vulkan_common/hybrid_memory.h"
|
||||||
|
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__)
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <sys/syscall.h>
|
||||||
|
#include <linux/userfaultfd.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Vulkan {
|
||||||
|
|
||||||
|
void PredictiveReuseManager::RecordUsage(u64 address, u64 size, bool write_access) {
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
|
||||||
|
// Add to history, removing oldest entries if we're past max_history
|
||||||
|
access_history.push_back({address, size, write_access, current_timestamp++});
|
||||||
|
if (access_history.size() > max_history) {
|
||||||
|
access_history.erase(access_history.begin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PredictiveReuseManager::IsHotRegion(u64 address, u64 size) const {
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
|
||||||
|
// Check if this memory region has been accessed frequently
|
||||||
|
const u64 end_address = address + size;
|
||||||
|
int access_count = 0;
|
||||||
|
|
||||||
|
for (const auto& access : access_history) {
|
||||||
|
const u64 access_end = access.address + access.size;
|
||||||
|
|
||||||
|
// Check for overlap
|
||||||
|
if (!(end_address <= access.address || address >= access_end)) {
|
||||||
|
access_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consider a region "hot" if it has been accessed in at least 10% of recent accesses
|
||||||
|
return access_count >= static_cast<int>(std::max<size_t>(1, max_history / 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PredictiveReuseManager::EvictRegion(u64 address, u64 size) {
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
|
||||||
|
// Remove any history entries that overlap with this region
|
||||||
|
const u64 end_address = address + size;
|
||||||
|
|
||||||
|
access_history.erase(
|
||||||
|
std::remove_if(access_history.begin(), access_history.end(),
|
||||||
|
[address, end_address](const MemoryAccess& access) {
|
||||||
|
const u64 access_end = access.address + access.size;
|
||||||
|
// Check for overlap
|
||||||
|
return !(end_address <= access.address || address >= access_end);
|
||||||
|
}),
|
||||||
|
access_history.end()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PredictiveReuseManager::ClearHistory() {
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
access_history.clear();
|
||||||
|
current_timestamp = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
|
||||||
|
void FaultManagedAllocator::Touch(size_t addr) {
|
||||||
|
lru.remove(addr);
|
||||||
|
lru.push_front(addr);
|
||||||
|
dirty_set.insert(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FaultManagedAllocator::EnforceLimit() {
|
||||||
|
while (lru.size() > MaxPages) {
|
||||||
|
size_t evict = lru.back();
|
||||||
|
lru.pop_back();
|
||||||
|
|
||||||
|
auto it = page_map.find(evict);
|
||||||
|
if (it != page_map.end()) {
|
||||||
|
if (dirty_set.count(evict)) {
|
||||||
|
// Compress and store dirty page before evicting
|
||||||
|
std::vector<u8> compressed((u8*)it->second, (u8*)it->second + PageSize);
|
||||||
|
compressed_store[evict] = std::move(compressed);
|
||||||
|
dirty_set.erase(evict);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__)
|
||||||
|
munmap(it->second, PageSize);
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
VirtualFree(it->second, 0, MEM_RELEASE);
|
||||||
|
#endif
|
||||||
|
page_map.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void* FaultManagedAllocator::GetOrAlloc(size_t addr) {
|
||||||
|
std::lock_guard<std::mutex> guard(lock);
|
||||||
|
|
||||||
|
if (page_map.count(addr)) {
|
||||||
|
Touch(addr);
|
||||||
|
return page_map[addr];
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__)
|
||||||
|
void* mem = mmap(nullptr, PageSize, PROT_READ | PROT_WRITE,
|
||||||
|
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||||
|
|
||||||
|
if (mem == MAP_FAILED) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Failed to mmap memory for fault handler");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
void* mem = VirtualAlloc(nullptr, PageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||||
|
if (!mem) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Failed to VirtualAlloc memory for fault handler");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (compressed_store.count(addr)) {
|
||||||
|
// Decompress stored page data
|
||||||
|
std::memcpy(mem, compressed_store[addr].data(), compressed_store[addr].size());
|
||||||
|
compressed_store.erase(addr);
|
||||||
|
} else {
|
||||||
|
std::memset(mem, 0, PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
page_map[addr] = mem;
|
||||||
|
lru.push_front(addr);
|
||||||
|
dirty_set.insert(addr);
|
||||||
|
EnforceLimit();
|
||||||
|
|
||||||
|
return mem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
// Static member initialization
|
||||||
|
FaultManagedAllocator* FaultManagedAllocator::current_instance = nullptr;
|
||||||
|
|
||||||
|
LONG WINAPI FaultManagedAllocator::VectoredExceptionHandler(PEXCEPTION_POINTERS exception_info) {
|
||||||
|
// Only handle access violations (page faults)
|
||||||
|
if (exception_info->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION) {
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!current_instance) {
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the faulting address - use ULONG_PTR for Windows
|
||||||
|
const ULONG_PTR fault_addr = static_cast<ULONG_PTR>(exception_info->ExceptionRecord->ExceptionInformation[1]);
|
||||||
|
const ULONG_PTR base_addr = reinterpret_cast<ULONG_PTR>(current_instance->base_address);
|
||||||
|
|
||||||
|
// Check if the address is within our managed range
|
||||||
|
if (fault_addr < base_addr ||
|
||||||
|
fault_addr >= (base_addr + static_cast<ULONG_PTR>(current_instance->memory_size))) {
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the base address of the page
|
||||||
|
const ULONG_PTR page_addr = fault_addr & ~(static_cast<ULONG_PTR>(PageSize) - 1);
|
||||||
|
const size_t relative_addr = static_cast<size_t>(page_addr - base_addr);
|
||||||
|
|
||||||
|
// Handle the fault by allocating memory
|
||||||
|
void* page = current_instance->GetOrAlloc(relative_addr);
|
||||||
|
if (!page) {
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the page data to the faulting address
|
||||||
|
DWORD old_protect;
|
||||||
|
void* target_addr = reinterpret_cast<void*>(page_addr);
|
||||||
|
|
||||||
|
// Make the target page writable
|
||||||
|
if (VirtualProtect(target_addr, PageSize, PAGE_READWRITE, &old_protect)) {
|
||||||
|
std::memcpy(target_addr, page, PageSize);
|
||||||
|
// Restore original protection
|
||||||
|
VirtualProtect(target_addr, PageSize, old_protect, &old_protect);
|
||||||
|
return EXCEPTION_CONTINUE_EXECUTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FaultManagedAllocator::ExceptionHandlerThread() {
|
||||||
|
while (running) {
|
||||||
|
// Sleep to avoid busy waiting
|
||||||
|
Sleep(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void FaultManagedAllocator::Initialize(void* base, size_t size) {
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__)
|
||||||
|
uffd = static_cast<int>(syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK));
|
||||||
|
if (uffd < 0) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Failed to create userfaultfd, fault handling disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct uffdio_api api = { .api = UFFD_API };
|
||||||
|
ioctl(uffd, UFFDIO_API, &api);
|
||||||
|
|
||||||
|
struct uffdio_register reg = {
|
||||||
|
.range = { .start = (uintptr_t)base, .len = size },
|
||||||
|
.mode = UFFDIO_REGISTER_MODE_MISSING
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ioctl(uffd, UFFDIO_REGISTER, ®) < 0) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Failed to register memory range with userfaultfd");
|
||||||
|
close(uffd);
|
||||||
|
uffd = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
fault_handler = std::thread(&FaultManagedAllocator::FaultThread, this);
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
// Setup Windows memory for fault handling
|
||||||
|
base_address = base;
|
||||||
|
memory_size = size;
|
||||||
|
|
||||||
|
// Reserve memory range but don't commit it yet - it will be demand-paged
|
||||||
|
DWORD oldProtect;
|
||||||
|
VirtualProtect(base, size, PAGE_NOACCESS, &oldProtect);
|
||||||
|
|
||||||
|
// Install a vectored exception handler
|
||||||
|
current_instance = this;
|
||||||
|
AddVectoredExceptionHandler(1, VectoredExceptionHandler);
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
exception_handler = std::thread(&FaultManagedAllocator::ExceptionHandlerThread, this);
|
||||||
|
|
||||||
|
LOG_INFO(Render_Vulkan, "Windows fault-managed memory initialized at {:p}, size: {}",
|
||||||
|
base, size);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__)
|
||||||
|
void FaultManagedAllocator::FaultThread() {
|
||||||
|
struct pollfd pfd = { uffd, POLLIN, 0 };
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
if (poll(&pfd, 1, 10) > 0) {
|
||||||
|
struct uffd_msg msg;
|
||||||
|
read(uffd, &msg, sizeof(msg));
|
||||||
|
|
||||||
|
if (msg.event == UFFD_EVENT_PAGEFAULT) {
|
||||||
|
size_t addr = msg.arg.pagefault.address & ~(PageSize - 1);
|
||||||
|
void* page = GetOrAlloc(addr);
|
||||||
|
|
||||||
|
if (page) {
|
||||||
|
struct uffdio_copy copy = {
|
||||||
|
.dst = (uintptr_t)addr,
|
||||||
|
.src = (uintptr_t)page,
|
||||||
|
.len = PageSize,
|
||||||
|
.mode = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
ioctl(uffd, UFFDIO_COPY, ©);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void* FaultManagedAllocator::Translate(size_t addr) {
|
||||||
|
std::lock_guard<std::mutex> guard(lock);
|
||||||
|
|
||||||
|
size_t base = addr & ~(PageSize - 1);
|
||||||
|
if (!page_map.count(base)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Touch(base);
|
||||||
|
return (u8*)page_map[base] + (addr % PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FaultManagedAllocator::SaveSnapshot(const std::string& path) {
|
||||||
|
std::lock_guard<std::mutex> guard(lock);
|
||||||
|
|
||||||
|
std::ofstream out(path, std::ios::binary);
|
||||||
|
if (!out) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Failed to open snapshot file for writing: {}", path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& [addr, mem] : page_map) {
|
||||||
|
out.write(reinterpret_cast<const char*>(&addr), sizeof(addr));
|
||||||
|
out.write(reinterpret_cast<const char*>(mem), PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(Render_Vulkan, "Saved memory snapshot to {}", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FaultManagedAllocator::SaveDifferentialSnapshot(const std::string& path) {
|
||||||
|
std::lock_guard<std::mutex> guard(lock);
|
||||||
|
|
||||||
|
std::ofstream out(path, std::ios::binary);
|
||||||
|
if (!out) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Failed to open diff snapshot file for writing: {}", path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t dirty_count = 0;
|
||||||
|
for (const auto& addr : dirty_set) {
|
||||||
|
if (page_map.count(addr)) {
|
||||||
|
out.write(reinterpret_cast<const char*>(&addr), sizeof(addr));
|
||||||
|
out.write(reinterpret_cast<const char*>(page_map[addr]), PageSize);
|
||||||
|
dirty_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(Render_Vulkan, "Saved differential snapshot to {} ({} dirty pages)",
|
||||||
|
path, dirty_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FaultManagedAllocator::ClearDirtySet() {
|
||||||
|
std::lock_guard<std::mutex> guard(lock);
|
||||||
|
dirty_set.clear();
|
||||||
|
LOG_DEBUG(Render_Vulkan, "Cleared dirty page tracking");
|
||||||
|
}
|
||||||
|
|
||||||
|
FaultManagedAllocator::~FaultManagedAllocator() {
|
||||||
|
running = false;
|
||||||
|
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__)
|
||||||
|
if (fault_handler.joinable()) {
|
||||||
|
fault_handler.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& [addr, mem] : page_map) {
|
||||||
|
munmap(mem, PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uffd != -1) {
|
||||||
|
close(uffd);
|
||||||
|
}
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
if (exception_handler.joinable()) {
|
||||||
|
exception_handler.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the vectored exception handler
|
||||||
|
RemoveVectoredExceptionHandler(VectoredExceptionHandler);
|
||||||
|
current_instance = nullptr;
|
||||||
|
|
||||||
|
for (auto& [addr, mem] : page_map) {
|
||||||
|
VirtualFree(mem, 0, MEM_RELEASE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free the base memory if needed
|
||||||
|
if (base_address) {
|
||||||
|
VirtualFree(base_address, 0, MEM_RELEASE);
|
||||||
|
base_address = nullptr;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif // defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
|
||||||
|
|
||||||
|
HybridMemory::HybridMemory(const Device& device_, MemoryAllocator& allocator, size_t reuse_history)
|
||||||
|
: device(device_), memory_allocator(allocator), reuse_manager(reuse_history) {
|
||||||
|
}
|
||||||
|
|
||||||
|
HybridMemory::~HybridMemory() = default;
|
||||||
|
|
||||||
|
void HybridMemory::InitializeGuestMemory(void* base, size_t size) {
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
|
||||||
|
fmaa.Initialize(base, size);
|
||||||
|
LOG_INFO(Render_Vulkan, "Initialized fault-managed guest memory at {:p}, size: {}",
|
||||||
|
base, size);
|
||||||
|
#else
|
||||||
|
LOG_INFO(Render_Vulkan, "Fault-managed memory not supported on this platform");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void* HybridMemory::TranslateAddress(size_t addr) {
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
|
||||||
|
return fmaa.Translate(addr);
|
||||||
|
#else
|
||||||
|
return nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
ComputeBuffer HybridMemory::CreateComputeBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
|
||||||
|
MemoryUsage memory_type) {
|
||||||
|
ComputeBuffer buffer;
|
||||||
|
buffer.size = size;
|
||||||
|
|
||||||
|
VkBufferCreateInfo buffer_ci = {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.flags = 0,
|
||||||
|
.size = size,
|
||||||
|
.usage = usage | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
|
||||||
|
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
||||||
|
.queueFamilyIndexCount = 0,
|
||||||
|
.pQueueFamilyIndices = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Using CreateBuffer directly handles memory allocation internally
|
||||||
|
buffer.buffer = memory_allocator.CreateBuffer(buffer_ci, memory_type);
|
||||||
|
|
||||||
|
LOG_DEBUG(Render_Vulkan, "Created compute buffer: size={}, usage={:x}",
|
||||||
|
size, usage);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HybridMemory::SaveSnapshot(const std::string& path) {
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
|
||||||
|
fmaa.SaveSnapshot(path);
|
||||||
|
#else
|
||||||
|
LOG_ERROR(Render_Vulkan, "Memory snapshots not supported on this platform");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void HybridMemory::SaveDifferentialSnapshot(const std::string& path) {
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
|
||||||
|
fmaa.SaveDifferentialSnapshot(path);
|
||||||
|
#else
|
||||||
|
LOG_ERROR(Render_Vulkan, "Differential memory snapshots not supported on this platform");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void HybridMemory::ResetDirtyTracking() {
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
|
||||||
|
fmaa.ClearDirtySet();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Vulkan
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <list>
|
||||||
|
#include <set>
|
||||||
|
#include <map>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "video_core/vulkan_common/vulkan_device.h"
|
||||||
|
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
|
||||||
|
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
||||||
|
|
||||||
|
namespace Vulkan {
|
||||||
|
|
||||||
|
struct ComputeBuffer {
|
||||||
|
vk::Buffer buffer{};
|
||||||
|
VkDeviceSize size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PredictiveReuseManager {
|
||||||
|
public:
|
||||||
|
explicit PredictiveReuseManager(size_t history_size) : max_history{history_size} {}
|
||||||
|
|
||||||
|
void RecordUsage(u64 address, u64 size, bool write_access);
|
||||||
|
bool IsHotRegion(u64 address, u64 size) const;
|
||||||
|
void EvictRegion(u64 address, u64 size);
|
||||||
|
void ClearHistory();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct MemoryAccess {
|
||||||
|
u64 address;
|
||||||
|
u64 size;
|
||||||
|
bool write_access;
|
||||||
|
u64 timestamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<MemoryAccess> access_history;
|
||||||
|
const size_t max_history;
|
||||||
|
u64 current_timestamp{0};
|
||||||
|
mutable std::mutex mutex;
|
||||||
|
};
|
||||||
|
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
|
||||||
|
class FaultManagedAllocator {
|
||||||
|
public:
|
||||||
|
static constexpr size_t PageSize = 0x1000;
|
||||||
|
static constexpr size_t MaxPages = 16384;
|
||||||
|
|
||||||
|
void Initialize(void* base, size_t size);
|
||||||
|
void* Translate(size_t addr);
|
||||||
|
void SaveSnapshot(const std::string& path);
|
||||||
|
void SaveDifferentialSnapshot(const std::string& path);
|
||||||
|
void ClearDirtySet();
|
||||||
|
~FaultManagedAllocator();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<size_t, void*> page_map;
|
||||||
|
std::list<size_t> lru;
|
||||||
|
std::set<size_t> dirty_set;
|
||||||
|
std::unordered_map<size_t, std::vector<u8>> compressed_store;
|
||||||
|
std::mutex lock;
|
||||||
|
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__)
|
||||||
|
int uffd = -1;
|
||||||
|
std::atomic<bool> running{false};
|
||||||
|
std::thread fault_handler;
|
||||||
|
void FaultThread();
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
void* base_address = nullptr;
|
||||||
|
size_t memory_size = 0;
|
||||||
|
HANDLE exception_port = nullptr;
|
||||||
|
std::atomic<bool> running{false};
|
||||||
|
std::thread exception_handler;
|
||||||
|
void ExceptionHandlerThread();
|
||||||
|
static LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS exception_info);
|
||||||
|
static FaultManagedAllocator* current_instance;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void Touch(size_t addr);
|
||||||
|
void EnforceLimit();
|
||||||
|
void* GetOrAlloc(size_t addr);
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class HybridMemory {
|
||||||
|
public:
|
||||||
|
explicit HybridMemory(const Device& device, MemoryAllocator& allocator, size_t reuse_history = 32);
|
||||||
|
~HybridMemory();
|
||||||
|
|
||||||
|
void InitializeGuestMemory(void* base, size_t size);
|
||||||
|
void* TranslateAddress(size_t addr);
|
||||||
|
|
||||||
|
ComputeBuffer CreateComputeBuffer(VkDeviceSize size, VkBufferUsageFlags usage, MemoryUsage memory_type);
|
||||||
|
|
||||||
|
void SaveSnapshot(const std::string& path);
|
||||||
|
void SaveDifferentialSnapshot(const std::string& path);
|
||||||
|
void ResetDirtyTracking();
|
||||||
|
|
||||||
|
private:
|
||||||
|
const Device& device;
|
||||||
|
MemoryAllocator& memory_allocator;
|
||||||
|
PredictiveReuseManager reuse_manager;
|
||||||
|
|
||||||
|
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
|
||||||
|
FaultManagedAllocator fmaa;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Vulkan
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
|
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
|
||||||
"name": "citron",
|
"name": "citron",
|
||||||
"builtin-baseline": "c82f74667287d3dc386bce81e44964370c91a289",
|
"builtin-baseline": "bc994510d2eb11aac7b43b03f67a7751d5bfe0e4",
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"boost-algorithm",
|
"boost-algorithm",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue