mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-10-28 18:34:19 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74d2ca0a66 | |||
| 465b95f353 | |||
| 44cd950747 | |||
| 7cef75b23d | |||
| 26a03d2473 | |||
| b7c1ded159 | |||
| cbd4cf2d8c | |||
| a3debcf731 | |||
| 5239cbe2a7 | |||
| 0e11fe8b5e | |||
| 0689f73147 | |||
| a95efaf18f | |||
| 58968bd127 |
+2
-2
@@ -351,13 +351,13 @@
|
||||
},
|
||||
"zstd": {
|
||||
"find_args": "MODULE",
|
||||
"hash": "cc5ad4b119a9c2ea57f0b71eeff01113bb506e0d17000159c5409cb8236d22e38c52d5e9e97e7947a4bf1b2dfc44b6c503ab2d9aedbd59458435c6a2849cb029",
|
||||
"hash": "2b006c3c110c38bd2f5896b42d36a38931c8eae261f30552f9a9fd04458d218d8f1a1425f117fd12af545ed77042e4b2d1a1a9b6286c4d0b9d74ef79f20a8f08",
|
||||
"min_version": "1.5",
|
||||
"options": [
|
||||
"ZSTD_BUILD_SHARED OFF"
|
||||
],
|
||||
"repo": "facebook/zstd",
|
||||
"source_subdir": "build/cmake",
|
||||
"version": "b8d6101fba"
|
||||
"version": "33618c8"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+318
-308
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+342
-337
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
-1
@@ -16,7 +16,6 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
||||
RENDERER_USE_SPEED_LIMIT("use_speed_limit"),
|
||||
USE_CUSTOM_CPU_TICKS("use_custom_cpu_ticks"),
|
||||
SKIP_CPU_INNER_INVALIDATION("skip_cpu_inner_invalidation"),
|
||||
NCE_INVALIDATION_GPU_READBACK("nce_invalidation_gpu_readback"),
|
||||
FIX_BLOOM_EFFECTS("fix_bloom_effects"),
|
||||
EMULATE_BGR565("emulate_bgr565"),
|
||||
RESCALE_HACK("rescale_hack"),
|
||||
|
||||
-7
@@ -859,13 +859,6 @@ abstract class SettingsItem(
|
||||
descriptionId = R.string.skip_cpu_inner_invalidation_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SwitchSetting(
|
||||
BooleanSetting.NCE_INVALIDATION_GPU_READBACK,
|
||||
titleId = R.string.nce_invalidation_gpu_readback,
|
||||
descriptionId = R.string.nce_invalidation_gpu_readback_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SwitchSetting(
|
||||
BooleanSetting.FIX_BLOOM_EFFECTS,
|
||||
|
||||
-1
@@ -554,7 +554,6 @@ class SettingsFragmentPresenter(
|
||||
add(HeaderSetting(R.string.hacks))
|
||||
|
||||
add(BooleanSetting.SKIP_CPU_INNER_INVALIDATION.key)
|
||||
add(BooleanSetting.NCE_INVALIDATION_GPU_READBACK.key)
|
||||
add(BooleanSetting.FIX_BLOOM_EFFECTS.key)
|
||||
add(BooleanSetting.EMULATE_BGR565.key)
|
||||
add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
|
||||
|
||||
@@ -100,7 +100,7 @@ class Game(
|
||||
|
||||
companion object {
|
||||
val extensions: Set<String> = HashSet(
|
||||
listOf("xci", "nsp", "nca", "nro")
|
||||
listOf("xci", "nsp", "nca", "nro", "nsz", "xcz")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ object GameHelper {
|
||||
|
||||
// File extensions considered as external content, buuut should
|
||||
// be done better imo.
|
||||
private val externalContentExtensions = setOf("nsp", "xci")
|
||||
private val externalContentExtensions = setOf("nsp", "xci", "nsz", "xcz")
|
||||
|
||||
private fun scanContentContainersRecursive(
|
||||
files: Array<MinimalDocumentFile>,
|
||||
|
||||
@@ -76,7 +76,9 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobj
|
||||
auto const file_type = loader->GetFileType();
|
||||
if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error)
|
||||
return false;
|
||||
if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI) && !Loader::IsBootableGameContainer(file, file_type))
|
||||
if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI ||
|
||||
file_type == Loader::FileType::NSZ || file_type == Loader::FileType::XCZ) &&
|
||||
!Loader::IsBootableGameContainer(file, file_type))
|
||||
return false;
|
||||
u64 program_id = 0;
|
||||
return loader->ReadProgramId(program_id) == Loader::ResultStatus::Success;
|
||||
|
||||
@@ -351,7 +351,7 @@
|
||||
<string name="frame_generation_unsupported">غير مدعوم (لا يتوفر نموذج ذاكرة Vulkan أو float16)</string>
|
||||
<string name="lossless_scaling">Lossless Scaling</string>
|
||||
<string name="lossless_scaling_description">قم بتوفير نسختك الخاصة من ملف Lossless.dll لتفعيل توليد الإطارات</string>
|
||||
<string name="lossless_scaling_installed">تم التثبيت</string>
|
||||
<string name="lossless_scaling_installed">مثبت</string>
|
||||
<string name="lossless_scaling_not_installed">غير مثبت</string>
|
||||
<string name="lossless_scaling_replace">استبدل</string>
|
||||
<string name="lossless_scaling_remove">إزالة</string>
|
||||
@@ -501,9 +501,9 @@
|
||||
<string name="frame_limit_enable_description">يحدد سرعة المحاكاة بنسبة مئوية محددة من السرعة العادية.</string>
|
||||
<string name="frame_limit_slider">نسبة الحد الأقصى للسرعة</string>
|
||||
<string name="frame_limit_slider_description">يحدد النسبة المئوية للحد من سرعة المحاكاة. %100 هي السرعة الطبيعية. القيم الأعلى أو الأقل ستزيد أو تقلل من الحد الأقصى للسرعة.</string>
|
||||
<string name="turbo_speed_limit">السرعة الفائقة</string>
|
||||
<string name="turbo_speed_limit">سرعة تيربو</string>
|
||||
<string name="turbo_speed_limit_description">عند تفعيل وضع التيربو، سيتم تشغيل المحاكاة بهذه السرعة.</string>
|
||||
<string name="slow_speed_limit">السرعة البطيئة</string>
|
||||
<string name="slow_speed_limit">سرعة بطيئة</string>
|
||||
<string name="slow_speed_limit_description">عند تفعيل الوضع البطيء، سيتم تشغيل المحاكاة بهذه السرعة.</string>
|
||||
<string name="cpu_backend">خلفية وحدة المعالجة المركزية</string>
|
||||
<string name="cpu_accuracy">دقة وحدة المعالجة المركزية</string>
|
||||
@@ -514,7 +514,7 @@
|
||||
|
||||
<!-- System settings strings -->
|
||||
<string name="device_name">اسم الجهاز</string>
|
||||
<string name="use_docked_mode">وضع المنصة</string>
|
||||
<string name="use_docked_mode">وضع الإرساء</string>
|
||||
<string name="use_docked_mode_description">يزيد الدقة ويقلل الأداء. يتم استخدام الوضع المحمول عند تعطيله، مما يقلل الدقة ويزيد الأداء.</string>
|
||||
<string name="emulated_region">المنطقة التي تمت محاكاتها</string>
|
||||
<string name="emulated_language">لغة المحاكاة</string>
|
||||
@@ -543,7 +543,7 @@
|
||||
<string name="network">الشبكة</string>
|
||||
|
||||
<!-- Graphics settings strings -->
|
||||
<string name="renderer_resolution">الدقة (المنصة/محمول)</string>
|
||||
<string name="renderer_resolution">الدقة (الإرساء/محمول)</string>
|
||||
<string name="renderer_vsync">VSync وضع</string>
|
||||
<string name="renderer_scaling_filter">مرشح ملائم للنافذة</string>
|
||||
<string name="fsr_sharpness">حدة FSR/SGSR</string>
|
||||
@@ -587,8 +587,6 @@
|
||||
<string name="fast_gpu_time_description">يجعل اللعبة تعتقد أن عمل وحدة معالجة الرسومات ينتهي بشكل أسرع مما هو عليه في الواقع، لذلك تتوقف عن خفض الدقة ومسافة العرض لتتناسب مع سرعات جهاز Switch.</string>
|
||||
<string name="skip_cpu_inner_invalidation">تخطي إبطال صلاحية وحدة المعالجة المركزية الداخلية</string>
|
||||
<string name="skip_cpu_inner_invalidation_description">يتخطى بعض عمليات إبطال ذاكرة التخزين المؤقتة من جانب وحدة المعالجة المركزية أثناء تحديثات الذاكرة، مما يقلل من استخدام وحدة المعالجة المركزية ويحسن أداءها. قد يتسبب ذلك في حدوث أعطال أو تعطل في بعض الألعاب.</string>
|
||||
<string name="nce_invalidation_gpu_readback">إعادة قراءة وحدة معالجة الرسومات قبل إبطال صلاحية NCE</string>
|
||||
<string name="nce_invalidation_gpu_readback_description">إصلاح مؤقت. يعالج مشكلة تعطل لعبة Ender Magnolia (وغيرها من ألعاب UE). قد يؤدي إلى انخفاض في الأداء. يجري العمل على تحسينه.</string>
|
||||
<string name="fix_bloom_effects">إصلاح تأثيرات التوهج</string>
|
||||
<string name="fix_bloom_effects_description">يقلل من ضبابية التوهج في LA/EOW (Adreno A6XX - A7XX/ Turnip)، ويزيل التوهج في Burnout. تحذير: قد يسبب تشوهات رسومية في ألعاب أخرى.</string>
|
||||
<string name="emulate_bgr565">محاكاة BGR565</string>
|
||||
@@ -696,7 +694,7 @@
|
||||
<string name="control_stick">عصا التحكم</string>
|
||||
<string name="right_stick">العصا اليمنى</string>
|
||||
<string name="c_stick">عصا-C</string>
|
||||
<string name="pressed">ضغط العصا</string>
|
||||
<string name="pressed">مضغوط</string>
|
||||
<string name="range">النطاق</string>
|
||||
<string name="deadzone">المنطقة الميتة</string>
|
||||
<string name="modifier">معدل</string>
|
||||
@@ -823,7 +821,7 @@
|
||||
<!-- Preferences Screen -->
|
||||
<string name="preferences_settings">الإعدادات</string>
|
||||
<string name="preferences_system">النظام</string>
|
||||
<string name="preferences_system_description">وضع المنصة، المنطقة، اللغة</string>
|
||||
<string name="preferences_system_description">وضع الإرساء، المنطقة، اللغة</string>
|
||||
<string name="preferences_graphics">الرسومات</string>
|
||||
<string name="preferences_graphics_description">مستوى دقة المحاكاة، الدقة، ذاكرة التخزين المؤقتة للتظليل</string>
|
||||
<string name="quick_settings">الإعدادات السريعة</string>
|
||||
@@ -871,12 +869,12 @@
|
||||
<string name="copy_details">نسخ التفاصيل</string>
|
||||
<string name="add_ons">الإضافات</string>
|
||||
<string name="add_ons_description">تفعيل/تعطيل التعديلات، التحديثات، المحتوى القابل للتنزيل</string>
|
||||
<string name="playtime">وقت اللعب:</string>
|
||||
<string name="reset_playtime">مسح وقت اللعب</string>
|
||||
<string name="reset_playtime_description">إعادة تعيين وقت لعب اللعبة الحالية إلى 0 ثانية</string>
|
||||
<string name="reset_playtime_warning_description">سيؤدي هذا إلى مسح بيانات وقت اللعب للعبة الحالية. هل أنت متأكد؟</string>
|
||||
<string name="playtime_reset_successfully">تمت إعادة تعيين وقت اللعب</string>
|
||||
<string name="edit_playtime">تعديل وقت اللعب</string>
|
||||
<string name="playtime">زمن اللعب:</string>
|
||||
<string name="reset_playtime">مسح زمن اللعب</string>
|
||||
<string name="reset_playtime_description">إعادة تعيين زمن اللعب للعبة الحالية إلى 0 ثانية</string>
|
||||
<string name="reset_playtime_warning_description">سيؤدي هذا إلى مسح بيانات زمن اللعب الحالية للعبة. هل أنت متأكد؟</string>
|
||||
<string name="playtime_reset_successfully">تمت إعادة تعيين زمن اللعب</string>
|
||||
<string name="edit_playtime">تعديل زمن اللعب</string>
|
||||
<string name="hours">ساعات</string>
|
||||
<string name="minutes">دقائق</string>
|
||||
<string name="hours_abbr">س</string>
|
||||
@@ -885,7 +883,7 @@
|
||||
<string name="hours_must_be_between_0_and_9999">يجب أن تكون الساعات بين 0 و 9999</string>
|
||||
<string name="minutes_must_be_between_0_and_59">يجب أن تكون الدقائق بين 0 و 59</string>
|
||||
<string name="seconds_must_be_between_0_and_59">يجب أن تكون الثواني بين 0 و 59</string>
|
||||
<string name="playtime_updated_successfully">تم تحديث وقت اللعب بنجاح</string>
|
||||
<string name="playtime_updated_successfully">تم تحديث زمن اللعب بنجاح</string>
|
||||
<string name="clear_shader_cache">مسح ذاكرة التخزين المؤقتة للتظليل</string>
|
||||
<string name="clear_shader_cache_description">يزيل جميع التظليلات التي تم إنشاؤها أثناء لعب هذه اللعبة</string>
|
||||
<string name="clear_shader_cache_warning_description">ستواجه المزيد من التقطع مع إعادة إنشاء ذاكرة التخزين المؤقتة للتظلل</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<string name="input_overlay_options">虛擬按鍵設定</string>
|
||||
<string name="input_overlay_options_description">調整虛擬按鍵的位置與顯示設定</string>
|
||||
<string name="edit_overlay_layout">調整虛擬按鍵</string>
|
||||
<string name="edit_overlay_layout">編輯虛擬按鍵</string>
|
||||
<string name="edit_overlay_layout_description">調整虛擬按鍵的位置和大小</string>
|
||||
|
||||
|
||||
@@ -253,8 +253,8 @@
|
||||
<string name="install_firmware_warning">跳過安裝韌體?</string>
|
||||
<string name="emulator_data">設定模擬器資料</string>
|
||||
<string name="emulator_data_description">模擬器需要金鑰才能正常執行,同時建議安裝韌體以啟動 QLaunch 小程式</string>
|
||||
<string name="permissions">授予通知權限</string>
|
||||
<string name="permissions_description">授予 Eden 傳送通知的權限以使用模擬器的特定功能</string>
|
||||
<string name="permissions">授予權限</string>
|
||||
<string name="permissions_description">授予權限以使用模擬器的特定功能</string>
|
||||
<string name="install_firmware_warning_description">許多遊戲需要韌體才能正常運作</string>
|
||||
<string name="install_firmware_warning_help">https://yuzu-mirror.github.io/help/quickstart/#guide-introduction</string>
|
||||
<string name="notifications">通知</string>
|
||||
@@ -268,8 +268,8 @@
|
||||
<string name="device_manufacturer">製造商</string>
|
||||
<string name="device_model">型號</string>
|
||||
<string name="product">型號代碼</string>
|
||||
<string name="android_version">Android 版本</string>
|
||||
<string name="android_security_patch">Android 安全性修補程式等級</string>
|
||||
<string name="android_version">安卓版本</string>
|
||||
<string name="android_security_patch">安卓安全性修補程式等級</string>
|
||||
<string name="build_id">版本號碼</string>
|
||||
<string name="general_information">基本資訊</string>
|
||||
<string name="hardware">硬體</string>
|
||||
@@ -287,7 +287,7 @@
|
||||
<string name="warning_skip">跳過</string>
|
||||
<string name="warning_cancel">取消</string>
|
||||
<string name="install_amiibo_keys">安裝 Amiibo 金鑰</string>
|
||||
<string name="install_amiibo_keys_description">安裝 Amiibo 以在日後遊玩時使用</string>
|
||||
<string name="install_amiibo_keys_description">需要先安裝才能在遊戲中使用 Amiibo</string>
|
||||
<string name="gpu_driver_fetcher">GPU 驅動程式下載器</string>
|
||||
<string name="gpu_driver_manager">GPU 驅動程式管理員</string>
|
||||
<string name="install_gpu_driver_description">安裝替代驅動程式以取得潛在的更佳效能或準確度</string>
|
||||
@@ -417,17 +417,17 @@
|
||||
<string name="custom_driver_not_supported">您的裝置不支援自訂驅動程式</string>
|
||||
<string name="custom_driver_not_supported_description">此裝置不支援注入自訂驅動程式\n請以後再來查看是否已新增支援!</string>
|
||||
<string name="manage_yuzu_data">管理 Eden 資料</string>
|
||||
<string name="manage_yuzu_data_description">安裝韌體、金鑰,匯入/匯出使用者資料及安裝其它項目!</string>
|
||||
<string name="manage_yuzu_data_description">安裝韌體、金鑰,導入/導出使用者資料及安裝其它項目!</string>
|
||||
<string name="game_folders">遊戲資料夾</string>
|
||||
<string name="deep_scan">深度掃描</string>
|
||||
<string name="add_game_folder">新增遊戲資料夾</string>
|
||||
<string name="folder_already_added">這個資料夾已經新增過了!</string>
|
||||
<string name="game_folder_properties">遊戲資料夾設定</string>
|
||||
<plurals name="saves_import_failed">
|
||||
<item quantity="other">%d 個存檔匯入失敗</item>
|
||||
<item quantity="other">%d 個存檔導入失敗</item>
|
||||
</plurals>
|
||||
<plurals name="saves_import_success">
|
||||
<item quantity="other">成功匯入 %d 個存檔</item>
|
||||
<item quantity="other">成功導入 %d 個存檔</item>
|
||||
</plurals>
|
||||
<string name="no_save_data_found">找不到儲存資料</string>
|
||||
<string name="verify_installed_content">驗證已安裝內容的完整性</string>
|
||||
@@ -577,10 +577,8 @@
|
||||
<string name="fast_gpu_time_description">讓遊戲誤以為 GPU 工作能更快完成,避免遊戲為了配合 Switch 的時脈而降低解析度與渲染距離</string>
|
||||
<string name="skip_cpu_inner_invalidation">跳過 CPU 內部失效處理</string>
|
||||
<string name="skip_cpu_inner_invalidation_description">在記憶體更新期間跳過某些 CPU 端快取的失效處理,減少 CPU 使用率並提高其性能。可能會導致某些遊戲出現故障或當機</string>
|
||||
<string name="nce_invalidation_gpu_readback">在 NCE 無效化前進行 GPU 回讀</string>
|
||||
<string name="nce_invalidation_gpu_readback_description">(暫時性功能)修復《終結者瑪格諾莉亞:霧中綻放》及其它使用虛幻引擎之遊戲中的當機問題,可能會降低效能。此功能正在改善中</string>
|
||||
<string name="fix_bloom_effects">修正光暈特效</string>
|
||||
<string name="fix_bloom_effects_description">在《薩爾達傳說:織夢島》和《薩爾達傳說:智慧的再現》中減少光暈模糊的問題並移除《橫衝直撞:狂飆樂園重製版》中的光暈效果(適用於 Adreno A6XX - A7XX/ Turnip)。警告:可能會在其它遊戲中導致圖形異常</string>
|
||||
<string name="fix_bloom_effects_description">在 \"薩爾達傳說:織夢島\" 和 \"薩爾達傳說:智慧的再現\" 中減少光暈模糊的問題並移除 \"橫衝直撞:狂飆樂園重製版\" 中的光暈效果(適用於 Adreno A6XX - A7XX/ Turnip)。警告:可能會在其它遊戲中導致圖形異常</string>
|
||||
<string name="emulate_bgr565">模擬 BGR565</string>
|
||||
<string name="emulate_bgr565_description">修正遊戲中出現的色彩反轉、圖形異常或陰影異常</string>
|
||||
<string name="rescale_hack">啟用舊版重新縮放處理</string>
|
||||
@@ -603,7 +601,7 @@
|
||||
<string name="extensions">擴充功能</string>
|
||||
|
||||
<string name="dyna_state">擴展動態狀態</string>
|
||||
<string name="dyna_state_description">控制 Vulkan 擴展動態狀態中可使用的功能數量,較高的數值可根據驅動程式支援的動態狀態減少管線編譯的次數</string>
|
||||
<string name="dyna_state_description">控制擴展動態狀態中可使用的功能數量,較高的數值可根據驅動程式支援的動態狀態減少管線編譯的次數</string>
|
||||
<string name="disabled">停用</string>
|
||||
<string name="vertex_input_dynamic_state">頂點輸入動態狀態</string>
|
||||
<string name="vertex_input_dynamic_state_description">啟用此功能可提供更彈性的頂點輸入處理方式,並可能減少頂點/緩衝區的管線編譯時間</string>
|
||||
@@ -808,7 +806,7 @@
|
||||
<string name="recommended_driver">推薦驅動程式:</string>
|
||||
<string name="gpu_model">GPU 型號</string>
|
||||
<string name="unsupported_gpu">不支援的 GPU</string>
|
||||
<string name="unsupported_gpu_warning">您的 GPU 不支援驅動程式注入,不建議嘗試安裝自訂驅動程式</string>
|
||||
<string name="unsupported_gpu_warning">您的 GPU 不支援驅動程式注入,不建議嘗試設定自訂驅動程式</string>
|
||||
|
||||
<!-- Preferences Screen -->
|
||||
<string name="preferences_settings">設定</string>
|
||||
@@ -833,9 +831,9 @@
|
||||
<string name="custom_save_directory">遊戲儲存資料路徑</string>
|
||||
<string name="custom_save_directory_description">設定遊戲儲存資料的自訂路徑</string>
|
||||
<string name="reset_to_nand">重設為預設值</string>
|
||||
<string name="migrate_save_data">移動儲存資料</string>
|
||||
<string name="migrate_save_data">遷移儲存資料</string>
|
||||
<string name="migrate_save_data_question">您要將現有的儲存資料移至新位置嗎?</string>
|
||||
<string name="save_migration_complete">儲存資料遷移成功</string>
|
||||
<string name="save_migration_complete">成功遷移儲存資料</string>
|
||||
<string name="save_migration_failed">儲存資料遷移失敗</string>
|
||||
<string name="save_directory_set">存檔目錄設定完成</string>
|
||||
<string name="destination_has_saves">所選取的位置已有儲存資料,是否要取代該檔案?</string>
|
||||
@@ -892,7 +890,7 @@
|
||||
<string name="select_content_type">內容類型</string>
|
||||
<string name="updates_and_dlc">更新及 DLC</string>
|
||||
<string name="mods_and_cheats">模組及金手指</string>
|
||||
<string name="addon_notice">注意!</string>
|
||||
<string name="addon_notice">重要的˙附加元件通知</string>
|
||||
<!-- \"cheats/" "romfs/" and \"exefs/ should not be translated -->
|
||||
<string name="addon_notice_description">若要安裝模組及金手指,您必須選擇一個包含 cheats/、romfs/ 或 exefs/ 的目錄。我們無法驗證這些內容是否與您的遊戲相容,所以請謹慎安裝!</string>
|
||||
<string name="invalid_directory">無效的目錄</string>
|
||||
@@ -981,7 +979,7 @@
|
||||
<string name="emulation_pause">暫停模擬</string>
|
||||
<string name="emulation_unpause">繼續模擬</string>
|
||||
<string name="emulation_input_overlay">疊加層/虛擬按鍵設定</string>
|
||||
<string name="load_amiibo">匯入 Amiibo</string>
|
||||
<string name="load_amiibo">導入 Amiibo</string>
|
||||
<string name="touchscreen">觸控螢幕</string>
|
||||
<string name="lock_drawer">鎖定側邊選單</string>
|
||||
<string name="unlock_drawer">解鎖側邊選單</string>
|
||||
@@ -1128,16 +1126,6 @@
|
||||
<string name="freedreno_clear_all">全部清除</string>
|
||||
<string name="freedreno_saved">Freedreno 設定已儲存</string>
|
||||
<string name="freedreno_cleared_all">已清除所有 Freedreno 變數</string>
|
||||
<string name="freedreno_variable_added">變數 %1$s 已新增</string>
|
||||
<string name="freedreno_preset_applied">設定檔 \"%1$s\" 已套用</string>
|
||||
<string name="freedreno_error_empty_name">變數名稱不可空白</string>
|
||||
<string name="freedreno_error_setting_variable">變數設定失敗</string>
|
||||
<string name="freedreno_info_title">關於 Freedreno 設定</string>
|
||||
<string name="freedreno_info_description">透過調整 Freedreno/Turnip GPU 驅動程式的設定來偵錯、分析和最佳化效能,會自動儲存變更。詳情請參閱 https://docs.mesa3d.org/drivers/freedreno.html</string>
|
||||
<string name="freedreno_per_game_title">Freedreno 設定</string>
|
||||
<string name="freedreno_per_game_description">調整此遊戲的 GPU 驅動程式設定</string>
|
||||
<string name="freedreno_per_game_saved">Freedreno 設定已儲存</string>
|
||||
|
||||
<!-- Gamepad Buttons -->
|
||||
<string name="gamepad_d_pad">方向鍵</string>
|
||||
<string name="gamepad_left_stick">左搖桿</string>
|
||||
@@ -1146,17 +1134,14 @@
|
||||
<string name="gamepad_screenshot">螢幕截圖</string>
|
||||
|
||||
<!-- Disk shader cache -->
|
||||
<string name="building_shaders">正在編譯著色器</string>
|
||||
<string name="building_shaders">正在建置著色器</string>
|
||||
|
||||
<!-- App options -->
|
||||
<string name="change_app_theme">變更應用程式主題</string>
|
||||
<string name="theme_default">預設</string>
|
||||
<string name="theme_material_you">Material You</string>
|
||||
<string name="app_settings">Eden 介面設定</string>
|
||||
<string name="theme_material_you">質感設計3</string>
|
||||
<string name="app_settings">Eden介面設定</string>
|
||||
<string name="theme_and_color">主題和顏色</string>
|
||||
<string name="fullscreen_mode">全螢幕模式</string>
|
||||
<string name="fullscreen_mode_description">在使用 Eden 時隱藏 Android 系統的導覽列,在直向時從螢幕底部往上滑以暫時顯示它們;橫向時則從螢幕側邊往內滑動</string>
|
||||
|
||||
<!-- Theme Modes -->
|
||||
<string name="change_theme_mode">變更主題模式</string>
|
||||
<string name="theme_mode_follow_system">跟隨系統</string>
|
||||
@@ -1165,26 +1150,18 @@
|
||||
|
||||
<!-- Black backgrounds theme -->
|
||||
<string name="use_black_backgrounds">黑色背景</string>
|
||||
<string name="use_black_backgrounds_description">使用深色主題時套用黑色背景</string>
|
||||
|
||||
<!-- Buttons -->
|
||||
<string name="enable_folder_button">遊戲資料夾</string>
|
||||
<string name="enable_folder_button_description">在 Eden 首頁顯示新增遊戲資料夾的按鈕</string>
|
||||
<string name="enable_qlaunch_button">QLaunch</string>
|
||||
<string name="enable_qlaunch_button_description">在 Eden 首頁顯示 QLaunch 按鈕</string>
|
||||
<string name="use_black_backgrounds_description">使用深色主題時,套用黑色背景。</string>
|
||||
|
||||
<!-- App Language -->
|
||||
<string name="app_language">語言</string>
|
||||
<string name="app_language_description">選擇 Eden 顯示的語言</string>
|
||||
<string name="app_language_description">選擇Eden顯示的語言</string>
|
||||
<string name="app_language_system">跟隨系統</string>
|
||||
<!-- Static Themes -->
|
||||
<string name="static_theme_color">主題色彩</string>
|
||||
<string name="eden_theme">Eden</string>
|
||||
<string name="violet">紫色</string>
|
||||
<string name="blue">藍色</string>
|
||||
<string name="cyan">青色</string>
|
||||
<string name="red">紅色</string>
|
||||
<string name="green">綠色</string>
|
||||
<string name="yellow">黃色</string>
|
||||
<string name="orange">橘色</string>
|
||||
<string name="pink">粉紅色</string>
|
||||
@@ -1206,28 +1183,14 @@
|
||||
<string name="applets_menu_description">更改小程式前端和設定</string>
|
||||
|
||||
<string name="applet_hle">自訂前端</string>
|
||||
<string name="applet_lle">Switch 小程式</string>
|
||||
<string name="applet_lle">Switch小程式</string>
|
||||
|
||||
<string name="swkbd_applet">軟體鍵盤</string>
|
||||
|
||||
<string name="airplane_mode">飛航模式</string>
|
||||
<string name="airplane_mode_description">將飛航模式傳遞給 Switch 作業系統</string>
|
||||
<string name="airplane_mode_description">將飛航模式傳遞給Switch作業系統</string>
|
||||
|
||||
<string name="enable_overlay">啟用小程式虛擬按鍵</string>
|
||||
<string name="enable_overlay_description">啟用 Horizon OS 內建的小程式虛擬按鍵,長按 Home 鍵 1 秒以顯示虛擬按鍵</string>
|
||||
|
||||
<!-- Profile Management -->
|
||||
<string name="profile_manager">使用者管理</string>
|
||||
<string name="profile_manager_description">更改使用者名稱、圖片及新增或刪除使用者</string>
|
||||
<string name="profile_add_user">新增使用者</string>
|
||||
<string name="profile_new_user">新增使用者</string>
|
||||
<string name="profile_edit_user">編輯使用者資料</string>
|
||||
<string name="profile_edit">編輯</string>
|
||||
<string name="profile_delete">刪除</string>
|
||||
<string name="profile_username">使用者名稱</string>
|
||||
<string name="profile_uuid">使用者 ID (通用唯一辨識碼)</string>
|
||||
<string name="profile_uuid_description">這是此使用者設定檔的唯一識別碼,建立後就無法進行更改</string>
|
||||
<!-- Licenses screen strings -->
|
||||
<string name="licenses">授權</string>
|
||||
<string name="license_fidelityfx_fsr_description">來自 AMD 的升級圖像品質</string>
|
||||
</resources>
|
||||
</resources>
|
||||
@@ -593,8 +593,6 @@
|
||||
<string name="fast_gpu_time_description">Makes the game believe GPU work finishes faster than it does, so it stops lowering resolution and render distance to fit the Switch\'s clocks.</string>
|
||||
<string name="skip_cpu_inner_invalidation">Skip CPU Inner Invalidation</string>
|
||||
<string name="skip_cpu_inner_invalidation_description">Skips certain CPU-side cache invalidations during memory updates, reducing CPU usage and improving it\'s performance. This may cause glitches or crashes on some games.</string>
|
||||
<string name="nce_invalidation_gpu_readback">GPU Readback Before NCE Invalidation</string>
|
||||
<string name="nce_invalidation_gpu_readback_description">Temporary. Fix Ender Magnolia (and other UE titles) crashes. May reduce performance. Improvement in progress.</string>
|
||||
<string name="fix_bloom_effects">Fix Bloom Effects</string>
|
||||
<string name="fix_bloom_effects_description">Reduces bloom blur in LA/EOW (Adreno A6XX - A7XX/ Turnip), removes bloom in Burnout. Warning: may cause graphical artifacts in other games.</string>
|
||||
<string name="emulate_bgr565">Emulate BGR565</string>
|
||||
|
||||
@@ -639,8 +639,6 @@ struct Values {
|
||||
Specialization::Default,
|
||||
true,
|
||||
true};
|
||||
SwitchableSetting<bool> nce_invalidation_gpu_readback{
|
||||
linkage, false, "nce_invalidation_gpu_readback", Category::RendererHacks};
|
||||
SwitchableSetting<bool> async_presentation{linkage,
|
||||
#ifdef __ANDROID__
|
||||
true,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -46,4 +49,10 @@ std::vector<u8> DecompressDataZSTD(std::span<const u8> compressed) {
|
||||
return decompressed;
|
||||
}
|
||||
|
||||
std::size_t DecompressDataZSTD(std::span<const u8> compressed, std::span<u8> output) {
|
||||
const std::size_t result =
|
||||
ZSTD_decompress(output.data(), output.size(), compressed.data(), compressed.size());
|
||||
return ZSTD_isError(result) ? 0 : result;
|
||||
}
|
||||
|
||||
} // namespace Common::Compression
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -42,4 +45,15 @@ namespace Common::Compression {
|
||||
*/
|
||||
[[nodiscard]] std::vector<u8> DecompressDataZSTD(std::span<const u8> compressed);
|
||||
|
||||
/**
|
||||
* Decompresses a single Zstandard frame into a caller-provided buffer of known size.
|
||||
*
|
||||
* @param compressed the compressed source memory region.
|
||||
* @param output the destination buffer. Its size is the maximum number of bytes to write.
|
||||
*
|
||||
* @return the number of bytes written, or 0 if the frame is invalid or doesn't fit. An empty
|
||||
* frame also returns 0.
|
||||
*/
|
||||
[[nodiscard]] std::size_t DecompressDataZSTD(std::span<const u8> compressed, std::span<u8> output);
|
||||
|
||||
} // namespace Common::Compression
|
||||
|
||||
@@ -33,6 +33,8 @@ add_library(core STATIC
|
||||
crypto/encryption_layer.h
|
||||
crypto/key_manager.cpp
|
||||
crypto/key_manager.h
|
||||
crypto/ncz_decompressor.cpp
|
||||
crypto/ncz_decompressor.h
|
||||
crypto/partition_data_manager.cpp
|
||||
crypto/partition_data_manager.h
|
||||
crypto/xts_encryption_layer.cpp
|
||||
|
||||
@@ -369,8 +369,8 @@ void ArmNce::SetContext(const Kernel::Svc::ThreadContext& ctx) {
|
||||
}
|
||||
|
||||
void ArmNce::SignalInterrupt(Kernel::KThread* thread) {
|
||||
// Add break loop condition.
|
||||
m_guest_ctx.esr_el1.fetch_or(static_cast<u64>(HaltReason::BreakLoop));
|
||||
constexpr u64 break_loop = static_cast<u64>(HaltReason::BreakLoop);
|
||||
if ((m_guest_ctx.esr_el1.fetch_or(break_loop) & break_loop) != 0) return;
|
||||
|
||||
auto* params = &thread->GetNativeExecutionParameters();
|
||||
LockThreadParameters(params);
|
||||
|
||||
+6
-34
@@ -4,7 +4,6 @@
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
#include "game_settings.h"
|
||||
@@ -249,30 +248,12 @@ struct System::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
void NotifyNVDECChannelOpen(u64 process_id) {
|
||||
std::scoped_lock lock{nvdec_active_mutex};
|
||||
++nvdec_active_channels[process_id];
|
||||
}
|
||||
|
||||
void NotifyNVDECChannelClose(u64 process_id) {
|
||||
std::scoped_lock lock{nvdec_active_mutex};
|
||||
const auto it = nvdec_active_channels.find(process_id);
|
||||
if (it == nvdec_active_channels.end()) {
|
||||
return;
|
||||
}
|
||||
if (--it->second == 0) {
|
||||
nvdec_active_channels.erase(it);
|
||||
}
|
||||
void SetNVDECActive(bool is_nvdec_active) {
|
||||
nvdec_active = is_nvdec_active;
|
||||
}
|
||||
|
||||
bool GetNVDECActive() {
|
||||
std::scoped_lock lock{nvdec_active_mutex};
|
||||
return !nvdec_active_channels.empty();
|
||||
}
|
||||
|
||||
bool IsNVDECActiveForProcess(u64 process_id) {
|
||||
std::scoped_lock lock{nvdec_active_mutex};
|
||||
return nvdec_active_channels.contains(process_id);
|
||||
return nvdec_active;
|
||||
}
|
||||
|
||||
void InitializeDebugger(System& system, u16 port) {
|
||||
@@ -528,8 +509,6 @@ struct System::Impl {
|
||||
|
||||
mutable std::mutex suspend_guard;
|
||||
std::mutex general_channel_mutex;
|
||||
std::mutex nvdec_active_mutex;
|
||||
std::unordered_map<u64, u32> nvdec_active_channels;
|
||||
std::atomic_bool is_paused{};
|
||||
std::atomic_bool is_shutting_down{};
|
||||
std::atomic_bool is_powered_on{};
|
||||
@@ -537,6 +516,7 @@ struct System::Impl {
|
||||
bool extended_memory_layout : 1 = false;
|
||||
bool exit_locked : 1 = false;
|
||||
bool exit_requested : 1 = false;
|
||||
bool nvdec_active : 1 = false;
|
||||
|
||||
void EnsureGeneralChannelInitialized(System& system) {
|
||||
if (!general_channel_event) {
|
||||
@@ -600,22 +580,14 @@ void System::UnstallApplication() {
|
||||
impl->UnstallApplication();
|
||||
}
|
||||
|
||||
void System::NotifyNVDECChannelOpen(u64 process_id) {
|
||||
impl->NotifyNVDECChannelOpen(process_id);
|
||||
}
|
||||
|
||||
void System::NotifyNVDECChannelClose(u64 process_id) {
|
||||
impl->NotifyNVDECChannelClose(process_id);
|
||||
void System::SetNVDECActive(bool is_nvdec_active) {
|
||||
impl->SetNVDECActive(is_nvdec_active);
|
||||
}
|
||||
|
||||
bool System::GetNVDECActive() {
|
||||
return impl->GetNVDECActive();
|
||||
}
|
||||
|
||||
bool System::IsNVDECActiveForProcess(u64 process_id) {
|
||||
return impl->IsNVDECActiveForProcess(process_id);
|
||||
}
|
||||
|
||||
void System::InitializeDebugger() {
|
||||
impl->InitializeDebugger(*this, Settings::values.gdbstub_port.GetValue());
|
||||
}
|
||||
|
||||
+1
-3
@@ -191,10 +191,8 @@ public:
|
||||
std::unique_lock<std::mutex> StallApplication();
|
||||
void UnstallApplication();
|
||||
|
||||
void NotifyNVDECChannelOpen(u64 process_id);
|
||||
void NotifyNVDECChannelClose(u64 process_id);
|
||||
void SetNVDECActive(bool is_nvdec_active);
|
||||
[[nodiscard]] bool GetNVDECActive();
|
||||
[[nodiscard]] bool IsNVDECActiveForProcess(u64 process_id);
|
||||
|
||||
/**
|
||||
* Initialize the debugger.
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <zstd.h>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging.h"
|
||||
#include "common/zstd_compression.h"
|
||||
#include "core/crypto/aes_util.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/crypto/ncz_decompressor.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
|
||||
namespace Core::Crypto {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u64 HeaderSize = 0x4000;
|
||||
constexpr std::size_t SectionWire = 0x40;
|
||||
constexpr std::size_t BlockFixed = 0x18;
|
||||
constexpr std::size_t Chunk = 0x100000;
|
||||
// Upper bound on NCZ section entries (64 bytes each, so the table is at most 4 MiB). A base NCA
|
||||
// has a handful of sections, but nsz stores an update's patch (BKTR) data one entry per
|
||||
// subsection, which is thousands of them.
|
||||
constexpr u64 MaxSectionCount = 0x10000;
|
||||
constexpr std::array<u8, 8> SectionsMagic{'N', 'C', 'Z', 'S', 'E', 'C', 'T', 'N'};
|
||||
constexpr std::array<u8, 8> BlockMagic{'N', 'C', 'Z', 'B', 'L', 'O', 'C', 'K'};
|
||||
|
||||
struct Section {
|
||||
s64 offset, size;
|
||||
u64 crypto_type;
|
||||
Key128 key, counter;
|
||||
bool Ctr() const { return crypto_type == 3 || crypto_type == 4; }
|
||||
};
|
||||
|
||||
struct BlockHeader {
|
||||
u32 exponent;
|
||||
u64 decompressed_size;
|
||||
std::vector<u32> sizes;
|
||||
};
|
||||
|
||||
u64 Le(const u8* p, std::size_t n) {
|
||||
u64 v = 0;
|
||||
for (std::size_t i = n; i-- > 0;) v = (v << 8) | p[i];
|
||||
return v;
|
||||
}
|
||||
|
||||
// Mirrors nsz AESCTR.seek(): upper 8 bytes = stored counter, lower 8 = absolute NCA offset / 16.
|
||||
void SetIv(Key128& iv, const Key128& base, u64 off) {
|
||||
iv = base;
|
||||
u64 blk = off >> 4;
|
||||
for (std::size_t i = 0; i < 8; ++i) { iv[15 - i] = static_cast<u8>(blk & 0xFF); blk >>= 8; }
|
||||
}
|
||||
|
||||
// Parse header + section table + optional block header. Returns payload_start.
|
||||
std::optional<std::pair<std::vector<Section>, std::optional<BlockHeader>>> ParseHeader(
|
||||
const FileSys::VirtualFile& f, u64& payload_start) {
|
||||
const u64 fsize = f->GetSize();
|
||||
if (fsize < HeaderSize + 16) return std::nullopt;
|
||||
|
||||
const auto head = f->ReadBytes(16, HeaderSize);
|
||||
if (head.size() != 16 || !std::equal(head.begin(), head.begin() + 8, SectionsMagic.begin()))
|
||||
return std::nullopt;
|
||||
const u64 count = Le(head.data() + 8, 8);
|
||||
if (count == 0 || count > MaxSectionCount || count * SectionWire > fsize - HeaderSize - 16)
|
||||
return std::nullopt;
|
||||
|
||||
const auto table = f->ReadBytes(count * SectionWire, HeaderSize + 16);
|
||||
if (table.size() != count * SectionWire) return std::nullopt;
|
||||
|
||||
std::vector<Section> sections;
|
||||
sections.reserve(count + 1);
|
||||
for (u64 i = 0; i < count; ++i) {
|
||||
const u8* e = table.data() + i * SectionWire;
|
||||
Section s{};
|
||||
s.offset = static_cast<s64>(Le(e, 8));
|
||||
s.size = static_cast<s64>(Le(e + 8, 8));
|
||||
s.crypto_type = Le(e + 16, 8);
|
||||
std::memcpy(s.key.data(), e + 32, 16);
|
||||
std::memcpy(s.counter.data(), e + 48, 16);
|
||||
sections.push_back(s);
|
||||
}
|
||||
|
||||
const u64 block_start = HeaderSize + 16 + count * SectionWire;
|
||||
const auto fixed = f->ReadBytes(BlockFixed, block_start);
|
||||
std::optional<BlockHeader> bh;
|
||||
if (fixed.size() == BlockFixed &&
|
||||
std::equal(fixed.begin(), fixed.begin() + 8, BlockMagic.begin())) {
|
||||
BlockHeader b{};
|
||||
b.exponent = fixed[11];
|
||||
const s32 n = static_cast<s32>(Le(fixed.data() + 12, 4));
|
||||
if (n <= 0) return std::nullopt;
|
||||
b.decompressed_size = Le(fixed.data() + 16, 8);
|
||||
const auto sizes = f->ReadBytes(static_cast<u64>(n) * 4, block_start + BlockFixed);
|
||||
if (sizes.size() != static_cast<u64>(n) * 4) return std::nullopt;
|
||||
b.sizes.resize(n);
|
||||
for (s32 i = 0; i < n; ++i) b.sizes[i] = static_cast<u32>(Le(sizes.data() + i * 4, 4));
|
||||
payload_start = block_start + BlockFixed + static_cast<u64>(n) * 4;
|
||||
bh = std::move(b);
|
||||
} else {
|
||||
payload_start = block_start;
|
||||
}
|
||||
|
||||
if (!sections.empty() && sections[0].offset > static_cast<s64>(HeaderSize)) {
|
||||
Section fake{};
|
||||
fake.offset = static_cast<s64>(HeaderSize);
|
||||
fake.size = sections[0].offset - static_cast<s64>(HeaderSize);
|
||||
sections.insert(sections.begin(), fake);
|
||||
}
|
||||
return std::make_pair(std::move(sections), std::move(bh));
|
||||
}
|
||||
|
||||
// Sequential reader over independently-compressed blocks. Uses a direct-mapped cache (like
|
||||
// NCZVirtualFile) instead of an LRU list: block_index % CacheSlots picks the slot, so a hit is
|
||||
// O(1). Reuses one ZSTD_DCtx for the lifetime of the reader.
|
||||
class BlockReader {
|
||||
public:
|
||||
static constexpr std::size_t CacheSlots = 32;
|
||||
|
||||
BlockReader(FileSys::VirtualFile f, const BlockHeader& bh, u64 payload_start)
|
||||
: source{std::move(f)}, block_size{1ULL << bh.exponent}, sizes{bh.sizes},
|
||||
decompressed_size{bh.decompressed_size} {
|
||||
if (bh.exponent < 14 || bh.exponent > 32 || sizes.empty())
|
||||
throw std::runtime_error("bad NCZBLOCK header");
|
||||
u64 off = payload_start;
|
||||
offsets.reserve(sizes.size());
|
||||
for (u32 s : sizes) { offsets.push_back(off); off += s; }
|
||||
dctx = ZSTD_createDCtx();
|
||||
if (dctx == nullptr) throw std::runtime_error("ZSTD_createDCtx failed");
|
||||
cache.resize(CacheSlots);
|
||||
for (auto& slot : cache) slot.id = ~0ULL;
|
||||
}
|
||||
|
||||
~BlockReader() {
|
||||
if (dctx != nullptr) ZSTD_freeDCtx(dctx);
|
||||
}
|
||||
|
||||
BlockReader(const BlockReader&) = delete;
|
||||
BlockReader& operator=(const BlockReader&) = delete;
|
||||
|
||||
std::size_t ReadAt(u64 position, std::span<u8> out) {
|
||||
std::size_t done = 0;
|
||||
while (done < out.size() && position < decompressed_size) {
|
||||
const u64 id = position / block_size;
|
||||
if (id >= sizes.size()) break;
|
||||
const std::vector<u8>* block = GetBlock(id);
|
||||
if (block == nullptr) break;
|
||||
const u64 in = position - id * block_size;
|
||||
if (in >= block->size()) break;
|
||||
const std::size_t n = static_cast<std::size_t>(
|
||||
std::min<u64>(out.size() - done, block->size() - in));
|
||||
std::memcpy(out.data() + done, block->data() + in, n);
|
||||
done += n;
|
||||
position += n;
|
||||
}
|
||||
return done;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Slot {
|
||||
u64 id = ~0ULL;
|
||||
std::vector<u8> data;
|
||||
};
|
||||
|
||||
const std::vector<u8>* GetBlock(u64 id) {
|
||||
Slot& slot = cache[id % CacheSlots];
|
||||
if (slot.id == id) {
|
||||
return &slot.data; // O(1) hit
|
||||
}
|
||||
std::vector<u8> buffer;
|
||||
buffer.swap(slot.data); // reuse existing allocation
|
||||
if (!DecodeBlock(id, buffer)) {
|
||||
return nullptr;
|
||||
}
|
||||
slot.data = std::move(buffer);
|
||||
slot.id = id;
|
||||
return &slot.data;
|
||||
}
|
||||
|
||||
bool DecodeBlock(u64 id, std::vector<u8>& buffer) {
|
||||
u64 want = block_size;
|
||||
if (id + 1 == sizes.size()) {
|
||||
const u64 rem = decompressed_size % block_size;
|
||||
if (rem > 0) want = rem; // avoid the 0-size bug from the original nsz
|
||||
}
|
||||
try { buffer.resize(static_cast<std::size_t>(want)); }
|
||||
catch (const std::bad_alloc&) { return false; }
|
||||
const u32 csize = sizes[id];
|
||||
if (csize < want) {
|
||||
const auto comp = source->ReadBytes(csize, offsets[id]);
|
||||
if (comp.size() != csize) return false;
|
||||
const std::size_t res = ZSTD_decompressDCtx(
|
||||
dctx, buffer.data(), buffer.size(), comp.data(), comp.size());
|
||||
if (ZSTD_isError(res) || res != want) {
|
||||
LOG_ERROR(Crypto, "Failed to decompress NCZBLOCK block {}: {}", id,
|
||||
ZSTD_isError(res) ? ZSTD_getErrorName(res) : "size mismatch");
|
||||
return false;
|
||||
}
|
||||
} else if (source->Read(buffer.data(), buffer.size(), offsets[id]) != buffer.size()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
FileSys::VirtualFile source;
|
||||
u64 block_size;
|
||||
std::vector<u64> offsets;
|
||||
std::vector<u32> sizes;
|
||||
u64 decompressed_size;
|
||||
ZSTD_DCtx* dctx = nullptr;
|
||||
std::vector<Slot> cache; // direct-mapped, CacheSlots entries
|
||||
};
|
||||
|
||||
class BlockNczFile final : public FileSys::VfsFile {
|
||||
public:
|
||||
BlockNczFile(std::string name_, std::vector<u8> hdr_, std::vector<Section> sections_,
|
||||
std::unique_ptr<BlockReader> body_)
|
||||
: name{std::move(name_)}, hdr{std::move(hdr_)}, sections{std::move(sections_)},
|
||||
body{std::move(body_)} {
|
||||
nca_size = HeaderSize;
|
||||
for (const auto& s : sections)
|
||||
nca_size = std::max<u64>(nca_size, static_cast<u64>(s.offset + s.size));
|
||||
}
|
||||
|
||||
std::string GetName() const override { return name; }
|
||||
std::size_t GetSize() const override { return static_cast<std::size_t>(nca_size); }
|
||||
bool Resize(std::size_t) override { return false; }
|
||||
FileSys::VirtualDir GetContainingDirectory() const override { return nullptr; }
|
||||
bool IsWritable() const override { return false; }
|
||||
bool IsReadable() const override { return true; }
|
||||
|
||||
// NCZ stores CTR sections decrypted; this file serves them as-is, and NcaFileSystemDriver
|
||||
// skips its AES layers because of this flag.
|
||||
bool HasDecryptedSections() const override { return true; }
|
||||
|
||||
std::size_t Read(u8* data, std::size_t len, std::size_t off) const override {
|
||||
if (off >= nca_size) return 0;
|
||||
len = static_cast<std::size_t>(std::min<u64>(len, nca_size - off));
|
||||
const u64 end = off + len;
|
||||
u64 pos = off;
|
||||
if (pos < HeaderSize) {
|
||||
const u64 n = std::min<u64>(end, HeaderSize) - pos;
|
||||
std::memcpy(data, hdr.data() + pos, static_cast<std::size_t>(n));
|
||||
pos += n;
|
||||
}
|
||||
std::scoped_lock lock{mutex};
|
||||
// Sections are sorted by offset; find the first one whose stop > pos.
|
||||
auto it = std::lower_bound(
|
||||
sections.begin(), sections.end(), pos, [](const Section& s, u64 p) {
|
||||
const u64 b = std::max<u64>(static_cast<u64>(s.offset), HeaderSize);
|
||||
const u64 e = std::max<u64>(static_cast<u64>(s.offset) + static_cast<u64>(s.size), b);
|
||||
return e <= p;
|
||||
});
|
||||
for (; it != sections.end() && pos < end; ++it) {
|
||||
const auto& s = *it;
|
||||
const u64 begin = std::max<u64>(static_cast<u64>(s.offset), HeaderSize);
|
||||
const u64 stop = std::max<u64>(static_cast<u64>(s.offset + s.size), begin);
|
||||
if (stop <= pos) continue;
|
||||
if (pos < begin) {
|
||||
const u64 g = std::min(end, begin);
|
||||
std::memset(data + (pos - off), 0, static_cast<std::size_t>(g - pos));
|
||||
pos = g;
|
||||
if (pos >= end) break;
|
||||
}
|
||||
const u64 pe = std::min(end, stop);
|
||||
const std::span<u8> piece(data + (pos - off), static_cast<std::size_t>(pe - pos));
|
||||
if (body->ReadAt(pos - HeaderSize, piece) != piece.size())
|
||||
return static_cast<std::size_t>(pos - off);
|
||||
// No CTR here: NCZ body is already decrypted, and NcaFileSystemDriver skips AES.
|
||||
pos = pe;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
std::size_t Write(const u8*, std::size_t, std::size_t) override { return 0; }
|
||||
bool Rename(std::string_view) override { return false; }
|
||||
|
||||
private:
|
||||
const std::string name;
|
||||
const std::vector<u8> hdr;
|
||||
const std::vector<Section> sections;
|
||||
const std::unique_ptr<BlockReader> body;
|
||||
mutable std::mutex mutex;
|
||||
u64 nca_size{};
|
||||
};
|
||||
|
||||
// Solid NCZ: one zstd stream over the whole body. Decoded incrementally into memory, up to the
|
||||
// requested offset, so sequential install reads are O(n) instead of O(n^2).
|
||||
class SolidNczFile final : public FileSys::VfsFile {
|
||||
public:
|
||||
SolidNczFile(std::string name_, FileSys::VirtualFile source_,
|
||||
std::vector<Section> sections_, u64 payload_start_)
|
||||
: name{std::move(name_)}, source{std::move(source_)}, sections{std::move(sections_)},
|
||||
payload_start{payload_start_} {
|
||||
const auto raw_header = source->ReadBytes(HeaderSize, 0);
|
||||
data.assign(raw_header.begin(), raw_header.end());
|
||||
frontier = HeaderSize;
|
||||
}
|
||||
|
||||
~SolidNczFile() override {
|
||||
if (stream != nullptr) ZSTD_freeDStream(stream);
|
||||
}
|
||||
|
||||
std::string GetName() const override { return name; }
|
||||
std::size_t GetSize() const override {
|
||||
u64 size = HeaderSize;
|
||||
for (const auto& s : sections)
|
||||
size = std::max<u64>(size, static_cast<u64>(s.offset + s.size));
|
||||
return static_cast<std::size_t>(size);
|
||||
}
|
||||
bool Resize(std::size_t) override { return false; }
|
||||
FileSys::VirtualDir GetContainingDirectory() const override { return nullptr; }
|
||||
bool IsWritable() const override { return false; }
|
||||
bool IsReadable() const override { return true; }
|
||||
|
||||
std::size_t Read(u8* out, std::size_t len, std::size_t off) const override {
|
||||
std::scoped_lock lock{mutex};
|
||||
const u64 nca_size = GetSize();
|
||||
if (off >= nca_size) return 0;
|
||||
len = static_cast<std::size_t>(std::min<u64>(len, nca_size - off));
|
||||
const u64 needed = off + len;
|
||||
if (needed > frontier && !DecodeTo(needed)) return 0;
|
||||
std::memcpy(out, data.data() + off, len);
|
||||
return len;
|
||||
}
|
||||
|
||||
std::size_t Write(const u8*, std::size_t, std::size_t) override { return 0; }
|
||||
bool Rename(std::string_view) override { return false; }
|
||||
|
||||
private:
|
||||
bool DecodeTo(u64 needed) const {
|
||||
if (stream == nullptr) {
|
||||
stream = ZSTD_createDStream();
|
||||
if (stream == nullptr) return false;
|
||||
ZSTD_DCtx_setParameter(stream, ZSTD_d_windowLogMax, 31);
|
||||
read_pos = payload_start;
|
||||
}
|
||||
|
||||
const u64 file_size = source->GetSize();
|
||||
std::vector<u8> out_buf(Chunk);
|
||||
|
||||
std::size_t index = 0;
|
||||
for (; index < sections.size(); ++index) {
|
||||
const auto& s = sections[index];
|
||||
const u64 begin = std::max<u64>(static_cast<u64>(s.offset), HeaderSize);
|
||||
const u64 end = std::max<u64>(static_cast<u64>(s.offset) + static_cast<u64>(s.size),
|
||||
begin);
|
||||
if (end > frontier) break;
|
||||
}
|
||||
|
||||
for (; index < sections.size() && frontier < needed; ++index) {
|
||||
const auto& s = sections[index];
|
||||
const u64 begin = std::max<u64>(static_cast<u64>(s.offset), HeaderSize);
|
||||
const u64 end = std::max<u64>(static_cast<u64>(s.offset) + static_cast<u64>(s.size),
|
||||
begin);
|
||||
if (end <= frontier) continue;
|
||||
u64 pos = std::max<u64>(begin, frontier);
|
||||
|
||||
std::optional<AESCipher<Key128>> cipher;
|
||||
if (s.Ctr()) cipher.emplace(s.key, Mode::CTR);
|
||||
|
||||
while (pos < end && frontier < needed) {
|
||||
const u64 want = std::min<u64>(end - pos, out_buf.size());
|
||||
std::size_t produced = 0;
|
||||
while (produced < want) {
|
||||
if (in.pos >= in.size) {
|
||||
const u64 remaining = file_size - read_pos;
|
||||
const std::size_t chunk = static_cast<std::size_t>(
|
||||
std::min<u64>(Chunk, remaining));
|
||||
if (chunk == 0) { LOG_ERROR(Crypto, "Solid NCZ: truncated"); return false; }
|
||||
const auto bytes = source->ReadBytes(chunk, read_pos);
|
||||
if (bytes.empty()) { LOG_ERROR(Crypto, "Solid NCZ: truncated"); return false; }
|
||||
read_pos += bytes.size();
|
||||
in_buf.assign(bytes.begin(), bytes.end());
|
||||
in.src = in_buf.data();
|
||||
in.size = in_buf.size();
|
||||
in.pos = 0;
|
||||
}
|
||||
ZSTD_outBuffer outb{out_buf.data() + produced, want - produced, 0};
|
||||
const std::size_t res = ZSTD_decompressStream(stream, &outb, &in);
|
||||
if (ZSTD_isError(res)) {
|
||||
LOG_ERROR(Crypto, "Solid NCZ: zstd error");
|
||||
return false;
|
||||
}
|
||||
produced += outb.pos;
|
||||
}
|
||||
if (cipher) {
|
||||
Key128 iv{};
|
||||
SetIv(iv, s.counter, pos);
|
||||
cipher->SetIV(iv);
|
||||
cipher->Transcode(out_buf.data(), produced, out_buf.data(), Op::Encrypt);
|
||||
}
|
||||
data.insert(data.end(), out_buf.begin(), out_buf.begin() + produced);
|
||||
pos += produced;
|
||||
frontier = pos;
|
||||
}
|
||||
}
|
||||
return frontier >= needed;
|
||||
}
|
||||
|
||||
const std::string name;
|
||||
const FileSys::VirtualFile source;
|
||||
const std::vector<Section> sections;
|
||||
const u64 payload_start;
|
||||
mutable std::mutex mutex;
|
||||
mutable std::vector<u8> data;
|
||||
mutable ZSTD_DStream* stream = nullptr;
|
||||
mutable u64 read_pos = 0;
|
||||
mutable u64 frontier = 0;
|
||||
mutable std::vector<u8> in_buf;
|
||||
mutable ZSTD_inBuffer in{nullptr, 0, 0};
|
||||
};
|
||||
|
||||
// Shared opener: block path always; solid only when allow_solid is true.
|
||||
FileSys::VirtualFile OpenNcz(const FileSys::VirtualFile& f, std::string_view name,
|
||||
bool allow_solid) {
|
||||
if (!f) return nullptr;
|
||||
u64 ps = 0;
|
||||
auto parsed = ParseHeader(f, ps);
|
||||
if (!parsed) return nullptr;
|
||||
auto& [sections, block] = *parsed;
|
||||
|
||||
if (block) {
|
||||
auto hdr = f->ReadBytes(HeaderSize, 0);
|
||||
if (hdr.size() != HeaderSize) return nullptr;
|
||||
std::unique_ptr<BlockReader> body;
|
||||
try { body = std::make_unique<BlockReader>(f, *block, ps); }
|
||||
catch (const std::exception& e) {
|
||||
LOG_ERROR(Crypto, "Failed to open NCZBLOCK: {}", e.what());
|
||||
return nullptr;
|
||||
}
|
||||
return std::make_shared<BlockNczFile>(std::string{name}, std::move(hdr),
|
||||
std::move(sections), std::move(body));
|
||||
}
|
||||
|
||||
if (allow_solid) {
|
||||
return std::make_shared<SolidNczFile>(std::string{name}, f, std::move(sections), ps);
|
||||
}
|
||||
|
||||
LOG_DEBUG(Crypto, "Solid NCZ {} skipped (only NCZBLOCK is supported for play)", name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
bool IsBlockCompressedNCZ(const FileSys::VirtualFile& f) {
|
||||
if (!f) return false;
|
||||
u64 ps = 0;
|
||||
const auto p = ParseHeader(f, ps);
|
||||
return p && p->second.has_value();
|
||||
}
|
||||
|
||||
FileSys::VirtualFile DecompressNCZ(const FileSys::VirtualFile& f, std::string_view name) {
|
||||
return OpenNcz(f, name, /*allow_solid=*/false);
|
||||
}
|
||||
|
||||
FileSys::VirtualFile DecompressNCZForInstall(const FileSys::VirtualFile& f,
|
||||
std::string_view name) {
|
||||
return OpenNcz(f, name, /*allow_solid=*/true);
|
||||
}
|
||||
|
||||
} // namespace Core::Crypto
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include "core/file_sys/vfs/vfs_types.h"
|
||||
|
||||
namespace Core::Crypto {
|
||||
|
||||
/**
|
||||
* Opens an NCZBLOCK-compressed NCA (nsz -B / .xcz block format) as a VirtualFile backed by
|
||||
* on-demand block decoding. Nothing is written to disk; each read decodes only the blocks it
|
||||
* covers and re-encrypts CTR sections. Solid NCZ is not supported.
|
||||
*
|
||||
* @param ncz_file The raw NCZ payload (e.g. "<id>.ncz" entry from an NSZ/XCZ PFS0).
|
||||
* @param output_name Name to give the returned VirtualFile (e.g. "<id>.nca").
|
||||
*
|
||||
* @return a VirtualFile for the NCA, or nullptr if the NCZ is malformed or not NCZBLOCK.
|
||||
*/
|
||||
[[nodiscard]] FileSys::VirtualFile DecompressNCZ(const FileSys::VirtualFile& ncz_file,
|
||||
std::string_view output_name);
|
||||
|
||||
/**
|
||||
* Returns true if the given file is an NCZ payload in block-compressed (NCZBLOCK) form,
|
||||
* i.e. one that DecompressNCZ() can open. Solid NCZ returns false.
|
||||
*/
|
||||
[[nodiscard]] bool IsBlockCompressedNCZ(const FileSys::VirtualFile& ncz_file);
|
||||
|
||||
// Install to NAND: same as DecompressNCZ, but solid NCZ is decoded on demand too.
|
||||
[[nodiscard]] FileSys::VirtualFile DecompressNCZForInstall(
|
||||
const FileSys::VirtualFile& ncz_file, std::string_view output_name);
|
||||
|
||||
} // namespace Core::Crypto
|
||||
@@ -45,6 +45,7 @@ NCA::NCA(VirtualFile file_, const NCA* base_nca, bool allow_missing_base)
|
||||
// Ensure we have the proper key area keys to continue.
|
||||
const u8 master_key_id = MasterKeyIdForKeyGeneration(reader->GetKeyGeneration());
|
||||
if (!keys.HasKey(Core::Crypto::S128KeyType::KeyArea, master_key_id, reader->GetKeyIndex())) {
|
||||
LOG_ERROR(Loader, "Cannot decrypt {}: keys for gen 0x{:02X}+ required.", file->GetName(), reader->GetKeyGeneration());
|
||||
status = Loader::ResultStatus::ErrorMissingKeyAreaKey;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
@@ -93,6 +93,11 @@ Result NcaFileSystemDriver::OpenStorageImpl(VirtualFile* out, NcaFsHeaderReader*
|
||||
// Initialize our header reader for the fs index.
|
||||
R_TRY(out_header_reader->Initialize(*m_reader, fs_index));
|
||||
|
||||
// NCZ files serve already-decrypted body data (CTR sections are stored decrypted).
|
||||
// Skip the AES layers so we don't decrypt plaintext twice.
|
||||
const bool is_pre_decrypted =
|
||||
m_reader->GetStorage() && m_reader->GetStorage()->HasDecryptedSections();
|
||||
|
||||
// Declare the storage we're opening.
|
||||
VirtualFile storage;
|
||||
|
||||
@@ -158,7 +163,7 @@ Result NcaFileSystemDriver::OpenStorageImpl(VirtualFile* out, NcaFsHeaderReader*
|
||||
out_header_reader->GetPatchMetaDataHashDataInfo()));
|
||||
}
|
||||
|
||||
if (patch_info.HasAesCtrExTable()) {
|
||||
if (patch_info.HasAesCtrExTable() && !is_pre_decrypted) {
|
||||
// Check the encryption type.
|
||||
ASSERT(out_header_reader->GetEncryptionType() == NcaFsHeader::EncryptionType::None ||
|
||||
out_header_reader->GetEncryptionType() == NcaFsHeader::EncryptionType::AesCtrEx ||
|
||||
@@ -196,32 +201,34 @@ Result NcaFileSystemDriver::OpenStorageImpl(VirtualFile* out, NcaFsHeaderReader*
|
||||
}
|
||||
} else {
|
||||
// Create the appropriate storage for the encryption type.
|
||||
switch (out_header_reader->GetEncryptionType()) {
|
||||
case NcaFsHeader::EncryptionType::None:
|
||||
// If there's no encryption, use the base storage we made previously.
|
||||
break;
|
||||
case NcaFsHeader::EncryptionType::AesXts:
|
||||
R_TRY(this->CreateAesXtsStorage(std::addressof(storage), std::move(storage),
|
||||
fs_data_offset));
|
||||
break;
|
||||
case NcaFsHeader::EncryptionType::AesCtr:
|
||||
R_TRY(this->CreateAesCtrStorage(std::addressof(storage), std::move(storage),
|
||||
fs_data_offset, out_header_reader->GetAesCtrUpperIv(),
|
||||
AlignmentStorageRequirement::None));
|
||||
break;
|
||||
case NcaFsHeader::EncryptionType::AesCtrSkipLayerHash: {
|
||||
// Create the aes ctr storage.
|
||||
VirtualFile aes_ctr_storage;
|
||||
R_TRY(this->CreateAesCtrStorage(std::addressof(aes_ctr_storage), storage,
|
||||
fs_data_offset, out_header_reader->GetAesCtrUpperIv(),
|
||||
AlignmentStorageRequirement::None));
|
||||
if (!is_pre_decrypted) {
|
||||
switch (out_header_reader->GetEncryptionType()) {
|
||||
case NcaFsHeader::EncryptionType::None:
|
||||
// If there's no encryption, use the base storage we made previously.
|
||||
break;
|
||||
case NcaFsHeader::EncryptionType::AesXts:
|
||||
R_TRY(this->CreateAesXtsStorage(std::addressof(storage), std::move(storage),
|
||||
fs_data_offset));
|
||||
break;
|
||||
case NcaFsHeader::EncryptionType::AesCtr:
|
||||
R_TRY(this->CreateAesCtrStorage(std::addressof(storage), std::move(storage),
|
||||
fs_data_offset, out_header_reader->GetAesCtrUpperIv(),
|
||||
AlignmentStorageRequirement::None));
|
||||
break;
|
||||
case NcaFsHeader::EncryptionType::AesCtrSkipLayerHash: {
|
||||
// Create the aes ctr storage.
|
||||
VirtualFile aes_ctr_storage;
|
||||
R_TRY(this->CreateAesCtrStorage(std::addressof(aes_ctr_storage), storage,
|
||||
fs_data_offset, out_header_reader->GetAesCtrUpperIv(),
|
||||
AlignmentStorageRequirement::None));
|
||||
|
||||
// Create region switch storage.
|
||||
R_TRY(this->CreateRegionSwitchStorage(std::addressof(storage), out_header_reader,
|
||||
std::move(storage), std::move(aes_ctr_storage)));
|
||||
} break;
|
||||
default:
|
||||
R_THROW(ResultInvalidNcaFsHeaderEncryptionType);
|
||||
// Create region switch storage.
|
||||
R_TRY(this->CreateRegionSwitchStorage(std::addressof(storage), out_header_reader,
|
||||
std::move(storage), std::move(aes_ctr_storage)));
|
||||
} break;
|
||||
default:
|
||||
R_THROW(ResultInvalidNcaFsHeaderEncryptionType);
|
||||
}
|
||||
}
|
||||
|
||||
// Potentially save storages to our context.
|
||||
@@ -382,21 +389,25 @@ Result NcaFileSystemDriver::OpenIndirectableStorageAsOriginal(
|
||||
}
|
||||
|
||||
// Create the appropriate storage for the encryption type.
|
||||
switch (header_reader->GetEncryptionType()) {
|
||||
case NcaFsHeader::EncryptionType::None:
|
||||
// If there's no encryption, use the base storage we made previously.
|
||||
break;
|
||||
case NcaFsHeader::EncryptionType::AesXts:
|
||||
R_TRY(
|
||||
this->CreateAesXtsStorage(std::addressof(storage), std::move(storage), fs_data_offset));
|
||||
break;
|
||||
case NcaFsHeader::EncryptionType::AesCtr:
|
||||
R_TRY(this->CreateAesCtrStorage(std::addressof(storage), std::move(storage), fs_data_offset,
|
||||
header_reader->GetAesCtrUpperIv(),
|
||||
AlignmentStorageRequirement::CacheBlockSize));
|
||||
break;
|
||||
default:
|
||||
R_THROW(ResultInvalidNcaFsHeaderEncryptionType);
|
||||
const bool is_pre_decrypted =
|
||||
m_reader->GetStorage() && m_reader->GetStorage()->HasDecryptedSections();
|
||||
if (!is_pre_decrypted) {
|
||||
switch (header_reader->GetEncryptionType()) {
|
||||
case NcaFsHeader::EncryptionType::None:
|
||||
// If there's no encryption, use the base storage we made previously.
|
||||
break;
|
||||
case NcaFsHeader::EncryptionType::AesXts:
|
||||
R_TRY(this->CreateAesXtsStorage(std::addressof(storage), std::move(storage),
|
||||
fs_data_offset));
|
||||
break;
|
||||
case NcaFsHeader::EncryptionType::AesCtr:
|
||||
R_TRY(this->CreateAesCtrStorage(std::addressof(storage), std::move(storage),
|
||||
fs_data_offset, header_reader->GetAesCtrUpperIv(),
|
||||
AlignmentStorageRequirement::CacheBlockSize));
|
||||
break;
|
||||
default:
|
||||
R_THROW(ResultInvalidNcaFsHeaderEncryptionType);
|
||||
}
|
||||
}
|
||||
|
||||
// Set output storage.
|
||||
@@ -432,6 +443,11 @@ Result NcaFileSystemDriver::CreateAesCtrStorage(
|
||||
ASSERT(out != nullptr);
|
||||
ASSERT(base_storage != nullptr);
|
||||
|
||||
if (m_reader->GetStorage() && m_reader->GetStorage()->HasDecryptedSections()) {
|
||||
*out = std::move(base_storage);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// Create the iv.
|
||||
std::array<u8, AesCtrStorage::IvSize> iv{};
|
||||
AesCtrStorage::MakeIv(iv.data(), sizeof(iv), upper_iv.value, offset);
|
||||
@@ -469,6 +485,11 @@ Result NcaFileSystemDriver::CreateAesXtsStorage(VirtualFile* out, VirtualFile ba
|
||||
ASSERT(out != nullptr);
|
||||
ASSERT(base_storage != nullptr);
|
||||
|
||||
if (m_reader->GetStorage() && m_reader->GetStorage()->HasDecryptedSections()) {
|
||||
*out = std::move(base_storage);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// Create the iv.
|
||||
std::array<u8, AesXtsStorage::IvSize> iv{};
|
||||
AesXtsStorage::MakeAesXtsIv(iv.data(), sizeof(iv), offset, NcaHeader::XtsBlockSize);
|
||||
@@ -809,6 +830,12 @@ Result NcaFileSystemDriver::CreateAesCtrExStorageMetaStorage(
|
||||
// Validate preconditions.
|
||||
ASSERT(out != nullptr);
|
||||
ASSERT(base_storage != nullptr);
|
||||
|
||||
if (m_reader->GetStorage() && m_reader->GetStorage()->HasDecryptedSections()) {
|
||||
*out = std::move(base_storage);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
ASSERT(encryption_type == NcaFsHeader::EncryptionType::None ||
|
||||
encryption_type == NcaFsHeader::EncryptionType::AesCtrEx ||
|
||||
encryption_type == NcaFsHeader::EncryptionType::AesCtrExSkipLayerHash);
|
||||
@@ -871,6 +898,11 @@ Result NcaFileSystemDriver::CreateAesCtrExStorage(
|
||||
ASSERT(meta_storage != nullptr);
|
||||
ASSERT(patch_info.HasAesCtrExTable());
|
||||
|
||||
if (m_reader->GetStorage() && m_reader->GetStorage()->HasDecryptedSections()) {
|
||||
*out = std::move(base_storage);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// Read the bucket tree header.
|
||||
BucketTree::Header header;
|
||||
std::memcpy(std::addressof(header), patch_info.aes_ctr_ex_header.data(), sizeof(header));
|
||||
|
||||
@@ -104,6 +104,7 @@ public:
|
||||
const NcaCompressionConfiguration& compression_cfg);
|
||||
|
||||
VirtualFile GetSharedBodyStorage();
|
||||
VirtualFile GetStorage() const { return m_body_storage; }
|
||||
u32 GetMagic() const;
|
||||
NcaHeader::DistributionType GetDistributionType() const;
|
||||
NcaHeader::ContentType GetContentType() const;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "common/random.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/crypto/ncz_decompressor.h"
|
||||
#include "core/file_sys/card_image.h"
|
||||
#include "core/file_sys/common_funcs.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
@@ -158,6 +159,25 @@ static std::shared_ptr<NSP> OpenContainerAsNsp(const VirtualFile& file, Loader::
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Checks that every .ncz entry in the container is NCZBLOCK. Solid NCZ has no random access,
|
||||
// and the whole point of this build is to reject it at scan time so such files never appear
|
||||
// in the game list. Containers without any .ncz (plain NSP/XCI) pass through.
|
||||
static bool ContainerIsFullyBlockCompressed(const std::shared_ptr<NSP>& nsp) {
|
||||
if (!nsp) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& entry : nsp->GetFiles()) {
|
||||
const auto name = entry->GetName();
|
||||
if (name.size() < 4 || name.substr(name.size() - 4) != ".ncz") {
|
||||
continue;
|
||||
}
|
||||
if (!Core::Crypto::IsBlockCompressedNCZ(entry)) {
|
||||
return false; // solid NCZ -> reject the whole container
|
||||
}
|
||||
}
|
||||
return true; // no .ncz at all, or all block-compressed
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
bool ForEachContainerEntry(const std::shared_ptr<NSP>& nsp, bool only_content,
|
||||
std::optional<u64> base_program_id, Callback&& on_entry) {
|
||||
@@ -811,7 +831,14 @@ std::vector<ContentProviderEntry> RegisteredCache::ListEntriesFilter(
|
||||
}
|
||||
|
||||
static std::shared_ptr<NCA> GetNCAFromNSPForID(const NSP& nsp, const NcaID& id) {
|
||||
auto file = nsp.GetFile(fmt::format("{}.nca", Common::HexToString(id, false)));
|
||||
const auto id_string = Common::HexToString(id, false);
|
||||
auto file = nsp.GetFile(fmt::format("{}.nca", id_string));
|
||||
if (file == nullptr) {
|
||||
if (auto ncz_file = nsp.GetFile(fmt::format("{}.ncz", id_string))) {
|
||||
file = Core::Crypto::DecompressNCZForInstall(
|
||||
ncz_file, fmt::format("{}.nca", id_string));
|
||||
}
|
||||
}
|
||||
if (file == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1273,6 +1300,12 @@ bool ManualContentProvider::AddEntriesFromContainer(VirtualFile file, bool only_
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject solid NCZ (.nsz without -B): only block-compressed NCZ is supported.
|
||||
if (!ContainerIsFullyBlockCompressed(nsp)) {
|
||||
LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName());
|
||||
return false;
|
||||
}
|
||||
|
||||
return ForEachContainerEntry(
|
||||
nsp, only_content, base_program_id,
|
||||
[this](TitleType title_type, ContentRecordType content_type, u64 title_id,
|
||||
@@ -1415,8 +1448,9 @@ void ExternalContentProvider::ScanDirectory(const VirtualDir& dir) {
|
||||
}
|
||||
|
||||
const auto extension = Common::ToLower(filename.substr(dot_pos + 1));
|
||||
|
||||
if (extension == "nsp") {
|
||||
// .xcz is an XCI container, but its updates only work through the NSP/PFS0 path
|
||||
// (DecompressNCZ), not ProcessXCI. So route .xcz here on purpose.
|
||||
if (extension == "nsp" || extension == "nsz" || extension == "xcz") {
|
||||
ProcessNSP(file);
|
||||
} else if (extension == "xci") {
|
||||
ProcessXCI(file);
|
||||
@@ -1434,6 +1468,12 @@ void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reject solid NCZ (.nsz without -B): Eden only supports block-compressed NCZ.
|
||||
if (!ContainerIsFullyBlockCompressed(nsp)) {
|
||||
LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG(Service_FS, "Processing NSP file: {}", file->GetName());
|
||||
AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/crypto/ncz_decompressor.h"
|
||||
#include "core/file_sys/common_funcs.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
@@ -246,6 +247,17 @@ void NSP::ReadNCAs(const std::vector<VirtualFile>& files) {
|
||||
const auto id_string = Common::HexToString(rec.nca_id, false);
|
||||
auto next_file = pfs->GetFile(fmt::format("{}.nca", id_string));
|
||||
|
||||
if (next_file == nullptr) {
|
||||
auto ncz_file = pfs->GetFile(fmt::format("{}.ncz", id_string));
|
||||
if (ncz_file != nullptr) {
|
||||
next_file = Core::Crypto::DecompressNCZ(ncz_file, fmt::format("{}.nca", id_string));
|
||||
if (next_file == nullptr) {
|
||||
LOG_DEBUG(Service_FS, "Skipping solid or unsupported NCZ content {}.ncz", id_string);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (next_file == nullptr) {
|
||||
if (rec.type != ContentRecordType::DeltaFragment) {
|
||||
LOG_WARNING(Service_FS,
|
||||
|
||||
@@ -112,6 +112,9 @@ public:
|
||||
// Returns whether or not the file can be read from.
|
||||
virtual bool IsReadable() const = 0;
|
||||
|
||||
// Returns true if this file serves NCA body data that is already decrypted (e.g. NCZ).
|
||||
virtual bool HasDecryptedSections() const { return false; }
|
||||
|
||||
// The primary method of reading from the file. Reads length bytes into data starting at offset
|
||||
// into file. Returns number of bytes successfully read.
|
||||
virtual std::size_t Read(u8* data, std::size_t length, std::size_t offset = 0) const = 0;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -37,6 +40,10 @@ public:
|
||||
|
||||
std::size_t GetOffset() const;
|
||||
|
||||
bool HasDecryptedSections() const override {
|
||||
return file && file->HasDecryptedSections();
|
||||
}
|
||||
|
||||
private:
|
||||
std::size_t TrimToFit(std::size_t r_size, std::size_t r_offset) const;
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/hle/service/cmif_serialization.h"
|
||||
#include "core/hle/service/filesystem/fsp/fs_i_storage.h"
|
||||
@@ -31,6 +35,14 @@ Result IStorage::Read(
|
||||
R_UNLESS(length >= 0, FileSys::ResultInvalidSize);
|
||||
R_UNLESS(offset >= 0, FileSys::ResultInvalidOffset);
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now < next_read_tick) {
|
||||
std::this_thread::sleep_for(next_read_tick - now);
|
||||
}
|
||||
next_read_tick = std::chrono::steady_clock::now() +
|
||||
std::chrono::nanoseconds{std::min(length, MaxChargedReadSize) *
|
||||
1'000'000'000 / ReadBytesPerSecond};
|
||||
|
||||
// Read the data from the Storage backend
|
||||
backend->Read(out_bytes.data(), length, offset);
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
#include "core/hle/service/cmif_types.h"
|
||||
#include "core/hle/service/filesystem/filesystem.h"
|
||||
@@ -20,6 +22,10 @@ public:
|
||||
private:
|
||||
FileSys::VirtualFile backend;
|
||||
|
||||
static constexpr s64 ReadBytesPerSecond = 40'000'000;
|
||||
static constexpr s64 MaxChargedReadSize = 64 * 1024;
|
||||
std::chrono::steady_clock::time_point next_read_tick{};
|
||||
|
||||
Result Read(
|
||||
OutBuffer<BufferAttr_HipcMapAlias | BufferAttr_HipcMapTransferAllowsNonSecure> out_bytes,
|
||||
s64 offset, s64 length);
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include "common/assert.h"
|
||||
#include "common/logging.h"
|
||||
#include "core/core.h"
|
||||
#include "core/hle/kernel/k_process.h"
|
||||
#include "core/hle/service/nvdrv/core/container.h"
|
||||
#include "core/hle/service/nvdrv/devices/ioctl_serialization.h"
|
||||
#include "core/hle/service/nvdrv/devices/nvhost_nvdec.h"
|
||||
@@ -72,23 +71,17 @@ NvResult nvhost_nvdec::Ioctl3(DeviceFD fd, Ioctl command, std::span<const u8> in
|
||||
|
||||
void nvhost_nvdec::OnOpen(NvCore::SessionId session_id, DeviceFD fd) {
|
||||
LOG_INFO(Service_NVDRV, "NVDEC video stream started");
|
||||
system.SetNVDECActive(true);
|
||||
sessions[fd] = session_id;
|
||||
if (const auto* session = core.GetSession(session_id);
|
||||
session != nullptr && session->process != nullptr) {
|
||||
system.NotifyNVDECChannelOpen(session->process->GetId());
|
||||
}
|
||||
host1x.StartDevice(fd, Tegra::Host1x::ChannelType::NvDec, channel_syncpoint);
|
||||
}
|
||||
|
||||
void nvhost_nvdec::OnClose(DeviceFD fd) {
|
||||
LOG_INFO(Service_NVDRV, "NVDEC video stream ended");
|
||||
host1x.StopDevice(fd, Tegra::Host1x::ChannelType::NvDec);
|
||||
system.SetNVDECActive(false);
|
||||
auto it = sessions.find(fd);
|
||||
if (it != sessions.end()) {
|
||||
if (const auto* session = core.GetSession(it->second);
|
||||
session != nullptr && session->process != nullptr) {
|
||||
system.NotifyNVDECChannelClose(session->process->GetId());
|
||||
}
|
||||
sessions.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,12 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <chrono>
|
||||
#include <fmt/ranges.h>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include "common/assert.h"
|
||||
#include "common/logging.h"
|
||||
#include "common/settings.h"
|
||||
#include "core/core.h"
|
||||
#include "core/hle/ipc.h"
|
||||
#include "core/hle/kernel/k_process.h"
|
||||
#include "core/hle/kernel/kernel.h"
|
||||
#include "core/hle/service/ipc_helpers.h"
|
||||
#include "core/hle/service/service.h"
|
||||
@@ -37,7 +33,6 @@ ServiceFrameworkBase::ServiceFrameworkBase(Core::System& system_, const char* se
|
||||
: SessionRequestHandler(system_.Kernel(), service_name_)
|
||||
, system{system_}
|
||||
, service_name{service_name_}
|
||||
, is_i_storage{std::string_view{service_name_} == "IStorage"}
|
||||
, handler_invoker{handler_invoker_}
|
||||
, max_sessions{max_sessions_}
|
||||
{}
|
||||
@@ -82,22 +77,13 @@ void ServiceFrameworkBase::ReportUnimplementedFunction(HLERequestContext& ctx,
|
||||
}
|
||||
|
||||
void ServiceFrameworkBase::InvokeRequest(HLERequestContext& ctx) {
|
||||
const auto command = ctx.GetCommand();
|
||||
auto it = handlers.find(command);
|
||||
const bool is_cmd_read = command == 0;
|
||||
auto it = handlers.find(ctx.GetCommand());
|
||||
FunctionInfoBase const* info = it == handlers.end() ? nullptr : &it->second;
|
||||
if (info == nullptr || info->handler_callback == nullptr)
|
||||
return ReportUnimplementedFunction(ctx, info);
|
||||
|
||||
LOG_TRACE(Service, "{}", MakeFunctionString(info->name, GetServiceName(), ctx.CommandBuffer()));
|
||||
handler_invoker(this, info->handler_callback, ctx);
|
||||
|
||||
if (is_i_storage && is_cmd_read) {
|
||||
const auto* const process = ctx.GetThread().GetOwnerProcess();
|
||||
if (process != nullptr && system.IsNVDECActiveForProcess(process->GetId())) {
|
||||
std::this_thread::sleep_for(std::chrono::microseconds{600});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ServiceFrameworkBase::InvokeRequestTipc(HLERequestContext& ctx) {
|
||||
|
||||
@@ -107,8 +107,6 @@ protected:
|
||||
Core::System& system;
|
||||
/// Identifier string used to connect to the service.
|
||||
const char* service_name;
|
||||
/// Whether this is the IStorage service.
|
||||
const bool is_i_storage;
|
||||
/// Function used to safely up-cast pointers to the derived class before invoking a handler.
|
||||
InvokerFn* handler_invoker;
|
||||
/// Maximum number of concurrent sessions that this service can handle.
|
||||
|
||||
@@ -82,23 +82,27 @@ std::shared_ptr<FileSys::NSP> OpenContainerAsNsp(FileSys::VirtualFile file, File
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (type == FileType::NSP) {
|
||||
if (type == FileType::NSP || type == FileType::NSZ) {
|
||||
auto nsp = std::make_shared<FileSys::NSP>(file, program_id, program_index);
|
||||
return nsp->GetStatus() == ResultStatus::Success ? nsp : nullptr;
|
||||
}
|
||||
|
||||
if (type == FileType::XCI) {
|
||||
if (type == FileType::XCI || type == FileType::XCZ) {
|
||||
FileSys::XCI xci{file, program_id, program_index};
|
||||
if (xci.GetStatus() != ResultStatus::Success) {
|
||||
return nullptr;
|
||||
if (xci.GetStatus() == ResultStatus::Success) {
|
||||
auto secure_nsp = xci.GetSecurePartitionNSP();
|
||||
if (secure_nsp != nullptr && secure_nsp->GetStatus() == ResultStatus::Success) {
|
||||
return secure_nsp;
|
||||
}
|
||||
}
|
||||
|
||||
auto secure_nsp = xci.GetSecurePartitionNSP();
|
||||
if (secure_nsp == nullptr || secure_nsp->GetStatus() != ResultStatus::Success) {
|
||||
return nullptr;
|
||||
// PFS0-shaped .xcz (NSZ-style container) does not parse as XCI; fall back to NSP.
|
||||
if (type == FileType::XCZ) {
|
||||
auto nsp = std::make_shared<FileSys::NSP>(file, program_id, program_index);
|
||||
return nsp->GetStatus() == ResultStatus::Success ? nsp : nullptr;
|
||||
}
|
||||
|
||||
return secure_nsp;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
@@ -144,7 +148,8 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
|
||||
}
|
||||
|
||||
bool IsContainerType(FileType type) {
|
||||
return type == FileType::NSP || type == FileType::XCI;
|
||||
return type == FileType::NSP || type == FileType::NSZ || type == FileType::XCI ||
|
||||
type == FileType::XCZ;
|
||||
}
|
||||
|
||||
bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 program_id,
|
||||
@@ -180,8 +185,12 @@ FileType GuessFromFilename(const std::string& name) {
|
||||
return FileType::NCA;
|
||||
else if (extension == "xci")
|
||||
return FileType::XCI;
|
||||
else if (extension == "xcz")
|
||||
return FileType::XCZ;
|
||||
else if (extension == "nsp")
|
||||
return FileType::NSP;
|
||||
else if (extension == "nsz")
|
||||
return FileType::NSZ;
|
||||
else if (extension == "kip")
|
||||
return FileType::KIP;
|
||||
return FileType::Unknown;
|
||||
@@ -197,10 +206,14 @@ std::string GetFileTypeString(FileType type) {
|
||||
return "NCA";
|
||||
case FileType::XCI:
|
||||
return "XCI";
|
||||
case FileType::XCZ:
|
||||
return "XCZ";
|
||||
case FileType::NAX:
|
||||
return "NAX";
|
||||
case FileType::NSP:
|
||||
return "NSP";
|
||||
case FileType::NSZ:
|
||||
return "NSZ";
|
||||
case FileType::KIP:
|
||||
return "KIP";
|
||||
case FileType::DeconstructedRomDirectory:
|
||||
@@ -326,12 +339,29 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileSys::V
|
||||
system.GetContentProvider(), program_id,
|
||||
program_index);
|
||||
|
||||
// NCZ-compressed XCI container. XCI-shaped .xcz goes to the XCI loader, while
|
||||
// PFS0-shaped (NSZ-style) .xcz containers fall back to the NSP loader.
|
||||
case FileType::XCZ: {
|
||||
FileSys::XCI probe{file, program_id, program_index};
|
||||
if (probe.GetStatus() == ResultStatus::Success &&
|
||||
probe.GetSecurePartitionNSP() != nullptr) {
|
||||
return std::make_unique<AppLoader_XCI>(std::move(file),
|
||||
system.GetFileSystemController(),
|
||||
system.GetContentProvider(), program_id,
|
||||
program_index);
|
||||
}
|
||||
return std::make_unique<AppLoader_NSP>(std::move(file), system.GetFileSystemController(),
|
||||
system.GetContentProvider(), program_id,
|
||||
program_index);
|
||||
}
|
||||
|
||||
// NX NAX (NintendoAesXts) file format.
|
||||
case FileType::NAX:
|
||||
return std::make_unique<AppLoader_NAX>(std::move(file));
|
||||
|
||||
// NX NSP (Nintendo Submission Package) file format
|
||||
case FileType::NSP:
|
||||
case FileType::NSZ:
|
||||
return std::make_unique<AppLoader_NSP>(std::move(file), system.GetFileSystemController(),
|
||||
system.GetContentProvider(), program_id,
|
||||
program_index);
|
||||
|
||||
@@ -42,7 +42,9 @@ enum class FileType {
|
||||
NRO,
|
||||
NCA,
|
||||
NSP,
|
||||
NSZ,
|
||||
XCI,
|
||||
XCZ,
|
||||
NAX,
|
||||
KIP,
|
||||
DeconstructedRomDirectory,
|
||||
|
||||
@@ -56,6 +56,17 @@ AppLoader_NSP::AppLoader_NSP(FileSys::VirtualFile file_,
|
||||
AppLoader_NSP::~AppLoader_NSP() = default;
|
||||
|
||||
FileType AppLoader_NSP::IdentifyType(const FileSys::VirtualFile& nsp_file) {
|
||||
// .nsz is NSP-compatible; return NSZ so the game list shows the right type.
|
||||
if (nsp_file->GetName().ends_with(".nsz")) {
|
||||
return FileType::NSZ;
|
||||
}
|
||||
|
||||
// PFS0-shaped .xcz containers are handled by this loader too; report XCZ so the game
|
||||
// list does not show them as NSP. XCI-shaped .xcz is identified by AppLoader_XCI.
|
||||
if (nsp_file->GetName().ends_with(".xcz")) {
|
||||
return FileType::XCZ;
|
||||
}
|
||||
|
||||
const FileSys::NSP nsp(nsp_file);
|
||||
|
||||
if (nsp.GetStatus() != ResultStatus::Success) {
|
||||
|
||||
@@ -54,7 +54,8 @@ FileType AppLoader_XCI::IdentifyType(const FileSys::VirtualFile& xci_file) {
|
||||
// Identify XCI as a valid container even when it does not include a bootable Program NCA.
|
||||
// Bootability is handled by AppLoader_XCI::Load().
|
||||
if (xci.GetSecurePartitionNSP() != nullptr) {
|
||||
return FileType::XCI;
|
||||
return xci_file->GetName().ends_with(".xcz") ? FileType::XCZ
|
||||
: FileType::XCI;
|
||||
}
|
||||
|
||||
return FileType::Error;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||
@@ -163,7 +163,9 @@ inline InstallResult InstallNSP(Core::System& system, FileSys::VfsFilesystem& vf
|
||||
|
||||
std::shared_ptr<FileSys::NSP> nsp;
|
||||
FileSys::VirtualFile file = vfs.OpenFile(filename, FileSys::OpenMode::Read);
|
||||
if (boost::to_lower_copy(file->GetName()).ends_with("nsp")) {
|
||||
const auto lower_name = boost::to_lower_copy(file->GetName());
|
||||
|
||||
if (lower_name.ends_with("nsp") || lower_name.ends_with("nsz")) {
|
||||
nsp = std::make_shared<FileSys::NSP>(file);
|
||||
if (nsp->IsExtractedType()) {
|
||||
return InstallResult::Failure;
|
||||
|
||||
@@ -194,10 +194,6 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
|
||||
INSERT(Settings, skip_cpu_inner_invalidation, tr("Skip CPU Inner Invalidation"),
|
||||
tr("Skips certain cache invalidations during memory updates, reducing CPU usage and "
|
||||
"improving latency. This may cause soft-crashes."));
|
||||
INSERT(Settings, nce_invalidation_gpu_readback,
|
||||
tr("GPU Readback Before NCE Invalidation"),
|
||||
tr("Flushes GPU buffer data back to guest memory before NCE invalidates the whole "
|
||||
"guest page, preserving it while the CPU store proceeds."));
|
||||
INSERT(Settings, vsync_mode, tr("VSync Mode:"),
|
||||
tr("FIFO (VSync) does not drop frames or exhibit tearing but is limited by the screen "
|
||||
"refresh rate.\nFIFO Relaxed allows tearing as it recovers from a slow down.\n"
|
||||
|
||||
@@ -360,7 +360,8 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||
}
|
||||
|
||||
if (target == ScanTarget::PopulateGameList &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP) &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ ||
|
||||
file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ) &&
|
||||
!Loader::IsBootableGameContainer(file, file_type)) {
|
||||
return true;
|
||||
}
|
||||
@@ -375,7 +376,9 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||
program_id, file);
|
||||
} else if (Settings::values.ext_content_from_game_dirs.GetValue() &&
|
||||
(file_type == Loader::FileType::XCI ||
|
||||
file_type == Loader::FileType::NSP)) {
|
||||
file_type == Loader::FileType::XCZ ||
|
||||
file_type == Loader::FileType::NSP ||
|
||||
file_type == Loader::FileType::NSZ)) {
|
||||
void(provider->AddEntriesFromContainer(file));
|
||||
}
|
||||
} else {
|
||||
@@ -402,7 +405,8 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||
};
|
||||
|
||||
if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ ||
|
||||
file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ)) {
|
||||
for (const auto id : program_ids) {
|
||||
// dravee suggested this, only viable way to
|
||||
// not show sub-games in qlaunch for now.
|
||||
|
||||
@@ -57,7 +57,8 @@ std::unique_ptr<EmuThread> emu_thread = nullptr;
|
||||
|
||||
const QStringList supported_file_extensions = {QStringLiteral("nro"), QStringLiteral("nso"),
|
||||
QStringLiteral("nca"), QStringLiteral("xci"),
|
||||
QStringLiteral("nsp"), QStringLiteral("kip")};
|
||||
QStringLiteral("xcz"), QStringLiteral("nsp"),
|
||||
QStringLiteral("nsz"), QStringLiteral("kip")};
|
||||
|
||||
Core::Frontend::WindowSystemType GetWindowSystemType() {
|
||||
// Determine WSI type based on Qt platform.
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <queue>
|
||||
@@ -13,9 +17,10 @@
|
||||
|
||||
namespace Shader::IR {
|
||||
|
||||
template <typename Pred>
|
||||
auto BreadthFirstSearch(const Value& value, Pred&& pred)
|
||||
template <typename Pred, typename Resolve = std::identity>
|
||||
auto BreadthFirstSearch(Value value, Pred&& pred, Resolve&& resolve = {})
|
||||
-> std::invoke_result_t<Pred, const Inst*> {
|
||||
value = resolve(value);
|
||||
if (value.IsImmediate()) {
|
||||
// Nothing to do with immediates
|
||||
return std::nullopt;
|
||||
@@ -36,7 +41,7 @@ auto BreadthFirstSearch(const Value& value, Pred&& pred)
|
||||
}
|
||||
// Visit the right most arguments first
|
||||
for (size_t arg = inst->NumArgs(); arg--;) {
|
||||
const Value arg_value{inst->Arg(arg)};
|
||||
const Value arg_value{resolve(inst->Arg(arg))};
|
||||
if (arg_value.IsImmediate()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,60 @@ struct TextureInst {
|
||||
|
||||
using TextureInstVector = boost::container::small_vector<TextureInst, 24>;
|
||||
|
||||
const IR::Block* ConditionalHeader(const IR::Block* body) {
|
||||
const auto predecessors{body->ImmPredecessors()};
|
||||
const auto successors{body->ImmSuccessors()};
|
||||
if (predecessors.size() != 1 || successors.size() != 1) return nullptr;
|
||||
const IR::Block* const header{predecessors[0]};
|
||||
const auto targets{header->ImmSuccessors()};
|
||||
if (targets.size() != 2 || targets[0] != body || targets[1] != successors[0] ||
|
||||
targets[1]->ImmPredecessors().size() != 2 || header->empty() ||
|
||||
header->back().GetOpcode() != IR::Opcode::ConditionRef) {
|
||||
return nullptr;
|
||||
}
|
||||
return header;
|
||||
}
|
||||
|
||||
std::pair<IR::Value, bool> Condition(IR::Value value) {
|
||||
bool negated{};
|
||||
while (const IR::Inst* inst = value.TryInstRecursive()) {
|
||||
if (inst->GetOpcode() == IR::Opcode::LogicalNot) negated = !negated;
|
||||
else if (inst->GetOpcode() != IR::Opcode::ConditionRef) break;
|
||||
value = inst->Arg(0);
|
||||
}
|
||||
return {value.Resolve(), negated};
|
||||
}
|
||||
|
||||
IR::Value ResolveConditionalPhi(IR::Value value, const IR::Block* block) {
|
||||
if (!value.Resolve().IsPhi() || !(block = ConditionalHeader(block))) return value;
|
||||
const auto [condition, negated]{Condition(block->back().Arg(0))};
|
||||
boost::container::small_vector<const IR::Block*, 16> visited;
|
||||
while (value.Resolve().IsPhi() && std::ranges::find(visited, block) == visited.end()) {
|
||||
visited.push_back(block);
|
||||
const IR::Inst* const phi{value.InstRecursive()};
|
||||
const auto predecessors{block->ImmPredecessors()};
|
||||
if (predecessors.size() == 1) {
|
||||
block = predecessors[0];
|
||||
continue;
|
||||
}
|
||||
if (predecessors.size() != 2 || phi->NumArgs() != 2) break;
|
||||
const IR::Block* body{predecessors[0]};
|
||||
const IR::Block* header{ConditionalHeader(body)};
|
||||
if (header != predecessors[1]) {
|
||||
body = predecessors[1];
|
||||
header = ConditionalHeader(body);
|
||||
if (header != predecessors[0]) break;
|
||||
}
|
||||
const auto [other_condition, other_negated]{Condition(header->back().Arg(0))};
|
||||
if (condition == other_condition && (phi->PhiBlock(0) == header || phi->PhiBlock(0) == body)) {
|
||||
const IR::Block* const incoming{negated == other_negated ? body : header};
|
||||
value = phi->Arg(phi->PhiBlock(0) == incoming ? 0 : 1);
|
||||
}
|
||||
block = header;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
constexpr u32 DESCRIPTOR_SIZE = 8;
|
||||
constexpr u32 DESCRIPTOR_SIZE_SHIFT = u32(std::countr_zero(DESCRIPTOR_SIZE));
|
||||
constexpr u32 DESCRIPTOR_MAX_COUNT = 1024;
|
||||
@@ -299,23 +353,23 @@ static inline bool IsTexturePixelFormatIntegerCached(Environment& env,
|
||||
}
|
||||
|
||||
|
||||
std::optional<ConstBufferAddr> Track(const IR::Value& value, Environment& env, const HostTranslateInfo& host_info);
|
||||
static inline std::optional<ConstBufferAddr> TrackCached(const IR::Value& v, Environment& env, const HostTranslateInfo& host_info) {
|
||||
if (const IR::Inst* key = v.InstRecursive()) {
|
||||
std::optional<ConstBufferAddr> Track(const IR::Value& value, Environment& env, const HostTranslateInfo& host_info, const IR::Block* block);
|
||||
static inline std::optional<ConstBufferAddr> TrackCached(const IR::Value& v, Environment& env, const HostTranslateInfo& host_info, const IR::Block* block) {
|
||||
if (const IR::Inst* key = v.TryInstRecursive()) {
|
||||
if (auto it = env.track_cache.find(key); it != env.track_cache.end()) return it->second;
|
||||
auto found = Track(v, env, host_info);
|
||||
auto found = Track(v, env, host_info, block);
|
||||
if (found) env.track_cache.emplace(key, *found);
|
||||
return found;
|
||||
}
|
||||
return Track(v, env, host_info);
|
||||
return Track(v, env, host_info, block);
|
||||
}
|
||||
|
||||
std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environment& env, const HostTranslateInfo& host_info);
|
||||
std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environment& env, const HostTranslateInfo& host_info, const IR::Block* block);
|
||||
|
||||
std::optional<ConstBufferAddr> Track(const IR::Value& value, Environment& env, const HostTranslateInfo& host_info) {
|
||||
return IR::BreadthFirstSearch(value, [&env, &host_info](const IR::Inst* inst) {
|
||||
return TryGetConstBuffer(inst, env, host_info);
|
||||
});
|
||||
std::optional<ConstBufferAddr> Track(const IR::Value& value, Environment& env, const HostTranslateInfo& host_info, const IR::Block* block) {
|
||||
return IR::BreadthFirstSearch(value, [&env, &host_info, block](const IR::Inst* inst) {
|
||||
return TryGetConstBuffer(inst, env, host_info, block);
|
||||
}, [&](IR::Value operand) { return ResolveConditionalPhi(operand, block); });
|
||||
}
|
||||
|
||||
std::optional<u32> TryGetConstant(IR::Value& value, Environment& env) {
|
||||
@@ -339,13 +393,13 @@ std::optional<u32> TryGetConstant(IR::Value& value, Environment& env) {
|
||||
return ReadCbufCached(env, index_number, offset_number);
|
||||
}
|
||||
|
||||
std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environment& env, const HostTranslateInfo& host_info) {
|
||||
std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environment& env, const HostTranslateInfo& host_info, const IR::Block* block) {
|
||||
switch (inst->GetOpcode()) {
|
||||
default:
|
||||
return std::nullopt;
|
||||
case IR::Opcode::BitwiseOr32: {
|
||||
std::optional lhs{TrackCached(inst->Arg(0), env, host_info)};
|
||||
std::optional rhs{TrackCached(inst->Arg(1), env, host_info)};
|
||||
std::optional lhs{TrackCached(inst->Arg(0), env, host_info, block)};
|
||||
std::optional rhs{TrackCached(inst->Arg(1), env, host_info, block)};
|
||||
if (!lhs || !rhs) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -375,7 +429,7 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
if (!shift.IsImmediate()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional lhs{TrackCached(inst->Arg(0), env, host_info)};
|
||||
std::optional lhs{TrackCached(inst->Arg(0), env, host_info, block)};
|
||||
if (lhs) {
|
||||
lhs->shift_left = shift.U32();
|
||||
}
|
||||
@@ -403,7 +457,7 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
return std::nullopt;
|
||||
} while (false);
|
||||
}
|
||||
std::optional lhs{TrackCached(op1, env, host_info)};
|
||||
std::optional lhs{TrackCached(op1, env, host_info, block)};
|
||||
if (lhs) {
|
||||
lhs->shift_left = static_cast<u32>(std::countr_zero(op2.U32()));
|
||||
}
|
||||
@@ -469,7 +523,7 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
TextureInst MakeInst(Environment& env, IR::Block* block, IR::Inst& inst, const HostTranslateInfo& host_info) {
|
||||
ConstBufferAddr addr;
|
||||
if (IsBindless(inst)) {
|
||||
const std::optional<ConstBufferAddr> track_addr{TrackCached(inst.Arg(0), env, host_info)};
|
||||
const std::optional<ConstBufferAddr> track_addr{TrackCached(inst.Arg(0), env, host_info, block)};
|
||||
|
||||
if (!track_addr) {
|
||||
throw NotImplementedException("Failed to track bindless texture constant buffer");
|
||||
@@ -671,6 +725,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
|
||||
env.track_cache.clear();
|
||||
TextureInstVector to_replace;
|
||||
for (IR::Block* const block : program.post_order_blocks) {
|
||||
env.track_cache.clear();
|
||||
for (IR::Inst& inst : block->Instructions()) {
|
||||
if (!IsTextureInstruction(inst)) {
|
||||
continue;
|
||||
|
||||
@@ -1738,36 +1738,64 @@ void BufferCache<P>::TouchBuffer(Buffer& buffer, BufferId buffer_id) noexcept {
|
||||
|
||||
template <class P>
|
||||
bool BufferCache<P>::SynchronizeBuffer(Buffer& buffer, DAddr device_addr, u32 size) {
|
||||
upload_copies.clear();
|
||||
u64 total_size_bytes = 0;
|
||||
u64 largest_copy = 0;
|
||||
const DAddr buffer_start = buffer.cpu_addr_cached;
|
||||
const auto add_upload = [&](DAddr start, DAddr end) {
|
||||
if (start == end) return;
|
||||
const u64 range_size = end - start;
|
||||
upload_copies.push_back(BufferCopy{
|
||||
.src_offset = total_size_bytes,
|
||||
.dst_offset = start - buffer_start,
|
||||
.size = range_size,
|
||||
#ifdef __ANDROID__
|
||||
const bool use_optimized_vertex_buffers = Settings::values.use_optimized_vertex_buffers.GetValue();
|
||||
#else
|
||||
constexpr bool use_optimized_vertex_buffers = true;
|
||||
#endif
|
||||
|
||||
if (use_optimized_vertex_buffers) {
|
||||
upload_copies.clear();
|
||||
u64 total_size_bytes = 0;
|
||||
u64 largest_copy = 0;
|
||||
const DAddr buffer_start = buffer.cpu_addr_cached;
|
||||
const auto add_upload = [&](DAddr start, DAddr end) {
|
||||
if (start == end) return;
|
||||
const u64 range_size = end - start;
|
||||
upload_copies.push_back(BufferCopy{
|
||||
.src_offset = total_size_bytes,
|
||||
.dst_offset = start - buffer_start,
|
||||
.size = range_size,
|
||||
});
|
||||
total_size_bytes += range_size;
|
||||
largest_copy = (std::max)(largest_copy, range_size);
|
||||
};
|
||||
memory_tracker.ForEachUploadRange(device_addr, size, [&](u64 device_addr_out, u64 range_size) {
|
||||
DAddr upload_start = device_addr_out;
|
||||
gpu_modified_ranges.ForEachInRange(device_addr_out, range_size, [&](DAddr gpu_start, DAddr gpu_end) {
|
||||
add_upload(upload_start, gpu_start);
|
||||
upload_start = gpu_end;
|
||||
});
|
||||
add_upload(upload_start, device_addr_out + range_size);
|
||||
});
|
||||
total_size_bytes += range_size;
|
||||
largest_copy = (std::max)(largest_copy, range_size);
|
||||
};
|
||||
memory_tracker.ForEachUploadRange(device_addr, size, [&](u64 device_addr_out, u64 range_size) {
|
||||
DAddr upload_start = device_addr_out;
|
||||
gpu_modified_ranges.ForEachInRange(device_addr_out, range_size, [&](DAddr gpu_start, DAddr gpu_end) {
|
||||
add_upload(upload_start, gpu_start);
|
||||
upload_start = gpu_end;
|
||||
if (total_size_bytes == 0) {
|
||||
return true;
|
||||
}
|
||||
const std::span<BufferCopy> copies_span(upload_copies.data(), upload_copies.size());
|
||||
UploadMemory(buffer, total_size_bytes, largest_copy, copies_span);
|
||||
any_buffer_uploaded = true;
|
||||
return false;
|
||||
} else {
|
||||
boost::container::small_vector<BufferCopy, 4> copies;
|
||||
u64 total_size_bytes = 0;
|
||||
u64 largest_copy = 0;
|
||||
DAddr buffer_start = buffer.CpuAddr();
|
||||
memory_tracker.ForEachUploadRange(device_addr, size, [&](u64 device_addr_out, u64 range_size) {
|
||||
copies.push_back(BufferCopy{
|
||||
.src_offset = total_size_bytes,
|
||||
.dst_offset = device_addr_out - buffer_start,
|
||||
.size = range_size,
|
||||
});
|
||||
total_size_bytes += range_size;
|
||||
largest_copy = (std::max)(largest_copy, range_size);
|
||||
});
|
||||
add_upload(upload_start, device_addr_out + range_size);
|
||||
});
|
||||
if (total_size_bytes == 0) {
|
||||
return true;
|
||||
if (total_size_bytes == 0) {
|
||||
return true;
|
||||
}
|
||||
const std::span<BufferCopy> copies_span(copies.data(), copies.size());
|
||||
UploadMemory(buffer, total_size_bytes, largest_copy, copies_span);
|
||||
return false;
|
||||
}
|
||||
const std::span<BufferCopy> copies_span(upload_copies.data(), upload_copies.size());
|
||||
UploadMemory(buffer, total_size_bytes, largest_copy, copies_span);
|
||||
any_buffer_uploaded = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
template <class P>
|
||||
|
||||
@@ -227,17 +227,17 @@ struct GPU::Impl {
|
||||
|
||||
/// Notify rasterizer that any caches of the specified region should be invalidated
|
||||
void InvalidateRegion(DAddr addr, u64 size, bool preserve_gpu_writes) {
|
||||
if (Settings::values.nce_invalidation_gpu_readback.GetValue()) {
|
||||
if (preserve_gpu_writes) {
|
||||
VideoCore::RasterizerInterface* rasterizer = renderer->ReadRasterizer();
|
||||
if (preserve_gpu_writes && rasterizer->MustFlushRegion(addr, size, VideoCommon::CacheType::BufferCache)) {
|
||||
if (!rasterizer->TryCacheInvalidation(addr, size)) {
|
||||
const u64 fence = RequestSyncOperation([rasterizer, addr, size] {
|
||||
rasterizer->FlushRegion(addr, size, VideoCommon::CacheType::BufferCache);
|
||||
rasterizer->OnCacheInvalidation(addr, size);
|
||||
});
|
||||
gpu_thread.TickGPU(is_async);
|
||||
WaitForSyncOperation(fence);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
gpu_thread.InvalidateRegion(addr, size);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -111,6 +114,12 @@ public:
|
||||
/// Notify rasterizer that any caches of the specified region are desync with guest
|
||||
virtual void OnCacheInvalidation(PAddr addr, u64 size) = 0;
|
||||
|
||||
virtual bool TryCacheInvalidation(DAddr addr, u64 size) {
|
||||
if (MustFlushRegion(addr, size, VideoCommon::CacheType::BufferCache)) return false;
|
||||
OnCacheInvalidation(addr, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool OnCPUWrite(PAddr addr, u64 size) = 0;
|
||||
|
||||
/// Sync memory between guest and host.
|
||||
|
||||
@@ -414,6 +414,11 @@ GraphicsPipeline* ShaderCache::BuiltPipeline(GraphicsPipeline* pipeline) const n
|
||||
if (!use_asynchronous_shaders) {
|
||||
return pipeline;
|
||||
}
|
||||
// If something is using depth, we can assume that games are not rendering anything which
|
||||
// will be used one time.
|
||||
if (maxwell3d->regs.zeta_enable) {
|
||||
return nullptr;
|
||||
}
|
||||
// If games are using a small index count, we can assume these are full screen quads.
|
||||
// Usually these shaders are only used once for building textures so we can assume they
|
||||
// can't be built async
|
||||
|
||||
@@ -447,7 +447,9 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
|
||||
driver_id == VK_DRIVER_ID_AMD_OPEN_SOURCE ||
|
||||
driver_id == VK_DRIVER_ID_MESA_RADV ||
|
||||
driver_id == VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS ||
|
||||
driver_id == VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA,
|
||||
driver_id == VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA ||
|
||||
driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY ||
|
||||
driver_id == VK_DRIVER_ID_MESA_TURNIP,
|
||||
|
||||
.has_broken_spirv_clamp = driver_id == VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS,
|
||||
.has_broken_spirv_position_input = driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY,
|
||||
@@ -787,6 +789,11 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
|
||||
if (!use_asynchronous_shaders) {
|
||||
return pipeline;
|
||||
}
|
||||
// If something is using depth, we can assume that games are not rendering anything which
|
||||
// will be used one time.
|
||||
if (maxwell3d->regs.zeta_enable) {
|
||||
return nullptr;
|
||||
}
|
||||
// If games are using a small index count, we can assume these are full screen quads.
|
||||
// Usually these shaders are only used once for building textures so we can assume they
|
||||
// can't be built async
|
||||
|
||||
@@ -798,6 +798,18 @@ void RasterizerVulkan::OnCacheInvalidation(DAddr addr, u64 size) {
|
||||
pipeline_cache.InvalidateRegion(addr, size);
|
||||
}
|
||||
|
||||
bool RasterizerVulkan::TryCacheInvalidation(DAddr addr, u64 size) {
|
||||
if (addr == 0 || size == 0) return true;
|
||||
{
|
||||
std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex};
|
||||
if (buffer_cache.IsRegionGpuModified(addr, size)) return false;
|
||||
texture_cache.WriteMemory(addr, size);
|
||||
buffer_cache.WriteMemory(addr, size);
|
||||
}
|
||||
pipeline_cache.InvalidateRegion(addr, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RasterizerVulkan::InvalidateGPUCache() {
|
||||
gpu.InvalidateGPUCache();
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ public:
|
||||
VideoCommon::CacheType which = VideoCommon::CacheType::All) override;
|
||||
void InnerInvalidation(std::span<const std::pair<DAddr, std::size_t>> sequences) override;
|
||||
void OnCacheInvalidation(DAddr addr, u64 size) override;
|
||||
bool TryCacheInvalidation(DAddr addr, u64 size) override;
|
||||
bool OnCPUWrite(DAddr addr, u64 size) override;
|
||||
void InvalidateGPUCache() override;
|
||||
void UnmapMemory(DAddr addr, u64 size) override;
|
||||
|
||||
@@ -2797,9 +2797,9 @@ void MainWindow::IncrementInstallProgress() {
|
||||
|
||||
void MainWindow::OnMenuInstallToNAND() {
|
||||
const QString file_filter =
|
||||
tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive "
|
||||
"(*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge "
|
||||
"Image (*.xci)");
|
||||
tr("Installable Switch File (*.nca *.nsp *.nsz *.xci *.xcz);;Nintendo Content Archive "
|
||||
"(*.nca);;Nintendo Submission Package (*.nsp *.nsz);;NX Cartridge "
|
||||
"Image (*.xci *.xcz)");
|
||||
|
||||
QStringList filenames = QFileDialog::getOpenFileNames(
|
||||
this, tr("Install Files"), QString::fromStdString(UISettings::values.roms_path),
|
||||
@@ -2856,7 +2856,10 @@ void MainWindow::OnMenuInstallToNAND() {
|
||||
QFuture<ContentManager::InstallResult> future;
|
||||
ContentManager::InstallResult result;
|
||||
|
||||
if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
|
||||
if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive) ||
|
||||
file.endsWith(QStringLiteral("nsz"), Qt::CaseInsensitive) ||
|
||||
file.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) ||
|
||||
file.endsWith(QStringLiteral("xcz"), Qt::CaseInsensitive)) {
|
||||
const auto progress_callback = [this](size_t size, size_t progress) {
|
||||
emit UpdateInstallProgress();
|
||||
if (install_progress->wasCanceled()) {
|
||||
|
||||
Reference in New Issue
Block a user