diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/LogFilterSetting.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/LogFilterSetting.kt new file mode 100644 index 0000000000..e3914cf2b6 --- /dev/null +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/LogFilterSetting.kt @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 Citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.citron.citron_emu.features.settings.model.view + +import androidx.annotation.StringRes +import org.citron.citron_emu.features.settings.model.AbstractStringSetting + +class LogFilterSetting( + private val stringSetting: AbstractStringSetting, + @StringRes titleId: Int = 0, + titleString: String = "", + @StringRes descriptionId: Int = 0, + descriptionString: String = "", + private val customChoice: String +) : SettingsItem(stringSetting, titleId, titleString, descriptionId, descriptionString) { + override val type = TYPE_LOG_FILTER + + private val presetValues = arrayOf( + "*:Warning", + "*:Info", + "*:Debug", + "*:Trace", + "*:Warning Service.Audio:Debug HW.GPU:Debug" + ) + + val customIndex: Int + get() = presetValues.size + + val choices: Array + get() = presetValues + customChoice + + val selectedValueIndex: Int + get() { + val index = presetValues.indexOf(getSelectedValue()) + return if (index >= 0) index else customIndex + } + + fun getValueAt(index: Int): String = + if (index >= 0 && index < presetValues.size) presetValues[index] else "" + + fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal) + + fun getDisplayValue(): String { + val value = getSelectedValue() + return value.ifEmpty { customChoice } + } + + fun setSelectedValue(value: String) = stringSetting.setString(value) +} diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt index f60649b529..25c2e323a0 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt @@ -94,6 +94,7 @@ abstract class SettingsItem( const val TYPE_INT_SINGLE_CHOICE = 9 const val TYPE_INPUT_PROFILE = 10 const val TYPE_STRING_INPUT = 11 + const val TYPE_LOG_FILTER = 12 const val FASTMEM_COMBINED = "fastmem_combined" diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsAdapter.kt index 0f060c4784..31a7c42417 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsAdapter.kt @@ -53,7 +53,9 @@ class SettingsAdapter( SwitchSettingViewHolder(ListItemSettingSwitchBinding.inflate(inflater), this) } - SettingsItem.TYPE_SINGLE_CHOICE, SettingsItem.TYPE_STRING_SINGLE_CHOICE -> { + SettingsItem.TYPE_SINGLE_CHOICE, + SettingsItem.TYPE_STRING_SINGLE_CHOICE, + SettingsItem.TYPE_LOG_FILTER -> { SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this) } @@ -129,6 +131,15 @@ class SettingsAdapter( ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) } + fun onLogFilterClick(item: LogFilterSetting, position: Int) { + SettingsDialogFragment.newInstance( + settingsViewModel, + item, + SettingsItem.TYPE_LOG_FILTER, + position + ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) + } + fun onIntSingleChoiceClick(item: IntSingleChoiceSetting, position: Int) { SettingsDialogFragment.newInstance( settingsViewModel, diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsDialogFragment.kt index d10856e302..f92765d544 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsDialogFragment.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsDialogFragment.kt @@ -21,6 +21,7 @@ import org.citron.citron_emu.features.input.model.AnalogDirection import org.citron.citron_emu.features.settings.model.view.AnalogInputSetting import org.citron.citron_emu.features.settings.model.view.ButtonInputSetting import org.citron.citron_emu.features.settings.model.view.IntSingleChoiceSetting +import org.citron.citron_emu.features.settings.model.view.LogFilterSetting import org.citron.citron_emu.features.settings.model.view.SettingsItem import org.citron.citron_emu.features.settings.model.view.SingleChoiceSetting import org.citron.citron_emu.features.settings.model.view.SliderSetting @@ -154,6 +155,26 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener .create() } + SettingsItem.TYPE_LOG_FILTER -> { + val item = settingsViewModel.clickedItem as LogFilterSetting + MaterialAlertDialogBuilder(requireContext()) + .setTitle(item.title) + .setSingleChoiceItems(item.choices, item.selectedValueIndex, this) + .create() + } + + TYPE_LOG_FILTER_CUSTOM -> { + stringInputBinding = DialogEditTextBinding.inflate(layoutInflater) + val item = settingsViewModel.clickedItem as LogFilterSetting + stringInputBinding.editText.setText(item.getSelectedValue()) + MaterialAlertDialogBuilder(requireContext()) + .setTitle(item.title) + .setView(stringInputBinding.root) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, defaultCancelListener) + .create() + } + SettingsItem.TYPE_INT_SINGLE_CHOICE -> { val item = settingsViewModel.clickedItem as IntSingleChoiceSetting MaterialAlertDialogBuilder(requireContext()) @@ -174,6 +195,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener return when (type) { SettingsItem.TYPE_SLIDER -> sliderBinding.root SettingsItem.TYPE_STRING_INPUT -> stringInputBinding.root + TYPE_LOG_FILTER_CUSTOM -> stringInputBinding.root else -> super.onCreateView(inflater, container, savedInstanceState) } } @@ -206,6 +228,26 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener scSetting.setSelectedValue(value) } + is LogFilterSetting -> { + val logFilterSetting = settingsViewModel.clickedItem as LogFilterSetting + if (type == TYPE_LOG_FILTER_CUSTOM) { + logFilterSetting.setSelectedValue( + (stringInputBinding.editText.text ?: "").toString() + ) + } else if (which == logFilterSetting.customIndex) { + dismiss() + newInstance( + settingsViewModel, + logFilterSetting, + TYPE_LOG_FILTER_CUSTOM, + position + ).show(parentFragmentManager, TAG) + return + } else { + logFilterSetting.setSelectedValue(logFilterSetting.getValueAt(which)) + } + } + is IntSingleChoiceSetting -> { val scSetting = settingsViewModel.clickedItem as IntSingleChoiceSetting val value = scSetting.getValueAt(which) @@ -265,6 +307,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener const val TAG = "SettingsDialogFragment" const val TYPE_RESET_SETTING = -1 + const val TYPE_LOG_FILTER_CUSTOM = -2 const val TITLE = "Title" const val TYPE = "Type" diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt index 90961cd696..b0bea286ed 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -1003,10 +1003,11 @@ class SettingsFragmentPresenter( add(HeaderSetting(R.string.logging)) add( - StringInputSetting( + LogFilterSetting( StringSetting.LOG_FILTER, titleId = R.string.log_filter, - descriptionId = R.string.log_filter_description + descriptionId = R.string.log_filter_description, + customChoice = context.getString(R.string.log_filter_custom) ) ) } diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt index 8a59b4d293..ffa7a5c474 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt @@ -6,6 +6,7 @@ package org.citron.citron_emu.features.settings.ui.viewholder import android.view.View import org.citron.citron_emu.databinding.ListItemSettingBinding import org.citron.citron_emu.features.settings.model.view.IntSingleChoiceSetting +import org.citron.citron_emu.features.settings.model.view.LogFilterSetting import org.citron.citron_emu.features.settings.model.view.SettingsItem import org.citron.citron_emu.features.settings.model.view.SingleChoiceSetting import org.citron.citron_emu.features.settings.model.view.StringSingleChoiceSetting @@ -39,6 +40,10 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti binding.textSettingValue.text = item.getSelectedValue() } + is LogFilterSetting -> { + binding.textSettingValue.text = item.getDisplayValue() + } + is IntSingleChoiceSetting -> { binding.textSettingValue.text = item.getChoiceAt(item.getSelectedValue()) } @@ -73,6 +78,10 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti ) } + is LogFilterSetting -> { + adapter.onLogFilterClick(setting as LogFilterSetting, bindingAdapterPosition) + } + is IntSingleChoiceSetting -> { adapter.onIntSingleChoiceClick( setting as IntSingleChoiceSetting, diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp index a29d965bea..0bdd663fcc 100644 --- a/src/android/app/src/main/jni/native_config.cpp +++ b/src/android/app/src/main/jni/native_config.cpp @@ -208,12 +208,17 @@ jstring Java_org_citron_citron_1emu_utils_NativeConfig_getString(JNIEnv* env, jo void Java_org_citron_citron_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject obj, jstring jkey, jstring value) { + const auto key = Common::Android::GetJString(env, jkey); auto setting = getSetting(env, jkey); if (setting == nullptr) { return; } - setting->SetValue(Common::Android::GetJString(env, value)); + auto setting_value = Common::Android::GetJString(env, value); + if (key == Settings::values.log_filter.GetLabel()) { + setting_value = Common::Log::CanonicalizeFilterString(setting_value); + } + setting->SetValue(setting_value); } jboolean Java_org_citron_citron_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEnv* env, jobject obj, diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index a002894dab..4deeb4ff56 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -272,7 +272,8 @@ Fastmem Logging Log Filter - Configure which log messages to display. Format: <class>:<level>. Example: *:Info Service:Debug + Custom... + Configure which log messages to display. Format: <class>:<level>. Example: *:Warning Service:Debug Output engine diff --git a/src/common/logging.cpp b/src/common/logging.cpp index 8055294656..fd39c3dd48 100644 --- a/src/common/logging.cpp +++ b/src/common/logging.cpp @@ -4,6 +4,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include #include @@ -95,11 +96,23 @@ std::string FormatLogMessage(const Entry& entry) noexcept { } } // namespace namespace { +template +bool ComparePartialStringCaseInsensitive(It begin, It end, const char* other) noexcept { + for (; begin != end && *other != '\0'; ++begin, ++other) { + const auto lhs = static_cast(*begin); + const auto rhs = static_cast(*other); + if (std::tolower(lhs) != std::tolower(rhs)) { + return false; + } + } + return (begin == end) == (*other == '\0'); +} + template Level GetLevelByName(const It begin, const It end) noexcept { for (u32 i = 0; i < u32(Level::Count); ++i) if (auto const name = GetLevelName(Level(i)); - Common::ComparePartialString(begin, end, name)) + ComparePartialStringCaseInsensitive(begin, end, name)) return Level(i); return Level::Count; } @@ -107,7 +120,7 @@ template Class GetClassByName(const It begin, const It end) noexcept { for (u32 i = 0; i < u32(Class::Count); ++i) if (auto const name = GetLogClassName(Class(i)); - Common::ComparePartialString(begin, end, name)) + ComparePartialStringCaseInsensitive(begin, end, name)) return Class(i); return Class::Count; } @@ -115,13 +128,13 @@ template bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) noexcept { auto level_separator = std::find(begin, end, ':'); if (level_separator == end) { - LOG_ERROR(Log, "Invalid log filter. Must specify a log level after `:`: {}", - std::string(begin, end)); + LOG_WARNING(Log, "Invalid log filter. Must specify a log level after `:`: {}", + std::string(begin, end)); return false; } const Level level = GetLevelByName(level_separator + 1, end); if (level == Level::Count) { - LOG_ERROR(Log, "Unknown log level in filter: {}", std::string(begin, end)); + LOG_WARNING(Log, "Unknown log level in filter: {}", std::string(begin, end)); return false; } if (Common::ComparePartialString(begin, level_separator, "*")) { @@ -130,7 +143,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) noexcept { } const Class log_class = GetClassByName(begin, level_separator); if (log_class == Class::Count) { - LOG_ERROR(Log, "Unknown log class in filter: {}", std::string(begin, end)); + LOG_WARNING(Log, "Unknown log class in filter: {}", std::string(begin, end)); return false; } instance.SetClassLevel(log_class, level); @@ -159,6 +172,48 @@ void Filter::ParseFilterString(std::string_view filter_view) noexcept { } } +std::string CanonicalizeFilterString(std::string_view filter_view) { + std::string canonicalized; + auto it = filter_view.cbegin(); + while (it != filter_view.cend()) { + auto end = std::find(it, filter_view.cend(), ' '); + if (end != it) { + if (!canonicalized.empty()) { + canonicalized.push_back(' '); + } + + auto level_separator = std::find(it, end, ':'); + if (level_separator == end) { + canonicalized.append(it, end); + } else { + if (ComparePartialStringCaseInsensitive(it, level_separator, "*")) { + canonicalized.push_back('*'); + } else { + const Class log_class = GetClassByName(it, level_separator); + if (log_class == Class::Count) { + canonicalized.append(it, level_separator); + } else { + canonicalized.append(GetLogClassName(log_class)); + } + } + + canonicalized.push_back(':'); + + const Level level = GetLevelByName(level_separator + 1, end); + if (level == Level::Count) { + canonicalized.append(level_separator + 1, end); + } else { + canonicalized.append(GetLevelName(level)); + } + } + } + if (end != filter_view.cend()) // Skip over the whitespace + ++end; + it = end; + } + return canonicalized; +} + namespace { /// @brief Trims up to and including the last of ../, ..\, src/, src\ in a string /// do not be fooled this isn't generating new strings on .rodata :) diff --git a/src/common/logging.h b/src/common/logging.h index c579a9a250..fc81628dfc 100644 --- a/src/common/logging.h +++ b/src/common/logging.h @@ -5,8 +5,10 @@ #pragma once -#include #include +#include +#include +#include #include #include #include "common/swap.h" @@ -67,6 +69,7 @@ struct Filter { } std::array class_levels; }; +[[nodiscard]] std::string CanonicalizeFilterString(std::string_view filter_view); void Initialize() noexcept; void Start() noexcept; void Stop() noexcept; diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 95fbfc9383..96fc5afe95 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -199,6 +199,10 @@ bool IsNceEnabled() { return is_nce_enabled; } +void DisableNceForCurrentProcess() { + is_nce_enabled = false; +} + bool IsDockedMode() { return values.use_docked_mode.GetValue() == Settings::ConsoleMode::Docked; } diff --git a/src/common/settings.h b/src/common/settings.h index bb2d6f4c05..b3f29ed357 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -778,7 +778,7 @@ struct Values { Setting perform_vulkan_check{linkage, true, "perform_vulkan_check", Category::Debugging}; // Miscellaneous - Setting log_filter{linkage, "*:Info", "log_filter", Category::Debugging}; + Setting log_filter{linkage, "*:Warning", "log_filter", Category::Debugging}; Setting use_dev_keys{linkage, false, "use_dev_keys", Category::Miscellaneous}; // Network @@ -840,6 +840,7 @@ bool IsGPULevelNormal(); bool IsFastmemEnabled(); bool IsCpuUltraLowAccuracy(); void SetNceEnabled(bool is_64bit); +void DisableNceForCurrentProcess(); bool IsNceEnabled(); bool IsDockedMode(); diff --git a/src/core/arm/nce/arm_nce.cpp b/src/core/arm/nce/arm_nce.cpp index 9143928a9c..5997c52ed6 100644 --- a/src/core/arm/nce/arm_nce.cpp +++ b/src/core/arm/nce/arm_nce.cpp @@ -2,8 +2,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include +#include "common/assert.h" +#include "common/logging.h" #include "common/signal_chain.h" #include "core/arm/nce/arm_nce.h" #include "core/arm/nce/interpreter_visitor.h" @@ -24,6 +27,57 @@ namespace { struct sigaction g_orig_bus_action; struct sigaction g_orig_segv_action; +template +void InvokeOriginalActionWithMask(int sig, const struct sigaction& original, Callback&& callback) { + sigset_t callback_mask = original.sa_mask; + sigset_t previous_mask; + + if ((original.sa_flags & SA_NODEFER) == 0) { + sigaddset(&callback_mask, sig); + } + + sigprocmask(SIG_BLOCK, &callback_mask, &previous_mask); + + if ((original.sa_flags & SA_NODEFER) != 0) { + sigset_t nodefer_mask; + sigemptyset(&nodefer_mask); + sigaddset(&nodefer_mask, sig); + sigprocmask(SIG_UNBLOCK, &nodefer_mask, nullptr); + } + + callback(); + sigprocmask(SIG_SETMASK, &previous_mask, nullptr); +} + +void ForwardSignalToOriginalAction(int sig, siginfo_t* info, void* raw_context, + const struct sigaction& original) { + if (original.sa_handler == SIG_IGN) { + return; + } + + if (original.sa_handler == SIG_DFL) { + Common::SigAction(sig, &original, nullptr); + syscall(SYS_tkill, gettid(), sig); + return; + } + + if ((original.sa_flags & SA_SIGINFO) != 0 && original.sa_sigaction != nullptr) { + InvokeOriginalActionWithMask(sig, original, + [&] { original.sa_sigaction(sig, info, raw_context); }); + return; + } + + if ((original.sa_flags & SA_SIGINFO) == 0 && original.sa_handler != nullptr) { + InvokeOriginalActionWithMask(sig, original, [&] { original.sa_handler(sig); }); + return; + } + + struct sigaction default_action {}; + default_action.sa_handler = SIG_DFL; + Common::SigAction(sig, &default_action, nullptr); + syscall(SYS_tkill, gettid(), sig); +} + // Verify assembly offsets. using NativeExecutionParameters = Kernel::KThread::NativeExecutionParameters; static_assert(offsetof(NativeExecutionParameters, native_context) == TpidrEl0NativeContext); @@ -31,11 +85,43 @@ static_assert(offsetof(NativeExecutionParameters, lock) == TpidrEl0Lock); static_assert(offsetof(NativeExecutionParameters, magic) == TpidrEl0TlsMagic); fpsimd_context* GetFloatingPointState(mcontext_t& host_ctx) { - _aarch64_ctx* header = reinterpret_cast<_aarch64_ctx*>(&host_ctx.__reserved); - while (header->magic != FPSIMD_MAGIC) { - header = reinterpret_cast<_aarch64_ctx*>(reinterpret_cast(header) + header->size); + auto* const begin = reinterpret_cast(&host_ctx.__reserved); + auto* const end = begin + sizeof(host_ctx.__reserved); + + for (auto* ptr = begin; ptr + sizeof(_aarch64_ctx) <= end;) { + auto* header = reinterpret_cast<_aarch64_ctx*>(ptr); + + if (header->magic == FPSIMD_MAGIC) { + if (header->size >= sizeof(fpsimd_context) && + static_cast(end - ptr) >= sizeof(fpsimd_context)) { + return reinterpret_cast(header); + } + + LOG_CRITICAL(Core_ARM, + "Malformed NCE signal frame FPSIMD context: size={:#x}, " + "remaining={:#x}", + header->size, static_cast(end - ptr)); + return nullptr; + } + + if (header->magic == 0 && header->size == 0) { + break; + } + + if (header->size < sizeof(_aarch64_ctx) || + header->size > static_cast(end - ptr)) { + LOG_CRITICAL(Core_ARM, + "Malformed NCE signal frame context entry: magic={:#x}, size={:#x}, " + "remaining={:#x}", + header->magic, header->size, static_cast(end - ptr)); + return nullptr; + } + + ptr += header->size; } - return reinterpret_cast(header); + + LOG_CRITICAL(Core_ARM, "NCE signal frame is missing FPSIMD context"); + return nullptr; } using namespace Common::Literals; @@ -53,6 +139,9 @@ void* ArmNce::RestoreGuestContext(void* raw_context) { // Retrieve the host floating point state. auto* fpctx = GetFloatingPointState(host_ctx); + if (fpctx == nullptr) { + UNREACHABLE_MSG("NCE cannot restore guest context without FPSIMD signal state"); + } // Save host callee-saved registers. std::memcpy(guest_ctx->host_ctx.host_saved_vregs.data(), &fpctx->vregs[8], @@ -82,6 +171,9 @@ void ArmNce::SaveGuestContext(GuestContext* guest_ctx, void* raw_context) { // Retrieve the host floating point state. auto* fpctx = GetFloatingPointState(host_ctx); + if (fpctx == nullptr) { + UNREACHABLE_MSG("NCE cannot save guest context without FPSIMD signal state"); + } // Save all guest registers except tpidr_el0. std::memcpy(guest_ctx->cpu_registers.data(), host_ctx.regs, sizeof(host_ctx.regs)); @@ -116,13 +208,15 @@ bool ArmNce::HandleFailedGuestFault(GuestContext* guest_ctx, void* raw_info, voi const bool is_prefetch_abort = host_ctx.pc == reinterpret_cast(info->si_addr); // For data aborts, skip the instruction and return to guest code. - // This will allow games to continue in many scenarios where they would otherwise crash. + // This preserves the historical NCE behavior while branch relocation fallback is tested. if (!is_prefetch_abort) { host_ctx.pc += 4; return true; } // This is a prefetch abort. + LOG_CRITICAL(Core_ARM, "Unhandled NCE prefetch abort: pc={:#x}, fault_addr={:#x}", + host_ctx.pc, reinterpret_cast(info->si_addr)); guest_ctx->esr_el1.fetch_or(static_cast(HaltReason::PrefetchAbort)); // Forcibly mark the context as locked. We are still running. @@ -141,6 +235,9 @@ bool ArmNce::HandleFailedGuestFault(GuestContext* guest_ctx, void* raw_info, voi bool ArmNce::HandleGuestAlignmentFault(GuestContext* guest_ctx, void* raw_info, void* raw_context) { auto& host_ctx = static_cast(raw_context)->uc_mcontext; auto* fpctx = GetFloatingPointState(host_ctx); + if (fpctx == nullptr) { + UNREACHABLE_MSG("NCE cannot handle guest alignment fault without FPSIMD signal state"); + } auto& memory = guest_ctx->system->ApplicationMemory(); // Match and execute an instruction. @@ -171,11 +268,13 @@ bool ArmNce::HandleGuestAccessFault(GuestContext* guest_ctx, void* raw_info, voi } void ArmNce::HandleHostAlignmentFault(int sig, void* raw_info, void* raw_context) { - return g_orig_bus_action.sa_sigaction(sig, static_cast(raw_info), raw_context); + ForwardSignalToOriginalAction(sig, static_cast(raw_info), raw_context, + g_orig_bus_action); } void ArmNce::HandleHostAccessFault(int sig, void* raw_info, void* raw_context) { - return g_orig_segv_action.sa_sigaction(sig, static_cast(raw_info), raw_context); + ForwardSignalToOriginalAction(sig, static_cast(raw_info), raw_context, + g_orig_segv_action); } void ArmNce::LockThread(Kernel::KThread* thread) { @@ -302,7 +401,7 @@ void ArmNce::Initialize() { alignment_fault_action.sa_sigaction = reinterpret_cast(&ArmNce::GuestAlignmentFaultSignalHandler); alignment_fault_action.sa_mask = signal_mask; - Common::SigAction(GuestAlignmentFaultSignal, &alignment_fault_action, nullptr); + Common::SigAction(GuestAlignmentFaultSignal, &alignment_fault_action, &g_orig_bus_action); struct sigaction access_fault_action {}; access_fault_action.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESTART; diff --git a/src/core/arm/nce/patcher.cpp b/src/core/arm/nce/patcher.cpp index 8ddfaebf10..f50eb24d02 100644 --- a/src/core/arm/nce/patcher.cpp +++ b/src/core/arm/nce/patcher.cpp @@ -4,6 +4,7 @@ #include "common/arm64/native_clock.h" #include "common/bit_cast.h" #include "common/literals.h" +#include "common/logging.h" #include "core/arm/nce/arm_nce.h" #include "core/arm/nce/guest_context.h" #include "core/arm/nce/instructions.h" @@ -20,6 +21,9 @@ using namespace oaknut::util; using NativeExecutionParameters = Kernel::KThread::NativeExecutionParameters; constexpr size_t MaxRelativeBranch = 128_MiB; +constexpr ptrdiff_t MinRelativeBranchOffset = -static_cast(MaxRelativeBranch); +constexpr ptrdiff_t MaxRelativeBranchOffset = + static_cast(MaxRelativeBranch) - sizeof(u32); constexpr u32 ModuleCodeIndex = 0x24 / sizeof(u32); Patcher::Patcher() : c(m_patch_instructions) { @@ -38,6 +42,35 @@ Patcher::Patcher() : c(m_patch_instructions) { Patcher::~Patcher() = default; +bool Patcher::IsValidBranchOffset(ptrdiff_t offset) noexcept { + return offset >= MinRelativeBranchOffset && offset <= MaxRelativeBranchOffset && + offset % sizeof(u32) == 0; +} + +ptrdiff_t Patcher::CalculateBranchToPatchOffset(PatchMode patch_mode, size_t patch_size, + size_t remaining_program_size, + const Relocation& rel) noexcept { + if (patch_mode == PatchMode::PreText) { + return rel.patch_offset - static_cast(patch_size) - + static_cast(rel.module_offset); + } + + return static_cast(remaining_program_size) - + static_cast(rel.module_offset) + rel.patch_offset; +} + +ptrdiff_t Patcher::CalculateBranchToModuleOffset(PatchMode patch_mode, size_t patch_size, + size_t remaining_program_size, + const Relocation& rel) noexcept { + if (patch_mode == PatchMode::PreText) { + return static_cast(patch_size) - rel.patch_offset + + static_cast(rel.module_offset); + } + + return static_cast(rel.module_offset) - + static_cast(remaining_program_size) - rel.patch_offset; +} + bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image, const Kernel::CodeSet::Segment& code) { // If we have patched modules but cannot reach the new module, then it needs its own patcher. @@ -49,6 +82,7 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image, // Add a new module patch to our list modules.emplace_back(); curr_patch = &modules.back(); + curr_patch->image_size = image_size; // The first word of the patch section is always a branch to the first instruction of the // module. @@ -116,6 +150,10 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image, // Determine patching mode for the final relocation step total_program_size += image_size; this->mode = image_size > MaxRelativeBranch ? PatchMode::PreText : PatchMode::PostData; + if (mode == PatchMode::PreText) { + ASSERT(modules.size() == 1); + ASSERT(total_program_size == image_size); + } return true; } @@ -131,22 +169,43 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base, const auto text_words = std::span{reinterpret_cast(text.data()), text.size() / sizeof(u32)}; + const auto LogBranchRelocation = [&](const char* kind, ptrdiff_t branch_offset, + const Relocation& rel) { + if (IsValidBranchOffset(branch_offset)) { + return; + } + + LOG_CRITICAL(Core_ARM, + "NCE branch relocation is outside AArch64 B range: kind={}, mode={}, " + "relocate_module={}, modules={}, branch_offset={:#x}, valid_range=[{:#x}, " + "{:#x}], patch_offset={:#x}, module_offset={:#x}, patch_size={:#x}, " + "image_size={:#x}, total_program_size={:#x}, load_base={:#x}", + kind, mode, m_relocate_module_index, modules.size(), branch_offset, + MinRelativeBranchOffset, MaxRelativeBranchOffset, rel.patch_offset, + rel.module_offset, patch_size, image_size, total_program_size, + GetInteger(load_base)); + }; + const auto ApplyBranchToPatchRelocation = [&](u32* target, const Relocation& rel) { oaknut::CodeGenerator rc{target}; - if (mode == PatchMode::PreText) { - rc.B(rel.patch_offset - patch_size - rel.module_offset); - } else { - rc.B(total_program_size - rel.module_offset + rel.patch_offset); + const ptrdiff_t branch_offset = + CalculateBranchToPatchOffset(mode, patch_size, total_program_size, rel); + LogBranchRelocation("module_to_patch", branch_offset, rel); + if (!IsValidBranchOffset(branch_offset)) { + UNREACHABLE_MSG("NCE branch relocation must be validated before RelocateAndCopy"); } + rc.B(branch_offset); }; const auto ApplyBranchToModuleRelocation = [&](u32* target, const Relocation& rel) { oaknut::CodeGenerator rc{target}; - if (mode == PatchMode::PreText) { - rc.B(patch_size - rel.patch_offset + rel.module_offset); - } else { - rc.B(rel.module_offset - total_program_size - rel.patch_offset); + const ptrdiff_t branch_offset = + CalculateBranchToModuleOffset(mode, patch_size, total_program_size, rel); + LogBranchRelocation("patch_to_module", branch_offset, rel); + if (!IsValidBranchOffset(branch_offset)) { + UNREACHABLE_MSG("NCE branch relocation must be validated before RelocateAndCopy"); } + rc.B(branch_offset); }; const auto RebasePatch = [&](ptrdiff_t patch_offset) { @@ -166,7 +225,10 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base, }; // We are now ready to relocate! - auto& patch = modules[m_relocate_module_index++]; + ASSERT(m_relocate_module_index < modules.size()); + const size_t module_index = m_relocate_module_index++; + auto& patch = modules[module_index]; + const size_t module_image_size = patch.image_size; for (const Relocation& rel : patch.m_branch_to_patch_relocations) { ApplyBranchToPatchRelocation(text_words.data() + rel.module_offset / sizeof(u32), rel); } @@ -193,12 +255,24 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base, // Remove the patched module size from the total. This is done so total_program_size // always represents the distance from the currently patched module to the patch section. - total_program_size -= image_size; + total_program_size -= module_image_size; // Only copy to the program image of the last module if (m_relocate_module_index == modules.size()) { if (this->mode == PatchMode::PreText) { - ASSERT(image_size == total_program_size); + const size_t expected_image_size = module_image_size + patch_size; + if (image_size != expected_image_size || total_program_size != 0) { + LOG_CRITICAL(Core_ARM, + "NCE PreText final copy state mismatch: image_size={:#x}, " + "expected_image_size={:#x}, module_image_size={:#x}, " + "total_program_size={:#x}, patch_size={:#x}, relocate_module={}, " + "modules={}, load_base={:#x}", + image_size, expected_image_size, module_image_size, + total_program_size, patch_size, m_relocate_module_index, + modules.size(), GetInteger(load_base)); + } + ASSERT(image_size == expected_image_size); + ASSERT(total_program_size == 0); std::memcpy(program_image.data(), m_patch_instructions.data(), m_patch_instructions.size() * sizeof(u32)); } else { @@ -216,6 +290,80 @@ size_t Patcher::GetSectionSize() const noexcept { return Common::AlignUp(m_patch_instructions.size() * sizeof(u32), Core::Memory::CITRON_PAGESIZE); } +bool Patcher::CanRelocateBranches() const noexcept { + if (modules.empty() || mode == PatchMode::None) { + LOG_CRITICAL(Core_ARM, + "NCE branch relocation preflight called without patched modules: mode={}, " + "modules={}, total_program_size={:#x}", + mode, modules.size(), total_program_size); + return false; + } + + const size_t patch_size = GetSectionSize(); + + const auto LogInvalidBranch = [&](const char* kind, size_t module_index, + size_t remaining_program_size, + const Relocation& rel, ptrdiff_t branch_offset) { + LOG_CRITICAL(Core_ARM, + "NCE branch relocation preflight failed: kind={}, mode={}, " + "relocate_module={}, modules={}, branch_offset={:#x}, valid_range=[{:#x}, " + "{:#x}], patch_offset={:#x}, module_offset={:#x}, patch_size={:#x}, " + "image_size={:#x}, total_program_size={:#x}", + kind, mode, module_index, modules.size(), branch_offset, + MinRelativeBranchOffset, MaxRelativeBranchOffset, rel.patch_offset, + rel.module_offset, patch_size, modules[module_index].image_size, + remaining_program_size); + }; + + size_t remaining_program_size = total_program_size; + for (size_t module_index = 0; module_index < modules.size(); module_index++) { + const auto& patch = modules[module_index]; + if (patch.image_size > remaining_program_size) { + LOG_CRITICAL(Core_ARM, + "NCE branch relocation preflight size state mismatch: mode={}, " + "relocate_module={}, modules={}, image_size={:#x}, " + "remaining_program_size={:#x}, total_program_size={:#x}", + mode, module_index, modules.size(), patch.image_size, + remaining_program_size, total_program_size); + return false; + } + + for (const Relocation& rel : patch.m_branch_to_patch_relocations) { + const ptrdiff_t branch_offset = + CalculateBranchToPatchOffset(mode, patch_size, remaining_program_size, rel); + + if (!IsValidBranchOffset(branch_offset)) { + LogInvalidBranch("module_to_patch", module_index, remaining_program_size, rel, + branch_offset); + return false; + } + } + + for (const Relocation& rel : patch.m_branch_to_module_relocations) { + const ptrdiff_t branch_offset = + CalculateBranchToModuleOffset(mode, patch_size, remaining_program_size, rel); + + if (!IsValidBranchOffset(branch_offset)) { + LogInvalidBranch("patch_to_module", module_index, remaining_program_size, rel, + branch_offset); + return false; + } + } + + remaining_program_size -= patch.image_size; + } + + if (remaining_program_size != 0) { + LOG_CRITICAL(Core_ARM, + "NCE branch relocation preflight size state mismatch after scan: mode={}, " + "modules={}, remaining_program_size={:#x}, total_program_size={:#x}", + mode, modules.size(), remaining_program_size, total_program_size); + return false; + } + + return true; +} + void Patcher::WriteLoadContext() { // This function was called, which modifies X30, so use that as a scratch register. // SP contains the guest X30, so save our return X30 to SP + 8, since we have allocated 16 bytes diff --git a/src/core/arm/nce/patcher.h b/src/core/arm/nce/patcher.h index a44f385e2e..a47f81cc23 100644 --- a/src/core/arm/nce/patcher.h +++ b/src/core/arm/nce/patcher.h @@ -36,6 +36,7 @@ public: bool RelocateAndCopy(Common::ProcessAddress load_base, const Kernel::CodeSet::Segment& code, Kernel::PhysicalMemory& program_image, EntryTrampolines* out_trampolines); size_t GetSectionSize() const noexcept; + bool CanRelocateBranches() const noexcept; [[nodiscard]] PatchMode GetPatchMode() const noexcept { return mode; @@ -84,7 +85,18 @@ private: uintptr_t module_offset; ///< Offset in bytes from the start of the text section. }; + [[nodiscard]] static bool IsValidBranchOffset(ptrdiff_t offset) noexcept; + [[nodiscard]] static ptrdiff_t CalculateBranchToPatchOffset(PatchMode patch_mode, + size_t patch_size, + size_t remaining_program_size, + const Relocation& rel) noexcept; + [[nodiscard]] static ptrdiff_t CalculateBranchToModuleOffset(PatchMode patch_mode, + size_t patch_size, + size_t remaining_program_size, + const Relocation& rel) noexcept; + struct ModulePatch { + size_t image_size{}; std::vector m_trampolines; std::vector m_branch_to_patch_relocations{}; std::vector m_branch_to_module_relocations{}; diff --git a/src/core/core.cpp b/src/core/core.cpp index 685ea45ad8..cd396b1dac 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -1118,6 +1118,10 @@ void System::Exit() { } void System::ApplySettings() { + Common::Log::Filter filter; + filter.ParseFilterString(Settings::values.log_filter.GetValue()); + Common::Log::SetGlobalFilter(filter); + impl->RefreshTime(*this); if (IsPoweredOn()) { diff --git a/src/core/hle/kernel/physical_core.cpp b/src/core/hle/kernel/physical_core.cpp index 37b4541949..6a76228bab 100644 --- a/src/core/hle/kernel/physical_core.cpp +++ b/src/core/hle/kernel/physical_core.cpp @@ -129,18 +129,11 @@ void PhysicalCore::RunThread(Kernel::KThread* thread) { // Notify the debugger and go to sleep on data abort. if (data_abort) { if (system.DebuggerEnabled()) { - system.GetDebugger().NotifyThreadWatchpoint(thread, *interface->HaltedWatchpoint()); - } else { - // Enhanced data abort handling for Nintendo SDK crashes - LOG_WARNING(Core_ARM, "Data abort detected - checking if recoverable..."); - - // For Nintendo SDK crashes, try to continue execution - // Many data aborts in Nintendo SDK are recoverable - LOG_INFO(Core_ARM, "Attempting to continue execution after data abort"); - // Don't suspend the thread, let it continue - } - - if (system.DebuggerEnabled()) { + if (const auto* watchpoint = interface->HaltedWatchpoint(); watchpoint != nullptr) { + system.GetDebugger().NotifyThreadWatchpoint(thread, *watchpoint); + } else { + system.GetDebugger().NotifyThreadStopped(thread); + } thread->RequestSuspend(SuspendType::Debug); return; } diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp index d285b7327d..99c976c56d 100644 --- a/src/core/loader/deconstructed_rom_directory.cpp +++ b/src/core/loader/deconstructed_rom_directory.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include "common/logging.h" #include "common/settings.h" #include "core/core.h" @@ -29,7 +30,12 @@ namespace Loader { struct PatchCollection { explicit PatchCollection(bool is_application_) : is_application{is_application_} { + Reset(); + } + + void Reset() { module_patcher_indices.fill(-1); + patchers.clear(); patchers.emplace_back(); } @@ -41,6 +47,10 @@ struct PatchCollection { } size_t GetTotalPatchSize() const { + if (!is_application || !Settings::IsNceEnabled()) { + return 0; + } + size_t total_size{}; #ifdef HAS_NCE for (auto& patcher : patchers) { @@ -62,6 +72,21 @@ struct PatchCollection { return static_cast(patchers.size()) - 1; } + bool CanRelocateBranches() const { +#ifdef HAS_NCE + if (!is_application || !Settings::IsNceEnabled()) { + return true; + } + + for (const auto& patcher : patchers) { + if (!patcher.CanRelocateBranches()) { + return false; + } + } +#endif + return true; + } + bool is_application; std::vector patchers; std::array module_patcher_indices{}; @@ -175,6 +200,9 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect } metadata.Print(); + const FileSys::PatchManager pm{metadata.GetTitleID(), system.GetFileSystemController(), + system.GetContentProvider()}; + // Enable NCE only for applications with 39-bit address space. const bool is_39bit = metadata.GetAddressSpaceType() == FileSys::ProgramAddressSpaceType::Is39Bit; @@ -185,30 +213,57 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect "subsdk3", "subsdk4", "subsdk5", "subsdk6", "subsdk7", "subsdk8", "subsdk9", "sdk"}; - std::size_t code_size{}; - // Define an nce patch context for each potential module. PatchCollection patch_ctx{is_application}; - // Use the NSO module loader to figure out the code layout - for (size_t i = 0; i < static_modules.size(); i++) { - const auto& module = static_modules[i]; - const FileSys::VirtualFile module_file{dir->GetFile(module)}; - if (!module_file) { - continue; + const auto CalculateCodeLayout = [&]() -> std::optional { + std::size_t next_code_size{}; + patch_ctx.Reset(); + + // Use the NSO module loader to figure out the code layout. + for (size_t i = 0; i < static_modules.size(); i++) { + const auto& module = static_modules[i]; + const FileSys::VirtualFile module_file{dir->GetFile(module)}; + if (!module_file) { + continue; + } + + const bool should_pass_arguments = std::strcmp(module, "rtld") == 0; + const auto tentative_next_load_addr = AppLoader_NSO::LoadModule( + process, system, *module_file, next_code_size, should_pass_arguments, false, pm, + patch_ctx.GetPatchers(), patch_ctx.GetLastIndex()); + if (!tentative_next_load_addr) { + return std::nullopt; + } + + patch_ctx.SaveIndex(i); + next_code_size = *tentative_next_load_addr; } - const bool should_pass_arguments = std::strcmp(module, "rtld") == 0; - const auto tentative_next_load_addr = AppLoader_NSO::LoadModule( - process, system, *module_file, code_size, should_pass_arguments, false, {}, - patch_ctx.GetPatchers(), patch_ctx.GetLastIndex()); - if (!tentative_next_load_addr) { + return next_code_size; + }; + + auto calculated_code_size = CalculateCodeLayout(); + if (!calculated_code_size) { + return {ResultStatus::ErrorLoadingNSO, {}}; + } + +#ifdef HAS_NCE + if (!patch_ctx.CanRelocateBranches()) { + LOG_WARNING(Loader, + "NCE patch layout contains an out-of-range branch relocation for " + "program_id={:016X}; falling back to Dynarmic for this boot", + metadata.GetTitleID()); + Settings::DisableNceForCurrentProcess(); + + calculated_code_size = CalculateCodeLayout(); + if (!calculated_code_size) { return {ResultStatus::ErrorLoadingNSO, {}}; } - - patch_ctx.SaveIndex(i); - code_size = *tentative_next_load_addr; } +#endif + + std::size_t code_size = *calculated_code_size; // Enable direct memory mapping in case of NCE. const u64 fastmem_base = [&]() -> size_t { @@ -232,8 +287,6 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect modules.clear(); const VAddr base_address{GetInteger(process.GetEntryPoint())}; VAddr next_load_addr{base_address}; - const FileSys::PatchManager pm{metadata.GetTitleID(), system.GetFileSystemController(), - system.GetContentProvider()}; for (size_t i = 0; i < static_modules.size(); i++) { const auto& module = static_modules[i]; const FileSys::VirtualFile module_file{dir->GetFile(module)}; diff --git a/src/core/loader/nso.cpp b/src/core/loader/nso.cpp index fc82561ac1..3d52aff442 100644 --- a/src/core/loader/nso.cpp +++ b/src/core/loader/nso.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "common/common_funcs.h" @@ -148,7 +149,10 @@ std::optional AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core:: // Apply patches if necessary const auto name = nso_file.GetName(); - if (pm && (pm->HasNSOPatch(nso_header.build_id, name) || Settings::values.dump_nso)) { + const bool has_nso_patch = pm && pm->HasNSOPatch(nso_header.build_id, name); + const bool should_patch_nso = + has_nso_patch || (load_into_process && Settings::values.dump_nso); + if (pm && should_patch_nso) { std::span patchable_section(program_image.data() + module_start, program_image.size() - module_start); std::vector pi_header(sizeof(NSOHeader) + patchable_section.size()); @@ -158,6 +162,21 @@ std::optional AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core:: pi_header = pm->PatchNSO(pi_header, name); + if (pi_header.size() < sizeof(NSOHeader)) { + LOG_ERROR(Loader, "Patched NSO is too small: module={}, patched_size={:#x}", name, + pi_header.size()); + return std::nullopt; + } + + const auto patched_size = pi_header.size() - sizeof(NSOHeader); + if (patched_size != patchable_section.size()) { + LOG_ERROR(Loader, + "Patched NSO size mismatch: module={}, original_size={:#x}, " + "patched_size={:#x}", + name, patchable_section.size(), patched_size); + return std::nullopt; + } + std::copy(pi_header.begin() + sizeof(NSOHeader), pi_header.end(), patchable_section.data()); } @@ -172,7 +191,36 @@ std::optional AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core:: } } else if (patch) { // Relocate code patch and copy to the program_image. - if (patch->RelocateAndCopy(load_base, code, program_image, &process.GetPostHandlers())) { + const auto build_id = Common::HexToString(nso_header.build_id); + LOG_DEBUG(Loader, + "NCE relocating NSO module: name={}, build_id={}, patch_index={}, mode={}, " + "load_base={:#x}, image_size={:#x}, code_offset={:#x}, code_size={:#x}, " + "patch_section_size={:#x}", + name, build_id, patch_index, patch->GetPatchMode(), load_base, + program_image.size(), code.offset, code.size, patch->GetSectionSize()); + + bool copied_patch_section; + try { + copied_patch_section = + patch->RelocateAndCopy(load_base, code, program_image, &process.GetPostHandlers()); + } catch (const std::exception& ex) { + LOG_CRITICAL(Loader, + "NCE failed while relocating NSO module: name={}, build_id={}, " + "patch_index={}, mode={}, load_base={:#x}, image_size={:#x}, " + "code_offset={:#x}, code_size={:#x}, patch_section_size={:#x}, " + "exception={}", + name, build_id, patch_index, patch->GetPatchMode(), load_base, + program_image.size(), code.offset, code.size, patch->GetSectionSize(), + ex.what()); + throw; + } + + LOG_DEBUG(Loader, + "NCE relocated NSO module: name={}, build_id={}, patch_index={}, " + "copied_patch_section={}, final_image_size={:#x}", + name, build_id, patch_index, copied_patch_section, program_image.size()); + + if (copied_patch_section) { // Update patch section. auto& patch_segment = codeset.PatchSegment(); patch_segment.addr = diff --git a/src/core/memory.cpp b/src/core/memory.cpp index a9e3959bca..5809b5bd1a 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -1065,7 +1065,7 @@ bool Memory::InvalidateNCE(Common::ProcessAddress vaddr, size_t size) { u8* const ptr = impl->GetPointerImpl( GetInteger(vaddr), [&] { - LOG_ERROR(HW_Memory, "Unmapped InvalidateNCE for {} bytes @ {:#x}", size, + LOG_DEBUG(HW_Memory, "Unmapped InvalidateNCE for {} bytes @ {:#x}", size, GetInteger(vaddr)); mapped = false; }, diff --git a/src/frontend_common/config.cpp b/src/frontend_common/config.cpp index 5a042d0ac3..ebf57168f9 100644 --- a/src/frontend_common/config.cpp +++ b/src/frontend_common/config.cpp @@ -1032,20 +1032,27 @@ void Config::WriteSettingGeneric(const Settings::BasicSetting* const setting) { } std::string key = AdjustKey(setting->GetLabel()); + const bool is_log_filter = setting->GetLabel() == Settings::values.log_filter.GetLabel(); + const auto canonicalize = [is_log_filter](std::string value) { + if (is_log_filter) { + return Common::Log::CanonicalizeFilterString(value); + } + return value; + }; + const std::string default_value = canonicalize(setting->DefaultToString()); if (setting->Switchable()) { if (!global) { WriteBooleanSetting(std::string(key).append("\\use_global"), setting->UsingGlobal()); } if (global || !setting->UsingGlobal()) { - auto value = global ? setting->ToStringGlobal() : setting->ToString(); - WriteBooleanSetting(std::string(key).append("\\default"), - value == setting->DefaultToString()); + auto value = canonicalize(global ? setting->ToStringGlobal() : setting->ToString()); + WriteBooleanSetting(std::string(key).append("\\default"), value == default_value); WriteStringSetting(key, value); } } else if (global) { - WriteBooleanSetting(std::string(key).append("\\default"), - setting->ToString() == setting->DefaultToString()); - WriteStringSetting(key, setting->ToString()); + auto value = canonicalize(setting->ToString()); + WriteBooleanSetting(std::string(key).append("\\default"), value == default_value); + WriteStringSetting(key, value); } } diff --git a/src/shader_recompiler/frontend/maxwell/translate/impl/vote.cpp b/src/shader_recompiler/frontend/maxwell/translate/impl/vote.cpp index 43af79d557..1fd9999227 100644 --- a/src/shader_recompiler/frontend/maxwell/translate/impl/vote.cpp +++ b/src/shader_recompiler/frontend/maxwell/translate/impl/vote.cpp @@ -47,8 +47,8 @@ void TranslatorVisitor::VOTE(u64 insn) { Vote(*this, insn); } -void TranslatorVisitor::VOTE_vtg(u64) { - LOG_WARNING(Shader, "(STUBBED) called"); +void TranslatorVisitor::VOTE_vtg(u64 insn) { + LOG_DEBUG(Shader, "(STUBBED) called with insn={:#X}", insn); } } // namespace Shader::Maxwell diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 338b7710a9..7b55cd3c36 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -7,6 +7,7 @@ add_executable(tests common/container_hash.cpp common/fibers.cpp common/host_memory.cpp + common/logging.cpp common/param_package.cpp common/range_map.cpp common/ring_buffer.cpp diff --git a/src/tests/common/logging.cpp b/src/tests/common/logging.cpp new file mode 100644 index 0000000000..6b6fc78b2d --- /dev/null +++ b/src/tests/common/logging.cpp @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2026 Citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "common/logging.h" + +namespace Common::Log { +namespace { + +[[nodiscard]] bool Allows(const Filter& filter, Class log_class, Level level) { + return filter.CheckMessage(log_class, level); +} + +} // Anonymous namespace + +TEST_CASE("Log filter parser accepts canonical warning", "[common][logging]") { + Filter filter{Level::Info}; + + filter.ParseFilterString("*:Warning"); + + REQUIRE_FALSE(Allows(filter, Class::Service, Level::Info)); + REQUIRE(Allows(filter, Class::Service, Level::Warning)); +} + +TEST_CASE("Log filter parser accepts lowercase warning", "[common][logging]") { + Filter filter{Level::Info}; + + filter.ParseFilterString("*:warning"); + + REQUIRE_FALSE(Allows(filter, Class::Service, Level::Info)); + REQUIRE(Allows(filter, Class::Service, Level::Warning)); +} + +TEST_CASE("Log filter parser accepts uppercase warning", "[common][logging]") { + Filter filter{Level::Info}; + + filter.ParseFilterString("*:WARNING"); + + REQUIRE_FALSE(Allows(filter, Class::Service, Level::Info)); + REQUIRE(Allows(filter, Class::Service, Level::Warning)); +} + +TEST_CASE("Log filter parser accepts service debug", "[common][logging]") { + Filter filter{Level::Warning}; + + filter.ParseFilterString("Service:Debug"); + + REQUIRE(Allows(filter, Class::Service, Level::Debug)); + REQUIRE_FALSE(Allows(filter, Class::Service, Level::Trace)); + REQUIRE_FALSE(Allows(filter, Class::HW_GPU, Level::Debug)); +} + +TEST_CASE("Log filter parser rejects unknown level", "[common][logging]") { + Filter filter{Level::Warning}; + + filter.ParseFilterString("Service:notalevel"); + + REQUIRE_FALSE(Allows(filter, Class::Service, Level::Debug)); + REQUIRE(Allows(filter, Class::Service, Level::Warning)); +} + +TEST_CASE("Log filter canonicalization normalizes known names", "[common][logging]") { + REQUIRE(CanonicalizeFilterString("*:Warning") == "*:Warning"); + REQUIRE(CanonicalizeFilterString("*:warning") == "*:Warning"); + REQUIRE(CanonicalizeFilterString("*:WARNING") == "*:Warning"); + REQUIRE(CanonicalizeFilterString("service:debug") == "Service:Debug"); + REQUIRE(CanonicalizeFilterString("Service:notalevel") == "Service:notalevel"); +} + +} // namespace Common::Log