mirror of
https://git.eden-emu.dev/archive/citron
synced 2026-03-23 01:56:08 -04:00
Merge pull request 'fix(ui): Improper Overlay Shutdown & Zombie Processes' (#89) from fix/zombie-overlays into main
Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/89
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
@@ -23,8 +24,16 @@ PlayerControlPreview::PlayerControlPreview(QWidget* parent) : QFrame(parent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PlayerControlPreview::~PlayerControlPreview() {
|
PlayerControlPreview::~PlayerControlPreview() {
|
||||||
|
// If the app is closing down, the "controller" memory is likely garbage.
|
||||||
|
// We must NOT touch it or call UnloadController().
|
||||||
|
if (QCoreApplication::closingDown()) {
|
||||||
|
controller = nullptr;
|
||||||
|
is_controller_set = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
UnloadController();
|
UnloadController();
|
||||||
};
|
}
|
||||||
|
|
||||||
void PlayerControlPreview::SetRawJoystickVisible(bool visible) {
|
void PlayerControlPreview::SetRawJoystickVisible(bool visible) {
|
||||||
raw_joystick_visible = visible;
|
raw_joystick_visible = visible;
|
||||||
@@ -51,14 +60,22 @@ void PlayerControlPreview::SetController(Core::HID::EmulatedController* controll
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PlayerControlPreview::UnloadController() {
|
void PlayerControlPreview::UnloadController() {
|
||||||
// Only try to access the controller if the pointer is valid.
|
// If the app is closing or the key is invalid, just reset pointers and quit.
|
||||||
|
if (QCoreApplication::closingDown() || callback_key < 0) {
|
||||||
|
is_controller_set = false;
|
||||||
|
controller = nullptr;
|
||||||
|
callback_key = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (controller) {
|
if (controller) {
|
||||||
controller->DeleteCallback(callback_key);
|
controller->DeleteCallback(callback_key);
|
||||||
// Also clear the destruction callback we set.
|
|
||||||
controller->SetDestructionCallback(nullptr);
|
controller->SetDestructionCallback(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
is_controller_set = false;
|
is_controller_set = false;
|
||||||
controller = nullptr;
|
controller = nullptr;
|
||||||
|
callback_key = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayerControlPreview::BeginMappingButton(std::size_t button_id) {
|
void PlayerControlPreview::BeginMappingButton(std::size_t button_id) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "hid_core/hid_core.h"
|
#include "hid_core/hid_core.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
@@ -75,10 +76,19 @@ ControllerOverlay::ControllerOverlay(GMainWindow* parent)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ControllerOverlay::~ControllerOverlay() = default;
|
ControllerOverlay::~ControllerOverlay() {
|
||||||
|
update_timer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
void ControllerOverlay::UpdateControllerState() {
|
void ControllerOverlay::UpdateControllerState() {
|
||||||
if (!main_window || !is_enabled) return;
|
// If we're shutting down, kill the timer and hide.
|
||||||
|
if (QCoreApplication::closingDown() || !main_window || main_window->isHidden()) {
|
||||||
|
update_timer.stop();
|
||||||
|
if (!this->isHidden()) this->hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_enabled) return;
|
||||||
|
|
||||||
if (UISettings::IsGamescope()) {
|
if (UISettings::IsGamescope()) {
|
||||||
bool ui_active = false;
|
bool ui_active = false;
|
||||||
|
|||||||
@@ -343,8 +343,10 @@ bool GMainWindow::CheckDarkMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulkan)
|
GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulkan)
|
||||||
: ui{std::make_unique<Ui::MainWindow>()}, system{std::make_unique<Core::System>()},
|
: system{std::make_unique<Core::System>()},
|
||||||
input_subsystem{std::make_shared<InputCommon::InputSubsystem>()}, config{std::move(config_)},
|
input_subsystem{std::make_shared<InputCommon::InputSubsystem>()},
|
||||||
|
ui{std::make_unique<Ui::MainWindow>()},
|
||||||
|
config{std::move(config_)},
|
||||||
vfs{std::make_shared<FileSys::RealVfsFilesystem>()},
|
vfs{std::make_shared<FileSys::RealVfsFilesystem>()},
|
||||||
provider{std::make_unique<FileSys::ManualContentProvider>()} {
|
provider{std::make_unique<FileSys::ManualContentProvider>()} {
|
||||||
#ifdef __unix__
|
#ifdef __unix__
|
||||||
@@ -5793,27 +5795,42 @@ void GMainWindow::closeEvent(QCloseEvent* event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This stops mirroring threads before we start saving configs.
|
// 1. STOP the emulation first
|
||||||
if (emu_thread != nullptr) {
|
if (emu_thread != nullptr) {
|
||||||
ShutdownGame();
|
ShutdownGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now save settings
|
// 2. FORCE the UI to stop talking to controllers
|
||||||
|
// Do this BEFORE UnloadInputDevices
|
||||||
|
if (controller_overlay) {
|
||||||
|
// We delete it here so its destructor runs while 'system' is still healthy
|
||||||
|
delete controller_overlay;
|
||||||
|
controller_overlay = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game_list) {
|
||||||
|
game_list->UnloadController();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller_dialog) {
|
||||||
|
controller_dialog->UnloadController();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Save settings
|
||||||
UpdateUISettings();
|
UpdateUISettings();
|
||||||
config->SaveAllValues();
|
config->SaveAllValues();
|
||||||
|
|
||||||
game_list->SaveInterfaceLayout();
|
game_list->SaveInterfaceLayout();
|
||||||
UISettings::SaveWindowState();
|
UISettings::SaveWindowState();
|
||||||
hotkey_registry.SaveHotkeys();
|
hotkey_registry.SaveHotkeys();
|
||||||
|
|
||||||
// Unload controllers
|
// 4. NOW it is safe to kill the hardware devices
|
||||||
controller_dialog->UnloadController();
|
|
||||||
game_list->UnloadController();
|
|
||||||
|
|
||||||
render_window->close();
|
render_window->close();
|
||||||
multiplayer_state->Close();
|
multiplayer_state->Close();
|
||||||
system->HIDCore().UnloadInputDevices();
|
|
||||||
system->GetRoomNetwork().Shutdown();
|
if (system) {
|
||||||
|
system->HIDCore().UnloadInputDevices();
|
||||||
|
system->GetRoomNetwork().Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
QWidget::closeEvent(event);
|
QWidget::closeEvent(event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -334,11 +334,11 @@ private:
|
|||||||
bool MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, std::filesystem::path& out_icon_path);
|
bool MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, std::filesystem::path& out_icon_path);
|
||||||
bool CreateShortcutLink(const std::filesystem::path& shortcut_path, const std::string& comment, const std::filesystem::path& icon_path, const std::filesystem::path& command, const std::string& arguments, const std::string& categories, const std::string& keywords, const std::string& name);
|
bool CreateShortcutLink(const std::filesystem::path& shortcut_path, const std::string& comment, const std::filesystem::path& icon_path, const std::filesystem::path& command, const std::string& arguments, const std::string& categories, const std::string& keywords, const std::string& name);
|
||||||
bool question(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No), QMessageBox::StandardButton defaultButton = QMessageBox::NoButton);
|
bool question(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No), QMessageBox::StandardButton defaultButton = QMessageBox::NoButton);
|
||||||
std::unique_ptr<Ui::MainWindow> ui;
|
|
||||||
std::unique_ptr<Core::System> system;
|
std::unique_ptr<Core::System> system;
|
||||||
|
std::shared_ptr<InputCommon::InputSubsystem> input_subsystem;
|
||||||
|
std::unique_ptr<Ui::MainWindow> ui;
|
||||||
std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;
|
std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;
|
||||||
std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;
|
std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;
|
||||||
std::shared_ptr<InputCommon::InputSubsystem> input_subsystem;
|
|
||||||
MultiplayerState* multiplayer_state = nullptr;
|
MultiplayerState* multiplayer_state = nullptr;
|
||||||
GRenderWindow* render_window;
|
GRenderWindow* render_window;
|
||||||
GameList* game_list;
|
GameList* game_list;
|
||||||
|
|||||||
@@ -2,20 +2,33 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
#include "common/settings_input.h"
|
#include "common/settings_input.h"
|
||||||
#include "hid_core/frontend/emulated_controller.h"
|
#include "hid_core/frontend/emulated_controller.h"
|
||||||
#include "hid_core/hid_core.h"
|
#include "hid_core/hid_core.h"
|
||||||
#include "citron/util/controller_navigation.h"
|
#include "citron/util/controller_navigation.h"
|
||||||
|
|
||||||
ControllerNavigation::ControllerNavigation(Core::HID::HIDCore& hid_core, QWidget* parent) {
|
ControllerNavigation::ControllerNavigation(Core::HID::HIDCore& hid_core, QWidget* parent) {
|
||||||
|
// Initialize keys to -1 immediately
|
||||||
|
player1_callback_key = -1;
|
||||||
|
handheld_callback_key = -1;
|
||||||
|
is_controller_set = false;
|
||||||
|
|
||||||
player1_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
|
player1_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
|
||||||
handheld_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
|
handheld_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
|
||||||
|
|
||||||
Core::HID::ControllerUpdateCallback engine_callback{
|
Core::HID::ControllerUpdateCallback engine_callback{
|
||||||
.on_change = [this](Core::HID::ControllerTriggerType type) { ControllerUpdateEvent(type); },
|
.on_change = [this](Core::HID::ControllerTriggerType type) { ControllerUpdateEvent(type); },
|
||||||
.is_npad_service = false,
|
.is_npad_service = false,
|
||||||
};
|
};
|
||||||
player1_callback_key = player1_controller->SetCallback(engine_callback);
|
|
||||||
handheld_callback_key = handheld_controller->SetCallback(engine_callback);
|
if (player1_controller) {
|
||||||
|
player1_callback_key = player1_controller->SetCallback(engine_callback);
|
||||||
|
}
|
||||||
|
if (handheld_controller) {
|
||||||
|
handheld_callback_key = handheld_controller->SetCallback(engine_callback);
|
||||||
|
}
|
||||||
|
|
||||||
is_controller_set = true;
|
is_controller_set = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,9 +37,27 @@ ControllerNavigation::~ControllerNavigation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ControllerNavigation::UnloadController() {
|
void ControllerNavigation::UnloadController() {
|
||||||
|
// 1. If the app is already exiting, the controller memory is GONE.
|
||||||
|
// Touching the pointers will crash. Just reset and leave.
|
||||||
|
if (QCoreApplication::closingDown()) {
|
||||||
|
is_controller_set = false;
|
||||||
|
player1_controller = nullptr;
|
||||||
|
handheld_controller = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (is_controller_set) {
|
if (is_controller_set) {
|
||||||
player1_controller->DeleteCallback(player1_callback_key);
|
// 2. Only delete if the pointer exists AND the key is valid (>= 0)
|
||||||
handheld_controller->DeleteCallback(handheld_callback_key);
|
if (player1_controller && player1_callback_key >= 0) {
|
||||||
|
player1_controller->DeleteCallback(player1_callback_key);
|
||||||
|
player1_callback_key = -1; // Prevent second deletion
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handheld_controller && handheld_callback_key >= 0) {
|
||||||
|
handheld_controller->DeleteCallback(handheld_callback_key);
|
||||||
|
handheld_callback_key = -1; // Prevent second deletion
|
||||||
|
}
|
||||||
|
|
||||||
is_controller_set = false;
|
is_controller_set = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
@@ -90,7 +91,9 @@ PerformanceOverlay::PerformanceOverlay(QWidget* parent) : QWidget(UISettings::Is
|
|||||||
UpdatePosition();
|
UpdatePosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
PerformanceOverlay::~PerformanceOverlay() = default;
|
PerformanceOverlay::~PerformanceOverlay() {
|
||||||
|
update_timer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
void PerformanceOverlay::SetVisible(bool visible) {
|
void PerformanceOverlay::SetVisible(bool visible) {
|
||||||
is_enabled = visible;
|
is_enabled = visible;
|
||||||
@@ -167,7 +170,14 @@ void PerformanceOverlay::mouseReleaseEvent(QMouseEvent* event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PerformanceOverlay::UpdatePerformanceStats() {
|
void PerformanceOverlay::UpdatePerformanceStats() {
|
||||||
if (!main_window || !is_enabled) return;
|
// Stop the timer and hide if the app is closing
|
||||||
|
if (QCoreApplication::closingDown() || !main_window || main_window->isHidden()) {
|
||||||
|
update_timer.stop();
|
||||||
|
if (!this->isHidden()) this->hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_enabled) return;
|
||||||
|
|
||||||
if (UISettings::IsGamescope()) {
|
if (UISettings::IsGamescope()) {
|
||||||
bool ui_active = (QApplication::activePopupWidget() != nullptr);
|
bool ui_active = (QApplication::activePopupWidget() != nullptr);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
@@ -45,10 +46,10 @@ VramOverlay::VramOverlay(QWidget* parent) : QWidget(UISettings::IsGamescope() ?
|
|||||||
|
|
||||||
// Branching Typography and Sizing
|
// Branching Typography and Sizing
|
||||||
if (UISettings::IsGamescope()) {
|
if (UISettings::IsGamescope()) {
|
||||||
title_font = QFont(QString::fromUtf8("Segoe UI"), 7, QFont::Bold);
|
title_font = QFont(QString::fromUtf8("Segoe UI"), 8, QFont::Bold);
|
||||||
value_font = QFont(QString::fromUtf8("Segoe UI"), 7, QFont::Medium);
|
value_font = QFont(QString::fromUtf8("Segoe UI"), 8, QFont::Medium);
|
||||||
small_font = QFont(QString::fromUtf8("Segoe UI"), 6, QFont::Normal);
|
small_font = QFont(QString::fromUtf8("Segoe UI"), 7, QFont::Normal);
|
||||||
warning_font = QFont(QString::fromUtf8("Segoe UI"), 8, QFont::Bold);
|
warning_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Bold);
|
||||||
setMinimumSize(180, 140);
|
setMinimumSize(180, 140);
|
||||||
resize(200, 160);
|
resize(200, 160);
|
||||||
} else {
|
} else {
|
||||||
@@ -81,7 +82,9 @@ VramOverlay::VramOverlay(QWidget* parent) : QWidget(UISettings::IsGamescope() ?
|
|||||||
UpdatePosition();
|
UpdatePosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
VramOverlay::~VramOverlay() = default;
|
VramOverlay::~VramOverlay() {
|
||||||
|
update_timer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
void VramOverlay::SetVisible(bool visible) {
|
void VramOverlay::SetVisible(bool visible) {
|
||||||
is_enabled = visible;
|
is_enabled = visible;
|
||||||
@@ -255,7 +258,14 @@ void VramOverlay::mouseReleaseEvent(QMouseEvent* event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VramOverlay::UpdateVramStats() {
|
void VramOverlay::UpdateVramStats() {
|
||||||
if (!main_window || !is_enabled) return;
|
// Stop the timer and hide if the app is closing
|
||||||
|
if (QCoreApplication::closingDown() || !main_window || main_window->isHidden()) {
|
||||||
|
update_timer.stop();
|
||||||
|
if (!this->isHidden()) this->hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_enabled) return;
|
||||||
|
|
||||||
if (UISettings::IsGamescope()) {
|
if (UISettings::IsGamescope()) {
|
||||||
bool ui_active = (QApplication::activePopupWidget() != nullptr);
|
bool ui_active = (QApplication::activePopupWidget() != nullptr);
|
||||||
|
|||||||
@@ -1998,13 +1998,23 @@ int EmulatedController::SetCallback(ControllerUpdateCallback update_callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EmulatedController::DeleteCallback(int key) {
|
void EmulatedController::DeleteCallback(int key) {
|
||||||
std::scoped_lock lock{callback_mutex};
|
// 1. If the key is invalid, get out immediately.
|
||||||
const auto& iterator = callback_list.find(key);
|
if (key < 0) {
|
||||||
if (iterator == callback_list.end()) {
|
|
||||||
LOG_ERROR(Input, "Tried to delete non-existent callback {}", key);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
callback_list.erase(iterator);
|
|
||||||
|
std::scoped_lock lock{callback_mutex};
|
||||||
|
|
||||||
|
// 2. Check if the list itself is valid.
|
||||||
|
if (callback_list.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Use a safe find. Do NOT use the result if it's the end of the map.
|
||||||
|
auto it = callback_list.find(key);
|
||||||
|
if (it != callback_list.end()) {
|
||||||
|
callback_list.erase(it);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EmulatedController::StatusUpdate() {
|
void EmulatedController::StatusUpdate() {
|
||||||
|
|||||||
Reference in New Issue
Block a user