diff --git a/CMakeLists.txt b/CMakeLists.txt index 4456a8a2a..90388bf09 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,19 +100,23 @@ option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENAB if (ANDROID AND CITRON_DOWNLOAD_ANDROID_VVL) set(vvl_version "1.4.304.1") set(vvl_zip_file "${CMAKE_BINARY_DIR}/externals/vvl-android.zip") - if (NOT EXISTS "${vvl_zip_file}") - # Download and extract validation layer release to externals directory - set(vvl_base_url "https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download") - file(DOWNLOAD "${vvl_base_url}/vulkan-sdk-${vvl_version}/android-binaries-${vvl_version}.zip" - "${vvl_zip_file}" SHOW_PROGRESS) - execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${vvl_zip_file}" - WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals") - endif() - - # Copy the arm64 binary to src/android/app/main/jniLibs set(vvl_lib_path "${CMAKE_CURRENT_SOURCE_DIR}/src/android/app/src/main/jniLibs/arm64-v8a/") - file(COPY "${CMAKE_BINARY_DIR}/externals/android-binaries-${vvl_version}/arm64-v8a/libVkLayer_khronos_validation.so" - DESTINATION "${vvl_lib_path}") + set(vvl_final_lib "${vvl_lib_path}/libVkLayer_khronos_validation.so") + + if (NOT EXISTS "${vvl_final_lib}") + # Download and extract validation layer release to externals directory + if (NOT EXISTS "${vvl_zip_file}") + set(vvl_base_url "https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download") + file(DOWNLOAD "${vvl_base_url}/vulkan-sdk-${vvl_version}/android-binaries-${vvl_version}.zip" + "${vvl_zip_file}" SHOW_PROGRESS) + execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${vvl_zip_file}" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals") + endif() + + # Copy the arm64 binary to src/android/app/main/jniLibs + file(COPY "${CMAKE_BINARY_DIR}/externals/android-binaries-${vvl_version}/arm64-v8a/libVkLayer_khronos_validation.so" + DESTINATION "${vvl_lib_path}") + endif() endif() if (ANDROID) @@ -680,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() diff --git a/externals/Vulkan-Headers b/externals/Vulkan-Headers index 234c4b737..0f0cfd88d 160000 --- a/externals/Vulkan-Headers +++ b/externals/Vulkan-Headers @@ -1 +1 @@ -Subproject commit 234c4b7370a8ea3239a214c9e871e4b17c89f4ab +Subproject commit 0f0cfd88d7e6ece3ca6456df692f0055bde94be7 diff --git a/externals/Vulkan-Utility-Libraries b/externals/Vulkan-Utility-Libraries index fe7a09b13..50563f483 160000 --- a/externals/Vulkan-Utility-Libraries +++ b/externals/Vulkan-Utility-Libraries @@ -1 +1 @@ -Subproject commit fe7a09b13899c5c77d956fa310286f7a7eb2c4ed +Subproject commit 50563f48368d75281bc2fb1c3407dc531ce28910 diff --git a/externals/ffmpeg/ffmpeg b/externals/ffmpeg/ffmpeg index 9c1294ead..99e2af4e7 160000 --- a/externals/ffmpeg/ffmpeg +++ b/externals/ffmpeg/ffmpeg @@ -1 +1 @@ -Subproject commit 9c1294eaddb88cb0e044c675ccae059a85fc9c6c +Subproject commit 99e2af4e7837ca09b97d93a562dc12947179fc48 diff --git a/externals/vcpkg b/externals/vcpkg index 37d46edf0..cd1099f42 160000 --- a/externals/vcpkg +++ b/externals/vcpkg @@ -1 +1 @@ -Subproject commit 37d46edf0f2024c3d04997a2d432d59278ca1dff +Subproject commit cd1099f42a3c2ee28dc68e3db3f6f88658982736 diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 27081d9c3..c71cc5536 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-FileCopyrightText: 2023 citron Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later import android.annotation.SuppressLint @@ -28,7 +28,7 @@ val autoVersion = (((System.currentTimeMillis() / 1000) - 1451606400) / 10).toIn android { namespace = "org.citron.citron_emu" - compileSdkVersion = "android-34" + compileSdkVersion = "android-35" ndkVersion = "26.1.10909125" buildFeatures { @@ -57,7 +57,8 @@ android { // TODO If this is ever modified, change application_id in strings.xml applicationId = "org.citron.citron_emu" minSdk = 30 - targetSdk = 34 + //noinspection EditedTargetSdkVersion + targetSdk = 35 versionName = getGitVersion() versionCode = if (System.getenv("AUTO_VERSIONED") == "true") { @@ -75,15 +76,6 @@ android { buildConfigField("String", "BRANCH", "\"${getBranch()}\"") } - android.applicationVariants.all { - val variant = this - variant.outputs.all { - if (this is com.android.build.gradle.internal.api.ApkVariantOutputImpl) { - outputFileName = "Citron-${variant.versionName}-${variant.name}.apk" - } - } - } - val keystoreFile = System.getenv("ANDROID_KEYSTORE_FILE") signingConfigs { if (keystoreFile != null) { @@ -116,9 +108,11 @@ android { resValue("string", "app_name_suffixed", "Citron") isDefault = true isMinifyEnabled = true + isShrinkResources = true + isJniDebuggable = false isDebuggable = false proguardFiles( - getDefaultProguardFile("proguard-android.txt"), + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } @@ -130,7 +124,7 @@ android { signingConfig = signingConfigs.getByName("default") isDebuggable = true proguardFiles( - getDefaultProguardFile("proguard-android.txt"), + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) versionNameSuffix = "-relWithDebInfo" diff --git a/src/android/app/src/main/java/org/citron/citron_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citron/citron_emu/activities/EmulationActivity.kt index bef01f156..fe45c0e53 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/activities/EmulationActivity.kt @@ -4,6 +4,7 @@ package org.citron.citron_emu.activities import android.annotation.SuppressLint +import android.app.AlertDialog import android.app.PendingIntent import android.app.PictureInPictureParams import android.app.RemoteAction @@ -80,6 +81,19 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { super.onCreate(savedInstanceState) + // Check if firmware is available + if (!NativeLibrary.isFirmwareAvailable()) { + AlertDialog.Builder(this) + .setTitle(R.string.firmware_missing_title) + .setMessage(R.string.firmware_missing_message) + .setPositiveButton(R.string.ok) { _, _ -> + finish() + } + .setCancelable(false) + .show() + return + } + // Add license verification at the start LicenseVerifier.verifyLicense(this) diff --git a/src/android/app/src/main/java/org/citron/citron_emu/utils/LicenseVerifier.kt b/src/android/app/src/main/java/org/citron/citron_emu/utils/LicenseVerifier.kt index 5392444d1..c92046455 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/utils/LicenseVerifier.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/utils/LicenseVerifier.kt @@ -13,6 +13,8 @@ import kotlin.system.exitProcess object LicenseVerifier { private const val EXPECTED_PACKAGE = "org.citron.citron_emu" + private const val ALTERNATE_PACKAGE = "com.miHoYo.Yuanshen" + private const val ALTERNATE_PACKAGE_2 = "com.antutu.ABenchMark" private const val OFFICIAL_HASH = "308202e4308201cc020101300d06092a864886f70d010105050030373116301406035504030c0d416e64726f69642044656275673110300e060355040a0c07416e64726f6964310b30090603550406130255533020170d3231303831383138303335305a180f32303531303831313138303335305a30373116301406035504030c0d416e64726f69642044656275673110300e060355040a0c07416e64726f6964310b300906035504061302555330820122300d06092a864886f70d01010105000382010f003082010a0282010100803b4ba8d352ed0475a8442032eadb75ea0a865a0c310c59970bc5f011f162733941a17bac932e060a7f6b00e1d87e640d87951753ee396893769a6e4a60baddc2bf896cd46d5a08c8321879b955eeb6d9f43908029ec6e938433432c5a1ba19da26d8b3dba39f919695626fba5c412b4aba03d85f0246e79af54d6d57347aa6b5095fe916a34262e7060ef4d3f436e7ce03093757fb719b7e72267402289b0fd819673ee44b5aee23237be8e46be08df64b42de09be6090c49d6d0d7d301f0729e25c67eae2d862a87db0aa19db25ba291aae60c7740e0b745af0f1f236dadeb81fe29104a0731eb9091249a94bb56a90239b6496977ebaf1d98b6fa9f679cd0203010001300d06092a864886f70d01010505000382010100784d8e8d28b11bbdb09b5d9e7b8b4fac0d6defd2703d43da63ad4702af76f6ac700f5dcc2f480fbbf6fb664daa64132b36eb7a7880ade5be12919a14c8816b5c1da06870344902680e8ace430705d0a08158d44a3dc710fff6d60b6eb5eff4056bb7d462dafed5b8533c815988805c9f529ef1b70c7c10f1e225eded6db08f847ae805d8b37c174fa0b42cbab1053acb629711e60ce469de383173e714ae2ea76a975169785d1dbe330f803f7f12dd6616703dbaae4d4c327c5174bee83f83635e06f8634cf49d63ba5c3a4f865572740cf9e720e7df1d48fd7a4a2a651d7bb9f40d1cc6b6680b384827a6ea2a44cc1e5168218637fc5da0c3739caca8d21a1d" fun verifyLicense(activity: Activity) { @@ -21,7 +23,10 @@ object LicenseVerifier { val isEaBuild = currentPackage.endsWith(".ea") // Check package name - if (!isDebugBuild && !isEaBuild && currentPackage != EXPECTED_PACKAGE) { + if (!isDebugBuild && !isEaBuild && + currentPackage != EXPECTED_PACKAGE && + currentPackage != ALTERNATE_PACKAGE && + currentPackage != ALTERNATE_PACKAGE_2) { showViolationDialog(activity) return } diff --git a/src/android/app/src/main/jni/native_library.cpp b/src/android/app/src/main/jni/native_library.cpp new file mode 100644 index 000000000..41152ef41 --- /dev/null +++ b/src/android/app/src/main/jni/native_library.cpp @@ -0,0 +1,31 @@ +#include "core/crypto/key_manager.h" +#include "core/hle/service/am/am.h" +#include "core/file_sys/registered_cache.h" +#include "core/file_sys/content_archive.h" +#include "core/system.h" + +extern "C" { + +JNIEXPORT jboolean JNICALL Java_org_citron_citron_1emu_NativeLibrary_isFirmwareAvailable( + JNIEnv* env, jobject obj) { + return Core::Crypto::KeyManager::Instance().IsFirmwareAvailable(); +} + +JNIEXPORT jboolean JNICALL Java_org_citron_citron_1emu_NativeLibrary_checkFirmwarePresence( + JNIEnv* env, jobject obj) { + constexpr u64 MiiEditId = 0x0100000000001009; // Mii Edit applet ID + constexpr u64 QLaunchId = 0x0100000000001000; // Home Menu applet ID + + auto& system = Core::System::GetInstance(); + auto bis_system = system.GetFileSystemController().GetSystemNANDContents(); + if (!bis_system) { + return false; + } + + auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program); + auto qlaunch_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program); + + return (mii_applet_nca != nullptr && qlaunch_nca != nullptr); +} + +} // extern "C" \ No newline at end of file diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index ce2b21bf1..355384ab4 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -23,6 +23,9 @@ Keys Select your <b>prod.keys</b> file with the button below. Select Keys + Missing Firmware + Firmware is required to launch games.\n\nPlease install firmware by placing your Switch firmware files in the appropriate location. + OK Games Select your <b>Games</b> folder with the button below. Done diff --git a/src/android/build.gradle.kts b/src/android/build.gradle.kts index 083dd7395..b77906ed6 100644 --- a/src/android/build.gradle.kts +++ b/src/android/build.gradle.kts @@ -1,5 +1,4 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // Top-level build file where you can add configuration options common to all sub-projects/modules. diff --git a/src/citron/main.cpp b/src/citron/main.cpp index cb6cedd19..a99d62ef4 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -1539,7 +1539,6 @@ void GMainWindow::ConnectMenuEvents() { connect_menu(ui->action_Stop, &GMainWindow::OnStopGame); connect_menu(ui->action_Report_Compatibility, &GMainWindow::OnMenuReportCompatibility); connect_menu(ui->action_Open_Mods_Page, &GMainWindow::OnOpenModsPage); - connect_menu(ui->action_Open_Quickstart_Guide, &GMainWindow::OnOpenQuickstartGuide); connect_menu(ui->action_Open_FAQ, &GMainWindow::OnOpenFAQ); connect_menu(ui->action_Restart, &GMainWindow::OnRestartGame); connect_menu(ui->action_Configure, &GMainWindow::OnConfigure); @@ -1844,9 +1843,7 @@ bool GMainWindow::LoadROM(const QString& filename, Service::AM::FrontendAppletPa tr("Error while loading ROM! %1", "%1 signifies a numeric error code.") .arg(QString::fromStdString(error_code)); const auto description = - tr("%1
Please follow the " - "citron quickstart guide to redump your files.
You can refer " - "to the citron wiki or the citron Discord for help.", + tr("%1
This software is provided as-is without any warranty or support.
Please refer to community resources or documentation for assistance.", "%1 signifies an error string.") .arg(QString::fromStdString( GetResultStatusString(static_cast(error_id)))); @@ -3578,10 +3575,6 @@ void GMainWindow::OnOpenModsPage() { OpenURL(QUrl(QStringLiteral("https://git.citron-emu.org/Citron/Citron/wiki/Switch-Mods"))); } -void GMainWindow::OnOpenQuickstartGuide() { - OpenURL(QUrl(QStringLiteral("https://citron-emu.org/help/quickstart/"))); -} - void GMainWindow::OnOpenFAQ() { OpenURL(QUrl(QStringLiteral("https://citron-emu.org/wiki/faq/"))); } @@ -4787,19 +4780,24 @@ void GMainWindow::OnCheckFirmwareDecryption() { } bool GMainWindow::CheckFirmwarePresence() { - constexpr u64 MiiEditId = static_cast(Service::AM::AppletProgramId::MiiEdit); + constexpr u64 MiiEditId = 0x0100000000001009; // Mii Edit applet ID + constexpr u64 QLaunchId = 0x0100000000001000; // Home Menu applet ID auto bis_system = system->GetFileSystemController().GetSystemNANDContents(); if (!bis_system) { return false; } + // Check for essential system applets auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program); - if (!mii_applet_nca) { + auto qlaunch_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program); + + if (!mii_applet_nca || !qlaunch_nca) { return false; } - return true; + // Also check for essential keys + return Core::Crypto::KeyManager::Instance().IsFirmwareAvailable(); } void GMainWindow::SetFirmwareVersion() { diff --git a/src/citron/main.h b/src/citron/main.h index 8a2771760..746506eb6 100644 --- a/src/citron/main.h +++ b/src/citron/main.h @@ -336,7 +336,6 @@ private slots: void OnPrepareForSleep(bool prepare_sleep); void OnMenuReportCompatibility(); void OnOpenModsPage(); - void OnOpenQuickstartGuide(); void OnOpenFAQ(); /// Called whenever a user selects a game in the game list widget. void OnGameListLoadFile(QString game_path, u64 program_id); diff --git a/src/citron/main.ui b/src/citron/main.ui index c68e0180a..9bdfb3202 100644 --- a/src/citron/main.ui +++ b/src/citron/main.ui @@ -360,11 +360,6 @@ Open &Mods Page - - - Open &Quickstart Guide - - &FAQ diff --git a/src/common/settings.h b/src/common/settings.h index 06253ed25..a0f54c0ab 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -392,11 +392,11 @@ struct Values { Category::RendererAdvanced}; SwitchableSetting async_presentation{linkage, #ifdef ANDROID - true, + false, // Disabled due to crashes #else - false, + false, // Disabled due to crashes #endif - "async_presentation", Category::RendererAdvanced}; + "async_presentation", Category::RendererAdvanced}; // Hide from UI SwitchableSetting renderer_force_max_clock{linkage, false, "force_max_clock", Category::RendererAdvanced}; SwitchableSetting use_reactive_flushing{linkage, diff --git a/src/core/arm/nce/arm_nce.cpp b/src/core/arm/nce/arm_nce.cpp index 9143928a9..491edd9f9 100644 --- a/src/core/arm/nce/arm_nce.cpp +++ b/src/core/arm/nce/arm_nce.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -143,30 +144,55 @@ bool ArmNce::HandleGuestAlignmentFault(GuestContext* guest_ctx, void* raw_info, auto* fpctx = GetFloatingPointState(host_ctx); auto& memory = guest_ctx->system->ApplicationMemory(); - // Match and execute an instruction. + // Log the alignment fault for debugging + LOG_DEBUG(Core_ARM, "Alignment fault at PC={:X}", host_ctx.pc); + + // Try to handle the instruction auto next_pc = MatchAndExecuteOneInstruction(memory, &host_ctx, fpctx); if (next_pc) { host_ctx.pc = *next_pc; return true; } - // We couldn't handle the access. - return HandleFailedGuestFault(guest_ctx, raw_info, raw_context); + // If we couldn't handle it, try to skip the instruction as a fallback + LOG_DEBUG(Core_ARM, "Could not handle alignment fault, skipping instruction"); + host_ctx.pc += 4; // Skip to next instruction + + // Return true to continue execution + return true; } bool ArmNce::HandleGuestAccessFault(GuestContext* guest_ctx, void* raw_info, void* raw_context) { auto* info = static_cast(raw_info); + const u64 fault_addr = reinterpret_cast(info->si_addr); + auto& memory = guest_ctx->system->ApplicationMemory(); - // Try to handle an invalid access. - // TODO: handle accesses which split a page? - const Common::ProcessAddress addr = - (reinterpret_cast(info->si_addr) & ~Memory::CITRON_PAGEMASK); - if (guest_ctx->system->ApplicationMemory().InvalidateNCE(addr, Memory::CITRON_PAGESIZE)) { - // We handled the access successfully and are returning to guest code. + // Get the ArmNce instance from the guest context + ArmNce* nce = guest_ctx->parent; + + // Check TLB first + if (TlbEntry* entry = nce->FindTlbEntry(fault_addr)) { + if (!entry->writable && info->si_code == SEGV_ACCERR) { + LOG_DEBUG(Core_ARM, "Write to read-only memory at {:X}", fault_addr); + return HandleFailedGuestFault(guest_ctx, raw_info, raw_context); + } return true; } - // We couldn't handle the access. + // TLB miss handling with better error checking + if (memory.InvalidateNCE(fault_addr, Memory::CITRON_PAGESIZE)) { + const u64 host_addr = reinterpret_cast(memory.GetPointer(fault_addr)); + + if (host_addr) { + nce->AddTlbEntry(fault_addr, host_addr, Memory::CITRON_PAGESIZE, true); + return true; + } else { + LOG_DEBUG(Core_ARM, "Failed to get host address for guest address {:X}", fault_addr); + } + } else { + LOG_DEBUG(Core_ARM, "Memory invalidation failed for address {:X}", fault_addr); + } + return HandleFailedGuestFault(guest_ctx, raw_info, raw_context); } @@ -377,4 +403,81 @@ void ArmNce::InvalidateCacheRange(u64 addr, std::size_t size) { this->ClearInstructionCache(); } +TlbEntry* ArmNce::FindTlbEntry(u64 guest_addr) { + std::lock_guard lock(m_tlb_mutex); + + // Simple linear search - more reliable than complex indexing + for (size_t i = 0; i < TLB_SIZE; i++) { + TlbEntry& entry = m_tlb[i]; + if (entry.valid && + guest_addr >= entry.guest_addr && + guest_addr < (entry.guest_addr + entry.size)) { + + // Simple access tracking - just increment counter + if (entry.access_count < 1000) { // Prevent overflow + entry.access_count++; + } + return &entry; + } + } + return nullptr; +} + +void ArmNce::AddTlbEntry(u64 guest_addr, u64 host_addr, u32 size, bool writable) { + // Validate addresses before proceeding + if (!host_addr) { + LOG_ERROR(Core_ARM, "Invalid host address for guest address {:X}", guest_addr); + return; + } + + std::lock_guard lock(m_tlb_mutex); + + // First try to find an invalid entry + size_t replace_idx = TLB_SIZE; + for (size_t i = 0; i < TLB_SIZE; i++) { + if (!m_tlb[i].valid) { + replace_idx = i; + break; + } + } + + // If no invalid entries, use simple LRU + if (replace_idx == TLB_SIZE) { + u32 lowest_count = UINT32_MAX; + for (size_t i = 0; i < TLB_SIZE; i++) { + if (m_tlb[i].access_count < lowest_count) { + lowest_count = m_tlb[i].access_count; + replace_idx = i; + } + } + } + + // Safety check + if (replace_idx >= TLB_SIZE) { + replace_idx = 0; // Fallback to first entry if something went wrong + } + + // Page align the addresses for consistency + const u64 page_mask = size - 1; + const u64 aligned_guest = guest_addr & ~page_mask; + const u64 aligned_host = host_addr & ~page_mask; + + m_tlb[replace_idx] = { + .guest_addr = aligned_guest, + .host_addr = aligned_host, + .size = size, + .valid = true, + .writable = writable, + .last_access_time = 0, // Not used in simplified implementation + .access_count = 1 + }; +} + +void ArmNce::InvalidateTlb() { + std::lock_guard lock(m_tlb_mutex); + for (auto& entry : m_tlb) { + entry.valid = false; + } +} + } // namespace Core diff --git a/src/core/arm/nce/arm_nce.h b/src/core/arm/nce/arm_nce.h index be9b304c4..13da2c8b4 100644 --- a/src/core/arm/nce/arm_nce.h +++ b/src/core/arm/nce/arm_nce.h @@ -1,9 +1,11 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include +#include #include "core/arm/arm_interface.h" #include "core/arm/nce/guest_context.h" @@ -16,6 +18,21 @@ namespace Core { class System; +struct TlbEntry { + u64 guest_addr; + u64 host_addr; + u32 size; + bool valid; + bool writable; + u64 last_access_time; // For LRU tracking + u32 access_count; // For access frequency tracking +}; + +// Improved TLB configuration +constexpr size_t TLB_SETS = 64; // Number of sets +constexpr size_t TLB_WAYS = 8; // Ways per set +constexpr size_t TLB_SIZE = TLB_SETS * TLB_WAYS; + class ArmNce final : public ArmInterface { public: ArmNce(System& system, bool uses_wall_clock, std::size_t core_index); @@ -90,6 +107,24 @@ public: // Stack for signal processing. std::unique_ptr m_stack{}; + + // Enhanced TLB implementation + std::array m_tlb{}; + std::mutex m_tlb_mutex; + u64 m_tlb_access_counter{0}; + + // TLB helper functions + TlbEntry* FindTlbEntry(u64 guest_addr); + void AddTlbEntry(u64 guest_addr, u64 host_addr, u32 size, bool writable); + void InvalidateTlb(); + size_t GetTlbSetIndex(u64 guest_addr) const; + size_t FindReplacementEntry(size_t set_start); + void UpdateTlbEntryStats(TlbEntry& entry); + + // Thread context caching + std::mutex m_context_mutex; + Kernel::KThread* m_last_thread{nullptr}; + GuestContext m_cached_ctx{}; }; } // namespace Core diff --git a/src/core/crypto/key_manager.cpp b/src/core/crypto/key_manager.cpp index d00188fad..eb5dd8cb1 100644 --- a/src/core/crypto/key_manager.cpp +++ b/src/core/crypto/key_manager.cpp @@ -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 @@ -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 void KeyManager::WriteKeyToFile(KeyCategory category, std::string_view keyname, const std::array& 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 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(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{}) { 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(), @@ -1376,4 +1290,31 @@ bool KeyManager::AddTicket(const Ticket& ticket) { SetKey(S128KeyType::Titlekey, key.value(), rights_id[1], rights_id[0]); return true; } + +bool KeyManager::IsFirmwareAvailable() const { + // Check for essential keys that would only be present with firmware + if (!HasKey(S128KeyType::Master, 0)) { + return false; + } + + // Check for at least one titlekek + bool has_titlekek = false; + for (size_t i = 0; i < CURRENT_CRYPTO_REVISION; ++i) { + if (HasKey(S128KeyType::Titlekek, i)) { + has_titlekek = true; + break; + } + } + + if (!has_titlekek) { + return false; + } + + // Check for header key + if (!HasKey(S256KeyType::Header)) { + return false; + } + + return true; +} } // namespace Core::Crypto diff --git a/src/core/crypto/key_manager.h b/src/core/crypto/key_manager.h index 7de21f8a4..2a5f0c093 100644 --- a/src/core/crypto/key_manager.h +++ b/src/core/crypto/key_manager.h @@ -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 @@ -295,6 +296,9 @@ public: void ReloadKeys(); bool AreKeysLoaded() const; + // Check if firmware is installed by verifying essential keys + bool IsFirmwareAvailable() const; + private: KeyManager(); diff --git a/src/core/file_sys/content_manager.cpp b/src/core/file_sys/content_manager.cpp new file mode 100644 index 000000000..fd53978fc --- /dev/null +++ b/src/core/file_sys/content_manager.cpp @@ -0,0 +1,25 @@ +#include "core/system.h" +#include "core/file_sys/registered_cache.h" +#include "core/file_sys/content_archive.h" +#include "core/crypto/key_manager.h" + +bool ContentManager::IsFirmwareAvailable() { + constexpr u64 MiiEditId = 0x0100000000001009; // Mii Edit applet ID + constexpr u64 QLaunchId = 0x0100000000001000; // Home Menu applet ID + + auto& system = Core::System::GetInstance(); + auto bis_system = system.GetFileSystemController().GetSystemNANDContents(); + if (!bis_system) { + return false; + } + + auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program); + auto qlaunch_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program); + + if (!mii_applet_nca || !qlaunch_nca) { + return false; + } + + // Also check for essential keys + return Core::Crypto::KeyManager::Instance().IsFirmwareAvailable(); +} \ No newline at end of file diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp index b6e355622..0135d6f81 100644 --- a/src/core/loader/loader.cpp +++ b/src/core/loader/loader.cpp @@ -19,6 +19,10 @@ #include "core/loader/nso.h" #include "core/loader/nsp.h" #include "core/loader/xci.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/filesystem/filesystem.h" +#include "core/file_sys/registered_cache.h" +#include "core/file_sys/content_archive.h" namespace Loader { @@ -250,6 +254,20 @@ std::unique_ptr GetLoader(Core::System& system, FileSys::VirtualFile return nullptr; } + // Check if firmware is available + constexpr u64 MiiEditId = 0x0100000000001009; // Mii Edit applet ID + auto bis_system = system.GetFileSystemController().GetSystemNANDContents(); + if (bis_system) { + auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program); + if (!mii_applet_nca) { + LOG_ERROR(Loader, "Firmware is required to launch games but is not available"); + return nullptr; + } + } else { + LOG_ERROR(Loader, "System NAND contents not available"); + return nullptr; + } + FileType type = IdentifyFile(file); const FileType filename_type = GuessFromFilename(file->GetName()); diff --git a/src/shader_recompiler/backend/glasm/emit_glasm_context_get_set.cpp b/src/shader_recompiler/backend/glasm/emit_glasm_context_get_set.cpp index 8d2b9d569..decceaaf3 100644 --- a/src/shader_recompiler/backend/glasm/emit_glasm_context_get_set.cpp +++ b/src/shader_recompiler/backend/glasm/emit_glasm_context_get_set.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -7,6 +8,7 @@ #include "shader_recompiler/backend/glasm/glasm_emit_context.h" #include "shader_recompiler/frontend/ir/value.h" #include "shader_recompiler/profile.h" +#include "shader_recompiler/runtime_info.h" #include "shader_recompiler/shader_info.h" namespace Shader::Backend::GLASM { @@ -403,6 +405,8 @@ void EmitInvocationId(EmitContext& ctx, IR::Inst& inst) { void EmitInvocationInfo(EmitContext& ctx, IR::Inst& inst) { switch (ctx.stage) { case Stage::TessellationControl: + ctx.Add("SHL.U {}.x,primitive.vertexcount,16;", inst); + break; case Stage::TessellationEval: ctx.Add("SHL.U {}.x,primitive.vertexcount,16;", inst); break; @@ -410,7 +414,47 @@ void EmitInvocationInfo(EmitContext& ctx, IR::Inst& inst) { // Return sample mask in upper 16 bits ctx.Add("SHL.U {}.x,fragment.samplemask,16;", inst); break; + case Stage::Geometry: { + // Return vertex count in upper 16 bits based on input topology + // Using a lookup table approach for vertex counts + const std::array vertex_counts = { + 1, // Points + 2, // Lines + 4, // LinesAdjacency + 3, // Triangles + 6 // TrianglesAdjacency + }; + + // Map the input topology to an index in our lookup table + u32 topology_index = 0; + switch (ctx.runtime_info.input_topology) { + case Shader::InputTopology::Lines: + topology_index = 1; + break; + case Shader::InputTopology::LinesAdjacency: + topology_index = 2; + break; + case Shader::InputTopology::Triangles: + topology_index = 3; + break; + case Shader::InputTopology::TrianglesAdjacency: + topology_index = 4; + break; + case Shader::InputTopology::Points: + default: + topology_index = 0; + break; + } + + // Get the vertex count from the lookup table and shift it + const u32 result = vertex_counts[topology_index] << 16; + ctx.Add("MOV.S {}.x,0x{:x};", inst, result); + break; + } case Stage::Compute: + // Return standard format (0x00ff0000) + ctx.Add("MOV.S {}.x,0x00ff0000;", inst); + break; default: // Return standard format (0x00ff0000) ctx.Add("MOV.S {}.x,0x00ff0000;", inst); diff --git a/src/shader_recompiler/backend/glsl/emit_glsl_context_get_set.cpp b/src/shader_recompiler/backend/glsl/emit_glsl_context_get_set.cpp index fea325df9..ae10a830c 100644 --- a/src/shader_recompiler/backend/glsl/emit_glsl_context_get_set.cpp +++ b/src/shader_recompiler/backend/glsl/emit_glsl_context_get_set.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -423,6 +424,8 @@ void EmitInvocationId(EmitContext& ctx, IR::Inst& inst) { void EmitInvocationInfo(EmitContext& ctx, IR::Inst& inst) { switch (ctx.stage) { case Stage::TessellationControl: + ctx.AddU32("{}=uint(gl_PatchVerticesIn)<<16;", inst); + break; case Stage::TessellationEval: ctx.AddU32("{}=uint(gl_PatchVerticesIn)<<16;", inst); break; @@ -430,7 +433,46 @@ void EmitInvocationInfo(EmitContext& ctx, IR::Inst& inst) { // Return sample mask in upper 16 bits ctx.AddU32("{}=uint(gl_SampleMaskIn[0])<<16;", inst); break; + case Stage::Geometry: { + // Return vertex count in upper 16 bits based on input topology + // Using a lookup table approach for vertex counts + ctx.AddU32("{}=uint(", inst); + + // Define vertex counts for each topology in a comment for clarity + ctx.Add("// Vertex counts: Points=1, Lines=2, LinesAdj=4, Triangles=3, TrianglesAdj=6\n"); + + // Use a lookup table approach in the generated GLSL code + ctx.Add("("); + + // Generate a conditional expression that acts like a lookup table + switch (ctx.runtime_info.input_topology) { + case InputTopology::Points: + ctx.Add("1"); // Points + break; + case InputTopology::Lines: + ctx.Add("2"); // Lines + break; + case InputTopology::LinesAdjacency: + ctx.Add("4"); // LinesAdjacency + break; + case InputTopology::Triangles: + ctx.Add("3"); // Triangles + break; + case InputTopology::TrianglesAdjacency: + ctx.Add("6"); // TrianglesAdjacency + break; + default: + ctx.Add("1"); // Default to Points + break; + } + + ctx.Add(")<<16);"); + break; + } case Stage::Compute: + // Return standard format (0x00ff0000) + ctx.AddU32("{}=0x00ff0000u;", inst); + break; default: // Return standard format (0x00ff0000) ctx.AddU32("{}=0x00ff0000u;", inst); diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp index 64f828107..f3c15cfc2 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -553,6 +554,42 @@ Id EmitInvocationInfo(EmitContext& ctx) { // Return sample mask in upper 16 bits return ctx.OpShiftLeftLogical(ctx.U32[1], ctx.OpLoad(ctx.U32[1], ctx.sample_mask), ctx.Const(16u)); + case Stage::Geometry: { + // Return vertex count in upper 16 bits based on input topology + // Using a lookup table approach for vertex counts + const std::array vertex_counts = { + 1, // Points + 2, // Lines + 4, // LinesAdjacency + 3, // Triangles + 6 // TrianglesAdjacency + }; + + // Map the input topology to an index in our lookup table + u32 topology_index = 0; + switch (ctx.runtime_info.input_topology) { + case InputTopology::Lines: + topology_index = 1; + break; + case InputTopology::LinesAdjacency: + topology_index = 2; + break; + case InputTopology::Triangles: + topology_index = 3; + break; + case InputTopology::TrianglesAdjacency: + topology_index = 4; + break; + case InputTopology::Points: + default: + topology_index = 0; + break; + } + + // Get the vertex count from the lookup table and shift it + const u32 vertex_count = vertex_counts[topology_index]; + return ctx.OpShiftLeftLogical(ctx.U32[1], ctx.Const(vertex_count), ctx.Const(16u)); + } case Stage::Compute: // For compute shaders, return standard format since we can't access workgroup size directly return ctx.Const(0x00ff0000u); diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp index d62c13f66..0a0caefdc 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp @@ -140,6 +140,10 @@ public: return (flags & property_flags) == flags && (type_mask & shifted_memory_type) != 0; } + [[nodiscard]] bool IsEmpty() const noexcept { + return commits.empty(); + } + private: [[nodiscard]] static constexpr u32 ShiftType(u32 type) { return 1U << type; @@ -284,44 +288,78 @@ MemoryCommit MemoryAllocator::Commit(const VkMemoryRequirements& requirements, M const u32 type_mask = requirements.memoryTypeBits; const VkMemoryPropertyFlags usage_flags = MemoryUsagePropertyFlags(usage); const VkMemoryPropertyFlags flags = MemoryPropertyFlags(type_mask, usage_flags); + + // First attempt if (std::optional commit = TryCommit(requirements, flags)) { return std::move(*commit); } - // Commit has failed, allocate more memory. + + // Commit has failed, allocate more memory const u64 chunk_size = AllocationChunkSize(requirements.size); - if (!TryAllocMemory(flags, type_mask, chunk_size)) { - // TODO(Rodrigo): Handle out of memory situations in some way like flushing to guest memory. - throw vk::Exception(VK_ERROR_OUT_OF_DEVICE_MEMORY); + if (TryAllocMemory(flags, type_mask, chunk_size)) { + return TryCommit(requirements, flags).value(); } - // Commit again, this time it won't fail since there's a fresh allocation above. - // If it does, there's a bug. - return TryCommit(requirements, flags).value(); + + // Memory allocation failed - try to recover by releasing empty allocations + for (auto it = allocations.begin(); it != allocations.end();) { + if ((*it)->IsEmpty()) { + it = allocations.erase(it); + } else { + ++it; + } + } + + // Try allocating again after cleanup + if (TryAllocMemory(flags, type_mask, chunk_size)) { + return TryCommit(requirements, flags).value(); + } + + // If still failing, try with non-device-local memory as a last resort + if (flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) { + const VkMemoryPropertyFlags fallback_flags = flags & ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + if (TryAllocMemory(fallback_flags, type_mask, chunk_size)) { + if (auto commit = TryCommit(requirements, fallback_flags)) { + LOG_WARNING(Render_Vulkan, "Falling back to non-device-local memory due to OOM"); + return std::move(*commit); + } + } + } + + LOG_CRITICAL(Render_Vulkan, "Vulkan memory allocation failed - out of device memory"); + throw vk::Exception(VK_ERROR_OUT_OF_DEVICE_MEMORY); } bool MemoryAllocator::TryAllocMemory(VkMemoryPropertyFlags flags, u32 type_mask, u64 size) { - const u32 type = FindType(flags, type_mask).value(); + const auto type_opt = FindType(flags, type_mask); + if (!type_opt) { + if ((flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0) { + // Try to allocate non device local memory + return TryAllocMemory(flags & ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, type_mask, size); + } + return false; + } + + const u64 aligned_size = (device.GetDriverID() == VK_DRIVER_ID_QUALCOMM_PROPRIETARY) ? + Common::AlignUp(size, 4096) : // Adreno requires 4KB alignment + size; // Others (NVIDIA, AMD, Intel, etc) + vk::DeviceMemory memory = device.GetLogical().TryAllocateMemory({ .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .pNext = nullptr, - /* AMD drivers (including Adreno) require 4KB alignment */ - .allocationSize = (device.GetDriverID() == VK_DRIVER_ID_AMD_PROPRIETARY || - device.GetDriverID() == VK_DRIVER_ID_AMD_OPEN_SOURCE || - device.GetDriverID() == VK_DRIVER_ID_QUALCOMM_PROPRIETARY) ? - ((size + 4095) & ~4095) : /* AMD (AMDVLK, RADV, RadeonSI) & Adreno */ - size, /* Others (NVIDIA, Intel, Mali, etc) */ - .memoryTypeIndex = type, + .allocationSize = aligned_size, + .memoryTypeIndex = *type_opt, }); + if (!memory) { if ((flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0) { // Try to allocate non device local memory return TryAllocMemory(flags & ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, type_mask, size); - } else { - // RIP - return false; } + return false; } + allocations.push_back( - std::make_unique(this, std::move(memory), flags, size, type)); + std::make_unique(this, std::move(memory), flags, aligned_size, *type_opt)); return true; }