mirror of
https://github.com/yuzu-emu/yuzu-android.git
synced 2025-06-27 11:57:51 -05:00
Merge branch 'master' into new-shortcut
This commit is contained in:
@ -227,14 +227,14 @@ add_executable(yuzu
|
||||
yuzu.rc
|
||||
)
|
||||
|
||||
if (WIN32 AND YUZU_CRASH_DUMPS)
|
||||
if (YUZU_CRASH_DUMPS)
|
||||
target_sources(yuzu PRIVATE
|
||||
mini_dump.cpp
|
||||
mini_dump.h
|
||||
breakpad.cpp
|
||||
breakpad.h
|
||||
)
|
||||
|
||||
target_link_libraries(yuzu PRIVATE ${DBGHELP_LIBRARY})
|
||||
target_compile_definitions(yuzu PRIVATE -DYUZU_DBGHELP)
|
||||
target_link_libraries(yuzu PRIVATE libbreakpad_client)
|
||||
target_compile_definitions(yuzu PRIVATE YUZU_CRASH_DUMPS)
|
||||
endif()
|
||||
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
|
@ -155,18 +155,27 @@ QtControllerSelectorDialog::QtControllerSelectorDialog(
|
||||
UpdateBorderColor(i);
|
||||
|
||||
connect(player_groupboxes[i], &QGroupBox::toggled, [this, i](bool checked) {
|
||||
if (checked) {
|
||||
// Hide eventual error message about number of controllers
|
||||
ui->labelError->setVisible(false);
|
||||
for (std::size_t index = 0; index <= i; ++index) {
|
||||
connected_controller_checkboxes[index]->setChecked(checked);
|
||||
}
|
||||
} else {
|
||||
for (std::size_t index = i; index < NUM_PLAYERS; ++index) {
|
||||
connected_controller_checkboxes[index]->setChecked(checked);
|
||||
}
|
||||
// Reconnect current controller if it was the last one checked
|
||||
// (player number was reduced by more than one)
|
||||
const bool reconnect_first = !checked && i < player_groupboxes.size() - 1 &&
|
||||
player_groupboxes[i + 1]->isChecked();
|
||||
|
||||
// Ensures that connecting a controller changes the number of players
|
||||
if (connected_controller_checkboxes[i]->isChecked() != checked) {
|
||||
// Ensures that the players are always connected in sequential order
|
||||
PropagatePlayerNumberChanged(i, checked, reconnect_first);
|
||||
}
|
||||
});
|
||||
connect(connected_controller_checkboxes[i], &QCheckBox::clicked, [this, i](bool checked) {
|
||||
// Reconnect current controller if it was the last one checked
|
||||
// (player number was reduced by more than one)
|
||||
const bool reconnect_first = !checked &&
|
||||
i < connected_controller_checkboxes.size() - 1 &&
|
||||
connected_controller_checkboxes[i + 1]->isChecked();
|
||||
|
||||
// Ensures that the players are always connected in sequential order
|
||||
PropagatePlayerNumberChanged(i, checked, reconnect_first);
|
||||
});
|
||||
|
||||
connect(emulated_controllers[i], qOverload<int>(&QComboBox::currentIndexChanged),
|
||||
[this, i](int) {
|
||||
@ -668,6 +677,29 @@ void QtControllerSelectorDialog::UpdateDockedState(bool is_handheld) {
|
||||
}
|
||||
}
|
||||
|
||||
void QtControllerSelectorDialog::PropagatePlayerNumberChanged(size_t player_index, bool checked,
|
||||
bool reconnect_current) {
|
||||
connected_controller_checkboxes[player_index]->setChecked(checked);
|
||||
// Hide eventual error message about number of controllers
|
||||
ui->labelError->setVisible(false);
|
||||
|
||||
if (checked) {
|
||||
// Check all previous buttons when checked
|
||||
if (player_index > 0) {
|
||||
PropagatePlayerNumberChanged(player_index - 1, checked);
|
||||
}
|
||||
} else {
|
||||
// Unchecked all following buttons when unchecked
|
||||
if (player_index < connected_controller_checkboxes.size() - 1) {
|
||||
PropagatePlayerNumberChanged(player_index + 1, checked);
|
||||
}
|
||||
}
|
||||
|
||||
if (reconnect_current) {
|
||||
connected_controller_checkboxes[player_index]->setCheckState(Qt::Checked);
|
||||
}
|
||||
}
|
||||
|
||||
void QtControllerSelectorDialog::DisableUnsupportedPlayers() {
|
||||
const auto max_supported_players = parameters.enable_single_mode ? 1 : parameters.max_players;
|
||||
|
||||
|
@ -100,6 +100,10 @@ private:
|
||||
// Updates the console mode.
|
||||
void UpdateDockedState(bool is_handheld);
|
||||
|
||||
// Enable preceding controllers or disable following ones
|
||||
void PropagatePlayerNumberChanged(size_t player_index, bool checked,
|
||||
bool reconnect_current = false);
|
||||
|
||||
// Disables and disconnects unsupported players based on the given parameters.
|
||||
void DisableUnsupportedPlayers();
|
||||
|
||||
|
77
src/yuzu/breakpad.cpp
Normal file
77
src/yuzu/breakpad.cpp
Normal file
@ -0,0 +1,77 @@
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <client/windows/handler/exception_handler.h>
|
||||
#elif defined(__linux__)
|
||||
#include <client/linux/handler/exception_handler.h>
|
||||
#else
|
||||
#error Minidump creation not supported on this platform
|
||||
#endif
|
||||
|
||||
#include "common/fs/fs_paths.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "yuzu/breakpad.h"
|
||||
|
||||
namespace Breakpad {
|
||||
|
||||
static void PruneDumpDirectory(const std::filesystem::path& dump_path) {
|
||||
// Code in this function should be exception-safe.
|
||||
struct Entry {
|
||||
std::filesystem::path path;
|
||||
std::filesystem::file_time_type last_write_time;
|
||||
};
|
||||
std::vector<Entry> existing_dumps;
|
||||
|
||||
// Get existing entries.
|
||||
std::error_code ec;
|
||||
std::filesystem::directory_iterator dir(dump_path, ec);
|
||||
for (auto& entry : dir) {
|
||||
if (entry.is_regular_file()) {
|
||||
existing_dumps.push_back(Entry{
|
||||
.path = entry.path(),
|
||||
.last_write_time = entry.last_write_time(ec),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort descending by creation date.
|
||||
std::ranges::stable_sort(existing_dumps, [](const auto& a, const auto& b) {
|
||||
return a.last_write_time > b.last_write_time;
|
||||
});
|
||||
|
||||
// Delete older dumps.
|
||||
for (size_t i = 5; i < existing_dumps.size(); i++) {
|
||||
std::filesystem::remove(existing_dumps[i].path, ec);
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
[[noreturn]] bool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* context,
|
||||
bool succeeded) {
|
||||
// Prevent time- and space-consuming core dumps from being generated, as we have
|
||||
// already generated a minidump and a core file will not be useful anyway.
|
||||
_exit(1);
|
||||
}
|
||||
#endif
|
||||
|
||||
void InstallCrashHandler() {
|
||||
// Write crash dumps to profile directory.
|
||||
const auto dump_path = GetYuzuPath(Common::FS::YuzuPath::CrashDumpsDir);
|
||||
PruneDumpDirectory(dump_path);
|
||||
|
||||
#if defined(_WIN32)
|
||||
// TODO: If we switch to MinGW builds for Windows, this needs to be wrapped in a C API.
|
||||
static google_breakpad::ExceptionHandler eh{dump_path, nullptr, nullptr, nullptr,
|
||||
google_breakpad::ExceptionHandler::HANDLER_ALL};
|
||||
#elif defined(__linux__)
|
||||
static google_breakpad::MinidumpDescriptor descriptor{dump_path};
|
||||
static google_breakpad::ExceptionHandler eh{descriptor, nullptr, DumpCallback,
|
||||
nullptr, true, -1};
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace Breakpad
|
10
src/yuzu/breakpad.h
Normal file
10
src/yuzu/breakpad.h
Normal file
@ -0,0 +1,10 @@
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Breakpad {
|
||||
|
||||
void InstallCrashHandler();
|
||||
|
||||
}
|
@ -27,16 +27,6 @@ ConfigureDebug::ConfigureDebug(const Core::System& system_, QWidget* parent)
|
||||
|
||||
connect(ui->toggle_gdbstub, &QCheckBox::toggled,
|
||||
[&]() { ui->gdbport_spinbox->setEnabled(ui->toggle_gdbstub->isChecked()); });
|
||||
|
||||
connect(ui->create_crash_dumps, &QCheckBox::stateChanged, [&](int) {
|
||||
if (crash_dump_warning_shown) {
|
||||
return;
|
||||
}
|
||||
QMessageBox::warning(this, tr("Restart Required"),
|
||||
tr("yuzu is required to restart in order to apply this setting."),
|
||||
QMessageBox::Ok, QMessageBox::Ok);
|
||||
crash_dump_warning_shown = true;
|
||||
});
|
||||
}
|
||||
|
||||
ConfigureDebug::~ConfigureDebug() = default;
|
||||
@ -89,13 +79,6 @@ void ConfigureDebug::SetConfiguration() {
|
||||
ui->disable_web_applet->setEnabled(false);
|
||||
ui->disable_web_applet->setText(tr("Web applet not compiled"));
|
||||
#endif
|
||||
|
||||
#ifdef YUZU_DBGHELP
|
||||
ui->create_crash_dumps->setChecked(Settings::values.create_crash_dumps.GetValue());
|
||||
#else
|
||||
ui->create_crash_dumps->setEnabled(false);
|
||||
ui->create_crash_dumps->setText(tr("MiniDump creation not compiled"));
|
||||
#endif
|
||||
}
|
||||
|
||||
void ConfigureDebug::ApplyConfiguration() {
|
||||
@ -107,7 +90,6 @@ void ConfigureDebug::ApplyConfiguration() {
|
||||
Settings::values.enable_fs_access_log = ui->fs_access_log->isChecked();
|
||||
Settings::values.reporting_services = ui->reporting_services->isChecked();
|
||||
Settings::values.dump_audio_commands = ui->dump_audio_commands->isChecked();
|
||||
Settings::values.create_crash_dumps = ui->create_crash_dumps->isChecked();
|
||||
Settings::values.quest_flag = ui->quest_flag->isChecked();
|
||||
Settings::values.use_debug_asserts = ui->use_debug_asserts->isChecked();
|
||||
Settings::values.use_auto_stub = ui->use_auto_stub->isChecked();
|
||||
|
@ -471,13 +471,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="create_crash_dumps">
|
||||
<property name="text">
|
||||
<string>Create Minidump After Crash</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="dump_audio_commands">
|
||||
<property name="toolTip">
|
||||
|
@ -101,13 +101,13 @@ void ConfigureInput::Initialize(InputCommon::InputSubsystem* input_subsystem,
|
||||
ui->tabPlayer5, ui->tabPlayer6, ui->tabPlayer7, ui->tabPlayer8,
|
||||
};
|
||||
|
||||
player_connected = {
|
||||
connected_controller_checkboxes = {
|
||||
ui->checkboxPlayer1Connected, ui->checkboxPlayer2Connected, ui->checkboxPlayer3Connected,
|
||||
ui->checkboxPlayer4Connected, ui->checkboxPlayer5Connected, ui->checkboxPlayer6Connected,
|
||||
ui->checkboxPlayer7Connected, ui->checkboxPlayer8Connected,
|
||||
};
|
||||
|
||||
std::array<QLabel*, 8> player_connected_labels = {
|
||||
std::array<QLabel*, 8> connected_controller_labels = {
|
||||
ui->label, ui->label_3, ui->label_4, ui->label_5,
|
||||
ui->label_6, ui->label_7, ui->label_8, ui->label_9,
|
||||
};
|
||||
@ -115,23 +115,37 @@ void ConfigureInput::Initialize(InputCommon::InputSubsystem* input_subsystem,
|
||||
for (std::size_t i = 0; i < player_tabs.size(); ++i) {
|
||||
player_tabs[i]->setLayout(new QHBoxLayout(player_tabs[i]));
|
||||
player_tabs[i]->layout()->addWidget(player_controllers[i]);
|
||||
connect(player_connected[i], &QCheckBox::clicked, [this, i](int checked) {
|
||||
// Ensures that the controllers are always connected in sequential order
|
||||
this->propagateMouseClickOnPlayers(i, checked, true);
|
||||
connect(player_controllers[i], &ConfigureInputPlayer::Connected, [this, i](bool checked) {
|
||||
// Ensures that connecting a controller changes the number of players
|
||||
if (connected_controller_checkboxes[i]->isChecked() != checked) {
|
||||
// Ensures that the players are always connected in sequential order
|
||||
PropagatePlayerNumberChanged(i, checked);
|
||||
}
|
||||
});
|
||||
connect(connected_controller_checkboxes[i], &QCheckBox::clicked, [this, i](bool checked) {
|
||||
// Reconnect current controller if it was the last one checked
|
||||
// (player number was reduced by more than one)
|
||||
const bool reconnect_first = !checked &&
|
||||
i < connected_controller_checkboxes.size() - 1 &&
|
||||
connected_controller_checkboxes[i + 1]->isChecked();
|
||||
|
||||
// Ensures that the players are always connected in sequential order
|
||||
PropagatePlayerNumberChanged(i, checked, reconnect_first);
|
||||
});
|
||||
connect(player_controllers[i], &ConfigureInputPlayer::RefreshInputDevices, this,
|
||||
&ConfigureInput::UpdateAllInputDevices);
|
||||
connect(player_controllers[i], &ConfigureInputPlayer::RefreshInputProfiles, this,
|
||||
&ConfigureInput::UpdateAllInputProfiles, Qt::QueuedConnection);
|
||||
connect(player_connected[i], &QCheckBox::stateChanged, [this, i](int state) {
|
||||
connect(connected_controller_checkboxes[i], &QCheckBox::stateChanged, [this, i](int state) {
|
||||
// Keep activated controllers synced with the "Connected Controllers" checkboxes
|
||||
player_controllers[i]->ConnectPlayer(state == Qt::Checked);
|
||||
});
|
||||
|
||||
// Remove/hide all the elements that exceed max_players, if applicable.
|
||||
if (i >= max_players) {
|
||||
ui->tabWidget->removeTab(static_cast<int>(max_players));
|
||||
player_connected[i]->hide();
|
||||
player_connected_labels[i]->hide();
|
||||
connected_controller_checkboxes[i]->hide();
|
||||
connected_controller_labels[i]->hide();
|
||||
}
|
||||
}
|
||||
// Only the first player can choose handheld mode so connect the signal just to player 1
|
||||
@ -175,28 +189,25 @@ void ConfigureInput::Initialize(InputCommon::InputSubsystem* input_subsystem,
|
||||
LoadConfiguration();
|
||||
}
|
||||
|
||||
void ConfigureInput::propagateMouseClickOnPlayers(size_t player_index, bool checked, bool origin) {
|
||||
// Origin has already been toggled
|
||||
if (!origin) {
|
||||
player_connected[player_index]->setChecked(checked);
|
||||
}
|
||||
void ConfigureInput::PropagatePlayerNumberChanged(size_t player_index, bool checked,
|
||||
bool reconnect_current) {
|
||||
connected_controller_checkboxes[player_index]->setChecked(checked);
|
||||
|
||||
if (checked) {
|
||||
// Check all previous buttons when checked
|
||||
if (player_index > 0) {
|
||||
propagateMouseClickOnPlayers(player_index - 1, checked, false);
|
||||
PropagatePlayerNumberChanged(player_index - 1, checked);
|
||||
}
|
||||
} else {
|
||||
// Unchecked all following buttons when unchecked
|
||||
if (player_index < player_tabs.size() - 1) {
|
||||
// Reconnect current player if it was the last one checked
|
||||
// (player number was reduced by more than one)
|
||||
if (origin && player_connected[player_index + 1]->checkState() == Qt::Checked) {
|
||||
player_connected[player_index]->setCheckState(Qt::Checked);
|
||||
}
|
||||
propagateMouseClickOnPlayers(player_index + 1, checked, false);
|
||||
if (player_index < connected_controller_checkboxes.size() - 1) {
|
||||
PropagatePlayerNumberChanged(player_index + 1, checked);
|
||||
}
|
||||
}
|
||||
|
||||
if (reconnect_current) {
|
||||
connected_controller_checkboxes[player_index]->setCheckState(Qt::Checked);
|
||||
}
|
||||
}
|
||||
|
||||
QList<QWidget*> ConfigureInput::GetSubTabs() const {
|
||||
@ -249,17 +260,17 @@ void ConfigureInput::LoadConfiguration() {
|
||||
}
|
||||
|
||||
void ConfigureInput::LoadPlayerControllerIndices() {
|
||||
for (std::size_t i = 0; i < player_connected.size(); ++i) {
|
||||
for (std::size_t i = 0; i < connected_controller_checkboxes.size(); ++i) {
|
||||
if (i == 0) {
|
||||
auto* handheld =
|
||||
system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
|
||||
if (handheld->IsConnected()) {
|
||||
player_connected[i]->setChecked(true);
|
||||
connected_controller_checkboxes[i]->setChecked(true);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const auto* controller = system.HIDCore().GetEmulatedControllerByIndex(i);
|
||||
player_connected[i]->setChecked(controller->IsConnected());
|
||||
connected_controller_checkboxes[i]->setChecked(controller->IsConnected());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,9 @@ private:
|
||||
void UpdateDockedState(bool is_handheld);
|
||||
void UpdateAllInputDevices();
|
||||
void UpdateAllInputProfiles(std::size_t player_index);
|
||||
void propagateMouseClickOnPlayers(size_t player_index, bool origin, bool checked);
|
||||
// Enable preceding controllers or disable following ones
|
||||
void PropagatePlayerNumberChanged(size_t player_index, bool checked,
|
||||
bool reconnect_current = false);
|
||||
|
||||
/// Load configuration settings.
|
||||
void LoadConfiguration();
|
||||
@ -71,7 +73,8 @@ private:
|
||||
|
||||
std::array<ConfigureInputPlayer*, 8> player_controllers;
|
||||
std::array<QWidget*, 8> player_tabs;
|
||||
std::array<QCheckBox*, 8> player_connected;
|
||||
// Checkboxes representing the "Connected Controllers".
|
||||
std::array<QCheckBox*, 8> connected_controller_checkboxes;
|
||||
ConfigureInputAdvanced* advanced;
|
||||
|
||||
Core::System& system;
|
||||
|
@ -75,7 +75,7 @@ public:
|
||||
void ClearAll();
|
||||
|
||||
signals:
|
||||
/// Emitted when this controller is connected by the user.
|
||||
/// Emitted when this controller is (dis)connected by the user.
|
||||
void Connected(bool connected);
|
||||
/// Emitted when the Handheld mode is selected (undocked with dual joycons attached).
|
||||
void HandheldStateChanged(bool is_handheld);
|
||||
@ -183,9 +183,6 @@ private:
|
||||
/// Stores a pair of "Connected Controllers" combobox index and Controller Type enum.
|
||||
std::vector<std::pair<int, Core::HID::NpadStyleIndex>> index_controller_type_pairs;
|
||||
|
||||
static constexpr int PLAYER_COUNT = 8;
|
||||
std::array<QCheckBox*, PLAYER_COUNT> player_connected_checkbox;
|
||||
|
||||
/// This will be the the setting function when an input is awaiting configuration.
|
||||
std::optional<std::function<void(const Common::ParamPackage&)>> input_setter;
|
||||
|
||||
|
@ -156,7 +156,6 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
|
||||
// Ui General
|
||||
INSERT(UISettings, select_user_on_boot, "Prompt for user on game boot", "");
|
||||
INSERT(UISettings, pause_when_in_background, "Pause emulation when in background", "");
|
||||
INSERT(UISettings, confirm_before_closing, "Confirm exit while emulation is running", "");
|
||||
INSERT(UISettings, confirm_before_stopping, "Confirm before stopping emulation", "");
|
||||
INSERT(UISettings, hide_mouse, "Hide mouse on inactivity", "");
|
||||
INSERT(UISettings, controller_applet_disabled, "Disable controller applet", "");
|
||||
|
@ -127,7 +127,7 @@ std::vector<std::unique_ptr<WaitTreeItem>> WaitTreeCallstack::GetChildren() cons
|
||||
return list;
|
||||
}
|
||||
|
||||
if (thread.GetOwnerProcess() == nullptr || !thread.GetOwnerProcess()->Is64BitProcess()) {
|
||||
if (thread.GetOwnerProcess() == nullptr || !thread.GetOwnerProcess()->Is64Bit()) {
|
||||
return list;
|
||||
}
|
||||
|
||||
|
@ -380,7 +380,6 @@ void GameList::UnloadController() {
|
||||
|
||||
GameList::~GameList() {
|
||||
UnloadController();
|
||||
emit ShouldCancelWorker();
|
||||
}
|
||||
|
||||
void GameList::SetFilterFocus() {
|
||||
@ -397,6 +396,10 @@ void GameList::ClearFilter() {
|
||||
search_field->clear();
|
||||
}
|
||||
|
||||
void GameList::WorkerEvent() {
|
||||
current_worker->ProcessEvents(this);
|
||||
}
|
||||
|
||||
void GameList::AddDirEntry(GameListDir* entry_items) {
|
||||
item_model->invisibleRootItem()->appendRow(entry_items);
|
||||
tree_view->setExpanded(
|
||||
@ -828,28 +831,21 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
|
||||
tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size);
|
||||
tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time);
|
||||
|
||||
// Before deleting rows, cancel the worker so that it is not using them
|
||||
emit ShouldCancelWorker();
|
||||
// Cancel any existing worker.
|
||||
current_worker.reset();
|
||||
|
||||
// Delete any rows that might already exist if we're repopulating
|
||||
item_model->removeRows(0, item_model->rowCount());
|
||||
search_field->clear();
|
||||
|
||||
GameListWorker* worker =
|
||||
new GameListWorker(vfs, provider, game_dirs, compatibility_list, play_time_manager, system);
|
||||
current_worker = std::make_unique<GameListWorker>(vfs, provider, game_dirs, compatibility_list,
|
||||
play_time_manager, system);
|
||||
|
||||
connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
|
||||
connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry,
|
||||
// Get events from the worker as data becomes available
|
||||
connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameList::WorkerEvent,
|
||||
Qt::QueuedConnection);
|
||||
connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating,
|
||||
Qt::QueuedConnection);
|
||||
// Use DirectConnection here because worker->Cancel() is thread-safe and we want it to
|
||||
// cancel without delay.
|
||||
connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel,
|
||||
Qt::DirectConnection);
|
||||
|
||||
QThreadPool::globalInstance()->start(worker);
|
||||
current_worker = std::move(worker);
|
||||
QThreadPool::globalInstance()->start(current_worker.get());
|
||||
}
|
||||
|
||||
void GameList::SaveInterfaceLayout() {
|
||||
|
@ -109,7 +109,6 @@ signals:
|
||||
void BootGame(const QString& game_path, u64 program_id, std::size_t program_index,
|
||||
StartGameType type, AmLaunchType launch_type);
|
||||
void GameChosen(const QString& game_path, const u64 title_id = 0);
|
||||
void ShouldCancelWorker();
|
||||
void OpenFolderRequested(u64 program_id, GameListOpenTarget target,
|
||||
const std::string& game_path);
|
||||
void OpenTransferableShaderCacheRequested(u64 program_id);
|
||||
@ -138,11 +137,16 @@ private slots:
|
||||
void OnUpdateThemedIcons();
|
||||
|
||||
private:
|
||||
friend class GameListWorker;
|
||||
void WorkerEvent();
|
||||
|
||||
void AddDirEntry(GameListDir* entry_items);
|
||||
void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent);
|
||||
void ValidateEntry(const QModelIndex& item);
|
||||
void DonePopulating(const QStringList& watch_list);
|
||||
|
||||
private:
|
||||
void ValidateEntry(const QModelIndex& item);
|
||||
|
||||
void RefreshGameDirectory();
|
||||
|
||||
void ToggleFavorite(u64 program_id);
|
||||
@ -165,7 +169,7 @@ private:
|
||||
QVBoxLayout* layout = nullptr;
|
||||
QTreeView* tree_view = nullptr;
|
||||
QStandardItemModel* item_model = nullptr;
|
||||
GameListWorker* current_worker = nullptr;
|
||||
std::unique_ptr<GameListWorker> current_worker;
|
||||
QFileSystemWatcher* watcher = nullptr;
|
||||
ControllerNavigation* controller_navigation = nullptr;
|
||||
CompatibilityList compatibility_list;
|
||||
|
@ -233,10 +233,53 @@ GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs_,
|
||||
const PlayTime::PlayTimeManager& play_time_manager_,
|
||||
Core::System& system_)
|
||||
: vfs{std::move(vfs_)}, provider{provider_}, game_dirs{game_dirs_},
|
||||
compatibility_list{compatibility_list_},
|
||||
play_time_manager{play_time_manager_}, system{system_} {}
|
||||
compatibility_list{compatibility_list_}, play_time_manager{play_time_manager_}, system{
|
||||
system_} {
|
||||
// We want the game list to manage our lifetime.
|
||||
setAutoDelete(false);
|
||||
}
|
||||
|
||||
GameListWorker::~GameListWorker() = default;
|
||||
GameListWorker::~GameListWorker() {
|
||||
this->disconnect();
|
||||
stop_requested.store(true);
|
||||
processing_completed.Wait();
|
||||
}
|
||||
|
||||
void GameListWorker::ProcessEvents(GameList* game_list) {
|
||||
while (true) {
|
||||
std::function<void(GameList*)> func;
|
||||
{
|
||||
// Lock queue to protect concurrent modification.
|
||||
std::scoped_lock lk(lock);
|
||||
|
||||
// If we can't pop a function, return.
|
||||
if (queued_events.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pop a function.
|
||||
func = std::move(queued_events.back());
|
||||
queued_events.pop_back();
|
||||
}
|
||||
|
||||
// Run the function.
|
||||
func(game_list);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
void GameListWorker::RecordEvent(F&& func) {
|
||||
{
|
||||
// Lock queue to protect concurrent modification.
|
||||
std::scoped_lock lk(lock);
|
||||
|
||||
// Add the function into the front of the queue.
|
||||
queued_events.emplace_front(std::move(func));
|
||||
}
|
||||
|
||||
// Data now available.
|
||||
emit DataAvailable();
|
||||
}
|
||||
|
||||
void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
|
||||
using namespace FileSys;
|
||||
@ -284,9 +327,9 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
|
||||
GetMetadataFromControlNCA(patch, *control, icon, name);
|
||||
}
|
||||
|
||||
emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader,
|
||||
program_id, compatibility_list, play_time_manager, patch),
|
||||
parent_dir);
|
||||
auto entry = MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader,
|
||||
program_id, compatibility_list, play_time_manager, patch);
|
||||
RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
|
||||
}
|
||||
}
|
||||
|
||||
@ -360,11 +403,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||
const FileSys::PatchManager patch{id, system.GetFileSystemController(),
|
||||
system.GetContentProvider()};
|
||||
|
||||
emit EntryReady(MakeGameListEntry(physical_name, name,
|
||||
Common::FS::GetSize(physical_name), icon,
|
||||
*loader, id, compatibility_list,
|
||||
play_time_manager, patch),
|
||||
parent_dir);
|
||||
auto entry = MakeGameListEntry(
|
||||
physical_name, name, Common::FS::GetSize(physical_name), icon, *loader,
|
||||
id, compatibility_list, play_time_manager, patch);
|
||||
|
||||
RecordEvent(
|
||||
[=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
|
||||
}
|
||||
} else {
|
||||
std::vector<u8> icon;
|
||||
@ -376,11 +420,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||
const FileSys::PatchManager patch{program_id, system.GetFileSystemController(),
|
||||
system.GetContentProvider()};
|
||||
|
||||
emit EntryReady(MakeGameListEntry(physical_name, name,
|
||||
Common::FS::GetSize(physical_name), icon,
|
||||
*loader, program_id, compatibility_list,
|
||||
play_time_manager, patch),
|
||||
parent_dir);
|
||||
auto entry = MakeGameListEntry(
|
||||
physical_name, name, Common::FS::GetSize(physical_name), icon, *loader,
|
||||
program_id, compatibility_list, play_time_manager, patch);
|
||||
|
||||
RecordEvent(
|
||||
[=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
|
||||
}
|
||||
}
|
||||
} else if (is_dir) {
|
||||
@ -399,25 +444,34 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||
}
|
||||
|
||||
void GameListWorker::run() {
|
||||
watch_list.clear();
|
||||
provider->ClearAllEntries();
|
||||
|
||||
const auto DirEntryReady = [&](GameListDir* game_list_dir) {
|
||||
RecordEvent([=](GameList* game_list) { game_list->AddDirEntry(game_list_dir); });
|
||||
};
|
||||
|
||||
for (UISettings::GameDir& game_dir : game_dirs) {
|
||||
if (stop_requested) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (game_dir.path == QStringLiteral("SDMC")) {
|
||||
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir);
|
||||
emit DirEntryReady(game_list_dir);
|
||||
DirEntryReady(game_list_dir);
|
||||
AddTitlesToGameList(game_list_dir);
|
||||
} else if (game_dir.path == QStringLiteral("UserNAND")) {
|
||||
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir);
|
||||
emit DirEntryReady(game_list_dir);
|
||||
DirEntryReady(game_list_dir);
|
||||
AddTitlesToGameList(game_list_dir);
|
||||
} else if (game_dir.path == QStringLiteral("SysNAND")) {
|
||||
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir);
|
||||
emit DirEntryReady(game_list_dir);
|
||||
DirEntryReady(game_list_dir);
|
||||
AddTitlesToGameList(game_list_dir);
|
||||
} else {
|
||||
watch_list.append(game_dir.path);
|
||||
auto* const game_list_dir = new GameListDir(game_dir);
|
||||
emit DirEntryReady(game_list_dir);
|
||||
DirEntryReady(game_list_dir);
|
||||
ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(),
|
||||
game_dir.deep_scan, game_list_dir);
|
||||
ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path.toStdString(),
|
||||
@ -425,12 +479,6 @@ void GameListWorker::run() {
|
||||
}
|
||||
}
|
||||
|
||||
emit Finished(watch_list);
|
||||
RecordEvent([=](GameList* game_list) { game_list->DonePopulating(watch_list); });
|
||||
processing_completed.Set();
|
||||
}
|
||||
|
||||
void GameListWorker::Cancel() {
|
||||
this->disconnect();
|
||||
stop_requested.store(true);
|
||||
processing_completed.Wait();
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
@ -20,6 +21,7 @@ namespace Core {
|
||||
class System;
|
||||
}
|
||||
|
||||
class GameList;
|
||||
class QStandardItem;
|
||||
|
||||
namespace FileSys {
|
||||
@ -46,24 +48,22 @@ public:
|
||||
/// Starts the processing of directory tree information.
|
||||
void run() override;
|
||||
|
||||
/// Tells the worker that it should no longer continue processing. Thread-safe.
|
||||
void Cancel();
|
||||
public:
|
||||
/**
|
||||
* Synchronously processes any events queued by the worker.
|
||||
*
|
||||
* AddDirEntry is called on the game list for every discovered directory.
|
||||
* AddEntry is called on the game list for every discovered program.
|
||||
* DonePopulating is called on the game list when processing completes.
|
||||
*/
|
||||
void ProcessEvents(GameList* game_list);
|
||||
|
||||
signals:
|
||||
/**
|
||||
* The `EntryReady` signal is emitted once an entry has been prepared and is ready
|
||||
* to be added to the game list.
|
||||
* @param entry_items a list with `QStandardItem`s that make up the columns of the new
|
||||
* entry.
|
||||
*/
|
||||
void DirEntryReady(GameListDir* entry_items);
|
||||
void EntryReady(QList<QStandardItem*> entry_items, GameListDir* parent_dir);
|
||||
void DataAvailable();
|
||||
|
||||
/**
|
||||
* After the worker has traversed the game directory looking for entries, this signal is
|
||||
* emitted with a list of folders that should be watched for changes as well.
|
||||
*/
|
||||
void Finished(QStringList watch_list);
|
||||
private:
|
||||
template <typename F>
|
||||
void RecordEvent(F&& func);
|
||||
|
||||
private:
|
||||
void AddTitlesToGameList(GameListDir* parent_dir);
|
||||
@ -84,8 +84,11 @@ private:
|
||||
|
||||
QStringList watch_list;
|
||||
|
||||
Common::Event processing_completed;
|
||||
std::mutex lock;
|
||||
std::condition_variable cv;
|
||||
std::deque<std::function<void(GameList*)>> queued_events;
|
||||
std::atomic_bool stop_requested = false;
|
||||
Common::Event processing_completed;
|
||||
|
||||
Core::System& system;
|
||||
};
|
||||
|
@ -159,8 +159,8 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
|
||||
#include "yuzu/util/clickable_label.h"
|
||||
#include "yuzu/vk_device_info.h"
|
||||
|
||||
#ifdef YUZU_DBGHELP
|
||||
#include "yuzu/mini_dump.h"
|
||||
#ifdef YUZU_CRASH_DUMPS
|
||||
#include "yuzu/breakpad.h"
|
||||
#endif
|
||||
|
||||
using namespace Common::Literals;
|
||||
@ -1072,7 +1072,7 @@ void GMainWindow::InitializeWidgets() {
|
||||
});
|
||||
volume_popup->layout()->addWidget(volume_slider);
|
||||
|
||||
volume_button = new QPushButton();
|
||||
volume_button = new VolumeButton();
|
||||
volume_button->setObjectName(QStringLiteral("TogglableStatusBarButton"));
|
||||
volume_button->setFocusPolicy(Qt::NoFocus);
|
||||
volume_button->setCheckable(true);
|
||||
@ -1103,6 +1103,8 @@ void GMainWindow::InitializeWidgets() {
|
||||
context_menu.exec(volume_button->mapToGlobal(menu_location));
|
||||
volume_button->repaint();
|
||||
});
|
||||
connect(volume_button, &VolumeButton::VolumeChanged, this, &GMainWindow::UpdateVolumeUI);
|
||||
|
||||
statusBar()->insertPermanentWidget(0, volume_button);
|
||||
|
||||
// setup AA button
|
||||
@ -2019,7 +2021,7 @@ void GMainWindow::BootGame(const QString& filename, u64 program_id, std::size_t
|
||||
std::filesystem::path{Common::U16StringFromBuffer(filename.utf16(), filename.size())}
|
||||
.filename());
|
||||
}
|
||||
const bool is_64bit = system->Kernel().ApplicationProcess()->Is64BitProcess();
|
||||
const bool is_64bit = system->Kernel().ApplicationProcess()->Is64Bit();
|
||||
const auto instruction_set_suffix = is_64bit ? tr("(64-bit)") : tr("(32-bit)");
|
||||
title_name = tr("%1 %2", "%1 is the title name. %2 indicates if the title is 64-bit or 32-bit")
|
||||
.arg(QString::fromStdString(title_name), instruction_set_suffix)
|
||||
@ -2172,6 +2174,7 @@ void GMainWindow::ShutdownGame() {
|
||||
return;
|
||||
}
|
||||
|
||||
play_time_manager->Stop();
|
||||
OnShutdownBegin();
|
||||
OnEmulationStopTimeExpired();
|
||||
OnEmulationStopped();
|
||||
@ -2735,7 +2738,7 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa
|
||||
return;
|
||||
}
|
||||
|
||||
const auto extracted = FileSys::ExtractRomFS(romfs, FileSys::RomFSExtractionType::Full);
|
||||
const auto extracted = FileSys::ExtractRomFS(romfs);
|
||||
if (extracted == nullptr) {
|
||||
failed();
|
||||
return;
|
||||
@ -2980,7 +2983,7 @@ bool GMainWindow::MakeShortcutIcoPath(const u64 program_id, const std::string_vi
|
||||
#elif defined(__linux__) || defined(__FreeBSD__)
|
||||
out_icon_path = Common::FS::GetDataDirectory("XDG_DATA_HOME") / "icons/hicolor/256x256";
|
||||
#endif
|
||||
|
||||
|
||||
// Create icons directory if it doesn't exist
|
||||
if (!Common::FS::CreateDirs(out_icon_path)) {
|
||||
QMessageBox::critical(
|
||||
@ -3571,7 +3574,7 @@ void GMainWindow::OnExecuteProgram(std::size_t program_index) {
|
||||
}
|
||||
|
||||
void GMainWindow::OnExit() {
|
||||
OnStopGame();
|
||||
ShutdownGame();
|
||||
}
|
||||
|
||||
void GMainWindow::OnSaveConfig() {
|
||||
@ -4087,6 +4090,66 @@ void GMainWindow::OpenPerGameConfiguration(u64 title_id, const std::string& file
|
||||
}
|
||||
}
|
||||
|
||||
bool GMainWindow::CreateShortcut(const std::string& shortcut_path, const std::string& title,
|
||||
const std::string& comment, const std::string& icon_path,
|
||||
const std::string& command, const std::string& arguments,
|
||||
const std::string& categories, const std::string& keywords) {
|
||||
#if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__)
|
||||
// This desktop file template was writing referencing
|
||||
// https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html
|
||||
std::string shortcut_contents{};
|
||||
shortcut_contents.append("[Desktop Entry]\n");
|
||||
shortcut_contents.append("Type=Application\n");
|
||||
shortcut_contents.append("Version=1.0\n");
|
||||
shortcut_contents.append(fmt::format("Name={:s}\n", title));
|
||||
shortcut_contents.append(fmt::format("Comment={:s}\n", comment));
|
||||
shortcut_contents.append(fmt::format("Icon={:s}\n", icon_path));
|
||||
shortcut_contents.append(fmt::format("TryExec={:s}\n", command));
|
||||
shortcut_contents.append(fmt::format("Exec={:s} {:s}\n", command, arguments));
|
||||
shortcut_contents.append(fmt::format("Categories={:s}\n", categories));
|
||||
shortcut_contents.append(fmt::format("Keywords={:s}\n", keywords));
|
||||
|
||||
std::ofstream shortcut_stream(shortcut_path);
|
||||
if (!shortcut_stream.is_open()) {
|
||||
LOG_WARNING(Common, "Failed to create file {:s}", shortcut_path);
|
||||
return false;
|
||||
}
|
||||
shortcut_stream << shortcut_contents;
|
||||
shortcut_stream.close();
|
||||
|
||||
return true;
|
||||
#elif defined(WIN32)
|
||||
IShellLinkW* shell_link;
|
||||
auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW,
|
||||
(void**)&shell_link);
|
||||
if (FAILED(hres)) {
|
||||
return false;
|
||||
}
|
||||
shell_link->SetPath(
|
||||
Common::UTF8ToUTF16W(command).data()); // Path to the object we are referring to
|
||||
shell_link->SetArguments(Common::UTF8ToUTF16W(arguments).data());
|
||||
shell_link->SetDescription(Common::UTF8ToUTF16W(comment).data());
|
||||
shell_link->SetIconLocation(Common::UTF8ToUTF16W(icon_path).data(), 0);
|
||||
|
||||
IPersistFile* persist_file;
|
||||
hres = shell_link->QueryInterface(IID_IPersistFile, (void**)&persist_file);
|
||||
if (FAILED(hres)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hres = persist_file->Save(Common::UTF8ToUTF16W(shortcut_path).data(), TRUE);
|
||||
if (FAILED(hres)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
persist_file->Release();
|
||||
shell_link->Release();
|
||||
|
||||
return true;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
void GMainWindow::OnLoadAmiibo() {
|
||||
if (emu_thread == nullptr || !emu_thread->IsRunning()) {
|
||||
return;
|
||||
@ -4873,7 +4936,12 @@ bool GMainWindow::SelectRomFSDumpTarget(const FileSys::ContentProvider& installe
|
||||
}
|
||||
|
||||
bool GMainWindow::ConfirmClose() {
|
||||
if (emu_thread == nullptr || !UISettings::values.confirm_before_closing) {
|
||||
if (emu_thread == nullptr ||
|
||||
UISettings::values.confirm_before_stopping.GetValue() == ConfirmStop::Ask_Never) {
|
||||
return true;
|
||||
}
|
||||
if (!system->GetExitLocked() &&
|
||||
UISettings::values.confirm_before_stopping.GetValue() == ConfirmStop::Ask_Based_On_Game) {
|
||||
return true;
|
||||
}
|
||||
const auto text = tr("Are you sure you want to close yuzu?");
|
||||
@ -4978,7 +5046,7 @@ bool GMainWindow::ConfirmChangeGame() {
|
||||
}
|
||||
|
||||
bool GMainWindow::ConfirmForceLockedExit() {
|
||||
if (emu_thread == nullptr || !UISettings::values.confirm_before_closing) {
|
||||
if (emu_thread == nullptr) {
|
||||
return true;
|
||||
}
|
||||
const auto text = tr("The currently running application has requested yuzu to not exit.\n\n"
|
||||
@ -5154,6 +5222,32 @@ void GMainWindow::changeEvent(QEvent* event) {
|
||||
QWidget::changeEvent(event);
|
||||
}
|
||||
|
||||
void VolumeButton::wheelEvent(QWheelEvent* event) {
|
||||
|
||||
int num_degrees = event->angleDelta().y() / 8;
|
||||
int num_steps = (num_degrees / 15) * scroll_multiplier;
|
||||
// Stated in QT docs: Most mouse types work in steps of 15 degrees, in which case the delta
|
||||
// value is a multiple of 120; i.e., 120 units * 1/8 = 15 degrees.
|
||||
|
||||
if (num_steps > 0) {
|
||||
Settings::values.volume.SetValue(
|
||||
std::min(200, Settings::values.volume.GetValue() + num_steps));
|
||||
} else {
|
||||
Settings::values.volume.SetValue(
|
||||
std::max(0, Settings::values.volume.GetValue() + num_steps));
|
||||
}
|
||||
|
||||
scroll_multiplier = std::min(MaxMultiplier, scroll_multiplier * 2);
|
||||
scroll_timer.start(100); // reset the multiplier if no scroll event occurs within 100 ms
|
||||
|
||||
emit VolumeChanged();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void VolumeButton::ResetMultiplier() {
|
||||
scroll_multiplier = 1;
|
||||
}
|
||||
|
||||
#ifdef main
|
||||
#undef main
|
||||
#endif
|
||||
@ -5215,22 +5309,15 @@ int main(int argc, char* argv[]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef YUZU_DBGHELP
|
||||
PROCESS_INFORMATION pi;
|
||||
if (!is_child && Settings::values.create_crash_dumps.GetValue() &&
|
||||
MiniDump::SpawnDebuggee(argv[0], pi)) {
|
||||
// Delete the config object so that it doesn't save when the program exits
|
||||
config.reset(nullptr);
|
||||
MiniDump::DebugDebuggee(pi);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (StartupChecks(argv[0], &has_broken_vulkan,
|
||||
Settings::values.perform_vulkan_check.GetValue())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef YUZU_CRASH_DUMPS
|
||||
Breakpad::InstallCrashHandler();
|
||||
#endif
|
||||
|
||||
Common::DetachedTasks detached_tasks;
|
||||
MicroProfileOnThreadCreate("Frontend");
|
||||
SCOPE_EXIT({ MicroProfileShutdown(); });
|
||||
|
@ -9,6 +9,7 @@
|
||||
#include <filesystem>
|
||||
#include <QMainWindow>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <QTranslator>
|
||||
|
||||
@ -138,6 +139,28 @@ namespace VkDeviceInfo {
|
||||
class Record;
|
||||
}
|
||||
|
||||
class VolumeButton : public QPushButton {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit VolumeButton(QWidget* parent = nullptr) : QPushButton(parent), scroll_multiplier(1) {
|
||||
connect(&scroll_timer, &QTimer::timeout, this, &VolumeButton::ResetMultiplier);
|
||||
}
|
||||
|
||||
signals:
|
||||
void VolumeChanged();
|
||||
|
||||
protected:
|
||||
void wheelEvent(QWheelEvent* event) override;
|
||||
|
||||
private slots:
|
||||
void ResetMultiplier();
|
||||
|
||||
private:
|
||||
int scroll_multiplier;
|
||||
QTimer scroll_timer;
|
||||
constexpr static int MaxMultiplier = 8;
|
||||
};
|
||||
|
||||
class GMainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
|
||||
@ -492,7 +515,7 @@ private:
|
||||
QPushButton* dock_status_button = nullptr;
|
||||
QPushButton* filter_status_button = nullptr;
|
||||
QPushButton* aa_status_button = nullptr;
|
||||
QPushButton* volume_button = nullptr;
|
||||
VolumeButton* volume_button = nullptr;
|
||||
QWidget* volume_popup = nullptr;
|
||||
QSlider* volume_slider = nullptr;
|
||||
QTimer status_bar_update_timer;
|
||||
|
@ -1,202 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <fmt/format.h>
|
||||
#include <windows.h>
|
||||
#include "yuzu/mini_dump.h"
|
||||
#include "yuzu/startup_checks.h"
|
||||
|
||||
// dbghelp.h must be included after windows.h
|
||||
#include <dbghelp.h>
|
||||
|
||||
namespace MiniDump {
|
||||
|
||||
void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_INFORMATION* info,
|
||||
EXCEPTION_POINTERS* pep) {
|
||||
char file_name[255];
|
||||
const std::time_t the_time = std::time(nullptr);
|
||||
std::strftime(file_name, 255, "yuzu-crash-%Y%m%d%H%M%S.dmp", std::localtime(&the_time));
|
||||
|
||||
// Open the file
|
||||
HANDLE file_handle = CreateFileA(file_name, GENERIC_READ | GENERIC_WRITE, 0, nullptr,
|
||||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
|
||||
if (file_handle == nullptr || file_handle == INVALID_HANDLE_VALUE) {
|
||||
fmt::print(stderr, "CreateFileA failed. Error: {}", GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the minidump
|
||||
const MINIDUMP_TYPE dump_type = MiniDumpNormal;
|
||||
|
||||
const bool write_dump_status = MiniDumpWriteDump(process_handle, process_id, file_handle,
|
||||
dump_type, (pep != 0) ? info : 0, 0, 0);
|
||||
|
||||
if (write_dump_status) {
|
||||
fmt::print(stderr, "MiniDump created: {}", file_name);
|
||||
} else {
|
||||
fmt::print(stderr, "MiniDumpWriteDump failed. Error: {}", GetLastError());
|
||||
}
|
||||
|
||||
// Close the file
|
||||
CloseHandle(file_handle);
|
||||
}
|
||||
|
||||
void DumpFromDebugEvent(DEBUG_EVENT& deb_ev, PROCESS_INFORMATION& pi) {
|
||||
EXCEPTION_RECORD& record = deb_ev.u.Exception.ExceptionRecord;
|
||||
|
||||
HANDLE thread_handle = OpenThread(THREAD_GET_CONTEXT, false, deb_ev.dwThreadId);
|
||||
if (thread_handle == nullptr) {
|
||||
fmt::print(stderr, "OpenThread failed ({})", GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
// Get child process context
|
||||
CONTEXT context = {};
|
||||
context.ContextFlags = CONTEXT_ALL;
|
||||
if (!GetThreadContext(thread_handle, &context)) {
|
||||
fmt::print(stderr, "GetThreadContext failed ({})", GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
// Create exception pointers for minidump
|
||||
EXCEPTION_POINTERS ep;
|
||||
ep.ExceptionRecord = &record;
|
||||
ep.ContextRecord = &context;
|
||||
|
||||
MINIDUMP_EXCEPTION_INFORMATION info;
|
||||
info.ThreadId = deb_ev.dwThreadId;
|
||||
info.ExceptionPointers = &ep;
|
||||
info.ClientPointers = false;
|
||||
|
||||
CreateMiniDump(pi.hProcess, pi.dwProcessId, &info, &ep);
|
||||
|
||||
if (CloseHandle(thread_handle) == 0) {
|
||||
fmt::print(stderr, "error: CloseHandle(thread_handle) failed ({})", GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
bool SpawnDebuggee(const char* arg0, PROCESS_INFORMATION& pi) {
|
||||
std::memset(&pi, 0, sizeof(pi));
|
||||
|
||||
// Don't debug if we are already being debugged
|
||||
if (IsDebuggerPresent()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SpawnChild(arg0, &pi, 0)) {
|
||||
fmt::print(stderr, "warning: continuing without crash dumps");
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool can_debug = DebugActiveProcess(pi.dwProcessId);
|
||||
if (!can_debug) {
|
||||
fmt::print(stderr,
|
||||
"warning: DebugActiveProcess failed ({}), continuing without crash dumps",
|
||||
GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char* ExceptionName(DWORD exception) {
|
||||
switch (exception) {
|
||||
case EXCEPTION_ACCESS_VIOLATION:
|
||||
return "EXCEPTION_ACCESS_VIOLATION";
|
||||
case EXCEPTION_DATATYPE_MISALIGNMENT:
|
||||
return "EXCEPTION_DATATYPE_MISALIGNMENT";
|
||||
case EXCEPTION_BREAKPOINT:
|
||||
return "EXCEPTION_BREAKPOINT";
|
||||
case EXCEPTION_SINGLE_STEP:
|
||||
return "EXCEPTION_SINGLE_STEP";
|
||||
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
|
||||
return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED";
|
||||
case EXCEPTION_FLT_DENORMAL_OPERAND:
|
||||
return "EXCEPTION_FLT_DENORMAL_OPERAND";
|
||||
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_FLT_INEXACT_RESULT:
|
||||
return "EXCEPTION_FLT_INEXACT_RESULT";
|
||||
case EXCEPTION_FLT_INVALID_OPERATION:
|
||||
return "EXCEPTION_FLT_INVALID_OPERATION";
|
||||
case EXCEPTION_FLT_OVERFLOW:
|
||||
return "EXCEPTION_FLT_OVERFLOW";
|
||||
case EXCEPTION_FLT_STACK_CHECK:
|
||||
return "EXCEPTION_FLT_STACK_CHECK";
|
||||
case EXCEPTION_FLT_UNDERFLOW:
|
||||
return "EXCEPTION_FLT_UNDERFLOW";
|
||||
case EXCEPTION_INT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_INT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_INT_OVERFLOW:
|
||||
return "EXCEPTION_INT_OVERFLOW";
|
||||
case EXCEPTION_PRIV_INSTRUCTION:
|
||||
return "EXCEPTION_PRIV_INSTRUCTION";
|
||||
case EXCEPTION_IN_PAGE_ERROR:
|
||||
return "EXCEPTION_IN_PAGE_ERROR";
|
||||
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||
return "EXCEPTION_ILLEGAL_INSTRUCTION";
|
||||
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
|
||||
return "EXCEPTION_NONCONTINUABLE_EXCEPTION";
|
||||
case EXCEPTION_STACK_OVERFLOW:
|
||||
return "EXCEPTION_STACK_OVERFLOW";
|
||||
case EXCEPTION_INVALID_DISPOSITION:
|
||||
return "EXCEPTION_INVALID_DISPOSITION";
|
||||
case EXCEPTION_GUARD_PAGE:
|
||||
return "EXCEPTION_GUARD_PAGE";
|
||||
case EXCEPTION_INVALID_HANDLE:
|
||||
return "EXCEPTION_INVALID_HANDLE";
|
||||
default:
|
||||
return "unknown exception type";
|
||||
}
|
||||
}
|
||||
|
||||
void DebugDebuggee(PROCESS_INFORMATION& pi) {
|
||||
DEBUG_EVENT deb_ev = {};
|
||||
|
||||
while (deb_ev.dwDebugEventCode != EXIT_PROCESS_DEBUG_EVENT) {
|
||||
const bool wait_success = WaitForDebugEvent(&deb_ev, INFINITE);
|
||||
if (!wait_success) {
|
||||
fmt::print(stderr, "error: WaitForDebugEvent failed ({})", GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
switch (deb_ev.dwDebugEventCode) {
|
||||
case OUTPUT_DEBUG_STRING_EVENT:
|
||||
case CREATE_PROCESS_DEBUG_EVENT:
|
||||
case CREATE_THREAD_DEBUG_EVENT:
|
||||
case EXIT_PROCESS_DEBUG_EVENT:
|
||||
case EXIT_THREAD_DEBUG_EVENT:
|
||||
case LOAD_DLL_DEBUG_EVENT:
|
||||
case RIP_EVENT:
|
||||
case UNLOAD_DLL_DEBUG_EVENT:
|
||||
// Continue on all other debug events
|
||||
ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_CONTINUE);
|
||||
break;
|
||||
case EXCEPTION_DEBUG_EVENT:
|
||||
EXCEPTION_RECORD& record = deb_ev.u.Exception.ExceptionRecord;
|
||||
|
||||
// We want to generate a crash dump if we are seeing the same exception again.
|
||||
if (!deb_ev.u.Exception.dwFirstChance) {
|
||||
fmt::print(stderr, "Creating MiniDump on ExceptionCode: 0x{:08x} {}\n",
|
||||
record.ExceptionCode, ExceptionName(record.ExceptionCode));
|
||||
DumpFromDebugEvent(deb_ev, pi);
|
||||
}
|
||||
|
||||
// Continue without handling the exception.
|
||||
// Lets the debuggee use its own exception handler.
|
||||
// - If one does not exist, we will see the exception once more where we make a minidump
|
||||
// for. Then when it reaches here again, yuzu will probably crash.
|
||||
// - DBG_CONTINUE on an exception that the debuggee does not handle can set us up for an
|
||||
// infinite loop of exceptions.
|
||||
ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_EXCEPTION_NOT_HANDLED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace MiniDump
|
@ -1,19 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <dbghelp.h>
|
||||
|
||||
namespace MiniDump {
|
||||
|
||||
void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_INFORMATION* info,
|
||||
EXCEPTION_POINTERS* pep);
|
||||
|
||||
void DumpFromDebugEvent(DEBUG_EVENT& deb_ev, PROCESS_INFORMATION& pi);
|
||||
bool SpawnDebuggee(const char* arg0, PROCESS_INFORMATION& pi);
|
||||
void DebugDebuggee(PROCESS_INFORMATION& pi);
|
||||
|
||||
} // namespace MiniDump
|
@ -93,10 +93,6 @@ struct Values {
|
||||
Setting<bool> show_filter_bar{linkage, true, "showFilterBar", Category::Ui};
|
||||
Setting<bool> show_status_bar{linkage, true, "showStatusBar", Category::Ui};
|
||||
|
||||
Setting<bool> confirm_before_closing{
|
||||
linkage, true, "confirmClose", Category::UiGeneral, Settings::Specialization::Default,
|
||||
true, true};
|
||||
|
||||
SwitchableSetting<ConfirmStop> confirm_before_stopping{linkage,
|
||||
ConfirmStop::Ask_Always,
|
||||
"confirmStop",
|
||||
|
Reference in New Issue
Block a user