mirror of
https://git.citron-emu.org/citron/emulator
synced 2026-01-11 04:04:16 +00:00
- Fix memory leak in PopulateGridView() and FilterGridView() by properly deleting old QStandardItemModel instances before creating new ones - Add safety checks in controller navigation to prevent crashes when accessing uninitialized controller data - Improve controller input handling to only send events to visible and properly initialized views - Add null pointer checks in SetViewMode() to prevent crashes when setting current index on empty models - Add proper cleanup in GameList destructor to prevent memory leaks on application shutdown The main issue was that switching between list and grid views created new models without properly cleaning up old ones, leading to memory leaks. Additionally, controller navigation would send keyboard events to both views simultaneously, causing crashes when one view was not properly initialized or visible. Fixes crashes when using controller navigation in grid view mode. Thanks to Beta Testers acarajé & Hayate Yoshida (吉田 疾風) for finding the bug. Signed-off-by: Zephyron <zephyron@citron-emu.org>
197 lines
7.2 KiB
C++
197 lines
7.2 KiB
C++
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "common/settings_input.h"
|
|
#include "hid_core/frontend/emulated_controller.h"
|
|
#include "hid_core/hid_core.h"
|
|
#include "citron/util/controller_navigation.h"
|
|
|
|
ControllerNavigation::ControllerNavigation(Core::HID::HIDCore& hid_core, QWidget* parent) {
|
|
player1_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
|
|
handheld_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
|
|
Core::HID::ControllerUpdateCallback engine_callback{
|
|
.on_change = [this](Core::HID::ControllerTriggerType type) { ControllerUpdateEvent(type); },
|
|
.is_npad_service = false,
|
|
};
|
|
player1_callback_key = player1_controller->SetCallback(engine_callback);
|
|
handheld_callback_key = handheld_controller->SetCallback(engine_callback);
|
|
is_controller_set = true;
|
|
}
|
|
|
|
ControllerNavigation::~ControllerNavigation() {
|
|
UnloadController();
|
|
}
|
|
|
|
void ControllerNavigation::UnloadController() {
|
|
if (is_controller_set) {
|
|
player1_controller->DeleteCallback(player1_callback_key);
|
|
handheld_controller->DeleteCallback(handheld_callback_key);
|
|
is_controller_set = false;
|
|
}
|
|
}
|
|
|
|
void ControllerNavigation::TriggerButton(Settings::NativeButton::Values native_button,
|
|
Qt::Key key) {
|
|
if (button_values[native_button].value && !button_values[native_button].locked) {
|
|
emit TriggerKeyboardEvent(key);
|
|
}
|
|
}
|
|
|
|
void ControllerNavigation::ControllerUpdateEvent(Core::HID::ControllerTriggerType type) {
|
|
std::scoped_lock lock{mutex};
|
|
if (!Settings::values.controller_navigation) {
|
|
return;
|
|
}
|
|
|
|
// Safety check: ensure controllers are properly initialized
|
|
if (!is_controller_set || !player1_controller || !handheld_controller) {
|
|
return;
|
|
}
|
|
|
|
if (type == Core::HID::ControllerTriggerType::Button) {
|
|
ControllerUpdateButton();
|
|
return;
|
|
}
|
|
|
|
if (type == Core::HID::ControllerTriggerType::Stick) {
|
|
ControllerUpdateStick();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void ControllerNavigation::ControllerUpdateButton() {
|
|
// Safety check: ensure controllers are properly initialized
|
|
if (!is_controller_set || !player1_controller || !handheld_controller) {
|
|
return;
|
|
}
|
|
|
|
const auto controller_type = player1_controller->GetNpadStyleIndex();
|
|
const auto& player1_buttons = player1_controller->GetButtonsValues();
|
|
const auto& handheld_buttons = handheld_controller->GetButtonsValues();
|
|
|
|
for (std::size_t i = 0; i < player1_buttons.size(); ++i) {
|
|
const bool button = player1_buttons[i].value || handheld_buttons[i].value;
|
|
// Trigger only once
|
|
button_values[i].locked = button == button_values[i].value;
|
|
button_values[i].value = button;
|
|
}
|
|
|
|
switch (controller_type) {
|
|
case Core::HID::NpadStyleIndex::Fullkey:
|
|
case Core::HID::NpadStyleIndex::JoyconDual:
|
|
case Core::HID::NpadStyleIndex::Handheld:
|
|
case Core::HID::NpadStyleIndex::GameCube:
|
|
TriggerButton(Settings::NativeButton::A, Qt::Key_Enter);
|
|
TriggerButton(Settings::NativeButton::B, Qt::Key_Escape);
|
|
TriggerButton(Settings::NativeButton::DDown, Qt::Key_Down);
|
|
TriggerButton(Settings::NativeButton::DLeft, Qt::Key_Left);
|
|
TriggerButton(Settings::NativeButton::DRight, Qt::Key_Right);
|
|
TriggerButton(Settings::NativeButton::DUp, Qt::Key_Up);
|
|
break;
|
|
case Core::HID::NpadStyleIndex::JoyconLeft:
|
|
TriggerButton(Settings::NativeButton::DDown, Qt::Key_Enter);
|
|
TriggerButton(Settings::NativeButton::DLeft, Qt::Key_Escape);
|
|
break;
|
|
case Core::HID::NpadStyleIndex::JoyconRight:
|
|
TriggerButton(Settings::NativeButton::X, Qt::Key_Enter);
|
|
TriggerButton(Settings::NativeButton::A, Qt::Key_Escape);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void ControllerNavigation::ControllerUpdateStick() {
|
|
// Safety check: ensure controllers are properly initialized
|
|
if (!is_controller_set || !player1_controller || !handheld_controller) {
|
|
return;
|
|
}
|
|
|
|
const auto controller_type = player1_controller->GetNpadStyleIndex();
|
|
const auto& player1_sticks = player1_controller->GetSticksValues();
|
|
const auto& handheld_sticks = player1_controller->GetSticksValues();
|
|
bool update = false;
|
|
|
|
for (std::size_t i = 0; i < player1_sticks.size(); ++i) {
|
|
const Common::Input::StickStatus stick{
|
|
.left = player1_sticks[i].left || handheld_sticks[i].left,
|
|
.right = player1_sticks[i].right || handheld_sticks[i].right,
|
|
.up = player1_sticks[i].up || handheld_sticks[i].up,
|
|
.down = player1_sticks[i].down || handheld_sticks[i].down,
|
|
};
|
|
// Trigger only once
|
|
if (stick.down != stick_values[i].down || stick.left != stick_values[i].left ||
|
|
stick.right != stick_values[i].right || stick.up != stick_values[i].up) {
|
|
update = true;
|
|
}
|
|
stick_values[i] = stick;
|
|
}
|
|
|
|
if (!update) {
|
|
return;
|
|
}
|
|
|
|
switch (controller_type) {
|
|
case Core::HID::NpadStyleIndex::Fullkey:
|
|
case Core::HID::NpadStyleIndex::JoyconDual:
|
|
case Core::HID::NpadStyleIndex::Handheld:
|
|
case Core::HID::NpadStyleIndex::GameCube:
|
|
if (stick_values[Settings::NativeAnalog::LStick].down) {
|
|
emit TriggerKeyboardEvent(Qt::Key_Down);
|
|
return;
|
|
}
|
|
if (stick_values[Settings::NativeAnalog::LStick].left) {
|
|
emit TriggerKeyboardEvent(Qt::Key_Left);
|
|
return;
|
|
}
|
|
if (stick_values[Settings::NativeAnalog::LStick].right) {
|
|
emit TriggerKeyboardEvent(Qt::Key_Right);
|
|
return;
|
|
}
|
|
if (stick_values[Settings::NativeAnalog::LStick].up) {
|
|
emit TriggerKeyboardEvent(Qt::Key_Up);
|
|
return;
|
|
}
|
|
break;
|
|
case Core::HID::NpadStyleIndex::JoyconLeft:
|
|
if (stick_values[Settings::NativeAnalog::LStick].left) {
|
|
emit TriggerKeyboardEvent(Qt::Key_Down);
|
|
return;
|
|
}
|
|
if (stick_values[Settings::NativeAnalog::LStick].up) {
|
|
emit TriggerKeyboardEvent(Qt::Key_Left);
|
|
return;
|
|
}
|
|
if (stick_values[Settings::NativeAnalog::LStick].down) {
|
|
emit TriggerKeyboardEvent(Qt::Key_Right);
|
|
return;
|
|
}
|
|
if (stick_values[Settings::NativeAnalog::LStick].right) {
|
|
emit TriggerKeyboardEvent(Qt::Key_Up);
|
|
return;
|
|
}
|
|
break;
|
|
case Core::HID::NpadStyleIndex::JoyconRight:
|
|
if (stick_values[Settings::NativeAnalog::RStick].right) {
|
|
emit TriggerKeyboardEvent(Qt::Key_Down);
|
|
return;
|
|
}
|
|
if (stick_values[Settings::NativeAnalog::RStick].down) {
|
|
emit TriggerKeyboardEvent(Qt::Key_Left);
|
|
return;
|
|
}
|
|
if (stick_values[Settings::NativeAnalog::RStick].up) {
|
|
emit TriggerKeyboardEvent(Qt::Key_Right);
|
|
return;
|
|
}
|
|
if (stick_values[Settings::NativeAnalog::RStick].left) {
|
|
emit TriggerKeyboardEvent(Qt::Key_Up);
|
|
return;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|