Compare commits

..

No commits in common. "master" and "v0.6-canary-refresh" have entirely different histories.

83 changed files with 258 additions and 3873 deletions

12
.gitmodules vendored
View File

@ -9,22 +9,22 @@
url = https://github.com/mozilla/cubeb.git
[submodule "dynarmic"]
path = externals/dynarmic
url = https://github.com/yuzu-mirror/dynarmic.git
url = https://git.citron-emu.org/Citron/dynarmic.git
[submodule "libusb"]
path = externals/libusb/libusb
url = https://github.com/libusb/libusb.git
[submodule "discord-rpc"]
path = externals/discord-rpc
url = https://github.com/yuzu-mirror/discord-rpc.git
url = https://git.citron-emu.org/Citron/discord-rpc.git
[submodule "Vulkan-Headers"]
path = externals/Vulkan-Headers
url = https://github.com/KhronosGroup/Vulkan-Headers.git
[submodule "sirit"]
path = externals/sirit
url = https://github.com/yuzu-mirror/sirit.git
url = https://git.citron-emu.org/Citron/sirit.git
[submodule "mbedtls"]
path = externals/mbedtls
url = https://github.com/yuzu-mirror/mbedtls.git
url = https://git.citron-emu.org/Citron/mbedtls.git
[submodule "xbyak"]
path = externals/xbyak
url = https://github.com/herumi/xbyak.git
@ -57,13 +57,13 @@
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
[submodule "breakpad"]
path = externals/breakpad
url = https://github.com/yuzu-mirror/breakpad.git
url = https://git.citron-emu.org/Citron/breakpad.git
[submodule "simpleini"]
path = externals/simpleini
url = https://github.com/brofield/simpleini.git
[submodule "oaknut"]
path = externals/oaknut
url = https://github.com/yuzu-mirror/oaknut
url = https://git.citron-emu.org/Citron/oaknut.git
[submodule "Vulkan-Utility-Libraries"]
path = externals/Vulkan-Utility-Libraries
url = https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git

View File

@ -17,45 +17,6 @@ if (MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3 /WX-")
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
if (TARGET SDL2::SDL2-static)
add_library(SDL2::SDL2 ALIAS SDL2::SDL2-static)
@ -137,7 +98,7 @@ endif()
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
if (ANDROID AND CITRON_DOWNLOAD_ANDROID_VVL)
set(vvl_version "1.4.309.0")
set(vvl_version "1.4.304.1")
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_final_lib "${vvl_lib_path}/libVkLayer_khronos_validation.so")
@ -169,23 +130,13 @@ if (CITRON_USE_BUNDLED_VCPKG)
if (CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a")
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")
else()
set(VCPKG_HOST_TRIPLET "x64-linux")
endif()
set(VCPKG_HOST_TRIPLET "x64-windows")
# this is to avoid CMake using the host pkg-config to find the host
# libraries when building for Android targets
set(PKG_CONFIG_EXECUTABLE "aarch64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE)
elseif (CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64")
set(VCPKG_TARGET_TRIPLET "x64-android")
# Detect host system (Windows or Linux)
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
set(VCPKG_HOST_TRIPLET "x64-windows")
else()
set(VCPKG_HOST_TRIPLET "x64-linux")
endif()
set(VCPKG_HOST_TRIPLET "x64-windows")
set(PKG_CONFIG_EXECUTABLE "x86_64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE)
else()
message(FATAL_ERROR "Unsupported Android architecture ${CMAKE_ANDROID_ARCH_ABI}")
@ -384,7 +335,7 @@ find_package(ZLIB REQUIRED)
find_package(zstd REQUIRED)
if (NOT CITRON_USE_EXTERNAL_VULKAN_HEADERS)
find_package(VulkanHeaders 1.4.313 REQUIRED)
find_package(VulkanHeaders 1.4.307 REQUIRED)
endif()
if (NOT CITRON_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES)
@ -447,7 +398,7 @@ if (ENABLE_SDL2)
if (CITRON_USE_BUNDLED_SDL2)
# Detect toolchain and platform
if ((MSVC_VERSION GREATER_EQUAL 1920) AND ARCHITECTURE_x86_64)
set(SDL2_VER "SDL2-2.28.2")
set(SDL2_VER "SDL2-2.32.0")
else()
message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable CITRON_USE_BUNDLED_SDL2 and provide your own.")
endif()
@ -594,7 +545,7 @@ endif()
# against all the src files. This should be used before making a pull request.
# =======================================================================
set(CLANG_FORMAT_POSTFIX "-15")
set(CLANG_FORMAT_POSTFIX "-18")
find_program(CLANG_FORMAT
NAMES clang-format${CLANG_FORMAT_POSTFIX}
clang-format
@ -605,7 +556,7 @@ if (NOT CLANG_FORMAT)
message(STATUS "Clang format not found! Downloading...")
set(CLANG_FORMAT "${PROJECT_BINARY_DIR}/externals/clang-format${CLANG_FORMAT_POSTFIX}.exe")
file(DOWNLOAD
https://github.com/yuzu-mirror/ext-windows-bin/raw/master/clang-format${CLANG_FORMAT_POSTFIX}.exe
https://git.citron-emu.org/Citron/ext-windows-bin/raw/master/clang-format${CLANG_FORMAT_POSTFIX}.exe
"${CLANG_FORMAT}" SHOW_PROGRESS
STATUS DOWNLOAD_SUCCESS)
if (NOT DOWNLOAD_SUCCESS EQUAL 0)
@ -733,3 +684,42 @@ if(ENABLE_QT AND UNIX AND NOT APPLE)
install(FILES "dist/org.citron_emu.citron.metainfo.xml"
DESTINATION "share/metainfo")
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()

View File

@ -8,7 +8,7 @@
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
function(download_bundled_external remote_path lib_name prefix_var)
set(package_base_url "https://github.com/yuzu-mirror/")
set(package_base_url "https://git.citron-emu.org/Citron/")
set(package_repo "no_platform")
set(package_extension "no_platform")
if (WIN32)

View File

@ -1,5 +1,4 @@
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
# SPDX-FileCopyrightText: 2025 citron Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
# Allow systemd-logind to manage user access to hidraw with this file
@ -8,13 +7,13 @@
# Switch Pro Controller (USB/Bluetooth)
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)
KERNEL=="hidraw*", KERNELS=="*057E:2006*", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", KERNELS=="*057e:2006*", MODE="0660", TAG+="uaccess"
# 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)
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="200e", MODE="0660", TAG+="uaccess"

2
externals/SDL vendored

@ -1 +1 @@
Subproject commit 2359383fc187386204c3bb22de89655a494cd128
Subproject commit cc016b0046d563287f0aa9f09b958b5e70d43696

@ -1 +1 @@
Subproject commit e2e53a724677f6eba8ff0ce1ccb64ee321785cbd
Subproject commit cacef3039d277c448c89336290ec3937270b0996

@ -1 +1 @@
Subproject commit 4e246c56ec5afb5ad66b9b04374d39ac04675c8e
Subproject commit bc3a4d9fd9b46729651a3cec4f5226f6272b8684

@ -1 +1 @@
Subproject commit 539c0a8d8e3733c9f25ea9a184c85c77504f1653
Subproject commit c788c52156f3ef7bc7ab769cb03c110a53ac8fcb

2
externals/vcpkg vendored

@ -1 +1 @@
Subproject commit 96d5fb3de135b86d7222c53f2352ca92827a156b
Subproject commit e40d24cb149dd138e7c11d490834fa2c81298b32

View File

@ -11,10 +11,10 @@ plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-parcelize")
kotlin("plugin.serialization") version "1.9.20"
kotlin("plugin.serialization") version "2.1.20-RC2"
id("androidx.navigation.safeargs.kotlin")
id("org.jlleitschuh.gradle.ktlint") version "11.4.0"
id("com.github.triplet.play") version "3.8.6"
id("org.jlleitschuh.gradle.ktlint") version "12.2.0"
id("com.github.triplet.play") version "3.12.1"
}
/**
@ -55,7 +55,7 @@ android {
defaultConfig {
// TODO If this is ever modified, change application_id in strings.xml
applicationId = "org.citron.citron_emu"
applicationId = "com.antutu.ABenchMark"
minSdk = 30
//noinspection EditedTargetSdkVersion
targetSdk = 35
@ -161,7 +161,7 @@ android {
externalNativeBuild {
cmake {
version = "4.0.1"
version = "3.31.6"
path = file("../../../CMakeLists.txt")
}
}
@ -179,8 +179,7 @@ android {
"-DCITRON_USE_BUNDLED_FFMPEG=ON",
"-DCITRON_ENABLE_LTO=ON",
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
)
abiFilters("arm64-v8a") // , "x86_64")
@ -204,7 +203,7 @@ tasks.getByPath("ktlintMainSourceSetCheck").doFirst { showFormatHelp.invoke() }
tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset")
ktlint {
version.set("0.47.1")
version.set("0.49.1")
android.set(true)
ignoreFailures.set(false)
disabledRules.set(
@ -229,23 +228,24 @@ play {
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.recyclerview:recyclerview:1.3.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.fragment:fragment-ktx:1.6.1")
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.recyclerview:recyclerview:1.4.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.fragment:fragment-ktx:1.8.6")
implementation("androidx.documentfile:documentfile:1.0.1")
implementation("com.google.android.material:material:1.9.0")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("io.coil-kt:coil:2.2.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
implementation("io.coil-kt:coil:2.7.0")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.window:window:1.2.0-beta03")
implementation("androidx.window:window:1.3.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.4")
implementation("androidx.navigation:navigation-ui-ktx:2.7.4")
implementation("androidx.navigation:navigation-fragment-ktx:2.8.8")
implementation("androidx.navigation:navigation-ui-ktx:2.8.8")
implementation("info.debatty:java-string-similarity:2.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}
fun runGitCommand(command: List<String>): String {

View File

@ -18,7 +18,6 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
RENDERER_DEBUG("debug"),
RENDERER_ENHANCED_SHADER_BUILDING("use_enhanced_shader_building"),
PICTURE_IN_PICTURE("picture_in_picture"),
USE_CUSTOM_RTC("custom_rtc_enabled"),
BLACK_BACKGROUNDS("black_backgrounds"),

View File

@ -180,62 +180,6 @@ 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(
SetupPage(
R.drawable.ic_controller,
@ -324,18 +268,6 @@ class SetupFragment : Fragment() {
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]) {
SetupWarningDialogFragment.newInstance(
currentPage.warningTitleId,
@ -414,30 +346,6 @@ 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
val getGamesDirectory =

View File

@ -17,7 +17,6 @@ class SetupWarningDialogFragment : DialogFragment() {
private var descriptionId: Int = 0
private var helpLinkId: Int = 0
private var page: Int = 0
private var allowSkip: Boolean = true
private lateinit var setupFragment: SetupFragment
@ -27,24 +26,17 @@ class SetupWarningDialogFragment : DialogFragment() {
descriptionId = requireArguments().getInt(DESCRIPTION)
helpLinkId = requireArguments().getInt(HELP_LINK)
page = requireArguments().getInt(PAGE)
allowSkip = requireArguments().getBoolean(ALLOW_SKIP, true)
setupFragment = requireParentFragment() as SetupFragment
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = MaterialAlertDialogBuilder(requireContext())
if (allowSkip) {
builder.setPositiveButton(R.string.warning_skip) { _: DialogInterface?, _: Int ->
.setPositiveButton(R.string.warning_skip) { _: DialogInterface?, _: Int ->
setupFragment.pageForward()
setupFragment.setPageWarned(page)
}
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)
}
.setNegativeButton(R.string.warning_cancel, null)
if (titleId != 0) {
builder.setTitle(titleId)
@ -56,7 +48,7 @@ class SetupWarningDialogFragment : DialogFragment() {
}
if (helpLinkId != 0) {
builder.setNeutralButton(R.string.warning_help) { _: DialogInterface?, _: Int ->
val helpLink = resources.getString(helpLinkId)
val helpLink = resources.getString(R.string.install_prod_keys_warning_help)
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink))
startActivity(intent)
}
@ -72,14 +64,12 @@ class SetupWarningDialogFragment : DialogFragment() {
private const val DESCRIPTION = "Description"
private const val HELP_LINK = "HelpLink"
private const val PAGE = "Page"
private const val ALLOW_SKIP = "AllowSkip"
fun newInstance(
titleId: Int,
descriptionId: Int,
helpLinkId: Int,
page: Int,
allowSkip: Boolean = true
page: Int
): SetupWarningDialogFragment {
val dialog = SetupWarningDialogFragment()
val bundle = Bundle()
@ -88,7 +78,6 @@ class SetupWarningDialogFragment : DialogFragment() {
putInt(DESCRIPTION, descriptionId)
putInt(HELP_LINK, helpLinkId)
putInt(PAGE, page)
putBoolean(ALLOW_SKIP, allowSkip)
}
dialog.arguments = bundle
return dialog

View File

@ -377,57 +377,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
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 =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result == null) {

View File

@ -108,15 +108,11 @@
<string name="import_saves">Import</string>
<string name="export_saves">Export</string>
<string name="install_firmware">Install firmware</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="install_firmware_description">Firmware must be in a ZIP archive and is needed to boot some games</string>
<string name="firmware_installing">Installing firmware</string>
<string name="firmware_installed_success">Firmware successfully installed</string>
<string name="firmware_installed_failure">Failed to install firmware</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="firmware_installed_success">Firmware installed successfully</string>
<string name="firmware_installed_failure">Firmware installation failed</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="share_log">Share debug logs</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>
@ -176,14 +172,6 @@
<string name="cabinet_restorer">Restorer</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 -->
<string name="gaia_is_not_real">Gaia isn\'t real</string>
<string name="copied_to_clipboard">Copied to clipboard</string>

View File

@ -4,9 +4,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.9.2" apply false
id("com.android.library") version "8.9.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
id("com.android.application") version "8.9.0" apply false
id("com.android.library") version "8.9.0" apply false
id("org.jetbrains.kotlin.android") version "2.1.20-RC2" apply false
}
tasks.register("clean").configure {
@ -18,6 +18,6 @@ buildscript {
google()
}
dependencies {
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.6.0")
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.8.8")
}
}

View File

@ -1,13 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <memory>
#include <typeinfo>
#include <vector>
#include <QComboBox>
#include <QSpinBox>
#include <QSlider>
#include "common/common_types.h"
#include "common/settings.h"
#include "common/settings_enums.h"
@ -41,22 +38,7 @@ ConfigureCpu::ConfigureCpu(const Core::System& system_,
ConfigureCpu::~ConfigureCpu() = default;
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::SetConfiguration() {}
void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
auto* accuracy_layout = ui->widget_accuracy->layout();
auto* backend_layout = ui->widget_backend->layout();
@ -117,9 +99,6 @@ void ConfigureCpu::ApplyConfiguration() {
for (const auto& apply_func : apply_funcs) {
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) {

View File

@ -126,67 +126,6 @@
</layout>
</widget>
</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>
<spacer name="verticalSpacer">
<property name="orientation">

View File

@ -423,12 +423,6 @@
</item>
<item row="6" column="0">
<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">
<string>Enable Auto-Stub**</string>
</property>
@ -436,12 +430,6 @@
</item>
<item row="0" column="0">
<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">
<string>Kiosk (Quest) Mode</string>
</property>

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "citron/configuration/shared_translation.h"
@ -147,11 +146,6 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
INSERT(
Settings, use_asynchronous_gpu_emulation, tr("Use asynchronous GPU emulation"),
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:"),
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"

View File

@ -36,7 +36,7 @@ system_
DiscordEventHandlers handlers {};
// The number is the client ID for citron, it's used for images and the
// application name
Discord_Initialize("1361252452329848892", & handlers, 1, nullptr);
Discord_Initialize("1322413013248118888", & handlers, 1, nullptr);
}
DiscordImpl::~DiscordImpl() {

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -90,7 +89,6 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
SUB(Service, BGTC) \
SUB(Service, BTDRV) \
SUB(Service, BTM) \
SUB(Service, BSD) \
SUB(Service, Capture) \
SUB(Service, ERPT) \
SUB(Service, ETicket) \

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -58,7 +57,6 @@ enum class Class : u8 {
Service_BPC, ///< The BPC service
Service_BTDRV, ///< The Bluetooth driver service
Service_BTM, ///< The BTM service
Service_BSD, ///< The BSD sockets service
Service_Capture, ///< The capture service
Service_ERPT, ///< The error reporting service
Service_ETicket, ///< The ETicket service

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -199,7 +198,6 @@ struct Values {
MemoryLayout::Memory_12Gb,
"memory_layout_mode",
Category::Core};
SwitchableSetting<u32> cpu_clock_rate{linkage, 1'020'000'000, "cpu_clock_rate", Category::Cpu};
SwitchableSetting<bool> use_speed_limit{
linkage, true, "use_speed_limit", Category::Core, Specialization::Paired, false, true};
SwitchableSetting<u16, true> speed_limit{linkage,
@ -212,11 +210,6 @@ struct Values {
true,
true,
&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
SwitchableSetting<CpuBackend, true> cpu_backend{linkage,
@ -285,8 +278,6 @@ struct Values {
Category::Renderer};
SwitchableSetting<bool> use_asynchronous_gpu_emulation{
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,
#ifdef ANDROID
AstcDecodeMode::Cpu,
@ -627,21 +618,11 @@ struct Values {
// Add-Ons
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;
void UpdateGPUAccuracy();
// boold isGPULevelNormal();
// TODO: ZEP
bool IsGPULevelExtreme();
bool IsGPULevelHigh();

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -9,7 +8,6 @@
#include <ratio>
#include "common/common_types.h"
#include "core/hardware_properties.h"
namespace Common {
@ -17,10 +15,7 @@ class WallClock {
public:
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
// 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
static constexpr u64 CPUTickFreq = 1'020'000'000; // T210/4 A57 CPU Tick Frequency = 1020.0 MHz
virtual ~WallClock() = default;
@ -81,28 +76,12 @@ protected:
using NsToCNTPCTRatio = std::ratio<CNTFRQ, std::nano::den>;
using NsToGPUTickRatio = std::ratio<GPUTickFreq, std::nano::den>;
// Cycle Timing - using functions for dynamic values
// Cycle Timing
// Update these to use functions instead of constexpr
struct CPUTickToNsRatio {
static inline std::intmax_t num = std::nano::den;
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();
};
using CPUTickToNsRatio = std::ratio<std::nano::den, CPUTickFreq>;
using CPUTickToUsRatio = std::ratio<std::micro::den, CPUTickFreq>;
using CPUTickToCNTPCTRatio = std::ratio<CNTFRQ, CPUTickFreq>;
using CPUTickToGPUTickRatio = std::ratio<GPUTickFreq, CPUTickFreq>;
};
std::unique_ptr<WallClock> CreateOptimalClock();

View File

@ -1,5 +1,4 @@
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
# SPDX-FileCopyrightText: 2025 citron Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
add_library(core STATIC
@ -777,12 +776,8 @@ add_library(core STATIC
hle/service/ngc/ngc.h
hle/service/nifm/nifm.cpp
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.h
hle/service/nim/nim_utils.cpp
hle/service/nim/nim_utils.h
hle/service/npns/npns.cpp
hle/service/npns/npns.h
hle/service/ns/account_proxy_interface.cpp
@ -1050,14 +1045,6 @@ add_library(core STATIC
hle/service/sm/sm_controller.h
hle/service/sockets/bsd.cpp
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.h
hle/service/sockets/sfdnsres.cpp
@ -1066,8 +1053,6 @@ add_library(core STATIC
hle/service/sockets/sockets.h
hle/service/sockets/sockets_translate.cpp
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.h
hle/service/spl/spl.cpp

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -648,17 +649,13 @@ void KeyManager::ReloadKeys() {
if (Settings::values.use_dev_keys) {
dev_mode = true;
LoadFromFile(citron_keys_dir / "dev.keys_autogenerated", false);
LoadFromFile(citron_keys_dir / "dev.keys", false);
} else {
dev_mode = false;
LoadFromFile(citron_keys_dir / "prod.keys_autogenerated", 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 / "console.keys_autogenerated", false);
LoadFromFile(citron_keys_dir / "console.keys", false);
}
@ -847,87 +844,15 @@ Key256 KeyManager::GetBISKey(u8 partition_id) const {
template <size_t Size>
void KeyManager::WriteKeyToFile(KeyCategory category, std::string_view keyname,
const std::array<u8, Size>& key) {
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);
// Function is now a no-op - keys are no longer written to autogenerated files
}
void KeyManager::SetKey(S128KeyType id, Key128 key, u64 field1, u64 field2) {
if (s128_keys.find({id, field1, field2}) != s128_keys.end() || key == Key128{}) {
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;
}
@ -935,14 +860,8 @@ void KeyManager::SetKey(S256KeyType id, Key256 key, u64 field1, u64 field2) {
if (s256_keys.find({id, field1, field2}) != s256_keys.end() || key == Key256{}) {
return;
}
const auto iter = std::find_if(
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);
}
// Store the key in memory but don't write to file
s256_keys[{id, field1, field2}] = key;
}
@ -1052,8 +971,6 @@ void KeyManager::DeriveBase() {
// Decrypt keyblob
if (keyblobs[i] == std::array<u8, 0x90>{}) {
keyblobs[i] = DecryptKeyblob(encrypted_keyblobs[i], key);
WriteKeyToFile<0x90>(KeyCategory::Console, fmt::format("keyblob_{:02X}", i),
keyblobs[i]);
}
Key128 package1;
@ -1183,7 +1100,6 @@ void KeyManager::DeriveETicket(PartitionDataManager& data,
data.DecryptProdInfo(GetBISKey(0));
eticket_extended_kek = data.GetETicketExtendedKek();
WriteKeyToFile(KeyCategory::Console, "eticket_extended_kek", eticket_extended_kek);
DeriveETicketRSAKey();
PopulateTickets();
}
@ -1261,8 +1177,6 @@ void KeyManager::PopulateFromPartitionData(PartitionDataManager& data) {
continue;
}
encrypted_keyblobs[i] = data.GetEncryptedKeyblob(i);
WriteKeyToFile<0xB0>(KeyCategory::Console, fmt::format("encrypted_keyblob_{:02X}", i),
encrypted_keyblobs[i]);
}
SetKeyWrapped(S128KeyType::Source, data.GetPackage2KeySource(),

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

View File

@ -9,13 +9,12 @@
#include "common/bit_util.h"
#include "common/common_types.h"
#include "common/settings.h"
namespace Core {
namespace Hardware {
inline u64 BASE_CLOCK_RATE() { return Settings::values.cpu_clock_rate.GetValue(); } // Default CPU Frequency set in settings, defaults to 1020 MHz
constexpr u64 BASE_CLOCK_RATE = 1'785'000'000; // Default CPU Frequency = 1785 MHz
constexpr u64 CNTFREQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz
constexpr u32 NUM_CPU_CORES = 4; // Number of CPU Cores

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -82,8 +81,8 @@ PerformanceConfiguration Controller::GetCurrentPerformanceConfiguration(Performa
void Controller::SetClockSpeed(u32 mhz) {
LOG_DEBUG(Service_APM, "called, mhz={:08X}", mhz);
// Update the clock rate setting with the provided MHz value (convert to Hz)
Settings::values.cpu_clock_rate = mhz * 1'000'000;
// TODO(DarkLordZach): Actually signal core_timing to change clock speed.
// TODO(Rodrigo): Remove [[maybe_unused]] when core_timing is used.
}
} // namespace Service::APM

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <queue>
@ -29,12 +28,12 @@ public:
{10102, nullptr, "UpdateFriendInfo"},
{10110, nullptr, "GetFriendProfileImage"},
{10120, &IFriendService::CheckFriendListAvailability, "CheckFriendListAvailability"},
{10121, &IFriendService::EnsureFriendListAvailable, "EnsureFriendListAvailable"},
{10121, nullptr, "EnsureFriendListAvailable"},
{10200, nullptr, "SendFriendRequestForApplication"},
{10211, nullptr, "AddFacedFriendRequestForApplication"},
{10400, &IFriendService::GetBlockedUserListIds, "GetBlockedUserListIds"},
{10420, &IFriendService::CheckBlockedUserListAvailability, "CheckBlockedUserListAvailability"},
{10421, &IFriendService::EnsureBlockedUserListAvailable, "EnsureBlockedUserListAvailable"},
{10421, nullptr, "EnsureBlockedUserListAvailable"},
{10500, nullptr, "GetProfileList"},
{10600, nullptr, "DeclareOpenOnlinePlaySession"},
{10601, &IFriendService::DeclareCloseOnlinePlaySession, "DeclareCloseOnlinePlaySession"},
@ -167,27 +166,11 @@ private:
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};
rb.Push(ResultSuccess);
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) {
// This is safe to stub, as there should be no adverse consequences from reporting no
// blocked users.
@ -203,45 +186,11 @@ private:
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};
rb.Push(ResultSuccess);
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) {
// Stub used by Splatoon 2
LOG_WARNING(Service_Friend, "(STUBBED) called");
@ -299,6 +248,14 @@ private:
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;
Kernel::KEvent* completion_event;
@ -330,9 +287,6 @@ private:
void GetEvent(HLERequestContext& ctx) {
LOG_DEBUG(Service_Friend, "called");
// Signal the notification event to unblock any waiting threads
notification_event->Signal();
IPC::ResponseBuilder rb{ctx, 2, 1};
rb.Push(ResultSuccess);
rb.PushCopyObjects(notification_event->GetReadableEvent());
@ -409,11 +363,10 @@ private:
};
void Module::Interface::CreateFriendService(HLERequestContext& ctx) {
LOG_DEBUG(Service_Friend, "CreateFriendService called");
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
rb.Push(ResultSuccess);
rb.PushIpcInterface<IFriendService>(system);
LOG_DEBUG(Service_Friend, "called");
}
void Module::Interface::CreateNotificationService(HLERequestContext& ctx) {

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/core.h"
@ -7,7 +6,6 @@
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.h"
#include "core/hle/service/nifm/nifm.h"
#include "core/hle/service/nifm/nifm_utils.h"
#include "core/hle/service/server_manager.h"
#include "network/network.h"

View File

@ -1,85 +0,0 @@
// 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

View File

@ -1,44 +0,0 @@
// 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

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <chrono>
@ -9,7 +8,6 @@
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.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/service.h"

View File

@ -1,124 +0,0 @@
// 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

View File

@ -1,57 +0,0 @@
// 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

View File

@ -1,11 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <boost/container/small_vector.hpp>
#include "common/microprofile.h"
#include "common/settings.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_consumer.h"
@ -23,20 +21,19 @@ s32 NormalizeSwapInterval(f32* out_speed_scale, s32 swap_interval) {
if (out_speed_scale) {
*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) {
// As an extension, treat high swap interval as precise speed control.
if (out_speed_scale) {
*out_speed_scale = static_cast<f32>(swap_interval) / 100.f;
}
swap_interval = 1;
}
return swap_interval;
}

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
@ -480,122 +479,6 @@ void BSD::EventFd(HLERequestContext& ctx) {
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>
void BSD::ExecuteWork(HLERequestContext& ctx, Work work) {
work.Execute(this);
@ -625,9 +508,9 @@ std::pair<s32, Errno> BSD::SocketImpl(Domain domain, Type type, Protocol protoco
LOG_INFO(Service, "New socket fd={}", fd);
auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
auto room_member = room_network.GetRoomMember().lock();
if (room_member && room_member->IsConnected()) {
descriptor.socket = std::make_shared<Network::ProxySocket>(system.GetRoomNetwork());
descriptor.socket = std::make_shared<Network::ProxySocket>(room_network);
} else {
descriptor.socket = std::make_shared<Network::Socket>();
}
@ -1077,41 +960,27 @@ void BSD::BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const noex
}
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) {
if (!optional_descriptor.has_value()) {
continue;
}
FileDescriptor& descriptor = *optional_descriptor;
if (descriptor.socket) {
descriptor.socket->HandleProxyPacket(packet);
}
descriptor.socket.get()->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)
: ServiceFramework{system_, name} {
: ServiceFramework{system_, name}, room_network{system_.GetRoomNetwork()} {
// clang-format off
static const FunctionInfo functions[] = {
{0, &BSD::RegisterClient, "RegisterClient"},
{1, &BSD::StartMonitoring, "StartMonitoring"},
{2, &BSD::Socket, "Socket"},
{3, &BSD::SocketExempt, "SocketExempt"},
{4, &BSD::Open, "Open"},
{3, nullptr, "SocketExempt"},
{4, nullptr, "Open"},
{5, &BSD::Select, "Select"},
{6, &BSD::Poll, "Poll"},
{7, &BSD::Sysctl, "Sysctl"},
{7, nullptr, "Sysctl"},
{8, &BSD::Recv, "Recv"},
{9, &BSD::RecvFrom, "RecvFrom"},
{10, &BSD::Send, "Send"},
@ -1123,32 +992,27 @@ BSD::BSD(Core::System& system_, const char* name)
{16, &BSD::GetSockName, "GetSockName"},
{17, &BSD::GetSockOpt, "GetSockOpt"},
{18, &BSD::Listen, "Listen"},
{19, &BSD::Ioctl, "Ioctl"},
{19, nullptr, "Ioctl"},
{20, &BSD::Fcntl, "Fcntl"},
{21, &BSD::SetSockOpt, "SetSockOpt"},
{22, &BSD::Shutdown, "Shutdown"},
{23, &BSD::ShutdownAllSockets, "ShutdownAllSockets"},
{23, nullptr, "ShutdownAllSockets"},
{24, &BSD::Write, "Write"},
{25, &BSD::Read, "Read"},
{26, &BSD::Close, "Close"},
{27, &BSD::DuplicateSocket, "DuplicateSocket"},
{28, &BSD::GetResourceStatistics, "GetResourceStatistics"},
{29, &BSD::RecvMMsg, "RecvMMsg"},
{30, &BSD::SendMMsg, "SendMMsg"},
{28, nullptr, "GetResourceStatistics"},
{29, nullptr, "RecvMMsg"},
{30, nullptr, "SendMMsg"},
{31, &BSD::EventFd, "EventFd"},
{32, &BSD::RegisterResourceStatisticsName, "RegisterResourceStatisticsName"},
{33, &BSD::RegisterClientShared, "RegisterClientShared"},
{34, &BSD::GetSocketStatistics, "GetSocketStatistics"},
{35, &BSD::NifIoctl, "NifIoctl"},
{200, &BSD::SetThreadCoreMask, "SetThreadCoreMask"},
{201, &BSD::GetThreadCoreMask, "GetThreadCoreMask"},
{32, nullptr, "RegisterResourceStatisticsName"},
{33, nullptr, "Initialize2"},
};
// clang-format on
RegisterHandlers(functions);
auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
if (room_member) {
if (auto room_member = room_network.GetRoomMember().lock()) {
proxy_packet_received = room_member->BindOnProxyPacketReceived(
[this](const Network::ProxyPacket& packet) { OnProxyPacketReceived(packet); });
} else {
@ -1157,8 +1021,7 @@ BSD::BSD(Core::System& system_, const char* name)
}
BSD::~BSD() {
auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
if (room_member) {
if (auto room_member = room_network.GetRoomMember().lock()) {
room_member->Unbind(proxy_packet_received);
}
}
@ -1168,4 +1031,31 @@ std::unique_lock<std::mutex> BSD::LockService() {
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

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -38,9 +37,6 @@ public:
Errno CloseImpl(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:
/// Maximum number of file descriptors
static constexpr size_t MAX_FD = 128;
@ -128,30 +124,11 @@ private:
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 StartMonitoring(HLERequestContext& ctx);
void Socket(HLERequestContext& ctx);
void SocketExempt(HLERequestContext& ctx);
void Open(HLERequestContext& ctx);
void Select(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 Bind(HLERequestContext& ctx);
void Connect(HLERequestContext& ctx);
@ -159,25 +136,18 @@ private:
void GetSockName(HLERequestContext& ctx);
void GetSockOpt(HLERequestContext& ctx);
void Listen(HLERequestContext& ctx);
void Ioctl(HLERequestContext& ctx);
void Fcntl(HLERequestContext& ctx);
void SetSockOpt(HLERequestContext& ctx);
void Shutdown(HLERequestContext& ctx);
void ShutdownAllSockets(HLERequestContext& ctx);
void Recv(HLERequestContext& ctx);
void RecvFrom(HLERequestContext& ctx);
void Send(HLERequestContext& ctx);
void SendTo(HLERequestContext& ctx);
void Write(HLERequestContext& ctx);
void Read(HLERequestContext& ctx);
void Close(HLERequestContext& ctx);
void DuplicateSocket(HLERequestContext& ctx);
void GetResourceStatistics(HLERequestContext& ctx);
void RecvMMsg(HLERequestContext& ctx);
void SendMMsg(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>
void ExecuteWork(HLERequestContext& ctx, Work work);
@ -201,23 +171,30 @@ private:
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::span<const u8> addr);
s32 FindFreeFileDescriptorHandle() noexcept;
bool IsFileDescriptorValid(s32 fd) 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);
// Callback identifier for the OnProxyPacketReceived event.
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:
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

View File

@ -1,91 +0,0 @@
// 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

View File

@ -1,46 +0,0 @@
// 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

View File

@ -1,140 +0,0 @@
// 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

View File

@ -1,41 +0,0 @@
// 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

View File

@ -1,20 +0,0 @@
// 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

View File

@ -1,20 +0,0 @@
// 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

View File

@ -1,31 +0,0 @@
// 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

View File

@ -1,26 +0,0 @@
// 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

View File

@ -1,95 +0,0 @@
// 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

View File

@ -1,57 +0,0 @@
// 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

View File

@ -1,16 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/hle/service/server_manager.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/sfdnsres.h"
#include "core/hle/service/sockets/socket_utils.h"
#include "core/hle/service/sockets/sockets.h"
namespace Service::Sockets {
@ -18,33 +12,12 @@ namespace Service::Sockets {
void LoopProcess(Core::System& 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: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));
// Register NSD services
server_manager->RegisterNamedService("nsd:a", std::make_shared<NSD>(system, "nsd:a"));
server_manager->RegisterNamedService("nsd:u", std::make_shared<NSD>(system, "nsd:u"));
// Register SFDNSRES service
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);
ServerManager::RunServer(std::move(server_manager));
}

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -18,11 +17,9 @@ enum class Errno : u32 {
BADF = 9,
AGAIN = 11,
NOMEM = 12,
BUSY = 16,
INVAL = 22,
MFILE = 24,
PIPE = 32,
NOTSOCK = 88,
MSGSIZE = 90,
CONNABORTED = 103,
CONNRESET = 104,

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <utility>
@ -40,10 +39,6 @@ Errno Translate(Network::Errno value) {
return Errno::INPROGRESS;
case Network::Errno::NOMEM:
return Errno::NOMEM;
case Network::Errno::BUSY:
return Errno::BUSY;
case Network::Errno::NOTSOCK:
return Errno::NOTSOCK;
default:
UNIMPLEMENTED_MSG("Unimplemented errno={}", value);
return Errno::SUCCESS;

View File

@ -565,7 +565,6 @@ void LoopProcess(Core::System& system) {
auto server_manager = std::make_unique<ServerManager>(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));
}

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -157,8 +156,6 @@ Errno TranslateNativeError(int e, CallType call_type = CallType::Other) {
return Errno::TIMEDOUT;
case WSAEINPROGRESS:
return Errno::INPROGRESS;
case WSAENOTSOCK:
return Errno::NOTSOCK;
default:
UNIMPLEMENTED_MSG("Unimplemented errno={}", e);
return Errno::OTHER;
@ -276,14 +273,14 @@ Errno TranslateNativeError(int e, CallType call_type = CallType::Other) {
return Errno::MFILE;
case EPIPE:
return Errno::PIPE;
case ECONNABORTED:
return Errno::CONNABORTED;
case ENOTCONN:
return Errno::NOTCONN;
case EAGAIN:
return Errno::AGAIN;
case ECONNREFUSED:
return Errno::CONNREFUSED;
case ECONNABORTED:
return Errno::CONNABORTED;
case ECONNRESET:
return Errno::CONNRESET;
case EHOSTUNREACH:
@ -298,14 +295,8 @@ Errno TranslateNativeError(int e, CallType call_type = CallType::Other) {
return Errno::TIMEDOUT;
case EINPROGRESS:
return Errno::INPROGRESS;
case ENOMEM:
return Errno::NOMEM;
case EBUSY:
return Errno::BUSY;
case ENOTSOCK:
return Errno::NOTSOCK;
default:
UNIMPLEMENTED_MSG("Unimplemented errno={}", e);
UNIMPLEMENTED_MSG("Unimplemented errno={} ({})", e, strerror(e));
return Errno::OTHER;
}
}
@ -324,14 +315,6 @@ Errno GetAndLogLastError(CallType call_type = CallType::Other) {
LOG_DEBUG(Network, "Socket operation error: {}", Common::NativeErrorToString(e));
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));
return err;
}

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -48,8 +47,6 @@ enum class Errno {
INPROGRESS,
OTHER,
NOMEM,
BUSY,
NOTSOCK,
};
enum class GetAddrInfoError {

View File

@ -1,6 +1,5 @@
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
// SPDX-FileCopyrightText: 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -910,9 +909,7 @@ struct Memory::Impl {
#endif
};
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());
Memory::Memory(Core::System& system_) : system{system_} {
Reset();
}
@ -1154,48 +1151,4 @@ bool Memory::InvalidateSeparateHeap(void* fault_address) {
#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

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
// SPDX-FileCopyrightText: 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -10,8 +9,6 @@
#include <span>
#include <string>
#include <vector>
#include <unordered_map>
#include <random>
#include "common/scratch_buffer.h"
#include "common/typed_address.h"
@ -46,9 +43,6 @@ constexpr std::size_t CITRON_PAGEBITS = 12;
constexpr u64 CITRON_PAGESIZE = 1ULL << CITRON_PAGEBITS;
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
enum : u64 {
/// TLS (Thread-Local Storage) related.
@ -56,18 +50,6 @@ enum : u64 {
/// Application stack
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.
@ -82,55 +64,6 @@ public:
Memory(Memory&&) = default;
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.
*/
@ -564,13 +497,6 @@ private:
struct 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>

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <random>
@ -27,8 +26,8 @@ public:
using clock = std::chrono::system_clock;
explicit Socket(const std::string& host, u16 port, SocketCallback callback_)
: callback(std::move(callback_)), timer(io_context),
socket(io_context, udp::endpoint(udp::v4(), 0)), client_id(GenerateRandomClientId()) {
: callback(std::move(callback_)), timer(io_service),
socket(io_service, udp::endpoint(udp::v4(), 0)), client_id(GenerateRandomClientId()) {
boost::system::error_code ec{};
auto ipv4 = boost::asio::ip::make_address_v4(host, ec);
if (ec.value() != boost::system::errc::success) {
@ -40,11 +39,11 @@ public:
}
void Stop() {
io_context.stop();
io_service.stop();
}
void Loop() {
io_context.run();
io_service.run();
}
void StartSend(const clock::time_point& from) {
@ -114,7 +113,7 @@ private:
}
SocketCallback callback;
boost::asio::io_context io_context;
boost::asio::io_service io_service;
boost::asio::basic_waitable_timer<clock> timer;
udp::socket socket;

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <optional>
@ -275,15 +274,8 @@ IR::Opcode GlobalToStorage(IR::Opcode opcode) {
/// Returns true when a storage buffer address satisfies a bias
bool MeetsBias(const StorageBufferAddr& storage_buffer, const Bias& bias) noexcept {
// For performance, strongly prefer addresses that meet the bias criteria
// 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;
return storage_buffer.index == bias.index && storage_buffer.offset >= bias.offset_begin &&
storage_buffer.offset < bias.offset_end;
}
struct LowAddrInfo {
@ -359,7 +351,7 @@ std::optional<StorageBufferAddr> Track(const IR::Value& value, const Bias* bias)
.index = index.U32(),
.offset = offset.U32(),
};
const u32 alignment{bias ? bias->alignment : 16U};
const u32 alignment{bias ? bias->alignment : 8U};
if (!Common::IsAligned(storage_buffer.offset, alignment)) {
// The SSBO pointer has to be aligned
return std::nullopt;
@ -380,9 +372,9 @@ void CollectStorageBuffers(IR::Block& block, IR::Inst& inst, StorageInfo& info)
// avoid getting false positives
static constexpr Bias nvn_bias{
.index = 0,
.offset_begin = 0x100, // Expanded from 0x110 to catch more potential storage buffers
.offset_end = 0x1000, // Substantially expanded to include all TOTK storage buffers
.alignment = 32, // Increased from 16 to optimize memory access patterns
.offset_begin = 0x110,
.offset_end = 0x610,
.alignment = 16,
};
// Track the low address of the instruction
const std::optional<LowAddrInfo> low_addr_info{TrackLowAddress(&inst)};
@ -394,16 +386,15 @@ void CollectStorageBuffers(IR::Block& block, IR::Inst& inst, StorageInfo& info)
const IR::U32 low_addr{low_addr_info->value};
std::optional<StorageBufferAddr> storage_buffer{Track(low_addr, &nvn_bias)};
if (!storage_buffer) {
// If it fails, track without a bias but with higher alignment requirements
// for better performance
// If it fails, track without a bias
storage_buffer = Track(low_addr, nullptr);
if (!storage_buffer) {
// If that also fails, use NVN fallbacks
LOG_WARNING(Shader, "Storage buffer failed to track, using global memory fallbacks");
return;
}
LOG_DEBUG(Shader, "Storage buffer tracked without bias, index {} offset 0x{:X}",
storage_buffer->index, storage_buffer->offset);
LOG_WARNING(Shader, "Storage buffer tracked without bias, index {} offset {}",
storage_buffer->index, storage_buffer->offset);
}
// Collect storage buffer and the instruction
if (IsGlobalMemoryWrite(inst)) {
@ -434,12 +425,8 @@ 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))};
// 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)));
// Also align the resulting offset for optimal memory access
IR::U32 result = ir.ISub(offset, low_cbuf);
return result;
return ir.ISub(offset, low_cbuf);
}
/// Replace a global memory load instruction with its storage buffer equivalent

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
@ -15,7 +14,7 @@
class FakeCemuhookServer {
public:
FakeCemuhookServer()
: socket(io_context, boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), 0)) {}
: socket(io_service, boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), 0)) {}
~FakeCemuhookServer() {
is_running = false;
@ -83,7 +82,7 @@ public:
}
private:
boost::asio::io_context io_context;
boost::asio::io_service io_service;
boost::asio::ip::udp::socket socket;
std::array<u8, InputCommon::CemuhookUDP::MAX_PACKET_SIZE> send_buffer;
std::array<u8, InputCommon::CemuhookUDP::MAX_PACKET_SIZE> receive_buffer;

View File

@ -1,5 +1,4 @@
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
# SPDX-FileCopyrightText: 2025 Citron Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
add_subdirectory(host_shaders)
@ -248,8 +247,6 @@ add_library(video_core STATIC
renderer_vulkan/vk_turbo_mode.h
renderer_vulkan/vk_update_descriptor.cpp
renderer_vulkan/vk_update_descriptor.h
renderer_vulkan/vk_texture_manager.cpp
renderer_vulkan/vk_texture_manager.h
shader_cache.cpp
shader_cache.h
shader_environment.cpp
@ -309,8 +306,6 @@ add_library(video_core STATIC
vulkan_common/vulkan_library.h
vulkan_common/vulkan_memory_allocator.cpp
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.h
vulkan_common/vulkan_wrapper.cpp

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@ -7,7 +6,6 @@
#include <algorithm>
#include <memory>
#include <numeric>
#include <unordered_map>
#include "common/range_sets.inc"
#include "video_core/buffer_cache/buffer_cache_base.h"
@ -20,7 +18,7 @@ using Core::DEVICE_PAGESIZE;
template <class P>
BufferCache<P>::BufferCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, Runtime& runtime_)
: runtime{runtime_}, device_memory{device_memory_}, memory_tracker{device_memory}, immediate_buffer_alloc{} {
: runtime{runtime_}, device_memory{device_memory_}, memory_tracker{device_memory} {
// Ensure the first slot is used for the null buffer
void(slot_buffers.insert(runtime, NullBufferParams{}));
gpu_modified_ranges.Clear();
@ -1721,31 +1719,8 @@ Binding BufferCache<P>::StorageBufferBinding(GPUVAddr ssbo_addr, u32 cbuf_index,
const std::optional<DAddr> aligned_device_addr = gpu_memory->GpuToCpuAddress(aligned_gpu_addr);
if (!aligned_device_addr || size == 0) {
// Use a static counter to track and limit warnings
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),
};
LOG_WARNING(HW_GPU, "Failed to find storage buffer for cbuf index {}", cbuf_index);
return NULL_BINDING;
}
const std::optional<DAddr> device_addr = gpu_memory->GpuToCpuAddress(gpu_addr);
ASSERT_MSG(device_addr, "Unaligned storage buffer address not found for cbuf index {}",

View File

@ -1,15 +1,12 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <cstddef>
#include <mutex>
#include <utility>
#include <vector>
#include "common/logging/log.h"
namespace VideoCommon {
@ -17,59 +14,18 @@ namespace VideoCommon {
template <typename T, size_t TICKS_TO_DESTROY>
class DelayedDestructionRing {
public:
DelayedDestructionRing() = default;
~DelayedDestructionRing() {
// Ensure all resources are properly released when ring is destroyed
for (auto& element_list : elements) {
element_list.clear();
}
}
void Tick() {
std::scoped_lock lock{ring_mutex};
// Move to next position in the ring
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) {
std::scoped_lock lock{ring_mutex};
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:
size_t index = 0;
std::array<std::vector<T>, TICKS_TO_DESTROY> elements;
mutable std::mutex ring_mutex;
};
} // namespace VideoCommon

View File

@ -1,13 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <string>
#include <vector>
#include <chrono>
#include <functional>
#include "common/settings.h" // for enum class Settings::ShaderBackend
#include "common/thread_worker.h"
@ -237,68 +234,26 @@ GraphicsPipeline::GraphicsPipeline(const Device& device, TextureCache& texture_c
auto func{[this, sources_ = std::move(sources), sources_spirv_ = std::move(sources_spirv),
shader_notify, backend, in_parallel,
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) {
switch (backend) {
case Settings::ShaderBackend::Glsl:
if (!sources_[stage].empty()) {
compilation_steps.emplace_back([this, stage, source = sources_[stage]]() {
source_programs[stage] = CreateProgram(source, Stage(stage));
});
source_programs[stage] = CreateProgram(sources_[stage], Stage(stage));
}
break;
case Settings::ShaderBackend::Glasm:
if (!sources_[stage].empty()) {
compilation_steps.emplace_back([this, stage, source = sources_[stage]]() {
assembly_programs[stage] = CompileProgram(source, AssemblyStage(stage));
});
assembly_programs[stage] =
CompileProgram(sources_[stage], AssemblyStage(stage));
}
break;
case Settings::ShaderBackend::SpirV:
if (!sources_spirv_[stage].empty()) {
compilation_steps.emplace_back([this, stage, source = sources_spirv_[stage]]() {
source_programs[stage] = CreateProgram(source, Stage(stage));
});
source_programs[stage] = CreateProgram(sources_spirv_[stage], Stage(stage));
}
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) {
std::scoped_lock lock{built_mutex};
built_fence.Create();
@ -668,41 +623,15 @@ void GraphicsPipeline::WaitForBuild() {
is_built = true;
}
bool GraphicsPipeline::IsBuilt() const noexcept {
bool GraphicsPipeline::IsBuilt() noexcept {
if (is_built) {
return true;
}
if (!built_fence.handle) {
if (built_fence.handle == 0) {
return false;
}
// 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;
is_built = built_fence.IsSignaled();
return is_built;
}
} // namespace OpenGL

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -103,7 +102,7 @@ public:
return uses_local_memory;
}
[[nodiscard]] bool IsBuilt() const noexcept;
[[nodiscard]] bool IsBuilt() noexcept;
template <typename Spec>
static auto MakeConfigureSpecFunc() {

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <atomic>
@ -609,33 +608,9 @@ std::unique_ptr<ComputePipeline> ShaderCache::CreateComputePipeline(
}
std::unique_ptr<ShaderWorker> ShaderCache::CreateWorkers() const {
// 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",
[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;
return std::make_unique<ShaderWorker>(std::max(std::thread::hardware_concurrency(), 2U) - 1,
"GlShaderBuilder",
[this] { return Context{emu_window}; });
}
} // namespace OpenGL

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -9,8 +8,6 @@
#include <optional>
#include <string>
#include <vector>
#include <fstream>
#include <filesystem>
#include <fmt/ranges.h>
@ -38,7 +35,6 @@
#include "video_core/vulkan_common/vulkan_instance.h"
#include "video_core/vulkan_common/vulkan_library.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_wrapper.h"
@ -127,93 +123,12 @@ RendererVulkan::RendererVulkan(Core::TelemetrySession& telemetry_session_,
PresentFiltersForAppletCapture),
rasterizer(render_window, gpu, device_memory, device, memory_allocator, state_tracker,
scheduler),
hybrid_memory(std::make_unique<HybridMemory>(device, memory_allocator)),
texture_manager(device, memory_allocator),
shader_manager(device),
applet_frame() {
if (Settings::values.renderer_force_max_clock.GetValue() && device.ShouldBoostClocks()) {
turbo_mode.emplace(instance, dld);
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();
InitializePlatformSpecific();
} catch (const vk::Exception& exception) {
LOG_ERROR(Render_Vulkan, "Vulkan initialization failed with error: {}", exception.what());
throw std::runtime_error{fmt::format("Vulkan initialization error {}", exception.what())};
@ -311,35 +226,6 @@ void RendererVulkan::RenderScreenshot(std::span<const Tegra::FramebufferConfig>
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 dst_buffer = RenderToBuffer(framebuffers, layout, VK_FORMAT_B8G8R8A8_UNORM,
layout.width * layout.height * 4);
@ -391,81 +277,4 @@ void RendererVulkan::RenderAppletCaptureLayer(
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

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -7,7 +6,6 @@
#include <memory>
#include <string>
#include <variant>
#include <functional>
#include "common/dynamic_library.h"
#include "video_core/host1x/gpu_device_memory_manager.h"
@ -19,11 +17,8 @@
#include "video_core/renderer_vulkan/vk_state_tracker.h"
#include "video_core/renderer_vulkan/vk_swapchain.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_memory_allocator.h"
#include "video_core/vulkan_common/hybrid_memory.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
namespace Core {
@ -63,9 +58,6 @@ public:
return device.GetDriverName();
}
// Enhanced platform-specific initialization
void InitializePlatformSpecific();
private:
void Report() const;
@ -75,10 +67,6 @@ private:
void RenderScreenshot(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;
Tegra::MaxwellDeviceMemoryManager& device_memory;
Tegra::GPU& gpu;
@ -102,13 +90,6 @@ private:
RasterizerVulkan rasterizer;
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;
};

View File

@ -1,10 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <vector>
#include <chrono>
#include <boost/container/small_vector.hpp>
@ -39,23 +37,10 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
if (shader_notify) {
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(),
uniform_buffer_sizes.begin());
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);
}
uniform_buffer_sizes.begin());
auto func{[this, &descriptor_pool, shader_notify, pipeline_statistics] {
DescriptorLayoutBuilder builder{device};
builder.Add(info, VK_SHADER_STAGE_COMPUTE_BIT);
@ -64,11 +49,15 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
descriptor_update_template =
builder.CreateTemplate(*descriptor_set_layout, *pipeline_layout, false);
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{};
if (device.IsKhrPipelineExecutablePropertiesEnabled()) {
flags |= VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR;
}
pipeline = device.GetLogical().CreateComputePipeline(
{
.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
@ -76,7 +65,8 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
.flags = flags,
.stage{
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.pNext = nullptr,
.pNext =
device.IsExtSubgroupSizeControlSupported() ? &subgroup_size_ci : nullptr,
.flags = 0,
.stage = VK_SHADER_STAGE_COMPUTE_BIT,
.module = *spv_module,
@ -89,15 +79,6 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
},
*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) {
pipeline_statistics->Collect(*pipeline);
}

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -259,16 +258,7 @@ GraphicsPipeline::GraphicsPipeline(
std::ranges::copy(info->constant_buffer_used_sizes, uniform_buffer_sizes[stage].begin());
num_textures += Shader::NumDescriptors(info->texture_descriptors);
}
// 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);
}
auto func{[this, shader_notify, &render_pass_cache, &descriptor_pool, pipeline_statistics] {
DescriptorLayoutBuilder builder{MakeBuilder(device, stage_infos)};
uses_push_descriptor = builder.CanUsePushDescriptor();
descriptor_set_layout = builder.CreateDescriptorSetLayout(uses_push_descriptor);
@ -283,17 +273,6 @@ GraphicsPipeline::GraphicsPipeline(
const VkRenderPass render_pass{render_pass_cache.Get(MakeRenderPassKey(key.state))};
Validate();
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) {
pipeline_statistics->Collect(*pipeline);
}
@ -332,9 +311,6 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
const auto& regs{maxwell3d->regs};
const bool via_header_index{regs.sampler_binding == Maxwell::SamplerBinding::ViaHeaderBinding};
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]};
buffer_cache.UnbindGraphicsStorageBuffers(stage);
if constexpr (Spec::has_storage_buffers) {
@ -346,7 +322,7 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
++ssbo_index;
}
}
const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
const auto read_handle{[&](const auto& desc, u32 index) {
ASSERT(cbufs[desc.cbuf_index].enabled);
const u32 index_offset{index << desc.size_shift};
@ -368,7 +344,6 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
}
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
}};
const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE {
for (u32 index = 0; index < desc.count; ++index) {
const auto handle{read_handle(desc, index)};

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -265,42 +264,18 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program
}
size_t GetTotalPipelineWorkers() {
const size_t num_cores = std::max<size_t>(static_cast<size_t>(std::thread::hardware_concurrency()), 2ULL);
// Calculate optimal number of workers based on available CPU cores
size_t optimal_workers;
const size_t max_core_threads =
std::max<size_t>(static_cast<size_t>(std::thread::hardware_concurrency()), 2ULL) - 1ULL;
#ifdef ANDROID
// Mobile devices need more conservative threading to avoid thermal issues
// Leave more cores free on Android for system processes and other apps
constexpr size_t min_free_cores = 3ULL;
if (num_cores <= min_free_cores + 1) {
return 1ULL; // At least one worker
// Leave at least a few cores free in android
constexpr size_t free_cores = 3ULL;
if (max_core_threads <= free_cores) {
return 1ULL;
}
optimal_workers = num_cores - min_free_cores;
return max_core_threads - free_cores;
#else
// 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
}
return max_core_threads;
#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
@ -611,35 +586,14 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
if (pipeline->IsBuilt()) {
return pipeline;
}
if (!use_asynchronous_shaders) {
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
// will be used one time.
if (maxwell3d->regs.zeta_enable) {
return nullptr;
}
// 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
// can't be built async
@ -647,23 +601,6 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
if (draw_state.index_buffer.count <= 6 || draw_state.vertex_buffer.count <= 6) {
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;
}

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -929,8 +928,6 @@ bool AccelerateDMA::BufferToImage(const Tegra::DMA::ImageCopy& copy_info,
void RasterizerVulkan::UpdateDynamicStates() {
auto& regs = maxwell3d->regs;
// Always update base dynamic states.
UpdateViewportsState(regs);
UpdateScissorsState(regs);
UpdateDepthBias(regs);
@ -938,9 +935,7 @@ void RasterizerVulkan::UpdateDynamicStates() {
UpdateDepthBounds(regs);
UpdateStencilFaces(regs);
UpdateLineWidth(regs);
if (device.IsExtExtendedDynamicStateSupported()) {
// Update extended dynamic states.
UpdateCullMode(regs);
UpdateDepthCompareOp(regs);
UpdateFrontFace(regs);
@ -951,44 +946,16 @@ void RasterizerVulkan::UpdateDynamicStates() {
UpdateDepthTestEnable(regs);
UpdateDepthWriteEnable(regs);
UpdateStencilTestEnable(regs);
if (device.IsExtExtendedDynamicState2Supported()) {
UpdatePrimitiveRestartEnable(regs);
UpdateRasterizerDiscardEnable(regs);
UpdateDepthBiasEnable(regs);
}
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);
// Restore the original value.
regs.logic_op.enable = oldLogicOpEnable;
} else {
UpdateLogicOpEnable(regs);
}
UpdateLogicOpEnable(regs);
UpdateDepthClampEnable(regs);
}
}
if (device.IsExtExtendedDynamicState2ExtrasSupported()) {
UpdateLogicOp(regs);
}
@ -996,7 +963,6 @@ void RasterizerVulkan::UpdateDynamicStates() {
UpdateBlending(regs);
}
}
if (device.IsExtVertexInputDynamicStateSupported()) {
UpdateVertexInput(regs);
}

View File

@ -1,141 +1,15 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#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/logging/log.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_wrapper.h"
#define SHADER_CACHE_DIR "./shader_cache"
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) {
return device.GetLogical().CreateShaderModule({
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
@ -146,368 +20,4 @@ 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

View File

@ -1,16 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#include <string>
#include <unordered_map>
#include <mutex>
#include <atomic>
#include <functional>
#include <vector>
#include "common/common_types.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
@ -18,48 +11,7 @@
namespace Vulkan {
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);
// 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

View File

@ -30,10 +30,6 @@
namespace Vulkan {
// TextureCacheManager implementations to fix linker errors
TextureCacheManager::TextureCacheManager() = default;
TextureCacheManager::~TextureCacheManager() = default;
using Tegra::Engines::Fermi2D;
using Tegra::Texture::SwizzleSource;
using Tegra::Texture::TextureMipmapFilter;

View File

@ -5,10 +5,6 @@
#pragma once
#include <span>
#include <mutex>
#include <atomic>
#include <string>
#include <unordered_map>
#include "video_core/texture_cache/texture_cache_base.h"
@ -42,22 +38,6 @@ class RenderPassCache;
class StagingBufferPool;
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 {
public:
explicit TextureCacheRuntime(const Device& device_, Scheduler& scheduler_,
@ -138,10 +118,6 @@ public:
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;
Scheduler& scheduler;
MemoryAllocator& memory_allocator;
@ -153,9 +129,6 @@ public:
const Settings::ResolutionScalingInfo& resolution;
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);
std::array<vk::Buffer, indexing_slots> buffers{};
};

View File

@ -1,146 +0,0 @@
// 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

View File

@ -1,57 +0,0 @@
// 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

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@ -81,10 +80,8 @@ void TextureCache<P>::RunGarbageCollector() {
const auto Configure = [&](bool allow_aggressive) {
high_priority_mode = total_used_memory >= expected_memory;
aggressive_mode = allow_aggressive && total_used_memory >= critical_memory;
// Reduce ticks_to_destroy to be more aggressive in freeing memory
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);
ticks_to_destroy = aggressive_mode ? 10ULL : high_priority_mode ? 25ULL : 50ULL;
num_iterations = aggressive_mode ? 40 : (high_priority_mode ? 20 : 10);
};
const auto Cleanup = [this, &num_iterations, &high_priority_mode,
&aggressive_mode](ImageId image_id) {
@ -98,8 +95,7 @@ void TextureCache<P>::RunGarbageCollector() {
// used by the async decoder thread.
return false;
}
// Be more aggressive with cleanup for memory-intensive games
if (!aggressive_mode && !high_priority_mode && True(image.flags & ImageFlagBits::CostlyLoad)) {
if (!aggressive_mode && True(image.flags & ImageFlagBits::CostlyLoad)) {
return false;
}
const bool must_download =
@ -122,20 +118,19 @@ void TextureCache<P>::RunGarbageCollector() {
DeleteImage(image_id, image.scale_tick > frame_tick + 5);
if (total_used_memory < critical_memory) {
if (aggressive_mode) {
// Sink the aggresiveness more gradually to prevent oscillation
num_iterations = num_iterations * 3 / 4;
// Sink the aggresiveness.
num_iterations >>= 2;
aggressive_mode = false;
return false;
}
if (high_priority_mode && total_used_memory < expected_memory) {
num_iterations = num_iterations * 3 / 4;
num_iterations >>= 1;
high_priority_mode = false;
}
}
return false;
};
// Run garbage collection more frequently for memory-intensive games
// Try to remove anything old enough and not high priority.
Configure(false);
lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
@ -143,67 +138,19 @@ void TextureCache<P>::RunGarbageCollector() {
// If pressure is still too high, prune aggressively.
if (total_used_memory >= critical_memory) {
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>
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 (runtime.CanReportMemoryUsage()) {
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) {
RunGarbageCollector();
}
sentenced_images.Tick();
sentenced_framebuffers.Tick();
sentenced_image_view.Tick();
@ -2218,35 +2165,27 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
if (image.HasScaled()) {
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);
if ((IsPixelFormatASTC(image.info.format) &&
True(image.flags & ImageFlagBits::AcceleratedUpload)) ||
True(image.flags & ImageFlagBits::Converted)) {
tentative_size = TranscodedAstcSize(tentative_size, image.info.format);
}
// Ensure memory usage is properly accounted for
total_used_memory -= Common::AlignUp(tentative_size, 1024);
const GPUVAddr gpu_addr = image.gpu_addr;
const auto alloc_it = image_allocs_table.find(gpu_addr);
if (alloc_it == image_allocs_table.end()) {
LOG_ERROR(HW_GPU, "Trying to delete an image alloc that does not exist in address 0x{:x}",
gpu_addr);
ASSERT_MSG(false, "Trying to delete an image alloc that does not exist in address 0x{:x}",
gpu_addr);
return;
}
const ImageAllocId alloc_id = alloc_it->second;
std::vector<ImageId>& alloc_images = slot_image_allocs[alloc_id].images;
const auto alloc_image_it = std::ranges::find(alloc_images, image_id);
if (alloc_image_it == alloc_images.end()) {
LOG_ERROR(HW_GPU, "Trying to delete an image that does not exist");
ASSERT_MSG(false, "Trying to delete an image that does not exist");
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::Registered), "Image was not unregistered");
@ -2257,8 +2196,6 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
for (size_t rt = 0; rt < NUM_RT; ++rt) {
dirty[Dirty::ColorBuffer0 + rt] = true;
}
// Clear render target references
const std::span<const ImageViewId> image_view_ids = image.image_view_ids;
for (const ImageViewId image_view_id : image_view_ids) {
std::ranges::replace(render_targets.color_buffer_ids, image_view_id, ImageViewId{});
@ -2266,12 +2203,9 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
render_targets.depth_buffer_id = ImageViewId{};
}
}
// Clean up references and dependencies
RemoveImageViewReferences(image_view_ids);
RemoveFramebuffers(image_view_ids);
// Handle aliased images
for (const AliasedImage& alias : image.aliased_images) {
ImageBase& other_image = slot_images[alias.id];
[[maybe_unused]] const size_t num_removed_aliases =
@ -2279,43 +2213,33 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
return other_alias.id == image_id;
});
other_image.CheckAliasState();
if (num_removed_aliases != 1) {
LOG_WARNING(HW_GPU, "Invalid number of removed aliases: {}", num_removed_aliases);
}
ASSERT_MSG(num_removed_aliases == 1, "Invalid number of removed aliases: {}",
num_removed_aliases);
}
// Handle overlapping images
for (const ImageId overlap_id : image.overlapping_images) {
ImageBase& other_image = slot_images[overlap_id];
[[maybe_unused]] const size_t num_removed_overlaps = std::erase_if(
other_image.overlapping_images,
[image_id](const ImageId other_overlap_id) { return other_overlap_id == image_id; });
other_image.CheckBadOverlapState();
if (num_removed_overlaps != 1) {
LOG_WARNING(HW_GPU, "Invalid number of removed overlaps: {}", num_removed_overlaps);
}
ASSERT_MSG(num_removed_overlaps == 1, "Invalid number of removed overlapps: {}",
num_removed_overlaps);
}
// Free resources - either immediately or queue for delayed destruction
for (const ImageViewId image_view_id : image_view_ids) {
if (!immediate_delete) {
sentenced_image_view.Push(std::move(slot_image_views[image_view_id]));
}
slot_image_views.erase(image_view_id);
}
if (!immediate_delete) {
sentenced_images.Push(std::move(slot_images[image_id]));
}
slot_images.erase(image_id);
// Clean up allocation table
alloc_images.erase(alloc_image_it);
if (alloc_images.empty()) {
image_allocs_table.erase(alloc_it);
}
// Mark tables as invalidated
for (size_t c : active_channel_ids) {
auto& channel_info = channel_storage[c];
if constexpr (ENABLE_VALIDATION) {

View File

@ -1,446 +0,0 @@
// 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, &reg) < 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, &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

View File

@ -1,119 +0,0 @@
// 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

View File

@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
"name": "citron",
"builtin-baseline": "bc994510d2eb11aac7b43b03f67a7751d5bfe0e4",
"builtin-baseline": "c82f74667287d3dc386bce81e44964370c91a289",
"version": "1.0",
"dependencies": [
"boost-algorithm",