diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java
index 34ab49c96..b5e547266 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java
@@ -12,6 +12,7 @@ import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
+import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
@@ -19,11 +20,13 @@ import android.view.View;
import android.widget.CheckBox;
import android.widget.SeekBar;
import android.widget.TextView;
+import android.widget.Toast;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.PopupMenu;
import androidx.core.app.NotificationManagerCompat;
import androidx.fragment.app.FragmentActivity;
@@ -74,6 +77,7 @@ public final class EmulationActivity extends AppCompatActivity {
public static final int MENU_ACTION_JOYSTICK_REL_CENTER = 15;
public static final int MENU_ACTION_DPAD_SLIDE_ENABLE = 16;
public static final int MENU_ACTION_OPEN_CHEATS = 17;
+ public static final int MENU_ACTION_CLOSE_GAME = 18;
public static final int REQUEST_SELECT_AMIIBO = 2;
private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
@@ -114,6 +118,8 @@ public final class EmulationActivity extends AppCompatActivity {
EmulationActivity.MENU_ACTION_DPAD_SLIDE_ENABLE);
buttonsActionsMap
.append(R.id.menu_emulation_open_cheats, EmulationActivity.MENU_ACTION_OPEN_CHEATS);
+ buttonsActionsMap
+ .append(R.id.menu_emulation_close_game, EmulationActivity.MENU_ACTION_CLOSE_GAME);
}
private View mDecorView;
@@ -223,21 +229,12 @@ public final class EmulationActivity extends AppCompatActivity {
@Override
public void onBackPressed() {
- NativeLibrary.PauseEmulation();
- new AlertDialog.Builder(this)
- .setTitle(R.string.emulation_close_game)
- .setMessage(R.string.emulation_close_game_message)
- .setPositiveButton(android.R.string.yes, (dialogInterface, i) ->
- {
- mEmulationFragment.stopEmulation();
- finish();
- })
- .setNegativeButton(android.R.string.cancel, (dialogInterface, i) ->
- NativeLibrary.UnPauseEmulation())
- .setOnCancelListener(dialogInterface ->
- NativeLibrary.UnPauseEmulation())
- .create()
- .show();
+ View anchor = findViewById(R.id.menu_anchor);
+ PopupMenu popupMenu = new PopupMenu(this, anchor);
+ onCreateOptionsMenu(popupMenu.getMenu(), popupMenu.getMenuInflater());
+ updateSavestateMenuOptions(popupMenu.getMenu());
+ popupMenu.setOnMenuItemClickListener(this::onOptionsItemSelected);
+ popupMenu.show();
}
@Override
@@ -271,6 +268,10 @@ public final class EmulationActivity extends AppCompatActivity {
}
}
+ public void onEmulationStarted() {
+ Toast.makeText(this, getString(R.string.emulation_menu_help), Toast.LENGTH_LONG).show();
+ }
+
private void enableFullscreenImmersive() {
// It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar.
mDecorView.setSystemUiVisibility(
@@ -285,7 +286,12 @@ public final class EmulationActivity extends AppCompatActivity {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
- getMenuInflater().inflate(R.menu.menu_emulation, menu);
+ onCreateOptionsMenu(menu, getMenuInflater());
+ return true;
+ }
+
+ private void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.menu_emulation, menu);
int layoutOptionMenuItem = R.id.menu_screen_layout_landscape;
switch (EmulationMenuSettings.getLandscapeScreenLayout()) {
@@ -306,8 +312,6 @@ public final class EmulationActivity extends AppCompatActivity {
menu.findItem(R.id.menu_emulation_show_fps).setChecked(EmulationMenuSettings.getShowFps());
menu.findItem(R.id.menu_emulation_swap_screens).setChecked(EmulationMenuSettings.getSwapScreens());
menu.findItem(R.id.menu_emulation_show_overlay).setChecked(EmulationMenuSettings.getShowOverlay());
-
- return true;
}
private void DisplaySavestateWarning() {
@@ -333,12 +337,16 @@ public final class EmulationActivity extends AppCompatActivity {
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
+ updateSavestateMenuOptions(menu);
+ return true;
+ }
+ private void updateSavestateMenuOptions(Menu menu) {
final NativeLibrary.SavestateInfo[] savestates = NativeLibrary.GetSavestateInfo();
if (savestates == null) {
menu.findItem(R.id.menu_emulation_save_state).setVisible(false);
menu.findItem(R.id.menu_emulation_load_state).setVisible(false);
- return true;
+ return;
}
menu.findItem(R.id.menu_emulation_save_state).setVisible(true);
menu.findItem(R.id.menu_emulation_load_state).setVisible(true);
@@ -367,7 +375,6 @@ public final class EmulationActivity extends AppCompatActivity {
saveStateMenu.getItem(info.slot - 1).setTitle(text);
loadStateMenu.getItem(info.slot - 1).setTitle(text).setEnabled(true);
}
- return true;
}
@SuppressWarnings("WrongConstant")
@@ -480,6 +487,24 @@ public final class EmulationActivity extends AppCompatActivity {
case MENU_ACTION_OPEN_CHEATS:
CheatsActivity.launch(this);
break;
+
+ case MENU_ACTION_CLOSE_GAME:
+ NativeLibrary.PauseEmulation();
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.emulation_close_game)
+ .setMessage(R.string.emulation_close_game_message)
+ .setPositiveButton(android.R.string.yes, (dialogInterface, i) ->
+ {
+ mEmulationFragment.stopEmulation();
+ finish();
+ })
+ .setNegativeButton(android.R.string.cancel, (dialogInterface, i) ->
+ NativeLibrary.UnPauseEmulation())
+ .setOnCancelListener(dialogInterface ->
+ NativeLibrary.UnPauseEmulation())
+ .create()
+ .show();
+ break;
}
return true;
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java b/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java
index 8f9d215a3..e71c2cfc1 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java
@@ -133,6 +133,8 @@ public class DiskShaderCacheProgress {
case Complete:
// Workaround for when dialog is dismissed when the app is in the background
fragment.dismissAllowingStateLoss();
+
+ emulationActivity.runOnUiThread(emulationActivity::onEmulationStarted);
break;
}
}
diff --git a/src/android/app/src/main/res/layout/activity_emulation.xml b/src/android/app/src/main/res/layout/activity_emulation.xml
index 7d7f36925..5f5ff777d 100644
--- a/src/android/app/src/main/res/layout/activity_emulation.xml
+++ b/src/android/app/src/main/res/layout/activity_emulation.xml
@@ -14,4 +14,10 @@
android:layout_height="match_parent"
android:transitionName="image_game_icon" />
-
\ No newline at end of file
+
+
+
diff --git a/src/android/app/src/main/res/menu/menu_emulation.xml b/src/android/app/src/main/res/menu/menu_emulation.xml
index b6c0d7cc4..8492dfdb9 100644
--- a/src/android/app/src/main/res/menu/menu_emulation.xml
+++ b/src/android/app/src/main/res/menu/menu_emulation.xml
@@ -115,4 +115,9 @@
app:showAsAction="never"
android:title="@string/emulation_open_settings" />
+
+
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index a3200d3d2..282b59a29 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -164,6 +164,7 @@
Invalid ROM format
+ Press Back to access the menu.
Save State
Load State
Slot %1$d
diff --git a/src/citra_qt/cheats.cpp b/src/citra_qt/cheats.cpp
index fee42d55c..343568004 100644
--- a/src/citra_qt/cheats.cpp
+++ b/src/citra_qt/cheats.cpp
@@ -17,7 +17,6 @@ CheatDialog::CheatDialog(QWidget* parent)
: QDialog(parent), ui(std::make_unique()) {
// Setup gui control settings
ui->setupUi(this);
- setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
ui->tableCheats->setColumnWidth(0, 30);
ui->tableCheats->setColumnWidth(2, 85);
ui->tableCheats->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed);
diff --git a/src/citra_qt/configuration/configure_dialog.cpp b/src/citra_qt/configuration/configure_dialog.cpp
index 80670f2af..1b00ddbe3 100644
--- a/src/citra_qt/configuration/configure_dialog.cpp
+++ b/src/citra_qt/configuration/configure_dialog.cpp
@@ -20,8 +20,6 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry, bool
PopulateSelectionList();
- setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
-
connect(ui->uiTab, &ConfigureUi::LanguageChanged, this, &ConfigureDialog::OnLanguageChanged);
connect(ui->selectorList, &QListWidget::itemSelectionChanged, this,
&ConfigureDialog::UpdateVisibleTabs);
diff --git a/src/citra_qt/configuration/configure_per_game.cpp b/src/citra_qt/configuration/configure_per_game.cpp
index 19760fba6..fc74e9e4c 100644
--- a/src/citra_qt/configuration/configure_per_game.cpp
+++ b/src/citra_qt/configuration/configure_per_game.cpp
@@ -41,8 +41,6 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const QString
setFocusPolicy(Qt::ClickFocus);
setWindowTitle(tr("Properties"));
- // remove Help question mark button from the title bar
- setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
scene = new QGraphicsScene;
ui->icon_view->setScene(scene);
diff --git a/src/citra_qt/debugger/profiler.cpp b/src/citra_qt/debugger/profiler.cpp
index 9f394a020..9f195f1ee 100644
--- a/src/citra_qt/debugger/profiler.cpp
+++ b/src/citra_qt/debugger/profiler.cpp
@@ -50,9 +50,8 @@ MicroProfileDialog::MicroProfileDialog(QWidget* parent) : QWidget(parent, Qt::Di
setObjectName(QStringLiteral("MicroProfile"));
setWindowTitle(tr("MicroProfile"));
resize(1000, 600);
- // Remove the "?" button from the titlebar and enable the maximize button
- setWindowFlags((windowFlags() & ~Qt::WindowContextHelpButtonHint) |
- Qt::WindowMaximizeButtonHint);
+ // Enable the maximize button
+ setWindowFlags(windowFlags() | Qt::WindowMaximizeButtonHint);
#if MICROPROFILE_ENABLED
diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index d16d40de3..dd4ad6857 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -1019,6 +1019,11 @@ bool GMainWindow::LoadROM(const QString& filename) {
"titles."));
break;
+ case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle:
+ QMessageBox::critical(this, tr("Unsupported ROM"),
+ tr("GBA Virtual Console ROMs are not supported by Citra."));
+ break;
+
case Core::System::ResultStatus::ErrorVideoCore:
QMessageBox::critical(
this, tr("Video Core Error"),
@@ -2659,6 +2664,10 @@ int main(int argc, char* argv[]) {
#ifdef __APPLE__
std::string bin_path = FileUtil::GetBundleDirectory() + DIR_SEP + "..";
chdir(bin_path.c_str());
+#endif
+#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
+ // Disables the "?" button on all dialogs. Disabled by default on Qt6.
+ QCoreApplication::setAttribute(Qt::AA_DisableWindowContextHelpButton);
#endif
QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity);
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
diff --git a/src/citra_qt/util/sequence_dialog/sequence_dialog.cpp b/src/citra_qt/util/sequence_dialog/sequence_dialog.cpp
index 1cfe3c0bd..0bba5348e 100644
--- a/src/citra_qt/util/sequence_dialog/sequence_dialog.cpp
+++ b/src/citra_qt/util/sequence_dialog/sequence_dialog.cpp
@@ -9,7 +9,6 @@
SequenceDialog::SequenceDialog(QWidget* parent) : QDialog(parent) {
setWindowTitle(tr("Enter a hotkey"));
- setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
key_sequence = new QKeySequenceEdit;
diff --git a/src/common/common_paths.h b/src/common/common_paths.h
index eec4dde9c..1699f14e6 100644
--- a/src/common/common_paths.h
+++ b/src/common/common_paths.h
@@ -20,6 +20,10 @@
#else
#ifdef _WIN32
#define EMU_DATA_DIR "Citra"
+#elif defined(__APPLE__)
+#define MACOS_EMU_DATA_DIR "Library" DIR_SEP "Application Support" DIR_SEP "Citra"
+// For compatibility with XDG paths.
+#define EMU_DATA_DIR "citra-emu"
#elif ANDROID
// On Android internal storage is mounted as "/sdcard"
#define SDCARD_DIR "sdcard"
diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp
index 73c606972..7a53d294a 100644
--- a/src/common/file_util.cpp
+++ b/src/common/file_util.cpp
@@ -709,13 +709,26 @@ void SetUserPath(const std::string& path) {
g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP);
g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP);
} else {
- std::string data_dir = GetUserDirectory("XDG_DATA_HOME");
- std::string config_dir = GetUserDirectory("XDG_CONFIG_HOME");
- std::string cache_dir = GetUserDirectory("XDG_CACHE_HOME");
+ std::string data_dir = GetUserDirectory("XDG_DATA_HOME") + DIR_SEP EMU_DATA_DIR DIR_SEP;
+ std::string config_dir =
+ GetUserDirectory("XDG_CONFIG_HOME") + DIR_SEP EMU_DATA_DIR DIR_SEP;
+ std::string cache_dir =
+ GetUserDirectory("XDG_CACHE_HOME") + DIR_SEP EMU_DATA_DIR DIR_SEP;
- user_path = data_dir + DIR_SEP EMU_DATA_DIR DIR_SEP;
- g_paths.emplace(UserPath::ConfigDir, config_dir + DIR_SEP EMU_DATA_DIR DIR_SEP);
- g_paths.emplace(UserPath::CacheDir, cache_dir + DIR_SEP EMU_DATA_DIR DIR_SEP);
+#if defined(__APPLE__)
+ // If XDG directories don't already exist from a previous setup, use standard macOS
+ // paths.
+ if (!FileUtil::Exists(data_dir) && !FileUtil::Exists(config_dir) &&
+ !FileUtil::Exists(cache_dir)) {
+ data_dir = GetHomeDirectory() + DIR_SEP MACOS_EMU_DATA_DIR DIR_SEP;
+ config_dir = data_dir + CONFIG_DIR DIR_SEP;
+ cache_dir = data_dir + CACHE_DIR DIR_SEP;
+ }
+#endif
+
+ user_path = data_dir;
+ g_paths.emplace(UserPath::ConfigDir, config_dir);
+ g_paths.emplace(UserPath::CacheDir, cache_dir);
}
#endif
}
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 11cbc1282..c369611e2 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -268,6 +268,8 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st
return ResultStatus::ErrorLoader_ErrorEncrypted;
case Loader::ResultStatus::ErrorInvalidFormat:
return ResultStatus::ErrorLoader_ErrorInvalidFormat;
+ case Loader::ResultStatus::ErrorGbaTitle:
+ return ResultStatus::ErrorLoader_ErrorGbaTitle;
default:
return ResultStatus::ErrorSystemMode;
}
@@ -292,7 +294,6 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st
telemetry_session->AddInitialInfo(*app_loader);
std::shared_ptr process;
const Loader::ResultStatus load_result{app_loader->Load(process)};
- kernel->SetCurrentProcess(process);
if (Loader::ResultStatus::Success != load_result) {
LOG_CRITICAL(Core, "Failed to load ROM (Error {})!", load_result);
System::Shutdown();
@@ -302,10 +303,13 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st
return ResultStatus::ErrorLoader_ErrorEncrypted;
case Loader::ResultStatus::ErrorInvalidFormat:
return ResultStatus::ErrorLoader_ErrorInvalidFormat;
+ case Loader::ResultStatus::ErrorGbaTitle:
+ return ResultStatus::ErrorLoader_ErrorGbaTitle;
default:
return ResultStatus::ErrorLoader;
}
}
+ kernel->SetCurrentProcess(process);
cheat_engine = std::make_unique(*this);
title_id = 0;
if (app_loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) {
@@ -537,7 +541,8 @@ void System::Shutdown(bool is_deserializing) {
perf_results.emulation_speed * 100.0);
telemetry_session->AddField(performance, "Shutdown_Framerate", perf_results.game_fps);
telemetry_session->AddField(performance, "Shutdown_Frametime", perf_results.frametime * 1000.0);
- telemetry_session->AddField(performance, "Mean_Frametime_MS", perf_stats->GetMeanFrametime());
+ telemetry_session->AddField(performance, "Mean_Frametime_MS",
+ perf_stats ? perf_stats->GetMeanFrametime() : 0);
// Shutdown emulation session
VideoCore::Shutdown();
diff --git a/src/core/core.h b/src/core/core.h
index 61b1005a1..cb4be20fe 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -82,10 +82,12 @@ public:
ErrorSystemMode, ///< Error determining the system mode
ErrorLoader, ///< Error loading the specified application
ErrorLoader_ErrorEncrypted, ///< Error loading the specified application due to encryption
- ErrorLoader_ErrorInvalidFormat, ///< Error loading the specified application due to an
- /// invalid format
- ErrorSystemFiles, ///< Error in finding system files
- ErrorVideoCore, ///< Error in the video core
+ ErrorLoader_ErrorInvalidFormat, ///< Error loading the specified application due to an
+ /// invalid format
+ ErrorLoader_ErrorGbaTitle, ///< Error loading the specified application as it is GBA Virtual
+ ///< Console
+ ErrorSystemFiles, ///< Error in finding system files
+ ErrorVideoCore, ///< Error in the video core
ErrorVideoCore_ErrorGenericDrivers, ///< Error in the video core due to the user having
/// generic drivers installed
ErrorSavestate, ///< Error saving or loading
diff --git a/src/core/file_sys/plugin_3gx.cpp b/src/core/file_sys/plugin_3gx.cpp
index e59d39090..61928982d 100644
--- a/src/core/file_sys/plugin_3gx.cpp
+++ b/src/core/file_sys/plugin_3gx.cpp
@@ -26,7 +26,8 @@
#include "core/loader/loader.h"
static std::string ReadTextInfo(FileUtil::IOFile& file, std::size_t offset, std::size_t max_size) {
- if (max_size > 0x400) { // Limit read string size to 0x400 bytes, just in case
+ if (offset == 0 || max_size == 0 ||
+ max_size > 0x400) { // Limit read string size to 0x400 bytes, just in case
return "";
}
std::vector char_data(max_size);
diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h
index d42aab3a1..9f50c79d1 100644
--- a/src/core/loader/loader.h
+++ b/src/core/loader/loader.h
@@ -75,6 +75,7 @@ enum class ResultStatus {
ErrorAlreadyLoaded,
ErrorMemoryAllocationFailed,
ErrorEncrypted,
+ ErrorGbaTitle,
};
constexpr u32 MakeMagic(char a, char b, char c, char d) {
diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp
index e376120a7..d233aef26 100644
--- a/src/core/loader/ncch.cpp
+++ b/src/core/loader/ncch.cpp
@@ -85,6 +85,11 @@ ResultStatus AppLoader_NCCH::LoadExec(std::shared_ptr& process)
u64_le program_id;
if (ResultStatus::Success == ReadCode(code) &&
ResultStatus::Success == ReadProgramId(program_id)) {
+ if (IsGbaVirtualConsole(code)) {
+ LOG_ERROR(Loader, "Encountered unsupported GBA Virtual Console code section.");
+ return ResultStatus::ErrorGbaTitle;
+ }
+
std::string process_name = Common::StringFromFixedZeroTerminatedBuffer(
(const char*)overlay_ncch->exheader_header.codeset_info.name, 8);
@@ -177,6 +182,12 @@ void AppLoader_NCCH::ParseRegionLockoutInfo() {
}
}
+bool AppLoader_NCCH::IsGbaVirtualConsole(const std::vector& code) {
+ const u32* gbaVcHeader = reinterpret_cast(code.data() + code.size() - 0x10);
+ return code.size() >= 0x10 && gbaVcHeader[0] == MakeMagic('.', 'C', 'A', 'A') &&
+ gbaVcHeader[1] == 1;
+}
+
ResultStatus AppLoader_NCCH::Load(std::shared_ptr& process) {
u64_le ncch_program_id;
diff --git a/src/core/loader/ncch.h b/src/core/loader/ncch.h
index 6f680b063..76ce61446 100644
--- a/src/core/loader/ncch.h
+++ b/src/core/loader/ncch.h
@@ -78,6 +78,9 @@ private:
/// Reads the region lockout info in the SMDH and send it to CFG service
void ParseRegionLockoutInfo();
+ /// Detects whether the NCCH contains GBA Virtual Console.
+ bool IsGbaVirtualConsole(const std::vector& code);
+
FileSys::NCCHContainer base_ncch;
FileSys::NCCHContainer update_ncch;
FileSys::NCCHContainer* overlay_ncch;