Compare commits

..

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

25 changed files with 171 additions and 539 deletions

View file

@ -100,23 +100,19 @@ 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")
set(vvl_lib_path "${CMAKE_CURRENT_SOURCE_DIR}/src/android/app/src/main/jniLibs/arm64-v8a/")
set(vvl_final_lib "${vvl_lib_path}/libVkLayer_khronos_validation.so")
if (NOT EXISTS "${vvl_final_lib}")
if (NOT EXISTS "${vvl_zip_file}")
# 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}")
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}")
endif()
if (ANDROID)
@ -684,42 +680,3 @@ 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()

@ -1 +1 @@
Subproject commit 0f0cfd88d7e6ece3ca6456df692f0055bde94be7
Subproject commit 234c4b7370a8ea3239a214c9e871e4b17c89f4ab

@ -1 +1 @@
Subproject commit 50563f48368d75281bc2fb1c3407dc531ce28910
Subproject commit fe7a09b13899c5c77d956fa310286f7a7eb2c4ed

@ -1 +1 @@
Subproject commit 99e2af4e7837ca09b97d93a562dc12947179fc48
Subproject commit 9c1294eaddb88cb0e044c675ccae059a85fc9c6c

2
externals/vcpkg vendored

@ -1 +1 @@
Subproject commit cd1099f42a3c2ee28dc68e3db3f6f88658982736
Subproject commit 37d46edf0f2024c3d04997a2d432d59278ca1dff

View file

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-FileCopyrightText: 2023 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-35"
compileSdkVersion = "android-34"
ndkVersion = "26.1.10909125"
buildFeatures {
@ -57,8 +57,7 @@ android {
// TODO If this is ever modified, change application_id in strings.xml
applicationId = "org.citron.citron_emu"
minSdk = 30
//noinspection EditedTargetSdkVersion
targetSdk = 35
targetSdk = 34
versionName = getGitVersion()
versionCode = if (System.getenv("AUTO_VERSIONED") == "true") {
@ -76,6 +75,15 @@ 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) {
@ -108,11 +116,9 @@ android {
resValue("string", "app_name_suffixed", "Citron")
isDefault = true
isMinifyEnabled = true
isShrinkResources = true
isJniDebuggable = false
isDebuggable = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro"
)
}
@ -124,7 +130,7 @@ android {
signingConfig = signingConfigs.getByName("default")
isDebuggable = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro"
)
versionNameSuffix = "-relWithDebInfo"

View file

@ -4,7 +4,6 @@
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
@ -81,19 +80,6 @@ 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)

View file

@ -13,8 +13,6 @@ 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) {
@ -23,10 +21,7 @@ object LicenseVerifier {
val isEaBuild = currentPackage.endsWith(".ea")
// Check package name
if (!isDebugBuild && !isEaBuild &&
currentPackage != EXPECTED_PACKAGE &&
currentPackage != ALTERNATE_PACKAGE &&
currentPackage != ALTERNATE_PACKAGE_2) {
if (!isDebugBuild && !isEaBuild && currentPackage != EXPECTED_PACKAGE) {
showViolationDialog(activity)
return
}

View file

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

View file

@ -23,9 +23,6 @@
<string name="keys">Keys</string>
<string name="keys_description">Select your &lt;b>prod.keys&lt;/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 &lt;b>Games&lt;/b> folder with the button below.</string>
<string name="done">Done</string>

View file

@ -1,4 +1,5 @@
// 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.

View file

@ -1539,6 +1539,7 @@ 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);
@ -1843,7 +1844,9 @@ 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>This software is provided as-is without any warranty or support.<br>Please refer to community resources or documentation for assistance.",
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.",
"%1 signifies an error string.")
.arg(QString::fromStdString(
GetResultStatusString(static_cast<Loader::ResultStatus>(error_id))));
@ -3575,6 +3578,10 @@ 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/")));
}
@ -4780,24 +4787,19 @@ void GMainWindow::OnCheckFirmwareDecryption() {
}
bool GMainWindow::CheckFirmwarePresence() {
constexpr u64 MiiEditId = 0x0100000000001009; // Mii Edit applet ID
constexpr u64 QLaunchId = 0x0100000000001000; // Home Menu applet ID
constexpr u64 MiiEditId = static_cast<u64>(Service::AM::AppletProgramId::MiiEdit);
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);
auto qlaunch_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program);
if (!mii_applet_nca || !qlaunch_nca) {
if (!mii_applet_nca) {
return false;
}
// Also check for essential keys
return Core::Crypto::KeyManager::Instance().IsFirmwareAvailable();
return true;
}
void GMainWindow::SetFirmwareVersion() {

View file

@ -336,6 +336,7 @@ 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);

View file

@ -360,6 +360,11 @@
<string>Open &amp;Mods Page</string>
</property>
</action>
<action name="action_Open_Quickstart_Guide">
<property name="text">
<string>Open &amp;Quickstart Guide</string>
</property>
</action>
<action name="action_Open_FAQ">
<property name="text">
<string>&amp;FAQ</string>

View file

@ -392,11 +392,11 @@ struct Values {
Category::RendererAdvanced};
SwitchableSetting<bool> async_presentation{linkage,
#ifdef ANDROID
false, // Disabled due to crashes
true,
#else
false, // Disabled due to crashes
false,
#endif
"async_presentation", Category::RendererAdvanced}; // Hide from UI
"async_presentation", Category::RendererAdvanced};
SwitchableSetting<bool> renderer_force_max_clock{linkage, false, "force_max_clock",
Category::RendererAdvanced};
SwitchableSetting<bool> use_reactive_flushing{linkage,

View file

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cinttypes>
@ -144,55 +143,30 @@ bool ArmNce::HandleGuestAlignmentFault(GuestContext* guest_ctx, void* raw_info,
auto* fpctx = GetFloatingPointState(host_ctx);
auto& memory = guest_ctx->system->ApplicationMemory();
// Log the alignment fault for debugging
LOG_DEBUG(Core_ARM, "Alignment fault at PC={:X}", host_ctx.pc);
// Try to handle the instruction
// Match and execute an instruction.
auto next_pc = MatchAndExecuteOneInstruction(memory, &host_ctx, fpctx);
if (next_pc) {
host_ctx.pc = *next_pc;
return true;
}
// 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;
// We couldn't handle the access.
return HandleFailedGuestFault(guest_ctx, raw_info, raw_context);
}
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();
// 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);
}
// 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.
return true;
}
// 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);
}
// We couldn't handle the access.
return HandleFailedGuestFault(guest_ctx, raw_info, raw_context);
}
@ -403,81 +377,4 @@ 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

View file

@ -1,11 +1,9 @@
// 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"
@ -18,21 +16,6 @@ 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);
@ -107,24 +90,6 @@ 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

View file

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -649,13 +648,17 @@ 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);
}
@ -844,15 +847,87 @@ 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) {
// Function is now a no-op - keys are no longer written to autogenerated files
const auto citron_keys_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::KeysDir);
std::string filename = "title.keys_autogenerated";
if (category == KeyCategory::Standard) {
filename = dev_mode ? "dev.keys_autogenerated" : "prod.keys_autogenerated";
} else if (category == KeyCategory::Console) {
filename = "console.keys_autogenerated";
}
const auto path = citron_keys_dir / filename;
const auto add_info_text = !Common::FS::Exists(path);
Common::FS::IOFile file{path, Common::FS::FileAccessMode::Append,
Common::FS::FileType::TextFile};
if (!file.IsOpen()) {
return;
}
if (add_info_text) {
void(file.WriteString(
"# This file is autogenerated by Citron\n"
"# It serves to store keys that were automatically generated from the normal keys\n"
"# If you are experiencing issues involving keys, it may help to delete this file\n"));
}
void(file.WriteString(fmt::format("\n{} = {}", keyname, Common::HexToString(key))));
LoadFromFile(path, category == KeyCategory::Title);
}
void KeyManager::SetKey(S128KeyType id, Key128 key, u64 field1, u64 field2) {
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;
}
@ -860,8 +935,14 @@ void KeyManager::SetKey(S256KeyType id, Key256 key, u64 field1, u64 field2) {
if (s256_keys.find({id, field1, field2}) != s256_keys.end() || key == Key256{}) {
return;
}
// Store the key in memory but don't write to file
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);
}
s256_keys[{id, field1, field2}] = key;
}
@ -971,6 +1052,8 @@ 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;
@ -1100,6 +1183,7 @@ 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();
}
@ -1177,6 +1261,8 @@ 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(),
@ -1290,31 +1376,4 @@ 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

View file

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -296,9 +295,6 @@ public:
void ReloadKeys();
bool AreKeysLoaded() const;
// Check if firmware is installed by verifying essential keys
bool IsFirmwareAvailable() const;
private:
KeyManager();

View file

@ -1,25 +0,0 @@
#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();
}

View file

@ -19,10 +19,6 @@
#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 {
@ -254,20 +250,6 @@ 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());

View file

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string_view>
@ -8,7 +7,6 @@
#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 {
@ -405,8 +403,6 @@ 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;
@ -414,47 +410,7 @@ 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);

View file

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string_view>
@ -424,8 +423,6 @@ 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;
@ -433,46 +430,7 @@ 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);

View file

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <bit>
@ -554,42 +553,6 @@ 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);

View file

@ -140,10 +140,6 @@ 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;
@ -288,78 +284,44 @@ 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)) {
return TryCommit(requirements, flags).value();
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);
}
// 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);
// 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();
}
bool MemoryAllocator::TryAllocMemory(VkMemoryPropertyFlags flags, u32 type_mask, u64 size) {
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)
const u32 type = FindType(flags, type_mask).value();
vk::DeviceMemory memory = device.GetLogical().TryAllocateMemory({
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
.pNext = nullptr,
.allocationSize = aligned_size,
.memoryTypeIndex = *type_opt,
/* 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,
});
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, aligned_size, *type_opt));
std::make_unique<MemoryAllocation>(this, std::move(memory), flags, size, type));
return true;
}