mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-11-08 04:22:39 +01:00
Merge branch 'master' into 4480-with-fix-John
This commit is contained in:
commit
f3e9c9976c
251 changed files with 31496 additions and 21100 deletions
|
|
@ -34,6 +34,13 @@ import org.yuzu.yuzu_emu.applets.web.WebBrowser
|
|||
* with the native side of the Yuzu code.
|
||||
*/
|
||||
object NativeLibrary {
|
||||
@Keep
|
||||
class RuntimeCheat(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val enabled: Boolean,
|
||||
val isMaster: Boolean
|
||||
)
|
||||
@Keep
|
||||
data class UpdateResult(
|
||||
var tag: String = "",
|
||||
|
|
@ -197,6 +204,9 @@ object NativeLibrary {
|
|||
*/
|
||||
external fun isPaused(): Boolean
|
||||
|
||||
external fun getRuntimeCheats(): Array<RuntimeCheat>
|
||||
external fun setRuntimeCheatEnabled(id: Int, enabled: Boolean): Boolean
|
||||
|
||||
/**
|
||||
* Returns the performance stats for the current game
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
|||
FASTMEM("cpuopt_fastmem"),
|
||||
FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives"),
|
||||
CORE_SYNC_CORE_SPEED("sync_core_speed"),
|
||||
ENABLE_CHEATS_BY_DEFAULT("enable_cheats_by_default"),
|
||||
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"),
|
||||
|
|
|
|||
|
|
@ -316,6 +316,13 @@ abstract class SettingsItem(
|
|||
descriptionId = R.string.use_sync_core_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SwitchSetting(
|
||||
BooleanSetting.ENABLE_CHEATS_BY_DEFAULT,
|
||||
titleId = R.string.enable_cheats_by_default,
|
||||
descriptionId = R.string.enable_cheats_by_default_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.REGION_INDEX,
|
||||
|
|
@ -859,13 +866,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,
|
||||
|
|
|
|||
|
|
@ -510,6 +510,7 @@ class SettingsFragmentPresenter(
|
|||
add(IntSetting.FAST_CPU_TIME.key)
|
||||
add(IntSetting.FAST_GPU_TIME.key)
|
||||
add(BooleanSetting.CORE_SYNC_CORE_SPEED.key)
|
||||
add(BooleanSetting.ENABLE_CHEATS_BY_DEFAULT.key)
|
||||
|
||||
add(IntSetting.MEMORY_LAYOUT.key)
|
||||
add(BooleanSetting.USE_CUSTOM_CPU_TICKS.key)
|
||||
|
|
@ -554,7 +555,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)
|
||||
|
|
|
|||
|
|
@ -79,9 +79,8 @@ class DriverFetcherFragment : Fragment() {
|
|||
IntRange(100, 599) to "Unsupported",
|
||||
IntRange(600, 639) to "Mr. Purple EOL-24.3.4",
|
||||
IntRange(640, 699) to "Mr. Purple T19",
|
||||
IntRange(700, 710) to "KIMCHI 25.2.0_r5",
|
||||
IntRange(711, 799) to "Mr. Purple T23",
|
||||
IntRange(800, 899) to "GameHub Adreno 8xx",
|
||||
IntRange(700, 710) to "Mr. Purple T29",
|
||||
IntRange(711, 899) to "Mr. Purple T30",
|
||||
IntRange(900, Int.MAX_VALUE) to "Unsupported"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -734,6 +734,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
|
||||
binding.inGameMenu.menu.findItem(R.id.menu_quick_settings)?.isVisible =
|
||||
BooleanSetting.ENABLE_QUICK_SETTINGS.getBoolean()
|
||||
binding.inGameMenu.menu.findItem(R.id.menu_cheats)?.isVisible = false
|
||||
|
||||
binding.pausedIcon.setOnClickListener {
|
||||
if (this::emulationState.isInitialized && emulationState.isPaused) {
|
||||
|
|
@ -817,6 +818,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
|
||||
R.id.menu_load_amiibo -> handleLoadAmiiboSelection()
|
||||
|
||||
R.id.menu_cheats -> {
|
||||
binding.drawerLayout.closeDrawer(binding.inGameMenu)
|
||||
showRuntimeCheatDialog()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.menu_controls -> {
|
||||
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
||||
null,
|
||||
|
|
@ -962,6 +969,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
|
||||
emulationState.updateSurface()
|
||||
|
||||
binding.inGameMenu.menu.findItem(R.id.menu_cheats)?.isVisible =
|
||||
NativeLibrary.getRuntimeCheats().isNotEmpty()
|
||||
updateShowStatsOverlay()
|
||||
updateSocOverlay()
|
||||
|
||||
|
|
@ -987,6 +996,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
}
|
||||
emulationViewModel.isEmulationStopping.collect(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
binding.inGameMenu.menu.findItem(R.id.menu_cheats)?.isVisible = false
|
||||
binding.loadingText.setText(R.string.shutting_down)
|
||||
ViewUtils.showView(binding.loadingIndicator)
|
||||
ViewUtils.hideView(binding.inputContainer)
|
||||
|
|
@ -1119,6 +1129,45 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
NativePostProcessing.persist()
|
||||
}
|
||||
|
||||
private fun showRuntimeCheatDialog() {
|
||||
val cheats = NativeLibrary.getRuntimeCheats()
|
||||
if (cheats.isEmpty()) return
|
||||
|
||||
val names = cheats.map { it.name.ifEmpty { getString(R.string.unnamed_cheat) } }.toTypedArray()
|
||||
lateinit var dialog: androidx.appcompat.app.AlertDialog
|
||||
dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.cheats)
|
||||
.setMultiChoiceItems(names, cheats.map { it.enabled }.toBooleanArray()) { _, which, enabled ->
|
||||
if (cheats[which].isMaster) {
|
||||
dialog.listView.setItemChecked(which, true)
|
||||
} else if (!NativeLibrary.setRuntimeCheatEnabled(cheats[which].id, enabled)) {
|
||||
dialog.listView.setItemChecked(which, !enabled)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.disable_all_cheats) { _, _ ->
|
||||
cheats.forEach {
|
||||
if (!it.isMaster) NativeLibrary.setRuntimeCheatEnabled(it.id, false)
|
||||
}
|
||||
}
|
||||
.setNeutralButton(R.string.enable_all_cheats) { _, _ ->
|
||||
cheats.forEach {
|
||||
if (!it.isMaster) NativeLibrary.setRuntimeCheatEnabled(it.id, true)
|
||||
}
|
||||
}
|
||||
.setPositiveButton(R.string.done, null)
|
||||
.create()
|
||||
dialog.setOnShowListener {
|
||||
val listView = dialog.listView
|
||||
fun updateMasterVisuals() = repeat(listView.childCount) { index ->
|
||||
listView.getChildAt(index).alpha =
|
||||
if (cheats[listView.firstVisiblePosition + index].isMaster) 0.38f else 1f
|
||||
}
|
||||
listView.setOnScrollChangeListener { _, _, _, _, _ -> updateMasterVisuals() }
|
||||
updateMasterVisuals()
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun addQuickSettings() {
|
||||
binding.quickSettingsSheet.apply {
|
||||
val container = binding.quickSettingsSheet.findViewById<ViewGroup>(R.id.quick_settings_container)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ extern "C" {
|
|||
#include "core/file_sys/submission_package.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
#include "core/file_sys/vfs/vfs_real.h"
|
||||
#include "core/memory/dmnt_cheat_types.h"
|
||||
#include "core/frontend/applets/cabinet.h"
|
||||
#include "core/frontend/applets/controller.h"
|
||||
#include "core/frontend/applets/error.h"
|
||||
|
|
@ -946,6 +947,31 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass claz
|
|||
return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
|
||||
}
|
||||
|
||||
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getRuntimeCheats(JNIEnv* env, jclass clazz) {
|
||||
const auto cheats = EmulationSession::GetInstance().System().GetRuntimeCheats();
|
||||
const auto runtime_cheat_class = env->FindClass("org/yuzu/yuzu_emu/NativeLibrary$RuntimeCheat");
|
||||
const auto constructor = env->GetMethodID(runtime_cheat_class, "<init>", "(ILjava/lang/String;ZZ)V");
|
||||
auto result = env->NewObjectArray(static_cast<jsize>(cheats.size()), runtime_cheat_class, nullptr);
|
||||
for (jsize i = 0; i < static_cast<jsize>(cheats.size()); ++i) {
|
||||
const auto& cheat = cheats[i];
|
||||
const auto name = Common::Android::ToJString(env, cheat.name);
|
||||
const auto item = env->NewObject(runtime_cheat_class, constructor, static_cast<jint>(cheat.id), name,
|
||||
static_cast<jboolean>(cheat.enabled),
|
||||
static_cast<jboolean>(cheat.is_master));
|
||||
env->SetObjectArrayElement(result, i, item);
|
||||
env->DeleteLocalRef(name);
|
||||
env->DeleteLocalRef(item);
|
||||
}
|
||||
env->DeleteLocalRef(runtime_cheat_class);
|
||||
return result;
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_setRuntimeCheatEnabled(JNIEnv* env, jclass clazz,
|
||||
jint id, jboolean enabled) {
|
||||
return static_cast<jboolean>(EmulationSession::GetInstance().System().SetCheatEnabled(
|
||||
static_cast<u32>(id), static_cast<bool>(enabled)));
|
||||
}
|
||||
|
||||
jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureBuffer(JNIEnv* env, jclass clazz) {
|
||||
using namespace VideoCore::Capture;
|
||||
|
||||
|
|
|
|||
30
src/android/app/src/main/res/drawable/ic_forgejo.xml
Normal file
30
src/android/app/src/main/res/drawable/ic_forgejo.xml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="212"
|
||||
android:viewportHeight="212"
|
||||
android:width="212dp"
|
||||
android:height="212dp">
|
||||
<group
|
||||
android:translateX="6"
|
||||
android:translateY="6">
|
||||
<path
|
||||
android:pathData="M58 168v-98a50 50 0 0 1 50 -50h20"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="25" />
|
||||
<path
|
||||
android:pathData="M58 168v-30a50 50 0 0 1 50 -50h20"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="25" />
|
||||
<path
|
||||
android:pathData="M160 20A18 18 0 0 1 124 20A18 18 0 0 1 160 20Z"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="15" />
|
||||
<path
|
||||
android:pathData="M160 88A18 18 0 0 1 124 88A18 18 0 0 1 160 88Z"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="15" />
|
||||
<path
|
||||
android:pathData="M76 180A18 18 0 0 1 40 180A18 18 0 0 1 76 180Z"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="15" />
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="98dp"
|
||||
android:height="96dp"
|
||||
android:viewportWidth="98"
|
||||
android:viewportHeight="96">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M48.854,0C21.839,0 0,22 0,49.217c0,21.756 13.993,40.172 33.405,46.69 2.427,0.49 3.316,-1.059 3.316,-2.362 0,-1.141 -0.08,-5.052 -0.08,-9.127 -13.59,2.934 -16.42,-5.867 -16.42,-5.867 -2.184,-5.704 -5.42,-7.17 -5.42,-7.17 -4.448,-3.015 0.324,-3.015 0.324,-3.015 4.934,0.326 7.523,5.052 7.523,5.052 4.367,7.496 11.404,5.378 14.235,4.074 0.404,-3.178 1.699,-5.378 3.074,-6.6 -10.839,-1.141 -22.243,-5.378 -22.243,-24.283 0,-5.378 1.94,-9.778 5.014,-13.2 -0.485,-1.222 -2.184,-6.275 0.486,-13.038 0,0 4.125,-1.304 13.426,5.052a46.97,46.97 0,0 1,12.214 -1.63c4.125,0 8.33,0.571 12.213,1.63 9.302,-6.356 13.427,-5.052 13.427,-5.052 2.67,6.763 0.97,11.816 0.485,13.038 3.155,3.422 5.015,7.822 5.015,13.2 0,18.905 -11.404,23.06 -22.324,24.283 1.78,1.548 3.316,4.481 3.316,9.126 0,6.6 -0.08,11.897 -0.08,13.526 0,1.304 0.89,2.853 3.316,2.364 19.412,-6.52 33.405,-24.935 33.405,-46.691C97.707,22 75.788,0 48.854,0z" />
|
||||
</vector>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
|
@ -250,7 +250,7 @@
|
|||
style="@style/EdenButton.Secondary"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
app:icon="@drawable/ic_github"
|
||||
app:icon="@drawable/ic_forgejo"
|
||||
app:iconGravity="textStart"
|
||||
app:iconSize="24dp"
|
||||
app:iconPadding="0dp" />
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@
|
|||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
app:icon="@drawable/ic_github"
|
||||
app:icon="@drawable/ic_forgejo"
|
||||
app:iconSize="24dp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@
|
|||
android:icon="@drawable/ic_nfc"
|
||||
android:title="@string/load_amiibo" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_cheats"
|
||||
android:icon="@drawable/ic_code"
|
||||
android:title="@string/cheats" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_overlay_controls"
|
||||
|
|
|
|||
|
|
@ -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,10 +587,8 @@
|
|||
<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="fix_bloom_effects_description">يزيل تأثير التوهج في لعبة Burnout.</string>
|
||||
<string name="emulate_bgr565">محاكاة BGR565</string>
|
||||
<string name="emulate_bgr565_description">يُصلح مشاكل انعكاس الألوان في الألعاب أو ظهور تشوهات غريبة أو ظلال غريبة.</string>
|
||||
<string name="rescale_hack">تفعيل ميزة إعادة التحجيم القديمة</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,10 +821,16 @@
|
|||
<!-- 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>
|
||||
<string name="cheats">رموز الغش</string>
|
||||
<string name="enable_all_cheats">تفعيل جميع رموز الغش</string>
|
||||
<string name="disable_all_cheats">تعطيل جميع رموز الغش</string>
|
||||
<string name="unnamed_cheat">رمز الغش بدون اسم</string>
|
||||
<string name="enable_cheats_by_default">تفعيل رموز الغش بشكل افتراضي</string>
|
||||
<string name="enable_cheats_by_default_description">تفعيل رموز الغش الاعتيادية عند بدء اللعبة</string>
|
||||
<string name="enable_quick_settings">تفعيل الإعدادات السريعة</string>
|
||||
<string name="enable_quick_settings_description">السماح بالوصول إلى قائمة الإعدادات السريعة عن طريق التمرير و زر القائمة</string>
|
||||
<string name="preferences_audio">الصوت</string>
|
||||
|
|
@ -871,12 +875,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 +889,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>
|
||||
|
|
|
|||
|
|
@ -531,7 +531,6 @@
|
|||
<string name="skip_cpu_inner_invalidation">Omitir invalidación interna de la CPU</string>
|
||||
<string name="skip_cpu_inner_invalidation_description">Omite ciertas invalidaciones de caché de la CPU durante las actualizaciones de memoria, lo que reduce el uso de la CPU y mejora su rendimiento. Esto puede causar fallos o bloqueos en algunos juegos.</string>
|
||||
<string name="fix_bloom_effects">Arreglar los efectos de resplandor</string>
|
||||
<string name="fix_bloom_effects_description">Reduce el efecto de resplandor en LA/EOW (Adreno A6XX - A7XX/ Turnip), elimina el resplandor en Burnout. Advertencia: puede causar artefactos gráficos en otros juegos.</string>
|
||||
<string name="emulate_bgr565">Emular BGR565</string>
|
||||
<string name="emulate_bgr565_description">Soluciona problemas con colores invertidos en juegos, artefactos o sombras extrañas.</string>
|
||||
<string name="rescale_hack">Activar la pasada de reescalado heredada</string>
|
||||
|
|
|
|||
|
|
@ -577,7 +577,6 @@
|
|||
<string name="skip_cpu_inner_invalidation">Пропустить внутреннюю инвалидацию ЦП</string>
|
||||
<string name="skip_cpu_inner_invalidation_description">Пропускает некоторые инвалидации кэша на стороне ЦП при обновлениях памяти, уменьшая нагрузку на процессор и повышая производительность. Может вызывать сбои в некоторых играх.</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>
|
||||
<string name="emulate_bgr565_description">Исправляет проблемы с инвертированными цветами в играх, а также со странными артефактами или некорректными тенями.</string>
|
||||
<string name="rescale_hack">Включить старый метод изменения разрешения</string>
|
||||
|
|
|
|||
|
|
@ -493,7 +493,6 @@
|
|||
<string name="skip_cpu_inner_invalidation">Пропустити внутрішнє інвалідування CPU</string>
|
||||
<string name="skip_cpu_inner_invalidation_description">Пропускає деякі інвалідації кешу на стороні CPU під час оновлення пам\'яті, зменшуючи навантаження на процесор і покращуючи продуктивність. Може спричинити збої в деяких іграх.</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>
|
||||
<string name="emulate_bgr565_description">Виправляє проблеми з інвертованими кольорами в іграх або дивними артефактами чи тінями.</string>
|
||||
<string name="rescale_hack">Увімкнути застаріле масштабування</string>
|
||||
|
|
|
|||
|
|
@ -578,7 +578,7 @@
|
|||
<string name="skip_cpu_inner_invalidation">跳过CPU内部无效化</string>
|
||||
<string name="skip_cpu_inner_invalidation_description">在更新内存时跳过某些 CPU 端的缓存失效操作,从而降低 CPU 占用率并提升性能。可能会在某些游戏中引发故障点或崩溃。</string>
|
||||
<string name="fix_bloom_effects">修复 Bloom 效果</string>
|
||||
<string name="fix_bloom_effects_description">减少《智慧的再现》和《众神的三角力量2》(Adreno A6XX - A7XX/ Turnip)中的 bloom 模糊,并移除《Burnout》中的 bloom 效果。警告:可能会导致在其他游戏中出现图形异常。</string>
|
||||
<string name="fix_bloom_effects_description">去除《火爆狂飙》中的泛光特效。</string>
|
||||
<string name="emulate_bgr565">模拟 BGR565</string>
|
||||
<string name="emulate_bgr565_description">修复游戏中的颜色反转或是异常的画面瑕疵或阴影问题</string>
|
||||
<string name="rescale_hack">启用旧版缩放处理</string>
|
||||
|
|
@ -815,6 +815,12 @@
|
|||
<string name="preferences_graphics">图形</string>
|
||||
<string name="preferences_graphics_description">精度等级、分辨率及着色器缓存</string>
|
||||
<string name="quick_settings">快速设置</string>
|
||||
<string name="cheats">作弊</string>
|
||||
<string name="enable_all_cheats">开启所有作弊</string>
|
||||
<string name="disable_all_cheats">关闭所有作弊</string>
|
||||
<string name="unnamed_cheat">未命名的作弊</string>
|
||||
<string name="enable_cheats_by_default">默认开启作弊</string>
|
||||
<string name="enable_cheats_by_default_description">在游戏启动时启用常规作弊</string>
|
||||
<string name="enable_quick_settings">启用快速设置</string>
|
||||
<string name="enable_quick_settings_description">允许通过滑动和菜单按钮访问快速设置菜单</string>
|
||||
<string name="preferences_audio">声音</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">授予權限以使用模擬器的特定功能</string>
|
||||
<string name="permissions">授予通知權限</string>
|
||||
<string name="permissions_description">授予 Eden 傳送通知的權限以使用模擬器的特定功能</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">安卓版本</string>
|
||||
<string name="android_security_patch">安卓安全性修補程式等級</string>
|
||||
<string name="android_version">Android 版本</string>
|
||||
<string name="android_security_patch">Android 安全性修補程式等級</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>
|
||||
|
|
@ -451,7 +451,7 @@
|
|||
|
||||
<!-- Applet launcher strings -->
|
||||
<string name="qlaunch_applet">Qlaunch</string>
|
||||
<string name="qlaunch_description">從 Switch 系統主畫面啟動應用程式(目前僅支援英文)</string>
|
||||
<string name="qlaunch_description">從 Switch 系統主畫面啟動應用程式(目前模擬區域尚不支援臺灣、澳洲、中國、南韓啟動 Qlaunch)</string>
|
||||
<string name="applets">小程式啟動器</string>
|
||||
<string name="applets_description">使用已安裝的韌體啟動系統小程式</string>
|
||||
<string name="applets_error_firmware">未安裝韌體</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">移除《橫衝直撞:狂飆樂園重製版》中的光暈效果</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">控制擴展動態狀態中可使用的功能數量,較高的數值可根據驅動程式支援的動態狀態減少管線編譯的次數</string>
|
||||
<string name="dyna_state_description">控制 Vulkan 擴展動態狀態中可使用的功能數量,較高的數值可根據驅動程式支援的動態狀態減少管線編譯的次數</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>
|
||||
|
|
@ -817,6 +815,12 @@
|
|||
<string name="preferences_graphics">圖形</string>
|
||||
<string name="preferences_graphics_description">準確度層級、解析度及著色器快取</string>
|
||||
<string name="quick_settings">快捷設定</string>
|
||||
<string name="cheats">金手指</string>
|
||||
<string name="enable_all_cheats">啟用所有金手指</string>
|
||||
<string name="disable_all_cheats">停用所有金手指</string>
|
||||
<string name="unnamed_cheat">未命名的金手指</string>
|
||||
<string name="enable_cheats_by_default">預設啟用金手指</string>
|
||||
<string name="enable_cheats_by_default_description">在遊戲啟動時啟用一般金手指</string>
|
||||
<string name="enable_quick_settings">啟用快捷設定</string>
|
||||
<string name="enable_quick_settings_description">允許透過滑動和選單按鈕存取快捷設定選單</string>
|
||||
<string name="preferences_audio">音訊</string>
|
||||
|
|
@ -833,9 +837,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>
|
||||
|
|
@ -891,10 +895,10 @@
|
|||
<string name="save_data_deleted_successfully">儲存資料已成功刪除</string>
|
||||
<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="mods_and_cheats">Mod 和金手指</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="addon_notice_description">若要安裝 Mod 和金手指,您必須選擇一個包含 cheats/、romfs/ 或 exefs/ 的目錄。我們無法驗證這些內容是否與您的遊戲相容,所以請謹慎安裝!</string>
|
||||
<string name="invalid_directory">無效的目錄</string>
|
||||
<!-- \"cheats/" "romfs/" and \"exefs/ should not be translated -->
|
||||
<string name="invalid_directory_description">請確保您選取的目錄包含 cheats/、romfs/ 或 exefs/ 資料夾,然後再試一次</string>
|
||||
|
|
@ -981,7 +985,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,6 +1132,16 @@
|
|||
<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>
|
||||
|
|
@ -1136,14 +1150,17 @@
|
|||
<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">質感設計3</string>
|
||||
<string name="app_settings">Eden介面設定</string>
|
||||
<string name="theme_material_you">Material You</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>
|
||||
|
|
@ -1152,18 +1169,26 @@
|
|||
|
||||
<!-- Black backgrounds theme -->
|
||||
<string name="use_black_backgrounds">黑色背景</string>
|
||||
<string name="use_black_backgrounds_description">使用深色主題時,套用黑色背景。</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>
|
||||
|
||||
<!-- 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>
|
||||
|
|
@ -1185,14 +1210,28 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -498,7 +498,7 @@
|
|||
<string name="user_data_import_failed_description">Make sure the user data folders are at the root of the zip folder and contain a config file at config/config.ini and try again.</string>
|
||||
<string name="discord_link" translatable="false">https://discord.gg/HstXbPch7X</string>
|
||||
<string name="stoat_link" translatable="false">https://stt.gg/qKgFEAbH</string>
|
||||
<string name="x_link" translatable="false">https://nitter.poast.org/edenemuofficial</string>
|
||||
<string name="x_link" translatable="false">https://x.com/edenemuofficial</string>
|
||||
<string name="website_link" translatable="false">https://eden-emu.dev</string>
|
||||
<string name="github_link" translatable="false">https://git.eden-emu.dev/eden-emu</string>
|
||||
|
||||
|
|
@ -593,10 +593,8 @@
|
|||
<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="fix_bloom_effects_description">Removes bloom in Burnout.</string>
|
||||
<string name="emulate_bgr565">Emulate BGR565</string>
|
||||
<string name="emulate_bgr565_description">Fixes problems with inverted colors in games or strange artifacts or strange shadows.</string>
|
||||
<string name="rescale_hack">Enable Legacy Rescale Pass</string>
|
||||
|
|
@ -842,6 +840,12 @@
|
|||
<string name="preferences_graphics">Graphics</string>
|
||||
<string name="preferences_graphics_description">Accuracy level, resolution, shader cache</string>
|
||||
<string name="quick_settings">Quick Settings</string>
|
||||
<string name="cheats">Cheats</string>
|
||||
<string name="enable_all_cheats">Enable All Cheats</string>
|
||||
<string name="disable_all_cheats">Disable All Cheats</string>
|
||||
<string name="unnamed_cheat">Unnamed Cheat</string>
|
||||
<string name="enable_cheats_by_default">Enable cheats by default</string>
|
||||
<string name="enable_cheats_by_default_description">Enable regular cheats when a game starts</string>
|
||||
<string name="enable_quick_settings">Enable Quick Settings</string>
|
||||
<string name="enable_quick_settings_description">Allow access to quick settings menu via swipe and menu button</string>
|
||||
<string name="preferences_audio">Audio</string>
|
||||
|
|
|
|||
125
src/common/controller_console_state.h
Normal file
125
src/common/controller_console_state.h
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "common/settings.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
/// State shared between the console-mode engine (in the main window) and the Controllers tab:
|
||||
/// the UI reads and requests, the engine applies on its next tick.
|
||||
struct ControllerConsoleState {
|
||||
std::mutex mutex;
|
||||
/// Live pads as (display name, identity): possible handheld controls first
|
||||
std::vector<std::pair<std::string, std::string>> pads;
|
||||
/// How many of `pads` could be a handheld's controls (the first ones)
|
||||
std::size_t builtin_pads = 0;
|
||||
/// Set by the Identify button: capture the next pad that has a button pressed
|
||||
bool identify_armed = false;
|
||||
std::chrono::steady_clock::time_point identify_armed_at{};
|
||||
|
||||
/// One assigned player, as the Controllers tab lists it
|
||||
struct PlayerRow {
|
||||
int player; ///< 0-based player index
|
||||
std::string name;
|
||||
bool active; ///< the pad has an input held (or was just tapped)
|
||||
bool leaving; ///< the controller left and nothing took its place: shown red, then gone
|
||||
int flash; ///< 0 none, 1 green (a controller took the player), 2 red (one left)
|
||||
long long flash_since; ///< ms since epoch; the flash blinks for 1.5 seconds from here
|
||||
std::string profile; ///< the input profile the player's controller carries ("" none)
|
||||
/// Turn Off: 0 not Bluetooth, 1 not yet (identical ones not told apart), 2 yes
|
||||
int turn_off = 0;
|
||||
/// The player its controller started as (-1: this one); a reconnect's guessed player
|
||||
int origin = -1;
|
||||
bool guessed = false;
|
||||
/// The name given to its controller ("" none): `name` is then "Name (Xbox)"
|
||||
std::string custom_name;
|
||||
/// Battery percent (-1 unknown), charging, wired
|
||||
int battery = -1;
|
||||
bool charging = false;
|
||||
bool wired = false;
|
||||
};
|
||||
/// True while an automatic assignment mode runs (the engine owns the players)
|
||||
bool engine_active = false;
|
||||
/// The players the engine assigned, in player order
|
||||
std::vector<PlayerRow> players;
|
||||
/// The handheld's own pad: its name ("" when not identified) and whether it holds a player
|
||||
std::string handheld_name;
|
||||
bool handheld_has_player = false;
|
||||
/// The Controllers tab asked to remove the handheld's controller (its ✕)
|
||||
bool builtin_remove_request = false;
|
||||
/// The removed controller auto-detect leaves alone, by name ("" when not connected)
|
||||
std::string handheld_blocked_name;
|
||||
/// Swaps requested from the settings and the overlay, for this session only
|
||||
std::vector<std::pair<int, int>> swap_requests;
|
||||
/// Players whose Bluetooth controller is to be turned off; the player is freed
|
||||
std::vector<int> remove_requests;
|
||||
/// Names given to players' controllers from the Controllers tab ("" takes it off)
|
||||
std::vector<std::pair<int, std::string>> rename_requests;
|
||||
/// Players whose controller the Controllers tab asked to rumble (a click on its row)
|
||||
std::vector<int> rumble_requests;
|
||||
/// An input profile picked for a player, put on its controller's set. No name takes it off
|
||||
/// (`restore`: the set's mappings from before this session's first profile come back).
|
||||
struct ProfileRequest {
|
||||
int player;
|
||||
std::string name;
|
||||
bool restore = false;
|
||||
};
|
||||
std::vector<ProfileRequest> profile_requests;
|
||||
/// Bypass Steam Input took effect at startup: SDL opens the real controllers
|
||||
std::atomic<bool> steam_bypassed{false};
|
||||
/// Whether Steam's virtual pads are around, and the models Steam hid that Eden opened again
|
||||
/// for their motion (vendor << 16 | product)
|
||||
std::atomic<bool> steam_pads_present{false};
|
||||
std::set<unsigned> steam_hidden_models;
|
||||
/// Bumped when the engine reloads the emulated controllers: the player tabs refresh
|
||||
unsigned long long generation = 0;
|
||||
/// Use Any Controller as Joy-Cons: each player playing as a single Joy-Con (0 no, 1 held
|
||||
/// sideways, 2 upright). Its mapping is the engine's then: the player tab can't edit it.
|
||||
std::array<int, 10> joycon_held{};
|
||||
};
|
||||
|
||||
[[nodiscard]] inline ControllerConsoleState& GetControllerConsoleState() {
|
||||
static ControllerConsoleState instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// True when Eden was launched through Steam: Steam Input then wraps the controllers in
|
||||
/// virtual ones
|
||||
[[nodiscard]] inline bool RunningUnderSteam() {
|
||||
static const bool launched =
|
||||
std::getenv("SteamGameId") != nullptr || std::getenv("SteamAppId") != nullptr;
|
||||
return launched;
|
||||
}
|
||||
|
||||
/// True when Steam Input's virtual pads are the players: under Steam, not bypassed, with
|
||||
/// Steam's pads around. Otherwise the real controllers are, as outside Steam.
|
||||
[[nodiscard]] inline bool SteamPadsInUse() {
|
||||
const auto& state = GetControllerConsoleState();
|
||||
return RunningUnderSteam() && !state.steam_bypassed && state.steam_pads_present;
|
||||
}
|
||||
|
||||
/// True for a controller model Steam hid that Eden opened again for its motion (never a player)
|
||||
[[nodiscard]] inline bool SteamHidModel(unsigned vendor, unsigned product) {
|
||||
auto& state = GetControllerConsoleState();
|
||||
std::scoped_lock lock{state.mutex};
|
||||
return state.steam_hidden_models.contains(vendor << 16 | product);
|
||||
}
|
||||
|
||||
/// Debug Probes: detailed controller traces in the log, for bug reports
|
||||
[[nodiscard]] inline bool ControllerProbes() {
|
||||
return Settings::values.console_debug_probes.GetValue();
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
|
|
@ -7,18 +7,25 @@
|
|||
#include <vector>
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/bit_util.h"
|
||||
#include "common/fs/file.h"
|
||||
#include "common/fs/fs.h"
|
||||
#include "common/fs/fs_types.h"
|
||||
#ifdef __ANDROID__
|
||||
#include "common/fs/fs_android.h"
|
||||
#endif
|
||||
#include "common/logging.h"
|
||||
#include "common/literals.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#include <share.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#endif
|
||||
|
||||
#ifdef _MSC_VER
|
||||
|
|
@ -36,13 +43,13 @@ namespace {
|
|||
#ifdef _WIN32
|
||||
|
||||
/**
|
||||
* Converts the file access mode and file type enums to a file access mode wide string.
|
||||
*
|
||||
* @param mode File access mode
|
||||
* @param type File type
|
||||
*
|
||||
* @returns A pointer to a wide string representing the file access mode.
|
||||
*/
|
||||
* Converts the file access mode and file type enums to a file access mode wide string.
|
||||
*
|
||||
* @param mode File access mode
|
||||
* @param type File type
|
||||
*
|
||||
* @returns A pointer to a wide string representing the file access mode.
|
||||
*/
|
||||
[[nodiscard]] constexpr const wchar_t* AccessModeToWStr(FileAccessMode mode, FileType type) {
|
||||
switch (type) {
|
||||
case FileType::BinaryFile:
|
||||
|
|
@ -79,12 +86,12 @@ namespace {
|
|||
}
|
||||
|
||||
/**
|
||||
* Converts the file-share access flag enum to a Windows defined file-share access flag.
|
||||
*
|
||||
* @param flag File-share access flag
|
||||
*
|
||||
* @returns Windows defined file-share access flag.
|
||||
*/
|
||||
* Converts the file-share access flag enum to a Windows defined file-share access flag.
|
||||
*
|
||||
* @param flag File-share access flag
|
||||
*
|
||||
* @returns Windows defined file-share access flag.
|
||||
*/
|
||||
[[nodiscard]] constexpr int ToWindowsFileShareFlag(FileShareFlag flag) {
|
||||
switch (flag) {
|
||||
case FileShareFlag::ShareNone:
|
||||
|
|
@ -102,13 +109,13 @@ namespace {
|
|||
#else
|
||||
|
||||
/**
|
||||
* Converts the file access mode and file type enums to a file access mode string.
|
||||
*
|
||||
* @param mode File access mode
|
||||
* @param type File type
|
||||
*
|
||||
* @returns A pointer to a string representing the file access mode.
|
||||
*/
|
||||
* Converts the file access mode and file type enums to a file access mode string.
|
||||
*
|
||||
* @param mode File access mode
|
||||
* @param type File type
|
||||
*
|
||||
* @returns A pointer to a string representing the file access mode.
|
||||
*/
|
||||
[[nodiscard]] constexpr const char* AccessModeToStr(FileAccessMode mode, FileType type) {
|
||||
switch (type) {
|
||||
case FileType::BinaryFile:
|
||||
|
|
@ -147,12 +154,12 @@ namespace {
|
|||
#endif
|
||||
|
||||
/**
|
||||
* Converts the seek origin enum to a seek origin integer.
|
||||
*
|
||||
* @param origin Seek origin
|
||||
*
|
||||
* @returns Seek origin integer.
|
||||
*/
|
||||
* Converts the seek origin enum to a seek origin integer.
|
||||
*
|
||||
* @param origin Seek origin
|
||||
*
|
||||
* @returns Seek origin integer.
|
||||
*/
|
||||
[[nodiscard]] constexpr int ToSeekOrigin(SeekOrigin origin) {
|
||||
switch (origin) {
|
||||
case SeekOrigin::SetOrigin:
|
||||
|
|
@ -178,7 +185,7 @@ std::string ReadStringFromFile(const std::filesystem::path& path, FileType type)
|
|||
}
|
||||
|
||||
size_t WriteStringToFile(const std::filesystem::path& path, FileType type,
|
||||
std::string_view string) {
|
||||
std::string_view string) {
|
||||
if (Exists(path) && !IsFile(path)) {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -189,7 +196,7 @@ size_t WriteStringToFile(const std::filesystem::path& path, FileType type,
|
|||
}
|
||||
|
||||
size_t AppendStringToFile(const std::filesystem::path& path, FileType type,
|
||||
std::string_view string) {
|
||||
std::string_view string) {
|
||||
if (Exists(path) && !IsFile(path)) {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -244,69 +251,189 @@ FileType IOFile::GetType() const {
|
|||
return file_type;
|
||||
}
|
||||
|
||||
#if defined(__unix__)
|
||||
static int PlatformMapReadOnly(IOFile& io, const char* path) {
|
||||
io.mmap_fd = open(path, O_RDONLY);
|
||||
if (io.mmap_fd > 0) {
|
||||
struct stat st;
|
||||
fstat(io.mmap_fd, &st);
|
||||
io.mmap_size = st.st_size;
|
||||
|
||||
int map_flags = MAP_PRIVATE;
|
||||
#ifdef MAP_PREFAULT_READ
|
||||
// Prefaults reads so the final resulting pagetable from this big stupid mmap()
|
||||
// isn't comically lazily loaded, we just coalesce everything in-place for our
|
||||
// lovely mmap flags; if we didn't prefault the reads the page table will be
|
||||
// constructed in-place (i.e on a read-by-read basis) causing lovely soft-faults
|
||||
// which would nuke any performance gains.
|
||||
//
|
||||
// This of course incurs a cost in the initial mmap(2) call, but that is fine.
|
||||
map_flags |= MAP_PREFAULT_READ;
|
||||
#endif
|
||||
#ifdef MAP_NOSYNC
|
||||
// This causes physical media to not be synched to our file/memory
|
||||
// This means that if the read-only file is written to, we won't see changes
|
||||
// or we may see changes which are just funnily scattered, in any case
|
||||
// this presumes the files won't be changed during execution
|
||||
//
|
||||
// Do not ever use this on write files (if we ever support that); this will create
|
||||
// a fun amount of fragmentation on the disk.
|
||||
map_flags |= MAP_NOSYNC;
|
||||
#endif
|
||||
#if defined(NDEBUG) && defined(MAP_NOCORE)
|
||||
map_flags |= MAP_NOCORE;
|
||||
#endif
|
||||
#ifdef MAP_HUGE_2MB
|
||||
map_flags |= MAP_HUGE_2MB; //2mb pages
|
||||
#elif defined(MAP_ALIGNED)
|
||||
// File must be big enough that it's worth to super align. We can't just super-align every
|
||||
// file otherwise we will run out of alignments for actually important files :)
|
||||
// System doesn't guarantee a super alignment, but if it's available it will delete
|
||||
// about 3 layers(?) of the TLB tree for each read/write.
|
||||
// Again the cost of faults may make this negligible gains, but hey, we gotta work
|
||||
// what we gotta work with.
|
||||
map_flags |= MAP_ALIGNED(21); //2^21 = 2mb use 2MB pages
|
||||
#endif
|
||||
io.mmap_base = (u8*)mmap(nullptr, io.mmap_size, PROT_READ, map_flags, io.mmap_fd, 0);
|
||||
if (io.mmap_base == MAP_FAILED) {
|
||||
close(io.mmap_fd);
|
||||
io.mmap_fd = -1;
|
||||
} else {
|
||||
using namespace Common::Literals;
|
||||
// For small files it is acceptable to use a full readahead
|
||||
// See https://github.com/torvalds/linux/blob/e80d033851b3bc94c3d254ac66660ddd0a49d72c/include/linux/pagemap.h#L1392
|
||||
if (u64(st.st_size) >= 256_MiB) {
|
||||
posix_madvise(io.mmap_base, io.mmap_size, POSIX_MADV_RANDOM);
|
||||
} else {
|
||||
posix_madvise(io.mmap_base, io.mmap_size, POSIX_MADV_SEQUENTIAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
return io.mmap_fd;
|
||||
}
|
||||
static void PlatformUnmap(IOFile& io) {
|
||||
if (io.mmap_fd != -1) {
|
||||
munmap(io.mmap_base, io.mmap_size);
|
||||
close(io.mmap_fd);
|
||||
io.mmap_fd = -1;
|
||||
}
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
// NO IMPLEMENTATION YET
|
||||
#else
|
||||
static int PlatformMapReadOnly(IOFile& io, const wchar_t* path) {
|
||||
io.file_handle = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
|
||||
if (HANDLE(io.file_handle) != INVALID_HANDLE_VALUE) {
|
||||
io.mapping_handle = CreateFileMappingW(HANDLE(io.file_handle), nullptr, PAGE_READONLY, 0, 0, nullptr);
|
||||
if (io.mapping_handle) {
|
||||
io.mmap_base = (u8*)MapViewOfFile(HANDLE(io.mapping_handle), FILE_MAP_READ, 0, 0, 0);
|
||||
if (io.mmap_base) {
|
||||
_LARGE_INTEGER pvalue;
|
||||
GetFileSizeEx(io.file_handle, &pvalue);
|
||||
io.mmap_size = uint32_t(pvalue.QuadPart);
|
||||
} else {
|
||||
CloseHandle(io.mapping_handle);
|
||||
CloseHandle(io.file_handle);
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
CloseHandle(io.file_handle);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static void PlatformUnmap(IOFile& io) {
|
||||
if(io.mapping_handle) {
|
||||
if(io.mmap_base)
|
||||
UnmapViewOfFile(HANDLE(io.mmap_base));
|
||||
CloseHandle(HANDLE(io.mapping_handle));
|
||||
}
|
||||
if(io.file_handle != INVALID_HANDLE_VALUE)
|
||||
CloseHandle(HANDLE(io.file_handle));
|
||||
}
|
||||
#endif
|
||||
|
||||
void IOFile::Open(const fs::path& path, FileAccessMode mode, FileType type, FileShareFlag flag) {
|
||||
Close();
|
||||
|
||||
file_path = path;
|
||||
file_access_mode = mode;
|
||||
file_type = type;
|
||||
|
||||
errno = 0;
|
||||
|
||||
#ifdef _WIN32
|
||||
if (flag != FileShareFlag::ShareNone) {
|
||||
file = _wfsopen(path.c_str(), AccessModeToWStr(mode, type), ToWindowsFileShareFlag(flag));
|
||||
} else {
|
||||
_wfopen_s(&file, path.c_str(), AccessModeToWStr(mode, type));
|
||||
// TODO: this probably can use better logic but oh well I'm not a windowser
|
||||
file_handle = nullptr;
|
||||
if (type == FileType::BinaryFile && mode == FileAccessMode::Read) {
|
||||
if (PlatformMapReadOnly(*this, path.c_str()) == -1) {
|
||||
LOG_ERROR(Common_Filesystem, "Error mmap'ing file"); //: {}", path.c_str());
|
||||
}
|
||||
}
|
||||
if (file_handle == nullptr) {
|
||||
if (flag != FileShareFlag::ShareNone) {
|
||||
file = _wfsopen(path.c_str(), AccessModeToWStr(mode, type), ToWindowsFileShareFlag(flag));
|
||||
} else {
|
||||
_wfopen_s(&file, path.c_str(), AccessModeToWStr(mode, type));
|
||||
}
|
||||
}
|
||||
#elif __ANDROID__
|
||||
if (Android::IsContentUri(path)) {
|
||||
ASSERT_MSG(mode == FileAccessMode::Read, "Content URI file access is for read-only!");
|
||||
const auto fd = Android::OpenContentUri(path, Android::OpenMode::Read);
|
||||
if (fd != -1) {
|
||||
file = fdopen(fd, "r");
|
||||
const auto error_num = errno;
|
||||
if (error_num != 0 && file == nullptr) {
|
||||
LOG_ERROR(Common_Filesystem, "Error opening file: {}, error: {}", path.c_str(),
|
||||
strerror(error_num));
|
||||
if (PlatformMapReadOnly(*this, path.c_str()) == -1) {
|
||||
LOG_ERROR(Common_Filesystem, "Error mmap'ing file: {}", path.c_str());
|
||||
int const fd = Android::OpenContentUri(path, Android::OpenMode::Read);
|
||||
if (fd != -1) {
|
||||
file = fdopen(fd, "r");
|
||||
if (errno != 0 && file == nullptr)
|
||||
LOG_ERROR(Common_Filesystem, "Error opening file: {}, error: {}", path.c_str(), strerror(errno));
|
||||
} else {
|
||||
LOG_ERROR(Common_Filesystem, "Error opening file: {}", path.c_str());
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(Common_Filesystem, "Error opening file: {}", path.c_str());
|
||||
}
|
||||
} else {
|
||||
file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
|
||||
}
|
||||
#elif defined(__HAIKU__) || defined(__managarm__) || defined(__OPENORBIS__) || defined(__APPLE__)
|
||||
file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
|
||||
#elif defined(__unix__)
|
||||
if (type == FileType::BinaryFile && mode == FileAccessMode::Read) {
|
||||
if (PlatformMapReadOnly(*this, path.c_str()) == -1) {
|
||||
LOG_ERROR(Common_Filesystem, "Error mmap'ing file: {}", path.c_str());
|
||||
}
|
||||
}
|
||||
if (mmap_fd == -1) {
|
||||
file = std::fopen(path.c_str(), AccessModeToStr(mode, type)); // mmap(2) failed or simply we can't use it
|
||||
}
|
||||
#else
|
||||
// Some other fancy OS (ahem fucking Darwin/Mac OSX)
|
||||
file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
|
||||
#endif
|
||||
|
||||
if (!IsOpen()) {
|
||||
const auto ec = std::error_code{errno, std::generic_category()};
|
||||
LOG_ERROR(Common_Filesystem, "Failed to open the file at path={}, ec_message={}",
|
||||
PathToUTF8String(file_path), ec.message());
|
||||
PathToUTF8String(file_path), ec.message());
|
||||
}
|
||||
}
|
||||
|
||||
void IOFile::Close() {
|
||||
if (!IsOpen()) {
|
||||
return;
|
||||
#if defined(__APPLE__)
|
||||
// NO IMPLEMENTATION YET
|
||||
#else
|
||||
PlatformUnmap(*this);
|
||||
#endif
|
||||
if (file) {
|
||||
errno = 0;
|
||||
const auto close_result = std::fclose(file) == 0;
|
||||
if (!close_result) {
|
||||
const auto ec = std::error_code{errno, std::generic_category()};
|
||||
LOG_ERROR(Common_Filesystem, "Failed to close the file at path={}, ec_message={}",
|
||||
PathToUTF8String(file_path), ec.message());
|
||||
}
|
||||
file = nullptr;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
|
||||
const auto close_result = std::fclose(file) == 0;
|
||||
|
||||
if (!close_result) {
|
||||
const auto ec = std::error_code{errno, std::generic_category()};
|
||||
LOG_ERROR(Common_Filesystem, "Failed to close the file at path={}, ec_message={}",
|
||||
PathToUTF8String(file_path), ec.message());
|
||||
}
|
||||
|
||||
file = nullptr;
|
||||
}
|
||||
|
||||
bool IOFile::IsOpen() const {
|
||||
return file != nullptr;
|
||||
return file != nullptr || IsMappedFile();
|
||||
}
|
||||
|
||||
std::string IOFile::ReadString(size_t length) const {
|
||||
|
|
@ -323,137 +450,132 @@ size_t IOFile::WriteString(std::span<const char> string) const {
|
|||
}
|
||||
|
||||
bool IOFile::Flush() const {
|
||||
if (!IsOpen()) {
|
||||
return false;
|
||||
ASSERT(!IsMappedFile());
|
||||
if (file) {
|
||||
errno = 0;
|
||||
auto const flush_result = std::fflush(file) == 0;
|
||||
if (!flush_result) {
|
||||
const auto ec = std::error_code{errno, std::generic_category()};
|
||||
LOG_ERROR(Common_Filesystem, "Failed to flush the file at path={}, ec_message={}",
|
||||
PathToUTF8String(file_path), ec.message());
|
||||
}
|
||||
return flush_result;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
|
||||
#ifdef _WIN32
|
||||
const auto flush_result = std::fflush(file) == 0;
|
||||
#else
|
||||
const auto flush_result = std::fflush(file) == 0;
|
||||
#endif
|
||||
|
||||
if (!flush_result) {
|
||||
const auto ec = std::error_code{errno, std::generic_category()};
|
||||
LOG_ERROR(Common_Filesystem, "Failed to flush the file at path={}, ec_message={}",
|
||||
PathToUTF8String(file_path), ec.message());
|
||||
}
|
||||
|
||||
return flush_result;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IOFile::Commit() const {
|
||||
if (!IsOpen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
|
||||
ASSERT(!IsMappedFile());
|
||||
if (file) {
|
||||
errno = 0;
|
||||
#ifdef _WIN32
|
||||
const auto commit_result = std::fflush(file) == 0 && _commit(fileno(file)) == 0;
|
||||
const auto commit_result = std::fflush(file) == 0 && _commit(fileno(file)) == 0;
|
||||
#else
|
||||
const auto commit_result = std::fflush(file) == 0 && fsync(fileno(file)) == 0;
|
||||
const auto commit_result = std::fflush(file) == 0 && fsync(fileno(file)) == 0;
|
||||
#endif
|
||||
|
||||
if (!commit_result) {
|
||||
const auto ec = std::error_code{errno, std::generic_category()};
|
||||
LOG_ERROR(Common_Filesystem, "Failed to commit the file at path={}, ec_message={}",
|
||||
PathToUTF8String(file_path), ec.message());
|
||||
if (!commit_result) {
|
||||
const auto ec = std::error_code{errno, std::generic_category()};
|
||||
LOG_ERROR(Common_Filesystem, "Failed to commit the file at path={}, ec_message={}",
|
||||
PathToUTF8String(file_path), ec.message());
|
||||
}
|
||||
return commit_result;
|
||||
}
|
||||
|
||||
return commit_result;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IOFile::SetSize(u64 size) const {
|
||||
if (!IsOpen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
|
||||
ASSERT(!IsMappedFile());
|
||||
if (file) {
|
||||
errno = 0;
|
||||
#ifdef _WIN32
|
||||
const auto set_size_result = _chsize_s(fileno(file), static_cast<s64>(size)) == 0;
|
||||
const auto set_size_result = _chsize_s(fileno(file), s64(size)) == 0;
|
||||
#else
|
||||
const auto set_size_result = ftruncate(fileno(file), static_cast<s64>(size)) == 0;
|
||||
const auto set_size_result = ftruncate(fileno(file), s64(size)) == 0;
|
||||
#endif
|
||||
|
||||
if (!set_size_result) {
|
||||
const auto ec = std::error_code{errno, std::generic_category()};
|
||||
LOG_ERROR(Common_Filesystem, "Failed to resize the file at path={}, size={}, ec_message={}",
|
||||
PathToUTF8String(file_path), size, ec.message());
|
||||
if (!set_size_result) {
|
||||
const auto ec = std::error_code{errno, std::generic_category()};
|
||||
LOG_ERROR(Common_Filesystem, "Failed to resize the file at path={}, size={}, ec_message={}",
|
||||
PathToUTF8String(file_path), size, ec.message());
|
||||
}
|
||||
return set_size_result;
|
||||
}
|
||||
|
||||
return set_size_result;
|
||||
return false;
|
||||
}
|
||||
|
||||
u64 IOFile::GetSize() const {
|
||||
if (!IsOpen()) {
|
||||
return 0;
|
||||
}
|
||||
if (IsMappedFile())
|
||||
return mmap_size;
|
||||
if (file) {
|
||||
// Flush any unwritten buffered data into the file prior to retrieving the file mmap_size.
|
||||
std::fflush(file);
|
||||
#if __ANDROID__
|
||||
u64 file_size = 0;
|
||||
if (Android::IsContentUri(file_path)) {
|
||||
file_size = Android::GetSize(file_path);
|
||||
} else {
|
||||
std::error_code ec;
|
||||
|
||||
// Flush any unwritten buffered data into the file prior to retrieving the file size.
|
||||
std::fflush(file);
|
||||
file_size = fs::file_size(file_path, ec);
|
||||
|
||||
#ifdef __ANDROID__
|
||||
u64 file_size = 0;
|
||||
if (Android::IsContentUri(file_path)) {
|
||||
file_size = Android::GetSize(file_path);
|
||||
} else {
|
||||
if (ec) {
|
||||
LOG_ERROR(Common_Filesystem, "Failed to retrieve the file mmap_size of path={}, ec_message={}",
|
||||
PathToUTF8String(file_path), ec.message());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#else
|
||||
std::error_code ec;
|
||||
|
||||
file_size = fs::file_size(file_path, ec);
|
||||
|
||||
auto const file_size = fs::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR(Common_Filesystem,
|
||||
"Failed to retrieve the file size of path={}, ec_message={}",
|
||||
PathToUTF8String(file_path), ec.message());
|
||||
LOG_ERROR(Common_Filesystem, "Failed to retrieve the file mmap_size of path={}, ec_message={}",
|
||||
PathToUTF8String(file_path), ec.message());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#else
|
||||
std::error_code ec;
|
||||
|
||||
const auto file_size = fs::file_size(file_path, ec);
|
||||
|
||||
if (ec) {
|
||||
LOG_ERROR(Common_Filesystem, "Failed to retrieve the file size of path={}, ec_message={}",
|
||||
PathToUTF8String(file_path), ec.message());
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
return file_size;
|
||||
return file_size;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool IOFile::Seek(s64 offset, SeekOrigin origin) const {
|
||||
if (!IsOpen()) {
|
||||
return false;
|
||||
if (IsMappedFile()) {
|
||||
// fuck you to whoever made this method const
|
||||
switch (origin) {
|
||||
case SeekOrigin::SetOrigin:
|
||||
mmap_offset = s64(offset);
|
||||
break;
|
||||
case SeekOrigin::CurrentPosition:
|
||||
mmap_offset += s64(offset);
|
||||
break;
|
||||
case SeekOrigin::End:
|
||||
mmap_offset = s64(mmap_size) + s64(offset);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
|
||||
const auto seek_result = fseeko(file, offset, ToSeekOrigin(origin)) == 0;
|
||||
|
||||
if (!seek_result) {
|
||||
const auto ec = std::error_code{errno, std::generic_category()};
|
||||
LOG_ERROR(Common_Filesystem,
|
||||
"Failed to seek the file at path={}, offset={}, origin={}, ec_message={}",
|
||||
PathToUTF8String(file_path), offset, origin, ec.message());
|
||||
if (file) {
|
||||
errno = 0;
|
||||
const auto seek_result = fseeko(file, offset, ToSeekOrigin(origin)) == 0;
|
||||
if (!seek_result) {
|
||||
const auto ec = std::error_code{errno, std::generic_category()};
|
||||
LOG_ERROR(Common_Filesystem, "Failed to seek the file at path={}, offset={}, origin={}, ec_message={}",
|
||||
PathToUTF8String(file_path), offset, origin, ec.message());
|
||||
}
|
||||
return seek_result;
|
||||
}
|
||||
|
||||
return seek_result;
|
||||
return false;
|
||||
}
|
||||
|
||||
s64 IOFile::Tell() const {
|
||||
if (!IsOpen()) {
|
||||
return 0;
|
||||
if (IsMappedFile()) {
|
||||
errno = 0;
|
||||
return s64(mmap_offset);
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
|
||||
return ftello(file);
|
||||
if (file) {
|
||||
errno = 0;
|
||||
return ftello(file);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace Common::FS
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
#include <type_traits>
|
||||
|
|
@ -24,12 +25,12 @@ enum class SeekOrigin {
|
|||
};
|
||||
|
||||
/**
|
||||
* Opens a file stream at path with the specified open mode.
|
||||
*
|
||||
* @param file_stream Reference to file stream
|
||||
* @param path Filesystem path
|
||||
* @param open_mode File stream open mode
|
||||
*/
|
||||
* Opens a file stream at path with the specified open mode.
|
||||
*
|
||||
* @param file_stream Reference to file stream
|
||||
* @param path Filesystem path
|
||||
* @param open_mode File stream open mode
|
||||
*/
|
||||
template <typename FileStream>
|
||||
void OpenFileStream(FileStream& file_stream, const std::filesystem::path& path,
|
||||
std::ios_base::openmode open_mode) {
|
||||
|
|
@ -48,14 +49,14 @@ void OpenFileStream(FileStream& file_stream, const Path& path, std::ios_base::op
|
|||
#endif
|
||||
|
||||
/**
|
||||
* Reads an entire file at path and returns a string of the contents read from the file.
|
||||
* If the filesystem object at path is not a regular file, this function returns an empty string.
|
||||
*
|
||||
* @param path Filesystem path
|
||||
* @param type File type
|
||||
*
|
||||
* @returns A string of the contents read from the file.
|
||||
*/
|
||||
* Reads an entire file at path and returns a string of the contents read from the file.
|
||||
* If the filesystem object at path is not a regular file, this function returns an empty string.
|
||||
*
|
||||
* @param path Filesystem path
|
||||
* @param type File type
|
||||
*
|
||||
* @returns A string of the contents read from the file.
|
||||
*/
|
||||
[[nodiscard]] std::string ReadStringFromFile(const std::filesystem::path& path, FileType type);
|
||||
|
||||
#ifdef _WIN32
|
||||
|
|
@ -70,18 +71,18 @@ template <typename Path>
|
|||
#endif
|
||||
|
||||
/**
|
||||
* Writes a string to a file at path and returns the number of characters successfully written.
|
||||
* If a file already exists at path, its contents will be erased.
|
||||
* If a file does not exist at path, it creates and opens a new empty file for writing.
|
||||
* If the filesystem object at path exists and is not a regular file, this function returns 0.
|
||||
*
|
||||
* @param path Filesystem path
|
||||
* @param type File type
|
||||
*
|
||||
* @returns Number of characters successfully written.
|
||||
*/
|
||||
* Writes a string to a file at path and returns the number of characters successfully written.
|
||||
* If a file already exists at path, its contents will be erased.
|
||||
* If a file does not exist at path, it creates and opens a new empty file for writing.
|
||||
* If the filesystem object at path exists and is not a regular file, this function returns 0.
|
||||
*
|
||||
* @param path Filesystem path
|
||||
* @param type File type
|
||||
*
|
||||
* @returns Number of characters successfully written.
|
||||
*/
|
||||
[[nodiscard]] size_t WriteStringToFile(const std::filesystem::path& path, FileType type,
|
||||
std::string_view string);
|
||||
std::string_view string);
|
||||
|
||||
#ifdef _WIN32
|
||||
template <typename Path>
|
||||
|
|
@ -95,15 +96,15 @@ template <typename Path>
|
|||
#endif
|
||||
|
||||
/**
|
||||
* Appends a string to a file at path and returns the number of characters successfully written.
|
||||
* If a file does not exist at path, it creates and opens a new empty file for appending.
|
||||
* If the filesystem object at path exists and is not a regular file, this function returns 0.
|
||||
*
|
||||
* @param path Filesystem path
|
||||
* @param type File type
|
||||
*
|
||||
* @returns Number of characters successfully written.
|
||||
*/
|
||||
* Appends a string to a file at path and returns the number of characters successfully written.
|
||||
* If a file does not exist at path, it creates and opens a new empty file for appending.
|
||||
* If the filesystem object at path exists and is not a regular file, this function returns 0.
|
||||
*
|
||||
* @param path Filesystem path
|
||||
* @param type File type
|
||||
*
|
||||
* @returns Number of characters successfully written.
|
||||
*/
|
||||
[[nodiscard]] size_t AppendStringToFile(const std::filesystem::path& path, FileType type,
|
||||
std::string_view string);
|
||||
|
||||
|
|
@ -131,14 +132,14 @@ public:
|
|||
FileShareFlag flag = FileShareFlag::ShareReadOnly);
|
||||
|
||||
/**
|
||||
* An IOFile is a lightweight wrapper on C Library file operations.
|
||||
* Automatically closes an open file on the destruction of an IOFile object.
|
||||
*
|
||||
* @param path Filesystem path
|
||||
* @param mode File access mode
|
||||
* @param type File type, default is BinaryFile. Use TextFile to open the file as a text file
|
||||
* @param flag (Windows only) File-share access flag, default is ShareReadOnly
|
||||
*/
|
||||
* An IOFile is a lightweight wrapper on C Library file operations.
|
||||
* Automatically closes an open file on the destruction of an IOFile object.
|
||||
*
|
||||
* @param path Filesystem path
|
||||
* @param mode File access mode
|
||||
* @param type File type, default is BinaryFile. Use TextFile to open the file as a text file
|
||||
* @param flag (Windows only) File-share access flag, default is ShareReadOnly
|
||||
*/
|
||||
explicit IOFile(const std::filesystem::path& path, FileAccessMode mode,
|
||||
FileType type = FileType::BinaryFile,
|
||||
FileShareFlag flag = FileShareFlag::ShareReadOnly);
|
||||
|
|
@ -152,84 +153,70 @@ public:
|
|||
IOFile& operator=(IOFile&& other) noexcept;
|
||||
|
||||
/**
|
||||
* Gets the path of the file.
|
||||
*
|
||||
* @returns The path of the file.
|
||||
*/
|
||||
* Gets the path of the file.
|
||||
*
|
||||
* @returns The path of the file.
|
||||
*/
|
||||
[[nodiscard]] std::filesystem::path GetPath() const;
|
||||
|
||||
/**
|
||||
* Gets the access mode of the file.
|
||||
*
|
||||
* @returns The access mode of the file.
|
||||
*/
|
||||
* Gets the access mode of the file.
|
||||
*
|
||||
* @returns The access mode of the file.
|
||||
*/
|
||||
[[nodiscard]] FileAccessMode GetAccessMode() const;
|
||||
|
||||
/**
|
||||
* Gets the type of the file.
|
||||
*
|
||||
* @returns The type of the file.
|
||||
*/
|
||||
* Gets the type of the file.
|
||||
*
|
||||
* @returns The type of the file.
|
||||
*/
|
||||
[[nodiscard]] FileType GetType() const;
|
||||
|
||||
/**
|
||||
* Opens a file at path with the specified file access mode.
|
||||
* This function behaves differently depending on the FileAccessMode.
|
||||
* These behaviors are documented in each enum value of FileAccessMode.
|
||||
*
|
||||
* @param path Filesystem path
|
||||
* @param mode File access mode
|
||||
* @param type File type, default is BinaryFile. Use TextFile to open the file as a text file
|
||||
* @param flag (Windows only) File-share access flag, default is ShareReadOnly
|
||||
*/
|
||||
* Opens a file at path with the specified file access mode.
|
||||
* This function behaves differently depending on the FileAccessMode.
|
||||
* These behaviors are documented in each enum value of FileAccessMode.
|
||||
*
|
||||
* @param path Filesystem path
|
||||
* @param mode File access mode
|
||||
* @param type File type, default is BinaryFile. Use TextFile to open the file as a text file
|
||||
* @param flag (Windows only) File-share access flag, default is ShareReadOnly
|
||||
*/
|
||||
void Open(const std::filesystem::path& path, FileAccessMode mode,
|
||||
FileType type = FileType::BinaryFile,
|
||||
FileShareFlag flag = FileShareFlag::ShareReadOnly);
|
||||
|
||||
// #ifdef _WIN32
|
||||
// template <typename Path>
|
||||
// void Open(const Path& path, FileAccessMode mode, FileType type = FileType::BinaryFile,
|
||||
// FileShareFlag flag = FileShareFlag::ShareReadOnly) {
|
||||
// using ValueType = typename Path::value_type;
|
||||
// if constexpr (IsChar<ValueType>) {
|
||||
// Open(ToU8String(path), mode, type, flag);
|
||||
// } else {
|
||||
// Open(std::filesystem::path{path}, mode, type, flag);
|
||||
// }
|
||||
// }
|
||||
// #endif
|
||||
FileType type = FileType::BinaryFile,
|
||||
FileShareFlag flag = FileShareFlag::ShareReadOnly);
|
||||
|
||||
/// Closes the file if it is opened.
|
||||
void Close();
|
||||
|
||||
/**
|
||||
* Checks whether the file is open.
|
||||
* Use this to check whether the calls to Open() or Close() succeeded.
|
||||
*
|
||||
* @returns True if the file is open, false otherwise.
|
||||
*/
|
||||
* Checks whether the file is open.
|
||||
* Use this to check whether the calls to Open() or Close() succeeded.
|
||||
*
|
||||
* @returns True if the file is open, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool IsOpen() const;
|
||||
|
||||
/**
|
||||
* Helper function which deduces the value type of a contiguous STL container used in ReadSpan.
|
||||
* If T is not a contiguous container as defined by the concept IsContiguousContainer, this
|
||||
* calls ReadObject and T must be a trivially copyable object.
|
||||
*
|
||||
* See ReadSpan for more details if T is a contiguous container.
|
||||
* See ReadObject for more details if T is a trivially copyable object.
|
||||
*
|
||||
* @tparam T Contiguous container or trivially copyable object
|
||||
*
|
||||
* @param data Container of T::value_type data or reference to object
|
||||
*
|
||||
* @returns Count of T::value_type data or objects successfully read.
|
||||
*/
|
||||
* Helper function which deduces the value type of a contiguous STL container used in ReadSpan.
|
||||
* If T is not a contiguous container as defined by the concept IsContiguousContainer, this
|
||||
* calls ReadObject and T must be a trivially copyable object.
|
||||
*
|
||||
* See ReadSpan for more details if T is a contiguous container.
|
||||
* See ReadObject for more details if T is a trivially copyable object.
|
||||
*
|
||||
* @tparam T Contiguous container or trivially copyable object
|
||||
*
|
||||
* @param data Container of T::value_type data or reference to object
|
||||
*
|
||||
* @returns Count of T::value_type data or objects successfully read.
|
||||
*/
|
||||
template <typename T>
|
||||
[[nodiscard]] size_t Read(T& data) const {
|
||||
if constexpr (IsContiguousContainer<T>) {
|
||||
using ContiguousType = typename T::value_type;
|
||||
static_assert(std::is_trivially_copyable_v<ContiguousType>,
|
||||
"Data type must be trivially copyable.");
|
||||
static_assert(std::is_trivially_copyable_v<ContiguousType>, "Data type must be trivially copyable.");
|
||||
return ReadSpan<ContiguousType>(data);
|
||||
} else {
|
||||
return ReadObject(data) ? 1 : 0;
|
||||
|
|
@ -237,25 +224,24 @@ public:
|
|||
}
|
||||
|
||||
/**
|
||||
* Helper function which deduces the value type of a contiguous STL container used in WriteSpan.
|
||||
* If T is not a contiguous STL container as defined by the concept IsContiguousContainer, this
|
||||
* calls WriteObject and T must be a trivially copyable object.
|
||||
*
|
||||
* See WriteSpan for more details if T is a contiguous container.
|
||||
* See WriteObject for more details if T is a trivially copyable object.
|
||||
*
|
||||
* @tparam T Contiguous container or trivially copyable object
|
||||
*
|
||||
* @param data Container of T::value_type data or const reference to object
|
||||
*
|
||||
* @returns Count of T::value_type data or objects successfully written.
|
||||
*/
|
||||
* Helper function which deduces the value type of a contiguous STL container used in WriteSpan.
|
||||
* If T is not a contiguous STL container as defined by the concept IsContiguousContainer, this
|
||||
* calls WriteObject and T must be a trivially copyable object.
|
||||
*
|
||||
* See WriteSpan for more details if T is a contiguous container.
|
||||
* See WriteObject for more details if T is a trivially copyable object.
|
||||
*
|
||||
* @tparam T Contiguous container or trivially copyable object
|
||||
*
|
||||
* @param data Container of T::value_type data or const reference to object
|
||||
*
|
||||
* @returns Count of T::value_type data or objects successfully written.
|
||||
*/
|
||||
template <typename T>
|
||||
[[nodiscard]] size_t Write(const T& data) const {
|
||||
if constexpr (IsContiguousContainer<T>) {
|
||||
using ContiguousType = typename T::value_type;
|
||||
static_assert(std::is_trivially_copyable_v<ContiguousType>,
|
||||
"Data type must be trivially copyable.");
|
||||
static_assert(std::is_trivially_copyable_v<ContiguousType>, "Data type must be trivially copyable.");
|
||||
return WriteSpan<ContiguousType>(data);
|
||||
} else {
|
||||
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
|
||||
|
|
@ -264,197 +250,219 @@ public:
|
|||
}
|
||||
|
||||
/**
|
||||
* Reads a span of T data from a file sequentially.
|
||||
* This function reads from the current position of the file pointer and
|
||||
* advances it by the (count of T * sizeof(T)) bytes successfully read.
|
||||
*
|
||||
* Failures occur when:
|
||||
* - The file is not open
|
||||
* - The opened file lacks read permissions
|
||||
* - Attempting to read beyond the end-of-file
|
||||
*
|
||||
* @tparam T Data type
|
||||
*
|
||||
* @param data Span of T data
|
||||
*
|
||||
* @returns Count of T data successfully read.
|
||||
*/
|
||||
* Reads a span of T data from a file sequentially.
|
||||
* This function reads from the current position of the file pointer and
|
||||
* advances it by the (count of T * sizeof(T)) bytes successfully read.
|
||||
*
|
||||
* Failures occur when:
|
||||
* - The file is not open
|
||||
* - The opened file lacks read permissions
|
||||
* - Attempting to read beyond the end-of-file
|
||||
*
|
||||
* @tparam T Data type
|
||||
*
|
||||
* @param data Span of T data
|
||||
*
|
||||
* @returns Count of T data successfully read.
|
||||
*/
|
||||
template <typename T>
|
||||
requires std::is_trivially_copyable_v<T>
|
||||
[[nodiscard]] size_t ReadSpan(std::span<T> data) const {
|
||||
if (!IsOpen()) {
|
||||
return 0;
|
||||
if (IsMappedFile()) {
|
||||
std::memcpy(data.data(), mmap_base + mmap_offset, sizeof(T) * data.size());
|
||||
return data.size();
|
||||
}
|
||||
return std::fread(data.data(), sizeof(T), data.size(), file);
|
||||
return IsOpen() ? std::fread(data.data(), sizeof(T), data.size(), file) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a span of T data to a file sequentially.
|
||||
* This function writes from the current position of the file pointer and
|
||||
* advances it by the (count of T * sizeof(T)) bytes successfully written.
|
||||
*
|
||||
* Failures occur when:
|
||||
* - The file is not open
|
||||
* - The opened file lacks write permissions
|
||||
*
|
||||
* @tparam T Data type
|
||||
*
|
||||
* @param data Span of T data
|
||||
*
|
||||
* @returns Count of T data successfully written.
|
||||
*/
|
||||
* Writes a span of T data to a file sequentially.
|
||||
* This function writes from the current position of the file pointer and
|
||||
* advances it by the (count of T * sizeof(T)) bytes successfully written.
|
||||
*
|
||||
* Failures occur when:
|
||||
* - The file is not open
|
||||
* - The opened file lacks write permissions
|
||||
*
|
||||
* @tparam T Data type
|
||||
*
|
||||
* @param data Span of T data
|
||||
*
|
||||
* @returns Count of T data successfully written.
|
||||
*/
|
||||
template <typename T>
|
||||
[[nodiscard]] size_t WriteSpan(std::span<const T> data) const {
|
||||
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
|
||||
|
||||
if (!IsOpen()) {
|
||||
return 0;
|
||||
if (IsMappedFile()) {
|
||||
std::memcpy(mmap_base + mmap_offset, data.data(), sizeof(T) * data.size());
|
||||
return data.size();
|
||||
}
|
||||
|
||||
return std::fwrite(data.data(), sizeof(T), data.size(), file);
|
||||
return IsOpen() ? std::fwrite(data.data(), sizeof(T), data.size(), file) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a T object from a file sequentially.
|
||||
* This function reads from the current position of the file pointer and
|
||||
* advances it by the sizeof(T) bytes successfully read.
|
||||
*
|
||||
* Failures occur when:
|
||||
* - The file is not open
|
||||
* - The opened file lacks read permissions
|
||||
* - Attempting to read beyond the end-of-file
|
||||
*
|
||||
* @tparam T Data type
|
||||
*
|
||||
* @param object Reference to object
|
||||
*
|
||||
* @returns True if the object is successfully read from the file, false otherwise.
|
||||
*/
|
||||
* Reads a T object from a file sequentially.
|
||||
* This function reads from the current position of the file pointer and
|
||||
* advances it by the sizeof(T) bytes successfully read.
|
||||
*
|
||||
* Failures occur when:
|
||||
* - The file is not open
|
||||
* - The opened file lacks read permissions
|
||||
* - Attempting to read beyond the end-of-file
|
||||
*
|
||||
* @tparam T Data type
|
||||
*
|
||||
* @param object Reference to object
|
||||
*
|
||||
* @returns True if the object is successfully read from the file, false otherwise.
|
||||
*/
|
||||
template <typename T>
|
||||
[[nodiscard]] bool ReadObject(T& object) const {
|
||||
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
|
||||
static_assert(!std::is_pointer_v<T>, "T must not be a pointer to an object.");
|
||||
|
||||
if (!IsOpen()) {
|
||||
return false;
|
||||
if (IsMappedFile()) {
|
||||
std::memcpy(&object, mmap_base + mmap_offset, sizeof(T));
|
||||
#ifdef _WIN32
|
||||
return bool(sizeof(T));
|
||||
#else
|
||||
return sizeof(T);
|
||||
#endif
|
||||
}
|
||||
|
||||
return std::fread(&object, sizeof(T), 1, file) == 1;
|
||||
return IsOpen() ? std::fread(&object, sizeof(T), 1, file) == 1 : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a T object to a file sequentially.
|
||||
* This function writes from the current position of the file pointer and
|
||||
* advances it by the sizeof(T) bytes successfully written.
|
||||
*
|
||||
* Failures occur when:
|
||||
* - The file is not open
|
||||
* - The opened file lacks write permissions
|
||||
*
|
||||
* @tparam T Data type
|
||||
*
|
||||
* @param object Const reference to object
|
||||
*
|
||||
* @returns True if the object is successfully written to the file, false otherwise.
|
||||
*/
|
||||
* Writes a T object to a file sequentially.
|
||||
* This function writes from the current position of the file pointer and
|
||||
* advances it by the sizeof(T) bytes successfully written.
|
||||
*
|
||||
* Failures occur when:
|
||||
* - The file is not open
|
||||
* - The opened file lacks write permissions
|
||||
*
|
||||
* @tparam T Data type
|
||||
*
|
||||
* @param object Const reference to object
|
||||
*
|
||||
* @returns True if the object is successfully written to the file, false otherwise.
|
||||
*/
|
||||
template <typename T>
|
||||
[[nodiscard]] bool WriteObject(const T& object) const {
|
||||
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
|
||||
static_assert(!std::is_pointer_v<T>, "T must not be a pointer to an object.");
|
||||
|
||||
if (!IsOpen()) {
|
||||
return false;
|
||||
if (IsMappedFile()) {
|
||||
std::memcpy(mmap_base + mmap_offset, &object, sizeof(T));
|
||||
#ifdef _WIN32
|
||||
return sizeof(T) != 0;
|
||||
#else
|
||||
return sizeof(T);
|
||||
#endif
|
||||
}
|
||||
|
||||
return std::fwrite(&object, sizeof(T), 1, file) == 1;
|
||||
return IsOpen() ? std::fwrite(&object, sizeof(T), 1, file) == 1 : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specialized function to read a string of a given length from a file sequentially.
|
||||
* This function writes from the current position of the file pointer and
|
||||
* advances it by the number of characters successfully read.
|
||||
* The size of the returned string may not match length if not all bytes are successfully read.
|
||||
*
|
||||
* @param length Length of the string
|
||||
*
|
||||
* @returns A string read from the file.
|
||||
*/
|
||||
* Specialized function to read a string of a given length from a file sequentially.
|
||||
* This function writes from the current position of the file pointer and
|
||||
* advances it by the number of characters successfully read.
|
||||
* The size of the returned string may not match length if not all bytes are successfully read.
|
||||
*
|
||||
* @param length Length of the string
|
||||
*
|
||||
* @returns A string read from the file.
|
||||
*/
|
||||
[[nodiscard]] std::string ReadString(size_t length) const;
|
||||
|
||||
/**
|
||||
* Specialized function to write a string to a file sequentially.
|
||||
* This function writes from the current position of the file pointer and
|
||||
* advances it by the number of characters successfully written.
|
||||
*
|
||||
* @param string Span of const char backed std::string or std::string_view
|
||||
*
|
||||
* @returns Number of characters successfully written.
|
||||
*/
|
||||
* Specialized function to write a string to a file sequentially.
|
||||
* This function writes from the current position of the file pointer and
|
||||
* advances it by the number of characters successfully written.
|
||||
*
|
||||
* @param string Span of const char backed std::string or std::string_view
|
||||
*
|
||||
* @returns Number of characters successfully written.
|
||||
*/
|
||||
[[nodiscard]] size_t WriteString(std::span<const char> string) const;
|
||||
|
||||
/**
|
||||
* Attempts to flush any unwritten buffered data into the file.
|
||||
*
|
||||
* @returns True if the flush was successful, false otherwise.
|
||||
*/
|
||||
* Attempts to flush any unwritten buffered data into the file.
|
||||
*
|
||||
* @returns True if the flush was successful, false otherwise.
|
||||
*/
|
||||
bool Flush() const;
|
||||
|
||||
/**
|
||||
* Attempts to commit the file into the disk.
|
||||
* Note that this is an expensive operation as this forces the operating system to write
|
||||
* the contents of the file associated with the file descriptor into the disk.
|
||||
*
|
||||
* @returns True if the commit was successful, false otherwise.
|
||||
*/
|
||||
* Attempts to commit the file into the disk.
|
||||
* Note that this is an expensive operation as this forces the operating system to write
|
||||
* the contents of the file associated with the file descriptor into the disk.
|
||||
*
|
||||
* @returns True if the commit was successful, false otherwise.
|
||||
*/
|
||||
bool Commit() const;
|
||||
|
||||
/**
|
||||
* Resizes the file to a given size.
|
||||
* If the file is resized to a smaller size, the remainder of the file is discarded.
|
||||
* If the file is resized to a larger size, the new area appears as if zero-filled.
|
||||
*
|
||||
* Failures occur when:
|
||||
* - The file is not open
|
||||
*
|
||||
* @param size File size in bytes
|
||||
*
|
||||
* @returns True if the file resize succeeded, false otherwise.
|
||||
*/
|
||||
* Resizes the file to a given size.
|
||||
* If the file is resized to a smaller size, the remainder of the file is discarded.
|
||||
* If the file is resized to a larger size, the new area appears as if zero-filled.
|
||||
*
|
||||
* Failures occur when:
|
||||
* - The file is not open
|
||||
*
|
||||
* @param size File size in bytes
|
||||
*
|
||||
* @returns True if the file resize succeeded, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool SetSize(u64 size) const;
|
||||
|
||||
/**
|
||||
* Gets the size of the file.
|
||||
*
|
||||
* Failures occur when:
|
||||
* - The file is not open
|
||||
*
|
||||
* @returns The file size in bytes of the file. Returns 0 on failure.
|
||||
*/
|
||||
* Gets the size of the file.
|
||||
*
|
||||
* Failures occur when:
|
||||
* - The file is not open
|
||||
*
|
||||
* @returns The file size in bytes of the file. Returns 0 on failure.
|
||||
*/
|
||||
[[nodiscard]] u64 GetSize() const;
|
||||
|
||||
/**
|
||||
* Moves the current position of the file pointer with the specified offset and seek origin.
|
||||
*
|
||||
* @param offset Offset from seek origin
|
||||
* @param origin Seek origin
|
||||
*
|
||||
* @returns True if the file pointer has moved to the specified offset, false otherwise.
|
||||
*/
|
||||
* Moves the current position of the file pointer with the specified offset and seek origin.
|
||||
*
|
||||
* @param offset Offset from seek origin
|
||||
* @param origin Seek origin
|
||||
*
|
||||
* @returns True if the file pointer has moved to the specified offset, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool Seek(s64 offset, SeekOrigin origin = SeekOrigin::SetOrigin) const;
|
||||
|
||||
/**
|
||||
* Gets the current position of the file pointer.
|
||||
*
|
||||
* @returns The current position of the file pointer.
|
||||
*/
|
||||
* Gets the current position of the file pointer.
|
||||
*
|
||||
* @returns The current position of the file pointer.
|
||||
*/
|
||||
[[nodiscard]] s64 Tell() const;
|
||||
|
||||
private:
|
||||
#ifdef _WIN32
|
||||
inline bool IsMappedFile() const noexcept { return mapping_handle != nullptr; }
|
||||
#else // POSIX
|
||||
inline bool IsMappedFile() const noexcept { return mmap_fd != -1; }
|
||||
#endif
|
||||
|
||||
std::filesystem::path file_path;
|
||||
FileAccessMode file_access_mode{};
|
||||
FileType file_type{};
|
||||
|
||||
std::FILE* file = nullptr;
|
||||
// Any decent system should have mmap() for files
|
||||
// Systems with artifical mmap() limitations should simply change the logic within file.cpp
|
||||
// and reduce the threshold for which the mmap() is set to
|
||||
#ifdef _WIN32
|
||||
void *mapping_handle = nullptr;
|
||||
void *file_handle = nullptr;
|
||||
#else // POSIX
|
||||
int mmap_fd = -1;
|
||||
#endif
|
||||
u8* mmap_base = nullptr;
|
||||
size_t mmap_size = 0;
|
||||
mutable s64 mmap_offset = 0; // fuck you
|
||||
};
|
||||
|
||||
} // namespace Common::FS
|
||||
|
|
|
|||
|
|
@ -418,6 +418,10 @@ void SetGlobalFilter(const Filter& filter) {
|
|||
logging_instance->filter = filter;
|
||||
}
|
||||
|
||||
bool IsMessageEnabled(Class log_class, Level log_level) {
|
||||
return logging_instance && logging_instance->filter.CheckMessage(log_class, log_level);
|
||||
}
|
||||
|
||||
void SetColorConsoleBackendEnabled(bool enabled) {
|
||||
if (logging_instance)
|
||||
logging_instance->color_console_backend.enabled = enabled;
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ void Stop();
|
|||
|
||||
/// The global filter will prevent any messages from even being processed if they are filtered.
|
||||
void SetGlobalFilter(const Filter& filter);
|
||||
[[nodiscard]] bool IsMessageEnabled(Class log_class, Level log_level);
|
||||
void SetColorConsoleBackendEnabled(bool enabled);
|
||||
|
||||
} // namespace Common::Log
|
||||
|
|
|
|||
107
src/common/overlay_panels.h
Normal file
107
src/common/overlay_panels.h
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
/// The screen corner a panel is placed against
|
||||
enum class OverlayAnchor {
|
||||
TopLeft,
|
||||
BottomLeft,
|
||||
TopRight,
|
||||
};
|
||||
|
||||
/// An image drawn over the game inside the presented frame, so it also shows under gamescope,
|
||||
/// where no Qt widget can composite over the game. The frontend fills it (RGBA8); the presenter
|
||||
/// blends it in last.
|
||||
struct OverlayPanel {
|
||||
std::mutex mutex;
|
||||
std::vector<u8> pixels; ///< RGBA8, width * height * 4
|
||||
u32 width = 0;
|
||||
u32 height = 0;
|
||||
bool enabled = false;
|
||||
/// Incremented on every push so presenters can cache by version
|
||||
u64 version = 0;
|
||||
OverlayAnchor anchor = OverlayAnchor::TopLeft;
|
||||
/// A top-anchored panel sits this share of the screen's height lower (clear of other
|
||||
/// overlays in the corner)
|
||||
float drop = 0.0f;
|
||||
};
|
||||
|
||||
/// The panels, in drawing order
|
||||
enum class OverlayPanelId : std::size_t {
|
||||
Players, ///< The controller players (top left)
|
||||
Hints, ///< The controller hints (bottom left)
|
||||
Cheats, ///< The cheats (top right)
|
||||
Count,
|
||||
};
|
||||
|
||||
inline constexpr std::size_t OverlayPanelCount = static_cast<std::size_t>(OverlayPanelId::Count);
|
||||
|
||||
[[nodiscard]] inline std::array<OverlayPanel, OverlayPanelCount>& GetOverlayPanels() {
|
||||
static std::array<OverlayPanel, OverlayPanelCount> panels;
|
||||
static const bool placed = [] {
|
||||
panels[static_cast<std::size_t>(OverlayPanelId::Hints)].anchor =
|
||||
OverlayAnchor::BottomLeft;
|
||||
panels[static_cast<std::size_t>(OverlayPanelId::Cheats)].anchor =
|
||||
OverlayAnchor::TopRight;
|
||||
return true;
|
||||
}();
|
||||
(void)placed;
|
||||
return panels;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline OverlayPanel& GetOverlayPanel(OverlayPanelId id) {
|
||||
return GetOverlayPanels()[static_cast<std::size_t>(id)];
|
||||
}
|
||||
|
||||
/// A panel's place, in the pixels of the screen area the game is drawn in
|
||||
struct OverlayRect {
|
||||
float x;
|
||||
float y;
|
||||
float width;
|
||||
float height;
|
||||
};
|
||||
|
||||
/// Where a panel goes on a screen area (left, top, right, bottom), for both the presenter and
|
||||
/// the mouse handling. Whole pixels, or the image's edge blends into a dark line. Call with the
|
||||
/// panel's mutex held.
|
||||
[[nodiscard]] inline OverlayRect PlaceOverlayPanel(const OverlayPanel& panel, float left,
|
||||
float top, float right, float bottom) {
|
||||
constexpr float margin = 4.0f;
|
||||
const float width = static_cast<float>(panel.width);
|
||||
const float height = static_cast<float>(panel.height);
|
||||
const float x = panel.anchor == OverlayAnchor::TopRight ? right - width - margin
|
||||
: left + margin;
|
||||
const float y = panel.anchor == OverlayAnchor::BottomLeft
|
||||
? bottom - height - margin
|
||||
: top + margin + (bottom - top) * panel.drop;
|
||||
return {std::floor(x), std::floor(y), width, height};
|
||||
}
|
||||
|
||||
/// The width a top-left panel has before it reaches the panels shown at the top right. Takes
|
||||
/// the other panels' mutexes: never call it with one held.
|
||||
[[nodiscard]] inline float OverlayRoomTopLeft(float screen_width) {
|
||||
constexpr float margin = 4.0f;
|
||||
constexpr float apart = 8.0f;
|
||||
float right_taken = 0.0f;
|
||||
for (auto& panel : GetOverlayPanels()) {
|
||||
std::scoped_lock lock{panel.mutex};
|
||||
if (panel.enabled && panel.anchor == OverlayAnchor::TopRight) {
|
||||
right_taken = (std::max)(right_taken, static_cast<float>(panel.width) + margin + apart);
|
||||
}
|
||||
}
|
||||
return screen_width - margin - right_taken;
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
|
|
@ -202,6 +202,66 @@ struct Values {
|
|||
|
||||
// Core
|
||||
SwitchableSetting<bool> use_multi_core{linkage, true, "use_multi_core", Category::Core};
|
||||
|
||||
/// Show the controller debug overlay (the real pad state plus the logic's actions)
|
||||
SwitchableSetting<bool> console_debug_overlay{linkage, true, "console_debug_overlay",
|
||||
Category::LibraryApplet,
|
||||
Specialization::Default, true, true};
|
||||
/// Modern Console: each player's starting input profile, comma-separated in player order
|
||||
Setting<std::string> modern_seat_profiles{linkage, std::string{}, "modern_seat_profiles",
|
||||
Category::LibraryApplet};
|
||||
/// Name a controller on screen for a moment when it takes a player or leaves one
|
||||
SwitchableSetting<bool> console_hints{linkage, true, "console_hints", Category::LibraryApplet,
|
||||
Specialization::Default, true, true};
|
||||
/// The overlay's button history: shown, how many inputs it keeps, how long each lasts (in
|
||||
/// seconds), and the time between inputs
|
||||
SwitchableSetting<bool> console_history{linkage, true, "console_history",
|
||||
Category::LibraryApplet};
|
||||
SwitchableSetting<int, true> console_history_size{
|
||||
linkage, 10, 5, 20, "console_history_size", Category::LibraryApplet};
|
||||
SwitchableSetting<int, true> console_history_seconds{
|
||||
linkage, 5, 1, 30, "console_history_seconds", Category::LibraryApplet};
|
||||
SwitchableSetting<bool> console_history_gaps{linkage, false, "console_history_gaps",
|
||||
Category::LibraryApplet};
|
||||
/// Under Steam: Eden reads the real controllers instead of Steam's virtual ones. At startup.
|
||||
SwitchableSetting<bool> console_bypass_steam{linkage, false, "console_bypass_steam",
|
||||
Category::LibraryApplet};
|
||||
/// Modern Console: when a game only takes Joy-Cons, controllers act as Joy-Cons
|
||||
SwitchableSetting<bool> console_joycon_auto{linkage, true, "console_joycon_auto",
|
||||
Category::LibraryApplet};
|
||||
/// Modern Console: pause when a player's controller is turned off or gone for 3 seconds,
|
||||
/// resume when a controller takes the player again
|
||||
SwitchableSetting<bool> console_pause_disconnect{linkage, true, "console_pause_disconnect",
|
||||
Category::LibraryApplet};
|
||||
/// Modern Console: controllers' player lights and light bars show their player
|
||||
SwitchableSetting<bool> console_player_lights{linkage, true, "console_player_lights",
|
||||
Category::LibraryApplet};
|
||||
/// Modern Console: Xbox and PlayStation style buttons by label instead of position
|
||||
SwitchableSetting<bool> console_button_labels{linkage, false, "console_button_labels",
|
||||
Category::LibraryApplet};
|
||||
/// Detailed controller traces in the log, for bug reports
|
||||
SwitchableSetting<bool> console_debug_probes{linkage, false, "console_debug_probes",
|
||||
Category::LibraryApplet};
|
||||
|
||||
/// The user picked the handheld's own pad (picker or Identify): auto-detect keeps it
|
||||
SwitchableSetting<bool> builtin_pad_manual{linkage, false, "builtin_pad_manual",
|
||||
Category::Core};
|
||||
|
||||
/// The handheld's own pad identity, latched when Handheld Console Mode engages
|
||||
SwitchableSetting<std::string> builtin_pad_identity{linkage, std::string{},
|
||||
"builtin_pad_identity", Category::Core};
|
||||
|
||||
/// A controller the user removed as the handheld's own (Steam handle, else identity):
|
||||
/// auto-detect skips it until it is allowed again
|
||||
SwitchableSetting<std::string> builtin_pad_blocked{linkage, std::string{},
|
||||
"builtin_pad_blocked", Category::Core};
|
||||
/// Under Steam, the handheld's own pad's Steam handle: stable across sessions, unlike the
|
||||
/// virtual pads' shared GUID
|
||||
SwitchableSetting<std::string> builtin_pad_steam_handle{
|
||||
linkage, std::string{}, "builtin_pad_steam_handle", Category::Core};
|
||||
/// The user picked the Steam handle (picker or Identify): kept whatever the pad reports
|
||||
SwitchableSetting<bool> builtin_pad_steam_manual{linkage, false, "builtin_pad_steam_manual",
|
||||
Category::Core};
|
||||
SwitchableSetting<MemoryLayout, true> memory_layout_mode{linkage,
|
||||
MemoryLayout::Memory_4Gb,
|
||||
"memory_layout_mode",
|
||||
|
|
@ -247,6 +307,7 @@ struct Values {
|
|||
|
||||
SwitchableSetting<bool> sync_core_speed{linkage, false, "sync_core_speed", Category::Core,
|
||||
Specialization::Default};
|
||||
SwitchableSetting<bool> enable_cheats_by_default{linkage, true, "enable_cheats_by_default", Category::Core};
|
||||
|
||||
// Cpu
|
||||
SwitchableSetting<CpuBackend, true> cpu_backend{linkage,
|
||||
|
|
@ -639,8 +700,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,
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, Lanczos, Scale
|
|||
ENUM(AntiAliasing, None, Fxaa, Smaa);
|
||||
ENUM(AspectRatio, R16_9, R4_3, R21_9, R16_10, Stretch);
|
||||
ENUM(ConsoleMode, Handheld, Docked);
|
||||
ENUM(AppletMode, HLE, LLE, Disabled);
|
||||
ENUM(AppletMode, HLE, LLE, Disabled, ConsoleMode, HandheldConsoleMode);
|
||||
ENUM(SpirvOptimizeMode, Never, OnLoad, Always);
|
||||
ENUM(GpuClock, Normal, Boost, Overclock)
|
||||
ENUM(GpuUnswizzleSize, VerySmall, Small, Normal, Large, VeryLarge)
|
||||
|
|
|
|||
|
|
@ -116,20 +116,122 @@ std::string ReplaceAll(std::string result, const std::string& src, const std::st
|
|||
}
|
||||
|
||||
std::string UTF16ToUTF8(std::u16string_view input) {
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
|
||||
return convert.to_bytes(input.data(), input.data() + input.size());
|
||||
std::string result;
|
||||
result.reserve(input.size() * 4);
|
||||
for (size_t i = 0; i < input.size(); ++i) {
|
||||
uint32_t code = input[i];
|
||||
// Handle surrogate pairs
|
||||
if (code >= 0xD800 && code <= 0xDBFF) {
|
||||
if (i + 1 < input.size()) {
|
||||
uint32_t low = input[i + 1];
|
||||
if (low >= 0xDC00 && low <= 0xDFFF) {
|
||||
code = ((code - 0xD800) << 10) + (low - 0xDC00) + 0x10000;
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (code <= 0x7F) {
|
||||
result.push_back(char(code));
|
||||
} else if (code <= 0x7FF) {
|
||||
result.push_back(char(0xC0 | (code >> 6)));
|
||||
result.push_back(char(0x80 | (code & 0x3F)));
|
||||
} else if (code <= 0xFFFF) {
|
||||
result.push_back(char(0xE0 | (code >> 12)));
|
||||
result.push_back(char(0x80 | ((code >> 6) & 0x3F)));
|
||||
result.push_back(char(0x80 | (code & 0x3F)));
|
||||
} else {
|
||||
result.push_back(char(0xF0 | (code >> 18)));
|
||||
result.push_back(char(0x80 | ((code >> 12) & 0x3F)));
|
||||
result.push_back(char(0x80 | ((code >> 6) & 0x3F)));
|
||||
result.push_back(char(0x80 | (code & 0x3F)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::u16string UTF8ToUTF16(std::string_view input) {
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
|
||||
return convert.from_bytes(input.data(), input.data() + input.size());
|
||||
std::u16string result;
|
||||
result.reserve(input.size() * 2);
|
||||
for (size_t i = 0; i < input.size(); ) {
|
||||
uint32_t code = 0;
|
||||
unsigned char c = input[i];
|
||||
size_t len = 0;
|
||||
if ((c & 0x80) == 0) {
|
||||
code = c;
|
||||
len = 1;
|
||||
} else if ((c & 0xE0) == 0xC0) {
|
||||
code = c & 0x1F;
|
||||
len = 2;
|
||||
} else if ((c & 0xF0) == 0xE0) {
|
||||
code = c & 0x0F;
|
||||
len = 3;
|
||||
} else if ((c & 0xF8) == 0xF0) {
|
||||
code = c & 0x07;
|
||||
len = 4;
|
||||
} else {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (i + len - 1 >= input.size())
|
||||
break;
|
||||
for (size_t j = 1; j <= len - 1; ++j) {
|
||||
if ((input[i + j] & 0xC0) != 0x80) {
|
||||
code = 0xFFFD;
|
||||
break;
|
||||
}
|
||||
code = (code << 6) | (input[i + j] & 0x3F);
|
||||
}
|
||||
if (code <= 0xFFFF) {
|
||||
result.push_back(char16_t(code));
|
||||
} else {
|
||||
code -= 0x10000;
|
||||
result.push_back(char16_t(0xD800 + (code >> 10)));
|
||||
result.push_back(char16_t(0xDC00 + (code & 0x3FF)));
|
||||
}
|
||||
i += len;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::u32string UTF8ToUTF32(std::string_view input) {
|
||||
std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> convert;
|
||||
return convert.from_bytes(input.data(), input.data() + input.size());
|
||||
std::u32string result;
|
||||
result.reserve(input.size());
|
||||
for (size_t i = 0; i < input.size(); ) {
|
||||
uint32_t code = 0;
|
||||
unsigned char c = input[i];
|
||||
size_t len = 0;
|
||||
if ((c & 0x80) == 0) {
|
||||
code = c;
|
||||
len = 1;
|
||||
} else if ((c & 0xE0) == 0xC0) {
|
||||
code = c & 0x1F;
|
||||
len = 2;
|
||||
} else if ((c & 0xF0) == 0xE0) {
|
||||
code = c & 0x0F;
|
||||
len = 3;
|
||||
} else if ((c & 0xF8) == 0xF0) {
|
||||
code = c & 0x07;
|
||||
len = 4;
|
||||
} else {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (i + len - 1 >= input.size())
|
||||
break;
|
||||
for (size_t j = 1; j <= len - 1; ++j) {
|
||||
if ((input[i + j] & 0xC0) != 0x80) {
|
||||
code = 0xFFFD;
|
||||
break;
|
||||
}
|
||||
code = (code << 6) | (input[i + j] & 0x3F);
|
||||
}
|
||||
result.push_back(code);
|
||||
i += len;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
#ifdef _WIN32
|
||||
static std::wstring CPToUTF16(u32 code_page, std::string_view input) {
|
||||
const auto size =
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -814,6 +786,17 @@ void System::RegisterCheatList(const std::vector<Memory::CheatEntry>& list,
|
|||
impl->cheat_engine->SetMainMemoryParameters(main_region_begin, main_region_size);
|
||||
}
|
||||
|
||||
std::vector<Memory::RuntimeCheatInfo> System::GetRuntimeCheats() const {
|
||||
if (!impl->cheat_engine) {
|
||||
return {};
|
||||
}
|
||||
return impl->cheat_engine->GetCheats();
|
||||
}
|
||||
|
||||
bool System::SetCheatEnabled(u32 cheat_id, bool enabled) {
|
||||
return impl->cheat_engine && impl->cheat_engine->SetCheatEnabled(cheat_id, enabled);
|
||||
}
|
||||
|
||||
void System::SetFrontendAppletSet(Service::AM::Frontend::FrontendAppletSet&& set) {
|
||||
impl->frontend_applets.SetFrontendAppletSet(std::move(set));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ enum class ResultStatus : u16;
|
|||
|
||||
namespace Core::Memory {
|
||||
struct CheatEntry;
|
||||
struct RuntimeCheatInfo;
|
||||
class Memory;
|
||||
} // namespace Core::Memory
|
||||
|
||||
|
|
@ -191,10 +192,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.
|
||||
|
|
@ -344,6 +343,8 @@ public:
|
|||
void RegisterCheatList(const std::vector<Memory::CheatEntry>& list,
|
||||
const std::array<u8, 0x20>& build_id, u64 main_region_begin,
|
||||
u64 main_region_size);
|
||||
[[nodiscard]] std::vector<Memory::RuntimeCheatInfo> GetRuntimeCheats() const;
|
||||
bool SetCheatEnabled(u32 cheat_id, bool enabled);
|
||||
|
||||
void SetFrontendAppletSet(Service::AM::Frontend::FrontendAppletSet&& set);
|
||||
|
||||
|
|
|
|||
472
src/core/crypto/ncz_decompressor.cpp
Normal file
472
src/core/crypto/ncz_decompressor.cpp
Normal file
|
|
@ -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
|
||||
35
src/core/crypto/ncz_decompressor.h
Normal file
35
src/core/crypto/ncz_decompressor.h
Normal file
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -503,10 +503,11 @@ std::vector<Core::Memory::CheatEntry> PatchManager::CreateCheatList(const BuildI
|
|||
for (const auto& subdir : patch_dirs) {
|
||||
if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) == disabled.cend()) {
|
||||
if (auto cheats_dir = FindSubdirectoryCaseless(subdir, "cheats"); cheats_dir != nullptr) {
|
||||
if (auto const res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, true))
|
||||
std::copy(res->begin(), res->end(), std::back_inserter(out));
|
||||
if (auto const res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, false))
|
||||
if (auto const res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, true)) {
|
||||
std::copy(res->begin(), res->end(), std::back_inserter(out));
|
||||
} else if (auto const lower_res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, false)) {
|
||||
std::copy(lower_res->begin(), lower_res->end(), std::back_inserter(out));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ Result IDeliveryCacheFileService::Open(const DirectoryName& dir_name_raw,
|
|||
LOG_DEBUG(Service_BCAT, "called, dir_name={}, file_name={}", dir_name, file_name);
|
||||
|
||||
R_TRY(VerifyNameValidDir(dir_name_raw));
|
||||
R_TRY(VerifyNameValidDir(file_name_raw));
|
||||
R_TRY(VerifyNameValidFile(file_name_raw));
|
||||
R_UNLESS(current_file == nullptr, ResultEntityAlreadyOpen);
|
||||
|
||||
const auto dir = root->GetSubdirectory(dir_name);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -22,6 +26,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"
|
||||
|
|
@ -18,6 +20,10 @@ public:
|
|||
explicit IStorage(Core::System& system_, FileSys::VirtualFile backend_);
|
||||
|
||||
private:
|
||||
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"
|
||||
|
|
@ -38,7 +34,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_}
|
||||
{}
|
||||
|
|
@ -87,7 +82,6 @@ void ServiceFrameworkBase::ReportUnimplementedFunction(HLERequestContext& ctx, c
|
|||
}
|
||||
|
||||
void ServiceFrameworkBase::InvokeRequest(HLERequestContext& ctx) {
|
||||
const bool is_cmd_read = ctx.GetCommand() == 0;
|
||||
auto const info = FindRequest(ctx.GetCommand());
|
||||
if (!info.has_value() || info->handler_callback == nullptr)
|
||||
return ReportUnimplementedFunction(ctx, &*info, false);
|
||||
|
|
@ -99,12 +93,6 @@ void ServiceFrameworkBase::InvokeRequest(HLERequestContext& ctx) {
|
|||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -116,8 +116,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.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#include <iphlpapi.h>
|
||||
#elif defined(__linux__) || defined(__ANDROID__)
|
||||
#elif defined(__linux__) || defined(__ANDROID__) || defined(__APPLE__)
|
||||
#include <cerrno>
|
||||
#include <ifaddrs.h>
|
||||
#include <net/if.h>
|
||||
|
|
@ -33,6 +33,13 @@
|
|||
#include <netinet/if_ether.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
// Darwin doesn't define this for some very odd reason
|
||||
// See https://stackoverflow.com/questions/5390164/getting-routing-table-on-macosx-programmatically
|
||||
#ifndef SA_SIZE
|
||||
#define SA_SIZE(sa) \
|
||||
((!(sa) || ((struct sockaddr *)(sa))->sa_len == 0) ? sizeof(long) \
|
||||
: 1 + ( (((struct sockaddr *)(sa))->sa_len - 1) | (sizeof(long) - 1) ) )
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include "common/common_types.h"
|
||||
|
|
@ -104,7 +111,7 @@ std::vector<Network::NetworkInterface> GetAvailableNetworkInterfaces() {
|
|||
#else
|
||||
|
||||
std::vector<Network::NetworkInterface> GetAvailableNetworkInterfaces() {
|
||||
#if defined(__ANDROID__) || defined(__linux__)
|
||||
#if defined(__ANDROID__) || defined(__linux__) || defined(__APPLE__) || defined(__managarm__)
|
||||
struct ifaddrs* ifaddr = nullptr;
|
||||
if (getifaddrs(&ifaddr) != 0) {
|
||||
LOG_ERROR(Network, "getifaddrs: {}", std::strerror(errno));
|
||||
|
|
@ -119,8 +126,9 @@ std::vector<Network::NetworkInterface> GetAvailableNetworkInterfaces() {
|
|||
u32 flags;
|
||||
};
|
||||
std::vector<RoutingEntry> routes{};
|
||||
#ifdef __ANDROID__
|
||||
#if defined(__ANDROID__) || defined(__APPLE__)
|
||||
// Even through Linux based, we can't reliably obtain routing information from there :(
|
||||
// macOS not Linux based and would murder us if we attempt to access /proc
|
||||
#else
|
||||
if (std::ifstream file("/proc/net/route"); file.is_open()) {
|
||||
file.ignore((std::numeric_limits<std::streamsize>::max)(), '\n'); //ignore header
|
||||
|
|
@ -138,7 +146,10 @@ std::vector<Network::NetworkInterface> GetAvailableNetworkInterfaces() {
|
|||
std::vector<Network::NetworkInterface> ifaces;
|
||||
for (auto ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) {
|
||||
if (ifa->ifa_addr == nullptr || ifa->ifa_netmask == nullptr /* Have a netmask and address */
|
||||
// Apple hates human beings so lets pretend all families are fine
|
||||
#if !defined(__APPLE__) && !defined(__managarm__)
|
||||
|| ifa->ifa_addr->sa_family != AF_INET /* Must be of kind AF_INET */
|
||||
#endif
|
||||
|| (ifa->ifa_flags & IFF_UP) == 0 || (ifa->ifa_flags & IFF_LOOPBACK) != 0) /* Not loopback */
|
||||
continue;
|
||||
// Just use 0 as the gateway address if not found OR routes are empty :)
|
||||
|
|
@ -158,7 +169,7 @@ std::vector<Network::NetworkInterface> GetAvailableNetworkInterfaces() {
|
|||
}
|
||||
freeifaddrs(ifaddr);
|
||||
return ifaces;
|
||||
#elif defined(__FreeBSD__)
|
||||
#elif defined(__FreeBSD__) || defined(__APPLE__)
|
||||
std::vector<Network::NetworkInterface> ifaces;
|
||||
int fd = ::socket(PF_ROUTE, SOCK_RAW, AF_UNSPEC);
|
||||
if (fd < 0) {
|
||||
|
|
@ -198,11 +209,15 @@ std::vector<Network::NetworkInterface> GetAvailableNetworkInterfaces() {
|
|||
if (msglen == 0 || msglen < SA_SIZE(sa))
|
||||
break;
|
||||
if (i == RTA_NETMASK && sa->sa_family == AF_LINK) {
|
||||
size_t namelen = 0;
|
||||
struct sockaddr_dl const* sdl = reinterpret_cast<struct sockaddr_dl const*>(sa);
|
||||
#if defined(__FreeBSD__) && __FreeBSD__ < 15
|
||||
iface.name = std::string{::link_ntoa(sdl)};
|
||||
#else
|
||||
size_t namelen = 0;
|
||||
::link_ntoa_r(sdl, nullptr, &namelen);
|
||||
iface.name = std::string(namelen, ' ');
|
||||
::link_ntoa_r(sdl, iface.name.data(), &namelen);
|
||||
#endif
|
||||
std::memcpy(&iface.ip_address, sa, sizeof(struct sockaddr_in));
|
||||
}
|
||||
msglen -= SA_SIZE(sa);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -757,7 +757,7 @@ struct Memory::Impl {
|
|||
};
|
||||
auto& gpu = system.GPU();
|
||||
gpu_device_memory->ApplyOpOnPointer(
|
||||
p, scratch_buffers[core], [&](DAddr address) { gpu.InvalidateRegion(address, size, true); });
|
||||
p, scratch_buffers[core], [&](DAddr address) { gpu.InvalidateRegion(address, size); });
|
||||
}
|
||||
|
||||
Core::System& system;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cctype>
|
||||
#include <locale>
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging.h"
|
||||
#include "common/settings.h"
|
||||
#include "common/swap.h"
|
||||
#include "core/arm/debug.h"
|
||||
#include "core/core.h"
|
||||
|
|
@ -28,11 +31,11 @@ constexpr auto CHEAT_ENGINE_NS = std::chrono::nanoseconds{1000000000 / 12};
|
|||
std::string_view ExtractName(std::size_t& out_name_size, std::string_view data,
|
||||
std::size_t start_index, char match) {
|
||||
auto end_index = start_index;
|
||||
while (data[end_index] != match) {
|
||||
while (end_index < data.size() && data[end_index] != match) {
|
||||
++end_index;
|
||||
if (end_index > data.size()) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (end_index == data.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
out_name_size = end_index - start_index;
|
||||
|
|
@ -130,6 +133,11 @@ void StandardVmCallbacks::CommandLog(std::string_view data) {
|
|||
data.back() == '\n' ? data.substr(0, data.size() - 1) : data);
|
||||
}
|
||||
|
||||
bool StandardVmCallbacks::IsCommandLogEnabled() const {
|
||||
return Common::Log::IsMessageEnabled(Common::Log::Class::CheatEngine,
|
||||
Common::Log::Level::Debug);
|
||||
}
|
||||
|
||||
bool StandardVmCallbacks::IsAddressInRange(VAddr in) const {
|
||||
if ((in < metadata.main_nso_extents.base ||
|
||||
in >= metadata.main_nso_extents.base + metadata.main_nso_extents.size) &&
|
||||
|
|
@ -166,8 +174,9 @@ std::vector<CheatEntry> TextCheatParser::Parse(std::string_view data) const {
|
|||
|
||||
if (data[i] == '{') {
|
||||
current_entry = 0;
|
||||
out[*current_entry].is_master = true;
|
||||
|
||||
if (out[*current_entry].definition.num_opcodes > 0) {
|
||||
if (!out[*current_entry].definition.opcodes.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
@ -203,32 +212,33 @@ std::vector<CheatEntry> TextCheatParser::Parse(std::string_view data) const {
|
|||
'\0';
|
||||
|
||||
i += name_size + 1;
|
||||
} else if (::isxdigit(data[i])) {
|
||||
if (!current_entry || out[*current_entry].definition.num_opcodes >=
|
||||
out[*current_entry].definition.opcodes.size()) {
|
||||
} else if (std::isxdigit(static_cast<unsigned char>(data[i]))) {
|
||||
if (!current_entry || data.size() - i < 8) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto hex = std::string(data.substr(i, 8));
|
||||
if (!std::all_of(hex.begin(), hex.end(), ::isxdigit)) {
|
||||
if (!std::all_of(hex.begin(), hex.end(), [](char value) {
|
||||
return std::isxdigit(static_cast<unsigned char>(value));
|
||||
})) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto value = static_cast<u32>(std::strtoul(hex.c_str(), nullptr, 0x10));
|
||||
out[*current_entry].definition.opcodes[out[*current_entry].definition.num_opcodes++] =
|
||||
value;
|
||||
out[*current_entry].definition.opcodes.push_back(value);
|
||||
|
||||
i += 8;
|
||||
i += 7;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
out[0].enabled = out[0].definition.num_opcodes > 0;
|
||||
out[0].enabled = out[0].is_master && !out[0].definition.opcodes.empty();
|
||||
out[0].cheat_id = 0;
|
||||
|
||||
for (u32 i = 1; i < out.size(); ++i) {
|
||||
out[i].enabled = out[i].definition.num_opcodes > 0;
|
||||
out[i].enabled = Settings::values.enable_cheats_by_default.GetValue() &&
|
||||
!out[i].definition.opcodes.empty();
|
||||
out[i].cheat_id = i;
|
||||
}
|
||||
|
||||
|
|
@ -240,6 +250,10 @@ CheatEngine::CheatEngine(System& system_, std::vector<CheatEntry> cheats_,
|
|||
: vm{std::make_unique<StandardVmCallbacks>(system_, metadata)},
|
||||
cheats(std::move(cheats_)), core_timing{system_.CoreTiming()}, system{system_} {
|
||||
metadata.main_nso_build_id = build_id_;
|
||||
for (u32 i = 0; i < cheats.size(); ++i) {
|
||||
cheats[i].cheat_id = i;
|
||||
cheats[i].enabled = cheats[i].is_master || cheats[i].enabled;
|
||||
}
|
||||
}
|
||||
|
||||
CheatEngine::~CheatEngine() {
|
||||
|
|
@ -286,12 +300,56 @@ void CheatEngine::SetMainMemoryParameters(VAddr main_region_begin, u64 main_regi
|
|||
}
|
||||
|
||||
void CheatEngine::Reload(std::vector<CheatEntry> reload_cheats) {
|
||||
cheats = std::move(reload_cheats);
|
||||
is_pending_reload.exchange(true);
|
||||
{
|
||||
std::scoped_lock lock{cheats_mutex};
|
||||
cheats = std::move(reload_cheats);
|
||||
for (u32 i = 0; i < cheats.size(); ++i) {
|
||||
cheats[i].cheat_id = i;
|
||||
cheats[i].enabled = cheats[i].is_master || cheats[i].enabled;
|
||||
}
|
||||
}
|
||||
is_pending_reload.store(true);
|
||||
}
|
||||
|
||||
std::vector<RuntimeCheatInfo> CheatEngine::GetCheats() const {
|
||||
std::scoped_lock lock{cheats_mutex};
|
||||
std::vector<RuntimeCheatInfo> result;
|
||||
result.reserve(cheats.size());
|
||||
for (const auto& cheat : cheats) {
|
||||
if (cheat.definition.opcodes.empty()) {
|
||||
continue;
|
||||
}
|
||||
result.push_back({
|
||||
.id = cheat.cheat_id,
|
||||
.name = cheat.definition.readable_name.data(),
|
||||
.enabled = cheat.enabled || cheat.is_master,
|
||||
.is_master = cheat.is_master,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool CheatEngine::SetCheatEnabled(u32 cheat_id, bool enabled) {
|
||||
std::scoped_lock lock{cheats_mutex};
|
||||
const auto entry = std::find_if(cheats.begin(), cheats.end(), [cheat_id](const auto& cheat) {
|
||||
return cheat.cheat_id == cheat_id && !cheat.definition.opcodes.empty();
|
||||
});
|
||||
if (entry == cheats.end()) {
|
||||
return false;
|
||||
}
|
||||
if (entry->is_master) {
|
||||
return enabled;
|
||||
}
|
||||
if (entry->enabled != enabled) {
|
||||
entry->enabled = enabled;
|
||||
is_pending_reload.store(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CheatEngine::FrameCallback(std::chrono::nanoseconds ns_late) {
|
||||
if (is_pending_reload.exchange(false)) {
|
||||
std::scoped_lock lock{cheats_mutex};
|
||||
vm.LoadProgram(cheats);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include "common/common_types.h"
|
||||
#include "core/memory/dmnt_cheat_types.h"
|
||||
|
|
@ -41,6 +42,7 @@ public:
|
|||
void ResumeProcess() override;
|
||||
void DebugLog(u8 id, u64 value) override;
|
||||
void CommandLog(std::string_view data) override;
|
||||
[[nodiscard]] bool IsCommandLogEnabled() const override;
|
||||
|
||||
private:
|
||||
bool IsAddressInRange(VAddr address) const;
|
||||
|
|
@ -81,6 +83,8 @@ public:
|
|||
void SetMainMemoryParameters(VAddr main_region_begin, u64 main_region_size);
|
||||
|
||||
void Reload(std::vector<CheatEntry> reload_cheats);
|
||||
[[nodiscard]] std::vector<RuntimeCheatInfo> GetCheats() const;
|
||||
bool SetCheatEnabled(u32 cheat_id, bool enabled);
|
||||
|
||||
private:
|
||||
void FrameCallback(std::chrono::nanoseconds ns_late);
|
||||
|
|
@ -89,6 +93,7 @@ private:
|
|||
CheatProcessMetadata metadata;
|
||||
|
||||
std::vector<CheatEntry> cheats;
|
||||
mutable std::mutex cheats_mutex;
|
||||
std::atomic_bool is_pending_reload{false};
|
||||
|
||||
std::shared_ptr<Core::Timing::EventType> event;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
// 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
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Core::Memory {
|
||||
|
|
@ -24,14 +30,21 @@ struct CheatProcessMetadata {
|
|||
|
||||
struct CheatDefinition {
|
||||
std::array<char, 0x40> readable_name{};
|
||||
u32 num_opcodes{};
|
||||
std::array<u32, 0x100> opcodes{};
|
||||
std::vector<u32> opcodes;
|
||||
};
|
||||
|
||||
struct CheatEntry {
|
||||
bool enabled{};
|
||||
bool is_master{};
|
||||
u32 cheat_id{};
|
||||
CheatDefinition definition{};
|
||||
};
|
||||
|
||||
struct RuntimeCheatInfo {
|
||||
u32 id{};
|
||||
std::string name;
|
||||
bool enabled{};
|
||||
bool is_master{};
|
||||
};
|
||||
|
||||
} // namespace Core::Memory
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -233,7 +236,7 @@ bool DmntCheatVm::DecodeNextOpcode(CheatVmOpcode& out) {
|
|||
|
||||
// Helper function for getting instruction dwords.
|
||||
const auto GetNextDword = [&] {
|
||||
if (instruction_ptr >= num_opcodes) {
|
||||
if (instruction_ptr >= program.size()) {
|
||||
valid = false;
|
||||
return static_cast<u32>(0);
|
||||
}
|
||||
|
|
@ -709,20 +712,20 @@ void DmntCheatVm::ResetState() {
|
|||
}
|
||||
|
||||
bool DmntCheatVm::LoadProgram(const std::vector<CheatEntry>& entries) {
|
||||
// Reset opcode count.
|
||||
num_opcodes = 0;
|
||||
program.clear();
|
||||
|
||||
for (std::size_t i = 0; i < entries.size(); i++) {
|
||||
if (entries[i].enabled) {
|
||||
// Bounds check.
|
||||
if (entries[i].definition.num_opcodes + num_opcodes > MaximumProgramOpcodeCount) {
|
||||
num_opcodes = 0;
|
||||
return false;
|
||||
}
|
||||
std::size_t program_size{};
|
||||
for (const auto& entry : entries) {
|
||||
if (entry.enabled || entry.is_master) {
|
||||
program_size += entry.definition.opcodes.size();
|
||||
}
|
||||
}
|
||||
program.reserve(program_size);
|
||||
|
||||
for (std::size_t n = 0; n < entries[i].definition.num_opcodes; n++) {
|
||||
program[num_opcodes++] = entries[i].definition.opcodes[n];
|
||||
}
|
||||
for (const auto& entry : entries) {
|
||||
if (entry.enabled || entry.is_master) {
|
||||
program.insert(program.end(), entry.definition.opcodes.begin(),
|
||||
entry.definition.opcodes.end());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -735,27 +738,35 @@ void DmntCheatVm::Execute(const CheatProcessMetadata& metadata) {
|
|||
// Get Keys down.
|
||||
u64 kDown = callbacks->HidKeysDown();
|
||||
|
||||
callbacks->CommandLog("Started VM execution.");
|
||||
callbacks->CommandLog(fmt::format("Main NSO: {:012X}", metadata.main_nso_extents.base));
|
||||
callbacks->CommandLog(fmt::format("Heap: {:012X}", metadata.main_nso_extents.base));
|
||||
callbacks->CommandLog(fmt::format("Keys Down: {:08X}", static_cast<u32>(kDown & 0x0FFFFFFF)));
|
||||
const bool command_log_enabled = callbacks->IsCommandLogEnabled();
|
||||
if (command_log_enabled) {
|
||||
callbacks->CommandLog("Started VM execution.");
|
||||
callbacks->CommandLog(fmt::format("Main NSO: {:012X}", metadata.main_nso_extents.base));
|
||||
callbacks->CommandLog(fmt::format("Heap: {:012X}", metadata.main_nso_extents.base));
|
||||
callbacks->CommandLog(
|
||||
fmt::format("Keys Down: {:08X}", static_cast<u32>(kDown & 0x0FFFFFFF)));
|
||||
}
|
||||
|
||||
// Clear VM state.
|
||||
ResetState();
|
||||
|
||||
// Loop until program finishes.
|
||||
while (DecodeNextOpcode(cur_opcode)) {
|
||||
callbacks->CommandLog(
|
||||
fmt::format("Instruction Ptr: {:04X}", static_cast<u32>(instruction_ptr)));
|
||||
if (command_log_enabled) {
|
||||
callbacks->CommandLog(
|
||||
fmt::format("Instruction Ptr: {:04X}", static_cast<u32>(instruction_ptr)));
|
||||
|
||||
for (std::size_t i = 0; i < NumRegisters; i++) {
|
||||
callbacks->CommandLog(fmt::format("Registers[{:02X}]: {:016X}", i, registers[i]));
|
||||
}
|
||||
for (std::size_t i = 0; i < NumRegisters; i++) {
|
||||
callbacks->CommandLog(fmt::format("Registers[{:02X}]: {:016X}", i,
|
||||
registers[i]));
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < NumRegisters; i++) {
|
||||
callbacks->CommandLog(fmt::format("SavedRegs[{:02X}]: {:016X}", i, saved_values[i]));
|
||||
for (std::size_t i = 0; i < NumRegisters; i++) {
|
||||
callbacks->CommandLog(fmt::format("SavedRegs[{:02X}]: {:016X}", i,
|
||||
saved_values[i]));
|
||||
}
|
||||
LogOpcode(cur_opcode);
|
||||
}
|
||||
LogOpcode(cur_opcode);
|
||||
|
||||
// Increment conditional depth, if relevant.
|
||||
if (cur_opcode.begin_conditional_block) {
|
||||
|
|
|
|||
|
|
@ -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 2019 yuzu Emulator Project
|
||||
|
|
@ -281,9 +281,9 @@ public:
|
|||
|
||||
virtual void DebugLog(u8 id, u64 value) = 0;
|
||||
virtual void CommandLog(std::string_view data) = 0;
|
||||
[[nodiscard]] virtual bool IsCommandLogEnabled() const = 0;
|
||||
};
|
||||
|
||||
static constexpr std::size_t MaximumProgramOpcodeCount = 0x400;
|
||||
static constexpr std::size_t NumRegisters = 0x10;
|
||||
static constexpr std::size_t NumReadableStaticRegisters = 0x80;
|
||||
static constexpr std::size_t NumWritableStaticRegisters = 0x80;
|
||||
|
|
@ -294,7 +294,7 @@ public:
|
|||
~DmntCheatVm();
|
||||
|
||||
std::size_t GetProgramSize() const {
|
||||
return this->num_opcodes;
|
||||
return program.size();
|
||||
}
|
||||
|
||||
bool LoadProgram(const std::vector<CheatEntry>& cheats);
|
||||
|
|
@ -303,11 +303,10 @@ public:
|
|||
private:
|
||||
std::unique_ptr<Callbacks> callbacks;
|
||||
|
||||
std::size_t num_opcodes = 0;
|
||||
std::size_t instruction_ptr = 0;
|
||||
std::size_t condition_depth = 0;
|
||||
bool decode_success = false;
|
||||
std::array<u32, MaximumProgramOpcodeCount> program{};
|
||||
std::vector<u32> program;
|
||||
std::array<u64, NumRegisters> registers{};
|
||||
std::array<u64, NumRegisters> saved_values{};
|
||||
std::array<u64, NumStaticRegisters> static_registers{};
|
||||
|
|
|
|||
|
|
@ -11,34 +11,15 @@ set(dynarmic_VERSION ${dynarmic_VERSION_MAJOR}.${dynarmic_VERSION_MINOR}.${dynar
|
|||
|
||||
project(dynarmic LANGUAGES C CXX ASM VERSION ${dynarmic_VERSION})
|
||||
|
||||
# Determine if we're built as a subproject (using add_subdirectory)
|
||||
# or if this is the master project.
|
||||
set(MASTER_PROJECT OFF)
|
||||
if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
|
||||
set(MASTER_PROJECT ON)
|
||||
endif()
|
||||
|
||||
if (MASTER_PROJECT)
|
||||
include(CTest)
|
||||
endif()
|
||||
|
||||
# Dynarmic project options
|
||||
option(DYNARMIC_ENABLE_CPU_FEATURE_DETECTION "Turning this off causes dynarmic to assume the host CPU doesn't support anything later than SSE3" ON)
|
||||
|
||||
if (OPENBSD OR DRAGONFLY OR NETBSD)
|
||||
set(REQUIRE_WX ON)
|
||||
else()
|
||||
set(REQUIRE_WX OFF)
|
||||
endif()
|
||||
option(DYNARMIC_ENABLE_NO_EXECUTE_SUPPORT "Enables support for systems that require W^X" ${REQUIRE_WX})
|
||||
|
||||
option(DYNARMIC_IGNORE_ASSERTS "Ignore asserts" ON)
|
||||
option(DYNARMIC_TESTS_USE_UNICORN "Enable fuzzing tests against unicorn" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DYNARMIC_USE_LLVM "Support disassembly of jitted x86_64 code using LLVM" OFF "NOT YUZU_DISABLE_LLVM" OFF)
|
||||
|
||||
option(DYNARMIC_INSTALL "Install dynarmic headers and CMake files" OFF)
|
||||
option(DYNARMIC_USE_BUNDLED_EXTERNALS "Use all bundled externals (useful when e.g. cross-compiling)" OFF)
|
||||
|
||||
# Set hard requirements for C++
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
|
@ -88,7 +69,7 @@ if (MSVC)
|
|||
-Wno-missing-braces)
|
||||
endif()
|
||||
else()
|
||||
set(DYNARMIC_CXX_FLAGS
|
||||
list(APPEND DYNARMIC_CXX_FLAGS
|
||||
-Wall
|
||||
-Wextra
|
||||
-Wcast-qual
|
||||
|
|
@ -103,37 +84,35 @@ else()
|
|||
# to Address& after first checking isMEM(), and that code is inlined in a situation where
|
||||
# GCC knows that the variable is actually a Reg64. isMEM() will never return true for a
|
||||
# Reg64, but GCC doesn't know that.
|
||||
list(APPEND DYNARMIC_CXX_FLAGS -Wno-array-bounds)
|
||||
list(APPEND DYNARMIC_CXX_FLAGS -Wstack-usage=4096)
|
||||
list(APPEND DYNARMIC_CXX_FLAGS
|
||||
-Wno-array-bounds
|
||||
-Wstack-usage=4096)
|
||||
endif()
|
||||
if (CXX_CLANG)
|
||||
# Bracket depth determines maximum size of a fold expression in Clang since 9c9974c3ccb6.
|
||||
# And this in turns limits the size of a std::array.
|
||||
list(APPEND DYNARMIC_CXX_FLAGS -fbracket-depth=1024)
|
||||
# Clang mistakenly blames CMake for using unused arguments during compilation
|
||||
list(APPEND DYNARMIC_CXX_FLAGS -Wno-unused-command-line-argument)
|
||||
list(APPEND DYNARMIC_CXX_FLAGS
|
||||
# Bracket depth determines maximum size of a fold expression in Clang since 9c9974c3ccb6.
|
||||
# And this in turns limits the size of a std::array.
|
||||
-fbracket-depth=1024
|
||||
# Clang mistakenly blames CMake for using unused arguments during compilation
|
||||
-Wno-unused-command-line-argument)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (NOT Boost_FOUND)
|
||||
find_package(Boost 1.57 REQUIRED)
|
||||
endif()
|
||||
|
||||
find_package(fmt 8 CONFIG)
|
||||
|
||||
if ("arm64" IN_LIST ARCHITECTURE OR DYNARMIC_TESTS)
|
||||
if (ARCHITECTURE_arm64)
|
||||
find_package(oaknut 2.0.1 CONFIG)
|
||||
endif()
|
||||
|
||||
if ("riscv64" IN_LIST ARCHITECTURE)
|
||||
if (ARCHITECTURE_riscv64)
|
||||
find_package(biscuit 0.9.1 REQUIRED)
|
||||
endif()
|
||||
|
||||
if ("loongarch64" IN_LIST ARCHITECTURE)
|
||||
if (ARCHITECTURE_loongarch64)
|
||||
find_package(lagoon REQUIRED)
|
||||
endif()
|
||||
|
||||
if ("x86_64" IN_LIST ARCHITECTURE)
|
||||
if (ARCHITECTURE_x86_64)
|
||||
find_package(xbyak 7 CONFIG)
|
||||
endif()
|
||||
|
||||
|
|
@ -142,44 +121,12 @@ if (DYNARMIC_USE_LLVM)
|
|||
separate_arguments(LLVM_DEFINITIONS)
|
||||
endif()
|
||||
|
||||
# Dynarmic project files
|
||||
add_subdirectory(src/dynarmic)
|
||||
if (DYNARMIC_TESTS)
|
||||
find_package(Catch2 3 CONFIG)
|
||||
if (DYNARMIC_TESTS_USE_UNICORN)
|
||||
find_package(Unicorn REQUIRED)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Dynarmic project files
|
||||
add_subdirectory(src/dynarmic)
|
||||
if (DYNARMIC_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
#
|
||||
# Install
|
||||
#
|
||||
if (DYNARMIC_INSTALL)
|
||||
include(GNUInstallDirs)
|
||||
include(CMakePackageConfigHelpers)
|
||||
|
||||
install(TARGETS dynarmic EXPORT dynarmicTargets)
|
||||
install(EXPORT dynarmicTargets
|
||||
NAMESPACE dynarmic::
|
||||
DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/dynarmic"
|
||||
)
|
||||
|
||||
configure_package_config_file(CMakeModules/dynarmicConfig.cmake.in
|
||||
dynarmicConfig.cmake
|
||||
INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/dynarmic"
|
||||
)
|
||||
write_basic_package_version_file(dynarmicConfigVersion.cmake
|
||||
COMPATIBILITY SameMajorVersion
|
||||
)
|
||||
install(FILES
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/dynarmicConfig.cmake"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/dynarmicConfigVersion.cmake"
|
||||
DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/dynarmic"
|
||||
)
|
||||
|
||||
install(DIRECTORY src/dynarmic TYPE INCLUDE FILES_MATCHING PATTERN "*.h")
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
function(target_architecture_specific_sources project arch)
|
||||
if (NOT MULTIARCH_BUILD)
|
||||
target_sources("${project}" PRIVATE ${ARGN})
|
||||
return()
|
||||
endif()
|
||||
|
||||
foreach(input_file IN LISTS ARGN)
|
||||
if(input_file MATCHES ".cpp$")
|
||||
if(NOT IS_ABSOLUTE ${input_file})
|
||||
set(input_file "${CMAKE_CURRENT_SOURCE_DIR}/${input_file}")
|
||||
endif()
|
||||
|
||||
set(output_file "${CMAKE_CURRENT_BINARY_DIR}/arch_gen/${input_file}")
|
||||
add_custom_command(
|
||||
OUTPUT "${output_file}"
|
||||
COMMAND ${CMAKE_COMMAND} "-Darch=${arch}"
|
||||
"-Dinput_file=${input_file}"
|
||||
"-Doutput_file=${output_file}"
|
||||
-P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/impl/TargetArchitectureSpecificSourcesWrapFile.cmake"
|
||||
DEPENDS "${input_file}"
|
||||
VERBATIM
|
||||
)
|
||||
target_sources(${project} PRIVATE "${output_file}")
|
||||
endif()
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
@PACKAGE_INIT@
|
||||
|
||||
include(CMakeFindDependencyMacro)
|
||||
|
||||
set(ARCHITECTURE "@ARCHITECTURE@")
|
||||
|
||||
if (NOT @BUILD_SHARED_LIBS@)
|
||||
find_dependency(Boost 1.57)
|
||||
find_dependency(fmt 9)
|
||||
find_dependency(mcl 0.1.12 EXACT)
|
||||
|
||||
if ("arm64" IN_LIST ARCHITECTURE)
|
||||
find_dependency(oaknut 2.0.1)
|
||||
endif()
|
||||
|
||||
if ("riscv" IN_LIST ARCHITECTURE)
|
||||
find_dependency(biscuit 0.9.1)
|
||||
endif()
|
||||
|
||||
if ("x86_64" IN_LIST ARCHITECTURE)
|
||||
find_dependency(xbyak 7)
|
||||
endif()
|
||||
|
||||
if (@DYNARMIC_USE_LLVM@)
|
||||
find_dependency(LLVM)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
|
||||
|
||||
check_required_components(@PROJECT_NAME@)
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
string(TOUPPER "${arch}" arch)
|
||||
file(READ "${input_file}" f_contents)
|
||||
file(WRITE "${output_file}" "#if defined(ARCHITECTURE_${arch})\n${f_contents}\n#endif\n")
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
include(TargetArchitectureSpecificSources)
|
||||
|
||||
add_library(dynarmic STATIC
|
||||
mcl/bit.hpp
|
||||
mcl/function_info.hpp
|
||||
|
|
@ -135,7 +133,7 @@ add_library(dynarmic STATIC
|
|||
interface/A64/config.h
|
||||
)
|
||||
|
||||
if ("x86_64" IN_LIST ARCHITECTURE)
|
||||
if (ARCHITECTURE_x86_64)
|
||||
# Newer versions of xbyak (>= 7.25.0) have stricter checks that currently
|
||||
# fail in dynarmic
|
||||
target_compile_definitions(dynarmic PRIVATE XBYAK_STRICT_CHECK_MEM_REG_SIZE=0)
|
||||
|
|
@ -143,7 +141,7 @@ if ("x86_64" IN_LIST ARCHITECTURE)
|
|||
target_compile_definitions(dynarmic PRIVATE XBYAK_OLD_DISP_CHECK=1)
|
||||
target_link_libraries(dynarmic PRIVATE xbyak::xbyak)
|
||||
|
||||
target_architecture_specific_sources(dynarmic "x86_64"
|
||||
target_sources(dynarmic PRIVATE
|
||||
backend/x64/abi.cpp
|
||||
backend/x64/abi.h
|
||||
backend/x64/block_of_code.cpp
|
||||
|
|
@ -201,10 +199,10 @@ if ("x86_64" IN_LIST ARCHITECTURE)
|
|||
)
|
||||
endif()
|
||||
|
||||
if ("arm64" IN_LIST ARCHITECTURE)
|
||||
if (ARCHITECTURE_arm64)
|
||||
target_link_libraries(dynarmic PRIVATE merry::oaknut)
|
||||
|
||||
target_architecture_specific_sources(dynarmic "arm64"
|
||||
target_sources(dynarmic PRIVATE
|
||||
backend/arm64/a32_jitstate.cpp
|
||||
backend/arm64/a32_jitstate.h
|
||||
backend/arm64/a64_jitstate.h
|
||||
|
|
@ -255,7 +253,7 @@ if ("arm64" IN_LIST ARCHITECTURE)
|
|||
)
|
||||
endif()
|
||||
|
||||
if ("riscv64" IN_LIST ARCHITECTURE)
|
||||
if (ARCHITECTURE_riscv64)
|
||||
target_link_libraries(dynarmic PRIVATE biscuit::biscuit)
|
||||
|
||||
target_sources(dynarmic PRIVATE
|
||||
|
|
@ -295,7 +293,7 @@ if ("riscv64" IN_LIST ARCHITECTURE)
|
|||
message(WARNING "TODO: Incomplete frontend for this host architecture")
|
||||
endif()
|
||||
|
||||
if ("loongarch64" IN_LIST ARCHITECTURE)
|
||||
if (ARCHITECTURE_loongarch64)
|
||||
target_link_libraries(dynarmic PRIVATE lagoon::lagoon)
|
||||
|
||||
target_sources(dynarmic PRIVATE
|
||||
|
|
@ -400,15 +398,9 @@ if (DYNARMIC_USE_LLVM)
|
|||
target_compile_definitions(dynarmic PRIVATE DYNARMIC_USE_LLVM=1 ${LLVM_DEFINITIONS})
|
||||
llvm_config(dynarmic USE_SHARED armdesc armdisassembler aarch64desc aarch64disassembler x86desc x86disassembler)
|
||||
endif()
|
||||
if (DYNARMIC_ENABLE_CPU_FEATURE_DETECTION)
|
||||
target_compile_definitions(dynarmic PRIVATE DYNARMIC_ENABLE_CPU_FEATURE_DETECTION=1)
|
||||
endif()
|
||||
if (DYNARMIC_ENABLE_NO_EXECUTE_SUPPORT)
|
||||
target_compile_definitions(dynarmic PRIVATE DYNARMIC_ENABLE_NO_EXECUTE_SUPPORT=1)
|
||||
endif()
|
||||
if (DYNARMIC_IGNORE_ASSERTS)
|
||||
target_compile_definitions(dynarmic PRIVATE MCL_IGNORE_ASSERTS=1)
|
||||
endif()
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
target_compile_definitions(dynarmic PRIVATE FMT_USE_WINDOWS_H=0)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ A32AddressSpace::A32AddressSpace(const A32::UserConfig& conf)
|
|||
|
||||
void A32AddressSpace::GenerateIR(IR::Block& ir_block, IR::LocationDescriptor descriptor) const {
|
||||
ir_block.Reset(descriptor);
|
||||
A32::Translate(ir_block, A32::LocationDescriptor{descriptor}, conf.callbacks, {conf.arch_version, conf.define_unpredictable_behaviour, conf.hook_hint_instructions});
|
||||
A32::Translate(ir_block, A32::LocationDescriptor{descriptor}, conf);
|
||||
Optimization::Optimize(ir_block, conf, {});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -366,8 +366,7 @@ A64AddressSpace::A64AddressSpace(const A64::UserConfig& conf)
|
|||
|
||||
void A64AddressSpace::GenerateIR(IR::Block& ir_block, IR::LocationDescriptor descriptor) const {
|
||||
ir_block.Reset(descriptor);
|
||||
const auto get_code = [this](u64 vaddr) { return conf.callbacks->MemoryReadCode(vaddr); };
|
||||
A64::Translate(ir_block, A64::LocationDescriptor{descriptor}, get_code, {conf.define_unpredictable_behaviour, conf.wall_clock_cntpct});
|
||||
A64::Translate(ir_block, A64::LocationDescriptor{descriptor}, conf);
|
||||
Optimization::Optimize(ir_block, conf, {});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ A32AddressSpace::A32AddressSpace(const A32::UserConfig& conf)
|
|||
}
|
||||
|
||||
void A32AddressSpace::GenerateIR(IR::Block& ir_block, IR::LocationDescriptor descriptor) const {
|
||||
A32::Translate(ir_block, A32::LocationDescriptor{descriptor}, conf.callbacks, {conf.arch_version, conf.define_unpredictable_behaviour, conf.hook_hint_instructions});
|
||||
A32::Translate(ir_block, A32::LocationDescriptor{descriptor}, conf);
|
||||
Optimization::Optimize(ir_block, conf, {.sha256 = true});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ A32AddressSpace::A32AddressSpace(const A32::UserConfig& conf)
|
|||
}
|
||||
|
||||
void A32AddressSpace::GenerateIR(IR::Block& ir_block, IR::LocationDescriptor descriptor) const {
|
||||
A32::Translate(ir_block, A32::LocationDescriptor{descriptor}, conf.callbacks, {conf.arch_version, conf.define_unpredictable_behaviour, conf.hook_hint_instructions});
|
||||
A32::Translate(ir_block, A32::LocationDescriptor{descriptor}, conf);
|
||||
Optimization::Optimize(ir_block, conf, {});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -197,15 +197,15 @@ void EmitA32Terminal(biscuit::Assembler& as, EmitContext& ctx, IR::Term::CheckHa
|
|||
|
||||
void EmitA32LeafTerminal(biscuit::Assembler& as, EmitContext& ctx, IR::Term::LeafTerminal terminal, IR::LocationDescriptor initial_location, bool is_single_step) {
|
||||
if (auto const x = std::get_if<IR::Term::ReturnToDispatch>(&terminal))
|
||||
return EmitA32LeafTerminal(as, ctx, *x, initial_location, is_single_step);
|
||||
return EmitA32Terminal(as, ctx, *x, initial_location, is_single_step);
|
||||
if (auto const x = std::get_if<IR::Term::LinkBlock>(&terminal))
|
||||
return EmitA32LeafTerminal(as, ctx, *x, initial_location, is_single_step);
|
||||
return EmitA32Terminal(as, ctx, *x, initial_location, is_single_step);
|
||||
if (auto const x = std::get_if<IR::Term::LinkBlockFast>(&terminal))
|
||||
return EmitA32LeafTerminal(as, ctx, *x, initial_location, is_single_step);
|
||||
return EmitA32Terminal(as, ctx, *x, initial_location, is_single_step);
|
||||
if (auto const x = std::get_if<IR::Term::PopRSBHint>(&terminal))
|
||||
return EmitA32LeafTerminal(as, ctx, *x, initial_location, is_single_step);
|
||||
return EmitA32Terminal(as, ctx, *x, initial_location, is_single_step);
|
||||
if (auto const x = std::get_if<IR::Term::FastDispatchHint>(&terminal))
|
||||
return EmitA32LeafTerminal(as, ctx, *x, initial_location, is_single_step);
|
||||
return EmitA32Terminal(as, ctx, *x, initial_location, is_single_step);
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ private:
|
|||
// LocationDescriptor ctor() does important ops (like tflags) do not skip
|
||||
auto const arch_descriptor = A32::LocationDescriptor{descriptor};
|
||||
ir_block.Reset(arch_descriptor);
|
||||
A32::Translate(ir_block, arch_descriptor, conf.callbacks, {conf.arch_version, conf.define_unpredictable_behaviour, conf.hook_hint_instructions});
|
||||
A32::Translate(ir_block, arch_descriptor, conf);
|
||||
Optimization::Optimize(ir_block, conf, polyfill_options);
|
||||
return emitter.Emit(ir_block);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,11 +254,10 @@ private:
|
|||
block_of_code.EnsureMemoryCommitted(MINIMUM_REMAINING_CODESIZE);
|
||||
|
||||
// JIT Compile
|
||||
const auto get_code = [this](u64 vaddr) { return conf.callbacks->MemoryReadCode(vaddr); };
|
||||
// LocationDescriptor ctor() does important ops (like tflags) do not skip
|
||||
auto const arch_descriptor = A64::LocationDescriptor{descriptor};
|
||||
ir_block.Reset(arch_descriptor);
|
||||
A64::Translate(ir_block, arch_descriptor, get_code, {conf.define_unpredictable_behaviour, conf.wall_clock_cntpct});
|
||||
A64::Translate(ir_block, arch_descriptor, conf);
|
||||
Optimization::Optimize(ir_block, conf, polyfill_options);
|
||||
return emitter.Emit(ir_block).entrypoint;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ void ProtectMemory(const void* base, size_t size, bool is_executable) {
|
|||
|
||||
static const HostFeature features = []() {
|
||||
HostFeature f{};
|
||||
#ifdef DYNARMIC_ENABLE_CPU_FEATURE_DETECTION
|
||||
using Cpu = Xbyak::util::Cpu;
|
||||
Xbyak::util::Cpu cpu_info{};
|
||||
if (cpu_info.has(Cpu::tSSSE3)) f |= HostFeature::SSSE3;
|
||||
|
|
@ -121,7 +120,6 @@ static const HostFeature features = []() {
|
|||
}
|
||||
}
|
||||
return f;
|
||||
#endif
|
||||
}();
|
||||
HostFeature GetHostFeatures() {
|
||||
return features;
|
||||
|
|
|
|||
|
|
@ -7,34 +7,27 @@
|
|||
*/
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "common/assert.h"
|
||||
|
||||
#include <cassert>
|
||||
#include "dynarmic/backend/x64/block_of_code.h"
|
||||
#include "dynarmic/backend/x64/constant_pool.h"
|
||||
|
||||
namespace Dynarmic::Backend::X64 {
|
||||
|
||||
ConstantPool::ConstantPool(BlockOfCode& code, size_t size)
|
||||
: insertion_point(0)
|
||||
{
|
||||
code.EnsureMemoryCommitted(align_size + size);
|
||||
code.int3();
|
||||
ConstantPool::ConstantPool(BlockOfCode& code, size_t size) noexcept {
|
||||
auto const align_size = 64; //always align to cacheline
|
||||
code.EnsureMemoryCommitted(2 * align_size + size);
|
||||
code.align(align_size);
|
||||
pool = std::span<ConstantT>(reinterpret_cast<ConstantT*>(code.AllocateFromCodeSpace(size)), size / align_size);
|
||||
pool = std::span<u128>(static_cast<u128*>(code.AllocateFromCodeSpace(size)), size / sizeof(u128));
|
||||
}
|
||||
|
||||
Xbyak::Address ConstantPool::GetConstant(BlockOfCode& code, const Xbyak::AddressFrame& frame, u64 lower, u64 upper) {
|
||||
const auto constant = ConstantT(lower, upper);
|
||||
auto it = constant_info.find(constant);
|
||||
if (it == constant_info.end()) {
|
||||
ASSERT(insertion_point < pool.size());
|
||||
ConstantT& target_constant = pool[insertion_point];
|
||||
target_constant = constant;
|
||||
it = constant_info.insert({constant, &target_constant}).first;
|
||||
++insertion_point;
|
||||
}
|
||||
return frame[code.rip + it->second];
|
||||
Xbyak::Address ConstantPool::GetConstant(BlockOfCode& code, const Xbyak::AddressFrame& frame, u64 lo, u64 hi) noexcept {
|
||||
// offset grows iff new constant is added
|
||||
auto const key = std::make_pair(lo, hi);
|
||||
auto const val = u32(constant_info.size());
|
||||
auto const [it, _] = constant_info.insert({key, val});
|
||||
// if it != constant_info.end(): assume(pool[it->second] == c);
|
||||
pool[it->second] = u128{lo, hi};
|
||||
return frame[code.rip + std::addressof(pool[it->second])];
|
||||
}
|
||||
|
||||
} // namespace Dynarmic::Backend::X64
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include <cstddef>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
#include <boost/functional/hash.hpp>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "common/container/unordered_map.h"
|
||||
|
|
@ -21,31 +22,17 @@ namespace Dynarmic::Backend::X64 {
|
|||
|
||||
class BlockOfCode;
|
||||
|
||||
/// ConstantPool allocates a block of memory from BlockOfCode.
|
||||
/// @brief ConstantPool allocates a block of memory from BlockOfCode.
|
||||
/// It places constants into this block of memory, returning the address
|
||||
/// of the memory location where the constant is placed. If the constant
|
||||
/// already exists, its memory location is reused.
|
||||
class ConstantPool final {
|
||||
public:
|
||||
ConstantPool(BlockOfCode& code, size_t size);
|
||||
|
||||
Xbyak::Address GetConstant(BlockOfCode& code, const Xbyak::AddressFrame& frame, u64 lower, u64 upper = 0);
|
||||
|
||||
private:
|
||||
static constexpr size_t align_size = 16; // bytes
|
||||
|
||||
using ConstantT = std::pair<u64, u64>;
|
||||
static_assert(sizeof(ConstantT) == align_size);
|
||||
|
||||
struct ConstantHash {
|
||||
std::size_t operator()(const ConstantT& constant) const noexcept {
|
||||
return constant.first ^ std::rotl<u64>(constant.second, 1);
|
||||
}
|
||||
};
|
||||
|
||||
::Common::unordered_map<ConstantT, void*, ConstantHash> constant_info;
|
||||
std::span<ConstantT> pool;
|
||||
std::size_t insertion_point;
|
||||
ConstantPool(BlockOfCode& code, size_t size) noexcept;
|
||||
Xbyak::Address GetConstant(BlockOfCode& code, const Xbyak::AddressFrame& frame, u64 lower, u64 upper = 0) noexcept;
|
||||
// key = identity hash, value = offset from pool.data()
|
||||
::Common::unordered_map<std::pair<u64, u64>, u32, boost::hash<std::pair<u64, u64>>> constant_info;
|
||||
std::span<u128> pool;
|
||||
};
|
||||
|
||||
} // namespace Dynarmic::Backend::X64
|
||||
|
|
|
|||
|
|
@ -275,6 +275,7 @@ const void* EmitReadMemoryMov(BlockOfCode& code, int value_idx, const Xbyak::Reg
|
|||
code.xadd(qword[addr], Xbyak::Reg64(value_idx));
|
||||
break;
|
||||
case 128:
|
||||
ASSERT(Xbyak::Xmm(value_idx) != xmm0);
|
||||
code.lock();
|
||||
code.cmpxchg16b(xword[addr]);
|
||||
if (code.HasHostFeature(HostFeature::SSE41)) {
|
||||
|
|
|
|||
|
|
@ -6226,9 +6226,7 @@ void EmitX64::EmitVectorZeroExtend64(EmitContext& ctx, IR::Inst* inst) {
|
|||
void EmitX64::EmitVectorZeroUpper(EmitContext& ctx, IR::Inst* inst) {
|
||||
auto args = ctx.reg_alloc.GetArgumentInfo(inst);
|
||||
auto const a = ctx.reg_alloc.UseScratchXmm(code, args[0]);
|
||||
|
||||
code.movq(a, a); // TODO: !IsLastUse
|
||||
|
||||
ctx.reg_alloc.DefineValue(code, inst, a);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,12 +14,6 @@
|
|||
#include "dynarmic/backend/x64/hostloc.h"
|
||||
#include "dynarmic/common/spin_lock.h"
|
||||
|
||||
#ifdef DYNARMIC_ENABLE_NO_EXECUTE_SUPPORT
|
||||
static const auto default_cg_mode = Xbyak::DontSetProtectRWE;
|
||||
#else
|
||||
static const auto default_cg_mode = nullptr; //Allow RWE
|
||||
#endif
|
||||
|
||||
namespace Dynarmic {
|
||||
|
||||
void EmitSpinLockLock(Xbyak::CodeGenerator& code, Xbyak::Reg64 ptr, Xbyak::Reg32 tmp, bool waitpkg) {
|
||||
|
|
@ -78,7 +72,13 @@ namespace {
|
|||
struct SpinLockImpl {
|
||||
void Initialize() noexcept;
|
||||
static void GlobalInitialize() noexcept;
|
||||
Xbyak::CodeGenerator code = Xbyak::CodeGenerator(4096, default_cg_mode);
|
||||
Xbyak::CodeGenerator code = Xbyak::CodeGenerator(4096
|
||||
#ifdef DYNARMIC_ENABLE_NO_EXECUTE_SUPPORT
|
||||
, Xbyak::DontSetProtectRWE
|
||||
#else
|
||||
, nullptr //Allow RWE
|
||||
#endif
|
||||
);
|
||||
void (*lock)(volatile int*) = nullptr;
|
||||
void (*unlock)(volatile int*) = nullptr;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,15 +9,16 @@
|
|||
#include "dynarmic/frontend/A32/translate/a32_translate.h"
|
||||
|
||||
#include "dynarmic/frontend/A32/a32_location_descriptor.h"
|
||||
#include "dynarmic/interface/A32/config.h"
|
||||
#include "dynarmic/ir/basic_block.h"
|
||||
|
||||
namespace Dynarmic::A32 {
|
||||
|
||||
void TranslateArm(IR::Block& block, LocationDescriptor descriptor, TranslateCallbacks* tcb, const TranslationOptions& options);
|
||||
void TranslateThumb(IR::Block& block, LocationDescriptor descriptor, TranslateCallbacks* tcb, const TranslationOptions& options);
|
||||
void TranslateArm(IR::Block& block, LocationDescriptor descriptor, const A32::UserConfig& conf);
|
||||
void TranslateThumb(IR::Block& block, LocationDescriptor descriptor, const A32::UserConfig& conf);
|
||||
|
||||
void Translate(IR::Block& block, LocationDescriptor descriptor, TranslateCallbacks* tcb, const TranslationOptions& options) {
|
||||
return (descriptor.TFlag() ? TranslateThumb : TranslateArm)(block, descriptor, tcb, options);
|
||||
void Translate(IR::Block& block, LocationDescriptor descriptor, const A32::UserConfig& conf) {
|
||||
return (descriptor.TFlag() ? TranslateThumb : TranslateArm)(block, descriptor, conf);
|
||||
}
|
||||
|
||||
bool TranslateSingleArmInstruction(IR::Block& block, LocationDescriptor descriptor, u32 instruction);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "common/common_types.h"
|
||||
|
||||
#include "dynarmic/interface/A32/arch_version.h"
|
||||
#include "dynarmic/interface/A32/config.h"
|
||||
|
||||
namespace Dynarmic::IR {
|
||||
class Block;
|
||||
|
|
@ -20,28 +21,14 @@ namespace Dynarmic::A32 {
|
|||
class LocationDescriptor;
|
||||
struct TranslateCallbacks;
|
||||
|
||||
struct TranslationOptions {
|
||||
ArchVersion arch_version;
|
||||
|
||||
/// This changes what IR we emit when we translate an unpredictable instruction.
|
||||
/// If this is false, the ExceptionRaised IR instruction is emitted.
|
||||
/// If this is true, we define some behaviour for some instructions.
|
||||
bool define_unpredictable_behaviour = false;
|
||||
|
||||
/// This changes what IR we emit when we translate a hint instruction.
|
||||
/// If this is false, we treat the instruction as a NOP.
|
||||
/// If this is true, we emit an ExceptionRaised instruction.
|
||||
bool hook_hint_instructions = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function translates instructions in memory into our intermediate representation.
|
||||
* @param descriptor The starting location of the basic block. Includes information like PC, Thumb state, &c.
|
||||
* @param tcb The callbacks we should use to read emulated memory.
|
||||
* @param options Configures how certain instructions are translated.
|
||||
* @param conf Configures how certain instructions are translated.
|
||||
* @return A translated basic block in the intermediate representation.
|
||||
*/
|
||||
void Translate(IR::Block& block, LocationDescriptor descriptor, TranslateCallbacks* tcb, const TranslationOptions& options);
|
||||
void Translate(IR::Block& block, LocationDescriptor descriptor, const A32::UserConfig& conf);
|
||||
|
||||
/**
|
||||
* This function translates a single provided instruction into our intermediate representation.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
* Copyright (c) 2016 MerryMage
|
||||
* SPDX-License-Identifier: 0BSD
|
||||
|
|
@ -10,7 +13,7 @@ namespace Dynarmic::A32 {
|
|||
|
||||
// BKPT #<imm16>
|
||||
bool TranslatorVisitor::arm_BKPT(Cond cond, Imm<12> /*imm12*/, Imm<4> /*imm4*/) {
|
||||
if (cond != Cond::AL && !options.define_unpredictable_behaviour) {
|
||||
if (cond != Cond::AL && !conf.define_unpredictable_behaviour) {
|
||||
return UnpredictableInstruction();
|
||||
}
|
||||
// UNPREDICTABLE: The instruction executes conditionally.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "dynarmic/interface/A32/config.h"
|
||||
#include "dynarmic/mcl/bit.hpp"
|
||||
|
||||
#include "dynarmic/frontend/A32/a32_ir_emitter.h"
|
||||
|
|
@ -25,12 +26,14 @@ enum class Exception;
|
|||
struct TranslatorVisitor final {
|
||||
using instruction_return_type = bool;
|
||||
|
||||
explicit TranslatorVisitor(IR::Block& block, LocationDescriptor descriptor, const TranslationOptions& options)
|
||||
: ir(block, descriptor, options.arch_version), options(options) {}
|
||||
explicit TranslatorVisitor(IR::Block& block, LocationDescriptor descriptor, const A32::UserConfig& conf)
|
||||
: ir(block, descriptor, conf.arch_version)
|
||||
, conf(conf)
|
||||
{}
|
||||
|
||||
A32::IREmitter ir;
|
||||
ConditionalState cond_state = ConditionalState::None;
|
||||
TranslationOptions options;
|
||||
A32::UserConfig const& conf;
|
||||
|
||||
size_t current_instruction_size;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
* Copyright (c) 2019 MerryMage
|
||||
* SPDX-License-Identifier: 0BSD
|
||||
|
|
@ -9,7 +12,7 @@
|
|||
namespace Dynarmic::A32 {
|
||||
|
||||
bool TranslatorVisitor::arm_PLD_imm(bool /*add*/, bool R, Reg /*n*/, Imm<12> /*imm12*/) {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -18,7 +21,7 @@ bool TranslatorVisitor::arm_PLD_imm(bool /*add*/, bool R, Reg /*n*/, Imm<12> /*i
|
|||
}
|
||||
|
||||
bool TranslatorVisitor::arm_PLD_reg(bool /*add*/, bool R, Reg /*n*/, Imm<5> /*imm5*/, ShiftType /*shift*/, Reg /*m*/) {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +30,7 @@ bool TranslatorVisitor::arm_PLD_reg(bool /*add*/, bool R, Reg /*n*/, Imm<5> /*im
|
|||
}
|
||||
|
||||
bool TranslatorVisitor::arm_SEV() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -35,7 +38,7 @@ bool TranslatorVisitor::arm_SEV() {
|
|||
}
|
||||
|
||||
bool TranslatorVisitor::arm_SEVL() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +46,7 @@ bool TranslatorVisitor::arm_SEVL() {
|
|||
}
|
||||
|
||||
bool TranslatorVisitor::arm_WFE() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +54,7 @@ bool TranslatorVisitor::arm_WFE() {
|
|||
}
|
||||
|
||||
bool TranslatorVisitor::arm_WFI() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +62,7 @@ bool TranslatorVisitor::arm_WFI() {
|
|||
}
|
||||
|
||||
bool TranslatorVisitor::arm_YIELD() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -642,7 +642,7 @@ bool TranslatorVisitor::thumb16_SUB_sp(Imm<7> imm7) {
|
|||
|
||||
// SEV<c>
|
||||
bool TranslatorVisitor::thumb16_SEV() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
return RaiseException(Exception::SendEvent);
|
||||
|
|
@ -650,7 +650,7 @@ bool TranslatorVisitor::thumb16_SEV() {
|
|||
|
||||
// SEVL<c>
|
||||
bool TranslatorVisitor::thumb16_SEVL() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
return RaiseException(Exception::SendEventLocal);
|
||||
|
|
@ -658,7 +658,7 @@ bool TranslatorVisitor::thumb16_SEVL() {
|
|||
|
||||
// WFE<c>
|
||||
bool TranslatorVisitor::thumb16_WFE() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
return RaiseException(Exception::WaitForEvent);
|
||||
|
|
@ -666,7 +666,7 @@ bool TranslatorVisitor::thumb16_WFE() {
|
|||
|
||||
// WFI<c>
|
||||
bool TranslatorVisitor::thumb16_WFI() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
return RaiseException(Exception::WaitForInterrupt);
|
||||
|
|
@ -674,7 +674,7 @@ bool TranslatorVisitor::thumb16_WFI() {
|
|||
|
||||
// YIELD<c>
|
||||
bool TranslatorVisitor::thumb16_YIELD() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
return RaiseException(Exception::Yield);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
* Copyright (c) 2021 MerryMage
|
||||
* SPDX-License-Identifier: 0BSD
|
||||
|
|
@ -8,7 +11,7 @@
|
|||
|
||||
namespace Dynarmic::A32 {
|
||||
static bool PLDHandler(TranslatorVisitor& v, bool W) {
|
||||
if (!v.options.hook_hint_instructions) {
|
||||
if (!v.conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -18,7 +21,7 @@ static bool PLDHandler(TranslatorVisitor& v, bool W) {
|
|||
}
|
||||
|
||||
static bool PLIHandler(TranslatorVisitor& v) {
|
||||
if (!v.options.hook_hint_instructions) {
|
||||
if (!v.conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,10 +22,12 @@
|
|||
|
||||
namespace Dynarmic::A32 {
|
||||
|
||||
void TranslateArm(IR::Block& block, LocationDescriptor descriptor, TranslateCallbacks* tcb, const TranslationOptions& options) {
|
||||
void TranslateArm(IR::Block& block, LocationDescriptor descriptor, const A32::UserConfig& conf) {
|
||||
const bool single_step = descriptor.SingleStepping();
|
||||
TranslatorVisitor visitor{block, descriptor, options};
|
||||
TranslatorVisitor visitor{block, descriptor, conf};
|
||||
bool should_continue = true;
|
||||
|
||||
auto tcb = conf.callbacks;
|
||||
do {
|
||||
const u32 arm_pc = visitor.ir.current_location.PC();
|
||||
u64 ticks_for_instruction = 1;
|
||||
|
|
|
|||
|
|
@ -103,10 +103,12 @@ inline bool MaybeVFPOrASIMDInstruction(u32 thumb_instruction) noexcept {
|
|||
|
||||
} // namespace
|
||||
|
||||
void TranslateThumb(IR::Block& block, LocationDescriptor descriptor, TranslateCallbacks* tcb, const TranslationOptions& options) {
|
||||
void TranslateThumb(IR::Block& block, LocationDescriptor descriptor, const A32::UserConfig& conf) {
|
||||
const bool single_step = descriptor.SingleStepping();
|
||||
TranslatorVisitor visitor{block, descriptor, options};
|
||||
TranslatorVisitor visitor{block, descriptor, conf};
|
||||
bool should_continue = true;
|
||||
|
||||
auto tcb = conf.callbacks;
|
||||
do {
|
||||
const u32 arm_pc = visitor.ir.current_location.PC();
|
||||
u64 ticks_for_instruction = 1;
|
||||
|
|
|
|||
|
|
@ -11,19 +11,20 @@
|
|||
#include "dynarmic/frontend/A64/a64_location_descriptor.h"
|
||||
#include "dynarmic/frontend/A64/decoder/a64.h"
|
||||
#include "dynarmic/frontend/A64/translate/impl/impl.h"
|
||||
#include "dynarmic/interface/A64/config.h"
|
||||
#include "dynarmic/ir/basic_block.h"
|
||||
#include "dynarmic/ir/terminal.h"
|
||||
|
||||
namespace Dynarmic::A64 {
|
||||
|
||||
void Translate(IR::Block& block, LocationDescriptor descriptor, MemoryReadCodeFuncType memory_read_code, TranslationOptions options) {
|
||||
void Translate(IR::Block& block, LocationDescriptor descriptor, A64::UserConfig const& conf) {
|
||||
const bool single_step = descriptor.SingleStepping();
|
||||
TranslatorVisitor visitor{block, descriptor, std::move(options)};
|
||||
TranslatorVisitor visitor{block, descriptor, conf};
|
||||
|
||||
bool should_continue = true;
|
||||
do {
|
||||
const u64 pc = visitor.ir.current_location->PC();
|
||||
if (const auto instruction = memory_read_code(pc)) {
|
||||
if (const auto instruction = conf.callbacks->MemoryReadCode(pc)) {
|
||||
if (auto decoder = Decode<TranslatorVisitor, bool>(visitor, *instruction)) {
|
||||
should_continue = *decoder;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include <optional>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "dynarmic/interface/A64/config.h"
|
||||
|
||||
namespace Dynarmic {
|
||||
|
||||
|
|
@ -22,32 +23,14 @@ namespace A64 {
|
|||
|
||||
class LocationDescriptor;
|
||||
|
||||
using MemoryReadCodeFuncType = std::function<std::optional<u32>(u64 vaddr)>;
|
||||
|
||||
struct TranslationOptions {
|
||||
/// This changes what IR we emit when we translate an unpredictable instruction.
|
||||
/// If this is false, the ExceptionRaised IR instruction is emitted.
|
||||
/// If this is true, we define some behaviour for some instructions.
|
||||
bool define_unpredictable_behaviour = false;
|
||||
|
||||
/// This tells the translator a wall clock will be used, thus allowing it
|
||||
/// to avoid writting certain unnecessary code only needed for cycle timers.
|
||||
bool wall_clock_cntpct = false;
|
||||
|
||||
/// This changes what IR we emit when we translate a hint instruction.
|
||||
/// If this is false, we treat the instruction as a NOP.
|
||||
/// If this is true, we emit an ExceptionRaised instruction.
|
||||
bool hook_hint_instructions = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function translates instructions in memory into our intermediate representation.
|
||||
* @param descriptor The starting location of the basic block. Includes information like PC, FPCR state, &c.
|
||||
* @param memory_read_code The function we should use to read emulated memory.
|
||||
* @param options Configures how certain instructions are translated.
|
||||
* @param conf Configures how certain instructions are translated.
|
||||
* @return A translated basic block in the intermediate representation.
|
||||
*/
|
||||
void Translate(IR::Block& block, LocationDescriptor descriptor, MemoryReadCodeFuncType memory_read_code, TranslationOptions options);
|
||||
void Translate(IR::Block& block, LocationDescriptor descriptor, A64::UserConfig const& conf);
|
||||
|
||||
/**
|
||||
* This function translates a single provided instruction into our intermediate representation.
|
||||
|
|
|
|||
|
|
@ -15,17 +15,20 @@
|
|||
#include "dynarmic/frontend/A64/a64_types.h"
|
||||
#include "dynarmic/frontend/A64/translate/a64_translate.h"
|
||||
#include "dynarmic/frontend/imm.h"
|
||||
#include "dynarmic/interface/A64/config.h"
|
||||
|
||||
namespace Dynarmic::A64 {
|
||||
|
||||
struct TranslatorVisitor final {
|
||||
using instruction_return_type = bool;
|
||||
|
||||
explicit TranslatorVisitor(IR::Block& block, LocationDescriptor descriptor, TranslationOptions options)
|
||||
: ir(block, descriptor), options(std::move(options)) {}
|
||||
explicit TranslatorVisitor(IR::Block& block, LocationDescriptor descriptor, A64::UserConfig const& conf)
|
||||
: ir(block, descriptor)
|
||||
, conf(conf)
|
||||
{}
|
||||
|
||||
A64::IREmitter ir;
|
||||
TranslationOptions options;
|
||||
A64::UserConfig const& conf;
|
||||
|
||||
bool UnpredictableInstruction();
|
||||
bool DecodeError();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
* Copyright (c) 2018 MerryMage
|
||||
* SPDX-License-Identifier: 0BSD
|
||||
|
|
@ -25,7 +28,7 @@ static bool ExclusiveSharedDecodeAndOperation(TranslatorVisitor& v, bool pair, s
|
|||
if (memop == IR::MemOp::LOAD && pair && Rt == *Rt2) {
|
||||
return v.UnpredictableInstruction();
|
||||
} else if (memop == IR::MemOp::STORE && (*Rs == Rt || (pair && *Rs == *Rt2))) {
|
||||
if (!v.options.define_unpredictable_behaviour) {
|
||||
if (!v.conf.define_unpredictable_behaviour) {
|
||||
return v.UnpredictableInstruction();
|
||||
}
|
||||
// UNPREDICTABLE: The Constraint_NONE case is executed.
|
||||
|
|
|
|||
|
|
@ -42,35 +42,35 @@ bool TranslatorVisitor::NOP() {
|
|||
}
|
||||
|
||||
bool TranslatorVisitor::YIELD() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
return RaiseException(Exception::Yield);
|
||||
}
|
||||
|
||||
bool TranslatorVisitor::WFE() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
return RaiseException(Exception::WaitForEvent);
|
||||
}
|
||||
|
||||
bool TranslatorVisitor::WFI() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
return RaiseException(Exception::WaitForInterrupt);
|
||||
}
|
||||
|
||||
bool TranslatorVisitor::SEV() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
return RaiseException(Exception::SendEvent);
|
||||
}
|
||||
|
||||
bool TranslatorVisitor::SEVL() {
|
||||
if (!options.hook_hint_instructions) {
|
||||
if (!conf.hook_hint_instructions) {
|
||||
return true;
|
||||
}
|
||||
return RaiseException(Exception::SendEventLocal);
|
||||
|
|
@ -129,7 +129,7 @@ bool TranslatorVisitor::MRS(Imm<1> o0, Imm<3> op1, Imm<4> CRn, Imm<4> CRm, Imm<3
|
|||
return true;
|
||||
case SystemRegisterEncoding::CNTPCT_EL0:
|
||||
// HACK: Ensure that this is the first instruction in the block it's emitted in, so the cycle count is most up-to-date.
|
||||
if (!ir.block.instructions.empty() && !options.wall_clock_cntpct) {
|
||||
if (!ir.block.instructions.empty() && !conf.wall_clock_cntpct) {
|
||||
ir.block.CycleCount()--;
|
||||
ir.SetTerm(IR::Term::LinkBlock{*ir.current_location});
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ struct detail {
|
|||
shifts[arg_index] = bit_position;
|
||||
}
|
||||
}
|
||||
#if !defined(DYNARMIC_IGNORE_ASSERTS) && !defined(__ANDROID__)
|
||||
#if !defined(__ANDROID__)
|
||||
// Avoids a MSVC ICE, and avoids Android NDK issue.
|
||||
DEBUG_ASSERT(std::all_of(masks.begin(), masks.end(), [](auto m) { return m != 0; }));
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ static void RunInstance(size_t run_number, ThumbTestEnv& test_env, A32::UserConf
|
|||
while (num_insts < instructions_to_execute_count) {
|
||||
A32::LocationDescriptor descriptor = {u32(num_insts * 4), cpsr, A32::FPSCR{}};
|
||||
ir_block.Reset(descriptor);
|
||||
A32::Translate(ir_block, descriptor, &test_env, {});
|
||||
A32::Translate(ir_block, descriptor, config);
|
||||
Optimization::Optimize(ir_block, config, {});
|
||||
printf("\n\nIR:\n%s", IR::DumpBlock(ir_block).c_str());
|
||||
printf("\n\nx86_64:\n");
|
||||
|
|
|
|||
|
|
@ -2632,3 +2632,66 @@ TEST_CASE("A64: Rounding", "[a64]") {
|
|||
CHECK(jit.GetVector(5) == Vector{0x4000000040000000, 0x4000000040000000});
|
||||
CHECK(jit.GetVector(6) == Vector{0x4000000040000000, 0x4000000040000000});
|
||||
}
|
||||
|
||||
TEST_CASE("A64: SM4", "[a64]") {
|
||||
A64TestEnv env;
|
||||
A64::UserConfig jit_user_config{};
|
||||
jit_user_config.callbacks = &env;
|
||||
A64::Jit jit{jit_user_config};
|
||||
|
||||
oaknut::VectorCodeGenerator code{env.code_mem, nullptr};
|
||||
|
||||
code.SM4EKEY(V4.S4(), V0.S4(), V1.S4());
|
||||
code.SM4E(V5.S4(), V0.S4(), V1.S4());
|
||||
|
||||
jit.SetPC(0);
|
||||
jit.SetVector(0, {0xefcdab8967452301, 0x1032547698badcfe});
|
||||
jit.SetVector(1, {0xefcdab8967452301, 0x1032547698badcfe});
|
||||
env.ticks_left = env.code_mem.size();
|
||||
CheckedRun([&]() { jit.Run(); });
|
||||
|
||||
CHECK(jit.GetVector(4) == Vector{0x486a0c2e00224466, 0x5c7e183a62402604});
|
||||
CHECK(jit.GetVector(5) == Vector{0xc2e086a43c1e785a, 0xd7f593b104264062});
|
||||
}
|
||||
|
||||
TEST_CASE("A64: SM3", "[a64]") {
|
||||
A64TestEnv env;
|
||||
A64::UserConfig jit_user_config{};
|
||||
jit_user_config.callbacks = &env;
|
||||
SECTION("SM3SS1") {
|
||||
A64::Jit jit{jit_user_config};
|
||||
oaknut::VectorCodeGenerator code{env.code_mem, nullptr};
|
||||
code.SM3SS1(V0.S4(), V0.S4(), V1.S4(), V2.S4());
|
||||
jit.SetPC(0);
|
||||
jit.SetVector(0, {0xec4aff517369c667, 0x46e3fbf2abbacd29});
|
||||
jit.SetVector(1, {0x8de7e81bf854c27c, 0x9ac99f33632e5a76});
|
||||
jit.SetVector(2, {0x5aa35831b70d3266, 0xd45ee95817055d25});
|
||||
env.ticks_left = env.code_mem.size();
|
||||
CheckedRun([&]() { jit.Run(); });
|
||||
CHECK(jit.GetVector(0) == Vector{0x0000000000000000, 0x73d67cd700000000});
|
||||
}
|
||||
SECTION("SM3PARTW1") {
|
||||
A64::Jit jit{jit_user_config};
|
||||
oaknut::VectorCodeGenerator code{env.code_mem, nullptr};
|
||||
code.SM3PARTW1(V0.S4(), V0.S4(), V1.S4(), V2.S4());
|
||||
jit.SetPC(0);
|
||||
jit.SetVector(0, {0xec4aff517369c667, 0x46e3fbf2abbacd29});
|
||||
jit.SetVector(1, {0x8de7e81bf854c27c, 0x9ac99f33632e5a76});
|
||||
jit.SetVector(2, {0x5aa35831b70d3266, 0xd45ee95817055d25});
|
||||
env.ticks_left = env.code_mem.size();
|
||||
CheckedRun([&]() { jit.Run(); });
|
||||
CHECK(jit.GetVector(0) == Vector{0x653c64995693280c, 0xabcc7db97adebf12});
|
||||
}
|
||||
SECTION("SM3PARTW2") {
|
||||
A64::Jit jit{jit_user_config};
|
||||
oaknut::VectorCodeGenerator code{env.code_mem, nullptr};
|
||||
code.SM3PARTW2(V0.S4(), V0.S4(), V1.S4(), V2.S4());
|
||||
jit.SetPC(0);
|
||||
jit.SetVector(0, {0xec4aff517369c667, 0x46e3fbf2abbacd29});
|
||||
jit.SetVector(1, {0x8de7e81bf854c27c, 0x9ac99f33632e5a76});
|
||||
jit.SetVector(2, {0x5aa35831b70d3266, 0xd45ee95817055d25});
|
||||
env.ticks_left = env.code_mem.size();
|
||||
CheckedRun([&]() { jit.Run(); });
|
||||
CHECK(jit.GetVector(0) == Vector{0x30010fe70da43740, 0x6702425b4a3a05d4});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue