[macOS/UI] Add ability to launch with Rosetta2 on Apple Silicon

Reworks the quick settings UI to remove Audio section that's no longer
very meaningful and adds platform specific toggles there instead on
macOS and Linux.
This commit is contained in:
Herman S.
2026-06-01 09:50:39 +09:00
parent 54e6e42505
commit d29f7530bd
3 changed files with 188 additions and 17 deletions
+69 -1
View File
@@ -112,6 +112,10 @@
#endif
#endif
#if XE_PLATFORM_MAC
#include <sys/sysctl.h>
#endif
#include "xenia/apu/apu_flags.h"
#include "xenia/apu/audio_system.h"
#include "xenia/cpu/backend/backend.h"
@@ -278,6 +282,11 @@ DEFINE_bool(
"Enable the Metal performance HUD (MTL_HUD_ENABLED=1) when launching "
"games in separate processes.",
"MacOS");
DEFINE_bool(
use_rosetta, false,
"Launch games under Rosetta 2 (x86_64) instead of native arm64.\n"
"Only meaningful on Apple Silicon hosts running a universal app bundle.",
"MacOS");
#endif
DEFINE_bool(disable_game_window_mouse, false,
"Disables mouse interactions in the game window, including "
@@ -312,6 +321,25 @@ struct WxToolbarState {
};
namespace {
#if XE_PLATFORM_MAC
// execv of a universal binary inherits the parent's arch; arch(1) is the
// only way to switch (e.g. Rosetta-running game spawning the native arm64
// UI back). Returns "-arm64" / "-x86_64" when a switch is needed, or null
// when default exec already picks the right slice.
const char* RosettaArchFlagIfSwitchNeeded(bool want_rosetta) {
int translated = 0;
size_t sz = sizeof(translated);
// Missing sysctl => assume native; only Rosetta children set this to 1.
sysctlbyname("sysctl.proc_translated", &translated, &sz, nullptr, 0);
const bool parent_rosetta = (translated != 0);
if (parent_rosetta == want_rosetta) {
return nullptr;
}
return want_rosetta ? "-x86_64" : "-arm64";
}
#endif
// The first tool flips between "Open" (no title) and "Back" (title running).
constexpr int kToolIdOpenBack = 11000;
constexpr int kToolIdSettings = 11001;
@@ -733,8 +761,13 @@ void EmulatorWindow::OnEmulatorInitialized() {
std::string gamemode_cmd;
#if XE_PLATFORM_LINUX
// unsetenv on the false branch: the env var is inherited across the
// UI <-> title spawn chain, so a stale "1" from a previous launch
// would otherwise outlive the unchecked cvar.
if (cvars::use_mangohud) {
setenv("MANGOHUD", "1", 1);
} else {
unsetenv("MANGOHUD");
}
if (cvars::use_gamemode) {
gamemode_cmd = "gamemoderun";
@@ -743,8 +776,17 @@ void EmulatorWindow::OnEmulatorInitialized() {
#if XE_PLATFORM_MAC
if (cvars::use_metal_hud) {
setenv("MTL_HUD_ENABLED", "1", 1);
} else {
unsetenv("MTL_HUD_ENABLED");
}
// Empty host_path = returning to the UI/game list; the per-title
// rosetta override shouldn't follow us back, so use native there.
const bool want_rosetta = !host_path.empty() && cvars::use_rosetta;
const char* arch_flag = RosettaArchFlagIfSwitchNeeded(want_rosetta);
#else
const char* arch_flag = nullptr;
#endif
const bool use_arch_wrapper = (arch_flag != nullptr);
arg_storage.push_back(executable_path.string());
@@ -790,6 +832,10 @@ void EmulatorWindow::OnEmulatorInitialized() {
if (!gamemode_cmd.empty()) {
argv.push_back(gamemode_cmd.c_str());
}
if (use_arch_wrapper) {
argv.push_back("arch");
argv.push_back(arch_flag);
}
for (const std::string& arg : arg_storage) {
argv.push_back(arg.c_str());
}
@@ -797,6 +843,8 @@ void EmulatorWindow::OnEmulatorInitialized() {
if (!gamemode_cmd.empty()) {
execvp(gamemode_cmd.c_str(), const_cast<char**>(argv.data()));
} else if (use_arch_wrapper) {
execvp("arch", const_cast<char**>(argv.data()));
} else {
execv(executable_path.c_str(), const_cast<char**>(argv.data()));
}
@@ -3458,8 +3506,11 @@ void EmulatorWindow::LaunchTitleInNewProcess(
std::string gamemode_cmd;
#if XE_PLATFORM_LINUX
// See OnEmulatorInitialized: env vars survive the UI<->title spawn chain.
if (cvars::use_mangohud) {
setenv("MANGOHUD", "1", 1);
} else {
unsetenv("MANGOHUD");
}
if (cvars::use_gamemode) {
gamemode_cmd = "gamemoderun";
@@ -3469,8 +3520,18 @@ void EmulatorWindow::LaunchTitleInNewProcess(
#if XE_PLATFORM_MAC
if (cvars::use_metal_hud) {
setenv("MTL_HUD_ENABLED", "1", 1);
} else {
unsetenv("MTL_HUD_ENABLED");
}
const char* arch_flag = RosettaArchFlagIfSwitchNeeded(cvars::use_rosetta);
#else
const char* arch_flag = nullptr;
#endif
const bool use_arch_wrapper = (arch_flag != nullptr);
if (use_arch_wrapper) {
argv.push_back("arch");
argv.push_back(arch_flag);
}
argv.push_back(executable_path.c_str());
@@ -3507,6 +3568,8 @@ void EmulatorWindow::LaunchTitleInNewProcess(
if (!gamemode_cmd.empty()) {
execvp(gamemode_cmd.c_str(), const_cast<char**>(argv.data()));
} else if (use_arch_wrapper) {
execvp("arch", const_cast<char**>(argv.data()));
} else {
execv(executable_path.c_str(), const_cast<char**>(argv.data()));
}
@@ -3616,7 +3679,12 @@ xe::X_STATUS EmulatorWindow::RunTitle(
}
#if XE_PLATFORM_LINUX || XE_PLATFORM_MAC
// In-process launch is unstable here; spawn a fresh process.
// In-process launch is unstable here; spawn a fresh process. Apply the
// title's overrides first — LaunchTitleInNewProcess reads cvars (e.g.
// use_rosetta, use_gamemode) before fork, so the child needs the
// overridden values inherited from the parent.
config::ReloadConfig();
config::LoadGameConfigForFile(abs_path);
LaunchTitleInNewProcess(abs_path);
return X_STATUS_SUCCESS;
#endif
+36 -16
View File
@@ -247,26 +247,49 @@ void QuickSettingsDialog::Build() {
main_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 6);
}
// ---- Audio ----
#if XE_PLATFORM_LINUX
// ---- Linux launch options ----
{
auto* box = new wxStaticBox(this, wxID_ANY, _("Audio"));
auto* box = new wxStaticBox(this, wxID_ANY, _("Linux"));
auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL);
auto* grid = new wxFlexGridSizer(2, 6, 8);
grid->AddGrowableCol(1, 1);
auto* apu = add_combo(box, "apu", find_options("apu"));
auto* apu_label = add_label(box, _("Audio Backend:"));
remember_label("apu", apu_label);
add_form_row(grid, apu_label, apu);
auto* mangohud = add_check(box, "use_mangohud");
auto* mangohud_label = add_label(box, _("MangoHUD overlay:"));
remember_label("use_mangohud", mangohud_label);
add_form_row(grid, mangohud_label, mangohud);
auto* volume = add_spin(box, "volume", 0, 100);
auto* volume_label = add_label(box, _("Default Volume:"));
remember_label("volume", volume_label);
add_form_row(grid, volume_label, volume);
auto* gamemode = add_check(box, "use_gamemode");
auto* gamemode_label = add_label(box, _("Feral GameMode:"));
remember_label("use_gamemode", gamemode_label);
add_form_row(grid, gamemode_label, gamemode);
box_sizer->Add(grid, 1, wxEXPAND | wxALL, 6);
main_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 6);
}
#elif XE_PLATFORM_MAC
// ---- macOS launch options ----
{
auto* box = new wxStaticBox(this, wxID_ANY, _("macOS"));
auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL);
auto* grid = new wxFlexGridSizer(2, 6, 8);
grid->AddGrowableCol(1, 1);
auto* metal_hud = add_check(box, "use_metal_hud");
auto* metal_hud_label = add_label(box, _("Metal performance HUD:"));
remember_label("use_metal_hud", metal_hud_label);
add_form_row(grid, metal_hud_label, metal_hud);
auto* rosetta = add_check(box, "use_rosetta");
auto* rosetta_label = add_label(box, _("Run under Rosetta 2 (x86_64):"));
remember_label("use_rosetta", rosetta_label);
add_form_row(grid, rosetta_label, rosetta);
box_sizer->Add(grid, 1, wxEXPAND | wxALL, 6);
main_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 6);
}
#endif
// ---- Other ----
{
@@ -549,11 +572,6 @@ void QuickSettingsDialog::Save() {
static_cast<int64_t>(std::stoull(opt.pending_value))));
} catch (...) {
}
} else if (name == "volume") {
try {
apply(var, toml::value(std::stoi(opt.pending_value)));
} catch (...) {
}
} else if (name == "license_mask") {
try {
apply(var, toml::value(std::stoi(opt.pending_value)));
@@ -561,7 +579,9 @@ void QuickSettingsDialog::Save() {
}
} else if (name == "fullscreen" || name == "present_letterbox" ||
name == "discord" || name == "use_dedicated_xma_thread" ||
name == "in_process_title_relaunch") {
name == "in_process_title_relaunch" || name == "use_mangohud" ||
name == "use_gamemode" || name == "use_metal_hud" ||
name == "use_rosetta") {
apply(var, toml::value(opt.pending_value == "true"));
} else if (ui::FindIntCvarEnumOptions(name)) {
// Dropdown int cvar: convert the selected option name to its id.
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
#
# Combine two single-arch Xenia-edge.app bundles into one universal bundle.
# Run after both arm64 and x86_64 builds complete. Similar to the lipo step
# in .github/workflows/CI.yml but meant for interactive dev use.
#
# Usage:
# tools/build-universal-macos.sh [arm64_app] [x86_64_app]
# Defaults assume the conventional layout: build/ for arm64, build-x64/ for
# x86_64, Release config.
set -euo pipefail
arm64_app=${1:-build/bin/macOS/Release/Xenia-edge.app}
x86_64_app=${2:-build-x64/bin/macOS/Release/Xenia-edge.app}
for app in "$arm64_app" "$x86_64_app"; do
if [[ ! -d "$app" ]]; then
echo "error: app bundle not found: $app" >&2
exit 1
fi
done
arm64_exe="$arm64_app/Contents/MacOS/Xenia-edge"
x86_64_exe="$x86_64_app/Contents/MacOS/Xenia-edge"
# Returns a path containing only the requested arch. If the input is already
# thin and matches, returns the original path; if it's fat (e.g. left over from
# a previous run of this script that lipo'd in place), extracts the slice to a
# temp file so the next lipo doesn't see duplicate archs.
thin_input() {
local exe=$1
local want=$2
local out=$3
local archs
archs=$(lipo -archs "$exe")
if [[ "$archs" != *"$want"* ]]; then
echo "error: $exe is '$archs', expected '$want'." >&2
echo " Reconfigure that build dir with -DCMAKE_OSX_ARCHITECTURES=$want." >&2
exit 1
fi
if [[ "$archs" == "$want" ]]; then
printf -v "$out" '%s' "$exe"
else
local tmp
tmp=$(mktemp -t Xenia-edge."$want")
lipo -thin "$want" "$exe" -output "$tmp"
printf -v "$out" '%s' "$tmp"
fi
}
thin_input "$arm64_exe" arm64 arm64_input
thin_input "$x86_64_exe" x86_64 x86_64_input
# Only the main Mach-O is merged
# Fail loud if anything else diverges so we don't silently create a
# bundle where resources don't match one of the slices.
# xenia.log is a runtime drop (the app cwd is Contents/MacOS), and
# _CodeSignature is regenerated by the codesign call below.
if ! diff -r --brief \
--exclude=Xenia-edge \
--exclude=xenia.log \
--exclude=_CodeSignature \
"$arm64_app/Contents" "$x86_64_app/Contents"; then
echo "error: bundles differ outside Contents/MacOS/Xenia-edge" >&2
exit 1
fi
tmp=$(mktemp -t Xenia-edge.universal)
lipo -create "$arm64_input" "$x86_64_input" -output "$tmp"
mv "$tmp" "$arm64_exe"
info=$(lipo -info "$arm64_exe")
echo "$info"
if ! grep -q arm64 <<<"$info" || ! grep -q x86_64 <<<"$info"; then
echo "error: lipo result missing an arch" >&2
exit 1
fi
# lipo invalidates the linker-applied ad-hoc signature; arm64 refuses to
# launch unsigned, so re-sign ad-hoc.
codesign --force --sign - "$arm64_app"
echo "universal bundle: $arm64_app"