Compare commits
42 commits
v0.5-canar
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae75413cc3 | ||
|
|
6a31da5905 | ||
|
|
a5125d008a | ||
|
|
b24dd921aa | ||
|
|
9a65205dba | ||
|
|
af4f08be33 | ||
|
|
0d0963d32f | ||
|
|
4491127f52 | ||
|
|
c304afe2b3 | ||
|
|
b8240b4214 | ||
|
|
91487f6d96 | ||
|
|
031c635095 | ||
|
|
90a8165f77 | ||
|
|
6565055865 | ||
|
|
ee3d858935 | ||
|
|
31694994f2 | ||
|
|
4197fa84a0 | ||
|
|
e4342324fe | ||
|
|
78b0080b96 | ||
|
|
644ed69285 | ||
|
|
3554f55fc9 | ||
|
|
dc9532b4d1 | ||
|
|
1308e2b935 | ||
|
|
5caecd8151 | ||
|
|
dc9fbcc893 | ||
|
|
b1d5d4e5be | ||
|
|
f0d8daf755 | ||
|
|
cbb9a35166 | ||
|
|
cfe437aacf | ||
|
|
9b293c3a98 | ||
|
|
84e5fbc089 | ||
|
|
a442078ee4 | ||
|
|
cc610ad9b6 | ||
|
|
5a65f9a094 | ||
|
|
5ca1f0e365 | ||
|
|
a36baad0f0 | ||
|
|
ed115d3f72 | ||
|
|
d9619b7eed | ||
|
|
dbe5bf1d18 | ||
|
|
7903415fa4 | ||
|
|
3bb4d97e9e | ||
|
|
a41f7b7a56 |
25 changed files with 540 additions and 172 deletions
|
|
@ -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()
|
||||
|
|
|
|||
2
externals/Vulkan-Headers
vendored
2
externals/Vulkan-Headers
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 234c4b7370a8ea3239a214c9e871e4b17c89f4ab
|
||||
Subproject commit 0f0cfd88d7e6ece3ca6456df692f0055bde94be7
|
||||
2
externals/Vulkan-Utility-Libraries
vendored
2
externals/Vulkan-Utility-Libraries
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit fe7a09b13899c5c77d956fa310286f7a7eb2c4ed
|
||||
Subproject commit 50563f48368d75281bc2fb1c3407dc531ce28910
|
||||
2
externals/ffmpeg/ffmpeg
vendored
2
externals/ffmpeg/ffmpeg
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 9c1294eaddb88cb0e044c675ccae059a85fc9c6c
|
||||
Subproject commit 99e2af4e7837ca09b97d93a562dc12947179fc48
|
||||
2
externals/vcpkg
vendored
2
externals/vcpkg
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 37d46edf0f2024c3d04997a2d432d59278ca1dff
|
||||
Subproject commit cd1099f42a3c2ee28dc68e3db3f6f88658982736
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
31
src/android/app/src/main/jni/native_library.cpp
Normal file
31
src/android/app/src/main/jni/native_library.cpp
Normal file
|
|
@ -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"
|
||||
|
|
@ -23,6 +23,9 @@
|
|||
<string name="keys">Keys</string>
|
||||
<string name="keys_description">Select your <b>prod.keys</b> file with the button below.</string>
|
||||
<string name="select_keys">Select Keys</string>
|
||||
<string name="firmware_missing_title">Missing Firmware</string>
|
||||
<string name="firmware_missing_message">Firmware is required to launch games.\n\nPlease install firmware by placing your Switch firmware files in the appropriate location.</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="games">Games</string>
|
||||
<string name="games_description">Select your <b>Games</b> folder with the button below.</string>
|
||||
<string name="done">Done</string>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<br>Please follow <a href='https://citron-emu.org/help/quickstart/'>the "
|
||||
"citron quickstart guide</a> to redump your files.<br>You can refer "
|
||||
"to the citron wiki</a> or the citron Discord</a> for help.",
|
||||
tr("%1<br>This software is provided as-is without any warranty or support.<br>Please refer to community resources or documentation for assistance.",
|
||||
"%1 signifies an error string.")
|
||||
.arg(QString::fromStdString(
|
||||
GetResultStatusString(static_cast<Loader::ResultStatus>(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<u64>(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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -360,11 +360,6 @@
|
|||
<string>Open &Mods Page</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Open_Quickstart_Guide">
|
||||
<property name="text">
|
||||
<string>Open &Quickstart Guide</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Open_FAQ">
|
||||
<property name="text">
|
||||
<string>&FAQ</string>
|
||||
|
|
|
|||
|
|
@ -392,11 +392,11 @@ struct Values {
|
|||
Category::RendererAdvanced};
|
||||
SwitchableSetting<bool> 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<bool> renderer_force_max_clock{linkage, false, "force_max_clock",
|
||||
Category::RendererAdvanced};
|
||||
SwitchableSetting<bool> use_reactive_flushing{linkage,
|
||||
|
|
|
|||
|
|
@ -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 <cinttypes>
|
||||
|
|
@ -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<siginfo_t*>(raw_info);
|
||||
const u64 fault_addr = reinterpret_cast<u64>(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<u64>(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<u64>(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
|
||||
|
|
|
|||
|
|
@ -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 <mutex>
|
||||
#include <array>
|
||||
|
||||
#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<u8[]> m_stack{};
|
||||
|
||||
// Enhanced TLB implementation
|
||||
std::array<TlbEntry, TLB_SIZE> 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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
25
src/core/file_sys/content_manager.cpp
Normal file
25
src/core/file_sys/content_manager.cpp
Normal file
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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<AppLoader> 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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <string_view>
|
||||
|
|
@ -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<u32, 5> 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);
|
||||
|
|
|
|||
|
|
@ -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 <string_view>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 <bit>
|
||||
|
|
@ -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<u32, 5> 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);
|
||||
|
|
|
|||
|
|
@ -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<MemoryCommit> 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<MemoryAllocation>(this, std::move(memory), flags, size, type));
|
||||
std::make_unique<MemoryAllocation>(this, std::move(memory), flags, aligned_size, *type_opt));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue