diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt index 55ddd5950c..2215afb663 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt @@ -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: 2023 yuzu Emulator Project @@ -11,6 +11,7 @@ import org.yuzu.yuzu_emu.utils.NativeConfig enum class StringSetting(override val key: String) : AbstractStringSetting { DRIVER_PATH("driver_path"), DEVICE_NAME("device_name"), + PROGRAM_ARGS("program_args"), WEB_TOKEN("eden_token"), WEB_USERNAME("eden_username") diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index f95c53720f..e3cd458a39 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -125,6 +125,13 @@ abstract class SettingsItem( // List of all general val settingsItems = HashMap().apply { put(StringInputSetting(StringSetting.DEVICE_NAME, titleId = R.string.device_name)) + put( + StringInputSetting( + StringSetting.PROGRAM_ARGS, + titleId = R.string.program_args, + descriptionId = R.string.program_args_description + ) + ) put( SwitchSetting( BooleanSetting.RENDERER_USE_SPEED_LIMIT, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 161579927c..e2d70f1670 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -1286,6 +1286,7 @@ class SettingsFragmentPresenter( add(HeaderSetting(R.string.general)) add(ShortSetting.DEBUG_KNOBS.key) + add(StringSetting.PROGRAM_ARGS.key) add(HeaderSetting(R.string.gpu_logging_header)) add(BooleanSetting.GPU_LOGGING_ENABLED.key) diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 77be250537..a425ce36ef 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -434,6 +434,9 @@ CPU accuracy %1$s%2$s + Homebrew Args + Command-line arguments passed to homebrew at launch (e.g. -noglsl). + Device name Docked Mode diff --git a/src/common/settings.h b/src/common/settings.h index be9c080bc6..5f6e1a2206 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -779,7 +779,13 @@ struct Values { bool record_frame_times; Setting use_gdbstub{linkage, false, "use_gdbstub", Category::Debugging}; Setting gdbstub_port{linkage, 6543, "gdbstub_port", Category::Debugging}; - Setting program_args{linkage, std::string(), "program_args", Category::Debugging}; + SwitchableSetting program_args{linkage, + std::string(), + "program_args", + Category::System, + Specialization::Default, + true, // save_ — persist in config file + false}; // runtime_modifiable_ — startup-only Setting dump_exefs{linkage, false, "dump_exefs", Category::Debugging}; Setting dump_nso{linkage, false, "dump_nso", Category::Debugging}; Setting dump_shaders{ diff --git a/src/core/hle/kernel/k_process.cpp b/src/core/hle/kernel/k_process.cpp index 364f44849b..f737a18736 100644 --- a/src/core/hle/kernel/k_process.cpp +++ b/src/core/hle/kernel/k_process.cpp @@ -211,6 +211,9 @@ Result KProcess::Initialize(const Svc::CreateProcessParameter& params, KResource m_version = params.version; m_program_id = params.program_id; m_code_address = params.code_address; + m_arg_pointer = 0; + m_arg_return_address = 0; + m_main_thread_handle_addr = 0; m_code_size = params.code_num_pages * PageSize; m_is_application = True(params.flags & Svc::CreateProcessFlag::IsApplication); @@ -995,9 +998,27 @@ Result KProcess::Run(s32 priority, size_t stack_size) { Handle thread_handle; R_TRY(m_handle_table.Add(std::addressof(thread_handle), main_thread)); - // Set the thread arguments. - main_thread->GetContext().r[0] = 0; - main_thread->GetContext().r[1] = thread_handle; + // Set the thread arguments. Two distinct entry conventions: + // * Kernel/NSO entry (no homebrew ABI): x0 = 0, x1 = thread_handle + // * Homebrew/NRO ABI (loader set arg ptr): x0 = ConfigEntry ptr, x1 = -1ULL + // libnx's switch_crt0.s tests `x0==0 || x1==0xFFFFFFFFFFFFFFFF` to take + // its normal init path; any other combination is interpreted as a user + // exception handler entry. + if (GetInteger(m_arg_pointer) != 0) { + main_thread->GetContext().r[0] = GetInteger(m_arg_pointer); + main_thread->GetContext().r[1] = UINT64_MAX; + main_thread->GetContext().lr = GetInteger(m_arg_return_address); + // Patch the MainThreadHandle entry in the ConfigEntry table now that + // the actual handle exists. libnx stores this verbatim and uses it + // for thread-control SVCs later; a pseudo-handle wouldn't survive + // svcCloseHandle on exit. + if (GetInteger(m_main_thread_handle_addr) != 0) { + this->GetMemory().Write32(m_main_thread_handle_addr, thread_handle); + } + } else { + main_thread->GetContext().r[0] = 0; + main_thread->GetContext().r[1] = thread_handle; + } // Pass the thread handle to the thread local region. this->GetMemory().Write32(GetInteger(main_thread->GetTlsAddress()) + 0x110, thread_handle); diff --git a/src/core/hle/kernel/k_process.h b/src/core/hle/kernel/k_process.h index b3922444cc..2413e5bfd9 100644 --- a/src/core/hle/kernel/k_process.h +++ b/src/core/hle/kernel/k_process.h @@ -84,6 +84,9 @@ private: Core::Memory::Memory m_memory; KCapabilities m_capabilities{}; KProcessAddress m_code_address{}; + KProcessAddress m_arg_pointer{}; + KProcessAddress m_arg_return_address{}; + KProcessAddress m_main_thread_handle_addr{}; KHandleTable m_handle_table; KProcessAddress m_plr_address{}; ThreadList m_thread_list{}; @@ -219,6 +222,16 @@ public: return m_code_address; } + void SetArgPointer(KProcessAddress addr) { + m_arg_pointer = addr; + } + void SetArgReturnAddress(KProcessAddress addr) { + m_arg_return_address = addr; + } + void SetMainThreadHandleAddr(KProcessAddress addr) { + m_main_thread_handle_addr = addr; + } + size_t GetMainStackSize() const { return m_main_thread_stack_size; } diff --git a/src/core/loader/nro.cpp b/src/core/loader/nro.cpp index 866059286a..21640b7330 100644 --- a/src/core/loader/nro.cpp +++ b/src/core/loader/nro.cpp @@ -4,9 +4,15 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include +#include +#include #include #include +#include "common/alignment.h" #include "common/common_funcs.h" #include "common/common_types.h" #include "common/logging.h" @@ -23,7 +29,6 @@ #include "core/hle/kernel/k_thread.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/loader/nro.h" -#include "core/loader/nso.h" #include "core/memory.h" #ifdef HAS_NCE @@ -174,19 +179,6 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process, codeset.segments[i].size = PageAlignSize(nro_header.segments[i].size); } - if (!Settings::values.program_args.GetValue().empty()) { - const auto arg_data = Settings::values.program_args.GetValue(); - codeset.DataSegment().size += NSO_ARGUMENT_DATA_ALLOCATION_SIZE; - NSOArgumentHeader args_header{ - NSO_ARGUMENT_DATA_ALLOCATION_SIZE, static_cast(arg_data.size()), {}}; - const auto end_offset = program_image.size(); - program_image.resize(static_cast(program_image.size()) + - NSO_ARGUMENT_DATA_ALLOCATION_SIZE); - std::memcpy(program_image.data() + end_offset, &args_header, sizeof(NSOArgumentHeader)); - std::memcpy(program_image.data() + end_offset + sizeof(NSOArgumentHeader), arg_data.data(), - arg_data.size()); - } - // Default .bss to NRO header bss size if MOD0 section doesn't exist u32 bss_size{PageAlignSize(nro_header.bss_size)}; @@ -203,6 +195,47 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process, codeset.DataSegment().size += bss_size; program_image.resize(static_cast(program_image.size()) + bss_size); + struct ConfigEntry { + u32_le key; + u32_le flags; + u64_le value[2]; + }; + static_assert(sizeof(ConfigEntry) == 0x18); + // AArch64 encoding for svc #0x7 (ExitProcess). + constexpr u32 kSvcExitProcessInstruction = 0xD40000E1; + constexpr size_t kNumEntries = 4; // MainThreadHandle, AppletType, Argv, EndOfList + constexpr size_t kConfigTableSize = kNumEntries * sizeof(ConfigEntry); + std::string argv_string; + size_t args_offset_in_image = 0; + std::optional exit_process_offset_in_image; + const auto& program_args = Settings::values.program_args.GetValue(); + if (!program_args.empty()) { + argv_string = "homebrew "; + argv_string += program_args; + argv_string.push_back('\0'); + + const auto& code = codeset.CodeSegment(); + const size_t code_end = (std::min)(program_image.size(), code.offset + code.size); + for (size_t offset = code.offset; offset + sizeof(u32) <= code_end; offset += sizeof(u32)) { + u32 instruction{}; + std::memcpy(&instruction, program_image.data() + offset, sizeof(instruction)); + if (instruction == kSvcExitProcessInstruction) { + exit_process_offset_in_image = offset; + break; + } + } + if (!exit_process_offset_in_image) { + LOG_WARNING(Loader, + "Unable to find svcExitProcess in NRO; returning from main may fault"); + } + + const size_t entries_and_argv = + Common::AlignUp(kConfigTableSize + argv_string.size(), Core::Memory::YUZU_PAGESIZE); + + args_offset_in_image = program_image.size(); + codeset.DataSegment().size += static_cast(entries_and_argv); + program_image.resize(args_offset_in_image + entries_and_argv); + } size_t image_size = program_image.size(); #ifdef HAS_NCE @@ -264,6 +297,37 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process, // Load codeset for current process codeset.memory = std::move(program_image); process.LoadModule(std::move(codeset), process.GetEntryPoint()); + if (!argv_string.empty()) { + constexpr u32 kEntryEndOfList = 0; + constexpr u32 kEntryMainThreadHandle = 1; + constexpr u32 kEntryArgv = 5; + constexpr u32 kEntryAppletType = 7; + constexpr u32 kAppletTypeApplication = 0; + + const u64 base = GetInteger(process.GetEntryPoint()); + const u64 config_addr = base + args_offset_in_image; + const u64 argv_addr = config_addr + kConfigTableSize; + + const ConfigEntry entries[kNumEntries] = { + {kEntryMainThreadHandle, 0, {0, 0}}, // Value[0] patched in Run() + {kEntryAppletType, 0, {kAppletTypeApplication, 0}}, + {kEntryArgv, 0, {0, argv_addr}}, + {kEntryEndOfList, 0, {0, 0}}, + }; + process.GetMemory().WriteBlock(Common::ProcessAddress{config_addr}, entries, + sizeof(entries)); + process.GetMemory().WriteBlock(Common::ProcessAddress{argv_addr}, + argv_string.data(), argv_string.size()); + + constexpr size_t kMainThreadHandleValueOffset = offsetof(ConfigEntry, value); + process.SetArgPointer(Kernel::KProcessAddress{config_addr}); + if (exit_process_offset_in_image) { + process.SetArgReturnAddress( + Kernel::KProcessAddress{base + *exit_process_offset_in_image}); + } + process.SetMainThreadHandleAddr( + Kernel::KProcessAddress{config_addr + kMainThreadHandleValueOffset}); + } return true; } diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index 5c63732a3e..6778f37f7b 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -301,6 +301,8 @@ std::unique_ptr InitializeTranslations(QObject* parent) { tr("Controls the seed of the random number generator.\nMainly used for speedrunning.")); INSERT(Settings, rng_seed_enabled, QString(), QString()); INSERT(Settings, device_name, tr("Device Name"), tr("The name of the console.")); + INSERT(Settings, program_args, tr("Homebrew Args"), + tr("Command-line arguments passed to homebrew at launch (e.g. -noglsl).")); INSERT(Settings, custom_rtc, tr("Custom RTC Date:"), tr("This option allows to change the clock of the console.\n" "Can be used to manipulate time in games."));